velloo 0.6.0 → 0.7.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 +116 -227
- package/THIRD-PARTY-NOTICES.md +3 -3
- package/canvas/assets/index-BQ5710lu.js +29 -0
- package/canvas/assets/{lucide-all-CH8LmVh0.js → lucide-all-DIRZdJPK.js} +1 -1
- package/canvas/index.html +1 -1
- package/{chunk-56z5xd5b.js → chunk-0e5j6nqs.js} +1 -1
- package/{chunk-esb7n55g.js → chunk-0pxm5v0b.js} +2 -2
- package/{chunk-xtwx2p9g.js → chunk-0vaff40g.js} +1 -1
- package/{chunk-t6qr2ay9.js → chunk-3m28pvzr.js} +1 -1
- package/{chunk-rjjc9vam.js → chunk-4y2xy1kf.js} +2 -2
- package/{chunk-63y5gdzw.js → chunk-531xzfq5.js} +1 -1
- package/{chunk-c4spp880.js → chunk-5s1wjhtj.js} +1 -1
- package/{chunk-8cj4swpr.js → chunk-5x4nw0zz.js} +3 -3
- package/{chunk-k423dkjz.js → chunk-5yhnnhkd.js} +1 -1
- package/{chunk-8vgpffn7.js → chunk-64pex3mw.js} +1 -1
- package/{chunk-x02z2ccm.js → chunk-667ag8ts.js} +1 -1
- package/chunk-7gyzh0vq.js +5 -0
- package/{chunk-jsvkqae2.js → chunk-82x2jkfq.js} +1 -1
- package/{chunk-6813v926.js → chunk-84n18xgx.js} +2 -2
- package/{chunk-hjc5eg5c.js → chunk-abr2hgay.js} +1 -1
- package/{chunk-ff0jfn0v.js → chunk-acsv6m3n.js} +1 -1
- package/{chunk-08mjtg1e.js → chunk-asfgvzse.js} +1 -1
- package/{chunk-mfch0b0t.js → chunk-b8vcbbrg.js} +1 -1
- package/{chunk-ta84ehgg.js → chunk-bms1sg4c.js} +1 -1
- package/chunk-brnkprt5.js +723 -0
- package/{chunk-1gkeepkm.js → chunk-bys7pqdb.js} +1 -1
- package/{chunk-52m26hh4.js → chunk-d0j8dg1e.js} +1 -1
- package/{chunk-ff2vt7cz.js → chunk-d93kpv7s.js} +1 -1
- package/chunk-dvh0b2r4.js +3 -0
- package/{chunk-1td6z6ey.js → chunk-eh2xp4v6.js} +1 -1
- package/{chunk-96qhpth5.js → chunk-ejppcfa5.js} +1 -1
- package/{chunk-chtv0yj6.js → chunk-ex36z3b3.js} +144 -27
- package/{chunk-3trkmgfr.js → chunk-fmcm06xb.js} +1 -1
- package/{chunk-56vfy0db.js → chunk-fvezyap4.js} +1 -1
- package/{chunk-tac2wvc7.js → chunk-g7d065ek.js} +1 -1
- package/{chunk-ab8fga32.js → chunk-g8xrwjvq.js} +2 -2
- package/{chunk-bc7pkjcn.js → chunk-gbq5wv7x.js} +1 -1
- package/chunk-gcce587w.js +3 -0
- package/{chunk-nx09zqgs.js → chunk-gfppsgvv.js} +1 -1
- package/{chunk-qwccndcd.js → chunk-j306gr0c.js} +1 -1
- package/{chunk-2q561dec.js → chunk-j4fn2a2b.js} +1 -1
- package/chunk-jbgde2v9.js +2 -0
- package/{chunk-2ba6edh8.js → chunk-kkvf0xk2.js} +1 -1
- package/{chunk-h85hv05d.js → chunk-md4mpvqc.js} +1 -1
- package/{chunk-hf4adxe0.js → chunk-mdgwdqnq.js} +1 -1
- package/{chunk-w2naqxqt.js → chunk-pjkkp7xa.js} +1 -1
- package/chunk-qp3wahg4.js +2 -0
- package/{chunk-g185gx8a.js → chunk-qqrac4va.js} +1 -1
- package/{chunk-t2x3mztc.js → chunk-qrnkrkkt.js} +1 -1
- package/{chunk-8grzf8q1.js → chunk-rqd4swxx.js} +1 -1
- package/{chunk-dar6wbf2.js → chunk-rxkvjyst.js} +1 -1
- package/chunk-s84vxcdv.js +4 -0
- package/chunk-sjjnm4a1.js +2 -0
- package/chunk-skk6yvnw.js +3 -0
- package/{chunk-pxaxgkaq.js → chunk-sme6hv6f.js} +1 -1
- package/chunk-snak5zpr.js +5 -0
- package/{chunk-hsfpbjpc.js → chunk-srp5gyr1.js} +1 -1
- package/{chunk-eqjtq673.js → chunk-vdbr81a3.js} +1 -1
- package/{chunk-8vam2893.js → chunk-vs8j3x6t.js} +1 -1
- package/{chunk-69qb8p8s.js → chunk-xg71h7nb.js} +1 -1
- package/chunk-xzf52wvn.js +6 -0
- package/{chunk-tbd6wn3q.js → chunk-ykpctq15.js} +1 -1
- package/{chunk-74yxy75r.js → chunk-zv17t1n7.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/manifest.ts +1 -0
- package/pkgs/provider-chakra/src/manifest.ts +1 -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/schema/src/config.ts +4 -1
- package/pkgs/schema/src/index.ts +3 -0
- package/pkgs/schema/src/node-identity.ts +6 -1
- package/pkgs/schema/src/node.ts +36 -19
- package/pkgs/schema/src/snippet-resolve.ts +18 -12
- package/pkgs/schema/src/svg-sanitize.ts +3 -0
- package/pkgs/schema/src/typeset.ts +21 -4
- package/pkgs/shadcn-snapshot/dist/manifest.json +6 -0
- package/canvas/assets/index-CnyI3aig.js +0 -29
- package/chunk-2web93qz.js +0 -2
- 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
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// @bun
|
|
2
|
-
import{isCancel,confirm,select}from"./chunk-80fjfyha.js";import{designNameIssue}from"./chunk-
|
|
2
|
+
import{isCancel,confirm,select}from"./chunk-80fjfyha.js";import{designNameIssue}from"./chunk-5x4nw0zz.js";import{managedDesignId,localDesignOf,removeLocalDesign,recordedDesignName,designRenameConflict,followDesignRename,loadDesignFolder}from"./chunk-brnkprt5.js";import"./chunk-ex36z3b3.js";import"./chunk-bys7pqdb.js";import{agentRootCandidates}from"./chunk-4y2xy1kf.js";import"./chunk-3m28pvzr.js";import{daemonRoot,readLock,isLive,stopDaemon}from"./chunk-xzf52wvn.js";import{fail}from"./chunk-s23gtejf.js";import"./chunk-eh2xp4v6.js";import"./chunk-acsv6m3n.js";import"./chunk-p93x4pn2.js";import{displayPath}from"./chunk-mdgwdqnq.js";import"./chunk-b8vcbbrg.js";import"./chunk-s84vxcdv.js";import{recordedAppRoot,planAppRootChange,applyAppRootChange,promptAppRootChoice,planRelocation,relocateDesign,runInit2}from"./chunk-g8xrwjvq.js";import"./chunk-kkvf0xk2.js";import{repinAgentConfigs,migrateFolder2}from"./chunk-0vaff40g.js";import"./chunk-pjtg3wnm.js";import"./chunk-5yhnnhkd.js";import"./chunk-vs8j3x6t.js";import"./chunk-82x2jkfq.js";import{findManifest,manifestListing,findDesigns,unregisterDesign,isWithin,writeDesignName,registerLocalDesign,DESIGN_ARG_DESCRIPTION,hasDesignConfig,DESIGN_ID_ARG,resolveDesign,checkoutDesigns}from"./chunk-64pex3mw.js";import"./chunk-abr2hgay.js";import{require_picocolors}from"./chunk-jrsknzjt.js";import{__toESM,defineCommand}from"./chunk-2smsg2ct.js";import{randomUUID}from"crypto";import{existsSync,realpathSync}from"fs";import{readdir,rm}from"fs/promises";import{join,relative,resolve}from"path";var import_picocolors=__toESM(require_picocolors(),1);async function countJson(dir,skipSuffix){try{return(await readdir(dir)).filter((f)=>f.endsWith(".json")&&!(skipSuffix&&f.endsWith(skipSuffix))).length}catch{return 0}}async function daemonState(folder){let rec=await readLock(daemonRoot(folder));if(!rec)return"stopped";return await isLive(rec)?`running :${rec.canvasPort}`:"stopped"}var list=defineCommand({meta:{name:"list",description:"List the repo's designs"},async run(){let cwd=resolve("."),found=await findDesigns(cwd).catch((err)=>{fail("design list",err.message)});if(!found||found.designs.length===0){let only=await resolveDesign(void 0,"design list",{requireConfig:!0,cwd,takesDesign:!1,onFail:(message)=>fail("design list",message)}),name=recordedDesignName(only)??"(unnamed)",owner=await manifestListing(only).catch(()=>null);if(owner&&owner.dir!==cwd){console.log(`velloo design: "${name}" (${relative(cwd,only)||only}) is listed in \`${relative(cwd,owner.path)}\`. Run \`velloo design list\` from \`${relative(cwd,owner.dir)||"."}\` to see that project's designs.`);return}console.log("velloo design: 1 design (no velloo.json yet)"),console.log(` ${import_picocolors.default.dim("1")} ${import_picocolors.default.bold(name)} ${relative(cwd,only)||only} ${await daemonState(only)}`),console.log(import_picocolors.default.dim(" `velloo design add` creates a second design and lists both."));return}let base=found.repo?.dir??found.local[0]?.root??cwd,rows=[];for(let design of found.designs){let live=design.exists&&!design.outsideRepo;rows.push({name:found.conflicts.has(design.name)?`${design.name} (duplicate)`:design.name,path:design.local?`${displayPath(design.root)} (local)`:relative(base,design.root)||".",boards:live?await countJson(join(design.root,"boards"),".notes.json"):0,state:design.outsideRepo?import_picocolors.default.yellow("outside the repo"):live?await daemonState(design.root):import_picocolors.default.yellow("missing"),live})}let pad=(text,width)=>text+" ".repeat(Math.max(0,width-Bun.stringWidth(text))),width=Math.max(...rows.map((r)=>Bun.stringWidth(r.name))),pathWidth=Math.max(...rows.map((r)=>Bun.stringWidth(r.path))),idWidth=String(rows.length).length;console.log(`velloo design: ${rows.length} in ${base}`);for(let[index,row]of rows.entries()){let marker=row.name===found.defaultDesign?import_picocolors.default.cyan("\u25CF"):" ",id=import_picocolors.default.dim(String(index+1).padStart(idWidth)),boards=row.live?`${row.boards} board${row.boards===1?"":"s"}`:"\u2014";console.log(` ${id} ${marker} ${import_picocolors.default.bold(pad(row.name,width))} ${import_picocolors.default.dim(pad(row.path,pathWidth))} ${boards.padEnd(9)} ${row.state}`)}console.log(import_picocolors.default.dim(" Pass a number as --id to the other `velloo design` commands."));for(let[name,designs]of found.conflicts)console.log(import_picocolors.default.yellow(` ! ${designs.length} designs are named "${name}" \u2014 rename one with \`velloo design rename <path> <new-name>\`.`))}}),add=defineCommand({meta:{name:"add",description:"Create another design in this repo (the init wizard, folder pre-chosen)"},args:{path:{type:"positional",required:!1,description:"Where the new design folder goes, relative to the repo (asked when omitted)"},external:{type:"boolean",description:"Create in Velloo-managed external storage"},nonInteractive:{type:"boolean",description:"Use defaults without prompting"},connect:{type:"boolean",default:!0,description:"Wire project agents"},library:{type:"string",description:"Component library (default: the sibling folder's)"},name:{type:"string",description:"The design's name (default: derived from its folder)"}},async run({args}){await runInit2({addFolder:!0,external:args.external,nonInteractive:args.nonInteractive,connect:args.connect,...args.path?{designFolder:args.path}:{},...args.library?{library:args.library}:{},...args.name?{name:args.name}:{}})}}),remove=defineCommand({meta:{name:"remove",description:"Remove a design \u2014 one outside the repo can keep its files (asked, or --delete-content)"},args:{design:{type:"positional",required:!1,description:DESIGN_ARG_DESCRIPTION},id:DESIGN_ID_ARG,deleteContent:{type:"boolean",description:"Also delete the files of a design outside the repo (with --yes, or to skip the question)"},yes:{type:"boolean",description:"Skip the confirmation (required without an interactive terminal)"}},async run({args}){let cwd=resolve("."),folder=await resolveDesign(args.design,"design remove",{id:args.id,interactive:!0,requireConfig:!0,cwd}),local=localDesignOf(folder);if(!Boolean(process.stdin.isTTY)&&args.yes!==!0)fail("design remove","refusing to delete a design folder without --yes");let boards=await countJson(join(folder,"boards"),".notes.json"),screens=await countJson(join(folder,"screens"),".annotations.json"),name=recordedDesignName(folder)??folder;console.log(`velloo design: ${name} (${folder})`),console.log(import_picocolors.default.dim(` ${boards} board${boards===1?"":"s"}, ${screens} screen${screens===1?"":"s"}`));let keepContent=local!==null&&args.deleteContent!==!0;if(args.yes!==!0)if(local&&args.deleteContent!==!0){let managed=managedDesignId(folder)!==null,choice=await select({message:"Remove this design?",initialValue:managed?"delete":"keep",options:[{value:"delete",label:"Delete it and its files",hint:managed?"in Velloo's storage":folder},{value:"keep",label:"Forget it, keep the files",hint:folder},{value:"cancel",label:"Cancel"}]});if(isCancel(choice)||choice==="cancel"){console.log(import_picocolors.default.dim(" Nothing changed."));return}keepContent=choice==="keep"}else{let approved=await confirm({message:"Delete this design folder and everything in it?",initialValue:!1});if(isCancel(approved)||!approved){console.log(import_picocolors.default.dim(" Nothing changed."));return}}if(await stopDaemon(daemonRoot(folder)))console.log(import_picocolors.default.dim(" Stopped its canvas."));let dropped=local?null:await unregisterDesign(folder,cwd).catch((err)=>fail("design remove",err.message));if(local)await removeLocalDesign(local.id);if(!keepContent)await rm(folder,{recursive:!0,force:!0});if(console.log(`velloo design: ${keepContent?"retained content at":"removed"} ${folder}`),local)console.log(import_picocolors.default.dim(` Forgot the local design "${name}".`));if(dropped?.name)console.log(import_picocolors.default.dim(dropped.removedManifest?` Dropped "${dropped.name}" \u2014 velloo.json listed no other designs, so it's gone too.`:` Dropped "${dropped.name}" from velloo.json.`))}}),move=defineCommand({meta:{name:"move",description:"Preview moving a design between the repository, managed storage and another directory; --yes applies"},args:{design:{type:"positional",required:!1,description:DESIGN_ARG_DESCRIPTION},id:DESIGN_ID_ARG,external:{type:"boolean",description:"Move to managed storage (a local design)"},to:{type:"string",description:"New directory \u2014 inside the repository it goes in velloo.json, outside it is local"},yes:{type:"boolean",description:"Apply the displayed move"}},async run({args}){let source=await resolveDesign(args.design,"design move",{id:args.id,interactive:!0,requireConfig:!0}),plan=await planRelocation(source,resolve("."),args.to,args.external===!0).catch((err)=>fail("design move",err.message)),local="a local design on this machine",recorded=plan.sourceManifestPath?plan.destinationLocal?`velloo.json path "${plan.sourceManifestPath}" \u2192 ${"a local design on this machine"}`:`velloo.json path "${plan.sourceManifestPath}" \u2192 "${plan.destinationManifestPath}"`:plan.destinationLocal?"stays a local design on this machine":`${"a local design on this machine"} \u2192 velloo.json path "${plan.destinationManifestPath}"`;if(console.log(`Source: ${plan.source}
|
|
3
3
|
Destination: ${plan.destination}
|
|
4
4
|
Design "${plan.designName}": ${recorded}`),!args.yes){console.log("Preview only. Add --yes to apply the move.");return}await relocateDesign(plan),console.log(`Moved ${plan.designName} to ${plan.destination}. Run velloo connect to refresh existing agent guidance.`)}});async function renameThroughCanvas(folder,name){let rec=await readLock(daemonRoot(folder));if(!rec||!await isLive(rec))return!1;let res;try{res=await fetch(`http://127.0.0.1:${rec.canvasPort}/api/mutate/update_design_name`,{method:"POST",headers:{"content-type":"application/json"},body:JSON.stringify({name}),signal:AbortSignal.timeout(1e4)})}catch(err){fail("design rename",`the running canvas didn't answer (${err.message}).`)}if(res.ok)return!0;let body=await res.json().catch(()=>null);fail("design rename",body?.error?.message??`the running canvas refused (HTTP ${res.status}).`)}var rename=defineCommand({meta:{name:"rename",description:"Rename a design"},args:{design:{type:"positional",required:!1,description:"The design to rename \u2014 its current name or its folder path (or use --id)"},name:{type:"positional",required:!1,description:"The new name"},id:DESIGN_ID_ARG},async run({args}){let cwd=resolve("."),newName=args.id!==void 0&&args.name===void 0?args.design:args.name,target=args.id!==void 0&&args.name===void 0?void 0:args.design;if(newName===void 0)fail("design rename","pass the design and its new name: `velloo design rename <design> <new name>`.");let folder=await resolveDesign(target,"design rename",{id:args.id,requireConfig:!0,cwd}),previous=recordedDesignName(folder);if(previous===newName){console.log(import_picocolors.default.dim(` Already named "${newName}" \u2014 nothing changed.`));return}let conflict=await designRenameConflict(folder,newName).catch((err)=>fail("design rename",err.message));if(conflict)fail("design rename",conflict);if(!await renameThroughCanvas(folder,newName)){if(await writeDesignName(folder,newName),previous&&await followDesignRename(folder,previous,newName))console.log(import_picocolors.default.dim(` velloo.json defaultDesign \u2192 "${newName}"`))}console.log(`velloo design: renamed ${previous?`"${previous}" `:""}to "${newName}".`);let repinned=previous?await repinAgentConfigs({projectRoots:await agentRootCandidates(folder),designFolder:folder,names:[previous]}).catch(()=>[]):[];for(let path of repinned)console.log(import_picocolors.default.dim(` ${relative(cwd,path)||path}: no longer names the design`))}}),upgrade=defineCommand({meta:{name:"upgrade",description:"Migrate a design to this Velloo's format, without upgrading Velloo itself"},args:{design:{type:"positional",required:!1,description:DESIGN_ARG_DESCRIPTION},id:DESIGN_ID_ARG,all:{type:"boolean",description:"Migrate every design in this checkout"},"dry-run":{type:"boolean",default:!1,description:"Show what would change without writing"},skills:{type:"boolean",default:!0,description:"Also refresh the installed agent skills / plugin / rules to this velloo's versions"}},async run({args}){if(args.all&&(args.design||args.id!==void 0))fail("design upgrade","pass a design or --all, not both.");let folders=args.all?await checkoutDesigns(resolve(".")):[await resolveDesign(args.design,"design upgrade",{id:args.id,interactive:!0,requireConfig:!0})];if(folders.length===0)fail("design upgrade","no designs here \u2014 `velloo init` creates one.");for(let folder of folders)await migrateFolder2(folder,{dryRun:args["dry-run"],skills:args.skills})}}),bind=defineCommand({meta:{name:"bind",description:"Attach a design folder outside this checkout to it, on this machine only"},args:{design:{type:"positional",required:!0,description:"The design folder (in managed storage or anywhere outside the checkout)"},name:{type:"string",description:"Rename the design as it is bound"},yes:{type:"boolean",description:"Confirm the local binding"}},async run({args}){let cwd=resolve("."),folder=resolve(args.design);if(!await hasDesignConfig(folder))fail("design bind",`${folder} is not a velloo design folder (no .design/config.json).`);let root=cwd;if(isWithin(root,folder))fail("design bind",`${folder} is inside this checkout (${root}); a design here belongs in velloo.json \u2014 see \`velloo design add\`.`);let name=args.name??recordedDesignName(folder);if(!name)fail("design bind",`${folder} has no design name \u2014 run \`velloo upgrade ${folder}\` first.`);let issue=designNameIssue(name);if(issue)fail("design bind",`invalid --name ${JSON.stringify(name)} \u2014 ${issue}`);let clash=(await findDesigns(root).catch((err)=>fail("design bind",err.message)))?.designs.find((d)=>d.name===name&&!(existsSync(d.root)&&realpathSync(d.root)===realpathSync(folder)));if(clash)fail("design bind",`a design named "${name}" already exists at ${clash.root} \u2014 pass --name to bind this one under another.`);let existing=localDesignOf(folder),appRoot=existing?join(root,relative(existing.root,existing.appRoot)):root,listed=(await findManifest(cwd).catch(()=>null))?.folders.some((path)=>existsSync(path)&&realpathSync(path)===realpathSync(folder));if(console.log(`Design: ${name} (${folder})
|
|
5
5
|
Checkout: ${root}
|
|
@@ -1,3 +1,3 @@
|
|
|
1
1
|
// @bun
|
|
2
|
-
import{loadDesignFolder,liveExtensions,LiveBundler,hostAssetRequest,resolveProviders,findHostTailwindConfig,TailwindJit,createCaptureMount,extraThemeBlock}from"./chunk-
|
|
2
|
+
import{loadDesignFolder,liveExtensions,LiveBundler,hostAssetRequest,resolveProviders,findHostTailwindConfig,TailwindJit,createCaptureMount,extraThemeBlock}from"./chunk-brnkprt5.js";import{join,sep}from"path";var LIVE_BUNDLE_PATH="/live/bundle.js";async function withAssetServer(folder,liveCode,fn,routes={}){let{bundle,host}=routes,assetsRoot=join(folder,"assets"),server=Bun.serve({port:0,hostname:"127.0.0.1",async fetch(req){let url=new URL(req.url),stored=host?.(req);if(stored)return stored;let mounted=bundle?await bundle(url):null;if(mounted!==null)return new Response(mounted,{headers:{"Content-Type":"text/javascript","Access-Control-Allow-Origin":"*"}});if(liveCode!==null&&url.pathname===LIVE_BUNDLE_PATH)return new Response(liveCode,{headers:{"Content-Type":"text/javascript"}});if(url.pathname.startsWith("/assets/")){let fsPath=join(folder,decodeURIComponent(url.pathname));if(fsPath.startsWith(assetsRoot+sep)){let file=Bun.file(fsPath);if(await file.exists())return new Response(file)}}let hostAsset=host&&hostAssetRequest(req),fromHost=hostAsset?host(hostAsset):null;if(fromHost)return fromHost;return new Response("not found",{status:404})}});try{return await fn(`http://127.0.0.1:${server.port}/`)}finally{server.stop(!0)}}import{join as join2}from"path";function createOneShotLiveBundler(root,config){if(Object.keys(liveExtensions(config.extensions)).length===0)return null;return new LiveBundler(root,()=>config,()=>liveExtensions(config.extensions),!0,!0)}async function buildLiveModule(bundler){let bundle=await bundler.build();return{code:bundle.code.length>0?bundle.code:null,warnings:bundle.errors.map((error)=>`live-island: ${error.message}`)}}async function loadPipeline(folderPath){let design=await loadDesignFolder(folderPath),config=design.config,{providers,defaultProvider}=await resolveProviders(config,folderPath),capture=createCaptureMount(design,providers,defaultProvider),liveBundler=createOneShotLiveBundler(folderPath,config),jit=new TailwindJit(Object.values(providers),join2(folderPath,"screens"),void 0,()=>extraThemeBlock(design),()=>[...liveBundler?.hostSourceDirs()??[],...capture.sourceDirs()],()=>findHostTailwindConfig(folderPath,config.hostApp),config.styling?.framework),snapshotCss=await jit.build(),live;return{folderPath,design,config,providers,defaultProvider,jit,snapshotCss,capture,liveModule(){return live??=liveBundler?buildLiveModule(liveBundler):Promise.resolve({code:null,warnings:[]}),live}}}
|
|
3
3
|
export{LIVE_BUNDLE_PATH,withAssetServer,createOneShotLiveBundler,buildLiveModule,loadPipeline};
|
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-acsv6m3n.js";import{TOOL_VERSION}from"./chunk-3m28pvzr.js";import{maybeNotifyAboutUpdate}from"./chunk-s84vxcdv.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/launcher.cjs
CHANGED
|
@@ -80,7 +80,9 @@ const runtime = join(runtimeRoot, "bin", process.platform === "win32" ? "bun.exe
|
|
|
80
80
|
if (!existsSync(runtime)) die(`the Bun runtime package ${runtimeName} is incomplete`);
|
|
81
81
|
|
|
82
82
|
const app = join(__dirname, "cli.js");
|
|
83
|
-
|
|
83
|
+
// `--no-install`: Bun must never answer an import from its global cache or
|
|
84
|
+
// fetch a package to do it — see BUN_RUNTIME_FLAGS in src/bun-runtime.ts.
|
|
85
|
+
const child = spawnSync(runtime, ["--no-install", app, ...process.argv.slice(2)], {
|
|
84
86
|
stdio: "inherit",
|
|
85
87
|
env: {
|
|
86
88
|
...process.env,
|
package/package.json
CHANGED
package/pkgs/helpers/src/box.tsx
CHANGED
|
@@ -9,13 +9,19 @@ import type * as React from "react";
|
|
|
9
9
|
import { createElement } from "react";
|
|
10
10
|
import { cn } from "./cn.ts";
|
|
11
11
|
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
12
|
+
/**
|
|
13
|
+
* An element name: an HTML tag, or one of SVG's camelCase ones
|
|
14
|
+
* (`linearGradient`, `clipPath`). Shared by everything that has to agree on
|
|
15
|
+
* what `as` may hold — compose's lowering, each `Box`, and codegen — because a
|
|
16
|
+
* tag one of them accepts and another doesn't renders as a `div`.
|
|
17
|
+
*/
|
|
18
|
+
export const ELEMENT_TAG = /^[a-z][a-zA-Z0-9]*$/;
|
|
17
19
|
|
|
20
|
+
// Restrict `as` to an element name. Anything else (a component reference, an
|
|
21
|
+
// injection attempt) falls back to div. Rendering the real tag means inline
|
|
22
|
+
// elements (span/strong/a/…) get their natural inline display from the UA
|
|
23
|
+
// stylesheet, instead of a div's block stacking inline runs.
|
|
18
24
|
export function Box({ as, className, ...props }: React.ComponentProps<"div"> & { as?: string }) {
|
|
19
|
-
const tag = typeof as === "string" &&
|
|
25
|
+
const tag = typeof as === "string" && ELEMENT_TAG.test(as) ? as : "div";
|
|
20
26
|
return createElement(tag, { "data-slot": "box", className: cn(className), ...props });
|
|
21
27
|
}
|
|
@@ -233,6 +233,12 @@ export const HELPER_DESCRIPTORS: readonly ComponentDescriptor[] = [
|
|
|
233
233
|
props: [
|
|
234
234
|
{ name: "content", type: "string | undefined", optional: true, control: "string" },
|
|
235
235
|
{ name: "viewBox", type: "string | undefined", optional: true, control: "string" },
|
|
236
|
+
{
|
|
237
|
+
name: "preserveAspectRatio",
|
|
238
|
+
type: "string | undefined",
|
|
239
|
+
optional: true,
|
|
240
|
+
control: "string",
|
|
241
|
+
},
|
|
236
242
|
{ name: "color", type: "string | undefined", optional: true, control: "color" },
|
|
237
243
|
],
|
|
238
244
|
example: {
|
|
@@ -27,11 +27,13 @@ function lookup(name: string): IconNode | undefined {
|
|
|
27
27
|
return viaPascal === undefined ? undefined : ICON_NODES[viaPascal];
|
|
28
28
|
}
|
|
29
29
|
|
|
30
|
-
const FALLBACK = lookup("HelpCircle");
|
|
31
|
-
|
|
32
30
|
export function Icon({ name, className, size = 16, strokeWidth = 2, ...props }: IconProps) {
|
|
33
31
|
const hit = lookup(name);
|
|
34
|
-
|
|
32
|
+
// Looked up here, not once at module level: a call at the top of this file
|
|
33
|
+
// is a side effect to a bundler, which then keeps the module — and the 0.6 MB
|
|
34
|
+
// of icon data behind it — in every bundle that imports anything at all from
|
|
35
|
+
// the helpers index, `Icon` on the screen or not.
|
|
36
|
+
const node = hit ?? lookup("HelpCircle");
|
|
35
37
|
if (!node) return null;
|
|
36
38
|
const tint = hit ? className : cn("text-[var(--color-fg-muted)]", className);
|
|
37
39
|
return (
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export { Box } from "./box.tsx";
|
|
1
|
+
export { Box, ELEMENT_TAG } from "./box.tsx";
|
|
2
2
|
export { cn, mergeTailwind } from "./cn.ts";
|
|
3
3
|
export { HELPER_DESCRIPTORS, helperDescriptors } from "./descriptors.ts";
|
|
4
4
|
export { Divider, type DividerProps } from "./divider.tsx";
|
|
@@ -29,4 +29,4 @@ export {
|
|
|
29
29
|
export { Prose, type ProseProps } from "./prose.tsx";
|
|
30
30
|
export { helpersRegistry } from "./registry.ts";
|
|
31
31
|
export { SVG, type SVGProps } from "./svg.tsx";
|
|
32
|
-
export { Text, type TextProps } from "./text.tsx";
|
|
32
|
+
export { Text, type TextProps, TextRunContext, textTag } from "./text.tsx";
|
|
@@ -3,14 +3,38 @@ import { textClasses } from "@velloo/schema/typeset";
|
|
|
3
3
|
import * as React from "react";
|
|
4
4
|
import { cn } from "./cn.ts";
|
|
5
5
|
|
|
6
|
-
export interface TextProps extends React.HTMLAttributes<
|
|
6
|
+
export interface TextProps extends React.HTMLAttributes<HTMLElement> {
|
|
7
7
|
/** Visual weight. Selects a rung of the theme's type ladder plus a tone. */
|
|
8
8
|
variant?: "default" | "muted" | "small" | "lead";
|
|
9
9
|
}
|
|
10
10
|
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
11
|
+
/**
|
|
12
|
+
* True beneath a `Text`. A paragraph cannot hold a paragraph: an HTML parser
|
|
13
|
+
* ends the outer `<p>` where the inner one starts, so markup that is rendered
|
|
14
|
+
* on the server and parsed again (a screenshot, a comparison) lays the inner
|
|
15
|
+
* text out as a sibling, while the same tree mounted on the client keeps it
|
|
16
|
+
* nested. A `Text` inside a `Text` is a run of that paragraph, so it renders
|
|
17
|
+
* the element a run is — and every path draws the same line.
|
|
18
|
+
*/
|
|
19
|
+
export const TextRunContext = React.createContext(false);
|
|
20
|
+
|
|
21
|
+
/** The element a `Text` renders: a paragraph, or a run inside one. */
|
|
22
|
+
export function textTag(run: boolean): "p" | "span" {
|
|
23
|
+
return run ? "span" : "p";
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export const Text = React.forwardRef<HTMLElement, TextProps>(
|
|
27
|
+
({ variant, className, ...props }, ref) => {
|
|
28
|
+
const run = React.useContext(TextRunContext);
|
|
29
|
+
return (
|
|
30
|
+
<TextRunContext.Provider value={true}>
|
|
31
|
+
{React.createElement(textTag(run), {
|
|
32
|
+
ref,
|
|
33
|
+
className: cn(textClasses(variant, run), className) || undefined,
|
|
34
|
+
...props,
|
|
35
|
+
})}
|
|
36
|
+
</TextRunContext.Provider>
|
|
37
|
+
);
|
|
38
|
+
},
|
|
15
39
|
);
|
|
16
40
|
Text.displayName = "Text";
|
|
@@ -838,6 +838,7 @@ export const ANTD_MANIFEST: Manifest = [
|
|
|
838
838
|
[
|
|
839
839
|
{ name: "content", type: "string", optional: false, control: "string" },
|
|
840
840
|
{ name: "viewBox", type: "string", optional: false, control: "string" },
|
|
841
|
+
{ name: "preserveAspectRatio", type: "string", optional: true, control: "string" },
|
|
841
842
|
],
|
|
842
843
|
"Raw inline SVG markup.",
|
|
843
844
|
),
|
|
@@ -403,6 +403,7 @@ export const CHAKRA_MANIFEST: Manifest = [
|
|
|
403
403
|
[
|
|
404
404
|
{ name: "content", type: "string", optional: false, control: "string" },
|
|
405
405
|
{ name: "viewBox", type: "string", optional: false, control: "string" },
|
|
406
|
+
{ name: "preserveAspectRatio", type: "string", optional: true, control: "string" },
|
|
406
407
|
],
|
|
407
408
|
"Raw inline SVG markup.",
|
|
408
409
|
),
|
|
@@ -250,6 +250,7 @@ export const MUI_MANIFEST: Manifest = [
|
|
|
250
250
|
[
|
|
251
251
|
{ name: "content", type: "string", optional: false, control: "string" },
|
|
252
252
|
{ name: "viewBox", type: "string", optional: false, control: "string" },
|
|
253
|
+
{ name: "preserveAspectRatio", type: "string", optional: true, control: "string" },
|
|
253
254
|
],
|
|
254
255
|
"Raw inline SVG markup.",
|
|
255
256
|
),
|
|
@@ -1,12 +1,7 @@
|
|
|
1
|
+
import { ELEMENT_TAG, TextRunContext, textTag } from "@velloo/helpers";
|
|
1
2
|
import { headingInlineStyle, textInlineStyle } from "@velloo/schema/typeset";
|
|
2
3
|
import * as React from "react";
|
|
3
|
-
import {
|
|
4
|
-
BOX_TAG,
|
|
5
|
-
type ButtonProps,
|
|
6
|
-
type ContainerProps,
|
|
7
|
-
type InputProps,
|
|
8
|
-
type StackProps,
|
|
9
|
-
} from "./components.tsx";
|
|
4
|
+
import type { ButtonProps, ContainerProps, InputProps, StackProps } from "./components.tsx";
|
|
10
5
|
|
|
11
6
|
/**
|
|
12
7
|
* Inline-styled variants of the no-library primitives, for a `none`-CSS folder
|
|
@@ -38,7 +33,10 @@ export const Box = React.forwardRef<
|
|
|
38
33
|
HTMLDivElement,
|
|
39
34
|
React.HTMLAttributes<HTMLDivElement> & { as?: string }
|
|
40
35
|
>(({ as, ...rest }, ref) =>
|
|
41
|
-
React.createElement(typeof as === "string" &&
|
|
36
|
+
React.createElement(typeof as === "string" && ELEMENT_TAG.test(as) ? as : "div", {
|
|
37
|
+
ref,
|
|
38
|
+
...rest,
|
|
39
|
+
}),
|
|
42
40
|
);
|
|
43
41
|
Box.displayName = "Box";
|
|
44
42
|
|
|
@@ -201,13 +199,21 @@ export const Heading = React.forwardRef<HTMLHeadingElement, HeadingProps>(
|
|
|
201
199
|
);
|
|
202
200
|
Heading.displayName = "Heading";
|
|
203
201
|
|
|
204
|
-
interface TextProps extends React.HTMLAttributes<
|
|
202
|
+
interface TextProps extends React.HTMLAttributes<HTMLElement> {
|
|
205
203
|
variant?: "default" | "muted" | "small" | "lead";
|
|
206
204
|
}
|
|
207
205
|
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
206
|
+
// A `Text` inside a `Text` is a run of it — see `TextRunContext`.
|
|
207
|
+
export const Text = React.forwardRef<HTMLElement, TextProps>(({ variant, style, ...rest }, ref) => {
|
|
208
|
+
const run = React.useContext(TextRunContext);
|
|
209
|
+
return (
|
|
210
|
+
<TextRunContext.Provider value={true}>
|
|
211
|
+
{React.createElement(textTag(run), {
|
|
212
|
+
ref,
|
|
213
|
+
style: merge(textInlineStyle(variant, run), style),
|
|
214
|
+
...rest,
|
|
215
|
+
})}
|
|
216
|
+
</TextRunContext.Provider>
|
|
217
|
+
);
|
|
218
|
+
});
|
|
213
219
|
Text.displayName = "Text";
|
|
@@ -3,6 +3,7 @@ import {
|
|
|
3
3
|
BUTTON_VARIANT_CLASS,
|
|
4
4
|
CARD_CLASS,
|
|
5
5
|
CONTAINER_WIDTH_CLASS,
|
|
6
|
+
ELEMENT_TAG,
|
|
6
7
|
INPUT_CLASS,
|
|
7
8
|
STACK_ALIGN_CLASS,
|
|
8
9
|
STACK_JUSTIFY_CLASS,
|
|
@@ -23,12 +24,9 @@ import * as React from "react";
|
|
|
23
24
|
|
|
24
25
|
interface DivProps extends React.HTMLAttributes<HTMLDivElement> {}
|
|
25
26
|
|
|
26
|
-
/** `as` takes a lowercase HTML tag only — anything else renders the div. */
|
|
27
|
-
export const BOX_TAG = /^[a-z][a-z0-9]*$/;
|
|
28
|
-
|
|
29
27
|
export const Box = React.forwardRef<HTMLDivElement, DivProps & { as?: string }>(
|
|
30
28
|
({ as, className, ...rest }, ref) =>
|
|
31
|
-
React.createElement(typeof as === "string" &&
|
|
29
|
+
React.createElement(typeof as === "string" && ELEMENT_TAG.test(as) ? as : "div", {
|
|
32
30
|
ref,
|
|
33
31
|
className: clsx(className),
|
|
34
32
|
...rest,
|
|
@@ -106,6 +106,9 @@ export const HostAppSchema = z.object({
|
|
|
106
106
|
|
|
107
107
|
export type HostApp = z.infer<typeof HostAppSchema>;
|
|
108
108
|
|
|
109
|
+
/** The CSS framework a folder styles with. See `ConfigSchema.styling`. */
|
|
110
|
+
export const StylingSchema = z.object({ framework: z.enum(["tailwind", "none"]) });
|
|
111
|
+
|
|
109
112
|
/**
|
|
110
113
|
* Folder config. Multi-library: a folder registers N libraries by id
|
|
111
114
|
* and pins one as the default; each screen optionally declares which
|
|
@@ -186,7 +189,7 @@ export const ConfigSchema = z
|
|
|
186
189
|
* server rejects a folder whose CSS framework no registered library
|
|
187
190
|
* supports (e.g. shadcn + none).
|
|
188
191
|
*/
|
|
189
|
-
styling:
|
|
192
|
+
styling: StylingSchema.optional(),
|
|
190
193
|
/** Host app location for the live-island bundler. See `HostAppSchema`. */
|
|
191
194
|
hostApp: HostAppSchema.optional(),
|
|
192
195
|
/**
|
package/pkgs/schema/src/index.ts
CHANGED
|
@@ -53,6 +53,7 @@ export {
|
|
|
53
53
|
HostAppSchema,
|
|
54
54
|
type Library,
|
|
55
55
|
LibrarySchema,
|
|
56
|
+
StylingSchema,
|
|
56
57
|
type ViewportPreset,
|
|
57
58
|
ViewportPresetSchema,
|
|
58
59
|
} from "./config.ts";
|
|
@@ -99,6 +100,7 @@ export {
|
|
|
99
100
|
isParamRef,
|
|
100
101
|
isRepoNode,
|
|
101
102
|
isSnippetInstance,
|
|
103
|
+
isTextNode,
|
|
102
104
|
type Node,
|
|
103
105
|
NodeIdSchema,
|
|
104
106
|
NodeSchema,
|
|
@@ -110,6 +112,7 @@ export {
|
|
|
110
112
|
repoImportIssue,
|
|
111
113
|
repoKey,
|
|
112
114
|
type SnippetInstance,
|
|
115
|
+
type TextNode,
|
|
113
116
|
} from "./node.ts";
|
|
114
117
|
export {
|
|
115
118
|
type EmitAsNode,
|
|
@@ -4,10 +4,12 @@ import {
|
|
|
4
4
|
isComponentNode,
|
|
5
5
|
isParamRef,
|
|
6
6
|
isSnippetInstance,
|
|
7
|
+
isTextNode,
|
|
7
8
|
type Node,
|
|
8
9
|
type ParamRef,
|
|
9
10
|
type RepoComponentRef,
|
|
10
11
|
type SnippetInstance,
|
|
12
|
+
type TextNode,
|
|
11
13
|
} from "./node.ts";
|
|
12
14
|
|
|
13
15
|
/**
|
|
@@ -46,7 +48,7 @@ export type EmitAsNode = ComponentNode & { $emitAs: EmitAsRef };
|
|
|
46
48
|
* The order of the `$`-keys is the contract, stated here once and nowhere
|
|
47
49
|
* else: `$snippet`, then `$param`, then a component — where an identity
|
|
48
50
|
* (`$repo`) beats an emit-time facade (`$emitAs`) beats a synthetic ref beats
|
|
49
|
-
* a plain name
|
|
51
|
+
* a plain name — then `$text`. A malformed value carrying both `$snippet` and `$ref` is a
|
|
50
52
|
* snippet instance, as every walker has always read it.
|
|
51
53
|
*/
|
|
52
54
|
export type NodeShape =
|
|
@@ -56,6 +58,8 @@ export type NodeShape =
|
|
|
56
58
|
| { kind: "named"; node: ComponentNode; ref: string }
|
|
57
59
|
| { kind: "snippet"; node: SnippetInstance }
|
|
58
60
|
| { kind: "param"; node: ParamRef }
|
|
61
|
+
/** A run of text beside elements: a bare text node, no element of its own. */
|
|
62
|
+
| { kind: "text"; node: TextNode; text: string }
|
|
59
63
|
/** Not a node at all — a raw scalar or object sitting in a node position. */
|
|
60
64
|
| { kind: "invalid"; value: unknown };
|
|
61
65
|
|
|
@@ -79,6 +83,7 @@ export function nodeShape(value: unknown): NodeShape {
|
|
|
79
83
|
}
|
|
80
84
|
return { kind: "named", node, ref: node.$ref };
|
|
81
85
|
}
|
|
86
|
+
if (isTextNode(node)) return { kind: "text", node, text: node.$text };
|
|
82
87
|
return { kind: "invalid", value };
|
|
83
88
|
}
|
|
84
89
|
|
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,7 +121,11 @@ 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;
|
|
120
129
|
|
|
121
130
|
export function isComponentNode(n: Node): n is ComponentNode {
|
|
122
131
|
return typeof (n as { $ref?: unknown }).$ref === "string";
|
|
@@ -130,6 +139,10 @@ export function isParamRef(n: Node): n is ParamRef {
|
|
|
130
139
|
return typeof (n as { $param?: unknown }).$param === "string";
|
|
131
140
|
}
|
|
132
141
|
|
|
142
|
+
export function isTextNode(n: Node): n is TextNode {
|
|
143
|
+
return typeof (n as { $text?: unknown }).$text === "string";
|
|
144
|
+
}
|
|
145
|
+
|
|
133
146
|
/**
|
|
134
147
|
* Format constraint for `$id` values. Must start with a letter; the rest
|
|
135
148
|
* is letters / digits / dashes / underscores. Matches typical anchor
|
|
@@ -147,18 +160,16 @@ export const NodeIdSchema = z
|
|
|
147
160
|
|
|
148
161
|
/**
|
|
149
162
|
* 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.
|
|
163
|
+
* ("just put the label here"), and it means what it says: text. It becomes a
|
|
164
|
+
* text node rather than being rejected, which keeps `children` typed `Node[]`
|
|
165
|
+
* for every downstream consumer. An empty string is nothing to hold.
|
|
156
166
|
*/
|
|
157
|
-
function
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
167
|
+
function scalarChildren(items: (string | number | Node)[]): Node[] {
|
|
168
|
+
return items.flatMap((item): Node[] => {
|
|
169
|
+
if (typeof item !== "string" && typeof item !== "number") return [item];
|
|
170
|
+
const text = String(item);
|
|
171
|
+
return text === "" ? [] : [{ $text: text }];
|
|
172
|
+
});
|
|
162
173
|
}
|
|
163
174
|
|
|
164
175
|
const EmitAsSchema = z.object({
|
|
@@ -209,7 +220,7 @@ const ComponentNodeSchema: z.ZodType<ComponentNode> = z.lazy(() =>
|
|
|
209
220
|
props: z.record(z.string(), z.unknown()).optional(),
|
|
210
221
|
children: z
|
|
211
222
|
.array(z.union([z.string(), z.number(), NodeSchema]))
|
|
212
|
-
.transform(
|
|
223
|
+
.transform(scalarChildren)
|
|
213
224
|
.optional(),
|
|
214
225
|
$emitAs: EmitAsSchema.optional(),
|
|
215
226
|
$repo: RepoComponentRefSchema.optional(),
|
|
@@ -267,10 +278,14 @@ const ParamRefSchema: z.ZodType<ParamRef> = z.object({
|
|
|
267
278
|
$param: z.string().min(1),
|
|
268
279
|
});
|
|
269
280
|
|
|
281
|
+
const TextNodeSchema: z.ZodType<TextNode> = z.strictObject({
|
|
282
|
+
$text: z.string().min(1),
|
|
283
|
+
});
|
|
284
|
+
|
|
270
285
|
/**
|
|
271
286
|
* Key-routed parse instead of `z.union`: a malformed node yields the
|
|
272
287
|
* issues of the *one* branch its `$`-key selects (with a full path),
|
|
273
|
-
* not a
|
|
288
|
+
* not a four-branch union explosion. The MCP layer surfaces these
|
|
274
289
|
* issues verbatim to agents, so error shape is part of the tool UX.
|
|
275
290
|
*/
|
|
276
291
|
export const NodeSchema: z.ZodType<Node> = z
|
|
@@ -280,7 +295,7 @@ export const NodeSchema: z.ZodType<Node> = z
|
|
|
280
295
|
ctx.addIssue({
|
|
281
296
|
code: "custom",
|
|
282
297
|
message:
|
|
283
|
-
'node must be an object with one of "$ref" (component), "$snippet" (instance),
|
|
298
|
+
'node must be an object with one of "$ref" (component), "$snippet" (instance), "$param" (param ref), or "$text" (text beside elements)',
|
|
284
299
|
});
|
|
285
300
|
return z.NEVER;
|
|
286
301
|
}
|
|
@@ -292,12 +307,14 @@ export const NodeSchema: z.ZodType<Node> = z
|
|
|
292
307
|
? SnippetInstanceSchema
|
|
293
308
|
: typeof v.$param === "string"
|
|
294
309
|
? ParamRefSchema
|
|
295
|
-
:
|
|
310
|
+
: typeof v.$text === "string"
|
|
311
|
+
? TextNodeSchema
|
|
312
|
+
: null;
|
|
296
313
|
if (!branch) {
|
|
297
314
|
ctx.addIssue({
|
|
298
315
|
code: "custom",
|
|
299
316
|
message:
|
|
300
|
-
'node needs exactly one of "$ref" (component), "$snippet" (snippet instance),
|
|
317
|
+
'node needs exactly one of "$ref" (component), "$snippet" (snippet instance), "$param" (param ref, snippet bodies only), or "$text" (text beside elements)',
|
|
301
318
|
});
|
|
302
319
|
return z.NEVER;
|
|
303
320
|
}
|
|
@@ -332,6 +349,6 @@ export const NodeSchema: z.ZodType<Node> = z
|
|
|
332
349
|
* have to narrow by node kind first.
|
|
333
350
|
*/
|
|
334
351
|
export function nodeId(n: Node): string | undefined {
|
|
335
|
-
if (isParamRef(n)) return undefined;
|
|
352
|
+
if (isParamRef(n) || isTextNode(n)) return undefined;
|
|
336
353
|
return (n as { $id?: string }).$id;
|
|
337
354
|
}
|
|
@@ -48,7 +48,7 @@ function isTruthy(v: unknown): boolean {
|
|
|
48
48
|
return true;
|
|
49
49
|
}
|
|
50
50
|
|
|
51
|
-
/** A `$param` that substituted to a
|
|
51
|
+
/** A `$param` that substituted to a boolean or null where a child node was expected. */
|
|
52
52
|
export interface InvalidParamPlacement {
|
|
53
53
|
param: string;
|
|
54
54
|
/** typeof the resolved value (or "null") — for a precise error message. */
|
|
@@ -63,13 +63,17 @@ export interface InvalidParamPlacement {
|
|
|
63
63
|
* Unknown param names are collected in `missing` (the offending
|
|
64
64
|
* substitution resolves to undefined) rather than thrown.
|
|
65
65
|
*
|
|
66
|
-
*
|
|
67
|
-
*
|
|
68
|
-
*
|
|
69
|
-
*
|
|
70
|
-
* the
|
|
71
|
-
*
|
|
72
|
-
*
|
|
66
|
+
* A string or number param in a *node* position — a component node's own
|
|
67
|
+
* `children` array — is the text it holds, and becomes a text node, exactly
|
|
68
|
+
* as a bare string written there does (`scalarChildren` in node.ts). It was a
|
|
69
|
+
* refusal before text nodes existed, and one that only surfaced at the next
|
|
70
|
+
* render: the write was accepted, and the screenshot after it failed. An
|
|
71
|
+
* empty string is nothing to hold.
|
|
72
|
+
*
|
|
73
|
+
* `invalid` collects what is left: a boolean or null param in a node position,
|
|
74
|
+
* which is a flag wired where content goes. A `children` *prop* value
|
|
75
|
+
* (`props.children`) is plain content, not a node position, so any scalar
|
|
76
|
+
* param there is correct and never flagged.
|
|
73
77
|
*/
|
|
74
78
|
export function substituteSnippetParams(
|
|
75
79
|
value: unknown,
|
|
@@ -92,11 +96,13 @@ export function substituteSnippetParams(
|
|
|
92
96
|
const resolved = args[name];
|
|
93
97
|
// An omitted optional param resolves to nothing — never a mis-wire.
|
|
94
98
|
if (resolved === OMITTED) return DROP;
|
|
95
|
-
if (inNodePosition
|
|
96
|
-
|
|
97
|
-
|
|
99
|
+
if (!inNodePosition || (resolved !== null && typeof resolved === "object")) return resolved;
|
|
100
|
+
if (typeof resolved === "string" || typeof resolved === "number") {
|
|
101
|
+
const text = String(resolved);
|
|
102
|
+
return text === "" ? DROP : { $text: text };
|
|
98
103
|
}
|
|
99
|
-
|
|
104
|
+
invalid.push({ param: name, valueType: resolved === null ? "null" : typeof resolved });
|
|
105
|
+
return undefined;
|
|
100
106
|
}
|
|
101
107
|
if (typeof (v as { $if?: unknown }).$if === "string") {
|
|
102
108
|
const cond = v as { $if: string; eq?: unknown; then?: unknown; else?: unknown };
|
|
@@ -76,6 +76,9 @@ const ALLOWED_ELEMENTS: ReadonlyMap<string, string> = new Map(
|
|
|
76
76
|
].map((name) => [name.toLowerCase(), name]),
|
|
77
77
|
);
|
|
78
78
|
|
|
79
|
+
/** The static SVG elements, by their canonical (camelCase) names. */
|
|
80
|
+
export const SVG_ELEMENT_NAMES: ReadonlySet<string> = new Set(ALLOWED_ELEMENTS.values());
|
|
81
|
+
|
|
79
82
|
/**
|
|
80
83
|
* Elements whose presence means the input tried to run or load something.
|
|
81
84
|
* They are dropped like any unknown element; naming them is what lets
|