@motion-proto/live-tokens 0.45.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 +67 -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 +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/ColorWheel.svelte +48 -29
- package/src/editor/ui/colors/ColorsTab.svelte +361 -172
- 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 -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,27 +1,35 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
2
|
import { tick } from 'svelte';
|
|
3
|
-
import { flip } from 'svelte/animate';
|
|
4
|
-
import { cubicOut } from 'svelte/easing';
|
|
5
3
|
import { oklchToHexClamped } from '../../core/palettes/oklch';
|
|
6
4
|
import { PALETTE_SPECS, type PaletteSpec } from '../../core/palettes/paletteDerivation';
|
|
7
|
-
import {
|
|
5
|
+
import { AXIS_COUNT, activeAxisCount, axisLabel, axisStatuses, type HarmonyMode, HARMONY_ELIGIBLE } from '../../core/palettes/colorHarmony';
|
|
8
6
|
import { editorState } from '../../core/store/editorStore';
|
|
7
|
+
import UIMenuButton from '../UIMenuButton.svelte';
|
|
8
|
+
import UIOptionItem from '../UIOptionItem.svelte';
|
|
9
|
+
import UIOptionList from '../UIOptionList.svelte';
|
|
10
|
+
import { modeLabel } from './harmonyModeIcons';
|
|
9
11
|
import { bindFamilyToAxis, unbindFamily } from './paletteBaseColor';
|
|
12
|
+
import { HARMONY_DRAG_TYPE, startFamilyDrag } from './harmonyDrag';
|
|
10
13
|
|
|
11
14
|
interface Props {
|
|
12
|
-
/** The applied harmony mode:
|
|
13
|
-
*
|
|
15
|
+
/** The applied harmony mode: it deals a distinct position to some axes only,
|
|
16
|
+
* which is what leaves an axis off the wheel or unused (complementary deals two). */
|
|
14
17
|
activeMode: HarmonyMode;
|
|
15
|
-
/** Family selected in the swatch grid / wheel — its row
|
|
18
|
+
/** Family selected in the swatch grid / wheel — its row reads as active here. */
|
|
16
19
|
selected: string | null;
|
|
17
|
-
|
|
20
|
+
/** An assignment moves its axis to the color's hue, which leaves an applied
|
|
21
|
+
* mode no longer describing the geometry. Mirrors the wheel's handle drag. */
|
|
22
|
+
oncustomize: () => void;
|
|
18
23
|
}
|
|
19
24
|
|
|
20
|
-
let { activeMode, selected,
|
|
25
|
+
let { activeMode, selected, oncustomize }: Props = $props();
|
|
21
26
|
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
27
|
+
function assign(family: string, index: number) {
|
|
28
|
+
if (bindFamilyToAxis(family, index)) oncustomize();
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// Empty-row swatch shows the axis's own hue, at fixed preview L/C so only the
|
|
32
|
+
// hue reads. An assignment overwrites it with the color's hue.
|
|
25
33
|
const PREVIEW_L = 0.65;
|
|
26
34
|
const PREVIEW_C = 0.12;
|
|
27
35
|
|
|
@@ -30,36 +38,39 @@
|
|
|
30
38
|
let listEl: HTMLElement | undefined = $state();
|
|
31
39
|
|
|
32
40
|
let axes = $derived($editorState.harmonyAxes);
|
|
33
|
-
let
|
|
34
|
-
let
|
|
35
|
-
// Keyboard sequence:
|
|
36
|
-
let axisSeq = $derived(
|
|
37
|
-
let positionCount = $derived(axisSeq.length + 1);
|
|
38
|
-
let lastAxis = $derived(axisSeq[axisSeq.length - 1]);
|
|
41
|
+
let statuses = $derived(axisStatuses(activeMode, axes));
|
|
42
|
+
let modeUsage = $derived(`${modeLabel(activeMode)} uses ${activeAxisCount(activeMode)}`);
|
|
43
|
+
// Keyboard move sequence: every axis that still accepts a family.
|
|
44
|
+
let axisSeq = $derived(statuses.flatMap((s, i) => (s === 'unused' ? [] : [i])));
|
|
39
45
|
|
|
40
46
|
function familyHex(family: string): string {
|
|
41
47
|
const { l, c, h } = $editorState.palettes[family]?.baseColor ?? SPEC_BY_LABEL[family].initialColor;
|
|
42
48
|
return oklchToHexClamped(l, c, h);
|
|
43
49
|
}
|
|
44
50
|
|
|
45
|
-
// Keep focus on the moved family so keyboard
|
|
46
|
-
//
|
|
51
|
+
// Keep focus on the moved family so keyboard moves chain without hunting for
|
|
52
|
+
// the row it landed in.
|
|
47
53
|
async function refocus(family: string) {
|
|
48
54
|
await tick();
|
|
49
55
|
listEl?.querySelector<HTMLElement>(`[data-family="${family}"]`)?.focus();
|
|
50
56
|
}
|
|
51
57
|
|
|
58
|
+
// The row a family leaves is where focus goes when it stops being a chip:
|
|
59
|
+
// both trigger forms answer to this selector, so an emptied row keeps focus.
|
|
60
|
+
async function refocusAxis(i: number) {
|
|
61
|
+
await tick();
|
|
62
|
+
listEl?.querySelector<HTMLElement>(`[data-axis="${i}"] [aria-haspopup="menu"]`)?.focus();
|
|
63
|
+
}
|
|
64
|
+
|
|
52
65
|
function moveFamily(family: string, dir: -1 | 1) {
|
|
53
66
|
const idx = axes.findIndex((a) => a.family === family);
|
|
54
|
-
const
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
if (target === axisSeq.length) unbindFamily(family);
|
|
58
|
-
else bindFamilyToAxis(family, axisSeq[target]);
|
|
67
|
+
const target = axisSeq.indexOf(idx) + dir;
|
|
68
|
+
if (target < 0 || target >= axisSeq.length) return;
|
|
69
|
+
assign(family, axisSeq[target]);
|
|
59
70
|
refocus(family);
|
|
60
71
|
}
|
|
61
72
|
|
|
62
|
-
function onChipKeydown(e: KeyboardEvent, family: string) {
|
|
73
|
+
function onChipKeydown(e: KeyboardEvent, family: string, i: number) {
|
|
63
74
|
if (e.key === 'ArrowUp') {
|
|
64
75
|
e.preventDefault();
|
|
65
76
|
moveFamily(family, -1);
|
|
@@ -69,125 +80,188 @@
|
|
|
69
80
|
} else if (e.key === 'Delete' || e.key === 'Backspace') {
|
|
70
81
|
e.preventDefault();
|
|
71
82
|
unbindFamily(family);
|
|
72
|
-
|
|
83
|
+
refocusAxis(i);
|
|
73
84
|
}
|
|
74
85
|
}
|
|
75
86
|
|
|
76
|
-
|
|
87
|
+
function assignFromMenu(family: string, index: number) {
|
|
88
|
+
const wasEmpty = axes[index].family === null;
|
|
89
|
+
assign(family, index);
|
|
90
|
+
// An empty row's text trigger is replaced by the chip, so focus would die
|
|
91
|
+
// with it; follow the family, as every keyboard move does.
|
|
92
|
+
if (wasEmpty) refocus(family);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function clearFromMenu(index: number) {
|
|
96
|
+
const family = axes[index].family;
|
|
97
|
+
if (family === null) return;
|
|
98
|
+
unbindFamily(family);
|
|
99
|
+
// Same reason: the chevron trigger leaves with the chip.
|
|
100
|
+
refocusAxis(index);
|
|
101
|
+
}
|
|
77
102
|
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
103
|
+
// The row is the control, so a click anywhere in it opens the menu. Routed
|
|
104
|
+
// through the trigger's own click rather than an imperative open, so the
|
|
105
|
+
// button keeps sole ownership of the open state and of aria-expanded.
|
|
106
|
+
function onRowClick(e: MouseEvent) {
|
|
107
|
+
const el = e.target as HTMLElement;
|
|
108
|
+
if (el.closest('[aria-haspopup="menu"]')) return;
|
|
109
|
+
(e.currentTarget as HTMLElement).querySelector<HTMLButtonElement>('[aria-haspopup="menu"]')?.click();
|
|
82
110
|
}
|
|
83
111
|
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
112
|
+
let dragTarget = $state<number | null>(null);
|
|
113
|
+
|
|
114
|
+
function onDragOver(e: DragEvent, target: number) {
|
|
115
|
+
if (!(e.dataTransfer?.types ?? []).includes(HARMONY_DRAG_TYPE)) return;
|
|
116
|
+
if (statuses[target] === 'unused') return;
|
|
87
117
|
e.preventDefault();
|
|
88
118
|
if (e.dataTransfer) e.dataTransfer.dropEffect = 'move';
|
|
89
119
|
dragTarget = target;
|
|
90
120
|
}
|
|
91
121
|
|
|
92
|
-
function onDragLeave(target: number
|
|
122
|
+
function onDragLeave(target: number) {
|
|
93
123
|
if (dragTarget === target) dragTarget = null;
|
|
94
124
|
}
|
|
95
125
|
|
|
96
|
-
function onDrop(e: DragEvent, target: number
|
|
126
|
+
function onDrop(e: DragEvent, target: number) {
|
|
97
127
|
e.preventDefault();
|
|
98
|
-
const family = e.dataTransfer?.getData(
|
|
128
|
+
const family = e.dataTransfer?.getData(HARMONY_DRAG_TYPE);
|
|
99
129
|
dragTarget = null;
|
|
100
130
|
if (!family) return;
|
|
101
|
-
if (
|
|
131
|
+
if (statuses[target] === 'unused') return;
|
|
102
132
|
// Self-drop is a no-op: the setters early-return when nothing changes.
|
|
103
|
-
|
|
104
|
-
else bindFamilyToAxis(family, target);
|
|
133
|
+
assign(family, target);
|
|
105
134
|
}
|
|
106
135
|
|
|
136
|
+
// Window-scoped: a drag can start in the swatch row, whose dragend never
|
|
137
|
+
// reaches this component, and abandoning it there would strand the highlight.
|
|
107
138
|
function onDragEnd() {
|
|
108
139
|
dragTarget = null;
|
|
109
140
|
}
|
|
141
|
+
|
|
142
|
+
// Opening the whole page as a drop zone is what lets a release over dead
|
|
143
|
+
// space mean something. Only our own payload, so file drops are untouched.
|
|
144
|
+
function onWindowDragOver(e: DragEvent) {
|
|
145
|
+
if (!(e.dataTransfer?.types ?? []).includes(HARMONY_DRAG_TYPE)) return;
|
|
146
|
+
e.preventDefault();
|
|
147
|
+
if (e.dataTransfer) e.dataTransfer.dropEffect = 'move';
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/** The drop no axis row and no swatch row claimed: letting a color go over
|
|
151
|
+
* dead space is the quickest way off an axis. A drop target that handled it
|
|
152
|
+
* has already called preventDefault on this same event. Escape cancels a
|
|
153
|
+
* drag without ever firing a drop, so it still leaves the color where it is. */
|
|
154
|
+
function onWindowDrop(e: DragEvent) {
|
|
155
|
+
if (e.defaultPrevented) return;
|
|
156
|
+
e.preventDefault();
|
|
157
|
+
dragTarget = null;
|
|
158
|
+
const family = e.dataTransfer?.getData(HARMONY_DRAG_TYPE);
|
|
159
|
+
if (family) unbindFamily(family);
|
|
160
|
+
}
|
|
110
161
|
</script>
|
|
111
162
|
|
|
163
|
+
<svelte:window ondragend={onDragEnd} ondragover={onWindowDragOver} ondrop={onWindowDrop} />
|
|
164
|
+
|
|
165
|
+
{#snippet axisMenu(i: number, close: () => void)}
|
|
166
|
+
<UIOptionList>
|
|
167
|
+
{#each HARMONY_ELIGIBLE as family (family)}
|
|
168
|
+
{@const from = axes.findIndex((a) => a.family === family)}
|
|
169
|
+
<UIOptionItem
|
|
170
|
+
active={from === i}
|
|
171
|
+
onclick={() => {
|
|
172
|
+
assignFromMenu(family, i);
|
|
173
|
+
close();
|
|
174
|
+
}}
|
|
175
|
+
>
|
|
176
|
+
{#snippet preview()}<span class="menu-swatch" style="--fill: {familyHex(family)}"></span>{/snippet}
|
|
177
|
+
{#snippet label()}{family}{/snippet}
|
|
178
|
+
{#snippet meta()}{#if from !== -1 && from !== i}moves from {axisLabel(from)}{/if}{/snippet}
|
|
179
|
+
</UIOptionItem>
|
|
180
|
+
{/each}
|
|
181
|
+
</UIOptionList>
|
|
182
|
+
<div class="menu-sep" role="separator"></div>
|
|
183
|
+
<UIOptionList>
|
|
184
|
+
<UIOptionItem
|
|
185
|
+
active={axes[i].family === null}
|
|
186
|
+
onclick={() => {
|
|
187
|
+
clearFromMenu(i);
|
|
188
|
+
close();
|
|
189
|
+
}}
|
|
190
|
+
>
|
|
191
|
+
{#snippet label()}Leave empty{/snippet}
|
|
192
|
+
</UIOptionItem>
|
|
193
|
+
</UIOptionList>
|
|
194
|
+
{/snippet}
|
|
195
|
+
|
|
112
196
|
<div class="axes-list" bind:this={listEl}>
|
|
113
197
|
{#each axes as axis, i (i)}
|
|
114
|
-
{@const
|
|
115
|
-
|
|
198
|
+
{@const status = statuses[i]}
|
|
199
|
+
{@const unused = status === 'unused'}
|
|
200
|
+
{@const rowSelected = axis.family !== null && axis.family === selected}
|
|
201
|
+
<!-- The click is a shortcut to the trigger button inside, which stays the
|
|
202
|
+
keyboard and screen-reader path. -->
|
|
203
|
+
<!-- svelte-ignore a11y_no_static_element_interactions, a11y_click_events_have_key_events -->
|
|
116
204
|
<div
|
|
117
205
|
class="axis-row"
|
|
118
|
-
class:
|
|
119
|
-
class:selected={
|
|
206
|
+
class:unused
|
|
207
|
+
class:selected={rowSelected}
|
|
120
208
|
class:drop-target={dragTarget === i}
|
|
121
|
-
|
|
209
|
+
class:off-wheel={status === 'off-wheel'}
|
|
210
|
+
class:openable={!unused}
|
|
211
|
+
data-axis={i}
|
|
212
|
+
aria-disabled={unused || undefined}
|
|
213
|
+
onclick={onRowClick}
|
|
122
214
|
ondragover={(e) => onDragOver(e, i)}
|
|
123
215
|
ondragleave={() => onDragLeave(i)}
|
|
124
216
|
ondrop={(e) => onDrop(e, i)}
|
|
125
217
|
>
|
|
126
|
-
<span class="role"
|
|
218
|
+
<span class="role">{i === 0 ? 'Anchor' : ''}</span>
|
|
127
219
|
{#if axis.family !== null}
|
|
128
220
|
{@const family = axis.family}
|
|
129
221
|
<span class="swatch" style="--fill: {familyHex(family)}"></span>
|
|
130
222
|
<button
|
|
131
223
|
type="button"
|
|
132
224
|
class="chip"
|
|
133
|
-
class:selected={family === selected}
|
|
134
225
|
draggable="true"
|
|
135
226
|
data-family={family}
|
|
136
|
-
aria-label={`${family}, ${
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
ondragend={onDragEnd}
|
|
141
|
-
onkeydown={(e) => onChipKeydown(e, family)}
|
|
142
|
-
onclick={() => onSelect(family)}
|
|
227
|
+
aria-label={`${family}, on ${axisLabel(i)}${i === 0 ? '' : ` of ${AXIS_COUNT}`}. Arrow up or down to move it, Delete to unassign.`}
|
|
228
|
+
title="Drag to another axis, or anywhere off the list to unassign. Arrow keys move it, Delete unassigns."
|
|
229
|
+
ondragstart={(e) => startFamilyDrag(e, family)}
|
|
230
|
+
onkeydown={(e) => onChipKeydown(e, family, i)}
|
|
143
231
|
>
|
|
144
232
|
<span class="grip" aria-hidden="true">⋮⋮</span>
|
|
145
233
|
<span class="chip-swatch" style="--fill: {familyHex(family)}"></span>
|
|
146
234
|
<span class="chip-name">{family}</span>
|
|
147
235
|
</button>
|
|
236
|
+
{#if status === 'off-wheel'}
|
|
237
|
+
<span
|
|
238
|
+
class="warn fas fa-triangle-exclamation"
|
|
239
|
+
role="img"
|
|
240
|
+
aria-label={`Off the wheel. ${modeUsage}.`}
|
|
241
|
+
title={`Off the wheel · ${modeUsage}`}
|
|
242
|
+
></span>
|
|
243
|
+
{/if}
|
|
244
|
+
<UIMenuButton
|
|
245
|
+
header={`Assign to ${axisLabel(i)}`}
|
|
246
|
+
triggerLabel={`Assign to ${axisLabel(i)}`}
|
|
247
|
+
triggerClass="axis-menu-trigger"
|
|
248
|
+
>
|
|
249
|
+
{#snippet trigger()}<i class="fas fa-chevron-down chev" aria-hidden="true"></i>{/snippet}
|
|
250
|
+
{#snippet children({ close })}{@render axisMenu(i, close)}{/snippet}
|
|
251
|
+
</UIMenuButton>
|
|
148
252
|
{:else}
|
|
149
253
|
<span class="swatch preview" style="--fill: {oklchToHexClamped(PREVIEW_L, PREVIEW_C, axis.hue)}"></span>
|
|
150
|
-
|
|
254
|
+
{#if unused}
|
|
255
|
+
<span class="reason">unused · {modeUsage}</span>
|
|
256
|
+
{:else}
|
|
257
|
+
<UIMenuButton header={`Assign to ${axisLabel(i)}`} triggerClass="assign-trigger">
|
|
258
|
+
{#snippet trigger()}<span>Assign a color…</span><i class="fas fa-chevron-down chev" aria-hidden="true"></i>{/snippet}
|
|
259
|
+
{#snippet children({ close })}{@render axisMenu(i, close)}{/snippet}
|
|
260
|
+
</UIMenuButton>
|
|
261
|
+
{/if}
|
|
151
262
|
{/if}
|
|
152
263
|
</div>
|
|
153
264
|
{/each}
|
|
154
|
-
|
|
155
|
-
<!-- svelte-ignore a11y_no_static_element_interactions -->
|
|
156
|
-
<div
|
|
157
|
-
class="unassigned"
|
|
158
|
-
class:drop-target={dragTarget === 'unassigned'}
|
|
159
|
-
ondragover={(e) => onDragOver(e, 'unassigned')}
|
|
160
|
-
ondragleave={() => onDragLeave('unassigned')}
|
|
161
|
-
ondrop={(e) => onDrop(e, 'unassigned')}
|
|
162
|
-
>
|
|
163
|
-
<span class="eyebrow">Unassigned</span>
|
|
164
|
-
<div class="chips">
|
|
165
|
-
{#each unassigned as family (family)}
|
|
166
|
-
<button
|
|
167
|
-
type="button"
|
|
168
|
-
class="chip"
|
|
169
|
-
class:selected={family === selected}
|
|
170
|
-
draggable="true"
|
|
171
|
-
data-family={family}
|
|
172
|
-
aria-label={`${family}, unassigned, position ${positionCount} of ${positionCount}. Arrow up to bind it to ${AXIS_ROLES[lastAxis]}.`}
|
|
173
|
-
aria-pressed={family === selected}
|
|
174
|
-
title={`Drag onto an axis to bind it. Arrow up binds it to ${AXIS_ROLES[lastAxis]}.`}
|
|
175
|
-
ondragstart={(e) => onDragStart(e, family)}
|
|
176
|
-
ondragend={onDragEnd}
|
|
177
|
-
onkeydown={(e) => onChipKeydown(e, family)}
|
|
178
|
-
onclick={() => onSelect(family)}
|
|
179
|
-
animate:flip={{ duration: 200, easing: cubicOut }}
|
|
180
|
-
>
|
|
181
|
-
<span class="grip" aria-hidden="true">⋮⋮</span>
|
|
182
|
-
<span class="chip-swatch" style="--fill: {familyHex(family)}"></span>
|
|
183
|
-
<span class="chip-name">{family}</span>
|
|
184
|
-
</button>
|
|
185
|
-
{/each}
|
|
186
|
-
{#if unassigned.length === 0}
|
|
187
|
-
<span class="empty">Every color is bound to an axis.</span>
|
|
188
|
-
{/if}
|
|
189
|
-
</div>
|
|
190
|
-
</div>
|
|
191
265
|
</div>
|
|
192
266
|
|
|
193
267
|
<style>
|
|
@@ -211,43 +285,66 @@
|
|
|
211
285
|
background var(--ui-transition-fast);
|
|
212
286
|
}
|
|
213
287
|
|
|
214
|
-
.
|
|
215
|
-
|
|
216
|
-
|
|
288
|
+
/* About to receive a drop: a doubled ring. Ring count is what separates it
|
|
289
|
+
from selection, which keeps a single line. Never dashed (dashed means
|
|
290
|
+
unused) and never colored. */
|
|
291
|
+
.axis-row.drop-target {
|
|
292
|
+
border-color: var(--ui-text-primary);
|
|
217
293
|
background: var(--ui-surface-low);
|
|
294
|
+
outline: 1px solid var(--ui-text-primary);
|
|
295
|
+
outline-offset: 1px;
|
|
218
296
|
}
|
|
219
297
|
|
|
220
|
-
/* The
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
298
|
+
/* The whole row opens the menu, so it hovers as one control. */
|
|
299
|
+
.axis-row.openable {
|
|
300
|
+
cursor: pointer;
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
/* Selection and the drop ring already own the border; hover must not undo them. */
|
|
304
|
+
.axis-row.openable:not(.selected):not(.drop-target):hover {
|
|
305
|
+
border-color: var(--ui-border-high);
|
|
225
306
|
}
|
|
226
307
|
|
|
227
|
-
.axis-row.
|
|
228
|
-
.axis-row.
|
|
308
|
+
.axis-row.openable:hover :global(.axis-menu-trigger),
|
|
309
|
+
.axis-row.openable:hover :global(.assign-trigger) {
|
|
229
310
|
color: var(--ui-text-primary);
|
|
230
|
-
font-weight: var(--ui-font-weight-semibold);
|
|
231
311
|
}
|
|
232
312
|
|
|
313
|
+
/* The family selected in the swatch grid / wheel. The row takes one border and
|
|
314
|
+
the chip inside stays plain, so the row lights up once. */
|
|
315
|
+
.axis-row.selected {
|
|
316
|
+
border-color: var(--ui-text-primary);
|
|
317
|
+
background: var(--ui-surface-low);
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
/* Only axis 1 fills this column, but every row reserves it so the swatches,
|
|
321
|
+
chips and pickers start at one x. Sized to fit "Anchor". The axis number
|
|
322
|
+
itself lives only on the left swatches, which is where a color is read. */
|
|
233
323
|
.role {
|
|
234
324
|
flex: none;
|
|
235
|
-
width:
|
|
325
|
+
width: 4rem;
|
|
236
326
|
color: var(--ui-text-secondary);
|
|
237
327
|
}
|
|
238
328
|
|
|
239
|
-
.
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
}
|
|
243
|
-
|
|
244
|
-
/* Disabled slot: the applied geometry deals it no distinct position. The Empty
|
|
245
|
-
label stays visible; shape + dimming carry the state. */
|
|
246
|
-
.axis-row.disabled {
|
|
329
|
+
/* Unused slot: nothing bound and no position dealt. The row states the reason
|
|
330
|
+
in text; shape and dimming only echo it. */
|
|
331
|
+
.axis-row.unused {
|
|
247
332
|
border-style: dashed;
|
|
248
333
|
opacity: 0.45;
|
|
249
334
|
}
|
|
250
335
|
|
|
336
|
+
/* Bound, but the applied mode deals this axis no position. Dimming carries it
|
|
337
|
+
and the triangle names it; never dashed, which is reserved for unused. */
|
|
338
|
+
.axis-row.off-wheel {
|
|
339
|
+
opacity: 0.6;
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
.warn {
|
|
343
|
+
flex: none;
|
|
344
|
+
color: var(--ui-text-secondary);
|
|
345
|
+
font-size: var(--ui-font-size-sm);
|
|
346
|
+
}
|
|
347
|
+
|
|
251
348
|
.swatch {
|
|
252
349
|
flex: none;
|
|
253
350
|
width: 0.375rem;
|
|
@@ -262,20 +359,24 @@
|
|
|
262
359
|
opacity: 0.7;
|
|
263
360
|
}
|
|
264
361
|
|
|
265
|
-
.
|
|
362
|
+
.reason {
|
|
266
363
|
color: var(--ui-text-tertiary);
|
|
267
|
-
font-size: var(--ui-font-size-
|
|
364
|
+
font-size: var(--ui-font-size-sm);
|
|
268
365
|
}
|
|
269
366
|
|
|
270
367
|
.grip {
|
|
271
368
|
flex: none;
|
|
272
|
-
color: var(--ui-text-
|
|
273
|
-
font-size: var(--ui-font-size-
|
|
369
|
+
color: var(--ui-text-secondary);
|
|
370
|
+
font-size: var(--ui-font-size-sm);
|
|
274
371
|
line-height: 1;
|
|
275
372
|
letter-spacing: -0.1em;
|
|
276
373
|
user-select: none;
|
|
277
374
|
}
|
|
278
375
|
|
|
376
|
+
.chip:hover .grip {
|
|
377
|
+
color: var(--ui-text-primary);
|
|
378
|
+
}
|
|
379
|
+
|
|
279
380
|
.chip {
|
|
280
381
|
display: inline-flex;
|
|
281
382
|
align-items: center;
|
|
@@ -295,11 +396,6 @@
|
|
|
295
396
|
border-color: var(--ui-border-high);
|
|
296
397
|
}
|
|
297
398
|
|
|
298
|
-
.chip.selected {
|
|
299
|
-
border-color: var(--ui-text-primary);
|
|
300
|
-
background: var(--ui-surface-high);
|
|
301
|
-
}
|
|
302
|
-
|
|
303
399
|
.chip:focus-visible {
|
|
304
400
|
outline: 2px solid var(--ui-border-higher);
|
|
305
401
|
outline-offset: 2px;
|
|
@@ -318,31 +414,58 @@
|
|
|
318
414
|
border: 1px solid var(--ui-border-low);
|
|
319
415
|
}
|
|
320
416
|
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
border:
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
background var(--ui-transition-fast);
|
|
417
|
+
/* Row furniture, not pills: the triggers match the editor's quiet
|
|
418
|
+
icon-button idiom (see UIInfoPopover's info-btn). */
|
|
419
|
+
.axis-row :global(.axis-menu-trigger) {
|
|
420
|
+
margin-left: auto;
|
|
421
|
+
width: var(--ui-space-24);
|
|
422
|
+
height: var(--ui-space-24);
|
|
423
|
+
border-radius: var(--ui-radius-sm);
|
|
424
|
+
color: var(--ui-text-secondary);
|
|
425
|
+
font-size: var(--ui-font-size-xs);
|
|
426
|
+
transition: color var(--ui-transition-fast);
|
|
332
427
|
}
|
|
333
428
|
|
|
334
|
-
.
|
|
335
|
-
|
|
336
|
-
color: var(--ui-text-
|
|
337
|
-
|
|
338
|
-
|
|
429
|
+
.axis-row :global(.axis-menu-trigger:hover),
|
|
430
|
+
.axis-row :global(.axis-menu-trigger[aria-expanded='true']) {
|
|
431
|
+
color: var(--ui-text-primary);
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
/* Claims the rest of the row so its chevron lands where a filled row's does:
|
|
435
|
+
every row carries the same control, whether or not it holds a color. */
|
|
436
|
+
.axis-row :global(.assign-trigger) {
|
|
437
|
+
flex: 1;
|
|
438
|
+
justify-content: space-between;
|
|
439
|
+
padding: var(--ui-space-4) 0;
|
|
440
|
+
color: var(--ui-text-secondary);
|
|
441
|
+
font-size: var(--ui-font-size-md);
|
|
442
|
+
transition: color var(--ui-transition-fast);
|
|
339
443
|
}
|
|
340
444
|
|
|
341
|
-
.
|
|
445
|
+
.chev {
|
|
342
446
|
display: flex;
|
|
343
|
-
flex-wrap: wrap;
|
|
344
|
-
gap: var(--ui-space-6);
|
|
345
|
-
min-height: 1.75rem;
|
|
346
447
|
align-items: center;
|
|
448
|
+
justify-content: center;
|
|
449
|
+
width: var(--ui-space-24);
|
|
450
|
+
font-size: var(--ui-font-size-xs);
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
.axis-row :global(.assign-trigger:hover),
|
|
454
|
+
.axis-row :global(.assign-trigger[aria-expanded='true']) {
|
|
455
|
+
color: var(--ui-text-primary);
|
|
347
456
|
}
|
|
457
|
+
|
|
458
|
+
.menu-swatch {
|
|
459
|
+
flex: none;
|
|
460
|
+
width: 0.85rem;
|
|
461
|
+
height: 0.85rem;
|
|
462
|
+
border-radius: var(--ui-radius-sm);
|
|
463
|
+
background: var(--fill);
|
|
464
|
+
border: 1px solid var(--ui-border-low);
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
.menu-sep {
|
|
468
|
+
border-top: 1px solid var(--ui-border-low);
|
|
469
|
+
}
|
|
470
|
+
|
|
348
471
|
</style>
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/** Drag payload shared by every surface that can hand a family to an axis: the
|
|
2
|
+
* axes list's own chips and the Selected color swatch row. The drop targets
|
|
3
|
+
* live in HarmonyAxesList and gate on this type alone. */
|
|
4
|
+
export const HARMONY_DRAG_TYPE = 'application/x-harmony-family';
|
|
5
|
+
|
|
6
|
+
export function startFamilyDrag(e: DragEvent, family: string): void {
|
|
7
|
+
if (!e.dataTransfer) return;
|
|
8
|
+
e.dataTransfer.effectAllowed = 'move';
|
|
9
|
+
e.dataTransfer.setData(HARMONY_DRAG_TYPE, family);
|
|
10
|
+
}
|
|
@@ -21,3 +21,8 @@ export const HARMONY_MODE_BUTTONS: { mode: HarmonyMode; label: string; svg: stri
|
|
|
21
21
|
{ mode: 'compound', label: 'Compound', svg: compoundSvg },
|
|
22
22
|
{ mode: 'custom', label: 'Custom', svg: customSvg },
|
|
23
23
|
];
|
|
24
|
+
|
|
25
|
+
/** Display name for copy and aria; the raw mode id must never reach a string. */
|
|
26
|
+
export function modeLabel(mode: HarmonyMode): string {
|
|
27
|
+
return HARMONY_MODE_BUTTONS.find((b) => b.mode === mode)!.label;
|
|
28
|
+
}
|