@motion-proto/live-tokens 0.96.0 → 0.98.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude/skills/live-tokens-check-compliance/SKILL.md +5 -0
- package/.claude/skills/live-tokens-create-component/SKILL.md +4 -3
- package/.claude/skills/live-tokens-create-component/references/contract-tests.md +71 -11
- package/CHANGELOG.md +309 -0
- package/README.md +2 -2
- package/bin/check-component.mjs +8 -0
- package/bin/check-page.mjs +6 -0
- package/bin/cli.mjs +36 -6
- package/bin/contractRunner.mjs +142 -17
- package/bin/lib/findings.mjs +20 -4
- package/bin/rules/a11y.mjs +53 -0
- package/bin/set-colors.mjs +1 -0
- package/bin/setup-claude.mjs +2 -2
- package/dist-plugin/{chunk-A4TBP4MM.js → chunk-SLICH4IH.js} +2 -2
- package/dist-plugin/index.cjs +148 -3
- package/dist-plugin/index.js +250 -105
- package/dist-plugin/setColors/index.cjs +7 -1
- package/dist-plugin/setColors/index.d.cts +9 -4
- package/dist-plugin/setColors/index.d.ts +9 -4
- package/dist-plugin/setColors/index.js +8 -2
- package/dist-plugin/setGeometry/index.d.cts +1 -1
- package/dist-plugin/setGeometry/index.d.ts +1 -1
- package/dist-plugin/setType/index.d.cts +1 -1
- package/dist-plugin/setType/index.d.ts +1 -1
- package/dist-plugin/{themeTypes-B8_Idrp4.d.cts → themeTypes-BaTQUyac.d.cts} +10 -1
- package/dist-plugin/{themeTypes-B8_Idrp4.d.ts → themeTypes-BaTQUyac.d.ts} +10 -1
- package/package.json +49 -2
- package/src/demo/LabeledSelect.svelte +1 -1
- package/src/editor/bootstrap.ts +3 -1
- package/src/editor/component-editor/BadgeEditor.svelte +13 -0
- package/src/editor/component-editor/ButtonEditor.svelte +33 -2
- package/src/editor/component-editor/CalloutEditor.svelte +13 -0
- package/src/editor/component-editor/CardEditor.svelte +12 -2
- package/src/editor/component-editor/CodeSnippetEditor.svelte +11 -1
- package/src/editor/component-editor/CollapsibleSectionEditor.svelte +18 -1
- package/src/editor/component-editor/CornerBadgeEditor.svelte +13 -0
- package/src/editor/component-editor/DialogEditor.svelte +15 -0
- package/src/editor/component-editor/IconButtonEditor.svelte +29 -1
- package/src/editor/component-editor/ImageEditor.svelte +20 -9
- package/src/editor/component-editor/ImageLightboxEditor.svelte +44 -0
- package/src/editor/component-editor/InlineEditActionsEditor.svelte +13 -1
- package/src/editor/component-editor/InputEditor.svelte +23 -0
- package/src/editor/component-editor/MenuSelectEditor.svelte +27 -1
- package/src/editor/component-editor/NotificationEditor.svelte +15 -0
- package/src/editor/component-editor/PanelEditor.svelte +1 -1
- package/src/editor/component-editor/ProgressBarEditor.svelte +13 -0
- package/src/editor/component-editor/RadioButtonEditor.svelte +20 -1
- package/src/editor/component-editor/SectionDividerEditor.svelte +41 -13
- package/src/editor/component-editor/SegmentedControlEditor.svelte +29 -1
- package/src/editor/component-editor/SideNavigationEditor.svelte +40 -1
- package/src/editor/component-editor/SlidePagerEditor.svelte +20 -0
- package/src/editor/component-editor/SliderEditor.svelte +33 -0
- package/src/editor/component-editor/TabBarEditor.svelte +23 -2
- package/src/editor/component-editor/TableEditor.svelte +14 -0
- package/src/editor/component-editor/ToggleEditor.svelte +17 -0
- package/src/editor/component-editor/TooltipEditor.svelte +11 -2
- package/src/editor/component-editor/VideoFrameEditor.svelte +8 -0
- package/src/editor/component-editor/VideoLightboxEditor.svelte +10 -0
- package/src/editor/component-editor/registration.ts +2 -1
- package/src/editor/component-editor/registry.ts +69 -27
- package/src/editor/component-editor/scaffolding/DemoHeader.svelte +2 -0
- package/src/editor/component-editor/scaffolding/StateBlock.svelte +19 -8
- package/src/editor/component-editor/scaffolding/TokenLayout.svelte +3 -0
- package/src/editor/component-editor/scaffolding/VariantGroup.svelte +70 -10
- package/src/editor/component-editor/scaffolding/editorContext.ts +7 -0
- package/src/editor/core/a11y/a11yReport.ts +101 -0
- package/src/editor/core/a11y/a11yReportTypes.ts +69 -0
- package/src/editor/core/a11y/contrastFailures.ts +22 -0
- package/src/editor/core/components/contrastPairRegistry.ts +18 -0
- package/src/editor/core/palettes/contrast.ts +44 -0
- package/src/editor/core/palettes/contrastPairs.ts +79 -0
- package/src/editor/core/palettes/resolveColor.ts +77 -0
- package/src/editor/core/store/editorPersistence.ts +9 -1
- package/src/editor/core/store/editorRenderer.ts +2 -0
- package/src/editor/core/store/editorStore.ts +29 -0
- package/src/editor/core/store/editorTypes.ts +9 -0
- package/src/editor/core/store/editorViewStore.ts +2 -2
- package/src/editor/core/store/unsavedEdits.ts +10 -0
- package/src/editor/core/themes/buildColors.ts +16 -5
- package/src/editor/core/themes/liveStateStream.ts +7 -1
- package/src/editor/core/themes/parsers/gradient.ts +5 -2
- package/src/editor/core/themes/slices/customColors.ts +226 -0
- package/src/editor/core/themes/themeTypes.ts +10 -0
- package/src/editor/docs/chapters.ts +1 -0
- package/src/editor/docs/content/accessibility.md +362 -0
- package/src/editor/docs/content/editing-tokens.md +56 -1
- package/src/editor/docs/content.generated.ts +2 -1
- package/src/editor/pages/ComponentEditorPage.svelte +10 -1
- package/src/editor/pages/EditorShell.svelte +97 -10
- package/src/editor/skill-atlas/skillSources.generated.ts +3 -3
- package/src/editor/skill-atlas/trees/check-compliance.ts +3 -3
- package/src/editor/skill-atlas/trees/create-component.ts +12 -12
- package/src/editor/ui/CustomColorReplaceStep.svelte +118 -0
- package/src/editor/ui/EditorViewSwitcher.svelte +15 -2
- package/src/editor/ui/GradientEditor.svelte +9 -1
- package/src/editor/ui/ThemePanel.svelte +4 -4
- package/src/editor/ui/UIContrastReadout.svelte +186 -0
- package/src/editor/ui/UIInfoPopover.svelte +16 -2
- package/src/editor/ui/UIPaletteSelector.svelte +347 -15
- package/src/editor/ui/UITokenSelector.svelte +33 -14
- package/src/editor/ui/VariablesTab.svelte +3 -0
- package/src/editor/ui/a11y/A11yComponentIssues.svelte +103 -0
- package/src/editor/ui/a11y/A11yFindingRow.svelte +57 -0
- package/src/editor/ui/a11y/A11yIssueRow.svelte +121 -0
- package/src/editor/ui/a11y/A11yOverview.svelte +242 -0
- package/src/editor/ui/a11y/A11yPanel.svelte +128 -0
- package/src/editor/ui/a11y/A11yPropertyMark.svelte +84 -0
- package/src/editor/ui/a11y/A11yRailMark.svelte +34 -0
- package/src/editor/ui/a11y/AccessibilityTab.svelte +334 -0
- package/src/editor/ui/a11y/a11yView.ts +218 -0
- package/src/editor/ui/anchorToTrigger.ts +63 -0
- package/src/editor/ui/contrastReadout.ts +45 -0
- package/src/editor/ui/sections/CustomColorsSection.svelte +254 -0
- package/src/live-tokens/data/themes/autumn.json +20 -20
- package/src/live-tokens/data/themes/halloween.json +20 -20
- package/src/live-tokens/data/themes/midnight-study.json +20 -20
- package/src/live-tokens/data/themes/ocean.json +20 -20
- package/src/live-tokens/data/themes/royal-velvet.json +20 -20
- package/src/live-tokens/data/themes/sketchy.json +20 -20
- package/src/live-tokens/data/themes/spring-meadow.json +20 -20
- package/src/live-tokens/data/themes/sunset.json +20 -20
- package/src/system/components/Badge.svelte +3 -3
- package/src/system/components/Button.svelte +9 -3
- package/src/system/components/CodeSnippet.svelte +8 -1
- package/src/system/components/CornerBadge.svelte +3 -3
- package/src/system/components/Dialog.svelte +43 -14
- package/src/system/components/IconButton.svelte +1 -1
- package/src/system/components/Image.svelte +7 -0
- package/src/system/components/ImageLightbox.svelte +26 -26
- package/src/system/components/Input.svelte +8 -1
- package/src/system/components/MenuSelect.svelte +30 -2
- package/src/system/components/Notification.svelte +1 -1
- package/src/system/components/ProgressBar.svelte +6 -0
- package/src/system/components/RadioButton.svelte +36 -3
- package/src/system/components/SegmentedControl.svelte +36 -2
- package/src/system/components/SlidePager.svelte +2 -2
- package/src/system/components/Slider.svelte +13 -9
- package/src/system/components/TabBar.svelte +33 -3
- package/src/system/components/Toggle.svelte +2 -2
- package/src/system/components/VideoFrame.svelte +1 -1
- package/src/system/components/VideoLightbox.svelte +2 -2
- package/src/system/internal/focusTrap.ts +35 -0
- package/src/testing-js/__aria__/badge.yml +40 -0
- package/src/testing-js/__aria__/button.yml +88 -0
- package/src/testing-js/__aria__/callout.yml +12 -0
- package/src/testing-js/__aria__/card.yml +8 -0
- package/src/testing-js/__aria__/codesnippet.yml +21 -0
- package/src/testing-js/__aria__/collapsiblesection.yml +23 -0
- package/src/testing-js/__aria__/cornerbadge.yml +24 -0
- package/src/testing-js/__aria__/dialog.yml +42 -0
- package/src/testing-js/__aria__/iconbutton.yml +57 -0
- package/src/testing-js/__aria__/image.yml +10 -0
- package/src/testing-js/__aria__/imagelightbox.yml +50 -0
- package/src/testing-js/__aria__/inlineeditactions.yml +16 -0
- package/src/testing-js/__aria__/input.yml +61 -0
- package/src/testing-js/__aria__/menuselect.yml +30 -0
- package/src/testing-js/__aria__/notification.yml +30 -0
- package/src/testing-js/__aria__/panel.yml +2 -0
- package/src/testing-js/__aria__/progressbar.yml +2 -0
- package/src/testing-js/__aria__/radiobutton.yml +20 -0
- package/src/testing-js/__aria__/sectiondivider.yml +9 -0
- package/src/testing-js/__aria__/segmentedcontrol.yml +35 -0
- package/src/testing-js/__aria__/sidenavigation.yml +304 -0
- package/src/testing-js/__aria__/slidepager.yml +72 -0
- package/src/testing-js/__aria__/slider.yml +28 -0
- package/src/testing-js/__aria__/tabbar.yml +35 -0
- package/src/testing-js/__aria__/table.yml +144 -0
- package/src/testing-js/__aria__/toggle.yml +12 -0
- package/src/testing-js/__aria__/tooltip.yml +2 -0
- package/src/testing-js/__aria__/videoframe.yml +6 -0
- package/src/testing-js/__aria__/videolightbox.yml +12 -0
- package/src/testing-js/{chunk-WIZ6W7UT.js → chunk-3WCLMOM7.js} +2 -2
- package/src/testing-js/{chunk-SHBYZUPD.js → chunk-5HNZHIHG.js} +211 -14
- package/src/testing-js/chunk-5HNZHIHG.js.map +1 -0
- package/src/testing-js/{chunk-NB3NZRBM.js → chunk-7CM5D7PF.js} +5 -1
- package/src/testing-js/chunk-7CM5D7PF.js.map +1 -0
- package/src/testing-js/chunk-EWSGE27V.js +168 -0
- package/src/testing-js/chunk-EWSGE27V.js.map +1 -0
- package/src/testing-js/{chunk-GKO27NGN.js → chunk-R54I7YL6.js} +2 -2
- package/src/testing-js/{chunk-X6KHEVIG.js → chunk-RWH46IU5.js} +97 -29
- package/src/testing-js/chunk-RWH46IU5.js.map +1 -0
- package/src/testing-js/chunk-SHLPDNAQ.js +658 -0
- package/src/testing-js/chunk-SHLPDNAQ.js.map +1 -0
- package/src/testing-js/{chunk-3UKGXCDL.js → chunk-YVVR3GJZ.js} +1 -1
- package/src/testing-js/chunk-YVVR3GJZ.js.map +1 -0
- package/src/testing-js/component-a11y.contract.js +283 -0
- package/src/testing-js/component-a11y.contract.js.map +1 -0
- package/src/testing-js/component-alias.contract.js +2 -2
- package/src/testing-js/component-behavior.contract.js +2 -2
- package/src/testing-js/component-editor.contract.js +6 -5
- package/src/testing-js/component-editor.contract.js.map +1 -1
- package/src/testing-js/component-render.contract.js +10 -6
- package/src/testing-js/component-render.contract.js.map +1 -1
- package/src/testing-js/index.d.ts +44 -16
- package/src/testing-js/index.js +9 -9
- package/src/testing-js/page-compliance.contract.js +163 -18
- package/src/testing-js/page-compliance.contract.js.map +1 -1
- package/src/testing-js/vitest.js +3 -3
- package/template/package.json +9 -0
- package/src/editor/ui/keepInViewport.ts +0 -84
- package/src/testing-js/chunk-3UKGXCDL.js.map +0 -1
- package/src/testing-js/chunk-NB3NZRBM.js.map +0 -1
- package/src/testing-js/chunk-SHBYZUPD.js.map +0 -1
- package/src/testing-js/chunk-X6KHEVIG.js.map +0 -1
- /package/src/testing-js/{chunk-WIZ6W7UT.js.map → chunk-3WCLMOM7.js.map} +0 -0
- /package/src/testing-js/{chunk-GKO27NGN.js.map → chunk-R54I7YL6.js.map} +0 -0
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import { derived, writable, type Readable } from 'svelte/store';
|
|
2
|
+
import { API_BASE } from '../storage/apiBase';
|
|
3
|
+
import type { A11yContrastFailure, A11yFinding, A11yFinishedReport, A11yReport } from './a11yReportTypes';
|
|
4
|
+
|
|
5
|
+
/** One component's share of the last finished run, sorted by its fix (decision 6). */
|
|
6
|
+
export interface ComponentA11yIssues {
|
|
7
|
+
/** Fix with a property: the editor's own contrast failures. */
|
|
8
|
+
contrast: A11yContrastFailure[];
|
|
9
|
+
/** Fix with a property: the run's findings whose engine rule measures a property's value. */
|
|
10
|
+
property: A11yFinding[];
|
|
11
|
+
/** Fix in the component's code: every other `a11y-*` finding but `a11y-review`. */
|
|
12
|
+
code: A11yFinding[];
|
|
13
|
+
/** Check by eye. */
|
|
14
|
+
review: A11yFinding[];
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/** Engine rules whose failure a property's value fixes. */
|
|
18
|
+
export const PROPERTY_FIX_RULES: ReadonlySet<string> = new Set(['focus-indicator-contrast', 'control-boundary-contrast']);
|
|
19
|
+
|
|
20
|
+
const latest = writable<A11yReport | null>(null);
|
|
21
|
+
const lastFinished = writable<A11yFinishedReport | null>(null);
|
|
22
|
+
let heard = false;
|
|
23
|
+
|
|
24
|
+
/** The dev server's latest word: a run going, finished or failed. */
|
|
25
|
+
export const a11yReport: Readable<A11yReport | null> = { subscribe: latest.subscribe };
|
|
26
|
+
|
|
27
|
+
/** The last run that finished, which the markers follow while a new run goes (decision 7). */
|
|
28
|
+
export const a11yFinishedReport: Readable<A11yFinishedReport | null> = { subscribe: lastFinished.subscribe };
|
|
29
|
+
|
|
30
|
+
export const a11yIssues: Readable<Record<string, ComponentA11yIssues>> = derived(lastFinished, ($report) => {
|
|
31
|
+
const out: Record<string, ComponentA11yIssues> = {};
|
|
32
|
+
if (!$report) return out;
|
|
33
|
+
const entry = (id: string) => (out[id] ??= { contrast: [], property: [], code: [], review: [] });
|
|
34
|
+
for (const failure of $report.contrast) entry(failure.component).contrast.push(failure);
|
|
35
|
+
for (const finding of $report.findings) {
|
|
36
|
+
if (!finding.component || !finding.rule.startsWith('a11y-')) continue;
|
|
37
|
+
const issues = entry(finding.component);
|
|
38
|
+
const group = finding.rule === 'a11y-review'
|
|
39
|
+
? issues.review
|
|
40
|
+
: PROPERTY_FIX_RULES.has(finding.details?.engineRule ?? '') ? issues.property : issues.code;
|
|
41
|
+
group.push(finding);
|
|
42
|
+
}
|
|
43
|
+
return out;
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
/** The run's findings under each component property they trace to (decision 7). The editor's contrast failures stay out. */
|
|
47
|
+
export const a11yPropertyFindings: Readable<Map<string, A11yFinding[]>> = derived(lastFinished, ($report) => {
|
|
48
|
+
const out = new Map<string, A11yFinding[]>();
|
|
49
|
+
for (const finding of $report?.findings ?? []) {
|
|
50
|
+
if (!finding.component || !finding.rule.startsWith('a11y-')) continue;
|
|
51
|
+
for (const variable of finding.details?.variables ?? []) {
|
|
52
|
+
const list = out.get(variable);
|
|
53
|
+
if (list) list.push(finding);
|
|
54
|
+
else out.set(variable, [finding]);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
return out;
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
export function receiveA11yReport(report: A11yReport): void {
|
|
61
|
+
heard = true;
|
|
62
|
+
latest.set(report);
|
|
63
|
+
if (report.state === 'finished') lastFinished.set(report);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export async function init(): Promise<void> {
|
|
67
|
+
const res = await fetch(`${API_BASE}/a11y/report`);
|
|
68
|
+
if (!res.ok) return;
|
|
69
|
+
const { report } = (await res.json()) as { report: A11yReport | null };
|
|
70
|
+
// An event that landed while the read was in flight is the newer word.
|
|
71
|
+
if (report && !heard) receiveA11yReport(report);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Starts a run with the contrast failures the editor measured. Answers 409
|
|
76
|
+
* `A11Y_RUN_IN_PROGRESS` while a run goes, and 409 `LEGACY_LAYOUT` on a tree
|
|
77
|
+
* `live-tokens migrate` has not healed; both arrive as the thrown error's `code`.
|
|
78
|
+
*/
|
|
79
|
+
export async function startA11yRun(contrast: A11yContrastFailure[]): Promise<A11yReport> {
|
|
80
|
+
const res = await fetch(`${API_BASE}/a11y/run`, {
|
|
81
|
+
method: 'POST',
|
|
82
|
+
headers: { 'Content-Type': 'application/json' },
|
|
83
|
+
body: JSON.stringify({ contrast }),
|
|
84
|
+
});
|
|
85
|
+
if (!res.ok) {
|
|
86
|
+
const body = (await res.json().catch(() => ({}))) as { error?: string; code?: string };
|
|
87
|
+
const err = new Error(body.error || 'The accessibility run did not start') as Error & { status?: number; code?: string };
|
|
88
|
+
err.status = res.status;
|
|
89
|
+
if (body.code) err.code = body.code;
|
|
90
|
+
throw err;
|
|
91
|
+
}
|
|
92
|
+
const report = (await res.json()) as A11yReport;
|
|
93
|
+
receiveA11yReport(report);
|
|
94
|
+
return report;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
export function __resetForTests(): void {
|
|
98
|
+
heard = false;
|
|
99
|
+
latest.set(null);
|
|
100
|
+
lastFinished.set(null);
|
|
101
|
+
}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import type { ContrastRole } from '../palettes/contrast';
|
|
2
|
+
|
|
3
|
+
/** The SSE event the dev server sends on a run's start and on its end. */
|
|
4
|
+
export const A11Y_EVENT = 'a11y';
|
|
5
|
+
|
|
6
|
+
/** One failing pair the editor's contrast checker measured when the run started (decision 4). */
|
|
7
|
+
export interface A11yContrastFailure {
|
|
8
|
+
component: string;
|
|
9
|
+
/** The foreground property, the row a fix changes. */
|
|
10
|
+
variable: string;
|
|
11
|
+
/** Component layers behind the foreground, top first, as the pair declares them. */
|
|
12
|
+
backdrop: string[];
|
|
13
|
+
role: ContrastRole;
|
|
14
|
+
ratio: number;
|
|
15
|
+
floor: number;
|
|
16
|
+
context?: string;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/** A finding as `check-component --tests --a11y --json` prints it. */
|
|
20
|
+
export interface A11yFinding {
|
|
21
|
+
rule: string;
|
|
22
|
+
file: string;
|
|
23
|
+
line: number;
|
|
24
|
+
message: string;
|
|
25
|
+
/** Absent on a run-level finding such as `tests-not-installed`. */
|
|
26
|
+
component?: string;
|
|
27
|
+
details?: {
|
|
28
|
+
engineRule: string;
|
|
29
|
+
impact?: string | null;
|
|
30
|
+
target: string;
|
|
31
|
+
view?: string;
|
|
32
|
+
html: string;
|
|
33
|
+
help: string;
|
|
34
|
+
helpUrl: string;
|
|
35
|
+
/** The component properties the result traced to, sorted. Absent when the trace found none. */
|
|
36
|
+
variables?: string[];
|
|
37
|
+
};
|
|
38
|
+
context?: Record<string, unknown>;
|
|
39
|
+
severity: 'error' | 'warn';
|
|
40
|
+
guidance?: string;
|
|
41
|
+
repair: string;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
interface A11yReportBase {
|
|
45
|
+
startedAt: string;
|
|
46
|
+
contrast: A11yContrastFailure[];
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export interface A11yRunningReport extends A11yReportBase {
|
|
50
|
+
state: 'running';
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/** Holds every CLI finding except axe's `color-contrast`, which `contrast` covers. */
|
|
54
|
+
export interface A11yFinishedReport extends A11yReportBase {
|
|
55
|
+
state: 'finished';
|
|
56
|
+
finishedAt: string;
|
|
57
|
+
/** Absent when the run stopped before it copied the data tree, as a missing tool stops it. */
|
|
58
|
+
theme?: string;
|
|
59
|
+
findings: A11yFinding[];
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/** The CLI crashed or printed no report. */
|
|
63
|
+
export interface A11yFailedReport extends A11yReportBase {
|
|
64
|
+
state: 'failed';
|
|
65
|
+
finishedAt: string;
|
|
66
|
+
error: string;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export type A11yReport = A11yRunningReport | A11yFinishedReport | A11yFailedReport;
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { measurePair, type ContrastPair, type Resolver } from '../palettes/contrastPairs';
|
|
2
|
+
import type { A11yContrastFailure } from './a11yReportTypes';
|
|
3
|
+
|
|
4
|
+
/** Each pair below its floor. `measurePair` passes an inactive pair, so a disabled state never fails. */
|
|
5
|
+
export function contrastFailures(
|
|
6
|
+
pairs: { component: string; pair: ContrastPair }[],
|
|
7
|
+
resolver: Resolver,
|
|
8
|
+
): A11yContrastFailure[] {
|
|
9
|
+
return pairs.flatMap(({ component, pair }) => {
|
|
10
|
+
const measure = measurePair(pair, resolver);
|
|
11
|
+
if (measure.passes) return [];
|
|
12
|
+
return [{
|
|
13
|
+
component,
|
|
14
|
+
variable: pair.foreground,
|
|
15
|
+
backdrop: pair.backdrop,
|
|
16
|
+
role: pair.role,
|
|
17
|
+
ratio: measure.ratio,
|
|
18
|
+
floor: measure.floor,
|
|
19
|
+
...(pair.context ? { context: pair.context } : {}),
|
|
20
|
+
}];
|
|
21
|
+
});
|
|
22
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { ContrastPair } from '../palettes/contrastPairs';
|
|
2
|
+
|
|
3
|
+
// The palette selector reads pairs from here. Importing `registry.ts` instead
|
|
4
|
+
// would close a cycle, because every editor imports the palette selector.
|
|
5
|
+
const pairsByComponent = new Map<string, ContrastPair[]>();
|
|
6
|
+
|
|
7
|
+
/** Replaces the component's pairs, so a custom entry that overrides a built-in drops the built-in's pairs. */
|
|
8
|
+
export function registerContrastPairs(id: string, pairs: ContrastPair[]): void {
|
|
9
|
+
pairsByComponent.set(id, pairs);
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function getContrastPairs(id: string, variable: string): ContrastPair[] {
|
|
13
|
+
return (pairsByComponent.get(id) ?? []).filter((pair) => pair.foreground === variable);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function allContrastPairs(): { component: string; pair: ContrastPair }[] {
|
|
17
|
+
return [...pairsByComponent].flatMap(([component, pairs]) => pairs.map((pair) => ({ component, pair })));
|
|
18
|
+
}
|
|
@@ -10,6 +10,50 @@ import { hexToOklch, oklchToHex, gamutClamp } from './oklch';
|
|
|
10
10
|
export const AA_BODY = 4.5;
|
|
11
11
|
export const AA_LARGE = 3.0;
|
|
12
12
|
|
|
13
|
+
/** WCAG's large-text sizes: the AA floor drops to 3:1 at either. */
|
|
14
|
+
export const LARGE_TEXT_PX = 24;
|
|
15
|
+
export const LARGE_BOLD_PX = 18.66;
|
|
16
|
+
export const BOLD = 700;
|
|
17
|
+
|
|
18
|
+
export function isLargeText(px: number, weight: number): boolean {
|
|
19
|
+
return px >= LARGE_TEXT_PX || (px >= LARGE_BOLD_PX && weight >= BOLD);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export type ContrastRole = 'text' | 'non-text';
|
|
23
|
+
|
|
24
|
+
export interface ContrastRule {
|
|
25
|
+
criterion: string;
|
|
26
|
+
name: string;
|
|
27
|
+
level: 'AA';
|
|
28
|
+
floor: number;
|
|
29
|
+
/** Text only: the floor once `isLargeText` holds. */
|
|
30
|
+
largeFloor?: number;
|
|
31
|
+
url: string;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export const CONTRAST_RULES: Record<ContrastRole, ContrastRule> = {
|
|
35
|
+
text: {
|
|
36
|
+
criterion: '1.4.3',
|
|
37
|
+
name: 'Contrast (Minimum)',
|
|
38
|
+
level: 'AA',
|
|
39
|
+
floor: AA_BODY,
|
|
40
|
+
largeFloor: AA_LARGE,
|
|
41
|
+
url: 'https://www.w3.org/TR/WCAG22/#contrast-minimum',
|
|
42
|
+
},
|
|
43
|
+
'non-text': {
|
|
44
|
+
criterion: '1.4.11',
|
|
45
|
+
name: 'Non-text Contrast',
|
|
46
|
+
level: 'AA',
|
|
47
|
+
floor: 3,
|
|
48
|
+
url: 'https://www.w3.org/TR/WCAG22/#non-text-contrast',
|
|
49
|
+
},
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
export function floorFor(role: ContrastRole, large: boolean): number {
|
|
53
|
+
const rule = CONTRAST_RULES[role];
|
|
54
|
+
return large && rule.largeFloor !== undefined ? rule.largeFloor : rule.floor;
|
|
55
|
+
}
|
|
56
|
+
|
|
13
57
|
function linearizeChannel(v8: number): number {
|
|
14
58
|
const s = v8 / 255;
|
|
15
59
|
return s <= 0.03928 ? s / 12.92 : Math.pow((s + 0.055) / 1.055, 2.4);
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import { contrastRatio, floorFor, isLargeText, type ContrastRole } from './contrast';
|
|
2
|
+
|
|
3
|
+
export const PAGE_BG = '--page-bg';
|
|
4
|
+
|
|
5
|
+
export interface ContrastPair {
|
|
6
|
+
foreground: string;
|
|
7
|
+
/** Component layers behind the foreground, top first. `--page-bg` always sits under the last one. */
|
|
8
|
+
backdrop: string[];
|
|
9
|
+
role: ContrastRole;
|
|
10
|
+
/** Text only. Sets the large-text floor. */
|
|
11
|
+
fontSize?: string;
|
|
12
|
+
/** Text only. Normal weight when absent. */
|
|
13
|
+
fontWeight?: string;
|
|
14
|
+
/** Disabled state: WCAG exempts it, so the pair always passes. */
|
|
15
|
+
inactive?: boolean;
|
|
16
|
+
/** Names the context when one foreground has several pairs ("inline", "zoomed"). */
|
|
17
|
+
context?: string;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/** sRGB channels from 0 to 255 and alpha from 0 to 1, as a canvas pixel reads. */
|
|
21
|
+
export type Rgba = [r: number, g: number, b: number, a: number];
|
|
22
|
+
|
|
23
|
+
export function over(front: Rgba, back: Rgba): Rgba {
|
|
24
|
+
const alpha = front[3] + back[3] * (1 - front[3]);
|
|
25
|
+
if (alpha === 0) return [0, 0, 0, 0];
|
|
26
|
+
const mix = (f: number, b: number) => (f * front[3] + b * back[3] * (1 - front[3])) / alpha;
|
|
27
|
+
return [mix(front[0], back[0]), mix(front[1], back[1]), mix(front[2], back[2]), alpha];
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export interface Resolver {
|
|
31
|
+
/** One color for a solid value, one per stop for a gradient. */
|
|
32
|
+
color(name: string): Rgba[];
|
|
33
|
+
fontPx(name: string): number;
|
|
34
|
+
fontWeight(name: string): number;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export interface PairMeasure {
|
|
38
|
+
pair: ContrastPair;
|
|
39
|
+
ratio: number;
|
|
40
|
+
floor: number;
|
|
41
|
+
large: boolean;
|
|
42
|
+
passes: boolean;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const NORMAL_WEIGHT = 400;
|
|
46
|
+
|
|
47
|
+
function hex([r, g, b]: Rgba): string {
|
|
48
|
+
return `#${[r, g, b].map((c) => Math.round(c).toString(16).padStart(2, '0')).join('')}`;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export function measurePair(pair: ContrastPair, resolver: Resolver): PairMeasure {
|
|
52
|
+
let behind = resolver.color(PAGE_BG);
|
|
53
|
+
for (const layer of [...pair.backdrop].reverse()) {
|
|
54
|
+
const below = behind;
|
|
55
|
+
behind = resolver.color(layer).flatMap((stop) => below.map((back) => over(stop, back)));
|
|
56
|
+
}
|
|
57
|
+
const fronts = resolver.color(pair.foreground);
|
|
58
|
+
let ratio = Infinity;
|
|
59
|
+
for (const back of behind) {
|
|
60
|
+
for (const front of fronts) {
|
|
61
|
+
ratio = Math.min(ratio, contrastRatio(hex(over(front, back)), hex(back)));
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
const large = pair.role === 'text'
|
|
65
|
+
&& pair.fontSize !== undefined
|
|
66
|
+
&& isLargeText(
|
|
67
|
+
resolver.fontPx(pair.fontSize),
|
|
68
|
+
pair.fontWeight ? resolver.fontWeight(pair.fontWeight) : NORMAL_WEIGHT,
|
|
69
|
+
);
|
|
70
|
+
const floor = floorFor(pair.role, large);
|
|
71
|
+
return { pair, ratio, floor, large, passes: pair.inactive === true || ratio >= floor };
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/** An inactive pair applies no floor, so it ranks only when every pair is inactive. */
|
|
75
|
+
export function closestToFailing(measures: PairMeasure[]): PairMeasure {
|
|
76
|
+
const active = measures.filter((m) => !m.pair.inactive);
|
|
77
|
+
const pool = active.length > 0 ? active : measures;
|
|
78
|
+
return pool.reduce((worst, m) => (m.ratio / m.floor < worst.ratio / worst.floor ? m : worst));
|
|
79
|
+
}
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import { splitTopLevel } from '../themes/parsers/gradient';
|
|
2
|
+
import type { Resolver, Rgba } from './contrastPairs';
|
|
3
|
+
|
|
4
|
+
const GRADIENT = /^(?:repeating-)?(?:linear|radial|conic)-gradient\((.*)\)$/is;
|
|
5
|
+
const DIRECTION = /^(?:to|at|from|in|circle|ellipse|closest-side|closest-corner|farthest-side|farthest-corner)$/i;
|
|
6
|
+
const POSITION = /^(?:[+-]?(?:\d+\.?\d*|\.\d+)(?:[a-z]+|%)?|(?:calc|min|max|clamp)\(.*\))$/is;
|
|
7
|
+
|
|
8
|
+
const isSpace = (c: string) => /\s/.test(c);
|
|
9
|
+
|
|
10
|
+
/** The stop colors of a gradient function, or null for any other value. */
|
|
11
|
+
export function gradientStops(value: string): string[] | null {
|
|
12
|
+
const match = value.trim().match(GRADIENT);
|
|
13
|
+
if (!match) return null;
|
|
14
|
+
const stops: string[] = [];
|
|
15
|
+
splitTopLevel(match[1]).forEach((arg, i) => {
|
|
16
|
+
const tokens = splitTopLevel(arg, isSpace).filter(Boolean);
|
|
17
|
+
if (i === 0 && (DIRECTION.test(tokens[0]) || POSITION.test(tokens[0]))) return;
|
|
18
|
+
const color = tokens.filter((token) => !POSITION.test(token)).join(' ');
|
|
19
|
+
if (color) stops.push(color);
|
|
20
|
+
});
|
|
21
|
+
return stops;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function createDomResolver(doc: Document = document): Resolver {
|
|
25
|
+
const view = doc.defaultView!;
|
|
26
|
+
// The canvas answers with the pixel the screen shows, gamut-clamped, for any
|
|
27
|
+
// color syntax the probe computes.
|
|
28
|
+
const canvas = doc.createElement('canvas');
|
|
29
|
+
canvas.width = 1;
|
|
30
|
+
canvas.height = 1;
|
|
31
|
+
const ctx = canvas.getContext('2d', { willReadFrequently: true })!;
|
|
32
|
+
ctx.globalCompositeOperation = 'copy';
|
|
33
|
+
|
|
34
|
+
const paint = (css: string): Rgba => {
|
|
35
|
+
ctx.fillStyle = 'rgba(0, 0, 0, 0)';
|
|
36
|
+
ctx.fillStyle = css;
|
|
37
|
+
ctx.fillRect(0, 0, 1, 1);
|
|
38
|
+
const [r, g, b, a] = ctx.getImageData(0, 0, 1, 1).data;
|
|
39
|
+
return [r, g, b, a / 255];
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
const withProbe = <T>(read: (probe: HTMLElement, style: CSSStyleDeclaration) => T): T => {
|
|
43
|
+
// A value `color` rejects makes the probe inherit, so the host's
|
|
44
|
+
// transparent reads as a layer that paints nothing.
|
|
45
|
+
const host = doc.createElement('div');
|
|
46
|
+
host.style.display = 'none';
|
|
47
|
+
host.style.color = 'transparent';
|
|
48
|
+
const probe = doc.createElement('div');
|
|
49
|
+
host.appendChild(probe);
|
|
50
|
+
doc.body.appendChild(host);
|
|
51
|
+
try {
|
|
52
|
+
return read(probe, view.getComputedStyle(probe));
|
|
53
|
+
} finally {
|
|
54
|
+
host.remove();
|
|
55
|
+
}
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
return {
|
|
59
|
+
color: (name) => withProbe((probe, style) => {
|
|
60
|
+
const stops = gradientStops(style.getPropertyValue(name)) ?? [`var(${name})`];
|
|
61
|
+
return stops.map((stop) => {
|
|
62
|
+
// An invalid assignment keeps the previous stop's color.
|
|
63
|
+
probe.style.color = '';
|
|
64
|
+
probe.style.color = stop;
|
|
65
|
+
return paint(style.color);
|
|
66
|
+
});
|
|
67
|
+
}),
|
|
68
|
+
fontPx: (name) => withProbe((probe, style) => {
|
|
69
|
+
probe.style.fontSize = `var(${name})`;
|
|
70
|
+
return parseFloat(style.fontSize);
|
|
71
|
+
}),
|
|
72
|
+
fontWeight: (name) => withProbe((probe, style) => {
|
|
73
|
+
probe.style.fontWeight = `var(${name})`;
|
|
74
|
+
return parseFloat(style.fontWeight);
|
|
75
|
+
}),
|
|
76
|
+
};
|
|
77
|
+
}
|
|
@@ -19,6 +19,7 @@ import { storageKey } from './editorConfig';
|
|
|
19
19
|
import { store } from './editorCore';
|
|
20
20
|
import { quietGet, quietSet } from '../storage/storage';
|
|
21
21
|
import { isGradientSlot, makeDefaultGradients } from '../themes/slices/gradients';
|
|
22
|
+
import { isCustomColorList } from '../themes/slices/customColors';
|
|
22
23
|
import { seedShadowsFromDom } from '../themes/slices/shadows';
|
|
23
24
|
import { makeDefaultWashScale, type WashFamily } from '../themes/slices/washes';
|
|
24
25
|
import { normalizeColumnsState } from '../themes/slices/columns';
|
|
@@ -73,6 +74,13 @@ function migrateGradients(state: EditorState): EditorState {
|
|
|
73
74
|
return { ...state, gradients: { tokens: makeDefaultGradients() } };
|
|
74
75
|
}
|
|
75
76
|
|
|
77
|
+
// Persisted state from before custom colours gets the empty default from the
|
|
78
|
+
// shallow merge; anything else that is not a well-formed list loads as empty.
|
|
79
|
+
function migrateCustomColors(state: EditorState): EditorState {
|
|
80
|
+
if (isCustomColorList(state.customColors?.tokens)) return state;
|
|
81
|
+
return { ...state, customColors: { tokens: [] } };
|
|
82
|
+
}
|
|
83
|
+
|
|
76
84
|
// `hydrate` shallow-merges the persisted `components` bag over the default, so
|
|
77
85
|
// a slice serialized by an older build can lack fields added since (e.g.
|
|
78
86
|
// `config`, added with the two-field alias/config split). `componentsToVars`
|
|
@@ -205,7 +213,7 @@ export function hydrate(): void {
|
|
|
205
213
|
store.set(
|
|
206
214
|
normalizeHarmonyAxes(
|
|
207
215
|
normalizeBaseAnchors(
|
|
208
|
-
normalizePaletteBasis(normalizePaletteLabels(normalizeComponents(normalizeColumns(normalizeWashes(migrateGradients(merged)))))),
|
|
216
|
+
normalizePaletteBasis(normalizePaletteLabels(normalizeComponents(normalizeColumns(normalizeWashes(migrateCustomColors(migrateGradients(merged))))))),
|
|
209
217
|
),
|
|
210
218
|
),
|
|
211
219
|
);
|
|
@@ -21,6 +21,7 @@ import {
|
|
|
21
21
|
washesToVars,
|
|
22
22
|
shadowsToVars,
|
|
23
23
|
gradientsToVars,
|
|
24
|
+
customColorsToVars,
|
|
24
25
|
componentsToVars,
|
|
25
26
|
} from './editorStore';
|
|
26
27
|
|
|
@@ -34,6 +35,7 @@ export function deriveCssVars(state: EditorState): Record<string, string> {
|
|
|
34
35
|
if (state.gradients.tokens.length > 0) {
|
|
35
36
|
Object.assign(out, gradientsToVars(state.gradients));
|
|
36
37
|
}
|
|
38
|
+
Object.assign(out, customColorsToVars(state.customColors));
|
|
37
39
|
Object.assign(out, palettesToVars(state.palettes));
|
|
38
40
|
Object.assign(out, componentsToVars(state.components));
|
|
39
41
|
Object.assign(out, resolveFontStackValues(state.fonts.stacks, state.fonts.sources));
|
|
@@ -74,6 +74,10 @@ import {
|
|
|
74
74
|
loadGradientsFromFile,
|
|
75
75
|
makeDefaultGradients,
|
|
76
76
|
} from '../themes/slices/gradients';
|
|
77
|
+
import {
|
|
78
|
+
customColorsToVars,
|
|
79
|
+
loadCustomColorsFromFile,
|
|
80
|
+
} from '../themes/slices/customColors';
|
|
77
81
|
import {
|
|
78
82
|
componentBaseline,
|
|
79
83
|
getComponentOwnedVarNames,
|
|
@@ -102,6 +106,7 @@ function emptyState(): EditorState {
|
|
|
102
106
|
columns: defaultColumns(),
|
|
103
107
|
components: {},
|
|
104
108
|
gradients: { tokens: makeDefaultGradients() },
|
|
109
|
+
customColors: { tokens: [] },
|
|
105
110
|
harmonyAxes: defaultHarmonyAxes(),
|
|
106
111
|
cssVars: {},
|
|
107
112
|
};
|
|
@@ -179,6 +184,20 @@ export {
|
|
|
179
184
|
removeGradientToken,
|
|
180
185
|
} from '../themes/slices/gradients';
|
|
181
186
|
|
|
187
|
+
export {
|
|
188
|
+
customColorsToVars,
|
|
189
|
+
nextCustomColorSlot,
|
|
190
|
+
isCustomColorSlot,
|
|
191
|
+
addCustomColor,
|
|
192
|
+
setCustomColor,
|
|
193
|
+
removeCustomColor,
|
|
194
|
+
customColorUsages,
|
|
195
|
+
describeCustomColorUsages,
|
|
196
|
+
nearestColorToken,
|
|
197
|
+
replaceAndRemoveCustomColor,
|
|
198
|
+
} from '../themes/slices/customColors';
|
|
199
|
+
export type { CustomColorUsage } from '../themes/slices/customColors';
|
|
200
|
+
|
|
182
201
|
export {
|
|
183
202
|
componentsToVars,
|
|
184
203
|
getComponentOwnedVarNames,
|
|
@@ -197,6 +216,10 @@ export {
|
|
|
197
216
|
markComponentSaved,
|
|
198
217
|
} from '../themes/slices/components';
|
|
199
218
|
|
|
219
|
+
export { a11yReport, a11yFinishedReport, a11yIssues, startA11yRun } from '../a11y/a11yReport';
|
|
220
|
+
export type { ComponentA11yIssues } from '../a11y/a11yReport';
|
|
221
|
+
export type { A11yContrastFailure, A11yFinding, A11yReport } from '../a11y/a11yReportTypes';
|
|
222
|
+
|
|
200
223
|
export {
|
|
201
224
|
setFontSources,
|
|
202
225
|
setFontStacks,
|
|
@@ -404,6 +427,7 @@ export function colorsAndTypeToState(colorsAndType: ColorsAndType): EditorState
|
|
|
404
427
|
// begin with).
|
|
405
428
|
for (const load of Object.values(domainLoaders)) load(next, rawVars);
|
|
406
429
|
loadGradientsFromFile(next, colorsAndType.gradients, rawVars);
|
|
430
|
+
loadCustomColorsFromFile(next, colorsAndType.customColors, rawVars);
|
|
407
431
|
next.harmonyAxes = sanitizeHarmonyAxes(renameBackgroundHarmonyFamily(colorsAndType.harmonyAxes), next.palettes);
|
|
408
432
|
next.cssVars = rawVars;
|
|
409
433
|
return next;
|
|
@@ -438,12 +462,17 @@ function colorsAndTypeContent(state: EditorState): Omit<ColorsAndType, 'name' |
|
|
|
438
462
|
if (state.gradients.tokens.length > 0) {
|
|
439
463
|
Object.assign(cssVariables, gradientsToVars(state.gradients));
|
|
440
464
|
}
|
|
465
|
+
// An empty list writes no field, so a theme that never held a custom colour
|
|
466
|
+
// still compares equal to the buffer its load produced.
|
|
467
|
+
const hasCustomColors = state.customColors.tokens.length > 0;
|
|
468
|
+
if (hasCustomColors) Object.assign(cssVariables, customColorsToVars(state.customColors));
|
|
441
469
|
return {
|
|
442
470
|
editorConfigs: state.palettes,
|
|
443
471
|
cssVariables,
|
|
444
472
|
fontSources: state.fonts.sources,
|
|
445
473
|
fontStacks: state.fonts.stacks,
|
|
446
474
|
gradients: state.gradients.tokens,
|
|
475
|
+
...(hasCustomColors ? { customColors: state.customColors.tokens } : {}),
|
|
447
476
|
harmonyAxes: state.harmonyAxes,
|
|
448
477
|
schemaVersion: CURRENT_COLORS_AND_TYPE_SCHEMA_VERSION,
|
|
449
478
|
};
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { PaletteConfig, FontSource, FontStack } from '../themes/themeTypes';
|
|
2
2
|
import type { GradientStop, GradientValue, LinearDirection } from '../themes/parsers/gradient';
|
|
3
3
|
import type { HarmonyAxis } from '../palettes/colorHarmony';
|
|
4
|
+
import type { Oklch } from '../palettes/oklch';
|
|
4
5
|
import type { ColumnViewport, ColumnViewportValues } from '../themes/slices/columns';
|
|
5
6
|
|
|
6
7
|
export interface ShadowGlobals {
|
|
@@ -80,6 +81,13 @@ export interface GradientToken {
|
|
|
80
81
|
stops: GradientTokenStop[];
|
|
81
82
|
}
|
|
82
83
|
|
|
84
|
+
/** A theme-owned `--color-custom-N` colour. Slots name it the way they name a
|
|
85
|
+
* palette step, so one edit moves every slot that names it. */
|
|
86
|
+
export interface CustomColorToken {
|
|
87
|
+
variable: string;
|
|
88
|
+
color: Oklch;
|
|
89
|
+
}
|
|
90
|
+
|
|
83
91
|
/** Structured gradient payload carried inline on a component alias.
|
|
84
92
|
* `GradientToken` minus `variable` (the alias key itself is the binding).
|
|
85
93
|
* Used when a component owns a per-instance gradient that doesn't share the
|
|
@@ -123,6 +131,7 @@ export interface EditorState {
|
|
|
123
131
|
columns: ColumnsState;
|
|
124
132
|
components: Record<string, ComponentSlice>;
|
|
125
133
|
gradients: { tokens: GradientToken[] };
|
|
134
|
+
customColors: { tokens: CustomColorToken[] };
|
|
126
135
|
/** Four fixed numbered axes (index 0 is the anchor); each owns a hue;
|
|
127
136
|
* family is the live-bound color or null. */
|
|
128
137
|
harmonyAxes: HarmonyAxis[];
|
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
import { writable } from 'svelte/store';
|
|
2
2
|
|
|
3
|
-
export type EditorView = 'tokens' | 'components' | 'colors' | 'sketch';
|
|
3
|
+
export type EditorView = 'tokens' | 'components' | 'colors' | 'sketch' | 'accessibility';
|
|
4
4
|
export type SidebarCondensed = boolean | 'auto';
|
|
5
5
|
|
|
6
6
|
const VIEW_KEY = 'lt.editorView';
|
|
7
7
|
const CONDENSED_KEY = 'lt.sidebarCondensed';
|
|
8
8
|
|
|
9
9
|
function isEditorView(v: unknown): v is EditorView {
|
|
10
|
-
return v === 'tokens' || v === 'components' || v === 'colors' || v === 'sketch';
|
|
10
|
+
return v === 'tokens' || v === 'components' || v === 'colors' || v === 'sketch' || v === 'accessibility';
|
|
11
11
|
}
|
|
12
12
|
|
|
13
13
|
// Session-scoped, not persistent: opening the editor fresh always starts on
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { derived, type Readable } from 'svelte/store';
|
|
2
|
+
import { colorsAndTypeDirty, componentDirty } from './editorStore';
|
|
3
|
+
import { sketchOffTheme } from '../sketch/sketchStore';
|
|
4
|
+
|
|
5
|
+
/** The open theme differs from its saved file: colors and type, any component, or the sketch layer. */
|
|
6
|
+
export const unsavedEdits: Readable<boolean> = derived(
|
|
7
|
+
[colorsAndTypeDirty, componentDirty, sketchOffTheme],
|
|
8
|
+
([$colorsAndType, $components, $sketch]) =>
|
|
9
|
+
$colorsAndType || Object.values($components).some(Boolean) || $sketch,
|
|
10
|
+
);
|
|
@@ -26,7 +26,7 @@ import { SHADOW_VAR_NAMES, parseShadowCss, shadowTokenCss } from './parsers/shad
|
|
|
26
26
|
import { AA_BODY, AA_LARGE, contrastRatio, findLForContrast } from '../palettes/contrast';
|
|
27
27
|
import { type HarmonyAxis, type HarmonyMode } from '../palettes/colorHarmony';
|
|
28
28
|
import { defaultPaletteConfig } from '../../ui/palette/paletteMath';
|
|
29
|
-
import type { GradientDiskToken, PaletteConfig } from './themeTypes';
|
|
29
|
+
import type { CustomColorDiskToken, GradientDiskToken, PaletteConfig } from './themeTypes';
|
|
30
30
|
|
|
31
31
|
export interface ColorsInput {
|
|
32
32
|
scheme: SchemeDirection;
|
|
@@ -41,12 +41,14 @@ export interface ColorsInput {
|
|
|
41
41
|
}
|
|
42
42
|
|
|
43
43
|
/** The live colors and type this pass reads before it replaces the color
|
|
44
|
-
* state: the override bag, so every name no palette owns rides through,
|
|
45
|
-
*
|
|
46
|
-
* derived, not carried — see
|
|
44
|
+
* state: the override bag, so every name no palette owns rides through, the
|
|
45
|
+
* swatch gradients, so user-tuned ones survive, and the custom colours, which
|
|
46
|
+
* ride through untouched. Shadow opacity is derived, not carried — see
|
|
47
|
+
* `shadowVars`. */
|
|
47
48
|
export interface CarryForward {
|
|
48
49
|
cssVariables?: Record<string, string>;
|
|
49
50
|
gradients?: GradientDiskToken[];
|
|
51
|
+
customColors?: CustomColorDiskToken[];
|
|
50
52
|
}
|
|
51
53
|
|
|
52
54
|
export interface ContrastCheck {
|
|
@@ -80,6 +82,9 @@ export interface ColorsState {
|
|
|
80
82
|
editorConfigs: Record<string, PaletteConfig>;
|
|
81
83
|
harmonyAxes: HarmonyAxis[];
|
|
82
84
|
gradients: GradientDiskToken[];
|
|
85
|
+
/** Present only when the carried colours and type held the field, so a
|
|
86
|
+
* recolor adds no key to a theme that never had one. */
|
|
87
|
+
customColors?: CustomColorDiskToken[];
|
|
83
88
|
cssVariables: Record<string, string>;
|
|
84
89
|
}
|
|
85
90
|
|
|
@@ -467,7 +472,13 @@ export function buildColors(input: ColorsInput, carry: CarryForward = {}): Build
|
|
|
467
472
|
Object.assign(cssVariables, shadowVars(carry.cssVariables ?? {}, baseColors.Canvas));
|
|
468
473
|
|
|
469
474
|
return {
|
|
470
|
-
colors: {
|
|
475
|
+
colors: {
|
|
476
|
+
editorConfigs: configs,
|
|
477
|
+
harmonyAxes,
|
|
478
|
+
gradients,
|
|
479
|
+
...(carry.customColors ? { customColors: carry.customColors } : {}),
|
|
480
|
+
cssVariables,
|
|
481
|
+
},
|
|
471
482
|
report: {
|
|
472
483
|
...report,
|
|
473
484
|
gradients: gradients === carry.gradients ? 'carried' : 'recipes',
|