From f769dd11bd32f0ab19ad86c5c064cda838a4b472 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 20 Aug 2026 11:03:56 -0700 Subject: [PATCH 01/31] test(design): require executable Storybook authority --- scripts/test_storybook_design_system.py | 98 +++++++++++++++++++++++++ 1 file changed, 98 insertions(+) create mode 100644 scripts/test_storybook_design_system.py diff --git a/scripts/test_storybook_design_system.py b/scripts/test_storybook_design_system.py new file mode 100644 index 000000000..b135ab819 --- /dev/null +++ b/scripts/test_storybook_design_system.py @@ -0,0 +1,98 @@ +from __future__ import annotations + +import json +import re +from pathlib import Path + +ROOT = Path(__file__).resolve().parents[1] +CSS = ROOT / "src/main/resources/static/assets/viewer/viewer.css" +TOKENS = ROOT / "design/tokens/clearfolio.tokens.json" +STORY = ROOT / "design/storybook/viewer-states.stories.js" +MAIN = ROOT / ".storybook/main.js" +PREVIEW = ROOT / ".storybook/preview.js" +PACKAGE = ROOT / "package.json" + +REQUIRED_STATES = { + "Loading", + "Ready", + "Failed", + "NotFound", + "InvalidDocument", + "NetworkError", + "KeyboardFocus", + "BusyDisabled", + "MobileLoading", + "TabletReady", +} +REQUIRED_TOKENS = { + "brand-blue", + "ink", + "muted", + "bg", + "panel", + "line", + "danger", + "focus", +} + + +def _runtime_tokens() -> dict[str, str]: + css = CSS.read_text(encoding="utf-8") + root = re.search(r":root\s*\{(?P.*?)\n\}", css, re.DOTALL) + assert root is not None + return { + name: value.strip() + for name, value in re.findall(r"--([a-z0-9-]+)\s*:\s*([^;]+);", root.group("body")) + if name in REQUIRED_TOKENS + } + + +def test_storybook_authority_files_exist() -> None: + for path in (TOKENS, STORY, MAIN, PREVIEW, PACKAGE): + assert path.is_file(), f"missing executable design-system authority: {path.relative_to(ROOT)}" + + +def test_dtcg_projection_matches_runtime_css() -> None: + payload = json.loads(TOKENS.read_text(encoding="utf-8")) + colors = payload["color"] + runtime = _runtime_tokens() + assert set(runtime) == REQUIRED_TOKENS + projected = {name: value["$value"] for name, value in colors.items()} + assert projected == runtime + assert all(value["$type"] == "color" for value in colors.values()) + + +def test_required_buyer_states_are_named_and_a11y_blocking() -> None: + story = STORY.read_text(encoding="utf-8") + exports = set(re.findall(r"export const ([A-Za-z0-9_]+)\s*=", story)) + assert REQUIRED_STATES <= exports + preview = PREVIEW.read_text(encoding="utf-8") + assert "test: 'error'" in preview or 'test: "error"' in preview + assert "wcag22aa" in preview + assert "prefers-reduced-motion" in story + assert "forced-colors" in story + + +def test_storybook_is_development_only_and_has_build_test_commands() -> None: + package = json.loads(PACKAGE.read_text(encoding="utf-8")) + assert "dependencies" not in package or not package["dependencies"] + dev = package["devDependencies"] + assert dev["storybook"].startswith("10.5") + assert dev["@storybook/web-components-vite"].startswith("10.5") + scripts = package["scripts"] + assert "storybook build" in scripts["build-storybook"] + assert "test-storybook" in scripts["test-storybook"] + + +def test_story_fixtures_exclude_customer_authority() -> None: + story = STORY.read_text(encoding="utf-8").lower() + forbidden = ( + "bearer ", + "authorization", + "tenant_id", + "signed_url", + "access_token", + "refresh_token", + ) + assert not any(term in story for term in forbidden) + assert "00000000-0000-0000-0000-000000000000" in story From 20a04a323953ea4f87016cc7d86fed0359cabf26 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 20 Aug 2026 11:14:01 -0700 Subject: [PATCH 02/31] feat(design): add executable Storybook viewer authority --- .github/workflows/storybook.yml | 43 +++++ .storybook/main.js | 13 ++ .storybook/preview.js | 16 ++ .storybook/vitest.setup.js | 5 + design/storybook/viewer-states.stories.js | 187 ++++++++++++++++++++++ design/tokens/clearfolio.tokens.json | 45 ++++++ docs/design-system-authority.md | 54 +++++++ package.json | 21 +++ scripts/test_storybook_design_system.py | 67 +++++++- vitest.config.js | 33 ++++ 10 files changed, 476 insertions(+), 8 deletions(-) create mode 100644 .github/workflows/storybook.yml create mode 100644 .storybook/main.js create mode 100644 .storybook/preview.js create mode 100644 .storybook/vitest.setup.js create mode 100644 design/storybook/viewer-states.stories.js create mode 100644 design/tokens/clearfolio.tokens.json create mode 100644 docs/design-system-authority.md create mode 100644 package.json create mode 100644 vitest.config.js diff --git a/.github/workflows/storybook.yml b/.github/workflows/storybook.yml new file mode 100644 index 000000000..e5f49d03e --- /dev/null +++ b/.github/workflows/storybook.yml @@ -0,0 +1,43 @@ +name: Storybook Design System + +on: + pull_request: {} + +permissions: + contents: read + +jobs: + storybook: + name: Storybook browser acceptance + runs-on: ubuntu-latest + 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: Resolve exact direct development dependencies + run: npm install --package-lock-only --ignore-scripts --no-audit --no-fund + + - name: Install development dependencies from generated lock + run: npm ci --ignore-scripts --no-audit --no-fund + + - name: Install Chromium for Vitest browser mode + run: npx --no-install playwright install --with-deps chromium + + - name: Build Storybook + run: npm run build-storybook + + - name: Run Storybook browser tests + run: npm run test-storybook -- --run diff --git a/.storybook/main.js b/.storybook/main.js new file mode 100644 index 000000000..040c0087d --- /dev/null +++ b/.storybook/main.js @@ -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; diff --git a/.storybook/preview.js b/.storybook/preview.js new file mode 100644 index 000000000..ff8c849e8 --- /dev/null +++ b/.storybook/preview.js @@ -0,0 +1,16 @@ +import '../src/main/resources/static/assets/viewer/viewer.css'; + +const preview = { + tags: ['test'], + parameters: { + layout: 'fullscreen', + a11y: { + test: 'error', + options: { + runOnly: ['wcag2a', 'wcag2aa', 'wcag21a', 'wcag21aa', 'wcag22aa', 'best-practice'], + }, + }, + }, +}; + +export default preview; diff --git a/.storybook/vitest.setup.js b/.storybook/vitest.setup.js new file mode 100644 index 000000000..d619ff044 --- /dev/null +++ b/.storybook/vitest.setup.js @@ -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]); diff --git a/design/storybook/viewer-states.stories.js b/design/storybook/viewer-states.stories.js new file mode 100644 index 000000000..3cdf9a5d6 --- /dev/null +++ b/design/storybook/viewer-states.stories.js @@ -0,0 +1,187 @@ +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 = ` + +`; + +function renderViewer({ state = 'ready', width = '100%', busy = false } = {}) { + const copy = STATE_COPY[state]; + const root = document.createElement('div'); + root.className = 'storybook-viewer-frame'; + root.style.maxWidth = width; + + const error = copy.errorTitle + ? `` + : ''; + const skeleton = state === 'loading' + ? '' + : `
${copy.preview}
`; + const primaryLabel = state === 'ready' ? 'Open document' : 'Try preview again'; + const retryDisabled = busy ? ' disabled' : ''; + + root.innerHTML = `${HARNESS_STYLES} +
+

Clearfolio document viewer

+

Review the document state, then choose the next available action.

+
+
+
+

Document preview

+

Reference ${DOCUMENT_ID}

+
+
+
${copy.status}
+ ${error} +
${skeleton}
+
+ + + +
+

Exact document reference: ${DOCUMENT_ID}

+
+
`; + return root; +} + +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 }, +}; + +export const Ready = { + args: { state: 'ready' }, +}; + +export const Failed = { + args: { state: 'failed' }, +}; + +export const NotFound = { + args: { state: 'notFound' }, +}; + +export const InvalidDocument = { + args: { state: 'invalid' }, +}; + +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' })).toBeDisabled(); + }, +}; + +export const MobileLoading = { + args: { state: 'loading', busy: true, width: '390px' }, + parameters: { + viewport: { defaultViewport: 'mobile1' }, + }, +}; + +export const TabletReady = { + args: { state: 'ready', width: '768px' }, +}; diff --git a/design/tokens/clearfolio.tokens.json b/design/tokens/clearfolio.tokens.json new file mode 100644 index 000000000..9ca34a55f --- /dev/null +++ b/design/tokens/clearfolio.tokens.json @@ -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"} + } + } +} diff --git a/docs/design-system-authority.md b/docs/design-system-authority.md new file mode 100644 index 000000000..01e7ab56b --- /dev/null +++ b/docs/design-system-authority.md @@ -0,0 +1,54 @@ +# Design-system authority + +## Decision + +Clearfolio keeps the shipped WebFlux/static JavaScript/CSS viewer as the production UI architecture. `src/main/resources/static/assets/viewer/viewer.css` is the runtime source of truth for shipped visual tokens and reusable viewer primitives. Storybook is development-only executable design evidence; it does not become a second production frontend or network authority. + +`design/tokens/clearfolio.tokens.json` is a Design Tokens Community Group (DTCG) 2025.10 interchange projection of the accepted runtime colors. The Buyer-readiness contract fails when its hexadecimal and sRGB values drift from the CSS source. The projection does not silently overwrite runtime CSS. + +## Executable buyer states + +`design/storybook/viewer-states.stories.js` covers the buyer-decision states currently represented by the live Figma plan and the repository viewer boundary: + +- loading and busy/disabled controls; +- ready; +- preview failure; +- document not found; +- invalid document link; +- network interruption; +- keyboard focus behavior; +- mobile loading; +- tablet-width ready state; +- reduced-motion and forced-colors alternatives. + +Every error state gives a concrete next action. Loading copy explains that no action is required yet. The stories use only a fixed all-zero document reference and make no network requests; they contain no customer data, tenant authority, signed links, or credentials. + +Project-level Storybook accessibility configuration sets `a11y.test` to `error` and runs WCAG A/AA tags through WCAG 2.2 AA plus axe best-practice checks. The Storybook Vitest integration executes smoke and `play` interaction tests in headless Chromium through Vitest Browser Mode and Playwright. + +## Figma traceability + +Fresh evidence was re-read on 2026-08-21 from Clearfolio Figma file `UPIMvnjyP1sTXhIy1wqD02`, page `0:1`, board `2:2` (`Clearfolio Viewer Product Design + Analytics Board`). The board identifies itself as a no-Code-Connect design plan and contains desktop loading/ready/failed, mobile loading, and tablet concepts. Its local swatches match protected-main runtime values: brand `#034ea2`, ink `#102032`, muted `#5a6777`, background `#f6f7f9`, panel `#ffffff`, line `#d7dde6`, danger `#b42318`, and focus `#ff595a`. + +Code Connect component inventory cannot currently be treated as executable evidence. The connected Figma account reports a Full seat on a Pro team, while the Code Connect API requires a Dev or Full seat on an Organization or Enterprise plan. Until that entitlement changes, the repository records the limitation and does not fabricate mappings. Storybook-to-runtime parity remains independently executable. + +## Security and operability boundary + +Storybook packages are development dependencies only. The dedicated pull-request workflow checks out the exact PR head, verifies the checked-out SHA, generates an installation lock for that run, installs without lifecycle scripts, installs only the Chromium browser required for the test provider, builds Storybook, and executes the browser tests. No model credential, repository write token, application secret, customer fixture, or runtime deployment authority is required. + +A checked-in npm lockfile is still preferred before this Draft becomes integration-ready so transitive development dependencies are reproducible between runs. A successful Storybook run is design evidence only; it does not replace Maven verification, security scans, protected-branch requirements, review-thread resolution, or qualifying independent approval. + +## References + +Design Tokens Community Group. (2025, October 28). *Design Tokens Format Module 2025.10*. https://www.designtokens.org/TR/2025.10/format/ + +Design Tokens Community Group. (2025, October 28). *Design Tokens Color Module 2025.10*. https://www.designtokens.org/TR/2025.10/color/ + +Storybook. (2026). *Accessibility tests*. https://storybook.js.org/docs/writing-tests/accessibility-testing + +Storybook. (2026). *Testing in CI*. https://storybook.js.org/docs/writing-tests/in-ci + +Storybook. (2026). *Vitest addon*. https://storybook.js.org/docs/writing-tests/integrations/vitest-addon + +Storybook. (2026). *Storybook for Web Components with Vite*. https://storybook.js.org/docs/get-started/frameworks/web-components-vite + +Vitest. (2026). *Browser mode*. https://vitest.dev/guide/browser/ diff --git a/package.json b/package.json new file mode 100644 index 000000000..22cbba439 --- /dev/null +++ b/package.json @@ -0,0 +1,21 @@ +{ + "name": "clearfolio-design-authority", + "private": true, + "type": "module", + "scripts": { + "storybook": "storybook dev -p 6006 --no-open", + "build-storybook": "storybook build --output-dir storybook-static", + "test-storybook": "vitest --project=storybook" + }, + "devDependencies": { + "@storybook/addon-a11y": "10.5.7", + "@storybook/addon-vitest": "10.5.7", + "@storybook/web-components-vite": "10.5.7", + "@vitest/browser-playwright": "4.1.10", + "storybook": "10.5.7", + "vitest": "4.1.10" + }, + "engines": { + "node": ">=22" + } +} diff --git a/scripts/test_storybook_design_system.py b/scripts/test_storybook_design_system.py index b135ab819..9c948af49 100644 --- a/scripts/test_storybook_design_system.py +++ b/scripts/test_storybook_design_system.py @@ -10,7 +10,10 @@ STORY = ROOT / "design/storybook/viewer-states.stories.js" MAIN = ROOT / ".storybook/main.js" PREVIEW = ROOT / ".storybook/preview.js" +VITEST_SETUP = ROOT / ".storybook/vitest.setup.js" +VITEST_CONFIG = ROOT / "vitest.config.js" PACKAGE = ROOT / "package.json" +WORKFLOW = ROOT / ".github/workflows/storybook.yml" REQUIRED_STATES = { "Loading", @@ -41,14 +44,29 @@ def _runtime_tokens() -> dict[str, str]: root = re.search(r":root\s*\{(?P.*?)\n\}", css, re.DOTALL) assert root is not None return { - name: value.strip() + name: value.strip().lower() for name, value in re.findall(r"--([a-z0-9-]+)\s*:\s*([^;]+);", root.group("body")) if name in REQUIRED_TOKENS } +def _hex_from_srgb(components: list[float]) -> str: + channels = [round(component * 255) for component in components] + assert all(0 <= channel <= 255 for channel in channels) + return "#" + "".join(f"{channel:02x}" for channel in channels) + + def test_storybook_authority_files_exist() -> None: - for path in (TOKENS, STORY, MAIN, PREVIEW, PACKAGE): + for path in ( + TOKENS, + STORY, + MAIN, + PREVIEW, + VITEST_SETUP, + VITEST_CONFIG, + PACKAGE, + WORKFLOW, + ): assert path.is_file(), f"missing executable design-system authority: {path.relative_to(ROOT)}" @@ -57,9 +75,14 @@ def test_dtcg_projection_matches_runtime_css() -> None: colors = payload["color"] runtime = _runtime_tokens() assert set(runtime) == REQUIRED_TOKENS - projected = {name: value["$value"] for name, value in colors.items()} - assert projected == runtime - assert all(value["$type"] == "color" for value in colors.values()) + assert set(colors) == REQUIRED_TOKENS + for name, token in colors.items(): + assert token["$type"] == "color" + value = token["$value"] + assert value["colorSpace"] == "srgb" + assert value["alpha"] == 1 + assert _hex_from_srgb(value["components"]) == value["hex"].lower() + assert value["hex"].lower() == runtime[name] def test_required_buyer_states_are_named_and_a11y_blocking() -> None: @@ -71,17 +94,36 @@ def test_required_buyer_states_are_named_and_a11y_blocking() -> None: assert "wcag22aa" in preview assert "prefers-reduced-motion" in story assert "forced-colors" in story + assert "viewer.css" in preview + + +def test_storybook_uses_current_browser_test_path() -> None: + setup = VITEST_SETUP.read_text(encoding="utf-8") + config = VITEST_CONFIG.read_text(encoding="utf-8") + assert "@storybook/addon-a11y/preview" in setup + assert "setProjectAnnotations" in setup + assert "@storybook/addon-vitest/vitest-plugin" in config + assert "@vitest/browser-playwright" in config + assert "chromium" in config + assert "headless: true" in config def test_storybook_is_development_only_and_has_build_test_commands() -> None: package = json.loads(PACKAGE.read_text(encoding="utf-8")) assert "dependencies" not in package or not package["dependencies"] dev = package["devDependencies"] - assert dev["storybook"].startswith("10.5") - assert dev["@storybook/web-components-vite"].startswith("10.5") + for dependency in ( + "storybook", + "@storybook/addon-a11y", + "@storybook/addon-vitest", + "@storybook/web-components-vite", + ): + assert dev[dependency].startswith("10.5") + assert dev["vitest"].startswith("4.1") + assert dev["@vitest/browser-playwright"].startswith("4.1") scripts = package["scripts"] assert "storybook build" in scripts["build-storybook"] - assert "test-storybook" in scripts["test-storybook"] + assert "vitest" in scripts["test-storybook"] def test_story_fixtures_exclude_customer_authority() -> None: @@ -96,3 +138,12 @@ def test_story_fixtures_exclude_customer_authority() -> None: ) assert not any(term in story for term in forbidden) assert "00000000-0000-0000-0000-000000000000" in story + + +def test_storybook_workflow_binds_to_exact_pr_head() -> None: + workflow = WORKFLOW.read_text(encoding="utf-8") + assert "github.event.pull_request.head.sha" in workflow + assert "git rev-parse HEAD" in workflow + assert "npm run build-storybook" in workflow + assert "npm run test-storybook -- --run" in workflow + assert "playwright install --with-deps chromium" in workflow diff --git a/vitest.config.js b/vitest.config.js new file mode 100644 index 000000000..bdebde333 --- /dev/null +++ b/vitest.config.js @@ -0,0 +1,33 @@ +import { storybookTest } from '@storybook/addon-vitest/vitest-plugin'; +import { playwright } from '@vitest/browser-playwright'; +import path from 'node:path'; +import { fileURLToPath } from 'node:url'; +import { defineConfig } from 'vitest/config'; + +const dirname = path.dirname(fileURLToPath(import.meta.url)); + +export default defineConfig({ + test: { + projects: [ + { + extends: true, + plugins: [ + storybookTest({ + configDir: path.join(dirname, '.storybook'), + storybookScript: 'npm run storybook -- --no-open', + }), + ], + test: { + name: 'storybook', + browser: { + enabled: true, + provider: playwright({}), + headless: true, + instances: [{ browser: 'chromium' }], + }, + setupFiles: ['./.storybook/vitest.setup.js'], + }, + }, + ], + }, +}); From 82c02525fe9731b9c93776b881c7830a5737737d Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 20 Aug 2026 11:20:46 -0700 Subject: [PATCH 03/31] test(design): require Storybook 10 responsive globals --- scripts/test_storybook_design_system.py | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/scripts/test_storybook_design_system.py b/scripts/test_storybook_design_system.py index 9c948af49..46927b68d 100644 --- a/scripts/test_storybook_design_system.py +++ b/scripts/test_storybook_design_system.py @@ -97,6 +97,21 @@ def test_required_buyer_states_are_named_and_a11y_blocking() -> None: assert "viewer.css" in preview +def test_responsive_stories_use_storybook_10_viewport_globals() -> None: + preview = PREVIEW.read_text(encoding="utf-8") + story = STORY.read_text(encoding="utf-8") + assert "viewport:" in preview + assert "clearfolioMobile" in preview + assert "width: '390px'" in preview + assert "clearfolioTablet" in preview + assert "width: '768px'" in preview + assert "viewport: { value: 'clearfolioMobile', isRotated: false }" in story + assert "viewport: { value: 'clearfolioTablet', isRotated: false }" in story + assert "defaultViewport" not in story + assert "width: '390px'" not in story + assert "width: '768px'" not in story + + def test_storybook_uses_current_browser_test_path() -> None: setup = VITEST_SETUP.read_text(encoding="utf-8") config = VITEST_CONFIG.read_text(encoding="utf-8") From b461ef70f2fe5a66821229f4a92911a292200287 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 20 Aug 2026 11:22:09 -0700 Subject: [PATCH 04/31] test(design): align responsive contract with Storybook 10 --- scripts/test_storybook_design_system.py | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/scripts/test_storybook_design_system.py b/scripts/test_storybook_design_system.py index 46927b68d..63fcde6d7 100644 --- a/scripts/test_storybook_design_system.py +++ b/scripts/test_storybook_design_system.py @@ -100,13 +100,12 @@ def test_required_buyer_states_are_named_and_a11y_blocking() -> None: def test_responsive_stories_use_storybook_10_viewport_globals() -> None: preview = PREVIEW.read_text(encoding="utf-8") story = STORY.read_text(encoding="utf-8") - assert "viewport:" in preview - assert "clearfolioMobile" in preview - assert "width: '390px'" in preview - assert "clearfolioTablet" in preview - assert "width: '768px'" in preview - assert "viewport: { value: 'clearfolioMobile', isRotated: false }" in story - assert "viewport: { value: 'clearfolioTablet', isRotated: false }" in story + assert "MINIMAL_VIEWPORTS" in preview + assert "storybook/viewport" in preview + assert "viewport: {" in preview + assert "options: MINIMAL_VIEWPORTS" in preview + assert "viewport: { value: 'mobile1', isRotated: false }" in story + assert "viewport: { value: 'tablet', isRotated: false }" in story assert "defaultViewport" not in story assert "width: '390px'" not in story assert "width: '768px'" not in story From 7d037e394b985ed65953f1a49813db444649beec Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 20 Aug 2026 11:22:26 -0700 Subject: [PATCH 05/31] fix(design): configure Storybook 10 viewports --- .storybook/preview.js | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/.storybook/preview.js b/.storybook/preview.js index ff8c849e8..40ab3bf13 100644 --- a/.storybook/preview.js +++ b/.storybook/preview.js @@ -1,9 +1,14 @@ +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: { From f59cd6f4f9c2fdeeca902cfb47c40c73df49c65d Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 20 Aug 2026 14:02:07 -0700 Subject: [PATCH 06/31] test(design): pin viewer recovery action semantics --- design/storybook/viewer-states.stories.js | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/design/storybook/viewer-states.stories.js b/design/storybook/viewer-states.stories.js index 3cdf9a5d6..f8b826685 100644 --- a/design/storybook/viewer-states.stories.js +++ b/design/storybook/viewer-states.stories.js @@ -144,10 +144,24 @@ export const 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 = { @@ -171,7 +185,7 @@ export const BusyDisabled = { 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' })).toBeDisabled(); + await expect(canvas.getByRole('button', { name: 'Back to documents' })).toBeEnabled(); }, }; From 6fb2429cece6807688d8083c86e3babf37290755 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 20 Aug 2026 14:03:39 -0700 Subject: [PATCH 07/31] fix(design): keep viewer recovery actions truthful --- design/storybook/viewer-states.stories.js | 34 +++++++++++++++-------- 1 file changed, 23 insertions(+), 11 deletions(-) diff --git a/design/storybook/viewer-states.stories.js b/design/storybook/viewer-states.stories.js index f8b826685..fd07f7160 100644 --- a/design/storybook/viewer-states.stories.js +++ b/design/storybook/viewer-states.stories.js @@ -76,11 +76,10 @@ const HARNESS_STYLES = ` `; -function renderViewer({ state = 'ready', width = '100%', busy = false } = {}) { +function renderViewer({ state = 'ready', busy = false } = {}) { const copy = STATE_COPY[state]; const root = document.createElement('div'); root.className = 'storybook-viewer-frame'; - root.style.maxWidth = width; const error = copy.errorTitle ? `
${copy.status}
${error} -
${skeleton}
+
${skeleton}
${downloadAction} From bbbe0979107b5ac1942b6d0f37de4579655ac09e Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sun, 23 Aug 2026 19:00:28 -0700 Subject: [PATCH 28/31] docs(a11y): trace named region semantics to WAI-ARIA 1.2 --- docs/design-system-authority.md | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/docs/design-system-authority.md b/docs/design-system-authority.md index 6195a1fe4..fb482a9aa 100644 --- a/docs/design-system-authority.md +++ b/docs/design-system-authority.md @@ -25,9 +25,11 @@ Every error state gives a concrete next action. Loading copy explains that no ac Project-level Storybook accessibility configuration sets `a11y.test` to `error` and runs WCAG A/AA tags through WCAG 2.2 AA plus axe best-practice checks. The Storybook Vitest integration executes smoke and `play` interaction tests in headless Chromium through Vitest Browser Mode and Playwright. +The named preview wrapper uses `role="region"` with `aria-labelledby="preview-title"`. WAI-ARIA 1.2 defines the implicit `generic` role of a plain HTML `div` as name-prohibited, so leaving `aria-labelledby` on a generic container would be invalid even if an automated browser scan did not surface it. The semantic region keeps the visible `Document preview` heading as the accessible name without inventing parallel copy. + ## Figma traceability -Fresh evidence was re-read on 2026-08-23 from Clearfolio Figma file `UPIMvnjyP1sTXhIy1wqD02`, page `0:1`, board `2:2` (`Clearfolio Viewer Product Design + Analytics Board`). The board identifies itself as a no-Code-Connect design plan and contains desktop loading/ready/failed, mobile loading, and tablet concepts. Its local swatches match protected-main runtime values: brand `#034ea2`, ink `#102032`, muted `#5a6777`, background `#f6f7f9`, panel `#ffffff`, line `#d7dde6`, danger `#b42318`, and focus `#ff595a`. +Fresh evidence was re-read on 2026-08-24 from Clearfolio Figma file `UPIMvnjyP1sTXhIy1wqD02`, page `0:1`, board `2:2` (`Clearfolio Viewer Product Design + Analytics Board`). The board contains desktop loading/ready/failed, mobile loading, tablet concepts, and a `Storybook buyer-state parity` frame (`10:2`) with document-not-found, invalid-document, and network-error states. Its local swatches match protected-main runtime values: brand `#034ea2`, ink `#102032`, muted `#5a6777`, background `#f6f7f9`, panel `#ffffff`, line `#d7dde6`, danger `#b42318`, and focus `#ff595a`. Code Connect component inventory cannot currently be treated as executable evidence. The connected Figma account reports a Full seat on a Pro team, while the Code Connect API requires a Dev or Full seat on an Organization or Enterprise plan. Until that entitlement changes, the repository records the limitation and does not fabricate mappings. Storybook-to-runtime parity remains independently executable. @@ -52,3 +54,5 @@ Storybook. (2026). *Vitest addon*. https://storybook.js.org/docs/writing-tests/i Storybook. (2026). *Storybook for Web Components with Vite*. https://storybook.js.org/docs/get-started/frameworks/web-components-vite Vitest. (2026). *Browser mode*. https://vitest.dev/guide/browser/ + +World Wide Web Consortium. (2023). *Accessible Rich Internet Applications (WAI-ARIA) 1.2*. https://www.w3.org/TR/wai-aria-1.2/ From d38ad4f0a97ce470a0f443b2ab38b8064d3ded1a Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 27 Aug 2026 18:00:18 -0700 Subject: [PATCH 29/31] test(design): require loading preview guidance parity --- design/storybook/viewer-states.stories.js | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/design/storybook/viewer-states.stories.js b/design/storybook/viewer-states.stories.js index 1dcb2314b..33b5991ce 100644 --- a/design/storybook/viewer-states.stories.js +++ b/design/storybook/viewer-states.stories.js @@ -141,6 +141,10 @@ 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 = { From dcfb79ec4b0bc7be50b2b6efe309f9f6c3533a7f Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 27 Aug 2026 18:01:01 -0700 Subject: [PATCH 30/31] fix(design): keep loading preview guidance visible --- design/storybook/viewer-states.stories.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/design/storybook/viewer-states.stories.js b/design/storybook/viewer-states.stories.js index 33b5991ce..45afdd165 100644 --- a/design/storybook/viewer-states.stories.js +++ b/design/storybook/viewer-states.stories.js @@ -88,7 +88,8 @@ function renderViewer({ state = 'ready', busy = false } = {}) {
` : ''; const skeleton = state === 'loading' - ? '' + ? ` +

${copy.preview}

` : `
${copy.preview}
`; const unrecoverableReference = state === 'notFound' || state === 'invalid'; const primaryLabel = state === 'ready' From 62a53f84f91c968b91e2c7bed4bce1c4cef72fac Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 27 Aug 2026 18:02:25 -0700 Subject: [PATCH 31/31] docs(design): trace loading guidance parity --- docs/design-system-authority.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/design-system-authority.md b/docs/design-system-authority.md index fb482a9aa..1aee513e9 100644 --- a/docs/design-system-authority.md +++ b/docs/design-system-authority.md @@ -21,7 +21,7 @@ Clearfolio keeps the shipped WebFlux/static JavaScript/CSS viewer as the product - tablet-width ready state; - reduced-motion and forced-colors alternatives. -Every error state gives a concrete next action. Loading copy explains that no action is required yet. The stories use only a fixed all-zero document reference and make no network requests; they contain no customer data, tenant authority, signed links, or credentials. +Every error state gives a concrete next action. Loading copy explains that no action is required yet and keeps the preview guidance visible beside the skeleton so buyers know what will happen next. The stories use only a fixed all-zero document reference and make no network requests; they contain no customer data, tenant authority, signed links, or credentials. Project-level Storybook accessibility configuration sets `a11y.test` to `error` and runs WCAG A/AA tags through WCAG 2.2 AA plus axe best-practice checks. The Storybook Vitest integration executes smoke and `play` interaction tests in headless Chromium through Vitest Browser Mode and Playwright. @@ -29,7 +29,7 @@ The named preview wrapper uses `role="region"` with `aria-labelledby="preview-ti ## Figma traceability -Fresh evidence was re-read on 2026-08-24 from Clearfolio Figma file `UPIMvnjyP1sTXhIy1wqD02`, page `0:1`, board `2:2` (`Clearfolio Viewer Product Design + Analytics Board`). The board contains desktop loading/ready/failed, mobile loading, tablet concepts, and a `Storybook buyer-state parity` frame (`10:2`) with document-not-found, invalid-document, and network-error states. Its local swatches match protected-main runtime values: brand `#034ea2`, ink `#102032`, muted `#5a6777`, background `#f6f7f9`, panel `#ffffff`, line `#d7dde6`, danger `#b42318`, and focus `#ff595a`. +Fresh evidence was re-read on 2026-08-28 from Clearfolio Figma file `UPIMvnjyP1sTXhIy1wqD02`, page `0:1`, board `2:2` (`Clearfolio Viewer Product Design + Analytics Board`). The board contains desktop loading/ready/failed, mobile loading, tablet concepts, and a `Storybook buyer-state parity` frame (`10:2`) with document-not-found, invalid-document, and network-error states. The desktop and mobile loading frames keep the same guidance text now rendered by the executable Storybook loading state: `The document preview will appear here when preparation finishes.` Its local swatches match protected-main runtime values: brand `#034ea2`, ink `#102032`, muted `#5a6777`, background `#f6f7f9`, panel `#ffffff`, line `#d7dde6`, danger `#b42318`, and focus `#ff595a`. Code Connect component inventory cannot currently be treated as executable evidence. The connected Figma account reports a Full seat on a Pro team, while the Code Connect API requires a Dev or Full seat on an Organization or Enterprise plan. Until that entitlement changes, the repository records the limitation and does not fabricate mappings. Storybook-to-runtime parity remains independently executable.