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
Benjamin Bädorf 2 years ago committed by GitHub
parent 44ceb9fbea
commit a9e29279ea
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 26
      .github/workflows/cd-storybook.yml
  2. 3
      .github/workflows/pullpreview.yml
  3. 4
      .gitignore
  4. 14
      docker-compose.yml
  5. 2
      docker/ci/Dockerfile
  6. 2
      docker/prod/Dockerfile
  7. 21
      docker/pullpreview-storybook/Dockerfile
  8. 12
      docker/pullpreview/docker-compose.yml
  9. 8
      docs/development/development-environment-osx/README.md
  10. 2
      docs/installation-and-operations/installation/manual/README.md
  11. 1
      frontend/.eslintrc.js
  12. 24
      frontend/.storybook/main.js
  13. 3
      frontend/.storybook/plugin-iframe/.babelrc.js
  14. 336
      frontend/.storybook/plugin-iframe/package-lock.json
  15. 17
      frontend/.storybook/plugin-iframe/package.json
  16. 5
      frontend/.storybook/plugin-iframe/src/preset.js
  17. 16
      frontend/.storybook/plugin-iframe/src/register.js
  18. 38
      frontend/.storybook/preview.js
  19. 21
      frontend/.storybook/tsconfig.json
  20. 4
      frontend/.storybook/typings.d.ts
  21. 66937
      frontend/package-lock.json
  22. 26
      frontend/package.json
  23. 191
      frontend/src/app/features/boards/add-list-modal/add-list-modal.component.ts
  24. 51
      frontend/src/app/spot/components/checkbox/stories/Checkbox.stories.mdx
  25. 8
      frontend/src/app/spot/components/checkbox/stories/CheckboxAngular.html
  26. 19
      frontend/src/app/spot/components/checkbox/stories/CheckboxAngular.ts
  27. 7
      frontend/src/app/spot/components/checkbox/stories/CheckboxHTML.html
  28. 45
      frontend/src/app/spot/components/drop-modal/stories/DropModal.stories.mdx
  29. 46
      frontend/src/app/spot/components/drop-modal/stories/DropModalList.component.html
  30. 33
      frontend/src/app/spot/components/drop-modal/stories/DropModalList.component.ts
  31. 78
      frontend/src/app/spot/components/text-field/stories/TextField.stories.mdx
  32. 83
      frontend/src/app/spot/components/toggle/stories/Toggle.stories.mdx
  33. 4
      frontend/src/app/spot/components/toggle/toggle.component.ts
  34. 8
      frontend/src/app/spot/components/tooltip/stories/Tooltip.component.html
  35. 28
      frontend/src/app/spot/components/tooltip/stories/Tooltip.component.ts
  36. 53
      frontend/src/app/spot/components/tooltip/stories/Tooltip.stories.mdx
  37. 4
      frontend/src/app/spot/spot.module.ts
  38. 2
      frontend/src/app/spot/styles/sass/components/drop-modal.sass
  39. 14
      frontend/src/stories/ActionBar.example.html
  40. 6
      frontend/src/stories/ActionBar.example.ts
  41. 106
      frontend/src/stories/ActionBar.stories.mdx
  42. 18
      frontend/src/stories/ActionBarLeftButtons.example.html
  43. 6
      frontend/src/stories/ActionBarLeftButtons.example.ts
  44. 129
      frontend/src/stories/Buttons.stories.mdx
  45. 239
      frontend/src/stories/Colors.stories.mdx
  46. 40
      frontend/src/stories/Divider.stories.mdx
  47. 6
      frontend/src/stories/Empty.component.ts
  48. 20
      frontend/src/stories/HowToUse.stories.mdx
  49. 33
      frontend/src/stories/Introduction.stories.mdx
  50. 4
      frontend/src/stories/Link.example.html
  51. 6
      frontend/src/stories/Link.example.ts
  52. 95
      frontend/src/stories/Link.stories.mdx
  53. 62
      frontend/src/stories/List.stories.mdx
  54. 11
      frontend/src/stories/ListCompact.example.html
  55. 6
      frontend/src/stories/ListCompact.example.ts
  56. 26
      frontend/src/stories/ListWithCheckboxes.example.html
  57. 6
      frontend/src/stories/ListWithCheckboxes.example.ts
  58. 11
      frontend/src/stories/ListWithLinks.example.html
  59. 6
      frontend/src/stories/ListWithLinks.example.ts
  60. 27
      frontend/src/stories/Mobile-Accessibility-Localisation.stories.mdx
  61. 97
      frontend/src/stories/ModalDialogue.stories.mdx
  62. 53
      frontend/src/stories/SelectorField.stories.mdx
  63. 92
      frontend/src/stories/Shadows.stories.mdx
  64. 69
      frontend/src/stories/Spacings.stories.mdx
  65. 96
      frontend/src/stories/Typography.stories.mdx
  66. 2
      frontend/src/stories/shadows-data.jsx
  67. 1
      frontend/src/test/i18n-shim.ts
  68. 3
      frontend/src/tsconfig.app.json
  69. 48
      nix/gemset.nix
  70. 2
      package.json

@ -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 }}" }}'

@ -27,13 +27,14 @@ jobs:
run: |
cp ./docker/pullpreview/docker-compose.yml ./docker-compose.pullpreview.yml
cp ./docker/prod/Dockerfile ./Dockerfile
cp ./docker/pullpreview-storybook/Dockerfile ./Dockerfile-storybook
- uses: pullpreview/action@v5
with:
admins: crohr,HDinger,machisuji,oliverguenther,ulferts,wielinde,b12f,cbliard
always_on: dev
compose_files: docker-compose.pullpreview.yml
instance_type: large_2_0
ports: 80,443
ports: 80,443,8080
default_port: 443
env:
AWS_ACCESS_KEY_ID: "${{ secrets.AWS_ACCESS_KEY_ID }}"

4
.gitignore vendored

@ -107,8 +107,12 @@ npm-debug.log*
/frontend/npm-debug.log*
/frontend/dist/
/frontend/tests/*.gif
/frontend/storybook-static
node_modules/
# Storybook data
/frontend/documentation.json
# Ignore global package-lock.json that generates
/package-lock.json
plaintext.yml

@ -90,6 +90,17 @@ services:
depends_on:
- backend
storybook:
build:
<<: *frontend-build
command: "npm run storybook:serve"
volumes:
- ".:/home/dev/openproject"
ports:
- "6006:6006"
networks:
- network
db:
image: postgres:13
<<: *restart_policy
@ -246,3 +257,6 @@ services:
# in case we want multiple sessions per container
NODE_MAX_INSTANCES: "${CI_JOBS:-4}"
NODE_MAX_SESSION: "${CI_JOBS:-4}"
# Volumes below here

@ -1,7 +1,7 @@
FROM ruby:3.1.2-buster
MAINTAINER operations@openproject.com
ENV NODE_VERSION="16.15.1"
ENV NODE_VERSION="16.17.0"
ENV CHROME_SOURCE_URL="https://dl.google.com/dl/linux/direct/google-chrome-stable_current_amd64.deb https://openproject-public.s3.eu-central-1.amazonaws.com/binaries/google-chrome-stable_current_amd64.deb"
ENV USER=dev

@ -7,7 +7,7 @@ ARG PLATFORM=on-prem
ARG GITHUB_OAUTH_TOKEN
ARG DEBIAN_FRONTEND=noninteractive
ENV NODE_VERSION="16.15.1"
ENV NODE_VERSION="16.17.0"
ENV BUNDLER_VERSION="2.3.12"
ENV BUNDLE_PATH__SYSTEM=false
ENV APP_USER=app

@ -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

@ -26,7 +26,6 @@ x-defaults: &defaults
networks:
- backend
services:
proxy:
image: caddy:2
@ -70,3 +69,14 @@ services:
command: "./docker/prod/worker --seed --set attachment_max_size=262144,host_name=${PULLPREVIEW_PUBLIC_DNS}"
depends_on:
- db
storybook:
build:
context: .
dockerfile: Dockerfile-storybook
target: prod
restart: unless-stopped
ports:
- "8080:8080"
volumes:
- "caddy_data:/data"

@ -97,11 +97,11 @@ $ nodenv init
You can find the latest LTS version here: [nodejs.org/en/download](https://nodejs.org/en/download/)
At the time of writing this is v16.15.1. Install and activate it with:
At the time of writing this is v16.17.0. Install and activate it with:
```bash
nodenv install 16.15.1
nodenv global 16.15.1
nodenv install 16.17.0
nodenv global 16.17.0
```
### Update NPM to the latest version
@ -122,7 +122,7 @@ $ bundler --version
Bundler version 2.3.12
node --version
v16.15.1
v16.17.0
npm --version
8.12.1

@ -142,7 +142,7 @@ time to finish.
To check our Node installation we run `node --version`. It should output something very similar to:
```
v16.15.1
v16.17.0
```
## Installation of OpenProject

@ -1,6 +1,7 @@
module.exports = {
extends: [
"eslint:recommended",
"plugin:storybook/recommended",
],
env: {
browser: true,

@ -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

@ -12,9 +12,22 @@
"@angular-eslint/schematics": "12.5.0",
"@angular-eslint/template-parser": "^12.0.0",
"@angular/language-service": "12.2.6",
"@babel/core": "^7.18.5",
"@compodoc/compodoc": "^1.1.19",
"@html-eslint/eslint-plugin": "^0.11.0",
"@html-eslint/parser": "^0.11.0",
"@jsdevtools/coverage-istanbul-loader": "3.0.5",
"@storybook/addon-actions": "^6.5.10",
"@storybook/addon-essentials": "^6.5.10",
"@storybook/addon-interactions": "^6.5.10",
"@storybook/addon-knobs": "^6.4.0",
"@storybook/addon-links": "^6.5.10",
"@storybook/angular": "^6.5.10",
"@storybook/builder-webpack5": "^6.5.10",
"@storybook/manager-webpack5": "^6.5.10",
"@storybook/mdx2-csf": "^0.0.3",
"@storybook/preset-scss": "^1.0.3",
"@storybook/testing-library": "^0.0.13",
"@types/chart.js": "^2.9.20",
"@types/codemirror": "0.0.87",
"@types/dragula": "^3.7.0",
@ -33,8 +46,10 @@
"@types/webpack-env": "^1.16.0",
"@typescript-eslint/eslint-plugin": "4.23.0",
"@typescript-eslint/parser": "4.23.0",
"babel-loader": "^8.2.5",
"browserslist": "^4.9.1",
"codelyzer": "^6.0.0",
"css-loader": "^6.7.1",
"eslint": "^7.26.0",
"eslint-config-airbnb-base": "^14.2.1",
"eslint-config-airbnb-typescript": "^12.3.1",
@ -44,6 +59,7 @@
"eslint-plugin-jsx-a11y": "^6.4.1",
"eslint-plugin-react": "^7.24.0",
"eslint-plugin-react-hooks": "^4.2.0",
"eslint-plugin-storybook": "^0.6.4",
"esprint": "^3.1.0",
"jasmine-core": "~3.6.0",
"jasmine-spec-reporter": "~5.0.0",
@ -55,7 +71,12 @@
"karma-jasmine-html-reporter": "^1.5.0",
"karma-spec-reporter": "^0.0.32",
"optimist": "^0.6.1",
"raw-loader": "^4.0.2",
"sass": "^1.52.3",
"sass-loader": "^13.0.0",
"source-map-explorer": "^2.5.2",
"storybook-addon-designs": "^6.3.1",
"style-loader": "^3.3.1",
"theo": "^8.1.5",
"ts-node": "~8.3.0",
"typescript": "~4.2.4",
@ -161,6 +182,9 @@
"lint": "esprint check",
"lint:fix": "esprint check --fix",
"lint:eslint": "eslint",
"generate-typings": "tsc -d -p src/tsconfig.app.json"
"generate-typings": "tsc -d -p src/tsconfig.app.json",
"docs:json": "compodoc -p ./tsconfig.base.json -e json -d .",
"storybook:serve": "npm run docs:json && start-storybook -p 6006 --no-manager-cache",
"storybook:build": "npm run docs:json && build-storybook"
}
}

@ -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>

@ -29,7 +29,7 @@ export class SpotToggleComponent<T> implements ControlValueAccessor {
@HostBinding('class.spot-toggle') public className = true;
@Output() checkedChange = new EventEmitter<boolean>();
@Output() valueChange = new EventEmitter<T>();
@Input() options:SpotToggleOption<T>[] = [];
@ -56,7 +56,7 @@ export class SpotToggleComponent<T> implements ControlValueAccessor {
onChange = (_:T):void => {};
onTouched = (_:T):void => {};
onTouched: (t:T) => void = (_:T):void => {};
registerOnChange(fn:(_:T) => void):void {
this.onChange = fn;

@ -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>

@ -3,6 +3,7 @@ import { FormsModule } from '@angular/forms';
import { CommonModule } from '@angular/common';
import { A11yModule } from '@angular/cdk/a11y';
import { UIRouterModule } from '@uirouter/angular';
import { I18nService } from 'core-app/core/i18n/i18n.service';
import { SPOT_DOCS_ROUTES } from './spot.routes';
import { SpotCheckboxComponent } from './components/checkbox/checkbox.component';
import { SpotToggleComponent } from './components/toggle/toggle.component';
@ -19,6 +20,9 @@ import { SpotDocsComponent } from './spot-docs.component';
FormsModule,
CommonModule,
],
providers: [
I18nService,
],
declarations: [
SpotDocsComponent,

@ -10,7 +10,6 @@
opacity: 1
position: fixed
top: $spot-spacing-1
height: auto
bottom: $spot-spacing-3_5
left: $spot-spacing-1
@ -33,7 +32,6 @@
@media #{$spot-mq-drop-modal-in-context}
position: absolute
top: unset
bottom: unset
left: unset
right: unset

@ -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,106 @@
import { moduleMetadata } from '@storybook/angular';
import { Canvas, Meta, Story } from '@storybook/addon-docs';
import { OpSpotModule } from '../app/spot/spot.module';
import { SbActionBarExample } from './ActionBar.example';
import { SbActionBarLeftButtonsExample } from './ActionBarLeftButtons.example';
<Meta
title="Patterns/Action Bar"
decorators={[
moduleMetadata({
imports: [
OpSpotModule,
],
}),
]}
parameters={{
design: {
type: 'figma',
url: 'https://www.figma.com/file/XhCsrvs6rePifqbBpKYRWD/Components-Library?node-id=1788%3A6829',
},
}}
/>
# Action Bar
> Example of action bar in a modal
The action bar is generally used at the bottom of modals to present the user with a set of actions. These actions are often relative to choices or selections made in the containing modal.
The most common choices are "Save/apply" and "Cancel".  At least one button (Cancel) is the absolute minimum.
## Composition
The action bar is composed of:
**Button set**
These are a set of buttons that form the main action choices presented to the user. Although a two-button set is most common, there can be a maximum of three buttons (see “[Options](#)” below).
**Side option (optional)**
The side option allows for a third action (usually a checkbox, but can also be a button, a toggle or any other control). Ideally, the text should be clear and concise.
## Variants
<Canvas>
<Story name="Default">
{{ component: SbActionBarExample }}
</Story>
</Canvas>
By default:
- the primary button set is on the right side of the action bar
- the side option on the left
- the bar has a grey background
There are however alternative variants:
**Left buttons**
<Canvas>
<Story name="Left Buttons">
{{ component: SbActionBarLeftButtonsExample }}
</Story>
</Canvas>
The button set can be moved to the left, placing the side action on the right. This should be used sparingly and only if absolutely necessary.
**Transparent background**
> Example
The transparent background is useful for when having the action bar in grey (default) does not work visually. The transparent version will simply take the background colour of the element in which it is contained, usually white.
## Options
**Side options**
> Example without side option
The side option is, as the name suggests, optional.
**More buttons**
> Example with three buttons: Watch, Mark as read, More
The action bar can also be composed of up to three action buttons. Should there be more than three actions to be made available to the user, the third button will be a “More” button opens a drop-down with additional options.
## Behaviour
At the very minimum there should be one action, ideally two: a primary action like ‘Save’ or ‘Delete’ and a secondary action like ‘Cancel’.
When the action bar is use as a toolbar with more three buttons (or with a “More” button), all actions can be secondary.
## Line breaks and wrapping
When the text is too long, the button set will remain in one line, and the side option (if present) will move to a new line.
If the text in the side option is too long, that will itself also wrap in mutliple lines.
## Margins, Padding and Styling
The action bar normally no external margin and usually spans the full length of the parent element.
It has 16px left/right padding and 12 px top/bottom padding, with 16 px spacing between the two buttons in the button set.

@ -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,27 @@
import { Meta } from '@storybook/addon-docs';
<Meta title="Devices and Accessibilty" />
# Devices and Accessibilty
## Desktop-first
OpenProject is not primarily designed for mobile use or with a mobile-first approach, despite most parts of the software adapting fairly well to smaller screens. The majority of features and views are designed to take advantage of larger screens and more complex interactions, including keyboard shortcuts.
Nevertheless,  certain features are particularly useful in a mobile context. These include accessing notifications on the go, viewing work packages, reading and responding to comments and viewing attachments and linked files. These features will be given particular attention and optimised for mobile use.
We do not consider project planning, complex scheduling and team planning to be priority use cases on mobile.
## Accessibility
We recognise the importance of accessibility and are aware that OpenProject still has a fair bit of progress to make in this regard. We intend to initially focus on improving contrast, ensuring UI elements have meaningful alt-text and descriptions and expanding the support for keyboard shortcuts.
We will incrementally evaluate ways to improve our design to better serve users with visual, auditory or motor impairment.
## Localisation and internationalisation
OpenProject supports several languages. Our designs must take internalisation into account, notably in terms of string length and spacing.
The primary design language is English. Our Figma prototypes are always designed first with strings in English and, when relevant, tested with German, French and Spanish translations. This covers our largest user base.
For other languages, we rely on our translators and our community on \[Crowdin\](https://crowdin.com/translate/openproject/) for their help. If there are design-specific issues that are present in certain languages, we encourage the community to file bug reports so we may fix them.

@ -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'];

@ -1,4 +1,5 @@
import { GlobalI18n } from 'core-app/core/i18n/i18n.service';
import * as _ from 'lodash';
export class I18nShim implements GlobalI18n {
public defaultLocale = 'en';

@ -15,5 +15,8 @@
"src/**/*.ts",
"**/*.d.ts",
"app/core/augmenting/dynamic-scripts/*.ts"
],
"exclude": [
"**/*.stories.*"
]
}

@ -475,7 +475,7 @@
groups = ["opf_plugins"];
platforms = [];
source = {
path = modules/budgets;
path = ../modules/budgets;
type = "path";
};
version = "1.0.0";
@ -701,7 +701,7 @@
groups = ["opf_plugins"];
platforms = [];
source = {
path = modules/costs;
path = ../modules/costs;
type = "path";
};
version = "1.0.0";
@ -796,7 +796,7 @@
groups = ["opf_plugins"];
platforms = [];
source = {
path = modules/dashboards;
path = ../modules/dashboards;
type = "path";
};
version = "1.0.0";
@ -1382,7 +1382,7 @@
groups = ["opf_plugins"];
platforms = [];
source = {
path = modules/grids;
path = ../modules/grids;
type = "path";
};
version = "1.0.0";
@ -1924,7 +1924,7 @@
groups = ["opf_plugins"];
platforms = [];
source = {
path = modules/my_page;
path = ../modules/my_page;
type = "path";
};
version = "1.0.0";
@ -2118,7 +2118,7 @@
groups = ["opf_plugins"];
platforms = [];
source = {
path = modules/auth_plugins;
path = ../modules/auth_plugins;
type = "path";
};
version = "1.0.0";
@ -2128,7 +2128,7 @@
groups = ["opf_plugins"];
platforms = [];
source = {
path = modules/auth_saml;
path = ../modules/auth_saml;
type = "path";
};
version = "1.0.0";
@ -2138,7 +2138,7 @@
groups = ["opf_plugins"];
platforms = [];
source = {
path = modules/avatars;
path = ../modules/avatars;
type = "path";
};
version = "1.0.0";
@ -2148,7 +2148,7 @@
groups = ["opf_plugins"];
platforms = [];
source = {
path = modules/backlogs;
path = ../modules/backlogs;
type = "path";
};
version = "1.0.0";
@ -2158,7 +2158,7 @@
groups = ["opf_plugins"];
platforms = [];
source = {
path = modules/bim;
path = ../modules/bim;
type = "path";
};
version = "1.0.0";
@ -2167,7 +2167,7 @@
groups = ["opf_plugins"];
platforms = [];
source = {
path = modules/boards;
path = ../modules/boards;
type = "path";
};
version = "1.0.0";
@ -2176,7 +2176,7 @@
groups = ["opf_plugins"];
platforms = [];
source = {
path = modules/documents;
path = ../modules/documents;
type = "path";
};
version = "1.0.0";
@ -2186,7 +2186,7 @@
groups = ["opf_plugins"];
platforms = [];
source = {
path = modules/github_integration;
path = ../modules/github_integration;
type = "path";
};
version = "1.0.0";
@ -2195,7 +2195,7 @@
groups = ["opf_plugins"];
platforms = [];
source = {
path = modules/job_status;
path = ../modules/job_status;
type = "path";
};
version = "1.0.0";
@ -2204,7 +2204,7 @@
groups = ["opf_plugins"];
platforms = [];
source = {
path = modules/ldap_groups;
path = ../modules/ldap_groups;
type = "path";
};
version = "1.0.0";
@ -2214,7 +2214,7 @@
groups = ["opf_plugins"];
platforms = [];
source = {
path = modules/meeting;
path = ../modules/meeting;
type = "path";
};
version = "1.0.0";
@ -2224,7 +2224,7 @@
groups = ["opf_plugins"];
platforms = [];
source = {
path = modules/openid_connect;
path = ../modules/openid_connect;
type = "path";
};
version = "1.0.0";
@ -2234,7 +2234,7 @@
groups = ["opf_plugins"];
platforms = [];
source = {
path = modules/pdf_export;
path = ../modules/pdf_export;
type = "path";
};
version = "1.0.0";
@ -2244,7 +2244,7 @@
groups = ["opf_plugins"];
platforms = [];
source = {
path = modules/recaptcha;
path = ../modules/recaptcha;
type = "path";
};
version = "1.0.0";
@ -2254,7 +2254,7 @@
groups = ["opf_plugins"];
platforms = [];
source = {
path = modules/reporting;
path = ../modules/reporting;
type = "path";
};
version = "1.0.0";
@ -2288,7 +2288,7 @@
groups = ["opf_plugins"];
platforms = [];
source = {
path = modules/two_factor_authentication;
path = ../modules/two_factor_authentication;
type = "path";
};
version = "1.0.0";
@ -2297,7 +2297,7 @@
groups = ["opf_plugins"];
platforms = [];
source = {
path = modules/webhooks;
path = ../modules/webhooks;
type = "path";
};
version = "1.0.0";
@ -2307,7 +2307,7 @@
groups = ["opf_plugins"];
platforms = [];
source = {
path = modules/xls_export;
path = ../modules/xls_export;
type = "path";
};
version = "1.0.0";
@ -2317,7 +2317,7 @@
groups = ["opf_plugins"];
platforms = [];
source = {
path = modules/overviews;
path = ../modules/overviews;
type = "path";
};
version = "1.0.0";

@ -10,7 +10,7 @@
},
"private": true,
"engines": {
"node": "~16.15.1",
"node": "~16.17.0",
"npm": "~8.12.1"
},
"devDependencies": {

Loading…
Cancel
Save