@aglyn/plugins-mui 1.0.0-beta.215 → 1.0.0-beta.216

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 (57) hide show
  1. package/package.json +7 -7
  2. package/src/lib/components/collection-author-links.d.ts +41 -0
  3. package/src/lib/components/collection-author-links.js +135 -0
  4. package/src/lib/components/collection-author-links.js.map +1 -0
  5. package/src/lib/components/collection-author-profile.d.ts +67 -0
  6. package/src/lib/components/collection-author-profile.js +269 -0
  7. package/src/lib/components/collection-author-profile.js.map +1 -0
  8. package/src/lib/components/collection-categories.d.ts +52 -0
  9. package/src/lib/components/collection-categories.js +163 -0
  10. package/src/lib/components/collection-categories.js.map +1 -0
  11. package/src/lib/components/collection-common.d.ts +104 -0
  12. package/src/lib/components/collection-common.js +95 -0
  13. package/src/lib/components/collection-common.js.map +1 -0
  14. package/src/lib/components/collection-entries.d.ts +137 -0
  15. package/src/lib/components/collection-entries.js +358 -0
  16. package/src/lib/components/collection-entries.js.map +1 -0
  17. package/src/lib/components/collection-entry-author.d.ts +81 -0
  18. package/src/lib/components/collection-entry-author.js +259 -0
  19. package/src/lib/components/collection-entry-author.js.map +1 -0
  20. package/src/lib/components/collection-entry-body.d.ts +44 -0
  21. package/src/lib/components/collection-entry-body.js +328 -0
  22. package/src/lib/components/collection-entry-body.js.map +1 -0
  23. package/src/lib/components/collection-entry-meta.d.ts +88 -0
  24. package/src/lib/components/collection-entry-meta.js +331 -0
  25. package/src/lib/components/collection-entry-meta.js.map +1 -0
  26. package/src/lib/components/collection-presets.d.ts +18 -0
  27. package/src/lib/components/collection-presets.js +328 -0
  28. package/src/lib/components/collection-presets.js.map +1 -0
  29. package/src/lib/components/collection-related.d.ts +101 -0
  30. package/src/lib/components/collection-related.js +536 -0
  31. package/src/lib/components/collection-related.js.map +1 -0
  32. package/src/lib/components/collection-search-box.d.ts +92 -0
  33. package/src/lib/components/collection-search-box.js +334 -0
  34. package/src/lib/components/collection-search-box.js.map +1 -0
  35. package/src/lib/components/collection-search-fuse.d.ts +28 -0
  36. package/src/lib/components/collection-search-fuse.js +28 -0
  37. package/src/lib/components/collection-search-fuse.js.map +1 -0
  38. package/src/lib/components/collection-search.d.ts +62 -0
  39. package/src/lib/components/collection-search.js +140 -0
  40. package/src/lib/components/collection-search.js.map +1 -0
  41. package/src/lib/components/collection-share.d.ts +31 -0
  42. package/src/lib/components/collection-share.js +154 -0
  43. package/src/lib/components/collection-share.js.map +1 -0
  44. package/src/lib/components/collection.d.ts +19 -497
  45. package/src/lib/components/collection.js +19 -2982
  46. package/src/lib/components/collection.js.map +1 -1
  47. package/src/lib/components/image-list.d.ts +2 -2
  48. package/src/lib/components/image-list.js +4 -6
  49. package/src/lib/components/image-list.js.map +1 -1
  50. package/src/lib/element-tier.d.ts +9 -1
  51. package/src/lib/element-tier.js +12 -1
  52. package/src/lib/element-tier.js.map +1 -1
  53. package/src/lib/plugin.js +20 -12
  54. package/src/lib/plugin.js.map +1 -1
  55. package/src/lib/utils/to-count.d.ts +25 -0
  56. package/src/lib/utils/to-count.js +30 -0
  57. package/src/lib/utils/to-count.js.map +1 -0
@@ -0,0 +1,536 @@
1
+ import { _ as _extends } from "@swc/helpers/_/_extends";
2
+ import { _ as _object_without_properties_loose } from "@swc/helpers/_/_object_without_properties_loose";
3
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
4
+ /**
5
+ * @license
6
+ * Copyright 2026 Aglyn LLC
7
+ *
8
+ * Licensed under the Apache License, Version 2.0 (the "License");
9
+ * you may not use this file except in compliance with the License.
10
+ * You may obtain a copy of the License at
11
+ *
12
+ * http://www.apache.org/licenses/LICENSE-2.0
13
+ *
14
+ * Unless required by applicable law or agreed to in writing, software
15
+ * distributed under the License is distributed on an "AS IS" BASIS,
16
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
17
+ * See the License for the specific language governing permissions and
18
+ * limitations under the License.
19
+ */ import * as Aglyn from "@aglyn/aglyn";
20
+ import { mdiNewspaperVariantOutline } from "@aglyn/shared-data-mdi";
21
+ import { AppLink } from "@aglyn/shared-ui-jsx";
22
+ import Box from "@mui/material/Box";
23
+ import Chip from "@mui/material/Chip";
24
+ import MuiStack from "@mui/material/Stack";
25
+ import Typography from "@mui/material/Typography";
26
+ import { forwardRef, useContext } from "react";
27
+ import { BUNDLE_ID } from "../constants/bundle-common.js";
28
+ // One coercion for every authored count in this bundle (AGL-1457) — number
29
+ // fields round-trip as strings, and a second parser here would drift.
30
+ import { toCount } from "../utils/to-count.js";
31
+ import { RELATED_SAMPLE_PUBLISHED_AT, RELATED_SAMPLE_CATEGORY } from "./collection-common.js";
32
+ // The theme's own type steps (AGL-2486), shared with the Typography element
33
+ // rather than a second list here — one rung list, one place to extend.
34
+ import { typographyVariants } from "./typography.js";
35
+ // Persisted component id (AGL-551/582); the compose pipeline references it
36
+ // through the @aglyn/aglyn constant. Never rename.
37
+ export const RELATED_ID = Aglyn.COLLECTION_RELATED_COMPONENT_ID;
38
+ /** The frame's 3-up (Figma 170:242) when nothing usable is authored. */ const RELATED_DEFAULT_COLUMNS = 3;
39
+ /**
40
+ * The cover's PROPORTION in the card grid.
41
+ *
42
+ * ONE ratio for every card, rather than each record's own shape: the titles
43
+ * under the covers only line up when the covers are the same height, and a
44
+ * grid of honest shapes is a ragged grid.
45
+ *
46
+ * The ratio is the shape the console asks authors for — 1200 x 630, the same
47
+ * image that heads the entry and doubles as its share card — so a cover
48
+ * authored as instructed loses nothing to the crop. The frame's own card is
49
+ * 445 x 180 (Figma 170:243), which is 2.47 against that art's 1.90: held
50
+ * there, `objectFit: cover` discarded 23% of every card's picture at every
51
+ * width. Cover art is usually a composed lockup, so what it discards is the
52
+ * title set on it.
53
+ *
54
+ * Before the ratio it was a fixed 180px height, which was the frame's number
55
+ * without the frame's shape: a card is 445 wide there, the grid's columns
56
+ * grow with the viewport and 180 does not, so every extra pixel of width
57
+ * flattened the box further — 3.57:1 by a 2000px viewport. A ratio holds the
58
+ * shape at every width; this ratio holds the picture as well.
59
+ */ const RELATED_COVER_ASPECT = '1200 / 630';
60
+ /**
61
+ * The card's category chip (AGL-1457). ONE fixed token pair for every
62
+ * category: colour-coding a chip per category needs conditional styling,
63
+ * which is not expressible yet (AGL-1307), and inventing a palette here
64
+ * would have to be unpicked when it is. Same shape as the tag chips Entry
65
+ * Meta already renders, so the two blocks read as one vocabulary.
66
+ */ const relatedChipSx = {
67
+ alignSelf: 'flex-start',
68
+ borderColor: 'divider',
69
+ color: 'text.secondary',
70
+ '& .MuiChip-label': {
71
+ // 9px, the frame's own inset on a 69 x 24 chip.
72
+ px: 1.125,
73
+ // The frame sets this label in the same uppercase, letter-spaced step as
74
+ // the kicker above an article title, which is what `overline` IS in the
75
+ // site theme. Naming the step keeps the chip on the theme's scale instead
76
+ // of a hand-written size that drifts the moment the theme moves.
77
+ typography: 'overline',
78
+ // `size="small"` gives the chip the frame's 24px height, and `overline`
79
+ // carries a 2.66 line-height meant for a standalone kicker with room
80
+ // around it. Inside a fixed-height chip the box sets the height, so the
81
+ // line collapses to it and the label centres.
82
+ lineHeight: 1
83
+ }
84
+ };
85
+ /** The heading step the block has always emitted. */ const RELATED_DEFAULT_HEADING_VARIANT = 'h5';
86
+ /** The card-title step the block has always emitted. */ const RELATED_DEFAULT_TITLE_VARIANT = 'subtitle1';
87
+ /**
88
+ * Titles for the sample cards. Deliberately unmistakable as copy: the author
89
+ * is looking at a LAYOUT, and a plausible-looking headline here is one an
90
+ * author can mistake for a post that exists — or worse, design around.
91
+ */ const RELATED_SAMPLE_TITLES = [
92
+ 'Sample related post',
93
+ 'A second sample post',
94
+ 'A third sample post'
95
+ ];
96
+ /** Excerpt text for the sample cards; one line, so the card keeps its shape. */ const RELATED_SAMPLE_EXCERPT = 'Each post’s own excerpt reads here, trimmed to a line or two.';
97
+ /**
98
+ * Stand-in posts for editing surfaces (AGL-2486).
99
+ *
100
+ * Related posts are resolved FROM the routed entry, and a besigner canvas has
101
+ * no routed entry — so the block used to draw a one-line dashed strip and the
102
+ * author styled a layout they could not see. The sample is the block's REAL
103
+ * markup, at the author's own settings, which is the only way the canvas can
104
+ * answer "what will this look like".
105
+ *
106
+ * It exists only where {@link ScreenLinkContext.suppressNavigation} is on —
107
+ * the besigner canvas and Preview. A published render reaches this function
108
+ * on no path at all, so no visitor can ever be shown a post that does not
109
+ * exist.
110
+ *
111
+ * The count follows the author's own `limit` so a 6-post rail at 3 columns
112
+ * previews as two rows, and is bounded by the same ceiling the server
113
+ * applies rather than a second one.
114
+ */ const sampleRelatedEntries = (limit, dateFormat)=>{
115
+ const count = Math.min(// `|| default` also rejects 0, which `toCount` would otherwise accept and
116
+ // preview as an empty rail — the one thing the sample exists to avoid.
117
+ toCount(limit, Aglyn.COLLECTION_RELATED_DEFAULT_LIMIT) || Aglyn.COLLECTION_RELATED_DEFAULT_LIMIT, Aglyn.COLLECTION_RELATED_MAX);
118
+ const date = Aglyn.formatCollectionEntryDate(RELATED_SAMPLE_PUBLISHED_AT, dateFormat);
119
+ return Array.from({
120
+ length: count
121
+ }, (_, index)=>{
122
+ var _RELATED_SAMPLE_TITLES_index;
123
+ return {
124
+ title: (_RELATED_SAMPLE_TITLES_index = RELATED_SAMPLE_TITLES[index]) != null ? _RELATED_SAMPLE_TITLES_index : `Sample related post ${index + 1}`,
125
+ // Empty, not a plausible path: every surface that renders the sample
126
+ // suppresses navigation, so the title is text rather than an anchor and
127
+ // this is never read. A real-looking href would be a link to nowhere the
128
+ // moment that stopped being true.
129
+ url: '',
130
+ date,
131
+ category: RELATED_SAMPLE_CATEGORY,
132
+ excerpt: RELATED_SAMPLE_EXCERPT
133
+ };
134
+ });
135
+ };
136
+ /**
137
+ * Lists other entries of the same collection sharing the current entry's
138
+ * category or a tag (AGL-582). The tenant stamps `entries` at compose time
139
+ * on entry renders; without them the published site renders nothing and an
140
+ * editing surface renders sample cards (AGL-2486).
141
+ *
142
+ * Two layouts (AGL-1457). `list` is the plain-link list that has always
143
+ * shipped and stays the default — the block is live on every blog entry, so
144
+ * a new default would restyle published pages nobody asked about. `cards` is
145
+ * the article frame's grid: cover, category chip, title, at an author-set
146
+ * column count.
147
+ *
148
+ * The block owns its markup, so nothing inside a card is a node the Styles
149
+ * panel can select. Every part it renders therefore needs an attribute, or
150
+ * it is unauthorable by construction — which is what the type steps and the
151
+ * `show*` switches are (AGL-2486).
152
+ */ const CollectionRelated = /*#__PURE__*/ forwardRef((props, ref)=>{
153
+ // `limit` and `dateFormat` are compose-time: the tenant resolves them
154
+ // while stamping `entries`; strip them so they never hit the DOM. The
155
+ // rest are read here, so they must not reach it either.
156
+ const { heading, limit, entries, showCover, layout, columns, headingVariant, titleVariant, showDate, dateFormat, showCategory, showExcerpt } = props, rest = _object_without_properties_loose(props, [
157
+ "heading",
158
+ "limit",
159
+ "entries",
160
+ "showCover",
161
+ "layout",
162
+ "columns",
163
+ "headingVariant",
164
+ "titleVariant",
165
+ "showDate",
166
+ "dateFormat",
167
+ "showCategory",
168
+ "showExcerpt"
169
+ ]);
170
+ // Node styles ride the renderer-merged sx; recompose (stack.ts pattern).
171
+ const nodeSx = Array.isArray(props['sx']) ? props['sx'] : [
172
+ props['sx']
173
+ ];
174
+ const { suppressNavigation } = useContext(Aglyn.ScreenLinkContext);
175
+ // The resolver every other surface shares (AGL-1215): a stamped `media:`
176
+ // reference becomes a CDN URL HERE, not in the document, so one
177
+ // reference keeps working across sites. Called before the early return —
178
+ // it is a hook.
179
+ const { hostId } = Aglyn.useSite();
180
+ // An editing surface has no routed entry, so nothing is stamped and the
181
+ // block used to be the one part of the page that was not WYSIWYG. The
182
+ // sample stands in for exactly that gap, and only there.
183
+ const sample = !(entries == null ? void 0 : entries.length) && Boolean(suppressNavigation);
184
+ const items = (entries == null ? void 0 : entries.length) ? entries : sample ? sampleRelatedEntries(limit, Aglyn.normalizeCollectionEntryDateFormat(dateFormat)) : [];
185
+ if (!items.length) return /*#__PURE__*/ _jsx(Box, _extends({
186
+ ref: ref
187
+ }, rest));
188
+ const title = heading != null ? heading : 'Related articles';
189
+ const headingNode = title ? /*#__PURE__*/ _jsx(Typography, {
190
+ variant: headingVariant || RELATED_DEFAULT_HEADING_VARIANT,
191
+ component: "h2",
192
+ children: title
193
+ }) : null;
194
+ /**
195
+ * The card title, as a HEADING in the site theme.
196
+ *
197
+ * It used to be a bare `AppLink`, which is MUI's `Link` — `primary.main`
198
+ * and `underline="always"` — so the one thing a related card is FOR read
199
+ * as a default browser link on a themed page, at no heading level, and
200
+ * with no attribute able to touch it. The type step now comes from the
201
+ * theme's own rungs and the anchor inherits the colour, exactly as the
202
+ * Entry Author card's byline link does.
203
+ *
204
+ * `linked` is false wherever the whole card is already the anchor: an
205
+ * `<a>` inside an `<a>` is invalid markup that browsers silently unnest.
206
+ */ const titleNode = (entry, linked)=>/*#__PURE__*/ _jsx(Typography, {
207
+ variant: titleVariant || RELATED_DEFAULT_TITLE_VARIANT,
208
+ component: "h3",
209
+ children: linked && !suppressNavigation && entry.url ? /*#__PURE__*/ _jsx(AppLink, {
210
+ href: entry.url,
211
+ sx: {
212
+ color: 'inherit'
213
+ },
214
+ underline: "hover",
215
+ children: entry.title
216
+ }) : entry.title
217
+ });
218
+ const dateNode = (entry)=>showDate !== false && entry.date ? /*#__PURE__*/ _jsx(Typography, {
219
+ variant: "caption",
220
+ sx: {
221
+ color: 'text.secondary'
222
+ },
223
+ children: entry.date
224
+ }) : null;
225
+ const excerptNode = (entry)=>showExcerpt && entry.excerpt ? /*#__PURE__*/ _jsx(Typography, {
226
+ variant: "body2",
227
+ sx: {
228
+ color: 'text.secondary'
229
+ },
230
+ children: entry.excerpt
231
+ }) : null;
232
+ /**
233
+ * The cover, when the author asked for one AND the entry has one. An
234
+ * entry without a cover gets no box at all rather than a placeholder:
235
+ * the related list is a real feed, and a row of grey rectangles is worse
236
+ * than a row of titles.
237
+ *
238
+ * The sample cards are the exception, and for the opposite reason: they
239
+ * stand in for posts that do not exist yet, so the cover SLOT is what
240
+ * the author is looking at. A card grid previewing without its images is
241
+ * not the grid being styled.
242
+ */ const coverNode = (entry)=>{
243
+ const src = showCover ? Aglyn.resolveMediaSrc(entry.coverImage, {
244
+ hostId
245
+ }) : undefined;
246
+ if (!src) {
247
+ if (!sample || !showCover) return null;
248
+ return /*#__PURE__*/ _jsx(Box, {
249
+ "aria-hidden": true,
250
+ sx: {
251
+ width: '100%',
252
+ aspectRatio: RELATED_COVER_ASPECT,
253
+ borderRadius: 1,
254
+ backgroundColor: 'action.hover'
255
+ }
256
+ });
257
+ }
258
+ return /*#__PURE__*/ _jsx(Box, _extends({
259
+ component: "img",
260
+ src: src,
261
+ // The author's own description wins, and the title stays the
262
+ // fallback (AGL-2418). Unlike the entry hero and the event
263
+ // thumbnail — which render empty when nothing is authored — this
264
+ // cover is the LINK's own content, so it must carry an accessible
265
+ // name rather than go silent.
266
+ alt: Aglyn.renderedMediaAlt(entry.coverImageAlt, entry.title),
267
+ sx: {
268
+ display: 'block',
269
+ width: '100%',
270
+ aspectRatio: RELATED_COVER_ASPECT,
271
+ objectFit: 'cover',
272
+ borderRadius: 1
273
+ }
274
+ }, Aglyn.DEFERRED_IMAGE_ATTRIBUTES));
275
+ };
276
+ /**
277
+ * The notice that says these cards are not posts (AGL-2486). Editing
278
+ * surfaces only, and outside the card markup rather than inside it, so
279
+ * the author is styling the same tree the site will render.
280
+ */ const sampleNotice = sample ? /*#__PURE__*/ _jsx(Typography, {
281
+ variant: "caption",
282
+ sx: {
283
+ display: 'block',
284
+ p: 1,
285
+ border: '1px dashed',
286
+ borderColor: 'divider',
287
+ borderRadius: 1,
288
+ color: 'text.secondary',
289
+ fontFamily: 'system-ui, sans-serif'
290
+ },
291
+ children: 'Sample posts — entries sharing this entry’s category or tags ' + 'replace these on the published page'
292
+ }) : null;
293
+ if (layout === 'cards') {
294
+ // `toCount` rounds and rejects junk; `|| default` also rejects 0, which
295
+ // it would otherwise accept as a column count and emit `repeat(0, …)`.
296
+ const columnCount = toCount(columns, RELATED_DEFAULT_COLUMNS) || RELATED_DEFAULT_COLUMNS;
297
+ /**
298
+ * One card's contents, always with a PLAIN title: the caller wraps the
299
+ * whole card in the anchor, because a reader aiming at a 180px cover
300
+ * should not have to hit the line of text under it — and a second
301
+ * anchor around the title inside that one is markup browsers unnest.
302
+ */ const card = (entry)=>// 12px between cover, chip and title — the frame's rhythm (170:243
303
+ // puts the chip at y=192 under a cover ending at 180, and the title
304
+ // at y=228 under a chip ending at 216).
305
+ /*#__PURE__*/ _jsxs(MuiStack, {
306
+ spacing: 1.5,
307
+ children: [
308
+ coverNode(entry),
309
+ showCategory !== false && entry.category ? /*#__PURE__*/ _jsx(Chip, {
310
+ label: entry.category,
311
+ size: "small",
312
+ variant: "outlined",
313
+ sx: relatedChipSx
314
+ }) : null,
315
+ titleNode(entry, false),
316
+ excerptNode(entry),
317
+ dateNode(entry)
318
+ ]
319
+ });
320
+ return /*#__PURE__*/ _jsxs(Box, _extends({
321
+ ref: ref
322
+ }, rest, {
323
+ // MERGE, never replace (AGL-1450) — the node's slice arrives as the
324
+ // ARRAY `mergeSxProps` builds in leaf.tsx, so folding it into an
325
+ // object spreads numeric keys and discards every authored property
326
+ // while these defaults still apply. Defaults first, node's after.
327
+ sx: [
328
+ {
329
+ display: 'grid',
330
+ gridTemplateColumns: `repeat(${columnCount}, minmax(0, 1fr))`,
331
+ alignItems: 'start',
332
+ columnGap: 3,
333
+ rowGap: 4
334
+ },
335
+ ...nodeSx
336
+ ],
337
+ children: [
338
+ headingNode ? /*#__PURE__*/ _jsx(Box, {
339
+ sx: {
340
+ gridColumn: '1 / -1'
341
+ },
342
+ children: headingNode
343
+ }) : null,
344
+ sampleNotice ? /*#__PURE__*/ _jsx(Box, {
345
+ sx: {
346
+ gridColumn: '1 / -1'
347
+ },
348
+ children: sampleNotice
349
+ }) : null,
350
+ items.map((entry, index)=>{
351
+ const linked = !suppressNavigation && Boolean(entry.url);
352
+ return linked ? /*#__PURE__*/ _jsx(AppLink, {
353
+ href: entry.url,
354
+ underline: "none",
355
+ // A grid ITEM, so it has to be a block; `color: inherit`
356
+ // keeps the cover, chip and date reading as themed content
357
+ // rather than as the inside of a link.
358
+ sx: {
359
+ color: 'inherit',
360
+ display: 'block'
361
+ },
362
+ children: card(entry)
363
+ }, index) : /*#__PURE__*/ _jsx(Box, {
364
+ children: card(entry)
365
+ }, index);
366
+ })
367
+ ]
368
+ }));
369
+ }
370
+ return /*#__PURE__*/ _jsxs(MuiStack, _extends({
371
+ ref: ref,
372
+ spacing: 1.5
373
+ }, rest, {
374
+ children: [
375
+ headingNode,
376
+ sampleNotice,
377
+ items.map((entry, index)=>{
378
+ const meta = [
379
+ showDate !== false ? entry.date : '',
380
+ showCategory !== false ? entry.category : ''
381
+ ].filter(Boolean);
382
+ return /*#__PURE__*/ _jsxs(MuiStack, {
383
+ spacing: 0.25,
384
+ children: [
385
+ coverNode(entry),
386
+ titleNode(entry, true),
387
+ excerptNode(entry),
388
+ meta.length ? /*#__PURE__*/ _jsx(Typography, {
389
+ variant: "caption",
390
+ sx: {
391
+ color: 'text.secondary'
392
+ },
393
+ children: meta.join(' · ')
394
+ }) : null
395
+ ]
396
+ }, index);
397
+ })
398
+ ]
399
+ }));
400
+ });
401
+ CollectionRelated.displayName = 'AglynCollectionRelated';
402
+ export const collectionRelatedSchema = {
403
+ $id: RELATED_ID,
404
+ pluginId: BUNDLE_ID,
405
+ displayName: 'Related Posts',
406
+ description: "Other entries sharing this one's category or tags.",
407
+ category: Aglyn.ComponentCategory.DATA_DISPLAY,
408
+ icon: {
409
+ path: mdiNewspaperVariantOutline.path,
410
+ sx: {
411
+ color: 'secondary.main'
412
+ }
413
+ },
414
+ flags: {
415
+ selfClosing: Aglyn.FEATURE_FLAG.ENABLED
416
+ },
417
+ attributes: [
418
+ {
419
+ name: 'heading',
420
+ label: 'Heading',
421
+ description: 'Section heading (blank hides it).',
422
+ component: Aglyn.FieldComponentType.TEXT_FIELD
423
+ },
424
+ {
425
+ name: 'limit',
426
+ label: 'Limit',
427
+ description: 'Most related posts listed (default 3).',
428
+ component: Aglyn.FieldComponentType.TEXT_FIELD,
429
+ type: 'number'
430
+ },
431
+ {
432
+ name: 'layout',
433
+ label: 'Layout',
434
+ description: 'List keeps the plain links this block has always rendered. ' + 'Cards is the article layout: cover, category, title, in a grid.',
435
+ component: Aglyn.FieldComponentType.SELECT,
436
+ // Both values are REAL (AGL-1451/AGL-1453): `''` cannot survive a
437
+ // save, so an author who switched to Cards would have no route back.
438
+ // `list` is also the value the render falls back to, so naming it is
439
+ // the same choice, not a second one.
440
+ options: [
441
+ {
442
+ value: 'list',
443
+ label: 'List (default)'
444
+ },
445
+ {
446
+ value: 'cards',
447
+ label: 'Card grid'
448
+ }
449
+ ]
450
+ },
451
+ {
452
+ name: 'columns',
453
+ label: 'Columns',
454
+ description: 'Cards per row in the card grid. Default 3.',
455
+ component: Aglyn.FieldComponentType.TEXT_FIELD,
456
+ type: 'number',
457
+ // Meaningless on the list, where each post is its own row.
458
+ condition: {
459
+ when: 'layout',
460
+ is: 'cards'
461
+ }
462
+ },
463
+ {
464
+ name: 'showCover',
465
+ label: 'Show cover',
466
+ description: 'Show each post’s cover image. Posts without one show their ' + 'title alone rather than an empty box.',
467
+ component: Aglyn.FieldComponentType.SWITCH
468
+ },
469
+ {
470
+ name: 'headingVariant',
471
+ label: 'Heading style',
472
+ description: 'Which type step from the site theme the section heading above ' + 'the posts reads at. Default Heading 5.',
473
+ component: Aglyn.FieldComponentType.SELECT,
474
+ // The theme's own rungs, shared with the Typography element rather
475
+ // than a second list here: two lists is how a step added to the
476
+ // theme becomes reachable in one place and not the other.
477
+ options: [
478
+ ...typographyVariants
479
+ ]
480
+ },
481
+ {
482
+ name: 'titleVariant',
483
+ label: 'Title style',
484
+ description: 'Which type step from the site theme each post’s title reads at. ' + 'Default Subtitle 1; the title takes the surrounding text color ' + 'rather than the link color.',
485
+ component: Aglyn.FieldComponentType.SELECT,
486
+ options: [
487
+ ...typographyVariants
488
+ ]
489
+ },
490
+ {
491
+ name: 'showDate',
492
+ label: 'Show date',
493
+ description: 'Off drops each post’s published date. The posts stay in ' + 'newest-first order either way — this is about the card, not the ' + 'ordering.',
494
+ component: Aglyn.FieldComponentType.SWITCH,
495
+ // Unset means SHOWN in the renderer above, so the switch has to
496
+ // OPEN in that position or it lies about the block in front of
497
+ // the author — and the first click, which writes the `true` that
498
+ // was already in effect, changes nothing and reads as a dead
499
+ // control (AGL-2506).
500
+ initialValue: true
501
+ },
502
+ {
503
+ name: 'dateFormat',
504
+ label: 'Date format',
505
+ description: 'How each post’s published date reads on its card. Site default ' + 'is the format this block has always used.',
506
+ component: Aglyn.FieldComponentType.SELECT,
507
+ // The formats the pure layer knows how to produce, so the list
508
+ // cannot offer a shape nothing renders. Every value is REAL,
509
+ // including the do-nothing one (AGL-1451/AGL-1453).
510
+ options: [
511
+ ...Aglyn.COLLECTION_ENTRY_DATE_FORMAT_OPTIONS
512
+ ]
513
+ },
514
+ {
515
+ name: 'showCategory',
516
+ label: 'Show category',
517
+ description: 'Off drops the category — the chip above each card title, or the ' + 'second half of the line under each link on the list layout.',
518
+ component: Aglyn.FieldComponentType.SWITCH,
519
+ // Unset means SHOWN in the renderer above, so the switch has to
520
+ // OPEN in that position or it lies about the block in front of
521
+ // the author — and the first click, which writes the `true` that
522
+ // was already in effect, changes nothing and reads as a dead
523
+ // control (AGL-2506).
524
+ initialValue: true
525
+ },
526
+ {
527
+ name: 'showExcerpt',
528
+ label: 'Show excerpt',
529
+ description: 'Adds each post’s excerpt under its title. Posts with no excerpt ' + 'are unchanged rather than leaving a gap.',
530
+ component: Aglyn.FieldComponentType.SWITCH
531
+ }
532
+ ]
533
+ };
534
+ export { CollectionRelated };
535
+
536
+ //# sourceMappingURL=collection-related.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../../../../../../libs/plugins/mui/src/lib/components/collection-related.tsx"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport * as Aglyn from '@aglyn/aglyn'\nimport { mdiNewspaperVariantOutline } from '@aglyn/shared-data-mdi'\nimport { AppLink } from '@aglyn/shared-ui-jsx'\nimport Box from '@mui/material/Box'\nimport Chip from '@mui/material/Chip'\nimport MuiStack, { type StackProps } from '@mui/material/Stack'\nimport Typography from '@mui/material/Typography'\nimport { forwardRef, useContext } from 'react'\nimport { BUNDLE_ID } from '../constants/bundle-common'\n// One coercion for every authored count in this bundle (AGL-1457) — number\n// fields round-trip as strings, and a second parser here would drift.\nimport { toCount } from '../utils/to-count'\nimport {\n RELATED_SAMPLE_PUBLISHED_AT,\n RELATED_SAMPLE_CATEGORY,\n} from './collection-common'\n// The theme's own type steps (AGL-2486), shared with the Typography element\n// rather than a second list here — one rung list, one place to extend.\nimport { typographyVariants } from './typography'\n\n// Persisted component id (AGL-551/582); the compose pipeline references it\n// through the @aglyn/aglyn constant. Never rename.\nexport const RELATED_ID: Aglyn.ComponentId =\n Aglyn.COLLECTION_RELATED_COMPONENT_ID\n\n/** How the related posts are laid out (AGL-1457). */\nexport type CollectionRelatedLayout = 'list' | 'cards'\n\n/** One related post as stamped by the compose pipeline (AGL-582). */\nexport interface CollectionRelatedProps extends StackProps {\n /** Section heading; empty string hides it. */\n heading?: string\n /** Compose-time: most related posts listed (default 3). */\n limit?: number | string\n /**\n * Emit each entry's cover image (AGL-1457). OFF is the default and the\n * shipped behaviour — the block is live on every blog entry, so turning\n * covers on by default would restyle published pages nobody asked about.\n */\n showCover?: boolean\n /**\n * `list` (default) is the plain-link list that has always shipped; `cards`\n * is the article frame's grid of cover + category chip + title.\n */\n layout?: CollectionRelatedLayout\n /** Columns in the `cards` grid (default 3, the frame's 3-up). */\n columns?: number | string\n /**\n * Type step the section heading reads at (default `h5`). One of the\n * theme's own rungs, shared with the Typography element — a heading sized\n * by hand in the Styles panel is the pixel-typing the tokens exist to stop.\n */\n headingVariant?: string\n /**\n * Type step each card title reads at (default `subtitle1`, what the block\n * has always emitted). The title is the block's own markup, so the Styles\n * panel — which edits the node's root — cannot reach it; this is the only\n * handle on it.\n */\n titleVariant?: string\n /** Show each post's published date. On is the shipped behaviour. */\n showDate?: boolean\n /**\n * How each card's published date reads (AGL-1459) — a COMPOSE-TIME prop\n * like {@link limit}: it is answered in `expandCollectionRelated`, where\n * the timestamp still exists, and never reaches the DOM. Blank, or\n * `default`, is the locale date the block has always emitted.\n *\n * Read here too, but only to date the SAMPLE cards: an editing surface has\n * no routed entry and therefore no server fill, so the picker would\n * otherwise preview nothing.\n */\n dateFormat?: Aglyn.CollectionEntryDateFormat\n /** Show each post's category. On is the shipped behaviour. */\n showCategory?: boolean\n /**\n * Show each post's excerpt. OFF is the default and the shipped behaviour:\n * the excerpt has been stamped onto every related item since AGL-582 and\n * rendered by nothing, so turning it on by default would add a paragraph\n * to every published entry nobody asked about.\n */\n showExcerpt?: boolean\n /**\n * Server-stamped related posts (`expandCollectionRelated`); never set by\n * hand — the tenant computes it from the current entry's category/tags.\n */\n entries?: Aglyn.CollectionRelatedItem[]\n}\n\n/** The frame's 3-up (Figma 170:242) when nothing usable is authored. */\nconst RELATED_DEFAULT_COLUMNS = 3\n\n/**\n * The cover's PROPORTION in the card grid.\n *\n * ONE ratio for every card, rather than each record's own shape: the titles\n * under the covers only line up when the covers are the same height, and a\n * grid of honest shapes is a ragged grid.\n *\n * The ratio is the shape the console asks authors for — 1200 x 630, the same\n * image that heads the entry and doubles as its share card — so a cover\n * authored as instructed loses nothing to the crop. The frame's own card is\n * 445 x 180 (Figma 170:243), which is 2.47 against that art's 1.90: held\n * there, `objectFit: cover` discarded 23% of every card's picture at every\n * width. Cover art is usually a composed lockup, so what it discards is the\n * title set on it.\n *\n * Before the ratio it was a fixed 180px height, which was the frame's number\n * without the frame's shape: a card is 445 wide there, the grid's columns\n * grow with the viewport and 180 does not, so every extra pixel of width\n * flattened the box further — 3.57:1 by a 2000px viewport. A ratio holds the\n * shape at every width; this ratio holds the picture as well.\n */\nconst RELATED_COVER_ASPECT = '1200 / 630'\n\n/**\n * The card's category chip (AGL-1457). ONE fixed token pair for every\n * category: colour-coding a chip per category needs conditional styling,\n * which is not expressible yet (AGL-1307), and inventing a palette here\n * would have to be unpicked when it is. Same shape as the tag chips Entry\n * Meta already renders, so the two blocks read as one vocabulary.\n */\nconst relatedChipSx = {\n alignSelf: 'flex-start',\n borderColor: 'divider',\n color: 'text.secondary',\n '& .MuiChip-label': {\n // 9px, the frame's own inset on a 69 x 24 chip.\n px: 1.125,\n // The frame sets this label in the same uppercase, letter-spaced step as\n // the kicker above an article title, which is what `overline` IS in the\n // site theme. Naming the step keeps the chip on the theme's scale instead\n // of a hand-written size that drifts the moment the theme moves.\n typography: 'overline',\n // `size=\"small\"` gives the chip the frame's 24px height, and `overline`\n // carries a 2.66 line-height meant for a standalone kicker with room\n // around it. Inside a fixed-height chip the box sets the height, so the\n // line collapses to it and the label centres.\n lineHeight: 1,\n },\n}\n\n/** The heading step the block has always emitted. */\nconst RELATED_DEFAULT_HEADING_VARIANT = 'h5'\n\n/** The card-title step the block has always emitted. */\nconst RELATED_DEFAULT_TITLE_VARIANT = 'subtitle1'\n\n/**\n * Titles for the sample cards. Deliberately unmistakable as copy: the author\n * is looking at a LAYOUT, and a plausible-looking headline here is one an\n * author can mistake for a post that exists — or worse, design around.\n */\nconst RELATED_SAMPLE_TITLES = [\n 'Sample related post',\n 'A second sample post',\n 'A third sample post',\n]\n\n/** Excerpt text for the sample cards; one line, so the card keeps its shape. */\nconst RELATED_SAMPLE_EXCERPT =\n 'Each post’s own excerpt reads here, trimmed to a line or two.'\n\n/**\n * Stand-in posts for editing surfaces (AGL-2486).\n *\n * Related posts are resolved FROM the routed entry, and a besigner canvas has\n * no routed entry — so the block used to draw a one-line dashed strip and the\n * author styled a layout they could not see. The sample is the block's REAL\n * markup, at the author's own settings, which is the only way the canvas can\n * answer \"what will this look like\".\n *\n * It exists only where {@link ScreenLinkContext.suppressNavigation} is on —\n * the besigner canvas and Preview. A published render reaches this function\n * on no path at all, so no visitor can ever be shown a post that does not\n * exist.\n *\n * The count follows the author's own `limit` so a 6-post rail at 3 columns\n * previews as two rows, and is bounded by the same ceiling the server\n * applies rather than a second one.\n */\nconst sampleRelatedEntries = (\n limit: number | string | undefined,\n dateFormat: Aglyn.CollectionEntryDateFormat,\n): Aglyn.CollectionRelatedItem[] => {\n const count = Math.min(\n // `|| default` also rejects 0, which `toCount` would otherwise accept and\n // preview as an empty rail — the one thing the sample exists to avoid.\n toCount(limit, Aglyn.COLLECTION_RELATED_DEFAULT_LIMIT) ||\n Aglyn.COLLECTION_RELATED_DEFAULT_LIMIT,\n Aglyn.COLLECTION_RELATED_MAX,\n )\n const date = Aglyn.formatCollectionEntryDate(\n RELATED_SAMPLE_PUBLISHED_AT,\n dateFormat,\n )\n return Array.from({ length: count }, (_, index) => ({\n title: RELATED_SAMPLE_TITLES[index] ?? `Sample related post ${index + 1}`,\n // Empty, not a plausible path: every surface that renders the sample\n // suppresses navigation, so the title is text rather than an anchor and\n // this is never read. A real-looking href would be a link to nowhere the\n // moment that stopped being true.\n url: '',\n date,\n category: RELATED_SAMPLE_CATEGORY,\n excerpt: RELATED_SAMPLE_EXCERPT,\n }))\n}\n\n/**\n * Lists other entries of the same collection sharing the current entry's\n * category or a tag (AGL-582). The tenant stamps `entries` at compose time\n * on entry renders; without them the published site renders nothing and an\n * editing surface renders sample cards (AGL-2486).\n *\n * Two layouts (AGL-1457). `list` is the plain-link list that has always\n * shipped and stays the default — the block is live on every blog entry, so\n * a new default would restyle published pages nobody asked about. `cards` is\n * the article frame's grid: cover, category chip, title, at an author-set\n * column count.\n *\n * The block owns its markup, so nothing inside a card is a node the Styles\n * panel can select. Every part it renders therefore needs an attribute, or\n * it is unauthorable by construction — which is what the type steps and the\n * `show*` switches are (AGL-2486).\n */\nconst CollectionRelated = forwardRef<HTMLDivElement, CollectionRelatedProps>(\n (props, ref) => {\n // `limit` and `dateFormat` are compose-time: the tenant resolves them\n // while stamping `entries`; strip them so they never hit the DOM. The\n // rest are read here, so they must not reach it either.\n const {\n heading,\n limit,\n entries,\n showCover,\n layout,\n columns,\n headingVariant,\n titleVariant,\n showDate,\n dateFormat,\n showCategory,\n showExcerpt,\n ...rest\n } = props\n // Node styles ride the renderer-merged sx; recompose (stack.ts pattern).\n const nodeSx = Array.isArray(props['sx']) ? props['sx'] : [props['sx']]\n const { suppressNavigation } = useContext(Aglyn.ScreenLinkContext)\n // The resolver every other surface shares (AGL-1215): a stamped `media:`\n // reference becomes a CDN URL HERE, not in the document, so one\n // reference keeps working across sites. Called before the early return —\n // it is a hook.\n const { hostId } = Aglyn.useSite()\n // An editing surface has no routed entry, so nothing is stamped and the\n // block used to be the one part of the page that was not WYSIWYG. The\n // sample stands in for exactly that gap, and only there.\n const sample = !entries?.length && Boolean(suppressNavigation)\n const items = entries?.length\n ? entries\n : sample\n ? sampleRelatedEntries(\n limit,\n Aglyn.normalizeCollectionEntryDateFormat(dateFormat),\n )\n : []\n if (!items.length) return <Box ref={ref} {...rest} />\n const title = heading ?? 'Related articles'\n const headingNode = title ? (\n <Typography\n variant={(headingVariant || RELATED_DEFAULT_HEADING_VARIANT) as 'h5'}\n component=\"h2\"\n >\n {title}\n </Typography>\n ) : null\n /**\n * The card title, as a HEADING in the site theme.\n *\n * It used to be a bare `AppLink`, which is MUI's `Link` — `primary.main`\n * and `underline=\"always\"` — so the one thing a related card is FOR read\n * as a default browser link on a themed page, at no heading level, and\n * with no attribute able to touch it. The type step now comes from the\n * theme's own rungs and the anchor inherits the colour, exactly as the\n * Entry Author card's byline link does.\n *\n * `linked` is false wherever the whole card is already the anchor: an\n * `<a>` inside an `<a>` is invalid markup that browsers silently unnest.\n */\n const titleNode = (entry: Aglyn.CollectionRelatedItem, linked: boolean) => (\n <Typography\n variant={(titleVariant || RELATED_DEFAULT_TITLE_VARIANT) as 'subtitle1'}\n component=\"h3\"\n >\n {linked && !suppressNavigation && entry.url ? (\n <AppLink href={entry.url} sx={{ color: 'inherit' }} underline=\"hover\">\n {entry.title}\n </AppLink>\n ) : (\n entry.title\n )}\n </Typography>\n )\n const dateNode = (entry: Aglyn.CollectionRelatedItem) =>\n showDate !== false && entry.date ? (\n <Typography variant=\"caption\" sx={{ color: 'text.secondary' }}>\n {entry.date}\n </Typography>\n ) : null\n const excerptNode = (entry: Aglyn.CollectionRelatedItem) =>\n showExcerpt && entry.excerpt ? (\n <Typography variant=\"body2\" sx={{ color: 'text.secondary' }}>\n {entry.excerpt}\n </Typography>\n ) : null\n /**\n * The cover, when the author asked for one AND the entry has one. An\n * entry without a cover gets no box at all rather than a placeholder:\n * the related list is a real feed, and a row of grey rectangles is worse\n * than a row of titles.\n *\n * The sample cards are the exception, and for the opposite reason: they\n * stand in for posts that do not exist yet, so the cover SLOT is what\n * the author is looking at. A card grid previewing without its images is\n * not the grid being styled.\n */\n const coverNode = (entry: Aglyn.CollectionRelatedItem) => {\n const src = showCover\n ? Aglyn.resolveMediaSrc(entry.coverImage, { hostId })\n : undefined\n if (!src) {\n if (!sample || !showCover) return null\n return (\n <Box\n aria-hidden\n sx={{\n width: '100%',\n aspectRatio: RELATED_COVER_ASPECT,\n borderRadius: 1,\n backgroundColor: 'action.hover',\n }}\n />\n )\n }\n return (\n <Box\n component=\"img\"\n src={src}\n // The author's own description wins, and the title stays the\n // fallback (AGL-2418). Unlike the entry hero and the event\n // thumbnail — which render empty when nothing is authored — this\n // cover is the LINK's own content, so it must carry an accessible\n // name rather than go silent.\n alt={Aglyn.renderedMediaAlt(entry.coverImageAlt, entry.title)}\n sx={{\n display: 'block',\n width: '100%',\n aspectRatio: RELATED_COVER_ASPECT,\n objectFit: 'cover',\n borderRadius: 1,\n }}\n // `lazy` ALONE was the bug in miniature (AGL-2486): a lazy image\n // at default priority still outranks a lazy image at `low`, so\n // this related-entries rail — which sits at the bottom of an entry\n // by construction — was beating the deferred Image elements in the\n // body above it. The hint only works as a set.\n {...Aglyn.DEFERRED_IMAGE_ATTRIBUTES}\n />\n )\n }\n\n /**\n * The notice that says these cards are not posts (AGL-2486). Editing\n * surfaces only, and outside the card markup rather than inside it, so\n * the author is styling the same tree the site will render.\n */\n const sampleNotice = sample ? (\n <Typography\n variant=\"caption\"\n sx={{\n display: 'block',\n p: 1,\n border: '1px dashed',\n borderColor: 'divider',\n borderRadius: 1,\n color: 'text.secondary',\n fontFamily: 'system-ui, sans-serif',\n }}\n >\n {'Sample posts — entries sharing this entry’s category or tags ' +\n 'replace these on the published page'}\n </Typography>\n ) : null\n\n if (layout === 'cards') {\n // `toCount` rounds and rejects junk; `|| default` also rejects 0, which\n // it would otherwise accept as a column count and emit `repeat(0, …)`.\n const columnCount =\n toCount(columns, RELATED_DEFAULT_COLUMNS) || RELATED_DEFAULT_COLUMNS\n /**\n * One card's contents, always with a PLAIN title: the caller wraps the\n * whole card in the anchor, because a reader aiming at a 180px cover\n * should not have to hit the line of text under it — and a second\n * anchor around the title inside that one is markup browsers unnest.\n */\n const card = (entry: Aglyn.CollectionRelatedItem) => (\n // 12px between cover, chip and title — the frame's rhythm (170:243\n // puts the chip at y=192 under a cover ending at 180, and the title\n // at y=228 under a chip ending at 216).\n <MuiStack spacing={1.5}>\n {coverNode(entry)}\n {showCategory !== false && entry.category ? (\n <Chip\n label={entry.category}\n size=\"small\"\n variant=\"outlined\"\n sx={relatedChipSx}\n />\n ) : null}\n {titleNode(entry, false)}\n {excerptNode(entry)}\n {dateNode(entry)}\n </MuiStack>\n )\n return (\n <Box\n ref={ref}\n {...rest}\n // MERGE, never replace (AGL-1450) — the node's slice arrives as the\n // ARRAY `mergeSxProps` builds in leaf.tsx, so folding it into an\n // object spreads numeric keys and discards every authored property\n // while these defaults still apply. Defaults first, node's after.\n sx={[\n {\n display: 'grid',\n gridTemplateColumns: `repeat(${columnCount}, minmax(0, 1fr))`,\n alignItems: 'start',\n columnGap: 3,\n rowGap: 4,\n },\n ...nodeSx,\n ]}\n >\n {headingNode ? (\n <Box sx={{ gridColumn: '1 / -1' }}>{headingNode}</Box>\n ) : null}\n {sampleNotice ? (\n <Box sx={{ gridColumn: '1 / -1' }}>{sampleNotice}</Box>\n ) : null}\n {items.map((entry, index) => {\n const linked = !suppressNavigation && Boolean(entry.url)\n return linked ? (\n <AppLink\n key={index}\n href={entry.url}\n underline=\"none\"\n // A grid ITEM, so it has to be a block; `color: inherit`\n // keeps the cover, chip and date reading as themed content\n // rather than as the inside of a link.\n sx={{ color: 'inherit', display: 'block' }}\n >\n {card(entry)}\n </AppLink>\n ) : (\n <Box key={index}>{card(entry)}</Box>\n )\n })}\n </Box>\n )\n }\n return (\n <MuiStack ref={ref} spacing={1.5} {...rest}>\n {headingNode}\n {sampleNotice}\n {items.map((entry, index) => {\n const meta = [\n showDate !== false ? entry.date : '',\n showCategory !== false ? entry.category : '',\n ].filter(Boolean)\n return (\n <MuiStack key={index} spacing={0.25}>\n {coverNode(entry)}\n {titleNode(entry, true)}\n {excerptNode(entry)}\n {meta.length ? (\n <Typography variant=\"caption\" sx={{ color: 'text.secondary' }}>\n {meta.join(' · ')}\n </Typography>\n ) : null}\n </MuiStack>\n )\n })}\n </MuiStack>\n )\n },\n)\nCollectionRelated.displayName = 'AglynCollectionRelated'\n\nexport const collectionRelatedSchema: Aglyn.ComponentSchema<CollectionRelatedProps> =\n {\n $id: RELATED_ID,\n pluginId: BUNDLE_ID,\n displayName: 'Related Posts',\n description: \"Other entries sharing this one's category or tags.\",\n category: Aglyn.ComponentCategory.DATA_DISPLAY,\n icon: { path: mdiNewspaperVariantOutline.path, sx: { color: 'secondary.main' } },\n flags: { selfClosing: Aglyn.FEATURE_FLAG.ENABLED },\n attributes: [\n {\n name: 'heading',\n label: 'Heading',\n description: 'Section heading (blank hides it).',\n component: Aglyn.FieldComponentType.TEXT_FIELD,\n },\n {\n name: 'limit',\n label: 'Limit',\n description: 'Most related posts listed (default 3).',\n component: Aglyn.FieldComponentType.TEXT_FIELD,\n type: 'number',\n },\n {\n name: 'layout',\n label: 'Layout',\n description:\n 'List keeps the plain links this block has always rendered. ' +\n 'Cards is the article layout: cover, category, title, in a grid.',\n component: Aglyn.FieldComponentType.SELECT,\n // Both values are REAL (AGL-1451/AGL-1453): `''` cannot survive a\n // save, so an author who switched to Cards would have no route back.\n // `list` is also the value the render falls back to, so naming it is\n // the same choice, not a second one.\n options: [\n { value: 'list', label: 'List (default)' },\n { value: 'cards', label: 'Card grid' },\n ],\n },\n {\n name: 'columns',\n label: 'Columns',\n description: 'Cards per row in the card grid. Default 3.',\n component: Aglyn.FieldComponentType.TEXT_FIELD,\n type: 'number',\n // Meaningless on the list, where each post is its own row.\n condition: { when: 'layout', is: 'cards' },\n },\n {\n name: 'showCover',\n label: 'Show cover',\n description:\n 'Show each post’s cover image. Posts without one show their ' +\n 'title alone rather than an empty box.',\n component: Aglyn.FieldComponentType.SWITCH,\n },\n {\n name: 'headingVariant',\n label: 'Heading style',\n description:\n 'Which type step from the site theme the section heading above ' +\n 'the posts reads at. Default Heading 5.',\n component: Aglyn.FieldComponentType.SELECT,\n // The theme's own rungs, shared with the Typography element rather\n // than a second list here: two lists is how a step added to the\n // theme becomes reachable in one place and not the other.\n options: [...typographyVariants],\n },\n {\n name: 'titleVariant',\n label: 'Title style',\n description:\n 'Which type step from the site theme each post’s title reads at. ' +\n 'Default Subtitle 1; the title takes the surrounding text color ' +\n 'rather than the link color.',\n component: Aglyn.FieldComponentType.SELECT,\n options: [...typographyVariants],\n },\n {\n name: 'showDate',\n label: 'Show date',\n description:\n 'Off drops each post’s published date. The posts stay in ' +\n 'newest-first order either way — this is about the card, not the ' +\n 'ordering.',\n component: Aglyn.FieldComponentType.SWITCH,\n // Unset means SHOWN in the renderer above, so the switch has to\n // OPEN in that position or it lies about the block in front of\n // the author — and the first click, which writes the `true` that\n // was already in effect, changes nothing and reads as a dead\n // control (AGL-2506).\n initialValue: true,\n },\n {\n name: 'dateFormat',\n label: 'Date format',\n description:\n 'How each post’s published date reads on its card. Site default ' +\n 'is the format this block has always used.',\n component: Aglyn.FieldComponentType.SELECT,\n // The formats the pure layer knows how to produce, so the list\n // cannot offer a shape nothing renders. Every value is REAL,\n // including the do-nothing one (AGL-1451/AGL-1453).\n options: [...Aglyn.COLLECTION_ENTRY_DATE_FORMAT_OPTIONS],\n },\n {\n name: 'showCategory',\n label: 'Show category',\n description:\n 'Off drops the category — the chip above each card title, or the ' +\n 'second half of the line under each link on the list layout.',\n component: Aglyn.FieldComponentType.SWITCH,\n // Unset means SHOWN in the renderer above, so the switch has to\n // OPEN in that position or it lies about the block in front of\n // the author — and the first click, which writes the `true` that\n // was already in effect, changes nothing and reads as a dead\n // control (AGL-2506).\n initialValue: true,\n },\n {\n name: 'showExcerpt',\n label: 'Show excerpt',\n description:\n 'Adds each post’s excerpt under its title. Posts with no excerpt ' +\n 'are unchanged rather than leaving a gap.',\n component: Aglyn.FieldComponentType.SWITCH,\n },\n ],\n }\n\nexport { CollectionRelated }\n"],"names":["Aglyn","mdiNewspaperVariantOutline","AppLink","Box","Chip","MuiStack","Typography","forwardRef","useContext","BUNDLE_ID","toCount","RELATED_SAMPLE_PUBLISHED_AT","RELATED_SAMPLE_CATEGORY","typographyVariants","RELATED_ID","COLLECTION_RELATED_COMPONENT_ID","RELATED_DEFAULT_COLUMNS","RELATED_COVER_ASPECT","relatedChipSx","alignSelf","borderColor","color","px","typography","lineHeight","RELATED_DEFAULT_HEADING_VARIANT","RELATED_DEFAULT_TITLE_VARIANT","RELATED_SAMPLE_TITLES","RELATED_SAMPLE_EXCERPT","sampleRelatedEntries","limit","dateFormat","count","Math","min","COLLECTION_RELATED_DEFAULT_LIMIT","COLLECTION_RELATED_MAX","date","formatCollectionEntryDate","Array","from","length","_","index","title","url","category","excerpt","CollectionRelated","props","ref","heading","entries","showCover","layout","columns","headingVariant","titleVariant","showDate","showCategory","showExcerpt","rest","nodeSx","isArray","suppressNavigation","ScreenLinkContext","hostId","useSite","sample","Boolean","items","normalizeCollectionEntryDateFormat","headingNode","variant","component","titleNode","entry","linked","href","sx","underline","dateNode","excerptNode","coverNode","src","resolveMediaSrc","coverImage","undefined","aria-hidden","width","aspectRatio","borderRadius","backgroundColor","alt","renderedMediaAlt","coverImageAlt","display","objectFit","DEFERRED_IMAGE_ATTRIBUTES","sampleNotice","p","border","fontFamily","columnCount","card","spacing","label","size","gridTemplateColumns","alignItems","columnGap","rowGap","gridColumn","map","meta","filter","join","displayName","collectionRelatedSchema","$id","pluginId","description","ComponentCategory","DATA_DISPLAY","icon","path","flags","selfClosing","FEATURE_FLAG","ENABLED","attributes","name","FieldComponentType","TEXT_FIELD","type","SELECT","options","value","condition","when","is","SWITCH","initialValue","COLLECTION_ENTRY_DATE_FORMAT_OPTIONS"],"mappings":"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"}