velloo 0.6.0 → 0.8.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 +121 -225
- package/THIRD-PARTY-NOTICES.md +3 -3
- package/canvas/assets/index-CcPwhD7W.js +31 -0
- package/canvas/assets/index-CkVteRZ9.css +2 -0
- package/canvas/assets/lucide-all-Cezxn40A.js +1 -0
- package/canvas/index.html +2 -2
- package/{chunk-rjjc9vam.js → chunk-07fxzrx2.js} +2 -2
- package/{chunk-ff0jfn0v.js → chunk-32cq4wpr.js} +1 -1
- package/{chunk-nx09zqgs.js → chunk-3612470d.js} +1 -1
- package/{chunk-hf4adxe0.js → chunk-45wv1fs5.js} +8 -8
- package/chunk-4g4c9h7k.js +2 -0
- package/{chunk-8vgpffn7.js → chunk-6w42sr61.js} +1 -1
- package/{chunk-esb7n55g.js → chunk-8n07n0cr.js} +3 -3
- package/{chunk-96qhpth5.js → chunk-8qf0ne1p.js} +1 -1
- package/{chunk-56z5xd5b.js → chunk-8ztgcajz.js} +1 -1
- package/chunk-9g45c5md.js +815 -0
- package/{chunk-w2naqxqt.js → chunk-aefn23tr.js} +1 -1
- package/{chunk-jsvkqae2.js → chunk-asqpx5v7.js} +1 -1
- package/chunk-b4hny6hw.js +3 -0
- package/chunk-bntsn3wj.js +2 -0
- package/{chunk-bc7pkjcn.js → chunk-c4164x55.js} +1 -1
- package/{chunk-mfch0b0t.js → chunk-cg0sjdqt.js} +1 -1
- package/{chunk-3trkmgfr.js → chunk-cq2q36d1.js} +1 -1
- package/chunk-d19ttv1a.js +4 -0
- package/{chunk-8grzf8q1.js → chunk-djwvt6jx.js} +1 -1
- package/chunk-dqqpbmxt.js +22 -0
- package/{chunk-1td6z6ey.js → chunk-dy7qwj3t.js} +1 -1
- package/{chunk-dar6wbf2.js → chunk-ejpccmcj.js} +1 -1
- package/{chunk-pxaxgkaq.js → chunk-emty5315.js} +1 -1
- package/chunk-esg0x95a.js +5 -0
- package/{chunk-k423dkjz.js → chunk-ez720ked.js} +1 -1
- package/{chunk-h85hv05d.js → chunk-ffm2jpxp.js} +1 -1
- package/{chunk-6813v926.js → chunk-g3k8k6fq.js} +3 -3
- package/{chunk-63y5gdzw.js → chunk-h3ser37q.js} +1 -1
- package/{chunk-74yxy75r.js → chunk-hsk91jms.js} +1 -1
- package/chunk-jxgwdz2r.js +5 -0
- package/chunk-kvq7pcph.js +2 -0
- package/chunk-m0v3d581.js +2 -0
- package/chunk-majd3gx9.js +3 -0
- package/{chunk-t2x3mztc.js → chunk-mqtnj8ws.js} +1 -1
- package/{chunk-xtwx2p9g.js → chunk-mqx96m2g.js} +1 -1
- package/{chunk-c4spp880.js → chunk-nrxqr4v5.js} +1 -1
- package/{chunk-g185gx8a.js → chunk-nx0ex7qw.js} +1 -1
- package/{chunk-qwccndcd.js → chunk-q7wwqn03.js} +1 -1
- package/chunk-qgbat4fv.js +6 -0
- package/{chunk-08mjtg1e.js → chunk-qqagacne.js} +1 -1
- package/{chunk-69qb8p8s.js → chunk-qx83fdv2.js} +1 -1
- package/{chunk-ff2vt7cz.js → chunk-r5y1sh4f.js} +1 -1
- package/{chunk-2ba6edh8.js → chunk-rg86bhtb.js} +2 -2
- package/{chunk-x02z2ccm.js → chunk-rhfzy9c0.js} +1 -1
- package/{chunk-t6qr2ay9.js → chunk-rrrn1mkq.js} +1 -1
- package/{chunk-56vfy0db.js → chunk-s1cwck4c.js} +1 -1
- package/{chunk-tac2wvc7.js → chunk-sjxp2bx3.js} +1 -1
- package/chunk-skk6yvnw.js +3 -0
- package/{chunk-52m26hh4.js → chunk-t08391mh.js} +1 -1
- package/{chunk-2q561dec.js → chunk-t61vyqpd.js} +1 -1
- package/{chunk-ta84ehgg.js → chunk-v6ks5v53.js} +1 -1
- package/{chunk-hjc5eg5c.js → chunk-w30zfv3m.js} +1 -1
- package/{chunk-tbd6wn3q.js → chunk-x84qrz4q.js} +1 -1
- package/{chunk-chtv0yj6.js → chunk-xg49s85z.js} +147 -30
- package/{chunk-8cj4swpr.js → chunk-xxw67jpk.js} +3 -3
- package/{chunk-eqjtq673.js → chunk-yjwtdm71.js} +1 -1
- package/{chunk-8vam2893.js → chunk-z6r6wxm6.js} +1 -1
- package/{chunk-hsfpbjpc.js → chunk-zgwvkgsz.js} +1 -1
- package/cli.js +1 -1
- package/launcher.cjs +3 -1
- package/package.json +1 -1
- package/pkgs/helpers/src/box.tsx +12 -6
- package/pkgs/helpers/src/descriptors.ts +6 -0
- package/pkgs/helpers/src/icon.tsx +5 -3
- package/pkgs/helpers/src/index.ts +2 -2
- package/pkgs/helpers/src/text.tsx +29 -5
- package/pkgs/provider-antd/src/index.ts +1 -0
- package/pkgs/provider-antd/src/manifest.ts +1 -0
- package/pkgs/provider-chakra/src/manifest.ts +1 -0
- package/pkgs/provider-html/src/index.ts +1 -0
- package/pkgs/provider-mui/src/index.ts +10 -0
- package/pkgs/provider-mui/src/manifest.ts +1 -0
- package/pkgs/provider-none/src/components-inline.tsx +20 -14
- package/pkgs/provider-none/src/components.tsx +2 -4
- package/pkgs/provider-none/src/index.ts +1 -0
- package/pkgs/schema/src/board.ts +13 -1
- package/pkgs/schema/src/config.ts +38 -1
- package/pkgs/schema/src/flow.ts +230 -0
- package/pkgs/schema/src/frame.ts +7 -0
- package/pkgs/schema/src/index.ts +24 -0
- package/pkgs/schema/src/node-identity.ts +54 -1
- package/pkgs/schema/src/node.ts +114 -26
- package/pkgs/schema/src/snippet-resolve.ts +18 -12
- package/pkgs/schema/src/svg-sanitize.ts +3 -0
- package/pkgs/schema/src/theme.ts +56 -0
- package/pkgs/schema/src/typeset.ts +21 -4
- package/pkgs/shadcn-snapshot/dist/manifest.json +6 -0
- package/plugins/claude/velloo/agents/velloo-designer.md +7 -4
- package/plugins/claude/velloo/commands/setup.md +13 -0
- package/plugins/gemini/GEMINI.md +9 -5
- package/plugins/gemini/commands/velloo/setup.toml +11 -0
- package/skills/velloo-design/SKILL.md +11 -6
- package/skills/velloo-setup/SKILL.md +179 -36
- package/canvas/assets/index-CnyI3aig.js +0 -29
- package/canvas/assets/index-GNLd0pCG.css +0 -2
- package/canvas/assets/lucide-all-CH8LmVh0.js +0 -1
- package/chunk-1gkeepkm.js +0 -2
- package/chunk-2web93qz.js +0 -2
- package/chunk-ab8fga32.js +0 -22
- package/chunk-azeqkms7.js +0 -6
- package/chunk-e2btp1wd.js +0 -3
- package/chunk-ey65err5.js +0 -5
- package/chunk-rk238m15.js +0 -4
- package/chunk-wfhv9jg4.js +0 -2
- package/chunk-wnj432fw.js +0 -3
- package/chunk-yfvc8d2g.js +0 -716
- package/chunk-z73q4dq9.js +0 -2
- package/chunk-ztsckexy.js +0 -5
|
@@ -0,0 +1,230 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* An arrow between two frames of one board — what makes a board readable as a
|
|
5
|
+
* flow. It is a spec, not behaviour: nothing here runs, and nothing here emits.
|
|
6
|
+
*
|
|
7
|
+
* One arrow says one of two things, and `state` is what tells them apart:
|
|
8
|
+
*
|
|
9
|
+
* - **A transition** (no `state`): from this step, `label` happens, and the
|
|
10
|
+
* user is on `to`.
|
|
11
|
+
* - **A state** (`state` set): `to` shows the *same step* as `from`, in that
|
|
12
|
+
* state — its loading, empty or error version — and `label` is when.
|
|
13
|
+
*
|
|
14
|
+
* Different layouts are separate screens, so the states of a step are separate
|
|
15
|
+
* frames; the arrows between them are what groups them into one step, which is
|
|
16
|
+
* why a frame carries no state of its own (see {@link deriveFlowSteps}).
|
|
17
|
+
*
|
|
18
|
+
* An arrow has no id: a board holds at most one per ordered pair of frames, so
|
|
19
|
+
* the pair is the identity.
|
|
20
|
+
*/
|
|
21
|
+
export const FlowEdgeSchema = z.object({
|
|
22
|
+
/** Frame id the arrow leaves. */
|
|
23
|
+
from: z.string().min(1),
|
|
24
|
+
/** Frame id the arrow points at. */
|
|
25
|
+
to: z.string().min(1),
|
|
26
|
+
/** The trigger or condition, in plain words ("Submit", "Email already taken"). */
|
|
27
|
+
label: z.string().min(1).optional(),
|
|
28
|
+
/** Present ⇒ `to` is `from`'s step in this state. Free text; see {@link FLOW_STATES}. */
|
|
29
|
+
state: z.string().min(1).optional(),
|
|
30
|
+
/**
|
|
31
|
+
* Where along the arrow its label sits, from 0 at the frame it leaves to 1
|
|
32
|
+
* at the one it reaches. Set when someone slides the label out of the way;
|
|
33
|
+
* absent, the canvas puts it mid-way along the part of the arrow in view.
|
|
34
|
+
* Layout only: no reader of the flow looks at it.
|
|
35
|
+
*/
|
|
36
|
+
labelAt: z.number().min(0).max(1).optional(),
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
export type FlowEdge = z.infer<typeof FlowEdgeSchema>;
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* The states a step conventionally has besides its default one. A vocabulary to
|
|
43
|
+
* offer, not a constraint: `state` stays free text on disk so "offline" or
|
|
44
|
+
* "expired" need no schema change.
|
|
45
|
+
*/
|
|
46
|
+
export const FLOW_STATES = ["loading", "empty", "error", "success"] as const;
|
|
47
|
+
|
|
48
|
+
/** What the frame a step's state arrows start from is called. Never stored. */
|
|
49
|
+
export const DEFAULT_FLOW_STATE = "default";
|
|
50
|
+
|
|
51
|
+
interface FlowBoard {
|
|
52
|
+
frames: ReadonlyArray<{ id: string }>;
|
|
53
|
+
edges?: FlowEdge[] | undefined;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* The arrows whose two ends are both still frames on the board. Reading goes
|
|
58
|
+
* through this so a hand-edited or half-synced file draws what it can instead
|
|
59
|
+
* of failing, and writing goes through it so removing a frame takes its arrows
|
|
60
|
+
* with it wherever the removal happened.
|
|
61
|
+
*/
|
|
62
|
+
export function liveFlowEdges(board: FlowBoard): FlowEdge[] {
|
|
63
|
+
if (!board.edges || board.edges.length === 0) return [];
|
|
64
|
+
const frames = new Set(board.frames.map((frame) => frame.id));
|
|
65
|
+
return board.edges.filter(
|
|
66
|
+
(edge) => edge.from !== edge.to && frames.has(edge.from) && frames.has(edge.to),
|
|
67
|
+
);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/** One frame of a step other than its default one. */
|
|
71
|
+
interface FlowStepState {
|
|
72
|
+
frame: string;
|
|
73
|
+
state: string;
|
|
74
|
+
/** The arrow's label: when the step is in this state. */
|
|
75
|
+
when?: string;
|
|
76
|
+
/**
|
|
77
|
+
* The state this one follows, when its arrow leaves another state rather
|
|
78
|
+
* than the step's default frame: loading → error is "it was sent, then it
|
|
79
|
+
* failed", and without this the read would say only that both exist.
|
|
80
|
+
*/
|
|
81
|
+
after?: string;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* A step of the flow: the frame its state arrows start from, plus the frames
|
|
86
|
+
* those arrows reach.
|
|
87
|
+
*/
|
|
88
|
+
export interface FlowStep {
|
|
89
|
+
/** The default state's frame. Its id stands for the step. */
|
|
90
|
+
frame: string;
|
|
91
|
+
states: FlowStepState[];
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
export interface FlowSteps {
|
|
95
|
+
/** Every frame belongs to exactly one step; a frame with no state arrows is a step alone. */
|
|
96
|
+
steps: FlowStep[];
|
|
97
|
+
/** Arrows that move between steps, endpoints as drawn (possibly a state's frame). */
|
|
98
|
+
transitions: FlowEdge[];
|
|
99
|
+
/** Frame id → the frame that stands for its step. */
|
|
100
|
+
stepOf: Map<string, string>;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Group a board's frames into steps by following its state arrows.
|
|
105
|
+
*
|
|
106
|
+
* A step is a tree: the default frame at the root, a state arrow down to each
|
|
107
|
+
* state, and onward from there (loading → error is "it loaded, then failed").
|
|
108
|
+
* Writes keep that shape — one state arrow into a frame, no cycles — but this
|
|
109
|
+
* reads whatever is on disk, so a state arrow that would break the tree is
|
|
110
|
+
* handed back as a transition rather than trusted.
|
|
111
|
+
*/
|
|
112
|
+
export function deriveFlowSteps(board: FlowBoard): FlowSteps {
|
|
113
|
+
const parent = new Map<string, FlowEdge>();
|
|
114
|
+
const transitions: FlowEdge[] = [];
|
|
115
|
+
for (const edge of liveFlowEdges(board)) {
|
|
116
|
+
if (edge.state === undefined || parent.has(edge.to) || reaches(parent, edge.from, edge.to)) {
|
|
117
|
+
transitions.push(edge);
|
|
118
|
+
} else {
|
|
119
|
+
parent.set(edge.to, edge);
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
const children = new Map<string, FlowEdge[]>();
|
|
124
|
+
for (const edge of parent.values()) {
|
|
125
|
+
const siblings = children.get(edge.from);
|
|
126
|
+
if (siblings) siblings.push(edge);
|
|
127
|
+
else children.set(edge.from, [edge]);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
const steps: FlowStep[] = [];
|
|
131
|
+
const stepOf = new Map<string, string>();
|
|
132
|
+
for (const { id } of board.frames) {
|
|
133
|
+
if (parent.has(id)) continue;
|
|
134
|
+
const states: FlowStepState[] = [];
|
|
135
|
+
stepOf.set(id, id);
|
|
136
|
+
const pending = [...(children.get(id) ?? [])].reverse();
|
|
137
|
+
for (let edge = pending.pop(); edge; edge = pending.pop()) {
|
|
138
|
+
stepOf.set(edge.to, id);
|
|
139
|
+
states.push({
|
|
140
|
+
frame: edge.to,
|
|
141
|
+
state: edge.state as string,
|
|
142
|
+
...(edge.label !== undefined ? { when: edge.label } : {}),
|
|
143
|
+
...(edge.from !== id ? { after: edge.from } : {}),
|
|
144
|
+
});
|
|
145
|
+
pending.push(...[...(children.get(edge.to) ?? [])].reverse());
|
|
146
|
+
}
|
|
147
|
+
steps.push({ frame: id, states });
|
|
148
|
+
}
|
|
149
|
+
return { steps, transitions, stepOf };
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/** Whether walking state arrows up from `start` arrives at `target`. */
|
|
153
|
+
function reaches(parent: Map<string, FlowEdge>, start: string, target: string): boolean {
|
|
154
|
+
for (let at: string | undefined = start; at !== undefined; at = parent.get(at)?.from) {
|
|
155
|
+
if (at === target) return true;
|
|
156
|
+
}
|
|
157
|
+
return false;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* State arrows that can only be transitions inside their step. Anyone who
|
|
162
|
+
* reads `state` as "what the arrow arrives at" draws these, and none of them
|
|
163
|
+
* contradicts anything — so a writer keeps each as the transition it is
|
|
164
|
+
* instead of refusing it:
|
|
165
|
+
*
|
|
166
|
+
* - **a way back**: from a state to the frame it came from, or to the step's
|
|
167
|
+
* default ("fix the fields and resubmit", tagged `recovery`). Taken as a
|
|
168
|
+
* state it would make the step's default a state of its own state.
|
|
169
|
+
* - **a state restated**: into a frame that is already that state of the same
|
|
170
|
+
* step ("try again", arriving at `loading` a second time).
|
|
171
|
+
* - **`default`**: the frame a step's states start from is not one of them.
|
|
172
|
+
*
|
|
173
|
+
* A *different* state for a frame that has one, or the same state claimed by
|
|
174
|
+
* another step, is a real contradiction and is left for the writer to refuse.
|
|
175
|
+
*/
|
|
176
|
+
export function transitionsDrawnAsStates(board: FlowBoard): FlowEdge[] {
|
|
177
|
+
const parent = new Map<string, FlowEdge>();
|
|
178
|
+
const drawn = new Set<FlowEdge>();
|
|
179
|
+
const second: FlowEdge[] = [];
|
|
180
|
+
const edges = liveFlowEdges(board);
|
|
181
|
+
for (const edge of edges) {
|
|
182
|
+
if (edge.state === undefined) continue;
|
|
183
|
+
if (edge.state === DEFAULT_FLOW_STATE || reaches(parent, edge.from, edge.to)) drawn.add(edge);
|
|
184
|
+
else if (parent.has(edge.to)) second.push(edge);
|
|
185
|
+
else parent.set(edge.to, edge);
|
|
186
|
+
}
|
|
187
|
+
// Which step a frame is in is only known once every arrow has been read:
|
|
188
|
+
// the one that puts `from` in the step may come after the one being judged.
|
|
189
|
+
const stepOf = (frame: string): string => {
|
|
190
|
+
let at = frame;
|
|
191
|
+
for (let up = parent.get(at); up; up = parent.get(at)) at = up.from;
|
|
192
|
+
return at;
|
|
193
|
+
};
|
|
194
|
+
for (const edge of second) {
|
|
195
|
+
if (parent.get(edge.to)?.state === edge.state && stepOf(edge.from) === stepOf(edge.to)) {
|
|
196
|
+
drawn.add(edge);
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
return edges.filter((edge) => drawn.has(edge));
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
/**
|
|
203
|
+
* Why a set of arrows can't be a board's flow, or null when it can. The one
|
|
204
|
+
* rule set shared by every writer, so a board never holds arrows
|
|
205
|
+
* {@link deriveFlowSteps} would have to reinterpret.
|
|
206
|
+
*/
|
|
207
|
+
export function flowEdgeProblem(board: FlowBoard): string | null {
|
|
208
|
+
const frames = new Set(board.frames.map((frame) => frame.id));
|
|
209
|
+
const pairs = new Set<string>();
|
|
210
|
+
const parent = new Map<string, FlowEdge>();
|
|
211
|
+
for (const edge of board.edges ?? []) {
|
|
212
|
+
for (const end of [edge.from, edge.to]) {
|
|
213
|
+
if (!frames.has(end)) return `no frame "${end}" on this board`;
|
|
214
|
+
}
|
|
215
|
+
if (edge.from === edge.to) return `"${edge.from}" can't point at itself`;
|
|
216
|
+
const pair = `${edge.from}\u0000${edge.to}`;
|
|
217
|
+
if (pairs.has(pair)) return `two arrows from "${edge.from}" to "${edge.to}"`;
|
|
218
|
+
pairs.add(pair);
|
|
219
|
+
if (edge.state === undefined) continue;
|
|
220
|
+
const other = parent.get(edge.to);
|
|
221
|
+
if (other) {
|
|
222
|
+
return `"${edge.to}" is already the ${other.state} state of "${other.from}" — a frame is one state of one step`;
|
|
223
|
+
}
|
|
224
|
+
if (reaches(parent, edge.from, edge.to)) {
|
|
225
|
+
return `"${edge.to}" → "${edge.from}" → "${edge.to}" loops — a step's states need one default frame to start from`;
|
|
226
|
+
}
|
|
227
|
+
parent.set(edge.to, edge);
|
|
228
|
+
}
|
|
229
|
+
return null;
|
|
230
|
+
}
|
package/pkgs/schema/src/frame.ts
CHANGED
|
@@ -27,6 +27,13 @@ export const FrameSchema = z.object({
|
|
|
27
27
|
group: z.string().optional(),
|
|
28
28
|
/** Pinned render scheme; absent means follow the canvas default. */
|
|
29
29
|
scheme: FrameSchemeSchema.optional(),
|
|
30
|
+
/**
|
|
31
|
+
* `"content"`: the height follows the screen — the daemon measures how tall
|
|
32
|
+
* the screen renders at this width after each change to it and sets `h`.
|
|
33
|
+
* Absent ⇒ `h` is whatever it was last set to, which is every frame someone
|
|
34
|
+
* sized: setting a frame's height drops this.
|
|
35
|
+
*/
|
|
36
|
+
fit: z.literal("content").optional(),
|
|
30
37
|
});
|
|
31
38
|
|
|
32
39
|
export type Frame = z.infer<typeof FrameSchema>;
|
package/pkgs/schema/src/index.ts
CHANGED
|
@@ -53,6 +53,9 @@ export {
|
|
|
53
53
|
HostAppSchema,
|
|
54
54
|
type Library,
|
|
55
55
|
LibrarySchema,
|
|
56
|
+
StylingSchema,
|
|
57
|
+
type ThemeSource,
|
|
58
|
+
ThemeSourceSchema,
|
|
56
59
|
type ViewportPreset,
|
|
57
60
|
ViewportPresetSchema,
|
|
58
61
|
} from "./config.ts";
|
|
@@ -73,6 +76,17 @@ export {
|
|
|
73
76
|
type RepoComponentsManifest,
|
|
74
77
|
RepoComponentsManifestSchema,
|
|
75
78
|
} from "./extension.ts";
|
|
79
|
+
export {
|
|
80
|
+
DEFAULT_FLOW_STATE,
|
|
81
|
+
deriveFlowSteps,
|
|
82
|
+
FLOW_STATES,
|
|
83
|
+
type FlowEdge,
|
|
84
|
+
FlowEdgeSchema,
|
|
85
|
+
type FlowStep,
|
|
86
|
+
flowEdgeProblem,
|
|
87
|
+
liveFlowEdges,
|
|
88
|
+
transitionsDrawnAsStates,
|
|
89
|
+
} from "./flow.ts";
|
|
76
90
|
export {
|
|
77
91
|
type Frame,
|
|
78
92
|
FrameSchema,
|
|
@@ -94,11 +108,15 @@ export {
|
|
|
94
108
|
} from "./migrate.ts";
|
|
95
109
|
export {
|
|
96
110
|
type ComponentNode,
|
|
111
|
+
type ComponentType,
|
|
112
|
+
type ComponentTypeRef,
|
|
97
113
|
type EmitAsRef,
|
|
98
114
|
isComponentNode,
|
|
115
|
+
isComponentType,
|
|
99
116
|
isParamRef,
|
|
100
117
|
isRepoNode,
|
|
101
118
|
isSnippetInstance,
|
|
119
|
+
isTextNode,
|
|
102
120
|
type Node,
|
|
103
121
|
NodeIdSchema,
|
|
104
122
|
NodeSchema,
|
|
@@ -110,11 +128,14 @@ export {
|
|
|
110
128
|
repoImportIssue,
|
|
111
129
|
repoKey,
|
|
112
130
|
type SnippetInstance,
|
|
131
|
+
type TextNode,
|
|
113
132
|
} from "./node.ts";
|
|
114
133
|
export {
|
|
134
|
+
componentTypesIn,
|
|
115
135
|
type EmitAsNode,
|
|
116
136
|
isNode,
|
|
117
137
|
isSyntheticRef,
|
|
138
|
+
mapComponentTypes,
|
|
118
139
|
type NodeShape,
|
|
119
140
|
nodeShape,
|
|
120
141
|
PARAM_TAG_REF,
|
|
@@ -151,12 +172,15 @@ export {
|
|
|
151
172
|
type ColorsOverride,
|
|
152
173
|
ColorsSchema,
|
|
153
174
|
type LooseTokenGroup,
|
|
175
|
+
namedPaletteRoles,
|
|
176
|
+
normalizeThemeName,
|
|
154
177
|
resolveColors,
|
|
155
178
|
spacingCssTokens,
|
|
156
179
|
TAILWIND_SIZE_NAMES,
|
|
157
180
|
type Theme,
|
|
158
181
|
ThemeSchema,
|
|
159
182
|
TypographySchema,
|
|
183
|
+
themeNameProblem,
|
|
160
184
|
} from "./theme.ts";
|
|
161
185
|
export {
|
|
162
186
|
DEFAULT_TYPESET_NAME,
|
|
@@ -1,13 +1,17 @@
|
|
|
1
1
|
import {
|
|
2
2
|
type ComponentNode,
|
|
3
|
+
type ComponentTypeRef,
|
|
3
4
|
type EmitAsRef,
|
|
4
5
|
isComponentNode,
|
|
6
|
+
isComponentType,
|
|
5
7
|
isParamRef,
|
|
6
8
|
isSnippetInstance,
|
|
9
|
+
isTextNode,
|
|
7
10
|
type Node,
|
|
8
11
|
type ParamRef,
|
|
9
12
|
type RepoComponentRef,
|
|
10
13
|
type SnippetInstance,
|
|
14
|
+
type TextNode,
|
|
11
15
|
} from "./node.ts";
|
|
12
16
|
|
|
13
17
|
/**
|
|
@@ -46,7 +50,7 @@ export type EmitAsNode = ComponentNode & { $emitAs: EmitAsRef };
|
|
|
46
50
|
* The order of the `$`-keys is the contract, stated here once and nowhere
|
|
47
51
|
* else: `$snippet`, then `$param`, then a component — where an identity
|
|
48
52
|
* (`$repo`) beats an emit-time facade (`$emitAs`) beats a synthetic ref beats
|
|
49
|
-
* a plain name
|
|
53
|
+
* a plain name — then `$text`. A malformed value carrying both `$snippet` and `$ref` is a
|
|
50
54
|
* snippet instance, as every walker has always read it.
|
|
51
55
|
*/
|
|
52
56
|
export type NodeShape =
|
|
@@ -56,6 +60,8 @@ export type NodeShape =
|
|
|
56
60
|
| { kind: "named"; node: ComponentNode; ref: string }
|
|
57
61
|
| { kind: "snippet"; node: SnippetInstance }
|
|
58
62
|
| { kind: "param"; node: ParamRef }
|
|
63
|
+
/** A run of text beside elements: a bare text node, no element of its own. */
|
|
64
|
+
| { kind: "text"; node: TextNode; text: string }
|
|
59
65
|
/** Not a node at all — a raw scalar or object sitting in a node position. */
|
|
60
66
|
| { kind: "invalid"; value: unknown };
|
|
61
67
|
|
|
@@ -79,6 +85,7 @@ export function nodeShape(value: unknown): NodeShape {
|
|
|
79
85
|
}
|
|
80
86
|
return { kind: "named", node, ref: node.$ref };
|
|
81
87
|
}
|
|
88
|
+
if (isTextNode(node)) return { kind: "text", node, text: node.$text };
|
|
82
89
|
return { kind: "invalid", value };
|
|
83
90
|
}
|
|
84
91
|
|
|
@@ -90,3 +97,49 @@ export function nodeShape(value: unknown): NodeShape {
|
|
|
90
97
|
export function isNode(value: unknown): value is Node {
|
|
91
98
|
return nodeShape(value).kind !== "invalid";
|
|
92
99
|
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Replace every component type in a prop value — the value itself, or one
|
|
103
|
+
* nested in the arrays and plain objects it holds — with what `resolve` makes
|
|
104
|
+
* of it. A node inside the value is left as it is: its own props are resolved
|
|
105
|
+
* when that node is walked. Returns `value` itself when nothing changed, so a
|
|
106
|
+
* props bag with no type in it keeps its identity.
|
|
107
|
+
*
|
|
108
|
+
* A component type is not a node (`nodeShape` calls one `invalid`, so no node
|
|
109
|
+
* position ever takes it), which is why the walkers that read prop values ask
|
|
110
|
+
* this rather than switching on the node union.
|
|
111
|
+
*/
|
|
112
|
+
export function mapComponentTypes(
|
|
113
|
+
value: unknown,
|
|
114
|
+
resolve: (type: ComponentTypeRef) => unknown,
|
|
115
|
+
): unknown {
|
|
116
|
+
if (isComponentType(value)) return resolve(value.$type);
|
|
117
|
+
if (Array.isArray(value)) {
|
|
118
|
+
let changed = false;
|
|
119
|
+
const out = value.map((item) => {
|
|
120
|
+
const next = mapComponentTypes(item, resolve);
|
|
121
|
+
if (next !== item) changed = true;
|
|
122
|
+
return next;
|
|
123
|
+
});
|
|
124
|
+
return changed ? out : value;
|
|
125
|
+
}
|
|
126
|
+
if (value === null || typeof value !== "object" || isNode(value)) return value;
|
|
127
|
+
let changed = false;
|
|
128
|
+
const out: Record<string, unknown> = {};
|
|
129
|
+
for (const [key, nested] of Object.entries(value)) {
|
|
130
|
+
const next = mapComponentTypes(nested, resolve);
|
|
131
|
+
if (next !== nested) changed = true;
|
|
132
|
+
out[key] = next;
|
|
133
|
+
}
|
|
134
|
+
return changed ? out : value;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/** Every component type in a prop value, by the same reach as {@link mapComponentTypes}. */
|
|
138
|
+
export function componentTypesIn(value: unknown): ComponentTypeRef[] {
|
|
139
|
+
const out: ComponentTypeRef[] = [];
|
|
140
|
+
mapComponentTypes(value, (type) => {
|
|
141
|
+
out.push(type);
|
|
142
|
+
return type;
|
|
143
|
+
});
|
|
144
|
+
return out;
|
|
145
|
+
}
|
package/pkgs/schema/src/node.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
|
-
* A node tree is a discriminated union over
|
|
4
|
+
* A node tree is a discriminated union over four shapes, distinguished by
|
|
5
5
|
* which `$`-prefixed key is present:
|
|
6
6
|
*
|
|
7
7
|
* - `ComponentNode` — a real shadcn / velloo component (`{ $ref }`).
|
|
@@ -13,6 +13,11 @@ import { z } from "zod";
|
|
|
13
13
|
* substituted with the corresponding instance arg at render time. May
|
|
14
14
|
* appear as a child node OR anywhere inside a `props` value via the
|
|
15
15
|
* normal JSON walk during substitution.
|
|
16
|
+
* - `TextNode` — `{ $text }`. A run of text sitting beside elements
|
|
17
|
+
* (`<li>Remote <a>Apply</a></li>`). It renders as a bare text node, with no
|
|
18
|
+
* element of its own, so no selector in the app's stylesheet can tell the
|
|
19
|
+
* design's DOM from the app's. Text that is a component's *whole* content
|
|
20
|
+
* stays in `props.children`.
|
|
16
21
|
*
|
|
17
22
|
* Optional fields are spelled `?: T | undefined` rather than `?: T`. Under
|
|
18
23
|
* `exactOptionalPropertyTypes` those differ, but not for a shape that round-
|
|
@@ -116,18 +121,54 @@ export type ParamRef = {
|
|
|
116
121
|
$param: string;
|
|
117
122
|
};
|
|
118
123
|
|
|
119
|
-
export type
|
|
124
|
+
export type TextNode = {
|
|
125
|
+
$text: string;
|
|
126
|
+
};
|
|
127
|
+
|
|
128
|
+
export type Node = ComponentNode | SnippetInstance | ParamRef | TextNode;
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* A component *type* held as a prop value: `component={ScrollArea}`,
|
|
132
|
+
* `as={Link}`, `icon={IconBolt}` — the component itself, not an element of it.
|
|
133
|
+
* The reference is a component node with no props, children or `$id`, so the
|
|
134
|
+
* name resolves exactly as a `$ref` of that name would (the app's component by
|
|
135
|
+
* its `$repo`, else an extension, else the screen's library). It is a value,
|
|
136
|
+
* not a node: it sits anywhere a prop value can, nested in arrays and objects
|
|
137
|
+
* too, and never in a tree's children.
|
|
138
|
+
*/
|
|
139
|
+
export type ComponentTypeRef = {
|
|
140
|
+
$ref: string;
|
|
141
|
+
$repo?: RepoComponentRef | undefined;
|
|
142
|
+
};
|
|
143
|
+
|
|
144
|
+
export type ComponentType = { $type: ComponentTypeRef };
|
|
120
145
|
|
|
146
|
+
// Walkers ask these of raw prop values too (`rightSection: null`), so each one
|
|
147
|
+
// has to answer for a value that is not an object at all.
|
|
121
148
|
export function isComponentNode(n: Node): n is ComponentNode {
|
|
122
|
-
return typeof (n as { $ref?: unknown })
|
|
149
|
+
return typeof (n as { $ref?: unknown } | null | undefined)?.$ref === "string";
|
|
123
150
|
}
|
|
124
151
|
|
|
125
152
|
export function isSnippetInstance(n: Node): n is SnippetInstance {
|
|
126
|
-
return typeof (n as { $snippet?: unknown })
|
|
153
|
+
return typeof (n as { $snippet?: unknown } | null | undefined)?.$snippet === "string";
|
|
127
154
|
}
|
|
128
155
|
|
|
129
156
|
export function isParamRef(n: Node): n is ParamRef {
|
|
130
|
-
return typeof (n as { $param?: unknown })
|
|
157
|
+
return typeof (n as { $param?: unknown } | null | undefined)?.$param === "string";
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
export function isTextNode(n: Node): n is TextNode {
|
|
161
|
+
return typeof (n as { $text?: unknown } | null | undefined)?.$text === "string";
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
export function isComponentType(value: unknown): value is ComponentType {
|
|
165
|
+
const inner = (value as { $type?: unknown } | null | undefined)?.$type;
|
|
166
|
+
return (
|
|
167
|
+
!Array.isArray(value) &&
|
|
168
|
+
typeof inner === "object" &&
|
|
169
|
+
inner !== null &&
|
|
170
|
+
typeof (inner as { $ref?: unknown }).$ref === "string"
|
|
171
|
+
);
|
|
131
172
|
}
|
|
132
173
|
|
|
133
174
|
/**
|
|
@@ -147,18 +188,16 @@ export const NodeIdSchema = z
|
|
|
147
188
|
|
|
148
189
|
/**
|
|
149
190
|
* A bare string/number in a `children` array is a common first-try shape
|
|
150
|
-
* ("just put the label here")
|
|
151
|
-
*
|
|
152
|
-
*
|
|
153
|
-
* `children` typed `Node[]` for every downstream consumer. The styled-run
|
|
154
|
-
* idiom (a mixed array in the `children` *prop*) is still preferred for rich
|
|
155
|
-
* text; this just removes a needless failure.
|
|
191
|
+
* ("just put the label here"), and it means what it says: text. It becomes a
|
|
192
|
+
* text node rather than being rejected, which keeps `children` typed `Node[]`
|
|
193
|
+
* for every downstream consumer. An empty string is nothing to hold.
|
|
156
194
|
*/
|
|
157
|
-
function
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
195
|
+
function scalarChildren(items: (string | number | Node)[]): Node[] {
|
|
196
|
+
return items.flatMap((item): Node[] => {
|
|
197
|
+
if (typeof item !== "string" && typeof item !== "number") return [item];
|
|
198
|
+
const text = String(item);
|
|
199
|
+
return text === "" ? [] : [{ $text: text }];
|
|
200
|
+
});
|
|
162
201
|
}
|
|
163
202
|
|
|
164
203
|
const EmitAsSchema = z.object({
|
|
@@ -202,14 +241,56 @@ export const RepoComponentRefSchema = z.object({
|
|
|
202
241
|
proxy: z.string().min(1).optional(),
|
|
203
242
|
});
|
|
204
243
|
|
|
244
|
+
const ComponentTypeSchema: z.ZodType<ComponentType> = z.strictObject({
|
|
245
|
+
$type: z.strictObject({
|
|
246
|
+
// Printed bare as a JS expression (`component={AppShell.Section}`), so it
|
|
247
|
+
// has to be an identifier path — which also keeps a synthetic `velloo:` ref out.
|
|
248
|
+
$ref: z.string().regex(/^[A-Za-z_$][\w$]*(?:\.[A-Za-z_$][\w$]*)*$/, {
|
|
249
|
+
message: 'a component type\'s $ref must be a component name like "ScrollArea"',
|
|
250
|
+
}),
|
|
251
|
+
$repo: RepoComponentRefSchema.optional(),
|
|
252
|
+
}),
|
|
253
|
+
});
|
|
254
|
+
|
|
255
|
+
/**
|
|
256
|
+
* Prop values are free-form JSON with one exception: an object carrying
|
|
257
|
+
* `$type` must be a well-formed component type, wherever it sits. Its `$repo`
|
|
258
|
+
* reaches codegen's imports and the canvas bundler exactly as a node's does, so
|
|
259
|
+
* it is held to the same checks.
|
|
260
|
+
*/
|
|
261
|
+
function checkComponentTypes(value: unknown, ctx: z.RefinementCtx, path: PropertyKey[]): void {
|
|
262
|
+
if (Array.isArray(value)) {
|
|
263
|
+
value.forEach((item, i) => {
|
|
264
|
+
checkComponentTypes(item, ctx, [...path, i]);
|
|
265
|
+
});
|
|
266
|
+
return;
|
|
267
|
+
}
|
|
268
|
+
if (value === null || typeof value !== "object") return;
|
|
269
|
+
if (Object.hasOwn(value, "$type")) {
|
|
270
|
+
const parsed = ComponentTypeSchema.safeParse(value);
|
|
271
|
+
if (!parsed.success) {
|
|
272
|
+
for (const issue of parsed.error.issues) {
|
|
273
|
+
ctx.addIssue({ code: "custom", message: issue.message, path: [...path, ...issue.path] });
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
return;
|
|
277
|
+
}
|
|
278
|
+
for (const [key, nested] of Object.entries(value))
|
|
279
|
+
checkComponentTypes(nested, ctx, [...path, key]);
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
const PropsSchema = z.record(z.string(), z.unknown()).superRefine((props, ctx) => {
|
|
283
|
+
checkComponentTypes(props, ctx, []);
|
|
284
|
+
});
|
|
285
|
+
|
|
205
286
|
const ComponentNodeSchema: z.ZodType<ComponentNode> = z.lazy(() =>
|
|
206
287
|
z.object({
|
|
207
288
|
$ref: z.string().min(1),
|
|
208
289
|
$id: NodeIdSchema.optional(),
|
|
209
|
-
props:
|
|
290
|
+
props: PropsSchema.optional(),
|
|
210
291
|
children: z
|
|
211
292
|
.array(z.union([z.string(), z.number(), NodeSchema]))
|
|
212
|
-
.transform(
|
|
293
|
+
.transform(scalarChildren)
|
|
213
294
|
.optional(),
|
|
214
295
|
$emitAs: EmitAsSchema.optional(),
|
|
215
296
|
$repo: RepoComponentRefSchema.optional(),
|
|
@@ -251,14 +332,14 @@ const SnippetInstanceSchema: z.ZodType<SnippetInstance> = z.object({
|
|
|
251
332
|
$snippet: z.string().min(1),
|
|
252
333
|
$id: NodeIdSchema.optional(),
|
|
253
334
|
$extraClassName: z.string().optional(),
|
|
254
|
-
args:
|
|
335
|
+
args: PropsSchema.optional(),
|
|
255
336
|
$overrides: z
|
|
256
337
|
.record(
|
|
257
338
|
z.string().regex(/^$|^\d+(\.\d+)*$|^@[a-zA-Z][a-zA-Z0-9_-]*$/, {
|
|
258
339
|
message:
|
|
259
340
|
'override key must be a dotted index path like "0.2", an "@id" reference with a literal leading @ (e.g. "@row-active" to target the body node whose $id is "row-active"), or "" for the body root',
|
|
260
341
|
}),
|
|
261
|
-
z.object({ props:
|
|
342
|
+
z.object({ props: PropsSchema }),
|
|
262
343
|
)
|
|
263
344
|
.optional(),
|
|
264
345
|
});
|
|
@@ -267,10 +348,14 @@ const ParamRefSchema: z.ZodType<ParamRef> = z.object({
|
|
|
267
348
|
$param: z.string().min(1),
|
|
268
349
|
});
|
|
269
350
|
|
|
351
|
+
const TextNodeSchema: z.ZodType<TextNode> = z.strictObject({
|
|
352
|
+
$text: z.string().min(1),
|
|
353
|
+
});
|
|
354
|
+
|
|
270
355
|
/**
|
|
271
356
|
* Key-routed parse instead of `z.union`: a malformed node yields the
|
|
272
357
|
* issues of the *one* branch its `$`-key selects (with a full path),
|
|
273
|
-
* not a
|
|
358
|
+
* not a four-branch union explosion. The MCP layer surfaces these
|
|
274
359
|
* issues verbatim to agents, so error shape is part of the tool UX.
|
|
275
360
|
*/
|
|
276
361
|
export const NodeSchema: z.ZodType<Node> = z
|
|
@@ -280,7 +365,7 @@ export const NodeSchema: z.ZodType<Node> = z
|
|
|
280
365
|
ctx.addIssue({
|
|
281
366
|
code: "custom",
|
|
282
367
|
message:
|
|
283
|
-
'node must be an object with one of "$ref" (component), "$snippet" (instance),
|
|
368
|
+
'node must be an object with one of "$ref" (component), "$snippet" (instance), "$param" (param ref), or "$text" (text beside elements)',
|
|
284
369
|
});
|
|
285
370
|
return z.NEVER;
|
|
286
371
|
}
|
|
@@ -292,12 +377,15 @@ export const NodeSchema: z.ZodType<Node> = z
|
|
|
292
377
|
? SnippetInstanceSchema
|
|
293
378
|
: typeof v.$param === "string"
|
|
294
379
|
? ParamRefSchema
|
|
295
|
-
:
|
|
380
|
+
: typeof v.$text === "string"
|
|
381
|
+
? TextNodeSchema
|
|
382
|
+
: null;
|
|
296
383
|
if (!branch) {
|
|
297
384
|
ctx.addIssue({
|
|
298
385
|
code: "custom",
|
|
299
|
-
message:
|
|
300
|
-
|
|
386
|
+
message: isComponentType(v)
|
|
387
|
+
? `a component type ({"$type": …}) is a prop value, not a node — to render one here, write {"$ref": ${JSON.stringify(v.$type.$ref)}}`
|
|
388
|
+
: 'node needs exactly one of "$ref" (component), "$snippet" (snippet instance), "$param" (param ref, snippet bodies only), or "$text" (text beside elements)',
|
|
301
389
|
});
|
|
302
390
|
return z.NEVER;
|
|
303
391
|
}
|
|
@@ -332,6 +420,6 @@ export const NodeSchema: z.ZodType<Node> = z
|
|
|
332
420
|
* have to narrow by node kind first.
|
|
333
421
|
*/
|
|
334
422
|
export function nodeId(n: Node): string | undefined {
|
|
335
|
-
if (isParamRef(n)) return undefined;
|
|
423
|
+
if (isParamRef(n) || isTextNode(n)) return undefined;
|
|
336
424
|
return (n as { $id?: string }).$id;
|
|
337
425
|
}
|