@popcraft/cli 0.20.2 → 0.21.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 (2) hide show
  1. package/dist/popcraft.mjs +207 -39
  2. package/package.json +2 -2
package/dist/popcraft.mjs CHANGED
@@ -1186,7 +1186,7 @@ function operations(spec = openapi) {
1186
1186
  }
1187
1187
  async function call(target2, method, path, opts = {}) {
1188
1188
  const qs = new URLSearchParams(opts.query ?? {}).toString();
1189
- const headers = { "user-agent": `popcraft-cli/${"0.20.2"}`, ...opts.headers };
1189
+ const headers = { "user-agent": `popcraft-cli/${"0.21.0"}`, ...opts.headers };
1190
1190
  if (target2.token) headers.authorization = `Bearer ${target2.token}`;
1191
1191
  if (opts.contentType) headers["content-type"] = opts.contentType;
1192
1192
  try {
@@ -5303,6 +5303,9 @@ var init_properties = __esm({
5303
5303
  // Fill
5304
5304
  itemProp(FILLS, "color", "Color", { valueType: "color", group: "Fill", control: color, essential: true, accepts: (it) => it.type === "SOLID" }),
5305
5305
  itemProp(FILLS, "opacity", "Opacity", { valueType: "number", group: "Fill", control: percent, accepts: () => true, defaultOf: () => 1 }),
5306
+ // A picture's address, swapped whole: what an IMAGE component property feeds (a product card's photo).
5307
+ itemProp(FILLS, "imageUrl", "Picture", { valueType: "string", group: "Fill", control: { kind: "text" }, accepts: (it) => it.type === "IMAGE" }),
5308
+ nodeProp("imageUrl", "Picture", { valueType: "string", group: "Fill", control: { kind: "text" }, appliesTo: (n) => n.type === "IMAGE" }),
5306
5309
  itemProp(FILLS, "color", "Color", { valueType: "color", group: "Fill", control: color, accepts: (it) => GRADIENTS.has(it.type), sub: { list: "gradientStops", label: (j) => `Stop ${j + 1}` } }),
5307
5310
  itemProp(FILLS, "position", "Position", { valueType: "number", group: "Fill", control: percent, accepts: (it) => GRADIENTS.has(it.type), sub: { list: "gradientStops", label: (j) => `Stop ${j + 1}` } }),
5308
5311
  shaderParamProp(),
@@ -32639,6 +32642,7 @@ var init_en2 = __esm({
32639
32642
  "command:node.align#right": "Align right",
32640
32643
  "command:node.align#top": "Align top",
32641
32644
  "command:node.applyAutoLayout": "Infer auto layout",
32645
+ "command:node.bindField": "Bind to a field",
32642
32646
  "command:node.delete": "Delete selection",
32643
32647
  "command:node.distribute": "Distribute",
32644
32648
  "command:node.distribute#horizontal": "Distribute horizontally",
@@ -32662,6 +32666,7 @@ var init_en2 = __esm({
32662
32666
  "command:node.setLock": "Lock",
32663
32667
  "command:node.setLock#lock": "Lock selection",
32664
32668
  "command:node.setLock#unlock": "Unlock selection",
32669
+ "command:node.setRecord": "Show a record",
32665
32670
  "command:node.setRepeat": "Repeat for each record",
32666
32671
  "command:node.setResponsive": "Breakpoint overrides",
32667
32672
  "command:node.setSemantics": "Web semantics",
@@ -32957,6 +32962,16 @@ var init_en2 = __esm({
32957
32962
  "cropToolbar.keepThisCropEnter": "Keep this crop (Enter)",
32958
32963
  "cropToolbar.putTheCropBackAs": "Put the crop back as it was (Esc)",
32959
32964
  "cropToolbar.ratio": "Ratio",
32965
+ "dataSection.collectionHint": "Which collection this layer shows a record of",
32966
+ "dataSection.fieldHint": "The field {name} shows",
32967
+ "dataSection.fromAround": "From around it: {name}",
32968
+ "dataSection.noRecord": "No record",
32969
+ "dataSection.notBound": "Not bound",
32970
+ "dataSection.pickFirst": "Pick a record above, or put this layer in a list, to bind it to fields.",
32971
+ "dataSection.picture": "Picture",
32972
+ "dataSection.recordHint": "Which record this layer shows",
32973
+ "dataSection.text": "Text",
32974
+ "dataSection.title": "Data",
32960
32975
  "deleteAccountDialog.cancel": "Cancel",
32961
32976
  "deleteAccountDialog.couldNotDelete": "Your account could not be deleted. Try again.",
32962
32977
  "deleteAccountDialog.deleteAccount": "Delete account",
@@ -33138,6 +33153,8 @@ var init_en2 = __esm({
33138
33153
  "designPanel.pairThePaddingLeftWith": "Pair the padding (left with right, top with bottom)",
33139
33154
  "designPanel.perspective": "Perspective",
33140
33155
  "designPanel.pick": "\u2014 Pick \u2014",
33156
+ "designPanel.pictureAddress": "The picture's address",
33157
+ "designPanel.pictureAddressPlaceholder": "https://\u2026",
33141
33158
  "designPanel.pixelCanvas": "Pixel canvas",
33142
33159
  "designPanel.placeholdersLikeTakeTheSelected": "Placeholders like '{{'Label'}}' take the selected instance's property values in the Dev panel.",
33143
33160
  "designPanel.pointCount": "Point count",
@@ -33737,9 +33754,9 @@ var init_en2 = __esm({
33737
33754
  "homeTeam.publicInMarketplace": "Public in the marketplace",
33738
33755
  "homeTeam.putOnline": "Put online",
33739
33756
  "homeTeam.reject": "Reject",
33740
- "homeTeam.rejectNote": "Why (for the publisher)",
33741
33757
  "homeTeam.rejected": "Rejected",
33742
33758
  "homeTeam.rejectedBecause": "Rejected: {note}",
33759
+ "homeTeam.rejectNote": "Why (for the publisher)",
33743
33760
  "homeTeam.removeConfirm": "Take {name} off the marketplace? A plugin is unlisted (those who installed it keep it); anything else is deleted.",
33744
33761
  "homeTeam.removeTitle": "Take {name} off the marketplace?",
33745
33762
  "homeTeam.review": "Review",
@@ -254923,14 +254940,24 @@ function repeatRecords(doc2, repeat, under) {
254923
254940
  return list2.slice(from2, repeat.limit !== void 0 ? from2 + Math.max(0, repeat.limit) : void 0);
254924
254941
  }
254925
254942
  function shownRecordOf(nodes, doc2, id3) {
254943
+ const own2 = pinnedRecord(doc2, nodes[id3]);
254944
+ if (own2) return own2;
254926
254945
  for (let at3 = nodes[id3]?.parentId, hops = 0; at3 && hops < 64; at3 = nodes[at3]?.parentId, hops++) {
254927
254946
  const frame2 = nodes[at3];
254947
+ const pinned = pinnedRecord(doc2, frame2);
254948
+ if (pinned) return pinned;
254928
254949
  if (frame2?.repeat) return repeatRecords(doc2, frame2.repeat)[0] ?? null;
254929
254950
  const of = frame2?.route?.collectionId;
254930
254951
  if (of && doc2.collections?.[of]) return repeatRecords(doc2, { collectionId: of })[0] ?? null;
254931
254952
  }
254932
254953
  return null;
254933
254954
  }
254955
+ function pinnedRecord(doc2, node2) {
254956
+ const pin = node2?.record;
254957
+ if (!pin) return null;
254958
+ const r = doc2.records?.[pin.recordId];
254959
+ return r && r.collectionId === pin.collectionId && doc2.collections?.[pin.collectionId] ? r : null;
254960
+ }
254934
254961
  function recordSlugs(doc2, collectionId) {
254935
254962
  const c2 = doc2.collections?.[collectionId];
254936
254963
  const out = /* @__PURE__ */ new Map();
@@ -254961,6 +254988,12 @@ function itemNode(node2, raw, collection, record2, scope) {
254961
254988
  if (bound) for (const [prop, src] of Object.entries(bound)) {
254962
254989
  const val = tryEvaluate(src, scope);
254963
254990
  if (val === void 0) continue;
254991
+ if (prop.startsWith(PROP_BINDING) && out.type === "INSTANCE") {
254992
+ const name2 = prop.slice(PROP_BINDING.length);
254993
+ const props = out.componentProperties;
254994
+ out = { ...out, componentProperties: { ...props, [name2]: typeof val === "boolean" ? val : fieldText(val) } };
254995
+ continue;
254996
+ }
254964
254997
  const value3 = prop === "imageUrl" ? String(val) : boundPropValue(prop, val);
254965
254998
  if (value3 !== void 0) out = { ...out, [prop]: value3 };
254966
254999
  }
@@ -255043,7 +255076,7 @@ function removeRecords(doc2, ids2) {
255043
255076
  const collections = Object.fromEntries(Object.entries(doc2.collections ?? {}).map(([id3, c2]) => [id3, c2.order.some((r) => gone.has(r)) ? { ...c2, order: c2.order.filter((r) => !gone.has(r)) } : c2]));
255044
255077
  return { ...doc2, records, collections };
255045
255078
  }
255046
- var COLLECTION_LIMITS, slugify2, dataMemo, withData, ITEM_RECORD, MARK, itemMark, templateRecord, compare, TOKEN2, hasItemToken, layerId, copySuffix, copyRecord, coerce3, newId;
255079
+ var COLLECTION_LIMITS, slugify2, dataMemo, withData, ITEM_RECORD, MARK, itemMark, templateRecord, compare, PROP_BINDING, TOKEN2, hasItemToken, layerId, copySuffix, copyRecord, coerce3, newId;
255047
255080
  var init_collections = __esm({
255048
255081
  "lib/document/collections.ts"() {
255049
255082
  "use strict";
@@ -255067,6 +255100,7 @@ var init_collections = __esm({
255067
255100
  values: Object.fromEntries(collection.fields.map((f2) => [f2.id, itemMark(fieldKey(f2.name))]))
255068
255101
  });
255069
255102
  compare = (a, b) => typeof a === "number" && typeof b === "number" ? a - b : String(a ?? "").localeCompare(String(b ?? ""), void 0, { numeric: true });
255103
+ PROP_BINDING = "prop:";
255070
255104
  TOKEN2 = /\{\{\s*item\.([A-Za-z0-9_]+)\s*\}\}/g;
255071
255105
  hasItemToken = (text) => !!text && text.includes("{{") && /\{\{\s*item\./.test(text);
255072
255106
  layerId = (id3) => {
@@ -256251,7 +256285,6 @@ function planEntryAction({ home }) {
256251
256285
  var STORAGE_KEY2, useHyperyTeam, TEAM_PROMPT_ENABLED, TEAM_PROMPTED_KEY, PROMPTED_KEY, useTeamPrompt, started;
256252
256286
  var init_team = __esm({
256253
256287
  "lib/client/cloud/team.ts"() {
256254
- "use strict";
256255
256288
  init_esm();
256256
256289
  init_api4();
256257
256290
  init_session();
@@ -305059,7 +305092,6 @@ function creditLine(c2) {
305059
305092
  var MAX_PHOTO_PAGE, MAX_PHOTO_QUERY, UUID, isPhotoId, photoImagePath;
305060
305093
  var init_stock = __esm({
305061
305094
  "lib/photos/stock.ts"() {
305062
- "use strict";
305063
305095
  MAX_PHOTO_PAGE = 12;
305064
305096
  MAX_PHOTO_QUERY = 200;
305065
305097
  UUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
@@ -310996,7 +311028,6 @@ function agentToolList() {
310996
311028
  var AGENT_CAPABILITIES, legacyCommandId, legacyTools, blobToDataUrl3, provided, legacyTool, legacyToolList;
310997
311029
  var init_agent = __esm({
310998
311030
  "lib/commands/agent.ts"() {
310999
- "use strict";
311000
311031
  init_types5();
311001
311032
  init_registry();
311002
311033
  init_errors();
@@ -316779,7 +316810,29 @@ function buildPlainTree(doc2, nodeId2, opts = {}, start2, hooks = {}) {
316779
316810
  const stylesOf = (n) => styleMapFor(n, doc2.styles, styleOpts);
316780
316811
  let motion = null;
316781
316812
  const timeline = !opts.static && !start2 && first2.type === "FRAME" ? frameWebMotion(doc2, first2.id, (n) => stylesOf(prepared(n, doc2))) : null;
316813
+ let nodesNow = doc2.nodes;
316782
316814
  const build3 = (raw, parent, in3D2, fold) => {
316815
+ const pinned = pinnedRecord(doc2, raw);
316816
+ const outerItem = item, outerNodes = nodesNow;
316817
+ const pinnedTo = pinned ? doc2.collections[pinned.collectionId] : void 0;
316818
+ const holds2 = raw.type === "INSTANCE" || !!raw.childIds?.length;
316819
+ const slugField = holds2 && pinnedTo?.schema && opts.listTemplates && opts.target === "site" ? pinnedTo.fields.find((f2) => f2.id === pinnedTo.slugFieldId) ?? pinnedTo.fields.find((f2) => f2.type === "SLUG") : void 0;
316820
+ if (pinned && pinnedTo) item = slugField ? { collection: pinnedTo, record: templateRecord(pinnedTo), suffix: item?.suffix ?? "" } : { collection: pinnedTo, record: pinned, suffix: item?.suffix ?? "" };
316821
+ if (item && raw.type === "INSTANCE" && raw.boundExpressions) {
316822
+ const shown2 = itemised(raw, raw, item);
316823
+ if (shown2 !== raw) nodesNow = { ...nodesNow, [raw.id]: shown2 };
316824
+ }
316825
+ try {
316826
+ const web = buildLayer(raw, parent, in3D2, fold);
316827
+ if (!web || !pinned || !slugField) return web;
316828
+ const value3 = String(pinned.values[slugField.id] ?? "");
316829
+ return { ...web, children: [], repeat: { collectionId: pinnedTo.id, template: web.children, limit: 1, pick: { key: fieldKey(slugField.name), value: value3 } } };
316830
+ } finally {
316831
+ item = outerItem;
316832
+ nodesNow = outerNodes;
316833
+ }
316834
+ };
316835
+ const buildLayer = (raw, parent, in3D2, fold) => {
316783
316836
  if (emitted >= cap2) {
316784
316837
  truncated = true;
316785
316838
  return null;
@@ -316978,7 +317031,7 @@ function buildPlainTree(doc2, nodeId2, opts = {}, start2, hooks = {}) {
316978
317031
  try {
316979
317032
  for (const record2 of repeatRecords(doc2, repeat, under)) {
316980
317033
  item = { collection, record: record2, suffix: `${outerItem?.suffix ?? ""}~${record2.id}` };
316981
- for (const child of childrenOf(node2, doc2.nodes)) {
317034
+ for (const child of childrenOf(node2, nodesNow)) {
316982
317035
  const built = build3(child, node2, kids3D, kidFold);
316983
317036
  if (built) web.children.push(built);
316984
317037
  }
@@ -316986,7 +317039,7 @@ function buildPlainTree(doc2, nodeId2, opts = {}, start2, hooks = {}) {
316986
317039
  if (collection.perVisitor && !outerItem || collection.schema && opts.listTemplates) {
316987
317040
  item = { collection, record: templateRecord(collection), suffix: `~${ITEM_RECORD}` };
316988
317041
  const template2 = [];
316989
- for (const child of childrenOf(node2, doc2.nodes)) {
317042
+ for (const child of childrenOf(node2, nodesNow)) {
316990
317043
  const built = build3(child, node2, kids3D, kidFold);
316991
317044
  if (built) template2.push(built);
316992
317045
  }
@@ -317003,7 +317056,7 @@ function buildPlainTree(doc2, nodeId2, opts = {}, start2, hooks = {}) {
317003
317056
  } finally {
317004
317057
  item = outerItem;
317005
317058
  }
317006
- } else for (const child of childrenOf(node2, doc2.nodes)) {
317059
+ } else for (const child of childrenOf(node2, nodesNow)) {
317007
317060
  const built = build3(child, node2, kids3D, kidFold);
317008
317061
  if (built) web.children.push(built);
317009
317062
  }
@@ -320600,7 +320653,7 @@ var init_package = __esm({
320600
320653
  "packages/runtime/package.json"() {
320601
320654
  package_default = {
320602
320655
  name: "@popcraft/runtime",
320603
- version: "0.20.2",
320656
+ version: "0.21.0",
320604
320657
  description: "What code exported from PopCraft shares: the meaning of expressions (operators and functions) and, as it grows, the parts of a design's behaviour that are common code rather than generated React.",
320605
320658
  license: "MIT",
320606
320659
  homepage: "https://popcraft.app/docs/exporting/nextjs",
@@ -346268,7 +346321,8 @@ function componentPlan(doc2, screens) {
346268
346321
  props.push({ name: propName2(one ? COMMERCE_PROP[c2.schema].name : camel3(c2.name)), type: one ? type : `${type}[]`, doc: `${c2.name}; none: the design's samples.`, schema: c2.schema, collectionId: c2.id });
346269
346322
  }
346270
346323
  for (const a of new Set(all3(s.tree.root).map((n) => n.action).filter((a2) => !!a2))) props.push({ name: COMMERCE_ACTION_PROP[a], type: "(formData: FormData) => void | Promise<void>", doc: `What sending its ${a} form does (a server action).`, action: a });
346271
- return { screen: s, name: name2, props };
346324
+ const picks = [...new Set(all3(s.tree.root).filter((n) => n.repeat?.pick && doc2.collections?.[n.repeat.collectionId]?.schema === "commerce.product").map((n) => n.repeat.pick.value))].sort();
346325
+ return { screen: s, name: name2, props, ...picks.length ? { picks } : {} };
346272
346326
  });
346273
346327
  }
346274
346328
  function componentMedia(doc2, screens) {
@@ -346321,6 +346375,7 @@ function componentFiles(doc2, screens, o = {}) {
346321
346375
  if (!p2) return null;
346322
346376
  const c2 = doc2.collections[r.collectionId];
346323
346377
  let out = COMMERCE_PROP[c2.schema].one ? `(${p2} ? [commerceItem(${JSON.stringify(c2.schema)}, ${p2})] : SAMPLE_${p2.toUpperCase()})` : `(${p2} ? ${p2}.map(v => commerceItem(${JSON.stringify(c2.schema)}, v)) : SAMPLE_${p2.toUpperCase()})`;
346378
+ if (r.pick) return `((list) => [list.find(item => String(item.values[${JSON.stringify(r.pick.key)}] ?? "") === ${JSON.stringify(r.pick.value)}) ?? list[0]].filter((item): item is CommerceItem => !!item))(${out})`;
346324
346379
  if (r.offset || r.limit) out += `.slice(${r.offset ?? 0}${r.limit ? `, ${(r.offset ?? 0) + r.limit}` : ""})`;
346325
346380
  return out;
346326
346381
  },
@@ -346495,6 +346550,10 @@ function shopifyThemeFiles(doc2, screens, o = {}) {
346495
346550
  const schema = n.repeat && schemaOf(n.repeat.collectionId);
346496
346551
  if (!schema || !n.repeat) return null;
346497
346552
  const range3 = `${n.repeat.limit ? ` limit: ${n.repeat.limit}` : ""}${n.repeat.offset ? ` offset: ${n.repeat.offset}` : ""}`;
346553
+ if (n.repeat.pick && schema === "commerce.product") {
346554
+ const handle = n.repeat.pick.value.replace(/[^a-z0-9-]/gi, "");
346555
+ return { open: `{% assign p = all_products['${handle}'] %}{% if p == empty %}{% assign p = ${source3} | first %}{% endif %}{% if p %}`, close: "{% endif %}" };
346556
+ }
346498
346557
  switch (schema) {
346499
346558
  case "commerce.product":
346500
346559
  return { open: `{% for p in ${source3}${range3} %}`, close: "{% endfor %}" };
@@ -346877,14 +346936,16 @@ function pageSource(plan2, kind) {
346877
346936
  cart: 'getProducts({ sortKey: "CREATED_AT", reverse: true })',
346878
346937
  page: 'getProducts({ sortKey: "CREATED_AT", reverse: true })'
346879
346938
  };
346939
+ const picks = uses("products") ? plan2.picks ?? [] : [];
346940
+ const listed = picks.length ? `Promise.all([${productsFrom[kind]}, ...${JSON.stringify(picks)}.map((h) => getProduct(h))]).then(([list, ...picked]) => [...picked.filter((p): p is NonNullable<typeof p> => !!p), ...list])` : productsFrom[kind];
346880
346941
  const fetches = [
346881
346942
  ...kind === "collection" ? [["collection", "getCollection(handle)"]] : [],
346882
- ...uses("products") ? [["products", productsFrom[kind]]] : [],
346943
+ ...uses("products") ? [["products", listed]] : [],
346883
346944
  ...uses("categories") ? [["categories", "storeCategories()"]] : [],
346884
346945
  ["frame", "storeFrame()"]
346885
346946
  ];
346886
346947
  const shopify = [
346887
- ...kind === "product" ? ["getProduct"] : [],
346948
+ ...kind === "product" || picks.length ? ["getProduct"] : [],
346888
346949
  ...kind === "collection" ? ["getCollection"] : [],
346889
346950
  ...uses("products") ? [productsFrom[kind].slice(0, productsFrom[kind].indexOf("("))] : []
346890
346951
  ];
@@ -347031,7 +347092,8 @@ function storefrontDesign(doc2, screens) {
347031
347092
  const c2 = n.repeat && named(n.repeat.collectionId);
347032
347093
  if (!c2 || !n.repeat) return null;
347033
347094
  const range3 = `${n.repeat.limit ? ` limit="${n.repeat.limit}"` : ""}${n.repeat.offset ? ` offset="${n.repeat.offset}"` : ""}`;
347034
- return { open: `<!-- pc:each list="${c2.name}" schema="${c2.schema ?? ""}" as="item"${range3} -->`, close: "<!-- pc:end -->" };
347095
+ const pick5 = n.repeat.pick ? ` pick-${n.repeat.pick.key}="${n.repeat.pick.value.replace(/[&"<>]/g, (ch) => `&#${ch.charCodeAt(0)};`)}"` : "";
347096
+ return { open: `<!-- pc:each list="${c2.name}" schema="${c2.schema ?? ""}" as="item"${range3}${pick5} -->`, close: "<!-- pc:end -->" };
347035
347097
  },
347036
347098
  form: (n) => n.action ? { attrs: { "data-pc-action": n.action } } : null
347037
347099
  });
@@ -350974,7 +351036,7 @@ var init_motion_rich = __esm({
350974
351036
  return { summary: `Playback on ${plural2(patches.size, "instance")}`, data: { ids: [...patches.keys()] } };
350975
351037
  }
350976
351038
  });
350977
- PROP_TYPES = ["NUMBER", "COLOR", "MOTION", "BOOLEAN", "TEXT"];
351039
+ PROP_TYPES = ["NUMBER", "COLOR", "MOTION", "BOOLEAN", "TEXT", "IMAGE"];
350978
351040
  componentPropOp = external_exports.discriminatedUnion("op", [
350979
351041
  external_exports.object({
350980
351042
  op: external_exports.literal("add"),
@@ -350991,8 +351053,8 @@ var init_motion_rich = __esm({
350991
351053
  external_exports.object({
350992
351054
  op: external_exports.literal("bindLayer"),
350993
351055
  layer: external_exports.string().describe("A layer inside the component."),
350994
- name: external_exports.string().describe("A NUMBER or COLOR property."),
350995
- path: external_exports.string().max(300).nullable().describe("The layer property it feeds (a number path for NUMBER, a colour path for COLOR: fills[#0].color, opacity, text.counter \u2026); null unbinds.")
351056
+ name: external_exports.string().describe("A NUMBER, COLOR or IMAGE property."),
351057
+ path: external_exports.string().max(300).nullable().describe("The layer property it feeds (a number path for NUMBER, a colour path for COLOR: fills[#0].color, opacity, text.counter \u2026; a picture for IMAGE: imageUrl on an image layer, fills[#0].imageUrl on an image fill); null unbinds.")
350996
351058
  }).describe("Make a layer property an input: each instance's value of the property plays there (a keyed path too).")
350997
351059
  ]);
350998
351060
  motionEditComponentProperty = defineCommand({
@@ -351008,7 +351070,7 @@ var init_motion_rich = __esm({
351008
351070
  ui: { label: "Edit component property", palette: false },
351009
351071
  agent: {
351010
351072
  name: "edit_component_property",
351011
- description: `A component's properties, as its instances set them: add a NUMBER, COLOR, MOTION (one playback setting shown on every instance), BOOLEAN or TEXT property, remove one, change its default, or bind a layer property inside the component to a NUMBER / COLOR property (a layer input). Keys can also take their value from a property per instance (set_keyframes with bind), so "count to N" or "slide this far" is set per instance with set_instance_playback properties.`
351073
+ description: `A component's properties, as its instances set them: add a NUMBER, COLOR, MOTION (one playback setting shown on every instance), BOOLEAN, TEXT or IMAGE (a picture's address) property, remove one, change its default, or bind a layer property inside the component to a NUMBER / COLOR / IMAGE property (a layer input; IMAGE feeds a picture). An instance's TEXT and IMAGE properties can follow a record's field (bind_field). Keys can also take their value from a property per instance (set_keyframes with bind), so "count to N" or "slide this far" is set per instance with set_instance_playback properties.`
351012
351074
  }
351013
351075
  },
351014
351076
  run: (args2, ctx2) => {
@@ -351050,7 +351112,7 @@ var init_motion_rich = __esm({
351050
351112
  }
351051
351113
  case "bindLayer": {
351052
351114
  const def = has(op4.name);
351053
- if (def.type !== "NUMBER" && def.type !== "COLOR") throw new ToolError("Only a NUMBER or COLOR property feeds a layer input.");
351115
+ if (def.type !== "NUMBER" && def.type !== "COLOR" && def.type !== "IMAGE") throw new ToolError("Only a NUMBER, COLOR or IMAGE property feeds a layer input.");
351054
351116
  const layer2 = nodeOrThrow(doc2, op4.layer);
351055
351117
  const owner = target2.type === "COMPONENT_SET" ? target2.id : target2.id;
351056
351118
  let inside2 = false;
@@ -356453,9 +356515,11 @@ are whole stores to begin from.
356453
356515
  - \`commerce.menu\` for the store's menu, \`commerce.collection\` for its categories
356454
356516
  Add a few real sample records with \`edit_records\`: the canvas is drawn with them. A store's real catalogue
356455
356517
  comes from the Ecommerce plugin (Marketplace \u203A Plugins): it fills these collections from Shopify.
356456
- 3. **The lists.** \`set_repeat\` on the frame that holds one card, over the collection; the card's texts say
356457
- \`{{item.title}}\`, \`{{item.price}}\`, and its picture follows \`item_image\`. The cart's totals are a list of one
356458
- (\`limit: 1\`) over the \`commerce.cart\` collection.
356518
+ 3. **The cards and lists.** Make the card a component with properties (\`edit_component_property\`: TEXT Title and
356519
+ Price, IMAGE Image fed to its picture with \`bindLayer\` path \`imageUrl\`). \`set_repeat\` on the frame that holds
356520
+ one instance, over the collection, and \`bind_field\` each property to a field (Title \u2190 title, Price \u2190 price,
356521
+ Image \u2190 image). One product in a hero or feature is \`set_record\` (by its handle), never a list of one with an
356522
+ offset. The cart's totals are a list of one (\`limit: 1\`) over the \`commerce.cart\` collection.
356459
356523
  4. **A page per product.** \`set_route\` on the product screen with the product collection's id: the route is
356460
356524
  \`/product\` and each product's page is \`/product/<handle>\`. A card links to that screen.
356461
356525
  5. **The forms.** \`set_control\` FORM with a storefront \`formAction\`:
@@ -358873,16 +358937,16 @@ var init_api5 = __esm({
358873
358937
  }
358874
358938
  });
358875
358939
 
358876
- // node_modules/.pnpm/@popcraft+plugins@1.0.0_patch_hash=e61849e2a6a59df95f0ebad36ab274c37a933fcf798d8b4dd6b3f0ffc625a1da/node_modules/@popcraft/plugins/dist/generated/runtime.js
358940
+ // node_modules/.pnpm/@popcraft+plugins@1.0.0_patch_hash=6826cb40a12857607fa1f7d7d102e5d8316bd483c1ff06865dac73139dfe709b/node_modules/@popcraft/plugins/dist/generated/runtime.js
358877
358941
  var MAIN_RUNTIME, UI_RUNTIME;
358878
358942
  var init_runtime4 = __esm({
358879
- "node_modules/.pnpm/@popcraft+plugins@1.0.0_patch_hash=e61849e2a6a59df95f0ebad36ab274c37a933fcf798d8b4dd6b3f0ffc625a1da/node_modules/@popcraft/plugins/dist/generated/runtime.js"() {
358880
- MAIN_RUNTIME = '"use strict";\n(() => {\n // src/runtime/rpc.ts\n function createRpcClient() {\n let port = null;\n const queue = [];\n const pending = {};\n const listeners = {};\n let nextId = 1;\n function send(msg) {\n if (port) port.postMessage(msg);\n else queue.push(msg);\n }\n function call(method, params) {\n return new Promise((resolve, reject) => {\n const id = nextId++;\n pending[id] = { resolve, reject };\n send({ kind: "rpc", id, method, params });\n });\n }\n function emit(event, data) {\n for (const cb of (listeners[event] || []).slice()) {\n try {\n cb(data);\n } catch (e) {\n console.error(e);\n }\n }\n }\n window.addEventListener("message", (e) => {\n const data = e.data;\n if (e.source !== window.parent || !data || data.type !== "popcraft-plugin-port" || port) return;\n port = e.ports[0];\n port.onmessage = (ev) => {\n const m = ev.data || {};\n if (m.kind === "result" && m.id !== void 0 && pending[m.id]) {\n const p = pending[m.id];\n delete pending[m.id];\n if (m.error) p.reject(new Error(m.error));\n else p.resolve(m.result);\n } else if (m.kind === "event" && m.event) {\n emit(m.event, m.data);\n }\n };\n for (const msg of queue.splice(0)) port.postMessage(msg);\n });\n return {\n call,\n send,\n on(event, cb) {\n (listeners[event] = listeners[event] || []).push(cb);\n },\n off(event, cb) {\n listeners[event] = (listeners[event] || []).filter((x) => x !== cb);\n }\n };\n }\n\n // src/runtime/main.ts\n function clone(v) {\n return v === void 0 ? {} : JSON.parse(JSON.stringify(v));\n }\n function createMainApi(rpc = createRpcClient()) {\n const uiHandlers = [];\n let uiOnMessage = null;\n const uiQueue = [];\n function deliverUI(msg) {\n for (const cb of uiHandlers) {\n try {\n cb(msg);\n } catch (e) {\n console.error(e);\n }\n }\n if (typeof uiOnMessage === "function") {\n try {\n uiOnMessage(msg);\n } catch (e) {\n console.error(e);\n }\n }\n }\n function flushUI() {\n if (uiHandlers.length || typeof uiOnMessage === "function") uiQueue.splice(0).forEach(deliverUI);\n }\n rpc.on("uimessage", (msg) => {\n if (!uiHandlers.length && typeof uiOnMessage !== "function") uiQueue.push(msg);\n else deliverUI(msg);\n });\n let widgetDef = null;\n const widgetQueue = [];\n function renderWidget(state) {\n const handlers = {};\n function walk(el, path) {\n if (!el || typeof el !== "object") return el;\n const out = {};\n for (const k of Object.keys(el)) {\n const v = el[k];\n if (k === "children") {\n out.children = (Array.isArray(v) ? v : [v]).filter((c) => c !== null && c !== void 0 && c !== false).map((c, i) => walk(c, path + "." + i));\n } else if (k === "onClick" && typeof v === "function") {\n handlers[path] = v;\n out.onClick = path;\n } else {\n out[k] = v;\n }\n }\n return out;\n }\n return { tree: walk(widgetDef.render(state), "root"), handlers };\n }\n function initialWidgetState() {\n const s = widgetDef && widgetDef.initialState;\n return clone(typeof s === "function" ? s() : s);\n }\n function commitWidget(nodeId, state) {\n return rpc.call("widgetUpdate", { id: nodeId, state, tree: renderWidget(clone(state)).tree });\n }\n function handleWidgetClick(e) {\n Promise.resolve().then(() => {\n const state = clone(e.state);\n const rendered = renderWidget(state);\n const h = rendered.handlers[e.handler] || widgetDef.handlers && widgetDef.handlers[e.handler];\n if (typeof h !== "function") return;\n return Promise.resolve(h(state, { nodeId: e.nodeId })).then((ret) => commitWidget(e.nodeId, ret === void 0 ? state : ret));\n }).catch((err) => rpc.send({ kind: "error", message: String((err == null ? void 0 : err.message) || err) }));\n }\n rpc.on("widgetclick", (e) => {\n if (widgetDef) handleWidgetClick(e);\n else widgetQueue.push(e);\n });\n const popcraft = {\n /** Which menu command the user picked, or null when the plugin ran some other way. */\n command: null,\n /** Host import mode when launched as an importer, else null. */\n importMode: null,\n /** Pending import file when the host handed one over, else null. */\n importFile: null,\n /** Pending export request (File \u2192 Export \u2192 this plugin\'s format), else null. */\n exportRequest: null,\n getSelection: () => rpc.call("getSelection"),\n setSelection: (ids) => rpc.call("setSelection", { ids }),\n getNode: (id) => rpc.call("getNode", { id }),\n findNodes: (query) => rpc.call("findNodes", query || {}),\n getCurrentPage: () => rpc.call("getCurrentPage"),\n createNode: (type, props) => rpc.call("createNode", { type, props: props || {} }),\n updateNode: (id, props) => rpc.call("updateNode", { id, props }),\n deleteNode: (id) => rpc.call("deleteNode", { id }),\n setText: (id, characters) => rpc.call("setText", { id, characters }),\n /** Put TEXT layers on a path (another layer\'s outline, or their own arc, circle or wave), or tune one: brackets, alignment, placement, effect, overflow. */\n setTextPath: (opts) => rpc.call("setTextPath", opts || {}),\n /** Area type: flow a TEXT layer inside a closed shape, kept `inset` px in. */\n flowTextIntoShape: (opts) => rpc.call("flowTextIntoShape", opts || {}),\n /** Take TEXT layers off their path or out of their shape (or free every text linked to a path / shape). */\n detachText: (opts) => rpc.call("detachText", opts || {}),\n /** Auto-size TEXT layers\' type to their box, shape or path. */\n setTextFit: (opts) => rpc.call("setTextFit", opts || {}),\n createPage: (name) => rpc.call("createPage", name != null ? { name: String(name) } : {}),\n setCurrentPage: (pageId) => rpc.call("setCurrentPage", { pageId }),\n createImage: (opts) => rpc.call("createImage", opts || {}),\n createDocument: (opts) => rpc.call("createDocument", opts || {}),\n getImportMode: () => rpc.call("getImportMode"),\n getImportFile: () => rpc.call("getImportFile"),\n /**\n * The open page as a storefront design in a neutral form, for an exporter that writes its own store\'s templates:\n * each page\'s HTML with the store\'s data marked (`{{page.title}}`, `{{item.price}}`, `<!-- pc:each \u2026 -->` lists,\n * `data-pc-action` forms), and its collections with their commerce schema and sample records.\n */\n getStorefront: () => rpc.call("getStorefront"),\n /**\n * A design\'s data (docs: Collections): a collection and its fields (`schema`: what it stands for, e.g.\n * `commerce.product`, whose fields are made for it), its records, and reading them.\n */\n editCollection: (opts) => rpc.call("editCollection", opts || {}),\n editRecords: (opts) => rpc.call("editRecords", opts || {}),\n getRecords: (opts) => rpc.call("getRecords", opts || {}),\n /** A layer as a web page\'s form control (a FORM\'s `formAction`, an INPUT\'s name and value), repeated per record, a page\'s address, its semantics. */\n setControl: (opts) => rpc.call("setControl", opts || {}),\n setRepeat: (opts) => rpc.call("setRepeat", opts || {}),\n setRoute: (opts) => rpc.call("setRoute", opts || {}),\n setSemantics: (opts) => rpc.call("setSemantics", opts || {}),\n /** The site\'s environment: public settings its host reads by name when it is deployed (never a secret: the file keeps them). */\n getSiteEnvironment: () => rpc.call("getSiteEnvironment"),\n setSiteEnvironment: (set) => rpc.call("setSiteEnvironment", { set }),\n /** What this plugin keeps with the open file (its settings for this design), under its own id; JSON only. */\n getFileData: (key) => rpc.call("getFileData", { key }),\n setFileData: (key, value) => rpc.call("setFileData", { key, value }),\n /** A store\'s project built from the open page, zipped (needs `export`): what an exporter saves with `saveExport`. */\n getStorefrontFiles: (target) => rpc.call("getStorefrontFiles", { target }),\n getExportRequest: () => rpc.call("getExportRequest"),\n /** Save the exporter\'s file (once per export request); the host enforces the contributed extension. */\n saveExport: (file) => rpc.call("saveExport", file || {}),\n /**\n * Presentation audio (docs/plugins/presentation-audio.md): the deck\'s slides, notes, narration and timing; the\n * soundtrack\'s settings, bed and stinger.\n */\n getPresentationAudio: (opts) => rpc.call("getPresentationAudio", opts || {}),\n /** Change the soundtrack\'s settings (voice, style, mood, loudness, beat alignment, levels) and re-time it. */\n setPresentationAudioSettings: (opts) => rpc.call("setPresentationAudioSettings", opts || {}),\n /** Put a sound in (a slide\'s narration, the bed or the stinger): bytes are normalised to the soundtrack\'s target and stored. */\n setPresentationAudioClip: (opts) => rpc.call("setPresentationAudioClip", opts || {}),\n removePresentationAudioClip: (opts) => rpc.call("removePresentationAudioClip", opts || {}),\n /** Re-time: fixed slide lengths (ms, null clears) and beat-aligned changes. */\n setPresentationTiming: (opts) => rpc.call("setPresentationTiming", opts || {}),\n /** The mix as the export carries it: loudness, true peak, and (wav: true) the 48 kHz stereo WAV. */\n getPresentationMix: (opts) => rpc.call("getPresentationMix", opts || {}),\n /** Ask the user to generate with AI (the panel opens with the request filled in; the user decides). */\n requestPresentationAudio: (opts) => rpc.call("requestPresentationAudio", opts || {}),\n /** Editor commands: each takes the arguments documented in docs/plugins/methods.md (the agent tool of the same purpose). */\n moveNodes: (opts) => rpc.call("moveNodes", opts || {}),\n resizeNodes: (opts) => rpc.call("resizeNodes", opts || {}),\n renameNodes: (opts) => rpc.call("renameNodes", opts || {}),\n reparentNodes: (opts) => rpc.call("reparentNodes", opts || {}),\n duplicateNodes: (opts) => rpc.call("duplicateNodes", opts || {}),\n groupNodes: (opts) => rpc.call("groupNodes", opts || {}),\n ungroupNodes: (opts) => rpc.call("ungroupNodes", opts || {}),\n alignNodes: (opts) => rpc.call("alignNodes", opts || {}),\n distributeNodes: (opts) => rpc.call("distributeNodes", opts || {}),\n flipNodes: (opts) => rpc.call("flipNodes", opts || {}),\n rotateNodes: (opts) => rpc.call("rotateNodes", opts || {}),\n reorderNodes: (opts) => rpc.call("reorderNodes", opts || {}),\n setLocked: (opts) => rpc.call("setLocked", opts || {}),\n setVisible: (opts) => rpc.call("setVisible", opts || {}),\n setAutoLayout: (opts) => rpc.call("setAutoLayout", opts || {}),\n importSvg: (opts) => rpc.call("importSvg", opts || {}),\n measureNodes: (opts) => rpc.call("measureNodes", opts || {}),\n setFills: (opts) => rpc.call("setFills", opts || {}),\n setStrokes: (opts) => rpc.call("setStrokes", opts || {}),\n setEffects: (opts) => rpc.call("setEffects", opts || {}),\n replaceColor: (opts) => rpc.call("replaceColor", opts || {}),\n setTypography: (opts) => rpc.call("setTypography", opts || {}),\n applyStyle: (opts) => rpc.call("applyStyle", opts || {}),\n createComponent: (opts) => rpc.call("createComponent", opts || {}),\n createInstance: (opts) => rpc.call("createInstance", opts || {}),\n renamePage: (opts) => rpc.call("renamePage", opts || {}),\n getDocumentOutline: (opts) => rpc.call("getDocumentOutline", opts || {}),\n setPageSetup: (opts) => rpc.call("setPageSetup", opts || {}),\n setShowAdvance: (opts) => rpc.call("setShowAdvance", opts || {}),\n booleanOperation: (opts) => rpc.call("booleanOperation", opts || {}),\n flattenNodes: (opts) => rpc.call("flattenNodes", opts || {}),\n outlineStroke: (opts) => rpc.call("outlineStroke", opts || {}),\n setMask: (opts) => rpc.call("setMask", opts || {}),\n setMaskType: (opts) => rpc.call("setMaskType", opts || {}),\n shapeBuilder: (opts) => rpc.call("shapeBuilder", opts || {}),\n setKeyframes: (opts) => rpc.call("setKeyframes", opts || {}),\n getAnimation: (opts) => rpc.call("getAnimation", opts || {}),\n getCompositions: (opts) => rpc.call("getCompositions", opts || {}),\n editBehaviours: (opts) => rpc.call("editBehaviours", opts || {}),\n setTimeline: (opts) => rpc.call("setTimeline", opts || {}),\n setLayerTiming: (opts) => rpc.call("setLayerTiming", opts || {}),\n editTextAnimation: (opts) => rpc.call("editTextAnimation", opts || {}),\n edit3DScene: (opts) => rpc.call("edit3DScene", opts || {}),\n setInstancePlayback: (opts) => rpc.call("setInstancePlayback", opts || {}),\n editComponentProperty: (opts) => rpc.call("editComponentProperty", opts || {}),\n setShaderFill: (opts) => rpc.call("setShaderFill", opts || {}),\n createComposition: (opts) => rpc.call("createComposition", opts || {}),\n editComposition: (opts) => rpc.call("editComposition", opts || {}),\n editStateMachine: (opts) => rpc.call("editStateMachine", opts || {}),\n getStateMachine: (opts) => rpc.call("getStateMachine", opts || {}),\n applyMotionPreset: (opts) => rpc.call("applyMotionPreset", opts || {}),\n staggerMotion: (opts) => rpc.call("staggerMotion", opts || {}),\n createMotionComponent: (opts) => rpc.call("createMotionComponent", opts || {}),\n addAudio: (opts) => rpc.call("addAudio", opts || {}),\n editFrameAudio: (opts) => rpc.call("editFrameAudio", opts || {}),\n editVideoClip: (opts) => rpc.call("editVideoClip", opts || {}),\n detectAudioOnsets: (opts) => rpc.call("detectAudioOnsets", opts || {}),\n cutToBeat: (opts) => rpc.call("cutToBeat", opts || {}),\n editPrototype: (opts) => rpc.call("editPrototype", opts || {}),\n usePresentationFallbackAudio: (opts) => rpc.call("usePresentationFallbackAudio", opts || {}),\n /** Bring a Lottie, PDF or PopCraft file into the open document (the `importFile` field is the file an importer was handed). */\n importDocumentFile: (opts) => rpc.call("importDocumentFile", opts || {}),\n getStyles: () => rpc.call("getStyles"),\n createPaintStyle: (name, paints) => rpc.call("createPaintStyle", { name, paints }),\n getVariables: () => rpc.call("getVariables"),\n setVariableValue: (variableId, modeId, value) => rpc.call("setVariableValue", { variableId, modeId, value }),\n exportNode: (id, options) => rpc.call("exportNode", { id, options: options || {} }),\n openExternal: (url) => rpc.call("openExternal", { url }),\n notify: (message, options) => rpc.call("notify", { message: String(message), error: !!(options && options.error) }),\n showUI: (options) => rpc.call("showUI", options || {}),\n hideUI: () => rpc.call("hideUI"),\n /** Ends the run. The optional message is shown as a notification. */\n closePlugin: (message) => rpc.call("closePlugin", { message }),\n storage: {\n get: (key) => rpc.call("storageGet", { key }),\n set: (key, value) => rpc.call("storageSet", { key, value })\n },\n ui: {\n get onmessage() {\n return uiOnMessage;\n },\n set onmessage(cb) {\n uiOnMessage = cb;\n flushUI();\n },\n postMessage: (msg) => rpc.send({ kind: "toUI", data: msg }),\n on: (cb) => {\n uiHandlers.push(cb);\n flushUI();\n }\n },\n widget: {\n register(def) {\n if (!def || typeof def.render !== "function") throw new Error("popcraft.widget.register needs a render(state) function");\n widgetDef = def;\n widgetQueue.splice(0).forEach(handleWidgetClick);\n },\n insert(options) {\n if (!widgetDef) return Promise.reject(new Error("Call popcraft.widget.register first"));\n const o = options || {};\n const state = o.state !== void 0 ? clone(o.state) : initialWidgetState();\n return rpc.call("widgetInsert", { state, tree: renderWidget(state).tree, x: o.x, y: o.y });\n },\n getState: (nodeId) => rpc.call("widgetGet", { id: nodeId }),\n setState(nodeId, state) {\n if (!widgetDef) return Promise.reject(new Error("Call popcraft.widget.register first"));\n return commitWidget(nodeId, clone(state));\n }\n },\n on: (event, cb) => rpc.on(event, cb),\n off: (event, cb) => rpc.off(event, cb)\n };\n rpc.on("init", (data) => {\n var _a, _b, _c;\n popcraft.command = data.command;\n popcraft.importMode = (_a = data.importMode) != null ? _a : null;\n popcraft.importFile = (_b = data.importFile) != null ? _b : null;\n popcraft.exportRequest = (_c = data.exportRequest) != null ? _c : null;\n });\n return popcraft;\n }\n function installMainApi() {\n const rpc = createRpcClient();\n const api = createMainApi(rpc);\n const w = window;\n w.__popcraftRpc = rpc;\n w.popcraft = api;\n w.vizpops = api;\n window.addEventListener("error", (e) => rpc.send({ kind: "error", message: String(e.message || e) }));\n window.addEventListener("unhandledrejection", (e) => rpc.send({ kind: "error", message: String(e.reason && e.reason.message || e.reason) }));\n return api;\n }\n\n // src/runtime/main.entry.ts\n installMainApi();\n})();';
358943
+ "node_modules/.pnpm/@popcraft+plugins@1.0.0_patch_hash=6826cb40a12857607fa1f7d7d102e5d8316bd483c1ff06865dac73139dfe709b/node_modules/@popcraft/plugins/dist/generated/runtime.js"() {
358944
+ MAIN_RUNTIME = '"use strict";\n(() => {\n // src/runtime/rpc.ts\n function createRpcClient() {\n let port = null;\n const queue = [];\n const pending = {};\n const listeners = {};\n let nextId = 1;\n function send(msg) {\n if (port) port.postMessage(msg);\n else queue.push(msg);\n }\n function call(method, params) {\n return new Promise((resolve, reject) => {\n const id = nextId++;\n pending[id] = { resolve, reject };\n send({ kind: "rpc", id, method, params });\n });\n }\n function emit(event, data) {\n for (const cb of (listeners[event] || []).slice()) {\n try {\n cb(data);\n } catch (e) {\n console.error(e);\n }\n }\n }\n window.addEventListener("message", (e) => {\n const data = e.data;\n if (e.source !== window.parent || !data || data.type !== "popcraft-plugin-port" || port) return;\n port = e.ports[0];\n port.onmessage = (ev) => {\n const m = ev.data || {};\n if (m.kind === "result" && m.id !== void 0 && pending[m.id]) {\n const p = pending[m.id];\n delete pending[m.id];\n if (m.error) p.reject(new Error(m.error));\n else p.resolve(m.result);\n } else if (m.kind === "event" && m.event) {\n emit(m.event, m.data);\n }\n };\n for (const msg of queue.splice(0)) port.postMessage(msg);\n });\n return {\n call,\n send,\n on(event, cb) {\n (listeners[event] = listeners[event] || []).push(cb);\n },\n off(event, cb) {\n listeners[event] = (listeners[event] || []).filter((x) => x !== cb);\n }\n };\n }\n\n // src/runtime/main.ts\n function clone(v) {\n return v === void 0 ? {} : JSON.parse(JSON.stringify(v));\n }\n function createMainApi(rpc = createRpcClient()) {\n const uiHandlers = [];\n let uiOnMessage = null;\n const uiQueue = [];\n function deliverUI(msg) {\n for (const cb of uiHandlers) {\n try {\n cb(msg);\n } catch (e) {\n console.error(e);\n }\n }\n if (typeof uiOnMessage === "function") {\n try {\n uiOnMessage(msg);\n } catch (e) {\n console.error(e);\n }\n }\n }\n function flushUI() {\n if (uiHandlers.length || typeof uiOnMessage === "function") uiQueue.splice(0).forEach(deliverUI);\n }\n rpc.on("uimessage", (msg) => {\n if (!uiHandlers.length && typeof uiOnMessage !== "function") uiQueue.push(msg);\n else deliverUI(msg);\n });\n let widgetDef = null;\n const widgetQueue = [];\n function renderWidget(state) {\n const handlers = {};\n function walk(el, path) {\n if (!el || typeof el !== "object") return el;\n const out = {};\n for (const k of Object.keys(el)) {\n const v = el[k];\n if (k === "children") {\n out.children = (Array.isArray(v) ? v : [v]).filter((c) => c !== null && c !== void 0 && c !== false).map((c, i) => walk(c, path + "." + i));\n } else if (k === "onClick" && typeof v === "function") {\n handlers[path] = v;\n out.onClick = path;\n } else {\n out[k] = v;\n }\n }\n return out;\n }\n return { tree: walk(widgetDef.render(state), "root"), handlers };\n }\n function initialWidgetState() {\n const s = widgetDef && widgetDef.initialState;\n return clone(typeof s === "function" ? s() : s);\n }\n function commitWidget(nodeId, state) {\n return rpc.call("widgetUpdate", { id: nodeId, state, tree: renderWidget(clone(state)).tree });\n }\n function handleWidgetClick(e) {\n Promise.resolve().then(() => {\n const state = clone(e.state);\n const rendered = renderWidget(state);\n const h = rendered.handlers[e.handler] || widgetDef.handlers && widgetDef.handlers[e.handler];\n if (typeof h !== "function") return;\n return Promise.resolve(h(state, { nodeId: e.nodeId })).then((ret) => commitWidget(e.nodeId, ret === void 0 ? state : ret));\n }).catch((err) => rpc.send({ kind: "error", message: String((err == null ? void 0 : err.message) || err) }));\n }\n rpc.on("widgetclick", (e) => {\n if (widgetDef) handleWidgetClick(e);\n else widgetQueue.push(e);\n });\n const popcraft = {\n /** Which menu command the user picked, or null when the plugin ran some other way. */\n command: null,\n /** Host import mode when launched as an importer, else null. */\n importMode: null,\n /** Pending import file when the host handed one over, else null. */\n importFile: null,\n /** Pending export request (File \u2192 Export \u2192 this plugin\'s format), else null. */\n exportRequest: null,\n getSelection: () => rpc.call("getSelection"),\n setSelection: (ids) => rpc.call("setSelection", { ids }),\n getNode: (id) => rpc.call("getNode", { id }),\n findNodes: (query) => rpc.call("findNodes", query || {}),\n getCurrentPage: () => rpc.call("getCurrentPage"),\n createNode: (type, props) => rpc.call("createNode", { type, props: props || {} }),\n updateNode: (id, props) => rpc.call("updateNode", { id, props }),\n deleteNode: (id) => rpc.call("deleteNode", { id }),\n setText: (id, characters) => rpc.call("setText", { id, characters }),\n /** Put TEXT layers on a path (another layer\'s outline, or their own arc, circle or wave), or tune one: brackets, alignment, placement, effect, overflow. */\n setTextPath: (opts) => rpc.call("setTextPath", opts || {}),\n /** Area type: flow a TEXT layer inside a closed shape, kept `inset` px in. */\n flowTextIntoShape: (opts) => rpc.call("flowTextIntoShape", opts || {}),\n /** Take TEXT layers off their path or out of their shape (or free every text linked to a path / shape). */\n detachText: (opts) => rpc.call("detachText", opts || {}),\n /** Auto-size TEXT layers\' type to their box, shape or path. */\n setTextFit: (opts) => rpc.call("setTextFit", opts || {}),\n createPage: (name) => rpc.call("createPage", name != null ? { name: String(name) } : {}),\n setCurrentPage: (pageId) => rpc.call("setCurrentPage", { pageId }),\n createImage: (opts) => rpc.call("createImage", opts || {}),\n createDocument: (opts) => rpc.call("createDocument", opts || {}),\n getImportMode: () => rpc.call("getImportMode"),\n getImportFile: () => rpc.call("getImportFile"),\n /**\n * The open page as a storefront design in a neutral form, for an exporter that writes its own store\'s templates:\n * each page\'s HTML with the store\'s data marked (`{{page.title}}`, `{{item.price}}`, `<!-- pc:each \u2026 -->` lists,\n * `data-pc-action` forms), and its collections with their commerce schema and sample records.\n */\n getStorefront: () => rpc.call("getStorefront"),\n /**\n * A design\'s data (docs: Collections): a collection and its fields (`schema`: what it stands for, e.g.\n * `commerce.product`, whose fields are made for it), its records, and reading them.\n */\n editCollection: (opts) => rpc.call("editCollection", opts || {}),\n editRecords: (opts) => rpc.call("editRecords", opts || {}),\n getRecords: (opts) => rpc.call("getRecords", opts || {}),\n /** A layer as a web page\'s form control (a FORM\'s `formAction`, an INPUT\'s name and value), repeated per record, a page\'s address, its semantics. */\n setControl: (opts) => rpc.call("setControl", opts || {}),\n setRepeat: (opts) => rpc.call("setRepeat", opts || {}),\n setRoute: (opts) => rpc.call("setRoute", opts || {}),\n setSemantics: (opts) => rpc.call("setSemantics", opts || {}),\n /** Which record layers are about (one product, picked from a collection), and which of its fields a layer\'s words, picture or an instance\'s properties follow. */\n setRecord: (opts) => rpc.call("setRecord", opts || {}),\n bindField: (opts) => rpc.call("bindField", opts || {}),\n /** The site\'s environment: public settings its host reads by name when it is deployed (never a secret: the file keeps them). */\n getSiteEnvironment: () => rpc.call("getSiteEnvironment"),\n setSiteEnvironment: (set) => rpc.call("setSiteEnvironment", { set }),\n /** What this plugin keeps with the open file (its settings for this design), under its own id; JSON only. */\n getFileData: (key) => rpc.call("getFileData", { key }),\n setFileData: (key, value) => rpc.call("setFileData", { key, value }),\n /** A store\'s project built from the open page, zipped (needs `export`): what an exporter saves with `saveExport`. */\n getStorefrontFiles: (target) => rpc.call("getStorefrontFiles", { target }),\n getExportRequest: () => rpc.call("getExportRequest"),\n /** Save the exporter\'s file (once per export request); the host enforces the contributed extension. */\n saveExport: (file) => rpc.call("saveExport", file || {}),\n /**\n * Presentation audio (docs/plugins/presentation-audio.md): the deck\'s slides, notes, narration and timing; the\n * soundtrack\'s settings, bed and stinger.\n */\n getPresentationAudio: (opts) => rpc.call("getPresentationAudio", opts || {}),\n /** Change the soundtrack\'s settings (voice, style, mood, loudness, beat alignment, levels) and re-time it. */\n setPresentationAudioSettings: (opts) => rpc.call("setPresentationAudioSettings", opts || {}),\n /** Put a sound in (a slide\'s narration, the bed or the stinger): bytes are normalised to the soundtrack\'s target and stored. */\n setPresentationAudioClip: (opts) => rpc.call("setPresentationAudioClip", opts || {}),\n removePresentationAudioClip: (opts) => rpc.call("removePresentationAudioClip", opts || {}),\n /** Re-time: fixed slide lengths (ms, null clears) and beat-aligned changes. */\n setPresentationTiming: (opts) => rpc.call("setPresentationTiming", opts || {}),\n /** The mix as the export carries it: loudness, true peak, and (wav: true) the 48 kHz stereo WAV. */\n getPresentationMix: (opts) => rpc.call("getPresentationMix", opts || {}),\n /** Ask the user to generate with AI (the panel opens with the request filled in; the user decides). */\n requestPresentationAudio: (opts) => rpc.call("requestPresentationAudio", opts || {}),\n /** Editor commands: each takes the arguments documented in docs/plugins/methods.md (the agent tool of the same purpose). */\n moveNodes: (opts) => rpc.call("moveNodes", opts || {}),\n resizeNodes: (opts) => rpc.call("resizeNodes", opts || {}),\n renameNodes: (opts) => rpc.call("renameNodes", opts || {}),\n reparentNodes: (opts) => rpc.call("reparentNodes", opts || {}),\n duplicateNodes: (opts) => rpc.call("duplicateNodes", opts || {}),\n groupNodes: (opts) => rpc.call("groupNodes", opts || {}),\n ungroupNodes: (opts) => rpc.call("ungroupNodes", opts || {}),\n alignNodes: (opts) => rpc.call("alignNodes", opts || {}),\n distributeNodes: (opts) => rpc.call("distributeNodes", opts || {}),\n flipNodes: (opts) => rpc.call("flipNodes", opts || {}),\n rotateNodes: (opts) => rpc.call("rotateNodes", opts || {}),\n reorderNodes: (opts) => rpc.call("reorderNodes", opts || {}),\n setLocked: (opts) => rpc.call("setLocked", opts || {}),\n setVisible: (opts) => rpc.call("setVisible", opts || {}),\n setAutoLayout: (opts) => rpc.call("setAutoLayout", opts || {}),\n importSvg: (opts) => rpc.call("importSvg", opts || {}),\n measureNodes: (opts) => rpc.call("measureNodes", opts || {}),\n setFills: (opts) => rpc.call("setFills", opts || {}),\n setStrokes: (opts) => rpc.call("setStrokes", opts || {}),\n setEffects: (opts) => rpc.call("setEffects", opts || {}),\n replaceColor: (opts) => rpc.call("replaceColor", opts || {}),\n setTypography: (opts) => rpc.call("setTypography", opts || {}),\n applyStyle: (opts) => rpc.call("applyStyle", opts || {}),\n createComponent: (opts) => rpc.call("createComponent", opts || {}),\n createInstance: (opts) => rpc.call("createInstance", opts || {}),\n renamePage: (opts) => rpc.call("renamePage", opts || {}),\n getDocumentOutline: (opts) => rpc.call("getDocumentOutline", opts || {}),\n setPageSetup: (opts) => rpc.call("setPageSetup", opts || {}),\n setShowAdvance: (opts) => rpc.call("setShowAdvance", opts || {}),\n booleanOperation: (opts) => rpc.call("booleanOperation", opts || {}),\n flattenNodes: (opts) => rpc.call("flattenNodes", opts || {}),\n outlineStroke: (opts) => rpc.call("outlineStroke", opts || {}),\n setMask: (opts) => rpc.call("setMask", opts || {}),\n setMaskType: (opts) => rpc.call("setMaskType", opts || {}),\n shapeBuilder: (opts) => rpc.call("shapeBuilder", opts || {}),\n setKeyframes: (opts) => rpc.call("setKeyframes", opts || {}),\n getAnimation: (opts) => rpc.call("getAnimation", opts || {}),\n getCompositions: (opts) => rpc.call("getCompositions", opts || {}),\n editBehaviours: (opts) => rpc.call("editBehaviours", opts || {}),\n setTimeline: (opts) => rpc.call("setTimeline", opts || {}),\n setLayerTiming: (opts) => rpc.call("setLayerTiming", opts || {}),\n editTextAnimation: (opts) => rpc.call("editTextAnimation", opts || {}),\n edit3DScene: (opts) => rpc.call("edit3DScene", opts || {}),\n setInstancePlayback: (opts) => rpc.call("setInstancePlayback", opts || {}),\n editComponentProperty: (opts) => rpc.call("editComponentProperty", opts || {}),\n setShaderFill: (opts) => rpc.call("setShaderFill", opts || {}),\n createComposition: (opts) => rpc.call("createComposition", opts || {}),\n editComposition: (opts) => rpc.call("editComposition", opts || {}),\n editStateMachine: (opts) => rpc.call("editStateMachine", opts || {}),\n getStateMachine: (opts) => rpc.call("getStateMachine", opts || {}),\n applyMotionPreset: (opts) => rpc.call("applyMotionPreset", opts || {}),\n staggerMotion: (opts) => rpc.call("staggerMotion", opts || {}),\n createMotionComponent: (opts) => rpc.call("createMotionComponent", opts || {}),\n addAudio: (opts) => rpc.call("addAudio", opts || {}),\n editFrameAudio: (opts) => rpc.call("editFrameAudio", opts || {}),\n editVideoClip: (opts) => rpc.call("editVideoClip", opts || {}),\n detectAudioOnsets: (opts) => rpc.call("detectAudioOnsets", opts || {}),\n cutToBeat: (opts) => rpc.call("cutToBeat", opts || {}),\n editPrototype: (opts) => rpc.call("editPrototype", opts || {}),\n usePresentationFallbackAudio: (opts) => rpc.call("usePresentationFallbackAudio", opts || {}),\n /** Bring a Lottie, PDF or PopCraft file into the open document (the `importFile` field is the file an importer was handed). */\n importDocumentFile: (opts) => rpc.call("importDocumentFile", opts || {}),\n getStyles: () => rpc.call("getStyles"),\n createPaintStyle: (name, paints) => rpc.call("createPaintStyle", { name, paints }),\n getVariables: () => rpc.call("getVariables"),\n setVariableValue: (variableId, modeId, value) => rpc.call("setVariableValue", { variableId, modeId, value }),\n exportNode: (id, options) => rpc.call("exportNode", { id, options: options || {} }),\n openExternal: (url) => rpc.call("openExternal", { url }),\n notify: (message, options) => rpc.call("notify", { message: String(message), error: !!(options && options.error) }),\n showUI: (options) => rpc.call("showUI", options || {}),\n hideUI: () => rpc.call("hideUI"),\n /** Ends the run. The optional message is shown as a notification. */\n closePlugin: (message) => rpc.call("closePlugin", { message }),\n storage: {\n get: (key) => rpc.call("storageGet", { key }),\n set: (key, value) => rpc.call("storageSet", { key, value })\n },\n ui: {\n get onmessage() {\n return uiOnMessage;\n },\n set onmessage(cb) {\n uiOnMessage = cb;\n flushUI();\n },\n postMessage: (msg) => rpc.send({ kind: "toUI", data: msg }),\n on: (cb) => {\n uiHandlers.push(cb);\n flushUI();\n }\n },\n widget: {\n register(def) {\n if (!def || typeof def.render !== "function") throw new Error("popcraft.widget.register needs a render(state) function");\n widgetDef = def;\n widgetQueue.splice(0).forEach(handleWidgetClick);\n },\n insert(options) {\n if (!widgetDef) return Promise.reject(new Error("Call popcraft.widget.register first"));\n const o = options || {};\n const state = o.state !== void 0 ? clone(o.state) : initialWidgetState();\n return rpc.call("widgetInsert", { state, tree: renderWidget(state).tree, x: o.x, y: o.y });\n },\n getState: (nodeId) => rpc.call("widgetGet", { id: nodeId }),\n setState(nodeId, state) {\n if (!widgetDef) return Promise.reject(new Error("Call popcraft.widget.register first"));\n return commitWidget(nodeId, clone(state));\n }\n },\n on: (event, cb) => rpc.on(event, cb),\n off: (event, cb) => rpc.off(event, cb)\n };\n rpc.on("init", (data) => {\n var _a, _b, _c;\n popcraft.command = data.command;\n popcraft.importMode = (_a = data.importMode) != null ? _a : null;\n popcraft.importFile = (_b = data.importFile) != null ? _b : null;\n popcraft.exportRequest = (_c = data.exportRequest) != null ? _c : null;\n });\n return popcraft;\n }\n function installMainApi() {\n const rpc = createRpcClient();\n const api = createMainApi(rpc);\n const w = window;\n w.__popcraftRpc = rpc;\n w.popcraft = api;\n w.vizpops = api;\n window.addEventListener("error", (e) => rpc.send({ kind: "error", message: String(e.message || e) }));\n window.addEventListener("unhandledrejection", (e) => rpc.send({ kind: "error", message: String(e.reason && e.reason.message || e.reason) }));\n return api;\n }\n\n // src/runtime/main.entry.ts\n installMainApi();\n})();';
358881
358945
  UI_RUNTIME = '"use strict";\n(() => {\n // src/runtime/rpc.ts\n function createRpcClient() {\n let port = null;\n const queue = [];\n const pending = {};\n const listeners = {};\n let nextId = 1;\n function send(msg) {\n if (port) port.postMessage(msg);\n else queue.push(msg);\n }\n function call(method, params) {\n return new Promise((resolve, reject) => {\n const id = nextId++;\n pending[id] = { resolve, reject };\n send({ kind: "rpc", id, method, params });\n });\n }\n function emit(event, data) {\n for (const cb of (listeners[event] || []).slice()) {\n try {\n cb(data);\n } catch (e) {\n console.error(e);\n }\n }\n }\n window.addEventListener("message", (e) => {\n const data = e.data;\n if (e.source !== window.parent || !data || data.type !== "popcraft-plugin-port" || port) return;\n port = e.ports[0];\n port.onmessage = (ev) => {\n const m = ev.data || {};\n if (m.kind === "result" && m.id !== void 0 && pending[m.id]) {\n const p = pending[m.id];\n delete pending[m.id];\n if (m.error) p.reject(new Error(m.error));\n else p.resolve(m.result);\n } else if (m.kind === "event" && m.event) {\n emit(m.event, m.data);\n }\n };\n for (const msg of queue.splice(0)) port.postMessage(msg);\n });\n return {\n call,\n send,\n on(event, cb) {\n (listeners[event] = listeners[event] || []).push(cb);\n },\n off(event, cb) {\n listeners[event] = (listeners[event] || []).filter((x) => x !== cb);\n }\n };\n }\n\n // src/runtime/ui.ts\n function createUiApi(rpc = createRpcClient()) {\n const popcraft = {\n onmessage: null,\n postMessage: (msg) => rpc.send({ kind: "fromUI", data: msg }),\n close: () => rpc.send({ kind: "closeUI" })\n };\n rpc.on("message", (msg) => {\n if (typeof popcraft.onmessage === "function") popcraft.onmessage(msg);\n window.dispatchEvent(new MessageEvent("message", { data: { pluginMessage: msg } }));\n });\n return popcraft;\n }\n function installUiApi() {\n const rpc = createRpcClient();\n const api = createUiApi(rpc);\n const w = window;\n w.__popcraftRpc = rpc;\n w.popcraft = api;\n w.vizpops = api;\n return api;\n }\n\n // src/runtime/ui.entry.ts\n installUiApi();\n})();';
358882
358946
  }
358883
358947
  });
358884
358948
 
358885
- // node_modules/.pnpm/@popcraft+plugins@1.0.0_patch_hash=e61849e2a6a59df95f0ebad36ab274c37a933fcf798d8b4dd6b3f0ffc625a1da/node_modules/@popcraft/plugins/dist/host.js
358949
+ // node_modules/.pnpm/@popcraft+plugins@1.0.0_patch_hash=6826cb40a12857607fa1f7d7d102e5d8316bd483c1ff06865dac73139dfe709b/node_modules/@popcraft/plugins/dist/host.js
358886
358950
  function csp(network) {
358887
358951
  const connect2 = network ? "connect-src * data: blob:;" : "connect-src 'none';";
358888
358952
  const img = network ? "img-src * data: blob:;" : "img-src data: blob:;";
@@ -358910,40 +358974,40 @@ function buildUIDocument(uiHtml, network) {
358910
358974
  }
358911
358975
  var escapeScript;
358912
358976
  var init_host2 = __esm({
358913
- "node_modules/.pnpm/@popcraft+plugins@1.0.0_patch_hash=e61849e2a6a59df95f0ebad36ab274c37a933fcf798d8b4dd6b3f0ffc625a1da/node_modules/@popcraft/plugins/dist/host.js"() {
358977
+ "node_modules/.pnpm/@popcraft+plugins@1.0.0_patch_hash=6826cb40a12857607fa1f7d7d102e5d8316bd483c1ff06865dac73139dfe709b/node_modules/@popcraft/plugins/dist/host.js"() {
358914
358978
  init_runtime4();
358915
358979
  escapeScript = (code) => code.replace(/<\/script/gi, "<\\/script");
358916
358980
  }
358917
358981
  });
358918
358982
 
358919
- // node_modules/.pnpm/@popcraft+plugins@1.0.0_patch_hash=e61849e2a6a59df95f0ebad36ab274c37a933fcf798d8b4dd6b3f0ffc625a1da/node_modules/@popcraft/plugins/dist/runtime/rpc.js
358983
+ // node_modules/.pnpm/@popcraft+plugins@1.0.0_patch_hash=6826cb40a12857607fa1f7d7d102e5d8316bd483c1ff06865dac73139dfe709b/node_modules/@popcraft/plugins/dist/runtime/rpc.js
358920
358984
  var init_rpc = __esm({
358921
- "node_modules/.pnpm/@popcraft+plugins@1.0.0_patch_hash=e61849e2a6a59df95f0ebad36ab274c37a933fcf798d8b4dd6b3f0ffc625a1da/node_modules/@popcraft/plugins/dist/runtime/rpc.js"() {
358985
+ "node_modules/.pnpm/@popcraft+plugins@1.0.0_patch_hash=6826cb40a12857607fa1f7d7d102e5d8316bd483c1ff06865dac73139dfe709b/node_modules/@popcraft/plugins/dist/runtime/rpc.js"() {
358922
358986
  }
358923
358987
  });
358924
358988
 
358925
- // node_modules/.pnpm/@popcraft+plugins@1.0.0_patch_hash=e61849e2a6a59df95f0ebad36ab274c37a933fcf798d8b4dd6b3f0ffc625a1da/node_modules/@popcraft/plugins/dist/runtime/main.js
358989
+ // node_modules/.pnpm/@popcraft+plugins@1.0.0_patch_hash=6826cb40a12857607fa1f7d7d102e5d8316bd483c1ff06865dac73139dfe709b/node_modules/@popcraft/plugins/dist/runtime/main.js
358926
358990
  var init_main = __esm({
358927
- "node_modules/.pnpm/@popcraft+plugins@1.0.0_patch_hash=e61849e2a6a59df95f0ebad36ab274c37a933fcf798d8b4dd6b3f0ffc625a1da/node_modules/@popcraft/plugins/dist/runtime/main.js"() {
358991
+ "node_modules/.pnpm/@popcraft+plugins@1.0.0_patch_hash=6826cb40a12857607fa1f7d7d102e5d8316bd483c1ff06865dac73139dfe709b/node_modules/@popcraft/plugins/dist/runtime/main.js"() {
358928
358992
  init_rpc();
358929
358993
  }
358930
358994
  });
358931
358995
 
358932
- // node_modules/.pnpm/@popcraft+plugins@1.0.0_patch_hash=e61849e2a6a59df95f0ebad36ab274c37a933fcf798d8b4dd6b3f0ffc625a1da/node_modules/@popcraft/plugins/dist/types.js
358996
+ // node_modules/.pnpm/@popcraft+plugins@1.0.0_patch_hash=6826cb40a12857607fa1f7d7d102e5d8316bd483c1ff06865dac73139dfe709b/node_modules/@popcraft/plugins/dist/types.js
358933
358997
  var init_types9 = __esm({
358934
- "node_modules/.pnpm/@popcraft+plugins@1.0.0_patch_hash=e61849e2a6a59df95f0ebad36ab274c37a933fcf798d8b4dd6b3f0ffc625a1da/node_modules/@popcraft/plugins/dist/types.js"() {
358998
+ "node_modules/.pnpm/@popcraft+plugins@1.0.0_patch_hash=6826cb40a12857607fa1f7d7d102e5d8316bd483c1ff06865dac73139dfe709b/node_modules/@popcraft/plugins/dist/types.js"() {
358935
358999
  }
358936
359000
  });
358937
359001
 
358938
- // node_modules/.pnpm/@popcraft+plugins@1.0.0_patch_hash=e61849e2a6a59df95f0ebad36ab274c37a933fcf798d8b4dd6b3f0ffc625a1da/node_modules/@popcraft/plugins/dist/widget.js
359002
+ // node_modules/.pnpm/@popcraft+plugins@1.0.0_patch_hash=6826cb40a12857607fa1f7d7d102e5d8316bd483c1ff06865dac73139dfe709b/node_modules/@popcraft/plugins/dist/widget.js
358939
359003
  var init_widget = __esm({
358940
- "node_modules/.pnpm/@popcraft+plugins@1.0.0_patch_hash=e61849e2a6a59df95f0ebad36ab274c37a933fcf798d8b4dd6b3f0ffc625a1da/node_modules/@popcraft/plugins/dist/widget.js"() {
359004
+ "node_modules/.pnpm/@popcraft+plugins@1.0.0_patch_hash=6826cb40a12857607fa1f7d7d102e5d8316bd483c1ff06865dac73139dfe709b/node_modules/@popcraft/plugins/dist/widget.js"() {
358941
359005
  }
358942
359006
  });
358943
359007
 
358944
- // node_modules/.pnpm/@popcraft+plugins@1.0.0_patch_hash=e61849e2a6a59df95f0ebad36ab274c37a933fcf798d8b4dd6b3f0ffc625a1da/node_modules/@popcraft/plugins/dist/index.js
359008
+ // node_modules/.pnpm/@popcraft+plugins@1.0.0_patch_hash=6826cb40a12857607fa1f7d7d102e5d8316bd483c1ff06865dac73139dfe709b/node_modules/@popcraft/plugins/dist/index.js
358945
359009
  var init_dist = __esm({
358946
- "node_modules/.pnpm/@popcraft+plugins@1.0.0_patch_hash=e61849e2a6a59df95f0ebad36ab274c37a933fcf798d8b4dd6b3f0ffc625a1da/node_modules/@popcraft/plugins/dist/index.js"() {
359010
+ "node_modules/.pnpm/@popcraft+plugins@1.0.0_patch_hash=6826cb40a12857607fa1f7d7d102e5d8316bd483c1ff06865dac73139dfe709b/node_modules/@popcraft/plugins/dist/index.js"() {
358947
359011
  init_host2();
358948
359012
  init_main();
358949
359013
  init_runtime4();
@@ -394047,7 +394111,31 @@ function collectionOf(doc2, ref3) {
394047
394111
  if (!c2) throw new ToolError(`No collection "${ref3}". ${Object.values(doc2.collections ?? {}).length ? `There is: ${Object.values(doc2.collections).map((x2) => x2.name).join(", ")}.` : "Make one with edit_collection."}`);
394048
394112
  return c2;
394049
394113
  }
394050
- var sizing2, padding2, overrides, nodeSetResponsive, link, nodeSetSemantics, nodeSetControl, siteSetRoute, breakpoint, siteSetBreakpoints, siteSetGpuTransitions, ENV_NAME, RESERVED_ENV, siteSetEnvironment, siteGetEnvironment, siteGetBreakpoints, described2, dataEditCollection, dataEditRecords, dataGetRecords, nodeSetRepeat;
394114
+ function recordOf(doc2, c2, ref3) {
394115
+ const own2 = doc2.records?.[ref3];
394116
+ if (own2?.collectionId === c2.id) return own2;
394117
+ const slugs = recordSlugs(doc2, c2.id);
394118
+ const want = ref3.trim().toLowerCase();
394119
+ const found = recordsIn(doc2, c2.id).find((r) => slugs.get(r.id) === want || c2.fields.some((f2) => String(r.values[f2.id] ?? "").trim().toLowerCase() === want));
394120
+ if (!found) throw new ToolError(`${c2.name} has no record "${ref3}". Read them with get_records.`);
394121
+ return found;
394122
+ }
394123
+ function bindingKey(doc2, node2, property) {
394124
+ if (property === "text") {
394125
+ if (node2.type !== "TEXT") throw new ToolError(`"${node2.name}" is not a text.`);
394126
+ return "characters";
394127
+ }
394128
+ if (property === "picture") {
394129
+ if (node2.type !== "IMAGE") throw new ToolError(`"${node2.name}" is not an image layer: give its component an IMAGE property instead.`);
394130
+ return "imageUrl";
394131
+ }
394132
+ if (node2.type !== "INSTANCE") throw new ToolError(`"${node2.name}" has no property "${property}": say text, picture, or an instance's property.`);
394133
+ const defs = componentDefinitions(doc2.nodes, node2.mainComponentId);
394134
+ const def = defs[property];
394135
+ if (!def || !["TEXT", "IMAGE", "BOOLEAN"].includes(def.type)) throw new ToolError(`"${node2.name}" has no TEXT, IMAGE or BOOLEAN property "${property}". It has: ${Object.keys(defs).join(", ") || "none"}.`);
394136
+ return `${PROP_BINDING}${property}`;
394137
+ }
394138
+ var sizing2, padding2, overrides, nodeSetResponsive, link, nodeSetSemantics, nodeSetControl, siteSetRoute, breakpoint, siteSetBreakpoints, siteSetGpuTransitions, ENV_NAME, RESERVED_ENV, siteSetEnvironment, siteGetEnvironment, siteGetBreakpoints, described2, dataEditCollection, dataEditRecords, dataGetRecords, nodeSetRepeat, nodeSetRecord, nodeBindField, recordLabel;
394051
394139
  var init_site3 = __esm({
394052
394140
  "lib/commands/site.ts"() {
394053
394141
  "use strict";
@@ -394056,6 +394144,7 @@ var init_site3 = __esm({
394056
394144
  init_breakpoints();
394057
394145
  init_collections();
394058
394146
  init_semantics();
394147
+ init_instances();
394059
394148
  init_controls();
394060
394149
  init_types();
394061
394150
  init_routes();
@@ -394654,6 +394743,83 @@ var init_site3 = __esm({
394654
394743
  return { summary: c2 ? `Repeats for ${c2.name} (${shown2} records)` : "No longer repeats", data: { ids: ids2, collectionId: c2?.id ?? null } };
394655
394744
  }
394656
394745
  });
394746
+ nodeSetRecord = defineCommand({
394747
+ id: "node.setRecord",
394748
+ title: "Show a record",
394749
+ domain: "node",
394750
+ effect: "document",
394751
+ permission: "write",
394752
+ undoLabel: "Show a record",
394753
+ params: external_exports.object({
394754
+ ids: external_exports.array(external_exports.string()).min(1).max(50).optional().describe("Layers to edit (default: the selection)."),
394755
+ collection: external_exports.string().nullable().describe("The collection, by id or name; null: the layers show no record of their own again."),
394756
+ record: external_exports.string().optional().describe("The record, by id, page address, or the value of one of its fields (a product's handle or title).")
394757
+ }).strict(),
394758
+ expose: {
394759
+ plugin: { method: "setRecord" },
394760
+ ui: { label: "Show a record", keywords: ["record", "data", "bind", "product", "cms", "collection", "source"], enabled: needsSelection(1), palette: false },
394761
+ agent: {
394762
+ name: "set_record",
394763
+ description: "Make layers about one record of a collection (one product, one post): they and what they hold show its values wherever they follow a field \u2014 a text with {{item.<field key>}}, a picture or an instance property bound with bind_field. The nearest record wins: a layer's own, then a list it is in, then its page's."
394764
+ }
394765
+ },
394766
+ run: (args2, ctx2) => {
394767
+ const { doc: doc2, ids: ids2 } = resolveTargets(ctx2, args2.ids, "set_record");
394768
+ const c2 = args2.collection ? collectionOf(doc2, args2.collection) : null;
394769
+ if (c2 && !args2.record) throw new ToolError("Say which record.");
394770
+ const r = c2 ? recordOf(doc2, c2, args2.record) : null;
394771
+ const pin = c2 && r ? { collectionId: c2.id, recordId: r.id } : void 0;
394772
+ commitUpdates(ctx2, doc2, new Map(ids2.map((id3) => [id3, { record: pin }])));
394773
+ return { summary: pin ? `Shows ${c2.name}: ${recordLabel(c2, r)}` : "Shows no record of its own", data: { ids: ids2, record: pin ?? null } };
394774
+ }
394775
+ });
394776
+ nodeBindField = defineCommand({
394777
+ id: "node.bindField",
394778
+ title: "Bind to a field",
394779
+ domain: "node",
394780
+ effect: "document",
394781
+ permission: "write",
394782
+ undoLabel: "Bind to a field",
394783
+ params: external_exports.object({
394784
+ ids: external_exports.array(external_exports.string()).min(1).max(50).optional().describe("Layers to edit (default: the selection)."),
394785
+ property: external_exports.string().min(1).max(120).describe(`What follows the field: "text" (a text's words), "picture" (an image layer's picture), or the name of one of an instance's TEXT, IMAGE or BOOLEAN properties.`),
394786
+ field: external_exports.string().nullable().describe("The field, by name or key, of the record the layer is about (its own, its list's or its page's); null: it stops following one.")
394787
+ }).strict(),
394788
+ expose: {
394789
+ plugin: { method: "bindField" },
394790
+ ui: { label: "Bind to a field", keywords: ["bind", "field", "data", "source", "dynamic", "cms", "product"], enabled: needsSelection(1), palette: false },
394791
+ agent: {
394792
+ name: "bind_field",
394793
+ description: "Make a layer show a field of the record it is about (set_record, a list, or a collection page): a text's words, an image layer's picture, or an instance's TEXT / IMAGE / BOOLEAN property \u2014 so a product card component takes its title, price and photo from the record. null unbinds."
394794
+ }
394795
+ },
394796
+ run: (args2, ctx2) => {
394797
+ const { doc: doc2, ids: ids2 } = resolveTargets(ctx2, args2.ids, "bind_field");
394798
+ const updates = /* @__PURE__ */ new Map();
394799
+ for (const id3 of ids2) {
394800
+ const node2 = doc2.nodes[id3];
394801
+ const key5 = bindingKey(doc2, node2, args2.property);
394802
+ const bound = { ...node2.boundExpressions ?? {} };
394803
+ if (args2.field === null) delete bound[key5];
394804
+ else {
394805
+ const record2 = shownRecordOf(doc2.nodes, doc2, id3);
394806
+ const c2 = record2 && doc2.collections?.[record2.collectionId];
394807
+ if (!c2) throw new ToolError(`"${node2.name}" is about no record: pick one with set_record, or put it in a list (set_repeat).`);
394808
+ const want = fieldKey(args2.field);
394809
+ const f2 = c2.fields.find((x2) => x2.id === args2.field || fieldKey(x2.name) === want);
394810
+ if (!f2) throw new ToolError(`${c2.name} has no field "${args2.field}". It has: ${c2.fields.map((x2) => fieldKey(x2.name)).join(", ")}.`);
394811
+ bound[key5] = `item_${fieldKey(f2.name)}`;
394812
+ }
394813
+ updates.set(id3, { boundExpressions: Object.keys(bound).length ? bound : void 0 });
394814
+ }
394815
+ commitUpdates(ctx2, doc2, updates);
394816
+ return { summary: args2.field === null ? `${args2.property} follows no field` : `${args2.property} \u2190 ${args2.field}`, data: { ids: ids2 } };
394817
+ }
394818
+ });
394819
+ recordLabel = (c2, r) => {
394820
+ const f2 = c2.fields.find((x2) => x2.type === "TEXT") ?? c2.fields[0];
394821
+ return String((f2 && r.values[f2.id]) ?? r.id).slice(0, 80);
394822
+ };
394657
394823
  }
394658
394824
  });
394659
394825
 
@@ -395751,6 +395917,8 @@ var init_builtin4 = __esm({
395751
395917
  siteSetRoute,
395752
395918
  nodeSetControl,
395753
395919
  nodeSetRepeat,
395920
+ nodeSetRecord,
395921
+ nodeBindField,
395754
395922
  siteSetEnvironment,
395755
395923
  siteGetEnvironment,
395756
395924
  codeEditModule,
@@ -403057,7 +403225,7 @@ init_resources();
403057
403225
  init_desktop();
403058
403226
  init_hypery();
403059
403227
  import { createInterface } from "node:readline";
403060
- var VERSION2 = "0.20.2";
403228
+ var VERSION2 = "0.21.0";
403061
403229
  var HELP = `popcraft ${VERSION2} \u2014 the PopCraft command line
403062
403230
 
403063
403231
  Files (headless, on .popcraft files):
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@popcraft/cli",
3
- "version": "0.20.2",
3
+ "version": "0.21.0",
4
4
  "description": "The PopCraft command line: edit .popcraft files headlessly, call the REST API, publish to the marketplace and manage the desktop app's local plugins, themes, fonts, skills and agents.",
5
5
  "license": "MIT",
6
6
  "homepage": "https://popcraft.app/docs/api/cli",
@@ -34,7 +34,7 @@
34
34
  "esbuild": "0.28.2"
35
35
  },
36
36
  "dependencies": {
37
- "@popcraft/kit": "0.20.2"
37
+ "@popcraft/kit": "0.21.0"
38
38
  },
39
39
  "scripts": {
40
40
  "build": "node build.mjs",