@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
|
@@ -1,35 +1,34 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
|
+
import { cubicOut } from 'svelte/easing';
|
|
2
3
|
import { oklchToHexClamped } from '../../core/palettes/oklch';
|
|
3
4
|
import { PALETTE_SPECS } from '../../core/palettes/paletteDerivation';
|
|
4
|
-
import { editorState, beginSliderGesture
|
|
5
|
-
import { applyHarmonyToAxes,
|
|
5
|
+
import { editorState, beginSliderGesture } from '../../core/store/editorStore';
|
|
6
|
+
import { applyHarmonyToAxes, axisStatuses, HARMONY_ELIGIBLE, type HarmonyMode } from '../../core/palettes/colorHarmony';
|
|
7
|
+
import AxisNumeral from './AxisNumeral.svelte';
|
|
6
8
|
import ColorEditPanel from '../ColorEditPanel.svelte';
|
|
7
9
|
import ColorWheel from './ColorWheel.svelte';
|
|
8
|
-
import LightnessBar from './LightnessBar.svelte';
|
|
9
10
|
import ColorReadouts from './ColorReadouts.svelte';
|
|
10
11
|
import ColorStory from './ColorStory.svelte';
|
|
11
12
|
import PaletteStepStrip from './PaletteStepStrip.svelte';
|
|
12
13
|
import HarmonyAxesList from './HarmonyAxesList.svelte';
|
|
13
|
-
import UIPillButton from '../UIPillButton.svelte';
|
|
14
|
-
import { recommendNeutralText } from '../../core/palettes/recommendText';
|
|
15
14
|
import {
|
|
16
15
|
setBaseColor,
|
|
17
|
-
setBaseColors,
|
|
18
16
|
setAxisHues,
|
|
19
|
-
|
|
20
|
-
applySuggestedNeutralText,
|
|
17
|
+
unbindFamily,
|
|
21
18
|
palettesWithDefaults,
|
|
22
19
|
} from './paletteBaseColor';
|
|
23
20
|
import { HARMONY_MODE_BUTTONS } from './harmonyModeIcons';
|
|
24
|
-
import {
|
|
21
|
+
import { HARMONY_DRAG_TYPE, startFamilyDrag } from './harmonyDrag';
|
|
22
|
+
import { maxChroma } from './colorWheelMath';
|
|
25
23
|
|
|
26
24
|
let selected = $state('Brand');
|
|
27
25
|
let activeMode = $state<HarmonyMode>('custom');
|
|
28
|
-
let hoverMode = $state<HarmonyMode | null>(null);
|
|
29
26
|
// Local UI only — deliberately NOT stored in PaletteConfig (shape unchanged).
|
|
30
27
|
let absoluteChroma = $state(false);
|
|
31
28
|
let infoOpen = $state(false);
|
|
32
29
|
let infoWrap: HTMLElement | undefined = $state();
|
|
30
|
+
let axesInfoOpen = $state(false);
|
|
31
|
+
let axesInfoWrap: HTMLElement | undefined = $state();
|
|
33
32
|
|
|
34
33
|
function select(label: string) {
|
|
35
34
|
selected = label;
|
|
@@ -50,6 +49,15 @@
|
|
|
50
49
|
return () => document.removeEventListener('click', onDoc);
|
|
51
50
|
});
|
|
52
51
|
|
|
52
|
+
$effect(() => {
|
|
53
|
+
if (!axesInfoOpen) return;
|
|
54
|
+
const onDoc = (e: MouseEvent) => {
|
|
55
|
+
if (axesInfoWrap && !axesInfoWrap.contains(e.target as Node)) axesInfoOpen = false;
|
|
56
|
+
};
|
|
57
|
+
document.addEventListener('click', onDoc);
|
|
58
|
+
return () => document.removeEventListener('click', onDoc);
|
|
59
|
+
});
|
|
60
|
+
|
|
53
61
|
function applyMode(mode: HarmonyMode) {
|
|
54
62
|
if (mode === 'custom') {
|
|
55
63
|
activeMode = 'custom';
|
|
@@ -62,33 +70,53 @@
|
|
|
62
70
|
setAxisHues(target.map((a, i) => ({ index: i, hue: a.hue })), `colors: harmony ${mode}`);
|
|
63
71
|
}
|
|
64
72
|
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
73
|
+
const ELIGIBLE = new Set(HARMONY_ELIGIBLE);
|
|
74
|
+
|
|
75
|
+
const reduceMotion =
|
|
76
|
+
typeof window !== 'undefined' && window.matchMedia?.('(prefers-reduced-motion: reduce)').matches === true;
|
|
77
|
+
|
|
78
|
+
/** The badge is the swatch's answer to "did it land": it fades up as the color
|
|
79
|
+
* takes an axis, and back out when the color comes home. It rides over the
|
|
80
|
+
* fill, so nothing reflows and opacity alone carries the change. */
|
|
81
|
+
function badgeFade(_node: Element) {
|
|
82
|
+
return {
|
|
83
|
+
duration: reduceMotion ? 0 : 200,
|
|
84
|
+
easing: cubicOut,
|
|
85
|
+
css: (t: number) => `opacity: ${t}; transform: translateY(-50%) scale(${0.85 + 0.15 * t});`,
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// The swatch row is the home a family returns to: dropping a chip back here
|
|
90
|
+
// is the unassign gesture, which is why no separate tray exists.
|
|
91
|
+
let unassignHover = $state(false);
|
|
92
|
+
|
|
93
|
+
function onSwatchesDragOver(e: DragEvent) {
|
|
94
|
+
if (!(e.dataTransfer?.types ?? []).includes(HARMONY_DRAG_TYPE)) return;
|
|
95
|
+
e.preventDefault();
|
|
96
|
+
if (e.dataTransfer) e.dataTransfer.dropEffect = 'move';
|
|
97
|
+
unassignHover = true;
|
|
68
98
|
}
|
|
69
99
|
|
|
70
|
-
function
|
|
71
|
-
|
|
100
|
+
function onSwatchesDrop(e: DragEvent) {
|
|
101
|
+
e.preventDefault();
|
|
102
|
+
unassignHover = false;
|
|
103
|
+
const family = e.dataTransfer?.getData(HARMONY_DRAG_TYPE);
|
|
104
|
+
if (family) unbindFamily(family);
|
|
72
105
|
}
|
|
73
106
|
|
|
74
|
-
// Color Story model: the story is a pure readout, so its palette map and text
|
|
75
|
-
// recommendation are owned here alongside the actions that write them.
|
|
76
|
-
let useBlackWhite = $state(false);
|
|
77
107
|
let fullPalettes = $derived(palettesWithDefaults($editorState.palettes));
|
|
78
|
-
let textRec = $derived(recommendNeutralText(fullPalettes, undefined, useBlackWhite));
|
|
79
|
-
|
|
80
|
-
let pureExtreme = $derived(textRec.scheme === 'light' ? 'pure black' : 'pure white');
|
|
81
108
|
|
|
82
109
|
let swatches = $derived.by(() => {
|
|
83
|
-
|
|
84
|
-
const active = modeActiveAxes(activeMode);
|
|
110
|
+
const statuses = axisStatuses(activeMode, $editorState.harmonyAxes);
|
|
85
111
|
return PALETTE_SPECS.map((spec) => {
|
|
86
112
|
const { l, c, h } = $editorState.palettes[spec.label]?.baseColor ?? spec.initialColor;
|
|
113
|
+
const index = $editorState.harmonyAxes.findIndex((a) => a.family === spec.label);
|
|
87
114
|
return {
|
|
88
115
|
label: spec.label,
|
|
89
116
|
display: spec.displayLabel ?? spec.label,
|
|
90
117
|
hex: oklchToHexClamped(l, c, h),
|
|
91
|
-
|
|
118
|
+
axis: index === -1 ? null : { index, status: statuses[index] },
|
|
119
|
+
eligible: ELIGIBLE.has(spec.label),
|
|
92
120
|
};
|
|
93
121
|
});
|
|
94
122
|
});
|
|
@@ -99,137 +127,171 @@
|
|
|
99
127
|
let coreSwatches = $derived(swatches.filter((sw) => !FUNCTIONAL_FAMILIES.has(sw.label)));
|
|
100
128
|
let functionalSwatches = $derived(swatches.filter((sw) => FUNCTIONAL_FAMILIES.has(sw.label)));
|
|
101
129
|
|
|
102
|
-
let shownModeLabel = $derived(
|
|
103
|
-
HARMONY_MODE_BUTTONS.find((b) => b.mode === (hoverMode ?? activeMode))?.label ?? '',
|
|
104
|
-
);
|
|
105
|
-
|
|
106
130
|
let selectedSpec = $derived(PALETTE_SPECS.find((s) => s.label === selected) ?? PALETTE_SPECS[0]);
|
|
107
131
|
let selectedOklch = $derived($editorState.palettes[selected]?.baseColor ?? selectedSpec.initialColor);
|
|
132
|
+
|
|
133
|
+
const rFracOf = (l: number, c: number, h: number) =>
|
|
134
|
+
Math.min(1, Math.max(0, c / (maxChroma(l, h) || 1e-6)));
|
|
135
|
+
|
|
136
|
+
// H and L slider edits honor the Absolute Chroma toggle the way the wheel
|
|
137
|
+
// does: with it off, the relative saturation fraction holds while the gamut
|
|
138
|
+
// moves. An explicit chroma edit (C slider, hex, eyedropper) is taken as-is.
|
|
139
|
+
function editBase(h: number, c: number, lPct: number) {
|
|
140
|
+
const l = lPct / 100;
|
|
141
|
+
const cur = selectedOklch;
|
|
142
|
+
const chroma = absoluteChroma || c !== cur.c
|
|
143
|
+
? c
|
|
144
|
+
: rFracOf(cur.l, cur.c, cur.h) * maxChroma(l, h);
|
|
145
|
+
setBaseColor(selected, { l, c: chroma, h });
|
|
146
|
+
}
|
|
108
147
|
</script>
|
|
109
148
|
|
|
110
149
|
<div class="colors-tab">
|
|
111
150
|
<div class="pane">
|
|
112
151
|
<section id="colors-wheel" class="block">
|
|
113
152
|
<header class="block-head">
|
|
114
|
-
<span class="eyebrow">Theme</span>
|
|
115
153
|
<h2 class="title">Color Wheel</h2>
|
|
116
154
|
</header>
|
|
117
155
|
|
|
118
|
-
<
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
onSelect={select}
|
|
123
|
-
discLightness={selectedOklch.l}
|
|
124
|
-
onCustomize={() => (activeMode = 'custom')}
|
|
125
|
-
/>
|
|
126
|
-
|
|
127
|
-
<LightnessBar {selected} {absoluteChroma} />
|
|
128
|
-
|
|
129
|
-
<div class="group">
|
|
130
|
-
<span class="eyebrow">Color Harmony <span class="mode-name">· {shownModeLabel}</span></span>
|
|
131
|
-
<div class="mode-row" role="group" aria-label="Color harmony mode">
|
|
132
|
-
{#each HARMONY_MODE_BUTTONS as m (m.mode)}
|
|
133
|
-
<button
|
|
134
|
-
type="button"
|
|
135
|
-
class="mode-btn"
|
|
136
|
-
class:active={activeMode === m.mode}
|
|
137
|
-
title={m.label}
|
|
138
|
-
aria-label={m.label}
|
|
139
|
-
aria-pressed={activeMode === m.mode}
|
|
140
|
-
onclick={() => applyMode(m.mode)}
|
|
141
|
-
onpointerenter={() => (hoverMode = m.mode)}
|
|
142
|
-
onpointerleave={() => (hoverMode = null)}
|
|
143
|
-
onfocus={() => (hoverMode = m.mode)}
|
|
144
|
-
onblur={() => (hoverMode = null)}
|
|
145
|
-
>{@html m.svg}</button>
|
|
146
|
-
{/each}
|
|
147
|
-
</div>
|
|
148
|
-
|
|
149
|
-
<div class="harmony-actions">
|
|
150
|
-
<UIPillButton
|
|
151
|
-
icon="fa-fill-drip"
|
|
152
|
-
title="Re-hue Neutral and Alternate to the anchor color (their own chroma and lightness kept)"
|
|
153
|
-
onclick={tintNeutrals}
|
|
154
|
-
>Tint neutrals from anchor</UIPillButton>
|
|
155
|
-
</div>
|
|
156
|
-
|
|
157
|
-
<div class="wheel-opts">
|
|
158
|
-
<label class="opt">
|
|
159
|
-
<input type="checkbox" bind:checked={absoluteChroma} />
|
|
160
|
-
<span>Absolute Chroma</span>
|
|
161
|
-
</label>
|
|
162
|
-
<div class="info" bind:this={infoWrap}>
|
|
163
|
-
<button
|
|
164
|
-
type="button"
|
|
165
|
-
class="info-btn"
|
|
166
|
-
aria-expanded={infoOpen}
|
|
167
|
-
aria-label="About Absolute Chroma"
|
|
168
|
-
title="About Absolute Chroma"
|
|
169
|
-
onclick={toggleInfo}
|
|
170
|
-
><i class="fas fa-circle-info" aria-hidden="true"></i></button>
|
|
171
|
-
{#if infoOpen}
|
|
172
|
-
<div class="info-box" role="region" aria-label="Absolute Chroma">
|
|
173
|
-
<strong class="info-title">Absolute Chroma</strong>
|
|
174
|
-
<p>When on, rotating a color keeps its exact chroma, its colorfulness. The reachable chroma changes from hue to hue, so the handle moves in and out as it orbits.</p>
|
|
175
|
-
<p>When off, rotating keeps the color's relative saturation, its fraction of the available gamut. The handle stays a constant distance from the center, and colorfulness shifts a little across hues to stay in gamut.</p>
|
|
176
|
-
</div>
|
|
177
|
-
{/if}
|
|
178
|
-
</div>
|
|
179
|
-
</div>
|
|
180
|
-
</div>
|
|
181
|
-
|
|
182
|
-
<div class="group">
|
|
183
|
-
<span class="eyebrow">Swatches</span>
|
|
184
|
-
{#snippet swatchRow(row: typeof swatches)}
|
|
185
|
-
{@const selIdx = row.findIndex((sw) => sw.label === selected)}
|
|
186
|
-
<div class="swatch-row">
|
|
187
|
-
{#each row as sw, i (sw.label)}
|
|
156
|
+
<div class="wheel-hero">
|
|
157
|
+
<div class="wheel-row">
|
|
158
|
+
<div class="mode-rail" role="group" aria-label="Color harmony mode">
|
|
159
|
+
{#each HARMONY_MODE_BUTTONS as m (m.mode)}
|
|
188
160
|
<button
|
|
189
161
|
type="button"
|
|
190
|
-
class="
|
|
191
|
-
class:active={
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
onclick={() => select(sw.label)}
|
|
198
|
-
>
|
|
199
|
-
<span class="chip">
|
|
200
|
-
{#if sw.onWheel}<span class="wheel-dot" title="On the wheel" aria-hidden="true"></span>{/if}
|
|
201
|
-
</span>
|
|
202
|
-
<span class="swatch-label">{sw.label}</span>
|
|
203
|
-
</button>
|
|
162
|
+
class="mode-btn"
|
|
163
|
+
class:active={activeMode === m.mode}
|
|
164
|
+
title={m.label}
|
|
165
|
+
aria-label={m.label}
|
|
166
|
+
aria-pressed={activeMode === m.mode}
|
|
167
|
+
onclick={() => applyMode(m.mode)}
|
|
168
|
+
>{@html m.svg}</button>
|
|
204
169
|
{/each}
|
|
205
170
|
</div>
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
171
|
+
|
|
172
|
+
<div class="wheel-slot">
|
|
173
|
+
<ColorWheel
|
|
174
|
+
{selected}
|
|
175
|
+
{absoluteChroma}
|
|
176
|
+
{activeMode}
|
|
177
|
+
onSelect={select}
|
|
178
|
+
discLightness={selectedOklch.l}
|
|
179
|
+
onCustomize={() => (activeMode = 'custom')}
|
|
180
|
+
/>
|
|
181
|
+
</div>
|
|
210
182
|
</div>
|
|
211
|
-
</div>
|
|
212
183
|
|
|
213
|
-
<div class="group">
|
|
214
|
-
<span class="eyebrow">{selectedSpec.displayLabel ?? selected}</span>
|
|
215
184
|
<ColorEditPanel
|
|
216
185
|
title={selectedSpec.displayLabel ?? selected}
|
|
217
186
|
hideActions
|
|
187
|
+
hidePreview
|
|
218
188
|
hue={selectedOklch.h}
|
|
219
189
|
chroma={selectedOklch.c}
|
|
220
190
|
lightness={selectedOklch.l * 100}
|
|
221
|
-
onHueChromaChange={
|
|
191
|
+
onHueChromaChange={editBase}
|
|
222
192
|
onSliderStart={() => beginSliderGesture(`colors: ${selected} base`)}
|
|
223
|
-
|
|
193
|
+
>
|
|
194
|
+
{#snippet actions()}
|
|
195
|
+
<div class="wheel-opts">
|
|
196
|
+
<label class="opt">
|
|
197
|
+
<input type="checkbox" bind:checked={absoluteChroma} />
|
|
198
|
+
<span>Absolute Chroma</span>
|
|
199
|
+
</label>
|
|
200
|
+
<div class="info" bind:this={infoWrap}>
|
|
201
|
+
<button
|
|
202
|
+
type="button"
|
|
203
|
+
class="info-btn"
|
|
204
|
+
aria-expanded={infoOpen}
|
|
205
|
+
aria-label="About Absolute Chroma"
|
|
206
|
+
title="About Absolute Chroma"
|
|
207
|
+
onclick={toggleInfo}
|
|
208
|
+
><i class="fas fa-circle-info" aria-hidden="true"></i></button>
|
|
209
|
+
{#if infoOpen}
|
|
210
|
+
<div class="info-box" role="region" aria-label="Absolute Chroma">
|
|
211
|
+
<strong class="info-title">Absolute Chroma</strong>
|
|
212
|
+
<p>When on, rotating a color keeps its exact chroma, its colorfulness. The reachable chroma changes from hue to hue, so the handle moves in and out as it orbits.</p>
|
|
213
|
+
<p>When off, rotating keeps the color's relative saturation, its fraction of the available gamut. The handle stays a constant distance from the center, and colorfulness shifts a little across hues to stay in gamut.</p>
|
|
214
|
+
</div>
|
|
215
|
+
{/if}
|
|
216
|
+
</div>
|
|
217
|
+
</div>
|
|
218
|
+
{/snippet}
|
|
219
|
+
</ColorEditPanel>
|
|
224
220
|
</div>
|
|
225
221
|
|
|
226
|
-
<div class="
|
|
227
|
-
<
|
|
228
|
-
|
|
229
|
-
|
|
222
|
+
<div class="harmony-cols">
|
|
223
|
+
<div class="group">
|
|
224
|
+
<h3 class="title">Selected color</h3>
|
|
225
|
+
{#snippet swatchRow(row: typeof swatches, acceptsDrop = false)}
|
|
226
|
+
<!-- svelte-ignore a11y_no_static_element_interactions -->
|
|
227
|
+
<div
|
|
228
|
+
class="swatch-row"
|
|
229
|
+
class:drop-target={acceptsDrop && unassignHover}
|
|
230
|
+
ondragover={acceptsDrop ? onSwatchesDragOver : undefined}
|
|
231
|
+
ondragleave={acceptsDrop ? () => (unassignHover = false) : undefined}
|
|
232
|
+
ondrop={acceptsDrop ? onSwatchesDrop : undefined}
|
|
233
|
+
>
|
|
234
|
+
{#each row as sw (sw.label)}
|
|
235
|
+
<button
|
|
236
|
+
type="button"
|
|
237
|
+
class="swatch"
|
|
238
|
+
class:active={selected === sw.label}
|
|
239
|
+
class:draggable={sw.eligible}
|
|
240
|
+
style="--swatch-fill: {sw.hex}"
|
|
241
|
+
draggable={sw.eligible}
|
|
242
|
+
title={sw.eligible ? `${sw.display} · drag onto an axis to assign it` : sw.display}
|
|
243
|
+
aria-label={`Select ${sw.display}`}
|
|
244
|
+
aria-pressed={selected === sw.label}
|
|
245
|
+
ondragstart={(e) => startFamilyDrag(e, sw.label)}
|
|
246
|
+
onclick={() => select(sw.label)}
|
|
247
|
+
>
|
|
248
|
+
<span class="frame">
|
|
249
|
+
{#if sw.eligible}<span class="grip" aria-hidden="true">⋮⋮</span>{/if}
|
|
250
|
+
<span class="bar">
|
|
251
|
+
{#if sw.axis}
|
|
252
|
+
<span class="axis-badge" aria-hidden="true" transition:badgeFade>
|
|
253
|
+
<AxisNumeral index={sw.axis.index} status={sw.axis.status} scrim />
|
|
254
|
+
</span>
|
|
255
|
+
{/if}
|
|
256
|
+
</span>
|
|
257
|
+
</span>
|
|
258
|
+
<span class="swatch-label">{sw.label}</span>
|
|
259
|
+
</button>
|
|
260
|
+
{/each}
|
|
261
|
+
</div>
|
|
262
|
+
{/snippet}
|
|
263
|
+
<div class="swatch-rows">
|
|
264
|
+
{@render swatchRow(coreSwatches, true)}
|
|
265
|
+
{@render swatchRow(functionalSwatches)}
|
|
266
|
+
</div>
|
|
267
|
+
</div>
|
|
230
268
|
|
|
231
|
-
|
|
232
|
-
|
|
269
|
+
<div class="gutter" aria-hidden="true">
|
|
270
|
+
<i class="fas fa-arrows-left-right" title="Drag a color onto an axis to assign it, back to the swatches to unassign"></i>
|
|
271
|
+
</div>
|
|
272
|
+
|
|
273
|
+
<div class="group">
|
|
274
|
+
<div class="group-head">
|
|
275
|
+
<h3 class="title">Harmony axes</h3>
|
|
276
|
+
<div class="info" bind:this={axesInfoWrap}>
|
|
277
|
+
<button
|
|
278
|
+
type="button"
|
|
279
|
+
class="info-btn"
|
|
280
|
+
aria-expanded={axesInfoOpen}
|
|
281
|
+
aria-label="About harmony axes"
|
|
282
|
+
title="About harmony axes"
|
|
283
|
+
onclick={() => (axesInfoOpen = !axesInfoOpen)}
|
|
284
|
+
><i class="fas fa-circle-info" aria-hidden="true"></i></button>
|
|
285
|
+
{#if axesInfoOpen}
|
|
286
|
+
<div class="info-box" role="region" aria-label="Harmony axes">
|
|
287
|
+
<strong class="info-title">Harmony axes</strong>
|
|
288
|
+
<p>Each axis owns a hue. Assign a color from the row menu, or drag one onto the axis. The axis moves to that color's hue and the color follows the axis from then on, so applying a harmony rotates every assigned color at once. To unassign, use the row menu, or drag the color off its axis and drop it anywhere else.</p>
|
|
289
|
+
</div>
|
|
290
|
+
{/if}
|
|
291
|
+
</div>
|
|
292
|
+
</div>
|
|
293
|
+
<HarmonyAxesList {activeMode} {selected} oncustomize={() => (activeMode = 'custom')} />
|
|
294
|
+
</div>
|
|
233
295
|
</div>
|
|
234
296
|
</section>
|
|
235
297
|
</div>
|
|
@@ -237,37 +299,23 @@
|
|
|
237
299
|
<div class="pane">
|
|
238
300
|
<section id="colors-story" class="block">
|
|
239
301
|
<header class="block-head">
|
|
240
|
-
<span class="eyebrow">Proportional preview</span>
|
|
241
302
|
<h2 class="title">Color Story</h2>
|
|
242
303
|
</header>
|
|
243
|
-
<
|
|
244
|
-
<UIPillButton
|
|
245
|
-
icon="fa-universal-access"
|
|
246
|
-
title="Every family: solve its Text lightness curve to the value that clears WCAG AA (4.5:1) against its own surfaces. Contrast-first, so secondary and tertiary can land close together. One undo."
|
|
247
|
-
onclick={deriveAccessibleText}
|
|
248
|
-
>Raise all text to AA</UIPillButton>
|
|
249
|
-
<UIPillButton
|
|
250
|
-
icon="fa-arrow-down-short-wide"
|
|
251
|
-
disabled={!textRec.anyDiffers}
|
|
252
|
-
title={textRec.anyDiffers
|
|
253
|
-
? 'Neutral text: keep primary where it is and step secondary and tertiary down in even lightness drops, floored at WCAG AA (4.5:1). Muted and disabled stay put. One undo.'
|
|
254
|
-
: 'The neutral text steps already match the suggestion shown below.'}
|
|
255
|
-
onclick={() => applySuggestedNeutralText(useBlackWhite)}
|
|
256
|
-
>Even neutral steps</UIPillButton>
|
|
257
|
-
<UIPillButton
|
|
258
|
-
icon="fa-circle-half-stroke"
|
|
259
|
-
ariaPressed={useBlackWhite}
|
|
260
|
-
title={`Start the suggested steps from ${pureExtreme} instead of the softer extreme most designers prefer. Changes the suggestion shown below.`}
|
|
261
|
-
onclick={() => (useBlackWhite = !useBlackWhite)}
|
|
262
|
-
>Anchor at {pureExtreme}</UIPillButton>
|
|
263
|
-
</div>
|
|
264
|
-
<ColorStory full={fullPalettes} rec={textRec} />
|
|
304
|
+
<ColorStory full={fullPalettes} />
|
|
265
305
|
</section>
|
|
266
306
|
|
|
267
|
-
<section id="colors-
|
|
268
|
-
<
|
|
269
|
-
|
|
270
|
-
|
|
307
|
+
<section id="colors-selected" class="block">
|
|
308
|
+
<header class="block-head">
|
|
309
|
+
<h2 class="title">Palette <span class="mode-name">· {selectedSpec.displayLabel ?? selected}</span></h2>
|
|
310
|
+
</header>
|
|
311
|
+
|
|
312
|
+
<div class="group">
|
|
313
|
+
<PaletteStepStrip spec={selectedSpec} />
|
|
314
|
+
</div>
|
|
315
|
+
|
|
316
|
+
<div class="group">
|
|
317
|
+
<ColorReadouts color={selectedOklch} />
|
|
318
|
+
</div>
|
|
271
319
|
</section>
|
|
272
320
|
</div>
|
|
273
321
|
</div>
|
|
@@ -298,11 +346,10 @@
|
|
|
298
346
|
min-width: 0;
|
|
299
347
|
}
|
|
300
348
|
|
|
301
|
-
.
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
color: var(--ui-text-secondary);
|
|
349
|
+
.group-head {
|
|
350
|
+
display: flex;
|
|
351
|
+
align-items: center;
|
|
352
|
+
gap: var(--ui-space-6);
|
|
306
353
|
}
|
|
307
354
|
|
|
308
355
|
.block {
|
|
@@ -312,24 +359,25 @@
|
|
|
312
359
|
min-width: 0;
|
|
313
360
|
}
|
|
314
361
|
|
|
315
|
-
|
|
362
|
+
/* The wheel and its lightness bar are one instrument, and the hero of the
|
|
363
|
+
view. Bind them tighter than the block rhythm and add the difference back
|
|
364
|
+
below, so the controls that follow stop reading at the wheel's weight. */
|
|
365
|
+
.wheel-hero {
|
|
316
366
|
display: flex;
|
|
317
367
|
flex-direction: column;
|
|
318
|
-
gap: var(--ui-space-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
.eyebrow {
|
|
322
|
-
font-size: var(--ui-font-size-xs);
|
|
323
|
-
font-weight: var(--ui-font-weight-semibold);
|
|
324
|
-
color: var(--ui-text-tertiary);
|
|
325
|
-
text-transform: uppercase;
|
|
326
|
-
letter-spacing: 0.04em;
|
|
368
|
+
gap: var(--ui-space-12);
|
|
369
|
+
margin-bottom: var(--ui-space-12);
|
|
370
|
+
min-width: 0;
|
|
327
371
|
}
|
|
328
372
|
|
|
329
|
-
.
|
|
330
|
-
|
|
373
|
+
.block-head {
|
|
374
|
+
display: flex;
|
|
375
|
+
flex-direction: column;
|
|
376
|
+
gap: var(--ui-space-2);
|
|
331
377
|
}
|
|
332
378
|
|
|
379
|
+
/* Every labeled region on this page carries the same heading, section and
|
|
380
|
+
group alike: the old xs tertiary eyebrows read as captions and got skipped. */
|
|
333
381
|
.title {
|
|
334
382
|
font-size: var(--ui-font-size-2xl);
|
|
335
383
|
font-weight: var(--ui-font-weight-semibold);
|
|
@@ -337,25 +385,87 @@
|
|
|
337
385
|
margin: 0;
|
|
338
386
|
}
|
|
339
387
|
|
|
388
|
+
.title .mode-name {
|
|
389
|
+
font-weight: var(--ui-font-weight-normal);
|
|
390
|
+
color: var(--ui-text-secondary);
|
|
391
|
+
}
|
|
392
|
+
|
|
340
393
|
.group {
|
|
341
394
|
display: flex;
|
|
342
395
|
flex-direction: column;
|
|
343
|
-
gap: var(--ui-space-
|
|
396
|
+
gap: var(--ui-space-12);
|
|
397
|
+
min-width: 0;
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
/* Swatches, drag gutter, axes. The axes column is the narrower of the two:
|
|
401
|
+
its rows are fixed furniture, while the swatches carry the color. */
|
|
402
|
+
.harmony-cols {
|
|
403
|
+
display: grid;
|
|
404
|
+
grid-template-columns: minmax(0, 1fr) auto minmax(0, 0.62fr);
|
|
405
|
+
gap: var(--ui-space-20);
|
|
406
|
+
align-items: start;
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
/* Names the gesture the two columns share, centered on the drag path. */
|
|
410
|
+
.gutter {
|
|
411
|
+
align-self: center;
|
|
412
|
+
display: flex;
|
|
413
|
+
color: var(--ui-text-muted);
|
|
414
|
+
font-size: var(--ui-font-size-md);
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
/* Rail anchored to the pane's left edge; the disc centers in whatever is left. */
|
|
418
|
+
.wheel-row {
|
|
419
|
+
display: flex;
|
|
420
|
+
align-items: center;
|
|
421
|
+
gap: var(--ui-space-20);
|
|
422
|
+
min-width: 0;
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
.wheel-slot {
|
|
426
|
+
flex: 1 1 auto;
|
|
344
427
|
min-width: 0;
|
|
345
428
|
}
|
|
346
429
|
|
|
347
|
-
.mode-
|
|
430
|
+
.mode-rail {
|
|
431
|
+
flex: 0 0 auto;
|
|
348
432
|
display: flex;
|
|
433
|
+
flex-direction: column;
|
|
349
434
|
gap: var(--ui-space-6);
|
|
350
|
-
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
/* The docked panel's iframe viewport is the panel itself, so this collapse
|
|
438
|
+
fires there long before the standalone page ever gets this narrow. */
|
|
439
|
+
@media (max-width: 720px) {
|
|
440
|
+
.harmony-cols {
|
|
441
|
+
grid-template-columns: minmax(0, 1fr);
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
/* Stacked, the drag runs top to bottom. */
|
|
445
|
+
.gutter {
|
|
446
|
+
justify-content: center;
|
|
447
|
+
transform: rotate(90deg);
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
/* Below the wheel's minimum the rail is taller than the disc it flanks. */
|
|
451
|
+
.wheel-row {
|
|
452
|
+
flex-direction: column;
|
|
453
|
+
align-items: stretch;
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
.mode-rail {
|
|
457
|
+
flex-direction: row;
|
|
458
|
+
flex-wrap: wrap;
|
|
459
|
+
justify-content: center;
|
|
460
|
+
}
|
|
351
461
|
}
|
|
352
462
|
|
|
353
463
|
.mode-btn {
|
|
354
464
|
display: flex;
|
|
355
465
|
align-items: center;
|
|
356
466
|
justify-content: center;
|
|
357
|
-
width:
|
|
358
|
-
height:
|
|
467
|
+
width: 3rem;
|
|
468
|
+
height: 3rem;
|
|
359
469
|
padding: 0;
|
|
360
470
|
background: var(--ui-surface-lowest);
|
|
361
471
|
border: 1px solid var(--ui-border-low);
|
|
@@ -387,6 +497,7 @@
|
|
|
387
497
|
display: flex;
|
|
388
498
|
align-items: center;
|
|
389
499
|
gap: var(--ui-space-8);
|
|
500
|
+
margin-left: auto;
|
|
390
501
|
}
|
|
391
502
|
|
|
392
503
|
.opt {
|
|
@@ -460,17 +571,6 @@
|
|
|
460
571
|
color: var(--ui-text-secondary);
|
|
461
572
|
}
|
|
462
573
|
|
|
463
|
-
.harmony-actions {
|
|
464
|
-
display: flex;
|
|
465
|
-
gap: var(--ui-space-8);
|
|
466
|
-
}
|
|
467
|
-
|
|
468
|
-
.story-actions {
|
|
469
|
-
display: flex;
|
|
470
|
-
flex-wrap: wrap;
|
|
471
|
-
gap: var(--ui-space-8);
|
|
472
|
-
}
|
|
473
|
-
|
|
474
574
|
.swatch-rows {
|
|
475
575
|
display: flex;
|
|
476
576
|
flex-direction: column;
|
|
@@ -478,14 +578,23 @@
|
|
|
478
578
|
min-width: 0;
|
|
479
579
|
}
|
|
480
580
|
|
|
481
|
-
/* Dock magnification, mirroring PaletteStepStrip: the flex-grow transition
|
|
482
|
-
animates the selected swatch opening in place. */
|
|
483
581
|
.swatch-row {
|
|
484
582
|
display: flex;
|
|
485
583
|
align-items: flex-end;
|
|
486
584
|
gap: var(--ui-space-8);
|
|
585
|
+
border-radius: var(--ui-radius-md);
|
|
586
|
+
outline: 1px solid transparent;
|
|
587
|
+
outline-offset: var(--ui-space-6);
|
|
588
|
+
transition: outline-color var(--ui-transition-fast);
|
|
487
589
|
}
|
|
488
590
|
|
|
591
|
+
/* Mirrors the axis row's drop ring: this is where a chip goes to unassign. */
|
|
592
|
+
.swatch-row.drop-target {
|
|
593
|
+
outline-color: var(--ui-text-primary);
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
/* Every swatch keeps one width: selection reads in the frame's height alone,
|
|
597
|
+
so nothing in the row shifts sideways as the selection moves. */
|
|
489
598
|
.swatch {
|
|
490
599
|
flex: 1 1 0;
|
|
491
600
|
min-width: 0;
|
|
@@ -497,38 +606,57 @@
|
|
|
497
606
|
background: none;
|
|
498
607
|
border: none;
|
|
499
608
|
cursor: pointer;
|
|
500
|
-
transition: flex-grow var(--ui-transition-fast);
|
|
501
609
|
}
|
|
502
610
|
|
|
503
|
-
.
|
|
611
|
+
/* Same grab idiom as the axes-list chips: these two are one drag surface. */
|
|
612
|
+
.swatch.draggable {
|
|
613
|
+
cursor: grab;
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
.swatch.draggable:active {
|
|
617
|
+
cursor: grabbing;
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
/* The same frame the axes-list chips wear, holding the color instead of a
|
|
621
|
+
name: one object in two states, wide here and compact once it lands. */
|
|
622
|
+
.frame {
|
|
504
623
|
position: relative;
|
|
505
|
-
display:
|
|
506
|
-
|
|
624
|
+
display: flex;
|
|
625
|
+
align-items: center;
|
|
626
|
+
gap: var(--ui-space-6);
|
|
627
|
+
height: 2.25rem;
|
|
507
628
|
/* Compensating margin: height + margin-top is constant every frame of the
|
|
508
629
|
magnification, so the row's layout height never dips mid-transition.
|
|
509
|
-
|
|
630
|
+
Frame grows upward, label pinned. */
|
|
510
631
|
margin-top: 0.5rem;
|
|
511
|
-
|
|
512
|
-
background: var(--
|
|
632
|
+
padding: var(--ui-space-4) var(--ui-space-6);
|
|
633
|
+
background: var(--ui-surface-low);
|
|
513
634
|
border: 1px solid var(--ui-border-low);
|
|
635
|
+
border-radius: var(--ui-radius-md);
|
|
514
636
|
transition: border-color var(--ui-transition-fast), height var(--ui-transition-fast), margin-top var(--ui-transition-fast);
|
|
515
637
|
}
|
|
516
638
|
|
|
517
|
-
.
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
639
|
+
.bar {
|
|
640
|
+
position: relative;
|
|
641
|
+
flex: 1;
|
|
642
|
+
min-width: 0;
|
|
643
|
+
align-self: stretch;
|
|
644
|
+
border-radius: var(--ui-radius-sm);
|
|
645
|
+
background: var(--swatch-fill);
|
|
524
646
|
}
|
|
525
647
|
|
|
526
|
-
.swatch.active .
|
|
648
|
+
.swatch.active .frame {
|
|
649
|
+
height: 2.75rem;
|
|
650
|
+
margin-top: 0;
|
|
527
651
|
border-color: var(--ui-border-higher);
|
|
528
652
|
outline: 2px solid var(--ui-text-primary);
|
|
529
653
|
outline-offset: 1px;
|
|
530
654
|
}
|
|
531
655
|
|
|
656
|
+
.swatch:hover .frame {
|
|
657
|
+
border-color: var(--ui-border-high);
|
|
658
|
+
}
|
|
659
|
+
|
|
532
660
|
.swatch-label {
|
|
533
661
|
font-size: var(--ui-font-size-xs);
|
|
534
662
|
color: var(--ui-text-secondary);
|
|
@@ -542,16 +670,37 @@
|
|
|
542
670
|
color: var(--ui-text-primary);
|
|
543
671
|
}
|
|
544
672
|
|
|
545
|
-
|
|
673
|
+
/* Rides the head of the color bar, opposite the grip: how to send it, and
|
|
674
|
+
where it went. Larger than the row numerals because this is the one place
|
|
675
|
+
the assignment has to read at a glance across six swatches. The badge sits
|
|
676
|
+
on an arbitrary user-chosen fill, so no surface token can keep it legible.
|
|
677
|
+
This scrim is the one literal color the file is allowed. */
|
|
678
|
+
.axis-badge {
|
|
679
|
+
--numeral-size: 1.5rem;
|
|
680
|
+
--numeral-font-size: var(--ui-font-size-sm);
|
|
546
681
|
position: absolute;
|
|
547
|
-
top:
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
682
|
+
top: 50%;
|
|
683
|
+
left: var(--ui-space-4);
|
|
684
|
+
display: flex;
|
|
685
|
+
transform: translateY(-50%);
|
|
551
686
|
border-radius: var(--ui-radius-full);
|
|
552
|
-
background:
|
|
553
|
-
|
|
687
|
+
background: rgba(0, 0, 0, 0.72);
|
|
688
|
+
pointer-events: none;
|
|
689
|
+
}
|
|
690
|
+
|
|
691
|
+
/* Reads on the frame, not the fill, exactly as the axes-list grip does. */
|
|
692
|
+
.grip {
|
|
693
|
+
flex: none;
|
|
694
|
+
color: var(--ui-text-secondary);
|
|
695
|
+
font-size: var(--ui-font-size-sm);
|
|
696
|
+
line-height: 1;
|
|
697
|
+
letter-spacing: -0.1em;
|
|
554
698
|
pointer-events: none;
|
|
699
|
+
user-select: none;
|
|
700
|
+
}
|
|
701
|
+
|
|
702
|
+
.swatch:hover .grip {
|
|
703
|
+
color: var(--ui-text-primary);
|
|
555
704
|
}
|
|
556
705
|
|
|
557
706
|
@media (max-width: 1024px) {
|