Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
ee89b4a
feat(tokens): design token foundation with JSON source of truth
talissoncosta Mar 30, 2026
a0600aa
feat(storybook): Storybook 10 with component stories and Chromatic
talissoncosta Mar 30, 2026
8d30815
docs(storybook): add token documentation stories
talissoncosta Mar 30, 2026
fce0970
docs(storybook): add MDX documentation pages
talissoncosta Mar 30, 2026
761897e
feat(tokens): add token utility classes and remove spacing tokens
talissoncosta Mar 31, 2026
aa246c7
feat(tokens): auto-generate utility classes from tokens.json
talissoncosta Mar 31, 2026
07fae71
fix(tokens): remove text-on-fill token that breaks naming convention
talissoncosta Mar 31, 2026
3fcc875
fix(tokens): remove font tokens — typography direction not yet decided
talissoncosta Apr 1, 2026
5f3a1b2
fix(tokens): remove stale space/SpaceScale exports from theme barrel
talissoncosta Apr 1, 2026
4c53b96
fix: restore package-lock.json from main to fix corrupted reference
talissoncosta Apr 1, 2026
58a2e8b
feat(tokens): add full primitive palette to tokens.json
talissoncosta Apr 1, 2026
91d7fb1
feat(tokens): reference primitives via var() and OKLCH in generated o…
talissoncosta Apr 1, 2026
3f30741
fix(tokens): add icon category to SemanticTokens Storybook story
talissoncosta Apr 1, 2026
c965674
docs: add Storybook-first rule to UI patterns context
talissoncosta Apr 1, 2026
2ff9ae1
docs(tokens): add OKLCH explanation comment in generator
talissoncosta Apr 1, 2026
59f7b82
feat(storybook): add webpack mocks and globals for component isolation
talissoncosta Apr 2, 2026
5385990
refactor: remove store/global deps from components for Storybook
talissoncosta Apr 2, 2026
b347360
feat(storybook): add stories for 39 components
talissoncosta Apr 2, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
45 changes: 45 additions & 0 deletions .github/workflows/frontend-chromatic.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
name: Frontend Chromatic

on:
pull_request:
types: [opened, synchronize, reopened, ready_for_review]
paths:
- frontend/**
- .github/workflows/frontend-chromatic.yml

permissions:
contents: read

jobs:
chromatic:
name: Chromatic
runs-on: ubuntu-latest
if: github.event.pull_request.draft == false

defaults:
run:
working-directory: frontend

steps:
- uses: actions/checkout@v5
with:
fetch-depth: 0

- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version-file: frontend/.nvmrc
cache: npm
cache-dependency-path: frontend/package-lock.json

- name: Install dependencies
run: npm ci

- name: Publish to Chromatic
uses: chromaui/action@v11
with:
workingDir: frontend
projectToken: ${{ secrets.CHROMATIC_PROJECT_TOKEN }}
exitZeroOnChanges: true
exitOnceUploaded: true
onlyChanged: true
4 changes: 4 additions & 0 deletions frontend/.claude/context/ui-patterns.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
# UI Patterns & Best Practices

## Storybook First

Before building or composing UI, query Storybook MCP (`list-all-documentation`, then `get-documentation`) to discover existing components and their props. Storybook is the source of truth for the design system — don't grep source files to find components when the catalogue is available.

## Table Components

### Pattern: Reusable Table Components
Expand Down
1 change: 1 addition & 0 deletions frontend/.eslintrc.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ module.exports = {
'plugin:@typescript-eslint/recommended',
'plugin:prettier/recommended',
'plugin:@dword-design/import-alias/recommended',
'plugin:storybook/recommended',
],
'globals': {
'$': true,
Expand Down
3 changes: 3 additions & 0 deletions frontend/.gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -33,3 +33,6 @@ common/project.js
# Playwright
e2e/playwright-report/
e2e/test-results/

*storybook.log
storybook-static
104 changes: 104 additions & 0 deletions frontend/.storybook/docs-theme.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
// =============================================================================
// Storybook Docs Theme Overrides
// =============================================================================
// The Storybook docs renderer uses its own styling that doesn't respond to
// our theme toggle. These overrides ensure documentation pages are readable
// in both light and dark mode.
// =============================================================================

// Light mode — fix invisible inline code
.sbdocs code {
color: #1a2634;
}

.dark {
// Docs page background
.sbdocs-wrapper {
background-color: var(--color-surface-default, #101628);
color: var(--color-text-default, #ffffff);
}

.sbdocs-content {
color: var(--color-text-default, #ffffff);
}

// Headings
.sbdocs h1,
.sbdocs h2,
.sbdocs h3,
.sbdocs h4,
.sbdocs h5 {
color: var(--color-text-default, #ffffff);
}

// Body text
.sbdocs p,
.sbdocs li,
.sbdocs td,
.sbdocs th {
color: var(--color-text-default, #ffffff);
}

// Code — inline and blocks
.sbdocs code {
background-color: var(--color-surface-emphasis, #202839);
color: var(--color-text-default, #ffffff);
}

.sbdocs pre {
background-color: var(--color-surface-emphasis, #202839);
color: var(--color-text-default, #ffffff);
}

// Storybook Source/code block container and all its children
.docblock-source,
.docblock-source > * {
background-color: var(--color-surface-emphasis, #202839) !important;
color: var(--color-text-default, #ffffff) !important;
}

// Copy button inside code blocks
.docblock-source button {
background-color: var(--color-surface-muted, #161d30) !important;
color: var(--color-text-default, #ffffff) !important;
border-color: var(--color-border-default, rgba(255, 255, 255, 0.16)) !important;
}

// Syntax highlighting tokens
.docblock-source .token {
color: var(--color-text-default, #ffffff) !important;
}

// Horizontal rules
.sbdocs hr {
border-color: var(--color-border-default, rgba(255, 255, 255, 0.16));
}

// Canvas (story preview) background
.docs-story {
background-color: var(--color-surface-default, #101628);
}

// Table borders
.sbdocs table {
border-color: var(--color-border-default, rgba(255, 255, 255, 0.16));
}

.sbdocs th,
.sbdocs td {
border-color: var(--color-border-default, rgba(255, 255, 255, 0.16));
}

// Table row backgrounds (override Storybook's alternating white rows)
.sbdocs tr {
background-color: var(--color-surface-default, #101628);
}

.sbdocs tr:nth-child(even) {
background-color: var(--color-surface-subtle, #15192b);
}

.sbdocs thead tr {
background-color: var(--color-surface-muted, #161d30);
}
}
77 changes: 77 additions & 0 deletions frontend/.storybook/main.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
const path = require('path')
const webpack = require('webpack')

/** @type { import('storybook').StorybookConfig } */
const config = {
stories: [
'../documentation/**/*.mdx',
'../documentation/**/*.stories.@(js|jsx|ts|tsx)',
],
staticDirs: ['../web'],
addons: [
'@storybook/addon-webpack5-compiler-swc',
'@storybook/addon-docs',
'@storybook/addon-a11y',
],
framework: {
name: '@storybook/react-webpack5',
options: {},
},
swc: () => ({
jsc: {
transform: {
react: {
runtime: 'automatic',
},
},
parser: {
syntax: 'typescript',
tsx: true,
},
},
}),
webpackFinal: async (config) => {
config.resolve = config.resolve || {}
config.resolve.alias = {
...config.resolve.alias,
common: path.resolve(__dirname, '../common'),
components: path.resolve(__dirname, '../web/components'),
project: path.resolve(__dirname, '../web/project'),
// Stub CommonJS modules that break Storybook's ESM bundler.
// code-help contains SDK snippets using module.exports — not needed for component rendering.
'common/code-help': path.resolve(__dirname, 'mocks/code-help.js'),
// Stub CommonJS data layer that breaks ESM bundler
[path.resolve(__dirname, '../common/data/base/_data.js')]: path.resolve(__dirname, 'mocks/_data.js'),
// Mock dompurify (CJS/ESM export mismatch)
'dompurify': path.resolve(__dirname, 'mocks/dompurify.js'),
}

config.module = config.module || {}
config.module.rules = config.module.rules || []
config.module.rules.push({
test: /\.scss$/,
use: [
'style-loader',
{ loader: 'css-loader', options: { importLoaders: 1 } },
{
loader: 'sass-loader',
options: {
sassOptions: {
silenceDeprecations: ['slash-div'],
},
},
},
],
})

config.plugins = config.plugins || []
config.plugins.push(
new webpack.DefinePlugin({
E2E: false,
}),
)

return config
},
}
module.exports = config
6 changes: 6 additions & 0 deletions frontend/.storybook/manager-head.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
<style>
.sidebar-header img {
max-width: 28px;
max-height: 28px;
}
</style>
61 changes: 61 additions & 0 deletions frontend/.storybook/manager.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
import { addons } from 'storybook/manager-api'
import { create } from 'storybook/theming'

// Primitive palette — mirrors _primitives.scss
// Storybook manager runs outside the app, so CSS vars aren't available.
const slate = {
0: '#ffffff',
50: '#fafafb',
100: '#eff1f4',
200: '#e0e3e9',
300: '#9da4ae',
500: '#656d7b',
600: '#1a2634',
850: '#161d30',
900: '#15192b',
}
const purple = { 600: '#6837fc' }

const shared = {
brandTitle: 'Flagsmith',
brandUrl: 'https://flagsmith.com',
brandImage: '/static/images/nav-logo.png',
brandTarget: '_blank',
fontBase: '"Inter", sans-serif',
colorPrimary: purple[600],
colorSecondary: purple[600],
}

const dark = create({
base: 'dark',
...shared,
appBg: slate[900],
appContentBg: slate[850],
appBorderColor: 'rgba(255, 255, 255, 0.1)',
barBg: slate[900],
barTextColor: slate[300],
barSelectedColor: purple[600],
textColor: slate[200],
textMutedColor: slate[300],
textInverseColor: slate[900],
})

const light = create({
base: 'light',
...shared,
appBg: slate[50],
appContentBg: slate[0],
appBorderColor: slate[100],
barBg: slate[0],
barTextColor: slate[500],
barSelectedColor: purple[600],
textColor: slate[600],
textMutedColor: slate[300],
textInverseColor: slate[0],
})

const prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches

addons.setConfig({
theme: prefersDark ? dark : light,
})
8 changes: 8 additions & 0 deletions frontend/.storybook/mocks/_data.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
// Stub for common/data/base/_data — CommonJS file that breaks Storybook's ESM bundler.
// This is the Flux data layer used for API calls, not needed for component rendering.
module.exports = {
get: () => Promise.resolve(),
post: () => Promise.resolve(),
put: () => Promise.resolve(),
delete: () => Promise.resolve(),
}
3 changes: 3 additions & 0 deletions frontend/.storybook/mocks/code-help.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
// Stub for common/code-help — CommonJS files that break Storybook's ESM bundler.
// These are SDK code snippets used in the docs UI, not needed for component rendering.
module.exports = {}
7 changes: 7 additions & 0 deletions frontend/.storybook/mocks/dompurify.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
// Mock for dompurify — the real module's CJS/ESM export mismatch breaks Storybook.
// Tooltip only uses sanitize() to clean HTML before rendering.
export function sanitize(html) {
return html
}

export default { sanitize }
9 changes: 9 additions & 0 deletions frontend/.storybook/modes.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
/** @type { Record<string, import('chromatic').Mode> } */
export const allModes = {
light: {
theme: 'light',
},
dark: {
theme: 'dark',
},
}
Loading
Loading