@motion-proto/live-tokens 0.45.0 → 0.46.1
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 +88 -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 +1 -1
- 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 +238 -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/ColorWheel.svelte +48 -29
- package/src/editor/ui/colors/ColorsTab.svelte +378 -172
- package/src/editor/ui/colors/HarmonyAxesList.svelte +264 -141
- package/src/editor/ui/colors/LightnessBar.svelte +0 -2
- package/src/editor/ui/colors/PaletteStepStrip.svelte +33 -15
- 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 -17
- 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
|
@@ -1,32 +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
5
|
import { editorState, beginSliderGesture } from '../../core/store/editorStore';
|
|
5
|
-
import { applyHarmonyToAxes,
|
|
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
14
|
import {
|
|
15
15
|
setBaseColor,
|
|
16
|
-
setBaseColors,
|
|
17
16
|
setAxisHues,
|
|
17
|
+
unbindFamily,
|
|
18
18
|
palettesWithDefaults,
|
|
19
19
|
} from './paletteBaseColor';
|
|
20
20
|
import { HARMONY_MODE_BUTTONS } from './harmonyModeIcons';
|
|
21
|
-
import {
|
|
21
|
+
import { HARMONY_DRAG_TYPE, startFamilyDrag } from './harmonyDrag';
|
|
22
|
+
import { maxChroma } from './colorWheelMath';
|
|
22
23
|
|
|
23
24
|
let selected = $state('Brand');
|
|
24
25
|
let activeMode = $state<HarmonyMode>('custom');
|
|
25
|
-
let hoverMode = $state<HarmonyMode | null>(null);
|
|
26
26
|
// Local UI only — deliberately NOT stored in PaletteConfig (shape unchanged).
|
|
27
27
|
let absoluteChroma = $state(false);
|
|
28
28
|
let infoOpen = $state(false);
|
|
29
29
|
let infoWrap: HTMLElement | undefined = $state();
|
|
30
|
+
let axesInfoOpen = $state(false);
|
|
31
|
+
let axesInfoWrap: HTMLElement | undefined = $state();
|
|
30
32
|
|
|
31
33
|
function select(label: string) {
|
|
32
34
|
selected = label;
|
|
@@ -47,6 +49,15 @@
|
|
|
47
49
|
return () => document.removeEventListener('click', onDoc);
|
|
48
50
|
});
|
|
49
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
|
+
|
|
50
61
|
function applyMode(mode: HarmonyMode) {
|
|
51
62
|
if (mode === 'custom') {
|
|
52
63
|
activeMode = 'custom';
|
|
@@ -59,23 +70,53 @@
|
|
|
59
70
|
setAxisHues(target.map((a, i) => ({ index: i, hue: a.hue })), `colors: harmony ${mode}`);
|
|
60
71
|
}
|
|
61
72
|
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
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;
|
|
98
|
+
}
|
|
99
|
+
|
|
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);
|
|
65
105
|
}
|
|
66
106
|
|
|
67
107
|
let fullPalettes = $derived(palettesWithDefaults($editorState.palettes));
|
|
68
108
|
|
|
69
109
|
let swatches = $derived.by(() => {
|
|
70
|
-
|
|
71
|
-
const active = modeActiveAxes(activeMode);
|
|
110
|
+
const statuses = axisStatuses(activeMode, $editorState.harmonyAxes);
|
|
72
111
|
return PALETTE_SPECS.map((spec) => {
|
|
73
112
|
const { l, c, h } = $editorState.palettes[spec.label]?.baseColor ?? spec.initialColor;
|
|
113
|
+
const index = $editorState.harmonyAxes.findIndex((a) => a.family === spec.label);
|
|
74
114
|
return {
|
|
75
115
|
label: spec.label,
|
|
76
116
|
display: spec.displayLabel ?? spec.label,
|
|
77
117
|
hex: oklchToHexClamped(l, c, h),
|
|
78
|
-
|
|
118
|
+
axis: index === -1 ? null : { index, status: statuses[index] },
|
|
119
|
+
eligible: ELIGIBLE.has(spec.label),
|
|
79
120
|
};
|
|
80
121
|
});
|
|
81
122
|
});
|
|
@@ -86,137 +127,171 @@
|
|
|
86
127
|
let coreSwatches = $derived(swatches.filter((sw) => !FUNCTIONAL_FAMILIES.has(sw.label)));
|
|
87
128
|
let functionalSwatches = $derived(swatches.filter((sw) => FUNCTIONAL_FAMILIES.has(sw.label)));
|
|
88
129
|
|
|
89
|
-
let shownModeLabel = $derived(
|
|
90
|
-
HARMONY_MODE_BUTTONS.find((b) => b.mode === (hoverMode ?? activeMode))?.label ?? '',
|
|
91
|
-
);
|
|
92
|
-
|
|
93
130
|
let selectedSpec = $derived(PALETTE_SPECS.find((s) => s.label === selected) ?? PALETTE_SPECS[0]);
|
|
94
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
|
+
}
|
|
95
147
|
</script>
|
|
96
148
|
|
|
97
149
|
<div class="colors-tab">
|
|
98
150
|
<div class="pane">
|
|
99
151
|
<section id="colors-wheel" class="block">
|
|
100
152
|
<header class="block-head">
|
|
101
|
-
<span class="eyebrow">Theme</span>
|
|
102
153
|
<h2 class="title">Color Wheel</h2>
|
|
103
154
|
</header>
|
|
104
155
|
|
|
105
|
-
<
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
onSelect={select}
|
|
110
|
-
discLightness={selectedOklch.l}
|
|
111
|
-
onCustomize={() => (activeMode = 'custom')}
|
|
112
|
-
/>
|
|
113
|
-
|
|
114
|
-
<LightnessBar {selected} {absoluteChroma} />
|
|
115
|
-
|
|
116
|
-
<div class="group">
|
|
117
|
-
<span class="eyebrow">Color Harmony <span class="mode-name">· {shownModeLabel}</span></span>
|
|
118
|
-
<div class="mode-row" role="group" aria-label="Color harmony mode">
|
|
119
|
-
{#each HARMONY_MODE_BUTTONS as m (m.mode)}
|
|
120
|
-
<button
|
|
121
|
-
type="button"
|
|
122
|
-
class="mode-btn"
|
|
123
|
-
class:active={activeMode === m.mode}
|
|
124
|
-
title={m.label}
|
|
125
|
-
aria-label={m.label}
|
|
126
|
-
aria-pressed={activeMode === m.mode}
|
|
127
|
-
onclick={() => applyMode(m.mode)}
|
|
128
|
-
onpointerenter={() => (hoverMode = m.mode)}
|
|
129
|
-
onpointerleave={() => (hoverMode = null)}
|
|
130
|
-
onfocus={() => (hoverMode = m.mode)}
|
|
131
|
-
onblur={() => (hoverMode = null)}
|
|
132
|
-
>{@html m.svg}</button>
|
|
133
|
-
{/each}
|
|
134
|
-
</div>
|
|
135
|
-
|
|
136
|
-
<div class="harmony-actions">
|
|
137
|
-
<UIPillButton
|
|
138
|
-
icon="fa-fill-drip"
|
|
139
|
-
title="Re-hue Neutral and Alternate to the anchor color (their own chroma and lightness kept)"
|
|
140
|
-
onclick={tintNeutrals}
|
|
141
|
-
>Tint neutrals from anchor</UIPillButton>
|
|
142
|
-
</div>
|
|
143
|
-
|
|
144
|
-
<div class="wheel-opts">
|
|
145
|
-
<label class="opt">
|
|
146
|
-
<input type="checkbox" bind:checked={absoluteChroma} />
|
|
147
|
-
<span>Absolute Chroma</span>
|
|
148
|
-
</label>
|
|
149
|
-
<div class="info" bind:this={infoWrap}>
|
|
150
|
-
<button
|
|
151
|
-
type="button"
|
|
152
|
-
class="info-btn"
|
|
153
|
-
aria-expanded={infoOpen}
|
|
154
|
-
aria-label="About Absolute Chroma"
|
|
155
|
-
title="About Absolute Chroma"
|
|
156
|
-
onclick={toggleInfo}
|
|
157
|
-
><i class="fas fa-circle-info" aria-hidden="true"></i></button>
|
|
158
|
-
{#if infoOpen}
|
|
159
|
-
<div class="info-box" role="region" aria-label="Absolute Chroma">
|
|
160
|
-
<strong class="info-title">Absolute Chroma</strong>
|
|
161
|
-
<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>
|
|
162
|
-
<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>
|
|
163
|
-
</div>
|
|
164
|
-
{/if}
|
|
165
|
-
</div>
|
|
166
|
-
</div>
|
|
167
|
-
</div>
|
|
168
|
-
|
|
169
|
-
<div class="group">
|
|
170
|
-
<span class="eyebrow">Swatches</span>
|
|
171
|
-
{#snippet swatchRow(row: typeof swatches)}
|
|
172
|
-
{@const selIdx = row.findIndex((sw) => sw.label === selected)}
|
|
173
|
-
<div class="swatch-row">
|
|
174
|
-
{#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)}
|
|
175
160
|
<button
|
|
176
161
|
type="button"
|
|
177
|
-
class="
|
|
178
|
-
class:active={
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
onclick={() => select(sw.label)}
|
|
185
|
-
>
|
|
186
|
-
<span class="chip">
|
|
187
|
-
{#if sw.onWheel}<span class="wheel-dot" title="On the wheel" aria-hidden="true"></span>{/if}
|
|
188
|
-
</span>
|
|
189
|
-
<span class="swatch-label">{sw.label}</span>
|
|
190
|
-
</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>
|
|
191
169
|
{/each}
|
|
192
170
|
</div>
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
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>
|
|
197
182
|
</div>
|
|
198
|
-
</div>
|
|
199
183
|
|
|
200
|
-
<div class="group">
|
|
201
|
-
<span class="eyebrow">{selectedSpec.displayLabel ?? selected}</span>
|
|
202
184
|
<ColorEditPanel
|
|
203
185
|
title={selectedSpec.displayLabel ?? selected}
|
|
204
186
|
hideActions
|
|
187
|
+
hidePreview
|
|
205
188
|
hue={selectedOklch.h}
|
|
206
189
|
chroma={selectedOklch.c}
|
|
207
190
|
lightness={selectedOklch.l * 100}
|
|
208
|
-
onHueChromaChange={
|
|
191
|
+
onHueChromaChange={editBase}
|
|
209
192
|
onSliderStart={() => beginSliderGesture(`colors: ${selected} base`)}
|
|
210
|
-
|
|
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>
|
|
211
220
|
</div>
|
|
212
221
|
|
|
213
|
-
<div class="
|
|
214
|
-
<
|
|
215
|
-
|
|
216
|
-
|
|
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>
|
|
217
268
|
|
|
218
|
-
|
|
219
|
-
|
|
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>
|
|
220
295
|
</div>
|
|
221
296
|
</section>
|
|
222
297
|
</div>
|
|
@@ -224,16 +299,23 @@
|
|
|
224
299
|
<div class="pane">
|
|
225
300
|
<section id="colors-story" class="block">
|
|
226
301
|
<header class="block-head">
|
|
227
|
-
<span class="eyebrow">Proportional preview</span>
|
|
228
302
|
<h2 class="title">Color Story</h2>
|
|
229
303
|
</header>
|
|
230
304
|
<ColorStory full={fullPalettes} />
|
|
231
305
|
</section>
|
|
232
306
|
|
|
233
|
-
<section id="colors-
|
|
234
|
-
<
|
|
235
|
-
|
|
236
|
-
|
|
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>
|
|
237
319
|
</section>
|
|
238
320
|
</div>
|
|
239
321
|
</div>
|
|
@@ -257,18 +339,21 @@
|
|
|
257
339
|
}
|
|
258
340
|
}
|
|
259
341
|
|
|
342
|
+
/* The pane is one column of a two-pane grid, so it goes narrow on a wide
|
|
343
|
+
standalone window just as it does in the docked panel. Every collapse
|
|
344
|
+
below keys on the pane's own width, never the viewport's. */
|
|
260
345
|
.pane {
|
|
346
|
+
container: pane / inline-size;
|
|
261
347
|
display: flex;
|
|
262
348
|
flex-direction: column;
|
|
263
349
|
gap: var(--ui-space-32);
|
|
264
350
|
min-width: 0;
|
|
265
351
|
}
|
|
266
352
|
|
|
267
|
-
.
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
color: var(--ui-text-secondary);
|
|
353
|
+
.group-head {
|
|
354
|
+
display: flex;
|
|
355
|
+
align-items: center;
|
|
356
|
+
gap: var(--ui-space-6);
|
|
272
357
|
}
|
|
273
358
|
|
|
274
359
|
.block {
|
|
@@ -278,24 +363,25 @@
|
|
|
278
363
|
min-width: 0;
|
|
279
364
|
}
|
|
280
365
|
|
|
281
|
-
|
|
366
|
+
/* The wheel and its lightness bar are one instrument, and the hero of the
|
|
367
|
+
view. Bind them tighter than the block rhythm and add the difference back
|
|
368
|
+
below, so the controls that follow stop reading at the wheel's weight. */
|
|
369
|
+
.wheel-hero {
|
|
282
370
|
display: flex;
|
|
283
371
|
flex-direction: column;
|
|
284
|
-
gap: var(--ui-space-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
.eyebrow {
|
|
288
|
-
font-size: var(--ui-font-size-xs);
|
|
289
|
-
font-weight: var(--ui-font-weight-semibold);
|
|
290
|
-
color: var(--ui-text-tertiary);
|
|
291
|
-
text-transform: uppercase;
|
|
292
|
-
letter-spacing: 0.04em;
|
|
372
|
+
gap: var(--ui-space-12);
|
|
373
|
+
margin-bottom: var(--ui-space-12);
|
|
374
|
+
min-width: 0;
|
|
293
375
|
}
|
|
294
376
|
|
|
295
|
-
.
|
|
296
|
-
|
|
377
|
+
.block-head {
|
|
378
|
+
display: flex;
|
|
379
|
+
flex-direction: column;
|
|
380
|
+
gap: var(--ui-space-2);
|
|
297
381
|
}
|
|
298
382
|
|
|
383
|
+
/* Every labeled region on this page carries the same heading, section and
|
|
384
|
+
group alike: the old xs tertiary eyebrows read as captions and got skipped. */
|
|
299
385
|
.title {
|
|
300
386
|
font-size: var(--ui-font-size-2xl);
|
|
301
387
|
font-weight: var(--ui-font-weight-semibold);
|
|
@@ -303,25 +389,88 @@
|
|
|
303
389
|
margin: 0;
|
|
304
390
|
}
|
|
305
391
|
|
|
392
|
+
.title .mode-name {
|
|
393
|
+
font-weight: var(--ui-font-weight-normal);
|
|
394
|
+
color: var(--ui-text-secondary);
|
|
395
|
+
}
|
|
396
|
+
|
|
306
397
|
.group {
|
|
307
398
|
display: flex;
|
|
308
399
|
flex-direction: column;
|
|
309
|
-
gap: var(--ui-space-
|
|
400
|
+
gap: var(--ui-space-12);
|
|
401
|
+
min-width: 0;
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
/* Swatches, drag gutter, axes. The axes column is the narrower of the two:
|
|
405
|
+
its rows are fixed furniture, while the swatches carry the color. */
|
|
406
|
+
.harmony-cols {
|
|
407
|
+
display: grid;
|
|
408
|
+
grid-template-columns: minmax(0, 1fr) auto minmax(0, 0.62fr);
|
|
409
|
+
gap: var(--ui-space-20);
|
|
410
|
+
align-items: start;
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
/* Names the gesture the two columns share, centered on the drag path. */
|
|
414
|
+
.gutter {
|
|
415
|
+
align-self: center;
|
|
416
|
+
display: flex;
|
|
417
|
+
color: var(--ui-text-muted);
|
|
418
|
+
font-size: var(--ui-font-size-md);
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
/* Rail anchored to the pane's left edge; the disc centers in whatever is left. */
|
|
422
|
+
.wheel-row {
|
|
423
|
+
display: flex;
|
|
424
|
+
align-items: center;
|
|
425
|
+
gap: var(--ui-space-20);
|
|
426
|
+
min-width: 0;
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
.wheel-slot {
|
|
430
|
+
flex: 1 1 auto;
|
|
310
431
|
min-width: 0;
|
|
311
432
|
}
|
|
312
433
|
|
|
313
|
-
.mode-
|
|
434
|
+
.mode-rail {
|
|
435
|
+
flex: 0 0 auto;
|
|
314
436
|
display: flex;
|
|
437
|
+
flex-direction: column;
|
|
315
438
|
gap: var(--ui-space-6);
|
|
316
|
-
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
/* Below this the three columns squeeze the swatches into slivers. */
|
|
442
|
+
@container pane (max-width: 44rem) {
|
|
443
|
+
.harmony-cols {
|
|
444
|
+
grid-template-columns: minmax(0, 1fr);
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
/* Stacked, the drag runs top to bottom. */
|
|
448
|
+
.gutter {
|
|
449
|
+
justify-content: center;
|
|
450
|
+
transform: rotate(90deg);
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
/* Below the wheel's minimum the rail is taller than the disc it flanks. */
|
|
455
|
+
@container pane (max-width: 26rem) {
|
|
456
|
+
.wheel-row {
|
|
457
|
+
flex-direction: column;
|
|
458
|
+
align-items: stretch;
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
.mode-rail {
|
|
462
|
+
flex-direction: row;
|
|
463
|
+
flex-wrap: wrap;
|
|
464
|
+
justify-content: center;
|
|
465
|
+
}
|
|
317
466
|
}
|
|
318
467
|
|
|
319
468
|
.mode-btn {
|
|
320
469
|
display: flex;
|
|
321
470
|
align-items: center;
|
|
322
471
|
justify-content: center;
|
|
323
|
-
width:
|
|
324
|
-
height:
|
|
472
|
+
width: 3rem;
|
|
473
|
+
height: 3rem;
|
|
325
474
|
padding: 0;
|
|
326
475
|
background: var(--ui-surface-lowest);
|
|
327
476
|
border: 1px solid var(--ui-border-low);
|
|
@@ -353,6 +502,7 @@
|
|
|
353
502
|
display: flex;
|
|
354
503
|
align-items: center;
|
|
355
504
|
gap: var(--ui-space-8);
|
|
505
|
+
margin-left: auto;
|
|
356
506
|
}
|
|
357
507
|
|
|
358
508
|
.opt {
|
|
@@ -426,26 +576,42 @@
|
|
|
426
576
|
color: var(--ui-text-secondary);
|
|
427
577
|
}
|
|
428
578
|
|
|
429
|
-
.harmony-actions {
|
|
430
|
-
display: flex;
|
|
431
|
-
gap: var(--ui-space-8);
|
|
432
|
-
}
|
|
433
|
-
|
|
434
579
|
.swatch-rows {
|
|
580
|
+
container: swatches / inline-size;
|
|
435
581
|
display: flex;
|
|
436
582
|
flex-direction: column;
|
|
437
583
|
gap: var(--ui-space-8);
|
|
438
584
|
min-width: 0;
|
|
439
585
|
}
|
|
440
586
|
|
|
441
|
-
/* Dock magnification, mirroring PaletteStepStrip: the flex-grow transition
|
|
442
|
-
animates the selected swatch opening in place. */
|
|
443
587
|
.swatch-row {
|
|
444
588
|
display: flex;
|
|
445
589
|
align-items: flex-end;
|
|
446
590
|
gap: var(--ui-space-8);
|
|
591
|
+
border-radius: var(--ui-radius-md);
|
|
592
|
+
outline: 1px solid transparent;
|
|
593
|
+
outline-offset: var(--ui-space-6);
|
|
594
|
+
transition: outline-color var(--ui-transition-fast);
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
/* Mirrors the axis row's drop ring: this is where a chip goes to unassign. */
|
|
598
|
+
.swatch-row.drop-target {
|
|
599
|
+
outline-color: var(--ui-text-primary);
|
|
447
600
|
}
|
|
448
601
|
|
|
602
|
+
/* Too narrow for one swatch per family: wrap into ranks that keep a readable
|
|
603
|
+
swatch width rather than shrinking toward slivers. auto-fill keeps every
|
|
604
|
+
rank on the same tracks, so a short last rank never stretches. */
|
|
605
|
+
@container swatches (max-width: 28rem) {
|
|
606
|
+
.swatch-row {
|
|
607
|
+
display: grid;
|
|
608
|
+
grid-template-columns: repeat(auto-fill, minmax(5.25rem, 1fr));
|
|
609
|
+
gap: var(--ui-space-8);
|
|
610
|
+
}
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
/* Every swatch keeps one width: selection reads in the frame's height alone,
|
|
614
|
+
so nothing in the row shifts sideways as the selection moves. */
|
|
449
615
|
.swatch {
|
|
450
616
|
flex: 1 1 0;
|
|
451
617
|
min-width: 0;
|
|
@@ -457,38 +623,57 @@
|
|
|
457
623
|
background: none;
|
|
458
624
|
border: none;
|
|
459
625
|
cursor: pointer;
|
|
460
|
-
transition: flex-grow var(--ui-transition-fast);
|
|
461
626
|
}
|
|
462
627
|
|
|
463
|
-
.
|
|
628
|
+
/* Same grab idiom as the axes-list chips: these two are one drag surface. */
|
|
629
|
+
.swatch.draggable {
|
|
630
|
+
cursor: grab;
|
|
631
|
+
}
|
|
632
|
+
|
|
633
|
+
.swatch.draggable:active {
|
|
634
|
+
cursor: grabbing;
|
|
635
|
+
}
|
|
636
|
+
|
|
637
|
+
/* The same frame the axes-list chips wear, holding the color instead of a
|
|
638
|
+
name: one object in two states, wide here and compact once it lands. */
|
|
639
|
+
.frame {
|
|
464
640
|
position: relative;
|
|
465
|
-
display:
|
|
466
|
-
|
|
641
|
+
display: flex;
|
|
642
|
+
align-items: center;
|
|
643
|
+
gap: var(--ui-space-6);
|
|
644
|
+
height: 2.25rem;
|
|
467
645
|
/* Compensating margin: height + margin-top is constant every frame of the
|
|
468
646
|
magnification, so the row's layout height never dips mid-transition.
|
|
469
|
-
|
|
647
|
+
Frame grows upward, label pinned. */
|
|
470
648
|
margin-top: 0.5rem;
|
|
471
|
-
|
|
472
|
-
background: var(--
|
|
649
|
+
padding: var(--ui-space-4) var(--ui-space-6);
|
|
650
|
+
background: var(--ui-surface-low);
|
|
473
651
|
border: 1px solid var(--ui-border-low);
|
|
652
|
+
border-radius: var(--ui-radius-md);
|
|
474
653
|
transition: border-color var(--ui-transition-fast), height var(--ui-transition-fast), margin-top var(--ui-transition-fast);
|
|
475
654
|
}
|
|
476
655
|
|
|
477
|
-
.
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
656
|
+
.bar {
|
|
657
|
+
position: relative;
|
|
658
|
+
flex: 1;
|
|
659
|
+
min-width: 0;
|
|
660
|
+
align-self: stretch;
|
|
661
|
+
border-radius: var(--ui-radius-sm);
|
|
662
|
+
background: var(--swatch-fill);
|
|
484
663
|
}
|
|
485
664
|
|
|
486
|
-
.swatch.active .
|
|
665
|
+
.swatch.active .frame {
|
|
666
|
+
height: 2.75rem;
|
|
667
|
+
margin-top: 0;
|
|
487
668
|
border-color: var(--ui-border-higher);
|
|
488
669
|
outline: 2px solid var(--ui-text-primary);
|
|
489
670
|
outline-offset: 1px;
|
|
490
671
|
}
|
|
491
672
|
|
|
673
|
+
.swatch:hover .frame {
|
|
674
|
+
border-color: var(--ui-border-high);
|
|
675
|
+
}
|
|
676
|
+
|
|
492
677
|
.swatch-label {
|
|
493
678
|
font-size: var(--ui-font-size-xs);
|
|
494
679
|
color: var(--ui-text-secondary);
|
|
@@ -502,16 +687,37 @@
|
|
|
502
687
|
color: var(--ui-text-primary);
|
|
503
688
|
}
|
|
504
689
|
|
|
505
|
-
|
|
690
|
+
/* Rides the head of the color bar, opposite the grip: how to send it, and
|
|
691
|
+
where it went. Larger than the row numerals because this is the one place
|
|
692
|
+
the assignment has to read at a glance across six swatches. The badge sits
|
|
693
|
+
on an arbitrary user-chosen fill, so no surface token can keep it legible.
|
|
694
|
+
This scrim is the one literal color the file is allowed. */
|
|
695
|
+
.axis-badge {
|
|
696
|
+
--numeral-size: 1.5rem;
|
|
697
|
+
--numeral-font-size: var(--ui-font-size-sm);
|
|
506
698
|
position: absolute;
|
|
507
|
-
top:
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
699
|
+
top: 50%;
|
|
700
|
+
left: var(--ui-space-4);
|
|
701
|
+
display: flex;
|
|
702
|
+
transform: translateY(-50%);
|
|
511
703
|
border-radius: var(--ui-radius-full);
|
|
512
|
-
background:
|
|
513
|
-
|
|
704
|
+
background: rgba(0, 0, 0, 0.72);
|
|
705
|
+
pointer-events: none;
|
|
706
|
+
}
|
|
707
|
+
|
|
708
|
+
/* Reads on the frame, not the fill, exactly as the axes-list grip does. */
|
|
709
|
+
.grip {
|
|
710
|
+
flex: none;
|
|
711
|
+
color: var(--ui-text-secondary);
|
|
712
|
+
font-size: var(--ui-font-size-sm);
|
|
713
|
+
line-height: 1;
|
|
714
|
+
letter-spacing: -0.1em;
|
|
514
715
|
pointer-events: none;
|
|
716
|
+
user-select: none;
|
|
717
|
+
}
|
|
718
|
+
|
|
719
|
+
.swatch:hover .grip {
|
|
720
|
+
color: var(--ui-text-primary);
|
|
515
721
|
}
|
|
516
722
|
|
|
517
723
|
@media (max-width: 1024px) {
|