├── .gitattributes
├── .github
└── workflows
│ └── ci.yaml
├── .gitignore
├── .prettierignore
├── .prettierrc.json
├── .storybook
├── DocsContainer.js
├── customTheme.js
├── main.js
├── manager-head.html
├── preview.js
└── static
│ ├── favicon
│ ├── apple-touch-icon.png
│ ├── favicon-32x32.png
│ └── favicon.ico
│ ├── logo.png
│ ├── onyxiaLogo.png
│ └── repo-card.png
├── LICENSE
├── README.md
├── package.json
├── renovate.json
├── scripts
├── link-in-app.ts
├── link-in-test-app.ts
├── link-in-web.sh
└── tools
│ ├── crawl.ts
│ └── transformCodebase.ts
├── src
├── Alert.tsx
├── BaseBar.tsx
├── Breadcrumb.tsx
├── Button.tsx
├── ButtonBar.tsx
├── ButtonBarButton.tsx
├── Card.tsx
├── Checkbox.tsx
├── CircularProgress.tsx
├── CollapsibleSectionHeader.tsx
├── CollapsibleWrapper.tsx
├── CopyToClipboardIconButton.tsx
├── DarkModeSwitch.tsx
├── Dialog.tsx
├── DirectoryHeader.tsx
├── GitHubPicker.tsx
├── Icon.tsx
├── IconButton.tsx
├── LanguageSelect.tsx
├── LeftBar.tsx
├── Markdown.tsx
├── PageHeader.tsx
├── Picker.tsx
├── RangeSlider
│ ├── RangeSlider.tsx
│ ├── SimpleOrRangeSlider.tsx
│ └── index.ts
├── SearchBar.tsx
├── Slider.tsx
├── Tabs.tsx
├── Tag.tsx
├── Text.tsx
├── TextField.tsx
├── ThemedImage.tsx
├── ThemedSvg.tsx
├── Tooltip.tsx
├── assets
│ ├── fonts
│ │ ├── Marianne
│ │ │ ├── Marianne-Bold.woff2
│ │ │ ├── Marianne-Bold_Italic.woff2
│ │ │ ├── Marianne-Light.woff2
│ │ │ ├── Marianne-Light_Italic.woff2
│ │ │ ├── Marianne-Medium.woff2
│ │ │ ├── Marianne-Regular.woff2
│ │ │ ├── Marianne-Regular_Italic.woff2
│ │ │ └── font.css
│ │ └── WorkSans
│ │ │ ├── font.css
│ │ │ ├── worksans-bold-webfont.woff2
│ │ │ ├── worksans-medium-webfont.woff2
│ │ │ ├── worksans-regular-webfont.woff2
│ │ │ └── worksans-semibold-webfont.woff2
│ └── logo.svg
├── global.d.ts
├── lib
│ ├── OnyxiaUi.tsx
│ ├── SplashScreen.tsx
│ ├── ThemedAssetUrl.ts
│ ├── breakpoints.ts
│ ├── color.ts
│ ├── darkMode.ts
│ ├── icon.ts
│ ├── index.ts
│ ├── shadows.ts
│ ├── spacing.ts
│ ├── theme.ts
│ ├── tss.ts
│ └── typography.ts
├── stories
│ ├── Alert.stories.tsx
│ ├── Breadcrumb.stories.tsx
│ ├── Button.stories.tsx
│ ├── ButtonBar.stories.tsx
│ ├── ButtonBarButton.stories.tsx
│ ├── Card.stories.tsx
│ ├── Checkbox.stories.tsx
│ ├── CollapsibleSectionHeader.stories.tsx
│ ├── CopyToClipboardIconButton.stories.tsx
│ ├── DarkModeSwitch.stories.tsx
│ ├── Dialog.stories.tsx
│ ├── DirectoryHeader.stories.tsx
│ ├── GitHubPicker.stories.tsx
│ ├── Icon.stories.tsx
│ ├── IconButton.stories.tsx
│ ├── LanguageSelect.stories.tsx
│ ├── LeftBar.stories.tsx
│ ├── Markdown.stories.tsx
│ ├── PageHeader.stories.tsx
│ ├── Picker.stories.tsx
│ ├── RangeSlider.stories.tsx
│ ├── SearchBar.stories.tsx
│ ├── Slider.stories.tsx
│ ├── Tabs.stories.tsx
│ ├── Tag.stories.tsx
│ ├── TestSpacing.stories.tsx
│ ├── Text.stories.ts
│ ├── TextField.stories.tsx
│ ├── Tooltip.stories.tsx
│ ├── assets
│ │ ├── img
│ │ │ └── utilitr.png
│ │ └── svg
│ │ │ ├── Services.svg
│ │ │ ├── Tour.svg
│ │ │ └── account_v1.svg
│ ├── documentation
│ │ ├── Fundamentals
│ │ │ └── colors.stories.mdx
│ │ └── components
│ │ │ ├── Alert.stories.mdx
│ │ │ ├── Button.stories.mdx
│ │ │ ├── Checkbox.stories.mdx
│ │ │ ├── Tabs.stories.mdx
│ │ │ └── Textfield.stories.mdx
│ ├── emotionCache.ts
│ ├── getStory.tsx
│ ├── global.d.ts
│ ├── i18n.tsx
│ ├── index.stories.mdx
│ ├── sectionName.ts
│ ├── theme.tsx
│ └── tss.ts
└── tools
│ ├── LazySvg.tsx
│ ├── ReactComponent.ts
│ ├── evtRootFontSizePx.ts
│ ├── evtWindowInnerSize.ts
│ ├── getBrowser.ts
│ ├── getContrastRatio.ts
│ ├── getIsDarkModeEnabledOsDefault.ts
│ ├── getSafeUrl.ts
│ ├── memoize.ts
│ ├── noUndefined.ts
│ ├── pxToNumber.ts
│ └── useNonPostableEvtLike.ts
├── test-app
├── .gitignore
├── README.md
├── package.json
├── public
│ ├── favicon.ico
│ ├── index.html
│ ├── logo192.png
│ ├── logo512.png
│ ├── manifest.json
│ └── robots.txt
├── src
│ ├── MyComponent.tsx
│ ├── TextFormDialog.tsx
│ ├── assets
│ │ ├── bar.svg
│ │ └── foo.svg
│ ├── index.tsx
│ ├── react-app-env.d.ts
│ ├── theme.ts
│ └── tss.ts
├── tsconfig.json
└── yarn.lock
├── tsconfig.json
└── yarn.lock
/.gitattributes:
--------------------------------------------------------------------------------
1 | src/test/spa/**/* linguist-documentation
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Logs
2 | logs
3 | *.log
4 | npm-debug.log*
5 |
6 | # Runtime data
7 | pids
8 | *.pid
9 | *.seed
10 |
11 | # Directory for instrumented libs generated by jscoverage/JSCover
12 | lib-cov
13 |
14 | # Coverage directory used by tools like istanbul
15 | coverage
16 |
17 | # nyc test coverage
18 | .nyc_output
19 |
20 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files)
21 | .grunt
22 |
23 | # node-waf configuration
24 | .lock-wscript
25 |
26 | # Compiled binary addons (http://nodejs.org/api/addons.html)
27 | build/Release
28 |
29 | # Dependency directories
30 | node_modules
31 | jspm_packages
32 |
33 | # Optional npm cache directory
34 | .npm
35 |
36 | # Optional REPL history
37 | .node_repl_history
38 |
39 | .vscode
40 |
41 | .DS_Store
42 |
43 | /.yarn_home
44 | /dist
45 | /src/MuiIconComponentName.ts
--------------------------------------------------------------------------------
/.prettierignore:
--------------------------------------------------------------------------------
1 | node_modules/
2 | /dist/
3 | /.eslintrc.js
4 | /docs/
5 | /CHANGELOG.md
6 | /src/test/
7 | /.yarn_home/
8 | /src/MuiIconComponentName.ts
9 |
--------------------------------------------------------------------------------
/.prettierrc.json:
--------------------------------------------------------------------------------
1 | {
2 | "printWidth": 80,
3 | "tabWidth": 4,
4 | "useTabs": false,
5 | "semi": true,
6 | "singleQuote": false,
7 | "trailingComma": "all",
8 | "bracketSpacing": true,
9 | "arrowParens": "avoid"
10 | }
11 |
--------------------------------------------------------------------------------
/.storybook/DocsContainer.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { DocsContainer as BaseContainer } from "@storybook/addon-docs/blocks";
3 | import { useDarkMode } from "storybook-dark-mode";
4 | import { darkTheme, lightTheme } from "./customTheme";
5 |
6 | export const DocsContainer = ({ children, context }) => {
7 | const dark = useDarkMode();
8 |
9 | return (
10 | {
14 | const storyContext = context.storyById(id);
15 | return {
16 | ...storyContext,
17 | parameters: {
18 | ...storyContext?.parameters,
19 | docs: {
20 | ...storyContext?.parameters?.docs,
21 | theme: dark ? darkTheme : lightTheme,
22 | },
23 | },
24 | };
25 | },
26 | }}
27 | >
28 | {children}
29 |
30 | );
31 | };
32 |
--------------------------------------------------------------------------------
/.storybook/customTheme.js:
--------------------------------------------------------------------------------
1 | import { create } from "@storybook/theming";
2 |
3 | export const darkTheme = create({
4 | "base": "dark",
5 | "appBg": "#2c323f",
6 | "appContentBg": "#2c323f",
7 | "barBg": "#2c323f",
8 | "colorSecondary": "#ff562c",
9 | "textColor": "#f1f0eb",
10 | "brandImage": "onyxiaLogo.png",
11 | "brandTitle": "Onyxia UI",
12 | "brandUrl": "https://github.com/garronej/onyxia-ui",
13 | "fontBase": '"Work Sans","Open Sans", sans-serif',
14 | "fontCode": "monospace",
15 | });
16 |
17 | export const lightTheme = create({
18 | "base": "light",
19 | "appBg": "#f1f0eb",
20 | "appContentBg": "#f1f0eb",
21 | "barBg": "#f1f0eb",
22 | "colorSecondary": "#ff562c",
23 | "textColor": "#2c323f",
24 | "textInverseColor": "#f1f0eb",
25 | "brandImage": "onyxiaLogo.png",
26 | "brandTitle": "Onyxia UI",
27 | "brandUrl": "https://github.com/garronej/onyxia-ui",
28 | "fontBase": '"Work Sans","Open Sans", sans-serif',
29 | "fontCode": "monospace",
30 | });
31 |
--------------------------------------------------------------------------------
/.storybook/main.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | "stories": [
3 | "../src/stories/**/*.stories.mdx",
4 | "../src/stories/**/*.stories.@(ts|tsx)",
5 | ],
6 | "addons": [
7 | "@storybook/addon-links",
8 | "@storybook/addon-essentials",
9 | "@storybook/preset-create-react-app",
10 | "storybook-dark-mode",
11 | ],
12 | "core": {
13 | "builder": "webpack5",
14 | },
15 | };
16 |
--------------------------------------------------------------------------------
/.storybook/manager-head.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
Onyxia UI
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
--------------------------------------------------------------------------------
/.storybook/preview.js:
--------------------------------------------------------------------------------
1 | import { darkTheme, lightTheme } from "./customTheme";
2 | import { DocsContainer } from "./DocsContainer";
3 |
4 | export const parameters = {
5 | actions: { argTypesRegex: "^on[A-Z].*" },
6 | controls: {
7 | matchers: {
8 | color: /(background|color)$/i,
9 | date: /Date$/,
10 | },
11 | },
12 | "darkMode": {
13 | "light": lightTheme,
14 | "dark": darkTheme,
15 | },
16 | "docs": {
17 | container: DocsContainer,
18 | },
19 | "viewport": {
20 | "viewports": {
21 | "1440p": {
22 | "name": "1440p",
23 | "styles": {
24 | "width": "2560px",
25 | "height": "1440px",
26 | },
27 | },
28 | "fullHD": {
29 | "name": "Full HD",
30 | "styles": {
31 | "width": "1920px",
32 | "height": "1080px",
33 | },
34 | },
35 | "macBookProBig": {
36 | "name": "MacBook Pro Big",
37 | "styles": {
38 | "width": "1024px",
39 | "height": "640px",
40 | },
41 | },
42 | "macBookProMedium": {
43 | "name": "MacBook Pro Medium",
44 | "styles": {
45 | "width": "1440px",
46 | "height": "900px",
47 | },
48 | },
49 | "macBookProSmall": {
50 | "name": "MacBook Pro Small",
51 | "styles": {
52 | "width": "1680px",
53 | "height": "1050px",
54 | },
55 | },
56 | "pcINSEE": {
57 | "name": "PC Agent INSEE",
58 | "styles": {
59 | "width": "960px",
60 | "height": "540px",
61 | },
62 | },
63 | "verySmallLandscape": {
64 | "name": "Very small landscape",
65 | "styles": {
66 | "width": "599px",
67 | "height": "337px",
68 | },
69 | },
70 | },
71 | },
72 | "options": {
73 | "storySort": (a, b) =>
74 | getHardCodedWeight(b[1].kind) - getHardCodedWeight(a[1].kind),
75 | },
76 | };
77 |
78 | const { getHardCodedWeight } = (() => {
79 | //TODO: Address this
80 | const mainServices = [
81 | "documentation/Fundamentals/Colors",
82 | "documentation/Components/Button",
83 | "documentation/Components/Alert",
84 | ];
85 |
86 | function getHardCodedWeight(kind) {
87 | for (let i = 0; i < mainServices.length; i++) {
88 | if (kind.toLowerCase().includes(mainServices[i].toLowerCase())) {
89 | return mainServices.length - i;
90 | }
91 | }
92 |
93 | return 0;
94 | }
95 |
96 | return { getHardCodedWeight };
97 | })();
98 |
--------------------------------------------------------------------------------
/.storybook/static/favicon/apple-touch-icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/InseeFrLab/onyxia-ui/18896e242dd81be703befc58c51811e477ac406f/.storybook/static/favicon/apple-touch-icon.png
--------------------------------------------------------------------------------
/.storybook/static/favicon/favicon-32x32.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/InseeFrLab/onyxia-ui/18896e242dd81be703befc58c51811e477ac406f/.storybook/static/favicon/favicon-32x32.png
--------------------------------------------------------------------------------
/.storybook/static/favicon/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/InseeFrLab/onyxia-ui/18896e242dd81be703befc58c51811e477ac406f/.storybook/static/favicon/favicon.ico
--------------------------------------------------------------------------------
/.storybook/static/logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/InseeFrLab/onyxia-ui/18896e242dd81be703befc58c51811e477ac406f/.storybook/static/logo.png
--------------------------------------------------------------------------------
/.storybook/static/onyxiaLogo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/InseeFrLab/onyxia-ui/18896e242dd81be703befc58c51811e477ac406f/.storybook/static/onyxiaLogo.png
--------------------------------------------------------------------------------
/.storybook/static/repo-card.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/InseeFrLab/onyxia-ui/18896e242dd81be703befc58c51811e477ac406f/.storybook/static/repo-card.png
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2020 GitHub user u/garronej
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "onyxia-ui",
3 | "version": "6.4.0",
4 | "description": "The Onyxia UI toolkit",
5 | "repository": {
6 | "type": "git",
7 | "url": "git://github.com/InseeFrLab/onyxia-ui.git"
8 | },
9 | "main": "dist/lib/index.js",
10 | "types": "dist/lib/index.d.ts",
11 | "scripts": {
12 | "build": "tsc && cpx \"src/assets/**/*.{svg,css,woff2}\" dist/assets/",
13 | "_format": "prettier '**/*.{ts,tsx,json,md}'",
14 | "format": "npm run _format -- --write",
15 | "format:check": "npm run _format -- --list-different",
16 | "storybook": "start-storybook -p 6006 --static-dir ./.storybook/static",
17 | "link-in-web": "bash scripts/link-in-web.sh",
18 | "start-test-app": "yarn build && ts-node --skipProject scripts/link-in-test-app.ts && cd test-app && rm -rf node_modules/.cache && yarn start"
19 | },
20 | "lint-staged": {
21 | "*.{ts,tsx,json,md}": [
22 | "prettier --write"
23 | ]
24 | },
25 | "husky": {
26 | "hooks": {
27 | "pre-commit": "lint-staged -v"
28 | }
29 | },
30 | "author": "u/garronej",
31 | "license": "MIT",
32 | "files": [
33 | "src/",
34 | "!src/stories/",
35 | "dist/",
36 | "!dist/package.json",
37 | "!dist/tsconfig.tsbuildinfo"
38 | ],
39 | "keywords": [],
40 | "peerDependencies": {
41 | "@emotion/react": "^11.0.0",
42 | "@mui/material": "^6.1.7",
43 | "@types/react": "^17.0.0 || ^18.0.0",
44 | "react": "^17.0.0 || ^18.0.0"
45 | },
46 | "peerDependenciesMeta": {
47 | "@types/react": {
48 | "optional": true
49 | }
50 | },
51 | "dependencies": {
52 | "@mui/icons-material": "^6.1.7",
53 | "evt": "^2.5.8",
54 | "memoizee": "^0.4.17",
55 | "@types/memoizee": "^0.4.11",
56 | "minimal-polyfills": "^2.2.3",
57 | "powerhooks": "^2.0.1",
58 | "react-markdown": "^9.0.1",
59 | "rehype-raw": "^7.0.0",
60 | "run-exclusive": "^2.2.19",
61 | "tsafe": "^1.8.5",
62 | "tss-react": "^4.9.13"
63 | },
64 | "devDependencies": {
65 | "@emotion/cache": "^11.11.0",
66 | "@emotion/react": "^11.11.1",
67 | "@emotion/styled": "^11.11.0",
68 | "@mui/material": "^6.1.7",
69 | "@mui/x-data-grid": "^7.22.3",
70 | "@storybook/addon-actions": "^6.5.9",
71 | "@storybook/addon-essentials": "^6.5.9",
72 | "@storybook/addon-links": "^6.5.9",
73 | "@storybook/addons": "^6.5.9",
74 | "@storybook/builder-webpack5": "^6.5.9",
75 | "@storybook/manager-webpack5": "^6.5.9",
76 | "@storybook/node-logger": "^6.5.9",
77 | "@storybook/preset-create-react-app": "^4.1.2",
78 | "@storybook/react": "^6.5.9",
79 | "@storybook/theming": "^6.5.9",
80 | "@types/color": "^3.0.3",
81 | "@types/dompurify": "^3.0.5",
82 | "@types/make-fetch-happen": "^10.0.3",
83 | "@types/node": "^17.0.24",
84 | "@types/react": "^18.0.14",
85 | "@types/react-dom": "^18.0.5",
86 | "@types/yauzl": "^2.10.2",
87 | "@types/yazl": "^2.4.4",
88 | "cpx": "^1.5.0",
89 | "husky": "^4.3.8",
90 | "i18nifty": "^1.3.6",
91 | "lint-staged": "^11.0.0",
92 | "prettier": "^2.3.0",
93 | "react": "^18.3.1",
94 | "react-dom": "^18.2.0",
95 | "react-scripts": "5.0.1",
96 | "storybook-dark-mode": "^1.1.0",
97 | "ts-node": "^10.3.0",
98 | "typescript": "^4.7.4",
99 | "webpack": "^5.73.0"
100 | }
101 | }
102 |
--------------------------------------------------------------------------------
/renovate.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "https://docs.renovatebot.com/renovate-schema.json",
3 | "baseBranches": ["main"],
4 | "extends": ["config:base"],
5 | "dependencyDashboard": false,
6 | "bumpVersion": "patch",
7 | "rangeStrategy": "bump",
8 | "ignorePaths": [".github/**"],
9 | "branchPrefix": "renovate_",
10 | "vulnerabilityAlerts": {
11 | "enabled": false
12 | },
13 | "packageRules": [
14 | {
15 | "packagePatterns": ["*"],
16 | "excludePackagePatterns": [
17 | "evt",
18 | "minimal-polyfills",
19 | "powerhooks",
20 | "run-exclusive",
21 | "tsafe",
22 | "tss-react"
23 | ],
24 | "enabled": false
25 | },
26 | {
27 | "packagePatterns": [
28 | "evt",
29 | "minimal-polyfills",
30 | "powerhooks",
31 | "run-exclusive",
32 | "tsafe",
33 | "tss-react"
34 | ],
35 | "matchUpdateTypes": ["minor", "patch"],
36 | "automerge": true,
37 | "automergeType": "pr",
38 | "platformAutomerge": true,
39 | "groupName": "garronej_modules_update"
40 | }
41 | ]
42 | }
43 |
--------------------------------------------------------------------------------
/scripts/link-in-web.sh:
--------------------------------------------------------------------------------
1 |
2 | rm -rf node_modules
3 | yarn
4 | yarn build
5 |
6 | DIR=$(pwd)
7 |
8 | cd ../web
9 | rm -rf node_modules
10 | yarn
11 | cd $DIR
12 | npx ts-node --skipProject scripts/link-in-app.ts web
13 |
14 | npx tsc -w
--------------------------------------------------------------------------------
/scripts/tools/crawl.ts:
--------------------------------------------------------------------------------
1 | import * as fs from "fs";
2 | import * as path from "path";
3 |
4 | const crawlRec = (dir_path: string, paths: string[]) => {
5 | for (const file_name of fs.readdirSync(dir_path)) {
6 | const file_path = path.join(dir_path, file_name);
7 |
8 | if (fs.lstatSync(file_path).isDirectory()) {
9 | crawlRec(file_path, paths);
10 |
11 | continue;
12 | }
13 |
14 | paths.push(file_path);
15 | }
16 | };
17 |
18 | /** List all files in a given directory return paths relative to the dir_path */
19 | export function crawl(params: {
20 | dirPath: string;
21 | returnedPathsType: "absolute" | "relative to dirPath";
22 | }): string[] {
23 | const { dirPath, returnedPathsType } = params;
24 |
25 | const filePaths: string[] = [];
26 |
27 | crawlRec(dirPath, filePaths);
28 |
29 | switch (returnedPathsType) {
30 | case "absolute":
31 | return filePaths;
32 | case "relative to dirPath":
33 | return filePaths.map(filePath => path.relative(dirPath, filePath));
34 | }
35 | }
36 |
--------------------------------------------------------------------------------
/scripts/tools/transformCodebase.ts:
--------------------------------------------------------------------------------
1 | import * as fs from "fs";
2 | import * as path from "path";
3 | import { crawl } from "./crawl";
4 | import { id } from "tsafe/id";
5 |
6 | type TransformSourceCode = (params: {
7 | sourceCode: Buffer;
8 | filePath: string;
9 | fileRelativePath: string;
10 | }) =>
11 | | {
12 | modifiedSourceCode: Buffer;
13 | newFileName?: string;
14 | }
15 | | undefined;
16 |
17 | /** Apply a transformation function to every file of directory */
18 | export function transformCodebase(params: {
19 | srcDirPath: string;
20 | destDirPath: string;
21 | transformSourceCode?: TransformSourceCode;
22 | }) {
23 | const {
24 | srcDirPath,
25 | destDirPath,
26 | transformSourceCode = id(({ sourceCode }) => ({
27 | modifiedSourceCode: sourceCode,
28 | })),
29 | } = params;
30 |
31 | for (const fileRelativePath of crawl({
32 | dirPath: srcDirPath,
33 | returnedPathsType: "relative to dirPath",
34 | })) {
35 | const filePath = path.join(srcDirPath, fileRelativePath);
36 |
37 | const transformSourceCodeResult = transformSourceCode({
38 | sourceCode: fs.readFileSync(filePath),
39 | filePath,
40 | fileRelativePath,
41 | });
42 |
43 | if (transformSourceCodeResult === undefined) {
44 | continue;
45 | }
46 |
47 | fs.mkdirSync(path.dirname(path.join(destDirPath, fileRelativePath)), {
48 | recursive: true,
49 | });
50 |
51 | const { newFileName, modifiedSourceCode } = transformSourceCodeResult;
52 |
53 | fs.writeFileSync(
54 | path.join(
55 | path.dirname(path.join(destDirPath, fileRelativePath)),
56 | newFileName ?? path.basename(fileRelativePath),
57 | ),
58 | modifiedSourceCode,
59 | );
60 | }
61 | }
62 |
--------------------------------------------------------------------------------
/src/Alert.tsx:
--------------------------------------------------------------------------------
1 | import { useReducer, memo, forwardRef } from "react";
2 | import type { ReactNode } from "react";
3 | import MuiAlert from "@mui/material/Alert";
4 | import { Text } from "./Text";
5 | import { tss } from "./lib/tss";
6 | import { symToStr } from "tsafe/symToStr";
7 | import { IconButton } from "./IconButton";
8 | import CloseSharpIcon from "@mui/icons-material/CloseSharp";
9 |
10 | export type AlertProps =
11 | | AlertProps.NonClosable
12 | | AlertProps.ClosableControlled
13 | | AlertProps.ClosableUncontrolled;
14 |
15 | export namespace AlertProps {
16 | export type Common = {
17 | className?: string;
18 | classes?: Partial["classes"]>;
19 | severity: "warning" | "info" | "error" | "success";
20 | children: NonNullable;
21 | };
22 |
23 | export type NonClosable = Common;
24 |
25 | export type ClosableUncontrolled = Common & {
26 | doDisplayCross: true;
27 | onClose?: () => void;
28 | };
29 |
30 | export type ClosableControlled = Common & {
31 | doDisplayCross: true;
32 | isClosed: boolean;
33 | onClose: () => void;
34 | };
35 | }
36 |
37 | export const Alert = memo(
38 | forwardRef((props, ref) => {
39 | const { severity, children, className, ...rest } = props;
40 |
41 | const { classes, cx } = useStyles({
42 | severity,
43 | classesOverrides: props.classes,
44 | });
45 |
46 | const { isClosed, uncontrolledClose } = (function useClosure() {
47 | const [isClosed, uncontrolledClose] = useReducer(() => true, false);
48 |
49 | return {
50 | isClosed: "isClosed" in rest ? rest.isClosed : isClosed,
51 | uncontrolledClose,
52 | };
53 | })();
54 |
55 | if (isClosed) {
56 | return null;
57 | }
58 |
59 | return (
60 | {
78 | rest.onClose?.();
79 | uncontrolledClose();
80 | }
81 | }
82 | />
83 | )
84 | }
85 | >
86 | {typeof children === "string" ? (
87 | {children}
88 | ) : (
89 | children
90 | )}
91 |
92 | );
93 | }),
94 | );
95 |
96 | Alert.displayName = symToStr({ Alert });
97 |
98 | const useStyles = tss
99 | .withName({ Alert })
100 | .withParams<{
101 | severity: AlertProps["severity"];
102 | }>()
103 | .create(({ theme, severity }) => ({
104 | root: {
105 | alignItems: "center",
106 | color: theme.colors.useCases.typography.textPrimary,
107 | backgroundColor:
108 | theme.colors.useCases.alertSeverity[severity].background,
109 | },
110 | icon: {
111 | "& svg": {
112 | color: theme.colors.useCases.alertSeverity[severity].main,
113 | },
114 | },
115 | action: {
116 | alignItems: "center",
117 | padding: 0,
118 | },
119 | }));
120 |
--------------------------------------------------------------------------------
/src/BaseBar.tsx:
--------------------------------------------------------------------------------
1 | import { ReactNode } from "react";
2 | import { tss } from "./lib/tss";
3 | import { symToStr } from "tsafe/symToStr";
4 |
5 | export type BaseBarProps = {
6 | className?: string;
7 | children: ReactNode;
8 | };
9 |
10 | export function BaseBar(props: BaseBarProps) {
11 | const { className, children } = props;
12 |
13 | const { classes, cx } = useStyles();
14 |
15 | return {children}
;
16 | }
17 |
18 | BaseBar.displayName = symToStr({ BaseBar });
19 |
20 | const useStyles = tss.withName({ BaseBar }).create(({ theme }) => ({
21 | root: {
22 | backgroundColor: theme.colors.useCases.surfaces.surface1,
23 | boxShadow: theme.shadows[1],
24 | borderRadius: 8,
25 | overflow: "hidden",
26 | },
27 | }));
28 |
--------------------------------------------------------------------------------
/src/ButtonBar.tsx:
--------------------------------------------------------------------------------
1 | import { memo, ReactNode } from "react";
2 | import { useCallbackFactory } from "powerhooks/useCallbackFactory";
3 | import { ButtonBarButton } from "./ButtonBarButton";
4 | import { symToStr } from "tsafe/symToStr";
5 | import { BaseBar } from "./BaseBar";
6 | import type { IconProps } from "./Icon";
7 |
8 | export type ButtonBarProps = {
9 | className?: string;
10 | buttons: Readonly[]>;
11 | onClick: (buttonId: ButtonId) => void;
12 | };
13 |
14 | export namespace ButtonBarProps {
15 | export type Button =
16 | | Button.Callback
17 | | Button.Link;
18 |
19 | export namespace Button {
20 | type Common = {
21 | icon: IconProps.Icon;
22 | label: ReactNode;
23 | isDisabled?: boolean;
24 | };
25 |
26 | export type Callback = Common & {
27 | buttonId: ButtonId;
28 | };
29 |
30 | export type Link = Common & {
31 | link: {
32 | href: string;
33 | onClick?: (event?: any) => void;
34 | target?: "_blank";
35 | };
36 | };
37 | }
38 | }
39 |
40 | function NonMemoizedButtonBar(
41 | props: ButtonBarProps,
42 | ) {
43 | const { className, buttons, onClick } = props;
44 |
45 | const onClickFactory = useCallbackFactory(([buttonId]: [ButtonId]) =>
46 | onClick(buttonId),
47 | );
48 |
49 | return (
50 |
51 | {buttons.map(button => (
52 |
67 | {button.label}
68 |
69 | ))}
70 |
71 | );
72 | }
73 |
74 | export const ButtonBar = memo(NonMemoizedButtonBar) as <
75 | ButtonId extends string = never,
76 | >(
77 | props: ButtonBarProps,
78 | ) => ReturnType;
79 |
80 | (ButtonBar as any).displayName = symToStr({ ButtonBar });
81 |
--------------------------------------------------------------------------------
/src/ButtonBarButton.tsx:
--------------------------------------------------------------------------------
1 | import type { ReactNode } from "react";
2 | import { forwardRef, memo } from "react";
3 | import { tss } from "./lib/tss";
4 | import { Button } from "./Button";
5 | import { symToStr } from "tsafe/symToStr";
6 | import type { IconProps } from "./Icon";
7 |
8 | export type ButtonBarButtonProps =
9 | | ButtonBarButtonProps.Regular
10 | | ButtonBarButtonProps.Submit;
11 |
12 | export namespace ButtonBarButtonProps {
13 | type Common = {
14 | className?: string;
15 | startIcon?: IconProps.Icon;
16 | disabled?: boolean;
17 | children: ReactNode;
18 | };
19 |
20 | export type Regular = Common & {
21 | onClick?: (e: React.MouseEvent) => void;
22 | href?: string;
23 | /** Defaults to true if href is defined */
24 | doOpenNewTabIfHref?: boolean;
25 | };
26 |
27 | export type Submit = Common & {
28 | type: "submit";
29 | };
30 | }
31 |
32 | export const ButtonBarButton = memo(
33 | forwardRef((props, ref) => {
34 | const { className, startIcon, disabled, children, ...rest } = props;
35 |
36 | const { classes, cx } = useStyles();
37 |
38 | return (
39 |
49 | );
50 | }),
51 | );
52 |
53 | ButtonBarButton.displayName = symToStr({ ButtonBarButton });
54 |
55 | const useStyles = tss.withName({ ButtonBarButton }).create(({ theme }) => ({
56 | root: {
57 | backgroundColor: "transparent",
58 | borderRadius: "unset",
59 | borderColor: "transparent",
60 | transition: "none",
61 | "& > *": {
62 | transition: "none",
63 | },
64 | "&:hover.MuiButton-text": {
65 | color: theme.colors.useCases.typography.textPrimary,
66 | borderBottomColor: theme.colors.useCases.buttons.actionActive,
67 | boxSizing: "border-box",
68 | backgroundColor: "unset",
69 | "& .MuiSvgIcon-root": {
70 | color: theme.colors.useCases.typography.textPrimary,
71 | },
72 | },
73 | "&:active.MuiButton-text": {
74 | color: theme.colors.useCases.typography.textFocus,
75 | "& .MuiSvgIcon-root": {
76 | color: theme.colors.useCases.typography.textFocus,
77 | },
78 | },
79 | },
80 | }));
81 |
--------------------------------------------------------------------------------
/src/Card.tsx:
--------------------------------------------------------------------------------
1 | import type { ReactNode } from "react";
2 | import { forwardRef, memo } from "react";
3 | import { tss } from "./lib/tss";
4 | import { assert } from "tsafe/assert";
5 | import type { Equals } from "tsafe";
6 |
7 | export type CardProps = {
8 | className?: string;
9 | aboveDivider?: ReactNode;
10 | children: ReactNode;
11 | };
12 |
13 | export const Card = memo(
14 | forwardRef((props, ref) => {
15 | const {
16 | className,
17 | aboveDivider,
18 | children,
19 | //For the forwarding, rest should be empty (typewise)
20 | ...rest
21 | } = props;
22 |
23 | //For the forwarding, rest should be empty (typewise),
24 | assert>();
25 |
26 | const { classes, cx } = useStyles();
27 |
28 | return (
29 |
30 | {aboveDivider !== undefined && (
31 |
{aboveDivider}
32 | )}
33 |
{children}
34 |
35 | );
36 | }),
37 | );
38 |
39 | const useStyles = tss.withName({ Card }).create(({ theme }) => ({
40 | root: {
41 | borderRadius: 8,
42 | boxShadow: theme.shadows[1],
43 | backgroundColor: theme.colors.useCases.surfaces.surface1,
44 | "&:hover": {
45 | boxShadow: theme.shadows[6],
46 | },
47 | display: "flex",
48 | flexDirection: "column",
49 | },
50 | aboveDivider: {
51 | padding: theme.spacing({ topBottom: 3, rightLeft: 4 }),
52 | borderBottom: `1px solid ${theme.colors.useCases.typography.textTertiary}`,
53 | boxSizing: "border-box",
54 | },
55 | belowDivider: {
56 | padding: theme.spacing(4),
57 | paddingTop: theme.spacing(3),
58 | flex: 1,
59 | display: "flex",
60 | flexDirection: "column",
61 | },
62 | }));
63 |
--------------------------------------------------------------------------------
/src/Checkbox.tsx:
--------------------------------------------------------------------------------
1 | import { useState, useEffect, memo } from "react";
2 | import MuiCheckbox from "@mui/material/Checkbox";
3 | import type { CheckboxProps as MuiCheckboxProps } from "@mui/material/Checkbox";
4 | import { useConstCallback } from "powerhooks/useConstCallback";
5 | import { symToStr } from "tsafe/symToStr";
6 |
7 | export type CheckboxProps = MuiCheckboxProps;
8 |
9 | export const Checkbox = memo((props: CheckboxProps) => {
10 | const { defaultChecked: props_defaultChecked, ...rest } = props;
11 |
12 | const defaultChecked =
13 | rest.checked === undefined ? false : props_defaultChecked ?? false;
14 |
15 | const [isChecked, setIsChecked] = useState(defaultChecked);
16 |
17 | useEffect(() => setIsChecked(defaultChecked), [defaultChecked]);
18 |
19 | const onChange = useConstCallback(
20 | (event, checked) => {
21 | setIsChecked(checked);
22 |
23 | rest.onChange?.(event, checked);
24 | },
25 | );
26 |
27 | return (
28 |
40 | );
41 | });
42 |
43 | Checkbox.displayName = symToStr({ Checkbox });
44 |
--------------------------------------------------------------------------------
/src/CircularProgress.tsx:
--------------------------------------------------------------------------------
1 | import { tss } from "./lib/tss";
2 | import { memo } from "react";
3 | import MuiCircularProgress from "@mui/material/CircularProgress";
4 |
5 | export type CircularProgressProps = {
6 | className?: string;
7 | size?: string | number;
8 | color?: "primary" | "textPrimary";
9 | };
10 |
11 | export const CircularProgress = memo((props: CircularProgressProps) => {
12 | const { className, size = 40, color = "primary" } = props;
13 |
14 | const { classes, cx } = useStyles({ color });
15 |
16 | return (
17 |
22 | );
23 | });
24 |
25 | const useStyles = tss
26 | .withName({ CircularProgress })
27 | .withParams, "color">>()
28 | .create(({ theme, color }) => ({
29 | root: {
30 | color:
31 | color !== "textPrimary"
32 | ? undefined
33 | : theme.colors.useCases.typography.textPrimary,
34 | },
35 | }));
36 |
--------------------------------------------------------------------------------
/src/CollapsibleSectionHeader.tsx:
--------------------------------------------------------------------------------
1 | import { memo, type ReactNode } from "react";
2 | import { tss } from "./lib/tss";
3 | import { Text } from "./Text";
4 | import MuiLink from "@mui/material/Link";
5 | import { pxToNumber } from "./tools/pxToNumber";
6 | import { IconButton } from "./IconButton";
7 | import { symToStr } from "tsafe/symToStr";
8 | import ChevronLeftIcon from "@mui/icons-material/ChevronLeft";
9 |
10 | export type CollapsibleSectionHeaderProps = {
11 | className?: string;
12 | isCollapsed: boolean;
13 | onToggleIsCollapsed(): void;
14 | title: ReactNode;
15 | total?: number;
16 | /** Default "Show all", provide your own string for internationalization. */
17 | showAllStr?: string;
18 | };
19 |
20 | export const CollapsibleSectionHeader = memo(
21 | (props: CollapsibleSectionHeaderProps) => {
22 | const {
23 | className,
24 | title,
25 | isCollapsed,
26 | onToggleIsCollapsed,
27 | total,
28 | showAllStr = "Show all",
29 | } = props;
30 |
31 | const { classes, cx } = useStyles({ isCollapsed });
32 |
33 | return (
34 |
35 |
41 |
{title}
42 |
43 | {isCollapsed && (
44 |
49 | {showAllStr}
50 | {total !== undefined && ({total})}
51 |
52 | )}
53 |
54 | );
55 | },
56 | );
57 |
58 | CollapsibleSectionHeader.displayName = symToStr({ CollapsibleSectionHeader });
59 |
60 | const useStyles = tss
61 | .withName({ CollapsibleSectionHeader })
62 | .withParams<{ isCollapsed: boolean }>()
63 | .create(({ theme, isCollapsed }) => ({
64 | root: {
65 | display: "flex",
66 | alignItems: "center",
67 | },
68 | chevron: {
69 | paddingLeft: 0,
70 | ...(!isCollapsed
71 | ? {}
72 | : {
73 | width: 0,
74 | paddingLeft: 0,
75 | paddingRight: 0,
76 | visibility: "hidden",
77 | }),
78 | },
79 | link: {
80 | cursor: "pointer",
81 | //Ugly solution to vertically align with text
82 | paddingTop:
83 | 0.183 *
84 | pxToNumber(
85 | theme.typography.variants["section heading"].style
86 | .lineHeight,
87 | ),
88 | },
89 | }));
90 |
--------------------------------------------------------------------------------
/src/CollapsibleWrapper.tsx:
--------------------------------------------------------------------------------
1 | import { useEffect, useReducer, useRef, memo } from "react";
2 | import type { RefObject } from "react";
3 | import { useDomRect } from "powerhooks/useDomRect";
4 | import type { ReactNode } from "react";
5 | import { useGuaranteedMemo } from "powerhooks/useGuaranteedMemo";
6 | import { assert } from "tsafe/assert";
7 | import { useStyles } from "tss-react";
8 |
9 | export type CollapseParams =
10 | | CollapseParams.Controlled
11 | | CollapseParams.CollapsesOnScroll;
12 | export namespace CollapseParams {
13 | export type Common = {
14 | /** Default 250ms */
15 | transitionDuration?: number;
16 | };
17 |
18 | export type Controlled = Common & {
19 | behavior: "controlled";
20 | isCollapsed: boolean;
21 | };
22 |
23 | export type CollapsesOnScroll = Common & {
24 | behavior: "collapses on scroll";
25 | scrollTopThreshold: number;
26 | // NOTE: If not provided assume window (body)
27 | scrollableElementRef?: RefObject;
28 | onIsCollapsedValueChange?: (isCollapsed: boolean) => void;
29 | };
30 | }
31 |
32 | export type CollapsibleWrapperProps = {
33 | className?: string;
34 | children: ReactNode;
35 | } & CollapseParams;
36 |
37 | export const CollapsibleWrapper = memo((props: CollapsibleWrapperProps) => {
38 | const { className, transitionDuration = 250, children, ...rest } = props;
39 |
40 | const {
41 | ref: childrenWrapperRef,
42 | domRect: { height: childrenWrapperHeight },
43 | } = useDomRect();
44 |
45 | const { css, cx } = useStyles();
46 |
47 | //We use a ref instead of a state because we want to be able to
48 | //synchronously reset the state when the div that scrolls have been changed
49 | const isCollapsedIfDependsOfScrollRef = useRef(false);
50 |
51 | useGuaranteedMemo(() => {
52 | isCollapsedIfDependsOfScrollRef.current = false;
53 | }, [
54 | rest.behavior === "collapses on scroll"
55 | ? rest.scrollableElementRef?.current
56 | : undefined,
57 | ]);
58 |
59 | useEffect(() => {
60 | if (rest.behavior !== "collapses on scroll") {
61 | return;
62 | }
63 |
64 | rest.onIsCollapsedValueChange?.(
65 | isCollapsedIfDependsOfScrollRef.current,
66 | );
67 | }, [isCollapsedIfDependsOfScrollRef.current]);
68 |
69 | {
70 | const ref =
71 | rest.behavior !== "collapses on scroll"
72 | ? undefined
73 | : rest.scrollableElementRef ?? { current: window };
74 |
75 | const [, forceUpdate] = useReducer(counter => counter + 1, 0);
76 |
77 | useEffect(() => {
78 | if (ref === undefined) {
79 | return;
80 | }
81 |
82 | assert(rest.behavior === "collapses on scroll");
83 |
84 | const element = ref.current;
85 |
86 | if (!element) {
87 | return;
88 | }
89 |
90 | const { scrollTopThreshold } = rest;
91 |
92 | const onScroll = (event: Event) => {
93 | const scrollTop =
94 | element === window
95 | ? window.scrollY
96 | : (event.target as HTMLElement).scrollTop;
97 |
98 | isCollapsedIfDependsOfScrollRef.current =
99 | isCollapsedIfDependsOfScrollRef.current
100 | ? scrollTop + childrenWrapperHeight * 1.3 >
101 | scrollTopThreshold
102 | : scrollTop > scrollTopThreshold;
103 |
104 | forceUpdate();
105 | };
106 |
107 | element.addEventListener("scroll", onScroll);
108 |
109 | return () => {
110 | element.removeEventListener("scroll", onScroll);
111 | };
112 | }, [
113 | rest.behavior,
114 | ref?.current ?? undefined,
115 | ...(rest.behavior !== "collapses on scroll"
116 | ? [null, null, null]
117 | : [
118 | rest.scrollTopThreshold,
119 | rest.scrollableElementRef,
120 | childrenWrapperHeight,
121 | ]),
122 | ]);
123 | }
124 |
125 | const isCollapsed = (() => {
126 | switch (rest.behavior) {
127 | case "collapses on scroll":
128 | return isCollapsedIfDependsOfScrollRef.current;
129 | case "controlled":
130 | return rest.isCollapsed;
131 | }
132 | })();
133 |
134 | return (
135 | `${prop} ${transitionDuration}ms`)
144 | .join(", "),
145 | overflow: "hidden",
146 | }),
147 | className,
148 | )}
149 | >
150 |
{children}
151 |
152 | );
153 | });
154 |
--------------------------------------------------------------------------------
/src/CopyToClipboardIconButton.tsx:
--------------------------------------------------------------------------------
1 | import { useState, memo } from "react";
2 | import { useConstCallback } from "powerhooks/useConstCallback";
3 | import { Tooltip } from "./Tooltip";
4 | import { IconButton } from "./IconButton";
5 | import { tss } from "./lib/tss";
6 | import CheckIcon from "@mui/icons-material/Check";
7 | import FilterNoneIcon from "@mui/icons-material/FilterNone";
8 |
9 | type Props = {
10 | className?: string;
11 | copyToClipboardText?: string;
12 | copiedToClipboardText?: string;
13 | textToCopy: string;
14 | /** Default: false */
15 | disabled?: boolean;
16 | };
17 |
18 | export const CopyToClipboardIconButton = memo((props: Props) => {
19 | const {
20 | className,
21 | textToCopy,
22 | copiedToClipboardText = "Copied!",
23 | copyToClipboardText = "Copy to clipboard",
24 | disabled = false,
25 | } = props;
26 |
27 | const { isCopyFeedbackOn, onClick } = (function useClosure() {
28 | const [isCopyFeedbackOn, setIsCopyFeedbackOn] = useState(false);
29 |
30 | const onClick = useConstCallback(() => {
31 | navigator.clipboard.writeText(textToCopy);
32 |
33 | (async () => {
34 | setIsCopyFeedbackOn(true);
35 |
36 | await new Promise(resolve => setTimeout(resolve, 1000));
37 |
38 | setIsCopyFeedbackOn(false);
39 | })();
40 | });
41 |
42 | return { isCopyFeedbackOn, onClick };
43 | })();
44 |
45 | const { classes, cx } = useStyles({ isCopyFeedbackOn });
46 |
47 | const size = "small";
48 |
49 | return (
50 |
55 |
62 |
63 | );
64 | });
65 |
66 | const useStyles = tss
67 | .withName({ CopyToClipboardIconButton })
68 | .withParams<{ isCopyFeedbackOn: boolean }>()
69 | .create(({ theme, isCopyFeedbackOn }) => ({
70 | root: {
71 | "&& svg": {
72 | color: isCopyFeedbackOn
73 | ? theme.colors.useCases.alertSeverity.success.main
74 | : undefined,
75 | },
76 | },
77 | }));
78 |
--------------------------------------------------------------------------------
/src/DarkModeSwitch.tsx:
--------------------------------------------------------------------------------
1 | import { memo } from "react";
2 | import { useConstCallback } from "powerhooks/useConstCallback";
3 | import { useDarkMode } from "./lib";
4 | import type { IconProps } from "./Icon";
5 | import { tss } from "./lib/tss";
6 | import { IconButton } from "./IconButton";
7 | import { symToStr } from "tsafe/symToStr";
8 | import Brightness7Icon from "@mui/icons-material/Brightness7";
9 | import Brightness4Icon from "@mui/icons-material/Brightness4";
10 |
11 | export type DarkModeSwitchProps = {
12 | className?: string;
13 | /** Default: default */
14 | size?: IconProps["size"];
15 | ariaLabel?: string;
16 | };
17 |
18 | export const DarkModeSwitch = memo((props: DarkModeSwitchProps) => {
19 | const { className, size, ariaLabel } = props;
20 | const { isDarkModeEnabled, setIsDarkModeEnabled } = useDarkMode();
21 |
22 | const onClick = useConstCallback(() => {
23 | setIsDarkModeEnabled(!isDarkModeEnabled);
24 | });
25 |
26 | const { classes, cx } = useStyles();
27 |
28 | return (
29 |
36 | );
37 | });
38 |
39 | DarkModeSwitch.displayName = symToStr({ DarkModeSwitch });
40 |
41 | const useStyles = tss.withName({ DarkModeSwitch }).create(({ theme }) => ({
42 | root: {
43 | transition: "transform 500ms",
44 | transform: `rotate(${theme.isDarkModeEnabled ? 180 : 0}deg)`,
45 | transitionTimingFunction: "cubic-bezier(.34,1.27,1,1)",
46 | },
47 | }));
48 |
--------------------------------------------------------------------------------
/src/DirectoryHeader.tsx:
--------------------------------------------------------------------------------
1 | import type { ReactNode } from "react";
2 | import { tss } from "./lib/tss";
3 | import { Text } from "./Text";
4 | import { memo } from "react";
5 | import { pxToNumber } from "./tools/pxToNumber";
6 | import { IconButton } from "./IconButton";
7 | import ChevronLeftIcon from "@mui/icons-material/ChevronLeft";
8 |
9 | /**
10 | * image:
11 | *
12 | * If you pass an (always square) make sure to set width and height: 100%
13 | *
14 | * If it's an SVG define the CSS as follow
15 | * Put "100%" on the BIGGER dimension and "unset" on the other. (You can tell by the viewBox)
16 | *
17 | * e.g:
18 | *
19 | * -----
20 | * | |
21 | * | |
22 | * | |
23 | * -----
24 | *
25 | * "height": "100%",
26 | * "width": "unset"
27 | *
28 | * It's ok to set padding (top bottom) to configure the spacing with the divider.
29 | *
30 | */
31 | export type Props = {
32 | className?: string;
33 | image: ReactNode;
34 | title: NonNullable;
35 | subtitle?: NonNullable;
36 | onGoBack(): void;
37 | classes?: Partial["classes"]>;
38 | };
39 |
40 | export const DirectoryHeader = memo((props: Props) => {
41 | const { className, image, title, subtitle, onGoBack } = props;
42 |
43 | const { classes, cx } = useStyles({
44 | classesOverrides: props.classes,
45 | });
46 |
47 | return (
48 |
49 |
50 |
55 |
56 |
{image}
57 |
58 | {title}
59 | {subtitle !== undefined && (
60 |
61 | {subtitle}
62 |
63 | )}
64 |
65 |
66 | );
67 | });
68 |
69 | const useStyles = tss.withName({ DirectoryHeader }).create(({ theme }) => ({
70 | root: {
71 | display: "flex",
72 | alignItems: "center",
73 | borderBottom: `1px solid ${theme.colors.useCases.typography.textTertiary}`,
74 | },
75 | imageWrapper: {
76 | margin: theme.spacing({ topBottom: 4, rightLeft: 3 }),
77 | marginLeft: theme.spacing(1),
78 | ...(() => {
79 | const height =
80 | pxToNumber(
81 | theme.typography.variants["object heading"].style
82 | .lineHeight,
83 | ) +
84 | pxToNumber(
85 | theme.typography.variants["caption"].style.lineHeight,
86 | ) +
87 | theme.spacing(2);
88 |
89 | return {
90 | width: height,
91 | height,
92 | };
93 | })(),
94 | display: "flex",
95 | justifyContent: "center",
96 | alignItems: "center",
97 | },
98 | subtitle: {
99 | marginTop: theme.spacing(2),
100 | color: theme.colors.useCases.typography.textSecondary,
101 | textTransform: "capitalize",
102 | },
103 | }));
104 |
--------------------------------------------------------------------------------
/src/Icon.tsx:
--------------------------------------------------------------------------------
1 | import { memo, forwardRef, type ElementType } from "react";
2 | import type { MouseEventHandler } from "react";
3 | import { tss } from "./lib/tss";
4 | import SvgIcon from "@mui/material/SvgIcon";
5 | import type { Equals } from "tsafe";
6 | import type { IconSizeName } from "./lib/icon";
7 | import { createLazySvg } from "./tools/LazySvg";
8 | import { symToStr } from "tsafe/symToStr";
9 | import memoize from "memoizee";
10 | import type { OverridableComponent } from "@mui/material/OverridableComponent";
11 | import type { SvgIconTypeMap } from "@mui/material/SvgIcon";
12 | import { assert } from "tsafe/assert";
13 | import CropSquareIcon from "@mui/icons-material/CropSquare";
14 |
15 | /**
16 | *
17 | * ======== icon:
18 | *
19 | * This can be either a MUI Icon Component or an url pointing to an SVG file.
20 | *
21 | * MUI Icons Component:
22 | * Find the icon you want to use here: https://mui.com/material-ui/material-icons/
23 | * If, for example you'd like to use this one: https://mui.com/material-ui/material-icons/?selected=AddHomeWork
24 | * ```ts
25 | * import AddHomeWorkIcon from '@mui/icons-material/AddHomeWork';
26 | *
27 | * ```
28 | *
29 | * SVG url:
30 | * Example: icon="https://example.com/myCustomIcon.svg"
31 | * It's important that the string ends with ".svg".
32 | * It can also be a data url like: "data:image/svg+xml..." in this case it doesn't need to end with ".svg".
33 | *
34 | * ======== Size:
35 | *
36 | * If you want to change the size of the icon you can set the font
37 | * size manually with css using one of the typography
38 | * fontSize of the root in px.
39 | *
40 | * If you place it inside a element you can define it's size proportional
41 | * to the font-height:
42 | * {
43 | * "fontSize": "inherit",
44 | * ...(()=>{
45 | * const factor = 1.3;
46 | * return { "width": `${factor}em`, "height": `${factor}em` }
47 | * })()
48 | * }
49 | *
50 | * Color:
51 | *
52 | * By default icons inherit the color.
53 | * If you want to change the color you can
54 | * simply set the style "color".
55 | *
56 | */
57 | export type IconProps = {
58 | icon: IconProps.Icon;
59 | className?: string;
60 | /** default default */
61 | size?: IconSizeName;
62 | onClick?: MouseEventHandler;
63 | };
64 |
65 | export namespace IconProps {
66 | type MuiComponentType = OverridableComponent> & {
67 | muiName: string;
68 | };
69 | type SvgUrl = `${"http" | "/" | ""}${string}.svg`;
70 | /**
71 | * Eg: "AddHomeWork"
72 | * All the MUI icons are listed here: https://mui.com/material-ui/material-icons/
73 | * The type is too big to be used here but can be imported from "onyxia-ui/MuiIconComponentName"
74 | */
75 | type MuiComponentName = string;
76 |
77 | export type Icon = MuiComponentName | SvgUrl | MuiComponentType;
78 | }
79 |
80 | export const Icon = memo(
81 | forwardRef((props, ref) => {
82 | const { icon, className, size = "default", onClick, ...rest } = props;
83 |
84 | //For the forwarding, rest should be empty (typewise),
85 | assert>();
86 |
87 | const { classes, cx } = useStyles({ size });
88 |
89 | if (typeof icon !== "string") {
90 | const MuiIconComponent = icon;
91 |
92 | return (
93 |
99 | );
100 | }
101 |
102 | const SvgComponent: ElementType = (() => {
103 | if (
104 | icon.startsWith("http") ||
105 | icon.startsWith("/") ||
106 | icon.endsWith(".svg") ||
107 | icon.startsWith("data:image/svg")
108 | ) {
109 | return createLazySvg(icon);
110 | }
111 |
112 | console.warn(`'${icon}' is not an url`);
113 |
114 | return CropSquareIcon;
115 | })();
116 |
117 | return (
118 |
125 | );
126 | }),
127 | );
128 |
129 | Icon.displayName = symToStr({ Icon });
130 |
131 | const useStyles = tss
132 | .withName({ Icon })
133 | .withParams<{
134 | size: IconSizeName;
135 | }>()
136 | .create(({ theme, size }) => ({
137 | root: {
138 | color: "inherit",
139 | // https://stackoverflow.com/a/24626986/3731798
140 | //"verticalAlign": "top",
141 | //"display": "inline-block"
142 | verticalAlign: "top",
143 | fontSize: theme.iconSizesInPxByName[size],
144 | width: "1em",
145 | height: "1em",
146 | },
147 | }));
148 |
149 | export const createSpecificIcon = memoize((icon: IconProps.Icon) => {
150 | const SpecificIcon = forwardRef<
151 | SVGSVGElement,
152 | Omit
153 | >((props, ref) => );
154 |
155 | SpecificIcon.displayName = Icon.displayName;
156 |
157 | return SpecificIcon;
158 | });
159 |
--------------------------------------------------------------------------------
/src/IconButton.tsx:
--------------------------------------------------------------------------------
1 | import { tss } from "./lib/tss";
2 | import { forwardRef, memo } from "react";
3 | import MuiIconButton from "@mui/material/IconButton";
4 | import { Icon, type IconProps } from "./Icon";
5 | import { assert } from "tsafe/assert";
6 | import type { Equals } from "tsafe";
7 | import { symToStr } from "tsafe/symToStr";
8 |
9 | export type IconButtonProps =
10 | | IconButtonProps.Clickable
11 | | IconButtonProps.Link
12 | | IconButtonProps.Submit;
13 |
14 | export namespace IconButtonProps {
15 | type Common = {
16 | className?: string;
17 | iconClassName?: string;
18 | icon: IconProps.Icon;
19 | size?: IconProps["size"];
20 | /** Defaults to false */
21 | disabled?: boolean;
22 |
23 | /** Defaults to false */
24 | autoFocus?: boolean;
25 |
26 | tabIndex?: number;
27 |
28 | name?: string;
29 | id?: string;
30 | "aria-label"?: string;
31 | };
32 |
33 | export type Clickable = Common & {
34 | onClick: (e: React.MouseEvent) => void;
35 | href?: string;
36 | };
37 |
38 | export type Link = Common & {
39 | href: string;
40 | /** Defaults to true */
41 | doOpenNewTabIfHref?: boolean;
42 | };
43 |
44 | export type Submit = Common & {
45 | type: "submit";
46 | };
47 | }
48 |
49 | export const IconButton = memo(
50 | forwardRef((props, ref) => {
51 | const {
52 | className,
53 | iconClassName,
54 | icon,
55 | size,
56 | disabled = false,
57 | autoFocus = false,
58 | tabIndex,
59 | name,
60 | id,
61 | "aria-label": ariaLabel,
62 | //For the forwarding, rest should be empty (typewise)
63 | ...rest
64 | } = props;
65 |
66 | const { classes, cx } = useStyles({ disabled });
67 |
68 | return (
69 | {
79 | if ("onClick" in rest) {
80 | const { onClick, href, ...restRest } = rest;
81 |
82 | //For the forwarding, rest should be empty (typewise),
83 | assert>();
84 |
85 | return { onClick, href, ...restRest };
86 | }
87 |
88 | if ("href" in rest) {
89 | const {
90 | href,
91 | doOpenNewTabIfHref = true,
92 | ...restRest
93 | } = rest;
94 |
95 | //For the forwarding, rest should be empty (typewise),
96 | assert>();
97 |
98 | return {
99 | href,
100 | target: doOpenNewTabIfHref ? "_blank" : undefined,
101 | ...restRest,
102 | };
103 | }
104 |
105 | if ("type" in rest) {
106 | const { type, ...restRest } = rest;
107 |
108 | //For the forwarding, rest should be empty (typewise),
109 | assert>();
110 |
111 | return {
112 | type,
113 | ...restRest,
114 | };
115 | }
116 | })()}
117 | >
118 |
123 |
124 | );
125 | }),
126 | );
127 |
128 | IconButton.displayName = symToStr({ IconButton });
129 |
130 | const useStyles = tss
131 | .withName({ IconButton })
132 | .withParams<{ disabled: boolean }>()
133 | .create(({ theme, disabled }) => ({
134 | root: {
135 | padding: theme.spacing(2),
136 | "&:hover": {
137 | backgroundColor: "unset",
138 | "& svg": {
139 | color: theme.colors.useCases.buttons.actionHoverPrimary,
140 | },
141 | },
142 | },
143 | icon: {
144 | color: theme.colors.useCases.typography[
145 | disabled ? "textDisabled" : "textPrimary"
146 | ],
147 | },
148 | }));
149 |
--------------------------------------------------------------------------------
/src/Markdown.tsx:
--------------------------------------------------------------------------------
1 | import { memo, useMemo, createElement } from "react";
2 | import ReactMarkdown from "react-markdown";
3 | import MuiLink from "@mui/material/Link";
4 | import { symToStr } from "tsafe/symToStr";
5 | import { id } from "tsafe/id";
6 | import { tss } from "./lib/tss";
7 | import rehypeRaw from "rehype-raw";
8 |
9 | export type MarkdownProps = {
10 | className?: string;
11 | children: string;
12 | getLinkProps?: MarkdownProps.GetLinkProps;
13 | /** Default: false */
14 | inline?: boolean;
15 | /** For accessibility only */
16 | lang?: string;
17 | };
18 |
19 | export namespace MarkdownProps {
20 | export type GetLinkProps = (params: {
21 | href: string;
22 | }) => React.ComponentProps;
23 | }
24 |
25 | export const Markdown = memo((props: MarkdownProps) => {
26 | const {
27 | className,
28 | children,
29 | getLinkProps = id(({ href }) => ({
30 | href,
31 | ...(!href.startsWith("/") ? { target: "blank" } : {}),
32 | })),
33 | inline: isInline = false,
34 | lang = undefined,
35 | } = props;
36 |
37 | const { classes, cx } = useStyles();
38 |
39 | return createElement(
40 | isInline ? "span" : "div",
41 | { lang: lang, className: cx(classes.root, className) },
42 | {
46 | const linkProps =
47 | href === undefined ? {} : getLinkProps({ href });
48 | return {children};
49 | },
50 | p: ({ children }) =>
51 | createElement(isInline ? "span" : "p", { children }),
52 | }}
53 | >
54 | {children}
55 | ,
56 | );
57 | });
58 |
59 | Markdown.displayName = symToStr({ Markdown });
60 |
61 | const useStyles = tss.withName("Markdown").create({
62 | root: {},
63 | });
64 |
65 | export function createMarkdown(params: {
66 | getLinkProps: MarkdownProps.GetLinkProps;
67 | }) {
68 | const { getLinkProps: getLinkProps_global } = params;
69 |
70 | const MarkdownWithLinkRenderer = (props: MarkdownProps) => {
71 | const { getLinkProps: getLinkProps_local, ...rest } = props;
72 |
73 | const getLinkProps = useMemo(
74 | (): MarkdownProps.GetLinkProps =>
75 | ({ href }) => ({
76 | ...getLinkProps_global({ href }),
77 | ...getLinkProps_local?.({ href }),
78 | }),
79 | [getLinkProps_local],
80 | );
81 |
82 | return ;
83 | };
84 |
85 | MarkdownWithLinkRenderer.displayName = Markdown.displayName;
86 |
87 | return { Markdown: MarkdownWithLinkRenderer };
88 | }
89 |
--------------------------------------------------------------------------------
/src/RangeSlider/RangeSlider.tsx:
--------------------------------------------------------------------------------
1 | import { SimpleOrRangeSlider } from "./SimpleOrRangeSlider";
2 | import type { SimpleOrRangeSliderProps } from "./SimpleOrRangeSlider";
3 | import type { ReactComponent } from "../tools/ReactComponent";
4 |
5 | export type RangeSliderProps = Omit & {
6 | valueLow: number;
7 | };
8 |
9 | export const RangeSlider: ReactComponent =
10 | SimpleOrRangeSlider;
11 |
--------------------------------------------------------------------------------
/src/RangeSlider/index.ts:
--------------------------------------------------------------------------------
1 | export { RangeSlider, type RangeSliderProps } from "./RangeSlider";
2 |
--------------------------------------------------------------------------------
/src/Slider.tsx:
--------------------------------------------------------------------------------
1 | import { memo } from "react";
2 | import { SimpleOrRangeSlider } from "./RangeSlider/SimpleOrRangeSlider";
3 | import { SimpleOrRangeSliderProps } from "./RangeSlider/SimpleOrRangeSlider";
4 | import { useConstCallback } from "powerhooks/useConstCallback";
5 |
6 | export type SliderProps = Omit<
7 | SimpleOrRangeSliderProps,
8 | | "lowExtremitySemantic"
9 | | "highExtremitySemantic"
10 | | "valueLow"
11 | | "valueHigh"
12 | | "onValueChange"
13 | > & {
14 | semantic?: string;
15 | value: number;
16 | onValueChange(value: number): void;
17 | };
18 |
19 | export const Slider = memo((props: SliderProps) => {
20 | const { value, onValueChange, semantic, ...rest } = props;
21 |
22 | const onSimpleOrRangeSliderValueChange = useConstCallback<
23 | SimpleOrRangeSliderProps["onValueChange"]
24 | >(({ valueHigh }) => onValueChange(valueHigh));
25 |
26 | return (
27 |
42 | );
43 | });
44 |
--------------------------------------------------------------------------------
/src/Tag.tsx:
--------------------------------------------------------------------------------
1 | import { memo } from "react";
2 | import type { ReactNode } from "react";
3 | import { tss } from "./lib/tss";
4 | import { Text } from "./Text";
5 |
6 | export type TagProps = {
7 | className?: string;
8 | classes?: Partial["classes"]>;
9 | text: NonNullable;
10 | onClick?: () => void;
11 | };
12 |
13 | export const Tag = memo((props: TagProps) => {
14 | const { text, className, onClick } = props;
15 |
16 | const { classes, cx } = useStyles({
17 | classesOverrides: props.classes,
18 | });
19 |
20 | return (
21 |
22 | {typeof text === "string" ? (
23 |
24 | {text}
25 |
26 | ) : (
27 | text
28 | )}
29 |
30 | );
31 | });
32 |
33 | const useStyles = tss.withName({ Tag }).create(({ theme }) => ({
34 | root: {
35 | backgroundColor:
36 | theme.colors.palette[theme.isDarkModeEnabled ? "light" : "dark"]
37 | .main,
38 | padding: theme.spacing({ topBottom: 1, rightLeft: 2 }),
39 | borderRadius: theme.spacing(3),
40 | display: "inline-block",
41 | cursor: "pointer",
42 | },
43 | text: {
44 | color: theme.colors.palette[theme.isDarkModeEnabled ? "dark" : "light"]
45 | .main,
46 | },
47 | }));
48 |
--------------------------------------------------------------------------------
/src/Text.tsx:
--------------------------------------------------------------------------------
1 | import { memo, forwardRef, createElement } from "react";
2 | import { assert, type Equals } from "tsafe/assert";
3 | import { tss } from "./lib/tss";
4 | import type { Theme } from "./lib/theme";
5 | import { symToStr } from "tsafe/symToStr";
6 | import type { TypographyDesc } from "./lib/typography";
7 |
8 | export type TextProps = {
9 | className?: string;
10 | //typo: TypographyVariantNameCustom | TypographyDesc.VariantNameBase;
11 | typo: TypographyDesc.VariantNameBase;
12 | color?: "primary" | "secondary" | "disabled" | "focus";
13 | children: React.ReactNode;
14 | htmlComponent?: TypographyDesc.HtmlComponent;
15 | componentProps?: JSX.IntrinsicElements[TypographyDesc.HtmlComponent];
16 |
17 | fixedSize_enabled?: boolean;
18 | fixedSize_content?: string;
19 | fixedSize_fontWeight?: number;
20 | };
21 |
22 | export const Text = memo(
23 | forwardRef((props, ref) => {
24 | const {
25 | className,
26 | children,
27 | typo,
28 | color = "primary",
29 | htmlComponent,
30 | componentProps = {},
31 | fixedSize_enabled = false,
32 | fixedSize_content,
33 | fixedSize_fontWeight,
34 | //For the forwarding, rest should be empty (typewise)
35 | ...rest
36 | } = props;
37 |
38 | //For the forwarding, rest should be empty (typewise),
39 | assert>();
40 |
41 | const { classes, cx, theme } = useStyles({
42 | typo,
43 | color,
44 | fixedSize_enabled,
45 | fixedSize_content,
46 | fixedSize_fontWeight,
47 | children:
48 | typeof children === "string" ? (children as string) : undefined,
49 | });
50 |
51 | return createElement(
52 | htmlComponent ?? theme.typography.variants[typo].htmlComponent,
53 | {
54 | className: cx(classes.root, className),
55 | ref,
56 | ...componentProps,
57 | ...rest,
58 | },
59 | children,
60 | );
61 | }),
62 | );
63 |
64 | Text.displayName = symToStr({ Text });
65 |
66 | const useStyles = tss
67 | //.withName({ Text })
68 | .withParams<
69 | {
70 | color: NonNullable;
71 | children: string | undefined;
72 | } & Pick<
73 | TextProps,
74 | | "typo"
75 | | "fixedSize_enabled"
76 | | "fixedSize_content"
77 | | "fixedSize_fontWeight"
78 | >
79 | >()
80 | .create(
81 | ({
82 | theme,
83 | typo,
84 | color,
85 | fixedSize_enabled,
86 | fixedSize_fontWeight,
87 | fixedSize_content,
88 | children,
89 | }) => ({
90 | root: {
91 | ...theme.typography.variants[typo].style,
92 | color: theme.colors.useCases.typography[
93 | (() => {
94 | switch (color) {
95 | case "primary":
96 | return "textPrimary";
97 | case "secondary":
98 | return "textSecondary";
99 | case "disabled":
100 | return "textDisabled";
101 | case "focus":
102 | return "textFocus";
103 | }
104 | })()
105 | ],
106 | padding: 0,
107 | margin: 0,
108 | ...(!fixedSize_enabled
109 | ? {}
110 | : {
111 | display: "inline-flex",
112 | flexDirection: "column",
113 | alignItems: "center",
114 | justifyContent: "space-between",
115 | "&::after": {
116 | content: fixedSize_content
117 | ? `"${fixedSize_content}"`
118 | : (assert(children !== undefined),
119 | `"${children}_"`),
120 | height: 0,
121 | visibility: "hidden",
122 | overflow: "hidden",
123 | userSelect: "none",
124 | pointerEvents: "none",
125 | fontWeight: fixedSize_fontWeight,
126 | "@media speech": {
127 | display: "none",
128 | },
129 | },
130 | }),
131 | },
132 | }),
133 | );
134 |
135 | type ExtractCustomTypographyVariantName = T extends Theme<
136 | any,
137 | any,
138 | infer CustomTypographyVariantName
139 | >
140 | ? CustomTypographyVariantName
141 | : never;
142 |
143 | export function createTextWithCustomTypos>() {
144 | type TypographyVariantNameCustom = ExtractCustomTypographyVariantName;
145 |
146 | return {
147 | Text: Text as any as React.MemoExoticComponent<
148 | React.ForwardRefExoticComponent<
149 | (Omit & {
150 | typo:
151 | | TypographyDesc.VariantNameBase
152 | | TypographyVariantNameCustom;
153 | }) &
154 | React.RefAttributes
155 | >
156 | >,
157 | };
158 | }
159 |
--------------------------------------------------------------------------------
/src/ThemedImage.tsx:
--------------------------------------------------------------------------------
1 | import { ThemedSvg, useThemedSvgAsBlobUrl } from "./ThemedSvg";
2 | import {
3 | type ThemedAssetUrl,
4 | useResolveThemedAssetUrl,
5 | } from "./lib/ThemedAssetUrl";
6 |
7 | type Props = {
8 | className?: string;
9 | url: ThemedAssetUrl;
10 | alt?: string;
11 | };
12 |
13 | function getIsSvg(url: string) {
14 | return (
15 | url.split("?")[0].endsWith(".svg") || url.startsWith("data:image/svg")
16 | );
17 | }
18 |
19 | export function ThemedImage(props: Props) {
20 | const { className, alt = "" } = props;
21 |
22 | const { resolveThemedAssetUrl } = useResolveThemedAssetUrl();
23 |
24 | const url = resolveThemedAssetUrl(props.url);
25 |
26 | return getIsSvg(url) ? (
27 |
28 | ) : (
29 |
30 | );
31 | }
32 |
33 | export function useThemedImageUrl(
34 | themedAssetUrl: ThemedAssetUrl | undefined,
35 | ): string | undefined {
36 | const { resolveThemedAssetUrl } = useResolveThemedAssetUrl();
37 |
38 | const url =
39 | themedAssetUrl === undefined
40 | ? undefined
41 | : resolveThemedAssetUrl(themedAssetUrl);
42 |
43 | const svgDataUrl = useThemedSvgAsBlobUrl(
44 | url === undefined ? undefined : getIsSvg(url) ? url : undefined,
45 | );
46 |
47 | return url === undefined ? undefined : getIsSvg(url) ? svgDataUrl : url;
48 | }
49 |
--------------------------------------------------------------------------------
/src/Tooltip.tsx:
--------------------------------------------------------------------------------
1 | import { memo } from "react";
2 | import type { ReactNode, ReactElement } from "react";
3 | import MuiTooltip from "@mui/material/Tooltip";
4 | import { tss } from "./lib/tss";
5 | import { Text } from "./Text";
6 |
7 | export type TooltipProps = {
8 | title: ReactNode;
9 | children: ReactElement;
10 | enterDelay?: number;
11 | };
12 |
13 | export const Tooltip = memo((props: TooltipProps) => {
14 | const { title, children, enterDelay } = props;
15 |
16 | const { classes } = useStyles();
17 |
18 | if (title === undefined) {
19 | return children;
20 | }
21 |
22 | return (
23 |
26 | {title}
27 |
28 | }
29 | enterDelay={enterDelay}
30 | >
31 | {children}
32 |
33 | );
34 | });
35 |
36 | const useStyles = tss.withName({ Tooltip }).create(({ theme }) => ({
37 | root: {
38 | color: theme.colors.palette.light.light,
39 | },
40 | }));
41 |
--------------------------------------------------------------------------------
/src/assets/fonts/Marianne/Marianne-Bold.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/InseeFrLab/onyxia-ui/18896e242dd81be703befc58c51811e477ac406f/src/assets/fonts/Marianne/Marianne-Bold.woff2
--------------------------------------------------------------------------------
/src/assets/fonts/Marianne/Marianne-Bold_Italic.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/InseeFrLab/onyxia-ui/18896e242dd81be703befc58c51811e477ac406f/src/assets/fonts/Marianne/Marianne-Bold_Italic.woff2
--------------------------------------------------------------------------------
/src/assets/fonts/Marianne/Marianne-Light.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/InseeFrLab/onyxia-ui/18896e242dd81be703befc58c51811e477ac406f/src/assets/fonts/Marianne/Marianne-Light.woff2
--------------------------------------------------------------------------------
/src/assets/fonts/Marianne/Marianne-Light_Italic.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/InseeFrLab/onyxia-ui/18896e242dd81be703befc58c51811e477ac406f/src/assets/fonts/Marianne/Marianne-Light_Italic.woff2
--------------------------------------------------------------------------------
/src/assets/fonts/Marianne/Marianne-Medium.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/InseeFrLab/onyxia-ui/18896e242dd81be703befc58c51811e477ac406f/src/assets/fonts/Marianne/Marianne-Medium.woff2
--------------------------------------------------------------------------------
/src/assets/fonts/Marianne/Marianne-Regular.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/InseeFrLab/onyxia-ui/18896e242dd81be703befc58c51811e477ac406f/src/assets/fonts/Marianne/Marianne-Regular.woff2
--------------------------------------------------------------------------------
/src/assets/fonts/Marianne/Marianne-Regular_Italic.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/InseeFrLab/onyxia-ui/18896e242dd81be703befc58c51811e477ac406f/src/assets/fonts/Marianne/Marianne-Regular_Italic.woff2
--------------------------------------------------------------------------------
/src/assets/fonts/Marianne/font.css:
--------------------------------------------------------------------------------
1 | /*
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 | */
11 |
12 | @font-face {
13 | font-family: Marianne;
14 | src: url("./Marianne-Light.woff2") format("woff2");
15 | font-weight: 300;
16 | font-style: normal;
17 | }
18 | @font-face {
19 | font-family: Marianne;
20 | src: url("./Marianne-Light_Italic.woff2") format("woff2");
21 | font-weight: 300;
22 | font-style: italic;
23 | }
24 |
25 | @font-face {
26 | font-family: Marianne;
27 | src: url("./Marianne-Regular.woff2") format("woff2");
28 | font-weight: normal; /*400*/
29 | font-style: normal;
30 | }
31 | @font-face {
32 | font-family: Marianne;
33 | src: url("./Marianne-Regular_Italic.woff2") format("woff2");
34 | font-weight: normal; /*400*/
35 | font-style: italic;
36 | }
37 |
38 | @font-face {
39 | font-family: Marianne;
40 | src: url("./Marianne-Medium.woff2") format("woff2");
41 | font-weight: 500;
42 | font-display: swap;
43 | font-style: normal;
44 | }
45 |
46 | @font-face {
47 | font-family: Marianne;
48 | src: url("./Marianne-Bold.woff2") format("woff2");
49 | font-weight: bold /*700*/;
50 | font-style: normal;
51 | }
52 | @font-face {
53 | font-family: Marianne;
54 | src: url("./Marianne-Bold_Italic.woff2") format("woff2");
55 | font-weight: bold; /*700*/
56 | font-style: italic;
57 | }
--------------------------------------------------------------------------------
/src/assets/fonts/WorkSans/font.css:
--------------------------------------------------------------------------------
1 | /*
2 |
3 |
4 |
5 |
6 |
7 | */
8 |
9 | @font-face {
10 | font-family: "Work Sans";
11 | font-style: normal;
12 | font-weight: normal; /*400*/
13 | font-display: swap;
14 | src: url("./worksans-regular-webfont.woff2") format("woff2");
15 | }
16 |
17 | @font-face {
18 | font-family: "Work Sans";
19 | font-style: normal;
20 | font-weight: 500;
21 | font-display: swap;
22 | src: url("./worksans-medium-webfont.woff2") format("woff2");
23 | }
24 |
25 | @font-face {
26 | font-family: "Work Sans";
27 | font-style: normal;
28 | font-weight: 600;
29 | font-display: swap;
30 | src: url("./worksans-semibold-webfont.woff2") format("woff2");
31 | }
32 |
33 | @font-face {
34 | font-family: "Work Sans";
35 | font-style: normal;
36 | font-weight: bold; /*700*/
37 | font-display: swap;
38 | src: url("./worksans-bold-webfont.woff2") format("woff2");
39 | }
--------------------------------------------------------------------------------
/src/assets/fonts/WorkSans/worksans-bold-webfont.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/InseeFrLab/onyxia-ui/18896e242dd81be703befc58c51811e477ac406f/src/assets/fonts/WorkSans/worksans-bold-webfont.woff2
--------------------------------------------------------------------------------
/src/assets/fonts/WorkSans/worksans-medium-webfont.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/InseeFrLab/onyxia-ui/18896e242dd81be703befc58c51811e477ac406f/src/assets/fonts/WorkSans/worksans-medium-webfont.woff2
--------------------------------------------------------------------------------
/src/assets/fonts/WorkSans/worksans-regular-webfont.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/InseeFrLab/onyxia-ui/18896e242dd81be703befc58c51811e477ac406f/src/assets/fonts/WorkSans/worksans-regular-webfont.woff2
--------------------------------------------------------------------------------
/src/assets/fonts/WorkSans/worksans-semibold-webfont.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/InseeFrLab/onyxia-ui/18896e242dd81be703befc58c51811e477ac406f/src/assets/fonts/WorkSans/worksans-semibold-webfont.woff2
--------------------------------------------------------------------------------
/src/assets/logo.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/src/global.d.ts:
--------------------------------------------------------------------------------
1 | declare module "*.svg" {
2 | import * as React from "react";
3 |
4 | export const ReactComponent: React.FunctionComponent<
5 | React.SVGProps & { title?: string }
6 | >;
7 |
8 | const src: string;
9 | export default src;
10 | }
11 |
--------------------------------------------------------------------------------
/src/lib/ThemedAssetUrl.ts:
--------------------------------------------------------------------------------
1 | import { useMemo } from "react";
2 | import { useDarkMode } from "./darkMode";
3 | import { getSafeUrl } from "../tools/getSafeUrl";
4 |
5 | /**
6 | * ThemedAssetUrl is a type that enable Onyxia administrators to provide a different asset url
7 | * depending on the user's dark mode preference.
8 | * If you don't need this level of customization, you can simply provide a string.
9 | *
10 | * Examples with the FAVICON environment variable:
11 | *
12 | * FAVICON: "https://example.com/favicon.svg"
13 | *
14 | * FAVICON: |
15 | * {
16 | * "light": "https://user-images.githubusercontent.com/6702424/280081114-85e465c0-34a2-47f4-8c38-6d5a5eba31c4.svg",
17 | * "dark": "https://example.com/favicon-dark.svg",
18 | * }
19 | */
20 | export type ThemedAssetUrl =
21 | | string
22 | | {
23 | light: string;
24 | dark: string;
25 | };
26 |
27 | export function resolveThemedAssetUrl(params: {
28 | isDarkModeEnabled: boolean;
29 | themedAssetUrl: ThemedAssetUrl;
30 | }): string {
31 | const { isDarkModeEnabled, themedAssetUrl } = params;
32 |
33 | if (typeof themedAssetUrl === "string") {
34 | return getSafeUrl(themedAssetUrl);
35 | }
36 |
37 | return getSafeUrl(
38 | isDarkModeEnabled ? themedAssetUrl.dark : themedAssetUrl.light,
39 | );
40 | }
41 |
42 | export function useResolveThemedAssetUrl() {
43 | const { isDarkModeEnabled } = useDarkMode();
44 |
45 | const f = useMemo(
46 | () => (themedAssetUrl: ThemedAssetUrl) =>
47 | resolveThemedAssetUrl({
48 | isDarkModeEnabled,
49 | themedAssetUrl,
50 | }),
51 | [isDarkModeEnabled],
52 | );
53 |
54 | return {
55 | resolveThemedAssetUrl: f,
56 | };
57 | }
58 |
--------------------------------------------------------------------------------
/src/lib/breakpoints.ts:
--------------------------------------------------------------------------------
1 | export const breakpointsValues = {
2 | sm: 600,
3 | md: 960,
4 | lg: 1280,
5 | xl: 1920,
6 | } as const;
7 |
--------------------------------------------------------------------------------
/src/lib/darkMode.ts:
--------------------------------------------------------------------------------
1 | import { useContext } from "react";
2 | import { createContext } from "react";
3 | import { createUseGlobalState } from "powerhooks/useGlobalState";
4 | import { getSearchParam } from "powerhooks/tools/urlSearchParams";
5 | import { updateSearchBarUrl } from "powerhooks/tools/updateSearchBar";
6 | import type { StatefulEvt } from "evt";
7 | import { statefulObservableToStatefulEvt } from "powerhooks/tools/StatefulObservable/statefulObservableToStatefulEvt";
8 |
9 | const GLOBAL_CONTEXT_KEY = "__onyxia-ui.darkMode.globalContext";
10 |
11 | declare global {
12 | interface Window {
13 | [GLOBAL_CONTEXT_KEY]: {
14 | initialLocationHref: string;
15 | };
16 | }
17 | }
18 |
19 | window[GLOBAL_CONTEXT_KEY] ??= {
20 | initialLocationHref: window.location.href,
21 | };
22 |
23 | const globalContext = window[GLOBAL_CONTEXT_KEY];
24 |
25 | const { initialLocationHref } = globalContext;
26 |
27 | type Context = {
28 | isDarkModeEnabled: boolean;
29 | setIsDarkModeEnabled: React.Dispatch>;
30 | };
31 |
32 | export const isDarkModeEnabledContext = createContext(
33 | undefined,
34 | );
35 |
36 | export function useDarkMode() {
37 | const contextValue = useContext(isDarkModeEnabledContext);
38 |
39 | if (contextValue === undefined) {
40 | throw new Error("Your app should be wrapped into ");
41 | }
42 |
43 | return contextValue;
44 | }
45 |
46 | export function createUseIsDarkModeEnabledGlobalState(params: {
47 | defaultIsDarkModeEnabled: boolean;
48 | }) {
49 | const { defaultIsDarkModeEnabled } = params;
50 |
51 | const { $isDarkModeEnabled } = createUseGlobalState({
52 | name: "isDarkModeEnabled",
53 | initialState: defaultIsDarkModeEnabled,
54 | doPersistAcrossReloads: true,
55 | });
56 |
57 | const evtIsDarkModeEnabled: StatefulEvt =
58 | statefulObservableToStatefulEvt({
59 | statefulObservable: $isDarkModeEnabled,
60 | });
61 |
62 | (() => {
63 | const URL_PARAM_NAME = "theme";
64 |
65 | const { wasPresent, value } = getSearchParam({
66 | url: initialLocationHref,
67 | name: URL_PARAM_NAME,
68 | });
69 |
70 | if (!wasPresent) {
71 | return;
72 | }
73 |
74 | {
75 | const { wasPresent, url_withoutTheParam } = getSearchParam({
76 | url: window.location.href,
77 | name: URL_PARAM_NAME,
78 | });
79 |
80 | if (wasPresent) {
81 | updateSearchBarUrl(url_withoutTheParam);
82 | }
83 | }
84 |
85 | const isDarkModeEnabled = (() => {
86 | switch (value) {
87 | case "dark":
88 | return true;
89 | case "light":
90 | return false;
91 | default:
92 | return undefined;
93 | }
94 | })();
95 |
96 | if (isDarkModeEnabled === undefined) {
97 | return;
98 | }
99 |
100 | evtIsDarkModeEnabled.state = isDarkModeEnabled;
101 | })();
102 |
103 | evtIsDarkModeEnabled.attach(isDarkModeEnabled => {
104 | {
105 | const id = "root-color-scheme";
106 |
107 | remove_existing_element: {
108 | const element = document.getElementById(id);
109 |
110 | if (element === null) {
111 | break remove_existing_element;
112 | }
113 |
114 | element.remove();
115 | }
116 |
117 | const element = document.createElement("style");
118 |
119 | element.id = id;
120 |
121 | element.innerHTML = `
122 | :root {
123 | color-scheme: ${isDarkModeEnabled ? "dark" : "light"}
124 | }
125 | `;
126 |
127 | document.getElementsByTagName("head")[0].appendChild(element);
128 | }
129 |
130 | // To enable custom css stylesheets to target a specific theme
131 | document.documentElement.setAttribute(
132 | "theme",
133 | isDarkModeEnabled ? "dark" : "light",
134 | );
135 | });
136 |
137 | return evtIsDarkModeEnabled;
138 | }
139 |
--------------------------------------------------------------------------------
/src/lib/icon.ts:
--------------------------------------------------------------------------------
1 | import { breakpointsValues } from "./breakpoints";
2 |
3 | export const iconSizeNames = [
4 | "extra small",
5 | "small",
6 | "default",
7 | "medium",
8 | "large",
9 | ] as const;
10 |
11 | export type IconSizeName = (typeof iconSizeNames)[number];
12 |
13 | export type GetIconSizeInPx = (params: {
14 | sizeName: IconSizeName;
15 | windowInnerWidth: number;
16 | rootFontSizePx: number;
17 | }) => number;
18 |
19 | export const defaultGetIconSizeInPx: GetIconSizeInPx = ({
20 | sizeName,
21 | windowInnerWidth,
22 | rootFontSizePx,
23 | }) =>
24 | rootFontSizePx *
25 | (() => {
26 | switch (sizeName) {
27 | case "extra small":
28 | return 1;
29 | case "small":
30 | if (windowInnerWidth >= breakpointsValues.lg) {
31 | return 1.25;
32 | }
33 |
34 | return 1;
35 |
36 | case "default":
37 | if (windowInnerWidth >= breakpointsValues.lg) {
38 | return 1.5;
39 | }
40 |
41 | return 1.25;
42 | case "medium":
43 | if (windowInnerWidth >= breakpointsValues.lg) {
44 | return 2;
45 | }
46 |
47 | return 1.25;
48 |
49 | case "large":
50 | if (windowInnerWidth >= breakpointsValues.xl) {
51 | return 2.5;
52 | }
53 |
54 | if (windowInnerWidth >= breakpointsValues.lg) {
55 | return 2;
56 | }
57 |
58 | return 1.5;
59 | }
60 | })();
61 |
62 | export function getIconSizesInPxByName(params: {
63 | getIconSizeInPx: GetIconSizeInPx;
64 | windowInnerWidth: number;
65 | rootFontSizePx: number;
66 | }): Record {
67 | const { getIconSizeInPx, windowInnerWidth, rootFontSizePx } = params;
68 |
69 | const out: ReturnType = {} as any;
70 |
71 | iconSizeNames.forEach(
72 | sizeName =>
73 | (out[sizeName] = getIconSizeInPx({
74 | windowInnerWidth,
75 | rootFontSizePx,
76 | sizeName,
77 | })),
78 | );
79 |
80 | return out;
81 | }
82 |
--------------------------------------------------------------------------------
/src/lib/index.ts:
--------------------------------------------------------------------------------
1 | export type {
2 | PaletteBase,
3 | ColorUseCasesBase,
4 | CreateColorUseCase,
5 | } from "./color";
6 |
7 | export {
8 | defaultPalette,
9 | francePalette,
10 | ultravioletPalette,
11 | verdantPalette,
12 | createDefaultColorUseCases,
13 | } from "./color";
14 |
15 | export type {
16 | TypographyDesc,
17 | ComputedTypography,
18 | GetTypographyDesc,
19 | } from "./typography";
20 | export { defaultGetTypographyDesc } from "./typography";
21 |
22 | export * from "./breakpoints";
23 | export { breakpointsValues } from "./breakpoints";
24 |
25 | export type { SpacingConfig, Spacing } from "./spacing";
26 | export { defaultSpacingConfig } from "./spacing";
27 |
28 | export type { IconSizeName, GetIconSizeInPx } from "./icon";
29 | export { defaultGetIconSizeInPx } from "./icon";
30 |
31 | export {
32 | type ThemedAssetUrl,
33 | resolveThemedAssetUrl,
34 | useResolveThemedAssetUrl,
35 | } from "./ThemedAssetUrl";
36 |
37 | export type { Theme } from "./theme";
38 | export { useTheme } from "./theme";
39 | export { useDarkMode } from "./darkMode";
40 | export { createOnyxiaUi } from "./OnyxiaUi";
41 | export { useSplashScreen } from "./SplashScreen";
42 |
43 | export { pxToNumber } from "../tools/pxToNumber";
44 |
--------------------------------------------------------------------------------
/src/lib/shadows.ts:
--------------------------------------------------------------------------------
1 | export const shadows = [
2 | "none",
3 | /** ButtonBar shadow */
4 | "0px 6px 10px 0px rgba(0,0,0,0.07)",
5 | /** Explorer items */
6 | "0px 4px 4px 0px rgba(0,0,0,0.1)",
7 | /** LeftBar */
8 | "6px 0px 16px 0px rgba(0,0,0,0.15)",
9 | /** AccountTab default */
10 | "4px 0px 10px 0px rgba(0,0,0,0.07)",
11 | /** AccountTab active */
12 | "-4px 0px 10px 0px rgba(0,0,0,0.07)",
13 | /** Card over */
14 | "0px 6px 10px 0px rgba(0,0,0,0.14)",
15 | /** Dialog **/
16 | "0px 8px 10px -7px rgba(0,0,0,0.07)",
17 | ] as const;
18 |
--------------------------------------------------------------------------------
/src/lib/spacing.ts:
--------------------------------------------------------------------------------
1 | import { breakpointsValues } from "./breakpoints";
2 | import { assert } from "tsafe/assert";
3 | export interface Spacing {
4 | (value: number): number;
5 | (params: Record<"topBottom" | "rightLeft", number | string>): string;
6 | rightLeft(
7 | kind: Kind,
8 | value: number | string,
9 | ): Record<`${"left" | "right"}${Capitalize}`, string>;
10 | topBottom(
11 | kind: Kind,
12 | value: number | string,
13 | ): Record<`${"top" | "bottom"}${Capitalize}`, string>;
14 | }
15 |
16 | /** Return number of pixel */
17 | export type SpacingConfig = (params: {
18 | /** Assert positive integer */
19 | factorOrExplicitNumberOfPx: number | `${number}px`;
20 | windowInnerWidth: number;
21 | rootFontSizePx: number;
22 | }) => number;
23 |
24 | export const defaultSpacingConfig: SpacingConfig = ({
25 | factorOrExplicitNumberOfPx,
26 | windowInnerWidth,
27 | rootFontSizePx,
28 | }) => {
29 | if (typeof factorOrExplicitNumberOfPx === "string") {
30 | const match = factorOrExplicitNumberOfPx.match(
31 | /^([+-]?([0-9]*[.])?[0-9]+)px$/,
32 | );
33 |
34 | assert(
35 | match !== null,
36 | `${factorOrExplicitNumberOfPx} don't match \\d+px`,
37 | );
38 |
39 | return Number.parseFloat(match[1]);
40 | }
41 |
42 | return (
43 | rootFontSizePx *
44 | (function callee(factor: number): number {
45 | assert(factor >= 0, "factor must be positive");
46 |
47 | if (!Number.isInteger(factor)) {
48 | return (
49 | (callee(Math.floor(factor)) +
50 | callee(Math.floor(factor) + 1)) /
51 | 2
52 | );
53 | }
54 |
55 | if (factor === 0) {
56 | return 0;
57 | }
58 |
59 | if (factor > 6) {
60 | return (factor - 5) * callee(6);
61 | }
62 |
63 | if (windowInnerWidth >= breakpointsValues.xl) {
64 | switch (factor) {
65 | case 1:
66 | return 0.25;
67 | case 2:
68 | return 0.5;
69 | case 3:
70 | return 1;
71 | case 4:
72 | return 1.5;
73 | case 5:
74 | return 2;
75 | case 6:
76 | return 2.5;
77 | }
78 | }
79 |
80 | if (windowInnerWidth >= breakpointsValues.lg) {
81 | switch (factor) {
82 | case 1:
83 | return 0.25;
84 | case 2:
85 | return 0.5;
86 | case 3:
87 | return 1;
88 | case 4:
89 | return 1;
90 | case 5:
91 | return 1.5;
92 | case 6:
93 | return 2;
94 | }
95 | }
96 |
97 | switch (factor) {
98 | case 1:
99 | return 0.25;
100 | case 2:
101 | return 0.25;
102 | case 3:
103 | return 0.5;
104 | case 4:
105 | return 1;
106 | case 5:
107 | return 1;
108 | case 6:
109 | return 1.5;
110 | }
111 |
112 | assert(false);
113 | })(factorOrExplicitNumberOfPx)
114 | );
115 | };
116 |
--------------------------------------------------------------------------------
/src/lib/tss.ts:
--------------------------------------------------------------------------------
1 | import { createTss } from "tss-react";
2 | import { useTheme } from "./theme";
3 |
4 | /** NOTE: Used internally, do not export globally */
5 | export const { tss } = createTss({
6 | useContext: function useTssContext() {
7 | const theme = useTheme();
8 |
9 | return { theme };
10 | },
11 | });
12 |
13 | /** NOTE: Used internally, do not export globally */
14 | export const useStyles = tss.create({});
15 |
--------------------------------------------------------------------------------
/src/stories/Alert.stories.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react";
2 | import { Alert } from "../Alert";
3 | import { Text } from "../Text";
4 | import { sectionName } from "./sectionName";
5 | import { getStoryFactory } from "./getStory";
6 |
7 | const { meta, getStory } = getStoryFactory({
8 | sectionName,
9 | wrappedComponent: { Alert },
10 | defaultWidth: 500,
11 | });
12 |
13 | export default meta;
14 |
15 | export const VueNoTitle = getStory({
16 | doDisplayCross: true,
17 | severity: "success",
18 | children: This is the text,
19 | });
20 |
--------------------------------------------------------------------------------
/src/stories/Breadcrumb.stories.tsx:
--------------------------------------------------------------------------------
1 | import { useReducer, useState } from "react";
2 | import { useEffectOnValueChange } from "powerhooks/useEffectOnValueChange";
3 | import { Breadcrumb } from "../Breadcrumb";
4 | import type { BreadcrumbProps } from "../Breadcrumb";
5 | import { sectionName } from "./sectionName";
6 | import { getStoryFactory, logCallbacks } from "./getStory";
7 | import { symToStr } from "tsafe/symToStr";
8 | import type { UnpackEvt } from "evt";
9 | import { Evt } from "evt";
10 |
11 | function Component(
12 | props: Omit & {
13 | /** Toggle to fire a translation event */
14 | tick: boolean;
15 | },
16 | ) {
17 | const {
18 | tick,
19 | minDepth,
20 | path,
21 | onNavigate,
22 | isNavigationDisabled,
23 | separatorChar,
24 | } = props;
25 |
26 | const [index, incrementIndex] = useReducer((index: number) => index + 1, 0);
27 |
28 | useEffectOnValueChange(() => {
29 | incrementIndex();
30 | }, [tick]);
31 |
32 | const [evtAction] = useState(() =>
33 | Evt.create>(),
34 | );
35 |
36 | useEffectOnValueChange(() => {
37 | evtAction.post({
38 | action: "DISPLAY COPY FEEDBACK",
39 | basename: "foo.svg",
40 | });
41 | }, [evtAction, index]);
42 |
43 | return (
44 |
52 | );
53 | }
54 |
55 | const { meta, getStory } = getStoryFactory({
56 | sectionName,
57 | wrappedComponent: { [symToStr({ Breadcrumb })]: Component },
58 | argTypes: {
59 | tick: {
60 | control: {
61 | type: "boolean",
62 | },
63 | },
64 | },
65 | });
66 |
67 | export default meta;
68 |
69 | export const VueDefault = getStory({
70 | path: ["aaa", "bbb", "cccc", "dddd"],
71 | isNavigationDisabled: false,
72 | minDepth: 0,
73 | tick: true,
74 | ...logCallbacks(["onNavigate"]),
75 | });
76 |
77 | export const VueOtherSeparator = getStory({
78 | path: ["aaa", "bbb", "cccc", "dddd"],
79 | separatorChar: "/",
80 | isNavigationDisabled: false,
81 | minDepth: 0,
82 | tick: true,
83 | ...logCallbacks(["onNavigate"]),
84 | });
85 |
86 | export const VueMinDepth2 = getStory({
87 | path: ["aaa", "bbb", "cccc", "dddd"],
88 | separatorChar: "/",
89 | isNavigationDisabled: false,
90 | minDepth: 2,
91 | tick: true,
92 | ...logCallbacks(["onNavigate"]),
93 | });
94 |
95 | export const VueFromRoot = getStory({
96 | path: ["", "aaa", "bbb", "cccc", "dddd"],
97 | isNavigationDisabled: false,
98 | minDepth: 0,
99 | tick: true,
100 | ...logCallbacks(["onNavigate"]),
101 | });
102 |
103 | export const VueStartFromCwd = getStory({
104 | path: [".", "aaa", "bbb", "cccc", "dddd"],
105 | isNavigationDisabled: false,
106 | minDepth: 0,
107 | tick: true,
108 | ...logCallbacks(["onNavigate"]),
109 | });
110 |
--------------------------------------------------------------------------------
/src/stories/Button.stories.tsx:
--------------------------------------------------------------------------------
1 | import { Button } from "../Button";
2 | import { sectionName } from "./sectionName";
3 | import { getStoryFactory, logCallbacks } from "./getStory";
4 | import GoogleIcon from "@mui/icons-material/Google";
5 | import HelpIcon from "@mui/icons-material/Help";
6 |
7 | const { meta, getStory } = getStoryFactory({
8 | sectionName,
9 | argTypes: {
10 | variant: {
11 | options: ["primary", "secondary", "ternary"],
12 | control: { type: "radio" },
13 | },
14 | },
15 | wrappedComponent: { Button },
16 | });
17 |
18 | export default meta;
19 |
20 | export const VueNoIcon = getStory({
21 | children: "Default",
22 | variant: "primary",
23 | ...logCallbacks(["onClick"]),
24 | });
25 |
26 | export const VueWithStartIcon = getStory({
27 | children: "Foo bar",
28 | startIcon: HelpIcon,
29 | variant: "primary",
30 | ...logCallbacks(["onClick"]),
31 | });
32 |
33 | export const WithManuallyAddedIcons = getStory({
34 | children: "Foo bar",
35 | startIcon: GoogleIcon,
36 | variant: "primary",
37 | ...logCallbacks(["onClick"]),
38 | });
39 |
--------------------------------------------------------------------------------
/src/stories/ButtonBar.stories.tsx:
--------------------------------------------------------------------------------
1 | import { ButtonBar } from "../ButtonBar";
2 | import { sectionName } from "./sectionName";
3 | import { getStoryFactory, logCallbacks } from "./getStory";
4 | import HelpIcon from "@mui/icons-material/Help";
5 | import HomeIcon from "@mui/icons-material/Home";
6 | import TourIcon from "@mui/icons-material/Tour";
7 | import { customIcons } from "./theme";
8 |
9 | const { meta, getStory } = getStoryFactory({
10 | sectionName,
11 | wrappedComponent: { ButtonBar },
12 | });
13 |
14 | export default meta;
15 |
16 | export const VueDefault = getStory({
17 | buttons: [
18 | {
19 | buttonId: "btn1",
20 | icon: HelpIcon,
21 | isDisabled: false,
22 | label: "Label 1",
23 | },
24 | {
25 | buttonId: "btn2",
26 | icon: HomeIcon,
27 | isDisabled: false,
28 | label: "Label 2",
29 | },
30 | {
31 | buttonId: "btn3",
32 | icon: customIcons.servicesSvgUrl,
33 | isDisabled: true,
34 | label: "Label 3",
35 | },
36 | {
37 | buttonId: "btn4",
38 | icon: TourIcon,
39 | isDisabled: false,
40 | label: "Label 4",
41 | },
42 | ] as const,
43 | ...logCallbacks(["onClick"]),
44 | });
45 |
--------------------------------------------------------------------------------
/src/stories/ButtonBarButton.stories.tsx:
--------------------------------------------------------------------------------
1 | import { ButtonBarButton } from "../ButtonBarButton";
2 | import { sectionName } from "./sectionName";
3 | import { getStoryFactory, logCallbacks } from "./getStory";
4 | import { customIcons } from "./theme";
5 |
6 | const { meta, getStory } = getStoryFactory({
7 | sectionName,
8 | wrappedComponent: { ButtonBarButton },
9 | });
10 |
11 | export default meta;
12 |
13 | export const VueDefault = getStory({
14 | children: "Click me",
15 | disabled: false,
16 | startIcon: customIcons.servicesSvgUrl,
17 | ...logCallbacks(["onClick"]),
18 | });
19 |
--------------------------------------------------------------------------------
/src/stories/Card.stories.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { Card } from "../Card";
3 | import { Text } from "../Text";
4 | import { sectionName } from "./sectionName";
5 | import { getStoryFactory } from "./getStory";
6 |
7 | const { meta, getStory } = getStoryFactory({
8 | sectionName,
9 | wrappedComponent: { Card },
10 | });
11 |
12 | export default meta;
13 |
14 | export const VueNoTitle = getStory({
15 | children: I am the body,
16 | });
17 |
18 | export const VueWithDivider = getStory({
19 | aboveDivider: This is the title,
20 | children: I am the body,
21 | });
22 |
--------------------------------------------------------------------------------
/src/stories/Checkbox.stories.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { useState, useEffect } from "react";
3 | import { useConstCallback } from "powerhooks/useConstCallback";
4 | import { Checkbox } from "../Checkbox";
5 | import type { CheckboxProps } from "../Checkbox";
6 | import { sectionName } from "./sectionName";
7 | import { getStoryFactory } from "./getStory";
8 | import { symToStr } from "tsafe/symToStr";
9 |
10 | function ComponentControlled(props: { defaultIsChecked: boolean }) {
11 | const { defaultIsChecked } = props;
12 |
13 | const [isChecked, setIsChecked] = useState(defaultIsChecked);
14 |
15 | useEffect(() => setIsChecked(defaultIsChecked), [defaultIsChecked]);
16 |
17 | const onChange = useConstCallback(
18 | (_event, checked) => setIsChecked(checked),
19 | );
20 |
21 | return ;
22 | }
23 |
24 | function ComponentUncontrolled(props: { defaultIsChecked: boolean }) {
25 | const { defaultIsChecked } = props;
26 |
27 | return ;
28 | }
29 |
30 | function Component(props: {
31 | mode: "controlled" | "uncontrolled";
32 | defaultIsChecked: boolean;
33 | }) {
34 | const { mode, ...rest } = props;
35 |
36 | switch (mode) {
37 | case "controlled":
38 | return ;
39 | case "uncontrolled":
40 | return ;
41 | }
42 | }
43 |
44 | const { meta, getStory } = getStoryFactory({
45 | sectionName,
46 | wrappedComponent: { [symToStr({ Checkbox })]: Component },
47 | });
48 |
49 | export default meta;
50 |
51 | export const VueControlled = getStory({
52 | mode: "controlled",
53 | defaultIsChecked: false,
54 | });
55 |
56 | export const VueUncontrolled = getStory({
57 | mode: "uncontrolled",
58 | defaultIsChecked: false,
59 | });
60 |
--------------------------------------------------------------------------------
/src/stories/CollapsibleSectionHeader.stories.tsx:
--------------------------------------------------------------------------------
1 | import { CollapsibleSectionHeader } from "../CollapsibleSectionHeader";
2 | import { sectionName } from "./sectionName";
3 | import { getStoryFactory, logCallbacks } from "./getStory";
4 |
5 | const { meta, getStory } = getStoryFactory({
6 | sectionName,
7 | wrappedComponent: { CollapsibleSectionHeader },
8 | defaultWidth: 600,
9 | });
10 |
11 | export default meta;
12 |
13 | export const VueCollapsed = getStory({
14 | isCollapsed: true,
15 | title: "This is the name of the section",
16 | total: 123,
17 | ...logCallbacks(["onToggleIsCollapsed"]),
18 | });
19 |
20 | export const VueExpanded = getStory({
21 | isCollapsed: false,
22 | title: "This is the name of the section",
23 | total: 123,
24 | ...logCallbacks(["onToggleIsCollapsed"]),
25 | });
26 |
--------------------------------------------------------------------------------
/src/stories/CopyToClipboardIconButton.stories.tsx:
--------------------------------------------------------------------------------
1 | import { CopyToClipboardIconButton } from "../CopyToClipboardIconButton";
2 | import { sectionName } from "./sectionName";
3 | import { getStoryFactory } from "./getStory";
4 | import { css } from "./tss";
5 |
6 | const { meta, getStory } = getStoryFactory({
7 | sectionName,
8 | wrappedComponent: { CopyToClipboardIconButton },
9 | defaultWidth: 600,
10 | });
11 |
12 | export default meta;
13 |
14 | export const View = getStory({
15 | className: css({
16 | margin: "30px",
17 | }),
18 | textToCopy: "Text to be copied",
19 | copyToClipboardText: "Copy to clipboard",
20 | copiedToClipboardText: " Copied!",
21 | disabled: false,
22 | });
23 |
--------------------------------------------------------------------------------
/src/stories/DarkModeSwitch.stories.tsx:
--------------------------------------------------------------------------------
1 | import { DarkModeSwitch } from "../DarkModeSwitch";
2 | import { sectionName } from "./sectionName";
3 | import { getStoryFactory } from "./getStory";
4 |
5 | const { meta, getStory } = getStoryFactory({
6 | sectionName,
7 | wrappedComponent: { DarkModeSwitch },
8 | argTypes: {
9 | size: {
10 | options: ["extra small", "small", "default", "medium", "large"],
11 | control: { type: "radio" },
12 | },
13 | },
14 | });
15 |
16 | export default meta;
17 |
18 | export const VueDefault = getStory({
19 | size: "default",
20 | });
21 |
--------------------------------------------------------------------------------
/src/stories/Dialog.stories.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { Dialog } from "../Dialog";
3 | import { Button } from "../Button";
4 | import type { DialogProps } from "../Dialog";
5 | import { sectionName } from "./sectionName";
6 | import { getStoryFactory, logCallbacks } from "./getStory";
7 |
8 | const { meta, getStory } = getStoryFactory({
9 | sectionName,
10 | wrappedComponent: { Dialog },
11 | });
12 |
13 | export default meta;
14 |
15 | const props: DialogProps = {
16 | /* spell-checker: disable */
17 | title: "Utiliser dans un service",
18 | subtitle: "Le chemin du secret a été copié. ",
19 | body: `
20 | Au moment de lancer un service, convertissez vos secrets en variables
21 | d'environnement. Pour cela, allez dans configuration avancée, puis dans
22 | l’onglet Vault et collez le chemin du dossier dans le champ prévu à cet effet.
23 | Vos clefs valeurs seront disponibles sous forme de variables d'environnement.`,
24 | /* spell-checker: enable */
25 | buttons: (
26 | <>
27 |
30 |
33 | >
34 | ),
35 | isOpen: true,
36 | ...logCallbacks(["onClose", "onDoNotDisplayAgainValueChange"]),
37 | };
38 |
39 | export const VueFull = getStory(props);
40 |
--------------------------------------------------------------------------------
/src/stories/DirectoryHeader.stories.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { memo } from "react";
3 | import { DirectoryHeader } from "../DirectoryHeader";
4 | import { sectionName } from "./sectionName";
5 | import { getStoryFactory, logCallbacks } from "./getStory";
6 | import { ReactComponent as ServicesSvg } from "./assets/svg/Services.svg";
7 | import imgUrl from "./assets/img/utilitr.png";
8 | import { useStyles } from "../lib/tss";
9 | import Avatar from "@mui/material/Avatar";
10 |
11 | const { meta, getStory } = getStoryFactory({
12 | sectionName,
13 | wrappedComponent: { DirectoryHeader },
14 | defaultWidth: 600,
15 | });
16 |
17 | export default meta;
18 |
19 | const ImageSvg = memo(() => {
20 | const { css, theme } = useStyles();
21 |
22 | return (
23 |
30 | );
31 | });
32 |
33 | export const VueDefaultSvg = getStory({
34 | image: ,
35 | title: "This is the title",
36 | subtitle: "This is the subtitle",
37 | ...logCallbacks(["onGoBack"]),
38 | });
39 |
40 | export const VueWithoutSubtitle = getStory({
41 | image: ,
42 | title: "This is the title",
43 | ...logCallbacks(["onGoBack"]),
44 | });
45 |
46 | export const VueImg = getStory({
47 | image: (
48 |
49 | ),
50 | title: "This is the title",
51 | subtitle: "This is the subtitle",
52 | ...logCallbacks(["onGoBack"]),
53 | });
54 |
--------------------------------------------------------------------------------
/src/stories/GitHubPicker.stories.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { GitHubPicker } from "../GitHubPicker";
3 | import { Button } from "../Button";
4 | import { useStateRef } from "powerhooks/useStateRef";
5 | import { useState } from "react";
6 | import type { GitHubPickerProps } from "../GitHubPicker";
7 | import { sectionName } from "./sectionName";
8 | import { getStoryFactory } from "./getStory";
9 | import { symToStr } from "tsafe/symToStr";
10 | import { Evt } from "evt";
11 | import type { UnpackEvt } from "evt";
12 | import { useConst } from "powerhooks/useConst";
13 | import { assert } from "tsafe/assert";
14 | import { useConstCallback } from "powerhooks/useConstCallback";
15 | import { useTranslation } from "./i18n";
16 |
17 | const { meta, getStory } = getStoryFactory({
18 | sectionName,
19 | wrappedComponent: { [symToStr({ GitHubPicker })]: Component },
20 | });
21 |
22 | export default meta;
23 |
24 | function getTagColor(tag: string) {
25 | return getRandomColor(tag);
26 | }
27 |
28 | function getRandomColor(stringInput: string) {
29 | const h = [...stringInput].reduce((acc, char) => {
30 | return char.charCodeAt(0) + ((acc << 5) - acc);
31 | }, 0);
32 | const s = 95,
33 | l = 35 / 100;
34 | const a = (s * Math.min(l, 1 - l)) / 100;
35 | const f = n => {
36 | const k = (n + h / 30) % 12;
37 | const color = l - a * Math.max(Math.min(k - 3, 9 - k, 1), -1);
38 | return Math.round(255 * color)
39 | .toString(16)
40 | .padStart(2, "0"); // convert to Hex and prefix "0" if needed
41 | };
42 | return `#${f(0)}${f(8)}${f(4)}`;
43 | }
44 |
45 | function Component() {
46 | const evtGitHubPickerAction = useConst(() =>
47 | Evt.create>(),
48 | );
49 |
50 | const [tags, setTags] = useState([
51 | "oauth",
52 | "sso",
53 | "datascience",
54 | "office",
55 | "docker",
56 | ]);
57 |
58 | const [selectedTags, setSelectedTags] = useState(["oauth", "docker"]);
59 |
60 | const buttonRef = useStateRef(null);
61 |
62 | const onSelectedTags = useConstCallback<
63 | GitHubPickerProps["onSelectedTags"]
64 | >(params => {
65 | if (params.isSelect && params.isNewTag) {
66 | setTags([params.tag, ...tags]);
67 | }
68 |
69 | setSelectedTags(
70 | params.isSelect
71 | ? [...selectedTags, params.tag]
72 | : selectedTags.filter(tag => tag !== params.tag),
73 | );
74 | });
75 |
76 | const { t } = useTranslation({ Picker: null });
77 |
78 | return (
79 |
80 | {selectedTags.map(tag => (
81 | {tag}
82 | ))}
83 |
84 |
97 |
106 | t("github picker create tag", { tag }),
107 | done: t("github picker done"),
108 | }}
109 | />
110 |
111 | );
112 | }
113 |
114 | export const VueDefault = getStory({});
115 |
--------------------------------------------------------------------------------
/src/stories/Icon.stories.tsx:
--------------------------------------------------------------------------------
1 | import { Icon } from "../Icon";
2 | import { sectionName } from "./sectionName";
3 | import { getStoryFactory } from "./getStory";
4 | import { customIcons } from "./theme";
5 | import HomeIcon from "@mui/icons-material/Home";
6 | import HelpIcon from "@mui/icons-material/Help";
7 | import { id } from "tsafe/id";
8 |
9 | const icons = [
10 | HomeIcon,
11 | HelpIcon,
12 | customIcons.tourSvgUrl,
13 | customIcons.servicesSvgUrl,
14 | ] as const;
15 |
16 | const { meta, getStory } = getStoryFactory({
17 | sectionName,
18 | wrappedComponent: { Icon },
19 | argTypes: {
20 | icon: {
21 | options: icons,
22 | control: { type: "radio" },
23 | },
24 | size: {
25 | options: ["extra small", "small", "default", "medium", "large"],
26 | control: { type: "radio" },
27 | },
28 | },
29 | });
30 |
31 | export default meta;
32 |
33 | export const Home = getStory({
34 | icon: icons[0],
35 | size: "default",
36 | });
37 |
--------------------------------------------------------------------------------
/src/stories/IconButton.stories.tsx:
--------------------------------------------------------------------------------
1 | import { IconButton } from "../IconButton";
2 | import { sectionName } from "./sectionName";
3 | import { getStoryFactory, logCallbacks } from "./getStory";
4 | import { customIcons } from "./theme";
5 | import HelpIcon from "@mui/icons-material/Help";
6 | import HomeIcon from "@mui/icons-material/Home";
7 |
8 | const icons = [
9 | HomeIcon,
10 | HelpIcon,
11 | customIcons.tourSvgUrl,
12 | customIcons.servicesSvgUrl,
13 | ] as const;
14 |
15 | const { meta, getStory } = getStoryFactory({
16 | sectionName,
17 | wrappedComponent: { IconButton },
18 | argTypes: {
19 | icon: {
20 | options: icons,
21 | control: { type: "radio" },
22 | },
23 | },
24 | });
25 |
26 | export default meta;
27 |
28 | export const Vue = getStory({
29 | icon: icons[0],
30 | ...logCallbacks(["onClick"]),
31 | });
32 |
--------------------------------------------------------------------------------
/src/stories/LanguageSelect.stories.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { useState } from "react";
3 | import { LanguageSelect, type LanguageSelectProps } from "../LanguageSelect";
4 | import { sectionName } from "./sectionName";
5 | import { getStoryFactory } from "./getStory";
6 | import { symToStr } from "tsafe/symToStr";
7 | import { id } from "tsafe/id";
8 |
9 | const languagesPrettyPrint = {
10 | en: "English",
11 | fr: "Français",
12 | };
13 |
14 | type Language = keyof typeof languagesPrettyPrint;
15 |
16 | function Component(
17 | props: Omit<
18 | LanguageSelectProps,
19 | "language" | "onLanguageChange" | "languagesPrettyPrint"
20 | >,
21 | ) {
22 | const [language, setLanguage] = useState("en");
23 |
24 | return (
25 |
31 | );
32 | }
33 |
34 | const { meta, getStory } = getStoryFactory({
35 | sectionName,
36 | wrappedComponent: { [symToStr({ LanguageSelect })]: Component },
37 | argTypes: {
38 | variant: {
39 | options: id["variant"][]>([
40 | "big",
41 | "small",
42 | ]),
43 | control: { type: "radio" },
44 | },
45 | },
46 | });
47 |
48 | export default meta;
49 |
50 | export const VueNoTitle = getStory({
51 | doShowIcon: true,
52 | changeLanguageText: "Change language",
53 | variant: "big",
54 | });
55 |
--------------------------------------------------------------------------------
/src/stories/LeftBar.stories.tsx:
--------------------------------------------------------------------------------
1 | import { LeftBar } from "../LeftBar";
2 | import { sectionName } from "./sectionName";
3 | import { getStoryFactory } from "./getStory";
4 | import { customIcons } from "./theme";
5 | import HelpIcon from "@mui/icons-material/Help";
6 | import HomeIcon from "@mui/icons-material/Home";
7 |
8 | const { meta, getStory } = getStoryFactory({
9 | sectionName,
10 | wrappedComponent: { LeftBar },
11 | });
12 |
13 | export default meta;
14 |
15 | export const VueNoTitle = getStory({
16 | defaultIsPanelOpen: true,
17 | doPersistIsPanelOpen: false,
18 | currentItemId: "item2",
19 | items: [
20 | {
21 | itemId: "item1",
22 | icon: customIcons.tourSvgUrl,
23 | label: "Item 1",
24 | link: {
25 | href: "https://example.com",
26 | },
27 | },
28 | {
29 | groupId: "group1",
30 | },
31 | {
32 | itemId: "item2",
33 | icon: customIcons.servicesSvgUrl,
34 | label: "Item two",
35 | link: {
36 | href: "#",
37 | onClick: () => console.log("click item 2"),
38 | },
39 | },
40 | {
41 | itemId: "item3",
42 | icon: HelpIcon,
43 | label: "Item three",
44 | link: {
45 | href: "#",
46 | },
47 | availability: "greyed",
48 | },
49 | {
50 | itemId: "item4",
51 | icon: HomeIcon,
52 | label: "The fourth item",
53 | link: {
54 | href: "#",
55 | },
56 | },
57 | {
58 | groupId: "group2",
59 | },
60 | ],
61 | });
62 |
--------------------------------------------------------------------------------
/src/stories/Markdown.stories.tsx:
--------------------------------------------------------------------------------
1 | import { Markdown } from "../Markdown";
2 | import { sectionName } from "./sectionName";
3 | import { getStoryFactory } from "./getStory";
4 |
5 | const { meta, getStory } = getStoryFactory({
6 | sectionName,
7 | wrappedComponent: { Markdown },
8 | defaultWidth: 500,
9 | });
10 |
11 | export default meta;
12 |
13 | export const DefaultView = getStory({
14 | children: `# This is a title
15 | This is a paragraph with [a link](https://www.example.com)
16 | `,
17 | });
18 |
19 | export const InlineView = getStory({
20 | inline: true,
21 | children: `Hello [with a link](https://www.example.com) world`,
22 | });
23 |
24 | export const AccordionView = getStory({
25 | children: `
26 |
27 | Click to expand
28 | This is the hidden content inside the details tag.
29 |
30 | `,
31 | });
32 |
--------------------------------------------------------------------------------
/src/stories/PageHeader.stories.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { PageHeader, type PageHeaderProps } from "../PageHeader";
3 | import { sectionName } from "./sectionName";
4 | import { getStoryFactory } from "./getStory";
5 | import { symToStr } from "tsafe/symToStr";
6 | import { useStateRef } from "powerhooks/useStateRef";
7 | import SentimentSatisfiedIcon from "@mui/icons-material/SentimentSatisfied";
8 | import HomeIcon from "@mui/icons-material/Home";
9 | import accountSvgUrl from "./assets/svg/account_v1.svg";
10 |
11 | function Component(
12 | props: Omit<
13 | PageHeaderProps,
14 | "titleCollapseParams" | "helpCollapseParams"
15 | > & {
16 | transitionDuration: number;
17 | },
18 | ) {
19 | const { transitionDuration, ...rest } = props;
20 |
21 | const scrollableElementRef = useStateRef(null);
22 |
23 | return (
24 |
31 |
46 |
Scroll below dit to trigger collapse
47 |
57 | {new Array(300).fill("").map(i => (
58 |
67 | ))}
68 |
69 |
70 | );
71 | }
72 |
73 | const { meta, getStory } = getStoryFactory({
74 | sectionName,
75 | wrappedComponent: { [symToStr({ PageHeader })]: Component },
76 | defaultWidth: 750,
77 | });
78 |
79 | export default meta;
80 |
81 | export const VueDefault = getStory({
82 | helpContent: "This is the content of the help",
83 | helpIcon: SentimentSatisfiedIcon,
84 | helpTitle: "This is the help title",
85 | mainIcon: HomeIcon,
86 | title: "This is the title",
87 | transitionDuration: 250,
88 | });
89 |
90 | export const VueWithCustomIcon = getStory({
91 | helpContent: "This is the content of the help",
92 | helpIcon: accountSvgUrl,
93 | helpTitle: "This is the help title",
94 | mainIcon: accountSvgUrl,
95 | title: "This is the title",
96 | transitionDuration: 250,
97 | });
98 |
--------------------------------------------------------------------------------
/src/stories/Picker.stories.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { Picker } from "../Picker";
3 | import type { PickerProps } from "../Picker";
4 | import { Button } from "../Button";
5 | import { useStateRef } from "powerhooks/useStateRef";
6 | import { useState } from "react";
7 | import { sectionName } from "./sectionName";
8 | import { getStoryFactory } from "./getStory";
9 | import { symToStr } from "tsafe/symToStr";
10 | import { Evt } from "evt";
11 | import type { UnpackEvt } from "evt";
12 | import { useConst } from "powerhooks/useConst";
13 | import { assert } from "tsafe/assert";
14 | import { useConstCallback } from "powerhooks/useConstCallback";
15 | import { useTranslation } from "./i18n";
16 |
17 | const { meta, getStory } = getStoryFactory({
18 | sectionName,
19 | wrappedComponent: { [symToStr({ Picker })]: Component },
20 | });
21 |
22 | export default meta;
23 |
24 | function getTagColor(tag: string) {
25 | return getRandomColor(tag);
26 | }
27 |
28 | function getRandomColor(stringInput: string) {
29 | const h = [...stringInput].reduce((acc, char) => {
30 | return char.charCodeAt(0) + ((acc << 5) - acc);
31 | }, 0);
32 | const s = 95,
33 | l = 35 / 100;
34 | const a = (s * Math.min(l, 1 - l)) / 100;
35 | const f = n => {
36 | const k = (n + h / 30) % 12;
37 | const color = l - a * Math.max(Math.min(k - 3, 9 - k, 1), -1);
38 | return Math.round(255 * color)
39 | .toString(16)
40 | .padStart(2, "0"); // convert to Hex and prefix "0" if needed
41 | };
42 | return `#${f(0)}${f(8)}${f(4)}`;
43 | }
44 |
45 | function Component() {
46 | const evtGitHubPickerAction = useConst(() =>
47 | Evt.create>(),
48 | );
49 |
50 | const [options, setOptions] = useState(
51 | ["oauth", "sso", "datascience", "office", "docker"].map(tag => ({
52 | id: tag,
53 | label: tag,
54 | })),
55 | );
56 |
57 | const [selectedOptionIds, setSelectedOptionIds] = useState([
58 | "oauth",
59 | "docker",
60 | ]);
61 |
62 | const buttonRef = useStateRef(null);
63 |
64 | const onSelectedOption = useConstCallback(
65 | params => {
66 | if (params.isSelect && params.isNewOption) {
67 | setSelectedOptionIds([
68 | params.optionLabel,
69 | ...selectedOptionIds,
70 | ]);
71 | setOptions([
72 | ...options,
73 | {
74 | id: params.optionLabel,
75 | label: params.optionLabel,
76 | },
77 | ]);
78 | }
79 |
80 | setSelectedOptionIds(
81 | params.isSelect
82 | ? [
83 | ...selectedOptionIds,
84 | params.isNewOption
85 | ? params.optionLabel
86 | : params.optionId,
87 | ]
88 | : selectedOptionIds.filter(id => id !== params.optionId),
89 | );
90 | },
91 | );
92 |
93 | const { t } = useTranslation({ Picker });
94 |
95 | return (
96 |
97 | {selectedOptionIds.map(id => (
98 |
{id}
99 | ))}
100 |
101 |
114 |
123 | t("github picker create tag", { tag: optionLabel }),
124 | done: t("github picker done"),
125 | }}
126 | />
127 |
128 | );
129 | }
130 |
131 | export const VueDefault = getStory({});
132 |
--------------------------------------------------------------------------------
/src/stories/RangeSlider.stories.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { RangeSlider } from "../RangeSlider";
3 | import type { RangeSliderProps } from "../RangeSlider";
4 | import { sectionName } from "./sectionName";
5 | import { getStoryFactory } from "./getStory";
6 | import { useState } from "react";
7 | import { symToStr } from "tsafe/symToStr";
8 | import { useConstCallback } from "powerhooks/useConstCallback";
9 |
10 | function Component(
11 | props: Omit,
12 | ) {
13 | const [valueLow, setValueLow] = useState(props.min);
14 | const [valueHigh, setValueHigh] = useState(props.max);
15 |
16 | const onValueChange = useConstCallback(
17 | ({ valueLow, valueHigh }) => {
18 | setValueLow(valueLow);
19 | setValueHigh(valueHigh);
20 | },
21 | );
22 |
23 | return (
24 |
30 | );
31 | }
32 |
33 | const { meta, getStory } = getStoryFactory({
34 | sectionName,
35 | wrappedComponent: { [symToStr({ RangeSlider })]: Component },
36 | });
37 |
38 | export default meta;
39 |
40 | export const Vue1 = getStory({
41 | label: "Random-access memory (RAM)",
42 | lowExtremitySemantic: "guaranteed",
43 | highExtremitySemantic: "maximum",
44 | unit: "Mi",
45 | min: 900,
46 | max: 1100,
47 | step: 1,
48 | });
49 |
--------------------------------------------------------------------------------
/src/stories/SearchBar.stories.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { useState } from "react";
3 | import { SearchBar } from "../SearchBar";
4 | import type { SearchBarProps } from "../SearchBar";
5 | import { sectionName } from "./sectionName";
6 | import { getStoryFactory, logCallbacks } from "./getStory";
7 | import { symToStr } from "tsafe/symToStr";
8 |
9 | function Component(props: Omit) {
10 | const [search, setState] = useState("");
11 |
12 | return ;
13 | }
14 |
15 | const { meta, getStory } = getStoryFactory({
16 | sectionName,
17 | wrappedComponent: { [symToStr({ SearchBar })]: Component },
18 | defaultWidth: 700,
19 | });
20 |
21 | export default meta;
22 |
23 | export const VueDefault = getStory({
24 | ...logCallbacks(["onKeyPress"]),
25 | });
26 |
--------------------------------------------------------------------------------
/src/stories/Slider.stories.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { Slider } from "../Slider";
3 | import type { SliderProps } from "../Slider";
4 | import { sectionName } from "./sectionName";
5 | import { getStoryFactory } from "./getStory";
6 | import { useState } from "react";
7 | import { symToStr } from "tsafe/symToStr";
8 |
9 | function Component(props: Omit) {
10 | const [value, setValue] = useState(props.min);
11 | return ;
12 | }
13 |
14 | const { meta, getStory } = getStoryFactory({
15 | sectionName,
16 | wrappedComponent: { [symToStr({ Slider })]: Component },
17 | });
18 |
19 | export default meta;
20 |
21 | export const Vue1 = getStory({
22 | label: "Random-access memory (RAM)",
23 | extraInfo: "This is some extra infos",
24 | semantic: "maximum",
25 | unit: "Mi",
26 | min: 1,
27 | max: 200,
28 | step: 1,
29 | });
30 |
31 | export const VueNoSemantic = getStory({
32 | label: "Random-access memory (RAM)",
33 | unit: "Mi",
34 | min: 1,
35 | max: 200,
36 | step: 1,
37 | });
38 |
--------------------------------------------------------------------------------
/src/stories/Tabs.stories.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { Tabs } from "../Tabs";
3 | import type { TabProps } from "../Tabs";
4 | import { sectionName } from "./sectionName";
5 | import { getStoryFactory } from "./getStory";
6 | import { symToStr } from "tsafe/symToStr";
7 | import { useState } from "react";
8 | import { useConstCallback } from "powerhooks";
9 |
10 | function Component(
11 | props: Omit<
12 | TabProps,
13 | "tabs" | "activeTabId" | "onRequestChangeActiveTab" | "children"
14 | > & {
15 | tabCount: number;
16 | },
17 | ) {
18 | const { tabCount, ...rest } = props;
19 |
20 | const [tabs] = useState(() =>
21 | new Array(tabCount).fill("").map(
22 | (...[, i]) =>
23 | ({
24 | id: `tab${i}`,
25 | title: `Tab ${i}`,
26 | } as const),
27 | ),
28 | );
29 | type TabId = (typeof tabs)[number]["id"];
30 |
31 | const [activeTabId, setActiveTabId] = useState("tab0");
32 |
33 | const onRequestChangeActiveTab = useConstCallback<
34 | TabProps["onRequestChangeActiveTab"]
35 | >(tabId => setActiveTabId(tabId));
36 |
37 | return (
38 |
44 | Tab selected: {activeTabId}
45 |
46 | );
47 | }
48 |
49 | const { meta, getStory } = getStoryFactory({
50 | sectionName,
51 | defaultWidth: 700,
52 | wrappedComponent: { [symToStr({ Tabs })]: Component },
53 | });
54 |
55 | export default meta;
56 |
57 | export const VueSmall = getStory({
58 | size: "small",
59 | maxTabCount: 4,
60 | tabCount: 9,
61 | });
62 |
63 | export const VueLarge = getStory({
64 | size: "big",
65 | maxTabCount: 4,
66 | tabCount: 9,
67 | });
68 |
69 | export const VueAllTabsVisible = getStory({
70 | size: "big",
71 | maxTabCount: 10,
72 | tabCount: 5,
73 | });
74 |
75 | export const NoArrowNeeded = getStory({
76 | size: "big",
77 | maxTabCount: 4,
78 | tabCount: 4,
79 | });
80 |
81 | export const OnlyTwoTabs = getStory({
82 | size: "big",
83 | maxTabCount: 4,
84 | tabCount: 2,
85 | });
86 |
87 | export const OnlyOneTab = getStory({
88 | size: "big",
89 | maxTabCount: 4,
90 | tabCount: 1,
91 | });
92 |
--------------------------------------------------------------------------------
/src/stories/Tag.stories.tsx:
--------------------------------------------------------------------------------
1 | import { Tag } from "../Tag";
2 | import { sectionName } from "./sectionName";
3 | import { getStoryFactory } from "./getStory";
4 | import { css } from "./tss";
5 |
6 | const { meta, getStory } = getStoryFactory({
7 | sectionName,
8 | wrappedComponent: { Tag },
9 | });
10 |
11 | export default meta;
12 |
13 | export const VueDefault = getStory({
14 | text: "Machine learning",
15 | });
16 |
17 | export const VueCustom = getStory({
18 | className: css({
19 | backgroundColor: "pink",
20 | "& > p": {
21 | color: "black",
22 | },
23 | }),
24 | text: "Machine learning",
25 | });
26 |
--------------------------------------------------------------------------------
/src/stories/TestSpacing.stories.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { useTheme } from "./theme";
3 | import { sectionName } from "./sectionName";
4 | import { getStoryFactory } from "./getStory";
5 |
6 | function TestSpacing() {
7 | const theme = useTheme();
8 |
9 | return (
10 | <>
11 | {([1, 1.5, 2, 2.5, 3, 3.5, 4, 4.5, 5, 5.5, 6, 7, 8] as const).map(
12 | factor => (
13 |
26 | ),
27 | )}
28 | >
29 | );
30 | }
31 |
32 | const { meta, getStory } = getStoryFactory({
33 | sectionName,
34 | wrappedComponent: { TestSpacing },
35 | });
36 |
37 | export default meta;
38 |
39 | export const VueDefault = getStory({});
40 |
--------------------------------------------------------------------------------
/src/stories/Text.stories.ts:
--------------------------------------------------------------------------------
1 | import { Text } from "../Text";
2 | import type { TypographyDesc } from "../lib/typography";
3 | import { sectionName } from "./sectionName";
4 | import { getStoryFactory } from "./getStory";
5 | import { assert } from "tsafe/assert";
6 | import type { Equals } from "tsafe";
7 |
8 | const variantNameBase = [
9 | "display heading",
10 | "page heading",
11 | "subtitle",
12 | "section heading",
13 | "object heading",
14 | "label 1",
15 | "label 2",
16 | "navigation label",
17 | "body 1",
18 | "body 2",
19 | "body 3",
20 | "caption",
21 | ] as const;
22 |
23 | {
24 | type A = TypographyDesc.VariantNameBase;
25 | type B = (typeof variantNameBase)[number];
26 |
27 | type X = Exclude;
28 | type Y = Exclude;
29 |
30 | assert>();
31 | assert>();
32 | }
33 |
34 | const { meta, getStory } = getStoryFactory({
35 | sectionName,
36 | wrappedComponent: { Text },
37 | argTypes: {
38 | typo: {
39 | options: variantNameBase,
40 | control: { type: "radio" },
41 | },
42 | },
43 | });
44 |
45 | export default meta;
46 |
47 | export const Vue1 = getStory({
48 | typo: "body 1",
49 | children: "Lorem ipsum dolor sit amet",
50 | });
51 |
--------------------------------------------------------------------------------
/src/stories/TextField.stories.tsx:
--------------------------------------------------------------------------------
1 | import { TextField } from "../TextField";
2 | import { sectionName } from "./sectionName";
3 | import { getStoryFactory, logCallbacks } from "./getStory";
4 |
5 | const { meta, getStory } = getStoryFactory({
6 | sectionName,
7 | wrappedComponent: { TextField },
8 | argTypes: {
9 | "inputProps_aria-invalid": {
10 | control: {
11 | type: "boolean",
12 | },
13 | },
14 | },
15 | });
16 |
17 | export default meta;
18 |
19 | export const VueDefault = getStory({
20 | defaultValue: "",
21 | "inputProps_aria-label": "the aria label",
22 | label: "This is the label",
23 | getIsValidValue: value => {
24 | console.log("getIsValidValue invoked: ", value);
25 |
26 | if (value.includes(" ")) {
27 | return { isValidValue: false, message: "Can't include spaces" };
28 | }
29 |
30 | return { isValidValue: true };
31 | },
32 | transformValueBeingTyped: value => {
33 | console.log("transformValueBeingTyped invoked: ", value);
34 | return value;
35 | },
36 | ...logCallbacks([
37 | "onEscapeKeyDown",
38 | "onEnterKeyDown",
39 | "onBlur",
40 | "onSubmit",
41 | "onValueBeingTypedChange",
42 | ]),
43 | });
44 |
45 | export const VuePassword = getStory({
46 | defaultValue: "",
47 | "inputProps_aria-label": "password",
48 | label: "Password",
49 | type: "password",
50 | getIsValidValue: value => {
51 | console.log("getIsValidValue invoked: ", value);
52 |
53 | if (value.includes(" ")) {
54 | return { isValidValue: false, message: "Can't include spaces" };
55 | }
56 |
57 | return { isValidValue: true };
58 | },
59 | transformValueBeingTyped: value => {
60 | console.log("transformValueBeingTyped invoked: ", value);
61 | return value;
62 | },
63 | ...logCallbacks([
64 | "onEscapeKeyDown",
65 | "onEnterKeyDown",
66 | "onBlur",
67 | "onSubmit",
68 | "onValueBeingTypedChange",
69 | ]),
70 | });
71 |
72 | export const VueWithHint = getStory({
73 | helperText: "This is an helper text",
74 | defaultValue: "",
75 | "inputProps_aria-label": "input with hint",
76 | label: "Foo bar",
77 | type: "text",
78 | getIsValidValue: value => {
79 | console.log("getIsValidValue invoked: ", value);
80 |
81 | if (value.includes(" ")) {
82 | return { isValidValue: false, message: "Can't include spaces" };
83 | }
84 |
85 | return { isValidValue: true };
86 | },
87 | transformValueBeingTyped: value => {
88 | console.log("transformValueBeingTyped invoked: ", value);
89 | return value;
90 | },
91 | ...logCallbacks([
92 | "onEscapeKeyDown",
93 | "onEnterKeyDown",
94 | "onBlur",
95 | "onSubmit",
96 | "onValueBeingTypedChange",
97 | ]),
98 | });
99 |
100 | export const VueWithHintAndQuestionMark = getStory({
101 | helperText: "This is an helper text",
102 | questionMarkHelperText: "This is an extra helper text",
103 | defaultValue: "",
104 | "inputProps_aria-label": "input with hint",
105 | label: "Foo bar",
106 | type: "text",
107 | getIsValidValue: value => {
108 | console.log("getIsValidValue invoked: ", value);
109 |
110 | if (value.includes(" ")) {
111 | return { isValidValue: false, message: "Can't include spaces" };
112 | }
113 |
114 | return { isValidValue: true };
115 | },
116 | transformValueBeingTyped: value => {
117 | console.log("transformValueBeingTyped invoked: ", value);
118 | return value;
119 | },
120 | ...logCallbacks([
121 | "onEscapeKeyDown",
122 | "onEnterKeyDown",
123 | "onBlur",
124 | "onSubmit",
125 | "onValueBeingTypedChange",
126 | ]),
127 | });
128 |
129 | export const VueTextArea = getStory({
130 | doRenderAsTextArea: true,
131 | defaultValue: "First line\nSecond line",
132 | "inputProps_aria-label": "the aria label",
133 | label: "This is the label",
134 | getIsValidValue: value => {
135 | console.log("getIsValidValue invoked: ", value);
136 |
137 | if (value.includes(" ")) {
138 | return { isValidValue: false, message: "Can't include spaces" };
139 | }
140 |
141 | return { isValidValue: true };
142 | },
143 | transformValueBeingTyped: value => {
144 | console.log("transformValueBeingTyped invoked: ", value);
145 | return value;
146 | },
147 | ...logCallbacks([
148 | "onEscapeKeyDown",
149 | "onEnterKeyDown",
150 | "onBlur",
151 | "onSubmit",
152 | "onValueBeingTypedChange",
153 | ]),
154 | });
155 |
156 | export const VueWithSuggestions = getStory({
157 | defaultValue: "",
158 | freeSolo: true,
159 | "inputProps_aria-label": "the aria label",
160 | label: "This is the label",
161 | getIsValidValue: value => {
162 | console.log("getIsValidValue invoked: ", value);
163 |
164 | if (value.includes(" ")) {
165 | return { isValidValue: false, message: "Can't include spaces" };
166 | }
167 |
168 | return { isValidValue: true };
169 | },
170 | transformValueBeingTyped: value => {
171 | console.log("transformValueBeingTyped invoked: ", value);
172 | return value;
173 | },
174 | options: ["first", "second", "third"],
175 | ...logCallbacks([
176 | "onEscapeKeyDown",
177 | "onEnterKeyDown",
178 | "onBlur",
179 | "onSubmit",
180 | "onValueBeingTypedChange",
181 | ]),
182 | });
183 |
--------------------------------------------------------------------------------
/src/stories/Tooltip.stories.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { Tooltip } from "../Tooltip";
3 | import { Icon } from "../Icon";
4 | import { sectionName } from "./sectionName";
5 | import { getStoryFactory } from "./getStory";
6 | import type { MuiIconComponentName } from "../MuiIconComponentName";
7 | import { id } from "tsafe/id";
8 |
9 | const { meta, getStory } = getStoryFactory({
10 | sectionName,
11 | wrappedComponent: { Tooltip },
12 | });
13 |
14 | export default meta;
15 |
16 | export const Vue1 = getStory({
17 | children: ("Help")} />,
18 | title: "This is the title",
19 | });
20 |
--------------------------------------------------------------------------------
/src/stories/assets/img/utilitr.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/InseeFrLab/onyxia-ui/18896e242dd81be703befc58c51811e477ac406f/src/stories/assets/img/utilitr.png
--------------------------------------------------------------------------------
/src/stories/assets/svg/Services.svg:
--------------------------------------------------------------------------------
1 |
7 |
--------------------------------------------------------------------------------
/src/stories/assets/svg/Tour.svg:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/src/stories/assets/svg/account_v1.svg:
--------------------------------------------------------------------------------
1 |
6 |
--------------------------------------------------------------------------------
/src/stories/documentation/components/Alert.stories.mdx:
--------------------------------------------------------------------------------
1 |
2 |
3 | import { Fragment } from "react";
4 | import { Meta, Story, Canvas, ArgsTable } from "@storybook/addon-docs";
5 | import { OnyxiaUi } from "../../theme";
6 | import { Text } from "../../../Text";
7 | import { Alert } from "../../../Alert";
8 | import { useDarkMode } from "storybook-dark-mode";
9 | import { css } from "../../tss";
10 |
11 | (
16 |
17 |
18 |
19 | ),
20 | ]}
21 | parameters={{
22 | viewMode: "docs",
23 | previewTabs: {
24 | canvas: { hidden: true },
25 | },
26 | }}
27 | />
28 |
29 | # Alert
30 |
31 | This is a documentation of `` component.
32 | Alerts are used to draw the user's attention to information.
33 |
34 | - [Structure](#structure)
35 | - [Types of alert](#types-of-alert)
36 | - [Rules of use](#rules-of-use)
37 | - [Properties](#properties)
38 |
39 | ## Structure
40 |
41 | The alert is made up of the following elements:
42 |
43 | - a title - mandatory.
44 |
45 | - a pictogram and a color - mandatory.
46 |
47 | - a description text - optional.
48 |
49 | - a closing cross - optional.
50 |
51 | Alerts are displayed contextually in a page / form, during user interactions with validation messages
52 | (example: success or error following submission of a form) or during application / system side events
53 | (example : information, alert, update messages, etc.).
54 |
55 | ## Types of alert
56 |
57 | There are 4 types of alert allowing information of different kinds to be given:
58 |
59 | - [Error alert](#error-alert)
60 | - [Success alert](#success-alert)
61 | - [Information alert](#information-alert)
62 | - [Warning alert](#warning-alert)
63 |
64 | ### Error alert
65 |
66 | Used when there are several errors in a form, or fatal errors for the user to report.
67 |
68 |
82 |
83 | ```tsx
84 |
85 | This is an error
86 |
87 | ```
88 |
89 | ### Success alert
90 |
91 | Used to indicate to the user that an action or task has been completed successfully.
92 |
93 |
107 |
108 | ```tsx
109 |
110 | This is an success
111 |
112 | ```
113 |
114 | ### Information alert
115 |
116 | Use to highlight important information
117 |
118 |
133 |
134 | ```tsx
135 |
136 | This is an info
137 |
138 | ```
139 |
140 | ### Warning alert
141 |
142 | Use to highlight important information
143 |
144 |
159 |
160 | ```tsx
161 |
162 | This is an info
163 |
164 | ```
165 |
166 | ## Rules of use
167 |
168 | - The title of the alert should be clear and concise.
169 | - The alert description text should clearly detail the information / problem to the user.
170 | - The tone should be courteous, not to blame the user, but to accompany him.
171 |
172 | ## Properties
173 |
174 |
175 |
--------------------------------------------------------------------------------
/src/stories/documentation/components/Checkbox.stories.mdx:
--------------------------------------------------------------------------------
1 |
3 |
4 | import { Fragment } from "react";
5 | import { Meta, Story, Canvas, ArgsTable } from "@storybook/addon-docs";
6 | import { OnyxiaUi } from "../../theme";
7 | import { Text } from "../../../Text";
8 | import { Checkbox } from "../../../Checkbox";
9 | import { useDarkMode } from "storybook-dark-mode";
10 |
11 | (
16 |
17 |
18 |
19 | ),
20 | ]}
21 | parameters={{
22 | viewMode: "docs",
23 | previewTabs: {
24 | canvas: { hidden: true },
25 | },
26 | }}
27 | />
28 |
29 | # Checkbox
30 |
31 | This is a documentation of `` component.
32 | The checkbox allow the user to select one or more options from a list.
33 | They are used to make multiple selections (from 0 to N elements) or to allow a binary choice,
34 | when the user can select or deselect a single option.
35 |
36 | ## Structure
37 |
38 | Checkbox is made up of the following elements:
39 |
40 | - a button / box - mandatory.
41 | - a label, associated with the button - mandatory.
42 | - an additional text for the buttons / labels - optional.
43 | - a title, describing the context of the button group - mandatory for groups of boxes.
44 | - an additional description for the legend - optional.
45 | - an error message - required if a change of state is to be notified to the user.
46 |
--------------------------------------------------------------------------------
/src/stories/documentation/components/Tabs.stories.mdx:
--------------------------------------------------------------------------------
1 |
3 |
4 | import { Fragment } from "react";
5 | import { Meta, Story, Canvas, ArgsTable } from "@storybook/addon-docs";
6 | import { OnyxiaUi } from "../../theme";
7 | import { Tabs } from "../../../Tabs";
8 | import { Text } from "../../../Text";
9 | import { css } from "../../tss";
10 | import { useDarkMode } from "storybook-dark-mode";
11 |
12 | (
17 |
18 |
19 |
20 | ),
21 | ]}
22 | parameters={{
23 | viewMode: "docs",
24 | previewTabs: {
25 | canvas: { hidden: true },
26 | },
27 | }}
28 | />
29 |
30 | # Tabs
31 |
32 | This is a documentation of `` component.
33 | Tabs are used to organize and navigate between related content on the same page.
34 | Tabs ensure that large amounts of content can be organized in a manner that is easier
35 | to digest for the user. Tabs are arranged in bars of tabs called tab groups, with the tab
36 | label providing the user with an indication of what content will be revealed when the tab is selected.
37 |
38 | - [Structure](#structure)
39 | - [States](#sizes)
40 | - [Sizes](#sizes)
41 | - [Options](#options)
42 | - [Rules of use](#rules-of-use)
43 | - [Properties](#properties)
44 |
45 | ## Structure
46 |
47 | Tabs are arranged in bars of tabs called tab groups, with the tab label providing the user
48 | with an indication of what content will be revealed when the tab is selected.
49 |
50 | ## Options
51 |
52 | ### Scrolling
53 |
54 | If there are more tabs provided than can fit within the viewport, tabs will scroll.
55 | The user may scroll using a native control like a horizontal scroll wheel, or by using the scroll buttons.
56 |
57 | Visibility of the scroll buttons can be controlled by the `areArrowsVisible` prop.
58 | By default, scroll buttons will appear automatically if needed. Options are...
59 |
60 | ## Rules of use
61 |
62 | - Sort tabs according to user needs, placing the most important first.
63 | - Tabs are relevant if the content can be usefully separated into clearly named sections.
64 | - Use the tab label to provide a clear and concise description of the content contained in that tab and to help differentiate between the different sections.
65 | - Do not use tabs if users need to read the content of all sections.
66 | - Do not use for too long content where tabs are difficult to find after reading.
67 | - Only one tab should be active at a given time.
68 | - Tabs should be positioned in a single, scrollable (if needed) row above the content it relates to.
69 | - Pay attention to the maximum number of tabs. consider further splitting the content or using a different navigation component.
70 | - Do not use tabs to create a sequence or progression of content that the user is expected to read in a given order.
71 | - Do not use tabs for comparing content (e.g. different model specifications).
72 | - Do not use tabs to navigate users to a different page.
73 |
74 | ## Properties
75 |
76 |
77 |
--------------------------------------------------------------------------------
/src/stories/documentation/components/Textfield.stories.mdx:
--------------------------------------------------------------------------------
1 |
3 |
4 | import { Fragment } from "react";
5 | import { Meta, Story, Canvas, ArgsTable } from "@storybook/addon-docs";
6 | import { OnyxiaUi } from "../../theme";
7 | import { Text } from "../../../Text";
8 | import { TextField } from "../../../TextField";
9 | import { css } from "../../tss";
10 | import { useDarkMode } from "storybook-dark-mode";
11 |
12 | (
17 |
18 |
19 |
20 | ),
21 | ]}
22 | parameters={{
23 | viewMode: "docs",
24 | previewTabs: {
25 | canvas: { hidden: true },
26 | },
27 | }}
28 | />
29 |
30 | # TextField
31 |
32 | This is a documentation of `` component.
33 | Text fields are used when the user is required to input short form content,
34 | including text, numbers, e-mail addresses, or passwords.
35 |
36 | - [Structure](#structure)
37 | - [States](#states)
38 | - [Options](#options)
39 | - [Rules of use](#rules-of-use)
40 | - [Properties](#properties)
41 |
42 | ## Structure
43 |
44 | The input field is made up of the following elements:
45 |
46 | - one field - required.
47 |
48 | - a label, linked to the field - mandatory.
49 |
50 | - an additional description (helptext) - optional.
51 |
52 | - one or two icons, which can be modified - optional.
53 |
54 | - an error message - required if a change of state is to be notified to the user.
55 |
56 |
57 |
58 | ### Field with help text
59 |
60 | An optional help text can be added to provide additional guidance to the user on how to interact with the component.
61 |
62 |
63 | ### Field with a long help text
64 |
65 | A long help text improve the guidance of help text for the user on how to interact with the component.
66 |
67 |
68 | ### Selection field
69 |
70 | Selection field provide a choice of options from a list. The selected components
71 | are typically used in a form to allow users to make the desired selection from the list of options.
72 |
73 | ### Passwords
74 |
75 | If the information being entered by the user is sensitive, use the password option to protect it.
76 |
77 | ## Rules of use
78 |
79 | - Keep the same label for fields requesting the same information
80 | - A help text may accompany this wording to clarify in particular the nature of the expected content.
81 | If a precise format is expected, it should be indicated as clearly as possible and given examples.
82 | - Information should not be hidden in a tooltip or infobox, if this content is essential to enter the field.
83 | - The eye spontaneously read from bottom to top, display the fields in a vertical list to facilitate reading and optimize ergonomics
84 | - Use a primary button to validate an input field or a form, the secondary button will be used to go back, reset the form or abandon the input.
85 | - The submission of a field must be followed by a message indicating the success or not of the desired action
86 | - Label the form as (optional) when the input is not required.
87 | - Avoid the use of placeholder as it can confuse the user. However, if you wish to use, it is necessary
88 | to respect the proposed color in order to remain accessible, and its content must present information
89 | not essential to understanding the field. In no case can it replace a label and it is only to be reserved
90 | for secondary input aids.
91 |
92 | ## Properties
93 |
94 |
95 |
--------------------------------------------------------------------------------
/src/stories/emotionCache.ts:
--------------------------------------------------------------------------------
1 | import createCache from "@emotion/cache";
2 |
3 | export const emotionCache = createCache({
4 | key: "tss",
5 | });
6 |
--------------------------------------------------------------------------------
/src/stories/getStory.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react";
2 | import type { Meta, Story } from "@storybook/react";
3 | import type { ArgType } from "@storybook/addons";
4 | import { useMemo } from "react";
5 | import { symToStr } from "tsafe/symToStr";
6 | import { useTheme, breakpointsValues } from "../lib";
7 | import { id } from "tsafe/id";
8 | import { GlobalStyles } from "tss-react";
9 | import { useWindowInnerSize } from "powerhooks/useWindowInnerSize";
10 | import type { ReactComponent } from "../tools/ReactComponent";
11 | import { OnyxiaUi } from "./theme";
12 | import { Text } from "../Text";
13 | import { getIsDarkModeEnabledOsDefault } from "../tools/getIsDarkModeEnabledOsDefault";
14 | import { assert, type Equals, is } from "tsafe/assert";
15 |
16 | export function getStoryFactory>(params: {
17 | sectionName: string;
18 | wrappedComponent: Record>;
19 | /** https://storybook.js.org/docs/react/essentials/controls */
20 | argTypes?: Partial>;
21 | defaultWidth?: number;
22 | }) {
23 | const {
24 | sectionName,
25 | wrappedComponent,
26 | argTypes = {},
27 | defaultWidth,
28 | } = params;
29 |
30 | const Component: any = Object.entries(wrappedComponent).map(
31 | ([, component]) => component,
32 | )[0];
33 |
34 | function ScreenSize() {
35 | const { windowInnerWidth } = useWindowInnerSize();
36 |
37 | const range = useMemo(() => {
38 | if (windowInnerWidth >= breakpointsValues["xl"]) {
39 | return "xl-∞";
40 | }
41 |
42 | if (windowInnerWidth >= breakpointsValues["lg"]) {
43 | return "lg-xl";
44 | }
45 |
46 | if (windowInnerWidth >= breakpointsValues["md"]) {
47 | return "md-lg";
48 | }
49 |
50 | if (windowInnerWidth >= breakpointsValues["sm"]) {
51 | return "sm-md";
52 | }
53 |
54 | return "0-sm";
55 | }, [windowInnerWidth]);
56 |
57 | return (
58 |
59 | {windowInnerWidth}px width: {range}
60 |
61 | );
62 | }
63 |
64 | type ReservedProps = {
65 | darkMode: boolean;
66 | width: number;
67 | };
68 |
69 | const reservedPropsName = ["darkMode", "width"] as const;
70 |
71 | assert>();
72 |
73 | const Template: Story = props => {
74 | const { darkMode, width, ...componentProps } = props;
75 |
76 | assert(
77 | Object.keys(componentProps).every(
78 | key => !id(reservedPropsName).includes(key),
79 | ),
80 | );
81 | assert(is(componentProps));
82 |
83 | return (
84 |
85 |
89 |
90 | );
91 | };
92 |
93 | const ContextualizedTemplate = ({
94 | width,
95 | componentProps,
96 | }: {
97 | componentProps: Props;
98 | width: number;
99 | }) => {
100 | const theme = useTheme();
101 |
102 | return (
103 | <>
104 |
115 |
116 |
123 |
124 |
125 | >
126 | );
127 | };
128 |
129 | function getStory(props: Props): typeof Template {
130 | const out = Template.bind({});
131 |
132 | out.args = {
133 | darkMode: getIsDarkModeEnabledOsDefault(),
134 | width: defaultWidth ?? 0,
135 | ...props,
136 | };
137 |
138 | return out;
139 | }
140 |
141 | return {
142 | meta: id({
143 | title: `${sectionName}/${symToStr(wrappedComponent)}`,
144 | component: Component,
145 | argTypes: {
146 | width: {
147 | control: {
148 | type: "range",
149 | min: 0,
150 | max: 1920,
151 | step: 1,
152 | },
153 | },
154 | ...argTypes,
155 | },
156 | }),
157 | getStory,
158 | };
159 | }
160 |
161 | export function logCallbacks(
162 | propertyNames: readonly T[],
163 | ): Record void> {
164 | const out: Record void> = id>({});
165 |
166 | propertyNames.forEach(
167 | propertyName =>
168 | (out[propertyName] = console.log.bind(console, propertyName)),
169 | );
170 |
171 | return out;
172 | }
173 |
--------------------------------------------------------------------------------
/src/stories/global.d.ts:
--------------------------------------------------------------------------------
1 | declare module "*.png" {
2 | const src: string;
3 | export default src;
4 | }
5 |
6 | declare module "*.svg" {
7 | import * as React from "react";
8 |
9 | export const ReactComponent: React.FunctionComponent<
10 | React.SVGProps & { title?: string }
11 | >;
12 |
13 | const src: string;
14 | export default src;
15 | }
16 |
--------------------------------------------------------------------------------
/src/stories/i18n.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { createI18nApi, declareComponentKeys } from "i18nifty";
3 |
4 | const { i18n } = declareComponentKeys<
5 | | "github picker label"
6 | | { K: "github picker create tag"; P: { tag: string } }
7 | | { K: "github picker done"; R: JSX.Element }
8 | | "something else"
9 | >()({ Picker: null });
10 |
11 | export const { useTranslation } = createI18nApi()(
12 | {
13 | languages: ["en", "fr"],
14 | fallbackLanguage: "en",
15 | },
16 | {
17 | en: {
18 | Picker: {
19 | "github picker label": "Pick tag",
20 | "github picker create tag": ({ tag }) =>
21 | `Create the "${tag}" tag`,
22 | "github picker done": <>Done>,
23 | "something else": "ok",
24 | },
25 | },
26 | fr: {
27 | Picker: {
28 | "github picker label": undefined,
29 | "github picker create tag": undefined,
30 | "github picker done": undefined,
31 | "something else": undefined,
32 | },
33 | },
34 | },
35 | );
36 |
--------------------------------------------------------------------------------
/src/stories/index.stories.mdx:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | ```bash
5 | yarn add onyxia-ui @mui/material @emotion/react @emotion/styled
6 | ```
7 |
8 | ## Icons
9 |
10 | Onyxia-ui enables you to use icons from [the Material Design Library](https://mui.com/material-ui/material-icons/).
11 | Or to provide your own icon as SVG urls.
12 |
13 |
14 | ### Using Material Icons: With hard import
15 |
16 | If you know what icon you'll need ahead of time, implement this approach:
17 |
18 | ```bash
19 | yarn add @mui/icons-material
20 | ```
21 |
22 | `src/theme.ts`
23 | ```ts
24 | const { ThemeProvider } = createThemeProvider({
25 | // ...
26 | publicUrl: undefined
27 | });
28 | ```
29 |
30 | Now if you want to use [AccessAlarms](https://mui.com/material-ui/material-icons/?selected=AccessAlarms)
31 |
32 | ```tsx
33 | import AccessAlarmIcon from "@mui/icons-material/AccessAlarm";
34 |
35 |
36 | ```
37 |
38 | ### Using Material Icons: With lazy loading
39 |
40 | If you don't know ahead of time what icon you will need. This is the case if your app
41 | renders user generated content that might include icons then you can opt for downloading the
42 | icons dynamically.
43 | Be aware that this involves including a 35MB directory of icons in your `public/` directory
44 | which will end up impacting your docker image size.
45 |
46 | ```diff
47 | "scripts": {
48 | "prepare": "copy-material-icons-to-public"
49 | }
50 | ```
51 | This will enable you to do this:
52 | ```tsx
53 | import { Icon } from "onyxia-ui/Icon";
54 |
55 | // https://mui.com/material-ui/material-icons/?selected=AccessAlarms
56 |
57 | ```
58 |
59 | Or, if you want type safety:
60 | ```tsx
61 | import { Icon } from "onyxia-ui/Icon";
62 | import { id } from "tsafe/id";
63 | import type { MuiIconComponentName } from "onyxia-ui/MuiIconComponentName"
64 |
65 | // https://mui.com/material-ui/material-icons/?selected=AccessAlarms
66 | ("AccessAlarms")} />
67 | ```
68 |
69 | ### Using custom SVGs as icons
70 |
71 | ```tsx
72 | import myIconSvgUrl from "./assets/my-icon.svg";
73 |
74 |
75 |
76 | ```
77 |
78 | ## Documentation
79 |
80 | The documentation is under the form of a very simple [demo project](https://github.com/garronej/onyxia-ui/tree/main/src/test).
81 | The actual theme configuration [happens here](https://github.com/garronej/onyxia-ui/blob/main/src/test/src/theme.ts).
82 | If you want to experiment with it you can run the demo app with:
--------------------------------------------------------------------------------
/src/stories/sectionName.ts:
--------------------------------------------------------------------------------
1 | export const sectionName = "sandbox";
2 |
--------------------------------------------------------------------------------
/src/stories/theme.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react";
2 | import "../assets/fonts/WorkSans/font.css";
3 | import "../assets/fonts/Marianne/font.css";
4 | import { createOnyxiaUi, defaultGetTypographyDesc } from "../lib";
5 | import { emotionCache } from "./emotionCache";
6 | import { CacheProvider } from "@emotion/react";
7 | import tourSvgUrl from "./assets/svg/Tour.svg";
8 | import servicesSvgUrl from "./assets/svg/Services.svg";
9 |
10 | console.log(tourSvgUrl, servicesSvgUrl);
11 |
12 | const { OnyxiaUi: OnyxiaUiWithoutEmotionCache } = createOnyxiaUi({
13 | isScoped: true,
14 | isReactStrictModeEnabled: false,
15 | getTypographyDesc: params => ({
16 | ...defaultGetTypographyDesc(params),
17 | fontFamily: '"Work Sans", sans-serif',
18 | //"fontFamily": "Marianne, sans-serif",
19 | }),
20 | });
21 |
22 | export function OnyxiaUi(props: {
23 | darkMode?: boolean;
24 | children: React.ReactNode;
25 | }) {
26 | const { darkMode, children } = props;
27 | return (
28 |
29 |
30 | {children}
31 |
32 |
33 | );
34 | }
35 |
36 | export const customIcons = {
37 | tourSvgUrl: tourSvgUrl.replace(/^\/?/, "/"),
38 | servicesSvgUrl: servicesSvgUrl.replace(/^\/?/, "/"),
39 | };
40 |
--------------------------------------------------------------------------------
/src/stories/tss.ts:
--------------------------------------------------------------------------------
1 | import { createTss } from "tss-react";
2 | import { useTheme } from "../lib";
3 | import { emotionCache } from "./emotionCache";
4 | import { createCssAndCx } from "tss-react/cssAndCx";
5 |
6 | /** NOTE: Used internally, do not export globally */
7 | export const { tss } = createTss({
8 | useContext: function useTssContext() {
9 | const theme = useTheme();
10 |
11 | return { theme };
12 | },
13 | });
14 |
15 | /** NOTE: Used internally, do not export globally */
16 | export const useStyles = tss.create({});
17 |
18 | export const { css, cx } = createCssAndCx({ cache: emotionCache });
19 |
--------------------------------------------------------------------------------
/src/tools/LazySvg.tsx:
--------------------------------------------------------------------------------
1 | import "minimal-polyfills/Object.fromEntries";
2 | import React, { useEffect, useState, forwardRef, memo } from "react";
3 | import memoize from "memoizee";
4 | import { symToStr } from "tsafe/symToStr";
5 | import { capitalize } from "tsafe/capitalize";
6 | import { getSafeUrl } from "./getSafeUrl";
7 |
8 | export type LazySvgProps = Omit, "ref"> & {
9 | svgUrl: string;
10 | };
11 |
12 | export const LazySvg = memo(
13 | forwardRef((props, ref) => {
14 | const { svgUrl, children: _, ...svgComponentProps } = props;
15 |
16 | const [state, setState] = useState<
17 | | {
18 | svgUrl: string;
19 | svgRootAttrs: Record;
20 | svgInnerHtml: string;
21 | }
22 | | undefined
23 | >(undefined);
24 |
25 | useEffect(() => {
26 | let isActive = true;
27 |
28 | (async () => {
29 | const svgElement = await fetchSvgAsHTMLElement(svgUrl);
30 |
31 | if (!isActive) {
32 | return;
33 | }
34 |
35 | if (svgElement === undefined) {
36 | console.error(`Failed to fetch ${svgUrl}`);
37 | return;
38 | }
39 |
40 | if (svgElement === undefined) {
41 | return undefined;
42 | }
43 |
44 | const svgRootAttrs = Object.fromEntries(
45 | Array.from(svgElement.attributes).map(({ name, value }) => [
46 | name,
47 | value,
48 | ]),
49 | );
50 |
51 | const svgInnerHtml = svgElement.innerHTML;
52 |
53 | setState(currentState => {
54 | if (currentState?.svgUrl === svgUrl) {
55 | return currentState;
56 | }
57 |
58 | return {
59 | svgUrl,
60 | svgInnerHtml,
61 | svgRootAttrs,
62 | };
63 | });
64 | })();
65 |
66 | return () => {
67 | isActive = false;
68 | };
69 | }, []);
70 |
71 | if (state === undefined) {
72 | return null;
73 | }
74 |
75 | const {
76 | svgRootAttrs: { class: class_svgRootAttrs, ...svgRootAttrs },
77 | svgInnerHtml,
78 | } = state;
79 |
80 | const svgRootProps = Object.fromEntries(
81 | Object.entries(svgRootAttrs).map(([key, value]) => [
82 | key
83 | .split("-")
84 | .map((part, index) =>
85 | index === 0 ? part : capitalize(part),
86 | )
87 | .join(""),
88 | value,
89 | ]),
90 | );
91 |
92 | return (
93 |