├── .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 | {alt} 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 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /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 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /src/stories/assets/svg/Tour.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/stories/assets/svg/account_v1.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 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 | 69 | 70 | {[false, true].map(darkMode => ( 71 | 72 |
73 | 74 | 75 | This is an error 76 | 77 | 78 | 79 | ))} 80 | 81 | 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 | 94 | 95 | {[false, true].map(darkMode => ( 96 | 97 |
98 | 99 | 100 | This is an success 101 | 102 | 103 | 104 | ))} 105 | 106 | 107 | 108 | ```tsx 109 | 110 | This is an success 111 | 112 | ``` 113 | 114 | ### Information alert 115 | 116 | Use to highlight important information 117 | 118 | 119 | 120 | 121 | 122 | This is an info 123 | 124 | 125 |
126 | 127 | 128 | This is an info 129 | 130 | 131 | 132 | 133 | 134 | ```tsx 135 | 136 | This is an info 137 | 138 | ``` 139 | 140 | ### Warning alert 141 | 142 | Use to highlight important information 143 | 144 | 145 | 146 | 147 | 148 | This is a warning 149 | 150 | 151 |
152 | 153 | 154 | This is a warning 155 | 156 | 157 | 158 | 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 | !!className) 99 | .join(" ")} 100 | dangerouslySetInnerHTML={{ __html: svgInnerHtml }} 101 | /> 102 | ); 103 | }), 104 | ); 105 | 106 | LazySvg.displayName = symToStr({ LazySvg }); 107 | 108 | export const createLazySvg = memoize((svgUrl: string) => { 109 | const LazySvgWithUrl = forwardRef< 110 | SVGSVGElement, 111 | Omit 112 | >((props, ref) => ); 113 | 114 | LazySvgWithUrl.displayName = LazySvg.displayName; 115 | 116 | return LazySvgWithUrl; 117 | }); 118 | 119 | export const fetchSvgAsHTMLElement = memoize( 120 | async (svgUrl: string) => { 121 | const rawSvgString = await (async () => { 122 | const safeUrl = getSafeUrl(svgUrl); 123 | 124 | if (safeUrl.startsWith("data:image/svg")) { 125 | const [meta, ...rest] = safeUrl.split(","); 126 | 127 | const data = rest.join(","); 128 | 129 | const [, encoding] = meta.split(";"); 130 | 131 | if (encoding?.toLowerCase() === "base64") { 132 | return atob(data); 133 | } 134 | 135 | return decodeURIComponent(data); 136 | } 137 | 138 | return fetch(getSafeUrl(svgUrl)) 139 | .then(response => response.text()) 140 | .catch(() => undefined); 141 | })(); 142 | 143 | if (rawSvgString === undefined) { 144 | return undefined; 145 | } 146 | 147 | const svgElement = (() => { 148 | let svgElement: SVGSVGElement | null; 149 | 150 | try { 151 | const parser = new DOMParser(); 152 | const doc = parser.parseFromString( 153 | rawSvgString, 154 | "image/svg+xml", 155 | ); 156 | svgElement = doc.querySelector("svg"); 157 | } catch (error) { 158 | console.error(`Failed to parse ${svgUrl}, ${String(error)}`); 159 | return undefined; 160 | } 161 | 162 | if (svgElement === null) { 163 | console.error(`${svgUrl} is empty`); 164 | return undefined; 165 | } 166 | 167 | return svgElement; 168 | })(); 169 | 170 | return svgElement; 171 | }, 172 | { promise: true }, 173 | ); 174 | -------------------------------------------------------------------------------- /src/tools/ReactComponent.ts: -------------------------------------------------------------------------------- 1 | import type { FC, ComponentClass } from "react"; 2 | 3 | export type ReactComponent = {}> = 4 | | ((props: Props) => ReturnType) 5 | | ComponentClass; 6 | -------------------------------------------------------------------------------- /src/tools/evtRootFontSizePx.ts: -------------------------------------------------------------------------------- 1 | import { Evt } from "evt"; 2 | import { onlyIfChanged } from "evt/operators/onlyIfChanged"; 3 | import { memoize } from "./memoize"; 4 | 5 | export const getEvtRootFontSizePx = memoize(() => { 6 | const evtRootFontSizePx = Evt.merge([ 7 | (() => { 8 | const evtRootStyleMutation = Evt.create(); 9 | 10 | const observer = new MutationObserver(() => { 11 | evtRootStyleMutation.post(); 12 | }); 13 | 14 | [document.body, document.documentElement].forEach(element => 15 | observer.observe(element, { 16 | attributes: true, 17 | attributeFilter: ["style"], 18 | }), 19 | ); 20 | 21 | return evtRootStyleMutation; 22 | })(), 23 | Evt.from(window, "focus"), 24 | ]) 25 | .toStateful() 26 | .pipe(() => { 27 | const value = window 28 | .getComputedStyle(document.documentElement) 29 | .getPropertyValue("font-size"); 30 | 31 | const match = value.match(/(\d+)px/); 32 | 33 | if (match === null) { 34 | return [16]; 35 | } 36 | 37 | const rootFontSizePx = parseFloat(match[1]); 38 | 39 | return [rootFontSizePx]; 40 | }) 41 | .pipe(onlyIfChanged()); 42 | 43 | return { evtRootFontSizePx }; 44 | }); 45 | -------------------------------------------------------------------------------- /src/tools/evtWindowInnerSize.ts: -------------------------------------------------------------------------------- 1 | import { Evt } from "evt"; 2 | import { onlyIfChanged } from "evt/operators/onlyIfChanged"; 3 | import { memoize } from "./memoize"; 4 | 5 | export const getEvtWindowInnerSize = memoize(() => { 6 | const evtWindowInnerSize = Evt.from(window, "resize") 7 | .toStateful() 8 | .pipe(() => [ 9 | { 10 | windowInnerWidth: window.innerWidth, 11 | windowInnerHeight: window.innerHeight, 12 | }, 13 | ]) 14 | .pipe(onlyIfChanged()); 15 | 16 | return { evtWindowInnerSize }; 17 | }); 18 | -------------------------------------------------------------------------------- /src/tools/getBrowser.ts: -------------------------------------------------------------------------------- 1 | import { memoize } from "./memoize"; 2 | 3 | export const getBrowser = memoize( 4 | (): "chrome" | "safari" | "firefox" | undefined => { 5 | const { userAgent } = navigator; 6 | 7 | for (const id of ["chrome", "safari", "firefox"] as const) { 8 | if (new RegExp(id, "i").test(userAgent)) { 9 | return id; 10 | } 11 | } 12 | }, 13 | ); 14 | -------------------------------------------------------------------------------- /src/tools/getContrastRatio.ts: -------------------------------------------------------------------------------- 1 | function getRGB(hex: string): [number, number, number] { 2 | const r = parseInt(hex.slice(1, 3), 16); 3 | const g = parseInt(hex.slice(3, 5), 16); 4 | const b = parseInt(hex.slice(5, 7), 16); 5 | return [r, g, b]; 6 | } 7 | 8 | function relativeLuminance([r, g, b]: [number, number, number]): number { 9 | const a = [r, g, b].map(v => { 10 | v /= 255; 11 | return v <= 0.03928 ? v / 12.92 : Math.pow((v + 0.055) / 1.055, 2.4); 12 | }); 13 | return 0.2126 * a[0] + 0.7152 * a[1] + 0.0722 * a[2]; 14 | } 15 | 16 | function contrastRatio(l1: number, l2: number): number { 17 | return (Math.max(l1, l2) + 0.05) / (Math.min(l1, l2) + 0.05); 18 | } 19 | 20 | export function getContrastRatio(params: { 21 | backgroundHex: string; 22 | textHex: string; 23 | }) { 24 | const { backgroundHex, textHex } = params; 25 | 26 | const backgroundLuminance = relativeLuminance(getRGB(backgroundHex)); 27 | const textLuminance = relativeLuminance(getRGB(textHex)); 28 | 29 | return contrastRatio(backgroundLuminance, textLuminance); 30 | } 31 | -------------------------------------------------------------------------------- /src/tools/getIsDarkModeEnabledOsDefault.ts: -------------------------------------------------------------------------------- 1 | export function getIsDarkModeEnabledOsDefault() { 2 | return ( 3 | window.matchMedia && 4 | window.matchMedia("(prefers-color-scheme: dark)").matches 5 | ); 6 | } 7 | -------------------------------------------------------------------------------- /src/tools/getSafeUrl.ts: -------------------------------------------------------------------------------- 1 | export function getSafeUrl(url: string) { 2 | if (url.startsWith("file://")) { 3 | return url; 4 | } 5 | 6 | if (url.startsWith("data:")) { 7 | return url; 8 | } 9 | 10 | let unsafeUrl = url; 11 | let toReturn = url; 12 | 13 | if (url.startsWith("/")) { 14 | unsafeUrl = `${window.location.origin}${url}`; 15 | } else if (!url.startsWith("http")) { 16 | unsafeUrl = `https://${url}`; 17 | toReturn = unsafeUrl; 18 | } 19 | 20 | try { 21 | new URL(unsafeUrl).href; 22 | } catch { 23 | throw new Error(`The url ${url} is not valid`); 24 | } 25 | 26 | return toReturn; 27 | } 28 | -------------------------------------------------------------------------------- /src/tools/memoize.ts: -------------------------------------------------------------------------------- 1 | type SimpleType = number | string | boolean | null | undefined; 2 | type FuncWithSimpleParams = (...args: T) => R; 3 | 4 | export function memoize( 5 | fn: FuncWithSimpleParams, 6 | options?: { 7 | argsLength?: number; 8 | max?: number; 9 | }, 10 | ): FuncWithSimpleParams { 11 | const cache = new Map>>(); 12 | 13 | const { argsLength = fn.length, max = Infinity } = options ?? {}; 14 | 15 | return ((...args: Parameters>) => { 16 | const key = JSON.stringify( 17 | args 18 | .slice(0, argsLength) 19 | .map(v => { 20 | if (v === null) { 21 | return "null"; 22 | } 23 | if (v === undefined) { 24 | return "undefined"; 25 | } 26 | switch (typeof v) { 27 | case "number": 28 | return `number-${v}`; 29 | case "string": 30 | return `string-${v}`; 31 | case "boolean": 32 | return `boolean-${v ? "true" : "false"}`; 33 | } 34 | }) 35 | .join("-sIs9sAslOdeWlEdIos3-"), 36 | ); 37 | 38 | if (cache.has(key)) { 39 | return cache.get(key); 40 | } 41 | 42 | if (max === cache.size) { 43 | for (const key of cache.keys()) { 44 | cache.delete(key); 45 | break; 46 | } 47 | } 48 | 49 | const value = fn(...args); 50 | 51 | cache.set(key, value); 52 | 53 | return value; 54 | }) as any; 55 | } 56 | -------------------------------------------------------------------------------- /src/tools/noUndefined.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Removes the enumerable properties whose values are 3 | * undefined. 4 | * 5 | * Example: 6 | * noUndefined({ "foo": undefined, "bar": 3 }) returns 7 | * a new object { "bar": 3 } 8 | */ 9 | export function noUndefined>(obj: T): T { 10 | const out: typeof obj = {} as any; 11 | 12 | for (const key in obj) { 13 | if (obj[key] === undefined) { 14 | continue; 15 | } 16 | 17 | out[key] = obj[key]; 18 | } 19 | 20 | return out; 21 | } 22 | -------------------------------------------------------------------------------- /src/tools/pxToNumber.ts: -------------------------------------------------------------------------------- 1 | export function pxToNumber(str: `${number}px`): number { 2 | return Number.parseFloat(str.split("px")[0]); 3 | } 4 | -------------------------------------------------------------------------------- /src/tools/useNonPostableEvtLike.ts: -------------------------------------------------------------------------------- 1 | import type { NonPostableEvtLike, NonPostableEvt, UnpackEvt } from "evt"; 2 | import { Evt } from "evt"; 3 | import { useGuaranteedMemo } from "powerhooks/useGuaranteedMemo"; 4 | import { useEvt } from "evt/hooks"; 5 | 6 | export function useNonPostableEvtLike< 7 | E extends NonPostableEvtLike | undefined, 8 | >(evtLike: E): NonPostableEvt> { 9 | const evt = useGuaranteedMemo(() => Evt.create(), [evtLike]); 10 | 11 | useEvt( 12 | ctx => { 13 | if (evtLike === undefined) { 14 | return; 15 | } 16 | evtLike.attach(ctx, data => evt.post(data)); 17 | }, 18 | [evtLike], 19 | ); 20 | 21 | return (evtLike === undefined ? undefined : evt) as any; 22 | } 23 | -------------------------------------------------------------------------------- /test-app/.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # production 12 | /build 13 | 14 | # misc 15 | .DS_Store 16 | .env.local 17 | .env.development.local 18 | .env.test.local 19 | .env.production.local 20 | 21 | npm-debug.log* 22 | yarn-debug.log* 23 | yarn-error.log* 24 | -------------------------------------------------------------------------------- /test-app/README.md: -------------------------------------------------------------------------------- 1 | # Getting Started with Create React App 2 | 3 | This project was bootstrapped with [Create React App](https://github.com/facebook/create-react-app). 4 | 5 | ## Available Scripts 6 | 7 | In the project directory, you can run: 8 | 9 | ### `yarn start` 10 | 11 | Runs the app in the development mode.\ 12 | Open [http://localhost:3000](http://localhost:3000) to view it in the browser. 13 | 14 | The page will reload if you make edits.\ 15 | You will also see any lint errors in the console. 16 | 17 | ### `yarn test` 18 | 19 | Launches the test runner in the interactive watch mode.\ 20 | See the section about [running tests](https://facebook.github.io/create-react-app/docs/running-tests) for more information. 21 | 22 | ### `yarn build` 23 | 24 | Builds the app for production to the `build` folder.\ 25 | It correctly bundles React in production mode and optimizes the build for the best performance. 26 | 27 | The build is minified and the filenames include the hashes.\ 28 | Your app is ready to be deployed! 29 | 30 | See the section about [deployment](https://facebook.github.io/create-react-app/docs/deployment) for more information. 31 | 32 | ### `yarn eject` 33 | 34 | **Note: this is a one-way operation. Once you `eject`, you can’t go back!** 35 | 36 | If you aren’t satisfied with the build tool and configuration choices, you can `eject` at any time. This command will remove the single build dependency from your project. 37 | 38 | Instead, it will copy all the configuration files and the transitive dependencies (webpack, Babel, ESLint, etc) right into your project so you have full control over them. All of the commands except `eject` will still work, but they will point to the copied scripts so you can tweak them. At this point you’re on your own. 39 | 40 | You don’t have to ever use `eject`. The curated feature set is suitable for small and middle deployments, and you shouldn’t feel obligated to use this feature. However we understand that this tool wouldn’t be useful if you couldn’t customize it when you are ready for it. 41 | 42 | ## Learn More 43 | 44 | You can learn more in the [Create React App documentation](https://facebook.github.io/create-react-app/docs/getting-started). 45 | 46 | To learn React, check out the [React documentation](https://reactjs.org/). 47 | -------------------------------------------------------------------------------- /test-app/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "spa", 3 | "version": "0.1.70", 4 | "private": true, 5 | "dependencies": { 6 | "@mui/material": "^6.1.7", 7 | "@emotion/react": "^11.4.1", 8 | "@emotion/styled": "^11.3.0", 9 | "tss-react": "^4.9.13", 10 | "powerhooks": "^0.27.2", 11 | "onyxia-ui": "file:../dist", 12 | "@types/node": "^12.0.0", 13 | "@types/react": "^18.0.14", 14 | "@types/react-dom": "^18.0.5", 15 | "react": "^18.3.1", 16 | "react-dom": "^18.3.1", 17 | "react-scripts": "5.0.1", 18 | "typescript": "^4.7.4", 19 | "mui-icons-material-lazy": "^1.0.2" 20 | }, 21 | "scripts": { 22 | "postinstall": "mui-icons-material-lazy postinstall", 23 | "start": "react-scripts start", 24 | "build": "react-scripts build", 25 | "test": "react-scripts test", 26 | "eject": "react-scripts eject" 27 | }, 28 | "eslintConfig": { 29 | "extends": [ 30 | "react-app", 31 | "react-app/jest" 32 | ] 33 | }, 34 | "browserslist": { 35 | "production": [ 36 | ">0.2%", 37 | "not dead", 38 | "not op_mini all" 39 | ], 40 | "development": [ 41 | "last 1 chrome version", 42 | "last 1 firefox version", 43 | "last 1 safari version" 44 | ] 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /test-app/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/InseeFrLab/onyxia-ui/18896e242dd81be703befc58c51811e477ac406f/test-app/public/favicon.ico -------------------------------------------------------------------------------- /test-app/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 12 | 13 | 17 | 18 | 27 | React App 28 | 29 | 30 | 31 |
32 | 42 | 43 | 44 | -------------------------------------------------------------------------------- /test-app/public/logo192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/InseeFrLab/onyxia-ui/18896e242dd81be703befc58c51811e477ac406f/test-app/public/logo192.png -------------------------------------------------------------------------------- /test-app/public/logo512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/InseeFrLab/onyxia-ui/18896e242dd81be703befc58c51811e477ac406f/test-app/public/logo512.png -------------------------------------------------------------------------------- /test-app/public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "React App", 3 | "name": "Create React App Sample", 4 | "icons": [ 5 | { 6 | "src": "favicon.ico", 7 | "sizes": "64x64 32x32 24x24 16x16", 8 | "type": "image/x-icon" 9 | }, 10 | { 11 | "src": "logo192.png", 12 | "type": "image/png", 13 | "sizes": "192x192" 14 | }, 15 | { 16 | "src": "logo512.png", 17 | "type": "image/png", 18 | "sizes": "512x512" 19 | } 20 | ], 21 | "start_url": ".", 22 | "display": "standalone", 23 | "theme_color": "#000000", 24 | "background_color": "#ffffff" 25 | } 26 | -------------------------------------------------------------------------------- /test-app/public/robots.txt: -------------------------------------------------------------------------------- 1 | # https://www.robotstxt.org/robotstxt.html 2 | User-agent: * 3 | Disallow: 4 | -------------------------------------------------------------------------------- /test-app/src/TextFormDialog.tsx: -------------------------------------------------------------------------------- 1 | import { useState, memo } from "react"; 2 | import { Dialog } from "onyxia-ui/Dialog"; 3 | import { TextField } from "onyxia-ui/TextField"; 4 | import { Button } from "onyxia-ui/Button"; 5 | import type { NonPostableEvt, UnpackEvt } from "evt"; 6 | import { useEvt } from "evt/hooks"; 7 | import { assert } from "tsafe/assert"; 8 | 9 | export type TextFormDialogProps = { 10 | evtOpen: NonPostableEvt<{ 11 | defaultText: string; 12 | resolveText: ( 13 | params: 14 | | { 15 | doProceed: false; 16 | text?: never; 17 | } 18 | | { 19 | doProceed: true; 20 | text: string; 21 | }, 22 | ) => void; 23 | }>; 24 | }; 25 | 26 | type OpenParams = UnpackEvt; 27 | 28 | export const TextFormDialog = memo((props: TextFormDialogProps) => { 29 | const { evtOpen } = props; 30 | 31 | const [openState, setOpenState] = useState< 32 | | { 33 | text: string; 34 | resolveText: OpenParams["resolveText"]; 35 | } 36 | | undefined 37 | >(undefined); 38 | 39 | useEvt( 40 | ctx => { 41 | evtOpen.attach(ctx, ({ defaultText, resolveText }) => 42 | setOpenState({ 43 | text: defaultText, 44 | resolveText, 45 | }), 46 | ); 47 | }, 48 | [evtOpen], 49 | ); 50 | 51 | const onCancel = () => { 52 | assert(openState !== undefined); 53 | 54 | openState.resolveText({ doProceed: false }); 55 | 56 | setOpenState(undefined); 57 | }; 58 | 59 | return ( 60 | 70 | { 76 | assert(openState !== undefined); 77 | setOpenState({ ...openState, text: value }); 78 | }} 79 | /> 80 | 81 | {/* 82 | new Array(10).fill(null).map((_, index) => ( 83 |

84 | Lorem ipsum dolor sit amet, consectetur 85 | adipiscing elit, sed do eiusmod tempor 86 | incididunt ut labore et dolore magna aliqua. 87 | Ut enim ad minim veniam, quis nostrud 88 | exercitation ullamco laboris nisi ut 89 | aliquip ex ea commodo consequat. Duis aute 90 | irure dolor in reprehenderit in voluptate 91 | velit esse cillum dolore eu fugiat nulla 92 | pariatur. Excepteur sint occaecat cupidatat 93 | non proident, sunt in culpa qui officia 94 | deserunt mollit anim id est laborum. 95 |

96 | )) 97 | */} 98 | 99 | ) 100 | } 101 | buttons={ 102 | <> 103 | 106 | 120 | 121 | } 122 | //doNotShowNextTimeText="Do not show next time" 123 | //onDoShowNextTimeValueChange={()=> {}} 124 | /> 125 | ); 126 | }); 127 | -------------------------------------------------------------------------------- /test-app/src/assets/bar.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /test-app/src/assets/foo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /test-app/src/index.tsx: -------------------------------------------------------------------------------- 1 | import { createRoot } from "react-dom/client"; 2 | import { OnyxiaUi } from "./theme"; 3 | import { MyComponent } from "./MyComponent"; 4 | 5 | createRoot(document.getElementById("root")!).render( 6 | 7 | 8 | , 9 | ); 10 | -------------------------------------------------------------------------------- /test-app/src/react-app-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /test-app/src/theme.ts: -------------------------------------------------------------------------------- 1 | import { 2 | createOnyxiaUi, 3 | defaultPalette, 4 | createDefaultColorUseCases, 5 | defaultGetTypographyDesc, 6 | } from "onyxia-ui"; 7 | import { createTextWithCustomTypos } from "onyxia-ui/Text"; 8 | import "onyxia-ui/assets/fonts/WorkSans/font.css"; 9 | import "onyxia-ui/assets/fonts/Marianne/font.css"; 10 | import logoSvgUrl from "onyxia-ui/assets/logo.svg"; 11 | import { createGetIconUrl } from "mui-icons-material-lazy"; 12 | 13 | //Import your custom icons 14 | import fooSvgUrl from "./assets/foo.svg"; 15 | import barSvgUrl from "./assets/bar.svg"; 16 | 17 | const { OnyxiaUi, ofTypeTheme } = createOnyxiaUi({ 18 | getTypographyDesc: params => { 19 | const typographyDesc = defaultGetTypographyDesc(params); 20 | 21 | return { 22 | ...typographyDesc, 23 | fontFamily: '"Work Sans", sans-serif', 24 | variants: { 25 | ...typographyDesc.variants, 26 | "display heading": { 27 | ...typographyDesc.variants["display heading"], 28 | fontFamily: "Marianne, sans-serif", 29 | }, 30 | //We add a typography variant to the default ones 31 | "my hero": { 32 | htmlComponent: "h1", 33 | //Be mindful to pick one of the fontWeight you imported 34 | //(in this example onyxia-design-lab/assets/fonts/work-sans.css) 35 | fontWeight: "bold", 36 | fontSizeRem: 4.5, 37 | lineHeightRem: 4, 38 | }, 39 | }, 40 | }; 41 | }, 42 | //We keep the default color palette but we add a custom color: a shiny pink. 43 | palette: { 44 | ...defaultPalette, 45 | shinyPink: { 46 | main: "#FF69B4", 47 | }, 48 | }, 49 | //We keep the default surceases colors except that we add 50 | //a new usage scenario: "flash" and we use our pink within. 51 | createColorUseCases: ({ isDarkModeEnabled, palette }) => ({ 52 | ...createDefaultColorUseCases({ isDarkModeEnabled, palette }), 53 | flashes: { 54 | cute: palette.shinyPink.main, 55 | warning: palette.orangeWarning.light, 56 | }, 57 | }), 58 | splashScreenParams: { 59 | assetUrl: logoSvgUrl, 60 | assetScaleFactor: 1, 61 | }, 62 | }); 63 | 64 | export { OnyxiaUi }; 65 | 66 | export type Theme = typeof ofTypeTheme; 67 | 68 | export const { Text } = createTextWithCustomTypos(); 69 | 70 | export const customIcons = { 71 | fooSvgUrl, 72 | barSvgUrl, 73 | }; 74 | 75 | export const { getIconUrl, getIconUrlByName } = createGetIconUrl({ 76 | BASE_URL: process.env.PUBLIC_URL, 77 | }); 78 | -------------------------------------------------------------------------------- /test-app/src/tss.ts: -------------------------------------------------------------------------------- 1 | import { createTss } from "tss-react"; 2 | import { useTheme } from "onyxia-ui"; 3 | import type { Theme } from "./theme"; 4 | 5 | export const { tss } = createTss({ 6 | useContext: function useContext() { 7 | const theme = useTheme(); 8 | return { theme }; 9 | }, 10 | }); 11 | 12 | export const useStyles = tss.create({}); 13 | -------------------------------------------------------------------------------- /test-app/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "lib": ["dom", "dom.iterable", "esnext"], 5 | "allowJs": true, 6 | "skipLibCheck": true, 7 | "esModuleInterop": true, 8 | "allowSyntheticDefaultImports": true, 9 | "strict": true, 10 | "forceConsistentCasingInFileNames": true, 11 | "noFallthroughCasesInSwitch": true, 12 | "module": "esnext", 13 | "moduleResolution": "node", 14 | "resolveJsonModule": true, 15 | "isolatedModules": true, 16 | "noEmit": true, 17 | "jsx": "react-jsx" 18 | }, 19 | "include": ["src"] 20 | } 21 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "CommonJS", 4 | "target": "ES6", 5 | "lib": ["es2015", "DOM", "esnext"], 6 | "esModuleInterop": true, 7 | "declaration": true, 8 | "outDir": "./dist", 9 | "sourceMap": true, 10 | "newLine": "LF", 11 | "noUnusedLocals": true, 12 | "noUnusedParameters": true, 13 | "incremental": true, 14 | "strict": true, 15 | "downlevelIteration": true, 16 | "jsx": "react-jsx", 17 | "noFallthroughCasesInSwitch": true, 18 | "skipLibCheck": true 19 | }, 20 | "include": ["src"], 21 | "exclude": ["src/stories"] 22 | } 23 | --------------------------------------------------------------------------------