@jini-ai/cms 0.5.5 → 0.5.7

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 (138) hide show
  1. package/CHANGELOG.md +4 -0
  2. package/dist/consent/express/index.d.ts +53 -0
  3. package/dist/consent/express/index.js +159 -0
  4. package/dist/consent/express/response.d.ts +21 -0
  5. package/dist/consent/express/response.js +89 -0
  6. package/dist/consent/html/client.d.ts +6 -0
  7. package/dist/consent/html/client.js +135 -0
  8. package/dist/consent/html/gate.d.ts +29 -0
  9. package/dist/consent/html/gate.js +104 -0
  10. package/dist/consent/html/index.d.ts +4 -0
  11. package/dist/consent/html/index.js +3 -0
  12. package/dist/consent/html/render.d.ts +27 -0
  13. package/dist/consent/html/render.js +62 -0
  14. package/dist/consent/html/static.d.ts +7 -0
  15. package/dist/consent/html/static.js +25 -0
  16. package/dist/consent/html/styles.d.ts +3 -0
  17. package/dist/consent/html/styles.js +22 -0
  18. package/dist/consent/index.d.ts +3 -0
  19. package/dist/consent/index.js +3 -0
  20. package/dist/consent/node/index.d.ts +47 -0
  21. package/dist/consent/node/index.js +169 -0
  22. package/dist/consent/policy.d.ts +47 -0
  23. package/dist/consent/policy.js +62 -0
  24. package/dist/consent/types.d.ts +40 -0
  25. package/dist/consent/types.js +5 -0
  26. package/dist/content-types/agent-tools.js +1 -1
  27. package/dist/content-types/tool-registrations.d.ts +2 -0
  28. package/dist/content-types/tool-registrations.js +1 -0
  29. package/dist/content-types/write-service.d.ts +2 -0
  30. package/dist/content-types/write-service.js +3 -0
  31. package/dist/forms/descriptor-rules.d.ts +9 -0
  32. package/dist/forms/descriptor-rules.js +112 -0
  33. package/dist/forms/email-format.d.ts +7 -0
  34. package/dist/forms/email-format.js +11 -0
  35. package/dist/forms/express/handler.js +4 -8
  36. package/dist/forms/field-registry.d.ts +193 -0
  37. package/dist/forms/field-registry.js +231 -0
  38. package/dist/forms/forms.d.ts +8 -5
  39. package/dist/forms/forms.js +29 -46
  40. package/dist/forms/html/html-authoring.js +63 -13
  41. package/dist/forms/html/html-submission-adapter.d.ts +5 -5
  42. package/dist/forms/html/html-submission-adapter.js +6 -13
  43. package/dist/forms/html-render.d.ts +10 -3
  44. package/dist/forms/html-render.js +116 -14
  45. package/dist/forms/index.d.ts +1 -0
  46. package/dist/forms/index.js +1 -0
  47. package/dist/forms/repo.memory.js +4 -4
  48. package/dist/forms/submission-attempts.d.ts +1 -1
  49. package/dist/forms/submission-attempts.js +13 -2
  50. package/dist/forms/submission-email-validation.js +2 -4
  51. package/dist/forms/submit-service.d.ts +3 -0
  52. package/dist/forms/submit-service.js +38 -5
  53. package/dist/forms/types.d.ts +50 -2
  54. package/dist/listing/batch.d.ts +53 -0
  55. package/dist/listing/batch.js +57 -0
  56. package/dist/listing/html.d.ts +72 -0
  57. package/dist/listing/html.js +109 -0
  58. package/dist/listing/index.d.ts +5 -0
  59. package/dist/listing/index.js +5 -0
  60. package/dist/listing/paths.d.ts +32 -0
  61. package/dist/listing/paths.js +37 -0
  62. package/dist/listing/see-more-script.d.ts +16 -0
  63. package/dist/listing/see-more-script.js +62 -0
  64. package/dist/media/content-type-sniffer.d.ts +1 -1
  65. package/dist/media/content-type-sniffer.js +15 -0
  66. package/dist/media/image-transformer.sharp.js +4 -1
  67. package/dist/media/media-service.d.ts +5 -3
  68. package/dist/media/media-service.js +10 -26
  69. package/dist/media/node.d.ts +1 -1
  70. package/dist/media/node.js +1 -1
  71. package/dist/media/slug.d.ts +8 -0
  72. package/dist/media/slug.js +32 -0
  73. package/dist/media/transform-registry.js +1 -1
  74. package/dist/media/transform-types.d.ts +2 -0
  75. package/dist/media/transform-types.js +3 -0
  76. package/dist/media/video-frames.ffmpeg.d.ts +15 -2
  77. package/dist/media/video-frames.ffmpeg.js +28 -13
  78. package/dist/navigation/dropdown.d.ts +41 -0
  79. package/dist/navigation/dropdown.js +85 -0
  80. package/dist/navigation/index.d.ts +2 -0
  81. package/dist/navigation/index.js +2 -0
  82. package/dist/redirects/capture.d.ts +2 -0
  83. package/dist/redirects/capture.js +4 -0
  84. package/dist/redirects/redirects.d.ts +8 -0
  85. package/dist/redirects/redirects.js +14 -0
  86. package/dist/routing/index.d.ts +5 -0
  87. package/dist/routing/index.js +2 -0
  88. package/dist/routing/ports.d.ts +51 -0
  89. package/dist/routing/ports.js +2 -0
  90. package/dist/routing/routing.d.ts +138 -0
  91. package/dist/routing/routing.js +443 -0
  92. package/dist/routing/types.d.ts +164 -0
  93. package/dist/routing/types.js +2 -0
  94. package/dist/seo/media.d.ts +23 -0
  95. package/dist/seo/media.js +30 -5
  96. package/dist/seo/page-head.js +8 -1
  97. package/dist/seo/seo.d.ts +2 -0
  98. package/dist/seo/seo.js +11 -6
  99. package/dist/seo/types.d.ts +8 -0
  100. package/dist/seo/xml-escape.d.ts +1 -3
  101. package/dist/seo/xml-escape.js +1 -6
  102. package/dist/widgets/concurrency.d.ts +27 -1
  103. package/dist/widgets/concurrency.js +12 -23
  104. package/dist/widgets/config-validation.js +6 -0
  105. package/dist/widgets/gallery-resolver.d.ts +23 -0
  106. package/dist/widgets/gallery-resolver.js +80 -0
  107. package/dist/widgets/html/gallery-render.d.ts +24 -0
  108. package/dist/widgets/html/gallery-render.js +65 -0
  109. package/dist/widgets/html/html-embeds.d.ts +4 -0
  110. package/dist/widgets/html/html-embeds.js +7 -1
  111. package/dist/widgets/html/index.d.ts +3 -0
  112. package/dist/widgets/html/index.js +3 -0
  113. package/dist/widgets/html/server-fragment.d.ts +9 -0
  114. package/dist/widgets/html/server-fragment.js +38 -0
  115. package/dist/widgets/markers/gallery.d.ts +50 -0
  116. package/dist/widgets/markers/gallery.js +164 -0
  117. package/dist/widgets/markers/html-attributes.d.ts +6 -1
  118. package/dist/widgets/markers/html-attributes.js +24 -9
  119. package/dist/widgets/markers/index.d.ts +2 -0
  120. package/dist/widgets/markers/index.js +2 -0
  121. package/dist/widgets/markers/marker.d.ts +4 -0
  122. package/dist/widgets/markers/marker.js +6 -0
  123. package/dist/widgets/markers/media-reference.d.ts +11 -0
  124. package/dist/widgets/markers/media-reference.js +11 -0
  125. package/dist/widgets/page-embed-types.d.ts +1 -1
  126. package/dist/widgets/page-embed-types.js +1 -1
  127. package/dist/widgets/registry.d.ts +144 -10
  128. package/dist/widgets/registry.js +40 -0
  129. package/dist/widgets/resolver-service.d.ts +10 -0
  130. package/dist/widgets/resolver-service.js +73 -29
  131. package/dist/widgets/resolvers/create-core-resolvers.d.ts +3 -0
  132. package/dist/widgets/resolvers/create-core-resolvers.js +2 -0
  133. package/dist/widgets/resolvers/gallery.d.ts +12 -0
  134. package/dist/widgets/resolvers/gallery.js +33 -0
  135. package/dist/widgets/resolvers/index.d.ts +1 -0
  136. package/dist/widgets/resolvers/index.js +1 -0
  137. package/dist/widgets/types.d.ts +3 -1
  138. package/package.json +100 -7
@@ -28,17 +28,151 @@
28
28
  * module load time.
29
29
  */
30
30
  import type { WidgetTypeKey, WidgetTypeRegistration } from "./types.js";
31
- /**
32
- * The complete v1 widget-type registry (REQ-09), keyed by the closed `WidgetTypeKey` union rather
33
- * than held as an array. Declared with `satisfies` rather than a `: Readonly<Record<...>>`
34
- * annotation — a `:` annotation would widen every entry back to the shared `WidgetTypeRegistration`
35
- * interface (losing e.g. `RECENT_ENTRIES_REGISTRATION`'s own non-optional `clamps.maxItems`);
36
- * `satisfies` checks the same shape constraint (every `WidgetTypeKey` has an entry, which is what
37
- * makes `getWidgetTypeRegistration` below total) while keeping each entry's own literal type intact.
38
- * Iterate with `Object.values(...)` where the whole table is needed (e.g. `agent-tools.ts`'s
39
- * published-type-list).
40
- */
41
31
  export declare const WIDGET_TYPE_REGISTRATIONS: {
32
+ media: {
33
+ typeKey: "media";
34
+ capability: "entry-reference";
35
+ configSchema: {
36
+ type: string;
37
+ 'x-gallery-config': true;
38
+ properties: {
39
+ mode: {
40
+ type: string;
41
+ };
42
+ html: {
43
+ type: string;
44
+ };
45
+ items: {
46
+ type: string;
47
+ maxItems: number;
48
+ items: {
49
+ type: string;
50
+ properties: {
51
+ slug: {
52
+ type: string;
53
+ 'x-ref-target': string;
54
+ };
55
+ alt: {
56
+ type: string;
57
+ };
58
+ caption: {
59
+ type: string;
60
+ };
61
+ };
62
+ required: string[];
63
+ additionalProperties: false;
64
+ };
65
+ };
66
+ layout: {
67
+ type: string;
68
+ };
69
+ columns: {
70
+ type: string;
71
+ minimum: number;
72
+ maximum: number;
73
+ };
74
+ aspectRatio: {
75
+ type: string;
76
+ };
77
+ htmlAttributes: {
78
+ type: string;
79
+ };
80
+ cssClass: {
81
+ type: string;
82
+ };
83
+ };
84
+ additionalProperties: false;
85
+ };
86
+ placementContexts: ("region" | "inline")[];
87
+ clamps: {
88
+ maxItems: number;
89
+ timeoutMs: number;
90
+ };
91
+ resolverId: string;
92
+ };
93
+ gallery: {
94
+ typeKey: "gallery";
95
+ capability: "entry-reference";
96
+ configSchema: {
97
+ type: string;
98
+ "x-gallery-config": true;
99
+ properties: {
100
+ mode: {
101
+ type: string;
102
+ };
103
+ html: {
104
+ type: string;
105
+ };
106
+ htmlAttributes: {
107
+ type: string;
108
+ };
109
+ cssClass: {
110
+ type: string;
111
+ };
112
+ items: {
113
+ type: string;
114
+ maxItems: number;
115
+ items: {
116
+ type: string;
117
+ properties: {
118
+ slug: {
119
+ type: string;
120
+ 'x-ref-target': string;
121
+ };
122
+ alt: {
123
+ type: string;
124
+ };
125
+ caption: {
126
+ type: string;
127
+ };
128
+ };
129
+ required: string[];
130
+ additionalProperties: false;
131
+ };
132
+ };
133
+ images: {
134
+ type: string;
135
+ maxItems: number;
136
+ items: {
137
+ type: string;
138
+ properties: {
139
+ mediaId: {
140
+ type: string;
141
+ "x-ref-target": string;
142
+ };
143
+ alt: {
144
+ type: string;
145
+ };
146
+ caption: {
147
+ type: string;
148
+ };
149
+ };
150
+ required: string[];
151
+ additionalProperties: false;
152
+ };
153
+ };
154
+ layout: {
155
+ type: string;
156
+ enum: string[];
157
+ };
158
+ columns: {
159
+ type: string;
160
+ minimum: number;
161
+ maximum: number;
162
+ };
163
+ aspectRatio: {
164
+ type: string;
165
+ };
166
+ };
167
+ additionalProperties: false;
168
+ };
169
+ placementContexts: ("region" | "inline")[];
170
+ clamps: {
171
+ maxItems: number;
172
+ timeoutMs: number;
173
+ };
174
+ resolverId: string;
175
+ };
42
176
  text: {
43
177
  typeKey: "text";
44
178
  capability: "static";
@@ -134,7 +134,47 @@ const CONTACT_FORM_REGISTRATION = {
134
134
  * Iterate with `Object.values(...)` where the whole table is needed (e.g. `agent-tools.ts`'s
135
135
  * published-type-list).
136
136
  */
137
+ /** Gallery owns no media lifecycle: its resolver adapts the existing gallery embed pipeline. */
138
+ const GALLERY_REGISTRATION = {
139
+ typeKey: "gallery",
140
+ capability: "entry-reference",
141
+ configSchema: {
142
+ type: "object", "x-gallery-config": true,
143
+ properties: {
144
+ mode: { type: 'string' }, html: { type: 'string' }, htmlAttributes: { type: 'string' }, cssClass: { type: 'string' },
145
+ items: { type: 'array', maxItems: 50, items: { type: 'object', properties: {
146
+ slug: { type: 'string', 'x-ref-target': 'asset' }, alt: { type: 'string' }, caption: { type: 'string' },
147
+ }, required: ['slug'], additionalProperties: false } },
148
+ images: { type: "array", maxItems: 50, items: { type: "object", properties: {
149
+ mediaId: { type: "string", "x-ref-target": "asset" }, alt: { type: "string" }, caption: { type: "string" },
150
+ }, required: ["mediaId"], additionalProperties: false } },
151
+ layout: { type: "string", enum: ["grid", "carousel", "masonry"] },
152
+ columns: { type: "integer", minimum: 1, maximum: 6 },
153
+ aspectRatio: { type: "string" },
154
+ }, additionalProperties: false,
155
+ },
156
+ placementContexts: ["region", "inline"],
157
+ clamps: { maxItems: 50, timeoutMs: 10000 }, resolverId: "gallery",
158
+ };
159
+ /** Generic media content; Gallery is a compatibility preset over the same resolver lifecycle. */
160
+ const MEDIA_REGISTRATION = {
161
+ typeKey: 'media', capability: 'entry-reference',
162
+ configSchema: {
163
+ type: 'object', 'x-gallery-config': true,
164
+ properties: {
165
+ mode: { type: 'string' }, html: { type: 'string' },
166
+ items: { type: 'array', maxItems: 50, items: { type: 'object', properties: {
167
+ slug: { type: 'string', 'x-ref-target': 'asset' }, alt: { type: 'string' }, caption: { type: 'string' },
168
+ }, required: ['slug'], additionalProperties: false } },
169
+ layout: { type: 'string' }, columns: { type: 'integer', minimum: 1, maximum: 6 },
170
+ aspectRatio: { type: 'string' }, htmlAttributes: { type: 'string' }, cssClass: { type: 'string' },
171
+ }, additionalProperties: false,
172
+ },
173
+ placementContexts: ['region', 'inline'], clamps: { maxItems: 50, timeoutMs: 10000 }, resolverId: 'gallery',
174
+ };
137
175
  export const WIDGET_TYPE_REGISTRATIONS = {
176
+ media: MEDIA_REGISTRATION,
177
+ gallery: GALLERY_REGISTRATION,
138
178
  text: TEXT_REGISTRATION,
139
179
  "social-links": SOCIAL_LINKS_REGISTRATION,
140
180
  "recent-entries": RECENT_ENTRIES_REGISTRATION,
@@ -132,6 +132,12 @@ export interface ResolveHtmlPageEmbedsDeps extends WidgetInstanceResolutionDeps
132
132
  };
133
133
  }
134
134
  type HtmlEmbedResolver = (refs: readonly PageHtmlEmbedRef[], deps: ResolveHtmlPageEmbedsDeps, context: WidgetResolveContext) => Promise<ReadonlyMap<string, WidgetRenderIR>>;
135
+ /** Gallery widget adapter over the same media owner used by gallery HTML embeds. */
136
+ export declare function resolveGalleryWidgetMedia({ refs, deps, context }: {
137
+ refs: readonly PageHtmlEmbedRef[];
138
+ deps: ResolveHtmlPageEmbedsDeps;
139
+ context: WidgetResolveContext;
140
+ }, _optional?: Record<string, never>): Promise<ReadonlyMap<string, WidgetRenderIR>>;
135
141
  /**
136
142
  * Registry of known embed types (SPEC-047, generalized 2026-08-07). Adding a new embed type is
137
143
  * registering one more entry here — no other file in `resolveHtmlPageEmbeds`'s call chain changes,
@@ -198,6 +204,10 @@ export declare const HTML_EMBED_RESOLVERS: Readonly<Record<string, HtmlEmbedReso
198
204
  *
199
205
  * `"featured-image"` (2026-10-05) joins for the identical reason: `static-render.ts`'s
200
206
  * `injectFeaturedImage` resolves it (see `FEATURED_IMAGE_MARKER_TYPE`, `core/embeds/marker.ts`).
207
+ *
208
+ * `"post-author"` (2026-10-11) joins for the identical reason: `static-render.ts`'s
209
+ * `injectPostAuthor` fills the byline. Without it this stage warned once per post render while the
210
+ * byline rendered correctly.
201
211
  */
202
212
  export declare const THEME_OWNED_MARKER_TYPES: ReadonlySet<string>;
203
213
  /**
@@ -1,4 +1,6 @@
1
1
  import { isNonArrayRecord } from "@jini-ai/core";
2
+ import { resolveGalleryEmbeds } from "./gallery-resolver.js";
3
+ import { isPlausibleMediaRefId } from "./markers/media-reference.js";
2
4
  import { resolveTaxonomyEmbeds } from "./resolvers/taxonomy.js";
3
5
  import { parseWidgetAreaPayload, parseWidgetInstancePayload } from "./entry-payload.js";
4
6
  import { resolveWidgetType } from "./resolvers/index.js";
@@ -27,7 +29,7 @@ function collectWidgetEmbeds(root, out) {
27
29
  if (!isNonArrayRecord(candidate))
28
30
  continue;
29
31
  if (isNonArrayRecord(attrs) && typeof attrs.value.widgetEntryId === "string" && typeof attrs.value.placementId === "string") {
30
- out.push({ placementId: attrs.value.placementId, widgetEntryId: attrs.value.widgetEntryId });
32
+ out.push({ placementId: attrs.value.placementId, widgetEntryId: attrs.value.widgetEntryId, ...(isNonArrayRecord({ value: attrs.value.content }) ? { content: attrs.value.content } : {}) });
31
33
  }
32
34
  if (Array.isArray(candidate.value.content))
33
35
  stack.push(candidate.value.content);
@@ -82,9 +84,19 @@ function groupWidgetInstancesByType(widgetRows, referencedIds) {
82
84
  }
83
85
  return byType;
84
86
  }
85
- async function resolveWidgetInstances(deps, workspaceId, referencedIds, context) {
87
+ async function resolveWidgetInstances(deps, workspaceId, referencedIds, context, overrides = [], originalIds = referencedIds) {
86
88
  const widgetRows = referencedIds.size > 0 ? await deps.entryRepo.listByWorkspace({ workspaceId }, { type: WIDGET_CONTENT_TYPE }) : [];
87
- const byType = groupWidgetInstancesByType(widgetRows, referencedIds);
89
+ const byType = groupWidgetInstancesByType(widgetRows, originalIds);
90
+ const rowsById = new Map(widgetRows.map(row => [row.id, row]));
91
+ for (const override of overrides) {
92
+ const row = rowsById.get(override.widgetEntryId);
93
+ const saved = row ? parseWidgetInstanceRow(row) : null;
94
+ if (!saved)
95
+ continue;
96
+ const instances = byType.get(saved.widgetType) ?? [];
97
+ instances.push({ ...saved, id: override.key, config: { ...saved.config, ...override.content } });
98
+ byType.set(saved.widgetType, instances);
99
+ }
88
100
  const resolvedById = new Map();
89
101
  for (const [typeKey, instances] of byType) {
90
102
  const results = await resolveWidgetType({ typeKey, instances, context });
@@ -154,21 +166,27 @@ export async function resolvePageWidgets(required, _optional = {}) {
154
166
  // handles them as "unresolved", not a crash), at most one `resolveWidgetType` call per distinct
155
167
  // type present (REQ-24) — see `resolveWidgetInstances`'s own doc for why this is shared rather
156
168
  // than inlined here.
157
- const resolvedById = await resolveWidgetInstances(deps, input.workspaceId, referencedIds, context);
169
+ const allPlacements = [...inlineEmbeds, ...[...regionPlacements.values()].flat()];
170
+ const originalIds = new Set(allPlacements.filter(placement => !placement.content).map(placement => placement.widgetEntryId));
171
+ const resolvedById = await resolveWidgetInstances(deps, input.workspaceId, referencedIds, context, inlineContentOverrides(allPlacements), originalIds);
158
172
  // 6. Assemble region -> IR[] (missing/failed placements degrade to the REQ-28 placeholder).
159
173
  const regions = {};
160
174
  for (const [regionKey, placements] of regionPlacements) {
161
- regions[regionKey] = placements.map((placement) => toRenderIr(resolvedById.get(placement.widgetEntryId)));
175
+ regions[regionKey] = placements.map((placement) => toRenderIr(resolvedById.get(placement.content ? placement.placementId : placement.widgetEntryId)));
162
176
  }
163
177
  // 7. Assemble placementId -> IR for inline embeds.
164
178
  return { regions, inlineResolved: assembleInlineResolved(inlineEmbeds, resolvedById) };
165
179
  }
180
+ /** Placement config merges before asset resolution; a repeated widget never shares overridden IR. */
181
+ function inlineContentOverrides(embeds) {
182
+ return embeds.flatMap(embed => embed.content ? [{ key: embed.placementId, widgetEntryId: embed.widgetEntryId, content: embed.content }] : []);
183
+ }
166
184
  /** placementId -> IR for every inline embed; a missing/trashed/failed widget degrades to the REQ-28
167
185
  * placeholder IR. Shared by {@link resolvePageWidgets} and {@link resolvePostContentWidgetContext}. */
168
186
  function assembleInlineResolved(inlineEmbeds, resolvedById) {
169
187
  const inlineResolved = new Map();
170
188
  for (const embed of inlineEmbeds) {
171
- inlineResolved.set(embed.placementId, toRenderIr(resolvedById.get(embed.widgetEntryId)));
189
+ inlineResolved.set(embed.placementId, toRenderIr(resolvedById.get(embed.content ? embed.placementId : embed.widgetEntryId)));
172
190
  }
173
191
  return inlineResolved;
174
192
  }
@@ -270,8 +288,22 @@ async function resolveWidgetTypeEmbeds(refs, deps, context) {
270
288
  const slugRefs = refs.filter((ref) => ref.id === null && ref.slug !== null);
271
289
  const idBySlug = await resolveWidgetSlugsToIds(slugRefs, deps, context);
272
290
  const ids = collectWidgetIdsToLoad(refs, idBySlug, context);
273
- const resolvedRaw = await resolveWidgetInstances(deps, context.workspaceId, ids, context);
274
- return buildResolvedWidgetMap(refs, idBySlug, resolvedRaw);
291
+ const overrides = refs.flatMap(ref => {
292
+ const content = ref.config?.content;
293
+ const widgetEntryId = ref.id ?? (ref.slug ? idBySlug.get(ref.slug) : undefined);
294
+ return widgetEntryId && ref.placementKey && isNonArrayRecord({ value: content })
295
+ ? [{ key: ref.placementKey, widgetEntryId, content: content }] : [];
296
+ });
297
+ const originalIds = new Set(refs.filter(ref => !ref.placementKey).flatMap(ref => {
298
+ const id = ref.id ?? (ref.slug ? idBySlug.get(ref.slug) : undefined);
299
+ return id ? [id] : [];
300
+ }));
301
+ const resolvedRaw = await resolveWidgetInstances(deps, context.workspaceId, ids, context, overrides, originalIds);
302
+ const resolved = buildResolvedWidgetMap(refs, idBySlug, resolvedRaw);
303
+ for (const override of overrides)
304
+ if (resolvedRaw.has(override.key))
305
+ resolved.set(override.key, toRenderIr(resolvedRaw.get(override.key)));
306
+ return resolved;
275
307
  }
276
308
  /** A form slug/id is a synthetic contact-form instance, resolved through the widget pipeline. */
277
309
  async function resolveFormTypeEmbeds(refs, _deps, context) {
@@ -283,19 +315,6 @@ async function resolveFormTypeEmbeds(refs, _deps, context) {
283
315
  });
284
316
  return new Map(keys.map((key) => [key, toRenderIr(results.get(key))]));
285
317
  }
286
- /** Mirrors `render.ts`'s own `MAX_MEDIA_REF_ID_LENGTH`/`PLAUSIBLE_MEDIA_REF_ID_PATTERN` shape check
287
- * for an `assetId`/`transformName` that will eventually become a `/m/` URL path segment —
288
- * duplicated here rather than imported for the same reason `core/entry-refs/extractor.ts`
289
- * duplicates `html-embeds.ts`'s scan pattern instead of importing it: `widgets/` (this file) must
290
- * not depend on `server/http/site/render.ts`, which sits above it in this codebase's layering. Both
291
- * copies guard the SAME hazard — this copy stops a malformed id/name from being used as a
292
- * repo-lookup key at resolve time; `render.ts`'s copy (`renderWidgetMediaImage`) re-checks
293
- * defense-in-depth immediately before URL templating. */
294
- const MAX_MEDIA_REF_ID_LENGTH = 200;
295
- const PLAUSIBLE_MEDIA_REF_ID_PATTERN = /^[^\s/]+$/;
296
- function isPlausibleMediaRefId(value) {
297
- return value.length > 0 && value.length <= MAX_MEDIA_REF_ID_LENGTH && PLAUSIBLE_MEDIA_REF_ID_PATTERN.test(value);
298
- }
299
318
  /** The IR's `slug` for a resolved asset: `null` means "has none" (`MediaAssetRenderMeta`'s own
300
319
  * contract), and a pre-backfill row stores that as `""`, so an empty slug is normalized to `null`
301
320
  * rather than passed through. Shared by every media IR builder in this file. */
@@ -318,7 +337,7 @@ function mediaIrSlug(record) {
318
337
  function parseMediaEmbedRef(ref, context, host) {
319
338
  const assetId = ref.id ?? ref.slug;
320
339
  const transformName = ref.variant ?? host.media.publicTransformName;
321
- if (assetId === null || !isPlausibleMediaRefId(assetId) || !isPlausibleMediaRefId(transformName)) {
340
+ if (assetId === null || !isPlausibleMediaRefId({ value: assetId }) || !isPlausibleMediaRefId({ value: transformName })) {
322
341
  console.warn('[widgets] resolveHtmlPageEmbeds: unresolved "media" reference — missing or invalid "id"/"slug"/"variant" in data-embed-config', { workspaceId: context.workspaceId });
323
342
  return undefined;
324
343
  }
@@ -378,8 +397,10 @@ async function resolveOneMediaEmbed(ref, mediaRepo, transformRepo, mediaContentT
378
397
  // previously-rendered HTML — that used to be `record.id`'s whole reason for being the emitted
379
398
  // value; now it is `media_slug_history` (S2a/S2b) keeping the OLD slug resolving too.
380
399
  const { assetId: refAssetId, transformName } = parsed;
381
- const record = await host.media.findMediaByIdOrSlug({ deps: { mediaRepo }, input: { workspaceId: context.workspaceId, idOrSlug: refAssetId } });
382
- if (!record) {
400
+ const record = ref.id === null && ref.slug !== null
401
+ ? await mediaRepo.findBySlug({ workspaceId: context.workspaceId, slug: ref.slug })
402
+ : await host.media.findMediaByIdOrSlug({ deps: { mediaRepo }, input: { workspaceId: context.workspaceId, idOrSlug: refAssetId } });
403
+ if (!record || record.status !== "active") {
383
404
  console.warn('[widgets] resolveHtmlPageEmbeds: unresolved "media" reference — no such asset', {
384
405
  workspaceId: context.workspaceId,
385
406
  assetId: refAssetId,
@@ -390,8 +411,8 @@ async function resolveOneMediaEmbed(ref, mediaRepo, transformRepo, mediaContentT
390
411
  const contentType = mediaContentTypeStore
391
412
  ? (await mediaContentTypeStore.getMany({ workspaceId: context.workspaceId, sha256s: [record.source.sha256] })).get(record.source.sha256)
392
413
  : undefined;
393
- if (contentType?.startsWith("video/")) {
394
- // Video needs no transform lookup at all: `render.ts`'s video branch points straight at
414
+ if (contentType?.startsWith("video/") || contentType?.startsWith("audio/")) {
415
+ // Video/audio need no image transform lookup: the shared native-player renderer points at
395
416
  // `/m/{assetId}/original` (the byte-passthrough route — see that route's own doc for why
396
417
  // video can't go through the image-transform pipeline below), so `transformName`/`version`
397
418
  // would be unused props for this IR.
@@ -431,6 +452,10 @@ async function resolveMediaTypeEmbeds(refs, deps, context) {
431
452
  }
432
453
  return resolved;
433
454
  }
455
+ /** Gallery widget adapter over the same media owner used by gallery HTML embeds. */
456
+ export function resolveGalleryWidgetMedia({ refs, deps, context }, _optional = {}) {
457
+ return resolveMediaTypeEmbeds(refs, deps, context);
458
+ }
434
459
  /** Scans a TipTap-shaped `bodyJson` tree for every ref-based `image`/`media` node's `assetId`
435
460
  * (ADR-027 §4's `{assetId, transformName}` shape) — both the legacy image-only ref shape and the
436
461
  * generic `media` node (2026-09-11, `render.ts`'s `renderDocMedia`'s own doc) share this collection,
@@ -586,7 +611,7 @@ async function resolvePostContentWidgetContext(deps, bodyJson, context) {
586
611
  if (inlineEmbeds.length === 0)
587
612
  return { inlineWidgets: {} };
588
613
  const referencedIds = new Set(inlineEmbeds.map((embed) => embed.widgetEntryId));
589
- const resolvedById = await resolveWidgetInstances(deps, context.workspaceId, referencedIds, context);
614
+ const resolvedById = await resolveWidgetInstances(deps, context.workspaceId, referencedIds, context, inlineContentOverrides(inlineEmbeds), new Set(inlineEmbeds.filter(embed => !embed.content).map(embed => embed.widgetEntryId)));
590
615
  const inlineResolved = assembleInlineResolved(inlineEmbeds, resolvedById);
591
616
  return { inlineWidgets: Object.fromEntries(inlineResolved) };
592
617
  }
@@ -845,7 +870,22 @@ export const HTML_EMBED_RESOLVERS = {
845
870
  widget: resolveWidgetTypeEmbeds,
846
871
  form: resolveFormTypeEmbeds,
847
872
  taxonomy: (refs, deps) => resolveTaxonomyEmbeds({ refs, deps }),
848
- media: resolveMediaTypeEmbeds,
873
+ media: async (refs, deps, context) => {
874
+ const inline = refs.filter(ref => ref.config?.content !== undefined || ref.config?.items !== undefined || ref.config?.html !== undefined);
875
+ const ordinary = refs.filter(ref => !inline.includes(ref));
876
+ const media = await resolveMediaTypeEmbeds(ordinary, deps, context);
877
+ const content = await resolveGalleryEmbeds({
878
+ refs: inline.map(ref => ({ ...ref, config: (ref.config?.content ?? ref.config) })),
879
+ resolveMedia: ({ refs }) => resolveMediaTypeEmbeds(refs, deps, context),
880
+ }, { reportIssue: ({ key, messages }) => console.warn('[widgets] unresolved media content', { key, messages }) });
881
+ return new Map([...media, ...content]);
882
+ },
883
+ gallery: (refs, deps, context) => resolveGalleryEmbeds({
884
+ refs,
885
+ resolveMedia: ({ refs: mediaRefs }) => resolveMediaTypeEmbeds(mediaRefs, deps, context),
886
+ }, {
887
+ reportIssue: ({ key, messages }) => console.warn('[widgets] unresolved gallery embed', { workspaceId: context.workspaceId, key, messages }),
888
+ }),
849
889
  post: resolvePostTypeEmbeds,
850
890
  content: resolveContentTypeEmbeds,
851
891
  };
@@ -898,8 +938,12 @@ export const HTML_EMBED_RESOLVERS = {
898
938
  *
899
939
  * `"featured-image"` (2026-10-05) joins for the identical reason: `static-render.ts`'s
900
940
  * `injectFeaturedImage` resolves it (see `FEATURED_IMAGE_MARKER_TYPE`, `core/embeds/marker.ts`).
941
+ *
942
+ * `"post-author"` (2026-10-11) joins for the identical reason: `static-render.ts`'s
943
+ * `injectPostAuthor` fills the byline. Without it this stage warned once per post render while the
944
+ * byline rendered correctly.
901
945
  */
902
- export const THEME_OWNED_MARKER_TYPES = new Set(["partial", "menu", "post-previews", "collection", "featured-image"]);
946
+ export const THEME_OWNED_MARKER_TYPES = new Set(["partial", "menu", "post-previews", "collection", "featured-image", "post-author"]);
903
947
  /**
904
948
  * `isPageEmbedType` (the "does this stage OWN the marker type" question, with its own doc) lives in
905
949
  * the dependency-free `page-embed-types.ts` so `render.ts` and the theme render workers can ask it
@@ -2,6 +2,7 @@ import type { EntryDisplayListPort, EntryListExcludingTypesPort, CollectionListP
2
2
  import type { EntryListPort } from "../../entries/index.js";
3
3
  import type { NavMenuReadModel, ResolveTargetHrefFn } from "../../navigation/index.js";
4
4
  import type { WidgetResolver, WidgetTypeKey } from "../types.js";
5
+ import { createGalleryWidgetResolver } from "./gallery.js";
5
6
  import { type ContentTypeLookup } from "./recent-entries.js";
6
7
  /**
7
8
  * @file Assembles the real, DI'd v1 dynamic resolvers against real infrastructure deps.
@@ -14,6 +15,8 @@ import { type ContentTypeLookup } from "./recent-entries.js";
14
15
  * test would otherwise need to construct.
15
16
  */
16
17
  export interface CoreResolverDeps {
18
+ /** Media remains host-owned. Older hosts degrade nonempty galleries until this port is wired. */
19
+ galleryMedia?: Parameters<typeof createGalleryWidgetResolver>[0]["resolveMedia"];
17
20
  menus: MenusPort;
18
21
  collectionList: CollectionListPort;
19
22
  resolveMenuTargetHref?: ResolveTargetHrefFn;
@@ -1,8 +1,10 @@
1
+ import { createGalleryWidgetResolver } from "./gallery.js";
1
2
  import { createContactFormResolver } from "./contact-form.js";
2
3
  import { createMenuResolver } from "./menu.js";
3
4
  import { createRecentEntriesResolver } from "./recent-entries.js";
4
5
  export function createCoreResolvers(deps, _optional = {}) {
5
6
  return {
7
+ gallery: createGalleryWidgetResolver({ resolveMedia: deps.galleryMedia ?? (async () => new Map()) }),
6
8
  "recent-entries": createRecentEntriesResolver({ collectionList: deps.collectionList, entryList: deps.entryList, contentTypes: deps.contentTypes }),
7
9
  menu: createMenuResolver({ menus: deps.menus, navMenuReadModel: deps.navMenuReadModel, ...(deps.resolveMenuTargetHref === undefined ? {} : { resolveTargetHref: deps.resolveMenuTargetHref }), publicOnly: true }),
8
10
  "contact-form": createContactFormResolver({ formDefinitionRepo: deps.formDefinitionRepo }),
@@ -0,0 +1,12 @@
1
+ import type { WidgetResolver } from '../types.js';
2
+ import type { GalleryMediaResolver } from '../gallery-resolver.js';
3
+ /** Adapt gallery widget instances to the existing embed resolver; one deduplicated media batch.
4
+ * Empty starter widgets render a note, while malformed stored configs degrade per instance.
5
+ * @complexity O(w*i) time/space, with i<=50 images and a shared 50-asset budget.
6
+ */
7
+ export declare function createGalleryWidgetResolver({ resolveMedia }: {
8
+ resolveMedia: (required: Parameters<GalleryMediaResolver>[0] & {
9
+ context: Parameters<WidgetResolver['resolveMany']>[1];
10
+ }, optional?: Record<string, never>) => ReturnType<GalleryMediaResolver>;
11
+ }, _optional?: Record<string, never>): WidgetResolver;
12
+ //# sourceMappingURL=gallery.d.ts.map
@@ -0,0 +1,33 @@
1
+ import { validateWidgetConfig } from '../config-validation.js';
2
+ import { getWidgetTypeRegistration } from '../registry.js';
3
+ import { resolveGalleryEmbeds } from '../gallery-resolver.js';
4
+ import { galleryMediaReferences, parseGalleryConfig } from '../markers/gallery.js';
5
+ /** Adapt gallery widget instances to the existing embed resolver; one deduplicated media batch.
6
+ * Empty starter widgets render a note, while malformed stored configs degrade per instance.
7
+ * @complexity O(w*i) time/space, with i<=50 images and a shared 50-asset budget.
8
+ */
9
+ export function createGalleryWidgetResolver({ resolveMedia }, _optional = {}) {
10
+ return { async resolveMany(instances, context) {
11
+ const refs = [];
12
+ const results = new Map();
13
+ for (const instance of instances) {
14
+ const parsed = parseGalleryConfig({ config: instance.config }, { allowEmpty: true });
15
+ if (!parsed.config || !validateWidgetConfig({ schema: getWidgetTypeRegistration({ typeKey: instance.widgetType === 'media' ? 'media' : 'gallery' }).configSchema, config: instance.config }).valid) {
16
+ results.set(instance.id, { ok: false, reason: 'resolver-error' });
17
+ continue;
18
+ }
19
+ if (parsed.config.images.length === 0 && parsed.config.mode !== 'html') {
20
+ results.set(instance.id, { ok: true, ir: { componentId: instance.widgetType === 'media' ? 'media' : 'gallery', props: { config: instance.config, media: {} } }, dependencyKeys: [instance.id] });
21
+ }
22
+ else
23
+ refs.push({ type: instance.widgetType, id: instance.id, slug: null, name: null, variant: null, header: true, config: instance.config });
24
+ }
25
+ const resolved = await resolveGalleryEmbeds({ refs, resolveMedia: ({ refs }) => resolveMedia({ refs, context }) });
26
+ for (const ref of refs) {
27
+ const ir = resolved.get(ref.id);
28
+ results.set(ref.id, ir ? { ok: true, ir, dependencyKeys: [ref.id, ...galleryMediaReferences({ config: ref.config }).map(image => image.mediaId)] } : { ok: false, reason: 'resolver-error' });
29
+ }
30
+ return results;
31
+ } };
32
+ }
33
+ //# sourceMappingURL=gallery.js.map
@@ -21,6 +21,7 @@
21
21
  import type { UUID } from "@jini-ai/core/primitives";
22
22
  import { type CoreResolverDeps } from "./create-core-resolvers.js";
23
23
  import type { WidgetInstanceView, WidgetResolveContext, WidgetResolveResult, WidgetResolver, WidgetTypeKey } from "../types.js";
24
+ export { createGalleryWidgetResolver } from "./gallery.js";
24
25
  export { createRecentEntriesResolver } from "./recent-entries.js";
25
26
  export { createMenuResolver } from "./menu.js";
26
27
  export { createContactFormResolver } from "./contact-form.js";
@@ -6,6 +6,7 @@ import { createCoreResolvers } from "./create-core-resolvers.js";
6
6
  // isolation (2026-08-18 widgets/resolvers deep-import triage), so each factory is re-exported here
7
7
  // narrowly, one line per consumer:
8
8
  // `createRecentEntriesResolver` — `widgets/__tests__/unit/resolvers-recent-entries.unit.test.ts`.
9
+ export { createGalleryWidgetResolver } from "./gallery.js";
9
10
  export { createRecentEntriesResolver } from "./recent-entries.js";
10
11
  // `createMenuResolver` — `widgets/__tests__/unit/resolvers-menu.unit.test.ts`.
11
12
  export { createMenuResolver } from "./menu.js";
@@ -47,7 +47,7 @@ export declare const WIDGET_AREA_FIELD_NAMESPACE: "widgets";
47
47
  * a bare `string` so a typo in a registration or a call site is a compile error,
48
48
  * not a runtime `unknown-type` surprise.
49
49
  */
50
- export type WidgetTypeKey = "text" | "social-links" | "recent-entries" | "menu" | "contact-form";
50
+ export type WidgetTypeKey = "text" | "social-links" | "recent-entries" | "menu" | "contact-form" | "gallery" | "media";
51
51
  /**
52
52
  * A widget type's declared capability class (ADR-047 Debate Fold-In Amendment
53
53
  * 3). `static` types render validated config directly with no resolver;
@@ -121,6 +121,8 @@ export interface WidgetPlacementNode {
121
121
  readonly placementId: UUID;
122
122
  readonly widgetEntryId: UUID;
123
123
  readonly enabled: boolean;
124
+ /** Optional per-placement config overlay, resolved before media lookup. */
125
+ readonly content?: JsonObject;
124
126
  }
125
127
  /** The full region-composition document stored in a `widget_area` entry's `bodyJson`. */
126
128
  export interface WidgetAreaDoc {