Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
39 commits
Select commit Hold shift + click to select a range
d89bd41
test(accessibility): define forced-colors state contract
seonghobae Aug 10, 2026
d94c6b5
fix(accessibility): preserve forced-colors state cues
seonghobae Aug 10, 2026
1b34fe4
test(accessibility): verify forced-colors in real engines
seonghobae Aug 10, 2026
3fdd42d
test(accessibility): define editor focus-visible contract
seonghobae Aug 10, 2026
3b44e67
fix(accessibility): restore editor focus-visible cue
seonghobae Aug 10, 2026
23a24f5
test(accessibility): prove editable forced-colors focus cue in browser
seonghobae Aug 10, 2026
bcd993a
test(a11y): prove forced-colors cascade ordering
seonghobae Aug 10, 2026
bd10925
fix(a11y): place forced-colors overrides after base rules
seonghobae Aug 10, 2026
940c2e1
test(a11y): scope forced-colors order before print media
seonghobae Aug 10, 2026
862db1d
test(a11y): distinguish media overrides from later screen rules
seonghobae Aug 10, 2026
a517183
chore(a11y): synchronize forced-colors lane with protected main
seonghobae Aug 17, 2026
5a0d410
Merge remote-tracking branch 'origin/main' into HEAD
seonghobae Aug 26, 2026
f522d1f
fix(a11y): unify forced-colors layer into one cascade-effective block
seonghobae Aug 26, 2026
48fffe1
test(accessibility): drive forced-colors focus by keyboard
seonghobae Aug 26, 2026
a10b7c9
test(accessibility): run forced-colors browser coverage
seonghobae Aug 28, 2026
eb3618c
fix(accessibility): respect browser harness ownership
seonghobae Aug 28, 2026
9cf8192
Merge remote-tracking branch 'origin/test/cjk-input-assurance-376' in…
seonghobae Sep 4, 2026
3378ac4
test(accessibility): use keyboard forced-colors journey
seonghobae Sep 4, 2026
e892384
test(accessibility): follow macOS WebKit tab semantics
seonghobae Sep 4, 2026
74580c1
chore(accessibility): synchronize input parent
seonghobae Sep 4, 2026
a7d9919
chore: synchronize input assurance parent
seonghobae Sep 5, 2026
9f82913
merge: inherit current input assurance into forced colors owner
seonghobae Sep 5, 2026
4bc3c8f
test(a11y): detect clipped controls in the real narrow toolbar
seonghobae Sep 6, 2026
e53eefa
fix(a11y): wrap toolbar groups before controls are clipped
seonghobae Sep 6, 2026
dd1a780
merge: inherit the verified input-owner teardown regression
seonghobae Sep 6, 2026
60fa76e
test: retain responsive keyboard focus screenshots
seonghobae Sep 6, 2026
8f8ff6e
test: expose disabled colors on readable editor content
seonghobae Sep 6, 2026
f97452a
Merge canonical browser stylesheet provenance into UI assurance
seonghobae Sep 6, 2026
a069699
fix: preserve readable content colors in forced color mode
seonghobae Sep 6, 2026
0e8d8e3
test: match the real toolbar focus label
seonghobae Sep 6, 2026
8378380
test: inspect real pressed toolbar state and empty guidance
seonghobae Sep 6, 2026
a9f2775
test: distinguish active-button transition and settled paint
seonghobae Sep 6, 2026
f6dbf52
test: expose forced-color highlight paint failures
seonghobae Sep 6, 2026
7f6edc7
fix: avoid interpolated colors in forced-color buttons
seonghobae Sep 6, 2026
0c29f3f
test: exercise authored collaboration colors in forced mode
seonghobae Sep 6, 2026
7a95e5a
fix: preserve readable system highlight labels
seonghobae Sep 6, 2026
85e6444
test: record forced-color adjustment capability per engine
seonghobae Sep 6, 2026
cc766fd
merge: inherit canonical collaboration browser probe
seonghobae Sep 6, 2026
59d3220
merge: inherit canonical browser archive evidence guard
seonghobae Sep 6, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions docs/PRD.md
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,8 @@ The product promise is: **author, convert, collaborate, and prove document chang

- Native controls, focus behavior, keyboard parity, truthful `aria-keyshortcuts` metadata, non-color status semantics, and host-facing lifecycle state support WCAG-oriented embedding.
- Toolbar shortcut metadata must reflect repository-level shipped behavior, including host/editor bindings such as link editing, rather than only extension-local defaults.
- Active PR / Proposed: at a 320 CSS-pixel viewport, every toolbar control must remain fully visible in normal and forced colors. A page without horizontal scrolling is insufficient evidence if controls are clipped inside the editor; see the [toolbar reflow record](doctoring/toolbar-reflow.md).
- Active PR / Proposed: empty-editor guidance and quoted content use readable document text colors in forced-colors mode, while disabled controls retain a distinct inactive appearance. Selected and hovered controls must keep their labels readable immediately after interaction; see the [visual inspection record](doctoring/forced-colors-readable-content.md).
- Application-visible saving/conflict/recovery messages must be derivable from programmatic state without Inkspan prescribing untranslated user-facing copy.
- Export/print surfaces must not rely on color alone or inaccessible interaction-only state where the corresponding product surface exists.

Expand Down
8 changes: 8 additions & 0 deletions docs/TRD.md
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,14 @@ Identity inspection returns no partial routing object on malformed input. An unk

## Accessibility and interaction semantics

Active PR / Proposed toolbar reflow lets controls within an oversized group wrap using native CSS while preserving command availability, DOM/keyboard order, theme tokens and print behavior. Real-editor browser checks compare every button's complete bounds with the toolbar at 320 CSS pixels in normal and forced colors; page scroll width or a simplified chrome fixture alone cannot prove that controls are visible. The [toolbar reflow record](doctoring/toolbar-reflow.md) records the causal failure and alternatives. No package/API, document, persistence or host-authority contract changes.

Active PR / Proposed forced-colors presentation uses `CanvasText` for readable placeholder guidance and blockquote content, retaining `GrayText` only for disabled toolbar controls. Browser checks observe the placeholder pseudo-element and quoted text against the selected document text color and preserve keyboard-focus screenshots. The [visual inspection record](doctoring/forced-colors-readable-content.md) distinguishes stylesheet/browser observations from physical OS palettes or whole-product WCAG conformance.

Within that forced-colors layer, toolbar color transitions are disabled to avoid temporarily interpolating away from system colors. Only hovered/active toolbar buttons and collaboration cursor labels use `forced-color-adjust: none`, paired with the user's `Highlight`/`HighlightText` colors, so automatic text backplates cannot conceal their labels. Generic controls, disabled unselected buttons and authored document content retain automatic adjustment; no hard-coded palette or editor-wide opt-out is introduced.

The two forced-mode cursor-label color declarations override the renderer's normal inline colors. The browser fixture uses the selected source/package collaboration renderer and checks normal colors before the forced system pair; normal mode and cursor input sanitization remain unchanged.

Toolbar shortcut metadata is implemented on protected `main`. Shipped keyboard behavior, focus behavior, native controls, `aria-pressed`, `aria-keyshortcuts`, programmatic save/conflict state, and visible shortcut documentation must agree. Repository-level keyboard behavior outranks extension-local defaults when determining metadata. Status must not depend on color alone. Inkspan exposes machine state sufficient for host WCAG-oriented messaging while leaving localization and application-specific live-region policy to the host.

Accessible editor placeholder semantics are implemented on protected `main`. Standalone and collaborative textbox surfaces expose the same normalized non-blank host-supplied visual placeholder through `aria-placeholder`; whitespace-only guidance is omitted, and placeholder updates do not replace the current TipTap editor or host-owned Yjs binding. `aria-labelledby`/`aria-label` remain the accessible-name authority and placeholder guidance never grants editability.
Expand Down
122 changes: 122 additions & 0 deletions docs/doctoring/forced-colors-readable-content.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
# Doctoring record: readable content in forced colors

Date: 2026-09-06
Status: Active PR / Proposed
Owner: Inkspan presentation, existing PR #151

## Visual finding and causal check

Direct inspection of the retained three-engine, 320 CSS-pixel editor screenshots
found unusually faint empty-editor guidance in WebKit's forced-colors rendering.
The images belong to `dd1a78045807e37e32f87810564be4b1fefd56df`, not a newer
source generation. Its stylesheet blob
`265e79cc53f540cdf3de1b4ab10a4139c4444756` matches the test-only integrated
candidate `f97452a902326d76061b6c770ebf69b68e8cdca0` exactly.

The shared stylesheet assigned `GrayText` to both placeholder guidance and
blockquote content. W3C defines that system color for disabled text. Neither
an instruction in an editable document nor authored quoted text is an inactive
control. The focused regression at
`8f8ff6e3040d0fd2e436a65090bf3350f571b824` failed both content selectors while
the six existing stylesheet checks passed. That is a stylesheet-contract RED;
the earlier screenshots are retained visual evidence, not a fresh browser RED
or a measured contrast ratio.

## Minimal repair and alternatives

Change the two content declarations to the existing `CanvasText` system color.
Keep `GrayText`, full opacity and a visible border for disabled toolbar buttons.
Normal themes, controls, keyboard order, print output, accessible names, document
serialization and host ownership do not change. No dependency or abstraction is
needed. CONTRACTS and ADR ownership remain unchanged.

Keeping disabled-text colors on readable content was rejected because it gave
the wrong semantic cue and produced faint guidance in the inspected rendering.
Hard-coded black/white colors or globally opting out of forced colors were rejected
because they can conflict with the user's selected palette. Text identity and
structural cues distinguish placeholders/quotes without borrowing disabled state.

## Follow-up finding: selected labels disappear

The nine-case prebuilt source-browser run at
`83783803d76c47694f4022ac7cb760f5e83815bf` passed its semantic and geometry
assertions, but direct screenshot inspection found Chromium's real pressed
Bold button blank. The smaller fixture also showed blank active/remote labels.
This visual failure prevents UI acceptance despite the passing test count.

At diagnostic head `a9f2775669a5346132ed5f4994dbb6e13e898271`, the real
button's initial foreground and background both computed to white during its
120 ms color transition. After animation completion, the background used the
system highlight color but a white text backplate still obscured the label.
The strengthened contract at `f6dbf5257a47b7ee05747306e924914292616348`
failed four stylesheet checks and all three selected real-engine cases.

Disabling only toolbar transitions at
`7f6edc7770104cdbd9d7e09aface53f9bbb26b92` removed the interpolated white
background but retained the blank Chromium label. Its screenshot, paint values
and failed check are preserved as an incomplete alternative, not a fix.

The repair retains that forced-mode-only transition removal and sets
`forced-color-adjust: none` only on the three existing system-highlight pairs:
hovered buttons, active buttons and collaboration cursor labels. Each still uses
the user's `Highlight` and `HighlightText`; none uses a hard-coded palette.
The editor, document text and generic controls retain automatic adjustment.
This narrow override corrects the observed backplate conflict under the CSS
Color Adjustment guidance; a whole-editor opt-out would be unnecessarily broad.

The actual collaboration renderer also supplies inline foreground/background
colors. At `0c29f3fed7c12a18ad032eb73273af99d7e5788a`, using that renderer
instead of hand-written cursor markup exposed another three-engine failure:
the narrow adjustment exception retained the author's colors. The two system
color declarations therefore use `!important` inside the forced-colors layer
to override those normal inline values. The browser fixture imports the selected
source or packaged collaboration entrypoint lazily, verifies its normal colors,
then requires the forced label to match the active system-highlight pair.
This does not change the renderer, its sanitization, or normal-mode colors.

Regression evidence must include immediate and settled selected-label screenshots,
hovered labels, keyboard focus and disabled-state cues. CSS/computed-style checks
alone cannot prove painted text is readable. The diagnostic prebuilt configuration
keeps normal test/assertion/startup deadlines, retries and browser projects but
runs the build separately after the standard command exceeded its startup limit.
It is not a passing standard startup run, packed-release proof or a latency claim.

## Runnable verification and limits

```sh
pnpm exec vitest run src/forcedColorsStyles.test.ts
pnpm --dir tests/browser exec playwright test --config playwright.config.ts specs/forced-colors.browser.spec.ts --workers=1
```

The browser regression checks both content selectors against document text,
including the actual editor's placeholder pseudo-element at 320 CSS pixels.
Retain and inspect normal/forced-colors toolbar images, actual Tab-focus images
and the focused editor fixture. Run the complete source and extracted-package
browser suites with their own exact-head/artifact receipts; the inherited
stylesheet-provenance checks must prove the selected CSS was loaded.

A selected system palette may intentionally differ between browsers or user
preferences. Do not infer a numeric contrast ratio from screenshot appearance,
claim physical-device/OS coverage from emulation, or treat these narrow checks
as whole-product WCAG certification. Final run outcomes belong in dated PR
evidence. Failed/partial runs stay retained rather than being normalized away.

The inspected WebKit build emulates the media query but does not expose the
`forced-color-adjust` property. Record its live `CSS.supports` result and empty
computed property rather than claiming support. All color, geometry, focus,
interaction and screenshot checks still run in that engine; no test is skipped
for this capability difference. Chromium and Firefox also report their actual
capabilities rather than receiving browser-name-based exceptions.

## Standards basis

World Wide Web Consortium. (2026). *CSS Color Module Level 4: CSS system colors*.
https://www.w3.org/TR/css-color-4/#css-system-colors

World Wide Web Consortium. (n.d.). *Understanding Success Criterion 1.4.3:
Contrast (Minimum)*. Web Accessibility Initiative. Retrieved September 6, 2026,
from https://www.w3.org/WAI/WCAG21/Understanding/contrast-minimum.html

World Wide Web Consortium. (2025, December 16). *CSS Color Adjustment Module
Level 1* (Candidate Recommendation Snapshot, Section 3.2).
https://www.w3.org/TR/2025/CR-css-color-adjust-1-20251216/#forced-color-adjust-prop
37 changes: 37 additions & 0 deletions docs/doctoring/toolbar-reflow.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
# Doctoring record: narrow toolbar control visibility

Date: 2026-09-06
Status: Active PR / Proposed
Owner: Inkspan presentation, existing PR #151

## User-visible failure and causal evidence

At 320 CSS pixels, authors could see only part of the image insertion button and could not see the image alternative-text button. The page itself had no horizontal overflow: its scroll width and viewport width both measured 320. The editor's clipping concealed overflow inside the toolbar, so page-width checks and the smaller forced-colors chrome fixture had passed without proving that all real controls were accessible.

The actual public editor is mounted by `tests/browser/input-harness.ts`. `Toolbar` in `src/components/Toolbar.tsx` renders the affected controls in one group; `src/styles.css` allowed the outer toolbar to wrap groups but did not allow a group to wrap its controls. The group's minimum content width exceeded the available space. This is an Inkspan presentation defect, separate from the reference host's previously repaired control-style leakage.

The test-only baseline `4bc3c8fab0c697dba0576686325cf09fe61242b3` adds full-button bounds checks to `tests/browser/specs/forced-colors.browser.spec.ts`. Each of Chromium, Firefox and WebKit reported the same two clipped controls in both normal and forced colors: 12 clipped-control observations across six independent engine/mode cases. The initial incorrectly named test locator failed before reaching geometry and is not counted as product evidence. Screenshots are captured before the assertion so failing states remain inspectable.

## Choice and alternatives

Allow the existing group to wrap with `flex-wrap: wrap`. It adds no JavaScript, dependency, breakpoint, observer or alternative navigation. It preserves every command, the existing control sizes, DOM order, keyboard behavior, theme tokens and host boundary. A narrow group may occupy additional vertical space.

Keeping the page-width-only check was rejected because it reproduced a false pass. Removing clipping from the editor was rejected because controls would escape their surface. Hiding commands or shrinking their targets would trade away functionality or accessibility. A horizontally scrolling group could preserve access, but adds a separate navigation action when native wrapping can keep every control visible. No new layout framework or component abstraction is needed.

## Verification and limits

Run the actual-control regression across the three configured engines:

```sh
pnpm --dir tests/browser exec playwright test specs/forced-colors.browser.spec.ts --grep 'keeps every real toolbar control'
```

The optimization metric is the total count of clipped controls across all six engine/mode cases; lower is better, and acceptance requires zero. Keep existing keyboard, touch, composition, print, package, production-coverage and Office checks. Inspect the screenshots as well as geometry. A successful source-harness run is not installed-tarball identity, physical-device/OS high-contrast evidence, general WCAG certification or a performance result. Final exact-head results and artifact identities belong in dated PR evidence, not protected-main claims.

The PRD accessibility requirement, TRD interaction boundary and product-technical gap baseline link this record. CONTRACTS remains unchanged because no public API, document/evidence schema, callback behavior or host ownership moves. The candidate must retain normal protected review and parent-before-child integration; local success does not ship the change. Before integration, a rejected experiment is reversed with an ordinary inverse change while retaining its evidence, never by hiding controls or weakening the visibility check.

## Standards basis

WCAG's Reflow guidance describes preserving information and functionality at a 320 CSS-pixel width and acknowledges exceptions for content that needs a two-dimensional layout. Inkspan does not need to use that exception to hide these controls: wrapping can preserve their availability. This record uses the guidance to choose and test a repair, not to claim whole-product conformance.

World Wide Web Consortium. (n.d.). *Understanding Success Criterion 1.4.10: Reflow*. Web Accessibility Initiative. Retrieved September 6, 2026, from https://www.w3.org/WAI/WCAG22/Understanding/reflow.html
8 changes: 8 additions & 0 deletions docs/product-technical-gap-baseline.md
Original file line number Diff line number Diff line change
Expand Up @@ -152,6 +152,14 @@ runtime boundary:
- package provenance, license/SBOM completeness, and reproducible artifacts;
- accessibility evidence across keyboard, forced-colors, print, narrow viewport,
and real browser engines;
- complete visibility of every actual toolbar control at narrow widths, checked
against its containing toolbar rather than inferred from page scroll width;
[toolbar reflow](doctoring/toolbar-reflow.md) records the reproducible clipping
failure and the Active PR / Proposed repair;
- direct visual inspection of readable guidance and document content in forced
colors, alongside immediate selected/hovered labels, disabled-state and keyboard-focus cues; the
[readable-content record](doctoring/forced-colors-readable-content.md) records
the Active PR / Proposed correction without claiming whole-product conformance;
- clear rollback and incident paths for ambiguous publication or persistence
outcomes.

Expand Down
35 changes: 22 additions & 13 deletions src/editorFocusStyles.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,30 +4,39 @@ import { resolve } from 'node:path';
import { describe, expect, it } from 'vitest';

const styles = readFileSync(resolve(process.cwd(), 'src/styles.css'), 'utf8');
const forcedColorsIndex = styles.indexOf('@media (forced-colors: active)');
const forcedColorsStyles =
forcedColorsIndex >= 0 ? styles.slice(forcedColorsIndex) : '';

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,
);
describe('editable surface focus stylesheet contract', () => {
it('replaces the removed browser outline with a visible keyboard focus cue', () => {
const ordinaryRule = /\.cwl-editor__content:focus-visible\s*\{[^}]*outline:\s*2px\s+solid\s+var\(--cwl-accent\)\s*;[^}]*outline-offset:\s*-2px\s*;/u;

expect(styles).toMatch(ordinaryRule);
// Exactly one ordinary rule keeps the forced-colors override authoritative.
expect(styles.match(new RegExp(ordinaryRule.source, 'gu'))?.length).toBe(1);
});

it('keeps the editable focus indicator visible in forced-colors mode', () => {
it('keeps the editable focus cue visible in forced-colors mode', () => {
const ordinaryFocusRule =
/\.cwl-editor__content:focus-visible\s*\{[^}]*outline:\s*2px\s+solid\s+var\(--cwl-accent\)\s*;[^}]*\}/u.exec(
/\.cwl-editor__content:focus-visible\s*\{[^}]*outline:\s*2px\s+solid\s+var\(--cwl-accent\)\s*;[^}]*outline-offset:\s*-2px\s*;/u.exec(
styles,
);
expect(ordinaryFocusRule).not.toBeNull();

const forcedColorsIndex = styles.indexOf('@media (forced-colors: active)');
expect(forcedColorsIndex).toBeGreaterThan(ordinaryFocusRule?.index ?? Number.MAX_SAFE_INTEGER);
const forcedColorsStyles = styles.slice(forcedColorsIndex);
// The single screen forced-colors layer must cascade after the
// theme-colored rule so the system-color override is effective.
expect(forcedColorsIndex).toBeGreaterThan(
ordinaryFocusRule?.index ?? Number.MAX_SAFE_INTEGER,
);

// Editor content keeps guaranteed canvas contrast under forced colors.
expect(forcedColorsStyles).toMatch(
/\.cwl-editor__content:focus-visible\s*\{[^}]*outline-color:\s*CanvasText\s*;/u,
);
// No competing shorthand may resurrect a second editor-content focus color.
expect(forcedColorsStyles).not.toMatch(
/\.cwl-editor__content:focus-visible\s*\{[^}]*outline:\s*2px\s+solid\s+(?:Highlight|var\(--cwl-accent\))\s*;/u,
);
});
});
Loading