@aglyn/plugins-mui 1.0.0-beta.143

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 (177) hide show
  1. package/LICENSE +201 -0
  2. package/README.md +22 -0
  3. package/package.json +47 -0
  4. package/src/index.d.ts +21 -0
  5. package/src/index.js +22 -0
  6. package/src/index.js.map +1 -0
  7. package/src/lib/components/accordion.d.ts +96 -0
  8. package/src/lib/components/accordion.js +392 -0
  9. package/src/lib/components/accordion.js.map +1 -0
  10. package/src/lib/components/app-bar.d.ts +32 -0
  11. package/src/lib/components/app-bar.js +86 -0
  12. package/src/lib/components/app-bar.js.map +1 -0
  13. package/src/lib/components/blocks.d.ts +46 -0
  14. package/src/lib/components/blocks.js +1007 -0
  15. package/src/lib/components/blocks.js.map +1 -0
  16. package/src/lib/components/box.d.ts +47 -0
  17. package/src/lib/components/box.js +125 -0
  18. package/src/lib/components/box.js.map +1 -0
  19. package/src/lib/components/breadcrumbs.d.ts +44 -0
  20. package/src/lib/components/breadcrumbs.js +172 -0
  21. package/src/lib/components/breadcrumbs.js.map +1 -0
  22. package/src/lib/components/button.d.ts +89 -0
  23. package/src/lib/components/button.js +272 -0
  24. package/src/lib/components/button.js.map +1 -0
  25. package/src/lib/components/card.d.ts +61 -0
  26. package/src/lib/components/card.js +336 -0
  27. package/src/lib/components/card.js.map +1 -0
  28. package/src/lib/components/collection.d.ts +514 -0
  29. package/src/lib/components/collection.js +2999 -0
  30. package/src/lib/components/collection.js.map +1 -0
  31. package/src/lib/components/container.d.ts +42 -0
  32. package/src/lib/components/container.js +169 -0
  33. package/src/lib/components/container.js.map +1 -0
  34. package/src/lib/components/custom-html.d.ts +78 -0
  35. package/src/lib/components/custom-html.js +219 -0
  36. package/src/lib/components/custom-html.js.map +1 -0
  37. package/src/lib/components/data-table.d.ts +57 -0
  38. package/src/lib/components/data-table.js +216 -0
  39. package/src/lib/components/data-table.js.map +1 -0
  40. package/src/lib/components/document-root.d.ts +77 -0
  41. package/src/lib/components/document-root.js +118 -0
  42. package/src/lib/components/document-root.js.map +1 -0
  43. package/src/lib/components/drawer.d.ts +93 -0
  44. package/src/lib/components/drawer.js +566 -0
  45. package/src/lib/components/drawer.js.map +1 -0
  46. package/src/lib/components/function-controls.d.ts +66 -0
  47. package/src/lib/components/function-controls.js +207 -0
  48. package/src/lib/components/function-controls.js.map +1 -0
  49. package/src/lib/components/function-scope.d.ts +110 -0
  50. package/src/lib/components/function-scope.js +478 -0
  51. package/src/lib/components/function-scope.js.map +1 -0
  52. package/src/lib/components/function-widget.d.ts +75 -0
  53. package/src/lib/components/function-widget.js +307 -0
  54. package/src/lib/components/function-widget.js.map +1 -0
  55. package/src/lib/components/grid.d.ts +91 -0
  56. package/src/lib/components/grid.js +322 -0
  57. package/src/lib/components/grid.js.map +1 -0
  58. package/src/lib/components/icon.d.ts +54 -0
  59. package/src/lib/components/icon.js +163 -0
  60. package/src/lib/components/icon.js.map +1 -0
  61. package/src/lib/components/image-list.d.ts +58 -0
  62. package/src/lib/components/image-list.js +336 -0
  63. package/src/lib/components/image-list.js.map +1 -0
  64. package/src/lib/components/image.d.ts +154 -0
  65. package/src/lib/components/image.js +451 -0
  66. package/src/lib/components/image.js.map +1 -0
  67. package/src/lib/components/inline-text.d.ts +74 -0
  68. package/src/lib/components/inline-text.js +407 -0
  69. package/src/lib/components/inline-text.js.map +1 -0
  70. package/src/lib/components/language-switcher.d.ts +35 -0
  71. package/src/lib/components/language-switcher.js +179 -0
  72. package/src/lib/components/language-switcher.js.map +1 -0
  73. package/src/lib/components/layout-slot.d.ts +68 -0
  74. package/src/lib/components/layout-slot.js +192 -0
  75. package/src/lib/components/layout-slot.js.map +1 -0
  76. package/src/lib/components/link-box-accessible-name.d.ts +79 -0
  77. package/src/lib/components/link-box-accessible-name.js +158 -0
  78. package/src/lib/components/link-box-accessible-name.js.map +1 -0
  79. package/src/lib/components/link-box.d.ts +66 -0
  80. package/src/lib/components/link-box.js +212 -0
  81. package/src/lib/components/link-box.js.map +1 -0
  82. package/src/lib/components/list-item-text.d.ts +22 -0
  83. package/src/lib/components/list-item-text.js +86 -0
  84. package/src/lib/components/list-item-text.js.map +1 -0
  85. package/src/lib/components/list-item.d.ts +32 -0
  86. package/src/lib/components/list-item.js +127 -0
  87. package/src/lib/components/list-item.js.map +1 -0
  88. package/src/lib/components/list.d.ts +34 -0
  89. package/src/lib/components/list.js +108 -0
  90. package/src/lib/components/list.js.map +1 -0
  91. package/src/lib/components/markdown.d.ts +129 -0
  92. package/src/lib/components/markdown.js +853 -0
  93. package/src/lib/components/markdown.js.map +1 -0
  94. package/src/lib/components/nav-menu.d.ts +68 -0
  95. package/src/lib/components/nav-menu.js +683 -0
  96. package/src/lib/components/nav-menu.js.map +1 -0
  97. package/src/lib/components/pagination.d.ts +59 -0
  98. package/src/lib/components/pagination.js +243 -0
  99. package/src/lib/components/pagination.js.map +1 -0
  100. package/src/lib/components/paper.d.ts +47 -0
  101. package/src/lib/components/paper.js +202 -0
  102. package/src/lib/components/paper.js.map +1 -0
  103. package/src/lib/components/plugin-frame.d.ts +64 -0
  104. package/src/lib/components/plugin-frame.js +335 -0
  105. package/src/lib/components/plugin-frame.js.map +1 -0
  106. package/src/lib/components/plugin.d.ts +95 -0
  107. package/src/lib/components/plugin.js +304 -0
  108. package/src/lib/components/plugin.js.map +1 -0
  109. package/src/lib/components/product.d.ts +40 -0
  110. package/src/lib/components/product.js +287 -0
  111. package/src/lib/components/product.js.map +1 -0
  112. package/src/lib/components/reusable-instance.d.ts +57 -0
  113. package/src/lib/components/reusable-instance.js +126 -0
  114. package/src/lib/components/reusable-instance.js.map +1 -0
  115. package/src/lib/components/screen-link.d.ts +85 -0
  116. package/src/lib/components/screen-link.js +324 -0
  117. package/src/lib/components/screen-link.js.map +1 -0
  118. package/src/lib/components/search-box.d.ts +41 -0
  119. package/src/lib/components/search-box.js +125 -0
  120. package/src/lib/components/search-box.js.map +1 -0
  121. package/src/lib/components/section.d.ts +56 -0
  122. package/src/lib/components/section.js +132 -0
  123. package/src/lib/components/section.js.map +1 -0
  124. package/src/lib/components/stack.d.ts +55 -0
  125. package/src/lib/components/stack.js +309 -0
  126. package/src/lib/components/stack.js.map +1 -0
  127. package/src/lib/components/tabs.d.ts +288 -0
  128. package/src/lib/components/tabs.js +788 -0
  129. package/src/lib/components/tabs.js.map +1 -0
  130. package/src/lib/components/theme-mode-switcher.d.ts +38 -0
  131. package/src/lib/components/theme-mode-switcher.js +168 -0
  132. package/src/lib/components/theme-mode-switcher.js.map +1 -0
  133. package/src/lib/components/toolbar.d.ts +37 -0
  134. package/src/lib/components/toolbar.js +117 -0
  135. package/src/lib/components/toolbar.js.map +1 -0
  136. package/src/lib/components/typography.d.ts +33 -0
  137. package/src/lib/components/typography.js +433 -0
  138. package/src/lib/components/typography.js.map +1 -0
  139. package/src/lib/components/video-lightbox-controls.d.ts +131 -0
  140. package/src/lib/components/video-lightbox-controls.js +536 -0
  141. package/src/lib/components/video-lightbox-controls.js.map +1 -0
  142. package/src/lib/components/video-lightbox.d.ts +60 -0
  143. package/src/lib/components/video-lightbox.js +247 -0
  144. package/src/lib/components/video-lightbox.js.map +1 -0
  145. package/src/lib/components/video-playback-beacon.d.ts +80 -0
  146. package/src/lib/components/video-playback-beacon.js +128 -0
  147. package/src/lib/components/video-playback-beacon.js.map +1 -0
  148. package/src/lib/components/video-player-frame.d.ts +58 -0
  149. package/src/lib/components/video-player-frame.js +51 -0
  150. package/src/lib/components/video-player-frame.js.map +1 -0
  151. package/src/lib/components/video.d.ts +220 -0
  152. package/src/lib/components/video.js +874 -0
  153. package/src/lib/components/video.js.map +1 -0
  154. package/src/lib/constants/bundle-common.d.ts +17 -0
  155. package/src/lib/constants/bundle-common.js +18 -0
  156. package/src/lib/constants/bundle-common.js.map +1 -0
  157. package/src/lib/constants/field-presets.d.ts +93 -0
  158. package/src/lib/constants/field-presets.js +248 -0
  159. package/src/lib/constants/field-presets.js.map +1 -0
  160. package/src/lib/plugin.d.ts +80 -0
  161. package/src/lib/plugin.js +599 -0
  162. package/src/lib/plugin.js.map +1 -0
  163. package/src/lib/utils/author-html-label.d.ts +73 -0
  164. package/src/lib/utils/author-html-label.js +89 -0
  165. package/src/lib/utils/author-html-label.js.map +1 -0
  166. package/src/lib/utils/drop-cleared-props.d.ts +18 -0
  167. package/src/lib/utils/drop-cleared-props.js +74 -0
  168. package/src/lib/utils/drop-cleared-props.js.map +1 -0
  169. package/src/lib/utils/element-picker.d.ts +99 -0
  170. package/src/lib/utils/element-picker.js +140 -0
  171. package/src/lib/utils/element-picker.js.map +1 -0
  172. package/src/lib/utils/generate-preset-id.d.ts +19 -0
  173. package/src/lib/utils/generate-preset-id.js +25 -0
  174. package/src/lib/utils/generate-preset-id.js.map +1 -0
  175. package/src/lib/utils/link-target-props.d.ts +53 -0
  176. package/src/lib/utils/link-target-props.js +53 -0
  177. package/src/lib/utils/link-target-props.js.map +1 -0
@@ -0,0 +1,2999 @@
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, Fragment as _Fragment } 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 { mdiAccountCircleOutline, mdiContentCopy, mdiEmail, mdiFacebook, mdiGithub, mdiInstagram, mdiLinkVariant, mdiLinkedin, mdiMagnify, mdiMastodon, mdiNewspaperVariantOutline, mdiPostOutline, mdiRss, mdiShareVariant, mdiTagMultipleOutline, mdiTagOutline, mdiTextLong, mdiTwitter, mdiWeb, mdiYoutube } from "@aglyn/shared-data-mdi";
21
+ import { AppLink, MdiIcon } from "@aglyn/shared-ui-jsx";
22
+ // The icon picker's fuzzy matcher (use-mdi-icons-fuzzy), not a re-implementation
23
+ // (AGL-1516): search here has to feel like search does everywhere else in the
24
+ // product, and two matchers is how they drift.
25
+ import { Fuse } from "@aglyn/shared-util-vendor/fuse";
26
+ import Box from "@mui/material/Box";
27
+ import Chip from "@mui/material/Chip";
28
+ import IconButton from "@mui/material/IconButton";
29
+ import InputBase from "@mui/material/InputBase";
30
+ import MuiLink from "@mui/material/Link";
31
+ import MuiStack from "@mui/material/Stack";
32
+ import Typography from "@mui/material/Typography";
33
+ import { Children, forwardRef, useContext, useMemo, useState } from "react";
34
+ import { BUNDLE_ID } from "../constants/bundle-common.js";
35
+ import { generatePresetId } from "../utils/generate-preset-id.js";
36
+ // One coercion for every authored count in this bundle (AGL-1457) — number
37
+ // fields round-trip as strings, and a second parser here would drift.
38
+ import { toCount } from "./image-list.js";
39
+ // The theme's own type steps (AGL-2486), shared with the Typography element
40
+ // rather than a second list here — one rung list, one place to extend.
41
+ import { typographyVariants } from "./typography.js";
42
+ // Persisted component ids (AGL-551/582); the compose pipeline references
43
+ // them through @aglyn/aglyn constants. Never rename.
44
+ export const ENTRIES_ID = Aglyn.COLLECTION_ENTRIES_COMPONENT_ID;
45
+ export const ENTRY_BODY_ID = Aglyn.COLLECTION_ENTRY_BODY_COMPONENT_ID;
46
+ export const RELATED_ID = Aglyn.COLLECTION_RELATED_COMPONENT_ID;
47
+ export const SHARE_ID = Aglyn.COLLECTION_SHARE_COMPONENT_ID;
48
+ export const ENTRY_META_ID = Aglyn.COLLECTION_ENTRY_META_COMPONENT_ID;
49
+ export const ENTRY_AUTHOR_ID = Aglyn.COLLECTION_ENTRY_AUTHOR_COMPONENT_ID;
50
+ export const CATEGORIES_ID = Aglyn.COLLECTION_CATEGORIES_COMPONENT_ID;
51
+ export const SEARCH_ID = Aglyn.COLLECTION_SEARCH_COMPONENT_ID;
52
+ export const AUTHOR_PROFILE_ID = Aglyn.CONTENT_AUTHOR_PROFILE_COMPONENT_ID;
53
+ /**
54
+ * Author-facing choices for {@link CollectionEntriesProps.searchMode}.
55
+ *
56
+ * Both values are truthy on purpose (AGL-1453): `''` cannot survive a save,
57
+ * so an author who tried the other mode would have no route back. "Filter"
58
+ * carries the absent-value reading, which is what every block published
59
+ * before AGL-1525 has.
60
+ */ export const COLLECTION_SEARCH_MODE_OPTIONS = [
61
+ {
62
+ value: 'filter',
63
+ label: 'Filter the entries in place'
64
+ },
65
+ {
66
+ value: 'suggest',
67
+ label: 'Show a suggestions dropdown'
68
+ }
69
+ ];
70
+ /** The toolbar search field, from the frame (Figma 494:1220). */ const SEARCH_FIELD_WIDTH = 240;
71
+ /** Suggestion panel (Figma 496:1218). */ const SUGGESTION_PANEL_WIDTH = 420;
72
+ /**
73
+ * Rows in the panel before it stops. The frame shows five; the sixth row is
74
+ * always the "View all results" link, which is the honest overflow answer —
75
+ * a scrolling suggestion panel is a worse results page.
76
+ */ const SUGGESTION_LIMIT = 5;
77
+ /** Where "View all results" goes — the reserved site-search route (AGL-88). */ const searchResultsHref = (query)=>`/search?q=${encodeURIComponent(query)}`;
78
+ /**
79
+ * The frame's compact filled field: magnify glyph + hint on the quiet
80
+ * surface token, right-aligned so it sits where the toolbar row puts it.
81
+ * Palette tokens only, so it follows the site theme in both modes.
82
+ */ const searchFieldSx = {
83
+ alignSelf: 'flex-end',
84
+ display: 'flex',
85
+ alignItems: 'center',
86
+ gap: 1,
87
+ px: 1.5,
88
+ py: 0.75,
89
+ borderRadius: 2,
90
+ bgcolor: 'action.hover',
91
+ color: 'text.secondary',
92
+ width: SEARCH_FIELD_WIDTH,
93
+ maxWidth: '100%'
94
+ };
95
+ /**
96
+ * The positioned parent of the suggestion panel (AGL-1525). It takes over
97
+ * `alignSelf` from the field so the toolbar row is unchanged — the panel
98
+ * hangs off the field, it does not move it.
99
+ */ const suggestionAnchorSx = {
100
+ alignSelf: 'flex-end',
101
+ position: 'relative',
102
+ width: SEARCH_FIELD_WIDTH,
103
+ maxWidth: '100%'
104
+ };
105
+ /**
106
+ * The floating panel itself. Right-aligned to the field it drops from, and
107
+ * wider than it, as the frame draws it — a suggestion row carries a title, a
108
+ * chip, a date and a line of excerpt, none of which fit in 240px.
109
+ *
110
+ * Palette and shadow tokens only, so it follows the site theme in both
111
+ * modes rather than pinning a light-mode card onto a dark page.
112
+ */ const suggestionPanelSx = {
113
+ position: 'absolute',
114
+ top: 'calc(100% + 8px)',
115
+ right: 0,
116
+ zIndex: 10,
117
+ width: SUGGESTION_PANEL_WIDTH,
118
+ // Never wider than the viewport on a phone, where the anchor is narrow
119
+ // and `right: 0` would otherwise hang the panel off the left edge.
120
+ maxWidth: 'calc(100vw - 32px)',
121
+ bgcolor: 'background.paper',
122
+ color: 'text.primary',
123
+ border: 1,
124
+ borderColor: 'divider',
125
+ borderRadius: 2,
126
+ boxShadow: 6,
127
+ overflow: 'hidden',
128
+ textAlign: 'left'
129
+ };
130
+ const suggestionRowLinkSx = {
131
+ display: 'block',
132
+ color: 'inherit',
133
+ '&:hover': {
134
+ bgcolor: 'action.hover'
135
+ }
136
+ };
137
+ const suggestionFooterSx = {
138
+ display: 'block',
139
+ px: 2,
140
+ py: 1.25,
141
+ borderTop: 1,
142
+ borderColor: 'divider',
143
+ bgcolor: 'action.hover',
144
+ fontSize: 14,
145
+ fontWeight: 500
146
+ };
147
+ /**
148
+ * The frame's search input (Figma 494:1220) — the magnify glyph and a bare
149
+ * `InputBase` on the quiet surface token.
150
+ *
151
+ * Presentational and stateless, so the entries block's in-place filter and
152
+ * the standalone toolbar box (AGL-1516) are the SAME field rather than two
153
+ * that merely look alike. `inert` is the editing-surface rendering: read-only
154
+ * and handler-free, so a click or stray keystroke in the besigner never edits
155
+ * state the canvas cannot use — how Category Pills go inert there too.
156
+ */ const SearchField = ({ value, placeholder, inert, landmark, onQuery, onEscape })=>/*#__PURE__*/ _jsxs(Box, _extends({}, landmark ? {
157
+ role: 'search'
158
+ } : {}, {
159
+ sx: searchFieldSx,
160
+ children: [
161
+ /*#__PURE__*/ _jsx(MdiIcon, {
162
+ path: mdiMagnify.path
163
+ }),
164
+ /*#__PURE__*/ _jsx(InputBase, _extends({
165
+ value: value,
166
+ name: "q",
167
+ placeholder: (placeholder != null ? placeholder : '').trim() || 'Search posts…',
168
+ inputProps: {
169
+ 'aria-label': 'Search entries'
170
+ },
171
+ sx: {
172
+ flex: 1,
173
+ fontSize: 14,
174
+ color: 'text.primary'
175
+ }
176
+ }, inert ? {
177
+ readOnly: true
178
+ } : {
179
+ onChange: (event)=>onQuery == null ? void 0 : onQuery(event.target.value),
180
+ onKeyDown: (event)=>{
181
+ if (event.key === 'Escape') onEscape == null ? void 0 : onEscape();
182
+ }
183
+ }))
184
+ ]
185
+ }));
186
+ /**
187
+ * The floating suggestion panel (AGL-1525, Figma 496:1218) — rows straight
188
+ * off the server-stamped index, and a "View all results" link that is there
189
+ * hits or no hits.
190
+ *
191
+ * `emptyText` is the caller's, not this component's, and that is the whole
192
+ * point of it being a parameter: a page-windowed entries block and a
193
+ * whole-collection toolbar box searched different sets, and a miss has to say
194
+ * WHICH set came back empty. A shared "No matches." would let one of them
195
+ * claim a reach it never had.
196
+ */ const SuggestionPanel = ({ suggestions, trimmed, emptyText })=>/*#__PURE__*/ _jsxs(Box, {
197
+ sx: suggestionPanelSx,
198
+ children: [
199
+ suggestions.length ? suggestions.map((item, index)=>{
200
+ const row = /*#__PURE__*/ _jsxs(MuiStack, {
201
+ spacing: 0.5,
202
+ sx: {
203
+ px: 2,
204
+ py: 1.25
205
+ },
206
+ children: [
207
+ /*#__PURE__*/ _jsxs(MuiStack, {
208
+ direction: "row",
209
+ spacing: 1,
210
+ sx: {
211
+ alignItems: 'center'
212
+ },
213
+ children: [
214
+ /*#__PURE__*/ _jsx(Typography, {
215
+ variant: "subtitle2",
216
+ sx: {
217
+ color: 'text.primary'
218
+ },
219
+ children: item.title
220
+ }),
221
+ item.category ? /*#__PURE__*/ _jsx(Chip, {
222
+ label: item.category,
223
+ size: "small"
224
+ }) : null,
225
+ item.date ? /*#__PURE__*/ _jsx(Typography, {
226
+ variant: "caption",
227
+ sx: {
228
+ color: 'text.secondary'
229
+ },
230
+ children: item.date
231
+ }) : null
232
+ ]
233
+ }),
234
+ item.excerpt ? /*#__PURE__*/ _jsx(Typography, {
235
+ variant: "body2",
236
+ sx: {
237
+ color: 'text.secondary',
238
+ // One line of excerpt, as the frame draws it. A row that
239
+ // grows with the prose turns the panel into a page.
240
+ overflow: 'hidden',
241
+ textOverflow: 'ellipsis',
242
+ whiteSpace: 'nowrap'
243
+ },
244
+ children: item.excerpt
245
+ }) : null
246
+ ]
247
+ });
248
+ // A row without a `url` is a page cached before AGL-1525 stamped one,
249
+ // or a slugless entry. It renders as TEXT rather than as a link to
250
+ // nowhere: a suggestion that navigates to the wrong page is worse
251
+ // than one the reader has to read.
252
+ return item.url ? /*#__PURE__*/ _jsx(AppLink, {
253
+ href: item.url,
254
+ sx: suggestionRowLinkSx,
255
+ underline: "none",
256
+ children: row
257
+ }, index) : /*#__PURE__*/ _jsx(Box, {
258
+ children: row
259
+ }, index);
260
+ }) : /*#__PURE__*/ _jsx(Typography, {
261
+ variant: "body2",
262
+ sx: {
263
+ color: 'text.secondary',
264
+ px: 2,
265
+ py: 1.25
266
+ },
267
+ children: emptyText
268
+ }),
269
+ /*#__PURE__*/ _jsx(AppLink, {
270
+ href: searchResultsHref(trimmed),
271
+ sx: suggestionFooterSx,
272
+ underline: "none",
273
+ children: `View all results for “${trimmed}” →`
274
+ })
275
+ ]
276
+ });
277
+ /**
278
+ * Field plus dropdown, in a REAL form (AGL-1525) — so Enter reaches the
279
+ * results page and the box still works with the panel's JS doing nothing at
280
+ * all. The panel is an enhancement over a working search box, not the search
281
+ * box.
282
+ *
283
+ * Owns the query, because nothing outside it needs one: `suggest` never
284
+ * touches the cards underneath. That is what lets the standalone toolbar
285
+ * block exist at all (AGL-1516) — a box with no clones to hide is a complete
286
+ * feature, not a crippled one.
287
+ */ const SuggestSearchBox = ({ fuzzy, items, placeholder, inert, emptyText })=>{
288
+ const [query, setQuery] = useState('');
289
+ /** Escape dismissed THIS answer (AGL-1525); typing brings it back. */ const [closed, setClosed] = useState(false);
290
+ const trimmed = query.trim();
291
+ const suggestions = !inert && trimmed && fuzzy && items ? fuzzy.search(trimmed).slice(0, SUGGESTION_LIMIT).map((result)=>items[result.refIndex]).filter(Boolean) : [];
292
+ const field = /*#__PURE__*/ _jsx(SearchField, _extends({
293
+ value: query
294
+ }, placeholder === undefined ? {} : {
295
+ placeholder
296
+ }, inert ? {
297
+ inert: true,
298
+ landmark: true
299
+ } : {}, {
300
+ onQuery: (next)=>{
301
+ setQuery(next);
302
+ // A new query reopens a panel the reader dismissed — Escape closes
303
+ // THIS answer, it does not turn the feature off for the visit.
304
+ setClosed(false);
305
+ },
306
+ onEscape: ()=>setClosed(true)
307
+ }));
308
+ if (inert) return field;
309
+ return /*#__PURE__*/ _jsxs(Box, {
310
+ component: "form",
311
+ role: "search",
312
+ action: "/search",
313
+ method: "get",
314
+ sx: suggestionAnchorSx,
315
+ children: [
316
+ field,
317
+ trimmed && !closed ? /*#__PURE__*/ _jsx(SuggestionPanel, {
318
+ suggestions: suggestions,
319
+ trimmed: trimmed,
320
+ emptyText: emptyText(trimmed)
321
+ }) : null
322
+ ]
323
+ });
324
+ };
325
+ /**
326
+ * How much of the collection an entries block's search could actually see
327
+ * (AGL-1516, AGL-2569) — `truncated` says the rendered window is smaller
328
+ * than the set, `paginated` says the missing part lives on other pages of
329
+ * this same list rather than merely outside the block.
330
+ *
331
+ * The test is whether the window is SMALLER THAN THE SET, which only the
332
+ * server knows — `perPage` is the wrong proxy for it in both directions. A
333
+ * block truncated by `entriesLimit` or by the 100-entry cap has no `perPage`
334
+ * at all and would claim a global miss over 6 of 40 posts; a block whose
335
+ * `perPage` exceeds its entry count is a single complete page and would
336
+ * blame pages that do not exist. `searchTotal` is stamped with `searchIndex`,
337
+ * so the two are always in step.
338
+ *
339
+ * Absent — a page cached before that stamp shipped — falls back to the old
340
+ * `perPage` reading rather than guessing "complete": understating the scope
341
+ * of a search is the safe direction to be wrong in.
342
+ *
343
+ * `searchCapped` is the second way a block can fail to hold the set, and the
344
+ * invisible one: `searchTotal` counts the entries the server READ, and that
345
+ * read is bounded. Past the bound `shown === searchTotal` is satisfied by a
346
+ * block holding 100 of 400 posts, and the wording that claims to have looked
347
+ * everywhere is exactly the branch it would take.
348
+ *
349
+ * Both the miss and the hit disclose off this one answer, so the two can
350
+ * never disagree about the same block.
351
+ */ const searchScope = ({ perPage, shown, searchTotal, searchCapped })=>{
352
+ var _toCount;
353
+ const paginated = ((_toCount = toCount(perPage, 0)) != null ? _toCount : 0) > 0;
354
+ return {
355
+ paginated,
356
+ truncated: typeof searchTotal === 'number' ? shown < searchTotal || Boolean(searchCapped) : paginated || Boolean(searchCapped)
357
+ };
358
+ };
359
+ /**
360
+ * Repeats its children once per published entry of a content collection
361
+ * (AGL-551) — the collections sibling of the dataset repeatable. The tenant
362
+ * expands it at compose time with `{{entry.*}}` tokens; in the besigner the
363
+ * template renders once with literal tokens, matching the repeatable UX.
364
+ *
365
+ * Search (AGL-1516): with `search` on, a toolbar search box filters the
366
+ * rendered entry clones client-side against the server-stamped
367
+ * `searchIndex` — fuzzy, via the same Fuse the icon picker uses. The clones
368
+ * arrive as `children` in stamp order, one group of template roots per
369
+ * entry, so group N of the children IS entry N of the index. Matches keep
370
+ * the list's own order rather than Fuse's score order: a blog list is
371
+ * chronological, and search narrows it, it does not reshuffle it. In the
372
+ * besigner the field renders as an inert affordance (the template renders
373
+ * once with literal tokens and there is no index to search), matching how
374
+ * Category Pills go inert on editing surfaces.
375
+ */ const CollectionEntries = /*#__PURE__*/ forwardRef(// collectionSlug/entriesLimit/filter*/search* are compose-time or
376
+ // search-only attributes: strip so they never hit the DOM.
377
+ (_0, _1)=>{
378
+ let _ref = [
379
+ _0,
380
+ _1
381
+ ], [_ref1, ..._rest] = _ref, { collectionSlug, entriesLimit, filterCategory, filterTag, perPage, page, firstPageOnly, search, searchMode, searchPlaceholder, searchIndex, searchTotal, searchCapped, children } = _ref1, props = _object_without_properties_loose(_ref1, [
382
+ "collectionSlug",
383
+ "entriesLimit",
384
+ "filterCategory",
385
+ "filterTag",
386
+ "perPage",
387
+ "page",
388
+ "firstPageOnly",
389
+ "search",
390
+ "searchMode",
391
+ "searchPlaceholder",
392
+ "searchIndex",
393
+ "searchTotal",
394
+ "searchCapped",
395
+ "children"
396
+ ]), [ref] = _rest;
397
+ const { suppressNavigation } = useContext(Aglyn.ScreenLinkContext);
398
+ /** `filter` mode only — `suggest` owns its own query (AGL-1516). */ const [query, setQuery] = useState('');
399
+ const items = search ? searchIndex : undefined;
400
+ const fuzzy = useMemo(()=>(items == null ? void 0 : items.length) ? // results page this block's panel links to — a query the panel
401
+ // forgave must not come back empty from "View all results".
402
+ new Fuse(items, _extends({}, Aglyn.COLLECTION_SEARCH_FUSE_OPTIONS)) : null, [
403
+ items
404
+ ]);
405
+ if (!search) {
406
+ return /*#__PURE__*/ _jsx(MuiStack, _extends({
407
+ ref: ref,
408
+ spacing: 4
409
+ }, props, {
410
+ children: children
411
+ }));
412
+ }
413
+ const childArray = Children.toArray(children);
414
+ // Template roots per entry. Every entry clones the same template, so the
415
+ // children divide evenly; anything else means the children are not the
416
+ // stamped clones, and filtering blind would hide the wrong cards —
417
+ // fail open and render everything.
418
+ const groupSize = (items == null ? void 0 : items.length) && childArray.length % items.length === 0 ? childArray.length / items.length : 0;
419
+ // `suggest` draws its rows from the INDEX and never touches the clones,
420
+ // so the clone-alignment precondition is a `filter` precondition only.
421
+ // A block whose children do not divide evenly still suggests correctly;
422
+ // requiring `groupSize` here would silently kill the panel on exactly
423
+ // the templates that made the fail-open necessary.
424
+ const mode = searchMode === 'suggest' ? 'suggest' : 'filter';
425
+ const live = !suppressNavigation && Boolean(fuzzy) && (mode === 'suggest' || groupSize > 0);
426
+ const trimmed = query.trim();
427
+ let visible = children;
428
+ /**
429
+ * The one line under the results that says what the search covered —
430
+ * either a miss scoped to the set that came back empty, or a hit scoped
431
+ * to the window it was drawn from. Never both.
432
+ */ let scopeNote = null;
433
+ if (mode === 'suggest') {
434
+ // The grid is deliberately untouched (Figma 496:1218): the panel is an
435
+ // overlay on a page the reader is still reading, not a filter.
436
+ visible = children;
437
+ } else if (live && trimmed && fuzzy) {
438
+ var _ref2;
439
+ const matched = new Set(fuzzy.search(trimmed).map((result)=>result.refIndex));
440
+ visible = childArray.filter((_, index)=>matched.has(Math.floor(index / groupSize)));
441
+ // HONEST scope (AGL-1516, AGL-2569): this block holds whatever window
442
+ // the expansion gave it, and pretending the search was global would
443
+ // turn every miss into a false "this post does not exist" — and every
444
+ // hit into a result set that looks like the whole answer.
445
+ const shown = (_ref2 = items == null ? void 0 : items.length) != null ? _ref2 : 0;
446
+ const { paginated, truncated } = searchScope({
447
+ perPage,
448
+ shown,
449
+ searchTotal,
450
+ searchCapped
451
+ });
452
+ if (!visible.length) {
453
+ scopeNote = /*#__PURE__*/ _jsx(Typography, {
454
+ variant: "body2",
455
+ sx: {
456
+ color: 'text.secondary'
457
+ },
458
+ children: !truncated ? `No matches for “${trimmed}”.` : paginated ? `No matches for “${trimmed}” on this page — other pages ` + 'are not searched.' : `No matches for “${trimmed}” in the ${shown} entries ` + 'shown here — the rest of the collection is not searched.'
459
+ });
460
+ } else if (truncated) {
461
+ // The hit half of the same honesty (AGL-2569). Matches are drawn from
462
+ // the same window the miss apologizes for, they carry no pager of
463
+ // their own, and nothing else on screen says the pages behind this
464
+ // one went unread — so a list that happens to be partial reads as
465
+ // complete. A block that holds the whole collection says nothing
466
+ // here: a disclaimer on a complete answer is noise, and every small
467
+ // site would carry it.
468
+ //
469
+ // No route out to `/search?q=` (AGL-88): the suggestion panel links
470
+ // there because it is the panel's only possible answer, but this
471
+ // mode has a working list under the field and that page does not
472
+ // exist yet. Trading a silent partial answer for a dead link is not
473
+ // the trade.
474
+ scopeNote = /*#__PURE__*/ _jsx(Typography, {
475
+ variant: "body2",
476
+ sx: {
477
+ color: 'text.secondary'
478
+ },
479
+ children: paginated ? 'Showing matches on this page — other pages are not searched.' : `Showing matches in the ${shown} entries here — the rest of ` + 'the collection is not searched.'
480
+ });
481
+ }
482
+ }
483
+ // The field renders when there is something to search, and as an inert
484
+ // affordance on editing surfaces so the author sees what they enabled.
485
+ // A live surface with nothing stamped (unknown collection, zero entries)
486
+ // renders no field at all — a search box over nothing is a lie.
487
+ const showField = suppressNavigation || live;
488
+ const field = !showField ? null : mode === 'suggest' ? // Rows in Fuse's SCORE order, the opposite of what `filter` does and
489
+ // deliberately: filtering narrows a chronological feed, so reshuffling
490
+ // it would be a second, unasked-for change to what the reader is
491
+ // looking at. A suggestion list is not a feed — it is an answer to a
492
+ // question, and the best answer belongs first.
493
+ /*#__PURE__*/ _jsx(SuggestSearchBox, _extends({
494
+ fuzzy: fuzzy
495
+ }, items ? {
496
+ items
497
+ } : {}, searchPlaceholder === undefined ? {} : {
498
+ placeholder: searchPlaceholder
499
+ }, suppressNavigation ? {
500
+ inert: true
501
+ } : {}, {
502
+ // This block searched the entries IT rendered — a page window, an
503
+ // `entriesLimit` slice, or a bounded read. Never "this collection".
504
+ emptyText: (text)=>`No matches for “${text}” on this page.`
505
+ })) : /*#__PURE__*/ _jsx(SearchField, _extends({
506
+ value: query,
507
+ landmark: true
508
+ }, searchPlaceholder === undefined ? {} : {
509
+ placeholder: searchPlaceholder
510
+ }, suppressNavigation ? {
511
+ inert: true
512
+ } : {}, {
513
+ onQuery: setQuery
514
+ }));
515
+ return /*#__PURE__*/ _jsxs(MuiStack, _extends({
516
+ ref: ref,
517
+ spacing: 4
518
+ }, props, {
519
+ children: [
520
+ field,
521
+ visible,
522
+ scopeNote
523
+ ]
524
+ }));
525
+ });
526
+ CollectionEntries.displayName = 'AglynCollectionEntries';
527
+ export const collectionEntriesSchema = {
528
+ $id: ENTRIES_ID,
529
+ pluginId: BUNDLE_ID,
530
+ displayName: 'Collection Entries',
531
+ description: 'Repeats its children once per entry in a content collection.',
532
+ category: Aglyn.ComponentCategory.DATA_DISPLAY,
533
+ icon: {
534
+ path: mdiPostOutline.path,
535
+ sx: {
536
+ color: 'secondary.main'
537
+ }
538
+ },
539
+ attributes: [
540
+ {
541
+ name: 'collectionSlug',
542
+ label: 'Collection slug',
543
+ description: 'Content collection whose published entries the children repeat ' + 'over (e.g. "blog"). Leave blank on a list-template screen to use ' + 'the collection from the URL.',
544
+ component: Aglyn.FieldComponentType.TEXT_FIELD
545
+ },
546
+ {
547
+ name: 'entriesLimit',
548
+ label: 'Entries limit',
549
+ description: 'Maximum entries rendered (blank = all, capped at 100).',
550
+ component: Aglyn.FieldComponentType.TEXT_FIELD,
551
+ type: 'number'
552
+ },
553
+ {
554
+ name: 'filterCategory',
555
+ label: 'Filter by category',
556
+ description: 'Only entries in this category repeat — the category name or its ' + 'stable id both match (e.g. "Guides"). Blank = no category filter.',
557
+ component: Aglyn.FieldComponentType.TEXT_FIELD
558
+ },
559
+ {
560
+ name: 'filterTag',
561
+ label: 'Filter by tag',
562
+ description: 'Only entries carrying this tag repeat (e.g. "nextjs"). Blank = ' + 'no tag filter.',
563
+ component: Aglyn.FieldComponentType.TEXT_FIELD
564
+ },
565
+ {
566
+ name: 'perPage',
567
+ label: 'Entries per page',
568
+ description: 'Paginate the list: entries per page (blank = no pagination). ' + 'Pairs with the page from /{collection}/page/{n}.',
569
+ component: Aglyn.FieldComponentType.TEXT_FIELD,
570
+ type: 'number'
571
+ },
572
+ {
573
+ name: 'page',
574
+ label: 'Page',
575
+ description: '1-based page to render when Entries per page is set.',
576
+ component: Aglyn.FieldComponentType.TEXT_FIELD,
577
+ type: 'number'
578
+ },
579
+ {
580
+ name: 'firstPageOnly',
581
+ label: 'Only on page 1',
582
+ description: 'Show this block on the first page of the listing only. Use it ' + 'for a featured or lead card: without it the same entry repeats ' + 'at the top of every /page/{n}, above a page of different ones.',
583
+ component: Aglyn.FieldComponentType.SWITCH
584
+ },
585
+ {
586
+ name: 'search',
587
+ label: 'Search',
588
+ description: 'Show a search box that filters the rendered entries by title ' + 'and excerpt as the reader types. It searches the entries this ' + 'block rendered — whatever paging, an entry limit or the ' + '100-entry cap left it holding, and it says so on a miss.',
589
+ component: Aglyn.FieldComponentType.SWITCH
590
+ },
591
+ {
592
+ name: 'searchMode',
593
+ label: 'When the reader types',
594
+ description: 'Filter the entries in place, or leave the list alone and open a ' + 'dropdown of matching entries under the box — each one a link, ' + 'ending in "View all results" for a search across the whole site.',
595
+ component: Aglyn.FieldComponentType.SELECT,
596
+ options: COLLECTION_SEARCH_MODE_OPTIONS.map((option)=>_extends({}, option)),
597
+ // Meaningless while there is no search box to type in.
598
+ condition: {
599
+ when: 'search',
600
+ is: true
601
+ }
602
+ },
603
+ {
604
+ name: 'searchPlaceholder',
605
+ label: 'Search placeholder',
606
+ description: 'Hint text inside this block’s own search box — the one that ' + 'filters the entries below it as a reader types. Blank shows ' + '"Search posts…".',
607
+ component: Aglyn.FieldComponentType.TEXT_FIELD,
608
+ // Meaningless while there is no search box to hint in.
609
+ condition: {
610
+ when: 'search',
611
+ is: true
612
+ }
613
+ },
614
+ // `searchIndex` and `searchTotal` are deliberately NOT attributes: both
615
+ // are server-stamped by expandCollectionEntries, like Category Pills'
616
+ // `items` and Related Posts' `entries`.
617
+ {
618
+ name: 'spacing',
619
+ label: 'Spacing',
620
+ description: 'Defines the space/gap between entries.',
621
+ component: Aglyn.FieldComponentType.TEXT_FIELD,
622
+ type: 'number'
623
+ }
624
+ ]
625
+ };
626
+ /** A still-unresolved `{{token}}` (no entry context on this render). */ const UNRESOLVED_TOKEN = /^\{\{[^}]+\}\}$/;
627
+ const renderInlines = (inlines, links)=>inlines.map((item, index)=>{
628
+ if (item.type === 'bold') return /*#__PURE__*/ _jsx("strong", {
629
+ children: item.text
630
+ }, index);
631
+ if (item.type === 'italic') return /*#__PURE__*/ _jsx("em", {
632
+ children: item.text
633
+ }, index);
634
+ if (item.type !== 'link') return /*#__PURE__*/ _jsx("span", {
635
+ children: item.text
636
+ }, index);
637
+ // Internal paths route through AppLink for client-side navigation
638
+ // (AGL-582); external links stay plain anchors. Editing surfaces render
639
+ // the link look without an href so clicks never navigate. A target named
640
+ // by REFERENCE resolves against the routing map first (AGL-3118), and one
641
+ // whose entry, listing or screen is gone renders as the words alone —
642
+ // never as an anchor holding the stored value.
643
+ const link = Aglyn.resolveMarkdownLink(item.href, links);
644
+ if (link.kind === 'text') return /*#__PURE__*/ _jsx("span", {
645
+ children: item.text
646
+ }, index);
647
+ if (link.kind === 'inert') {
648
+ return /*#__PURE__*/ _jsx(MuiLink, {
649
+ component: "span",
650
+ sx: {
651
+ cursor: 'default'
652
+ },
653
+ children: item.text
654
+ }, index);
655
+ }
656
+ return link.kind === 'internal' ? /*#__PURE__*/ _jsx(AppLink, {
657
+ href: link.href,
658
+ children: item.text
659
+ }, index) : /*#__PURE__*/ _jsx(MuiLink, {
660
+ href: link.href,
661
+ children: item.text
662
+ }, index);
663
+ });
664
+ /**
665
+ * Renders a content entry's markdown-lite body as themed MUI elements
666
+ * (AGL-551): headings, paragraphs, lists, and images pick up the site
667
+ * theme's typography instead of the old unthemed article HTML. Parsing is
668
+ * pure, so the full body server-renders (SEO keeps the article text).
669
+ */ const CollectionEntryBody = /*#__PURE__*/ forwardRef((props, ref)=>{
670
+ // `intrinsicSizes` is destructured out rather than left in `rest`: `rest` is
671
+ // spread onto a Box, and a composition-stamped object would reach the DOM as
672
+ // an attribute React has no idea what to do with.
673
+ const { markdown, intrinsicSizes } = props, rest = _object_without_properties_loose(props, [
674
+ "markdown",
675
+ "intrinsicSizes"
676
+ ]);
677
+ // Node styles ride the renderer-merged sx; recompose (stack.ts pattern).
678
+ const nodeSx = Array.isArray(props['sx']) ? props['sx'] : [
679
+ props['sx']
680
+ ];
681
+ // The routing map travels with the suppression flag: both decide what a
682
+ // link in this body renders as (AGL-3118).
683
+ const { suppressNavigation, screens } = useContext(Aglyn.ScreenLinkContext);
684
+ const links = useMemo(()=>({
685
+ suppressNavigation,
686
+ screens
687
+ }), [
688
+ suppressNavigation,
689
+ screens
690
+ ]);
691
+ // The site being rendered, for image blocks (AGL-1686) — read once here
692
+ // because hooks cannot run inside the block map below.
693
+ const { hostId } = Aglyn.useSite();
694
+ const source = (markdown != null ? markdown : '').trim();
695
+ const unresolved = !source || UNRESOLVED_TOKEN.test(source);
696
+ const blocks = useMemo(()=>unresolved ? [] : Aglyn.parseMarkdownLite(source), [
697
+ source,
698
+ unresolved
699
+ ]);
700
+ // Heading anchors, on the same terms as the Markdown element (AGL-1162).
701
+ // Without them a blog article could not carry an "On this page" aside at
702
+ // all: the Table of Contents element emits `#slug` links, and there was
703
+ // nothing on this page for them to land on.
704
+ const slugs = useMemo(()=>Aglyn.markdownHeadingSlugs(blocks), [
705
+ blocks
706
+ ]);
707
+ if (unresolved) {
708
+ // Editing surfaces get an affordance; the published site renders
709
+ // nothing rather than a literal token.
710
+ if (!suppressNavigation) return /*#__PURE__*/ _jsx(Box, _extends({
711
+ ref: ref
712
+ }, rest));
713
+ return /*#__PURE__*/ _jsx(Box, _extends({
714
+ ref: ref
715
+ }, rest, {
716
+ sx: [
717
+ {
718
+ p: 2,
719
+ border: '1px dashed',
720
+ borderColor: 'divider',
721
+ color: 'text.secondary',
722
+ fontSize: 12,
723
+ fontFamily: 'system-ui, sans-serif'
724
+ },
725
+ ...nodeSx
726
+ ],
727
+ children: 'Entry body — the {{entry.body}} markdown renders here'
728
+ }));
729
+ }
730
+ return /*#__PURE__*/ _jsx(Box, _extends({
731
+ ref: ref
732
+ }, rest, {
733
+ children: blocks.map((block, index)=>{
734
+ if (block.type === 'heading') {
735
+ return /*#__PURE__*/ _jsx(Typography, {
736
+ id: slugs[index],
737
+ variant: block.level === 2 ? 'h4' : 'h5',
738
+ component: block.level === 2 ? 'h2' : 'h3',
739
+ gutterBottom: true,
740
+ sx: {
741
+ scrollMarginTop: `${Aglyn.HEADING_ANCHOR_SCROLL_MARGIN}px`
742
+ },
743
+ children: renderInlines(block.inlines, links)
744
+ }, index);
745
+ }
746
+ if (block.type === 'image') {
747
+ return /*#__PURE__*/ _jsx(Box, _extends({
748
+ component: "img"
749
+ }, Aglyn.mediaBodyImageAttributes({
750
+ src: block.src,
751
+ hostId,
752
+ size: intrinsicSizes == null ? void 0 : intrinsicSizes[block.src]
753
+ }), {
754
+ alt: block.alt,
755
+ // Paired with the `width`/`height` above — see the Markdown
756
+ // element's copy of this: without it the height hint outlives
757
+ // the `maxWidth` cap and squashes the picture.
758
+ sx: {
759
+ maxWidth: '100%',
760
+ height: 'auto',
761
+ borderRadius: 1,
762
+ my: 1
763
+ }
764
+ }, Aglyn.DEFERRED_IMAGE_ATTRIBUTES), index);
765
+ }
766
+ if (block.type === 'list') {
767
+ return /*#__PURE__*/ _jsx(Box, {
768
+ component: "ul",
769
+ sx: {
770
+ lineHeight: 1.7,
771
+ pl: 3
772
+ },
773
+ children: block.items.map((item, itemIndex)=>/*#__PURE__*/ _jsx("li", {
774
+ children: renderInlines(item, links)
775
+ }, itemIndex))
776
+ }, index);
777
+ }
778
+ // A numbered list (AGL-1320) — a real `<ol>`, so the markers are the
779
+ // browser's and `start` survives: a notice that resumes at 7 has to
780
+ // read as 7 for the enumeration to mean anything.
781
+ if (block.type === 'orderedList') {
782
+ return /*#__PURE__*/ _jsx(Box, {
783
+ component: "ol",
784
+ start: block.start,
785
+ sx: {
786
+ lineHeight: 1.7,
787
+ pl: 3
788
+ },
789
+ children: block.items.map((item, itemIndex)=>/*#__PURE__*/ _jsx("li", {
790
+ children: renderInlines(item, links)
791
+ }, itemIndex))
792
+ }, index);
793
+ }
794
+ // Code blocks and tables (AGL-974). Both scroll rather than wrap:
795
+ // a wrapped code line or a squeezed table column loses the very
796
+ // structure that made the block worth writing.
797
+ if (block.type === 'code') {
798
+ return /*#__PURE__*/ _jsx(Box, {
799
+ component: "pre",
800
+ sx: {
801
+ my: 2,
802
+ p: 2,
803
+ overflowX: 'auto',
804
+ borderRadius: 1,
805
+ bgcolor: 'action.hover',
806
+ fontFamily: 'monospace',
807
+ fontSize: 14
808
+ },
809
+ children: /*#__PURE__*/ _jsx("code", {
810
+ children: block.text
811
+ })
812
+ }, index);
813
+ }
814
+ if (block.type === 'table') {
815
+ return /*#__PURE__*/ _jsx(Box, {
816
+ sx: {
817
+ my: 2,
818
+ overflowX: 'auto'
819
+ },
820
+ children: /*#__PURE__*/ _jsxs(Box, {
821
+ component: "table",
822
+ sx: {
823
+ borderCollapse: 'collapse',
824
+ width: '100%',
825
+ '& th, & td': {
826
+ border: '1px solid',
827
+ borderColor: 'divider',
828
+ px: 1.5,
829
+ py: 1
830
+ },
831
+ '& th': {
832
+ bgcolor: 'action.hover'
833
+ }
834
+ },
835
+ children: [
836
+ /*#__PURE__*/ _jsx("thead", {
837
+ children: /*#__PURE__*/ _jsx("tr", {
838
+ children: block.header.map((cell, cellIndex)=>{
839
+ var _block_align_cellIndex;
840
+ return /*#__PURE__*/ _jsx("th", {
841
+ style: {
842
+ textAlign: (_block_align_cellIndex = block.align[cellIndex]) != null ? _block_align_cellIndex : 'left'
843
+ },
844
+ children: renderInlines(cell, links)
845
+ }, cellIndex);
846
+ })
847
+ })
848
+ }),
849
+ /*#__PURE__*/ _jsx("tbody", {
850
+ children: block.rows.map((row, rowIndex)=>/*#__PURE__*/ _jsx("tr", {
851
+ children: row.map((cell, cellIndex)=>{
852
+ var _block_align_cellIndex;
853
+ return /*#__PURE__*/ _jsx("td", {
854
+ style: {
855
+ textAlign: (_block_align_cellIndex = block.align[cellIndex]) != null ? _block_align_cellIndex : 'left'
856
+ },
857
+ children: renderInlines(cell, links)
858
+ }, cellIndex);
859
+ })
860
+ }, rowIndex))
861
+ })
862
+ ]
863
+ })
864
+ }, index);
865
+ }
866
+ // The article template's pull-quote (AGL-1315, Figma 170:225):
867
+ // larger italic prose behind a left accent. Palette tokens only, so
868
+ // it follows the site theme in both modes.
869
+ if (block.type === 'quote') {
870
+ return /*#__PURE__*/ _jsx(Typography, {
871
+ component: "blockquote",
872
+ sx: {
873
+ my: 3,
874
+ mx: 0,
875
+ pl: 2.5,
876
+ borderLeft: '3px solid',
877
+ borderColor: 'primary.main',
878
+ fontStyle: 'italic',
879
+ fontSize: '1.25em',
880
+ lineHeight: 1.6,
881
+ color: 'text.primary'
882
+ },
883
+ children: renderInlines(block.inlines, links)
884
+ }, index);
885
+ }
886
+ return /*#__PURE__*/ _jsx(Typography, {
887
+ variant: "body1",
888
+ sx: {
889
+ lineHeight: 1.7
890
+ },
891
+ gutterBottom: true,
892
+ children: renderInlines(block.inlines, links)
893
+ }, index);
894
+ })
895
+ }));
896
+ });
897
+ CollectionEntryBody.displayName = 'AglynCollectionEntryBody';
898
+ export const collectionEntryBodySchema = {
899
+ $id: ENTRY_BODY_ID,
900
+ pluginId: BUNDLE_ID,
901
+ displayName: 'Entry Body',
902
+ description: "The current entry's markdown body, on an entry template.",
903
+ category: Aglyn.ComponentCategory.TEXT,
904
+ icon: {
905
+ path: mdiTextLong.path,
906
+ sx: {
907
+ color: 'secondary.main'
908
+ }
909
+ },
910
+ flags: {
911
+ selfClosing: Aglyn.FEATURE_FLAG.ENABLED
912
+ },
913
+ attributes: [
914
+ {
915
+ name: 'markdown',
916
+ label: 'Markdown',
917
+ description: 'Markdown-lite content. Keep {{entry.body}} on entry-template ' + "screens so each entry's body renders here.",
918
+ component: Aglyn.FieldComponentType.TEXT_FIELD
919
+ }
920
+ ]
921
+ };
922
+ /** The frame's 3-up (Figma 170:242) when nothing usable is authored. */ const RELATED_DEFAULT_COLUMNS = 3;
923
+ /**
924
+ * The cover's PROPORTION in the card grid.
925
+ *
926
+ * ONE ratio for every card, rather than each record's own shape: the titles
927
+ * under the covers only line up when the covers are the same height, and a
928
+ * grid of honest shapes is a ragged grid.
929
+ *
930
+ * The ratio is the shape the console asks authors for — 1200 x 630, the same
931
+ * image that heads the entry and doubles as its share card — so a cover
932
+ * authored as instructed loses nothing to the crop. The frame's own card is
933
+ * 445 x 180 (Figma 170:243), which is 2.47 against that art's 1.90: held
934
+ * there, `objectFit: cover` discarded 23% of every card's picture at every
935
+ * width. Cover art is usually a composed lockup, so what it discards is the
936
+ * title set on it.
937
+ *
938
+ * Before the ratio it was a fixed 180px height, which was the frame's number
939
+ * without the frame's shape: a card is 445 wide there, the grid's columns
940
+ * grow with the viewport and 180 does not, so every extra pixel of width
941
+ * flattened the box further — 3.57:1 by a 2000px viewport. A ratio holds the
942
+ * shape at every width; this ratio holds the picture as well.
943
+ */ const RELATED_COVER_ASPECT = '1200 / 630';
944
+ /**
945
+ * The card's category chip (AGL-1457). ONE fixed token pair for every
946
+ * category: colour-coding a chip per category needs conditional styling,
947
+ * which is not expressible yet (AGL-1307), and inventing a palette here
948
+ * would have to be unpicked when it is. Same shape as the tag chips Entry
949
+ * Meta already renders, so the two blocks read as one vocabulary.
950
+ */ const relatedChipSx = {
951
+ alignSelf: 'flex-start',
952
+ borderColor: 'divider',
953
+ color: 'text.secondary',
954
+ '& .MuiChip-label': {
955
+ // 9px, the frame's own inset on a 69 x 24 chip.
956
+ px: 1.125,
957
+ // The frame sets this label in the same uppercase, letter-spaced step as
958
+ // the kicker above an article title, which is what `overline` IS in the
959
+ // site theme. Naming the step keeps the chip on the theme's scale instead
960
+ // of a hand-written size that drifts the moment the theme moves.
961
+ typography: 'overline',
962
+ // `size="small"` gives the chip the frame's 24px height, and `overline`
963
+ // carries a 2.66 line-height meant for a standalone kicker with room
964
+ // around it. Inside a fixed-height chip the box sets the height, so the
965
+ // line collapses to it and the label centres.
966
+ lineHeight: 1
967
+ }
968
+ };
969
+ /** The heading step the block has always emitted. */ const RELATED_DEFAULT_HEADING_VARIANT = 'h5';
970
+ /** The card-title step the block has always emitted. */ const RELATED_DEFAULT_TITLE_VARIANT = 'subtitle1';
971
+ /**
972
+ * The instant the sample cards are dated. A FIXED one, never `Date.now()`:
973
+ * the canvas has to render the same bytes every time it opens, and this is
974
+ * the date the Date-format labels use as their worked example — so the
975
+ * dropdown and the cards it previews agree word for word.
976
+ */ const RELATED_SAMPLE_PUBLISHED_AT = {
977
+ seconds: Date.UTC(2026, 7, 9) / 1000
978
+ };
979
+ /**
980
+ * Titles for the sample cards. Deliberately unmistakable as copy: the author
981
+ * is looking at a LAYOUT, and a plausible-looking headline here is one an
982
+ * author can mistake for a post that exists — or worse, design around.
983
+ */ const RELATED_SAMPLE_TITLES = [
984
+ 'Sample related post',
985
+ 'A second sample post',
986
+ 'A third sample post'
987
+ ];
988
+ /** Category chip text for the sample cards; names the field, not a taxonomy. */ const RELATED_SAMPLE_CATEGORY = 'Category';
989
+ /** 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.';
990
+ /**
991
+ * Stand-in posts for editing surfaces (AGL-2486).
992
+ *
993
+ * Related posts are resolved FROM the routed entry, and a besigner canvas has
994
+ * no routed entry — so the block used to draw a one-line dashed strip and the
995
+ * author styled a layout they could not see. The sample is the block's REAL
996
+ * markup, at the author's own settings, which is the only way the canvas can
997
+ * answer "what will this look like".
998
+ *
999
+ * It exists only where {@link ScreenLinkContext.suppressNavigation} is on —
1000
+ * the besigner canvas and Preview. A published render reaches this function
1001
+ * on no path at all, so no visitor can ever be shown a post that does not
1002
+ * exist.
1003
+ *
1004
+ * The count follows the author's own `limit` so a 6-post rail at 3 columns
1005
+ * previews as two rows, and is bounded by the same ceiling the server
1006
+ * applies rather than a second one.
1007
+ */ const sampleRelatedEntries = (limit, dateFormat)=>{
1008
+ const count = Math.min(// `|| default` also rejects 0, which `toCount` would otherwise accept and
1009
+ // preview as an empty rail — the one thing the sample exists to avoid.
1010
+ toCount(limit, Aglyn.COLLECTION_RELATED_DEFAULT_LIMIT) || Aglyn.COLLECTION_RELATED_DEFAULT_LIMIT, Aglyn.COLLECTION_RELATED_MAX);
1011
+ const date = Aglyn.formatCollectionEntryDate(RELATED_SAMPLE_PUBLISHED_AT, dateFormat);
1012
+ return Array.from({
1013
+ length: count
1014
+ }, (_, index)=>{
1015
+ var _RELATED_SAMPLE_TITLES_index;
1016
+ return {
1017
+ title: (_RELATED_SAMPLE_TITLES_index = RELATED_SAMPLE_TITLES[index]) != null ? _RELATED_SAMPLE_TITLES_index : `Sample related post ${index + 1}`,
1018
+ // Empty, not a plausible path: every surface that renders the sample
1019
+ // suppresses navigation, so the title is text rather than an anchor and
1020
+ // this is never read. A real-looking href would be a link to nowhere the
1021
+ // moment that stopped being true.
1022
+ url: '',
1023
+ date,
1024
+ category: RELATED_SAMPLE_CATEGORY,
1025
+ excerpt: RELATED_SAMPLE_EXCERPT
1026
+ };
1027
+ });
1028
+ };
1029
+ /**
1030
+ * Lists other entries of the same collection sharing the current entry's
1031
+ * category or a tag (AGL-582). The tenant stamps `entries` at compose time
1032
+ * on entry renders; without them the published site renders nothing and an
1033
+ * editing surface renders sample cards (AGL-2486).
1034
+ *
1035
+ * Two layouts (AGL-1457). `list` is the plain-link list that has always
1036
+ * shipped and stays the default — the block is live on every blog entry, so
1037
+ * a new default would restyle published pages nobody asked about. `cards` is
1038
+ * the article frame's grid: cover, category chip, title, at an author-set
1039
+ * column count.
1040
+ *
1041
+ * The block owns its markup, so nothing inside a card is a node the Styles
1042
+ * panel can select. Every part it renders therefore needs an attribute, or
1043
+ * it is unauthorable by construction — which is what the type steps and the
1044
+ * `show*` switches are (AGL-2486).
1045
+ */ const CollectionRelated = /*#__PURE__*/ forwardRef((props, ref)=>{
1046
+ // `limit` and `dateFormat` are compose-time: the tenant resolves them
1047
+ // while stamping `entries`; strip them so they never hit the DOM. The
1048
+ // rest are read here, so they must not reach it either.
1049
+ const { heading, limit, entries, showCover, layout, columns, headingVariant, titleVariant, showDate, dateFormat, showCategory, showExcerpt } = props, rest = _object_without_properties_loose(props, [
1050
+ "heading",
1051
+ "limit",
1052
+ "entries",
1053
+ "showCover",
1054
+ "layout",
1055
+ "columns",
1056
+ "headingVariant",
1057
+ "titleVariant",
1058
+ "showDate",
1059
+ "dateFormat",
1060
+ "showCategory",
1061
+ "showExcerpt"
1062
+ ]);
1063
+ // Node styles ride the renderer-merged sx; recompose (stack.ts pattern).
1064
+ const nodeSx = Array.isArray(props['sx']) ? props['sx'] : [
1065
+ props['sx']
1066
+ ];
1067
+ const { suppressNavigation } = useContext(Aglyn.ScreenLinkContext);
1068
+ // The resolver every other surface shares (AGL-1215): a stamped `media:`
1069
+ // reference becomes a CDN URL HERE, not in the document, so one
1070
+ // reference keeps working across sites. Called before the early return —
1071
+ // it is a hook.
1072
+ const { hostId } = Aglyn.useSite();
1073
+ // An editing surface has no routed entry, so nothing is stamped and the
1074
+ // block used to be the one part of the page that was not WYSIWYG. The
1075
+ // sample stands in for exactly that gap, and only there.
1076
+ const sample = !(entries == null ? void 0 : entries.length) && Boolean(suppressNavigation);
1077
+ const items = (entries == null ? void 0 : entries.length) ? entries : sample ? sampleRelatedEntries(limit, Aglyn.normalizeCollectionEntryDateFormat(dateFormat)) : [];
1078
+ if (!items.length) return /*#__PURE__*/ _jsx(Box, _extends({
1079
+ ref: ref
1080
+ }, rest));
1081
+ const title = heading != null ? heading : 'Related articles';
1082
+ const headingNode = title ? /*#__PURE__*/ _jsx(Typography, {
1083
+ variant: headingVariant || RELATED_DEFAULT_HEADING_VARIANT,
1084
+ component: "h2",
1085
+ children: title
1086
+ }) : null;
1087
+ /**
1088
+ * The card title, as a HEADING in the site theme.
1089
+ *
1090
+ * It used to be a bare `AppLink`, which is MUI's `Link` — `primary.main`
1091
+ * and `underline="always"` — so the one thing a related card is FOR read
1092
+ * as a default browser link on a themed page, at no heading level, and
1093
+ * with no attribute able to touch it. The type step now comes from the
1094
+ * theme's own rungs and the anchor inherits the colour, exactly as the
1095
+ * Entry Author card's byline link does.
1096
+ *
1097
+ * `linked` is false wherever the whole card is already the anchor: an
1098
+ * `<a>` inside an `<a>` is invalid markup that browsers silently unnest.
1099
+ */ const titleNode = (entry, linked)=>/*#__PURE__*/ _jsx(Typography, {
1100
+ variant: titleVariant || RELATED_DEFAULT_TITLE_VARIANT,
1101
+ component: "h3",
1102
+ children: linked && !suppressNavigation && entry.url ? /*#__PURE__*/ _jsx(AppLink, {
1103
+ href: entry.url,
1104
+ sx: {
1105
+ color: 'inherit'
1106
+ },
1107
+ underline: "hover",
1108
+ children: entry.title
1109
+ }) : entry.title
1110
+ });
1111
+ const dateNode = (entry)=>showDate !== false && entry.date ? /*#__PURE__*/ _jsx(Typography, {
1112
+ variant: "caption",
1113
+ sx: {
1114
+ color: 'text.secondary'
1115
+ },
1116
+ children: entry.date
1117
+ }) : null;
1118
+ const excerptNode = (entry)=>showExcerpt && entry.excerpt ? /*#__PURE__*/ _jsx(Typography, {
1119
+ variant: "body2",
1120
+ sx: {
1121
+ color: 'text.secondary'
1122
+ },
1123
+ children: entry.excerpt
1124
+ }) : null;
1125
+ /**
1126
+ * The cover, when the author asked for one AND the entry has one. An
1127
+ * entry without a cover gets no box at all rather than a placeholder:
1128
+ * the related list is a real feed, and a row of grey rectangles is worse
1129
+ * than a row of titles.
1130
+ *
1131
+ * The sample cards are the exception, and for the opposite reason: they
1132
+ * stand in for posts that do not exist yet, so the cover SLOT is what
1133
+ * the author is looking at. A card grid previewing without its images is
1134
+ * not the grid being styled.
1135
+ */ const coverNode = (entry)=>{
1136
+ const src = showCover ? Aglyn.resolveMediaSrc(entry.coverImage, {
1137
+ hostId
1138
+ }) : undefined;
1139
+ if (!src) {
1140
+ if (!sample || !showCover) return null;
1141
+ return /*#__PURE__*/ _jsx(Box, {
1142
+ "aria-hidden": true,
1143
+ sx: {
1144
+ width: '100%',
1145
+ aspectRatio: RELATED_COVER_ASPECT,
1146
+ borderRadius: 1,
1147
+ backgroundColor: 'action.hover'
1148
+ }
1149
+ });
1150
+ }
1151
+ return /*#__PURE__*/ _jsx(Box, _extends({
1152
+ component: "img",
1153
+ src: src,
1154
+ // The author's own description wins, and the title stays the
1155
+ // fallback (AGL-2418). Unlike the entry hero and the event
1156
+ // thumbnail — which render empty when nothing is authored — this
1157
+ // cover is the LINK's own content, so it must carry an accessible
1158
+ // name rather than go silent.
1159
+ alt: Aglyn.renderedMediaAlt(entry.coverImageAlt, entry.title),
1160
+ sx: {
1161
+ display: 'block',
1162
+ width: '100%',
1163
+ aspectRatio: RELATED_COVER_ASPECT,
1164
+ objectFit: 'cover',
1165
+ borderRadius: 1
1166
+ }
1167
+ }, Aglyn.DEFERRED_IMAGE_ATTRIBUTES));
1168
+ };
1169
+ /**
1170
+ * The notice that says these cards are not posts (AGL-2486). Editing
1171
+ * surfaces only, and outside the card markup rather than inside it, so
1172
+ * the author is styling the same tree the site will render.
1173
+ */ const sampleNotice = sample ? /*#__PURE__*/ _jsx(Typography, {
1174
+ variant: "caption",
1175
+ sx: {
1176
+ display: 'block',
1177
+ p: 1,
1178
+ border: '1px dashed',
1179
+ borderColor: 'divider',
1180
+ borderRadius: 1,
1181
+ color: 'text.secondary',
1182
+ fontFamily: 'system-ui, sans-serif'
1183
+ },
1184
+ children: 'Sample posts — entries sharing this entry’s category or tags ' + 'replace these on the published page'
1185
+ }) : null;
1186
+ if (layout === 'cards') {
1187
+ // `toCount` rounds and rejects junk; `|| default` also rejects 0, which
1188
+ // it would otherwise accept as a column count and emit `repeat(0, …)`.
1189
+ const columnCount = toCount(columns, RELATED_DEFAULT_COLUMNS) || RELATED_DEFAULT_COLUMNS;
1190
+ /**
1191
+ * One card's contents, always with a PLAIN title: the caller wraps the
1192
+ * whole card in the anchor, because a reader aiming at a 180px cover
1193
+ * should not have to hit the line of text under it — and a second
1194
+ * anchor around the title inside that one is markup browsers unnest.
1195
+ */ const card = (entry)=>// 12px between cover, chip and title — the frame's rhythm (170:243
1196
+ // puts the chip at y=192 under a cover ending at 180, and the title
1197
+ // at y=228 under a chip ending at 216).
1198
+ /*#__PURE__*/ _jsxs(MuiStack, {
1199
+ spacing: 1.5,
1200
+ children: [
1201
+ coverNode(entry),
1202
+ showCategory !== false && entry.category ? /*#__PURE__*/ _jsx(Chip, {
1203
+ label: entry.category,
1204
+ size: "small",
1205
+ variant: "outlined",
1206
+ sx: relatedChipSx
1207
+ }) : null,
1208
+ titleNode(entry, false),
1209
+ excerptNode(entry),
1210
+ dateNode(entry)
1211
+ ]
1212
+ });
1213
+ return /*#__PURE__*/ _jsxs(Box, _extends({
1214
+ ref: ref
1215
+ }, rest, {
1216
+ // MERGE, never replace (AGL-1450) — the node's slice arrives as the
1217
+ // ARRAY `mergeSxProps` builds in leaf.tsx, so folding it into an
1218
+ // object spreads numeric keys and discards every authored property
1219
+ // while these defaults still apply. Defaults first, node's after.
1220
+ sx: [
1221
+ {
1222
+ display: 'grid',
1223
+ gridTemplateColumns: `repeat(${columnCount}, minmax(0, 1fr))`,
1224
+ alignItems: 'start',
1225
+ columnGap: 3,
1226
+ rowGap: 4
1227
+ },
1228
+ ...nodeSx
1229
+ ],
1230
+ children: [
1231
+ headingNode ? /*#__PURE__*/ _jsx(Box, {
1232
+ sx: {
1233
+ gridColumn: '1 / -1'
1234
+ },
1235
+ children: headingNode
1236
+ }) : null,
1237
+ sampleNotice ? /*#__PURE__*/ _jsx(Box, {
1238
+ sx: {
1239
+ gridColumn: '1 / -1'
1240
+ },
1241
+ children: sampleNotice
1242
+ }) : null,
1243
+ items.map((entry, index)=>{
1244
+ const linked = !suppressNavigation && Boolean(entry.url);
1245
+ return linked ? /*#__PURE__*/ _jsx(AppLink, {
1246
+ href: entry.url,
1247
+ underline: "none",
1248
+ // A grid ITEM, so it has to be a block; `color: inherit`
1249
+ // keeps the cover, chip and date reading as themed content
1250
+ // rather than as the inside of a link.
1251
+ sx: {
1252
+ color: 'inherit',
1253
+ display: 'block'
1254
+ },
1255
+ children: card(entry)
1256
+ }, index) : /*#__PURE__*/ _jsx(Box, {
1257
+ children: card(entry)
1258
+ }, index);
1259
+ })
1260
+ ]
1261
+ }));
1262
+ }
1263
+ return /*#__PURE__*/ _jsxs(MuiStack, _extends({
1264
+ ref: ref,
1265
+ spacing: 1.5
1266
+ }, rest, {
1267
+ children: [
1268
+ headingNode,
1269
+ sampleNotice,
1270
+ items.map((entry, index)=>{
1271
+ const meta = [
1272
+ showDate !== false ? entry.date : '',
1273
+ showCategory !== false ? entry.category : ''
1274
+ ].filter(Boolean);
1275
+ return /*#__PURE__*/ _jsxs(MuiStack, {
1276
+ spacing: 0.25,
1277
+ children: [
1278
+ coverNode(entry),
1279
+ titleNode(entry, true),
1280
+ excerptNode(entry),
1281
+ meta.length ? /*#__PURE__*/ _jsx(Typography, {
1282
+ variant: "caption",
1283
+ sx: {
1284
+ color: 'text.secondary'
1285
+ },
1286
+ children: meta.join(' · ')
1287
+ }) : null
1288
+ ]
1289
+ }, index);
1290
+ })
1291
+ ]
1292
+ }));
1293
+ });
1294
+ CollectionRelated.displayName = 'AglynCollectionRelated';
1295
+ export const collectionRelatedSchema = {
1296
+ $id: RELATED_ID,
1297
+ pluginId: BUNDLE_ID,
1298
+ displayName: 'Related Posts',
1299
+ description: "Other entries sharing this one's category or tags.",
1300
+ category: Aglyn.ComponentCategory.DATA_DISPLAY,
1301
+ icon: {
1302
+ path: mdiNewspaperVariantOutline.path,
1303
+ sx: {
1304
+ color: 'secondary.main'
1305
+ }
1306
+ },
1307
+ flags: {
1308
+ selfClosing: Aglyn.FEATURE_FLAG.ENABLED
1309
+ },
1310
+ attributes: [
1311
+ {
1312
+ name: 'heading',
1313
+ label: 'Heading',
1314
+ description: 'Section heading (blank hides it).',
1315
+ component: Aglyn.FieldComponentType.TEXT_FIELD
1316
+ },
1317
+ {
1318
+ name: 'limit',
1319
+ label: 'Limit',
1320
+ description: 'Most related posts listed (default 3).',
1321
+ component: Aglyn.FieldComponentType.TEXT_FIELD,
1322
+ type: 'number'
1323
+ },
1324
+ {
1325
+ name: 'layout',
1326
+ label: 'Layout',
1327
+ description: 'List keeps the plain links this block has always rendered. ' + 'Cards is the article layout: cover, category, title, in a grid.',
1328
+ component: Aglyn.FieldComponentType.SELECT,
1329
+ // Both values are REAL (AGL-1451/AGL-1453): `''` cannot survive a
1330
+ // save, so an author who switched to Cards would have no route back.
1331
+ // `list` is also the value the render falls back to, so naming it is
1332
+ // the same choice, not a second one.
1333
+ options: [
1334
+ {
1335
+ value: 'list',
1336
+ label: 'List (default)'
1337
+ },
1338
+ {
1339
+ value: 'cards',
1340
+ label: 'Card grid'
1341
+ }
1342
+ ]
1343
+ },
1344
+ {
1345
+ name: 'columns',
1346
+ label: 'Columns',
1347
+ description: 'Cards per row in the card grid. Default 3.',
1348
+ component: Aglyn.FieldComponentType.TEXT_FIELD,
1349
+ type: 'number',
1350
+ // Meaningless on the list, where each post is its own row.
1351
+ condition: {
1352
+ when: 'layout',
1353
+ is: 'cards'
1354
+ }
1355
+ },
1356
+ {
1357
+ name: 'showCover',
1358
+ label: 'Show cover',
1359
+ description: 'Show each post’s cover image. Posts without one show their ' + 'title alone rather than an empty box.',
1360
+ component: Aglyn.FieldComponentType.SWITCH
1361
+ },
1362
+ {
1363
+ name: 'headingVariant',
1364
+ label: 'Heading style',
1365
+ description: 'Which type step from the site theme the section heading above ' + 'the posts reads at. Default Heading 5.',
1366
+ component: Aglyn.FieldComponentType.SELECT,
1367
+ // The theme's own rungs, shared with the Typography element rather
1368
+ // than a second list here: two lists is how a step added to the
1369
+ // theme becomes reachable in one place and not the other.
1370
+ options: [
1371
+ ...typographyVariants
1372
+ ]
1373
+ },
1374
+ {
1375
+ name: 'titleVariant',
1376
+ label: 'Title style',
1377
+ description: 'Which type step from the site theme each post’s title reads at. ' + 'Default Subtitle 1; the title takes the surrounding text colour ' + 'rather than the link colour.',
1378
+ component: Aglyn.FieldComponentType.SELECT,
1379
+ options: [
1380
+ ...typographyVariants
1381
+ ]
1382
+ },
1383
+ {
1384
+ name: 'showDate',
1385
+ label: 'Show date',
1386
+ 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.',
1387
+ component: Aglyn.FieldComponentType.SWITCH,
1388
+ // Unset means SHOWN in the renderer above, so the switch has to
1389
+ // OPEN in that position or it lies about the block in front of
1390
+ // the author — and the first click, which writes the `true` that
1391
+ // was already in effect, changes nothing and reads as a dead
1392
+ // control (AGL-2506).
1393
+ initialValue: true
1394
+ },
1395
+ {
1396
+ name: 'dateFormat',
1397
+ label: 'Date format',
1398
+ description: 'How each post’s published date reads on its card. Site default ' + 'is the format this block has always used.',
1399
+ component: Aglyn.FieldComponentType.SELECT,
1400
+ // The formats the pure layer knows how to produce, so the list
1401
+ // cannot offer a shape nothing renders. Every value is REAL,
1402
+ // including the do-nothing one (AGL-1451/AGL-1453).
1403
+ options: [
1404
+ ...Aglyn.COLLECTION_ENTRY_DATE_FORMAT_OPTIONS
1405
+ ]
1406
+ },
1407
+ {
1408
+ name: 'showCategory',
1409
+ label: 'Show category',
1410
+ 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.',
1411
+ component: Aglyn.FieldComponentType.SWITCH,
1412
+ // Unset means SHOWN in the renderer above, so the switch has to
1413
+ // OPEN in that position or it lies about the block in front of
1414
+ // the author — and the first click, which writes the `true` that
1415
+ // was already in effect, changes nothing and reads as a dead
1416
+ // control (AGL-2506).
1417
+ initialValue: true
1418
+ },
1419
+ {
1420
+ name: 'showExcerpt',
1421
+ label: 'Show excerpt',
1422
+ description: 'Adds each post’s excerpt under its title. Posts with no excerpt ' + 'are unchanged rather than leaving a gap.',
1423
+ component: Aglyn.FieldComponentType.SWITCH
1424
+ }
1425
+ ]
1426
+ };
1427
+ const SHARE_TARGETS = [
1428
+ {
1429
+ label: 'Share on X',
1430
+ path: mdiTwitter.path,
1431
+ href: (url)=>`https://twitter.com/intent/tweet?url=${encodeURIComponent(url)}`
1432
+ },
1433
+ {
1434
+ label: 'Share on LinkedIn',
1435
+ path: mdiLinkedin.path,
1436
+ href: (url)=>'https://www.linkedin.com/sharing/share-offsite/?url=' + encodeURIComponent(url)
1437
+ },
1438
+ {
1439
+ label: 'Share on Facebook',
1440
+ path: mdiFacebook.path,
1441
+ href: (url)=>`https://www.facebook.com/sharer/sharer.php?u=${encodeURIComponent(url)}`
1442
+ }
1443
+ ];
1444
+ /**
1445
+ * Share buttons for the CURRENT page URL (AGL-582): X, LinkedIn, Facebook,
1446
+ * and copy-link. Pure client behavior — the URL is read at click time so
1447
+ * SSR and besigner renders stay markup-identical; editing surfaces no-op.
1448
+ */ const CollectionShare = /*#__PURE__*/ forwardRef((props, ref)=>{
1449
+ const { heading } = props, rest = _object_without_properties_loose(props, [
1450
+ "heading"
1451
+ ]);
1452
+ // Node styles ride the renderer-merged sx; recompose (stack.ts pattern).
1453
+ const nodeSx = Array.isArray(props['sx']) ? props['sx'] : [
1454
+ props['sx']
1455
+ ];
1456
+ const { suppressNavigation } = useContext(Aglyn.ScreenLinkContext);
1457
+ const [copied, setCopied] = useState(false);
1458
+ const title = heading != null ? heading : 'Share';
1459
+ const open = (buildHref)=>()=>{
1460
+ if (suppressNavigation || typeof window === 'undefined') return;
1461
+ window.open(buildHref(window.location.href), '_blank', 'noopener,noreferrer');
1462
+ };
1463
+ const copy = async ()=>{
1464
+ if (suppressNavigation || typeof window === 'undefined') return;
1465
+ try {
1466
+ await navigator.clipboard.writeText(window.location.href);
1467
+ setCopied(true);
1468
+ setTimeout(()=>setCopied(false), 2000);
1469
+ } catch (unused) {
1470
+ // Clipboard unavailable (permissions, http) — silently skip.
1471
+ }
1472
+ };
1473
+ return /*#__PURE__*/ _jsxs(MuiStack, _extends({
1474
+ ref: ref,
1475
+ direction: "row",
1476
+ spacing: 0.5
1477
+ }, rest, {
1478
+ // MERGE, never replace (AGL-1450) — see Entry Meta below.
1479
+ sx: [
1480
+ {
1481
+ alignItems: 'center'
1482
+ },
1483
+ ...nodeSx
1484
+ ],
1485
+ children: [
1486
+ title ? // `component="p"` because this labels a row of buttons; it is not a
1487
+ // section heading (AGL-2486). MUI's `defaultVariantMapping` sends
1488
+ // `subtitle2` to `<h6>`, so without this the Share Bar puts a level-6
1489
+ // heading into the document outline of every page carrying it. On a
1490
+ // blog post the nearest preceding heading is an `h2`, which makes it
1491
+ // a skipped level and a `heading-order` failure.
1492
+ //
1493
+ // Said here rather than as a theme-wide `variantMapping` override: a
1494
+ // default would silently change the element under every `subtitle2`
1495
+ // in the codebase, including surfaces where an `h6` is correct.
1496
+ /*#__PURE__*/ _jsx(Typography, {
1497
+ component: "p",
1498
+ variant: "subtitle2",
1499
+ sx: {
1500
+ mr: 1
1501
+ },
1502
+ children: title
1503
+ }) : null,
1504
+ SHARE_TARGETS.map((target)=>/*#__PURE__*/ _jsx(IconButton, {
1505
+ "aria-label": target.label,
1506
+ size: "small",
1507
+ onClick: open(target.href),
1508
+ children: /*#__PURE__*/ _jsx(MdiIcon, {
1509
+ path: target.path
1510
+ })
1511
+ }, target.label)),
1512
+ /*#__PURE__*/ _jsx(IconButton, {
1513
+ "aria-label": copied ? 'Link copied' : 'Copy link',
1514
+ size: "small",
1515
+ color: copied ? 'success' : 'default',
1516
+ onClick: copy,
1517
+ children: /*#__PURE__*/ _jsx(MdiIcon, {
1518
+ path: mdiContentCopy.path
1519
+ })
1520
+ })
1521
+ ]
1522
+ }));
1523
+ });
1524
+ CollectionShare.displayName = 'AglynCollectionShare';
1525
+ export const collectionShareSchema = {
1526
+ $id: SHARE_ID,
1527
+ pluginId: BUNDLE_ID,
1528
+ displayName: 'Share Bar',
1529
+ description: 'Share buttons for the current page, plus a copy link.',
1530
+ category: Aglyn.ComponentCategory.NAVIGATION,
1531
+ icon: {
1532
+ path: mdiShareVariant.path,
1533
+ sx: {
1534
+ color: 'secondary.main'
1535
+ }
1536
+ },
1537
+ flags: {
1538
+ selfClosing: Aglyn.FEATURE_FLAG.ENABLED
1539
+ },
1540
+ attributes: [
1541
+ {
1542
+ name: 'heading',
1543
+ label: 'Heading',
1544
+ description: 'Text before the buttons (blank hides it).',
1545
+ component: Aglyn.FieldComponentType.TEXT_FIELD
1546
+ }
1547
+ ]
1548
+ };
1549
+ /**
1550
+ * THE SAMPLE VALUES THE ENTRY BLOCKS PREVIEW WITH (AGL-2486).
1551
+ *
1552
+ * Entry Meta and Entry Author resolve FROM the routed entry, and a besigner
1553
+ * canvas has no routed entry — so both drew a one-line dashed strip and an
1554
+ * author styling a byline or an author card was styling something they could
1555
+ * not see. Related Posts had the same gap and was answered the same way: the
1556
+ * block's REAL markup, at the author's own settings, on editing surfaces
1557
+ * only.
1558
+ *
1559
+ * These two need no "this is a sample" notice the way the related CARDS do.
1560
+ * A card carries a title, a cover and a category, which is what a real post
1561
+ * looks like; a byline reading `Sample author` says what it is in the words
1562
+ * themselves, and a notice above a single line of caption text would be
1563
+ * taller than the thing it labels.
1564
+ *
1565
+ * The date is a FIXED instant, never `Date.now()`: the canvas has to render
1566
+ * the same bytes every time it opens, and it is the same instant the related
1567
+ * sample uses, so two blocks on one template never disagree about what day
1568
+ * their example is.
1569
+ */ const ENTRY_SAMPLE_PUBLISHED_AT = RELATED_SAMPLE_PUBLISHED_AT;
1570
+ /** Byline name for the samples; names the field rather than a person. */ const ENTRY_SAMPLE_AUTHOR = 'Sample author';
1571
+ /** Category text for the samples; names the field, not a taxonomy. */ const ENTRY_SAMPLE_CATEGORY = RELATED_SAMPLE_CATEGORY;
1572
+ /** Two chips, so the row previews its own wrapping and gap. */ const ENTRY_SAMPLE_TAGS = 'first tag, second tag';
1573
+ /** Bio line for the author-card sample; one sentence, as a real one is. */ const ENTRY_SAMPLE_BIO = 'The bio from this author’s record reads here, in a line or two.';
1574
+ /**
1575
+ * The portrait STAND-IN, for a sample that has no image to resolve.
1576
+ *
1577
+ * A neutral plate rather than nothing, and for the reason the related sample
1578
+ * draws its cover slot: on an entry template the portrait arrives from the
1579
+ * author record at render, so the slot is exactly what the author is sizing
1580
+ * and spacing. `aria-hidden` because it depicts nothing.
1581
+ */ const samplePortraitSx = (size)=>({
1582
+ width: size,
1583
+ height: size,
1584
+ flexShrink: 0,
1585
+ borderRadius: '50%',
1586
+ backgroundColor: 'action.hover'
1587
+ });
1588
+ /** Byline avatar, from the article frame (Figma 170:190). */ const ENTRY_AVATAR_SIZE = 36;
1589
+ /** Unresolved tokens render empty on the site, literal in the besigner. */ const metaValue = (value, suppressNavigation)=>{
1590
+ const trimmed = (value != null ? value : '').trim();
1591
+ if (!trimmed) return '';
1592
+ if (UNRESOLVED_TOKEN.test(trimmed) && !suppressNavigation) return '';
1593
+ return trimmed;
1594
+ };
1595
+ /**
1596
+ * "{{entry.date}} · {{entry.category}}" meta line plus tag chips
1597
+ * (AGL-582). Values arrive through entry tokens on entry renders; on other
1598
+ * surfaces unresolved tokens collapse to nothing instead of leaking.
1599
+ *
1600
+ * On an entry template the tenant now also FILLS the three values from the
1601
+ * routed entry when nothing is bound (`expandCollectionEntryMeta`, AGL-1385).
1602
+ * Before that, a block dragged from the palette rather than dropped as the
1603
+ * preset had no values at all — three "Show" switches gating nothing — and
1604
+ * rendered as the empty `<Box>` below, at height 0.
1605
+ */ const CollectionEntryMeta = /*#__PURE__*/ forwardRef((props, ref)=>{
1606
+ const { date, // Compose-time (AGL-1459): read by `expandCollectionEntryMeta`, which has
1607
+ // the timestamp. Destructured so it never reaches the DOM.
1608
+ dateFormat: _dateFormat, author, authorPageUrl, linkAuthor, category, tags, avatarImage, showDate, showAuthor, showCategory, showTags, showAvatar } = props, rest = _object_without_properties_loose(props, [
1609
+ "date",
1610
+ "dateFormat",
1611
+ "author",
1612
+ "authorPageUrl",
1613
+ "linkAuthor",
1614
+ "category",
1615
+ "tags",
1616
+ "avatarImage",
1617
+ "showDate",
1618
+ "showAuthor",
1619
+ "showCategory",
1620
+ "showTags",
1621
+ "showAvatar"
1622
+ ]);
1623
+ // Node styles ride the renderer-merged sx; recompose (stack.ts pattern).
1624
+ const nodeSx = Array.isArray(props['sx']) ? props['sx'] : [
1625
+ props['sx']
1626
+ ];
1627
+ const { suppressNavigation } = useContext(Aglyn.ScreenLinkContext);
1628
+ // The resolver every other surface shares (AGL-1215), so one media
1629
+ // reference keeps working across sites. A hook — before any early return.
1630
+ const { hostId } = Aglyn.useSite();
1631
+ const dateValue = showDate !== false ? metaValue(date, suppressNavigation) : '';
1632
+ const authorValue = showAuthor !== false ? metaValue(author, suppressNavigation) : '';
1633
+ const categoryValue = showCategory !== false ? metaValue(category, suppressNavigation) : '';
1634
+ const tagsValue = showTags !== false ? metaValue(tags, suppressNavigation) : '';
1635
+ const tagList = tagsValue.split(',').map((tag)=>tag.trim()).filter(Boolean);
1636
+ // An unresolved token empties on EVERY surface here, unlike the text
1637
+ // fields: a literal `{{…}}` in a src is a broken image in the canvas, and a
1638
+ // byline that renders a broken avatar on every post is worse than no
1639
+ // avatar at all.
1640
+ const avatarRaw = (avatarImage != null ? avatarImage : '').trim();
1641
+ const avatarSrc = showAvatar !== false && avatarRaw && !UNRESOLVED_TOKEN.test(avatarRaw) ? Aglyn.resolveMediaSrc(avatarRaw, {
1642
+ hostId
1643
+ }) : '';
1644
+ // Author leads, so the frame's "The Aglyn Team · Jul 2026" reads in that
1645
+ // order. With no author the join is character-for-character what it was.
1646
+ const filledLine = [
1647
+ authorValue,
1648
+ dateValue,
1649
+ categoryValue
1650
+ ].filter(Boolean).join(' · ');
1651
+ /*
1652
+ The rest of the line, WITHOUT the author (AGL-2519).
1653
+
1654
+ The row is one string everywhere else, which is what a `·`-joined byline
1655
+ wants — but a link is an element, so the author has to come out of the
1656
+ join to be one. Split rather than reassembled from parts, so the
1657
+ separator, the order and the empty-field collapsing all stay in the one
1658
+ expression above: the linked row and the plain row cannot render a
1659
+ different sentence.
1660
+ */ const authorHrefRaw = metaValue(authorPageUrl, suppressNavigation);
1661
+ const authorHref = linkAuthor !== false && SAFE_AUTHOR_HREF.test(authorHrefRaw) ? authorHrefRaw : '';
1662
+ const trailingLine = [
1663
+ dateValue,
1664
+ categoryValue
1665
+ ].filter(Boolean).join(' · ');
1666
+ // Nothing to show and nothing to route from: an editing surface previews
1667
+ // the block's own markup at the author's own settings, and a published page
1668
+ // renders nothing at all (AGL-2486). The switches are honoured here exactly
1669
+ // as they are below, so a row with Show date off previews without one.
1670
+ const sample = !filledLine && !tagList.length && !avatarSrc && suppressNavigation;
1671
+ const line = sample ? [
1672
+ showAuthor !== false ? ENTRY_SAMPLE_AUTHOR : '',
1673
+ showDate !== false ? Aglyn.formatCollectionEntryDate(ENTRY_SAMPLE_PUBLISHED_AT, Aglyn.normalizeCollectionEntryDateFormat(_dateFormat)) : '',
1674
+ showCategory !== false ? ENTRY_SAMPLE_CATEGORY : ''
1675
+ ].filter(Boolean).join(' · ') : filledLine;
1676
+ const chips = sample ? showTags !== false ? ENTRY_SAMPLE_TAGS.split(',').map((tag)=>tag.trim()) : [] : tagList;
1677
+ // The portrait an entry's own author record supplies arrives at compose
1678
+ // time, so on the canvas there is none to resolve — the slot is drawn
1679
+ // instead, which is what a byline's spacing is actually built around.
1680
+ const samplePortrait = sample && showAvatar !== false;
1681
+ if (!line && !chips.length && !avatarSrc && !samplePortrait) {
1682
+ return /*#__PURE__*/ _jsx(Box, _extends({
1683
+ ref: ref
1684
+ }, rest));
1685
+ }
1686
+ return /*#__PURE__*/ _jsxs(MuiStack, _extends({
1687
+ ref: ref,
1688
+ direction: "row",
1689
+ spacing: 1
1690
+ }, rest, {
1691
+ // MERGE, never replace (AGL-1450, same class as AGL-1284). `rest.sx`
1692
+ // is the ARRAY `mergeSxProps` builds in leaf.tsx, so spreading it into
1693
+ // an object produced `{0: …, 1: …, 2: …}` — numeric keys emotion emits
1694
+ // as invalid selectors — and discarded EVERY authored property while
1695
+ // these two defaults still applied. The block looked deliberately
1696
+ // styled, so nothing suggested the value had been dropped. Defaults
1697
+ // go first; the node's slice comes after and can override them.
1698
+ sx: [
1699
+ {
1700
+ alignItems: 'center',
1701
+ flexWrap: 'wrap'
1702
+ },
1703
+ ...nodeSx
1704
+ ],
1705
+ children: [
1706
+ avatarSrc ? /*#__PURE__*/ _jsx(Box, _extends({
1707
+ component: "img",
1708
+ src: avatarSrc,
1709
+ // Decorative: the byline names the author in text right beside it,
1710
+ // so a screen reader announcing the mark again is noise.
1711
+ alt: "",
1712
+ sx: {
1713
+ display: 'block',
1714
+ width: ENTRY_AVATAR_SIZE,
1715
+ height: ENTRY_AVATAR_SIZE,
1716
+ borderRadius: '50%',
1717
+ objectFit: 'cover'
1718
+ }
1719
+ }, Aglyn.DEFERRED_IMAGE_ATTRIBUTES)) : samplePortrait ? /*#__PURE__*/ _jsx(Box, {
1720
+ "aria-hidden": true,
1721
+ sx: samplePortraitSx(ENTRY_AVATAR_SIZE)
1722
+ }) : null,
1723
+ line ? /*#__PURE__*/ _jsx(Typography, {
1724
+ variant: "caption",
1725
+ sx: {
1726
+ color: 'text.secondary'
1727
+ },
1728
+ children: // Linked only on the real byline, never on the canvas sample: a
1729
+ // preview whose placeholder name navigates is a trap, and the
1730
+ // sample author has no page to go to.
1731
+ authorHref && authorValue && !sample ? /*#__PURE__*/ _jsxs(_Fragment, {
1732
+ children: [
1733
+ /*#__PURE__*/ _jsx(AppLink, {
1734
+ href: authorHref,
1735
+ sx: {
1736
+ color: 'inherit'
1737
+ },
1738
+ underline: "hover",
1739
+ children: authorValue
1740
+ }),
1741
+ trailingLine ? ` · ${trailingLine}` : ''
1742
+ ]
1743
+ }) : line
1744
+ }) : null,
1745
+ chips.map((tag)=>/*#__PURE__*/ _jsx(Chip, {
1746
+ label: tag,
1747
+ size: "small",
1748
+ variant: "outlined"
1749
+ }, tag))
1750
+ ]
1751
+ }));
1752
+ });
1753
+ CollectionEntryMeta.displayName = 'AglynCollectionEntryMeta';
1754
+ export const collectionEntryMetaSchema = {
1755
+ $id: ENTRY_META_ID,
1756
+ pluginId: BUNDLE_ID,
1757
+ displayName: 'Entry Meta',
1758
+ description: 'The byline row for an entry — author, date, category and tags.',
1759
+ category: Aglyn.ComponentCategory.TEXT,
1760
+ icon: {
1761
+ path: mdiTagOutline.path,
1762
+ sx: {
1763
+ color: 'secondary.main'
1764
+ }
1765
+ },
1766
+ flags: {
1767
+ selfClosing: Aglyn.FEATURE_FLAG.ENABLED
1768
+ },
1769
+ attributes: [
1770
+ {
1771
+ name: 'date',
1772
+ label: 'Date',
1773
+ description: 'Blank shows the entry’s own published date on entry templates. ' + 'Type here (or bind {{entry.date}}) only to override it.',
1774
+ component: Aglyn.FieldComponentType.TEXT_FIELD
1775
+ },
1776
+ {
1777
+ name: 'dateFormat',
1778
+ label: 'Date format',
1779
+ description: 'How the published date reads. Site default is the format this ' + 'block has always used; it has no effect on a Date you typed in ' + 'yourself.',
1780
+ component: Aglyn.FieldComponentType.SELECT,
1781
+ // The formats the pure layer knows how to produce, so the list cannot
1782
+ // offer a shape nothing renders. Every value is REAL — including the
1783
+ // do-nothing one (AGL-1451/AGL-1453): `''` cannot survive a save, so
1784
+ // an author who tried a format would have no route back.
1785
+ options: [
1786
+ ...Aglyn.COLLECTION_ENTRY_DATE_FORMAT_OPTIONS
1787
+ ]
1788
+ },
1789
+ {
1790
+ name: 'author',
1791
+ label: 'Author',
1792
+ description: 'Blank shows the entry’s own author. Type here (or bind ' + '{{entry.author}}) only to override it.',
1793
+ component: Aglyn.FieldComponentType.TEXT_FIELD
1794
+ },
1795
+ {
1796
+ name: 'category',
1797
+ label: 'Category',
1798
+ description: 'Blank shows the entry’s own category. Type here (or bind ' + '{{entry.category}}) only to override it.',
1799
+ component: Aglyn.FieldComponentType.TEXT_FIELD
1800
+ },
1801
+ {
1802
+ name: 'tags',
1803
+ label: 'Tags',
1804
+ description: 'Comma-separated. Blank shows the entry’s own tags; type here (or ' + 'bind {{entry.tags}}) only to override them.',
1805
+ component: Aglyn.FieldComponentType.TEXT_FIELD
1806
+ },
1807
+ {
1808
+ name: 'avatarImage',
1809
+ label: 'Avatar',
1810
+ description: 'Round image before the byline — your brand mark, or any image ' + 'from the media library. Blank shows no avatar.',
1811
+ component: Aglyn.FieldComponentType.TEXT_FIELD
1812
+ },
1813
+ {
1814
+ name: 'showDate',
1815
+ label: 'Show date',
1816
+ description: 'Off hides the published date from the line without clearing what ' + 'the Date field would have shown.',
1817
+ component: Aglyn.FieldComponentType.SWITCH,
1818
+ // Unset means SHOWN in the renderer above, so the switch has to
1819
+ // OPEN in that position or it lies about the block in front of
1820
+ // the author — and the first click, which writes the `true` that
1821
+ // was already in effect, changes nothing and reads as a dead
1822
+ // control (AGL-2506).
1823
+ initialValue: true
1824
+ },
1825
+ {
1826
+ name: 'showCategory',
1827
+ label: 'Show category',
1828
+ description: 'Off drops the category from the line. The entry keeps its ' + 'category — this is about the byline, not the taxonomy.',
1829
+ component: Aglyn.FieldComponentType.SWITCH,
1830
+ // Unset means SHOWN in the renderer above, so the switch has to
1831
+ // OPEN in that position or it lies about the block in front of
1832
+ // the author — and the first click, which writes the `true` that
1833
+ // was already in effect, changes nothing and reads as a dead
1834
+ // control (AGL-2506).
1835
+ initialValue: true
1836
+ },
1837
+ {
1838
+ name: 'showTags',
1839
+ label: 'Show tags',
1840
+ description: 'Off hides the tag chips below the line. Leave it on and the ' + 'other three off for a tags-only row.',
1841
+ component: Aglyn.FieldComponentType.SWITCH,
1842
+ // Unset means SHOWN in the renderer above, so the switch has to
1843
+ // OPEN in that position or it lies about the block in front of
1844
+ // the author — and the first click, which writes the `true` that
1845
+ // was already in effect, changes nothing and reads as a dead
1846
+ // control (AGL-2506).
1847
+ initialValue: true
1848
+ },
1849
+ {
1850
+ name: 'authorPageUrl',
1851
+ label: 'Author links to',
1852
+ description: 'Blank sends the byline to the author’s page on this site. Type ' + 'here (or bind {{entry.authorPageUrl}}) only to override it.',
1853
+ component: Aglyn.FieldComponentType.TEXT_FIELD
1854
+ },
1855
+ {
1856
+ name: 'linkAuthor',
1857
+ label: 'Link author',
1858
+ description: 'Off leaves the byline as plain text even when the author has a ' + 'page. On, the name links there and the date and category beside ' + 'it do not.',
1859
+ component: Aglyn.FieldComponentType.SWITCH,
1860
+ // Unset means LINKED in the renderer above, so the switch has to open
1861
+ // in that position or it lies about the block in front of the author
1862
+ // (AGL-2506).
1863
+ initialValue: true
1864
+ },
1865
+ {
1866
+ name: 'showAuthor',
1867
+ label: 'Show author',
1868
+ description: 'Off hides the byline. The avatar follows it: a block with no ' + 'author shown never fills in the author’s portrait.',
1869
+ component: Aglyn.FieldComponentType.SWITCH,
1870
+ // Unset means SHOWN in the renderer above, so the switch has to
1871
+ // OPEN in that position or it lies about the block in front of
1872
+ // the author — and the first click, which writes the `true` that
1873
+ // was already in effect, changes nothing and reads as a dead
1874
+ // control (AGL-2506).
1875
+ initialValue: true
1876
+ },
1877
+ {
1878
+ name: 'showAvatar',
1879
+ label: 'Show avatar',
1880
+ description: 'Off hides the round image in front of the byline, including an ' + 'author’s own portrait.',
1881
+ component: Aglyn.FieldComponentType.SWITCH,
1882
+ // Unset means SHOWN in the renderer above, so the switch has to
1883
+ // OPEN in that position or it lies about the block in front of
1884
+ // the author — and the first click, which writes the `true` that
1885
+ // was already in effect, changes nothing and reads as a dead
1886
+ // control (AGL-2506).
1887
+ initialValue: true
1888
+ }
1889
+ ]
1890
+ };
1891
+ /** The card's portrait, larger than the byline's 36px mark (Figma 170:190). */ const AUTHOR_AVATAR_SIZE = 48;
1892
+ /**
1893
+ * The mark for each platform the author model knows (AGL-2516).
1894
+ *
1895
+ * Imported rather than resolved through the icon catalog, because this set is
1896
+ * CLOSED and known at build time — the catalog is ~2.9 MB and only picker
1897
+ * surfaces load it, so a renderer that reached for it to draw ten glyphs
1898
+ * would ship the whole thing to every visitor of every article.
1899
+ *
1900
+ * Keyed by the registry's `icon`, so adding a platform is one entry there and
1901
+ * one import here, and a platform whose mark was never imported falls through
1902
+ * to the generic link glyph rather than rendering nothing.
1903
+ */ const AUTHOR_LINK_ICON_PATHS = {
1904
+ twitter: mdiTwitter.path,
1905
+ linkedin: mdiLinkedin.path,
1906
+ github: mdiGithub.path,
1907
+ mastodon: mdiMastodon.path,
1908
+ youtube: mdiYoutube.path,
1909
+ instagram: mdiInstagram.path,
1910
+ facebook: mdiFacebook.path,
1911
+ web: mdiWeb.path,
1912
+ email: mdiEmail.path,
1913
+ rss: mdiRss.path
1914
+ };
1915
+ /**
1916
+ * What one link row draws.
1917
+ *
1918
+ * A known platform takes its mark from the table above. A CUSTOM link takes
1919
+ * the path the console stored beside the picked id (AGL-1212) — and falls back
1920
+ * to a generic link glyph rather than to nothing, because a row with a label
1921
+ * and a url is still a working link whose icon simply failed to resolve.
1922
+ */ function authorLinkIconPath(link) {
1923
+ var _link_iconPath;
1924
+ const platform = Aglyn.authorLinkPlatform(link.platform);
1925
+ if (platform) {
1926
+ var _AUTHOR_LINK_ICON_PATHS_platform_icon;
1927
+ return (_AUTHOR_LINK_ICON_PATHS_platform_icon = AUTHOR_LINK_ICON_PATHS[platform.icon]) != null ? _AUTHOR_LINK_ICON_PATHS_platform_icon : mdiLinkVariant.path;
1928
+ }
1929
+ return ((_link_iconPath = link.iconPath) != null ? _link_iconPath : '').trim() || mdiLinkVariant.path;
1930
+ }
1931
+ /**
1932
+ * Schemes a rendered author link may use — the model's own guard, restated at
1933
+ * the boundary that produces the `href`.
1934
+ *
1935
+ * `normalizeContentAuthorLinks` already drops everything else, and this is
1936
+ * deliberately not trusting that: the props on this component are authorable,
1937
+ * so a link can reach the renderer without ever passing through the store's
1938
+ * normalizer. Two checks, because only one of them is on the path an attacker
1939
+ * would use.
1940
+ */ const SAFE_AUTHOR_LINK_HREF = /^(https:\/\/|mailto:)/i;
1941
+ /**
1942
+ * What the byline may link to — an absolute `https:` page, or a route on this
1943
+ * site. The Social Links block guards its hrefs the same way and for the same
1944
+ * reason: the value comes from a stored record, so `javascript:` and friends
1945
+ * have to be unreachable rather than merely unlikely.
1946
+ */ const SAFE_AUTHOR_HREF = /^(https:\/\/|\/(?!\/))/i;
1947
+ /** An off-site author page opens in a new tab; a route on this site does not. */ const EXTERNAL_AUTHOR_HREF = /^https:\/\//i;
1948
+ /**
1949
+ * The author's profile links as a row of marks (AGL-2516).
1950
+ *
1951
+ * Shared by the article's Entry Author card and the Author Profile block on
1952
+ * the author's own page (AGL-2518) — the one part of the two that would
1953
+ * genuinely drift, since an icon row is where the accessible-name rule and
1954
+ * the `rel` on an off-site profile live.
1955
+ *
1956
+ * Renders nothing for an empty list, so a caller can drop it in
1957
+ * unconditionally rather than guarding at every call site.
1958
+ */ function AuthorLinkRows(props) {
1959
+ if (!props.links.length) return null;
1960
+ return /*#__PURE__*/ _jsx(MuiStack, {
1961
+ direction: "row",
1962
+ spacing: 0.5,
1963
+ sx: {
1964
+ flexWrap: 'wrap',
1965
+ ml: -1
1966
+ },
1967
+ children: props.links.map((link, index)=>{
1968
+ var _link_url, _props_size;
1969
+ const label = Aglyn.authorLinkLabel(link);
1970
+ return /*#__PURE__*/ _jsx(AppLink, {
1971
+ componentVariant: "icon-button",
1972
+ href: ((_link_url = link.url) != null ? _link_url : '').trim(),
1973
+ // The name IS the icon here, so it has to be announced — a row of
1974
+ // unlabelled icon buttons is a row a screen reader reads as
1975
+ // "link, link, link".
1976
+ "aria-label": label,
1977
+ title: label,
1978
+ size: "small",
1979
+ sx: {
1980
+ color: 'text.secondary'
1981
+ },
1982
+ // A profile lives off this site by definition, and `mailto:` must
1983
+ // not replace the article either.
1984
+ target: "_blank",
1985
+ rel: "noopener noreferrer me",
1986
+ children: /*#__PURE__*/ _jsx(MdiIcon, {
1987
+ path: authorLinkIconPath(link),
1988
+ size: (_props_size = props.size) != null ? _props_size : 0.85
1989
+ })
1990
+ }, index);
1991
+ })
1992
+ });
1993
+ }
1994
+ /**
1995
+ * The rows a card will actually draw, guarded at the boundary.
1996
+ *
1997
+ * These arrive as PROPS, so a link can reach a renderer without ever passing
1998
+ * through the store's normalizer — the reason the byline's own `url` is
1999
+ * checked twice as well.
2000
+ */ function safeAuthorLinks(links, show) {
2001
+ if (show === false) return [];
2002
+ return (links != null ? links : []).filter((link)=>{
2003
+ var _ref;
2004
+ return SAFE_AUTHOR_LINK_HREF.test(((_ref = link == null ? void 0 : link.url) != null ? _ref : '').trim());
2005
+ });
2006
+ }
2007
+ /**
2008
+ * The author card that closes an article (AGL-2486) — portrait, byline, bio.
2009
+ *
2010
+ * Entry Meta prints a NAME beside a date, which is all an entry carried when
2011
+ * it was written: one free-typed string. Custom authors made the byline a
2012
+ * record with a portrait, a bio and a url, and a template that wanted the
2013
+ * card the article frame draws still had to type the name and the blurb in as
2014
+ * literal text — text that keeps saying whatever it said under posts somebody
2015
+ * else wrote, and that no edit to the author record can ever reach. This is
2016
+ * that card, filled from the record.
2017
+ *
2018
+ * Every field is independently overridable and every one of them collapses
2019
+ * when empty: an author with no portrait renders the text alone rather than a
2020
+ * gap where a face would be, and an entry with no author at all renders
2021
+ * nothing — never an empty bordered box.
2022
+ */ const CollectionEntryAuthor = /*#__PURE__*/ forwardRef((props, ref)=>{
2023
+ // None of these are DOM attributes (`name` least of all), so they are
2024
+ // destructured rather than spread — the stack.ts pattern.
2025
+ const { name, bio, image, url, pageUrl, links, showBio, showAvatar, showLinks } = props, rest = _object_without_properties_loose(props, [
2026
+ "name",
2027
+ "bio",
2028
+ "image",
2029
+ "url",
2030
+ "pageUrl",
2031
+ "links",
2032
+ "showBio",
2033
+ "showAvatar",
2034
+ "showLinks"
2035
+ ]);
2036
+ const nodeSx = Array.isArray(props['sx']) ? props['sx'] : [
2037
+ props['sx']
2038
+ ];
2039
+ const { suppressNavigation } = useContext(Aglyn.ScreenLinkContext);
2040
+ const { hostId } = Aglyn.useSite();
2041
+ const nameValue = metaValue(name, suppressNavigation);
2042
+ const bioValue = showBio !== false ? metaValue(bio, suppressNavigation) : '';
2043
+ // Their page here first, their own site second (AGL-2519) — see `pageUrl`.
2044
+ const hrefRaw = metaValue(pageUrl, suppressNavigation) || metaValue(url, suppressNavigation);
2045
+ const urlValue = SAFE_AUTHOR_HREF.test(hrefRaw) ? hrefRaw : '';
2046
+ // An unresolved token empties on EVERY surface, as in the byline: a literal
2047
+ // `{{…}}` in a src is a broken portrait in the canvas.
2048
+ const imageRaw = (image != null ? image : '').trim();
2049
+ const imageSrc = showAvatar !== false && imageRaw && !UNRESOLVED_TOKEN.test(imageRaw) ? Aglyn.resolveMediaSrc(imageRaw, {
2050
+ hostId
2051
+ }) : '';
2052
+ // Nothing to show and nothing to route from: an editing surface previews
2053
+ // the card's own markup at the author's own settings, and a published page
2054
+ // renders nothing at all (AGL-2486). Every part still answers its own
2055
+ // switch, so a card with Show bio off previews as portrait and name.
2056
+ const sample = !nameValue && !bioValue && !imageSrc && Boolean(suppressNavigation);
2057
+ const displayName = sample ? ENTRY_SAMPLE_AUTHOR : nameValue;
2058
+ const displayBio = sample && showBio !== false ? ENTRY_SAMPLE_BIO : bioValue;
2059
+ // The record's portrait arrives at compose time, so there is none to
2060
+ // resolve on the canvas — the card draws its slot, which is the thing its
2061
+ // spacing is built around.
2062
+ const samplePortrait = sample && showAvatar !== false;
2063
+ /**
2064
+ * The rows this card will actually draw (AGL-2516).
2065
+ *
2066
+ * Guarded here rather than trusted from the store: these arrive as PROPS,
2067
+ * so a link can reach the renderer without passing the record normalizer,
2068
+ * and `javascript:` has to be unreachable rather than merely unlikely — the
2069
+ * same reason the byline's own `url` is checked twice.
2070
+ */ const linkRows = safeAuthorLinks(links, showLinks);
2071
+ if (!displayName && !displayBio && !imageSrc && !samplePortrait && !linkRows.length) {
2072
+ return /*#__PURE__*/ _jsx(Box, _extends({
2073
+ ref: ref
2074
+ }, rest));
2075
+ }
2076
+ return /*#__PURE__*/ _jsxs(MuiStack, _extends({
2077
+ ref: ref,
2078
+ direction: "row",
2079
+ spacing: 2
2080
+ }, rest, {
2081
+ // MERGE, never replace (AGL-1450): `rest.sx` is the array leaf.tsx
2082
+ // builds, and spreading it into an object drops every authored value.
2083
+ sx: [
2084
+ {
2085
+ alignItems: 'flex-start',
2086
+ p: 2.5,
2087
+ border: '1px solid',
2088
+ borderColor: 'divider',
2089
+ borderRadius: 1
2090
+ },
2091
+ ...nodeSx
2092
+ ],
2093
+ children: [
2094
+ imageSrc ? /*#__PURE__*/ _jsx(Box, _extends({
2095
+ component: "img",
2096
+ src: imageSrc,
2097
+ // Decorative: the card names the author in text right beside it.
2098
+ alt: "",
2099
+ sx: {
2100
+ display: 'block',
2101
+ flexShrink: 0,
2102
+ width: AUTHOR_AVATAR_SIZE,
2103
+ height: AUTHOR_AVATAR_SIZE,
2104
+ borderRadius: '50%',
2105
+ objectFit: 'cover'
2106
+ }
2107
+ }, Aglyn.DEFERRED_IMAGE_ATTRIBUTES)) : samplePortrait ? /*#__PURE__*/ _jsx(Box, {
2108
+ "aria-hidden": true,
2109
+ sx: samplePortraitSx(AUTHOR_AVATAR_SIZE)
2110
+ }) : null,
2111
+ /*#__PURE__*/ _jsxs(MuiStack, {
2112
+ spacing: 0.5,
2113
+ sx: {
2114
+ minWidth: 0
2115
+ },
2116
+ children: [
2117
+ displayName ? /*#__PURE__*/ _jsx(Typography, {
2118
+ component: "p",
2119
+ variant: "subtitle2",
2120
+ children: urlValue ? /*#__PURE__*/ _jsx(AppLink, _extends({
2121
+ href: urlValue,
2122
+ sx: {
2123
+ color: 'inherit'
2124
+ },
2125
+ underline: "hover"
2126
+ }, EXTERNAL_AUTHOR_HREF.test(urlValue) ? {
2127
+ target: '_blank',
2128
+ rel: 'noopener noreferrer'
2129
+ } : {}, {
2130
+ children: displayName
2131
+ })) : displayName
2132
+ }) : null,
2133
+ displayBio ? /*#__PURE__*/ _jsx(Typography, {
2134
+ variant: "body2",
2135
+ sx: {
2136
+ color: 'text.secondary'
2137
+ },
2138
+ children: displayBio
2139
+ }) : null,
2140
+ /*#__PURE__*/ _jsx(AuthorLinkRows, {
2141
+ links: linkRows
2142
+ })
2143
+ ]
2144
+ })
2145
+ ]
2146
+ }));
2147
+ });
2148
+ CollectionEntryAuthor.displayName = 'AglynCollectionEntryAuthor';
2149
+ export const collectionEntryAuthorSchema = {
2150
+ $id: ENTRY_AUTHOR_ID,
2151
+ pluginId: BUNDLE_ID,
2152
+ displayName: 'Entry Author',
2153
+ description: 'The author card for an entry — portrait, byline and bio.',
2154
+ category: Aglyn.ComponentCategory.TEXT,
2155
+ icon: {
2156
+ path: mdiAccountCircleOutline.path,
2157
+ sx: {
2158
+ color: 'secondary.main'
2159
+ }
2160
+ },
2161
+ flags: {
2162
+ selfClosing: Aglyn.FEATURE_FLAG.ENABLED
2163
+ },
2164
+ attributes: [
2165
+ {
2166
+ name: 'name',
2167
+ label: 'Name',
2168
+ description: 'Blank shows the name from the entry’s author record. Type here ' + '(or bind {{entry.author}}) to sign this card differently from ' + 'the byline above the article.',
2169
+ component: Aglyn.FieldComponentType.TEXT_FIELD
2170
+ },
2171
+ {
2172
+ name: 'bio',
2173
+ label: 'Bio',
2174
+ description: 'Blank shows the bio from the author’s record. Type here (or bind ' + '{{entry.authorBio}}) only to override it.',
2175
+ component: Aglyn.FieldComponentType.TEXTAREA
2176
+ },
2177
+ {
2178
+ name: 'image',
2179
+ label: 'Portrait',
2180
+ description: 'Blank shows the portrait from the author’s record. Pick from your ' + 'media library with "Browse media" only to override it.',
2181
+ component: Aglyn.FieldComponentType.TEXT_FIELD
2182
+ },
2183
+ {
2184
+ name: 'pageUrl',
2185
+ label: 'Name links to',
2186
+ description: 'Blank sends the name to the author’s page on this site — ' + 'everything they wrote. Falls back to their own url when they have ' + 'no page; type here to override both.',
2187
+ component: Aglyn.FieldComponentType.TEXT_FIELD
2188
+ },
2189
+ {
2190
+ name: 'url',
2191
+ label: 'Their own site',
2192
+ description: 'Blank uses the url on the author’s record. Used only when they ' + 'have no page on this site for the name to link to.',
2193
+ component: Aglyn.FieldComponentType.TEXT_FIELD
2194
+ },
2195
+ {
2196
+ name: 'showLinks',
2197
+ label: 'Show links',
2198
+ description: 'Off hides the author’s profile links. They come from the author ' + 'record — a known platform brings its own mark, and a custom link ' + 'brings the icon and label chosen there.',
2199
+ component: Aglyn.FieldComponentType.SWITCH,
2200
+ // Unset means SHOWN in the renderer above, so the switch has to open
2201
+ // in that position or it lies about the card in front of the author
2202
+ // (AGL-2506).
2203
+ initialValue: true
2204
+ },
2205
+ {
2206
+ name: 'showBio',
2207
+ label: 'Show bio',
2208
+ description: 'Off leaves the card as a portrait and a name — for a masthead ' + 'where the blurb would repeat what the page already says.',
2209
+ component: Aglyn.FieldComponentType.SWITCH,
2210
+ // Unset means SHOWN in the renderer above, so the switch has to
2211
+ // OPEN in that position or it lies about the block in front of
2212
+ // the author — and the first click, which writes the `true` that
2213
+ // was already in effect, changes nothing and reads as a dead
2214
+ // control (AGL-2506).
2215
+ initialValue: true
2216
+ },
2217
+ {
2218
+ name: 'showAvatar',
2219
+ label: 'Show portrait',
2220
+ description: 'Off leaves the text alone, with no space held for a picture.',
2221
+ component: Aglyn.FieldComponentType.SWITCH,
2222
+ // Unset means SHOWN in the renderer above, so the switch has to
2223
+ // OPEN in that position or it lies about the block in front of
2224
+ // the author — and the first click, which writes the `true` that
2225
+ // was already in effect, changes nothing and reads as a dead
2226
+ // control (AGL-2506).
2227
+ initialValue: true
2228
+ }
2229
+ ]
2230
+ };
2231
+ /** The page subject's portrait — a step up from the card's 48px. */ const PROFILE_AVATAR_SIZE = 96;
2232
+ /**
2233
+ * Who an author page is about (AGL-2518) — portrait, name, role, bio, links.
2234
+ *
2235
+ * The Entry Author card renders the same person as a footnote under an
2236
+ * article. This renders them as the SUBJECT of a page, which is why it has
2237
+ * the two fields that card has no room for: `jobTitle` and `worksFor` are
2238
+ * what tell a stranger who they are looking at, and they are already on the
2239
+ * record because `schema.org/Person` defines them (AGL-2486).
2240
+ *
2241
+ * Every field is independently overridable and every one collapses when
2242
+ * empty, exactly as the card's do — an author with no portrait renders the
2243
+ * text alone rather than a gap where a face would be. On an editing surface
2244
+ * the block previews its own shape with sample text, because a template is
2245
+ * designed before any author is routed through it.
2246
+ */ const ContentAuthorProfile = /*#__PURE__*/ forwardRef((props, ref)=>{
2247
+ // None of these are DOM attributes (`name` least of all), so they are
2248
+ // destructured rather than spread — the stack.ts pattern.
2249
+ const { name, bio, image, jobTitle, worksFor, url, links, showBio, showAvatar, showRole, showLinks } = props, rest = _object_without_properties_loose(props, [
2250
+ "name",
2251
+ "bio",
2252
+ "image",
2253
+ "jobTitle",
2254
+ "worksFor",
2255
+ "url",
2256
+ "links",
2257
+ "showBio",
2258
+ "showAvatar",
2259
+ "showRole",
2260
+ "showLinks"
2261
+ ]);
2262
+ const nodeSx = Array.isArray(props['sx']) ? props['sx'] : [
2263
+ props['sx']
2264
+ ];
2265
+ const { suppressNavigation } = useContext(Aglyn.ScreenLinkContext);
2266
+ const { hostId } = Aglyn.useSite();
2267
+ const nameValue = metaValue(name, suppressNavigation);
2268
+ const bioValue = showBio !== false ? metaValue(bio, suppressNavigation) : '';
2269
+ const jobValue = showRole !== false ? metaValue(jobTitle, suppressNavigation) : '';
2270
+ const worksForValue = showRole !== false ? metaValue(worksFor, suppressNavigation) : '';
2271
+ const urlRaw = metaValue(url, suppressNavigation);
2272
+ const urlValue = SAFE_AUTHOR_HREF.test(urlRaw) ? urlRaw : '';
2273
+ // An unresolved token empties on EVERY surface, as in the byline: a literal
2274
+ // `{{…}}` in a src is a broken portrait in the canvas.
2275
+ const imageRaw = (image != null ? image : '').trim();
2276
+ const imageSrc = showAvatar !== false && imageRaw && !UNRESOLVED_TOKEN.test(imageRaw) ? Aglyn.resolveMediaSrc(imageRaw, {
2277
+ hostId
2278
+ }) : '';
2279
+ const linkRows = safeAuthorLinks(links, showLinks);
2280
+ // Nothing routed and nothing typed: an editing surface previews the block's
2281
+ // own markup at its own settings, and a published page renders nothing at
2282
+ // all. Every part still answers its own switch, so a block with Show bio
2283
+ // off previews as portrait and name.
2284
+ const sample = !nameValue && !bioValue && !jobValue && !imageSrc && !linkRows.length && Boolean(suppressNavigation);
2285
+ const displayName = sample ? ENTRY_SAMPLE_AUTHOR : nameValue;
2286
+ const displayBio = sample && showBio !== false ? ENTRY_SAMPLE_BIO : bioValue;
2287
+ const samplePortrait = sample && showAvatar !== false;
2288
+ /**
2289
+ * "Founder at Aglyn" — one line, from two fields, and a middle dot would be
2290
+ * wrong here: this is a phrase, not a list of facts like the byline's
2291
+ * `name · date`. Either field alone still reads.
2292
+ */ const roleLine = [
2293
+ jobValue,
2294
+ worksForValue
2295
+ ].filter(Boolean).join(' at ');
2296
+ if (!displayName && !displayBio && !roleLine && !imageSrc && !samplePortrait && !linkRows.length) {
2297
+ return /*#__PURE__*/ _jsx(Box, _extends({
2298
+ ref: ref
2299
+ }, rest));
2300
+ }
2301
+ return /*#__PURE__*/ _jsxs(MuiStack, _extends({
2302
+ ref: ref,
2303
+ direction: "row",
2304
+ spacing: 3
2305
+ }, rest, {
2306
+ // MERGE, never replace (AGL-1450): `rest.sx` is the array leaf.tsx
2307
+ // builds, and spreading it into an object drops every authored value.
2308
+ sx: [
2309
+ {
2310
+ alignItems: 'flex-start'
2311
+ },
2312
+ ...nodeSx
2313
+ ],
2314
+ children: [
2315
+ imageSrc ? /*#__PURE__*/ _jsx(Box, _extends({
2316
+ component: "img",
2317
+ src: imageSrc,
2318
+ // Decorative: the block names the author in text right beside it.
2319
+ alt: "",
2320
+ sx: {
2321
+ display: 'block',
2322
+ flexShrink: 0,
2323
+ width: PROFILE_AVATAR_SIZE,
2324
+ height: PROFILE_AVATAR_SIZE,
2325
+ borderRadius: '50%',
2326
+ objectFit: 'cover'
2327
+ }
2328
+ }, Aglyn.DEFERRED_IMAGE_ATTRIBUTES)) : samplePortrait ? /*#__PURE__*/ _jsx(Box, {
2329
+ "aria-hidden": true,
2330
+ sx: samplePortraitSx(PROFILE_AVATAR_SIZE)
2331
+ }) : null,
2332
+ /*#__PURE__*/ _jsxs(MuiStack, {
2333
+ spacing: 1,
2334
+ sx: {
2335
+ minWidth: 0
2336
+ },
2337
+ children: [
2338
+ displayName ? // `h1` because the author IS this page's subject — the one heading
2339
+ // rule a built-in body has to get right, since the template around
2340
+ // it cannot know what the block will be handed.
2341
+ /*#__PURE__*/ _jsx(Typography, {
2342
+ component: "h1",
2343
+ variant: "h4",
2344
+ children: urlValue ? /*#__PURE__*/ _jsx(AppLink, _extends({
2345
+ href: urlValue,
2346
+ sx: {
2347
+ color: 'inherit'
2348
+ },
2349
+ underline: "hover"
2350
+ }, EXTERNAL_AUTHOR_HREF.test(urlValue) ? {
2351
+ target: '_blank',
2352
+ rel: 'noopener noreferrer'
2353
+ } : {}, {
2354
+ children: displayName
2355
+ })) : displayName
2356
+ }) : null,
2357
+ roleLine ? /*#__PURE__*/ _jsx(Typography, {
2358
+ variant: "subtitle2",
2359
+ sx: {
2360
+ color: 'text.secondary'
2361
+ },
2362
+ children: roleLine
2363
+ }) : null,
2364
+ displayBio ? /*#__PURE__*/ _jsx(Typography, {
2365
+ variant: "body1",
2366
+ sx: {
2367
+ color: 'text.secondary'
2368
+ },
2369
+ children: displayBio
2370
+ }) : null,
2371
+ /*#__PURE__*/ _jsx(AuthorLinkRows, {
2372
+ links: linkRows,
2373
+ size: 1
2374
+ })
2375
+ ]
2376
+ })
2377
+ ]
2378
+ }));
2379
+ });
2380
+ ContentAuthorProfile.displayName = 'AglynContentAuthorProfile';
2381
+ export { ContentAuthorProfile };
2382
+ export const contentAuthorProfileSchema = {
2383
+ $id: AUTHOR_PROFILE_ID,
2384
+ pluginId: BUNDLE_ID,
2385
+ displayName: 'Author Profile',
2386
+ description: 'Who an author page is about — portrait, name, role, bio and links.',
2387
+ category: Aglyn.ComponentCategory.TEXT,
2388
+ icon: {
2389
+ path: mdiAccountCircleOutline.path,
2390
+ sx: {
2391
+ color: 'secondary.main'
2392
+ }
2393
+ },
2394
+ flags: {
2395
+ selfClosing: Aglyn.FEATURE_FLAG.ENABLED
2396
+ },
2397
+ attributes: [
2398
+ {
2399
+ name: 'name',
2400
+ label: 'Name',
2401
+ description: 'Blank shows the name of the author whose page this is. Type here ' + '(or bind {{author.name}}) only to override it.',
2402
+ component: Aglyn.FieldComponentType.TEXT_FIELD
2403
+ },
2404
+ {
2405
+ name: 'bio',
2406
+ label: 'Bio',
2407
+ description: 'Blank shows the bio from the author’s record. Type here (or bind ' + '{{author.bio}}) only to override it.',
2408
+ component: Aglyn.FieldComponentType.TEXTAREA
2409
+ },
2410
+ {
2411
+ name: 'image',
2412
+ label: 'Portrait',
2413
+ description: 'Blank shows the portrait of the author whose page this is, at the ' + 'larger size a profile takes. Pick from your media library only to ' + 'override it.',
2414
+ component: Aglyn.FieldComponentType.TEXT_FIELD
2415
+ },
2416
+ {
2417
+ name: 'jobTitle',
2418
+ label: 'Role',
2419
+ description: 'Blank shows the job title from the author’s record. Only a person ' + 'has one — an author record set to Organization leaves it empty.',
2420
+ component: Aglyn.FieldComponentType.TEXT_FIELD
2421
+ },
2422
+ {
2423
+ name: 'worksFor',
2424
+ label: 'Organization',
2425
+ description: 'Blank shows who the author writes for, from their record. Shown ' + 'after the role as "Role at Organization".',
2426
+ component: Aglyn.FieldComponentType.TEXT_FIELD
2427
+ },
2428
+ {
2429
+ name: 'url',
2430
+ label: 'Link',
2431
+ description: 'Blank uses the author’s own url. The name links here; with no url ' + 'it renders as plain text. This block is already the author’s page ' + 'on this site, so it never links to itself.',
2432
+ component: Aglyn.FieldComponentType.TEXT_FIELD
2433
+ },
2434
+ {
2435
+ name: 'showLinks',
2436
+ label: 'Show links',
2437
+ description: 'Off hides the row of profile links under the bio — the place a ' + 'reader follows this author elsewhere. Turning it off on a page ' + 'about them is rarely what you want.',
2438
+ component: Aglyn.FieldComponentType.SWITCH,
2439
+ // Unset means SHOWN in the renderer above, so the switch has to open
2440
+ // in that position or it lies about the block in front of the author
2441
+ // (AGL-2506).
2442
+ initialValue: true
2443
+ },
2444
+ {
2445
+ name: 'showBio',
2446
+ label: 'Show bio',
2447
+ description: 'Off leaves the block as a portrait, a name and a role.',
2448
+ component: Aglyn.FieldComponentType.SWITCH,
2449
+ initialValue: true
2450
+ },
2451
+ {
2452
+ name: 'showRole',
2453
+ label: 'Show role',
2454
+ description: 'Off hides the "Role at Organization" line under the name.',
2455
+ component: Aglyn.FieldComponentType.SWITCH,
2456
+ initialValue: true
2457
+ },
2458
+ {
2459
+ name: 'showAvatar',
2460
+ label: 'Show portrait',
2461
+ description: 'Off drops the portrait and lets the name and bio run the full ' + 'width, with no space held for a picture.',
2462
+ component: Aglyn.FieldComponentType.SWITCH,
2463
+ initialValue: true
2464
+ }
2465
+ ]
2466
+ };
2467
+ /** Pill look, in palette tokens so it follows the site theme in both modes. */ const pillSx = (active)=>({
2468
+ px: 1.75,
2469
+ py: 0.75,
2470
+ borderRadius: 999,
2471
+ border: '1px solid',
2472
+ borderColor: active ? 'primary.main' : 'divider',
2473
+ bgcolor: active ? 'primary.main' : 'transparent',
2474
+ color: active ? 'primary.contrastText' : 'text.secondary',
2475
+ fontSize: 14,
2476
+ lineHeight: 1.2,
2477
+ whiteSpace: 'nowrap',
2478
+ '&:hover': {
2479
+ borderColor: active ? 'primary.main' : 'text.primary',
2480
+ color: active ? 'primary.contrastText' : 'text.primary'
2481
+ }
2482
+ });
2483
+ /**
2484
+ * The category filter row for a collection listing (AGL-1321): "All" plus one
2485
+ * pill per category, each a REAL anchor to `/{collection}/category/{slug}`.
2486
+ *
2487
+ * Anchors, not click handlers, and that is the whole design. A JS-only toggle
2488
+ * would be invisible to crawlers, unopenable in a new tab, unlinkable, and
2489
+ * unreachable without hydration — and it could not exist anyway, since the
2490
+ * filter is resolved server-side before the page is composed. The pill the
2491
+ * current URL selected carries `aria-current="page"`, stamped on the server so
2492
+ * it is in the HTML rather than derived after hydration.
2493
+ */ const CollectionCategories = /*#__PURE__*/ forwardRef((props, ref)=>{
2494
+ // `collectionSlug`/`allLabel` are compose-time: the tenant resolves them
2495
+ // while stamping `items`; strip so they never hit the DOM.
2496
+ const { collectionSlug, allLabel, items } = props, rest = _object_without_properties_loose(props, [
2497
+ "collectionSlug",
2498
+ "allLabel",
2499
+ "items"
2500
+ ]);
2501
+ // Node styles ride the renderer-merged sx; recompose (stack.ts pattern).
2502
+ const nodeSx = Array.isArray(props['sx']) ? props['sx'] : [
2503
+ props['sx']
2504
+ ];
2505
+ const { suppressNavigation } = useContext(Aglyn.ScreenLinkContext);
2506
+ if (!(items == null ? void 0 : items.length)) {
2507
+ if (!suppressNavigation) return /*#__PURE__*/ _jsx(Box, _extends({
2508
+ ref: ref
2509
+ }, rest));
2510
+ return /*#__PURE__*/ _jsx(Box, _extends({
2511
+ ref: ref
2512
+ }, rest, {
2513
+ sx: [
2514
+ {
2515
+ p: 2,
2516
+ border: '1px dashed',
2517
+ borderColor: 'divider',
2518
+ color: 'text.secondary',
2519
+ fontSize: 12,
2520
+ fontFamily: 'system-ui, sans-serif'
2521
+ },
2522
+ ...nodeSx
2523
+ ],
2524
+ children: 'Category pills — All + one pill per collection category render here'
2525
+ }));
2526
+ }
2527
+ return /*#__PURE__*/ _jsx(MuiStack, _extends({
2528
+ ref: ref,
2529
+ direction: "row",
2530
+ spacing: 1
2531
+ }, rest, {
2532
+ // MERGE, never replace (AGL-1450) — see Entry Meta above.
2533
+ sx: [
2534
+ {
2535
+ flexWrap: 'wrap',
2536
+ rowGap: 1
2537
+ },
2538
+ ...nodeSx
2539
+ ],
2540
+ children: items.map((item)=>// Editing surfaces render the pill look without an href, so a click
2541
+ // in the besigner never navigates the canvas away.
2542
+ suppressNavigation ? /*#__PURE__*/ _jsx(Box, {
2543
+ sx: pillSx(item.active),
2544
+ children: item.label
2545
+ }, item.href) : /*#__PURE__*/ _jsx(AppLink, {
2546
+ href: item.href,
2547
+ underline: "none",
2548
+ "aria-current": item.active ? 'page' : undefined,
2549
+ sx: pillSx(item.active),
2550
+ children: item.label
2551
+ }, item.href))
2552
+ }));
2553
+ });
2554
+ CollectionCategories.displayName = 'AglynCollectionCategories';
2555
+ export const collectionCategoriesSchema = {
2556
+ $id: CATEGORIES_ID,
2557
+ pluginId: BUNDLE_ID,
2558
+ displayName: 'Category Pills',
2559
+ description: 'A pill per collection category, each filtering the listing.',
2560
+ category: Aglyn.ComponentCategory.NAVIGATION,
2561
+ icon: {
2562
+ path: mdiTagMultipleOutline.path,
2563
+ sx: {
2564
+ color: 'secondary.main'
2565
+ }
2566
+ },
2567
+ flags: {
2568
+ selfClosing: Aglyn.FEATURE_FLAG.ENABLED
2569
+ },
2570
+ attributes: [
2571
+ {
2572
+ name: 'collectionSlug',
2573
+ label: 'Collection slug',
2574
+ description: 'Content collection whose categories render as pills (e.g. ' + '"blog"). Leave blank on a list-template screen to use the ' + 'collection from the URL.',
2575
+ component: Aglyn.FieldComponentType.TEXT_FIELD
2576
+ },
2577
+ {
2578
+ name: 'allLabel',
2579
+ label: 'All label',
2580
+ description: 'Label for the unfiltered pill, which links to the collection ' + 'root (default "All"). Clear the box to omit that pill — it then ' + 'reads "none", which is what makes the omission stick. Typing ' + '"none" does the same thing.',
2581
+ component: Aglyn.FieldComponentType.TEXT_FIELD,
2582
+ // Without this the emptied field persists NOTHING (AGL-1336): ddf
2583
+ // maps an emptied value to `clearedValue`, final-form's parse turns
2584
+ // `''` into `undefined`, and the missing key takes the runtime
2585
+ // default — so "clear it to omit that pill" was undoable by
2586
+ // clicking. Same shape as FIELD_COLOR_ALT1's `'default'` (AGL-1191):
2587
+ // a value that means something has to BE something.
2588
+ //
2589
+ // ddf only substitutes `clearedValue` when the field HAD an initial
2590
+ // value (`enhancedOnChange`'s `typeof initial !== 'undefined'`),
2591
+ // which is why the preset below seeds `allLabel` and why the
2592
+ // sentinel is a word an author can also type. A block that never
2593
+ // carried the prop has an already-empty box and nothing to clear.
2594
+ clearedValue: Aglyn.COLLECTION_ALL_PILL_NONE
2595
+ }
2596
+ ]
2597
+ };
2598
+ /**
2599
+ * A search box for a collection that lives where the author puts it
2600
+ * (AGL-1516, Figma 494:1220).
2601
+ *
2602
+ * The entries block has had a search box since AGL-1516's first half, and it
2603
+ * renders inside that block — first child of its own stack. The frame puts
2604
+ * the field in the listing's TOOLBAR: category pills on the left, search and
2605
+ * RSS on the right, one row. That was unbuildable, and unbuildable in both
2606
+ * directions — a block's own child cannot be lifted out of it, and the pills
2607
+ * cannot be moved in, because every child of an entries block is cloned once
2608
+ * per entry. Three passes of authoring hit the same wall.
2609
+ *
2610
+ * So the field became its own block. It searches the collection the listing
2611
+ * beside it is drawn from, and answers with the suggestion panel (496:1218)
2612
+ * — the only behaviour a standalone box can honestly have, since it owns no
2613
+ * cards to hide. Enter submits to the site-wide results page, so the box
2614
+ * works with no JavaScript at all.
2615
+ *
2616
+ * Inert on editing surfaces, like Category Pills: the besigner has no index
2617
+ * to search, and a field that answered there would be answering about
2618
+ * nothing.
2619
+ */ const CollectionSearch = /*#__PURE__*/ forwardRef((props, ref)=>{
2620
+ // Compose-time and search-only attributes: strip so they never hit the
2621
+ // DOM.
2622
+ const { collectionSlug, searchPlaceholder, searchIndex, searchTotal, searchCapped } = props, rest = _object_without_properties_loose(props, [
2623
+ "collectionSlug",
2624
+ "searchPlaceholder",
2625
+ "searchIndex",
2626
+ "searchTotal",
2627
+ "searchCapped"
2628
+ ]);
2629
+ // Node styles ride the renderer-merged sx; recompose (stack.ts pattern).
2630
+ const nodeSx = Array.isArray(props['sx']) ? props['sx'] : [
2631
+ props['sx']
2632
+ ];
2633
+ const { suppressNavigation } = useContext(Aglyn.ScreenLinkContext);
2634
+ const fuzzy = useMemo(()=>(searchIndex == null ? void 0 : searchIndex.length) ? new Fuse(searchIndex, _extends({}, Aglyn.COLLECTION_SEARCH_FUSE_OPTIONS)) : null, [
2635
+ searchIndex
2636
+ ]);
2637
+ if (!suppressNavigation && !fuzzy) {
2638
+ // Nothing stamped on a live surface: an unknown collection, an empty
2639
+ // one, or a page composed before this block existed. Renders NOTHING
2640
+ // rather than a field that can only ever answer "no matches" — that
2641
+ // answer would read as a fact about the reader's query instead of
2642
+ // about the absent index behind it.
2643
+ return /*#__PURE__*/ _jsx(Box, _extends({
2644
+ ref: ref
2645
+ }, rest));
2646
+ }
2647
+ return /*#__PURE__*/ _jsx(MuiStack, _extends({
2648
+ ref: ref
2649
+ }, rest, {
2650
+ sx: [
2651
+ {
2652
+ alignItems: 'flex-end'
2653
+ },
2654
+ ...nodeSx
2655
+ ],
2656
+ children: /*#__PURE__*/ _jsx(SuggestSearchBox, _extends({
2657
+ fuzzy: fuzzy
2658
+ }, searchIndex ? {
2659
+ items: searchIndex
2660
+ } : {}, searchPlaceholder === undefined ? {} : {
2661
+ placeholder: searchPlaceholder
2662
+ }, suppressNavigation ? {
2663
+ inert: true
2664
+ } : {}, {
2665
+ // The honest miss for THIS box. It searched one bounded read of the
2666
+ // collection — never "the collection", and on a category route not
2667
+ // even the whole of it, because the source arrives filtered. So it
2668
+ // reports the number it actually looked through, and says plainly
2669
+ // when that number was a ceiling rather than a total.
2670
+ emptyText: (text)=>{
2671
+ var _ref, _ref1;
2672
+ return searchCapped ? `No matches for “${text}” in the ${(_ref = searchIndex == null ? void 0 : searchIndex.length) != null ? _ref : 0} ` + 'entries searched here — the collection holds more, which ' + 'this box has not read.' : `No matches for “${text}” in the ${(_ref1 = searchIndex == null ? void 0 : searchIndex.length) != null ? _ref1 : 0} ` + 'entries searched here.';
2673
+ }
2674
+ }))
2675
+ }));
2676
+ });
2677
+ CollectionSearch.displayName = 'AglynCollectionSearch';
2678
+ export const collectionSearchSchema = {
2679
+ $id: SEARCH_ID,
2680
+ pluginId: BUNDLE_ID,
2681
+ displayName: 'Collection Search',
2682
+ description: 'A search box for a content collection, with a suggestions dropdown.',
2683
+ category: Aglyn.ComponentCategory.NAVIGATION,
2684
+ icon: {
2685
+ path: mdiMagnify.path,
2686
+ sx: {
2687
+ color: 'secondary.main'
2688
+ }
2689
+ },
2690
+ flags: {
2691
+ selfClosing: Aglyn.FEATURE_FLAG.ENABLED
2692
+ },
2693
+ attributes: [
2694
+ {
2695
+ name: 'collectionSlug',
2696
+ label: 'Collection slug',
2697
+ description: 'Content collection this box searches (e.g. "blog"). Leave blank ' + 'on a list-template screen to use the collection from the URL.',
2698
+ component: Aglyn.FieldComponentType.TEXT_FIELD
2699
+ },
2700
+ {
2701
+ name: 'searchPlaceholder',
2702
+ label: 'Search placeholder',
2703
+ description: 'Hint text inside this box. Blank shows "Search posts…", which is ' + 'worth changing when the collection is not posts.',
2704
+ component: Aglyn.FieldComponentType.TEXT_FIELD
2705
+ }
2706
+ ]
2707
+ };
2708
+ /* ── Presets ────────────────────────────────────────────────────────────── */ /**
2709
+ * Styling rides the node's own `sx`, never `props.sx` (AGL-1346): both
2710
+ * render, but only `node.sx` is the record the Styles panel can edit or
2711
+ * clear. `Leaf` composes `node.sx` last, so this is the same result.
2712
+ */ const entryText = (variant, children, extra)=>{
2713
+ const _ref = extra != null ? extra : {}, { sx } = _ref, props = _object_without_properties_loose(_ref, [
2714
+ "sx"
2715
+ ]);
2716
+ return _extends({
2717
+ $id: null,
2718
+ componentId: 'muiTypography',
2719
+ pluginId: BUNDLE_ID,
2720
+ props: _extends({
2721
+ variant,
2722
+ children
2723
+ }, props)
2724
+ }, sx ? {
2725
+ sx
2726
+ } : {});
2727
+ };
2728
+ export const collectionPresets = [
2729
+ {
2730
+ $id: generatePresetId(ENTRIES_ID),
2731
+ type: Aglyn.NodeType.PRESET,
2732
+ displayName: 'Collection Entries',
2733
+ pluginId: BUNDLE_ID,
2734
+ description: 'Repeats a card (title, date, excerpt, Read more) per published ' + 'entry of a content collection',
2735
+ category: Aglyn.ComponentCategory.BLOCKS,
2736
+ icon: {
2737
+ path: mdiPostOutline.path,
2738
+ sx: {
2739
+ color: 'secondary.main'
2740
+ }
2741
+ },
2742
+ data: {
2743
+ $id: null,
2744
+ componentId: ENTRIES_ID,
2745
+ pluginId: BUNDLE_ID,
2746
+ props: {
2747
+ spacing: 4
2748
+ },
2749
+ nodes: [
2750
+ {
2751
+ $id: null,
2752
+ componentId: 'muiStack',
2753
+ pluginId: BUNDLE_ID,
2754
+ props: {
2755
+ spacing: 0.5
2756
+ },
2757
+ nodes: [
2758
+ entryText('h5', '{{entry.title}}', {
2759
+ component: 'h2'
2760
+ }),
2761
+ entryText('caption', '{{entry.date}}', {
2762
+ sx: {
2763
+ color: 'text.secondary'
2764
+ }
2765
+ }),
2766
+ entryText('body1', '{{entry.excerpt}}'),
2767
+ {
2768
+ $id: null,
2769
+ componentId: 'muiScreenLink',
2770
+ pluginId: BUNDLE_ID,
2771
+ props: {
2772
+ children: 'Read more',
2773
+ href: '{{entry.url}}',
2774
+ size: 'small'
2775
+ },
2776
+ sx: {
2777
+ alignSelf: 'flex-start'
2778
+ }
2779
+ }
2780
+ ]
2781
+ }
2782
+ ]
2783
+ }
2784
+ },
2785
+ {
2786
+ $id: generatePresetId(ENTRY_BODY_ID),
2787
+ type: Aglyn.NodeType.PRESET,
2788
+ displayName: 'Entry Body',
2789
+ pluginId: BUNDLE_ID,
2790
+ description: "Renders the current entry's markdown body, themed",
2791
+ category: Aglyn.ComponentCategory.TEXT,
2792
+ icon: {
2793
+ path: mdiTextLong.path,
2794
+ sx: {
2795
+ color: 'secondary.main'
2796
+ }
2797
+ },
2798
+ data: {
2799
+ $id: null,
2800
+ componentId: ENTRY_BODY_ID,
2801
+ pluginId: BUNDLE_ID,
2802
+ props: {
2803
+ markdown: '{{entry.body}}'
2804
+ }
2805
+ }
2806
+ },
2807
+ {
2808
+ $id: generatePresetId(RELATED_ID),
2809
+ type: Aglyn.NodeType.PRESET,
2810
+ displayName: 'Related Posts',
2811
+ pluginId: BUNDLE_ID,
2812
+ description: "Other entries sharing the current entry's category or tags",
2813
+ category: Aglyn.ComponentCategory.DATA_DISPLAY,
2814
+ icon: {
2815
+ path: mdiNewspaperVariantOutline.path,
2816
+ sx: {
2817
+ color: 'secondary.main'
2818
+ }
2819
+ },
2820
+ data: {
2821
+ $id: null,
2822
+ componentId: RELATED_ID,
2823
+ pluginId: BUNDLE_ID,
2824
+ // `layout`, the two type steps and `dateFormat` are seeded rather than
2825
+ // left to the runtime fallback so each dropdown opens on the value the
2826
+ // block is actually rendering, and an author who tries another has a
2827
+ // named route back (AGL-1457/AGL-1459). The `show*` switches are
2828
+ // deliberately absent: their unset state IS the shipped behaviour.
2829
+ props: {
2830
+ heading: 'Related articles',
2831
+ limit: 3,
2832
+ layout: 'list',
2833
+ headingVariant: 'h5',
2834
+ titleVariant: 'subtitle1',
2835
+ dateFormat: Aglyn.COLLECTION_ENTRY_DATE_FORMAT_DEFAULT
2836
+ }
2837
+ }
2838
+ },
2839
+ {
2840
+ $id: generatePresetId(SHARE_ID),
2841
+ type: Aglyn.NodeType.PRESET,
2842
+ displayName: 'Share Bar',
2843
+ pluginId: BUNDLE_ID,
2844
+ description: 'X, LinkedIn, Facebook, and copy-link buttons for the page',
2845
+ category: Aglyn.ComponentCategory.NAVIGATION,
2846
+ icon: {
2847
+ path: mdiShareVariant.path,
2848
+ sx: {
2849
+ color: 'secondary.main'
2850
+ }
2851
+ },
2852
+ data: {
2853
+ $id: null,
2854
+ componentId: SHARE_ID,
2855
+ pluginId: BUNDLE_ID,
2856
+ props: {
2857
+ heading: 'Share'
2858
+ }
2859
+ }
2860
+ },
2861
+ {
2862
+ $id: generatePresetId(CATEGORIES_ID),
2863
+ type: Aglyn.NodeType.PRESET,
2864
+ displayName: 'Category Pills',
2865
+ pluginId: BUNDLE_ID,
2866
+ description: "Links to each of the collection's categories, filtering the listing",
2867
+ category: Aglyn.ComponentCategory.NAVIGATION,
2868
+ icon: {
2869
+ path: mdiTagMultipleOutline.path,
2870
+ sx: {
2871
+ color: 'secondary.main'
2872
+ }
2873
+ },
2874
+ data: {
2875
+ $id: null,
2876
+ componentId: CATEGORIES_ID,
2877
+ pluginId: BUNDLE_ID,
2878
+ // Seeded, not left to the runtime default: an explicit initial value
2879
+ // is what lets the attributes form substitute the cleared sentinel
2880
+ // when the author empties the box (AGL-1336).
2881
+ props: {
2882
+ allLabel: Aglyn.COLLECTION_ALL_PILL_DEFAULT
2883
+ }
2884
+ }
2885
+ },
2886
+ {
2887
+ // Every author-placeable component needs a preset: the element drawer is
2888
+ // built from PRESETS alone (`ComponentManager.schemasByCategory` iterates
2889
+ // `this.presets`), so a component with only a schema renders correctly
2890
+ // once a node exists but can never be put on a canvas by clicking.
2891
+ //
2892
+ // This one earns its place in the drawer because the entries block's own
2893
+ // search field cannot leave that block, and the toolbar row wants the
2894
+ // search beside the category pills rather than above the entries.
2895
+ $id: generatePresetId(SEARCH_ID),
2896
+ type: Aglyn.NodeType.PRESET,
2897
+ displayName: 'Collection Search',
2898
+ pluginId: BUNDLE_ID,
2899
+ description: 'Search box for a collection, with a dropdown of matching entries',
2900
+ category: Aglyn.ComponentCategory.NAVIGATION,
2901
+ icon: {
2902
+ path: mdiMagnify.path,
2903
+ sx: {
2904
+ color: 'secondary.main'
2905
+ }
2906
+ },
2907
+ data: {
2908
+ $id: null,
2909
+ componentId: SEARCH_ID,
2910
+ pluginId: BUNDLE_ID,
2911
+ // Deliberately no seeded props. Both attributes read blank as a real
2912
+ // choice — `collectionSlug` blank means "take the collection from the
2913
+ // URL", which is correct on the list templates this block is for, and
2914
+ // `searchPlaceholder` blank means the designed "Search posts…".
2915
+ props: {}
2916
+ }
2917
+ },
2918
+ {
2919
+ $id: generatePresetId(ENTRY_META_ID),
2920
+ type: Aglyn.NodeType.PRESET,
2921
+ displayName: 'Entry Meta',
2922
+ pluginId: BUNDLE_ID,
2923
+ description: 'Author · date · category line with tag chips for the entry',
2924
+ category: Aglyn.ComponentCategory.TEXT,
2925
+ icon: {
2926
+ path: mdiTagOutline.path,
2927
+ sx: {
2928
+ color: 'secondary.main'
2929
+ }
2930
+ },
2931
+ data: {
2932
+ $id: null,
2933
+ componentId: ENTRY_META_ID,
2934
+ pluginId: BUNDLE_ID,
2935
+ props: {
2936
+ date: '{{entry.date}}',
2937
+ // Named rather than left to the runtime fallback (AGL-1459): the
2938
+ // dropdown opens on the value the block is actually rendering, and
2939
+ // an author who tries a format has a named route back. `default` is
2940
+ // also the fallback, so naming it is the same choice, not a second.
2941
+ dateFormat: Aglyn.COLLECTION_ENTRY_DATE_FORMAT_DEFAULT,
2942
+ author: '{{entry.author}}',
2943
+ category: '{{entry.category}}',
2944
+ tags: '{{entry.tags}}'
2945
+ }
2946
+ }
2947
+ },
2948
+ {
2949
+ $id: generatePresetId(ENTRY_AUTHOR_ID),
2950
+ type: Aglyn.NodeType.PRESET,
2951
+ displayName: 'Entry Author',
2952
+ pluginId: BUNDLE_ID,
2953
+ description: 'Portrait, byline and bio card for the entry’s author',
2954
+ category: Aglyn.ComponentCategory.TEXT,
2955
+ icon: {
2956
+ path: mdiAccountCircleOutline.path,
2957
+ sx: {
2958
+ color: 'secondary.main'
2959
+ }
2960
+ },
2961
+ data: {
2962
+ $id: null,
2963
+ componentId: ENTRY_AUTHOR_ID,
2964
+ pluginId: BUNDLE_ID,
2965
+ // Seeded EMPTY, unlike Entry Meta's preset. Those bindings predate the
2966
+ // server fill and stay for compatibility; here the fill is the only
2967
+ // mechanism, and a seeded `{{entry.authorImage}}` would render a broken
2968
+ // portrait on every surface that is not an entry template.
2969
+ props: {}
2970
+ }
2971
+ },
2972
+ {
2973
+ $id: generatePresetId(AUTHOR_PROFILE_ID),
2974
+ type: Aglyn.NodeType.PRESET,
2975
+ displayName: 'Author Profile',
2976
+ pluginId: BUNDLE_ID,
2977
+ description: 'Portrait, name, role, bio and links for an author page',
2978
+ category: Aglyn.ComponentCategory.TEXT,
2979
+ icon: {
2980
+ path: mdiAccountCircleOutline.path,
2981
+ sx: {
2982
+ color: 'secondary.main'
2983
+ }
2984
+ },
2985
+ data: {
2986
+ $id: null,
2987
+ componentId: AUTHOR_PROFILE_ID,
2988
+ pluginId: BUNDLE_ID,
2989
+ // Seeded EMPTY, for the reason the card above it is: the server fill is
2990
+ // the only mechanism here, and a seeded `{{author.image}}` would render
2991
+ // a broken portrait on every surface that is not an author page.
2992
+ props: {}
2993
+ }
2994
+ }
2995
+ ];
2996
+ export { CollectionCategories, CollectionEntries, CollectionEntryAuthor, CollectionEntryBody, CollectionEntryMeta, CollectionRelated, CollectionSearch, CollectionShare, };
2997
+ export default CollectionEntries;
2998
+
2999
+ //# sourceMappingURL=collection.js.map