@amplifyup/sdk 0.1.62 → 0.1.63

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/README.md CHANGED
@@ -61,10 +61,10 @@ export default function Page() {
61
61
  }
62
62
  ```
63
63
 
64
- ### 3. Components use Field / Slot
64
+ ### 3. Write a component
65
65
 
66
66
  ```tsx
67
- import { Field, Slot } from '@amplifyup/sdk/react';
67
+ import { Field } from '@amplifyup/sdk/react';
68
68
  import type { Fields } from '@amplifyup/sdk/react';
69
69
 
70
70
  export function Hero({ fields }: { fields: Fields<{ heading: string }> }) {
@@ -78,13 +78,400 @@ export function Hero({ fields }: { fields: Fields<{ heading: string }> }) {
78
78
  }
79
79
  ```
80
80
 
81
- AmplifyUp passes `fields` on the component props. Use
82
- `<Field field={fields.heading} />` to show and edit, or
83
- `{fields.heading.value}` to render the text. You do not pass `name` unless
84
- you computed the value yourself. An empty field renders nothing on the live
85
- site; in Composer it stays clickable. Default copy belongs in your component
86
- (`{fields.heading.value ?? 'Welcome'}`). Connect content in Composer, then
87
- **Deploy** so the live site can serve the page.
81
+ Connect content in Composer, then **Deploy** so the live site can serve the page.
82
+
83
+ That's the whole loop. The rest of this section is the detail.
84
+
85
+ ---
86
+
87
+ ## Working with `fields`
88
+
89
+ Every component receives a `fields` prop. It is not raw CMS data — every entry is a **field envelope**:
90
+
91
+ ```ts
92
+ fields.heading
93
+ // → { value: 'Welcome to AmplifyUp', name: 'heading' }
94
+
95
+ fields.hero.image
96
+ // → { value: { url: '…', alt: '…' }, name: 'hero.image' }
97
+
98
+ fields.subheading // empty on a new page
99
+ // → { value: null, name: 'subheading' }
100
+ ```
101
+
102
+ `name` is the field's dot path. It is what lets `<Field>` know which field it is bound to without you telling it. `value` is the content, typed from your schema.
103
+
104
+ **The shape is identical on the live site, in a draft, and in Composer.** There is no mode where you get a bare string instead of an envelope, and no mode where an empty field is `undefined` instead of `{ value: null, name }`.
105
+
106
+ ### Two ways to read a field
107
+
108
+ ```tsx
109
+ // Editable — use an SDK component
110
+ <Field field={fields.heading} />
111
+
112
+ // Display only — read .value
113
+ <title>{fields.heading.value}</title>
114
+ ```
115
+
116
+ Use the component when the field should be clickable in Composer. Use `.value` when it shouldn't — meta tags, `aria-label`, an `href`, a condition, anything that isn't visible text.
117
+
118
+ ```tsx
119
+ export function Card({ fields }: { fields: Fields<{ title: string; link: string; image: ImageValue }> }) {
120
+ return (
121
+ <a href={fields.link.value ?? '#'} aria-label={fields.title.value ?? undefined}>
122
+ <Image field={fields.image} />
123
+ <h3><Field field={fields.title} /></h3>
124
+ </a>
125
+ );
126
+ }
127
+ ```
128
+
129
+ ### SDK field components
130
+
131
+ | Component | Field type | Renders |
132
+ | --- | --- | --- |
133
+ | `<Field>` | `string`, `number` | text node — no wrapper element |
134
+ | `<RichText>` | markdown / rich text | rendered HTML |
135
+ | `<Image>` | `ImageValue` | `<img>` |
136
+
137
+ All three take a `field` prop and read `name` from it. Pass any extra props (`className`, `loading`, etc.) and they go on the rendered element.
138
+
139
+ ```tsx
140
+ <Field field={fields.eyebrow} className="text-xs uppercase" />
141
+ <RichText field={fields.body} className="prose" />
142
+ <Image field={fields.cover} className="rounded-lg" loading="lazy" />
143
+ ```
144
+
145
+ `<Field>` is scalar-only. Passing a list is a type error:
146
+
147
+ ```tsx
148
+ // ✗ Field<string[]> is not assignable to Field<string | number>
149
+ <Field field={fields.tags} />
150
+ ```
151
+
152
+ ### Empty fields
153
+
154
+ An empty field renders **nothing** on the live site and a clickable placeholder in Composer. The SDK never invents default copy.
155
+
156
+ ```tsx
157
+ // A new page, `subheading` not yet filled in:
158
+
159
+ <Field field={fields.subheading} />
160
+ // live: (nothing)
161
+ // Composer: clickable empty placeholder
162
+ ```
163
+
164
+ If you want a default, it's yours to write, and it reads from `.value`:
165
+
166
+ ```tsx
167
+ // ✓ your default, in your code
168
+ <p>{fields.subheading.value ?? 'Built for teams that ship.'}</p>
169
+
170
+ // ✓ hide the whole component when the key field is empty
171
+ export function Banner({ fields }: { fields: Fields<{ message: string }> }) {
172
+ if (!fields.message.value) return null;
173
+ return <div className="banner"><Field field={fields.message} /></div>;
174
+ }
175
+ ```
176
+
177
+ Why no `fallback` prop: a fallback turns "this field was never populated" into "this field says *Welcome*". Nobody notices until a customer does. Empty should look empty.
178
+
179
+ ### Wrapper elements
180
+
181
+ `<Field>` renders a text node, not an element, so you supply the tag:
182
+
183
+ ```tsx
184
+ // ✓
185
+ <h1><Field field={fields.heading} /></h1>
186
+ <p className="lead"><Field field={fields.intro} /></p>
187
+
188
+ // ✗ — Field has no `as` prop; put the element around it
189
+ <Field as="h1" field={fields.heading} />
190
+ ```
191
+
192
+ If the wrapper should disappear when the field is empty, check `.value`:
193
+
194
+ ```tsx
195
+ {fields.intro.value ? (
196
+ <p className="lead"><Field field={fields.intro} /></p>
197
+ ) : null}
198
+ ```
199
+
200
+ ### Computed values
201
+
202
+ Occasionally what you display isn't a field — it's derived from one. Then you pass `value` and must also pass `name` so Composer knows which field a click edits:
203
+
204
+ ```tsx
205
+ // Show a formatted price but edit the raw number
206
+ <Field
207
+ value={formatPrice(fields.price.value)}
208
+ name={fields.price.name}
209
+ />
210
+ ```
211
+
212
+ `name` is only accepted alongside `value`. This is the one place you write it.
213
+
214
+ ```tsx
215
+ // ✗ — redundant, and a type error
216
+ <Field field={fields.price} name="price" />
217
+
218
+ // ✗ — value without name has nothing to bind to
219
+ <Field value={formatPrice(fields.price.value)} />
220
+ ```
221
+
222
+ ### Rendering differently in Composer
223
+
224
+ Most components render the same everywhere. Sometimes the live markup can't host an editable field — the text is a CSS background, an SVG `<title>`, a `<meta>` tag, an attribute, or it's behind interaction (a closed accordion, an auto-playing carousel). In Composer you still need something the author can click.
225
+
226
+ `useInComposer()` tells you which mode you're in:
227
+
228
+ ```tsx
229
+ import { Field, Image, useInComposer } from '@amplifyup/sdk/react';
230
+
231
+ export function HeroBackground({ fields }: { fields: Fields<{ heading: string; image: ImageValue }> }) {
232
+ const inComposer = useInComposer();
233
+
234
+ return (
235
+ <section
236
+ className="hero"
237
+ style={{ backgroundImage: `url(${fields.image.value?.url ?? ''})` }}
238
+ >
239
+ {/* Live: image is a CSS background. Composer: also render it so it's clickable. */}
240
+ {inComposer ? <Image field={fields.image} className="hidden" /> : null}
241
+ <h1><Field field={fields.heading} /></h1>
242
+ </section>
243
+ );
244
+ }
245
+ ```
246
+
247
+ Other common cases:
248
+
249
+ ```tsx
250
+ // Attribute on the live site, editable in Composer
251
+ const inComposer = useInComposer();
252
+ return (
253
+ <>
254
+ <button aria-label={fields.label.value ?? undefined}>
255
+ <Icon />
256
+ {inComposer ? <Field field={fields.label} /> : null}
257
+ </button>
258
+ </>
259
+ );
260
+ ```
261
+
262
+ ```tsx
263
+ // Component that hides itself when empty on live, but stays visible for authors
264
+ export function Announcement({ fields }: { fields: Fields<{ message: string }> }) {
265
+ const inComposer = useInComposer();
266
+ if (!fields.message.value && !inComposer) return null;
267
+ return <div className="announcement"><Field field={fields.message} /></div>;
268
+ }
269
+ ```
270
+
271
+ ```tsx
272
+ // Interaction that fights the canvas — turn it off in Composer
273
+ export function Carousel({ fields }) {
274
+ const inComposer = useInComposer();
275
+ return (
276
+ <Collection field={fields.slides}>
277
+ {(slide) => <Slide slide={slide} autoplay={!inComposer} />}
278
+ </Collection>
279
+ );
280
+ }
281
+ ```
282
+
283
+ Rules of thumb:
284
+
285
+ - The **field component is the source of truth in Composer.** If a field is visible to authors, it must be rendered through `<Field>` / `<RichText>` / `<Image>` / `<Collection>` in Composer mode, even if the live site reads `.value`.
286
+ - **Don't fork the whole component.** Branch the one element that differs, not the entire return. Two divergent trees drift and the preview stops matching production.
287
+ - **Empty gating uses `inComposer`.** `if (!x.value) return null` is correct for live and wrong for authors — they can't click what isn't rendered. Add `&& !inComposer`.
288
+ - **Don't use it to hide editable text on live.** If the text is visible on the live site, render it with the field component in both modes. `useInComposer` is for content the live markup genuinely can't expose as a field.
289
+
290
+ ```tsx
291
+ // ✗ — heading is visible on live; no reason to fork
292
+ {inComposer ? <Field field={fields.heading} /> : fields.heading.value}
293
+
294
+ // ✓
295
+ <Field field={fields.heading} />
296
+ ```
297
+
298
+ ---
299
+
300
+ ## Lists
301
+
302
+ There are two kinds of list field and they are handled differently. Check your schema to know which you have.
303
+
304
+ | Shape | Schema | What it holds | In your component | Edited in Composer via |
305
+ | --- | --- | --- | --- | --- |
306
+ | **Reference list** | `array` + `entity` `of` | Linked entries — a query or a curated set of documents | Map `fields.posts.value` — plain rows | Props panel (pick, reorder) |
307
+ | **Repeater** | `array` + `of: 'json'` | Inline-authored items (slides, cards, FAQs) | `<Collection>` + `<Field>` on each item | Canvas (add, remove, reorder, click to edit) |
308
+
309
+ Scalar lists (tags, multi-select) are plain `.value` arrays edited in the props panel — same as reference lists.
310
+
311
+ ### Reference lists
312
+
313
+ Rows are plain objects, not envelopes. Read them directly:
314
+
315
+ ```tsx
316
+ export function LatestPosts({ fields }: { fields: Fields<{ heading: string; posts: Post[] }> }) {
317
+ const posts = fields.posts.value ?? [];
318
+ return (
319
+ <section>
320
+ <h2><Field field={fields.heading} /></h2>
321
+ <ul>
322
+ {posts.map((post) => (
323
+ <li key={post.id}>
324
+ <img src={post.mainImage?.url} alt={post.mainImage?.alt ?? ''} />
325
+ <a href={`/blog/${post.slug}`}>{post.title}</a>
326
+ </li>
327
+ ))}
328
+ </ul>
329
+ </section>
330
+ );
331
+ }
332
+ ```
333
+
334
+ ```tsx
335
+ // ✗ — reference rows aren't editable inline; there's nothing for Field to bind to
336
+ {posts.map((post) => <Field field={post.title} />)}
337
+
338
+ // ✗ — Collection is for repeaters
339
+ <Collection field={fields.posts}>…</Collection>
340
+ ```
341
+
342
+ The list itself (which posts, in what order) is edited in the props panel. When the component is selected on the canvas, the component outline is the affordance — you don't need to add one.
343
+
344
+ ### Repeaters
345
+
346
+ Items **are** enveloped. `fields.slides.value[0].title` is `{ value, name: 'slides.0.title' }`. Render through `<Collection>`:
347
+
348
+ ```tsx
349
+ import { Collection, Field, Image } from '@amplifyup/sdk/react';
350
+
351
+ export function Slideshow({
352
+ fields,
353
+ }: {
354
+ fields: Fields<{ slides: Array<{ title: string; caption: string; image: ImageValue }> }>;
355
+ }) {
356
+ return (
357
+ <Collection field={fields.slides}>
358
+ {(slide, i) => (
359
+ <figure className="slide" data-index={i}>
360
+ <Image field={slide.image} />
361
+ <figcaption>
362
+ <h2><Field field={slide.title} /></h2>
363
+ <Field field={slide.caption} className="text-sm" />
364
+ </figcaption>
365
+ </figure>
366
+ )}
367
+ </Collection>
368
+ );
369
+ }
370
+ ```
371
+
372
+ On the live site `Collection` renders your children and nothing else — no wrapper element. In Composer it adds an empty-state placeholder and add / remove / reorder controls.
373
+
374
+ Need a wrapping `<ul>`? Put it outside:
375
+
376
+ ```tsx
377
+ <ul className="faq">
378
+ <Collection field={fields.questions}>
379
+ {(q) => (
380
+ <li>
381
+ <h3><Field field={q.question} /></h3>
382
+ <RichText field={q.answer} />
383
+ </li>
384
+ )}
385
+ </Collection>
386
+ </ul>
387
+ ```
388
+
389
+ ```tsx
390
+ // ✗ — mapping .value skips the canvas chrome; items won't be addable in Composer
391
+ {fields.slides.value.map((slide) => <Field field={slide.title} />)}
392
+ ```
393
+
394
+ ### Mixing a list with a runtime query
395
+
396
+ Search or "load more" results come from `queryContent`, which returns plain rows. Your reference list is also plain rows, so they share a renderer:
397
+
398
+ ```tsx
399
+ const posts = hits ?? fields.posts.value ?? [];
400
+ return <PostGrid posts={posts} />;
401
+ ```
402
+
403
+ No SDK component is involved on either path.
404
+
405
+ ---
406
+
407
+ ## Slots
408
+
409
+ A slot is a region where authors drop other components. Render it with `<Slot>`:
410
+
411
+ ```tsx
412
+ import { Field, Slot } from '@amplifyup/sdk/react';
413
+
414
+ export function TwoColumn({ fields, slots }) {
415
+ return (
416
+ <div className="grid grid-cols-2">
417
+ <div><Slot name="left" slots={slots} /></div>
418
+ <div><Slot name="right" slots={slots} /></div>
419
+ </div>
420
+ );
421
+ }
422
+ ```
423
+
424
+ Slots hold components, not fields — nothing in a slot is read through `fields`.
425
+
426
+ ---
427
+
428
+ ## Do / Don't
429
+
430
+ | Do | Don't |
431
+ | --- | --- |
432
+ | `<Field field={fields.heading} />` | `<Field field={fields.heading} name="heading" />` |
433
+ | `{fields.heading.value}` for non-visible uses | `{fields.heading}` as a JSX child (it's an object) |
434
+ | `{fields.x.value ?? 'default'}` in your code | expect the SDK to supply default copy |
435
+ | `if (!fields.x.value) return null` to hide a component | wrap `Field` in conditional logic that hides the field in Composer |
436
+ | `<h1><Field … /></h1>` | look for an `as` / `wrapper` prop |
437
+ | `fields.posts.value.map(…)` for reference lists | `<Field>` or `<Collection>` on reference rows |
438
+ | `<Collection>` for repeaters | `fields.slides.value.map(…)` for repeaters |
439
+ | `value` + `name` together for computed output | `value` alone, or `name` alone |
440
+ | `<Image field={fields.cover} />` | `<img src={fields.cover.value.url} />` when it should be editable |
441
+ | `queryContent` rows as plain data | treat query rows as `fields` |
442
+ | `useInComposer()` to expose a field the live markup can't (CSS background, attribute, hidden panel) | fork the whole component's return on `inComposer` |
443
+ | `if (!x.value && !inComposer) return null` | `if (!x.value) return null` — authors can't click what isn't rendered |
444
+
445
+ ---
446
+
447
+ ## Types
448
+
449
+ ```ts
450
+ import type { Field, Fields, ImageValue } from '@amplifyup/sdk/react';
451
+
452
+ type Field<T> = { value: T | null; name: string };
453
+
454
+ type Fields<T> = { [K in keyof T]: /* Field<T[K]>, recursing into objects and repeater arrays */ };
455
+ ```
456
+
457
+ Declare the schema shape once on the component and let inference do the rest:
458
+
459
+ ```ts
460
+ type HeroFields = Fields<{
461
+ eyebrow: string;
462
+ heading: string;
463
+ body: string; // markdown → use <RichText>
464
+ image: ImageValue; // → use <Image>
465
+ ctas: Array<{ label: string; href: string }>; // repeater
466
+ related: Post[]; // reference list — rows are plain Post
467
+ }>;
468
+
469
+ export function Hero({ fields }: { fields: HeroFields }) { … }
470
+ ```
471
+
472
+ `fields.heading.value` is `string | null`. `fields.image.value` is `ImageValue | null`. `fields.ctas.value[0].label` is `Field<string>`. `fields.related.value[0]` is `Post`.
473
+
474
+ ---
88
475
 
89
476
  ## Content sources
90
477
 
@@ -95,6 +482,8 @@ site; in Composer it stays clickable. Default copy belongs in your component
95
482
  | Composer preview | Orchestrator (Control Plane) layout provider | `?preview=true` / Composer iframe |
96
483
  | Runtime queries | Edge `/v1/query` | `queryContent` — search, load more, pickers |
97
484
 
485
+ Field envelopes are attached once, when the SDK reads the Edge response. Edge's JSON is raw; you never see it that way if you go through `AmplifyPageContent` or `fetchPageConfigServer`. If you call `fetchEdgeResolve` directly you get raw props and must map them yourself — prefer the documented path.
486
+
98
487
  ## Pattern pages (From page)
99
488
 
100
489
  Components using Composer **From page** require `pageContext` on the provider (or `init`) so the first paint and first page view include fields. Late `setPageContext` is fine for subsequent events, not for rendering those components.
@@ -116,7 +505,7 @@ Components using Composer **From page** require `pageContext` on the provider (o
116
505
 
117
506
  Pass `pageContext` at provider/init for From-page components. Do not wait until after first paint. Fixed (static) page fields still attach to events when `pageContext` is omitted.
118
507
 
119
- The SDK maps Edge’s resolved tree into `layoutTree` and renders it. Edge already applies personalization and CMS projection — the site does not call CMS or Decision APIs for layout. After paint, the overlay calls `POST /v1/select` (or `selectVariants()`) then `/v1/resolve?variants=` so decisions stay on the edge (`clientPersonalization`).
508
+ The SDK maps Edge's resolved tree into `layoutTree` and renders it. Edge already applies personalization and CMS projection — the site does not call CMS or Decision APIs for layout. After paint, the overlay calls `POST /v1/select` (or `selectVariants()`) then `/v1/resolve?variants=` so decisions stay on the edge (`clientPersonalization`).
120
509
 
121
510
  ## Runtime queries
122
511
 
@@ -143,7 +532,7 @@ const hits = await queryContent({
143
532
  });
144
533
  ```
145
534
 
146
- Only entities published with that route can be queried, and visitors only see published content. Results are plain rows, not `fields` objects — they are not canvas-editable. Also available from `@amplifyup/sdk/server`. See the [runtime queries guide](../../docs/sdk-site-setup.md#8-runtime-content-queries).
535
+ Only entities published with that route can be queried, and visitors only see published content. Results are plain rows — not `fields` envelopes, not canvas-editable. Also available from `@amplifyup/sdk/server`. See the [runtime queries guide](../../docs/sdk-site-setup.md#8-runtime-content-queries).
147
536
 
148
537
  ## Track events
149
538
 
@@ -167,4 +556,4 @@ React context: `useAmplifyUp()` → `{ pageConfig, loading, error, source, meta
167
556
 
168
557
  ## License
169
558
 
170
- MIT
559
+ MIT
@@ -1,4 +1,4 @@
1
- import * as React from 'react';
1
+ import * as react from 'react';
2
2
  import { ReactNode } from 'react';
3
3
  import * as react_jsx_runtime from 'react/jsx-runtime';
4
4
  import { P as PageConfig } from './types-Db_eLrfU.js';
@@ -19,11 +19,30 @@ type Field<T> = {
19
19
  value: T;
20
20
  name: string;
21
21
  };
22
+ /** Portable entry ref after normalize (`id` required). */
23
+ type PortableRef = {
24
+ id: string;
25
+ type?: string;
26
+ };
27
+ /**
28
+ * Repeater envelope: each item is a `Fields` bag.
29
+ * Paths are dotted indexes (`slides.0.title`).
30
+ */
31
+ type CollectionField<T extends Record<string, unknown>> = {
32
+ value: Array<Fields<T>>;
33
+ name: string;
34
+ };
22
35
  type IsPlainObject<T> = T extends object ? T extends readonly unknown[] ? false : T extends ImageValue ? false : true : false;
36
+ /** Resolved / authored entry refs carry `id` (see `stampPlatformIdentity`). */
37
+ type IsEntryRefItem<T> = T extends {
38
+ id: string;
39
+ } ? true : false;
40
+ type AsRecord<T> = T extends Record<string, unknown> ? T : Record<string, unknown>;
41
+ type FieldsProp<T> = NonNullable<T> extends ImageValue ? Field<T | null> : NonNullable<T> extends readonly (infer U)[] ? IsEntryRefItem<U> extends true ? Field<T> : IsPlainObject<U> extends true ? CollectionField<AsRecord<U>> : Field<T> : IsPlainObject<NonNullable<T>> extends true ? Fields<NonNullable<T>> : Field<T>;
23
42
  type Fields<T> = {
24
43
  readonly [FIELDS_BRAND]: true;
25
44
  } & {
26
- [K in keyof T]-?: NonNullable<T[K]> extends ImageValue ? Field<T[K] | null> : IsPlainObject<NonNullable<T[K]>> extends true ? Fields<NonNullable<T[K]>> : Field<T[K]>;
45
+ [K in keyof T]-?: FieldsProp<T[K]>;
27
46
  };
28
47
  /** Props passed into site components. Raw CMS bags are not assignable. */
29
48
  type LayoutComponentProps<T extends Record<string, unknown> = Record<string, unknown>> = {
@@ -69,6 +88,6 @@ interface AmplifyRendererProps {
69
88
  composerPreview?: boolean;
70
89
  }
71
90
  declare function AmplifyRendererInner({ renderComponent, pageConfig: serverPageConfig, loadingComponent, errorComponent, emptyComponent, composerPreview, }: AmplifyRendererProps): react_jsx_runtime.JSX.Element | null;
72
- declare const AmplifyRenderer: React.MemoExoticComponent<typeof AmplifyRendererInner>;
91
+ declare const AmplifyRenderer: react.MemoExoticComponent<typeof AmplifyRendererInner>;
73
92
 
74
- export { AmplifyRenderer as A, type Field as F, type ImageValue as I, type LayoutComponentProps as L, isBrandedFields as a, type Fields as b, isFieldEnvelope as i, toFieldEnvelopes as t };
93
+ export { AmplifyRenderer as A, type CollectionField as C, type Field as F, type ImageValue as I, type LayoutComponentProps as L, type PortableRef as P, type Fields as a, isBrandedFields as b, isFieldEnvelope as i, toFieldEnvelopes as t };
@@ -1,4 +1,4 @@
1
- import * as React from 'react';
1
+ import * as react from 'react';
2
2
  import { ReactNode } from 'react';
3
3
  import * as react_jsx_runtime from 'react/jsx-runtime';
4
4
  import { P as PageConfig } from './types-Db_eLrfU.mjs';
@@ -19,11 +19,30 @@ type Field<T> = {
19
19
  value: T;
20
20
  name: string;
21
21
  };
22
+ /** Portable entry ref after normalize (`id` required). */
23
+ type PortableRef = {
24
+ id: string;
25
+ type?: string;
26
+ };
27
+ /**
28
+ * Repeater envelope: each item is a `Fields` bag.
29
+ * Paths are dotted indexes (`slides.0.title`).
30
+ */
31
+ type CollectionField<T extends Record<string, unknown>> = {
32
+ value: Array<Fields<T>>;
33
+ name: string;
34
+ };
22
35
  type IsPlainObject<T> = T extends object ? T extends readonly unknown[] ? false : T extends ImageValue ? false : true : false;
36
+ /** Resolved / authored entry refs carry `id` (see `stampPlatformIdentity`). */
37
+ type IsEntryRefItem<T> = T extends {
38
+ id: string;
39
+ } ? true : false;
40
+ type AsRecord<T> = T extends Record<string, unknown> ? T : Record<string, unknown>;
41
+ type FieldsProp<T> = NonNullable<T> extends ImageValue ? Field<T | null> : NonNullable<T> extends readonly (infer U)[] ? IsEntryRefItem<U> extends true ? Field<T> : IsPlainObject<U> extends true ? CollectionField<AsRecord<U>> : Field<T> : IsPlainObject<NonNullable<T>> extends true ? Fields<NonNullable<T>> : Field<T>;
23
42
  type Fields<T> = {
24
43
  readonly [FIELDS_BRAND]: true;
25
44
  } & {
26
- [K in keyof T]-?: NonNullable<T[K]> extends ImageValue ? Field<T[K] | null> : IsPlainObject<NonNullable<T[K]>> extends true ? Fields<NonNullable<T[K]>> : Field<T[K]>;
45
+ [K in keyof T]-?: FieldsProp<T[K]>;
27
46
  };
28
47
  /** Props passed into site components. Raw CMS bags are not assignable. */
29
48
  type LayoutComponentProps<T extends Record<string, unknown> = Record<string, unknown>> = {
@@ -69,6 +88,6 @@ interface AmplifyRendererProps {
69
88
  composerPreview?: boolean;
70
89
  }
71
90
  declare function AmplifyRendererInner({ renderComponent, pageConfig: serverPageConfig, loadingComponent, errorComponent, emptyComponent, composerPreview, }: AmplifyRendererProps): react_jsx_runtime.JSX.Element | null;
72
- declare const AmplifyRenderer: React.MemoExoticComponent<typeof AmplifyRendererInner>;
91
+ declare const AmplifyRenderer: react.MemoExoticComponent<typeof AmplifyRendererInner>;
73
92
 
74
- export { AmplifyRenderer as A, type Field as F, type ImageValue as I, type LayoutComponentProps as L, isBrandedFields as a, type Fields as b, isFieldEnvelope as i, toFieldEnvelopes as t };
93
+ export { AmplifyRenderer as A, type CollectionField as C, type Field as F, type ImageValue as I, type LayoutComponentProps as L, type PortableRef as P, type Fields as a, isBrandedFields as b, isFieldEnvelope as i, toFieldEnvelopes as t };
@@ -1,4 +1,4 @@
1
1
  import 'react';
2
2
  import 'react/jsx-runtime';
3
- export { A as AmplifyRenderer } from './AmplifyRenderer-W7eqltSj.mjs';
3
+ export { A as AmplifyRenderer } from './AmplifyRenderer-TawbUIKZ.mjs';
4
4
  import './types-Db_eLrfU.mjs';
@@ -1,4 +1,4 @@
1
1
  import 'react';
2
2
  import 'react/jsx-runtime';
3
- export { A as AmplifyRenderer } from './AmplifyRenderer-BQ8-H2H4.js';
3
+ export { A as AmplifyRenderer } from './AmplifyRenderer-Bwob7ICJ.js';
4
4
  import './types-Db_eLrfU.js';
@@ -430,6 +430,21 @@ function isPortableImageShape(value) {
430
430
  if (!value || typeof value !== "object" || Array.isArray(value)) return false;
431
431
  return "url" in value;
432
432
  }
433
+ function isEntryRefItem(value) {
434
+ if (!value || typeof value !== "object" || Array.isArray(value)) return false;
435
+ const rec = value;
436
+ return typeof rec.id === "string" && rec.id.trim().length > 0 || typeof rec._id === "string" && rec._id.trim().length > 0 || typeof rec.docId === "string" && rec.docId.trim().length > 0;
437
+ }
438
+ function isRepeaterArray(value) {
439
+ if (!Array.isArray(value) || value.length === 0) return false;
440
+ const sample = value.find((item) => item != null);
441
+ if (sample === void 0) return false;
442
+ if (typeof sample !== "object" || Array.isArray(sample)) return false;
443
+ if (isPortableImageShape(sample)) return false;
444
+ if (isEntryRefItem(sample)) return false;
445
+ if (sample.$$typeof) return false;
446
+ return true;
447
+ }
433
448
  function joinPath(prefix, key) {
434
449
  return prefix ? `${prefix}.${key}` : key;
435
450
  }
@@ -446,9 +461,21 @@ function toFieldEnvelopes(source, prefix = "") {
446
461
  for (const [key, raw] of Object.entries(input)) {
447
462
  const path = joinPath(prefix, key);
448
463
  if (isFieldEnvelope(raw)) {
464
+ const inner = raw.value;
465
+ if (isRepeaterArray(inner)) {
466
+ out[key] = {
467
+ value: envelopeRepeaterItems(inner, raw.name || path),
468
+ name: raw.name || path
469
+ };
470
+ continue;
471
+ }
449
472
  out[key] = { value: raw.value ?? null, name: raw.name || path };
450
473
  continue;
451
474
  }
475
+ if (isRepeaterArray(raw)) {
476
+ out[key] = { value: envelopeRepeaterItems(raw, path), name: path };
477
+ continue;
478
+ }
452
479
  if (raw && typeof raw === "object" && !Array.isArray(raw) && !isPortableImageShape(raw) && !raw.$$typeof) {
453
480
  out[key] = toFieldEnvelopes(raw, path);
454
481
  continue;
@@ -457,6 +484,16 @@ function toFieldEnvelopes(source, prefix = "") {
457
484
  }
458
485
  return brandFields(out);
459
486
  }
487
+ function envelopeRepeaterItems(items, prefix) {
488
+ return items.map((item, i) => {
489
+ const path = joinPath(prefix, String(i));
490
+ if (isBrandedFields(item)) return item;
491
+ if (item && typeof item === "object" && !Array.isArray(item)) {
492
+ return toFieldEnvelopes(item, path);
493
+ }
494
+ return toFieldEnvelopes({}, path);
495
+ });
496
+ }
460
497
  function attachFieldEnvelopes(props) {
461
498
  const raw = props && typeof props === "object" ? { ...props } : {};
462
499
  if (isBrandedFields(raw.fields)) return raw;
@@ -483,6 +520,14 @@ function envelopePageConfig(page) {
483
520
  }
484
521
  function collectFieldPatches(props) {
485
522
  const out = [];
523
+ const walkRepeater = (items, prefix) => {
524
+ items.forEach((item, i) => {
525
+ const itemPath = joinPath(prefix, String(i));
526
+ if (isBrandedFields(item) || item && typeof item === "object" && !Array.isArray(item) && !isFieldEnvelope(item)) {
527
+ walk(item, itemPath);
528
+ }
529
+ });
530
+ };
486
531
  const walk = (obj, prefix) => {
487
532
  for (const [key, raw] of Object.entries(obj)) {
488
533
  if (key === "fields" && raw && typeof raw === "object" && !Array.isArray(raw) && !isFieldEnvelope(raw)) {
@@ -491,9 +536,17 @@ function collectFieldPatches(props) {
491
536
  }
492
537
  const path = joinPath(prefix, key);
493
538
  if (isFieldEnvelope(raw)) {
539
+ if (isRepeaterArray(raw.value)) {
540
+ walkRepeater(raw.value, raw.name || path);
541
+ continue;
542
+ }
494
543
  out.push({ name: raw.name || path, value: raw.value });
495
544
  continue;
496
545
  }
546
+ if (isRepeaterArray(raw)) {
547
+ walkRepeater(raw, path);
548
+ continue;
549
+ }
497
550
  if (raw && typeof raw === "object" && !Array.isArray(raw) && !isPortableImageShape(raw) && !raw.$$typeof) {
498
551
  walk(raw, path);
499
552
  continue;
@@ -3234,6 +3287,16 @@ var ComposerFieldStore = class {
3234
3287
  if (set.size === 0) this.listeners.delete(name);
3235
3288
  };
3236
3289
  }
3290
+ keys() {
3291
+ return [...this.values.keys()];
3292
+ }
3293
+ delete(name) {
3294
+ if (!name || !this.values.has(name)) return;
3295
+ this.values.delete(name);
3296
+ const set = this.listeners.get(name);
3297
+ if (!set) return;
3298
+ for (const fn of set) fn();
3299
+ }
3237
3300
  };
3238
3301
  var ComposerStoreContext = react.createContext(null);
3239
3302
  function matchesNode(eventNodeId, layoutNodeId) {
@@ -3254,6 +3317,13 @@ function ComposerProvider({
3254
3317
  if (!fieldName || typeof fieldName !== "string") return;
3255
3318
  if (!matchesNode(nodeId, layoutNodeId)) return;
3256
3319
  store.set(fieldName, value);
3320
+ if (value && typeof value === "object") {
3321
+ for (const patch of collectFieldPatches(
3322
+ toFieldEnvelopes({ [fieldName]: value })
3323
+ )) {
3324
+ store.set(patch.name, patch.value);
3325
+ }
3326
+ }
3257
3327
  };
3258
3328
  const applyProps = (nextProps, nodeId) => {
3259
3329
  if (!nextProps || typeof nextProps !== "object" || Array.isArray(nextProps)) {