Remove storybook from actively building, keep last build

This commit is contained in:
Oliver Günther
2023-11-05 20:35:32 +01:00
parent c6281901fe
commit b82ddbc583
62 changed files with 5 additions and 2833 deletions
+1 -5
View File
@@ -8,16 +8,12 @@ updates:
open-pull-requests-limit: 3
versioning-strategy: lockfile-only
groups:
storybook:
patterns:
- 'storybook*'
- '@storybook/*'
angular:
patterns:
- '@angular*'
fullcalendar:
patterns:
- '@fullcalendar*'
- '@fullcalendar*'
- package-ecosystem: "bundler"
directory: "/"
schedule:
-26
View File
@@ -1,26 +0,0 @@
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-with-body -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 }}" }}'
-1
View File
@@ -41,7 +41,6 @@ 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,cbliard
-4
View File
@@ -111,12 +111,8 @@ 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
-4
View File
@@ -12,10 +12,6 @@ services:
ports:
- "${PORT}:4200"
storybook:
ports:
- '6006:6006'
db:
ports:
- '5432:5432'
-9
View File
@@ -87,15 +87,6 @@ services:
depends_on:
- backend
storybook:
build:
<<: *frontend-build
command: "npm run storybook:serve"
volumes:
- ".:/home/dev/openproject"
networks:
- network
db:
image: postgres:13
<<: *restart_policy
-21
View File
@@ -1,21 +0,0 @@
FROM node:20.9 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
+3 -1
View File
@@ -9,4 +9,6 @@ keywords: Design system, Primer, styles, design, components
Starting in OpenProject 13.0., the [Primer Design System](https://primer.style/design/) is being used in OpenProject. Relevant reusable components from Primer as well as common patterns and compositions of these components will be documented in our [Lookbook](https://qa.openproject-edge.com/lookbook/).
Prior to 13.0., components were defined in its own Design System called SPOT which is slowly being replaced by Primer. Components still defined for SPOT are documented in a storybook found here: https://opf.github.io/design-system
Prior to 13.0., components were defined in its own Design System called SPOT which is slowly being replaced by Primer.
Components still defined for SPOT are documented in the last build of storybook found
here: https://opf.github.io/design-system
@@ -161,18 +161,6 @@ Again the first request to the server can take some time too. But subsequent req
Changes you make to the code will be picked up automatically. No need to restart the containers.
### Storybook
There is a service to launch the storybook of the SPOT design system in the local development environment. To run it,
simply use:
```shell
# Start the worker and let them run continuously
docker compose up -d storybook
```
If you used the default overrides you will access the storybook now under `http://localhost:6006`.
### Volumes
There are volumes for
-1
View File
@@ -1,7 +1,6 @@
module.exports = {
extends: [
"eslint:recommended",
"plugin:storybook/recommended",
],
env: {
browser: true,
-10
View File
@@ -1,10 +0,0 @@
import { create } from '@storybook/theming';
export default create({
base: 'light',
brandTitle: 'OpenProject Design System',
brandUrl: '/',
brandImage: '/assets/frontend/logo_openproject_spot.png',
brandTarget: '_self',
});
-76
View File
@@ -1,76 +0,0 @@
import * as path from 'path';
import remarkGfm from 'remark-gfm';
import type { StorybookConfig } from '@storybook/angular';
const config:StorybookConfig = {
stories: [
"../src/**/*.mdx",
"../src/**/*.stories.@(js|jsx|ts|tsx)",
],
addons: [
"@storybook/addon-links",
"@storybook/addon-essentials",
"@storybook/addon-interactions",
{
name: '@storybook/addon-docs',
options: {
mdxPluginOptions: {
mdxCompileOptions: {
remarkPlugins: [remarkGfm],
},
},
},
},
"@storybook/preset-scss",
"storybook-addon-designs",
"./plugin-iframe/src/preset.js",
'@storybook/addon-mdx-gfm',
],
framework: {
name: '@storybook/angular',
options: {}
},
core: {
disableTelemetry: true
},
features: {
},
staticDirs: [
// Copy local static assets
'../src/stories/assets/logo_openproject.png',
'../src/stories/assets/logo_openproject_spot.png',
// Copy font files to specific locations so the normal core SASS
// will load the files correctly without having to use variables
'../src/assets/fonts/openproject_icon/openproject-icon-font.ttf',
'../src/assets/fonts/openproject_icon/openproject-icon-font.svg',
'../src/assets/fonts/openproject_icon/openproject-icon-font.eot',
'../src/assets/fonts/openproject_icon/openproject-icon-font.woff',
'../src/assets/fonts/openproject_icon/openproject-icon-font.woff2',
'../src/assets/fonts/lato/Lato-Regular.woff',
'../src/assets/fonts/lato/Lato-Regular.woff2',
'../src/assets/fonts/lato/Lato-Bold.woff',
'../src/assets/fonts/lato/Lato-Bold.woff2',
'../src/assets/fonts/lato/Lato-Light.woff',
'../src/assets/fonts/lato/Lato-Light.woff2',
'../src/assets/fonts/lato/Lato-Italic.woff',
'../src/assets/fonts/lato/Lato-Italic.woff2',
'../src/assets/fonts/lato/Lato-BoldItalic.woff',
'../src/assets/fonts/lato/Lato-BoldItalic.woff2',
'../src/assets/fonts/lato/Lato-LightItalic.woff',
'../src/assets/fonts/lato/Lato-LightItalic.woff2',
].map(from => ({
from,
to: path.join('/assets/frontend/', path.basename(from))
})),
docs: {
autodocs: true
}
};
export default config;
-6
View File
@@ -1,6 +0,0 @@
import { addons } from '@storybook/addons';
import spotTheme from './SpotTheme';
addons.setConfig({
theme: spotTheme,
});
@@ -1,3 +0,0 @@
module.exports = {
presets: ["@babel/preset-env", "@babel/preset-react"],
};
-336
View File
@@ -1,336 +0,0 @@
{
"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.2",
"resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz",
"integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g=="
},
"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=="
}
}
}
@@ -1,17 +0,0 @@
{
"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"
}
}
@@ -1,5 +0,0 @@
function managerEntries(entry = []) {
return [...entry, require.resolve("./register")]; //👈 Addon implementation
}
module.exports = { managerEntries }
@@ -1,16 +0,0 @@
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;
}
});
});
}
-32
View File
@@ -1,32 +0,0 @@
import { setCompodocJson } from "@storybook/addon-docs/angular";
import { themes } from '@storybook/theming';
import docJson from "../documentation.json";
setCompodocJson(docJson);
export const parameters = {
parameters: {
viewMode: 'docs',
},
actions: { argTypesRegex: "^on[A-Z].*" },
controls: {
matchers: {
color: /(background|color)$/i,
date: /Date$/,
},
},
docs: {
inlineStories: true,
theme: themes.light,
},
options: {
storySort: {
method: 'alphabetical',
order: [
'OpenProject Angular SPOT components',
'Blocks',
// TODO: Add manual sort order for components and patterns
],
},
},
}
-21
View File
@@ -1,21 +0,0 @@
{
"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"
]
}
-4
View File
@@ -1,4 +0,0 @@
declare module '*.md' {
const content: string;
export default content;
}
-35
View File
@@ -177,41 +177,6 @@
],
"preserveSymlinks": true
}
},
"storybook": {
"builder": "@storybook/angular:start-storybook",
"options": {
"configDir": ".storybook",
"browserTarget": "OpenProject:build",
"compodoc": true,
"compodocArgs": [
"-e",
"json",
"-d",
".",
"-p",
"src/tsconfig.compodoc.json"
],
"port": 6006,
"host": "0.0.0.0"
}
},
"build-storybook": {
"builder": "@storybook/angular:build-storybook",
"options": {
"configDir": ".storybook",
"browserTarget": "OpenProject:build",
"compodoc": true,
"compodocArgs": [
"-e",
"json",
"-d",
".",
"-p",
"src/tsconfig.compodoc.json"
],
"outputDir": "storybook-static"
}
}
}
}
+1 -16
View File
@@ -17,16 +17,6 @@
"@html-eslint/eslint-plugin": "^0.15.0",
"@html-eslint/parser": "^0.15.0",
"@jsdevtools/coverage-istanbul-loader": "3.0.5",
"@storybook/addon-actions": "^7.0.18",
"@storybook/addon-essentials": "^7.0.18",
"@storybook/addon-interactions": "^7.0.18",
"@storybook/addon-knobs": "^7.0.2",
"@storybook/addon-links": "^7.0.18",
"@storybook/addon-mdx-gfm": "^7.0.18",
"@storybook/angular": "^7.2.2",
"@storybook/mdx2-csf": "^1.1.0",
"@storybook/preset-scss": "^1.0.3",
"@storybook/testing-library": "^0.1.0",
"@types/codemirror": "5.60.5",
"@types/dragula": "^3.7.0",
"@types/hammerjs": "^2.0.36",
@@ -55,7 +45,6 @@
"eslint-plugin-jsx-a11y": "^6.6.1",
"eslint-plugin-react": "^7.31.11",
"eslint-plugin-react-hooks": "^4.6.0",
"eslint-plugin-storybook": "^0.6.12",
"esprint": "^3.1.0",
"jasmine-core": "~3.6.0",
"jasmine-spec-reporter": "~5.0.0",
@@ -72,8 +61,6 @@
"sass": "^1.52.3",
"sass-loader": "^13.0.0",
"source-map-explorer": "^2.5.2",
"storybook": "^7.0.18",
"storybook-addon-designs": "^7.0.0-beta.2",
"style-loader": "^3.3.1",
"theo": "^8.1.5",
"ts-node": "~8.3.0",
@@ -193,9 +180,7 @@
"lint": "esprint check",
"lint:fix": "esprint check --fix",
"lint:eslint": "eslint",
"generate-typings": "tsc -d -p src/tsconfig.app.json",
"storybook:serve": "ng run OpenProject:storybook",
"storybook:build": "ng run OpenProject:build-storybook"
"generate-typings": "tsc -d -p src/tsconfig.app.json"
},
"overrides": {
"@primer/view-components": "npm:@openproject/primer-view-components@^0.13.1"
@@ -1,18 +0,0 @@
import { Canvas, Meta, ArgsTable } from '@storybook/blocks';
import { SpotCheckboxComponent } from '../checkbox.component';
import * as CheckboxStories from './Checkbox.stories';
<Meta of={CheckboxStories} />
# 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*.
<Canvas of={CheckboxStories.Basic} />
<ArgsTable of={SpotCheckboxComponent} />
@@ -1,32 +0,0 @@
import type { Meta, StoryObj } from '@storybook/angular';
import { moduleMetadata } from '@storybook/angular';
import { OpSpotModule } from '../../../spot.module';
const meta:Meta = {
title: 'Components/Checkbox',
decorators: [
moduleMetadata({
imports: [OpSpotModule],
}),
],
};
export default meta;
type Story = StoryObj;
export const Basic:Story = {
render: (args) => ({
props: args,
template: `
<label>
<spot-checkbox
[name]="name"
[disabled]="disabled"
[checked]="checked"
(change)="checkedChange"
></spot-checkbox>
</label>
`,
}),
};
@@ -1,31 +0,0 @@
import { Canvas, Meta, ArgsTable } from '@storybook/blocks';
import { SpotDropModalComponent } from '../drop-modal.component';
import * as DropModalStories from './DropModal.stories';
<Meta of={DropModalStories} />
# Drop Modal
Drop modals are in-between dropdowns and modals. In fact, on mobile they're indistinguishable from a normal spot-modal.
The key difference however, is that they're tied to a specific context. On desktop views, the modal will open
in-context, meaning that it will attach to the button or input field that opened the modal.
<Canvas
of={DropModalStories.Default}
height="400px"
layout="centered"
/>
## Usage
To use drop-modals, make sure to also add the drop-modal portal to the base of your application HTML:
```
<spot-drop-modal-portal></spot-drop-modal-portal>
```
Even though drop-modals look like they are attached to an element, they're actually rendered in this portal to make sure
they're not cut off my scrolling context or `overflow: visible` rules on parent elements.
<ArgsTable of={SpotDropModalComponent} />
@@ -1,90 +0,0 @@
import type { Meta, StoryObj } from '@storybook/angular';
import { moduleMetadata } from '@storybook/angular';
import { I18nService } from '../../../../core/i18n/i18n.service';
import { I18nServiceStub } from '../../../../../stories/i18n.service.stub';
import { OpSpotModule } from '../../../spot.module';
import SpotDropAlignmentOption from '../../../drop-alignment-options';
import { SpotDropModalComponent } from '../drop-modal.component';
const meta:Meta = {
title: 'Patterns/DropModal',
component: SpotDropModalComponent,
decorators: [
moduleMetadata({
imports: [
OpSpotModule,
],
providers: [
{
provide: I18nService,
useFactory: () => I18nServiceStub,
},
],
}),
],
};
export default meta;
type Story = StoryObj;
export const Default:Story = {
render: (args) => ({
props: {
...args,
dropModalOpen: false,
alignment: SpotDropAlignmentOption.BottomCenter,
},
template: `
<spot-drop-modal-portal></spot-drop-modal-portal>
<spot-drop-modal
[opened]="dropModalOpen"
(closed)="dropModalOpen = false"
[alignment]="alignment"
>
<button
aria-haspopup="true"
type="button"
slot="trigger"
(click)="dropModalOpen = !dropModalOpen"
class="button"
>
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>
`,
}),
};
@@ -1,83 +0,0 @@
import { Canvas, Meta, ArgsTable } from '@storybook/blocks';
import { SpotFormFieldComponent } from '../form-field.component';
import * as FormFieldStories from './FormField.stories';
<Meta of={FormFieldStories} />
# Form Field
Form fields are a smart wrapper around input fields. Form fields handle labels, validation messages, help texts, and
extra actions around an input field.
## Basic usage
Form fields work with `ng-content` a lot, which we call `slot` in SPOT. There are four slots in which content can be
fitted:
1. `input` holds the input. It can be a text input or select dropdown, or anything else that is not a
selection toggle.
2. `help-text` holds the attribute help text. To achieve separation of concerns, if you want to show an attribute help
text this is where the `attribute-help-text` component goes.
3. `errors` holds validation errors.
4. `description` holds a short description of the input field. Oftentimes these are short, helpful guidelines about how
or what to input into the field.
5. `action` holds extra input actions. Most often, these are `button` elements styled as a `spot-link` that allow the user to
perform an action related to the input.
Some of these require adding an extra class on that element, so watch the examples carefully!
## Adding an input
Use `slot="input"` to select the input element:
<Canvas of={FormFieldStories.InputSlot} />
## Adding a description
Use `slot="description"` to select the input element:
<Canvas of={FormFieldStories.DescriptionSlot} />
## Adding a help text
Use `slot="help-text"` to select the `attribute-help-text` component:
```html
<spot-form-field label="Form field with help text">
<spot-text-field slot="input"></spot-text-field>
<attribute-help-text
slot="help-text"
class="spot-form-field--help-text"
></attribute-help-text>
</spot-form-field>
```
The example above cannot currently be rendered in Storybook, because the `attribute-help-text` component requires the
API to be available. They are in use in the project and work package dynamic form. So if you add an attribute help text,
they will show up there.
## Handling validation
SPOT works best with reactive forms. If you have a `FormControl` for your input, `spot-form-field` will be able to
figure out the validation state automagically.
Use `slot="errors"` to set validation errors. You can set multiple errors independently by all giving them the `slot`
attribute.
<Canvas of={FormFieldStories.BasicValidation} />
As you can see, by default validation messages will only be shown on submit. You can change this behavior by changing
the `showValidationErrorOn` input to the `spot-form-field` component.
The `required` input only makes sure the asterisk at the end of the label is shown, it does not add any validation to
the form or input.
## Adding extra input actions
Form field actions allow you to add extra action buttons to the context of an input field.
<Canvas of={FormFieldStories.ActionSlot} />
<ArgsTable of={SpotFormFieldComponent} />
@@ -1,142 +0,0 @@
import type { Meta, StoryObj } from '@storybook/angular';
import { moduleMetadata } from '@storybook/angular';
import {
UntypedFormControl,
UntypedFormGroup,
Validators,
} from '@angular/forms';
import { ReactiveFormsModule } from '@angular/forms';
import { I18nService } from '../../../../core/i18n/i18n.service';
import { I18nServiceStub } from '../../../../../stories/i18n.service.stub';
import { OpSpotModule } from '../../../spot.module';
import { SpotFormFieldComponent } from '../form-field.component';
const meta:Meta = {
title: 'Patterns/FormField',
component: SpotFormFieldComponent,
decorators: [
moduleMetadata({
imports: [
OpSpotModule,
ReactiveFormsModule,
],
providers: [
{
provide: I18nService,
useFactory: () => I18nServiceStub,
},
],
}),
],
};
export default meta;
type Story = StoryObj;
export const InputSlot:Story = {
render: (args) => ({
props: args,
template: `
<spot-form-field label="Form field with input">
<spot-text-field slot="input"></spot-text-field>
</spot-form-field>
`,
}),
};
export const DescriptionSlot:Story = {
render: (args) => ({
props: args,
template: `
<spot-form-field label="Form field with description">
<spot-text-field slot="input"></spot-text-field>
<span slot="description">
Helpful guidelines so the user can be confident about their input.
</span>
</spot-form-field>
`,
}),
};
export const BasicValidation:Story = {
render: (args) => ({
props: {
...args,
myForm: new UntypedFormGroup({
myInput: new UntypedFormControl(null, [Validators.required, Validators.minLength(8)]),
}),
onSubmit: (event:any) => console.log('onSubmit', event),
},
template: `
<form
[formGroup]="myForm"
(ngSubmit)="onSubmit($event)"
class="spot-container"
>
<spot-form-field
label="Form field with validation"
[required]="true"
>
<spot-text-field
formControlName="myInput"
slot="input"
></spot-text-field>
<div
slot="errors"
class="spot-form-field--error"
*ngIf="myForm.get('myInput')!.errors?.required"
>
This input is required.
</div>
<div
slot="errors"
class="spot-form-field--error"
*ngIf="myForm.get('myInput')!.errors?.minlength"
>
This input needs to be at least 8 characters long.
</div>
</spot-form-field>
<div class="spot-action-bar">
<div class="spot-action-bar--right">
<button
type="submit"
class="button -highlight spot-action-bar--action"
>Submit data</button>
</div>
</div>
</form>
`,
}),
};
export const ActionSlot:Story = {
render: (args) => ({
props: {
...args,
alert: (s:string) => console.log(s),
},
template: `
<spot-form-field label="Form field with input">
<spot-text-field slot="input"></spot-text-field>
<button
type="button"
(click)="log('Some action')"
class="spot-link"
slot="action"
>Some action</button>
<button
type="button"
(click)="log('Another action')"
class="spot-link"
slot="action"
>Another action</button>
</spot-form-field>
`,
}),
};
@@ -1,51 +0,0 @@
import { Canvas, Meta, ArgsTable } from '@storybook/blocks';
import { SpotSelectorFieldComponent } from '../selector-field.component';
import * as SelectorFieldStories from './SelectorField.stories';
<Meta of={SelectorFieldStories} />
# Selector Field
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.
<Canvas of={SelectorFieldStories.Default} />
## 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:
<Canvas of={SelectorFieldStories.LongLabel} />
## Options
Selector labels can be **bold** or regular. However, these two styles should not be mixed in a single set of selector fields.
<Canvas of={SelectorFieldStories.FontWeight} />
## Basic usage
Selector fields work with `ng-content` a lot, which we call `slot` in SPOT. There are four slots in which content can be
fitted:
1. `input` holds the input. It can be any selector toggle
2. `help-text` holds the attribute help text. To achieve separation of concerns, if you want to show an attribute help
text this is where the `attribute-help-text` component goes.
3. `errors` holds validation errors.
4. `description` holds a short description of the input field. Oftentimes these are short, helpful guidelines about how
or what to input into the field.
Some of these require adding an extra class on that element, so watch the examples carefully!
## 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.
<ArgsTable of={SpotSelectorFieldComponent} />
@@ -1,105 +0,0 @@
import type { Meta, StoryObj } from '@storybook/angular';
import { moduleMetadata } from '@storybook/angular';
import { FormsModule, ReactiveFormsModule } from '@angular/forms';
import { I18nService } from '../../../../core/i18n/i18n.service';
import { I18nServiceStub } from '../../../../../stories/i18n.service.stub';
import { OpSpotModule } from '../../../spot.module';
import { SpotSelectorFieldComponent } from '../selector-field.component';
const meta:Meta = {
title: 'Patterns/SelectorField',
component: SpotSelectorFieldComponent,
decorators: [
moduleMetadata({
imports: [
OpSpotModule,
ReactiveFormsModule,
FormsModule,
],
providers: [
{
provide: I18nService,
useFactory: () => I18nServiceStub,
},
],
}),
],
};
export default meta;
type Story = StoryObj;
export const Default:Story = {
render: (args) => ({
props: {
...args,
mixed: null,
},
template: `
<div class="spot-container">
<spot-selector-field label="With a spot-checkbox">
<spot-checkbox slot="input"></spot-checkbox>
</spot-selector-field>
<spot-selector-field
label="Reverse label with a spot-checkbox"
[reverseLabel]="true"
>
<spot-checkbox slot="input"></spot-checkbox>
</spot-selector-field>
<hr class="spot-divider">
<spot-selector-field label="With a spot-switch">
<spot-switch
slot="input"
[checked]="false"
></spot-switch>
</spot-selector-field>
<spot-selector-field
label="Reverse label with a spot-switch"
[reverseLabel]="true"
>
<spot-switch slot="input"></spot-switch>
</spot-selector-field>
</div>
`,
}),
};
export const LongLabel:Story = {
render: (args) => ({
props: args,
template: `
<spot-selector-field
label="This is an incredibly long label in the hopes that we'll be able to make it run over multiple lines even on very wide screens, incredibly wide screens that are way past full hd"
>
<spot-checkbox slot="input"></spot-checkbox>
</spot-selector-field>
`,
}),
};
export const FontWeight:Story = {
render: (args) => ({
props: args,
template: `
<div class="spot-container">
<spot-selector-field
label="Bold Label"
labelWeight="bold"
>
<spot-checkbox slot="input"></spot-checkbox>
</spot-selector-field>
<spot-selector-field label="Regular Label">
<spot-checkbox slot="input"></spot-checkbox>
</spot-selector-field>
</div>
`,
}),
};
@@ -1,26 +0,0 @@
import { Canvas, Meta, ArgsTable } from '@storybook/blocks';
import { SpotSwitchComponent } from '../switch.component';
import * as SwitchStories from './Switch.stories';
<Meta of={SwitchStories} />
# Switches
This component describes only the actual switch, 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*.
### Enabled
<Canvas of={SwitchStories.Checked} />
<Canvas of={SwitchStories.Unchecked} />
### Disabled
<Canvas of={SwitchStories.DisabledChecked} />
<Canvas of={SwitchStories.Disabled} />
<ArgsTable of={SpotSwitchComponent} />
@@ -1,46 +0,0 @@
import type { Meta, StoryObj } from '@storybook/angular';
import { moduleMetadata } from '@storybook/angular';
import { OpSpotModule } from '../../../spot.module';
import { SpotSwitchComponent } from '../switch.component';
const meta:Meta = {
title: 'Components/Switch',
component: SpotSwitchComponent,
decorators: [
moduleMetadata({
imports: [OpSpotModule],
}),
],
};
export default meta;
type Story = StoryObj;
export const Checked:Story = {
args: {
checked: true,
disabled: false,
},
};
export const Unchecked:Story = {
args: {
checked: false,
disabled: false,
},
};
export const Disabled:Story = {
args: {
checked: false,
disabled: true,
},
};
export const DisabledChecked:Story = {
args: {
checked: true,
disabled: true,
},
};
@@ -1,67 +0,0 @@
import { Canvas, Meta, ArgsTable } from '@storybook/blocks';
import * as TextFieldStories from './TextField.stories';
import { SpotTextFieldComponent } from '../text-field.component';
<Meta of={TextFieldStories} />
# Text Field
The text field is one of the most common input elements used in a wide range of scenarios: fill a form, enter work package details, search for something, pick a date.
## Structure and Options
The most basic text field is simply an input field where the user can type in text.
<Canvas of={TextFieldStories.Default} />
The text field component affords a few optional elements:
An optional icon can be added to the left edge to provide additional context when this is necessary (in a search field, for example):
<Canvas of={TextFieldStories.SearchWithIcon} />
An icon can also optionally be placed on the right edge. This arrow can provide additional hints on how the text input field will behave; for example, a right arrow might suggest that the value will be immediately submitted, or a dropdown arrow might indicate that the text field will open a drop modal underneath it (a list of assignees, for example).
The right icon can also be converted to an icon-only link that allows the user to clear the field.
<Canvas of={TextFieldStories.SearchWithIconAndValue} />
## Behaviour
Clicking on a text field will move it to the focused state.
A text field can have placeholder text to provide additionanl context or information; this text is replaced by actual text that the user inputs, and is shown again only if the field is cleared.
<Canvas of={TextFieldStories.Placeholder} />
The clear icon-only link on the right only appears on some text fields.
The width of a text field is fixed and defined either manually, or by the width of the containing element. When text exceeds the available width, it scrolls with the cursor within the text field (and clips on the other end).
## States
The **Default** state indicates that a text field is ready to accept text input.
<Canvas of={TextFieldStories.WithValue} />
The **Focus** state has a coloured outline, the same as any focused element in OpenProject.
The **Hover** element has a darker border (Basic/Gray 1).
The **Disabled** state shows all elements (including icons and any placeholder or input text) in grey, and makes any interaction impossible.
<Canvas of={TextFieldStories.Disabled} />
<Canvas of={TextFieldStories.DisabledWithValue} />
The **Error** state puts a red outline around the text input. (_Note_: This state is only used when the text input is part of a _Form element_, which supplements this red outline with an message error message). The error state is usually temporary; fixing the cause of the error (via asyncronous form validation) usually allows it to revert back to the focus state.
> Example: Error state
## Margins and Spacing
The text field has 0.5 rem left and right margins, and a 0.25 rem top and bottom margins.
There is also a 0.25 rem spacing between items (icon and text).
<ArgsTable of={SpotTextFieldComponent} />
@@ -1,81 +0,0 @@
import type { Meta, StoryObj } from '@storybook/angular';
import { moduleMetadata } from '@storybook/angular';
import { OpSpotModule } from '../../../spot.module';
import { SpotTextFieldComponent } from '../text-field.component';
const meta:Meta = {
title: 'Components/TextField',
component: SpotTextFieldComponent,
decorators: [
moduleMetadata({
imports: [OpSpotModule],
}),
],
};
export default meta;
type Story = StoryObj;
export const Default:Story = {
args: {
value: '',
disabled: false,
placeholder: '',
showClearButton: true,
name: 'my-input',
},
};
export const SearchWithIcon:Story = {
render: (args) => ({
props: args,
template: `
<spot-text-field>
<span
class="spot-icon spot-icon_search"
slot="before"
></span>
</spot-text-field>
`,
}),
};
export const SearchWithIconAndValue:Story = {
render: (args) => ({
props: args,
template: `
<spot-text-field value="Some value">
<span
class="spot-icon spot-icon_search"
slot="before"
></span>
</spot-text-field>
`,
}),
};
export const Placeholder:Story = {
args: {
placeholder: 'Enter a value here',
},
};
export const WithValue:Story = {
args: {
value: 'Some value',
},
};
export const Disabled:Story = {
args: {
disabled: true,
},
};
export const DisabledWithValue:Story = {
args: {
value: 'Disabled with value',
disabled: true,
},
};
@@ -1,16 +0,0 @@
import { Canvas, Meta, ArgsTable } from '@storybook/blocks';
import { SpotToggleComponent } from '../toggle.component';
import * as ToggleStories from './Toggle.stories';
<Meta of={ToggleStories} />
# Toggles
All of these examples are static.
<Canvas of={ToggleStories.Default} />
<Canvas of={ToggleStories.WithValue} />
<Canvas of={ToggleStories.FourOptions} />
<ArgsTable of={SpotToggleComponent} />
@@ -1,52 +0,0 @@
import type { Meta, StoryObj } from '@storybook/angular';
import { moduleMetadata } from '@storybook/angular';
import { OpSpotModule } from '../../../spot.module';
import { SpotToggleComponent } from '../toggle.component';
const meta:Meta = {
title: 'Components/Toggle',
component: SpotToggleComponent,
decorators: [
moduleMetadata({
imports: [OpSpotModule],
}),
],
};
export default meta;
type Story = StoryObj;
export const Default:Story = {
args: {
name: 'my-toggle',
value: null,
options: [
{ value: 'first', title: 'Unread' },
{ value: 'second', title: 'All' },
],
},
};
export const WithValue:Story = {
args: {
value: 'first',
options: [
{ value: 'first', title: 'Unread' },
{ value: 'second', title: 'All' },
],
},
};
export const FourOptions:Story = {
args: {
name: 'my-toggle',
value: 'first',
options: [
{ value: 'first', title: 'First option' },
{ value: 'second', title: 'Second option' },
{ value: 'third', title: 'Third option' },
{ value: 'best', title: 'Best option' },
],
},
};
@@ -1,57 +0,0 @@
import { Canvas, Meta, ArgsTable } from '@storybook/blocks';
import { SpotTooltipComponent } from '../tooltip.component';
import * as TooltipStories from './Tooltip.stories';
<Meta of={TooltipStories} />
# Tooltip
<Canvas of={TooltipStories.Default} />
The tooltip provides additional textual context on hover over interactive elements.
This context can be used to provide additional information (hover over a "help" icon) or to signal state information (for example, if a certain item is disabled).
By default, the tooltip takes 80% of the width of the container. This can be overridden if needed.
<Canvas of={TooltipStories.InList} />
## Structure and Options
Tooltips have a sky blue background in the default theme (_Feedback/Info/Light_). A darker version is available is but currently not used.
<Canvas of={TooltipStories.Dark} />
## Behaviour
The tooltip should appear either above or below the item being hovered. By default, it will be displayed above, left-aligned. This can be manually changed to these alternative positions:
- Above, left-aligned
- Above, center-aligned
- Above, right-aligned
- Below, left-aligned
- Below, center-aligned
- Below, right- aligned
The item over which the user is hovering itself must never be covered. A tooltip' does not follow the cursor; its position is fixed as long as the user is hovering over the item. Hovering out of the element will cause the tooltip to disappear.
The tooltip will appear 200 ms after the user starts hovering and disappear 200 ms after the user has moved the cursor away, to avoid jittery behaviour.
The tooltip will automatically become multi-line if necessary (i.e, if the container does not have sufficient space).
<Canvas of={TooltipStories.Multiline} />
## Dos and Don'ts
A tooltip is, in its basic form, a line of text. However, it can also contain icons.
It must never contain interactive elements (a mouse pointer will not always be able to reach it).
## Margins and Spacing
A tooltip will never hug either side of the container; it will respect the internal padding of the that container.
> Position, in-tree or out-of-tree to be decided
Due to technical limitations, the minimum width of a tooltip is defined by its container (i.e, 80% of it). Tooltips with content shorter this width will have additional space on the right.
@@ -1,126 +0,0 @@
import type { Meta, StoryObj } from '@storybook/angular';
import { moduleMetadata } from '@storybook/angular';
import { OpSpotModule } from '../../../spot.module';
import { SpotTooltipComponent } from '../tooltip.component';
const meta:Meta = {
title: 'Components/Tooltip',
component: SpotTooltipComponent,
decorators: [
moduleMetadata({
imports: [OpSpotModule],
}),
],
};
export default meta;
type Story = StoryObj;
export const Default:Story = {
render: (args) => ({
props: args,
template: `
<spot-tooltip>
<span slot="trigger">
Hover me to see the tooltip. By default, tooltips take a maximum of 80% of their parents' width
</span>
<p
slot="body"
class="spot-body-small"
>
This is an example tooltip.
</p>
</spot-tooltip>
`,
}),
};
export const InList:Story = {
render: (args) => ({
props: args,
template: `
<ul class="spot-list">
<li class="spot-list--item">
<spot-tooltip alignment="bottom-center">
<ng-container slot="trigger">
<label class="spot-list--item-action">
<spot-checkbox></spot-checkbox>
<div class="spot-list--item-title spot-list--item-title_ellipse-text">Checky with a tooltip</div>
</label>
</ng-container>
<p
slot="body"
class="spot-body-small"
>
This is a great checkbox.
</p>
</spot-tooltip>
</li>
<li class="spot-list--item">
<spot-tooltip
alignment="bottom-center"
disabled="true"
>
<ng-container slot="trigger">
<label class="spot-list--item-action">
<spot-checkbox></spot-checkbox>
<div class="spot-list--item-title spot-list--item-title_ellipse-text">Checky with a disabled tooltip</div>
</label>
</ng-container>
<p
slot="body"
class="spot-body-small"
>
This tooltip is not going to show.
</p>
</spot-tooltip>
</li>
<li class="spot-list--item">
<label class="spot-list--item-action">
<spot-checkbox></spot-checkbox>
<div class="spot-list--item-title spot-list--item-title_ellipse-text">Checky without a tooltip</div>
</label>
</li>
</ul>
`,
}),
};
export const Dark:Story = {
render: (args) => ({
props: args,
template: `
<spot-tooltip dark="true">
<span slot="trigger">Hover me to see the tooltip.</span>
<p
slot="body"
class="spot-body-small"
>
This is a dark tooltip.
</p>
</spot-tooltip>
`,
}),
};
export const Multiline:Story = {
render: (args) => ({
props: args,
template: `
<spot-tooltip>
<span slot="trigger">Hover me to see the tooltip.</span>
<p
slot="body"
class="spot-body-small"
>
This is a tooltip with a very long text <br />
that has a break in the middle so that we <br />
can get some multiline action.
</p>
</spot-tooltip>
`,
}),
};
-76
View File
@@ -1,76 +0,0 @@
import { Canvas, Meta } from '@storybook/blocks';
import * as ActionBarStories from './ActionBar.stories';
<Meta of={ActionBarStories} />
# Action Bar
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.
Above, you see an action bar embedded inside a modal dialogue.
<Canvas of={ActionBarStories.InModal} />
## 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 of={ActionBarStories.Default} />
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**
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.
<Canvas of={ActionBarStories.LeftButtons} />
**Transparent background**
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.
<Canvas of={ActionBarStories.Transparent} />
## Options
**Side options**
The side option is, as the name suggests, optional. An action bar can simply be empty on one side, like so:
<Canvas of={ActionBarStories.NoSideOption} />
**More buttons**
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.
<Canvas of={ActionBarStories.MoreButtons} />
## 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 multiple lines.
-187
View File
@@ -1,187 +0,0 @@
import type { Meta, StoryObj } from '@storybook/angular';
import { moduleMetadata } from '@storybook/angular';
import { OpSpotModule } from '../app/spot/spot.module';
const meta:Meta = {
title: 'Patterns/ActionBar',
decorators: [
moduleMetadata({
imports: [OpSpotModule],
}),
],
};
export default meta;
type Story = StoryObj;
export const InModal:Story = {
render: (args) => ({
props: args,
template: `
<div class="spot-modal" style="border: rgb(224, 224, 224) 1px solid">
<div class="spot-modal--header">Delete attachment</div>
<div class="spot-modal--body spot-container">
<span class="spot-body-small">Are you sure you want to delete this file? This action is not reversible.</span>
</div>
<div class="spot-action-bar">
<div class="spot-action-bar--right">
<button
type="button"
class="spot-action-bar--action button"
>Cancel</button>
<button
type="button"
class="spot-action-bar--action button -danger"
>
<span class="spot-icon spot-icon_delete"></span>
<span>Delete attachment</span>
</button>
</div>
</div>
</div>
`,
}),
};
export const Default:Story = {
render: (args) => ({
props: args,
template: `
<div class="spot-action-bar">
<div class="spot-action-bar--left">
<spot-selector-field
class="spot-action-bar--action"
label="Remember this choice"
>
<spot-checkbox slot="input"></spot-checkbox>
</spot-selector-field>
</div>
<div class="spot-action-bar--right">
<button
type="button"
class="spot-action-bar--action button -highlight"
>Okay
</button>
</div>
</div>
`,
}),
};
export const LeftButtons:Story = {
render: (args) => ({
props: args,
template: `
<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">
<spot-selector-field
class="spot-action-bar--action"
[label]="'Remember this choice'"
>
<spot-checkbox slot="input"></spot-checkbox>
</spot-selector-field>
</div>
</div>
`,
}),
};
export const Transparent:Story = {
render: (args) => ({
props: args,
template: `
<div class="spot-action-bar spot-action-bar_transparent">
<div class="spot-action-bar--left">
<spot-selector-field
class="spot-action-bar--action"
[label]="'Remember this choice'"
>
<spot-checkbox slot="input"></spot-checkbox>
</spot-selector-field>
</div>
<div class="spot-action-bar--right">
<button
type="button"
class="spot-action-bar--action button"
>
<span>Cancel</span>
</button>
<button
type="button"
class="spot-action-bar--action button -danger"
>
<span class="spot-icon spot-icon_delete"></span>
<span>Delete</span>
</button>
</div>
</div>
`,
}),
};
export const NoSideOption:Story = {
render: (args) => ({
props: args,
template: `
<div class="spot-action-bar">
<div class="spot-action-bar--right">
<button
type="button"
class="spot-action-bar--action button"
>
<span>Cancel</span>
</button>
<button
type="button"
class="spot-action-bar--action button -highlight"
>
<span>Next</span>
</button>
</div>
</div>
`,
}),
};
export const MoreButtons:Story = {
render: (args) => ({
props: args,
template: `
<div class="spot-action-bar">
<div class="spot-action-bar--left">
<button
type="button"
class="spot-action-bar--action button"
>
<span class="spot-icon spot-icon_watched"></span>
<span>Watch</span>
</button>
<button
type="button"
class="spot-action-bar--action button"
>
<span class="spot-icon spot-icon_mark-read"></span>
<span>Mark as read</span>
</button>
<button
type="button"
class="spot-action-bar--action button"
>
<span>More</span>
<span class="spot-icon spot-icon_dropdown"></span>
</button>
</div>
</div>
`,
}),
};
-56
View File
@@ -1,56 +0,0 @@
import { Canvas, Meta } from '@storybook/blocks';
import * as BreadcrumbsStories from './Breadcrumbs.stories';
<Meta of={BreadcrumbsStories} />
# Breadcrumbs
Breadcrumbs provide important semantic context to information being presented on the screen by allowing users to identify where they are in a hierarchical structure.
Breadcrumbs allow users to:
- identify their current location (in a folder structure or hierarchy of work packages)
- navigate up to the parent(s)
<Canvas of={BreadcrumbsStories.Default} />
## Structure and Options
Breadcrumbs consist of a series of "levels" presented horizontally, separated by a right arrow (▶). Each level is a child of the level preceding it, except the first level, which is the root.
<Canvas of={BreadcrumbsStories.FourLevels} />
## Behaviour
The last level is always the current level; this is never clickable. All other levels (the parents) are links, and clicking on them will take users to that level.
Because the length of the breadcrumb can vary significantly (depending on the title of each level and the available space), it can condense to fit the available space:
- The breadcrumb can show four (4) levels in full, space permitting, before going to "collapsed" mode.
- If the available space in the parent container not does afford sufficient width to display the four levels in full, the middle levels are truncated with ellipses ("Root ▶ Second le... ▶ Third le... ▶ Fourth level").
<Canvas of={BreadcrumbsStories.FiveLevels} />
There are special rules on mobile:
- The root level shows only an icon and ellipses ("...", fully truncated).
- The direct parent (...) is displayed as non-shrinkable ellipses that are clickable
- Further parents (between the direct parent and the root) are not displayed
- The current level maybe be truncated with ellipses ("[Icon] ... ▶ ... ▶ Current le...) if needed.
To view the mobile style, reduce the viewport width of your browser below 680px.
## Options
Each level can consist of:
- an icon
- a text
The icon is almost always only used for the first item, although the component does allow it to be present at any level.
The text for each level is always a link, except for:
- current level
- collapsed levels between the direct parent and root in collapsed mode
@@ -1,62 +0,0 @@
import type { Meta, StoryObj } from '@storybook/angular';
import { moduleMetadata } from '@storybook/angular';
import { OpSpotModule } from '../app/spot/spot.module';
import { BreadcrumbsContent } from '../app/spot/components/breadcrumbs/breadcrumbs-content';
import { SpotBreadcrumbsComponent } from '../app/spot/components/breadcrumbs/breadcrumbs.component';
const meta:Meta = {
title: 'Patterns/Breadcrumbs',
component: SpotBreadcrumbsComponent,
decorators: [
moduleMetadata({
imports: [OpSpotModule],
}),
],
};
export default meta;
type Story = StoryObj;
export const Default:Story = {
render: (args) => ({
props: {
...args,
content: new BreadcrumbsContent([
{ icon: 'folder', text: 'OpenProject storage' },
{ text: 'Public' },
{ text: 'Shared' },
]),
},
}),
};
export const FourLevels:Story = {
render: (args) => ({
props: {
...args,
content: new BreadcrumbsContent([
{ icon: 'folder', text: 'Root' },
{ text: 'Second level' },
{ text: 'Third level' },
{ text: 'Current level' },
]),
},
}),
};
export const FiveLevels:Story = {
render: (args) => ({
props: {
...args,
content: new BreadcrumbsContent([
{ icon: 'folder', text: 'Root folder with a long name' },
{ text: 'Second level' },
{ text: 'Third level with an even longer name' },
{ text: 'Fourth level with the longest name from all' },
{ text: 'Current level and even this one has a long name' },
]),
},
}),
};
-129
View File
@@ -1,129 +0,0 @@
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 default. Use it for secondary actions or in a group of buttons where there are no primary actions (for example, 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 button 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 presence of icons). In certain situations, the button might have a fixed length.
-39
View File
@@ -1,39 +0,0 @@
import { Canvas, Meta } from '@storybook/blocks';
import * as DividerStories from './Divider.stories';
<Meta of={DividerStories} />
# 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)
<Canvas of={DividerStories.Soft} />
**Strong**
1px, Grey-4 (#CCCCCC)
<Canvas of={DividerStories.Strong} />
_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._
-47
View File
@@ -1,47 +0,0 @@
import type { Meta, StoryObj } from '@storybook/angular';
import { moduleMetadata } from '@storybook/angular';
import { OpSpotModule } from '../app/spot/spot.module';
const meta:Meta = {
title: 'Components/Divider',
decorators: [
moduleMetadata({
imports: [OpSpotModule],
}),
],
};
export default meta;
type Story = StoryObj;
export const Soft:Story = {
render: (args) => ({
props: args,
template: `
<div class="spot-container">
<h1 class="spot-subheader-small">Some header with a soft divider below</h1>
<div class="spot-divider"></div>
<p class="spot-body-big">
Lorem ipsum goes here but I'm too lazy to copy paste it from somewhere so I'll just ramble on
until I think it has been enough. That was a very long sentence so I'll do one shorter one. Blablabla is what I say.
</p>
</div>
`,
}),
};
export const Strong:Story = {
render: (args) => ({
props: args,
template: `
<div class="spot-container">
<h1 class="spot-subheader-small">Strong divider below</h1>
<div class="spot-divider spot-divider_strong"></div>
<p class="spot-body-big">
Lorem ipsum goes here but I'm too lazy to copy paste it from somewhere so I'll just ramble on
until I think it has been enough. That was a very long sentence so I'll do one shorter one. Blablabla is what I say.
</p>
</div>
`,
}),
};
-26
View File
@@ -1,26 +0,0 @@
import { Meta } from '@storybook/blocks';
<Meta title="Components/Dropdown button" />
# Dropdown button
> Example: Filter button with a badge value of 3.
## Description
The dropdown button is a special type of button with a permanent down arrow on the right edge (suggesting further actions) and an optional badge.
## Behaviour
Clicking on a dropdown button generally opens the dropdown menu, or a drop modal. When the menu or modal is open, clicking on the button again closes it.
> Example: A more button that opens a dropdown menu
In some (rarer) cases, it can open a panel. An example of this is the Filter button.
## Options
The badge is optional and can hold a number from 0 to 99. After 99, the number simply reads "99+".
-35
View File
@@ -1,35 +0,0 @@
import { Meta, Story, Canvas } from '@storybook/addon-docs';
<Meta title="Patterns/Dropdown menu" />
# Dropdown menu
> Example: A dropdown menu with four items
The dropdown menu is a contextual menu triggered by clicking on a dropdown button. It provides options to the use.
## Structure and Options
The dropdown menu consists of a list of items, generally *menu rows* and an optional title.
The title is not interactive. It serves to provide additional context, if necessary.
A menu item is interactive and generally represents an action. The menu item can optionally have an icon on the left side.
> Example of an existing more menu that has icons next to each menu row
## States
Each menu item has three states: regular, hover and clicked.
## Margins and Spacing
Each item has a height of 1.5 rem. There is a 0.25 rem spacing between the optional icon and the label, and a 0.5 rem rem spacing to the left and right.
The width is determined by the longest object, or can be defined manually. If defined manually, items that are very long and exceed this length are truncated.
> Eg: a menu item that's very long and truncated
-8
View File
@@ -1,8 +0,0 @@
import { Meta } from '@storybook/addon-docs';
<Meta title="OpenProject Angular SPOT components" />
# OpenProject Angular SPOT components
This page contains documentation for OpenProject angular components and is mostly deprecated.
Documentation will be kept updated instead in Lookbok, which you can find in a development server under http://localhost:3000/lookbook
-62
View File
@@ -1,62 +0,0 @@
import { Canvas, Meta } from '@storybook/blocks';
import * as LinkStories from './Link.stories';
<Meta of={LinkStories} />
# 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 of={LinkStories.Basic} />
**Text with left icon**
The left icon provides additional context when necessary.
<Canvas of={LinkStories.LeftIcon} />
**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.
<Canvas of={LinkStories.RightIcon} />
**Text with both icons**
This is available but we discourage its use.
<Canvas of={LinkStories.BothIcons} />
**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)._
## 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.
-73
View File
@@ -1,73 +0,0 @@
import type { Meta, StoryObj } from '@storybook/angular';
import { moduleMetadata } from '@storybook/angular';
import { OpSpotModule } from '../app/spot/spot.module';
const meta:Meta = {
title: 'Components/Link',
decorators: [
moduleMetadata({
imports: [OpSpotModule],
}),
],
};
export default meta;
type Story = StoryObj;
export const Basic:Story = {
render: (args) => ({
props: args,
template: `
<a
href="#"
class="spot-link"
>This is a spot-link</a>
`,
}),
};
export const LeftIcon:Story = {
render: (args) => ({
props: args,
template: `
<a
href="#"
class="spot-link"
>
<span>This is a spot-link</span>
<span class="spot-icon spot-icon_add"></span>
</a>
`,
}),
};
export const RightIcon:Story = {
render: (args) => ({
props: args,
template: `
<a
href="#"
class="spot-link"
>
<span class="spot-icon spot-icon_add"></span>
<span>This is a spot-link</span>
</a>
`,
}),
};
export const BothIcons:Story = {
render: (args) => ({
props: args,
template: `
<a
href="#"
class="spot-link"
>
<span class="spot-icon spot-icon_add"></span>
<span>This is a spot-link</span>
<span class="spot-icon spot-icon_add"></span>
</a>
`,
}),
};
-36
View File
@@ -1,36 +0,0 @@
import { Canvas, Meta } from '@storybook/blocks';
import * as ListStories from './List.stories';
<Meta of={ListStories} />
# 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 of={ListStories.WithLinks} />
# 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 of={ListStories.WithCheckboxes} />
# Compact
The compact version makes the items a little bit less tall.
<Canvas of={ListStories.Compact} />
-87
View File
@@ -1,87 +0,0 @@
import type { Meta, StoryObj } from '@storybook/angular';
import { moduleMetadata } from '@storybook/angular';
import { OpSpotModule } from '../app/spot/spot.module';
const meta:Meta = {
title: 'Components/List',
decorators: [
moduleMetadata({
imports: [OpSpotModule],
}),
],
};
export default meta;
type Story = StoryObj;
export const WithLinks:Story = {
render: (args) => ({
props: args,
template: `
<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>
`,
}),
};
export const WithCheckboxes:Story = {
render: (args) => ({
props: args,
template: `
<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>
`,
}),
};
export const Compact:Story = {
render: (args) => ({
props: args,
template: `
<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>
`,
}),
};
-51
View File
@@ -1,51 +0,0 @@
import { Meta } from '@storybook/blocks';
<Meta title="Components/Section Headers" />
# Section Headers
> Example: Section header example
The section header is used to separate different sections of a page or a view, or different grouping of similar functionality.
In the Files tab for example, the section header separates the Attachments section and the the different file storages that may be available to a work package.
## Structure and Options
Section headers are at their most basic form a label (in all-caps) with a line underneath.
> Example of a basic section header
However, they can be extended with a few additional features:
**Left icon**
This icon can be added to provide context. In a files list, for example, the left icon is used to indicate to which storage provider (for example, Nextcloud) that file storage is linked.
In some parts of the application, this icon is also used to indicate open/closed state when the section header functions as an expandable/collapsible section (see *Behaviour*).
*Note: Since this element is always all-caps, the baseline of the font requires the icon to be raised by a few pixels. See* Margins and Spacing *for more details.*
**Right icon**
The icon can be placed to the right of the text too, if required. It can optionally also be used as a link (in icon-only mode). This should nevertheless be used sparingly.
**Action**
This is usually a button on the far edge (by default the right edge) of the component, but can also be a link. This is used to provide additional contextual action related to that whole section (to access the home directory in Nextcloud in a new tab, for example).
> Example of a section header with a left icon and an action (like a Nextcloud file storage)
**Right position**
The entire structure can be inverted, with the action buton on the left and the title and icons on the right. This should ideally never be used, but exists because such a format already exists in some places. Use sparingly.
## Behaviour
The section header behaves either as a regular header (static) or as an expandable/collapsible section, where clicking on the entire section header will expand and collapse the content underneath.
> Example: "Group by" expandable/collapsible section, like a cost report page
## Margins and Spacing
The headers have an 0.5 rem top and bottom margin.
Because the label is all-caps, the baseline of the font is (visually) slightly higher than it otherwise would have been with mixed case. If the optional icons are used, the CSS of this component automatically pushed them up by a few pixels for better alignment.
@@ -1,7 +0,0 @@
import type { Meta } from '@storybook/angular';
const meta:Meta = {
title: 'Section Header',
};
export default meta;
-51
View File
@@ -1,51 +0,0 @@
import { Meta } from '@storybook/blocks';
<Meta title="Components/Toast" />
# Toast
> Example: A success toast
Toasts are small generally one-line messages that appear to inform the user of the result of an action, or an update to the current view.
## Structure and Options
The toast consists of:
- An optional icon
- A text
- A close button
There are four types of toasts, each with a specific icon and colour associated with it:
An **Information** toast is blue (background: Feedback/Info/Light, stroke: Feedback/Info/Dark) and provides the user with helpful information about the current view.
> Eg: a blue notification toast (as it exists today)
A **Success** toast is green (background: Feedback/Success/Light, stroke: Feedback/Success/Dark)and indicates to the user that an action has been successfully carried out.
> Eg: a green success toast (as it exists today)
A **Warning** toast is orange (background: Feedback/Warning/Light, stroke: Feedback/Warning/Dark) and indicates that there are potential risks or possibility of error.
> Eg: an orange toast (is this used somewhere?)
An **Error** toast is red (background: Feedback/Error/Light, stroke: Feedback/Error/Dark) and indicates that something did not not go as expected, and that there is a problem.
> Eg: a red toast (as it exists today, when moving an WP in GANTT view is not possible)
## Behaviour
The toast appears either as result of user action (a work package attribute is modified, for example) or a background action (there are new notifications).
The text is generally merely informational, although there might sometimes be a link with an approproate action.
> Eg. Blue notification toast with "Load new notifications" link.
Toasts generally disappear automatically; they must be manually closed by the user. The only exception is the Success toast, which _can_ disappear automatically, if the user has enabled this in their account settings.
Ideally, only one toast should be visible at any time. Hoever, there are circumstances where multiple toasts might be generated and visible to the user; in this case, the toasts must "pile" vertically with a 1 rem (?) spacing between them.
## Margins and Spacing
The toast has a height of 2.5 rem, with a 0.5 rem padding on all sides, and a 0.5 rem spacing between the icon, text and the close button.
Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.1 KiB

@@ -1,4 +0,0 @@
export const ConfigurationServiceStub = {
startOfWeek: () => 1,
startOfWeekPresent: () => true,
};
-52
View File
@@ -1,52 +0,0 @@
export const I18nServiceStub = {
t<T=string>(name:string):any {
return {
'date.abbr_day_names': [
'Mon',
'Tue',
'Wed',
'Thu',
'Fri',
'Sat',
'Sun',
],
'date.day_names': [
'Monday',
'Tuesday',
'Wednesday',
'Thursday',
'Friday',
'Saturday',
'Sunday',
],
'date.abbr_month_names': [
'Jan',
'Feb',
'Mar',
'Apr',
'May',
'Jun',
'Jul',
'Aug',
'Sep',
'Oct',
'Nov',
'Dec',
],
'date.month_names': [
'January',
'February',
'March',
'April',
'May',
'June',
'July',
'August',
'September',
'October',
'November',
'December',
],
}[name] || name as T;
},
};
@@ -1,2 +0,0 @@
export const TimezoneServiceStub = {
};
@@ -1,59 +0,0 @@
// -- copyright
// OpenProject is an open source project management software.
// Copyright (C) 2012-2023 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 {
Injectable,
} from '@angular/core';
import * as moment from 'moment';
import { IWeekday } from 'core-app/core/state/days/weekday.model';
import {
Observable,
of,
} from 'rxjs';
@Injectable({ providedIn: 'root' })
export class WeekdayServiceStub {
private weekdays:IWeekday[] = [];
/**
* @param date The iso day number (1-7) or a date instance
* @return {boolean} whether the given iso day is working or not
*/
public isNonWorkingDay(date:Date|number):boolean {
const isoDayOfWeek = (typeof date === 'number') ? date : moment(date).isoWeekday();
return !!(this.weekdays || []).find((wd) => wd.day === isoDayOfWeek && !wd.working);
}
public get nonWorkingDays():IWeekday[] {
return this.weekdays.filter((day) => !day.working);
}
loadWeekdays():Observable<IWeekday[]> {
return of(this.weekdays);
}
}