velloo 0.7.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 +8 -1
- 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-4y2xy1kf.js → chunk-07fxzrx2.js} +1 -1
- package/{chunk-acsv6m3n.js → chunk-32cq4wpr.js} +1 -1
- package/{chunk-gfppsgvv.js → chunk-3612470d.js} +1 -1
- package/{chunk-mdgwdqnq.js → chunk-45wv1fs5.js} +8 -8
- package/chunk-4g4c9h7k.js +2 -0
- package/{chunk-64pex3mw.js → chunk-6w42sr61.js} +1 -1
- package/{chunk-0pxm5v0b.js → chunk-8n07n0cr.js} +2 -2
- package/{chunk-ejppcfa5.js → chunk-8qf0ne1p.js} +1 -1
- package/{chunk-0e5j6nqs.js → chunk-8ztgcajz.js} +1 -1
- package/{chunk-brnkprt5.js → chunk-9g45c5md.js} +142 -50
- package/{chunk-pjkkp7xa.js → chunk-aefn23tr.js} +1 -1
- package/{chunk-82x2jkfq.js → chunk-asqpx5v7.js} +1 -1
- package/{chunk-dvh0b2r4.js → chunk-b4hny6hw.js} +1 -1
- package/chunk-bntsn3wj.js +2 -0
- package/{chunk-gbq5wv7x.js → chunk-c4164x55.js} +1 -1
- package/{chunk-b8vcbbrg.js → chunk-cg0sjdqt.js} +1 -1
- package/{chunk-fmcm06xb.js → chunk-cq2q36d1.js} +1 -1
- package/{chunk-s84vxcdv.js → chunk-d19ttv1a.js} +1 -1
- package/{chunk-rqd4swxx.js → chunk-djwvt6jx.js} +1 -1
- package/chunk-dqqpbmxt.js +22 -0
- package/{chunk-eh2xp4v6.js → chunk-dy7qwj3t.js} +1 -1
- package/{chunk-rxkvjyst.js → chunk-ejpccmcj.js} +1 -1
- package/{chunk-sme6hv6f.js → chunk-emty5315.js} +1 -1
- package/{chunk-7gyzh0vq.js → chunk-esg0x95a.js} +1 -1
- package/{chunk-5yhnnhkd.js → chunk-ez720ked.js} +1 -1
- package/{chunk-md4mpvqc.js → chunk-ffm2jpxp.js} +1 -1
- package/{chunk-84n18xgx.js → chunk-g3k8k6fq.js} +2 -2
- package/{chunk-531xzfq5.js → chunk-h3ser37q.js} +1 -1
- package/{chunk-zv17t1n7.js → chunk-hsk91jms.js} +1 -1
- package/{chunk-snak5zpr.js → chunk-jxgwdz2r.js} +1 -1
- package/chunk-kvq7pcph.js +2 -0
- package/{chunk-qp3wahg4.js → chunk-m0v3d581.js} +1 -1
- package/{chunk-gcce587w.js → chunk-majd3gx9.js} +1 -1
- package/{chunk-qrnkrkkt.js → chunk-mqtnj8ws.js} +1 -1
- package/{chunk-0vaff40g.js → chunk-mqx96m2g.js} +1 -1
- package/{chunk-5s1wjhtj.js → chunk-nrxqr4v5.js} +1 -1
- package/{chunk-qqrac4va.js → chunk-nx0ex7qw.js} +1 -1
- package/{chunk-j306gr0c.js → chunk-q7wwqn03.js} +1 -1
- package/{chunk-xzf52wvn.js → chunk-qgbat4fv.js} +1 -1
- package/{chunk-asfgvzse.js → chunk-qqagacne.js} +1 -1
- package/{chunk-xg71h7nb.js → chunk-qx83fdv2.js} +1 -1
- package/{chunk-d93kpv7s.js → chunk-r5y1sh4f.js} +1 -1
- package/{chunk-kkvf0xk2.js → chunk-rg86bhtb.js} +2 -2
- package/{chunk-667ag8ts.js → chunk-rhfzy9c0.js} +1 -1
- package/{chunk-3m28pvzr.js → chunk-rrrn1mkq.js} +1 -1
- package/{chunk-fvezyap4.js → chunk-s1cwck4c.js} +1 -1
- package/{chunk-g7d065ek.js → chunk-sjxp2bx3.js} +1 -1
- package/{chunk-d0j8dg1e.js → chunk-t08391mh.js} +1 -1
- package/{chunk-j4fn2a2b.js → chunk-t61vyqpd.js} +1 -1
- package/{chunk-bms1sg4c.js → chunk-v6ks5v53.js} +1 -1
- package/{chunk-abr2hgay.js → chunk-w30zfv3m.js} +1 -1
- package/{chunk-ykpctq15.js → chunk-x84qrz4q.js} +1 -1
- package/{chunk-ex36z3b3.js → chunk-xg49s85z.js} +8 -8
- package/{chunk-5x4nw0zz.js → chunk-xxw67jpk.js} +2 -2
- package/{chunk-vdbr81a3.js → chunk-yjwtdm71.js} +1 -1
- package/{chunk-vs8j3x6t.js → chunk-z6r6wxm6.js} +1 -1
- package/{chunk-srp5gyr1.js → chunk-zgwvkgsz.js} +1 -1
- package/cli.js +1 -1
- package/package.json +1 -1
- package/pkgs/provider-antd/src/index.ts +1 -0
- package/pkgs/provider-html/src/index.ts +1 -0
- package/pkgs/provider-mui/src/index.ts +10 -0
- package/pkgs/provider-none/src/index.ts +1 -0
- package/pkgs/schema/src/board.ts +13 -1
- package/pkgs/schema/src/config.ts +34 -0
- package/pkgs/schema/src/flow.ts +230 -0
- package/pkgs/schema/src/frame.ts +7 -0
- package/pkgs/schema/src/index.ts +21 -0
- package/pkgs/schema/src/node-identity.ts +48 -0
- package/pkgs/schema/src/node.ts +80 -9
- package/pkgs/schema/src/theme.ts +56 -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-BQ5710lu.js +0 -29
- package/canvas/assets/index-GNLd0pCG.css +0 -2
- package/canvas/assets/lucide-all-DIRZdJPK.js +0 -1
- package/chunk-bys7pqdb.js +0 -2
- package/chunk-g8xrwjvq.js +0 -22
- package/chunk-jbgde2v9.js +0 -2
- package/chunk-sjjnm4a1.js +0 -2
|
@@ -1,3 +1,3 @@
|
|
|
1
1
|
// @bun
|
|
2
|
-
import{CURRENT_SCHEMA_VERSION}from"./chunk-
|
|
2
|
+
import{CURRENT_SCHEMA_VERSION}from"./chunk-xxw67jpk.js";import{snapshotVersion2}from"./chunk-p93x4pn2.js";import{TOOL_VERSION}from"./chunk-rrrn1mkq.js";function buildDefaultConfig(opts={}){return{schemaVersion:CURRENT_SCHEMA_VERSION,toolVersion:TOOL_VERSION,name:opts.name??"design",folderId:crypto.randomUUID(),libraries:{default:opts.library??{id:"shadcn-upstream",version:snapshotVersion2,source:"binary",componentsPath:"binary"}},defaultLibrary:"default",viewportPresets:[{name:"Mobile",w:390,h:844},{name:"Tablet",w:768,h:1024},{name:"Desktop",w:1440,h:900}],...opts.defaultScreen?{defaultScreen:opts.defaultScreen}:{},...opts.defaultBoard?{defaultBoard:opts.defaultBoard}:{},...opts.hostApp?{hostApp:opts.hostApp}:{},...opts.hostApps?{hostApps:opts.hostApps}:{},...opts.feedback?{feedback:opts.feedback}:{},...opts.styling?{styling:opts.styling}:{},...opts.codegen?{codegen:opts.codegen}:{},...opts.designSystem?{designSystem:opts.designSystem}:{},...opts.themeSource?{themeSource:opts.themeSource}:{}}}
|
|
3
3
|
export{buildDefaultConfig};
|
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
// @bun
|
|
2
|
-
import"./chunk-
|
|
2
|
+
import"./chunk-bntsn3wj.js";import"./chunk-xxw67jpk.js";import{normalizeCloudUrl,loadCredential,listCredentials,deleteCredential,clearCredentials,revokeCredential}from"./chunk-rg86bhtb.js";import{defaultCloudUrl}from"./chunk-w30zfv3m.js";import{defineCommand}from"./chunk-2smsg2ct.js";var logout_default=defineCommand({meta:{name:"logout",description:"Log out of velloo-cloud (signs the CLI token out and removes it)"},args:{url:{type:"string",description:"velloo-cloud base URL (default: $VELLOO_CLOUD_URL or the built-in default)"},all:{type:"boolean",default:!1,description:"Remove saved credentials for every cloud"}},async run({args}){if(args.all){await Promise.all(Object.entries(await listCredentials()).map(([cloudUrl,credential])=>revokeCredential(cloudUrl,credential.token)));let count=await clearCredentials();console.log(count>0?`velloo logout: removed credentials for ${count} cloud${count===1?"":"s"}`:"velloo logout: no saved credentials");return}let cloudUrl=args.url?normalizeCloudUrl(args.url):defaultCloudUrl(),existing=await loadCredential(cloudUrl);if(existing)await revokeCredential(cloudUrl,existing.token);if(await deleteCredential(cloudUrl))console.log(`velloo logout: logged out of ${cloudUrl}${existing?.email?` (${existing.email})`:""}`);else console.log(`velloo logout: not logged in to ${cloudUrl}`)}});export{logout_default as default};
|
package/cli.js
CHANGED
|
@@ -1,3 +1,3 @@
|
|
|
1
1
|
#!/usr/bin/env bun
|
|
2
2
|
// @bun
|
|
3
|
-
import"./chunk-s23gtejf.js";import{COMMANDS}from"./chunk-
|
|
3
|
+
import"./chunk-s23gtejf.js";import{COMMANDS}from"./chunk-32cq4wpr.js";import{TOOL_VERSION}from"./chunk-rrrn1mkq.js";import{maybeNotifyAboutUpdate}from"./chunk-d19ttv1a.js";import{defineCommand,runMain}from"./chunk-2smsg2ct.js";var main=defineCommand({meta:{name:"velloo",version:TOOL_VERSION,description:"Open-source, local-first canvas for agent-driven design."},subCommands:COMMANDS});await runMain(main);var command=process.argv[2];if(!["mcp","__daemon","__update_check","run","upgrade"].includes(command??""))await maybeNotifyAboutUpdate();
|
package/package.json
CHANGED
|
@@ -54,6 +54,7 @@ export function createProvider(): FrameworkAdapter {
|
|
|
54
54
|
),
|
|
55
55
|
styleChannel: STYLE_PROP,
|
|
56
56
|
styleChannels: ["style"],
|
|
57
|
+
themeRoles: ["background", "foreground", "primary", "destructive", "card"],
|
|
57
58
|
mcpIntro: () => [...ANTD_INTRO],
|
|
58
59
|
renderPass: (theme, dark) => makeRenderPass(theme, dark ?? false),
|
|
59
60
|
codegenModule: "antd",
|
|
@@ -59,6 +59,7 @@ export function createProvider(): FrameworkAdapter {
|
|
|
59
59
|
loadManifest: async () => HTML_MANIFEST,
|
|
60
60
|
styleChannel: STYLE_PROP,
|
|
61
61
|
styleChannels: ["style"],
|
|
62
|
+
themeRoles: ["background", "foreground", "card", "muted", "border", "input"],
|
|
62
63
|
registryForChannel: () => registry,
|
|
63
64
|
mcpIntro: () => HTML_INTRO,
|
|
64
65
|
hostStylesheets: true,
|
|
@@ -57,6 +57,16 @@ export function createProvider(): FrameworkAdapter {
|
|
|
57
57
|
),
|
|
58
58
|
styleChannel: SX_PROP,
|
|
59
59
|
styleChannels: ["sx"],
|
|
60
|
+
themeRoles: [
|
|
61
|
+
"background",
|
|
62
|
+
"foreground",
|
|
63
|
+
"primary",
|
|
64
|
+
"secondary",
|
|
65
|
+
"destructive",
|
|
66
|
+
"card",
|
|
67
|
+
"muted",
|
|
68
|
+
"border",
|
|
69
|
+
],
|
|
60
70
|
mcpIntro: () => [...MUI_INTRO],
|
|
61
71
|
renderPass: (theme, dark) => makeRenderPass(theme, dark ?? false),
|
|
62
72
|
codegenModule: "@mui/material",
|
|
@@ -59,6 +59,7 @@ export function createProvider(): FrameworkAdapter {
|
|
|
59
59
|
// is `none` resolves to the inline-`style` channel instead.
|
|
60
60
|
styleChannel: TAILWIND_CLASSNAME,
|
|
61
61
|
styleChannels: ["tailwind-classname", "style"],
|
|
62
|
+
themeRoles: ["background", "foreground", "card", "muted", "border", "input"],
|
|
62
63
|
mcpIntro: (channel) => [...(channel === "style" ? NONE_INLINE_INTRO : NONE_INTRO)],
|
|
63
64
|
// Inline-styled primitives for the `style` channel; Tailwind-classed for
|
|
64
65
|
// every other channel. Lets a `none/none` folder paint with the JIT off.
|
package/pkgs/schema/src/board.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
|
+
import { FlowEdgeSchema } from "./flow.ts";
|
|
2
3
|
import { FrameSchema } from "./frame.ts";
|
|
3
4
|
import { ResourceIdSchema } from "./ids.ts";
|
|
4
5
|
|
|
@@ -12,7 +13,11 @@ import { ResourceIdSchema } from "./ids.ts";
|
|
|
12
13
|
export const BoardGroupSchema = z.object({
|
|
13
14
|
id: ResourceIdSchema,
|
|
14
15
|
name: z.string().min(1),
|
|
15
|
-
/**
|
|
16
|
+
/**
|
|
17
|
+
* CSS color value. The sidebar uses a board group's for its chip and rail;
|
|
18
|
+
* a frame group may carry one, but the canvas draws nothing for frame
|
|
19
|
+
* groups yet.
|
|
20
|
+
*/
|
|
16
21
|
color: z.string().optional(),
|
|
17
22
|
});
|
|
18
23
|
|
|
@@ -61,6 +66,13 @@ export const BoardSchema = z.object({
|
|
|
61
66
|
frames: z.array(FrameSchema).default([]),
|
|
62
67
|
/** Frame regions within this board (see {@link BoardGroupSchema}). */
|
|
63
68
|
groups: z.array(BoardGroupSchema).default([]),
|
|
69
|
+
/**
|
|
70
|
+
* Arrows between this board's frames (see {@link FlowEdgeSchema}). Absent ⇒
|
|
71
|
+
* a board of unconnected frames, which is every existing one — and the field
|
|
72
|
+
* is dropped again when the last arrow goes, so such a board's file never
|
|
73
|
+
* changes shape.
|
|
74
|
+
*/
|
|
75
|
+
edges: z.array(FlowEdgeSchema).optional(),
|
|
64
76
|
});
|
|
65
77
|
|
|
66
78
|
export type Board = z.infer<typeof BoardSchema>;
|
|
@@ -106,6 +106,35 @@ export const HostAppSchema = z.object({
|
|
|
106
106
|
|
|
107
107
|
export type HostApp = z.infer<typeof HostAppSchema>;
|
|
108
108
|
|
|
109
|
+
/**
|
|
110
|
+
* Where the design's theme came from in the app: what `velloo init` read (or
|
|
111
|
+
* left alone), and what an `import_theme` from a stylesheet into the default
|
|
112
|
+
* theme records again. Agents are told by it what checking the theme against
|
|
113
|
+
* the app means — a theme read from a JavaScript theme object (`mantine`,
|
|
114
|
+
* `mui`) is compared with that module, since no stylesheet holds it.
|
|
115
|
+
*
|
|
116
|
+
* "stylesheet" — the app's stylesheet, and what it brings with it.
|
|
117
|
+
* "design-md" — a DESIGN.md, layered on the stylesheet.
|
|
118
|
+
* "mantine" / "mui" — the theme object the app hands that library.
|
|
119
|
+
* "library-defaults" — the app uses `library` and sets no theme of its own,
|
|
120
|
+
* so the design is on that library's.
|
|
121
|
+
* "preset" — nothing of the app's: a built-in preset.
|
|
122
|
+
*/
|
|
123
|
+
export const ThemeSourceSchema = z.object({
|
|
124
|
+
kind: z.enum(["stylesheet", "design-md", "mantine", "mui", "library-defaults", "preset"]),
|
|
125
|
+
/**
|
|
126
|
+
* The app's files it was read from, relative to the app root, the one
|
|
127
|
+
* `kind` names first: a stylesheet is followed by the sheets it imports,
|
|
128
|
+
* the tailwind.config it merged and the modules whose font loader calls
|
|
129
|
+
* set a face. Absent when nothing was read from a file in the app.
|
|
130
|
+
*/
|
|
131
|
+
files: z.array(AppRelativePathSchema).min(1).optional(),
|
|
132
|
+
/** The library whose own theme it is — for `"library-defaults"`. */
|
|
133
|
+
library: z.string().min(1).optional(),
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
export type ThemeSource = z.infer<typeof ThemeSourceSchema>;
|
|
137
|
+
|
|
109
138
|
/** The CSS framework a folder styles with. See `ConfigSchema.styling`. */
|
|
110
139
|
export const StylingSchema = z.object({ framework: z.enum(["tailwind", "none"]) });
|
|
111
140
|
|
|
@@ -206,6 +235,11 @@ export const ConfigSchema = z
|
|
|
206
235
|
* a file somewhere unconventional, or pins the choice when several exist.
|
|
207
236
|
*/
|
|
208
237
|
designSystem: z.object({ path: AppRelativePathSchema }).optional(),
|
|
238
|
+
/**
|
|
239
|
+
* Where the default theme was read from in the app. See `ThemeSourceSchema`.
|
|
240
|
+
* Absent for a design made before it was recorded: nothing is assumed.
|
|
241
|
+
*/
|
|
242
|
+
themeSource: ThemeSourceSchema.optional(),
|
|
209
243
|
/**
|
|
210
244
|
* Named host apps for monorepos — the multi-app twin of `hostApp`, keyed
|
|
211
245
|
* by a short app name (`"web"`, `"admin"`; `init`'s multi-app scan uses
|
|
@@ -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
|
@@ -54,6 +54,8 @@ export {
|
|
|
54
54
|
type Library,
|
|
55
55
|
LibrarySchema,
|
|
56
56
|
StylingSchema,
|
|
57
|
+
type ThemeSource,
|
|
58
|
+
ThemeSourceSchema,
|
|
57
59
|
type ViewportPreset,
|
|
58
60
|
ViewportPresetSchema,
|
|
59
61
|
} from "./config.ts";
|
|
@@ -74,6 +76,17 @@ export {
|
|
|
74
76
|
type RepoComponentsManifest,
|
|
75
77
|
RepoComponentsManifestSchema,
|
|
76
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";
|
|
77
90
|
export {
|
|
78
91
|
type Frame,
|
|
79
92
|
FrameSchema,
|
|
@@ -95,8 +108,11 @@ export {
|
|
|
95
108
|
} from "./migrate.ts";
|
|
96
109
|
export {
|
|
97
110
|
type ComponentNode,
|
|
111
|
+
type ComponentType,
|
|
112
|
+
type ComponentTypeRef,
|
|
98
113
|
type EmitAsRef,
|
|
99
114
|
isComponentNode,
|
|
115
|
+
isComponentType,
|
|
100
116
|
isParamRef,
|
|
101
117
|
isRepoNode,
|
|
102
118
|
isSnippetInstance,
|
|
@@ -115,9 +131,11 @@ export {
|
|
|
115
131
|
type TextNode,
|
|
116
132
|
} from "./node.ts";
|
|
117
133
|
export {
|
|
134
|
+
componentTypesIn,
|
|
118
135
|
type EmitAsNode,
|
|
119
136
|
isNode,
|
|
120
137
|
isSyntheticRef,
|
|
138
|
+
mapComponentTypes,
|
|
121
139
|
type NodeShape,
|
|
122
140
|
nodeShape,
|
|
123
141
|
PARAM_TAG_REF,
|
|
@@ -154,12 +172,15 @@ export {
|
|
|
154
172
|
type ColorsOverride,
|
|
155
173
|
ColorsSchema,
|
|
156
174
|
type LooseTokenGroup,
|
|
175
|
+
namedPaletteRoles,
|
|
176
|
+
normalizeThemeName,
|
|
157
177
|
resolveColors,
|
|
158
178
|
spacingCssTokens,
|
|
159
179
|
TAILWIND_SIZE_NAMES,
|
|
160
180
|
type Theme,
|
|
161
181
|
ThemeSchema,
|
|
162
182
|
TypographySchema,
|
|
183
|
+
themeNameProblem,
|
|
163
184
|
} from "./theme.ts";
|
|
164
185
|
export {
|
|
165
186
|
DEFAULT_TYPESET_NAME,
|
|
@@ -1,7 +1,9 @@
|
|
|
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,
|
|
7
9
|
isTextNode,
|
|
@@ -95,3 +97,49 @@ export function nodeShape(value: unknown): NodeShape {
|
|
|
95
97
|
export function isNode(value: unknown): value is Node {
|
|
96
98
|
return nodeShape(value).kind !== "invalid";
|
|
97
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
|
@@ -127,20 +127,48 @@ export type TextNode = {
|
|
|
127
127
|
|
|
128
128
|
export type Node = ComponentNode | SnippetInstance | ParamRef | TextNode;
|
|
129
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 };
|
|
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.
|
|
130
148
|
export function isComponentNode(n: Node): n is ComponentNode {
|
|
131
|
-
return typeof (n as { $ref?: unknown })
|
|
149
|
+
return typeof (n as { $ref?: unknown } | null | undefined)?.$ref === "string";
|
|
132
150
|
}
|
|
133
151
|
|
|
134
152
|
export function isSnippetInstance(n: Node): n is SnippetInstance {
|
|
135
|
-
return typeof (n as { $snippet?: unknown })
|
|
153
|
+
return typeof (n as { $snippet?: unknown } | null | undefined)?.$snippet === "string";
|
|
136
154
|
}
|
|
137
155
|
|
|
138
156
|
export function isParamRef(n: Node): n is ParamRef {
|
|
139
|
-
return typeof (n as { $param?: unknown })
|
|
157
|
+
return typeof (n as { $param?: unknown } | null | undefined)?.$param === "string";
|
|
140
158
|
}
|
|
141
159
|
|
|
142
160
|
export function isTextNode(n: Node): n is TextNode {
|
|
143
|
-
return typeof (n as { $text?: unknown })
|
|
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
|
+
);
|
|
144
172
|
}
|
|
145
173
|
|
|
146
174
|
/**
|
|
@@ -213,11 +241,53 @@ export const RepoComponentRefSchema = z.object({
|
|
|
213
241
|
proxy: z.string().min(1).optional(),
|
|
214
242
|
});
|
|
215
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
|
+
|
|
216
286
|
const ComponentNodeSchema: z.ZodType<ComponentNode> = z.lazy(() =>
|
|
217
287
|
z.object({
|
|
218
288
|
$ref: z.string().min(1),
|
|
219
289
|
$id: NodeIdSchema.optional(),
|
|
220
|
-
props:
|
|
290
|
+
props: PropsSchema.optional(),
|
|
221
291
|
children: z
|
|
222
292
|
.array(z.union([z.string(), z.number(), NodeSchema]))
|
|
223
293
|
.transform(scalarChildren)
|
|
@@ -262,14 +332,14 @@ const SnippetInstanceSchema: z.ZodType<SnippetInstance> = z.object({
|
|
|
262
332
|
$snippet: z.string().min(1),
|
|
263
333
|
$id: NodeIdSchema.optional(),
|
|
264
334
|
$extraClassName: z.string().optional(),
|
|
265
|
-
args:
|
|
335
|
+
args: PropsSchema.optional(),
|
|
266
336
|
$overrides: z
|
|
267
337
|
.record(
|
|
268
338
|
z.string().regex(/^$|^\d+(\.\d+)*$|^@[a-zA-Z][a-zA-Z0-9_-]*$/, {
|
|
269
339
|
message:
|
|
270
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',
|
|
271
341
|
}),
|
|
272
|
-
z.object({ props:
|
|
342
|
+
z.object({ props: PropsSchema }),
|
|
273
343
|
)
|
|
274
344
|
.optional(),
|
|
275
345
|
});
|
|
@@ -313,8 +383,9 @@ export const NodeSchema: z.ZodType<Node> = z
|
|
|
313
383
|
if (!branch) {
|
|
314
384
|
ctx.addIssue({
|
|
315
385
|
code: "custom",
|
|
316
|
-
message:
|
|
317
|
-
|
|
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)',
|
|
318
389
|
});
|
|
319
390
|
return z.NEVER;
|
|
320
391
|
}
|