Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
f769dd1
test(design): require executable Storybook authority
seonghobae Aug 20, 2026
20a04a3
feat(design): add executable Storybook viewer authority
seonghobae Aug 20, 2026
82c0252
test(design): require Storybook 10 responsive globals
seonghobae Aug 20, 2026
b461ef7
test(design): align responsive contract with Storybook 10
seonghobae Aug 20, 2026
7d037e3
fix(design): configure Storybook 10 viewports
seonghobae Aug 20, 2026
f59cd6f
test(design): pin viewer recovery action semantics
seonghobae Aug 20, 2026
6fb2429
fix(design): keep viewer recovery actions truthful
seonghobae Aug 20, 2026
2a4c83f
test(design): require reproducible Storybook install
seonghobae Aug 20, 2026
b27cc1e
chore(design): expose generated lock for review bootstrap
seonghobae Aug 20, 2026
d33e5df
chore(design): publish lock bootstrap artifact
seonghobae Aug 21, 2026
a6b8ebe
test(design): make Storybook contracts unittest-discoverable
seonghobae Aug 21, 2026
d14a8cb
test(design): enforce Storybook contract discovery
seonghobae Aug 21, 2026
20031c8
chore(design): commit reproducible Storybook lock
seonghobae Aug 21, 2026
5a8b7f4
test(design): remove invalid Storybook lock for clean resolution
seonghobae Aug 21, 2026
cdbd176
chore(design): bootstrap reviewed Storybook lock atomically
seonghobae Aug 21, 2026
4c76837
chore(design): refresh reviewed Storybook lock artifact
seonghobae Aug 23, 2026
62e5e0a
fix(design): stream Storybook lock bootstrap safely
seonghobae Aug 23, 2026
e816e1d
fix(design): bootstrap reviewed lock with guarded push
seonghobae Aug 23, 2026
82044d3
fix(design): lock Storybook dependency graph
github-actions[bot] Aug 23, 2026
7724d3d
fix(design): lock Storybook workflow to reviewed dependencies
seonghobae Aug 23, 2026
4937133
test(design): reject out-of-range sRGB token components
seonghobae Aug 23, 2026
bfccb02
fix(design): validate sRGB components before rounding
seonghobae Aug 23, 2026
2ea2bc5
docs(governance): register Storybook design gate
seonghobae Aug 23, 2026
db23649
docs(design): align Storybook lock authority
seonghobae Aug 23, 2026
b397ad7
docs: align repo-local Storybook workflow guidance
seonghobae Aug 23, 2026
4dead75
test(a11y): require semantic nameable preview region
seonghobae Aug 24, 2026
accbe89
fix(a11y): give named preview a semantic region role
seonghobae Aug 24, 2026
bbbe097
docs(a11y): trace named region semantics to WAI-ARIA 1.2
seonghobae Aug 24, 2026
d38ad4f
test(design): require loading preview guidance parity
seonghobae Aug 28, 2026
dcfb79e
fix(design): keep loading preview guidance visible
seonghobae Aug 28, 2026
62a53f8
docs(design): trace loading guidance parity
seonghobae Aug 28, 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
43 changes: 43 additions & 0 deletions .github/workflows/storybook.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
name: Storybook Design System
Comment thread
seonghobae marked this conversation as resolved.
Resolved

on:
pull_request: {}

permissions:
contents: read

jobs:
storybook:
name: Storybook browser acceptance
runs-on: ubuntu-latest
Comment thread
seonghobae marked this conversation as resolved.
Resolved
steps:
- name: Checkout exact pull request head
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
ref: ${{ github.event.pull_request.head.sha }}

- name: Verify exact checked-out revision
env:
EXPECTED_SHA: ${{ github.event.pull_request.head.sha }}
run: test "$(git rev-parse HEAD)" = "$EXPECTED_SHA"

- name: Setup Node.js
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
with:
node-version: "24"

- name: Install development dependencies from checked-in lock
run: npm ci --ignore-scripts --no-audit --no-fund

- name: Run executable design-system contract tests
run: python3 -m unittest discover -s scripts
Comment thread
seonghobae marked this conversation as resolved.
Resolved

- name: Install Chromium for Vitest browser mode
run: npx --no-install playwright install --with-deps chromium
Comment thread
seonghobae marked this conversation as resolved.
Resolved
Comment thread
seonghobae marked this conversation as resolved.
Resolved

- name: Build Storybook
run: npm run build-storybook

- name: Run Storybook browser tests
run: npm run test-storybook -- --run
13 changes: 13 additions & 0 deletions .storybook/main.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
const config = {
stories: ['../design/storybook/**/*.stories.js'],
addons: ['@storybook/addon-a11y', '@storybook/addon-vitest'],
framework: {
name: '@storybook/web-components-vite',
options: {},
},
core: {
disableTelemetry: true,
},
};

export default config;
21 changes: 21 additions & 0 deletions .storybook/preview.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import { MINIMAL_VIEWPORTS } from 'storybook/viewport';

import '../src/main/resources/static/assets/viewer/viewer.css';

const preview = {
tags: ['test'],
parameters: {
layout: 'fullscreen',
viewport: {
options: MINIMAL_VIEWPORTS,
},
a11y: {
test: 'error',
options: {
runOnly: ['wcag2a', 'wcag2aa', 'wcag21a', 'wcag21aa', 'wcag22aa', 'best-practice'],
},
},
Comment thread
seonghobae marked this conversation as resolved.
Resolved
},
};

export default preview;
5 changes: 5 additions & 0 deletions .storybook/vitest.setup.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import * as a11yAddonAnnotations from '@storybook/addon-a11y/preview';
import { setProjectAnnotations } from '@storybook/web-components-vite';
import * as projectAnnotations from './preview.js';

setProjectAnnotations([a11yAddonAnnotations, projectAnnotations]);
5 changes: 5 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,11 @@ including mandatory quality and security merge gates.
return values, and thrown failures must be understandable without reading the
implementation.
- Markdown lint for changed docs must pass.
- Storybook Design System must pass on the exact current PR head. Its acceptance
path uses the reviewed `package-lock.json`, `npm ci --ignore-scripts --no-audit
--no-fund`, the Python design-system contract suite, a Storybook production
build, and headless Chromium interaction/accessibility tests. It is
development-only design evidence and does not replace Maven or security gates.
- Security evidence must be attached on PR (SAST/code-scanning checks).
- CodeQL Java/Kotlin analysis must remain enabled through repository default
setup or an org-central workflow; do not add a repo-local advanced CodeQL
Expand Down
10 changes: 6 additions & 4 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -56,10 +56,12 @@ readiness scorecard, and Figma deck payload drift checks are Python scripts unde
`scripts/`; the exact invocations (with the current evidence/policy file paths)
are listed in `AGENTS.md` and must pass for doc/dependency changes that touch them.

There are no repo-local CI workflows in `.github/workflows`; gates are reproduced
locally and evidence is committed under `docs/qa/evidence/`. CodeQL runs through
GitHub default setup (do not add a repo-local advanced CodeQL workflow), and
Dependabot config lives in `.github/dependabot.yml`.
The repository-local `.github/workflows/storybook.yml` workflow runs the
`Storybook Design System` pull-request gate documented in `AGENTS.md`. Other
quality/security gates and their local reproduction/evidence are documented in
`AGENTS.md` and `docs/qa/evidence/`. CodeQL runs through GitHub default setup (do
not add a repo-local advanced CodeQL workflow), and Dependabot config lives in
`.github/dependabot.yml`.

## What this repository is

Expand Down
218 changes: 218 additions & 0 deletions design/storybook/viewer-states.stories.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,218 @@
import { expect, userEvent, within } from 'storybook/test';

const DOCUMENT_ID = '00000000-0000-0000-0000-000000000000';

const STATE_COPY = {
loading: {
status: 'Preparing the preview. No action is needed yet.',
preview: 'The document preview will appear here when preparation finishes.',
},
ready: {
status: 'Preview ready. Open the document or download the original.',
preview: 'Document preview is ready for review.',
},
failed: {
status: 'Preview preparation failed.',
errorTitle: 'Preview unavailable',
errorMessage: 'Try the preview again. If it fails again, download the original file.',
preview: 'The preview could not be prepared.',
},
notFound: {
status: 'Document not found.',
errorTitle: 'Document unavailable',
errorMessage: 'Check the link or return to your document list.',
preview: 'No document is available for this reference.',
},
invalid: {
status: 'This document link is invalid.',
errorTitle: 'Invalid document link',
errorMessage: 'Open a valid document link, then try again.',
preview: 'A valid document link is required before a preview can be shown.',
},
network: {
status: 'The preview service could not be reached.',
errorTitle: 'Connection interrupted',
errorMessage: 'Check your connection, then try the preview again.',
preview: 'The preview is waiting for a working connection.',
},
};

const HARNESS_STYLES = `
<style>
.storybook-viewer-frame {
width: 100%;
margin: 0 auto;
}
.storybook-viewer-frame .preview-placeholder {
min-height: 190px;
display: grid;
place-items: center;
text-align: center;
color: var(--muted);
padding: 24px;
}
.storybook-viewer-frame .story-note {
margin: 12px 0 0;
color: var(--muted);
font-size: 13px;
overflow-wrap: anywhere;
}
@media (prefers-reduced-motion: reduce) {
.storybook-viewer-frame .skeleton {
animation: none;
}
}
@media (forced-colors: active) {
.storybook-viewer-frame .status,
.storybook-viewer-frame .error,
.storybook-viewer-frame .preview,
.storybook-viewer-frame .btn {
border-color: CanvasText;
}
.storybook-viewer-frame .btn-primary {
forced-color-adjust: auto;
}
}
</style>
`;

function renderViewer({ state = 'ready', busy = false } = {}) {
const copy = STATE_COPY[state];
const root = document.createElement('div');
root.className = 'storybook-viewer-frame';

const error = copy.errorTitle
? `<div class="error" role="alert">
<p class="error__title">${copy.errorTitle}</p>
<p class="error__message">${copy.errorMessage}</p>
</div>`
: '';
const skeleton = state === 'loading'
? `<div class="skeleton" aria-hidden="true"></div>
<p class="story-note">${copy.preview}</p>`
: `<div class="preview-placeholder" role="document" aria-label="Document preview">${copy.preview}</div>`;
const unrecoverableReference = state === 'notFound' || state === 'invalid';
const primaryLabel = state === 'ready'
? 'Open document'
: state === 'notFound'
? 'Check document link'
: state === 'invalid'
? 'Open valid document link'
: 'Try preview again';
const primaryDisabled = busy ? ' disabled' : '';
const downloadAction = unrecoverableReference
? ''
: `<button class="btn btn-secondary" type="button"${primaryDisabled}>Download original</button>`;

root.innerHTML = `${HARNESS_STYLES}
<main class="app-main" aria-labelledby="viewer-title">
<h1 class="page-title" id="viewer-title">Clearfolio document viewer</h1>
<p class="page-subtitle">Review the document state, then choose the next available action.</p>
<section class="panel" aria-label="Preview workspace" aria-busy="${busy}">
<div class="panel-header">
<div>
<h2 class="panel__title" id="preview-title">Document preview</h2>
<p class="panel__caption">Reference ${DOCUMENT_ID}</p>
</div>
</div>
<div class="status" role="status" aria-live="polite">${copy.status}</div>
${error}
<div class="preview" role="region" aria-labelledby="preview-title">${skeleton}</div>
<div class="actions">
<button class="btn btn-primary" type="button"${primaryDisabled}>${primaryLabel}</button>
${downloadAction}
<button class="btn btn-secondary" type="button">Back to documents</button>
</div>
<p class="story-note">Exact document reference: ${DOCUMENT_ID}</p>
</section>
</main>`;
return root;
Comment thread
seonghobae marked this conversation as resolved.
Resolved
}

const meta = {
title: 'Viewer/Buyer states',
tags: ['test'],
render: (args) => renderViewer(args),
parameters: {
layout: 'fullscreen',
},
};

export default meta;

export const Loading = {
args: { state: 'loading', busy: true },
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);
await expect(canvas.getByText(STATE_COPY.loading.preview)).toBeVisible();
},
};

export const Ready = {
args: { state: 'ready' },
};

export const Failed = {
args: { state: 'failed' },
};

export const NotFound = {
args: { state: 'notFound' },
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);
await expect(canvas.getByRole('button', { name: 'Check document link' })).toBeEnabled();
await expect(canvas.getByRole('button', { name: 'Back to documents' })).toBeEnabled();
await expect(canvas.queryByRole('button', { name: 'Try preview again' })).not.toBeInTheDocument();
await expect(canvas.queryByRole('button', { name: 'Download original' })).not.toBeInTheDocument();
},
};

export const InvalidDocument = {
args: { state: 'invalid' },
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);
await expect(canvas.getByRole('button', { name: 'Open valid document link' })).toBeEnabled();
await expect(canvas.getByRole('button', { name: 'Back to documents' })).toBeEnabled();
await expect(canvas.queryByRole('button', { name: 'Try preview again' })).not.toBeInTheDocument();
await expect(canvas.queryByRole('button', { name: 'Download original' })).not.toBeInTheDocument();
},
};

export const NetworkError = {
args: { state: 'network' },
};

export const KeyboardFocus = {
args: { state: 'ready' },
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);
const primary = canvas.getByRole('button', { name: 'Open document' });
await userEvent.tab();
await expect(primary).toHaveFocus();
},
};

export const BusyDisabled = {
args: { state: 'loading', busy: true },
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);
await expect(canvas.getByRole('region', { name: 'Preview workspace' })).toHaveAttribute('aria-busy', 'true');
await expect(canvas.getByRole('button', { name: 'Try preview again' })).toBeDisabled();
await expect(canvas.getByRole('button', { name: 'Download original' })).toBeDisabled();
await expect(canvas.getByRole('button', { name: 'Back to documents' })).toBeEnabled();
},
};

export const MobileLoading = {
args: { state: 'loading', busy: true },
globals: {
viewport: { value: 'mobile1', isRotated: false },
},
Comment thread
seonghobae marked this conversation as resolved.
Resolved
};

export const TabletReady = {
args: { state: 'ready' },
globals: {
viewport: { value: 'tablet', isRotated: false },
},
};
45 changes: 45 additions & 0 deletions design/tokens/clearfolio.tokens.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
{
"$schema": "https://www.designtokens.org/schemas/2025.10/format.json",
"color": {
"brand-blue": {
"$type": "color",
"$description": "Projection of runtime CSS --brand-blue.",
"$value": {"colorSpace": "srgb", "components": [0.0117647059, 0.3058823529, 0.6352941176], "alpha": 1, "hex": "#034ea2"}
},
"ink": {
"$type": "color",
"$description": "Projection of runtime CSS --ink.",
"$value": {"colorSpace": "srgb", "components": [0.062745098, 0.1254901961, 0.1960784314], "alpha": 1, "hex": "#102032"}
},
"muted": {
"$type": "color",
"$description": "Projection of runtime CSS --muted.",
"$value": {"colorSpace": "srgb", "components": [0.3529411765, 0.4039215686, 0.4666666667], "alpha": 1, "hex": "#5a6777"}
},
"bg": {
"$type": "color",
"$description": "Projection of runtime CSS --bg.",
"$value": {"colorSpace": "srgb", "components": [0.9647058824, 0.968627451, 0.9764705882], "alpha": 1, "hex": "#f6f7f9"}
},
"panel": {
"$type": "color",
"$description": "Projection of runtime CSS --panel.",
"$value": {"colorSpace": "srgb", "components": [1, 1, 1], "alpha": 1, "hex": "#ffffff"}
},
"line": {
"$type": "color",
"$description": "Projection of runtime CSS --line.",
"$value": {"colorSpace": "srgb", "components": [0.8431372549, 0.8666666667, 0.9019607843], "alpha": 1, "hex": "#d7dde6"}
},
"danger": {
"$type": "color",
"$description": "Projection of runtime CSS --danger.",
"$value": {"colorSpace": "srgb", "components": [0.7058823529, 0.137254902, 0.0941176471], "alpha": 1, "hex": "#b42318"}
},
"focus": {
"$type": "color",
"$description": "Projection of runtime CSS --focus.",
"$value": {"colorSpace": "srgb", "components": [1, 0.3490196078, 0.3529411765], "alpha": 1, "hex": "#ff595a"}
}
}
}
Loading
Loading