@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,171 @@
|
|
|
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
|
+
*/ /**
|
|
18
|
+
* Withholds the node definitions inside lazy tab panels that will not mount on
|
|
19
|
+
* first render (AGL-1285).
|
|
20
|
+
*
|
|
21
|
+
* Lazy mounting (AGL-1283) already stops a Tabs container rendering anything
|
|
22
|
+
* but the selected panel. The document did not follow: every panel's nodes were
|
|
23
|
+
* still serialized into the page. On `/pricing` that is 1,489 of 2,523 nodes —
|
|
24
|
+
* eight copies of a 50-row table — and the RSC payload is 63% of the page's
|
|
25
|
+
* gzipped weight, so the render knowing they are unneeded was worth nothing.
|
|
26
|
+
*
|
|
27
|
+
* This prunes them server-side. The panel node stays (its tab, its aria wiring
|
|
28
|
+
* and its `label` are all still needed); only its subtree is withheld, and the
|
|
29
|
+
* panel is marked so the client can fetch the rest when the reader first opens
|
|
30
|
+
* one.
|
|
31
|
+
*
|
|
32
|
+
* Lazy is the DEFAULT (AGL-1283 option 3), so this prunes every multi-panel
|
|
33
|
+
* Tabs unless the author set `ssrPanels` — the same predicate the client
|
|
34
|
+
* mount uses (`tabs.tsx`), and the two MUST agree: a panel this function
|
|
35
|
+
* withholds that the client would render arrives empty, and a panel it ships
|
|
36
|
+
* that the client defers is payload for nothing. The legacy `lazyPanels`
|
|
37
|
+
* opt-in is subsumed by the default and no longer consulted.
|
|
38
|
+
*
|
|
39
|
+
* WHICH panel survives follows the author's `opensOn` (AGL-3164) by the same
|
|
40
|
+
* rule the element applies, for the same reason: the tab that opens first is
|
|
41
|
+
* the one whose panel has to be in the page source, or the page paints an
|
|
42
|
+
* empty panel and fills it after a round trip.
|
|
43
|
+
*
|
|
44
|
+
* SEO note: withheld content is not in the HTML. Where a panel holds content
|
|
45
|
+
* that appears nowhere else on the page and must reach crawlers, `ssrPanels`
|
|
46
|
+
* is the escape hatch — it keeps the panels in the payload AND in the markup.
|
|
47
|
+
*/ /** Marks a panel whose children were withheld. */ export const DEFERRED_PANEL_PROP = 'aglynDeferred';
|
|
48
|
+
const TABS_ID = 'muiTabs';
|
|
49
|
+
const TAB_PANEL_ID = 'muiTabPanel';
|
|
50
|
+
/**
|
|
51
|
+
* Label parsing is duplicated from the mui plugin's `parseLabels` rather than
|
|
52
|
+
* imported: tenant-runtime must not depend on a plugin bundle (module
|
|
53
|
+
* boundaries), and this needs to agree with it exactly or the wrong panel gets
|
|
54
|
+
* pruned. `defer-lazy-panels.spec.ts` pins the cases that matter.
|
|
55
|
+
*/ function parseLabels(value) {
|
|
56
|
+
if (value == null) return [];
|
|
57
|
+
return String(value).split(/[\n,]/).map((label)=>label.trim()).filter(Boolean);
|
|
58
|
+
}
|
|
59
|
+
const labelsMatch = (a, b)=>String(a != null ? a : '').trim().toLowerCase() === String(b != null ? b : '').trim().toLowerCase();
|
|
60
|
+
/**
|
|
61
|
+
* The label the strip opens on, which is NOT always the first one.
|
|
62
|
+
*
|
|
63
|
+
* Two rules, and they must be the mui plugin's `openingTabIndex` exactly —
|
|
64
|
+
* this is the function that decides whose nodes SURVIVE into the page, so a
|
|
65
|
+
* disagreement does not merely paint the wrong panel first, it ships the
|
|
66
|
+
* opening panel empty:
|
|
67
|
+
*
|
|
68
|
+
* 1. the tab the author named in `opensOn` (AGL-3164), refused when that tab
|
|
69
|
+
* carries a screen link, because a linked tab navigates instead of
|
|
70
|
+
* revealing a panel;
|
|
71
|
+
* 2. otherwise the first tab WITHOUT a link (AGL-1312 — a navigation row is
|
|
72
|
+
* placed on each screen it names, and there the tab for that screen is
|
|
73
|
+
* the unlinked one).
|
|
74
|
+
*
|
|
75
|
+
* Every tab linked means no panel is really open, and the client falls back
|
|
76
|
+
* to the first label; this must too, or it withholds the one panel the
|
|
77
|
+
* reader is looking at.
|
|
78
|
+
*
|
|
79
|
+
* Duplicated from the mui plugin's prop names for the same reason
|
|
80
|
+
* `parseLabels` is: tenant-runtime must not depend on a plugin bundle.
|
|
81
|
+
*/ function openingLabel(props) {
|
|
82
|
+
const labels = parseLabels(props == null ? void 0 : props.labels);
|
|
83
|
+
const linked = (position)=>!!(props == null ? void 0 : props[`tabLink${position + 1}`]);
|
|
84
|
+
const named = labels.findIndex((label, position)=>!linked(position) && labelsMatch(label, props == null ? void 0 : props.opensOn));
|
|
85
|
+
if (named >= 0) return labels[named];
|
|
86
|
+
const landing = labels.findIndex((_label, position)=>!linked(position));
|
|
87
|
+
return labels[landing < 0 ? 0 : landing];
|
|
88
|
+
}
|
|
89
|
+
/** Every descendant id of `id`, excluding `id` itself. */ function descendantsOf(nodes, id) {
|
|
90
|
+
var _ref;
|
|
91
|
+
var _nodes_id;
|
|
92
|
+
const out = [];
|
|
93
|
+
const stack = [
|
|
94
|
+
...(_ref = (_nodes_id = nodes[id]) == null ? void 0 : _nodes_id.nodes) != null ? _ref : []
|
|
95
|
+
];
|
|
96
|
+
while(stack.length){
|
|
97
|
+
var _nodes_next_nodes;
|
|
98
|
+
const next = stack.pop();
|
|
99
|
+
if (typeof next !== 'string' || !nodes[next]) continue;
|
|
100
|
+
out.push(next);
|
|
101
|
+
stack.push(...(_nodes_next_nodes = nodes[next].nodes) != null ? _nodes_next_nodes : []);
|
|
102
|
+
}
|
|
103
|
+
return out;
|
|
104
|
+
}
|
|
105
|
+
export function deferLazyPanelNodes(nodes) {
|
|
106
|
+
if (!nodes) return {
|
|
107
|
+
nodes: nodes,
|
|
108
|
+
deferredPanelIds: [],
|
|
109
|
+
removed: 0
|
|
110
|
+
};
|
|
111
|
+
const drop = new Set();
|
|
112
|
+
const deferredPanelIds = [];
|
|
113
|
+
for (const [id, node] of Object.entries(nodes)){
|
|
114
|
+
var _node_nodes;
|
|
115
|
+
var _node_props;
|
|
116
|
+
if ((node == null ? void 0 : node.componentId) !== TABS_ID) continue;
|
|
117
|
+
// Defer by default; `ssrPanels` is the author's SEO escape hatch
|
|
118
|
+
// (AGL-1283 option 3). Must match the client-side mount predicate.
|
|
119
|
+
if (node == null ? void 0 : (_node_props = node.props) == null ? void 0 : _node_props.ssrPanels) continue;
|
|
120
|
+
const labels = parseLabels(node.props.labels);
|
|
121
|
+
const panelIds = ((_node_nodes = node.nodes) != null ? _node_nodes : []).filter((childId)=>{
|
|
122
|
+
var _nodes_childId;
|
|
123
|
+
return ((_nodes_childId = nodes[childId]) == null ? void 0 : _nodes_childId.componentId) === TAB_PANEL_ID;
|
|
124
|
+
});
|
|
125
|
+
if (!labels.length || panelIds.length < 2) continue;
|
|
126
|
+
// The kept panel is the one matching the OPENING label, not the first
|
|
127
|
+
// child: panels can be reordered in the hierarchy independently of the
|
|
128
|
+
// label list, and pruning the panel that is actually open would leave the
|
|
129
|
+
// reader looking at an empty tab. If no panel matches, defer NOTHING —
|
|
130
|
+
// a mislabelled set is exactly when guessing is most expensive.
|
|
131
|
+
const opening = panelIds.find((panelId)=>{
|
|
132
|
+
var _nodes_panelId_props, _nodes_panelId;
|
|
133
|
+
return labelsMatch((_nodes_panelId = nodes[panelId]) == null ? void 0 : (_nodes_panelId_props = _nodes_panelId.props) == null ? void 0 : _nodes_panelId_props.label, openingLabel(node.props));
|
|
134
|
+
});
|
|
135
|
+
if (!opening) continue;
|
|
136
|
+
for (const panelId of panelIds){
|
|
137
|
+
if (panelId === opening) continue;
|
|
138
|
+
const kids = descendantsOf(nodes, panelId);
|
|
139
|
+
if (!kids.length) continue;
|
|
140
|
+
kids.forEach((kid)=>drop.add(kid));
|
|
141
|
+
deferredPanelIds.push(panelId);
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
if (!drop.size) return {
|
|
145
|
+
nodes,
|
|
146
|
+
deferredPanelIds: [],
|
|
147
|
+
removed: 0
|
|
148
|
+
};
|
|
149
|
+
// Rebuild rather than mutate: the composed document is CACHED
|
|
150
|
+
// (`loadPageDataCached`), so mutating it would poison every later request
|
|
151
|
+
// for this screen with a permanently half-empty page.
|
|
152
|
+
const deferred = new Set(deferredPanelIds);
|
|
153
|
+
const out = {};
|
|
154
|
+
for (const [id, node] of Object.entries(nodes)){
|
|
155
|
+
var _node_props1;
|
|
156
|
+
if (drop.has(id)) continue;
|
|
157
|
+
out[id] = deferred.has(id) ? _extends({}, node, {
|
|
158
|
+
nodes: [],
|
|
159
|
+
props: _extends({}, (_node_props1 = node.props) != null ? _node_props1 : {}, {
|
|
160
|
+
[DEFERRED_PANEL_PROP]: true
|
|
161
|
+
})
|
|
162
|
+
}) : node;
|
|
163
|
+
}
|
|
164
|
+
return {
|
|
165
|
+
nodes: out,
|
|
166
|
+
deferredPanelIds,
|
|
167
|
+
removed: drop.size
|
|
168
|
+
};
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
//# sourceMappingURL=defer-lazy-panels.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../../../../../libs/tenant/runtime/src/lib/defer-lazy-panels.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 * Withholds the node definitions inside lazy tab panels that will not mount on\n * first render (AGL-1285).\n *\n * Lazy mounting (AGL-1283) already stops a Tabs container rendering anything\n * but the selected panel. The document did not follow: every panel's nodes were\n * still serialized into the page. On `/pricing` that is 1,489 of 2,523 nodes —\n * eight copies of a 50-row table — and the RSC payload is 63% of the page's\n * gzipped weight, so the render knowing they are unneeded was worth nothing.\n *\n * This prunes them server-side. The panel node stays (its tab, its aria wiring\n * and its `label` are all still needed); only its subtree is withheld, and the\n * panel is marked so the client can fetch the rest when the reader first opens\n * one.\n *\n * Lazy is the DEFAULT (AGL-1283 option 3), so this prunes every multi-panel\n * Tabs unless the author set `ssrPanels` — the same predicate the client\n * mount uses (`tabs.tsx`), and the two MUST agree: a panel this function\n * withholds that the client would render arrives empty, and a panel it ships\n * that the client defers is payload for nothing. The legacy `lazyPanels`\n * opt-in is subsumed by the default and no longer consulted.\n *\n * WHICH panel survives follows the author's `opensOn` (AGL-3164) by the same\n * rule the element applies, for the same reason: the tab that opens first is\n * the one whose panel has to be in the page source, or the page paints an\n * empty panel and fills it after a round trip.\n *\n * SEO note: withheld content is not in the HTML. Where a panel holds content\n * that appears nowhere else on the page and must reach crawlers, `ssrPanels`\n * is the escape hatch — it keeps the panels in the payload AND in the markup.\n */\n\n/** Marks a panel whose children were withheld. */\nexport const DEFERRED_PANEL_PROP = 'aglynDeferred'\n\nconst TABS_ID = 'muiTabs'\nconst TAB_PANEL_ID = 'muiTabPanel'\n\nexport interface DeferLazyPanelsResult {\n /** A NEW node map; the input is never mutated. */\n nodes: Record<string, any>\n /** Panel node ids whose children were withheld. */\n deferredPanelIds: string[]\n /** Node definitions removed. */\n removed: number\n}\n\n/**\n * Label parsing is duplicated from the mui plugin's `parseLabels` rather than\n * imported: tenant-runtime must not depend on a plugin bundle (module\n * boundaries), and this needs to agree with it exactly or the wrong panel gets\n * pruned. `defer-lazy-panels.spec.ts` pins the cases that matter.\n */\nfunction parseLabels(value: unknown): string[] {\n if (value == null) return []\n return String(value)\n .split(/[\\n,]/)\n .map((label) => label.trim())\n .filter(Boolean)\n}\n\nconst labelsMatch = (a: unknown, b: unknown): boolean =>\n String(a ?? '')\n .trim()\n .toLowerCase() ===\n String(b ?? '')\n .trim()\n .toLowerCase()\n\n/**\n * The label the strip opens on, which is NOT always the first one.\n *\n * Two rules, and they must be the mui plugin's `openingTabIndex` exactly —\n * this is the function that decides whose nodes SURVIVE into the page, so a\n * disagreement does not merely paint the wrong panel first, it ships the\n * opening panel empty:\n *\n * 1. the tab the author named in `opensOn` (AGL-3164), refused when that tab\n * carries a screen link, because a linked tab navigates instead of\n * revealing a panel;\n * 2. otherwise the first tab WITHOUT a link (AGL-1312 — a navigation row is\n * placed on each screen it names, and there the tab for that screen is\n * the unlinked one).\n *\n * Every tab linked means no panel is really open, and the client falls back\n * to the first label; this must too, or it withholds the one panel the\n * reader is looking at.\n *\n * Duplicated from the mui plugin's prop names for the same reason\n * `parseLabels` is: tenant-runtime must not depend on a plugin bundle.\n */\nfunction openingLabel(props: Record<string, any>): string | undefined {\n const labels = parseLabels(props?.labels)\n const linked = (position: number): boolean =>\n !!props?.[`tabLink${position + 1}`]\n const named = labels.findIndex(\n (label, position) =>\n !linked(position) && labelsMatch(label, props?.opensOn),\n )\n if (named >= 0) return labels[named]\n const landing = labels.findIndex((_label, position) => !linked(position))\n return labels[landing < 0 ? 0 : landing]\n}\n\n/** Every descendant id of `id`, excluding `id` itself. */\nfunction descendantsOf(nodes: Record<string, any>, id: string): string[] {\n const out: string[] = []\n const stack = [...(nodes[id]?.nodes ?? [])]\n while (stack.length) {\n const next = stack.pop()\n if (typeof next !== 'string' || !nodes[next]) continue\n out.push(next)\n stack.push(...(nodes[next].nodes ?? []))\n }\n return out\n}\n\nexport function deferLazyPanelNodes(\n nodes: Record<string, any> | null | undefined,\n): DeferLazyPanelsResult {\n if (!nodes) return { nodes: nodes as any, deferredPanelIds: [], removed: 0 }\n\n const drop = new Set<string>()\n const deferredPanelIds: string[] = []\n\n for (const [id, node] of Object.entries(nodes)) {\n if (node?.componentId !== TABS_ID) continue\n // Defer by default; `ssrPanels` is the author's SEO escape hatch\n // (AGL-1283 option 3). Must match the client-side mount predicate.\n if (node?.props?.ssrPanels) continue\n\n const labels = parseLabels(node.props.labels)\n const panelIds: string[] = (node.nodes ?? []).filter(\n (childId: string) => nodes[childId]?.componentId === TAB_PANEL_ID,\n )\n if (!labels.length || panelIds.length < 2) continue\n\n // The kept panel is the one matching the OPENING label, not the first\n // child: panels can be reordered in the hierarchy independently of the\n // label list, and pruning the panel that is actually open would leave the\n // reader looking at an empty tab. If no panel matches, defer NOTHING —\n // a mislabelled set is exactly when guessing is most expensive.\n const opening = panelIds.find((panelId) =>\n labelsMatch(nodes[panelId]?.props?.label, openingLabel(node.props)),\n )\n if (!opening) continue\n\n for (const panelId of panelIds) {\n if (panelId === opening) continue\n const kids = descendantsOf(nodes, panelId)\n if (!kids.length) continue\n kids.forEach((kid) => drop.add(kid))\n deferredPanelIds.push(panelId)\n }\n }\n\n if (!drop.size) return { nodes, deferredPanelIds: [], removed: 0 }\n\n // Rebuild rather than mutate: the composed document is CACHED\n // (`loadPageDataCached`), so mutating it would poison every later request\n // for this screen with a permanently half-empty page.\n const deferred = new Set(deferredPanelIds)\n const out: Record<string, any> = {}\n for (const [id, node] of Object.entries(nodes)) {\n if (drop.has(id)) continue\n out[id] = deferred.has(id)\n ? {\n ...node,\n nodes: [],\n props: { ...(node.props ?? {}), [DEFERRED_PANEL_PROP]: true },\n }\n : node\n }\n return { nodes: out, deferredPanelIds, removed: drop.size }\n}\n"],"names":["DEFERRED_PANEL_PROP","TABS_ID","TAB_PANEL_ID","parseLabels","value","String","split","map","label","trim","filter","Boolean","labelsMatch","a","b","toLowerCase","openingLabel","props","labels","linked","position","named","findIndex","opensOn","landing","_label","descendantsOf","nodes","id","out","stack","length","next","pop","push","deferLazyPanelNodes","deferredPanelIds","removed","drop","Set","node","Object","entries","componentId","ssrPanels","panelIds","childId","opening","find","panelId","kids","forEach","kid","add","size","deferred","has"],"mappings":";AAAA;;;;;;;;;;;;;;;CAeC,GAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CA8BC,GAED,gDAAgD,GAChD,OAAO,MAAMA,sBAAsB,gBAAe;AAElD,MAAMC,UAAU;AAChB,MAAMC,eAAe;AAWrB;;;;;CAKC,GACD,SAASC,YAAYC,KAAc;IACjC,IAAIA,SAAS,MAAM,OAAO,EAAE;IAC5B,OAAOC,OAAOD,OACXE,KAAK,CAAC,SACNC,GAAG,CAAC,CAACC,QAAUA,MAAMC,IAAI,IACzBC,MAAM,CAACC;AACZ;AAEA,MAAMC,cAAc,CAACC,GAAYC,IAC/BT,OAAOQ,YAAAA,IAAK,IACTJ,IAAI,GACJM,WAAW,OACdV,OAAOS,YAAAA,IAAK,IACTL,IAAI,GACJM,WAAW;AAEhB;;;;;;;;;;;;;;;;;;;;;CAqBC,GACD,SAASC,aAAaC,KAA0B;IAC9C,MAAMC,SAASf,YAAYc,yBAAAA,MAAOC,MAAM;IACxC,MAAMC,SAAS,CAACC,WACd,CAAC,EAACH,yBAAAA,KAAO,CAAC,CAAC,OAAO,EAAEG,WAAW,GAAG,CAAC;IACrC,MAAMC,QAAQH,OAAOI,SAAS,CAC5B,CAACd,OAAOY,WACN,CAACD,OAAOC,aAAaR,YAAYJ,OAAOS,yBAAAA,MAAOM,OAAO;IAE1D,IAAIF,SAAS,GAAG,OAAOH,MAAM,CAACG,MAAM;IACpC,MAAMG,UAAUN,OAAOI,SAAS,CAAC,CAACG,QAAQL,WAAa,CAACD,OAAOC;IAC/D,OAAOF,MAAM,CAACM,UAAU,IAAI,IAAIA,QAAQ;AAC1C;AAEA,wDAAwD,GACxD,SAASE,cAAcC,KAA0B,EAAEC,EAAU;;QAExCD;IADnB,MAAME,MAAgB,EAAE;IACxB,MAAMC,QAAQ;oBAAKH,YAAAA,KAAK,CAACC,GAAG,qBAATD,UAAWA,KAAK,mBAAI,EAAE;KAAE;IAC3C,MAAOG,MAAMC,MAAM,CAAE;YAIJJ;QAHf,MAAMK,OAAOF,MAAMG,GAAG;QACtB,IAAI,OAAOD,SAAS,YAAY,CAACL,KAAK,CAACK,KAAK,EAAE;QAC9CH,IAAIK,IAAI,CAACF;QACTF,MAAMI,IAAI,KAAKP,oBAAAA,KAAK,CAACK,KAAK,CAACL,KAAK,YAAjBA,oBAAqB,EAAE;IACxC;IACA,OAAOE;AACT;AAEA,OAAO,SAASM,oBACdR,KAA6C;IAE7C,IAAI,CAACA,OAAO,OAAO;QAAEA,OAAOA;QAAcS,kBAAkB,EAAE;QAAEC,SAAS;IAAE;IAE3E,MAAMC,OAAO,IAAIC;IACjB,MAAMH,mBAA6B,EAAE;IAErC,KAAK,MAAM,CAACR,IAAIY,KAAK,IAAIC,OAAOC,OAAO,CAACf,OAAQ;YAOlBa;YAHxBA;QAHJ,IAAIA,CAAAA,wBAAAA,KAAMG,WAAW,MAAK1C,SAAS;QACnC,iEAAiE;QACjE,mEAAmE;QACnE,IAAIuC,yBAAAA,cAAAA,KAAMvB,KAAK,qBAAXuB,YAAaI,SAAS,EAAE;QAE5B,MAAM1B,SAASf,YAAYqC,KAAKvB,KAAK,CAACC,MAAM;QAC5C,MAAM2B,WAAqB,EAACL,cAAAA,KAAKb,KAAK,YAAVa,cAAc,EAAE,EAAE9B,MAAM,CAClD,CAACoC;gBAAoBnB;mBAAAA,EAAAA,iBAAAA,KAAK,CAACmB,QAAQ,qBAAdnB,eAAgBgB,WAAW,MAAKzC;;QAEvD,IAAI,CAACgB,OAAOa,MAAM,IAAIc,SAASd,MAAM,GAAG,GAAG;QAE3C,sEAAsE;QACtE,uEAAuE;QACvE,0EAA0E;QAC1E,uEAAuE;QACvE,gEAAgE;QAChE,MAAMgB,UAAUF,SAASG,IAAI,CAAC,CAACC;gBACjBtB,sBAAAA;mBAAZf,aAAYe,iBAAAA,KAAK,CAACsB,QAAQ,sBAAdtB,uBAAAA,eAAgBV,KAAK,qBAArBU,qBAAuBnB,KAAK,EAAEQ,aAAawB,KAAKvB,KAAK;;QAEnE,IAAI,CAAC8B,SAAS;QAEd,KAAK,MAAME,WAAWJ,SAAU;YAC9B,IAAII,YAAYF,SAAS;YACzB,MAAMG,OAAOxB,cAAcC,OAAOsB;YAClC,IAAI,CAACC,KAAKnB,MAAM,EAAE;YAClBmB,KAAKC,OAAO,CAAC,CAACC,MAAQd,KAAKe,GAAG,CAACD;YAC/BhB,iBAAiBF,IAAI,CAACe;QACxB;IACF;IAEA,IAAI,CAACX,KAAKgB,IAAI,EAAE,OAAO;QAAE3B;QAAOS,kBAAkB,EAAE;QAAEC,SAAS;IAAE;IAEjE,8DAA8D;IAC9D,0EAA0E;IAC1E,sDAAsD;IACtD,MAAMkB,WAAW,IAAIhB,IAAIH;IACzB,MAAMP,MAA2B,CAAC;IAClC,KAAK,MAAM,CAACD,IAAIY,KAAK,IAAIC,OAAOC,OAAO,CAACf,OAAQ;YAM3Ba;QALnB,IAAIF,KAAKkB,GAAG,CAAC5B,KAAK;QAClBC,GAAG,CAACD,GAAG,GAAG2B,SAASC,GAAG,CAAC5B,MACnB,aACKY;YACHb,OAAO,EAAE;YACTV,OAAO,cAAMuB,eAAAA,KAAKvB,KAAK,YAAVuB,eAAc,CAAC;gBAAI,CAACxC,oBAAoB,EAAE;;aAEzDwC;IACN;IACA,OAAO;QAAEb,OAAOE;QAAKO;QAAkBC,SAASC,KAAKgB,IAAI;IAAC;AAC5D"}
|
|
@@ -0,0 +1,62 @@
|
|
|
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
|
+
* The stylesheet. Built once at module load, not per request.
|
|
19
|
+
*
|
|
20
|
+
* The keyframe name is threaded through a custom property so the trigger rules
|
|
21
|
+
* are two selectors rather than two per preset — the sheet stays flat as
|
|
22
|
+
* presets are added.
|
|
23
|
+
*/
|
|
24
|
+
export declare const ELEMENT_ANIMATION_STYLE_TEXT: string;
|
|
25
|
+
/** Id of the injected style tag (idempotence marker, mirrors AGL-562). */
|
|
26
|
+
export declare const ELEMENT_ANIMATION_STYLE_ID = "aglyn-element-animation-style";
|
|
27
|
+
/**
|
|
28
|
+
* The scroll runtime, inlined verbatim into a `<script>`.
|
|
29
|
+
*
|
|
30
|
+
* One `IntersectionObserver` for the whole document. Re-scanning is safe and
|
|
31
|
+
* cheap because `observe()` on an already-observed element is a no-op, which
|
|
32
|
+
* is how late-arriving nodes (a deferred lazy tab panel the reader opens, a
|
|
33
|
+
* collection page appending rows) get picked up without a per-element
|
|
34
|
+
* observer. The `MutationObserver` is what makes that correct rather than
|
|
35
|
+
* best-effort: an element inserted after load would otherwise stay at
|
|
36
|
+
* `opacity:0` forever, because the hide rule applies the moment the ready
|
|
37
|
+
* class is on `<html>`.
|
|
38
|
+
*
|
|
39
|
+
* The selector is the trigger ATTRIBUTE alone rather than the base class,
|
|
40
|
+
* because there are now two base classes — an element that animates itself
|
|
41
|
+
* and a stagger host whose children animate. Both carry the attribute, only
|
|
42
|
+
* the renderer ever writes it, and matching on it keeps the runtime unaware
|
|
43
|
+
* that the second shape exists.
|
|
44
|
+
*
|
|
45
|
+
* Written in ES5 with short locals because it ships as source; there is no
|
|
46
|
+
* minifier in this path.
|
|
47
|
+
*/
|
|
48
|
+
export declare const ELEMENT_ANIMATION_SCRIPT_TEXT: string;
|
|
49
|
+
export interface PageAnimationAssets {
|
|
50
|
+
styleText: string;
|
|
51
|
+
/** Present only when the page carries a scroll-triggered element. */
|
|
52
|
+
scriptText: string | null;
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* Decides what a page must ship, from the flat node map the route already
|
|
56
|
+
* holds. Returns `null` — the common case — when nothing on the page animates,
|
|
57
|
+
* and the route then renders neither tag.
|
|
58
|
+
*
|
|
59
|
+
* The scroll runtime is withheld separately: a page whose only animations play
|
|
60
|
+
* on load or on hover is pure CSS and ships no script either.
|
|
61
|
+
*/
|
|
62
|
+
export declare function pageAnimationAssets(nodes: Record<string, any> | null | undefined): PageAnimationAssets | null;
|
|
@@ -0,0 +1,290 @@
|
|
|
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
|
+
* The published page's animation stylesheet and scroll runtime (AGL-2486).
|
|
18
|
+
*
|
|
19
|
+
* ## Why this is a server-only module
|
|
20
|
+
*
|
|
21
|
+
* `apps/tenant`'s route is a SERVER component and imports this by subpath, so
|
|
22
|
+
* neither the CSS nor the script text ever enters the client bundle. That is
|
|
23
|
+
* the whole point: a page that animates nothing must pay nothing, and here it
|
|
24
|
+
* pays literally zero bytes — `pageAnimationAssets` returns `null` and the
|
|
25
|
+
* route renders no `<style>` and no `<script>` at all.
|
|
26
|
+
*
|
|
27
|
+
* ## Why the runtime is inline rather than a chunk
|
|
28
|
+
*
|
|
29
|
+
* The only thing that needs JS is the scroll trigger, and it needs one shared
|
|
30
|
+
* `IntersectionObserver` for the whole document — not one per element. That is
|
|
31
|
+
* a few hundred bytes. As a module it would cost a chunk, a request, a
|
|
32
|
+
* parse and a hydration tick; inline in the SSR HTML it costs bytes that gzip
|
|
33
|
+
* with the document and runs before hydration. `apps/tenant` sets NO
|
|
34
|
+
* `script-src` in either CSP header (deliberate — see `middleware.ts`, and
|
|
35
|
+
* `specs/csp-no-script-src.spec.ts` holds it there), so an inline script needs
|
|
36
|
+
* no nonce.
|
|
37
|
+
*
|
|
38
|
+
* ## prefers-reduced-motion
|
|
39
|
+
*
|
|
40
|
+
* EVERY rule below — the keyframes, the transitions, and critically the rule
|
|
41
|
+
* that hides a scroll-triggered element before it plays — lives inside a
|
|
42
|
+
* single `@media (prefers-reduced-motion: no-preference)` block. A visitor who
|
|
43
|
+
* asked their OS to reduce motion therefore gets a page where nothing is
|
|
44
|
+
* hidden and nothing moves, and the switch is live: because the gate is CSS
|
|
45
|
+
* rather than a JS branch, toggling the OS setting after load resolves
|
|
46
|
+
* correctly in BOTH directions. The runtime deliberately does NOT test
|
|
47
|
+
* `matchMedia` itself — an early return there would leave elements hidden by a
|
|
48
|
+
* rule that had just started applying again.
|
|
49
|
+
*
|
|
50
|
+
* ## No layout shift
|
|
51
|
+
*
|
|
52
|
+
* Every keyframe animates `opacity` and `transform` only. Neither affects
|
|
53
|
+
* layout, so an entrance animation contributes nothing to CLS — the element
|
|
54
|
+
* occupies its final box from the first frame.
|
|
55
|
+
*
|
|
56
|
+
* ## Stagger costs no extra JS and no extra observer entry
|
|
57
|
+
*
|
|
58
|
+
* A staggered row is ONE element as far as this file's runtime is concerned:
|
|
59
|
+
* the host is what the observer watches, and the children play off the class
|
|
60
|
+
* the observer puts on it. The per-child offset is a `:nth-child` ladder in
|
|
61
|
+
* the sheet, so twelve cards arriving one after another cost twelve CSS
|
|
62
|
+
* declarations and zero JS — where a runtime would have cost twelve
|
|
63
|
+
* observer entries, twelve callbacks and twelve style writes on the main
|
|
64
|
+
* thread, during scroll.
|
|
65
|
+
*/ import { ANIMATION_CLASS, ANIMATION_DELAY_VAR, ANIMATION_DURATION_VAR, ANIMATION_EASE_CLASS_PREFIX, ANIMATION_EASINGS, ANIMATION_GROUP_CLASS, ANIMATION_IN_CLASS, ANIMATION_PRESET_CLASS_PREFIX, ANIMATION_READY_CLASS, ANIMATION_REPEAT_ATTR, ANIMATION_STAGGER_MAX_CHILDREN, ANIMATION_STAGGER_OFFSET_VAR, ANIMATION_STAGGER_STEP_VAR, ANIMATION_TRIGGER_ATTR, NODE_ANIMATION_TRIGGER_PROP, nodePropsAnimate } from "@aglyn/aglyn/server";
|
|
66
|
+
/**
|
|
67
|
+
* The easing curves, keyed by the ids `@aglyn/aglyn` publishes.
|
|
68
|
+
*
|
|
69
|
+
* The ids live in the shared module and the CURVES live here, so a page that
|
|
70
|
+
* animates nothing carries neither. Every one of these is a plain
|
|
71
|
+
* `cubic-bezier`; none is a spring, because a spring is not expressible in
|
|
72
|
+
* CSS and buying one would mean buying a JS runtime.
|
|
73
|
+
*
|
|
74
|
+
* `overshoot` is the only curve that leaves the 0–1 range, and it does so on
|
|
75
|
+
* the way IN only (`.34,1.56,.64,1` is the standard "back out"), so an
|
|
76
|
+
* element settles rather than oscillating. Like every other rule here it sits
|
|
77
|
+
* inside the reduced-motion gate.
|
|
78
|
+
*
|
|
79
|
+
* Typed as a total record so adding an id to `ANIMATION_EASINGS` without a
|
|
80
|
+
* curve is a compile error rather than an element with no easing.
|
|
81
|
+
*/ const EASE_CURVES = {
|
|
82
|
+
smooth: 'cubic-bezier(.16,1,.3,1)',
|
|
83
|
+
steady: 'linear',
|
|
84
|
+
'gentle-start': 'cubic-bezier(.4,0,1,1)',
|
|
85
|
+
'gentle-end': 'cubic-bezier(0,0,.2,1)',
|
|
86
|
+
'gentle-both': 'cubic-bezier(.4,0,.2,1)',
|
|
87
|
+
overshoot: 'cubic-bezier(.34,1.56,.64,1)'
|
|
88
|
+
};
|
|
89
|
+
/**
|
|
90
|
+
* The curve an element gets when it carries no easing class at all — which is
|
|
91
|
+
* every element authored before easing shipped. Deliberately the same string
|
|
92
|
+
* `EASE` used to be, so nothing that already exists moves.
|
|
93
|
+
*/ const EASE = EASE_CURVES.smooth;
|
|
94
|
+
/**
|
|
95
|
+
* Per-preset custom properties. One rule per preset sets the keyframe name
|
|
96
|
+
* used by the load/scroll triggers AND the transform the hover trigger eases
|
|
97
|
+
* to, which is why the preset table is a single list rather than three.
|
|
98
|
+
*
|
|
99
|
+
* Hover is a TRANSITION to an emphasis state, not a keyframe run: "Slide up"
|
|
100
|
+
* on hover lifts the element, "Zoom in" grows it. That is what an author means
|
|
101
|
+
* when they pick a direction for a hover effect.
|
|
102
|
+
*/ const PRESETS = [
|
|
103
|
+
{
|
|
104
|
+
id: 'fade',
|
|
105
|
+
from: 'opacity:0',
|
|
106
|
+
hover: 'none',
|
|
107
|
+
hoverOpacity: '.7'
|
|
108
|
+
},
|
|
109
|
+
{
|
|
110
|
+
id: 'slide-up',
|
|
111
|
+
from: 'opacity:0;transform:translate3d(0,24px,0)',
|
|
112
|
+
hover: 'translate3d(0,-8px,0)'
|
|
113
|
+
},
|
|
114
|
+
{
|
|
115
|
+
id: 'slide-down',
|
|
116
|
+
from: 'opacity:0;transform:translate3d(0,-24px,0)',
|
|
117
|
+
hover: 'translate3d(0,8px,0)'
|
|
118
|
+
},
|
|
119
|
+
{
|
|
120
|
+
id: 'slide-left',
|
|
121
|
+
from: 'opacity:0;transform:translate3d(24px,0,0)',
|
|
122
|
+
hover: 'translate3d(-8px,0,0)'
|
|
123
|
+
},
|
|
124
|
+
{
|
|
125
|
+
id: 'slide-right',
|
|
126
|
+
from: 'opacity:0;transform:translate3d(-24px,0,0)',
|
|
127
|
+
hover: 'translate3d(8px,0,0)'
|
|
128
|
+
},
|
|
129
|
+
{
|
|
130
|
+
id: 'zoom-in',
|
|
131
|
+
from: 'opacity:0;transform:scale3d(.92,.92,1)',
|
|
132
|
+
hover: 'scale3d(1.04,1.04,1)'
|
|
133
|
+
},
|
|
134
|
+
{
|
|
135
|
+
id: 'zoom-out',
|
|
136
|
+
from: 'opacity:0;transform:scale3d(1.08,1.08,1)',
|
|
137
|
+
hover: 'scale3d(.96,.96,1)'
|
|
138
|
+
}
|
|
139
|
+
];
|
|
140
|
+
const NAME_VAR = '--aglyn-anim-name';
|
|
141
|
+
const HOVER_VAR = '--aglyn-anim-hover';
|
|
142
|
+
const HOVER_OPACITY_VAR = '--aglyn-anim-hover-opacity';
|
|
143
|
+
/**
|
|
144
|
+
* Set by the easing class rules and read by the timing rules. Not exported:
|
|
145
|
+
* the renderer writes a CLASS, never this property, so an author cannot get a
|
|
146
|
+
* raw curve of their own into the page through it.
|
|
147
|
+
*/ const EASE_VAR = '--aglyn-anim-ease';
|
|
148
|
+
const ANIMATED = `.${ANIMATION_CLASS}`;
|
|
149
|
+
const GROUP = `.${ANIMATION_GROUP_CLASS}`;
|
|
150
|
+
/**
|
|
151
|
+
* Everything the timing rules apply to: a plain animated element, and the
|
|
152
|
+
* CHILDREN of a stagger host. The host itself is deliberately absent — it
|
|
153
|
+
* sets the custom properties its children inherit and animates nothing.
|
|
154
|
+
*/ const TARGETS = `${ANIMATED},${GROUP}>*`;
|
|
155
|
+
const DURATION = `var(${ANIMATION_DURATION_VAR},600ms)`;
|
|
156
|
+
const DELAY = `var(${ANIMATION_DELAY_VAR},0ms)`;
|
|
157
|
+
const STEP = `var(${ANIMATION_STAGGER_STEP_VAR},90ms)`;
|
|
158
|
+
const TIMING = `var(${EASE_VAR},${EASE})`;
|
|
159
|
+
/**
|
|
160
|
+
* The author's own delay plus whatever rung of the stagger ladder this child
|
|
161
|
+
* landed on. A plain animated element never matches a ladder rule, so its
|
|
162
|
+
* offset falls back to `0ms` and the sum is exactly the delay it had before
|
|
163
|
+
* stagger existed.
|
|
164
|
+
*/ const TOTAL_DELAY = `calc(${DELAY} + var(${ANIMATION_STAGGER_OFFSET_VAR},0ms))`;
|
|
165
|
+
/**
|
|
166
|
+
* The stagger ladder: one rule per rung, each pushing a child one more STEP
|
|
167
|
+
* behind the one before it.
|
|
168
|
+
*
|
|
169
|
+
* The index has to come from CSS rather than the renderer. The renderer sees
|
|
170
|
+
* a node, not a position — and a node that knew its index would have to
|
|
171
|
+
* re-render every time a sibling was inserted, which is exactly what an
|
|
172
|
+
* author does while building a row of cards.
|
|
173
|
+
*
|
|
174
|
+
* The first child gets NO rule: `var(--aglyn-anim-stagger,0ms)` already falls
|
|
175
|
+
* back to zero, so emitting `calc(STEP * 0)` would be a rule that changes
|
|
176
|
+
* nothing. The last rung is `:nth-child(n+N)` rather than an Nth rule, so a
|
|
177
|
+
* collection with two hundred rows shares the final rung instead of leaving
|
|
178
|
+
* its tail invisible for half a minute.
|
|
179
|
+
*/ const STAGGER_LADDER = [
|
|
180
|
+
// Reset first, and it must STAY first. A nested stagger host's own ladder
|
|
181
|
+
// rule has identical specificity (one class + one pseudo-class against two
|
|
182
|
+
// classes), so source order is the only thing that lets the inner host win
|
|
183
|
+
// for its own children. What this catches is the other case: an animated
|
|
184
|
+
// element deeper inside a staggered child, which would otherwise INHERIT
|
|
185
|
+
// its ancestor's rung and start late for no reason an author could see.
|
|
186
|
+
`${GROUP}>* ${ANIMATED}{${ANIMATION_STAGGER_OFFSET_VAR}:0ms}`,
|
|
187
|
+
...Array.from({
|
|
188
|
+
length: ANIMATION_STAGGER_MAX_CHILDREN - 2
|
|
189
|
+
}, (_unused, index)=>{
|
|
190
|
+
const nth = index + 2;
|
|
191
|
+
return `${GROUP}>*:nth-child(${nth}){${ANIMATION_STAGGER_OFFSET_VAR}:calc(${STEP} * ${nth - 1})}`;
|
|
192
|
+
}),
|
|
193
|
+
`${GROUP}>*:nth-child(n+${ANIMATION_STAGGER_MAX_CHILDREN}){${ANIMATION_STAGGER_OFFSET_VAR}:calc(${STEP} * ${ANIMATION_STAGGER_MAX_CHILDREN - 1})}`
|
|
194
|
+
];
|
|
195
|
+
/**
|
|
196
|
+
* The stylesheet. Built once at module load, not per request.
|
|
197
|
+
*
|
|
198
|
+
* The keyframe name is threaded through a custom property so the trigger rules
|
|
199
|
+
* are two selectors rather than two per preset — the sheet stays flat as
|
|
200
|
+
* presets are added.
|
|
201
|
+
*/ export const ELEMENT_ANIMATION_STYLE_TEXT = [
|
|
202
|
+
'@media (prefers-reduced-motion:no-preference){',
|
|
203
|
+
// Keyframes.
|
|
204
|
+
...PRESETS.map((preset)=>`@keyframes aglyn-anim-${preset.id}{from{${preset.from}}to{opacity:1;transform:none}}`),
|
|
205
|
+
// Per-preset custom properties. Keyed on the preset class ALONE, not on the
|
|
206
|
+
// base class as well: a stagger host carries `aglyn-anim-group` rather than
|
|
207
|
+
// `aglyn-anim`, and its children read these through inheritance, so a rule
|
|
208
|
+
// that required the base class would leave a whole staggered row with no
|
|
209
|
+
// keyframe name.
|
|
210
|
+
...PRESETS.map((preset)=>`.${ANIMATION_PRESET_CLASS_PREFIX}${preset.id}{${NAME_VAR}:aglyn-anim-${preset.id};${HOVER_VAR}:${preset.hover}${preset.hoverOpacity ? `;${HOVER_OPACITY_VAR}:${preset.hoverOpacity}` : ''}}`),
|
|
211
|
+
// Per-easing custom property, same shape and the same inheritance.
|
|
212
|
+
...ANIMATION_EASINGS.map((ease)=>`.${ANIMATION_EASE_CLASS_PREFIX}${ease}{${EASE_VAR}:${EASE_CURVES[ease]}}`),
|
|
213
|
+
// Shared timing for the keyframe triggers.
|
|
214
|
+
`${TARGETS}{animation-duration:${DURATION};animation-delay:${TOTAL_DELAY};animation-timing-function:${TIMING};animation-fill-mode:both}`,
|
|
215
|
+
// The stagger ladder — no-ops for every element that is not inside a host.
|
|
216
|
+
...STAGGER_LADDER,
|
|
217
|
+
// On load: plays as soon as the element is parsed. No JS involved at all.
|
|
218
|
+
`${ANIMATED}[${ANIMATION_TRIGGER_ATTR}=load],${GROUP}[${ANIMATION_TRIGGER_ATTR}=load]>*{animation-name:var(${NAME_VAR})}`,
|
|
219
|
+
// On scroll: plays when the runtime marks it as entered. For a stagger host
|
|
220
|
+
// the ONE observed element is the host, and its children play off its class
|
|
221
|
+
// — one observer entry for a whole row, not one per card.
|
|
222
|
+
`${ANIMATED}[${ANIMATION_TRIGGER_ATTR}=scroll].${ANIMATION_IN_CLASS},${GROUP}[${ANIMATION_TRIGGER_ATTR}=scroll].${ANIMATION_IN_CLASS}>*{animation-name:var(${NAME_VAR})}`,
|
|
223
|
+
// The ONLY rule that hides anything, and it is scoped under a class the
|
|
224
|
+
// inline runtime adds to <html>. No JS (or no IntersectionObserver) means
|
|
225
|
+
// the class is never added, the rule never matches, and every element is
|
|
226
|
+
// visible in its final state. `opacity` rather than `display`/`visibility`
|
|
227
|
+
// so the content stays in the accessibility tree and in the DOM a crawler
|
|
228
|
+
// reads either way.
|
|
229
|
+
`html.${ANIMATION_READY_CLASS} ${ANIMATED}[${ANIMATION_TRIGGER_ATTR}=scroll]:not(.${ANIMATION_IN_CLASS}),html.${ANIMATION_READY_CLASS} ${GROUP}[${ANIMATION_TRIGGER_ATTR}=scroll]:not(.${ANIMATION_IN_CLASS})>*{opacity:0}`,
|
|
230
|
+
// On hover: a transition, never a keyframe run, so it reverses cleanly when
|
|
231
|
+
// the pointer leaves. Staggering is refused for hover upstream, so this rule
|
|
232
|
+
// reads the author's plain delay rather than the ladder's sum — a
|
|
233
|
+
// transition has no `animation-delay` to add a rung to.
|
|
234
|
+
`${ANIMATED}[${ANIMATION_TRIGGER_ATTR}=hover]{transition:transform ${DURATION} ${TIMING} ${DELAY},opacity ${DURATION} ${TIMING} ${DELAY}}`,
|
|
235
|
+
`${ANIMATED}[${ANIMATION_TRIGGER_ATTR}=hover]:hover{transform:var(${HOVER_VAR},none);opacity:var(${HOVER_OPACITY_VAR},1)}`,
|
|
236
|
+
'}'
|
|
237
|
+
].join('');
|
|
238
|
+
/** Id of the injected style tag (idempotence marker, mirrors AGL-562). */ export const ELEMENT_ANIMATION_STYLE_ID = 'aglyn-element-animation-style';
|
|
239
|
+
/**
|
|
240
|
+
* The scroll runtime, inlined verbatim into a `<script>`.
|
|
241
|
+
*
|
|
242
|
+
* One `IntersectionObserver` for the whole document. Re-scanning is safe and
|
|
243
|
+
* cheap because `observe()` on an already-observed element is a no-op, which
|
|
244
|
+
* is how late-arriving nodes (a deferred lazy tab panel the reader opens, a
|
|
245
|
+
* collection page appending rows) get picked up without a per-element
|
|
246
|
+
* observer. The `MutationObserver` is what makes that correct rather than
|
|
247
|
+
* best-effort: an element inserted after load would otherwise stay at
|
|
248
|
+
* `opacity:0` forever, because the hide rule applies the moment the ready
|
|
249
|
+
* class is on `<html>`.
|
|
250
|
+
*
|
|
251
|
+
* The selector is the trigger ATTRIBUTE alone rather than the base class,
|
|
252
|
+
* because there are now two base classes — an element that animates itself
|
|
253
|
+
* and a stagger host whose children animate. Both carry the attribute, only
|
|
254
|
+
* the renderer ever writes it, and matching on it keeps the runtime unaware
|
|
255
|
+
* that the second shape exists.
|
|
256
|
+
*
|
|
257
|
+
* Written in ES5 with short locals because it ships as source; there is no
|
|
258
|
+
* minifier in this path.
|
|
259
|
+
*/ export const ELEMENT_ANIMATION_SCRIPT_TEXT = `(function(){var w=window,d=document;if(!w.IntersectionObserver||!w.MutationObserver)return;d.documentElement.classList.add(${JSON.stringify(ANIMATION_READY_CLASS)});var S=${JSON.stringify(`[${ANIMATION_TRIGGER_ATTR}=scroll]`)},I=${JSON.stringify(ANIMATION_IN_CLASS)},R=${JSON.stringify(ANIMATION_REPEAT_ATTR)};var o=new w.IntersectionObserver(function(es){for(var i=0;i<es.length;i++){var e=es[i],t=e.target,r=t.getAttribute(R)==='1';if(e.isIntersecting){t.classList.add(I);if(!r)o.unobserve(t)}else if(r)t.classList.remove(I)}},{rootMargin:'0px 0px -10% 0px'});var q=0,scan=function(){q=0;var n=d.querySelectorAll(S);for(var i=0;i<n.length;i++)o.observe(n[i])};scan();if(d.readyState==='loading')d.addEventListener('DOMContentLoaded',scan);var m=new w.MutationObserver(function(){if(q)return;q=w.requestAnimationFrame?w.requestAnimationFrame(scan):w.setTimeout(scan,0)});var start=function(){if(d.body)m.observe(d.body,{childList:true,subtree:true})};if(d.body)start();else d.addEventListener('DOMContentLoaded',start)})()`;
|
|
260
|
+
/**
|
|
261
|
+
* Decides what a page must ship, from the flat node map the route already
|
|
262
|
+
* holds. Returns `null` — the common case — when nothing on the page animates,
|
|
263
|
+
* and the route then renders neither tag.
|
|
264
|
+
*
|
|
265
|
+
* The scroll runtime is withheld separately: a page whose only animations play
|
|
266
|
+
* on load or on hover is pure CSS and ships no script either.
|
|
267
|
+
*/ export function pageAnimationAssets(nodes) {
|
|
268
|
+
if (!nodes) return null;
|
|
269
|
+
let animates = false;
|
|
270
|
+
let needsScroll = false;
|
|
271
|
+
for(const id in nodes){
|
|
272
|
+
var _nodes_id;
|
|
273
|
+
const props = (_nodes_id = nodes[id]) == null ? void 0 : _nodes_id.props;
|
|
274
|
+
if (!nodePropsAnimate(props)) continue;
|
|
275
|
+
animates = true;
|
|
276
|
+
// `scroll` is the default trigger, so an unset trigger needs the runtime.
|
|
277
|
+
const trigger = props == null ? void 0 : props[NODE_ANIMATION_TRIGGER_PROP];
|
|
278
|
+
if (trigger === undefined || trigger === null || trigger === 'scroll') {
|
|
279
|
+
needsScroll = true;
|
|
280
|
+
break;
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
if (!animates) return null;
|
|
284
|
+
return {
|
|
285
|
+
styleText: ELEMENT_ANIMATION_STYLE_TEXT,
|
|
286
|
+
scriptText: needsScroll ? ELEMENT_ANIMATION_SCRIPT_TEXT : null
|
|
287
|
+
};
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
//# sourceMappingURL=element-animation-assets.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../../../../../libs/tenant/runtime/src/lib/element-animation-assets.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 * The published page's animation stylesheet and scroll runtime (AGL-2486).\n *\n * ## Why this is a server-only module\n *\n * `apps/tenant`'s route is a SERVER component and imports this by subpath, so\n * neither the CSS nor the script text ever enters the client bundle. That is\n * the whole point: a page that animates nothing must pay nothing, and here it\n * pays literally zero bytes — `pageAnimationAssets` returns `null` and the\n * route renders no `<style>` and no `<script>` at all.\n *\n * ## Why the runtime is inline rather than a chunk\n *\n * The only thing that needs JS is the scroll trigger, and it needs one shared\n * `IntersectionObserver` for the whole document — not one per element. That is\n * a few hundred bytes. As a module it would cost a chunk, a request, a\n * parse and a hydration tick; inline in the SSR HTML it costs bytes that gzip\n * with the document and runs before hydration. `apps/tenant` sets NO\n * `script-src` in either CSP header (deliberate — see `middleware.ts`, and\n * `specs/csp-no-script-src.spec.ts` holds it there), so an inline script needs\n * no nonce.\n *\n * ## prefers-reduced-motion\n *\n * EVERY rule below — the keyframes, the transitions, and critically the rule\n * that hides a scroll-triggered element before it plays — lives inside a\n * single `@media (prefers-reduced-motion: no-preference)` block. A visitor who\n * asked their OS to reduce motion therefore gets a page where nothing is\n * hidden and nothing moves, and the switch is live: because the gate is CSS\n * rather than a JS branch, toggling the OS setting after load resolves\n * correctly in BOTH directions. The runtime deliberately does NOT test\n * `matchMedia` itself — an early return there would leave elements hidden by a\n * rule that had just started applying again.\n *\n * ## No layout shift\n *\n * Every keyframe animates `opacity` and `transform` only. Neither affects\n * layout, so an entrance animation contributes nothing to CLS — the element\n * occupies its final box from the first frame.\n *\n * ## Stagger costs no extra JS and no extra observer entry\n *\n * A staggered row is ONE element as far as this file's runtime is concerned:\n * the host is what the observer watches, and the children play off the class\n * the observer puts on it. The per-child offset is a `:nth-child` ladder in\n * the sheet, so twelve cards arriving one after another cost twelve CSS\n * declarations and zero JS — where a runtime would have cost twelve\n * observer entries, twelve callbacks and twelve style writes on the main\n * thread, during scroll.\n */\n\nimport {\n ANIMATION_CLASS,\n ANIMATION_DELAY_VAR,\n ANIMATION_DURATION_VAR,\n ANIMATION_EASE_CLASS_PREFIX,\n ANIMATION_EASINGS,\n ANIMATION_GROUP_CLASS,\n ANIMATION_IN_CLASS,\n ANIMATION_PRESET_CLASS_PREFIX,\n ANIMATION_READY_CLASS,\n ANIMATION_REPEAT_ATTR,\n ANIMATION_STAGGER_MAX_CHILDREN,\n ANIMATION_STAGGER_OFFSET_VAR,\n ANIMATION_STAGGER_STEP_VAR,\n ANIMATION_TRIGGER_ATTR,\n NODE_ANIMATION_TRIGGER_PROP,\n nodePropsAnimate,\n type AnimationEase,\n} from '@aglyn/aglyn/server'\n\n/**\n * The easing curves, keyed by the ids `@aglyn/aglyn` publishes.\n *\n * The ids live in the shared module and the CURVES live here, so a page that\n * animates nothing carries neither. Every one of these is a plain\n * `cubic-bezier`; none is a spring, because a spring is not expressible in\n * CSS and buying one would mean buying a JS runtime.\n *\n * `overshoot` is the only curve that leaves the 0–1 range, and it does so on\n * the way IN only (`.34,1.56,.64,1` is the standard \"back out\"), so an\n * element settles rather than oscillating. Like every other rule here it sits\n * inside the reduced-motion gate.\n *\n * Typed as a total record so adding an id to `ANIMATION_EASINGS` without a\n * curve is a compile error rather than an element with no easing.\n */\nconst EASE_CURVES: Record<AnimationEase, string> = {\n smooth: 'cubic-bezier(.16,1,.3,1)',\n steady: 'linear',\n 'gentle-start': 'cubic-bezier(.4,0,1,1)',\n 'gentle-end': 'cubic-bezier(0,0,.2,1)',\n 'gentle-both': 'cubic-bezier(.4,0,.2,1)',\n overshoot: 'cubic-bezier(.34,1.56,.64,1)',\n}\n\n/**\n * The curve an element gets when it carries no easing class at all — which is\n * every element authored before easing shipped. Deliberately the same string\n * `EASE` used to be, so nothing that already exists moves.\n */\nconst EASE = EASE_CURVES.smooth\n\n/**\n * Per-preset custom properties. One rule per preset sets the keyframe name\n * used by the load/scroll triggers AND the transform the hover trigger eases\n * to, which is why the preset table is a single list rather than three.\n *\n * Hover is a TRANSITION to an emphasis state, not a keyframe run: \"Slide up\"\n * on hover lifts the element, \"Zoom in\" grows it. That is what an author means\n * when they pick a direction for a hover effect.\n */\nconst PRESETS: Array<{\n id: string\n /** `from` state of the entrance keyframe. */\n from: string\n /** Hover emphasis transform. */\n hover: string\n /** Hover emphasis opacity. */\n hoverOpacity?: string\n}> = [\n { id: 'fade', from: 'opacity:0', hover: 'none', hoverOpacity: '.7' },\n {\n id: 'slide-up',\n from: 'opacity:0;transform:translate3d(0,24px,0)',\n hover: 'translate3d(0,-8px,0)',\n },\n {\n id: 'slide-down',\n from: 'opacity:0;transform:translate3d(0,-24px,0)',\n hover: 'translate3d(0,8px,0)',\n },\n {\n id: 'slide-left',\n from: 'opacity:0;transform:translate3d(24px,0,0)',\n hover: 'translate3d(-8px,0,0)',\n },\n {\n id: 'slide-right',\n from: 'opacity:0;transform:translate3d(-24px,0,0)',\n hover: 'translate3d(8px,0,0)',\n },\n {\n id: 'zoom-in',\n from: 'opacity:0;transform:scale3d(.92,.92,1)',\n hover: 'scale3d(1.04,1.04,1)',\n },\n {\n id: 'zoom-out',\n from: 'opacity:0;transform:scale3d(1.08,1.08,1)',\n hover: 'scale3d(.96,.96,1)',\n },\n]\n\nconst NAME_VAR = '--aglyn-anim-name'\nconst HOVER_VAR = '--aglyn-anim-hover'\nconst HOVER_OPACITY_VAR = '--aglyn-anim-hover-opacity'\n/**\n * Set by the easing class rules and read by the timing rules. Not exported:\n * the renderer writes a CLASS, never this property, so an author cannot get a\n * raw curve of their own into the page through it.\n */\nconst EASE_VAR = '--aglyn-anim-ease'\n\nconst ANIMATED = `.${ANIMATION_CLASS}`\nconst GROUP = `.${ANIMATION_GROUP_CLASS}`\n/**\n * Everything the timing rules apply to: a plain animated element, and the\n * CHILDREN of a stagger host. The host itself is deliberately absent — it\n * sets the custom properties its children inherit and animates nothing.\n */\nconst TARGETS = `${ANIMATED},${GROUP}>*`\nconst DURATION = `var(${ANIMATION_DURATION_VAR},600ms)`\nconst DELAY = `var(${ANIMATION_DELAY_VAR},0ms)`\nconst STEP = `var(${ANIMATION_STAGGER_STEP_VAR},90ms)`\nconst TIMING = `var(${EASE_VAR},${EASE})`\n/**\n * The author's own delay plus whatever rung of the stagger ladder this child\n * landed on. A plain animated element never matches a ladder rule, so its\n * offset falls back to `0ms` and the sum is exactly the delay it had before\n * stagger existed.\n */\nconst TOTAL_DELAY = `calc(${DELAY} + var(${ANIMATION_STAGGER_OFFSET_VAR},0ms))`\n\n/**\n * The stagger ladder: one rule per rung, each pushing a child one more STEP\n * behind the one before it.\n *\n * The index has to come from CSS rather than the renderer. The renderer sees\n * a node, not a position — and a node that knew its index would have to\n * re-render every time a sibling was inserted, which is exactly what an\n * author does while building a row of cards.\n *\n * The first child gets NO rule: `var(--aglyn-anim-stagger,0ms)` already falls\n * back to zero, so emitting `calc(STEP * 0)` would be a rule that changes\n * nothing. The last rung is `:nth-child(n+N)` rather than an Nth rule, so a\n * collection with two hundred rows shares the final rung instead of leaving\n * its tail invisible for half a minute.\n */\nconst STAGGER_LADDER: string[] = [\n // Reset first, and it must STAY first. A nested stagger host's own ladder\n // rule has identical specificity (one class + one pseudo-class against two\n // classes), so source order is the only thing that lets the inner host win\n // for its own children. What this catches is the other case: an animated\n // element deeper inside a staggered child, which would otherwise INHERIT\n // its ancestor's rung and start late for no reason an author could see.\n `${GROUP}>* ${ANIMATED}{${ANIMATION_STAGGER_OFFSET_VAR}:0ms}`,\n ...Array.from(\n { length: ANIMATION_STAGGER_MAX_CHILDREN - 2 },\n (_unused, index) => {\n const nth = index + 2\n return `${GROUP}>*:nth-child(${nth}){${ANIMATION_STAGGER_OFFSET_VAR}:calc(${STEP} * ${nth - 1})}`\n },\n ),\n `${GROUP}>*:nth-child(n+${ANIMATION_STAGGER_MAX_CHILDREN}){${ANIMATION_STAGGER_OFFSET_VAR}:calc(${STEP} * ${\n ANIMATION_STAGGER_MAX_CHILDREN - 1\n })}`,\n]\n\n/**\n * The stylesheet. Built once at module load, not per request.\n *\n * The keyframe name is threaded through a custom property so the trigger rules\n * are two selectors rather than two per preset — the sheet stays flat as\n * presets are added.\n */\nexport const ELEMENT_ANIMATION_STYLE_TEXT = [\n '@media (prefers-reduced-motion:no-preference){',\n // Keyframes.\n ...PRESETS.map(\n (preset) =>\n `@keyframes aglyn-anim-${preset.id}{from{${preset.from}}to{opacity:1;transform:none}}`,\n ),\n // Per-preset custom properties. Keyed on the preset class ALONE, not on the\n // base class as well: a stagger host carries `aglyn-anim-group` rather than\n // `aglyn-anim`, and its children read these through inheritance, so a rule\n // that required the base class would leave a whole staggered row with no\n // keyframe name.\n ...PRESETS.map(\n (preset) =>\n `.${ANIMATION_PRESET_CLASS_PREFIX}${preset.id}{${NAME_VAR}:aglyn-anim-${preset.id};${HOVER_VAR}:${preset.hover}${\n preset.hoverOpacity ? `;${HOVER_OPACITY_VAR}:${preset.hoverOpacity}` : ''\n }}`,\n ),\n // Per-easing custom property, same shape and the same inheritance.\n ...ANIMATION_EASINGS.map(\n (ease) =>\n `.${ANIMATION_EASE_CLASS_PREFIX}${ease}{${EASE_VAR}:${EASE_CURVES[ease]}}`,\n ),\n // Shared timing for the keyframe triggers.\n `${TARGETS}{animation-duration:${DURATION};animation-delay:${TOTAL_DELAY};animation-timing-function:${TIMING};animation-fill-mode:both}`,\n // The stagger ladder — no-ops for every element that is not inside a host.\n ...STAGGER_LADDER,\n // On load: plays as soon as the element is parsed. No JS involved at all.\n `${ANIMATED}[${ANIMATION_TRIGGER_ATTR}=load],${GROUP}[${ANIMATION_TRIGGER_ATTR}=load]>*{animation-name:var(${NAME_VAR})}`,\n // On scroll: plays when the runtime marks it as entered. For a stagger host\n // the ONE observed element is the host, and its children play off its class\n // — one observer entry for a whole row, not one per card.\n `${ANIMATED}[${ANIMATION_TRIGGER_ATTR}=scroll].${ANIMATION_IN_CLASS},${GROUP}[${ANIMATION_TRIGGER_ATTR}=scroll].${ANIMATION_IN_CLASS}>*{animation-name:var(${NAME_VAR})}`,\n // The ONLY rule that hides anything, and it is scoped under a class the\n // inline runtime adds to <html>. No JS (or no IntersectionObserver) means\n // the class is never added, the rule never matches, and every element is\n // visible in its final state. `opacity` rather than `display`/`visibility`\n // so the content stays in the accessibility tree and in the DOM a crawler\n // reads either way.\n `html.${ANIMATION_READY_CLASS} ${ANIMATED}[${ANIMATION_TRIGGER_ATTR}=scroll]:not(.${ANIMATION_IN_CLASS}),html.${ANIMATION_READY_CLASS} ${GROUP}[${ANIMATION_TRIGGER_ATTR}=scroll]:not(.${ANIMATION_IN_CLASS})>*{opacity:0}`,\n // On hover: a transition, never a keyframe run, so it reverses cleanly when\n // the pointer leaves. Staggering is refused for hover upstream, so this rule\n // reads the author's plain delay rather than the ladder's sum — a\n // transition has no `animation-delay` to add a rung to.\n `${ANIMATED}[${ANIMATION_TRIGGER_ATTR}=hover]{transition:transform ${DURATION} ${TIMING} ${DELAY},opacity ${DURATION} ${TIMING} ${DELAY}}`,\n `${ANIMATED}[${ANIMATION_TRIGGER_ATTR}=hover]:hover{transform:var(${HOVER_VAR},none);opacity:var(${HOVER_OPACITY_VAR},1)}`,\n '}',\n].join('')\n\n/** Id of the injected style tag (idempotence marker, mirrors AGL-562). */\nexport const ELEMENT_ANIMATION_STYLE_ID = 'aglyn-element-animation-style'\n\n/**\n * The scroll runtime, inlined verbatim into a `<script>`.\n *\n * One `IntersectionObserver` for the whole document. Re-scanning is safe and\n * cheap because `observe()` on an already-observed element is a no-op, which\n * is how late-arriving nodes (a deferred lazy tab panel the reader opens, a\n * collection page appending rows) get picked up without a per-element\n * observer. The `MutationObserver` is what makes that correct rather than\n * best-effort: an element inserted after load would otherwise stay at\n * `opacity:0` forever, because the hide rule applies the moment the ready\n * class is on `<html>`.\n *\n * The selector is the trigger ATTRIBUTE alone rather than the base class,\n * because there are now two base classes — an element that animates itself\n * and a stagger host whose children animate. Both carry the attribute, only\n * the renderer ever writes it, and matching on it keeps the runtime unaware\n * that the second shape exists.\n *\n * Written in ES5 with short locals because it ships as source; there is no\n * minifier in this path.\n */\nexport const ELEMENT_ANIMATION_SCRIPT_TEXT = `(function(){var w=window,d=document;if(!w.IntersectionObserver||!w.MutationObserver)return;d.documentElement.classList.add(${JSON.stringify(\n ANIMATION_READY_CLASS,\n)});var S=${JSON.stringify(\n `[${ANIMATION_TRIGGER_ATTR}=scroll]`,\n)},I=${JSON.stringify(ANIMATION_IN_CLASS)},R=${JSON.stringify(\n ANIMATION_REPEAT_ATTR,\n)};var o=new w.IntersectionObserver(function(es){for(var i=0;i<es.length;i++){var e=es[i],t=e.target,r=t.getAttribute(R)==='1';if(e.isIntersecting){t.classList.add(I);if(!r)o.unobserve(t)}else if(r)t.classList.remove(I)}},{rootMargin:'0px 0px -10% 0px'});var q=0,scan=function(){q=0;var n=d.querySelectorAll(S);for(var i=0;i<n.length;i++)o.observe(n[i])};scan();if(d.readyState==='loading')d.addEventListener('DOMContentLoaded',scan);var m=new w.MutationObserver(function(){if(q)return;q=w.requestAnimationFrame?w.requestAnimationFrame(scan):w.setTimeout(scan,0)});var start=function(){if(d.body)m.observe(d.body,{childList:true,subtree:true})};if(d.body)start();else d.addEventListener('DOMContentLoaded',start)})()`\n\nexport interface PageAnimationAssets {\n styleText: string\n /** Present only when the page carries a scroll-triggered element. */\n scriptText: string | null\n}\n\n/**\n * Decides what a page must ship, from the flat node map the route already\n * holds. Returns `null` — the common case — when nothing on the page animates,\n * and the route then renders neither tag.\n *\n * The scroll runtime is withheld separately: a page whose only animations play\n * on load or on hover is pure CSS and ships no script either.\n */\nexport function pageAnimationAssets(\n nodes: Record<string, any> | null | undefined,\n): PageAnimationAssets | null {\n if (!nodes) return null\n let animates = false\n let needsScroll = false\n for (const id in nodes) {\n const props = nodes[id]?.props\n if (!nodePropsAnimate(props)) continue\n animates = true\n // `scroll` is the default trigger, so an unset trigger needs the runtime.\n const trigger = props?.[NODE_ANIMATION_TRIGGER_PROP]\n if (trigger === undefined || trigger === null || trigger === 'scroll') {\n needsScroll = true\n break\n }\n }\n if (!animates) return null\n return {\n styleText: ELEMENT_ANIMATION_STYLE_TEXT,\n scriptText: needsScroll ? ELEMENT_ANIMATION_SCRIPT_TEXT : null,\n }\n}\n"],"names":["ANIMATION_CLASS","ANIMATION_DELAY_VAR","ANIMATION_DURATION_VAR","ANIMATION_EASE_CLASS_PREFIX","ANIMATION_EASINGS","ANIMATION_GROUP_CLASS","ANIMATION_IN_CLASS","ANIMATION_PRESET_CLASS_PREFIX","ANIMATION_READY_CLASS","ANIMATION_REPEAT_ATTR","ANIMATION_STAGGER_MAX_CHILDREN","ANIMATION_STAGGER_OFFSET_VAR","ANIMATION_STAGGER_STEP_VAR","ANIMATION_TRIGGER_ATTR","NODE_ANIMATION_TRIGGER_PROP","nodePropsAnimate","EASE_CURVES","smooth","steady","overshoot","EASE","PRESETS","id","from","hover","hoverOpacity","NAME_VAR","HOVER_VAR","HOVER_OPACITY_VAR","EASE_VAR","ANIMATED","GROUP","TARGETS","DURATION","DELAY","STEP","TIMING","TOTAL_DELAY","STAGGER_LADDER","Array","length","_unused","index","nth","ELEMENT_ANIMATION_STYLE_TEXT","map","preset","ease","join","ELEMENT_ANIMATION_STYLE_ID","ELEMENT_ANIMATION_SCRIPT_TEXT","JSON","stringify","pageAnimationAssets","nodes","animates","needsScroll","props","trigger","undefined","styleText","scriptText"],"mappings":"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"}
|
|
@@ -0,0 +1,29 @@
|
|
|
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 type { HostActionAlert, HostEventType } from '@aglyn/aglyn/server';
|
|
18
|
+
import { type HostEventPayload } from './host-event-listeners';
|
|
19
|
+
/**
|
|
20
|
+
* One emit point for host events (AGL-148): hands the event to every
|
|
21
|
+
* registered host-event listener (see `host-event-listeners.ts`) and returns
|
|
22
|
+
* the site alerts they produced, so request/response emitters (form submit,
|
|
23
|
+
* booking) can surface them; fire-and-forget emitters ignore the result. No
|
|
24
|
+
* listener throws into the emitting request.
|
|
25
|
+
*/
|
|
26
|
+
export declare function emitHostEvent(hostId: string, event: HostEventType, payload?: HostEventPayload): Promise<{
|
|
27
|
+
alerts: HostActionAlert[];
|
|
28
|
+
}>;
|
|
29
|
+
export default emitHostEvent;
|