diff --git a/.storybook/vite.config.ts b/.storybook/vite.config.ts deleted file mode 100644 index 70d62a4502..0000000000 --- a/.storybook/vite.config.ts +++ /dev/null @@ -1,44 +0,0 @@ -import { resolve } from 'path' -import svgr from '@svgr/rollup' -import react from '@vitejs/plugin-react' -import { defineConfig } from 'vite' -import { vanillaExtractPlugin } from '@vanilla-extract/vite-plugin'; - -// https://vitejs.dev/config/ -export default defineConfig({ - resolve: { - extensions: ['.mjs', '.js', '.ts', '.tsx', '.json', '.css'], - // alias is needed or Vite will not resolve the packages correctly with storybook - alias: { - '@ultraviolet/ui': resolve('packages/ui/src'), - '@ultraviolet/themes': resolve('packages/themes/src'), - '@ultraviolet/plus': resolve('packages/plus/src'), - '@ultraviolet/illustrations/various': resolve( - 'packages/illustrations/src/assets/various', - ), - '@ultraviolet/illustrations/products': resolve( - 'packages/illustrations/src/assets/products', - ), - '@ultraviolet/illustrations': resolve( - 'packages/illustrations/src/components', - ), - '@ultraviolet/form': resolve('packages/form/src'), - '@ultraviolet/fonts': resolve('packages/fonts/src'), - }, - }, - assetsInclude: ['**/*.md'], - build: { - outDir: 'build', - reportCompressedSize: true, - }, - optimizeDeps: { - exclude: ['@ultraviolet/*'], - }, - plugins: [ - svgr({ memo: true, svgo: false }), - react({ - jsxRuntime: 'automatic', - }), - vanillaExtractPlugin({}) - ], -}) diff --git a/Dockerfile b/Dockerfile index 0b6cbac1af..b20e703a99 100644 --- a/Dockerfile +++ b/Dockerfile @@ -2,14 +2,22 @@ FROM node:24.12.0-alpine WORKDIR /build ARG TURBO_TOKEN=token +ENV TURBO_TOKEN=${TURBO_TOKEN} -ENV TURBO_TOKEN ${TURBO_TOKEN} - -COPY . . +# Copy only necessary files for installation +COPY package.json pnpm-lock.yaml pnpm-workspace.yaml .npmrc ./ +# Install corepack and pnpm RUN npm install -g corepack@0.31.0 RUN corepack enable + +# Install dependencies RUN pnpm install --frozen-lockfile + +# Copy source code +COPY . . + +# Build storybook RUN pnpm turbo run build:storybook EXPOSE 80/tcp diff --git a/.storybook/.babelrc b/apps/storybook/.storybook/.babelrc similarity index 100% rename from .storybook/.babelrc rename to apps/storybook/.storybook/.babelrc diff --git a/.storybook/assets/brand-background.png b/apps/storybook/.storybook/assets/brand-background.png similarity index 100% rename from .storybook/assets/brand-background.png rename to apps/storybook/.storybook/assets/brand-background.png diff --git a/.storybook/assets/fonts/asap/Asap-Bold.woff2 b/apps/storybook/.storybook/assets/fonts/asap/Asap-Bold.woff2 similarity index 100% rename from .storybook/assets/fonts/asap/Asap-Bold.woff2 rename to apps/storybook/.storybook/assets/fonts/asap/Asap-Bold.woff2 diff --git a/.storybook/assets/fonts/asap/Asap-Medium.woff2 b/apps/storybook/.storybook/assets/fonts/asap/Asap-Medium.woff2 similarity index 100% rename from .storybook/assets/fonts/asap/Asap-Medium.woff2 rename to apps/storybook/.storybook/assets/fonts/asap/Asap-Medium.woff2 diff --git a/.storybook/assets/fonts/asap/Asap-Regular.woff2 b/apps/storybook/.storybook/assets/fonts/asap/Asap-Regular.woff2 similarity index 100% rename from .storybook/assets/fonts/asap/Asap-Regular.woff2 rename to apps/storybook/.storybook/assets/fonts/asap/Asap-Regular.woff2 diff --git a/.storybook/assets/fonts/inter/Inter-Medium.woff2 b/apps/storybook/.storybook/assets/fonts/inter/Inter-Medium.woff2 similarity index 100% rename from .storybook/assets/fonts/inter/Inter-Medium.woff2 rename to apps/storybook/.storybook/assets/fonts/inter/Inter-Medium.woff2 diff --git a/.storybook/assets/fonts/inter/Inter-Regular.woff2 b/apps/storybook/.storybook/assets/fonts/inter/Inter-Regular.woff2 similarity index 100% rename from .storybook/assets/fonts/inter/Inter-Regular.woff2 rename to apps/storybook/.storybook/assets/fonts/inter/Inter-Regular.woff2 diff --git a/.storybook/assets/fonts/inter/Inter-SemiBold.woff2 b/apps/storybook/.storybook/assets/fonts/inter/Inter-SemiBold.woff2 similarity index 100% rename from .storybook/assets/fonts/inter/Inter-SemiBold.woff2 rename to apps/storybook/.storybook/assets/fonts/inter/Inter-SemiBold.woff2 diff --git a/.storybook/assets/fonts/jetbrains/JetBrainsMono-Regular.woff2 b/apps/storybook/.storybook/assets/fonts/jetbrains/JetBrainsMono-Regular.woff2 similarity index 100% rename from .storybook/assets/fonts/jetbrains/JetBrainsMono-Regular.woff2 rename to apps/storybook/.storybook/assets/fonts/jetbrains/JetBrainsMono-Regular.woff2 diff --git a/.storybook/assets/fonts/space-grotesk/SpaceGrotesk-Medium.woff2 b/apps/storybook/.storybook/assets/fonts/space-grotesk/SpaceGrotesk-Medium.woff2 similarity index 100% rename from .storybook/assets/fonts/space-grotesk/SpaceGrotesk-Medium.woff2 rename to apps/storybook/.storybook/assets/fonts/space-grotesk/SpaceGrotesk-Medium.woff2 diff --git a/.storybook/assets/fonts/space-grotesk/SpaceGrotesk-Regular.woff2 b/apps/storybook/.storybook/assets/fonts/space-grotesk/SpaceGrotesk-Regular.woff2 similarity index 100% rename from .storybook/assets/fonts/space-grotesk/SpaceGrotesk-Regular.woff2 rename to apps/storybook/.storybook/assets/fonts/space-grotesk/SpaceGrotesk-Regular.woff2 diff --git a/.storybook/assets/fonts/space-grotesk/SpaceGrotesk.woff2 b/apps/storybook/.storybook/assets/fonts/space-grotesk/SpaceGrotesk.woff2 similarity index 100% rename from .storybook/assets/fonts/space-grotesk/SpaceGrotesk.woff2 rename to apps/storybook/.storybook/assets/fonts/space-grotesk/SpaceGrotesk.woff2 diff --git a/.storybook/assets/logo-dark.png b/apps/storybook/.storybook/assets/logo-dark.png similarity index 100% rename from .storybook/assets/logo-dark.png rename to apps/storybook/.storybook/assets/logo-dark.png diff --git a/.storybook/assets/logo-light.png b/apps/storybook/.storybook/assets/logo-light.png similarity index 100% rename from .storybook/assets/logo-light.png rename to apps/storybook/.storybook/assets/logo-light.png diff --git a/.storybook/components/DocsContainer.tsx b/apps/storybook/.storybook/components/DocsContainer.tsx similarity index 100% rename from .storybook/components/DocsContainer.tsx rename to apps/storybook/.storybook/components/DocsContainer.tsx diff --git a/.storybook/components/Page.tsx b/apps/storybook/.storybook/components/Page.tsx similarity index 100% rename from .storybook/components/Page.tsx rename to apps/storybook/.storybook/components/Page.tsx diff --git a/.storybook/components/globalStyle.css.ts b/apps/storybook/.storybook/components/globalStyle.css.ts similarity index 100% rename from .storybook/components/globalStyle.css.ts rename to apps/storybook/.storybook/components/globalStyle.css.ts diff --git a/.storybook/locales/en.json b/apps/storybook/.storybook/locales/en.json similarity index 100% rename from .storybook/locales/en.json rename to apps/storybook/.storybook/locales/en.json diff --git a/.storybook/locales/es.json b/apps/storybook/.storybook/locales/es.json similarity index 100% rename from .storybook/locales/es.json rename to apps/storybook/.storybook/locales/es.json diff --git a/.storybook/locales/fr.json b/apps/storybook/.storybook/locales/fr.json similarity index 100% rename from .storybook/locales/fr.json rename to apps/storybook/.storybook/locales/fr.json diff --git a/.storybook/main.ts b/apps/storybook/.storybook/main.ts similarity index 85% rename from .storybook/main.ts rename to apps/storybook/.storybook/main.ts index a43a627dd6..851537c76d 100644 --- a/.storybook/main.ts +++ b/apps/storybook/.storybook/main.ts @@ -3,13 +3,13 @@ import remarkGfm from 'remark-gfm' export default { stories: [ - '../packages/*/src/**/__stories__/index.stories.tsx', - '../utils/stories/src/**/*.mdx' + '../../../packages/*/src/**/__stories__/index.stories.tsx', + // '../utils/stories/src/**/*.mdx' ], addons: [ "@storybook/addon-links", - "@storybook/addon-a11y", + "@storybook/addon-a11y", "@storybook/addon-themes", "storybook-addon-tag-badges", { diff --git a/.storybook/manager.ts b/apps/storybook/.storybook/manager.ts similarity index 100% rename from .storybook/manager.ts rename to apps/storybook/.storybook/manager.ts diff --git a/.storybook/preview.tsx b/apps/storybook/.storybook/preview.tsx similarity index 98% rename from .storybook/preview.tsx rename to apps/storybook/.storybook/preview.tsx index ebf641c585..fcbf3df1fc 100644 --- a/.storybook/preview.tsx +++ b/apps/storybook/.storybook/preview.tsx @@ -185,9 +185,11 @@ const decorators = [ withThemeProvider, ] -export default { +const defaultExport = { parameters, decorators, preview, tags: ['autodocs'] } satisfies Preview + +export default defaultExport diff --git a/.storybook/storybookThemes.ts b/apps/storybook/.storybook/storybookThemes.ts similarity index 76% rename from .storybook/storybookThemes.ts rename to apps/storybook/.storybook/storybookThemes.ts index 2072d3a7f8..1527919f6b 100644 --- a/.storybook/storybookThemes.ts +++ b/apps/storybook/.storybook/storybookThemes.ts @@ -1,8 +1,8 @@ import { create } from 'storybook/theming' -import lightTheme, { darkTheme } from '../packages/ui/src/theme' -import logoDark from './assets/logo-dark.png' -import logoLight from './assets/logo-light.png' -import type lightBrandImage from './assets/scaleway-text-light.png' +import { consoleLightTheme, consoleDarkTheme } from '@ultraviolet/themes' +// import logoDark from './assets/logo-dark.png' +// import logoLight from './assets/logo-light.png' +// import type lightBrandImage from './assets/scaleway-text-light.png' enum Base { LIGHT = 'light', @@ -11,16 +11,16 @@ enum Base { type GenerateStorybookThemeProps = { base: Base - theme: typeof darkTheme | typeof lightTheme + theme: typeof consoleDarkTheme | typeof consoleLightTheme brandUrl: string - brandImage: typeof lightBrandImage + // brandImage: typeof lightBrandImage } const generateStorybookTheme = ({ base, theme, brandUrl, - brandImage, + // brandImage, }: GenerateStorybookThemeProps) => create({ base, @@ -39,7 +39,7 @@ const generateStorybookTheme = ({ // BIZARRE booleanBg: theme.colors.neutral.background, booleanSelectedBg: theme.colors.primary.background, - brandImage, + // brandImage, brandTitle: 'Ultraviolet UI', brandUrl, @@ -60,14 +60,14 @@ const generateStorybookTheme = ({ export const light = generateStorybookTheme({ base: Base.LIGHT, - brandImage: logoLight, + // brandImage: logoLight, brandUrl: 'https://github.com/scaleway/ultraviolet', - theme: lightTheme, + theme: consoleLightTheme, }) export const dark = generateStorybookTheme({ base: Base.DARK, - brandImage: logoDark, + // brandImage: logoDark, brandUrl: 'https://github.com/scaleway/ultraviolet', - theme: darkTheme, + theme: consoleDarkTheme, }) diff --git a/apps/storybook/.storybook/vite.config.ts b/apps/storybook/.storybook/vite.config.ts new file mode 100644 index 0000000000..8e560f377d --- /dev/null +++ b/apps/storybook/.storybook/vite.config.ts @@ -0,0 +1,26 @@ +import svgr from '@svgr/rollup' +import react from '@vitejs/plugin-react' +import { defineConfig } from 'vite' +import { vanillaExtractPlugin } from '@vanilla-extract/vite-plugin'; + +// https://vitejs.dev/config/ +export default defineConfig({ + resolve: { + extensions: ['.mjs', '.js', '.ts', '.tsx', '.json', '.css'], + }, + assetsInclude: ['**/*.md'], + build: { + outDir: 'build', + reportCompressedSize: true, + }, + optimizeDeps: { + exclude: ['@ultraviolet/*'], + }, + plugins: [ + svgr({ memo: true, svgo: false }), + react({ + jsxRuntime: 'automatic', + }), + vanillaExtractPlugin({}) + ], +}) diff --git a/apps/storybook/package.json b/apps/storybook/package.json new file mode 100644 index 0000000000..f9cb19c3ce --- /dev/null +++ b/apps/storybook/package.json @@ -0,0 +1,54 @@ +{ + "name": "@ultraviolet/storybook", + "version": "0.0.0", + "description": "Ultraviolet Storybook", + "repository": { + "type": "git", + "url": "git+https://github.com/scaleway/ultraviolet.git", + "directory": "utils/stories" + }, + "scripts": { + "typecheck": "tsc --noEmit", + "start:storybook": "STORYBOOK_ENVIRONMENT=development storybook dev -p 6006", + "start:storybook:production": "STORYBOOK_ENVIRONMENT=production storybook dev -p 6006", + "build:storybook": "STORYBOOK_ENVIRONMENT=production storybook build", + "build:storybook:stats": "pnpm run build:storybook -- --webpack-stats-json" + }, + "keywords": [ + "fonts", + "ui" + ], + "license": "Apache-2.0", + "engines": { + "node": ">=18.x", + "pnpm": ">=10.x" + }, + "os": [ + "darwin", + "linux" + ], + "type": "module", + "dependencies": { + "@storybook/addon-a11y": "10.1.10", + "@storybook/addon-docs": "10.1.10", + "@storybook/addon-links": "10.1.10", + "@storybook/addon-themes": "10.1.10", + "@storybook/builder-vite": "10.1.10", + "@storybook/mdx2-csf": "1.1.0", + "@storybook/react-vite": "10.1.10", + "@types/node": "24.10.4", + "@ultraviolet/form": "workspace:*", + "@ultraviolet/icons": "workspace:*", + "@ultraviolet/illustrations": "workspace:*", + "@ultraviolet/plus": "workspace:*", + "@ultraviolet/themes": "workspace:*", + "@ultraviolet/ui": "workspace:*", + "@vanilla-extract/css": "1.17.4", + "@vanilla-extract/dynamic": "2.1.5", + "@vanilla-extract/recipes": "0.5.7", + "@vanilla-extract/sprinkles": "1.6.5", + "react": "19.2.3", + "storybook": "10.1.10", + "storybook-addon-tag-badges": "3.0.4" + } +} diff --git a/apps/storybook/src/Changelog/Changelog.stories.tsx b/apps/storybook/src/Changelog/Changelog.stories.tsx new file mode 100644 index 0000000000..5ecfd711bb --- /dev/null +++ b/apps/storybook/src/Changelog/Changelog.stories.tsx @@ -0,0 +1,33 @@ +import { Markdown } from '@storybook/addon-docs/blocks' +import { Stack, Tabs } from '@ultraviolet/ui' +import { useState } from 'react' +import ChangelogMdForm from '../../../../packages/form/CHANGELOG.md?raw' +import ChangelogMdIcons from '../../../../packages/icons/CHANGELOG.md?raw' +import ChangelogMdPlus from '../../../../packages/plus/CHANGELOG.md?raw' +import ChangelogMdThemes from '../../../../packages/themes/CHANGELOG.md?raw' +import ChangelogMdUi from '../../../../packages/ui/CHANGELOG.md?raw' + +export const Changelog = () => { + const [selected, setSelected] = useState('components') + const onChangeHandler = (e: string | number) => setSelected(e) + + return ( + + + Components + Form + Themes + Icons + Plus + + + {selected === 'ui' ? {ChangelogMdUi} : null} + {selected === 'form' ? {ChangelogMdForm} : null} + {selected === 'themes' ? {ChangelogMdThemes} : null} + {selected === 'icons' ? {ChangelogMdIcons} : null} + {selected === 'plus' ? {ChangelogMdPlus} : null} + + ) +} + +export default Changelog diff --git a/apps/storybook/src/Changelog/index.mdx b/apps/storybook/src/Changelog/index.mdx new file mode 100644 index 0000000000..f3458c8835 --- /dev/null +++ b/apps/storybook/src/Changelog/index.mdx @@ -0,0 +1,9 @@ +import { Meta } from '@storybook/addon-docs/blocks' +import ThemeWrapper from '../components/ThemeWrapper' +import { Changelog } from './Changelog.stories' + + + + + + diff --git a/apps/storybook/src/ComponentState/ComponentState.tsx b/apps/storybook/src/ComponentState/ComponentState.tsx new file mode 100644 index 0000000000..fc0154edb8 --- /dev/null +++ b/apps/storybook/src/ComponentState/ComponentState.tsx @@ -0,0 +1,187 @@ +// oxlint-disable import/no-namespace +// +import { linkTo } from '@storybook/addon-links' +import { Button, Stack, Table, Text } from '@ultraviolet/ui' +import { useEffect, useState } from 'react' +import * as components from '../../../../packages/ui/src/components' + +const findComponentState = (parameters: { + deprecated?: boolean + experimental?: boolean +}) => { + if (parameters?.deprecated) { + return 'โ›” Deprecated' + } + + if (parameters?.experimental) { + return '๐Ÿงช Experimental' + } + + return 'โœ… Stable' +} + +const componentsNames = Object.keys(components) + +const ComponentState = () => { + const [modules, setModules] = useState< + | PromiseSettledResult<{ + default: { title: string; parameters: { deprecated: boolean } } + }>[] + | null + >(null) + + useEffect(() => { + Promise.allSettled( + componentsNames.map( + name => + import( + `../../../../packages/ui/src/components/${name}/__stories__/index.stories.tsx` + ), + ), + ) + .then(localModules => { + setModules(localModules) + }) + .catch(error => { + const { error: consoleError } = console + + consoleError('Error loading component stories:', error) + }) + }, []) + + return ( + + + Here you will find all our components and their states. They are updated + automatically based on configuration of the component story. + + + + + Definition of states + + + + โœ… Stable + + + Stable state means the component is ready for production. If a + breaking change occurs it will generate a major version. + + + + + + ๐Ÿงช Experimental + + + Experimental state means the component is being tested and props + might change in the future. The component itself might even + disappear if we don't find a real purpose for it. This state is + also used for new version of a component (ex: Button v2) that we + want to test before replacing the old one. In any case{' '} + + this state means the component is not ready for production + + . + + + An experimental component won't generate major version when + having a breaking change. + + + + + + โ›” Deprecated + + + Deprecated state means the component is not recommended for use and + will be removed in the future. When seeing a component you use being + deprecated you should start migrating to another component as soon + as possible. To know what to use instead you can check the story of + the deprecated component. + + + + + + Components list + + + + + + Number of components + + : {componentsNames.length} + + + + {modules?.map(module => { + if (module.status === 'fulfilled') { + const desctructuredName: string[] = + module.value.default.title + .replace('Components/', '') + .split('/') ?? [] + + const componentCategory = desctructuredName[1] + ? desctructuredName[0] + : 'Others' + const componentName = desctructuredName[1] + ? desctructuredName[1] + : desctructuredName[0] + + const componentState = findComponentState( + module.value.default.parameters, + ) + + return ( + + + + + + + + + {componentCategory} + + + + + {componentState} + + + + ) + } + + return null + })} + +
+
+
+
+ ) +} + +export default ComponentState diff --git a/apps/storybook/src/ComponentState/index.mdx b/apps/storybook/src/ComponentState/index.mdx new file mode 100644 index 0000000000..fd571a1d13 --- /dev/null +++ b/apps/storybook/src/ComponentState/index.mdx @@ -0,0 +1,11 @@ +import { Meta } from '@storybook/addon-docs/blocks' +import ComponentState from './ComponentState' +import ThemeWrapper from '../components/ThemeWrapper' + + + +# Component state + + + + diff --git a/apps/storybook/src/Documentation.mdx b/apps/storybook/src/Documentation.mdx new file mode 100644 index 0000000000..3610357722 --- /dev/null +++ b/apps/storybook/src/Documentation.mdx @@ -0,0 +1,133 @@ +import { Meta } from '@storybook/addon-docs/blocks' + + + +# Documentation Guidelines + +This page explains what are the guidelines for writing documentation for components as a maintainer. + +## Folder and files structure + +First of all, you need to understand how is structured our current stories. We have a folder for each component, +and inside of it, we have a folder for stories and a file for each story. If we take the example of Button component +here is how the folder should look alike: + +```bash +Button +โ”œโ”€โ”€ __stories__ +โ”‚ โ”œโ”€โ”€ index.stories.tsx +โ”‚ โ”œโ”€โ”€ Template.stories.tsx +โ”‚ โ”œโ”€โ”€ Playground.stories.tsx +``` + +*** + +`index.stories.tsx` contains exports of each story and the parameters for the component story page. Here is an example with just Playground story: + +```tsx +import type { Meta } from '@storybook/react-vite' +import { Button } from '..' + +export default { + component: Button, + decorators: [ + StoryComponent => ( +
+ +
+ ), + ], + parameters: { + docs: { + description: { + component: 'A button is a component used to define a call to action', + }, + }, + }, + title: 'Components/Action/Button', +} as Meta + +export { Playground } from './Playground.stories' +``` + +Here we have the `component` parameter that is used to define the component that the story is related to. The `decorators` +parameter is used to define the decorators that will be applied to the story. The `parameters` parameter is used to define the parameters for the component story page. +The `title` parameter is used to define the title of the story. + +*** + +`Template.stories.tsx` contains the default template of a story. If your story only change properties values you can use this template as base. Here is an example: + +```tsx +import type { StoryFn } from '@storybook/react-vite' +import { Button } from '..' + +export const Template: StoryFn = ({ ...props }) => ( + + ) : null} + + + + ) + })} + + + {confirmResetForm ? ( + + + + + ) : ( + + )} + + Generate + + + ) +} diff --git a/apps/storybook/src/Tools/ThemeGenerator/FormContent/style.css.ts b/apps/storybook/src/Tools/ThemeGenerator/FormContent/style.css.ts new file mode 100644 index 0000000000..51a81e6ea8 --- /dev/null +++ b/apps/storybook/src/Tools/ThemeGenerator/FormContent/style.css.ts @@ -0,0 +1,11 @@ +import { style } from '@vanilla-extract/css' + +export const capitalizeText = style({ + selectors: { + '&::first-letter': { + textTransform: 'capitalize', + }, + }, +}) + +export const row = style({ width: '100%' }) diff --git a/apps/storybook/src/Tools/ThemeGenerator/ThemeResult/CodeIntegration.tsx b/apps/storybook/src/Tools/ThemeGenerator/ThemeResult/CodeIntegration.tsx new file mode 100644 index 0000000000..dd33db46d1 --- /dev/null +++ b/apps/storybook/src/Tools/ThemeGenerator/ThemeResult/CodeIntegration.tsx @@ -0,0 +1,51 @@ +import type { UltravioletUITheme } from '@ultraviolet/ui' +import { Snippet, Stack, Tabs, Text } from '@ultraviolet/ui' +import { useState } from 'react' +import { snippetResult } from './styles.css' + +type CodeIntegrationProps = { + theme: UltravioletUITheme +} + +const reactCode = (theme: string) => ` +import { Button, normalize } from '@ultraviolet/ui' +import { ThemeProvider } from "@ultraviolet/themes" +import "@ultraviolet/themes/global" + +const THEME = ${theme} + +const App = () => ( + + + +) + +export default App +` + +export const CodeIntegration = ({ theme }: CodeIntegrationProps) => { + const [tabState, setTabState] = useState(1) + const formattedTheme = JSON.stringify(theme, null, 4) + + return ( + + + Code integration + + + setTabState(e)} + selected={tabState} + > + JSON + React + + + {tabState === 1 ? formattedTheme : reactCode(formattedTheme)} + + + + ) +} diff --git a/apps/storybook/src/Tools/ThemeGenerator/ThemeResult/Demo.tsx b/apps/storybook/src/Tools/ThemeGenerator/ThemeResult/Demo.tsx new file mode 100644 index 0000000000..60422e1a4f --- /dev/null +++ b/apps/storybook/src/Tools/ThemeGenerator/ThemeResult/Demo.tsx @@ -0,0 +1,240 @@ +import { + AlertCircleIcon, + ArrowRightIcon, + CheckIcon, + ClockOutlineIcon, +} from '@ultraviolet/icons' +import { + Alert, + Avatar, + Badge, + Button, + Card, + Checkbox, + Link, + RadioGroup, + Row, + SelectableCard, + Stack, + Status, + StepList, + Stepper, + SwitchButton, + Tabs, + Text, + Toggle, +} from '@ultraviolet/ui' +import type { ChangeEvent } from 'react' +import { useState } from 'react' +import { + themeGeneratorContainer, + themeGeneratorStack, + themeGeneratorStepList, + themeGeneratorStepper, +} from './styles.css' + +export const Demo = () => { + const [tabState, setTabState] = useState(1) + const [radioState, setRadioState] = useState('option-1') + const [buttonLoading, setButtonLoading] = useState(false) + const [switchState, setSwitchState] = useState<'downgrade' | 'upgrade'>( + 'downgrade', + ) + const [selectableCardState, setSelectableCardState] = useState('option-1') + + return ( +
+ + + setTabState(e)} + selected={tabState} + > + UI + Form + Icons + Core + + + + + + 21.08.2023 + + + + + Update soon available + + + + + A new major version of Ultraviolet is coming soon. It will + include a lot of new features and improvements.  + + Learn more + + + + I accept terms and conditions + + + + + + + + + Initialize + Create + Done + + + UV-UI + UV-CORE + UV-FORM + + + Badge + + Badge + Badge + + + + + + + } + sentiment="success" + size="small" + > + Registration completed + + } + sentiment="info" + size="small" + > + You have 10 days of trial + + + + + + + + Seems like an error occurred + + + Do not forget your towel + + + It is possible to cut onion without crying + + + You have update Ultraviolet UI + + + + + + + + + + Review from Marc - 2 days ago + + + Ultraviolet is the best UI library I... + + + + + + + + + ) => + setSwitchState( + event.target.value as 'downgrade' | 'upgrade', + ) + } + value={switchState} + > + + Downgrade + + + Upgrade + + + ) => + setRadioState(event.target.value) + } + value={radioState} + > + + + + + + + + + setSelectableCardState(event.currentTarget.value) + } + value="option-1" + /> + + setSelectableCardState(event.currentTarget.value) + } + value="option-2" + /> + + + + +
+ ) +} diff --git a/apps/storybook/src/Tools/ThemeGenerator/ThemeResult/index.tsx b/apps/storybook/src/Tools/ThemeGenerator/ThemeResult/index.tsx new file mode 100644 index 0000000000..700bf4498c --- /dev/null +++ b/apps/storybook/src/Tools/ThemeGenerator/ThemeResult/index.tsx @@ -0,0 +1,79 @@ +import { ArrowLeftIcon, EyeIcon, EyeOffIcon } from '@ultraviolet/icons' +import { ThemeProvider } from '@ultraviolet/themes' +import type { UltravioletUITheme } from '@ultraviolet/ui' +import { + Button, + theme as consoleLightTheme, + Row, + Stack, + Text, +} from '@ultraviolet/ui' +import { useCallback, useState } from 'react' +import { CodeIntegration } from './CodeIntegration' +import { Demo } from './Demo' // For some reason Global doesn't work here this is the workaround I found + +type ThemeResultProps = { + theme: UltravioletUITheme + setTheme: (theme: UltravioletUITheme) => void + generatedPalette: UltravioletUITheme + setStep: (step: number) => void +} + +export const ThemeResult = ({ + theme, + setTheme, + generatedPalette, + setStep, +}: ThemeResultProps) => { + const [isVisible, setIsVisible] = useState(false) + + const onMouseUp = useCallback(() => { + setIsVisible(false) + setTheme(generatedPalette) + }, [generatedPalette, setTheme]) + + const onMouseDown = useCallback(() => { + setIsVisible(true) + setTheme(consoleLightTheme) + }, [setTheme]) + + return ( + + +
+ +
+ + + Generated Result + + +
+ +
+
+ + + + + + +
+ ) +} diff --git a/apps/storybook/src/Tools/ThemeGenerator/ThemeResult/styles.css.ts b/apps/storybook/src/Tools/ThemeGenerator/ThemeResult/styles.css.ts new file mode 100644 index 0000000000..7e7df5459e --- /dev/null +++ b/apps/storybook/src/Tools/ThemeGenerator/ThemeResult/styles.css.ts @@ -0,0 +1,23 @@ +import { theme } from '@ultraviolet/themes' +import { globalStyle, style } from '@vanilla-extract/css' + +export const snippetResult = style({}) + +globalStyle(`${snippetResult} pre`, { + padding: theme.space[2], +}) + +export const themeGeneratorStepper = style({ + padding: `0 ${theme.space[2]}`, +}) + +export const themeGeneratorStepList = style({ margin: 0 }) + +export const themeGeneratorStack = style({ gap: 6 }) + +export const themeGeneratorContainer = style({ + background: theme.colors.neutral.background, + boxShadow: theme.shadows.hoverNeutral, + borderRadius: theme.radii.large, + padding: theme.space[4], +}) diff --git a/apps/storybook/src/Tools/ThemeGenerator/contants.ts b/apps/storybook/src/Tools/ThemeGenerator/contants.ts new file mode 100644 index 0000000000..6a5a26ed00 --- /dev/null +++ b/apps/storybook/src/Tools/ThemeGenerator/contants.ts @@ -0,0 +1,71 @@ +export const hexadecimalColorRegex = /^#([A-Fa-f0-9]{6}|[A-Fa-f0-9]{3})$/i + +export const SHADES_KEYS = [ + '100', + '200', + '300', + '400', + '500', + '600', + '700', + '800', + '900', + '1000', + '1100', + '1200', + '1300', + '1400', +].toReversed() + +/** + * This is the mapping between shades name and sentiments names. + * If there is an array it means that one sentiment share the same shades. + */ +export const SHADES_KEYS_MATCHING = { + danger: 'fuchsia', + info: 'blue', + primary: 'violet', + secondary: 'purple', + success: 'emerald', + warning: ['yellow', 'brown'], +} + +/** + * Those are the default values in the form + */ +export const INITIAL_VALUES = { + sentiment_neutral: 'neutral', + sentiment_neutral_value: '#FFFFFF', + sentiments: [ + { + key: 'primary', + required: true, + value: '#521094', + }, + { + key: 'secondary', + required: true, + value: '#b824f9', + }, + { + key: 'success', + required: true, + value: '#2c8564', + }, + { + key: 'warning', + required: true, + value: '#fbc600', + }, + { + key: 'danger', + required: true, + value: '#e51963', + }, + { + key: 'info', + required: true, + value: '#0078d2', + }, + ], +} as const diff --git a/apps/storybook/src/Tools/ThemeGenerator/helpers.ts b/apps/storybook/src/Tools/ThemeGenerator/helpers.ts new file mode 100644 index 0000000000..5300c5e8ad --- /dev/null +++ b/apps/storybook/src/Tools/ThemeGenerator/helpers.ts @@ -0,0 +1,34 @@ +// eslint-disable no-bitwise +// Function imported from https://github.com/PimpTrizkit/PJs/wiki/12.-Shade,-Blend-and-Convert-a-Web-Color-(pSBC.js)#stackoverflow-archive-begin to shade hexa colors + +type ShadeHexColorType = (color: string, percent: number) => string + +export const shadeHexColor: ShadeHexColorType = (color, percent) => { + const f = Number.parseInt(color.slice(1), 16) + const t = percent < 0 ? 0 : 255 + const p = percent < 0 ? percent * -1 : percent + + const R = f >> 16 + + const G = (f >> 8) & 0x00ff + + const B = f & 0x0000ff + + return `#${( + 0x1000000 + + (Math.round((t - R) * p) + R) * 0x10000 + + (Math.round((t - G) * p) + G) * 0x100 + + (Math.round((t - B) * p) + B) + ) + .toString(16) + .slice(1)}` +} + +export const generateShadeContrast = ( + shadeKey: string, + value: string, + index: number, +) => + Number(shadeKey) < 900 + ? shadeHexColor(value, (1 / 15) * (index + 1)) + : shadeHexColor(value, -(1 / 15) * (index + 1)) diff --git a/apps/storybook/src/Tools/ThemeGenerator/index.tsx b/apps/storybook/src/Tools/ThemeGenerator/index.tsx new file mode 100644 index 0000000000..27636ef9a9 --- /dev/null +++ b/apps/storybook/src/Tools/ThemeGenerator/index.tsx @@ -0,0 +1,159 @@ +import { Form, useForm } from '@ultraviolet/form' +import type { UltravioletUITheme } from '@ultraviolet/ui' +import { theme as consoleLightTheme, Stack, Text } from '@ultraviolet/ui' +import { useCallback, useEffect, useState } from 'react' +import { TOKENS_URL } from '../../../../../utils/scripts/figma-synchronise-token/constants' +import { generatePalette } from '../../../../../utils/scripts/figma-synchronise-token/generatePalette' +import { INITIAL_VALUES, SHADES_KEYS, SHADES_KEYS_MATCHING } from './contants' +import { FormContent } from './FormContent' +import { generateShadeContrast } from './helpers' +import { ThemeResult } from './ThemeResult' + +type JsonTokenType = { + paletteLight: { + shades: Record< + string, + { + value: string + type: 'color' + } + > + } +} + +export const ThemeGenerator = () => { + const [step, setStep] = useState(0) + const [generatedPalette, setGeneratedPalette] = useState(consoleLightTheme) + const [theme, setTheme] = useState(consoleLightTheme) + const [savedFormValues, setSavedFormValues] = useState< + typeof INITIAL_VALUES | null + >(null) + + const methods = useForm({ + mode: 'onChange', + values: savedFormValues ?? INITIAL_VALUES, + }) + + useEffect(() => { + setTheme(generatedPalette) + }, [generatedPalette]) + + const onSubmit = useCallback(async (values: typeof INITIAL_VALUES) => { + setSavedFormValues(values) + const shades = values.sentiments.reduce((acc, { key, value }) => { + if (Array.isArray(SHADES_KEYS_MATCHING[key])) { + return { + ...(SHADES_KEYS_MATCHING[key] as string[]).reduce( + (firstacc, localKey) => ({ + [localKey]: SHADES_KEYS.reduce( + (secondAcc, shadeKey, index) => ({ + [shadeKey]: { + type: 'color', + value: generateShadeContrast(shadeKey, value, index), + }, + ...secondAcc, + }), + {}, + ), + ...firstacc, + }), + {}, + ), + ...acc, + } + } + + return { + [SHADES_KEYS_MATCHING[key]]: SHADES_KEYS.reduce( + (secondAcc, shadeKey, index) => ({ + [shadeKey]: { + type: 'color', + value: generateShadeContrast(shadeKey, value, index), + }, + ...secondAcc, + }), + {}, + ), + ...acc, + } + }, {}) + + const figmaTokensResponse = await fetch(TOKENS_URL as string) + const figmaTokensJson = (await figmaTokensResponse.json()) as JsonTokenType + + const overloadedTokens = { + ...figmaTokensJson, + paletteLight: { + ...figmaTokensJson.paletteLight, + shades: { + ...figmaTokensJson.paletteLight.shades, + ...shades, + }, + }, + } + + const tempGeneratedPalette = generatePalette(overloadedTokens, { + inputTheme: 'productLight', + outputTheme: 'light', + palette: 'paletteLight', + }) as UltravioletUITheme + + setGeneratedPalette(tempGeneratedPalette) + setStep(1) + }, []) + + return ( + + {step === 0 ? ( + + + + โœจ Theme generator + + + There is around 175 token colors in Ultraviolet theme. It + can be hard to set each of them one by one and understanding their + usage in the components. Theme generator is here to help you + create a new theme for your project FAST ๐Ÿš€. + + + + How does it work? + + + You basically just need to fill this form with the name and color + of each sentiment you want to use in your project. The color you + will set will be the most used color in the theme and the other + colors will be generated from it. So do not choose a color too + dark or too bright. + + +
'', + max: () => '', + maxDate: () => '', + maxLength: () => '', + min: () => '', + minDate: () => '', + minLength: () => '', + pattern: () => 'The hexadecimal color is not valid.', + required: () => '', + }} + methods={methods} + onSubmit={onSubmit} + > + + +
+ ) : ( + + )} +
+ ) +} diff --git a/apps/storybook/src/Tools/index.mdx b/apps/storybook/src/Tools/index.mdx new file mode 100644 index 0000000000..7d8dc5446d --- /dev/null +++ b/apps/storybook/src/Tools/index.mdx @@ -0,0 +1,9 @@ +import { Meta } from '@storybook/addon-docs/blocks' +import { ThemeGenerator } from './ThemeGenerator/' +import ThemeWrapper from '../components/ThemeWrapper' + + + + + + diff --git a/apps/storybook/src/components/Colors.tsx b/apps/storybook/src/components/Colors.tsx new file mode 100644 index 0000000000..5e4f418996 --- /dev/null +++ b/apps/storybook/src/components/Colors.tsx @@ -0,0 +1,336 @@ +import type { consoleLightTheme } from '@ultraviolet/themes' +import { useTheme } from '@ultraviolet/themes' +import { Card, Row, Separator, Stack, Text } from '@ultraviolet/ui' +import { assignInlineVars } from '@vanilla-extract/dynamic' +import { + capitalizedText, + card, + computedBackground, + noMarginText, + paddingCard, + separator, +} from '../styles.css' +import ThemeWrapper from '../ThemeWrapper' + +type Color = Extract< + keyof typeof consoleLightTheme.colors, + | 'primary' + | 'secondary' + | 'neutral' + | 'success' + | 'danger' + | 'warning' + | 'info' +> + +type AvailableContexts = keyof (typeof consoleLightTheme)['colors'][Color] + +const Colors = () => { + const theme = useTheme() + + const filteredColors = Object.keys(theme.colors).filter( + color => !['other', 'overlay'].includes(color), + ) as Color[] + + const dataColors = theme.colors.other.data.charts + const iconColors = theme.colors.other.icon + const gradientBackgroundColors = theme.colors.other.gradients.background + + return ( + + + Sentiments + + {filteredColors.map(sentiment => { + const colorContextKeys = Object.keys( + theme.colors[sentiment], + ) as AvailableContexts[] + + return ( + + + {sentiment} + + + + {colorContextKeys + .filter(context => context.includes('background')) + .map(context => ( + + + + {context} + + + {theme.colors[sentiment][context]} + + + + + {' '} + + + ))} + + + {colorContextKeys + .filter(context => context.includes('text')) + .map(context => ( + + + + {context} + + + {theme.colors[sentiment][context]} + + + + {' '} + + + ))} + + + {colorContextKeys + .filter(context => context.includes('border')) + .map(context => ( + + + + {context} + + + {theme.colors[sentiment][context]} + + + + {' '} + + + ))} + + + + + ) + })} + + + + Other + + + Data + + + {(Object.keys(dataColors) as (keyof typeof dataColors)[]).map( + data => ( + + + + {data} + + + {dataColors[data]} + + + + {' '} + + + ), + )} + + + Gradients + + + {Object.keys(gradientBackgroundColors) + .filter( + background => + ![ + 'gold', + 'purple', + 'strong', + 'accent', + 'aqua', + 'blue', + 'emerald', + 'fuschia', + 'magenta', + 'primary', + ].includes(background), + ) + .map(type => ( + + + {type} + + + {Object.keys( + gradientBackgroundColors[ + type as keyof typeof gradientBackgroundColors + ], + ).map(background => { + const gradientBackgroundColorsType = + gradientBackgroundColors[ + type as keyof typeof gradientBackgroundColors + ] + + const gradient = gradientBackgroundColorsType[ + background as keyof typeof gradientBackgroundColorsType + ].replace(/;$/, '') + + return ( + + + + {background} + + + + {' '} + + + ) + })} + + + ))} + + + Icon + + + {Object.keys(iconColors).map(type => ( + + + + {type} + + + {Object.keys(iconColors[type as keyof typeof iconColors]).map( + sentiment => ( + + + {sentiment} + + + {Object.keys( + // @ts-expect-error can't infer properly + // eslint-disable-next-line @typescript-eslint/no-unsafe-argument + iconColors[type][sentiment], + ).map(value => ( + + + + {value} + + + { + // @ts-expect-error can't infer properly + iconColors[type][sentiment][value] + } + + + + {' '} + + + ))} + + + ), + )} + + + + ))} + + + + ) +} + +const Container = () => ( + + + +) + +export default Container diff --git a/apps/storybook/src/components/ThemeWrapper.tsx b/apps/storybook/src/components/ThemeWrapper.tsx new file mode 100644 index 0000000000..534e40d28d --- /dev/null +++ b/apps/storybook/src/components/ThemeWrapper.tsx @@ -0,0 +1,8 @@ +import { consoleLightTheme, ThemeProvider } from '@ultraviolet/themes' +import type { ReactNode } from 'react' + +const ThemeWrapper = ({ children }: { children: ReactNode }) => ( + {children} +) + +export default ThemeWrapper diff --git a/apps/storybook/src/migrations/form/MigrationFormV3.mdx b/apps/storybook/src/migrations/form/MigrationFormV3.mdx new file mode 100644 index 0000000000..585c5d8a63 --- /dev/null +++ b/apps/storybook/src/migrations/form/MigrationFormV3.mdx @@ -0,0 +1,67 @@ +import { Meta } from '@storybook/addon-docs/blocks' + + + +# Migrate Formv2 to Formv3 + +## Remove defaultValues + +You must use the `useForm` hook to provide initial values: + +```tsx +// Old +
+ // ... +
+ +// New +const methods = useForm({ defaultValues: { foo: 'bar' }}) + +
+ // ... +
+``` + +## Remove function as child component + +Function as child component must be remove: + +```tsx +// Old +
+ {({ isSubmitting }) => ( + + )} +
+ +// New +const methods = useForm() + +const { isSubmitting } = methods.formState + +
+ +
+``` + +## onRawSubmit renamed to onSubmit + +The `onRawSubmit` is renamed to `onSubmit`. + +The return of the function is now a string if an error occurred. + +```tsx +// Old +
{ + return { [FORM_ERROR]: 'ERROR' } +}}> + // ... +
+ +// New +
{ + return 'ERROR' +}}> + // ... +
+``` diff --git a/apps/storybook/src/migrations/icons/MigrationIconV3.mdx b/apps/storybook/src/migrations/icons/MigrationIconV3.mdx new file mode 100644 index 0000000000..ff2933b405 --- /dev/null +++ b/apps/storybook/src/migrations/icons/MigrationIconV3.mdx @@ -0,0 +1,70 @@ +import { Meta } from '@storybook/addon-docs/blocks' + + + +# Package icons from v2 to v3 + +New big change in icons, we added a new way to import icons. You can now directly import the icon name through `@ultraviolet/icons`. + +```tsx +// Before +import { Icon } from '@ultraviolet/icons'; + +const MyComponent = () => ( + +); +``` + +```tsx +// Now +import { AddressIcon } from '@ultraviolet/icons'; +// OR +import { AddressIcon } from '@ultraviolet/icons/AddressIcon'; + +const MyComponent = () => ( +
+); +``` + +This change will make it easier to use icons in your project and reduce the bundle size. +Exact same pattern is changed for `` and ``: + +```tsx +// Before +import { CategoryIcon } from '@ultraviolet/icons' + +// Now +import { BaremetalCategoryIcon } from '@ultraviolet/icons/category' +// OR +import { BaremetalCategoryIcon } from '@ultraviolet/icons/category/BaremetalCategoryIcon' +``` + +```tsx +// Before +import { ProductIcon } from '@ultraviolet/icons' + +// Now +import { InstanceProductIcon } from '@ultraviolet/icons/product' +// OR +import { InstanceProductIcon } from '@ultraviolet/icons/product/InstanceProductIcon' +``` + +## Prop changes + +- `name` prop is removed from all icons as the import itself is the name of the icon. + +**All other props are the same as before.** + +## Specific needs + +If needed you can import all icons at once: + +```tsx +import * as Icons from '@ultraviolet/icons'; + +const MyComponent = ({ iconName }) => ( +
+ {Icons[iconName]} +
+) +``` diff --git a/apps/storybook/src/migrations/styles.css b/apps/storybook/src/migrations/styles.css new file mode 100644 index 0000000000..2e91fa217c --- /dev/null +++ b/apps/storybook/src/migrations/styles.css @@ -0,0 +1,16 @@ +table { + width: 100%; + border-collapse: collapse; + margin: 16px 0; +} + +th, +td { + padding: 16px; + text-align: left; + border-bottom: 1px solid lightgray; +} + +th { + font-weight: 600; +} diff --git a/apps/storybook/src/migrations/ui/MigrationAvatarV2.mdx b/apps/storybook/src/migrations/ui/MigrationAvatarV2.mdx new file mode 100644 index 0000000000..02f2a26392 --- /dev/null +++ b/apps/storybook/src/migrations/ui/MigrationAvatarV2.mdx @@ -0,0 +1,49 @@ +import { Meta } from '@storybook/addon-docs/blocks' + + + +# Migrate Avatar to AvatarV2 + +#### `image` prop + +Use variant `image` with prop `image`: + +```tsx +// Before + + +// After + +``` + +#### `size` prop + +Instead of a number you can use `small`, `medium`, `large` or `xlarge`. + +#### `textBgColor`, `textColor`, `textSize` prop + +Those props are deprecated as the background is a fixed color. For the `textSize` use prop `size` instead it will define the size of the text. + +#### `lock` prop + +Use variant `icon` and prop `icon="lock"`: + +```tsx +// Before + + +// After + +``` + +#### `text` prop + +Use variant `text` with prop `text`: + +```tsx +// Before + + +// After + +``` diff --git a/apps/storybook/src/migrations/ui/MigrationIconUsages.mdx b/apps/storybook/src/migrations/ui/MigrationIconUsages.mdx new file mode 100644 index 0000000000..7e27d7ddf1 --- /dev/null +++ b/apps/storybook/src/migrations/ui/MigrationIconUsages.mdx @@ -0,0 +1,196 @@ +import { Meta } from '@storybook/addon-docs/blocks' +import '../styles.css' + + + +# Migrate Icon Usages + +This migration has per main goal to optimize the bundle size and improve the performance of the application by removing the import of all icons from the `@ultraviolet/icons` package. +
+Currently we import all the icons contained in the library but depending on your usage you may not need all of them. This issue is particularly important for the web application where the bundle size is a critical point. + +
+ +## Summary of deprecated props among components + +You can find below the list of components that have deprecated props related to icons and the new usage to follow. Click on the component name to see the detailed migration. + +| Component | Deprecated Props | New Usage | +|------------------|--------------------------------------|------------------------------------------------| +| [Button](#button) | `icon`, `iconPosition`, `iconVariant` | Use the imported icon directly in the children | +| [Badge](#badge) | `icon` | Use the imported icon directly in the children | +| [Bullet](#bullet) | `icon`, `iconVariant` | Use the imported icon directly in the children | +| [AvatarV2](#avatarv2) | `icon` | Use the imported icon directly in the children | +| [Separator](#separator) | `icon` | Use the imported icon directly in the children | +| [Tag](#tag) | `icon` | Use the imported icon directly in the children | + +
+ +## Size changes + +The size of the icons has been updated to be more consistent across the library. You can find below the mapping between the old sizes and the new sizes. + +| Previous Size | New Size | +|---------------|----------| +| - | xsmall | +| small | small | +| large | medium | +| - | large | +| xlarge | xlarge | +| xxlarge | xxlarge | + +## PX to size + +We shouldn't use px anymore in the code. The size of the icons has been updated to be more consistent across the library. You can find below the mapping between the old sizes and the new sizes. + +| PX Value | Size Token | +|----------|------------| +| 12px | xsmall | +| 16px | small | +| 20px | medium | +| 24px | large | +| 32px | xlarge | +| 40px | xxlarge | + +
+ +## Detailed migration per components + +### Button + +`icon`, `iconPosition`, `iconVariant` props are deprecated. You can directly use the imported icon you need in the children. + +```tsx +// Before +import { Button } from '@ultraviolet/ui' + + +``` + +```tsx +// After +import { Button } from '@ultraviolet/ui' +import { PencilOutlineIcon } from '@ultraviolet/icons' + + +``` + +### Badge + +`icon` props is deprecated. You can directly use the imported icon you need in the children. + +```tsx +// Before +import { Badge } from '@ultraviolet/ui' + + + Edit + +``` + +```tsx +// After +import { Badge } from '@ultraviolet/ui' +import { PencilOutlineIcon } from '@ultraviolet/icons' + + + Edit + +``` + +### Bullet + +`icon` and `iconVariant` props are deprecated. You can directly use the imported icon you need in the children. + +```tsx +// Before +import { Bullet } from '@ultraviolet/ui' + + + +``` + +```tsx +// After +import { Bullet } from '@ultraviolet/ui' +import { CheckIcon, CheckCircleOutlineIcon } from '@ultraviolet/icons' + + + + + + + + +``` + +### AvatarV2 + +`icon` prop is deprecated. You can directly use the imported icon you need in the children. + +```tsx +// Before +import { AvatarV2 } from '@ultraviolet/ui' + + +``` + +```tsx +// After +import { AvatarV2 } from '@ultraviolet/ui' +import { MosaicIcon } from '@ultraviolet/icons' + + + + +``` + +### Separator + +`icon` prop is deprecated. You can directly use the imported icon you need in the children. + +```tsx +// Before +import { Separator } from '@ultraviolet/ui' + + +``` + +```tsx +// After +import { Separator } from '@ultraviolet/ui' +import { RayTopArrowIcon } from '@ultraviolet/icons' + + + + +``` + + +### Tag + +`icon` prop is deprecated. You can directly use the imported icon you need in the children. + +```tsx +// Before +import { Tag } from '@ultraviolet/ui' + + + Valid + +``` + +```tsx +// After +import { Tag } from '@ultraviolet/ui' +import { CheckIcon } from '@ultraviolet/icons' + + + + Valid + +``` diff --git a/apps/storybook/src/migrations/ui/MigrationIllustrations.mdx b/apps/storybook/src/migrations/ui/MigrationIllustrations.mdx new file mode 100644 index 0000000000..9316a1409f --- /dev/null +++ b/apps/storybook/src/migrations/ui/MigrationIllustrations.mdx @@ -0,0 +1,28 @@ +import { Meta } from '@storybook/addon-docs/blocks' + + + +# Illustrations V2 + +## How to use @ultraviolet/illustrations + +Assets are now imported directly from an S3 bucket in order to reduce the size of the package. + +**Path to the components changed**. Although `@ultraviolet/illustrations/components/DynamicIllustration` is still supported, **a simpler way to import it was added**: to use `` simply write +```javascript +import { DynamicIllustration } from '@ultraviolet/illustrations' +``` + +The path to import illustrations did not change. +For example: + +```javascript +import { notFound } from '@ultraviolet/illustrations/various/feedback' +``` + +A typo was corrected on quantum, make sure to import `quantumOriginal` and not `quantumOrignal` + +## How to add illustrations to the package +Simply add the illustrations you want to add to the correct folder and simply create a PR. Once the branch is approved and merged, they will be automatically added to the bucket and exported. + +To add the new assets to ``, follow the same steps but before creating the PR, do not forget to add the light and dark version of the new illustrations to src/components/DynamicIllustration/Illustrations.tsx. \ No newline at end of file diff --git a/apps/storybook/src/migrations/ui/MigrationMenuV2.mdx b/apps/storybook/src/migrations/ui/MigrationMenuV2.mdx new file mode 100644 index 0000000000..868ae6bab7 --- /dev/null +++ b/apps/storybook/src/migrations/ui/MigrationMenuV2.mdx @@ -0,0 +1,22 @@ +import { Meta } from '@storybook/addon-docs/blocks' + + + +# Migrate Menu to MenuV2 + +## `placement` prop + +The `placement` prop has been updated and now uses the values from the internal `Popup` component. The following values are now valid: + +* `left` +* `right` +* `top` +* `right` + +## `disclosure` prop + +The `disclosure` prop object argument only has a `visible` property. + +## `children` prop + +The `children` prop no longer have a popover argument. diff --git a/apps/storybook/src/migrations/ui/MigrationNumberInputV2.mdx b/apps/storybook/src/migrations/ui/MigrationNumberInputV2.mdx new file mode 100644 index 0000000000..0ba878088b --- /dev/null +++ b/apps/storybook/src/migrations/ui/MigrationNumberInputV2.mdx @@ -0,0 +1,33 @@ +import { Meta } from '@storybook/addon-docs/blocks' + + + +# Migrate NumberInput to NumberInputV2 + +## Properties to migrate + +* `minValue` (string) -> `min` ( number) +* `maxValue` (string) -> `max` ( number) +* `step` (string) -> `step` (string | number) +* `value` (string) -> `value` (number) +* `text` (string) -> `unit` (string) +* `disabledTooltip` (string) -> `tooltip` (string) + +## Removed properties + +* `onMaxCrossed` (function) +* `onMinCrossed` (function) +* `defaultValue` (string) + +## Behavior change + +* The component act as uncontrolled component as long as the `value` prop is not provided. + If you set `value`, you will have to handle the `onChange` event to update the value. + +* If you set a `min` or `max` value and that the component is controlled (you set the `value` prop). + +* โš  WARNING: If the `min` or `max` prop is set in addition with `step` prop + you need to be coherent in the number you are setting up. If the `step` is a multiple + of 5 then you need to set your `min` or `max` to be also a multiple of 5. + +* If the input is empty the `onChange` will receive `null` as value diff --git a/apps/storybook/src/migrations/ui/MigrationSelectInputV2.mdx b/apps/storybook/src/migrations/ui/MigrationSelectInputV2.mdx new file mode 100644 index 0000000000..8591c8f710 --- /dev/null +++ b/apps/storybook/src/migrations/ui/MigrationSelectInputV2.mdx @@ -0,0 +1,43 @@ +import { Meta } from '@storybook/addon-docs/blocks' + + + +# Migrate SelectInput to SelectInputV2 + +Welcome to the migration of SelectInput into SelectInputV2, here we will see how to easily migrate to SelectInputV2 component. + +## Properties to migrate + +* `children` (ReactNode) => `options`{'(Record | OptionType[])'} +* `isDisabled`(boolean) => `disabled` (boolean) +* `emptyState`{'((obj: {inputValue: string;}) => ReactNode) '} => `emptyState` (ReactNode) +* `isSearchable` (boolean): `searchable` (boolean) +* `isClearable` (boolean): `clearable` (boolean) +* `inlineDescription` (string): `description`, defined directly in the options props +* `value`(string | selectOption): `value` (string[] | string) + +## Removed Properties + +* `additionalStyles` (CSSInterpolation) +* `checked` (boolean) +* `labelId` (string) +* `time` (boolean) +* `customStyle`: You can directly customize the options style by passing a ReactNode as a label for the options +* `animation` (string) +* `animationDuration` (number) +* `noTopLabel` (boolean): Simply do not pass a label +* `animationOnChange` (boolean) +* `customComponents` +* `loading` + +## `options` prop + +Options are now only passed as a prop, not children. They can be of two types, one for grouped options and one for ungrouped options. Each option is of type `OptionType` with the following properties: + +* `value` (string): value of the option. *This prop is required*. +* `label` (ReactNode): label to display in the dropdown and on the selectBar when the option is selected. *This prop is required*. +* `disabled` (boolean): whether the option is disabled. *This prop is optional*. +* `description` (ReactNode): more information on the option. The description can be placed at the right or beneath the option label in the dropdown with the prop `direction`, which is **global**. When description is a string, it is also used when searching an option. *This prop is optional*. +* `optionalInfo` (ReactNode): if there is badges or icons to add next to the label (left or right) which are not meant to be visible when the option is selected. Its position is **globally** set using the prop `optionalInfoPlacement`. *This prop is optional*. +* `searchText` (string): define here what should the user search to get this option. This prop is optional when the label is a string, required otherwise. +* `tooltip` (string): If defined, a tooltip with the string defined will be shown when hovering the option in the dropdown. *This prop is optional*. diff --git a/apps/storybook/src/migrations/ui/MigrationTextInputV2.mdx b/apps/storybook/src/migrations/ui/MigrationTextInputV2.mdx new file mode 100644 index 0000000000..08ed8cf6c5 --- /dev/null +++ b/apps/storybook/src/migrations/ui/MigrationTextInputV2.mdx @@ -0,0 +1,30 @@ +import { Meta } from '@storybook/addon-docs/blocks' + + + +# Migrate TextInput to TextInputV2 + +## Properties to migrate + +* `type` (string) => `type` (text | password | url | email) +* `random` (string) => `onRandomize` (`() => void`): the function will be triggered when the random button is clicked +* `notice` (string) => `helper` (string) +* `height` (string | number) => `size` (small | medium | large) +* `defaultValue` (string | number) => `value` +* `unit` (string) => `suffix` (string) +* `valid` (boolean) => `success` (string | boolean): note that only `success` prop allows both `string` and `boolean` values + +## Removed properties + +* `min` (string | number): if the need is to set a minLength, use the `minLength` (number) prop +* `max` (string | number): if the need is to set a maxLength, use the `maxLength` (number) prop +* `cols` (number): not relevant for this component, if needed use `TextArea` component +* `edit` (boolean) +* `fillAvailable` (boolean) +* `generated` (boolean) +* `multiline` (boolean): not relevant for this component, use `TextArea` component instead +* `noTopLabel` (boolean): just don't set the `label` prop if you don't want a label, you will still need to set `aria-label` or `aria-labelledby` to make the input accessible +* `resizable` (boolean): not relevant for this component, use `TextArea` component instead +* `rows` (number): not relevant for this component, if needed use `TextArea` component +* `wrap` (string) +* `inputProps` (object) diff --git a/apps/storybook/src/packages/form/Introduction.mdx b/apps/storybook/src/packages/form/Introduction.mdx new file mode 100644 index 0000000000..6cb87a37fc --- /dev/null +++ b/apps/storybook/src/packages/form/Introduction.mdx @@ -0,0 +1,8 @@ +import { Markdown, Meta } from '@storybook/addon-docs/blocks' +import Readme from '../../../../../packages/form/README.md?raw' + + + + + {Readme} + diff --git a/apps/storybook/src/packages/icons/Introduction.mdx b/apps/storybook/src/packages/icons/Introduction.mdx new file mode 100644 index 0000000000..e91762c0a7 --- /dev/null +++ b/apps/storybook/src/packages/icons/Introduction.mdx @@ -0,0 +1,8 @@ +import { Markdown, Meta } from '@storybook/addon-docs/blocks' +import Readme from '../../../../../packages/icons/README.md?raw' + + + + + {Readme} + diff --git a/apps/storybook/src/packages/illustrations/Introduction.mdx b/apps/storybook/src/packages/illustrations/Introduction.mdx new file mode 100644 index 0000000000..f8c0ec5337 --- /dev/null +++ b/apps/storybook/src/packages/illustrations/Introduction.mdx @@ -0,0 +1,6 @@ +import { Markdown, Meta } from '@storybook/addon-docs/blocks' +import Readme from '../../../../../packages/illustrations/README.md?raw' + + + +{Readme} diff --git a/apps/storybook/src/packages/illustrations/List.tsx b/apps/storybook/src/packages/illustrations/List.tsx new file mode 100644 index 0000000000..3995182f2e --- /dev/null +++ b/apps/storybook/src/packages/illustrations/List.tsx @@ -0,0 +1,237 @@ +// oxlint-disable import/no-namespace + +import { + ArrowDownIcon, + ArrowUpIcon, + MinusIcon, + PlusIcon, +} from '@ultraviolet/icons' + +import * as productsIllustrations from '@ultraviolet/illustrations/products' +import * as variousIllustrations from '@ultraviolet/illustrations/various' +import { Button, Expandable, Snippet, Stack, Text } from '@ultraviolet/ui' +import { useReducer } from 'react' +// import * as assets from '../index' +import { + buttonStory, + cardStory, + imageProductStory, + imageVariousStory, + margedStackStory, + snippetStory, + stackStory, +} from './style.css' + +const defaultAssets = { + products: productsIllustrations, + various: variousIllustrations, +} + +type AssetsModule = Record>> +/* +const StyledSnippet = styled(Snippet)` + padding: ${({ theme }) => theme.space['2']}; +` + +const StyledStack = styled(Stack)` + min-width: 0; + padding-right: ${({ theme }) => theme.space['2']}; +` + +const StyledButton = styled(Button)` + width: fit-content; + height: fit-content; + background: none; + border: none; + padding: ${({ theme }) => theme.space['0.5']} ${({ theme }) => theme.space['1']}; + text-align: left; +` + +const StyledImageProduct = styled.img` + border-radius: ${({ theme }) => theme.radii.large} 0 0 + ${({ theme }) => theme.radii.large}; + background: ${({ theme }) => + theme.theme === 'light' ? theme.colors.neutral.backgroundStronger : null}; +` + +const StyledImageVarious = styled.img` + border-radius: ${({ theme }) => theme.radii.large} 0 0 + ${({ theme }) => theme.radii.large}; + background: ${({ theme }) => + theme.theme === 'light' ? null : theme.colors.neutral.backgroundStronger}; +` + +const MargedStack = styled(Stack)` + margin-left: ${({ theme }) => theme.space['4']}; +` + +const Card = styled(Stack)` + border: 1px solid ${({ theme }) => theme.colors.neutral.borderWeak}; + border-radius: ${({ theme }) => theme.radii.large}; +` + */ +type SubListElementProps = { + productName: string + isExpanded: boolean + setIsExpanded: () => void + category: string +} + +const SubListElement = ({ + productName, + isExpanded, + setIsExpanded, + category, +}: SubListElementProps) => ( + + + + + {Object.keys( + (defaultAssets as AssetsModule)[category][productName], + ).map(productImg => { + const imgSrc = (defaultAssets as AssetsModule)[category][productName][ + productImg + ] + + return ( + + {category === 'products' ? ( + {productName} + ) : ( + {productName} + )} + + + {productImg}.{imgSrc.split('.').pop()} + + + {`import { ${productImg} } from '@ultraviolet/illustrations/${category}/${productName}'`} + + + + ) + })} + + + +) + +export const List = () => { + const [expandedStates, setExpandedStates] = useReducer( + (oldState: Record, newState: Record) => ({ + ...oldState, + ...newState, + }), + {}, + ) + const [isAllExpanded, setIsAllExpanded] = useReducer(state => !state, false) + + const toggleAllExpanded = () => { + const newExpandedCategoryStates = Object.keys(defaultAssets).reduce( + (acc, category) => ({ + ...acc, + [category]: !isAllExpanded, + }), + {}, + ) + setExpandedStates(newExpandedCategoryStates) + + const newExpandedProductStates = Object.keys(defaultAssets).reduce( + (acc, category) => ({ + ...acc, + ...Object.keys((defaultAssets as AssetsModule)[category]).reduce( + (localAcc, productName) => ({ + ...localAcc, + [productName]: !isAllExpanded, + }), + {}, + ), + }), + {}, + ) + setExpandedStates(newExpandedProductStates) + setIsAllExpanded() + } + + return ( + + + + {Object.keys(defaultAssets).map(category => ( +
+ + + + {Object.keys((defaultAssets as AssetsModule)[category]).map( + productName => ( + { + // Toggle the expanded state for a specific element + const newExpandedStates = { + ...expandedStates, + [productName]: !expandedStates[productName], + } + + setExpandedStates(newExpandedStates) + }} + /> + ), + )} + + +
+ ))} +
+
+ ) +} diff --git a/apps/storybook/src/packages/illustrations/index.mdx b/apps/storybook/src/packages/illustrations/index.mdx new file mode 100644 index 0000000000..252a313bc9 --- /dev/null +++ b/apps/storybook/src/packages/illustrations/index.mdx @@ -0,0 +1,8 @@ +import { Meta } from '@storybook/addon-docs/blocks' +import { List } from './List.tsx' + + + +# Illustrations + + diff --git a/apps/storybook/src/packages/illustrations/style.css.ts b/apps/storybook/src/packages/illustrations/style.css.ts new file mode 100644 index 0000000000..cbc421f469 --- /dev/null +++ b/apps/storybook/src/packages/illustrations/style.css.ts @@ -0,0 +1,39 @@ +import { theme } from '@ultraviolet/themes' +import { style } from '@vanilla-extract/css' + +export const snippetStory = style({ + padding: theme.space[2], +}) + +export const stackStory = style({ + minWidth: 0, + paddingRight: theme.space[2], +}) + +export const buttonStory = style({ + width: 'fit-content', + height: 'fit-content', + background: 'none', + border: 'none', + padding: `${theme.space['0.5']} ${theme.space[1]}`, + textAlign: 'left', +}) + +export const imageProductStory = style({ + borderRadius: `${theme.radii.large} 0 0 ${theme.radii.large}`, + background: theme.colors.neutral.backgroundStronger, +}) + +export const imageVariousStory = style({ + borderRadius: `${theme.radii.large} 0 0 ${theme.radii.large}`, + background: theme.colors.neutral.backgroundStronger, +}) + +export const margedStackStory = style({ + marginLeft: theme.space[4], +}) + +export const cardStory = style({ + border: `1px solid ${theme.colors.neutral.borderWeak}`, + borderRadius: theme.radii.large, +}) diff --git a/apps/storybook/src/packages/plus/Introduction.mdx b/apps/storybook/src/packages/plus/Introduction.mdx new file mode 100644 index 0000000000..d039170c95 --- /dev/null +++ b/apps/storybook/src/packages/plus/Introduction.mdx @@ -0,0 +1,8 @@ +import { Markdown, Meta } from '@storybook/addon-docs/blocks' +import Readme from '../../../../../packages/plus/README.md?raw' + + + + + {Readme} + diff --git a/apps/storybook/src/packages/ui/Introduction.mdx b/apps/storybook/src/packages/ui/Introduction.mdx new file mode 100644 index 0000000000..043d0c6600 --- /dev/null +++ b/apps/storybook/src/packages/ui/Introduction.mdx @@ -0,0 +1,8 @@ +import { Markdown, Meta } from '@storybook/addon-docs/blocks' +import Readme from '../../../../../packages/ui/README.md?raw' + + + + + {Readme} + diff --git a/apps/storybook/src/styles.css.ts b/apps/storybook/src/styles.css.ts new file mode 100644 index 0000000000..738eaec97c --- /dev/null +++ b/apps/storybook/src/styles.css.ts @@ -0,0 +1,26 @@ +import { theme } from '@ultraviolet/themes' +import { createVar, style, styleVariants } from '@vanilla-extract/css' + +export const computedBackground = createVar() +export const separator = style({ + margin: `${theme.space[3]} 0`, +}) + +export const capitalizedText = style({ + textTransform: 'capitalize', +}) + +export const noMarginText = style({ margin: 0 }) + +export const card = style({ + alignItems: 'center', + display: 'flex', + justifyContent: 'space-between', + width: '100%', + padding: 8, + background: computedBackground, +}) +export const paddingCard = styleVariants({ + default: { padding: 8 }, + large: { padding: 32 }, +}) diff --git a/apps/storybook/src/theme/assets/sizing-table.png b/apps/storybook/src/theme/assets/sizing-table.png new file mode 100644 index 0000000000..e118c867aa Binary files /dev/null and b/apps/storybook/src/theme/assets/sizing-table.png differ diff --git a/apps/storybook/src/theme/assets/space-table.png b/apps/storybook/src/theme/assets/space-table.png new file mode 100644 index 0000000000..7f59749af6 Binary files /dev/null and b/apps/storybook/src/theme/assets/space-table.png differ diff --git a/apps/storybook/src/theme/colors.mdx b/apps/storybook/src/theme/colors.mdx new file mode 100644 index 0000000000..bf48e9a472 --- /dev/null +++ b/apps/storybook/src/theme/colors.mdx @@ -0,0 +1,67 @@ +import { Meta } from '@storybook/addon-docs/blocks' +import Colors from '../components/Colors' + + + +# Customize colors + +You can customize colors by extending the theme.
+This means default colors will apply if you didn't override them all. + +To customize theme you will need to use `extendTheme` utility function. It will generate a well formatted theme to pass +to your Theme Provider. + +You will need to pass an object with the same structure as the [default theme](https://github.com/scaleway/ultraviolet/blob/main/src/theme/tokens/light.ts), but with your own colors. + +```tsx +import { ThemeProvider } from '@ultraviolet/themes' +import { Badge, extendTheme } from '@ultraviolet/ui' + +function App() { + const myTheme = extendTheme({ + colors: { + danger: { + background: '#ffe1e7', + backgroundDisabled: '#fff5f7', + backgroundHover: '#ffe1e7', + backgroundStrong: '#dd3252', + backgroundStrongDisabled: '#fbbac6', + backgroundStrongHover: '#a6102d', + border: '#a6102d', + borderDisabled: '#fbbac6', + borderHover: '#a6102d', + borderStrong: '#ffffff', + borderStrongDisabled: '#fbbac6', + borderStrongHover: '#a6102d', + text: '#a6102d', + textDisabled: '#fbbac6', + textHover: '#a6102d', + textStrong: '#ffffff', + textStrongDisabled: '#fff5f7', + textStrongHover: '#ffffff', + }, + ..., + } + }) + + return ( + + Example + + ) +} + +export default App +``` + +### API + +#### `extendTheme({extendedTheme}: {extendedTheme: Theme})` + +This function take in parameters an object with de same structure of the default theme. It will return a deep merged object with the default theme. + +## Default Values + +Here are all default colors we use in Ultraviolet UI. You can switch to dark theme on storybook to see dark mode colors. + + diff --git a/apps/storybook/src/theme/darkMode.mdx b/apps/storybook/src/theme/darkMode.mdx new file mode 100644 index 0000000000..e47fdf5e5b --- /dev/null +++ b/apps/storybook/src/theme/darkMode.mdx @@ -0,0 +1,34 @@ +import { Meta } from '@storybook/addon-docs/blocks' + + + +# Dark mode + +## How to? + +Ultraviolet UI is designed to be used in both light and dark mode. The kit also provide both light and dark theme.
+You can enable easily switch between light and dark mode by passing `theme` or `darkTheme` to the `ThemeProvider` component. + +Example: + +```tsx +import { ThemeProvider } from '@ultraviolet/themes' +import { theme, darkTheme, Button, Text } from '@ultraviolet/ui' +import { useState } from 'react' + +function App() { + const [isDarkTheme, setIsDarkTheme] = useState(false) + + return ( + + + Current theme: {theme} + + + + + ) +} + +export default App +``` diff --git a/apps/storybook/src/theme/shadows.mdx b/apps/storybook/src/theme/shadows.mdx new file mode 100644 index 0000000000..f3ef0d5a6f --- /dev/null +++ b/apps/storybook/src/theme/shadows.mdx @@ -0,0 +1,35 @@ +import { Meta } from '@storybook/addon-docs/blocks' + + + +# Shadows + +The default theme has a set of shadows that can be used to create a sense of depth and hierarchy. + +## Customize shadows + +You may also want to customize your shadows. To do that you need to create your own theme and pass it to the `ThemeProvider` component.
+You will need to pass an object with the same structure as the [default theme](https://github.com/scaleway/ultraviolet/blob/main/src/theme/tokens/light.ts), but with your own typography. + +```tsx +import { ThemeProvider } from '@ultraviolet/themes' +import { Tooltip, extendTheme } from '@ultraviolet/ui' + +function App() { + const myTheme = extendTheme({ + shadows: { + tooltip: '10px 0px 20px 16px #000000', + }, + }) + + return ( + + +

Example

+
+
+ ) +} + +export default App +``` diff --git a/apps/storybook/src/theme/spaces.mdx b/apps/storybook/src/theme/spaces.mdx new file mode 100644 index 0000000000..fee5717426 --- /dev/null +++ b/apps/storybook/src/theme/spaces.mdx @@ -0,0 +1,113 @@ +import { Meta } from '@storybook/addon-docs/blocks' +import SpaceTable from './assets/space-table.png' +import SizingTable from './assets/sizing-table.png' +import { consoleLightTheme } from '@ultraviolet/themes' +import { Card, Alert } from '@ultraviolet/ui' + + + +# Dimensions + +## Concept + +In the theme tokens you will find 2 major dimension categories for spacing `space` and `sizing`. +
+### Space + +Our spacing is structured on the 8px base unit. All the keys are based on this unit and are multiples of it. This allows for a consistent spacing scale that can be used across the application. +
+Space is used for spacing paddings, margins and gaps. It is a scale of values that can be used to create consistent spacing between elements. +
+ +
+You can access to space in Ultraviolet using the theme: + +```tsx +const MyComponent = styled.div` + margin: ${({ theme }) => theme.space['1']}; // 0.5rem or 8px +` +``` + + +| **Do** | **Don't** | +|--------|-----------| +| โœ… **Do**: Use `space` for setting spacing (padding, margin, gap) to maintain consistency. | ๐Ÿšซ **Don't**: Use `space` on height, max-height, width, max-width. Use `sizing` for that. | +| | ๐Ÿšซ **Don't**: Use `space` on border radius, `theme.radii` exists for that | +| | ๐Ÿšซ **Don't**: Use `space` on font size and line height, `theme.typography` exists for that or use `` component | + + + +### Sizing + +Sizing is a of 8px just like space. In order to differenciate space and sizing and avoid usage confusion we create different keys for sizing. Those keys are named by the multiplier * 1000. Multiplier being the number of 8px units. +
+Sizing is used mostly for height and width of elements. It is a scale of values that can be used to create consistent sizing of elements. +
+ +
+ +Tokens that have yellow background in the above picture are deprecated and should be avoided as much as possible. We will remove them in a future major. +
+You can access to sizing in Ultraviolet using the theme: + +```tsx +const MyComponent = styled.div` + margin: ${({ theme }) => theme.sizing['200']}; // 1rem or 16px +` +``` + + +| **Do** | **Don't** | +|--------|-----------| +| โœ… **Do**: Use `sizing` for setting width and height (min and max included) to maintain consistency. | ๐Ÿšซ **Don't**: Use `sizing` on padding, margin, gap, and any other spacing element. Use `space` for that. | +| | ๐Ÿšซ **Don't**: Use `sizing` on border and box shadow, we want to keep those in px and not in rem | +| | ๐Ÿšซ **Don't**: Use `sizing` on border radius, `theme.radii` exists for that | +| | ๐Ÿšซ **Don't**: Use `sizing` on font size and line height, `theme.typography` exists for that or use `` component | + +
+ +### Customize + +You can customize the space and sizing by passing a custom theme to the `ThemeProvider`: + +```tsx +import { ThemeProvider } from '@ultraviolet/themes' +import { Button, extendTheme } from '@ultraviolet/ui' + +function App() { + const myTheme = extendTheme({ + space: { + 0: '0rem', + 1: '0.5rem', + 2: '1rem', + 3: '1.5rem', + 4: '2rem', + 5: '2.5rem', + 6: '3rem', + 7: '3.5rem', + 8: '4rem', + ... + }, + sizing: { + 0: '0rem', + 100: '0.5rem', + 125: '0.625rem', + 150: '0.75rem', + 175: '0.875rem', + 200: '1rem', + 250: '1.25rem', + ... + } + }) + + return ( + + + + ) +} + +export default App +``` diff --git a/apps/storybook/src/theme/typography.mdx b/apps/storybook/src/theme/typography.mdx new file mode 100644 index 0000000000..469c98891c --- /dev/null +++ b/apps/storybook/src/theme/typography.mdx @@ -0,0 +1,58 @@ +import { Meta } from '@storybook/addon-docs/blocks' + + + +# Typography + +In general way you won't have to use typography from theme as you may use Text component for any text.
+But if you need to use it, you can use it like this: `theme.typography`. + +## Customize typography + +You may also want to customize your typography fonts or sizes. To do that you need to create your own theme and pass it to the `ThemeProvider` component.
+You will need to pass an object with the same structure as the [default theme](https://github.com/scaleway/ultraviolet/blob/main/src/theme/tokens/light.ts), but with your own typography. + +```tsx +import { css, Global, ThemeProvider } from '@ultraviolet/themes' +import { Text, extendTheme } from '@ultraviolet/ui' +import AsapRegularWoff2 from 'assets/fonts/asap/Asap-Regular.woff2' + +const fonts = css` + @font-face { + font-family: 'Asap'; + font-style: normal; + src: url(${AsapRegularWoff2}) format('woff2'); + font-weight: 400; + font-display: swap; + } +` + +function App() { + const myTheme = extendTheme({ + typography: { + myTypography: { + fontFamily: 'Asap', + fontSize: '16px', + fontWeight: 'Regular', + letterSpacing: '0', + lineHeight: '24px', + paragraphSpacing: 'none', + textCase: 'none', + textDecoration: 'none', + weight: '400', + }, + }, + }) + + return ( + + + + Example + + + ) +} + +export default App +``` diff --git a/apps/storybook/src/theme/understandTokens.mdx b/apps/storybook/src/theme/understandTokens.mdx new file mode 100644 index 0000000000..8270224c9b --- /dev/null +++ b/apps/storybook/src/theme/understandTokens.mdx @@ -0,0 +1,83 @@ +import { Meta } from '@storybook/addon-docs/blocks' +import { Badge } from '@ultraviolet/ui' +import ThemeWrapper from '../components/ThemeWrapper' + + + +# Colors + +Ultraviolet UI is based on `vanilla-extract` library. + +## Structure + +Ultraviolet UI provides you a set of colors defined trough theme allowing you to customize the theme as you need. + +Tokens are designed as follows: `sentiment.usageProminenceInteraction` + +* **sentiment** can be `primary`, `secondary`, `neutral`, `info`, `danger`, `warning` and `success` +* **usage** can be `text`, `background` and `border`. +* **prominence** can be `weak` and `strong`. +* **interaction** can be `disabled` and `hover`. + +To goal of this pattern is to provide a consistent and verbose way to use colors in your application. + +When choosing what color to use from theme **you will first think about the sentiment** of the element you want to style: is it a danger? a warning? just an information?
+**Then you will think about the usage** of the color: is it applied on a text? a background? a border?
+**Then you will think about the prominence** of the color: is it a weak color? a strong color?
+**Finally you will think about the interaction**: is it a disabled element? is it a hovered element?
+ +> **Note:** Prominence will determine the importance you want to give to your color and so to what you display on screen. +> A strong prominence will show a full and consistent color, it is made to put forward information. +> Weak prominence on the other hand will show a blank color, it is made to be less intrusive +> and show less important information. + +Once all those questions are asked, and you have answers, you can easily call the corresponding color. See the example below. + +

ย 

+ +## Simple example + +Let's start with an example: we want to create a Badge component. Let's say I want to create an informative Badge with an icon and a text.
+The badge shouldn't be too much prominent, but it shouldn't be invisible too. It will have a medium prominence or in our case a `default` prominence. + +Let's define background color by asking previous questions: + +1. What is the sentiment? It's an information as said before, so we will use `info` sentiment. +2. What is the usage? It's a background, so we will use `background` usage. +3. What is the prominence? It's a default prominence, so we will use `default` prominence. **Important: when default prominence / interaction is used, you can omit it**. +4. What is the interaction? It's a default interaction, so we will use `default` interaction. **Important: when default prominence / interaction is used, you can omit it**. + +This way we will get the following background color: `theme.colors.info.background`.
+We can now redo the same for Text and Icon, and we will get: `theme.colors.info.text`. + +> **Note:** we only use `theme.colors.info.text` for both Icon and Text as we consider Icons the same usage as Text. + +All those colors combined will result in: + + + This is a simple badge with info sentiment + + +

ย 

+ +## Advanced example + +Easy right? Let's now make it a bit more advanced adding prominence in our context, let's ask same questions as before the find best matching colors: + +1. What is the sentiment? It's an information, so we will use `info` sentiment. +2. What is the usage? It's a background, so we will use `background` usage. +3. What is the prominence? It's a strong prominence, so we will use `strong` prominence. +4. What is the interaction? It's a default interaction, so we will use `default` interaction. **Important: when default prominence / interaction is used, you can omit it**. + +This way we will get the following background color: `theme.colors.info.backgroundStrong`.
+Text and Icon color will be: `theme.colors.info.textStrong`. + +All those colors combined will result in: + + + + This is a simple badge with info sentiment and prominence strong + + + +It results in a different sets of colors and style to express a different context on where the badge is displayed. diff --git a/apps/storybook/tsconfig.json b/apps/storybook/tsconfig.json new file mode 100644 index 0000000000..3886d9c423 --- /dev/null +++ b/apps/storybook/tsconfig.json @@ -0,0 +1,5 @@ +{ + "extends": "../../tsconfig.json", + "include": ["src/**/*.ts", "src/**/*.tsx", "src/**/*.mdx"], + "exclude": ["node_modules"] +} diff --git a/eslint.config.mjs b/eslint.config.mjs index f59538bea2..8a4092ff8c 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -44,6 +44,7 @@ export default [ '**/.vitest/', '**/coverage/', '.storybook', + 'apps/storybook/.storybook', 'eslint.config.mjs', 'next-env.d.ts' ], @@ -145,6 +146,7 @@ export default [ 'utils/test/**/*.{ts,tsx}', '**/vitest.setup.ts', '.storybook/**', + 'apps/storybook/.storybook/**', ], rules: { diff --git a/package.json b/package.json index 31fd33cd0d..828bbbc36b 100644 --- a/package.json +++ b/package.json @@ -16,15 +16,15 @@ "typecheck": "turbo run typecheck", "build": "turbo run build --filter='./packages/*' ", "build:examples": "turbo run build --filter './examples/*' ", - "build:storybook": "STORYBOOK_ENVIRONMENT=production storybook build", - "build:storybook:stats": "pnpm turbo build:storybook -- --webpack-stats-json", + "build:story": "pnpm turbo build:storybook", + "build:story:stats": "pnpm run build:story -- --webpack-stats-json", "test:unit": "turbo run test:unit", "test:unit:coverage": "turbo run test:unit:coverage", "test:e2e": "turbo e2e --filter @repo/e2e", "check:deps": "npx depcheck . --skip-missing=true --ignores='bin,eslint,vite,jest,husky,@commitlint/*,@babel/*,babel-*'", "commit": "npx git-cz -a --disable-emoji", - "start": "STORYBOOK_ENVIRONMENT=development storybook dev -p 6006", - "start:production": "STORYBOOK_ENVIRONMENT=production storybook dev -p 6006", + "start:story": "pnpm turbo start:storybook", + "start:story:production": "pnpm turbo start:storybook:production", "format": "biome check --linter-enabled=false --write .", "format:check": "biome check --linter-enabled=false --verbose .", "format:ci": "biome ci --linter-enabled=false .", @@ -115,13 +115,6 @@ "@scaleway/tsconfig": "1.1.3", "@size-limit/file": "12.0.0", "@size-limit/preset-big-lib": "12.0.0", - "@storybook/addon-a11y": "10.1.10", - "@storybook/addon-docs": "10.1.10", - "@storybook/addon-links": "10.1.10", - "@storybook/addon-themes": "10.1.10", - "@storybook/builder-vite": "10.1.10", - "@storybook/mdx2-csf": "1.1.0", - "@storybook/react-vite": "10.1.10", "@svgr/rollup": "8.1.0", "@testing-library/dom": "10.4.1", "@testing-library/jest-dom": "6.9.1", @@ -174,8 +167,6 @@ "rollup-plugin-postcss": "4.0.2", "rollup-preserve-directives": "1.1.3", "size-limit": "12.0.0", - "storybook": "10.1.10", - "storybook-addon-tag-badges": "3.0.4", "svgo": "4.0.0", "timekeeper": "2.3.1", "tsx": "4.21.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 6b5486addb..5072212407 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -74,27 +74,6 @@ importers: '@size-limit/preset-big-lib': specifier: 12.0.0 version: 12.0.0(esbuild@0.27.0)(size-limit@12.0.0(jiti@2.4.2)) - '@storybook/addon-a11y': - specifier: 10.1.10 - version: 10.1.10(storybook@10.1.10(@testing-library/dom@10.4.1)(prettier@2.8.8)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)) - '@storybook/addon-docs': - specifier: 10.1.10 - version: 10.1.10(@types/react@19.2.7)(esbuild@0.27.0)(rollup@4.53.3)(storybook@10.1.10(@testing-library/dom@10.4.1)(prettier@2.8.8)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(vite@7.3.0(@types/node@24.10.4)(jiti@2.4.2)(lightningcss@1.30.2)(sass@1.97.1)(terser@5.39.0)(tsx@4.21.0)(yaml@2.8.1))(webpack@5.104.1(esbuild@0.27.0)) - '@storybook/addon-links': - specifier: 10.1.10 - version: 10.1.10(react@19.2.3)(storybook@10.1.10(@testing-library/dom@10.4.1)(prettier@2.8.8)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)) - '@storybook/addon-themes': - specifier: 10.1.10 - version: 10.1.10(storybook@10.1.10(@testing-library/dom@10.4.1)(prettier@2.8.8)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)) - '@storybook/builder-vite': - specifier: 10.1.10 - version: 10.1.10(esbuild@0.27.0)(rollup@4.53.3)(storybook@10.1.10(@testing-library/dom@10.4.1)(prettier@2.8.8)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(vite@7.3.0(@types/node@24.10.4)(jiti@2.4.2)(lightningcss@1.30.2)(sass@1.97.1)(terser@5.39.0)(tsx@4.21.0)(yaml@2.8.1))(webpack@5.104.1(esbuild@0.27.0)) - '@storybook/mdx2-csf': - specifier: 1.1.0 - version: 1.1.0 - '@storybook/react-vite': - specifier: 10.1.10 - version: 10.1.10(esbuild@0.27.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(rollup@4.53.3)(storybook@10.1.10(@testing-library/dom@10.4.1)(prettier@2.8.8)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(typescript@5.9.3)(vite@7.3.0(@types/node@24.10.4)(jiti@2.4.2)(lightningcss@1.30.2)(sass@1.97.1)(terser@5.39.0)(tsx@4.21.0)(yaml@2.8.1))(webpack@5.104.1(esbuild@0.27.0)) '@svgr/rollup': specifier: 8.1.0 version: 8.1.0(rollup@4.53.3)(typescript@5.9.3) @@ -251,12 +230,6 @@ importers: size-limit: specifier: 12.0.0 version: 12.0.0(jiti@2.4.2) - storybook: - specifier: 10.1.10 - version: 10.1.10(@testing-library/dom@10.4.1)(prettier@2.8.8)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) - storybook-addon-tag-badges: - specifier: 3.0.4 - version: 3.0.4(storybook@10.1.10(@testing-library/dom@10.4.1)(prettier@2.8.8)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)) svgo: specifier: 4.0.0 version: 4.0.0 @@ -288,6 +261,72 @@ importers: specifier: 4.4.2 version: 4.4.2 + apps/storybook: + dependencies: + '@storybook/addon-a11y': + specifier: 10.1.10 + version: 10.1.10(storybook@10.1.10(@testing-library/dom@10.4.1)(prettier@2.8.8)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)) + '@storybook/addon-docs': + specifier: 10.1.10 + version: 10.1.10(@types/react@19.2.7)(esbuild@0.27.0)(rollup@4.53.3)(storybook@10.1.10(@testing-library/dom@10.4.1)(prettier@2.8.8)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(vite@7.3.0(@types/node@24.10.4)(jiti@2.4.2)(lightningcss@1.30.2)(sass@1.97.1)(terser@5.39.0)(tsx@4.21.0)(yaml@2.8.1))(webpack@5.104.1(esbuild@0.27.0)) + '@storybook/addon-links': + specifier: 10.1.10 + version: 10.1.10(react@19.2.3)(storybook@10.1.10(@testing-library/dom@10.4.1)(prettier@2.8.8)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)) + '@storybook/addon-themes': + specifier: 10.1.10 + version: 10.1.10(storybook@10.1.10(@testing-library/dom@10.4.1)(prettier@2.8.8)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)) + '@storybook/builder-vite': + specifier: 10.1.10 + version: 10.1.10(esbuild@0.27.0)(rollup@4.53.3)(storybook@10.1.10(@testing-library/dom@10.4.1)(prettier@2.8.8)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(vite@7.3.0(@types/node@24.10.4)(jiti@2.4.2)(lightningcss@1.30.2)(sass@1.97.1)(terser@5.39.0)(tsx@4.21.0)(yaml@2.8.1))(webpack@5.104.1(esbuild@0.27.0)) + '@storybook/mdx2-csf': + specifier: 1.1.0 + version: 1.1.0 + '@storybook/react-vite': + specifier: 10.1.10 + version: 10.1.10(esbuild@0.27.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(rollup@4.53.3)(storybook@10.1.10(@testing-library/dom@10.4.1)(prettier@2.8.8)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(typescript@5.9.3)(vite@7.3.0(@types/node@24.10.4)(jiti@2.4.2)(lightningcss@1.30.2)(sass@1.97.1)(terser@5.39.0)(tsx@4.21.0)(yaml@2.8.1))(webpack@5.104.1(esbuild@0.27.0)) + '@types/node': + specifier: 24.10.4 + version: 24.10.4 + '@ultraviolet/form': + specifier: workspace:* + version: link:../../packages/form + '@ultraviolet/icons': + specifier: workspace:* + version: link:../../packages/icons + '@ultraviolet/illustrations': + specifier: workspace:* + version: link:../../packages/illustrations + '@ultraviolet/plus': + specifier: workspace:* + version: link:../../packages/plus + '@ultraviolet/themes': + specifier: workspace:* + version: link:../../packages/themes + '@ultraviolet/ui': + specifier: workspace:* + version: link:../../packages/ui + '@vanilla-extract/css': + specifier: 1.17.4 + version: 1.17.4(babel-plugin-macros@3.1.0) + '@vanilla-extract/dynamic': + specifier: 2.1.5 + version: 2.1.5 + '@vanilla-extract/recipes': + specifier: 0.5.7 + version: 0.5.7(@vanilla-extract/css@1.17.4(babel-plugin-macros@3.1.0)) + '@vanilla-extract/sprinkles': + specifier: 1.6.5 + version: 1.6.5(@vanilla-extract/css@1.17.4(babel-plugin-macros@3.1.0)) + react: + specifier: 19.2.3 + version: 19.2.3 + storybook: + specifier: 10.1.10 + version: 10.1.10(@testing-library/dom@10.4.1)(prettier@2.8.8)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + storybook-addon-tag-badges: + specifier: 3.0.4 + version: 3.0.4(storybook@10.1.10(@testing-library/dom@10.4.1)(prettier@2.8.8)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)) + e2e: dependencies: '@ultraviolet/fonts': diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index 2ef3e15d4e..546ba19f7c 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -1,4 +1,5 @@ packages: + - "apps/*" - "packages/*" - "utils/*" - "examples/*" diff --git a/turbo.json b/turbo.json index f8011731db..6b82c0021d 100644 --- a/turbo.json +++ b/turbo.json @@ -8,12 +8,13 @@ "//#lint:fix": { "cache": false }, - "//#build:storybook": { + "build:storybook": { "dependsOn": ["^build"], "passThroughEnv": ["STORYBOOK_ENVIRONMENT"], - "outputs": ["build/**", "dist/**", "storybook-static/**"] + "outputs": ["build/**", "dist/**", "storybook-static/**"], + "cache": true }, - "//#build:storybook:stats": { + "build:storybook:stats": { "dependsOn": ["^build"], "passThroughEnv": ["STORYBOOK_ENVIRONMENT"] },