@vendoai/vendo 0.4.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/LICENSE +202 -0
- package/README.md +28 -0
- package/bin/vendo.mjs +4 -0
- package/dist/ai-sdk.d.ts +35 -0
- package/dist/ai-sdk.js +62 -0
- package/dist/auth-presets/auth-js.d.ts +14 -0
- package/dist/auth-presets/auth-js.js +91 -0
- package/dist/auth-presets/auth0.d.ts +23 -0
- package/dist/auth-presets/auth0.js +98 -0
- package/dist/auth-presets/clerk.d.ts +23 -0
- package/dist/auth-presets/clerk.js +65 -0
- package/dist/auth-presets/conformance.d.ts +42 -0
- package/dist/auth-presets/conformance.js +139 -0
- package/dist/auth-presets/identity.d.ts +56 -0
- package/dist/auth-presets/identity.js +152 -0
- package/dist/auth-presets/index.d.ts +14 -0
- package/dist/auth-presets/index.js +12 -0
- package/dist/auth-presets/jwt.d.ts +16 -0
- package/dist/auth-presets/jwt.js +54 -0
- package/dist/auth-presets/shared.d.ts +35 -0
- package/dist/auth-presets/shared.js +1 -0
- package/dist/auth-presets/supabase.d.ts +42 -0
- package/dist/auth-presets/supabase.js +229 -0
- package/dist/byo-approvals.d.ts +37 -0
- package/dist/byo-approvals.js +189 -0
- package/dist/capability-misses.d.ts +38 -0
- package/dist/capability-misses.js +172 -0
- package/dist/catalog.d.ts +19 -0
- package/dist/catalog.js +152 -0
- package/dist/cli/cloud/args.d.ts +3 -0
- package/dist/cli/cloud/args.js +32 -0
- package/dist/cli/cloud/auth.d.ts +17 -0
- package/dist/cli/cloud/auth.js +123 -0
- package/dist/cli/cloud/client.d.ts +28 -0
- package/dist/cli/cloud/client.js +133 -0
- package/dist/cli/cloud/command.d.ts +22 -0
- package/dist/cli/cloud/command.js +73 -0
- package/dist/cli/cloud/deploy.d.ts +26 -0
- package/dist/cli/cloud/deploy.js +187 -0
- package/dist/cli/cloud/device-login.d.ts +32 -0
- package/dist/cli/cloud/device-login.js +259 -0
- package/dist/cli/cloud/index.d.ts +5 -0
- package/dist/cli/cloud/index.js +59 -0
- package/dist/cli/cloud/keys.d.ts +2 -0
- package/dist/cli/cloud/keys.js +31 -0
- package/dist/cli/cloud/members.d.ts +3 -0
- package/dist/cli/cloud/members.js +25 -0
- package/dist/cli/cloud/output.d.ts +4 -0
- package/dist/cli/cloud/output.js +12 -0
- package/dist/cli/cloud/pending-claim.d.ts +29 -0
- package/dist/cli/cloud/pending-claim.js +38 -0
- package/dist/cli/cloud/read.d.ts +3 -0
- package/dist/cli/cloud/read.js +12 -0
- package/dist/cli/cloud/services.d.ts +2 -0
- package/dist/cli/cloud/services.js +9 -0
- package/dist/cli/cloud/session.d.ts +13 -0
- package/dist/cli/cloud/session.js +41 -0
- package/dist/cli/cloud-init.d.ts +60 -0
- package/dist/cli/cloud-init.js +149 -0
- package/dist/cli/dep-versions.d.ts +26 -0
- package/dist/cli/dep-versions.js +77 -0
- package/dist/cli/doctor-codes.d.ts +58 -0
- package/dist/cli/doctor-codes.js +60 -0
- package/dist/cli/doctor-live.d.ts +67 -0
- package/dist/cli/doctor-live.js +169 -0
- package/dist/cli/doctor.d.ts +52 -0
- package/dist/cli/doctor.js +467 -0
- package/dist/cli/eject.d.ts +29 -0
- package/dist/cli/eject.js +141 -0
- package/dist/cli/extract/apply.d.ts +26 -0
- package/dist/cli/extract/apply.js +123 -0
- package/dist/cli/extract/claude-cli-harness.d.ts +18 -0
- package/dist/cli/extract/claude-cli-harness.js +132 -0
- package/dist/cli/extract/claude-harness.d.ts +14 -0
- package/dist/cli/extract/claude-harness.js +127 -0
- package/dist/cli/extract/codex-cli-harness.d.ts +31 -0
- package/dist/cli/extract/codex-cli-harness.js +154 -0
- package/dist/cli/extract/delegate.d.ts +21 -0
- package/dist/cli/extract/delegate.js +77 -0
- package/dist/cli/extract/extraction.d.ts +65 -0
- package/dist/cli/extract/extraction.js +300 -0
- package/dist/cli/extract/gateway-fuel.d.ts +69 -0
- package/dist/cli/extract/gateway-fuel.js +69 -0
- package/dist/cli/extract/harness.d.ts +114 -0
- package/dist/cli/extract/harness.js +84 -0
- package/dist/cli/extract/index.d.ts +9 -0
- package/dist/cli/extract/index.js +9 -0
- package/dist/cli/extract/npx-engine-harness.d.ts +73 -0
- package/dist/cli/extract/npx-engine-harness.js +225 -0
- package/dist/cli/extract/stages.d.ts +149 -0
- package/dist/cli/extract/stages.js +398 -0
- package/dist/cli/framework.d.ts +8 -0
- package/dist/cli/framework.js +36 -0
- package/dist/cli/init-auth.d.ts +67 -0
- package/dist/cli/init-auth.js +142 -0
- package/dist/cli/init-scaffolds.d.ts +28 -0
- package/dist/cli/init-scaffolds.js +235 -0
- package/dist/cli/init.d.ts +115 -0
- package/dist/cli/init.js +1008 -0
- package/dist/cli/mcp/index.d.ts +7 -0
- package/dist/cli/mcp/index.js +59 -0
- package/dist/cli/mcp/registry.d.ts +9 -0
- package/dist/cli/mcp/registry.js +117 -0
- package/dist/cli/mcp/server-json.d.ts +12 -0
- package/dist/cli/mcp/server-json.js +65 -0
- package/dist/cli/mcp/server.schema.json +574 -0
- package/dist/cli/mcp/verify-domain.d.ts +11 -0
- package/dist/cli/mcp/verify-domain.js +48 -0
- package/dist/cli/playground/app/embed-entry.d.ts +20 -0
- package/dist/cli/playground/app/embed-entry.js +46 -0
- package/dist/cli/playground/app/fake-client.d.ts +3 -0
- package/dist/cli/playground/app/fake-client.js +215 -0
- package/dist/cli/playground/app/fixtures.d.ts +47 -0
- package/dist/cli/playground/app/fixtures.js +472 -0
- package/dist/cli/playground/app/main.d.ts +1 -0
- package/dist/cli/playground/app/main.js +108 -0
- package/dist/cli/playground/app/scenario-mount.d.ts +13 -0
- package/dist/cli/playground/app/scenario-mount.js +48 -0
- package/dist/cli/playground/app/scenarios.d.ts +25 -0
- package/dist/cli/playground/app/scenarios.js +152 -0
- package/dist/cli/playground/app/theme-editor.d.ts +13 -0
- package/dist/cli/playground/app/theme-editor.js +149 -0
- package/dist/cli/playground/app/theme-state.d.ts +29 -0
- package/dist/cli/playground/app/theme-state.js +151 -0
- package/dist/cli/playground/bundle.gen.d.ts +2 -0
- package/dist/cli/playground/bundle.gen.js +2 -0
- package/dist/cli/playground/embed-bundle.gen.d.ts +2 -0
- package/dist/cli/playground/embed-bundle.gen.js +2 -0
- package/dist/cli/playground.d.ts +33 -0
- package/dist/cli/playground.js +120 -0
- package/dist/cli/pretty.d.ts +48 -0
- package/dist/cli/pretty.js +330 -0
- package/dist/cli/refine.d.ts +43 -0
- package/dist/cli/refine.js +181 -0
- package/dist/cli/semantics.d.ts +31 -0
- package/dist/cli/semantics.js +122 -0
- package/dist/cli/shared.d.ts +68 -0
- package/dist/cli/shared.js +184 -0
- package/dist/cli/sync.d.ts +36 -0
- package/dist/cli/sync.js +202 -0
- package/dist/cli/theme/color.d.ts +15 -0
- package/dist/cli/theme/color.js +188 -0
- package/dist/cli/theme/css-vars.d.ts +13 -0
- package/dist/cli/theme/css-vars.js +40 -0
- package/dist/cli/theme/entry-candidates.d.ts +5 -0
- package/dist/cli/theme/entry-candidates.js +22 -0
- package/dist/cli/theme/extract-theme.d.ts +184 -0
- package/dist/cli/theme/extract-theme.js +358 -0
- package/dist/cli/theme/walk.d.ts +2 -0
- package/dist/cli/theme/walk.js +24 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +333 -0
- package/dist/cloud-apps.d.ts +17 -0
- package/dist/cloud-apps.js +46 -0
- package/dist/cloud-console.d.ts +32 -0
- package/dist/cloud-console.js +72 -0
- package/dist/cloud-tools.d.ts +23 -0
- package/dist/cloud-tools.js +196 -0
- package/dist/connections.d.ts +71 -0
- package/dist/connections.js +192 -0
- package/dist/deployment-identity.d.ts +12 -0
- package/dist/deployment-identity.js +69 -0
- package/dist/dev-creds/model.d.ts +65 -0
- package/dist/dev-creds/model.js +159 -0
- package/dist/dev-creds/resolve.d.ts +36 -0
- package/dist/dev-creds/resolve.js +57 -0
- package/dist/hosted-store.d.ts +52 -0
- package/dist/hosted-store.js +322 -0
- package/dist/index.d.ts +13 -0
- package/dist/index.js +1 -0
- package/dist/mastra.d.ts +46 -0
- package/dist/mastra.js +88 -0
- package/dist/react.d.ts +10 -0
- package/dist/react.js +32 -0
- package/dist/refine.d.ts +244 -0
- package/dist/refine.js +599 -0
- package/dist/remixable.d.ts +18 -0
- package/dist/remixable.js +42 -0
- package/dist/runtime-capture.d.ts +18 -0
- package/dist/runtime-capture.js +111 -0
- package/dist/sandbox-wire.d.ts +101 -0
- package/dist/sandbox-wire.js +101 -0
- package/dist/sandbox.d.ts +38 -0
- package/dist/sandbox.js +389 -0
- package/dist/server.d.ts +216 -0
- package/dist/server.js +1320 -0
- package/dist/sync-impact.d.ts +14 -0
- package/dist/sync-impact.js +75 -0
- package/dist/turn-liveness.d.ts +33 -0
- package/dist/turn-liveness.js +105 -0
- package/dist/wire/approvals.d.ts +10 -0
- package/dist/wire/approvals.js +59 -0
- package/dist/wire/apps.d.ts +5 -0
- package/dist/wire/apps.js +155 -0
- package/dist/wire/automations.d.ts +5 -0
- package/dist/wire/automations.js +57 -0
- package/dist/wire/box.d.ts +3 -0
- package/dist/wire/box.js +268 -0
- package/dist/wire/connections.d.ts +5 -0
- package/dist/wire/connections.js +47 -0
- package/dist/wire/context.d.ts +22 -0
- package/dist/wire/context.js +190 -0
- package/dist/wire/doctor.d.ts +19 -0
- package/dist/wire/doctor.js +116 -0
- package/dist/wire/misc.d.ts +18 -0
- package/dist/wire/misc.js +227 -0
- package/dist/wire/shared.d.ts +158 -0
- package/dist/wire/shared.js +134 -0
- package/dist/wire/threads.d.ts +3 -0
- package/dist/wire/threads.js +68 -0
- package/package.json +151 -0
- package/skills/vendo-setup/SKILL.md +128 -0
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The scenario registry: each entry is one linkable URL (`#<id>`) mounting a
|
|
3
|
+
* REAL chrome surface against scripted data. The nav renders from this list;
|
|
4
|
+
* the harness (main.tsx) supplies the provider, fake client, scripted
|
|
5
|
+
* transport, and the auto-sent opening turn.
|
|
6
|
+
*/
|
|
7
|
+
import type { DirectorScript } from "@vendoai/ui";
|
|
8
|
+
import { type ReactElement } from "react";
|
|
9
|
+
import { type PlaygroundFixtures } from "./fixtures.js";
|
|
10
|
+
export interface PlaygroundScenario {
|
|
11
|
+
id: string;
|
|
12
|
+
group: "Overlay" | "Thread" | "Approvals" | "Activities" | "Slot" | "Page" | "Mobile";
|
|
13
|
+
title: string;
|
|
14
|
+
description: string;
|
|
15
|
+
/** Scripted turns this scenario's sends play (ScriptedTransport). */
|
|
16
|
+
script?: DirectorScript;
|
|
17
|
+
/** ScriptedTransport pacing multiplier (1 = authored pacing). */
|
|
18
|
+
speed?: number;
|
|
19
|
+
/** The opening user turn the harness auto-sends into the composer. */
|
|
20
|
+
autoSend?: string;
|
|
21
|
+
/** Wire fixture override; defaults to the shared populated set. */
|
|
22
|
+
fixtures?: () => PlaygroundFixtures;
|
|
23
|
+
render(): ReactElement;
|
|
24
|
+
}
|
|
25
|
+
export declare const scenarios: PlaygroundScenario[];
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { VendoActivities, VendoOverlay, VendoPage, VendoSlot, VendoThread } from "@vendoai/ui/chrome";
|
|
3
|
+
import { useMemo } from "react";
|
|
4
|
+
import { approvalScript, brokenViewPayload, connectScript, emptyActivitiesFixtures, renewalsViewPayload, streamingScript, viewScript, } from "./fixtures.js";
|
|
5
|
+
/** Stand-in for the host's own markup: the slot's fallback/exit frame. */
|
|
6
|
+
function HostOriginalCard() {
|
|
7
|
+
return (_jsxs("div", { style: {
|
|
8
|
+
border: "1px solid #e3e0da",
|
|
9
|
+
borderRadius: 12,
|
|
10
|
+
padding: "18px 20px",
|
|
11
|
+
background: "#fffdf9",
|
|
12
|
+
fontFamily: "Inter, system-ui, sans-serif",
|
|
13
|
+
}, children: [_jsx("div", { style: { fontSize: 13, fontWeight: 650, color: "#14151a", marginBottom: 6 }, children: "Quarterly summary" }), _jsx("div", { style: { fontSize: 12.5, color: "#6b6c72", lineHeight: 1.5 }, children: "This is the host product\u2019s original component. When a generated view cannot render, Vendo falls back to exactly this markup \u2014 the host surface is never sacrificed." })] }));
|
|
14
|
+
}
|
|
15
|
+
/** The phone-viewport iframe. Its src is fixed at mount (carrying the theme
|
|
16
|
+
* the page loaded with); later theme edits stream in over postMessage from the
|
|
17
|
+
* harness, so slider scrubbing never reloads the frame. */
|
|
18
|
+
function PhoneEmbed() {
|
|
19
|
+
const src = useMemo(() => {
|
|
20
|
+
const params = new URLSearchParams(window.location.search);
|
|
21
|
+
params.set("embed", "1");
|
|
22
|
+
return `/?${params.toString()}#overlay-open`;
|
|
23
|
+
}, []);
|
|
24
|
+
return (_jsx("iframe", { title: "Vendo playground \u2014 phone viewport", src: src, style: { width: "100%", height: "100%", border: "none" } }));
|
|
25
|
+
}
|
|
26
|
+
function ThreadPane() {
|
|
27
|
+
return (_jsx("div", { style: { height: "min(680px, 78vh)", display: "flex", flexDirection: "column" }, children: _jsx(VendoThread, { greeting: "What can I help you build?", suggestions: ["Which renewals are at risk?", "Build me a renewals view", "Post the digest to #renewals"] }) }));
|
|
28
|
+
}
|
|
29
|
+
export const scenarios = [
|
|
30
|
+
{
|
|
31
|
+
id: "overlay-launcher",
|
|
32
|
+
group: "Overlay",
|
|
33
|
+
title: "Closed, with launcher",
|
|
34
|
+
description: "The default drop-in: a brand-styled launcher pill in the corner. Click it — the overlay opens on a fresh conversation.",
|
|
35
|
+
script: viewScript(),
|
|
36
|
+
render: () => _jsx(VendoOverlay, {}),
|
|
37
|
+
},
|
|
38
|
+
{
|
|
39
|
+
id: "overlay-open",
|
|
40
|
+
group: "Overlay",
|
|
41
|
+
title: "Open, mid-conversation",
|
|
42
|
+
description: "The overlay mid-build: tool beats, then a generated view lands inline and the turn wraps up.",
|
|
43
|
+
script: viewScript(),
|
|
44
|
+
autoSend: "Build me a view of my upcoming renewals.",
|
|
45
|
+
render: () => _jsx(VendoOverlay, { defaultOpen: true }),
|
|
46
|
+
},
|
|
47
|
+
{
|
|
48
|
+
id: "overlay-streaming",
|
|
49
|
+
group: "Overlay",
|
|
50
|
+
title: "Streaming turn",
|
|
51
|
+
description: "A long answer streaming into the overlay at authored pacing — the state you see while the agent is talking.",
|
|
52
|
+
script: streamingScript(),
|
|
53
|
+
autoSend: "Where do my renewals stand this month?",
|
|
54
|
+
render: () => _jsx(VendoOverlay, { defaultOpen: true }),
|
|
55
|
+
},
|
|
56
|
+
{
|
|
57
|
+
id: "thread-streaming",
|
|
58
|
+
group: "Thread",
|
|
59
|
+
title: "Streaming text",
|
|
60
|
+
description: "The bare thread surface while a reply streams: tool chip, live text, stop affordance.",
|
|
61
|
+
script: streamingScript(),
|
|
62
|
+
autoSend: "Where do my renewals stand this month?",
|
|
63
|
+
render: () => _jsx(ThreadPane, {}),
|
|
64
|
+
},
|
|
65
|
+
{
|
|
66
|
+
id: "thread-view",
|
|
67
|
+
group: "Thread",
|
|
68
|
+
title: "Generated view arriving",
|
|
69
|
+
description: "A build turn: the agent reads host data, then a vendo-genui view streams into the transcript and finishes.",
|
|
70
|
+
script: viewScript(),
|
|
71
|
+
autoSend: "Build me a view of my upcoming renewals.",
|
|
72
|
+
render: () => _jsx(ThreadPane, {}),
|
|
73
|
+
},
|
|
74
|
+
{
|
|
75
|
+
id: "thread-connect",
|
|
76
|
+
group: "Thread",
|
|
77
|
+
title: "Connect card",
|
|
78
|
+
description: "A connector call that needs the user's own account first: the turn ends with an inline connect card (04-actions §3).",
|
|
79
|
+
script: connectScript(),
|
|
80
|
+
autoSend: "Post this week's renewals digest to #renewals.",
|
|
81
|
+
render: () => _jsx(ThreadPane, {}),
|
|
82
|
+
},
|
|
83
|
+
{
|
|
84
|
+
id: "approval-flow",
|
|
85
|
+
group: "Approvals",
|
|
86
|
+
title: "Pending → approved → resumed",
|
|
87
|
+
description: "A write action parks the turn on an in-thread approval card. Approve it and the SAME turn resumes: the tool runs and the agent confirms.",
|
|
88
|
+
script: approvalScript(),
|
|
89
|
+
autoSend: "Give my team a heads-up about the at-risk renewals.",
|
|
90
|
+
render: () => _jsx(ThreadPane, {}),
|
|
91
|
+
},
|
|
92
|
+
{
|
|
93
|
+
id: "activities",
|
|
94
|
+
group: "Activities",
|
|
95
|
+
title: "Approvals queue + activity feed",
|
|
96
|
+
description: "The shelf's VendoActivities piece: pending approvals as actionable cards on top, the humanized recent-activity feed below (ui-usage-dx §2).",
|
|
97
|
+
render: () => _jsx(VendoActivities, { pollMs: 0 }),
|
|
98
|
+
},
|
|
99
|
+
{
|
|
100
|
+
id: "activities-empty",
|
|
101
|
+
group: "Activities",
|
|
102
|
+
title: "Empty state",
|
|
103
|
+
description: "The same piece before the agent has done anything: a quiet one-liner instead of an invisible component — hosts place this in their own pages.",
|
|
104
|
+
fixtures: emptyActivitiesFixtures,
|
|
105
|
+
render: () => _jsx(VendoActivities, { pollMs: 0 }),
|
|
106
|
+
},
|
|
107
|
+
{
|
|
108
|
+
id: "slot-empty",
|
|
109
|
+
group: "Slot",
|
|
110
|
+
title: "Empty ghost",
|
|
111
|
+
description: "A VendoSlot with nothing pinned: the ghost skeleton with a real “Design a view” CTA.",
|
|
112
|
+
render: () => _jsx(VendoSlot, { id: "playground-empty" }),
|
|
113
|
+
},
|
|
114
|
+
{
|
|
115
|
+
id: "slot-filled",
|
|
116
|
+
group: "Slot",
|
|
117
|
+
title: "Filled with a pinned view",
|
|
118
|
+
description: "The same slot holding a pinned generated view — the user's own “Renewals radar” mounted in the host page.",
|
|
119
|
+
render: () => (_jsx(VendoSlot, { id: "playground-filled", pin: { payload: renewalsViewPayload() }, children: _jsx(HostOriginalCard, {}) })),
|
|
120
|
+
},
|
|
121
|
+
{
|
|
122
|
+
id: "slot-broken",
|
|
123
|
+
group: "Slot",
|
|
124
|
+
title: "Broken view → host fallback",
|
|
125
|
+
description: "A pinned view that cannot render. The error boundary keeps the host's original component visible — never a blank hole (06-apps §8).",
|
|
126
|
+
render: () => (_jsx(VendoSlot, { id: "playground-broken", pin: { payload: brokenViewPayload() }, children: _jsx(HostOriginalCard, {}) })),
|
|
127
|
+
},
|
|
128
|
+
{
|
|
129
|
+
id: "page",
|
|
130
|
+
group: "Page",
|
|
131
|
+
title: "Workspace console",
|
|
132
|
+
description: "The full VendoPage: conversations with history, the waiting-on-you approval strip, apps, automations, accounts, and activity.",
|
|
133
|
+
script: viewScript(),
|
|
134
|
+
render: () => _jsx(VendoPage, {}),
|
|
135
|
+
},
|
|
136
|
+
{
|
|
137
|
+
id: "mobile",
|
|
138
|
+
group: "Mobile",
|
|
139
|
+
title: "Phone viewport",
|
|
140
|
+
description: "The overlay's Intercom-style full-bleed takeover at a phone viewport (390×760), embedded live below.",
|
|
141
|
+
script: viewScript(),
|
|
142
|
+
render: () => (_jsx("div", { style: { display: "flex", justifyContent: "center", padding: "12px 0" }, children: _jsx("div", { style: {
|
|
143
|
+
width: 390,
|
|
144
|
+
height: 760,
|
|
145
|
+
border: "10px solid #14151a",
|
|
146
|
+
borderRadius: 34,
|
|
147
|
+
overflow: "hidden",
|
|
148
|
+
boxShadow: "0 18px 50px rgba(20, 21, 26, 0.25)",
|
|
149
|
+
background: "#fff",
|
|
150
|
+
}, children: _jsx(PhoneEmbed, {}) }) })),
|
|
151
|
+
},
|
|
152
|
+
];
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The playground's theme editor: a floating card top-right (Yousef's pick C)
|
|
3
|
+
* that collapses to a "Theme" pill, and a grabber-handled bottom sheet at
|
|
4
|
+
* phone widths. Every VendoTheme token is editable live; state lives in
|
|
5
|
+
* main.tsx so the whole page (and the embed iframe) re-themes together.
|
|
6
|
+
*/
|
|
7
|
+
import type { VendoTheme } from "@vendoai/core";
|
|
8
|
+
export declare function ThemeEditor({ theme, onChange }: {
|
|
9
|
+
theme: VendoTheme;
|
|
10
|
+
onChange(next: VendoTheme): void;
|
|
11
|
+
}): import("react").JSX.Element;
|
|
12
|
+
/** Keep the page's on-demand Google Fonts <link> in sync with the theme. */
|
|
13
|
+
export declare function useGoogleFont(fontFamily: string): void;
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
|
+
import { useEffect, useState } from "react";
|
|
3
|
+
import { curatedFonts, fontStack, googleFontHref, primaryFontFamily, themeJson, themePresets, } from "./theme-state.js";
|
|
4
|
+
const EDITOR_CSS = `
|
|
5
|
+
/* Above the chrome's own overlay/takeover layers (they top out at
|
|
6
|
+
2147483100) — the editor is harness tooling and must stay reachable
|
|
7
|
+
while the overlay scenario is open. */
|
|
8
|
+
.te-card, .te-pill { position: fixed; top: 16px; right: 16px; z-index: 2147483200; font-family: Inter, system-ui, sans-serif; }
|
|
9
|
+
.te-card { width: 302px; max-height: calc(100vh - 32px); display: flex; flex-direction: column; background: #faf6ee; border: 1px solid #e3ddd2; border-radius: 14px; box-shadow: 0 18px 44px rgba(20, 21, 26, 0.18); transform-origin: top right; transition: transform 320ms cubic-bezier(0.2, 0.8, 0.2, 1), opacity 320ms cubic-bezier(0.2, 0.8, 0.2, 1); }
|
|
10
|
+
.te-card[data-closed="true"] { transform: scale(0.6) translateY(-8px); opacity: 0; pointer-events: none; }
|
|
11
|
+
.te-pill { display: inline-flex; align-items: center; gap: 7px; background: #14151a; color: #fffdf9; border: none; border-radius: 999px; font: 600 12.5px Inter, sans-serif; padding: 9px 15px; cursor: pointer; box-shadow: 0 10px 26px rgba(20, 21, 26, 0.25); opacity: 0; pointer-events: none; transition: opacity 320ms cubic-bezier(0.2, 0.8, 0.2, 1); }
|
|
12
|
+
.te-pill[data-shown="true"] { opacity: 1; pointer-events: auto; }
|
|
13
|
+
.te-dot { width: 13px; height: 13px; border-radius: 50%; background: conic-gradient(#e7683c, #f5c644, #55a06f, #2f5af5, #7c6cff, #e7683c); border: 1px solid rgba(255, 255, 255, 0.4); }
|
|
14
|
+
.te-grab { display: none; }
|
|
15
|
+
.te-head { display: flex; align-items: center; justify-content: space-between; padding: 12px 14px 10px; border-bottom: 1px solid #e3ddd2; flex-shrink: 0; }
|
|
16
|
+
.te-head b { font-size: 12.5px; color: #14151a; }
|
|
17
|
+
.te-head small { display: block; font-weight: 500; font-size: 10.5px; color: #8a8b92; margin-top: 2px; }
|
|
18
|
+
.te-x { border: none; background: none; color: #8a8b92; font-size: 14px; cursor: pointer; padding: 2px 6px; border-radius: 6px; line-height: 1; }
|
|
19
|
+
.te-x:hover { background: #f0e9dc; color: #14151a; }
|
|
20
|
+
.te-body { overflow-y: auto; padding: 10px 14px 12px; font-size: 12.5px; color: #14151a; }
|
|
21
|
+
.te-sec { margin-bottom: 14px; }
|
|
22
|
+
.te-label { font-size: 10px; font-weight: 700; text-transform: uppercase; letter-spacing: 0.08em; color: #8a8b92; margin-bottom: 7px; }
|
|
23
|
+
.te-presets { display: flex; flex-wrap: wrap; gap: 6px; }
|
|
24
|
+
.te-chip { display: inline-flex; align-items: center; gap: 6px; border: 1px solid #e3ddd2; background: #fffdf9; border-radius: 999px; padding: 4px 10px 4px 5px; font: 600 11.5px Inter, sans-serif; cursor: pointer; color: #14151a; }
|
|
25
|
+
.te-chip .te-swatch { width: 16px; height: 16px; border-radius: 50%; border: 1px solid rgba(0, 0, 0, 0.12); }
|
|
26
|
+
.te-chip[aria-pressed="true"] { border-color: #14151a; box-shadow: inset 0 0 0 1px #14151a; }
|
|
27
|
+
.te-crow { display: flex; align-items: center; gap: 8px; margin-bottom: 5px; }
|
|
28
|
+
.te-crow input[type="color"] { width: 24px; height: 24px; border: 1px solid #e3ddd2; border-radius: 7px; padding: 1px; background: #fffdf9; cursor: pointer; flex-shrink: 0; }
|
|
29
|
+
.te-crow span { flex: 1; font-weight: 550; font-size: 11.5px; }
|
|
30
|
+
.te-crow code { font: 500 10.5px ui-monospace, monospace; color: #8a8b92; }
|
|
31
|
+
.te-body select, .te-body input[type="text"] { width: 100%; font: 500 12px Inter, sans-serif; color: #14151a; background: #fffdf9; border: 1px solid #e3ddd2; border-radius: 8px; padding: 6px 8px; }
|
|
32
|
+
.te-body input[type="text"] { margin-top: 6px; }
|
|
33
|
+
.te-srow { display: flex; align-items: center; gap: 8px; margin-top: 6px; }
|
|
34
|
+
.te-srow label { font-size: 11.5px; font-weight: 550; width: 62px; flex-shrink: 0; }
|
|
35
|
+
.te-srow input[type="range"] { flex: 1; accent-color: #14151a; min-width: 0; }
|
|
36
|
+
.te-srow output { font: 500 10.5px ui-monospace, monospace; color: #8a8b92; width: 34px; text-align: right; flex-shrink: 0; }
|
|
37
|
+
.te-segs { display: grid; grid-template-columns: 1fr 1fr; gap: 10px; }
|
|
38
|
+
.te-seg { display: flex; background: #fffdf9; border: 1px solid #e3ddd2; border-radius: 9px; padding: 2px; gap: 2px; }
|
|
39
|
+
.te-seg button { flex: 1; border: none; background: none; font: 600 11px Inter, sans-serif; color: #6b6c72; padding: 5px 0; border-radius: 7px; cursor: pointer; }
|
|
40
|
+
.te-seg button[aria-pressed="true"] { background: #14151a; color: #fffdf9; }
|
|
41
|
+
.te-foot { display: flex; gap: 8px; padding: 10px 14px; border-top: 1px solid #e3ddd2; flex-shrink: 0; }
|
|
42
|
+
.te-foot button { flex: 1; font: 600 11.5px Inter, sans-serif; border-radius: 8px; padding: 7px 0; cursor: pointer; }
|
|
43
|
+
.te-copy { background: #14151a; color: #fffdf9; border: 1px solid #14151a; }
|
|
44
|
+
.te-link { background: #fffdf9; color: #14151a; border: 1px solid #e3ddd2; }
|
|
45
|
+
@media (prefers-reduced-motion: reduce) {
|
|
46
|
+
.te-card, .te-pill { transition: none; }
|
|
47
|
+
}
|
|
48
|
+
@media (max-width: 640px) {
|
|
49
|
+
.te-card { top: auto; right: 0; left: 0; bottom: 0; width: auto; max-height: 72vh; border-radius: 18px 18px 0 0; border-left: none; border-right: none; border-bottom: none; padding-bottom: env(safe-area-inset-bottom, 0px); transform-origin: bottom center; box-shadow: 0 -16px 44px rgba(20, 21, 26, 0.3); }
|
|
50
|
+
.te-card[data-closed="true"] { transform: translateY(105%); opacity: 1; }
|
|
51
|
+
.te-pill { top: auto; bottom: calc(env(safe-area-inset-bottom, 0px) + 16px); }
|
|
52
|
+
.te-grab { display: block; width: 40px; height: 4px; border-radius: 2px; background: #d7d2c7; margin: 8px auto 0; flex-shrink: 0; }
|
|
53
|
+
.te-x { padding: 6px 10px; }
|
|
54
|
+
}
|
|
55
|
+
`;
|
|
56
|
+
const COLOR_KEYS = ["background", "surface", "text", "muted", "accent", "accentText", "danger", "border"];
|
|
57
|
+
function samePreset(a, b) {
|
|
58
|
+
return JSON.stringify(a) === JSON.stringify(b);
|
|
59
|
+
}
|
|
60
|
+
/** "12px" → 12 (slider value); tolerates rem-less numeric strings. */
|
|
61
|
+
function px(value) {
|
|
62
|
+
return Number.parseFloat(value) || 0;
|
|
63
|
+
}
|
|
64
|
+
/** Selection-based copy for contexts where the async clipboard is missing or
|
|
65
|
+
* rejects (non-secure origins, unfocused automation documents). */
|
|
66
|
+
function copyViaSelection(value) {
|
|
67
|
+
const area = document.createElement("textarea");
|
|
68
|
+
area.value = value;
|
|
69
|
+
area.style.position = "fixed";
|
|
70
|
+
area.style.opacity = "0";
|
|
71
|
+
document.body.append(area);
|
|
72
|
+
area.select();
|
|
73
|
+
let copied = false;
|
|
74
|
+
try {
|
|
75
|
+
copied = document.execCommand("copy");
|
|
76
|
+
}
|
|
77
|
+
catch {
|
|
78
|
+
copied = false;
|
|
79
|
+
}
|
|
80
|
+
area.remove();
|
|
81
|
+
return copied;
|
|
82
|
+
}
|
|
83
|
+
function CopyButton({ className, label, copiedLabel, text }) {
|
|
84
|
+
const [copied, setCopied] = useState(false);
|
|
85
|
+
useEffect(() => {
|
|
86
|
+
if (!copied)
|
|
87
|
+
return;
|
|
88
|
+
const timer = setTimeout(() => setCopied(false), 1600);
|
|
89
|
+
return () => clearTimeout(timer);
|
|
90
|
+
}, [copied]);
|
|
91
|
+
const copy = async () => {
|
|
92
|
+
const value = text();
|
|
93
|
+
try {
|
|
94
|
+
await navigator.clipboard.writeText(value);
|
|
95
|
+
setCopied(true);
|
|
96
|
+
}
|
|
97
|
+
catch {
|
|
98
|
+
if (copyViaSelection(value))
|
|
99
|
+
setCopied(true);
|
|
100
|
+
}
|
|
101
|
+
};
|
|
102
|
+
return (_jsx("button", { type: "button", className: className, onClick: () => void copy(), children: copied ? copiedLabel : label }));
|
|
103
|
+
}
|
|
104
|
+
export function ThemeEditor({ theme, onChange }) {
|
|
105
|
+
const [open, setOpen] = useState(false);
|
|
106
|
+
const [freeFont, setFreeFont] = useState("");
|
|
107
|
+
const primary = primaryFontFamily(theme.typography.fontFamily);
|
|
108
|
+
const selectValue = curatedFonts.find((font) => primaryFontFamily(fontStack(font)) === primary) ?? "custom";
|
|
109
|
+
const set = (patch) => onChange({ ...theme, ...patch });
|
|
110
|
+
const setColor = (key, value) => set({ colors: { ...theme.colors, [key]: value } });
|
|
111
|
+
const setRadius = (key, value) => set({ radius: { ...theme.radius, [key]: `${value}px` } });
|
|
112
|
+
const applyFont = (family) => {
|
|
113
|
+
if (!family.trim())
|
|
114
|
+
return;
|
|
115
|
+
set({ typography: { ...theme.typography, fontFamily: fontStack(family) } });
|
|
116
|
+
};
|
|
117
|
+
return (_jsxs(_Fragment, { children: [_jsx("style", { children: EDITOR_CSS }), _jsxs("button", { type: "button", className: "te-pill", "data-shown": !open, inert: open, onClick: () => setOpen(true), "aria-label": "Open theme editor", children: [_jsx("span", { className: "te-dot" }), "Theme"] }), _jsxs("div", { className: "te-card", "data-closed": !open, role: "dialog", "aria-label": "Theme editor", inert: !open, children: [_jsx("div", { className: "te-grab" }), _jsxs("div", { className: "te-head", children: [_jsxs("div", { children: [_jsx("b", { children: "Theme editor" }), _jsx("small", { children: "edits every scenario live \u00B7 VendoTheme" })] }), _jsx("button", { type: "button", className: "te-x", onClick: () => setOpen(false), "aria-label": "Close theme editor", children: "\u2715" })] }), _jsxs("div", { className: "te-body", children: [_jsxs("div", { className: "te-sec", children: [_jsx("div", { className: "te-label", children: "Presets" }), _jsx("div", { className: "te-presets", children: themePresets.map((preset) => (_jsxs("button", { type: "button", className: "te-chip", "aria-pressed": samePreset(theme, preset.theme), onClick: () => onChange(preset.theme), children: [_jsx("span", { className: "te-swatch", style: { background: preset.theme.colors.accent } }), preset.name] }, preset.name))) })] }), _jsxs("div", { className: "te-sec", children: [_jsx("div", { className: "te-label", children: "Colors" }), COLOR_KEYS.map((key) => (_jsxs("div", { className: "te-crow", children: [_jsx("input", { type: "color", value: theme.colors[key], onChange: (event) => setColor(key, event.target.value), "aria-label": `${key} color` }), _jsx("span", { children: key }), _jsx("code", { children: theme.colors[key] })] }, key)))] }), _jsxs("div", { className: "te-sec", children: [_jsx("div", { className: "te-label", children: "Typography" }), _jsxs("select", { value: selectValue, onChange: (event) => applyFont(event.target.value), "aria-label": "Font family", children: [curatedFonts.map((font) => (_jsx("option", { value: font, children: font }, font))), selectValue === "custom" && _jsxs("option", { value: "custom", children: [primary, " (custom)"] })] }), _jsx("input", { type: "text", placeholder: "or any Google Font \u2014 Enter to apply", value: freeFont, onChange: (event) => setFreeFont(event.target.value), onKeyDown: (event) => {
|
|
118
|
+
if (event.key !== "Enter")
|
|
119
|
+
return;
|
|
120
|
+
applyFont(freeFont);
|
|
121
|
+
setFreeFont("");
|
|
122
|
+
}, "aria-label": "Custom Google Font" }), _jsxs("div", { className: "te-srow", children: [_jsx("label", { htmlFor: "te-basesize", children: "baseSize" }), _jsx("input", { id: "te-basesize", type: "range", min: 12, max: 18, value: px(theme.typography.baseSize), onChange: (event) => set({ typography: { ...theme.typography, baseSize: `${event.target.value}px` } }) }), _jsx("output", { children: theme.typography.baseSize })] })] }), _jsxs("div", { className: "te-sec", children: [_jsx("div", { className: "te-label", children: "Radius" }), [
|
|
123
|
+
["small", 14],
|
|
124
|
+
["medium", 20],
|
|
125
|
+
["large", 28],
|
|
126
|
+
].map(([key, max]) => (_jsxs("div", { className: "te-srow", children: [_jsx("label", { htmlFor: `te-radius-${key}`, children: key }), _jsx("input", { id: `te-radius-${key}`, type: "range", min: 0, max: max, value: px(theme.radius[key]), onChange: (event) => setRadius(key, Number(event.target.value)) }), _jsx("output", { children: theme.radius[key] })] }, key)))] }), _jsxs("div", { className: "te-sec", children: [_jsx("div", { className: "te-label", children: "Density \u00B7 Motion" }), _jsxs("div", { className: "te-segs", children: [_jsx("div", { className: "te-seg", role: "group", "aria-label": "Density", children: ["comfortable", "compact"].map((value) => (_jsx("button", { type: "button", "aria-pressed": theme.density === value, onClick: () => set({ density: value }), children: value }, value))) }), _jsx("div", { className: "te-seg", role: "group", "aria-label": "Motion", children: ["full", "reduced"].map((value) => (_jsx("button", { type: "button", "aria-pressed": theme.motion === value, onClick: () => set({ motion: value }), children: value }, value))) })] })] })] }), _jsxs("div", { className: "te-foot", children: [_jsx(CopyButton, { className: "te-copy", label: "Copy theme.json", copiedLabel: "Copied \u2713", text: () => themeJson(theme) }), _jsx(CopyButton, { className: "te-link", label: "Copy link", copiedLabel: "Link copied \u2713", text: () => window.location.href })] })] })] }));
|
|
127
|
+
}
|
|
128
|
+
/** Keep the page's on-demand Google Fonts <link> in sync with the theme. */
|
|
129
|
+
export function useGoogleFont(fontFamily) {
|
|
130
|
+
useEffect(() => {
|
|
131
|
+
const href = googleFontHref(primaryFontFamily(fontFamily));
|
|
132
|
+
const id = "vendo-playground-google-font";
|
|
133
|
+
const existing = document.getElementById(id);
|
|
134
|
+
if (!href) {
|
|
135
|
+
existing?.remove();
|
|
136
|
+
return;
|
|
137
|
+
}
|
|
138
|
+
if (existing) {
|
|
139
|
+
if (existing.href !== href)
|
|
140
|
+
existing.href = href;
|
|
141
|
+
return;
|
|
142
|
+
}
|
|
143
|
+
const link = document.createElement("link");
|
|
144
|
+
link.id = id;
|
|
145
|
+
link.rel = "stylesheet";
|
|
146
|
+
link.href = href;
|
|
147
|
+
document.head.append(link);
|
|
148
|
+
}, [fontFamily]);
|
|
149
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Theme-editor state helpers: presets, the `?theme=` URL codec, font-stack
|
|
3
|
+
* utilities, and the `.vendo/theme.json` emitter. Pure module — the panel
|
|
4
|
+
* component (theme-editor.tsx) renders it; nothing here touches the DOM.
|
|
5
|
+
*/
|
|
6
|
+
import { type VendoTheme } from "@vendoai/core";
|
|
7
|
+
export interface ThemePreset {
|
|
8
|
+
name: string;
|
|
9
|
+
theme: VendoTheme;
|
|
10
|
+
}
|
|
11
|
+
/** Default black leads (the Cadence-extracted look Yousef calls the default);
|
|
12
|
+
* Ultramarine is the shipped defaultVendoTheme; the rest are stress presets —
|
|
13
|
+
* one dark (color-scheme flip) and one radius/type torture test. */
|
|
14
|
+
export declare const themePresets: ThemePreset[];
|
|
15
|
+
/** Serialize a theme for the shareable `?theme=` search param. */
|
|
16
|
+
export declare function encodeThemeParam(theme: VendoTheme): string;
|
|
17
|
+
/** Parse a `?theme=` payload: partials resolve over the shipped defaults, and
|
|
18
|
+
* anything that does not validate as a VendoTheme is dropped (undefined). */
|
|
19
|
+
export declare function decodeThemeParam(value: string | null): VendoTheme | undefined;
|
|
20
|
+
/** The font picker's curated list; anything else arrives via free text. */
|
|
21
|
+
export declare const curatedFonts: string[];
|
|
22
|
+
/** First family of a CSS font stack, unquoted. */
|
|
23
|
+
export declare function primaryFontFamily(fontFamily: string): string;
|
|
24
|
+
/** Google Fonts css2 href for a webfont family; undefined for system fonts. */
|
|
25
|
+
export declare function googleFontHref(family: string): string | undefined;
|
|
26
|
+
/** Full fontFamily token for a picked family, quoting multi-word names. */
|
|
27
|
+
export declare function fontStack(family: string): string;
|
|
28
|
+
/** Pretty-printed `.vendo/theme.json` document for the current theme. */
|
|
29
|
+
export declare function themeJson(theme: VendoTheme): string;
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Theme-editor state helpers: presets, the `?theme=` URL codec, font-stack
|
|
3
|
+
* utilities, and the `.vendo/theme.json` emitter. Pure module — the panel
|
|
4
|
+
* component (theme-editor.tsx) renders it; nothing here touches the DOM.
|
|
5
|
+
*/
|
|
6
|
+
import { vendoThemeSchema } from "@vendoai/core";
|
|
7
|
+
import { defaultVendoTheme, resolveTheme } from "@vendoai/ui";
|
|
8
|
+
/** Default black leads (the Cadence-extracted look Yousef calls the default);
|
|
9
|
+
* Ultramarine is the shipped defaultVendoTheme; the rest are stress presets —
|
|
10
|
+
* one dark (color-scheme flip) and one radius/type torture test. */
|
|
11
|
+
export const themePresets = [
|
|
12
|
+
{
|
|
13
|
+
name: "Default black",
|
|
14
|
+
theme: {
|
|
15
|
+
colors: {
|
|
16
|
+
background: "#fbfbfa",
|
|
17
|
+
surface: "#ffffff",
|
|
18
|
+
text: "#111111",
|
|
19
|
+
muted: "#46443f",
|
|
20
|
+
accent: "#111111",
|
|
21
|
+
accentText: "#ffffff",
|
|
22
|
+
danger: "#b0473a",
|
|
23
|
+
border: "#ecebe8",
|
|
24
|
+
},
|
|
25
|
+
typography: { fontFamily: "Inter, ui-sans-serif, system-ui, sans-serif", baseSize: "15px" },
|
|
26
|
+
radius: { small: "6px", medium: "12px", large: "12px" },
|
|
27
|
+
density: "comfortable",
|
|
28
|
+
motion: "full",
|
|
29
|
+
},
|
|
30
|
+
},
|
|
31
|
+
{
|
|
32
|
+
// The pre-black-default shipped look, kept as an accent stress preset.
|
|
33
|
+
name: "Ultramarine",
|
|
34
|
+
theme: {
|
|
35
|
+
colors: {
|
|
36
|
+
background: "#ffffff",
|
|
37
|
+
surface: "#f7f7f8",
|
|
38
|
+
text: "#1a1a1e",
|
|
39
|
+
muted: "#6b6b76",
|
|
40
|
+
accent: "#2f5af5",
|
|
41
|
+
accentText: "#ffffff",
|
|
42
|
+
danger: "#c62f2f",
|
|
43
|
+
border: "#e3e3e8",
|
|
44
|
+
},
|
|
45
|
+
typography: { fontFamily: "Inter, ui-sans-serif, system-ui, sans-serif", baseSize: "15px" },
|
|
46
|
+
radius: { small: "6px", medium: "10px", large: "16px" },
|
|
47
|
+
density: "comfortable",
|
|
48
|
+
motion: "full",
|
|
49
|
+
},
|
|
50
|
+
},
|
|
51
|
+
{
|
|
52
|
+
name: "Dark violet",
|
|
53
|
+
theme: {
|
|
54
|
+
colors: {
|
|
55
|
+
background: "#0f1116",
|
|
56
|
+
surface: "#171a21",
|
|
57
|
+
text: "#e8e9ee",
|
|
58
|
+
muted: "#9aa0ae",
|
|
59
|
+
accent: "#7c6cff",
|
|
60
|
+
accentText: "#ffffff",
|
|
61
|
+
danger: "#ff6b5e",
|
|
62
|
+
border: "#262a34",
|
|
63
|
+
},
|
|
64
|
+
typography: { fontFamily: "Onest, system-ui, sans-serif", baseSize: "15px" },
|
|
65
|
+
radius: { small: "6px", medium: "10px", large: "16px" },
|
|
66
|
+
density: "comfortable",
|
|
67
|
+
motion: "full",
|
|
68
|
+
},
|
|
69
|
+
},
|
|
70
|
+
{
|
|
71
|
+
name: "Playful round",
|
|
72
|
+
theme: {
|
|
73
|
+
colors: {
|
|
74
|
+
background: "#fffdf6",
|
|
75
|
+
surface: "#ffffff",
|
|
76
|
+
text: "#1c1917",
|
|
77
|
+
muted: "#78716c",
|
|
78
|
+
accent: "#ff5c38",
|
|
79
|
+
accentText: "#ffffff",
|
|
80
|
+
danger: "#c62f2f",
|
|
81
|
+
border: "#ede5d8",
|
|
82
|
+
},
|
|
83
|
+
typography: { fontFamily: "Onest, system-ui, sans-serif", baseSize: "16px" },
|
|
84
|
+
radius: { small: "10px", medium: "16px", large: "24px" },
|
|
85
|
+
density: "compact",
|
|
86
|
+
motion: "full",
|
|
87
|
+
},
|
|
88
|
+
},
|
|
89
|
+
];
|
|
90
|
+
/** Serialize a theme for the shareable `?theme=` search param. */
|
|
91
|
+
export function encodeThemeParam(theme) {
|
|
92
|
+
return JSON.stringify(theme);
|
|
93
|
+
}
|
|
94
|
+
/** Parse a `?theme=` payload: partials resolve over the shipped defaults, and
|
|
95
|
+
* anything that does not validate as a VendoTheme is dropped (undefined). */
|
|
96
|
+
export function decodeThemeParam(value) {
|
|
97
|
+
if (!value)
|
|
98
|
+
return undefined;
|
|
99
|
+
let parsed;
|
|
100
|
+
try {
|
|
101
|
+
parsed = JSON.parse(value);
|
|
102
|
+
}
|
|
103
|
+
catch {
|
|
104
|
+
return undefined;
|
|
105
|
+
}
|
|
106
|
+
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed))
|
|
107
|
+
return undefined;
|
|
108
|
+
const resolved = resolveTheme(defaultVendoTheme, parsed);
|
|
109
|
+
return vendoThemeSchema.safeParse(resolved).success ? resolved : undefined;
|
|
110
|
+
}
|
|
111
|
+
/** The font picker's curated list; anything else arrives via free text. */
|
|
112
|
+
export const curatedFonts = ["Onest", "Inter", "Geist", "IBM Plex Sans", "Newsreader", "system-ui (no webfont)"];
|
|
113
|
+
/** Families that must never be fetched from Google Fonts. */
|
|
114
|
+
const SYSTEM_FAMILIES = new Set([
|
|
115
|
+
"system-ui",
|
|
116
|
+
"system-ui (no webfont)",
|
|
117
|
+
"-apple-system",
|
|
118
|
+
"blinkmacsystemfont",
|
|
119
|
+
"segoe ui",
|
|
120
|
+
"ui-sans-serif",
|
|
121
|
+
"ui-serif",
|
|
122
|
+
"ui-monospace",
|
|
123
|
+
"sans-serif",
|
|
124
|
+
"serif",
|
|
125
|
+
"monospace",
|
|
126
|
+
]);
|
|
127
|
+
/** First family of a CSS font stack, unquoted. */
|
|
128
|
+
export function primaryFontFamily(fontFamily) {
|
|
129
|
+
const first = fontFamily.split(",")[0].trim();
|
|
130
|
+
return first.replace(/^['"]|['"]$/g, "");
|
|
131
|
+
}
|
|
132
|
+
/** Google Fonts css2 href for a webfont family; undefined for system fonts. */
|
|
133
|
+
export function googleFontHref(family) {
|
|
134
|
+
const name = family.trim();
|
|
135
|
+
if (!name || SYSTEM_FAMILIES.has(name.toLowerCase()))
|
|
136
|
+
return undefined;
|
|
137
|
+
return `https://fonts.googleapis.com/css2?family=${name.replace(/ /g, "+")}:wght@400;500;600;700&display=swap`;
|
|
138
|
+
}
|
|
139
|
+
/** Full fontFamily token for a picked family, quoting multi-word names. */
|
|
140
|
+
export function fontStack(family) {
|
|
141
|
+
const name = family.trim();
|
|
142
|
+
if (!name || name.toLowerCase().startsWith("system-ui")) {
|
|
143
|
+
return "system-ui, -apple-system, 'Segoe UI', sans-serif";
|
|
144
|
+
}
|
|
145
|
+
const quoted = name.includes(" ") ? `'${name}'` : name;
|
|
146
|
+
return `${quoted}, system-ui, sans-serif`;
|
|
147
|
+
}
|
|
148
|
+
/** Pretty-printed `.vendo/theme.json` document for the current theme. */
|
|
149
|
+
export function themeJson(theme) {
|
|
150
|
+
return `${JSON.stringify(theme, null, 2)}\n`;
|
|
151
|
+
}
|