Procházet zdrojové kódy

Create Storybook guideline and add addon-designs to embed Figma (#27310)

* Add foundations and migrations docs

* Add storybook/addon-designs version 8.2.1

* Test Figma link

* Refactor Modal stories

* Create figmaDesignUrl

* Create foundations

* Create feature flags docs

* Create Storybook builds docs

* Add storybook/addon-designs version 8.2.1

* Test Figma link

* Add an example of Story with split-tests within the Storybook guidelines (#27260)

* Add FormatCurrency demo in feature-flags.mdx

* Add syntax highlight to code samples

* Update stories with figmaDesignUrl

* Figma access token

* Use OLButton

* Hide control for children and footer

* Add primitive colors

* Use useSplitTest instead

* Update cloud builds docs with `storybook-push-trigger`

* Make Foundations the default story

---------

Co-authored-by: Antoine Clausse <antoine.clausse@overleaf.com>
GitOrigin-RevId: 0729759803f190d89cf543d087eea86265b725f1
Rebeka Dekany před 1 rokem
rodič
revize
ee8a9d9caf

+ 118 - 2
package-lock.json

@@ -4912,6 +4912,30 @@
         "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
       }
     },
+    "node_modules/@figspec/components": {
+      "version": "1.0.3",
+      "resolved": "https://registry.npmjs.org/@figspec/components/-/components-1.0.3.tgz",
+      "integrity": "sha512-fBwHzJ4ouuOUJEi+yBZIrOy+0/fAjB3AeTcIHTT1PRxLz8P63xwC7R0EsIJXhScIcc+PljGmqbbVJCjLsnaGYA==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "lit": "^2.1.3"
+      }
+    },
+    "node_modules/@figspec/react": {
+      "version": "1.0.4",
+      "resolved": "https://registry.npmjs.org/@figspec/react/-/react-1.0.4.tgz",
+      "integrity": "sha512-jaPvkIef4d6NjsRiw91OZabrfdPH9FtoPGYcY5mpXjYEcdUqIq1aHtLq3SkMVyVysEapTEJ6yS8amy93MyXBEQ==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@figspec/components": "^1.0.1",
+        "@lit-labs/react": "^1.0.2"
+      },
+      "peerDependencies": {
+        "react": "^16.14.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
+      }
+    },
     "node_modules/@google-cloud/bigquery": {
       "version": "5.10.0",
       "resolved": "https://registry.npmjs.org/@google-cloud/bigquery/-/bigquery-5.10.0.tgz",
@@ -6543,6 +6567,30 @@
         "@lezer/highlight": "^1.0.0"
       }
     },
+    "node_modules/@lit-labs/react": {
+      "version": "1.2.1",
+      "resolved": "https://registry.npmjs.org/@lit-labs/react/-/react-1.2.1.tgz",
+      "integrity": "sha512-DiZdJYFU0tBbdQkfwwRSwYyI/mcWkg3sWesKRsHUd4G+NekTmmeq9fzsurvcKTNVa0comNljwtg4Hvi1ds3V+A==",
+      "dev": true,
+      "license": "BSD-3-Clause"
+    },
+    "node_modules/@lit-labs/ssr-dom-shim": {
+      "version": "1.4.0",
+      "resolved": "https://registry.npmjs.org/@lit-labs/ssr-dom-shim/-/ssr-dom-shim-1.4.0.tgz",
+      "integrity": "sha512-ficsEARKnmmW5njugNYKipTm4SFnbik7CXtoencDZzmzo/dQ+2Q0bgkzJuoJP20Aj0F+izzJjOqsnkd6F/o1bw==",
+      "dev": true,
+      "license": "BSD-3-Clause"
+    },
+    "node_modules/@lit/reactive-element": {
+      "version": "1.6.3",
+      "resolved": "https://registry.npmjs.org/@lit/reactive-element/-/reactive-element-1.6.3.tgz",
+      "integrity": "sha512-QuTgnG52Poic7uM1AN5yJ09QMe0O28e10XzSvWDz02TJiiKee4stsiownEIadWm8nYzyDAyT+gKzUoZmiWQtsQ==",
+      "dev": true,
+      "license": "BSD-3-Clause",
+      "dependencies": {
+        "@lit-labs/ssr-dom-shim": "^1.0.0"
+      }
+    },
     "node_modules/@lukeed/csprng": {
       "version": "1.1.0",
       "resolved": "https://registry.npmjs.org/@lukeed/csprng/-/csprng-1.1.0.tgz",
@@ -10288,6 +10336,40 @@
         "storybook": "^8.6.12"
       }
     },
+    "node_modules/@storybook/addon-designs": {
+      "version": "8.2.1",
+      "resolved": "https://registry.npmjs.org/@storybook/addon-designs/-/addon-designs-8.2.1.tgz",
+      "integrity": "sha512-orwihs1D5alhh4Qu3BSJKbSgQOdSagvRX/25m5fYZQAaqVErBY0lRR4vCAU/G/STkcdv+MHwIQ5U+0kX5Tm2+w==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@figspec/react": "^1.0.0"
+      },
+      "peerDependencies": {
+        "@storybook/blocks": "^8.0.0 || ^8.1.0-0 || ^8.2.0-0 || ^8.3.0-0 || ^8.4.0-0 || ^8.5.0-0 || ^8.6.0-0",
+        "@storybook/components": "^8.0.0 || ^8.1.0-0 || ^8.2.0-0 || ^8.3.0-0 || ^8.4.0-0 || ^8.5.0-0 || ^8.6.0-0",
+        "@storybook/theming": "^8.0.0 || ^8.1.0-0 || ^8.2.0-0 || ^8.3.0-0 || ^8.4.0-0 || ^8.5.0-0 || ^8.6.0-0",
+        "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0-beta",
+        "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0-beta"
+      },
+      "peerDependenciesMeta": {
+        "@storybook/blocks": {
+          "optional": true
+        },
+        "@storybook/components": {
+          "optional": true
+        },
+        "@storybook/theming": {
+          "optional": true
+        },
+        "react": {
+          "optional": true
+        },
+        "react-dom": {
+          "optional": true
+        }
+      }
+    },
     "node_modules/@storybook/addon-docs": {
       "version": "8.6.12",
       "resolved": "https://registry.npmjs.org/@storybook/addon-docs/-/addon-docs-8.6.12.tgz",
@@ -12605,8 +12687,7 @@
       "version": "2.0.7",
       "resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.7.tgz",
       "integrity": "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==",
-      "dev": true,
-      "optional": true
+      "dev": true
     },
     "node_modules/@types/uuid": {
       "version": "9.0.8",
@@ -27787,6 +27868,40 @@
         }
       }
     },
+    "node_modules/lit": {
+      "version": "2.8.0",
+      "resolved": "https://registry.npmjs.org/lit/-/lit-2.8.0.tgz",
+      "integrity": "sha512-4Sc3OFX9QHOJaHbmTMk28SYgVxLN3ePDjg7hofEft2zWlehFL3LiAuapWc4U/kYwMYJSh2hTCPZ6/LIC7ii0MA==",
+      "dev": true,
+      "license": "BSD-3-Clause",
+      "dependencies": {
+        "@lit/reactive-element": "^1.6.0",
+        "lit-element": "^3.3.0",
+        "lit-html": "^2.8.0"
+      }
+    },
+    "node_modules/lit-element": {
+      "version": "3.3.3",
+      "resolved": "https://registry.npmjs.org/lit-element/-/lit-element-3.3.3.tgz",
+      "integrity": "sha512-XbeRxmTHubXENkV4h8RIPyr8lXc+Ff28rkcQzw3G6up2xg5E8Zu1IgOWIwBLEQsu3cOVFqdYwiVi0hv0SlpqUA==",
+      "dev": true,
+      "license": "BSD-3-Clause",
+      "dependencies": {
+        "@lit-labs/ssr-dom-shim": "^1.1.0",
+        "@lit/reactive-element": "^1.3.0",
+        "lit-html": "^2.8.0"
+      }
+    },
+    "node_modules/lit-html": {
+      "version": "2.8.0",
+      "resolved": "https://registry.npmjs.org/lit-html/-/lit-html-2.8.0.tgz",
+      "integrity": "sha512-o9t+MQM3P4y7M7yNzqAyjp7z+mQGa4NS4CxiyLqFPyFWyc4O+nodLrkrxSaCTrla6M5YOLaT3RpbbqjszB5g3Q==",
+      "dev": true,
+      "license": "BSD-3-Clause",
+      "dependencies": {
+        "@types/trusted-types": "^2.0.2"
+      }
+    },
     "node_modules/load-script": {
       "version": "1.0.0",
       "resolved": "https://registry.npmjs.org/load-script/-/load-script-1.0.0.tgz",
@@ -44516,6 +44631,7 @@
         "@replit/codemirror-vim": "overleaf/codemirror-vim#1bef138382d948018f3f9b8a4d7a70ab61774e4b",
         "@sentry/browser": "7.46.0",
         "@storybook/addon-a11y": "^8.6.12",
+        "@storybook/addon-designs": "^8.2.1",
         "@storybook/addon-essentials": "^8.6.12",
         "@storybook/addon-interactions": "^8.6.12",
         "@storybook/addon-links": "^8.6.12",

+ 3 - 0
services/web/.storybook/main.ts

@@ -19,12 +19,15 @@ const config: StorybookConfig = {
   stories: [
     path.join(rootDir, 'frontend/stories/**/*.stories.{js,jsx,ts,tsx}'),
     path.join(rootDir, 'modules/**/stories/**/*.stories.{js,jsx,ts,tsx}'),
+    path.join(rootDir, 'frontend/stories/**/*.mdx'),
+    path.join(rootDir, 'modules/**/stories/**/*.mdx'),
   ],
   addons: [
     getAbsolutePath('@storybook/addon-links'),
     getAbsolutePath('@storybook/addon-essentials'),
     getAbsolutePath('@storybook/addon-interactions'),
     getAbsolutePath('@storybook/addon-a11y'),
+    getAbsolutePath('@storybook/addon-designs'),
     getAbsolutePath('@storybook/addon-webpack5-compiler-babel'),
     {
       name: getAbsolutePath('@storybook/addon-styling-webpack'),

+ 5 - 1
services/web/.storybook/preview.tsx

@@ -131,7 +131,11 @@ const preview: Preview = {
     options: {
       storySort: {
         method: 'alphabetical',
-        order: ['Shared'],
+        order: [
+          'Storybook Guideline',
+          ['Foundations', 'Storybook builds', 'Feature Flags'],
+          'Shared',
+        ],
       },
     },
   },

+ 20 - 0
services/web/.storybook/utils/figma-design-url.ts

@@ -0,0 +1,20 @@
+/** Creates design parameters for a Storybook story conditionally.
+ * Helper function to generate design parameters based on the presence of a Figma access token.
+ * The benefit of the token is that it allows component inspection directly in Storybook.
+ * If token is not available, it defaults to a basic Figma URL.
+ * Token can be generated in your Figma account settings.
+ * To copy URL: In your Figma file, click to select the specific component frame you want to display in Storybook.
+ * It's important to select the outer frame of the component, not just a single layer inside it.
+ */
+
+export const figmaDesignUrl = (url: string) => {
+  const accessToken = process.env.STORYBOOK_FIGMA_ACCESS_TOKEN
+
+  const designConfig = accessToken
+    ? { type: 'figspec', url, accessToken }
+    : { type: 'figma', url }
+
+  return {
+    design: designConfig,
+  }
+}

+ 7 - 4
services/web/.storybook/utils/with-split-tests.tsx

@@ -1,8 +1,8 @@
 import type { Meta } from '@storybook/react'
 import _ from 'lodash'
-import { SplitTestContext } from '../../frontend/js/shared/context/split-test-context'
+import { SplitTestContext } from '@/shared/context/split-test-context'
 
-export const splitTestsArgTypes = {
+export const defaultSplitTestsArgTypes = {
   // to be able to use this utility, you need to add the argTypes for each split test in this object
   // Check the original implementation for an example: https://github.com/overleaf/internal/pull/17809
   'editor-redesign': {
@@ -14,10 +14,13 @@ export const splitTestsArgTypes = {
   },
 }
 
-export const withSplitTests = (
+export const withSplitTests = <ArgTypes = typeof defaultSplitTestsArgTypes,>(
   story: Meta,
-  splitTests: (keyof typeof splitTestsArgTypes)[] = []
+  splitTests: (keyof ArgTypes)[] = [],
+  /** @deprecated For demo purposes only. Add actual split tests in defaultSplitTestsArgTypes */
+  _splitTestsArgTypes?: ArgTypes
 ): Meta => {
+  const splitTestsArgTypes = _splitTestsArgTypes ?? defaultSplitTestsArgTypes
   return {
     ...story,
     argTypes: { ...story.argTypes, ..._.pick(splitTestsArgTypes, splitTests) },

+ 24 - 0
services/web/frontend/stories/docs/cloud-builds.mdx

@@ -0,0 +1,24 @@
+import { Meta } from '@storybook/blocks';
+
+<Meta title="Storybook Guideline / Storybook builds" />
+
+
+# Cloud Builds
+
+Storybook builds can be used to share your development work. They can be found at [https://storybook.dev-overleaf.com/](https://storybook.dev-overleaf.com/).
+
+The naming scheme is predictable, i.e.: `https://storybook.dev-overleaf.com/<BRANCH_NAME>/`.
+
+# Triggers
+
+Storybook builds are automatically triggered for the main branch. For ad hoc builds of feature branches, the commits must be pushed to branches prefixed with `storybook-`.
+
+For example, you may temporarily push your branch to a `storybook-` branch: `git push origin my-example-feature:storybook-my-example-feature`
+
+You can delete it shortly after: `git push origin :storybook-my-example-feature`
+
+Alternatively, **if you already have a branch**, but you would like to create a Storybook, you can manually trigger the `storybook-push-trigger` on your branch via [Google Cloud Build triggers](https://console.cloud.google.com/cloud-build/triggers?project=overleaf-dev).
+
+Storybook builds take about 10 minutes to finish.
+
+**Source:** [Storybook in the developer manual](https://manual.dev-overleaf.com/development/code/storybook/)

+ 40 - 0
services/web/frontend/stories/docs/feature-flags.mdx

@@ -0,0 +1,40 @@
+import { Canvas, Controls, Meta } from '@storybook/blocks';
+import * as FormatCurrency from './format-currency.stories'
+
+<Meta title="Storybook Guideline / Feature flags" />
+
+# Feature flags
+
+You can wrap your story with the `withSplitTests` utility to add split test variants to your Storybook stories, so you can toggle feature flags directly in the Storybook UI. See [withSplitTests on GitHub](https://github.com/overleaf/internal/blob/main/services/web/.storybook/utils/with-split-tests.tsx).
+
+1. Define your split test argTypes:
+Add your split test configurations to the `splitTestsArgTypes` object.
+
+```js
+export const splitTestsArgTypes = {
+  'local-ccy-format': {
+    description: 'Use local currency formatting',
+    control: { type: 'radio' },
+    options: ['default', 'enabled'],
+  },
+}
+```
+2. Wrap your story with `withSplitTests`.
+
+Import `withSplitTests` and `Meta` from '@storybook/react' in your stories.
+
+```js
+export default {
+  ...config,
+  ...withSplitTests(config, ['local-ccy-format']),
+}
+```
+
+## Example
+
+Resulting stories will have added controls to define the variants of the split tests.
+
+<div>
+  <Canvas of={FormatCurrency.Story} />
+  <Controls of={FormatCurrency.Story} />
+</div>

+ 44 - 0
services/web/frontend/stories/docs/format-currency.stories.tsx

@@ -0,0 +1,44 @@
+import React from 'react'
+import { useSplitTest } from '@/shared/context/split-test-context'
+import { withSplitTests } from '../../../.storybook/utils/with-split-tests'
+
+const FormatCurrency = () => {
+  const { variant } = useSplitTest('local-ccy-format')
+  const formatCurrency = (amount: number, narrowSymbol: boolean) => {
+    return variant === 'enabled'
+      ? new Intl.NumberFormat('en-US', {
+          style: 'currency',
+          currency: 'USD',
+          currencyDisplay: narrowSymbol ? 'narrowSymbol' : 'name',
+        }).format(amount)
+      : `USD ${amount}`
+  }
+  return (
+    <div>
+      <strong>Variant:</strong> {JSON.stringify(variant)}
+      <br />
+      <strong>Long:</strong> {formatCurrency(1234.56, false)}
+      <br />
+      <strong>Short:</strong> {formatCurrency(1234.56, true)}
+    </div>
+  )
+}
+
+const config = {
+  title: 'Storybook Guideline / Feature Flags', // Must match MDX title exactly
+  component: FormatCurrency,
+}
+
+export default {
+  ...config,
+  ...withSplitTests(config, ['local-ccy-format'], {
+    'local-ccy-format': {
+      description: 'Use local currency formatting',
+      control: { type: 'radio' as const },
+      options: ['default', 'enabled'],
+    },
+  }),
+  tags: ['!dev'], // hides in the sidebar
+}
+
+export const Story = {}

+ 234 - 0
services/web/frontend/stories/docs/foundations.mdx

@@ -0,0 +1,234 @@
+import { Meta, ColorItem, ColorPalette, Title, Typeset } from '@storybook/blocks';
+import colors from '../../stylesheets/bootstrap-5/foundations/tokens/colors.json';
+import typography from '../../stylesheets/bootstrap-5/foundations/tokens/typography.json';
+import borderRadius from '../../stylesheets/bootstrap-5/foundations/tokens/borderRadius.json';
+import spacing from '../../stylesheets/bootstrap-5/foundations/tokens/spacing.json';
+
+<Meta title="Storybook Guideline / Foundations" />
+
+# Foundations
+
+Foundations in UX design are the basic rules and core elements, like colors, fonts, and spacing, that ensure consistency across the design.
+<p>
+  These palettes are generated from our token files. The tokens were exported from Figma and split into separate files to categorise: <a href="https://github.com/overleaf/internal/tree/main/services/web/frontend/stylesheets/bootstrap-5/foundations/tokens" target="_blank" rel="noopener noreferrer">Token files on GitHub</a>
+</p>
+
+**Table of Contents**
+- [Primitive colors](#primitive-colors)
+- [Background colors](#background-colors)
+- [Content colors](#content-colors)
+- [Border colors](#border-colors)
+- [Link colors](#link-colors)
+- [Special colors](#special-colors)
+- [Font weight](#font-weight)
+- [Font size](#font-size)
+- [Border radius](#border-radius)
+- [Spacing](#spacing)
+
+# Colors
+<br/>
+
+## Primitive colors
+
+<ColorPalette>
+  {Object.entries(colors.PrimitiveColor).map(([name, color]) => (
+    <ColorItem
+      key={name}
+      title={name}
+      colors={[color.$value]}
+    />
+  ))}
+</ColorPalette>
+
+## Background colors
+
+Colors used for page and component backgrounds.
+
+<ColorPalette>
+  {Object.entries(colors)
+    .filter(([name]) => name.startsWith('bg-'))
+    .map(([name, colors]) => (
+      <ColorItem
+        key={name}
+        title={name}
+        subtitle={colors.$primitive}
+        colors={[colors.$value]}
+      />
+    ))}
+</ColorPalette>
+
+## Content colors
+
+Colors used for text and icons.
+
+<ColorPalette>
+  {Object.entries(colors)
+    .filter(([name]) => name.startsWith('content-'))
+    .map(([name, colors]) => (
+      <ColorItem
+        key={name}
+        title={name}
+        subtitle={colors.$primitive}
+        colors={[colors.$value]}
+      />
+    ))}
+</ColorPalette>
+
+## Border colors
+
+Colors used for borders and dividers.
+
+<ColorPalette>
+  {Object.entries(colors)
+    .filter(([name]) => name.startsWith('border-'))
+    .map(([name, colors]) => (
+      <ColorItem
+        key={name}
+        title={name}
+        subtitle={colors.$primitive}
+        colors={[colors.$value]}
+      />
+    ))}
+</ColorPalette>
+
+## Link colors
+
+Colors used for hyperlink states.
+
+<ColorPalette>
+  {Object.entries(colors)
+    .filter(([name]) => name.startsWith('link-'))
+    .map(([name, colors]) => (
+      <ColorItem
+        key={name}
+        title={name}
+        subtitle={colors.$primitive}
+        colors={[colors.$value]}
+      />
+    ))}
+</ColorPalette>
+
+## Special colors
+
+Colors used for specific UI elements like color pickers.
+
+<ColorPalette>
+  {Object.entries(colors)
+    .filter(([name]) => name.startsWith('special-'))
+    .map(([name, colors]) => (
+      <ColorItem
+        key={name}
+        title={name}
+        subtitle={colors.$primitive}
+        colors={[colors.$value]}
+      />
+    ))}
+</ColorPalette>
+
+# Typography
+<br/>
+
+## Font weight
+<br/>
+
+{Object.entries(typography.Fontweight).map(([name, typography]) => (
+  <div key={name}>
+    <strong>{name}</strong>
+    <Typeset
+      fontSizes={[14]}
+      fontWeight={typography.$value}
+      sampleText="The quick brown fox jumps over the lazy dog"
+    />
+  </div>
+))}
+
+## Font size
+<br/>
+
+{Object.entries(typography['Fontsize']).map(([name, typography]) => {
+  const mixins = typography.$mixin ? [].concat(typography.$mixin) : [];
+
+  return (
+    <div key={name} style={{ marginBottom: '2rem' }}>
+      <strong>
+        {name}
+        {mixins.map(mixin => (
+          <span key={mixin}> / @mixin {mixin}</span>
+        ))}
+      </strong>
+      <Typeset
+        fontSizes={[`${typography.$value}px`]}
+        fontWeight={500}
+        sampleText="The quick brown fox jumps over the lazy dog"
+      />
+    </div>
+  );
+})}
+
+## Border radius
+<br/>
+
+<table style={{ width: '100%' }}>
+  <thead>
+    <tr>
+      <th>Variable name</th>
+      <th>Value</th>
+      <th style={{ textAlign: 'center' }}>Example</th>
+    </tr>
+  </thead>
+  <tbody>
+    {Object.entries(borderRadius['BorderRadius']).map(([name, borderRadius]) => (
+      <tr key={name}>
+        <td>
+          <strong>{name}</strong>
+        </td>
+        <td style={{ textAlign: 'center' }}>{borderRadius.$value}px</td>
+        <td style={{ textAlign: 'center', padding: '0.5rem' }}>
+          <div
+            style={{
+              width: '80px',
+              height: '80px',
+              borderRadius: `${borderRadius.$value}px`,
+              backgroundColor: '#098842',
+              display: 'inline-block',
+            }}
+          />
+        </td>
+      </tr>
+    ))}
+  </tbody>
+</table>
+
+## Spacing
+<br/>
+
+<table style={{ width: '100%' }}>
+  <thead>
+    <tr>
+      <th>Variable Name</th>
+      <th>Value</th>
+      <th style={{ textAlign: 'left', paddingLeft: '1rem' }}>Example</th>
+    </tr>
+  </thead>
+  <tbody>
+    {Object.entries(spacing.Spacing)
+      .filter(([, spacing]) => spacing.$value > 0)
+      .map(([name, spacing]) => (
+        <tr key={name}>
+          <td>
+            <strong>{name}</strong>
+          </td>
+          <td style={{ textAlign: 'center' }}>{spacing.$value}px</td>
+          <td style={{ padding: '0.5rem 1rem', verticalAlign: 'middle' }}>
+            <div
+              style={{
+                height: `${spacing.$value}px`,
+                width: '100%',
+                backgroundColor: '#098842',
+              }}
+            />
+          </td>
+        </tr>
+      ))}
+  </tbody>
+</table>

+ 74 - 117
services/web/frontend/stories/modals/modal.stories.tsx

@@ -1,140 +1,80 @@
-import Button from '@/shared/components/button/button'
 import type { Meta, StoryObj } from '@storybook/react'
+import { figmaDesignUrl } from './../../../.storybook/utils/figma-design-url'
 import OLModal, {
   OLModalHeader,
   OLModalBody,
   OLModalFooter,
   OLModalTitle,
 } from '@/shared/components/ol/ol-modal'
+import OLButton from '@/shared/components/ol/ol-button'
 
 type Story = StoryObj<typeof OLModal>
 
 export const Default: Story = {
-  render: args => {
-    return (
-      <OLModal show {...args}>
-        <OLModalHeader closeButton>
-          <OLModalTitle>Heading</OLModalTitle>
-        </OLModalHeader>
-        <OLModalBody>
-          <p>
-            Lorem ipsum dolor sit lorem a amet, consectetur adipiscing elit, sed
-            do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut
-            enim ad minim veniam.
-          </p>
-        </OLModalBody>
-        <OLModalFooter>
-          <Button variant="secondary">Cancel</Button>
-          <Button variant="primary">Primary</Button>
-        </OLModalFooter>
-      </OLModal>
-    )
+  args: {
+    title: 'Heading',
+    children: (
+      <p>
+        Always use the modal actions in the footer and use descriptive words for
+        them.
+      </p>
+    ),
+    footer: (
+      <>
+        <OLButton variant="secondary">Cancel</OLButton>
+        <OLButton variant="primary">Primary</OLButton>
+      </>
+    ),
   },
+  parameters: figmaDesignUrl(
+    'https://www.figma.com/design/V7Ogph1Ocs4ux2A4WMNAh7/Overleaf---Components?m=auto&node-id=3488-82657&m=dev'
+  ),
 }
 
-export const ModalWithAcknowledgment: Story = {
-  render: args => {
-    return (
-      <OLModal show {...args}>
-        <OLModalHeader closeButton>
-          <OLModalTitle>Acknowledgment</OLModalTitle>
-        </OLModalHeader>
-        <OLModalBody>
-          <p>
-            System requires an acknowledgment from the user. Usually contains
-            only one primary button.
-          </p>
-        </OLModalBody>
-        <OLModalFooter>
-          <Button variant="primary">Accept</Button>
-        </OLModalFooter>
-      </OLModal>
-    )
+export const Informative: Story = {
+  parameters: figmaDesignUrl(
+    'https://www.figma.com/design/V7Ogph1Ocs4ux2A4WMNAh7/Overleaf---Components?m=auto&node-id=3488-86576&m=dev'
+  ),
+  args: {
+    title: 'Informative',
+    children: (
+      <p>
+        Presents information for the user to be aware of and doesn’t require any
+        action.
+      </p>
+    ),
   },
 }
 
-export const ModalWithSecondary: Story = {
-  render: args => {
-    return (
-      <OLModal show {...args}>
-        <OLModalHeader closeButton>
-          <OLModalTitle>Heading</OLModalTitle>
-        </OLModalHeader>
-        <OLModalBody>
-          <p>
-            Lorem ipsum dolor sit lorem a amet, consectetur adipiscing elit, sed
-            do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut
-            enim ad minim veniam.
-          </p>
-        </OLModalBody>
-        <OLModalFooter>
-          <Button variant="secondary">Cancel</Button>
-        </OLModalFooter>
-      </OLModal>
-    )
+export const Acknowledgment: Story = {
+  parameters: figmaDesignUrl(
+    'https://www.figma.com/design/V7Ogph1Ocs4ux2A4WMNAh7/Overleaf---Components?m=auto&node-id=3488-86581&m=dev'
+  ),
+  args: {
+    title: 'Acknowledgment',
+    children: (
+      <p>
+        System requires an acknowledgment from the user. Usually contains only
+        one primary button.
+      </p>
+    ),
+    footer: <OLButton variant="primary">Accept</OLButton>,
   },
 }
 
-export const ModalWithTertiary: Story = {
-  render: args => {
-    return (
-      <OLModal show {...args}>
-        <OLModalHeader closeButton>
-          <OLModalTitle>Heading</OLModalTitle>
-        </OLModalHeader>
-        <OLModalBody>
-          <p>Used for destructive or irreversible actions.</p>
-        </OLModalBody>
-        <OLModalFooter>
-          <Button variant="secondary">Third</Button>
-          <Button variant="secondary" className="ms-auto">
-            Cancel
-          </Button>
-          <Button variant="primary">Primary</Button>
-        </OLModalFooter>
-      </OLModal>
-    )
-  },
-}
-
-export const ModalInformative: Story = {
-  render: args => {
-    return (
-      <OLModal show {...args}>
-        <OLModalHeader closeButton>
-          <OLModalTitle>Informative</OLModalTitle>
-        </OLModalHeader>
-        <OLModalBody>
-          <p>
-            Presents information for the user to be aware of and doesn’t require
-            any action.
-          </p>
-        </OLModalBody>
-      </OLModal>
-    )
-  },
-}
-
-export const ModalDanger: Story = {
-  render: args => {
-    return (
-      <OLModal show {...args}>
-        <OLModalHeader closeButton>
-          <OLModalTitle>Danger</OLModalTitle>
-        </OLModalHeader>
-        <OLModalBody>
-          <p>
-            Lorem ipsum dolor sit lorem a amet, consectetur adipiscing elit, sed
-            do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut
-            enim ad minim veniam.
-          </p>
-        </OLModalBody>
-        <OLModalFooter>
-          <Button variant="secondary">Cancel</Button>
-          <Button variant="danger">Delete</Button>
-        </OLModalFooter>
-      </OLModal>
-    )
+export const Danger: Story = {
+  parameters: figmaDesignUrl(
+    'https://www.figma.com/design/V7Ogph1Ocs4ux2A4WMNAh7/Overleaf---Components?m=auto&node-id=3488-86586&m=dev'
+  ),
+  args: {
+    title: 'Danger',
+    children: <p>Used for destructive or irreversible actions.</p>,
+    footer: (
+      <>
+        <OLButton variant="secondary">Cancel</OLButton>
+        <OLButton variant="danger">Delete</OLButton>
+      </>
+    ),
   },
 }
 
@@ -146,7 +86,24 @@ const meta: Meta<typeof OLModal> = {
       control: 'radio',
       options: ['lg', 'md', 'sm'],
     },
+    title: { control: 'text' },
+    children: { control: false },
+    footer: { control: false },
+  },
+  args: {
+    show: true,
+    size: 'sm',
+    onHide: () => {},
   },
+  render: ({ title, children, footer, ...args }) => (
+    <OLModal {...args}>
+      <OLModalHeader closeButton>
+        <OLModalTitle>{title}</OLModalTitle>
+      </OLModalHeader>
+      <OLModalBody>{children}</OLModalBody>
+      {footer && <OLModalFooter>{footer}</OLModalFooter>}
+    </OLModal>
+  ),
 }
 
 export default meta

+ 20 - 15
services/web/frontend/stories/ui/badge.stories.tsx

@@ -1,7 +1,8 @@
-import Badge from '@/shared/components/badge/badge'
-import MaterialIcon from '@/shared/components/material-icon'
 import type { Meta, StoryObj } from '@storybook/react'
 import classnames from 'classnames'
+import { figmaDesignUrl } from '../../../.storybook/utils/figma-design-url'
+import Badge from '@/shared/components/badge/badge'
+import MaterialIcon from '@/shared/components/material-icon'
 
 const meta: Meta<typeof Badge> = {
   title: 'Shared / Components / Badge',
@@ -31,20 +32,27 @@ export default meta
 type Story = StoryObj<typeof Badge>
 
 export const BadgeDefault: Story = {
-  render: args => {
-    return (
-      <Badge
-        className={classnames({ 'text-dark': args.bg === 'light' })}
-        {...args}
-      />
-    )
+  args: {
+    bg: meta.argTypes!.bg!.options![0],
   },
-}
-BadgeDefault.args = {
-  bg: meta.argTypes!.bg!.options![0],
+  parameters: figmaDesignUrl(
+    'https://www.figma.com/design/V7Ogph1Ocs4ux2A4WMNAh7/Overleaf---Components?node-id=3458-9502&m=dev'
+  ),
+  render: args => (
+    <Badge
+      className={classnames({ 'text-dark': args.bg === 'light' })}
+      {...args}
+    />
+  ),
 }
 
 export const BadgePrepend: Story = {
+  args: {
+    bg: meta.argTypes!.bg!.options![0],
+  },
+  parameters: figmaDesignUrl(
+    'https://www.figma.com/design/V7Ogph1Ocs4ux2A4WMNAh7/Overleaf---Components?node-id=3458-11319&m=dev'
+  ),
   render: args => {
     return (
       <Badge
@@ -55,6 +63,3 @@ export const BadgePrepend: Story = {
     )
   },
 }
-BadgePrepend.args = {
-  bg: meta.argTypes!.bg!.options![0],
-}

+ 14 - 9
services/web/frontend/stories/ui/button.stories.tsx

@@ -1,27 +1,28 @@
-import Button from '@/shared/components/button/button'
 import { Meta } from '@storybook/react'
+import { figmaDesignUrl } from '../../../.storybook/utils/figma-design-url'
+import OLButton from '@/shared/components/ol/ol-button'
 
-type Args = React.ComponentProps<typeof Button>
+type Args = React.ComponentProps<typeof OLButton>
 
 export const NewButton = (args: Args) => {
-  return <Button {...args} />
+  return <OLButton {...args} />
 }
 
 export const ButtonWithLeadingIcon = (args: Args) => {
-  return <Button leadingIcon="add" {...args} />
+  return <OLButton leadingIcon="add" {...args} />
 }
 
 export const ButtonWithTrailingIcon = (args: Args) => {
-  return <Button trailingIcon="add" {...args} />
+  return <OLButton trailingIcon="add" {...args} />
 }
 
 export const ButtonWithIcons = (args: Args) => {
-  return <Button trailingIcon="add" leadingIcon="add" {...args} />
+  return <OLButton trailingIcon="add" leadingIcon="add" {...args} />
 }
 
-const meta: Meta<typeof Button> = {
+const meta: Meta<typeof OLButton> = {
   title: 'Shared / Components / Button',
-  component: Button,
+  component: OLButton,
   args: {
     children: 'A Button',
     disabled: false,
@@ -30,7 +31,7 @@ const meta: Meta<typeof Button> = {
   argTypes: {
     size: {
       control: 'radio',
-      options: ['small', 'default', 'large'],
+      options: ['lg', 'md', 'sm'],
     },
     variant: {
       control: 'radio',
@@ -42,9 +43,13 @@ const meta: Meta<typeof Button> = {
         'danger-ghost',
         'premium',
         'premium-secondary',
+        'link',
       ],
     },
   },
+  parameters: figmaDesignUrl(
+    'https://www.figma.com/design/V7Ogph1Ocs4ux2A4WMNAh7/Overleaf---Components?node-id=3458-22412&m=dev'
+  ),
 }
 
 export default meta

+ 24 - 0
services/web/frontend/stylesheets/bootstrap-5/foundations/tokens/borderRadius.json

@@ -0,0 +1,24 @@
+{
+  "BorderRadius": {
+    "border-radius-none": {
+      "$type": "number",
+      "$value": 0
+    },
+    "border-radius-base": {
+      "$type": "number",
+      "$value": 4
+    },
+    "border-radius-medium": {
+      "$type": "number",
+      "$value": 8
+    },
+    "border-radius-large": {
+      "$type": "number",
+      "$value": 16
+    },
+    "border-radius-full": {
+      "$type": "number",
+      "$value": 9999
+    }
+  }
+}

+ 449 - 0
services/web/frontend/stylesheets/bootstrap-5/foundations/tokens/colors.json

@@ -0,0 +1,449 @@
+{
+  "PrimitiveColor": {
+    "white": {
+      "$type": "color",
+      "$value": "#fff",
+      "$primitive": null
+    },
+    "neutral-10": {
+      "$type": "color",
+      "$value": "#f4f5f6",
+      "$primitive": null
+    },
+    "neutral-20": {
+      "$type": "color",
+      "$value": "#e7e9ee",
+      "$primitive": null
+    },
+    "neutral-30": {
+      "$type": "color",
+      "$value": "#d0d5dd",
+      "$primitive": null
+    },
+    "neutral-40": {
+      "$type": "color",
+      "$value": "#afb5c0",
+      "$primitive": null
+    },
+    "neutral-50": {
+      "$type": "color",
+      "$value": "#8d96a5",
+      "$primitive": null
+    },
+    "neutral-60": {
+      "$type": "color",
+      "$value": "#677283",
+      "$primitive": null
+    },
+    "neutral-70": {
+      "$type": "color",
+      "$value": "#495365",
+      "$primitive": null
+    },
+    "neutral-80": {
+      "$type": "color",
+      "$value": "#2f3a4c",
+      "$primitive": null
+    },
+    "neutral-90": {
+      "$type": "color",
+      "$value": "#1b222c",
+      "$primitive": null
+    },
+    "green-10": {
+      "$type": "color",
+      "$value": "#eaf6ef",
+      "$primitive": null
+    },
+    "green-20": {
+      "$type": "color",
+      "$value": "#b8dbc8",
+      "$primitive": null
+    },
+    "green-30": {
+      "$type": "color",
+      "$value": "#86caa5",
+      "$primitive": null
+    },
+    "green-40": {
+      "$type": "color",
+      "$value": "#53b57f",
+      "$primitive": null
+    },
+    "green-50": {
+      "$type": "color",
+      "$value": "#098842",
+      "$primitive": null
+    },
+    "green-60": {
+      "$type": "color",
+      "$value": "#1e6b41",
+      "$primitive": null
+    },
+    "green-70": {
+      "$type": "color",
+      "$value": "#195936",
+      "$primitive": null
+    },
+    "blue-10": {
+      "$type": "color",
+      "$value": "#f1f4f9",
+      "$primitive": null
+    },
+    "blue-20": {
+      "$type": "color",
+      "$value": "#c3d0e3",
+      "$primitive": null
+    },
+    "blue-30": {
+      "$type": "color",
+      "$value": "#97b6e5",
+      "$primitive": null
+    },
+    "blue-40": {
+      "$type": "color",
+      "$value": "#6597e0",
+      "$primitive": null
+    },
+    "blue-50": {
+      "$type": "color",
+      "$value": "#366cbf",
+      "$primitive": null
+    },
+    "blue-60": {
+      "$type": "color",
+      "$value": "#28518f",
+      "$primitive": null
+    },
+    "blue-70": {
+      "$type": "color",
+      "$value": "#214475",
+      "$primitive": null
+    },
+    "red-10": {
+      "$type": "color",
+      "$value": "#f9f1f1",
+      "$primitive": null
+    },
+    "red-20": {
+      "$type": "color",
+      "$value": "#f5beba",
+      "$primitive": null
+    },
+    "red-30": {
+      "$type": "color",
+      "$value": "#e59d9a",
+      "$primitive": null
+    },
+    "red-40": {
+      "$type": "color",
+      "$value": "#e36d66",
+      "$primitive": null
+    },
+    "red-50": {
+      "$type": "color",
+      "$value": "#b83a33",
+      "$primitive": null
+    },
+    "red-60": {
+      "$type": "color",
+      "$value": "#942f2a",
+      "$primitive": null
+    },
+    "red-70": {
+      "$type": "color",
+      "$value": "#782722",
+      "$primitive": null
+    },
+    "yellow-10": {
+      "$type": "color",
+      "$value": "#fcf1e3",
+      "$primitive": null
+    },
+    "yellow-20": {
+      "$type": "color",
+      "$value": "#fcc483",
+      "$primitive": null
+    },
+    "yellow-30": {
+      "$type": "color",
+      "$value": "#f7a445",
+      "$primitive": null
+    },
+    "yellow-40": {
+      "$type": "color",
+      "$value": "#de8014",
+      "$primitive": null
+    },
+    "yellow-50": {
+      "$type": "color",
+      "$value": "#8f5514",
+      "$primitive": null
+    },
+    "yellow-60": {
+      "$type": "color",
+      "$value": "#7a4304",
+      "$primitive": null
+    },
+    "yellow-70": {
+      "$type": "color",
+      "$value": "#633a0b",
+      "$primitive": null
+    }
+  },
+  "special-neutral": {
+    "$type": "color",
+    "$value": "#a7b1c2",
+    "$primitive": "Neutral"
+  },
+  "special-red": {
+    "$type": "color",
+    "$value": "#f04343",
+    "$primitive": "Red"
+  },
+  "special-orange": {
+    "$type": "color",
+    "$value": "#dd8a3e",
+    "$primitive": "Orange"
+  },
+  "special-yellow": {
+    "$type": "color",
+    "$value": "#e4ca3e",
+    "$primitive": "Yellow"
+  },
+  "special-green": {
+    "$type": "color",
+    "$value": "#33cf67",
+    "$primitive": "Green"
+  },
+  "special-lightblue": {
+    "$type": "color",
+    "$value": "#43a7f0",
+    "$primitive": "LightBlue"
+  },
+  "special-blue": {
+    "$type": "color",
+    "$value": "#434af0",
+    "$primitive": "Blue"
+  },
+  "special-violet": {
+    "$type": "color",
+    "$value": "#b943f0",
+    "$primitive": "Violet"
+  },
+  "special-pink": {
+    "$type": "color",
+    "$value": "#ff4bcd",
+    "$primitive": "Pink"
+  },
+  "bg-light-primary": {
+    "$type": "color",
+    "$value": "#ffffff",
+    "$primitive": "white"
+  },
+  "bg-light-secondary": {
+    "$type": "color",
+    "$value": "#f4f5f6",
+    "$primitive": "neutral-10"
+  },
+  "bg-light-tertiary": {
+    "$type": "color",
+    "$value": "#e7e9ee",
+    "$primitive": "neutral-20"
+  },
+  "bg-light-disabled": {
+    "$type": "color",
+    "$value": "#e7e9ee",
+    "$primitive": "neutral-20"
+  },
+  "bg-dark-primary": {
+    "$type": "color",
+    "$value": "#1b222c",
+    "$primitive": "neutral-90"
+  },
+  "bg-dark-secondary": {
+    "$type": "color",
+    "$value": "#2f3a4c",
+    "$primitive": "neutral-80"
+  },
+  "bg-dark-tertiary": {
+    "$type": "color",
+    "$value": "#495365",
+    "$primitive": "neutral-70"
+  },
+  "bg-dark-disabled": {
+    "$type": "color",
+    "$value": "#495365",
+    "$primitive": "neutral-70"
+  },
+  "bg-accent-01": {
+    "$type": "color",
+    "$value": "#098842",
+    "$primitive": "green-50"
+  },
+  "bg-accent-02": {
+    "$type": "color",
+    "$value": "#1e6b41",
+    "$primitive": "green-60"
+  },
+  "bg-accent-03": {
+    "$type": "color",
+    "$value": "#eaf6ef",
+    "$primitive": "green-10"
+  },
+  "bg-danger-01": {
+    "$type": "color",
+    "$value": "#b83a33",
+    "$primitive": "red-50"
+  },
+  "bg-danger-02": {
+    "$type": "color",
+    "$value": "#942f2a",
+    "$primitive": "red-60"
+  },
+  "bg-danger-03": {
+    "$type": "color",
+    "$value": "#f9f1f1",
+    "$primitive": "red-10"
+  },
+  "bg-warning-01": {
+    "$type": "color",
+    "$value": "#8f5514",
+    "$primitive": "yellow-50"
+  },
+  "bg-warning-02": {
+    "$type": "color",
+    "$value": "#7a4304",
+    "$primitive": "yellow-60"
+  },
+  "bg-warning-03": {
+    "$type": "color",
+    "$value": "#fcf1e3",
+    "$primitive": "yellow-10"
+  },
+  "bg-info-01": {
+    "$type": "color",
+    "$value": "#366cbf",
+    "$primitive": "blue-50"
+  },
+  "bg-info-02": {
+    "$type": "color",
+    "$value": "#28518f",
+    "$primitive": "blue-60"
+  },
+  "bg-info-03": {
+    "$type": "color",
+    "$value": "#f1f4f9",
+    "$primitive": "blue-10"
+  },
+  "content-primary": {
+    "$type": "color",
+    "$value": "#1b222c",
+    "$primitive": "neutral-90"
+  },
+  "content-secondary": {
+    "$type": "color",
+    "$value": "#495365",
+    "$primitive": "neutral-70"
+  },
+  "content-disabled": {
+    "$type": "color",
+    "$value": "#afb5c0",
+    "$primitive": "neutral-40"
+  },
+  "content-placeholder": {
+    "$type": "color",
+    "$value": "#677283",
+    "$primitive": "neutral-60"
+  },
+  "content-danger": {
+    "$type": "color",
+    "$value": "#b83a33",
+    "$primitive": "red-50"
+  },
+  "content-warning": {
+    "$type": "color",
+    "$value": "#8f5514",
+    "$primitive": "yellow-50"
+  },
+  "content-positive": {
+    "$type": "color",
+    "$value": "#098842",
+    "$primitive": "green-50"
+  },
+  "content-info": {
+    "$type": "color",
+    "$value": "#366cbf",
+    "$primitive": "blue-50"
+  },
+  "border-primary": {
+    "$type": "color",
+    "$value": "#677283",
+    "$primitive": "neutral-60"
+  },
+  "border-hover": {
+    "$type": "color",
+    "$value": "#495365",
+    "$primitive": "neutral-70"
+  },
+  "border-disabled": {
+    "$type": "color",
+    "$value": "#e7e9ee",
+    "$primitive": "neutral-20"
+  },
+  "border-danger": {
+    "$type": "color",
+    "$value": "#b83a33",
+    "$primitive": "red-50"
+  },
+  "border-divider": {
+    "$type": "color",
+    "$value": "#e7e9ee",
+    "$primitive": "neutral-20"
+  },
+  "border-active": {
+    "$type": "color",
+    "$value": "#366cbf",
+    "$primitive": "blue-50"
+  },
+  "link-web": {
+    "$type": "color",
+    "$value": "#1e6b41",
+    "$primitive": "green-60"
+  },
+  "link-web-hover": {
+    "$type": "color",
+    "$value": "#195936",
+    "$primitive": "green-70"
+  },
+  "link-web-visited": {
+    "$type": "color",
+    "$value": "#1e6b41",
+    "$primitive": "green-60"
+  },
+  "link-ui": {
+    "$type": "color",
+    "$value": "#366cbf",
+    "$primitive": "blue-50"
+  },
+  "link-ui-hover": {
+    "$type": "color",
+    "$value": "#28518f",
+    "$primitive": "blue-60"
+  },
+  "link-ui-visited": {
+    "$type": "color",
+    "$value": "#28518f",
+    "$primitive": "blue-60"
+  },
+  "hover-interaction": {
+    "$type": "color",
+    "$value": "rgba(27, 34, 44, 0.0800)",
+    "$primitive": null
+  },
+  "overlay": {
+    "$type": "color",
+    "$value": "rgba(27, 34, 44, 0.8000)",
+    "$primitive": null
+  }
+}

+ 72 - 0
services/web/frontend/stylesheets/bootstrap-5/foundations/tokens/spacing.json

@@ -0,0 +1,72 @@
+{
+  "Spacing": {
+    "spacing-00": {
+      "$type": "number",
+      "$value": 0
+    },
+    "spacing-01": {
+      "$type": "number",
+      "$value": 2
+    },
+    "spacing-02": {
+      "$type": "number",
+      "$value": 4
+    },
+    "spacing-03": {
+      "$type": "number",
+      "$value": 6
+    },
+    "spacing-04": {
+      "$type": "number",
+      "$value": 8
+    },
+    "spacing-05": {
+      "$type": "number",
+      "$value": 12
+    },
+    "spacing-06": {
+      "$type": "number",
+      "$value": 16
+    },
+    "spacing-07": {
+      "$type": "number",
+      "$value": 20
+    },
+    "spacing-08": {
+      "$type": "number",
+      "$value": 24
+    },
+    "spacing-09": {
+      "$type": "number",
+      "$value": 32
+    },
+    "spacing-10": {
+      "$type": "number",
+      "$value": 40
+    },
+    "spacing-11": {
+      "$type": "number",
+      "$value": 48
+    },
+    "spacing-12": {
+      "$type": "number",
+      "$value": 56
+    },
+    "spacing-13": {
+      "$type": "number",
+      "$value": 64
+    },
+    "spacing-14": {
+      "$type": "number",
+      "$value": 72
+    },
+    "spacing-15": {
+      "$type": "number",
+      "$value": 80
+    },
+    "spacing-16": {
+      "$type": "number",
+      "$value": 96
+    }
+  }
+}

+ 133 - 0
services/web/frontend/stylesheets/bootstrap-5/foundations/tokens/typography.json

@@ -0,0 +1,133 @@
+{
+  "Fontweight": {
+    "font-weight-regular": {
+      "$type": "number",
+      "$value": 400
+    },
+    "font-weight-medium": {
+      "$type": "number",
+      "$value": 500
+    },
+    "font-weight-semibold": {
+      "$type": "number",
+      "$value": 600
+    }
+  },
+  "Fontsize": {
+    "font-size-01": {
+      "$type": "number",
+      "$value": 12,
+      "$mixin": "body-xs"
+    },
+    "font-size-02": {
+      "$type": "number",
+      "$value": 14,
+      "$mixin": "body-sm"
+    },
+    "font-size-03": {
+      "$type": "number",
+      "$value": 16,
+      "$mixin": "body-base"
+    },
+    "font-size-04": {
+      "$type": "number",
+      "$value": 18,
+      "$mixin": ["body-lg", "heading-xs"]
+    },
+    "font-size-05": {
+      "$type": "number",
+      "$value": 20,
+      "$mixin": "heading-sm"
+    },
+    "font-size-06": {
+      "$type": "number",
+      "$value": 24,
+      "$mixin": "heading-md"
+    },
+    "font-size-07": {
+      "$type": "number",
+      "$value": 30,
+      "$mixin": "heading-lg"
+    },
+    "font-size-08": {
+      "$type": "number",
+      "$value": 36,
+      "$mixin": "heading-xl"
+    },
+    "font-size-09": {
+      "$type": "number",
+      "$value": 48,
+      "$mixin": "heading-2xl"
+    },
+    "font-size-10": {
+      "$type": "number",
+      "$value": 52,
+      "$mixin": "display-xs"
+    },
+    "font-size-11": {
+      "$type": "number",
+      "$value": 60,
+      "$mixin": "display-sm"
+    },
+    "font-size-12": {
+      "$type": "number",
+      "$value": 72,
+      "$mixin": "display-md"
+    },
+    "font-size-13": {
+      "$type": "number",
+      "$value": 96,
+      "$mixin": "display-lg"
+    }
+  },
+  "Lineheight": {
+    "line-height-01": {
+      "$type": "number",
+      "$value": 16
+    },
+    "line-height-02": {
+      "$type": "number",
+      "$value": 20
+    },
+    "line-height-03": {
+      "$type": "number",
+      "$value": 24
+    },
+    "line-height-04": {
+      "$type": "number",
+      "$value": 28
+    },
+    "line-height-05": {
+      "$type": "number",
+      "$value": 32
+    },
+    "line-height-06": {
+      "$type": "number",
+      "$value": 40
+    },
+    "line-height-07": {
+      "$type": "number",
+      "$value": 48
+    },
+    "line-height-08": {
+      "$type": "number",
+      "$value": 64
+    },
+    "line-height-09": {
+      "$type": "number",
+      "$value": 68
+    },
+    "line-height-10": {
+      "$type": "number",
+      "$value": 80
+    },
+    "line-height-11": {
+      "$type": "number",
+      "$value": 96
+    },
+    "line-height-12": {
+      "$type": "number",
+      "$value": 128
+    }
+  }
+}

+ 1 - 0
services/web/package.json

@@ -218,6 +218,7 @@
     "@replit/codemirror-vim": "overleaf/codemirror-vim#1bef138382d948018f3f9b8a4d7a70ab61774e4b",
     "@sentry/browser": "7.46.0",
     "@storybook/addon-a11y": "^8.6.12",
+    "@storybook/addon-designs": "^8.2.1",
     "@storybook/addon-essentials": "^8.6.12",
     "@storybook/addon-interactions": "^8.6.12",
     "@storybook/addon-links": "^8.6.12",