@motion-proto/live-tokens 0.40.0 → 0.41.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 +115 -0
- package/dist-plugin/{chunk-D77VD4Z6.js → chunk-H4TRUINI.js} +102 -13
- package/dist-plugin/index.cjs +288 -67
- package/dist-plugin/index.js +171 -55
- package/dist-plugin/tokensCssMigrations/index.cjs +102 -13
- package/dist-plugin/tokensCssMigrations/index.js +1 -1
- package/package.json +3 -2
- package/src/app/site.css +97 -20
- package/src/editor/core/palettes/colorHarmony.ts +228 -0
- package/src/editor/core/palettes/contrast.ts +128 -0
- package/src/editor/core/palettes/oklch.ts +8 -0
- package/src/editor/core/palettes/paletteDerivation.ts +267 -72
- package/src/editor/core/palettes/recommendText.ts +216 -0
- package/src/editor/core/palettes/solveTextContrast.ts +251 -0
- package/src/editor/core/store/editorCore.ts +2 -1
- package/src/editor/core/store/editorPersistence.ts +18 -1
- package/src/editor/core/store/editorStore.ts +20 -1
- package/src/editor/core/store/editorTypes.ts +4 -0
- package/src/editor/core/store/editorViewStore.ts +7 -3
- package/src/editor/core/themes/migrations/2026-06-05-palette-unification.ts +11 -9
- package/src/editor/core/themes/migrations/2026-07-21-palette-oklch-basis.ts +51 -0
- package/src/editor/core/themes/migrations/2026-07-24-base-anchor-placement.ts +37 -0
- package/src/editor/core/themes/migrations/2026-07-25-background-spot-to-base.ts +40 -0
- package/src/editor/core/themes/themeTypes.ts +23 -3
- package/src/editor/docs/CodeBlock.svelte +1 -1
- package/src/editor/docs/Docs.svelte +30 -13
- package/src/editor/pages/EditorShell.svelte +31 -3
- package/src/editor/ui/ColorEditPanel.svelte +22 -16
- package/src/editor/ui/EditorViewSwitcher.svelte +43 -9
- package/src/editor/ui/FontStackEditor.svelte +0 -1
- package/src/editor/ui/PaletteEditor.svelte +117 -155
- package/src/editor/ui/ProjectFontsSection.svelte +0 -3
- package/src/editor/ui/TextStylesSection.svelte +173 -0
- package/src/editor/ui/UILineHeightSelector.svelte +6 -5
- package/src/editor/ui/UIPaletteSelector.svelte +6 -1
- package/src/editor/ui/UITextTransformSelector.svelte +106 -0
- package/src/editor/ui/VariablesTab.svelte +23 -10
- package/src/editor/ui/colors/ColorReadouts.svelte +123 -0
- package/src/editor/ui/colors/ColorStory.svelte +375 -0
- package/src/editor/ui/colors/ColorWheel.svelte +740 -0
- package/src/editor/ui/colors/ColorsTab.svelte +508 -0
- package/src/editor/ui/colors/HarmonyAxesList.svelte +320 -0
- package/src/editor/ui/colors/LightnessBar.svelte +261 -0
- package/src/editor/ui/colors/PaletteStepStrip.svelte +142 -0
- package/src/editor/ui/colors/colorWheelMath.ts +9 -0
- package/src/editor/ui/colors/harmony-icons/analogous.svg +13 -0
- package/src/editor/ui/colors/harmony-icons/complementary.svg +10 -0
- package/src/editor/ui/colors/harmony-icons/compound.svg +15 -0
- package/src/editor/ui/colors/harmony-icons/custom.svg +10 -0
- package/src/editor/ui/colors/harmony-icons/monochromatic.svg +11 -0
- package/src/editor/ui/colors/harmony-icons/shades.svg +9 -0
- package/src/editor/ui/colors/harmony-icons/split-complementary.svg +13 -0
- package/src/editor/ui/colors/harmony-icons/square.svg +13 -0
- package/src/editor/ui/colors/harmony-icons/tetradic.svg +15 -0
- package/src/editor/ui/colors/harmony-icons/triadic.svg +13 -0
- package/src/editor/ui/colors/harmonyModeIcons.ts +23 -0
- package/src/editor/ui/colors/paletteBaseColor.ts +214 -0
- package/src/editor/ui/index.ts +0 -1
- package/src/editor/ui/palette/OverridesPanel.svelte +6 -3
- package/src/editor/ui/palette/PaletteBase.svelte +16 -8
- package/src/editor/ui/palette/dockMagnify.ts +22 -0
- package/src/editor/ui/palette/paletteEditorState.ts +5 -4
- package/src/editor/ui/palette/paletteMath.ts +93 -181
- package/src/editor/ui/sections/textStyles.ts +82 -0
- package/src/editor/ui/sections/tokenScales.ts +3 -3
- package/src/live-tokens/data/themes/default.json +3 -3
- package/src/live-tokens/data/tokens.generated.css +96 -95
- package/src/system/components/Badge.svelte +11 -11
- package/src/system/components/Button.svelte +7 -7
- package/src/system/components/Callout.svelte +8 -8
- package/src/system/components/Card.svelte +2 -2
- package/src/system/components/CodeSnippet.svelte +1 -1
- package/src/system/components/CollapsibleSection.svelte +7 -7
- package/src/system/components/CornerBadge.svelte +1 -1
- package/src/system/components/Dialog.svelte +2 -2
- package/src/system/components/FloatingTokenTags.css +6 -5
- package/src/system/components/Input.svelte +6 -6
- package/src/system/components/MenuSelect.svelte +4 -4
- package/src/system/components/Notification.svelte +8 -8
- package/src/system/components/ProgressBar.svelte +2 -2
- package/src/system/components/RadioButton.svelte +4 -4
- package/src/system/components/SectionDivider.svelte +6 -6
- package/src/system/components/SegmentedControl.svelte +5 -5
- package/src/system/components/SideNavigation.svelte +12 -12
- package/src/system/components/TabBar.svelte +4 -4
- package/src/system/components/Table.svelte +2 -2
- package/src/system/components/Tooltip.svelte +1 -1
- package/src/system/styles/tokens.css +59 -6
- package/src/editor/ui/TextTab.svelte +0 -203
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import UIFontFamilySelector from './UIFontFamilySelector.svelte';
|
|
3
|
+
import UIFontSizeSelector from './UIFontSizeSelector.svelte';
|
|
4
|
+
import UIFontWeightSelector from './UIFontWeightSelector.svelte';
|
|
5
|
+
import UILetterSpacingSelector from './UILetterSpacingSelector.svelte';
|
|
6
|
+
import UILineHeightSelector from './UILineHeightSelector.svelte';
|
|
7
|
+
import UITextTransformSelector from './UITextTransformSelector.svelte';
|
|
8
|
+
import { TEXT_STYLES } from './sections/textStyles';
|
|
9
|
+
</script>
|
|
10
|
+
|
|
11
|
+
<div class="text-styles">
|
|
12
|
+
{#each TEXT_STYLES as style (style.prefix)}
|
|
13
|
+
<div class="ts-card">
|
|
14
|
+
<div class="ts-id">
|
|
15
|
+
<code class="ts-name">{style.name}</code>
|
|
16
|
+
<span class="ts-el">{style.defaultElement}</span>
|
|
17
|
+
</div>
|
|
18
|
+
|
|
19
|
+
<div
|
|
20
|
+
class="ts-preview"
|
|
21
|
+
style="font-family: var({style.prefix}-font-family); font-size: var({style.prefix}-font-size); font-weight: var({style.prefix}-font-weight); line-height: var({style.prefix}-line-height); letter-spacing: var({style.prefix}-letter-spacing);{style.hasTextTransform ? ` text-transform: var(${style.prefix}-text-transform);` : ''}"
|
|
22
|
+
>{style.preview}</div>
|
|
23
|
+
|
|
24
|
+
<div class="ts-controls">
|
|
25
|
+
<div class="ts-field">
|
|
26
|
+
<span class="ts-label">family</span>
|
|
27
|
+
<UIFontFamilySelector variable={`${style.prefix}-font-family`} />
|
|
28
|
+
</div>
|
|
29
|
+
<div class="ts-field">
|
|
30
|
+
<span class="ts-label">size</span>
|
|
31
|
+
<UIFontSizeSelector variable={`${style.prefix}-font-size`} />
|
|
32
|
+
</div>
|
|
33
|
+
<div class="ts-field">
|
|
34
|
+
<span class="ts-label">weight</span>
|
|
35
|
+
<UIFontWeightSelector variable={`${style.prefix}-font-weight`} />
|
|
36
|
+
</div>
|
|
37
|
+
<div class="ts-field">
|
|
38
|
+
<span class="ts-label">letter spacing</span>
|
|
39
|
+
<UILetterSpacingSelector variable={`${style.prefix}-letter-spacing`} />
|
|
40
|
+
</div>
|
|
41
|
+
<div class="ts-field">
|
|
42
|
+
<span class="ts-label">line height</span>
|
|
43
|
+
<UILineHeightSelector variable={`${style.prefix}-line-height`} />
|
|
44
|
+
</div>
|
|
45
|
+
{#if style.hasTextTransform}
|
|
46
|
+
<div class="ts-field">
|
|
47
|
+
<span class="ts-label">transform</span>
|
|
48
|
+
<UITextTransformSelector variable={`${style.prefix}-text-transform`} />
|
|
49
|
+
</div>
|
|
50
|
+
{/if}
|
|
51
|
+
</div>
|
|
52
|
+
</div>
|
|
53
|
+
{/each}
|
|
54
|
+
</div>
|
|
55
|
+
|
|
56
|
+
<style>
|
|
57
|
+
/* One frame around the stacked list; cards butt together and are separated
|
|
58
|
+
by a faint rule rather than each being its own box. No overflow clipping —
|
|
59
|
+
the pickers' dropdowns must be free to spill past the frame. */
|
|
60
|
+
.text-styles {
|
|
61
|
+
display: flex;
|
|
62
|
+
flex-direction: column;
|
|
63
|
+
border: 1px solid var(--ui-border);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/* Two columns: a fixed identity rail and the content column holding the
|
|
67
|
+
preview (top) then the controls (below). The rail spans both rows so a
|
|
68
|
+
stack of cards reads as one continuous, scannable left column. */
|
|
69
|
+
.ts-card {
|
|
70
|
+
display: grid;
|
|
71
|
+
grid-template-columns: 8rem minmax(0, 1fr);
|
|
72
|
+
column-gap: var(--ui-space-12);
|
|
73
|
+
row-gap: var(--ui-space-8);
|
|
74
|
+
padding: var(--ui-space-12) var(--ui-space-10);
|
|
75
|
+
border-bottom: 1px solid var(--ui-border-low);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
.ts-card:last-child {
|
|
79
|
+
border-bottom: none;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
.ts-id {
|
|
83
|
+
grid-column: 1;
|
|
84
|
+
grid-row: 1 / span 2;
|
|
85
|
+
display: flex;
|
|
86
|
+
flex-direction: column;
|
|
87
|
+
gap: var(--ui-space-2);
|
|
88
|
+
min-width: 0;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
.ts-name {
|
|
92
|
+
font-family: var(--ui-font-mono);
|
|
93
|
+
font-size: var(--ui-font-size-sm);
|
|
94
|
+
color: var(--ui-text-primary);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
.ts-el {
|
|
98
|
+
font-family: var(--ui-font-mono);
|
|
99
|
+
font-size: var(--ui-font-size-xs);
|
|
100
|
+
color: var(--ui-text-muted);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/* Fixed-height preview well with the sample pinned to the top, so its
|
|
104
|
+
cap-line meets the identity label instead of floating mid-row. Height
|
|
105
|
+
clears the largest style (heading-xl) so every well is uniform and the
|
|
106
|
+
rows keep a steady rhythm regardless of sample size. Preview renders
|
|
107
|
+
through the bundle's own vars (letterforms, not ink); ink stays the
|
|
108
|
+
editor's text colour since colour is outside the style bundle. */
|
|
109
|
+
.ts-preview {
|
|
110
|
+
grid-column: 2;
|
|
111
|
+
grid-row: 1;
|
|
112
|
+
align-self: start;
|
|
113
|
+
min-height: 3rem;
|
|
114
|
+
color: var(--ui-text-primary);
|
|
115
|
+
max-width: 100%;
|
|
116
|
+
min-width: 0;
|
|
117
|
+
text-align: left;
|
|
118
|
+
overflow: hidden;
|
|
119
|
+
text-overflow: ellipsis;
|
|
120
|
+
white-space: nowrap;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/* Fixed-width tracks packed to the left rather than stretching across the
|
|
124
|
+
full-bleed section, so the axis pickers stay a tight, scannable cluster.
|
|
125
|
+
auto-fill wraps them when the panel is too narrow for one row. */
|
|
126
|
+
.ts-controls {
|
|
127
|
+
grid-column: 2;
|
|
128
|
+
grid-row: 2;
|
|
129
|
+
display: grid;
|
|
130
|
+
grid-template-columns: repeat(auto-fill, minmax(6.5rem, 7.5rem));
|
|
131
|
+
justify-content: start;
|
|
132
|
+
gap: var(--ui-space-12);
|
|
133
|
+
align-items: start;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
.ts-field {
|
|
137
|
+
display: flex;
|
|
138
|
+
flex-direction: column;
|
|
139
|
+
gap: var(--ui-space-4);
|
|
140
|
+
min-width: 0;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/* The shared picker renders trigger + resolved value side by side (a subgrid
|
|
144
|
+
spanning two parent tracks). Here we want them stacked — label, control,
|
|
145
|
+
then the resolved value beneath — so flip its own layout to a column and
|
|
146
|
+
left-align the value the component otherwise centres. */
|
|
147
|
+
.ts-field :global(.ui-token-selector) {
|
|
148
|
+
display: flex;
|
|
149
|
+
flex-direction: column;
|
|
150
|
+
align-items: flex-start;
|
|
151
|
+
gap: var(--ui-space-4);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
.ts-field :global(.ui-ts-trigger-wrap) {
|
|
155
|
+
width: 100%;
|
|
156
|
+
min-width: 0;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
.ts-field :global(.ui-ts-meta-text) {
|
|
160
|
+
align-self: flex-start;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/* Quiet eyebrow-scale labels so the eye lands on the control and its value,
|
|
164
|
+
not the field name. */
|
|
165
|
+
.ts-label {
|
|
166
|
+
font-size: var(--ui-font-size-xs);
|
|
167
|
+
font-weight: var(--ui-font-weight-medium);
|
|
168
|
+
letter-spacing: 0.04em;
|
|
169
|
+
text-transform: uppercase;
|
|
170
|
+
color: var(--ui-text-muted);
|
|
171
|
+
white-space: nowrap;
|
|
172
|
+
}
|
|
173
|
+
</style>
|
|
@@ -21,11 +21,12 @@
|
|
|
21
21
|
}: Props = $props();
|
|
22
22
|
|
|
23
23
|
const options = [
|
|
24
|
-
{ key: '
|
|
25
|
-
{ key: '
|
|
26
|
-
{ key: '
|
|
27
|
-
{ key: '
|
|
28
|
-
{ key: '
|
|
24
|
+
{ key: 'none', label: 'None', value: '1' },
|
|
25
|
+
{ key: 'tightest', label: 'Tightest', value: '1.1' },
|
|
26
|
+
{ key: 'tighter', label: 'Tighter', value: '1.25' },
|
|
27
|
+
{ key: 'tight', label: 'Tight', value: '1.35' },
|
|
28
|
+
{ key: 'normal', label: 'Normal', value: '1.5' },
|
|
29
|
+
{ key: 'relaxed', label: 'Relaxed', value: '1.75' },
|
|
29
30
|
] as const;
|
|
30
31
|
</script>
|
|
31
32
|
|
|
@@ -113,6 +113,10 @@
|
|
|
113
113
|
const borderStepKeys = borderSteps.map(s => s.key);
|
|
114
114
|
const textStepKeys = textSteps.map(s => s.key);
|
|
115
115
|
|
|
116
|
+
/** Neutral additionally exposes the derived flip of primary; chromatic
|
|
117
|
+
* families have no inverted step. */
|
|
118
|
+
const neutralTextSteps = [...textSteps, { key: 'inverted', label: 'Inverted' }];
|
|
119
|
+
|
|
116
120
|
const familiesWithText = ['neutral', 'alternate', 'canvas', 'brand', 'accent', 'special', 'success', 'warning', 'info', 'danger'];
|
|
117
121
|
|
|
118
122
|
const allCategories: { id: Category; label: string }[] = [
|
|
@@ -205,6 +209,7 @@
|
|
|
205
209
|
if (varName === '--text-tertiary') return { family: 'neutral', step: 'tertiary' };
|
|
206
210
|
if (varName === '--text-muted') return { family: 'neutral', step: 'muted' };
|
|
207
211
|
if (varName === '--text-disabled') return { family: 'neutral', step: 'disabled' };
|
|
212
|
+
if (varName === '--text-inverted') return { family: 'neutral', step: 'inverted' };
|
|
208
213
|
const m = varName.match(/^--text-([a-z]+)(?:-([a-z]+))?$/);
|
|
209
214
|
if (m) {
|
|
210
215
|
const fam = m[1];
|
|
@@ -702,7 +707,7 @@
|
|
|
702
707
|
</div>
|
|
703
708
|
{:else if selectedTab === 'text'}
|
|
704
709
|
<div class="step-grid">
|
|
705
|
-
{#each textSteps as step}
|
|
710
|
+
{#each (selectedFamily === 'neutral' ? neutralTextSteps : textSteps) as step}
|
|
706
711
|
<button
|
|
707
712
|
class="step-item"
|
|
708
713
|
class:active={chosenCategory === 'text' && chosenFamily === selectedFamily && chosenStep === step.key}
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { onMount, onDestroy } from 'svelte';
|
|
3
|
+
import { CSS_VAR_CHANGE_EVENT } from '../core/cssVarSync';
|
|
4
|
+
import UITokenSelector from './UITokenSelector.svelte';
|
|
5
|
+
import UIOptionList from './UIOptionList.svelte';
|
|
6
|
+
import UIOptionItem from './UIOptionItem.svelte';
|
|
7
|
+
|
|
8
|
+
interface Props {
|
|
9
|
+
variable: string;
|
|
10
|
+
component?: string | undefined;
|
|
11
|
+
canBeLinked?: boolean;
|
|
12
|
+
disabled?: boolean;
|
|
13
|
+
selectionsLocked?: boolean;
|
|
14
|
+
onchange?: () => void;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
let {
|
|
18
|
+
variable,
|
|
19
|
+
component = undefined,
|
|
20
|
+
canBeLinked = false,
|
|
21
|
+
disabled = false,
|
|
22
|
+
selectionsLocked = false,
|
|
23
|
+
onchange,
|
|
24
|
+
}: Props = $props();
|
|
25
|
+
|
|
26
|
+
// text-transform is a literal CSS keyword, not a scale alias — the picker
|
|
27
|
+
// writes the keyword itself through UITokenSelector.writeOverride.
|
|
28
|
+
const options = [
|
|
29
|
+
{ key: 'none', label: 'None', sample: 'Overline' },
|
|
30
|
+
{ key: 'uppercase', label: 'Uppercase', sample: 'Overline' },
|
|
31
|
+
{ key: 'lowercase', label: 'Lowercase', sample: 'Overline' },
|
|
32
|
+
{ key: 'capitalize', label: 'Capitalize', sample: 'overline label' },
|
|
33
|
+
] as const;
|
|
34
|
+
|
|
35
|
+
let selector: UITokenSelector | undefined = $state();
|
|
36
|
+
let current = $state('');
|
|
37
|
+
|
|
38
|
+
function readResolved() {
|
|
39
|
+
current = getComputedStyle(document.documentElement).getPropertyValue(variable).trim();
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function handleVarChange(e: Event) {
|
|
43
|
+
const detail = (e as CustomEvent<{ name: string }>).detail;
|
|
44
|
+
if (detail?.name === variable) readResolved();
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function select(keyword: string, close: () => void) {
|
|
48
|
+
selector?.writeOverride(keyword);
|
|
49
|
+
readResolved();
|
|
50
|
+
close();
|
|
51
|
+
onchange?.();
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function handleReset() {
|
|
55
|
+
readResolved();
|
|
56
|
+
onchange?.();
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
onMount(() => {
|
|
60
|
+
readResolved();
|
|
61
|
+
document.addEventListener(CSS_VAR_CHANGE_EVENT, handleVarChange);
|
|
62
|
+
});
|
|
63
|
+
onDestroy(() => {
|
|
64
|
+
document.removeEventListener(CSS_VAR_CHANGE_EVENT, handleVarChange);
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
let activeKey = $derived(options.find((o) => o.key === current)?.key ?? null);
|
|
68
|
+
let activeLabel = $derived(options.find((o) => o.key === activeKey)?.label ?? current);
|
|
69
|
+
</script>
|
|
70
|
+
|
|
71
|
+
<UITokenSelector
|
|
72
|
+
bind:this={selector}
|
|
73
|
+
{variable}
|
|
74
|
+
{component}
|
|
75
|
+
{canBeLinked}
|
|
76
|
+
{disabled}
|
|
77
|
+
{selectionsLocked}
|
|
78
|
+
dropdownMinWidth="12rem"
|
|
79
|
+
onreset={handleReset}
|
|
80
|
+
onvarChange={readResolved}
|
|
81
|
+
>
|
|
82
|
+
{#snippet triggerTitle()}{activeLabel}{/snippet}
|
|
83
|
+
{#snippet triggerMeta()}{current || '—'}{/snippet}
|
|
84
|
+
|
|
85
|
+
{#snippet children({ close })}
|
|
86
|
+
<UIOptionList>
|
|
87
|
+
{#each options as opt (opt.key)}
|
|
88
|
+
<UIOptionItem active={activeKey === opt.key} onclick={() => select(opt.key, close)}>
|
|
89
|
+
{#snippet preview()}
|
|
90
|
+
<span class="tt-sample" style="text-transform: {opt.key};">{opt.sample}</span>
|
|
91
|
+
{/snippet}
|
|
92
|
+
{#snippet label()}{opt.label}{/snippet}
|
|
93
|
+
</UIOptionItem>
|
|
94
|
+
{/each}
|
|
95
|
+
</UIOptionList>
|
|
96
|
+
{/snippet}
|
|
97
|
+
</UITokenSelector>
|
|
98
|
+
|
|
99
|
+
<style>
|
|
100
|
+
.tt-sample {
|
|
101
|
+
display: inline-block;
|
|
102
|
+
min-width: 4.5rem;
|
|
103
|
+
font-size: var(--ui-font-size-sm);
|
|
104
|
+
color: var(--ui-text-primary);
|
|
105
|
+
}
|
|
106
|
+
</style>
|
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
2
|
import { onMount } from 'svelte';
|
|
3
3
|
import PaletteEditor from './PaletteEditor.svelte';
|
|
4
|
+
import { PALETTE_SPECS } from '../core/palettes/paletteDerivation';
|
|
4
5
|
import FontStackEditor from './FontStackEditor.svelte';
|
|
5
6
|
import ProjectFontsSection from './ProjectFontsSection.svelte';
|
|
7
|
+
import TextStylesSection from './TextStylesSection.svelte';
|
|
6
8
|
import ColumnsSection from './sections/ColumnsSection.svelte';
|
|
7
9
|
import TokenScaleTable from './sections/TokenScaleTable.svelte';
|
|
8
10
|
import OverlaysSection from './sections/OverlaysSection.svelte';
|
|
@@ -49,16 +51,16 @@
|
|
|
49
51
|
<h2 class="section-title">Palette Editor</h2>
|
|
50
52
|
<p class="editor-intro">Derived palettes via <code>color-mix(in oklch)</code>. Change a base color to update all derived steps. Click any derived swatch to add a manual override.</p>
|
|
51
53
|
<div class="palette-editors">
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
54
|
+
{#each PALETTE_SPECS as spec (spec.label)}
|
|
55
|
+
<PaletteEditor
|
|
56
|
+
label={spec.label}
|
|
57
|
+
displayLabel={spec.displayLabel ?? null}
|
|
58
|
+
initialColor={spec.initialColor}
|
|
59
|
+
neutral={spec.neutral ?? false}
|
|
60
|
+
cssNamespace={spec.cssNamespace}
|
|
61
|
+
emptySelector={spec.emptySelector ?? false}
|
|
62
|
+
/>
|
|
63
|
+
{/each}
|
|
62
64
|
</div>
|
|
63
65
|
</section>
|
|
64
66
|
|
|
@@ -97,6 +99,11 @@
|
|
|
97
99
|
<FontStackEditor />
|
|
98
100
|
</div>
|
|
99
101
|
|
|
102
|
+
<div class="typography-group text-styles-group">
|
|
103
|
+
<h3 class="group-title">Text Styles</h3>
|
|
104
|
+
<TextStylesSection />
|
|
105
|
+
</div>
|
|
106
|
+
|
|
100
107
|
<div class="typography-group">
|
|
101
108
|
<h3 class="group-title">Font Sizes</h3>
|
|
102
109
|
<TokenScaleTable kind="font-size" vars={FONT_SIZE_VARS} {liveVersion} {copiedVar} oncopy={copyVariable} />
|
|
@@ -248,6 +255,12 @@
|
|
|
248
255
|
gap: var(--ui-space-32);
|
|
249
256
|
}
|
|
250
257
|
|
|
258
|
+
/* The definition table is wide (four pickers + preview per row); give it the
|
|
259
|
+
full row so it isn't squeezed into an auto-fill column. */
|
|
260
|
+
.text-styles-group {
|
|
261
|
+
grid-column: 1 / -1;
|
|
262
|
+
}
|
|
263
|
+
|
|
251
264
|
/* Utility Tokens */
|
|
252
265
|
.utility-columns {
|
|
253
266
|
display: grid;
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { oklchToHexClamped, type Oklch } from '../../core/palettes/oklch';
|
|
3
|
+
import { showCopyPopover } from '../copyPopover';
|
|
4
|
+
|
|
5
|
+
interface Props {
|
|
6
|
+
color: Oklch;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
let { color }: Props = $props();
|
|
10
|
+
|
|
11
|
+
// The basis is numeric OKLCH; hex/RGB/HSL are clamped sRGB projections off it.
|
|
12
|
+
let hex = $derived(oklchToHexClamped(color.l, color.c, color.h));
|
|
13
|
+
|
|
14
|
+
function toRgb(h: string): [number, number, number] {
|
|
15
|
+
return [parseInt(h.slice(1, 3), 16), parseInt(h.slice(3, 5), 16), parseInt(h.slice(5, 7), 16)];
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function toHsl(r: number, g: number, b: number): [number, number, number] {
|
|
19
|
+
const rn = r / 255, gn = g / 255, bn = b / 255;
|
|
20
|
+
const max = Math.max(rn, gn, bn);
|
|
21
|
+
const min = Math.min(rn, gn, bn);
|
|
22
|
+
const l = (max + min) / 2;
|
|
23
|
+
let h = 0;
|
|
24
|
+
let s = 0;
|
|
25
|
+
if (max !== min) {
|
|
26
|
+
const d = max - min;
|
|
27
|
+
s = l > 0.5 ? d / (2 - max - min) : d / (max + min);
|
|
28
|
+
if (max === rn) h = (gn - bn) / d + (gn < bn ? 6 : 0);
|
|
29
|
+
else if (max === gn) h = (bn - rn) / d + 2;
|
|
30
|
+
else h = (rn - gn) / d + 4;
|
|
31
|
+
h *= 60;
|
|
32
|
+
}
|
|
33
|
+
return [Math.round(h), Math.round(s * 100), Math.round(l * 100)];
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
let rows = $derived.by(() => {
|
|
37
|
+
const [r, g, b] = toRgb(hex);
|
|
38
|
+
const [hh, ss, ll] = toHsl(r, g, b);
|
|
39
|
+
return [
|
|
40
|
+
{ label: 'HEX', value: hex },
|
|
41
|
+
{ label: 'RGB', value: `rgb(${r}, ${g}, ${b})` },
|
|
42
|
+
{ label: 'HSL', value: `hsl(${hh}, ${ss}%, ${ll}%)` },
|
|
43
|
+
{ label: 'OKLCH', value: `oklch(${Math.round(color.l * 100)}% ${color.c.toFixed(3)} ${Math.round(color.h)})` },
|
|
44
|
+
];
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
let copied: string | null = $state(null);
|
|
48
|
+
function copy(label: string, value: string, e: MouseEvent) {
|
|
49
|
+
navigator.clipboard?.writeText(value);
|
|
50
|
+
copied = label;
|
|
51
|
+
showCopyPopover(value, e.currentTarget);
|
|
52
|
+
setTimeout(() => { if (copied === label) copied = null; }, 1500);
|
|
53
|
+
}
|
|
54
|
+
</script>
|
|
55
|
+
|
|
56
|
+
<div class="readouts">
|
|
57
|
+
{#each rows as row (row.label)}
|
|
58
|
+
<button class="row" type="button" class:copied={copied === row.label} onclick={(e) => copy(row.label, row.value, e)} title="Copy {row.label}">
|
|
59
|
+
<span class="row-label">{row.label}</span>
|
|
60
|
+
<span class="row-value">{copied === row.label ? 'copied!' : row.value}</span>
|
|
61
|
+
<i class="fas fa-copy" aria-hidden="true"></i>
|
|
62
|
+
</button>
|
|
63
|
+
{/each}
|
|
64
|
+
</div>
|
|
65
|
+
|
|
66
|
+
<style>
|
|
67
|
+
.readouts {
|
|
68
|
+
display: flex;
|
|
69
|
+
flex-direction: column;
|
|
70
|
+
gap: var(--ui-space-2);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
.row {
|
|
74
|
+
display: grid;
|
|
75
|
+
grid-template-columns: 3.5rem 1fr auto;
|
|
76
|
+
align-items: center;
|
|
77
|
+
gap: var(--ui-space-8);
|
|
78
|
+
padding: var(--ui-space-4) var(--ui-space-8);
|
|
79
|
+
background: var(--ui-surface-lowest);
|
|
80
|
+
border: 1px solid var(--ui-border-low);
|
|
81
|
+
border-radius: var(--ui-radius-sm);
|
|
82
|
+
cursor: pointer;
|
|
83
|
+
text-align: left;
|
|
84
|
+
transition: background var(--ui-transition-fast), border-color var(--ui-transition-fast);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
.row:hover {
|
|
88
|
+
background: var(--ui-surface-low);
|
|
89
|
+
border-color: var(--ui-border);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
.row-label {
|
|
93
|
+
font-size: var(--ui-font-size-xs);
|
|
94
|
+
font-weight: var(--ui-font-weight-semibold);
|
|
95
|
+
color: var(--ui-text-tertiary);
|
|
96
|
+
text-transform: uppercase;
|
|
97
|
+
letter-spacing: 0.04em;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
.row-value {
|
|
101
|
+
font-size: var(--ui-font-size-sm);
|
|
102
|
+
font-family: var(--ui-font-mono);
|
|
103
|
+
color: var(--ui-text-primary);
|
|
104
|
+
overflow: hidden;
|
|
105
|
+
text-overflow: ellipsis;
|
|
106
|
+
white-space: nowrap;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
.row i {
|
|
110
|
+
font-size: var(--ui-font-size-xs);
|
|
111
|
+
color: var(--ui-text-tertiary);
|
|
112
|
+
opacity: 0;
|
|
113
|
+
transition: opacity var(--ui-transition-fast);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
.row:hover i {
|
|
117
|
+
opacity: 1;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
.row.copied .row-value {
|
|
121
|
+
color: var(--ui-text-accent);
|
|
122
|
+
}
|
|
123
|
+
</style>
|