Repository navigation
feat(design): establish executable Storybook viewer authority #489
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Draft
seonghobae
wants to merge
31
commits into
main
Choose a base branch
from
feat/storybook-design-authority
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Draft
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 20a04a3
feat(design): add executable Storybook viewer authority
seonghobae 82c0252
test(design): require Storybook 10 responsive globals
seonghobae b461ef7
test(design): align responsive contract with Storybook 10
seonghobae 7d037e3
fix(design): configure Storybook 10 viewports
seonghobae f59cd6f
test(design): pin viewer recovery action semantics
seonghobae 6fb2429
fix(design): keep viewer recovery actions truthful
seonghobae 2a4c83f
test(design): require reproducible Storybook install
seonghobae b27cc1e
chore(design): expose generated lock for review bootstrap
seonghobae d33e5df
chore(design): publish lock bootstrap artifact
seonghobae a6b8ebe
test(design): make Storybook contracts unittest-discoverable
seonghobae d14a8cb
test(design): enforce Storybook contract discovery
seonghobae 20031c8
chore(design): commit reproducible Storybook lock
seonghobae 5a8b7f4
test(design): remove invalid Storybook lock for clean resolution
seonghobae cdbd176
chore(design): bootstrap reviewed Storybook lock atomically
seonghobae 4c76837
chore(design): refresh reviewed Storybook lock artifact
seonghobae 62e5e0a
fix(design): stream Storybook lock bootstrap safely
seonghobae e816e1d
fix(design): bootstrap reviewed lock with guarded push
seonghobae 82044d3
fix(design): lock Storybook dependency graph
github-actions[bot] 7724d3d
fix(design): lock Storybook workflow to reviewed dependencies
seonghobae 4937133
test(design): reject out-of-range sRGB token components
seonghobae bfccb02
fix(design): validate sRGB components before rounding
seonghobae 2ea2bc5
docs(governance): register Storybook design gate
seonghobae db23649
docs(design): align Storybook lock authority
seonghobae b397ad7
docs: align repo-local Storybook workflow guidance
seonghobae 4dead75
test(a11y): require semantic nameable preview region
seonghobae accbe89
fix(a11y): give named preview a semantic region role
seonghobae bbbe097
docs(a11y): trace named region semantics to WAI-ARIA 1.2
seonghobae d38ad4f
test(design): require loading preview guidance parity
seonghobae dcfb79e
fix(design): keep loading preview guidance visible
seonghobae 62a53f8
docs(design): trace loading guidance parity
seonghobae File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Some comments aren't visible on the classic Files Changed page.
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,43 @@ | ||
| name: Storybook Design System | ||
|
|
||
| on: | ||
| pull_request: {} | ||
|
|
||
| permissions: | ||
| contents: read | ||
|
|
||
| jobs: | ||
| storybook: | ||
| name: Storybook browser acceptance | ||
| runs-on: ubuntu-latest | ||
|
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 | ||
|
seonghobae marked this conversation as resolved.
Resolved
|
||
|
|
||
| - name: Install Chromium for Vitest browser mode | ||
| run: npx --no-install playwright install --with-deps chromium | ||
|
seonghobae marked this conversation as resolved.
Resolved
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 | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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; |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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'], | ||
| }, | ||
| }, | ||
|
seonghobae marked this conversation as resolved.
Resolved
|
||
| }, | ||
| }; | ||
|
|
||
| export default preview; | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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]); |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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; | ||
|
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 }, | ||
| }, | ||
|
seonghobae marked this conversation as resolved.
Resolved
|
||
| }; | ||
|
|
||
| export const TabletReady = { | ||
| args: { state: 'ready' }, | ||
| globals: { | ||
| viewport: { value: 'tablet', isRotated: false }, | ||
| }, | ||
| }; | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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"} | ||
| } | ||
| } | ||
| } |
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.