Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
16 commits
Select commit Hold shift + click to select a range
1cfff01
feat(tokens): add primitive colour palette and semantic token layer
talissoncosta Mar 10, 2026
efd8181
feat(storybook): add Storybook 8 with colour palette and token stories
talissoncosta Mar 10, 2026
7aac418
ci: add Chromatic visual regression workflow for Storybook
talissoncosta Mar 10, 2026
0b563f0
docs(storybook): add Introduction welcome page
talissoncosta Mar 10, 2026
38702df
chore(storybook): upgrade from 8.6 to 10.2
talissoncosta Mar 10, 2026
5678c6b
feat(storybook): add Flagsmith branding and dark theme to manager
talissoncosta Mar 10, 2026
94b733e
feat(tokens): add interactive surface and feedback hover/active tokens
talissoncosta Mar 10, 2026
ecd7a4f
refactor(storybook): make token and palette stories fully dynamic
talissoncosta Mar 10, 2026
08581d8
fix(storybook): fix code tag visibility and palette swatch layout
talissoncosta Mar 10, 2026
437d32c
fix(ci): add --legacy-peer-deps to frontend PR workflow
talissoncosta Mar 10, 2026
cc8cf0a
revert: keep Storybook on v8 until TypeScript is upgraded
talissoncosta Mar 10, 2026
8c7f7e5
refactor(storybook): extract inline styles into shared docs components
talissoncosta Mar 10, 2026
68f514c
fix(ci): clean up Chromatic workflow and add useEffect comment
talissoncosta Mar 10, 2026
a17d5dc
deps(storybook): upgrade from v8 to v10.3
talissoncosta Mar 24, 2026
4144313
refactor(storybook): migrate config and imports to v10 conventions
talissoncosta Mar 24, 2026
03d3462
refactor(storybook): move stories from stories/ to documentation/
talissoncosta Mar 24, 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
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
62 changes: 62 additions & 0 deletions frontend/.storybook/main.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
const path = require('path')

/** @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'),
}

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'],
},
},
},
],
})

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>
44 changes: 44 additions & 0 deletions frontend/.storybook/manager.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
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 = {
200: '#e0e3e9',
300: '#9da4ae',
850: '#161d30',
900: '#15192b',
}
const purple = { 600: '#6837fc' }

addons.setConfig({
theme: create({
base: 'dark',
brandTitle: 'Flagsmith',
brandUrl: 'https://flagsmith.com',
brandImage: '/static/images/nav-logo.png',
brandTarget: '_blank',

// Sidebar
appBg: slate[900],
appContentBg: slate[850],
appBorderColor: 'rgba(255, 255, 255, 0.1)',

// Typography
fontBase: '"Inter", sans-serif',

// Toolbar
barBg: slate[900],
barTextColor: slate[300],
barSelectedColor: purple[600],

// Colours
colorPrimary: purple[600],
colorSecondary: purple[600],

// Text
textColor: slate[200],
textMutedColor: slate[300],
textInverseColor: slate[900],
}),
})
47 changes: 47 additions & 0 deletions frontend/.storybook/preview.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import '../web/styles/styles.scss'

/** @type { import('storybook').Preview } */
const preview = {
globalTypes: {
theme: {
description: 'Dark mode toggle',
toolbar: {
title: 'Theme',
icon: 'moon',
items: [
{ value: 'light', title: 'Light', icon: 'sun' },
{ value: 'dark', title: 'Dark', icon: 'moon' },
],
dynamicTitle: true,
},
},
},
initialGlobals: {
theme: 'light',
},
decorators: [
(Story, context) => {
const theme = context.globals.theme || 'light'
const isDark = theme === 'dark'

document.documentElement.setAttribute(
'data-bs-theme',
isDark ? 'dark' : 'light',
)
document.body.classList.toggle('dark', isDark)

return Story()
},
],
parameters: {
controls: {
matchers: {
color: /(background|color)$/i,
date: /Date$/i,
},
},
backgrounds: { disable: true },
},
}

export default preview
79 changes: 79 additions & 0 deletions frontend/documentation/ColourPalette.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
import React, { useEffect, useState } from 'react'
import type { Meta, StoryObj } from 'storybook'

import './docs.scss'
import DocPage from './components/DocPage'
import ScaleRow from './components/ScaleRow'
import type { Scale } from './components/ScaleRow'

// @ts-expect-error raw-loader import
import primitivesSource from '!!raw-loader!../web/styles/_primitives.scss'

const meta: Meta = {
parameters: { layout: 'padded' },
title: 'Design System/Colour Palette',
}
export default meta

// ---------------------------------------------------------------------------
// Parse _primitives.scss at build time
// ---------------------------------------------------------------------------

function parsePrimitives(source: string): Scale[] {
const scales: Scale[] = []
let current: Scale | null = null

for (const line of source.split('\n')) {
// Section comment: "// Slate (neutrals)" → name = "Slate"
const sectionMatch = line.match(/^\/\/\s+(\w+)\s+\(/)
if (sectionMatch) {
current = { name: sectionMatch[1], swatches: [] }
scales.push(current)
continue
}

// Variable: "$slate-50: #fafafb;" → step=50, hex=#fafafb
const varMatch = line.match(/^\$(\w+)-(\d+):\s*(#[0-9a-fA-F]{6});/)
if (varMatch && current) {
current.swatches.push({
hex: varMatch[3],
step: varMatch[2],
variable: `$${varMatch[1]}-${varMatch[2]}`,
})
}
}

return scales
}

// ---------------------------------------------------------------------------
// Story
// ---------------------------------------------------------------------------

const PalettePage: React.FC = () => {
const [scales, setScales] = useState<Scale[]>([])

useEffect(() => {
setScales(parsePrimitives(primitivesSource))
}, [])

return (
<DocPage
title='Primitive Colour Palette'
description={
<>
Auto-generated from <code>web/styles/_primitives.scss</code>. Add a
new variable to the SCSS file and it will appear here automatically.
</>
}
>
{scales.map((scale) => (
<ScaleRow key={scale.name} scale={scale} />
))}
</DocPage>
)
}

export const Primitives: StoryObj = {
render: () => <PalettePage />,
}
72 changes: 72 additions & 0 deletions frontend/documentation/Introduction.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
{/* Introduction.mdx */}
import { Meta } from '@storybook/addon-docs/blocks'

<Meta title="Introduction" />

# Flagsmith Frontend

Welcome to the Flagsmith Storybook — the single source of truth for UI components, design tokens, and visual patterns used across the frontend.

If a component doesn't have a story, it doesn't exist.

## Getting started

```bash
npm run storybook
```

Launches Storybook at [http://localhost:6006](http://localhost:6006). Browse the sidebar to explore components and documentation.

## Why Storybook

- **Develop in isolation** — build and test components without spinning up the full app
- **Visual documentation** — every component variant is visible and interactive
- **Dark mode validation** — toggle themes in the toolbar to verify both modes
- **Accessibility** — the a11y addon runs checks automatically on every story

## Dark mode

Use the **Theme** toggle in the toolbar (moon/sun icon) to switch between light and dark mode. Every story should look correct in both themes.

## Writing stories

Create a `*.stories.tsx` file in the `documentation/` directory. Use the existing stories as a reference.

A good story should:

- Cover all meaningful variants of the component
- Work in both light and dark mode
- Include representative content, not just placeholder text

## Project structure

<table>
<thead>
<tr>
<th>Path</th>
<th>Purpose</th>
</tr>
</thead>
<tbody>
<tr>
<td>documentation/</td>
<td>Storybook stories and documentation (self-contained, portable)</td>
</tr>
<tr>
<td>web/components/</td>
<td>React components</td>
</tr>
<tr>
<td>web/styles/</td>
<td>SCSS styles and design tokens</td>
</tr>
<tr>
<td>common/</td>
<td>Shared state, services, types, and utilities</td>
</tr>
</tbody>
</table>

## Links

- [Frontend README](https://github.com/Flagsmith/flagsmith/blob/main/frontend/README.md)
Loading
Loading