From c9f99c8146ccc8388e15d22a24a553eb1aeea990 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sun, 16 Aug 2026 15:31:32 +0000 Subject: [PATCH 01/67] feat(ui): publish editor chrome tokens and Storybook inventory Give hosts a typed catalog of the repeating --cwl-* chrome tokens, a DTCG 2025.10 interchange snapshot, and Storybook stories for toolbar button states so brand theming happens on .cwl-editor after a WCAG 2.2 contrast check. Co-authored-by: Seongho Bae --- .gitignore | 3 + .storybook/main.ts | 9 + .storybook/preview.ts | 11 + ARCHITECTURE.md | 2 + CHANGELOG.md | 3 + README.md | 15 + docs/CONTRACTS.md | 1 + docs/DOCUMENTATION_FITNESS.md | 2 + docs/PRD.md | 2 + docs/README.md | 1 + docs/TRACEABILITY.md | 5 + .../0027-editor-design-tokens-storybook.md | 42 + docs/adr/README.md | 1 + docs/design-tokens.md | 38 + docs/doctoring/editor-design-tokens.md | 47 + docs/storybook-inventory.md | 12 + package.json | 6 + pnpm-lock.yaml | 1070 ++++++++++++++++- src/designTokenDocumentation.test.ts | 36 + src/designTokens.test.ts | 73 ++ src/designTokens.ts | 173 +++ src/index.test.ts | 7 + src/index.ts | 15 + stories/EditorChrome.stories.tsx | 62 + 24 files changed, 1633 insertions(+), 3 deletions(-) create mode 100644 .storybook/main.ts create mode 100644 .storybook/preview.ts create mode 100644 docs/adr/0027-editor-design-tokens-storybook.md create mode 100644 docs/design-tokens.md create mode 100644 docs/doctoring/editor-design-tokens.md create mode 100644 docs/storybook-inventory.md create mode 100644 src/designTokenDocumentation.test.ts create mode 100644 src/designTokens.test.ts create mode 100644 src/designTokens.ts create mode 100644 stories/EditorChrome.stories.tsx diff --git a/.gitignore b/.gitignore index 2f74949f..56fca8cb 100644 --- a/.gitignore +++ b/.gitignore @@ -23,3 +23,6 @@ htmlcov/ # CodeGraph local index .codegraph/ + +# Storybook local build output +storybook-static/ diff --git a/.storybook/main.ts b/.storybook/main.ts new file mode 100644 index 00000000..34c6c5fa --- /dev/null +++ b/.storybook/main.ts @@ -0,0 +1,9 @@ +import type { StorybookConfig } from '@storybook/react-vite'; + +const config: StorybookConfig = { + framework: '@storybook/react-vite', + stories: ['../stories/**/*.stories.@(ts|tsx)'], + addons: ['@storybook/addon-docs'], +}; + +export default config; diff --git a/.storybook/preview.ts b/.storybook/preview.ts new file mode 100644 index 00000000..3470a462 --- /dev/null +++ b/.storybook/preview.ts @@ -0,0 +1,11 @@ +import type { Preview } from '@storybook/react'; + +import '../src/styles.css'; + +const preview: Preview = { + parameters: { + controls: { disable: true }, + }, +}; + +export default preview; diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md index a42dd5a5..5c9e6900 100644 --- a/ARCHITECTURE.md +++ b/ARCHITECTURE.md @@ -25,6 +25,8 @@ The protected standalone product provides: - dependency-locked Chromium/Firefox/WebKit rich-clipboard release assurance; and - a network-free Office renderer for deterministic DOCX, XLSX, and PPTX output. +A named editor-chrome theme-token catalog and Storybook inventory for repeating toolbar/editor objects are Active PR / Proposed. Hosts override `--cwl-*` on `.cwl-editor`; Inkspan does not own Figma Variables, brand certification, or design-tool sync. + Hosts own transport, authorization, tenant isolation, persistence, credentials, migration, retention, and model-use policy. They also own authentication, deployment, durable audit, print destination policy, and any durable PDF/print-service authority; persistence includes durable storage and commit authority. Inkspan therefore never opens a production collaboration connection, chooses a tenant, stores a provider secret, creates a durable database transaction, decides a retention schedule, authorizes an AI operation, or claims that a browser print destination constitutes a durable authorized export. A standalone adopter can provide those capabilities directly; a CWL host can provide them through shared platform services. diff --git a/CHANGELOG.md b/CHANGELOG.md index f5d1d3dc..33d83d26 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,9 @@ Historical release entries from **0.1.0 through 0.5.27** are preserved verbatim ## [Unreleased] +### Added +- Named the repeating editor chrome as a host-facing theme-token catalog and Storybook inventory so hosts can override `--cwl-*` custom properties on `.cwl-editor` after checking WCAG 2.2 contrast, without editing Inkspan internals. + ## [0.6.0] — 2026-08-10 ### Release diff --git a/README.md b/README.md index f2b02332..67d108fb 100644 --- a/README.md +++ b/README.md @@ -119,6 +119,21 @@ export function Example() { Set `mode="html"` when `value` and `onChange` should exchange HTML. Both modes embed accepted images as inline data URIs. +### Theme the repeating chrome + +Override the named `--cwl-*` custom properties on `.cwl-editor` after checking +WCAG 2.2 contrast. Do not edit Inkspan internals. See +[`docs/design-tokens.md`](docs/design-tokens.md) for the token catalog and +[`docs/storybook-inventory.md`](docs/storybook-inventory.md) for the Storybook +preview of toolbar and editor chrome. + +```css +.cwl-editor { + --cwl-accent: #0b6e4f; + --cwl-accent-soft: #d8f3e8; +} +``` + ### Server rendering `CwlEditor` and `CollaborativeCwlEditor` are safe to include in server-rendered diff --git a/docs/CONTRACTS.md b/docs/CONTRACTS.md index 52ab1bdf..8b79aea9 100644 --- a/docs/CONTRACTS.md +++ b/docs/CONTRACTS.md @@ -152,6 +152,7 @@ Rollback must preserve readable canonical documents and must not require silentl | autosave | local ordering/state, callback contract, validator validation | transport, durable CAS, retry/offline policy, persistence | | collaboration | provider-neutral editor/Yjs binding | provider lifecycle, rooms, identity, authorization, persistence, awareness privacy | | Office rendering | deterministic bounded JSON→artifact conversion | file destination policy, downstream distribution, tenant authorization | +| editor chrome theming (Active PR / Proposed) | named `--cwl-*` tokens, DTCG interchange snapshot, Storybook inventory | host brand CSS, contrast certification, Figma Variables, design-tool sync | | naruon composition | stable local package/module boundary | authenticated compose transport, tenancy, provider/model policy | | model assistance | deterministic proposal acceptance boundary | provider, prompt/data policy, credentials, human approval | | release evidence | exact three-file draft inventory, package/artifact/digest verification and repository evidence | downstream deployment and operational rollout | diff --git a/docs/DOCUMENTATION_FITNESS.md b/docs/DOCUMENTATION_FITNESS.md index cdb52e18..38d37131 100644 --- a/docs/DOCUMENTATION_FITNESS.md +++ b/docs/DOCUMENTATION_FITNESS.md @@ -56,6 +56,7 @@ Document fitness and implementation maturity are independent. A `present_current | Unified stable registry release train | ADR 0019, protected release workflow and release doctoring | `present_current` | `implemented_on_protected_main` | Stable npm/Office version equality, OIDC Trusted Publishing, exact-artifact publication and post-publication digest verification are source-integrated; live registry publication remains separate operational evidence. | | Framework-neutral Markdown package boundary | ADR 0020, protected `@contextualwisdomlab/cwl-editor/markdown` package subpath and shared policy modules from #114 | `present_current` | `implemented_on_protected_main` | Server/worker consumers can reuse deterministic Markdown/HTML/email/plain-text conversion without evaluating the React/TipTap editor graph. | | CSS paged-media print boundary | ADR 0021, protected `src/styles.css`, packaged stylesheet evidence and real-browser print tests from #116/#127 | `present_current` | `implemented_on_protected_main` | Declarative print output removes screen-only clipping/chrome while preserving authored content without creating a durable PDF service. | +| Editor chrome design tokens / Storybook inventory | ADR 0027, `docs/design-tokens.md`, doctoring, token catalog, and Storybook stories | `present_current` | `implemented_on_active_pr` | Hosts can name, override, and preview repeating toolbar/editor tokens without treating the interchange snapshot as shipped protected-main authority. | | Informative DOCX PNG figures | ADR 0022, Office schema/renderer/tests and guidance | `present_current` | `implemented_on_protected_main` | Strict bounded inline PNG figures preserve informative alternative text without remote-resource or arbitrary OOXML authority. | | DOCX bounded rich-text runs | ADR 0023, Office schema/renderer/tests and doctoring | `present_current` | `implemented_on_protected_main` | Ordered bold/italic/underline runs preserve common inline fidelity under one bounded deterministic contract. | | DOCX bounded paragraph alignment | ADR 0024, Office schema/renderer/tests, Office guidance and doctoring | `present_current` | `implemented_on_protected_main` | `paragraph` and `rich_paragraph` preserve explicit left/center/right/justify alignment while omission retains inherited/default behavior. | @@ -82,6 +83,7 @@ The documentation pack is substantially complete for acquisition review, but rep 1. The protected manifests now agree at `0.6.0`, while registry operational acceptance remains open under issue #118 because the exact protected release still needs its tag/GitHub Release, live npm/PyPI Trusted Publisher execution, and public artifact digest verification. 2. Future protected-source changes must continue to reconcile PRD/TRD/Architecture/ADR/UML/DATA_MODEL/security/test/operability/traceability semantics rather than treating this baseline as permanently complete. 3. Documentation becoming mergeable, green, or protected-merged is never a reason for the commercial loop to stop; the next safe product, release, security, accessibility, package, Office-fidelity, or interoperability lane continues. +4. Hosts still need a named, Storybook-previewable chrome-token catalog so brand theming does not require editing Inkspan internals; that lane is Active PR / Proposed and must not be described as shipped until protected integration. ## Sufficiency decision diff --git a/docs/PRD.md b/docs/PRD.md index 899086f3..62cbe523 100644 --- a/docs/PRD.md +++ b/docs/PRD.md @@ -146,3 +146,5 @@ Shareable acquisition evidence excludes production tenant content and credential Protected `main` is the sole implemented baseline. Open PRs may describe Proposed or Active work but are not shipped contracts until protected integration. Canonical documentation must state when a requirement is target architecture rather than current implementation. SafeClipboard, real Chromium/Firefox/WebKit release assurance, lifecycle observation, the root security disclosure lifecycle, toolbar shortcut accessibility metadata, SSR/native-form serialization, revision-scoped selection evidence, W3C text-position selector evidence, document-transition evidence, and envelope identity migration routing are implemented on protected `main`. + +A named editor-chrome theme-token catalog, DTCG 2025.10 interchange snapshot, and Storybook inventory for repeating toolbar/editor objects are Active PR / Proposed and are not shipped claims until protected integration. diff --git a/docs/README.md b/docs/README.md index 2b617f7c..3f9c3270 100644 --- a/docs/README.md +++ b/docs/README.md @@ -18,6 +18,7 @@ This directory is the discoverable index for Inkspan's product, technical, secur | [`package-distribution.md`](package-distribution.md) | Buyer-facing public npm package entrypoints, packaged contents, runtime dependency boundaries, and consumer verification | | [`email-output.md`](email-output.md) | Deterministic email fragment/full-document authority, language/direction metadata, accessibility and host-owned transport boundary | | [`print-output.md`](print-output.md) | Browser print/paged-media presentation, accessibility/fidelity limits, host-owned governed-export boundary, and rollback | +| [`design-tokens.md`](design-tokens.md) | Host-facing editor chrome tokens, DTCG 2025.10 interchange snapshot, and Storybook inventory (Active PR / Proposed) | | [`UML.md`](UML.md) | Component, sequence, state and authority-flow diagrams | | [`DATA_MODEL.md`](DATA_MODEL.md) | Conceptual evidence/domain model and persistence ownership | | [`THREAT_MODEL.md`](THREAT_MODEL.md) | Trust boundaries, abuse cases, security/privacy controls and residual risks | diff --git a/docs/TRACEABILITY.md b/docs/TRACEABILITY.md index d120cec7..1c830e7c 100644 --- a/docs/TRACEABILITY.md +++ b/docs/TRACEABILITY.md @@ -21,6 +21,7 @@ This record maps durable Inkspan product decisions to authoritative standards, p | Browser clipboard behavior | Security-relevant rich HTML handling requires actual paste-pipeline integration and bounded semantic reconstruction before editor state | WHATWG HTML parsing; W3C Clipboard API | protected-main rich-clipboard unit/integration corpus and SafeClipboard ADR | Protected jsdom/TipTap integration success is not universal browser-engine conformance | | Cross-engine release assurance | The same committed synthetic adversarial corpus runs under required Chromium, Firefox, and WebKit projects; exact package-lock and packed npm artifact SHA-256 digests are required, and only focused standards-grounded safe differences may be admitted | WHATWG HTML Living Standard; W3C Clipboard API and events; Playwright 1.62 release notes and browser/project documentation | ADR 0016, protected-main browser evidence source/workflows, TEST_STRATEGY, OPERABILITY and UML | Protected-main implementation is the release-policy authority; every release candidate must regenerate fresh exact-source/lock/run/browser evidence bound to the exact packed npm artifact SHA-256 and does not claim byte-identical browser serialization or branded enterprise-policy coverage | | CSS paged-media output | Shipped editor CSS has a declarative print boundary that removes interactive chrome and screen clipping while preserving authored document flow and bounded fragmentation behavior | W3C Media Queries Level 3; CSS Fragmentation Level 3; CSS Paged Media Level 3 as tracked draft input | protected-main #116 packaged stylesheet, real-browser print-media evidence, ADR 0021, print doctoring and tests | `implemented_on_protected_main`; browser print styling does not create a durable PDF service, page-number/header authority, persistence, signing, or PDF-conformance claim | +| Editor chrome design tokens | Repeating toolbar/editor surfaces share named `--cwl-*` custom properties; hosts override those properties on `.cwl-editor` and preview them in Storybook | Design Tokens Format Module 2025.10; WCAG 2.2 contrast; Storybook React/Vite | Active-PR doctoring `docs/doctoring/editor-design-tokens.md`, operator guide, ADR 0027, token catalog tests, and Storybook inventory | Proposed until protected `main`; the interchange snapshot is not complete DTCG conformance, Figma Variables sync, or a host WCAG certification | | Editor integration | Public behavior must exercise the actual TipTap/ProseMirror integration path, not an inert extension field or test-only hook | official TipTap and ProseMirror documentation for the locked dependency line | integration tests and package consumers | Inkspan does not claim compatibility with untested major-version integration semantics | | Collaboration | Inkspan provides provider-neutral editor/Yjs bindings; host owns provider lifecycle, room authorization, awareness privacy, persistence and audit | official Yjs/provider documentation plus Inkspan public contract | collaboration tests and architecture ownership matrix | No network-provider or tenant-authorization authority is implied | | Secure development | Security controls are developed test-first, with exact-head scanning/review/package evidence and root-cause regression | NIST SP 800-218 SSDF 1.1 | CI/security/SAST/package/provenance gates, doctoring and regression history | Repository evidence is not a claim of complete SSDF organizational conformance | @@ -38,6 +39,8 @@ Bray, T. (Ed.). (2015). *The I-JSON Message Format* (RFC 7493). RFC Editor. http Bray, T. (Ed.). (2017). *The JavaScript Object Notation (JSON) Data Interchange Format* (RFC 8259; STD 90). RFC Editor. https://doi.org/10.17487/RFC8259 +Design Tokens Community Group. (2025, October 28). *Design Tokens Format Module 2025.10* (Final Community Group Report). World Wide Web Consortium. https://www.w3.org/community/reports/design-tokens/CG-FINAL-format-20251028/ + Ecma International. (2021). *ECMA-376: Office Open XML file formats* (5th ed.). https://ecma-international.org/publications-and-standards/standards/ecma-376/ Ecma International. (2026). *ECMA-402: ECMAScript 2026 internationalization API specification* (13th ed.). https://402.ecma-international.org/ @@ -70,6 +73,8 @@ Rundgren, A., Jordan, B., & Erdtman, S. (2020). *JSON Canonicalization Scheme (J Souppaya, M., Scarfone, K., & Dodson, D. (2022). *Secure Software Development Framework (SSDF) Version 1.1: Recommendations for Mitigating the Risk of Software Vulnerabilities* (NIST SP 800-218). National Institute of Standards and Technology. https://doi.org/10.6028/NIST.SP.800-218 +Storybook. (n.d.). *Storybook for React with Vite*. Retrieved August 16, 2026, from https://storybook.js.org/docs/get-started/frameworks/react-vite + Web Hypertext Application Technology Working Group. (2026). *HTML Standard: Parsing HTML documents* (Living Standard). Retrieved August 10, 2026, from https://html.spec.whatwg.org/multipage/parsing.html World Wide Web Consortium. (2013). *PROV-DM: The PROV Data Model*. https://www.w3.org/TR/prov-dm/ diff --git a/docs/adr/0027-editor-design-tokens-storybook.md b/docs/adr/0027-editor-design-tokens-storybook.md new file mode 100644 index 00000000..00faaf76 --- /dev/null +++ b/docs/adr/0027-editor-design-tokens-storybook.md @@ -0,0 +1,42 @@ +# ADR 0027: Editor chrome design tokens and Storybook inventory + +Status: Proposed + +## Context + +Inkspan ships repeating toolbar buttons, groups, and editor chrome styled through `--cwl-*` custom properties. Hosts already re-theme by overriding those properties, but the names, light/dark/forced-colors values, and buyer next action lived only inside `src/styles.css`. There was no typed catalog, no Design Tokens Format Module interchange snapshot, and no Storybook inventory of the repeating objects. + +## Alternatives considered + +1. **Leave theming as undocumented CSS.** Rejected because hosts would keep reading internals and drift from the shipped token set. +2. **Generate CSS from a new token source of truth.** Rejected for this slice because it would rewrite the protected stylesheet and collide with print, forced-colors, and Office-unrelated presentation contracts. +3. **Add Figma Variables sync or a design-tool connector.** Rejected because Figma/design-tool credentials and sync authority are host-owned; Inkspan remains independently usable. +4. **Publish a named catalog plus Storybook inventory while CSS remains runtime authority.** Selected because hosts can override and preview repeating chrome without moving presentation authority or adding a design-tool runtime. + +## Decision + +Inkspan publishes a host-facing theme-token catalog for the nine shipped chrome tokens, a Design Tokens Format Module 2025.10 interchange snapshot, and a Storybook inventory of repeating toolbar/editor objects. Hosts override `--cwl-*` on `.cwl-editor` after checking WCAG 2.2 contrast. Unknown token names fail closed. No Figma, network, persistence, credential, or model authority is added. + +## Consequences and ownership trade-offs + +Hosts gain a copyable token list and a Storybook preview. Inkspan keeps CSS as the runtime source. naruon and other CWL hosts can apply the same overrides through host-owned brand CSS. Complete DTCG conformance, Figma Variables, and host WCAG certification remain out of scope. + +## Failure and recovery + +An unknown token name throws `EditorThemeTokenError` without reflecting caller input. A host override that fails contrast is recovered by changing only the named tokens, not by editing Inkspan internals or disabling forced-colors. + +## Security and privacy impact + +The catalog contains only public presentation values. It does not carry document bodies, tenant identifiers, credentials, or diagnostics. Storybook is a local development preview and is not a production transport. + +## Compatibility and migration + +The catalog is additive. Existing CSS overrides on `.cwl-editor` continue to work. A later CSS token addition must update the catalog, Storybook inventory, and this ADR together. + +## Verification and acceptance evidence + +Required evidence includes token-catalog tests against `src/styles.css`, documentation-contract tests, Storybook inventory stories for toolbar button states and token swatches, and exact-head CI/coverage/package/security gates on the unchanged head. This ADR stays Proposed until protected integration. + +## Rollback or supersession + +Rollback removes the catalog export, Storybook inventory/config, operator/doctoring records, and this ADR together. Supersession requires a new ADR if CSS ceases to be runtime presentation authority or if a design-tool sync contract is accepted. diff --git a/docs/adr/README.md b/docs/adr/README.md index df8b7b80..9dcf2787 100644 --- a/docs/adr/README.md +++ b/docs/adr/README.md @@ -32,6 +32,7 @@ This index records durable architectural decisions. Protected-main implementatio | [0024](0024-bounded-docx-paragraph-alignment.md) | Accepted | Bounded paragraph alignment in deterministic DOCX output | | [0025](0025-bounded-docx-heading-alignment.md) | Accepted | Bounded heading alignment in deterministic DOCX output | | [0026](0026-bounded-docx-external-hyperlinks.md) | Accepted | Bounded external hyperlinks in deterministic DOCX rich text | +| [0027](0027-editor-design-tokens-storybook.md) | Proposed | Editor chrome design tokens and Storybook inventory | ## Decision discipline diff --git a/docs/design-tokens.md b/docs/design-tokens.md new file mode 100644 index 00000000..67a734b3 --- /dev/null +++ b/docs/design-tokens.md @@ -0,0 +1,38 @@ +# Editor chrome design tokens + +Status: Active PR / Proposed + +Use this catalog when you need to re-theme Inkspan's repeating toolbar and editor chrome. Override the named custom properties on `.cwl-editor` after checking WCAG 2.2 contrast. Do not edit Inkspan internals. + +```css +.cwl-editor { + --cwl-accent: #0b6e4f; + --cwl-accent-soft: #d8f3e8; +} +``` + +```ts +import { + listEditorThemeTokens, + toDesignTokenFormatGroup, +} from '@contextualwisdomlab/cwl-editor'; + +const tokens = listEditorThemeTokens(); +const dtcgGroup = toDesignTokenFormatGroup(); +void tokens; +void dtcgGroup; +``` + +The stylesheet remains runtime presentation authority. `toDesignTokenFormatGroup()` is an interchange snapshot aligned to Design Tokens Format Module 2025.10. It is not complete DTCG conformance, Figma Variables sync, or a host WCAG certification. + +Preview the repeating objects in Storybook (`pnpm storybook`) using the inventory in [`storybook-inventory.md`](storybook-inventory.md). + +```mermaid +flowchart LR + Host[Host brand CSS] --> Editor[".cwl-editor custom properties"] + Catalog[listEditorThemeTokens] --> Host + Editor --> Toolbar[".cwl-toolbar / .cwl-tb-btn"] + Editor --> Surface[".cwl-editor document surface"] + Storybook[Storybook inventory] --> Toolbar + Storybook --> Surface +``` diff --git a/docs/doctoring/editor-design-tokens.md b/docs/doctoring/editor-design-tokens.md new file mode 100644 index 00000000..a4adfa73 --- /dev/null +++ b/docs/doctoring/editor-design-tokens.md @@ -0,0 +1,47 @@ +# Doctoring record: editor chrome design tokens + +**Date:** 2026-08-16 +**Status:** Active PR / Proposed +**Decision owner:** ContextualWisdomLab +**Scope:** Named `--cwl-*` theme tokens, DTCG 2025.10 interchange snapshot, and Storybook inventory for repeating toolbar/editor objects. + +## Buyer-visible gap + +Hosts embed Inkspan and need to match brand color, radius, and font without forking `src/styles.css`. The stylesheet already used `--cwl-*` custom properties, but buyers had no typed catalog, no interchange snapshot, and no Storybook inventory of the repeating toolbar button and editor chrome. Theme work therefore required reading CSS internals. + +If contrast fails after a re-theme, override only the named tokens on `.cwl-editor` and re-check WCAG 2.2 contrast against `--cwl-bg`. Do not disable forced-colors overrides. + +## Decision + +1. Keep `src/styles.css` as runtime presentation authority. +2. Publish `listEditorThemeTokens()` / `getEditorThemeToken()` / `toDesignTokenFormatGroup()` as a host-facing catalog of the nine shipped chrome tokens. +3. Reject unknown token names with a stable payload-redacted `EditorThemeTokenError`. +4. Preview repeating `.cwl-tb-btn` and token swatches in Storybook. +5. Add no network, persistence, credential, model, tenant, Figma, or design-tool sync authority. + +## Standards rationale + +The Design Tokens Format Module 2025.10 defines a vendor-neutral JSON interchange for token groups, `$type`, and `$value` (Design Tokens Community Group, 2025). Inkspan emits a snapshot of its CSS custom properties in that shape. The report is a W3C Community Final Specification, not a W3C Standard, so this record does not claim W3C standardization or complete DTCG conformance. + +WCAG 2.2 requires sufficient contrast for text and user-interface components (World Wide Web Consortium, 2024). Host overrides remain the host's contrast responsibility. Storybook's React/Vite preview is the inventory surface for repeating chrome (Storybook, n.d.). + +## Test-first evidence + +- RED: `src/designTokens.test.ts` failed because `./designTokens.js` did not exist. +- GREEN: the catalog lists the nine shipped tokens, aligns light/dark/forced-colors color values with `src/styles.css`, rejects unknown names without reflecting caller input, and emits a DTCG 2025.10 group. + +## Residual risk + +Forced-colors and print media still apply after a host override. The font token snapshot splits a CSS font-family list and does not execute CSS. Storybook success is not Chromium/Firefox/WebKit release evidence. + +## Rollback + +Rollback must remove the catalog export, this record, the operator guide, the Storybook inventory/config/stories, ADR 0027, the changelog entry, and the documentation-index rows together. + +## References (APA 7th edition) + +Design Tokens Community Group. (2025, October 28). *Design Tokens Format Module 2025.10* (Final Community Group Report). World Wide Web Consortium. https://www.w3.org/community/reports/design-tokens/CG-FINAL-format-20251028/ + +Storybook. (n.d.). *Storybook for React with Vite*. Retrieved August 16, 2026, from https://storybook.js.org/docs/get-started/frameworks/react-vite + +World Wide Web Consortium. (2024, December 12). *Web Content Accessibility Guidelines (WCAG) 2.2* (W3C Recommendation). https://www.w3.org/TR/WCAG22/ diff --git a/docs/storybook-inventory.md b/docs/storybook-inventory.md new file mode 100644 index 00000000..f2c4f71b --- /dev/null +++ b/docs/storybook-inventory.md @@ -0,0 +1,12 @@ +# Storybook inventory + +Status: Active PR / Proposed + +Run `pnpm storybook` to preview repeating Inkspan chrome before you override tokens in a host. Each story uses the shipped `.cwl-*` classes so a token change is visible on the same objects buyers see. + +| Story | Repeating object | Host next action | +| --- | --- | --- | +| `Editor Chrome / Toolbar Button States` | `.cwl-tb-btn` default and `is-active` | Override `--cwl-accent` and `--cwl-accent-soft` on `.cwl-editor` after checking WCAG 2.2 contrast | +| `Editor Chrome / Theme Tokens` | Color, radius, and font tokens | Copy `toDesignTokenFormatGroup()` into a host theme file; do not edit Inkspan internals | + +Storybook is a development preview. It does not authorize documents, persist content, or replace packed-package evidence. diff --git a/package.json b/package.json index 4e55d924..7e861123 100644 --- a/package.json +++ b/package.json @@ -107,6 +107,8 @@ "test": "vitest run", "test:watch": "vitest", "coverage": "vitest run --coverage", + "storybook": "storybook dev -p 6006", + "build-storybook": "storybook build", "test:package-config": "node --test ./scripts/revision-evidence-consumer-config.test.mjs ./scripts/release-metadata.test.mjs ./scripts/javascript-runtime-authority.test.mjs", "verify:package": "pnpm run test:package-config && node ./tests/package/verify-package.mjs && node ./tests/package/verify-editor-placeholder-package.mjs && node ./scripts/verify-canonical-envelope-package.mjs && node ./scripts/verify-revision-evidence-package.mjs && node ./scripts/verify-framework-free-revision-evidence-package.mjs && node ./scripts/verify-framework-free-envelope-identity-package.mjs && node ./tests/package/verify-framework-free-autosave-package.mjs && node ./scripts/verify-text-position-selector-package.mjs && node ./scripts/verify-text-position-selector-subpath-package.mjs && node ./scripts/verify-markdown-subpath-package.mjs" }, @@ -143,6 +145,9 @@ "yjs": "^13.6.30" }, "devDependencies": { + "@storybook/addon-docs": "^10.5.8", + "@storybook/react": "^10.5.8", + "@storybook/react-vite": "^10.5.8", "@testing-library/jest-dom": "^6.6.3", "@testing-library/react": "^16.2.0", "@testing-library/user-event": "^14.5.2", @@ -155,6 +160,7 @@ "jsdom": "^25.0.1", "react": "^18.3.1", "react-dom": "^18.3.1", + "storybook": "^10.5.8", "typescript": "^5.7.3", "vite": "^6.4.3", "vite-plugin-dts": "^4.5.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 3a3d3e0e..00b6f66e 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -71,6 +71,15 @@ importers: specifier: ^13.6.30 version: 13.6.31 devDependencies: + '@storybook/addon-docs': + specifier: ^10.5.8 + version: 10.5.8(@types/react-dom@18.3.7(@types/react@18.3.31))(@types/react@18.3.31)(esbuild@0.25.12)(rollup@4.62.2)(storybook@10.5.8(@types/react@18.3.31)(react@18.3.1))(vite@6.4.3(@types/node@22.20.1)) + '@storybook/react': + specifier: ^10.5.8 + version: 10.5.8(@types/react-dom@18.3.7(@types/react@18.3.31))(@types/react@18.3.31)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(storybook@10.5.8(@types/react@18.3.31)(react@18.3.1))(typescript@5.9.3) + '@storybook/react-vite': + specifier: ^10.5.8 + version: 10.5.8(@types/react-dom@18.3.7(@types/react@18.3.31))(@types/react@18.3.31)(esbuild@0.25.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(rollup@4.62.2)(storybook@10.5.8(@types/react@18.3.31)(react@18.3.1))(typescript@5.9.3)(vite@6.4.3(@types/node@22.20.1)) '@testing-library/jest-dom': specifier: ^6.6.3 version: 6.9.1 @@ -107,6 +116,9 @@ importers: react-dom: specifier: ^18.3.1 version: 18.3.1(react@18.3.1) + storybook: + specifier: ^10.5.8 + version: 10.5.8(@types/react@18.3.31)(react@18.3.1) typescript: specifier: ^5.7.3 version: 5.9.3 @@ -251,6 +263,24 @@ packages: resolution: {integrity: sha512-Vd/9EVDiu6PPJt9yAh6roZP6El1xHrdvIVGjyBsHR0RYwNHgL7FJPyIIW4fANJNG6FtyZfvlRPpFI4ZM/lubvw==} engines: {node: '>=18'} + '@emnapi/core@1.11.2': + resolution: {integrity: sha512-TC8MkTuZUtcTSiFeuC0ksCh9QIJ5+F21MvZ4Wn4ORfYaFJ/0dsiudv5tVkejgwZlwQ39jL9WWDe2lz8x0WglOA==} + + '@emnapi/core@1.9.2': + resolution: {integrity: sha512-UC+ZhH3XtczQYfOlu3lNEkdW/p4dsJ1r/bP7H8+rhao3TTTMO1ATq/4DdIi23XuGoFY+Cz0JmCbdVl0hz9jZcA==} + + '@emnapi/runtime@1.11.2': + resolution: {integrity: sha512-kyOl3X0DuTiT1h2ft8r2fYO8JYtU9a9Xis/zBSiGArNaagCOWx90N1k2wxp18czFDH+OgcWGb5ZP/XMt3dcyPA==} + + '@emnapi/runtime@1.9.2': + resolution: {integrity: sha512-3U4+MIWHImeyu1wnmVygh5WlgfYDtyf0k8AbLhMFxOipihf6nrWC4syIm/SwEeec0mNSafiiNnMJwbza/Is6Lw==} + + '@emnapi/wasi-threads@1.2.1': + resolution: {integrity: sha512-uTII7OYF+/Mes/MrcIOYp5yOtSMLBWSIoLPpcgwipoiKbli6k322tcoFsxoIIxPDqW01SQGAgko4EzZi2BNv2w==} + + '@emnapi/wasi-threads@1.2.2': + resolution: {integrity: sha512-c95qOXkHdydNKhscBTebqEC1CVAZpyqOfVfBzQ1qgzyl3gfeldUjIggDbIZgDKsHLgnsM+igH7TJ/eAasaVuMA==} + '@esbuild/aix-ppc64@0.25.12': resolution: {integrity: sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA==} engines: {node: '>=18'} @@ -415,6 +445,15 @@ packages: resolution: {integrity: sha512-+Sg6GCR/wy1oSmQDFq4LQDAhm3ETKnorxN+y5nbLULOR3P0c14f2Wurzj3/xqPXtasLFfHd5iRFQ7AJt4KH2cw==} engines: {node: '>=8'} + '@joshwooding/vite-plugin-react-docgen-typescript@0.7.0': + resolution: {integrity: sha512-qvsTEwEFefhdirGOPnu9Wp6ChfIwy2dBCRuETU3uE+4cC+PFoxMSiiEhxk4lOluA34eARHA0OxqsEUYDqRMgeQ==} + peerDependencies: + typescript: '>= 4.3.x' + vite: ^6.4.3 + peerDependenciesMeta: + typescript: + optional: true + '@jridgewell/gen-mapping@0.3.13': resolution: {integrity: sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==} @@ -431,6 +470,12 @@ packages: '@jridgewell/trace-mapping@0.3.31': resolution: {integrity: sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==} + '@mdx-js/react@3.1.1': + resolution: {integrity: sha512-f++rKLQgUVYDAtECQ6fn/is15GkEH9+nZPM3MS0RcxVqoTfawHvDlSCH7JbMhAM6uJ32v3eXLvLmLvjGu7PTQw==} + peerDependencies: + '@types/react': '>=16' + react: '>=16' + '@microsoft/api-extractor-model@7.33.8': resolution: {integrity: sha512-aIcoQggPyer3B6Ze3usz0YWC/oBwUHfRH5ETUsr+oT2BRA6SfTJl7IKPcPZkX4UR+PohowzW4uMxsvjrn8vm+w==} @@ -447,6 +492,246 @@ packages: '@mixmark-io/domino@2.2.0': resolution: {integrity: sha512-Y28PR25bHXUg88kCV7nivXrP2Nj2RueZ3/l/jdx6J9f8J4nsEGcgX0Qe6lt7Pa+J79+kPiJU3LguR6O/6zrLOw==} + '@napi-rs/wasm-runtime@1.2.3': + resolution: {integrity: sha512-UMduMbqO5s5zF2NkNacMT/yK5Y5QiKvWr2+50bzIIxFDwVJ2h49b+oyjaCGPhJxd2/gC2x39EHv/gHVuu36x2Q==} + engines: {node: ^20.19.0 || ^22.13.0 || >=23.5.0} + peerDependencies: + '@emnapi/core': ^1.7.1 || ^2.0.0-alpha.4 + '@emnapi/runtime': ^1.7.1 || ^2.0.0-alpha.4 + + '@oxc-parser/binding-android-arm-eabi@0.127.0': + resolution: {integrity: sha512-0LC7ye4hvqbIKxAzThzvswgHLFu2AURKzYLeSVvLdu2TBOYWQDmHnTqPLeA597BcUCxiLqLsS4CJ5uoI5WYWCQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm] + os: [android] + + '@oxc-parser/binding-android-arm64@0.127.0': + resolution: {integrity: sha512-b5jtVTH6AU5CJXHNdj7Jj9IEiR9yVjjnwHzPJhGyHGPdcsZSzBCkS9GBbV33niRMvKthDwQRFRJfI4a+k4PvYg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [android] + + '@oxc-parser/binding-darwin-arm64@0.127.0': + resolution: {integrity: sha512-obCE8B7ISKkJidjlhv9xRGJPOSDG2Yu6PRga9Ruaz35uintHxbp1Ki/Yc71wx4rj3Edrm0a1kzG1TAwit0wFpg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [darwin] + + '@oxc-parser/binding-darwin-x64@0.127.0': + resolution: {integrity: sha512-JL6Xb5IwPQT8rUzlpsX7E+AgfcdNklXNPFp8pjCQQ5MQOQo5rtEB2ui+3Hgg9Sn7Y9Egj6YOLLiHhLpdAe12Aw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [darwin] + + '@oxc-parser/binding-freebsd-x64@0.127.0': + resolution: {integrity: sha512-SDQ/3MQFw58fqQz3Z1PhSKFF3JoCF4gmlNjziDm8X02tTahCw0qJbd7FGPDKw1i4VTBZene9JPyC3mHtSvi+wA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [freebsd] + + '@oxc-parser/binding-linux-arm-gnueabihf@0.127.0': + resolution: {integrity: sha512-Av+D1MIqzV0YMGPT9we2SIZaMKD7Cxs4CvXSx/yxaWHewZjYEjScpOf5igc8IILASViw4WTnjlwUdI1KzVtDHQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm] + os: [linux] + + '@oxc-parser/binding-linux-arm-musleabihf@0.127.0': + resolution: {integrity: sha512-Cs2fdJ8cPpFdeebj6p4dag8A4+56hPvZ0AhQQzlaLswGz1tz7bXt1nETLeorrM9+AMcWFFkqxcXwDGfTVidY8g==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm] + os: [linux] + + '@oxc-parser/binding-linux-arm64-gnu@0.127.0': + resolution: {integrity: sha512-qdOfTcT6SY8gsJrrV92uyEUyjqMGPpIB5JZUG6QN5dukYd+7/j0kX6MwK1DgQj39jtUYixxPiaRUiEN1+0CXgQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [linux] + libc: [glibc] + + '@oxc-parser/binding-linux-arm64-musl@0.127.0': + resolution: {integrity: sha512-EoTCZneNFU/P2qrpEM+RHmQwt+CvDkyGESG6qhr7KaegXLZwePfbrkCDfAk8/rhxbDUVGsZILX+2tqPzFtoFWA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [linux] + libc: [musl] + + '@oxc-parser/binding-linux-ppc64-gnu@0.127.0': + resolution: {integrity: sha512-zALjmZYgxFLHjXeudcDF0xFGNydTAtkAeXAr2EuC17ywCyFxcmQra4w0BMde0Yi/re4Bi4iwEoEXtYN7l6eBLQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [ppc64] + os: [linux] + libc: [glibc] + + '@oxc-parser/binding-linux-riscv64-gnu@0.127.0': + resolution: {integrity: sha512-fPP8M6zQLS7Jz7o9d5ArUSuAuSK3e+WCYVrCpdzeCOejidtZExJ9tjhDrAd3HEPqARBCPmdpqxESPFqy44vkBQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [riscv64] + os: [linux] + libc: [glibc] + + '@oxc-parser/binding-linux-riscv64-musl@0.127.0': + resolution: {integrity: sha512-7IcC4Ao02oGpfnjt+X/oF4U2mllo2qoSkw5xxiXNKL9MCTsTiAC6616beOuehdxGcnz1bRoPC1RQ2f1GQDdN+g==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [riscv64] + os: [linux] + libc: [musl] + + '@oxc-parser/binding-linux-s390x-gnu@0.127.0': + resolution: {integrity: sha512-pbXIhiNFHoqWeqDNLiJ9JkpHz1IM9k4DXa66x+1GTWMG7iLxtkXgE53iiuKSXwmk3zIYmaPVfBvgcAhS583K4Q==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [s390x] + os: [linux] + libc: [glibc] + + '@oxc-parser/binding-linux-x64-gnu@0.127.0': + resolution: {integrity: sha512-MYCguB9RvBvlSd6gbuNI7QwiLoCCAlGnlRJFPrzLI6U1/9wkC/WK6LtBAUln55H1Ctqw45PWmqrobKoMhsYQzQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [linux] + libc: [glibc] + + '@oxc-parser/binding-linux-x64-musl@0.127.0': + resolution: {integrity: sha512-5eY0B/bxf1xIUxb4NOTvOI3KWtBQfPWYyKAzgcrCt0mDibSZygVpO1Pz8bkeiSZ5Jj9+M09dkggG3H8I5d0Uyg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [linux] + libc: [musl] + + '@oxc-parser/binding-openharmony-arm64@0.127.0': + resolution: {integrity: sha512-Gld0ajrFTUXNtdw20fVBuTQx66FA75nIVg+//pPfR3sXkuABB4mTBhl3r9JNzrJpgW//qiwxf0nWXUWGJSL3UQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [openharmony] + + '@oxc-parser/binding-wasm32-wasi@0.127.0': + resolution: {integrity: sha512-T6KVD7rhLzFlwGRXMnxUFfkCZD8FHnb968wVXW1mXzgRFc5RNXOBY2mPPDZ77x5Ln76ltLMgtPg0cOkU1NSrEQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [wasm32] + + '@oxc-parser/binding-win32-arm64-msvc@0.127.0': + resolution: {integrity: sha512-Ujvw4X+LD1CCGULcsQcvb4YNVoBGqt+JHgNNzGGaCImELiZLk477ifUH53gIbE7EKd933NdTi25JWEr9K2HwXw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [win32] + + '@oxc-parser/binding-win32-ia32-msvc@0.127.0': + resolution: {integrity: sha512-0cwxKO7KHQQQfo4Uf4B2SQrhgm+cJaP9OvFFhx52Tkg4bezsacu83GB2/In5bC415Ueeym+kXdnge/57rbSfTw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [ia32] + os: [win32] + + '@oxc-parser/binding-win32-x64-msvc@0.127.0': + resolution: {integrity: sha512-rOrnSQSCbhI2kowr9XxE7m9a8oQXnBHjnS6j95LxxAnEZ0+Fz20WlRXG4ondQb+ejjt2KOsa65sE6++L6kUd+w==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [win32] + + '@oxc-project/types@0.127.0': + resolution: {integrity: sha512-aIYXQBo4lCbO4z0R3FHeucQHpF46l2LbMdxRvqvuRuW2OxdnSkcng5B8+K12spgLDj93rtN3+J2Vac/TIO+ciQ==} + + '@oxc-resolver/binding-android-arm-eabi@11.24.2': + resolution: {integrity: sha512-y09e0L0SRI2OA2tUIrjBgoV3eH5hvUKXNkJqXmNo5V2WxIjyC7I7aJfRLMEVpA8yi95f90gFDvO0VMgrDw+vwA==} + cpu: [arm] + os: [android] + + '@oxc-resolver/binding-android-arm64@11.24.2': + resolution: {integrity: sha512-cl4icWaZFnLdg8m6qtnh5rBMuGbxc/ptStFHLeCNwr+2cZjkjNwQu/jYRS0CHlnPecOJMpuS5M6/BH+0J/YkEg==} + cpu: [arm64] + os: [android] + + '@oxc-resolver/binding-darwin-arm64@11.24.2': + resolution: {integrity: sha512-At29QEMF6HajbQvgY8K6OXnHD1x9rad74xBEfmCB6ZqCGsdq75aK7tOYcTbOanMy8qdIBrfL3SMr3p/lfSlb9w==} + cpu: [arm64] + os: [darwin] + + '@oxc-resolver/binding-darwin-x64@11.24.2': + resolution: {integrity: sha512-A5Kqr1EUj4oIL5CF4WRssq/o5P0Y11cwoFouMRmQ7YnC/A8V93nv1nb7aSU8HwcgmXropjLNkVTl4MN87cu28Q==} + cpu: [x64] + os: [darwin] + + '@oxc-resolver/binding-freebsd-x64@11.24.2': + resolution: {integrity: sha512-R5xkRBRRz7ceH/P5Jrc6G7FmdUdgpLYyESFAUDVTNQ9K0sGPxcp4ljiwEwEqsvNcQ4sYbMRrWcHHBCu7ksAJVw==} + cpu: [x64] + os: [freebsd] + + '@oxc-resolver/binding-linux-arm-gnueabihf@11.24.2': + resolution: {integrity: sha512-k/RuYL4L/R58IBn3wT5ma3Wh4k62bp1eYCFRWCmMsasUOqL+H6sW0VGFadEzKWXFFlz+2uIMoeMk9ySSZJHgbg==} + cpu: [arm] + os: [linux] + + '@oxc-resolver/binding-linux-arm-musleabihf@11.24.2': + resolution: {integrity: sha512-bnHAak3ujYfH5pKk4NieFNbvYvernfoQDgwLddbZ3OtMYrem87/qjlA+u+aKG0oZcqSLGCful/6/CEA+aeAgaA==} + cpu: [arm] + os: [linux] + + '@oxc-resolver/binding-linux-arm64-gnu@11.24.2': + resolution: {integrity: sha512-vDT3KHgzYp47gmtNOqL2VNhCyl5Zv643eyxm//A68J8DeUGXrvD1pZFiaT4jSfe+RInfnn1R2yVHye4enx6RnA==} + cpu: [arm64] + os: [linux] + libc: [glibc] + + '@oxc-resolver/binding-linux-arm64-musl@11.24.2': + resolution: {integrity: sha512-+kMlQvbzfyEYtu5FcjE4p+ttBLpKW4d/AsAsuE69BxV6V4twZJeIQZFfD8gh/wqglY0MkPSezWXQH0jBV13MUw==} + cpu: [arm64] + os: [linux] + libc: [musl] + + '@oxc-resolver/binding-linux-ppc64-gnu@11.24.2': + resolution: {integrity: sha512-shjfMhmZ3gq9fv/w7bi3PnZlgOPG+2QAOFf0BJF0EgBSIGZ6PMLN2zbGEblTUYB/NKVDRyYhE2ff3dJ1QqNPkA==} + cpu: [ppc64] + os: [linux] + libc: [glibc] + + '@oxc-resolver/binding-linux-riscv64-gnu@11.24.2': + resolution: {integrity: sha512-zGelwFR5oRo+b69k8Lrzun86DyUHzfKN6cnjbR9l7Z7NIRznOE/2ZvPa1IUKqAL2PzAXOdwkfVqNvO1H2RlpAw==} + cpu: [riscv64] + os: [linux] + libc: [glibc] + + '@oxc-resolver/binding-linux-riscv64-musl@11.24.2': + resolution: {integrity: sha512-qxZ1SWCXJY0eyhAlP6Lmo9F2Nrtx7EkYj9oCgL8apDPCwXwCEDA2U697bbT81JIc2IrVjxO4KX6WU2N+oN9Z4w==} + cpu: [riscv64] + os: [linux] + libc: [musl] + + '@oxc-resolver/binding-linux-s390x-gnu@11.24.2': + resolution: {integrity: sha512-sGCecF3cx2DFlH4t/z7ApnOnXqN48p5p5mlHDEnHTAukQa2P+qMVE4CwyWE9W+q/m3QJ7kKfGrIjax31f44oFQ==} + cpu: [s390x] + os: [linux] + libc: [glibc] + + '@oxc-resolver/binding-linux-x64-gnu@11.24.2': + resolution: {integrity: sha512-k/VlMMcSzMlahb3/fENM4rTlsJ0s3fFROA0KXPBmKggqmTSaE383sl8F3KCOXPLmVsYfW6hCitMhXCEtNeZxxg==} + cpu: [x64] + os: [linux] + libc: [glibc] + + '@oxc-resolver/binding-linux-x64-musl@11.24.2': + resolution: {integrity: sha512-8hbnZyNi97b/8wapYaIF9+t9GmZKBW2vunaOc3h9HGJptH7b7XpvZqOTBSm/MpTjr7H497BlgOaSfLUdhmy2bw==} + cpu: [x64] + os: [linux] + libc: [musl] + + '@oxc-resolver/binding-openharmony-arm64@11.24.2': + resolution: {integrity: sha512-MvyGik3a6pVgZ0t/kWlbmFxFLmXQJwgLsY2eYFHLpy0wGwRbfzeIGgDwQ3kXqE30z+kSXennRkCrT7TUvkptNg==} + cpu: [arm64] + os: [openharmony] + + '@oxc-resolver/binding-wasm32-wasi@11.24.2': + resolution: {integrity: sha512-vHcssMPwO08RTvj/c0iOBz90attxyG3wQJ0dTcyEQK43LRpcdLWZlV5feBhv6Isn6ahbQIzHbCgfa81+RiML0Q==} + engines: {node: '>=14.0.0'} + cpu: [wasm32] + + '@oxc-resolver/binding-win32-arm64-msvc@11.24.2': + resolution: {integrity: sha512-uokJqro2iBqkFvJdKQLP7d8/BUmFwESQFVmIJUQKj1Xn1a/LysJoe1vmeECLF5b3jsV8CAL5sEMJXX6SdK9Nhg==} + cpu: [arm64] + os: [win32] + + '@oxc-resolver/binding-win32-x64-msvc@11.24.2': + resolution: {integrity: sha512-UqGPmo56KDfLlfXFAFIrNflHT8tFxWGEivWg3Zeyp4Uy2NlKN1FGPr6/BxcLGG3+kZ6Wp14g5Uj+n71boqZfiw==} + cpu: [x64] + os: [win32] + '@pkgjs/parseargs@0.11.0': resolution: {integrity: sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==} engines: {node: '>=14'} @@ -637,6 +922,90 @@ packages: '@rushstack/ts-command-line@5.3.10': resolution: {integrity: sha512-fwI076HYknC0IrMXdY6UmjDv+PH7NHhNJX3/pY2UblSE5XrXgndXZPiOe/6ZtuFpn6DvVDVNhtkIzQ+Qu/MhVQ==} + '@storybook/addon-docs@10.5.8': + resolution: {integrity: sha512-NlHiMKW/UvW/uL8HXFDCEVwoH3qZeGYZ/qlWax4d7H471b/T54MBq2KcB4ZrdA785FfIH3numAJdBb5jwn00Mg==} + peerDependencies: + '@types/react': ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + storybook: ^10.5.8 + peerDependenciesMeta: + '@types/react': + optional: true + + '@storybook/builder-vite@10.5.8': + resolution: {integrity: sha512-UeRnn7yT55WmBlHNOQzLrvN7vsHEvVgIukhKDO+4cMbGXN87wZkbxhx6NstpuXRH8OxGqwKS0SZNVp+SC1ftLQ==} + peerDependencies: + storybook: ^10.5.8 + vite: ^6.4.3 + + '@storybook/csf-plugin@10.5.8': + resolution: {integrity: sha512-/FHiMyOWWEXfwK/lM0WxmkP9GLzbSJJuzGtfeuNWSOVDnvAMbjavitxfHb5wSbWKIQo0XYC1EJ2Y7x91XNYP4w==} + peerDependencies: + esbuild: ^0.25.0 + rollup: '*' + storybook: ^10.5.8 + vite: ^6.4.3 + webpack: '*' + peerDependenciesMeta: + esbuild: + optional: true + rollup: + optional: true + vite: + optional: true + webpack: + optional: true + + '@storybook/global@5.0.0': + resolution: {integrity: sha512-FcOqPAXACP0I3oJ/ws6/rrPT9WGhu915Cg8D02a9YxLo0DE9zI+a9A5gRGvmQ09fiWPukqI8ZAEoQEdWUKMQdQ==} + + '@storybook/icons@2.1.0': + resolution: {integrity: sha512-Fxh9vYpX9bQqFeHRiY8h2ApeRGDzRSMLwJwNZ/AIRqnyOKHxRKL+yFe+ctEkVJmuptRE9u1Hrn8ZZNHyfDKKNg==} + peerDependencies: + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + + '@storybook/react-dom-shim@10.5.8': + resolution: {integrity: sha512-N8D13/Xny+V3kfe1KBgsAHS0nKWXLLdgOOXS9poKdYzVwVCN+CGEGBxWX0zMMtdCptqa6/57em9coPlZMoO+bg==} + peerDependencies: + '@types/react': ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + '@types/react-dom': ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + storybook: ^10.5.8 + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@storybook/react-vite@10.5.8': + resolution: {integrity: sha512-ioMJGi4YzueGsJBlYio+2+UhfCFB9QV5Bs1lOilkek+a4BZgKJl0D1mVSJl6k96stQBPZmLgI9/l0hLVcUL6Kg==} + peerDependencies: + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + storybook: ^10.5.8 + typescript: '>= 4.9.x' + vite: ^6.4.3 + peerDependenciesMeta: + typescript: + optional: true + + '@storybook/react@10.5.8': + resolution: {integrity: sha512-6qqkmqX6imtL+0Z9Uan2tIfYivOI0FiVmWr0zpqqQR15AkJ18JfNcNTQoyjeAlCO0Kei56SWqnu2qLq52TYplg==} + peerDependencies: + '@types/react': ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + '@types/react-dom': ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + storybook: ^10.5.8 + typescript: '>= 4.9.x' + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + typescript: + optional: true + '@testing-library/dom@10.4.1': resolution: {integrity: sha512-o4PXJQidqJl82ckFaXUeoAW+XysPLauYI43Abki5hABd853iMhitooc6znOnczgbTYmEP6U6/y1ZyKAIsvMKGg==} engines: {node: '>=18'} @@ -848,6 +1217,9 @@ packages: '@tiptap/starter-kit@2.27.2': resolution: {integrity: sha512-bb0gJvPoDuyRUQ/iuN52j1//EtWWttw+RXAv1uJxfR0uKf8X7uAqzaOOgwjknoCIDC97+1YHwpGdnRjpDkOBxw==} + '@tybys/wasm-util@0.10.3': + resolution: {integrity: sha512-F3fo1MYrRJYL3zER0OUOmkutjr1Vp23m7OsSgp7nq4SP6OqX6C/56XFIPAl5bt3zaBRjmW7SGz3u/6LwFpYcOg==} + '@types/argparse@1.0.38': resolution: {integrity: sha512-ebDJ9b0e702Yr7pWgB0jzm+CX4Srzz8RcXtLJDJB+BSccqMa36uyH/zUsSYao5+BD1ytv3k3rPYCq4mAE1hsXA==} @@ -872,6 +1244,9 @@ packages: '@types/deep-eql@4.0.2': resolution: {integrity: sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==} + '@types/doctrine@0.0.9': + resolution: {integrity: sha512-eOIHzCUSH7SMfonMG1LsC2f8vxBFtho6NGBznK41R84YzPuvSBzrhEps33IsQiOW9+VL6NQ9DbjQJznk/S4uRA==} + '@types/estree@1.0.9': resolution: {integrity: sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==} @@ -884,6 +1259,9 @@ packages: '@types/mdurl@2.0.0': resolution: {integrity: sha512-RGdgjQUZba5p6QEFAVx2OGb8rQDL/cPRG7GiedRzMcJ1tYnUANBncjbSB1NRGwbvjcPeikRABz2nshyPk1bhWg==} + '@types/mdx@2.0.14': + resolution: {integrity: sha512-T48PeuJtvLosNTPVhfnIp3i/n3a4g4Bad7YCq5k64D4u7NwDrAotikQ+5+sjtUvBmxCMlbo3dVL+C2dP0rWHzg==} + '@types/node@22.20.1': resolution: {integrity: sha512-EANqOCF9QFyra+4pfxUcX9STKJpCLjMbObVzljIJomAWSnuSIEAvyzEU53GaajbXJEgdh0iEcPL+DGvpUd4k1Q==} @@ -898,6 +1276,9 @@ packages: '@types/react@18.3.31': resolution: {integrity: sha512-vfEqpXTvwT91yhmwdfouStN2hSKwTvyRs8qpLfADyrq/kxDw0hZM7Wk9Ug1FELj8hIby+S/+kQCSRFF32nv2Qw==} + '@types/resolve@1.20.6': + resolution: {integrity: sha512-A4STmOXPhMUtHH+S6ymgE2GiBSMqf4oTvcQZMcHzokuTLVYzXTB8ttjcgxOVaAp2lGwEdzZ0J+cRbbeevQj1UQ==} + '@types/turndown@5.0.6': resolution: {integrity: sha512-ru00MoyeeouE5BX4gRL+6m/BsDfbRayOskWqUvh7CLGW+UXxHQItqALa38kKnOiZPqJrtzJUgAC2+F0rL1S4Pg==} @@ -919,6 +1300,9 @@ packages: '@vitest/browser': optional: true + '@vitest/expect@3.2.4': + resolution: {integrity: sha512-Io0yyORnB6sikFlt8QW5K7slY4OjqNX9jmJQ02QDda8lyM6B5oNgVWoSoKPac8/kgnCUzuHQKrSLtu/uOqqrig==} + '@vitest/expect@3.2.7': resolution: {integrity: sha512-E8eBXaKibuvH2pSZErOjdVb5vF4PbKYcrnluBTYxEk1l/VhhwZg1kZQsdtjq+CsF5CFydf2Rdkz7jDHKSisi3w==} @@ -933,6 +1317,9 @@ packages: vite: optional: true + '@vitest/pretty-format@3.2.4': + resolution: {integrity: sha512-IVNZik8IVRJRTr9fxlitMKeJeXFFFN0JaB9PHPGQ8NKQbGpfjlTx9zO4RefN8gp7eqjNy8nyK3NZmBzOPeIxtA==} + '@vitest/pretty-format@3.2.7': resolution: {integrity: sha512-KUHlwqVu0sRlhCdyPdQ/wBoTfRahjUky1MubOmYw9fWfIZy1gNoHpuaaQBPAaMaVYdQYHJLurzj8ECCj5OwTqA==} @@ -942,9 +1329,15 @@ packages: '@vitest/snapshot@3.2.7': resolution: {integrity: sha512-7C+MwShwtBSI5Buwoyg3s/iY1eHL9PKAf+O1wVh/TdnjXUtkoL/9YQtre90i4MtNXM6edP1wJ2zOBpfCyhIS7g==} + '@vitest/spy@3.2.4': + resolution: {integrity: sha512-vAfasCOe6AIK70iP5UD11Ac4siNUNJ9i/9PZ3NKx07sG6sUxeag1LWdNrMWeKKYBLlzuK+Gn65Yd5nyL6ds+nw==} + '@vitest/spy@3.2.7': resolution: {integrity: sha512-Q2eQGI6d2L/hBtZ0qNuKcAGid68XK6cv1xsoaIma6PaJhHPoqcEJhYpXZ/5myCMqkNgtP6UKuBhbc0nHKnrkuQ==} + '@vitest/utils@3.2.4': + resolution: {integrity: sha512-fB2V0JFrQSMsCo9HiSq3Ezpdv4iYaXRG1Sx8edX3MwxfyNn83mKiGzOcH+Fkxt4MHxr3y42fQi1oeAInqgX2QA==} + '@vitest/utils@3.2.7': resolution: {integrity: sha512-x6BDOd7dyo3PFLY3I9/HJ25X/6OurhGXk2/B9gOZNPF7XDVjeBK4k01lQE5uvDpbuheErh91qYuE1E2OEjK3Rw==} @@ -977,6 +1370,9 @@ packages: '@vue/shared@3.5.39': resolution: {integrity: sha512-l1rrBtBfTnmxvtsvdQDXltUUy8S1Y+ZaqdfUzmAnJkTd8Z8rv5v/ytW+TKiqEOWyHPoqtPlNFSs0lhRmYVSHVA==} + '@webcontainer/env@1.1.1': + resolution: {integrity: sha512-6aN99yL695Hi9SuIk1oC88l9o0gmxL1nGWWQ/kNy81HigJ0FoaoTXpytCj6ItzgyCEwA9kF1wixsTuv5cjsgng==} + acorn@8.17.0: resolution: {integrity: sha512-xRQbDb9BnwDafYNn6Vwl839DYVjqXYb1XVGtWAZ1kcDc6iwAL4hg3B1dZlRiuENFeO2H53gFG3in621AdERVAg==} engines: {node: '>=0.4.0'} @@ -1045,6 +1441,10 @@ packages: resolution: {integrity: sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==} engines: {node: '>=12'} + ast-types@0.16.1: + resolution: {integrity: sha512-6t10qk83GOG8p0vKmaCr8eiilZwO171AvbROMtvvNiwrTly62t+7XkA8RdIIVbpMhCASAsxgAzdRSwh6nw/5Dg==} + engines: {node: '>=4'} + ast-v8-to-istanbul@0.3.12: resolution: {integrity: sha512-BRRC8VRZY2R4Z4lFIL35MwNXmwVqBityvOIwETtsCSwvjl0IdgFsy9NhdaA6j74nUdtJJlIypeRhpDam19Wq3g==} @@ -1069,6 +1469,10 @@ packages: engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} hasBin: true + bundle-name@4.1.0: + resolution: {integrity: sha512-tjwM5exMg6BGRI+kNmTntNsvdZS1X8BFYS6tnJ2hdH0kVxM6/eVZ2xy+FqStSWvYmtfFMDLIxurorHwDKfDz5Q==} + engines: {node: '>=18'} + cac@6.7.14: resolution: {integrity: sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==} engines: {node: '>=8'} @@ -1151,6 +1555,18 @@ packages: resolution: {integrity: sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q==} engines: {node: '>=6'} + default-browser-id@5.0.1: + resolution: {integrity: sha512-x1VCxdX4t+8wVfd1so/9w+vQ4vx7lKd2Qp5tDRutErwmR85OgmfX7RlLRMWafRMY7hbEiXIbudNrjOAPa/hL8Q==} + engines: {node: '>=18'} + + default-browser@5.5.0: + resolution: {integrity: sha512-H9LMLr5zwIbSxrmvikGuI/5KGhZ8E2zH3stkMgM5LpOWDutGM2JZaj460Udnf1a+946zc7YBgrqEWwbk7zHvGw==} + engines: {node: '>=18'} + + define-lazy-prop@3.0.0: + resolution: {integrity: sha512-N+MeXYoqr3pOgn8xfyRPREN7gHakLYjhsHhWGT3fWAiL4IkAt0iDw14QiiEm2bE30c5XX5q0FtAA3CK5f9/BUg==} + engines: {node: '>=12'} + delayed-stream@1.0.0: resolution: {integrity: sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==} engines: {node: '>=0.4.0'} @@ -1163,6 +1579,10 @@ packages: resolution: {integrity: sha512-DPi0FmjiSU5EvQV0++GFDOJ9ASQUVFh5kD+OzOnYdi7n3Wpm9hWWGfB/O2blfHcMVTL5WkQXSnRiK9makhrcnw==} engines: {node: '>=0.3.1'} + doctrine@3.0.0: + resolution: {integrity: sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==} + engines: {node: '>=6.0.0'} + dom-accessibility-api@0.5.16: resolution: {integrity: sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg==} @@ -1185,6 +1605,10 @@ packages: emoji-regex@9.2.2: resolution: {integrity: sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==} + empathic@2.0.1: + resolution: {integrity: sha512-YGRs8knHhKHVShLkFET/rWAU8kmHbOV5LwN938RHI0pljAJ1Gf6SzXsSmRaEzcXTtOOmVqJ5+WtQPL5uigY50Q==} + engines: {node: '>=14'} + entities@4.5.0: resolution: {integrity: sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==} engines: {node: '>=0.12'} @@ -1229,12 +1653,21 @@ packages: resolution: {integrity: sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==} engines: {node: '>=10'} + esprima@4.0.1: + resolution: {integrity: sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==} + engines: {node: '>=4'} + hasBin: true + estree-walker@2.0.2: resolution: {integrity: sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==} estree-walker@3.0.3: resolution: {integrity: sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==} + esutils@2.0.3: + resolution: {integrity: sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==} + engines: {node: '>=0.10.0'} + expect-type@1.4.0: resolution: {integrity: sha512-KfYbmpRm0VbLjEvVa9yGwCi9GI34xvi7A/HXYWQO65CSD2u3MczUJSuwXKFIxlGsgBQizV9q5J9NHj4VG0n+pA==} engines: {node: '>=12.0.0'} @@ -1294,6 +1727,10 @@ packages: deprecated: Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me hasBin: true + glob@13.0.6: + resolution: {integrity: sha512-Wjlyrolmm8uDpm/ogGyXZXb1Z+Ca2B8NbJwqBVg0axK9GbBeoS7yGV6vjXnYdGm6X53iehEuxxbyiKp8QmN4Vw==} + engines: {node: 18 || 20 || >=22} + gopd@1.2.0: resolution: {integrity: sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==} engines: {node: '>= 0.4'} @@ -1352,13 +1789,27 @@ packages: resolution: {integrity: sha512-evOr8xfXKxE6qSR0hSXL2r3sd7ALj8+7jQEUvPYcm5sgZFdJ+AYzT6yNmJenvIYQBgIGwfwz08sL8zoL7yq2BA==} engines: {node: '>= 0.4'} + is-docker@3.0.0: + resolution: {integrity: sha512-eljcgEDlEns/7AXFosB5K/2nCM4P7FQPkGc/DWLy5rmFEWvZayGrik1d9/QIY5nJ4f9YsVvBkA6kJpHn9rISdQ==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + hasBin: true + is-fullwidth-code-point@3.0.0: resolution: {integrity: sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==} engines: {node: '>=8'} + is-inside-container@1.0.0: + resolution: {integrity: sha512-KIYLCCJghfHZxqjYBE7rEy0OBuTd5xCHS7tHVgvCLkx7StIoaxwNW3hCALgEUjFfeRk+MG/Qxmp/vtETEF3tRA==} + engines: {node: '>=14.16'} + hasBin: true + is-potential-custom-element-name@1.0.1: resolution: {integrity: sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==} + is-wsl@3.1.1: + resolution: {integrity: sha512-e6rvdUCiQCAuumZslxRJWR/Doq4VpPR82kqclvcS0efgt430SlGIk05vdCN58+VrzgtIcfNODjozVielycD4Sw==} + engines: {node: '>=16'} + isexe@2.0.0: resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} @@ -1418,6 +1869,9 @@ packages: engines: {node: '>=6'} hasBin: true + jsonc-parser@3.3.1: + resolution: {integrity: sha512-HUgH65KyejrUFPvHFPbqOY0rsFip3Bo5wb4ngvdi1EpCYWUQDC5V+Y7mZws+DLkr4M//zQJoanu1SP+87Dv1oQ==} + jsonfile@6.2.1: resolution: {integrity: sha512-zwOTdL3rFQ/lRdBnntKVOX6k5cKJwEc1HdilT71BWEu7J41gXIB2MRp+vxduPSwZJPWBxEzv4yH1wYLJGUHX4Q==} @@ -1449,6 +1903,10 @@ packages: lru-cache@10.4.3: resolution: {integrity: sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==} + lru-cache@11.5.2: + resolution: {integrity: sha512-4pfM1Ff0x50o0tQwb5ucw/RzNyD0/YJME6IVcStalZuMWxdt3sR3huStTtxz4PUmvZfRguvDejasvQ2kifR11g==} + engines: {node: 20 || >=22} + lru-cache@5.1.1: resolution: {integrity: sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==} @@ -1502,6 +1960,9 @@ packages: resolution: {integrity: sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==} engines: {node: 18 || 20 || >=22} + minimist@1.2.8: + resolution: {integrity: sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==} + minipass@7.1.3: resolution: {integrity: sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==} engines: {node: '>=16 || 14 >=14.17'} @@ -1527,9 +1988,20 @@ packages: nwsapi@2.2.24: resolution: {integrity: sha512-7YRhZ3jS45LwmSCT4b2sVFHt/WuovaktDU07QrtOBY2PXskss5a9jfmR9jptyumwXST+rFjrmppMY1KT/yn35A==} + open@10.2.0: + resolution: {integrity: sha512-YgBpdJHPyQ2UE5x+hlSXcnejzAvD0b22U2OuAP+8OnlJT+PjWPxtgmGqKKc+RgTM63U9gN0YzrYc71R2WT/hTA==} + engines: {node: '>=18'} + orderedmap@2.1.1: resolution: {integrity: sha512-TvAWxi0nDe1j/rtMcWcIj94+Ffe6n7zhow33h40SKxmsmozs6dz/e+EajymfoFcHd7sxNn8yHM8839uixMOV6g==} + oxc-parser@0.127.0: + resolution: {integrity: sha512-bkgD4qHlN7WxLdX8bLXdaU54TtQtAIg/ZBAfm0aje/mo3MRDo3P0hZSgr4U7O3xfX+fQmR5AP04JS/TGcZLcFA==} + engines: {node: ^20.19.0 || >=22.12.0} + + oxc-resolver@11.24.2: + resolution: {integrity: sha512-FY91FiDBj7ls5MsFS9jN3tjz2o0/zsdSsymlakySaBwVJZorHhkWyICLZMKxlu1R9vYo+sd3z1jwb4J8x7bNDw==} + package-json-from-dist@1.0.1: resolution: {integrity: sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==} @@ -1550,6 +2022,10 @@ packages: resolution: {integrity: sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==} engines: {node: '>=16 || 14 >=14.18'} + path-scurry@2.0.2: + resolution: {integrity: sha512-3O/iVVsJAPsOnpwWIeD+d6z/7PmqApyQePUtCndjatj/9I5LylHvt5qluFaBT3I5h3r1ejfR056c+FCv+NnNXg==} + engines: {node: 18 || 20 || >=22} + pathe@2.0.3: resolution: {integrity: sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==} @@ -1647,6 +2123,15 @@ packages: quansync@0.2.11: resolution: {integrity: sha512-AifT7QEbW9Nri4tAwR5M/uzpBuqfZf+zwaEM/QkzEjj7NBuFD2rBuy0K3dE+8wltbezDV7JMA0WfnCPYRSYbXA==} + react-docgen-typescript@2.4.0: + resolution: {integrity: sha512-ZtAp5XTO5HRzQctjPU0ybY0RRCQO19X/8fxn3w7y2VVTUbGHDKULPTL4ky3vB05euSgG5NpALhEhDPvQ56wvXg==} + peerDependencies: + typescript: '>= 4.3.x' + + react-docgen@8.0.3: + resolution: {integrity: sha512-aEZ9qP+/M+58x2qgfSFEWH1BxLyHe5+qkLNJOZQb5iGS017jpbRnoKhNRrXPeA6RfBrZO5wZrT9DMC1UqE1f1w==} + engines: {node: ^20.9.0 || >=22} + react-dom@18.3.1: resolution: {integrity: sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==} peerDependencies: @@ -1663,6 +2148,10 @@ packages: resolution: {integrity: sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==} engines: {node: '>=0.10.0'} + recast@0.23.21: + resolution: {integrity: sha512-mFAyJq9vUbSTARLZUvAEf1z3YxlvAwswbmxMx2mPA/MSm4KmpwvwvhsH/NIrZhyOuwD60Lzyw2qh83uCbgTPYw==} + engines: {node: '>= 4'} + redent@3.0.0: resolution: {integrity: sha512-6tDA8g98We0zd0GvVeMT9arEOnTw9qM03L9cJXaCjrip1OO764RDBLBfrB4cwzNGDj5OA5ioymC9GkizgWJDUg==} engines: {node: '>=8'} @@ -1690,6 +2179,10 @@ packages: rrweb-cssom@0.8.0: resolution: {integrity: sha512-guoltQEx+9aMf2gDZ0s62EcV8lsXR+0w8915TC3ITdn2YueuNjdAYh/levpU9nFaoChh9RUS5ZdQMrKfVEN9tw==} + run-applescript@7.1.0: + resolution: {integrity: sha512-DPe5pVFaAsinSaV6QjQ6gdiedWDcRCbUuiQfQa2wmWV7+xC9bGulGI8+TdRmoFkAPaBXk8CrAbnlY2ISniJ47Q==} + engines: {node: '>=18'} + safer-buffer@2.1.2: resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==} @@ -1746,6 +2239,21 @@ packages: std-env@3.10.0: resolution: {integrity: sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==} + storybook@10.5.8: + resolution: {integrity: sha512-rR4oFMSiWBSqI0lvsJPtcQUPj8+hzj3TkLu+Mw61Wo6YxPSb5FsLSHai0jZnuaIdKIlmu25KCfwlSQl4e1uvnA==} + hasBin: true + peerDependencies: + '@types/react': ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + prettier: ^2 || ^3 + vite-plus: ^0.1.15 || ^0.2.0 + peerDependenciesMeta: + '@types/react': + optional: true + prettier: + optional: true + vite-plus: + optional: true + string-argv@0.3.2: resolution: {integrity: sha512-aqD2Q0144Z+/RqG52NeHEkZauTAUWJO8c6yTftGJKO3Tja5tUgIfmIl6kExvhtxSDP7fXB6DvzkfMpCd/F3G+Q==} engines: {node: '>=0.6.19'} @@ -1766,10 +2274,18 @@ packages: resolution: {integrity: sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w==} engines: {node: '>=12'} + strip-bom@3.0.0: + resolution: {integrity: sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==} + engines: {node: '>=4'} + strip-indent@3.0.0: resolution: {integrity: sha512-laJTa3Jb+VQpaC6DseHhF7dXVqHTfJPCRDaEbid/drOhgitgYku/letMUqOXFoWV0zIIUbjpdH2t+tYj4bQMRQ==} engines: {node: '>=8'} + strip-indent@4.1.1: + resolution: {integrity: sha512-SlyRoSkdh1dYP0PzclLE7r0M9sgbFKKMFXpFRUMNuKhQSbC6VQIGzq3E0qsfvGJaUFJPGv6Ws1NZ/haTAjfbMA==} + engines: {node: '>=12'} + strip-literal@3.1.0: resolution: {integrity: sha512-8r3mkIM/2+PpjHoOtiAW8Rg3jJLHaV7xPwG+YRGrv6FP0wwk/toTpATxWYOW0BKdWwl82VT2tFYi5DlROa0Mxg==} @@ -1792,6 +2308,9 @@ packages: resolution: {integrity: sha512-u9E6A+ZDYdp7a4WnarkXPZOx8Ilz46+kby6p1yZ8zsGTz9gYa6FIS7lj2oezzNKmtdyyJNNmmXDppga5GB7kSw==} engines: {node: '>=18'} + tiny-invariant@1.3.3: + resolution: {integrity: sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg==} + tinybench@2.9.0: resolution: {integrity: sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==} @@ -1832,6 +2351,17 @@ packages: resolution: {integrity: sha512-hdF5ZgjTqgAntKkklYw0R03MG2x/bSzTtkxmIRw/sTNV8YXsCJ1tfLAX23lhxhHJlEf3CRCOCGGWw3vI3GaSPw==} engines: {node: '>=18'} + ts-dedent@2.3.0: + resolution: {integrity: sha512-JfJeIHke7y2egdGGgRAvpCwYFUsHlM2gPcrVOxFkznt/4uzQ7HFmvE63iFHVLBJNDuyDOQgijDK/tXH/f6Msjg==} + engines: {node: '>=6.10'} + + tsconfig-paths@4.2.0: + resolution: {integrity: sha512-NoZ4roiN7LnbKn9QqE1amc9DJfzvZXxF4xDavcOWt1BPkdx+m+0gJuPM+S0vCe7zTJMYUP0R8pO2XMr+Y8oLIg==} + engines: {node: '>=6'} + + tslib@2.8.1: + resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==} + turndown-plugin-gfm@1.0.2: resolution: {integrity: sha512-vwz9tfvF7XN/jE0dGoBei3FXWuvll78ohzCZQuOb+ZjWrs3a0XhQVomJEb2Qh4VHTPNRO4GPZh0V7VRbiWwkRg==} @@ -1857,6 +2387,10 @@ packages: resolution: {integrity: sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==} engines: {node: '>= 10.0.0'} + unplugin@2.3.11: + resolution: {integrity: sha512-5uKD0nqiYVzlmCRs01Fhs2BdkEgBS3SAVP6ndrBsuK42iC2+JHyxM05Rm9G8+5mkmRtzMZGY8Ct5+mliZxU/Ww==} + engines: {node: '>=18.12.0'} + update-browserslist-db@1.2.3: resolution: {integrity: sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==} hasBin: true @@ -1964,6 +2498,9 @@ packages: resolution: {integrity: sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==} engines: {node: '>=12'} + webpack-virtual-modules@0.6.2: + resolution: {integrity: sha512-66/V2i5hQanC51vBQKPH4aI8NMAcBW59FVBs+rC7eGHupMyfn34q7rZIE+ETlJ+XTevqfUhVVBgSUNSW2flEUQ==} + whatwg-encoding@3.1.1: resolution: {integrity: sha512-6qN4hJdMwfYBtE3YBTTHhoeuUrDBPZmbQaxWAqSALV/MeEnR5z1xd8UKud2RAkFoPkmB+hli1TZSnyi84xz1vQ==} engines: {node: '>=18'} @@ -2007,6 +2544,22 @@ packages: utf-8-validate: optional: true + ws@8.21.3: + resolution: {integrity: sha512-201TZ/kPWxoPr/OKWjquZR1SWKXcvxdH+e1xrx89b3YbmzLMFCLfnaG1HFIgWzJOEWZ7MvpK++odZufgYR50Rw==} + engines: {node: '>=10.0.0'} + peerDependencies: + bufferutil: ^4.0.1 + utf-8-validate: '>=5.0.2' + peerDependenciesMeta: + bufferutil: + optional: true + utf-8-validate: + optional: true + + wsl-utils@0.1.0: + resolution: {integrity: sha512-h3Fbisa2nKGPxCpm89Hk33lBLsnaGBvctQopaBSOW/uIs6FTe1ATyAnKFJrzVs9vpGdsTe73WF3V4lIsk4Gacw==} + engines: {node: '>=18'} + xml-name-validator@5.0.0: resolution: {integrity: sha512-EvGK8EJ3DhaHfbRlETOWAS5pO9MZITeauHKJyb8wyajUfQUenkIg2MvLDTZ4T/TgIcm3HU0TFBgWWboAZ30UHg==} engines: {node: '>=18'} @@ -2184,11 +2737,43 @@ snapshots: '@csstools/css-parser-algorithms': 3.0.5(@csstools/css-tokenizer@3.0.4) '@csstools/css-tokenizer': 3.0.4 - '@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4)': + '@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4)': + dependencies: + '@csstools/css-tokenizer': 3.0.4 + + '@csstools/css-tokenizer@3.0.4': {} + + '@emnapi/core@1.11.2': + dependencies: + '@emnapi/wasi-threads': 1.2.2 + tslib: 2.8.1 + optional: true + + '@emnapi/core@1.9.2': + dependencies: + '@emnapi/wasi-threads': 1.2.1 + tslib: 2.8.1 + optional: true + + '@emnapi/runtime@1.11.2': + dependencies: + tslib: 2.8.1 + optional: true + + '@emnapi/runtime@1.9.2': + dependencies: + tslib: 2.8.1 + optional: true + + '@emnapi/wasi-threads@1.2.1': dependencies: - '@csstools/css-tokenizer': 3.0.4 + tslib: 2.8.1 + optional: true - '@csstools/css-tokenizer@3.0.4': {} + '@emnapi/wasi-threads@1.2.2': + dependencies: + tslib: 2.8.1 + optional: true '@esbuild/aix-ppc64@0.25.12': optional: true @@ -2279,6 +2864,14 @@ snapshots: '@istanbuljs/schema@0.1.6': {} + '@joshwooding/vite-plugin-react-docgen-typescript@0.7.0(typescript@5.9.3)(vite@6.4.3(@types/node@22.20.1))': + dependencies: + glob: 13.0.6 + react-docgen-typescript: 2.4.0(typescript@5.9.3) + vite: 6.4.3(@types/node@22.20.1) + optionalDependencies: + typescript: 5.9.3 + '@jridgewell/gen-mapping@0.3.13': dependencies: '@jridgewell/sourcemap-codec': 1.5.5 @@ -2298,6 +2891,12 @@ snapshots: '@jridgewell/resolve-uri': 3.1.2 '@jridgewell/sourcemap-codec': 1.5.5 + '@mdx-js/react@3.1.1(@types/react@18.3.31)(react@18.3.1)': + dependencies: + '@types/mdx': 2.0.14 + '@types/react': 18.3.31 + react: 18.3.1 + '@microsoft/api-extractor-model@7.33.8(@types/node@22.20.1)': dependencies: '@microsoft/tsdoc': 0.16.0 @@ -2335,6 +2934,147 @@ snapshots: '@mixmark-io/domino@2.2.0': {} + '@napi-rs/wasm-runtime@1.2.3(@emnapi/core@1.11.2)(@emnapi/runtime@1.11.2)': + dependencies: + '@emnapi/core': 1.11.2 + '@emnapi/runtime': 1.11.2 + '@tybys/wasm-util': 0.10.3 + optional: true + + '@napi-rs/wasm-runtime@1.2.3(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2)': + dependencies: + '@emnapi/core': 1.9.2 + '@emnapi/runtime': 1.9.2 + '@tybys/wasm-util': 0.10.3 + optional: true + + '@oxc-parser/binding-android-arm-eabi@0.127.0': + optional: true + + '@oxc-parser/binding-android-arm64@0.127.0': + optional: true + + '@oxc-parser/binding-darwin-arm64@0.127.0': + optional: true + + '@oxc-parser/binding-darwin-x64@0.127.0': + optional: true + + '@oxc-parser/binding-freebsd-x64@0.127.0': + optional: true + + '@oxc-parser/binding-linux-arm-gnueabihf@0.127.0': + optional: true + + '@oxc-parser/binding-linux-arm-musleabihf@0.127.0': + optional: true + + '@oxc-parser/binding-linux-arm64-gnu@0.127.0': + optional: true + + '@oxc-parser/binding-linux-arm64-musl@0.127.0': + optional: true + + '@oxc-parser/binding-linux-ppc64-gnu@0.127.0': + optional: true + + '@oxc-parser/binding-linux-riscv64-gnu@0.127.0': + optional: true + + '@oxc-parser/binding-linux-riscv64-musl@0.127.0': + optional: true + + '@oxc-parser/binding-linux-s390x-gnu@0.127.0': + optional: true + + '@oxc-parser/binding-linux-x64-gnu@0.127.0': + optional: true + + '@oxc-parser/binding-linux-x64-musl@0.127.0': + optional: true + + '@oxc-parser/binding-openharmony-arm64@0.127.0': + optional: true + + '@oxc-parser/binding-wasm32-wasi@0.127.0': + dependencies: + '@emnapi/core': 1.9.2 + '@emnapi/runtime': 1.9.2 + '@napi-rs/wasm-runtime': 1.2.3(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2) + optional: true + + '@oxc-parser/binding-win32-arm64-msvc@0.127.0': + optional: true + + '@oxc-parser/binding-win32-ia32-msvc@0.127.0': + optional: true + + '@oxc-parser/binding-win32-x64-msvc@0.127.0': + optional: true + + '@oxc-project/types@0.127.0': {} + + '@oxc-resolver/binding-android-arm-eabi@11.24.2': + optional: true + + '@oxc-resolver/binding-android-arm64@11.24.2': + optional: true + + '@oxc-resolver/binding-darwin-arm64@11.24.2': + optional: true + + '@oxc-resolver/binding-darwin-x64@11.24.2': + optional: true + + '@oxc-resolver/binding-freebsd-x64@11.24.2': + optional: true + + '@oxc-resolver/binding-linux-arm-gnueabihf@11.24.2': + optional: true + + '@oxc-resolver/binding-linux-arm-musleabihf@11.24.2': + optional: true + + '@oxc-resolver/binding-linux-arm64-gnu@11.24.2': + optional: true + + '@oxc-resolver/binding-linux-arm64-musl@11.24.2': + optional: true + + '@oxc-resolver/binding-linux-ppc64-gnu@11.24.2': + optional: true + + '@oxc-resolver/binding-linux-riscv64-gnu@11.24.2': + optional: true + + '@oxc-resolver/binding-linux-riscv64-musl@11.24.2': + optional: true + + '@oxc-resolver/binding-linux-s390x-gnu@11.24.2': + optional: true + + '@oxc-resolver/binding-linux-x64-gnu@11.24.2': + optional: true + + '@oxc-resolver/binding-linux-x64-musl@11.24.2': + optional: true + + '@oxc-resolver/binding-openharmony-arm64@11.24.2': + optional: true + + '@oxc-resolver/binding-wasm32-wasi@11.24.2': + dependencies: + '@emnapi/core': 1.11.2 + '@emnapi/runtime': 1.11.2 + '@napi-rs/wasm-runtime': 1.2.3(@emnapi/core@1.11.2)(@emnapi/runtime@1.11.2) + optional: true + + '@oxc-resolver/binding-win32-arm64-msvc@11.24.2': + optional: true + + '@oxc-resolver/binding-win32-x64-msvc@11.24.2': + optional: true + '@pkgjs/parseargs@0.11.0': optional: true @@ -2466,6 +3206,101 @@ snapshots: transitivePeerDependencies: - '@types/node' + '@storybook/addon-docs@10.5.8(@types/react-dom@18.3.7(@types/react@18.3.31))(@types/react@18.3.31)(esbuild@0.25.12)(rollup@4.62.2)(storybook@10.5.8(@types/react@18.3.31)(react@18.3.1))(vite@6.4.3(@types/node@22.20.1))': + dependencies: + '@mdx-js/react': 3.1.1(@types/react@18.3.31)(react@18.3.1) + '@storybook/csf-plugin': 10.5.8(esbuild@0.25.12)(rollup@4.62.2)(storybook@10.5.8(@types/react@18.3.31)(react@18.3.1))(vite@6.4.3(@types/node@22.20.1)) + '@storybook/icons': 2.1.0(react@18.3.1) + '@storybook/react-dom-shim': 10.5.8(@types/react-dom@18.3.7(@types/react@18.3.31))(@types/react@18.3.31)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(storybook@10.5.8(@types/react@18.3.31)(react@18.3.1)) + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + storybook: 10.5.8(@types/react@18.3.31)(react@18.3.1) + ts-dedent: 2.3.0 + optionalDependencies: + '@types/react': 18.3.31 + transitivePeerDependencies: + - '@types/react-dom' + - esbuild + - rollup + - vite + - webpack + + '@storybook/builder-vite@10.5.8(esbuild@0.25.12)(rollup@4.62.2)(storybook@10.5.8(@types/react@18.3.31)(react@18.3.1))(vite@6.4.3(@types/node@22.20.1))': + dependencies: + '@storybook/csf-plugin': 10.5.8(esbuild@0.25.12)(rollup@4.62.2)(storybook@10.5.8(@types/react@18.3.31)(react@18.3.1))(vite@6.4.3(@types/node@22.20.1)) + storybook: 10.5.8(@types/react@18.3.31)(react@18.3.1) + ts-dedent: 2.3.0 + vite: 6.4.3(@types/node@22.20.1) + transitivePeerDependencies: + - esbuild + - rollup + - webpack + + '@storybook/csf-plugin@10.5.8(esbuild@0.25.12)(rollup@4.62.2)(storybook@10.5.8(@types/react@18.3.31)(react@18.3.1))(vite@6.4.3(@types/node@22.20.1))': + dependencies: + storybook: 10.5.8(@types/react@18.3.31)(react@18.3.1) + unplugin: 2.3.11 + optionalDependencies: + esbuild: 0.25.12 + rollup: 4.62.2 + vite: 6.4.3(@types/node@22.20.1) + + '@storybook/global@5.0.0': {} + + '@storybook/icons@2.1.0(react@18.3.1)': + dependencies: + react: 18.3.1 + + '@storybook/react-dom-shim@10.5.8(@types/react-dom@18.3.7(@types/react@18.3.31))(@types/react@18.3.31)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(storybook@10.5.8(@types/react@18.3.31)(react@18.3.1))': + dependencies: + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + storybook: 10.5.8(@types/react@18.3.31)(react@18.3.1) + optionalDependencies: + '@types/react': 18.3.31 + '@types/react-dom': 18.3.7(@types/react@18.3.31) + + '@storybook/react-vite@10.5.8(@types/react-dom@18.3.7(@types/react@18.3.31))(@types/react@18.3.31)(esbuild@0.25.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(rollup@4.62.2)(storybook@10.5.8(@types/react@18.3.31)(react@18.3.1))(typescript@5.9.3)(vite@6.4.3(@types/node@22.20.1))': + dependencies: + '@joshwooding/vite-plugin-react-docgen-typescript': 0.7.0(typescript@5.9.3)(vite@6.4.3(@types/node@22.20.1)) + '@rollup/pluginutils': 5.4.0(rollup@4.62.2) + '@storybook/builder-vite': 10.5.8(esbuild@0.25.12)(rollup@4.62.2)(storybook@10.5.8(@types/react@18.3.31)(react@18.3.1))(vite@6.4.3(@types/node@22.20.1)) + '@storybook/react': 10.5.8(@types/react-dom@18.3.7(@types/react@18.3.31))(@types/react@18.3.31)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(storybook@10.5.8(@types/react@18.3.31)(react@18.3.1))(typescript@5.9.3) + empathic: 2.0.1 + magic-string: 0.30.21 + react: 18.3.1 + react-docgen: 8.0.3 + react-dom: 18.3.1(react@18.3.1) + resolve: 1.22.12 + storybook: 10.5.8(@types/react@18.3.31)(react@18.3.1) + tsconfig-paths: 4.2.0 + vite: 6.4.3(@types/node@22.20.1) + optionalDependencies: + typescript: 5.9.3 + transitivePeerDependencies: + - '@types/react' + - '@types/react-dom' + - esbuild + - rollup + - supports-color + - webpack + + '@storybook/react@10.5.8(@types/react-dom@18.3.7(@types/react@18.3.31))(@types/react@18.3.31)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(storybook@10.5.8(@types/react@18.3.31)(react@18.3.1))(typescript@5.9.3)': + dependencies: + '@storybook/global': 5.0.0 + '@storybook/react-dom-shim': 10.5.8(@types/react-dom@18.3.7(@types/react@18.3.31))(@types/react@18.3.31)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(storybook@10.5.8(@types/react@18.3.31)(react@18.3.1)) + react: 18.3.1 + react-docgen: 8.0.3 + react-docgen-typescript: 2.4.0(typescript@5.9.3) + react-dom: 18.3.1(react@18.3.1) + storybook: 10.5.8(@types/react@18.3.31)(react@18.3.1) + optionalDependencies: + '@types/react': 18.3.31 + '@types/react-dom': 18.3.7(@types/react@18.3.31) + typescript: 5.9.3 + transitivePeerDependencies: + - supports-color + '@testing-library/dom@10.4.1': dependencies: '@babel/code-frame': 7.29.7 @@ -2697,6 +3532,11 @@ snapshots: '@tiptap/extension-text-style': 2.27.2(@tiptap/core@2.27.2(@tiptap/pm@2.27.2)) '@tiptap/pm': 2.27.2 + '@tybys/wasm-util@0.10.3': + dependencies: + tslib: 2.8.1 + optional: true + '@types/argparse@1.0.38': {} '@types/aria-query@5.0.4': {} @@ -2729,6 +3569,8 @@ snapshots: '@types/deep-eql@4.0.2': {} + '@types/doctrine@0.0.9': {} + '@types/estree@1.0.9': {} '@types/linkify-it@5.0.0': {} @@ -2740,6 +3582,8 @@ snapshots: '@types/mdurl@2.0.0': {} + '@types/mdx@2.0.14': {} + '@types/node@22.20.1': dependencies: undici-types: 6.21.0 @@ -2755,6 +3599,8 @@ snapshots: '@types/prop-types': 15.7.15 csstype: 3.2.3 + '@types/resolve@1.20.6': {} + '@types/turndown@5.0.6': {} '@types/use-sync-external-store@0.0.6': {} @@ -2790,6 +3636,14 @@ snapshots: transitivePeerDependencies: - supports-color + '@vitest/expect@3.2.4': + dependencies: + '@types/chai': 5.2.3 + '@vitest/spy': 3.2.4 + '@vitest/utils': 3.2.4 + chai: 5.3.3 + tinyrainbow: 2.0.0 + '@vitest/expect@3.2.7': dependencies: '@types/chai': 5.2.3 @@ -2806,6 +3660,10 @@ snapshots: optionalDependencies: vite: 6.4.3(@types/node@22.20.1) + '@vitest/pretty-format@3.2.4': + dependencies: + tinyrainbow: 2.0.0 + '@vitest/pretty-format@3.2.7': dependencies: tinyrainbow: 2.0.0 @@ -2822,10 +3680,20 @@ snapshots: magic-string: 0.30.21 pathe: 2.0.3 + '@vitest/spy@3.2.4': + dependencies: + tinyspy: 4.0.4 + '@vitest/spy@3.2.7': dependencies: tinyspy: 4.0.4 + '@vitest/utils@3.2.4': + dependencies: + '@vitest/pretty-format': 3.2.4 + loupe: 3.2.1 + tinyrainbow: 2.0.0 + '@vitest/utils@3.2.7': dependencies: '@vitest/pretty-format': 3.2.7 @@ -2877,6 +3745,8 @@ snapshots: '@vue/shared@3.5.39': {} + '@webcontainer/env@1.1.1': {} + acorn@8.17.0: {} agent-base@7.1.4: {} @@ -2924,6 +3794,10 @@ snapshots: assertion-error@2.0.1: {} + ast-types@0.16.1: + dependencies: + tslib: 2.8.1 + ast-v8-to-istanbul@0.3.12: dependencies: '@jridgewell/trace-mapping': 0.3.31 @@ -2948,6 +3822,10 @@ snapshots: node-releases: 2.0.51 update-browserslist-db: 1.2.3(browserslist@4.28.5) + bundle-name@4.1.0: + dependencies: + run-applescript: 7.1.0 + cac@6.7.14: {} call-bind-apply-helpers@1.0.2: @@ -3017,12 +3895,25 @@ snapshots: deep-eql@5.0.2: {} + default-browser-id@5.0.1: {} + + default-browser@5.5.0: + dependencies: + bundle-name: 4.1.0 + default-browser-id: 5.0.1 + + define-lazy-prop@3.0.0: {} + delayed-stream@1.0.0: {} dequal@2.0.3: {} diff@8.0.4: {} + doctrine@3.0.0: + dependencies: + esutils: 2.0.3 + dom-accessibility-api@0.5.16: {} dom-accessibility-api@0.6.3: {} @@ -3041,6 +3932,8 @@ snapshots: emoji-regex@9.2.2: {} + empathic@2.0.1: {} + entities@4.5.0: {} entities@6.0.1: {} @@ -3097,12 +3990,16 @@ snapshots: escape-string-regexp@4.0.0: {} + esprima@4.0.1: {} + estree-walker@2.0.2: {} estree-walker@3.0.3: dependencies: '@types/estree': 1.0.9 + esutils@2.0.3: {} + expect-type@1.4.0: {} exsolve@1.1.0: {} @@ -3168,6 +4065,12 @@ snapshots: package-json-from-dist: 1.0.1 path-scurry: 1.11.1 + glob@13.0.6: + dependencies: + minimatch: 10.2.5 + minipass: 7.1.3 + path-scurry: 2.0.2 + gopd@1.2.0: {} graceful-fs@4.2.11: {} @@ -3218,10 +4121,20 @@ snapshots: dependencies: hasown: 2.0.4 + is-docker@3.0.0: {} + is-fullwidth-code-point@3.0.0: {} + is-inside-container@1.0.0: + dependencies: + is-docker: 3.0.0 + is-potential-custom-element-name@1.0.1: {} + is-wsl@3.1.1: + dependencies: + is-inside-container: 1.0.0 + isexe@2.0.0: {} isomorphic.js@0.2.5: {} @@ -3295,6 +4208,8 @@ snapshots: json5@2.2.3: {} + jsonc-parser@3.3.1: {} + jsonfile@6.2.1: dependencies: universalify: 2.0.1 @@ -3327,6 +4242,8 @@ snapshots: lru-cache@10.4.3: {} + lru-cache@11.5.2: {} + lru-cache@5.1.1: dependencies: yallist: 3.1.1 @@ -3378,6 +4295,8 @@ snapshots: dependencies: brace-expansion: 5.0.9 + minimist@1.2.8: {} + minipass@7.1.3: {} mlly@1.8.2: @@ -3397,8 +4316,62 @@ snapshots: nwsapi@2.2.24: {} + open@10.2.0: + dependencies: + default-browser: 5.5.0 + define-lazy-prop: 3.0.0 + is-inside-container: 1.0.0 + wsl-utils: 0.1.0 + orderedmap@2.1.1: {} + oxc-parser@0.127.0: + dependencies: + '@oxc-project/types': 0.127.0 + optionalDependencies: + '@oxc-parser/binding-android-arm-eabi': 0.127.0 + '@oxc-parser/binding-android-arm64': 0.127.0 + '@oxc-parser/binding-darwin-arm64': 0.127.0 + '@oxc-parser/binding-darwin-x64': 0.127.0 + '@oxc-parser/binding-freebsd-x64': 0.127.0 + '@oxc-parser/binding-linux-arm-gnueabihf': 0.127.0 + '@oxc-parser/binding-linux-arm-musleabihf': 0.127.0 + '@oxc-parser/binding-linux-arm64-gnu': 0.127.0 + '@oxc-parser/binding-linux-arm64-musl': 0.127.0 + '@oxc-parser/binding-linux-ppc64-gnu': 0.127.0 + '@oxc-parser/binding-linux-riscv64-gnu': 0.127.0 + '@oxc-parser/binding-linux-riscv64-musl': 0.127.0 + '@oxc-parser/binding-linux-s390x-gnu': 0.127.0 + '@oxc-parser/binding-linux-x64-gnu': 0.127.0 + '@oxc-parser/binding-linux-x64-musl': 0.127.0 + '@oxc-parser/binding-openharmony-arm64': 0.127.0 + '@oxc-parser/binding-wasm32-wasi': 0.127.0 + '@oxc-parser/binding-win32-arm64-msvc': 0.127.0 + '@oxc-parser/binding-win32-ia32-msvc': 0.127.0 + '@oxc-parser/binding-win32-x64-msvc': 0.127.0 + + oxc-resolver@11.24.2: + optionalDependencies: + '@oxc-resolver/binding-android-arm-eabi': 11.24.2 + '@oxc-resolver/binding-android-arm64': 11.24.2 + '@oxc-resolver/binding-darwin-arm64': 11.24.2 + '@oxc-resolver/binding-darwin-x64': 11.24.2 + '@oxc-resolver/binding-freebsd-x64': 11.24.2 + '@oxc-resolver/binding-linux-arm-gnueabihf': 11.24.2 + '@oxc-resolver/binding-linux-arm-musleabihf': 11.24.2 + '@oxc-resolver/binding-linux-arm64-gnu': 11.24.2 + '@oxc-resolver/binding-linux-arm64-musl': 11.24.2 + '@oxc-resolver/binding-linux-ppc64-gnu': 11.24.2 + '@oxc-resolver/binding-linux-riscv64-gnu': 11.24.2 + '@oxc-resolver/binding-linux-riscv64-musl': 11.24.2 + '@oxc-resolver/binding-linux-s390x-gnu': 11.24.2 + '@oxc-resolver/binding-linux-x64-gnu': 11.24.2 + '@oxc-resolver/binding-linux-x64-musl': 11.24.2 + '@oxc-resolver/binding-openharmony-arm64': 11.24.2 + '@oxc-resolver/binding-wasm32-wasi': 11.24.2 + '@oxc-resolver/binding-win32-arm64-msvc': 11.24.2 + '@oxc-resolver/binding-win32-x64-msvc': 11.24.2 + package-json-from-dist@1.0.1: {} parse5@7.3.0: @@ -3416,6 +4389,11 @@ snapshots: lru-cache: 10.4.3 minipass: 7.1.3 + path-scurry@2.0.2: + dependencies: + lru-cache: 11.5.2 + minipass: 7.1.3 + pathe@2.0.3: {} pathval@2.0.1: {} @@ -3557,6 +4535,25 @@ snapshots: quansync@0.2.11: {} + react-docgen-typescript@2.4.0(typescript@5.9.3): + dependencies: + typescript: 5.9.3 + + react-docgen@8.0.3: + dependencies: + '@babel/core': 7.29.7 + '@babel/traverse': 7.29.7 + '@babel/types': 7.29.7 + '@types/babel__core': 7.20.5 + '@types/babel__traverse': 7.28.0 + '@types/doctrine': 0.0.9 + '@types/resolve': 1.20.6 + doctrine: 3.0.0 + resolve: 1.22.12 + strip-indent: 4.1.1 + transitivePeerDependencies: + - supports-color + react-dom@18.3.1(react@18.3.1): dependencies: loose-envify: 1.4.0 @@ -3571,6 +4568,14 @@ snapshots: dependencies: loose-envify: 1.4.0 + recast@0.23.21: + dependencies: + ast-types: 0.16.1 + esprima: 4.0.1 + source-map: 0.6.1 + tiny-invariant: 1.3.3 + tslib: 2.8.1 + redent@3.0.0: dependencies: indent-string: 4.0.0 @@ -3622,6 +4627,8 @@ snapshots: rrweb-cssom@0.8.0: {} + run-applescript@7.1.0: {} + safer-buffer@2.1.2: {} saxes@6.0.0: @@ -3658,6 +4665,32 @@ snapshots: std-env@3.10.0: {} + storybook@10.5.8(@types/react@18.3.31)(react@18.3.1): + dependencies: + '@storybook/global': 5.0.0 + '@storybook/icons': 2.1.0(react@18.3.1) + '@testing-library/dom': 10.4.1 + '@testing-library/jest-dom': 6.9.1 + '@testing-library/user-event': 14.6.1(@testing-library/dom@10.4.1) + '@vitest/expect': 3.2.4 + '@vitest/spy': 3.2.4 + '@webcontainer/env': 1.1.1 + esbuild: 0.25.12 + jsonc-parser: 3.3.1 + open: 10.2.0 + oxc-parser: 0.127.0 + oxc-resolver: 11.24.2 + recast: 0.23.21 + semver: 7.8.5 + use-sync-external-store: 1.6.0(react@18.3.1) + ws: 8.21.3 + optionalDependencies: + '@types/react': 18.3.31 + transitivePeerDependencies: + - bufferutil + - react + - utf-8-validate + string-argv@0.3.2: {} string-width@4.2.3: @@ -3680,10 +4713,14 @@ snapshots: dependencies: ansi-regex: 6.2.2 + strip-bom@3.0.0: {} + strip-indent@3.0.0: dependencies: min-indent: 1.0.1 + strip-indent@4.1.1: {} + strip-literal@3.1.0: dependencies: js-tokens: 9.0.1 @@ -3706,6 +4743,8 @@ snapshots: glob: 10.5.0 minimatch: 10.2.5 + tiny-invariant@1.3.3: {} + tinybench@2.9.0: {} tinyexec@0.3.2: {} @@ -3739,6 +4778,16 @@ snapshots: dependencies: punycode: 2.3.1 + ts-dedent@2.3.0: {} + + tsconfig-paths@4.2.0: + dependencies: + json5: 2.2.3 + minimist: 1.2.8 + strip-bom: 3.0.0 + + tslib@2.8.1: {} + turndown-plugin-gfm@1.0.2: {} turndown@7.2.4: @@ -3755,6 +4804,13 @@ snapshots: universalify@2.0.1: {} + unplugin@2.3.11: + dependencies: + '@jridgewell/remapping': 2.3.5 + acorn: 8.17.0 + picomatch: 4.0.5 + webpack-virtual-modules: 0.6.2 + update-browserslist-db@1.2.3(browserslist@4.28.5): dependencies: browserslist: 4.28.5 @@ -3869,6 +4925,8 @@ snapshots: webidl-conversions@7.0.0: {} + webpack-virtual-modules@0.6.2: {} + whatwg-encoding@3.1.1: dependencies: iconv-lite: 0.6.3 @@ -3903,6 +4961,12 @@ snapshots: ws@8.21.0: {} + ws@8.21.3: {} + + wsl-utils@0.1.0: + dependencies: + is-wsl: 3.1.1 + xml-name-validator@5.0.0: {} xmlchars@2.2.0: {} diff --git a/src/designTokenDocumentation.test.ts b/src/designTokenDocumentation.test.ts new file mode 100644 index 00000000..235daca9 --- /dev/null +++ b/src/designTokenDocumentation.test.ts @@ -0,0 +1,36 @@ +import { readFileSync } from 'node:fs'; +import { resolve } from 'node:path'; + +import { describe, expect, it } from 'vitest'; + +const repositoryFile = (path: string): string => + readFileSync(resolve(process.cwd(), path), 'utf8'); + +const normalize = (value: string): string => value.replace(/\s+/gu, ' ').trim(); + +describe('editor design-token documentation contract', () => { + it('tells hosts how to re-theme repeating chrome without editing internals', () => { + const operatorGuide = normalize(repositoryFile('docs/design-tokens.md')); + const inventory = normalize(repositoryFile('docs/storybook-inventory.md')); + const doctoring = repositoryFile('docs/doctoring/editor-design-tokens.md'); + const changelog = normalize(repositoryFile('CHANGELOG.md')); + const index = repositoryFile('docs/README.md'); + const adrIndex = repositoryFile('docs/adr/README.md'); + + expect(index).toContain('[`design-tokens.md`](design-tokens.md)'); + expect(adrIndex).toContain('[0027](0027-editor-design-tokens-storybook.md) | Proposed'); + expect(operatorGuide).toContain('Override the named custom properties on `.cwl-editor`'); + expect(operatorGuide).toContain('Do not edit Inkspan internals'); + expect(inventory).toContain('Editor Chrome / Toolbar Button States'); + expect(inventory).toContain('Override `--cwl-accent`'); + expect(doctoring).toContain('Design Tokens Format Module 2025.10'); + expect(doctoring).toContain( + 'https://www.w3.org/community/reports/design-tokens/CG-FINAL-format-20251028/', + ); + expect(doctoring).toContain('https://www.w3.org/TR/WCAG22/'); + expect(doctoring).toContain('https://storybook.js.org/docs/get-started/frameworks/react-vite'); + expect(changelog).toContain('Named the repeating editor chrome as a host-facing theme-token catalog'); + expect(repositoryFile('.storybook/main.ts')).toContain("@storybook/react-vite"); + expect(repositoryFile('stories/EditorChrome.stories.tsx')).toContain('cwl-tb-btn'); + }); +}); diff --git a/src/designTokens.test.ts b/src/designTokens.test.ts new file mode 100644 index 00000000..0c707803 --- /dev/null +++ b/src/designTokens.test.ts @@ -0,0 +1,73 @@ +import { readFileSync } from 'node:fs'; +import { resolve } from 'node:path'; + +import { describe, expect, it } from 'vitest'; + +import { + EditorThemeTokenError, + getEditorThemeToken, + listEditorThemeTokens, + toDesignTokenFormatGroup, +} from './designTokens.js'; + +const stylesheet = readFileSync(resolve(process.cwd(), 'src/styles.css'), 'utf8'); + +describe('editor theme token catalog', () => { + it('lists every shipped .cwl-editor custom property hosts can override', () => { + const tokens = listEditorThemeTokens(); + const names = tokens.map((token) => token.name); + + expect(names).toEqual([ + 'cwl-fg', + 'cwl-muted', + 'cwl-border', + 'cwl-bg', + 'cwl-surface', + 'cwl-accent', + 'cwl-accent-soft', + 'cwl-radius', + 'cwl-font', + ]); + + for (const token of tokens) { + expect(stylesheet).toContain(`${token.cssCustomProperty}:`); + expect(token.hostAction).toContain('Override'); + expect(token.hostAction).toContain('.cwl-editor'); + expect(token.hostAction).toContain(token.cssCustomProperty); + } + }); + + it('keeps light, dark, and forced-colors color values aligned with the stylesheet', () => { + const foreground = getEditorThemeToken('cwl-fg'); + + expect(foreground.role).toBe('color'); + expect(foreground.lightValue).toBe('#1f2328'); + expect(foreground.darkValue).toBe('#e6edf3'); + expect(foreground.forcedColorsValue).toBe('#000000'); + expect(stylesheet).toContain(`${foreground.cssCustomProperty}: ${foreground.lightValue};`); + expect(stylesheet).toContain(`${foreground.cssCustomProperty}: ${foreground.darkValue};`); + expect(stylesheet).toContain( + `${foreground.cssCustomProperty}: ${foreground.forcedColorsValue};`, + ); + }); + + it('rejects unknown token names without reflecting caller input', () => { + expect(() => getEditorThemeToken('not-a-theme-token')).toThrow(EditorThemeTokenError); + expect(() => getEditorThemeToken('not-a-theme-token')).toThrow( + /unknown editor theme token/iu, + ); + expect(() => getEditorThemeToken('not-a-theme-token')).not.toThrow(/not-a-theme-token/u); + }); + + it('emits a DTCG 2025.10 group that hosts can copy into a theme file', () => { + const group = toDesignTokenFormatGroup(); + const foreground = group.cwl.fg; + + expect(foreground.$type).toBe('color'); + expect(foreground.$value).toBe('#1f2328'); + expect(foreground.$description).toContain('Override --cwl-fg on .cwl-editor'); + expect(group.cwl.radius.$type).toBe('dimension'); + expect(group.cwl.font.$type).toBe('fontFamily'); + expect(Array.isArray(group.cwl.font.$value)).toBe(true); + }); +}); diff --git a/src/designTokens.ts b/src/designTokens.ts new file mode 100644 index 00000000..99d7a873 --- /dev/null +++ b/src/designTokens.ts @@ -0,0 +1,173 @@ +/** + * Host-facing catalog of the repeating editor chrome tokens shipped in + * `src/styles.css`. + * + * The stylesheet remains the runtime presentation authority. This module names + * those custom properties, maps them to Design Tokens Format Module 2025.10 + * types, and tells a host which property to override on `.cwl-editor`. + */ + +export type EditorThemeTokenRole = 'color' | 'dimension' | 'fontFamily'; + +export type EditorThemeTokenName = + | 'cwl-fg' + | 'cwl-muted' + | 'cwl-border' + | 'cwl-bg' + | 'cwl-surface' + | 'cwl-accent' + | 'cwl-accent-soft' + | 'cwl-radius' + | 'cwl-font'; + +export interface EditorThemeToken { + readonly name: EditorThemeTokenName; + readonly cssCustomProperty: `--${EditorThemeTokenName}`; + readonly role: EditorThemeTokenRole; + readonly lightValue: string; + readonly darkValue?: string; + readonly forcedColorsValue?: string; + readonly hostAction: string; +} + +export interface DesignTokenFormatNode { + readonly $type: 'color' | 'dimension' | 'fontFamily'; + readonly $value: string | readonly string[]; + readonly $description: string; +} + +export interface DesignTokenFormatGroup { + readonly cwl: { + readonly fg: DesignTokenFormatNode; + readonly muted: DesignTokenFormatNode; + readonly border: DesignTokenFormatNode; + readonly bg: DesignTokenFormatNode; + readonly surface: DesignTokenFormatNode; + readonly accent: DesignTokenFormatNode; + readonly 'accent-soft': DesignTokenFormatNode; + readonly radius: DesignTokenFormatNode; + readonly font: DesignTokenFormatNode; + }; +} + +const ROLE_TO_FORMAT_TYPE = { + color: 'color', + dimension: 'dimension', + fontFamily: 'fontFamily', +} as const satisfies Record; + +/** Stable fail-closed error for unknown theme-token lookups. */ +export class EditorThemeTokenError extends Error { + readonly code = 'unknown_theme_token'; + + constructor() { + super('Unknown editor theme token.'); + this.name = 'EditorThemeTokenError'; + } +} + +function hostAction(cssCustomProperty: `--${EditorThemeTokenName}`): string { + return `Override ${cssCustomProperty} on .cwl-editor after checking WCAG 2.2 contrast against --cwl-bg. Do not edit Inkspan internals.`; +} + +function themeToken( + name: EditorThemeTokenName, + role: EditorThemeTokenRole, + lightValue: string, + darkValue?: string, + forcedColorsValue?: string, +): EditorThemeToken { + const cssCustomProperty = `--${name}` as const; + return Object.freeze({ + name, + cssCustomProperty, + role, + lightValue, + darkValue, + forcedColorsValue, + hostAction: hostAction(cssCustomProperty), + }); +} + +const EDITOR_THEME_TOKENS: readonly EditorThemeToken[] = Object.freeze([ + themeToken('cwl-fg', 'color', '#1f2328', '#e6edf3', '#000000'), + themeToken('cwl-muted', 'color', '#59636e', '#9198a1', '#444444'), + themeToken('cwl-border', 'color', '#d1d9e0', '#3d444d', '#999999'), + themeToken('cwl-bg', 'color', '#ffffff', '#0d1117', '#ffffff'), + themeToken('cwl-surface', 'color', '#f6f8fa', '#161b22', '#ffffff'), + themeToken('cwl-accent', 'color', '#0969da', '#4493f8', '#000000'), + themeToken('cwl-accent-soft', 'color', '#ddf4ff', '#163356', '#ffffff'), + themeToken('cwl-radius', 'dimension', '8px'), + themeToken( + 'cwl-font', + 'fontFamily', + "'Noto Sans', 'Noto Sans KR', 'Noto Sans JP', 'Noto Sans SC', 'Noto Sans TC', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif", + ), +]); + +const EDITOR_THEME_TOKEN_BY_NAME: ReadonlyMap = new Map( + EDITOR_THEME_TOKENS.map((token) => [token.name, token]), +); + +const TOKEN_GROUP_KEY: Record = { + 'cwl-fg': 'fg', + 'cwl-muted': 'muted', + 'cwl-border': 'border', + 'cwl-bg': 'bg', + 'cwl-surface': 'surface', + 'cwl-accent': 'accent', + 'cwl-accent-soft': 'accent-soft', + 'cwl-radius': 'radius', + 'cwl-font': 'font', +}; + +/** Return the frozen catalog of shipped editor theme tokens. */ +export function listEditorThemeTokens(): readonly EditorThemeToken[] { + return EDITOR_THEME_TOKENS; +} + +/** + * Return one shipped theme token by exact catalog name. + * + * @throws {EditorThemeTokenError} When the name is not a shipped token. + */ +export function getEditorThemeToken(name: string): EditorThemeToken { + const token = EDITOR_THEME_TOKEN_BY_NAME.get(name); + if (!token) { + throw new EditorThemeTokenError(); + } + return token; +} + +function formatTokenValue(token: EditorThemeToken): string | readonly string[] { + if (token.role !== 'fontFamily') { + return token.lightValue; + } + return token.lightValue.split(',').map((family) => family.trim().replace(/^['"]|['"]$/gu, '')); +} + +function toFormatNode(token: EditorThemeToken): DesignTokenFormatNode { + return Object.freeze({ + $type: ROLE_TO_FORMAT_TYPE[token.role], + $value: formatTokenValue(token), + $description: token.hostAction, + }); +} + +/** + * Return a Design Tokens Format Module 2025.10 group for the shipped chrome. + * + * This is an interchange snapshot of Inkspan's CSS custom properties. It is not + * a claim of complete DTCG conformance, Figma Variables sync, or WCAG contrast + * certification for host overrides. + */ +export function toDesignTokenFormatGroup(): DesignTokenFormatGroup { + const cwl = {} as DesignTokenFormatGroup['cwl']; + for (const token of EDITOR_THEME_TOKENS) { + Object.defineProperty(cwl, TOKEN_GROUP_KEY[token.name], { + value: toFormatNode(token), + enumerable: true, + }); + } + return Object.freeze({ cwl: Object.freeze(cwl) }); +} diff --git a/src/index.test.ts b/src/index.test.ts index 506096ad..28b51fa6 100644 --- a/src/index.test.ts +++ b/src/index.test.ts @@ -49,6 +49,13 @@ describe('package entry point', () => { expect(typeof api.htmlToPlainText).toBe('function'); }); + it('re-exports the editor theme token catalog', () => { + expect(typeof api.listEditorThemeTokens).toBe('function'); + expect(typeof api.getEditorThemeToken).toBe('function'); + expect(typeof api.toDesignTokenFormatGroup).toBe('function'); + expect(typeof api.EditorThemeTokenError).toBe('function'); + }); + it('re-exports the standalone base64 converter', () => { expect(typeof api.bytesToBase64).toBe('function'); expect(typeof api.base64ToBytes).toBe('function'); diff --git a/src/index.ts b/src/index.ts index f7905449..e0bb5bc1 100644 --- a/src/index.ts +++ b/src/index.ts @@ -166,6 +166,21 @@ export { } from './markdown/plainText.js'; export type { PlainTextOptions } from './markdown/plainText.js'; +// Host-facing editor chrome theme tokens (CSS remains runtime authority). +export { + EditorThemeTokenError, + getEditorThemeToken, + listEditorThemeTokens, + toDesignTokenFormatGroup, +} from './designTokens.js'; +export type { + DesignTokenFormatGroup, + DesignTokenFormatNode, + EditorThemeToken, + EditorThemeTokenName, + EditorThemeTokenRole, +} from './designTokens.js'; + // Standalone, framework-agnostic base64 / data-URI converter. export * from './converter/index.js'; export { diff --git a/stories/EditorChrome.stories.tsx b/stories/EditorChrome.stories.tsx new file mode 100644 index 00000000..1ed63663 --- /dev/null +++ b/stories/EditorChrome.stories.tsx @@ -0,0 +1,62 @@ +import type { Meta, StoryObj } from '@storybook/react'; + +import { listEditorThemeTokens } from '../src/designTokens.js'; +import '../src/styles.css'; + +const meta = { + title: 'Editor Chrome', + parameters: { + layout: 'centered', + }, +} satisfies Meta; + +export default meta; + +type Story = StoryObj; + +export const ToolbarButtonStates: Story = { + name: 'Toolbar Button States', + render: () => ( +
+
+
+ + +
+
+
+ ), +}; + +export const ThemeTokens: Story = { + name: 'Theme Tokens', + render: () => ( +
+

Override these tokens on .cwl-editor after checking WCAG 2.2 contrast.

+
    + {listEditorThemeTokens().map((token) => ( +
  • + {token.cssCustomProperty} + {token.role === 'color' ? ( +
  • + ))} +
+
+ ), +}; From df748103c087ef703dc7c633e9c3bace77d6b5b4 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Mon, 17 Aug 2026 00:51:38 +0900 Subject: [PATCH 02/67] fix(docs): renumber design-token ADR to 0031 --- .../0031-editor-design-tokens-storybook.md | 42 +++++++++++++++++++ 1 file changed, 42 insertions(+) create mode 100644 docs/adr/0031-editor-design-tokens-storybook.md diff --git a/docs/adr/0031-editor-design-tokens-storybook.md b/docs/adr/0031-editor-design-tokens-storybook.md new file mode 100644 index 00000000..4ea51cf8 --- /dev/null +++ b/docs/adr/0031-editor-design-tokens-storybook.md @@ -0,0 +1,42 @@ +# ADR 0031: Editor chrome design tokens and Storybook inventory + +Status: Proposed + +## Context + +Inkspan ships repeating toolbar buttons, groups, and editor chrome styled through `--cwl-*` custom properties. Hosts already re-theme by overriding those properties, but the names, light/dark/forced-colors values, and buyer next action lived only inside `src/styles.css`. There was no typed catalog, no Design Tokens Format Module interchange snapshot, and no Storybook inventory of the repeating objects. + +## Alternatives considered + +1. **Leave theming as undocumented CSS.** Rejected because hosts would keep reading internals and drift from the shipped token set. +2. **Generate CSS from a new token source of truth.** Rejected for this slice because it would rewrite the protected stylesheet and collide with print, forced-colors, and Office-unrelated presentation contracts. +3. **Add Figma Variables sync or a design-tool connector.** Rejected because Figma/design-tool credentials and sync authority are host-owned; Inkspan remains independently usable. +4. **Publish a named catalog plus Storybook inventory while CSS remains runtime authority.** Selected because hosts can override and preview repeating chrome without moving presentation authority or adding a design-tool runtime. + +## Decision + +Inkspan publishes a host-facing theme-token catalog for the nine shipped chrome tokens, a Design Tokens Format Module 2025.10 interchange snapshot, and a Storybook inventory of repeating toolbar/editor objects. Hosts override `--cwl-*` on `.cwl-editor` after checking WCAG 2.2 contrast. Unknown token names fail closed. No Figma, network, persistence, credential, or model authority is added. + +## Consequences and ownership trade-offs + +Hosts gain a copyable token list and a Storybook preview. Inkspan keeps CSS as the runtime source. naruon and other CWL hosts can apply the same overrides through host-owned brand CSS. Complete DTCG conformance, Figma Variables, and host WCAG certification remain out of scope. + +## Failure and recovery + +An unknown token name throws `EditorThemeTokenError` without reflecting caller input. A host override that fails contrast is recovered by changing only the named tokens, not by editing Inkspan internals or disabling forced-colors. + +## Security and privacy impact + +The catalog contains only public presentation values. It does not carry document bodies, tenant identifiers, credentials, or diagnostics. Storybook is a local development preview and is not a production transport. + +## Compatibility and migration + +The catalog is additive. Existing CSS overrides on `.cwl-editor` continue to work. A later CSS token addition must update the catalog, Storybook inventory, and this ADR together. + +## Verification and acceptance evidence + +Required evidence includes token-catalog tests against `src/styles.css`, documentation-contract tests, Storybook inventory stories for toolbar button states and token swatches, and exact-head CI/coverage/package/security gates on the unchanged head. This ADR stays Proposed until protected integration. + +## Rollback or supersession + +Rollback removes the catalog export, Storybook inventory/config, operator/doctoring records, and this ADR together. Supersession requires a new ADR if CSS ceases to be runtime presentation authority or if a design-tool sync contract is accepted. From 720befffb540470890bde6bfddc6d55702ff64b7 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Mon, 17 Aug 2026 00:52:03 +0900 Subject: [PATCH 03/67] fix(docs): index design-token ADR as 0031 --- docs/adr/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/adr/README.md b/docs/adr/README.md index 9dcf2787..b797812e 100644 --- a/docs/adr/README.md +++ b/docs/adr/README.md @@ -32,7 +32,7 @@ This index records durable architectural decisions. Protected-main implementatio | [0024](0024-bounded-docx-paragraph-alignment.md) | Accepted | Bounded paragraph alignment in deterministic DOCX output | | [0025](0025-bounded-docx-heading-alignment.md) | Accepted | Bounded heading alignment in deterministic DOCX output | | [0026](0026-bounded-docx-external-hyperlinks.md) | Accepted | Bounded external hyperlinks in deterministic DOCX rich text | -| [0027](0027-editor-design-tokens-storybook.md) | Proposed | Editor chrome design tokens and Storybook inventory | +| [0031](0031-editor-design-tokens-storybook.md) | Proposed | Editor chrome design tokens and Storybook inventory | ## Decision discipline From 89dd568894dd58355df0ebd4a657139243e21711 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Mon, 17 Aug 2026 00:52:22 +0900 Subject: [PATCH 04/67] test(docs): bind design-token contract to ADR 0031 --- src/designTokenDocumentation.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/designTokenDocumentation.test.ts b/src/designTokenDocumentation.test.ts index 235daca9..93fdb9a9 100644 --- a/src/designTokenDocumentation.test.ts +++ b/src/designTokenDocumentation.test.ts @@ -18,7 +18,7 @@ describe('editor design-token documentation contract', () => { const adrIndex = repositoryFile('docs/adr/README.md'); expect(index).toContain('[`design-tokens.md`](design-tokens.md)'); - expect(adrIndex).toContain('[0027](0027-editor-design-tokens-storybook.md) | Proposed'); + expect(adrIndex).toContain('[0031](0031-editor-design-tokens-storybook.md) | Proposed'); expect(operatorGuide).toContain('Override the named custom properties on `.cwl-editor`'); expect(operatorGuide).toContain('Do not edit Inkspan internals'); expect(inventory).toContain('Editor Chrome / Toolbar Button States'); From 89dcc2a40d0181d1eddb111f7f23a77408fa5a08 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Mon, 17 Aug 2026 00:53:40 +0900 Subject: [PATCH 05/67] fix(docs): align design-token fitness with ADR 0031 --- docs/DOCUMENTATION_FITNESS.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/DOCUMENTATION_FITNESS.md b/docs/DOCUMENTATION_FITNESS.md index 38d37131..46d9737f 100644 --- a/docs/DOCUMENTATION_FITNESS.md +++ b/docs/DOCUMENTATION_FITNESS.md @@ -56,7 +56,7 @@ Document fitness and implementation maturity are independent. A `present_current | Unified stable registry release train | ADR 0019, protected release workflow and release doctoring | `present_current` | `implemented_on_protected_main` | Stable npm/Office version equality, OIDC Trusted Publishing, exact-artifact publication and post-publication digest verification are source-integrated; live registry publication remains separate operational evidence. | | Framework-neutral Markdown package boundary | ADR 0020, protected `@contextualwisdomlab/cwl-editor/markdown` package subpath and shared policy modules from #114 | `present_current` | `implemented_on_protected_main` | Server/worker consumers can reuse deterministic Markdown/HTML/email/plain-text conversion without evaluating the React/TipTap editor graph. | | CSS paged-media print boundary | ADR 0021, protected `src/styles.css`, packaged stylesheet evidence and real-browser print tests from #116/#127 | `present_current` | `implemented_on_protected_main` | Declarative print output removes screen-only clipping/chrome while preserving authored content without creating a durable PDF service. | -| Editor chrome design tokens / Storybook inventory | ADR 0027, `docs/design-tokens.md`, doctoring, token catalog, and Storybook stories | `present_current` | `implemented_on_active_pr` | Hosts can name, override, and preview repeating toolbar/editor tokens without treating the interchange snapshot as shipped protected-main authority. | +| Editor chrome design tokens / Storybook inventory | ADR 0031, `docs/design-tokens.md`, doctoring, token catalog, and Storybook stories | `present_current` | `implemented_on_active_pr` | Hosts can name, override, and preview repeating toolbar/editor tokens without treating the interchange snapshot as shipped protected-main authority. | | Informative DOCX PNG figures | ADR 0022, Office schema/renderer/tests and guidance | `present_current` | `implemented_on_protected_main` | Strict bounded inline PNG figures preserve informative alternative text without remote-resource or arbitrary OOXML authority. | | DOCX bounded rich-text runs | ADR 0023, Office schema/renderer/tests and doctoring | `present_current` | `implemented_on_protected_main` | Ordered bold/italic/underline runs preserve common inline fidelity under one bounded deterministic contract. | | DOCX bounded paragraph alignment | ADR 0024, Office schema/renderer/tests, Office guidance and doctoring | `present_current` | `implemented_on_protected_main` | `paragraph` and `rich_paragraph` preserve explicit left/center/right/justify alignment while omission retains inherited/default behavior. | From d99cf3a18b31a0bbea3d0b18586f1c47e890a9ef Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sun, 16 Aug 2026 16:06:45 +0000 Subject: [PATCH 06/67] fix(ui): retire colliding token ADR 0027 and name print values Delete the leftover 0027 design-token ADR so this lane no longer collides with #141, #248, and #320. Catalog color remaps are print-media values, not forced-colors assignments, and Storybook now shows disabled, surface, and collaboration-status chrome as a class-level sample. Co-authored-by: Seongho Bae --- CHANGELOG.md | 2 +- docs/TRACEABILITY.md | 2 +- docs/TRD.md | 2 + docs/UML.md | 2 + .../0027-editor-design-tokens-storybook.md | 42 ----------------- .../0031-editor-design-tokens-storybook.md | 2 +- docs/doctoring/editor-design-tokens.md | 6 +-- docs/storybook-inventory.md | 4 +- src/designTokenDocumentation.test.ts | 22 ++++++++- src/designTokens.test.ts | 45 ++++++++++++++----- src/designTokens.ts | 6 +-- stories/EditorChrome.stories.tsx | 10 +++++ 12 files changed, 79 insertions(+), 66 deletions(-) delete mode 100644 docs/adr/0027-editor-design-tokens-storybook.md diff --git a/CHANGELOG.md b/CHANGELOG.md index 33d83d26..943ccc4e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,7 +7,7 @@ Historical release entries from **0.1.0 through 0.5.27** are preserved verbatim ## [Unreleased] ### Added -- Named the repeating editor chrome as a host-facing theme-token catalog and Storybook inventory so hosts can override `--cwl-*` custom properties on `.cwl-editor` after checking WCAG 2.2 contrast, without editing Inkspan internals. +- Named the repeating editor chrome as a host-facing theme-token catalog and Storybook inventory so hosts can override `--cwl-*` custom properties on `.cwl-editor` after checking WCAG 2.2 contrast, without editing Inkspan internals. Color catalog values now distinguish light, dark, and `@media print` remaps; forced-colors mode is not treated as a token assignment. ## [0.6.0] — 2026-08-10 diff --git a/docs/TRACEABILITY.md b/docs/TRACEABILITY.md index 1c830e7c..7f05cd22 100644 --- a/docs/TRACEABILITY.md +++ b/docs/TRACEABILITY.md @@ -21,7 +21,7 @@ This record maps durable Inkspan product decisions to authoritative standards, p | Browser clipboard behavior | Security-relevant rich HTML handling requires actual paste-pipeline integration and bounded semantic reconstruction before editor state | WHATWG HTML parsing; W3C Clipboard API | protected-main rich-clipboard unit/integration corpus and SafeClipboard ADR | Protected jsdom/TipTap integration success is not universal browser-engine conformance | | Cross-engine release assurance | The same committed synthetic adversarial corpus runs under required Chromium, Firefox, and WebKit projects; exact package-lock and packed npm artifact SHA-256 digests are required, and only focused standards-grounded safe differences may be admitted | WHATWG HTML Living Standard; W3C Clipboard API and events; Playwright 1.62 release notes and browser/project documentation | ADR 0016, protected-main browser evidence source/workflows, TEST_STRATEGY, OPERABILITY and UML | Protected-main implementation is the release-policy authority; every release candidate must regenerate fresh exact-source/lock/run/browser evidence bound to the exact packed npm artifact SHA-256 and does not claim byte-identical browser serialization or branded enterprise-policy coverage | | CSS paged-media output | Shipped editor CSS has a declarative print boundary that removes interactive chrome and screen clipping while preserving authored document flow and bounded fragmentation behavior | W3C Media Queries Level 3; CSS Fragmentation Level 3; CSS Paged Media Level 3 as tracked draft input | protected-main #116 packaged stylesheet, real-browser print-media evidence, ADR 0021, print doctoring and tests | `implemented_on_protected_main`; browser print styling does not create a durable PDF service, page-number/header authority, persistence, signing, or PDF-conformance claim | -| Editor chrome design tokens | Repeating toolbar/editor surfaces share named `--cwl-*` custom properties; hosts override those properties on `.cwl-editor` and preview them in Storybook | Design Tokens Format Module 2025.10; WCAG 2.2 contrast; Storybook React/Vite | Active-PR doctoring `docs/doctoring/editor-design-tokens.md`, operator guide, ADR 0027, token catalog tests, and Storybook inventory | Proposed until protected `main`; the interchange snapshot is not complete DTCG conformance, Figma Variables sync, or a host WCAG certification | +| Editor chrome design tokens | Repeating toolbar/editor surfaces share named `--cwl-*` custom properties; hosts override those properties on `.cwl-editor` and preview them in Storybook | Design Tokens Format Module 2025.10; WCAG 2.2 contrast; Storybook React/Vite | Active-PR doctoring `docs/doctoring/editor-design-tokens.md`, operator guide, ADR 0031, token catalog tests, and Storybook inventory | Proposed until protected `main`; the interchange snapshot is not complete DTCG conformance, Figma Variables sync, or a host WCAG certification | | Editor integration | Public behavior must exercise the actual TipTap/ProseMirror integration path, not an inert extension field or test-only hook | official TipTap and ProseMirror documentation for the locked dependency line | integration tests and package consumers | Inkspan does not claim compatibility with untested major-version integration semantics | | Collaboration | Inkspan provides provider-neutral editor/Yjs bindings; host owns provider lifecycle, room authorization, awareness privacy, persistence and audit | official Yjs/provider documentation plus Inkspan public contract | collaboration tests and architecture ownership matrix | No network-provider or tenant-authorization authority is implied | | Secure development | Security controls are developed test-first, with exact-head scanning/review/package evidence and root-cause regression | NIST SP 800-218 SSDF 1.1 | CI/security/SAST/package/provenance gates, doctoring and regression history | Repository evidence is not a claim of complete SSDF organizational conformance | diff --git a/docs/TRD.md b/docs/TRD.md index 80cec07a..460b9a2c 100644 --- a/docs/TRD.md +++ b/docs/TRD.md @@ -97,6 +97,8 @@ CSS paged-media print boundary is implemented on protected `main` under Accepted The CSS contract does not introduce JavaScript print orchestration, a PDF service, durable export authority, credentials, network access, identity, tenancy, persistence, page-number/header/footer generation, signature authority, or PDF-conformance claims. Browser/OS print destination and durable artifact policy remain host/user responsibilities. +A named editor-chrome theme-token catalog, DTCG 2025.10 interchange snapshot, and Storybook inventory are Active PR / Proposed. CSS remains runtime presentation authority. Hosts override `--cwl-*` on `.cwl-editor` after checking WCAG 2.2 contrast. The catalog's optional print values are the `@media print` remaps; forced-colors mode does not assign those custom properties. This slice adds no Figma, network, persistence, credential, or model authority. + ## Model-assisted authoring Inkspan does not own model credentials, provider routing, prompt retention, external-data-use approval, tenant disclosure policy, or model audit. A host may route assistance through contextual-orchestrator or another approved provider. Model output is untrusted proposed content and cannot bypass deterministic clipboard/document/conversion validation, user/host approval, or durable save authorization. diff --git a/docs/UML.md b/docs/UML.md index 5b67b163..4cf90c62 100644 --- a/docs/UML.md +++ b/docs/UML.md @@ -38,6 +38,8 @@ flowchart LR The host owns transport, authentication, authorization, tenant isolation, persistence, credentials, provider lifecycle, retention, deployment, durable audit, and model-use policy. Inkspan owns deterministic local editor/conversion/evidence behavior only. +A named editor-chrome theme-token catalog and Storybook inventory are Active PR / Proposed. Hosts override `--cwl-*` on `.cwl-editor` after checking WCAG 2.2 contrast. CSS remains runtime presentation authority. Storybook previews class-level chrome samples, not a mounted `Toolbar` or `CwlEditor`. Print-media remaps live in `@media print`; forced-colors mode does not assign those custom properties. Figma Variables, brand certification, and design-tool sync remain host-owned. + ## Rich paste sequence ```mermaid diff --git a/docs/adr/0027-editor-design-tokens-storybook.md b/docs/adr/0027-editor-design-tokens-storybook.md deleted file mode 100644 index 00faaf76..00000000 --- a/docs/adr/0027-editor-design-tokens-storybook.md +++ /dev/null @@ -1,42 +0,0 @@ -# ADR 0027: Editor chrome design tokens and Storybook inventory - -Status: Proposed - -## Context - -Inkspan ships repeating toolbar buttons, groups, and editor chrome styled through `--cwl-*` custom properties. Hosts already re-theme by overriding those properties, but the names, light/dark/forced-colors values, and buyer next action lived only inside `src/styles.css`. There was no typed catalog, no Design Tokens Format Module interchange snapshot, and no Storybook inventory of the repeating objects. - -## Alternatives considered - -1. **Leave theming as undocumented CSS.** Rejected because hosts would keep reading internals and drift from the shipped token set. -2. **Generate CSS from a new token source of truth.** Rejected for this slice because it would rewrite the protected stylesheet and collide with print, forced-colors, and Office-unrelated presentation contracts. -3. **Add Figma Variables sync or a design-tool connector.** Rejected because Figma/design-tool credentials and sync authority are host-owned; Inkspan remains independently usable. -4. **Publish a named catalog plus Storybook inventory while CSS remains runtime authority.** Selected because hosts can override and preview repeating chrome without moving presentation authority or adding a design-tool runtime. - -## Decision - -Inkspan publishes a host-facing theme-token catalog for the nine shipped chrome tokens, a Design Tokens Format Module 2025.10 interchange snapshot, and a Storybook inventory of repeating toolbar/editor objects. Hosts override `--cwl-*` on `.cwl-editor` after checking WCAG 2.2 contrast. Unknown token names fail closed. No Figma, network, persistence, credential, or model authority is added. - -## Consequences and ownership trade-offs - -Hosts gain a copyable token list and a Storybook preview. Inkspan keeps CSS as the runtime source. naruon and other CWL hosts can apply the same overrides through host-owned brand CSS. Complete DTCG conformance, Figma Variables, and host WCAG certification remain out of scope. - -## Failure and recovery - -An unknown token name throws `EditorThemeTokenError` without reflecting caller input. A host override that fails contrast is recovered by changing only the named tokens, not by editing Inkspan internals or disabling forced-colors. - -## Security and privacy impact - -The catalog contains only public presentation values. It does not carry document bodies, tenant identifiers, credentials, or diagnostics. Storybook is a local development preview and is not a production transport. - -## Compatibility and migration - -The catalog is additive. Existing CSS overrides on `.cwl-editor` continue to work. A later CSS token addition must update the catalog, Storybook inventory, and this ADR together. - -## Verification and acceptance evidence - -Required evidence includes token-catalog tests against `src/styles.css`, documentation-contract tests, Storybook inventory stories for toolbar button states and token swatches, and exact-head CI/coverage/package/security gates on the unchanged head. This ADR stays Proposed until protected integration. - -## Rollback or supersession - -Rollback removes the catalog export, Storybook inventory/config, operator/doctoring records, and this ADR together. Supersession requires a new ADR if CSS ceases to be runtime presentation authority or if a design-tool sync contract is accepted. diff --git a/docs/adr/0031-editor-design-tokens-storybook.md b/docs/adr/0031-editor-design-tokens-storybook.md index 4ea51cf8..4790519a 100644 --- a/docs/adr/0031-editor-design-tokens-storybook.md +++ b/docs/adr/0031-editor-design-tokens-storybook.md @@ -4,7 +4,7 @@ Status: Proposed ## Context -Inkspan ships repeating toolbar buttons, groups, and editor chrome styled through `--cwl-*` custom properties. Hosts already re-theme by overriding those properties, but the names, light/dark/forced-colors values, and buyer next action lived only inside `src/styles.css`. There was no typed catalog, no Design Tokens Format Module interchange snapshot, and no Storybook inventory of the repeating objects. +Inkspan ships repeating toolbar buttons, groups, and editor chrome styled through `--cwl-*` custom properties. Hosts already re-theme by overriding those properties, but the names, light/dark/print values, and buyer next action lived only inside `src/styles.css`. There was no typed catalog, no Design Tokens Format Module interchange snapshot, and no Storybook inventory of the repeating objects. ## Alternatives considered diff --git a/docs/doctoring/editor-design-tokens.md b/docs/doctoring/editor-design-tokens.md index a4adfa73..01ed21cd 100644 --- a/docs/doctoring/editor-design-tokens.md +++ b/docs/doctoring/editor-design-tokens.md @@ -28,15 +28,15 @@ WCAG 2.2 requires sufficient contrast for text and user-interface components (Wo ## Test-first evidence - RED: `src/designTokens.test.ts` failed because `./designTokens.js` did not exist. -- GREEN: the catalog lists the nine shipped tokens, aligns light/dark/forced-colors color values with `src/styles.css`, rejects unknown names without reflecting caller input, and emits a DTCG 2025.10 group. +- GREEN: the catalog lists the nine shipped tokens, aligns light/dark/print color values with the matching `src/styles.css` media blocks, rejects unknown names without reflecting caller input, and emits a DTCG 2025.10 group. ## Residual risk -Forced-colors and print media still apply after a host override. The font token snapshot splits a CSS font-family list and does not execute CSS. Storybook success is not Chromium/Firefox/WebKit release evidence. +Print media still remaps the color tokens after a host override. Forced-colors mode only restyles the toolbar focus outline to `CanvasText`; it does not assign `--cwl-*` values. The font token snapshot splits a CSS font-family list and does not execute CSS. Storybook success is not Chromium/Firefox/WebKit release evidence. ## Rollback -Rollback must remove the catalog export, this record, the operator guide, the Storybook inventory/config/stories, ADR 0027, the changelog entry, and the documentation-index rows together. +Rollback must remove the catalog export, this record, the operator guide, the Storybook inventory/config/stories, ADR 0031, the changelog entry, and the documentation-index rows together. ## References (APA 7th edition) diff --git a/docs/storybook-inventory.md b/docs/storybook-inventory.md index f2c4f71b..277ffdde 100644 --- a/docs/storybook-inventory.md +++ b/docs/storybook-inventory.md @@ -2,11 +2,11 @@ Status: Active PR / Proposed -Run `pnpm storybook` to preview repeating Inkspan chrome before you override tokens in a host. Each story uses the shipped `.cwl-*` classes so a token change is visible on the same objects buyers see. +Run `pnpm storybook` to preview repeating Inkspan chrome before you override tokens in a host. Each story uses the shipped `.cwl-*` classes so a token change is visible on the same class-level chrome sample buyers style. These stories do not mount `Toolbar` or `CwlEditor`. | Story | Repeating object | Host next action | | --- | --- | --- | -| `Editor Chrome / Toolbar Button States` | `.cwl-tb-btn` default and `is-active` | Override `--cwl-accent` and `--cwl-accent-soft` on `.cwl-editor` after checking WCAG 2.2 contrast | +| `Editor Chrome / Toolbar Button States` | `.cwl-tb-btn` default, `is-active`, and disabled, plus `.cwl-collaboration-status` and `.cwl-editor__surface` | Override `--cwl-accent` and `--cwl-accent-soft` on `.cwl-editor` after checking WCAG 2.2 contrast | | `Editor Chrome / Theme Tokens` | Color, radius, and font tokens | Copy `toDesignTokenFormatGroup()` into a host theme file; do not edit Inkspan internals | Storybook is a development preview. It does not authorize documents, persist content, or replace packed-package evidence. diff --git a/src/designTokenDocumentation.test.ts b/src/designTokenDocumentation.test.ts index 93fdb9a9..fdfe0c81 100644 --- a/src/designTokenDocumentation.test.ts +++ b/src/designTokenDocumentation.test.ts @@ -1,10 +1,12 @@ -import { readFileSync } from 'node:fs'; +import { existsSync, readFileSync } from 'node:fs'; import { resolve } from 'node:path'; import { describe, expect, it } from 'vitest'; +const repositoryPath = (path: string): string => resolve(process.cwd(), path); + const repositoryFile = (path: string): string => - readFileSync(resolve(process.cwd(), path), 'utf8'); + readFileSync(repositoryPath(path), 'utf8'); const normalize = (value: string): string => value.replace(/\s+/gu, ' ').trim(); @@ -19,6 +21,16 @@ describe('editor design-token documentation contract', () => { expect(index).toContain('[`design-tokens.md`](design-tokens.md)'); expect(adrIndex).toContain('[0031](0031-editor-design-tokens-storybook.md) | Proposed'); + expect(repositoryFile('docs/TRD.md')).toContain('theme-token catalog'); + expect(repositoryFile('docs/UML.md')).toContain('Active PR / Proposed'); + expect(repositoryFile('docs/UML.md')).toContain('--cwl-*'); + expect(existsSync(repositoryPath('docs/adr/0027-editor-design-tokens-storybook.md'))).toBe( + false, + ); + expect(repositoryFile('docs/TRACEABILITY.md')).toContain('ADR 0031'); + expect(repositoryFile('docs/TRACEABILITY.md')).not.toContain('ADR 0027'); + expect(doctoring).toContain('ADR 0031'); + expect(doctoring).not.toContain('ADR 0027'); expect(operatorGuide).toContain('Override the named custom properties on `.cwl-editor`'); expect(operatorGuide).toContain('Do not edit Inkspan internals'); expect(inventory).toContain('Editor Chrome / Toolbar Button States'); @@ -32,5 +44,11 @@ describe('editor design-token documentation contract', () => { expect(changelog).toContain('Named the repeating editor chrome as a host-facing theme-token catalog'); expect(repositoryFile('.storybook/main.ts')).toContain("@storybook/react-vite"); expect(repositoryFile('stories/EditorChrome.stories.tsx')).toContain('cwl-tb-btn'); + expect(repositoryFile('stories/EditorChrome.stories.tsx')).toContain('disabled'); + expect(repositoryFile('stories/EditorChrome.stories.tsx')).toContain('cwl-editor__surface'); + expect(repositoryFile('stories/EditorChrome.stories.tsx')).toContain( + 'cwl-collaboration-status', + ); + expect(inventory).toContain('class-level chrome sample'); }); }); diff --git a/src/designTokens.test.ts b/src/designTokens.test.ts index 0c707803..80a18524 100644 --- a/src/designTokens.test.ts +++ b/src/designTokens.test.ts @@ -12,6 +12,27 @@ import { const stylesheet = readFileSync(resolve(process.cwd(), 'src/styles.css'), 'utf8'); +function mediaBlock(css: string, query: string): string { + const marker = `@media ${query}`; + const start = css.indexOf(marker); + expect(start).toBeGreaterThan(-1); + const open = css.indexOf('{', start); + let depth = 0; + for (let index = open; index < css.length; index += 1) { + const character = css[index]; + if (character === '{') { + depth += 1; + } + if (character === '}') { + depth -= 1; + if (depth === 0) { + return css.slice(open + 1, index); + } + } + } + throw new Error(`Unclosed media query: ${query}`); +} + describe('editor theme token catalog', () => { it('lists every shipped .cwl-editor custom property hosts can override', () => { const tokens = listEditorThemeTokens(); @@ -37,18 +58,20 @@ describe('editor theme token catalog', () => { } }); - it('keeps light, dark, and forced-colors color values aligned with the stylesheet', () => { - const foreground = getEditorThemeToken('cwl-fg'); + it('keeps light, dark, and print color values inside the matching stylesheet media blocks', () => { + const printBlock = mediaBlock(stylesheet, 'print'); + const darkBlock = mediaBlock(stylesheet, '(prefers-color-scheme: dark)'); + const forcedColorsBlock = mediaBlock(stylesheet, '(forced-colors: active)'); + const colorTokens = listEditorThemeTokens().filter((token) => token.role === 'color'); - expect(foreground.role).toBe('color'); - expect(foreground.lightValue).toBe('#1f2328'); - expect(foreground.darkValue).toBe('#e6edf3'); - expect(foreground.forcedColorsValue).toBe('#000000'); - expect(stylesheet).toContain(`${foreground.cssCustomProperty}: ${foreground.lightValue};`); - expect(stylesheet).toContain(`${foreground.cssCustomProperty}: ${foreground.darkValue};`); - expect(stylesheet).toContain( - `${foreground.cssCustomProperty}: ${foreground.forcedColorsValue};`, - ); + expect(forcedColorsBlock).toContain('outline-color: CanvasText'); + + for (const token of colorTokens) { + expect(stylesheet).toContain(`${token.cssCustomProperty}: ${token.lightValue};`); + expect(darkBlock).toContain(`${token.cssCustomProperty}: ${token.darkValue};`); + expect(printBlock).toContain(`${token.cssCustomProperty}: ${token.printValue};`); + expect(forcedColorsBlock).not.toContain(`${token.cssCustomProperty}:`); + } }); it('rejects unknown token names without reflecting caller input', () => { diff --git a/src/designTokens.ts b/src/designTokens.ts index 99d7a873..c12241bd 100644 --- a/src/designTokens.ts +++ b/src/designTokens.ts @@ -26,7 +26,7 @@ export interface EditorThemeToken { readonly role: EditorThemeTokenRole; readonly lightValue: string; readonly darkValue?: string; - readonly forcedColorsValue?: string; + readonly printValue?: string; readonly hostAction: string; } @@ -75,7 +75,7 @@ function themeToken( role: EditorThemeTokenRole, lightValue: string, darkValue?: string, - forcedColorsValue?: string, + printValue?: string, ): EditorThemeToken { const cssCustomProperty = `--${name}` as const; return Object.freeze({ @@ -84,7 +84,7 @@ function themeToken( role, lightValue, darkValue, - forcedColorsValue, + printValue, hostAction: hostAction(cssCustomProperty), }); } diff --git a/stories/EditorChrome.stories.tsx b/stories/EditorChrome.stories.tsx index 1ed63663..c64f5757 100644 --- a/stories/EditorChrome.stories.tsx +++ b/stories/EditorChrome.stories.tsx @@ -26,6 +26,16 @@ export const ToolbarButtonStates: Story = { + + + +
Idle
+
+
+ Preview the same `.cwl-*` classes buyers ship. This is a class-level + chrome sample, not a mounted Toolbar or CwlEditor.
From 7f734e5ff352b94c2d24044c341daeeb4d4f3d67 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sun, 16 Aug 2026 16:09:03 +0000 Subject: [PATCH 07/67] feat(ui): mount shipped Toolbar in Storybook token inventory Add a Live Toolbar story that constructs the real TipTap toolbar so hosts can preview token overrides on the same buttons buyers ship, while the class-level sample stays available and CwlEditor stays unmounted. Co-authored-by: Seongho Bae --- docs/UML.md | 2 +- docs/doctoring/editor-design-tokens.md | 2 +- docs/storybook-inventory.md | 3 +- src/designTokenDocumentation.test.ts | 5 ++++ stories/EditorChrome.stories.tsx | 41 ++++++++++++++++++++++++++ 5 files changed, 50 insertions(+), 3 deletions(-) diff --git a/docs/UML.md b/docs/UML.md index 4cf90c62..566fbbe6 100644 --- a/docs/UML.md +++ b/docs/UML.md @@ -38,7 +38,7 @@ flowchart LR The host owns transport, authentication, authorization, tenant isolation, persistence, credentials, provider lifecycle, retention, deployment, durable audit, and model-use policy. Inkspan owns deterministic local editor/conversion/evidence behavior only. -A named editor-chrome theme-token catalog and Storybook inventory are Active PR / Proposed. Hosts override `--cwl-*` on `.cwl-editor` after checking WCAG 2.2 contrast. CSS remains runtime presentation authority. Storybook previews class-level chrome samples, not a mounted `Toolbar` or `CwlEditor`. Print-media remaps live in `@media print`; forced-colors mode does not assign those custom properties. Figma Variables, brand certification, and design-tool sync remain host-owned. +A named editor-chrome theme-token catalog and Storybook inventory are Active PR / Proposed. Hosts override `--cwl-*` on `.cwl-editor` after checking WCAG 2.2 contrast. CSS remains runtime presentation authority. Storybook previews a class-level chrome sample and a live shipped Toolbar. It does not mount `CwlEditor`. Print-media remaps live in `@media print`; forced-colors mode does not assign those custom properties. Figma Variables, brand certification, and design-tool sync remain host-owned. ## Rich paste sequence diff --git a/docs/doctoring/editor-design-tokens.md b/docs/doctoring/editor-design-tokens.md index 01ed21cd..c9e71854 100644 --- a/docs/doctoring/editor-design-tokens.md +++ b/docs/doctoring/editor-design-tokens.md @@ -16,7 +16,7 @@ If contrast fails after a re-theme, override only the named tokens on `.cwl-edit 1. Keep `src/styles.css` as runtime presentation authority. 2. Publish `listEditorThemeTokens()` / `getEditorThemeToken()` / `toDesignTokenFormatGroup()` as a host-facing catalog of the nine shipped chrome tokens. 3. Reject unknown token names with a stable payload-redacted `EditorThemeTokenError`. -4. Preview repeating `.cwl-tb-btn` and token swatches in Storybook. +4. Preview repeating `.cwl-tb-btn` states, the shipped Toolbar, and token swatches in Storybook. 5. Add no network, persistence, credential, model, tenant, Figma, or design-tool sync authority. ## Standards rationale diff --git a/docs/storybook-inventory.md b/docs/storybook-inventory.md index 277ffdde..8833303d 100644 --- a/docs/storybook-inventory.md +++ b/docs/storybook-inventory.md @@ -2,11 +2,12 @@ Status: Active PR / Proposed -Run `pnpm storybook` to preview repeating Inkspan chrome before you override tokens in a host. Each story uses the shipped `.cwl-*` classes so a token change is visible on the same class-level chrome sample buyers style. These stories do not mount `Toolbar` or `CwlEditor`. +Run `pnpm storybook` to preview repeating Inkspan chrome before you override tokens in a host. The button-state story is a class-level chrome sample. **Editor Chrome / Live Toolbar** mounts the shipped Toolbar so a token change is visible on the same objects buyers ship. These stories do not mount `CwlEditor`. | Story | Repeating object | Host next action | | --- | --- | --- | | `Editor Chrome / Toolbar Button States` | `.cwl-tb-btn` default, `is-active`, and disabled, plus `.cwl-collaboration-status` and `.cwl-editor__surface` | Override `--cwl-accent` and `--cwl-accent-soft` on `.cwl-editor` after checking WCAG 2.2 contrast | +| `Editor Chrome / Live Toolbar` | shipped Toolbar buttons, groups, and disabled/active states | Override `--cwl-accent` and `--cwl-accent-soft` on `.cwl-editor` after checking WCAG 2.2 contrast | | `Editor Chrome / Theme Tokens` | Color, radius, and font tokens | Copy `toDesignTokenFormatGroup()` into a host theme file; do not edit Inkspan internals | Storybook is a development preview. It does not authorize documents, persist content, or replace packed-package evidence. diff --git a/src/designTokenDocumentation.test.ts b/src/designTokenDocumentation.test.ts index fdfe0c81..a11a1610 100644 --- a/src/designTokenDocumentation.test.ts +++ b/src/designTokenDocumentation.test.ts @@ -34,6 +34,8 @@ describe('editor design-token documentation contract', () => { expect(operatorGuide).toContain('Override the named custom properties on `.cwl-editor`'); expect(operatorGuide).toContain('Do not edit Inkspan internals'); expect(inventory).toContain('Editor Chrome / Toolbar Button States'); + expect(inventory).toContain('Editor Chrome / Live Toolbar'); + expect(inventory).toContain('shipped Toolbar'); expect(inventory).toContain('Override `--cwl-accent`'); expect(doctoring).toContain('Design Tokens Format Module 2025.10'); expect(doctoring).toContain( @@ -49,6 +51,9 @@ describe('editor design-token documentation contract', () => { expect(repositoryFile('stories/EditorChrome.stories.tsx')).toContain( 'cwl-collaboration-status', ); + expect(repositoryFile('stories/EditorChrome.stories.tsx')).toContain( + "from '../src/components/Toolbar.js'", + ); expect(inventory).toContain('class-level chrome sample'); }); }); diff --git a/stories/EditorChrome.stories.tsx b/stories/EditorChrome.stories.tsx index c64f5757..48a44a75 100644 --- a/stories/EditorChrome.stories.tsx +++ b/stories/EditorChrome.stories.tsx @@ -1,8 +1,44 @@ +import { useEffect, useState } from 'react'; import type { Meta, StoryObj } from '@storybook/react'; +import { Editor } from '@tiptap/react'; +import { Toolbar } from '../src/components/Toolbar.js'; import { listEditorThemeTokens } from '../src/designTokens.js'; +import { buildExtensions } from '../src/extensions/kit.js'; import '../src/styles.css'; +function LiveToolbarPreview() { + const [editor, setEditor] = useState(null); + + useEffect(() => { + const instance = new Editor({ + extensions: buildExtensions({ image: { maxDimension: 0 } }), + content: + '

Override tokens on .cwl-editor after checking WCAG 2.2 contrast.

', + }); + setEditor(instance); + return () => { + instance.destroy(); + }; + }, []); + + if (!editor) { + return

Preparing the shipped toolbar.

; + } + + return ( +
+ +
+
+ This story mounts the shipped Toolbar. Override `--cwl-*` on + `.cwl-editor`. It does not mount CwlEditor. +
+
+
+ ); +} + const meta = { title: 'Editor Chrome', parameters: { @@ -42,6 +78,11 @@ export const ToolbarButtonStates: Story = { ), }; +export const LiveToolbar: Story = { + name: 'Live Toolbar', + render: () => , +}; + export const ThemeTokens: Story = { name: 'Theme Tokens', render: () => ( From c9615d97cb66f432e0a3f2becf6f2d84e6088941 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sun, 16 Aug 2026 16:11:00 +0000 Subject: [PATCH 08/67] feat(ui): publish WCAG contrast checks for editor chrome tokens Hosts can compare shipped color pairs and their own #rrggbb overrides against the WCAG 2.2 4.5:1 text threshold without reading CSS internals or treating the ratio as a certification. Co-authored-by: Seongho Bae --- CHANGELOG.md | 2 +- docs/design-tokens.md | 7 ++ docs/doctoring/editor-design-tokens.md | 4 +- src/designTokenDocumentation.test.ts | 2 + src/designTokens.test.ts | 43 ++++++++++ src/designTokens.ts | 104 +++++++++++++++++++++++++ src/index.test.ts | 3 + src/index.ts | 5 ++ 8 files changed, 167 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 943ccc4e..5d37ffa1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,7 +7,7 @@ Historical release entries from **0.1.0 through 0.5.27** are preserved verbatim ## [Unreleased] ### Added -- Named the repeating editor chrome as a host-facing theme-token catalog and Storybook inventory so hosts can override `--cwl-*` custom properties on `.cwl-editor` after checking WCAG 2.2 contrast, without editing Inkspan internals. Color catalog values now distinguish light, dark, and `@media print` remaps; forced-colors mode is not treated as a token assignment. +- Named the repeating editor chrome as a host-facing theme-token catalog and Storybook inventory so hosts can override `--cwl-*` custom properties on `.cwl-editor` after checking WCAG 2.2 contrast, without editing Inkspan internals. Color catalog values now distinguish light, dark, and `@media print` remaps; forced-colors mode is not treated as a token assignment. Hosts can call `getEditorThemeTokenContrast()` to compare shipped color pairs against the 4.5:1 text threshold. ## [0.6.0] — 2026-08-10 diff --git a/docs/design-tokens.md b/docs/design-tokens.md index 67a734b3..b7c78e9f 100644 --- a/docs/design-tokens.md +++ b/docs/design-tokens.md @@ -13,12 +13,19 @@ Use this catalog when you need to re-theme Inkspan's repeating toolbar and edito ```ts import { + contrastRatioFromHex, + getEditorThemeTokenContrast, listEditorThemeTokens, toDesignTokenFormatGroup, } from '@contextualwisdomlab/cwl-editor'; const tokens = listEditorThemeTokens(); const dtcgGroup = toDesignTokenFormatGroup(); +const contrast = getEditorThemeTokenContrast('cwl-fg', 'cwl-bg', 'light'); +const overrideRatio = contrastRatioFromHex('#0b6e4f', '#ffffff'); +if (contrast.ratio < 4.5 || overrideRatio < 4.5) { + throw new Error(contrast.hostAction); +} void tokens; void dtcgGroup; ``` diff --git a/docs/doctoring/editor-design-tokens.md b/docs/doctoring/editor-design-tokens.md index c9e71854..434f278d 100644 --- a/docs/doctoring/editor-design-tokens.md +++ b/docs/doctoring/editor-design-tokens.md @@ -14,7 +14,7 @@ If contrast fails after a re-theme, override only the named tokens on `.cwl-edit ## Decision 1. Keep `src/styles.css` as runtime presentation authority. -2. Publish `listEditorThemeTokens()` / `getEditorThemeToken()` / `toDesignTokenFormatGroup()` as a host-facing catalog of the nine shipped chrome tokens. +2. Publish `listEditorThemeTokens()` / `getEditorThemeToken()` / `getEditorThemeTokenContrast()` / `toDesignTokenFormatGroup()` as a host-facing catalog of the nine shipped chrome tokens. 3. Reject unknown token names with a stable payload-redacted `EditorThemeTokenError`. 4. Preview repeating `.cwl-tb-btn` states, the shipped Toolbar, and token swatches in Storybook. 5. Add no network, persistence, credential, model, tenant, Figma, or design-tool sync authority. @@ -28,7 +28,7 @@ WCAG 2.2 requires sufficient contrast for text and user-interface components (Wo ## Test-first evidence - RED: `src/designTokens.test.ts` failed because `./designTokens.js` did not exist. -- GREEN: the catalog lists the nine shipped tokens, aligns light/dark/print color values with the matching `src/styles.css` media blocks, rejects unknown names without reflecting caller input, and emits a DTCG 2025.10 group. +- GREEN: the catalog lists the nine shipped tokens, aligns light/dark/print color values with the matching `src/styles.css` media blocks, reports WCAG 2.2 contrast for shipped color pairs, rejects unknown names without reflecting caller input, and emits a DTCG 2025.10 group. ## Residual risk diff --git a/src/designTokenDocumentation.test.ts b/src/designTokenDocumentation.test.ts index a11a1610..d0e84018 100644 --- a/src/designTokenDocumentation.test.ts +++ b/src/designTokenDocumentation.test.ts @@ -32,6 +32,8 @@ describe('editor design-token documentation contract', () => { expect(doctoring).toContain('ADR 0031'); expect(doctoring).not.toContain('ADR 0027'); expect(operatorGuide).toContain('Override the named custom properties on `.cwl-editor`'); + expect(operatorGuide).toContain('getEditorThemeTokenContrast'); + expect(operatorGuide).toContain('contrastRatioFromHex'); expect(operatorGuide).toContain('Do not edit Inkspan internals'); expect(inventory).toContain('Editor Chrome / Toolbar Button States'); expect(inventory).toContain('Editor Chrome / Live Toolbar'); diff --git a/src/designTokens.test.ts b/src/designTokens.test.ts index 80a18524..5cd1fce5 100644 --- a/src/designTokens.test.ts +++ b/src/designTokens.test.ts @@ -4,8 +4,11 @@ import { resolve } from 'node:path'; import { describe, expect, it } from 'vitest'; import { + EditorThemeTokenContrastError, EditorThemeTokenError, + contrastRatioFromHex, getEditorThemeToken, + getEditorThemeTokenContrast, listEditorThemeTokens, toDesignTokenFormatGroup, } from './designTokens.js'; @@ -82,6 +85,46 @@ describe('editor theme token catalog', () => { expect(() => getEditorThemeToken('not-a-theme-token')).not.toThrow(/not-a-theme-token/u); }); + it('reports WCAG 2.2 contrast for shipped color pairs so hosts can check overrides', () => { + const light = getEditorThemeTokenContrast('cwl-fg', 'cwl-bg', 'light'); + const dark = getEditorThemeTokenContrast('cwl-fg', 'cwl-bg', 'dark'); + const print = getEditorThemeTokenContrast('cwl-fg', 'cwl-bg', 'print'); + const muted = getEditorThemeTokenContrast('cwl-muted', 'cwl-bg', 'light'); + + expect(light.ratio).toBeCloseTo(15.797619425332647, 8); + expect(dark.ratio).toBeCloseTo(16.016082890827004, 8); + expect(print.ratio).toBeCloseTo(21, 8); + expect(muted.ratio).toBeCloseTo(6.114136455475549, 8); + expect(light.ratio).toBeGreaterThanOrEqual(4.5); + expect(light.hostAction).toContain('Override --cwl-fg and --cwl-bg on .cwl-editor'); + expect(light.hostAction).toContain('WCAG 2.2'); + }); + + it('rejects contrast lookups that are not shipped color tokens without reflecting caller input', () => { + expect(() => getEditorThemeTokenContrast('not-a-theme-token', 'cwl-bg')).toThrow( + EditorThemeTokenError, + ); + expect(() => getEditorThemeTokenContrast('not-a-theme-token', 'cwl-bg')).not.toThrow( + /not-a-theme-token/u, + ); + expect(() => getEditorThemeTokenContrast('cwl-font', 'cwl-bg')).toThrow( + EditorThemeTokenContrastError, + ); + expect(() => getEditorThemeTokenContrast('cwl-font', 'cwl-bg')).toThrow( + /theme token contrast requires color tokens/iu, + ); + expect(() => getEditorThemeTokenContrast('cwl-font', 'cwl-bg')).not.toThrow(/cwl-font/u); + expect(() => + getEditorThemeTokenContrast('cwl-fg', 'cwl-bg', 'solar' as 'light'), + ).toThrow(EditorThemeTokenContrastError); + expect(contrastRatioFromHex('#1f2328', '#ffffff')).toBeCloseTo(15.797619425332647, 8); + expect(contrastRatioFromHex('#000000', '#ffffff')).toBeCloseTo(21, 8); + expect(() => contrastRatioFromHex('not-a-hex', '#ffffff')).toThrow( + EditorThemeTokenContrastError, + ); + expect(() => contrastRatioFromHex('not-a-hex', '#ffffff')).not.toThrow(/not-a-hex/u); + }); + it('emits a DTCG 2025.10 group that hosts can copy into a theme file', () => { const group = toDesignTokenFormatGroup(); const foreground = group.cwl.fg; diff --git a/src/designTokens.ts b/src/designTokens.ts index c12241bd..70274ffb 100644 --- a/src/designTokens.ts +++ b/src/designTokens.ts @@ -9,6 +9,8 @@ export type EditorThemeTokenRole = 'color' | 'dimension' | 'fontFamily'; +export type EditorThemeTokenScheme = 'light' | 'dark' | 'print'; + export type EditorThemeTokenName = | 'cwl-fg' | 'cwl-muted' @@ -30,6 +32,14 @@ export interface EditorThemeToken { readonly hostAction: string; } +export interface EditorThemeTokenContrast { + readonly foreground: EditorThemeTokenName; + readonly background: EditorThemeTokenName; + readonly scheme: EditorThemeTokenScheme; + readonly ratio: number; + readonly hostAction: string; +} + export interface DesignTokenFormatNode { readonly $type: 'color' | 'dimension' | 'fontFamily'; readonly $value: string | readonly string[]; @@ -66,6 +76,16 @@ export class EditorThemeTokenError extends Error { } } +/** Stable fail-closed error when contrast is requested for a non-color token. */ +export class EditorThemeTokenContrastError extends Error { + readonly code = 'non_color_theme_token'; + + constructor() { + super('Theme token contrast requires color tokens.'); + this.name = 'EditorThemeTokenContrastError'; + } +} + function hostAction(cssCustomProperty: `--${EditorThemeTokenName}`): string { return `Override ${cssCustomProperty} on .cwl-editor after checking WCAG 2.2 contrast against --cwl-bg. Do not edit Inkspan internals.`; } @@ -139,6 +159,90 @@ export function getEditorThemeToken(name: string): EditorThemeToken { return token; } +function srgbChannel(value: number): number { + const channel = value / 255; + return channel <= 0.04045 ? channel / 12.92 : ((channel + 0.055) / 1.055) ** 2.4; +} + +function relativeLuminance(hex: string): number { + const match = /^#([0-9a-f]{6})$/iu.exec(hex); + if (!match) { + throw new EditorThemeTokenContrastError(); + } + const integer = Number.parseInt(match[1], 16); + return ( + 0.2126 * srgbChannel((integer >> 16) & 0xff) + + 0.7152 * srgbChannel((integer >> 8) & 0xff) + + 0.0722 * srgbChannel(integer & 0xff) + ); +} + +/** + * Return the WCAG 2.2 contrast ratio for two `#rrggbb` colors. + * + * Use this after a host override so the new hex pair can be checked without + * editing Inkspan internals. The ratio is not a host WCAG certification. + * + * @throws {EditorThemeTokenContrastError} When either value is not `#rrggbb`. + */ +export function contrastRatioFromHex(left: string, right: string): number { + const first = relativeLuminance(left); + const second = relativeLuminance(right); + const lighter = Math.max(first, second); + const darker = Math.min(first, second); + return (lighter + 0.05) / (darker + 0.05); +} + +function colorValueForScheme( + token: EditorThemeToken, + scheme: EditorThemeTokenScheme, +): string { + switch (scheme) { + case 'light': + return token.lightValue; + case 'dark': + return token.darkValue!; + case 'print': + return token.printValue!; + default: { + const exhaustive: never = scheme; + void exhaustive; + throw new EditorThemeTokenContrastError(); + } + } +} + +/** + * Return the WCAG 2.2 contrast ratio for two shipped color tokens. + * + * Use this after a host override to decide whether `--cwl-fg` still meets + * 4.5:1 against `--cwl-bg`. The ratio is not a host WCAG certification. + * + * @throws {EditorThemeTokenError} When either name is not a shipped token. + * @throws {EditorThemeTokenContrastError} When either token is not a color. + */ +export function getEditorThemeTokenContrast( + foregroundName: string, + backgroundName: string, + scheme: EditorThemeTokenScheme = 'light', +): EditorThemeTokenContrast { + const foreground = getEditorThemeToken(foregroundName); + const background = getEditorThemeToken(backgroundName); + if (foreground.role !== 'color' || background.role !== 'color') { + throw new EditorThemeTokenContrastError(); + } + return Object.freeze({ + foreground: foreground.name, + background: background.name, + scheme, + ratio: contrastRatioFromHex( + colorValueForScheme(foreground, scheme), + colorValueForScheme(background, scheme), + ), + hostAction: `Override --${foreground.name} and --${background.name} on .cwl-editor after checking WCAG 2.2 contrast. Do not edit Inkspan internals.`, + }); +} + function formatTokenValue(token: EditorThemeToken): string | readonly string[] { if (token.role !== 'fontFamily') { return token.lightValue; diff --git a/src/index.test.ts b/src/index.test.ts index 28b51fa6..0ad7fa08 100644 --- a/src/index.test.ts +++ b/src/index.test.ts @@ -52,8 +52,11 @@ describe('package entry point', () => { it('re-exports the editor theme token catalog', () => { expect(typeof api.listEditorThemeTokens).toBe('function'); expect(typeof api.getEditorThemeToken).toBe('function'); + expect(typeof api.getEditorThemeTokenContrast).toBe('function'); + expect(typeof api.contrastRatioFromHex).toBe('function'); expect(typeof api.toDesignTokenFormatGroup).toBe('function'); expect(typeof api.EditorThemeTokenError).toBe('function'); + expect(typeof api.EditorThemeTokenContrastError).toBe('function'); }); it('re-exports the standalone base64 converter', () => { diff --git a/src/index.ts b/src/index.ts index e0bb5bc1..d54c99eb 100644 --- a/src/index.ts +++ b/src/index.ts @@ -168,8 +168,11 @@ export type { PlainTextOptions } from './markdown/plainText.js'; // Host-facing editor chrome theme tokens (CSS remains runtime authority). export { + EditorThemeTokenContrastError, EditorThemeTokenError, + contrastRatioFromHex, getEditorThemeToken, + getEditorThemeTokenContrast, listEditorThemeTokens, toDesignTokenFormatGroup, } from './designTokens.js'; @@ -177,8 +180,10 @@ export type { DesignTokenFormatGroup, DesignTokenFormatNode, EditorThemeToken, + EditorThemeTokenContrast, EditorThemeTokenName, EditorThemeTokenRole, + EditorThemeTokenScheme, } from './designTokens.js'; // Standalone, framework-agnostic base64 / data-URI converter. From 03a823d52fe0cc05d1a7431c79f15cad783593cc Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sun, 16 Aug 2026 16:26:04 +0000 Subject: [PATCH 09/67] fix(ui): disclose inventoried active-chrome contrast below 4.5:1 Point hosts at --cwl-accent on --cwl-accent-soft, publish meetsTextContrast for that pair, and add a :focus-visible Storybook sample so dark active toolbar text is not treated as certified. Co-authored-by: Seongho Bae --- CHANGELOG.md | 2 +- README.md | 4 +- docs/CONTRACTS.md | 2 +- docs/PRD.md | 2 +- docs/TRACEABILITY.md | 2 +- docs/TRD.md | 2 +- docs/UML.md | 2 +- .../0031-editor-design-tokens-storybook.md | 2 +- docs/design-tokens.md | 11 ++-- docs/doctoring/editor-design-tokens.md | 6 +-- docs/storybook-inventory.md | 2 +- src/designTokenDocumentation.test.ts | 6 +++ src/designTokens.test.ts | 30 +++++++++++ src/designTokens.ts | 51 +++++++++++++++---- src/index.test.ts | 2 + src/index.ts | 2 + stories/EditorChrome.stories.tsx | 9 +++- 17 files changed, 108 insertions(+), 29 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5d37ffa1..2d8162b1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,7 +7,7 @@ Historical release entries from **0.1.0 through 0.5.27** are preserved verbatim ## [Unreleased] ### Added -- Named the repeating editor chrome as a host-facing theme-token catalog and Storybook inventory so hosts can override `--cwl-*` custom properties on `.cwl-editor` after checking WCAG 2.2 contrast, without editing Inkspan internals. Color catalog values now distinguish light, dark, and `@media print` remaps; forced-colors mode is not treated as a token assignment. Hosts can call `getEditorThemeTokenContrast()` to compare shipped color pairs against the 4.5:1 text threshold. +- Named the repeating editor chrome as a host-facing theme-token catalog and Storybook inventory so hosts can override `--cwl-*` custom properties on `.cwl-editor` after checking WCAG 2.2 contrast, without editing Inkspan internals. Color catalog values now distinguish light, dark, and `@media print` remaps; forced-colors mode is not treated as a token assignment. Hosts can call `getEditorThemeTokenContrast()` to compare inventoried pairs, including `--cwl-accent` on `--cwl-accent-soft`, against the 4.5:1 text threshold via `meetsTextContrast`. ## [0.6.0] — 2026-08-10 diff --git a/README.md b/README.md index 67d108fb..110312d4 100644 --- a/README.md +++ b/README.md @@ -122,7 +122,9 @@ embed accepted images as inline data URIs. ### Theme the repeating chrome Override the named `--cwl-*` custom properties on `.cwl-editor` after checking -WCAG 2.2 contrast. Do not edit Inkspan internals. See +WCAG 2.2 contrast for body text and active toolbar text +(`getEditorThemeTokenContrast('cwl-accent', 'cwl-accent-soft', 'dark')`). Do not +edit Inkspan internals. See [`docs/design-tokens.md`](docs/design-tokens.md) for the token catalog and [`docs/storybook-inventory.md`](docs/storybook-inventory.md) for the Storybook preview of toolbar and editor chrome. diff --git a/docs/CONTRACTS.md b/docs/CONTRACTS.md index 8b79aea9..36c98871 100644 --- a/docs/CONTRACTS.md +++ b/docs/CONTRACTS.md @@ -152,7 +152,7 @@ Rollback must preserve readable canonical documents and must not require silentl | autosave | local ordering/state, callback contract, validator validation | transport, durable CAS, retry/offline policy, persistence | | collaboration | provider-neutral editor/Yjs binding | provider lifecycle, rooms, identity, authorization, persistence, awareness privacy | | Office rendering | deterministic bounded JSON→artifact conversion | file destination policy, downstream distribution, tenant authorization | -| editor chrome theming (Active PR / Proposed) | named `--cwl-*` tokens, DTCG interchange snapshot, Storybook inventory | host brand CSS, contrast certification, Figma Variables, design-tool sync | +| editor chrome theming (Active PR / Proposed) | named `--cwl-*` tokens, DTCG interchange snapshot, Storybook inventory, inventoried pair contrast including `--cwl-accent` on `--cwl-accent-soft` | host brand CSS, contrast certification, Figma Variables, design-tool sync | | naruon composition | stable local package/module boundary | authenticated compose transport, tenancy, provider/model policy | | model assistance | deterministic proposal acceptance boundary | provider, prompt/data policy, credentials, human approval | | release evidence | exact three-file draft inventory, package/artifact/digest verification and repository evidence | downstream deployment and operational rollout | diff --git a/docs/PRD.md b/docs/PRD.md index 62cbe523..3ccec07c 100644 --- a/docs/PRD.md +++ b/docs/PRD.md @@ -147,4 +147,4 @@ Protected `main` is the sole implemented baseline. Open PRs may describe Propose SafeClipboard, real Chromium/Firefox/WebKit release assurance, lifecycle observation, the root security disclosure lifecycle, toolbar shortcut accessibility metadata, SSR/native-form serialization, revision-scoped selection evidence, W3C text-position selector evidence, document-transition evidence, and envelope identity migration routing are implemented on protected `main`. -A named editor-chrome theme-token catalog, DTCG 2025.10 interchange snapshot, and Storybook inventory for repeating toolbar/editor objects are Active PR / Proposed and are not shipped claims until protected integration. +A named editor-chrome theme-token catalog, DTCG 2025.10 interchange snapshot, and Storybook inventory for repeating toolbar/editor objects are Active PR / Proposed and are not shipped claims until protected integration. Hosts must check inventoried active-chrome contrast (`--cwl-accent` on `--cwl-accent-soft`) in addition to body text. diff --git a/docs/TRACEABILITY.md b/docs/TRACEABILITY.md index 7f05cd22..ffe5d86d 100644 --- a/docs/TRACEABILITY.md +++ b/docs/TRACEABILITY.md @@ -21,7 +21,7 @@ This record maps durable Inkspan product decisions to authoritative standards, p | Browser clipboard behavior | Security-relevant rich HTML handling requires actual paste-pipeline integration and bounded semantic reconstruction before editor state | WHATWG HTML parsing; W3C Clipboard API | protected-main rich-clipboard unit/integration corpus and SafeClipboard ADR | Protected jsdom/TipTap integration success is not universal browser-engine conformance | | Cross-engine release assurance | The same committed synthetic adversarial corpus runs under required Chromium, Firefox, and WebKit projects; exact package-lock and packed npm artifact SHA-256 digests are required, and only focused standards-grounded safe differences may be admitted | WHATWG HTML Living Standard; W3C Clipboard API and events; Playwright 1.62 release notes and browser/project documentation | ADR 0016, protected-main browser evidence source/workflows, TEST_STRATEGY, OPERABILITY and UML | Protected-main implementation is the release-policy authority; every release candidate must regenerate fresh exact-source/lock/run/browser evidence bound to the exact packed npm artifact SHA-256 and does not claim byte-identical browser serialization or branded enterprise-policy coverage | | CSS paged-media output | Shipped editor CSS has a declarative print boundary that removes interactive chrome and screen clipping while preserving authored document flow and bounded fragmentation behavior | W3C Media Queries Level 3; CSS Fragmentation Level 3; CSS Paged Media Level 3 as tracked draft input | protected-main #116 packaged stylesheet, real-browser print-media evidence, ADR 0021, print doctoring and tests | `implemented_on_protected_main`; browser print styling does not create a durable PDF service, page-number/header authority, persistence, signing, or PDF-conformance claim | -| Editor chrome design tokens | Repeating toolbar/editor surfaces share named `--cwl-*` custom properties; hosts override those properties on `.cwl-editor` and preview them in Storybook | Design Tokens Format Module 2025.10; WCAG 2.2 contrast; Storybook React/Vite | Active-PR doctoring `docs/doctoring/editor-design-tokens.md`, operator guide, ADR 0031, token catalog tests, and Storybook inventory | Proposed until protected `main`; the interchange snapshot is not complete DTCG conformance, Figma Variables sync, or a host WCAG certification | +| Editor chrome design tokens | Repeating toolbar/editor surfaces share named `--cwl-*` custom properties; hosts override those properties on `.cwl-editor` and preview them in Storybook | Design Tokens Format Module 2025.10; WCAG 2.2 contrast including inventoried `--cwl-accent` on `--cwl-accent-soft`; Storybook React/Vite | Active-PR doctoring `docs/doctoring/editor-design-tokens.md`, operator guide, ADR 0031, token catalog tests, and Storybook inventory | Proposed until protected `main`; the interchange snapshot is not complete DTCG conformance, Figma Variables sync, or a host WCAG certification | | Editor integration | Public behavior must exercise the actual TipTap/ProseMirror integration path, not an inert extension field or test-only hook | official TipTap and ProseMirror documentation for the locked dependency line | integration tests and package consumers | Inkspan does not claim compatibility with untested major-version integration semantics | | Collaboration | Inkspan provides provider-neutral editor/Yjs bindings; host owns provider lifecycle, room authorization, awareness privacy, persistence and audit | official Yjs/provider documentation plus Inkspan public contract | collaboration tests and architecture ownership matrix | No network-provider or tenant-authorization authority is implied | | Secure development | Security controls are developed test-first, with exact-head scanning/review/package evidence and root-cause regression | NIST SP 800-218 SSDF 1.1 | CI/security/SAST/package/provenance gates, doctoring and regression history | Repository evidence is not a claim of complete SSDF organizational conformance | diff --git a/docs/TRD.md b/docs/TRD.md index 460b9a2c..1b6ffae9 100644 --- a/docs/TRD.md +++ b/docs/TRD.md @@ -97,7 +97,7 @@ CSS paged-media print boundary is implemented on protected `main` under Accepted The CSS contract does not introduce JavaScript print orchestration, a PDF service, durable export authority, credentials, network access, identity, tenancy, persistence, page-number/header/footer generation, signature authority, or PDF-conformance claims. Browser/OS print destination and durable artifact policy remain host/user responsibilities. -A named editor-chrome theme-token catalog, DTCG 2025.10 interchange snapshot, and Storybook inventory are Active PR / Proposed. CSS remains runtime presentation authority. Hosts override `--cwl-*` on `.cwl-editor` after checking WCAG 2.2 contrast. The catalog's optional print values are the `@media print` remaps; forced-colors mode does not assign those custom properties. This slice adds no Figma, network, persistence, credential, or model authority. +A named editor-chrome theme-token catalog, DTCG 2025.10 interchange snapshot, and Storybook inventory are Active PR / Proposed. CSS remains runtime presentation authority. Hosts override `--cwl-*` on `.cwl-editor` after checking WCAG 2.2 contrast for body text and the inventoried active toolbar pair (`--cwl-accent` on `--cwl-accent-soft`). The catalog's optional print values are the `@media print` remaps; forced-colors mode does not assign those custom properties. This slice adds no Figma, network, persistence, credential, or model authority. ## Model-assisted authoring diff --git a/docs/UML.md b/docs/UML.md index 566fbbe6..de4e20d3 100644 --- a/docs/UML.md +++ b/docs/UML.md @@ -38,7 +38,7 @@ flowchart LR The host owns transport, authentication, authorization, tenant isolation, persistence, credentials, provider lifecycle, retention, deployment, durable audit, and model-use policy. Inkspan owns deterministic local editor/conversion/evidence behavior only. -A named editor-chrome theme-token catalog and Storybook inventory are Active PR / Proposed. Hosts override `--cwl-*` on `.cwl-editor` after checking WCAG 2.2 contrast. CSS remains runtime presentation authority. Storybook previews a class-level chrome sample and a live shipped Toolbar. It does not mount `CwlEditor`. Print-media remaps live in `@media print`; forced-colors mode does not assign those custom properties. Figma Variables, brand certification, and design-tool sync remain host-owned. +A named editor-chrome theme-token catalog and Storybook inventory are Active PR / Proposed. Hosts override `--cwl-*` on `.cwl-editor` after checking WCAG 2.2 contrast for body text and active toolbar text (`--cwl-accent` on `--cwl-accent-soft`). CSS remains runtime presentation authority. Storybook previews a class-level chrome sample (including `:focus-visible`) and a live shipped Toolbar. It does not mount `CwlEditor`. Print-media remaps live in `@media print`; forced-colors mode does not assign those custom properties. Figma Variables, brand certification, and design-tool sync remain host-owned. ## Rich paste sequence diff --git a/docs/adr/0031-editor-design-tokens-storybook.md b/docs/adr/0031-editor-design-tokens-storybook.md index 4790519a..4d751d09 100644 --- a/docs/adr/0031-editor-design-tokens-storybook.md +++ b/docs/adr/0031-editor-design-tokens-storybook.md @@ -15,7 +15,7 @@ Inkspan ships repeating toolbar buttons, groups, and editor chrome styled throug ## Decision -Inkspan publishes a host-facing theme-token catalog for the nine shipped chrome tokens, a Design Tokens Format Module 2025.10 interchange snapshot, and a Storybook inventory of repeating toolbar/editor objects. Hosts override `--cwl-*` on `.cwl-editor` after checking WCAG 2.2 contrast. Unknown token names fail closed. No Figma, network, persistence, credential, or model authority is added. +Inkspan publishes a host-facing theme-token catalog for the nine shipped chrome tokens, a Design Tokens Format Module 2025.10 interchange snapshot, and a Storybook inventory of repeating toolbar/editor objects. Hosts override `--cwl-*` on `.cwl-editor` after checking WCAG 2.2 contrast for body text and inventoried active toolbar text (`--cwl-accent` on `--cwl-accent-soft`). Unknown token names fail closed. No Figma, network, persistence, credential, or model authority is added. ## Consequences and ownership trade-offs diff --git a/docs/design-tokens.md b/docs/design-tokens.md index b7c78e9f..eb97a983 100644 --- a/docs/design-tokens.md +++ b/docs/design-tokens.md @@ -2,7 +2,7 @@ Status: Active PR / Proposed -Use this catalog when you need to re-theme Inkspan's repeating toolbar and editor chrome. Override the named custom properties on `.cwl-editor` after checking WCAG 2.2 contrast. Do not edit Inkspan internals. +Use this catalog when you need to re-theme Inkspan's repeating toolbar and editor chrome. Override the named custom properties on `.cwl-editor` after checking WCAG 2.2 contrast for both body text (`--cwl-fg` on `--cwl-bg`) and active toolbar text (`--cwl-accent` on `--cwl-accent-soft`). Do not edit Inkspan internals. ```css .cwl-editor { @@ -21,10 +21,11 @@ import { const tokens = listEditorThemeTokens(); const dtcgGroup = toDesignTokenFormatGroup(); -const contrast = getEditorThemeTokenContrast('cwl-fg', 'cwl-bg', 'light'); -const overrideRatio = contrastRatioFromHex('#0b6e4f', '#ffffff'); -if (contrast.ratio < 4.5 || overrideRatio < 4.5) { - throw new Error(contrast.hostAction); +const body = getEditorThemeTokenContrast('cwl-fg', 'cwl-bg', 'light'); +const activeDark = getEditorThemeTokenContrast('cwl-accent', 'cwl-accent-soft', 'dark'); +const overrideRatio = contrastRatioFromHex('#0b6e4f', '#d8f3e8'); +if (!body.meetsTextContrast || !activeDark.meetsTextContrast || overrideRatio < 4.5) { + throw new Error(activeDark.hostAction); } void tokens; void dtcgGroup; diff --git a/docs/doctoring/editor-design-tokens.md b/docs/doctoring/editor-design-tokens.md index 434f278d..d15ed4b0 100644 --- a/docs/doctoring/editor-design-tokens.md +++ b/docs/doctoring/editor-design-tokens.md @@ -9,7 +9,7 @@ Hosts embed Inkspan and need to match brand color, radius, and font without forking `src/styles.css`. The stylesheet already used `--cwl-*` custom properties, but buyers had no typed catalog, no interchange snapshot, and no Storybook inventory of the repeating toolbar button and editor chrome. Theme work therefore required reading CSS internals. -If contrast fails after a re-theme, override only the named tokens on `.cwl-editor` and re-check WCAG 2.2 contrast against `--cwl-bg`. Do not disable forced-colors overrides. +If contrast fails after a re-theme, override only the named tokens on `.cwl-editor` and re-check WCAG 2.2 text contrast for `--cwl-fg` on `--cwl-bg` and `--cwl-accent` on `--cwl-accent-soft`. Do not disable forced-colors overrides. ## Decision @@ -28,11 +28,11 @@ WCAG 2.2 requires sufficient contrast for text and user-interface components (Wo ## Test-first evidence - RED: `src/designTokens.test.ts` failed because `./designTokens.js` did not exist. -- GREEN: the catalog lists the nine shipped tokens, aligns light/dark/print color values with the matching `src/styles.css` media blocks, reports WCAG 2.2 contrast for shipped color pairs, rejects unknown names without reflecting caller input, and emits a DTCG 2025.10 group. +- GREEN: the catalog lists the nine shipped tokens, aligns light/dark/print color values with the matching `src/styles.css` media blocks, reports WCAG 2.2 contrast for shipped color pairs including the inventoried `--cwl-accent` / `--cwl-accent-soft` active chrome, rejects unknown names without reflecting caller input, and emits a DTCG 2025.10 group. ## Residual risk -Print media still remaps the color tokens after a host override. Forced-colors mode only restyles the toolbar focus outline to `CanvasText`; it does not assign `--cwl-*` values. The font token snapshot splits a CSS font-family list and does not execute CSS. Storybook success is not Chromium/Firefox/WebKit release evidence. +Print media still remaps the color tokens after a host override. Forced-colors mode only restyles the toolbar focus outline to `CanvasText`; it does not assign `--cwl-*` values. The shipped dark `.cwl-tb-btn.is-active` pair (`--cwl-accent` on `--cwl-accent-soft`) is below the WCAG 2.2 4.5:1 text threshold and still meets the 3:1 non-text threshold. Hosts must call `getEditorThemeTokenContrast('cwl-accent', 'cwl-accent-soft', 'dark')` and override those tokens before treating dark active chrome as text-contrast-safe. The font token snapshot splits a CSS font-family list and does not execute CSS. Storybook success is not Chromium/Firefox/WebKit release evidence. ## Rollback diff --git a/docs/storybook-inventory.md b/docs/storybook-inventory.md index 8833303d..1c72bc3d 100644 --- a/docs/storybook-inventory.md +++ b/docs/storybook-inventory.md @@ -6,7 +6,7 @@ Run `pnpm storybook` to preview repeating Inkspan chrome before you override tok | Story | Repeating object | Host next action | | --- | --- | --- | -| `Editor Chrome / Toolbar Button States` | `.cwl-tb-btn` default, `is-active`, and disabled, plus `.cwl-collaboration-status` and `.cwl-editor__surface` | Override `--cwl-accent` and `--cwl-accent-soft` on `.cwl-editor` after checking WCAG 2.2 contrast | +| `Editor Chrome / Toolbar Button States` | `.cwl-tb-btn` default, `is-active`, disabled, and `:focus-visible`, plus `.cwl-collaboration-status` and `.cwl-editor__surface` | Override `--cwl-accent` and `--cwl-accent-soft` on `.cwl-editor` after checking WCAG 2.2 contrast, including `getEditorThemeTokenContrast('cwl-accent', 'cwl-accent-soft', 'dark')` | | `Editor Chrome / Live Toolbar` | shipped Toolbar buttons, groups, and disabled/active states | Override `--cwl-accent` and `--cwl-accent-soft` on `.cwl-editor` after checking WCAG 2.2 contrast | | `Editor Chrome / Theme Tokens` | Color, radius, and font tokens | Copy `toDesignTokenFormatGroup()` into a host theme file; do not edit Inkspan internals | diff --git a/src/designTokenDocumentation.test.ts b/src/designTokenDocumentation.test.ts index d0e84018..3c05dbd9 100644 --- a/src/designTokenDocumentation.test.ts +++ b/src/designTokenDocumentation.test.ts @@ -33,12 +33,17 @@ describe('editor design-token documentation contract', () => { expect(doctoring).not.toContain('ADR 0027'); expect(operatorGuide).toContain('Override the named custom properties on `.cwl-editor`'); expect(operatorGuide).toContain('getEditorThemeTokenContrast'); + expect(operatorGuide).toContain("getEditorThemeTokenContrast('cwl-accent', 'cwl-accent-soft', 'dark')"); + expect(operatorGuide).toContain('meetsTextContrast'); expect(operatorGuide).toContain('contrastRatioFromHex'); expect(operatorGuide).toContain('Do not edit Inkspan internals'); expect(inventory).toContain('Editor Chrome / Toolbar Button States'); expect(inventory).toContain('Editor Chrome / Live Toolbar'); expect(inventory).toContain('shipped Toolbar'); + expect(inventory).toContain(':focus-visible'); expect(inventory).toContain('Override `--cwl-accent`'); + expect(doctoring).toContain('cwl-accent-soft'); + expect(doctoring).toContain('below the WCAG 2.2 4.5:1'); expect(doctoring).toContain('Design Tokens Format Module 2025.10'); expect(doctoring).toContain( 'https://www.w3.org/community/reports/design-tokens/CG-FINAL-format-20251028/', @@ -48,6 +53,7 @@ describe('editor design-token documentation contract', () => { expect(changelog).toContain('Named the repeating editor chrome as a host-facing theme-token catalog'); expect(repositoryFile('.storybook/main.ts')).toContain("@storybook/react-vite"); expect(repositoryFile('stories/EditorChrome.stories.tsx')).toContain('cwl-tb-btn'); + expect(repositoryFile('stories/EditorChrome.stories.tsx')).toContain('autoFocus'); expect(repositoryFile('stories/EditorChrome.stories.tsx')).toContain('disabled'); expect(repositoryFile('stories/EditorChrome.stories.tsx')).toContain('cwl-editor__surface'); expect(repositoryFile('stories/EditorChrome.stories.tsx')).toContain( diff --git a/src/designTokens.test.ts b/src/designTokens.test.ts index 5cd1fce5..262e35b0 100644 --- a/src/designTokens.test.ts +++ b/src/designTokens.test.ts @@ -96,10 +96,40 @@ describe('editor theme token catalog', () => { expect(print.ratio).toBeCloseTo(21, 8); expect(muted.ratio).toBeCloseTo(6.114136455475549, 8); expect(light.ratio).toBeGreaterThanOrEqual(4.5); + expect(light.meetsTextContrast).toBe(true); + expect(light.meetsNonTextContrast).toBe(true); expect(light.hostAction).toContain('Override --cwl-fg and --cwl-bg on .cwl-editor'); expect(light.hostAction).toContain('WCAG 2.2'); }); + it('reports inventoried active-chrome contrast including the dark pair below 4.5:1', () => { + const lightActive = getEditorThemeTokenContrast('cwl-accent', 'cwl-accent-soft', 'light'); + const darkActive = getEditorThemeTokenContrast('cwl-accent', 'cwl-accent-soft', 'dark'); + const printActive = getEditorThemeTokenContrast('cwl-accent', 'cwl-accent-soft', 'print'); + const accentOnBackground = getEditorThemeTokenContrast('cwl-accent', 'cwl-bg', 'light'); + + expect(lightActive.ratio).toBeCloseTo(4.563748387142551, 8); + expect(lightActive.meetsTextContrast).toBe(true); + expect(darkActive.ratio).toBeCloseTo(4.125850234317593, 8); + expect(darkActive.meetsTextContrast).toBe(false); + expect(darkActive.meetsNonTextContrast).toBe(true); + expect(darkActive.hostAction).toContain('--cwl-accent'); + expect(darkActive.hostAction).toContain('--cwl-accent-soft'); + expect(darkActive.hostAction).toContain('below 4.5:1'); + expect(printActive.meetsTextContrast).toBe(true); + expect(accentOnBackground.meetsTextContrast).toBe(true); + }); + + it('points accent token host actions at the inventoried active pair', () => { + const accent = getEditorThemeToken('cwl-accent'); + const accentSoft = getEditorThemeToken('cwl-accent-soft'); + const foreground = getEditorThemeToken('cwl-fg'); + + expect(accent.hostAction).toContain('--cwl-accent-soft'); + expect(accentSoft.hostAction).toContain('--cwl-accent'); + expect(foreground.hostAction).toContain('--cwl-bg'); + }); + it('rejects contrast lookups that are not shipped color tokens without reflecting caller input', () => { expect(() => getEditorThemeTokenContrast('not-a-theme-token', 'cwl-bg')).toThrow( EditorThemeTokenError, diff --git a/src/designTokens.ts b/src/designTokens.ts index 70274ffb..5ecb2b11 100644 --- a/src/designTokens.ts +++ b/src/designTokens.ts @@ -37,9 +37,17 @@ export interface EditorThemeTokenContrast { readonly background: EditorThemeTokenName; readonly scheme: EditorThemeTokenScheme; readonly ratio: number; + readonly meetsTextContrast: boolean; + readonly meetsNonTextContrast: boolean; readonly hostAction: string; } +/** WCAG 2.2 Success Criterion 1.4.3 minimum contrast for regular text. */ +export const WCAG_TEXT_CONTRAST_RATIO = 4.5; + +/** WCAG 2.2 Success Criterion 1.4.11 minimum contrast for UI components. */ +export const WCAG_NON_TEXT_CONTRAST_RATIO = 3; + export interface DesignTokenFormatNode { readonly $type: 'color' | 'dimension' | 'fontFamily'; readonly $value: string | readonly string[]; @@ -86,8 +94,22 @@ export class EditorThemeTokenContrastError extends Error { } } -function hostAction(cssCustomProperty: `--${EditorThemeTokenName}`): string { - return `Override ${cssCustomProperty} on .cwl-editor after checking WCAG 2.2 contrast against --cwl-bg. Do not edit Inkspan internals.`; +function hostAction( + name: EditorThemeTokenName, + cssCustomProperty: `--${EditorThemeTokenName}`, +): string { + const actions = { + 'cwl-accent': `Override ${cssCustomProperty} on .cwl-editor after checking WCAG 2.2 text contrast against --cwl-accent-soft (active toolbar) and --cwl-bg (links). Do not edit Inkspan internals.`, + 'cwl-accent-soft': `Override ${cssCustomProperty} on .cwl-editor after checking WCAG 2.2 text contrast against --cwl-accent on active toolbar buttons. Do not edit Inkspan internals.`, + 'cwl-fg': `Override ${cssCustomProperty} on .cwl-editor after checking WCAG 2.2 text contrast against --cwl-bg. Do not edit Inkspan internals.`, + 'cwl-muted': `Override ${cssCustomProperty} on .cwl-editor after checking WCAG 2.2 text contrast against --cwl-bg. Do not edit Inkspan internals.`, + 'cwl-border': `Override ${cssCustomProperty} on .cwl-editor after checking WCAG 2.2 non-text contrast against adjacent chrome. Do not edit Inkspan internals.`, + 'cwl-bg': `Override ${cssCustomProperty} on .cwl-editor after checking WCAG 2.2 text contrast of --cwl-fg and --cwl-muted against this background. Do not edit Inkspan internals.`, + 'cwl-surface': `Override ${cssCustomProperty} on .cwl-editor after checking WCAG 2.2 text contrast of --cwl-fg and --cwl-muted against this background. Do not edit Inkspan internals.`, + 'cwl-radius': `Override ${cssCustomProperty} on .cwl-editor. Do not edit Inkspan internals.`, + 'cwl-font': `Override ${cssCustomProperty} on .cwl-editor. Do not edit Inkspan internals.`, + } as const satisfies Record; + return actions[name]; } function themeToken( @@ -105,7 +127,7 @@ function themeToken( lightValue, darkValue, printValue, - hostAction: hostAction(cssCustomProperty), + hostAction: hostAction(name, cssCustomProperty), }); } @@ -215,8 +237,9 @@ function colorValueForScheme( /** * Return the WCAG 2.2 contrast ratio for two shipped color tokens. * - * Use this after a host override to decide whether `--cwl-fg` still meets - * 4.5:1 against `--cwl-bg`. The ratio is not a host WCAG certification. + * Use this after a host override to decide whether an inventoried pair still + * meets 4.5:1 text contrast, including `--cwl-accent` on `--cwl-accent-soft`. + * The ratio is not a host WCAG certification. * * @throws {EditorThemeTokenError} When either name is not a shipped token. * @throws {EditorThemeTokenContrastError} When either token is not a color. @@ -231,15 +254,23 @@ export function getEditorThemeTokenContrast( if (foreground.role !== 'color' || background.role !== 'color') { throw new EditorThemeTokenContrastError(); } + const ratio = contrastRatioFromHex( + colorValueForScheme(foreground, scheme), + colorValueForScheme(background, scheme), + ); + const meetsTextContrast = ratio >= WCAG_TEXT_CONTRAST_RATIO; + const meetsNonTextContrast = ratio >= WCAG_NON_TEXT_CONTRAST_RATIO; + const pairNames = `--${foreground.name} and --${background.name}`; return Object.freeze({ foreground: foreground.name, background: background.name, scheme, - ratio: contrastRatioFromHex( - colorValueForScheme(foreground, scheme), - colorValueForScheme(background, scheme), - ), - hostAction: `Override --${foreground.name} and --${background.name} on .cwl-editor after checking WCAG 2.2 contrast. Do not edit Inkspan internals.`, + ratio, + meetsTextContrast, + meetsNonTextContrast, + hostAction: meetsTextContrast + ? `Override ${pairNames} on .cwl-editor after checking WCAG 2.2 contrast. Do not edit Inkspan internals.` + : `Override ${pairNames} on .cwl-editor; shipped ${scheme} text contrast is below 4.5:1. Do not edit Inkspan internals.`, }); } diff --git a/src/index.test.ts b/src/index.test.ts index 0ad7fa08..edc46671 100644 --- a/src/index.test.ts +++ b/src/index.test.ts @@ -54,6 +54,8 @@ describe('package entry point', () => { expect(typeof api.getEditorThemeToken).toBe('function'); expect(typeof api.getEditorThemeTokenContrast).toBe('function'); expect(typeof api.contrastRatioFromHex).toBe('function'); + expect(api.WCAG_TEXT_CONTRAST_RATIO).toBe(4.5); + expect(api.WCAG_NON_TEXT_CONTRAST_RATIO).toBe(3); expect(typeof api.toDesignTokenFormatGroup).toBe('function'); expect(typeof api.EditorThemeTokenError).toBe('function'); expect(typeof api.EditorThemeTokenContrastError).toBe('function'); diff --git a/src/index.ts b/src/index.ts index d54c99eb..ca2fabe4 100644 --- a/src/index.ts +++ b/src/index.ts @@ -170,6 +170,8 @@ export type { PlainTextOptions } from './markdown/plainText.js'; export { EditorThemeTokenContrastError, EditorThemeTokenError, + WCAG_NON_TEXT_CONTRAST_RATIO, + WCAG_TEXT_CONTRAST_RATIO, contrastRatioFromHex, getEditorThemeToken, getEditorThemeTokenContrast, diff --git a/stories/EditorChrome.stories.tsx b/stories/EditorChrome.stories.tsx index 48a44a75..5f60c904 100644 --- a/stories/EditorChrome.stories.tsx +++ b/stories/EditorChrome.stories.tsx @@ -62,6 +62,9 @@ export const ToolbarButtonStates: Story = { + @@ -70,8 +73,10 @@ export const ToolbarButtonStates: Story = {
Idle
- Preview the same `.cwl-*` classes buyers ship. This is a class-level - chrome sample, not a mounted Toolbar or CwlEditor. + Preview the same `.cwl-*` classes buyers ship, including + `:focus-visible`. Forced-colors restyles that outline to CanvasText + and does not assign `--cwl-*`. This is a class-level chrome sample, + not a mounted Toolbar or CwlEditor.
From a831359d1509811ab8777e7356f6ebd5f251b5cf Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Mon, 17 Aug 2026 06:05:00 +0900 Subject: [PATCH 10/67] test(a11y): require compliant dark active-toolbar contrast --- src/designTokens.test.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/designTokens.test.ts b/src/designTokens.test.ts index 262e35b0..b5944838 100644 --- a/src/designTokens.test.ts +++ b/src/designTokens.test.ts @@ -102,7 +102,7 @@ describe('editor theme token catalog', () => { expect(light.hostAction).toContain('WCAG 2.2'); }); - it('reports inventoried active-chrome contrast including the dark pair below 4.5:1', () => { + it('requires inventoried active-chrome text contrast to meet WCAG 2.2 AA', () => { const lightActive = getEditorThemeTokenContrast('cwl-accent', 'cwl-accent-soft', 'light'); const darkActive = getEditorThemeTokenContrast('cwl-accent', 'cwl-accent-soft', 'dark'); const printActive = getEditorThemeTokenContrast('cwl-accent', 'cwl-accent-soft', 'print'); @@ -110,12 +110,12 @@ describe('editor theme token catalog', () => { expect(lightActive.ratio).toBeCloseTo(4.563748387142551, 8); expect(lightActive.meetsTextContrast).toBe(true); - expect(darkActive.ratio).toBeCloseTo(4.125850234317593, 8); - expect(darkActive.meetsTextContrast).toBe(false); + expect(darkActive.ratio).toBeCloseTo(5.062920561609967, 8); + expect(darkActive.meetsTextContrast).toBe(true); expect(darkActive.meetsNonTextContrast).toBe(true); expect(darkActive.hostAction).toContain('--cwl-accent'); expect(darkActive.hostAction).toContain('--cwl-accent-soft'); - expect(darkActive.hostAction).toContain('below 4.5:1'); + expect(darkActive.hostAction).not.toContain('below 4.5:1'); expect(printActive.meetsTextContrast).toBe(true); expect(accentOnBackground.meetsTextContrast).toBe(true); }); From 837560af06de2c8856a26285792ef9cfbcda2010 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Mon, 17 Aug 2026 06:06:56 +0900 Subject: [PATCH 11/67] fix(a11y): raise dark active-toolbar contrast --- src/designTokens.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/designTokens.ts b/src/designTokens.ts index 5ecb2b11..83e6e89e 100644 --- a/src/designTokens.ts +++ b/src/designTokens.ts @@ -137,7 +137,7 @@ const EDITOR_THEME_TOKENS: readonly EditorThemeToken[] = Object.freeze([ themeToken('cwl-border', 'color', '#d1d9e0', '#3d444d', '#999999'), themeToken('cwl-bg', 'color', '#ffffff', '#0d1117', '#ffffff'), themeToken('cwl-surface', 'color', '#f6f8fa', '#161b22', '#ffffff'), - themeToken('cwl-accent', 'color', '#0969da', '#4493f8', '#000000'), + themeToken('cwl-accent', 'color', '#0969da', '#58a6ff', '#000000'), themeToken('cwl-accent-soft', 'color', '#ddf4ff', '#163356', '#ffffff'), themeToken('cwl-radius', 'dimension', '8px'), themeToken( From 081f1a65d1b98a4d31c0cb21ca1ead9053b19b33 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Mon, 17 Aug 2026 06:07:29 +0900 Subject: [PATCH 12/67] fix(a11y): ship compliant dark active-toolbar accent --- src/styles.css | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/styles.css b/src/styles.css index 0970a931..25f98adb 100644 --- a/src/styles.css +++ b/src/styles.css @@ -41,7 +41,7 @@ --cwl-border: #3d444d; --cwl-bg: #0d1117; --cwl-surface: #161b22; - --cwl-accent: #4493f8; + --cwl-accent: #58a6ff; --cwl-accent-soft: #163356; } } From a2cf5256c49cc6a4bed04efb8db316500b841d25 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Mon, 17 Aug 2026 06:08:06 +0900 Subject: [PATCH 13/67] test(a11y): bind docs to compliant shipped active contrast --- src/designTokenDocumentation.test.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/designTokenDocumentation.test.ts b/src/designTokenDocumentation.test.ts index 3c05dbd9..89a23ace 100644 --- a/src/designTokenDocumentation.test.ts +++ b/src/designTokenDocumentation.test.ts @@ -43,7 +43,8 @@ describe('editor design-token documentation contract', () => { expect(inventory).toContain(':focus-visible'); expect(inventory).toContain('Override `--cwl-accent`'); expect(doctoring).toContain('cwl-accent-soft'); - expect(doctoring).toContain('below the WCAG 2.2 4.5:1'); + expect(doctoring).toContain('meets the WCAG 2.2 4.5:1'); + expect(doctoring).not.toContain('below the WCAG 2.2 4.5:1'); expect(doctoring).toContain('Design Tokens Format Module 2025.10'); expect(doctoring).toContain( 'https://www.w3.org/community/reports/design-tokens/CG-FINAL-format-20251028/', @@ -51,6 +52,7 @@ describe('editor design-token documentation contract', () => { expect(doctoring).toContain('https://www.w3.org/TR/WCAG22/'); expect(doctoring).toContain('https://storybook.js.org/docs/get-started/frameworks/react-vite'); expect(changelog).toContain('Named the repeating editor chrome as a host-facing theme-token catalog'); + expect(changelog).toContain('Raised the shipped dark active-toolbar accent'); expect(repositoryFile('.storybook/main.ts')).toContain("@storybook/react-vite"); expect(repositoryFile('stories/EditorChrome.stories.tsx')).toContain('cwl-tb-btn'); expect(repositoryFile('stories/EditorChrome.stories.tsx')).toContain('autoFocus'); From c92bcd5de1d00ceef864b5157a48bd2cc7ed7448 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Mon, 17 Aug 2026 06:08:32 +0900 Subject: [PATCH 14/67] docs(a11y): record compliant shipped dark active contrast --- docs/doctoring/editor-design-tokens.md | 21 +++++++++++++-------- 1 file changed, 13 insertions(+), 8 deletions(-) diff --git a/docs/doctoring/editor-design-tokens.md b/docs/doctoring/editor-design-tokens.md index d15ed4b0..f3f33d4e 100644 --- a/docs/doctoring/editor-design-tokens.md +++ b/docs/doctoring/editor-design-tokens.md @@ -9,34 +9,39 @@ Hosts embed Inkspan and need to match brand color, radius, and font without forking `src/styles.css`. The stylesheet already used `--cwl-*` custom properties, but buyers had no typed catalog, no interchange snapshot, and no Storybook inventory of the repeating toolbar button and editor chrome. Theme work therefore required reading CSS internals. +The same inventory exposed an Inkspan-owned default-theme defect rather than a host-only customization problem: dark `.cwl-tb-btn.is-active` rendered `--cwl-accent` text on `--cwl-accent-soft` at about 4.13:1, below the WCAG 2.2 4.5:1 threshold for normal text. Inkspan now ships dark `--cwl-accent: #58a6ff` against unchanged `--cwl-accent-soft: #163356`, producing about 5.06:1 for the active toolbar pair. Host overrides must still re-check their own resulting pairs. + If contrast fails after a re-theme, override only the named tokens on `.cwl-editor` and re-check WCAG 2.2 text contrast for `--cwl-fg` on `--cwl-bg` and `--cwl-accent` on `--cwl-accent-soft`. Do not disable forced-colors overrides. ## Decision 1. Keep `src/styles.css` as runtime presentation authority. 2. Publish `listEditorThemeTokens()` / `getEditorThemeToken()` / `getEditorThemeTokenContrast()` / `toDesignTokenFormatGroup()` as a host-facing catalog of the nine shipped chrome tokens. -3. Reject unknown token names with a stable payload-redacted `EditorThemeTokenError`. -4. Preview repeating `.cwl-tb-btn` states, the shipped Toolbar, and token swatches in Storybook. -5. Add no network, persistence, credential, model, tenant, Figma, or design-tool sync authority. +3. Keep the shipped light/dark/print catalog values synchronized with the stylesheet and require the inventoried active-toolbar text pair to meet the WCAG 2.2 4.5:1 threshold in Inkspan's own default themes. +4. Reject unknown token names with a stable payload-redacted `EditorThemeTokenError`. +5. Preview repeating `.cwl-tb-btn` states, the shipped Toolbar, and token swatches in Storybook. +6. Add no network, persistence, credential, model, tenant, Figma, or design-tool sync authority. ## Standards rationale The Design Tokens Format Module 2025.10 defines a vendor-neutral JSON interchange for token groups, `$type`, and `$value` (Design Tokens Community Group, 2025). Inkspan emits a snapshot of its CSS custom properties in that shape. The report is a W3C Community Final Specification, not a W3C Standard, so this record does not claim W3C standardization or complete DTCG conformance. -WCAG 2.2 requires sufficient contrast for text and user-interface components (World Wide Web Consortium, 2024). Host overrides remain the host's contrast responsibility. Storybook's React/Vite preview is the inventory surface for repeating chrome (Storybook, n.d.). +WCAG 2.2 requires at least 4.5:1 contrast for normal text under Success Criterion 1.4.3 and at least 3:1 for meaningful user-interface component boundaries/states under Success Criterion 1.4.11 (World Wide Web Consortium, 2024). Inkspan therefore fixes a failing shipped default at its own presentation boundary; host overrides remain the host's contrast responsibility. Storybook's React/Vite preview is the inventory surface for repeating chrome (Storybook, n.d.). ## Test-first evidence -- RED: `src/designTokens.test.ts` failed because `./designTokens.js` did not exist. -- GREEN: the catalog lists the nine shipped tokens, aligns light/dark/print color values with the matching `src/styles.css` media blocks, reports WCAG 2.2 contrast for shipped color pairs including the inventoried `--cwl-accent` / `--cwl-accent-soft` active chrome, rejects unknown names without reflecting caller input, and emits a DTCG 2025.10 group. +- Original RED: `src/designTokens.test.ts` failed because `./designTokens.js` did not exist. +- Initial GREEN: the catalog lists the nine shipped tokens, aligns light/dark/print color values with the matching `src/styles.css` media blocks, reports WCAG 2.2 contrast for shipped color pairs, rejects unknown names without reflecting caller input, and emits a DTCG 2025.10 group. +- Accessibility RED: exact test-only head `a831359d1509811ab8777e7356f6ebd5f251b5cf` changed the active-chrome contract to require the shipped dark `--cwl-accent` / `--cwl-accent-soft` pair to meet 4.5:1. The predecessor production values remained `#4493f8` on `#163356` (about 4.13:1), so the new expectation could not pass without a real default-theme change. +- Accessibility GREEN: dark `--cwl-accent` is `#58a6ff` in both the typed catalog and runtime stylesheet; against unchanged `#163356` it measures about 5.06:1 and `meetsTextContrast` is true. The host override helper remains available for custom themes. ## Residual risk -Print media still remaps the color tokens after a host override. Forced-colors mode only restyles the toolbar focus outline to `CanvasText`; it does not assign `--cwl-*` values. The shipped dark `.cwl-tb-btn.is-active` pair (`--cwl-accent` on `--cwl-accent-soft`) is below the WCAG 2.2 4.5:1 text threshold and still meets the 3:1 non-text threshold. Hosts must call `getEditorThemeTokenContrast('cwl-accent', 'cwl-accent-soft', 'dark')` and override those tokens before treating dark active chrome as text-contrast-safe. The font token snapshot splits a CSS font-family list and does not execute CSS. Storybook success is not Chromium/Firefox/WebKit release evidence. +Print media still remaps the color tokens after a host override. Forced-colors mode only restyles the toolbar focus outline to `CanvasText`; it does not assign `--cwl-*` values. The shipped dark `.cwl-tb-btn.is-active` pair (`--cwl-accent` on `--cwl-accent-soft`) now meets the WCAG 2.2 4.5:1 normal-text threshold and also exceeds the 3:1 non-text threshold; hosts must still call `getEditorThemeTokenContrast('cwl-accent', 'cwl-accent-soft', 'dark')` after overriding either token because custom values can reintroduce a contrast failure. The font token snapshot splits a CSS font-family list and does not execute CSS. Storybook success is not Chromium/Firefox/WebKit release evidence. ## Rollback -Rollback must remove the catalog export, this record, the operator guide, the Storybook inventory/config/stories, ADR 0031, the changelog entry, and the documentation-index rows together. +Rollback must remove the catalog export, this record, the operator guide, the Storybook inventory/config/stories, ADR 0031, the changelog entry, and the documentation-index rows together. Reverting the dark accent value without also reverting the active-pair contrast contract would deliberately recreate a known accessibility defect and is not a valid partial rollback. ## References (APA 7th edition) From 119c4e362bb9b43c1b6d86a576232908b1f8f4f3 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Mon, 17 Aug 2026 06:08:44 +0900 Subject: [PATCH 15/67] docs(a11y): distinguish shipped contrast from host overrides --- docs/design-tokens.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/design-tokens.md b/docs/design-tokens.md index eb97a983..32b40634 100644 --- a/docs/design-tokens.md +++ b/docs/design-tokens.md @@ -2,7 +2,7 @@ Status: Active PR / Proposed -Use this catalog when you need to re-theme Inkspan's repeating toolbar and editor chrome. Override the named custom properties on `.cwl-editor` after checking WCAG 2.2 contrast for both body text (`--cwl-fg` on `--cwl-bg`) and active toolbar text (`--cwl-accent` on `--cwl-accent-soft`). Do not edit Inkspan internals. +Use this catalog when you need to re-theme Inkspan's repeating toolbar and editor chrome. Inkspan's shipped light, dark, and print token pairs are checked by repository tests; the shipped dark active-toolbar pair now uses `--cwl-accent: #58a6ff` on `--cwl-accent-soft: #163356` and measures about 5.06:1. When a host overrides any color token, re-check WCAG 2.2 contrast for both body text (`--cwl-fg` on `--cwl-bg`) and active toolbar text (`--cwl-accent` on `--cwl-accent-soft`). Do not edit Inkspan internals. ```css .cwl-editor { @@ -31,7 +31,7 @@ void tokens; void dtcgGroup; ``` -The stylesheet remains runtime presentation authority. `toDesignTokenFormatGroup()` is an interchange snapshot aligned to Design Tokens Format Module 2025.10. It is not complete DTCG conformance, Figma Variables sync, or a host WCAG certification. +The default-theme checks above are product evidence, not a host WCAG certification. The stylesheet remains runtime presentation authority. `toDesignTokenFormatGroup()` is an interchange snapshot aligned to Design Tokens Format Module 2025.10; it is not complete DTCG conformance or Figma Variables sync. Preview the repeating objects in Storybook (`pnpm storybook`) using the inventory in [`storybook-inventory.md`](storybook-inventory.md). From f0afb6f3117b01cbefeb9a2b26595eb5f253c2a5 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Mon, 17 Aug 2026 06:09:39 +0900 Subject: [PATCH 16/67] docs(adr): own default active-toolbar contrast --- docs/adr/0031-editor-design-tokens-storybook.md | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/docs/adr/0031-editor-design-tokens-storybook.md b/docs/adr/0031-editor-design-tokens-storybook.md index 4d751d09..8fd3a582 100644 --- a/docs/adr/0031-editor-design-tokens-storybook.md +++ b/docs/adr/0031-editor-design-tokens-storybook.md @@ -6,24 +6,27 @@ Status: Proposed Inkspan ships repeating toolbar buttons, groups, and editor chrome styled through `--cwl-*` custom properties. Hosts already re-theme by overriding those properties, but the names, light/dark/print values, and buyer next action lived only inside `src/styles.css`. There was no typed catalog, no Design Tokens Format Module interchange snapshot, and no Storybook inventory of the repeating objects. +Inventorying the actual active-toolbar foreground/background pair also exposed a product-owned accessibility defect: the former dark `--cwl-accent: #4493f8` on `--cwl-accent-soft: #163356` produced about 4.13:1 for 13px active-button text, below the WCAG 2.2 4.5:1 normal-text threshold. Treating that shipped-default failure as a host override responsibility would contradict Inkspan's ownership of its default presentation. + ## Alternatives considered 1. **Leave theming as undocumented CSS.** Rejected because hosts would keep reading internals and drift from the shipped token set. 2. **Generate CSS from a new token source of truth.** Rejected for this slice because it would rewrite the protected stylesheet and collide with print, forced-colors, and Office-unrelated presentation contracts. 3. **Add Figma Variables sync or a design-tool connector.** Rejected because Figma/design-tool credentials and sync authority are host-owned; Inkspan remains independently usable. -4. **Publish a named catalog plus Storybook inventory while CSS remains runtime authority.** Selected because hosts can override and preview repeating chrome without moving presentation authority or adding a design-tool runtime. +4. **Disclose the failing shipped dark pair and require every host to override it.** Rejected because the default palette is Inkspan-owned; documenting a product defect does not repair it. +5. **Publish a named catalog plus Storybook inventory while CSS remains runtime authority, and repair any failing shipped inventoried pair at the stylesheet boundary.** Selected because hosts can override and preview repeating chrome without moving presentation authority, while Inkspan remains accountable for its own defaults. ## Decision -Inkspan publishes a host-facing theme-token catalog for the nine shipped chrome tokens, a Design Tokens Format Module 2025.10 interchange snapshot, and a Storybook inventory of repeating toolbar/editor objects. Hosts override `--cwl-*` on `.cwl-editor` after checking WCAG 2.2 contrast for body text and inventoried active toolbar text (`--cwl-accent` on `--cwl-accent-soft`). Unknown token names fail closed. No Figma, network, persistence, credential, or model authority is added. +Inkspan publishes a host-facing theme-token catalog for the nine shipped chrome tokens, a Design Tokens Format Module 2025.10 interchange snapshot, and a Storybook inventory of repeating toolbar/editor objects. Inkspan's own inventoried normal-text pairs must meet the WCAG 2.2 4.5:1 threshold in shipped light/dark/print defaults; the dark accent is therefore `#58a6ff` on `#163356` for active toolbar text, about 5.06:1. Hosts overriding `--cwl-*` on `.cwl-editor` must re-check contrast for their resulting body and active-toolbar pairs. Unknown token names fail closed. No Figma, network, persistence, credential, or model authority is added. ## Consequences and ownership trade-offs -Hosts gain a copyable token list and a Storybook preview. Inkspan keeps CSS as the runtime source. naruon and other CWL hosts can apply the same overrides through host-owned brand CSS. Complete DTCG conformance, Figma Variables, and host WCAG certification remain out of scope. +Hosts gain a copyable token list and a Storybook preview. Inkspan keeps CSS as the runtime source and owns accessibility defects in its default token combinations. naruon and other CWL hosts can apply their own overrides through host-owned brand CSS and then use the same contrast helpers to validate those custom values. Complete DTCG conformance, Figma Variables, host-theme WCAG certification, and automated remediation of arbitrary host palettes remain out of scope. ## Failure and recovery -An unknown token name throws `EditorThemeTokenError` without reflecting caller input. A host override that fails contrast is recovered by changing only the named tokens, not by editing Inkspan internals or disabling forced-colors. +An unknown token name throws `EditorThemeTokenError` without reflecting caller input. A shipped default pair that fails an applicable repository contrast contract is repaired in Inkspan's token catalog and stylesheet together. A host override that fails contrast is recovered by changing only the named host tokens, not by editing Inkspan internals or disabling forced-colors. ## Security and privacy impact @@ -31,12 +34,12 @@ The catalog contains only public presentation values. It does not carry document ## Compatibility and migration -The catalog is additive. Existing CSS overrides on `.cwl-editor` continue to work. A later CSS token addition must update the catalog, Storybook inventory, and this ADR together. +The catalog is additive. Existing CSS overrides on `.cwl-editor` continue to work. The dark default accent changes from `#4493f8` to `#58a6ff`; hosts that already override `--cwl-accent` are unaffected by that default-value change but remain responsible for validating their custom pair. A later CSS token addition or default-value change must update the catalog, directly affected documentation/tests, and this ADR together. ## Verification and acceptance evidence -Required evidence includes token-catalog tests against `src/styles.css`, documentation-contract tests, Storybook inventory stories for toolbar button states and token swatches, and exact-head CI/coverage/package/security gates on the unchanged head. This ADR stays Proposed until protected integration. +Required evidence includes token-catalog tests against `src/styles.css`, deterministic contrast assertions for inventoried normal-text pairs, documentation-contract tests, Storybook inventory stories for toolbar button states and token swatches, and exact-head CI/coverage/package/security gates on the unchanged head. The accessibility regression must fail against the former `#4493f8`/`#163356` dark active pair and pass against the new `#58a6ff`/`#163356` pair. This ADR stays Proposed until protected integration. ## Rollback or supersession -Rollback removes the catalog export, Storybook inventory/config, operator/doctoring records, and this ADR together. Supersession requires a new ADR if CSS ceases to be runtime presentation authority or if a design-tool sync contract is accepted. +Rollback removes the catalog export, Storybook inventory/config, operator/doctoring records, and this ADR together. Reverting only the compliant dark accent while retaining the active-text contrast requirement is not a valid partial rollback. Supersession requires a new ADR if CSS ceases to be runtime presentation authority or if a design-tool sync contract is accepted. From 8b7bccb46cd874e7265498f20772345ac47eee14 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Mon, 17 Aug 2026 06:10:47 +0900 Subject: [PATCH 17/67] docs(changelog): record dark active-toolbar contrast fix --- CHANGELOG.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2d8162b1..ce99b09f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,9 @@ Historical release entries from **0.1.0 through 0.5.27** are preserved verbatim ### Added - Named the repeating editor chrome as a host-facing theme-token catalog and Storybook inventory so hosts can override `--cwl-*` custom properties on `.cwl-editor` after checking WCAG 2.2 contrast, without editing Inkspan internals. Color catalog values now distinguish light, dark, and `@media print` remaps; forced-colors mode is not treated as a token assignment. Hosts can call `getEditorThemeTokenContrast()` to compare inventoried pairs, including `--cwl-accent` on `--cwl-accent-soft`, against the 4.5:1 text threshold via `meetsTextContrast`. +### Accessibility +- Raised the shipped dark active-toolbar accent from `#4493f8` to `#58a6ff`, increasing `--cwl-accent` text on `--cwl-accent-soft: #163356` from about 4.13:1 to about 5.06:1 so Inkspan's default 13px active-button text meets the WCAG 2.2 4.5:1 normal-text threshold while host overrides remain explicitly re-checkable through `getEditorThemeTokenContrast()`. + ## [0.6.0] — 2026-08-10 ### Release From 57413d50c91363f9631c199f089bcebbc088fb90 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Mon, 17 Aug 2026 06:13:44 +0900 Subject: [PATCH 18/67] test(a11y): distinguish default repair from host re-theming --- src/designTokenDocumentation.test.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/designTokenDocumentation.test.ts b/src/designTokenDocumentation.test.ts index 89a23ace..3060a5b1 100644 --- a/src/designTokenDocumentation.test.ts +++ b/src/designTokenDocumentation.test.ts @@ -41,6 +41,7 @@ describe('editor design-token documentation contract', () => { expect(inventory).toContain('Editor Chrome / Live Toolbar'); expect(inventory).toContain('shipped Toolbar'); expect(inventory).toContain(':focus-visible'); + expect(inventory).toContain('Shipped defaults require no host override'); expect(inventory).toContain('Override `--cwl-accent`'); expect(doctoring).toContain('cwl-accent-soft'); expect(doctoring).toContain('meets the WCAG 2.2 4.5:1'); From d22a8d508bcbf86546f522c817014d57d0cc48ac Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Mon, 17 Aug 2026 06:13:56 +0900 Subject: [PATCH 19/67] docs(a11y): separate shipped defaults from host re-theming --- docs/storybook-inventory.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/storybook-inventory.md b/docs/storybook-inventory.md index 1c72bc3d..bd35b8c0 100644 --- a/docs/storybook-inventory.md +++ b/docs/storybook-inventory.md @@ -2,12 +2,12 @@ Status: Active PR / Proposed -Run `pnpm storybook` to preview repeating Inkspan chrome before you override tokens in a host. The button-state story is a class-level chrome sample. **Editor Chrome / Live Toolbar** mounts the shipped Toolbar so a token change is visible on the same objects buyers ship. These stories do not mount `CwlEditor`. +Run `pnpm storybook` to preview repeating Inkspan chrome before you override tokens in a host. **Shipped defaults require no host override for the inventoried active-toolbar contrast pair.** Override tokens only when re-theming, and re-check the resulting host palette. The button-state story is a class-level chrome sample. **Editor Chrome / Live Toolbar** mounts the shipped Toolbar so a token change is visible on the same objects buyers ship. These stories do not mount `CwlEditor`. | Story | Repeating object | Host next action | | --- | --- | --- | -| `Editor Chrome / Toolbar Button States` | `.cwl-tb-btn` default, `is-active`, disabled, and `:focus-visible`, plus `.cwl-collaboration-status` and `.cwl-editor__surface` | Override `--cwl-accent` and `--cwl-accent-soft` on `.cwl-editor` after checking WCAG 2.2 contrast, including `getEditorThemeTokenContrast('cwl-accent', 'cwl-accent-soft', 'dark')` | -| `Editor Chrome / Live Toolbar` | shipped Toolbar buttons, groups, and disabled/active states | Override `--cwl-accent` and `--cwl-accent-soft` on `.cwl-editor` after checking WCAG 2.2 contrast | +| `Editor Chrome / Toolbar Button States` | `.cwl-tb-btn` default, `is-active`, disabled, and `:focus-visible`, plus `.cwl-collaboration-status` and `.cwl-editor__surface` | If re-theming, override `--cwl-accent` and `--cwl-accent-soft` on `.cwl-editor` after checking WCAG 2.2 contrast, including `getEditorThemeTokenContrast('cwl-accent', 'cwl-accent-soft', 'dark')` | +| `Editor Chrome / Live Toolbar` | shipped Toolbar buttons, groups, and disabled/active states | If re-theming, override `--cwl-accent` and `--cwl-accent-soft` on `.cwl-editor` after checking WCAG 2.2 contrast | | `Editor Chrome / Theme Tokens` | Color, radius, and font tokens | Copy `toDesignTokenFormatGroup()` into a host theme file; do not edit Inkspan internals | Storybook is a development preview. It does not authorize documents, persist content, or replace packed-package evidence. From 8c853e7a9e0fccd6d2a2bb4d9431c8fa25f5d87e Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Mon, 17 Aug 2026 07:56:01 +0900 Subject: [PATCH 20/67] test(a11y): align re-theming contract with shipped defaults --- src/designTokenDocumentation.test.ts | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/designTokenDocumentation.test.ts b/src/designTokenDocumentation.test.ts index 3060a5b1..21f5be70 100644 --- a/src/designTokenDocumentation.test.ts +++ b/src/designTokenDocumentation.test.ts @@ -31,7 +31,8 @@ describe('editor design-token documentation contract', () => { expect(repositoryFile('docs/TRACEABILITY.md')).not.toContain('ADR 0027'); expect(doctoring).toContain('ADR 0031'); expect(doctoring).not.toContain('ADR 0027'); - expect(operatorGuide).toContain('Override the named custom properties on `.cwl-editor`'); + expect(operatorGuide).toContain('When a host overrides any color token'); + expect(operatorGuide).toContain('shipped dark active-toolbar pair now uses'); expect(operatorGuide).toContain('getEditorThemeTokenContrast'); expect(operatorGuide).toContain("getEditorThemeTokenContrast('cwl-accent', 'cwl-accent-soft', 'dark')"); expect(operatorGuide).toContain('meetsTextContrast'); @@ -42,7 +43,7 @@ describe('editor design-token documentation contract', () => { expect(inventory).toContain('shipped Toolbar'); expect(inventory).toContain(':focus-visible'); expect(inventory).toContain('Shipped defaults require no host override'); - expect(inventory).toContain('Override `--cwl-accent`'); + expect(inventory).toContain('If re-theming, override `--cwl-accent`'); expect(doctoring).toContain('cwl-accent-soft'); expect(doctoring).toContain('meets the WCAG 2.2 4.5:1'); expect(doctoring).not.toContain('below the WCAG 2.2 4.5:1'); @@ -67,4 +68,4 @@ describe('editor design-token documentation contract', () => { ); expect(inventory).toContain('class-level chrome sample'); }); -}); +}); \ No newline at end of file From 1fd0bdc44b75b265dc5f6b7951c27f6f90974cde Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Mon, 17 Aug 2026 08:01:11 +0900 Subject: [PATCH 21/67] test(a11y): distinguish historical contrast defect from shipped truth --- src/designTokenDocumentation.test.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/designTokenDocumentation.test.ts b/src/designTokenDocumentation.test.ts index 21f5be70..6a3bea25 100644 --- a/src/designTokenDocumentation.test.ts +++ b/src/designTokenDocumentation.test.ts @@ -46,7 +46,9 @@ describe('editor design-token documentation contract', () => { expect(inventory).toContain('If re-theming, override `--cwl-accent`'); expect(doctoring).toContain('cwl-accent-soft'); expect(doctoring).toContain('meets the WCAG 2.2 4.5:1'); - expect(doctoring).not.toContain('below the WCAG 2.2 4.5:1'); + expect(doctoring).toContain('Inkspan now ships dark `--cwl-accent: #58a6ff`'); + expect(doctoring).toContain('producing about 5.06:1'); + expect(doctoring).toContain('Host overrides must still re-check their own resulting pairs'); expect(doctoring).toContain('Design Tokens Format Module 2025.10'); expect(doctoring).toContain( 'https://www.w3.org/community/reports/design-tokens/CG-FINAL-format-20251028/', From d4a2311eb8210236f92334553a4986a6eaf1ebdd Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Mon, 17 Aug 2026 08:07:15 +0900 Subject: [PATCH 22/67] test(a11y): cover low-contrast host guidance --- src/designTokens.test.ts | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/designTokens.test.ts b/src/designTokens.test.ts index b5944838..a2620253 100644 --- a/src/designTokens.test.ts +++ b/src/designTokens.test.ts @@ -90,6 +90,7 @@ describe('editor theme token catalog', () => { const dark = getEditorThemeTokenContrast('cwl-fg', 'cwl-bg', 'dark'); const print = getEditorThemeTokenContrast('cwl-fg', 'cwl-bg', 'print'); const muted = getEditorThemeTokenContrast('cwl-muted', 'cwl-bg', 'light'); + const lowContrast = getEditorThemeTokenContrast('cwl-border', 'cwl-bg', 'light'); expect(light.ratio).toBeCloseTo(15.797619425332647, 8); expect(dark.ratio).toBeCloseTo(16.016082890827004, 8); @@ -100,6 +101,9 @@ describe('editor theme token catalog', () => { expect(light.meetsNonTextContrast).toBe(true); expect(light.hostAction).toContain('Override --cwl-fg and --cwl-bg on .cwl-editor'); expect(light.hostAction).toContain('WCAG 2.2'); + expect(lowContrast.meetsTextContrast).toBe(false); + expect(lowContrast.hostAction).toContain('shipped light text contrast is below 4.5:1'); + expect(lowContrast.hostAction).toContain('Override --cwl-border and --cwl-bg on .cwl-editor'); }); it('requires inventoried active-chrome text contrast to meet WCAG 2.2 AA', () => { @@ -166,4 +170,4 @@ describe('editor theme token catalog', () => { expect(group.cwl.font.$type).toBe('fontFamily'); expect(Array.isArray(group.cwl.font.$value)).toBe(true); }); -}); +}); \ No newline at end of file From e4a14d1b95359cc0dcab3b90c295a2c8b51a798f Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 18 Aug 2026 06:08:01 -0700 Subject: [PATCH 23/67] test(tokens): require DTCG 2025.10 value shapes --- src/designTokens.test.ts | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/src/designTokens.test.ts b/src/designTokens.test.ts index a2620253..1068fdba 100644 --- a/src/designTokens.test.ts +++ b/src/designTokens.test.ts @@ -159,14 +159,19 @@ describe('editor theme token catalog', () => { expect(() => contrastRatioFromHex('not-a-hex', '#ffffff')).not.toThrow(/not-a-hex/u); }); - it('emits a DTCG 2025.10 group that hosts can copy into a theme file', () => { + it('emits DTCG 2025.10-native color and dimension values hosts can copy into a theme file', () => { const group = toDesignTokenFormatGroup(); const foreground = group.cwl.fg; expect(foreground.$type).toBe('color'); - expect(foreground.$value).toBe('#1f2328'); + expect(foreground.$value).toEqual({ + colorSpace: 'srgb', + components: [31 / 255, 35 / 255, 40 / 255], + hex: '#1f2328', + }); expect(foreground.$description).toContain('Override --cwl-fg on .cwl-editor'); expect(group.cwl.radius.$type).toBe('dimension'); + expect(group.cwl.radius.$value).toEqual({ value: 8, unit: 'px' }); expect(group.cwl.font.$type).toBe('fontFamily'); expect(Array.isArray(group.cwl.font.$value)).toBe(true); }); From 45052935e69a12dee8294200a8fda63ad17f5c3c Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 18 Aug 2026 06:13:45 -0700 Subject: [PATCH 24/67] fix(tokens): emit DTCG 2025.10 native values --- src/designTokens.ts | 61 +++++++++++++++++++++++++++++++++++++++------ 1 file changed, 54 insertions(+), 7 deletions(-) diff --git a/src/designTokens.ts b/src/designTokens.ts index 83e6e89e..c1663323 100644 --- a/src/designTokens.ts +++ b/src/designTokens.ts @@ -48,9 +48,28 @@ export const WCAG_TEXT_CONTRAST_RATIO = 4.5; /** WCAG 2.2 Success Criterion 1.4.11 minimum contrast for UI components. */ export const WCAG_NON_TEXT_CONTRAST_RATIO = 3; +/** DTCG 2025.10 sRGB color value used by the interchange snapshot. */ +export interface DesignTokenFormatColorValue { + readonly colorSpace: 'srgb'; + readonly components: readonly [number, number, number]; + readonly hex: string; +} + +/** DTCG 2025.10 dimension value used by the interchange snapshot. */ +export interface DesignTokenFormatDimensionValue { + readonly value: number; + readonly unit: 'px' | 'rem'; +} + +export type DesignTokenFormatValue = + | DesignTokenFormatColorValue + | DesignTokenFormatDimensionValue + | string + | readonly string[]; + export interface DesignTokenFormatNode { readonly $type: 'color' | 'dimension' | 'fontFamily'; - readonly $value: string | readonly string[]; + readonly $value: DesignTokenFormatValue; readonly $description: string; } @@ -274,11 +293,38 @@ export function getEditorThemeTokenContrast( }); } -function formatTokenValue(token: EditorThemeToken): string | readonly string[] { - if (token.role !== 'fontFamily') { - return token.lightValue; +function formatColorTokenValue(value: string): DesignTokenFormatColorValue { + const integer = Number.parseInt(value.slice(1), 16); + return Object.freeze({ + colorSpace: 'srgb', + components: Object.freeze([ + ((integer >> 16) & 0xff) / 255, + ((integer >> 8) & 0xff) / 255, + (integer & 0xff) / 255, + ] as const), + hex: value, + }); +} + +function formatDimensionTokenValue(value: string): DesignTokenFormatDimensionValue { + const [, numericValue, unit] = /^(-?(?:\d+(?:\.\d+)?|\.\d+))(px|rem)$/u.exec(value)!; + return Object.freeze({ + value: Number.parseFloat(numericValue), + unit: unit as DesignTokenFormatDimensionValue['unit'], + }); +} + +function formatTokenValue(token: EditorThemeToken): DesignTokenFormatValue { + switch (token.role) { + case 'color': + return formatColorTokenValue(token.lightValue); + case 'dimension': + return formatDimensionTokenValue(token.lightValue); + case 'fontFamily': + return token.lightValue + .split(',') + .map((family) => family.trim().replace(/^['"]|['"]$/gu, '')); } - return token.lightValue.split(',').map((family) => family.trim().replace(/^['"]|['"]$/gu, '')); } function toFormatNode(token: EditorThemeToken): DesignTokenFormatNode { @@ -292,8 +338,9 @@ function toFormatNode(token: EditorThemeToken): DesignTokenFormatNode { /** * Return a Design Tokens Format Module 2025.10 group for the shipped chrome. * - * This is an interchange snapshot of Inkspan's CSS custom properties. It is not - * a claim of complete DTCG conformance, Figma Variables sync, or WCAG contrast + * The light/default snapshot emits DTCG-native color, dimension, and font-family + * value shapes. Dark and print CSS schemes remain available from the token + * catalog and stylesheet authority. This is not Figma Variables sync or a WCAG * certification for host overrides. */ export function toDesignTokenFormatGroup(): DesignTokenFormatGroup { From 3cc73f5dea89bbfe2310a64375a34db4d42ea2e6 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 18 Aug 2026 06:41:23 -0700 Subject: [PATCH 25/67] test(release): require four-file canonical inventory --- ...eleaseContractCanonicalConsistency.test.ts | 51 +++++++++++++++++++ 1 file changed, 51 insertions(+) create mode 100644 src/releaseContractCanonicalConsistency.test.ts diff --git a/src/releaseContractCanonicalConsistency.test.ts b/src/releaseContractCanonicalConsistency.test.ts new file mode 100644 index 00000000..b001c79f --- /dev/null +++ b/src/releaseContractCanonicalConsistency.test.ts @@ -0,0 +1,51 @@ +import { readFileSync } from 'node:fs'; +import { resolve } from 'node:path'; + +import { describe, expect, it } from 'vitest'; + +/** Read one authoritative repository file as UTF-8 text. */ +function repositoryFile(path: string): string { + return readFileSync(resolve(process.cwd(), path), 'utf8'); +} + +describe('canonical release artifact inventory consistency', () => { + it('keeps canonical release documents aligned with the protected four-file boundary', () => { + const contracts = repositoryFile('docs/CONTRACTS.md'); + const operability = repositoryFile('docs/OPERABILITY.md'); + const releaseDoctoring = repositoryFile( + 'docs/doctoring/release-draft-asset-inventory.md', + ); + const releaseSecurity = repositoryFile('docs/release-security.md'); + const testStrategy = repositoryFile('docs/TEST_STRATEGY.md'); + + expect(releaseSecurity).toContain( + 'Each successful GitHub release contains exactly four files', + ); + expect(releaseSecurity).toContain('`inkspan.spdx.json`'); + + expect(contracts).toContain('exactly four regular top-level files'); + expect(contracts).toContain('`inkspan.spdx.json`'); + expect(contracts).toMatch(/release evidence \| exact four-file draft inventory/u); + + expect(testStrategy).toContain('exact four-file inventory violations'); + expect(testStrategy).toContain( + 'exactly one npm tarball, exactly one Office wheel, `inkspan.spdx.json`, and `SHA256SUMS`', + ); + + expect(operability).toContain('build exactly four regular top-level release files'); + expect(operability).toContain( + 'exactly one npm tarball, exactly one Inkspan Office wheel, `inkspan.spdx.json`, and `SHA256SUMS`', + ); + + expect(releaseDoctoring).toContain( + 'exactly four top-level entries and require all four to be regular files', + ); + expect(releaseDoctoring).toContain('`inkspan.spdx.json`'); + + for (const document of [contracts, testStrategy, operability, releaseDoctoring]) { + expect(document).not.toContain('exactly three regular top-level files'); + expect(document).not.toContain('exact three-file draft inventory'); + expect(document).not.toContain('exactly three top-level entries'); + } + }); +}); From c1429c322f2748a4fa1f7b8c9cf65b12f8e4e83a Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 18 Aug 2026 06:45:02 -0700 Subject: [PATCH 26/67] docs(release): align contracts with four-file inventory --- docs/CONTRACTS.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/CONTRACTS.md b/docs/CONTRACTS.md index 36c98871..5a62716e 100644 --- a/docs/CONTRACTS.md +++ b/docs/CONTRACTS.md @@ -138,7 +138,7 @@ Expected degraded states are explicit rather than mapped to false success: A public release binds one exact integrated protected source head to package/artifact identity, applicable CI/security/accessibility/document-fidelity evidence, owned production coverage, public-docstring evidence, SBOM/provenance/reproducibility where configured, formal review requirements, rollback guidance, and post-publication smoke verification. -Before immutable publication, the canonical draft inventory is **exactly three regular top-level files**: exactly one npm tarball, exactly one Inkspan Office wheel, and `SHA256SUMS`. Missing, stale, unexpected, duplicate, non-regular, incompletely uploaded, or digest-mismatched assets fail closed. After upload and before publication, the authenticated paginated GitHub Releases API inventory must equal the local release directory by exact asset name, every remote asset must report an uploaded state, and every GitHub-reported `sha256:` digest must equal the digest of the exact transferred local file. The workflow does not silently delete an unexpected remote asset to make an ambiguous draft look clean. +Before immutable publication, the canonical draft inventory is **exactly four regular top-level files**: exactly one npm tarball, exactly one Inkspan Office wheel, `inkspan.spdx.json`, and `SHA256SUMS`. Missing, stale, unexpected, duplicate, non-regular, incompletely uploaded, or digest-mismatched assets fail closed. After upload and before publication, the authenticated paginated GitHub Releases API inventory must equal the local release directory by exact asset name, every remote asset must report an uploaded state, and every GitHub-reported `sha256:` digest must equal the digest of the exact transferred local file. The workflow does not silently delete an unexpected remote asset to make an ambiguous draft look clean. Rollback must preserve readable canonical documents and must not require silently reinterpreting persisted schema or selector-projection semantics. Host-owned migrations, persistence rollback, annotation re-anchoring, tenant recovery, and deployment rollback remain host responsibilities unless a future versioned contract explicitly assigns them to Inkspan. @@ -155,7 +155,7 @@ Rollback must preserve readable canonical documents and must not require silentl | editor chrome theming (Active PR / Proposed) | named `--cwl-*` tokens, DTCG interchange snapshot, Storybook inventory, inventoried pair contrast including `--cwl-accent` on `--cwl-accent-soft` | host brand CSS, contrast certification, Figma Variables, design-tool sync | | naruon composition | stable local package/module boundary | authenticated compose transport, tenancy, provider/model policy | | model assistance | deterministic proposal acceptance boundary | provider, prompt/data policy, credentials, human approval | -| release evidence | exact three-file draft inventory, package/artifact/digest verification and repository evidence | downstream deployment and operational rollout | +| release evidence | exact four-file draft inventory, package/artifact/digest verification and repository evidence | downstream deployment and operational rollout | ## Related canonical documents From d36c2e8e9ec5b018514ac1ad1eddbf47aa5c928f Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 18 Aug 2026 06:45:45 -0700 Subject: [PATCH 27/67] docs(release): align test strategy with four-file inventory --- docs/TEST_STRATEGY.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/TEST_STRATEGY.md b/docs/TEST_STRATEGY.md index 34e903fc..d9cde200 100644 --- a/docs/TEST_STRATEGY.md +++ b/docs/TEST_STRATEGY.md @@ -56,7 +56,7 @@ At minimum, maintain regressions for: - autosave stale validators, conflict/failure recovery, ambiguous transport outcomes, duplicate/no-op lifecycle transitions, callback exceptions, queue bounds, flush/close behavior, and durable-validator coherence; - selection/revision races and document movement during asynchronous hashing; - Office formula prefixes, invalid XML characters, malicious strings, path/publication races, invalid worksheet names, invalid freeze panes, cyclic input, pathological nesting, excessive container size, and partial write failure; -- package/release stale draft assets, unexpected or non-regular local entries, exact three-file inventory violations, incomplete remote uploads, GitHub-vs-local digest mismatch, stale exact-head evidence, mutable provenance inputs, and isolated packed-consumer behavior. +- package/release stale draft assets, unexpected or non-regular local entries, exact four-file inventory violations, incomplete remote uploads, GitHub-vs-local digest mismatch, stale exact-head evidence, mutable provenance inputs, and isolated packed-consumer behavior. ## Concurrency and failure testing @@ -68,7 +68,7 @@ Host persistence transactions, tenant isolation, distributed collaboration autho A release candidate requires the exact integrated protected head to satisfy applicable CI, security, JavaScript/TypeScript 100% statement/branch/function/line coverage, Office coverage.py 100% report plus public-docstring completeness, package-consumer, accessibility, browser differential, Office artifact, SBOM/provenance, reproducibility, unresolved-thread, actually required independent-review, and release-workflow gates. Queued, skipped-required, cancelled, absent, stale-head, predecessor-head, status-only, or synthetic-merge evidence is not accepted as success. -The release workflow must also satisfy the normative `docs/CONTRACTS.md` draft inventory contract: exactly one npm tarball, exactly one Office wheel, and `SHA256SUMS`; no other top-level entry; remote uploaded asset names exactly equal local names; and every GitHub-reported `sha256:` digest equals the exact transferred local file digest. Missing, stale, unexpected, non-regular, incomplete, or digest-mismatched assets are failures, not cleanup opportunities. +The release workflow must also satisfy the normative `docs/CONTRACTS.md` draft inventory contract: exactly one npm tarball, exactly one Office wheel, `inkspan.spdx.json`, and `SHA256SUMS`; no other top-level entry; remote uploaded asset names exactly equal local names; and every GitHub-reported `sha256:` digest equals the exact transferred local file digest. Missing, stale, unexpected, non-regular, incomplete, or digest-mismatched assets are failures, not cleanup opportunities. The 0.6.0 rich-clipboard release line specifically requires the protected dependency-locked **Playwright 1.62.0** Chromium, Firefox, and WebKit differential gate on the exact integrated protected release candidate before publication. Deterministic jsdom coverage remains useful but is not a substitute for browser-engine acceptance. Tagged release evidence must be generated anew from the release candidate and must verify the exact packed npm artifact, not merely reuse a previously green feature-branch run. From 938640afc0e2b9dc49d0635bc1a7a4129056518d Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 18 Aug 2026 06:46:30 -0700 Subject: [PATCH 28/67] docs(release): align operability with four-file inventory --- docs/OPERABILITY.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/OPERABILITY.md b/docs/OPERABILITY.md index e96d053c..23a22c6e 100644 --- a/docs/OPERABILITY.md +++ b/docs/OPERABILITY.md @@ -60,7 +60,7 @@ Release publication occurs only from an exact integrated protected head. The rel Before publication: 1. fetch the current protected `main` ref and require the release tag event commit SHA to equal that exact integration tip, not merely be an ancestor of it; -2. build exactly three regular top-level release files: exactly one npm tarball, exactly one Inkspan Office wheel, and `SHA256SUMS`; +2. build exactly four regular top-level release files: exactly one npm tarball, exactly one Inkspan Office wheel, `inkspan.spdx.json`, and `SHA256SUMS`; 3. reject missing, duplicate, non-regular, stale, or unexpected local entries and verify the local digests; 4. after upload, query the authenticated paginated GitHub Releases API and require the resumed remote draft asset-name set to equal the local release directory exactly; 5. require every remote asset state to be uploaded and every GitHub-reported `sha256:` digest to equal the exact transferred local file digest; From a74d336d34fcc619a96acaa64c5c3596a7347b7b Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 18 Aug 2026 06:47:14 -0700 Subject: [PATCH 29/67] docs(release): supersede stale three-file doctoring --- .../release-draft-asset-inventory.md | 29 +++++++++++-------- 1 file changed, 17 insertions(+), 12 deletions(-) diff --git a/docs/doctoring/release-draft-asset-inventory.md b/docs/doctoring/release-draft-asset-inventory.md index efee470e..5675b4c2 100644 --- a/docs/doctoring/release-draft-asset-inventory.md +++ b/docs/doctoring/release-draft-asset-inventory.md @@ -1,21 +1,24 @@ # Doctoring record: exact draft release asset inventory -**Decision date:** 2026-08-07 +**Decision date:** 2026-08-07 +**Current contract correction:** 2026-08-18 **Scope:** GitHub Release publication only; no Inkspan runtime, editor, persistence, collaboration, provider, tenant, credential, or database behavior changes. ## Problem Inkspan's release workflow intentionally supports retrying an unpublished GitHub Release draft. The upload command uses `--clobber`, which replaces same-name release assets. A prior failed attempt or operator action can nevertheless leave a differently named asset in the draft. Publishing that draft while immutable releases are enabled would freeze the extra asset into the official release identity. -This is a provenance-completeness problem rather than a checksum-collision problem. The expected npm tarball, Office wheel, and checksum file can each be correctly hashed and attested while an unrelated fourth asset remains attached to the draft. Verifying only the expected local artifacts therefore does not prove that the complete release asset set is the reviewed set. +This is a provenance-completeness problem rather than a checksum-collision problem. The current protected release workflow produces four reviewed top-level assets: one npm tarball, one Inkspan Office wheel, `inkspan.spdx.json`, and `SHA256SUMS`. All four can be correctly generated while an unrelated fifth asset remains attached to the draft. Verifying only the expected local artifacts therefore does not prove that the complete release asset set is the reviewed set. -A second local-boundary failure mode exists before upload. Counting only top-level regular files does not prove that the transfer directory has exactly three entries: an unexpected directory, symlink, socket, or other non-regular top-level entry could coexist with the three expected files. The shell upload glob can expand such an entry even though a `find ... -type f` count ignores it. The local release boundary therefore has to bind both the complete top-level entry set and the regular-file subset before attestation or upload. +A second local-boundary failure mode exists before upload. Counting only top-level regular files does not prove that the transfer directory has exactly four entries: an unexpected directory, symlink, socket, or other non-regular top-level entry could coexist with the four expected files. The shell upload glob can expand such an entry even though a `find ... -type f` count ignores it. The local release boundary therefore has to bind both the complete top-level entry set and the regular-file subset before attestation or upload. + +The original 2026-08-07 record described a three-file inventory because the release workflow at that stage did not yet publish the SPDX SBOM as a top-level release asset. Protected release source and executable contract tests now require `inkspan.spdx.json` as the fourth asset. This record supersedes that stale cardinality while retaining the original threat model and provenance rationale. ## Primary evidence GitHub documents that immutable-release protection begins after publication and that draft releases remain mutable beforehand. GitHub also recommends attaching all intended assets to the draft before publishing it. The GitHub CLI documents `gh release upload --clobber` as deleting and re-uploading an existing asset of the same name; it does not define `--clobber` as pruning differently named assets. The current Releases REST representation exposes each asset's `name`, `state`, and content `digest`, including SHA-256 digests. -Draft retrieval has an important API distinction. GitHub documents **List releases** as returning draft releases to authenticated callers with repository push access, while **Get a release by tag name** is explicitly documented as retrieving a published release. A pre-publication control must therefore not rely on the by-tag endpoint for draft evidence. Inkspan uses the paginated authenticated release listing, selects the exact tag, requires exactly one match, and then validates that match as a draft. +Draft retrieval has an important API distinction. GitHub documents **List releases** as returning draft releases to authenticated callers with repository push access, while **Get a release by tag name** is documented for a published release. A pre-publication control must therefore not rely on the by-tag endpoint for draft evidence. Inkspan uses the paginated authenticated release listing, selects the exact tag, requires exactly one match, and then validates that match as a draft. These properties make the pre-publication draft the last safe point at which Inkspan can verify both set completeness and byte identity without trying to repair an already immutable release. @@ -23,7 +26,7 @@ These properties make the pre-publication draft the last safe point at which Ink After the expected files cross the workflow-artifact privilege boundary and before attestation or upload, and again after upload before `gh release edit ... --draft=false`, the release workflow must: -1. Require the local transfer directory to contain exactly three top-level entries and require all three to be regular files: one `*.tgz`, one `*.whl`, and `SHA256SUMS`. Any extra directory, symlink, socket, device, or other non-regular top-level entry fails closed. +1. Require the local transfer directory to contain exactly four top-level entries and require all four to be regular files: one `*.tgz`, one `*.whl`, `inkspan.spdx.json`, and `SHA256SUMS`. Any extra directory, symlink, socket, device, regular file, or other top-level entry fails closed. 2. Paginate the authenticated GitHub **List releases** REST endpoint and select the exact release tag; require exactly one matching release object. 3. Require the selected remote object to remain a draft. 4. Compare the sorted local and remote asset-name sets for exact equality. @@ -37,14 +40,14 @@ The workflow does not automatically delete an unexpected stale asset. Automatic ### Covered -- A failed prior attempt leaves an obsolete wheel, tarball, checksum file, or other differently named asset in the draft. +- A failed prior attempt leaves an obsolete wheel, tarball, SBOM, checksum file, or other differently named asset in the draft. - A same-name remote asset contains bytes different from the transferred local file. - An upload is incomplete or an asset does not report the `uploaded` state. -- The local artifact directory unexpectedly contains multiple npm tarballs, multiple wheels, missing checksums, another regular file, or any additional non-regular top-level entry. +- The local artifact directory unexpectedly contains multiple npm tarballs, multiple wheels, a missing or duplicate `inkspan.spdx.json`, missing checksums, another regular file, or any additional non-regular top-level entry. - A symlink or directory replaces one of the expected regular files. - The release stops being a draft before the inventory check. - The draft cannot be uniquely identified in the complete authenticated release listing. -- A future refactor accidentally replaces draft-aware list evidence with the published-only by-tag endpoint. +- A future refactor accidentally replaces draft-aware list evidence with the published-only by-tag route. ### Deliberately outside this gate @@ -58,15 +61,17 @@ The comparison uses only public release filenames, asset states, tag identity, f ## Rollback -This change is workflow-only and can be reverted without changing package APIs or stored data. If GitHub changes draft visibility, release asset representation, pagination, or the digest contract, publication must remain fail-closed until the workflow and deterministic repository contract are updated against the new official API. Operators must not bypass the gate by weakening the expected artifact count or entry-type boundary, accepting ambiguous release matches, or accepting missing digest evidence. +This change is release-contract documentation and workflow assurance; it does not change package APIs or stored data. If GitHub changes draft visibility, release asset representation, pagination, or the digest contract, publication must remain fail-closed until the workflow and deterministic repository contract are updated against the new official API. Operators must not bypass the gate by weakening the expected artifact count or entry-type boundary, accepting ambiguous release matches, or accepting missing digest evidence. ## Verification -`src/releaseDraftAssetInventory.test.ts` fixes the security ordering and semantic markers in the permanent release workflow. It requires the exact-inventory check to occur after upload and before publication, requires the bounded three-file artifact set, requires paginated draft-aware list evidence rather than the published-only by-tag route, and requires state and digest validation plus explicit fail-closed diagnostics. On Linux, which is the release-runner class, the same test extracts and executes the exact reviewed shell body with a local fake `gh api` response and the runner's real Bash, `jq`, `find`, `diff`, and `sha256sum`. Deterministic fixtures prove the accepted exact-inventory path and fail-closed behavior for an unexpected remote asset, a digest mismatch, an incomplete upload state, and a release that is no longer a draft without network access or publication authority. +`src/releaseDraftAssetInventory.test.ts` is the executable protected release authority for the inventory. It requires the exact-inventory check to occur after upload and before publication, requires `expected_asset_count=4`, requires one npm tarball, one Office wheel, `inkspan.spdx.json`, and `SHA256SUMS`, uses paginated draft-aware list evidence rather than a published-only by-tag route, and requires state and digest validation plus explicit fail-closed diagnostics. On Linux, which is the release-runner class, the same test extracts and executes the reviewed shell body with a local fake `gh api` response and the runner's real Bash, `jq`, `find`, `diff`, and `sha256sum`. + +`src/releaseDraftAssetEntryType.test.ts` separately exercises the pre-attestation local entry-type boundary. Historical RED `0d905d9b244d36d55317de2237e3e3480c7ece5f` proved that a regular-file-only count could admit an unexpected top-level directory under the then-current inventory. The current contract generalizes that invariant to the exact four-file set: complete top-level cardinality and regular-file cardinality must both match before attestation or upload. -`src/releaseDraftAssetEntryType.test.ts` separately extracts and executes the exact pre-attestation local validation shell. Its RED test commit `0d905d9b244d36d55317de2237e3e3480c7ece5f` proved that the earlier regular-file-only count admitted a fourth top-level directory. The production repair counts the complete top-level entry set as well as the regular-file subset, so the same deterministic fixture now fails closed before attestation or upload while the exact three-file fixture remains accepted. +`src/releaseContractCanonicalConsistency.test.ts` rejects stale three-file wording across `docs/CONTRACTS.md`, `docs/TEST_STRATEGY.md`, `docs/OPERABILITY.md`, and this doctoring record while requiring all canonical documents to name the protected four-file SBOM-inclusive inventory. This prevents documentation from drifting behind executable release behavior again. -The feature branch was created from protected `main` commit `ca49a3249403be88ba3cb7c9589b3652f820e17c`. Test-only commits preceded the corresponding workflow implementations. Exact-current-head CI, security, automated review, independent review, and branch protection remain authoritative; predecessor-head or synthetic-merge evidence is not accepted as completion evidence. +Exact-current-head CI, security, automated review, independent review, and branch protection remain authoritative. Predecessor-head, queued, cancelled, skipped, status-only, or synthetic-merge evidence is not completion evidence. ## References From 0f7d3bdc7d69b278d11356fc571bee43ca54c2f0 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 18 Aug 2026 07:00:54 -0700 Subject: [PATCH 30/67] test(docs): reject active-PR shipped theme claims --- src/designTokenDocumentation.test.ts | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/src/designTokenDocumentation.test.ts b/src/designTokenDocumentation.test.ts index 6a3bea25..f9f25b65 100644 --- a/src/designTokenDocumentation.test.ts +++ b/src/designTokenDocumentation.test.ts @@ -32,7 +32,8 @@ describe('editor design-token documentation contract', () => { expect(doctoring).toContain('ADR 0031'); expect(doctoring).not.toContain('ADR 0027'); expect(operatorGuide).toContain('When a host overrides any color token'); - expect(operatorGuide).toContain('shipped dark active-toolbar pair now uses'); + expect(operatorGuide).toContain('active-PR dark active-toolbar pair uses'); + expect(operatorGuide).not.toContain('shipped dark active-toolbar pair now uses'); expect(operatorGuide).toContain('getEditorThemeTokenContrast'); expect(operatorGuide).toContain("getEditorThemeTokenContrast('cwl-accent', 'cwl-accent-soft', 'dark')"); expect(operatorGuide).toContain('meetsTextContrast'); @@ -40,13 +41,16 @@ describe('editor design-token documentation contract', () => { expect(operatorGuide).toContain('Do not edit Inkspan internals'); expect(inventory).toContain('Editor Chrome / Toolbar Button States'); expect(inventory).toContain('Editor Chrome / Live Toolbar'); - expect(inventory).toContain('shipped Toolbar'); + expect(inventory).toContain('active-PR Toolbar'); + expect(inventory).not.toContain('shipped Toolbar'); expect(inventory).toContain(':focus-visible'); - expect(inventory).toContain('Shipped defaults require no host override'); + expect(inventory).toContain('Active-PR defaults require no host override'); + expect(inventory).not.toContain('Shipped defaults require no host override'); expect(inventory).toContain('If re-theming, override `--cwl-accent`'); expect(doctoring).toContain('cwl-accent-soft'); expect(doctoring).toContain('meets the WCAG 2.2 4.5:1'); - expect(doctoring).toContain('Inkspan now ships dark `--cwl-accent: #58a6ff`'); + expect(doctoring).toContain('Active PR now uses dark `--cwl-accent: #58a6ff`'); + expect(doctoring).not.toContain('Inkspan now ships dark `--cwl-accent: #58a6ff`'); expect(doctoring).toContain('producing about 5.06:1'); expect(doctoring).toContain('Host overrides must still re-check their own resulting pairs'); expect(doctoring).toContain('Design Tokens Format Module 2025.10'); @@ -56,7 +60,8 @@ describe('editor design-token documentation contract', () => { expect(doctoring).toContain('https://www.w3.org/TR/WCAG22/'); expect(doctoring).toContain('https://storybook.js.org/docs/get-started/frameworks/react-vite'); expect(changelog).toContain('Named the repeating editor chrome as a host-facing theme-token catalog'); - expect(changelog).toContain('Raised the shipped dark active-toolbar accent'); + expect(changelog).toContain('Raised the active-PR dark active-toolbar accent'); + expect(changelog).not.toContain('Raised the shipped dark active-toolbar accent'); expect(repositoryFile('.storybook/main.ts')).toContain("@storybook/react-vite"); expect(repositoryFile('stories/EditorChrome.stories.tsx')).toContain('cwl-tb-btn'); expect(repositoryFile('stories/EditorChrome.stories.tsx')).toContain('autoFocus'); From 6d5dbe7371302b1f057a3a122de8039ba28a964d Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 18 Aug 2026 07:04:48 -0700 Subject: [PATCH 31/67] test(docs): narrow active-PR truth contract --- src/designTokenDocumentation.test.ts | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/src/designTokenDocumentation.test.ts b/src/designTokenDocumentation.test.ts index f9f25b65..3d3af081 100644 --- a/src/designTokenDocumentation.test.ts +++ b/src/designTokenDocumentation.test.ts @@ -18,6 +18,7 @@ describe('editor design-token documentation contract', () => { const changelog = normalize(repositoryFile('CHANGELOG.md')); const index = repositoryFile('docs/README.md'); const adrIndex = repositoryFile('docs/adr/README.md'); + const adr = repositoryFile('docs/adr/0031-editor-design-tokens-storybook.md'); expect(index).toContain('[`design-tokens.md`](design-tokens.md)'); expect(adrIndex).toContain('[0031](0031-editor-design-tokens-storybook.md) | Proposed'); @@ -32,7 +33,7 @@ describe('editor design-token documentation contract', () => { expect(doctoring).toContain('ADR 0031'); expect(doctoring).not.toContain('ADR 0027'); expect(operatorGuide).toContain('When a host overrides any color token'); - expect(operatorGuide).toContain('active-PR dark active-toolbar pair uses'); + expect(operatorGuide).toContain('Active-PR repaired dark active-toolbar pair uses'); expect(operatorGuide).not.toContain('shipped dark active-toolbar pair now uses'); expect(operatorGuide).toContain('getEditorThemeTokenContrast'); expect(operatorGuide).toContain("getEditorThemeTokenContrast('cwl-accent', 'cwl-accent-soft', 'dark')"); @@ -41,16 +42,17 @@ describe('editor design-token documentation contract', () => { expect(operatorGuide).toContain('Do not edit Inkspan internals'); expect(inventory).toContain('Editor Chrome / Toolbar Button States'); expect(inventory).toContain('Editor Chrome / Live Toolbar'); - expect(inventory).toContain('active-PR Toolbar'); - expect(inventory).not.toContain('shipped Toolbar'); + expect(inventory).toContain('shipped Toolbar'); expect(inventory).toContain(':focus-visible'); - expect(inventory).toContain('Active-PR defaults require no host override'); + expect(inventory).toContain('Active-PR repaired defaults require no host override'); expect(inventory).not.toContain('Shipped defaults require no host override'); expect(inventory).toContain('If re-theming, override `--cwl-accent`'); expect(doctoring).toContain('cwl-accent-soft'); expect(doctoring).toContain('meets the WCAG 2.2 4.5:1'); - expect(doctoring).toContain('Active PR now uses dark `--cwl-accent: #58a6ff`'); + expect(doctoring).toContain('This Active PR now carries dark `--cwl-accent: #58a6ff`'); expect(doctoring).not.toContain('Inkspan now ships dark `--cwl-accent: #58a6ff`'); + expect(doctoring).toContain('host-facing catalog of the nine inventoried chrome tokens'); + expect(doctoring).not.toContain('host-facing catalog of the nine shipped chrome tokens'); expect(doctoring).toContain('producing about 5.06:1'); expect(doctoring).toContain('Host overrides must still re-check their own resulting pairs'); expect(doctoring).toContain('Design Tokens Format Module 2025.10'); @@ -59,8 +61,11 @@ describe('editor design-token documentation contract', () => { ); expect(doctoring).toContain('https://www.w3.org/TR/WCAG22/'); expect(doctoring).toContain('https://storybook.js.org/docs/get-started/frameworks/react-vite'); + expect(adr).toContain('Status: Proposed'); + expect(adr).toContain('If integrated, Inkspan will publish a host-facing theme-token catalog'); + expect(adr).not.toContain('Inkspan publishes a host-facing theme-token catalog'); expect(changelog).toContain('Named the repeating editor chrome as a host-facing theme-token catalog'); - expect(changelog).toContain('Raised the active-PR dark active-toolbar accent'); + expect(changelog).toContain('Prepared the active-PR dark active-toolbar accent'); expect(changelog).not.toContain('Raised the shipped dark active-toolbar accent'); expect(repositoryFile('.storybook/main.ts')).toContain("@storybook/react-vite"); expect(repositoryFile('stories/EditorChrome.stories.tsx')).toContain('cwl-tb-btn'); From 24e519558ebfe5c85f5fbf663e2881f92fba274a Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 18 Aug 2026 07:05:23 -0700 Subject: [PATCH 32/67] test(release): include changelog in four-asset contract --- src/releaseContractCanonicalConsistency.test.ts | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/src/releaseContractCanonicalConsistency.test.ts b/src/releaseContractCanonicalConsistency.test.ts index b001c79f..749a7045 100644 --- a/src/releaseContractCanonicalConsistency.test.ts +++ b/src/releaseContractCanonicalConsistency.test.ts @@ -10,6 +10,7 @@ function repositoryFile(path: string): string { describe('canonical release artifact inventory consistency', () => { it('keeps canonical release documents aligned with the protected four-file boundary', () => { + const changelog = repositoryFile('CHANGELOG.md'); const contracts = repositoryFile('docs/CONTRACTS.md'); const operability = repositoryFile('docs/OPERABILITY.md'); const releaseDoctoring = repositoryFile( @@ -42,10 +43,21 @@ describe('canonical release artifact inventory consistency', () => { ); expect(releaseDoctoring).toContain('`inkspan.spdx.json`'); - for (const document of [contracts, testStrategy, operability, releaseDoctoring]) { + expect(changelog).toContain( + 'one npm tarball, one Office wheel, `inkspan.spdx.json`, and `SHA256SUMS`', + ); + + for (const document of [ + changelog, + contracts, + testStrategy, + operability, + releaseDoctoring, + ]) { expect(document).not.toContain('exactly three regular top-level files'); expect(document).not.toContain('exact three-file draft inventory'); expect(document).not.toContain('exactly three top-level entries'); + expect(document).not.toContain('one npm tarball, one Office wheel, and `SHA256SUMS`'); } }); }); From 15486eb28413f907232d38799add778f671224d3 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 18 Aug 2026 07:08:30 -0700 Subject: [PATCH 33/67] docs(theme): separate active repair from shipped defaults --- docs/design-tokens.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/design-tokens.md b/docs/design-tokens.md index 32b40634..42255293 100644 --- a/docs/design-tokens.md +++ b/docs/design-tokens.md @@ -2,7 +2,7 @@ Status: Active PR / Proposed -Use this catalog when you need to re-theme Inkspan's repeating toolbar and editor chrome. Inkspan's shipped light, dark, and print token pairs are checked by repository tests; the shipped dark active-toolbar pair now uses `--cwl-accent: #58a6ff` on `--cwl-accent-soft: #163356` and measures about 5.06:1. When a host overrides any color token, re-check WCAG 2.2 contrast for both body text (`--cwl-fg` on `--cwl-bg`) and active toolbar text (`--cwl-accent` on `--cwl-accent-soft`). Do not edit Inkspan internals. +Use this catalog when you need to re-theme Inkspan's repeating toolbar and editor chrome. Protected-main CSS defaults remain the shipped presentation baseline; the Active-PR repaired dark active-toolbar pair uses `--cwl-accent: #58a6ff` on `--cwl-accent-soft: #163356` and measures about 5.06:1. When a host overrides any color token, re-check WCAG 2.2 contrast for both body text (`--cwl-fg` on `--cwl-bg`) and active toolbar text (`--cwl-accent` on `--cwl-accent-soft`). Do not edit Inkspan internals. ```css .cwl-editor { @@ -31,7 +31,7 @@ void tokens; void dtcgGroup; ``` -The default-theme checks above are product evidence, not a host WCAG certification. The stylesheet remains runtime presentation authority. `toDesignTokenFormatGroup()` is an interchange snapshot aligned to Design Tokens Format Module 2025.10; it is not complete DTCG conformance or Figma Variables sync. +The default-theme checks above are active-PR product evidence, not protected-main shipped evidence or a host WCAG certification. The stylesheet remains runtime presentation authority. `toDesignTokenFormatGroup()` is an interchange snapshot aligned to Design Tokens Format Module 2025.10; it is not complete DTCG conformance or Figma Variables sync. Preview the repeating objects in Storybook (`pnpm storybook`) using the inventory in [`storybook-inventory.md`](storybook-inventory.md). From cfa8a2f3982a2834f516384ce7f74a0e47bc6598 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 18 Aug 2026 07:08:55 -0700 Subject: [PATCH 34/67] docs(storybook): label repaired contrast as active PR --- docs/storybook-inventory.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/storybook-inventory.md b/docs/storybook-inventory.md index bd35b8c0..239e40f6 100644 --- a/docs/storybook-inventory.md +++ b/docs/storybook-inventory.md @@ -2,7 +2,7 @@ Status: Active PR / Proposed -Run `pnpm storybook` to preview repeating Inkspan chrome before you override tokens in a host. **Shipped defaults require no host override for the inventoried active-toolbar contrast pair.** Override tokens only when re-theming, and re-check the resulting host palette. The button-state story is a class-level chrome sample. **Editor Chrome / Live Toolbar** mounts the shipped Toolbar so a token change is visible on the same objects buyers ship. These stories do not mount `CwlEditor`. +Run `pnpm storybook` to preview repeating Inkspan chrome before you override tokens in a host. **Active-PR repaired defaults require no host override for the inventoried active-toolbar contrast pair; protected main remains the pre-repair shipped baseline until integration.** Override tokens only when re-theming, and re-check the resulting host palette. The button-state story is a class-level chrome sample. **Editor Chrome / Live Toolbar** mounts the shipped Toolbar component so a token change is visible on the same object buyers already use; the Storybook story and repaired accent remain Active PR / Proposed. These stories do not mount `CwlEditor`. | Story | Repeating object | Host next action | | --- | --- | --- | From affd46a9e0c702e690179b8eddd2ffa8176b4c75 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 18 Aug 2026 07:09:28 -0700 Subject: [PATCH 35/67] docs(doctoring): separate proposal from shipped truth --- docs/doctoring/editor-design-tokens.md | 26 ++++++++++++++------------ 1 file changed, 14 insertions(+), 12 deletions(-) diff --git a/docs/doctoring/editor-design-tokens.md b/docs/doctoring/editor-design-tokens.md index f3f33d4e..bf7720b7 100644 --- a/docs/doctoring/editor-design-tokens.md +++ b/docs/doctoring/editor-design-tokens.md @@ -7,41 +7,43 @@ ## Buyer-visible gap -Hosts embed Inkspan and need to match brand color, radius, and font without forking `src/styles.css`. The stylesheet already used `--cwl-*` custom properties, but buyers had no typed catalog, no interchange snapshot, and no Storybook inventory of the repeating toolbar button and editor chrome. Theme work therefore required reading CSS internals. +Hosts embed Inkspan and need to match brand color, radius, and font without forking `src/styles.css`. The protected stylesheet already uses `--cwl-*` custom properties, but buyers have no protected-main typed catalog, interchange snapshot, or Storybook inventory of the repeating toolbar button and editor chrome. Theme work therefore still requires reading CSS internals until this active PR integrates. -The same inventory exposed an Inkspan-owned default-theme defect rather than a host-only customization problem: dark `.cwl-tb-btn.is-active` rendered `--cwl-accent` text on `--cwl-accent-soft` at about 4.13:1, below the WCAG 2.2 4.5:1 threshold for normal text. Inkspan now ships dark `--cwl-accent: #58a6ff` against unchanged `--cwl-accent-soft: #163356`, producing about 5.06:1 for the active toolbar pair. Host overrides must still re-check their own resulting pairs. +The same inventory exposed an Inkspan-owned protected-main default-theme defect rather than a host-only customization problem: dark `.cwl-tb-btn.is-active` renders `--cwl-accent` text on `--cwl-accent-soft` at about 4.13:1, below the WCAG 2.2 4.5:1 threshold for normal text. This Active PR now carries dark `--cwl-accent: #58a6ff` against unchanged `--cwl-accent-soft: #163356`, producing about 5.06:1 for the active toolbar pair. That repaired value is active-PR evidence, not shipped protected-main truth, until integration. Host overrides must still re-check their own resulting pairs. If contrast fails after a re-theme, override only the named tokens on `.cwl-editor` and re-check WCAG 2.2 text contrast for `--cwl-fg` on `--cwl-bg` and `--cwl-accent` on `--cwl-accent-soft`. Do not disable forced-colors overrides. ## Decision +If integrated: + 1. Keep `src/styles.css` as runtime presentation authority. -2. Publish `listEditorThemeTokens()` / `getEditorThemeToken()` / `getEditorThemeTokenContrast()` / `toDesignTokenFormatGroup()` as a host-facing catalog of the nine shipped chrome tokens. -3. Keep the shipped light/dark/print catalog values synchronized with the stylesheet and require the inventoried active-toolbar text pair to meet the WCAG 2.2 4.5:1 threshold in Inkspan's own default themes. +2. Publish `listEditorThemeTokens()` / `getEditorThemeToken()` / `getEditorThemeTokenContrast()` / `toDesignTokenFormatGroup()` as a host-facing catalog of the nine inventoried chrome tokens. +3. Keep the catalog light/dark/print values synchronized with the stylesheet and require the inventoried active-toolbar text pair to meet the WCAG 2.2 4.5:1 threshold in Inkspan's own default themes. 4. Reject unknown token names with a stable payload-redacted `EditorThemeTokenError`. -5. Preview repeating `.cwl-tb-btn` states, the shipped Toolbar, and token swatches in Storybook. +5. Preview repeating `.cwl-tb-btn` states, the shipped Toolbar component, and token swatches in Storybook. 6. Add no network, persistence, credential, model, tenant, Figma, or design-tool sync authority. ## Standards rationale -The Design Tokens Format Module 2025.10 defines a vendor-neutral JSON interchange for token groups, `$type`, and `$value` (Design Tokens Community Group, 2025). Inkspan emits a snapshot of its CSS custom properties in that shape. The report is a W3C Community Final Specification, not a W3C Standard, so this record does not claim W3C standardization or complete DTCG conformance. +The Design Tokens Format Module 2025.10 defines a vendor-neutral JSON interchange for token groups, `$type`, and `$value` (Design Tokens Community Group, 2025). This active PR emits a snapshot of Inkspan CSS custom properties in that shape. The report is a W3C Community Final Specification, not a W3C Standard, so this record does not claim W3C standardization or complete DTCG conformance. -WCAG 2.2 requires at least 4.5:1 contrast for normal text under Success Criterion 1.4.3 and at least 3:1 for meaningful user-interface component boundaries/states under Success Criterion 1.4.11 (World Wide Web Consortium, 2024). Inkspan therefore fixes a failing shipped default at its own presentation boundary; host overrides remain the host's contrast responsibility. Storybook's React/Vite preview is the inventory surface for repeating chrome (Storybook, n.d.). +WCAG 2.2 requires at least 4.5:1 contrast for normal text under Success Criterion 1.4.3 and at least 3:1 for meaningful user-interface component boundaries/states under Success Criterion 1.4.11 (World Wide Web Consortium, 2024). The protected-main failing default is therefore an Inkspan-owned defect; this active PR repairs it at Inkspan's presentation boundary while host overrides remain the host's contrast responsibility. Storybook's React/Vite preview is the proposed inventory surface for repeating chrome (Storybook, n.d.). ## Test-first evidence - Original RED: `src/designTokens.test.ts` failed because `./designTokens.js` did not exist. -- Initial GREEN: the catalog lists the nine shipped tokens, aligns light/dark/print color values with the matching `src/styles.css` media blocks, reports WCAG 2.2 contrast for shipped color pairs, rejects unknown names without reflecting caller input, and emits a DTCG 2025.10 group. -- Accessibility RED: exact test-only head `a831359d1509811ab8777e7356f6ebd5f251b5cf` changed the active-chrome contract to require the shipped dark `--cwl-accent` / `--cwl-accent-soft` pair to meet 4.5:1. The predecessor production values remained `#4493f8` on `#163356` (about 4.13:1), so the new expectation could not pass without a real default-theme change. -- Accessibility GREEN: dark `--cwl-accent` is `#58a6ff` in both the typed catalog and runtime stylesheet; against unchanged `#163356` it measures about 5.06:1 and `meetsTextContrast` is true. The host override helper remains available for custom themes. +- Initial GREEN: the active-PR catalog lists nine inventoried tokens, aligns light/dark/print color values with the matching `src/styles.css` media blocks, reports WCAG 2.2 contrast for inventoried color pairs, rejects unknown names without reflecting caller input, and emits a DTCG 2025.10 group. +- Accessibility RED: exact test-only head `a831359d1509811ab8777e7356f6ebd5f251b5cf` changed the active-chrome contract to require the dark `--cwl-accent` / `--cwl-accent-soft` pair to meet 4.5:1. The protected-main values remained `#4493f8` on `#163356` (about 4.13:1), so the new expectation could not pass without a real default-theme change. +- Accessibility GREEN: the active-PR dark `--cwl-accent` is `#58a6ff` in both the typed catalog and runtime stylesheet; against unchanged `#163356` it measures about 5.06:1 and `meetsTextContrast` is true. The host override helper remains available for custom themes. ## Residual risk -Print media still remaps the color tokens after a host override. Forced-colors mode only restyles the toolbar focus outline to `CanvasText`; it does not assign `--cwl-*` values. The shipped dark `.cwl-tb-btn.is-active` pair (`--cwl-accent` on `--cwl-accent-soft`) now meets the WCAG 2.2 4.5:1 normal-text threshold and also exceeds the 3:1 non-text threshold; hosts must still call `getEditorThemeTokenContrast('cwl-accent', 'cwl-accent-soft', 'dark')` after overriding either token because custom values can reintroduce a contrast failure. The font token snapshot splits a CSS font-family list and does not execute CSS. Storybook success is not Chromium/Firefox/WebKit release evidence. +Print media still remaps the color tokens after a host override. Forced-colors mode only restyles the toolbar focus outline to `CanvasText`; it does not assign `--cwl-*` values. The active-PR repaired dark `.cwl-tb-btn.is-active` pair (`--cwl-accent` on `--cwl-accent-soft`) meets the WCAG 2.2 4.5:1 normal-text threshold and also exceeds the 3:1 non-text threshold; protected main remains at the failing pre-repair pair until integration. Hosts must still call `getEditorThemeTokenContrast('cwl-accent', 'cwl-accent-soft', 'dark')` after overriding either token because custom values can reintroduce a contrast failure. The font token snapshot splits a CSS font-family list and does not execute CSS. Storybook success is not Chromium/Firefox/WebKit release evidence. ## Rollback -Rollback must remove the catalog export, this record, the operator guide, the Storybook inventory/config/stories, ADR 0031, the changelog entry, and the documentation-index rows together. Reverting the dark accent value without also reverting the active-pair contrast contract would deliberately recreate a known accessibility defect and is not a valid partial rollback. +Rollback before integration removes the catalog export, this record, the operator guide, the Storybook inventory/config/stories, ADR 0031, the changelog entry, and the documentation-index rows together. After integration, reverting the compliant dark accent without also reverting the active-pair contrast contract would deliberately recreate a known accessibility defect and is not a valid partial rollback. ## References (APA 7th edition) From d0b3494bc876abdcecba32eda023d3f85f363cdf Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 18 Aug 2026 07:10:03 -0700 Subject: [PATCH 36/67] docs(adr): keep proposed theme decision non-shipped --- .../0031-editor-design-tokens-storybook.md | 24 +++++++++---------- 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/docs/adr/0031-editor-design-tokens-storybook.md b/docs/adr/0031-editor-design-tokens-storybook.md index 8fd3a582..fe30bec6 100644 --- a/docs/adr/0031-editor-design-tokens-storybook.md +++ b/docs/adr/0031-editor-design-tokens-storybook.md @@ -4,42 +4,42 @@ Status: Proposed ## Context -Inkspan ships repeating toolbar buttons, groups, and editor chrome styled through `--cwl-*` custom properties. Hosts already re-theme by overriding those properties, but the names, light/dark/print values, and buyer next action lived only inside `src/styles.css`. There was no typed catalog, no Design Tokens Format Module interchange snapshot, and no Storybook inventory of the repeating objects. +Inkspan ships repeating toolbar buttons, groups, and editor chrome styled through `--cwl-*` custom properties. Hosts already re-theme by overriding those properties, but the names, light/dark/print values, and buyer next action live only inside protected `src/styles.css`. There is no protected-main typed catalog, Design Tokens Format Module interchange snapshot, or Storybook inventory of the repeating objects; those capabilities are proposed on this active PR. -Inventorying the actual active-toolbar foreground/background pair also exposed a product-owned accessibility defect: the former dark `--cwl-accent: #4493f8` on `--cwl-accent-soft: #163356` produced about 4.13:1 for 13px active-button text, below the WCAG 2.2 4.5:1 normal-text threshold. Treating that shipped-default failure as a host override responsibility would contradict Inkspan's ownership of its default presentation. +Inventorying the actual active-toolbar foreground/background pair also exposed a product-owned accessibility defect: protected main's dark `--cwl-accent: #4493f8` on `--cwl-accent-soft: #163356` produces about 4.13:1 for 13px active-button text, below the WCAG 2.2 4.5:1 normal-text threshold. Treating that shipped-default failure as a host override responsibility would contradict Inkspan's ownership of its default presentation. ## Alternatives considered -1. **Leave theming as undocumented CSS.** Rejected because hosts would keep reading internals and drift from the shipped token set. +1. **Leave theming as undocumented CSS.** Rejected because hosts would keep reading internals and drift from the protected token set. 2. **Generate CSS from a new token source of truth.** Rejected for this slice because it would rewrite the protected stylesheet and collide with print, forced-colors, and Office-unrelated presentation contracts. 3. **Add Figma Variables sync or a design-tool connector.** Rejected because Figma/design-tool credentials and sync authority are host-owned; Inkspan remains independently usable. -4. **Disclose the failing shipped dark pair and require every host to override it.** Rejected because the default palette is Inkspan-owned; documenting a product defect does not repair it. -5. **Publish a named catalog plus Storybook inventory while CSS remains runtime authority, and repair any failing shipped inventoried pair at the stylesheet boundary.** Selected because hosts can override and preview repeating chrome without moving presentation authority, while Inkspan remains accountable for its own defaults. +4. **Disclose the failing shipped dark pair and require every host to override it.** Rejected because the protected default palette is Inkspan-owned; documenting a product defect does not repair it. +5. **Publish a named catalog plus Storybook inventory while CSS remains runtime authority, and repair the failing protected-main inventoried pair at the stylesheet boundary.** Selected because hosts can override and preview repeating chrome without moving presentation authority, while Inkspan remains accountable for its own defaults. ## Decision -Inkspan publishes a host-facing theme-token catalog for the nine shipped chrome tokens, a Design Tokens Format Module 2025.10 interchange snapshot, and a Storybook inventory of repeating toolbar/editor objects. Inkspan's own inventoried normal-text pairs must meet the WCAG 2.2 4.5:1 threshold in shipped light/dark/print defaults; the dark accent is therefore `#58a6ff` on `#163356` for active toolbar text, about 5.06:1. Hosts overriding `--cwl-*` on `.cwl-editor` must re-check contrast for their resulting body and active-toolbar pairs. Unknown token names fail closed. No Figma, network, persistence, credential, or model authority is added. +If integrated, Inkspan will publish a host-facing theme-token catalog for nine inventoried chrome tokens, a Design Tokens Format Module 2025.10 interchange snapshot, and a Storybook inventory of repeating toolbar/editor objects. Inkspan's inventoried normal-text pairs will be required to meet the WCAG 2.2 4.5:1 threshold in the resulting protected light/dark/print defaults; the active-PR candidate therefore uses `#58a6ff` on `#163356` for dark active-toolbar text, about 5.06:1. Hosts overriding `--cwl-*` on `.cwl-editor` must re-check contrast for their resulting body and active-toolbar pairs. Unknown token names fail closed. No Figma, network, persistence, credential, or model authority is added. ## Consequences and ownership trade-offs -Hosts gain a copyable token list and a Storybook preview. Inkspan keeps CSS as the runtime source and owns accessibility defects in its default token combinations. naruon and other CWL hosts can apply their own overrides through host-owned brand CSS and then use the same contrast helpers to validate those custom values. Complete DTCG conformance, Figma Variables, host-theme WCAG certification, and automated remediation of arbitrary host palettes remain out of scope. +After integration, hosts gain a copyable token list and a Storybook preview. Inkspan keeps CSS as the runtime source and owns accessibility defects in its default token combinations. naruon and other CWL hosts can apply their own overrides through host-owned brand CSS and then use the same contrast helpers to validate those custom values. Complete DTCG conformance, Figma Variables, host-theme WCAG certification, and automated remediation of arbitrary host palettes remain out of scope. ## Failure and recovery -An unknown token name throws `EditorThemeTokenError` without reflecting caller input. A shipped default pair that fails an applicable repository contrast contract is repaired in Inkspan's token catalog and stylesheet together. A host override that fails contrast is recovered by changing only the named host tokens, not by editing Inkspan internals or disabling forced-colors. +An unknown token name throws `EditorThemeTokenError` without reflecting caller input. A protected default pair that fails an applicable repository contrast contract must be repaired in Inkspan's token catalog and stylesheet together before integration or release. A host override that fails contrast is recovered by changing only the named host tokens, not by editing Inkspan internals or disabling forced-colors. ## Security and privacy impact -The catalog contains only public presentation values. It does not carry document bodies, tenant identifiers, credentials, or diagnostics. Storybook is a local development preview and is not a production transport. +The proposed catalog contains only public presentation values. It does not carry document bodies, tenant identifiers, credentials, or diagnostics. Storybook is a local development preview and is not a production transport. ## Compatibility and migration -The catalog is additive. Existing CSS overrides on `.cwl-editor` continue to work. The dark default accent changes from `#4493f8` to `#58a6ff`; hosts that already override `--cwl-accent` are unaffected by that default-value change but remain responsible for validating their custom pair. A later CSS token addition or default-value change must update the catalog, directly affected documentation/tests, and this ADR together. +The catalog is additive if integrated. Existing CSS overrides on `.cwl-editor` continue to work. The active-PR candidate changes the dark default accent from `#4493f8` to `#58a6ff`; hosts that already override `--cwl-accent` are unaffected by that candidate default-value change but remain responsible for validating their custom pair. A later CSS token addition or default-value change must update the catalog, directly affected documentation/tests, and this ADR together. ## Verification and acceptance evidence -Required evidence includes token-catalog tests against `src/styles.css`, deterministic contrast assertions for inventoried normal-text pairs, documentation-contract tests, Storybook inventory stories for toolbar button states and token swatches, and exact-head CI/coverage/package/security gates on the unchanged head. The accessibility regression must fail against the former `#4493f8`/`#163356` dark active pair and pass against the new `#58a6ff`/`#163356` pair. This ADR stays Proposed until protected integration. +Required evidence includes token-catalog tests against `src/styles.css`, deterministic contrast assertions for inventoried normal-text pairs, documentation-contract tests, Storybook inventory stories for toolbar button states and token swatches, and exact-head CI/coverage/package/security gates on the unchanged head. The accessibility regression must fail against protected main's `#4493f8`/`#163356` dark active pair and pass against the active-PR `#58a6ff`/`#163356` pair. This ADR stays Proposed until protected integration. ## Rollback or supersession -Rollback removes the catalog export, Storybook inventory/config, operator/doctoring records, and this ADR together. Reverting only the compliant dark accent while retaining the active-text contrast requirement is not a valid partial rollback. Supersession requires a new ADR if CSS ceases to be runtime presentation authority or if a design-tool sync contract is accepted. +Before integration, rollback removes the catalog export, Storybook inventory/config, operator/doctoring records, and this ADR together. After integration, reverting only the compliant dark accent while retaining the active-text contrast requirement is not a valid partial rollback. Supersession requires a new ADR if CSS ceases to be runtime presentation authority or if a design-tool sync contract is accepted. From 2a23839b423c2a6003a22c11c23c9540dd6f9e60 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 18 Aug 2026 07:12:03 -0700 Subject: [PATCH 37/67] docs(changelog): converge active and release truth --- CHANGELOG.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ce99b09f..0df47b64 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,7 +10,7 @@ Historical release entries from **0.1.0 through 0.5.27** are preserved verbatim - Named the repeating editor chrome as a host-facing theme-token catalog and Storybook inventory so hosts can override `--cwl-*` custom properties on `.cwl-editor` after checking WCAG 2.2 contrast, without editing Inkspan internals. Color catalog values now distinguish light, dark, and `@media print` remaps; forced-colors mode is not treated as a token assignment. Hosts can call `getEditorThemeTokenContrast()` to compare inventoried pairs, including `--cwl-accent` on `--cwl-accent-soft`, against the 4.5:1 text threshold via `meetsTextContrast`. ### Accessibility -- Raised the shipped dark active-toolbar accent from `#4493f8` to `#58a6ff`, increasing `--cwl-accent` text on `--cwl-accent-soft: #163356` from about 4.13:1 to about 5.06:1 so Inkspan's default 13px active-button text meets the WCAG 2.2 4.5:1 normal-text threshold while host overrides remain explicitly re-checkable through `getEditorThemeTokenContrast()`. +- Prepared the active-PR dark active-toolbar accent change from protected-main `#4493f8` to `#58a6ff`, increasing `--cwl-accent` text on `--cwl-accent-soft: #163356` from about 4.13:1 to about 5.06:1 so the candidate default 13px active-button text meets the WCAG 2.2 4.5:1 normal-text threshold; this remains active-PR evidence until protected integration, and host overrides remain explicitly re-checkable through `getEditorThemeTokenContrast()`. ## [0.6.0] — 2026-08-10 @@ -21,7 +21,7 @@ Historical release entries from **0.1.0 through 0.5.27** are preserved verbatim - Added the selected standalone Markdown or HTML value to an explicitly configured SSR native form field, preserving controlled-value precedence, external form association, React attribute escaping, and the synchronous post-hydration TipTap transaction mirror ### Security -- Added a fail-closed draft release asset inventory gate that requires exactly one npm tarball, one Office wheel, and `SHA256SUMS`, rejects stale or unexpected draft assets before immutable publication, and verifies every GitHub-reported `sha256:` asset digest against the transferred local file +- Added a fail-closed draft release asset inventory gate that requires exactly one npm tarball, one Office wheel, `inkspan.spdx.json`, and `SHA256SUMS`, rejects stale or unexpected draft assets before immutable publication, and verifies every GitHub-reported `sha256:` asset digest against the transferred local file - Kept SSR document disclosure opt-in through `formFieldName`; hidden-field values remain client-controlled submission data and do not replace host authentication, authorization, tenant isolation, CSRF defenses, server validation, durable concurrency, or persistence controls - Kept collaborative Yjs document content out of server markup until the host-owned client collaboration lifecycle is bound - Added packed headless Markdown authority verification that rejects external runtime imports, dynamic module loaders, ambient network/environment credential access, React/TipTap/Yjs runtime coupling, CWL host coupling, and model credential references from the dedicated conversion artifact From bb49858d5900971ee09aee3fd1f4ac75575e6c05 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 18 Aug 2026 07:36:01 -0700 Subject: [PATCH 38/67] test(a11y): require actionable override contrast guidance --- src/designTokens.test.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/designTokens.test.ts b/src/designTokens.test.ts index 1068fdba..278a2533 100644 --- a/src/designTokens.test.ts +++ b/src/designTokens.test.ts @@ -119,6 +119,7 @@ describe('editor theme token catalog', () => { expect(darkActive.meetsNonTextContrast).toBe(true); expect(darkActive.hostAction).toContain('--cwl-accent'); expect(darkActive.hostAction).toContain('--cwl-accent-soft'); + expect(darkActive.hostAction).toContain('contrastRatioFromHex'); expect(darkActive.hostAction).not.toContain('below 4.5:1'); expect(printActive.meetsTextContrast).toBe(true); expect(accentOnBackground.meetsTextContrast).toBe(true); @@ -175,4 +176,4 @@ describe('editor theme token catalog', () => { expect(group.cwl.font.$type).toBe('fontFamily'); expect(Array.isArray(group.cwl.font.$value)).toBe(true); }); -}); \ No newline at end of file +}); From 8df97362add8e3ae9148df7c3f78934d30fc917f Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 18 Aug 2026 07:39:14 -0700 Subject: [PATCH 39/67] fix(a11y): distinguish catalog and override contrast checks --- src/designTokens.ts | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/src/designTokens.ts b/src/designTokens.ts index c1663323..e148458f 100644 --- a/src/designTokens.ts +++ b/src/designTokens.ts @@ -254,11 +254,12 @@ function colorValueForScheme( } /** - * Return the WCAG 2.2 contrast ratio for two shipped color tokens. + * Return the WCAG 2.2 contrast ratio for two shipped catalog color tokens. * - * Use this after a host override to decide whether an inventoried pair still - * meets 4.5:1 text contrast, including `--cwl-accent` on `--cwl-accent-soft`. - * The ratio is not a host WCAG certification. + * This helper evaluates Inkspan's catalog values for the requested scheme; it + * cannot observe host CSS overrides. After overriding a pair on `.cwl-editor`, + * pass the resolved `#rrggbb` values to `contrastRatioFromHex()` instead. The + * ratio is not a host WCAG certification. * * @throws {EditorThemeTokenError} When either name is not a shipped token. * @throws {EditorThemeTokenContrastError} When either token is not a color. @@ -280,6 +281,8 @@ export function getEditorThemeTokenContrast( const meetsTextContrast = ratio >= WCAG_TEXT_CONTRAST_RATIO; const meetsNonTextContrast = ratio >= WCAG_NON_TEXT_CONTRAST_RATIO; const pairNames = `--${foreground.name} and --${background.name}`; + const resolvedOverrideAction = + 'For host overrides, pass the resolved #rrggbb pair to contrastRatioFromHex(actualForegroundHex, actualBackgroundHex) before shipping.'; return Object.freeze({ foreground: foreground.name, background: background.name, @@ -288,8 +291,8 @@ export function getEditorThemeTokenContrast( meetsTextContrast, meetsNonTextContrast, hostAction: meetsTextContrast - ? `Override ${pairNames} on .cwl-editor after checking WCAG 2.2 contrast. Do not edit Inkspan internals.` - : `Override ${pairNames} on .cwl-editor; shipped ${scheme} text contrast is below 4.5:1. Do not edit Inkspan internals.`, + ? `Catalog ${scheme} contrast is ${ratio.toFixed(2)}:1. ${resolvedOverrideAction} Override ${pairNames} only on .cwl-editor; do not edit Inkspan internals.` + : `Catalog ${scheme} text contrast is below 4.5:1. Override ${pairNames} on .cwl-editor and ${resolvedOverrideAction} Do not edit Inkspan internals.`, }); } From 4cce549496cab714b1727553fdff4d4820b35655 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 18 Aug 2026 07:40:40 -0700 Subject: [PATCH 40/67] test(docs): require truthful override contrast guidance --- src/designTokenDocumentation.test.ts | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/src/designTokenDocumentation.test.ts b/src/designTokenDocumentation.test.ts index 3d3af081..a5d8f09b 100644 --- a/src/designTokenDocumentation.test.ts +++ b/src/designTokenDocumentation.test.ts @@ -12,6 +12,7 @@ const normalize = (value: string): string => value.replace(/\s+/gu, ' ').trim(); describe('editor design-token documentation contract', () => { it('tells hosts how to re-theme repeating chrome without editing internals', () => { + const readme = normalize(repositoryFile('README.md')); const operatorGuide = normalize(repositoryFile('docs/design-tokens.md')); const inventory = normalize(repositoryFile('docs/storybook-inventory.md')); const doctoring = repositoryFile('docs/doctoring/editor-design-tokens.md'); @@ -37,9 +38,15 @@ describe('editor design-token documentation contract', () => { expect(operatorGuide).not.toContain('shipped dark active-toolbar pair now uses'); expect(operatorGuide).toContain('getEditorThemeTokenContrast'); expect(operatorGuide).toContain("getEditorThemeTokenContrast('cwl-accent', 'cwl-accent-soft', 'dark')"); + expect(operatorGuide).toContain('getEditorThemeTokenContrast() checks only Inkspan catalog values'); + expect(operatorGuide).toContain( + 'contrastRatioFromHex(actualForegroundHex, actualBackgroundHex)', + ); expect(operatorGuide).toContain('meetsTextContrast'); expect(operatorGuide).toContain('contrastRatioFromHex'); expect(operatorGuide).toContain('Do not edit Inkspan internals'); + expect(readme).toContain('catalog baseline'); + expect(readme).toContain('contrastRatioFromHex'); expect(inventory).toContain('Editor Chrome / Toolbar Button States'); expect(inventory).toContain('Editor Chrome / Live Toolbar'); expect(inventory).toContain('shipped Toolbar'); @@ -55,6 +62,10 @@ describe('editor design-token documentation contract', () => { expect(doctoring).not.toContain('host-facing catalog of the nine shipped chrome tokens'); expect(doctoring).toContain('producing about 5.06:1'); expect(doctoring).toContain('Host overrides must still re-check their own resulting pairs'); + expect(doctoring).toContain('contrastRatioFromHex'); + expect(doctoring).not.toContain( + "Hosts must still call `getEditorThemeTokenContrast('cwl-accent', 'cwl-accent-soft', 'dark')` after overriding either token", + ); expect(doctoring).toContain('Design Tokens Format Module 2025.10'); expect(doctoring).toContain( 'https://www.w3.org/community/reports/design-tokens/CG-FINAL-format-20251028/', @@ -80,4 +91,4 @@ describe('editor design-token documentation contract', () => { ); expect(inventory).toContain('class-level chrome sample'); }); -}); \ No newline at end of file +}); From 0ddf23be3831768b59e7921a9af06a79fa64a70c Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 18 Aug 2026 07:43:29 -0700 Subject: [PATCH 41/67] docs(a11y): distinguish catalog and resolved override contrast --- docs/design-tokens.md | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/docs/design-tokens.md b/docs/design-tokens.md index 42255293..b1056994 100644 --- a/docs/design-tokens.md +++ b/docs/design-tokens.md @@ -4,6 +4,8 @@ Status: Active PR / Proposed Use this catalog when you need to re-theme Inkspan's repeating toolbar and editor chrome. Protected-main CSS defaults remain the shipped presentation baseline; the Active-PR repaired dark active-toolbar pair uses `--cwl-accent: #58a6ff` on `--cwl-accent-soft: #163356` and measures about 5.06:1. When a host overrides any color token, re-check WCAG 2.2 contrast for both body text (`--cwl-fg` on `--cwl-bg`) and active toolbar text (`--cwl-accent` on `--cwl-accent-soft`). Do not edit Inkspan internals. +`getEditorThemeTokenContrast()` checks only Inkspan catalog values for the requested light/dark/print scheme; it does not read resolved host CSS. After an override, obtain the actual resolved hex colors from the host theme and call `contrastRatioFromHex(actualForegroundHex, actualBackgroundHex)` before shipping that theme. + ```css .cwl-editor { --cwl-accent: #0b6e4f; @@ -23,7 +25,9 @@ const tokens = listEditorThemeTokens(); const dtcgGroup = toDesignTokenFormatGroup(); const body = getEditorThemeTokenContrast('cwl-fg', 'cwl-bg', 'light'); const activeDark = getEditorThemeTokenContrast('cwl-accent', 'cwl-accent-soft', 'dark'); -const overrideRatio = contrastRatioFromHex('#0b6e4f', '#d8f3e8'); +const actualForegroundHex = '#0b6e4f'; +const actualBackgroundHex = '#d8f3e8'; +const overrideRatio = contrastRatioFromHex(actualForegroundHex, actualBackgroundHex); if (!body.meetsTextContrast || !activeDark.meetsTextContrast || overrideRatio < 4.5) { throw new Error(activeDark.hostAction); } From c25607e19729ff50820011102bdff95e0392ef5e Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 18 Aug 2026 07:46:35 -0700 Subject: [PATCH 42/67] fix(a11y): preserve actionable override instruction contract --- src/designTokens.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/designTokens.ts b/src/designTokens.ts index e148458f..5231ee35 100644 --- a/src/designTokens.ts +++ b/src/designTokens.ts @@ -291,7 +291,7 @@ export function getEditorThemeTokenContrast( meetsTextContrast, meetsNonTextContrast, hostAction: meetsTextContrast - ? `Catalog ${scheme} contrast is ${ratio.toFixed(2)}:1. ${resolvedOverrideAction} Override ${pairNames} only on .cwl-editor; do not edit Inkspan internals.` + ? `Catalog ${scheme} contrast is ${ratio.toFixed(2)}:1. ${resolvedOverrideAction} Override ${pairNames} on .cwl-editor only; do not edit Inkspan internals.` : `Catalog ${scheme} text contrast is below 4.5:1. Override ${pairNames} on .cwl-editor and ${resolvedOverrideAction} Do not edit Inkspan internals.`, }); } From c7ea3387498343ddd2f1378065d7c7c31bf2b9aa Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 18 Aug 2026 07:48:07 -0700 Subject: [PATCH 43/67] docs(a11y): bind custom themes to resolved contrast values --- docs/doctoring/editor-design-tokens.md | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/docs/doctoring/editor-design-tokens.md b/docs/doctoring/editor-design-tokens.md index bf7720b7..34ff217b 100644 --- a/docs/doctoring/editor-design-tokens.md +++ b/docs/doctoring/editor-design-tokens.md @@ -11,14 +11,14 @@ Hosts embed Inkspan and need to match brand color, radius, and font without fork The same inventory exposed an Inkspan-owned protected-main default-theme defect rather than a host-only customization problem: dark `.cwl-tb-btn.is-active` renders `--cwl-accent` text on `--cwl-accent-soft` at about 4.13:1, below the WCAG 2.2 4.5:1 threshold for normal text. This Active PR now carries dark `--cwl-accent: #58a6ff` against unchanged `--cwl-accent-soft: #163356`, producing about 5.06:1 for the active toolbar pair. That repaired value is active-PR evidence, not shipped protected-main truth, until integration. Host overrides must still re-check their own resulting pairs. -If contrast fails after a re-theme, override only the named tokens on `.cwl-editor` and re-check WCAG 2.2 text contrast for `--cwl-fg` on `--cwl-bg` and `--cwl-accent` on `--cwl-accent-soft`. Do not disable forced-colors overrides. +If contrast fails after a re-theme, override only the named tokens on `.cwl-editor` and re-check WCAG 2.2 text contrast for `--cwl-fg` on `--cwl-bg` and `--cwl-accent` on `--cwl-accent-soft`. `getEditorThemeTokenContrast()` reports Inkspan's catalog baseline only; after a host override, pass the actual resolved pair to `contrastRatioFromHex(actualForegroundHex, actualBackgroundHex)` before shipping. Do not disable forced-colors overrides. ## Decision If integrated: 1. Keep `src/styles.css` as runtime presentation authority. -2. Publish `listEditorThemeTokens()` / `getEditorThemeToken()` / `getEditorThemeTokenContrast()` / `toDesignTokenFormatGroup()` as a host-facing catalog of the nine inventoried chrome tokens. +2. Publish `listEditorThemeTokens()` / `getEditorThemeToken()` / `getEditorThemeTokenContrast()` / `contrastRatioFromHex()` / `toDesignTokenFormatGroup()` as host-facing theme evidence: the name-based contrast helper evaluates the catalog baseline, while the hex helper evaluates actual resolved host colors. 3. Keep the catalog light/dark/print values synchronized with the stylesheet and require the inventoried active-toolbar text pair to meet the WCAG 2.2 4.5:1 threshold in Inkspan's own default themes. 4. Reject unknown token names with a stable payload-redacted `EditorThemeTokenError`. 5. Preview repeating `.cwl-tb-btn` states, the shipped Toolbar component, and token swatches in Storybook. @@ -35,11 +35,13 @@ WCAG 2.2 requires at least 4.5:1 contrast for normal text under Success Criterio - Original RED: `src/designTokens.test.ts` failed because `./designTokens.js` did not exist. - Initial GREEN: the active-PR catalog lists nine inventoried tokens, aligns light/dark/print color values with the matching `src/styles.css` media blocks, reports WCAG 2.2 contrast for inventoried color pairs, rejects unknown names without reflecting caller input, and emits a DTCG 2025.10 group. - Accessibility RED: exact test-only head `a831359d1509811ab8777e7356f6ebd5f251b5cf` changed the active-chrome contract to require the dark `--cwl-accent` / `--cwl-accent-soft` pair to meet 4.5:1. The protected-main values remained `#4493f8` on `#163356` (about 4.13:1), so the new expectation could not pass without a real default-theme change. -- Accessibility GREEN: the active-PR dark `--cwl-accent` is `#58a6ff` in both the typed catalog and runtime stylesheet; against unchanged `#163356` it measures about 5.06:1 and `meetsTextContrast` is true. The host override helper remains available for custom themes. +- Accessibility GREEN: the active-PR dark `--cwl-accent` is `#58a6ff` in both the typed catalog and runtime stylesheet; against unchanged `#163356` it measures about 5.06:1 and `meetsTextContrast` is true. +- Override-truth RED: exact-head CI `32149806678` / build-test `95752733876` proved the documentation and API action text still conflated catalog-token contrast with resolved host override contrast. +- Override-truth repair: `getEditorThemeTokenContrast()` is explicitly catalog-only and custom host themes are checked with `contrastRatioFromHex()` using the actual resolved colors. ## Residual risk -Print media still remaps the color tokens after a host override. Forced-colors mode only restyles the toolbar focus outline to `CanvasText`; it does not assign `--cwl-*` values. The active-PR repaired dark `.cwl-tb-btn.is-active` pair (`--cwl-accent` on `--cwl-accent-soft`) meets the WCAG 2.2 4.5:1 normal-text threshold and also exceeds the 3:1 non-text threshold; protected main remains at the failing pre-repair pair until integration. Hosts must still call `getEditorThemeTokenContrast('cwl-accent', 'cwl-accent-soft', 'dark')` after overriding either token because custom values can reintroduce a contrast failure. The font token snapshot splits a CSS font-family list and does not execute CSS. Storybook success is not Chromium/Firefox/WebKit release evidence. +Print media still remaps the color tokens after a host override. Forced-colors mode only restyles the toolbar focus outline to `CanvasText`; it does not assign `--cwl-*` values. The active-PR repaired dark `.cwl-tb-btn.is-active` pair (`--cwl-accent` on `--cwl-accent-soft`) meets the WCAG 2.2 4.5:1 normal-text threshold and also exceeds the 3:1 non-text threshold; protected main remains at the failing pre-repair pair until integration. `getEditorThemeTokenContrast()` reports only that catalog evidence; after overriding either token, hosts must call `contrastRatioFromHex(actualForegroundHex, actualBackgroundHex)` with the actual resolved values because custom values can reintroduce a contrast failure. The font token snapshot splits a CSS font-family list and does not execute CSS. Storybook success is not Chromium/Firefox/WebKit release evidence. ## Rollback From 9f1f6e52716f975a66277112450676b9a618a472 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 18 Aug 2026 07:48:56 -0700 Subject: [PATCH 44/67] docs(adr): separate catalog and custom-theme contrast authority --- docs/adr/0031-editor-design-tokens-storybook.md | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/docs/adr/0031-editor-design-tokens-storybook.md b/docs/adr/0031-editor-design-tokens-storybook.md index fe30bec6..2e3cfdb0 100644 --- a/docs/adr/0031-editor-design-tokens-storybook.md +++ b/docs/adr/0031-editor-design-tokens-storybook.md @@ -18,15 +18,15 @@ Inventorying the actual active-toolbar foreground/background pair also exposed a ## Decision -If integrated, Inkspan will publish a host-facing theme-token catalog for nine inventoried chrome tokens, a Design Tokens Format Module 2025.10 interchange snapshot, and a Storybook inventory of repeating toolbar/editor objects. Inkspan's inventoried normal-text pairs will be required to meet the WCAG 2.2 4.5:1 threshold in the resulting protected light/dark/print defaults; the active-PR candidate therefore uses `#58a6ff` on `#163356` for dark active-toolbar text, about 5.06:1. Hosts overriding `--cwl-*` on `.cwl-editor` must re-check contrast for their resulting body and active-toolbar pairs. Unknown token names fail closed. No Figma, network, persistence, credential, or model authority is added. +If integrated, Inkspan will publish a host-facing theme-token catalog for nine inventoried chrome tokens, a Design Tokens Format Module 2025.10 interchange snapshot, and a Storybook inventory of repeating toolbar/editor objects. Inkspan's inventoried normal-text pairs will be required to meet the WCAG 2.2 4.5:1 threshold in the resulting protected light/dark/print defaults; the active-PR candidate therefore uses `#58a6ff` on `#163356` for dark active-toolbar text, about 5.06:1. `getEditorThemeTokenContrast()` evaluates only the catalog values for a named scheme. Hosts overriding `--cwl-*` on `.cwl-editor` must pass their actual resolved foreground/background hex values to `contrastRatioFromHex()` and re-check their resulting body and active-toolbar pairs. Unknown token names fail closed. No Figma, network, persistence, credential, or model authority is added. ## Consequences and ownership trade-offs -After integration, hosts gain a copyable token list and a Storybook preview. Inkspan keeps CSS as the runtime source and owns accessibility defects in its default token combinations. naruon and other CWL hosts can apply their own overrides through host-owned brand CSS and then use the same contrast helpers to validate those custom values. Complete DTCG conformance, Figma Variables, host-theme WCAG certification, and automated remediation of arbitrary host palettes remain out of scope. +After integration, hosts gain a copyable token list and a Storybook preview. Inkspan keeps CSS as the runtime source and owns accessibility defects in its default token combinations. naruon and other CWL hosts can apply their own overrides through host-owned brand CSS, use `getEditorThemeTokenContrast()` to inspect the Inkspan catalog baseline, and use `contrastRatioFromHex()` to validate their actual resolved custom values. Complete DTCG conformance, Figma Variables, host-theme WCAG certification, and automated remediation of arbitrary host palettes remain out of scope. ## Failure and recovery -An unknown token name throws `EditorThemeTokenError` without reflecting caller input. A protected default pair that fails an applicable repository contrast contract must be repaired in Inkspan's token catalog and stylesheet together before integration or release. A host override that fails contrast is recovered by changing only the named host tokens, not by editing Inkspan internals or disabling forced-colors. +An unknown token name throws `EditorThemeTokenError` without reflecting caller input. A protected default pair that fails an applicable repository contrast contract must be repaired in Inkspan's token catalog and stylesheet together before integration or release. A host override that fails contrast is recovered by changing only the named host tokens, measuring the actual resolved custom pair with `contrastRatioFromHex()`, and rechecking the applicable WCAG threshold; do not edit Inkspan internals or disable forced-colors. ## Security and privacy impact @@ -34,11 +34,11 @@ The proposed catalog contains only public presentation values. It does not carry ## Compatibility and migration -The catalog is additive if integrated. Existing CSS overrides on `.cwl-editor` continue to work. The active-PR candidate changes the dark default accent from `#4493f8` to `#58a6ff`; hosts that already override `--cwl-accent` are unaffected by that candidate default-value change but remain responsible for validating their custom pair. A later CSS token addition or default-value change must update the catalog, directly affected documentation/tests, and this ADR together. +The catalog is additive if integrated. Existing CSS overrides on `.cwl-editor` continue to work. The active-PR candidate changes the dark default accent from `#4493f8` to `#58a6ff`; hosts that already override `--cwl-accent` are unaffected by that candidate default-value change but remain responsible for validating their actual custom pair. A later CSS token addition or default-value change must update the catalog, directly affected documentation/tests, and this ADR together. ## Verification and acceptance evidence -Required evidence includes token-catalog tests against `src/styles.css`, deterministic contrast assertions for inventoried normal-text pairs, documentation-contract tests, Storybook inventory stories for toolbar button states and token swatches, and exact-head CI/coverage/package/security gates on the unchanged head. The accessibility regression must fail against protected main's `#4493f8`/`#163356` dark active pair and pass against the active-PR `#58a6ff`/`#163356` pair. This ADR stays Proposed until protected integration. +Required evidence includes token-catalog tests against `src/styles.css`, deterministic contrast assertions for inventoried normal-text pairs, resolved-hex override guidance tests, documentation-contract tests, Storybook inventory stories for toolbar button states and token swatches, and exact-head CI/coverage/package/security gates on the unchanged head. The accessibility regression must fail against protected main's `#4493f8`/`#163356` dark active pair and pass against the active-PR `#58a6ff`/`#163356` pair. This ADR stays Proposed until protected integration. ## Rollback or supersession From ce078cf93ae45c09d44af872ac313d4a5d26d997 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 18 Aug 2026 07:53:04 -0700 Subject: [PATCH 45/67] docs(a11y): make host theme contrast verification truthful --- README.md | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 110312d4..dfc5713c 100644 --- a/README.md +++ b/README.md @@ -122,9 +122,11 @@ embed accepted images as inline data URIs. ### Theme the repeating chrome Override the named `--cwl-*` custom properties on `.cwl-editor` after checking -WCAG 2.2 contrast for body text and active toolbar text -(`getEditorThemeTokenContrast('cwl-accent', 'cwl-accent-soft', 'dark')`). Do not -edit Inkspan internals. See +WCAG 2.2 contrast for body text and active toolbar text. Use +`getEditorThemeTokenContrast('cwl-accent', 'cwl-accent-soft', 'dark')` to inspect +Inkspan's catalog baseline. After overriding CSS, pass the actual resolved +foreground/background hex values to `contrastRatioFromHex()` before shipping the +host theme. Do not edit Inkspan internals. See [`docs/design-tokens.md`](docs/design-tokens.md) for the token catalog and [`docs/storybook-inventory.md`](docs/storybook-inventory.md) for the Storybook preview of toolbar and editor chrome. From 92d8f5ac8176f20bb3a8fd0700c37b21f7e2a3b2 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 18 Aug 2026 08:08:18 -0700 Subject: [PATCH 46/67] fix(a11y): align contrast guidance with exact contracts --- src/designTokenDocumentation.test.ts | 2 +- src/designTokens.test.ts | 5 +++-- src/designTokens.ts | 2 +- 3 files changed, 5 insertions(+), 4 deletions(-) diff --git a/src/designTokenDocumentation.test.ts b/src/designTokenDocumentation.test.ts index a5d8f09b..7337452b 100644 --- a/src/designTokenDocumentation.test.ts +++ b/src/designTokenDocumentation.test.ts @@ -38,7 +38,7 @@ describe('editor design-token documentation contract', () => { expect(operatorGuide).not.toContain('shipped dark active-toolbar pair now uses'); expect(operatorGuide).toContain('getEditorThemeTokenContrast'); expect(operatorGuide).toContain("getEditorThemeTokenContrast('cwl-accent', 'cwl-accent-soft', 'dark')"); - expect(operatorGuide).toContain('getEditorThemeTokenContrast() checks only Inkspan catalog values'); + expect(operatorGuide).toContain('`getEditorThemeTokenContrast()` checks only Inkspan catalog values'); expect(operatorGuide).toContain( 'contrastRatioFromHex(actualForegroundHex, actualBackgroundHex)', ); diff --git a/src/designTokens.test.ts b/src/designTokens.test.ts index 278a2533..3c856b47 100644 --- a/src/designTokens.test.ts +++ b/src/designTokens.test.ts @@ -85,7 +85,7 @@ describe('editor theme token catalog', () => { expect(() => getEditorThemeToken('not-a-theme-token')).not.toThrow(/not-a-theme-token/u); }); - it('reports WCAG 2.2 contrast for shipped color pairs so hosts can check overrides', () => { + it('reports WCAG 2.2 contrast for catalog color pairs so hosts can check overrides', () => { const light = getEditorThemeTokenContrast('cwl-fg', 'cwl-bg', 'light'); const dark = getEditorThemeTokenContrast('cwl-fg', 'cwl-bg', 'dark'); const print = getEditorThemeTokenContrast('cwl-fg', 'cwl-bg', 'print'); @@ -102,7 +102,8 @@ describe('editor theme token catalog', () => { expect(light.hostAction).toContain('Override --cwl-fg and --cwl-bg on .cwl-editor'); expect(light.hostAction).toContain('WCAG 2.2'); expect(lowContrast.meetsTextContrast).toBe(false); - expect(lowContrast.hostAction).toContain('shipped light text contrast is below 4.5:1'); + expect(lowContrast.hostAction).toContain('Catalog light text contrast is below 4.5:1'); + expect(lowContrast.hostAction).toContain('WCAG 2.2'); expect(lowContrast.hostAction).toContain('Override --cwl-border and --cwl-bg on .cwl-editor'); }); diff --git a/src/designTokens.ts b/src/designTokens.ts index 5231ee35..7a0ca594 100644 --- a/src/designTokens.ts +++ b/src/designTokens.ts @@ -282,7 +282,7 @@ export function getEditorThemeTokenContrast( const meetsNonTextContrast = ratio >= WCAG_NON_TEXT_CONTRAST_RATIO; const pairNames = `--${foreground.name} and --${background.name}`; const resolvedOverrideAction = - 'For host overrides, pass the resolved #rrggbb pair to contrastRatioFromHex(actualForegroundHex, actualBackgroundHex) before shipping.'; + 'For host overrides, re-check WCAG 2.2 using the resolved #rrggbb pair with contrastRatioFromHex(actualForegroundHex, actualBackgroundHex) before shipping.'; return Object.freeze({ foreground: foreground.name, background: background.name, From 93bb31cfd9721eb8987796c21b9b1825e2eff63c Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 18 Aug 2026 08:13:15 -0700 Subject: [PATCH 47/67] docs(a11y): make token inventory authority explicit --- docs/doctoring/editor-design-tokens.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/doctoring/editor-design-tokens.md b/docs/doctoring/editor-design-tokens.md index 34ff217b..17846810 100644 --- a/docs/doctoring/editor-design-tokens.md +++ b/docs/doctoring/editor-design-tokens.md @@ -18,7 +18,7 @@ If contrast fails after a re-theme, override only the named tokens on `.cwl-edit If integrated: 1. Keep `src/styles.css` as runtime presentation authority. -2. Publish `listEditorThemeTokens()` / `getEditorThemeToken()` / `getEditorThemeTokenContrast()` / `contrastRatioFromHex()` / `toDesignTokenFormatGroup()` as host-facing theme evidence: the name-based contrast helper evaluates the catalog baseline, while the hex helper evaluates actual resolved host colors. +2. Publish `listEditorThemeTokens()` / `getEditorThemeToken()` / `getEditorThemeTokenContrast()` / `contrastRatioFromHex()` / `toDesignTokenFormatGroup()` as a host-facing catalog of the nine inventoried chrome tokens and theme evidence: the name-based contrast helper evaluates the catalog baseline, while the hex helper evaluates actual resolved host colors. 3. Keep the catalog light/dark/print values synchronized with the stylesheet and require the inventoried active-toolbar text pair to meet the WCAG 2.2 4.5:1 threshold in Inkspan's own default themes. 4. Reject unknown token names with a stable payload-redacted `EditorThemeTokenError`. 5. Preview repeating `.cwl-tb-btn` states, the shipped Toolbar component, and token swatches in Storybook. From db527249309e1bba819e5e2f347f49ae1605035f Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 18 Aug 2026 10:13:43 -0700 Subject: [PATCH 48/67] test(release): cover release-security stale inventory phrases --- src/releaseContractCanonicalConsistency.test.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/releaseContractCanonicalConsistency.test.ts b/src/releaseContractCanonicalConsistency.test.ts index 749a7045..e6ebb384 100644 --- a/src/releaseContractCanonicalConsistency.test.ts +++ b/src/releaseContractCanonicalConsistency.test.ts @@ -50,6 +50,7 @@ describe('canonical release artifact inventory consistency', () => { for (const document of [ changelog, contracts, + releaseSecurity, testStrategy, operability, releaseDoctoring, From 422b0bed1c94861c50229fdc160c4bc545292113 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 18 Aug 2026 10:16:22 -0700 Subject: [PATCH 49/67] docs(a11y): clarify catalog versus resolved override contrast --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0df47b64..e0c53db1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,7 +10,7 @@ Historical release entries from **0.1.0 through 0.5.27** are preserved verbatim - Named the repeating editor chrome as a host-facing theme-token catalog and Storybook inventory so hosts can override `--cwl-*` custom properties on `.cwl-editor` after checking WCAG 2.2 contrast, without editing Inkspan internals. Color catalog values now distinguish light, dark, and `@media print` remaps; forced-colors mode is not treated as a token assignment. Hosts can call `getEditorThemeTokenContrast()` to compare inventoried pairs, including `--cwl-accent` on `--cwl-accent-soft`, against the 4.5:1 text threshold via `meetsTextContrast`. ### Accessibility -- Prepared the active-PR dark active-toolbar accent change from protected-main `#4493f8` to `#58a6ff`, increasing `--cwl-accent` text on `--cwl-accent-soft: #163356` from about 4.13:1 to about 5.06:1 so the candidate default 13px active-button text meets the WCAG 2.2 4.5:1 normal-text threshold; this remains active-PR evidence until protected integration, and host overrides remain explicitly re-checkable through `getEditorThemeTokenContrast()`. +- Prepared the active-PR dark active-toolbar accent change from protected-main `#4493f8` to `#58a6ff`, increasing `--cwl-accent` text on `--cwl-accent-soft: #163356` from about 4.13:1 to about 5.06:1 so the candidate default 13px active-button text meets the WCAG 2.2 4.5:1 normal-text threshold; this remains active-PR evidence until protected integration. `getEditorThemeTokenContrast()` checks catalog values, and host overrides must be re-checked with `contrastRatioFromHex(actualForegroundHex, actualBackgroundHex)` using the actual resolved colors. ## [0.6.0] — 2026-08-10 From f20cadb3922b9f0390aa43e13abb0a3055e23bf9 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 19 Aug 2026 08:29:56 -0700 Subject: [PATCH 50/67] docs: reconcile protected DOCX hyperlink maturity --- docs/TRD.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/TRD.md b/docs/TRD.md index 1b6ffae9..22ecdaa7 100644 --- a/docs/TRD.md +++ b/docs/TRD.md @@ -85,9 +85,9 @@ The Office JSON→DOCX/XLSX/PPTX renderer is **network-free**, **macro-free**, m Protected DOCX fidelity includes informative bounded inline PNG figures under ADR 0022, bounded `rich_paragraph` bold/italic/underline runs under ADR 0023, exact optional `left`/`center`/`right`/`justify` alignment for `paragraph` and `rich_paragraph` under ADR 0024, and the same bounded alignment for `heading` under ADR 0025. Omitted alignment preserves inherited/default Word semantics. -Active PR #137 implements the Proposed ADR 0026 extension for one optional relationship-backed hyperlink on a rich-text run. That active contract deliberately permits only printable-ASCII absolute HTTP(S) targets up to 4,096 characters, rejects local/executable/data/mail/telephone/relative/protocol-relative/credential-bearing/backslash/whitespace-control/non-ASCII targets, preserves the exact accepted target and existing run emphasis, and remains network-free. Until protected integration, this is `implemented_on_active_pr`, not shipped authority. +Protected `main` implements Accepted ADR 0026 through integrated PR #137: one optional relationship-backed hyperlink on a rich-text run. The protected contract deliberately permits only printable-ASCII absolute HTTP(S) targets up to 4,096 characters, rejects local/executable/data/mail/telephone/relative/protocol-relative/credential-bearing/backslash/whitespace-control/non-ASCII targets, preserves the exact accepted target and existing run emphasis, and remains network-free. This is `implemented_on_protected_main`. -The protected additive fidelity contracts and active hyperlink proposal do not grant arbitrary style, font, color, spacing, indentation, arbitrary relationship IDs/types, internal/bookmark hyperlinks, field codes, raw OOXML, remote-resource fetching, decorative-image semantics, destination trust validation, or source-format parsing authority. +The protected additive fidelity and hyperlink contracts do not grant arbitrary style, font, color, spacing, indentation, arbitrary relationship IDs/types, internal/bookmark hyperlinks, field codes, raw OOXML, remote-resource fetching, decorative-image semantics, destination trust validation, or source-format parsing authority. Format-fidelity claims are limited to tested supported constructs. The renderer does not execute macros, scripts, formula calculation, embedded external resources, or Desktop Office automation. A stored external hyperlink relationship is document metadata, not permission to dereference or trust its destination. @@ -141,6 +141,6 @@ Queued, cancelled, skipped-required, absent, stale-head, predecessor-head, statu ## Implemented versus proposed -Protected `main` is the sole shipped implementation baseline. SafeClipboard, cross-engine browser assurance, the security disclosure lifecycle, autosave lifecycle observation, toolbar shortcut accessibility metadata, accessible editor placeholder semantics, SSR/native-form serialization, revision-scoped selection evidence, W3C text-position selector evidence, the React-free text-position-selector subpath, document-transition evidence, envelope identity routing, framework-neutral deterministic Markdown conversion, CSS paged-media print output, DOCX informative PNG figures, bounded rich-text runs, bounded paragraph alignment, bounded heading alignment, and the OIDC-backed unified stable registry release train are `implemented_on_protected_main`. +Protected `main` is the sole shipped implementation baseline. SafeClipboard, cross-engine browser assurance, the security disclosure lifecycle, autosave lifecycle observation, toolbar shortcut accessibility metadata, accessible editor placeholder semantics, SSR/native-form serialization, revision-scoped selection evidence, W3C text-position selector evidence, the React-free text-position-selector subpath, document-transition evidence, envelope identity routing, framework-neutral deterministic Markdown conversion, CSS paged-media print output, DOCX informative PNG figures, bounded rich-text runs, bounded paragraph alignment, bounded heading alignment, bounded DOCX rich-run external hyperlinks, and the OIDC-backed unified stable registry release train are `implemented_on_protected_main`. -The bounded DOCX rich-run external hyperlink contract in #137 is `implemented_on_active_pr` under Proposed ADR 0026. Open branches may extend the protected boundary, but no active-PR capability becomes shipped merely because its design, tests, or documentation are complete. +Open branches may extend the protected boundary, but no active-PR capability becomes shipped merely because its design, tests, or documentation are complete. From ac23bc280a77f60c57874f4a8dacc3695b113c49 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 19 Aug 2026 08:31:25 -0700 Subject: [PATCH 51/67] test: lock protected DOCX hyperlink documentation maturity --- ...docxHyperlinkDocumentationMaturity.test.ts | 31 +++++++++++++++++++ 1 file changed, 31 insertions(+) create mode 100644 src/docxHyperlinkDocumentationMaturity.test.ts diff --git a/src/docxHyperlinkDocumentationMaturity.test.ts b/src/docxHyperlinkDocumentationMaturity.test.ts new file mode 100644 index 00000000..ae73b15e --- /dev/null +++ b/src/docxHyperlinkDocumentationMaturity.test.ts @@ -0,0 +1,31 @@ +import { readFileSync } from 'node:fs'; +import { resolve } from 'node:path'; + +import { describe, expect, it } from 'vitest'; + +const repositoryFile = (path: string): string => + readFileSync(resolve(process.cwd(), path), 'utf8'); + +describe('protected DOCX hyperlink documentation maturity', () => { + it('keeps the technical requirements aligned with accepted protected ADR 0026', () => { + const trd = repositoryFile('docs/TRD.md'); + const adr = repositoryFile( + 'docs/adr/0026-bounded-docx-external-hyperlinks.md', + ); + + expect(adr).toContain('Status: Accepted'); + expect(adr).toContain('implemented on protected `main` through PR #137'); + expect(trd).toContain( + 'Protected `main` implements Accepted ADR 0026 through integrated PR #137', + ); + expect(trd).toContain( + 'bounded DOCX rich-run external hyperlinks, and the OIDC-backed unified stable registry release train are `implemented_on_protected_main`', + ); + expect(trd).not.toContain( + 'Active PR #137 implements the Proposed ADR 0026 extension', + ); + expect(trd).not.toContain( + 'The bounded DOCX rich-run external hyperlink contract in #137 is `implemented_on_active_pr` under Proposed ADR 0026', + ); + }); +}); From 4d33e4cc5265632d01a76038d8f318712d6e164e Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 19 Aug 2026 16:21:28 -0700 Subject: [PATCH 52/67] test(design-tokens): reject hostile contrast color coercion --- src/designTokensRuntimeInput.test.ts | 30 ++++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) create mode 100644 src/designTokensRuntimeInput.test.ts diff --git a/src/designTokensRuntimeInput.test.ts b/src/designTokensRuntimeInput.test.ts new file mode 100644 index 00000000..90abe635 --- /dev/null +++ b/src/designTokensRuntimeInput.test.ts @@ -0,0 +1,30 @@ +import { describe, expect, it } from 'vitest'; + +import { + EditorThemeTokenContrastError, + contrastRatioFromHex, +} from './designTokens.js'; + +describe('theme contrast runtime input boundary', () => { + it('rejects non-string colors before caller-controlled coercion', () => { + const privateSentinel = new Error('private theme color coercion sentinel'); + let coercionCalls = 0; + const hostileColor = { + toString(): never { + coercionCalls += 1; + throw privateSentinel; + }, + }; + + let observed: unknown; + try { + contrastRatioFromHex(hostileColor as unknown as string, '#ffffff'); + } catch (error) { + observed = error; + } + + expect(coercionCalls).toBe(0); + expect(observed).toBeInstanceOf(EditorThemeTokenContrastError); + expect(observed).not.toBe(privateSentinel); + }); +}); From d56be5a6c9fae490ef26030c917ebef2c6e66088 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 19 Aug 2026 16:23:03 -0700 Subject: [PATCH 53/67] fix(design-tokens): reject hostile contrast inputs --- src/designTokens.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/designTokens.ts b/src/designTokens.ts index 7a0ca594..da0f447d 100644 --- a/src/designTokens.ts +++ b/src/designTokens.ts @@ -206,6 +206,9 @@ function srgbChannel(value: number): number { } function relativeLuminance(hex: string): number { + if (typeof hex !== 'string') { + throw new EditorThemeTokenContrastError(); + } const match = /^#([0-9a-f]{6})$/iu.exec(hex); if (!match) { throw new EditorThemeTokenContrastError(); From b20d5341a93019f36935ee752cff7bae4603da9c Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 19 Aug 2026 16:57:08 -0700 Subject: [PATCH 54/67] test(design-tokens): require immutable DTCG font values --- src/designTokenFormatImmutability.test.ts | 14 ++++++++++++++ 1 file changed, 14 insertions(+) create mode 100644 src/designTokenFormatImmutability.test.ts diff --git a/src/designTokenFormatImmutability.test.ts b/src/designTokenFormatImmutability.test.ts new file mode 100644 index 00000000..348402dd --- /dev/null +++ b/src/designTokenFormatImmutability.test.ts @@ -0,0 +1,14 @@ +import { describe, expect, it } from 'vitest'; + +import { toDesignTokenFormatGroup } from './designTokens.js'; + +describe('design-token interchange snapshot immutability', () => { + it('freezes nested font-family values before returning them to hosts', () => { + const group = toDesignTokenFormatGroup(); + const fontValue = group.cwl.font.$value; + + expect(Array.isArray(fontValue)).toBe(true); + expect(Object.isFrozen(fontValue)).toBe(true); + expect(() => (fontValue as string[]).push('Host mutation')).toThrow(TypeError); + }); +}); From 0dbb7f4c5864c39ea2ea49e3aba82037977caa87 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 19 Aug 2026 16:59:00 -0700 Subject: [PATCH 55/67] fix(design-tokens): freeze DTCG font values --- src/designTokens.ts | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/designTokens.ts b/src/designTokens.ts index da0f447d..87ef2bf2 100644 --- a/src/designTokens.ts +++ b/src/designTokens.ts @@ -327,9 +327,11 @@ function formatTokenValue(token: EditorThemeToken): DesignTokenFormatValue { case 'dimension': return formatDimensionTokenValue(token.lightValue); case 'fontFamily': - return token.lightValue - .split(',') - .map((family) => family.trim().replace(/^['"]|['"]$/gu, '')); + return Object.freeze( + token.lightValue + .split(',') + .map((family) => family.trim().replace(/^['"]|['"]$/gu, '')), + ); } } From 08b576eef322f9dc180a3613c9f062b46af9df24 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 19 Aug 2026 22:17:36 -0700 Subject: [PATCH 56/67] test(a11y): expose missing editor focus indicator --- src/editorFocusStyles.test.ts | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) create mode 100644 src/editorFocusStyles.test.ts diff --git a/src/editorFocusStyles.test.ts b/src/editorFocusStyles.test.ts new file mode 100644 index 00000000..5adf7944 --- /dev/null +++ b/src/editorFocusStyles.test.ts @@ -0,0 +1,27 @@ +import { readFileSync } from 'node:fs'; +import { resolve } from 'node:path'; + +import { describe, expect, it } from 'vitest'; + +const styles = readFileSync(resolve(process.cwd(), 'src/styles.css'), 'utf8'); + +describe('editable surface focus visibility', () => { + it('preserves a visible keyboard focus indicator on the textbox surface', () => { + expect(styles).not.toMatch( + /\.cwl-editor__content:focus\s*\{[^}]*outline:\s*none\s*;/u, + ); + expect(styles).toMatch( + /\.cwl-editor__content:focus-visible\s*\{[^}]*outline:\s*2px\s+solid\s+var\(--cwl-accent\)\s*;[^}]*outline-offset:\s*-?2px\s*;/u, + ); + }); + + it('keeps the editable focus indicator visible in forced-colors mode', () => { + const forcedColorsIndex = styles.indexOf('@media (forced-colors: active)'); + expect(forcedColorsIndex).toBeGreaterThan(-1); + const forcedColorsStyles = styles.slice(forcedColorsIndex); + + expect(forcedColorsStyles).toMatch( + /\.cwl-editor__content:focus-visible\s*\{[^}]*outline-color:\s*CanvasText\s*;/u, + ); + }); +}); From 44807c9e2c84e307bc6a2d077138b5b33cf94615 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 19 Aug 2026 22:18:25 -0700 Subject: [PATCH 57/67] fix(a11y): restore visible editor keyboard focus --- src/styles.css | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/styles.css b/src/styles.css index 25f98adb..b9a2d8e2 100644 --- a/src/styles.css +++ b/src/styles.css @@ -107,7 +107,8 @@ } @media (forced-colors: active) { - .cwl-tb-btn:focus-visible { + .cwl-tb-btn:focus-visible, + .cwl-editor__content:focus-visible { outline-color: CanvasText; } } @@ -125,8 +126,9 @@ font-size: 15px; } -.cwl-editor__content:focus { - outline: none; +.cwl-editor__content:focus-visible { + outline: 2px solid var(--cwl-accent); + outline-offset: -2px; } .cwl-editor__content > * + * { From 0a5f18b59e08598e72a0e6f60c6fb88b787a040e Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 19 Aug 2026 22:20:00 -0700 Subject: [PATCH 58/67] test(a11y): verify packed editor focus indicator --- tests/browser/specs/focus.browser.spec.ts | 61 +++++++++++++++++++++++ 1 file changed, 61 insertions(+) create mode 100644 tests/browser/specs/focus.browser.spec.ts diff --git a/tests/browser/specs/focus.browser.spec.ts b/tests/browser/specs/focus.browser.spec.ts new file mode 100644 index 00000000..71d12e1d --- /dev/null +++ b/tests/browser/specs/focus.browser.spec.ts @@ -0,0 +1,61 @@ +import { expect, test } from '@playwright/test'; + +const HARNESS_URL = 'http://127.0.0.1:4173/tests/browser/harness.html'; +const STYLES_URL = 'http://127.0.0.1:4173/dist/cwl-editor.css'; + +const allowHarnessRequest = (requestUrl: string): boolean => { + const url = new URL(requestUrl); + return url.hostname === '127.0.0.1' && url.port === '4173'; +}; + +test('shows a packed-stylesheet focus indicator on the editable textbox', async ({ + page, +}) => { + const rejectedExternalRequests: string[] = []; + await page.route('**/*', async (route) => { + if (allowHarnessRequest(route.request().url())) { + await route.continue(); + return; + } + rejectedExternalRequests.push(new URL(route.request().url()).origin); + await route.abort('blockedbyclient'); + }); + + await page.goto(HARNESS_URL); + await page.addStyleTag({ url: STYLES_URL }); + await page.locator('#harness').evaluate((element) => { + element.innerHTML = ` +
+
+
Keyboard focus target
+
+
+ `; + }); + expect(rejectedExternalRequests).toEqual([]); + + await page.keyboard.press('Tab'); + const content = page.getByRole('textbox', { name: 'Document' }); + await expect(content).toBeFocused(); + + const focusStyle = await content.evaluate((element) => { + const style = getComputedStyle(element); + return { + outlineStyle: style.outlineStyle, + outlineWidth: style.outlineWidth, + outlineOffset: style.outlineOffset, + }; + }); + + expect(focusStyle).toEqual({ + outlineStyle: 'solid', + outlineWidth: '2px', + outlineOffset: '-2px', + }); +}); From 94b9a239f92c6cbe6748e808dc380f97e2224fab Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 19 Aug 2026 22:20:17 -0700 Subject: [PATCH 59/67] test(a11y): run focus acceptance cross-engine --- tests/browser/playwright.config.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/browser/playwright.config.ts b/tests/browser/playwright.config.ts index 375056da..d7d4c2a2 100644 --- a/tests/browser/playwright.config.ts +++ b/tests/browser/playwright.config.ts @@ -2,7 +2,7 @@ import { defineConfig, devices } from '@playwright/test'; const HARNESS_ORIGIN = 'http://127.0.0.1:4173'; const HARNESS_URL = `${HARNESS_ORIGIN}/tests/browser/harness.html`; -const ENGINE_BROWSER_SPECS = /(?:clipboard|print)\.browser\.spec\.ts/u; +const ENGINE_BROWSER_SPECS = /(?:clipboard|focus|print)\.browser\.spec\.ts/u; export default defineConfig({ testDir: './specs', @@ -44,4 +44,4 @@ export default defineConfig({ dependencies: ['chromium', 'firefox', 'webkit'], }, ], -}); \ No newline at end of file +}); From 97cce9648703f0ebf7b38dc13a964a8f2b3d1c24 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 19 Aug 2026 22:27:59 -0700 Subject: [PATCH 60/67] test(print): reject focus chrome in paged output --- tests/browser/specs/focus.browser.spec.ts | 30 ++++++++++++++++++++--- 1 file changed, 27 insertions(+), 3 deletions(-) diff --git a/tests/browser/specs/focus.browser.spec.ts b/tests/browser/specs/focus.browser.spec.ts index 71d12e1d..66ac9e96 100644 --- a/tests/browser/specs/focus.browser.spec.ts +++ b/tests/browser/specs/focus.browser.spec.ts @@ -8,9 +8,7 @@ const allowHarnessRequest = (requestUrl: string): boolean => { return url.hostname === '127.0.0.1' && url.port === '4173'; }; -test('shows a packed-stylesheet focus indicator on the editable textbox', async ({ - page, -}) => { +const mountEditableSurface = async (page: import('@playwright/test').Page) => { const rejectedExternalRequests: string[] = []; await page.route('**/*', async (route) => { if (allowHarnessRequest(route.request().url())) { @@ -43,6 +41,13 @@ test('shows a packed-stylesheet focus indicator on the editable textbox', async await page.keyboard.press('Tab'); const content = page.getByRole('textbox', { name: 'Document' }); await expect(content).toBeFocused(); + return content; +}; + +test('shows a packed-stylesheet focus indicator on the editable textbox', async ({ + page, +}) => { + const content = await mountEditableSurface(page); const focusStyle = await content.evaluate((element) => { const style = getComputedStyle(element); @@ -59,3 +64,22 @@ test('shows a packed-stylesheet focus indicator on the editable textbox', async outlineOffset: '-2px', }); }); + +test('does not print the interactive focus indicator', async ({ page }) => { + const content = await mountEditableSurface(page); + await page.emulateMedia({ media: 'print' }); + expect(await page.evaluate(() => matchMedia('print').matches)).toBe(true); + + const printFocusStyle = await content.evaluate((element) => { + const style = getComputedStyle(element); + return { + outlineStyle: style.outlineStyle, + outlineWidth: style.outlineWidth, + }; + }); + + expect(printFocusStyle).toEqual({ + outlineStyle: 'none', + outlineWidth: '0px', + }); +}); From 082c19eda773ad78f01b50e0be98a6dc38d0976c Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 19 Aug 2026 22:28:43 -0700 Subject: [PATCH 61/67] fix(print): suppress interactive focus chrome --- src/styles.css | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/styles.css b/src/styles.css index b9a2d8e2..0f650a8a 100644 --- a/src/styles.css +++ b/src/styles.css @@ -298,6 +298,10 @@ widows: 3; } + .cwl-editor__content:focus-visible { + outline: none; + } + .cwl-editor__content .is-editor-empty:first-child::before { content: none; } From e2c4dc87dd092c1c3505afabde3b2fc28940a498 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 19 Aug 2026 22:32:51 -0700 Subject: [PATCH 62/67] test(print): assert rendered outline suppression --- tests/browser/specs/focus.browser.spec.ts | 19 ++++++++----------- 1 file changed, 8 insertions(+), 11 deletions(-) diff --git a/tests/browser/specs/focus.browser.spec.ts b/tests/browser/specs/focus.browser.spec.ts index 66ac9e96..af7719d2 100644 --- a/tests/browser/specs/focus.browser.spec.ts +++ b/tests/browser/specs/focus.browser.spec.ts @@ -70,16 +70,13 @@ test('does not print the interactive focus indicator', async ({ page }) => { await page.emulateMedia({ media: 'print' }); expect(await page.evaluate(() => matchMedia('print').matches)).toBe(true); - const printFocusStyle = await content.evaluate((element) => { - const style = getComputedStyle(element); - return { - outlineStyle: style.outlineStyle, - outlineWidth: style.outlineWidth, - }; - }); + const printOutlineStyle = await content.evaluate( + (element) => getComputedStyle(element).outlineStyle, + ); - expect(printFocusStyle).toEqual({ - outlineStyle: 'none', - outlineWidth: '0px', - }); + // CSS `outline: none` suppresses painting via outline-style. Engines may + // still report the initial computed outline-width (`medium`, typically 3px), + // which is inert while outline-style is `none` and is therefore not evidence + // of printed focus chrome. + expect(printOutlineStyle).toBe('none'); }); From e56301cc2e338df1ba6156c3b5fed910d5e536c2 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 19 Aug 2026 22:35:52 -0700 Subject: [PATCH 63/67] docs(changelog): record editor focus accessibility contract --- CHANGELOG.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e0c53db1..0008f483 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,7 @@ Historical release entries from **0.1.0 through 0.5.27** are preserved verbatim ### Accessibility - Prepared the active-PR dark active-toolbar accent change from protected-main `#4493f8` to `#58a6ff`, increasing `--cwl-accent` text on `--cwl-accent-soft: #163356` from about 4.13:1 to about 5.06:1 so the candidate default 13px active-button text meets the WCAG 2.2 4.5:1 normal-text threshold; this remains active-PR evidence until protected integration. `getEditorThemeTokenContrast()` checks catalog values, and host overrides must be re-checked with `contrastRatioFromHex(actualForegroundHex, actualBackgroundHex)` using the actual resolved colors. +- Restored a visible `:focus-visible` indicator on the editable textbox, mapped it to `CanvasText` in forced-colors mode, and suppresses that interactive focus chrome under `@media print`; dependency-locked Chromium, Firefox, and WebKit acceptance exercises the packed stylesheet on the real `role="textbox"` surface. ## [0.6.0] — 2026-08-10 @@ -112,7 +113,7 @@ Historical release entries from **0.1.0 through 0.5.27** are preserved verbatim - Corrected the autosave onboarding so initial and replacement validators are checked before use and come from the durable host's server-issued strong `ETag` rather than local revision evidence; missing, weak, or malformed validators fail closed in the example - Documented that host-owned save callbacks must apply their own timeout or abort signal because an unresolved callback intentionally retains the active single-flight operation; retry policy remains host-owned - Added a deterministic repository contract test and APA 7th-style doctoring for README, npm-search, Node.js package-export discoverability, RFC 9110 validator ownership, and quoted opaque-tag syntax -- Added operator, doctoring, and release evidence for the durable autosave session, coherent recovery-time flush snapshots, host ownership boundaries, exact-head verification, and acquisition-review scope +- Added operator, doctoring, and release evidence for the durable autosave session, coherent recovery-time flush snapshots, exact-head verification, and acquisition-review scope ## [0.5.28] — 2026-08-05 From 6778fe1d04df0351c308fcf59c889324fb1209e3 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 20 Aug 2026 00:19:43 -0700 Subject: [PATCH 64/67] test(docs): reject stale protected-capability maturity --- ...tectureProtectedCapabilityMaturity.test.ts | 27 +++++++++++++++++++ 1 file changed, 27 insertions(+) create mode 100644 src/architectureProtectedCapabilityMaturity.test.ts diff --git a/src/architectureProtectedCapabilityMaturity.test.ts b/src/architectureProtectedCapabilityMaturity.test.ts new file mode 100644 index 00000000..03707863 --- /dev/null +++ b/src/architectureProtectedCapabilityMaturity.test.ts @@ -0,0 +1,27 @@ +import { readFileSync } from 'node:fs'; +import { resolve } from 'node:path'; + +import { describe, expect, it } from 'vitest'; + +const architecture = readFileSync( + resolve(process.cwd(), 'ARCHITECTURE.md'), + 'utf8', +); + +describe('protected architecture capability maturity', () => { + it('does not regress integrated Markdown packaging to an active proposal', () => { + expect(architecture).toContain('Protected markdown subpath'); + expect(architecture).toContain('ADR 0020 is Accepted on protected `main`'); + expect(architecture).not.toContain('active PR #114'); + expect(architecture).not.toContain('Proposed markdown subpath'); + }); + + it('does not regress integrated paged-media print behavior to an active proposal', () => { + expect(architecture).toContain( + 'CSS paged-media print boundary is implemented on protected `main`', + ); + expect(architecture).toContain('Accepted ADR 0021'); + expect(architecture).not.toContain('active PR #116'); + expect(architecture).not.toContain('Until #116 integrates'); + }); +}); From 6bcb6abb37939728baf6071ca8a265691d94e304 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 20 Aug 2026 00:20:49 -0700 Subject: [PATCH 65/67] docs(architecture): align protected package and print maturity --- ARCHITECTURE.md | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md index 5c9e6900..09059aaa 100644 --- a/ARCHITECTURE.md +++ b/ARCHITECTURE.md @@ -45,7 +45,7 @@ flowchart TB Selector[Protected text-position-selector subpath] Autosave[Protected autosave subpath] Converter[Protected converter subpath] - Markdown[Proposed markdown subpath\nactive PR #114] + Markdown[Protected markdown subpath] SharedPolicy[Shared deterministic URI / inline-image policy] Serializers[Shared deterministic serializers] @@ -60,7 +60,7 @@ flowchart TB Markdown --> Serializers ``` -The active `@contextualwisdomlab/cwl-editor/markdown` work in PR #114 is governed by Proposed ADR 0020. Its purpose is dependency isolation, not a second serializer authority. Until that PR or a verified successor integrates, the proposed subpath is unshipped and the protected root package remains the public authority for those serializers. +ADR 0020 is Accepted on protected `main`. The protected `@contextualwisdomlab/cwl-editor/markdown` subpath isolates the deterministic Markdown/HTML/email/plain-text serializers and shared safe-link/inline-image policy without creating a second serialization authority. The dependency-isolated subpath is shipped protected behavior and does not gain network, credential, persistence, tenant, delivery, collaboration-provider, or model authority. A framework-independent subpath must prove its declared dependency boundary from the packed npm artifact under ESM, CommonJS, and strict TypeScript consumers. Source-level import shape alone is insufficient release evidence. @@ -68,15 +68,15 @@ A framework-independent subpath must prove its declared dependency boundary from Inkspan owns the CSS rules it ships for its editor. It does not own the operating system print spooler, printer, browser's pagination implementation, downstream PDF storage, or host disclosure policy. -Protected `main` currently remains the stylesheet authority. Proposed ADR 0021 and active PR #116 define a CSS-only `@media print` boundary intended to: +CSS paged-media print boundary is implemented on protected `main` under Accepted ADR 0021. The shipped CSS-only `@media print` boundary: -- remove Inkspan-owned screen-only scroll/max-height clipping; -- hide toolbar, collaboration status, remote caret/cursor-label, and placeholder UI from printed document output; -- preserve authored document structures and links; -- use conservative paged-fragmentation hints; and -- keep links distinguishable without relying on color alone. +- removes Inkspan-owned screen-only scroll/max-height clipping; +- hides toolbar, collaboration status, remote caret/cursor-label, and placeholder UI from printed document output; +- preserves authored document structures and links; +- uses conservative paged-fragmentation hints; and +- keeps links distinguishable without relying on color alone. -This proposed presentation line does **not** create a JavaScript print mode, PDF service, page-number/header/footer authority, timestamp/signature claim, persistence layer, network requirement, credential, or model dependency. Until #116 integrates, the new print behavior is not shipped. +This protected presentation line does **not** create a JavaScript print mode, PDF service, page-number/header/footer authority, timestamp/signature claim, persistence layer, network requirement, credential, or model dependency. Browser/OS print destination and any durable authorized export remain host/user responsibilities. ## Modular MSA composition From fcaf68e83cb8a02e7509aa6b71d379a1f30092ab Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 20 Aug 2026 00:32:44 -0700 Subject: [PATCH 66/67] chore(ownership): restore release-blocker scope --- ARCHITECTURE.md | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md index 09059aaa..5c9e6900 100644 --- a/ARCHITECTURE.md +++ b/ARCHITECTURE.md @@ -45,7 +45,7 @@ flowchart TB Selector[Protected text-position-selector subpath] Autosave[Protected autosave subpath] Converter[Protected converter subpath] - Markdown[Protected markdown subpath] + Markdown[Proposed markdown subpath\nactive PR #114] SharedPolicy[Shared deterministic URI / inline-image policy] Serializers[Shared deterministic serializers] @@ -60,7 +60,7 @@ flowchart TB Markdown --> Serializers ``` -ADR 0020 is Accepted on protected `main`. The protected `@contextualwisdomlab/cwl-editor/markdown` subpath isolates the deterministic Markdown/HTML/email/plain-text serializers and shared safe-link/inline-image policy without creating a second serialization authority. The dependency-isolated subpath is shipped protected behavior and does not gain network, credential, persistence, tenant, delivery, collaboration-provider, or model authority. +The active `@contextualwisdomlab/cwl-editor/markdown` work in PR #114 is governed by Proposed ADR 0020. Its purpose is dependency isolation, not a second serializer authority. Until that PR or a verified successor integrates, the proposed subpath is unshipped and the protected root package remains the public authority for those serializers. A framework-independent subpath must prove its declared dependency boundary from the packed npm artifact under ESM, CommonJS, and strict TypeScript consumers. Source-level import shape alone is insufficient release evidence. @@ -68,15 +68,15 @@ A framework-independent subpath must prove its declared dependency boundary from Inkspan owns the CSS rules it ships for its editor. It does not own the operating system print spooler, printer, browser's pagination implementation, downstream PDF storage, or host disclosure policy. -CSS paged-media print boundary is implemented on protected `main` under Accepted ADR 0021. The shipped CSS-only `@media print` boundary: +Protected `main` currently remains the stylesheet authority. Proposed ADR 0021 and active PR #116 define a CSS-only `@media print` boundary intended to: -- removes Inkspan-owned screen-only scroll/max-height clipping; -- hides toolbar, collaboration status, remote caret/cursor-label, and placeholder UI from printed document output; -- preserves authored document structures and links; -- uses conservative paged-fragmentation hints; and -- keeps links distinguishable without relying on color alone. +- remove Inkspan-owned screen-only scroll/max-height clipping; +- hide toolbar, collaboration status, remote caret/cursor-label, and placeholder UI from printed document output; +- preserve authored document structures and links; +- use conservative paged-fragmentation hints; and +- keep links distinguishable without relying on color alone. -This protected presentation line does **not** create a JavaScript print mode, PDF service, page-number/header/footer authority, timestamp/signature claim, persistence layer, network requirement, credential, or model dependency. Browser/OS print destination and any durable authorized export remain host/user responsibilities. +This proposed presentation line does **not** create a JavaScript print mode, PDF service, page-number/header/footer authority, timestamp/signature claim, persistence layer, network requirement, credential, or model dependency. Until #116 integrates, the new print behavior is not shipped. ## Modular MSA composition From 11d5cfecdcc0949ec98e6ca110d482124bff00c4 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 20 Aug 2026 00:32:54 -0700 Subject: [PATCH 67/67] chore(ownership): remove duplicate maturity contract --- ...tectureProtectedCapabilityMaturity.test.ts | 27 ------------------- 1 file changed, 27 deletions(-) delete mode 100644 src/architectureProtectedCapabilityMaturity.test.ts diff --git a/src/architectureProtectedCapabilityMaturity.test.ts b/src/architectureProtectedCapabilityMaturity.test.ts deleted file mode 100644 index 03707863..00000000 --- a/src/architectureProtectedCapabilityMaturity.test.ts +++ /dev/null @@ -1,27 +0,0 @@ -import { readFileSync } from 'node:fs'; -import { resolve } from 'node:path'; - -import { describe, expect, it } from 'vitest'; - -const architecture = readFileSync( - resolve(process.cwd(), 'ARCHITECTURE.md'), - 'utf8', -); - -describe('protected architecture capability maturity', () => { - it('does not regress integrated Markdown packaging to an active proposal', () => { - expect(architecture).toContain('Protected markdown subpath'); - expect(architecture).toContain('ADR 0020 is Accepted on protected `main`'); - expect(architecture).not.toContain('active PR #114'); - expect(architecture).not.toContain('Proposed markdown subpath'); - }); - - it('does not regress integrated paged-media print behavior to an active proposal', () => { - expect(architecture).toContain( - 'CSS paged-media print boundary is implemented on protected `main`', - ); - expect(architecture).toContain('Accepted ADR 0021'); - expect(architecture).not.toContain('active PR #116'); - expect(architecture).not.toContain('Until #116 integrates'); - }); -});