camox 0.46.0 → 0.48.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 (82) hide show
  1. package/dist/core/createApp.d.ts +19 -6
  2. package/dist/core/createApp.js +7 -2
  3. package/dist/core/createBlock.js +54 -43
  4. package/dist/core/createCollection.d.ts +25 -7
  5. package/dist/core/createCollection.js +28 -9
  6. package/dist/core/createLayout.d.ts +98 -71
  7. package/dist/core/createLayout.js +10 -16
  8. package/dist/core/derivedRoutes.js +1 -1
  9. package/dist/core/editing/BlockEditingRuntime.d.ts +3 -2
  10. package/dist/core/editing/CompleteBlockEditingRuntime.js +2 -1
  11. package/dist/core/editing/createEditableBlock.d.ts +59 -19
  12. package/dist/core/editing/createEditableBlock.js +162 -151
  13. package/dist/core/editing/useLatestRecord.js +7 -7
  14. package/dist/core/layoutContext.d.ts +54 -0
  15. package/dist/core/layoutContext.js +66 -0
  16. package/dist/core/lib/contentType.d.ts +91 -4
  17. package/dist/core/lib/contentType.js +84 -7
  18. package/dist/core/lib/recordScope.js +19 -17
  19. package/dist/core/lib/reference.d.ts +6 -2
  20. package/dist/core/recordItem.js +55 -2
  21. package/dist/core/requestContext.d.ts +18 -0
  22. package/dist/core/structuredData.d.ts +126 -0
  23. package/dist/core/structuredData.js +40 -0
  24. package/dist/features/canvas/canvasCamera.js +21 -6
  25. package/dist/features/canvas/sidebar/CanvasSidebarFrame.js +1 -1
  26. package/dist/features/canvas/useCanvasCamera.js +16 -11
  27. package/dist/features/content/ContentCollection.js +13 -7
  28. package/dist/features/content/collection-form.js +6 -2
  29. package/dist/features/content/components/MissingFromLocale.js +1 -1
  30. package/dist/features/og/pageImage.js +89 -0
  31. package/dist/features/page/CuratedPageContent.js +45 -32
  32. package/dist/features/page/DerivedPageContent.js +49 -22
  33. package/dist/features/page/SharedLayoutContent.js +26 -22
  34. package/dist/features/preview/CamoxPreview.js +176 -145
  35. package/dist/features/preview/components/AddBlockSheet.js +1 -0
  36. package/dist/features/preview/components/BlockActionsPopover.js +8 -6
  37. package/dist/features/preview/components/PageInfo.js +244 -264
  38. package/dist/features/preview/components/PageTree.js +63 -53
  39. package/dist/features/preview/components/PreviewToolbar.js +74 -85
  40. package/dist/features/preview/components/RecordCard.js +50 -34
  41. package/dist/features/preview/components/RecordCombobox.js +12 -5
  42. package/dist/features/preview/components/ReferenceFieldEditor.js +4 -2
  43. package/dist/features/preview/components/ReferenceListFieldEditor.js +24 -19
  44. package/dist/features/preview/components/RepeatableItemsList.js +62 -46
  45. package/dist/features/preview/components/SelectionEditor.js +3 -2
  46. package/dist/features/preview/components/useFieldWriter.js +2 -4
  47. package/dist/features/preview/components/useRecordView.js +2 -2
  48. package/dist/features/preview/entityName.js +11 -0
  49. package/dist/features/preview/pageRecord.js +17 -18
  50. package/dist/features/provider/CamoxProvider.js +8 -16
  51. package/dist/features/provider/CoreCamoxProvider.js +1 -6
  52. package/dist/features/provider/signInShortcut.js +60 -0
  53. package/dist/features/routes/pageRuntime.d.ts +15 -1
  54. package/dist/features/routes/pageRuntime.js +35 -38
  55. package/dist/features/runtime/RuntimeChrome.js +2 -2
  56. package/dist/features/runtime/breadcrumbs.js +68 -0
  57. package/dist/features/runtime/livePages.js +3 -2
  58. package/dist/features/runtime/pageApp.js +11 -39
  59. package/dist/features/runtime/pageGraph.js +92 -0
  60. package/dist/features/runtime/projectLogo.js +110 -0
  61. package/dist/features/runtime/runtime.d.ts +11 -6
  62. package/dist/features/runtime/runtime.js +278 -55
  63. package/dist/features/runtime/runtimeRoutes.d.ts +1 -0
  64. package/dist/features/runtime/runtimeRoutes.js +1 -0
  65. package/dist/features/runtime/structuredData.js +213 -0
  66. package/dist/features/studio/components/ChatSidebar.js +5 -5
  67. package/dist/features/studio/components/ProjectMenu.js +127 -100
  68. package/dist/features/studio/components/SocialProfilesDialog.js +342 -0
  69. package/dist/features/studio/components/ThreadMessages.js +2 -2
  70. package/dist/features/vite/blockBoilerplate.js +3 -2
  71. package/dist/features/vite/layoutGeneration.js +3 -2
  72. package/dist/features/vite/vite.js +1 -0
  73. package/dist/lib/queries.d.ts +4 -0
  74. package/dist/lib/queries.js +1 -0
  75. package/dist/lib/studio-reads.js +3 -1
  76. package/dist/package.js +1 -1
  77. package/dist/studio.css +1 -1
  78. package/package.json +5 -4
  79. package/skills/camox/references/block-definitions.md +105 -25
  80. package/skills/camox/references/layout-definitions.md +53 -17
  81. package/skills/camox/references/routed-layouts.md +17 -14
  82. package/dist/features/provider/LocalhostPreviewProvider.js +0 -158
@@ -1,3 +1,4 @@
1
+ import { BlockStructuredData, KnownNodesOnly, RepeatedNodes } from "../structuredData.js";
1
2
  import { Collection } from "../createCollection.js";
2
3
  import { IconId, IconValue } from "./iconTypes.js";
3
4
  import { SystemOrderKey } from "@camox/api-contract";
@@ -61,7 +62,75 @@ export type ContentProxy<TShape extends Record<string, TSchema>> = { [K in keyof
61
62
  */
62
63
  type SettingCallable<T extends TSchema> = Static<T> extends boolean ? (lines: ConditionalLines) => Conditional : Static<T> extends string ? (value: Static<T>, lines: ConditionalLines) => Conditional : never;
63
64
  export type SettingsProxy<TShape extends Record<string, TSchema>> = { [K in keyof TShape & string]: SettingCallable<TShape[K]>; };
64
- export type ToMarkdownBuilder<TContent extends Record<string, TSchema>, TSettings extends Record<string, TSchema> = Record<string, never>> = (c: ContentProxy<TContent>, s: SettingsProxy<TSettings>) => ReadonlyArray<string | FieldToken | Conditional>;
65
+ /** What a block's or repeatable item's templates receive: proxies of its content and settings. */
66
+ export interface TemplateContext<TContent extends Record<string, TSchema>, TSettings extends Record<string, TSchema> = Record<string, never>> {
67
+ content: ContentProxy<TContent>;
68
+ settings: SettingsProxy<TSettings>;
69
+ }
70
+ /** Builds a Markdown template from `{ content, settings }`. */
71
+ export type ToMarkdownBuilder<TContent extends Record<string, TSchema>, TSettings extends Record<string, TSchema> = Record<string, never>> = (context: TemplateContext<TContent, TSettings>) => ReadonlyArray<string | FieldToken | Conditional>;
72
+ /** A collection's String fields as field tokens: what its record templates can name. */
73
+ export type RecordTextProxy<TShape extends Record<string, TSchema>> = { [K in keyof TShape & string as TShape[K] extends {
74
+ fieldType: "String";
75
+ } ? K : never]: string & FieldToken; };
76
+ /** What a collection's `toTitle` and `toDescription` receive: the record's String fields. */
77
+ export interface RecordTemplateContext<TShape extends Record<string, TSchema>> {
78
+ content: RecordTextProxy<TShape>;
79
+ }
80
+ /**
81
+ * Builds a title template from `{ content, settings }`: one line naming a block or repeatable item
82
+ * until it has a Summary, such as its headline. The API evaluates it on default-locale content.
83
+ */
84
+ export type ToTitleBuilder<TContent extends Record<string, TSchema>, TSettings extends Record<string, TSchema> = Record<string, never>> = (context: TemplateContext<TContent, TSettings>) => string | FieldToken;
85
+ /**
86
+ * An image field's token in a Structured Data template, which the API evaluates into an
87
+ * `ImageObject` with its absolute URL, dimensions and alt text.
88
+ */
89
+ export type ImageToken = {
90
+ "@type": "ImageObject";
91
+ url: string;
92
+ contentUrl: string;
93
+ };
94
+ /**
95
+ * A reference's token in a Structured Data template: a link to the record's node, which Camox
96
+ * adds to the page's graph, or the record's title when its collection has no `toStructuredData`.
97
+ */
98
+ export type RecordToken = {
99
+ readonly "@id": string;
100
+ };
101
+ /**
102
+ * What a field reads as in a Structured Data template. Text, files, links and embeds read as
103
+ * strings, links and files as absolute URLs. References link their records' nodes, and their
104
+ * fields read as the record's. Icons, artifacts and lists of files aren't Structured Data.
105
+ */
106
+ type StructuredDataToken<T extends TSchema> = T extends ReferenceSchema<infer R> ? RecordToken & StructuredDataProxy<R> : T extends ReferenceListSchema<any> ? RecordToken[] : T extends {
107
+ fieldType: "Artifact";
108
+ } ? never : T extends TArray<TObject<any>> ? RepeatedNodes : T extends TArray<any> ? never : Static<T> extends IconValue ? never : Static<T> extends ImageValue ? ImageToken : Static<T> extends string | LinkValue | FileValue ? string & FieldToken : never;
109
+ /** A block's or repeatable item's fields as a Structured Data template reads them. */
110
+ export type StructuredDataProxy<TShape extends Record<string, TSchema>> = { [K in keyof TShape & string as [StructuredDataToken<TShape[K]>] extends [never] ? never : K]: StructuredDataToken<TShape[K]>; };
111
+ /**
112
+ * One setting in a Structured Data template: shows a part of the template only while it holds,
113
+ * typed as the part it shows. A Boolean takes the part; an Enum takes the option it must equal,
114
+ * then the part. Settings of other shapes aren't conditions.
115
+ */
116
+ type StructuredDataSettingCallable<T extends TSchema> = Static<T> extends boolean ? <const V>(value: V) => V : Static<T> extends string ? <const V>(equals: Static<T>, value: V) => V : never;
117
+ /** A block's or repeatable item's settings, as its Structured Data template reads them. */
118
+ export type StructuredDataSettingsProxy<TShape extends Record<string, TSchema>> = { [K in keyof TShape & string]: StructuredDataSettingCallable<TShape[K]>; };
119
+ /** What a block's or repeatable item's `toStructuredData` receives. */
120
+ export interface StructuredDataContext<TContent extends Record<string, TSchema>, TSettings extends Record<string, TSchema> = Record<string, never>> {
121
+ content: StructuredDataProxy<TContent>;
122
+ settings: StructuredDataSettingsProxy<TSettings>;
123
+ }
124
+ /**
125
+ * Builds a block's or repeatable item's Structured Data template from `{ content, settings }`:
126
+ * the nodes it shows. A repeater's token places its items' nodes. The API evaluates the
127
+ * template on each page's content, and Camox links each node to its page.
128
+ */
129
+ export type ToStructuredDataBuilder<TContent extends Record<string, TSchema>, TSettings extends Record<string, TSchema>, Nodes extends BlockStructuredData> = (context: StructuredDataContext<TContent, TSettings>) => Nodes & KnownNodesOnly<Nodes>;
130
+ /** What a collection's `toStructuredData` receives: the record's fields as tokens. */
131
+ export interface RecordStructuredDataContext<TShape extends Record<string, TSchema>> {
132
+ content: StructuredDataProxy<TShape>;
133
+ }
65
134
  declare const EmbedURLBrand: unique symbol;
66
135
  export type EmbedURL = string & {
67
136
  readonly [EmbedURLBrand]: true;
@@ -269,7 +338,7 @@ export interface ContentFieldBuilder {
269
338
  * cannot pick, remove or reorder records, and nothing is stored on the block.
270
339
  *
271
340
  * @example
272
- * field.referenceList(customers, { maxItems: 6, toMarkdown: (c) => [c.name] })
341
+ * field.referenceList(customers, { maxItems: 6, toMarkdown: ({ content }) => [content.name] })
273
342
  * field.referenceList(articles, { query: { orderBy: { publishedAt: "desc" }, limit: 3 } })
274
343
  */
275
344
  referenceList: <T extends Record<string, TSchema>, const L extends number = number>(collection: Collection<T>, options?: {
@@ -295,16 +364,34 @@ export interface ContentFieldBuilder {
295
364
  * minItems: 1,
296
365
  * maxItems: 10,
297
366
  * title: "Items",
298
- * toMarkdown: (c, s) => [`### ${c.title}`, s.highlighted(c.description)],
367
+ * toTitle: ({ content }) => content.title,
368
+ * toMarkdown: ({ content, settings }) => [
369
+ * `### ${content.title}`,
370
+ * settings.highlighted(content.description),
371
+ * ],
299
372
  * })
300
373
  */
301
- repeater: <T extends Record<string, TSchema>, S extends Record<string, TSchema> = Record<string, never>>(options: {
374
+ repeater: <T extends Record<string, TSchema>, S extends Record<string, TSchema> = Record<string, never>, N extends BlockStructuredData = BlockStructuredData>(options: {
302
375
  content: (field: ContentFieldBuilder) => T & NoInfer<ArtifactDataCheck<T, S>>;
303
376
  settings?: (setting: SettingBuilder) => S;
304
377
  minItems: number;
305
378
  maxItems: number;
306
379
  title?: string;
380
+ /** Names each item until it has a Summary, such as its title. See `ToTitleBuilder`. */
381
+ toTitle: ToTitleBuilder<T, S>;
307
382
  toMarkdown: ToMarkdownBuilder<T, S>;
383
+ /**
384
+ * The nodes each item shows, placed wherever the parent's template places the repeater's
385
+ * token. See `ToStructuredDataBuilder`.
386
+ *
387
+ * @example
388
+ * toStructuredData: ({ content }) => ({
389
+ * "@type": "Question",
390
+ * name: content.question,
391
+ * acceptedAnswer: { "@type": "Answer", text: content.answer },
392
+ * })
393
+ */
394
+ toStructuredData?: ToStructuredDataBuilder<T, S, N>;
308
395
  }) => RepeaterField<T, S>;
309
396
  }
310
397
  /** Content fields that can't be Artifact Data: artifact sources, repeaters and references. */
@@ -48,10 +48,21 @@ function createContentProxy(shape, prefix = "") {
48
48
  if (typeof prop !== "string") return void 0;
49
49
  const path = prefix ? `${prefix}.${prop}` : prop;
50
50
  const field = shape?.[prop];
51
- if (field?.fieldType === "Reference") return createContentProxy(field.referenceSchema.properties, path);
51
+ if (field?.fieldType === "Reference") return referenceToken(field.referenceSchema.properties, path);
52
52
  return new FieldToken(path);
53
53
  } });
54
54
  }
55
+ /**
56
+ * A reference field's token, which names the reference itself, as Structured Data templates
57
+ * link records by it, and whose properties are the tokens of the record's fields.
58
+ */
59
+ function referenceToken(shape, path) {
60
+ const fields = createContentProxy(shape, path);
61
+ return new Proxy(new FieldToken(path), { get(target, prop, receiver) {
62
+ if (typeof prop !== "string" || prop in target) return Reflect.get(target, prop, receiver);
63
+ return fields[prop];
64
+ } });
65
+ }
55
66
  function createSettingsProxy(settingsShape, scope) {
56
67
  return new Proxy({}, { get(_target, prop) {
57
68
  if (typeof prop !== "string") return void 0;
@@ -69,12 +80,78 @@ function serializeConditional(cond) {
69
80
  return out;
70
81
  }
71
82
  function resolveToMarkdown(builder, settingsShape, scope, contentShape) {
72
- const entries = builder(createContentProxy(contentShape), createSettingsProxy(settingsShape, scope));
83
+ const entries = builder({
84
+ content: createContentProxy(contentShape),
85
+ settings: createSettingsProxy(settingsShape, scope)
86
+ });
73
87
  const out = [];
74
88
  for (const entry of entries) if (entry instanceof Conditional) out.push(...serializeConditional(entry));
75
89
  else out.push(entry instanceof FieldToken ? entry.toString() : String(entry));
76
90
  return out;
77
91
  }
92
+ /**
93
+ * Runs a collection's record template, such as `toTitle`, into the token template the API fills
94
+ * with each record's plain text. Null when it returns nothing.
95
+ */
96
+ function resolveRecordTemplate(template, contentShape) {
97
+ const result = template({ content: createContentProxy(contentShape) });
98
+ return result === void 0 || result === null || String(result) === "" ? null : String(result);
99
+ }
100
+ /** The one-line title template a `toTitle` builder declares. */
101
+ function resolveToTitle(builder, settingsShape, scope, contentShape) {
102
+ const content = createContentProxy(contentShape);
103
+ const settings = createSettingsProxy(settingsShape, scope);
104
+ return String(builder({
105
+ content,
106
+ settings
107
+ }));
108
+ }
109
+ /** A part of a Structured Data template shown only while a setting holds. */
110
+ var SettingCondition = class {
111
+ scope;
112
+ settingName;
113
+ equals;
114
+ show;
115
+ constructor(scope, settingName, equals, show) {
116
+ this.scope = scope;
117
+ this.settingName = settingName;
118
+ this.equals = equals;
119
+ this.show = show;
120
+ }
121
+ };
122
+ function createStructuredDataSettingsProxy(settingsShape, scope) {
123
+ return new Proxy({}, { get(_target, prop) {
124
+ if (typeof prop !== "string") return void 0;
125
+ const fieldType = (settingsShape?.[prop])?.fieldType;
126
+ if (fieldType === "Boolean") return (value) => new SettingCondition(scope, prop, null, value);
127
+ if (fieldType === "Enum") return (equals, value) => new SettingCondition(scope, prop, equals, value);
128
+ throw new Error(`toStructuredData settings proxy: "${prop}" is not a Boolean or Enum setting on this ${scope}.`);
129
+ } });
130
+ }
131
+ /** A template's value as the Definition Set syncs it: JSON, with tokens as `{{field}}`. */
132
+ function serializeTemplate(value) {
133
+ if (value instanceof FieldToken) return value.toString();
134
+ if (value instanceof SettingCondition) return {
135
+ "@camox": "if",
136
+ setting: `${value.scope === "block" ? "settings" : "itemSettings"}.${value.settingName}`,
137
+ ...value.equals !== null && { equals: value.equals },
138
+ show: serializeTemplate(value.show)
139
+ };
140
+ if (Array.isArray(value)) return value.map(serializeTemplate);
141
+ if (typeof value === "object" && value !== null) return Object.fromEntries(Object.entries(value).flatMap(([key, entry]) => entry === void 0 ? [] : [[key, serializeTemplate(entry)]]));
142
+ return value;
143
+ }
144
+ /** The Structured Data template a `toStructuredData` builder declares. */
145
+ function resolveToStructuredData(builder, settingsShape, scope, contentShape) {
146
+ return serializeTemplate(builder({
147
+ content: createContentProxy(contentShape),
148
+ settings: createStructuredDataSettingsProxy(settingsShape, scope)
149
+ }));
150
+ }
151
+ /** The Structured Data template of each record of a collection, as its builder declares it. */
152
+ function resolveRecordStructuredData(builder, contentShape) {
153
+ return serializeTemplate(builder({ content: createContentProxy(contentShape) }));
154
+ }
78
155
  /** The longest artifact source content writes accept (256 KB). */
79
156
  const ARTIFACT_MAX_LENGTH = 262144;
80
157
  const IMAGE_PLACEHOLDER = Symbol("camox.imagePlaceholder");
@@ -259,8 +336,7 @@ const contentFieldBuilder = {
259
336
  required: options.required ?? false,
260
337
  title: options.title ?? collection._internal.title,
261
338
  default: null,
262
- referenceSchema: collection._internal.contentSchema,
263
- labelField: collection._internal.label
339
+ referenceSchema: collection._internal.contentSchema
264
340
  }),
265
341
  referenceList: (collection, options = {}) => Type.Unsafe({
266
342
  type: "array",
@@ -275,8 +351,7 @@ const contentFieldBuilder = {
275
351
  ...options.maxItems === void 0 ? {} : { maxItems: options.maxItems },
276
352
  ...options.query === void 0 ? { default: [] } : { query: validateQuery(options.query, collection) },
277
353
  ...options.toMarkdown === void 0 ? {} : { toMarkdown: resolveToMarkdown(options.toMarkdown, void 0, "item", collection._internal.contentSchema.properties) },
278
- referenceSchema: collection._internal.contentSchema,
279
- labelField: collection._internal.label
354
+ referenceSchema: collection._internal.contentSchema
280
355
  }),
281
356
  repeater: (options) => {
282
357
  if (options.minItems < 1) throw new Error("Repeater requires minItems to be at least 1");
@@ -301,7 +376,9 @@ const contentFieldBuilder = {
301
376
  default: defaultArray,
302
377
  title: options.title,
303
378
  fieldType: "Repeater",
379
+ toTitle: resolveToTitle(options.toTitle, settings, "item", content),
304
380
  toMarkdown: resolveToMarkdown(options.toMarkdown, settings, "item", content),
381
+ ...options.toStructuredData && { toStructuredData: resolveToStructuredData(options.toStructuredData, settings, "item", content) },
305
382
  itemSettingsSchema,
306
383
  defaultItemSettings: settingsObjectSchema ? collectSchemaDefaults(settingsObjectSchema.properties) : void 0
307
384
  });
@@ -344,4 +421,4 @@ function resolveBlockFields(options) {
344
421
  }
345
422
 
346
423
  //#endregion
347
- export { ARTIFACT_MAX_LENGTH, Conditional, FieldToken, assertFieldKinds, collectionField, collectionFieldBuilder, contentFieldBuilder, getImagePlaceholderTitle, isImagePlaceholder, resolveBlockFields, resolveToMarkdown, settingBuilder };
424
+ export { ARTIFACT_MAX_LENGTH, Conditional, FieldToken, assertFieldKinds, collectionField, collectionFieldBuilder, contentFieldBuilder, getImagePlaceholderTitle, isImagePlaceholder, resolveBlockFields, resolveRecordStructuredData, resolveRecordTemplate, resolveToMarkdown, resolveToStructuredData, resolveToTitle, settingBuilder };
@@ -250,7 +250,7 @@ const ReferenceList = (t0) => {
250
250
  * (the second hop), which link nothing further.
251
251
  */
252
252
  function RecordScope(t0) {
253
- const $ = c(14);
253
+ const $ = c(15);
254
254
  const { record, properties, hop, children } = t0;
255
255
  let t1;
256
256
  if ($[0] !== properties || $[1] !== record.content) {
@@ -265,10 +265,11 @@ function RecordScope(t0) {
265
265
  const value = t1;
266
266
  const t2 = hop === 1 ? record.references : void 0;
267
267
  let t3;
268
- if ($[3] !== children || $[4] !== hop || $[5] !== record.id || $[6] !== record.label) {
268
+ if ($[3] !== children || $[4] !== hop || $[5] !== record.description || $[6] !== record.id || $[7] !== record.title) {
269
269
  t3 = children({
270
270
  id: record.id,
271
- label: record.label,
271
+ title: record.title,
272
+ description: record.description,
272
273
  Field,
273
274
  Image,
274
275
  File,
@@ -282,30 +283,31 @@ function RecordScope(t0) {
282
283
  });
283
284
  $[3] = children;
284
285
  $[4] = hop;
285
- $[5] = record.id;
286
- $[6] = record.label;
287
- $[7] = t3;
288
- } else t3 = $[7];
286
+ $[5] = record.description;
287
+ $[6] = record.id;
288
+ $[7] = record.title;
289
+ $[8] = t3;
290
+ } else t3 = $[8];
289
291
  let t4;
290
- if ($[8] !== t2 || $[9] !== t3) {
292
+ if ($[9] !== t2 || $[10] !== t3) {
291
293
  t4 = /* @__PURE__ */ jsx(LinksContext.Provider, {
292
294
  value: t2,
293
295
  children: t3
294
296
  });
295
- $[8] = t2;
296
- $[9] = t3;
297
- $[10] = t4;
298
- } else t4 = $[10];
297
+ $[9] = t2;
298
+ $[10] = t3;
299
+ $[11] = t4;
300
+ } else t4 = $[11];
299
301
  let t5;
300
- if ($[11] !== t4 || $[12] !== value) {
302
+ if ($[12] !== t4 || $[13] !== value) {
301
303
  t5 = /* @__PURE__ */ jsx(RecordContext.Provider, {
302
304
  value,
303
305
  children: t4
304
306
  });
305
- $[11] = t4;
306
- $[12] = value;
307
- $[13] = t5;
308
- } else t5 = $[13];
307
+ $[12] = t4;
308
+ $[13] = value;
309
+ $[14] = t5;
310
+ } else t5 = $[14];
309
311
  return t5;
310
312
  }
311
313
  function _temp(location) {
@@ -29,7 +29,10 @@ type LinkedRecords<T extends Record<string, TSchema>> = {
29
29
  };
30
30
  type RecordFields<T extends Record<string, TSchema>> = {
31
31
  id: string;
32
- label: string;
32
+ /** The record's title, from its collection's `toTitle`. */
33
+ title: string;
34
+ /** The record's description, from its collection's `toDescription`, or null for none. */
35
+ description: string | null;
33
36
  Field: (props: InlineTextStyles & {
34
37
  name: TextKeys<T>;
35
38
  children: (props: React.HTMLAttributes<HTMLElement> & {
@@ -70,7 +73,8 @@ export type ReferenceContent<T extends TSchema> = T extends ReferenceSchema<infe
70
73
  export type ReferenceRecord = {
71
74
  id: string;
72
75
  collectionId: string;
73
- label: string;
76
+ title: string;
77
+ description: string | null;
74
78
  content: Record<string, unknown>;
75
79
  /** When its content last changed, for a record page's record. */
76
80
  updatedAt?: number;
@@ -1,5 +1,6 @@
1
1
  import { resolveToMarkdown } from "./lib/contentType.js";
2
2
  import { isLexicalState, lexicalStateToPlainText } from "./lib/lexicalState.js";
3
+ import { recordLink, recordText, templateFields } from "@camox/api-contract";
3
4
 
4
5
  //#region src/core/recordItem.ts
5
6
  /**
@@ -17,6 +18,58 @@ function recordPlainText(collection, content) {
17
18
  for (const [name, field] of Object.entries(properties)) if (field.fieldType === "String") text[name] = fieldPlainText(content[name]);
18
19
  return text;
19
20
  }
21
+ /**
22
+ * A record's references as links to their records' nodes: one for a reference, which is absent
23
+ * when it resolves to nothing, and a list for a reference list.
24
+ */
25
+ function recordLinks(collection, references) {
26
+ const properties = collection._internal.contentSchema.properties;
27
+ const links = {};
28
+ for (const [name, field] of Object.entries(properties)) {
29
+ const resolved = references?.[name];
30
+ if (field.fieldType === "ReferenceList") links[name] = (Array.isArray(resolved) ? resolved : []).map(({ id }) => recordLink(id));
31
+ if (field.fieldType === "Reference" && resolved && !Array.isArray(resolved)) links[name] = recordLink(resolved.id);
32
+ }
33
+ return links;
34
+ }
35
+ /** The titles of the records a record references, by record id. */
36
+ function referencedTitles(references) {
37
+ return Object.fromEntries(Object.values(references ?? {}).flatMap((value) => [value ?? []].flat()).map((record) => [record.id, record.title]));
38
+ }
39
+ /**
40
+ * The record a record page renders, as its layout's context names it, from the record a loader
41
+ * found and when it was published to Live.
42
+ */
43
+ function recordPageValues(collection, { data: record, published }) {
44
+ return {
45
+ title: record.title ?? "",
46
+ description: record.description ?? void 0,
47
+ content: {
48
+ ...recordLinks(collection, record.references),
49
+ ...recordPlainText(collection, record.content)
50
+ },
51
+ titles: referencedTitles(record.references),
52
+ ...published && { published }
53
+ };
54
+ }
55
+ /**
56
+ * A record's title and description: its collection's templates filled with its fields' plain
57
+ * text, as the API fills them. For records edited here, before the API names them again.
58
+ */
59
+ function recordText$1(collection, content) {
60
+ if (!collection) return {
61
+ title: "",
62
+ description: null
63
+ };
64
+ return recordText(collection._internal, (name) => fieldPlainText(content[name]));
65
+ }
66
+ /**
67
+ * The first field a collection's title template names, such as `firstName` in
68
+ * `{{firstName}} {{lastName}}`: where a new record's name goes.
69
+ */
70
+ function titleField(toTitle) {
71
+ return templateFields(toTitle)[0];
72
+ }
20
73
  /** A String field's text, whether it is stored as plain text or rich-text state. */
21
74
  function fieldPlainText(value) {
22
75
  if (typeof value !== "string" && !isLexicalState(value)) return "";
@@ -27,8 +80,8 @@ function fieldPlainText(value) {
27
80
  * field tokens are left for the API to fill with the live record.
28
81
  */
29
82
  function recordMarkdownTemplate(collection, toMarkdown) {
30
- return resolveToMarkdown((item) => toMarkdown(item), void 0, "item", collection._internal.contentSchema.properties);
83
+ return resolveToMarkdown(({ content }) => toMarkdown(content), void 0, "item", collection._internal.contentSchema.properties);
31
84
  }
32
85
 
33
86
  //#endregion
34
- export { asCollectionItem, fieldPlainText, recordMarkdownTemplate, recordPlainText };
87
+ export { asCollectionItem, fieldPlainText, recordMarkdownTemplate, recordPageValues, recordPlainText, recordText$1 as recordText, titleField };
@@ -1,4 +1,5 @@
1
1
  import { ReferenceRecord } from "./lib/reference.js";
2
+ import { EvaluatedNode, RecordStructuredData } from "@camox/api-contract";
2
3
  //#region src/core/requestContext.d.ts
3
4
  declare const requestContextBrand: unique symbol;
4
5
  /**
@@ -16,11 +17,28 @@ export interface CollectionRecordId {
16
17
  /** A collection record as a record page loads it. */
17
18
  export interface LoadedRecord<Content = Record<string, unknown>> {
18
19
  id: string;
20
+ /** Its title, as the API evaluates its collection's `toTitle`. */
21
+ title?: string;
22
+ /** Its description, as the API evaluates its collection's `toDescription`: null for none. */
23
+ description?: string | null;
19
24
  content: Content;
20
25
  /** The records it links, resolved by Camox with the record as the first hop. */
21
26
  references?: ReferenceRecord["references"];
22
27
  /** When its content last changed. */
23
28
  updatedAt?: number;
29
+ /** When it was first and last published to Live. Absent from older APIs. */
30
+ firstPublishedAt?: number | null;
31
+ publishedAt?: number | null;
32
+ /** Its Structured Data, as the API evaluates it. Absent from older APIs. */
33
+ structuredData?: RecordPageStructuredData;
34
+ }
35
+ /**
36
+ * A record page's record as Structured Data: its node, when its collection has a template, and
37
+ * the nodes of the records it references and of those they link.
38
+ */
39
+ export interface RecordPageStructuredData {
40
+ node?: EvaluatedNode;
41
+ records: RecordStructuredData;
24
42
  }
25
43
  //#endregion
26
44
  export {};
@@ -0,0 +1,126 @@
1
+ import { ARTICLE_TYPES } from "@camox/api-contract";
2
+ import { AggregateRatingSimple, ArticleSimple, BookSimple, CourseSimple, EventSimple, HowToSimple, ImageSimple, JobPostingSimple, OfferSimple, OrganizationSimple, PersonSimple, PlaceSimple, ProductSimple, QuestionSimple, RecipeSimple, ReviewSimple, ServiceSimple, SoftwareAppSimple, VideoSimple, WebPageSimple } from "@unhead/schema-org";
3
+ //#region src/core/structuredData.d.ts
4
+ export type ArticleType = (typeof ARTICLE_TYPES)[number];
5
+ /** The kinds of web page a layout can say its pages are. */
6
+ export declare const WEB_PAGE_TYPES: readonly ["AboutPage", "CheckoutPage", "CollectionPage", "ContactPage", "FAQPage", "ItemPage", "MedicalWebPage", "ProfilePage", "QAPage", "RealEstateListing", "SearchResultsPage"];
7
+ export type WebPageType = (typeof WEB_PAGE_TYPES)[number];
8
+ /** A type's own properties, without the index signature that would let any key through. */
9
+ type Known<T> = T extends unknown ? { [K in keyof T as string extends K ? never : number extends K ? never : K]: T[K]; } : never;
10
+ /** The given properties of a type, required. */
11
+ type Need<T, K extends keyof Known<T>> = { [P in K]-?: Exclude<Known<T>[P], undefined>; };
12
+ /** Each curated `@type`, with the node whose properties it takes. */
13
+ type NodeProperties = { [T in ArticleType]: ArticleSimple; } & {
14
+ Product: ProductSimple;
15
+ Offer: OfferSimple;
16
+ Event: EventSimple;
17
+ Person: PersonSimple;
18
+ Place: PlaceSimple;
19
+ Organization: OrganizationSimple;
20
+ Review: ReviewSimple;
21
+ AggregateRating: AggregateRatingSimple;
22
+ Question: QuestionSimple;
23
+ HowTo: HowToSimple;
24
+ Recipe: RecipeSimple;
25
+ Course: CourseSimple;
26
+ JobPosting: JobPostingSimple;
27
+ SoftwareApplication: SoftwareAppSimple;
28
+ VideoObject: VideoSimple;
29
+ ImageObject: ImageSimple;
30
+ Book: BookSimple;
31
+ Service: ServiceSimple;
32
+ };
33
+ /** Every `@type` a definition's nodes can have. */
34
+ export type StructuredDataType = keyof NodeProperties;
35
+ /** What search engines require of each type, including what Camox may fill in. */
36
+ type NodeRequirements = { [T in ArticleType]: Need<ArticleSimple, "headline">; } & {
37
+ Product: Need<ProductSimple, "name"> & (Need<ProductSimple, "offers"> | Need<ProductSimple, "review"> | Need<ProductSimple, "aggregateRating">);
38
+ Offer: {
39
+ price: number | string;
40
+ priceCurrency: string;
41
+ };
42
+ Event: Need<EventSimple, "name" | "startDate" | "location">;
43
+ Person: Need<PersonSimple, "name">;
44
+ Place: Need<PlaceSimple, "name">;
45
+ Organization: Need<OrganizationSimple, "name">;
46
+ Review: Need<ReviewSimple, "author">;
47
+ AggregateRating: Need<AggregateRatingSimple, "ratingValue"> & (Need<AggregateRatingSimple, "ratingCount"> | Need<AggregateRatingSimple, "reviewCount">);
48
+ Question: Need<QuestionSimple, "name" | "acceptedAnswer">;
49
+ HowTo: Need<HowToSimple, "name" | "step">;
50
+ Recipe: Need<RecipeSimple, "name" | "image">;
51
+ Course: Need<CourseSimple, "name" | "description">;
52
+ JobPosting: Need<JobPostingSimple, "title" | "description" | "datePosted" | "hiringOrganization">;
53
+ SoftwareApplication: Need<SoftwareAppSimple, "name" | "offers"> & (Need<SoftwareAppSimple, "aggregateRating"> | Need<SoftwareAppSimple, "review">);
54
+ VideoObject: Need<VideoSimple, "name" | "thumbnailUrl" | "uploadDate">;
55
+ ImageObject: Need<ImageSimple, "contentUrl">;
56
+ Book: Need<BookSimple, "name" | "author">;
57
+ Service: Need<ServiceSimple, "name">;
58
+ };
59
+ /** Properties Camox sets on every node: its identity, and its link to its page. */
60
+ type CamoxOwned = "@type" | "@id" | "mainEntityOfPage";
61
+ /** A requirement, without the properties Camox fills. */
62
+ type Unfilled<Requirement, Filled extends PropertyKey> = Requirement extends unknown ? Omit<Requirement, Filled> : never;
63
+ type NodeOf<T extends StructuredDataType, Filled extends PropertyKey> = T extends unknown ? {
64
+ "@type": T;
65
+ } & Partial<Omit<Known<NodeProperties[T]>, CamoxOwned>> & Unfilled<NodeRequirements[T], Filled> : never;
66
+ /**
67
+ * A node of Camox's vocabulary. `Excluded` lists the types a definition can't emit, and `Filled`
68
+ * the properties Camox fills when the definition leaves them out.
69
+ */
70
+ export type StructuredDataNode<Excluded extends StructuredDataType = never, Filled extends PropertyKey = never> = NodeOf<Exclude<StructuredDataType, Excluded>, Filled>;
71
+ /** The properties Camox fills on a layout's main entity, from its page. */
72
+ export type MainEntityFilled = "name" | "headline" | "description" | "url" | "image" | "datePublished" | "dateModified" | "publisher" | "inLanguage";
73
+ /** What a page is about, beside its own node. Only collections' records can be organizations. */
74
+ export type MainEntity = StructuredDataNode<"Organization", MainEntityFilled>;
75
+ /** The properties Camox fills on a record's node, from the record's title and description. */
76
+ export type RecordNodeFilled = "name" | "headline" | "description";
77
+ /**
78
+ * What a collection's `toStructuredData` describes each record as. Records can be organizations,
79
+ * such as customers or partners, with their own `@id`s.
80
+ */
81
+ export type RecordNode = StructuredDataNode<never, RecordNodeFilled>;
82
+ /** What blocks and repeatable items describe: anything but an organization, as Camox emits the site's. */
83
+ export type BlockNode = StructuredDataNode<"Organization">;
84
+ declare const RepeatedNodesBrand: unique symbol;
85
+ /**
86
+ * A repeater's token in a Structured Data template: its items' nodes, from their own template,
87
+ * where the token is placed.
88
+ */
89
+ export type RepeatedNodes = never[] & {
90
+ readonly [RepeatedNodesBrand]: true;
91
+ };
92
+ /** What a block's or repeatable item's `toStructuredData` returns: one or more nodes. */
93
+ export type BlockStructuredData = BlockNode | RepeatedNodes | ReadonlyArray<BlockNode | RepeatedNodes>;
94
+ /** Requires nothing of a list of nodes, or a node, that only take their types' properties. */
95
+ export type KnownNodesOnly<Nodes> = [Nodes] extends [readonly (infer Node)[]] ? KnownPropertiesOnly<Node, BlockNode> : KnownPropertiesOnly<Nodes, BlockNode>;
96
+ /** The page node's properties that Camox alone sets, from the page and the layout's outputs. */
97
+ type PageOwned = CamoxOwned | "url" | "name" | "description" | "isPartOf" | "inLanguage" | "datePublished" | "dateModified" | "primaryImageOfPage" | "breadcrumb" | "mainEntity" | "potentialAction";
98
+ /** A kind of web page, merged into the page's node. */
99
+ export type WebPageKind = {
100
+ "@type": WebPageType;
101
+ } & Partial<Omit<Known<WebPageSimple>, PageOwned>>;
102
+ /** What a layout's `toStructuredData` says its page is: a kind of web page, or a main entity. */
103
+ export type LayoutStructuredData = WebPageKind | MainEntity;
104
+ /** The members of a vocabulary a node of type `T` can be. */
105
+ type MembersOf<Vocabulary, T> = Vocabulary extends {
106
+ "@type": infer U;
107
+ } ? T extends U ? Vocabulary : never : never;
108
+ /**
109
+ * A node's keys that hold a value. TypeScript gives each object of a list the others' keys as
110
+ * `?: undefined`, which say nothing.
111
+ */
112
+ type SetKeys<Node> = { [K in keyof Node]-?: [Exclude<Node[K], undefined>] extends [never] ? never : K; }[keyof Node];
113
+ /**
114
+ * The properties of a node that its type doesn't take. Object literals returned by a function
115
+ * escape TypeScript's excess property checks, so definitions infer their nodes and check them
116
+ * with `KnownPropertiesOnly`.
117
+ */
118
+ export type UnknownProperties<Node, Vocabulary> = Node extends {
119
+ "@type": infer T;
120
+ } ? Exclude<SetKeys<Node>, keyof MembersOf<Vocabulary, T>> : never;
121
+ /** Requires nothing of a definition whose nodes only take their types' properties. */
122
+ export type KnownPropertiesOnly<Node, Vocabulary> = [UnknownProperties<Node, Vocabulary>] extends [never] ? unknown : {
123
+ "❌ Camox: these structured data properties aren't in Camox's vocabulary": UnknownProperties<Node, Vocabulary>;
124
+ };
125
+ //#endregion
126
+ export { ARTICLE_TYPES };
@@ -0,0 +1,40 @@
1
+ import { ARTICLE_TYPES } from "@camox/api-contract";
2
+
3
+ //#region src/core/structuredData.ts
4
+ /** The kinds of web page a layout can say its pages are. */
5
+ const WEB_PAGE_TYPES = [
6
+ "AboutPage",
7
+ "CheckoutPage",
8
+ "CollectionPage",
9
+ "ContactPage",
10
+ "FAQPage",
11
+ "ItemPage",
12
+ "MedicalWebPage",
13
+ "ProfilePage",
14
+ "QAPage",
15
+ "RealEstateListing",
16
+ "SearchResultsPage"
17
+ ];
18
+ /** The curated types that are creative works, which pages contain rather than talk about. */
19
+ const CREATIVE_WORK_TYPES = [
20
+ ...ARTICLE_TYPES,
21
+ "Book",
22
+ "Course",
23
+ "HowTo",
24
+ "ImageObject",
25
+ "Question",
26
+ "Recipe",
27
+ "Review",
28
+ "SoftwareApplication",
29
+ "VideoObject"
30
+ ];
31
+ const includes = (types, type) => types.includes(type);
32
+ /** Whether a node is a kind of web page. */
33
+ const isWebPageKind = (node) => includes(WEB_PAGE_TYPES, node["@type"]);
34
+ /** Whether a node is an Article. */
35
+ const isArticle = (node) => includes(ARTICLE_TYPES, node["@type"]);
36
+ /** Whether a node is a creative work, rather than a thing a page talks about. */
37
+ const isCreativeWork = (node) => includes(CREATIVE_WORK_TYPES, node["@type"]);
38
+
39
+ //#endregion
40
+ export { ARTICLE_TYPES, CREATIVE_WORK_TYPES, WEB_PAGE_TYPES, isArticle, isCreativeWork, isWebPageKind };