webstudio 0.268.0 → 0.269.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/lib/cli.js CHANGED
@@ -374,7 +374,7 @@ const getApiCompatibilityPayload = (value) => {
374
374
  return findPayload(value, /* @__PURE__ */ new WeakSet());
375
375
  };
376
376
  const name = "webstudio";
377
- const version = "0.268.0";
377
+ const version = "0.269.0";
378
378
  const description = "Webstudio CLI";
379
379
  const author = "Webstudio <github@webstudio.is>";
380
380
  const homepage = "https://webstudio.is";
@@ -4207,6 +4207,14 @@ var Page = z.object({
4207
4207
  ...commonPageFields,
4208
4208
  path: z.union([HomePagePath, PagePath])
4209
4209
  });
4210
+ var PageTemplate = z.object({
4211
+ id: PageId,
4212
+ name: PageName,
4213
+ title: PageTitle,
4214
+ rootInstanceId: z.string(),
4215
+ systemDataSourceId: z.string().optional(),
4216
+ meta: commonPageFields.meta
4217
+ });
4210
4218
  var ProjectMeta = z.object({
4211
4219
  // All fields are optional to ensure consistency and allow for the addition of new fields without requiring migration
4212
4220
  siteName: z.string().optional(),
@@ -4239,6 +4247,7 @@ var Pages = z.object({
4239
4247
  homePageId: PageId,
4240
4248
  rootFolderId: FolderId,
4241
4249
  pages: z.map(PageId, Page),
4250
+ pageTemplates: z.map(PageId, PageTemplate).optional(),
4242
4251
  folders: z.map(FolderId, Folder).refine((folders) => folders.size > 0, "Folders can't be empty")
4243
4252
  }).superRefine((pages, context) => {
4244
4253
  const homePage = pages.pages.get(pages.homePageId);
@@ -4280,6 +4289,22 @@ var Pages = z.object({
4280
4289
  });
4281
4290
  }
4282
4291
  }
4292
+ for (const [templateId, template] of pages.pageTemplates ?? []) {
4293
+ if (template.id !== templateId) {
4294
+ context.addIssue({
4295
+ code: z.ZodIssueCode.custom,
4296
+ path: ["pageTemplates", templateId, "id"],
4297
+ message: "Page template id must match its record key"
4298
+ });
4299
+ }
4300
+ if (pages.pages.has(templateId)) {
4301
+ context.addIssue({
4302
+ code: z.ZodIssueCode.custom,
4303
+ path: ["pageTemplates", templateId, "id"],
4304
+ message: "Page template id must not match an existing page id"
4305
+ });
4306
+ }
4307
+ }
4283
4308
  for (const [folderId, folder] of pages.folders) {
4284
4309
  if (folder.id !== folderId) {
4285
4310
  context.addIssue({
@@ -4923,6 +4948,13 @@ var Select = z.object({
4923
4948
  defaultValue: z.string().optional(),
4924
4949
  options: z.array(z.string())
4925
4950
  });
4951
+ var TimeZone = z.object({
4952
+ ...common,
4953
+ control: z.literal("timeZone"),
4954
+ type: z.literal("string"),
4955
+ defaultValue: z.string().optional(),
4956
+ options: z.array(z.string())
4957
+ });
4926
4958
  var Check = z.object({
4927
4959
  ...common,
4928
4960
  control: z.literal("check"),
@@ -5003,6 +5035,7 @@ var PropMeta = z.union([
5003
5035
  Radio,
5004
5036
  InlineRadio,
5005
5037
  Select,
5038
+ TimeZone,
5006
5039
  MultiSelect,
5007
5040
  Check,
5008
5041
  InlineCheck,
@@ -5906,6 +5939,29 @@ var parseComponentName = (componentName) => {
5906
5939
  }
5907
5940
  return [namespace, name2];
5908
5941
  };
5942
+ var getHtmlTagsFromProps = (props) => {
5943
+ const tags2 = /* @__PURE__ */ new Map();
5944
+ for (const prop of props.values()) {
5945
+ if (prop.type === "string" && prop.name === "tag") {
5946
+ tags2.set(prop.instanceId, prop.value);
5947
+ }
5948
+ }
5949
+ return tags2;
5950
+ };
5951
+ var getHtmlTagFromInstance = ({
5952
+ instance: instance2,
5953
+ metas,
5954
+ props,
5955
+ htmlTagsByInstanceId
5956
+ }) => {
5957
+ if (instance2.component === "XmlNode") {
5958
+ return;
5959
+ }
5960
+ const meta = metas.get(instance2.component);
5961
+ const metaTag = Object.keys((meta == null ? void 0 : meta.presetStyle) ?? {}).at(0);
5962
+ const propTag = (htmlTagsByInstanceId == null ? void 0 : htmlTagsByInstanceId.get(instance2.id)) ?? getHtmlTagsFromProps(props).get(instance2.id);
5963
+ return instance2.tag ?? propTag ?? metaTag;
5964
+ };
5909
5965
  var getIndexesWithinAncestors = (metas, instances, rootIds) => {
5910
5966
  const ancestors = /* @__PURE__ */ new Set();
5911
5967
  for (const meta of metas.values()) {
@@ -5958,6 +6014,24 @@ var systemParameter = {
5958
6014
  type: "parameter",
5959
6015
  name: "system"
5960
6016
  };
6017
+ var walkAssignmentTarget = (node, visitor) => {
6018
+ var _a2, _b2, _c2, _d2;
6019
+ if (node.type === "Identifier") {
6020
+ (_a2 = visitor.Identifier) == null ? void 0 : _a2.call(visitor, node, "binding");
6021
+ return;
6022
+ }
6023
+ if (node.type === "MemberExpression") {
6024
+ (_b2 = visitor.MemberExpression) == null ? void 0 : _b2.call(visitor, node);
6025
+ const { object } = node;
6026
+ if (object.type === "Identifier") {
6027
+ (_c2 = visitor.Identifier) == null ? void 0 : _c2.call(visitor, object, "memberObject");
6028
+ } else if (object.type === "MemberExpression") {
6029
+ walkAssignmentTarget(object, visitor);
6030
+ }
6031
+ return;
6032
+ }
6033
+ (_d2 = visitor.UnsupportedPattern) == null ? void 0 : _d2.call(visitor, node);
6034
+ };
5961
6035
  var stringMethodReturnKindByName = /* @__PURE__ */ new Map([
5962
6036
  ["toLowerCase", "string"],
5963
6037
  ["replace", "string"],
@@ -5995,17 +6069,44 @@ var transpileExpression = ({
5995
6069
  const message = error.message;
5996
6070
  throw Error(`${message} in ${JSON.stringify(expression)}`);
5997
6071
  }
6072
+ const assignmentTargetMemberRanges = [];
6073
+ if (executable) {
6074
+ simple(root, {
6075
+ AssignmentExpression(node) {
6076
+ walkAssignmentTarget(node.left, {
6077
+ MemberExpression(node2) {
6078
+ assignmentTargetMemberRanges.push([node2.start, node2.end]);
6079
+ }
6080
+ });
6081
+ }
6082
+ });
6083
+ }
5998
6084
  const replacements = [];
6085
+ const replacementIndexByRange = /* @__PURE__ */ new Map();
6086
+ const addReplacement = (start, end, fragment, { replaceExisting = false } = {}) => {
6087
+ const range = `${start}:${end}`;
6088
+ const existingIndex = replacementIndexByRange.get(range);
6089
+ if (existingIndex !== void 0) {
6090
+ if (replaceExisting) {
6091
+ replacements[existingIndex] = [start, end, fragment];
6092
+ }
6093
+ return;
6094
+ }
6095
+ replacementIndexByRange.set(range, replacements.length);
6096
+ replacements.push([start, end, fragment]);
6097
+ };
5999
6098
  const replaceIdentifier = (node, assignee) => {
6000
6099
  const newName = replaceVariable == null ? void 0 : replaceVariable(node.name, assignee);
6001
6100
  if (newName) {
6002
- replacements.push([node.start, node.end, newName]);
6101
+ addReplacement(node.start, node.end, newName, {
6102
+ replaceExisting: assignee
6103
+ });
6003
6104
  }
6004
6105
  };
6005
6106
  simple(root, {
6006
6107
  Identifier: (node) => replaceIdentifier(node, false),
6007
6108
  AssignmentExpression(node) {
6008
- simple(node.left, {
6109
+ walkAssignmentTarget(node.left, {
6009
6110
  Identifier: (node2) => replaceIdentifier(node2, true)
6010
6111
  });
6011
6112
  },
@@ -6013,13 +6114,18 @@ var transpileExpression = ({
6013
6114
  if (executable === false || node.optional) {
6014
6115
  return;
6015
6116
  }
6117
+ if (assignmentTargetMemberRanges.some(
6118
+ ([start, end]) => start === node.start && end === node.end
6119
+ )) {
6120
+ return;
6121
+ }
6016
6122
  if (node.computed === false) {
6017
6123
  const dotIndex = expression.indexOf(".", node.object.end);
6018
- replacements.push([dotIndex, dotIndex, "?"]);
6124
+ addReplacement(dotIndex, dotIndex, "?");
6019
6125
  }
6020
6126
  if (node.computed === true) {
6021
6127
  const dotIndex = expression.indexOf("[", node.object.end);
6022
- replacements.push([dotIndex, dotIndex, "?."]);
6128
+ addReplacement(dotIndex, dotIndex, "?.");
6023
6129
  }
6024
6130
  },
6025
6131
  CallExpression(node) {
@@ -6029,7 +6135,7 @@ var transpileExpression = ({
6029
6135
  if (node.callee.type === "MemberExpression") {
6030
6136
  const openParenIndex = expression.indexOf("(", node.callee.end);
6031
6137
  if (openParenIndex !== -1) {
6032
- replacements.push([openParenIndex, openParenIndex, "?."]);
6138
+ addReplacement(openParenIndex, openParenIndex, "?.");
6033
6139
  }
6034
6140
  }
6035
6141
  }
@@ -6103,14 +6209,21 @@ var getHomePage = (pages) => {
6103
6209
  }
6104
6210
  return homePage;
6105
6211
  };
6106
- var findPageByIdOrPath = (idOrPath, pages) => {
6212
+ function findPageByIdOrPath(idOrPath, pages, options = {}) {
6213
+ var _a2;
6107
6214
  if (idOrPath === "" || idOrPath === "/" || idOrPath === pages.homePageId) {
6108
6215
  return getHomePage(pages);
6109
6216
  }
6110
- return getAllPages(pages).find(
6217
+ const found = getAllPages(pages).find(
6111
6218
  (page) => page.id === idOrPath || getPagePath(page.id, pages) === idOrPath
6112
6219
  );
6113
- };
6220
+ if (found) {
6221
+ return found;
6222
+ }
6223
+ if (options.includeTemplates) {
6224
+ return (_a2 = pages.pageTemplates) == null ? void 0 : _a2.get(idOrPath);
6225
+ }
6226
+ }
6114
6227
  var getPagePath = (id, pages) => {
6115
6228
  const foldersMap = /* @__PURE__ */ new Map();
6116
6229
  const childParentMap = /* @__PURE__ */ new Map();
@@ -6576,22 +6689,19 @@ var generateCss = ({
6576
6689
  const scope = createScope([], normalizeClassName, "-");
6577
6690
  const tagsByComponent = /* @__PURE__ */ new Map();
6578
6691
  tagsByComponent.set(rootComponent, /* @__PURE__ */ new Set(["html"]));
6579
- const tagByInstanceId = /* @__PURE__ */ new Map();
6580
- for (const prop of props.values()) {
6581
- if (prop.type === "string" && prop.name === "tag") {
6582
- tagByInstanceId.set(prop.instanceId, prop.value);
6583
- }
6584
- }
6692
+ const htmlTagsByInstanceId = getHtmlTagsFromProps(props);
6585
6693
  for (const instance2 of instances.values()) {
6586
- const propTag = tagByInstanceId.get(instance2.id);
6587
- const meta = componentMetas.get(instance2.component);
6588
- const metaTag = Object.keys((meta == null ? void 0 : meta.presetStyle) ?? {}).at(0);
6589
6694
  let componentTags = tagsByComponent.get(instance2.component);
6590
6695
  if (componentTags === void 0) {
6591
6696
  componentTags = /* @__PURE__ */ new Set();
6592
6697
  tagsByComponent.set(instance2.component, componentTags);
6593
6698
  }
6594
- const tag = instance2.tag ?? propTag ?? metaTag;
6699
+ const tag = getHtmlTagFromInstance({
6700
+ instance: instance2,
6701
+ metas: componentMetas,
6702
+ props,
6703
+ htmlTagsByInstanceId
6704
+ });
6595
6705
  if (tag) {
6596
6706
  componentTags.add(tag);
6597
6707
  }
@@ -7293,7 +7403,7 @@ var generateJsxChildren = ({
7293
7403
  usedDataSources,
7294
7404
  scope
7295
7405
  });
7296
- generatedChildren = `{${expression}}
7406
+ generatedChildren = `{renderText(${expression})}
7297
7407
  `;
7298
7408
  continue;
7299
7409
  }
@@ -7474,11 +7584,12 @@ var migratePages = (pages) => {
7474
7584
  if (isSerializedPages(pages) && pages.pages instanceof Map && pages.folders instanceof Map) {
7475
7585
  const currentPages = pages;
7476
7586
  const result = Pages.safeParse(currentPages);
7477
- if (result.success) {
7587
+ if (result.success && currentPages.pageTemplates !== void 0) {
7478
7588
  return currentPages;
7479
7589
  }
7480
7590
  return {
7481
7591
  ...currentPages,
7592
+ pageTemplates: currentPages.pageTemplates ?? /* @__PURE__ */ new Map(),
7482
7593
  folders: removeOrphanFolderChildren(
7483
7594
  currentPages.pages,
7484
7595
  currentPages.folders
@@ -7495,6 +7606,7 @@ var migratePages = (pages) => {
7495
7606
  homePageId: pages.homePageId,
7496
7607
  rootFolderId: pages.rootFolderId,
7497
7608
  pages: nextPages2,
7609
+ pageTemplates: pages.pageTemplates === void 0 ? /* @__PURE__ */ new Map() : toMap(pages.pageTemplates),
7498
7610
  folders: removeOrphanFolderChildren(nextPages2, nextFolders2)
7499
7611
  };
7500
7612
  }
@@ -7551,6 +7663,7 @@ var migratePages = (pages) => {
7551
7663
  homePageId: homePage.id,
7552
7664
  rootFolderId: rootFolder.id,
7553
7665
  pages: nextPages,
7666
+ pageTemplates: /* @__PURE__ */ new Map(),
7554
7667
  folders: nextFolders
7555
7668
  };
7556
7669
  };
@@ -8012,7 +8125,7 @@ const g$3 = {
8012
8125
  const t$f = {
8013
8126
  tag: { required: false, control: "text", type: "string" }
8014
8127
  };
8015
- const n$6 = {
8128
+ const n$7 = {
8016
8129
  icon: TextIcon,
8017
8130
  presetStyle: {
8018
8131
  div: [
@@ -8208,14 +8321,14 @@ const c$4 = {
8208
8321
  }
8209
8322
  };
8210
8323
  const o$v = {};
8211
- const r$d = {
8324
+ const r$c = {
8212
8325
  form: [
8213
8326
  ...form,
8214
8327
  { property: "min-height", value: { type: "unit", unit: "px", value: 20 } }
8215
8328
  ]
8216
8329
  }, p$4 = {
8217
8330
  label: "Form",
8218
- presetStyle: r$d,
8331
+ presetStyle: r$c,
8219
8332
  initialProps: ["id", "class", "action"],
8220
8333
  props: o$v
8221
8334
  };
@@ -8229,7 +8342,7 @@ const e$o = {
8229
8342
  },
8230
8343
  quality: { required: false, control: "number", type: "number" }
8231
8344
  };
8232
- const r$c = {
8345
+ const r$b = {
8233
8346
  img: [
8234
8347
  ...img,
8235
8348
  // Otherwise on new image insert onto canvas it can overfit screen size multiple times
@@ -8254,7 +8367,7 @@ const r$c = {
8254
8367
  }, i$7 = {
8255
8368
  category: "media",
8256
8369
  description: "Add an image asset to the page. Webstudio automatically converts images to WebP or AVIF format and makes them responsive for best performance.",
8257
- presetStyle: r$c,
8370
+ presetStyle: r$b,
8258
8371
  order: 0,
8259
8372
  initialProps: [
8260
8373
  "id",
@@ -8345,7 +8458,7 @@ const e$n = {
8345
8458
  value: { type: "rgb", r: 226, g: 226, b: 226, alpha: 1 }
8346
8459
  }
8347
8460
  ]
8348
- }, r$b = {
8461
+ }, r$a = {
8349
8462
  presetStyle: e$n,
8350
8463
  initialProps: ["id", "class", "cite"],
8351
8464
  props: o$u
@@ -8532,7 +8645,7 @@ const e$k = {
8532
8645
  description: "Value of the form control"
8533
8646
  }
8534
8647
  };
8535
- const r$a = {
8648
+ const r$9 = {
8536
8649
  input: [
8537
8650
  ...radio,
8538
8651
  {
@@ -8543,7 +8656,7 @@ const r$a = {
8543
8656
  }, m$7 = {
8544
8657
  label: "Radio",
8545
8658
  icon: RadioCheckedIcon,
8546
- presetStyle: r$a,
8659
+ presetStyle: r$9,
8547
8660
  initialProps: ["id", "class", "name", "value", "required", "checked"],
8548
8661
  props: e$k
8549
8662
  };
@@ -9307,15 +9420,22 @@ const e$d = {
9307
9420
  defaultValue: "medium",
9308
9421
  options: ["full", "long", "medium", "short", "none"]
9309
9422
  },
9423
+ datetime: {
9424
+ required: false,
9425
+ control: "text",
9426
+ type: "string",
9427
+ defaultValue: "dateTime attribute is not set",
9428
+ description: "Machine-readable value"
9429
+ },
9310
9430
  format: {
9311
9431
  description: `Custom format template. Overrides Date Style and Time Style.
9312
9432
 
9313
9433
  Tokens: YYYY/YY (year), MMMM/MMM/MM/M (month), DDDD/DDD/DD/D (day), HH/H (hours), mm/m (minutes), ss/s (seconds)
9314
9434
 
9315
9435
  Examples:
9316
- "YYYY-MM-DD" → 2025-11-03
9317
- "DDDD, MMMM D" → Monday, November 3
9318
- "DDD, D. MMM YYYY" → Mon, 3. Nov 2025
9436
+ - "YYYY-MM-DD" → 2025-11-03
9437
+ - "DDDD, MMMM D" → Monday, November 3
9438
+ - "DDD, D. MMM YYYY" → Mon, 3. Nov 2025
9319
9439
 
9320
9440
  Day and month names use the selected language.`,
9321
9441
  required: false,
@@ -9407,9 +9527,19 @@ Day and month names use the selected language.`,
9407
9527
  type: "string",
9408
9528
  defaultValue: "none",
9409
9529
  options: ["full", "long", "medium", "short", "none"]
9530
+ },
9531
+ timeZone: {
9532
+ description: `Time zone used to format the date.
9533
+
9534
+ Use "UTC" for deterministic UTC output, "visitor" to use the browser time
9535
+ zone after hydration, or an IANA time zone like "Europe/Berlin".`,
9536
+ required: false,
9537
+ control: "text",
9538
+ type: "string",
9539
+ defaultValue: "UTC"
9410
9540
  }
9411
9541
  };
9412
- const r$9 = {
9542
+ const n$6 = {
9413
9543
  category: "localization",
9414
9544
  description: "Converts machine-readable date and time to a human-readable format.",
9415
9545
  contentModel: {
@@ -9425,6 +9555,7 @@ const r$9 = {
9425
9555
  "country",
9426
9556
  "dateStyle",
9427
9557
  "timeStyle",
9558
+ "timeZone",
9428
9559
  "format"
9429
9560
  ],
9430
9561
  props: {
@@ -9451,6 +9582,15 @@ const r$9 = {
9451
9582
  ...e$d.timeStyle,
9452
9583
  contentMode: true
9453
9584
  },
9585
+ timeZone: {
9586
+ required: false,
9587
+ control: "timeZone",
9588
+ type: "string",
9589
+ defaultValue: "UTC",
9590
+ options: ["UTC", "visitor"],
9591
+ description: 'Timezone used to display the date. Use "visitor" to display each visitor’s browser timezone after the page loads, or select/type an IANA timezone like "Europe/Berlin".',
9592
+ contentMode: true
9593
+ },
9454
9594
  format: {
9455
9595
  ...e$d.format,
9456
9596
  contentMode: true
@@ -9605,7 +9745,7 @@ const n$5 = {
9605
9745
  };
9606
9746
  const baseComponentMetas = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({
9607
9747
  __proto__: null,
9608
- Blockquote: r$b,
9748
+ Blockquote: r$a,
9609
9749
  Body: i$9,
9610
9750
  Bold: p$6,
9611
9751
  Box: g$3,
@@ -9639,9 +9779,9 @@ const baseComponentMetas = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.
9639
9779
  Span: e$s,
9640
9780
  Subscript: s$3,
9641
9781
  Superscript: o$z,
9642
- Text: n$6,
9782
+ Text: n$7,
9643
9783
  Textarea: l$4,
9644
- Time: r$9,
9784
+ Time: n$6,
9645
9785
  Video: n$5,
9646
9786
  Vimeo: s$2,
9647
9787
  VimeoPlayButton: c$2,
@@ -10246,7 +10386,15 @@ const e$5 = {
10246
10386
  type: "string",
10247
10387
  description: "Current value of the element"
10248
10388
  }
10249
- }, t$2 = {}, r$3 = {}, n$2 = {};
10389
+ }, t$2 = {}, n$2 = {}, r$3 = {
10390
+ forceMount: {
10391
+ description: "Used to force mounting when more control is needed. Useful when controlling animation with React animation libraries or keeping content available in the DOM.",
10392
+ required: false,
10393
+ control: "boolean",
10394
+ type: "boolean",
10395
+ defaultValue: true
10396
+ }
10397
+ };
10250
10398
  const C$1 = {
10251
10399
  icon: AccordionIcon,
10252
10400
  contentModel: {
@@ -10313,7 +10461,7 @@ const C$1 = {
10313
10461
  presetStyle: {
10314
10462
  button: [button, b$2].flat()
10315
10463
  },
10316
- props: r$3
10464
+ props: n$2
10317
10465
  }, H = {
10318
10466
  label: "Item Content",
10319
10467
  icon: ContentIcon,
@@ -10328,7 +10476,7 @@ const C$1 = {
10328
10476
  presetStyle: {
10329
10477
  div
10330
10478
  },
10331
- props: n$2
10479
+ props: r$3
10332
10480
  };
10333
10481
  const e$4 = {
10334
10482
  defaultValue: { required: false, control: "text", type: "string" },
@@ -10927,7 +11075,7 @@ const createFramework$2 = async () => {
10927
11075
  "utf8"
10928
11076
  );
10929
11077
  await rm(routeTemplatesDir, { recursive: true, force: true });
10930
- const base = "@webstudio-is/sdk-components-react";
11078
+ const base = "@webstudio-is/sdk-components-react/components";
10931
11079
  const remix = "@webstudio-is/sdk-components-react-remix";
10932
11080
  const reactRadix = "@webstudio-is/sdk-components-react-radix";
10933
11081
  const animation = "@webstudio-is/sdk-components-animation";
@@ -11018,7 +11166,7 @@ const createFramework$1 = async () => {
11018
11166
  "utf8"
11019
11167
  );
11020
11168
  await rm(routeTemplatesDir, { recursive: true, force: true });
11021
- const base = "@webstudio-is/sdk-components-react";
11169
+ const base = "@webstudio-is/sdk-components-react/components";
11022
11170
  const reactRouter = "@webstudio-is/sdk-components-react-router";
11023
11171
  const reactRadix = "@webstudio-is/sdk-components-react-radix";
11024
11172
  const animation = "@webstudio-is/sdk-components-animation";
@@ -11107,7 +11255,7 @@ const createFramework = async () => {
11107
11255
  "utf8"
11108
11256
  );
11109
11257
  await rm(routeTemplatesDir, { recursive: true, force: true });
11110
- const base = "@webstudio-is/sdk-components-react";
11258
+ const base = "@webstudio-is/sdk-components-react/components";
11111
11259
  const reactRadix = "@webstudio-is/sdk-components-react-radix";
11112
11260
  const animation = "@webstudio-is/sdk-components-animation";
11113
11261
  const components = {};
@@ -11130,7 +11278,8 @@ const createFramework = async () => {
11130
11278
  tags: {
11131
11279
  textarea: `${base}:Textarea`,
11132
11280
  input: `${base}:Input`,
11133
- select: `${base}:Select`
11281
+ select: `${base}:Select`,
11282
+ a: `${base}:Link`
11134
11283
  },
11135
11284
  html: ({ pagePath }) => {
11136
11285
  if (isPathnamePattern(pagePath)) {
@@ -11568,7 +11717,7 @@ Please check webstudio --help for more details`
11568
11717
 
11569
11718
 
11570
11719
  import { Fragment, useState } from "react";
11571
- import { useResource, useVariableState } from "@webstudio-is/react-sdk/runtime";
11720
+ import { renderText, useResource, useVariableState } from "@webstudio-is/react-sdk/runtime";
11572
11721
  ${importsString}
11573
11722
 
11574
11723
  export const projectId = "${siteData.build.projectId}";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "webstudio",
3
- "version": "0.268.0",
3
+ "version": "0.269.0",
4
4
  "description": "Webstudio CLI",
5
5
  "author": "Webstudio <github@webstudio.is>",
6
6
  "homepage": "https://webstudio.is",
@@ -43,8 +43,8 @@
43
43
  "warn-once": "^0.1.1",
44
44
  "yargs": "^17.7.2",
45
45
  "zod": "^3.24.2",
46
- "@webstudio-is/trpc-interface": "0.268.0",
47
- "@webstudio-is/project-migrations": "0.268.0"
46
+ "@webstudio-is/project-migrations": "0.269.0",
47
+ "@webstudio-is/trpc-interface": "0.269.0"
48
48
  },
49
49
  "devDependencies": {
50
50
  "@cloudflare/vite-plugin": "^1.1.0",
@@ -74,18 +74,18 @@
74
74
  "vite": "^6.3.4",
75
75
  "vitest": "^3.1.2",
76
76
  "wrangler": "^3.63.2",
77
- "@webstudio-is/css-engine": "0.268.0",
78
- "@webstudio-is/http-client": "0.268.0",
79
- "@webstudio-is/image": "0.268.0",
80
- "@webstudio-is/react-sdk": "0.268.0",
81
- "@webstudio-is/sdk": "0.268.0",
82
- "@webstudio-is/sdk-components-animation": "0.268.0",
83
- "@webstudio-is/sdk-components-react-radix": "0.268.0",
84
- "@webstudio-is/sdk-components-react-remix": "0.268.0",
85
- "@webstudio-is/sdk-components-react-router": "0.268.0",
86
- "@webstudio-is/sdk-components-react": "0.268.0",
87
- "@webstudio-is/wsauth": "0.268.0",
88
- "@webstudio-is/tsconfig": "1.0.7"
77
+ "@webstudio-is/http-client": "0.269.0",
78
+ "@webstudio-is/image": "0.269.0",
79
+ "@webstudio-is/react-sdk": "0.269.0",
80
+ "@webstudio-is/sdk-components-animation": "0.269.0",
81
+ "@webstudio-is/css-engine": "0.269.0",
82
+ "@webstudio-is/sdk": "0.269.0",
83
+ "@webstudio-is/sdk-components-react": "0.269.0",
84
+ "@webstudio-is/sdk-components-react-radix": "0.269.0",
85
+ "@webstudio-is/sdk-components-react-remix": "0.269.0",
86
+ "@webstudio-is/sdk-components-react-router": "0.269.0",
87
+ "@webstudio-is/tsconfig": "1.0.7",
88
+ "@webstudio-is/wsauth": "0.269.0"
89
89
  },
90
90
  "scripts": {
91
91
  "typecheck": "tsgo --noEmit",
@@ -12,7 +12,7 @@
12
12
  "build-cf-types": "wrangler types"
13
13
  },
14
14
  "dependencies": {
15
- "@webstudio-is/wsauth": "0.268.0",
15
+ "@webstudio-is/wsauth": "0.269.0",
16
16
  "@remix-run/cloudflare": "2.16.5",
17
17
  "@remix-run/cloudflare-pages": "2.16.5"
18
18
  },
@@ -0,0 +1,21 @@
1
+ import { createPath, generatePath, parsePath } from "@remix-run/react";
2
+
3
+ /**
4
+ * Expands route params in local redirect targets.
5
+ * External and protocol-relative URLs are returned unchanged because route params
6
+ * only apply to app paths.
7
+ */
8
+ export const generateRedirectUrl = (
9
+ url: string,
10
+ params: Record<string, string | undefined>
11
+ ) => {
12
+ if (url.startsWith("/") === false || url.startsWith("//")) {
13
+ return url;
14
+ }
15
+
16
+ const path = parsePath(url);
17
+ return createPath({
18
+ ...path,
19
+ pathname: generatePath(path.pathname ?? "/", params),
20
+ });
21
+ };
@@ -1,3 +1,4 @@
1
+ import { type ComponentProps, memo, useMemo } from "react";
1
2
  import {
2
3
  type ServerRuntimeMetaFunction as MetaFunction,
3
4
  type LinksFunction,
@@ -328,20 +329,33 @@ export const action = async ({
328
329
  }
329
330
  };
330
331
 
332
+ const PageBoundary = memo(
333
+ ({ url, system }: ComponentProps<typeof Page> & { url: string }) => {
334
+ // Use the URL as the key to force scripts in HTML Embed to reload on dynamic pages
335
+ return <Page key={url} system={system} />;
336
+ },
337
+ // React Router can rerender the current route while the next route loaders are
338
+ // still pending. Keep the generated page out of that pending-navigation render
339
+ // path, but let URL changes remount it.
340
+ (prevProps, nextProps) => prevProps.url === nextProps.url
341
+ );
342
+
331
343
  const Outlet = () => {
332
344
  const { system, resources, url, pageMeta, host } =
333
345
  useLoaderData<typeof loader>();
346
+ const sdkContext = useMemo(
347
+ () => ({
348
+ ...constants,
349
+ resources,
350
+ breakpoints,
351
+ onError: console.error,
352
+ }),
353
+ [resources]
354
+ );
355
+
334
356
  return (
335
- <ReactSdkContext.Provider
336
- value={{
337
- ...constants,
338
- resources,
339
- breakpoints,
340
- onError: console.error,
341
- }}
342
- >
343
- {/* Use the URL as the key to force scripts in HTML Embed to reload on dynamic pages */}
344
- <Page key={url} system={system} />
357
+ <ReactSdkContext.Provider value={sdkContext}>
358
+ <PageBoundary url={url} system={system} />
345
359
  <PageSettingsMeta
346
360
  url={url}
347
361
  pageMeta={pageMeta}
@@ -1,6 +1,7 @@
1
- import { redirect } from "@remix-run/server-runtime";
1
+ import { type LoaderFunctionArgs, redirect } from "@remix-run/server-runtime";
2
+ import { generateRedirectUrl } from "../redirect-url";
2
3
  import { url, status } from "__REDIRECT__";
3
4
 
4
- export const loader = () => {
5
- return redirect(url, status);
5
+ export const loader = (arg: LoaderFunctionArgs) => {
6
+ return redirect(generateRedirectUrl(url, arg.params), status);
6
7
  };
@@ -11,14 +11,14 @@
11
11
  "@remix-run/node": "2.16.5",
12
12
  "@remix-run/react": "2.16.5",
13
13
  "@remix-run/server-runtime": "2.16.5",
14
- "@webstudio-is/image": "0.268.0",
15
- "@webstudio-is/react-sdk": "0.268.0",
16
- "@webstudio-is/sdk": "0.268.0",
17
- "@webstudio-is/sdk-components-react": "0.268.0",
18
- "@webstudio-is/sdk-components-animation": "0.268.0",
19
- "@webstudio-is/sdk-components-react-radix": "0.268.0",
20
- "@webstudio-is/sdk-components-react-remix": "0.268.0",
21
- "@webstudio-is/wsauth": "0.268.0",
14
+ "@webstudio-is/image": "0.269.0",
15
+ "@webstudio-is/react-sdk": "0.269.0",
16
+ "@webstudio-is/sdk": "0.269.0",
17
+ "@webstudio-is/sdk-components-react": "0.269.0",
18
+ "@webstudio-is/sdk-components-animation": "0.269.0",
19
+ "@webstudio-is/sdk-components-react-radix": "0.269.0",
20
+ "@webstudio-is/sdk-components-react-remix": "0.269.0",
21
+ "@webstudio-is/wsauth": "0.269.0",
22
22
  "isbot": "^5.1.25",
23
23
  "react": "18.3.0-canary-14898b6a9-20240318",
24
24
  "react-dom": "18.3.0-canary-14898b6a9-20240318"
@@ -0,0 +1,21 @@
1
+ import { createPath, generatePath, parsePath } from "react-router";
2
+
3
+ /**
4
+ * Expands route params in local redirect targets.
5
+ * External and protocol-relative URLs are returned unchanged because route params
6
+ * only apply to app paths.
7
+ */
8
+ export const generateRedirectUrl = (
9
+ url: string,
10
+ params: Record<string, string | undefined>
11
+ ) => {
12
+ if (url.startsWith("/") === false || url.startsWith("//")) {
13
+ return url;
14
+ }
15
+
16
+ const path = parsePath(url);
17
+ return createPath({
18
+ ...path,
19
+ pathname: generatePath(path.pathname ?? "/", params),
20
+ });
21
+ };
@@ -1,3 +1,4 @@
1
+ import { type ComponentProps, memo, useMemo } from "react";
1
2
  import {
2
3
  type MetaFunction,
3
4
  type LinksFunction,
@@ -327,20 +328,33 @@ export const action = async ({
327
328
  }
328
329
  };
329
330
 
331
+ const PageBoundary = memo(
332
+ ({ url, system }: ComponentProps<typeof Page> & { url: string }) => {
333
+ // Use the URL as the key to force scripts in HTML Embed to reload on dynamic pages
334
+ return <Page key={url} system={system} />;
335
+ },
336
+ // React Router can rerender the current route while the next route loaders are
337
+ // still pending. Keep the generated page out of that pending-navigation render
338
+ // path, but let URL changes remount it.
339
+ (prevProps, nextProps) => prevProps.url === nextProps.url
340
+ );
341
+
330
342
  const Outlet = () => {
331
343
  const { system, resources, url, pageMeta, host } =
332
344
  useLoaderData<typeof loader>();
345
+ const sdkContext = useMemo(
346
+ () => ({
347
+ ...constants,
348
+ resources,
349
+ breakpoints,
350
+ onError: console.error,
351
+ }),
352
+ [resources]
353
+ );
354
+
333
355
  return (
334
- <ReactSdkContext.Provider
335
- value={{
336
- ...constants,
337
- resources,
338
- breakpoints,
339
- onError: console.error,
340
- }}
341
- >
342
- {/* Use the URL as the key to force scripts in HTML Embed to reload on dynamic pages */}
343
- <Page key={url} system={system} />
356
+ <ReactSdkContext.Provider value={sdkContext}>
357
+ <PageBoundary url={url} system={system} />
344
358
  <PageSettingsMeta
345
359
  url={url}
346
360
  pageMeta={pageMeta}
@@ -1,6 +1,7 @@
1
- import { redirect } from "react-router";
1
+ import { type LoaderFunctionArgs, redirect } from "react-router";
2
+ import { generateRedirectUrl } from "../redirect-url";
2
3
  import { url, status } from "__REDIRECT__";
3
4
 
4
- export const loader = () => {
5
- throw redirect(url, status);
5
+ export const loader = (arg: LoaderFunctionArgs) => {
6
+ throw redirect(generateRedirectUrl(url, arg.params), status);
6
7
  };
@@ -10,14 +10,14 @@
10
10
  "dependencies": {
11
11
  "@react-router/dev": "^7.5.3",
12
12
  "@react-router/fs-routes": "^7.5.3",
13
- "@webstudio-is/image": "0.268.0",
14
- "@webstudio-is/react-sdk": "0.268.0",
15
- "@webstudio-is/sdk": "0.268.0",
16
- "@webstudio-is/sdk-components-animation": "0.268.0",
17
- "@webstudio-is/sdk-components-react-radix": "0.268.0",
18
- "@webstudio-is/sdk-components-react-router": "0.268.0",
19
- "@webstudio-is/sdk-components-react": "0.268.0",
20
- "@webstudio-is/wsauth": "0.268.0",
13
+ "@webstudio-is/image": "0.269.0",
14
+ "@webstudio-is/react-sdk": "0.269.0",
15
+ "@webstudio-is/sdk": "0.269.0",
16
+ "@webstudio-is/sdk-components-animation": "0.269.0",
17
+ "@webstudio-is/sdk-components-react-radix": "0.269.0",
18
+ "@webstudio-is/sdk-components-react-router": "0.269.0",
19
+ "@webstudio-is/sdk-components-react": "0.269.0",
20
+ "@webstudio-is/wsauth": "0.269.0",
21
21
  "isbot": "^5.1.25",
22
22
  "react": "18.3.0-canary-14898b6a9-20240318",
23
23
  "react-dom": "18.3.0-canary-14898b6a9-20240318",
@@ -7,7 +7,7 @@
7
7
  },
8
8
  "dependencies": {
9
9
  "@cloudflare/vite-plugin": "^1.1.0",
10
- "@webstudio-is/wsauth": "0.268.0",
10
+ "@webstudio-is/wsauth": "0.269.0",
11
11
  "wrangler": "^4.14.1"
12
12
  }
13
13
  }
@@ -1,26 +1,49 @@
1
+ import { type ComponentProps, memo, useMemo } from "react";
1
2
  import type { PageContext } from "vike/types";
2
3
  import {
3
4
  PageSettingsMeta,
4
5
  PageSettingsTitle,
5
6
  ReactSdkContext,
6
7
  } from "@webstudio-is/react-sdk/runtime";
8
+ import { LinkCurrentUrlContext } from "@webstudio-is/sdk-components-react";
7
9
  import { assetBaseUrl, imageLoader } from "__CONSTANTS__";
8
10
  import { Page, breakpoints, siteName } from "__CLIENT__";
9
11
 
12
+ const getPageKey = (url: string) => {
13
+ const { origin, pathname, search } = new URL(url);
14
+ return `${origin}${pathname}${search}`;
15
+ };
16
+
17
+ const PageBoundary = memo(
18
+ ({ pageKey, system }: ComponentProps<typeof Page> & { pageKey: string }) => {
19
+ // Use the URL as the key to force scripts in HTML Embed to reload on dynamic pages
20
+ return <Page key={pageKey} system={system} />;
21
+ },
22
+ // Vike can rerender the current page during client-side navigation and
23
+ // hash-only URL updates. Keep the generated page out of that render path,
24
+ // but let actual page URL changes remount it.
25
+ (prevProps, nextProps) => prevProps.pageKey === nextProps.pageKey
26
+ );
27
+
10
28
  const PageComponent = ({ data }: { data: PageContext["data"] }) => {
11
29
  const { system, resources, url, pageMeta } = data;
30
+ const pageKey = getPageKey(url);
31
+ const sdkContext = useMemo(
32
+ () => ({
33
+ imageLoader,
34
+ assetBaseUrl,
35
+ resources,
36
+ breakpoints,
37
+ onError: console.error,
38
+ }),
39
+ [resources]
40
+ );
41
+
12
42
  return (
13
- <ReactSdkContext.Provider
14
- value={{
15
- imageLoader,
16
- assetBaseUrl,
17
- resources,
18
- breakpoints,
19
- onError: console.error,
20
- }}
21
- >
22
- {/* Use the URL as the key to force scripts in HTML Embed to reload on dynamic pages */}
23
- <Page key={url} system={system} />
43
+ <ReactSdkContext.Provider value={sdkContext}>
44
+ <LinkCurrentUrlContext.Provider value={url}>
45
+ <PageBoundary pageKey={pageKey} system={system} />
46
+ </LinkCurrentUrlContext.Provider>
24
47
  <PageSettingsMeta
25
48
  url={url}
26
49
  pageMeta={pageMeta}
@@ -8,12 +8,12 @@
8
8
  "typecheck": "tsgo --noEmit"
9
9
  },
10
10
  "dependencies": {
11
- "@webstudio-is/image": "0.268.0",
12
- "@webstudio-is/react-sdk": "0.268.0",
13
- "@webstudio-is/sdk": "0.268.0",
14
- "@webstudio-is/sdk-components-react": "0.268.0",
15
- "@webstudio-is/sdk-components-animation": "0.268.0",
16
- "@webstudio-is/sdk-components-react-radix": "0.268.0",
11
+ "@webstudio-is/image": "0.269.0",
12
+ "@webstudio-is/react-sdk": "0.269.0",
13
+ "@webstudio-is/sdk": "0.269.0",
14
+ "@webstudio-is/sdk-components-react": "0.269.0",
15
+ "@webstudio-is/sdk-components-animation": "0.269.0",
16
+ "@webstudio-is/sdk-components-react-radix": "0.269.0",
17
17
  "react": "18.3.0-canary-14898b6a9-20240318",
18
18
  "react-dom": "18.3.0-canary-14898b6a9-20240318",
19
19
  "vike": "^0.4.229"
@@ -1,4 +1,4 @@
1
- import { type Root, createRoot } from "react-dom/client";
1
+ import { type Root, hydrateRoot } from "react-dom/client";
2
2
  import type { OnRenderClientSync } from "vike/types";
3
3
 
4
4
  let root: Root;
@@ -19,7 +19,8 @@ export const onRenderClient: OnRenderClientSync = (pageContext) => {
19
19
  </>
20
20
  );
21
21
  if (root === undefined) {
22
- root = createRoot(document.documentElement);
22
+ root = hydrateRoot(document.documentElement, htmlContent);
23
+ return;
23
24
  }
24
25
  document.documentElement.lang = lang;
25
26
  root.render(htmlContent);
Binary file