@motion-proto/live-tokens 0.96.0 → 0.97.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 +37 -0
- package/bin/set-colors.mjs +1 -0
- package/bin/setup-claude.mjs +2 -2
- package/dist-plugin/setColors/index.cjs +7 -1
- package/dist-plugin/setColors/index.d.cts +9 -4
- package/dist-plugin/setColors/index.d.ts +9 -4
- package/dist-plugin/setColors/index.js +7 -1
- package/dist-plugin/setGeometry/index.d.cts +1 -1
- package/dist-plugin/setGeometry/index.d.ts +1 -1
- package/dist-plugin/setType/index.d.cts +1 -1
- package/dist-plugin/setType/index.d.ts +1 -1
- package/dist-plugin/{themeTypes-B8_Idrp4.d.cts → themeTypes-BaTQUyac.d.cts} +10 -1
- package/dist-plugin/{themeTypes-B8_Idrp4.d.ts → themeTypes-BaTQUyac.d.ts} +10 -1
- package/package.json +4 -2
- package/src/editor/component-editor/scaffolding/VariantGroup.svelte +0 -7
- package/src/editor/core/store/editorPersistence.ts +9 -1
- package/src/editor/core/store/editorRenderer.ts +2 -0
- package/src/editor/core/store/editorStore.ts +25 -0
- package/src/editor/core/store/editorTypes.ts +9 -0
- package/src/editor/core/themes/buildColors.ts +16 -5
- package/src/editor/core/themes/slices/customColors.ts +226 -0
- package/src/editor/core/themes/themeTypes.ts +10 -0
- package/src/editor/docs/content/editing-tokens.md +28 -0
- package/src/editor/docs/content.generated.ts +1 -1
- package/src/editor/pages/EditorShell.svelte +1 -0
- package/src/editor/ui/CustomColorReplaceStep.svelte +118 -0
- package/src/editor/ui/UIPaletteSelector.svelte +338 -15
- package/src/editor/ui/UITokenSelector.svelte +7 -12
- package/src/editor/ui/VariablesTab.svelte +3 -0
- package/src/editor/ui/anchorToTrigger.ts +63 -0
- package/src/editor/ui/sections/CustomColorsSection.svelte +254 -0
- package/src/system/components/ImageLightbox.svelte +8 -1
- package/src/testing-js/page-compliance.contract.js +31 -5
- package/src/testing-js/page-compliance.contract.js.map +1 -1
- package/src/editor/ui/keepInViewport.ts +0 -84
|
@@ -1,13 +1,30 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
2
|
import { untrack } from 'svelte';
|
|
3
|
+
import { get } from 'svelte/store';
|
|
3
4
|
import { slide } from 'svelte/transition';
|
|
4
5
|
import { cubicOut, cubicIn } from 'svelte/easing';
|
|
5
6
|
import { resolveAliasChain } from '../core/palettes/tokenRegistry';
|
|
6
|
-
import {
|
|
7
|
+
import {
|
|
8
|
+
editorState,
|
|
9
|
+
beginScope,
|
|
10
|
+
commitScope,
|
|
11
|
+
cancelScope,
|
|
12
|
+
beginSliderGesture,
|
|
13
|
+
addCustomColor,
|
|
14
|
+
setCustomColor,
|
|
15
|
+
removeCustomColor,
|
|
16
|
+
customColorUsages,
|
|
17
|
+
replaceAndRemoveCustomColor,
|
|
18
|
+
type Scope,
|
|
19
|
+
} from '../core/store/editorStore';
|
|
20
|
+
import { setCssVar, removeCssVar } from '../core/cssVarSync';
|
|
21
|
+
import { cssColorToOklch, hexToOklch, type Oklch } from '../core/palettes/oklch';
|
|
7
22
|
import { formatGradientStops } from '../core/themes/slices/gradients';
|
|
8
23
|
import { formatColorOpacity, parseColorOpacity } from '../core/themes/parsers/colorOpacity';
|
|
9
24
|
import type { GradientToken } from '../core/store/editorTypes';
|
|
10
25
|
import UITokenSelector from './UITokenSelector.svelte';
|
|
26
|
+
import ColorEditPanel from './ColorEditPanel.svelte';
|
|
27
|
+
import CustomColorReplaceStep from './CustomColorReplaceStep.svelte';
|
|
11
28
|
|
|
12
29
|
/** Honor prefers-reduced-motion: `t()` zeroes durations when the OS asks for
|
|
13
30
|
* less motion, so the detail-row enter/exit slides skip outright. Mirrors
|
|
@@ -52,6 +69,9 @@
|
|
|
52
69
|
/** Forwarded to UITokenSelector — when set, writes route through this
|
|
53
70
|
* callback instead of the DOM. See UITokenSelector.onwrite. */
|
|
54
71
|
onwrite?: (value: string | null) => void;
|
|
72
|
+
/** Set on the picker a replace step opens to choose a stand-in for this
|
|
73
|
+
* custom colour. The picker omits the colour and offers no add or edit. */
|
|
74
|
+
replacing?: string | null;
|
|
55
75
|
}
|
|
56
76
|
|
|
57
77
|
let {
|
|
@@ -66,6 +86,7 @@
|
|
|
66
86
|
minOpacity = 0,
|
|
67
87
|
onchange,
|
|
68
88
|
onwrite,
|
|
89
|
+
replacing = null,
|
|
69
90
|
}: Props = $props();
|
|
70
91
|
|
|
71
92
|
const clampOpacity = (n: number) => Math.max(minOpacity, Math.min(100, Math.round(n)));
|
|
@@ -88,6 +109,12 @@
|
|
|
88
109
|
|
|
89
110
|
const familyNames = families.map(f => f.name);
|
|
90
111
|
|
|
112
|
+
/** The Custom row's family name. A custom colour is its palette step `N`, so
|
|
113
|
+
* `getVarName('palette', CUSTOM, N)` yields `--color-custom-N`. */
|
|
114
|
+
const CUSTOM = 'custom';
|
|
115
|
+
const CUSTOM_PREFIX = '--color-custom-';
|
|
116
|
+
const customNumber = (v: string) => v.slice(CUSTOM_PREFIX.length);
|
|
117
|
+
|
|
91
118
|
const paletteSteps = ['100', '200', '300', '400', '500', '600', '700', '800', '850', '900', '950'];
|
|
92
119
|
|
|
93
120
|
const surfaceSteps = [
|
|
@@ -233,6 +260,9 @@
|
|
|
233
260
|
if (!varMatch) return null;
|
|
234
261
|
const varName = varMatch[1];
|
|
235
262
|
|
|
263
|
+
const customM = varName.match(/^--color-custom-(\d+)$/);
|
|
264
|
+
if (customM) return { category: 'palette', family: CUSTOM, step: customM[1] };
|
|
265
|
+
|
|
236
266
|
const paletteM = varName.match(/^--color-([a-z]+)-(\d{3})$/);
|
|
237
267
|
if (paletteM && familyNames.includes(paletteM[1]) && paletteSteps.includes(paletteM[2])) {
|
|
238
268
|
return { category: 'palette', family: paletteM[1], step: paletteM[2] };
|
|
@@ -452,6 +482,7 @@
|
|
|
452
482
|
}
|
|
453
483
|
|
|
454
484
|
function handleClose() {
|
|
485
|
+
if (session) cancelSession();
|
|
455
486
|
selectedFamily = null;
|
|
456
487
|
}
|
|
457
488
|
|
|
@@ -464,6 +495,7 @@
|
|
|
464
495
|
}
|
|
465
496
|
|
|
466
497
|
function backToFamilies() {
|
|
498
|
+
if (session) cancelSession();
|
|
467
499
|
selectedFamily = null;
|
|
468
500
|
}
|
|
469
501
|
|
|
@@ -530,6 +562,130 @@
|
|
|
530
562
|
onchange?.();
|
|
531
563
|
}
|
|
532
564
|
|
|
565
|
+
let customTokens = $derived(
|
|
566
|
+
$editorState.customColors.tokens
|
|
567
|
+
.filter((t) => t.variable !== replacing)
|
|
568
|
+
.sort((a, b) => Number(customNumber(a.variable)) - Number(customNumber(b.variable))),
|
|
569
|
+
);
|
|
570
|
+
let customOutOfFamily = $derived(familyFilter !== null && familyFilter !== CUSTOM);
|
|
571
|
+
|
|
572
|
+
/** An open add, edit, or replace-on-remove of one custom colour. */
|
|
573
|
+
interface CustomSession {
|
|
574
|
+
mode: 'add' | 'edit' | 'remove';
|
|
575
|
+
variable: string;
|
|
576
|
+
draft: Oklch;
|
|
577
|
+
}
|
|
578
|
+
let session = $state<CustomSession | null>(null);
|
|
579
|
+
// Held outside $state: the store matches an open scope by identity, and a
|
|
580
|
+
// proxied handle would never match.
|
|
581
|
+
let sessionScope: Scope | null = null;
|
|
582
|
+
let slotRawBeforeAdd = '';
|
|
583
|
+
|
|
584
|
+
function readSlotRaw(): string {
|
|
585
|
+
return document.documentElement.style.getPropertyValue(variable).trim();
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
function rgbChannelsToOklch(r: number, g: number, b: number): Oklch {
|
|
589
|
+
const hex = (n: number) => Math.max(0, Math.min(255, Math.round(n))).toString(16).padStart(2, '0');
|
|
590
|
+
return hexToOklch(`#${hex(r)}${hex(g)}${hex(b)}`);
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
/** The slot's colour as the browser paints it. A custom property's computed
|
|
594
|
+
* value is its unresolved text, so a probe's `color` does the resolving. */
|
|
595
|
+
function resolvedSlotColor(): Oklch {
|
|
596
|
+
const probe = document.createElement('span');
|
|
597
|
+
probe.style.color = `var(${variable})`;
|
|
598
|
+
document.body.appendChild(probe);
|
|
599
|
+
const css = getComputedStyle(probe).color.trim();
|
|
600
|
+
probe.remove();
|
|
601
|
+
const rgb = css.match(/^rgba?\(\s*([\d.]+)[\s,]+([\d.]+)[\s,]+([\d.]+)/i);
|
|
602
|
+
if (rgb) return rgbChannelsToOklch(+rgb[1], +rgb[2], +rgb[3]);
|
|
603
|
+
const srgb = css.match(/^color\(srgb\s+([\d.]+)\s+([\d.]+)\s+([\d.]+)/i);
|
|
604
|
+
if (srgb) return rgbChannelsToOklch(+srgb[1] * 255, +srgb[2] * 255, +srgb[3] * 255);
|
|
605
|
+
return cssColorToOklch(css) ?? { l: 0.6, c: 0, h: 0 };
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
function openAdd() {
|
|
609
|
+
const seed = resolvedSlotColor();
|
|
610
|
+
slotRawBeforeAdd = readSlotRaw();
|
|
611
|
+
sessionScope = beginScope({ label: 'add custom colour', collapseToOne: true, clipUndoFloor: true });
|
|
612
|
+
const added = addCustomColor(seed);
|
|
613
|
+
chosenNone = false;
|
|
614
|
+
chosenStatic = null;
|
|
615
|
+
chosenGradient = null;
|
|
616
|
+
chosenAngle = null;
|
|
617
|
+
chosenCategory = 'palette';
|
|
618
|
+
chosenFamily = CUSTOM;
|
|
619
|
+
chosenStep = customNumber(added);
|
|
620
|
+
selector?.writeOverride(buildValue(added));
|
|
621
|
+
onchange?.();
|
|
622
|
+
session = { mode: 'add', variable: added, draft: seed };
|
|
623
|
+
}
|
|
624
|
+
|
|
625
|
+
function openEdit(customVar: string) {
|
|
626
|
+
const token = get(editorState).customColors.tokens.find((t) => t.variable === customVar);
|
|
627
|
+
if (!token) return;
|
|
628
|
+
sessionScope = beginScope({ label: `edit custom colour ${customVar}`, collapseToOne: true, clipUndoFloor: true });
|
|
629
|
+
session = { mode: 'edit', variable: customVar, draft: { ...token.color } };
|
|
630
|
+
}
|
|
631
|
+
|
|
632
|
+
function editDraft(h: number, c: number, lPct: number) {
|
|
633
|
+
if (!session) return;
|
|
634
|
+
session.draft = { l: lPct / 100, c, h };
|
|
635
|
+
setCustomColor(session.variable, session.draft);
|
|
636
|
+
}
|
|
637
|
+
|
|
638
|
+
function confirmSession() {
|
|
639
|
+
if (!session) return;
|
|
640
|
+
const added = session.mode === 'add';
|
|
641
|
+
if (sessionScope) commitScope(sessionScope);
|
|
642
|
+
sessionScope = null;
|
|
643
|
+
session = null;
|
|
644
|
+
if (added) {
|
|
645
|
+
selectedFamily = null;
|
|
646
|
+
selector?.close();
|
|
647
|
+
onchange?.();
|
|
648
|
+
}
|
|
649
|
+
}
|
|
650
|
+
|
|
651
|
+
// A cancelled add would leave the slot naming a colour the scope removed, so
|
|
652
|
+
// the scope restores the slot with it. A slot that writes only to the DOM
|
|
653
|
+
// sits outside the store, so its prior value goes back by hand.
|
|
654
|
+
function cancelSession() {
|
|
655
|
+
const added = session?.mode === 'add';
|
|
656
|
+
if (sessionScope) cancelScope(sessionScope);
|
|
657
|
+
sessionScope = null;
|
|
658
|
+
session = null;
|
|
659
|
+
if (added && !component && !onwrite) {
|
|
660
|
+
if (slotRawBeforeAdd) setCssVar(variable, slotRawBeforeAdd);
|
|
661
|
+
else removeCssVar(variable);
|
|
662
|
+
}
|
|
663
|
+
initFromCurrent();
|
|
664
|
+
}
|
|
665
|
+
|
|
666
|
+
function requestRemove(customVar: string) {
|
|
667
|
+
if (customColorUsages(get(editorState), customVar).length === 0) {
|
|
668
|
+
removeCustomColor(customVar);
|
|
669
|
+
initFromCurrent();
|
|
670
|
+
return;
|
|
671
|
+
}
|
|
672
|
+
const token = get(editorState).customColors.tokens.find((t) => t.variable === customVar);
|
|
673
|
+
if (!token) return;
|
|
674
|
+
session = { mode: 'remove', variable: customVar, draft: { ...token.color } };
|
|
675
|
+
}
|
|
676
|
+
|
|
677
|
+
function confirmReplace(replacement: string) {
|
|
678
|
+
if (!session) return;
|
|
679
|
+
const customVar = session.variable;
|
|
680
|
+
session = null;
|
|
681
|
+
replaceAndRemoveCustomColor(customVar, replacement);
|
|
682
|
+
initFromCurrent();
|
|
683
|
+
}
|
|
684
|
+
|
|
685
|
+
function cancelReplace() {
|
|
686
|
+
session = null;
|
|
687
|
+
}
|
|
688
|
+
|
|
533
689
|
// Re-derive trigger state when the bound `variable` changes (e.g. when a
|
|
534
690
|
// VariantGroup tabs view reuses the same selector instance across states).
|
|
535
691
|
// The wrapper UITokenSelector forwards `var-change` only for the currently
|
|
@@ -567,9 +723,11 @@
|
|
|
567
723
|
? getVarName(chosenCategory, chosenFamily, chosenStep).replace(/^--/, '') + (opacity < 100 ? ` (${opacity}%)` : '')
|
|
568
724
|
: ''));
|
|
569
725
|
|
|
570
|
-
let availableTabs = $derived(selectedFamily
|
|
571
|
-
? allCategories.filter(c => c.id
|
|
572
|
-
:
|
|
726
|
+
let availableTabs = $derived(selectedFamily === CUSTOM
|
|
727
|
+
? allCategories.filter(c => c.id === 'palette')
|
|
728
|
+
: selectedFamily
|
|
729
|
+
? allCategories.filter(c => c.id !== 'text' || familiesWithText.includes(selectedFamily!))
|
|
730
|
+
: allCategories);
|
|
573
731
|
|
|
574
732
|
$effect(() => {
|
|
575
733
|
const family = selectedFamily;
|
|
@@ -590,7 +748,7 @@
|
|
|
590
748
|
{disabled}
|
|
591
749
|
{selectionsLocked}
|
|
592
750
|
{onwrite}
|
|
593
|
-
dropdownMinWidth=
|
|
751
|
+
dropdownMinWidth={session ? '22rem' : '14rem'}
|
|
594
752
|
dropdownMaxWidth="calc(100vw - 2rem)"
|
|
595
753
|
hideDefaultHeader={!!selectedFamily}
|
|
596
754
|
onreset={handleReset}
|
|
@@ -632,7 +790,7 @@
|
|
|
632
790
|
{/if}
|
|
633
791
|
</div>
|
|
634
792
|
<div class="family-list">
|
|
635
|
-
{#each families as fam}
|
|
793
|
+
{#each families as fam (fam.name)}
|
|
636
794
|
{@const outOfFamily = familyFilter !== null && fam.name !== familyFilter}
|
|
637
795
|
<button
|
|
638
796
|
class="family-item"
|
|
@@ -652,9 +810,30 @@
|
|
|
652
810
|
{/if}
|
|
653
811
|
</button>
|
|
654
812
|
{/each}
|
|
813
|
+
<button
|
|
814
|
+
class="family-item"
|
|
815
|
+
class:active={!chosenNone && chosenFamily === CUSTOM}
|
|
816
|
+
class:out-of-family={customOutOfFamily}
|
|
817
|
+
disabled={customOutOfFamily}
|
|
818
|
+
onclick={() => selectFamily(CUSTOM)}
|
|
819
|
+
>
|
|
820
|
+
<div class="family-swatches">
|
|
821
|
+
{#each [0, 1, 2] as i (i)}
|
|
822
|
+
{#if customTokens[i]}
|
|
823
|
+
<div class="mini-swatch" style="background: var({customTokens[i].variable});"></div>
|
|
824
|
+
{:else}
|
|
825
|
+
<div class="mini-swatch mini-swatch--empty"></div>
|
|
826
|
+
{/if}
|
|
827
|
+
{/each}
|
|
828
|
+
</div>
|
|
829
|
+
<span class="family-label">Custom</span>
|
|
830
|
+
{#if !customOutOfFamily}
|
|
831
|
+
<i class="fas fa-chevron-right family-arrow"></i>
|
|
832
|
+
{/if}
|
|
833
|
+
</button>
|
|
655
834
|
{#if gradientsAllowed && gradientTokens.length > 0}
|
|
656
835
|
<div class="family-divider">Gradients</div>
|
|
657
|
-
{#each gradientTokens as g}
|
|
836
|
+
{#each gradientTokens as g (g.variable)}
|
|
658
837
|
<button class="family-item" class:active={chosenGradient === g.variable} onclick={() => selectGradient(g.variable, close)}>
|
|
659
838
|
<div class="family-swatches">
|
|
660
839
|
<div class="gradient-swatch" style="background: var({g.variable});"></div>
|
|
@@ -667,11 +846,11 @@
|
|
|
667
846
|
{:else}
|
|
668
847
|
<button class="dropdown-back" onclick={backToFamilies}>
|
|
669
848
|
<i class="fas fa-chevron-left"></i>
|
|
670
|
-
<span>{families.find(f => f.name === selectedFamily)?.label}</span>
|
|
849
|
+
<span>{selectedFamily === CUSTOM ? 'Custom' : families.find(f => f.name === selectedFamily)?.label}</span>
|
|
671
850
|
</button>
|
|
672
851
|
|
|
673
852
|
<div class="tab-bar">
|
|
674
|
-
{#each availableTabs as tab}
|
|
853
|
+
{#each availableTabs as tab (tab.id)}
|
|
675
854
|
<button
|
|
676
855
|
class="tab-btn"
|
|
677
856
|
class:selected={selectedCategory === tab.id}
|
|
@@ -681,9 +860,79 @@
|
|
|
681
860
|
{/each}
|
|
682
861
|
</div>
|
|
683
862
|
|
|
684
|
-
{#if
|
|
863
|
+
{#if selectedFamily === CUSTOM}
|
|
685
864
|
<div class="step-grid">
|
|
686
|
-
{#each
|
|
865
|
+
{#each customTokens as token (token.variable)}
|
|
866
|
+
{@const n = customNumber(token.variable)}
|
|
867
|
+
<div class="custom-cell">
|
|
868
|
+
<button
|
|
869
|
+
class="step-item"
|
|
870
|
+
class:active={chosenCategory === 'palette' && chosenFamily === CUSTOM && chosenStep === n}
|
|
871
|
+
disabled={session !== null}
|
|
872
|
+
onclick={() => selectSwatch('palette', n, close)}
|
|
873
|
+
>
|
|
874
|
+
<div class="step-swatch" style="background: var({token.variable});"></div>
|
|
875
|
+
<span class="step-label">{n}</span>
|
|
876
|
+
</button>
|
|
877
|
+
{#if !replacing}
|
|
878
|
+
<button
|
|
879
|
+
type="button"
|
|
880
|
+
class="custom-edit"
|
|
881
|
+
title="Edit custom colour {n}"
|
|
882
|
+
aria-label="Edit custom colour {n}"
|
|
883
|
+
disabled={session !== null}
|
|
884
|
+
onclick={() => openEdit(token.variable)}
|
|
885
|
+
><i class="fas fa-pen" aria-hidden="true"></i></button>
|
|
886
|
+
<button
|
|
887
|
+
type="button"
|
|
888
|
+
class="custom-remove"
|
|
889
|
+
title="Remove custom colour {n}"
|
|
890
|
+
aria-label="Remove custom colour {n}"
|
|
891
|
+
disabled={session !== null}
|
|
892
|
+
onclick={() => requestRemove(token.variable)}
|
|
893
|
+
><i class="fas fa-trash" aria-hidden="true"></i></button>
|
|
894
|
+
{/if}
|
|
895
|
+
</div>
|
|
896
|
+
{/each}
|
|
897
|
+
{#if !replacing}
|
|
898
|
+
<button
|
|
899
|
+
class="step-item"
|
|
900
|
+
title="Add a custom colour"
|
|
901
|
+
aria-label="Add a custom colour"
|
|
902
|
+
disabled={session !== null}
|
|
903
|
+
onclick={openAdd}
|
|
904
|
+
>
|
|
905
|
+
<div class="step-swatch step-swatch--add"><i class="fas fa-plus" aria-hidden="true"></i></div>
|
|
906
|
+
<span class="step-label">Add</span>
|
|
907
|
+
</button>
|
|
908
|
+
{/if}
|
|
909
|
+
</div>
|
|
910
|
+
{#if session}
|
|
911
|
+
<div class="custom-panel">
|
|
912
|
+
{#if session.mode === 'remove'}
|
|
913
|
+
<CustomColorReplaceStep
|
|
914
|
+
variable={session.variable}
|
|
915
|
+
color={session.draft}
|
|
916
|
+
onconfirm={confirmReplace}
|
|
917
|
+
oncancel={cancelReplace}
|
|
918
|
+
/>
|
|
919
|
+
{:else}
|
|
920
|
+
<ColorEditPanel
|
|
921
|
+
title="Custom › {customNumber(session.variable)}"
|
|
922
|
+
hue={session.draft.h}
|
|
923
|
+
chroma={session.draft.c}
|
|
924
|
+
lightness={session.draft.l * 100}
|
|
925
|
+
onHueChromaChange={editDraft}
|
|
926
|
+
onSliderStart={() => beginSliderGesture(`edit ${session?.variable ?? 'custom colour'}`)}
|
|
927
|
+
onConfirm={confirmSession}
|
|
928
|
+
onCancel={cancelSession}
|
|
929
|
+
/>
|
|
930
|
+
{/if}
|
|
931
|
+
</div>
|
|
932
|
+
{/if}
|
|
933
|
+
{:else if selectedCategory === 'palette'}
|
|
934
|
+
<div class="step-grid">
|
|
935
|
+
{#each paletteSteps as step (step)}
|
|
687
936
|
<button
|
|
688
937
|
class="step-item"
|
|
689
938
|
class:active={chosenCategory === 'palette' && chosenFamily === selectedFamily && chosenStep === step}
|
|
@@ -696,7 +945,7 @@
|
|
|
696
945
|
</div>
|
|
697
946
|
{:else if selectedCategory === 'surface'}
|
|
698
947
|
<div class="step-grid">
|
|
699
|
-
{#each surfaceSteps as step}
|
|
948
|
+
{#each surfaceSteps as step (step.key)}
|
|
700
949
|
<button
|
|
701
950
|
class="step-item"
|
|
702
951
|
class:active={chosenCategory === 'surface' && chosenFamily === selectedFamily && chosenStep === step.key}
|
|
@@ -709,7 +958,7 @@
|
|
|
709
958
|
</div>
|
|
710
959
|
{:else if selectedCategory === 'border'}
|
|
711
960
|
<div class="step-grid">
|
|
712
|
-
{#each borderSteps as step}
|
|
961
|
+
{#each borderSteps as step (step.key)}
|
|
713
962
|
<button
|
|
714
963
|
class="step-item"
|
|
715
964
|
class:active={chosenCategory === 'border' && chosenFamily === selectedFamily && chosenStep === step.key}
|
|
@@ -722,7 +971,7 @@
|
|
|
722
971
|
</div>
|
|
723
972
|
{:else if selectedCategory === 'text'}
|
|
724
973
|
<div class="step-grid">
|
|
725
|
-
{#each (selectedFamily === 'neutral' ? neutralTextSteps : textSteps) as step}
|
|
974
|
+
{#each (selectedFamily === 'neutral' ? neutralTextSteps : textSteps) as step (step.key)}
|
|
726
975
|
<button
|
|
727
976
|
class="step-item"
|
|
728
977
|
class:active={chosenCategory === 'text' && chosenFamily === selectedFamily && chosenStep === step.key}
|
|
@@ -756,7 +1005,7 @@
|
|
|
756
1005
|
<span class="detail-label">orientation</span>
|
|
757
1006
|
<div class="orientation-body">
|
|
758
1007
|
<div class="dir-grid">
|
|
759
|
-
{#each directionGrid as d}
|
|
1008
|
+
{#each directionGrid as d (d.angle)}
|
|
760
1009
|
<button
|
|
761
1010
|
type="button"
|
|
762
1011
|
class="dir-btn"
|
|
@@ -1061,6 +1310,80 @@
|
|
|
1061
1310
|
border-radius: 2px;
|
|
1062
1311
|
}
|
|
1063
1312
|
|
|
1313
|
+
.mini-swatch--empty {
|
|
1314
|
+
border: 1px dashed var(--ui-border);
|
|
1315
|
+
}
|
|
1316
|
+
|
|
1317
|
+
.custom-cell {
|
|
1318
|
+
position: relative;
|
|
1319
|
+
display: flex;
|
|
1320
|
+
}
|
|
1321
|
+
|
|
1322
|
+
.custom-cell > .step-item {
|
|
1323
|
+
flex: 1;
|
|
1324
|
+
}
|
|
1325
|
+
|
|
1326
|
+
.custom-edit,
|
|
1327
|
+
.custom-remove {
|
|
1328
|
+
position: absolute;
|
|
1329
|
+
top: 0;
|
|
1330
|
+
display: inline-flex;
|
|
1331
|
+
align-items: center;
|
|
1332
|
+
justify-content: center;
|
|
1333
|
+
width: 1.125rem;
|
|
1334
|
+
height: 1.125rem;
|
|
1335
|
+
padding: 0;
|
|
1336
|
+
background: var(--ui-surface-high);
|
|
1337
|
+
border: 1px solid var(--ui-border);
|
|
1338
|
+
border-radius: var(--ui-radius-sm);
|
|
1339
|
+
color: var(--ui-text-secondary);
|
|
1340
|
+
font-size: 0.5625rem;
|
|
1341
|
+
cursor: pointer;
|
|
1342
|
+
}
|
|
1343
|
+
|
|
1344
|
+
.custom-edit {
|
|
1345
|
+
right: 0;
|
|
1346
|
+
}
|
|
1347
|
+
|
|
1348
|
+
.custom-remove {
|
|
1349
|
+
left: 0;
|
|
1350
|
+
}
|
|
1351
|
+
|
|
1352
|
+
.custom-edit:hover:not(:disabled),
|
|
1353
|
+
.custom-remove:hover:not(:disabled) {
|
|
1354
|
+
color: var(--ui-text-primary);
|
|
1355
|
+
border-color: var(--ui-border-higher);
|
|
1356
|
+
}
|
|
1357
|
+
|
|
1358
|
+
.custom-edit:disabled,
|
|
1359
|
+
.custom-remove:disabled {
|
|
1360
|
+
cursor: default;
|
|
1361
|
+
opacity: 0.5;
|
|
1362
|
+
}
|
|
1363
|
+
|
|
1364
|
+
.step-item:disabled {
|
|
1365
|
+
cursor: default;
|
|
1366
|
+
opacity: 0.5;
|
|
1367
|
+
}
|
|
1368
|
+
|
|
1369
|
+
.step-item:disabled:hover {
|
|
1370
|
+
background: none;
|
|
1371
|
+
border-color: transparent;
|
|
1372
|
+
}
|
|
1373
|
+
|
|
1374
|
+
.step-swatch--add {
|
|
1375
|
+
display: flex;
|
|
1376
|
+
align-items: center;
|
|
1377
|
+
justify-content: center;
|
|
1378
|
+
border: 1px dashed var(--ui-border-higher);
|
|
1379
|
+
color: var(--ui-text-muted);
|
|
1380
|
+
font-size: var(--ui-font-size-xs);
|
|
1381
|
+
}
|
|
1382
|
+
|
|
1383
|
+
.custom-panel {
|
|
1384
|
+
padding: 0 var(--ui-space-8) var(--ui-space-8);
|
|
1385
|
+
}
|
|
1386
|
+
|
|
1064
1387
|
.gradient-swatch {
|
|
1065
1388
|
width: 2.5rem;
|
|
1066
1389
|
height: 0.75rem;
|
|
@@ -22,7 +22,7 @@
|
|
|
22
22
|
} from '../core/store/editorStore';
|
|
23
23
|
import UILinkToggle from './UILinkToggle.svelte';
|
|
24
24
|
import UIRelinkConfirmDialog from './UIRelinkConfirmDialog.svelte';
|
|
25
|
-
import {
|
|
25
|
+
import { anchorToTrigger } from './anchorToTrigger';
|
|
26
26
|
|
|
27
27
|
type DropdownContext = { close: () => void; handleReset: () => void };
|
|
28
28
|
|
|
@@ -100,6 +100,7 @@
|
|
|
100
100
|
|
|
101
101
|
let open = $state(false);
|
|
102
102
|
let container: HTMLElement;
|
|
103
|
+
let triggerEl = $state<HTMLButtonElement | undefined>(undefined);
|
|
103
104
|
let relinkOpen = $state(false);
|
|
104
105
|
let relinkCandidates: { variable: string; alias: string }[] = $state([]);
|
|
105
106
|
|
|
@@ -292,6 +293,7 @@
|
|
|
292
293
|
>
|
|
293
294
|
<div class="ui-ts-trigger-wrap">
|
|
294
295
|
<button
|
|
296
|
+
bind:this={triggerEl}
|
|
295
297
|
class="ui-ts-trigger"
|
|
296
298
|
class:linked={isLinkedDisplay}
|
|
297
299
|
class:unlinked={showLinkToggle && !isLinkedDisplay}
|
|
@@ -329,7 +331,7 @@
|
|
|
329
331
|
<div
|
|
330
332
|
class="ui-ts-dropdown"
|
|
331
333
|
style="min-width: {dropdownMinWidth};{dropdownMaxWidth ? ` max-width: ${dropdownMaxWidth};` : ''}"
|
|
332
|
-
use:
|
|
334
|
+
use:anchorToTrigger={triggerEl!}
|
|
333
335
|
>
|
|
334
336
|
{#if !hideDefaultHeader}
|
|
335
337
|
{#if header}
|
|
@@ -452,8 +454,7 @@
|
|
|
452
454
|
background: var(--ui-surface-high);
|
|
453
455
|
}
|
|
454
456
|
|
|
455
|
-
/* Link-state pop-bar. The wrap already has `position: relative`
|
|
456
|
-
dropdown can anchor). The bar is a `::before` anchored at the wrap's
|
|
457
|
+
/* Link-state pop-bar. The wrap already has `position: relative`. The bar is a `::before` anchored at the wrap's
|
|
457
458
|
left edge — same column position regardless of state, so a stack of
|
|
458
459
|
mixed linked/unlinked rows shares one continuous link-state column.
|
|
459
460
|
Linked rows: bar is teal, full size, sitting flush against the trigger.
|
|
@@ -488,12 +489,6 @@
|
|
|
488
489
|
height: 0.875rem;
|
|
489
490
|
border-radius: 1px;
|
|
490
491
|
}
|
|
491
|
-
/* Keep the dropdown's left edge aligned with the (indented) trigger rather
|
|
492
|
-
than the column anchor, so an open dropdown doesn't bleed left of its
|
|
493
|
-
associated control. */
|
|
494
|
-
.ui-ts-trigger-wrap:has(> .ui-ts-trigger.unlinked) > .ui-ts-dropdown {
|
|
495
|
-
left: 0.5rem;
|
|
496
|
-
}
|
|
497
492
|
@media (prefers-reduced-motion: reduce) {
|
|
498
493
|
.ui-ts-trigger-wrap,
|
|
499
494
|
.ui-ts-trigger-wrap::before { transition: none; }
|
|
@@ -534,8 +529,8 @@
|
|
|
534
529
|
}
|
|
535
530
|
|
|
536
531
|
.ui-ts-dropdown {
|
|
537
|
-
position:
|
|
538
|
-
top:
|
|
532
|
+
position: fixed;
|
|
533
|
+
top: 0;
|
|
539
534
|
left: 0;
|
|
540
535
|
background: var(--ui-surface-higher);
|
|
541
536
|
border: 1px solid var(--ui-border-high);
|
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
import ColumnsSection from './sections/ColumnsSection.svelte';
|
|
9
9
|
import TokenScaleTable from './sections/TokenScaleTable.svelte';
|
|
10
10
|
import WashesSection from './sections/WashesSection.svelte';
|
|
11
|
+
import CustomColorsSection from './sections/CustomColorsSection.svelte';
|
|
11
12
|
import GradientsSection from './sections/GradientsSection.svelte';
|
|
12
13
|
import ShadowsSection from './sections/ShadowsSection.svelte';
|
|
13
14
|
import {
|
|
@@ -139,6 +140,8 @@
|
|
|
139
140
|
<!-- Overlays -->
|
|
140
141
|
<WashesSection {copiedVar} oncopy={copyVariable} />
|
|
141
142
|
|
|
143
|
+
<CustomColorsSection {copiedVar} oncopy={copyVariable} />
|
|
144
|
+
|
|
142
145
|
<!-- Gradients -->
|
|
143
146
|
<GradientsSection {copiedVar} oncopy={copyVariable} />
|
|
144
147
|
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Svelte action: pin a `position: fixed` popover under its trigger so no
|
|
3
|
+
* scrolling or clipping ancestor can cut it off. It aligns to the trigger's
|
|
4
|
+
* left edge, flips above when the space below runs out, and clamps to the
|
|
5
|
+
* viewport. It re-places on scroll, resize, and content reflow.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
const GAP = 4;
|
|
9
|
+
const MARGIN = 8;
|
|
10
|
+
|
|
11
|
+
export function anchorToTrigger(node: HTMLElement, trigger: HTMLElement) {
|
|
12
|
+
let anchor = trigger;
|
|
13
|
+
let pendingFrame: number | null = null;
|
|
14
|
+
|
|
15
|
+
function place(): void {
|
|
16
|
+
// A container-type or transformed ancestor moves the fixed containing
|
|
17
|
+
// block off the viewport origin. Measuring at 0,0 reads that offset.
|
|
18
|
+
node.style.left = '0px';
|
|
19
|
+
node.style.top = '0px';
|
|
20
|
+
const origin = node.getBoundingClientRect();
|
|
21
|
+
const a = anchor.getBoundingClientRect();
|
|
22
|
+
const vw = window.innerWidth;
|
|
23
|
+
const vh = window.innerHeight;
|
|
24
|
+
|
|
25
|
+
const left = Math.max(MARGIN, Math.min(a.left, vw - MARGIN - origin.width));
|
|
26
|
+
let top = a.bottom + GAP;
|
|
27
|
+
if (top + origin.height > vh - MARGIN) {
|
|
28
|
+
const above = a.top - GAP - origin.height;
|
|
29
|
+
top = above >= MARGIN ? above : Math.max(MARGIN, vh - MARGIN - origin.height);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
node.style.left = `${left - origin.left}px`;
|
|
33
|
+
node.style.top = `${top - origin.top}px`;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function schedule(): void {
|
|
37
|
+
if (pendingFrame !== null) return;
|
|
38
|
+
pendingFrame = requestAnimationFrame(() => {
|
|
39
|
+
pendingFrame = null;
|
|
40
|
+
place();
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
place();
|
|
45
|
+
|
|
46
|
+
const ro = new ResizeObserver(schedule);
|
|
47
|
+
ro.observe(node);
|
|
48
|
+
window.addEventListener('resize', schedule);
|
|
49
|
+
window.addEventListener('scroll', schedule, true);
|
|
50
|
+
|
|
51
|
+
return {
|
|
52
|
+
update(next: HTMLElement) {
|
|
53
|
+
anchor = next;
|
|
54
|
+
schedule();
|
|
55
|
+
},
|
|
56
|
+
destroy() {
|
|
57
|
+
if (pendingFrame !== null) cancelAnimationFrame(pendingFrame);
|
|
58
|
+
ro.disconnect();
|
|
59
|
+
window.removeEventListener('resize', schedule);
|
|
60
|
+
window.removeEventListener('scroll', schedule, true);
|
|
61
|
+
},
|
|
62
|
+
};
|
|
63
|
+
}
|