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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (57) hide show
  1. package/package.json +7 -7
  2. package/src/lib/components/collection-author-links.d.ts +41 -0
  3. package/src/lib/components/collection-author-links.js +135 -0
  4. package/src/lib/components/collection-author-links.js.map +1 -0
  5. package/src/lib/components/collection-author-profile.d.ts +67 -0
  6. package/src/lib/components/collection-author-profile.js +269 -0
  7. package/src/lib/components/collection-author-profile.js.map +1 -0
  8. package/src/lib/components/collection-categories.d.ts +52 -0
  9. package/src/lib/components/collection-categories.js +163 -0
  10. package/src/lib/components/collection-categories.js.map +1 -0
  11. package/src/lib/components/collection-common.d.ts +104 -0
  12. package/src/lib/components/collection-common.js +95 -0
  13. package/src/lib/components/collection-common.js.map +1 -0
  14. package/src/lib/components/collection-entries.d.ts +137 -0
  15. package/src/lib/components/collection-entries.js +358 -0
  16. package/src/lib/components/collection-entries.js.map +1 -0
  17. package/src/lib/components/collection-entry-author.d.ts +81 -0
  18. package/src/lib/components/collection-entry-author.js +259 -0
  19. package/src/lib/components/collection-entry-author.js.map +1 -0
  20. package/src/lib/components/collection-entry-body.d.ts +44 -0
  21. package/src/lib/components/collection-entry-body.js +328 -0
  22. package/src/lib/components/collection-entry-body.js.map +1 -0
  23. package/src/lib/components/collection-entry-meta.d.ts +88 -0
  24. package/src/lib/components/collection-entry-meta.js +331 -0
  25. package/src/lib/components/collection-entry-meta.js.map +1 -0
  26. package/src/lib/components/collection-presets.d.ts +18 -0
  27. package/src/lib/components/collection-presets.js +328 -0
  28. package/src/lib/components/collection-presets.js.map +1 -0
  29. package/src/lib/components/collection-related.d.ts +101 -0
  30. package/src/lib/components/collection-related.js +536 -0
  31. package/src/lib/components/collection-related.js.map +1 -0
  32. package/src/lib/components/collection-search-box.d.ts +92 -0
  33. package/src/lib/components/collection-search-box.js +334 -0
  34. package/src/lib/components/collection-search-box.js.map +1 -0
  35. package/src/lib/components/collection-search-fuse.d.ts +28 -0
  36. package/src/lib/components/collection-search-fuse.js +28 -0
  37. package/src/lib/components/collection-search-fuse.js.map +1 -0
  38. package/src/lib/components/collection-search.d.ts +62 -0
  39. package/src/lib/components/collection-search.js +140 -0
  40. package/src/lib/components/collection-search.js.map +1 -0
  41. package/src/lib/components/collection-share.d.ts +31 -0
  42. package/src/lib/components/collection-share.js +154 -0
  43. package/src/lib/components/collection-share.js.map +1 -0
  44. package/src/lib/components/collection.d.ts +19 -497
  45. package/src/lib/components/collection.js +19 -2982
  46. package/src/lib/components/collection.js.map +1 -1
  47. package/src/lib/components/image-list.d.ts +2 -2
  48. package/src/lib/components/image-list.js +4 -6
  49. package/src/lib/components/image-list.js.map +1 -1
  50. package/src/lib/element-tier.d.ts +9 -1
  51. package/src/lib/element-tier.js +12 -1
  52. package/src/lib/element-tier.js.map +1 -1
  53. package/src/lib/plugin.js +20 -12
  54. package/src/lib/plugin.js.map +1 -1
  55. package/src/lib/utils/to-count.d.ts +25 -0
  56. package/src/lib/utils/to-count.js +30 -0
  57. package/src/lib/utils/to-count.js.map +1 -0
@@ -0,0 +1,358 @@
1
+ import { _ as _extends } from "@swc/helpers/_/_extends";
2
+ import { _ as _object_without_properties_loose } from "@swc/helpers/_/_object_without_properties_loose";
3
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
4
+ /**
5
+ * @license
6
+ * Copyright 2026 Aglyn LLC
7
+ *
8
+ * Licensed under the Apache License, Version 2.0 (the "License");
9
+ * you may not use this file except in compliance with the License.
10
+ * You may obtain a copy of the License at
11
+ *
12
+ * http://www.apache.org/licenses/LICENSE-2.0
13
+ *
14
+ * Unless required by applicable law or agreed to in writing, software
15
+ * distributed under the License is distributed on an "AS IS" BASIS,
16
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
17
+ * See the License for the specific language governing permissions and
18
+ * limitations under the License.
19
+ */ import * as Aglyn from "@aglyn/aglyn";
20
+ import { mdiPostOutline } from "@aglyn/shared-data-mdi";
21
+ import MuiStack from "@mui/material/Stack";
22
+ import Typography from "@mui/material/Typography";
23
+ import { Children, forwardRef, useContext, useState } from "react";
24
+ import { BUNDLE_ID } from "../constants/bundle-common.js";
25
+ // One coercion for every authored count in this bundle (AGL-1457) — number
26
+ // fields round-trip as strings, and a second parser here would drift.
27
+ import { toCount } from "../utils/to-count.js";
28
+ import { SearchField, SuggestSearchBox, useEntryFuse } from "./collection-search-box.js";
29
+ // Persisted component id (AGL-551/582); the compose pipeline references it
30
+ // through the @aglyn/aglyn constant. Never rename.
31
+ export const ENTRIES_ID = Aglyn.COLLECTION_ENTRIES_COMPONENT_ID;
32
+ /**
33
+ * Author-facing choices for {@link CollectionEntriesProps.searchMode}.
34
+ *
35
+ * Both values are truthy on purpose (AGL-1453): `''` cannot survive a save,
36
+ * so an author who tried the other mode would have no route back. "Filter"
37
+ * carries the absent-value reading, which is what every block published
38
+ * before AGL-1525 has.
39
+ */ export const COLLECTION_SEARCH_MODE_OPTIONS = [
40
+ {
41
+ value: 'filter',
42
+ label: 'Filter the entries in place'
43
+ },
44
+ {
45
+ value: 'suggest',
46
+ label: 'Show a suggestions dropdown'
47
+ }
48
+ ];
49
+ /**
50
+ * How much of the collection an entries block's search could actually see
51
+ * (AGL-1516, AGL-2569) — `truncated` says the rendered window is smaller
52
+ * than the set, `paginated` says the missing part lives on other pages of
53
+ * this same list rather than merely outside the block.
54
+ *
55
+ * The test is whether the window is SMALLER THAN THE SET, which only the
56
+ * server knows — `perPage` is the wrong proxy for it in both directions. A
57
+ * block truncated by `entriesLimit` or by the 100-entry cap has no `perPage`
58
+ * at all and would claim a global miss over 6 of 40 posts; a block whose
59
+ * `perPage` exceeds its entry count is a single complete page and would
60
+ * blame pages that do not exist. `searchTotal` is stamped with `searchIndex`,
61
+ * so the two are always in step.
62
+ *
63
+ * Absent — a page cached before that stamp shipped — falls back to the old
64
+ * `perPage` reading rather than guessing "complete": understating the scope
65
+ * of a search is the safe direction to be wrong in.
66
+ *
67
+ * `searchCapped` is the second way a block can fail to hold the set, and the
68
+ * invisible one: `searchTotal` counts the entries the server READ, and that
69
+ * read is bounded. Past the bound `shown === searchTotal` is satisfied by a
70
+ * block holding 100 of 400 posts, and the wording that claims to have looked
71
+ * everywhere is exactly the branch it would take.
72
+ *
73
+ * Both the miss and the hit disclose off this one answer, so the two can
74
+ * never disagree about the same block.
75
+ */ const searchScope = ({ perPage, shown, searchTotal, searchCapped })=>{
76
+ var _toCount;
77
+ const paginated = ((_toCount = toCount(perPage, 0)) != null ? _toCount : 0) > 0;
78
+ return {
79
+ paginated,
80
+ truncated: typeof searchTotal === 'number' ? shown < searchTotal || Boolean(searchCapped) : paginated || Boolean(searchCapped)
81
+ };
82
+ };
83
+ /**
84
+ * Repeats its children once per published entry of a content collection
85
+ * (AGL-551) — the collections sibling of the dataset repeatable. The tenant
86
+ * expands it at compose time with `{{entry.*}}` tokens; in the besigner the
87
+ * template renders once with literal tokens, matching the repeatable UX.
88
+ *
89
+ * Search (AGL-1516): with `search` on, a toolbar search box filters the
90
+ * rendered entry clones client-side against the server-stamped
91
+ * `searchIndex` — fuzzy, via the same Fuse the icon picker uses. The clones
92
+ * arrive as `children` in stamp order, one group of template roots per
93
+ * entry, so group N of the children IS entry N of the index. Matches keep
94
+ * the list's own order rather than Fuse's score order: a blog list is
95
+ * chronological, and search narrows it, it does not reshuffle it. In the
96
+ * besigner the field renders as an inert affordance (the template renders
97
+ * once with literal tokens and there is no index to search), matching how
98
+ * Category Pills go inert on editing surfaces.
99
+ */ const CollectionEntries = /*#__PURE__*/ forwardRef(// collectionSlug/entriesLimit/filter*/search* are compose-time or
100
+ // search-only attributes: strip so they never hit the DOM.
101
+ (_0, _1)=>{
102
+ let _ref = [
103
+ _0,
104
+ _1
105
+ ], [_ref1, ..._rest] = _ref, { collectionSlug, entriesLimit, filterCategory, filterTag, perPage, page, firstPageOnly, search, searchMode, searchPlaceholder, searchIndex, searchTotal, searchCapped, children } = _ref1, props = _object_without_properties_loose(_ref1, [
106
+ "collectionSlug",
107
+ "entriesLimit",
108
+ "filterCategory",
109
+ "filterTag",
110
+ "perPage",
111
+ "page",
112
+ "firstPageOnly",
113
+ "search",
114
+ "searchMode",
115
+ "searchPlaceholder",
116
+ "searchIndex",
117
+ "searchTotal",
118
+ "searchCapped",
119
+ "children"
120
+ ]), [ref] = _rest;
121
+ const { suppressNavigation } = useContext(Aglyn.ScreenLinkContext);
122
+ /** `filter` mode only — `suggest` owns its own query (AGL-1516). */ const [query, setQuery] = useState('');
123
+ const items = search ? searchIndex : undefined;
124
+ const { fuzzy, requestFuzzy } = useEntryFuse(items);
125
+ if (!search) {
126
+ return /*#__PURE__*/ _jsx(MuiStack, _extends({
127
+ ref: ref,
128
+ spacing: 4
129
+ }, props, {
130
+ children: children
131
+ }));
132
+ }
133
+ const childArray = Children.toArray(children);
134
+ // Template roots per entry. Every entry clones the same template, so the
135
+ // children divide evenly; anything else means the children are not the
136
+ // stamped clones, and filtering blind would hide the wrong cards —
137
+ // fail open and render everything.
138
+ const groupSize = (items == null ? void 0 : items.length) && childArray.length % items.length === 0 ? childArray.length / items.length : 0;
139
+ // `suggest` draws its rows from the INDEX and never touches the clones,
140
+ // so the clone-alignment precondition is a `filter` precondition only.
141
+ // A block whose children do not divide evenly still suggests correctly;
142
+ // requiring `groupSize` here would silently kill the panel on exactly
143
+ // the templates that made the fail-open necessary.
144
+ const mode = searchMode === 'suggest' ? 'suggest' : 'filter';
145
+ // Whether there is an index to search, NOT whether the matcher has loaded:
146
+ // the field has to be there for the keystroke that loads it.
147
+ const live = !suppressNavigation && Boolean(items == null ? void 0 : items.length) && (mode === 'suggest' || groupSize > 0);
148
+ const trimmed = query.trim();
149
+ let visible = children;
150
+ /**
151
+ * The one line under the results that says what the search covered —
152
+ * either a miss scoped to the set that came back empty, or a hit scoped
153
+ * to the window it was drawn from. Never both.
154
+ */ let scopeNote = null;
155
+ if (mode === 'suggest') {
156
+ // The grid is deliberately untouched (Figma 496:1218): the panel is an
157
+ // overlay on a page the reader is still reading, not a filter.
158
+ visible = children;
159
+ } else if (live && trimmed && fuzzy) {
160
+ var _ref2;
161
+ const matched = new Set(fuzzy.search(trimmed).map((result)=>result.refIndex));
162
+ visible = childArray.filter((_, index)=>matched.has(Math.floor(index / groupSize)));
163
+ // HONEST scope (AGL-1516, AGL-2569): this block holds whatever window
164
+ // the expansion gave it, and pretending the search was global would
165
+ // turn every miss into a false "this post does not exist" — and every
166
+ // hit into a result set that looks like the whole answer.
167
+ const shown = (_ref2 = items == null ? void 0 : items.length) != null ? _ref2 : 0;
168
+ const { paginated, truncated } = searchScope({
169
+ perPage,
170
+ shown,
171
+ searchTotal,
172
+ searchCapped
173
+ });
174
+ if (!visible.length) {
175
+ scopeNote = /*#__PURE__*/ _jsx(Typography, {
176
+ variant: "body2",
177
+ sx: {
178
+ color: 'text.secondary'
179
+ },
180
+ 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.'
181
+ });
182
+ } else if (truncated) {
183
+ // The hit half of the same honesty (AGL-2569). Matches are drawn from
184
+ // the same window the miss apologizes for, they carry no pager of
185
+ // their own, and nothing else on screen says the pages behind this
186
+ // one went unread — so a list that happens to be partial reads as
187
+ // complete. A block that holds the whole collection says nothing
188
+ // here: a disclaimer on a complete answer is noise, and every small
189
+ // site would carry it.
190
+ //
191
+ // No route out to `/search?q=` (AGL-88): the suggestion panel links
192
+ // there because it is the panel's only possible answer, but this
193
+ // mode has a working list under the field and that page does not
194
+ // exist yet. Trading a silent partial answer for a dead link is not
195
+ // the trade.
196
+ scopeNote = /*#__PURE__*/ _jsx(Typography, {
197
+ variant: "body2",
198
+ sx: {
199
+ color: 'text.secondary'
200
+ },
201
+ 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.'
202
+ });
203
+ }
204
+ }
205
+ // The field renders when there is something to search, and as an inert
206
+ // affordance on editing surfaces so the author sees what they enabled.
207
+ // A live surface with nothing stamped (unknown collection, zero entries)
208
+ // renders no field at all — a search box over nothing is a lie.
209
+ const showField = suppressNavigation || live;
210
+ const field = !showField ? null : mode === 'suggest' ? // Rows in Fuse's SCORE order, the opposite of what `filter` does and
211
+ // deliberately: filtering narrows a chronological feed, so reshuffling
212
+ // it would be a second, unasked-for change to what the reader is
213
+ // looking at. A suggestion list is not a feed — it is an answer to a
214
+ // question, and the best answer belongs first.
215
+ /*#__PURE__*/ _jsx(SuggestSearchBox, _extends({
216
+ fuzzy: fuzzy,
217
+ requestFuzzy: requestFuzzy
218
+ }, items ? {
219
+ items
220
+ } : {}, searchPlaceholder === undefined ? {} : {
221
+ placeholder: searchPlaceholder
222
+ }, suppressNavigation ? {
223
+ inert: true
224
+ } : {}, {
225
+ // This block searched the entries IT rendered — a page window, an
226
+ // `entriesLimit` slice, or a bounded read. Never "this collection".
227
+ emptyText: (text)=>`No matches for “${text}” on this page.`
228
+ })) : /*#__PURE__*/ _jsx(SearchField, _extends({
229
+ value: query,
230
+ landmark: true
231
+ }, searchPlaceholder === undefined ? {} : {
232
+ placeholder: searchPlaceholder
233
+ }, suppressNavigation ? {
234
+ inert: true
235
+ } : {}, {
236
+ onQuery: (next)=>{
237
+ requestFuzzy();
238
+ setQuery(next);
239
+ }
240
+ }));
241
+ return /*#__PURE__*/ _jsxs(MuiStack, _extends({
242
+ ref: ref,
243
+ spacing: 4
244
+ }, props, {
245
+ children: [
246
+ field,
247
+ visible,
248
+ scopeNote
249
+ ]
250
+ }));
251
+ });
252
+ CollectionEntries.displayName = 'AglynCollectionEntries';
253
+ export const collectionEntriesSchema = {
254
+ $id: ENTRIES_ID,
255
+ pluginId: BUNDLE_ID,
256
+ displayName: 'Collection Entries',
257
+ description: 'Repeats its children once per entry in a content collection.',
258
+ category: Aglyn.ComponentCategory.DATA_DISPLAY,
259
+ icon: {
260
+ path: mdiPostOutline.path,
261
+ sx: {
262
+ color: 'secondary.main'
263
+ }
264
+ },
265
+ attributes: [
266
+ {
267
+ name: 'collectionSlug',
268
+ label: 'Collection slug',
269
+ 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.',
270
+ component: Aglyn.FieldComponentType.TEXT_FIELD
271
+ },
272
+ {
273
+ name: 'entriesLimit',
274
+ label: 'Entries limit',
275
+ description: 'Maximum entries rendered (blank = all, capped at 100).',
276
+ component: Aglyn.FieldComponentType.TEXT_FIELD,
277
+ type: 'number'
278
+ },
279
+ {
280
+ name: 'filterCategory',
281
+ label: 'Filter by category',
282
+ description: 'Only entries in this category repeat — the category name or its ' + 'stable id both match (e.g. "Guides"). Blank = no category filter.',
283
+ component: Aglyn.FieldComponentType.TEXT_FIELD
284
+ },
285
+ {
286
+ name: 'filterTag',
287
+ label: 'Filter by tag',
288
+ description: 'Only entries carrying this tag repeat (e.g. "nextjs"). Blank = ' + 'no tag filter.',
289
+ component: Aglyn.FieldComponentType.TEXT_FIELD
290
+ },
291
+ {
292
+ name: 'perPage',
293
+ label: 'Entries per page',
294
+ description: 'Paginate the list: entries per page (blank = no pagination). ' + 'Pairs with the page from /{collection}/page/{n}.',
295
+ component: Aglyn.FieldComponentType.TEXT_FIELD,
296
+ type: 'number'
297
+ },
298
+ {
299
+ name: 'page',
300
+ label: 'Page',
301
+ description: '1-based page to render when Entries per page is set.',
302
+ component: Aglyn.FieldComponentType.TEXT_FIELD,
303
+ type: 'number'
304
+ },
305
+ {
306
+ name: 'firstPageOnly',
307
+ label: 'Only on page 1',
308
+ 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.',
309
+ component: Aglyn.FieldComponentType.SWITCH
310
+ },
311
+ {
312
+ name: 'search',
313
+ label: 'Search',
314
+ 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.',
315
+ component: Aglyn.FieldComponentType.SWITCH
316
+ },
317
+ {
318
+ name: 'searchMode',
319
+ label: 'When the reader types',
320
+ 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.',
321
+ component: Aglyn.FieldComponentType.SELECT,
322
+ options: COLLECTION_SEARCH_MODE_OPTIONS.map((option)=>_extends({}, option)),
323
+ // Meaningless while there is no search box to type in.
324
+ condition: {
325
+ when: 'search',
326
+ is: true
327
+ }
328
+ },
329
+ {
330
+ name: 'searchPlaceholder',
331
+ label: 'Search placeholder',
332
+ 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…".',
333
+ component: Aglyn.FieldComponentType.TEXT_FIELD,
334
+ // Meaningless while there is no search box to hint in.
335
+ condition: {
336
+ when: 'search',
337
+ is: true
338
+ }
339
+ },
340
+ // `searchIndex` and `searchTotal` are deliberately NOT attributes: both
341
+ // are server-stamped by expandCollectionEntries, like Category Pills'
342
+ // `items` and Related Posts' `entries`.
343
+ {
344
+ name: 'spacing',
345
+ label: 'Spacing',
346
+ description: 'Defines the space/gap between entries.',
347
+ component: Aglyn.FieldComponentType.TEXT_FIELD,
348
+ type: 'number'
349
+ }
350
+ ]
351
+ };
352
+ export { CollectionEntries };
353
+ // The collection family's palette presets ride this element's registry entry
354
+ // (`presets: 'collectionPresets'` in plugin.ts), as they did when the family
355
+ // was one module.
356
+ export { collectionPresets } from "./collection-presets.js";
357
+
358
+ //# sourceMappingURL=collection-entries.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../../../../../../libs/plugins/mui/src/lib/components/collection-entries.tsx"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport * as Aglyn from '@aglyn/aglyn'\nimport { mdiPostOutline } from '@aglyn/shared-data-mdi'\nimport MuiStack, { type StackProps } from '@mui/material/Stack'\nimport Typography from '@mui/material/Typography'\nimport type { ReactNode } from 'react'\nimport { Children, forwardRef, useContext, useState } from 'react'\nimport { BUNDLE_ID } from '../constants/bundle-common'\n// One coercion for every authored count in this bundle (AGL-1457) — number\n// fields round-trip as strings, and a second parser here would drift.\nimport { toCount } from '../utils/to-count'\nimport {\n SearchField,\n SuggestSearchBox,\n useEntryFuse,\n} from './collection-search-box'\n\n// Persisted component id (AGL-551/582); the compose pipeline references it\n// through the @aglyn/aglyn constant. Never rename.\nexport const ENTRIES_ID: Aglyn.ComponentId =\n Aglyn.COLLECTION_ENTRIES_COMPONENT_ID\n\nexport interface CollectionEntriesProps extends StackProps {\n /**\n * Collection to repeat over (compose-time, AGL-551). Blank = the\n * collection routed by the current URL on list-template screens.\n */\n collectionSlug?: string\n /** Maximum entries rendered (compose-time; blank = all, capped at 100). */\n entriesLimit?: number | string\n /**\n * Only entries in this category repeat (compose-time, AGL-582).\n * Matches the collection's category by stable id or display name.\n */\n filterCategory?: string\n /** Only entries carrying this tag repeat (compose-time, AGL-582). */\n filterTag?: string\n /**\n * Entries per page (compose-time, AGL-620). When set, the block renders one\n * page window instead of the top `entriesLimit`; the built-in collection\n * list uses this with the `page` from `/{collection}/page/{n}`.\n */\n perPage?: number | string\n /** 1-based page for `perPage` (compose-time, AGL-620). */\n page?: number | string\n /**\n * Render this block only on page 1 of the routed listing (compose-time,\n * AGL-1871). For a LEAD card — a block with no `perPage`, showing the top\n * of the set — every `/{collection}/page/{n}` past the first otherwise\n * repeats the identical entry above a page of different ones.\n *\n * Opt-in and default off: a block that legitimately belongs on every page\n * of a listing (a \"popular posts\" rail) has exactly the same shape.\n */\n firstPageOnly?: boolean\n /**\n * Show a search box that filters the RENDERED entries by title/excerpt as\n * the reader types (AGL-1516, Figma 494:1220). Opt-in and default off, so\n * every existing instance renders exactly as before.\n *\n * Client-evaluated over the entries this block already holds — the page is\n * ISR-cached, so a keystroke never costs a Firestore read. That set is\n * whatever the expansion left the block holding, which paging, an\n * `entriesLimit` or the 100-entry cap can each cut down. A truncated block\n * says so under its results either way — on a miss and on a hit\n * (AGL-1516, AGL-2569) — rather than pretending global search.\n */\n search?: boolean\n /**\n * What the search box DOES with a query (AGL-1525, Figma 496:1218).\n *\n * - `filter` (default, and the reading of an absent value) — hides the\n * non-matching cards in place. What AGL-1516 shipped.\n * - `suggest` — the card grid stays exactly as it was and a floating panel\n * of matching entries opens under the field, ending in a link to the\n * site-wide results page. The frame's toolbar behaviour: a reader\n * skimming the list is not made to lose it to a typo.\n */\n searchMode?: CollectionSearchMode\n /** Hint text inside the search box (blank = \"Search posts…\"). */\n searchPlaceholder?: string\n /**\n * Server-stamped matchable text per rendered entry\n * (`expandCollectionEntries`, AGL-1516); never set by hand.\n */\n searchIndex?: Aglyn.CollectionEntrySearchItem[]\n /**\n * How many entries `searchIndex` was drawn from — server-stamped by\n * `expandCollectionEntries` alongside it (AGL-1516), never set by hand.\n * `searchIndex.length < searchTotal` is the only honest test of whether\n * this block holds the whole set; `perPage` is not, because\n * `entriesLimit` and the 100-entry cap truncate too.\n */\n searchTotal?: number\n /**\n * Whether the read behind `searchTotal` reached its own bound — server-\n * stamped beside it (AGL-1516), never set by hand.\n *\n * `searchTotal` is a count of what the server SAW, and the collection read\n * is limited. Without this flag a block holding 100 of 400 posts satisfies\n * `searchIndex.length === searchTotal` and renders the one empty state that\n * claims to have looked everywhere.\n */\n searchCapped?: boolean\n}\n\n/** The two things the toolbar search box can do (AGL-1516/AGL-1525). */\nexport type CollectionSearchMode = 'filter' | 'suggest'\n\n/**\n * Author-facing choices for {@link CollectionEntriesProps.searchMode}.\n *\n * Both values are truthy on purpose (AGL-1453): `''` cannot survive a save,\n * so an author who tried the other mode would have no route back. \"Filter\"\n * carries the absent-value reading, which is what every block published\n * before AGL-1525 has.\n */\nexport const COLLECTION_SEARCH_MODE_OPTIONS: ReadonlyArray<{\n value: CollectionSearchMode\n label: string\n}> = [\n { value: 'filter', label: 'Filter the entries in place' },\n { value: 'suggest', label: 'Show a suggestions dropdown' },\n]\n\n/**\n * How much of the collection an entries block's search could actually see\n * (AGL-1516, AGL-2569) — `truncated` says the rendered window is smaller\n * than the set, `paginated` says the missing part lives on other pages of\n * this same list rather than merely outside the block.\n *\n * The test is whether the window is SMALLER THAN THE SET, which only the\n * server knows — `perPage` is the wrong proxy for it in both directions. A\n * block truncated by `entriesLimit` or by the 100-entry cap has no `perPage`\n * at all and would claim a global miss over 6 of 40 posts; a block whose\n * `perPage` exceeds its entry count is a single complete page and would\n * blame pages that do not exist. `searchTotal` is stamped with `searchIndex`,\n * so the two are always in step.\n *\n * Absent — a page cached before that stamp shipped — falls back to the old\n * `perPage` reading rather than guessing \"complete\": understating the scope\n * of a search is the safe direction to be wrong in.\n *\n * `searchCapped` is the second way a block can fail to hold the set, and the\n * invisible one: `searchTotal` counts the entries the server READ, and that\n * read is bounded. Past the bound `shown === searchTotal` is satisfied by a\n * block holding 100 of 400 posts, and the wording that claims to have looked\n * everywhere is exactly the branch it would take.\n *\n * Both the miss and the hit disclose off this one answer, so the two can\n * never disagree about the same block.\n */\nconst searchScope = ({\n perPage,\n shown,\n searchTotal,\n searchCapped,\n}: {\n perPage?: CollectionEntriesProps['perPage']\n /** Entries this block searched — the length of its stamped index. */\n shown: number\n searchTotal?: number\n searchCapped?: boolean\n}): { paginated: boolean; truncated: boolean } => {\n const paginated = (toCount(perPage, 0) ?? 0) > 0\n return {\n paginated,\n truncated:\n typeof searchTotal === 'number'\n ? shown < searchTotal || Boolean(searchCapped)\n : paginated || Boolean(searchCapped),\n }\n}\n\n/**\n * Repeats its children once per published entry of a content collection\n * (AGL-551) — the collections sibling of the dataset repeatable. The tenant\n * expands it at compose time with `{{entry.*}}` tokens; in the besigner the\n * template renders once with literal tokens, matching the repeatable UX.\n *\n * Search (AGL-1516): with `search` on, a toolbar search box filters the\n * rendered entry clones client-side against the server-stamped\n * `searchIndex` — fuzzy, via the same Fuse the icon picker uses. The clones\n * arrive as `children` in stamp order, one group of template roots per\n * entry, so group N of the children IS entry N of the index. Matches keep\n * the list's own order rather than Fuse's score order: a blog list is\n * chronological, and search narrows it, it does not reshuffle it. In the\n * besigner the field renders as an inert affordance (the template renders\n * once with literal tokens and there is no index to search), matching how\n * Category Pills go inert on editing surfaces.\n */\nconst CollectionEntries = forwardRef<HTMLDivElement, CollectionEntriesProps>(\n // collectionSlug/entriesLimit/filter*/search* are compose-time or\n // search-only attributes: strip so they never hit the DOM.\n (\n {\n collectionSlug,\n entriesLimit,\n filterCategory,\n filterTag,\n perPage,\n page,\n firstPageOnly,\n search,\n searchMode,\n searchPlaceholder,\n searchIndex,\n searchTotal,\n searchCapped,\n children,\n ...props\n },\n ref,\n ) => {\n const { suppressNavigation } = useContext(Aglyn.ScreenLinkContext)\n /** `filter` mode only — `suggest` owns its own query (AGL-1516). */\n const [query, setQuery] = useState('')\n const items = search ? searchIndex : undefined\n const { fuzzy, requestFuzzy } = useEntryFuse(items)\n if (!search) {\n return (\n <MuiStack ref={ref} spacing={4} {...props}>\n {children}\n </MuiStack>\n )\n }\n const childArray = Children.toArray(children)\n // Template roots per entry. Every entry clones the same template, so the\n // children divide evenly; anything else means the children are not the\n // stamped clones, and filtering blind would hide the wrong cards —\n // fail open and render everything.\n const groupSize =\n items?.length && childArray.length % items.length === 0\n ? childArray.length / items.length\n : 0\n // `suggest` draws its rows from the INDEX and never touches the clones,\n // so the clone-alignment precondition is a `filter` precondition only.\n // A block whose children do not divide evenly still suggests correctly;\n // requiring `groupSize` here would silently kill the panel on exactly\n // the templates that made the fail-open necessary.\n const mode: CollectionSearchMode =\n searchMode === 'suggest' ? 'suggest' : 'filter'\n // Whether there is an index to search, NOT whether the matcher has loaded:\n // the field has to be there for the keystroke that loads it.\n const live =\n !suppressNavigation &&\n Boolean(items?.length) &&\n (mode === 'suggest' || groupSize > 0)\n const trimmed = query.trim()\n let visible: ReactNode[] | ReactNode = children\n /**\n * The one line under the results that says what the search covered —\n * either a miss scoped to the set that came back empty, or a hit scoped\n * to the window it was drawn from. Never both.\n */\n let scopeNote: ReactNode = null\n if (mode === 'suggest') {\n // The grid is deliberately untouched (Figma 496:1218): the panel is an\n // overlay on a page the reader is still reading, not a filter.\n visible = children\n } else if (live && trimmed && fuzzy) {\n const matched = new Set(\n fuzzy.search(trimmed).map((result) => result.refIndex),\n )\n visible = childArray.filter((_, index) =>\n matched.has(Math.floor(index / groupSize)),\n )\n // HONEST scope (AGL-1516, AGL-2569): this block holds whatever window\n // the expansion gave it, and pretending the search was global would\n // turn every miss into a false \"this post does not exist\" — and every\n // hit into a result set that looks like the whole answer.\n const shown = items?.length ?? 0\n const { paginated, truncated } = searchScope({\n perPage,\n shown,\n searchTotal,\n searchCapped,\n })\n if (!(visible as ReactNode[]).length) {\n scopeNote = (\n <Typography variant=\"body2\" sx={{ color: 'text.secondary' }}>\n {!truncated\n ? `No matches for “${trimmed}”.`\n : paginated\n ? `No matches for “${trimmed}” on this page — other pages ` +\n 'are not searched.'\n : `No matches for “${trimmed}” in the ${shown} entries ` +\n 'shown here — the rest of the collection is not searched.'}\n </Typography>\n )\n } else if (truncated) {\n // The hit half of the same honesty (AGL-2569). Matches are drawn from\n // the same window the miss apologizes for, they carry no pager of\n // their own, and nothing else on screen says the pages behind this\n // one went unread — so a list that happens to be partial reads as\n // complete. A block that holds the whole collection says nothing\n // here: a disclaimer on a complete answer is noise, and every small\n // site would carry it.\n //\n // No route out to `/search?q=` (AGL-88): the suggestion panel links\n // there because it is the panel's only possible answer, but this\n // mode has a working list under the field and that page does not\n // exist yet. Trading a silent partial answer for a dead link is not\n // the trade.\n scopeNote = (\n <Typography variant=\"body2\" sx={{ color: 'text.secondary' }}>\n {paginated\n ? 'Showing matches on this page — other pages are not searched.'\n : `Showing matches in the ${shown} entries here — the rest of ` +\n 'the collection is not searched.'}\n </Typography>\n )\n }\n }\n // The field renders when there is something to search, and as an inert\n // affordance on editing surfaces so the author sees what they enabled.\n // A live surface with nothing stamped (unknown collection, zero entries)\n // renders no field at all — a search box over nothing is a lie.\n const showField = suppressNavigation || live\n const field = !showField ? null : mode === 'suggest' ? (\n // Rows in Fuse's SCORE order, the opposite of what `filter` does and\n // deliberately: filtering narrows a chronological feed, so reshuffling\n // it would be a second, unasked-for change to what the reader is\n // looking at. A suggestion list is not a feed — it is an answer to a\n // question, and the best answer belongs first.\n <SuggestSearchBox\n fuzzy={fuzzy}\n requestFuzzy={requestFuzzy}\n {...(items ? { items } : {})}\n {...(searchPlaceholder === undefined\n ? {}\n : { placeholder: searchPlaceholder })}\n {...(suppressNavigation ? { inert: true } : {})}\n // This block searched the entries IT rendered — a page window, an\n // `entriesLimit` slice, or a bounded read. Never \"this collection\".\n emptyText={(text) => `No matches for “${text}” on this page.`}\n />\n ) : (\n <SearchField\n value={query}\n landmark\n {...(searchPlaceholder === undefined ? {} : { placeholder: searchPlaceholder })}\n {...(suppressNavigation ? { inert: true } : {})}\n onQuery={(next) => {\n requestFuzzy()\n setQuery(next)\n }}\n />\n )\n return (\n <MuiStack ref={ref} spacing={4} {...props}>\n {field}\n {visible}\n {scopeNote}\n </MuiStack>\n )\n },\n)\nCollectionEntries.displayName = 'AglynCollectionEntries'\n\nexport const collectionEntriesSchema: Aglyn.ComponentSchema<CollectionEntriesProps> =\n {\n $id: ENTRIES_ID,\n pluginId: BUNDLE_ID,\n displayName: 'Collection Entries',\n description: 'Repeats its children once per entry in a content collection.',\n category: Aglyn.ComponentCategory.DATA_DISPLAY,\n icon: { path: mdiPostOutline.path, sx: { color: 'secondary.main' } },\n attributes: [\n {\n name: 'collectionSlug',\n label: 'Collection slug',\n description:\n 'Content collection whose published entries the children repeat ' +\n 'over (e.g. \"blog\"). Leave blank on a list-template screen to use ' +\n 'the collection from the URL.',\n component: Aglyn.FieldComponentType.TEXT_FIELD,\n },\n {\n name: 'entriesLimit',\n label: 'Entries limit',\n description: 'Maximum entries rendered (blank = all, capped at 100).',\n component: Aglyn.FieldComponentType.TEXT_FIELD,\n type: 'number',\n },\n {\n name: 'filterCategory',\n label: 'Filter by category',\n description:\n 'Only entries in this category repeat — the category name or its ' +\n 'stable id both match (e.g. \"Guides\"). Blank = no category filter.',\n component: Aglyn.FieldComponentType.TEXT_FIELD,\n },\n {\n name: 'filterTag',\n label: 'Filter by tag',\n description:\n 'Only entries carrying this tag repeat (e.g. \"nextjs\"). Blank = ' +\n 'no tag filter.',\n component: Aglyn.FieldComponentType.TEXT_FIELD,\n },\n {\n name: 'perPage',\n label: 'Entries per page',\n description:\n 'Paginate the list: entries per page (blank = no pagination). ' +\n 'Pairs with the page from /{collection}/page/{n}.',\n component: Aglyn.FieldComponentType.TEXT_FIELD,\n type: 'number',\n },\n {\n name: 'page',\n label: 'Page',\n description: '1-based page to render when Entries per page is set.',\n component: Aglyn.FieldComponentType.TEXT_FIELD,\n type: 'number',\n },\n {\n name: 'firstPageOnly',\n label: 'Only on page 1',\n description:\n 'Show this block on the first page of the listing only. Use it ' +\n 'for a featured or lead card: without it the same entry repeats ' +\n 'at the top of every /page/{n}, above a page of different ones.',\n component: Aglyn.FieldComponentType.SWITCH,\n },\n {\n name: 'search',\n label: 'Search',\n description:\n 'Show a search box that filters the rendered entries by title ' +\n 'and excerpt as the reader types. It searches the entries this ' +\n 'block rendered — whatever paging, an entry limit or the ' +\n '100-entry cap left it holding, and it says so on a miss.',\n component: Aglyn.FieldComponentType.SWITCH,\n },\n {\n name: 'searchMode',\n label: 'When the reader types',\n description:\n 'Filter the entries in place, or leave the list alone and open a ' +\n 'dropdown of matching entries under the box — each one a link, ' +\n 'ending in \"View all results\" for a search across the whole site.',\n component: Aglyn.FieldComponentType.SELECT,\n options: COLLECTION_SEARCH_MODE_OPTIONS.map((option) => ({\n ...option,\n })),\n // Meaningless while there is no search box to type in.\n condition: { when: 'search', is: true },\n },\n {\n name: 'searchPlaceholder',\n label: 'Search placeholder',\n description:\n 'Hint text inside this block’s own search box — the one that ' +\n 'filters the entries below it as a reader types. Blank shows ' +\n '\"Search posts…\".',\n component: Aglyn.FieldComponentType.TEXT_FIELD,\n // Meaningless while there is no search box to hint in.\n condition: { when: 'search', is: true },\n },\n // `searchIndex` and `searchTotal` are deliberately NOT attributes: both\n // are server-stamped by expandCollectionEntries, like Category Pills'\n // `items` and Related Posts' `entries`.\n {\n name: 'spacing',\n label: 'Spacing',\n description: 'Defines the space/gap between entries.',\n component: Aglyn.FieldComponentType.TEXT_FIELD,\n type: 'number',\n },\n ],\n }\n\nexport { CollectionEntries }\n// The collection family's palette presets ride this element's registry entry\n// (`presets: 'collectionPresets'` in plugin.ts), as they did when the family\n// was one module.\nexport { collectionPresets } from './collection-presets'\n"],"names":["Aglyn","mdiPostOutline","MuiStack","Typography","Children","forwardRef","useContext","useState","BUNDLE_ID","toCount","SearchField","SuggestSearchBox","useEntryFuse","ENTRIES_ID","COLLECTION_ENTRIES_COMPONENT_ID","COLLECTION_SEARCH_MODE_OPTIONS","value","label","searchScope","perPage","shown","searchTotal","searchCapped","paginated","truncated","Boolean","CollectionEntries","collectionSlug","entriesLimit","filterCategory","filterTag","page","firstPageOnly","search","searchMode","searchPlaceholder","searchIndex","children","props","ref","suppressNavigation","ScreenLinkContext","query","setQuery","items","undefined","fuzzy","requestFuzzy","spacing","childArray","toArray","groupSize","length","mode","live","trimmed","trim","visible","scopeNote","matched","Set","map","result","refIndex","filter","_","index","has","Math","floor","variant","sx","color","showField","field","placeholder","inert","emptyText","text","landmark","onQuery","next","displayName","collectionEntriesSchema","$id","pluginId","description","category","ComponentCategory","DATA_DISPLAY","icon","path","attributes","name","component","FieldComponentType","TEXT_FIELD","type","SWITCH","SELECT","options","option","condition","when","is","collectionPresets"],"mappings":"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"}
@@ -0,0 +1,81 @@
1
+ /**
2
+ * @license
3
+ * Copyright 2026 Aglyn LLC
4
+ *
5
+ * Licensed under the Apache License, Version 2.0 (the "License");
6
+ * you may not use this file except in compliance with the License.
7
+ * You may obtain a copy of the License at
8
+ *
9
+ * http://www.apache.org/licenses/LICENSE-2.0
10
+ *
11
+ * Unless required by applicable law or agreed to in writing, software
12
+ * distributed under the License is distributed on an "AS IS" BASIS,
13
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
+ * See the License for the specific language governing permissions and
15
+ * limitations under the License.
16
+ */
17
+ import * as Aglyn from '@aglyn/aglyn';
18
+ import { type StackProps } from '@mui/material/Stack';
19
+ export declare const ENTRY_AUTHOR_ID: Aglyn.ComponentId;
20
+ export interface CollectionEntryAuthorProps extends StackProps {
21
+ /**
22
+ * The byline. Server-filled from the routed entry's author record on entry
23
+ * templates (`expandCollectionEntryAuthor`), exactly like Entry Meta's
24
+ * fields; set it — or bind `{{entry.author}}` — only to override.
25
+ */
26
+ name?: string;
27
+ /**
28
+ * The author's blurb, from their record's `bio`; bind
29
+ * `{{entry.authorBio}}` to override.
30
+ */
31
+ bio?: string;
32
+ /**
33
+ * Portrait or logo — a media reference (AGL-1215) or any URL. Server-filled
34
+ * from the author record, so the face changes with the byline instead of
35
+ * being picked once on the template the way Entry Meta's avatar is.
36
+ */
37
+ image?: string;
38
+ /** The author's own site; offered as a link row beside the profile ones. */
39
+ url?: string;
40
+ /**
41
+ * The author's page on THIS site (AGL-2519), server-filled from the routed
42
+ * entry. The name links here.
43
+ *
44
+ * It wins over {@link url} for the NAME, and the ordering is the point: a
45
+ * reader who clicks a byline at the end of an article is asking "what else
46
+ * has this person written here", not "take me off this site". Their own
47
+ * url is still reachable — it is one of the link rows — so nothing is lost
48
+ * by giving the name the destination that answers the question asked.
49
+ */
50
+ pageUrl?: string;
51
+ /**
52
+ * The author's profile links, server-filled from their record (AGL-2516).
53
+ *
54
+ * Not authorable as text: each row carries a platform or a picked icon, and
55
+ * the console's author editor is where those are chosen. `Show links` is
56
+ * the template's control over them.
57
+ */
58
+ links?: Aglyn.ContentAuthorLink[];
59
+ showBio?: boolean;
60
+ showAvatar?: boolean;
61
+ showLinks?: boolean;
62
+ }
63
+ /**
64
+ * The author card that closes an article (AGL-2486) — portrait, byline, bio.
65
+ *
66
+ * Entry Meta prints a NAME beside a date, which is all an entry carried when
67
+ * it was written: one free-typed string. Custom authors made the byline a
68
+ * record with a portrait, a bio and a url, and a template that wanted the
69
+ * card the article frame draws still had to type the name and the blurb in as
70
+ * literal text — text that keeps saying whatever it said under posts somebody
71
+ * else wrote, and that no edit to the author record can ever reach. This is
72
+ * that card, filled from the record.
73
+ *
74
+ * Every field is independently overridable and every one of them collapses
75
+ * when empty: an author with no portrait renders the text alone rather than a
76
+ * gap where a face would be, and an entry with no author at all renders
77
+ * nothing — never an empty bordered box.
78
+ */
79
+ declare const CollectionEntryAuthor: import("react").ForwardRefExoticComponent<Omit<CollectionEntryAuthorProps, "ref"> & import("react").RefAttributes<HTMLDivElement>>;
80
+ export declare const collectionEntryAuthorSchema: Aglyn.ComponentSchema<CollectionEntryAuthorProps>;
81
+ export { CollectionEntryAuthor };