From 1c5acab526238c84d410b2866a6957f03c40fc15 Mon Sep 17 00:00:00 2001 From: Nihit <24500178+knihit@users.noreply.github.com> Date: Sun, 25 Oct 2026 17:55:30 +0000 Subject: [PATCH 001/476] Add the docs theme: tokens, bundled fonts and a light/dark mode switch --- workflow-canvas/package-lock.json | 19 +- workflow-canvas/package.json | 3 +- workflow-canvas/src/theme/contrast.ts | 26 ++ workflow-canvas/src/theme/fonts.ts | 10 + workflow-canvas/src/theme/install.ts | 29 ++ workflow-canvas/src/theme/mode.test.ts | 89 ++++++ workflow-canvas/src/theme/mode.ts | 60 ++++ workflow-canvas/src/theme/tokens.test.ts | 72 +++++ workflow-canvas/src/theme/tokens.ts | 327 ++++++++++++++++++++ workflow-canvas/src/theme/use-theme-mode.ts | 27 ++ 10 files changed, 657 insertions(+), 5 deletions(-) create mode 100644 workflow-canvas/src/theme/contrast.ts create mode 100644 workflow-canvas/src/theme/fonts.ts create mode 100644 workflow-canvas/src/theme/install.ts create mode 100644 workflow-canvas/src/theme/mode.test.ts create mode 100644 workflow-canvas/src/theme/mode.ts create mode 100644 workflow-canvas/src/theme/tokens.test.ts create mode 100644 workflow-canvas/src/theme/tokens.ts create mode 100644 workflow-canvas/src/theme/use-theme-mode.ts diff --git a/workflow-canvas/package-lock.json b/workflow-canvas/package-lock.json index 139d142f..1fb21f24 100644 --- a/workflow-canvas/package-lock.json +++ b/workflow-canvas/package-lock.json @@ -18,7 +18,8 @@ "devDependencies": { "@apidevtools/swagger-parser": "13.1.0", "@axe-core/playwright": "4.14.0", - "@fontsource-variable/open-sans": "5.3.0", + "@fontsource/jetbrains-mono": "5.3.0", + "@fontsource/outfit": "5.3.0", "@playwright/test": "1.64.1", "@testing-library/dom": "10.4.2", "@testing-library/react": "16.3.3", @@ -620,10 +621,20 @@ } } }, - "node_modules/@fontsource-variable/open-sans": { + "node_modules/@fontsource/jetbrains-mono": { "version": "5.3.0", - "resolved": "https://registry.npmjs.org/@fontsource-variable/open-sans/-/open-sans-5.3.0.tgz", - "integrity": "sha512-VOfhZfLIOBVa6K/dTEu7scqQXYdGa4jBX8quFpQ0S6574B+W2EOBJf9PkqyVtMEI7WWIWNEbpvPc3vY1mN2mmQ==", + "resolved": "https://registry.npmjs.org/@fontsource/jetbrains-mono/-/jetbrains-mono-5.3.0.tgz", + "integrity": "sha512-fqDfB5I9f1p1TV486aUgB9t8zP84P0O1FtQR5Ol9vjwPy+S+EIGlVYm1cvj2W5shcZMTg2nZFdVMoH5wFu8a1A==", + "dev": true, + "license": "OFL-1.1", + "funding": { + "url": "https://github.com/sponsors/ayuhito" + } + }, + "node_modules/@fontsource/outfit": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/@fontsource/outfit/-/outfit-5.3.0.tgz", + "integrity": "sha512-0AVHzTTVJSxWDOxPKSfMeHhZeBFDU3cmgrcOb76ZEyhGo0+xgyG3GsLF3h9+X8w0IX7G4jzjJgFQEBiWdX6VgA==", "dev": true, "license": "OFL-1.1", "funding": { diff --git a/workflow-canvas/package.json b/workflow-canvas/package.json index a053db86..dbf91c8b 100644 --- a/workflow-canvas/package.json +++ b/workflow-canvas/package.json @@ -31,7 +31,8 @@ "devDependencies": { "@apidevtools/swagger-parser": "13.1.0", "@axe-core/playwright": "4.14.0", - "@fontsource-variable/open-sans": "5.3.0", + "@fontsource/jetbrains-mono": "5.3.0", + "@fontsource/outfit": "5.3.0", "@playwright/test": "1.64.1", "@testing-library/dom": "10.4.2", "@testing-library/react": "16.3.3", diff --git a/workflow-canvas/src/theme/contrast.ts b/workflow-canvas/src/theme/contrast.ts new file mode 100644 index 00000000..5c7fa155 --- /dev/null +++ b/workflow-canvas/src/theme/contrast.ts @@ -0,0 +1,26 @@ +// SPDX-License-Identifier: Apache-2.0 + +function channel(hex: string, start: number): number { + const value = parseInt(hex.slice(start, start + 2), 16) / 255; + return value <= 0.03928 ? value / 12.92 : ((value + 0.055) / 1.055) ** 2.4; +} + +function luminance(hex: string): number { + const digits = hex.replace('#', ''); + return ( + 0.2126 * channel(digits, 0) + + 0.7152 * channel(digits, 2) + + 0.0722 * channel(digits, 4) + ); +} + +/** The WCAG 2 contrast ratio of two `#rrggbb` colours, from 1 to 21. */ +export function contrastRatio(a: string, b: string): number { + const [light, dark] = [luminance(a), luminance(b)].sort((x, y) => y - x); + return ((light ?? 0) + 0.05) / ((dark ?? 0) + 0.05); +} + +/** The least ratio for body text (WCAG 2 AA). */ +export const AA_TEXT = 4.5; +/** The least ratio for the edge of a control or an icon (WCAG 2 AA). */ +export const AA_GRAPHIC = 3; diff --git a/workflow-canvas/src/theme/fonts.ts b/workflow-canvas/src/theme/fonts.ts new file mode 100644 index 00000000..40cfaff3 --- /dev/null +++ b/workflow-canvas/src/theme/fonts.ts @@ -0,0 +1,10 @@ +// SPDX-License-Identifier: Apache-2.0 +// Outfit for text and JetBrains Mono for code, both licensed under the SIL Open +// Font License 1.1. The Latin files are bundled with the page, so no font is +// requested from another site. +import '@fontsource/outfit/latin-400.css'; +import '@fontsource/outfit/latin-500.css'; +import '@fontsource/outfit/latin-600.css'; +import '@fontsource/outfit/latin-700.css'; +import '@fontsource/jetbrains-mono/latin-400.css'; +import '@fontsource/jetbrains-mono/latin-700.css'; diff --git a/workflow-canvas/src/theme/install.ts b/workflow-canvas/src/theme/install.ts new file mode 100644 index 00000000..0e9e6152 --- /dev/null +++ b/workflow-canvas/src/theme/install.ts @@ -0,0 +1,29 @@ +// SPDX-License-Identifier: Apache-2.0 +import { applyMode, Mode } from '@cloudscape-design/global-styles'; +import { applyTheme } from '@cloudscape-design/components/theming'; +import { + APP_THEME, + FONT_TEXT, + TAB_STRIP_THEME, + type ThemeMode, +} from './tokens'; + +/** Where the tab strip of the page header is, for `TAB_STRIP_THEME`. */ +export const TAB_STRIP_SELECTOR = '.wf-tab-strip [role="tablist"]'; + +/** + * Applies the konductor docs theme to the page. Call it once, before the + * first render; the theme has a value for each mode. Cloudscape sets fonts on + * its components only, so the page text outside them gets the font here. + */ +export function installTheme(body: HTMLElement): void { + body.style.fontFamily = FONT_TEXT; + applyTheme({ theme: APP_THEME }); + applyTheme({ theme: TAB_STRIP_THEME, selector: TAB_STRIP_SELECTOR }); +} + +/** Switches the page, its components and its native controls to `mode`. */ +export function showMode(mode: ThemeMode, root: HTMLElement): void { + applyMode(mode === 'dark' ? Mode.Dark : Mode.Light); + root.style.colorScheme = mode; +} diff --git a/workflow-canvas/src/theme/mode.test.ts b/workflow-canvas/src/theme/mode.test.ts new file mode 100644 index 00000000..2672b18d --- /dev/null +++ b/workflow-canvas/src/theme/mode.test.ts @@ -0,0 +1,89 @@ +// SPDX-License-Identifier: Apache-2.0 +import { describe, expect, it } from 'vitest'; +import { + initialMode, + MODE_STORAGE_KEY, + otherMode, + rememberMode, + type ModeEnvironment, +} from './mode'; + +function memoryStorage(initial: Record = {}) { + const items = new Map(Object.entries(initial)); + return { + getItem: (key: string) => items.get(key) ?? null, + setItem: (key: string, value: string) => void items.set(key, value), + }; +} + +const systemDark = (dark: boolean) => (query: string) => ({ + matches: dark && query === '(prefers-color-scheme: dark)', +}); + +describe('initialMode', () => { + it('uses the saved choice before the system setting', () => { + const env: ModeEnvironment = { + localStorage: memoryStorage({ [MODE_STORAGE_KEY]: 'light' }), + matchMedia: systemDark(true), + }; + + expect(initialMode(env)).toBe('light'); + }); + + it('follows the system setting when nothing was saved', () => { + const storage = memoryStorage(); + + expect( + initialMode({ localStorage: storage, matchMedia: systemDark(true) }), + ).toBe('dark'); + expect( + initialMode({ localStorage: storage, matchMedia: systemDark(false) }), + ).toBe('light'); + }); + + it('ignores a saved value that is not a mode', () => { + const env: ModeEnvironment = { + localStorage: memoryStorage({ [MODE_STORAGE_KEY]: 'sepia' }), + matchMedia: systemDark(true), + }; + + expect(initialMode(env)).toBe('dark'); + }); + + it('is light when the browser tells nothing', () => { + expect(initialMode({})).toBe('light'); + }); + + it('survives storage that throws', () => { + const blocked = { + getItem: () => { + throw new Error('blocked'); + }, + setItem: () => { + throw new Error('blocked'); + }, + }; + + expect( + initialMode({ localStorage: blocked, matchMedia: systemDark(true) }), + ).toBe('dark'); + expect(() => rememberMode({ localStorage: blocked }, 'dark')).not.toThrow(); + }); +}); + +describe('rememberMode', () => { + it('keeps the mode for the next visit', () => { + const env: ModeEnvironment = { localStorage: memoryStorage() }; + + rememberMode(env, 'dark'); + + expect(initialMode(env)).toBe('dark'); + }); +}); + +describe('otherMode', () => { + it('switches between the two modes', () => { + expect(otherMode('light')).toBe('dark'); + expect(otherMode('dark')).toBe('light'); + }); +}); diff --git a/workflow-canvas/src/theme/mode.ts b/workflow-canvas/src/theme/mode.ts new file mode 100644 index 00000000..23592863 --- /dev/null +++ b/workflow-canvas/src/theme/mode.ts @@ -0,0 +1,60 @@ +// SPDX-License-Identifier: Apache-2.0 +import type { ThemeMode } from './tokens'; + +/** The key under which the chosen mode is kept in local storage. */ +export const MODE_STORAGE_KEY = 'konductor-workflow-canvas-theme'; + +/** The parts of `window` that decide the mode. */ +export interface ModeEnvironment { + localStorage?: Pick; + matchMedia?: (query: string) => { matches: boolean }; +} + +function isMode(value: unknown): value is ThemeMode { + return value === 'light' || value === 'dark'; +} + +// Reading `localStorage` can throw when the browser blocks storage. +function storageOf(env: ModeEnvironment) { + try { + return env.localStorage; + } catch { + return undefined; + } +} + +function storedMode(env: ModeEnvironment): ThemeMode | undefined { + try { + const value = storageOf(env)?.getItem(MODE_STORAGE_KEY); + return isMode(value) ? value : undefined; + } catch { + return undefined; + } +} + +function systemMode(env: ModeEnvironment): ThemeMode { + const dark = env.matchMedia?.('(prefers-color-scheme: dark)').matches; + return dark ? 'dark' : 'light'; +} + +/** + * The mode to show on first paint: the one the person chose before, else the + * one the system asks for, else light. + */ +export function initialMode(env: ModeEnvironment): ThemeMode { + return storedMode(env) ?? systemMode(env); +} + +/** The mode that a toggle switches to. */ +export function otherMode(mode: ThemeMode): ThemeMode { + return mode === 'dark' ? 'light' : 'dark'; +} + +/** Keeps a chosen mode for the next visit. A blocked storage is ignored. */ +export function rememberMode(env: ModeEnvironment, mode: ThemeMode): void { + try { + storageOf(env)?.setItem(MODE_STORAGE_KEY, mode); + } catch { + // The choice then lasts until the page closes. + } +} diff --git a/workflow-canvas/src/theme/tokens.test.ts b/workflow-canvas/src/theme/tokens.test.ts new file mode 100644 index 00000000..5f192bda --- /dev/null +++ b/workflow-canvas/src/theme/tokens.test.ts @@ -0,0 +1,72 @@ +// SPDX-License-Identifier: Apache-2.0 +import { describe, expect, it } from 'vitest'; +import { AA_GRAPHIC, AA_TEXT, contrastRatio } from './contrast'; +import { APP_THEME, BAND_BACKGROUND, colorsOf, type ThemeMode } from './tokens'; + +const MODES: ThemeMode[] = ['light', 'dark']; + +describe.each(MODES)('the %s palette', (mode) => { + const c = colorsOf(mode); + + it.each([ + ['text on the page', c.text, c.bg], + ['text on a surface', c.text, c.surface], + ['muted text on a surface', c.textMuted, c.surface], + ['muted text on the page', c.textMuted, c.bg], + ['muted text on a raised area', c.textMuted, c.surfaceAlt], + ['links on a surface', c.link, c.surface], + ['hovered links on a surface', c.linkHover, c.surface], + ['white text on a filled button', '#ffffff', c.fill], + ['white text on a hovered filled button', '#ffffff', c.fillHover], + ['success text on a surface', c.success, c.surface], + ['error text on a surface', c.error, c.surface], + ['warning text on a surface', c.warning, c.surface], + ['success text on its wash', c.success, c.successBg], + ['error text on its wash', c.error, c.errorBg], + ['warning text on its wash', c.warning, c.warningBg], + ])('has AA contrast for %s', (_name, foreground, background) => { + expect(contrastRatio(foreground, background)).toBeGreaterThanOrEqual( + AA_TEXT, + ); + }); + + it('draws control edges that reach 3:1 on the surface and the field', () => { + expect(contrastRatio(c.control, c.surface)).toBeGreaterThanOrEqual( + AA_GRAPHIC, + ); + expect(contrastRatio(c.control, c.field)).toBeGreaterThanOrEqual( + AA_GRAPHIC, + ); + }); +}); + +describe('the header band', () => { + it.each([ + ['#f5f3ee', 'text'], + ['#c9c2e0', 'muted text'], + ['#c4b2ff', 'links'], + ])('has AA contrast for %s (%s)', (colour) => { + expect(contrastRatio(colour, BAND_BACKGROUND)).toBeGreaterThanOrEqual( + AA_TEXT, + ); + }); +}); + +describe('APP_THEME', () => { + it('gives the base font to the page', () => { + expect(APP_THEME.tokens.fontFamilyBase).toMatch(/^Outfit,/); + expect(APP_THEME.tokens.fontFamilyMonospace).toMatch(/^'JetBrains Mono',/); + }); + + it('gives every colour a value for both modes', () => { + const colourTokens = Object.entries(APP_THEME.tokens).filter( + ([name, value]) => name.startsWith('color') && typeof value === 'object', + ); + + expect(colourTokens.length).toBeGreaterThan(40); + for (const [, value] of colourTokens) { + expect(value).toHaveProperty('light'); + expect(value).toHaveProperty('dark'); + } + }); +}); diff --git a/workflow-canvas/src/theme/tokens.ts b/workflow-canvas/src/theme/tokens.ts new file mode 100644 index 00000000..623089b6 --- /dev/null +++ b/workflow-canvas/src/theme/tokens.ts @@ -0,0 +1,327 @@ +// SPDX-License-Identifier: Apache-2.0 +import type { Theme } from '@cloudscape-design/components/theming'; + +/** The two looks of the page. */ +export type ThemeMode = 'light' | 'dark'; + +/** The colours of one mode, named by what they are used for. */ +export interface Colors { + /** The page. */ + bg: string; + /** The background of text boxes and other form controls. */ + field: string; + /** Cards, panels and containers. */ + surface: string; + /** Raised or secondary areas inside a surface. */ + surfaceAlt: string; + /** Hairlines that divide areas. */ + border: string; + /** The edge of a popup or a panel. */ + borderStrong: string; + /** The edge of a control. It must reach 3:1 on the surface. */ + control: string; + text: string; + textMuted: string; + /** Filled buttons and the selected segment. White text is set on it. */ + fill: string; + fillHover: string; + fillActive: string; + /** Links and outlined buttons: text and edge in one colour. */ + link: string; + linkHover: string; + success: string; + error: string; + warning: string; + /** The wash on a hovered or selected row, translucent. */ + tint: string; + tintStrong: string; + successBg: string; + errorBg: string; + warningBg: string; + infoBg: string; + overlay: string; +} + +/** Light mode: the paper tones of the konductor docs theme. */ +export const LIGHT: Colors = { + bg: '#f5f3ee', + field: '#ffffff', + surface: '#ffffff', + surfaceAlt: '#faf8f4', + border: '#e0dad0', + borderStrong: '#cfc7ba', + control: '#7a7490', + text: '#16131f', + textMuted: '#56516a', + fill: '#6236f0', + fillHover: '#5128db', + fillActive: '#4f26c9', + link: '#6236f0', + linkHover: '#4f26c9', + success: '#0b7a52', + error: '#b3261e', + warning: '#8a5a00', + tint: 'rgba(98, 54, 240, 0.08)', + tintStrong: 'rgba(98, 54, 240, 0.16)', + successBg: '#e7f5ee', + errorBg: '#fdecec', + warningBg: '#fff4db', + infoBg: '#efebff', + overlay: 'rgba(22, 19, 31, 0.45)', +}; + +/** Dark mode: the night tones of the konductor docs theme. */ +export const DARK: Colors = { + bg: '#0b0a12', + field: '#100e1a', + surface: '#171331', + surfaceAlt: '#1c1740', + border: '#2a2450', + borderStrong: '#3a3168', + control: '#8f86ad', + text: '#f5f3ee', + textMuted: '#9c93b8', + fill: '#6b4bef', + fillHover: '#5b3bdb', + fillActive: '#4f2fc8', + link: '#a78bfa', + linkHover: '#c4b2ff', + success: '#3ddc97', + error: '#ff8b8b', + warning: '#f5b454', + tint: 'rgba(124, 92, 255, 0.14)', + tintStrong: 'rgba(124, 92, 255, 0.28)', + successBg: '#10281f', + errorBg: '#2b1722', + warningBg: '#2b2314', + infoBg: '#1f1a44', + overlay: 'rgba(11, 10, 18, 0.72)', +}; + +/** The colours of a mode. */ +export function colorsOf(mode: ThemeMode): Colors { + return mode === 'dark' ? DARK : LIGHT; +} + +/** + * The header band and the top bar are dark in both modes. These are the + * colours of the text and controls on them; each pair reaches 4.5:1 on + * `BAND_BACKGROUND`. + */ +export const BAND_BACKGROUND = '#141026'; +const BAND_TEXT = '#f5f3ee'; +const BAND_TEXT_MUTED = '#c9c2e0'; +const BAND_LINK = '#c4b2ff'; + +/** The font of the page text. Self-hosted by `fonts.ts`. */ +export const FONT_TEXT = + "Outfit, system-ui, -apple-system, 'Segoe UI', Roboto, Arial, sans-serif"; +/** The font of code and small labels. Self-hosted by `fonts.ts`. */ +export const FONT_CODE = + "'JetBrains Mono', ui-monospace, Menlo, Consolas, monospace"; + +/** The corner radius of cards and panels, of controls, and of small parts. */ +export const RADIUS = { card: '12px', control: '10px', small: '8px' } as const; + +/** The logo of the top bar: a violet square with a white K. */ +export const LOGO_DATA_URI = `data:image/svg+xml;utf8,${encodeURIComponent( + '' + + '' + + '' + + '' + + '', +)}`; + +type Tokens = Theme['tokens']; + +/** A token that has one value in light mode and one in dark mode. */ +function perMode(pick: (colors: Colors) => string) { + return { light: pick(LIGHT), dark: pick(DARK) }; +} + +function backgroundTokens(): Tokens { + return { + colorBackgroundLayoutMain: perMode((c) => c.bg), + colorBackgroundContainerContent: perMode((c) => c.surface), + colorBackgroundContainerHeader: perMode((c) => c.surface), + colorBackgroundLayoutPanelContent: perMode((c) => c.surface), + colorBackgroundPopover: perMode((c) => c.surfaceAlt), + colorBackgroundDropdownItemDefault: perMode((c) => c.surfaceAlt), + colorBackgroundDropdownItemHover: perMode((c) => c.tint), + colorBackgroundDropdownItemSelected: perMode((c) => c.tintStrong), + colorBackgroundInputDefault: perMode((c) => c.field), + colorBackgroundControlDefault: perMode((c) => c.field), + colorBackgroundControlChecked: perMode((c) => c.fill), + colorBackgroundItemSelected: perMode((c) => c.tintStrong), + colorBackgroundCellShaded: perMode((c) => c.surfaceAlt), + colorBackgroundToggleDefault: perMode((c) => c.control), + colorBackgroundHomeHeader: BAND_BACKGROUND, + colorBackgroundStatusError: perMode((c) => c.errorBg), + colorBackgroundStatusInfo: perMode((c) => c.infoBg), + colorBackgroundStatusSuccess: perMode((c) => c.successBg), + colorBackgroundStatusWarning: perMode((c) => c.warningBg), + }; +} + +function buttonTokens(): Tokens { + return { + colorBackgroundButtonNormalDefault: 'transparent', + colorBackgroundButtonNormalHover: perMode((c) => c.tint), + colorBackgroundButtonNormalActive: perMode((c) => c.tintStrong), + colorBackgroundButtonPrimaryDefault: perMode((c) => c.fill), + colorBackgroundButtonPrimaryHover: perMode((c) => c.fillHover), + colorBackgroundButtonPrimaryActive: perMode((c) => c.fillActive), + colorBackgroundButtonLinkHover: perMode((c) => c.tint), + colorBackgroundButtonLinkActive: perMode((c) => c.tintStrong), + colorTextButtonNormalDefault: perMode((c) => c.link), + colorTextButtonNormalHover: perMode((c) => c.linkHover), + colorTextButtonNormalActive: perMode((c) => c.linkHover), + colorTextButtonPrimaryDefault: '#ffffff', + colorTextButtonPrimaryHover: '#ffffff', + colorTextButtonPrimaryActive: '#ffffff', + colorTextButtonInlineIconDefault: perMode((c) => c.link), + colorTextButtonInlineIconHover: perMode((c) => c.linkHover), + colorBorderButtonNormalDefault: perMode((c) => c.link), + colorBorderButtonNormalHover: perMode((c) => c.linkHover), + colorBorderButtonNormalActive: perMode((c) => c.linkHover), + }; +} + +function textTokens(): Tokens { + return { + colorTextBodyDefault: perMode((c) => c.text), + colorTextBodySecondary: perMode((c) => c.textMuted), + colorTextHeadingDefault: perMode((c) => c.text), + colorTextHeadingSecondary: perMode((c) => c.textMuted), + colorTextLabel: perMode((c) => c.text), + colorTextFormLabel: perMode((c) => c.text), + colorTextFormDefault: perMode((c) => c.text), + colorTextFormSecondary: perMode((c) => c.textMuted), + colorTextEmpty: perMode((c) => c.textMuted), + colorTextAccent: perMode((c) => c.link), + colorTextLinkDefault: perMode((c) => c.link), + colorTextLinkHover: perMode((c) => c.linkHover), + colorTextInteractiveDefault: perMode((c) => c.textMuted), + colorTextInteractiveHover: perMode((c) => c.text), + colorTextInteractiveActive: perMode((c) => c.text), + colorTextInputPlaceholder: perMode((c) => c.textMuted), + colorTextStatusError: perMode((c) => c.error), + colorTextStatusInfo: perMode((c) => c.link), + colorTextStatusSuccess: perMode((c) => c.success), + colorTextStatusWarning: perMode((c) => c.warning), + colorTextStatusInactive: perMode((c) => c.textMuted), + colorTextDropdownItemDefault: perMode((c) => c.text), + colorTextDropdownItemHighlighted: perMode((c) => c.text), + colorTextDropdownItemSecondary: perMode((c) => c.textMuted), + colorTextGroupLabel: perMode((c) => c.textMuted), + colorTextHomeHeaderDefault: BAND_TEXT, + colorTextHomeHeaderSecondary: BAND_TEXT_MUTED, + colorTextBreadcrumbCurrent: perMode((c) => c.textMuted), + colorTextBreadcrumbIcon: perMode((c) => c.textMuted), + }; +} + +function borderTokens(): Tokens { + return { + colorBorderDividerDefault: perMode((c) => c.border), + colorBorderDividerSecondary: perMode((c) => c.border), + colorBorderContainerTop: perMode((c) => c.border), + colorBorderLayout: perMode((c) => c.border), + colorBorderInputDefault: perMode((c) => c.control), + colorBorderInputFocused: perMode((c) => c.link), + colorBorderControlDefault: perMode((c) => c.control), + colorBorderDropdownContainer: perMode((c) => c.borderStrong), + colorBorderDropdownItemFocused: perMode((c) => c.link), + colorBorderItemFocused: perMode((c) => c.link), + colorBorderItemSelected: perMode((c) => c.link), + colorBorderPopover: perMode((c) => c.borderStrong), + colorBorderStatusError: perMode((c) => c.error), + colorBorderStatusInfo: perMode((c) => c.link), + colorBorderStatusSuccess: perMode((c) => c.success), + colorBorderStatusWarning: perMode((c) => c.warning), + }; +} + +function shapeTokens(): Tokens { + const { card, control, small } = RADIUS; + return { + borderRadiusButton: control, + borderRadiusInput: control, + borderRadiusContainer: card, + borderRadiusDropdown: card, + borderRadiusPopover: card, + borderRadiusAlert: card, + borderRadiusFlashbar: card, + borderRadiusBadge: small, + borderRadiusToken: small, + borderRadiusItem: small, + borderRadiusTabsFocusRing: small, + fontFamilyBase: FONT_TEXT, + fontFamilyHeading: FONT_TEXT, + fontFamilyDisplay: FONT_TEXT, + fontFamilyMonospace: FONT_CODE, + }; +} + +/** + * Inside the dark band the global text and border colours do not apply, so + * the same roles are set again here. Cloudscape reads these in the page + * header, which holds the breadcrumbs and the title. + */ +function headerBandTokens(): Tokens { + return { + colorTextAccent: BAND_LINK, + colorTextLinkDefault: BAND_LINK, + colorTextLinkHover: '#ffffff', + colorTextBodySecondary: BAND_TEXT_MUTED, + colorTextBreadcrumbCurrent: BAND_TEXT_MUTED, + colorTextBreadcrumbIcon: BAND_TEXT_MUTED, + colorTextInteractiveDefault: BAND_TEXT_MUTED, + colorTextInteractiveHover: '#ffffff', + colorTextInteractiveActive: '#ffffff', + colorBorderItemFocused: BAND_LINK, + }; +} + +/** + * The Cloudscape theme of the page, for `applyTheme`: colours, fonts and + * corner radii, with a value for each mode. `applyMode` picks the mode. + */ +export const APP_THEME: Theme = { + tokens: { + ...backgroundTokens(), + ...buttonTokens(), + ...textTokens(), + ...borderTokens(), + ...shapeTokens(), + }, + contexts: { + header: { + tokens: { + ...headerBandTokens(), + colorBackgroundLayoutMain: BAND_BACKGROUND, + }, + }, + 'top-navigation': { + tokens: { + ...headerBandTokens(), + colorBackgroundContainerContent: BAND_BACKGROUND, + colorTextHeadingDefault: BAND_TEXT, + colorTextTopNavigationTitle: BAND_TEXT, + colorTextInteractiveDefault: BAND_TEXT, + colorTextInteractiveHover: '#ffffff', + colorBorderDividerDefault: '#2a2450', + }, + }, + }, +}; + +/** + * The same roles for the Designer and Execution tabs, which sit on the band + * but are page content, so the header context does not reach them. + */ +export const TAB_STRIP_THEME: Theme = { + tokens: { + ...headerBandTokens(), + }, +}; diff --git a/workflow-canvas/src/theme/use-theme-mode.ts b/workflow-canvas/src/theme/use-theme-mode.ts new file mode 100644 index 00000000..8b527e85 --- /dev/null +++ b/workflow-canvas/src/theme/use-theme-mode.ts @@ -0,0 +1,27 @@ +// SPDX-License-Identifier: Apache-2.0 +import { useCallback, useLayoutEffect, useState } from 'react'; +import { showMode } from './install'; +import { initialMode, otherMode, rememberMode } from './mode'; +import type { ThemeMode } from './tokens'; + +/** + * The mode of the page and a function that switches it. The first mode is + * the saved choice or the system setting. Only a switch is saved, so a page + * that was never switched keeps following the system. + */ +export function useThemeMode(): [ThemeMode, () => void] { + const [mode, setMode] = useState(() => initialMode(window)); + + // Before paint, so the first frame is already in the right mode. + useLayoutEffect(() => { + showMode(mode, document.documentElement); + }, [mode]); + + const toggle = useCallback(() => { + const next = otherMode(mode); + rememberMode(window, next); + setMode(next); + }, [mode]); + + return [mode, toggle]; +} From 340ce97ab60cb8ffc9ae0ccca804accb190233fe Mon Sep 17 00:00:00 2001 From: Nihit <24500178+knihit@users.noreply.github.com> Date: Sun, 25 Oct 2026 17:55:30 +0000 Subject: [PATCH 002/476] Give the page a top bar, a header band and themed canvas nodes and ports --- workflow-canvas/src/App.test.tsx | 71 ++++++++- workflow-canvas/src/App.tsx | 66 +++++--- workflow-canvas/src/canvas/canvas.css | 149 ++++++++++++++---- workflow-canvas/src/canvas/nodes.tsx | 42 ++++- workflow-canvas/src/chrome/Breadcrumbs.tsx | 18 +++ workflow-canvas/src/chrome/TopBar.tsx | 40 +++++ workflow-canvas/src/main.tsx | 4 + workflow-canvas/src/workflow/load-workflow.ts | 7 + .../test-support/canvas-contrast.test.ts | 63 ++++++++ 9 files changed, 403 insertions(+), 57 deletions(-) create mode 100644 workflow-canvas/src/chrome/Breadcrumbs.tsx create mode 100644 workflow-canvas/src/chrome/TopBar.tsx create mode 100644 workflow-canvas/test-support/canvas-contrast.test.ts diff --git a/workflow-canvas/src/App.test.tsx b/workflow-canvas/src/App.test.tsx index 9e6d88f5..1f151f3c 100644 --- a/workflow-canvas/src/App.test.tsx +++ b/workflow-canvas/src/App.test.tsx @@ -1,12 +1,28 @@ // SPDX-License-Identifier: Apache-2.0 import { fireEvent, render, screen, within } from '@testing-library/react'; -import { describe, expect, it } from 'vitest'; +import { afterEach, describe, expect, it } from 'vitest'; +import { MODE_STORAGE_KEY } from './theme/mode'; import { App } from './App'; import { HELLO_DOCUMENT, failed, loaded } from './test-support/fixtures'; import type { LoadResult } from './workflow/load-workflow'; const settled = (result: LoadResult) => Promise.resolve(result); +// The top bar measures itself, and jsdom has no layout, so it shows its +// buttons in the overflow menu. The measuring copy is hidden from the +// accessibility tree, but it is the same button with the same handler. +const themeButton = (mode: 'light' | 'dark') => + screen.getAllByRole('button', { + name: new RegExp(`^Theme: ${mode}`), + hidden: true, + })[0]!; + +afterEach(() => { + window.localStorage.clear(); + document.body.classList.remove('awsui-dark-mode'); + document.documentElement.style.colorScheme = ''; +}); + describe('App', () => { it('renders the page title inside a main landmark', () => { render(); @@ -83,4 +99,57 @@ describe('App', () => { expect(screen.getByTestId('node-end')).toBeTruthy(); expect(screen.queryByText('Loading the workflow')).toBeNull(); }); + + it('shows the top bar with the page name and a theme button', () => { + render(); + + expect(themeButton('light').textContent).toBe('Theme: light'); + expect(themeButton('light').getAttribute('aria-label')).toBe( + 'Theme: light. Switch to dark', + ); + }); + + it('shows the breadcrumb Workflows > the open workflow', async () => { + render(); + + const trail = await screen.findByRole('navigation', { + name: 'Breadcrumbs', + }); + expect(within(trail).getAllByText('Workflows').length).toBeGreaterThan(0); + // The trail holds a hidden copy that it measures, so the name appears twice. + expect((await within(trail).findAllByText('hello')).length).toBeGreaterThan( + 0, + ); + }); + + it('switches to the dark mode, remembers it and switches back', () => { + render(); + + fireEvent.click(themeButton('light')); + + expect(document.body.classList.contains('awsui-dark-mode')).toBe(true); + expect(document.documentElement.style.colorScheme).toBe('dark'); + expect(window.localStorage.getItem(MODE_STORAGE_KEY)).toBe('dark'); + expect(themeButton('dark').textContent).toBe('Theme: dark'); + + fireEvent.click(themeButton('dark')); + + expect(document.body.classList.contains('awsui-dark-mode')).toBe(false); + expect(window.localStorage.getItem(MODE_STORAGE_KEY)).toBe('light'); + }); + + it('starts in the mode that was saved', () => { + window.localStorage.setItem(MODE_STORAGE_KEY, 'dark'); + + render(); + + expect(document.body.classList.contains('awsui-dark-mode')).toBe(true); + expect(themeButton('dark')).toBeTruthy(); + }); + + it('does not save a mode that nobody chose', () => { + render(); + + expect(window.localStorage.getItem(MODE_STORAGE_KEY)).toBeNull(); + }); }); diff --git a/workflow-canvas/src/App.tsx b/workflow-canvas/src/App.tsx index 481692cb..274f0003 100644 --- a/workflow-canvas/src/App.tsx +++ b/workflow-canvas/src/App.tsx @@ -4,39 +4,59 @@ import Box from '@cloudscape-design/components/box'; import ContentLayout from '@cloudscape-design/components/content-layout'; import Header from '@cloudscape-design/components/header'; import Tabs from '@cloudscape-design/components/tabs'; +import { Breadcrumbs } from './chrome/Breadcrumbs'; +import { TOP_BAR_ID, TopBar } from './chrome/TopBar'; import { Designer } from './designer/Designer'; import { useSettled } from './designer/use-settled'; -import type { LoadResult } from './workflow/load-workflow'; +import { useThemeMode } from './theme/use-theme-mode'; +import { openWorkflowName, type LoadResult } from './workflow/load-workflow'; const placeholder = Nothing here yet.; /** - * Root component: the page title and the Designer and Execution tabs. The - * Designer draws the workflow that `load` settles with; the Execution tab - * shows a placeholder until its content is built. + * Root component: the top bar, then the page header (breadcrumbs and title) + * with the Designer and Execution tabs. The Designer draws the workflow that + * `load` settles with; the Execution tab shows a placeholder until its + * content is built. */ export function App({ load }: { load: Promise }) { // Read here, not in the tab, so switching tabs does not load again. const result = useSettled(load); + const [mode, toggleMode] = useThemeMode(); return ( - Workflow canvas}> - , - }, - { id: 'execution', label: 'Execution', content: placeholder }, - ]} - /> - - } - /> + <> + + + } + header={
Workflow canvas
} + > +
+ , + }, + { id: 'execution', label: 'Execution', content: placeholder }, + ]} + /> +
+ + } + /> + ); } diff --git a/workflow-canvas/src/canvas/canvas.css b/workflow-canvas/src/canvas/canvas.css index 9f1fa66c..7b5e682a 100644 --- a/workflow-canvas/src/canvas/canvas.css +++ b/workflow-canvas/src/canvas/canvas.css @@ -1,21 +1,57 @@ /* SPDX-License-Identifier: Apache-2.0 */ -/* Colours of the light theme for the parts that the canvas draws itself. */ +/* + * The colours of the parts that the canvas draws itself, for the light and the + * dark mode. The dark values are set when Cloudscape puts its dark mode class + * on the page. A test (canvas-contrast.test.ts) reads this file and checks + * that the text and the edges reach WCAG AA in both modes. + */ .wf-canvas { --wf-canvas-bg: #f5f3ee; --wf-canvas-dots: #c9c1b3; --wf-edge: #5f6b7a; - --wf-focus: #0b57d0; + --wf-focus: #6236f0; --wf-node-bg: #ffffff; --wf-node-border: #7a7490; --wf-text: #16131f; --wf-text-muted: #56516a; - --wf-port-bg: #414d5c; - --wf-port-text: #ffffff; - --wf-port-ring: #16131f; + --wf-port-rim: #16191f; + --wf-port-fill: #5f6b7a; + --wf-port-letter-out: #ffffff; + --wf-port-inner: #ffffff; + --wf-port-ring: #5f6b7a; + --wf-port-letter-in: #414d5c; + --wf-controls-bg: #ffffff; + --wf-controls-bg-hover: #efebff; + --wf-controls-border: #cfc7ba; border-radius: 12px; background: var(--wf-canvas-bg); + /* The graph library reads these for its zoom buttons and its credit line. */ + --xy-controls-button-background-color: var(--wf-controls-bg); + --xy-controls-button-background-color-hover: var(--wf-controls-bg-hover); + --xy-controls-button-color: var(--wf-text); + --xy-controls-button-color-hover: var(--wf-text); + --xy-controls-button-border-color: var(--wf-controls-border); + --xy-attribution-background-color: var(--wf-controls-bg); +} + +.awsui-dark-mode .wf-canvas { + --wf-canvas-bg: #0b0a12; + --wf-canvas-dots: #3a3168; + --wf-edge: #a39bc0; + --wf-focus: #a78bfa; + --wf-node-bg: #171331; + --wf-node-border: #8f86ad; + --wf-text: #f5f3ee; + --wf-text-muted: #b2a9cc; + --wf-controls-bg: #171331; + --wf-controls-bg-hover: #1f1a44; + --wf-controls-border: #3a3168; +} + +.wf-canvas .react-flow__attribution a { + color: var(--wf-text-muted); } /* The ring that shows which node the keyboard is on. */ @@ -31,7 +67,7 @@ height: 100%; padding: 10px 12px; border: 2px solid var(--wf-node-border); - border-radius: 12px; + border-radius: 10px; background: var(--wf-node-bg); color: var(--wf-text); font-size: 12px; @@ -46,6 +82,11 @@ white-space: nowrap; } +/* Keeps a long name clear of the run order notch at the top right. */ +.wf-node-stage .wf-node-title { + padding-right: 32px; +} + .wf-node-sub { overflow: hidden; color: var(--wf-text-muted); @@ -67,40 +108,92 @@ display: flex; align-items: center; justify-content: center; + border-radius: 22px; } -/* A run order port: a small square with an S, at the right of the node. */ +/* + * A run order port is a piece of a jigsaw: the S out tab hangs below the card, + * the S in notch is cut into the card at the top, both at the right. They have + * the same outline, so a tab fits a notch. The outline is an SVG; the tab is + * filled by clipping the letter box to the same outline. + */ .react-flow .wf-port { + display: block; + width: 26px; + height: 16px; + min-width: 0; + min-height: 0; + padding: 0; + border: none; + border-radius: 0; + background: none; + overflow: visible; + pointer-events: none; + right: 16px; + left: auto; + transform: translate(50%, 0); +} + +.react-flow .wf-port-in { + top: 0; + bottom: auto; +} + +.react-flow .wf-port-out { + top: 100%; + bottom: auto; +} + +.wf-port-svg { + position: absolute; + top: 0; + left: 0; + width: 26px; + height: 16px; + overflow: visible; +} + +.wf-port-rim { + fill: var(--wf-port-rim); + stroke: var(--wf-port-rim); + stroke-linejoin: round; + stroke-width: 3.6; +} + +.wf-port-inner { + fill: var(--wf-port-inner); +} + +.wf-port-ring { + fill: none; + stroke: var(--wf-port-ring); + stroke-linejoin: round; + stroke-width: 2.4; +} + +.wf-port-letter { + position: relative; display: flex; align-items: center; justify-content: center; - width: 18px; - height: 18px; - min-width: 0; - min-height: 0; - border: 2px solid var(--wf-port-ring); - border-radius: 4px; - background: var(--wf-port-bg); - color: var(--wf-port-text); - /* Inherit the page font. Naming a system family here (Arial) would resolve - to a different face per host and make the screenshots differ. */ + box-sizing: border-box; + width: 26px; + height: 16px; + padding-top: 4px; + /* The pinned page font, not a system family, so the letter looks the same + on every machine. */ font-family: inherit; font-size: 10px; font-weight: 700; line-height: 1; - pointer-events: none; } -.react-flow .wf-port-in { - top: 0; - right: 16px; - left: auto; - transform: translate(50%, -50%); +.wf-port-out .wf-port-letter { + background: var(--wf-port-fill); + clip-path: path('M9.5 0V4H7V16H19V4H16.5V0Z'); + color: var(--wf-port-letter-out); } -.react-flow .wf-port-out { - right: 16px; - bottom: 0; - left: auto; - transform: translate(50%, 50%); +.wf-port-in .wf-port-letter { + color: var(--wf-port-letter-in); } diff --git a/workflow-canvas/src/canvas/nodes.tsx b/workflow-canvas/src/canvas/nodes.tsx index ad32c461..4da52210 100644 --- a/workflow-canvas/src/canvas/nodes.tsx +++ b/workflow-canvas/src/canvas/nodes.tsx @@ -12,7 +12,36 @@ type FlowNode }> = Node< N['type'] >; -// The port where run order arrives, at the top right of a node. +// The outline of a run order port, as a tab seen from the card and as a notch. +// A tab and a notch share it, so one fits the other. +const PORT_OUTLINE = 'M9.5 0V4H7V16H19V4H16.5V0Z'; +const PORT_OPENING = 'M9.5 0V4H7V16H19V4H16.5V0'; + +function PortShape({ kind }: { kind: 'tab' | 'notch' }) { + return ( + <> + + + + ); +} + +// The port where run order arrives: a notch at the top right of a node. function SequenceIn() { return ( - + ); } -// The port where run order leaves, at the bottom right of a node. +// The port where run order leaves: a tab at the bottom right of a node. function SequenceOut() { return ( - + ); } @@ -56,7 +85,10 @@ export function IntentNode({ data }: NodeProps>) { /** One stage: its name, then its agent and harness, with run order in and out. */ export function StageNode({ data }: NodeProps>) { return ( -
+
{data.name}
diff --git a/workflow-canvas/src/chrome/Breadcrumbs.tsx b/workflow-canvas/src/chrome/Breadcrumbs.tsx new file mode 100644 index 00000000..55fcaceb --- /dev/null +++ b/workflow-canvas/src/chrome/Breadcrumbs.tsx @@ -0,0 +1,18 @@ +// SPDX-License-Identifier: Apache-2.0 +import BreadcrumbGroup from '@cloudscape-design/components/breadcrumb-group'; + +/** + * The trail above the title: Workflows, then the workflow that is open. The + * page has no list of workflows yet, so both stay on this page. + */ +export function Breadcrumbs({ workflowName }: { workflowName?: string }) { + const items = [{ text: 'Workflows', href: '#' }]; + if (workflowName) items.push({ text: workflowName, href: '#' }); + return ( + event.preventDefault()} + /> + ); +} diff --git a/workflow-canvas/src/chrome/TopBar.tsx b/workflow-canvas/src/chrome/TopBar.tsx new file mode 100644 index 00000000..3cd89156 --- /dev/null +++ b/workflow-canvas/src/chrome/TopBar.tsx @@ -0,0 +1,40 @@ +// SPDX-License-Identifier: Apache-2.0 +import TopNavigation from '@cloudscape-design/components/top-navigation'; +import { otherMode } from '../theme/mode'; +import { LOGO_DATA_URI, type ThemeMode } from '../theme/tokens'; + +/** The id of the top bar, which the page layout reads to sit below it. */ +export const TOP_BAR_ID = 'wf-top-bar'; + +/** + * The bar across the top: the logo and the page name, and the button that + * switches between the light and the dark mode. The accessible name of the + * button starts with its visible text. + */ +export function TopBar({ + mode, + onToggle, +}: { + mode: ThemeMode; + onToggle: () => void; +}) { + return ( +
+ +
+ ); +} diff --git a/workflow-canvas/src/main.tsx b/workflow-canvas/src/main.tsx index 5786fdd7..198e22c6 100644 --- a/workflow-canvas/src/main.tsx +++ b/workflow-canvas/src/main.tsx @@ -1,15 +1,19 @@ // SPDX-License-Identifier: Apache-2.0 import '@cloudscape-design/global-styles/index.css'; +import './theme/fonts'; import { StrictMode } from 'react'; import { createRoot } from 'react-dom/client'; import { App } from './App'; import { createHttpApi } from './api/http-api'; import { reloadOnLaunchCode, takeLaunchCode } from './launch/launch-code'; +import { installTheme } from './theme/install'; import { loadWorkflow } from './workflow/load-workflow'; const root = document.getElementById('root'); if (!root) throw new Error('Missing #root element'); +installTheme(document.body); + // Started once, outside React: the launch code works only once, and strict mode // runs effects twice. const load = loadWorkflow(createHttpApi(), takeLaunchCode(window)); diff --git a/workflow-canvas/src/workflow/load-workflow.ts b/workflow-canvas/src/workflow/load-workflow.ts index 16a746fe..f18d38af 100644 --- a/workflow-canvas/src/workflow/load-workflow.ts +++ b/workflow-canvas/src/workflow/load-workflow.ts @@ -45,3 +45,10 @@ export async function loadWorkflow( return { ok: false, message: messageOf(error) }; } } + +/** The name of the workflow that a load drew, or `undefined` if none is open. */ +export function openWorkflowName( + result: LoadResult | undefined, +): string | undefined { + return result?.ok ? result.workflow.document.view?.name : undefined; +} diff --git a/workflow-canvas/test-support/canvas-contrast.test.ts b/workflow-canvas/test-support/canvas-contrast.test.ts new file mode 100644 index 00000000..a8fddfb0 --- /dev/null +++ b/workflow-canvas/test-support/canvas-contrast.test.ts @@ -0,0 +1,63 @@ +// SPDX-License-Identifier: Apache-2.0 +import { readFileSync } from 'node:fs'; +import { describe, expect, it } from 'vitest'; +import { AA_GRAPHIC, AA_TEXT, contrastRatio } from '../src/theme/contrast.ts'; + +const css = readFileSync( + new URL('../src/canvas/canvas.css', import.meta.url), + 'utf8', +); + +describe('canvas.css colours', () => { + // The declarations of the rule that starts with `selector`. + function variables(selector: string): Record { + const block = css.split(`${selector} {`)[1]?.split('}')[0] ?? ''; + return Object.fromEntries( + [...block.matchAll(/(--[\w-]+):\s*(#[0-9a-f]{6})/g)].map((m) => [ + m[1], + m[2], + ]), + ); + } + + const light = variables('.wf-canvas'); + const dark = { ...light, ...variables('.awsui-dark-mode .wf-canvas') }; + + it.each([ + ['light', light], + ['dark', dark], + ])('keeps the canvas text readable in %s', (_mode, v) => { + expect( + contrastRatio(v['--wf-text']!, v['--wf-node-bg']!), + ).toBeGreaterThanOrEqual(AA_TEXT); + expect( + contrastRatio(v['--wf-text-muted']!, v['--wf-node-bg']!), + ).toBeGreaterThanOrEqual(AA_TEXT); + expect( + contrastRatio(v['--wf-text-muted']!, v['--wf-canvas-bg']!), + ).toBeGreaterThanOrEqual(AA_TEXT); + expect( + contrastRatio(v['--wf-text']!, v['--wf-controls-bg']!), + ).toBeGreaterThanOrEqual(AA_TEXT); + }); + + it.each([ + ['light', light], + ['dark', dark], + ])('keeps nodes, edges and the focus ring visible in %s', (_mode, v) => { + for (const name of ['--wf-node-border', '--wf-edge', '--wf-focus']) { + expect( + contrastRatio(v[name]!, v['--wf-canvas-bg']!), + ).toBeGreaterThanOrEqual(AA_GRAPHIC); + } + }); + + it('keeps the port letters readable on their ports', () => { + expect( + contrastRatio(light['--wf-port-letter-out']!, light['--wf-port-fill']!), + ).toBeGreaterThanOrEqual(AA_TEXT); + expect( + contrastRatio(light['--wf-port-letter-in']!, light['--wf-port-inner']!), + ).toBeGreaterThanOrEqual(AA_TEXT); + }); +}); From 8a43f7ee1e5bb3ec555dc4c64960d2675557a3b0 Mon Sep 17 00:00:00 2001 From: Nihit <24500178+knihit@users.noreply.github.com> Date: Sun, 25 Oct 2026 17:55:30 +0000 Subject: [PATCH 003/476] Cover the theme in e2e tests, add a dark screenshot, document the theme --- workflow-canvas/README.md | 51 ++++++- .../e2e/capture/screenshots.spec.ts | 8 + .../e2e/screenshots/designer-tab-dark.png | Bin 0 -> 46734 bytes .../e2e/screenshots/designer-tab.png | Bin 41157 -> 46257 bytes .../e2e/screenshots/execution-tab.png | Bin 14778 -> 20943 bytes workflow-canvas/e2e/support/screenshots.ts | 39 ++--- .../e2e/support/workflow-canvas-page.ts | 21 +++ workflow-canvas/e2e/tests/theme.spec.ts | 139 ++++++++++++++++++ 8 files changed, 228 insertions(+), 30 deletions(-) create mode 100644 workflow-canvas/e2e/screenshots/designer-tab-dark.png create mode 100644 workflow-canvas/e2e/tests/theme.spec.ts diff --git a/workflow-canvas/README.md b/workflow-canvas/README.md index dd9e4ce7..5d79a270 100644 --- a/workflow-canvas/README.md +++ b/workflow-canvas/README.md @@ -235,6 +235,42 @@ The rest wraps the core, so the rules can be tested without a browser: text is small; zoom in with the controls, or Tab to a node to bring it into view. +## Theme + +The page has the look of the Konductor docs theme: its colours, fonts and +corner radii, in a light and a dark mode. + +- `src/theme/tokens.ts` holds every colour, font and radius of the theme, as + two palettes and the Cloudscape token map built from them. Nothing else + names a colour of Cloudscape. The colours of the canvas itself are in + `src/canvas/canvas.css`, with their dark values under `.awsui-dark-mode`. +- `src/theme/install.ts` applies the token map with `applyTheme` from + `@cloudscape-design/components/theming` (colours, fonts and radii only, + with a value for each mode) and switches the mode with `applyMode` from + `@cloudscape-design/global-styles`. The header band and the top bar are dark + in both modes, so their text colours are set for the band in `tokens.ts`. +- `src/theme/fonts.ts` loads Outfit and JetBrains Mono from the pinned + `@fontsource/outfit` and `@fontsource/jetbrains-mono` packages (SIL Open + Font License 1.1). The Latin files are bundled, so the page makes no request + to a font site. +- The top bar (`src/chrome/TopBar.tsx`) has the K logo, the page name and the + **Theme: light** / **Theme: dark** button. The first visit follows the + system setting (`prefers-color-scheme`). Pressing the button saves the mode + in local storage (`src/theme/mode.ts`), and the saved mode wins from then + on. A page that was never switched keeps following the system. +- The page header is a dark band with the breadcrumb `Workflows > ` + (the page has no list of workflows yet, so the first link stays on the + page), the title and the Designer and Execution tabs. +- Nodes are cards with the corner radius of the theme. The `S` run-order + ports are pieces of a jigsaw: the out port is a square tab under the card, + the in port a square notch in the top edge, both at the right, so a tab + fits a notch. Links are rails with an arrowhead. + +Text meets WCAG 2 AA in both modes. `src/theme/tokens.test.ts` and +`test-support/canvas-contrast.test.ts` check the contrast of each text and edge +colour pair, and the end-to-end tests run axe (which includes colour +contrast) in both modes. + ## End-to-end tests The tests in `e2e/tests/` drive the production build in headless Chromium. @@ -260,7 +296,7 @@ npx playwright install chromium Then `npm run test:e2e`. The HTML report is written to `playwright-report/`; open it with `npx playwright show-report`. Accessibility is checked with -axe-core on each tab and on the error state. +axe-core on each tab, on the error state and in both theme modes. Shared page objects and helpers live in `e2e/support/`. New tests should extend them rather than repeat selectors: import `test` and `expect` from @@ -270,18 +306,19 @@ the app. ## Screenshots `npm run screenshots` saves a full-page PNG of each tab to -`e2e/screenshots/`, with the Designer tab drawn from the stub, in the light -theme at 1280x800 with animations frozen. Pull request descriptions embed these +`e2e/screenshots/`, with the Designer tab drawn from the stub, at 1280x800 with +animations frozen. The tabs are in the light theme (`designer-tab.png`, +`execution-tab.png`), and `designer-tab-dark.png` is the Designer tab after +the theme button is pressed. Pull request descriptions embed these files, so regenerate and commit them when the UI changes. Only that script writes the PNGs. It uses its own config, `playwright.screenshots.config.ts`, so `npm run test:e2e`, a bare `npx playwright test` and editor integrations never touch committed files. -The capture loads the Open Sans font from the `@fontsource-variable/open-sans` -dev dependency (Cloudscape names Open Sans first but does not ship it) and -turns off text hinting and LCD smoothing, so the PNGs come out byte-identical -on different machines and in CI. CI regenerates them, uploads that set as the +The capture waits until the bundled Outfit and JetBrains Mono faces are loaded +and turns off text hinting and LCD smoothing, so the PNGs come out +byte-identical on different machines and in CI. CI regenerates them, uploads that set as the `screenshots` artifact, and fails when it differs from the committed files. The comparison is byte-exact, so CI fails if a committed screenshot differs by diff --git a/workflow-canvas/e2e/capture/screenshots.spec.ts b/workflow-canvas/e2e/capture/screenshots.spec.ts index 29115b39..54d1e840 100644 --- a/workflow-canvas/e2e/capture/screenshots.spec.ts +++ b/workflow-canvas/e2e/capture/screenshots.spec.ts @@ -13,3 +13,11 @@ for (const name of TAB_NAMES) { await saveFullPageScreenshot(canvas.page, `${name.toLowerCase()}-tab`); }); } + +// The Designer tab in the dark mode, switched with the button like a person would. +test('capture the Designer tab in the dark mode', async ({ canvas }) => { + await canvas.switchTheme(); + await canvas.expectTabLoaded('Designer'); + + await saveFullPageScreenshot(canvas.page, 'designer-tab-dark'); +}); diff --git a/workflow-canvas/e2e/screenshots/designer-tab-dark.png b/workflow-canvas/e2e/screenshots/designer-tab-dark.png new file mode 100644 index 0000000000000000000000000000000000000000..72615cef579852a8ef16dbf87bcde6a3c40f133c GIT binary patch literal 46734 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

1aBG^;e-vW_U%O4im&gysugr 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 ROoBUeS4KfP=a#{X{|C37MHB!4 literal 0 HcmV?d00001 diff --git a/workflow-canvas/e2e/screenshots/designer-tab.png b/workflow-canvas/e2e/screenshots/designer-tab.png index 4de06aeb6198eea3f611c269f92f697de9bf2a26..ddfa715e7f54ef5391100acddc1187c296646c4b 100644 GIT binary patch literal 46257 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¥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 literal 41157 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 z�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
n=ybDs`0<)FP2RZfU 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 diff --git a/workflow-canvas/e2e/screenshots/execution-tab.png b/workflow-canvas/e2e/screenshots/execution-tab.png index ce7f412c6515c277119a82a55997685460827ba0..aef9c280a848522f9746649b8113ee581e5c78a0 100644 GIT binary patch literal 20943 zcmeIacT`hp`!=fMPjN)R8AlnVWM(XkB1I68npYV`KtM%6YD5u~-UFnm2#liOfPfGn zBGOCfEhM1~V(1+LB%!wu0)#Zu&gOmBZ>{g2^R4xs@2qcq>zwx=x1{X->}TK4eP8!= zT~A)$u{M)Aa{9==efwn0Z~bv^-@b#ue-Hd}_&eah+I>*EZ{M+f=6_tfACb9EIoNRj zNn6X7sRdN;&kr?k{`}?P^9lUd$p`ewET3{pV>ndm@SnGid_Mf+Z-r>xcEFH$Sl$F$bHj3w-XnTpTgf0nl zlD)WD#HYS{H;B4bu61th?9?CG(|l-Ih+DE#!E#@g3STrROT(0EUU{Egky)c}v`hlgs;-du=P) z)#XxtcT(jirl?UL5?w((S*nn|D-%2GKJND?&up(PPM+8Q@(=M`W5{xr!E*fGwdyA9 zG5ggaRgSr*Rs1S@p6gRUP9J zg&Ol4KIC6%6TAdY>Ki|w*X4&qyGOH&m6Cx^x15|>+wZb1Z!J=@eJ1*JeH7(P<-A{* zvAsTbo<4TYXY4Gof&80(;LWW%hpIoe#>${2_i7(S)!$CPH=7E4zk=<@-ly(Vv7L&Y zUVY_3%dI|?gQP_nIcjp{$1hbr*$=1k3kZ_d;n}fLeqVZwA=5?0wL!39r7cY##b&8D z?^IPM(U_^u8tAK$4;vVx;oRQxfx^3ZC=C|;t)g6mxO+#kpBc{J%s~>2dGvs$S5u1) znR`R8NLjS1t5VuNL21*Gqi3!sA&N>hw*-^Xq?KRwE6C=@3$FO3rVykQ8<={gH}j

Vk6>GA_aK1-C;UqUO|amTcn7-bekhF+{~o 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 Lf0SQ;`11b(hE#{5 literal 14778 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 diff --git a/workflow-canvas/e2e/support/screenshots.ts b/workflow-canvas/e2e/support/screenshots.ts index b20ef8c0..68c4b66d 100644 --- a/workflow-canvas/e2e/support/screenshots.ts +++ b/workflow-canvas/e2e/support/screenshots.ts @@ -1,6 +1,4 @@ // SPDX-License-Identifier: Apache-2.0 -import { readFile } from 'node:fs/promises'; -import { createRequire } from 'node:module'; import path from 'node:path'; import { fileURLToPath } from 'node:url'; import type { Page } from '@playwright/test'; @@ -9,40 +7,35 @@ const SCREENSHOT_DIR = fileURLToPath( new URL('../screenshots', import.meta.url), ); -// Cloudscape lists Open Sans first in its font stack but does not ship it, so -// the page falls back to whichever font the machine has. Loading the same file -// on every machine keeps the text identical. -const FONT_FAMILY = 'Open Sans'; -const FONT_FILE = createRequire(import.meta.url).resolve( - '@fontsource-variable/open-sans/files/open-sans-latin-wght-normal.woff2', -); +// The faces that the page bundles. The browser loads a face only when text +// uses it, so the capture asks for each one, then waits until all can paint. +const BUNDLED_FACES = [ + '400 1em Outfit', + '500 1em Outfit', + '600 1em Outfit', + '700 1em Outfit', + "400 1em 'JetBrains Mono'", + "700 1em 'JetBrains Mono'", +]; -/** Loads the bundled font into the page and waits until it can paint. */ -async function pinFont(page: Page): Promise { - const fontData = (await readFile(FONT_FILE)).toString('base64'); - await page.addStyleTag({ - content: `@font-face { - font-family: '${FONT_FAMILY}'; - font-weight: 300 800; - src: url(data:font/woff2;base64,${fontData}) format('woff2'); - }`, - }); +/** Waits until every bundled font can paint. */ +async function waitForFonts(page: Page): Promise { await page.evaluate( - (family) => document.fonts.load(`1em '${family}'`), - FONT_FAMILY, + (faces) => Promise.all(faces.map((face) => document.fonts.load(face))), + BUNDLED_FACES, ); await page.evaluate(() => document.fonts.ready); } /** * Saves a full-page PNG as `e2e/screenshots/.png`, rendered with the - * pinned font, with animations and the caret frozen so repeated runs match. + * bundled fonts, with animations and the caret frozen so repeated runs match. */ export async function saveFullPageScreenshot( page: Page, name: string, ): Promise { - await pinFont(page); + await waitForFonts(page); await page.screenshot({ path: path.join(SCREENSHOT_DIR, `${name}.png`), fullPage: true, diff --git a/workflow-canvas/e2e/support/workflow-canvas-page.ts b/workflow-canvas/e2e/support/workflow-canvas-page.ts index 7dbdeeb6..24f02e59 100644 --- a/workflow-canvas/e2e/support/workflow-canvas-page.ts +++ b/workflow-canvas/e2e/support/workflow-canvas-page.ts @@ -6,6 +6,9 @@ export const TAB_NAMES = ['Designer', 'Execution'] as const; /** The label of one tab. */ export type TabName = (typeof TAB_NAMES)[number]; +/** The two modes of the page. */ +export type ThemeName = 'light' | 'dark'; + /** Text the Execution tab shows until it has real content. */ export const PLACEHOLDER_TEXT = 'Nothing here yet.'; @@ -36,12 +39,16 @@ export class WorkflowCanvasPage { readonly title: Locator; readonly graph: Locator; readonly edges: Locator; + readonly themeButton: Locator; + readonly breadcrumbs: Locator; constructor(readonly page: Page) { this.main = page.getByRole('main'); this.title = this.main.getByRole('heading', { level: 1 }); this.graph = page.getByRole('region', { name: 'Workflow graph' }); this.edges = this.graph.locator('.react-flow__edge'); + this.themeButton = page.getByRole('button', { name: /^Theme:/ }); + this.breadcrumbs = page.getByRole('navigation', { name: 'Breadcrumbs' }); } /** @@ -53,6 +60,20 @@ export class WorkflowCanvasPage { await this.title.waitFor(); } + /** Presses the theme button and waits until the page is in the other mode. */ + async switchTheme(): Promise { + const next: ThemeName = + (await this.themeName()) === 'dark' ? 'light' : 'dark'; + await this.themeButton.click(); + await expect(this.themeButton).toHaveText(`Theme: ${next}`); + } + + /** The mode the page is in, as the theme button says it. */ + async themeName(): Promise { + const text = (await this.themeButton.textContent()) ?? ''; + return text.endsWith('dark') ? 'dark' : 'light'; + } + tab(name: TabName): Locator { return this.page.getByRole('tab', { name }); } diff --git a/workflow-canvas/e2e/tests/theme.spec.ts b/workflow-canvas/e2e/tests/theme.spec.ts new file mode 100644 index 00000000..84442440 --- /dev/null +++ b/workflow-canvas/e2e/tests/theme.spec.ts @@ -0,0 +1,139 @@ +// SPDX-License-Identifier: Apache-2.0 +import { STUB_LAUNCH_CODE } from '../../playwright.shared'; +import { expectNoAccessibilityViolations } from '../support/accessibility'; +import { expect, test } from '../support/fixtures'; + +const DARK_CANVAS = 'rgb(11, 10, 18)'; +const LIGHT_CANVAS = 'rgb(245, 243, 238)'; + +test('shows the top bar, the breadcrumb and the title of the page', async ({ + canvas, +}) => { + await expect( + canvas.page.getByRole('link', { name: 'Workflow canvas' }), + ).toBeVisible(); + await expect(canvas.themeButton).toBeVisible(); + await expect(canvas.breadcrumbs).toContainText('Workflows'); + await expect(canvas.breadcrumbs).toContainText('hello'); + await expect(canvas.title).toHaveText('Workflow canvas'); +}); + +test('self-hosts the fonts of the theme', async ({ canvas }) => { + const requested: string[] = []; + canvas.page.on('request', (request) => requested.push(request.url())); + await canvas.page.reload(); + await canvas.title.waitFor(); + + const fonts = await canvas.page.evaluate(async () => { + // A face downloads only when text uses it, so ask for each one. + const [outfit, mono] = await Promise.all([ + document.fonts.load('400 1em Outfit'), + document.fonts.load("400 1em 'JetBrains Mono'"), + ]); + return { + outfit: outfit.length > 0, + mono: mono.length > 0, + body: getComputedStyle(document.body).fontFamily, + }; + }); + + expect(fonts.outfit).toBe(true); + expect(fonts.mono).toBe(true); + expect(fonts.body).toContain('Outfit'); + const origin = new URL(canvas.page.url()).origin; + expect( + requested.filter( + (url) => !url.startsWith(origin) && !url.startsWith('data:'), + ), + ).toEqual([]); +}); + +test.describe('light first load', () => { + test.use({ colorScheme: 'light' }); + + test('starts light and does not save a choice nobody made', async ({ + canvas, + }) => { + await expect(canvas.themeButton).toHaveText('Theme: light'); + await expect(canvas.graph).toHaveCSS('background-color', LIGHT_CANVAS); + expect(await canvas.page.evaluate(() => localStorage.length)).toBe(0); + }); +}); + +test.describe('dark system setting', () => { + test.use({ colorScheme: 'dark' }); + + test('starts dark when the system asks for it', async ({ canvas }) => { + await expect(canvas.themeButton).toHaveText('Theme: dark'); + await expect(canvas.page.locator('body')).toHaveClass(/awsui-dark-mode/); + }); +}); + +test('switches to the dark mode and back', async ({ canvas }) => { + await canvas.switchTheme(); + + await expect(canvas.page.locator('body')).toHaveClass(/awsui-dark-mode/); + await expect(canvas.graph).toHaveCSS('background-color', DARK_CANVAS); + + await canvas.switchTheme(); + + await expect(canvas.page.locator('body')).not.toHaveClass(/awsui-dark-mode/); + await expect(canvas.graph).toHaveCSS('background-color', LIGHT_CANVAS); +}); + +test('remembers the choice after a reload', async ({ canvas }) => { + await canvas.switchTheme(); + + await canvas.open(STUB_LAUNCH_CODE); + + await expect(canvas.themeButton).toHaveText('Theme: dark'); +}); + +test('a saved choice wins over the system setting', async ({ canvas }) => { + await canvas.switchTheme(); + await canvas.page.emulateMedia({ colorScheme: 'light' }); + + await canvas.open(STUB_LAUNCH_CODE); + + await expect(canvas.themeButton).toHaveText('Theme: dark'); +}); + +test('has no axe violations in the light mode, text contrast included', async ({ + canvas, +}) => { + await canvas.expectWorkflowDrawn(); + + await expectNoAccessibilityViolations(canvas.page); +}); + +test('has no axe violations in the dark mode, text contrast included', async ({ + canvas, +}) => { + await canvas.switchTheme(); + await canvas.expectWorkflowDrawn(); + + await expectNoAccessibilityViolations(canvas.page); +}); + +test('keeps the keyboard ring of a node visible in the dark mode', async ({ + canvas, +}) => { + await canvas.switchTheme(); + await canvas.expectWorkflowDrawn(); + await canvas.tab('Designer').focus(); + + await canvas.tabToNextNode(); + + await expect(canvas.page.locator('.react-flow__node:focus')).toHaveCSS( + 'outline-color', + 'rgb(167, 139, 250)', + ); +}); + +test('switches with the keyboard', async ({ canvas }) => { + await canvas.themeButton.focus(); + + await canvas.page.keyboard.press('Enter'); + + await expect(canvas.themeButton).toHaveText('Theme: dark'); +}); From 2f531a1986360691311d038e1c8d9a4682994cc0 Mon Sep 17 00:00:00 2001 From: Nihit <24500178+knihit@users.noreply.github.com> Date: Sun, 25 Oct 2026 18:13:48 +0000 Subject: [PATCH 004/476] Put the tab strip on the header band and finish the dark canvas ports 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. --- workflow-canvas/README.md | 25 +++-- .../e2e/screenshots/designer-tab-dark.png | Bin 46734 -> 46398 bytes .../e2e/screenshots/designer-tab.png | Bin 46257 -> 46160 bytes .../e2e/screenshots/execution-tab.png | Bin 20943 -> 20898 bytes workflow-canvas/e2e/tests/theme.spec.ts | 66 ++++++++++- workflow-canvas/index.html | 20 ++++ workflow-canvas/src/App.test.tsx | 15 ++- workflow-canvas/src/App.tsx | 3 + workflow-canvas/src/canvas/canvas.css | 25 +++-- workflow-canvas/src/canvas/nodes.tsx | 1 + workflow-canvas/src/chrome/Breadcrumbs.tsx | 36 ++++-- workflow-canvas/src/chrome/TopBar.tsx | 1 + workflow-canvas/src/chrome/chrome.css | 27 +++++ workflow-canvas/src/theme/install.ts | 24 +++- workflow-canvas/src/theme/mode.ts | 15 +-- workflow-canvas/src/theme/tokens.test.ts | 38 +++++-- workflow-canvas/src/theme/tokens.ts | 73 ++++++++----- .../test-support/canvas-contrast.test.ts | 103 +++++++++++------- .../test-support/early-mode.test.ts | 55 ++++++++++ 19 files changed, 403 insertions(+), 124 deletions(-) create mode 100644 workflow-canvas/src/chrome/chrome.css create mode 100644 workflow-canvas/test-support/early-mode.test.ts diff --git a/workflow-canvas/README.md b/workflow-canvas/README.md index 5d79a270..ab2e3be1 100644 --- a/workflow-canvas/README.md +++ b/workflow-canvas/README.md @@ -243,12 +243,16 @@ corner radii, in a light and a dark mode. - `src/theme/tokens.ts` holds every colour, font and radius of the theme, as two palettes and the Cloudscape token map built from them. Nothing else names a colour of Cloudscape. The colours of the canvas itself are in - `src/canvas/canvas.css`, with their dark values under `.awsui-dark-mode`. + `src/canvas/canvas.css`, with their dark values under + `:root[data-theme='dark']`. A test checks that they match the palettes. - `src/theme/install.ts` applies the token map with `applyTheme` from `@cloudscape-design/components/theming` (colours, fonts and radii only, with a value for each mode) and switches the mode with `applyMode` from `@cloudscape-design/global-styles`. The header band and the top bar are dark in both modes, so their text colours are set for the band in `tokens.ts`. + The root element gets `data-theme` (`light` or `dark`) and `color-scheme`; a + small script in `index.html` sets both before the first paint, so a dark page + does not flash white. - `src/theme/fonts.ts` loads Outfit and JetBrains Mono from the pinned `@fontsource/outfit` and `@fontsource/jetbrains-mono` packages (SIL Open Font License 1.1). The Latin files are bundled, so the page makes no request @@ -257,19 +261,24 @@ corner radii, in a light and a dark mode. **Theme: light** / **Theme: dark** button. The first visit follows the system setting (`prefers-color-scheme`). Pressing the button saves the mode in local storage (`src/theme/mode.ts`), and the saved mode wins from then - on. A page that was never switched keeps following the system. + on. A page that was never switched keeps following the system, but only as it + was at load: a later change of the system setting, or of the choice in + another tab, shows after a reload. - The page header is a dark band with the breadcrumb `Workflows > ` - (the page has no list of workflows yet, so the first link stays on the - page), the title and the Designer and Execution tabs. + (the page has no list of workflows yet, so no crumb is a link), the title + and the Designer and Execution tabs. The whole tab strip, with its rule and + the selected-tab indicator, is part of the band. - Nodes are cards with the corner radius of the theme. The `S` run-order ports are pieces of a jigsaw: the out port is a square tab under the card, the in port a square notch in the top edge, both at the right, so a tab fits a notch. Links are rails with an arrowhead. -Text meets WCAG 2 AA in both modes. `src/theme/tokens.test.ts` and -`test-support/canvas-contrast.test.ts` check the contrast of each text and edge -colour pair, and the end-to-end tests run axe (which includes colour -contrast) in both modes. +Text meets WCAG 2 AA (4.5:1) in both modes, and the edges of controls, nodes, +ports and the selected-tab indicator reach 3:1. `src/theme/tokens.test.ts` +and `test-support/canvas-contrast.test.ts` check each of these colour pairs +for the palettes, the band and the canvas in both modes, and the end-to-end +tests run axe (which checks text contrast) in both modes and read the colour +of the tab strip. ## End-to-end tests diff --git a/workflow-canvas/e2e/screenshots/designer-tab-dark.png b/workflow-canvas/e2e/screenshots/designer-tab-dark.png index 72615cef579852a8ef16dbf87bcde6a3c40f133c..34742769e07f1f804a8c2b10abc81644158c4098 100644 GIT binary patch literal 46398 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

1aBG^;e-vW_U%O4im&gysugr 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 ROoBUeS4KfP=a#{X{|C37MHB!4 diff --git a/workflow-canvas/e2e/screenshots/designer-tab.png b/workflow-canvas/e2e/screenshots/designer-tab.png index ddfa715e7f54ef5391100acddc1187c296646c4b..c5e48facc345237b6e1b0a95daa8c2320fdc0e05 100644 GIT binary patch literal 46160 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 literal 46257 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¥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 diff --git a/workflow-canvas/e2e/screenshots/execution-tab.png b/workflow-canvas/e2e/screenshots/execution-tab.png index aef9c280a848522f9746649b8113ee581e5c78a0..941e658bf583beeb9d8ac2caec21f4505dc5cabf 100644 GIT binary patch literal 20898 zcmeIacT|(<`ZlWLR#XIL#t{Y)7)J#KDT;v9j53N8K|y+nNK;zqA(TW#hzJB z?(503TUMqLhmRlLvuBTl*^NJK_UzdY9Paz=;19qPgX>tfXU~s&%>KM`Cn}Fh+~07= zyQ6K>1PVnSeEjg?<5!RO-(w&5_rws1{!rCOX#A@i=4XGqVUbn){t`KcHX94wdyMs% z+KMM@4HBUPd3s?RQEb@GV9Y@FOkvW56>y7LgxkPBfal+Td$@Pco~MuZ9N4qxx5GdD zx@XVBvj^`2AH4l{KA5+v$NYRPvclPiR(&Q?)uH+J+z$(IsK6UEn9UfW>m?;xqbHz4 z!-oz5SLUD7Gnc`j0%RIP7m4q-6L4dOTD}9WJ&IVkplR-?frPARIZ?v@DDc&t_|Rj0 zV3nzD^JBz?Z${vf+a975dcyfD6LZgwnXlh2pD|`gCp%U}!*{0*@oD?A*kx$N59^VL zvZ$9iJz)C}jxGDPqb~WT82)|dp9r1@8elD@B^iO~Ug>;T(n4Q$%pxDNd#_19LhOMC z*JoW4CFthQ$iFhaHU!>YK&AObq=8577o{v%TrvZ`cDFQ;v8Mtoc%_!lb}4ZU{oE;w z?U!qqg7-}6D$7G|m}e5VbUF_}i=$n4$K(>2D;E3=tjEHJe@>3yvJ-m3ay!#Zti_+@ z228#vbIIbb`iT2>%6=w*pMC%Aj16*bG*1{M4i{P^x&Y_5)nohjHQab)?t%|Rrl$!~ z`+t4Hjiar2Z9WFO)$EM#rzgmugqzBPNEeC{?t{!mlmGf?hY>sAm3xB)(YZLm!4+n; z)lCdp%CIexI)ZH>P#ZrVJAP?$p))mKj_Z~+L5?jGJ(R+Q$FUT<6}jt`19OpMV@qd% zp$B+2rU#w}=l|}~xcHNNI#Ej_&aTf>^JdjMV;_FejdDi zg!j^sm+zCW#3vR>M=pQUsiT+cg3PL2@(p46YvI-je=Q7Kw61F3{k(++X8d8fZGFJX zxAUd;<*S0Rwt3pEf_qcI4_9-m4*&W})};oH+m?OAW=)XKyHy)bT?k<8AsyEXQ|it~ zj}GSPyVdB!)*pEdFQ0Y^r;#9>>V<^EGJYdIg9Z?CL*>of#o{Y#Ejb@1EY)0VeENOS z>>JetArEUcMte%T4vpUEKW&pXSGBJU{3va)6$Tfgm z8Br2bMj*8e6cFQ1%8~2SJZg#qh)M4kJ5f2onjkVZyNQTEK_zx>>55qXMZeLVvR=kQ zZ_JpsEVA90wWG?AId5M7Zhh0J&86OhB3kUrIDr`I;=0vcgsp4@l#=g${tGR}6zLpU z9W8dM@fkbKuuVT80W~vfVhm^xi_2};>-Ovf-&?NV34R+omaHsj6~VLU?4MS30k_@3 z9P^1!`bEvPEUIcgwKrWC+MS~Ogn*9Wf;ZWNdlVy`Z5npEHW^T*nxfNFr7qZ3v0Ac0 z&*fPdT3G?Xc^Db9@HInKu)KGD8@{+QF|9bMR9ESn3NKy=oYtXDZtf)F##~x_`oBR; zjal=hok;H!SKKP^KGG#e8y-=ze4Tg7G)1rtY4KsLLP?z1beX`>s0kNH%lRYY6)sr@ z&_o?MSaaWiafsJf+)upsD|; zwIP3!B3WG1Yc=wr|G4ICuCii`9_y#xR$iTT>2xDI_c-1dW^YP0k%mh#P<<8NDRi?G`S{Ma=a)K3bl1Snm=HcF3v<^Yaq zxIgrJ=|q_k=Y5iWg=<7tw&=3(NvEp)SstqmVgMDc*4m~XT8rJ8lhJ6|33~Z9VWWXk z^5G^_)voE≶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

Vk6>GA_aK1-C;UqUO|amTcn7-bekhF+{~o zqW0{gaafc0lkFj667Q~g!pdMSEvduui2Q}=I>tslwHfI?Ri`$5jjoG(GIm?O+^Kdg zHxXc?&deP z-iXryYx1=-sr(7;zN-Z1da74AVU8e>RML=s<&C=uTpDxBk{*Yb0n1HDzWjBj)3*U0 zr?d@0J{fbbNHi9luYYx(Z-0GpbPTg2;7qx;)65EKK*-Jz z?LW$>JC)wr`mrOKrU4N*XBmRz{ZZ?ylGk9=A>Mo2q~FesC1MCV=_bFD94R<11v~F% zKDNJ~0gB}DMFusbWEGpGL6N~a8`}w4Yd}qfy7$`%^17$$e8`Qg%6I514;sBk-(%yz zoRxS}BFW%o)191`SOO>T=eJ6N$K@`K9CAIoI7O8fF+cy#t>#&ERzUOCoaO_(rxmq1 z(jrYgT^-D>Au1wy;%cv6#x#H13R?Te>qpC(A&=q`V0OhW?n0h96F6Yg4{0Jk zsQj$zP-e3Aij}4T37T9*>2n%PRqZZuJ6f=~P`>a(a^ewsvtUD|-&o)|_$VfzadDdx z?sKo{HmUOQS2@L<-s6cxEuXPW;Q4(E@44$$5Zx(e_aBg*5HR#`F7Pd@)uf$2=NNx? z$W!iPsR?*6=!4LK&pL3`mUwM3Lm$-Q3g)k4>LJ-05JEV^IcA?f%|EdsaJMGiJvgFs zQfIPm%J{zm1)Q>^(vZY)_(x*|YU`X=CaU#WPQb9*IuV6KaLBC55@r;mRxWs%l#+ch zQ+&HOT^k#*B>}7G|FMnWxPsQ{z( z&4krzgV3i6{;OAw$9Fk~1wHJ3RfJRUZ|2RIas@%t zUtk;Jae0V`t5K0Z#c4Skx~8m#E`QKi3?a5cX?*Ol3k6UU&U;giKr9G4XK|8-;Cf=% z>nIxD#x06WhSgFK3*Ro+OIUx+wwpc@Amb>P#0`WlPC?#vq|rLd$^HDL zo~c?NV&^F@Wm+k@&wb2~FI2uB z#_6t_${7dSO2iOa6zc$~uF5s@Kr^r4%Rgq%1`s?X^T|x2`AN}-g-2sv)7MH5Cs?VKko}SJ>$MwbgJm9g%&n)!`Ry9!iyqD58a!`477e(nW&v91;W{qvH zXRm?@@0*oMcHp%?r-B!|>&QObI8c;2tJNkV#URXE-L2}%2fqfvRwrI}+Vj?Q4bn7# z%LF&rm#xhn&rnsz)rK?H^r)dhg|y8AGEw57o!{?W4=t%e3v~0Jbf-d@V=C*Wwcd^p zr?Bk{W?<2X50f~~_&Yh_l3np1yb=*_zF*eF12JGC8Yyv)bRx!|RNrW9bxtEcoMg;e z5$K%GHn06koB-J3qU82o!8-YQX=FIz`)I8^OTy;R0Vz=E+FXYvHG;nN60$;PERoZ*Rq&5_JpLxYwz z^Bf8|ZuGITsZhP^qFA*wQA4QckfAYuB#X~lwyg4^3;|>34@`avS)Lzi^{a=RYE$|9 z#z$AnwDsnYtW(3D=cWcQCtJWa*z?r|=lZFsSEy%7kkH@XD@HkO58=)G*7(ne$698S zlYGjJiGhE$xs$w%rC#|TMd`VF@^z@)i zmlGG}vN`;6XB5nvk=UroS_038coug<&Sdk$K&CmzeRsH^_Xw(Ub&J|>BKT^hHw`*8 zYr{$Q*YIz0(4z#>RI$w<&mij)B^IbQDsI7By_6=lY&1~aHzw&C-+TjGU*0%pc~awU^V9i!nO*j&oq_g#b+?); z07|ZV@bvw$@>$SA0G{xoiI zffhVHAzRX*Ic>cAt-1}|4MEHg9vi|js5L>kyJ*A)H1}({W3^|`$1QXjO(rXJ`)&>r zB@Eh*Ty~T9UVIT3aFNA2^{J?&yxF*?1c(TwUg)!*&i|ruRRYqZM$o?9#l*yM{Gsa& zLC2H)R%~#G?U($73!|kMX!|i|Z9BavWmA`)jcAb^L5Kc6Usi2C@0 zRo#P*Ba1`8!$zQG+rADT`0?e%6jk^p(tbMj;s*R{&??*)+kfb+W5MkbKbXM>n zev=gkI{&I(ou0>~R-4A*+a!K%yio_L9N6+nV!Ja%wA_AnS0o(E+rTfs-jrY}vgQ-P zJA<7PaskayO1S|F;C*O=O=FHMR%Z`Ydl3gT zM7uJ|ypC~&GELu=Lj8~>g!UCdhDD6@SyVBYd2ygo%s`pS5m|p!>C%51S@@Kk{PISU z;!o5KO&PS%$i^^mW?)nPn8kYhe#gkvaMzFRnOc!iKlfy5gIymKSoiL%vXR-H7;TuF zCt3kHp4aa0GRVCu zBvDb4VRKGV8*F{hh}87Zjwz#e6-9j+npSLw{g9qz{e*yOuvjsA!=woi*1{DvD`nCL6 zf0}EMH89meWcnd3FqmIt1=-Sw)JAVO_m6SPJ?Ir1HfHK*(I6N|YUdE(5 z`%W)1RFq}`-W;%sQ*NRSb}EMQ`~MoFSBpsj{O0kNa@Xp`7fk4CDS)>9*v8qG?T0BduvVCDtG|K zL?j^>jvc=2!d>%lKZ6Hle8BHi2TiE z6>*afwkd>@S=u;fYi%4w6HWS(Y}+6`u27>?4;v+ksM&U}zuU{SNr>OsS>*2I(!TW! zN3iJp>;`2VRe1b2{hsdwYI>dhqclANlMp&fD%nl*#U=fI6BQe(SQFvsOn`vh?LBiZ z&te+hoi|(SR8LjoXSwb2Z!cBEX_!bn4f=MibIBpe>mjsuXYOWh{g;?b12NAu5j<;Jm3VYM1fX3|=zf8b{UHoad6U4pUZYV;gGX}zgPOQAG^+~Q*w&@d^jh(BK zrDsk<&8vcMX!$G$9iK!smiD}?xZbMY;j`m?*MA{_#Jqx-Ed8O~3;>!a0PW;;($15| zJ$85Lvue(|a_o;6Dm&s+mBV=HUO@f7+^Lv}Y#i)4RSiM%@dep%wu5-e%S8Tri9#Ld zG!J4!RX$0LqcNc8^YbVa61Y#@hi={5+_7!)A@9l2W*qv92M>K*7DW(hLJ5 zpYV!_T;IgSaotD$WDaYXTcBFEPjtmXYws;?uO?lKj&(yUGQJ0Rgx4W`O;Ne$W-+4C z60zi9avPDOJrCgNv(s&7ZCkG&G(&Msn>LpeK!*X4q!re_9KIGYhXvOI-L`-c(l$~w ztT+8av-70C0zQhDCb??{+Uyw)n1(WK4H@_6NA;9tq*XjYrA@P{{tenWjk@rs&Rur- ze`I<17zEM8G14E%p^+3%5=5i#baSZ9wrBtaD;(Z3FIg`ML5-?LO41j)iNuVtfQwfG zIAC0|<4DT=ezZ509_goY&8}HF*7 z$HNW-?e(~5zU_vzyiK+i-VLu5vfQWSnK)H%c;4757*;*%^?X@fSRVMISLu+j`v!Tw z_lvmPoK!_BXC#||&MS-syr;^b0ft6X3O5^;QlIXq?G?bu=s`W;!Jhyvu&%3e6E_}% zJ(yN~oh-JP_Ro_CNX9yR0S1X5OBhWwT6=znm}{1(aAgzi;D&?>N@*Tb{}ospjQ)Ox zOqWCVHgQI8+G8!K2rYefvB&6}1qWSVS?${_aH{vGj5<($@F$_7%amJkRfxldOBW(p zzQUM)usqK}y%2ICRS!2jv)S`dQttTq?o?m#ppK9IyX+nNJr>T{w*+!9sx=7&5CD%@ za=jEdE#LF@ZiksvVq{?J&}$%OfYR-^rE|;ltBHxA0F2b~1D2RxGa6;znlqnm0lYs0 zaDDrm!zslkG&f>610+0OcpC5Iar){}PcY}kgvZ`>h@JM5(_bX0JeNplNeFUS$#A(( zWJn0mJ+s5A*s?+KMF#-XZM}Ck=w$f*%!`hp1;tOMK4_3@;IH#<97&b*vgI|<`mBPS z>Loe!66Kf80IR0{r{OwY`rV zJNR$B@oPY%mJHCA?&CqyN^ZY4*qaC;HstUX;Q0Uer2qaf{wLp%_yY}^$Lk-@?M;KY zWZ}a7Podki^C*JSk&~CIf=AzYpbb9_U-QC5ZYZm9*J;H5s>N>Pc!;V)@x6L$%6A8) zdoW;>(Y#ZJ5|h-f;su!a%+J@ti+?f53n6M0?h7&t8tQ_Pm8>$ zp#P&=(W5*=W&@MkcL2IxA$cchU{5?2H^tB)!Gk9fjd(mNgZurw=HYr^p`P2D>|gbP zKI*L`d_3RJ=YB~VwKq*eVggel6W9`|z>>Zi()VEje5X1JKWx*mTMGr9HmY&h z_BtF=k>-YUe>$US4C7{T4NsolG?oydl z{5Ty}uMh9q*$QapKJ~0Vc;u(1g>@JKq~}}j$%sFpW>@&|FT-+_@g)tn=ANcnpW!fF ziYLFyPTgcV8wSF8EUlO7FNat+ZVj6Ryi)=jy|ppgo!ew}=rN6jE!wBYd@A7`L+iq9w)EsnsQHGd=QG!HOp6BpW>_pO3yb(ZYx#LULVhY`#oK;N^(I&T-S2rJwq$Z;^~Y+IWe*uGg0WCJXSXOliIx+D|#Jwn*A`zl_+W8_ry&v_-H5jEAw0yKusb#n5SmTM5c=36DQkJx5~F^vaAi?Xsru34_A-Q6GE7hz zDYVx(+BP4zfm#y_6eX%t!cwN8pw@6H1*$v&!=Bk*APonP%SxieU&mmI4FG93u+|9* z5;uj>Zzi7FcBDav9qHhQt4Zeoob+tyi?q6dSE4beSvXXpdcW{a{%!JnVsFzWd#*BO z2Q*NBd$kH>xZHfeP)Rr!-|x#=UF|ZBg|lbQaL0@71Xc0aLO1+ZH@`{FZnGDV?^`sw zXPNtzcft}~(KR$YpU{32Q5nw2a6cne>V|7tR2S%$(zfGUF|ST|+xuv)30l*&BZ#NR za1REP+$VM4rm4AZOV|nvU86Y^i1)Z}L^s|`&9Q04!EGn4j-1OyyBjq5GxBpt7{nr{ z0-V!6Rcr8t)}rpdxViHA28)SXjP_BFYVO^TI!m+#QY5o^o?sBRW{tTliba=CZL3&k zYdu>mw!;hH*;F;)ejBS6+ds7<9moGPVKU`o?P>0D!aNHPfF|3J; z%=I8`XL0licSWhc#4DF^R?qoXrw4_D#G;_s^2UY#~!t2A}5#24uwJWh0puk6%F`b9;=JIpE4s^ zUVh;|B@Wo5tIh=9liCYBdbFPZajgh))j13Ynft8WxYO!q{UD36(p3_w zaeW}6z#4tIQm`Db(2LeU5xyg(lE}NglcfGCYKbh+!CA5>97;huO%mSvCPfvssx^+U zaxhtce?~cKrqWCq`T0KSo@MH#4epIS+2uJTg(Yuuq{F$)g3)uWJ30U3r_Fi1+Qy&~bO-<)~n%g|WelN9r~Ig0(9ZP$TD^l24$X zpdm7JW3P?3?_1rCo=ozXsGlccEmEtdkH5+ovU#dD#x{*CK>8tHcP@^C!I44zxv6(SE-#+IsesUcfu4kb00_- z&nFfx+J3g)_L?ZN&n?nMx-^9bC54DvblYt``_nnYZH>+i;ulhi!usldv{qP`odd)m zK-_%RjBy{gv2$wpq=s9~Io0uUSFgTIvi`jp;4xn24S&ZYj{gt`67iRfLz-XA9>-r}|4aRhp(E>HTUW+?b=wS^{6?ffkQF1&*I*{&o0wb7-UawsBpK0DZYwq!(xfHSxRz-!j>j>`0i@K!S=X!LSC+a$Wo7?=!CLrY96WdP0xD?dw!9iRyIKQw*Ni?>-=4iM=pE zgRyde=Gu=k4ucpV*IvtSG}31mz{91Hxj@sZuEeFYI)>5p#cEIAfMzU6)c3SCARyvb3RRN|31@&b^61 zt(ha>vY!H2Nb`n^2cYjeF8MHvZ?jK`u^vke5zo*cGeqK>qEgiV?&#PIWDm) z$xINysbF%fH}ihUy~Q`H4L)hg@{mBQy(}&%&ul)j69 z6ymT+`c{FliA`M&fEqo%9#^@&hu(RZ)MRY{wkd(#+rFIEDL$v5IN?%xFD8mS+;$3} z9RsF4mILD6-LAq;WU6QCgmBWL3D%u4M@|^5et2xSL4?+>**TQE=+AbVGYO(ubU-uF zpVAZ>lcg|56vuWCZ%HwsWv;j-H$UeTb=#=Pk!Q?i(V!>SFVP<78nHzi3R)dH2RIlo zZu1wzR@|j(Cbj8pgSG|M0(Q~Nc`JqN$T5<;l*NZjmjT(Y6(Y+Y&+WgRapfGzWJs{B zs&UH!Cam;=vDcFmu#Fo-n}XPD?>CSg(%^h?A zMmWv_WPLz0*(W}$FVwk(h~nBDXmG0SV$;X@8@S6#1~oSQQf^fqs~I{zZ%(LYc)0S4 zt)5m?Eq3WnH2UxU4M0|qMA2z0TM$roFYjKIo`?6a=est>w_G`6=5)8IN?c4%*U*=v zZ6}AAsz*E1!+hR7n<%NI}HB1S>^O3xFfCle&7WXrkhGJfM-Yy1(_2lGuwl_R% z{Vsi<3GcmV7;*Si<1>-&(>q*XY(;mgqdX;+Y1A-oDlXP43)w|$w{3$7CXRDcv3Jy6 zfAxrL@N0;<;$n)*$5w?iCiQ5DoQSPQtGToD&neCuKSr@*mffU(fX#L+f9VD!(oeVd z)lXJ;WS5Pj&?5pSQgIC`4XNR)TVdc!FM5`x-H(}UA4abY1Tm%iaBF(R6u?9DP?bb7 z{S>WCG3kHHrB9d$r>lE+i)J((CyBIAq0KR*NE1(LfPfiON@AO>LIewcnqC^enIwO8o8`5NR(+?m!r(@;07BLNY zK4x4f5^7)njkAFl*DM=zTrder9GYPn?JUgg%1_UJ{&2yz*gbc5warRSC>zigqG6m- zM3X6V%sLk;w(U30{C-gFFGJ|YZINfHOCdC{9;wm>jxcMF569 z+-#^<3TX-@>B1O)QC(}E-o>nkHZRuEwf5_}w=LAub}N8nQ|;Anm3F_}WwqExow{=a z%or4`sRk$xNVgxW8J7wiM2pEBCs=57>Pv!%U~#u2}Ot-9okd1>2T5LK*iZ< zDvUet0T5Jl6G5G2r@OMDP#sI$?Q@8Z^R(YvhNpG>2g>NXq~)q7mU=$at0V*YCPwU( z{_ijw4z=FTNes{rFjTTb^Xiz-o0Do82?e)F28FopPUAM5!Ly-}Wb3Q2X*sZ4;2=5A zBITCDgNDGnTz@Rg*Yqt0M=z5y)7uTwGS^n^W z>UPs~W6*Bz<{<4IWm~??wm#h8utd#ZBZSu~rKsQr6$>$aVD2_kmyZS5ii+Na&0hzu zVf%N*iw$Smez_g24Ain!q1W$to2H!ywchMvv;$zE(Acs2b4YWvQGCHz5PPDnCUCTv zzKk@zkQE_xbib+h)@P)U)1_dOaw**oQpTM;lk8qJP`)f;RB1+V8SHcE`gT`;cPw5Q z9WM`9DzYtb4O#`a&5#Qq)>U3PhDsI4Bv$t|2slDQL@uTotSrBhEypdT_V~CW80S>7 zg=wMiiCHOX@Gyn7B>J%7J{4YOc2rV6N`&kybrp=sO>-lbdY1n>LqHovPR6RPlcTt6 zqa|PF-YW)X7V!@7rPxoP1wz|v;>Q)DKh4Tie7||3H4SSjaDX67C{#Z!CJT)va&c~fLlwap*tmnKd> zy;J8x4z2JY4fRKHtg1BFAIRT?e2K0zkb&EJ8Xi>sreIPN#a{D}e%07D^u$61b${v0 z7zXK7F}tl_z5L|~S_t1?W`iXB*La^y_4$_Jp!#xSsU3(U9)esvxcWoi>}#}g5PlY| z89)_!3>%Zw*Vw8(lKL#ihQOe0Y2A-M*=QLy=cKts>{&9*Ic7)A6vF}q_;FOc-O5yj zm>D%FU?Q&EE(o|vQ<&qA02~mrbqYX)&%ri*Qh^^2G6bx1S=v#EI)!@b;dGsJOLvi5$Wkd- zPQQm1Th)6BEc`QCYkUhRllD9^fT(sZt!%#W1ME^JBej-^HaMaltz9f|Go1}LcHFciu=NvDtbo<;6u}|JoDZyyVbgf1l_s~zA%AACz5j8sWi(r z5xzgJjNlcOyPEXEuMY-E@bppI1pw9Yk{96nRKTF2%pX3hhkV-p`*pJ3?r)j56}WW0 zz}2BAs|}`|94IBkb|udqRu{2GmhxfNh+Ag9KYsq5vav4tQMG3;fl2Kg3M#sHZiB#G z0*C+NPcu0%eC^{eh;;}Q6gl#1PM!^T{%<-`Y%n*&>feHD2b)Spf>ee6tGs?l$eruYsXSrs$L~QAEDXgd4 zhQ?dG>KxLr#{P@W6S-zgaJzA=D49l@e3cfuv@T`46O?S+y6>#j^AfXc35 z_@$hh0oM7GgaVmeI5nW%3>FXw9caO79nWsfl>?71&MTuSOOv%erlNd^yb$IZ)?pQY zvW%E<|6`pWTOfBLTic6zPTmzaTl7ugG}yoGhP+GG1=gmeRo1R#JiPCff1*ELOPY9E z*sI}sf)B(Ya+#5+E4Tw_SZH0piCn?529f7kb)uOy7MH#?ae021t@N=G7o*YS9KRHE zx3haXs4+5-_LkNw?A)J3|KndSF~S5?D^oaltDU` z<`U5h4rtU1Y4J-!ocI3L5z}=t2X$5{CQW z`hj0lo1gLmLfb06UNvrLa?y?r9$QJ9>XguSS3nPF!KgT-{}`6UxCi)8=z4`XIDcF7 zMC0tGEsQ$Q!x)a0I}+ezQgS}fVCaxD-u$FC^ywPq*n6N|Ba$Bz0rJGDC^*L5)J)yA z=?dAtZsA6|Q6S6p^l~p6y1SF^9&3c}JGFP2rvjOWz*K&_=1l)*e?C$EKeo{Si+1t< zEvK4ce3No@vjT2n!r@jfD9E)G4JY5RiQqGze?iz}1E3b-DWpb5$kr z@<1+V3y710EmTSs+)mFF{^|gUbgg{Up=w_YmmGkq{7`tOe3G4J(zv?a$nwla&bpE6 zg8QwQ8xL|@X97%!Y(jM#MV28B_Ow6o_`kV;uFABy)@SlH>3XO=d2Q2cBH_D;>eG7O z!Q)C?pxG+{gyGxUf_Dnwa>wDN$DQK8mcf{2nNgP|E zYF~IIZ1TfdEhc>D?*atXqE3#MusfI8ZTRXi1n+6LH!c;jwbye~x^=pi{xo0=4(Yku zXbuS6i=9#(9p(o`7)+t@UK+eGaX8Ye^NrnsPS!023!`wE12uultfq zL*TmHV`YReCz7?~24%|t0)yny%=3C`B>o>SzUu@D#+Q1-%N`T~Wb|=|GN%+nhm9?8 z!rqiSGaJWe<7?pqa7(v=dPY2Y_K@WTP>?q=npYHEzi$qO#R zzjHOpopFwNZ*&0ns)wGZrT~ zl9;MtyEsRr96v-m%5*07l^c%wUfC94-Jc*xW!%D)n2Ix=!^#j(eJ{*waJ@dOwMX|VFKeCoc3WrwPKHso-wbgK9(BioMK@}m0-JB%saohs#V8OGHOd81^mV|s7u9Dx4oIfkf{eEWCVz+PLXFz18oEt_3r9P@XmmV@3~Y z$A@+q+pFEVoyh7=es#Ca-mx%XY&aD{trXrSZ(_x49klcUAOhxDTZ8okJ3%2j+_5hcC#dPR z|LNh2>+VK8w41SjaQsm3X)nZ;3P2N(Wk`PXcW?Ie8&jC7aW6S<=!L&Qdv|L4Z)v@l zYZwg^A$vSa%g?1j%{e=4h1Yh26$hX4$~|$K*=)wsUlU+4NIu40b76|m4>tNGfC|T5 z@fVn59frfG;+5B%*AYbC)&B}?aIT6--%HuxI=ob1fk^I_R69ZGUYXO_hb39V1=?oI z{4Xdf+_eemf_Gw{RC9{epBm^%a}8e181>oZFtZ%J01Iq%Tg;KX=$8kj{da>`y?yKh z;AQzq=PY8Ui1?>s;p#(nF=MOE+BQF7>WWG9jd=M>j#Zj6GwIb5#^gibBEN>L2VI0v zg@BYO8-31F-mfMrEL|@Y?}B#b#cL@XEouh1gJUg{x~;Q5hUFgtC(1%8L;bmB>cv{R zIxglK5O#pAq}~A??ryYz#&#xxz`+ZfvVw3I!lRCZ@*=;k52T!o$r@_O__A^Pkvxku zz%RfDc8gdncG>ykR}+TvEcN`xeB1Ylm(bW$4T#&Pf4rcV<13fFj4wJA4}CGORwH8u zbIsyNR1iuLMOu^xh|J$SHmwau`N5l?JOuW6+X=l25{bN|+!g1!@~7B6kb0^f=n&jj zhdKbVSicb1hLhCJ0K}I`DE5p)=>sGQEQ`_dgHw(8oN2r*b3f>;qY8Jd#HDgc-HJ2m zlNjAzx(?)V9p8oLmGg0unupdAHMjoy(QuriV|1?c_(q>(wVpT+?U8G5>#|bVAQvLL zXK6A<8-KEfPlI%pN`Qy@&}VYvyN1YW*EYZQbKtjv*>YTfTr^T7P(+MW+Ff*rToHGE zO$0gA`8FN{WY!VXT#;D-ev6|CZbo$;*z+Ntj7=%9K@(-mRNTL+}d*GHnG|qWqLXrPkd^T?UawNO z@8(iTcK;<|(9p^?2+FDQI(u(MN=RA0Y1gf+GOnyvyJ1XrHbrTW$h%QNMVc|N4!g9s zyZ#AUH00T#{I(0mCH1>UU4?{t>*VGReuzLzP@wZ zW_H_#t&L0-g=Zz=Je0^dTSikoCCaPy`TMZh)-qyXzwi==|Fb8JOX5An6;MRXP6WMOb#*E5m2bJGq0IVO>7@<4B1 zb}{wT%zZ#$R{%_w)k_ILPb2=~oCai`vs1^lTL8rE+|Lmkx9HOgFiYIkDVQPgx%S?x zrkCRWv1iHB|Cc2zSk3W@hPy!1mzQXDdUQ{#m^kvUJ|Xb`)^W9c-q&lXzYF{r-n!Tg z*WChLxD)6XYNUoz{A#YN{Nj=(+2yQO4>`wUe~385&wR8hG7kS>c~$n=uM2d z+>1Tc3YqFd`W$I){j;vH6Zq)9V5j7pI?0*uAMcHB-<0;h#`^Er$Nne2t$*8zrgDH+ zY38PI8NMECX+qauNiU#^*j`p>U3xS(DsTSUfj9}H6s_mF+__~Ni!#*Z%ejOsM!Vv6 zZ0ZisWEF;qG`&6d+LXQZp-sSEkC>~}lNZ+fUhTnIj|9ja0o$wh!Jv#ao92qN03vH2>VCFQIpH_b zub6PqM3YrD^X5AU0scgEs5WVx+()inuBYK{{2X^t*(l;~GtiDWeC1lyh=4~)DQ8UQ=)$xHViP7DmDsHOqlZRG>RpR<8w-euPnGui}I*c^2@qo7+V zG9MSyj`ID7zN7;jjL+Yjpso)W?tcDbaA!15_LQhq3WL($oC5qnaGpDJv}F8#(LIyu z01U#Q5Y8WRpLfM^Btg>)PKNfqfcLh^A^MPvgcnV9mpUS^e!n<$t3tE6YaEi8p%u9V zY+@sXlBzOvE~?OztQ;&6hQ2+lemw0F_h#g_2O>k$d!)e&6NTlMvssJX0ulFkQy62s zzW@E%@up47-j62BIZ|%Q+Tlaf`m8t^5y7MA(PCH1Uc^nD4~{t0aOQfpLT;gqcW=IU$#2I3eaBYYjU#c_0K#O?wOE(ZY;IlRT>mvEt`@bE;}Fou5cafb zS{e9GaO-K2chjrRwNn5~7bQ@X%ivNOGp3U=r?x2-?vE3$dehDdetR&xobILM*c>sK zo3I)vs+B{r7yqnim8pjm-D?~+1pRPG3N*<{l4c%Bcgl^*%f`qWu-0?APe;$XBFPL)a2|vS}er$~TZ!Tb1ZCTy1d7V*jA`(P)>iFMN zDGmAN&QK?8mPgC_DIj|R#B7Z=F%%&HL_Ogdto3~S9DOJRC8u8_0`?+-w+BjeeC+yC zR2?2@&2|=@xcI>wAer29@BK*vAwS-$b?SJhd#_?fHV4`Mi&3Tb=n?lrVYw%|Qk{u^ zRXA?*ghdI6xGQ% zI5!5{alXcTqzZ?;SFeqw&xrWKpVK_7#WNfY)sgd z()~{Z(WzkMp{peS@ena`ojq=(<=0i3L@ww)d8wv7vr!sAfOkK&$j)xOJQxr7R!*K2 ze+{pjNoh+QB5PcZ+tqm^?3$J}F`ltOMd0|t(I*V+<<;CW zsD72O++$h&V3rub8m5Q9q6I^Mrg#QyOiw!Am7+m`rybHT5{N&&Xg50w5_~ho#(Y1e z7WpWiq5uGs4LbJbSeY~4v$xApzM)HY99%Jp6fiFHy8l}2O0!V;Typ;cus^pLZz8C& z7~KXGbpW1-oArSP_d>&9wTZZ}-!K4?0faj6;{j#+WY_cx!zN~71@m(q@oo`YuYvRn zX6-2S>9dv&N}q&;l3t_xYQ^qZ{YIZ1Sy2JaVv+(D8ZiCEzvIo55D{l5Xw1zua0>#2 zDF5xS|5fg~Gg49S-1t&<-MNy{B4=Zp>}e^*8V65Ot@?2~x0H;6f86@8^eq zBd(?rPaA1X@0n)m2riZ_dKTL{tfT<|BJ=@Kc@l^)c>j% z|38!XpGo{bW!J^Oi{AeeyZ=9q-S+?%Jn)~w1JvLDGqz0qXYu~07XK#__x)!X{O?!> z10Li%`}Xa>wfEbB|2wYt|LJSQNPwV~NJz)ZDu8NoXm3sr>^l>nGV1eVNEYzQKJy#a Lf0SQ;`11b(hE#{5 diff --git a/workflow-canvas/e2e/tests/theme.spec.ts b/workflow-canvas/e2e/tests/theme.spec.ts index 84442440..452a5088 100644 --- a/workflow-canvas/e2e/tests/theme.spec.ts +++ b/workflow-canvas/e2e/tests/theme.spec.ts @@ -2,10 +2,15 @@ import { STUB_LAUNCH_CODE } from '../../playwright.shared'; import { expectNoAccessibilityViolations } from '../support/accessibility'; import { expect, test } from '../support/fixtures'; +import type { WorkflowCanvasPage } from '../support/workflow-canvas-page'; const DARK_CANVAS = 'rgb(11, 10, 18)'; const LIGHT_CANVAS = 'rgb(245, 243, 238)'; +const BAND_RGB = 'rgb(20, 16, 38)'; +const BAND_DIVIDER_RGB = 'rgb(42, 36, 80)'; +const BAND_LINK_RGB = 'rgb(196, 178, 255)'; + test('shows the top bar, the breadcrumb and the title of the page', async ({ canvas, }) => { @@ -65,19 +70,28 @@ test.describe('dark system setting', () => { test('starts dark when the system asks for it', async ({ canvas }) => { await expect(canvas.themeButton).toHaveText('Theme: dark'); - await expect(canvas.page.locator('body')).toHaveClass(/awsui-dark-mode/); + await expect(canvas.page.locator('html')).toHaveAttribute( + 'data-theme', + 'dark', + ); }); }); test('switches to the dark mode and back', async ({ canvas }) => { await canvas.switchTheme(); - await expect(canvas.page.locator('body')).toHaveClass(/awsui-dark-mode/); + await expect(canvas.page.locator('html')).toHaveAttribute( + 'data-theme', + 'dark', + ); await expect(canvas.graph).toHaveCSS('background-color', DARK_CANVAS); await canvas.switchTheme(); - await expect(canvas.page.locator('body')).not.toHaveClass(/awsui-dark-mode/); + await expect(canvas.page.locator('html')).toHaveAttribute( + 'data-theme', + 'light', + ); await expect(canvas.graph).toHaveCSS('background-color', LIGHT_CANVAS); }); @@ -137,3 +151,49 @@ test('switches with the keyboard', async ({ canvas }) => { await expect(canvas.themeButton).toHaveText('Theme: dark'); }); + +// The strip of the tabs, the selected-tab indicator and the rule under it. +async function expectTabStripOnBand(canvas: WorkflowCanvasPage) { + const strip = canvas.page + .getByRole('tablist') + .locator('xpath=ancestor::*[2]'); + await expect(strip).toHaveCSS('background-color', BAND_RGB); + await expect(strip).toHaveCSS('border-bottom-color', BAND_DIVIDER_RGB); + // The indicator is drawn after the element that wraps the tab button. + const indicator = await canvas + .tab('Designer') + .evaluate( + (tab) => getComputedStyle(tab.parentElement!, '::after').backgroundColor, + ); + expect(indicator).toBe(BAND_LINK_RGB); +} + +test('puts the tab strip on the dark band in the light mode', async ({ + canvas, +}) => { + await expectTabStripOnBand(canvas); +}); + +test('puts the tab strip on the dark band in the dark mode', async ({ + canvas, +}) => { + await canvas.switchTheme(); + + await expectTabStripOnBand(canvas); +}); + +test('sets the saved mode before the app script runs', async ({ canvas }) => { + await canvas.switchTheme(); + await canvas.page.route('**/*', (route) => + route.request().resourceType() === 'script' + ? route.abort() + : route.fallback(), + ); + + await canvas.page.reload(); + + const root = canvas.page.locator('html'); + await expect(root).toHaveAttribute('data-theme', 'dark'); + await expect(root).toHaveCSS('color-scheme', 'dark'); + await expect(canvas.themeButton).toHaveCount(0); +}); diff --git a/workflow-canvas/index.html b/workflow-canvas/index.html index c42a03f9..7863aed8 100644 --- a/workflow-canvas/index.html +++ b/workflow-canvas/index.html @@ -5,6 +5,26 @@ Workflow canvas +

diff --git a/workflow-canvas/src/App.test.tsx b/workflow-canvas/src/App.test.tsx index 1f151f3c..698c05ed 100644 --- a/workflow-canvas/src/App.test.tsx +++ b/workflow-canvas/src/App.test.tsx @@ -20,6 +20,7 @@ const themeButton = (mode: 'light' | 'dark') => afterEach(() => { window.localStorage.clear(); document.body.classList.remove('awsui-dark-mode'); + delete document.documentElement.dataset.theme; document.documentElement.style.colorScheme = ''; }); @@ -109,17 +110,17 @@ describe('App', () => { ); }); - it('shows the breadcrumb Workflows > the open workflow', async () => { + it('shows the breadcrumb Workflows > the open workflow, with no dead link', async () => { render(); const trail = await screen.findByRole('navigation', { name: 'Breadcrumbs', }); - expect(within(trail).getAllByText('Workflows').length).toBeGreaterThan(0); - // The trail holds a hidden copy that it measures, so the name appears twice. - expect((await within(trail).findAllByText('hello')).length).toBeGreaterThan( - 0, - ); + expect(within(trail).getByText('Workflows')).toBeTruthy(); + expect( + (await within(trail).findByText('hello')).closest('[aria-current]'), + ).toBeTruthy(); + expect(within(trail).queryAllByRole('link')).toEqual([]); }); it('switches to the dark mode, remembers it and switches back', () => { @@ -129,12 +130,14 @@ describe('App', () => { expect(document.body.classList.contains('awsui-dark-mode')).toBe(true); expect(document.documentElement.style.colorScheme).toBe('dark'); + expect(document.documentElement.dataset.theme).toBe('dark'); expect(window.localStorage.getItem(MODE_STORAGE_KEY)).toBe('dark'); expect(themeButton('dark').textContent).toBe('Theme: dark'); fireEvent.click(themeButton('dark')); expect(document.body.classList.contains('awsui-dark-mode')).toBe(false); + expect(document.documentElement.dataset.theme).toBe('light'); expect(window.localStorage.getItem(MODE_STORAGE_KEY)).toBe('light'); }); diff --git a/workflow-canvas/src/App.tsx b/workflow-canvas/src/App.tsx index 274f0003..f8c053c0 100644 --- a/workflow-canvas/src/App.tsx +++ b/workflow-canvas/src/App.tsx @@ -5,9 +5,11 @@ import ContentLayout from '@cloudscape-design/components/content-layout'; import Header from '@cloudscape-design/components/header'; import Tabs from '@cloudscape-design/components/tabs'; import { Breadcrumbs } from './chrome/Breadcrumbs'; +import './chrome/chrome.css'; import { TOP_BAR_ID, TopBar } from './chrome/TopBar'; import { Designer } from './designer/Designer'; import { useSettled } from './designer/use-settled'; +import { TAB_STRIP_STYLE } from './theme/tokens'; import { useThemeMode } from './theme/use-theme-mode'; import { openWorkflowName, type LoadResult } from './workflow/load-workflow'; @@ -44,6 +46,7 @@ export function App({ load }: { load: Promise }) {
+ {kind === 'tab' && } {kind === 'notch' && ( <> diff --git a/workflow-canvas/src/chrome/Breadcrumbs.tsx b/workflow-canvas/src/chrome/Breadcrumbs.tsx index 55fcaceb..71da872f 100644 --- a/workflow-canvas/src/chrome/Breadcrumbs.tsx +++ b/workflow-canvas/src/chrome/Breadcrumbs.tsx @@ -1,18 +1,36 @@ // SPDX-License-Identifier: Apache-2.0 -import BreadcrumbGroup from '@cloudscape-design/components/breadcrumb-group'; +import Box from '@cloudscape-design/components/box'; +import Icon from '@cloudscape-design/components/icon'; /** * The trail above the title: Workflows, then the workflow that is open. The - * page has no list of workflows yet, so both stay on this page. + * page has no list of workflows yet, so no crumb is a link; the last one is + * the current page. */ export function Breadcrumbs({ workflowName }: { workflowName?: string }) { - const items = [{ text: 'Workflows', href: '#' }]; - if (workflowName) items.push({ text: workflowName, href: '#' }); return ( - event.preventDefault()} - /> + ); } diff --git a/workflow-canvas/src/chrome/TopBar.tsx b/workflow-canvas/src/chrome/TopBar.tsx index 3cd89156..d4b450e9 100644 --- a/workflow-canvas/src/chrome/TopBar.tsx +++ b/workflow-canvas/src/chrome/TopBar.tsx @@ -23,6 +23,7 @@ export function TopBar({ event.preventDefault(), title: 'Workflow canvas', logo: { src: LOGO_DATA_URI, alt: 'Konductor' }, }} diff --git a/workflow-canvas/src/chrome/chrome.css b/workflow-canvas/src/chrome/chrome.css new file mode 100644 index 00000000..f990720d --- /dev/null +++ b/workflow-canvas/src/chrome/chrome.css @@ -0,0 +1,27 @@ +/* SPDX-License-Identifier: Apache-2.0 */ + +/* + * The strip of the Designer and Execution tabs is part of the dark header + * band: it is painted with the band colour across the full width of the + * page, so the tab labels, the selected-tab indicator and the rule under + * them all sit on the band. The colours come from the theme module through + * `--wf-band-background` and `--wf-band-divider`. + */ +.wf-tab-strip :has(> * > [role='tablist']) { + background: var(--wf-band-background); + box-shadow: 0 0 0 100vmax var(--wf-band-background); + /* The library sets this colour with a more specific rule. */ + border-block-end-color: var(--wf-band-divider) !important; + clip-path: inset(0 -100vmax); +} + +/* The trail above the title. */ +.wf-crumbs { + display: flex; + flex-wrap: wrap; + align-items: center; + gap: 8px; + margin: 0; + padding: 0; + list-style: none; +} diff --git a/workflow-canvas/src/theme/install.ts b/workflow-canvas/src/theme/install.ts index 0e9e6152..2ebc59ba 100644 --- a/workflow-canvas/src/theme/install.ts +++ b/workflow-canvas/src/theme/install.ts @@ -3,13 +3,24 @@ import { applyMode, Mode } from '@cloudscape-design/global-styles'; import { applyTheme } from '@cloudscape-design/components/theming'; import { APP_THEME, + BAND, FONT_TEXT, TAB_STRIP_THEME, type ThemeMode, } from './tokens'; -/** Where the tab strip of the page header is, for `TAB_STRIP_THEME`. */ -export const TAB_STRIP_SELECTOR = '.wf-tab-strip [role="tablist"]'; +/** + * Where the strip of the page tabs is, for `TAB_STRIP_THEME`: the element + * that holds the tab list, found through its role so no library class is + * needed. `chrome.css` paints the same element with the band. + */ +export const TAB_STRIP_SELECTOR = '.wf-tab-strip :has(> * > [role="tablist"])'; + +// The band colours that `chrome.css` reads, as custom properties. +const BAND_VARIABLES = { + '--wf-band-background': BAND.background, + '--wf-band-divider': BAND.divider, +} as const; /** * Applies the konductor docs theme to the page. Call it once, before the @@ -18,12 +29,19 @@ export const TAB_STRIP_SELECTOR = '.wf-tab-strip [role="tablist"]'; */ export function installTheme(body: HTMLElement): void { body.style.fontFamily = FONT_TEXT; + for (const [name, value] of Object.entries(BAND_VARIABLES)) { + body.style.setProperty(name, value); + } applyTheme({ theme: APP_THEME }); applyTheme({ theme: TAB_STRIP_THEME, selector: TAB_STRIP_SELECTOR }); } -/** Switches the page, its components and its native controls to `mode`. */ +/** + * Switches the page, its components and its native controls to `mode`. The + * root's `data-theme` is what the canvas styles key on. + */ export function showMode(mode: ThemeMode, root: HTMLElement): void { applyMode(mode === 'dark' ? Mode.Dark : Mode.Light); root.style.colorScheme = mode; + root.dataset.theme = mode; } diff --git a/workflow-canvas/src/theme/mode.ts b/workflow-canvas/src/theme/mode.ts index 23592863..987cee84 100644 --- a/workflow-canvas/src/theme/mode.ts +++ b/workflow-canvas/src/theme/mode.ts @@ -14,18 +14,11 @@ function isMode(value: unknown): value is ThemeMode { return value === 'light' || value === 'dark'; } -// Reading `localStorage` can throw when the browser blocks storage. -function storageOf(env: ModeEnvironment) { - try { - return env.localStorage; - } catch { - return undefined; - } -} - +// Reading `localStorage` can throw when the browser blocks storage, so the +// read of the property is inside the `try` too. function storedMode(env: ModeEnvironment): ThemeMode | undefined { try { - const value = storageOf(env)?.getItem(MODE_STORAGE_KEY); + const value = env.localStorage?.getItem(MODE_STORAGE_KEY); return isMode(value) ? value : undefined; } catch { return undefined; @@ -53,7 +46,7 @@ export function otherMode(mode: ThemeMode): ThemeMode { /** Keeps a chosen mode for the next visit. A blocked storage is ignored. */ export function rememberMode(env: ModeEnvironment, mode: ThemeMode): void { try { - storageOf(env)?.setItem(MODE_STORAGE_KEY, mode); + env.localStorage?.setItem(MODE_STORAGE_KEY, mode); } catch { // The choice then lasts until the page closes. } diff --git a/workflow-canvas/src/theme/tokens.test.ts b/workflow-canvas/src/theme/tokens.test.ts index 5f192bda..90ffca81 100644 --- a/workflow-canvas/src/theme/tokens.test.ts +++ b/workflow-canvas/src/theme/tokens.test.ts @@ -1,7 +1,14 @@ // SPDX-License-Identifier: Apache-2.0 import { describe, expect, it } from 'vitest'; import { AA_GRAPHIC, AA_TEXT, contrastRatio } from './contrast'; -import { APP_THEME, BAND_BACKGROUND, colorsOf, type ThemeMode } from './tokens'; +import { + APP_THEME, + BAND, + colorsOf, + TAB_STRIP_STYLE, + TAB_STRIP_THEME, + type ThemeMode, +} from './tokens'; const MODES: ThemeMode[] = ['light', 'dark']; @@ -41,17 +48,32 @@ describe.each(MODES)('the %s palette', (mode) => { }); describe('the header band', () => { - it.each([ - ['#f5f3ee', 'text'], - ['#c9c2e0', 'muted text'], - ['#c4b2ff', 'links'], - ])('has AA contrast for %s (%s)', (colour) => { - expect(contrastRatio(colour, BAND_BACKGROUND)).toBeGreaterThanOrEqual( - AA_TEXT, + it.each([BAND.text, BAND.textMuted, BAND.link])( + 'has AA contrast for %s', + (colour) => { + expect(contrastRatio(colour, BAND.background)).toBeGreaterThanOrEqual( + AA_TEXT, + ); + }, + ); + + it('draws the selected-tab indicator at 3:1 or more on the band', () => { + expect(contrastRatio(BAND.link, BAND.background)).toBeGreaterThanOrEqual( + AA_GRAPHIC, ); }); }); +describe('TAB_STRIP_THEME', () => { + it('colours the selected-tab indicator from the band', () => { + expect(TAB_STRIP_THEME.tokens.colorBorderItemSelected).toBe(BAND.link); + }); + + it('colours the line between tabs from the band', () => { + expect(TAB_STRIP_STYLE.tabSeparator.color).toBe(BAND.divider); + }); +}); + describe('APP_THEME', () => { it('gives the base font to the page', () => { expect(APP_THEME.tokens.fontFamilyBase).toMatch(/^Outfit,/); diff --git a/workflow-canvas/src/theme/tokens.ts b/workflow-canvas/src/theme/tokens.ts index 623089b6..b2e6b7c2 100644 --- a/workflow-canvas/src/theme/tokens.ts +++ b/workflow-canvas/src/theme/tokens.ts @@ -105,13 +105,17 @@ export function colorsOf(mode: ThemeMode): Colors { /** * The header band and the top bar are dark in both modes. These are the - * colours of the text and controls on them; each pair reaches 4.5:1 on - * `BAND_BACKGROUND`. + * colours of the text and controls on them. Text and the selected-tab + * indicator reach their contrast (4.5:1 and 3:1) on `background`. */ -export const BAND_BACKGROUND = '#141026'; -const BAND_TEXT = '#f5f3ee'; -const BAND_TEXT_MUTED = '#c9c2e0'; -const BAND_LINK = '#c4b2ff'; +export const BAND = { + background: '#141026', + text: '#f5f3ee', + textMuted: '#c9c2e0', + link: '#c4b2ff', + /** The hairline under the top bar and under the tabs. */ + divider: '#2a2450', +} as const; /** The font of the page text. Self-hosted by `fonts.ts`. */ export const FONT_TEXT = @@ -134,7 +138,7 @@ export const LOGO_DATA_URI = `data:image/svg+xml;utf8,${encodeURIComponent( type Tokens = Theme['tokens']; -/** A token that has one value in light mode and one in dark mode. */ +// A token that has one value in light mode and one in dark mode. function perMode(pick: (colors: Colors) => string) { return { light: pick(LIGHT), dark: pick(DARK) }; } @@ -155,7 +159,7 @@ function backgroundTokens(): Tokens { colorBackgroundItemSelected: perMode((c) => c.tintStrong), colorBackgroundCellShaded: perMode((c) => c.surfaceAlt), colorBackgroundToggleDefault: perMode((c) => c.control), - colorBackgroundHomeHeader: BAND_BACKGROUND, + colorBackgroundHomeHeader: BAND.background, colorBackgroundStatusError: perMode((c) => c.errorBg), colorBackgroundStatusInfo: perMode((c) => c.infoBg), colorBackgroundStatusSuccess: perMode((c) => c.successBg), @@ -214,8 +218,8 @@ function textTokens(): Tokens { colorTextDropdownItemHighlighted: perMode((c) => c.text), colorTextDropdownItemSecondary: perMode((c) => c.textMuted), colorTextGroupLabel: perMode((c) => c.textMuted), - colorTextHomeHeaderDefault: BAND_TEXT, - colorTextHomeHeaderSecondary: BAND_TEXT_MUTED, + colorTextHomeHeaderDefault: BAND.text, + colorTextHomeHeaderSecondary: BAND.textMuted, colorTextBreadcrumbCurrent: perMode((c) => c.textMuted), colorTextBreadcrumbIcon: perMode((c) => c.textMuted), }; @@ -263,23 +267,21 @@ function shapeTokens(): Tokens { }; } -/** - * Inside the dark band the global text and border colours do not apply, so - * the same roles are set again here. Cloudscape reads these in the page - * header, which holds the breadcrumbs and the title. - */ +// Inside the dark band the global text and border colours do not apply, so +// the same roles are set again here. Cloudscape reads these in the page +// header, which holds the breadcrumbs and the title. function headerBandTokens(): Tokens { return { - colorTextAccent: BAND_LINK, - colorTextLinkDefault: BAND_LINK, + colorTextAccent: BAND.link, + colorTextLinkDefault: BAND.link, colorTextLinkHover: '#ffffff', - colorTextBodySecondary: BAND_TEXT_MUTED, - colorTextBreadcrumbCurrent: BAND_TEXT_MUTED, - colorTextBreadcrumbIcon: BAND_TEXT_MUTED, - colorTextInteractiveDefault: BAND_TEXT_MUTED, + colorTextBodySecondary: BAND.textMuted, + colorTextBreadcrumbCurrent: BAND.textMuted, + colorTextBreadcrumbIcon: BAND.textMuted, + colorTextInteractiveDefault: BAND.textMuted, colorTextInteractiveHover: '#ffffff', colorTextInteractiveActive: '#ffffff', - colorBorderItemFocused: BAND_LINK, + colorBorderItemFocused: BAND.link, }; } @@ -299,29 +301,40 @@ export const APP_THEME: Theme = { header: { tokens: { ...headerBandTokens(), - colorBackgroundLayoutMain: BAND_BACKGROUND, + colorBackgroundLayoutMain: BAND.background, }, }, 'top-navigation': { tokens: { ...headerBandTokens(), - colorBackgroundContainerContent: BAND_BACKGROUND, - colorTextHeadingDefault: BAND_TEXT, - colorTextTopNavigationTitle: BAND_TEXT, - colorTextInteractiveDefault: BAND_TEXT, + colorBackgroundContainerContent: BAND.background, + colorTextHeadingDefault: BAND.text, + colorTextTopNavigationTitle: BAND.text, + colorTextInteractiveDefault: BAND.text, colorTextInteractiveHover: '#ffffff', - colorBorderDividerDefault: '#2a2450', + colorBorderDividerDefault: BAND.divider, }, }, }, }; /** - * The same roles for the Designer and Execution tabs, which sit on the band - * but are page content, so the header context does not reach them. + * The same roles for the strip of the Designer and Execution tabs, which is + * page content that sits on the band, so the header context does not reach + * it. The selected-tab indicator is set too. */ export const TAB_STRIP_THEME: Theme = { tokens: { ...headerBandTokens(), + colorBorderItemSelected: BAND.link, }, }; + +/** + * The `style` of the page tabs: the line between two tabs, in the colour of + * the other hairlines of the band. (The rule under the strip is set in + * `chrome.css`.) + */ +export const TAB_STRIP_STYLE = { + tabSeparator: { color: BAND.divider }, +} as const; diff --git a/workflow-canvas/test-support/canvas-contrast.test.ts b/workflow-canvas/test-support/canvas-contrast.test.ts index a8fddfb0..ef267f3a 100644 --- a/workflow-canvas/test-support/canvas-contrast.test.ts +++ b/workflow-canvas/test-support/canvas-contrast.test.ts @@ -2,62 +2,89 @@ import { readFileSync } from 'node:fs'; import { describe, expect, it } from 'vitest'; import { AA_GRAPHIC, AA_TEXT, contrastRatio } from '../src/theme/contrast.ts'; +import { colorsOf, RADIUS, type ThemeMode } from '../src/theme/tokens.ts'; const css = readFileSync( new URL('../src/canvas/canvas.css', import.meta.url), 'utf8', ); -describe('canvas.css colours', () => { - // The declarations of the rule that starts with `selector`. - function variables(selector: string): Record { - const block = css.split(`${selector} {`)[1]?.split('}')[0] ?? ''; - return Object.fromEntries( - [...block.matchAll(/(--[\w-]+):\s*(#[0-9a-f]{6})/g)].map((m) => [ - m[1], - m[2], - ]), - ); - } +// The declarations of the rule that starts with `selector`. +function block(selector: string): string { + return css.split(`${selector} {`)[1]?.split('}')[0] ?? ''; +} - const light = variables('.wf-canvas'); - const dark = { ...light, ...variables('.awsui-dark-mode .wf-canvas') }; +function variables(selector: string): Record { + return Object.fromEntries( + [...block(selector).matchAll(/(--[\w-]+):\s*(#[0-9a-f]{6})/g)].map((m) => [ + m[1], + m[2], + ]), + ); +} - it.each([ - ['light', light], - ['dark', dark], - ])('keeps the canvas text readable in %s', (_mode, v) => { - expect( - contrastRatio(v['--wf-text']!, v['--wf-node-bg']!), - ).toBeGreaterThanOrEqual(AA_TEXT); - expect( - contrastRatio(v['--wf-text-muted']!, v['--wf-node-bg']!), - ).toBeGreaterThanOrEqual(AA_TEXT); - expect( - contrastRatio(v['--wf-text-muted']!, v['--wf-canvas-bg']!), - ).toBeGreaterThanOrEqual(AA_TEXT); - expect( - contrastRatio(v['--wf-text']!, v['--wf-controls-bg']!), - ).toBeGreaterThanOrEqual(AA_TEXT); +const light = variables('.wf-canvas'); +const dark = { ...light, ...variables(":root[data-theme='dark'] .wf-canvas") }; +const MODES: [ThemeMode, Record][] = [ + ['light', light], + ['dark', dark], +]; + +describe.each(MODES)('canvas.css colours in %s', (mode, v) => { + const ratio = (a: string, b: string) => contrastRatio(v[a]!, v[b]!); + + it('keeps the canvas text readable', () => { + expect(ratio('--wf-text', '--wf-node-bg')).toBeGreaterThanOrEqual(AA_TEXT); + expect(ratio('--wf-text-muted', '--wf-node-bg')).toBeGreaterThanOrEqual( + AA_TEXT, + ); + expect(ratio('--wf-text-muted', '--wf-canvas-bg')).toBeGreaterThanOrEqual( + AA_TEXT, + ); + expect(ratio('--wf-text', '--wf-controls-bg')).toBeGreaterThanOrEqual( + AA_TEXT, + ); }); - it.each([ - ['light', light], - ['dark', dark], - ])('keeps nodes, edges and the focus ring visible in %s', (_mode, v) => { + it('keeps nodes, edges and the focus ring visible', () => { for (const name of ['--wf-node-border', '--wf-edge', '--wf-focus']) { - expect( - contrastRatio(v[name]!, v['--wf-canvas-bg']!), - ).toBeGreaterThanOrEqual(AA_GRAPHIC); + expect(ratio(name, '--wf-canvas-bg')).toBeGreaterThanOrEqual(AA_GRAPHIC); } }); it('keeps the port letters readable on their ports', () => { expect( - contrastRatio(light['--wf-port-letter-out']!, light['--wf-port-fill']!), + ratio('--wf-port-letter-out', '--wf-port-fill'), ).toBeGreaterThanOrEqual(AA_TEXT); expect( - contrastRatio(light['--wf-port-letter-in']!, light['--wf-port-inner']!), + ratio('--wf-port-letter-in', '--wf-port-inner'), ).toBeGreaterThanOrEqual(AA_TEXT); }); + + it('keeps the ports visible on the canvas and on the node', () => { + for (const part of ['--wf-port-fill', '--wf-port-ring']) { + expect(ratio(part, '--wf-canvas-bg')).toBeGreaterThanOrEqual(AA_GRAPHIC); + expect(ratio(part, '--wf-node-bg')).toBeGreaterThanOrEqual(AA_GRAPHIC); + } + }); + + it('uses the colours of the theme module', () => { + const c = colorsOf(mode); + + expect(v['--wf-canvas-bg']).toBe(c.bg); + expect(v['--wf-node-bg']).toBe(c.surface); + expect(v['--wf-node-border']).toBe(c.control); + expect(v['--wf-text']).toBe(c.text); + expect(v['--wf-focus']).toBe(c.link); + expect(v['--wf-controls-bg']).toBe(c.surface); + expect(v['--wf-controls-border']).toBe(c.borderStrong); + expect(v['--wf-controls-bg-hover']).toBe(c.infoBg); + }); +}); + +describe('canvas.css shapes', () => { + it('uses the radii of the theme module', () => { + expect(block('.wf-canvas')).toContain(`border-radius: ${RADIUS.card}`); + expect(block('.wf-node')).toContain(`border-radius: ${RADIUS.control}`); + }); }); diff --git a/workflow-canvas/test-support/early-mode.test.ts b/workflow-canvas/test-support/early-mode.test.ts new file mode 100644 index 00000000..6443c5e2 --- /dev/null +++ b/workflow-canvas/test-support/early-mode.test.ts @@ -0,0 +1,55 @@ +// SPDX-License-Identifier: Apache-2.0 +import { readFileSync } from 'node:fs'; +import { describe, expect, it } from 'vitest'; +import { MODE_STORAGE_KEY } from '../src/theme/mode.ts'; + +const html = readFileSync(new URL('../index.html', import.meta.url), 'utf8'); +const script = /
diff --git a/workflow-canvas/src/App.test.tsx b/workflow-canvas/src/App.test.tsx index 1f151f3c..698c05ed 100644 --- a/workflow-canvas/src/App.test.tsx +++ b/workflow-canvas/src/App.test.tsx @@ -20,6 +20,7 @@ const themeButton = (mode: 'light' | 'dark') => afterEach(() => { window.localStorage.clear(); document.body.classList.remove('awsui-dark-mode'); + delete document.documentElement.dataset.theme; document.documentElement.style.colorScheme = ''; }); @@ -109,17 +110,17 @@ describe('App', () => { ); }); - it('shows the breadcrumb Workflows > the open workflow', async () => { + it('shows the breadcrumb Workflows > the open workflow, with no dead link', async () => { render(); const trail = await screen.findByRole('navigation', { name: 'Breadcrumbs', }); - expect(within(trail).getAllByText('Workflows').length).toBeGreaterThan(0); - // The trail holds a hidden copy that it measures, so the name appears twice. - expect((await within(trail).findAllByText('hello')).length).toBeGreaterThan( - 0, - ); + expect(within(trail).getByText('Workflows')).toBeTruthy(); + expect( + (await within(trail).findByText('hello')).closest('[aria-current]'), + ).toBeTruthy(); + expect(within(trail).queryAllByRole('link')).toEqual([]); }); it('switches to the dark mode, remembers it and switches back', () => { @@ -129,12 +130,14 @@ describe('App', () => { expect(document.body.classList.contains('awsui-dark-mode')).toBe(true); expect(document.documentElement.style.colorScheme).toBe('dark'); + expect(document.documentElement.dataset.theme).toBe('dark'); expect(window.localStorage.getItem(MODE_STORAGE_KEY)).toBe('dark'); expect(themeButton('dark').textContent).toBe('Theme: dark'); fireEvent.click(themeButton('dark')); expect(document.body.classList.contains('awsui-dark-mode')).toBe(false); + expect(document.documentElement.dataset.theme).toBe('light'); expect(window.localStorage.getItem(MODE_STORAGE_KEY)).toBe('light'); }); diff --git a/workflow-canvas/src/App.tsx b/workflow-canvas/src/App.tsx index 274f0003..f8c053c0 100644 --- a/workflow-canvas/src/App.tsx +++ b/workflow-canvas/src/App.tsx @@ -5,9 +5,11 @@ import ContentLayout from '@cloudscape-design/components/content-layout'; import Header from '@cloudscape-design/components/header'; import Tabs from '@cloudscape-design/components/tabs'; import { Breadcrumbs } from './chrome/Breadcrumbs'; +import './chrome/chrome.css'; import { TOP_BAR_ID, TopBar } from './chrome/TopBar'; import { Designer } from './designer/Designer'; import { useSettled } from './designer/use-settled'; +import { TAB_STRIP_STYLE } from './theme/tokens'; import { useThemeMode } from './theme/use-theme-mode'; import { openWorkflowName, type LoadResult } from './workflow/load-workflow'; @@ -44,6 +46,7 @@ export function App({ load }: { load: Promise }) {
+ {kind === 'tab' && } {kind === 'notch' && ( <> diff --git a/workflow-canvas/src/chrome/Breadcrumbs.tsx b/workflow-canvas/src/chrome/Breadcrumbs.tsx index 55fcaceb..71da872f 100644 --- a/workflow-canvas/src/chrome/Breadcrumbs.tsx +++ b/workflow-canvas/src/chrome/Breadcrumbs.tsx @@ -1,18 +1,36 @@ // SPDX-License-Identifier: Apache-2.0 -import BreadcrumbGroup from '@cloudscape-design/components/breadcrumb-group'; +import Box from '@cloudscape-design/components/box'; +import Icon from '@cloudscape-design/components/icon'; /** * The trail above the title: Workflows, then the workflow that is open. The - * page has no list of workflows yet, so both stay on this page. + * page has no list of workflows yet, so no crumb is a link; the last one is + * the current page. */ export function Breadcrumbs({ workflowName }: { workflowName?: string }) { - const items = [{ text: 'Workflows', href: '#' }]; - if (workflowName) items.push({ text: workflowName, href: '#' }); return ( - event.preventDefault()} - /> + ); } diff --git a/workflow-canvas/src/chrome/TopBar.tsx b/workflow-canvas/src/chrome/TopBar.tsx index 3cd89156..d4b450e9 100644 --- a/workflow-canvas/src/chrome/TopBar.tsx +++ b/workflow-canvas/src/chrome/TopBar.tsx @@ -23,6 +23,7 @@ export function TopBar({ event.preventDefault(), title: 'Workflow canvas', logo: { src: LOGO_DATA_URI, alt: 'Konductor' }, }} diff --git a/workflow-canvas/src/chrome/chrome.css b/workflow-canvas/src/chrome/chrome.css new file mode 100644 index 00000000..f990720d --- /dev/null +++ b/workflow-canvas/src/chrome/chrome.css @@ -0,0 +1,27 @@ +/* SPDX-License-Identifier: Apache-2.0 */ + +/* + * The strip of the Designer and Execution tabs is part of the dark header + * band: it is painted with the band colour across the full width of the + * page, so the tab labels, the selected-tab indicator and the rule under + * them all sit on the band. The colours come from the theme module through + * `--wf-band-background` and `--wf-band-divider`. + */ +.wf-tab-strip :has(> * > [role='tablist']) { + background: var(--wf-band-background); + box-shadow: 0 0 0 100vmax var(--wf-band-background); + /* The library sets this colour with a more specific rule. */ + border-block-end-color: var(--wf-band-divider) !important; + clip-path: inset(0 -100vmax); +} + +/* The trail above the title. */ +.wf-crumbs { + display: flex; + flex-wrap: wrap; + align-items: center; + gap: 8px; + margin: 0; + padding: 0; + list-style: none; +} diff --git a/workflow-canvas/src/theme/install.ts b/workflow-canvas/src/theme/install.ts index 0e9e6152..2ebc59ba 100644 --- a/workflow-canvas/src/theme/install.ts +++ b/workflow-canvas/src/theme/install.ts @@ -3,13 +3,24 @@ import { applyMode, Mode } from '@cloudscape-design/global-styles'; import { applyTheme } from '@cloudscape-design/components/theming'; import { APP_THEME, + BAND, FONT_TEXT, TAB_STRIP_THEME, type ThemeMode, } from './tokens'; -/** Where the tab strip of the page header is, for `TAB_STRIP_THEME`. */ -export const TAB_STRIP_SELECTOR = '.wf-tab-strip [role="tablist"]'; +/** + * Where the strip of the page tabs is, for `TAB_STRIP_THEME`: the element + * that holds the tab list, found through its role so no library class is + * needed. `chrome.css` paints the same element with the band. + */ +export const TAB_STRIP_SELECTOR = '.wf-tab-strip :has(> * > [role="tablist"])'; + +// The band colours that `chrome.css` reads, as custom properties. +const BAND_VARIABLES = { + '--wf-band-background': BAND.background, + '--wf-band-divider': BAND.divider, +} as const; /** * Applies the konductor docs theme to the page. Call it once, before the @@ -18,12 +29,19 @@ export const TAB_STRIP_SELECTOR = '.wf-tab-strip [role="tablist"]'; */ export function installTheme(body: HTMLElement): void { body.style.fontFamily = FONT_TEXT; + for (const [name, value] of Object.entries(BAND_VARIABLES)) { + body.style.setProperty(name, value); + } applyTheme({ theme: APP_THEME }); applyTheme({ theme: TAB_STRIP_THEME, selector: TAB_STRIP_SELECTOR }); } -/** Switches the page, its components and its native controls to `mode`. */ +/** + * Switches the page, its components and its native controls to `mode`. The + * root's `data-theme` is what the canvas styles key on. + */ export function showMode(mode: ThemeMode, root: HTMLElement): void { applyMode(mode === 'dark' ? Mode.Dark : Mode.Light); root.style.colorScheme = mode; + root.dataset.theme = mode; } diff --git a/workflow-canvas/src/theme/mode.ts b/workflow-canvas/src/theme/mode.ts index 23592863..987cee84 100644 --- a/workflow-canvas/src/theme/mode.ts +++ b/workflow-canvas/src/theme/mode.ts @@ -14,18 +14,11 @@ function isMode(value: unknown): value is ThemeMode { return value === 'light' || value === 'dark'; } -// Reading `localStorage` can throw when the browser blocks storage. -function storageOf(env: ModeEnvironment) { - try { - return env.localStorage; - } catch { - return undefined; - } -} - +// Reading `localStorage` can throw when the browser blocks storage, so the +// read of the property is inside the `try` too. function storedMode(env: ModeEnvironment): ThemeMode | undefined { try { - const value = storageOf(env)?.getItem(MODE_STORAGE_KEY); + const value = env.localStorage?.getItem(MODE_STORAGE_KEY); return isMode(value) ? value : undefined; } catch { return undefined; @@ -53,7 +46,7 @@ export function otherMode(mode: ThemeMode): ThemeMode { /** Keeps a chosen mode for the next visit. A blocked storage is ignored. */ export function rememberMode(env: ModeEnvironment, mode: ThemeMode): void { try { - storageOf(env)?.setItem(MODE_STORAGE_KEY, mode); + env.localStorage?.setItem(MODE_STORAGE_KEY, mode); } catch { // The choice then lasts until the page closes. } diff --git a/workflow-canvas/src/theme/tokens.test.ts b/workflow-canvas/src/theme/tokens.test.ts index 5f192bda..90ffca81 100644 --- a/workflow-canvas/src/theme/tokens.test.ts +++ b/workflow-canvas/src/theme/tokens.test.ts @@ -1,7 +1,14 @@ // SPDX-License-Identifier: Apache-2.0 import { describe, expect, it } from 'vitest'; import { AA_GRAPHIC, AA_TEXT, contrastRatio } from './contrast'; -import { APP_THEME, BAND_BACKGROUND, colorsOf, type ThemeMode } from './tokens'; +import { + APP_THEME, + BAND, + colorsOf, + TAB_STRIP_STYLE, + TAB_STRIP_THEME, + type ThemeMode, +} from './tokens'; const MODES: ThemeMode[] = ['light', 'dark']; @@ -41,17 +48,32 @@ describe.each(MODES)('the %s palette', (mode) => { }); describe('the header band', () => { - it.each([ - ['#f5f3ee', 'text'], - ['#c9c2e0', 'muted text'], - ['#c4b2ff', 'links'], - ])('has AA contrast for %s (%s)', (colour) => { - expect(contrastRatio(colour, BAND_BACKGROUND)).toBeGreaterThanOrEqual( - AA_TEXT, + it.each([BAND.text, BAND.textMuted, BAND.link])( + 'has AA contrast for %s', + (colour) => { + expect(contrastRatio(colour, BAND.background)).toBeGreaterThanOrEqual( + AA_TEXT, + ); + }, + ); + + it('draws the selected-tab indicator at 3:1 or more on the band', () => { + expect(contrastRatio(BAND.link, BAND.background)).toBeGreaterThanOrEqual( + AA_GRAPHIC, ); }); }); +describe('TAB_STRIP_THEME', () => { + it('colours the selected-tab indicator from the band', () => { + expect(TAB_STRIP_THEME.tokens.colorBorderItemSelected).toBe(BAND.link); + }); + + it('colours the line between tabs from the band', () => { + expect(TAB_STRIP_STYLE.tabSeparator.color).toBe(BAND.divider); + }); +}); + describe('APP_THEME', () => { it('gives the base font to the page', () => { expect(APP_THEME.tokens.fontFamilyBase).toMatch(/^Outfit,/); diff --git a/workflow-canvas/src/theme/tokens.ts b/workflow-canvas/src/theme/tokens.ts index 623089b6..b2e6b7c2 100644 --- a/workflow-canvas/src/theme/tokens.ts +++ b/workflow-canvas/src/theme/tokens.ts @@ -105,13 +105,17 @@ export function colorsOf(mode: ThemeMode): Colors { /** * The header band and the top bar are dark in both modes. These are the - * colours of the text and controls on them; each pair reaches 4.5:1 on - * `BAND_BACKGROUND`. + * colours of the text and controls on them. Text and the selected-tab + * indicator reach their contrast (4.5:1 and 3:1) on `background`. */ -export const BAND_BACKGROUND = '#141026'; -const BAND_TEXT = '#f5f3ee'; -const BAND_TEXT_MUTED = '#c9c2e0'; -const BAND_LINK = '#c4b2ff'; +export const BAND = { + background: '#141026', + text: '#f5f3ee', + textMuted: '#c9c2e0', + link: '#c4b2ff', + /** The hairline under the top bar and under the tabs. */ + divider: '#2a2450', +} as const; /** The font of the page text. Self-hosted by `fonts.ts`. */ export const FONT_TEXT = @@ -134,7 +138,7 @@ export const LOGO_DATA_URI = `data:image/svg+xml;utf8,${encodeURIComponent( type Tokens = Theme['tokens']; -/** A token that has one value in light mode and one in dark mode. */ +// A token that has one value in light mode and one in dark mode. function perMode(pick: (colors: Colors) => string) { return { light: pick(LIGHT), dark: pick(DARK) }; } @@ -155,7 +159,7 @@ function backgroundTokens(): Tokens { colorBackgroundItemSelected: perMode((c) => c.tintStrong), colorBackgroundCellShaded: perMode((c) => c.surfaceAlt), colorBackgroundToggleDefault: perMode((c) => c.control), - colorBackgroundHomeHeader: BAND_BACKGROUND, + colorBackgroundHomeHeader: BAND.background, colorBackgroundStatusError: perMode((c) => c.errorBg), colorBackgroundStatusInfo: perMode((c) => c.infoBg), colorBackgroundStatusSuccess: perMode((c) => c.successBg), @@ -214,8 +218,8 @@ function textTokens(): Tokens { colorTextDropdownItemHighlighted: perMode((c) => c.text), colorTextDropdownItemSecondary: perMode((c) => c.textMuted), colorTextGroupLabel: perMode((c) => c.textMuted), - colorTextHomeHeaderDefault: BAND_TEXT, - colorTextHomeHeaderSecondary: BAND_TEXT_MUTED, + colorTextHomeHeaderDefault: BAND.text, + colorTextHomeHeaderSecondary: BAND.textMuted, colorTextBreadcrumbCurrent: perMode((c) => c.textMuted), colorTextBreadcrumbIcon: perMode((c) => c.textMuted), }; @@ -263,23 +267,21 @@ function shapeTokens(): Tokens { }; } -/** - * Inside the dark band the global text and border colours do not apply, so - * the same roles are set again here. Cloudscape reads these in the page - * header, which holds the breadcrumbs and the title. - */ +// Inside the dark band the global text and border colours do not apply, so +// the same roles are set again here. Cloudscape reads these in the page +// header, which holds the breadcrumbs and the title. function headerBandTokens(): Tokens { return { - colorTextAccent: BAND_LINK, - colorTextLinkDefault: BAND_LINK, + colorTextAccent: BAND.link, + colorTextLinkDefault: BAND.link, colorTextLinkHover: '#ffffff', - colorTextBodySecondary: BAND_TEXT_MUTED, - colorTextBreadcrumbCurrent: BAND_TEXT_MUTED, - colorTextBreadcrumbIcon: BAND_TEXT_MUTED, - colorTextInteractiveDefault: BAND_TEXT_MUTED, + colorTextBodySecondary: BAND.textMuted, + colorTextBreadcrumbCurrent: BAND.textMuted, + colorTextBreadcrumbIcon: BAND.textMuted, + colorTextInteractiveDefault: BAND.textMuted, colorTextInteractiveHover: '#ffffff', colorTextInteractiveActive: '#ffffff', - colorBorderItemFocused: BAND_LINK, + colorBorderItemFocused: BAND.link, }; } @@ -299,29 +301,40 @@ export const APP_THEME: Theme = { header: { tokens: { ...headerBandTokens(), - colorBackgroundLayoutMain: BAND_BACKGROUND, + colorBackgroundLayoutMain: BAND.background, }, }, 'top-navigation': { tokens: { ...headerBandTokens(), - colorBackgroundContainerContent: BAND_BACKGROUND, - colorTextHeadingDefault: BAND_TEXT, - colorTextTopNavigationTitle: BAND_TEXT, - colorTextInteractiveDefault: BAND_TEXT, + colorBackgroundContainerContent: BAND.background, + colorTextHeadingDefault: BAND.text, + colorTextTopNavigationTitle: BAND.text, + colorTextInteractiveDefault: BAND.text, colorTextInteractiveHover: '#ffffff', - colorBorderDividerDefault: '#2a2450', + colorBorderDividerDefault: BAND.divider, }, }, }, }; /** - * The same roles for the Designer and Execution tabs, which sit on the band - * but are page content, so the header context does not reach them. + * The same roles for the strip of the Designer and Execution tabs, which is + * page content that sits on the band, so the header context does not reach + * it. The selected-tab indicator is set too. */ export const TAB_STRIP_THEME: Theme = { tokens: { ...headerBandTokens(), + colorBorderItemSelected: BAND.link, }, }; + +/** + * The `style` of the page tabs: the line between two tabs, in the colour of + * the other hairlines of the band. (The rule under the strip is set in + * `chrome.css`.) + */ +export const TAB_STRIP_STYLE = { + tabSeparator: { color: BAND.divider }, +} as const; diff --git a/workflow-canvas/test-support/canvas-contrast.test.ts b/workflow-canvas/test-support/canvas-contrast.test.ts index a8fddfb0..ef267f3a 100644 --- a/workflow-canvas/test-support/canvas-contrast.test.ts +++ b/workflow-canvas/test-support/canvas-contrast.test.ts @@ -2,62 +2,89 @@ import { readFileSync } from 'node:fs'; import { describe, expect, it } from 'vitest'; import { AA_GRAPHIC, AA_TEXT, contrastRatio } from '../src/theme/contrast.ts'; +import { colorsOf, RADIUS, type ThemeMode } from '../src/theme/tokens.ts'; const css = readFileSync( new URL('../src/canvas/canvas.css', import.meta.url), 'utf8', ); -describe('canvas.css colours', () => { - // The declarations of the rule that starts with `selector`. - function variables(selector: string): Record { - const block = css.split(`${selector} {`)[1]?.split('}')[0] ?? ''; - return Object.fromEntries( - [...block.matchAll(/(--[\w-]+):\s*(#[0-9a-f]{6})/g)].map((m) => [ - m[1], - m[2], - ]), - ); - } +// The declarations of the rule that starts with `selector`. +function block(selector: string): string { + return css.split(`${selector} {`)[1]?.split('}')[0] ?? ''; +} - const light = variables('.wf-canvas'); - const dark = { ...light, ...variables('.awsui-dark-mode .wf-canvas') }; +function variables(selector: string): Record { + return Object.fromEntries( + [...block(selector).matchAll(/(--[\w-]+):\s*(#[0-9a-f]{6})/g)].map((m) => [ + m[1], + m[2], + ]), + ); +} - it.each([ - ['light', light], - ['dark', dark], - ])('keeps the canvas text readable in %s', (_mode, v) => { - expect( - contrastRatio(v['--wf-text']!, v['--wf-node-bg']!), - ).toBeGreaterThanOrEqual(AA_TEXT); - expect( - contrastRatio(v['--wf-text-muted']!, v['--wf-node-bg']!), - ).toBeGreaterThanOrEqual(AA_TEXT); - expect( - contrastRatio(v['--wf-text-muted']!, v['--wf-canvas-bg']!), - ).toBeGreaterThanOrEqual(AA_TEXT); - expect( - contrastRatio(v['--wf-text']!, v['--wf-controls-bg']!), - ).toBeGreaterThanOrEqual(AA_TEXT); +const light = variables('.wf-canvas'); +const dark = { ...light, ...variables(":root[data-theme='dark'] .wf-canvas") }; +const MODES: [ThemeMode, Record][] = [ + ['light', light], + ['dark', dark], +]; + +describe.each(MODES)('canvas.css colours in %s', (mode, v) => { + const ratio = (a: string, b: string) => contrastRatio(v[a]!, v[b]!); + + it('keeps the canvas text readable', () => { + expect(ratio('--wf-text', '--wf-node-bg')).toBeGreaterThanOrEqual(AA_TEXT); + expect(ratio('--wf-text-muted', '--wf-node-bg')).toBeGreaterThanOrEqual( + AA_TEXT, + ); + expect(ratio('--wf-text-muted', '--wf-canvas-bg')).toBeGreaterThanOrEqual( + AA_TEXT, + ); + expect(ratio('--wf-text', '--wf-controls-bg')).toBeGreaterThanOrEqual( + AA_TEXT, + ); }); - it.each([ - ['light', light], - ['dark', dark], - ])('keeps nodes, edges and the focus ring visible in %s', (_mode, v) => { + it('keeps nodes, edges and the focus ring visible', () => { for (const name of ['--wf-node-border', '--wf-edge', '--wf-focus']) { - expect( - contrastRatio(v[name]!, v['--wf-canvas-bg']!), - ).toBeGreaterThanOrEqual(AA_GRAPHIC); + expect(ratio(name, '--wf-canvas-bg')).toBeGreaterThanOrEqual(AA_GRAPHIC); } }); it('keeps the port letters readable on their ports', () => { expect( - contrastRatio(light['--wf-port-letter-out']!, light['--wf-port-fill']!), + ratio('--wf-port-letter-out', '--wf-port-fill'), ).toBeGreaterThanOrEqual(AA_TEXT); expect( - contrastRatio(light['--wf-port-letter-in']!, light['--wf-port-inner']!), + ratio('--wf-port-letter-in', '--wf-port-inner'), ).toBeGreaterThanOrEqual(AA_TEXT); }); + + it('keeps the ports visible on the canvas and on the node', () => { + for (const part of ['--wf-port-fill', '--wf-port-ring']) { + expect(ratio(part, '--wf-canvas-bg')).toBeGreaterThanOrEqual(AA_GRAPHIC); + expect(ratio(part, '--wf-node-bg')).toBeGreaterThanOrEqual(AA_GRAPHIC); + } + }); + + it('uses the colours of the theme module', () => { + const c = colorsOf(mode); + + expect(v['--wf-canvas-bg']).toBe(c.bg); + expect(v['--wf-node-bg']).toBe(c.surface); + expect(v['--wf-node-border']).toBe(c.control); + expect(v['--wf-text']).toBe(c.text); + expect(v['--wf-focus']).toBe(c.link); + expect(v['--wf-controls-bg']).toBe(c.surface); + expect(v['--wf-controls-border']).toBe(c.borderStrong); + expect(v['--wf-controls-bg-hover']).toBe(c.infoBg); + }); +}); + +describe('canvas.css shapes', () => { + it('uses the radii of the theme module', () => { + expect(block('.wf-canvas')).toContain(`border-radius: ${RADIUS.card}`); + expect(block('.wf-node')).toContain(`border-radius: ${RADIUS.control}`); + }); }); diff --git a/workflow-canvas/test-support/early-mode.test.ts b/workflow-canvas/test-support/early-mode.test.ts new file mode 100644 index 00000000..6443c5e2 --- /dev/null +++ b/workflow-canvas/test-support/early-mode.test.ts @@ -0,0 +1,55 @@ +// SPDX-License-Identifier: Apache-2.0 +import { readFileSync } from 'node:fs'; +import { describe, expect, it } from 'vitest'; +import { MODE_STORAGE_KEY } from '../src/theme/mode.ts'; + +const html = readFileSync(new URL('../index.html', import.meta.url), 'utf8'); +const script = /'], + [ + 'an upper case tag with an attribute', + '', + ], + ['an end tag with a space', ''], + ['an end tag with an attribute', ''], + ['a start tag with a space', ''], + ])('finds the body in %s', (_, html) => { + expect(inlineScriptOf(html)).toBe('a()'); + }); + + it('skips a script with no body, such as one that loads a file', () => { + expect( + inlineScriptOf( + '', + ), + ).toBe('b()'); + }); + + it('does not take a tag whose name only starts with script', () => { + expect(inlineScriptOf('a()')).toBe(''); + }); + + it('is empty when there is no script', () => { + expect(inlineScriptOf('

none

')).toBe(''); + }); +}); diff --git a/workflow-canvas/test-support/inline-script.ts b/workflow-canvas/test-support/inline-script.ts new file mode 100644 index 00000000..7faa9a6b --- /dev/null +++ b/workflow-canvas/test-support/inline-script.ts @@ -0,0 +1,13 @@ +// SPDX-License-Identifier: Apache-2.0 + +// Tag names are case-insensitive, a start tag may carry attributes, and an end +// tag may carry whitespace. +const SCRIPT = /]*>([\s\S]*?)<\/script\b[^>]*>/gi; + +/** The body of the first `
diff --git a/workflow-canvas/src/App.test.tsx b/workflow-canvas/src/App.test.tsx index 1f151f3c..698c05ed 100644 --- a/workflow-canvas/src/App.test.tsx +++ b/workflow-canvas/src/App.test.tsx @@ -20,6 +20,7 @@ const themeButton = (mode: 'light' | 'dark') => afterEach(() => { window.localStorage.clear(); document.body.classList.remove('awsui-dark-mode'); + delete document.documentElement.dataset.theme; document.documentElement.style.colorScheme = ''; }); @@ -109,17 +110,17 @@ describe('App', () => { ); }); - it('shows the breadcrumb Workflows > the open workflow', async () => { + it('shows the breadcrumb Workflows > the open workflow, with no dead link', async () => { render(); const trail = await screen.findByRole('navigation', { name: 'Breadcrumbs', }); - expect(within(trail).getAllByText('Workflows').length).toBeGreaterThan(0); - // The trail holds a hidden copy that it measures, so the name appears twice. - expect((await within(trail).findAllByText('hello')).length).toBeGreaterThan( - 0, - ); + expect(within(trail).getByText('Workflows')).toBeTruthy(); + expect( + (await within(trail).findByText('hello')).closest('[aria-current]'), + ).toBeTruthy(); + expect(within(trail).queryAllByRole('link')).toEqual([]); }); it('switches to the dark mode, remembers it and switches back', () => { @@ -129,12 +130,14 @@ describe('App', () => { expect(document.body.classList.contains('awsui-dark-mode')).toBe(true); expect(document.documentElement.style.colorScheme).toBe('dark'); + expect(document.documentElement.dataset.theme).toBe('dark'); expect(window.localStorage.getItem(MODE_STORAGE_KEY)).toBe('dark'); expect(themeButton('dark').textContent).toBe('Theme: dark'); fireEvent.click(themeButton('dark')); expect(document.body.classList.contains('awsui-dark-mode')).toBe(false); + expect(document.documentElement.dataset.theme).toBe('light'); expect(window.localStorage.getItem(MODE_STORAGE_KEY)).toBe('light'); }); diff --git a/workflow-canvas/src/App.tsx b/workflow-canvas/src/App.tsx index 274f0003..f8c053c0 100644 --- a/workflow-canvas/src/App.tsx +++ b/workflow-canvas/src/App.tsx @@ -5,9 +5,11 @@ import ContentLayout from '@cloudscape-design/components/content-layout'; import Header from '@cloudscape-design/components/header'; import Tabs from '@cloudscape-design/components/tabs'; import { Breadcrumbs } from './chrome/Breadcrumbs'; +import './chrome/chrome.css'; import { TOP_BAR_ID, TopBar } from './chrome/TopBar'; import { Designer } from './designer/Designer'; import { useSettled } from './designer/use-settled'; +import { TAB_STRIP_STYLE } from './theme/tokens'; import { useThemeMode } from './theme/use-theme-mode'; import { openWorkflowName, type LoadResult } from './workflow/load-workflow'; @@ -44,6 +46,7 @@ export function App({ load }: { load: Promise }) {
+ {kind === 'tab' && } {kind === 'notch' && ( <> diff --git a/workflow-canvas/src/chrome/Breadcrumbs.tsx b/workflow-canvas/src/chrome/Breadcrumbs.tsx index 55fcaceb..71da872f 100644 --- a/workflow-canvas/src/chrome/Breadcrumbs.tsx +++ b/workflow-canvas/src/chrome/Breadcrumbs.tsx @@ -1,18 +1,36 @@ // SPDX-License-Identifier: Apache-2.0 -import BreadcrumbGroup from '@cloudscape-design/components/breadcrumb-group'; +import Box from '@cloudscape-design/components/box'; +import Icon from '@cloudscape-design/components/icon'; /** * The trail above the title: Workflows, then the workflow that is open. The - * page has no list of workflows yet, so both stay on this page. + * page has no list of workflows yet, so no crumb is a link; the last one is + * the current page. */ export function Breadcrumbs({ workflowName }: { workflowName?: string }) { - const items = [{ text: 'Workflows', href: '#' }]; - if (workflowName) items.push({ text: workflowName, href: '#' }); return ( - event.preventDefault()} - /> + ); } diff --git a/workflow-canvas/src/chrome/TopBar.tsx b/workflow-canvas/src/chrome/TopBar.tsx index 3cd89156..d4b450e9 100644 --- a/workflow-canvas/src/chrome/TopBar.tsx +++ b/workflow-canvas/src/chrome/TopBar.tsx @@ -23,6 +23,7 @@ export function TopBar({ event.preventDefault(), title: 'Workflow canvas', logo: { src: LOGO_DATA_URI, alt: 'Konductor' }, }} diff --git a/workflow-canvas/src/chrome/chrome.css b/workflow-canvas/src/chrome/chrome.css new file mode 100644 index 00000000..f990720d --- /dev/null +++ b/workflow-canvas/src/chrome/chrome.css @@ -0,0 +1,27 @@ +/* SPDX-License-Identifier: Apache-2.0 */ + +/* + * The strip of the Designer and Execution tabs is part of the dark header + * band: it is painted with the band colour across the full width of the + * page, so the tab labels, the selected-tab indicator and the rule under + * them all sit on the band. The colours come from the theme module through + * `--wf-band-background` and `--wf-band-divider`. + */ +.wf-tab-strip :has(> * > [role='tablist']) { + background: var(--wf-band-background); + box-shadow: 0 0 0 100vmax var(--wf-band-background); + /* The library sets this colour with a more specific rule. */ + border-block-end-color: var(--wf-band-divider) !important; + clip-path: inset(0 -100vmax); +} + +/* The trail above the title. */ +.wf-crumbs { + display: flex; + flex-wrap: wrap; + align-items: center; + gap: 8px; + margin: 0; + padding: 0; + list-style: none; +} diff --git a/workflow-canvas/src/theme/install.ts b/workflow-canvas/src/theme/install.ts index 0e9e6152..2ebc59ba 100644 --- a/workflow-canvas/src/theme/install.ts +++ b/workflow-canvas/src/theme/install.ts @@ -3,13 +3,24 @@ import { applyMode, Mode } from '@cloudscape-design/global-styles'; import { applyTheme } from '@cloudscape-design/components/theming'; import { APP_THEME, + BAND, FONT_TEXT, TAB_STRIP_THEME, type ThemeMode, } from './tokens'; -/** Where the tab strip of the page header is, for `TAB_STRIP_THEME`. */ -export const TAB_STRIP_SELECTOR = '.wf-tab-strip [role="tablist"]'; +/** + * Where the strip of the page tabs is, for `TAB_STRIP_THEME`: the element + * that holds the tab list, found through its role so no library class is + * needed. `chrome.css` paints the same element with the band. + */ +export const TAB_STRIP_SELECTOR = '.wf-tab-strip :has(> * > [role="tablist"])'; + +// The band colours that `chrome.css` reads, as custom properties. +const BAND_VARIABLES = { + '--wf-band-background': BAND.background, + '--wf-band-divider': BAND.divider, +} as const; /** * Applies the konductor docs theme to the page. Call it once, before the @@ -18,12 +29,19 @@ export const TAB_STRIP_SELECTOR = '.wf-tab-strip [role="tablist"]'; */ export function installTheme(body: HTMLElement): void { body.style.fontFamily = FONT_TEXT; + for (const [name, value] of Object.entries(BAND_VARIABLES)) { + body.style.setProperty(name, value); + } applyTheme({ theme: APP_THEME }); applyTheme({ theme: TAB_STRIP_THEME, selector: TAB_STRIP_SELECTOR }); } -/** Switches the page, its components and its native controls to `mode`. */ +/** + * Switches the page, its components and its native controls to `mode`. The + * root's `data-theme` is what the canvas styles key on. + */ export function showMode(mode: ThemeMode, root: HTMLElement): void { applyMode(mode === 'dark' ? Mode.Dark : Mode.Light); root.style.colorScheme = mode; + root.dataset.theme = mode; } diff --git a/workflow-canvas/src/theme/mode.ts b/workflow-canvas/src/theme/mode.ts index 23592863..987cee84 100644 --- a/workflow-canvas/src/theme/mode.ts +++ b/workflow-canvas/src/theme/mode.ts @@ -14,18 +14,11 @@ function isMode(value: unknown): value is ThemeMode { return value === 'light' || value === 'dark'; } -// Reading `localStorage` can throw when the browser blocks storage. -function storageOf(env: ModeEnvironment) { - try { - return env.localStorage; - } catch { - return undefined; - } -} - +// Reading `localStorage` can throw when the browser blocks storage, so the +// read of the property is inside the `try` too. function storedMode(env: ModeEnvironment): ThemeMode | undefined { try { - const value = storageOf(env)?.getItem(MODE_STORAGE_KEY); + const value = env.localStorage?.getItem(MODE_STORAGE_KEY); return isMode(value) ? value : undefined; } catch { return undefined; @@ -53,7 +46,7 @@ export function otherMode(mode: ThemeMode): ThemeMode { /** Keeps a chosen mode for the next visit. A blocked storage is ignored. */ export function rememberMode(env: ModeEnvironment, mode: ThemeMode): void { try { - storageOf(env)?.setItem(MODE_STORAGE_KEY, mode); + env.localStorage?.setItem(MODE_STORAGE_KEY, mode); } catch { // The choice then lasts until the page closes. } diff --git a/workflow-canvas/src/theme/tokens.test.ts b/workflow-canvas/src/theme/tokens.test.ts index 5f192bda..90ffca81 100644 --- a/workflow-canvas/src/theme/tokens.test.ts +++ b/workflow-canvas/src/theme/tokens.test.ts @@ -1,7 +1,14 @@ // SPDX-License-Identifier: Apache-2.0 import { describe, expect, it } from 'vitest'; import { AA_GRAPHIC, AA_TEXT, contrastRatio } from './contrast'; -import { APP_THEME, BAND_BACKGROUND, colorsOf, type ThemeMode } from './tokens'; +import { + APP_THEME, + BAND, + colorsOf, + TAB_STRIP_STYLE, + TAB_STRIP_THEME, + type ThemeMode, +} from './tokens'; const MODES: ThemeMode[] = ['light', 'dark']; @@ -41,17 +48,32 @@ describe.each(MODES)('the %s palette', (mode) => { }); describe('the header band', () => { - it.each([ - ['#f5f3ee', 'text'], - ['#c9c2e0', 'muted text'], - ['#c4b2ff', 'links'], - ])('has AA contrast for %s (%s)', (colour) => { - expect(contrastRatio(colour, BAND_BACKGROUND)).toBeGreaterThanOrEqual( - AA_TEXT, + it.each([BAND.text, BAND.textMuted, BAND.link])( + 'has AA contrast for %s', + (colour) => { + expect(contrastRatio(colour, BAND.background)).toBeGreaterThanOrEqual( + AA_TEXT, + ); + }, + ); + + it('draws the selected-tab indicator at 3:1 or more on the band', () => { + expect(contrastRatio(BAND.link, BAND.background)).toBeGreaterThanOrEqual( + AA_GRAPHIC, ); }); }); +describe('TAB_STRIP_THEME', () => { + it('colours the selected-tab indicator from the band', () => { + expect(TAB_STRIP_THEME.tokens.colorBorderItemSelected).toBe(BAND.link); + }); + + it('colours the line between tabs from the band', () => { + expect(TAB_STRIP_STYLE.tabSeparator.color).toBe(BAND.divider); + }); +}); + describe('APP_THEME', () => { it('gives the base font to the page', () => { expect(APP_THEME.tokens.fontFamilyBase).toMatch(/^Outfit,/); diff --git a/workflow-canvas/src/theme/tokens.ts b/workflow-canvas/src/theme/tokens.ts index 623089b6..b2e6b7c2 100644 --- a/workflow-canvas/src/theme/tokens.ts +++ b/workflow-canvas/src/theme/tokens.ts @@ -105,13 +105,17 @@ export function colorsOf(mode: ThemeMode): Colors { /** * The header band and the top bar are dark in both modes. These are the - * colours of the text and controls on them; each pair reaches 4.5:1 on - * `BAND_BACKGROUND`. + * colours of the text and controls on them. Text and the selected-tab + * indicator reach their contrast (4.5:1 and 3:1) on `background`. */ -export const BAND_BACKGROUND = '#141026'; -const BAND_TEXT = '#f5f3ee'; -const BAND_TEXT_MUTED = '#c9c2e0'; -const BAND_LINK = '#c4b2ff'; +export const BAND = { + background: '#141026', + text: '#f5f3ee', + textMuted: '#c9c2e0', + link: '#c4b2ff', + /** The hairline under the top bar and under the tabs. */ + divider: '#2a2450', +} as const; /** The font of the page text. Self-hosted by `fonts.ts`. */ export const FONT_TEXT = @@ -134,7 +138,7 @@ export const LOGO_DATA_URI = `data:image/svg+xml;utf8,${encodeURIComponent( type Tokens = Theme['tokens']; -/** A token that has one value in light mode and one in dark mode. */ +// A token that has one value in light mode and one in dark mode. function perMode(pick: (colors: Colors) => string) { return { light: pick(LIGHT), dark: pick(DARK) }; } @@ -155,7 +159,7 @@ function backgroundTokens(): Tokens { colorBackgroundItemSelected: perMode((c) => c.tintStrong), colorBackgroundCellShaded: perMode((c) => c.surfaceAlt), colorBackgroundToggleDefault: perMode((c) => c.control), - colorBackgroundHomeHeader: BAND_BACKGROUND, + colorBackgroundHomeHeader: BAND.background, colorBackgroundStatusError: perMode((c) => c.errorBg), colorBackgroundStatusInfo: perMode((c) => c.infoBg), colorBackgroundStatusSuccess: perMode((c) => c.successBg), @@ -214,8 +218,8 @@ function textTokens(): Tokens { colorTextDropdownItemHighlighted: perMode((c) => c.text), colorTextDropdownItemSecondary: perMode((c) => c.textMuted), colorTextGroupLabel: perMode((c) => c.textMuted), - colorTextHomeHeaderDefault: BAND_TEXT, - colorTextHomeHeaderSecondary: BAND_TEXT_MUTED, + colorTextHomeHeaderDefault: BAND.text, + colorTextHomeHeaderSecondary: BAND.textMuted, colorTextBreadcrumbCurrent: perMode((c) => c.textMuted), colorTextBreadcrumbIcon: perMode((c) => c.textMuted), }; @@ -263,23 +267,21 @@ function shapeTokens(): Tokens { }; } -/** - * Inside the dark band the global text and border colours do not apply, so - * the same roles are set again here. Cloudscape reads these in the page - * header, which holds the breadcrumbs and the title. - */ +// Inside the dark band the global text and border colours do not apply, so +// the same roles are set again here. Cloudscape reads these in the page +// header, which holds the breadcrumbs and the title. function headerBandTokens(): Tokens { return { - colorTextAccent: BAND_LINK, - colorTextLinkDefault: BAND_LINK, + colorTextAccent: BAND.link, + colorTextLinkDefault: BAND.link, colorTextLinkHover: '#ffffff', - colorTextBodySecondary: BAND_TEXT_MUTED, - colorTextBreadcrumbCurrent: BAND_TEXT_MUTED, - colorTextBreadcrumbIcon: BAND_TEXT_MUTED, - colorTextInteractiveDefault: BAND_TEXT_MUTED, + colorTextBodySecondary: BAND.textMuted, + colorTextBreadcrumbCurrent: BAND.textMuted, + colorTextBreadcrumbIcon: BAND.textMuted, + colorTextInteractiveDefault: BAND.textMuted, colorTextInteractiveHover: '#ffffff', colorTextInteractiveActive: '#ffffff', - colorBorderItemFocused: BAND_LINK, + colorBorderItemFocused: BAND.link, }; } @@ -299,29 +301,40 @@ export const APP_THEME: Theme = { header: { tokens: { ...headerBandTokens(), - colorBackgroundLayoutMain: BAND_BACKGROUND, + colorBackgroundLayoutMain: BAND.background, }, }, 'top-navigation': { tokens: { ...headerBandTokens(), - colorBackgroundContainerContent: BAND_BACKGROUND, - colorTextHeadingDefault: BAND_TEXT, - colorTextTopNavigationTitle: BAND_TEXT, - colorTextInteractiveDefault: BAND_TEXT, + colorBackgroundContainerContent: BAND.background, + colorTextHeadingDefault: BAND.text, + colorTextTopNavigationTitle: BAND.text, + colorTextInteractiveDefault: BAND.text, colorTextInteractiveHover: '#ffffff', - colorBorderDividerDefault: '#2a2450', + colorBorderDividerDefault: BAND.divider, }, }, }, }; /** - * The same roles for the Designer and Execution tabs, which sit on the band - * but are page content, so the header context does not reach them. + * The same roles for the strip of the Designer and Execution tabs, which is + * page content that sits on the band, so the header context does not reach + * it. The selected-tab indicator is set too. */ export const TAB_STRIP_THEME: Theme = { tokens: { ...headerBandTokens(), + colorBorderItemSelected: BAND.link, }, }; + +/** + * The `style` of the page tabs: the line between two tabs, in the colour of + * the other hairlines of the band. (The rule under the strip is set in + * `chrome.css`.) + */ +export const TAB_STRIP_STYLE = { + tabSeparator: { color: BAND.divider }, +} as const; diff --git a/workflow-canvas/test-support/canvas-contrast.test.ts b/workflow-canvas/test-support/canvas-contrast.test.ts index a8fddfb0..ef267f3a 100644 --- a/workflow-canvas/test-support/canvas-contrast.test.ts +++ b/workflow-canvas/test-support/canvas-contrast.test.ts @@ -2,62 +2,89 @@ import { readFileSync } from 'node:fs'; import { describe, expect, it } from 'vitest'; import { AA_GRAPHIC, AA_TEXT, contrastRatio } from '../src/theme/contrast.ts'; +import { colorsOf, RADIUS, type ThemeMode } from '../src/theme/tokens.ts'; const css = readFileSync( new URL('../src/canvas/canvas.css', import.meta.url), 'utf8', ); -describe('canvas.css colours', () => { - // The declarations of the rule that starts with `selector`. - function variables(selector: string): Record { - const block = css.split(`${selector} {`)[1]?.split('}')[0] ?? ''; - return Object.fromEntries( - [...block.matchAll(/(--[\w-]+):\s*(#[0-9a-f]{6})/g)].map((m) => [ - m[1], - m[2], - ]), - ); - } +// The declarations of the rule that starts with `selector`. +function block(selector: string): string { + return css.split(`${selector} {`)[1]?.split('}')[0] ?? ''; +} - const light = variables('.wf-canvas'); - const dark = { ...light, ...variables('.awsui-dark-mode .wf-canvas') }; +function variables(selector: string): Record { + return Object.fromEntries( + [...block(selector).matchAll(/(--[\w-]+):\s*(#[0-9a-f]{6})/g)].map((m) => [ + m[1], + m[2], + ]), + ); +} - it.each([ - ['light', light], - ['dark', dark], - ])('keeps the canvas text readable in %s', (_mode, v) => { - expect( - contrastRatio(v['--wf-text']!, v['--wf-node-bg']!), - ).toBeGreaterThanOrEqual(AA_TEXT); - expect( - contrastRatio(v['--wf-text-muted']!, v['--wf-node-bg']!), - ).toBeGreaterThanOrEqual(AA_TEXT); - expect( - contrastRatio(v['--wf-text-muted']!, v['--wf-canvas-bg']!), - ).toBeGreaterThanOrEqual(AA_TEXT); - expect( - contrastRatio(v['--wf-text']!, v['--wf-controls-bg']!), - ).toBeGreaterThanOrEqual(AA_TEXT); +const light = variables('.wf-canvas'); +const dark = { ...light, ...variables(":root[data-theme='dark'] .wf-canvas") }; +const MODES: [ThemeMode, Record][] = [ + ['light', light], + ['dark', dark], +]; + +describe.each(MODES)('canvas.css colours in %s', (mode, v) => { + const ratio = (a: string, b: string) => contrastRatio(v[a]!, v[b]!); + + it('keeps the canvas text readable', () => { + expect(ratio('--wf-text', '--wf-node-bg')).toBeGreaterThanOrEqual(AA_TEXT); + expect(ratio('--wf-text-muted', '--wf-node-bg')).toBeGreaterThanOrEqual( + AA_TEXT, + ); + expect(ratio('--wf-text-muted', '--wf-canvas-bg')).toBeGreaterThanOrEqual( + AA_TEXT, + ); + expect(ratio('--wf-text', '--wf-controls-bg')).toBeGreaterThanOrEqual( + AA_TEXT, + ); }); - it.each([ - ['light', light], - ['dark', dark], - ])('keeps nodes, edges and the focus ring visible in %s', (_mode, v) => { + it('keeps nodes, edges and the focus ring visible', () => { for (const name of ['--wf-node-border', '--wf-edge', '--wf-focus']) { - expect( - contrastRatio(v[name]!, v['--wf-canvas-bg']!), - ).toBeGreaterThanOrEqual(AA_GRAPHIC); + expect(ratio(name, '--wf-canvas-bg')).toBeGreaterThanOrEqual(AA_GRAPHIC); } }); it('keeps the port letters readable on their ports', () => { expect( - contrastRatio(light['--wf-port-letter-out']!, light['--wf-port-fill']!), + ratio('--wf-port-letter-out', '--wf-port-fill'), ).toBeGreaterThanOrEqual(AA_TEXT); expect( - contrastRatio(light['--wf-port-letter-in']!, light['--wf-port-inner']!), + ratio('--wf-port-letter-in', '--wf-port-inner'), ).toBeGreaterThanOrEqual(AA_TEXT); }); + + it('keeps the ports visible on the canvas and on the node', () => { + for (const part of ['--wf-port-fill', '--wf-port-ring']) { + expect(ratio(part, '--wf-canvas-bg')).toBeGreaterThanOrEqual(AA_GRAPHIC); + expect(ratio(part, '--wf-node-bg')).toBeGreaterThanOrEqual(AA_GRAPHIC); + } + }); + + it('uses the colours of the theme module', () => { + const c = colorsOf(mode); + + expect(v['--wf-canvas-bg']).toBe(c.bg); + expect(v['--wf-node-bg']).toBe(c.surface); + expect(v['--wf-node-border']).toBe(c.control); + expect(v['--wf-text']).toBe(c.text); + expect(v['--wf-focus']).toBe(c.link); + expect(v['--wf-controls-bg']).toBe(c.surface); + expect(v['--wf-controls-border']).toBe(c.borderStrong); + expect(v['--wf-controls-bg-hover']).toBe(c.infoBg); + }); +}); + +describe('canvas.css shapes', () => { + it('uses the radii of the theme module', () => { + expect(block('.wf-canvas')).toContain(`border-radius: ${RADIUS.card}`); + expect(block('.wf-node')).toContain(`border-radius: ${RADIUS.control}`); + }); }); diff --git a/workflow-canvas/test-support/early-mode.test.ts b/workflow-canvas/test-support/early-mode.test.ts new file mode 100644 index 00000000..6443c5e2 --- /dev/null +++ b/workflow-canvas/test-support/early-mode.test.ts @@ -0,0 +1,55 @@ +// SPDX-License-Identifier: Apache-2.0 +import { readFileSync } from 'node:fs'; +import { describe, expect, it } from 'vitest'; +import { MODE_STORAGE_KEY } from '../src/theme/mode.ts'; + +const html = readFileSync(new URL('../index.html', import.meta.url), 'utf8'); +const script = /'], + [ + 'an upper case tag with an attribute', + '', + ], + ['an end tag with a space', ''], + ['an end tag with an attribute', ''], + ['a start tag with a space', ''], + ])('finds the body in %s', (_, html) => { + expect(inlineScriptOf(html)).toBe('a()'); + }); + + it('skips a script with no body, such as one that loads a file', () => { + expect( + inlineScriptOf( + '', + ), + ).toBe('b()'); + }); + + it('does not take a tag whose name only starts with script', () => { + expect(inlineScriptOf('a()')).toBe(''); + }); + + it('is empty when there is no script', () => { + expect(inlineScriptOf('

none

')).toBe(''); + }); +}); diff --git a/workflow-canvas/test-support/inline-script.ts b/workflow-canvas/test-support/inline-script.ts new file mode 100644 index 00000000..7faa9a6b --- /dev/null +++ b/workflow-canvas/test-support/inline-script.ts @@ -0,0 +1,13 @@ +// SPDX-License-Identifier: Apache-2.0 + +// Tag names are case-insensitive, a start tag may carry attributes, and an end +// tag may carry whitespace. +const SCRIPT = /]*>([\s\S]*?)<\/script\b[^>]*>/gi; + +/** The body of the first `
diff --git a/workflow-canvas/src/App.test.tsx b/workflow-canvas/src/App.test.tsx index 1f151f3c..698c05ed 100644 --- a/workflow-canvas/src/App.test.tsx +++ b/workflow-canvas/src/App.test.tsx @@ -20,6 +20,7 @@ const themeButton = (mode: 'light' | 'dark') => afterEach(() => { window.localStorage.clear(); document.body.classList.remove('awsui-dark-mode'); + delete document.documentElement.dataset.theme; document.documentElement.style.colorScheme = ''; }); @@ -109,17 +110,17 @@ describe('App', () => { ); }); - it('shows the breadcrumb Workflows > the open workflow', async () => { + it('shows the breadcrumb Workflows > the open workflow, with no dead link', async () => { render(); const trail = await screen.findByRole('navigation', { name: 'Breadcrumbs', }); - expect(within(trail).getAllByText('Workflows').length).toBeGreaterThan(0); - // The trail holds a hidden copy that it measures, so the name appears twice. - expect((await within(trail).findAllByText('hello')).length).toBeGreaterThan( - 0, - ); + expect(within(trail).getByText('Workflows')).toBeTruthy(); + expect( + (await within(trail).findByText('hello')).closest('[aria-current]'), + ).toBeTruthy(); + expect(within(trail).queryAllByRole('link')).toEqual([]); }); it('switches to the dark mode, remembers it and switches back', () => { @@ -129,12 +130,14 @@ describe('App', () => { expect(document.body.classList.contains('awsui-dark-mode')).toBe(true); expect(document.documentElement.style.colorScheme).toBe('dark'); + expect(document.documentElement.dataset.theme).toBe('dark'); expect(window.localStorage.getItem(MODE_STORAGE_KEY)).toBe('dark'); expect(themeButton('dark').textContent).toBe('Theme: dark'); fireEvent.click(themeButton('dark')); expect(document.body.classList.contains('awsui-dark-mode')).toBe(false); + expect(document.documentElement.dataset.theme).toBe('light'); expect(window.localStorage.getItem(MODE_STORAGE_KEY)).toBe('light'); }); diff --git a/workflow-canvas/src/App.tsx b/workflow-canvas/src/App.tsx index b3326416..3f294dec 100644 --- a/workflow-canvas/src/App.tsx +++ b/workflow-canvas/src/App.tsx @@ -6,9 +6,11 @@ import Header from '@cloudscape-design/components/header'; import Tabs from '@cloudscape-design/components/tabs'; import type { ReactNode } from 'react'; import { Breadcrumbs } from './chrome/Breadcrumbs'; +import './chrome/chrome.css'; import { TOP_BAR_ID, TopBar } from './chrome/TopBar'; import { Designer } from './designer/Designer'; import { useSettled } from './designer/use-settled'; +import { TAB_STRIP_STYLE } from './theme/tokens'; import { useThemeMode } from './theme/use-theme-mode'; import { openWorkflowName, type LoadResult } from './workflow/load-workflow'; @@ -53,6 +55,7 @@ export function App({
+ {kind === 'tab' && } {kind === 'notch' && ( <> diff --git a/workflow-canvas/src/chrome/Breadcrumbs.tsx b/workflow-canvas/src/chrome/Breadcrumbs.tsx index 55fcaceb..71da872f 100644 --- a/workflow-canvas/src/chrome/Breadcrumbs.tsx +++ b/workflow-canvas/src/chrome/Breadcrumbs.tsx @@ -1,18 +1,36 @@ // SPDX-License-Identifier: Apache-2.0 -import BreadcrumbGroup from '@cloudscape-design/components/breadcrumb-group'; +import Box from '@cloudscape-design/components/box'; +import Icon from '@cloudscape-design/components/icon'; /** * The trail above the title: Workflows, then the workflow that is open. The - * page has no list of workflows yet, so both stay on this page. + * page has no list of workflows yet, so no crumb is a link; the last one is + * the current page. */ export function Breadcrumbs({ workflowName }: { workflowName?: string }) { - const items = [{ text: 'Workflows', href: '#' }]; - if (workflowName) items.push({ text: workflowName, href: '#' }); return ( - event.preventDefault()} - /> + ); } diff --git a/workflow-canvas/src/chrome/TopBar.tsx b/workflow-canvas/src/chrome/TopBar.tsx index 3cd89156..d4b450e9 100644 --- a/workflow-canvas/src/chrome/TopBar.tsx +++ b/workflow-canvas/src/chrome/TopBar.tsx @@ -23,6 +23,7 @@ export function TopBar({ event.preventDefault(), title: 'Workflow canvas', logo: { src: LOGO_DATA_URI, alt: 'Konductor' }, }} diff --git a/workflow-canvas/src/chrome/chrome.css b/workflow-canvas/src/chrome/chrome.css new file mode 100644 index 00000000..f990720d --- /dev/null +++ b/workflow-canvas/src/chrome/chrome.css @@ -0,0 +1,27 @@ +/* SPDX-License-Identifier: Apache-2.0 */ + +/* + * The strip of the Designer and Execution tabs is part of the dark header + * band: it is painted with the band colour across the full width of the + * page, so the tab labels, the selected-tab indicator and the rule under + * them all sit on the band. The colours come from the theme module through + * `--wf-band-background` and `--wf-band-divider`. + */ +.wf-tab-strip :has(> * > [role='tablist']) { + background: var(--wf-band-background); + box-shadow: 0 0 0 100vmax var(--wf-band-background); + /* The library sets this colour with a more specific rule. */ + border-block-end-color: var(--wf-band-divider) !important; + clip-path: inset(0 -100vmax); +} + +/* The trail above the title. */ +.wf-crumbs { + display: flex; + flex-wrap: wrap; + align-items: center; + gap: 8px; + margin: 0; + padding: 0; + list-style: none; +} diff --git a/workflow-canvas/src/theme/install.ts b/workflow-canvas/src/theme/install.ts index 0e9e6152..2ebc59ba 100644 --- a/workflow-canvas/src/theme/install.ts +++ b/workflow-canvas/src/theme/install.ts @@ -3,13 +3,24 @@ import { applyMode, Mode } from '@cloudscape-design/global-styles'; import { applyTheme } from '@cloudscape-design/components/theming'; import { APP_THEME, + BAND, FONT_TEXT, TAB_STRIP_THEME, type ThemeMode, } from './tokens'; -/** Where the tab strip of the page header is, for `TAB_STRIP_THEME`. */ -export const TAB_STRIP_SELECTOR = '.wf-tab-strip [role="tablist"]'; +/** + * Where the strip of the page tabs is, for `TAB_STRIP_THEME`: the element + * that holds the tab list, found through its role so no library class is + * needed. `chrome.css` paints the same element with the band. + */ +export const TAB_STRIP_SELECTOR = '.wf-tab-strip :has(> * > [role="tablist"])'; + +// The band colours that `chrome.css` reads, as custom properties. +const BAND_VARIABLES = { + '--wf-band-background': BAND.background, + '--wf-band-divider': BAND.divider, +} as const; /** * Applies the konductor docs theme to the page. Call it once, before the @@ -18,12 +29,19 @@ export const TAB_STRIP_SELECTOR = '.wf-tab-strip [role="tablist"]'; */ export function installTheme(body: HTMLElement): void { body.style.fontFamily = FONT_TEXT; + for (const [name, value] of Object.entries(BAND_VARIABLES)) { + body.style.setProperty(name, value); + } applyTheme({ theme: APP_THEME }); applyTheme({ theme: TAB_STRIP_THEME, selector: TAB_STRIP_SELECTOR }); } -/** Switches the page, its components and its native controls to `mode`. */ +/** + * Switches the page, its components and its native controls to `mode`. The + * root's `data-theme` is what the canvas styles key on. + */ export function showMode(mode: ThemeMode, root: HTMLElement): void { applyMode(mode === 'dark' ? Mode.Dark : Mode.Light); root.style.colorScheme = mode; + root.dataset.theme = mode; } diff --git a/workflow-canvas/src/theme/mode.ts b/workflow-canvas/src/theme/mode.ts index 23592863..987cee84 100644 --- a/workflow-canvas/src/theme/mode.ts +++ b/workflow-canvas/src/theme/mode.ts @@ -14,18 +14,11 @@ function isMode(value: unknown): value is ThemeMode { return value === 'light' || value === 'dark'; } -// Reading `localStorage` can throw when the browser blocks storage. -function storageOf(env: ModeEnvironment) { - try { - return env.localStorage; - } catch { - return undefined; - } -} - +// Reading `localStorage` can throw when the browser blocks storage, so the +// read of the property is inside the `try` too. function storedMode(env: ModeEnvironment): ThemeMode | undefined { try { - const value = storageOf(env)?.getItem(MODE_STORAGE_KEY); + const value = env.localStorage?.getItem(MODE_STORAGE_KEY); return isMode(value) ? value : undefined; } catch { return undefined; @@ -53,7 +46,7 @@ export function otherMode(mode: ThemeMode): ThemeMode { /** Keeps a chosen mode for the next visit. A blocked storage is ignored. */ export function rememberMode(env: ModeEnvironment, mode: ThemeMode): void { try { - storageOf(env)?.setItem(MODE_STORAGE_KEY, mode); + env.localStorage?.setItem(MODE_STORAGE_KEY, mode); } catch { // The choice then lasts until the page closes. } diff --git a/workflow-canvas/src/theme/tokens.test.ts b/workflow-canvas/src/theme/tokens.test.ts index 5f192bda..90ffca81 100644 --- a/workflow-canvas/src/theme/tokens.test.ts +++ b/workflow-canvas/src/theme/tokens.test.ts @@ -1,7 +1,14 @@ // SPDX-License-Identifier: Apache-2.0 import { describe, expect, it } from 'vitest'; import { AA_GRAPHIC, AA_TEXT, contrastRatio } from './contrast'; -import { APP_THEME, BAND_BACKGROUND, colorsOf, type ThemeMode } from './tokens'; +import { + APP_THEME, + BAND, + colorsOf, + TAB_STRIP_STYLE, + TAB_STRIP_THEME, + type ThemeMode, +} from './tokens'; const MODES: ThemeMode[] = ['light', 'dark']; @@ -41,17 +48,32 @@ describe.each(MODES)('the %s palette', (mode) => { }); describe('the header band', () => { - it.each([ - ['#f5f3ee', 'text'], - ['#c9c2e0', 'muted text'], - ['#c4b2ff', 'links'], - ])('has AA contrast for %s (%s)', (colour) => { - expect(contrastRatio(colour, BAND_BACKGROUND)).toBeGreaterThanOrEqual( - AA_TEXT, + it.each([BAND.text, BAND.textMuted, BAND.link])( + 'has AA contrast for %s', + (colour) => { + expect(contrastRatio(colour, BAND.background)).toBeGreaterThanOrEqual( + AA_TEXT, + ); + }, + ); + + it('draws the selected-tab indicator at 3:1 or more on the band', () => { + expect(contrastRatio(BAND.link, BAND.background)).toBeGreaterThanOrEqual( + AA_GRAPHIC, ); }); }); +describe('TAB_STRIP_THEME', () => { + it('colours the selected-tab indicator from the band', () => { + expect(TAB_STRIP_THEME.tokens.colorBorderItemSelected).toBe(BAND.link); + }); + + it('colours the line between tabs from the band', () => { + expect(TAB_STRIP_STYLE.tabSeparator.color).toBe(BAND.divider); + }); +}); + describe('APP_THEME', () => { it('gives the base font to the page', () => { expect(APP_THEME.tokens.fontFamilyBase).toMatch(/^Outfit,/); diff --git a/workflow-canvas/src/theme/tokens.ts b/workflow-canvas/src/theme/tokens.ts index 623089b6..b2e6b7c2 100644 --- a/workflow-canvas/src/theme/tokens.ts +++ b/workflow-canvas/src/theme/tokens.ts @@ -105,13 +105,17 @@ export function colorsOf(mode: ThemeMode): Colors { /** * The header band and the top bar are dark in both modes. These are the - * colours of the text and controls on them; each pair reaches 4.5:1 on - * `BAND_BACKGROUND`. + * colours of the text and controls on them. Text and the selected-tab + * indicator reach their contrast (4.5:1 and 3:1) on `background`. */ -export const BAND_BACKGROUND = '#141026'; -const BAND_TEXT = '#f5f3ee'; -const BAND_TEXT_MUTED = '#c9c2e0'; -const BAND_LINK = '#c4b2ff'; +export const BAND = { + background: '#141026', + text: '#f5f3ee', + textMuted: '#c9c2e0', + link: '#c4b2ff', + /** The hairline under the top bar and under the tabs. */ + divider: '#2a2450', +} as const; /** The font of the page text. Self-hosted by `fonts.ts`. */ export const FONT_TEXT = @@ -134,7 +138,7 @@ export const LOGO_DATA_URI = `data:image/svg+xml;utf8,${encodeURIComponent( type Tokens = Theme['tokens']; -/** A token that has one value in light mode and one in dark mode. */ +// A token that has one value in light mode and one in dark mode. function perMode(pick: (colors: Colors) => string) { return { light: pick(LIGHT), dark: pick(DARK) }; } @@ -155,7 +159,7 @@ function backgroundTokens(): Tokens { colorBackgroundItemSelected: perMode((c) => c.tintStrong), colorBackgroundCellShaded: perMode((c) => c.surfaceAlt), colorBackgroundToggleDefault: perMode((c) => c.control), - colorBackgroundHomeHeader: BAND_BACKGROUND, + colorBackgroundHomeHeader: BAND.background, colorBackgroundStatusError: perMode((c) => c.errorBg), colorBackgroundStatusInfo: perMode((c) => c.infoBg), colorBackgroundStatusSuccess: perMode((c) => c.successBg), @@ -214,8 +218,8 @@ function textTokens(): Tokens { colorTextDropdownItemHighlighted: perMode((c) => c.text), colorTextDropdownItemSecondary: perMode((c) => c.textMuted), colorTextGroupLabel: perMode((c) => c.textMuted), - colorTextHomeHeaderDefault: BAND_TEXT, - colorTextHomeHeaderSecondary: BAND_TEXT_MUTED, + colorTextHomeHeaderDefault: BAND.text, + colorTextHomeHeaderSecondary: BAND.textMuted, colorTextBreadcrumbCurrent: perMode((c) => c.textMuted), colorTextBreadcrumbIcon: perMode((c) => c.textMuted), }; @@ -263,23 +267,21 @@ function shapeTokens(): Tokens { }; } -/** - * Inside the dark band the global text and border colours do not apply, so - * the same roles are set again here. Cloudscape reads these in the page - * header, which holds the breadcrumbs and the title. - */ +// Inside the dark band the global text and border colours do not apply, so +// the same roles are set again here. Cloudscape reads these in the page +// header, which holds the breadcrumbs and the title. function headerBandTokens(): Tokens { return { - colorTextAccent: BAND_LINK, - colorTextLinkDefault: BAND_LINK, + colorTextAccent: BAND.link, + colorTextLinkDefault: BAND.link, colorTextLinkHover: '#ffffff', - colorTextBodySecondary: BAND_TEXT_MUTED, - colorTextBreadcrumbCurrent: BAND_TEXT_MUTED, - colorTextBreadcrumbIcon: BAND_TEXT_MUTED, - colorTextInteractiveDefault: BAND_TEXT_MUTED, + colorTextBodySecondary: BAND.textMuted, + colorTextBreadcrumbCurrent: BAND.textMuted, + colorTextBreadcrumbIcon: BAND.textMuted, + colorTextInteractiveDefault: BAND.textMuted, colorTextInteractiveHover: '#ffffff', colorTextInteractiveActive: '#ffffff', - colorBorderItemFocused: BAND_LINK, + colorBorderItemFocused: BAND.link, }; } @@ -299,29 +301,40 @@ export const APP_THEME: Theme = { header: { tokens: { ...headerBandTokens(), - colorBackgroundLayoutMain: BAND_BACKGROUND, + colorBackgroundLayoutMain: BAND.background, }, }, 'top-navigation': { tokens: { ...headerBandTokens(), - colorBackgroundContainerContent: BAND_BACKGROUND, - colorTextHeadingDefault: BAND_TEXT, - colorTextTopNavigationTitle: BAND_TEXT, - colorTextInteractiveDefault: BAND_TEXT, + colorBackgroundContainerContent: BAND.background, + colorTextHeadingDefault: BAND.text, + colorTextTopNavigationTitle: BAND.text, + colorTextInteractiveDefault: BAND.text, colorTextInteractiveHover: '#ffffff', - colorBorderDividerDefault: '#2a2450', + colorBorderDividerDefault: BAND.divider, }, }, }, }; /** - * The same roles for the Designer and Execution tabs, which sit on the band - * but are page content, so the header context does not reach them. + * The same roles for the strip of the Designer and Execution tabs, which is + * page content that sits on the band, so the header context does not reach + * it. The selected-tab indicator is set too. */ export const TAB_STRIP_THEME: Theme = { tokens: { ...headerBandTokens(), + colorBorderItemSelected: BAND.link, }, }; + +/** + * The `style` of the page tabs: the line between two tabs, in the colour of + * the other hairlines of the band. (The rule under the strip is set in + * `chrome.css`.) + */ +export const TAB_STRIP_STYLE = { + tabSeparator: { color: BAND.divider }, +} as const; diff --git a/workflow-canvas/test-support/canvas-contrast.test.ts b/workflow-canvas/test-support/canvas-contrast.test.ts index a8fddfb0..ef267f3a 100644 --- a/workflow-canvas/test-support/canvas-contrast.test.ts +++ b/workflow-canvas/test-support/canvas-contrast.test.ts @@ -2,62 +2,89 @@ import { readFileSync } from 'node:fs'; import { describe, expect, it } from 'vitest'; import { AA_GRAPHIC, AA_TEXT, contrastRatio } from '../src/theme/contrast.ts'; +import { colorsOf, RADIUS, type ThemeMode } from '../src/theme/tokens.ts'; const css = readFileSync( new URL('../src/canvas/canvas.css', import.meta.url), 'utf8', ); -describe('canvas.css colours', () => { - // The declarations of the rule that starts with `selector`. - function variables(selector: string): Record { - const block = css.split(`${selector} {`)[1]?.split('}')[0] ?? ''; - return Object.fromEntries( - [...block.matchAll(/(--[\w-]+):\s*(#[0-9a-f]{6})/g)].map((m) => [ - m[1], - m[2], - ]), - ); - } +// The declarations of the rule that starts with `selector`. +function block(selector: string): string { + return css.split(`${selector} {`)[1]?.split('}')[0] ?? ''; +} - const light = variables('.wf-canvas'); - const dark = { ...light, ...variables('.awsui-dark-mode .wf-canvas') }; +function variables(selector: string): Record { + return Object.fromEntries( + [...block(selector).matchAll(/(--[\w-]+):\s*(#[0-9a-f]{6})/g)].map((m) => [ + m[1], + m[2], + ]), + ); +} - it.each([ - ['light', light], - ['dark', dark], - ])('keeps the canvas text readable in %s', (_mode, v) => { - expect( - contrastRatio(v['--wf-text']!, v['--wf-node-bg']!), - ).toBeGreaterThanOrEqual(AA_TEXT); - expect( - contrastRatio(v['--wf-text-muted']!, v['--wf-node-bg']!), - ).toBeGreaterThanOrEqual(AA_TEXT); - expect( - contrastRatio(v['--wf-text-muted']!, v['--wf-canvas-bg']!), - ).toBeGreaterThanOrEqual(AA_TEXT); - expect( - contrastRatio(v['--wf-text']!, v['--wf-controls-bg']!), - ).toBeGreaterThanOrEqual(AA_TEXT); +const light = variables('.wf-canvas'); +const dark = { ...light, ...variables(":root[data-theme='dark'] .wf-canvas") }; +const MODES: [ThemeMode, Record][] = [ + ['light', light], + ['dark', dark], +]; + +describe.each(MODES)('canvas.css colours in %s', (mode, v) => { + const ratio = (a: string, b: string) => contrastRatio(v[a]!, v[b]!); + + it('keeps the canvas text readable', () => { + expect(ratio('--wf-text', '--wf-node-bg')).toBeGreaterThanOrEqual(AA_TEXT); + expect(ratio('--wf-text-muted', '--wf-node-bg')).toBeGreaterThanOrEqual( + AA_TEXT, + ); + expect(ratio('--wf-text-muted', '--wf-canvas-bg')).toBeGreaterThanOrEqual( + AA_TEXT, + ); + expect(ratio('--wf-text', '--wf-controls-bg')).toBeGreaterThanOrEqual( + AA_TEXT, + ); }); - it.each([ - ['light', light], - ['dark', dark], - ])('keeps nodes, edges and the focus ring visible in %s', (_mode, v) => { + it('keeps nodes, edges and the focus ring visible', () => { for (const name of ['--wf-node-border', '--wf-edge', '--wf-focus']) { - expect( - contrastRatio(v[name]!, v['--wf-canvas-bg']!), - ).toBeGreaterThanOrEqual(AA_GRAPHIC); + expect(ratio(name, '--wf-canvas-bg')).toBeGreaterThanOrEqual(AA_GRAPHIC); } }); it('keeps the port letters readable on their ports', () => { expect( - contrastRatio(light['--wf-port-letter-out']!, light['--wf-port-fill']!), + ratio('--wf-port-letter-out', '--wf-port-fill'), ).toBeGreaterThanOrEqual(AA_TEXT); expect( - contrastRatio(light['--wf-port-letter-in']!, light['--wf-port-inner']!), + ratio('--wf-port-letter-in', '--wf-port-inner'), ).toBeGreaterThanOrEqual(AA_TEXT); }); + + it('keeps the ports visible on the canvas and on the node', () => { + for (const part of ['--wf-port-fill', '--wf-port-ring']) { + expect(ratio(part, '--wf-canvas-bg')).toBeGreaterThanOrEqual(AA_GRAPHIC); + expect(ratio(part, '--wf-node-bg')).toBeGreaterThanOrEqual(AA_GRAPHIC); + } + }); + + it('uses the colours of the theme module', () => { + const c = colorsOf(mode); + + expect(v['--wf-canvas-bg']).toBe(c.bg); + expect(v['--wf-node-bg']).toBe(c.surface); + expect(v['--wf-node-border']).toBe(c.control); + expect(v['--wf-text']).toBe(c.text); + expect(v['--wf-focus']).toBe(c.link); + expect(v['--wf-controls-bg']).toBe(c.surface); + expect(v['--wf-controls-border']).toBe(c.borderStrong); + expect(v['--wf-controls-bg-hover']).toBe(c.infoBg); + }); +}); + +describe('canvas.css shapes', () => { + it('uses the radii of the theme module', () => { + expect(block('.wf-canvas')).toContain(`border-radius: ${RADIUS.card}`); + expect(block('.wf-node')).toContain(`border-radius: ${RADIUS.control}`); + }); }); diff --git a/workflow-canvas/test-support/early-mode.test.ts b/workflow-canvas/test-support/early-mode.test.ts new file mode 100644 index 00000000..6443c5e2 --- /dev/null +++ b/workflow-canvas/test-support/early-mode.test.ts @@ -0,0 +1,55 @@ +// SPDX-License-Identifier: Apache-2.0 +import { readFileSync } from 'node:fs'; +import { describe, expect, it } from 'vitest'; +import { MODE_STORAGE_KEY } from '../src/theme/mode.ts'; + +const html = readFileSync(new URL('../index.html', import.meta.url), 'utf8'); +const script = /'], + [ + 'an upper case tag with an attribute', + '', + ], + ['an end tag with a space', ''], + ['an end tag with an attribute', ''], + ['a start tag with a space', ''], + ])('finds the body in %s', (_, html) => { + expect(inlineScriptOf(html)).toBe('a()'); + }); + + it('skips a script with no body, such as one that loads a file', () => { + expect( + inlineScriptOf( + '', + ), + ).toBe('b()'); + }); + + it('does not take a tag whose name only starts with script', () => { + expect(inlineScriptOf('a()')).toBe(''); + }); + + it('is empty when there is no script', () => { + expect(inlineScriptOf('

none

')).toBe(''); + }); +}); diff --git a/workflow-canvas/test-support/inline-script.ts b/workflow-canvas/test-support/inline-script.ts new file mode 100644 index 00000000..7faa9a6b --- /dev/null +++ b/workflow-canvas/test-support/inline-script.ts @@ -0,0 +1,13 @@ +// SPDX-License-Identifier: Apache-2.0 + +// Tag names are case-insensitive, a start tag may carry attributes, and an end +// tag may carry whitespace. +const SCRIPT = /]*>([\s\S]*?)<\/script\b[^>]*>/gi; + +/** The body of the first `
diff --git a/workflow-canvas/src/App.test.tsx b/workflow-canvas/src/App.test.tsx index 1f151f3c..698c05ed 100644 --- a/workflow-canvas/src/App.test.tsx +++ b/workflow-canvas/src/App.test.tsx @@ -20,6 +20,7 @@ const themeButton = (mode: 'light' | 'dark') => afterEach(() => { window.localStorage.clear(); document.body.classList.remove('awsui-dark-mode'); + delete document.documentElement.dataset.theme; document.documentElement.style.colorScheme = ''; }); @@ -109,17 +110,17 @@ describe('App', () => { ); }); - it('shows the breadcrumb Workflows > the open workflow', async () => { + it('shows the breadcrumb Workflows > the open workflow, with no dead link', async () => { render(); const trail = await screen.findByRole('navigation', { name: 'Breadcrumbs', }); - expect(within(trail).getAllByText('Workflows').length).toBeGreaterThan(0); - // The trail holds a hidden copy that it measures, so the name appears twice. - expect((await within(trail).findAllByText('hello')).length).toBeGreaterThan( - 0, - ); + expect(within(trail).getByText('Workflows')).toBeTruthy(); + expect( + (await within(trail).findByText('hello')).closest('[aria-current]'), + ).toBeTruthy(); + expect(within(trail).queryAllByRole('link')).toEqual([]); }); it('switches to the dark mode, remembers it and switches back', () => { @@ -129,12 +130,14 @@ describe('App', () => { expect(document.body.classList.contains('awsui-dark-mode')).toBe(true); expect(document.documentElement.style.colorScheme).toBe('dark'); + expect(document.documentElement.dataset.theme).toBe('dark'); expect(window.localStorage.getItem(MODE_STORAGE_KEY)).toBe('dark'); expect(themeButton('dark').textContent).toBe('Theme: dark'); fireEvent.click(themeButton('dark')); expect(document.body.classList.contains('awsui-dark-mode')).toBe(false); + expect(document.documentElement.dataset.theme).toBe('light'); expect(window.localStorage.getItem(MODE_STORAGE_KEY)).toBe('light'); }); diff --git a/workflow-canvas/src/App.tsx b/workflow-canvas/src/App.tsx index b3326416..3f294dec 100644 --- a/workflow-canvas/src/App.tsx +++ b/workflow-canvas/src/App.tsx @@ -6,9 +6,11 @@ import Header from '@cloudscape-design/components/header'; import Tabs from '@cloudscape-design/components/tabs'; import type { ReactNode } from 'react'; import { Breadcrumbs } from './chrome/Breadcrumbs'; +import './chrome/chrome.css'; import { TOP_BAR_ID, TopBar } from './chrome/TopBar'; import { Designer } from './designer/Designer'; import { useSettled } from './designer/use-settled'; +import { TAB_STRIP_STYLE } from './theme/tokens'; import { useThemeMode } from './theme/use-theme-mode'; import { openWorkflowName, type LoadResult } from './workflow/load-workflow'; @@ -53,6 +55,7 @@ export function App({
+ {kind === 'tab' && } {kind === 'notch' && ( <> diff --git a/workflow-canvas/src/chrome/Breadcrumbs.tsx b/workflow-canvas/src/chrome/Breadcrumbs.tsx index 55fcaceb..71da872f 100644 --- a/workflow-canvas/src/chrome/Breadcrumbs.tsx +++ b/workflow-canvas/src/chrome/Breadcrumbs.tsx @@ -1,18 +1,36 @@ // SPDX-License-Identifier: Apache-2.0 -import BreadcrumbGroup from '@cloudscape-design/components/breadcrumb-group'; +import Box from '@cloudscape-design/components/box'; +import Icon from '@cloudscape-design/components/icon'; /** * The trail above the title: Workflows, then the workflow that is open. The - * page has no list of workflows yet, so both stay on this page. + * page has no list of workflows yet, so no crumb is a link; the last one is + * the current page. */ export function Breadcrumbs({ workflowName }: { workflowName?: string }) { - const items = [{ text: 'Workflows', href: '#' }]; - if (workflowName) items.push({ text: workflowName, href: '#' }); return ( - event.preventDefault()} - /> + ); } diff --git a/workflow-canvas/src/chrome/TopBar.tsx b/workflow-canvas/src/chrome/TopBar.tsx index 3cd89156..d4b450e9 100644 --- a/workflow-canvas/src/chrome/TopBar.tsx +++ b/workflow-canvas/src/chrome/TopBar.tsx @@ -23,6 +23,7 @@ export function TopBar({ event.preventDefault(), title: 'Workflow canvas', logo: { src: LOGO_DATA_URI, alt: 'Konductor' }, }} diff --git a/workflow-canvas/src/chrome/chrome.css b/workflow-canvas/src/chrome/chrome.css new file mode 100644 index 00000000..f990720d --- /dev/null +++ b/workflow-canvas/src/chrome/chrome.css @@ -0,0 +1,27 @@ +/* SPDX-License-Identifier: Apache-2.0 */ + +/* + * The strip of the Designer and Execution tabs is part of the dark header + * band: it is painted with the band colour across the full width of the + * page, so the tab labels, the selected-tab indicator and the rule under + * them all sit on the band. The colours come from the theme module through + * `--wf-band-background` and `--wf-band-divider`. + */ +.wf-tab-strip :has(> * > [role='tablist']) { + background: var(--wf-band-background); + box-shadow: 0 0 0 100vmax var(--wf-band-background); + /* The library sets this colour with a more specific rule. */ + border-block-end-color: var(--wf-band-divider) !important; + clip-path: inset(0 -100vmax); +} + +/* The trail above the title. */ +.wf-crumbs { + display: flex; + flex-wrap: wrap; + align-items: center; + gap: 8px; + margin: 0; + padding: 0; + list-style: none; +} diff --git a/workflow-canvas/src/theme/install.ts b/workflow-canvas/src/theme/install.ts index 0e9e6152..2ebc59ba 100644 --- a/workflow-canvas/src/theme/install.ts +++ b/workflow-canvas/src/theme/install.ts @@ -3,13 +3,24 @@ import { applyMode, Mode } from '@cloudscape-design/global-styles'; import { applyTheme } from '@cloudscape-design/components/theming'; import { APP_THEME, + BAND, FONT_TEXT, TAB_STRIP_THEME, type ThemeMode, } from './tokens'; -/** Where the tab strip of the page header is, for `TAB_STRIP_THEME`. */ -export const TAB_STRIP_SELECTOR = '.wf-tab-strip [role="tablist"]'; +/** + * Where the strip of the page tabs is, for `TAB_STRIP_THEME`: the element + * that holds the tab list, found through its role so no library class is + * needed. `chrome.css` paints the same element with the band. + */ +export const TAB_STRIP_SELECTOR = '.wf-tab-strip :has(> * > [role="tablist"])'; + +// The band colours that `chrome.css` reads, as custom properties. +const BAND_VARIABLES = { + '--wf-band-background': BAND.background, + '--wf-band-divider': BAND.divider, +} as const; /** * Applies the konductor docs theme to the page. Call it once, before the @@ -18,12 +29,19 @@ export const TAB_STRIP_SELECTOR = '.wf-tab-strip [role="tablist"]'; */ export function installTheme(body: HTMLElement): void { body.style.fontFamily = FONT_TEXT; + for (const [name, value] of Object.entries(BAND_VARIABLES)) { + body.style.setProperty(name, value); + } applyTheme({ theme: APP_THEME }); applyTheme({ theme: TAB_STRIP_THEME, selector: TAB_STRIP_SELECTOR }); } -/** Switches the page, its components and its native controls to `mode`. */ +/** + * Switches the page, its components and its native controls to `mode`. The + * root's `data-theme` is what the canvas styles key on. + */ export function showMode(mode: ThemeMode, root: HTMLElement): void { applyMode(mode === 'dark' ? Mode.Dark : Mode.Light); root.style.colorScheme = mode; + root.dataset.theme = mode; } diff --git a/workflow-canvas/src/theme/mode.ts b/workflow-canvas/src/theme/mode.ts index 23592863..987cee84 100644 --- a/workflow-canvas/src/theme/mode.ts +++ b/workflow-canvas/src/theme/mode.ts @@ -14,18 +14,11 @@ function isMode(value: unknown): value is ThemeMode { return value === 'light' || value === 'dark'; } -// Reading `localStorage` can throw when the browser blocks storage. -function storageOf(env: ModeEnvironment) { - try { - return env.localStorage; - } catch { - return undefined; - } -} - +// Reading `localStorage` can throw when the browser blocks storage, so the +// read of the property is inside the `try` too. function storedMode(env: ModeEnvironment): ThemeMode | undefined { try { - const value = storageOf(env)?.getItem(MODE_STORAGE_KEY); + const value = env.localStorage?.getItem(MODE_STORAGE_KEY); return isMode(value) ? value : undefined; } catch { return undefined; @@ -53,7 +46,7 @@ export function otherMode(mode: ThemeMode): ThemeMode { /** Keeps a chosen mode for the next visit. A blocked storage is ignored. */ export function rememberMode(env: ModeEnvironment, mode: ThemeMode): void { try { - storageOf(env)?.setItem(MODE_STORAGE_KEY, mode); + env.localStorage?.setItem(MODE_STORAGE_KEY, mode); } catch { // The choice then lasts until the page closes. } diff --git a/workflow-canvas/src/theme/tokens.test.ts b/workflow-canvas/src/theme/tokens.test.ts index 5f192bda..90ffca81 100644 --- a/workflow-canvas/src/theme/tokens.test.ts +++ b/workflow-canvas/src/theme/tokens.test.ts @@ -1,7 +1,14 @@ // SPDX-License-Identifier: Apache-2.0 import { describe, expect, it } from 'vitest'; import { AA_GRAPHIC, AA_TEXT, contrastRatio } from './contrast'; -import { APP_THEME, BAND_BACKGROUND, colorsOf, type ThemeMode } from './tokens'; +import { + APP_THEME, + BAND, + colorsOf, + TAB_STRIP_STYLE, + TAB_STRIP_THEME, + type ThemeMode, +} from './tokens'; const MODES: ThemeMode[] = ['light', 'dark']; @@ -41,17 +48,32 @@ describe.each(MODES)('the %s palette', (mode) => { }); describe('the header band', () => { - it.each([ - ['#f5f3ee', 'text'], - ['#c9c2e0', 'muted text'], - ['#c4b2ff', 'links'], - ])('has AA contrast for %s (%s)', (colour) => { - expect(contrastRatio(colour, BAND_BACKGROUND)).toBeGreaterThanOrEqual( - AA_TEXT, + it.each([BAND.text, BAND.textMuted, BAND.link])( + 'has AA contrast for %s', + (colour) => { + expect(contrastRatio(colour, BAND.background)).toBeGreaterThanOrEqual( + AA_TEXT, + ); + }, + ); + + it('draws the selected-tab indicator at 3:1 or more on the band', () => { + expect(contrastRatio(BAND.link, BAND.background)).toBeGreaterThanOrEqual( + AA_GRAPHIC, ); }); }); +describe('TAB_STRIP_THEME', () => { + it('colours the selected-tab indicator from the band', () => { + expect(TAB_STRIP_THEME.tokens.colorBorderItemSelected).toBe(BAND.link); + }); + + it('colours the line between tabs from the band', () => { + expect(TAB_STRIP_STYLE.tabSeparator.color).toBe(BAND.divider); + }); +}); + describe('APP_THEME', () => { it('gives the base font to the page', () => { expect(APP_THEME.tokens.fontFamilyBase).toMatch(/^Outfit,/); diff --git a/workflow-canvas/src/theme/tokens.ts b/workflow-canvas/src/theme/tokens.ts index 623089b6..b2e6b7c2 100644 --- a/workflow-canvas/src/theme/tokens.ts +++ b/workflow-canvas/src/theme/tokens.ts @@ -105,13 +105,17 @@ export function colorsOf(mode: ThemeMode): Colors { /** * The header band and the top bar are dark in both modes. These are the - * colours of the text and controls on them; each pair reaches 4.5:1 on - * `BAND_BACKGROUND`. + * colours of the text and controls on them. Text and the selected-tab + * indicator reach their contrast (4.5:1 and 3:1) on `background`. */ -export const BAND_BACKGROUND = '#141026'; -const BAND_TEXT = '#f5f3ee'; -const BAND_TEXT_MUTED = '#c9c2e0'; -const BAND_LINK = '#c4b2ff'; +export const BAND = { + background: '#141026', + text: '#f5f3ee', + textMuted: '#c9c2e0', + link: '#c4b2ff', + /** The hairline under the top bar and under the tabs. */ + divider: '#2a2450', +} as const; /** The font of the page text. Self-hosted by `fonts.ts`. */ export const FONT_TEXT = @@ -134,7 +138,7 @@ export const LOGO_DATA_URI = `data:image/svg+xml;utf8,${encodeURIComponent( type Tokens = Theme['tokens']; -/** A token that has one value in light mode and one in dark mode. */ +// A token that has one value in light mode and one in dark mode. function perMode(pick: (colors: Colors) => string) { return { light: pick(LIGHT), dark: pick(DARK) }; } @@ -155,7 +159,7 @@ function backgroundTokens(): Tokens { colorBackgroundItemSelected: perMode((c) => c.tintStrong), colorBackgroundCellShaded: perMode((c) => c.surfaceAlt), colorBackgroundToggleDefault: perMode((c) => c.control), - colorBackgroundHomeHeader: BAND_BACKGROUND, + colorBackgroundHomeHeader: BAND.background, colorBackgroundStatusError: perMode((c) => c.errorBg), colorBackgroundStatusInfo: perMode((c) => c.infoBg), colorBackgroundStatusSuccess: perMode((c) => c.successBg), @@ -214,8 +218,8 @@ function textTokens(): Tokens { colorTextDropdownItemHighlighted: perMode((c) => c.text), colorTextDropdownItemSecondary: perMode((c) => c.textMuted), colorTextGroupLabel: perMode((c) => c.textMuted), - colorTextHomeHeaderDefault: BAND_TEXT, - colorTextHomeHeaderSecondary: BAND_TEXT_MUTED, + colorTextHomeHeaderDefault: BAND.text, + colorTextHomeHeaderSecondary: BAND.textMuted, colorTextBreadcrumbCurrent: perMode((c) => c.textMuted), colorTextBreadcrumbIcon: perMode((c) => c.textMuted), }; @@ -263,23 +267,21 @@ function shapeTokens(): Tokens { }; } -/** - * Inside the dark band the global text and border colours do not apply, so - * the same roles are set again here. Cloudscape reads these in the page - * header, which holds the breadcrumbs and the title. - */ +// Inside the dark band the global text and border colours do not apply, so +// the same roles are set again here. Cloudscape reads these in the page +// header, which holds the breadcrumbs and the title. function headerBandTokens(): Tokens { return { - colorTextAccent: BAND_LINK, - colorTextLinkDefault: BAND_LINK, + colorTextAccent: BAND.link, + colorTextLinkDefault: BAND.link, colorTextLinkHover: '#ffffff', - colorTextBodySecondary: BAND_TEXT_MUTED, - colorTextBreadcrumbCurrent: BAND_TEXT_MUTED, - colorTextBreadcrumbIcon: BAND_TEXT_MUTED, - colorTextInteractiveDefault: BAND_TEXT_MUTED, + colorTextBodySecondary: BAND.textMuted, + colorTextBreadcrumbCurrent: BAND.textMuted, + colorTextBreadcrumbIcon: BAND.textMuted, + colorTextInteractiveDefault: BAND.textMuted, colorTextInteractiveHover: '#ffffff', colorTextInteractiveActive: '#ffffff', - colorBorderItemFocused: BAND_LINK, + colorBorderItemFocused: BAND.link, }; } @@ -299,29 +301,40 @@ export const APP_THEME: Theme = { header: { tokens: { ...headerBandTokens(), - colorBackgroundLayoutMain: BAND_BACKGROUND, + colorBackgroundLayoutMain: BAND.background, }, }, 'top-navigation': { tokens: { ...headerBandTokens(), - colorBackgroundContainerContent: BAND_BACKGROUND, - colorTextHeadingDefault: BAND_TEXT, - colorTextTopNavigationTitle: BAND_TEXT, - colorTextInteractiveDefault: BAND_TEXT, + colorBackgroundContainerContent: BAND.background, + colorTextHeadingDefault: BAND.text, + colorTextTopNavigationTitle: BAND.text, + colorTextInteractiveDefault: BAND.text, colorTextInteractiveHover: '#ffffff', - colorBorderDividerDefault: '#2a2450', + colorBorderDividerDefault: BAND.divider, }, }, }, }; /** - * The same roles for the Designer and Execution tabs, which sit on the band - * but are page content, so the header context does not reach them. + * The same roles for the strip of the Designer and Execution tabs, which is + * page content that sits on the band, so the header context does not reach + * it. The selected-tab indicator is set too. */ export const TAB_STRIP_THEME: Theme = { tokens: { ...headerBandTokens(), + colorBorderItemSelected: BAND.link, }, }; + +/** + * The `style` of the page tabs: the line between two tabs, in the colour of + * the other hairlines of the band. (The rule under the strip is set in + * `chrome.css`.) + */ +export const TAB_STRIP_STYLE = { + tabSeparator: { color: BAND.divider }, +} as const; diff --git a/workflow-canvas/test-support/canvas-contrast.test.ts b/workflow-canvas/test-support/canvas-contrast.test.ts index a8fddfb0..ef267f3a 100644 --- a/workflow-canvas/test-support/canvas-contrast.test.ts +++ b/workflow-canvas/test-support/canvas-contrast.test.ts @@ -2,62 +2,89 @@ import { readFileSync } from 'node:fs'; import { describe, expect, it } from 'vitest'; import { AA_GRAPHIC, AA_TEXT, contrastRatio } from '../src/theme/contrast.ts'; +import { colorsOf, RADIUS, type ThemeMode } from '../src/theme/tokens.ts'; const css = readFileSync( new URL('../src/canvas/canvas.css', import.meta.url), 'utf8', ); -describe('canvas.css colours', () => { - // The declarations of the rule that starts with `selector`. - function variables(selector: string): Record { - const block = css.split(`${selector} {`)[1]?.split('}')[0] ?? ''; - return Object.fromEntries( - [...block.matchAll(/(--[\w-]+):\s*(#[0-9a-f]{6})/g)].map((m) => [ - m[1], - m[2], - ]), - ); - } +// The declarations of the rule that starts with `selector`. +function block(selector: string): string { + return css.split(`${selector} {`)[1]?.split('}')[0] ?? ''; +} - const light = variables('.wf-canvas'); - const dark = { ...light, ...variables('.awsui-dark-mode .wf-canvas') }; +function variables(selector: string): Record { + return Object.fromEntries( + [...block(selector).matchAll(/(--[\w-]+):\s*(#[0-9a-f]{6})/g)].map((m) => [ + m[1], + m[2], + ]), + ); +} - it.each([ - ['light', light], - ['dark', dark], - ])('keeps the canvas text readable in %s', (_mode, v) => { - expect( - contrastRatio(v['--wf-text']!, v['--wf-node-bg']!), - ).toBeGreaterThanOrEqual(AA_TEXT); - expect( - contrastRatio(v['--wf-text-muted']!, v['--wf-node-bg']!), - ).toBeGreaterThanOrEqual(AA_TEXT); - expect( - contrastRatio(v['--wf-text-muted']!, v['--wf-canvas-bg']!), - ).toBeGreaterThanOrEqual(AA_TEXT); - expect( - contrastRatio(v['--wf-text']!, v['--wf-controls-bg']!), - ).toBeGreaterThanOrEqual(AA_TEXT); +const light = variables('.wf-canvas'); +const dark = { ...light, ...variables(":root[data-theme='dark'] .wf-canvas") }; +const MODES: [ThemeMode, Record][] = [ + ['light', light], + ['dark', dark], +]; + +describe.each(MODES)('canvas.css colours in %s', (mode, v) => { + const ratio = (a: string, b: string) => contrastRatio(v[a]!, v[b]!); + + it('keeps the canvas text readable', () => { + expect(ratio('--wf-text', '--wf-node-bg')).toBeGreaterThanOrEqual(AA_TEXT); + expect(ratio('--wf-text-muted', '--wf-node-bg')).toBeGreaterThanOrEqual( + AA_TEXT, + ); + expect(ratio('--wf-text-muted', '--wf-canvas-bg')).toBeGreaterThanOrEqual( + AA_TEXT, + ); + expect(ratio('--wf-text', '--wf-controls-bg')).toBeGreaterThanOrEqual( + AA_TEXT, + ); }); - it.each([ - ['light', light], - ['dark', dark], - ])('keeps nodes, edges and the focus ring visible in %s', (_mode, v) => { + it('keeps nodes, edges and the focus ring visible', () => { for (const name of ['--wf-node-border', '--wf-edge', '--wf-focus']) { - expect( - contrastRatio(v[name]!, v['--wf-canvas-bg']!), - ).toBeGreaterThanOrEqual(AA_GRAPHIC); + expect(ratio(name, '--wf-canvas-bg')).toBeGreaterThanOrEqual(AA_GRAPHIC); } }); it('keeps the port letters readable on their ports', () => { expect( - contrastRatio(light['--wf-port-letter-out']!, light['--wf-port-fill']!), + ratio('--wf-port-letter-out', '--wf-port-fill'), ).toBeGreaterThanOrEqual(AA_TEXT); expect( - contrastRatio(light['--wf-port-letter-in']!, light['--wf-port-inner']!), + ratio('--wf-port-letter-in', '--wf-port-inner'), ).toBeGreaterThanOrEqual(AA_TEXT); }); + + it('keeps the ports visible on the canvas and on the node', () => { + for (const part of ['--wf-port-fill', '--wf-port-ring']) { + expect(ratio(part, '--wf-canvas-bg')).toBeGreaterThanOrEqual(AA_GRAPHIC); + expect(ratio(part, '--wf-node-bg')).toBeGreaterThanOrEqual(AA_GRAPHIC); + } + }); + + it('uses the colours of the theme module', () => { + const c = colorsOf(mode); + + expect(v['--wf-canvas-bg']).toBe(c.bg); + expect(v['--wf-node-bg']).toBe(c.surface); + expect(v['--wf-node-border']).toBe(c.control); + expect(v['--wf-text']).toBe(c.text); + expect(v['--wf-focus']).toBe(c.link); + expect(v['--wf-controls-bg']).toBe(c.surface); + expect(v['--wf-controls-border']).toBe(c.borderStrong); + expect(v['--wf-controls-bg-hover']).toBe(c.infoBg); + }); +}); + +describe('canvas.css shapes', () => { + it('uses the radii of the theme module', () => { + expect(block('.wf-canvas')).toContain(`border-radius: ${RADIUS.card}`); + expect(block('.wf-node')).toContain(`border-radius: ${RADIUS.control}`); + }); }); diff --git a/workflow-canvas/test-support/early-mode.test.ts b/workflow-canvas/test-support/early-mode.test.ts new file mode 100644 index 00000000..6443c5e2 --- /dev/null +++ b/workflow-canvas/test-support/early-mode.test.ts @@ -0,0 +1,55 @@ +// SPDX-License-Identifier: Apache-2.0 +import { readFileSync } from 'node:fs'; +import { describe, expect, it } from 'vitest'; +import { MODE_STORAGE_KEY } from '../src/theme/mode.ts'; + +const html = readFileSync(new URL('../index.html', import.meta.url), 'utf8'); +const script = /'], + [ + 'an upper case tag with an attribute', + '', + ], + ['an end tag with a space', ''], + ['an end tag with an attribute', ''], + ['a start tag with a space', ''], + ])('finds the body in %s', (_, html) => { + expect(inlineScriptOf(html)).toBe('a()'); + }); + + it('skips a script with no body, such as one that loads a file', () => { + expect( + inlineScriptOf( + '', + ), + ).toBe('b()'); + }); + + it('does not take a tag whose name only starts with script', () => { + expect(inlineScriptOf('a()')).toBe(''); + }); + + it('is empty when there is no script', () => { + expect(inlineScriptOf('

none

')).toBe(''); + }); +}); diff --git a/workflow-canvas/test-support/inline-script.ts b/workflow-canvas/test-support/inline-script.ts new file mode 100644 index 00000000..7faa9a6b --- /dev/null +++ b/workflow-canvas/test-support/inline-script.ts @@ -0,0 +1,13 @@ +// SPDX-License-Identifier: Apache-2.0 + +// Tag names are case-insensitive, a start tag may carry attributes, and an end +// tag may carry whitespace. +const SCRIPT = /]*>([\s\S]*?)<\/script\b[^>]*>/gi; + +/** The body of the first `
diff --git a/workflow-canvas/src/App.test.tsx b/workflow-canvas/src/App.test.tsx index 1f151f3c..698c05ed 100644 --- a/workflow-canvas/src/App.test.tsx +++ b/workflow-canvas/src/App.test.tsx @@ -20,6 +20,7 @@ const themeButton = (mode: 'light' | 'dark') => afterEach(() => { window.localStorage.clear(); document.body.classList.remove('awsui-dark-mode'); + delete document.documentElement.dataset.theme; document.documentElement.style.colorScheme = ''; }); @@ -109,17 +110,17 @@ describe('App', () => { ); }); - it('shows the breadcrumb Workflows > the open workflow', async () => { + it('shows the breadcrumb Workflows > the open workflow, with no dead link', async () => { render(); const trail = await screen.findByRole('navigation', { name: 'Breadcrumbs', }); - expect(within(trail).getAllByText('Workflows').length).toBeGreaterThan(0); - // The trail holds a hidden copy that it measures, so the name appears twice. - expect((await within(trail).findAllByText('hello')).length).toBeGreaterThan( - 0, - ); + expect(within(trail).getByText('Workflows')).toBeTruthy(); + expect( + (await within(trail).findByText('hello')).closest('[aria-current]'), + ).toBeTruthy(); + expect(within(trail).queryAllByRole('link')).toEqual([]); }); it('switches to the dark mode, remembers it and switches back', () => { @@ -129,12 +130,14 @@ describe('App', () => { expect(document.body.classList.contains('awsui-dark-mode')).toBe(true); expect(document.documentElement.style.colorScheme).toBe('dark'); + expect(document.documentElement.dataset.theme).toBe('dark'); expect(window.localStorage.getItem(MODE_STORAGE_KEY)).toBe('dark'); expect(themeButton('dark').textContent).toBe('Theme: dark'); fireEvent.click(themeButton('dark')); expect(document.body.classList.contains('awsui-dark-mode')).toBe(false); + expect(document.documentElement.dataset.theme).toBe('light'); expect(window.localStorage.getItem(MODE_STORAGE_KEY)).toBe('light'); }); diff --git a/workflow-canvas/src/App.tsx b/workflow-canvas/src/App.tsx index b3326416..3f294dec 100644 --- a/workflow-canvas/src/App.tsx +++ b/workflow-canvas/src/App.tsx @@ -6,9 +6,11 @@ import Header from '@cloudscape-design/components/header'; import Tabs from '@cloudscape-design/components/tabs'; import type { ReactNode } from 'react'; import { Breadcrumbs } from './chrome/Breadcrumbs'; +import './chrome/chrome.css'; import { TOP_BAR_ID, TopBar } from './chrome/TopBar'; import { Designer } from './designer/Designer'; import { useSettled } from './designer/use-settled'; +import { TAB_STRIP_STYLE } from './theme/tokens'; import { useThemeMode } from './theme/use-theme-mode'; import { openWorkflowName, type LoadResult } from './workflow/load-workflow'; @@ -53,6 +55,7 @@ export function App({
+ {kind === 'tab' && } {kind === 'notch' && ( <> diff --git a/workflow-canvas/src/chrome/Breadcrumbs.tsx b/workflow-canvas/src/chrome/Breadcrumbs.tsx index 55fcaceb..71da872f 100644 --- a/workflow-canvas/src/chrome/Breadcrumbs.tsx +++ b/workflow-canvas/src/chrome/Breadcrumbs.tsx @@ -1,18 +1,36 @@ // SPDX-License-Identifier: Apache-2.0 -import BreadcrumbGroup from '@cloudscape-design/components/breadcrumb-group'; +import Box from '@cloudscape-design/components/box'; +import Icon from '@cloudscape-design/components/icon'; /** * The trail above the title: Workflows, then the workflow that is open. The - * page has no list of workflows yet, so both stay on this page. + * page has no list of workflows yet, so no crumb is a link; the last one is + * the current page. */ export function Breadcrumbs({ workflowName }: { workflowName?: string }) { - const items = [{ text: 'Workflows', href: '#' }]; - if (workflowName) items.push({ text: workflowName, href: '#' }); return ( - event.preventDefault()} - /> + ); } diff --git a/workflow-canvas/src/chrome/TopBar.tsx b/workflow-canvas/src/chrome/TopBar.tsx index 3cd89156..d4b450e9 100644 --- a/workflow-canvas/src/chrome/TopBar.tsx +++ b/workflow-canvas/src/chrome/TopBar.tsx @@ -23,6 +23,7 @@ export function TopBar({ event.preventDefault(), title: 'Workflow canvas', logo: { src: LOGO_DATA_URI, alt: 'Konductor' }, }} diff --git a/workflow-canvas/src/chrome/chrome.css b/workflow-canvas/src/chrome/chrome.css new file mode 100644 index 00000000..f990720d --- /dev/null +++ b/workflow-canvas/src/chrome/chrome.css @@ -0,0 +1,27 @@ +/* SPDX-License-Identifier: Apache-2.0 */ + +/* + * The strip of the Designer and Execution tabs is part of the dark header + * band: it is painted with the band colour across the full width of the + * page, so the tab labels, the selected-tab indicator and the rule under + * them all sit on the band. The colours come from the theme module through + * `--wf-band-background` and `--wf-band-divider`. + */ +.wf-tab-strip :has(> * > [role='tablist']) { + background: var(--wf-band-background); + box-shadow: 0 0 0 100vmax var(--wf-band-background); + /* The library sets this colour with a more specific rule. */ + border-block-end-color: var(--wf-band-divider) !important; + clip-path: inset(0 -100vmax); +} + +/* The trail above the title. */ +.wf-crumbs { + display: flex; + flex-wrap: wrap; + align-items: center; + gap: 8px; + margin: 0; + padding: 0; + list-style: none; +} diff --git a/workflow-canvas/src/theme/install.ts b/workflow-canvas/src/theme/install.ts index 0e9e6152..2ebc59ba 100644 --- a/workflow-canvas/src/theme/install.ts +++ b/workflow-canvas/src/theme/install.ts @@ -3,13 +3,24 @@ import { applyMode, Mode } from '@cloudscape-design/global-styles'; import { applyTheme } from '@cloudscape-design/components/theming'; import { APP_THEME, + BAND, FONT_TEXT, TAB_STRIP_THEME, type ThemeMode, } from './tokens'; -/** Where the tab strip of the page header is, for `TAB_STRIP_THEME`. */ -export const TAB_STRIP_SELECTOR = '.wf-tab-strip [role="tablist"]'; +/** + * Where the strip of the page tabs is, for `TAB_STRIP_THEME`: the element + * that holds the tab list, found through its role so no library class is + * needed. `chrome.css` paints the same element with the band. + */ +export const TAB_STRIP_SELECTOR = '.wf-tab-strip :has(> * > [role="tablist"])'; + +// The band colours that `chrome.css` reads, as custom properties. +const BAND_VARIABLES = { + '--wf-band-background': BAND.background, + '--wf-band-divider': BAND.divider, +} as const; /** * Applies the konductor docs theme to the page. Call it once, before the @@ -18,12 +29,19 @@ export const TAB_STRIP_SELECTOR = '.wf-tab-strip [role="tablist"]'; */ export function installTheme(body: HTMLElement): void { body.style.fontFamily = FONT_TEXT; + for (const [name, value] of Object.entries(BAND_VARIABLES)) { + body.style.setProperty(name, value); + } applyTheme({ theme: APP_THEME }); applyTheme({ theme: TAB_STRIP_THEME, selector: TAB_STRIP_SELECTOR }); } -/** Switches the page, its components and its native controls to `mode`. */ +/** + * Switches the page, its components and its native controls to `mode`. The + * root's `data-theme` is what the canvas styles key on. + */ export function showMode(mode: ThemeMode, root: HTMLElement): void { applyMode(mode === 'dark' ? Mode.Dark : Mode.Light); root.style.colorScheme = mode; + root.dataset.theme = mode; } diff --git a/workflow-canvas/src/theme/mode.ts b/workflow-canvas/src/theme/mode.ts index 23592863..987cee84 100644 --- a/workflow-canvas/src/theme/mode.ts +++ b/workflow-canvas/src/theme/mode.ts @@ -14,18 +14,11 @@ function isMode(value: unknown): value is ThemeMode { return value === 'light' || value === 'dark'; } -// Reading `localStorage` can throw when the browser blocks storage. -function storageOf(env: ModeEnvironment) { - try { - return env.localStorage; - } catch { - return undefined; - } -} - +// Reading `localStorage` can throw when the browser blocks storage, so the +// read of the property is inside the `try` too. function storedMode(env: ModeEnvironment): ThemeMode | undefined { try { - const value = storageOf(env)?.getItem(MODE_STORAGE_KEY); + const value = env.localStorage?.getItem(MODE_STORAGE_KEY); return isMode(value) ? value : undefined; } catch { return undefined; @@ -53,7 +46,7 @@ export function otherMode(mode: ThemeMode): ThemeMode { /** Keeps a chosen mode for the next visit. A blocked storage is ignored. */ export function rememberMode(env: ModeEnvironment, mode: ThemeMode): void { try { - storageOf(env)?.setItem(MODE_STORAGE_KEY, mode); + env.localStorage?.setItem(MODE_STORAGE_KEY, mode); } catch { // The choice then lasts until the page closes. } diff --git a/workflow-canvas/src/theme/tokens.test.ts b/workflow-canvas/src/theme/tokens.test.ts index 5f192bda..90ffca81 100644 --- a/workflow-canvas/src/theme/tokens.test.ts +++ b/workflow-canvas/src/theme/tokens.test.ts @@ -1,7 +1,14 @@ // SPDX-License-Identifier: Apache-2.0 import { describe, expect, it } from 'vitest'; import { AA_GRAPHIC, AA_TEXT, contrastRatio } from './contrast'; -import { APP_THEME, BAND_BACKGROUND, colorsOf, type ThemeMode } from './tokens'; +import { + APP_THEME, + BAND, + colorsOf, + TAB_STRIP_STYLE, + TAB_STRIP_THEME, + type ThemeMode, +} from './tokens'; const MODES: ThemeMode[] = ['light', 'dark']; @@ -41,17 +48,32 @@ describe.each(MODES)('the %s palette', (mode) => { }); describe('the header band', () => { - it.each([ - ['#f5f3ee', 'text'], - ['#c9c2e0', 'muted text'], - ['#c4b2ff', 'links'], - ])('has AA contrast for %s (%s)', (colour) => { - expect(contrastRatio(colour, BAND_BACKGROUND)).toBeGreaterThanOrEqual( - AA_TEXT, + it.each([BAND.text, BAND.textMuted, BAND.link])( + 'has AA contrast for %s', + (colour) => { + expect(contrastRatio(colour, BAND.background)).toBeGreaterThanOrEqual( + AA_TEXT, + ); + }, + ); + + it('draws the selected-tab indicator at 3:1 or more on the band', () => { + expect(contrastRatio(BAND.link, BAND.background)).toBeGreaterThanOrEqual( + AA_GRAPHIC, ); }); }); +describe('TAB_STRIP_THEME', () => { + it('colours the selected-tab indicator from the band', () => { + expect(TAB_STRIP_THEME.tokens.colorBorderItemSelected).toBe(BAND.link); + }); + + it('colours the line between tabs from the band', () => { + expect(TAB_STRIP_STYLE.tabSeparator.color).toBe(BAND.divider); + }); +}); + describe('APP_THEME', () => { it('gives the base font to the page', () => { expect(APP_THEME.tokens.fontFamilyBase).toMatch(/^Outfit,/); diff --git a/workflow-canvas/src/theme/tokens.ts b/workflow-canvas/src/theme/tokens.ts index 623089b6..b2e6b7c2 100644 --- a/workflow-canvas/src/theme/tokens.ts +++ b/workflow-canvas/src/theme/tokens.ts @@ -105,13 +105,17 @@ export function colorsOf(mode: ThemeMode): Colors { /** * The header band and the top bar are dark in both modes. These are the - * colours of the text and controls on them; each pair reaches 4.5:1 on - * `BAND_BACKGROUND`. + * colours of the text and controls on them. Text and the selected-tab + * indicator reach their contrast (4.5:1 and 3:1) on `background`. */ -export const BAND_BACKGROUND = '#141026'; -const BAND_TEXT = '#f5f3ee'; -const BAND_TEXT_MUTED = '#c9c2e0'; -const BAND_LINK = '#c4b2ff'; +export const BAND = { + background: '#141026', + text: '#f5f3ee', + textMuted: '#c9c2e0', + link: '#c4b2ff', + /** The hairline under the top bar and under the tabs. */ + divider: '#2a2450', +} as const; /** The font of the page text. Self-hosted by `fonts.ts`. */ export const FONT_TEXT = @@ -134,7 +138,7 @@ export const LOGO_DATA_URI = `data:image/svg+xml;utf8,${encodeURIComponent( type Tokens = Theme['tokens']; -/** A token that has one value in light mode and one in dark mode. */ +// A token that has one value in light mode and one in dark mode. function perMode(pick: (colors: Colors) => string) { return { light: pick(LIGHT), dark: pick(DARK) }; } @@ -155,7 +159,7 @@ function backgroundTokens(): Tokens { colorBackgroundItemSelected: perMode((c) => c.tintStrong), colorBackgroundCellShaded: perMode((c) => c.surfaceAlt), colorBackgroundToggleDefault: perMode((c) => c.control), - colorBackgroundHomeHeader: BAND_BACKGROUND, + colorBackgroundHomeHeader: BAND.background, colorBackgroundStatusError: perMode((c) => c.errorBg), colorBackgroundStatusInfo: perMode((c) => c.infoBg), colorBackgroundStatusSuccess: perMode((c) => c.successBg), @@ -214,8 +218,8 @@ function textTokens(): Tokens { colorTextDropdownItemHighlighted: perMode((c) => c.text), colorTextDropdownItemSecondary: perMode((c) => c.textMuted), colorTextGroupLabel: perMode((c) => c.textMuted), - colorTextHomeHeaderDefault: BAND_TEXT, - colorTextHomeHeaderSecondary: BAND_TEXT_MUTED, + colorTextHomeHeaderDefault: BAND.text, + colorTextHomeHeaderSecondary: BAND.textMuted, colorTextBreadcrumbCurrent: perMode((c) => c.textMuted), colorTextBreadcrumbIcon: perMode((c) => c.textMuted), }; @@ -263,23 +267,21 @@ function shapeTokens(): Tokens { }; } -/** - * Inside the dark band the global text and border colours do not apply, so - * the same roles are set again here. Cloudscape reads these in the page - * header, which holds the breadcrumbs and the title. - */ +// Inside the dark band the global text and border colours do not apply, so +// the same roles are set again here. Cloudscape reads these in the page +// header, which holds the breadcrumbs and the title. function headerBandTokens(): Tokens { return { - colorTextAccent: BAND_LINK, - colorTextLinkDefault: BAND_LINK, + colorTextAccent: BAND.link, + colorTextLinkDefault: BAND.link, colorTextLinkHover: '#ffffff', - colorTextBodySecondary: BAND_TEXT_MUTED, - colorTextBreadcrumbCurrent: BAND_TEXT_MUTED, - colorTextBreadcrumbIcon: BAND_TEXT_MUTED, - colorTextInteractiveDefault: BAND_TEXT_MUTED, + colorTextBodySecondary: BAND.textMuted, + colorTextBreadcrumbCurrent: BAND.textMuted, + colorTextBreadcrumbIcon: BAND.textMuted, + colorTextInteractiveDefault: BAND.textMuted, colorTextInteractiveHover: '#ffffff', colorTextInteractiveActive: '#ffffff', - colorBorderItemFocused: BAND_LINK, + colorBorderItemFocused: BAND.link, }; } @@ -299,29 +301,40 @@ export const APP_THEME: Theme = { header: { tokens: { ...headerBandTokens(), - colorBackgroundLayoutMain: BAND_BACKGROUND, + colorBackgroundLayoutMain: BAND.background, }, }, 'top-navigation': { tokens: { ...headerBandTokens(), - colorBackgroundContainerContent: BAND_BACKGROUND, - colorTextHeadingDefault: BAND_TEXT, - colorTextTopNavigationTitle: BAND_TEXT, - colorTextInteractiveDefault: BAND_TEXT, + colorBackgroundContainerContent: BAND.background, + colorTextHeadingDefault: BAND.text, + colorTextTopNavigationTitle: BAND.text, + colorTextInteractiveDefault: BAND.text, colorTextInteractiveHover: '#ffffff', - colorBorderDividerDefault: '#2a2450', + colorBorderDividerDefault: BAND.divider, }, }, }, }; /** - * The same roles for the Designer and Execution tabs, which sit on the band - * but are page content, so the header context does not reach them. + * The same roles for the strip of the Designer and Execution tabs, which is + * page content that sits on the band, so the header context does not reach + * it. The selected-tab indicator is set too. */ export const TAB_STRIP_THEME: Theme = { tokens: { ...headerBandTokens(), + colorBorderItemSelected: BAND.link, }, }; + +/** + * The `style` of the page tabs: the line between two tabs, in the colour of + * the other hairlines of the band. (The rule under the strip is set in + * `chrome.css`.) + */ +export const TAB_STRIP_STYLE = { + tabSeparator: { color: BAND.divider }, +} as const; diff --git a/workflow-canvas/test-support/canvas-contrast.test.ts b/workflow-canvas/test-support/canvas-contrast.test.ts index a8fddfb0..ef267f3a 100644 --- a/workflow-canvas/test-support/canvas-contrast.test.ts +++ b/workflow-canvas/test-support/canvas-contrast.test.ts @@ -2,62 +2,89 @@ import { readFileSync } from 'node:fs'; import { describe, expect, it } from 'vitest'; import { AA_GRAPHIC, AA_TEXT, contrastRatio } from '../src/theme/contrast.ts'; +import { colorsOf, RADIUS, type ThemeMode } from '../src/theme/tokens.ts'; const css = readFileSync( new URL('../src/canvas/canvas.css', import.meta.url), 'utf8', ); -describe('canvas.css colours', () => { - // The declarations of the rule that starts with `selector`. - function variables(selector: string): Record { - const block = css.split(`${selector} {`)[1]?.split('}')[0] ?? ''; - return Object.fromEntries( - [...block.matchAll(/(--[\w-]+):\s*(#[0-9a-f]{6})/g)].map((m) => [ - m[1], - m[2], - ]), - ); - } +// The declarations of the rule that starts with `selector`. +function block(selector: string): string { + return css.split(`${selector} {`)[1]?.split('}')[0] ?? ''; +} - const light = variables('.wf-canvas'); - const dark = { ...light, ...variables('.awsui-dark-mode .wf-canvas') }; +function variables(selector: string): Record { + return Object.fromEntries( + [...block(selector).matchAll(/(--[\w-]+):\s*(#[0-9a-f]{6})/g)].map((m) => [ + m[1], + m[2], + ]), + ); +} - it.each([ - ['light', light], - ['dark', dark], - ])('keeps the canvas text readable in %s', (_mode, v) => { - expect( - contrastRatio(v['--wf-text']!, v['--wf-node-bg']!), - ).toBeGreaterThanOrEqual(AA_TEXT); - expect( - contrastRatio(v['--wf-text-muted']!, v['--wf-node-bg']!), - ).toBeGreaterThanOrEqual(AA_TEXT); - expect( - contrastRatio(v['--wf-text-muted']!, v['--wf-canvas-bg']!), - ).toBeGreaterThanOrEqual(AA_TEXT); - expect( - contrastRatio(v['--wf-text']!, v['--wf-controls-bg']!), - ).toBeGreaterThanOrEqual(AA_TEXT); +const light = variables('.wf-canvas'); +const dark = { ...light, ...variables(":root[data-theme='dark'] .wf-canvas") }; +const MODES: [ThemeMode, Record][] = [ + ['light', light], + ['dark', dark], +]; + +describe.each(MODES)('canvas.css colours in %s', (mode, v) => { + const ratio = (a: string, b: string) => contrastRatio(v[a]!, v[b]!); + + it('keeps the canvas text readable', () => { + expect(ratio('--wf-text', '--wf-node-bg')).toBeGreaterThanOrEqual(AA_TEXT); + expect(ratio('--wf-text-muted', '--wf-node-bg')).toBeGreaterThanOrEqual( + AA_TEXT, + ); + expect(ratio('--wf-text-muted', '--wf-canvas-bg')).toBeGreaterThanOrEqual( + AA_TEXT, + ); + expect(ratio('--wf-text', '--wf-controls-bg')).toBeGreaterThanOrEqual( + AA_TEXT, + ); }); - it.each([ - ['light', light], - ['dark', dark], - ])('keeps nodes, edges and the focus ring visible in %s', (_mode, v) => { + it('keeps nodes, edges and the focus ring visible', () => { for (const name of ['--wf-node-border', '--wf-edge', '--wf-focus']) { - expect( - contrastRatio(v[name]!, v['--wf-canvas-bg']!), - ).toBeGreaterThanOrEqual(AA_GRAPHIC); + expect(ratio(name, '--wf-canvas-bg')).toBeGreaterThanOrEqual(AA_GRAPHIC); } }); it('keeps the port letters readable on their ports', () => { expect( - contrastRatio(light['--wf-port-letter-out']!, light['--wf-port-fill']!), + ratio('--wf-port-letter-out', '--wf-port-fill'), ).toBeGreaterThanOrEqual(AA_TEXT); expect( - contrastRatio(light['--wf-port-letter-in']!, light['--wf-port-inner']!), + ratio('--wf-port-letter-in', '--wf-port-inner'), ).toBeGreaterThanOrEqual(AA_TEXT); }); + + it('keeps the ports visible on the canvas and on the node', () => { + for (const part of ['--wf-port-fill', '--wf-port-ring']) { + expect(ratio(part, '--wf-canvas-bg')).toBeGreaterThanOrEqual(AA_GRAPHIC); + expect(ratio(part, '--wf-node-bg')).toBeGreaterThanOrEqual(AA_GRAPHIC); + } + }); + + it('uses the colours of the theme module', () => { + const c = colorsOf(mode); + + expect(v['--wf-canvas-bg']).toBe(c.bg); + expect(v['--wf-node-bg']).toBe(c.surface); + expect(v['--wf-node-border']).toBe(c.control); + expect(v['--wf-text']).toBe(c.text); + expect(v['--wf-focus']).toBe(c.link); + expect(v['--wf-controls-bg']).toBe(c.surface); + expect(v['--wf-controls-border']).toBe(c.borderStrong); + expect(v['--wf-controls-bg-hover']).toBe(c.infoBg); + }); +}); + +describe('canvas.css shapes', () => { + it('uses the radii of the theme module', () => { + expect(block('.wf-canvas')).toContain(`border-radius: ${RADIUS.card}`); + expect(block('.wf-node')).toContain(`border-radius: ${RADIUS.control}`); + }); }); diff --git a/workflow-canvas/test-support/early-mode.test.ts b/workflow-canvas/test-support/early-mode.test.ts new file mode 100644 index 00000000..6443c5e2 --- /dev/null +++ b/workflow-canvas/test-support/early-mode.test.ts @@ -0,0 +1,55 @@ +// SPDX-License-Identifier: Apache-2.0 +import { readFileSync } from 'node:fs'; +import { describe, expect, it } from 'vitest'; +import { MODE_STORAGE_KEY } from '../src/theme/mode.ts'; + +const html = readFileSync(new URL('../index.html', import.meta.url), 'utf8'); +const script = /'], + [ + 'an upper case tag with an attribute', + '', + ], + ['an end tag with a space', ''], + ['an end tag with an attribute', ''], + ['a start tag with a space', ''], + ])('finds the body in %s', (_, html) => { + expect(inlineScriptOf(html)).toBe('a()'); + }); + + it('skips a script with no body, such as one that loads a file', () => { + expect( + inlineScriptOf( + '', + ), + ).toBe('b()'); + }); + + it('does not take a tag whose name only starts with script', () => { + expect(inlineScriptOf('a()')).toBe(''); + }); + + it('is empty when there is no script', () => { + expect(inlineScriptOf('

none

')).toBe(''); + }); +}); diff --git a/workflow-canvas/test-support/inline-script.ts b/workflow-canvas/test-support/inline-script.ts new file mode 100644 index 00000000..7faa9a6b --- /dev/null +++ b/workflow-canvas/test-support/inline-script.ts @@ -0,0 +1,13 @@ +// SPDX-License-Identifier: Apache-2.0 + +// Tag names are case-insensitive, a start tag may carry attributes, and an end +// tag may carry whitespace. +const SCRIPT = /]*>([\s\S]*?)<\/script\b[^>]*>/gi; + +/** The body of the first `
diff --git a/workflow-canvas/src/App.test.tsx b/workflow-canvas/src/App.test.tsx index 1f151f3c..698c05ed 100644 --- a/workflow-canvas/src/App.test.tsx +++ b/workflow-canvas/src/App.test.tsx @@ -20,6 +20,7 @@ const themeButton = (mode: 'light' | 'dark') => afterEach(() => { window.localStorage.clear(); document.body.classList.remove('awsui-dark-mode'); + delete document.documentElement.dataset.theme; document.documentElement.style.colorScheme = ''; }); @@ -109,17 +110,17 @@ describe('App', () => { ); }); - it('shows the breadcrumb Workflows > the open workflow', async () => { + it('shows the breadcrumb Workflows > the open workflow, with no dead link', async () => { render(); const trail = await screen.findByRole('navigation', { name: 'Breadcrumbs', }); - expect(within(trail).getAllByText('Workflows').length).toBeGreaterThan(0); - // The trail holds a hidden copy that it measures, so the name appears twice. - expect((await within(trail).findAllByText('hello')).length).toBeGreaterThan( - 0, - ); + expect(within(trail).getByText('Workflows')).toBeTruthy(); + expect( + (await within(trail).findByText('hello')).closest('[aria-current]'), + ).toBeTruthy(); + expect(within(trail).queryAllByRole('link')).toEqual([]); }); it('switches to the dark mode, remembers it and switches back', () => { @@ -129,12 +130,14 @@ describe('App', () => { expect(document.body.classList.contains('awsui-dark-mode')).toBe(true); expect(document.documentElement.style.colorScheme).toBe('dark'); + expect(document.documentElement.dataset.theme).toBe('dark'); expect(window.localStorage.getItem(MODE_STORAGE_KEY)).toBe('dark'); expect(themeButton('dark').textContent).toBe('Theme: dark'); fireEvent.click(themeButton('dark')); expect(document.body.classList.contains('awsui-dark-mode')).toBe(false); + expect(document.documentElement.dataset.theme).toBe('light'); expect(window.localStorage.getItem(MODE_STORAGE_KEY)).toBe('light'); }); diff --git a/workflow-canvas/src/App.tsx b/workflow-canvas/src/App.tsx index b3326416..3f294dec 100644 --- a/workflow-canvas/src/App.tsx +++ b/workflow-canvas/src/App.tsx @@ -6,9 +6,11 @@ import Header from '@cloudscape-design/components/header'; import Tabs from '@cloudscape-design/components/tabs'; import type { ReactNode } from 'react'; import { Breadcrumbs } from './chrome/Breadcrumbs'; +import './chrome/chrome.css'; import { TOP_BAR_ID, TopBar } from './chrome/TopBar'; import { Designer } from './designer/Designer'; import { useSettled } from './designer/use-settled'; +import { TAB_STRIP_STYLE } from './theme/tokens'; import { useThemeMode } from './theme/use-theme-mode'; import { openWorkflowName, type LoadResult } from './workflow/load-workflow'; @@ -53,6 +55,7 @@ export function App({
+ {kind === 'tab' && } {kind === 'notch' && ( <> diff --git a/workflow-canvas/src/chrome/Breadcrumbs.tsx b/workflow-canvas/src/chrome/Breadcrumbs.tsx index 55fcaceb..71da872f 100644 --- a/workflow-canvas/src/chrome/Breadcrumbs.tsx +++ b/workflow-canvas/src/chrome/Breadcrumbs.tsx @@ -1,18 +1,36 @@ // SPDX-License-Identifier: Apache-2.0 -import BreadcrumbGroup from '@cloudscape-design/components/breadcrumb-group'; +import Box from '@cloudscape-design/components/box'; +import Icon from '@cloudscape-design/components/icon'; /** * The trail above the title: Workflows, then the workflow that is open. The - * page has no list of workflows yet, so both stay on this page. + * page has no list of workflows yet, so no crumb is a link; the last one is + * the current page. */ export function Breadcrumbs({ workflowName }: { workflowName?: string }) { - const items = [{ text: 'Workflows', href: '#' }]; - if (workflowName) items.push({ text: workflowName, href: '#' }); return ( - event.preventDefault()} - /> + ); } diff --git a/workflow-canvas/src/chrome/TopBar.tsx b/workflow-canvas/src/chrome/TopBar.tsx index 3cd89156..d4b450e9 100644 --- a/workflow-canvas/src/chrome/TopBar.tsx +++ b/workflow-canvas/src/chrome/TopBar.tsx @@ -23,6 +23,7 @@ export function TopBar({ event.preventDefault(), title: 'Workflow canvas', logo: { src: LOGO_DATA_URI, alt: 'Konductor' }, }} diff --git a/workflow-canvas/src/chrome/chrome.css b/workflow-canvas/src/chrome/chrome.css new file mode 100644 index 00000000..f990720d --- /dev/null +++ b/workflow-canvas/src/chrome/chrome.css @@ -0,0 +1,27 @@ +/* SPDX-License-Identifier: Apache-2.0 */ + +/* + * The strip of the Designer and Execution tabs is part of the dark header + * band: it is painted with the band colour across the full width of the + * page, so the tab labels, the selected-tab indicator and the rule under + * them all sit on the band. The colours come from the theme module through + * `--wf-band-background` and `--wf-band-divider`. + */ +.wf-tab-strip :has(> * > [role='tablist']) { + background: var(--wf-band-background); + box-shadow: 0 0 0 100vmax var(--wf-band-background); + /* The library sets this colour with a more specific rule. */ + border-block-end-color: var(--wf-band-divider) !important; + clip-path: inset(0 -100vmax); +} + +/* The trail above the title. */ +.wf-crumbs { + display: flex; + flex-wrap: wrap; + align-items: center; + gap: 8px; + margin: 0; + padding: 0; + list-style: none; +} diff --git a/workflow-canvas/src/theme/install.ts b/workflow-canvas/src/theme/install.ts index 0e9e6152..2ebc59ba 100644 --- a/workflow-canvas/src/theme/install.ts +++ b/workflow-canvas/src/theme/install.ts @@ -3,13 +3,24 @@ import { applyMode, Mode } from '@cloudscape-design/global-styles'; import { applyTheme } from '@cloudscape-design/components/theming'; import { APP_THEME, + BAND, FONT_TEXT, TAB_STRIP_THEME, type ThemeMode, } from './tokens'; -/** Where the tab strip of the page header is, for `TAB_STRIP_THEME`. */ -export const TAB_STRIP_SELECTOR = '.wf-tab-strip [role="tablist"]'; +/** + * Where the strip of the page tabs is, for `TAB_STRIP_THEME`: the element + * that holds the tab list, found through its role so no library class is + * needed. `chrome.css` paints the same element with the band. + */ +export const TAB_STRIP_SELECTOR = '.wf-tab-strip :has(> * > [role="tablist"])'; + +// The band colours that `chrome.css` reads, as custom properties. +const BAND_VARIABLES = { + '--wf-band-background': BAND.background, + '--wf-band-divider': BAND.divider, +} as const; /** * Applies the konductor docs theme to the page. Call it once, before the @@ -18,12 +29,19 @@ export const TAB_STRIP_SELECTOR = '.wf-tab-strip [role="tablist"]'; */ export function installTheme(body: HTMLElement): void { body.style.fontFamily = FONT_TEXT; + for (const [name, value] of Object.entries(BAND_VARIABLES)) { + body.style.setProperty(name, value); + } applyTheme({ theme: APP_THEME }); applyTheme({ theme: TAB_STRIP_THEME, selector: TAB_STRIP_SELECTOR }); } -/** Switches the page, its components and its native controls to `mode`. */ +/** + * Switches the page, its components and its native controls to `mode`. The + * root's `data-theme` is what the canvas styles key on. + */ export function showMode(mode: ThemeMode, root: HTMLElement): void { applyMode(mode === 'dark' ? Mode.Dark : Mode.Light); root.style.colorScheme = mode; + root.dataset.theme = mode; } diff --git a/workflow-canvas/src/theme/mode.ts b/workflow-canvas/src/theme/mode.ts index 23592863..987cee84 100644 --- a/workflow-canvas/src/theme/mode.ts +++ b/workflow-canvas/src/theme/mode.ts @@ -14,18 +14,11 @@ function isMode(value: unknown): value is ThemeMode { return value === 'light' || value === 'dark'; } -// Reading `localStorage` can throw when the browser blocks storage. -function storageOf(env: ModeEnvironment) { - try { - return env.localStorage; - } catch { - return undefined; - } -} - +// Reading `localStorage` can throw when the browser blocks storage, so the +// read of the property is inside the `try` too. function storedMode(env: ModeEnvironment): ThemeMode | undefined { try { - const value = storageOf(env)?.getItem(MODE_STORAGE_KEY); + const value = env.localStorage?.getItem(MODE_STORAGE_KEY); return isMode(value) ? value : undefined; } catch { return undefined; @@ -53,7 +46,7 @@ export function otherMode(mode: ThemeMode): ThemeMode { /** Keeps a chosen mode for the next visit. A blocked storage is ignored. */ export function rememberMode(env: ModeEnvironment, mode: ThemeMode): void { try { - storageOf(env)?.setItem(MODE_STORAGE_KEY, mode); + env.localStorage?.setItem(MODE_STORAGE_KEY, mode); } catch { // The choice then lasts until the page closes. } diff --git a/workflow-canvas/src/theme/tokens.test.ts b/workflow-canvas/src/theme/tokens.test.ts index 5f192bda..90ffca81 100644 --- a/workflow-canvas/src/theme/tokens.test.ts +++ b/workflow-canvas/src/theme/tokens.test.ts @@ -1,7 +1,14 @@ // SPDX-License-Identifier: Apache-2.0 import { describe, expect, it } from 'vitest'; import { AA_GRAPHIC, AA_TEXT, contrastRatio } from './contrast'; -import { APP_THEME, BAND_BACKGROUND, colorsOf, type ThemeMode } from './tokens'; +import { + APP_THEME, + BAND, + colorsOf, + TAB_STRIP_STYLE, + TAB_STRIP_THEME, + type ThemeMode, +} from './tokens'; const MODES: ThemeMode[] = ['light', 'dark']; @@ -41,17 +48,32 @@ describe.each(MODES)('the %s palette', (mode) => { }); describe('the header band', () => { - it.each([ - ['#f5f3ee', 'text'], - ['#c9c2e0', 'muted text'], - ['#c4b2ff', 'links'], - ])('has AA contrast for %s (%s)', (colour) => { - expect(contrastRatio(colour, BAND_BACKGROUND)).toBeGreaterThanOrEqual( - AA_TEXT, + it.each([BAND.text, BAND.textMuted, BAND.link])( + 'has AA contrast for %s', + (colour) => { + expect(contrastRatio(colour, BAND.background)).toBeGreaterThanOrEqual( + AA_TEXT, + ); + }, + ); + + it('draws the selected-tab indicator at 3:1 or more on the band', () => { + expect(contrastRatio(BAND.link, BAND.background)).toBeGreaterThanOrEqual( + AA_GRAPHIC, ); }); }); +describe('TAB_STRIP_THEME', () => { + it('colours the selected-tab indicator from the band', () => { + expect(TAB_STRIP_THEME.tokens.colorBorderItemSelected).toBe(BAND.link); + }); + + it('colours the line between tabs from the band', () => { + expect(TAB_STRIP_STYLE.tabSeparator.color).toBe(BAND.divider); + }); +}); + describe('APP_THEME', () => { it('gives the base font to the page', () => { expect(APP_THEME.tokens.fontFamilyBase).toMatch(/^Outfit,/); diff --git a/workflow-canvas/src/theme/tokens.ts b/workflow-canvas/src/theme/tokens.ts index 623089b6..b2e6b7c2 100644 --- a/workflow-canvas/src/theme/tokens.ts +++ b/workflow-canvas/src/theme/tokens.ts @@ -105,13 +105,17 @@ export function colorsOf(mode: ThemeMode): Colors { /** * The header band and the top bar are dark in both modes. These are the - * colours of the text and controls on them; each pair reaches 4.5:1 on - * `BAND_BACKGROUND`. + * colours of the text and controls on them. Text and the selected-tab + * indicator reach their contrast (4.5:1 and 3:1) on `background`. */ -export const BAND_BACKGROUND = '#141026'; -const BAND_TEXT = '#f5f3ee'; -const BAND_TEXT_MUTED = '#c9c2e0'; -const BAND_LINK = '#c4b2ff'; +export const BAND = { + background: '#141026', + text: '#f5f3ee', + textMuted: '#c9c2e0', + link: '#c4b2ff', + /** The hairline under the top bar and under the tabs. */ + divider: '#2a2450', +} as const; /** The font of the page text. Self-hosted by `fonts.ts`. */ export const FONT_TEXT = @@ -134,7 +138,7 @@ export const LOGO_DATA_URI = `data:image/svg+xml;utf8,${encodeURIComponent( type Tokens = Theme['tokens']; -/** A token that has one value in light mode and one in dark mode. */ +// A token that has one value in light mode and one in dark mode. function perMode(pick: (colors: Colors) => string) { return { light: pick(LIGHT), dark: pick(DARK) }; } @@ -155,7 +159,7 @@ function backgroundTokens(): Tokens { colorBackgroundItemSelected: perMode((c) => c.tintStrong), colorBackgroundCellShaded: perMode((c) => c.surfaceAlt), colorBackgroundToggleDefault: perMode((c) => c.control), - colorBackgroundHomeHeader: BAND_BACKGROUND, + colorBackgroundHomeHeader: BAND.background, colorBackgroundStatusError: perMode((c) => c.errorBg), colorBackgroundStatusInfo: perMode((c) => c.infoBg), colorBackgroundStatusSuccess: perMode((c) => c.successBg), @@ -214,8 +218,8 @@ function textTokens(): Tokens { colorTextDropdownItemHighlighted: perMode((c) => c.text), colorTextDropdownItemSecondary: perMode((c) => c.textMuted), colorTextGroupLabel: perMode((c) => c.textMuted), - colorTextHomeHeaderDefault: BAND_TEXT, - colorTextHomeHeaderSecondary: BAND_TEXT_MUTED, + colorTextHomeHeaderDefault: BAND.text, + colorTextHomeHeaderSecondary: BAND.textMuted, colorTextBreadcrumbCurrent: perMode((c) => c.textMuted), colorTextBreadcrumbIcon: perMode((c) => c.textMuted), }; @@ -263,23 +267,21 @@ function shapeTokens(): Tokens { }; } -/** - * Inside the dark band the global text and border colours do not apply, so - * the same roles are set again here. Cloudscape reads these in the page - * header, which holds the breadcrumbs and the title. - */ +// Inside the dark band the global text and border colours do not apply, so +// the same roles are set again here. Cloudscape reads these in the page +// header, which holds the breadcrumbs and the title. function headerBandTokens(): Tokens { return { - colorTextAccent: BAND_LINK, - colorTextLinkDefault: BAND_LINK, + colorTextAccent: BAND.link, + colorTextLinkDefault: BAND.link, colorTextLinkHover: '#ffffff', - colorTextBodySecondary: BAND_TEXT_MUTED, - colorTextBreadcrumbCurrent: BAND_TEXT_MUTED, - colorTextBreadcrumbIcon: BAND_TEXT_MUTED, - colorTextInteractiveDefault: BAND_TEXT_MUTED, + colorTextBodySecondary: BAND.textMuted, + colorTextBreadcrumbCurrent: BAND.textMuted, + colorTextBreadcrumbIcon: BAND.textMuted, + colorTextInteractiveDefault: BAND.textMuted, colorTextInteractiveHover: '#ffffff', colorTextInteractiveActive: '#ffffff', - colorBorderItemFocused: BAND_LINK, + colorBorderItemFocused: BAND.link, }; } @@ -299,29 +301,40 @@ export const APP_THEME: Theme = { header: { tokens: { ...headerBandTokens(), - colorBackgroundLayoutMain: BAND_BACKGROUND, + colorBackgroundLayoutMain: BAND.background, }, }, 'top-navigation': { tokens: { ...headerBandTokens(), - colorBackgroundContainerContent: BAND_BACKGROUND, - colorTextHeadingDefault: BAND_TEXT, - colorTextTopNavigationTitle: BAND_TEXT, - colorTextInteractiveDefault: BAND_TEXT, + colorBackgroundContainerContent: BAND.background, + colorTextHeadingDefault: BAND.text, + colorTextTopNavigationTitle: BAND.text, + colorTextInteractiveDefault: BAND.text, colorTextInteractiveHover: '#ffffff', - colorBorderDividerDefault: '#2a2450', + colorBorderDividerDefault: BAND.divider, }, }, }, }; /** - * The same roles for the Designer and Execution tabs, which sit on the band - * but are page content, so the header context does not reach them. + * The same roles for the strip of the Designer and Execution tabs, which is + * page content that sits on the band, so the header context does not reach + * it. The selected-tab indicator is set too. */ export const TAB_STRIP_THEME: Theme = { tokens: { ...headerBandTokens(), + colorBorderItemSelected: BAND.link, }, }; + +/** + * The `style` of the page tabs: the line between two tabs, in the colour of + * the other hairlines of the band. (The rule under the strip is set in + * `chrome.css`.) + */ +export const TAB_STRIP_STYLE = { + tabSeparator: { color: BAND.divider }, +} as const; diff --git a/workflow-canvas/test-support/canvas-contrast.test.ts b/workflow-canvas/test-support/canvas-contrast.test.ts index a8fddfb0..ef267f3a 100644 --- a/workflow-canvas/test-support/canvas-contrast.test.ts +++ b/workflow-canvas/test-support/canvas-contrast.test.ts @@ -2,62 +2,89 @@ import { readFileSync } from 'node:fs'; import { describe, expect, it } from 'vitest'; import { AA_GRAPHIC, AA_TEXT, contrastRatio } from '../src/theme/contrast.ts'; +import { colorsOf, RADIUS, type ThemeMode } from '../src/theme/tokens.ts'; const css = readFileSync( new URL('../src/canvas/canvas.css', import.meta.url), 'utf8', ); -describe('canvas.css colours', () => { - // The declarations of the rule that starts with `selector`. - function variables(selector: string): Record { - const block = css.split(`${selector} {`)[1]?.split('}')[0] ?? ''; - return Object.fromEntries( - [...block.matchAll(/(--[\w-]+):\s*(#[0-9a-f]{6})/g)].map((m) => [ - m[1], - m[2], - ]), - ); - } +// The declarations of the rule that starts with `selector`. +function block(selector: string): string { + return css.split(`${selector} {`)[1]?.split('}')[0] ?? ''; +} - const light = variables('.wf-canvas'); - const dark = { ...light, ...variables('.awsui-dark-mode .wf-canvas') }; +function variables(selector: string): Record { + return Object.fromEntries( + [...block(selector).matchAll(/(--[\w-]+):\s*(#[0-9a-f]{6})/g)].map((m) => [ + m[1], + m[2], + ]), + ); +} - it.each([ - ['light', light], - ['dark', dark], - ])('keeps the canvas text readable in %s', (_mode, v) => { - expect( - contrastRatio(v['--wf-text']!, v['--wf-node-bg']!), - ).toBeGreaterThanOrEqual(AA_TEXT); - expect( - contrastRatio(v['--wf-text-muted']!, v['--wf-node-bg']!), - ).toBeGreaterThanOrEqual(AA_TEXT); - expect( - contrastRatio(v['--wf-text-muted']!, v['--wf-canvas-bg']!), - ).toBeGreaterThanOrEqual(AA_TEXT); - expect( - contrastRatio(v['--wf-text']!, v['--wf-controls-bg']!), - ).toBeGreaterThanOrEqual(AA_TEXT); +const light = variables('.wf-canvas'); +const dark = { ...light, ...variables(":root[data-theme='dark'] .wf-canvas") }; +const MODES: [ThemeMode, Record][] = [ + ['light', light], + ['dark', dark], +]; + +describe.each(MODES)('canvas.css colours in %s', (mode, v) => { + const ratio = (a: string, b: string) => contrastRatio(v[a]!, v[b]!); + + it('keeps the canvas text readable', () => { + expect(ratio('--wf-text', '--wf-node-bg')).toBeGreaterThanOrEqual(AA_TEXT); + expect(ratio('--wf-text-muted', '--wf-node-bg')).toBeGreaterThanOrEqual( + AA_TEXT, + ); + expect(ratio('--wf-text-muted', '--wf-canvas-bg')).toBeGreaterThanOrEqual( + AA_TEXT, + ); + expect(ratio('--wf-text', '--wf-controls-bg')).toBeGreaterThanOrEqual( + AA_TEXT, + ); }); - it.each([ - ['light', light], - ['dark', dark], - ])('keeps nodes, edges and the focus ring visible in %s', (_mode, v) => { + it('keeps nodes, edges and the focus ring visible', () => { for (const name of ['--wf-node-border', '--wf-edge', '--wf-focus']) { - expect( - contrastRatio(v[name]!, v['--wf-canvas-bg']!), - ).toBeGreaterThanOrEqual(AA_GRAPHIC); + expect(ratio(name, '--wf-canvas-bg')).toBeGreaterThanOrEqual(AA_GRAPHIC); } }); it('keeps the port letters readable on their ports', () => { expect( - contrastRatio(light['--wf-port-letter-out']!, light['--wf-port-fill']!), + ratio('--wf-port-letter-out', '--wf-port-fill'), ).toBeGreaterThanOrEqual(AA_TEXT); expect( - contrastRatio(light['--wf-port-letter-in']!, light['--wf-port-inner']!), + ratio('--wf-port-letter-in', '--wf-port-inner'), ).toBeGreaterThanOrEqual(AA_TEXT); }); + + it('keeps the ports visible on the canvas and on the node', () => { + for (const part of ['--wf-port-fill', '--wf-port-ring']) { + expect(ratio(part, '--wf-canvas-bg')).toBeGreaterThanOrEqual(AA_GRAPHIC); + expect(ratio(part, '--wf-node-bg')).toBeGreaterThanOrEqual(AA_GRAPHIC); + } + }); + + it('uses the colours of the theme module', () => { + const c = colorsOf(mode); + + expect(v['--wf-canvas-bg']).toBe(c.bg); + expect(v['--wf-node-bg']).toBe(c.surface); + expect(v['--wf-node-border']).toBe(c.control); + expect(v['--wf-text']).toBe(c.text); + expect(v['--wf-focus']).toBe(c.link); + expect(v['--wf-controls-bg']).toBe(c.surface); + expect(v['--wf-controls-border']).toBe(c.borderStrong); + expect(v['--wf-controls-bg-hover']).toBe(c.infoBg); + }); +}); + +describe('canvas.css shapes', () => { + it('uses the radii of the theme module', () => { + expect(block('.wf-canvas')).toContain(`border-radius: ${RADIUS.card}`); + expect(block('.wf-node')).toContain(`border-radius: ${RADIUS.control}`); + }); }); diff --git a/workflow-canvas/test-support/early-mode.test.ts b/workflow-canvas/test-support/early-mode.test.ts new file mode 100644 index 00000000..6443c5e2 --- /dev/null +++ b/workflow-canvas/test-support/early-mode.test.ts @@ -0,0 +1,55 @@ +// SPDX-License-Identifier: Apache-2.0 +import { readFileSync } from 'node:fs'; +import { describe, expect, it } from 'vitest'; +import { MODE_STORAGE_KEY } from '../src/theme/mode.ts'; + +const html = readFileSync(new URL('../index.html', import.meta.url), 'utf8'); +const script = /'], + [ + 'an upper case tag with an attribute', + '', + ], + ['an end tag with a space', ''], + ['an end tag with an attribute', ''], + ['a start tag with a space', ''], + ])('finds the body in %s', (_, html) => { + expect(inlineScriptOf(html)).toBe('a()'); + }); + + it('skips a script with no body, such as one that loads a file', () => { + expect( + inlineScriptOf( + '', + ), + ).toBe('b()'); + }); + + it('does not take a tag whose name only starts with script', () => { + expect(inlineScriptOf('a()')).toBe(''); + }); + + it('is empty when there is no script', () => { + expect(inlineScriptOf('

none

')).toBe(''); + }); +}); diff --git a/workflow-canvas/test-support/inline-script.ts b/workflow-canvas/test-support/inline-script.ts new file mode 100644 index 00000000..7faa9a6b --- /dev/null +++ b/workflow-canvas/test-support/inline-script.ts @@ -0,0 +1,13 @@ +// SPDX-License-Identifier: Apache-2.0 + +// Tag names are case-insensitive, a start tag may carry attributes, and an end +// tag may carry whitespace. +const SCRIPT = /]*>([\s\S]*?)<\/script\b[^>]*>/gi; + +/** The body of the first `
diff --git a/workflow-canvas/src/App.test.tsx b/workflow-canvas/src/App.test.tsx index 1f151f3c..698c05ed 100644 --- a/workflow-canvas/src/App.test.tsx +++ b/workflow-canvas/src/App.test.tsx @@ -20,6 +20,7 @@ const themeButton = (mode: 'light' | 'dark') => afterEach(() => { window.localStorage.clear(); document.body.classList.remove('awsui-dark-mode'); + delete document.documentElement.dataset.theme; document.documentElement.style.colorScheme = ''; }); @@ -109,17 +110,17 @@ describe('App', () => { ); }); - it('shows the breadcrumb Workflows > the open workflow', async () => { + it('shows the breadcrumb Workflows > the open workflow, with no dead link', async () => { render(); const trail = await screen.findByRole('navigation', { name: 'Breadcrumbs', }); - expect(within(trail).getAllByText('Workflows').length).toBeGreaterThan(0); - // The trail holds a hidden copy that it measures, so the name appears twice. - expect((await within(trail).findAllByText('hello')).length).toBeGreaterThan( - 0, - ); + expect(within(trail).getByText('Workflows')).toBeTruthy(); + expect( + (await within(trail).findByText('hello')).closest('[aria-current]'), + ).toBeTruthy(); + expect(within(trail).queryAllByRole('link')).toEqual([]); }); it('switches to the dark mode, remembers it and switches back', () => { @@ -129,12 +130,14 @@ describe('App', () => { expect(document.body.classList.contains('awsui-dark-mode')).toBe(true); expect(document.documentElement.style.colorScheme).toBe('dark'); + expect(document.documentElement.dataset.theme).toBe('dark'); expect(window.localStorage.getItem(MODE_STORAGE_KEY)).toBe('dark'); expect(themeButton('dark').textContent).toBe('Theme: dark'); fireEvent.click(themeButton('dark')); expect(document.body.classList.contains('awsui-dark-mode')).toBe(false); + expect(document.documentElement.dataset.theme).toBe('light'); expect(window.localStorage.getItem(MODE_STORAGE_KEY)).toBe('light'); }); diff --git a/workflow-canvas/src/App.tsx b/workflow-canvas/src/App.tsx index b3326416..3f294dec 100644 --- a/workflow-canvas/src/App.tsx +++ b/workflow-canvas/src/App.tsx @@ -6,9 +6,11 @@ import Header from '@cloudscape-design/components/header'; import Tabs from '@cloudscape-design/components/tabs'; import type { ReactNode } from 'react'; import { Breadcrumbs } from './chrome/Breadcrumbs'; +import './chrome/chrome.css'; import { TOP_BAR_ID, TopBar } from './chrome/TopBar'; import { Designer } from './designer/Designer'; import { useSettled } from './designer/use-settled'; +import { TAB_STRIP_STYLE } from './theme/tokens'; import { useThemeMode } from './theme/use-theme-mode'; import { openWorkflowName, type LoadResult } from './workflow/load-workflow'; @@ -53,6 +55,7 @@ export function App({
+ {kind === 'tab' && } {kind === 'notch' && ( <> diff --git a/workflow-canvas/src/chrome/Breadcrumbs.tsx b/workflow-canvas/src/chrome/Breadcrumbs.tsx index 55fcaceb..71da872f 100644 --- a/workflow-canvas/src/chrome/Breadcrumbs.tsx +++ b/workflow-canvas/src/chrome/Breadcrumbs.tsx @@ -1,18 +1,36 @@ // SPDX-License-Identifier: Apache-2.0 -import BreadcrumbGroup from '@cloudscape-design/components/breadcrumb-group'; +import Box from '@cloudscape-design/components/box'; +import Icon from '@cloudscape-design/components/icon'; /** * The trail above the title: Workflows, then the workflow that is open. The - * page has no list of workflows yet, so both stay on this page. + * page has no list of workflows yet, so no crumb is a link; the last one is + * the current page. */ export function Breadcrumbs({ workflowName }: { workflowName?: string }) { - const items = [{ text: 'Workflows', href: '#' }]; - if (workflowName) items.push({ text: workflowName, href: '#' }); return ( - event.preventDefault()} - /> + ); } diff --git a/workflow-canvas/src/chrome/TopBar.tsx b/workflow-canvas/src/chrome/TopBar.tsx index 3cd89156..d4b450e9 100644 --- a/workflow-canvas/src/chrome/TopBar.tsx +++ b/workflow-canvas/src/chrome/TopBar.tsx @@ -23,6 +23,7 @@ export function TopBar({ event.preventDefault(), title: 'Workflow canvas', logo: { src: LOGO_DATA_URI, alt: 'Konductor' }, }} diff --git a/workflow-canvas/src/chrome/chrome.css b/workflow-canvas/src/chrome/chrome.css new file mode 100644 index 00000000..f990720d --- /dev/null +++ b/workflow-canvas/src/chrome/chrome.css @@ -0,0 +1,27 @@ +/* SPDX-License-Identifier: Apache-2.0 */ + +/* + * The strip of the Designer and Execution tabs is part of the dark header + * band: it is painted with the band colour across the full width of the + * page, so the tab labels, the selected-tab indicator and the rule under + * them all sit on the band. The colours come from the theme module through + * `--wf-band-background` and `--wf-band-divider`. + */ +.wf-tab-strip :has(> * > [role='tablist']) { + background: var(--wf-band-background); + box-shadow: 0 0 0 100vmax var(--wf-band-background); + /* The library sets this colour with a more specific rule. */ + border-block-end-color: var(--wf-band-divider) !important; + clip-path: inset(0 -100vmax); +} + +/* The trail above the title. */ +.wf-crumbs { + display: flex; + flex-wrap: wrap; + align-items: center; + gap: 8px; + margin: 0; + padding: 0; + list-style: none; +} diff --git a/workflow-canvas/src/theme/install.ts b/workflow-canvas/src/theme/install.ts index 0e9e6152..2ebc59ba 100644 --- a/workflow-canvas/src/theme/install.ts +++ b/workflow-canvas/src/theme/install.ts @@ -3,13 +3,24 @@ import { applyMode, Mode } from '@cloudscape-design/global-styles'; import { applyTheme } from '@cloudscape-design/components/theming'; import { APP_THEME, + BAND, FONT_TEXT, TAB_STRIP_THEME, type ThemeMode, } from './tokens'; -/** Where the tab strip of the page header is, for `TAB_STRIP_THEME`. */ -export const TAB_STRIP_SELECTOR = '.wf-tab-strip [role="tablist"]'; +/** + * Where the strip of the page tabs is, for `TAB_STRIP_THEME`: the element + * that holds the tab list, found through its role so no library class is + * needed. `chrome.css` paints the same element with the band. + */ +export const TAB_STRIP_SELECTOR = '.wf-tab-strip :has(> * > [role="tablist"])'; + +// The band colours that `chrome.css` reads, as custom properties. +const BAND_VARIABLES = { + '--wf-band-background': BAND.background, + '--wf-band-divider': BAND.divider, +} as const; /** * Applies the konductor docs theme to the page. Call it once, before the @@ -18,12 +29,19 @@ export const TAB_STRIP_SELECTOR = '.wf-tab-strip [role="tablist"]'; */ export function installTheme(body: HTMLElement): void { body.style.fontFamily = FONT_TEXT; + for (const [name, value] of Object.entries(BAND_VARIABLES)) { + body.style.setProperty(name, value); + } applyTheme({ theme: APP_THEME }); applyTheme({ theme: TAB_STRIP_THEME, selector: TAB_STRIP_SELECTOR }); } -/** Switches the page, its components and its native controls to `mode`. */ +/** + * Switches the page, its components and its native controls to `mode`. The + * root's `data-theme` is what the canvas styles key on. + */ export function showMode(mode: ThemeMode, root: HTMLElement): void { applyMode(mode === 'dark' ? Mode.Dark : Mode.Light); root.style.colorScheme = mode; + root.dataset.theme = mode; } diff --git a/workflow-canvas/src/theme/mode.ts b/workflow-canvas/src/theme/mode.ts index 23592863..987cee84 100644 --- a/workflow-canvas/src/theme/mode.ts +++ b/workflow-canvas/src/theme/mode.ts @@ -14,18 +14,11 @@ function isMode(value: unknown): value is ThemeMode { return value === 'light' || value === 'dark'; } -// Reading `localStorage` can throw when the browser blocks storage. -function storageOf(env: ModeEnvironment) { - try { - return env.localStorage; - } catch { - return undefined; - } -} - +// Reading `localStorage` can throw when the browser blocks storage, so the +// read of the property is inside the `try` too. function storedMode(env: ModeEnvironment): ThemeMode | undefined { try { - const value = storageOf(env)?.getItem(MODE_STORAGE_KEY); + const value = env.localStorage?.getItem(MODE_STORAGE_KEY); return isMode(value) ? value : undefined; } catch { return undefined; @@ -53,7 +46,7 @@ export function otherMode(mode: ThemeMode): ThemeMode { /** Keeps a chosen mode for the next visit. A blocked storage is ignored. */ export function rememberMode(env: ModeEnvironment, mode: ThemeMode): void { try { - storageOf(env)?.setItem(MODE_STORAGE_KEY, mode); + env.localStorage?.setItem(MODE_STORAGE_KEY, mode); } catch { // The choice then lasts until the page closes. } diff --git a/workflow-canvas/src/theme/tokens.test.ts b/workflow-canvas/src/theme/tokens.test.ts index 5f192bda..90ffca81 100644 --- a/workflow-canvas/src/theme/tokens.test.ts +++ b/workflow-canvas/src/theme/tokens.test.ts @@ -1,7 +1,14 @@ // SPDX-License-Identifier: Apache-2.0 import { describe, expect, it } from 'vitest'; import { AA_GRAPHIC, AA_TEXT, contrastRatio } from './contrast'; -import { APP_THEME, BAND_BACKGROUND, colorsOf, type ThemeMode } from './tokens'; +import { + APP_THEME, + BAND, + colorsOf, + TAB_STRIP_STYLE, + TAB_STRIP_THEME, + type ThemeMode, +} from './tokens'; const MODES: ThemeMode[] = ['light', 'dark']; @@ -41,17 +48,32 @@ describe.each(MODES)('the %s palette', (mode) => { }); describe('the header band', () => { - it.each([ - ['#f5f3ee', 'text'], - ['#c9c2e0', 'muted text'], - ['#c4b2ff', 'links'], - ])('has AA contrast for %s (%s)', (colour) => { - expect(contrastRatio(colour, BAND_BACKGROUND)).toBeGreaterThanOrEqual( - AA_TEXT, + it.each([BAND.text, BAND.textMuted, BAND.link])( + 'has AA contrast for %s', + (colour) => { + expect(contrastRatio(colour, BAND.background)).toBeGreaterThanOrEqual( + AA_TEXT, + ); + }, + ); + + it('draws the selected-tab indicator at 3:1 or more on the band', () => { + expect(contrastRatio(BAND.link, BAND.background)).toBeGreaterThanOrEqual( + AA_GRAPHIC, ); }); }); +describe('TAB_STRIP_THEME', () => { + it('colours the selected-tab indicator from the band', () => { + expect(TAB_STRIP_THEME.tokens.colorBorderItemSelected).toBe(BAND.link); + }); + + it('colours the line between tabs from the band', () => { + expect(TAB_STRIP_STYLE.tabSeparator.color).toBe(BAND.divider); + }); +}); + describe('APP_THEME', () => { it('gives the base font to the page', () => { expect(APP_THEME.tokens.fontFamilyBase).toMatch(/^Outfit,/); diff --git a/workflow-canvas/src/theme/tokens.ts b/workflow-canvas/src/theme/tokens.ts index 623089b6..b2e6b7c2 100644 --- a/workflow-canvas/src/theme/tokens.ts +++ b/workflow-canvas/src/theme/tokens.ts @@ -105,13 +105,17 @@ export function colorsOf(mode: ThemeMode): Colors { /** * The header band and the top bar are dark in both modes. These are the - * colours of the text and controls on them; each pair reaches 4.5:1 on - * `BAND_BACKGROUND`. + * colours of the text and controls on them. Text and the selected-tab + * indicator reach their contrast (4.5:1 and 3:1) on `background`. */ -export const BAND_BACKGROUND = '#141026'; -const BAND_TEXT = '#f5f3ee'; -const BAND_TEXT_MUTED = '#c9c2e0'; -const BAND_LINK = '#c4b2ff'; +export const BAND = { + background: '#141026', + text: '#f5f3ee', + textMuted: '#c9c2e0', + link: '#c4b2ff', + /** The hairline under the top bar and under the tabs. */ + divider: '#2a2450', +} as const; /** The font of the page text. Self-hosted by `fonts.ts`. */ export const FONT_TEXT = @@ -134,7 +138,7 @@ export const LOGO_DATA_URI = `data:image/svg+xml;utf8,${encodeURIComponent( type Tokens = Theme['tokens']; -/** A token that has one value in light mode and one in dark mode. */ +// A token that has one value in light mode and one in dark mode. function perMode(pick: (colors: Colors) => string) { return { light: pick(LIGHT), dark: pick(DARK) }; } @@ -155,7 +159,7 @@ function backgroundTokens(): Tokens { colorBackgroundItemSelected: perMode((c) => c.tintStrong), colorBackgroundCellShaded: perMode((c) => c.surfaceAlt), colorBackgroundToggleDefault: perMode((c) => c.control), - colorBackgroundHomeHeader: BAND_BACKGROUND, + colorBackgroundHomeHeader: BAND.background, colorBackgroundStatusError: perMode((c) => c.errorBg), colorBackgroundStatusInfo: perMode((c) => c.infoBg), colorBackgroundStatusSuccess: perMode((c) => c.successBg), @@ -214,8 +218,8 @@ function textTokens(): Tokens { colorTextDropdownItemHighlighted: perMode((c) => c.text), colorTextDropdownItemSecondary: perMode((c) => c.textMuted), colorTextGroupLabel: perMode((c) => c.textMuted), - colorTextHomeHeaderDefault: BAND_TEXT, - colorTextHomeHeaderSecondary: BAND_TEXT_MUTED, + colorTextHomeHeaderDefault: BAND.text, + colorTextHomeHeaderSecondary: BAND.textMuted, colorTextBreadcrumbCurrent: perMode((c) => c.textMuted), colorTextBreadcrumbIcon: perMode((c) => c.textMuted), }; @@ -263,23 +267,21 @@ function shapeTokens(): Tokens { }; } -/** - * Inside the dark band the global text and border colours do not apply, so - * the same roles are set again here. Cloudscape reads these in the page - * header, which holds the breadcrumbs and the title. - */ +// Inside the dark band the global text and border colours do not apply, so +// the same roles are set again here. Cloudscape reads these in the page +// header, which holds the breadcrumbs and the title. function headerBandTokens(): Tokens { return { - colorTextAccent: BAND_LINK, - colorTextLinkDefault: BAND_LINK, + colorTextAccent: BAND.link, + colorTextLinkDefault: BAND.link, colorTextLinkHover: '#ffffff', - colorTextBodySecondary: BAND_TEXT_MUTED, - colorTextBreadcrumbCurrent: BAND_TEXT_MUTED, - colorTextBreadcrumbIcon: BAND_TEXT_MUTED, - colorTextInteractiveDefault: BAND_TEXT_MUTED, + colorTextBodySecondary: BAND.textMuted, + colorTextBreadcrumbCurrent: BAND.textMuted, + colorTextBreadcrumbIcon: BAND.textMuted, + colorTextInteractiveDefault: BAND.textMuted, colorTextInteractiveHover: '#ffffff', colorTextInteractiveActive: '#ffffff', - colorBorderItemFocused: BAND_LINK, + colorBorderItemFocused: BAND.link, }; } @@ -299,29 +301,40 @@ export const APP_THEME: Theme = { header: { tokens: { ...headerBandTokens(), - colorBackgroundLayoutMain: BAND_BACKGROUND, + colorBackgroundLayoutMain: BAND.background, }, }, 'top-navigation': { tokens: { ...headerBandTokens(), - colorBackgroundContainerContent: BAND_BACKGROUND, - colorTextHeadingDefault: BAND_TEXT, - colorTextTopNavigationTitle: BAND_TEXT, - colorTextInteractiveDefault: BAND_TEXT, + colorBackgroundContainerContent: BAND.background, + colorTextHeadingDefault: BAND.text, + colorTextTopNavigationTitle: BAND.text, + colorTextInteractiveDefault: BAND.text, colorTextInteractiveHover: '#ffffff', - colorBorderDividerDefault: '#2a2450', + colorBorderDividerDefault: BAND.divider, }, }, }, }; /** - * The same roles for the Designer and Execution tabs, which sit on the band - * but are page content, so the header context does not reach them. + * The same roles for the strip of the Designer and Execution tabs, which is + * page content that sits on the band, so the header context does not reach + * it. The selected-tab indicator is set too. */ export const TAB_STRIP_THEME: Theme = { tokens: { ...headerBandTokens(), + colorBorderItemSelected: BAND.link, }, }; + +/** + * The `style` of the page tabs: the line between two tabs, in the colour of + * the other hairlines of the band. (The rule under the strip is set in + * `chrome.css`.) + */ +export const TAB_STRIP_STYLE = { + tabSeparator: { color: BAND.divider }, +} as const; diff --git a/workflow-canvas/test-support/canvas-contrast.test.ts b/workflow-canvas/test-support/canvas-contrast.test.ts index a8fddfb0..ef267f3a 100644 --- a/workflow-canvas/test-support/canvas-contrast.test.ts +++ b/workflow-canvas/test-support/canvas-contrast.test.ts @@ -2,62 +2,89 @@ import { readFileSync } from 'node:fs'; import { describe, expect, it } from 'vitest'; import { AA_GRAPHIC, AA_TEXT, contrastRatio } from '../src/theme/contrast.ts'; +import { colorsOf, RADIUS, type ThemeMode } from '../src/theme/tokens.ts'; const css = readFileSync( new URL('../src/canvas/canvas.css', import.meta.url), 'utf8', ); -describe('canvas.css colours', () => { - // The declarations of the rule that starts with `selector`. - function variables(selector: string): Record { - const block = css.split(`${selector} {`)[1]?.split('}')[0] ?? ''; - return Object.fromEntries( - [...block.matchAll(/(--[\w-]+):\s*(#[0-9a-f]{6})/g)].map((m) => [ - m[1], - m[2], - ]), - ); - } +// The declarations of the rule that starts with `selector`. +function block(selector: string): string { + return css.split(`${selector} {`)[1]?.split('}')[0] ?? ''; +} - const light = variables('.wf-canvas'); - const dark = { ...light, ...variables('.awsui-dark-mode .wf-canvas') }; +function variables(selector: string): Record { + return Object.fromEntries( + [...block(selector).matchAll(/(--[\w-]+):\s*(#[0-9a-f]{6})/g)].map((m) => [ + m[1], + m[2], + ]), + ); +} - it.each([ - ['light', light], - ['dark', dark], - ])('keeps the canvas text readable in %s', (_mode, v) => { - expect( - contrastRatio(v['--wf-text']!, v['--wf-node-bg']!), - ).toBeGreaterThanOrEqual(AA_TEXT); - expect( - contrastRatio(v['--wf-text-muted']!, v['--wf-node-bg']!), - ).toBeGreaterThanOrEqual(AA_TEXT); - expect( - contrastRatio(v['--wf-text-muted']!, v['--wf-canvas-bg']!), - ).toBeGreaterThanOrEqual(AA_TEXT); - expect( - contrastRatio(v['--wf-text']!, v['--wf-controls-bg']!), - ).toBeGreaterThanOrEqual(AA_TEXT); +const light = variables('.wf-canvas'); +const dark = { ...light, ...variables(":root[data-theme='dark'] .wf-canvas") }; +const MODES: [ThemeMode, Record][] = [ + ['light', light], + ['dark', dark], +]; + +describe.each(MODES)('canvas.css colours in %s', (mode, v) => { + const ratio = (a: string, b: string) => contrastRatio(v[a]!, v[b]!); + + it('keeps the canvas text readable', () => { + expect(ratio('--wf-text', '--wf-node-bg')).toBeGreaterThanOrEqual(AA_TEXT); + expect(ratio('--wf-text-muted', '--wf-node-bg')).toBeGreaterThanOrEqual( + AA_TEXT, + ); + expect(ratio('--wf-text-muted', '--wf-canvas-bg')).toBeGreaterThanOrEqual( + AA_TEXT, + ); + expect(ratio('--wf-text', '--wf-controls-bg')).toBeGreaterThanOrEqual( + AA_TEXT, + ); }); - it.each([ - ['light', light], - ['dark', dark], - ])('keeps nodes, edges and the focus ring visible in %s', (_mode, v) => { + it('keeps nodes, edges and the focus ring visible', () => { for (const name of ['--wf-node-border', '--wf-edge', '--wf-focus']) { - expect( - contrastRatio(v[name]!, v['--wf-canvas-bg']!), - ).toBeGreaterThanOrEqual(AA_GRAPHIC); + expect(ratio(name, '--wf-canvas-bg')).toBeGreaterThanOrEqual(AA_GRAPHIC); } }); it('keeps the port letters readable on their ports', () => { expect( - contrastRatio(light['--wf-port-letter-out']!, light['--wf-port-fill']!), + ratio('--wf-port-letter-out', '--wf-port-fill'), ).toBeGreaterThanOrEqual(AA_TEXT); expect( - contrastRatio(light['--wf-port-letter-in']!, light['--wf-port-inner']!), + ratio('--wf-port-letter-in', '--wf-port-inner'), ).toBeGreaterThanOrEqual(AA_TEXT); }); + + it('keeps the ports visible on the canvas and on the node', () => { + for (const part of ['--wf-port-fill', '--wf-port-ring']) { + expect(ratio(part, '--wf-canvas-bg')).toBeGreaterThanOrEqual(AA_GRAPHIC); + expect(ratio(part, '--wf-node-bg')).toBeGreaterThanOrEqual(AA_GRAPHIC); + } + }); + + it('uses the colours of the theme module', () => { + const c = colorsOf(mode); + + expect(v['--wf-canvas-bg']).toBe(c.bg); + expect(v['--wf-node-bg']).toBe(c.surface); + expect(v['--wf-node-border']).toBe(c.control); + expect(v['--wf-text']).toBe(c.text); + expect(v['--wf-focus']).toBe(c.link); + expect(v['--wf-controls-bg']).toBe(c.surface); + expect(v['--wf-controls-border']).toBe(c.borderStrong); + expect(v['--wf-controls-bg-hover']).toBe(c.infoBg); + }); +}); + +describe('canvas.css shapes', () => { + it('uses the radii of the theme module', () => { + expect(block('.wf-canvas')).toContain(`border-radius: ${RADIUS.card}`); + expect(block('.wf-node')).toContain(`border-radius: ${RADIUS.control}`); + }); }); diff --git a/workflow-canvas/test-support/early-mode.test.ts b/workflow-canvas/test-support/early-mode.test.ts new file mode 100644 index 00000000..6443c5e2 --- /dev/null +++ b/workflow-canvas/test-support/early-mode.test.ts @@ -0,0 +1,55 @@ +// SPDX-License-Identifier: Apache-2.0 +import { readFileSync } from 'node:fs'; +import { describe, expect, it } from 'vitest'; +import { MODE_STORAGE_KEY } from '../src/theme/mode.ts'; + +const html = readFileSync(new URL('../index.html', import.meta.url), 'utf8'); +const script = /'], + [ + 'an upper case tag with an attribute', + '', + ], + ['an end tag with a space', ''], + ['an end tag with an attribute', ''], + ['a start tag with a space', ''], + ])('finds the body in %s', (_, html) => { + expect(inlineScriptOf(html)).toBe('a()'); + }); + + it('skips a script with no body, such as one that loads a file', () => { + expect( + inlineScriptOf( + '', + ), + ).toBe('b()'); + }); + + it('does not take a tag whose name only starts with script', () => { + expect(inlineScriptOf('a()')).toBe(''); + }); + + it('is empty when there is no script', () => { + expect(inlineScriptOf('

none

')).toBe(''); + }); +}); diff --git a/workflow-canvas/test-support/inline-script.ts b/workflow-canvas/test-support/inline-script.ts new file mode 100644 index 00000000..7faa9a6b --- /dev/null +++ b/workflow-canvas/test-support/inline-script.ts @@ -0,0 +1,13 @@ +// SPDX-License-Identifier: Apache-2.0 + +// Tag names are case-insensitive, a start tag may carry attributes, and an end +// tag may carry whitespace. +const SCRIPT = /]*>([\s\S]*?)<\/script\b[^>]*>/gi; + +/** The body of the first `', + ), + ).toEqual(['a()', 'b()']); + }); + + it('is empty when there is no inline script', () => { + expect(inlineScriptsOf('')).toEqual([]); + }); +}); diff --git a/workflow-canvas/test-support/inline-script.ts b/workflow-canvas/test-support/inline-script.ts index 7faa9a6b..5d09e7c3 100644 --- a/workflow-canvas/test-support/inline-script.ts +++ b/workflow-canvas/test-support/inline-script.ts @@ -4,10 +4,14 @@ // tag may carry whitespace. const SCRIPT = /]*>([\s\S]*?)<\/script\b[^>]*>/gi; -/** The body of the first `