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.
Files changed (95) hide show
  1. package/README.md +116 -227
  2. package/THIRD-PARTY-NOTICES.md +3 -3
  3. package/canvas/assets/index-BQ5710lu.js +29 -0
  4. package/canvas/assets/{lucide-all-CH8LmVh0.js → lucide-all-DIRZdJPK.js} +1 -1
  5. package/canvas/index.html +1 -1
  6. package/{chunk-56z5xd5b.js → chunk-0e5j6nqs.js} +1 -1
  7. package/{chunk-esb7n55g.js → chunk-0pxm5v0b.js} +2 -2
  8. package/{chunk-xtwx2p9g.js → chunk-0vaff40g.js} +1 -1
  9. package/{chunk-t6qr2ay9.js → chunk-3m28pvzr.js} +1 -1
  10. package/{chunk-rjjc9vam.js → chunk-4y2xy1kf.js} +2 -2
  11. package/{chunk-63y5gdzw.js → chunk-531xzfq5.js} +1 -1
  12. package/{chunk-c4spp880.js → chunk-5s1wjhtj.js} +1 -1
  13. package/{chunk-8cj4swpr.js → chunk-5x4nw0zz.js} +3 -3
  14. package/{chunk-k423dkjz.js → chunk-5yhnnhkd.js} +1 -1
  15. package/{chunk-8vgpffn7.js → chunk-64pex3mw.js} +1 -1
  16. package/{chunk-x02z2ccm.js → chunk-667ag8ts.js} +1 -1
  17. package/chunk-7gyzh0vq.js +5 -0
  18. package/{chunk-jsvkqae2.js → chunk-82x2jkfq.js} +1 -1
  19. package/{chunk-6813v926.js → chunk-84n18xgx.js} +2 -2
  20. package/{chunk-hjc5eg5c.js → chunk-abr2hgay.js} +1 -1
  21. package/{chunk-ff0jfn0v.js → chunk-acsv6m3n.js} +1 -1
  22. package/{chunk-08mjtg1e.js → chunk-asfgvzse.js} +1 -1
  23. package/{chunk-mfch0b0t.js → chunk-b8vcbbrg.js} +1 -1
  24. package/{chunk-ta84ehgg.js → chunk-bms1sg4c.js} +1 -1
  25. package/chunk-brnkprt5.js +723 -0
  26. package/{chunk-1gkeepkm.js → chunk-bys7pqdb.js} +1 -1
  27. package/{chunk-52m26hh4.js → chunk-d0j8dg1e.js} +1 -1
  28. package/{chunk-ff2vt7cz.js → chunk-d93kpv7s.js} +1 -1
  29. package/chunk-dvh0b2r4.js +3 -0
  30. package/{chunk-1td6z6ey.js → chunk-eh2xp4v6.js} +1 -1
  31. package/{chunk-96qhpth5.js → chunk-ejppcfa5.js} +1 -1
  32. package/{chunk-chtv0yj6.js → chunk-ex36z3b3.js} +144 -27
  33. package/{chunk-3trkmgfr.js → chunk-fmcm06xb.js} +1 -1
  34. package/{chunk-56vfy0db.js → chunk-fvezyap4.js} +1 -1
  35. package/{chunk-tac2wvc7.js → chunk-g7d065ek.js} +1 -1
  36. package/{chunk-ab8fga32.js → chunk-g8xrwjvq.js} +2 -2
  37. package/{chunk-bc7pkjcn.js → chunk-gbq5wv7x.js} +1 -1
  38. package/chunk-gcce587w.js +3 -0
  39. package/{chunk-nx09zqgs.js → chunk-gfppsgvv.js} +1 -1
  40. package/{chunk-qwccndcd.js → chunk-j306gr0c.js} +1 -1
  41. package/{chunk-2q561dec.js → chunk-j4fn2a2b.js} +1 -1
  42. package/chunk-jbgde2v9.js +2 -0
  43. package/{chunk-2ba6edh8.js → chunk-kkvf0xk2.js} +1 -1
  44. package/{chunk-h85hv05d.js → chunk-md4mpvqc.js} +1 -1
  45. package/{chunk-hf4adxe0.js → chunk-mdgwdqnq.js} +1 -1
  46. package/{chunk-w2naqxqt.js → chunk-pjkkp7xa.js} +1 -1
  47. package/chunk-qp3wahg4.js +2 -0
  48. package/{chunk-g185gx8a.js → chunk-qqrac4va.js} +1 -1
  49. package/{chunk-t2x3mztc.js → chunk-qrnkrkkt.js} +1 -1
  50. package/{chunk-8grzf8q1.js → chunk-rqd4swxx.js} +1 -1
  51. package/{chunk-dar6wbf2.js → chunk-rxkvjyst.js} +1 -1
  52. package/chunk-s84vxcdv.js +4 -0
  53. package/chunk-sjjnm4a1.js +2 -0
  54. package/chunk-skk6yvnw.js +3 -0
  55. package/{chunk-pxaxgkaq.js → chunk-sme6hv6f.js} +1 -1
  56. package/chunk-snak5zpr.js +5 -0
  57. package/{chunk-hsfpbjpc.js → chunk-srp5gyr1.js} +1 -1
  58. package/{chunk-eqjtq673.js → chunk-vdbr81a3.js} +1 -1
  59. package/{chunk-8vam2893.js → chunk-vs8j3x6t.js} +1 -1
  60. package/{chunk-69qb8p8s.js → chunk-xg71h7nb.js} +1 -1
  61. package/chunk-xzf52wvn.js +6 -0
  62. package/{chunk-tbd6wn3q.js → chunk-ykpctq15.js} +1 -1
  63. package/{chunk-74yxy75r.js → chunk-zv17t1n7.js} +1 -1
  64. package/cli.js +1 -1
  65. package/launcher.cjs +3 -1
  66. package/package.json +1 -1
  67. package/pkgs/helpers/src/box.tsx +12 -6
  68. package/pkgs/helpers/src/descriptors.ts +6 -0
  69. package/pkgs/helpers/src/icon.tsx +5 -3
  70. package/pkgs/helpers/src/index.ts +2 -2
  71. package/pkgs/helpers/src/text.tsx +29 -5
  72. package/pkgs/provider-antd/src/manifest.ts +1 -0
  73. package/pkgs/provider-chakra/src/manifest.ts +1 -0
  74. package/pkgs/provider-mui/src/manifest.ts +1 -0
  75. package/pkgs/provider-none/src/components-inline.tsx +20 -14
  76. package/pkgs/provider-none/src/components.tsx +2 -4
  77. package/pkgs/schema/src/config.ts +4 -1
  78. package/pkgs/schema/src/index.ts +3 -0
  79. package/pkgs/schema/src/node-identity.ts +6 -1
  80. package/pkgs/schema/src/node.ts +36 -19
  81. package/pkgs/schema/src/snippet-resolve.ts +18 -12
  82. package/pkgs/schema/src/svg-sanitize.ts +3 -0
  83. package/pkgs/schema/src/typeset.ts +21 -4
  84. package/pkgs/shadcn-snapshot/dist/manifest.json +6 -0
  85. package/canvas/assets/index-CnyI3aig.js +0 -29
  86. package/chunk-2web93qz.js +0 -2
  87. package/chunk-azeqkms7.js +0 -6
  88. package/chunk-e2btp1wd.js +0 -3
  89. package/chunk-ey65err5.js +0 -5
  90. package/chunk-rk238m15.js +0 -4
  91. package/chunk-wfhv9jg4.js +0 -2
  92. package/chunk-wnj432fw.js +0 -3
  93. package/chunk-yfvc8d2g.js +0 -716
  94. package/chunk-z73q4dq9.js +0 -2
  95. 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-8cj4swpr.js";import{managedDesignId,localDesignOf,removeLocalDesign,recordedDesignName,designRenameConflict,followDesignRename,loadDesignFolder}from"./chunk-yfvc8d2g.js";import"./chunk-chtv0yj6.js";import"./chunk-1gkeepkm.js";import{agentRootCandidates}from"./chunk-rjjc9vam.js";import"./chunk-t6qr2ay9.js";import{daemonRoot,readLock,isLive,stopDaemon}from"./chunk-azeqkms7.js";import{fail}from"./chunk-s23gtejf.js";import"./chunk-1td6z6ey.js";import"./chunk-ff0jfn0v.js";import"./chunk-p93x4pn2.js";import{displayPath}from"./chunk-hf4adxe0.js";import"./chunk-mfch0b0t.js";import"./chunk-rk238m15.js";import{recordedAppRoot,planAppRootChange,applyAppRootChange,promptAppRootChoice,planRelocation,relocateDesign,runInit2}from"./chunk-ab8fga32.js";import"./chunk-2ba6edh8.js";import{repinAgentConfigs,migrateFolder2}from"./chunk-xtwx2p9g.js";import"./chunk-pjtg3wnm.js";import"./chunk-k423dkjz.js";import"./chunk-8vam2893.js";import"./chunk-jsvkqae2.js";import{findManifest,manifestListing,findDesigns,unregisterDesign,isWithin,writeDesignName,registerLocalDesign,DESIGN_ARG_DESCRIPTION,hasDesignConfig,DESIGN_ID_ARG,resolveDesign,checkoutDesigns}from"./chunk-8vgpffn7.js";import"./chunk-hjc5eg5c.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}
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-yfvc8d2g.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}}}
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-ff0jfn0v.js";import{TOOL_VERSION}from"./chunk-t6qr2ay9.js";import{maybeNotifyAboutUpdate}from"./chunk-rk238m15.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();
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
- const child = spawnSync(runtime, [app, ...process.argv.slice(2)], {
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "velloo",
3
- "version": "0.6.0",
3
+ "version": "0.7.0",
4
4
  "type": "module",
5
5
  "description": "Local-first, code-shaped design canvas — your AI agent designs with your real components, in your repo",
6
6
  "license": "Apache-2.0",
@@ -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
- // Restrict `as` to a lowercase HTML tag name. Anything else (a component
13
- // reference, an injection attempt) falls back to div. Rendering the real tag
14
- // means inline elements (span/strong/a/…) get their natural inline display
15
- // from the UA stylesheet, instead of a div's block stacking inline runs.
16
- const TAG = /^[a-z][a-z0-9]*$/;
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" && TAG.test(as) ? as : "div";
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
- const node = hit ?? FALLBACK;
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<HTMLParagraphElement> {
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
- export const Text = React.forwardRef<HTMLParagraphElement, TextProps>(
12
- ({ variant = "default", className, ...props }, ref) => (
13
- <p ref={ref} className={cn(textClasses(variant), className)} {...props} />
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" && BOX_TAG.test(as) ? as : "div", { ref, ...rest }),
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<HTMLParagraphElement> {
202
+ interface TextProps extends React.HTMLAttributes<HTMLElement> {
205
203
  variant?: "default" | "muted" | "small" | "lead";
206
204
  }
207
205
 
208
- export const Text = React.forwardRef<HTMLParagraphElement, TextProps>(
209
- ({ variant = "default", style, ...rest }, ref) => (
210
- <p ref={ref} style={merge(textInlineStyle(variant), style)} {...rest} />
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" && BOX_TAG.test(as) ? as : "div", {
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: z.object({ framework: z.enum(["tailwind", "none"]) }).optional(),
192
+ styling: StylingSchema.optional(),
190
193
  /** Host app location for the live-island bundler. See `HostAppSchema`. */
191
194
  hostApp: HostAppSchema.optional(),
192
195
  /**
@@ -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. A malformed value carrying both `$snippet` and `$ref` is a
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
 
@@ -1,7 +1,7 @@
1
1
  import { z } from "zod";
2
2
 
3
3
  /**
4
- * A node tree is a discriminated union over three shapes, distinguished by
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 Node = ComponentNode | SnippetInstance | ParamRef;
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"). Rather than reject it — `children` renders
151
- * nodes only — auto-wrap it into an inline `Box as="span"` so it renders as
152
- * text without stacking. Wrapping (vs. allowing scalars through) keeps
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 wrapScalarChild(item: string | number | Node): Node {
158
- if (typeof item === "string" || typeof item === "number") {
159
- return { $ref: "Box", props: { as: "span", children: item } };
160
- }
161
- return item;
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((items) => items.map(wrapScalarChild))
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 three-branch union explosion. The MCP layer surfaces these
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), or "$param" (param ref)',
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
- : null;
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), or "$param" (param ref, snippet bodies only)',
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 scalar where a child node was expected. */
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
- * `invalid` collects scalar params dropped into a *node* position — a
67
- * component node's own `children` array, where only subtrees render. This
68
- * is the classic mis-wire (a `string` param used as a child instead of as
69
- * a prop value); flagging it lets the renderer name the param and point at
70
- * the fix instead of failing opaquely deep in React. A `children` *prop*
71
- * value (`props.children`) is plain content, not a node position, so a
72
- * scalar param there is correct and never flagged.
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 && (resolved === null || typeof resolved !== "object")) {
96
- invalid.push({ param: name, valueType: resolved === null ? "null" : typeof resolved });
97
- return undefined;
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
- return resolved;
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