@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
package/src/lib/oklch.ts
ADDED
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
// OKLCH color space conversions — hand-rolled, no dependencies
|
|
2
|
+
// Pipeline: sRGB hex → linear RGB → OKLab (via M1×M2 + cube root) → OKLCH (polar)
|
|
3
|
+
|
|
4
|
+
export interface Oklch {
|
|
5
|
+
l: number; // 0..1
|
|
6
|
+
c: number; // 0..~0.4
|
|
7
|
+
h: number; // 0..360
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
// --- sRGB gamma ---
|
|
11
|
+
|
|
12
|
+
function srgbToLinear(c: number): number {
|
|
13
|
+
return c <= 0.04045 ? c / 12.92 : Math.pow((c + 0.055) / 1.055, 2.4);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function linearToSrgb(c: number): number {
|
|
17
|
+
return c <= 0.0031308 ? 12.92 * c : 1.055 * Math.pow(c, 1 / 2.4) - 0.055;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
// --- Hex ↔ linear RGB ---
|
|
21
|
+
|
|
22
|
+
function hexToLinearRgb(hex: string): [number, number, number] {
|
|
23
|
+
const r = parseInt(hex.slice(1, 3), 16) / 255;
|
|
24
|
+
const g = parseInt(hex.slice(3, 5), 16) / 255;
|
|
25
|
+
const b = parseInt(hex.slice(5, 7), 16) / 255;
|
|
26
|
+
return [srgbToLinear(r), srgbToLinear(g), srgbToLinear(b)];
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function linearRgbToHex(r: number, g: number, b: number): string {
|
|
30
|
+
const toHex = (c: number) => {
|
|
31
|
+
const v = Math.round(Math.max(0, Math.min(1, linearToSrgb(c))) * 255);
|
|
32
|
+
return v.toString(16).padStart(2, '0');
|
|
33
|
+
};
|
|
34
|
+
return `#${toHex(r)}${toHex(g)}${toHex(b)}`;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// --- Linear RGB ↔ OKLab ---
|
|
38
|
+
// Uses the two-matrix approach from Björn Ottosson's blog
|
|
39
|
+
|
|
40
|
+
function linearRgbToOklab(r: number, g: number, b: number): [number, number, number] {
|
|
41
|
+
const l_ = 0.4122214708 * r + 0.5363325363 * g + 0.0514459929 * b;
|
|
42
|
+
const m_ = 0.2119034982 * r + 0.6806995451 * g + 0.1073969566 * b;
|
|
43
|
+
const s_ = 0.0883024619 * r + 0.2817188376 * g + 0.6299787005 * b;
|
|
44
|
+
|
|
45
|
+
const l = Math.cbrt(l_);
|
|
46
|
+
const m = Math.cbrt(m_);
|
|
47
|
+
const s = Math.cbrt(s_);
|
|
48
|
+
|
|
49
|
+
return [
|
|
50
|
+
0.2104542553 * l + 0.7936177850 * m - 0.0040720468 * s,
|
|
51
|
+
1.9779984951 * l - 2.4285922050 * m + 0.4505937099 * s,
|
|
52
|
+
0.0259040371 * l + 0.7827717662 * m - 0.8086757660 * s,
|
|
53
|
+
];
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function oklabToLinearRgb(L: number, a: number, b: number): [number, number, number] {
|
|
57
|
+
const l_ = L + 0.3963377774 * a + 0.2158037573 * b;
|
|
58
|
+
const m_ = L - 0.1055613458 * a - 0.0638541728 * b;
|
|
59
|
+
const s_ = L - 0.0894841775 * a - 1.2914855480 * b;
|
|
60
|
+
|
|
61
|
+
const l = l_ * l_ * l_;
|
|
62
|
+
const m = m_ * m_ * m_;
|
|
63
|
+
const s = s_ * s_ * s_;
|
|
64
|
+
|
|
65
|
+
return [
|
|
66
|
+
+4.0767416621 * l - 3.3077115913 * m + 0.2309699292 * s,
|
|
67
|
+
-1.2684380046 * l + 2.6097574011 * m - 0.3413193965 * s,
|
|
68
|
+
-0.0041960863 * l - 0.7034186147 * m + 1.7076147010 * s,
|
|
69
|
+
];
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// --- OKLab ↔ OKLCH ---
|
|
73
|
+
|
|
74
|
+
function oklabToOklch(L: number, a: number, b: number): Oklch {
|
|
75
|
+
const c = Math.sqrt(a * a + b * b);
|
|
76
|
+
let h = (Math.atan2(b, a) * 180) / Math.PI;
|
|
77
|
+
if (h < 0) h += 360;
|
|
78
|
+
return { l: L, c, h };
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function oklchToOklab(l: number, c: number, h: number): [number, number, number] {
|
|
82
|
+
const hRad = (h * Math.PI) / 180;
|
|
83
|
+
return [l, c * Math.cos(hRad), c * Math.sin(hRad)];
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// --- Public API ---
|
|
87
|
+
|
|
88
|
+
export function hexToOklch(hex: string): Oklch {
|
|
89
|
+
const [r, g, b] = hexToLinearRgb(hex);
|
|
90
|
+
const [L, a, bVal] = linearRgbToOklab(r, g, b);
|
|
91
|
+
return oklabToOklch(L, a, bVal);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
export function oklchToHex(l: number, c: number, h: number): string {
|
|
95
|
+
const [L, a, b] = oklchToOklab(l, c, h);
|
|
96
|
+
const [r, g, bVal] = oklabToLinearRgb(L, a, b);
|
|
97
|
+
return linearRgbToHex(r, g, bVal);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function isInGamut(r: number, g: number, b: number): boolean {
|
|
101
|
+
const eps = 0.0001;
|
|
102
|
+
return r >= -eps && r <= 1 + eps && g >= -eps && g <= 1 + eps && b >= -eps && b <= 1 + eps;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
export function gamutClamp(l: number, c: number, h: number): Oklch {
|
|
106
|
+
// Clamp lightness
|
|
107
|
+
if (l <= 0) return { l: 0, c: 0, h };
|
|
108
|
+
if (l >= 1) return { l: 1, c: 0, h };
|
|
109
|
+
|
|
110
|
+
// Binary search: reduce chroma until in sRGB gamut
|
|
111
|
+
const [L, a, b] = oklchToOklab(l, c, h);
|
|
112
|
+
const [r, g, bVal] = oklabToLinearRgb(L, a, b);
|
|
113
|
+
|
|
114
|
+
if (isInGamut(r, g, bVal)) return { l, c, h };
|
|
115
|
+
|
|
116
|
+
let lo = 0;
|
|
117
|
+
let hi = c;
|
|
118
|
+
for (let i = 0; i < 20; i++) {
|
|
119
|
+
const mid = (lo + hi) / 2;
|
|
120
|
+
const [La, aa, ba] = oklchToOklab(l, mid, h);
|
|
121
|
+
const [rr, gg, bb] = oklabToLinearRgb(La, aa, ba);
|
|
122
|
+
if (isInGamut(rr, gg, bb)) {
|
|
123
|
+
lo = mid;
|
|
124
|
+
} else {
|
|
125
|
+
hi = mid;
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
return { l, c: lo, h };
|
|
129
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
export function resolvePageSource(route: string): string | undefined {
|
|
2
|
+
if (route === '/') return 'src/pages/Landing.svelte';
|
|
3
|
+
if (route === '/components') return 'src/pages/ShowcasePage.svelte';
|
|
4
|
+
if (route === '/admin') return 'src/pages/Admin.svelte';
|
|
5
|
+
return undefined;
|
|
6
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { getActiveTokens, applyCssVariables } from './tokenService';
|
|
2
|
+
import { activeFileName, loadedConfigs, configsLoadedFromFile } from './editorConfigStore';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Fetch the active token file from the server and apply its CSS variables
|
|
6
|
+
* to :root before the app mounts. Also populates the loadedConfigs store
|
|
7
|
+
* so PaletteEditors initialize from the token file instead of stale localStorage.
|
|
8
|
+
*/
|
|
9
|
+
export async function initializeTokens(): Promise<void> {
|
|
10
|
+
try {
|
|
11
|
+
const tokens = await getActiveTokens();
|
|
12
|
+
if (tokens) {
|
|
13
|
+
if (tokens.cssVariables && Object.keys(tokens.cssVariables).length > 0) {
|
|
14
|
+
applyCssVariables(tokens.cssVariables);
|
|
15
|
+
}
|
|
16
|
+
const fileName = (tokens as any)._fileName || 'default';
|
|
17
|
+
activeFileName.set(fileName);
|
|
18
|
+
// Push editor configs so PaletteEditors load from the token file on mount.
|
|
19
|
+
// The store is left populated; PaletteEditors read on mount then
|
|
20
|
+
// VisualsTab (or the mount lifecycle) clears it after tick().
|
|
21
|
+
if (tokens.editorConfigs && Object.keys(tokens.editorConfigs).length > 0) {
|
|
22
|
+
loadedConfigs.set(tokens.editorConfigs);
|
|
23
|
+
configsLoadedFromFile.set(true);
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
} catch {
|
|
27
|
+
// Silent fallback — variables.css provides defaults
|
|
28
|
+
}
|
|
29
|
+
}
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
import type { TokenFile, TokenFileMeta } from './tokenTypes';
|
|
2
|
+
import {
|
|
3
|
+
applyCssVariables as applyCssVariablesSync,
|
|
4
|
+
clearAllCssVarOverrides as clearAllCssVarOverridesSync,
|
|
5
|
+
scrapeCssVariables as scrapeCssVariablesSync,
|
|
6
|
+
} from './cssVarSync';
|
|
7
|
+
|
|
8
|
+
// ── API helpers ──────────────────────────────────────────────
|
|
9
|
+
|
|
10
|
+
export async function listTokenFiles(): Promise<TokenFileMeta[]> {
|
|
11
|
+
const res = await fetch('/api/tokens');
|
|
12
|
+
const data = await res.json();
|
|
13
|
+
return data.files;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export async function loadTokenFile(fileName: string): Promise<TokenFile> {
|
|
17
|
+
const res = await fetch(`/api/tokens/${encodeURIComponent(fileName)}`);
|
|
18
|
+
if (!res.ok) throw new Error(`Failed to load token file: ${fileName}`);
|
|
19
|
+
return res.json();
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export async function saveTokenFile(fileName: string, data: TokenFile): Promise<void> {
|
|
23
|
+
const res = await fetch(`/api/tokens/${encodeURIComponent(fileName)}`, {
|
|
24
|
+
method: 'PUT',
|
|
25
|
+
headers: { 'Content-Type': 'application/json' },
|
|
26
|
+
body: JSON.stringify(data),
|
|
27
|
+
});
|
|
28
|
+
if (!res.ok) {
|
|
29
|
+
const err = await res.json().catch(() => ({ error: 'Unknown error' }));
|
|
30
|
+
throw new Error(err.error || 'Save failed');
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export async function deleteTokenFile(fileName: string): Promise<void> {
|
|
35
|
+
const res = await fetch(`/api/tokens/${encodeURIComponent(fileName)}`, {
|
|
36
|
+
method: 'DELETE',
|
|
37
|
+
});
|
|
38
|
+
if (!res.ok) {
|
|
39
|
+
const err = await res.json().catch(() => ({ error: 'Unknown error' }));
|
|
40
|
+
throw new Error(err.error || 'Delete failed');
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export async function getActiveTokens(): Promise<TokenFile | null> {
|
|
45
|
+
try {
|
|
46
|
+
const res = await fetch('/api/tokens/active');
|
|
47
|
+
if (!res.ok) return null;
|
|
48
|
+
return res.json();
|
|
49
|
+
} catch {
|
|
50
|
+
return null;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export async function setActiveFile(fileName: string): Promise<void> {
|
|
55
|
+
await fetch('/api/tokens/active', {
|
|
56
|
+
method: 'PUT',
|
|
57
|
+
headers: { 'Content-Type': 'application/json' },
|
|
58
|
+
body: JSON.stringify({ name: fileName }),
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// ── Production API helpers ─────────────────────────────────
|
|
63
|
+
|
|
64
|
+
export interface ProductionInfo {
|
|
65
|
+
fileName: string;
|
|
66
|
+
name: string;
|
|
67
|
+
updatedAt: string;
|
|
68
|
+
cssVariables: Record<string, string>;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export async function getProductionInfo(): Promise<ProductionInfo> {
|
|
72
|
+
const res = await fetch('/api/tokens/production');
|
|
73
|
+
return res.json();
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export async function setProductionFile(fileName: string): Promise<{ ok: boolean; fileName: string; name: string }> {
|
|
77
|
+
const res = await fetch('/api/tokens/production', {
|
|
78
|
+
method: 'PUT',
|
|
79
|
+
headers: { 'Content-Type': 'application/json' },
|
|
80
|
+
body: JSON.stringify({ name: fileName }),
|
|
81
|
+
});
|
|
82
|
+
if (!res.ok) {
|
|
83
|
+
const err = await res.json().catch(() => ({ error: 'Unknown error' }));
|
|
84
|
+
throw new Error(err.error || 'Update failed');
|
|
85
|
+
}
|
|
86
|
+
return res.json();
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// ── Backup API helpers ──────────────────────────────────────
|
|
90
|
+
|
|
91
|
+
export interface BackupEntry {
|
|
92
|
+
type: 'tokens' | 'css';
|
|
93
|
+
file: string;
|
|
94
|
+
name: string;
|
|
95
|
+
timestamp: string;
|
|
96
|
+
size: number;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
export async function listBackups(): Promise<BackupEntry[]> {
|
|
100
|
+
const res = await fetch('/api/backups');
|
|
101
|
+
const data = await res.json();
|
|
102
|
+
return data.backups;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
export async function getBackupContent(type: string, file: string): Promise<string> {
|
|
106
|
+
const res = await fetch(`/api/backups/${type}/${encodeURIComponent(file)}`);
|
|
107
|
+
if (!res.ok) throw new Error('Failed to load backup');
|
|
108
|
+
const data = await res.json();
|
|
109
|
+
return data.content;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
export async function restoreBackup(type: string, file: string): Promise<void> {
|
|
113
|
+
const res = await fetch(`/api/backups/${type}/${encodeURIComponent(file)}/restore`, {
|
|
114
|
+
method: 'POST',
|
|
115
|
+
});
|
|
116
|
+
if (!res.ok) throw new Error('Restore failed');
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
export async function getCurrentCss(): Promise<string> {
|
|
120
|
+
const res = await fetch('/api/current-css');
|
|
121
|
+
const data = await res.json();
|
|
122
|
+
return data.content;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// ── CSS variable utilities ───────────────────────────────────
|
|
126
|
+
// Implementations live in cssVarSync.ts so writes can fan out to the
|
|
127
|
+
// parent document when running inside the live-preview overlay iframe.
|
|
128
|
+
// Re-exported here to preserve existing call sites.
|
|
129
|
+
|
|
130
|
+
export const clearAllCssVarOverrides = clearAllCssVarOverridesSync;
|
|
131
|
+
export const applyCssVariables = applyCssVariablesSync;
|
|
132
|
+
export const scrapeCssVariables = scrapeCssVariablesSync;
|
|
133
|
+
|
|
134
|
+
/** Sanitize a display name to a safe file name */
|
|
135
|
+
export function sanitizeFileName(name: string): string {
|
|
136
|
+
return name
|
|
137
|
+
.toLowerCase()
|
|
138
|
+
.trim()
|
|
139
|
+
.replace(/\s+/g, '-')
|
|
140
|
+
.replace(/[^a-z0-9\-_]/g, '')
|
|
141
|
+
.replace(/-+/g, '-')
|
|
142
|
+
.replace(/^-|-$/g, '')
|
|
143
|
+
|| 'unnamed';
|
|
144
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import type { CurveAnchor } from '../showcase/curveEngine';
|
|
2
|
+
|
|
3
|
+
export type GradientStyle = 'linear' | 'radial' | 'conic';
|
|
4
|
+
|
|
5
|
+
export interface GradientStop {
|
|
6
|
+
position: number;
|
|
7
|
+
paletteLabel: string;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export interface PaletteConfig {
|
|
11
|
+
baseColor: string;
|
|
12
|
+
tintHue: number;
|
|
13
|
+
tintChroma?: number;
|
|
14
|
+
lightnessCurve: CurveAnchor[];
|
|
15
|
+
saturationCurve: CurveAnchor[];
|
|
16
|
+
grayLightnessCurve: CurveAnchor[];
|
|
17
|
+
graySaturationCurve: CurveAnchor[];
|
|
18
|
+
scaleCurves: Record<string, { lightness: CurveAnchor[]; saturation: CurveAnchor[] }>;
|
|
19
|
+
curveOffset: Record<string, number>;
|
|
20
|
+
overrides: Record<string, string>;
|
|
21
|
+
snappedScales: string[];
|
|
22
|
+
emptyMode?: 'solid' | 'gradient';
|
|
23
|
+
emptyStep?: string;
|
|
24
|
+
gradientStyle?: GradientStyle;
|
|
25
|
+
gradientAngle?: number;
|
|
26
|
+
gradientReverse?: boolean;
|
|
27
|
+
gradientStops?: GradientStop[];
|
|
28
|
+
gradientSize?: 'page' | 'window';
|
|
29
|
+
anchorToBase?: boolean;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export interface TokenFile {
|
|
33
|
+
name: string;
|
|
34
|
+
createdAt: string;
|
|
35
|
+
updatedAt: string;
|
|
36
|
+
editorConfigs: Record<string, PaletteConfig>;
|
|
37
|
+
cssVariables: Record<string, string>;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export interface TokenFileMeta {
|
|
41
|
+
name: string;
|
|
42
|
+
fileName: string;
|
|
43
|
+
updatedAt: string;
|
|
44
|
+
isActive: boolean;
|
|
45
|
+
}
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import VisualsTab from '../showcase/VisualsTab.svelte';
|
|
3
|
+
|
|
4
|
+
const inOverlay = typeof window !== 'undefined' && window.parent !== window;
|
|
5
|
+
|
|
6
|
+
function closeOverlay() {
|
|
7
|
+
try {
|
|
8
|
+
window.parent.postMessage({ type: 'lt-overlay-close' }, window.location.origin);
|
|
9
|
+
} catch {
|
|
10
|
+
// cross-origin parent — shouldn't happen, but fall back to a noop
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
</script>
|
|
14
|
+
|
|
15
|
+
<div class="admin-page">
|
|
16
|
+
<div class="admin-bar">
|
|
17
|
+
<div class="bar-left">
|
|
18
|
+
{#if inOverlay}
|
|
19
|
+
<button class="back-link as-button" on:click={closeOverlay}>
|
|
20
|
+
<i class="fas fa-times"></i>
|
|
21
|
+
<span>Close</span>
|
|
22
|
+
</button>
|
|
23
|
+
{:else}
|
|
24
|
+
<a href="/" class="back-link">
|
|
25
|
+
<i class="fas fa-arrow-left"></i>
|
|
26
|
+
<span>Back to site</span>
|
|
27
|
+
</a>
|
|
28
|
+
{/if}
|
|
29
|
+
<span class="admin-label">Design System</span>
|
|
30
|
+
</div>
|
|
31
|
+
|
|
32
|
+
<div class="bar-right"></div>
|
|
33
|
+
</div>
|
|
34
|
+
|
|
35
|
+
<VisualsTab />
|
|
36
|
+
</div>
|
|
37
|
+
|
|
38
|
+
<style>
|
|
39
|
+
@import '../showcase/editor.css';
|
|
40
|
+
|
|
41
|
+
.admin-page {
|
|
42
|
+
min-height: 100vh;
|
|
43
|
+
background: black;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
.admin-bar {
|
|
47
|
+
display: flex;
|
|
48
|
+
align-items: center;
|
|
49
|
+
gap: var(--space-16);
|
|
50
|
+
padding: var(--space-10) var(--space-16);
|
|
51
|
+
background: black;
|
|
52
|
+
border-bottom: 1px solid var(--ui-border-faint);
|
|
53
|
+
min-height: 52px;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
.bar-left {
|
|
57
|
+
display: flex;
|
|
58
|
+
align-items: center;
|
|
59
|
+
gap: var(--space-16);
|
|
60
|
+
min-width: 0;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
.bar-right {
|
|
64
|
+
/* Reserved for future right-aligned controls; keeps grid balanced. */
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
.back-link {
|
|
68
|
+
display: flex;
|
|
69
|
+
align-items: center;
|
|
70
|
+
gap: var(--space-6);
|
|
71
|
+
color: var(--ui-text-tertiary);
|
|
72
|
+
text-decoration: none;
|
|
73
|
+
font-size: var(--font-md);
|
|
74
|
+
transition: color var(--transition-fast);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
.back-link:hover {
|
|
78
|
+
color: var(--ui-text-primary);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
.back-link.as-button {
|
|
82
|
+
background: none;
|
|
83
|
+
border: none;
|
|
84
|
+
padding: 0;
|
|
85
|
+
cursor: pointer;
|
|
86
|
+
font-family: inherit;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
.admin-label {
|
|
90
|
+
font-size: var(--font-md);
|
|
91
|
+
font-weight: var(--font-weight-semibold);
|
|
92
|
+
color: var(--ui-text-secondary);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
@media (max-width: 1100px) {
|
|
96
|
+
.admin-label {
|
|
97
|
+
display: none;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
</style>
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import ComponentsTab from '../showcase/ComponentsTab.svelte';
|
|
3
|
+
|
|
4
|
+
let selectedComponent = 'standardButtons';
|
|
5
|
+
|
|
6
|
+
const componentNavItems = [
|
|
7
|
+
{ id: 'choiceButtons', label: 'Choice Sets', icon: 'fas fa-hand-pointer' },
|
|
8
|
+
{ id: 'standardButtons', label: 'Button', icon: 'fas fa-square' },
|
|
9
|
+
{ id: 'notifications', label: 'Notification', icon: 'fas fa-bell' },
|
|
10
|
+
{ id: 'dialog', label: 'Dialog', icon: 'fas fa-window-restore' },
|
|
11
|
+
{ id: 'radioButtons', label: 'Radio Button', icon: 'fas fa-dot-circle' },
|
|
12
|
+
{ id: 'cards', label: 'Card', icon: 'fas fa-id-card' },
|
|
13
|
+
{ id: 'traitBadges', label: 'Trait Badge', icon: 'fas fa-tag' },
|
|
14
|
+
{ id: 'inlineEdit', label: 'Inline Edit Actions', icon: 'fas fa-pen' },
|
|
15
|
+
{ id: 'sectionDivider', label: 'Section Divider', icon: 'fas fa-minus' },
|
|
16
|
+
{ id: 'collapsible', label: 'Collapsible Section', icon: 'fas fa-chevron-down' },
|
|
17
|
+
{ id: 'tabBar', label: 'Tab Bar', icon: 'fas fa-columns' },
|
|
18
|
+
{ id: 'tooltip', label: 'Tooltip', icon: 'fas fa-comment-dots' },
|
|
19
|
+
{ id: 'progressBar', label: 'Progress Bar', icon: 'fas fa-tasks' }
|
|
20
|
+
];
|
|
21
|
+
</script>
|
|
22
|
+
|
|
23
|
+
<!--
|
|
24
|
+
Site-level Components showcase page. Wrapped in .admin-page so the
|
|
25
|
+
ComponentsTab demo chrome (labels, section wrappers) can resolve its
|
|
26
|
+
--ui-* custom properties from editor.css. The actual components inside
|
|
27
|
+
still read the user's design tokens, so live edits in the overlay
|
|
28
|
+
editor flow straight through to this page.
|
|
29
|
+
-->
|
|
30
|
+
<div class="admin-page components-shell">
|
|
31
|
+
<nav class="sidebar">
|
|
32
|
+
<div class="sidebar-header">Components</div>
|
|
33
|
+
<div class="nav-items">
|
|
34
|
+
{#each componentNavItems as item}
|
|
35
|
+
{#if item.id.startsWith('divider-')}
|
|
36
|
+
<div class="nav-divider-label"><span>{item.label}</span></div>
|
|
37
|
+
{:else}
|
|
38
|
+
<button
|
|
39
|
+
class="nav-item"
|
|
40
|
+
class:active={selectedComponent === item.id}
|
|
41
|
+
on:click={() => selectedComponent = item.id}
|
|
42
|
+
>
|
|
43
|
+
<i class={item.icon}></i>
|
|
44
|
+
<span>{item.label}</span>
|
|
45
|
+
</button>
|
|
46
|
+
{/if}
|
|
47
|
+
{/each}
|
|
48
|
+
</div>
|
|
49
|
+
</nav>
|
|
50
|
+
|
|
51
|
+
<main class="content">
|
|
52
|
+
<ComponentsTab {selectedComponent} />
|
|
53
|
+
</main>
|
|
54
|
+
</div>
|
|
55
|
+
|
|
56
|
+
<style>
|
|
57
|
+
@import '../showcase/editor.css';
|
|
58
|
+
@import '../styles/variables.css';
|
|
59
|
+
|
|
60
|
+
.components-shell {
|
|
61
|
+
display: grid;
|
|
62
|
+
grid-template-columns: 240px minmax(0, 1fr);
|
|
63
|
+
min-height: 100vh;
|
|
64
|
+
width: 100%;
|
|
65
|
+
background: black;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
.sidebar {
|
|
69
|
+
position: sticky;
|
|
70
|
+
top: 0;
|
|
71
|
+
height: 100vh;
|
|
72
|
+
overflow: hidden;
|
|
73
|
+
background: black;
|
|
74
|
+
border-right: 1px solid var(--ui-border-faint);
|
|
75
|
+
display: flex;
|
|
76
|
+
flex-direction: column;
|
|
77
|
+
min-width: 0;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
.sidebar-header {
|
|
81
|
+
padding: var(--space-16) var(--space-16) var(--space-12);
|
|
82
|
+
font-size: var(--font-lg);
|
|
83
|
+
font-weight: var(--font-weight-bold);
|
|
84
|
+
color: var(--ui-text-primary);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
.nav-items {
|
|
88
|
+
display: flex;
|
|
89
|
+
flex-direction: column;
|
|
90
|
+
gap: var(--space-2);
|
|
91
|
+
padding: 0 var(--space-8) var(--space-16);
|
|
92
|
+
flex: 1;
|
|
93
|
+
overflow-y: auto;
|
|
94
|
+
min-height: 0;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
.nav-item {
|
|
98
|
+
display: flex;
|
|
99
|
+
align-items: center;
|
|
100
|
+
gap: var(--space-8);
|
|
101
|
+
width: 100%;
|
|
102
|
+
padding: var(--space-6) var(--space-12) var(--space-6) var(--space-16);
|
|
103
|
+
background: none;
|
|
104
|
+
border: none;
|
|
105
|
+
border-radius: var(--radius-md);
|
|
106
|
+
color: var(--ui-text-tertiary);
|
|
107
|
+
font-size: var(--font-md);
|
|
108
|
+
cursor: pointer;
|
|
109
|
+
text-align: left;
|
|
110
|
+
transition: all var(--transition-fast);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
.nav-item:hover {
|
|
114
|
+
color: var(--ui-text-secondary);
|
|
115
|
+
background: var(--ui-hover);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
.nav-item.active {
|
|
119
|
+
color: var(--ui-text-primary);
|
|
120
|
+
background: var(--ui-surface-high);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
.nav-item i {
|
|
124
|
+
width: 1.25rem;
|
|
125
|
+
text-align: center;
|
|
126
|
+
font-size: var(--font-md);
|
|
127
|
+
opacity: 0.7;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
.nav-divider-label {
|
|
131
|
+
padding: var(--space-12) var(--space-12) var(--space-4);
|
|
132
|
+
font-size: var(--font-xs);
|
|
133
|
+
font-weight: var(--font-weight-semibold);
|
|
134
|
+
color: var(--ui-text-tertiary);
|
|
135
|
+
text-transform: uppercase;
|
|
136
|
+
letter-spacing: 0.05em;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
.content {
|
|
140
|
+
padding: var(--space-24) var(--space-32);
|
|
141
|
+
overflow-y: auto;
|
|
142
|
+
background: black;
|
|
143
|
+
min-width: 0;
|
|
144
|
+
max-width: 1280px;
|
|
145
|
+
}
|
|
146
|
+
</style>
|