Skip to content

Give the workflow canvas the docs theme, with light and dark modes - #85

Open
knihit wants to merge 20 commits into
wc/05-canvasfrom
wc/05b-theme
Open

knihit wants to merge 20 commits into
wc/05-canvasfrom
wc/05b-theme

Conversation

@knihit

@knihit knihit commented Oct 25, 2026 •

Copy link
Copy Markdown
Contributor

Description

The workflow canvas page now has the look of the Konductor docs theme, in a light and a dark mode. It adds no panel, no toolbar and no API call; the contract and the stub are unchanged.

Theme (workflow-canvas/src/theme/):

  • tokens.ts is the one place for the theme: two palettes (light and dark), the Outfit and JetBrains Mono font stacks, the corner radii, and the Cloudscape token map built from them. It is applied with the public applyTheme (@cloudscape-design/components/theming) for colours, fonts and radii, and the mode is switched with applyMode (@cloudscape-design/global-styles).
  • fonts.ts loads Outfit and JetBrains Mono from the pinned @fontsource/outfit and @fontsource/jetbrains-mono packages (5.3.0, SIL Open Font License 1.1). The Latin files are bundled, so the page requests no font from another site. The page imports the base Cloudscape styles, which leave out the Open Sans faces. The screenshot capture now uses these faces, so the Open Sans dev dependency is removed.
  • mode.ts and use-theme-mode.ts: the first visit follows prefers-color-scheme; pressing the toggle saves the mode in local storage, and the saved mode wins from then on. A blocked storage is handled (the choice then lasts until the page closes).

Page (src/chrome/, src/App.tsx):

  • A top bar with the K logo, "Workflow canvas" and a Theme: light / Theme: dark button. The accessible name of the button starts with its visible text.
  • A dark header band with the breadcrumb Workflows > hello (plain text, no dead link), the title and the Designer and Execution tabs. The band is dark in both modes, with text colours set for it, and the whole tab strip (labels, rule and selected-tab indicator) is on the band.

Canvas (src/canvas/canvas.css, nodes.tsx):

  • Cards with the theme's corner radius, an end pill, and light and dark canvas colours. The canvas colours stay in canvas.css; the theme module names every one of them, and a test fails when the file holds a colour that the module does not name, a --wf-* value that is not 6-digit hex, or any hex or colour function (3-digit hex, rgb()) outside the two variable blocks. In forced-colors mode the card uses the system colours.
  • The S run-order ports are pieces of a jigsaw, 26 by 16 px with a dark rim and the letter S: the out port is a square tab that hangs from the bottom edge, and the in port is a notch of the same outline in the top edge (white inside, with a ring in the colour of the tab), both in the middle of the card, so a tab fits the notch of the card under it. The tab is the same gray in both modes, and jigsaw.ts defines the shape once. A link runs from the tip of the tab to the top of the notch. The letter still uses the page font, so screenshots match in CI. The keyboard focus ring is drawn around the card and the tab, so it follows the tab instead of cutting across it. A stage is 84 px high and the end node 48, so the text starts below the notch. Links are rails with an arrowhead.

Left out on purpose: Library, Properties and YAML panels, the toolbar, and any editing. The harness is still kiro-cli only.

Docs: workflow-canvas/README.md has a Theme section. CHANGELOG has an entry under Unreleased. VERSION is untouched.

How to run

cd workflow-canvas
npm ci
npm run dev:stub

Open the address it prints, then use the Theme button in the top bar.

Type of Change

  • Dependency change (package.json, Cargo.toml, or CLI dependency)
  • Documentation
  • Other: the look of the workflow canvas, in workflow-canvas/

Testing

  • Root npm test (tests/scripts/**/*.test.js): not run; this PR changes only workflow-canvas/, the root CHANGELOG.md and its own docs.
  • If cli/ changed: not applicable
  • Smoke tested affected agent(s): not applicable, no agent changed
  • If an agent's or skill's behavior changed: not applicable
  • New or changed code files carry the required SPDX header (see AGENTS.md's License headers rule); formats without comment syntax (e.g. JSON) are exempt

Checked in workflow-canvas/: npm run lint, npm run typecheck (3 projects), npm test (44 files, 726 tests), npm run build and npm run test:e2e (45 tests, against the stub) all pass at 7746391. npm run screenshots gives byte-identical files on a second run. The build prints only the existing chunk-size warning.

Tests added:

  • Unit: the mode choice (saved choice, system setting, bad value, blocked storage); the contrast of every text and edge colour pair of both palettes, of the header band (including the selected-tab indicator) and of the canvas CSS, tab and notch included, in both modes against WCAG 2 AA (4.5:1 for text, 3:1 for edges, ports and the focus ring); every canvas CSS colour and the radii match the theme module (an unnamed colour fails); the base styles carry no font face; the shape of the ports and the node outline (jigsaw.test.ts); the tab-strip selector is the same in the script and the stylesheet; the inline script that sets the mode before first paint; every colour token has a value for both modes; the toggle in the page (label, data-theme, saved value, saved mode on start, nothing saved until a choice).
  • e2e (e2e/tests/theme.spec.ts): the tab strip colours in both modes; the saved mode is set before the app script runs; top bar, breadcrumb and title; fonts load and no request leaves the origin; light first load saves nothing; dark system setting starts dark; toggle and reload; a saved choice beats the system setting; axe (colour contrast included) in both modes; the focus ring in dark mode; the toggle by keyboard.
  • e2e (e2e/tests/canvas.spec.ts): a tab under every node but the end and a notch in every node but the intent; each tab is centred on its card and over the notch under it; the link starts at the tip of the tab and ends at the top of the notch; the focus ring goes around the tab.

Screenshots

Designer tab, full page, 1280x800 (regenerated by npm run screenshots):

Light Dark
Designer tab, light theme Designer tab, dark theme

Checklist

  • My code follows the style guidelines of this project (see AGENTS.md's Code Style & Conventions)
  • I have performed a self-review of my own changes
  • I have commented my code where necessary
  • I have made corresponding changes to the documentation
  • My changes generate no new warnings — build is clean
  • I have added or updated tests that prove my change works (see Testing above for the exact commands)

Notes for Reviewers

  • The PR is stacked on Draw the workflow on the Designer tab with React Flow #83 (wc/05-canvas); the base is wc/05-canvas, and this PR has 17 commits on top of it, the last four answering adversarial reviews.
  • Follow-ups: the Execution tab is still a placeholder; Library, Properties and YAML panels; editing.
  • Known limits: the mode is read at load, so a later change of the system setting, or of the choice in another tab, shows after a reload. The top bar moves the theme button into its overflow menu on a very narrow window (Cloudscape behaviour). The product name in the top bar is a Cloudscape link that needs an href, so it points at # and does nothing. The header band is drawn with solid colours, because Cloudscape's theming API sets colours and not gradients.

Fixes from the second adversarial review

  • Rebased onto the new wc/05-canvas.
  • CodeQL js/bad-tag-filter (alert 16): the early-mode test found the inline script with a pattern whose start tag was a bare script tag. A small helper (test-support/inline-script.ts) now takes attributes and any case on both tags, with a word boundary after the name, and its test covers <SCRIPT type="module"> and </script >. The alert closes after CodeQL scans a head of this stack, or when the stack reaches main; it cannot be closed from here.
  • The strong wash is lighter, and a test lays each wash over every area it can lie on and checks AA contrast for text, muted text and links, in both modes. The old washes were under 4.5:1 in four places.
  • Rejected: the top bar identity link. Cloudscape's TopNavigation needs an href, and the page has no other place to go, so following it does nothing; a comment says why.

Fixes from the third adversarial review

  • The colour guard sees 3-digit hex, rgb() and the other colour functions, in a variable or in a rule (tests mutated each path and failed).
  • Rebased onto the new wc/05-canvas.
  • Rejected: forced-colors rules for the ports (cosmetic; they stay legible); the hover-over-selected row in the contrast matrix (this page renders no selectable rows yet); wording and size nits of the port shapes.

Restack

Round 6

Rebased on #83. The early-mode inline script would be blocked by the new Content-Security-Policy, so the policy names it by the hash of its text (script-src 'self' 'sha256-...'), and test-support/csp.test.ts pins the whole policy, hash included, so it fails when the hash and the script drift apart or when a source is added. The e2e suite shows no violation.

Round 8: rebased on #83. test-support/csp.test.ts now hashes every inline script of index.html and expects each hash in script-src, so a second inline script fails the test until its hash is added on purpose (before, only the first script was read). A planted second script failed the new test and passed the old one. npm test (47 files, 806 tests), lint and typecheck pass.

Round 9: rebased on #83. No change of its own. npm test (47 files, 817 tests), lint and typecheck pass.

@knihit
knihit added this pull request to stack #79 October 25, 2026 17:56
@knihit
knihit marked this pull request as ready for review October 26, 2026 03:47
@knihit
knihit removed this pull request from stack #79 October 27, 2026 02:47
@knihit
knihit added this pull request to stack #101 October 27, 2026 02:47
@knihit
knihit removed this pull request from stack #101 October 27, 2026 02:48
@knihit
knihit added this pull request to stack #102 October 27, 2026 02:48
import { MODE_STORAGE_KEY } from '../src/theme/mode.ts';

const html = readFileSync(new URL('../index.html', import.meta.url), 'utf8');
const script = /<script>([\s\S]*?)<\/script>/.exec(html)?.[1] ?? '';
@knihit
knihit force-pushed the wc/05b-theme branch 5 times, most recently from e9e78ee to 48ed79c Compare October 27, 2026 21:25
@knihit
knihit removed this pull request from stack #102 October 27, 2026 22:40
@knihit
knihit force-pushed the wc/05b-theme branch 9 times, most recently from d791911 to 4390c73 Compare October 29, 2026 01:49
knihit added 20 commits October 29, 2026 02:40
The Designer and Execution tab strip, its rule and the selected-tab
indicator now sit on the dark band, so the indicator reaches 3:1. The
dark mode gets its own port colours, all canvas colour and radius tests run
in both modes and compare with the theme module, and the dark styles key on
a data-theme attribute that the page sets on the root, before the first
paint. The breadcrumb no longer has a link that does nothing.
The out port is a round knob on a narrow neck under the card, the in port
a matching round socket cut into the top edge. The card border is one
path that bends around both, and the shape is defined once in jigsaw.ts.
A link now runs from the knob tip to just above the socket mouth, so the
arrowhead no longer covers the socket.

Also restore the indent of two CHANGELOG entries, check that the tab
strip selector matches in the script and the stylesheet, assert the
theme attribute instead of a library class, name the white colour once
and derive the band divider from the dark border.
The dark --wf-text-muted was #b2a9cc while the theme module's muted text
is #9c93b8. Use the module's value, which still passes the AA text
contrast checks on the node and the canvas, assert it in the contrast
test, and regenerate the dark Designer screenshot.
…e outline

The out knob now sits on a neck about 9 px long and 7 px wide, with a
straight run before the head, so it reads as a jigsaw tab; the socket
uses the same curve. The keyboard focus ring is an SVG stroke around the
node outline and the knob instead of a box outline, so it no longer cuts
across the neck. Also wrap a long CHANGELOG line and regenerate the
Designer screenshots.
…sign

The out port is a 26 by 16 square tab with a dark rim that hangs from the
bottom edge, and the in port is a notch of the same outline in the top edge
(white inside, with a ring in the colour of the tab). Both sit in the middle
of the card, so a tab fits the notch of the card under it. The same gray tab
is used in the light and dark modes. The focus ring follows the card and the
tab. Links start at the tip of the tab and stop at the top of the notch.
A stage is 84 pixels high and the end node 48, and their text starts below
the notch, so a state badge on a stage no longer covers its second line.
HTML tag names are case-insensitive and an end tag may carry whitespace or attributes.
…n CSS

- Name every canvas colour in the theme module and make the test fail on a
  colour variable of canvas.css that it does not name.
- Draw the card in the system colours in forced-colors mode, so text does not
  sit on a patch of the dark fill.
- Import the Cloudscape base styles, which leaves out about 180 KB of Open Sans
  faces that no token selects.
- Take the logo accent from the palette, drop the unused overlay colour, stop
  exporting borderLines, and correct the port wording in the README and CSS.
- Lower the strong wash so text, muted text and links reach AA contrast on
  every area they can lie on, and test each pair with the wash laid over its
  backing colour.
- Say why the top bar identity link does nothing.
The start tag of the pattern was a bare script tag, so an upper case tag or
one with attributes was missed, as the end tag was before. The pattern now
takes attributes on both tags and a word boundary after the name, and the
body is found by a small helper with its own test.
The guard now rejects any --wf-* variable that is not 6-digit hex and any hex
or colour function outside the two variable blocks, 3-digit hex and rgb()
included.
The inline script that sets the mode before first paint would be blocked by default-src 'self'. The policy names it by the hash of its text, and a test keeps the hash in step.
The policy test hashed only the first inline script, so a second inline script added to index.html left the test green. It now hashes every inline script and expects each hash in script-src, so an extra script fails the test until its hash is added on purpose.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants