@unoverse-platform/studio 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/bin/studio.mjs +124 -0
- package/index.html +12 -0
- package/local/catalog.ts +105 -0
- package/local/keys.ts +118 -0
- package/local/nodes.ts +77 -0
- package/local/plugin.ts +401 -0
- package/local/scaffold.d.mts +13 -0
- package/local/scaffold.mjs +85 -0
- package/package.json +43 -0
- package/public/logo.png +0 -0
- package/screens/AtomsView.tsx +206 -0
- package/screens/ComponentsView.tsx +551 -0
- package/screens/ConfigForm.tsx +217 -0
- package/screens/DesignSystemView.tsx +324 -0
- package/screens/IdentityHeader.tsx +88 -0
- package/screens/PromptBlocksView.tsx +137 -0
- package/screens/SkillsView.tsx +167 -0
- package/screens/TemplatesView.tsx +1005 -0
- package/screens/index.ts +26 -0
- package/screens/states.ts +44 -0
- package/src/App.tsx +252 -0
- package/src/ConnectUniverse.tsx +232 -0
- package/src/NewProject.tsx +89 -0
- package/src/NodeKeys.tsx +98 -0
- package/src/NodesView.tsx +443 -0
- package/src/PublishDialog.tsx +309 -0
- package/src/UniverseSession.tsx +121 -0
- package/src/host.ts +165 -0
- package/src/index.css +7 -0
- package/src/main.tsx +11 -0
- package/src/universes.ts +210 -0
- package/vendor/sdk/appEngine.tsx +219 -0
- package/vendor/sdk/lib/UnoverseComponent.tsx +120 -0
- package/vendor/sdk/lib/connection.tsx +302 -0
- package/vendor/sdk/lib/core/actions.ts +84 -0
- package/vendor/sdk/lib/core/client.ts +134 -0
- package/vendor/sdk/lib/core/conditions.ts +43 -0
- package/vendor/sdk/lib/core/connection.ts +142 -0
- package/vendor/sdk/lib/core/index.ts +32 -0
- package/vendor/sdk/lib/core/store.ts +455 -0
- package/vendor/sdk/lib/core/types.ts +194 -0
- package/vendor/sdk/lib/index.ts +59 -0
- package/vendor/sdk/lib/isolate.tsx +70 -0
- package/vendor/sdk/lib/primitives.tsx +453 -0
- package/vendor/sdk/lib/realtime/audioUtils.ts +40 -0
- package/vendor/sdk/lib/realtime/types.ts +45 -0
- package/vendor/sdk/lib/realtime/useAudioCapture.ts +237 -0
- package/vendor/sdk/lib/realtime/useAudioPlayback.ts +145 -0
- package/vendor/sdk/lib/realtime/useRealtimeWebSocket.ts +164 -0
- package/vendor/sdk/lib/render.tsx +178 -0
- package/vendor/sdk/lib/streamed.tsx +65 -0
- package/vendor/sdk/lib/style.ts +227 -0
- package/vendor/sdk/lib/template.tsx +521 -0
- package/vendor/sdk/lib/theme.ts +55 -0
- package/vendor/sdk/lib/voice.tsx +311 -0
- package/vendor/sdk/main.tsx +96 -0
- package/vite.config.ts +70 -0
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Schema-driven config form — renders a node's `configSchema` as labelled fields
|
|
3
|
+
* (select / number / toggle / text / JSON), the same way the Canvas does, instead
|
|
4
|
+
* of a raw JSON blob. A compact port of GravityCanvas's FieldRenderer.
|
|
5
|
+
*/
|
|
6
|
+
import { useEffect, useState, type ReactNode } from "react";
|
|
7
|
+
// Colours come from the HOST, never from this shared file. Canvas renders these fields
|
|
8
|
+
// on a white panel; local Studio renders them on dark chrome. Hardcoding either one
|
|
9
|
+
// makes the text unreadable in the other.
|
|
10
|
+
import { chrome } from "studio-host";
|
|
11
|
+
|
|
12
|
+
type Property = Record<string, any>;
|
|
13
|
+
interface ConfigSchema {
|
|
14
|
+
properties?: Record<string, Property>;
|
|
15
|
+
required?: string[];
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const inputCls =
|
|
19
|
+
`rounded-md border px-3 py-2 text-sm focus:border-emerald-500 focus:outline-none ${chrome.input}`;
|
|
20
|
+
|
|
21
|
+
/** Build the initial config object from the schema's declared defaults. */
|
|
22
|
+
export function defaultsFromSchema(schema?: ConfigSchema): Record<string, any> {
|
|
23
|
+
const props = schema?.properties ?? {};
|
|
24
|
+
return Object.fromEntries(
|
|
25
|
+
Object.entries(props).map(([k, p]) => [
|
|
26
|
+
k,
|
|
27
|
+
p.default ?? (p.type === "boolean" ? false : p.type === "array" ? [] : p.type === "object" ? "" : ""),
|
|
28
|
+
]),
|
|
29
|
+
);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function FieldShell({
|
|
33
|
+
label,
|
|
34
|
+
required,
|
|
35
|
+
description,
|
|
36
|
+
children,
|
|
37
|
+
}: {
|
|
38
|
+
label: string;
|
|
39
|
+
required?: boolean;
|
|
40
|
+
description?: string;
|
|
41
|
+
children: ReactNode;
|
|
42
|
+
}) {
|
|
43
|
+
return (
|
|
44
|
+
<label className="flex flex-col gap-1">
|
|
45
|
+
<span className={`text-sm font-semibold ${chrome.label}`}>
|
|
46
|
+
{label}
|
|
47
|
+
{required && <span className="ml-0.5 text-red-400">*</span>}
|
|
48
|
+
</span>
|
|
49
|
+
{description && <span className={`text-[11px] leading-snug ${chrome.muted}`}>{description}</span>}
|
|
50
|
+
{children}
|
|
51
|
+
</label>
|
|
52
|
+
);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/** Textarea with a local text mirror so half-typed JSON is allowed while editing. */
|
|
56
|
+
function Jsonish({
|
|
57
|
+
fieldKey,
|
|
58
|
+
property,
|
|
59
|
+
value,
|
|
60
|
+
required,
|
|
61
|
+
onChange,
|
|
62
|
+
}: {
|
|
63
|
+
fieldKey: string;
|
|
64
|
+
property: Property;
|
|
65
|
+
value: any;
|
|
66
|
+
required?: boolean;
|
|
67
|
+
onChange: (k: string, v: any) => void;
|
|
68
|
+
}) {
|
|
69
|
+
const toText = (v: any) => (typeof v === "string" ? v : v == null ? "" : JSON.stringify(v, null, 2));
|
|
70
|
+
const [text, setText] = useState(toText(value));
|
|
71
|
+
// Re-seed only when the field identity changes (node switch), not on each keystroke.
|
|
72
|
+
useEffect(() => setText(toText(value)), [fieldKey]); // eslint-disable-line react-hooks/exhaustive-deps
|
|
73
|
+
const isStructured = property.type === "object" || property.type === "array";
|
|
74
|
+
return (
|
|
75
|
+
<FieldShell label={property.title || fieldKey} required={required} description={property.description}>
|
|
76
|
+
<textarea
|
|
77
|
+
value={text}
|
|
78
|
+
spellCheck={false}
|
|
79
|
+
rows={property["ui:field"] === "template" ? 3 : 5}
|
|
80
|
+
className={`${inputCls} resize-y font-mono text-xs`}
|
|
81
|
+
onChange={(e) => {
|
|
82
|
+
setText(e.target.value);
|
|
83
|
+
if (isStructured) {
|
|
84
|
+
try {
|
|
85
|
+
onChange(fieldKey, JSON.parse(e.target.value));
|
|
86
|
+
} catch {
|
|
87
|
+
onChange(fieldKey, e.target.value); // keep raw until valid
|
|
88
|
+
}
|
|
89
|
+
} else {
|
|
90
|
+
onChange(fieldKey, e.target.value);
|
|
91
|
+
}
|
|
92
|
+
}}
|
|
93
|
+
/>
|
|
94
|
+
</FieldShell>
|
|
95
|
+
);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function Field({
|
|
99
|
+
fieldKey,
|
|
100
|
+
property,
|
|
101
|
+
value,
|
|
102
|
+
required,
|
|
103
|
+
onChange,
|
|
104
|
+
}: {
|
|
105
|
+
fieldKey: string;
|
|
106
|
+
property: Property;
|
|
107
|
+
value: any;
|
|
108
|
+
required?: boolean;
|
|
109
|
+
onChange: (k: string, v: any) => void;
|
|
110
|
+
}) {
|
|
111
|
+
const label = property.title || fieldKey;
|
|
112
|
+
|
|
113
|
+
// Enum → select
|
|
114
|
+
if (property.enum) {
|
|
115
|
+
const labels: any[] = property.enumNames ?? property.enum;
|
|
116
|
+
return (
|
|
117
|
+
<FieldShell label={label} required={required} description={property.description}>
|
|
118
|
+
<select className={inputCls} value={value ?? ""} onChange={(e) => onChange(fieldKey, e.target.value)}>
|
|
119
|
+
{property.enum.map((opt: any, i: number) => (
|
|
120
|
+
<option key={String(opt)} value={opt}>
|
|
121
|
+
{labels[i] ?? String(opt)}
|
|
122
|
+
</option>
|
|
123
|
+
))}
|
|
124
|
+
</select>
|
|
125
|
+
</FieldShell>
|
|
126
|
+
);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// Number / integer
|
|
130
|
+
if (property.type === "number" || property.type === "integer") {
|
|
131
|
+
return (
|
|
132
|
+
<FieldShell label={label} required={required} description={property.description}>
|
|
133
|
+
<input
|
|
134
|
+
type="number"
|
|
135
|
+
className={inputCls}
|
|
136
|
+
value={value ?? ""}
|
|
137
|
+
min={property.minimum}
|
|
138
|
+
max={property.maximum}
|
|
139
|
+
step={property.step ?? (property.type === "integer" ? 1 : "any")}
|
|
140
|
+
onChange={(e) => onChange(fieldKey, e.target.value === "" ? undefined : Number(e.target.value))}
|
|
141
|
+
/>
|
|
142
|
+
</FieldShell>
|
|
143
|
+
);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// Boolean → checkbox/toggle
|
|
147
|
+
if (property.type === "boolean") {
|
|
148
|
+
return (
|
|
149
|
+
<label className="flex items-center gap-2">
|
|
150
|
+
<input
|
|
151
|
+
type="checkbox"
|
|
152
|
+
checked={!!value}
|
|
153
|
+
onChange={(e) => onChange(fieldKey, e.target.checked)}
|
|
154
|
+
className="h-4 w-4 accent-emerald-500"
|
|
155
|
+
/>
|
|
156
|
+
<span className={`text-sm font-semibold ${chrome.label}`}>{label}</span>
|
|
157
|
+
{property.description && <span className={`text-[11px] ${chrome.muted}`}>{property.description}</span>}
|
|
158
|
+
</label>
|
|
159
|
+
);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// Object / array / template / JSON / textarea → multi-line editor
|
|
163
|
+
if (
|
|
164
|
+
property.type === "object" ||
|
|
165
|
+
property.type === "array" ||
|
|
166
|
+
property["ui:field"] === "template" ||
|
|
167
|
+
property["ui:field"] === "JSON" ||
|
|
168
|
+
property["ui:widget"] === "textarea"
|
|
169
|
+
) {
|
|
170
|
+
return <Jsonish fieldKey={fieldKey} property={property} value={value} required={required} onChange={onChange} />;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
// String (default)
|
|
174
|
+
return (
|
|
175
|
+
<FieldShell label={label} required={required} description={property.description}>
|
|
176
|
+
<input
|
|
177
|
+
type="text"
|
|
178
|
+
className={inputCls}
|
|
179
|
+
value={value ?? ""}
|
|
180
|
+
placeholder={property.placeholder}
|
|
181
|
+
onChange={(e) => onChange(fieldKey, e.target.value)}
|
|
182
|
+
/>
|
|
183
|
+
</FieldShell>
|
|
184
|
+
);
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
export function ConfigForm({
|
|
188
|
+
schema,
|
|
189
|
+
value,
|
|
190
|
+
onChange,
|
|
191
|
+
}: {
|
|
192
|
+
schema?: ConfigSchema;
|
|
193
|
+
value: Record<string, any>;
|
|
194
|
+
onChange: (key: string, val: any) => void;
|
|
195
|
+
}) {
|
|
196
|
+
const props = schema?.properties ?? {};
|
|
197
|
+
const required = new Set(schema?.required ?? []);
|
|
198
|
+
const entries = Object.entries(props);
|
|
199
|
+
if (!entries.length) return <div className={`text-xs ${chrome.muted}`}>No configuration for this node.</div>;
|
|
200
|
+
return (
|
|
201
|
+
<div className="flex flex-col gap-4">
|
|
202
|
+
{entries.map(([key, p]) => {
|
|
203
|
+
// ui:dependencies — hide when a dependency value isn't met.
|
|
204
|
+
const deps = p["ui:dependencies"];
|
|
205
|
+
if (
|
|
206
|
+
deps &&
|
|
207
|
+
Object.entries(deps).some(([f, dv]: [string, any]) =>
|
|
208
|
+
Array.isArray(dv) ? !dv.includes(value[f]) : value[f] !== dv,
|
|
209
|
+
)
|
|
210
|
+
) {
|
|
211
|
+
return null;
|
|
212
|
+
}
|
|
213
|
+
return <Field key={key} fieldKey={key} property={p} value={value[key] ?? p.default} required={required.has(key)} onChange={onChange} />;
|
|
214
|
+
})}
|
|
215
|
+
</div>
|
|
216
|
+
);
|
|
217
|
+
}
|
|
@@ -0,0 +1,324 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Design System showcase — renders the live design tokens (from the SDK's compiled
|
|
3
|
+
* theme) so you can SEE the colors, typography, spacing, radius and shadows. Dogfoods
|
|
4
|
+
* the real `themes` export, so this IS the design system, not a copy. Driven by the
|
|
5
|
+
* active theme, so the light/dark toggle restyles it. Left nav jumps between token
|
|
6
|
+
* groups (no one big scroll); each group shows how many tokens THIS project overrides.
|
|
7
|
+
*/
|
|
8
|
+
import { useState } from "react";
|
|
9
|
+
import type { ResolvedTheme } from "@unoverse/sdk";
|
|
10
|
+
|
|
11
|
+
const COLOR_GROUP_ORDER = [
|
|
12
|
+
"brand", "primary", "secondary", "success", "action", "text", "surface", "border", "status",
|
|
13
|
+
"gray", "info", "warning", "error", "chart", "white", "black",
|
|
14
|
+
];
|
|
15
|
+
|
|
16
|
+
function groupColors(color: Record<string, string>) {
|
|
17
|
+
const groups: Record<string, [string, string][]> = {};
|
|
18
|
+
for (const [k, v] of Object.entries(color)) {
|
|
19
|
+
const g = k.split(".")[0];
|
|
20
|
+
(groups[g] ??= []).push([k, v]);
|
|
21
|
+
}
|
|
22
|
+
const ordered = Object.keys(groups).sort(
|
|
23
|
+
(a, b) => (COLOR_GROUP_ORDER.indexOf(a) + 1 || 99) - (COLOR_GROUP_ORDER.indexOf(b) + 1 || 99),
|
|
24
|
+
);
|
|
25
|
+
return ordered.map((g) => [g, groups[g]] as const);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// Motion families — group animations by what they DO (slide/pulse/fade/loop) so the
|
|
29
|
+
// gallery reads as related sets rather than a flat wall. First matching family wins;
|
|
30
|
+
// anything unmatched falls to "Other".
|
|
31
|
+
const ANIM_GROUPS: { title: string; test: (n: string) => boolean }[] = [
|
|
32
|
+
{ title: "Slide & enter", test: (n) => /slide|enter|reveal/i.test(n) },
|
|
33
|
+
{ title: "Pulse & breathe", test: (n) => /pulse|ring|breathe|beat|glow/i.test(n) },
|
|
34
|
+
{ title: "Fade", test: (n) => /fade|dissolve/i.test(n) },
|
|
35
|
+
{ title: "Loop & ambient", test: (n) => /wave|equalize|spin|loop|bounce|shimmer/i.test(n) },
|
|
36
|
+
];
|
|
37
|
+
function groupAnimations(names: string[]) {
|
|
38
|
+
const buckets: Record<string, string[]> = {};
|
|
39
|
+
for (const n of names) {
|
|
40
|
+
const g = ANIM_GROUPS.find((g) => g.test(n));
|
|
41
|
+
(buckets[g ? g.title : "Other"] ??= []).push(n);
|
|
42
|
+
}
|
|
43
|
+
const order = [...ANIM_GROUPS.map((g) => g.title), "Other"];
|
|
44
|
+
return order.filter((t) => buckets[t]?.length).map((t) => [t, buckets[t]] as const);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export function DesignSystemView({
|
|
48
|
+
theme,
|
|
49
|
+
foundation = null,
|
|
50
|
+
org = "all",
|
|
51
|
+
}: {
|
|
52
|
+
theme: ResolvedTheme;
|
|
53
|
+
/** The design-system foundation theme (same variant) to diff against — null when
|
|
54
|
+
* viewing the foundation itself (nothing to compare, all tokens read as inherited). */
|
|
55
|
+
foundation?: ResolvedTheme | null;
|
|
56
|
+
org?: string;
|
|
57
|
+
}) {
|
|
58
|
+
const ink = theme.color["text.primary"] ?? "#111827";
|
|
59
|
+
const sub = theme.color["text.secondary"] ?? "#6b7280";
|
|
60
|
+
const surface = theme.color["surface.base"] ?? "#ffffff";
|
|
61
|
+
const raised = theme.color["surface.raised"] ?? surface;
|
|
62
|
+
const borderC = theme.color["border.subtle"] ?? "#e5e7eb";
|
|
63
|
+
// FIXED neutral chrome — the view's OWN accent (nav highlight, override ring/pencil,
|
|
64
|
+
// spacing/radius demo blocks). Deliberately NOT the viewed project's action colour, so a
|
|
65
|
+
// loud brand (e.g. an all-red project) doesn't turn the whole page its brand colour. The
|
|
66
|
+
// real token swatches still render their true values; only the chrome is neutralised.
|
|
67
|
+
const accent = "#6366f1";
|
|
68
|
+
const onAction = "#ffffff";
|
|
69
|
+
const bodyFont = (theme.text["body.md"]?.fontFamily as string) ?? "system-ui";
|
|
70
|
+
|
|
71
|
+
// A token is THIS PROJECT'S OVERRIDE when its resolved value differs from the
|
|
72
|
+
// foundation's; otherwise it's INHERITED from the design system. No foundation to
|
|
73
|
+
// compare (viewing the design system itself) → everything is inherited.
|
|
74
|
+
const isOverride = (bucket: keyof ResolvedTheme, key: string) =>
|
|
75
|
+
!!foundation &&
|
|
76
|
+
JSON.stringify((foundation as any)[bucket]?.[key]) !== JSON.stringify((theme as any)[bucket]?.[key]);
|
|
77
|
+
const overrideCount = (bucket: keyof ResolvedTheme) =>
|
|
78
|
+
!foundation ? 0 : Object.keys((theme as any)[bucket] ?? {}).filter((k) => isOverride(bucket, k)).length;
|
|
79
|
+
|
|
80
|
+
// A small pencil marks a token this project has CUSTOMISED (overridden). Inherited
|
|
81
|
+
// tokens carry no mark and are NOT dimmed — full-strength, just unmarked — so the eye
|
|
82
|
+
// lands on what changed without washing out the rest of the palette.
|
|
83
|
+
const OverrideMark = () => (
|
|
84
|
+
<span className="ml-1 inline-flex align-middle" title={`Customised in ${org} — overrides the design-system value`}>
|
|
85
|
+
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke={accent} strokeWidth="2.5" strokeLinecap="round" strokeLinejoin="round" aria-hidden>
|
|
86
|
+
<path d="M12 20h9" />
|
|
87
|
+
<path d="M16.5 3.5a2.121 2.121 0 0 1 3 3L7 19l-4 1 1-4Z" />
|
|
88
|
+
</svg>
|
|
89
|
+
</span>
|
|
90
|
+
);
|
|
91
|
+
|
|
92
|
+
// Serialise the theme's keyframes (DATA: stop → decls) into real @keyframes CSS so the
|
|
93
|
+
// Animations section can PLAY each one. Prefixed `uds-` to not collide with any globally
|
|
94
|
+
// injected keyframes of the same name.
|
|
95
|
+
const kebab = (s: string) => s.replace(/[A-Z]/g, (m) => "-" + m.toLowerCase());
|
|
96
|
+
const keyframesCss = Object.entries((theme.keyframes ?? {}) as Record<string, Record<string, Record<string, string>>>)
|
|
97
|
+
.map(
|
|
98
|
+
([name, stops]) =>
|
|
99
|
+
`@keyframes uds-${name}{` +
|
|
100
|
+
Object.entries(stops)
|
|
101
|
+
.map(([stop, decls]) => `${stop}{${Object.entries(decls).map(([p, v]) => `${kebab(p)}:${v}`).join(";")}}`)
|
|
102
|
+
.join("") +
|
|
103
|
+
`}`,
|
|
104
|
+
)
|
|
105
|
+
.join("\n");
|
|
106
|
+
|
|
107
|
+
const [active, setActive] = useState("colors");
|
|
108
|
+
|
|
109
|
+
const SECTIONS: { id: string; title: string; bucket?: keyof ResolvedTheme; body: React.ReactNode }[] = [
|
|
110
|
+
{
|
|
111
|
+
id: "colors",
|
|
112
|
+
title: "Colors",
|
|
113
|
+
bucket: "color",
|
|
114
|
+
body: (
|
|
115
|
+
<div className="space-y-5">
|
|
116
|
+
{groupColors(theme.color).map(([group, entries]) => (
|
|
117
|
+
<div key={group}>
|
|
118
|
+
<div className="mb-2 text-[11px] font-semibold uppercase tracking-wider" style={{ color: sub }}>{group}</div>
|
|
119
|
+
<div className="flex flex-wrap gap-3">
|
|
120
|
+
{entries.map(([name, val]) => {
|
|
121
|
+
const ov = isOverride("color", name);
|
|
122
|
+
return (
|
|
123
|
+
<div key={name} className="w-[110px]">
|
|
124
|
+
<div
|
|
125
|
+
className="h-14 w-full rounded-lg"
|
|
126
|
+
style={{ background: val, border: ov ? `2px solid ${accent}` : `1px solid ${borderC}` }}
|
|
127
|
+
/>
|
|
128
|
+
<div className="mt-1 truncate text-[11px] font-medium" title={name}>
|
|
129
|
+
{name}
|
|
130
|
+
{ov && <OverrideMark />}
|
|
131
|
+
</div>
|
|
132
|
+
<div className="text-[10px] uppercase" style={{ color: sub }}>{val}</div>
|
|
133
|
+
</div>
|
|
134
|
+
);
|
|
135
|
+
})}
|
|
136
|
+
</div>
|
|
137
|
+
</div>
|
|
138
|
+
))}
|
|
139
|
+
</div>
|
|
140
|
+
),
|
|
141
|
+
},
|
|
142
|
+
{
|
|
143
|
+
id: "typography",
|
|
144
|
+
title: "Typography",
|
|
145
|
+
bucket: "text",
|
|
146
|
+
body: (
|
|
147
|
+
<div className="space-y-5 rounded-xl p-6" style={{ background: raised, border: `1px solid ${borderC}` }}>
|
|
148
|
+
{Object.entries(theme.text).map(([name, style]) => {
|
|
149
|
+
const s = style as Record<string, unknown>;
|
|
150
|
+
const fams = String(s?.fontFamily ?? "")
|
|
151
|
+
.split(",")
|
|
152
|
+
.map((f) => f.trim().replace(/^['"]|['"]$/g, ""))
|
|
153
|
+
.filter(Boolean);
|
|
154
|
+
const meta = [s?.fontSize, s?.fontWeight && `weight ${s.fontWeight}`, s?.lineHeight && `lh ${s.lineHeight}`]
|
|
155
|
+
.filter(Boolean)
|
|
156
|
+
.join(" · ");
|
|
157
|
+
return (
|
|
158
|
+
<div key={name} className="flex items-baseline gap-4">
|
|
159
|
+
<div className="w-32 shrink-0 text-[11px] uppercase tracking-wider" style={{ color: sub }}>
|
|
160
|
+
{name}
|
|
161
|
+
{isOverride("text", name) && <OverrideMark />}
|
|
162
|
+
</div>
|
|
163
|
+
<div className="min-w-0 flex-1">
|
|
164
|
+
<div style={style as React.CSSProperties}>The quick brown fox</div>
|
|
165
|
+
<div className="mt-0.5 text-[10px]" style={{ color: sub }}>
|
|
166
|
+
{fams.length ? (
|
|
167
|
+
<>
|
|
168
|
+
<span className="font-semibold" style={{ color: ink }}>{fams[0]}</span>
|
|
169
|
+
{fams.length > 1 && <span> → {fams.slice(1).join(", ")}</span>}
|
|
170
|
+
</>
|
|
171
|
+
) : (
|
|
172
|
+
"—"
|
|
173
|
+
)}
|
|
174
|
+
{meta && <span> · {meta}</span>}
|
|
175
|
+
</div>
|
|
176
|
+
</div>
|
|
177
|
+
</div>
|
|
178
|
+
);
|
|
179
|
+
})}
|
|
180
|
+
</div>
|
|
181
|
+
),
|
|
182
|
+
},
|
|
183
|
+
{
|
|
184
|
+
id: "spacing",
|
|
185
|
+
title: "Spacing",
|
|
186
|
+
bucket: "space",
|
|
187
|
+
body: (
|
|
188
|
+
<div className="space-y-2 rounded-xl p-6" style={{ background: raised, border: `1px solid ${borderC}` }}>
|
|
189
|
+
{Object.entries(theme.space).map(([name, val]) => (
|
|
190
|
+
<div key={name} className="flex items-center gap-4">
|
|
191
|
+
<div className="w-20 shrink-0 text-[11px]" style={{ color: sub }}>
|
|
192
|
+
{name}
|
|
193
|
+
{isOverride("space", name) && <OverrideMark />}
|
|
194
|
+
</div>
|
|
195
|
+
<div className="h-3 rounded" style={{ width: val, background: accent }} />
|
|
196
|
+
<div className="text-[10px]" style={{ color: sub }}>{val}</div>
|
|
197
|
+
</div>
|
|
198
|
+
))}
|
|
199
|
+
</div>
|
|
200
|
+
),
|
|
201
|
+
},
|
|
202
|
+
{
|
|
203
|
+
id: "radius",
|
|
204
|
+
title: "Radius",
|
|
205
|
+
bucket: "radius",
|
|
206
|
+
body: (
|
|
207
|
+
<div className="flex flex-wrap gap-5">
|
|
208
|
+
{Object.entries(theme.radius).map(([name, val]) => (
|
|
209
|
+
<div key={name} className="text-center">
|
|
210
|
+
<div className="h-16 w-16" style={{ background: accent, borderRadius: val }} />
|
|
211
|
+
<div className="mt-1 text-[11px] font-medium">
|
|
212
|
+
{name}
|
|
213
|
+
{isOverride("radius", name) && <OverrideMark />}
|
|
214
|
+
</div>
|
|
215
|
+
<div className="text-[10px]" style={{ color: sub }}>{val}</div>
|
|
216
|
+
</div>
|
|
217
|
+
))}
|
|
218
|
+
</div>
|
|
219
|
+
),
|
|
220
|
+
},
|
|
221
|
+
{
|
|
222
|
+
id: "shadow",
|
|
223
|
+
title: "Shadow / Elevation",
|
|
224
|
+
body: (
|
|
225
|
+
<div className="flex flex-wrap gap-6 rounded-xl p-8" style={{ background: theme.color["surface.sunken"] ?? "#f3f4f6" }}>
|
|
226
|
+
{Object.entries(theme.shadow).map(([name, val]) => (
|
|
227
|
+
<div key={name} className="text-center">
|
|
228
|
+
<div className="h-20 w-28 rounded-lg" style={{ background: surface, boxShadow: val }} />
|
|
229
|
+
<div className="mt-2 text-[11px] font-medium">{name}</div>
|
|
230
|
+
</div>
|
|
231
|
+
))}
|
|
232
|
+
</div>
|
|
233
|
+
),
|
|
234
|
+
},
|
|
235
|
+
{
|
|
236
|
+
id: "animations",
|
|
237
|
+
title: "Animations",
|
|
238
|
+
bucket: "keyframes",
|
|
239
|
+
body: (
|
|
240
|
+
<>
|
|
241
|
+
<style>{keyframesCss}</style>
|
|
242
|
+
<div className="space-y-6">
|
|
243
|
+
{groupAnimations(Object.keys(theme.keyframes ?? {})).map(([title, names]) => (
|
|
244
|
+
<div key={title}>
|
|
245
|
+
<div className="mb-2 text-[11px] font-semibold uppercase tracking-wider" style={{ color: sub }}>{title}</div>
|
|
246
|
+
<div className="flex flex-wrap gap-6">
|
|
247
|
+
{names.map((name) => (
|
|
248
|
+
<div key={name} className="text-center">
|
|
249
|
+
<div
|
|
250
|
+
className="flex h-24 w-32 items-center justify-center overflow-hidden rounded-xl"
|
|
251
|
+
style={{ background: raised, border: `1px solid ${borderC}` }}
|
|
252
|
+
>
|
|
253
|
+
<div style={{ width: 28, height: 28, borderRadius: 8, background: accent, animation: `uds-${name} 1.8s ease-in-out infinite` }} />
|
|
254
|
+
</div>
|
|
255
|
+
<div className="mt-2 text-[11px] font-medium">
|
|
256
|
+
{name}
|
|
257
|
+
{isOverride("keyframes", name) && <OverrideMark />}
|
|
258
|
+
</div>
|
|
259
|
+
</div>
|
|
260
|
+
))}
|
|
261
|
+
</div>
|
|
262
|
+
</div>
|
|
263
|
+
))}
|
|
264
|
+
</div>
|
|
265
|
+
</>
|
|
266
|
+
),
|
|
267
|
+
},
|
|
268
|
+
];
|
|
269
|
+
const current = SECTIONS.find((s) => s.id === active) ?? SECTIONS[0];
|
|
270
|
+
|
|
271
|
+
return (
|
|
272
|
+
<div className="flex h-full" style={{ background: theme.color["surface.sunken"] ?? "#f9fafb", color: ink }}>
|
|
273
|
+
{/* LEFT NAV — jump between token groups; badge = how many tokens THIS project overrides */}
|
|
274
|
+
<nav className="w-56 shrink-0 overflow-auto border-r p-5" style={{ borderColor: borderC, background: surface, fontFamily: bodyFont }}>
|
|
275
|
+
<h1 className="mb-1 text-lg font-bold">Design System</h1>
|
|
276
|
+
<p className="mb-5 text-[11px] leading-snug" style={{ color: sub }}>
|
|
277
|
+
{foundation ? (
|
|
278
|
+
<>Viewing <b style={{ color: ink }}>{org}</b></>
|
|
279
|
+
) : (
|
|
280
|
+
<>The base foundation</>
|
|
281
|
+
)}
|
|
282
|
+
</p>
|
|
283
|
+
<ul className="space-y-1">
|
|
284
|
+
{SECTIONS.map((s) => {
|
|
285
|
+
const n = foundation && s.bucket ? overrideCount(s.bucket) : 0;
|
|
286
|
+
const on = active === s.id;
|
|
287
|
+
return (
|
|
288
|
+
<li key={s.id}>
|
|
289
|
+
<button
|
|
290
|
+
onClick={() => setActive(s.id)}
|
|
291
|
+
className="flex w-full items-center justify-between rounded px-3 py-2 text-left text-sm font-medium transition-colors"
|
|
292
|
+
style={on ? { background: accent, color: onAction } : { color: ink }}
|
|
293
|
+
>
|
|
294
|
+
<span>{s.title}</span>
|
|
295
|
+
{n > 0 && (
|
|
296
|
+
<span
|
|
297
|
+
className="rounded-full px-1.5 py-0.5 text-[10px] font-semibold"
|
|
298
|
+
style={{ background: on ? "rgba(255,255,255,0.25)" : accent, color: onAction }}
|
|
299
|
+
>
|
|
300
|
+
{n}
|
|
301
|
+
</span>
|
|
302
|
+
)}
|
|
303
|
+
</button>
|
|
304
|
+
</li>
|
|
305
|
+
);
|
|
306
|
+
})}
|
|
307
|
+
</ul>
|
|
308
|
+
</nav>
|
|
309
|
+
|
|
310
|
+
{/* CONTENT — the selected group only */}
|
|
311
|
+
<div className="flex-1 overflow-auto p-10" style={{ fontFamily: bodyFont }}>
|
|
312
|
+
<h2 className="mb-1 text-2xl font-bold">{current.title}</h2>
|
|
313
|
+
<p className="mb-8 text-xs" style={{ color: sub }}>
|
|
314
|
+
{foundation ? (
|
|
315
|
+
<>Tokens with an <OverrideMark /> are <b style={{ color: ink }}>{org}</b>’s own; the rest are inherited from the design-system foundation.</>
|
|
316
|
+
) : (
|
|
317
|
+
<>Live tokens from the design-system foundation (<code>rx/design-system/styles</code>). Toggle the theme to restyle everything.</>
|
|
318
|
+
)}
|
|
319
|
+
</p>
|
|
320
|
+
{current.body}
|
|
321
|
+
</div>
|
|
322
|
+
</div>
|
|
323
|
+
);
|
|
324
|
+
}
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* IdentityHeader — the standardized identity presentation for catalog items
|
|
3
|
+
* (apps, skills, prompts, nodes): initials avatar, title + category pill +
|
|
4
|
+
* version, description, and the accented "When to use" callout. Extracted from
|
|
5
|
+
* the Apps view's MCP-app meta panel so every section presents the same anatomy.
|
|
6
|
+
* Designed for the white main panel.
|
|
7
|
+
*/
|
|
8
|
+
import type { ReactNode } from "react";
|
|
9
|
+
|
|
10
|
+
export function IdentityHeader({
|
|
11
|
+
name,
|
|
12
|
+
category,
|
|
13
|
+
version,
|
|
14
|
+
id,
|
|
15
|
+
description,
|
|
16
|
+
whenToUse,
|
|
17
|
+
accent = "#6366f1",
|
|
18
|
+
logoUrl,
|
|
19
|
+
chips,
|
|
20
|
+
right,
|
|
21
|
+
}: {
|
|
22
|
+
name: string;
|
|
23
|
+
category?: string;
|
|
24
|
+
version?: string;
|
|
25
|
+
/** A mono identity line under the title (node type, `{{prompt.*}}` key, URI). */
|
|
26
|
+
id?: string;
|
|
27
|
+
description?: string;
|
|
28
|
+
whenToUse?: string;
|
|
29
|
+
accent?: string;
|
|
30
|
+
/** The item's real logo (node/package `logoUrl`) — replaces the initials avatar. */
|
|
31
|
+
logoUrl?: string | null;
|
|
32
|
+
/** Extra chips after the version (e.g. execution mode). */
|
|
33
|
+
chips?: ReactNode;
|
|
34
|
+
/** Right-aligned actions (e.g. a ToggleSwitch). */
|
|
35
|
+
right?: ReactNode;
|
|
36
|
+
}) {
|
|
37
|
+
const initials = name
|
|
38
|
+
.split(/\s+/)
|
|
39
|
+
.map((w) => w[0])
|
|
40
|
+
.slice(0, 2)
|
|
41
|
+
.join("")
|
|
42
|
+
.toUpperCase();
|
|
43
|
+
return (
|
|
44
|
+
<div className="mb-5 flex items-start gap-3.5">
|
|
45
|
+
{logoUrl ? (
|
|
46
|
+
<div
|
|
47
|
+
className="flex h-11 w-11 shrink-0 items-center justify-center overflow-hidden rounded-xl bg-white p-1.5"
|
|
48
|
+
style={{ border: `1px solid ${accent}33` }}
|
|
49
|
+
>
|
|
50
|
+
<img src={logoUrl} alt="" className="max-h-full max-w-full object-contain" />
|
|
51
|
+
</div>
|
|
52
|
+
) : (
|
|
53
|
+
<div
|
|
54
|
+
className="flex h-11 w-11 shrink-0 items-center justify-center rounded-xl text-[13px] font-bold tracking-tight"
|
|
55
|
+
style={{ background: `${accent}1a`, color: accent, border: `1px solid ${accent}33` }}
|
|
56
|
+
>
|
|
57
|
+
{initials}
|
|
58
|
+
</div>
|
|
59
|
+
)}
|
|
60
|
+
<div className="min-w-0 flex-1">
|
|
61
|
+
<div className="flex flex-wrap items-center gap-2">
|
|
62
|
+
<h1 className="text-xl font-bold leading-tight tracking-tight text-gray-900">{name}</h1>
|
|
63
|
+
{category && (
|
|
64
|
+
<span
|
|
65
|
+
className="rounded-full px-2 py-0.5 text-[10px] font-semibold uppercase tracking-wide"
|
|
66
|
+
style={{ background: `${accent}14`, color: accent }}
|
|
67
|
+
>
|
|
68
|
+
{category}
|
|
69
|
+
</span>
|
|
70
|
+
)}
|
|
71
|
+
{version && <span className="font-mono text-[10px] text-gray-400">v{version}</span>}
|
|
72
|
+
{chips}
|
|
73
|
+
{right && <span className="ml-auto">{right}</span>}
|
|
74
|
+
</div>
|
|
75
|
+
{id && <div className="mt-0.5 font-mono text-[11px] text-gray-400">{id}</div>}
|
|
76
|
+
{description && <p className="mt-1 max-w-3xl text-sm leading-relaxed text-gray-600">{description}</p>}
|
|
77
|
+
{whenToUse && (
|
|
78
|
+
<div className="mt-2 max-w-3xl rounded-r py-1.5 pl-3" style={{ borderLeft: `3px solid ${accent}`, background: "#f3f4f6" }}>
|
|
79
|
+
<div className="mb-0.5 text-[10px] font-semibold uppercase tracking-wider" style={{ color: accent }}>
|
|
80
|
+
When to use
|
|
81
|
+
</div>
|
|
82
|
+
<p className="text-[15px] font-medium leading-relaxed text-gray-900">{whenToUse}</p>
|
|
83
|
+
</div>
|
|
84
|
+
)}
|
|
85
|
+
</div>
|
|
86
|
+
</div>
|
|
87
|
+
);
|
|
88
|
+
}
|