@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.
- package/package.json +7 -7
- package/src/lib/components/collection-author-links.d.ts +41 -0
- package/src/lib/components/collection-author-links.js +135 -0
- package/src/lib/components/collection-author-links.js.map +1 -0
- package/src/lib/components/collection-author-profile.d.ts +67 -0
- package/src/lib/components/collection-author-profile.js +269 -0
- package/src/lib/components/collection-author-profile.js.map +1 -0
- package/src/lib/components/collection-categories.d.ts +52 -0
- package/src/lib/components/collection-categories.js +163 -0
- package/src/lib/components/collection-categories.js.map +1 -0
- package/src/lib/components/collection-common.d.ts +104 -0
- package/src/lib/components/collection-common.js +95 -0
- package/src/lib/components/collection-common.js.map +1 -0
- package/src/lib/components/collection-entries.d.ts +137 -0
- package/src/lib/components/collection-entries.js +358 -0
- package/src/lib/components/collection-entries.js.map +1 -0
- package/src/lib/components/collection-entry-author.d.ts +81 -0
- package/src/lib/components/collection-entry-author.js +259 -0
- package/src/lib/components/collection-entry-author.js.map +1 -0
- package/src/lib/components/collection-entry-body.d.ts +44 -0
- package/src/lib/components/collection-entry-body.js +328 -0
- package/src/lib/components/collection-entry-body.js.map +1 -0
- package/src/lib/components/collection-entry-meta.d.ts +88 -0
- package/src/lib/components/collection-entry-meta.js +331 -0
- package/src/lib/components/collection-entry-meta.js.map +1 -0
- package/src/lib/components/collection-presets.d.ts +18 -0
- package/src/lib/components/collection-presets.js +328 -0
- package/src/lib/components/collection-presets.js.map +1 -0
- package/src/lib/components/collection-related.d.ts +101 -0
- package/src/lib/components/collection-related.js +536 -0
- package/src/lib/components/collection-related.js.map +1 -0
- package/src/lib/components/collection-search-box.d.ts +92 -0
- package/src/lib/components/collection-search-box.js +334 -0
- package/src/lib/components/collection-search-box.js.map +1 -0
- package/src/lib/components/collection-search-fuse.d.ts +28 -0
- package/src/lib/components/collection-search-fuse.js +28 -0
- package/src/lib/components/collection-search-fuse.js.map +1 -0
- package/src/lib/components/collection-search.d.ts +62 -0
- package/src/lib/components/collection-search.js +140 -0
- package/src/lib/components/collection-search.js.map +1 -0
- package/src/lib/components/collection-share.d.ts +31 -0
- package/src/lib/components/collection-share.js +154 -0
- package/src/lib/components/collection-share.js.map +1 -0
- package/src/lib/components/collection.d.ts +19 -497
- package/src/lib/components/collection.js +19 -2982
- package/src/lib/components/collection.js.map +1 -1
- package/src/lib/components/image-list.d.ts +2 -2
- package/src/lib/components/image-list.js +4 -6
- package/src/lib/components/image-list.js.map +1 -1
- package/src/lib/element-tier.d.ts +9 -1
- package/src/lib/element-tier.js +12 -1
- package/src/lib/element-tier.js.map +1 -1
- package/src/lib/plugin.js +20 -12
- package/src/lib/plugin.js.map +1 -1
- package/src/lib/utils/to-count.d.ts +25 -0
- package/src/lib/utils/to-count.js +30 -0
- 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 };
|