@aglyn/plugins-mui 1.0.0-beta.221 → 1.0.0-beta.222

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.
@@ -1,6 +1,6 @@
1
1
  import { _ as _extends } from "@swc/helpers/_/_extends";
2
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";
3
+ import { jsx as _jsx } from "react/jsx-runtime";
4
4
  /**
5
5
  * @license
6
6
  * Copyright 2026 Aglyn LLC
@@ -19,13 +19,8 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
19
19
  */ import * as Aglyn from "@aglyn/aglyn";
20
20
  import { mdiPostOutline } from "@aglyn/shared-data-mdi";
21
21
  import MuiStack from "@mui/material/Stack";
22
- import Typography from "@mui/material/Typography";
23
- import { Children, forwardRef, useContext, useState } from "react";
22
+ import { forwardRef, use } from "react";
24
23
  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
24
  // Persisted component id (AGL-551/582); the compose pipeline references it
30
25
  // through the @aglyn/aglyn constant. Never rename.
31
26
  export const ENTRIES_ID = Aglyn.COLLECTION_ENTRIES_COMPONENT_ID;
@@ -47,62 +42,75 @@ export const ENTRIES_ID = Aglyn.COLLECTION_ENTRIES_COMPONENT_ID;
47
42
  }
48
43
  ];
49
44
  /**
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.
45
+ * What a searching block stamps on its root. The browser looks for it before
46
+ * the page hydrates (`prepareEntriesSearch`); nothing styles or targets it.
47
+ */ export const ENTRIES_SEARCH_ATTRIBUTE = 'data-aglyn-entries-search';
48
+ let searchLoad;
49
+ /**
50
+ * The searching variant, fetched once per visit however many blocks ask. A
51
+ * failed fetch (a deploy mid-visit) is forgotten rather than cached, so the
52
+ * next render that asks tries again.
53
+ */ export const loadEntriesSearch = ()=>{
54
+ if (searchLoad) return searchLoad;
55
+ const load = import("./collection-entries-search.js");
56
+ searchLoad = load;
57
+ load.then((module)=>{
58
+ load.status = 'fulfilled';
59
+ load.value = module;
60
+ }, (reason)=>{
61
+ load.status = 'rejected';
62
+ load.reason = reason;
63
+ if (searchLoad === load) searchLoad = undefined;
64
+ });
65
+ return load;
66
+ };
67
+ /**
68
+ * Loads the searching variant BEFORE the element renders, wherever a render
69
+ * that waited for it would cost something (AGL-3438). The plugin calls it as
70
+ * the element registers (`prepare` in `plugin.ts`), and awaits it.
66
71
  *
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
+ * - **On the server, always.** A render that suspended there would hold the
73
+ * whole streamed shell for the fetch — and a first, cold one would do it
74
+ * on a page the ISR cache then keeps.
75
+ * - **Where the whole library is loading** (`everything`): the console and
76
+ * the besigner, where an author can turn search on at any moment.
77
+ * - **In a browser about to hydrate a searching block**, which the server
78
+ * HTML says by carrying {@link ENTRIES_SEARCH_ATTRIBUTE}. The fetch then
79
+ * rides along with the element's own instead of starting when hydration
80
+ * reaches the block.
72
81
  *
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
- };
82
+ * Anywhere else nothing is fetched: a page whose blocks have search off —
83
+ * most of them — never downloads the box. A searching block that renders
84
+ * without it (a client-side navigation onto one) suspends until it lands,
85
+ * which holds the navigation, never a half-drawn list.
86
+ */ export const prepareEntriesSearch = (everything)=>everything || typeof document === 'undefined' || document.querySelector(`[${ENTRIES_SEARCH_ATTRIBUTE}]`) ? loadEntriesSearch() : undefined;
83
87
  /**
84
88
  * Repeats its children once per published entry of a content collection
85
89
  * (AGL-551) — the collections sibling of the dataset repeatable. The tenant
86
90
  * expands it at compose time with `{{entry.*}}` tokens; in the besigner the
87
91
  * template renders once with literal tokens, matching the repeatable UX.
88
92
  *
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, [
93
+ * With `search` on (AGL-1516) the block is `SearchingEntries`, from its own
94
+ * module (AGL-3438). Off — the default, and most blocks — it is the plain
95
+ * stack below, and the search box's MUI input never reaches the page.
96
+ *
97
+ * There is NO Suspense boundary around the searching variant, on purpose. A
98
+ * boundary would have to be in the server's tree too, and a client render
99
+ * that suspended inside it would show its fallback: a list without its box,
100
+ * then the box pushing it down. Without one, a render that has to wait holds
101
+ * whatever is above it — the hydration of the page (whose server HTML stays
102
+ * exactly as it is), or a navigation (whose old page stays up) — and
103
+ * `prepareEntriesSearch` makes sure the published page almost never has to.
104
+ */ const CollectionEntries = /*#__PURE__*/ forwardRef((props, ref)=>{
105
+ if (props.search) {
106
+ const { SearchingEntries } = use(loadEntriesSearch());
107
+ return /*#__PURE__*/ _jsx(SearchingEntries, _extends({
108
+ ref: ref
109
+ }, props));
110
+ }
111
+ // collectionSlug/entriesLimit/filter*/search* are compose-time or
112
+ // search-only attributes: strip so they never hit the DOM.
113
+ const { collectionSlug, entriesLimit, filterCategory, filterTag, perPage, page, firstPageOnly, search, searchMode, searchPlaceholder, searchIndex, searchTotal, searchCapped, children } = props, rest = _object_without_properties_loose(props, [
106
114
  "collectionSlug",
107
115
  "entriesLimit",
108
116
  "filterCategory",
@@ -117,136 +125,12 @@ export const ENTRIES_ID = Aglyn.COLLECTION_ENTRIES_COMPONENT_ID;
117
125
  "searchTotal",
118
126
  "searchCapped",
119
127
  "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({
128
+ ]);
129
+ return /*#__PURE__*/ _jsx(MuiStack, _extends({
242
130
  ref: ref,
243
131
  spacing: 4
244
- }, props, {
245
- children: [
246
- field,
247
- visible,
248
- scopeNote
249
- ]
132
+ }, rest, {
133
+ children: children
250
134
  }));
251
135
  });
252
136
  CollectionEntries.displayName = 'AglynCollectionEntries';
@@ -1 +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 page 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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"}
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 { forwardRef, use } from 'react'\nimport { BUNDLE_ID } from '../constants/bundle-common'\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/** The searching variant's module (AGL-3438). */\ntype EntriesSearchModule = typeof import('./collection-entries-search')\n\n/**\n * What a searching block stamps on its root. The browser looks for it before\n * the page hydrates (`prepareEntriesSearch`); nothing styles or targets it.\n */\nexport const ENTRIES_SEARCH_ATTRIBUTE = 'data-aglyn-entries-search'\n\n/**\n * React's thenable contract (`status`/`value`), stamped on the load once it\n * settles — the plugin loader's ensure promise carries it for the same reason\n * (AGL-1541): `use()` unwraps a stamped promise synchronously, and suspends on\n * a bare one even after it has resolved.\n */\ntype StampedLoad = Promise<EntriesSearchModule> & {\n status?: 'pending' | 'fulfilled' | 'rejected'\n value?: EntriesSearchModule\n reason?: unknown\n}\n\nlet searchLoad: StampedLoad | undefined\n\n/**\n * The searching variant, fetched once per visit however many blocks ask. A\n * failed fetch (a deploy mid-visit) is forgotten rather than cached, so the\n * next render that asks tries again.\n */\nexport const loadEntriesSearch = (): Promise<EntriesSearchModule> => {\n if (searchLoad) return searchLoad\n const load: StampedLoad = import('./collection-entries-search')\n searchLoad = load\n load.then(\n (module) => {\n load.status = 'fulfilled'\n load.value = module\n },\n (reason: unknown) => {\n load.status = 'rejected'\n load.reason = reason\n if (searchLoad === load) searchLoad = undefined\n },\n )\n return load\n}\n\n/**\n * Loads the searching variant BEFORE the element renders, wherever a render\n * that waited for it would cost something (AGL-3438). The plugin calls it as\n * the element registers (`prepare` in `plugin.ts`), and awaits it.\n *\n * - **On the server, always.** A render that suspended there would hold the\n * whole streamed shell for the fetch — and a first, cold one would do it\n * on a page the ISR cache then keeps.\n * - **Where the whole library is loading** (`everything`): the console and\n * the besigner, where an author can turn search on at any moment.\n * - **In a browser about to hydrate a searching block**, which the server\n * HTML says by carrying {@link ENTRIES_SEARCH_ATTRIBUTE}. The fetch then\n * rides along with the element's own instead of starting when hydration\n * reaches the block.\n *\n * Anywhere else nothing is fetched: a page whose blocks have search off —\n * most of them — never downloads the box. A searching block that renders\n * without it (a client-side navigation onto one) suspends until it lands,\n * which holds the navigation, never a half-drawn list.\n */\nexport const prepareEntriesSearch = (\n everything: boolean,\n): Promise<unknown> | undefined =>\n everything ||\n typeof document === 'undefined' ||\n document.querySelector(`[${ENTRIES_SEARCH_ATTRIBUTE}]`)\n ? loadEntriesSearch()\n : undefined\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 * With `search` on (AGL-1516) the block is `SearchingEntries`, from its own\n * module (AGL-3438). Off — the default, and most blocks — it is the plain\n * stack below, and the search box's MUI input never reaches the page.\n *\n * There is NO Suspense boundary around the searching variant, on purpose. A\n * boundary would have to be in the server's tree too, and a client render\n * that suspended inside it would show its fallback: a list without its box,\n * then the box pushing it down. Without one, a render that has to wait holds\n * whatever is above it — the hydration of the page (whose server HTML stays\n * exactly as it is), or a navigation (whose old page stays up) — and\n * `prepareEntriesSearch` makes sure the published page almost never has to.\n */\nconst CollectionEntries = forwardRef<HTMLDivElement, CollectionEntriesProps>(\n (props, ref) => {\n if (props.search) {\n const { SearchingEntries } = use(loadEntriesSearch())\n return <SearchingEntries ref={ref} {...props} />\n }\n // collectionSlug/entriesLimit/filter*/search* are compose-time or\n // search-only attributes: strip so they never hit the DOM.\n const {\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 ...rest\n } = props\n return (\n <MuiStack ref={ref} spacing={4} {...rest}>\n {children}\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 page 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","forwardRef","use","BUNDLE_ID","ENTRIES_ID","COLLECTION_ENTRIES_COMPONENT_ID","COLLECTION_SEARCH_MODE_OPTIONS","value","label","ENTRIES_SEARCH_ATTRIBUTE","searchLoad","loadEntriesSearch","load","then","module","status","reason","undefined","prepareEntriesSearch","everything","document","querySelector","CollectionEntries","props","ref","search","SearchingEntries","collectionSlug","entriesLimit","filterCategory","filterTag","perPage","page","firstPageOnly","searchMode","searchPlaceholder","searchIndex","searchTotal","searchCapped","children","rest","spacing","displayName","collectionEntriesSchema","$id","pluginId","description","category","ComponentCategory","DATA_DISPLAY","icon","path","sx","color","attributes","name","component","FieldComponentType","TEXT_FIELD","type","SWITCH","SELECT","options","map","option","condition","when","is","collectionPresets"],"mappings":"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"}
@@ -31,6 +31,7 @@ export interface CollectionEntryBodyProps {
31
31
  intrinsicSizes?: Record<string, {
32
32
  width: number;
33
33
  height: number;
34
+ version?: string;
34
35
  }>;
35
36
  }
36
37
  /**
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../../../../../libs/plugins/mui/src/lib/components/collection-entry-body.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 { mdiTextLong } from '@aglyn/shared-data-mdi'\nimport { AppLink } from '@aglyn/shared-ui-jsx'\nimport Box from '@mui/material/Box'\nimport MuiLink from '@mui/material/Link'\nimport Typography from '@mui/material/Typography'\nimport type { ReactNode } from 'react'\nimport { forwardRef, useContext, useMemo } from 'react'\nimport { BUNDLE_ID } from '../constants/bundle-common'\nimport { UNRESOLVED_TOKEN } from './collection-common'\n\n// Persisted component id (AGL-551/582); the compose pipeline references it\n// through the @aglyn/aglyn constant. Never rename.\nexport const ENTRY_BODY_ID: Aglyn.ComponentId =\n Aglyn.COLLECTION_ENTRY_BODY_COMPONENT_ID\n\nexport interface CollectionEntryBodyProps {\n /**\n * Markdown-lite source. On entry-template screens `{{entry.body}}`\n * resolves to the rendered entry's body at compose time.\n */\n markdown?: string\n /**\n * The pixel pair of each library image this body names, keyed by the target\n * as the body writes it, stamped by the composition from the assets' DAM\n * records (AGL-3149) — never set by hand, and never on the template, where\n * `markdown` is still the `{{entry.body}}` token and names no asset.\n */\n intrinsicSizes?: Record<string, { width: number; height: number }>\n}\n\nconst renderInlines = (\n inlines: Aglyn.MarkdownInline[],\n links: { suppressNavigation?: boolean; screens?: Aglyn.ScreenRouteMap },\n): ReactNode[] =>\n inlines.map((item, index) => {\n if (item.type === 'bold') return <strong key={index}>{item.text}</strong>\n if (item.type === 'italic') return <em key={index}>{item.text}</em>\n if (item.type !== 'link') return <span key={index}>{item.text}</span>\n // Internal paths route through AppLink for client-side navigation\n // (AGL-582); external links stay plain anchors. Editing surfaces render\n // the link look without an href so clicks never navigate. A target named\n // by REFERENCE resolves against the routing map first (AGL-3118), and one\n // whose entry, listing or screen is gone renders as the words alone —\n // never as an anchor holding the stored value.\n const link = Aglyn.resolveMarkdownLink(item.href, links)\n if (link.kind === 'text') return <span key={index}>{item.text}</span>\n if (link.kind === 'inert') {\n return (\n <MuiLink key={index} component=\"span\" sx={{ cursor: 'default' }}>\n {item.text}\n </MuiLink>\n )\n }\n return link.kind === 'internal' ? (\n <AppLink key={index} href={link.href}>\n {item.text}\n </AppLink>\n ) : (\n <MuiLink key={index} href={link.href}>\n {item.text}\n </MuiLink>\n )\n })\n\n/**\n * Renders a content entry's markdown-lite body as themed MUI elements\n * (AGL-551): headings, paragraphs, lists, and images pick up the site\n * theme's typography instead of the old unthemed article HTML. Parsing is\n * pure, so the full body server-renders (SEO keeps the article text).\n */\nconst CollectionEntryBody = forwardRef<\n HTMLDivElement,\n CollectionEntryBodyProps\n>((props, ref) => {\n // `intrinsicSizes` is destructured out rather than left in `rest`: `rest` is\n // spread onto a Box, and a composition-stamped object would reach the DOM as\n // an attribute React has no idea what to do with.\n const { markdown, intrinsicSizes, ...rest } = props\n // Node styles ride the renderer-merged sx; recompose (stack.ts pattern).\n const nodeSx = Array.isArray(props['sx']) ? props['sx'] : [props['sx']]\n // The routing map travels with the suppression flag: both decide what a\n // link in this body renders as (AGL-3118).\n const { suppressNavigation, screens } = useContext(Aglyn.ScreenLinkContext)\n const links = useMemo(\n () => ({ suppressNavigation, screens }),\n [suppressNavigation, screens],\n )\n // The site being rendered, for image blocks (AGL-1686) — read once here\n // because hooks cannot run inside the block map below.\n const { hostId } = Aglyn.useSite()\n const source = (markdown ?? '').trim()\n const unresolved = !source || UNRESOLVED_TOKEN.test(source)\n const blocks = useMemo(\n () => (unresolved ? [] : Aglyn.parseMarkdownLite(source)),\n [source, unresolved],\n )\n // Heading anchors, on the same terms as the Markdown element (AGL-1162).\n // Without them a blog article could not carry an \"On this page\" aside at\n // all: the Table of Contents element emits `#slug` links, and there was\n // nothing on this page for them to land on.\n const slugs = useMemo(() => Aglyn.markdownHeadingSlugs(blocks), [blocks])\n if (unresolved) {\n // Editing surfaces get an affordance; the published site renders\n // nothing rather than a literal token.\n if (!suppressNavigation) return <Box ref={ref} {...rest} />\n return (\n <Box\n ref={ref}\n {...rest}\n sx={[\n {\n p: 2,\n border: '1px dashed',\n borderColor: 'divider',\n color: 'text.secondary',\n fontSize: 12,\n fontFamily: 'system-ui, sans-serif',\n },\n ...nodeSx,\n ]}\n >\n {'Entry body — the {{entry.body}} markdown renders here'}\n </Box>\n )\n }\n return (\n <Box ref={ref} {...rest}>\n {blocks.map((block, index) => {\n if (block.type === 'heading') {\n return (\n <Typography\n key={index}\n id={slugs[index]}\n variant={block.level === 2 ? 'h4' : 'h5'}\n component={block.level === 2 ? 'h2' : 'h3'}\n gutterBottom\n sx={{\n scrollMarginTop: `${Aglyn.HEADING_ANCHOR_SCROLL_MARGIN}px`,\n }}\n >\n {renderInlines(block.inlines, links)}\n </Typography>\n )\n }\n if (block.type === 'image') {\n return (\n <Box\n key={index}\n component=\"img\"\n // Resolved like the related cover image in collection-related.tsx\n // (AGL-1686): a body image is the same kind of asset and\n // has no reason to be stored in a more fragile form.\n //\n // And sized like one too, as of AGL-3149: the same call gives\n // the Markdown element's body images their candidate list and\n // intrinsic pair, so the two bodies cannot disagree about how a\n // picture in prose is delivered.\n {...Aglyn.mediaBodyImageAttributes({\n src: block.src,\n hostId,\n size: intrinsicSizes?.[block.src],\n })}\n alt={block.alt}\n // Paired with the `width`/`height` above — see the Markdown\n // element's copy of this: without it the height hint outlives\n // the `maxWidth` cap and squashes the picture.\n sx={{ maxWidth: '100%', height: 'auto', borderRadius: 1, my: 1 }}\n // An image inside an entry body is below the fold by\n // construction — the title, byline and opening paragraphs are\n // above it. This one carried no loading hint at all, so it was\n // fetched EAGERLY, at default priority, competing with the\n // entry's own cover (AGL-2486).\n {...Aglyn.DEFERRED_IMAGE_ATTRIBUTES}\n />\n )\n }\n if (block.type === 'list') {\n return (\n <Box key={index} component=\"ul\" sx={{ lineHeight: 1.7, pl: 3 }}>\n {block.items.map((item, itemIndex) => (\n <li key={itemIndex}>\n {renderInlines(item, links)}\n </li>\n ))}\n </Box>\n )\n }\n // A numbered list (AGL-1320) — a real `<ol>`, so the markers are the\n // browser's and `start` survives: a notice that resumes at 7 has to\n // read as 7 for the enumeration to mean anything.\n if (block.type === 'orderedList') {\n return (\n <Box\n key={index}\n component=\"ol\"\n start={block.start}\n sx={{ lineHeight: 1.7, pl: 3 }}\n >\n {block.items.map((item, itemIndex) => (\n <li key={itemIndex}>\n {renderInlines(item, links)}\n </li>\n ))}\n </Box>\n )\n }\n // Code blocks and tables (AGL-974). Both scroll rather than wrap:\n // a wrapped code line or a squeezed table column loses the very\n // structure that made the block worth writing.\n if (block.type === 'code') {\n return (\n <Box\n key={index}\n component=\"pre\"\n sx={{\n my: 2,\n p: 2,\n overflowX: 'auto',\n borderRadius: 1,\n bgcolor: 'action.hover',\n fontFamily: 'monospace',\n fontSize: 14,\n }}\n >\n <code>{block.text}</code>\n </Box>\n )\n }\n if (block.type === 'table') {\n return (\n <Box key={index} sx={{ my: 2, overflowX: 'auto' }}>\n <Box\n component=\"table\"\n sx={{\n borderCollapse: 'collapse',\n width: '100%',\n '& th, & td': {\n border: '1px solid',\n borderColor: 'divider',\n px: 1.5,\n py: 1,\n },\n '& th': { bgcolor: 'action.hover' },\n }}\n >\n <thead>\n <tr>\n {block.header.map((cell, cellIndex) => (\n <th\n key={cellIndex}\n style={{ textAlign: block.align[cellIndex] ?? 'left' }}\n >\n {renderInlines(cell, links)}\n </th>\n ))}\n </tr>\n </thead>\n <tbody>\n {block.rows.map((row, rowIndex) => (\n <tr key={rowIndex}>\n {row.map((cell, cellIndex) => (\n <td\n key={cellIndex}\n style={{\n textAlign: block.align[cellIndex] ?? 'left',\n }}\n >\n {renderInlines(cell, links)}\n </td>\n ))}\n </tr>\n ))}\n </tbody>\n </Box>\n </Box>\n )\n }\n // The article template's pull-quote (AGL-1315, Figma 170:225):\n // larger italic prose behind a left accent. Palette tokens only, so\n // it follows the site theme in both modes.\n if (block.type === 'quote') {\n return (\n <Typography\n key={index}\n component=\"blockquote\"\n sx={{\n my: 3,\n mx: 0,\n pl: 2.5,\n borderLeft: '3px solid',\n borderColor: 'primary.main',\n fontStyle: 'italic',\n fontSize: '1.25em',\n lineHeight: 1.6,\n color: 'text.primary',\n }}\n >\n {renderInlines(block.inlines, links)}\n </Typography>\n )\n }\n return (\n <Typography\n key={index}\n variant=\"body1\"\n sx={{ lineHeight: 1.7 }}\n gutterBottom\n >\n {renderInlines(block.inlines, links)}\n </Typography>\n )\n })}\n </Box>\n )\n})\nCollectionEntryBody.displayName = 'AglynCollectionEntryBody'\n\nexport const collectionEntryBodySchema: Aglyn.ComponentSchema<CollectionEntryBodyProps> =\n {\n $id: ENTRY_BODY_ID,\n pluginId: BUNDLE_ID,\n displayName: 'Entry Body',\n description: \"The current entry's markdown body, on an entry template.\",\n category: Aglyn.ComponentCategory.TEXT,\n icon: { path: mdiTextLong.path, sx: { color: 'secondary.main' } },\n flags: { selfClosing: Aglyn.FEATURE_FLAG.ENABLED },\n attributes: [\n {\n name: 'markdown',\n label: 'Markdown',\n description:\n 'Markdown-lite content. Keep {{entry.body}} on entry-template ' +\n \"pages so each entry's body renders here.\",\n component: Aglyn.FieldComponentType.TEXT_FIELD,\n },\n ],\n }\n\nexport { CollectionEntryBody }\n"],"names":["Aglyn","mdiTextLong","AppLink","Box","MuiLink","Typography","forwardRef","useContext","useMemo","BUNDLE_ID","UNRESOLVED_TOKEN","ENTRY_BODY_ID","COLLECTION_ENTRY_BODY_COMPONENT_ID","renderInlines","inlines","links","map","item","index","type","strong","text","em","span","link","resolveMarkdownLink","href","kind","component","sx","cursor","CollectionEntryBody","props","ref","markdown","intrinsicSizes","rest","nodeSx","Array","isArray","suppressNavigation","screens","ScreenLinkContext","hostId","useSite","source","trim","unresolved","test","blocks","parseMarkdownLite","slugs","markdownHeadingSlugs","p","border","borderColor","color","fontSize","fontFamily","block","id","variant","level","gutterBottom","scrollMarginTop","HEADING_ANCHOR_SCROLL_MARGIN","mediaBodyImageAttributes","src","size","alt","maxWidth","height","borderRadius","my","DEFERRED_IMAGE_ATTRIBUTES","lineHeight","pl","items","itemIndex","li","start","overflowX","bgcolor","code","borderCollapse","width","px","py","thead","tr","header","cell","cellIndex","th","style","textAlign","align","tbody","rows","row","rowIndex","td","mx","borderLeft","fontStyle","displayName","collectionEntryBodySchema","$id","pluginId","description","category","ComponentCategory","TEXT","icon","path","flags","selfClosing","FEATURE_FLAG","ENABLED","attributes","name","label","FieldComponentType","TEXT_FIELD"],"mappings":"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"}
1
+ {"version":3,"sources":["../../../../../../../libs/plugins/mui/src/lib/components/collection-entry-body.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 { mdiTextLong } from '@aglyn/shared-data-mdi'\nimport { AppLink } from '@aglyn/shared-ui-jsx'\nimport Box from '@mui/material/Box'\nimport MuiLink from '@mui/material/Link'\nimport Typography from '@mui/material/Typography'\nimport type { ReactNode } from 'react'\nimport { forwardRef, useContext, useMemo } from 'react'\nimport { BUNDLE_ID } from '../constants/bundle-common'\nimport { UNRESOLVED_TOKEN } from './collection-common'\n\n// Persisted component id (AGL-551/582); the compose pipeline references it\n// through the @aglyn/aglyn constant. Never rename.\nexport const ENTRY_BODY_ID: Aglyn.ComponentId =\n Aglyn.COLLECTION_ENTRY_BODY_COMPONENT_ID\n\nexport interface CollectionEntryBodyProps {\n /**\n * Markdown-lite source. On entry-template screens `{{entry.body}}`\n * resolves to the rendered entry's body at compose time.\n */\n markdown?: string\n /**\n * The pixel pair of each library image this body names, keyed by the target\n * as the body writes it, stamped by the composition from the assets' DAM\n * records (AGL-3149) — never set by hand, and never on the template, where\n * `markdown` is still the `{{entry.body}}` token and names no asset.\n */\n intrinsicSizes?: Record<\n string,\n { width: number; height: number; version?: string }\n >\n}\n\nconst renderInlines = (\n inlines: Aglyn.MarkdownInline[],\n links: { suppressNavigation?: boolean; screens?: Aglyn.ScreenRouteMap },\n): ReactNode[] =>\n inlines.map((item, index) => {\n if (item.type === 'bold') return <strong key={index}>{item.text}</strong>\n if (item.type === 'italic') return <em key={index}>{item.text}</em>\n if (item.type !== 'link') return <span key={index}>{item.text}</span>\n // Internal paths route through AppLink for client-side navigation\n // (AGL-582); external links stay plain anchors. Editing surfaces render\n // the link look without an href so clicks never navigate. A target named\n // by REFERENCE resolves against the routing map first (AGL-3118), and one\n // whose entry, listing or screen is gone renders as the words alone —\n // never as an anchor holding the stored value.\n const link = Aglyn.resolveMarkdownLink(item.href, links)\n if (link.kind === 'text') return <span key={index}>{item.text}</span>\n if (link.kind === 'inert') {\n return (\n <MuiLink key={index} component=\"span\" sx={{ cursor: 'default' }}>\n {item.text}\n </MuiLink>\n )\n }\n return link.kind === 'internal' ? (\n <AppLink key={index} href={link.href}>\n {item.text}\n </AppLink>\n ) : (\n <MuiLink key={index} href={link.href}>\n {item.text}\n </MuiLink>\n )\n })\n\n/**\n * Renders a content entry's markdown-lite body as themed MUI elements\n * (AGL-551): headings, paragraphs, lists, and images pick up the site\n * theme's typography instead of the old unthemed article HTML. Parsing is\n * pure, so the full body server-renders (SEO keeps the article text).\n */\nconst CollectionEntryBody = forwardRef<\n HTMLDivElement,\n CollectionEntryBodyProps\n>((props, ref) => {\n // `intrinsicSizes` is destructured out rather than left in `rest`: `rest` is\n // spread onto a Box, and a composition-stamped object would reach the DOM as\n // an attribute React has no idea what to do with.\n const { markdown, intrinsicSizes, ...rest } = props\n // Node styles ride the renderer-merged sx; recompose (stack.ts pattern).\n const nodeSx = Array.isArray(props['sx']) ? props['sx'] : [props['sx']]\n // The routing map travels with the suppression flag: both decide what a\n // link in this body renders as (AGL-3118).\n const { suppressNavigation, screens } = useContext(Aglyn.ScreenLinkContext)\n const links = useMemo(\n () => ({ suppressNavigation, screens }),\n [suppressNavigation, screens],\n )\n // The site being rendered, for image blocks (AGL-1686) — read once here\n // because hooks cannot run inside the block map below.\n const { hostId } = Aglyn.useSite()\n const source = (markdown ?? '').trim()\n const unresolved = !source || UNRESOLVED_TOKEN.test(source)\n const blocks = useMemo(\n () => (unresolved ? [] : Aglyn.parseMarkdownLite(source)),\n [source, unresolved],\n )\n // Heading anchors, on the same terms as the Markdown element (AGL-1162).\n // Without them a blog article could not carry an \"On this page\" aside at\n // all: the Table of Contents element emits `#slug` links, and there was\n // nothing on this page for them to land on.\n const slugs = useMemo(() => Aglyn.markdownHeadingSlugs(blocks), [blocks])\n if (unresolved) {\n // Editing surfaces get an affordance; the published site renders\n // nothing rather than a literal token.\n if (!suppressNavigation) return <Box ref={ref} {...rest} />\n return (\n <Box\n ref={ref}\n {...rest}\n sx={[\n {\n p: 2,\n border: '1px dashed',\n borderColor: 'divider',\n color: 'text.secondary',\n fontSize: 12,\n fontFamily: 'system-ui, sans-serif',\n },\n ...nodeSx,\n ]}\n >\n {'Entry body — the {{entry.body}} markdown renders here'}\n </Box>\n )\n }\n return (\n <Box ref={ref} {...rest}>\n {blocks.map((block, index) => {\n if (block.type === 'heading') {\n return (\n <Typography\n key={index}\n id={slugs[index]}\n variant={block.level === 2 ? 'h4' : 'h5'}\n component={block.level === 2 ? 'h2' : 'h3'}\n gutterBottom\n sx={{\n scrollMarginTop: `${Aglyn.HEADING_ANCHOR_SCROLL_MARGIN}px`,\n }}\n >\n {renderInlines(block.inlines, links)}\n </Typography>\n )\n }\n if (block.type === 'image') {\n return (\n <Box\n key={index}\n component=\"img\"\n // Resolved like the related cover image in collection-related.tsx\n // (AGL-1686): a body image is the same kind of asset and\n // has no reason to be stored in a more fragile form.\n //\n // And sized like one too, as of AGL-3149: the same call gives\n // the Markdown element's body images their candidate list and\n // intrinsic pair, so the two bodies cannot disagree about how a\n // picture in prose is delivered.\n {...Aglyn.mediaBodyImageAttributes({\n src: block.src,\n hostId,\n size: intrinsicSizes?.[block.src],\n })}\n alt={block.alt}\n // Paired with the `width`/`height` above — see the Markdown\n // element's copy of this: without it the height hint outlives\n // the `maxWidth` cap and squashes the picture.\n sx={{ maxWidth: '100%', height: 'auto', borderRadius: 1, my: 1 }}\n // An image inside an entry body is below the fold by\n // construction — the title, byline and opening paragraphs are\n // above it. This one carried no loading hint at all, so it was\n // fetched EAGERLY, at default priority, competing with the\n // entry's own cover (AGL-2486).\n {...Aglyn.DEFERRED_IMAGE_ATTRIBUTES}\n />\n )\n }\n if (block.type === 'list') {\n return (\n <Box key={index} component=\"ul\" sx={{ lineHeight: 1.7, pl: 3 }}>\n {block.items.map((item, itemIndex) => (\n <li key={itemIndex}>\n {renderInlines(item, links)}\n </li>\n ))}\n </Box>\n )\n }\n // A numbered list (AGL-1320) — a real `<ol>`, so the markers are the\n // browser's and `start` survives: a notice that resumes at 7 has to\n // read as 7 for the enumeration to mean anything.\n if (block.type === 'orderedList') {\n return (\n <Box\n key={index}\n component=\"ol\"\n start={block.start}\n sx={{ lineHeight: 1.7, pl: 3 }}\n >\n {block.items.map((item, itemIndex) => (\n <li key={itemIndex}>\n {renderInlines(item, links)}\n </li>\n ))}\n </Box>\n )\n }\n // Code blocks and tables (AGL-974). Both scroll rather than wrap:\n // a wrapped code line or a squeezed table column loses the very\n // structure that made the block worth writing.\n if (block.type === 'code') {\n return (\n <Box\n key={index}\n component=\"pre\"\n sx={{\n my: 2,\n p: 2,\n overflowX: 'auto',\n borderRadius: 1,\n bgcolor: 'action.hover',\n fontFamily: 'monospace',\n fontSize: 14,\n }}\n >\n <code>{block.text}</code>\n </Box>\n )\n }\n if (block.type === 'table') {\n return (\n <Box key={index} sx={{ my: 2, overflowX: 'auto' }}>\n <Box\n component=\"table\"\n sx={{\n borderCollapse: 'collapse',\n width: '100%',\n '& th, & td': {\n border: '1px solid',\n borderColor: 'divider',\n px: 1.5,\n py: 1,\n },\n '& th': { bgcolor: 'action.hover' },\n }}\n >\n <thead>\n <tr>\n {block.header.map((cell, cellIndex) => (\n <th\n key={cellIndex}\n style={{ textAlign: block.align[cellIndex] ?? 'left' }}\n >\n {renderInlines(cell, links)}\n </th>\n ))}\n </tr>\n </thead>\n <tbody>\n {block.rows.map((row, rowIndex) => (\n <tr key={rowIndex}>\n {row.map((cell, cellIndex) => (\n <td\n key={cellIndex}\n style={{\n textAlign: block.align[cellIndex] ?? 'left',\n }}\n >\n {renderInlines(cell, links)}\n </td>\n ))}\n </tr>\n ))}\n </tbody>\n </Box>\n </Box>\n )\n }\n // The article template's pull-quote (AGL-1315, Figma 170:225):\n // larger italic prose behind a left accent. Palette tokens only, so\n // it follows the site theme in both modes.\n if (block.type === 'quote') {\n return (\n <Typography\n key={index}\n component=\"blockquote\"\n sx={{\n my: 3,\n mx: 0,\n pl: 2.5,\n borderLeft: '3px solid',\n borderColor: 'primary.main',\n fontStyle: 'italic',\n fontSize: '1.25em',\n lineHeight: 1.6,\n color: 'text.primary',\n }}\n >\n {renderInlines(block.inlines, links)}\n </Typography>\n )\n }\n return (\n <Typography\n key={index}\n variant=\"body1\"\n sx={{ lineHeight: 1.7 }}\n gutterBottom\n >\n {renderInlines(block.inlines, links)}\n </Typography>\n )\n })}\n </Box>\n )\n})\nCollectionEntryBody.displayName = 'AglynCollectionEntryBody'\n\nexport const collectionEntryBodySchema: Aglyn.ComponentSchema<CollectionEntryBodyProps> =\n {\n $id: ENTRY_BODY_ID,\n pluginId: BUNDLE_ID,\n displayName: 'Entry Body',\n description: \"The current entry's markdown body, on an entry template.\",\n category: Aglyn.ComponentCategory.TEXT,\n icon: { path: mdiTextLong.path, sx: { color: 'secondary.main' } },\n flags: { selfClosing: Aglyn.FEATURE_FLAG.ENABLED },\n attributes: [\n {\n name: 'markdown',\n label: 'Markdown',\n description:\n 'Markdown-lite content. Keep {{entry.body}} on entry-template ' +\n \"pages so each entry's body renders here.\",\n component: Aglyn.FieldComponentType.TEXT_FIELD,\n },\n ],\n }\n\nexport { CollectionEntryBody }\n"],"names":["Aglyn","mdiTextLong","AppLink","Box","MuiLink","Typography","forwardRef","useContext","useMemo","BUNDLE_ID","UNRESOLVED_TOKEN","ENTRY_BODY_ID","COLLECTION_ENTRY_BODY_COMPONENT_ID","renderInlines","inlines","links","map","item","index","type","strong","text","em","span","link","resolveMarkdownLink","href","kind","component","sx","cursor","CollectionEntryBody","props","ref","markdown","intrinsicSizes","rest","nodeSx","Array","isArray","suppressNavigation","screens","ScreenLinkContext","hostId","useSite","source","trim","unresolved","test","blocks","parseMarkdownLite","slugs","markdownHeadingSlugs","p","border","borderColor","color","fontSize","fontFamily","block","id","variant","level","gutterBottom","scrollMarginTop","HEADING_ANCHOR_SCROLL_MARGIN","mediaBodyImageAttributes","src","size","alt","maxWidth","height","borderRadius","my","DEFERRED_IMAGE_ATTRIBUTES","lineHeight","pl","items","itemIndex","li","start","overflowX","bgcolor","code","borderCollapse","width","px","py","thead","tr","header","cell","cellIndex","th","style","textAlign","align","tbody","rows","row","rowIndex","td","mx","borderLeft","fontStyle","displayName","collectionEntryBodySchema","$id","pluginId","description","category","ComponentCategory","TEXT","icon","path","flags","selfClosing","FEATURE_FLAG","ENABLED","attributes","name","label","FieldComponentType","TEXT_FIELD"],"mappings":"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"}
@@ -18,6 +18,7 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
18
18
  */ import * as Aglyn from "@aglyn/aglyn";
19
19
  import { mdiMagnify } from "@aglyn/shared-data-mdi";
20
20
  import { AppLink, MdiIcon } from "@aglyn/shared-ui-jsx";
21
+ import { useSoftGetSubmit } from "@aglyn/shared-ui-jsx/hooks/use-soft-get-submit";
21
22
  import Box from "@mui/material/Box";
22
23
  import Chip from "@mui/material/Chip";
23
24
  import InputBase from "@mui/material/InputBase";
@@ -294,6 +295,7 @@ let fuseLoad;
294
295
  */ export const SuggestSearchBox = ({ fuzzy, requestFuzzy, items, placeholder, inert, emptyText })=>{
295
296
  const [query, setQuery] = useState('');
296
297
  /** Escape dismissed THIS answer (AGL-1525); typing brings it back. */ const [closed, setClosed] = useState(false);
298
+ const softSubmit = useSoftGetSubmit();
297
299
  const trimmed = query.trim();
298
300
  const suggestions = !inert && trimmed && fuzzy && items ? fuzzy.search(trimmed).slice(0, SUGGESTION_LIMIT).map((result)=>items[result.refIndex]).filter(Boolean) : [];
299
301
  const field = /*#__PURE__*/ _jsx(SearchField, _extends({
@@ -319,6 +321,13 @@ let fuseLoad;
319
321
  role: "search",
320
322
  action: "/search",
321
323
  method: "get",
324
+ onSubmit: (event)=>{
325
+ // The client router keeps this box mounted when it sits in the
326
+ // site's chrome, so the panel is shut here rather than left open
327
+ // over the results page it just opened.
328
+ setClosed(true);
329
+ softSubmit(event);
330
+ },
322
331
  sx: suggestionAnchorSx,
323
332
  children: [
324
333
  field,