@aglyn/aglyn 1.0.0-beta.147 → 1.0.0-beta.149
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 +10 -10
- package/src/lib/app-utils/binding-token-catalog.js +9 -2
- package/src/lib/app-utils/binding-token-catalog.js.map +1 -1
- package/src/lib/app-utils/collection-entries.d.ts +52 -3
- package/src/lib/app-utils/collection-entries.js +98 -9
- package/src/lib/app-utils/collection-entries.js.map +1 -1
- package/src/lib/app-utils/console-routes.d.ts +2 -0
- package/src/lib/app-utils/console-routes.js +14 -0
- package/src/lib/app-utils/console-routes.js.map +1 -1
- package/src/lib/app-utils/docs-help.generated.d.ts +1 -1
- package/src/lib/app-utils/docs-help.generated.js +3 -0
- package/src/lib/app-utils/docs-help.generated.js.map +1 -1
- package/src/lib/app-utils/docs-index.generated.js +1 -1
- package/src/lib/app-utils/docs-index.generated.js.map +1 -1
- package/src/lib/app-utils/notifications.d.ts +136 -2
- package/src/lib/app-utils/notifications.js +139 -3
- package/src/lib/app-utils/notifications.js.map +1 -1
- package/src/lib/foundation/definitions/host-content-collections.d.ts +42 -40
- package/src/lib/foundation/definitions/host-content-collections.js +47 -109
- package/src/lib/foundation/definitions/host-content-collections.js.map +1 -1
- package/src/lib/plugin-manager/feature-plugins.d.ts +70 -0
- package/src/lib/plugin-manager/feature-plugins.js +27 -0
- package/src/lib/plugin-manager/feature-plugins.js.map +1 -1
- package/src/lib/plugin-manager/first-party-plugins.generated.d.ts +13 -0
- package/src/lib/plugin-manager/first-party-plugins.generated.js +257 -0
- package/src/lib/plugin-manager/first-party-plugins.generated.js.map +1 -1
- package/src/lib/plugin-manager/plugin-contact-capture.d.ts +16 -0
- package/src/lib/plugin-manager/plugin-contact-capture.js.map +1 -1
- package/src/lib/plugin-manager/plugin-host-collections.d.ts +27 -5
- package/src/lib/plugin-manager/plugin-host-collections.js +25 -16
- package/src/lib/plugin-manager/plugin-host-collections.js.map +1 -1
- package/src/lib/plugin-manager/plugin-org-capacity.d.ts +57 -0
- package/src/lib/plugin-manager/plugin-org-capacity.js +68 -0
- package/src/lib/plugin-manager/plugin-org-capacity.js.map +1 -0
- package/src/lib/plugin-manager/record-captured-contact.d.ts +89 -0
- package/src/lib/plugin-manager/record-captured-contact.js +128 -0
- package/src/lib/plugin-manager/record-captured-contact.js.map +1 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@aglyn/aglyn",
|
|
3
|
-
"version": "1.0.0-beta.
|
|
3
|
+
"version": "1.0.0-beta.149",
|
|
4
4
|
"license": "Apache-2.0",
|
|
5
5
|
"homepage": "https://aglyn.com",
|
|
6
6
|
"repository": {
|
|
@@ -33,15 +33,15 @@
|
|
|
33
33
|
"./package.json": "./package.json"
|
|
34
34
|
},
|
|
35
35
|
"dependencies": {
|
|
36
|
-
"@aglyn/shared-data-enums": "1.0.0-beta.
|
|
37
|
-
"@aglyn/shared-data-mdi": "1.0.0-beta.
|
|
38
|
-
"@aglyn/shared-data-types": "1.0.0-beta.
|
|
39
|
-
"@aglyn/shared-util-email": "1.0.0-beta.
|
|
40
|
-
"@aglyn/shared-util-http": "1.0.0-beta.
|
|
41
|
-
"@aglyn/shared-util-logger": "1.0.0-beta.
|
|
42
|
-
"@aglyn/shared-util-timestamp": "1.0.0-beta.
|
|
43
|
-
"@aglyn/shared-util-tools": "1.0.0-beta.
|
|
44
|
-
"@aglyn/shared-util-vendor": "1.0.0-beta.
|
|
36
|
+
"@aglyn/shared-data-enums": "1.0.0-beta.149",
|
|
37
|
+
"@aglyn/shared-data-mdi": "1.0.0-beta.149",
|
|
38
|
+
"@aglyn/shared-data-types": "1.0.0-beta.149",
|
|
39
|
+
"@aglyn/shared-util-email": "1.0.0-beta.149",
|
|
40
|
+
"@aglyn/shared-util-http": "1.0.0-beta.149",
|
|
41
|
+
"@aglyn/shared-util-logger": "1.0.0-beta.149",
|
|
42
|
+
"@aglyn/shared-util-timestamp": "1.0.0-beta.149",
|
|
43
|
+
"@aglyn/shared-util-tools": "1.0.0-beta.149",
|
|
44
|
+
"@aglyn/shared-util-vendor": "1.0.0-beta.149",
|
|
45
45
|
"@data-driven-forms/react-form-renderer": "^4.2.0",
|
|
46
46
|
"@msgpack/msgpack": "^3.1.3",
|
|
47
47
|
"@types/unist": "^3.0.3",
|
|
@@ -177,7 +177,11 @@ import { humanizeDatasetFieldId } from "./datasets.js";
|
|
|
177
177
|
{
|
|
178
178
|
token: '{{pagination.totalPages}}',
|
|
179
179
|
label: 'Total pages',
|
|
180
|
-
|
|
180
|
+
// A listing bigger than one read cannot be counted honestly (AGL-3219):
|
|
181
|
+
// the count and the entries were read at two different moments, and the
|
|
182
|
+
// pager built from them disagreed with itself at the page boundary. It
|
|
183
|
+
// still resolves, so a template binding it keeps rendering.
|
|
184
|
+
description: 'Pages in the listing, after any category filter. Empty on a listing too large to count — bind the next link instead.'
|
|
181
185
|
},
|
|
182
186
|
{
|
|
183
187
|
token: '{{pagination.prevUrl}}',
|
|
@@ -187,7 +191,10 @@ import { humanizeDatasetFieldId } from "./datasets.js";
|
|
|
187
191
|
{
|
|
188
192
|
token: '{{pagination.nextUrl}}',
|
|
189
193
|
label: 'Next page link',
|
|
190
|
-
|
|
194
|
+
// The reliable "is there more" (AGL-3219): it is set from a read that
|
|
195
|
+
// asked for one entry more than the page needed, so empty means there was
|
|
196
|
+
// no such entry rather than that a total said so.
|
|
197
|
+
description: 'Keeps the category; empty when there is nothing older to show.'
|
|
191
198
|
}
|
|
192
199
|
];
|
|
193
200
|
/**
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../../../../libs/aglyn/src/lib/app-utils/binding-token-catalog.ts"],"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 {\n DATASET_FIELD_TYPE_LABELS,\n type DatasetModel,\n} from './dataset-models'\nimport { humanizeDatasetFieldId } from './datasets'\n\n/**\n * Browsable data-placeholder catalogs for the designer's insert picker\n * (AGL-583). Hand-typing `{{entry.title}}` stays the advanced path; these\n * catalogs give every token a friendly label + description so editors can\n * browse and insert instead of memorizing the grammar.\n *\n * The token STRINGS are owned elsewhere — `collectionEntryTokens`\n * (collection-entries.ts) and the tenant compose pipeline resolve them —\n * this module only names them for pickers. The spec cross-checks the entry\n * catalog against the resolver so the two can never drift.\n */\nexport interface BindingTokenCatalogEntry {\n /** The literal token inserted into the prop, e.g. `{{entry.title}}`. */\n token: string\n /** Friendly picker label, e.g. `Title`. */\n label: string\n /** One-line description shown as the option's secondary text. */\n description?: string\n}\n\n/**\n * `{{entry.*}}` tokens (AGL-105/551/582) — resolve per entry inside a\n * Collection entries block and page-wide on entry-template screens.\n * Mirrors {@link collectionEntryTokens}; the spec enforces the mirror.\n */\nexport const ENTRY_TOKEN_CATALOG: readonly BindingTokenCatalogEntry[] = [\n { token: '{{entry.title}}', label: 'Title', description: 'The entry headline.' },\n {\n token: '{{entry.excerpt}}',\n label: 'Excerpt',\n description: 'Short summary text.',\n },\n {\n token: '{{entry.body}}',\n label: 'Body',\n description: 'The full entry body.',\n },\n {\n token: '{{entry.url}}',\n label: 'Link URL',\n description: 'Auto-route to the entry page.',\n },\n {\n token: '{{entry.date}}',\n label: 'Published date',\n description: 'Formatted publish date.',\n },\n /*\n Beside the readable date on purpose: a designer reaching for \"the date\" in\n a Video element's Publication date field has to see that the readable one\n is the wrong pick there, and why.\n */\n {\n token: '{{entry.publishedAt}}',\n label: 'Publish timestamp',\n description:\n 'The publish date and time in ISO 8601, for a field that needs a ' +\n 'machine-readable date, such as a Video element’s publication date.',\n },\n {\n token: '{{entry.author}}',\n label: 'Author',\n description: 'The byline set on the entry.',\n },\n {\n token: '{{entry.authorBio}}',\n label: 'Author bio',\n description: 'Blurb from the author’s record.',\n },\n {\n token: '{{entry.authorImage}}',\n label: 'Author portrait',\n description: 'Portrait or logo from the author’s record.',\n },\n {\n token: '{{entry.authorUrl}}',\n label: 'Author link',\n description: 'The author’s own page.',\n },\n {\n token: '{{entry.authorPageUrl}}',\n label: 'Author page',\n description:\n 'This author’s page on this site — everything they wrote, across every ' +\n 'collection. Separate from Author link, which is their own site.',\n },\n {\n token: '{{entry.slug}}',\n label: 'Slug',\n description: 'URL-safe entry identifier.',\n },\n /*\n Named \"Its collection…\" rather than \"Collection…\", which is what these\n describe and also exactly what the `{{collection.*}}` entries below are\n called. The picker groups options by heading, so the two sets sit apart —\n but a designer scanning it reads the LABEL, and two options reading\n \"Collection name\" three lines apart is a choice made by guessing. The\n pronoun is doing real work: on the one page where both resolve, they mean\n different collections.\n */\n {\n token: '{{entry.collection}}',\n label: 'Its collection',\n description:\n 'Which section this entry belongs to. Worth binding on a listing that ' +\n 'MIXES collections — an author page — where a card otherwise cannot ' +\n 'tell a release note from an essay.',\n },\n {\n token: '{{entry.collectionSlug}}',\n label: 'Its collection slug',\n description: 'That collection’s URL segment.',\n },\n {\n token: '{{entry.collectionUrl}}',\n label: 'Its collection link',\n description: 'The listing this entry belongs to.',\n },\n {\n token: '{{entry.coverImage}}',\n label: 'Cover image',\n description: 'Cover image URL.',\n },\n {\n token: '{{entry.coverVideo}}',\n label: 'Featured video',\n description:\n 'The featured video’s source — a library film, a video link or a ' +\n 'Wistia link — for a Video element.',\n },\n {\n token: '{{entry.category}}',\n label: 'Category',\n description: 'The entry category.',\n },\n {\n token: '{{entry.tags}}',\n label: 'Tags',\n description: 'Tags, comma separated.',\n },\n {\n token: '{{entry.seoTitle}}',\n label: 'SEO title',\n description: 'Search title; falls back to Title.',\n },\n {\n token: '{{entry.seoDescription}}',\n label: 'SEO description',\n description: 'Meta description; falls back to Excerpt.',\n },\n]\n\n/**\n * `{{collection.*}}` and `{{pagination.*}}` tokens (AGL-551/1321/1386) —\n * resolve on collection list/entry template screens (see the tenant compose\n * pipeline's collection tokens).\n *\n * The category and pagination tokens all resolve to the empty string where\n * they do not apply, so they are safe to bind unconditionally: ONE list\n * screen serves the bare listing, every `/page/{n}` and every\n * `/category/{slug}`, and a template has no runtime conditional to vary\n * itself with.\n */\nexport const COLLECTION_TOKEN_CATALOG: readonly BindingTokenCatalogEntry[] = [\n {\n token: '{{collection.name}}',\n label: 'Collection name',\n description: 'Display name of the routed collection.',\n },\n {\n token: '{{collection.slug}}',\n label: 'Collection slug',\n description: 'URL slug of the routed collection.',\n },\n {\n token: '{{collection.category}}',\n label: 'Filtered category',\n description: 'Category the URL filtered on; empty when unfiltered.',\n },\n {\n token: '{{collection.categorySlug}}',\n label: 'Filtered category slug',\n description: 'That category’s URL segment; empty when unfiltered.',\n },\n {\n token: '{{pagination.page}}',\n label: 'Current page',\n description: 'Page number this URL is showing.',\n },\n {\n token: '{{pagination.totalPages}}',\n label: 'Total pages',\n description: 'Pages in the listing, after any category filter.',\n },\n {\n token: '{{pagination.prevUrl}}',\n label: 'Previous page link',\n description: 'Keeps the category; empty on the first page.',\n },\n {\n token: '{{pagination.nextUrl}}',\n label: 'Next page link',\n description: 'Keeps the category; empty on the last page.',\n },\n]\n\n/**\n * `{{author.*}}` tokens (AGL-2518) — resolve on an author's own page,\n * `/author/{slug}`.\n *\n * Empty everywhere else, like the category tokens above and for the same\n * reason: a template has no runtime conditional, so the tokens have to be the\n * thing that varies. A heading bound to `{{author.name}}` prints a name on an\n * author page and nothing anywhere else.\n *\n * The `{{pagination.*}}` tokens resolve HERE TOO, over this author's own\n * archive — deliberately the same four names a collection listing uses, so a\n * pager built once works on both.\n */\nexport const AUTHOR_TOKEN_CATALOG: readonly BindingTokenCatalogEntry[] = [\n {\n token: '{{author.name}}',\n label: 'Name',\n description: 'The byline this page collects.',\n },\n {\n token: '{{author.bio}}',\n label: 'Bio',\n description: 'Their blurb, from the author record.',\n },\n {\n token: '{{author.image}}',\n label: 'Portrait',\n description: 'Their portrait or logo, from the author record.',\n },\n {\n token: '{{author.jobTitle}}',\n label: 'Role',\n description: 'Their job title; empty for an Organization author.',\n },\n {\n token: '{{author.worksFor}}',\n label: 'Organization',\n description: 'Who they write for; empty for an Organization author.',\n },\n {\n token: '{{author.url}}',\n label: 'Their own site',\n description:\n 'The url on their record — a personal site, not this page. Empty when ' +\n 'they have none.',\n },\n {\n token: '{{author.pageUrl}}',\n label: 'This page',\n description: 'The canonical address of the page you are designing.',\n },\n {\n token: '{{author.entryCount}}',\n label: 'Post count',\n description: 'How many entries they have published, across every collection.',\n },\n {\n token: '{{author.entryCountLabel}}',\n label: 'Post count, worded',\n description:\n '\"1 post\" or \"12 posts\" — pluralized for you, because a template has no ' +\n 'conditional to do it with.',\n },\n]\n\n/**\n * Renders an `{{item.*}}` token for a dataset field, optionally hopping\n * one reference to a field of the referenced record (AGL-180). Always\n * takes the stable model field ID — display names are labels only.\n */\nexport function datasetItemToken(\n fieldId: string,\n targetFieldId?: string,\n): string {\n return targetFieldId\n ? `{{item.${fieldId}.${targetFieldId}}}`\n : `{{item.${fieldId}}}`\n}\n\n/**\n * Dataset-item token entries for a repeatable container's model: one per\n * field in model order, labeled with the CURRENT display name while the\n * token carries the stable reference id (AGL-578 — renaming a label never\n * breaks bindings). Reference fields with a configured display field also\n * get their one-hop token (`{{item.author.name}}`).\n */\nexport function datasetItemTokens(\n model: DatasetModel,\n): BindingTokenCatalogEntry[] {\n const entries: BindingTokenCatalogEntry[] = []\n for (const fieldId of model.order ?? []) {\n const field = model.fields?.[fieldId]\n if (!field) continue\n const label = field.name?.trim() || humanizeDatasetFieldId(fieldId)\n entries.push({\n token: datasetItemToken(fieldId),\n label,\n description: DATASET_FIELD_TYPE_LABELS[field.type] ?? field.type,\n })\n // One reference hop (AGL-180): surface the configured display field of\n // the referenced record — the hop editors reach for most.\n const hopId =\n field.type === 'reference' ? field.reference?.displayFieldId : undefined\n if (hopId) {\n entries.push({\n token: datasetItemToken(fieldId, hopId),\n label: `${label} → ${humanizeDatasetFieldId(hopId)}`,\n description: 'Field from the referenced record.',\n })\n }\n }\n return entries\n}\n"],"names":["DATASET_FIELD_TYPE_LABELS","humanizeDatasetFieldId","ENTRY_TOKEN_CATALOG","token","label","description","COLLECTION_TOKEN_CATALOG","AUTHOR_TOKEN_CATALOG","datasetItemToken","fieldId","targetFieldId","datasetItemTokens","model","entries","order","field","fields","name","trim","push","type","hopId","reference","displayFieldId","undefined"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GAED,SACEA,yBAAyB,QAEpB,sBAAkB;AACzB,SAASC,sBAAsB,QAAQ,gBAAY;AAsBnD;;;;CAIC,GACD,OAAO,MAAMC,sBAA2D;IACtE;QAAEC,OAAO;QAAmBC,OAAO;QAASC,aAAa;IAAsB;IAC/E;QACEF,OAAO;QACPC,OAAO;QACPC,aAAa;IACf;IACA;QACEF,OAAO;QACPC,OAAO;QACPC,aAAa;IACf;IACA;QACEF,OAAO;QACPC,OAAO;QACPC,aAAa;IACf;IACA;QACEF,OAAO;QACPC,OAAO;QACPC,aAAa;IACf;IACA;;;;EAIA,GACA;QACEF,OAAO;QACPC,OAAO;QACPC,aACE,qEACA;IACJ;IACA;QACEF,OAAO;QACPC,OAAO;QACPC,aAAa;IACf;IACA;QACEF,OAAO;QACPC,OAAO;QACPC,aAAa;IACf;IACA;QACEF,OAAO;QACPC,OAAO;QACPC,aAAa;IACf;IACA;QACEF,OAAO;QACPC,OAAO;QACPC,aAAa;IACf;IACA;QACEF,OAAO;QACPC,OAAO;QACPC,aACE,2EACA;IACJ;IACA;QACEF,OAAO;QACPC,OAAO;QACPC,aAAa;IACf;IACA;;;;;;;;EAQA,GACA;QACEF,OAAO;QACPC,OAAO;QACPC,aACE,0EACA,wEACA;IACJ;IACA;QACEF,OAAO;QACPC,OAAO;QACPC,aAAa;IACf;IACA;QACEF,OAAO;QACPC,OAAO;QACPC,aAAa;IACf;IACA;QACEF,OAAO;QACPC,OAAO;QACPC,aAAa;IACf;IACA;QACEF,OAAO;QACPC,OAAO;QACPC,aACE,qEACA;IACJ;IACA;QACEF,OAAO;QACPC,OAAO;QACPC,aAAa;IACf;IACA;QACEF,OAAO;QACPC,OAAO;QACPC,aAAa;IACf;IACA;QACEF,OAAO;QACPC,OAAO;QACPC,aAAa;IACf;IACA;QACEF,OAAO;QACPC,OAAO;QACPC,aAAa;IACf;CACD,CAAA;AAED;;;;;;;;;;CAUC,GACD,OAAO,MAAMC,2BAAgE;IAC3E;QACEH,OAAO;QACPC,OAAO;QACPC,aAAa;IACf;IACA;QACEF,OAAO;QACPC,OAAO;QACPC,aAAa;IACf;IACA;QACEF,OAAO;QACPC,OAAO;QACPC,aAAa;IACf;IACA;QACEF,OAAO;QACPC,OAAO;QACPC,aAAa;IACf;IACA;QACEF,OAAO;QACPC,OAAO;QACPC,aAAa;IACf;IACA;QACEF,OAAO;QACPC,OAAO;QACPC,aAAa;IACf;IACA;QACEF,OAAO;QACPC,OAAO;QACPC,aAAa;IACf;IACA;QACEF,OAAO;QACPC,OAAO;QACPC,aAAa;IACf;CACD,CAAA;AAED;;;;;;;;;;;;CAYC,GACD,OAAO,MAAME,uBAA4D;IACvE;QACEJ,OAAO;QACPC,OAAO;QACPC,aAAa;IACf;IACA;QACEF,OAAO;QACPC,OAAO;QACPC,aAAa;IACf;IACA;QACEF,OAAO;QACPC,OAAO;QACPC,aAAa;IACf;IACA;QACEF,OAAO;QACPC,OAAO;QACPC,aAAa;IACf;IACA;QACEF,OAAO;QACPC,OAAO;QACPC,aAAa;IACf;IACA;QACEF,OAAO;QACPC,OAAO;QACPC,aACE,0EACA;IACJ;IACA;QACEF,OAAO;QACPC,OAAO;QACPC,aAAa;IACf;IACA;QACEF,OAAO;QACPC,OAAO;QACPC,aAAa;IACf;IACA;QACEF,OAAO;QACPC,OAAO;QACPC,aACE,4EACA;IACJ;CACD,CAAA;AAED;;;;CAIC,GACD,OAAO,SAASG,iBACdC,OAAe,EACfC,aAAsB;IAEtB,OAAOA,gBACH,CAAC,OAAO,EAAED,QAAQ,CAAC,EAAEC,cAAc,EAAE,CAAC,GACtC,CAAC,OAAO,EAAED,QAAQ,EAAE,CAAC;AAC3B;AAEA;;;;;;CAMC,GACD,OAAO,SAASE,kBACdC,KAAmB;QAGGA;IADtB,MAAMC,UAAsC,EAAE;IAC9C,KAAK,MAAMJ,YAAWG,eAAAA,MAAME,KAAK,YAAXF,eAAe,EAAE,CAAE;YAOxBZ;YANDY,eAEAG,aASiBA;QAX/B,MAAMA,SAAQH,gBAAAA,MAAMI,MAAM,qBAAZJ,aAAc,CAACH,QAAQ;QACrC,IAAI,CAACM,OAAO;QACZ,MAAMX,QAAQW,EAAAA,cAAAA,MAAME,IAAI,qBAAVF,YAAYG,IAAI,OAAMjB,uBAAuBQ;QAC3DI,QAAQM,IAAI,CAAC;YACXhB,OAAOK,iBAAiBC;YACxBL;YACAC,WAAW,GAAEL,wCAAAA,yBAAyB,CAACe,MAAMK,IAAI,CAAC,YAArCpB,wCAAyCe,MAAMK,IAAI;QAClE;QACA,uEAAuE;QACvE,0DAA0D;QAC1D,MAAMC,QACJN,MAAMK,IAAI,KAAK,eAAcL,mBAAAA,MAAMO,SAAS,qBAAfP,iBAAiBQ,cAAc,GAAGC;QACjE,IAAIH,OAAO;YACTR,QAAQM,IAAI,CAAC;gBACXhB,OAAOK,iBAAiBC,SAASY;gBACjCjB,OAAO,GAAGA,MAAM,GAAG,EAAEH,uBAAuBoB,QAAQ;gBACpDhB,aAAa;YACf;QACF;IACF;IACA,OAAOQ;AACT"}
|
|
1
|
+
{"version":3,"sources":["../../../../../../libs/aglyn/src/lib/app-utils/binding-token-catalog.ts"],"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 {\n DATASET_FIELD_TYPE_LABELS,\n type DatasetModel,\n} from './dataset-models'\nimport { humanizeDatasetFieldId } from './datasets'\n\n/**\n * Browsable data-placeholder catalogs for the designer's insert picker\n * (AGL-583). Hand-typing `{{entry.title}}` stays the advanced path; these\n * catalogs give every token a friendly label + description so editors can\n * browse and insert instead of memorizing the grammar.\n *\n * The token STRINGS are owned elsewhere — `collectionEntryTokens`\n * (collection-entries.ts) and the tenant compose pipeline resolve them —\n * this module only names them for pickers. The spec cross-checks the entry\n * catalog against the resolver so the two can never drift.\n */\nexport interface BindingTokenCatalogEntry {\n /** The literal token inserted into the prop, e.g. `{{entry.title}}`. */\n token: string\n /** Friendly picker label, e.g. `Title`. */\n label: string\n /** One-line description shown as the option's secondary text. */\n description?: string\n}\n\n/**\n * `{{entry.*}}` tokens (AGL-105/551/582) — resolve per entry inside a\n * Collection entries block and page-wide on entry-template screens.\n * Mirrors {@link collectionEntryTokens}; the spec enforces the mirror.\n */\nexport const ENTRY_TOKEN_CATALOG: readonly BindingTokenCatalogEntry[] = [\n { token: '{{entry.title}}', label: 'Title', description: 'The entry headline.' },\n {\n token: '{{entry.excerpt}}',\n label: 'Excerpt',\n description: 'Short summary text.',\n },\n {\n token: '{{entry.body}}',\n label: 'Body',\n description: 'The full entry body.',\n },\n {\n token: '{{entry.url}}',\n label: 'Link URL',\n description: 'Auto-route to the entry page.',\n },\n {\n token: '{{entry.date}}',\n label: 'Published date',\n description: 'Formatted publish date.',\n },\n /*\n Beside the readable date on purpose: a designer reaching for \"the date\" in\n a Video element's Publication date field has to see that the readable one\n is the wrong pick there, and why.\n */\n {\n token: '{{entry.publishedAt}}',\n label: 'Publish timestamp',\n description:\n 'The publish date and time in ISO 8601, for a field that needs a ' +\n 'machine-readable date, such as a Video element’s publication date.',\n },\n {\n token: '{{entry.author}}',\n label: 'Author',\n description: 'The byline set on the entry.',\n },\n {\n token: '{{entry.authorBio}}',\n label: 'Author bio',\n description: 'Blurb from the author’s record.',\n },\n {\n token: '{{entry.authorImage}}',\n label: 'Author portrait',\n description: 'Portrait or logo from the author’s record.',\n },\n {\n token: '{{entry.authorUrl}}',\n label: 'Author link',\n description: 'The author’s own page.',\n },\n {\n token: '{{entry.authorPageUrl}}',\n label: 'Author page',\n description:\n 'This author’s page on this site — everything they wrote, across every ' +\n 'collection. Separate from Author link, which is their own site.',\n },\n {\n token: '{{entry.slug}}',\n label: 'Slug',\n description: 'URL-safe entry identifier.',\n },\n /*\n Named \"Its collection…\" rather than \"Collection…\", which is what these\n describe and also exactly what the `{{collection.*}}` entries below are\n called. The picker groups options by heading, so the two sets sit apart —\n but a designer scanning it reads the LABEL, and two options reading\n \"Collection name\" three lines apart is a choice made by guessing. The\n pronoun is doing real work: on the one page where both resolve, they mean\n different collections.\n */\n {\n token: '{{entry.collection}}',\n label: 'Its collection',\n description:\n 'Which section this entry belongs to. Worth binding on a listing that ' +\n 'MIXES collections — an author page — where a card otherwise cannot ' +\n 'tell a release note from an essay.',\n },\n {\n token: '{{entry.collectionSlug}}',\n label: 'Its collection slug',\n description: 'That collection’s URL segment.',\n },\n {\n token: '{{entry.collectionUrl}}',\n label: 'Its collection link',\n description: 'The listing this entry belongs to.',\n },\n {\n token: '{{entry.coverImage}}',\n label: 'Cover image',\n description: 'Cover image URL.',\n },\n {\n token: '{{entry.coverVideo}}',\n label: 'Featured video',\n description:\n 'The featured video’s source — a library film, a video link or a ' +\n 'Wistia link — for a Video element.',\n },\n {\n token: '{{entry.category}}',\n label: 'Category',\n description: 'The entry category.',\n },\n {\n token: '{{entry.tags}}',\n label: 'Tags',\n description: 'Tags, comma separated.',\n },\n {\n token: '{{entry.seoTitle}}',\n label: 'SEO title',\n description: 'Search title; falls back to Title.',\n },\n {\n token: '{{entry.seoDescription}}',\n label: 'SEO description',\n description: 'Meta description; falls back to Excerpt.',\n },\n]\n\n/**\n * `{{collection.*}}` and `{{pagination.*}}` tokens (AGL-551/1321/1386) —\n * resolve on collection list/entry template screens (see the tenant compose\n * pipeline's collection tokens).\n *\n * The category and pagination tokens all resolve to the empty string where\n * they do not apply, so they are safe to bind unconditionally: ONE list\n * screen serves the bare listing, every `/page/{n}` and every\n * `/category/{slug}`, and a template has no runtime conditional to vary\n * itself with.\n */\nexport const COLLECTION_TOKEN_CATALOG: readonly BindingTokenCatalogEntry[] = [\n {\n token: '{{collection.name}}',\n label: 'Collection name',\n description: 'Display name of the routed collection.',\n },\n {\n token: '{{collection.slug}}',\n label: 'Collection slug',\n description: 'URL slug of the routed collection.',\n },\n {\n token: '{{collection.category}}',\n label: 'Filtered category',\n description: 'Category the URL filtered on; empty when unfiltered.',\n },\n {\n token: '{{collection.categorySlug}}',\n label: 'Filtered category slug',\n description: 'That category’s URL segment; empty when unfiltered.',\n },\n {\n token: '{{pagination.page}}',\n label: 'Current page',\n description: 'Page number this URL is showing.',\n },\n {\n token: '{{pagination.totalPages}}',\n label: 'Total pages',\n // A listing bigger than one read cannot be counted honestly (AGL-3219):\n // the count and the entries were read at two different moments, and the\n // pager built from them disagreed with itself at the page boundary. It\n // still resolves, so a template binding it keeps rendering.\n description:\n 'Pages in the listing, after any category filter. Empty on a listing too large to count — bind the next link instead.',\n },\n {\n token: '{{pagination.prevUrl}}',\n label: 'Previous page link',\n description: 'Keeps the category; empty on the first page.',\n },\n {\n token: '{{pagination.nextUrl}}',\n label: 'Next page link',\n // The reliable \"is there more\" (AGL-3219): it is set from a read that\n // asked for one entry more than the page needed, so empty means there was\n // no such entry rather than that a total said so.\n description:\n 'Keeps the category; empty when there is nothing older to show.',\n },\n]\n\n/**\n * `{{author.*}}` tokens (AGL-2518) — resolve on an author's own page,\n * `/author/{slug}`.\n *\n * Empty everywhere else, like the category tokens above and for the same\n * reason: a template has no runtime conditional, so the tokens have to be the\n * thing that varies. A heading bound to `{{author.name}}` prints a name on an\n * author page and nothing anywhere else.\n *\n * The `{{pagination.*}}` tokens resolve HERE TOO, over this author's own\n * archive — deliberately the same four names a collection listing uses, so a\n * pager built once works on both.\n */\nexport const AUTHOR_TOKEN_CATALOG: readonly BindingTokenCatalogEntry[] = [\n {\n token: '{{author.name}}',\n label: 'Name',\n description: 'The byline this page collects.',\n },\n {\n token: '{{author.bio}}',\n label: 'Bio',\n description: 'Their blurb, from the author record.',\n },\n {\n token: '{{author.image}}',\n label: 'Portrait',\n description: 'Their portrait or logo, from the author record.',\n },\n {\n token: '{{author.jobTitle}}',\n label: 'Role',\n description: 'Their job title; empty for an Organization author.',\n },\n {\n token: '{{author.worksFor}}',\n label: 'Organization',\n description: 'Who they write for; empty for an Organization author.',\n },\n {\n token: '{{author.url}}',\n label: 'Their own site',\n description:\n 'The url on their record — a personal site, not this page. Empty when ' +\n 'they have none.',\n },\n {\n token: '{{author.pageUrl}}',\n label: 'This page',\n description: 'The canonical address of the page you are designing.',\n },\n {\n token: '{{author.entryCount}}',\n label: 'Post count',\n description: 'How many entries they have published, across every collection.',\n },\n {\n token: '{{author.entryCountLabel}}',\n label: 'Post count, worded',\n description:\n '\"1 post\" or \"12 posts\" — pluralized for you, because a template has no ' +\n 'conditional to do it with.',\n },\n]\n\n/**\n * Renders an `{{item.*}}` token for a dataset field, optionally hopping\n * one reference to a field of the referenced record (AGL-180). Always\n * takes the stable model field ID — display names are labels only.\n */\nexport function datasetItemToken(\n fieldId: string,\n targetFieldId?: string,\n): string {\n return targetFieldId\n ? `{{item.${fieldId}.${targetFieldId}}}`\n : `{{item.${fieldId}}}`\n}\n\n/**\n * Dataset-item token entries for a repeatable container's model: one per\n * field in model order, labeled with the CURRENT display name while the\n * token carries the stable reference id (AGL-578 — renaming a label never\n * breaks bindings). Reference fields with a configured display field also\n * get their one-hop token (`{{item.author.name}}`).\n */\nexport function datasetItemTokens(\n model: DatasetModel,\n): BindingTokenCatalogEntry[] {\n const entries: BindingTokenCatalogEntry[] = []\n for (const fieldId of model.order ?? []) {\n const field = model.fields?.[fieldId]\n if (!field) continue\n const label = field.name?.trim() || humanizeDatasetFieldId(fieldId)\n entries.push({\n token: datasetItemToken(fieldId),\n label,\n description: DATASET_FIELD_TYPE_LABELS[field.type] ?? field.type,\n })\n // One reference hop (AGL-180): surface the configured display field of\n // the referenced record — the hop editors reach for most.\n const hopId =\n field.type === 'reference' ? field.reference?.displayFieldId : undefined\n if (hopId) {\n entries.push({\n token: datasetItemToken(fieldId, hopId),\n label: `${label} → ${humanizeDatasetFieldId(hopId)}`,\n description: 'Field from the referenced record.',\n })\n }\n }\n return entries\n}\n"],"names":["DATASET_FIELD_TYPE_LABELS","humanizeDatasetFieldId","ENTRY_TOKEN_CATALOG","token","label","description","COLLECTION_TOKEN_CATALOG","AUTHOR_TOKEN_CATALOG","datasetItemToken","fieldId","targetFieldId","datasetItemTokens","model","entries","order","field","fields","name","trim","push","type","hopId","reference","displayFieldId","undefined"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GAED,SACEA,yBAAyB,QAEpB,sBAAkB;AACzB,SAASC,sBAAsB,QAAQ,gBAAY;AAsBnD;;;;CAIC,GACD,OAAO,MAAMC,sBAA2D;IACtE;QAAEC,OAAO;QAAmBC,OAAO;QAASC,aAAa;IAAsB;IAC/E;QACEF,OAAO;QACPC,OAAO;QACPC,aAAa;IACf;IACA;QACEF,OAAO;QACPC,OAAO;QACPC,aAAa;IACf;IACA;QACEF,OAAO;QACPC,OAAO;QACPC,aAAa;IACf;IACA;QACEF,OAAO;QACPC,OAAO;QACPC,aAAa;IACf;IACA;;;;EAIA,GACA;QACEF,OAAO;QACPC,OAAO;QACPC,aACE,qEACA;IACJ;IACA;QACEF,OAAO;QACPC,OAAO;QACPC,aAAa;IACf;IACA;QACEF,OAAO;QACPC,OAAO;QACPC,aAAa;IACf;IACA;QACEF,OAAO;QACPC,OAAO;QACPC,aAAa;IACf;IACA;QACEF,OAAO;QACPC,OAAO;QACPC,aAAa;IACf;IACA;QACEF,OAAO;QACPC,OAAO;QACPC,aACE,2EACA;IACJ;IACA;QACEF,OAAO;QACPC,OAAO;QACPC,aAAa;IACf;IACA;;;;;;;;EAQA,GACA;QACEF,OAAO;QACPC,OAAO;QACPC,aACE,0EACA,wEACA;IACJ;IACA;QACEF,OAAO;QACPC,OAAO;QACPC,aAAa;IACf;IACA;QACEF,OAAO;QACPC,OAAO;QACPC,aAAa;IACf;IACA;QACEF,OAAO;QACPC,OAAO;QACPC,aAAa;IACf;IACA;QACEF,OAAO;QACPC,OAAO;QACPC,aACE,qEACA;IACJ;IACA;QACEF,OAAO;QACPC,OAAO;QACPC,aAAa;IACf;IACA;QACEF,OAAO;QACPC,OAAO;QACPC,aAAa;IACf;IACA;QACEF,OAAO;QACPC,OAAO;QACPC,aAAa;IACf;IACA;QACEF,OAAO;QACPC,OAAO;QACPC,aAAa;IACf;CACD,CAAA;AAED;;;;;;;;;;CAUC,GACD,OAAO,MAAMC,2BAAgE;IAC3E;QACEH,OAAO;QACPC,OAAO;QACPC,aAAa;IACf;IACA;QACEF,OAAO;QACPC,OAAO;QACPC,aAAa;IACf;IACA;QACEF,OAAO;QACPC,OAAO;QACPC,aAAa;IACf;IACA;QACEF,OAAO;QACPC,OAAO;QACPC,aAAa;IACf;IACA;QACEF,OAAO;QACPC,OAAO;QACPC,aAAa;IACf;IACA;QACEF,OAAO;QACPC,OAAO;QACP,wEAAwE;QACxE,wEAAwE;QACxE,uEAAuE;QACvE,4DAA4D;QAC5DC,aACE;IACJ;IACA;QACEF,OAAO;QACPC,OAAO;QACPC,aAAa;IACf;IACA;QACEF,OAAO;QACPC,OAAO;QACP,sEAAsE;QACtE,0EAA0E;QAC1E,kDAAkD;QAClDC,aACE;IACJ;CACD,CAAA;AAED;;;;;;;;;;;;CAYC,GACD,OAAO,MAAME,uBAA4D;IACvE;QACEJ,OAAO;QACPC,OAAO;QACPC,aAAa;IACf;IACA;QACEF,OAAO;QACPC,OAAO;QACPC,aAAa;IACf;IACA;QACEF,OAAO;QACPC,OAAO;QACPC,aAAa;IACf;IACA;QACEF,OAAO;QACPC,OAAO;QACPC,aAAa;IACf;IACA;QACEF,OAAO;QACPC,OAAO;QACPC,aAAa;IACf;IACA;QACEF,OAAO;QACPC,OAAO;QACPC,aACE,0EACA;IACJ;IACA;QACEF,OAAO;QACPC,OAAO;QACPC,aAAa;IACf;IACA;QACEF,OAAO;QACPC,OAAO;QACPC,aAAa;IACf;IACA;QACEF,OAAO;QACPC,OAAO;QACPC,aACE,4EACA;IACJ;CACD,CAAA;AAED;;;;CAIC,GACD,OAAO,SAASG,iBACdC,OAAe,EACfC,aAAsB;IAEtB,OAAOA,gBACH,CAAC,OAAO,EAAED,QAAQ,CAAC,EAAEC,cAAc,EAAE,CAAC,GACtC,CAAC,OAAO,EAAED,QAAQ,EAAE,CAAC;AAC3B;AAEA;;;;;;CAMC,GACD,OAAO,SAASE,kBACdC,KAAmB;QAGGA;IADtB,MAAMC,UAAsC,EAAE;IAC9C,KAAK,MAAMJ,YAAWG,eAAAA,MAAME,KAAK,YAAXF,eAAe,EAAE,CAAE;YAOxBZ;YANDY,eAEAG,aASiBA;QAX/B,MAAMA,SAAQH,gBAAAA,MAAMI,MAAM,qBAAZJ,aAAc,CAACH,QAAQ;QACrC,IAAI,CAACM,OAAO;QACZ,MAAMX,QAAQW,EAAAA,cAAAA,MAAME,IAAI,qBAAVF,YAAYG,IAAI,OAAMjB,uBAAuBQ;QAC3DI,QAAQM,IAAI,CAAC;YACXhB,OAAOK,iBAAiBC;YACxBL;YACAC,WAAW,GAAEL,wCAAAA,yBAAyB,CAACe,MAAMK,IAAI,CAAC,YAArCpB,wCAAyCe,MAAMK,IAAI;QAClE;QACA,uEAAuE;QACvE,0DAA0D;QAC1D,MAAMC,QACJN,MAAMK,IAAI,KAAK,eAAcL,mBAAAA,MAAMO,SAAS,qBAAfP,iBAAiBQ,cAAc,GAAGC;QACjE,IAAIH,OAAO;YACTR,QAAQM,IAAI,CAAC;gBACXhB,OAAOK,iBAAiBC,SAASY;gBACjCjB,OAAO,GAAGA,MAAM,GAAG,EAAEH,uBAAuBoB,QAAQ;gBACpDhB,aAAa;YACf;QACF;IACF;IACA,OAAOQ;AACT"}
|
|
@@ -81,6 +81,24 @@ export declare const COLLECTION_SEARCH_COMPONENT_ID = "collectionSearch";
|
|
|
81
81
|
*/
|
|
82
82
|
export declare const COLLECTION_PAGE_ROUTE_SEGMENT = "page";
|
|
83
83
|
export declare const COLLECTION_CATEGORY_ROUTE_SEGMENT = "category";
|
|
84
|
+
/**
|
|
85
|
+
* `/{collection}/after/{cursor}` — the older page (AGL-3219).
|
|
86
|
+
*
|
|
87
|
+
* ## Why a PATH segment and not `?after=`
|
|
88
|
+
*
|
|
89
|
+
* The tenant catch-all is statically rendered and ISR-cached with the URL
|
|
90
|
+
* PATH as its key (`revalidate = 3600`, AGL-1152). A query string is not part
|
|
91
|
+
* of that key, so every cursor address would share ONE cache entry and serve
|
|
92
|
+
* whichever of them rendered first to everybody — the same reason a routed
|
|
93
|
+
* category is `/{collection}/category/{slug}` rather than `?category=`. And
|
|
94
|
+
* reading `searchParams` at all opts the whole catch-all out of static
|
|
95
|
+
* rendering, which is the opposite of what that page is for.
|
|
96
|
+
*
|
|
97
|
+
* A path segment is cached per address, crawlable, and costs nothing.
|
|
98
|
+
*/
|
|
99
|
+
export declare const COLLECTION_AFTER_ROUTE_SEGMENT = "after";
|
|
100
|
+
/** `/{collection}/before/{cursor}` — the newer page (AGL-3219). */
|
|
101
|
+
export declare const COLLECTION_BEFORE_ROUTE_SEGMENT = "before";
|
|
84
102
|
/** Namespaces cloned template ids per container/entry (cf. `rep__`). */
|
|
85
103
|
export declare const COLLECTION_ENTRIES_NODE_ID_PREFIX = "centry__";
|
|
86
104
|
/** Hard bound on entries a single block renders, before `entriesLimit`. */
|
|
@@ -475,16 +493,25 @@ export declare function collectionListUrl(options: {
|
|
|
475
493
|
collectionSlug: string;
|
|
476
494
|
categorySlug?: string | null;
|
|
477
495
|
page?: number | null;
|
|
496
|
+
/** Continue after this entry's document id — the older direction. */
|
|
497
|
+
after?: string | null;
|
|
498
|
+
/** Continue before this entry's document id — the newer direction. */
|
|
499
|
+
before?: string | null;
|
|
478
500
|
}): string;
|
|
479
501
|
/** Where a listing's pager can go from here (AGL-1386). */
|
|
480
502
|
export interface CollectionPaginationLinks {
|
|
481
503
|
/** 1-based current page; 1 when the listing is unpaginated. */
|
|
482
504
|
page: number;
|
|
483
|
-
/**
|
|
505
|
+
/**
|
|
506
|
+
* Total pages in the CURRENT set.
|
|
507
|
+
*
|
|
508
|
+
* @deprecated AGL-3219 — 0 when it cannot be known, which is any collection
|
|
509
|
+
* past one read's bound. See `CollectionPagination.totalPages`.
|
|
510
|
+
*/
|
|
484
511
|
totalPages: number;
|
|
485
512
|
/** The previous page's URL, or `''` on the first page. */
|
|
486
513
|
prevUrl: string;
|
|
487
|
-
/** The next page's URL, or `''`
|
|
514
|
+
/** The next page's URL, or `''` when nothing older follows. */
|
|
488
515
|
nextUrl: string;
|
|
489
516
|
}
|
|
490
517
|
/**
|
|
@@ -511,8 +538,26 @@ export declare function collectionPaginationLinks(options: {
|
|
|
511
538
|
categorySlug?: string | null;
|
|
512
539
|
/** 1-based current page; anything unusable reads as page 1. */
|
|
513
540
|
page?: number | null;
|
|
514
|
-
/**
|
|
541
|
+
/**
|
|
542
|
+
* Total pages.
|
|
543
|
+
*
|
|
544
|
+
* @deprecated AGL-3219 — no longer consulted for whether a next link
|
|
545
|
+
* exists; {@link collectionPaginationLinks} asks `nextCursor` instead.
|
|
546
|
+
*/
|
|
515
547
|
totalPages?: number | null;
|
|
548
|
+
/**
|
|
549
|
+
* The document id the older page continues from, or `''` when there is no
|
|
550
|
+
* older page (AGL-3219).
|
|
551
|
+
*
|
|
552
|
+
* This is the ONLY thing that decides whether `nextUrl` is a link. It comes
|
|
553
|
+
* from a `limit(perPage + 1)` probe, so an empty string means the read
|
|
554
|
+
* looked for an eleventh entry and there was not one — which is a fact
|
|
555
|
+
* about the collection, unlike a total that two differently-cached reads
|
|
556
|
+
* could disagree about.
|
|
557
|
+
*/
|
|
558
|
+
nextCursor?: string | null;
|
|
559
|
+
/** The document id the newer page continues before, or `''`. */
|
|
560
|
+
prevCursor?: string | null;
|
|
516
561
|
}): CollectionPaginationLinks;
|
|
517
562
|
/** A content-collection path resolved to what it addresses (AGL-1321). */
|
|
518
563
|
export interface CollectionRoute {
|
|
@@ -523,6 +568,10 @@ export interface CollectionRoute {
|
|
|
523
568
|
categorySlug?: string;
|
|
524
569
|
/** 1-based list page; always 1 for entry routes. */
|
|
525
570
|
page: number;
|
|
571
|
+
/** Set only for `/{collection}/after/{cursor}` (AGL-3219). */
|
|
572
|
+
after?: string;
|
|
573
|
+
/** Set only for `/{collection}/before/{cursor}` (AGL-3219). */
|
|
574
|
+
before?: string;
|
|
526
575
|
}
|
|
527
576
|
/**
|
|
528
577
|
* Parses a non-screen path against the content-collection route shapes
|
|
@@ -72,6 +72,22 @@ export * from "./collection-entry-date.js";
|
|
|
72
72
|
* slugged `page` or `category` keeps serving at `/{collection}/{entry}`.
|
|
73
73
|
*/ export const COLLECTION_PAGE_ROUTE_SEGMENT = 'page';
|
|
74
74
|
export const COLLECTION_CATEGORY_ROUTE_SEGMENT = 'category';
|
|
75
|
+
/**
|
|
76
|
+
* `/{collection}/after/{cursor}` — the older page (AGL-3219).
|
|
77
|
+
*
|
|
78
|
+
* ## Why a PATH segment and not `?after=`
|
|
79
|
+
*
|
|
80
|
+
* The tenant catch-all is statically rendered and ISR-cached with the URL
|
|
81
|
+
* PATH as its key (`revalidate = 3600`, AGL-1152). A query string is not part
|
|
82
|
+
* of that key, so every cursor address would share ONE cache entry and serve
|
|
83
|
+
* whichever of them rendered first to everybody — the same reason a routed
|
|
84
|
+
* category is `/{collection}/category/{slug}` rather than `?category=`. And
|
|
85
|
+
* reading `searchParams` at all opts the whole catch-all out of static
|
|
86
|
+
* rendering, which is the opposite of what that page is for.
|
|
87
|
+
*
|
|
88
|
+
* A path segment is cached per address, crawlable, and costs nothing.
|
|
89
|
+
*/ export const COLLECTION_AFTER_ROUTE_SEGMENT = 'after';
|
|
90
|
+
/** `/{collection}/before/{cursor}` — the newer page (AGL-3219). */ export const COLLECTION_BEFORE_ROUTE_SEGMENT = 'before';
|
|
75
91
|
/** Namespaces cloned template ids per container/entry (cf. `rep__`). */ export const COLLECTION_ENTRIES_NODE_ID_PREFIX = 'centry__';
|
|
76
92
|
/** Hard bound on entries a single block renders, before `entriesLimit`. */ export const COLLECTION_ENTRIES_MAX = 100;
|
|
77
93
|
/**
|
|
@@ -446,7 +462,23 @@ categories) {
|
|
|
446
462
|
* a second address for the page that already exists.
|
|
447
463
|
*/ export function collectionListUrl(options) {
|
|
448
464
|
const scoped = options.categorySlug ? `/${options.collectionSlug}/${COLLECTION_CATEGORY_ROUTE_SEGMENT}/` + collectionCategorySlug(options.categorySlug) : `/${options.collectionSlug}`;
|
|
449
|
-
|
|
465
|
+
/*
|
|
466
|
+
* A cursor address (AGL-3219) wins over a page number when both are given,
|
|
467
|
+
* because it is the one that survives a publish at the head of the list.
|
|
468
|
+
*/ const cursor = (options.before || options.after || '').trim();
|
|
469
|
+
if (cursor) {
|
|
470
|
+
const segment = options.before ? COLLECTION_BEFORE_ROUTE_SEGMENT : COLLECTION_AFTER_ROUTE_SEGMENT;
|
|
471
|
+
return `${scoped}/${segment}/${encodeURIComponent(cursor)}`;
|
|
472
|
+
}
|
|
473
|
+
/*
|
|
474
|
+
* `/{collection}/page/{n}` is still MINTED for a collection that fits
|
|
475
|
+
* inside one read, where it is exactly as stable as it ever was: every one
|
|
476
|
+
* of those pages is sliced out of a single cached snapshot, so no publish
|
|
477
|
+
* can move one page's entries relative to another's. It is past the bound —
|
|
478
|
+
* where the head and the tail were two different reads of a moving list —
|
|
479
|
+
* that a number stopped being an address, and there the pager mints
|
|
480
|
+
* cursors instead.
|
|
481
|
+
*/ const page = Number(options.page);
|
|
450
482
|
return Number.isFinite(page) && page > 1 ? `${scoped}/${COLLECTION_PAGE_ROUTE_SEGMENT}/${Math.floor(page)}` : scoped;
|
|
451
483
|
}
|
|
452
484
|
/**
|
|
@@ -468,24 +500,59 @@ categories) {
|
|
|
468
500
|
* template binds. A second derivation is how a fallback and a template start
|
|
469
501
|
* disagreeing about which page they are on.
|
|
470
502
|
*/ export function collectionPaginationLinks(options) {
|
|
503
|
+
var _options_nextCursor, _options_prevCursor;
|
|
471
504
|
const positive = (value)=>{
|
|
472
505
|
const parsed = Math.floor(Number(value));
|
|
473
506
|
return Number.isFinite(parsed) && parsed > 0 ? parsed : 1;
|
|
474
507
|
};
|
|
475
508
|
const page = positive(options.page);
|
|
476
|
-
const
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
509
|
+
const totalPagesRaw = Math.floor(Number(options.totalPages));
|
|
510
|
+
/*
|
|
511
|
+
* Zero means UNKNOWN, and only a caller that deals in cursors can mean it
|
|
512
|
+
* (AGL-3219).
|
|
513
|
+
*
|
|
514
|
+
* A listing past the bound passes cursors and no total, and 0 is how it
|
|
515
|
+
* says the number cannot be had. A caller that passes neither — an entry
|
|
516
|
+
* route, which has no listing at all — is the inert case the tokens have
|
|
517
|
+
* always answered with "page 1 of 1", and it keeps that answer rather than
|
|
518
|
+
* telling an entry template it is on page 1 of 0.
|
|
519
|
+
*/ const knowsCursors = options.nextCursor !== undefined || options.prevCursor !== undefined;
|
|
520
|
+
const totalPages = Number.isFinite(totalPagesRaw) && totalPagesRaw > 0 ? totalPagesRaw : knowsCursors ? 0 : 1;
|
|
521
|
+
const scope = _extends({
|
|
522
|
+
collectionSlug: options.collectionSlug
|
|
523
|
+
}, options.categorySlug ? {
|
|
524
|
+
categorySlug: options.categorySlug
|
|
525
|
+
} : {});
|
|
526
|
+
const next = ((_options_nextCursor = options.nextCursor) != null ? _options_nextCursor : '').trim();
|
|
527
|
+
const prev = ((_options_prevCursor = options.prevCursor) != null ? _options_prevCursor : '').trim();
|
|
528
|
+
/*
|
|
529
|
+
* Numbered while the whole set fits inside ONE read, cursors once it does
|
|
530
|
+
* not (AGL-3219).
|
|
531
|
+
*
|
|
532
|
+
* A total is only ever stated for a collection the shared read holds
|
|
533
|
+
* entirely, and there every page is sliced out of that one cached snapshot:
|
|
534
|
+
* no publish can move one page's entries relative to another's, so a number
|
|
535
|
+
* is a perfectly good address and stays the one readers and crawlers
|
|
536
|
+
* already have. Past the bound the head and the tail were two reads of a
|
|
537
|
+
* moving list, a number stopped naming the same ten entries from one render
|
|
538
|
+
* to the next, and the pager mints the cursor its own page proves instead.
|
|
539
|
+
*/ const numbered = totalPages > 0;
|
|
540
|
+
const numberedHref = (n)=>collectionListUrl(_extends({}, scope, {
|
|
482
541
|
page: n
|
|
483
542
|
}));
|
|
484
543
|
return {
|
|
485
544
|
page,
|
|
486
545
|
totalPages,
|
|
487
|
-
|
|
488
|
-
|
|
546
|
+
/*
|
|
547
|
+
* Page 2 going back lands on the bare listing either way: it is the head
|
|
548
|
+
* of the collection, and `/{collection}` is its address — there is no
|
|
549
|
+
* cursor for "the beginning".
|
|
550
|
+
*/ prevUrl: page === 2 ? collectionListUrl(scope) : page > 2 ? numbered ? numberedHref(page - 1) : prev ? collectionListUrl(_extends({}, scope, {
|
|
551
|
+
before: prev
|
|
552
|
+
})) : '' : '',
|
|
553
|
+
nextUrl: numbered ? page < totalPages ? numberedHref(page + 1) : '' : next ? collectionListUrl(_extends({}, scope, {
|
|
554
|
+
after: next
|
|
555
|
+
})) : ''
|
|
489
556
|
};
|
|
490
557
|
}
|
|
491
558
|
const POSITIVE_INTEGER = /^[1-9]\d*$/;
|
|
@@ -525,6 +592,28 @@ const POSITIVE_INTEGER = /^[1-9]\d*$/;
|
|
|
525
592
|
page: Number(rest[1])
|
|
526
593
|
} : null;
|
|
527
594
|
}
|
|
595
|
+
/*
|
|
596
|
+
* `/{collection}/after/{cursor}` and its `before` twin (AGL-3219).
|
|
597
|
+
*
|
|
598
|
+
* Checked BEFORE the category shape and after the one-segment entry shape,
|
|
599
|
+
* so an entry whose slug happens to be `after` still answers at
|
|
600
|
+
* `/{collection}/after` — that is one segment, and this is two.
|
|
601
|
+
*
|
|
602
|
+
* `page` stays 1: nothing counts a cursor address, and it is the display
|
|
603
|
+
* label rather than a position anything reads back.
|
|
604
|
+
*/ if (rest.length === 2 && (rest[0] === COLLECTION_AFTER_ROUTE_SEGMENT || rest[0] === COLLECTION_BEFORE_ROUTE_SEGMENT) && rest[1]) {
|
|
605
|
+
const cursor = decodeURIComponent(rest[1]).trim();
|
|
606
|
+
if (!cursor) return null;
|
|
607
|
+
return rest[0] === COLLECTION_AFTER_ROUTE_SEGMENT ? {
|
|
608
|
+
collectionSlug,
|
|
609
|
+
after: cursor,
|
|
610
|
+
page: 1
|
|
611
|
+
} : {
|
|
612
|
+
collectionSlug,
|
|
613
|
+
before: cursor,
|
|
614
|
+
page: 1
|
|
615
|
+
};
|
|
616
|
+
}
|
|
528
617
|
if (rest.length === 2 && rest[0] === COLLECTION_CATEGORY_ROUTE_SEGMENT) {
|
|
529
618
|
const categorySlug = collectionCategorySlug(rest[1]);
|
|
530
619
|
return categorySlug ? {
|