Feature/storybook (#10797)
Squash commit of storybook addition to aid design system documentation efforts. The older commit titles were: * Initial storybook commit * Fix documentation.json links * Don't track documentation.json in git * Enable sass in storybook * Initial version of a story that uses angular components * Remove example stories, clean up button story * More example stories * Fix sb build * Always use dev * Try without auth header * Update workflow name * More logs * Check if token set * Use release/storybook branch for testing * Send ref input to workflow * Escape input to curl call * Adding logging * Different type of escaping * Fix JOSN * Use dev branch for opf/design-system storybook publishing * Add plugin to message path to parent window * Remove extraneous story * Add a ton of docs * Update stories * Fix syntax error caused by multiple newlines inside of a JSX component * Add text-field story * Add basic html stories that don't work yet * Try to get plain HTML examples working * HTML Examples work, but slowly revert to components anyway * Fix HTML examples * Remove extraneous files * Put storybook eslint rules back in * Improve docs * Show docs tab by default * Add pullpreview for storybook * Use the same pullpreview tag for both storybook and normal deployments * Change name of second pullpreview workflow * Pin node version to 16.17.0 * Initial update to docs Added/updated: Foundation pages: - Colours (major update) - Shadows (minor) - Typography (new) Blocks - Checkbox (minor) - Action bar (major) - Buttons (new) - Link (major) - Modal Dialogue (new) - Selector Field (new) * Make sure all code is available during storybook pp build * Change storybook pullpreview file name * Add production target to docker-compose sb pp * Fix acme check for sb pp * Only run cd-storybook on dev branch * Run without https on 8080 * Added intro and new page - Introduction renamed to "Design System", page rewritten completely - Added page "Devices and Accessibility" * Remove domain from caddy * Add port to listen command * Remove double pullpreview workflows * Added Divider component * Change sorting of stories * Update section titles and order for styles and blocks * add extra action bar story * Updated organising + new page - Updated organisation into Styles, Components and Patterns. - Added page "Using Storybook" (mostly a skeleton for now) * Added note about colours not being implemented yet * Minor Co-authored-by: Parimal Satyal <88370597+psatyal@users.noreply.github.com>pull/11308/head
parent
44ceb9fbea
commit
a9e29279ea
@ -0,0 +1,26 @@ |
|||||||
|
name: cd-storybook |
||||||
|
on: |
||||||
|
push: |
||||||
|
branches: |
||||||
|
- dev |
||||||
|
|
||||||
|
permissions: |
||||||
|
contents: read |
||||||
|
|
||||||
|
jobs: |
||||||
|
trigger_design_system_workflow: |
||||||
|
permissions: |
||||||
|
contents: none |
||||||
|
if: github.repository == 'opf/openproject' |
||||||
|
runs-on: ubuntu-latest |
||||||
|
steps: |
||||||
|
- name: Trigger downstream workflow |
||||||
|
env: |
||||||
|
TOKEN: ${{ secrets.OPENPROJECT_CI_TOKEN }} |
||||||
|
DS_CD_WORKFLOW_ID: build-docs.yml |
||||||
|
DS_REPOSITORY: opf/design-system |
||||||
|
run: | |
||||||
|
curl -i --fail -H"authorization: Bearer $TOKEN" \ |
||||||
|
-XPOST -H"Accept: application/vnd.github.v3+json" \ |
||||||
|
https://api.github.com/repos/$DS_REPOSITORY/actions/workflows/$DS_CD_WORKFLOW_ID/dispatches \ |
||||||
|
-d '{ "ref": "dev", "inputs": { "ref": "${{ github.ref }}" }}' |
@ -0,0 +1,21 @@ |
|||||||
|
FROM node:16.17 as build |
||||||
|
|
||||||
|
COPY . /build |
||||||
|
WORKDIR /build/frontend |
||||||
|
RUN npm ci |
||||||
|
RUN touch ./src/app/features/plugins/linked-plugins.styles.sass |
||||||
|
RUN cp ./src/app/features/plugins/linked-plugins.module.ts.example ./src/app/features/plugins/linked-plugins.module.ts |
||||||
|
RUN npm run storybook:build |
||||||
|
|
||||||
|
FROM caddy:2-alpine as prod |
||||||
|
|
||||||
|
ARG DOMAIN=my.pullpreview.com |
||||||
|
ENV DOMAIN=$DOMAIN |
||||||
|
|
||||||
|
RUN mkdir -p /srv |
||||||
|
COPY --from=build /build/frontend/storybook-static /srv/storybook |
||||||
|
WORKDIR /srv/storybook |
||||||
|
CMD caddy file-server \ |
||||||
|
-root /srv/storybook \ |
||||||
|
-listen 0.0.0.0:8080 |
||||||
|
|
@ -0,0 +1,24 @@ |
|||||||
|
module.exports = { |
||||||
|
stories: [ |
||||||
|
"../src/**/*.stories.mdx", |
||||||
|
"../src/**/*.stories.@(js|jsx|ts|tsx)" |
||||||
|
], |
||||||
|
addons: [ |
||||||
|
"@storybook/addon-links", |
||||||
|
"@storybook/addon-essentials", |
||||||
|
"@storybook/addon-interactions", |
||||||
|
"@storybook/addon-docs", |
||||||
|
"@storybook/preset-scss", |
||||||
|
"storybook-addon-designs", |
||||||
|
"./plugin-iframe/src/preset.js" |
||||||
|
], |
||||||
|
framework: "@storybook/angular", |
||||||
|
core: { |
||||||
|
builder: "@storybook/builder-webpack5", |
||||||
|
disableTelemetry: true, |
||||||
|
}, |
||||||
|
features: { |
||||||
|
previewMdx2: true, |
||||||
|
modernInlineRender: true, |
||||||
|
}, |
||||||
|
}; |
@ -0,0 +1,3 @@ |
|||||||
|
module.exports = { |
||||||
|
presets: ["@babel/preset-env", "@babel/preset-react"], |
||||||
|
}; |
@ -0,0 +1,336 @@ |
|||||||
|
{ |
||||||
|
"name": "storybook-plugin-iframe", |
||||||
|
"version": "1.0.0", |
||||||
|
"lockfileVersion": 1, |
||||||
|
"requires": true, |
||||||
|
"dependencies": { |
||||||
|
"@babel/cli": { |
||||||
|
"version": "7.18.10", |
||||||
|
"resolved": "https://registry.npmjs.org/@babel/cli/-/cli-7.18.10.tgz", |
||||||
|
"integrity": "sha512-dLvWH+ZDFAkd2jPBSghrsFBuXrREvFwjpDycXbmUoeochqKYe4zNSLEJYErpLg8dvxvZYe79/MkN461XCwpnGw==", |
||||||
|
"requires": { |
||||||
|
"@jridgewell/trace-mapping": "^0.3.8", |
||||||
|
"@nicolo-ribaudo/chokidar-2": "2.1.8-no-fsevents.3", |
||||||
|
"chokidar": "^3.4.0", |
||||||
|
"commander": "^4.0.1", |
||||||
|
"convert-source-map": "^1.1.0", |
||||||
|
"fs-readdir-recursive": "^1.1.0", |
||||||
|
"glob": "^7.2.0", |
||||||
|
"make-dir": "^2.1.0", |
||||||
|
"slash": "^2.0.0" |
||||||
|
} |
||||||
|
}, |
||||||
|
"@jridgewell/resolve-uri": { |
||||||
|
"version": "3.1.0", |
||||||
|
"resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.0.tgz", |
||||||
|
"integrity": "sha512-F2msla3tad+Mfht5cJq7LSXcdudKTWCVYUgw6pLFOOHSTtZlj6SWNYAp+AhuqLmWdBO2X5hPrLcu8cVP8fy28w==" |
||||||
|
}, |
||||||
|
"@jridgewell/sourcemap-codec": { |
||||||
|
"version": "1.4.14", |
||||||
|
"resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.14.tgz", |
||||||
|
"integrity": "sha512-XPSJHWmi394fuUuzDnGz1wiKqWfo1yXecHQMRf2l6hztTO+nPru658AyDngaBe7isIxEkRsPR3FZh+s7iVa4Uw==" |
||||||
|
}, |
||||||
|
"@jridgewell/trace-mapping": { |
||||||
|
"version": "0.3.15", |
||||||
|
"resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.15.tgz", |
||||||
|
"integrity": "sha512-oWZNOULl+UbhsgB51uuZzglikfIKSUBO/M9W2OfEjn7cmqoAiCgmv9lyACTUacZwBz0ITnJ2NqjU8Tx0DHL88g==", |
||||||
|
"requires": { |
||||||
|
"@jridgewell/resolve-uri": "^3.0.3", |
||||||
|
"@jridgewell/sourcemap-codec": "^1.4.10" |
||||||
|
} |
||||||
|
}, |
||||||
|
"@nicolo-ribaudo/chokidar-2": { |
||||||
|
"version": "2.1.8-no-fsevents.3", |
||||||
|
"resolved": "https://registry.npmjs.org/@nicolo-ribaudo/chokidar-2/-/chokidar-2-2.1.8-no-fsevents.3.tgz", |
||||||
|
"integrity": "sha512-s88O1aVtXftvp5bCPB7WnmXc5IwOZZ7YPuwNPt+GtOOXpPvad1LfbmjYv+qII7zP6RU2QGnqve27dnLycEnyEQ==", |
||||||
|
"optional": true |
||||||
|
}, |
||||||
|
"anymatch": { |
||||||
|
"version": "3.1.2", |
||||||
|
"resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.2.tgz", |
||||||
|
"integrity": "sha512-P43ePfOAIupkguHUycrc4qJ9kz8ZiuOUijaETwX7THt0Y/GNK7v0aa8rY816xWjZ7rJdA5XdMcpVFTKMq+RvWg==", |
||||||
|
"optional": true, |
||||||
|
"requires": { |
||||||
|
"normalize-path": "^3.0.0", |
||||||
|
"picomatch": "^2.0.4" |
||||||
|
} |
||||||
|
}, |
||||||
|
"balanced-match": { |
||||||
|
"version": "1.0.2", |
||||||
|
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", |
||||||
|
"integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==" |
||||||
|
}, |
||||||
|
"binary-extensions": { |
||||||
|
"version": "2.2.0", |
||||||
|
"resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.2.0.tgz", |
||||||
|
"integrity": "sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==", |
||||||
|
"optional": true |
||||||
|
}, |
||||||
|
"brace-expansion": { |
||||||
|
"version": "1.1.11", |
||||||
|
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", |
||||||
|
"integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", |
||||||
|
"requires": { |
||||||
|
"balanced-match": "^1.0.0", |
||||||
|
"concat-map": "0.0.1" |
||||||
|
} |
||||||
|
}, |
||||||
|
"braces": { |
||||||
|
"version": "3.0.2", |
||||||
|
"resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", |
||||||
|
"integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", |
||||||
|
"optional": true, |
||||||
|
"requires": { |
||||||
|
"fill-range": "^7.0.1" |
||||||
|
} |
||||||
|
}, |
||||||
|
"chokidar": { |
||||||
|
"version": "3.5.3", |
||||||
|
"resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.3.tgz", |
||||||
|
"integrity": "sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw==", |
||||||
|
"optional": true, |
||||||
|
"requires": { |
||||||
|
"anymatch": "~3.1.2", |
||||||
|
"braces": "~3.0.2", |
||||||
|
"fsevents": "~2.3.2", |
||||||
|
"glob-parent": "~5.1.2", |
||||||
|
"is-binary-path": "~2.1.0", |
||||||
|
"is-glob": "~4.0.1", |
||||||
|
"normalize-path": "~3.0.0", |
||||||
|
"readdirp": "~3.6.0" |
||||||
|
} |
||||||
|
}, |
||||||
|
"commander": { |
||||||
|
"version": "4.1.1", |
||||||
|
"resolved": "https://registry.npmjs.org/commander/-/commander-4.1.1.tgz", |
||||||
|
"integrity": "sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==" |
||||||
|
}, |
||||||
|
"concat-map": { |
||||||
|
"version": "0.0.1", |
||||||
|
"resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", |
||||||
|
"integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==" |
||||||
|
}, |
||||||
|
"convert-source-map": { |
||||||
|
"version": "1.8.0", |
||||||
|
"resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.8.0.tgz", |
||||||
|
"integrity": "sha512-+OQdjP49zViI/6i7nIJpA8rAl4sV/JdPfU9nZs3VqOwGIgizICvuN2ru6fMd+4llL0tar18UYJXfZ/TWtmhUjA==", |
||||||
|
"requires": { |
||||||
|
"safe-buffer": "~5.1.1" |
||||||
|
} |
||||||
|
}, |
||||||
|
"fill-range": { |
||||||
|
"version": "7.0.1", |
||||||
|
"resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", |
||||||
|
"integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", |
||||||
|
"optional": true, |
||||||
|
"requires": { |
||||||
|
"to-regex-range": "^5.0.1" |
||||||
|
} |
||||||
|
}, |
||||||
|
"fs-readdir-recursive": { |
||||||
|
"version": "1.1.0", |
||||||
|
"resolved": "https://registry.npmjs.org/fs-readdir-recursive/-/fs-readdir-recursive-1.1.0.tgz", |
||||||
|
"integrity": "sha512-GNanXlVr2pf02+sPN40XN8HG+ePaNcvM0q5mZBd668Obwb0yD5GiUbZOFgwn8kGMY6I3mdyDJzieUy3PTYyTRA==" |
||||||
|
}, |
||||||
|
"fs.realpath": { |
||||||
|
"version": "1.0.0", |
||||||
|
"resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", |
||||||
|
"integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==" |
||||||
|
}, |
||||||
|
"fsevents": { |
||||||
|
"version": "2.3.2", |
||||||
|
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", |
||||||
|
"integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", |
||||||
|
"optional": true |
||||||
|
}, |
||||||
|
"glob": { |
||||||
|
"version": "7.2.3", |
||||||
|
"resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", |
||||||
|
"integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", |
||||||
|
"requires": { |
||||||
|
"fs.realpath": "^1.0.0", |
||||||
|
"inflight": "^1.0.4", |
||||||
|
"inherits": "2", |
||||||
|
"minimatch": "^3.1.1", |
||||||
|
"once": "^1.3.0", |
||||||
|
"path-is-absolute": "^1.0.0" |
||||||
|
} |
||||||
|
}, |
||||||
|
"glob-parent": { |
||||||
|
"version": "5.1.2", |
||||||
|
"resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", |
||||||
|
"integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", |
||||||
|
"optional": true, |
||||||
|
"requires": { |
||||||
|
"is-glob": "^4.0.1" |
||||||
|
} |
||||||
|
}, |
||||||
|
"inflight": { |
||||||
|
"version": "1.0.6", |
||||||
|
"resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", |
||||||
|
"integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", |
||||||
|
"requires": { |
||||||
|
"once": "^1.3.0", |
||||||
|
"wrappy": "1" |
||||||
|
} |
||||||
|
}, |
||||||
|
"inherits": { |
||||||
|
"version": "2.0.4", |
||||||
|
"resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", |
||||||
|
"integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" |
||||||
|
}, |
||||||
|
"is-binary-path": { |
||||||
|
"version": "2.1.0", |
||||||
|
"resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", |
||||||
|
"integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", |
||||||
|
"optional": true, |
||||||
|
"requires": { |
||||||
|
"binary-extensions": "^2.0.0" |
||||||
|
} |
||||||
|
}, |
||||||
|
"is-extglob": { |
||||||
|
"version": "2.1.1", |
||||||
|
"resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", |
||||||
|
"integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", |
||||||
|
"optional": true |
||||||
|
}, |
||||||
|
"is-glob": { |
||||||
|
"version": "4.0.3", |
||||||
|
"resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", |
||||||
|
"integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", |
||||||
|
"optional": true, |
||||||
|
"requires": { |
||||||
|
"is-extglob": "^2.1.1" |
||||||
|
} |
||||||
|
}, |
||||||
|
"is-number": { |
||||||
|
"version": "7.0.0", |
||||||
|
"resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", |
||||||
|
"integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", |
||||||
|
"optional": true |
||||||
|
}, |
||||||
|
"js-tokens": { |
||||||
|
"version": "4.0.0", |
||||||
|
"resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", |
||||||
|
"integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==" |
||||||
|
}, |
||||||
|
"loose-envify": { |
||||||
|
"version": "1.4.0", |
||||||
|
"resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", |
||||||
|
"integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", |
||||||
|
"requires": { |
||||||
|
"js-tokens": "^3.0.0 || ^4.0.0" |
||||||
|
} |
||||||
|
}, |
||||||
|
"make-dir": { |
||||||
|
"version": "2.1.0", |
||||||
|
"resolved": "https://registry.npmjs.org/make-dir/-/make-dir-2.1.0.tgz", |
||||||
|
"integrity": "sha512-LS9X+dc8KLxXCb8dni79fLIIUA5VyZoyjSMCwTluaXA0o27cCK0bhXkpgw+sTXVpPy/lSO57ilRixqk0vDmtRA==", |
||||||
|
"requires": { |
||||||
|
"pify": "^4.0.1", |
||||||
|
"semver": "^5.6.0" |
||||||
|
} |
||||||
|
}, |
||||||
|
"minimatch": { |
||||||
|
"version": "3.1.2", |
||||||
|
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", |
||||||
|
"integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", |
||||||
|
"requires": { |
||||||
|
"brace-expansion": "^1.1.7" |
||||||
|
} |
||||||
|
}, |
||||||
|
"normalize-path": { |
||||||
|
"version": "3.0.0", |
||||||
|
"resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", |
||||||
|
"integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", |
||||||
|
"optional": true |
||||||
|
}, |
||||||
|
"once": { |
||||||
|
"version": "1.4.0", |
||||||
|
"resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", |
||||||
|
"integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", |
||||||
|
"requires": { |
||||||
|
"wrappy": "1" |
||||||
|
} |
||||||
|
}, |
||||||
|
"path-is-absolute": { |
||||||
|
"version": "1.0.1", |
||||||
|
"resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", |
||||||
|
"integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==" |
||||||
|
}, |
||||||
|
"picomatch": { |
||||||
|
"version": "2.3.1", |
||||||
|
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", |
||||||
|
"integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", |
||||||
|
"optional": true |
||||||
|
}, |
||||||
|
"pify": { |
||||||
|
"version": "4.0.1", |
||||||
|
"resolved": "https://registry.npmjs.org/pify/-/pify-4.0.1.tgz", |
||||||
|
"integrity": "sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g==" |
||||||
|
}, |
||||||
|
"react": { |
||||||
|
"version": "18.2.0", |
||||||
|
"resolved": "https://registry.npmjs.org/react/-/react-18.2.0.tgz", |
||||||
|
"integrity": "sha512-/3IjMdb2L9QbBdWiW5e3P2/npwMBaU9mHCSCUzNln0ZCYbcfTsGbTJrU/kGemdH2IWmB2ioZ+zkxtmq6g09fGQ==", |
||||||
|
"requires": { |
||||||
|
"loose-envify": "^1.1.0" |
||||||
|
} |
||||||
|
}, |
||||||
|
"react-dom": { |
||||||
|
"version": "18.2.0", |
||||||
|
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.2.0.tgz", |
||||||
|
"integrity": "sha512-6IMTriUmvsjHUjNtEDudZfuDQUoWXVxKHhlEGSk81n4YFS+r/Kl99wXiwlVXtPBtJenozv2P+hxDsw9eA7Xo6g==", |
||||||
|
"requires": { |
||||||
|
"loose-envify": "^1.1.0", |
||||||
|
"scheduler": "^0.23.0" |
||||||
|
} |
||||||
|
}, |
||||||
|
"readdirp": { |
||||||
|
"version": "3.6.0", |
||||||
|
"resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", |
||||||
|
"integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", |
||||||
|
"optional": true, |
||||||
|
"requires": { |
||||||
|
"picomatch": "^2.2.1" |
||||||
|
} |
||||||
|
}, |
||||||
|
"safe-buffer": { |
||||||
|
"version": "5.1.2", |
||||||
|
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", |
||||||
|
"integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" |
||||||
|
}, |
||||||
|
"scheduler": { |
||||||
|
"version": "0.23.0", |
||||||
|
"resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.0.tgz", |
||||||
|
"integrity": "sha512-CtuThmgHNg7zIZWAXi3AsyIzA3n4xx7aNyjwC2VJldO2LMVDhFK+63xGqq6CsJH4rTAt6/M+N4GhZiDYPx9eUw==", |
||||||
|
"requires": { |
||||||
|
"loose-envify": "^1.1.0" |
||||||
|
} |
||||||
|
}, |
||||||
|
"semver": { |
||||||
|
"version": "5.7.1", |
||||||
|
"resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", |
||||||
|
"integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==" |
||||||
|
}, |
||||||
|
"slash": { |
||||||
|
"version": "2.0.0", |
||||||
|
"resolved": "https://registry.npmjs.org/slash/-/slash-2.0.0.tgz", |
||||||
|
"integrity": "sha512-ZYKh3Wh2z1PpEXWr0MpSBZ0V6mZHAQfYevttO11c51CaWjGTaadiKZ+wVt1PbMlDV5qhMFslpZCemhwOK7C89A==" |
||||||
|
}, |
||||||
|
"to-regex-range": { |
||||||
|
"version": "5.0.1", |
||||||
|
"resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", |
||||||
|
"integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", |
||||||
|
"optional": true, |
||||||
|
"requires": { |
||||||
|
"is-number": "^7.0.0" |
||||||
|
} |
||||||
|
}, |
||||||
|
"wrappy": { |
||||||
|
"version": "1.0.2", |
||||||
|
"resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", |
||||||
|
"integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==" |
||||||
|
} |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,17 @@ |
|||||||
|
{ |
||||||
|
"name": "storybook-plugin-iframe", |
||||||
|
"version": "1.0.0", |
||||||
|
"description": "", |
||||||
|
"main": "index.js", |
||||||
|
"scripts": { |
||||||
|
"test": "echo \"Error: no test specified\" && exit 1", |
||||||
|
"build": "babel ./src --out-dir ./dist" |
||||||
|
}, |
||||||
|
"author": "", |
||||||
|
"license": "GPL-3.0", |
||||||
|
"dependencies": { |
||||||
|
"@babel/cli": "^7.18.10", |
||||||
|
"react": "^18.2.0", |
||||||
|
"react-dom": "^18.2.0" |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,5 @@ |
|||||||
|
function managerEntries(entry = []) { |
||||||
|
return [...entry, require.resolve("./register")]; //👈 Addon implementation
|
||||||
|
} |
||||||
|
|
||||||
|
module.exports = { managerEntries } |
@ -0,0 +1,16 @@ |
|||||||
|
import { addons } from '@storybook/addons'; |
||||||
|
|
||||||
|
const ADDON_ID = 'iframe'; |
||||||
|
|
||||||
|
if (window && window.parent) { |
||||||
|
addons.register(ADDON_ID, () => { |
||||||
|
let previousLocation = window.location.toString(); |
||||||
|
document.body.addEventListener('click', () => { |
||||||
|
const newLocation = window.location.toString(); |
||||||
|
if (previousLocation !== newLocation) { |
||||||
|
window.parent.postMessage(newLocation, '*'); |
||||||
|
previousLocation = newLocation; |
||||||
|
} |
||||||
|
}); |
||||||
|
}); |
||||||
|
} |
@ -0,0 +1,38 @@ |
|||||||
|
import { setCompodocJson } from "@storybook/addon-docs/angular"; |
||||||
|
import { addParameters } from '@storybook/client-api'; |
||||||
|
import { Pan } from "hammerjs"; |
||||||
|
import docJson from "../documentation.json"; |
||||||
|
setCompodocJson(docJson); |
||||||
|
|
||||||
|
addParameters({ |
||||||
|
viewMode: 'docs', |
||||||
|
}); |
||||||
|
|
||||||
|
export const parameters = { |
||||||
|
actions: { argTypesRegex: "^on[A-Z].*" }, |
||||||
|
controls: { |
||||||
|
matchers: { |
||||||
|
color: /(background|color)$/i, |
||||||
|
date: /Date$/, |
||||||
|
}, |
||||||
|
}, |
||||||
|
docs: { inlineStories: true }, |
||||||
|
options: { |
||||||
|
storySort: { |
||||||
|
method: 'alphabetical', |
||||||
|
order: [ |
||||||
|
'Design System', |
||||||
|
'Devices and Accessibility', |
||||||
|
'Styles', |
||||||
|
[ |
||||||
|
'Typography', |
||||||
|
'Colors', |
||||||
|
'Spacings', |
||||||
|
'Shadows', |
||||||
|
], |
||||||
|
'Blocks', |
||||||
|
// TODO: Add manual sort order for components and patterns
|
||||||
|
], |
||||||
|
}, |
||||||
|
}, |
||||||
|
} |
@ -0,0 +1,21 @@ |
|||||||
|
{ |
||||||
|
"extends": "../src/tsconfig.app.json", |
||||||
|
"compilerOptions": { |
||||||
|
"types": [ |
||||||
|
"node" |
||||||
|
], |
||||||
|
"allowSyntheticDefaultImports": true |
||||||
|
}, |
||||||
|
"exclude": [ |
||||||
|
"../src/test.ts", |
||||||
|
"../src/**/*.spec.ts", |
||||||
|
"../src/**/spec/**/*.ts" |
||||||
|
], |
||||||
|
"include": [ |
||||||
|
"../src/**/*", |
||||||
|
"../projects/**/*" |
||||||
|
], |
||||||
|
"files": [ |
||||||
|
"./typings.d.ts" |
||||||
|
] |
||||||
|
} |
@ -0,0 +1,4 @@ |
|||||||
|
declare module '*.md' { |
||||||
|
const content: string; |
||||||
|
export default content; |
||||||
|
} |
File diff suppressed because it is too large
Load Diff
@ -1,191 +0,0 @@ |
|||||||
// -- copyright
|
|
||||||
// OpenProject is an open source project management software.
|
|
||||||
// Copyright (C) 2012-2022 the OpenProject GmbH
|
|
||||||
//
|
|
||||||
// This program is free software; you can redistribute it and/or
|
|
||||||
// modify it under the terms of the GNU General Public License version 3.
|
|
||||||
//
|
|
||||||
// OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
|
|
||||||
// Copyright (C) 2006-2013 Jean-Philippe Lang
|
|
||||||
// Copyright (C) 2010-2013 the ChiliProject Team
|
|
||||||
//
|
|
||||||
// This program is free software; you can redistribute it and/or
|
|
||||||
// modify it under the terms of the GNU General Public License
|
|
||||||
// as published by the Free Software Foundation; either version 2
|
|
||||||
// of the License, or (at your option) any later version.
|
|
||||||
//
|
|
||||||
// This program is distributed in the hope that it will be useful,
|
|
||||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
// GNU General Public License for more details.
|
|
||||||
//
|
|
||||||
// You should have received a copy of the GNU General Public License
|
|
||||||
// along with this program; if not, write to the Free Software
|
|
||||||
// Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
|
||||||
//
|
|
||||||
// See COPYRIGHT and LICENSE files for more details.
|
|
||||||
//++
|
|
||||||
|
|
||||||
import { |
|
||||||
ChangeDetectorRef, Component, ElementRef, Inject, OnInit, |
|
||||||
} from '@angular/core'; |
|
||||||
import { StateService } from '@uirouter/core'; |
|
||||||
import { OpModalComponent } from 'core-app/shared/components/modal/modal.component'; |
|
||||||
import { |
|
||||||
DebouncedRequestSwitchmap, |
|
||||||
errorNotificationHandler, |
|
||||||
} from 'core-app/shared/helpers/rxjs/debounced-input-switchmap'; |
|
||||||
import { OpModalLocalsMap } from 'core-app/shared/components/modal/modal.types'; |
|
||||||
import { Board } from 'core-app/features/boards/board/board'; |
|
||||||
import { BoardService } from 'core-app/features/boards/board/board.service'; |
|
||||||
import { I18nService } from 'core-app/core/i18n/i18n.service'; |
|
||||||
import { BoardActionsRegistryService } from 'core-app/features/boards/board/board-actions/board-actions-registry.service'; |
|
||||||
import { BoardActionService } from 'core-app/features/boards/board/board-actions/board-action.service'; |
|
||||||
import { trackByHref } from 'core-app/shared/helpers/angular/tracking-functions'; |
|
||||||
import { CreateAutocompleterComponent } from 'core-app/shared/components/autocompleter/create-autocompleter/create-autocompleter.component'; |
|
||||||
import { ValueOption } from 'core-app/shared/components/fields/edit/field-types/select-edit-field/select-edit-field.component'; |
|
||||||
import { OpModalLocalsToken } from 'core-app/shared/components/modal/modal.service'; |
|
||||||
import { HalResource } from 'core-app/features/hal/resources/hal-resource'; |
|
||||||
import { HalResourceNotificationService } from 'core-app/features/hal/services/hal-resource-notification.service'; |
|
||||||
|
|
||||||
@Component({ |
|
||||||
templateUrl: './add-list-modal.html', |
|
||||||
}) |
|
||||||
export class AddListModalComponent extends OpModalComponent implements OnInit { |
|
||||||
/** Keep a switchmap for search term and loading state */ |
|
||||||
public requests = new DebouncedRequestSwitchmap<string, ValueOption>( |
|
||||||
(searchTerm:string) => this.actionService.loadAvailable(this.active, searchTerm), |
|
||||||
errorNotificationHandler(this.halNotification), |
|
||||||
true, |
|
||||||
); |
|
||||||
|
|
||||||
public showClose:boolean; |
|
||||||
|
|
||||||
public confirmed = false; |
|
||||||
|
|
||||||
/** Active board */ |
|
||||||
public board:Board; |
|
||||||
|
|
||||||
/** Current active set of values */ |
|
||||||
public active:Set<string>; |
|
||||||
|
|
||||||
/** Action service used by the board */ |
|
||||||
public actionService:BoardActionService; |
|
||||||
|
|
||||||
/** The selected attribute */ |
|
||||||
public selectedAttribute:HalResource|undefined; |
|
||||||
|
|
||||||
/** avoid double click */ |
|
||||||
public inFlight = false; |
|
||||||
|
|
||||||
public trackByHref = trackByHref; |
|
||||||
|
|
||||||
/* Do not close on outside click (because the select option are appended to the body */ |
|
||||||
public closeOnOutsideClick = false; |
|
||||||
|
|
||||||
public warningText:string|undefined; |
|
||||||
|
|
||||||
public text:any = { |
|
||||||
title: this.I18n.t('js.boards.add_list'), |
|
||||||
button_add: this.I18n.t('js.button_add'), |
|
||||||
button_cancel: this.I18n.t('js.button_cancel'), |
|
||||||
close_popup: this.I18n.t('js.close_popup_title'), |
|
||||||
|
|
||||||
free_board: this.I18n.t('js.boards.board_type.free'), |
|
||||||
free_board_text: this.I18n.t('js.boards.board_type.free_text'), |
|
||||||
|
|
||||||
action_board: this.I18n.t('js.boards.board_type.action'), |
|
||||||
action_board_text: this.I18n.t('js.boards.board_type.action_text'), |
|
||||||
select_attribute: this.I18n.t('js.boards.board_type.select_attribute'), |
|
||||||
placeholder: this.I18n.t('js.placeholders.selection'), |
|
||||||
}; |
|
||||||
|
|
||||||
public referenceOutputs = { |
|
||||||
onAddNew: (value:HalResource) => this.onNewActionCreated(value), |
|
||||||
onOpen: () => this.requests.input$.next(''), |
|
||||||
onChange: (value:HalResource) => this.onModelChange(value), |
|
||||||
onAfterViewInit: (component:CreateAutocompleterComponent) => component.focusInputField(), |
|
||||||
}; |
|
||||||
|
|
||||||
/** The loaded available values */ |
|
||||||
availableValues:any; |
|
||||||
|
|
||||||
/** Whether the no results warning is displayed */ |
|
||||||
showWarning = false; |
|
||||||
|
|
||||||
constructor(readonly elementRef:ElementRef, |
|
||||||
@Inject(OpModalLocalsToken) public locals:OpModalLocalsMap, |
|
||||||
readonly cdRef:ChangeDetectorRef, |
|
||||||
readonly boardActions:BoardActionsRegistryService, |
|
||||||
readonly halNotification:HalResourceNotificationService, |
|
||||||
readonly state:StateService, |
|
||||||
readonly boardService:BoardService, |
|
||||||
readonly I18n:I18nService) { |
|
||||||
super(locals, cdRef, elementRef); |
|
||||||
} |
|
||||||
|
|
||||||
ngOnInit() { |
|
||||||
super.ngOnInit(); |
|
||||||
|
|
||||||
this.board = this.locals.board; |
|
||||||
this.active = new Set(this.locals.active as string[]); |
|
||||||
this.actionService = this.boardActions.get(this.board.actionAttribute!); |
|
||||||
|
|
||||||
this |
|
||||||
.requests |
|
||||||
.output$ |
|
||||||
.pipe( |
|
||||||
this.untilDestroyed(), |
|
||||||
) |
|
||||||
.subscribe((values:unknown[]) => { |
|
||||||
let hasMember = false; |
|
||||||
if (values.length === 0) { |
|
||||||
if (this.requests.lastRequestedValue !== undefined && this.requests.lastRequestedValue !== '') { |
|
||||||
hasMember = true; |
|
||||||
} else { |
|
||||||
hasMember = false; |
|
||||||
} |
|
||||||
} else { |
|
||||||
hasMember = false; |
|
||||||
} |
|
||||||
this.actionService |
|
||||||
.warningTextWhenNoOptionsAvailable(hasMember) |
|
||||||
.then((text) => { |
|
||||||
this.warningText = text; |
|
||||||
}) |
|
||||||
.catch(() => {}); |
|
||||||
this.availableValues = values; |
|
||||||
this.showWarning = this.requests.lastRequestedValue !== undefined && (values.length === 0); |
|
||||||
this.cdRef.detectChanges(); |
|
||||||
}); |
|
||||||
|
|
||||||
// Request an empty value to load warning early on
|
|
||||||
this.requests.input$.next(''); |
|
||||||
} |
|
||||||
|
|
||||||
onModelChange(element:HalResource) { |
|
||||||
this.selectedAttribute = element; |
|
||||||
} |
|
||||||
|
|
||||||
create() { |
|
||||||
this.inFlight = true; |
|
||||||
this.actionService |
|
||||||
.addColumnWithActionAttribute(this.board, this.selectedAttribute!) |
|
||||||
.then((board) => this.boardService.save(board).toPromise()) |
|
||||||
.then((board) => { |
|
||||||
this.inFlight = false; |
|
||||||
this.closeMe(); |
|
||||||
this.state.go('boards.partitioned.show', { board_id: board.id, isNew: true }); |
|
||||||
}) |
|
||||||
.catch(() => (this.inFlight = false)); |
|
||||||
} |
|
||||||
|
|
||||||
onNewActionCreated(newValue:HalResource) { |
|
||||||
this.selectedAttribute = newValue; |
|
||||||
this.create(); |
|
||||||
} |
|
||||||
|
|
||||||
autocompleterComponent() { |
|
||||||
return this.actionService.autocompleterComponent(); |
|
||||||
} |
|
||||||
} |
|
@ -0,0 +1,51 @@ |
|||||||
|
import { moduleMetadata } from '@storybook/angular'; |
||||||
|
import { Canvas, Meta, Story, ArgsTable } from '@storybook/addon-docs'; |
||||||
|
|
||||||
|
import { OpSpotModule } from '../../../spot.module'; |
||||||
|
import { SpotCheckboxComponent } from '../checkbox.component'; |
||||||
|
|
||||||
|
<Meta |
||||||
|
title="Components/Checkbox" |
||||||
|
component={SpotCheckboxComponent} |
||||||
|
decorators={[ |
||||||
|
moduleMetadata({ |
||||||
|
imports: [ |
||||||
|
OpSpotModule, |
||||||
|
], |
||||||
|
}), |
||||||
|
]} |
||||||
|
parameters={{ |
||||||
|
design: { |
||||||
|
type: 'figma', |
||||||
|
url: 'https://www.figma.com/file/XhCsrvs6rePifqbBpKYRWD/Components-Library?node-id=1785%3A6910', |
||||||
|
}, |
||||||
|
}} |
||||||
|
/> |
||||||
|
|
||||||
|
# Checkboxes |
||||||
|
|
||||||
|
This component describes only the actual checkbox, without the label. For the full component, please refer to Selector field component, which provides a label. |
||||||
|
|
||||||
|
## States |
||||||
|
|
||||||
|
The selector field itself only has two states, *enabled* and *disabled*. |
||||||
|
|
||||||
|
export const HTML = require('!!raw-loader!./CheckboxHTML.html').default; |
||||||
|
|
||||||
|
<Canvas> |
||||||
|
<Story name="HTML Template"> |
||||||
|
{{ template: HTML }} |
||||||
|
</Story> |
||||||
|
</Canvas> |
||||||
|
|
||||||
|
<ArgsTable of={SpotCheckboxComponent} /> |
||||||
|
|
||||||
|
export const Angular = ({ checked, disabled }) => ({ |
||||||
|
props: { checked, disabled }, |
||||||
|
}); |
||||||
|
|
||||||
|
<Canvas> |
||||||
|
<Story name="Angular component"> |
||||||
|
{Angular.bind({ checked: true, disabled: false })} |
||||||
|
</Story> |
||||||
|
</Canvas> |
@ -0,0 +1,8 @@ |
|||||||
|
<label> |
||||||
|
<spot-checkbox |
||||||
|
[name]="name" |
||||||
|
[disabled]="disabled" |
||||||
|
[checked]="checked" |
||||||
|
(change)="checkedChange" |
||||||
|
></spot-checkbox> |
||||||
|
</label> |
@ -0,0 +1,19 @@ |
|||||||
|
import { |
||||||
|
Component, |
||||||
|
EventEmitter, |
||||||
|
Input, |
||||||
|
Output, |
||||||
|
} from '@angular/core'; |
||||||
|
|
||||||
|
@Component({ |
||||||
|
templateUrl: './CheckboxAngular.html', |
||||||
|
}) |
||||||
|
export class CheckboxAngularStoryComponent { |
||||||
|
@Input() disabled = false; |
||||||
|
|
||||||
|
@Input() name = `spot-checkbox-${+(new Date())}`; |
||||||
|
|
||||||
|
@Input() public checked = false; |
||||||
|
|
||||||
|
@Output() checkedChange = new EventEmitter<boolean>(); |
||||||
|
} |
@ -0,0 +1,7 @@ |
|||||||
|
<label class="spot-checkbox"> |
||||||
|
<input |
||||||
|
type="checkbox" |
||||||
|
class="spot-checkbox--input" |
||||||
|
/> |
||||||
|
<span class="spot-checkbox--fake"></span> |
||||||
|
</label> |
@ -0,0 +1,45 @@ |
|||||||
|
import { moduleMetadata } from '@storybook/angular'; |
||||||
|
import { Canvas, Meta, Story, ArgsTable } from '@storybook/addon-docs'; |
||||||
|
import { I18nShim } from 'test/i18n-shim'; |
||||||
|
|
||||||
|
import { OpSpotModule } from '../../../spot.module'; |
||||||
|
import { SpotDropModalComponent } from '../drop-modal.component'; |
||||||
|
import { SbDropModalListComponent } from './DropModalList.component'; |
||||||
|
|
||||||
|
export const dummy = (() => window.I18n = new I18nShim())(); |
||||||
|
|
||||||
|
<Meta |
||||||
|
title="Patterns/DropModal" |
||||||
|
component={SpotDropModalComponent} |
||||||
|
decorators={[ |
||||||
|
moduleMetadata({ |
||||||
|
imports: [ |
||||||
|
OpSpotModule, |
||||||
|
], |
||||||
|
}), |
||||||
|
]} |
||||||
|
parameters={{ |
||||||
|
design: { |
||||||
|
type: 'figma', |
||||||
|
url: 'https://www.figma.com/file/XhCsrvs6rePifqbBpKYRWD/Components-Library?node-id=917%3A7920', |
||||||
|
}, |
||||||
|
}} |
||||||
|
/> |
||||||
|
|
||||||
|
# Drop Modal |
||||||
|
|
||||||
|
<Canvas> |
||||||
|
<Story |
||||||
|
name="Angular component" |
||||||
|
parameters={{ |
||||||
|
layout: 'centered', |
||||||
|
}} |
||||||
|
> |
||||||
|
{{ |
||||||
|
component: SbDropModalListComponent, |
||||||
|
}} |
||||||
|
</Story> |
||||||
|
</Canvas> |
||||||
|
|
||||||
|
<ArgsTable of={SpotDropModalComponent} /> |
||||||
|
|
@ -0,0 +1,46 @@ |
|||||||
|
<spot-drop-modal |
||||||
|
[open]="dropModalOpen" |
||||||
|
(closed)="close()" |
||||||
|
[alignment]="alignment" |
||||||
|
class="op-project-list-modal" |
||||||
|
data-searchable-list-parent="true" |
||||||
|
> |
||||||
|
<button |
||||||
|
aria-haspopup="true" |
||||||
|
type="button" |
||||||
|
slot="trigger" |
||||||
|
(click)="toggleDropModal()" |
||||||
|
> |
||||||
|
Open drop-modal |
||||||
|
</button> |
||||||
|
|
||||||
|
<ng-container slot="body"> |
||||||
|
<div class="spot-container"> |
||||||
|
<ul class="spot-list"> |
||||||
|
<li class="spot-list--item"> |
||||||
|
<button type="button" class="spot-list--item-action">Random Option 1</button> |
||||||
|
</li> |
||||||
|
<li class="spot-list--item"> |
||||||
|
<button type="button" class="spot-list--item-action">Random Option 2</button> |
||||||
|
</li> |
||||||
|
<li class="spot-list--item"> |
||||||
|
<button type="button" class="spot-list--item-action">Random Option 3</button> |
||||||
|
</li> |
||||||
|
<li class="spot-list--item"> |
||||||
|
<button type="button" class="spot-list--item-action">Random Option 4</button> |
||||||
|
</li> |
||||||
|
</ul> |
||||||
|
|
||||||
|
<div class="spot-action-bar"> |
||||||
|
<div class="spot-action-bar--right"> |
||||||
|
<button |
||||||
|
class="spot-button" |
||||||
|
type="button" |
||||||
|
> |
||||||
|
Some action |
||||||
|
</button> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
</ng-container> |
||||||
|
</spot-drop-modal> |
@ -0,0 +1,33 @@ |
|||||||
|
import { |
||||||
|
Component, |
||||||
|
EventEmitter, |
||||||
|
HostBinding, |
||||||
|
Input, |
||||||
|
Output, |
||||||
|
} from '@angular/core'; |
||||||
|
import SpotDropAlignmentOption from '../../../drop-alignment-options'; |
||||||
|
|
||||||
|
@Component({ |
||||||
|
selector: 'sb-drop-modal-list', |
||||||
|
templateUrl: './DropModalList.component.html', |
||||||
|
}) |
||||||
|
export class SbDropModalListComponent { |
||||||
|
@HostBinding('class.spot-drop-modal') public className = true; |
||||||
|
|
||||||
|
@Input() public alignment:SpotDropAlignmentOption = SpotDropAlignmentOption.BottomLeft; |
||||||
|
|
||||||
|
@Input('open') public dropModalOpen = false; |
||||||
|
|
||||||
|
@Output('closed') public closed = new EventEmitter(); |
||||||
|
|
||||||
|
constructor() {} |
||||||
|
|
||||||
|
public toggleDropModal() { |
||||||
|
this.dropModalOpen = !this.dropModalOpen; |
||||||
|
} |
||||||
|
|
||||||
|
close():void { |
||||||
|
this.dropModalOpen = false; |
||||||
|
this.closed.emit(); |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,78 @@ |
|||||||
|
import { moduleMetadata } from '@storybook/angular'; |
||||||
|
import { Canvas, Meta, Story, ArgsTable } from '@storybook/addon-docs'; |
||||||
|
|
||||||
|
import { OpSpotModule } from '../../../spot.module'; |
||||||
|
import { SpotTextFieldComponent } from '../text-field.component'; |
||||||
|
|
||||||
|
<Meta |
||||||
|
title="Components/Text Field" |
||||||
|
component={SpotTextFieldComponent} |
||||||
|
decorators={[ |
||||||
|
moduleMetadata({ |
||||||
|
imports: [ |
||||||
|
OpSpotModule, |
||||||
|
], |
||||||
|
}), |
||||||
|
]} |
||||||
|
parameters={{ |
||||||
|
design: { |
||||||
|
type: 'figma', |
||||||
|
url: 'https://www.figma.com/file/XhCsrvs6rePifqbBpKYRWD/Components-Library?node-id=2066%3A8130', |
||||||
|
}, |
||||||
|
}} |
||||||
|
/> |
||||||
|
|
||||||
|
# Text Fields |
||||||
|
|
||||||
|
<Canvas> |
||||||
|
<Story |
||||||
|
name="Default" |
||||||
|
> |
||||||
|
{Template.bind({})} |
||||||
|
</Story> |
||||||
|
</Canvas> |
||||||
|
|
||||||
|
<ArgsTable of={SpotTextFieldComponent} /> |
||||||
|
|
||||||
|
export const Template = (args) => ({ |
||||||
|
parameters: { |
||||||
|
component: SpotTextFieldComponent, |
||||||
|
}, |
||||||
|
props: { |
||||||
|
value: args.value, |
||||||
|
disabled: args.disabled, |
||||||
|
name: args.name, |
||||||
|
placeholder: args.placeholder, |
||||||
|
}, |
||||||
|
}) |
||||||
|
|
||||||
|
<Canvas> |
||||||
|
<Story |
||||||
|
name="Preset value" |
||||||
|
args={{ |
||||||
|
value: 'I have a value set', |
||||||
|
}} |
||||||
|
> |
||||||
|
{Template.bind({})} |
||||||
|
</Story> |
||||||
|
</Canvas> |
||||||
|
<Canvas> |
||||||
|
<Story |
||||||
|
name="Disabled" |
||||||
|
args={{ |
||||||
|
disabled: true, |
||||||
|
}} |
||||||
|
> |
||||||
|
{Template.bind({})} |
||||||
|
</Story> |
||||||
|
</Canvas> |
||||||
|
<Canvas> |
||||||
|
<Story |
||||||
|
name="Placeholder" |
||||||
|
args={{ |
||||||
|
placeholder: 'Placeholders get cut off if they\'re too long', |
||||||
|
}} |
||||||
|
> |
||||||
|
{Template.bind({})} |
||||||
|
</Story> |
||||||
|
</Canvas> |
@ -0,0 +1,83 @@ |
|||||||
|
import { moduleMetadata } from '@storybook/angular'; |
||||||
|
import { Canvas, Meta, Story, ArgsTable } from '@storybook/addon-docs'; |
||||||
|
|
||||||
|
import { OpSpotModule } from '../../../spot.module'; |
||||||
|
import { SpotToggleComponent } from '../toggle.component'; |
||||||
|
|
||||||
|
<Meta |
||||||
|
title="Components/Toggle" |
||||||
|
component={SpotToggleComponent} |
||||||
|
decorators={[ |
||||||
|
moduleMetadata({ |
||||||
|
imports: [ |
||||||
|
OpSpotModule, |
||||||
|
], |
||||||
|
}), |
||||||
|
]} |
||||||
|
parameters={{ |
||||||
|
design: { |
||||||
|
type: 'figma', |
||||||
|
url: 'https://www.figma.com/file/XhCsrvs6rePifqbBpKYRWD/Components-Library?node-id=384%3A3399', |
||||||
|
}, |
||||||
|
}} |
||||||
|
/> |
||||||
|
|
||||||
|
# Toggles |
||||||
|
|
||||||
|
Some text here. |
||||||
|
|
||||||
|
<Canvas> |
||||||
|
<Story |
||||||
|
name="Preset value" |
||||||
|
args={{ |
||||||
|
options: [ |
||||||
|
{ value: 'first', title: 'Unread' }, |
||||||
|
{ value: 'second', title: 'All' }, |
||||||
|
], |
||||||
|
value: 'second', |
||||||
|
}} |
||||||
|
> |
||||||
|
{Template.bind({})} |
||||||
|
</Story> |
||||||
|
</Canvas> |
||||||
|
|
||||||
|
<ArgsTable of={SpotToggleComponent} /> |
||||||
|
|
||||||
|
export const Template = ({ name, value, options }) => { |
||||||
|
return { |
||||||
|
props: { name, value, options }, |
||||||
|
parameters: { |
||||||
|
layout: 'centered', |
||||||
|
}, |
||||||
|
}; |
||||||
|
}; |
||||||
|
|
||||||
|
<Canvas> |
||||||
|
<Story |
||||||
|
name="Preset value 2" |
||||||
|
args={{ |
||||||
|
options: [ |
||||||
|
{ value: 'first', title: 'First option' }, |
||||||
|
{ value: 'second', title: 'Second option' }, |
||||||
|
], |
||||||
|
value: 'second', |
||||||
|
}} |
||||||
|
> |
||||||
|
{Template.bind({})} |
||||||
|
</Story> |
||||||
|
</Canvas> |
||||||
|
<Canvas> |
||||||
|
<Story |
||||||
|
name="More than two options" |
||||||
|
args={{ |
||||||
|
options: [ |
||||||
|
{ value: 'first', title: 'First option' }, |
||||||
|
{ value: 'second', title: 'Second option' }, |
||||||
|
{ value: 'third', title: 'Third option' }, |
||||||
|
{ value: 'best', title: 'Best option' }, |
||||||
|
], |
||||||
|
}} |
||||||
|
> |
||||||
|
{Template.bind({})} |
||||||
|
</Story> |
||||||
|
</Canvas> |
@ -0,0 +1,8 @@ |
|||||||
|
<spot-tooltip |
||||||
|
[disabled]="disabled" |
||||||
|
[dark]="dark" |
||||||
|
[alignment]="alignment" |
||||||
|
> |
||||||
|
<div slot="trigger">Hovering the trigger slot will show the tooltip</div> |
||||||
|
<div slot="body">{{ body }}</div> |
||||||
|
</spot-tooltip> |
@ -0,0 +1,28 @@ |
|||||||
|
import { |
||||||
|
Component, |
||||||
|
Input, |
||||||
|
OnInit, |
||||||
|
} from '@angular/core'; |
||||||
|
import SpotDropAlignmentOption from '../../../drop-alignment-options'; |
||||||
|
|
||||||
|
@Component({ |
||||||
|
selector: 'sb-tooltip', |
||||||
|
templateUrl: './Tooltip.component.html', |
||||||
|
}) |
||||||
|
export class SbTooltipComponent implements OnInit { |
||||||
|
@Input() public dark = false; |
||||||
|
|
||||||
|
@Input() public disabled = false; |
||||||
|
|
||||||
|
@Input() public alignment:SpotDropAlignmentOption = SpotDropAlignmentOption.BottomCenter; |
||||||
|
|
||||||
|
@Input() public body:string = ''; |
||||||
|
|
||||||
|
get alignmentClass():string { |
||||||
|
return `spot-tooltip--body_${this.alignment}`; |
||||||
|
} |
||||||
|
|
||||||
|
ngOnInit() { |
||||||
|
console.log(this.dark, this.disabled, this.body, this.alignment); |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,53 @@ |
|||||||
|
import { moduleMetadata } from '@storybook/angular'; |
||||||
|
import { Canvas, Meta, Story } from '@storybook/addon-docs'; |
||||||
|
|
||||||
|
import { OpSpotModule } from '../../../spot.module'; |
||||||
|
import { SpotTooltipComponent } from '../tooltip.component'; |
||||||
|
import { SbTooltipComponent } from './Tooltip.component'; |
||||||
|
|
||||||
|
<Meta |
||||||
|
title="Components/Tooltip" |
||||||
|
component={SpotTooltipComponent} |
||||||
|
decorators={[ |
||||||
|
moduleMetadata({ |
||||||
|
imports: [ |
||||||
|
OpSpotModule, |
||||||
|
], |
||||||
|
}), |
||||||
|
]} |
||||||
|
parameters={{ |
||||||
|
design: { |
||||||
|
type: 'figma', |
||||||
|
url: 'https://www.figma.com/file/XhCsrvs6rePifqbBpKYRWD/Components-Library?node-id=1202%3A6802', |
||||||
|
}, |
||||||
|
}} |
||||||
|
/> |
||||||
|
|
||||||
|
## Tooltip |
||||||
|
|
||||||
|
The include projects drop modal allows users to include projects other than the current one, so that the current view (be it table view, the team planner, boards view...) also displays work packages of these included projects. |
||||||
|
|
||||||
|
export const Template = (args) => ({ |
||||||
|
parameters: { |
||||||
|
component: SbTooltipComponent, |
||||||
|
}, |
||||||
|
props: { |
||||||
|
disabled: args.disabled, |
||||||
|
dark: args.dark, |
||||||
|
alignment: args.alignment, |
||||||
|
body: args.body, |
||||||
|
}, |
||||||
|
}) |
||||||
|
|
||||||
|
<Canvas> |
||||||
|
<Story |
||||||
|
name="Default" |
||||||
|
args={{ |
||||||
|
body: 'This is a tooltip', |
||||||
|
dark: false, |
||||||
|
disabled: false, |
||||||
|
}} |
||||||
|
> |
||||||
|
{Template.bind({})} |
||||||
|
</Story> |
||||||
|
</Canvas> |
@ -0,0 +1,14 @@ |
|||||||
|
<div class="spot-action-bar"> |
||||||
|
<div class="spot-action-bar--left"> |
||||||
|
<a |
||||||
|
class="spot-link" |
||||||
|
href="#" |
||||||
|
>Some link</a> |
||||||
|
</div> |
||||||
|
<div class="spot-action-bar--right"> |
||||||
|
<button |
||||||
|
type="button" |
||||||
|
class="spot-action-bar--action button" |
||||||
|
>Some Button</button> |
||||||
|
</div> |
||||||
|
</div> |
@ -0,0 +1,6 @@ |
|||||||
|
import { Component } from '@angular/core'; |
||||||
|
|
||||||
|
@Component({ |
||||||
|
templateUrl: './ActionBar.example.html', |
||||||
|
}) |
||||||
|
export class SbActionBarExample { } |
@ -0,0 +1,18 @@ |
|||||||
|
<div class="spot-action-bar"> |
||||||
|
<div class="spot-action-bar--left"> |
||||||
|
<button |
||||||
|
type="button" |
||||||
|
class="spot-action-bar--action button" |
||||||
|
>Cancel</button> |
||||||
|
<button |
||||||
|
type="button" |
||||||
|
class="spot-action-bar--action button -highlight" |
||||||
|
>Save</button> |
||||||
|
</div> |
||||||
|
<div class="spot-action-bar--right"> |
||||||
|
<label> |
||||||
|
Remember this choice |
||||||
|
<spot-checkbox></spot-checkbox> |
||||||
|
</label> |
||||||
|
</div> |
||||||
|
</div> |
@ -0,0 +1,6 @@ |
|||||||
|
import { Component } from '@angular/core'; |
||||||
|
|
||||||
|
@Component({ |
||||||
|
templateUrl: './ActionBarLeftButtons.example.html', |
||||||
|
}) |
||||||
|
export class SbActionBarLeftButtonsExample { } |
@ -0,0 +1,129 @@ |
|||||||
|
import { Meta, Story, Canvas } from '@storybook/addon-docs'; |
||||||
|
import tokens from '../app/spot/styles/tokens/dist/tokens.json'; |
||||||
|
|
||||||
|
<Meta title="Components/Buttons" /> |
||||||
|
|
||||||
|
# Buttons |
||||||
|
|
||||||
|
> Example: White more button with right icon, Main Save button, Accent Create button with left icon, danger Delete button with icon, Disabled Save button with left icon |
||||||
|
|
||||||
|
Buttons allow users to perform a specific action related to the present context. Compared to links, these actions tend to carry some weight: confirm an action, save settings, delete something, cancel an action. |
||||||
|
|
||||||
|
Buttons have a number of different options (presence of icons/text), styles (white, main, accent, danger, disabled) and states (regular, hover, clicked) that are described below. |
||||||
|
|
||||||
|
Buttons can be used in toolbars, in action bars, in button sets on pages, at the end of a settings page and in modals. They can also be used in combination with links to give certain actions more prominence than others. |
||||||
|
|
||||||
|
## Behaviour |
||||||
|
|
||||||
|
Buttons react immediately on click. They may submit a form, open a link, save or change state, launch a dropdown or a modal. |
||||||
|
|
||||||
|
## Styles |
||||||
|
|
||||||
|
There are four button styles and a disabled one. |
||||||
|
|
||||||
|
**Basic** |
||||||
|
|
||||||
|
The basic style is grey by defualt. Use it for secondary actions or in a group of buttons where there are no primary actions (for exmaple, in a toolbar). |
||||||
|
|
||||||
|
> Example: Open in Nextcloud with right icon, Settings with left icon, More with right icon |
||||||
|
|
||||||
|
**Main** |
||||||
|
|
||||||
|
The main style is used to represent the primary action, like Save or Confirm. |
||||||
|
|
||||||
|
> Example: Save with left icon, More with right icon |
||||||
|
|
||||||
|
**Accent** |
||||||
|
|
||||||
|
The accent style allows an action to stand out (considerably) from the normal colour set. It should be used sparingly to draw special attention to an action that the user might otherwise not notice. |
||||||
|
|
||||||
|
> Example: Create button with left and right icon, Icon-only create button |
||||||
|
|
||||||
|
**Danger** |
||||||
|
|
||||||
|
The danger style should also be used sparingly to draw attention to actions that might be destructive, like delete. |
||||||
|
|
||||||
|
> Example: Delete button left icon |
||||||
|
|
||||||
|
**Disabled** |
||||||
|
|
||||||
|
A button can be *enabled* or *disabled*. The disabled style is the same for all of the above-described styles. |
||||||
|
|
||||||
|
> Example: Disabled Open in Nextcloud buttin with right icon, disabled Settings button with left icon, disabled More icon with right button |
||||||
|
|
||||||
|
## Options |
||||||
|
|
||||||
|
A button can optionally have: |
||||||
|
|
||||||
|
- A left-icon |
||||||
|
- A right-icon |
||||||
|
- Text |
||||||
|
|
||||||
|
These can be combined. The most common combinations are: |
||||||
|
|
||||||
|
**Text-only** |
||||||
|
|
||||||
|
> Example |
||||||
|
|
||||||
|
A button does not require any icons if the text is sufficient context. |
||||||
|
|
||||||
|
**Text with left icon** |
||||||
|
|
||||||
|
> Example |
||||||
|
|
||||||
|
The left icon provides additional context when necessary. |
||||||
|
|
||||||
|
**Text with right icon** |
||||||
|
|
||||||
|
> Example |
||||||
|
|
||||||
|
The right icon is not used as much but is available. The most common use case is to have a down-pointing arrow to signal the presence of drop-down menu, a right-pointing arrow to signal forward movement (in a multi-step process) or an ‘external’ icon to indicate that the link opens in a separate tab. |
||||||
|
|
||||||
|
**Text with both icons** |
||||||
|
|
||||||
|
> Example |
||||||
|
|
||||||
|
Using both left and right icons is less common, but can be useful in certain contexts, like when you need a left icon for context and a right icon to indicate a drop-down list. |
||||||
|
|
||||||
|
**Icon-only** |
||||||
|
|
||||||
|
> Example |
||||||
|
|
||||||
|
Some buttons are so common in OpenProject that they do not necessarily need a text label. However, these icons need an alt-text for accessibility. |
||||||
|
|
||||||
|
_Note: We discourage using icon-only buttons if the icon unfamiliar to the user, or the action is one that the user would not have previously encountered. The only exception is when space is very tight and there is immediate feedback (eg. “configure” button on a search bar)._ |
||||||
|
|
||||||
|
## States |
||||||
|
|
||||||
|
**Regular** |
||||||
|
|
||||||
|
>Example: Basic More with left icon, Main Save, Accent Create with left icon, Danger Delete with left icon |
||||||
|
|
||||||
|
**Hover** |
||||||
|
|
||||||
|
>Example: Basic More with left icon, Main Save, Accent Create with left icon, Danger Delete with left icon |
||||||
|
|
||||||
|
**Clicked** |
||||||
|
|
||||||
|
>Example: Basic More with left icon, Main Save, Accent Create with left icon, Danger Delete with left icon |
||||||
|
|
||||||
|
## Truncation/Variable width |
||||||
|
|
||||||
|
The labels on buttons should ideally be as short as possible. |
||||||
|
|
||||||
|
Nevertheless, there will be time when the width of a label will be longer than the available space. This can happen for a number of reasons: |
||||||
|
|
||||||
|
- The label being longer in another language (“Add assignee” vs “Abtretungsempfänger hinzufügen”) |
||||||
|
- Changes to the layout due to window resizing or a new pane that compresses previously wider space |
||||||
|
|
||||||
|
There are two possible solutions, depending on the context: |
||||||
|
|
||||||
|
**Truncate label within a fixed-width button** |
||||||
|
If the button has to be fixed-width, and the text is too long, the button should wrap around to fit the entire label. |
||||||
|
|
||||||
|
**Resize the button to fit the label** |
||||||
|
If space allows, the button can have a max-width setting, so that it can first stretch vertically to allow the entire label to fit in one line, failing which, it should wrap to the next line. |
||||||
|
|
||||||
|
## Margin and Spacing |
||||||
|
|
||||||
|
The width of the button is generally set by the contents (notably by the length of the text and the precense of icons). In certain situations, the button might have a fixed length. |
@ -0,0 +1,239 @@ |
|||||||
|
import { Meta, Story, Canvas } from '@storybook/addon-docs'; |
||||||
|
import tokens from '../app/spot/styles/tokens/dist/tokens.json'; |
||||||
|
|
||||||
|
export const ColorsPreview = ({ tokens }) => <div className="colors"> |
||||||
|
{Object.keys(tokens).filter(key => key.startsWith('spot-color-')).map(name => ( |
||||||
|
<div |
||||||
|
className="colors--color" |
||||||
|
key={name} |
||||||
|
> |
||||||
|
<div |
||||||
|
className="colors--preview" |
||||||
|
style={{ 'backgroundColor': tokens[name] }} |
||||||
|
/> |
||||||
|
<div className="colors--name">${name}</div> |
||||||
|
<div className="colors--value">{tokens[name]}</div> |
||||||
|
</div> |
||||||
|
))} |
||||||
|
</div>; |
||||||
|
|
||||||
|
<Meta title="Styles/Colors" /> |
||||||
|
|
||||||
|
# Colors (WIP) |
||||||
|
|
||||||
|
**Note: These colours are neither completely implemented nor used in code. This remains a work in progress.** |
||||||
|
|
||||||
|
Because OpenProject can be customised with custom colour schemes, our foundation library only describe the colour palette of the default OpenProject theme. |
||||||
|
|
||||||
|
Colours are organised in this format: **Category/_name_**. |
||||||
|
|
||||||
|
There there are six categories: |
||||||
|
|
||||||
|
- **Basic** |
||||||
|
- **Main** |
||||||
|
- **Accent** |
||||||
|
- **Danger** |
||||||
|
- **Indication** |
||||||
|
- **Feedback** |
||||||
|
|
||||||
|
## Basic |
||||||
|
|
||||||
|
The basic colour set include eight shades of gray used mostly for basic text and backgrounds. The shades do not have additional semantic significance are are simply a continuum between **Basic/_Black_** and **Basic/_White_**. |
||||||
|
|
||||||
|
The choice of which gray to use depends on the colourspace. |
||||||
|
|
||||||
|
Note: **Basic/_Gray-1_** is the default colour for "black" text. |
||||||
|
|
||||||
|
> Code example here (Black, Gray-1, Gray-2, Gray-3, Gray-4, Gray-5, Gray-6, White) |
||||||
|
|
||||||
|
## Main |
||||||
|
|
||||||
|
The **Main/** colourset is a set of three shades of the OpenProject blue. |
||||||
|
|
||||||
|
**Main/_Main_** |
||||||
|
|
||||||
|
> Example |
||||||
|
|
||||||
|
This is the primary blue used for all main interactions and action buttons, including text links. |
||||||
|
|
||||||
|
It is also used in the background of the header. |
||||||
|
|
||||||
|
**Main/_Main Dark_** |
||||||
|
|
||||||
|
> Example |
||||||
|
|
||||||
|
This darker version of the main blue is mainly used: |
||||||
|
|
||||||
|
- to communicate state information (a hover or a pressed state of a button, for example) |
||||||
|
- when the use of **Main/_Main_** does not provide enough contrast (for a border colour, for example) |
||||||
|
|
||||||
|
**Main/_Main Light_** |
||||||
|
|
||||||
|
> Example |
||||||
|
|
||||||
|
This lighter version of the main blue is used: |
||||||
|
|
||||||
|
- to communicate state information (hover on dop-down list or indicating selected toggle element, for example) |
||||||
|
- as a background colour of interactive elements (tooltip, toast or information banners) |
||||||
|
|
||||||
|
## Accent |
||||||
|
|
||||||
|
> Example |
||||||
|
|
||||||
|
The **Accent/** colourset is a set of three shades of green that is used to accent certain special functions. This colour should be used sparingly, so that when it is used, it draws the user’s eye. |
||||||
|
|
||||||
|
**Accent/_Accent_** |
||||||
|
|
||||||
|
> Example |
||||||
|
|
||||||
|
This is the primary accent green, used for example in the default state of an accent button. |
||||||
|
|
||||||
|
**Accent/_Accent Dark_** |
||||||
|
|
||||||
|
> Example |
||||||
|
|
||||||
|
This darker version is mainly used: |
||||||
|
|
||||||
|
- to communicate state information (a hover or a pressed state of a button, for example) |
||||||
|
- when the use of **Accent/Main** does not provide enough contrast (for a border colour, for example) |
||||||
|
|
||||||
|
|
||||||
|
**Accent/_Accent Light_** |
||||||
|
|
||||||
|
> Example |
||||||
|
|
||||||
|
This lighter version of the main green is used: |
||||||
|
|
||||||
|
- to communicate state information (hover on dop-down list or indicating selected toggle element, for example) |
||||||
|
- as a background colour of interactive elements (tooltip, toast or information banners) |
||||||
|
|
||||||
|
## Danger |
||||||
|
|
||||||
|
The **Danger/** colourset is a set of three shades of red used to communicate potential danger to the user, or to warn them of a problem. |
||||||
|
|
||||||
|
It should also be used to indicate potential destructive actions (actions like delete account or delete user) and exceptionally also used to indicate information (like in a notification badge) when the Indication/ colours cannot be used. |
||||||
|
|
||||||
|
**Danger/_Danger_** |
||||||
|
|
||||||
|
> Example |
||||||
|
|
||||||
|
This is the primary danger red, used for example in the default state of an accent button. |
||||||
|
|
||||||
|
**Danger/_Danger Dark_** |
||||||
|
|
||||||
|
> Example |
||||||
|
|
||||||
|
This darker version is mainly used: |
||||||
|
|
||||||
|
- to communicate state information (a hover or a pressed state of a button, for example) |
||||||
|
- when the use of **Danger/Danger** does not provide enough contrast (for a border colour, for example) |
||||||
|
|
||||||
|
**Danger/_Danger Light_** |
||||||
|
|
||||||
|
> Example |
||||||
|
|
||||||
|
This darker version of the main danger red is used: |
||||||
|
|
||||||
|
- to communicate state information (hover on dop-down list or indicating selected toggle element, for example) |
||||||
|
- as a background colour of interactive elements (tooltip, toast or information banners) |
||||||
|
|
||||||
|
## Indication |
||||||
|
|
||||||
|
Indication colours are used to indicate certain special states. |
||||||
|
|
||||||
|
**Indication/_Attention_** |
||||||
|
|
||||||
|
> Example |
||||||
|
|
||||||
|
Attention is used to draw user attention to certain updates on the screen, like in the Notification center. In certain cases, these badges can also be associated with a number. |
||||||
|
|
||||||
|
**Indication/_Flagged_** |
||||||
|
|
||||||
|
> Example |
||||||
|
|
||||||
|
This colour is used when “flagging” work packges. It is a teal colour meant to be distinct from **Indication/_Attention_**. |
||||||
|
|
||||||
|
**Indication/_Current date_** |
||||||
|
|
||||||
|
> Example |
||||||
|
|
||||||
|
This colour is used to indicate the current date (today) in a calendar. |
||||||
|
|
||||||
|
## Feedback |
||||||
|
|
||||||
|
The **Feedback/**- colors are used to communicate for user feedback: |
||||||
|
|
||||||
|
- Error |
||||||
|
- Warning |
||||||
|
- Info |
||||||
|
- Success |
||||||
|
|
||||||
|
Each of these has two versions (Dark and Light), the dark one usually used for the foreground text and icons and the light one for the background. |
||||||
|
|
||||||
|
These are typically used in toast messages. |
||||||
|
|
||||||
|
**Feedback/_Error_** |
||||||
|
|
||||||
|
> Example |
||||||
|
|
||||||
|
This colour indicates that a response that is different from what the user would have expected. The user flow is usually interrupted when they see an error of this type. |
||||||
|
|
||||||
|
**Feedback/_Warning_** |
||||||
|
|
||||||
|
> Example |
||||||
|
|
||||||
|
The warning is information that suggests something requires attention (and that could cause a problem), but that no error has been caused, and that the expected user flow can continue. |
||||||
|
|
||||||
|
**Feedback/_Info_** |
||||||
|
|
||||||
|
> Example |
||||||
|
|
||||||
|
This colour is used to indicate additional information that could be of interest to the user. (Like a toast that says “Loading...”, indicating something is happening in the background) |
||||||
|
|
||||||
|
**Feedback/_Success_** |
||||||
|
|
||||||
|
> Example |
||||||
|
|
||||||
|
This colour indicates that the requested user action was successful. It should be used sparingly and only when such a feedback is absolutely required. |
||||||
|
|
||||||
|
<ColorsPreview tokens={tokens} /> |
||||||
|
|
||||||
|
|
||||||
|
<style> |
||||||
|
{` |
||||||
|
.colors { |
||||||
|
display: flex; |
||||||
|
flex-wrap: wrap; |
||||||
|
margin-right: -1rem; |
||||||
|
} |
||||||
|
|
||||||
|
.colors--color { |
||||||
|
border-radius: 2px; |
||||||
|
border: 1px solid #eeeeee; |
||||||
|
margin-bottom: 1rem; |
||||||
|
margin-right: 1rem; |
||||||
|
display: flex; |
||||||
|
flex-direction: column; |
||||||
|
flex-basis: 200px; |
||||||
|
flex-grow: 1; |
||||||
|
flex-shrink: 1; |
||||||
|
min-width: 150px; |
||||||
|
max-width: 400px; |
||||||
|
text-align: center; |
||||||
|
} |
||||||
|
|
||||||
|
.colors--preview { |
||||||
|
flex-basis: 200px; |
||||||
|
} |
||||||
|
|
||||||
|
.colors--name { |
||||||
|
font-weight: bold; |
||||||
|
padding: 0.3rem; |
||||||
|
} |
||||||
|
|
||||||
|
.colors--value { |
||||||
|
padding: 0.3rem; |
||||||
|
padding-top: 0rem; |
||||||
|
} |
||||||
|
`} |
||||||
|
</style> |
@ -0,0 +1,40 @@ |
|||||||
|
import { Meta, Story, Canvas } from '@storybook/addon-docs'; |
||||||
|
import tokens from '../app/spot/styles/tokens/dist/tokens.json'; |
||||||
|
|
||||||
|
<Meta title="Components/Divider" /> |
||||||
|
|
||||||
|
# Divider |
||||||
|
|
||||||
|
A divider is a non-interactive visual element that allows for better grouping, organisation and hierarchy of elements on a page or a modal. |
||||||
|
|
||||||
|
The divider should only be used when the absence of such a separation can lead to a view looking too busy or unstructured. |
||||||
|
|
||||||
|
## Behaviour |
||||||
|
|
||||||
|
The divider is not interactive. |
||||||
|
|
||||||
|
The divider can either be full-width (in the [Modal Dialogue](#) header, for example) or span only a part of the width of the parent (in the activity split screen). |
||||||
|
|
||||||
|
This is determined by the parent element within which the divider is contained. |
||||||
|
|
||||||
|
Divider are usually placed horizontally, although they can also be placed vertically. |
||||||
|
|
||||||
|
## Options |
||||||
|
|
||||||
|
A divider can be soft or strong. The correct one to use depends on the structure and contrast of surrounding elements: |
||||||
|
|
||||||
|
**Soft** |
||||||
|
|
||||||
|
1 px, Grey-5 (#E0E0E0) |
||||||
|
|
||||||
|
> Example |
||||||
|
|
||||||
|
**Strong** |
||||||
|
|
||||||
|
1px, Grey-4 (#CCCCCC) |
||||||
|
|
||||||
|
_Example: In the work package details view, the main three-way split (top header, left-side description, right-side split screen) is done using strong dividers, but on the Activity tab, dates are separated with soft ones._ |
||||||
|
|
||||||
|
## Margins and Spacing |
||||||
|
|
||||||
|
The divider itself does inherently have margin and spacing. They can however be given margins within containing elements (like a modal) if necessary. |
@ -0,0 +1,6 @@ |
|||||||
|
import { Component } from '@angular/core'; |
||||||
|
|
||||||
|
@Component({ |
||||||
|
template: '', |
||||||
|
}) |
||||||
|
export class SbEmptyComponent { } |
@ -0,0 +1,20 @@ |
|||||||
|
import { Meta } from '@storybook/addon-docs'; |
||||||
|
|
||||||
|
<Meta title="Using Storybook" /> |
||||||
|
|
||||||
|
# Using Storybook (WIP) |
||||||
|
|
||||||
|
We use [Storybook](https://storybook.js.org/) to document our design system. |
||||||
|
|
||||||
|
## Browing through the docs |
||||||
|
|
||||||
|
To be added: |
||||||
|
|
||||||
|
- How styles, components and patterns are organised |
||||||
|
- What the left-side menu includes (and does not include) |
||||||
|
- How to find linked/dependent elements |
||||||
|
|
||||||
|
## Contributing |
||||||
|
|
||||||
|
- As a developer |
||||||
|
- As a designer |
@ -0,0 +1,33 @@ |
|||||||
|
import { Meta } from '@storybook/addon-docs'; |
||||||
|
|
||||||
|
<Meta title="Design System" /> |
||||||
|
|
||||||
|
# Design System |
||||||
|
|
||||||
|
At OpenProject, we use a design system to ensure that our design delivers a consistent experience to users. The system describes the styles, components and patterns that come together to define the overall user interface. To do this, we provide documentation explaining how each element should be used, the different states, variations and options it offers, along with example implementation in Angular or Rails. |
||||||
|
|
||||||
|
Our goal is to reach a point where every view in OpenProject is built with elements described in the design system. However, we are aware that reaching this goal will take time for a tool as complex and layered as OpenProject. |
||||||
|
|
||||||
|
As such, the design system is still in its infancy and will grow with each release. |
||||||
|
|
||||||
|
## SPOT |
||||||
|
|
||||||
|
Internally, our design system is called SPOT, which stands for "Single Point of Truth". You will often see the `spot-` prefix used in code; this is primarily to distinguish newer SPOT-based components from older elements that have the `op-` prefix. |
||||||
|
|
||||||
|
## Approach |
||||||
|
|
||||||
|
OpenProject is a complex, powerful tool. One of its key strengths is its customisability and its ability to adapt to a range of different needs. This includes complex filtering options, custom types and statuses, custom fields and a wide range of options to configure views and work package forms. |
||||||
|
|
||||||
|
Nevertheless, it is very important that OpenProject be intuitive for new users who might not necessarily need that complexity, or indeed be overwhelmed by it. |
||||||
|
|
||||||
|
Our design approach aims to strike the right balance between powerful and accessible with a two-tiered approach: apply sane defaults and present the most common options, and allow advanced users the option (via an additional click) to customise and fine-tune. |
||||||
|
|
||||||
|
## The UX of Open Source |
||||||
|
|
||||||
|
As an open source project with a considerably long history and a large number of contributors, different parts of OpenProject have evolved at different paces, sometimes with completely different technology. Similar components are sometimes implemented somewhat differently in different parts of the software, and there are even multiple implementations of the same basic design. |
||||||
|
|
||||||
|
This is quite normal for a large open-source project that has not had a dedicated design team for most of its conception. |
||||||
|
|
||||||
|
One of the goals of the design system is to introduce more coherence and introduce a more modern design language. Whilst we would naturally prefer to be able to update everything at the same time and push the new design system to the entire software, we recognise the need for a more pragmatic approach. The design system will be rolled out in phases, with a careful study of the consequences of updating each component or pattern, and the potential dependencies that will be affected. |
||||||
|
|
||||||
|
We recognise that UI/UX has not always been the highest priority for open-source projects. This is somewhat understandable given how open source projects have relatively fewer design resources dedicated to it than commercial products. Our goal is to do our part to improve that situation as much as we can and document our process. |
@ -0,0 +1,4 @@ |
|||||||
|
<a |
||||||
|
href="#" |
||||||
|
class="spot-link" |
||||||
|
>This is a spot-link</a> |
@ -0,0 +1,6 @@ |
|||||||
|
import { Component } from '@angular/core'; |
||||||
|
|
||||||
|
@Component({ |
||||||
|
templateUrl: './Link.example.html', |
||||||
|
}) |
||||||
|
export class SbLinkExample { } |
@ -0,0 +1,95 @@ |
|||||||
|
import { moduleMetadata } from '@storybook/angular'; |
||||||
|
import { Canvas, Meta, Story } from '@storybook/addon-docs'; |
||||||
|
|
||||||
|
import { OpSpotModule } from '../app/spot/spot.module'; |
||||||
|
import { SbLinkExample } from './Link.example.ts'; |
||||||
|
|
||||||
|
<Meta |
||||||
|
title="Components/Link" |
||||||
|
decorators={[ |
||||||
|
moduleMetadata({ |
||||||
|
imports: [ |
||||||
|
OpSpotModule, |
||||||
|
], |
||||||
|
}), |
||||||
|
]} |
||||||
|
parameters={{ |
||||||
|
design: { |
||||||
|
type: 'figma', |
||||||
|
url: 'https://www.figma.com/file/XhCsrvs6rePifqbBpKYRWD/Components-Library?node-id=859%3A6760', |
||||||
|
}, |
||||||
|
}} |
||||||
|
/> |
||||||
|
|
||||||
|
# Links |
||||||
|
|
||||||
|
The link is used for contextual actions where a button would take too much space, or break the flow of existing content. |
||||||
|
|
||||||
|
Links are usually in-line. |
||||||
|
|
||||||
|
## Behaviour |
||||||
|
|
||||||
|
The link works like a classic HTML link. The action is triggered immediately on click. |
||||||
|
|
||||||
|
## Options |
||||||
|
|
||||||
|
A link can optionally have: |
||||||
|
|
||||||
|
- A left-icon |
||||||
|
- A right-icon |
||||||
|
- Text |
||||||
|
|
||||||
|
These can be combined. The most common combinations are: |
||||||
|
|
||||||
|
**Text only** |
||||||
|
|
||||||
|
This is the most basic link. |
||||||
|
|
||||||
|
<Canvas> |
||||||
|
<Story name="Default">{{ component: SbLinkExample }}</Story> |
||||||
|
</Canvas> |
||||||
|
|
||||||
|
**Text with left icon** |
||||||
|
|
||||||
|
The left icon provides additional context when necessary. |
||||||
|
|
||||||
|
> Example |
||||||
|
|
||||||
|
**Text with right icon** |
||||||
|
|
||||||
|
The right icon is not used as much but is available. The most common use case is to have a down-pointing arrow to signal the presence of drop-down menu, a right-pointing arrow to signal forward movement (in a multi-step process) or an ‘external’ icon to indicate that the link opens in a separate tab. |
||||||
|
|
||||||
|
> Example |
||||||
|
|
||||||
|
**Text with both icons** |
||||||
|
|
||||||
|
This is available but we discourage its use. |
||||||
|
|
||||||
|
> Example |
||||||
|
|
||||||
|
**Icon-only** |
||||||
|
|
||||||
|
This is essentially just an icon, but is offered here as a way of degrading a link with icon to just an icon when there are spatial constraints. |
||||||
|
|
||||||
|
_Note: We discourage using icon-only links if the icon unfamiliar to the user, or the action is one that the user would not have previously encountered. The only exception is when space is very tight and there is immediate feedback (eg. “configure” button on a search bar)._ |
||||||
|
|
||||||
|
## States |
||||||
|
|
||||||
|
**Regular** |
||||||
|
|
||||||
|
> Example |
||||||
|
|
||||||
|
**Hover** |
||||||
|
|
||||||
|
> Example |
||||||
|
|
||||||
|
**Clicked** |
||||||
|
|
||||||
|
> Example |
||||||
|
|
||||||
|
## Margins and Spacing |
||||||
|
|
||||||
|
Contrary to buttons, links do not inherently have margin and padding. |
||||||
|
|
||||||
|
When there are icons present, there is a 0.25 rem margin between the text and the icon. |
||||||
|
|
@ -0,0 +1,62 @@ |
|||||||
|
import { moduleMetadata } from '@storybook/angular'; |
||||||
|
import { Canvas, Meta, Story } from '@storybook/addon-docs'; |
||||||
|
|
||||||
|
import { OpSpotModule } from '../app/spot/spot.module'; |
||||||
|
import { SbEmptyComponent } from './Empty.component.ts'; |
||||||
|
import { SbListWithLinksExample } from './ListWithLinks.example.ts'; |
||||||
|
import { SbListWithCheckboxesExample } from './ListWithCheckboxes.example.ts'; |
||||||
|
import { SbListCompactExample } from './ListCompact.example.ts'; |
||||||
|
|
||||||
|
<Meta |
||||||
|
title="Components/List" |
||||||
|
decorators={[ |
||||||
|
moduleMetadata({ |
||||||
|
imports: [ |
||||||
|
OpSpotModule, |
||||||
|
], |
||||||
|
}), |
||||||
|
]} |
||||||
|
parameters={{ |
||||||
|
design: { |
||||||
|
type: 'figma', |
||||||
|
url: 'https://www.figma.com/file/XhCsrvs6rePifqbBpKYRWD/Components-Library?node-id=859%3A6760', |
||||||
|
}, |
||||||
|
}} |
||||||
|
/> |
||||||
|
|
||||||
|
# List |
||||||
|
|
||||||
|
Lists are simply a collection of components in a vertical list. They can be used inside modals like dropdowns, within a small scrollable module, or anywhere else a series of items needs to be presented. |
||||||
|
|
||||||
|
This component does not have a Figma object associated with it, since a group of elements itself is the list. This component represents lists that are interactive (checkboxes and drop down selections), or when the list is generated as a result of interaction. The items in the list are all individual components (or list primitives). |
||||||
|
|
||||||
|
This list is not to be confused with a standard HTML list element, which generates a bullet list of text. For this, a component is not needed in the Design System. |
||||||
|
|
||||||
|
<Canvas> |
||||||
|
<Story name="With Links">{{ component: SbListWithLinksExample }}</Story> |
||||||
|
</Canvas> |
||||||
|
|
||||||
|
# Behaviour |
||||||
|
|
||||||
|
Items are displayed stacked vertically. The behaviour of each individual element is inherited from properties of that element. |
||||||
|
|
||||||
|
Lists also allow nesting. Each nested item has an additional 16px padding to the left in relation to its parent. |
||||||
|
|
||||||
|
The dimensions of the list are defined by the dimensions of the containing element, and its overflow rules. |
||||||
|
|
||||||
|
# Actions |
||||||
|
|
||||||
|
List items have a primary action attached to them. This can be linking somewhere, a button listener, or acting |
||||||
|
as a label for a checkbox. |
||||||
|
|
||||||
|
<Canvas> |
||||||
|
<Story name="With Checkboxes">{{ component: SbListWithCheckboxesExample }}</Story> |
||||||
|
</Canvas> |
||||||
|
|
||||||
|
# Compact |
||||||
|
|
||||||
|
The compact version makes the items a little bit less tall. |
||||||
|
|
||||||
|
<Canvas> |
||||||
|
<Story name="Compact">{{ component: SbListCompactExample }}</Story> |
||||||
|
</Canvas> |
@ -0,0 +1,11 @@ |
|||||||
|
<ul class="spot-list spot-list_compact"> |
||||||
|
<li class="spot-list--item"> |
||||||
|
<a href="#" class="spot-list--item-action">First link</a> |
||||||
|
</li> |
||||||
|
<li class="spot-list--item"> |
||||||
|
<a href="#" class="spot-list--item-action">Second link</a> |
||||||
|
</li> |
||||||
|
<li class="spot-list--item"> |
||||||
|
<a href="#" class="spot-list--item-action">Third link</a> |
||||||
|
</li> |
||||||
|
</ul> |
@ -0,0 +1,6 @@ |
|||||||
|
import { Component } from '@angular/core'; |
||||||
|
|
||||||
|
@Component({ |
||||||
|
templateUrl: './ListCompact.example.html', |
||||||
|
}) |
||||||
|
export class SbListCompactExample { } |
@ -0,0 +1,26 @@ |
|||||||
|
<ul class="spot-list"> |
||||||
|
<li class="spot-list--item"> |
||||||
|
<label class="spot-list--item-action"> |
||||||
|
<spot-checkbox [tabindex]="-1"></spot-checkbox> |
||||||
|
<div class="spot-list--item-title"> |
||||||
|
First checky |
||||||
|
</div> |
||||||
|
</label> |
||||||
|
</li> |
||||||
|
<li class="spot-list--item"> |
||||||
|
<label class="spot-list--item-action"> |
||||||
|
<spot-checkbox [tabindex]="-1"></spot-checkbox> |
||||||
|
<div class="spot-list--item-title"> |
||||||
|
Second checky |
||||||
|
</div> |
||||||
|
</label> |
||||||
|
</li> |
||||||
|
<li class="spot-list--item"> |
||||||
|
<label class="spot-list--item-action"> |
||||||
|
<spot-checkbox [tabindex]="-1"></spot-checkbox> |
||||||
|
<div class="spot-list--item-title"> |
||||||
|
Third checky |
||||||
|
</div> |
||||||
|
</label> |
||||||
|
</li> |
||||||
|
</ul> |
@ -0,0 +1,6 @@ |
|||||||
|
import { Component } from '@angular/core'; |
||||||
|
|
||||||
|
@Component({ |
||||||
|
templateUrl: './ListWithCheckboxes.example.html', |
||||||
|
}) |
||||||
|
export class SbListWithCheckboxesExample { } |
@ -0,0 +1,11 @@ |
|||||||
|
<ul class="spot-list"> |
||||||
|
<li class="spot-list--item"> |
||||||
|
<a href="#" class="spot-list--item-action">First link</a> |
||||||
|
</li> |
||||||
|
<li class="spot-list--item"> |
||||||
|
<a href="#" class="spot-list--item-action">Second link</a> |
||||||
|
</li> |
||||||
|
<li class="spot-list--item"> |
||||||
|
<a href="#" class="spot-list--item-action">Third link</a> |
||||||
|
</li> |
||||||
|
</ul> |
@ -0,0 +1,6 @@ |
|||||||
|
import { Component } from '@angular/core'; |
||||||
|
|
||||||
|
@Component({ |
||||||
|
templateUrl: './ListWithLinks.example.html', |
||||||
|
}) |
||||||
|
export class SbListWithLinksExample { } |
@ -0,0 +1,97 @@ |
|||||||
|
import { Meta, Story, Canvas } from '@storybook/addon-docs'; |
||||||
|
import tokens from '../app/spot/styles/tokens/dist/tokens.json'; |
||||||
|
|
||||||
|
<Meta title="Patterns/Modal Dialogue" /> |
||||||
|
|
||||||
|
# Modal dialogue |
||||||
|
|
||||||
|
> Example of modal with two buttons: Cancel and Delete (danger) |
||||||
|
|
||||||
|
The modal dialogue is used to to provide actions that require the user’s attention. They interrupt the user’s regular navigation in that they cover the screen and make interaction behind it not possible whilst it is displayed. |
||||||
|
|
||||||
|
The most common use-case are alert dialogues (before deleting a file, for example), but the modal is also used for in a number of other places (see “Where it’s used”). |
||||||
|
|
||||||
|
## Composition |
||||||
|
|
||||||
|
This pattern is composed of these components: |
||||||
|
|
||||||
|
**Modal header** |
||||||
|
This consists of the top header, with text and an optional horizontal divider. The text is ideally short and serves to clarify the context of the modal to the user. |
||||||
|
|
||||||
|
**Modal body** |
||||||
|
This part contains all the elements that form the modal: text, form elements, videos, images, work package cards. The user normally is invited to perform actions or view something. |
||||||
|
|
||||||
|
**Modal footer** |
||||||
|
The footer of a modal dialogue is always an [action bar](?path=/docs/blocks-action-bar--default-story). |
||||||
|
|
||||||
|
## Container |
||||||
|
|
||||||
|
The modal is contained in frame with a white background, rounded corners (4px). The footer, being an action bar, can either have a grey (default) or a white background. |
||||||
|
|
||||||
|
## Modal body content |
||||||
|
|
||||||
|
In its simplest form, the modal’s content is simply text (as in the example above). However, the modal can contain a range of other components, including images, video, tables, work package cards, checkbox and scrollable areas. |
||||||
|
|
||||||
|
This component does not by itself define the types of content it can contain. Some Dos and Don’ts are nevertheless to be respected. |
||||||
|
|
||||||
|
## Behaviour |
||||||
|
|
||||||
|
The modal is launched by user action and displayed in a lightbox (a semi-transparent grey background). |
||||||
|
|
||||||
|
The action bar always has only two actions, one of which is always “Cancel” (secondary) and the other one usually an confirmational action like “Apply” or “Save”. As with any action bar, a third action (usually a checkbox) can optionally be displayed on the left corner. |
||||||
|
|
||||||
|
Clicking outside the modal, in the grey area, is the equivalent of pressing the cancel button. |
||||||
|
|
||||||
|
## Dos and Dont's |
||||||
|
|
||||||
|
**Do** |
||||||
|
|
||||||
|
- Do try to keep the modal as simple as possible. The goal is to direct user attention to something specific in the context of another action or place. If a feature requires a lot of configuration options and that itself has different modes, consider a full page. |
||||||
|
|
||||||
|
**Don't** |
||||||
|
|
||||||
|
- Don’t nest modals into modals, simple drop-downs are fine. |
||||||
|
- Customise the modal with variations (like colours) unless they are absolutely necessary, In which case, the modal component can be expanded to allow for them. |
||||||
|
- Make titles very long (avoid having them span more than a line) |
||||||
|
|
||||||
|
**Margins, Padding and Styling** |
||||||
|
|
||||||
|
The container has 4px rounded corners. |
||||||
|
|
||||||
|
The modal (in desktop form) has two acceptable widths: 40 REM and 60 REM. |
||||||
|
|
||||||
|
The header has 16px padding (left and right) and a 16px top margin. |
||||||
|
|
||||||
|
Note that the optional divider in the header a 16px top margin but no additional margin/padding, and must take 100% of the width of the modal. |
||||||
|
|
||||||
|
The content area has has 16px margins all around it. |
||||||
|
The action bar has no margins relative to the container (but does have its own internal margins defined in the action bar component). |
||||||
|
|
||||||
|
## Options |
||||||
|
|
||||||
|
**Header divider** |
||||||
|
|
||||||
|
The header can have an optional divider at the bottom (Grey-5 #E0E0E0, 1px). |
||||||
|
|
||||||
|
**Header icon** |
||||||
|
|
||||||
|
The header can have an optional icon (24px square) on the left side. |
||||||
|
|
||||||
|
## Where it's used |
||||||
|
|
||||||
|
- Filepicker / location picker |
||||||
|
- Alert dialogue |
||||||
|
- Delete file links |
||||||
|
- Remove file |
||||||
|
- Work package deletion |
||||||
|
- Invite user |
||||||
|
- Log time |
||||||
|
- Add widget |
||||||
|
- New Board |
||||||
|
- Work package table settings |
||||||
|
- Help text |
||||||
|
- Help > Introduction video |
||||||
|
- My account > Two-factor authentication > Backup codes password |
||||||
|
- My account > Two-factor authentication > Delete authentication |
||||||
|
- Administration > Attribute help texts > Preview help text |
||||||
|
- Administration > Enterprise Edition > Delete token |
@ -0,0 +1,53 @@ |
|||||||
|
import { moduleMetadata } from '@storybook/angular'; |
||||||
|
import { Canvas, Meta, Story } from '@storybook/addon-docs'; |
||||||
|
|
||||||
|
import { OpSpotModule } from '../app/spot/spot.module'; |
||||||
|
|
||||||
|
<Meta |
||||||
|
title="Patterns/Selector Field" |
||||||
|
decorators={[ |
||||||
|
moduleMetadata({ |
||||||
|
imports: [ |
||||||
|
OpSpotModule, |
||||||
|
], |
||||||
|
}), |
||||||
|
]} |
||||||
|
parameters={{ |
||||||
|
design: { |
||||||
|
type: 'figma', |
||||||
|
url: 'https://www.figma.com/file/XhCsrvs6rePifqbBpKYRWD/Components-Library?node-id=2316%3A8391', |
||||||
|
}, |
||||||
|
}} |
||||||
|
/> |
||||||
|
|
||||||
|
|
||||||
|
# Selector Field |
||||||
|
|
||||||
|
> Example: Checkbox with 3 items with hierarchy, showing 3 modes (mixed, checked and unchecked), and 2 states (enabled and disabled). A set of three radio buttons (unselected, selected, disabled). |
||||||
|
|
||||||
|
Selector fields are used to offer the user a number of different options. |
||||||
|
|
||||||
|
The selector field consists of either a checkbox or a radio button along with a label. |
||||||
|
|
||||||
|
Checkboxes offer the possibility to make multiple selections. |
||||||
|
Radio buttons require the user to choose only one from a list of options. |
||||||
|
|
||||||
|
## Behaviour |
||||||
|
|
||||||
|
The selector field extends the clickable zone of the checkbox or radio button to the entire label. Clicking on the label is then the same as clicking on the control element itself. |
||||||
|
|
||||||
|
If the label text is particular long, it should wrap within the container, but be top-aligned, like so: |
||||||
|
|
||||||
|
> Example: A checkbox wrapping around when the item label is very long |
||||||
|
|
||||||
|
## States |
||||||
|
|
||||||
|
The selector field itself only has two states, *enabled* and *disabled*. |
||||||
|
|
||||||
|
> Example: Checkboxes and radio buttons in both enabled and disabled states |
||||||
|
|
||||||
|
## Positioning and Margins |
||||||
|
|
||||||
|
Because the selector field itself is often placed in containers that have their own margins (like the action bar), it does not inherently have any margins of its own. |
||||||
|
|
||||||
|
Note however the 2 REM space between the element and the label. |
@ -0,0 +1,92 @@ |
|||||||
|
import { Meta, Story, Canvas } from '@storybook/addon-docs'; |
||||||
|
import tokens from '../app/spot/styles/tokens/dist/tokens.json'; |
||||||
|
import { rows, cols } from './shadows-data.jsx'; |
||||||
|
|
||||||
|
export const ShadowsTable = ({ tokens }) => (<table className="shadows"> |
||||||
|
<tbody> |
||||||
|
<tr> |
||||||
|
<td></td> |
||||||
|
{cols.map((col) => <td key={col}>{col}</td>)} |
||||||
|
</tr> |
||||||
|
{rows |
||||||
|
.map((row) => (<tr key={row}> |
||||||
|
<td>{row}</td> |
||||||
|
{cols |
||||||
|
.map((col) => `spot-shadow-${row.toLowerCase()}-${col.toLowerCase()}`) |
||||||
|
.map((name) => (<td key={name}> |
||||||
|
<div |
||||||
|
class="shadows--preview" |
||||||
|
style={{ 'boxShadow': tokens[name] }} |
||||||
|
> |
||||||
|
<div className="shadows--name">${name}</div> |
||||||
|
<div className="shadows--value">{tokens[name]}</div> |
||||||
|
</div> |
||||||
|
</td>)) |
||||||
|
} |
||||||
|
</tr>)) |
||||||
|
} |
||||||
|
</tbody> |
||||||
|
</table>); |
||||||
|
|
||||||
|
|
||||||
|
<Meta title="Styles/Shadows" /> |
||||||
|
|
||||||
|
# Shadows |
||||||
|
|
||||||
|
Shadows are important when certain components are displayed on top of other components. This is usually the case with contextual menus, drop-downs or dialogues that supplement or expand an existing view. |
||||||
|
|
||||||
|
Although it is best to avoid layering beyond two levels (a base screen + an overlay), it is sometimes necessary and indeed unavoidable. |
||||||
|
|
||||||
|
We use different shadows to communicate depth and allow the user to intuitively understand what is "on top". |
||||||
|
|
||||||
|
Our shadows definitions divided between Light and Hard and three levels of elevation. The shadow is always based on a black #000000 transparancy level, a X and Y px value and a spread px value. |
||||||
|
|
||||||
|
<ShadowsTable tokens={tokens} /> |
||||||
|
|
||||||
|
<style> |
||||||
|
{` |
||||||
|
.shadows { |
||||||
|
margin-right: -1rem; |
||||||
|
} |
||||||
|
|
||||||
|
.shadows tr, |
||||||
|
.shadows td { |
||||||
|
background: transparent !important; |
||||||
|
border: 0 !important; |
||||||
|
} |
||||||
|
|
||||||
|
.shadows td { |
||||||
|
padding: 2rem !important; |
||||||
|
text-align: center; |
||||||
|
} |
||||||
|
|
||||||
|
.shadows--spacing { |
||||||
|
margin-bottom: 1rem; |
||||||
|
margin-right: 1rem; |
||||||
|
display: flex; |
||||||
|
align-items: center; |
||||||
|
width: 100%; |
||||||
|
} |
||||||
|
|
||||||
|
.shadows--spacing > * { |
||||||
|
flex-basis: 30%; |
||||||
|
} |
||||||
|
|
||||||
|
.shadows--preview { |
||||||
|
border-radius: 3px; |
||||||
|
flex-shrink: 1; |
||||||
|
flex-basis: 100%; |
||||||
|
padding: 1rem; |
||||||
|
} |
||||||
|
|
||||||
|
.shadows--name { |
||||||
|
font-weight: bold; |
||||||
|
padding: 0.3rem; |
||||||
|
} |
||||||
|
|
||||||
|
.shadows--value { |
||||||
|
padding: 0.3rem; |
||||||
|
white-space: nowrap; |
||||||
|
} |
||||||
|
`} |
||||||
|
</style> |
@ -0,0 +1,69 @@ |
|||||||
|
import { Meta, Story, Canvas } from '@storybook/addon-docs'; |
||||||
|
import tokens from '../app/spot/styles/tokens/dist/tokens.json'; |
||||||
|
|
||||||
|
<Meta title="Styles/Spacings" /> |
||||||
|
|
||||||
|
# Spacings |
||||||
|
|
||||||
|
If I write some explanatory text around these spacings. |
||||||
|
|
||||||
|
<div className="spacings"> |
||||||
|
{Object.keys(tokens) |
||||||
|
.filter(key => key.startsWith('spot-spacing-')) |
||||||
|
.map(key => ({ |
||||||
|
i: parseFloat(key.split('-')[2].replace('_', '.'), 10), |
||||||
|
name: key, |
||||||
|
})) |
||||||
|
.sort((a, b) => a.i - b.i) |
||||||
|
.map(({ name }) => ( |
||||||
|
<div |
||||||
|
className="spacings--spacing" |
||||||
|
key={name} |
||||||
|
> |
||||||
|
<div className="spacings--name">${name}</div> |
||||||
|
<div className="spacings--value">{tokens[name]}</div> |
||||||
|
<div |
||||||
|
className="spacings--preview" |
||||||
|
style={{ 'height': tokens[name] }} |
||||||
|
/> |
||||||
|
</div> |
||||||
|
))} |
||||||
|
</div> |
||||||
|
|
||||||
|
|
||||||
|
<style> |
||||||
|
{` |
||||||
|
.spacings { |
||||||
|
display: flex; |
||||||
|
flex-wrap: wrap; |
||||||
|
margin-right: -1rem; |
||||||
|
} |
||||||
|
|
||||||
|
.spacings--spacing { |
||||||
|
margin-bottom: 1rem; |
||||||
|
margin-right: 1rem; |
||||||
|
display: flex; |
||||||
|
align-items: center; |
||||||
|
width: 100%; |
||||||
|
} |
||||||
|
|
||||||
|
.spacings--spacing > * { |
||||||
|
flex-basis: 30%; |
||||||
|
} |
||||||
|
|
||||||
|
.spacings--preview { |
||||||
|
background-color: #507193; |
||||||
|
flex-shrink: 1; |
||||||
|
flex-basis: 100%; |
||||||
|
} |
||||||
|
|
||||||
|
.spacings--name { |
||||||
|
font-weight: bold; |
||||||
|
padding: 0.3rem; |
||||||
|
} |
||||||
|
|
||||||
|
.spacings--value { |
||||||
|
padding: 0.3rem; |
||||||
|
} |
||||||
|
`} |
||||||
|
</style> |
@ -0,0 +1,96 @@ |
|||||||
|
import { Meta, Story, Canvas } from '@storybook/addon-docs'; |
||||||
|
import tokens from '../app/spot/styles/tokens/dist/tokens.json'; |
||||||
|
|
||||||
|
<Meta title="Styles/Typography" /> |
||||||
|
|
||||||
|
# Typography |
||||||
|
|
||||||
|
OpenProject uses the "[Lato Sans](https://github.com/latofonts/lato-source)" typeface created by Adam Twardoch, Botio Nikoltchev, and Łukasz Dziedzic (released with the [SIL Open Font license](https://scripts.sil.org/cms/scripts/page.php?site_id=nrsi&id=OFL)). |
||||||
|
|
||||||
|
There are 7 sizes, each with between three and eight variations. |
||||||
|
|
||||||
|
## Considerations |
||||||
|
|
||||||
|
A few things to keep in mind: |
||||||
|
|
||||||
|
### Black text |
||||||
|
|
||||||
|
“Black” text in OpenProject does not use the standard black HEX code (#000000); instead, black is defined as Grey-1 (#333333). |
||||||
|
|
||||||
|
### Line height |
||||||
|
|
||||||
|
The default line height is 16px, which corresponds to 1 REM with our Spacing baseline. |
||||||
|
|
||||||
|
For each style, line height is specified with a slash. For example, for Header Small Bold, 24/32 represents 1.5 REM font size and 2 REM line height. |
||||||
|
|
||||||
|
## Header Big |
||||||
|
|
||||||
|
This is a larger header than the default, used in rare occasions where a header is needed for a containing element to a page. To be used only if absolutely necessary. Prefer "Header small" as much as possible. |
||||||
|
|
||||||
|
So far, zero recorded use. Might get removed if this style is not used in until mid-2022. |
||||||
|
|
||||||
|
> Code example here |
||||||
|
|
||||||
|
## Header Small |
||||||
|
|
||||||
|
This is the default type used for page headers. "Bold" is default and is almost always preferred, unless there is a need to use regular to distinguish additional or supporting information. |
||||||
|
|
||||||
|
> Code example here |
||||||
|
|
||||||
|
## Subheader Big |
||||||
|
|
||||||
|
Very rarely used. Used if we need a header small than the regular header but still distinct from body text. |
||||||
|
|
||||||
|
So far, zero recorded use. Might get removed if this style is not used in until mid-2022. |
||||||
|
|
||||||
|
> Code example here |
||||||
|
|
||||||
|
## Subheader Small |
||||||
|
|
||||||
|
Very rarely used. Used if we need a header small than the regular header but still distinct from body text. |
||||||
|
|
||||||
|
> Code example here |
||||||
|
|
||||||
|
## Body Big |
||||||
|
|
||||||
|
Used occassionally (where?) |
||||||
|
|
||||||
|
> Code example here |
||||||
|
|
||||||
|
## Body Small |
||||||
|
|
||||||
|
This is the default style for most text on OpenProject. This goes for labels but also input text. By far the most used style style in the application. |
||||||
|
|
||||||
|
The regular version is used, among other places, on: |
||||||
|
- Button text |
||||||
|
- Drop down select |
||||||
|
- Text fields |
||||||
|
- Table text |
||||||
|
- Card: work package title |
||||||
|
- Sidebar element |
||||||
|
|
||||||
|
The bold version is used in: |
||||||
|
|
||||||
|
- Table headers |
||||||
|
- Calendar header |
||||||
|
- Sidebar header |
||||||
|
|
||||||
|
> Code example here |
||||||
|
|
||||||
|
## Caption |
||||||
|
|
||||||
|
The caption is used for indications and auxillery information that usually adds context to a view, but are not primary elements. They are also used on elements where space is limited (like the date information on Team planner or Board cards). |
||||||
|
|
||||||
|
The regular version is used, among other places, in: |
||||||
|
|
||||||
|
- Chips |
||||||
|
- Toast text |
||||||
|
- Tooltips |
||||||
|
- Card: project name and ID |
||||||
|
|
||||||
|
The bold version is used in: |
||||||
|
|
||||||
|
- Sidebar tabs |
||||||
|
|
||||||
|
> Code example here |
||||||
|
|
@ -0,0 +1,2 @@ |
|||||||
|
export const cols = ['Low', 'Mid', 'High']; |
||||||
|
export const rows = ['Light', 'Hard']; |
Loading…
Reference in new issue