@motion-proto/live-tokens 0.44.0 → 0.46.0
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/CHANGELOG.md +103 -0
- package/dist-plugin/index.cjs +1 -1
- package/dist-plugin/index.js +1 -1
- package/package.json +1 -1
- package/src/editor/component-editor/scaffolding/ComponentFileManager.svelte +0 -1
- package/src/editor/component-editor/scaffolding/ComponentFileMenu.svelte +1 -1
- package/src/editor/core/palettes/colorHarmony.ts +35 -6
- package/src/editor/core/palettes/paletteDerivation.ts +2 -3
- package/src/editor/core/store/editorTypes.ts +2 -2
- package/src/editor/docs/CodeBlock.svelte +0 -2
- package/src/editor/docs/Docs.svelte +0 -3
- package/src/editor/pages/ComponentEditorPage.svelte +3 -5
- package/src/editor/pages/EditorShell.svelte +3 -5
- package/src/editor/ui/ColorEditPanel.svelte +19 -22
- package/src/editor/ui/EditorViewSwitcher.svelte +2 -3
- package/src/editor/ui/FileLoadList.svelte +7 -9
- package/src/editor/ui/ManifestFileManager.svelte +1 -3
- package/src/editor/ui/ProjectFontsSection.svelte +0 -4
- package/src/editor/ui/SurfacesTab.svelte +3 -3
- package/src/editor/ui/TextStylesSection.svelte +6 -8
- package/src/editor/ui/ThemeFileManager.svelte +1 -3
- package/src/editor/ui/UIEasingSelector.svelte +0 -2
- package/src/editor/ui/UIFontFamilySelector.svelte +9 -3
- package/src/editor/ui/UIMenuButton.svelte +230 -0
- package/src/editor/ui/UIPaletteSelector.svelte +0 -1
- package/src/editor/ui/VariablesTab.svelte +3 -4
- package/src/editor/ui/colors/AxisNumeral.svelte +70 -0
- package/src/editor/ui/colors/ColorReadouts.svelte +0 -2
- package/src/editor/ui/colors/ColorStory.svelte +24 -77
- package/src/editor/ui/colors/ColorWheel.svelte +48 -29
- package/src/editor/ui/colors/ColorsTab.svelte +361 -212
- package/src/editor/ui/colors/HarmonyAxesList.svelte +264 -141
- package/src/editor/ui/colors/LightnessBar.svelte +0 -2
- package/src/editor/ui/colors/harmonyDrag.ts +10 -0
- package/src/editor/ui/colors/harmonyModeIcons.ts +5 -0
- package/src/editor/ui/colors/paletteBaseColor.ts +25 -39
- package/src/live-tokens/data/tokens.generated.css +28 -28
- package/src/system/components/ImageLightbox.svelte +0 -2
- package/src/system/components/SectionDivider.svelte +4 -4
- package/src/editor/core/palettes/recommendText.ts +0 -216
- package/src/editor/core/palettes/solveTextContrast.ts +0 -254
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
import { resolveAliasChain } from '../core/palettes/tokenRegistry';
|
|
6
6
|
import { editorState } from '../core/store/editorStore';
|
|
7
7
|
import { CSS_VAR_CHANGE_EVENT } from '../core/cssVarSync';
|
|
8
|
-
import type { FontFamily, FontSource } from '../core/themes/themeTypes';
|
|
8
|
+
import type { FontFamily, FontSource, FontSourceKind } from '../core/themes/themeTypes';
|
|
9
9
|
import UITokenSelector from './UITokenSelector.svelte';
|
|
10
10
|
import UIOptionList from './UIOptionList.svelte';
|
|
11
11
|
import UIOptionItem from './UIOptionItem.svelte';
|
|
@@ -28,6 +28,13 @@
|
|
|
28
28
|
onchange,
|
|
29
29
|
}: Props = $props();
|
|
30
30
|
|
|
31
|
+
const SOURCE_KIND_LABELS: Record<FontSourceKind, string> = {
|
|
32
|
+
google: 'Google Fonts',
|
|
33
|
+
typekit: 'Typekit',
|
|
34
|
+
'css-url': 'CSS URL',
|
|
35
|
+
'font-face': 'Font face',
|
|
36
|
+
};
|
|
37
|
+
|
|
31
38
|
const options = [
|
|
32
39
|
{ key: 'display', label: 'Display' },
|
|
33
40
|
{ key: 'sans', label: 'Sans' },
|
|
@@ -219,7 +226,7 @@
|
|
|
219
226
|
<div class="pfs-submenu">
|
|
220
227
|
{#each fontSources as source (source.id)}
|
|
221
228
|
{#if source.families.length > 0}
|
|
222
|
-
<div class="pfs-source-label">{source.label ?? source.kind}</div>
|
|
229
|
+
<div class="pfs-source-label">{source.label ?? SOURCE_KIND_LABELS[source.kind]}</div>
|
|
223
230
|
{#each source.families as fam (fam.id)}
|
|
224
231
|
<UIOptionItem
|
|
225
232
|
active={chosenFamilyId === fam.id}
|
|
@@ -329,6 +336,5 @@
|
|
|
329
336
|
font-family: var(--ui-font-mono);
|
|
330
337
|
font-size: var(--ui-font-size-xs);
|
|
331
338
|
color: var(--ui-text-tertiary);
|
|
332
|
-
text-transform: uppercase;
|
|
333
339
|
}
|
|
334
340
|
</style>
|
|
@@ -0,0 +1,230 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { onMount, onDestroy } from 'svelte';
|
|
3
|
+
import type { Snippet } from 'svelte';
|
|
4
|
+
|
|
5
|
+
interface Props {
|
|
6
|
+
/** Heading line inside the menu; also its accessible name. */
|
|
7
|
+
header: string;
|
|
8
|
+
/** Accessible name for an icon-only trigger; a text trigger names itself. */
|
|
9
|
+
triggerLabel?: string;
|
|
10
|
+
/** Lets the caller style the trigger as its own furniture via `:global`. */
|
|
11
|
+
triggerClass?: string;
|
|
12
|
+
menuMinWidth?: string;
|
|
13
|
+
trigger: Snippet;
|
|
14
|
+
children: Snippet<[{ close: () => void }]>;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
let {
|
|
18
|
+
header,
|
|
19
|
+
triggerLabel,
|
|
20
|
+
triggerClass = '',
|
|
21
|
+
menuMinWidth = '14rem',
|
|
22
|
+
trigger,
|
|
23
|
+
children,
|
|
24
|
+
}: Props = $props();
|
|
25
|
+
|
|
26
|
+
let open = $state(false);
|
|
27
|
+
let btnEl = $state<HTMLButtonElement | undefined>(undefined);
|
|
28
|
+
let menuEl = $state<HTMLDivElement | undefined>(undefined);
|
|
29
|
+
let initialItem = $state<'first' | 'last'>('first');
|
|
30
|
+
|
|
31
|
+
function items(): HTMLButtonElement[] {
|
|
32
|
+
return menuEl ? Array.from(menuEl.querySelectorAll<HTMLButtonElement>('button:not(:disabled)')) : [];
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function close(refocusTrigger = true) {
|
|
36
|
+
if (!open) return;
|
|
37
|
+
open = false;
|
|
38
|
+
if (refocusTrigger) btnEl?.focus();
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function toggle() {
|
|
42
|
+
if (open) {
|
|
43
|
+
close();
|
|
44
|
+
} else {
|
|
45
|
+
initialItem = 'first';
|
|
46
|
+
open = true;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function onTriggerKeydown(e: KeyboardEvent) {
|
|
51
|
+
if (e.key !== 'ArrowDown' && e.key !== 'ArrowUp') return;
|
|
52
|
+
e.preventDefault();
|
|
53
|
+
initialItem = e.key === 'ArrowUp' ? 'last' : 'first';
|
|
54
|
+
open = true;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// Enter and Space activate the focused item natively: items are real buttons.
|
|
58
|
+
function onMenuKeydown(e: KeyboardEvent) {
|
|
59
|
+
if (e.key === 'Escape' || e.key === 'Tab') {
|
|
60
|
+
e.preventDefault();
|
|
61
|
+
close();
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
const list = items();
|
|
65
|
+
if (list.length === 0) return;
|
|
66
|
+
const idx = list.indexOf(document.activeElement as HTMLButtonElement);
|
|
67
|
+
let next = -1;
|
|
68
|
+
if (e.key === 'ArrowDown') next = idx === -1 ? 0 : (idx + 1) % list.length;
|
|
69
|
+
else if (e.key === 'ArrowUp') next = idx === -1 ? list.length - 1 : (idx - 1 + list.length) % list.length;
|
|
70
|
+
else if (e.key === 'Home') next = 0;
|
|
71
|
+
else if (e.key === 'End') next = list.length - 1;
|
|
72
|
+
if (next === -1) return;
|
|
73
|
+
e.preventDefault();
|
|
74
|
+
list[next].focus();
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function onWindowKeydown(e: KeyboardEvent) {
|
|
78
|
+
if (e.key === 'Escape' && open) close();
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function onDocumentMousedown(e: MouseEvent) {
|
|
82
|
+
if (!open) return;
|
|
83
|
+
const target = e.target as Node | null;
|
|
84
|
+
if (target && (btnEl?.contains(target) || menuEl?.contains(target))) return;
|
|
85
|
+
// The user is directing attention elsewhere; refocusing the trigger would
|
|
86
|
+
// steal focus from whatever they clicked.
|
|
87
|
+
close(false);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
onMount(() => {
|
|
91
|
+
window.addEventListener('keydown', onWindowKeydown);
|
|
92
|
+
document.addEventListener('mousedown', onDocumentMousedown, true);
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
onDestroy(() => {
|
|
96
|
+
window.removeEventListener('keydown', onWindowKeydown);
|
|
97
|
+
document.removeEventListener('mousedown', onDocumentMousedown, true);
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
/* Fixed positioning escapes any parent overflow/stacking context. Anchored
|
|
101
|
+
below the trigger, right edges aligned; flips above near the viewport
|
|
102
|
+
bottom. Inline visibility rather than a state flag so the item focus that
|
|
103
|
+
follows lands on an already-visible menu (hidden elements refuse focus). */
|
|
104
|
+
function position(): void {
|
|
105
|
+
const btn = btnEl;
|
|
106
|
+
const menu = menuEl;
|
|
107
|
+
if (!btn || !menu) return;
|
|
108
|
+
const br = btn.getBoundingClientRect();
|
|
109
|
+
const mr = menu.getBoundingClientRect();
|
|
110
|
+
const margin = 8;
|
|
111
|
+
const vw = window.innerWidth;
|
|
112
|
+
const vh = window.innerHeight;
|
|
113
|
+
let left = br.right - mr.width;
|
|
114
|
+
if (left < margin) left = margin;
|
|
115
|
+
if (left + mr.width > vw - margin) left = vw - margin - mr.width;
|
|
116
|
+
let top = br.bottom + margin / 2;
|
|
117
|
+
if (top + mr.height > vh - margin) {
|
|
118
|
+
top = br.top - margin / 2 - mr.height;
|
|
119
|
+
if (top < margin) top = margin;
|
|
120
|
+
}
|
|
121
|
+
menu.style.left = `${left}px`;
|
|
122
|
+
menu.style.top = `${top}px`;
|
|
123
|
+
menu.style.visibility = 'visible';
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
$effect(() => {
|
|
127
|
+
if (!open) return;
|
|
128
|
+
let raf = requestAnimationFrame(() => {
|
|
129
|
+
raf = requestAnimationFrame(() => {
|
|
130
|
+
// The composed option primitives expose no role prop; stamping
|
|
131
|
+
// menuitem + tabindex here is what makes the popup a real menu with
|
|
132
|
+
// roving focus instead of a cluster of tab stops.
|
|
133
|
+
for (const b of items()) {
|
|
134
|
+
b.setAttribute('role', 'menuitem');
|
|
135
|
+
b.tabIndex = -1;
|
|
136
|
+
}
|
|
137
|
+
position();
|
|
138
|
+
const list = items();
|
|
139
|
+
(initialItem === 'last' ? list[list.length - 1] : list[0])?.focus();
|
|
140
|
+
});
|
|
141
|
+
});
|
|
142
|
+
window.addEventListener('scroll', position, true);
|
|
143
|
+
window.addEventListener('resize', position);
|
|
144
|
+
return () => {
|
|
145
|
+
cancelAnimationFrame(raf);
|
|
146
|
+
window.removeEventListener('scroll', position, true);
|
|
147
|
+
window.removeEventListener('resize', position);
|
|
148
|
+
};
|
|
149
|
+
});
|
|
150
|
+
</script>
|
|
151
|
+
|
|
152
|
+
<button
|
|
153
|
+
type="button"
|
|
154
|
+
class="menu-trigger {triggerClass}"
|
|
155
|
+
aria-haspopup="menu"
|
|
156
|
+
aria-expanded={open}
|
|
157
|
+
aria-label={triggerLabel}
|
|
158
|
+
bind:this={btnEl}
|
|
159
|
+
onclick={toggle}
|
|
160
|
+
onkeydown={onTriggerKeydown}
|
|
161
|
+
>
|
|
162
|
+
{@render trigger()}
|
|
163
|
+
</button>
|
|
164
|
+
|
|
165
|
+
{#if open}
|
|
166
|
+
<div
|
|
167
|
+
class="menu"
|
|
168
|
+
role="menu"
|
|
169
|
+
tabindex="-1"
|
|
170
|
+
aria-label={header}
|
|
171
|
+
style="min-width: {menuMinWidth};"
|
|
172
|
+
bind:this={menuEl}
|
|
173
|
+
onkeydown={onMenuKeydown}
|
|
174
|
+
>
|
|
175
|
+
<div class="menu-header" role="presentation">{header}</div>
|
|
176
|
+
{@render children({ close })}
|
|
177
|
+
</div>
|
|
178
|
+
{/if}
|
|
179
|
+
|
|
180
|
+
<style>
|
|
181
|
+
.menu-trigger {
|
|
182
|
+
display: inline-flex;
|
|
183
|
+
align-items: center;
|
|
184
|
+
justify-content: center;
|
|
185
|
+
gap: var(--ui-space-4);
|
|
186
|
+
padding: 0;
|
|
187
|
+
background: transparent;
|
|
188
|
+
border: 0;
|
|
189
|
+
color: inherit;
|
|
190
|
+
font: inherit;
|
|
191
|
+
line-height: 1;
|
|
192
|
+
cursor: pointer;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
.menu-trigger:focus-visible {
|
|
196
|
+
outline: 2px solid var(--ui-border-higher);
|
|
197
|
+
outline-offset: 2px;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
.menu {
|
|
201
|
+
position: fixed;
|
|
202
|
+
top: 0;
|
|
203
|
+
left: 0;
|
|
204
|
+
max-width: calc(100vw - var(--ui-space-24));
|
|
205
|
+
background: var(--ui-surface-higher);
|
|
206
|
+
border: 1px solid var(--ui-border-high);
|
|
207
|
+
border-radius: var(--ui-radius-lg);
|
|
208
|
+
box-shadow: var(--ui-shadow-lg);
|
|
209
|
+
z-index: 1000;
|
|
210
|
+
overflow: hidden;
|
|
211
|
+
visibility: hidden;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
.menu-header {
|
|
215
|
+
padding: var(--ui-space-8) var(--ui-space-12);
|
|
216
|
+
border-bottom: 1px solid var(--ui-border-low);
|
|
217
|
+
color: var(--ui-text-primary);
|
|
218
|
+
font-size: var(--ui-font-size-xs);
|
|
219
|
+
font-weight: var(--ui-font-weight-semibold);
|
|
220
|
+
line-height: 1.2;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
/* Roving focus is the menu's selection cursor; the inset ring keeps it
|
|
224
|
+
inside the clipped rounded corners. */
|
|
225
|
+
.menu :global(button:focus-visible) {
|
|
226
|
+
background: var(--ui-hover);
|
|
227
|
+
outline: 2px solid var(--ui-border-higher);
|
|
228
|
+
outline-offset: -2px;
|
|
229
|
+
}
|
|
230
|
+
</style>
|
|
@@ -204,10 +204,9 @@
|
|
|
204
204
|
/* Subsection title (used by Spacing & Borders) */
|
|
205
205
|
.subsection-title {
|
|
206
206
|
margin: var(--ui-space-16) 0 var(--ui-space-8);
|
|
207
|
-
font-size: var(--ui-font-size-
|
|
208
|
-
font-weight: var(--ui-font-weight-
|
|
209
|
-
color: var(--ui-text-
|
|
210
|
-
text-transform: uppercase;
|
|
207
|
+
font-size: var(--ui-font-size-xl);
|
|
208
|
+
font-weight: var(--ui-font-weight-bold);
|
|
209
|
+
color: var(--ui-text-primary);
|
|
211
210
|
}
|
|
212
211
|
|
|
213
212
|
.subsection-title:first-child {
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import type { AxisStatus } from '../../core/palettes/colorHarmony';
|
|
3
|
+
|
|
4
|
+
interface Props {
|
|
5
|
+
index: number;
|
|
6
|
+
status: AxisStatus;
|
|
7
|
+
/** The axis carries the family selected across the Colors surfaces. */
|
|
8
|
+
selected?: boolean;
|
|
9
|
+
/** Rendered over the swatch badge's scrim rather than an editor surface. */
|
|
10
|
+
scrim?: boolean;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
let { index, status, selected = false, scrim = false }: Props = $props();
|
|
14
|
+
</script>
|
|
15
|
+
|
|
16
|
+
<!-- Carries no positioning: the wheel places it absolutely, the rows inline. -->
|
|
17
|
+
<span class="numeral" class:filled={status === 'on-wheel'} class:selected class:scrim>{index + 1}</span>
|
|
18
|
+
|
|
19
|
+
<style>
|
|
20
|
+
/* Hollow is the resting shape; filled means the axis has a rail on the wheel
|
|
21
|
+
right now. An unused axis is hollow too, but it never holds a family, so a
|
|
22
|
+
hollow numeral beside a color always reads "bound, off the wheel". */
|
|
23
|
+
.numeral {
|
|
24
|
+
box-sizing: border-box;
|
|
25
|
+
flex: none;
|
|
26
|
+
display: inline-flex;
|
|
27
|
+
align-items: center;
|
|
28
|
+
justify-content: center;
|
|
29
|
+
/* Callers that need a bigger numeral set these two; the row default is the
|
|
30
|
+
size the wheel and the axes list share. */
|
|
31
|
+
width: var(--numeral-size, 1rem);
|
|
32
|
+
height: var(--numeral-size, 1rem);
|
|
33
|
+
border: 1px dashed var(--ui-text-muted);
|
|
34
|
+
border-radius: var(--ui-radius-full);
|
|
35
|
+
color: var(--ui-text-muted);
|
|
36
|
+
font-size: var(--numeral-font-size, var(--ui-font-size-xs));
|
|
37
|
+
font-weight: var(--ui-font-weight-semibold);
|
|
38
|
+
/* No tabular figures: the box is a fixed circle, so nothing needs aligning,
|
|
39
|
+
and a padded tabular "1" sits off-center inside its own advance. */
|
|
40
|
+
line-height: 1;
|
|
41
|
+
user-select: none;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/* The scrim composites with the swatch fill beneath it, which lands close
|
|
45
|
+
enough to the muted token to erase the ring and the digit. */
|
|
46
|
+
.numeral.scrim:not(.filled) {
|
|
47
|
+
border-color: var(--ui-text-secondary);
|
|
48
|
+
color: var(--ui-text-secondary);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
.numeral.filled {
|
|
52
|
+
border-style: solid;
|
|
53
|
+
border-color: var(--ui-border-higher);
|
|
54
|
+
background: var(--ui-surface-high);
|
|
55
|
+
color: var(--ui-text-secondary);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
.numeral.filled.selected {
|
|
59
|
+
background: var(--ui-text-primary);
|
|
60
|
+
border-color: var(--ui-text-primary);
|
|
61
|
+
color: var(--ui-surface-lowest);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/* Selection raises contrast but never fills: filled is reserved for "has a
|
|
65
|
+
rail", so inverting here would make the list contradict the wheel. */
|
|
66
|
+
.numeral.selected:not(.filled) {
|
|
67
|
+
border-color: var(--ui-text-primary);
|
|
68
|
+
color: var(--ui-text-primary);
|
|
69
|
+
}
|
|
70
|
+
</style>
|
|
@@ -2,18 +2,14 @@
|
|
|
2
2
|
import { oklchToHexClamped } from '../../core/palettes/oklch';
|
|
3
3
|
import { palettesToVars } from '../../core/palettes/paletteDerivation';
|
|
4
4
|
import { contrastRatio } from '../../core/palettes/contrast';
|
|
5
|
-
import type { NeutralTextRecommendation } from '../../core/palettes/recommendText';
|
|
6
5
|
import type { PaletteConfig } from '../../core/themes/themeTypes';
|
|
7
6
|
|
|
8
7
|
interface Props {
|
|
9
8
|
/** Every family's config, spec defaults filled in (palettesWithDefaults). */
|
|
10
9
|
full: Record<string, PaletteConfig>;
|
|
11
|
-
/** Suggested neutral text hierarchy — read out here, acted on from the
|
|
12
|
-
* section's action pills. The story itself stays non-interactive. */
|
|
13
|
-
rec: NeutralTextRecommendation;
|
|
14
10
|
}
|
|
15
11
|
|
|
16
|
-
let { full
|
|
12
|
+
let { full }: Props = $props();
|
|
17
13
|
|
|
18
14
|
// 60-30-10 as an area composition: the background surface is the dominant 60%
|
|
19
15
|
// field (the page), and Brand/Accent/Special are the tones shown against it.
|
|
@@ -24,6 +20,8 @@
|
|
|
24
20
|
{ label: 'Special', ns: 'special', share: 2 },
|
|
25
21
|
] as const;
|
|
26
22
|
|
|
23
|
+
const TEXT_STEPS = ['primary', 'secondary', 'tertiary'] as const;
|
|
24
|
+
|
|
27
25
|
const FUNCTIONAL = [
|
|
28
26
|
{ label: 'Info', ns: 'info' },
|
|
29
27
|
{ label: 'Success', ns: 'success' },
|
|
@@ -69,8 +67,6 @@
|
|
|
69
67
|
return r === null ? null : `${(Math.floor(r * 10) / 10).toFixed(1)}:1`;
|
|
70
68
|
}
|
|
71
69
|
|
|
72
|
-
let bgRatio = $derived(ratioOnBg('--text-primary'));
|
|
73
|
-
|
|
74
70
|
// Inverted is measured against the fill it sits on (primary), not the page:
|
|
75
71
|
// the pill below IS that pairing, so the number describes what is shown. A
|
|
76
72
|
// mid-lightness primary makes this pairing inherently low-contrast — the
|
|
@@ -86,19 +82,20 @@
|
|
|
86
82
|
|
|
87
83
|
<div class="story" style={storyVars}>
|
|
88
84
|
<div class="field">
|
|
89
|
-
<
|
|
90
|
-
|
|
91
|
-
|
|
85
|
+
<span class="field-label">Canvas</span>
|
|
86
|
+
|
|
87
|
+
<!-- Each step renders in its own token, so the word IS the sample and the
|
|
88
|
+
ratio beside it measures exactly what you are reading. -->
|
|
89
|
+
<div class="text-steps">
|
|
90
|
+
{#each TEXT_STEPS as step (step)}
|
|
91
|
+
{@const r = fmt(ratioOnBg(`--text-${step}`))}
|
|
92
|
+
<p class="text-step" style:color="var(--text-{step})">
|
|
93
|
+
{step}
|
|
94
|
+
{#if r}<span class="ratio">{r}</span>{/if}
|
|
95
|
+
</p>
|
|
96
|
+
{/each}
|
|
92
97
|
</div>
|
|
93
|
-
|
|
94
|
-
<p class="secondary">
|
|
95
|
-
Secondary text follows the same field.
|
|
96
|
-
{#if fmt(ratioOnBg('--text-secondary'))}<span class="ratio">{fmt(ratioOnBg('--text-secondary'))}</span>{/if}
|
|
97
|
-
</p>
|
|
98
|
-
<p class="tertiary">
|
|
99
|
-
Tertiary text steps back once more.
|
|
100
|
-
{#if fmt(ratioOnBg('--text-tertiary'))}<span class="ratio">{fmt(ratioOnBg('--text-tertiary'))}</span>{/if}
|
|
101
|
-
</p>
|
|
98
|
+
|
|
102
99
|
<p class="inverted-row">
|
|
103
100
|
<span class="inverted-pill">
|
|
104
101
|
Inverted text flips onto primary.
|
|
@@ -106,18 +103,6 @@
|
|
|
106
103
|
</span>
|
|
107
104
|
</p>
|
|
108
105
|
|
|
109
|
-
<!-- Readout only: applied from the section's actions, not from here. -->
|
|
110
|
-
<div class="suggest-row" title="What “Even neutral steps” would set these three text steps to">
|
|
111
|
-
<span class="suggest-label">Suggested steps</span>
|
|
112
|
-
{#each rec.suggestions as s (s.step)}
|
|
113
|
-
<span class="suggest-entry" class:dim={!s.differs}>
|
|
114
|
-
<span class="fn-dot" style:--dot={s.suggested.hex}></span>
|
|
115
|
-
<span class="suggest-step">{s.step}</span>
|
|
116
|
-
<span class="ratio">{fmt(contrastRatio(s.suggested.hex, seedHex('Canvas')))}</span>
|
|
117
|
-
</span>
|
|
118
|
-
{/each}
|
|
119
|
-
</div>
|
|
120
|
-
|
|
121
106
|
<div class="functional-row">
|
|
122
107
|
{#each FUNCTIONAL as fn (fn.ns)}
|
|
123
108
|
{@const r = fmt(ratioOnBg(`--text-${fn.ns}`))}
|
|
@@ -172,36 +157,11 @@
|
|
|
172
157
|
color: var(--text-primary);
|
|
173
158
|
}
|
|
174
159
|
|
|
175
|
-
.field-head {
|
|
176
|
-
display: flex;
|
|
177
|
-
align-items: baseline;
|
|
178
|
-
justify-content: space-between;
|
|
179
|
-
gap: var(--ui-space-8);
|
|
180
|
-
}
|
|
181
|
-
|
|
182
160
|
.field-label {
|
|
183
161
|
font-size: var(--ui-font-size-md);
|
|
184
162
|
font-weight: var(--ui-font-weight-semibold);
|
|
185
163
|
}
|
|
186
164
|
|
|
187
|
-
.body {
|
|
188
|
-
margin: 0;
|
|
189
|
-
font-size: var(--ui-font-size-md);
|
|
190
|
-
color: var(--text-primary);
|
|
191
|
-
}
|
|
192
|
-
|
|
193
|
-
.secondary {
|
|
194
|
-
margin: 0;
|
|
195
|
-
font-size: var(--ui-font-size-md);
|
|
196
|
-
color: var(--text-secondary);
|
|
197
|
-
}
|
|
198
|
-
|
|
199
|
-
.tertiary {
|
|
200
|
-
margin: 0;
|
|
201
|
-
font-size: var(--ui-font-size-md);
|
|
202
|
-
color: var(--text-tertiary);
|
|
203
|
-
}
|
|
204
|
-
|
|
205
165
|
.inverted-row {
|
|
206
166
|
margin: var(--ui-space-8) 0 0;
|
|
207
167
|
}
|
|
@@ -219,31 +179,18 @@
|
|
|
219
179
|
font-size: var(--ui-font-size-md);
|
|
220
180
|
}
|
|
221
181
|
|
|
222
|
-
.
|
|
182
|
+
.text-steps {
|
|
223
183
|
display: flex;
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
gap: var(--ui-space-8);
|
|
227
|
-
margin-top: var(--ui-space-12);
|
|
228
|
-
}
|
|
229
|
-
|
|
230
|
-
.suggest-label {
|
|
231
|
-
font-size: var(--ui-font-size-md);
|
|
232
|
-
font-weight: var(--ui-font-weight-semibold);
|
|
233
|
-
opacity: 0.6;
|
|
234
|
-
}
|
|
235
|
-
|
|
236
|
-
.suggest-entry {
|
|
237
|
-
display: flex;
|
|
238
|
-
align-items: center;
|
|
184
|
+
flex-direction: column;
|
|
185
|
+
align-items: flex-start;
|
|
239
186
|
gap: var(--ui-space-6);
|
|
240
187
|
}
|
|
241
188
|
|
|
242
|
-
.
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
189
|
+
.text-step {
|
|
190
|
+
display: flex;
|
|
191
|
+
align-items: baseline;
|
|
192
|
+
gap: var(--ui-space-8);
|
|
193
|
+
margin: 0;
|
|
247
194
|
font-size: var(--ui-font-size-md);
|
|
248
195
|
}
|
|
249
196
|
|