@aglyn/tenant-runtime 1.0.0-beta.143
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +201 -0
- package/README.md +26 -0
- package/package.json +39 -0
- package/src/index.d.ts +21 -0
- package/src/index.js +22 -0
- package/src/index.js.map +1 -0
- package/src/lib/apply-publish-schedule.d.ts +34 -0
- package/src/lib/apply-publish-schedule.js +285 -0
- package/src/lib/apply-publish-schedule.js.map +1 -0
- package/src/lib/assign-contact-owner.d.ts +162 -0
- package/src/lib/assign-contact-owner.js +322 -0
- package/src/lib/assign-contact-owner.js.map +1 -0
- package/src/lib/associate-company-by-domain.d.ts +70 -0
- package/src/lib/associate-company-by-domain.js +150 -0
- package/src/lib/associate-company-by-domain.js.map +1 -0
- package/src/lib/author-page-nodes.d.ts +36 -0
- package/src/lib/author-page-nodes.js +296 -0
- package/src/lib/author-page-nodes.js.map +1 -0
- package/src/lib/built-in-page-layout.d.ts +48 -0
- package/src/lib/built-in-page-layout.js +107 -0
- package/src/lib/built-in-page-layout.js.map +1 -0
- package/src/lib/capture-host-contact.d.ts +95 -0
- package/src/lib/capture-host-contact.js +137 -0
- package/src/lib/capture-host-contact.js.map +1 -0
- package/src/lib/collection-fallback-nodes.d.ts +90 -0
- package/src/lib/collection-fallback-nodes.js +602 -0
- package/src/lib/collection-fallback-nodes.js.map +1 -0
- package/src/lib/compose-author-page.d.ts +109 -0
- package/src/lib/compose-author-page.js +225 -0
- package/src/lib/compose-author-page.js.map +1 -0
- package/src/lib/compose-collection-page.d.ts +94 -0
- package/src/lib/compose-collection-page.js +256 -0
- package/src/lib/compose-collection-page.js.map +1 -0
- package/src/lib/compose-screen-nodes.d.ts +156 -0
- package/src/lib/compose-screen-nodes.js +547 -0
- package/src/lib/compose-screen-nodes.js.map +1 -0
- package/src/lib/compose-search-page.d.ts +39 -0
- package/src/lib/compose-search-page.js +57 -0
- package/src/lib/compose-search-page.js.map +1 -0
- package/src/lib/convert-host-lead.d.ts +189 -0
- package/src/lib/convert-host-lead.js +443 -0
- package/src/lib/convert-host-lead.js.map +1 -0
- package/src/lib/defer-lazy-panels.d.ts +58 -0
- package/src/lib/defer-lazy-panels.js +171 -0
- package/src/lib/defer-lazy-panels.js.map +1 -0
- package/src/lib/element-animation-assets.d.ts +62 -0
- package/src/lib/element-animation-assets.js +290 -0
- package/src/lib/element-animation-assets.js.map +1 -0
- package/src/lib/emit-host-event.d.ts +29 -0
- package/src/lib/emit-host-event.js +30 -0
- package/src/lib/emit-host-event.js.map +1 -0
- package/src/lib/enrich-gated-page.d.ts +67 -0
- package/src/lib/enrich-gated-page.js +134 -0
- package/src/lib/enrich-gated-page.js.map +1 -0
- package/src/lib/entry-link-routes.d.ts +61 -0
- package/src/lib/entry-link-routes.js +169 -0
- package/src/lib/entry-link-routes.js.map +1 -0
- package/src/lib/get-author-content.d.ts +119 -0
- package/src/lib/get-author-content.js +288 -0
- package/src/lib/get-author-content.js.map +1 -0
- package/src/lib/get-collection-content.d.ts +290 -0
- package/src/lib/get-collection-content.js +565 -0
- package/src/lib/get-collection-content.js.map +1 -0
- package/src/lib/get-components.d.ts +24 -0
- package/src/lib/get-components.js +102 -0
- package/src/lib/get-components.js.map +1 -0
- package/src/lib/get-datasets.d.ts +45 -0
- package/src/lib/get-datasets.js +169 -0
- package/src/lib/get-datasets.js.map +1 -0
- package/src/lib/get-forms.d.ts +24 -0
- package/src/lib/get-forms.js +116 -0
- package/src/lib/get-forms.js.map +1 -0
- package/src/lib/get-layout-version.d.ts +26 -0
- package/src/lib/get-layout-version.js +99 -0
- package/src/lib/get-layout-version.js.map +1 -0
- package/src/lib/get-media-asset-facts.d.ts +74 -0
- package/src/lib/get-media-asset-facts.js +116 -0
- package/src/lib/get-media-asset-facts.js.map +1 -0
- package/src/lib/get-plugin-installs.d.ts +29 -0
- package/src/lib/get-plugin-installs.js +94 -0
- package/src/lib/get-plugin-installs.js.map +1 -0
- package/src/lib/get-screen-version.d.ts +27 -0
- package/src/lib/get-screen-version.js +66 -0
- package/src/lib/get-screen-version.js.map +1 -0
- package/src/lib/get-screen.d.ts +39 -0
- package/src/lib/get-screen.js +126 -0
- package/src/lib/get-screen.js.map +1 -0
- package/src/lib/get-variables.d.ts +41 -0
- package/src/lib/get-variables.js +128 -0
- package/src/lib/get-variables.js.map +1 -0
- package/src/lib/get-video-asset-facts.d.ts +22 -0
- package/src/lib/get-video-asset-facts.js +22 -0
- package/src/lib/get-video-asset-facts.js.map +1 -0
- package/src/lib/host-event-listeners.d.ts +83 -0
- package/src/lib/host-event-listeners.js +91 -0
- package/src/lib/host-event-listeners.js.map +1 -0
- package/src/lib/org-permissions.d.ts +63 -0
- package/src/lib/org-permissions.js +187 -0
- package/src/lib/org-permissions.js.map +1 -0
- package/src/lib/repeat-record-pages.d.ts +48 -0
- package/src/lib/repeat-record-pages.js +64 -0
- package/src/lib/repeat-record-pages.js.map +1 -0
- package/src/lib/required-site-plugins.d.ts +114 -0
- package/src/lib/required-site-plugins.js +121 -0
- package/src/lib/required-site-plugins.js.map +1 -0
- package/src/lib/resolve-dataset.d.ts +40 -0
- package/src/lib/resolve-dataset.js +64 -0
- package/src/lib/resolve-dataset.js.map +1 -0
- package/src/lib/search-results-nodes.d.ts +48 -0
- package/src/lib/search-results-nodes.js +302 -0
- package/src/lib/search-results-nodes.js.map +1 -0
- package/src/lib/social-image-facts.d.ts +107 -0
- package/src/lib/social-image-facts.js +133 -0
- package/src/lib/social-image-facts.js.map +1 -0
- package/src/lib/stamp-form-dataset-bindings.d.ts +32 -0
- package/src/lib/stamp-form-dataset-bindings.js +59 -0
- package/src/lib/stamp-form-dataset-bindings.js.map +1 -0
- package/src/lib/template-screens.d.ts +227 -0
- package/src/lib/template-screens.js +364 -0
- package/src/lib/template-screens.js.map +1 -0
|
@@ -0,0 +1,121 @@
|
|
|
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
|
+
* Which site plugins a page must have registered *before it can render*
|
|
18
|
+
* (AGL-1289).
|
|
19
|
+
*
|
|
20
|
+
* The tenant client suspends on `sitePluginLoader.ensure(enabledPlugins)`, and
|
|
21
|
+
* `enabledPlugins` is the ORG's list — nothing narrows it to the page. On
|
|
22
|
+
* aglyn.com that is twelve enabled plugins, of which the `/pricing` document
|
|
23
|
+
* uses exactly one: 2,816 of its 2,818 nodes are `mui` and the other two carry
|
|
24
|
+
* no `pluginId` at all. The bundles with a site surface that it does not use —
|
|
25
|
+
* bookings, commerce, email, events-calendar — were downloaded and evaluated
|
|
26
|
+
* in front of first render, in a second request wave measured starting ~1.7 s
|
|
27
|
+
* after the first and finishing at ~4.0 s.
|
|
28
|
+
*
|
|
29
|
+
* A plugin belongs in front of the render for exactly two reasons:
|
|
30
|
+
*
|
|
31
|
+
* 1. **Its components are on the page.** Without it registered the canvas has
|
|
32
|
+
* no component to render for those nodes — the blank-site failure
|
|
33
|
+
* (AGL-52). Read straight off each node's `pluginId`.
|
|
34
|
+
* 2. **It contributed something to this page.** A site runtime only mounts
|
|
35
|
+
* once its plugin registers, so a late one shows its announcement bar a
|
|
36
|
+
* beat after paint and — the case that actually costs something — an
|
|
37
|
+
* experiment runner can paint the control variant before it swaps.
|
|
38
|
+
*
|
|
39
|
+
* The second is why this takes `contributors` rather than inspecting props: it
|
|
40
|
+
* must distinguish "the marketing plugin returned `announcementBar: null`"
|
|
41
|
+
* from "the marketing plugin returned a live bar". Every marketing enricher
|
|
42
|
+
* returns its whole key set on every page, so presence proves nothing;
|
|
43
|
+
* `runSitePageEnrichers` reports only the plugins whose output had content.
|
|
44
|
+
*
|
|
45
|
+
* `/pricing` is the case that shows why the distinction matters. Its
|
|
46
|
+
* `announcementBar`, `popup`, `experiments` and `automationOverlays` are all
|
|
47
|
+
* empty — but its `clientAutomations` is NOT, so `marketing` genuinely has to
|
|
48
|
+
* register early and only the other four move.
|
|
49
|
+
*
|
|
50
|
+
* Everything here fails toward "no narrowing": an unattributable contribution,
|
|
51
|
+
* a missing enabled list, no nodes to read. The cost of refusing is a slower
|
|
52
|
+
* page; the cost of wrongly dropping a plugin is a site that does not render.
|
|
53
|
+
*/ import { isPluginUsedOnPage, pagePresence } from "@aglyn/aglyn/plugin-manager/plugin-contributions";
|
|
54
|
+
/** Registered regardless of the org's list; the canvas cannot render without it. */ export const ALWAYS_ON_SITE_PLUGINS = [
|
|
55
|
+
'mui'
|
|
56
|
+
];
|
|
57
|
+
/**
|
|
58
|
+
* The plugins that must register before first render, or `null` when the page
|
|
59
|
+
* has to keep waiting for the whole enabled set.
|
|
60
|
+
*
|
|
61
|
+
* Always a subset of `enabledPlugins`, in that list's order, so this can never
|
|
62
|
+
* turn on a plugin the org has not enabled.
|
|
63
|
+
*/ export function requiredSitePlugins({ nodes, contributors, unattributed, enabledPlugins }) {
|
|
64
|
+
if (!(enabledPlugins == null ? void 0 : enabledPlugins.length) || !nodes || unattributed) return null;
|
|
65
|
+
const needed = new Set(ALWAYS_ON_SITE_PLUGINS);
|
|
66
|
+
for (const node of Object.values(nodes)){
|
|
67
|
+
if (node == null ? void 0 : node.pluginId) needed.add(node.pluginId);
|
|
68
|
+
}
|
|
69
|
+
for (const id of contributors != null ? contributors : [])needed.add(id);
|
|
70
|
+
const required = enabledPlugins.filter((id)=>needed.has(id));
|
|
71
|
+
// Nothing gained, and an empty list would be a strictly worse thing to hand
|
|
72
|
+
// the loader than the list it already had.
|
|
73
|
+
if (!required.length || required.length === enabledPlugins.length) return null;
|
|
74
|
+
return required;
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* The canvas component ids a published page places (AGL-3141).
|
|
78
|
+
*
|
|
79
|
+
* The loader carries this to each site plugin's register function, and a
|
|
80
|
+
* plugin that can register a part of itself — the MUI bundle, which owns
|
|
81
|
+
* sixty elements where a page renders about nineteen — registers only what
|
|
82
|
+
* is here.
|
|
83
|
+
*
|
|
84
|
+
* Read from the FULL composed document, like `requiredSitePlugins`: an
|
|
85
|
+
* element inside a withheld lazy panel is still this page's element, and the
|
|
86
|
+
* visitor who opens that panel would otherwise find nothing in it.
|
|
87
|
+
*
|
|
88
|
+
* `null` for a page with no document to read, because a narrowing that cannot
|
|
89
|
+
* see the page must not be made: an element whose component never registered
|
|
90
|
+
* renders NOTHING, silently (AGL-52). Sorted so the loader's cache key for a
|
|
91
|
+
* page is the same whatever order the document stored its nodes in.
|
|
92
|
+
*/ export function placedComponentIds(nodes) {
|
|
93
|
+
if (!nodes) return null;
|
|
94
|
+
return [
|
|
95
|
+
...pagePresence(nodes).componentIds
|
|
96
|
+
].sort();
|
|
97
|
+
}
|
|
98
|
+
/**
|
|
99
|
+
* The trusted-realm installs a published page uses (AGL-3116), and only
|
|
100
|
+
* those: the page loads the realm-plugin host — which hands a bundle the
|
|
101
|
+
* whole core namespace — and each bundle only when something on the page
|
|
102
|
+
* uses it.
|
|
103
|
+
*
|
|
104
|
+
* Installation is not use. A workspace that installed a plugin for its
|
|
105
|
+
* console widget used to load the host and the bundle on every page of every
|
|
106
|
+
* site it owns, and the bundle's `register()` then added a widget no
|
|
107
|
+
* published page renders. An install is kept when its version declares a
|
|
108
|
+
* component this page places or a site feature, or when a node is stamped
|
|
109
|
+
* with its id; one that declares nothing is kept only for a stamped node, the
|
|
110
|
+
* default `plugin-contributions.ts` documents.
|
|
111
|
+
*
|
|
112
|
+
* Read from the FULL composed document, like `requiredSitePlugins`: an
|
|
113
|
+
* element inside a withheld lazy panel is still this page's element. A page
|
|
114
|
+
* with no nodes places nothing, so only a site feature can be in use on it.
|
|
115
|
+
*/ export function realmPluginsInUse(installs, nodes) {
|
|
116
|
+
if (!installs.length) return [];
|
|
117
|
+
const page = pagePresence(nodes);
|
|
118
|
+
return installs.filter((install)=>isPluginUsedOnPage(install, page));
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
//# sourceMappingURL=required-site-plugins.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../../../../../libs/tenant/runtime/src/lib/required-site-plugins.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\n/**\n * Which site plugins a page must have registered *before it can render*\n * (AGL-1289).\n *\n * The tenant client suspends on `sitePluginLoader.ensure(enabledPlugins)`, and\n * `enabledPlugins` is the ORG's list — nothing narrows it to the page. On\n * aglyn.com that is twelve enabled plugins, of which the `/pricing` document\n * uses exactly one: 2,816 of its 2,818 nodes are `mui` and the other two carry\n * no `pluginId` at all. The bundles with a site surface that it does not use —\n * bookings, commerce, email, events-calendar — were downloaded and evaluated\n * in front of first render, in a second request wave measured starting ~1.7 s\n * after the first and finishing at ~4.0 s.\n *\n * A plugin belongs in front of the render for exactly two reasons:\n *\n * 1. **Its components are on the page.** Without it registered the canvas has\n * no component to render for those nodes — the blank-site failure\n * (AGL-52). Read straight off each node's `pluginId`.\n * 2. **It contributed something to this page.** A site runtime only mounts\n * once its plugin registers, so a late one shows its announcement bar a\n * beat after paint and — the case that actually costs something — an\n * experiment runner can paint the control variant before it swaps.\n *\n * The second is why this takes `contributors` rather than inspecting props: it\n * must distinguish \"the marketing plugin returned `announcementBar: null`\"\n * from \"the marketing plugin returned a live bar\". Every marketing enricher\n * returns its whole key set on every page, so presence proves nothing;\n * `runSitePageEnrichers` reports only the plugins whose output had content.\n *\n * `/pricing` is the case that shows why the distinction matters. Its\n * `announcementBar`, `popup`, `experiments` and `automationOverlays` are all\n * empty — but its `clientAutomations` is NOT, so `marketing` genuinely has to\n * register early and only the other four move.\n *\n * Everything here fails toward \"no narrowing\": an unattributable contribution,\n * a missing enabled list, no nodes to read. The cost of refusing is a slower\n * page; the cost of wrongly dropping a plugin is a site that does not render.\n */\n\nimport {\n isPluginUsedOnPage,\n pagePresence,\n type PresenceNode,\n} from '@aglyn/aglyn/plugin-manager/plugin-contributions'\nimport type { RealmPluginInstall } from '@aglyn/aglyn/plugin-manager/realm-plugins'\n\n/** Registered regardless of the org's list; the canvas cannot render without it. */\nexport const ALWAYS_ON_SITE_PLUGINS: readonly string[] = ['mui']\n\nexport interface RequiredSitePluginsInput {\n /** The FULL composed document — not one already pruned for deferral. */\n nodes?: Record<string, { pluginId?: string }> | null\n /** Plugins whose enrichers produced non-empty output for this page. */\n contributors?: readonly string[]\n /** True when some contribution could not be traced to a plugin. */\n unattributed?: boolean\n enabledPlugins?: readonly string[] | null\n}\n\n/**\n * The plugins that must register before first render, or `null` when the page\n * has to keep waiting for the whole enabled set.\n *\n * Always a subset of `enabledPlugins`, in that list's order, so this can never\n * turn on a plugin the org has not enabled.\n */\nexport function requiredSitePlugins({\n nodes,\n contributors,\n unattributed,\n enabledPlugins,\n}: RequiredSitePluginsInput): string[] | null {\n if (!enabledPlugins?.length || !nodes || unattributed) return null\n\n const needed = new Set<string>(ALWAYS_ON_SITE_PLUGINS)\n for (const node of Object.values(nodes)) {\n if (node?.pluginId) needed.add(node.pluginId)\n }\n for (const id of contributors ?? []) needed.add(id)\n\n const required = enabledPlugins.filter((id) => needed.has(id))\n // Nothing gained, and an empty list would be a strictly worse thing to hand\n // the loader than the list it already had.\n if (!required.length || required.length === enabledPlugins.length) return null\n return required\n}\n\n/**\n * The canvas component ids a published page places (AGL-3141).\n *\n * The loader carries this to each site plugin's register function, and a\n * plugin that can register a part of itself — the MUI bundle, which owns\n * sixty elements where a page renders about nineteen — registers only what\n * is here.\n *\n * Read from the FULL composed document, like `requiredSitePlugins`: an\n * element inside a withheld lazy panel is still this page's element, and the\n * visitor who opens that panel would otherwise find nothing in it.\n *\n * `null` for a page with no document to read, because a narrowing that cannot\n * see the page must not be made: an element whose component never registered\n * renders NOTHING, silently (AGL-52). Sorted so the loader's cache key for a\n * page is the same whatever order the document stored its nodes in.\n */\nexport function placedComponentIds(\n nodes: Record<string, PresenceNode | null | undefined> | null | undefined,\n): string[] | null {\n if (!nodes) return null\n return [...pagePresence(nodes).componentIds].sort()\n}\n\n/**\n * The trusted-realm installs a published page uses (AGL-3116), and only\n * those: the page loads the realm-plugin host — which hands a bundle the\n * whole core namespace — and each bundle only when something on the page\n * uses it.\n *\n * Installation is not use. A workspace that installed a plugin for its\n * console widget used to load the host and the bundle on every page of every\n * site it owns, and the bundle's `register()` then added a widget no\n * published page renders. An install is kept when its version declares a\n * component this page places or a site feature, or when a node is stamped\n * with its id; one that declares nothing is kept only for a stamped node, the\n * default `plugin-contributions.ts` documents.\n *\n * Read from the FULL composed document, like `requiredSitePlugins`: an\n * element inside a withheld lazy panel is still this page's element. A page\n * with no nodes places nothing, so only a site feature can be in use on it.\n */\nexport function realmPluginsInUse(\n installs: readonly RealmPluginInstall[],\n nodes: Record<string, PresenceNode | null | undefined> | null | undefined,\n): RealmPluginInstall[] {\n if (!installs.length) return []\n const page = pagePresence(nodes)\n return installs.filter((install) => isPluginUsedOnPage(install, page))\n}\n"],"names":["isPluginUsedOnPage","pagePresence","ALWAYS_ON_SITE_PLUGINS","requiredSitePlugins","nodes","contributors","unattributed","enabledPlugins","length","needed","Set","node","Object","values","pluginId","add","id","required","filter","has","placedComponentIds","componentIds","sort","realmPluginsInUse","installs","page","install"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAqCC,GAED,SACEA,kBAAkB,EAClBC,YAAY,QAEP,mDAAkD;AAGzD,kFAAkF,GAClF,OAAO,MAAMC,yBAA4C;IAAC;CAAM,CAAA;AAYhE;;;;;;CAMC,GACD,OAAO,SAASC,oBAAoB,EAClCC,KAAK,EACLC,YAAY,EACZC,YAAY,EACZC,cAAc,EACW;IACzB,IAAI,EAACA,kCAAAA,eAAgBC,MAAM,KAAI,CAACJ,SAASE,cAAc,OAAO;IAE9D,MAAMG,SAAS,IAAIC,IAAYR;IAC/B,KAAK,MAAMS,QAAQC,OAAOC,MAAM,CAACT,OAAQ;QACvC,IAAIO,wBAAAA,KAAMG,QAAQ,EAAEL,OAAOM,GAAG,CAACJ,KAAKG,QAAQ;IAC9C;IACA,KAAK,MAAME,MAAMX,uBAAAA,eAAgB,EAAE,CAAEI,OAAOM,GAAG,CAACC;IAEhD,MAAMC,WAAWV,eAAeW,MAAM,CAAC,CAACF,KAAOP,OAAOU,GAAG,CAACH;IAC1D,4EAA4E;IAC5E,2CAA2C;IAC3C,IAAI,CAACC,SAAST,MAAM,IAAIS,SAAST,MAAM,KAAKD,eAAeC,MAAM,EAAE,OAAO;IAC1E,OAAOS;AACT;AAEA;;;;;;;;;;;;;;;;CAgBC,GACD,OAAO,SAASG,mBACdhB,KAAyE;IAEzE,IAAI,CAACA,OAAO,OAAO;IACnB,OAAO;WAAIH,aAAaG,OAAOiB,YAAY;KAAC,CAACC,IAAI;AACnD;AAEA;;;;;;;;;;;;;;;;;CAiBC,GACD,OAAO,SAASC,kBACdC,QAAuC,EACvCpB,KAAyE;IAEzE,IAAI,CAACoB,SAAShB,MAAM,EAAE,OAAO,EAAE;IAC/B,MAAMiB,OAAOxB,aAAaG;IAC1B,OAAOoB,SAASN,MAAM,CAAC,CAACQ,UAAY1B,mBAAmB0B,SAASD;AAClE"}
|
|
@@ -0,0 +1,40 @@
|
|
|
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
|
+
/**
|
|
18
|
+
* Resolves a dataset by its human name: console-created docs store it as
|
|
19
|
+
* `displayName` (AGL-536); the `name` fallback covers pre-migration docs.
|
|
20
|
+
*
|
|
21
|
+
* `scope` narrows the candidates to what one host may see (AGL-1039). It
|
|
22
|
+
* is applied INSIDE the query rather than to the result, which is what
|
|
23
|
+
* makes the collision case come out right: when a host-scoped dataset and
|
|
24
|
+
* an org-wide one share a display name, an unscoped `limit(1)` would pick
|
|
25
|
+
* one arbitrarily and a post-filter would then reject it — reading as
|
|
26
|
+
* "no such dataset" when a perfectly visible one exists.
|
|
27
|
+
*/
|
|
28
|
+
export declare function findDatasetByName(datasetsRef: FirebaseFirestore.CollectionReference, datasetName: string, scope?: (query: FirebaseFirestore.Query) => FirebaseFirestore.Query): Promise<FirebaseFirestore.QueryDocumentSnapshot | undefined>;
|
|
29
|
+
/**
|
|
30
|
+
* Id-first dataset resolution (AGL-556): a `datasetId` binding is a
|
|
31
|
+
* direct doc get — display names never enter into it, so renamed
|
|
32
|
+
* datasets keep receiving records. When the id is absent or doesn't
|
|
33
|
+
* resolve, the human-name query (AGL-536 semantics) is the legacy
|
|
34
|
+
* fallback. Returns undefined when neither resolves; callers still own
|
|
35
|
+
* the `deletedAt` check.
|
|
36
|
+
*/
|
|
37
|
+
export declare function resolveDatasetDoc(datasetsRef: FirebaseFirestore.CollectionReference, binding: {
|
|
38
|
+
datasetId?: string | null;
|
|
39
|
+
datasetName?: string | null;
|
|
40
|
+
}, hostId: string): Promise<FirebaseFirestore.DocumentSnapshot | undefined>;
|
|
@@ -0,0 +1,64 @@
|
|
|
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
|
+
*/ import * as Aglyn from "@aglyn/aglyn/server";
|
|
17
|
+
/**
|
|
18
|
+
* Resolves a dataset by its human name: console-created docs store it as
|
|
19
|
+
* `displayName` (AGL-536); the `name` fallback covers pre-migration docs.
|
|
20
|
+
*
|
|
21
|
+
* `scope` narrows the candidates to what one host may see (AGL-1039). It
|
|
22
|
+
* is applied INSIDE the query rather than to the result, which is what
|
|
23
|
+
* makes the collision case come out right: when a host-scoped dataset and
|
|
24
|
+
* an org-wide one share a display name, an unscoped `limit(1)` would pick
|
|
25
|
+
* one arbitrarily and a post-filter would then reject it — reading as
|
|
26
|
+
* "no such dataset" when a perfectly visible one exists.
|
|
27
|
+
*/ export async function findDatasetByName(datasetsRef, datasetName, scope) {
|
|
28
|
+
const narrow = scope != null ? scope : (query)=>query;
|
|
29
|
+
const byDisplayName = await narrow(datasetsRef.where('displayName', '==', datasetName)).limit(1).get();
|
|
30
|
+
if (!byDisplayName.empty) return byDisplayName.docs[0];
|
|
31
|
+
return (await narrow(datasetsRef.where('name', '==', datasetName)).limit(1).get()).docs[0];
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Id-first dataset resolution (AGL-556): a `datasetId` binding is a
|
|
35
|
+
* direct doc get — display names never enter into it, so renamed
|
|
36
|
+
* datasets keep receiving records. When the id is absent or doesn't
|
|
37
|
+
* resolve, the human-name query (AGL-536 semantics) is the legacy
|
|
38
|
+
* fallback. Returns undefined when neither resolves; callers still own
|
|
39
|
+
* the `deletedAt` check.
|
|
40
|
+
*/ export async function resolveDatasetDoc(datasetsRef, binding, hostId) {
|
|
41
|
+
var _datasetsRef_parent_parent, _datasetsRef_parent, _binding_datasetId, _binding_datasetName;
|
|
42
|
+
// `hostId` is required rather than optional on purpose (AGL-1039): every
|
|
43
|
+
// caller here writes records into whatever comes back, and an optional
|
|
44
|
+
// scope is one a future caller forgets to pass. The Admin SDK ignores
|
|
45
|
+
// rules, so this is the only thing standing between a client site and
|
|
46
|
+
// another client's dataset.
|
|
47
|
+
const orgScoped = ((_datasetsRef_parent = datasetsRef.parent) == null ? void 0 : (_datasetsRef_parent_parent = _datasetsRef_parent.parent) == null ? void 0 : _datasetsRef_parent_parent.id) === 'orgs';
|
|
48
|
+
const datasetId = (_binding_datasetId = binding.datasetId) == null ? void 0 : _binding_datasetId.trim();
|
|
49
|
+
if (datasetId) {
|
|
50
|
+
const datasetDoc = await datasetsRef.doc(datasetId).get();
|
|
51
|
+
if (datasetDoc.exists && (!orgScoped || Aglyn.visibleToHost(datasetDoc.get('visibleTo'), hostId))) {
|
|
52
|
+
return datasetDoc;
|
|
53
|
+
}
|
|
54
|
+
// A hit this host cannot see is NOT a reason to fall through to the
|
|
55
|
+
// name lookup — that would answer "which dataset is called X" for a
|
|
56
|
+
// binding that already named a specific one.
|
|
57
|
+
if (datasetDoc.exists) return undefined;
|
|
58
|
+
}
|
|
59
|
+
const datasetName = (_binding_datasetName = binding.datasetName) == null ? void 0 : _binding_datasetName.trim();
|
|
60
|
+
if (!datasetName) return undefined;
|
|
61
|
+
return findDatasetByName(datasetsRef, datasetName, orgScoped ? (query)=>query.where('visibleTo', 'array-contains-any', Aglyn.scopeTokensForHost(hostId)) : undefined);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
//# sourceMappingURL=resolve-dataset.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../../../../../libs/tenant/runtime/src/lib/resolve-dataset.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 * as Aglyn from '@aglyn/aglyn/server'\n\n/**\n * Resolves a dataset by its human name: console-created docs store it as\n * `displayName` (AGL-536); the `name` fallback covers pre-migration docs.\n *\n * `scope` narrows the candidates to what one host may see (AGL-1039). It\n * is applied INSIDE the query rather than to the result, which is what\n * makes the collision case come out right: when a host-scoped dataset and\n * an org-wide one share a display name, an unscoped `limit(1)` would pick\n * one arbitrarily and a post-filter would then reject it — reading as\n * \"no such dataset\" when a perfectly visible one exists.\n */\nexport async function findDatasetByName(\n datasetsRef: FirebaseFirestore.CollectionReference,\n datasetName: string,\n scope?: (query: FirebaseFirestore.Query) => FirebaseFirestore.Query,\n): Promise<FirebaseFirestore.QueryDocumentSnapshot | undefined> {\n const narrow = scope ?? ((query: FirebaseFirestore.Query) => query)\n const byDisplayName = await narrow(\n datasetsRef.where('displayName', '==', datasetName),\n )\n .limit(1)\n .get()\n if (!byDisplayName.empty) return byDisplayName.docs[0]\n return (\n await narrow(datasetsRef.where('name', '==', datasetName)).limit(1).get()\n ).docs[0]\n}\n\n/**\n * Id-first dataset resolution (AGL-556): a `datasetId` binding is a\n * direct doc get — display names never enter into it, so renamed\n * datasets keep receiving records. When the id is absent or doesn't\n * resolve, the human-name query (AGL-536 semantics) is the legacy\n * fallback. Returns undefined when neither resolves; callers still own\n * the `deletedAt` check.\n */\nexport async function resolveDatasetDoc(\n datasetsRef: FirebaseFirestore.CollectionReference,\n binding: { datasetId?: string | null; datasetName?: string | null },\n hostId: string,\n): Promise<FirebaseFirestore.DocumentSnapshot | undefined> {\n // `hostId` is required rather than optional on purpose (AGL-1039): every\n // caller here writes records into whatever comes back, and an optional\n // scope is one a future caller forgets to pass. The Admin SDK ignores\n // rules, so this is the only thing standing between a client site and\n // another client's dataset.\n const orgScoped = datasetsRef.parent?.parent?.id === 'orgs'\n const datasetId = binding.datasetId?.trim()\n if (datasetId) {\n const datasetDoc = await datasetsRef.doc(datasetId).get()\n if (\n datasetDoc.exists &&\n (!orgScoped ||\n Aglyn.visibleToHost(datasetDoc.get('visibleTo'), hostId))\n ) {\n return datasetDoc\n }\n // A hit this host cannot see is NOT a reason to fall through to the\n // name lookup — that would answer \"which dataset is called X\" for a\n // binding that already named a specific one.\n if (datasetDoc.exists) return undefined\n }\n const datasetName = binding.datasetName?.trim()\n if (!datasetName) return undefined\n return findDatasetByName(\n datasetsRef,\n datasetName,\n orgScoped\n ? (query) =>\n query.where(\n 'visibleTo',\n 'array-contains-any',\n Aglyn.scopeTokensForHost(hostId),\n )\n : undefined,\n )\n}\n"],"names":["Aglyn","findDatasetByName","datasetsRef","datasetName","scope","narrow","query","byDisplayName","where","limit","get","empty","docs","resolveDatasetDoc","binding","hostId","orgScoped","parent","id","datasetId","trim","datasetDoc","doc","exists","visibleToHost","undefined","scopeTokensForHost"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GAED,YAAYA,WAAW,sBAAqB;AAE5C;;;;;;;;;;CAUC,GACD,OAAO,eAAeC,kBACpBC,WAAkD,EAClDC,WAAmB,EACnBC,KAAmE;IAEnE,MAAMC,SAASD,gBAAAA,QAAU,CAACE,QAAmCA;IAC7D,MAAMC,gBAAgB,MAAMF,OAC1BH,YAAYM,KAAK,CAAC,eAAe,MAAML,cAEtCM,KAAK,CAAC,GACNC,GAAG;IACN,IAAI,CAACH,cAAcI,KAAK,EAAE,OAAOJ,cAAcK,IAAI,CAAC,EAAE;IACtD,OAAO,AACL,CAAA,MAAMP,OAAOH,YAAYM,KAAK,CAAC,QAAQ,MAAML,cAAcM,KAAK,CAAC,GAAGC,GAAG,EAAC,EACxEE,IAAI,CAAC,EAAE;AACX;AAEA;;;;;;;CAOC,GACD,OAAO,eAAeC,kBACpBX,WAAkD,EAClDY,OAAmE,EACnEC,MAAc;QAOIb,4BAAAA,qBACAY,oBAeEA;IArBpB,yEAAyE;IACzE,uEAAuE;IACvE,sEAAsE;IACtE,sEAAsE;IACtE,4BAA4B;IAC5B,MAAME,YAAYd,EAAAA,sBAAAA,YAAYe,MAAM,sBAAlBf,6BAAAA,oBAAoBe,MAAM,qBAA1Bf,2BAA4BgB,EAAE,MAAK;IACrD,MAAMC,aAAYL,qBAAAA,QAAQK,SAAS,qBAAjBL,mBAAmBM,IAAI;IACzC,IAAID,WAAW;QACb,MAAME,aAAa,MAAMnB,YAAYoB,GAAG,CAACH,WAAWT,GAAG;QACvD,IACEW,WAAWE,MAAM,IAChB,CAAA,CAACP,aACAhB,MAAMwB,aAAa,CAACH,WAAWX,GAAG,CAAC,cAAcK,OAAM,GACzD;YACA,OAAOM;QACT;QACA,oEAAoE;QACpE,oEAAoE;QACpE,6CAA6C;QAC7C,IAAIA,WAAWE,MAAM,EAAE,OAAOE;IAChC;IACA,MAAMtB,eAAcW,uBAAAA,QAAQX,WAAW,qBAAnBW,qBAAqBM,IAAI;IAC7C,IAAI,CAACjB,aAAa,OAAOsB;IACzB,OAAOxB,kBACLC,aACAC,aACAa,YACI,CAACV,QACCA,MAAME,KAAK,CACT,aACA,sBACAR,MAAM0B,kBAAkB,CAACX,WAE7BU;AAER"}
|
|
@@ -0,0 +1,48 @@
|
|
|
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/server';
|
|
18
|
+
type NodesMap = Record<string, Aglyn.AglynNodeSchema>;
|
|
19
|
+
export interface SearchResultsNode {
|
|
20
|
+
title: string;
|
|
21
|
+
url: string;
|
|
22
|
+
snippet: string;
|
|
23
|
+
collection?: {
|
|
24
|
+
slug: string;
|
|
25
|
+
title: string;
|
|
26
|
+
};
|
|
27
|
+
date?: string;
|
|
28
|
+
}
|
|
29
|
+
export interface SearchFacetNode {
|
|
30
|
+
key: string;
|
|
31
|
+
label: string;
|
|
32
|
+
count: number;
|
|
33
|
+
}
|
|
34
|
+
export interface SearchResultsNodesOptions {
|
|
35
|
+
query: string;
|
|
36
|
+
results: readonly SearchResultsNode[];
|
|
37
|
+
facets: readonly SearchFacetNode[];
|
|
38
|
+
/** The `?in=` facet in force; `all` (or absent) is the unfiltered view. */
|
|
39
|
+
activeFacet: string;
|
|
40
|
+
/** `all` — passed in so the tenant's route table stays the one source. */
|
|
41
|
+
allFacetKey: string;
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* The results page's node tree, root first. Grafted into a layout's slot by
|
|
45
|
+
* `composeNodesWithChrome`, exactly like a screen's own nodes.
|
|
46
|
+
*/
|
|
47
|
+
export declare function buildSearchResultsNodes(options: SearchResultsNodesOptions): NodesMap;
|
|
48
|
+
export default buildSearchResultsNodes;
|
|
@@ -0,0 +1,302 @@
|
|
|
1
|
+
import { _ as _extends } from "@swc/helpers/_/_extends";
|
|
2
|
+
/**
|
|
3
|
+
* @license
|
|
4
|
+
* Copyright 2026 Aglyn LLC
|
|
5
|
+
*
|
|
6
|
+
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
7
|
+
* you may not use this file except in compliance with the License.
|
|
8
|
+
* You may obtain a copy of the License at
|
|
9
|
+
*
|
|
10
|
+
* http://www.apache.org/licenses/LICENSE-2.0
|
|
11
|
+
*
|
|
12
|
+
* Unless required by applicable law or agreed to in writing, software
|
|
13
|
+
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
14
|
+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
15
|
+
* See the License for the specific language governing permissions and
|
|
16
|
+
* limitations under the License.
|
|
17
|
+
*/ import * as Aglyn from "@aglyn/aglyn/server";
|
|
18
|
+
/**
|
|
19
|
+
* The site-search results page as canvas nodes (AGL-2513).
|
|
20
|
+
*
|
|
21
|
+
* ## Why nodes and not the React component it replaces
|
|
22
|
+
*
|
|
23
|
+
* `/search` rendered a bare MUI `Container` — no header, no nav, no footer —
|
|
24
|
+
* because a React body has nowhere to sit inside a designed layout: the
|
|
25
|
+
* layout's slot is filled at COMPOSE time, with nodes. Every other built-in
|
|
26
|
+
* page that wanted the site's chrome hit the same wall and answered it the
|
|
27
|
+
* same way; `collection-fallback-nodes.ts` (AGL-551) is the precedent this
|
|
28
|
+
* file follows, down to the synthetic id prefix.
|
|
29
|
+
*
|
|
30
|
+
* Building the body as nodes puts search on the one render path everything
|
|
31
|
+
* else uses — the site's theme, its shared layout, its nav and footer, its
|
|
32
|
+
* plugin runtimes — instead of a second, chrome-less one that had to be kept
|
|
33
|
+
* in step by hand.
|
|
34
|
+
*
|
|
35
|
+
* Ids are synthetic and NEVER persisted; the `srx__` prefix keeps them out of
|
|
36
|
+
* every id space an author's document uses.
|
|
37
|
+
*/ const ID_PREFIX = 'srx__';
|
|
38
|
+
const id = (suffix)=>`${ID_PREFIX}${suffix}`;
|
|
39
|
+
const facetHref = (query, key, allKey)=>key === allKey ? `/search?q=${encodeURIComponent(query)}` : `/search?q=${encodeURIComponent(query)}&in=${encodeURIComponent(key)}`;
|
|
40
|
+
/**
|
|
41
|
+
* The results page's node tree, root first. Grafted into a layout's slot by
|
|
42
|
+
* `composeNodesWithChrome`, exactly like a screen's own nodes.
|
|
43
|
+
*/ export function buildSearchResultsNodes(options) {
|
|
44
|
+
const { query, results, facets, activeFacet, allFacetKey } = options;
|
|
45
|
+
const nodes = {};
|
|
46
|
+
const stackChildren = [];
|
|
47
|
+
const push = (nodeId, parentId, node)=>{
|
|
48
|
+
nodes[nodeId] = _extends({
|
|
49
|
+
$id: nodeId,
|
|
50
|
+
parentId
|
|
51
|
+
}, node);
|
|
52
|
+
return nodeId;
|
|
53
|
+
};
|
|
54
|
+
nodes[Aglyn.NODE_ROOT_ID] = {
|
|
55
|
+
$id: Aglyn.NODE_ROOT_ID,
|
|
56
|
+
componentId: 'div',
|
|
57
|
+
nodes: [
|
|
58
|
+
id('container')
|
|
59
|
+
]
|
|
60
|
+
};
|
|
61
|
+
push(id('container'), Aglyn.NODE_ROOT_ID, {
|
|
62
|
+
componentId: 'muiContainer',
|
|
63
|
+
pluginId: 'mui',
|
|
64
|
+
// `lg`, not the `md` a collection article takes: a result list is a list,
|
|
65
|
+
// not a prose measure, and the count tabs wrap early at `md`.
|
|
66
|
+
props: {
|
|
67
|
+
maxWidth: 'lg',
|
|
68
|
+
sx: {
|
|
69
|
+
paddingTop: 6,
|
|
70
|
+
paddingBottom: 8
|
|
71
|
+
}
|
|
72
|
+
},
|
|
73
|
+
nodes: [
|
|
74
|
+
id('stack')
|
|
75
|
+
]
|
|
76
|
+
});
|
|
77
|
+
push(id('stack'), id('container'), {
|
|
78
|
+
componentId: 'muiStack',
|
|
79
|
+
pluginId: 'mui',
|
|
80
|
+
props: {
|
|
81
|
+
spacing: 4
|
|
82
|
+
},
|
|
83
|
+
nodes: stackChildren
|
|
84
|
+
});
|
|
85
|
+
// Heading block: the eyebrow and the question the reader asked.
|
|
86
|
+
push(id('head'), id('stack'), {
|
|
87
|
+
componentId: 'muiStack',
|
|
88
|
+
pluginId: 'mui',
|
|
89
|
+
props: {
|
|
90
|
+
spacing: 1
|
|
91
|
+
},
|
|
92
|
+
nodes: [
|
|
93
|
+
id('eyebrow'),
|
|
94
|
+
id('title')
|
|
95
|
+
]
|
|
96
|
+
});
|
|
97
|
+
stackChildren.push(id('head'));
|
|
98
|
+
push(id('eyebrow'), id('head'), {
|
|
99
|
+
componentId: 'muiTypography',
|
|
100
|
+
pluginId: 'mui',
|
|
101
|
+
props: {
|
|
102
|
+
variant: 'overline',
|
|
103
|
+
children: 'SEARCH',
|
|
104
|
+
sx: {
|
|
105
|
+
color: 'text.secondary',
|
|
106
|
+
letterSpacing: '0.12em'
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
});
|
|
110
|
+
push(id('title'), id('head'), {
|
|
111
|
+
componentId: 'muiTypography',
|
|
112
|
+
pluginId: 'mui',
|
|
113
|
+
props: {
|
|
114
|
+
variant: 'h3',
|
|
115
|
+
component: 'h1',
|
|
116
|
+
children: query ? `Results for “${query}”` : 'Search'
|
|
117
|
+
}
|
|
118
|
+
});
|
|
119
|
+
// The field stays on the page, prefilled. A results page that cannot be
|
|
120
|
+
// refined without going back is a dead end — and `?in=` is deliberately not
|
|
121
|
+
// carried into it: a new query is a new question, and silently keeping the
|
|
122
|
+
// old collection filter is how a reader ends up staring at "no results" for
|
|
123
|
+
// a term the site does match.
|
|
124
|
+
// The width lives on a wrapper, not on the box itself: `searchBox` spreads
|
|
125
|
+
// its remaining props onto a plain `<form>`, so an `sx` handed to it would
|
|
126
|
+
// reach the DOM as an unknown attribute rather than as a style.
|
|
127
|
+
push(id('form'), id('stack'), {
|
|
128
|
+
componentId: 'muiBox',
|
|
129
|
+
pluginId: 'mui',
|
|
130
|
+
props: {
|
|
131
|
+
sx: {
|
|
132
|
+
maxWidth: 560
|
|
133
|
+
}
|
|
134
|
+
},
|
|
135
|
+
nodes: [
|
|
136
|
+
id('form-box')
|
|
137
|
+
]
|
|
138
|
+
});
|
|
139
|
+
stackChildren.push(id('form'));
|
|
140
|
+
push(id('form-box'), id('form'), {
|
|
141
|
+
componentId: 'searchBox',
|
|
142
|
+
pluginId: 'mui',
|
|
143
|
+
props: {
|
|
144
|
+
placeholder: 'Search this site…',
|
|
145
|
+
defaultValue: query
|
|
146
|
+
}
|
|
147
|
+
});
|
|
148
|
+
// Count tabs. `searchResultFacets` returns nothing when every match came
|
|
149
|
+
// from one place — a lone "All · 6" tab reads as a filter over a list it
|
|
150
|
+
// cannot change.
|
|
151
|
+
if (facets.length) {
|
|
152
|
+
const facetIds = facets.map((facet)=>id(`facet-${facet.key}`));
|
|
153
|
+
push(id('facets'), id('stack'), {
|
|
154
|
+
componentId: 'muiStack',
|
|
155
|
+
pluginId: 'mui',
|
|
156
|
+
props: {
|
|
157
|
+
direction: 'row',
|
|
158
|
+
spacing: 1,
|
|
159
|
+
component: 'nav',
|
|
160
|
+
'aria-label': 'Filter results by section',
|
|
161
|
+
sx: {
|
|
162
|
+
flexWrap: 'wrap',
|
|
163
|
+
rowGap: 1
|
|
164
|
+
}
|
|
165
|
+
},
|
|
166
|
+
nodes: facetIds
|
|
167
|
+
});
|
|
168
|
+
stackChildren.push(id('facets'));
|
|
169
|
+
for (const facet of facets){
|
|
170
|
+
const active = facet.key === activeFacet;
|
|
171
|
+
push(id(`facet-${facet.key}`), id('facets'), {
|
|
172
|
+
componentId: 'muiScreenLink',
|
|
173
|
+
pluginId: 'mui',
|
|
174
|
+
props: {
|
|
175
|
+
element: 'a',
|
|
176
|
+
href: facetHref(query, facet.key, allFacetKey),
|
|
177
|
+
size: 'small',
|
|
178
|
+
variant: active ? 'contained' : 'outlined',
|
|
179
|
+
// `muiScreenLink` is a MUI Button, and a Button uppercases its
|
|
180
|
+
// label. A count tab is a label, not a call to action.
|
|
181
|
+
sx: {
|
|
182
|
+
textTransform: 'none'
|
|
183
|
+
},
|
|
184
|
+
// The active tab is the one the list is showing, so it is state, not
|
|
185
|
+
// navigation to somewhere else.
|
|
186
|
+
'aria-current': active ? 'page' : undefined,
|
|
187
|
+
children: `${facet.label} · ${facet.count}`
|
|
188
|
+
}
|
|
189
|
+
});
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
if (!results.length) {
|
|
193
|
+
push(id('empty'), id('stack'), {
|
|
194
|
+
componentId: 'muiTypography',
|
|
195
|
+
pluginId: 'mui',
|
|
196
|
+
props: {
|
|
197
|
+
variant: 'body1',
|
|
198
|
+
sx: {
|
|
199
|
+
color: 'text.secondary'
|
|
200
|
+
},
|
|
201
|
+
children: query ? `Nothing matched “${query}”. Try a different word, or fewer of them.` : 'Type something above to search this site.'
|
|
202
|
+
}
|
|
203
|
+
});
|
|
204
|
+
stackChildren.push(id('empty'));
|
|
205
|
+
return nodes;
|
|
206
|
+
}
|
|
207
|
+
const resultIds = [];
|
|
208
|
+
push(id('results'), id('stack'), {
|
|
209
|
+
componentId: 'muiStack',
|
|
210
|
+
pluginId: 'mui',
|
|
211
|
+
props: {
|
|
212
|
+
spacing: 3,
|
|
213
|
+
component: 'ul',
|
|
214
|
+
sx: {
|
|
215
|
+
listStyle: 'none',
|
|
216
|
+
p: 0,
|
|
217
|
+
m: 0
|
|
218
|
+
}
|
|
219
|
+
},
|
|
220
|
+
nodes: resultIds
|
|
221
|
+
});
|
|
222
|
+
stackChildren.push(id('results'));
|
|
223
|
+
results.forEach((result, index)=>{
|
|
224
|
+
var _result_collection;
|
|
225
|
+
const itemId = id(`item-${index}`);
|
|
226
|
+
const children = [
|
|
227
|
+
id(`item-${index}-link`)
|
|
228
|
+
];
|
|
229
|
+
resultIds.push(itemId);
|
|
230
|
+
push(itemId, id('results'), {
|
|
231
|
+
componentId: 'muiStack',
|
|
232
|
+
pluginId: 'mui',
|
|
233
|
+
props: {
|
|
234
|
+
spacing: 0.5,
|
|
235
|
+
component: 'li'
|
|
236
|
+
},
|
|
237
|
+
nodes: children
|
|
238
|
+
});
|
|
239
|
+
push(id(`item-${index}-link`), itemId, {
|
|
240
|
+
componentId: 'muiScreenLink',
|
|
241
|
+
pluginId: 'mui',
|
|
242
|
+
props: {
|
|
243
|
+
element: 'a',
|
|
244
|
+
href: result.url,
|
|
245
|
+
variant: 'text',
|
|
246
|
+
sx: {
|
|
247
|
+
p: 0,
|
|
248
|
+
justifyContent: 'flex-start',
|
|
249
|
+
textAlign: 'left',
|
|
250
|
+
// Same Button inheritance as the tabs above, and it matters more
|
|
251
|
+
// here: a result's title is the author's own words, and the
|
|
252
|
+
// uppercase default reprints every one of them shouting.
|
|
253
|
+
textTransform: 'none',
|
|
254
|
+
fontSize: '1.0625rem',
|
|
255
|
+
fontWeight: 600
|
|
256
|
+
},
|
|
257
|
+
children: result.title
|
|
258
|
+
}
|
|
259
|
+
});
|
|
260
|
+
// The meta line is one string rather than a row of nodes: it is read as a
|
|
261
|
+
// sentence ("Blog · 12 August 2026"), and a wrapping row of separate
|
|
262
|
+
// elements breaks it in the middle on a phone.
|
|
263
|
+
const meta = [
|
|
264
|
+
(_result_collection = result.collection) == null ? void 0 : _result_collection.title,
|
|
265
|
+
result.date
|
|
266
|
+
].filter(Boolean).join(' · ');
|
|
267
|
+
if (meta) {
|
|
268
|
+
const metaId = id(`item-${index}-meta`);
|
|
269
|
+
children.push(metaId);
|
|
270
|
+
push(metaId, itemId, {
|
|
271
|
+
componentId: 'muiTypography',
|
|
272
|
+
pluginId: 'mui',
|
|
273
|
+
props: {
|
|
274
|
+
variant: 'caption',
|
|
275
|
+
sx: {
|
|
276
|
+
color: 'text.secondary'
|
|
277
|
+
},
|
|
278
|
+
children: meta
|
|
279
|
+
}
|
|
280
|
+
});
|
|
281
|
+
}
|
|
282
|
+
if (result.snippet) {
|
|
283
|
+
const snippetId = id(`item-${index}-snippet`);
|
|
284
|
+
children.push(snippetId);
|
|
285
|
+
push(snippetId, itemId, {
|
|
286
|
+
componentId: 'muiTypography',
|
|
287
|
+
pluginId: 'mui',
|
|
288
|
+
props: {
|
|
289
|
+
variant: 'body2',
|
|
290
|
+
sx: {
|
|
291
|
+
color: 'text.secondary'
|
|
292
|
+
},
|
|
293
|
+
children: result.snippet
|
|
294
|
+
}
|
|
295
|
+
});
|
|
296
|
+
}
|
|
297
|
+
});
|
|
298
|
+
return nodes;
|
|
299
|
+
}
|
|
300
|
+
export default buildSearchResultsNodes;
|
|
301
|
+
|
|
302
|
+
//# sourceMappingURL=search-results-nodes.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../../../../../libs/tenant/runtime/src/lib/search-results-nodes.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 * as Aglyn from '@aglyn/aglyn/server'\n\n/**\n * The site-search results page as canvas nodes (AGL-2513).\n *\n * ## Why nodes and not the React component it replaces\n *\n * `/search` rendered a bare MUI `Container` — no header, no nav, no footer —\n * because a React body has nowhere to sit inside a designed layout: the\n * layout's slot is filled at COMPOSE time, with nodes. Every other built-in\n * page that wanted the site's chrome hit the same wall and answered it the\n * same way; `collection-fallback-nodes.ts` (AGL-551) is the precedent this\n * file follows, down to the synthetic id prefix.\n *\n * Building the body as nodes puts search on the one render path everything\n * else uses — the site's theme, its shared layout, its nav and footer, its\n * plugin runtimes — instead of a second, chrome-less one that had to be kept\n * in step by hand.\n *\n * Ids are synthetic and NEVER persisted; the `srx__` prefix keeps them out of\n * every id space an author's document uses.\n */\n\nconst ID_PREFIX = 'srx__'\nconst id = (suffix: string) => `${ID_PREFIX}${suffix}`\n\ntype NodesMap = Record<string, Aglyn.AglynNodeSchema>\n\nexport interface SearchResultsNode {\n title: string\n url: string\n snippet: string\n collection?: { slug: string; title: string }\n date?: string\n}\n\nexport interface SearchFacetNode {\n key: string\n label: string\n count: number\n}\n\nexport interface SearchResultsNodesOptions {\n query: string\n results: readonly SearchResultsNode[]\n facets: readonly SearchFacetNode[]\n /** The `?in=` facet in force; `all` (or absent) is the unfiltered view. */\n activeFacet: string\n /** `all` — passed in so the tenant's route table stays the one source. */\n allFacetKey: string\n}\n\nconst facetHref = (query: string, key: string, allKey: string) =>\n key === allKey\n ? `/search?q=${encodeURIComponent(query)}`\n : `/search?q=${encodeURIComponent(query)}&in=${encodeURIComponent(key)}`\n\n/**\n * The results page's node tree, root first. Grafted into a layout's slot by\n * `composeNodesWithChrome`, exactly like a screen's own nodes.\n */\nexport function buildSearchResultsNodes(\n options: SearchResultsNodesOptions,\n): NodesMap {\n const { query, results, facets, activeFacet, allFacetKey } = options\n const nodes: NodesMap = {}\n const stackChildren: string[] = []\n\n const push = (\n nodeId: string,\n parentId: string,\n node: Omit<Aglyn.AglynNodeSchema, '$id' | 'parentId'>,\n ) => {\n nodes[nodeId] = { $id: nodeId, parentId, ...node } as Aglyn.AglynNodeSchema\n return nodeId\n }\n\n nodes[Aglyn.NODE_ROOT_ID] = {\n $id: Aglyn.NODE_ROOT_ID,\n componentId: 'div',\n nodes: [id('container')],\n } as Aglyn.AglynNodeSchema\n push(id('container'), Aglyn.NODE_ROOT_ID, {\n componentId: 'muiContainer',\n pluginId: 'mui',\n // `lg`, not the `md` a collection article takes: a result list is a list,\n // not a prose measure, and the count tabs wrap early at `md`.\n props: { maxWidth: 'lg', sx: { paddingTop: 6, paddingBottom: 8 } },\n nodes: [id('stack')],\n })\n push(id('stack'), id('container'), {\n componentId: 'muiStack',\n pluginId: 'mui',\n props: { spacing: 4 },\n nodes: stackChildren,\n })\n\n // Heading block: the eyebrow and the question the reader asked.\n push(id('head'), id('stack'), {\n componentId: 'muiStack',\n pluginId: 'mui',\n props: { spacing: 1 },\n nodes: [id('eyebrow'), id('title')],\n })\n stackChildren.push(id('head'))\n push(id('eyebrow'), id('head'), {\n componentId: 'muiTypography',\n pluginId: 'mui',\n props: {\n variant: 'overline',\n children: 'SEARCH',\n sx: { color: 'text.secondary', letterSpacing: '0.12em' },\n },\n })\n push(id('title'), id('head'), {\n componentId: 'muiTypography',\n pluginId: 'mui',\n props: {\n variant: 'h3',\n component: 'h1',\n children: query ? `Results for “${query}”` : 'Search',\n },\n })\n\n // The field stays on the page, prefilled. A results page that cannot be\n // refined without going back is a dead end — and `?in=` is deliberately not\n // carried into it: a new query is a new question, and silently keeping the\n // old collection filter is how a reader ends up staring at \"no results\" for\n // a term the site does match.\n // The width lives on a wrapper, not on the box itself: `searchBox` spreads\n // its remaining props onto a plain `<form>`, so an `sx` handed to it would\n // reach the DOM as an unknown attribute rather than as a style.\n push(id('form'), id('stack'), {\n componentId: 'muiBox',\n pluginId: 'mui',\n props: { sx: { maxWidth: 560 } },\n nodes: [id('form-box')],\n })\n stackChildren.push(id('form'))\n push(id('form-box'), id('form'), {\n componentId: 'searchBox',\n pluginId: 'mui',\n props: { placeholder: 'Search this site…', defaultValue: query },\n })\n\n // Count tabs. `searchResultFacets` returns nothing when every match came\n // from one place — a lone \"All · 6\" tab reads as a filter over a list it\n // cannot change.\n if (facets.length) {\n const facetIds = facets.map((facet) => id(`facet-${facet.key}`))\n push(id('facets'), id('stack'), {\n componentId: 'muiStack',\n pluginId: 'mui',\n props: {\n direction: 'row',\n spacing: 1,\n component: 'nav',\n 'aria-label': 'Filter results by section',\n sx: { flexWrap: 'wrap', rowGap: 1 },\n },\n nodes: facetIds,\n })\n stackChildren.push(id('facets'))\n for (const facet of facets) {\n const active = facet.key === activeFacet\n push(id(`facet-${facet.key}`), id('facets'), {\n componentId: 'muiScreenLink',\n pluginId: 'mui',\n props: {\n element: 'a',\n href: facetHref(query, facet.key, allFacetKey),\n size: 'small',\n variant: active ? 'contained' : 'outlined',\n // `muiScreenLink` is a MUI Button, and a Button uppercases its\n // label. A count tab is a label, not a call to action.\n sx: { textTransform: 'none' },\n // The active tab is the one the list is showing, so it is state, not\n // navigation to somewhere else.\n 'aria-current': active ? 'page' : undefined,\n children: `${facet.label} · ${facet.count}`,\n },\n })\n }\n }\n\n if (!results.length) {\n push(id('empty'), id('stack'), {\n componentId: 'muiTypography',\n pluginId: 'mui',\n props: {\n variant: 'body1',\n sx: { color: 'text.secondary' },\n children: query\n ? `Nothing matched “${query}”. Try a different word, or fewer of them.`\n : 'Type something above to search this site.',\n },\n })\n stackChildren.push(id('empty'))\n return nodes\n }\n\n const resultIds: string[] = []\n push(id('results'), id('stack'), {\n componentId: 'muiStack',\n pluginId: 'mui',\n props: { spacing: 3, component: 'ul', sx: { listStyle: 'none', p: 0, m: 0 } },\n nodes: resultIds,\n })\n stackChildren.push(id('results'))\n\n results.forEach((result, index) => {\n const itemId = id(`item-${index}`)\n const children = [id(`item-${index}-link`)]\n resultIds.push(itemId)\n push(itemId, id('results'), {\n componentId: 'muiStack',\n pluginId: 'mui',\n props: { spacing: 0.5, component: 'li' },\n nodes: children,\n })\n push(id(`item-${index}-link`), itemId, {\n componentId: 'muiScreenLink',\n pluginId: 'mui',\n props: {\n element: 'a',\n href: result.url,\n variant: 'text',\n sx: {\n p: 0,\n justifyContent: 'flex-start',\n textAlign: 'left',\n // Same Button inheritance as the tabs above, and it matters more\n // here: a result's title is the author's own words, and the\n // uppercase default reprints every one of them shouting.\n textTransform: 'none',\n fontSize: '1.0625rem',\n fontWeight: 600,\n },\n children: result.title,\n },\n })\n // The meta line is one string rather than a row of nodes: it is read as a\n // sentence (\"Blog · 12 August 2026\"), and a wrapping row of separate\n // elements breaks it in the middle on a phone.\n const meta = [result.collection?.title, result.date]\n .filter(Boolean)\n .join(' · ')\n if (meta) {\n const metaId = id(`item-${index}-meta`)\n children.push(metaId)\n push(metaId, itemId, {\n componentId: 'muiTypography',\n pluginId: 'mui',\n props: {\n variant: 'caption',\n sx: { color: 'text.secondary' },\n children: meta,\n },\n })\n }\n if (result.snippet) {\n const snippetId = id(`item-${index}-snippet`)\n children.push(snippetId)\n push(snippetId, itemId, {\n componentId: 'muiTypography',\n pluginId: 'mui',\n props: {\n variant: 'body2',\n sx: { color: 'text.secondary' },\n children: result.snippet,\n },\n })\n }\n })\n\n return nodes\n}\n\nexport default buildSearchResultsNodes\n"],"names":["Aglyn","ID_PREFIX","id","suffix","facetHref","query","key","allKey","encodeURIComponent","buildSearchResultsNodes","options","results","facets","activeFacet","allFacetKey","nodes","stackChildren","push","nodeId","parentId","node","$id","NODE_ROOT_ID","componentId","pluginId","props","maxWidth","sx","paddingTop","paddingBottom","spacing","variant","children","color","letterSpacing","component","placeholder","defaultValue","length","facetIds","map","facet","direction","flexWrap","rowGap","active","element","href","size","textTransform","undefined","label","count","resultIds","listStyle","p","m","forEach","result","index","itemId","url","justifyContent","textAlign","fontSize","fontWeight","title","meta","collection","date","filter","Boolean","join","metaId","snippet","snippetId"],"mappings":";AAAA;;;;;;;;;;;;;;;CAeC,GAED,YAAYA,WAAW,sBAAqB;AAE5C;;;;;;;;;;;;;;;;;;;CAmBC,GAED,MAAMC,YAAY;AAClB,MAAMC,KAAK,CAACC,SAAmB,GAAGF,YAAYE,QAAQ;AA4BtD,MAAMC,YAAY,CAACC,OAAeC,KAAaC,SAC7CD,QAAQC,SACJ,CAAC,UAAU,EAAEC,mBAAmBH,QAAQ,GACxC,CAAC,UAAU,EAAEG,mBAAmBH,OAAO,IAAI,EAAEG,mBAAmBF,MAAM;AAE5E;;;CAGC,GACD,OAAO,SAASG,wBACdC,OAAkC;IAElC,MAAM,EAAEL,KAAK,EAAEM,OAAO,EAAEC,MAAM,EAAEC,WAAW,EAAEC,WAAW,EAAE,GAAGJ;IAC7D,MAAMK,QAAkB,CAAC;IACzB,MAAMC,gBAA0B,EAAE;IAElC,MAAMC,OAAO,CACXC,QACAC,UACAC;QAEAL,KAAK,CAACG,OAAO,GAAG;YAAEG,KAAKH;YAAQC;WAAaC;QAC5C,OAAOF;IACT;IAEAH,KAAK,CAACf,MAAMsB,YAAY,CAAC,GAAG;QAC1BD,KAAKrB,MAAMsB,YAAY;QACvBC,aAAa;QACbR,OAAO;YAACb,GAAG;SAAa;IAC1B;IACAe,KAAKf,GAAG,cAAcF,MAAMsB,YAAY,EAAE;QACxCC,aAAa;QACbC,UAAU;QACV,0EAA0E;QAC1E,8DAA8D;QAC9DC,OAAO;YAAEC,UAAU;YAAMC,IAAI;gBAAEC,YAAY;gBAAGC,eAAe;YAAE;QAAE;QACjEd,OAAO;YAACb,GAAG;SAAS;IACtB;IACAe,KAAKf,GAAG,UAAUA,GAAG,cAAc;QACjCqB,aAAa;QACbC,UAAU;QACVC,OAAO;YAAEK,SAAS;QAAE;QACpBf,OAAOC;IACT;IAEA,gEAAgE;IAChEC,KAAKf,GAAG,SAASA,GAAG,UAAU;QAC5BqB,aAAa;QACbC,UAAU;QACVC,OAAO;YAAEK,SAAS;QAAE;QACpBf,OAAO;YAACb,GAAG;YAAYA,GAAG;SAAS;IACrC;IACAc,cAAcC,IAAI,CAACf,GAAG;IACtBe,KAAKf,GAAG,YAAYA,GAAG,SAAS;QAC9BqB,aAAa;QACbC,UAAU;QACVC,OAAO;YACLM,SAAS;YACTC,UAAU;YACVL,IAAI;gBAAEM,OAAO;gBAAkBC,eAAe;YAAS;QACzD;IACF;IACAjB,KAAKf,GAAG,UAAUA,GAAG,SAAS;QAC5BqB,aAAa;QACbC,UAAU;QACVC,OAAO;YACLM,SAAS;YACTI,WAAW;YACXH,UAAU3B,QAAQ,CAAC,aAAa,EAAEA,MAAM,CAAC,CAAC,GAAG;QAC/C;IACF;IAEA,wEAAwE;IACxE,4EAA4E;IAC5E,2EAA2E;IAC3E,4EAA4E;IAC5E,8BAA8B;IAC9B,2EAA2E;IAC3E,2EAA2E;IAC3E,gEAAgE;IAChEY,KAAKf,GAAG,SAASA,GAAG,UAAU;QAC5BqB,aAAa;QACbC,UAAU;QACVC,OAAO;YAAEE,IAAI;gBAAED,UAAU;YAAI;QAAE;QAC/BX,OAAO;YAACb,GAAG;SAAY;IACzB;IACAc,cAAcC,IAAI,CAACf,GAAG;IACtBe,KAAKf,GAAG,aAAaA,GAAG,SAAS;QAC/BqB,aAAa;QACbC,UAAU;QACVC,OAAO;YAAEW,aAAa;YAAqBC,cAAchC;QAAM;IACjE;IAEA,yEAAyE;IACzE,yEAAyE;IACzE,iBAAiB;IACjB,IAAIO,OAAO0B,MAAM,EAAE;QACjB,MAAMC,WAAW3B,OAAO4B,GAAG,CAAC,CAACC,QAAUvC,GAAG,CAAC,MAAM,EAAEuC,MAAMnC,GAAG,EAAE;QAC9DW,KAAKf,GAAG,WAAWA,GAAG,UAAU;YAC9BqB,aAAa;YACbC,UAAU;YACVC,OAAO;gBACLiB,WAAW;gBACXZ,SAAS;gBACTK,WAAW;gBACX,cAAc;gBACdR,IAAI;oBAAEgB,UAAU;oBAAQC,QAAQ;gBAAE;YACpC;YACA7B,OAAOwB;QACT;QACAvB,cAAcC,IAAI,CAACf,GAAG;QACtB,KAAK,MAAMuC,SAAS7B,OAAQ;YAC1B,MAAMiC,SAASJ,MAAMnC,GAAG,KAAKO;YAC7BI,KAAKf,GAAG,CAAC,MAAM,EAAEuC,MAAMnC,GAAG,EAAE,GAAGJ,GAAG,WAAW;gBAC3CqB,aAAa;gBACbC,UAAU;gBACVC,OAAO;oBACLqB,SAAS;oBACTC,MAAM3C,UAAUC,OAAOoC,MAAMnC,GAAG,EAAEQ;oBAClCkC,MAAM;oBACNjB,SAASc,SAAS,cAAc;oBAChC,+DAA+D;oBAC/D,uDAAuD;oBACvDlB,IAAI;wBAAEsB,eAAe;oBAAO;oBAC5B,qEAAqE;oBACrE,gCAAgC;oBAChC,gBAAgBJ,SAAS,SAASK;oBAClClB,UAAU,GAAGS,MAAMU,KAAK,CAAC,GAAG,EAAEV,MAAMW,KAAK,EAAE;gBAC7C;YACF;QACF;IACF;IAEA,IAAI,CAACzC,QAAQ2B,MAAM,EAAE;QACnBrB,KAAKf,GAAG,UAAUA,GAAG,UAAU;YAC7BqB,aAAa;YACbC,UAAU;YACVC,OAAO;gBACLM,SAAS;gBACTJ,IAAI;oBAAEM,OAAO;gBAAiB;gBAC9BD,UAAU3B,QACN,CAAC,iBAAiB,EAAEA,MAAM,0CAA0C,CAAC,GACrE;YACN;QACF;QACAW,cAAcC,IAAI,CAACf,GAAG;QACtB,OAAOa;IACT;IAEA,MAAMsC,YAAsB,EAAE;IAC9BpC,KAAKf,GAAG,YAAYA,GAAG,UAAU;QAC/BqB,aAAa;QACbC,UAAU;QACVC,OAAO;YAAEK,SAAS;YAAGK,WAAW;YAAMR,IAAI;gBAAE2B,WAAW;gBAAQC,GAAG;gBAAGC,GAAG;YAAE;QAAE;QAC5EzC,OAAOsC;IACT;IACArC,cAAcC,IAAI,CAACf,GAAG;IAEtBS,QAAQ8C,OAAO,CAAC,CAACC,QAAQC;YAkCTD;QAjCd,MAAME,SAAS1D,GAAG,CAAC,KAAK,EAAEyD,OAAO;QACjC,MAAM3B,WAAW;YAAC9B,GAAG,CAAC,KAAK,EAAEyD,MAAM,KAAK,CAAC;SAAE;QAC3CN,UAAUpC,IAAI,CAAC2C;QACf3C,KAAK2C,QAAQ1D,GAAG,YAAY;YAC1BqB,aAAa;YACbC,UAAU;YACVC,OAAO;gBAAEK,SAAS;gBAAKK,WAAW;YAAK;YACvCpB,OAAOiB;QACT;QACAf,KAAKf,GAAG,CAAC,KAAK,EAAEyD,MAAM,KAAK,CAAC,GAAGC,QAAQ;YACrCrC,aAAa;YACbC,UAAU;YACVC,OAAO;gBACLqB,SAAS;gBACTC,MAAMW,OAAOG,GAAG;gBAChB9B,SAAS;gBACTJ,IAAI;oBACF4B,GAAG;oBACHO,gBAAgB;oBAChBC,WAAW;oBACX,iEAAiE;oBACjE,4DAA4D;oBAC5D,yDAAyD;oBACzDd,eAAe;oBACfe,UAAU;oBACVC,YAAY;gBACd;gBACAjC,UAAU0B,OAAOQ,KAAK;YACxB;QACF;QACA,0EAA0E;QAC1E,qEAAqE;QACrE,+CAA+C;QAC/C,MAAMC,OAAO;aAACT,qBAAAA,OAAOU,UAAU,qBAAjBV,mBAAmBQ,KAAK;YAAER,OAAOW,IAAI;SAAC,CACjDC,MAAM,CAACC,SACPC,IAAI,CAAC;QACR,IAAIL,MAAM;YACR,MAAMM,SAASvE,GAAG,CAAC,KAAK,EAAEyD,MAAM,KAAK,CAAC;YACtC3B,SAASf,IAAI,CAACwD;YACdxD,KAAKwD,QAAQb,QAAQ;gBACnBrC,aAAa;gBACbC,UAAU;gBACVC,OAAO;oBACLM,SAAS;oBACTJ,IAAI;wBAAEM,OAAO;oBAAiB;oBAC9BD,UAAUmC;gBACZ;YACF;QACF;QACA,IAAIT,OAAOgB,OAAO,EAAE;YAClB,MAAMC,YAAYzE,GAAG,CAAC,KAAK,EAAEyD,MAAM,QAAQ,CAAC;YAC5C3B,SAASf,IAAI,CAAC0D;YACd1D,KAAK0D,WAAWf,QAAQ;gBACtBrC,aAAa;gBACbC,UAAU;gBACVC,OAAO;oBACLM,SAAS;oBACTJ,IAAI;wBAAEM,OAAO;oBAAiB;oBAC9BD,UAAU0B,OAAOgB,OAAO;gBAC1B;YACF;QACF;IACF;IAEA,OAAO3D;AACT;AAEA,eAAeN,wBAAuB"}
|