@motion-proto/live-tokens 0.1.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/README.md +41 -0
- package/dist-plugin/index.cjs +444 -0
- package/dist-plugin/index.d.cts +12 -0
- package/dist-plugin/index.d.ts +12 -0
- package/dist-plugin/index.js +407 -0
- package/package.json +86 -0
- package/src/components/Badge.svelte +82 -0
- package/src/components/Button.svelte +333 -0
- package/src/components/Card.svelte +83 -0
- package/src/components/CollapsibleSection.svelte +82 -0
- package/src/components/DetailNav.svelte +78 -0
- package/src/components/Dialog.svelte +269 -0
- package/src/components/InlineEditActions.svelte +73 -0
- package/src/components/Notification.svelte +308 -0
- package/src/components/ProgressBar.svelte +99 -0
- package/src/components/RadioButton.svelte +87 -0
- package/src/components/SectionDivider.svelte +121 -0
- package/src/components/TabBar.svelte +92 -0
- package/src/components/Toggle.svelte +86 -0
- package/src/components/Tooltip.svelte +64 -0
- package/src/lib/ColumnsOverlay.svelte +120 -0
- package/src/lib/LiveEditorOverlay.svelte +467 -0
- package/src/lib/columnsOverlay.ts +26 -0
- package/src/lib/cssVarSync.ts +72 -0
- package/src/lib/editorConfig.ts +9 -0
- package/src/lib/editorConfigStore.ts +14 -0
- package/src/lib/index.ts +51 -0
- package/src/lib/oklch.ts +129 -0
- package/src/lib/pageSource.ts +6 -0
- package/src/lib/tokenInit.ts +29 -0
- package/src/lib/tokenService.ts +144 -0
- package/src/lib/tokenTypes.ts +45 -0
- package/src/pages/Admin.svelte +100 -0
- package/src/pages/ShowcasePage.svelte +146 -0
- package/src/showcase/BackupBrowser.svelte +617 -0
- package/src/showcase/BezierCurveEditor.svelte +648 -0
- package/src/showcase/ColorEditPanel.svelte +498 -0
- package/src/showcase/ComponentsTab.svelte +107 -0
- package/src/showcase/EditorDialog.svelte +137 -0
- package/src/showcase/PaletteEditor.svelte +2579 -0
- package/src/showcase/PaletteSelector.svelte +627 -0
- package/src/showcase/SurfacesTab.svelte +409 -0
- package/src/showcase/TextTab.svelte +205 -0
- package/src/showcase/TokenFileManager.svelte +683 -0
- package/src/showcase/TokenMap.svelte +54 -0
- package/src/showcase/VariablesTab.svelte +2657 -0
- package/src/showcase/VisualsTab.svelte +233 -0
- package/src/showcase/curveEngine.ts +190 -0
- package/src/showcase/demos/BadgeDemo.svelte +58 -0
- package/src/showcase/demos/CardDemo.svelte +52 -0
- package/src/showcase/demos/ChoiceButtonsDemo.svelte +194 -0
- package/src/showcase/demos/CollapsibleSectionDemo.svelte +56 -0
- package/src/showcase/demos/DialogDemo.svelte +42 -0
- package/src/showcase/demos/InlineEditActionsDemo.svelte +27 -0
- package/src/showcase/demos/NotificationDemo.svelte +149 -0
- package/src/showcase/demos/ProgressBarDemo.svelte +56 -0
- package/src/showcase/demos/RadioButtonDemo.svelte +58 -0
- package/src/showcase/demos/SectionDividerDemo.svelte +79 -0
- package/src/showcase/demos/StandardButtonsDemo.svelte +457 -0
- package/src/showcase/demos/TabBarDemo.svelte +60 -0
- package/src/showcase/demos/TooltipDemo.svelte +54 -0
- package/src/showcase/editor.css +93 -0
- package/src/showcase/index.ts +17 -0
- package/src/styles/fonts/Domine/Domine-VariableFont_wght.ttf +0 -0
- package/src/styles/fonts/Domine/OFL.txt +97 -0
- package/src/styles/fonts/Domine/README.txt +66 -0
- package/src/styles/fonts.css +18 -0
- package/src/styles/form-controls.css +190 -0
|
@@ -0,0 +1,233 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { tick } from 'svelte';
|
|
3
|
+
import VariablesTab from './VariablesTab.svelte';
|
|
4
|
+
import TokenFileManager from './TokenFileManager.svelte';
|
|
5
|
+
import type { TokenFile } from '../lib/tokenTypes';
|
|
6
|
+
import {
|
|
7
|
+
saveTokenFile,
|
|
8
|
+
loadTokenFile,
|
|
9
|
+
setActiveFile,
|
|
10
|
+
scrapeCssVariables,
|
|
11
|
+
clearAllCssVarOverrides,
|
|
12
|
+
applyCssVariables,
|
|
13
|
+
} from '../lib/tokenService';
|
|
14
|
+
import { editorConfigs, activeFileName, configsLoadedFromFile } from '../lib/editorConfigStore';
|
|
15
|
+
import { get } from 'svelte/store';
|
|
16
|
+
|
|
17
|
+
const tokenNavItems = [
|
|
18
|
+
{ id: 'palette-editor', label: 'Palette Editor' },
|
|
19
|
+
{ id: 'spacing', label: 'Spacing' },
|
|
20
|
+
{ id: 'columns', label: 'Columns' },
|
|
21
|
+
{ id: 'border-radius', label: 'Border Radius' },
|
|
22
|
+
{ id: 'typography', label: 'Typography' },
|
|
23
|
+
{ id: 'shadows', label: 'Shadows' },
|
|
24
|
+
{ id: 'overlays', label: 'Overlays' },
|
|
25
|
+
{ id: 'gradients', label: 'Gradients' },
|
|
26
|
+
{ id: 'utility-tokens', label: 'Utility Tokens' }
|
|
27
|
+
];
|
|
28
|
+
|
|
29
|
+
let selectedTokenSection: string | null = null;
|
|
30
|
+
|
|
31
|
+
function scrollToSection(sectionId: string) {
|
|
32
|
+
selectedTokenSection = sectionId;
|
|
33
|
+
document.getElementById(sectionId)?.scrollIntoView({ behavior: 'smooth' });
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
let saveSignal = 0;
|
|
37
|
+
let saveStatus: 'idle' | 'saving' | 'saved' | 'error' = 'idle';
|
|
38
|
+
|
|
39
|
+
async function handleSave(e: CustomEvent<{ fileName: string; displayName: string }>) {
|
|
40
|
+
const { fileName, displayName } = e.detail;
|
|
41
|
+
if (!get(configsLoadedFromFile)) {
|
|
42
|
+
saveStatus = 'error';
|
|
43
|
+
console.warn('Save blocked: editor configs were not loaded from a token file.');
|
|
44
|
+
setTimeout(() => { saveStatus = 'idle'; }, 3000);
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
47
|
+
saveStatus = 'saving';
|
|
48
|
+
try {
|
|
49
|
+
// Flush pending Svelte reactive updates so inline CSS vars are current
|
|
50
|
+
await tick();
|
|
51
|
+
const cssVariables = scrapeCssVariables();
|
|
52
|
+
const configs = get(editorConfigs);
|
|
53
|
+
const now = new Date().toISOString();
|
|
54
|
+
const tokenFile: TokenFile = {
|
|
55
|
+
name: displayName,
|
|
56
|
+
createdAt: now,
|
|
57
|
+
updatedAt: now,
|
|
58
|
+
editorConfigs: configs,
|
|
59
|
+
cssVariables,
|
|
60
|
+
};
|
|
61
|
+
await saveTokenFile(fileName, tokenFile);
|
|
62
|
+
await setActiveFile(fileName);
|
|
63
|
+
$activeFileName = fileName;
|
|
64
|
+
// Also trigger localStorage save in all PaletteEditors
|
|
65
|
+
saveSignal++;
|
|
66
|
+
saveStatus = 'saved';
|
|
67
|
+
setTimeout(() => { saveStatus = 'idle'; }, 2000);
|
|
68
|
+
} catch {
|
|
69
|
+
saveStatus = 'error';
|
|
70
|
+
setTimeout(() => { saveStatus = 'idle'; }, 3000);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
let variablesTab: VariablesTab;
|
|
75
|
+
|
|
76
|
+
async function handleLoad(e: CustomEvent<{ fileName: string }>) {
|
|
77
|
+
const { fileName } = e.detail;
|
|
78
|
+
try {
|
|
79
|
+
const tokenFile = await loadTokenFile(fileName);
|
|
80
|
+
// Clear current inline CSS vars so stale values don't linger
|
|
81
|
+
clearAllCssVarOverrides();
|
|
82
|
+
// Immediately apply the file's pre-computed CSS variables so the site
|
|
83
|
+
// updates in one shot (shadows, overlays, and all palette vars).
|
|
84
|
+
if (tokenFile.cssVariables && Object.keys(tokenFile.cssVariables).length > 0) {
|
|
85
|
+
applyCssVariables(tokenFile.cssVariables);
|
|
86
|
+
}
|
|
87
|
+
// Directly push configs to each PaletteEditor via method call
|
|
88
|
+
if (tokenFile.editorConfigs && Object.keys(tokenFile.editorConfigs).length > 0) {
|
|
89
|
+
variablesTab.loadAllConfigs(tokenFile.editorConfigs);
|
|
90
|
+
configsLoadedFromFile.set(true);
|
|
91
|
+
}
|
|
92
|
+
} catch {
|
|
93
|
+
// silent — the UI still shows current state
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
</script>
|
|
97
|
+
|
|
98
|
+
<div class="layout">
|
|
99
|
+
<nav class="sidebar">
|
|
100
|
+
<div class="sidebar-header">Variables & Tokens</div>
|
|
101
|
+
|
|
102
|
+
<div class="nav-items">
|
|
103
|
+
{#each tokenNavItems as item}
|
|
104
|
+
<button
|
|
105
|
+
class="nav-item"
|
|
106
|
+
class:active={selectedTokenSection === item.id}
|
|
107
|
+
on:click={() => scrollToSection(item.id)}
|
|
108
|
+
>
|
|
109
|
+
<span>{item.label}</span>
|
|
110
|
+
</button>
|
|
111
|
+
{/each}
|
|
112
|
+
</div>
|
|
113
|
+
|
|
114
|
+
<div class="sidebar-footer">
|
|
115
|
+
<TokenFileManager {saveStatus} on:save={handleSave} on:load={handleLoad} />
|
|
116
|
+
</div>
|
|
117
|
+
</nav>
|
|
118
|
+
|
|
119
|
+
<main class="content">
|
|
120
|
+
<VariablesTab bind:this={variablesTab} {saveSignal} />
|
|
121
|
+
</main>
|
|
122
|
+
</div>
|
|
123
|
+
|
|
124
|
+
<style>
|
|
125
|
+
@import '../styles/variables.css';
|
|
126
|
+
|
|
127
|
+
.layout {
|
|
128
|
+
display: grid;
|
|
129
|
+
grid-template-columns: 240px minmax(0, 1fr);
|
|
130
|
+
min-height: 100vh;
|
|
131
|
+
width: 100%;
|
|
132
|
+
max-width: 1920px;
|
|
133
|
+
box-sizing: border-box;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
.sidebar {
|
|
137
|
+
position: sticky;
|
|
138
|
+
top: 0;
|
|
139
|
+
height: calc(100vh - 52px);
|
|
140
|
+
overflow: hidden;
|
|
141
|
+
background: black;
|
|
142
|
+
border-right: 1px solid var(--ui-border-faint);
|
|
143
|
+
display: flex;
|
|
144
|
+
z-index: 1;
|
|
145
|
+
flex-direction: column;
|
|
146
|
+
min-width: 0;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
.sidebar-header {
|
|
150
|
+
padding: var(--space-16) var(--space-16) var(--space-12);
|
|
151
|
+
font-size: var(--font-lg);
|
|
152
|
+
font-weight: var(--font-weight-bold);
|
|
153
|
+
color: var(--ui-text-primary);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
.nav-items {
|
|
157
|
+
display: flex;
|
|
158
|
+
flex-direction: column;
|
|
159
|
+
gap: var(--space-2);
|
|
160
|
+
padding: 0 var(--space-8);
|
|
161
|
+
flex-shrink: 0;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
.nav-item {
|
|
165
|
+
display: flex;
|
|
166
|
+
align-items: center;
|
|
167
|
+
gap: var(--space-8);
|
|
168
|
+
width: 100%;
|
|
169
|
+
padding: var(--space-6) var(--space-12) var(--space-6) var(--space-24);
|
|
170
|
+
background: none;
|
|
171
|
+
border: none;
|
|
172
|
+
border-radius: var(--radius-md);
|
|
173
|
+
color: var(--ui-text-tertiary);
|
|
174
|
+
font-size: var(--font-md);
|
|
175
|
+
cursor: pointer;
|
|
176
|
+
text-align: left;
|
|
177
|
+
transition: all var(--transition-fast);
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
.nav-item:hover {
|
|
181
|
+
color: var(--ui-text-secondary);
|
|
182
|
+
background: var(--ui-hover);
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
.nav-item.active {
|
|
186
|
+
color: var(--ui-text-primary);
|
|
187
|
+
background: var(--ui-surface-high);
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
.content {
|
|
191
|
+
padding: var(--space-24) var(--space-32);
|
|
192
|
+
overflow-y: auto;
|
|
193
|
+
background: black;
|
|
194
|
+
min-width: 0;
|
|
195
|
+
isolation: isolate;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/* Narrow desktop: side-by-side windows, ~1280px and below */
|
|
199
|
+
@media (max-width: 1280px) {
|
|
200
|
+
.layout {
|
|
201
|
+
grid-template-columns: 200px minmax(0, 1fr);
|
|
202
|
+
}
|
|
203
|
+
.content {
|
|
204
|
+
padding: var(--space-20) var(--space-20);
|
|
205
|
+
}
|
|
206
|
+
.nav-item {
|
|
207
|
+
padding: var(--space-6) var(--space-8) var(--space-6) var(--space-16);
|
|
208
|
+
}
|
|
209
|
+
.sidebar-header {
|
|
210
|
+
padding: var(--space-12) var(--space-12) var(--space-8);
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
/* Tight desktop: very narrow, ~1024px and below */
|
|
215
|
+
@media (max-width: 1024px) {
|
|
216
|
+
.layout {
|
|
217
|
+
grid-template-columns: 180px minmax(0, 1fr);
|
|
218
|
+
}
|
|
219
|
+
.content {
|
|
220
|
+
padding: var(--space-16) var(--space-12);
|
|
221
|
+
}
|
|
222
|
+
.nav-item span {
|
|
223
|
+
font-size: var(--font-sm);
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
.sidebar-footer {
|
|
228
|
+
flex-shrink: 0;
|
|
229
|
+
margin-top: auto;
|
|
230
|
+
padding: var(--space-12) var(--space-8) var(--space-16);
|
|
231
|
+
border-top: 1px solid var(--ui-border-faint);
|
|
232
|
+
}
|
|
233
|
+
</style>
|
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
export interface CurveAnchor {
|
|
2
|
+
x: number;
|
|
3
|
+
y: number;
|
|
4
|
+
inDx: number;
|
|
5
|
+
inDy: number;
|
|
6
|
+
outDx: number;
|
|
7
|
+
outDy: number;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export interface CurveConfig {
|
|
11
|
+
yMin: number;
|
|
12
|
+
yMax: number;
|
|
13
|
+
label: string;
|
|
14
|
+
gridLines: number[];
|
|
15
|
+
dashedLines: number[];
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export interface CurveTemplate {
|
|
19
|
+
name: string;
|
|
20
|
+
icon: string;
|
|
21
|
+
anchors: (cfg: CurveConfig) => CurveAnchor[];
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export const CURVE_H = 200;
|
|
25
|
+
export const CURVE_PAD_Y = 2;
|
|
26
|
+
export const CURVE_Y_PAD = 0.2;
|
|
27
|
+
|
|
28
|
+
export const lightnessCurveConfig: CurveConfig = {
|
|
29
|
+
yMin: 0, yMax: 100,
|
|
30
|
+
label: 'Lightness',
|
|
31
|
+
gridLines: [50],
|
|
32
|
+
dashedLines: [25, 75],
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
export const saturationCurveConfig: CurveConfig = {
|
|
36
|
+
yMin: 0, yMax: 200,
|
|
37
|
+
label: 'Saturation',
|
|
38
|
+
gridLines: [100],
|
|
39
|
+
dashedLines: [50, 150],
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
export const textLightnessCurveConfig: CurveConfig = {
|
|
43
|
+
yMin: 0, yMax: 200,
|
|
44
|
+
label: 'Lightness',
|
|
45
|
+
gridLines: [100],
|
|
46
|
+
dashedLines: [50, 150],
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
export const curveTemplates: CurveTemplate[] = [
|
|
50
|
+
{
|
|
51
|
+
name: 'Flat',
|
|
52
|
+
icon: 'M2,6 L18,6',
|
|
53
|
+
anchors: (cfg) => {
|
|
54
|
+
const mid = (cfg.yMin + cfg.yMax) / 2;
|
|
55
|
+
return [
|
|
56
|
+
{ x: 0, y: mid, inDx: 0, inDy: 0, outDx: 30, outDy: 0 },
|
|
57
|
+
{ x: 100, y: mid, inDx: -30, inDy: 0, outDx: 0, outDy: 0 },
|
|
58
|
+
];
|
|
59
|
+
},
|
|
60
|
+
},
|
|
61
|
+
{
|
|
62
|
+
name: 'Peak',
|
|
63
|
+
icon: 'M2,10 L10,2 L18,10',
|
|
64
|
+
anchors: (cfg) => [
|
|
65
|
+
{ x: 0, y: cfg.yMin, inDx: 0, inDy: 0, outDx: 15, outDy: 0 },
|
|
66
|
+
{ x: 50, y: cfg.yMax / 2, inDx: -15, inDy: 0, outDx: 15, outDy: 0 },
|
|
67
|
+
{ x: 100, y: cfg.yMin, inDx: -15, inDy: 0, outDx: 0, outDy: 0 },
|
|
68
|
+
],
|
|
69
|
+
},
|
|
70
|
+
{
|
|
71
|
+
name: 'Ramp up',
|
|
72
|
+
icon: 'M2,10 L18,2',
|
|
73
|
+
anchors: (cfg) => {
|
|
74
|
+
const lo = cfg.yMax * 0.1;
|
|
75
|
+
const hi = cfg.yMax * 0.9;
|
|
76
|
+
return [
|
|
77
|
+
{ x: 0, y: lo, inDx: 0, inDy: 0, outDx: 30, outDy: 0 },
|
|
78
|
+
{ x: 100, y: hi, inDx: -30, inDy: 0, outDx: 0, outDy: 0 },
|
|
79
|
+
];
|
|
80
|
+
},
|
|
81
|
+
},
|
|
82
|
+
{
|
|
83
|
+
name: 'Ramp down',
|
|
84
|
+
icon: 'M2,2 L18,10',
|
|
85
|
+
anchors: (cfg) => {
|
|
86
|
+
const lo = cfg.yMax * 0.1;
|
|
87
|
+
const hi = cfg.yMax * 0.9;
|
|
88
|
+
return [
|
|
89
|
+
{ x: 0, y: hi, inDx: 0, inDy: 0, outDx: 30, outDy: 0 },
|
|
90
|
+
{ x: 100, y: lo, inDx: -30, inDy: 0, outDx: 0, outDy: 0 },
|
|
91
|
+
];
|
|
92
|
+
},
|
|
93
|
+
},
|
|
94
|
+
];
|
|
95
|
+
|
|
96
|
+
export function makeAnchor(x: number, y: number, tangentLen = 15): CurveAnchor {
|
|
97
|
+
return { x, y, inDx: -tangentLen, inDy: 0, outDx: tangentLen, outDy: 0 };
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
export function isCornerAnchor(a: CurveAnchor): boolean {
|
|
101
|
+
return a.inDx === 0 && a.inDy === 0 && a.outDx === 0 && a.outDy === 0;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
export function curveXToSvg(v: number, w: number, padX: number = 0): number {
|
|
105
|
+
return padX + (v / 100) * (w - 2 * padX);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
export function curveYToSvg(v: number, cfg: CurveConfig): number {
|
|
109
|
+
const range = cfg.yMax - cfg.yMin;
|
|
110
|
+
const dMin = cfg.yMin - CURVE_Y_PAD * range;
|
|
111
|
+
const dMax = cfg.yMax + CURVE_Y_PAD * range;
|
|
112
|
+
return CURVE_H - CURVE_PAD_Y - ((v - dMin) / (dMax - dMin)) * (CURVE_H - 2 * CURVE_PAD_Y);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
export function svgToX(px: number, w: number, padX: number = 0): number {
|
|
116
|
+
return Math.max(0, Math.min(100, ((px - padX) / (w - 2 * padX)) * 100));
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
export function svgToY(py: number, cfg: CurveConfig): number {
|
|
120
|
+
const range = cfg.yMax - cfg.yMin;
|
|
121
|
+
const dMin = cfg.yMin - CURVE_Y_PAD * range;
|
|
122
|
+
const dMax = cfg.yMax + CURVE_Y_PAD * range;
|
|
123
|
+
const raw = dMin + ((CURVE_H - CURVE_PAD_Y - py) / (CURVE_H - 2 * CURVE_PAD_Y)) * (dMax - dMin);
|
|
124
|
+
return Math.max(cfg.yMin, Math.min(cfg.yMax, raw));
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
export function evalBezier(
|
|
128
|
+
p0x: number, p0y: number, c0x: number, c0y: number,
|
|
129
|
+
c1x: number, c1y: number, p1x: number, p1y: number, t: number,
|
|
130
|
+
): { x: number; y: number } {
|
|
131
|
+
const u = 1 - t, u2 = u * u, u3 = u2 * u;
|
|
132
|
+
const t2 = t * t, t3 = t2 * t;
|
|
133
|
+
return {
|
|
134
|
+
x: u3 * p0x + 3 * u2 * t * c0x + 3 * u * t2 * c1x + t3 * p1x,
|
|
135
|
+
y: u3 * p0y + 3 * u2 * t * c0y + 3 * u * t2 * c1y + t3 * p1y,
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
export function sampleCurve(anchors: CurveAnchor[], xPos: number): number {
|
|
140
|
+
if (anchors.length === 0) return 0;
|
|
141
|
+
if (anchors.length === 1) return anchors[0].y;
|
|
142
|
+
if (xPos <= anchors[0].x) return anchors[0].y;
|
|
143
|
+
if (xPos >= anchors[anchors.length - 1].x) return anchors[anchors.length - 1].y;
|
|
144
|
+
|
|
145
|
+
let seg = 0;
|
|
146
|
+
while (seg < anchors.length - 2 && anchors[seg + 1].x < xPos) seg++;
|
|
147
|
+
|
|
148
|
+
const a0 = anchors[seg], a1 = anchors[seg + 1];
|
|
149
|
+
const p0x = a0.x, p0y = a0.y;
|
|
150
|
+
const c0x = a0.x + a0.outDx, c0y = a0.y + a0.outDy;
|
|
151
|
+
const c1x = a1.x + a1.inDx, c1y = a1.y + a1.inDy;
|
|
152
|
+
const p1x = a1.x, p1y = a1.y;
|
|
153
|
+
|
|
154
|
+
let lo = 0, hi = 1;
|
|
155
|
+
for (let i = 0; i < 20; i++) {
|
|
156
|
+
const mid = (lo + hi) / 2;
|
|
157
|
+
const pt = evalBezier(p0x, p0y, c0x, c0y, c1x, c1y, p1x, p1y, mid);
|
|
158
|
+
if (pt.x < xPos) lo = mid; else hi = mid;
|
|
159
|
+
}
|
|
160
|
+
return evalBezier(p0x, p0y, c0x, c0y, c1x, c1y, p1x, p1y, (lo + hi) / 2).y;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
// --- Clipboard serialization ---
|
|
164
|
+
|
|
165
|
+
const CLIPBOARD_PREFIX = 'mp-curve:';
|
|
166
|
+
|
|
167
|
+
export function serializeCurve(anchors: CurveAnchor[], offset: number): string {
|
|
168
|
+
return CLIPBOARD_PREFIX + JSON.stringify({ anchors, offset });
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
export function deserializeCurve(text: string): { anchors: CurveAnchor[]; offset: number } | null {
|
|
172
|
+
if (!text.startsWith(CLIPBOARD_PREFIX)) return null;
|
|
173
|
+
try {
|
|
174
|
+
const data = JSON.parse(text.slice(CLIPBOARD_PREFIX.length));
|
|
175
|
+
if (!Array.isArray(data.anchors) || typeof data.offset !== 'number') return null;
|
|
176
|
+
return data;
|
|
177
|
+
} catch {
|
|
178
|
+
return null;
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
export function buildCurvePath(anchors: CurveAnchor[], cfg: CurveConfig, w: number, padX: number = 0): string {
|
|
183
|
+
if (anchors.length < 2) return '';
|
|
184
|
+
let d = `M${curveXToSvg(anchors[0].x, w, padX)},${curveYToSvg(anchors[0].y, cfg)}`;
|
|
185
|
+
for (let i = 0; i < anchors.length - 1; i++) {
|
|
186
|
+
const a0 = anchors[i], a1 = anchors[i + 1];
|
|
187
|
+
d += ` C${curveXToSvg(a0.x + a0.outDx, w, padX)},${curveYToSvg(a0.y + a0.outDy, cfg)} ${curveXToSvg(a1.x + a1.inDx, w, padX)},${curveYToSvg(a1.y + a1.inDy, cfg)} ${curveXToSvg(a1.x, w, padX)},${curveYToSvg(a1.y, cfg)}`;
|
|
188
|
+
}
|
|
189
|
+
return d;
|
|
190
|
+
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import Badge from '../../components/Badge.svelte';
|
|
3
|
+
import TokenMap from '../TokenMap.svelte';
|
|
4
|
+
</script>
|
|
5
|
+
|
|
6
|
+
<div class="demo-block">
|
|
7
|
+
<h2 class="component-title">Badge Component</h2>
|
|
8
|
+
<p class="demo-description">
|
|
9
|
+
Pill-shaped badges with variant support. Import from <code>components/Badge.svelte</code>
|
|
10
|
+
</p>
|
|
11
|
+
|
|
12
|
+
<div class="demo-section">
|
|
13
|
+
<div class="trait-demo-row">
|
|
14
|
+
<Badge variant="trait">arcane</Badge>
|
|
15
|
+
<Badge variant="trait">divine</Badge>
|
|
16
|
+
<Badge variant="trait">primal</Badge>
|
|
17
|
+
<Badge variant="accent">scenes</Badge>
|
|
18
|
+
<Badge icon="fa-solid fa-dice-d20">System Agnostic</Badge>
|
|
19
|
+
</div>
|
|
20
|
+
</div>
|
|
21
|
+
|
|
22
|
+
<div class="demo-section">
|
|
23
|
+
<h3 class="demo-subtitle">Tokens</h3>
|
|
24
|
+
<div class="token-sections">
|
|
25
|
+
<TokenMap title="info" tokens={[
|
|
26
|
+
{ label: 'BG', variable: '--surface-neutral-higher' },
|
|
27
|
+
{ label: 'Border', variable: '--border-neutral-default' },
|
|
28
|
+
{ label: 'Text', variable: '--text-primary' },
|
|
29
|
+
]} />
|
|
30
|
+
<TokenMap title="accent" tokens={[
|
|
31
|
+
{ label: 'BG', variable: '--surface-neutral-higher' },
|
|
32
|
+
{ label: 'Border', variable: '--border-accent' },
|
|
33
|
+
{ label: 'Text', variable: '--color-accent-300' },
|
|
34
|
+
]} />
|
|
35
|
+
<TokenMap title="trait" tokens={[
|
|
36
|
+
{ label: 'BG', variable: '--surface-primary-high' },
|
|
37
|
+
{ label: 'Border', variable: '--border-primary-strong' },
|
|
38
|
+
{ label: 'Text', variable: '--text-primary' },
|
|
39
|
+
]} />
|
|
40
|
+
</div>
|
|
41
|
+
</div>
|
|
42
|
+
</div>
|
|
43
|
+
|
|
44
|
+
<style>
|
|
45
|
+
@import '../../styles/variables.css';
|
|
46
|
+
|
|
47
|
+
.trait-demo-row {
|
|
48
|
+
display: flex;
|
|
49
|
+
gap: var(--space-8);
|
|
50
|
+
flex-wrap: wrap;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
.token-sections {
|
|
54
|
+
display: flex;
|
|
55
|
+
flex-direction: column;
|
|
56
|
+
gap: var(--space-16);
|
|
57
|
+
}
|
|
58
|
+
</style>
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import Card from '../../components/Card.svelte';
|
|
3
|
+
import TokenMap from '../TokenMap.svelte';
|
|
4
|
+
</script>
|
|
5
|
+
|
|
6
|
+
<div class="demo-block">
|
|
7
|
+
<h2 class="component-title">Card Component</h2>
|
|
8
|
+
<p class="demo-description">
|
|
9
|
+
Generic card with icon, title, and slotted body. Import from <code>components/Card.svelte</code>
|
|
10
|
+
</p>
|
|
11
|
+
|
|
12
|
+
<div class="demo-section">
|
|
13
|
+
<div class="card-demo-grid">
|
|
14
|
+
<Card icon="fas fa-star" iconColor="var(--text-accent)" title="Featured">
|
|
15
|
+
<p style="margin: 0;">A highlighted card with amber accent.</p>
|
|
16
|
+
</Card>
|
|
17
|
+
|
|
18
|
+
<Card icon="fas fa-shield-alt" iconColor="var(--text-info)" title="Security">
|
|
19
|
+
<p style="margin: 0;">Card with blue icon color on hover border.</p>
|
|
20
|
+
</Card>
|
|
21
|
+
|
|
22
|
+
<Card icon="fas fa-leaf" iconColor="var(--text-success)" title="Compact" size="compact">
|
|
23
|
+
<p style="margin: 0;">A compact-sized card variant.</p>
|
|
24
|
+
</Card>
|
|
25
|
+
|
|
26
|
+
<Card title="No Icon">
|
|
27
|
+
<p style="margin: 0;">Cards work without icons too.</p>
|
|
28
|
+
</Card>
|
|
29
|
+
</div>
|
|
30
|
+
</div>
|
|
31
|
+
|
|
32
|
+
<div class="demo-section">
|
|
33
|
+
<h3 class="demo-subtitle">Tokens</h3>
|
|
34
|
+
<TokenMap tokens={[
|
|
35
|
+
{ label: 'Background', variable: '--surface-neutral-high' },
|
|
36
|
+
{ label: 'Border', variable: '--border-neutral-default' },
|
|
37
|
+
{ label: 'Hover Border', variable: '--border-neutral-strong' },
|
|
38
|
+
{ label: 'Title', variable: '--text-primary' },
|
|
39
|
+
{ label: 'Body', variable: '--text-secondary' },
|
|
40
|
+
]} />
|
|
41
|
+
</div>
|
|
42
|
+
</div>
|
|
43
|
+
|
|
44
|
+
<style>
|
|
45
|
+
@import '../../styles/variables.css';
|
|
46
|
+
|
|
47
|
+
.card-demo-grid {
|
|
48
|
+
display: grid;
|
|
49
|
+
grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
|
|
50
|
+
gap: var(--space-16);
|
|
51
|
+
}
|
|
52
|
+
</style>
|