@aglyn/aglyn 1.0.0-beta.266 → 1.0.0-beta.269
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +12 -12
- package/src/lib/app-utils/docs-index.generated.js +1 -1
- package/src/lib/app-utils/docs-index.generated.js.map +1 -1
- package/src/lib/app-utils/element-motion.d.ts +14 -2
- package/src/lib/app-utils/element-motion.js +33 -13
- package/src/lib/app-utils/element-motion.js.map +1 -1
- package/src/lib/app-utils/health-report.d.ts +78 -6
- package/src/lib/app-utils/health-report.js +113 -6
- package/src/lib/app-utils/health-report.js.map +1 -1
- package/src/lib/app-utils/notifications.js +6 -10
- package/src/lib/app-utils/notifications.js.map +1 -1
- package/src/lib/app-utils/upload-inspection.d.ts +9 -0
- package/src/lib/app-utils/upload-inspection.js +27 -0
- package/src/lib/app-utils/upload-inspection.js.map +1 -1
- package/src/lib/plugin-manager/operator-alerts.js +10 -1
- package/src/lib/plugin-manager/operator-alerts.js.map +1 -1
- package/src/lib/plugin-manager/plugin-config.js +10 -1
- package/src/lib/plugin-manager/plugin-config.js.map +1 -1
- package/src/lib/plugin-manager/plugin-consent-group-change.js +11 -2
- package/src/lib/plugin-manager/plugin-consent-group-change.js.map +1 -1
- package/src/lib/plugin-manager/plugin-console-crons.js +10 -1
- package/src/lib/plugin-manager/plugin-console-crons.js.map +1 -1
- package/src/lib/plugin-manager/plugin-events.js +11 -2
- package/src/lib/plugin-manager/plugin-events.js.map +1 -1
- package/src/lib/plugin-manager/plugin-lead-conversion.js +10 -1
- package/src/lib/plugin-manager/plugin-lead-conversion.js.map +1 -1
- package/src/lib/plugin-manager/plugin-membership-detach.js +10 -1
- package/src/lib/plugin-manager/plugin-membership-detach.js.map +1 -1
- package/src/lib/plugin-manager/plugin-person-matches.js +10 -1
- package/src/lib/plugin-manager/plugin-person-matches.js.map +1 -1
- package/src/lib/plugin-manager/plugin-site-erase.d.ts +81 -0
- package/src/lib/plugin-manager/plugin-site-erase.js +56 -0
- package/src/lib/plugin-manager/plugin-site-erase.js.map +1 -0
- package/src/lib/plugin-manager/plugin-usage-meters.js +10 -1
- package/src/lib/plugin-manager/plugin-usage-meters.js.map +1 -1
- package/src/lib/plugin-manager/usage-alert-contributors.js +10 -1
- package/src/lib/plugin-manager/usage-alert-contributors.js.map +1 -1
|
@@ -44,10 +44,12 @@ import { type AnimationEase, type AnimationPreset } from './element-animation';
|
|
|
44
44
|
* sides when the character says {@link ElementMotionCharacter.sides}:
|
|
45
45
|
* the left column from the left, the right from the right, the second a
|
|
46
46
|
* stagger step after the first. A picture whose preset zooms keeps its
|
|
47
|
-
* zoom.
|
|
47
|
+
* zoom. Without sides its columns still arrive one after the other, the
|
|
48
|
+
* picture's in the media preset and the words' in the section's.
|
|
48
49
|
* - A group of like items — a grid of cards, a list, the questions of an
|
|
49
50
|
* FAQ, a dataset repeated over one card — STAGGERS its items, one after
|
|
50
|
-
* another, rather than arriving as one block
|
|
51
|
+
* another, rather than arriving as one block, in the character's {@link
|
|
52
|
+
* ElementMotionCharacter.groupPreset} where it names one.
|
|
51
53
|
* - A picture, and a group whose items each lead with one, takes the
|
|
52
54
|
* character's media preset.
|
|
53
55
|
* - Never touched: forms and their fields, navigation and the site's frame,
|
|
@@ -57,6 +59,10 @@ import { type AnimationEase, type AnimationPreset } from './element-animation';
|
|
|
57
59
|
* `none` included, is never overwritten. A block that HOLDS one of those
|
|
58
60
|
* is not animated as a whole either; its other children are animated one
|
|
59
61
|
* by one instead, so a form never slides in with its section.
|
|
62
|
+
* - Except a picture's FRAME: a box in the flow whose picture is laid over
|
|
63
|
+
* it (`position: absolute`), as the AI's layout compiler draws every
|
|
64
|
+
* picture slot. The frame is the picture, so it moves as one, picture and
|
|
65
|
+
* all, and counts as a picture for the media preset and for a split.
|
|
60
66
|
* - Never nested: once a block animates, nothing inside it does. Two
|
|
61
67
|
* entrances on the same pixels multiply their fades and add their
|
|
62
68
|
* offsets, which reads as a stutter, not as motion.
|
|
@@ -87,6 +93,12 @@ export interface ElementMotionCharacter {
|
|
|
87
93
|
sectionPresets?: readonly AnimationPreset[];
|
|
88
94
|
/** Whether a split's two columns come in from their own sides. */
|
|
89
95
|
sides?: boolean;
|
|
96
|
+
/**
|
|
97
|
+
* What a group of like items — a grid of cards, a list, an FAQ — arrives
|
|
98
|
+
* with, whatever its section's turn; the section's own preset when absent.
|
|
99
|
+
* A group whose every item leads with a picture still takes {@link mediaPreset}.
|
|
100
|
+
*/
|
|
101
|
+
groupPreset?: AnimationPreset;
|
|
90
102
|
}
|
|
91
103
|
/** The motion a page gets when nothing says otherwise: a soft rise, as the attributes panel defaults to. */
|
|
92
104
|
export declare const ELEMENT_MOTION_DEFAULT: ElementMotionCharacter;
|
|
@@ -135,25 +135,45 @@ const clamp = (value, max)=>Math.min(Math.max(Math.round(value), 0), max);
|
|
|
135
135
|
};
|
|
136
136
|
const patches = new Map();
|
|
137
137
|
/** The first section's Largest Contentful Paint candidates, painted where they stand. */ const still = new Set();
|
|
138
|
-
/** Left alone
|
|
138
|
+
/** Left alone for what it is, wherever it stands. */ const refused = (id)=>{
|
|
139
139
|
var _at_componentId;
|
|
140
|
-
var _at_sx;
|
|
141
140
|
const at = node(id);
|
|
142
141
|
if (!at) return true;
|
|
143
142
|
if (at.hidden || still.has(id)) return true;
|
|
144
143
|
if (ELEMENT_MOTION_NEVER.has(String((_at_componentId = at.componentId) != null ? _at_componentId : ''))) return true;
|
|
145
|
-
|
|
144
|
+
return !!at.props && NODE_ANIMATION_PROPS.some((prop)=>{
|
|
146
145
|
var _at_props;
|
|
147
146
|
return ((_at_props = at.props) == null ? void 0 : _at_props[prop]) !== undefined;
|
|
148
|
-
})
|
|
149
|
-
|
|
147
|
+
});
|
|
148
|
+
};
|
|
149
|
+
/** Left alone, and so is everything inside it. */ const closed = (id)=>{
|
|
150
|
+
var _node_sx, _node;
|
|
151
|
+
return refused(id) || pins((_node = node(id)) == null ? void 0 : (_node_sx = _node.sx) == null ? void 0 : _node_sx['position']);
|
|
152
|
+
};
|
|
153
|
+
/** A picture laid over its box, free to move with it. */ const overlay = (id)=>{
|
|
154
|
+
var _ref;
|
|
155
|
+
var _node, _node_sx, _node1;
|
|
156
|
+
return MEDIA.has(String((_ref = (_node = node(id)) == null ? void 0 : _node.componentId) != null ? _ref : '')) && pins((_node1 = node(id)) == null ? void 0 : (_node_sx = _node1.sx) == null ? void 0 : _node_sx['position']) && !refused(id);
|
|
157
|
+
};
|
|
158
|
+
/**
|
|
159
|
+
* A picture's frame: a box in the flow whose picture is laid over it
|
|
160
|
+
* (`position: absolute`, filling it), beside at most the placeholder icon
|
|
161
|
+
* an empty slot shows. The frame IS the picture, so it moves as one,
|
|
162
|
+
* picture and all; its picture being out of the flow does not hold it still
|
|
163
|
+
* the way a cover's backdrop holds its band, which is itself out of the flow.
|
|
164
|
+
*/ const isFrame = (id)=>{
|
|
165
|
+
const kids = children(id);
|
|
166
|
+
return !closed(id) && kids.some(overlay) && kids.every((child)=>{
|
|
167
|
+
var _node;
|
|
168
|
+
return overlay(child) || ((_node = node(child)) == null ? void 0 : _node.componentId) === 'icon' && !refused(child) && !children(child).length;
|
|
169
|
+
});
|
|
150
170
|
};
|
|
151
171
|
const holdsClosed = new Map();
|
|
152
172
|
/** The element, or one inside it, is left alone: it cannot move as one block. */ const holds = (id)=>{
|
|
153
173
|
const known = holdsClosed.get(id);
|
|
154
174
|
if (known !== undefined) return known;
|
|
155
175
|
holdsClosed.set(id, false);
|
|
156
|
-
const result = closed(id) || children(id).some(holds);
|
|
176
|
+
const result = closed(id) || !isFrame(id) && children(id).some(holds);
|
|
157
177
|
holdsClosed.set(id, result);
|
|
158
178
|
return result;
|
|
159
179
|
};
|
|
@@ -195,12 +215,12 @@ const clamp = (value, max)=>Math.min(Math.max(Math.round(value), 0), max);
|
|
|
195
215
|
belowMemo.set(id, result);
|
|
196
216
|
return result;
|
|
197
217
|
};
|
|
198
|
-
/** Leads with a picture: its first leaf, down its first children, is one. */ const leadsWithMedia = (id)=>{
|
|
218
|
+
/** Leads with a picture: its first leaf, down its first children, is one, or the frame of one. */ const leadsWithMedia = (id)=>{
|
|
199
219
|
let at = id;
|
|
200
220
|
for(let depth = 0; depth < 12; depth += 1){
|
|
201
221
|
var _ref;
|
|
202
222
|
var _node;
|
|
203
|
-
if (MEDIA.has(String((_ref = (_node = node(at)) == null ? void 0 : _node.componentId) != null ? _ref : ''))) return true;
|
|
223
|
+
if (MEDIA.has(String((_ref = (_node = node(at)) == null ? void 0 : _node.componentId) != null ? _ref : '')) || isFrame(at)) return true;
|
|
204
224
|
const kids = children(at);
|
|
205
225
|
if (!kids.length) return false;
|
|
206
226
|
at = kids[0];
|
|
@@ -213,7 +233,7 @@ const clamp = (value, max)=>Math.min(Math.max(Math.round(value), 0), max);
|
|
|
213
233
|
};
|
|
214
234
|
const animate = (id, trigger, delayMs, words = character.preset, side)=>{
|
|
215
235
|
const group = isGroup(id);
|
|
216
|
-
const preset = presetOf(id, group, words);
|
|
236
|
+
const preset = presetOf(id, group, group && character.groupPreset ? character.groupPreset : words);
|
|
217
237
|
patches.set(id, _extends({
|
|
218
238
|
[NODE_ANIMATION_PROP]: side && !preset.startsWith('zoom') ? side : preset,
|
|
219
239
|
[NODE_ANIMATION_TRIGGER_PROP]: trigger,
|
|
@@ -246,10 +266,10 @@ const clamp = (value, max)=>Math.min(Math.max(Math.round(value), 0), max);
|
|
|
246
266
|
};
|
|
247
267
|
/** A later section's content: on scroll, groups staggered, splits from their sides, nothing nested. */ const place = (id, words)=>{
|
|
248
268
|
if (closed(id)) return;
|
|
249
|
-
if (
|
|
269
|
+
if (isSplit(id)) {
|
|
250
270
|
const [start, end] = children(id);
|
|
251
|
-
animate(start, 'scroll', 0, words, 'slide-right');
|
|
252
|
-
animate(end, 'scroll', character.staggerStepMs, words, 'slide-left');
|
|
271
|
+
animate(start, 'scroll', 0, words, character.sides ? 'slide-right' : undefined);
|
|
272
|
+
animate(end, 'scroll', character.staggerStepMs, words, character.sides ? 'slide-left' : undefined);
|
|
253
273
|
return;
|
|
254
274
|
}
|
|
255
275
|
if (holds(id) || !isGroup(id) && holdsGroup(id)) {
|
|
@@ -349,7 +369,7 @@ const clamp = (value, max)=>Math.min(Math.max(Math.round(value), 0), max);
|
|
|
349
369
|
if (content === id && !children(id).length) return;
|
|
350
370
|
const words = (_turns_ = turns[(index - 1) % turns.length]) != null ? _turns_ : character.preset;
|
|
351
371
|
if (index === 0) ladder(content);
|
|
352
|
-
else if (content === id && !isGroup(id) && !
|
|
372
|
+
else if (content === id && !isGroup(id) && !isSplit(id)) for (const child of children(id))place(child, words);
|
|
353
373
|
else place(content, words);
|
|
354
374
|
});
|
|
355
375
|
if (!patches.size) return nodes;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../../../../libs/aglyn/src/lib/app-utils/element-motion.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 { CANVAS_ROOT_ELEMENT_ID } from '../foundation/constants/canvas'\nimport {\n ANIMATION_MAX_DELAY_MS,\n ANIMATION_MAX_DURATION_MS,\n ANIMATION_MAX_STAGGER_STEP_MS,\n NODE_ANIMATION_DELAY_PROP,\n NODE_ANIMATION_DURATION_PROP,\n NODE_ANIMATION_EASE_PROP,\n NODE_ANIMATION_PROPS,\n NODE_ANIMATION_PROP,\n NODE_ANIMATION_STAGGER_PROP,\n NODE_ANIMATION_STAGGER_STEP_PROP,\n NODE_ANIMATION_TRIGGER_PROP,\n type AnimationEase,\n type AnimationPreset,\n type AnimationTrigger,\n} from './element-animation'\n\n/**\n * Entrance motion for a whole page (AGL-3838): the pass that writes the\n * existing animation props (`element-animation.ts`) onto a page built by\n * code — a starter, a new site's home, a page the AI composed — so it moves\n * the way an author would have set it up by hand in the attributes panel.\n *\n * Nothing new is rendered or shipped: the pass only writes the reserved\n * `aglynAnimation*` props the renderer and the tenant's stylesheet already\n * read, with values from their own vocabulary. A page that moves pays the\n * stylesheet and the scroll observer it always paid; a page this pass leaves\n * alone pays nothing.\n *\n * ## The rules\n *\n * - The FIRST section's secondary content — its eyebrow, its lede, its\n * buttons, a small picture — plays on `load`, as a short ladder: each\n * block starts {@link ElementMotionCharacter.heroStepMs} after the one\n * before. Its main heading and its largest picture, the candidates for the\n * page's Largest Contentful Paint, never move: they are painted where\n * they stand from the first frame. Its band — a background color or a\n * cover photo — never moves either.\n * - Every later section's content slides in on `scroll`, each section in\n * turn taking the next of the character's {@link\n * ElementMotionCharacter.sectionPresets}, so a page does not arrive as\n * one repeated move.\n * - A split — a column of words beside a picture — comes in from its\n * sides when the character says {@link ElementMotionCharacter.sides}:\n * the left column from the left, the right from the right, the second a\n * stagger step after the first. A picture whose preset zooms keeps its\n * zoom.\n * - A group of like items — a grid of cards, a list, the questions of an\n * FAQ, a dataset repeated over one card — STAGGERS its items, one after\n * another, rather than arriving as one block.\n * - A picture, and a group whose items each lead with one, takes the\n * character's media preset.\n * - Never touched: forms and their fields, navigation and the site's frame,\n * anything out of the flow (`position: fixed | sticky | absolute` — a\n * cover photo's backdrop is its band's), anything hidden, and\n * anything that already carries an animation prop — an author's choice,\n * `none` included, is never overwritten. A block that HOLDS one of those\n * is not animated as a whole either; its other children are animated one\n * by one instead, so a form never slides in with its section.\n * - Never nested: once a block animates, nothing inside it does. Two\n * entrances on the same pixels multiply their fades and add their\n * offsets, which reads as a stutter, not as motion.\n *\n * Pure and idempotent: a page it already moved comes back as the same\n * object, which is what lets a caller compare by identity to skip a write.\n *\n * Leaf imports only, the same as `starter-template-nodes.ts`: the starter\n * kit reads this from the console's client gallery and its seed route alike.\n */\n\n/** A page's motion: the presets, curve and timing every entrance on it shares. */\nexport interface ElementMotionCharacter {\n /** Words and blocks: a section's content, a group of cards. */\n preset: AnimationPreset\n /** A picture, or a group whose every item leads with one. */\n mediaPreset: AnimationPreset\n ease: AnimationEase\n durationMs: number\n /** Gap between the items of a staggered group. */\n staggerStepMs: number\n /** Gap between the rungs of the first section's load ladder. */\n heroStepMs: number\n /**\n * The presets later sections take in turn, the second section the first of\n * them; {@link preset} alone when absent. A section's pictures still take\n * {@link mediaPreset}.\n */\n sectionPresets?: readonly AnimationPreset[]\n /** Whether a split's two columns come in from their own sides. */\n sides?: boolean\n}\n\n/** The motion a page gets when nothing says otherwise: a soft rise, as the attributes panel defaults to. */\nexport const ELEMENT_MOTION_DEFAULT: ElementMotionCharacter = {\n preset: 'slide-up',\n mediaPreset: 'fade',\n ease: 'smooth',\n durationMs: 600,\n staggerStepMs: 90,\n heroStepMs: 120,\n sectionPresets: ['slide-up', 'fade'],\n sides: true,\n}\n\n/** The most rungs the first section's ladder climbs; later blocks share the last. */\nexport const ELEMENT_MOTION_HERO_RUNGS = 6\n\n/**\n * Elements that are never animated, nor animated as part of a block that\n * holds them: forms and every field of one, and the site's navigation and\n * frame. A field that slides in under a visitor's cursor is a field they\n * miss, and a header that moves is a site that looks broken.\n */\nexport const ELEMENT_MOTION_NEVER: ReadonlySet<string> = new Set([\n // Forms and their fields.\n 'form',\n 'formField',\n 'muiFormControl',\n 'muiFormLabel',\n 'muiFormControlLabel',\n 'muiFormHelperText',\n 'muiInput',\n 'muiSelect',\n 'muiSelectOption',\n 'muiRadioGroup',\n 'muiRadio',\n 'muiCheckbox',\n 'muiRating',\n 'muiDatePicker',\n 'muiTimePicker',\n 'newsletter-signup',\n 'member-signin',\n 'member-signup',\n 'member-recovery',\n 'booking',\n 'reservation-widget',\n 'customer-account',\n 'return-request',\n 'order-status',\n 'searchBox',\n 'collectionSearch',\n // Navigation and the site's frame.\n 'layoutSlot',\n 'muiAppBar',\n 'muiToolbar',\n 'muiDrawer',\n 'muiDrawerToggle',\n 'muiNavMenu',\n 'muiMegaMenu',\n 'muiBreadcrumbs',\n 'muiTabs',\n 'languageSwitcher',\n 'themeModeSwitcher',\n 'cart',\n])\n\n/** Pictures: what the media preset is for. */\nconst MEDIA: ReadonlySet<string> = new Set(['image', 'video', 'videoEmbed', 'gated-video'])\n\n/** Components that list like items whatever their children are. */\nconst LISTS: ReadonlySet<string> = new Set(['muiList', 'muiImageList', 'collectionEntries'])\n\n/**\n * What a group's items are made of when they are alike: cards, cells,\n * panels, pictures. Words and buttons never count, however alike — a row of\n * two buttons is a call to action, not a group of cards.\n */\nconst ITEMS: ReadonlySet<string> = new Set([\n 'muiCard',\n 'muiPaper',\n 'muiGrid',\n 'muiStack',\n 'muiBox',\n 'muiLinkBox',\n 'muiListItem',\n 'muiImageListItem',\n 'muiAccordion',\n 'reusableInstance',\n 'div',\n 'image',\n 'video',\n])\n\ninterface MotionNode {\n componentId?: string\n props?: Record<string, unknown>\n sx?: Record<string, unknown>\n nodes?: string[]\n hidden?: boolean\n}\n\nexport interface ElementMotionOptions {\n /** The page's root; the canvas root when absent. */\n rootId?: string\n /** The page's sections in order; the root's own children when absent. */\n sectionIds?: readonly string[]\n character?: ElementMotionCharacter\n}\n\nconst clamp = (value: number, max: number): number => Math.min(Math.max(Math.round(value), 0), max)\n\n/**\n * Whether a style value — plain or one per breakpoint — takes the element out\n * of the flow: pinned to the screen, or laid over its band, as a cover\n * photo's backdrop and its scrim are. Either is part of the band, not of what\n * the band holds.\n */\nfunction pins(value: unknown): boolean {\n if (value === 'fixed' || value === 'sticky' || value === 'absolute') return true\n return typeof value === 'object' && value !== null && Object.values(value).some(pins)\n}\n\n/**\n * Writes entrance motion onto a page's nodes by the rules above, and returns\n * the new node map — or `nodes` itself, unchanged, when there was nothing to\n * move.\n */\nexport function applyElementMotion<T extends Record<string, any>>(nodes: T, options: ElementMotionOptions = {}): T {\n if (!nodes) return nodes\n const character = options.character ?? ELEMENT_MOTION_DEFAULT\n const rootId = options.rootId ?? CANVAS_ROOT_ELEMENT_ID\n const node = (id: string): MotionNode | undefined => nodes[id] as MotionNode | undefined\n const children = (id: string): string[] => (node(id)?.nodes ?? []).filter((child) => typeof child === 'string' && !!node(child))\n const patches = new Map<string, Record<string, unknown>>()\n\n /** The first section's Largest Contentful Paint candidates, painted where they stand. */\n const still = new Set<string>()\n /** Left alone, and so is everything inside it. */\n const closed = (id: string): boolean => {\n const at = node(id)\n if (!at) return true\n if (at.hidden || still.has(id)) return true\n if (ELEMENT_MOTION_NEVER.has(String(at.componentId ?? ''))) return true\n if (at.props && NODE_ANIMATION_PROPS.some((prop) => at.props?.[prop] !== undefined)) return true\n return pins(at.sx?.['position'])\n }\n const holdsClosed = new Map<string, boolean>()\n /** The element, or one inside it, is left alone: it cannot move as one block. */\n const holds = (id: string): boolean => {\n const known = holdsClosed.get(id)\n if (known !== undefined) return known\n holdsClosed.set(id, false)\n const result = closed(id) || children(id).some(holds)\n holdsClosed.set(id, result)\n return result\n }\n const groupMemo = new Map<string, boolean>()\n const belowMemo = new Map<string, boolean>()\n /**\n * A group of like items, whose items stagger. A grid or a row of panels\n * that holds a group of its own is the LAYOUT around it — a heading beside\n * its cards — so it is opened up and the group inside staggers instead.\n */\n const isGroup = (id: string): boolean => {\n const known = groupMemo.get(id)\n if (known !== undefined) return known\n const at = node(id) as MotionNode\n const componentId = String(at.componentId ?? '')\n const kids = children(id)\n let result: boolean\n // A dataset repeated over one card renders one child per record.\n if ((at.props?.['repeatDataset'] || LISTS.has(componentId)) && kids.length) result = true\n else if (kids.length < 2) result = false\n else {\n const first = String(node(kids[0])?.componentId ?? '')\n const alike =\n (componentId === 'muiGrid' && !!at.props?.['container']) ||\n (ITEMS.has(first) && kids.every((child) => node(child)?.componentId === first))\n result = alike && !holdsGroup(id)\n }\n groupMemo.set(id, result)\n return result\n }\n /** A group sits somewhere inside the element. */\n const holdsGroup = (id: string): boolean => {\n const known = belowMemo.get(id)\n if (known !== undefined) return known\n const result = children(id).some((child) => isGroup(child) || holdsGroup(child))\n belowMemo.set(id, result)\n return result\n }\n /** Leads with a picture: its first leaf, down its first children, is one. */\n const leadsWithMedia = (id: string): boolean => {\n let at = id\n for (let depth = 0; depth < 12; depth += 1) {\n if (MEDIA.has(String(node(at)?.componentId ?? ''))) return true\n const kids = children(at)\n if (!kids.length) return false\n at = kids[0]\n }\n return false\n }\n const presetOf = (id: string, group: boolean, words: AnimationPreset): AnimationPreset => {\n const media = group ? children(id).every(leadsWithMedia) : leadsWithMedia(id)\n return media ? character.mediaPreset : words\n }\n\n const animate = (\n id: string,\n trigger: AnimationTrigger,\n delayMs: number,\n words: AnimationPreset = character.preset,\n side?: AnimationPreset,\n ): void => {\n const group = isGroup(id)\n const preset = presetOf(id, group, words)\n patches.set(id, {\n [NODE_ANIMATION_PROP]: side && !preset.startsWith('zoom') ? side : preset,\n [NODE_ANIMATION_TRIGGER_PROP]: trigger,\n [NODE_ANIMATION_DURATION_PROP]: clamp(character.durationMs, ANIMATION_MAX_DURATION_MS),\n ...(delayMs > 0 ? { [NODE_ANIMATION_DELAY_PROP]: clamp(delayMs, ANIMATION_MAX_DELAY_MS) } : {}),\n [NODE_ANIMATION_EASE_PROP]: character.ease,\n ...(group\n ? {\n [NODE_ANIMATION_STAGGER_PROP]: true,\n [NODE_ANIMATION_STAGGER_STEP_PROP]: clamp(character.staggerStepMs, ANIMATION_MAX_STAGGER_STEP_MS),\n }\n : {}),\n })\n }\n\n /** Down through wrappers of one child, to where a section's content starts. */\n const contentOf = (id: string): string => {\n let at = id\n while (!closed(at) && !isGroup(at)) {\n const kids = children(at)\n if (kids.length !== 1) break\n at = kids[0]\n }\n return at\n }\n\n /**\n * A column of words beside a picture: two blocks, one of which leads with a\n * picture and one of which does not, neither left alone. Two cards that\n * both lead with one are a group, and stagger.\n */\n const isSplit = (id: string): boolean => {\n const kids = children(id)\n return kids.length === 2 && leadsWithMedia(kids[0]) !== leadsWithMedia(kids[1]) && !kids.some(holds)\n }\n\n /** A later section's content: on scroll, groups staggered, splits from their sides, nothing nested. */\n const place = (id: string, words: AnimationPreset): void => {\n if (closed(id)) return\n if (character.sides && isSplit(id)) {\n const [start, end] = children(id)\n animate(start, 'scroll', 0, words, 'slide-right')\n animate(end, 'scroll', character.staggerStepMs, words, 'slide-left')\n return\n }\n if (holds(id) || (!isGroup(id) && holdsGroup(id))) {\n for (const child of children(id)) place(child, words)\n return\n }\n animate(id, 'scroll', 0, words)\n }\n\n /** Down through wrappers of one child that are free to move. */\n const descend = (id: string): string => {\n let at = id\n for (let kids = children(at); kids.length === 1 && !closed(kids[0]); kids = children(at)) at = kids[0]\n return at\n }\n\n /**\n * The first section's content: on load, one rung per block, around the\n * blocks that stay still. A rung that holds one is opened up, down to the\n * parts beside it, so the eyebrow, the lede and the buttons still climb.\n */\n const ladder = (id: string): void => {\n if (closed(id)) return\n // A repeat's one child is the card every record is drawn with: the\n // repeat staggers its records rather than each record playing at once.\n if (isGroup(id) && children(id).length < 2) return animate(id, 'load', 0)\n const around = (at: string): string[] =>\n children(at).flatMap((child) => (closed(child) ? [] : holds(child) ? around(child) : [child]))\n const base = children(id).length ? around(id) : [id]\n // A column of words beside a picture is opened up, so its lines climb one\n // after another before the picture lands.\n const rungs: string[] = []\n base.forEach((rung, index) => {\n const inner = descend(rung)\n const kids = children(inner)\n const opens =\n kids.length >= 2 &&\n !isGroup(inner) &&\n !leadsWithMedia(inner) &&\n rungs.length + kids.length + (base.length - index - 1) <= ELEMENT_MOTION_HERO_RUNGS\n rungs.push(...(opens ? kids : [rung]))\n })\n // A rung left alone takes no step of the ladder: the first block that moves starts at once.\n rungs\n .filter((rung) => !closed(rung))\n .forEach((rung, index) => animate(rung, 'load', Math.min(index, ELEMENT_MOTION_HERO_RUNGS - 1) * character.heroStepMs))\n }\n\n /**\n * What the first section paints first, and so what Lighthouse times as its\n * Largest Contentful Paint (AGL-3581): its main heading — the h1, else its\n * first heading — and its largest picture. Neither ever starts at opacity 0,\n * which would hold the paint back by the length of the entrance.\n */\n const markStill = (sectionId: string): void => {\n const order: string[] = []\n const walk = (id: string): void => {\n order.push(id)\n for (const child of children(id)) walk(child)\n }\n walk(sectionId)\n const typeOf = (id: string) => {\n const at = node(id)\n return at?.componentId === 'muiTypography' ? String(at.props?.['variant'] ?? '') : null\n }\n const main = (id: string) => /^(h1|display)/i.test(typeOf(id) ?? '') || (typeOf(id) !== null && node(id)?.props?.['component'] === 'h1')\n const heading = order.find(main) ?? order.find((id) => /^(h[1-6]|display)/i.test(typeOf(id) ?? ''))\n if (heading) still.add(heading)\n const size = (id: string): number => {\n const at = node(id)\n const read = (value: unknown) => (typeof value === 'number' ? value : parseFloat(String(value ?? '')) || 0)\n return Math.max(read(at?.props?.['height']), read(at?.props?.['width']), read(at?.sx?.['height']), read(at?.sx?.['width']))\n }\n let picture: string | undefined\n for (const id of order) {\n if (MEDIA.has(String(node(id)?.componentId ?? '')) && (picture === undefined || size(id) > size(picture))) picture = id\n }\n if (picture) still.add(picture)\n }\n\n const sections = (options.sectionIds ?? children(rootId)).filter((id) => !!node(id))\n if (sections[0]) markStill(sections[0])\n const turns = character.sectionPresets?.length ? character.sectionPresets : [character.preset]\n sections.forEach((id, index) => {\n if (closed(id)) return\n const content = contentOf(id)\n // The band itself never moves — only what it holds — so a section that\n // is nothing but its band has nothing to move.\n if (content === id && !children(id).length) return\n const words = turns[(index - 1) % turns.length] ?? character.preset\n if (index === 0) ladder(content)\n else if (content === id && !isGroup(id) && !(character.sides && isSplit(id)))\n for (const child of children(id)) place(child, words)\n else place(content, words)\n })\n\n if (!patches.size) return nodes\n const next: Record<string, any> = { ...nodes }\n for (const [id, props] of patches) {\n next[id] = { ...nodes[id], props: { ...(nodes[id]?.props ?? {}), ...props } }\n }\n return next as T\n}\n"],"names":["CANVAS_ROOT_ELEMENT_ID","ANIMATION_MAX_DELAY_MS","ANIMATION_MAX_DURATION_MS","ANIMATION_MAX_STAGGER_STEP_MS","NODE_ANIMATION_DELAY_PROP","NODE_ANIMATION_DURATION_PROP","NODE_ANIMATION_EASE_PROP","NODE_ANIMATION_PROPS","NODE_ANIMATION_PROP","NODE_ANIMATION_STAGGER_PROP","NODE_ANIMATION_STAGGER_STEP_PROP","NODE_ANIMATION_TRIGGER_PROP","ELEMENT_MOTION_DEFAULT","preset","mediaPreset","ease","durationMs","staggerStepMs","heroStepMs","sectionPresets","sides","ELEMENT_MOTION_HERO_RUNGS","ELEMENT_MOTION_NEVER","Set","MEDIA","LISTS","ITEMS","clamp","value","max","Math","min","round","pins","Object","values","some","applyElementMotion","nodes","options","character","rootId","node","id","children","filter","child","patches","Map","still","closed","at","hidden","has","String","componentId","props","prop","undefined","sx","holdsClosed","holds","known","get","set","result","groupMemo","belowMemo","isGroup","kids","length","first","alike","every","holdsGroup","leadsWithMedia","depth","presetOf","group","words","media","animate","trigger","delayMs","side","startsWith","contentOf","isSplit","place","start","end","descend","ladder","around","flatMap","base","rungs","forEach","rung","index","inner","opens","push","markStill","sectionId","order","walk","typeOf","main","test","heading","find","add","size","read","parseFloat","picture","sections","sectionIds","turns","content","next"],"mappings":"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"}
|
|
1
|
+
{"version":3,"sources":["../../../../../../libs/aglyn/src/lib/app-utils/element-motion.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 { CANVAS_ROOT_ELEMENT_ID } from '../foundation/constants/canvas'\nimport {\n ANIMATION_MAX_DELAY_MS,\n ANIMATION_MAX_DURATION_MS,\n ANIMATION_MAX_STAGGER_STEP_MS,\n NODE_ANIMATION_DELAY_PROP,\n NODE_ANIMATION_DURATION_PROP,\n NODE_ANIMATION_EASE_PROP,\n NODE_ANIMATION_PROPS,\n NODE_ANIMATION_PROP,\n NODE_ANIMATION_STAGGER_PROP,\n NODE_ANIMATION_STAGGER_STEP_PROP,\n NODE_ANIMATION_TRIGGER_PROP,\n type AnimationEase,\n type AnimationPreset,\n type AnimationTrigger,\n} from './element-animation'\n\n/**\n * Entrance motion for a whole page (AGL-3838): the pass that writes the\n * existing animation props (`element-animation.ts`) onto a page built by\n * code — a starter, a new site's home, a page the AI composed — so it moves\n * the way an author would have set it up by hand in the attributes panel.\n *\n * Nothing new is rendered or shipped: the pass only writes the reserved\n * `aglynAnimation*` props the renderer and the tenant's stylesheet already\n * read, with values from their own vocabulary. A page that moves pays the\n * stylesheet and the scroll observer it always paid; a page this pass leaves\n * alone pays nothing.\n *\n * ## The rules\n *\n * - The FIRST section's secondary content — its eyebrow, its lede, its\n * buttons, a small picture — plays on `load`, as a short ladder: each\n * block starts {@link ElementMotionCharacter.heroStepMs} after the one\n * before. Its main heading and its largest picture, the candidates for the\n * page's Largest Contentful Paint, never move: they are painted where\n * they stand from the first frame. Its band — a background color or a\n * cover photo — never moves either.\n * - Every later section's content slides in on `scroll`, each section in\n * turn taking the next of the character's {@link\n * ElementMotionCharacter.sectionPresets}, so a page does not arrive as\n * one repeated move.\n * - A split — a column of words beside a picture — comes in from its\n * sides when the character says {@link ElementMotionCharacter.sides}:\n * the left column from the left, the right from the right, the second a\n * stagger step after the first. A picture whose preset zooms keeps its\n * zoom. Without sides its columns still arrive one after the other, the\n * picture's in the media preset and the words' in the section's.\n * - A group of like items — a grid of cards, a list, the questions of an\n * FAQ, a dataset repeated over one card — STAGGERS its items, one after\n * another, rather than arriving as one block, in the character's {@link\n * ElementMotionCharacter.groupPreset} where it names one.\n * - A picture, and a group whose items each lead with one, takes the\n * character's media preset.\n * - Never touched: forms and their fields, navigation and the site's frame,\n * anything out of the flow (`position: fixed | sticky | absolute` — a\n * cover photo's backdrop is its band's), anything hidden, and\n * anything that already carries an animation prop — an author's choice,\n * `none` included, is never overwritten. A block that HOLDS one of those\n * is not animated as a whole either; its other children are animated one\n * by one instead, so a form never slides in with its section.\n * - Except a picture's FRAME: a box in the flow whose picture is laid over\n * it (`position: absolute`), as the AI's layout compiler draws every\n * picture slot. The frame is the picture, so it moves as one, picture and\n * all, and counts as a picture for the media preset and for a split.\n * - Never nested: once a block animates, nothing inside it does. Two\n * entrances on the same pixels multiply their fades and add their\n * offsets, which reads as a stutter, not as motion.\n *\n * Pure and idempotent: a page it already moved comes back as the same\n * object, which is what lets a caller compare by identity to skip a write.\n *\n * Leaf imports only, the same as `starter-template-nodes.ts`: the starter\n * kit reads this from the console's client gallery and its seed route alike.\n */\n\n/** A page's motion: the presets, curve and timing every entrance on it shares. */\nexport interface ElementMotionCharacter {\n /** Words and blocks: a section's content, a group of cards. */\n preset: AnimationPreset\n /** A picture, or a group whose every item leads with one. */\n mediaPreset: AnimationPreset\n ease: AnimationEase\n durationMs: number\n /** Gap between the items of a staggered group. */\n staggerStepMs: number\n /** Gap between the rungs of the first section's load ladder. */\n heroStepMs: number\n /**\n * The presets later sections take in turn, the second section the first of\n * them; {@link preset} alone when absent. A section's pictures still take\n * {@link mediaPreset}.\n */\n sectionPresets?: readonly AnimationPreset[]\n /** Whether a split's two columns come in from their own sides. */\n sides?: boolean\n /**\n * What a group of like items — a grid of cards, a list, an FAQ — arrives\n * with, whatever its section's turn; the section's own preset when absent.\n * A group whose every item leads with a picture still takes {@link mediaPreset}.\n */\n groupPreset?: AnimationPreset\n}\n\n/** The motion a page gets when nothing says otherwise: a soft rise, as the attributes panel defaults to. */\nexport const ELEMENT_MOTION_DEFAULT: ElementMotionCharacter = {\n preset: 'slide-up',\n mediaPreset: 'fade',\n ease: 'smooth',\n durationMs: 600,\n staggerStepMs: 90,\n heroStepMs: 120,\n sectionPresets: ['slide-up', 'fade'],\n sides: true,\n}\n\n/** The most rungs the first section's ladder climbs; later blocks share the last. */\nexport const ELEMENT_MOTION_HERO_RUNGS = 6\n\n/**\n * Elements that are never animated, nor animated as part of a block that\n * holds them: forms and every field of one, and the site's navigation and\n * frame. A field that slides in under a visitor's cursor is a field they\n * miss, and a header that moves is a site that looks broken.\n */\nexport const ELEMENT_MOTION_NEVER: ReadonlySet<string> = new Set([\n // Forms and their fields.\n 'form',\n 'formField',\n 'muiFormControl',\n 'muiFormLabel',\n 'muiFormControlLabel',\n 'muiFormHelperText',\n 'muiInput',\n 'muiSelect',\n 'muiSelectOption',\n 'muiRadioGroup',\n 'muiRadio',\n 'muiCheckbox',\n 'muiRating',\n 'muiDatePicker',\n 'muiTimePicker',\n 'newsletter-signup',\n 'member-signin',\n 'member-signup',\n 'member-recovery',\n 'booking',\n 'reservation-widget',\n 'customer-account',\n 'return-request',\n 'order-status',\n 'searchBox',\n 'collectionSearch',\n // Navigation and the site's frame.\n 'layoutSlot',\n 'muiAppBar',\n 'muiToolbar',\n 'muiDrawer',\n 'muiDrawerToggle',\n 'muiNavMenu',\n 'muiMegaMenu',\n 'muiBreadcrumbs',\n 'muiTabs',\n 'languageSwitcher',\n 'themeModeSwitcher',\n 'cart',\n])\n\n/** Pictures: what the media preset is for. */\nconst MEDIA: ReadonlySet<string> = new Set(['image', 'video', 'videoEmbed', 'gated-video'])\n\n/** Components that list like items whatever their children are. */\nconst LISTS: ReadonlySet<string> = new Set(['muiList', 'muiImageList', 'collectionEntries'])\n\n/**\n * What a group's items are made of when they are alike: cards, cells,\n * panels, pictures. Words and buttons never count, however alike — a row of\n * two buttons is a call to action, not a group of cards.\n */\nconst ITEMS: ReadonlySet<string> = new Set([\n 'muiCard',\n 'muiPaper',\n 'muiGrid',\n 'muiStack',\n 'muiBox',\n 'muiLinkBox',\n 'muiListItem',\n 'muiImageListItem',\n 'muiAccordion',\n 'reusableInstance',\n 'div',\n 'image',\n 'video',\n])\n\ninterface MotionNode {\n componentId?: string\n props?: Record<string, unknown>\n sx?: Record<string, unknown>\n nodes?: string[]\n hidden?: boolean\n}\n\nexport interface ElementMotionOptions {\n /** The page's root; the canvas root when absent. */\n rootId?: string\n /** The page's sections in order; the root's own children when absent. */\n sectionIds?: readonly string[]\n character?: ElementMotionCharacter\n}\n\nconst clamp = (value: number, max: number): number => Math.min(Math.max(Math.round(value), 0), max)\n\n/**\n * Whether a style value — plain or one per breakpoint — takes the element out\n * of the flow: pinned to the screen, or laid over its band, as a cover\n * photo's backdrop and its scrim are. Either is part of the band, not of what\n * the band holds.\n */\nfunction pins(value: unknown): boolean {\n if (value === 'fixed' || value === 'sticky' || value === 'absolute') return true\n return typeof value === 'object' && value !== null && Object.values(value).some(pins)\n}\n\n/**\n * Writes entrance motion onto a page's nodes by the rules above, and returns\n * the new node map — or `nodes` itself, unchanged, when there was nothing to\n * move.\n */\nexport function applyElementMotion<T extends Record<string, any>>(nodes: T, options: ElementMotionOptions = {}): T {\n if (!nodes) return nodes\n const character = options.character ?? ELEMENT_MOTION_DEFAULT\n const rootId = options.rootId ?? CANVAS_ROOT_ELEMENT_ID\n const node = (id: string): MotionNode | undefined => nodes[id] as MotionNode | undefined\n const children = (id: string): string[] => (node(id)?.nodes ?? []).filter((child) => typeof child === 'string' && !!node(child))\n const patches = new Map<string, Record<string, unknown>>()\n\n /** The first section's Largest Contentful Paint candidates, painted where they stand. */\n const still = new Set<string>()\n /** Left alone for what it is, wherever it stands. */\n const refused = (id: string): boolean => {\n const at = node(id)\n if (!at) return true\n if (at.hidden || still.has(id)) return true\n if (ELEMENT_MOTION_NEVER.has(String(at.componentId ?? ''))) return true\n return !!at.props && NODE_ANIMATION_PROPS.some((prop) => at.props?.[prop] !== undefined)\n }\n /** Left alone, and so is everything inside it. */\n const closed = (id: string): boolean => refused(id) || pins(node(id)?.sx?.['position'])\n /** A picture laid over its box, free to move with it. */\n const overlay = (id: string): boolean =>\n MEDIA.has(String(node(id)?.componentId ?? '')) && pins(node(id)?.sx?.['position']) && !refused(id)\n /**\n * A picture's frame: a box in the flow whose picture is laid over it\n * (`position: absolute`, filling it), beside at most the placeholder icon\n * an empty slot shows. The frame IS the picture, so it moves as one,\n * picture and all; its picture being out of the flow does not hold it still\n * the way a cover's backdrop holds its band, which is itself out of the flow.\n */\n const isFrame = (id: string): boolean => {\n const kids = children(id)\n return (\n !closed(id) &&\n kids.some(overlay) &&\n kids.every((child) => overlay(child) || (node(child)?.componentId === 'icon' && !refused(child) && !children(child).length))\n )\n }\n const holdsClosed = new Map<string, boolean>()\n /** The element, or one inside it, is left alone: it cannot move as one block. */\n const holds = (id: string): boolean => {\n const known = holdsClosed.get(id)\n if (known !== undefined) return known\n holdsClosed.set(id, false)\n const result = closed(id) || (!isFrame(id) && children(id).some(holds))\n holdsClosed.set(id, result)\n return result\n }\n const groupMemo = new Map<string, boolean>()\n const belowMemo = new Map<string, boolean>()\n /**\n * A group of like items, whose items stagger. A grid or a row of panels\n * that holds a group of its own is the LAYOUT around it — a heading beside\n * its cards — so it is opened up and the group inside staggers instead.\n */\n const isGroup = (id: string): boolean => {\n const known = groupMemo.get(id)\n if (known !== undefined) return known\n const at = node(id) as MotionNode\n const componentId = String(at.componentId ?? '')\n const kids = children(id)\n let result: boolean\n // A dataset repeated over one card renders one child per record.\n if ((at.props?.['repeatDataset'] || LISTS.has(componentId)) && kids.length) result = true\n else if (kids.length < 2) result = false\n else {\n const first = String(node(kids[0])?.componentId ?? '')\n const alike =\n (componentId === 'muiGrid' && !!at.props?.['container']) ||\n (ITEMS.has(first) && kids.every((child) => node(child)?.componentId === first))\n result = alike && !holdsGroup(id)\n }\n groupMemo.set(id, result)\n return result\n }\n /** A group sits somewhere inside the element. */\n const holdsGroup = (id: string): boolean => {\n const known = belowMemo.get(id)\n if (known !== undefined) return known\n const result = children(id).some((child) => isGroup(child) || holdsGroup(child))\n belowMemo.set(id, result)\n return result\n }\n /** Leads with a picture: its first leaf, down its first children, is one, or the frame of one. */\n const leadsWithMedia = (id: string): boolean => {\n let at = id\n for (let depth = 0; depth < 12; depth += 1) {\n if (MEDIA.has(String(node(at)?.componentId ?? '')) || isFrame(at)) return true\n const kids = children(at)\n if (!kids.length) return false\n at = kids[0]\n }\n return false\n }\n const presetOf = (id: string, group: boolean, words: AnimationPreset): AnimationPreset => {\n const media = group ? children(id).every(leadsWithMedia) : leadsWithMedia(id)\n return media ? character.mediaPreset : words\n }\n\n const animate = (\n id: string,\n trigger: AnimationTrigger,\n delayMs: number,\n words: AnimationPreset = character.preset,\n side?: AnimationPreset,\n ): void => {\n const group = isGroup(id)\n const preset = presetOf(id, group, group && character.groupPreset ? character.groupPreset : words)\n patches.set(id, {\n [NODE_ANIMATION_PROP]: side && !preset.startsWith('zoom') ? side : preset,\n [NODE_ANIMATION_TRIGGER_PROP]: trigger,\n [NODE_ANIMATION_DURATION_PROP]: clamp(character.durationMs, ANIMATION_MAX_DURATION_MS),\n ...(delayMs > 0 ? { [NODE_ANIMATION_DELAY_PROP]: clamp(delayMs, ANIMATION_MAX_DELAY_MS) } : {}),\n [NODE_ANIMATION_EASE_PROP]: character.ease,\n ...(group\n ? {\n [NODE_ANIMATION_STAGGER_PROP]: true,\n [NODE_ANIMATION_STAGGER_STEP_PROP]: clamp(character.staggerStepMs, ANIMATION_MAX_STAGGER_STEP_MS),\n }\n : {}),\n })\n }\n\n /** Down through wrappers of one child, to where a section's content starts. */\n const contentOf = (id: string): string => {\n let at = id\n while (!closed(at) && !isGroup(at)) {\n const kids = children(at)\n if (kids.length !== 1) break\n at = kids[0]\n }\n return at\n }\n\n /**\n * A column of words beside a picture: two blocks, one of which leads with a\n * picture and one of which does not, neither left alone. Two cards that\n * both lead with one are a group, and stagger.\n */\n const isSplit = (id: string): boolean => {\n const kids = children(id)\n return kids.length === 2 && leadsWithMedia(kids[0]) !== leadsWithMedia(kids[1]) && !kids.some(holds)\n }\n\n /** A later section's content: on scroll, groups staggered, splits from their sides, nothing nested. */\n const place = (id: string, words: AnimationPreset): void => {\n if (closed(id)) return\n if (isSplit(id)) {\n const [start, end] = children(id)\n animate(start, 'scroll', 0, words, character.sides ? 'slide-right' : undefined)\n animate(end, 'scroll', character.staggerStepMs, words, character.sides ? 'slide-left' : undefined)\n return\n }\n if (holds(id) || (!isGroup(id) && holdsGroup(id))) {\n for (const child of children(id)) place(child, words)\n return\n }\n animate(id, 'scroll', 0, words)\n }\n\n /** Down through wrappers of one child that are free to move. */\n const descend = (id: string): string => {\n let at = id\n for (let kids = children(at); kids.length === 1 && !closed(kids[0]); kids = children(at)) at = kids[0]\n return at\n }\n\n /**\n * The first section's content: on load, one rung per block, around the\n * blocks that stay still. A rung that holds one is opened up, down to the\n * parts beside it, so the eyebrow, the lede and the buttons still climb.\n */\n const ladder = (id: string): void => {\n if (closed(id)) return\n // A repeat's one child is the card every record is drawn with: the\n // repeat staggers its records rather than each record playing at once.\n if (isGroup(id) && children(id).length < 2) return animate(id, 'load', 0)\n const around = (at: string): string[] =>\n children(at).flatMap((child) => (closed(child) ? [] : holds(child) ? around(child) : [child]))\n const base = children(id).length ? around(id) : [id]\n // A column of words beside a picture is opened up, so its lines climb one\n // after another before the picture lands.\n const rungs: string[] = []\n base.forEach((rung, index) => {\n const inner = descend(rung)\n const kids = children(inner)\n const opens =\n kids.length >= 2 &&\n !isGroup(inner) &&\n !leadsWithMedia(inner) &&\n rungs.length + kids.length + (base.length - index - 1) <= ELEMENT_MOTION_HERO_RUNGS\n rungs.push(...(opens ? kids : [rung]))\n })\n // A rung left alone takes no step of the ladder: the first block that moves starts at once.\n rungs\n .filter((rung) => !closed(rung))\n .forEach((rung, index) => animate(rung, 'load', Math.min(index, ELEMENT_MOTION_HERO_RUNGS - 1) * character.heroStepMs))\n }\n\n /**\n * What the first section paints first, and so what Lighthouse times as its\n * Largest Contentful Paint (AGL-3581): its main heading — the h1, else its\n * first heading — and its largest picture. Neither ever starts at opacity 0,\n * which would hold the paint back by the length of the entrance.\n */\n const markStill = (sectionId: string): void => {\n const order: string[] = []\n const walk = (id: string): void => {\n order.push(id)\n for (const child of children(id)) walk(child)\n }\n walk(sectionId)\n const typeOf = (id: string) => {\n const at = node(id)\n return at?.componentId === 'muiTypography' ? String(at.props?.['variant'] ?? '') : null\n }\n const main = (id: string) => /^(h1|display)/i.test(typeOf(id) ?? '') || (typeOf(id) !== null && node(id)?.props?.['component'] === 'h1')\n const heading = order.find(main) ?? order.find((id) => /^(h[1-6]|display)/i.test(typeOf(id) ?? ''))\n if (heading) still.add(heading)\n const size = (id: string): number => {\n const at = node(id)\n const read = (value: unknown) => (typeof value === 'number' ? value : parseFloat(String(value ?? '')) || 0)\n return Math.max(read(at?.props?.['height']), read(at?.props?.['width']), read(at?.sx?.['height']), read(at?.sx?.['width']))\n }\n let picture: string | undefined\n for (const id of order) {\n if (MEDIA.has(String(node(id)?.componentId ?? '')) && (picture === undefined || size(id) > size(picture))) picture = id\n }\n if (picture) still.add(picture)\n }\n\n const sections = (options.sectionIds ?? children(rootId)).filter((id) => !!node(id))\n if (sections[0]) markStill(sections[0])\n const turns = character.sectionPresets?.length ? character.sectionPresets : [character.preset]\n sections.forEach((id, index) => {\n if (closed(id)) return\n const content = contentOf(id)\n // The band itself never moves — only what it holds — so a section that\n // is nothing but its band has nothing to move.\n if (content === id && !children(id).length) return\n const words = turns[(index - 1) % turns.length] ?? character.preset\n if (index === 0) ladder(content)\n else if (content === id && !isGroup(id) && !isSplit(id))\n for (const child of children(id)) place(child, words)\n else place(content, words)\n })\n\n if (!patches.size) return nodes\n const next: Record<string, any> = { ...nodes }\n for (const [id, props] of patches) {\n next[id] = { ...nodes[id], props: { ...(nodes[id]?.props ?? {}), ...props } }\n }\n return next as T\n}\n"],"names":["CANVAS_ROOT_ELEMENT_ID","ANIMATION_MAX_DELAY_MS","ANIMATION_MAX_DURATION_MS","ANIMATION_MAX_STAGGER_STEP_MS","NODE_ANIMATION_DELAY_PROP","NODE_ANIMATION_DURATION_PROP","NODE_ANIMATION_EASE_PROP","NODE_ANIMATION_PROPS","NODE_ANIMATION_PROP","NODE_ANIMATION_STAGGER_PROP","NODE_ANIMATION_STAGGER_STEP_PROP","NODE_ANIMATION_TRIGGER_PROP","ELEMENT_MOTION_DEFAULT","preset","mediaPreset","ease","durationMs","staggerStepMs","heroStepMs","sectionPresets","sides","ELEMENT_MOTION_HERO_RUNGS","ELEMENT_MOTION_NEVER","Set","MEDIA","LISTS","ITEMS","clamp","value","max","Math","min","round","pins","Object","values","some","applyElementMotion","nodes","options","character","rootId","node","id","children","filter","child","patches","Map","still","refused","at","hidden","has","String","componentId","props","prop","undefined","closed","sx","overlay","isFrame","kids","every","length","holdsClosed","holds","known","get","set","result","groupMemo","belowMemo","isGroup","first","alike","holdsGroup","leadsWithMedia","depth","presetOf","group","words","media","animate","trigger","delayMs","side","groupPreset","startsWith","contentOf","isSplit","place","start","end","descend","ladder","around","flatMap","base","rungs","forEach","rung","index","inner","opens","push","markStill","sectionId","order","walk","typeOf","main","test","heading","find","add","size","read","parseFloat","picture","sections","sectionIds","turns","content","next"],"mappings":";AAAA;;;;;;;;;;;;;;;CAeC,GAED,SAASA,sBAAsB,QAAQ,oCAAgC;AACvE,SACEC,sBAAsB,EACtBC,yBAAyB,EACzBC,6BAA6B,EAC7BC,yBAAyB,EACzBC,4BAA4B,EAC5BC,wBAAwB,EACxBC,oBAAoB,EACpBC,mBAAmB,EACnBC,2BAA2B,EAC3BC,gCAAgC,EAChCC,2BAA2B,QAItB,yBAAqB;AAyF5B,0GAA0G,GAC1G,OAAO,MAAMC,yBAAiD;IAC5DC,QAAQ;IACRC,aAAa;IACbC,MAAM;IACNC,YAAY;IACZC,eAAe;IACfC,YAAY;IACZC,gBAAgB;QAAC;QAAY;KAAO;IACpCC,OAAO;AACT,EAAC;AAED,mFAAmF,GACnF,OAAO,MAAMC,4BAA4B,EAAC;AAE1C;;;;;CAKC,GACD,OAAO,MAAMC,uBAA4C,IAAIC,IAAI;IAC/D,0BAA0B;IAC1B;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA,mCAAmC;IACnC;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;CACD,EAAC;AAEF,4CAA4C,GAC5C,MAAMC,QAA6B,IAAID,IAAI;IAAC;IAAS;IAAS;IAAc;CAAc;AAE1F,iEAAiE,GACjE,MAAME,QAA6B,IAAIF,IAAI;IAAC;IAAW;IAAgB;CAAoB;AAE3F;;;;CAIC,GACD,MAAMG,QAA6B,IAAIH,IAAI;IACzC;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;CACD;AAkBD,MAAMI,QAAQ,CAACC,OAAeC,MAAwBC,KAAKC,GAAG,CAACD,KAAKD,GAAG,CAACC,KAAKE,KAAK,CAACJ,QAAQ,IAAIC;AAE/F;;;;;CAKC,GACD,SAASI,KAAKL,KAAc;IAC1B,IAAIA,UAAU,WAAWA,UAAU,YAAYA,UAAU,YAAY,OAAO;IAC5E,OAAO,OAAOA,UAAU,YAAYA,UAAU,QAAQM,OAAOC,MAAM,CAACP,OAAOQ,IAAI,CAACH;AAClF;AAEA;;;;CAIC,GACD,OAAO,SAASI,mBAAkDC,KAAQ,EAAEC,UAAgC,CAAC,CAAC;QAE1FA,oBACHA,iBAoOGA;QAEJC;IAxOd,IAAI,CAACF,OAAO,OAAOA;IACnB,MAAME,aAAYD,qBAAAA,QAAQC,SAAS,YAAjBD,qBAAqB3B;IACvC,MAAM6B,UAASF,kBAAAA,QAAQE,MAAM,YAAdF,kBAAkBvC;IACjC,MAAM0C,OAAO,CAACC,KAAuCL,KAAK,CAACK,GAAG;IAC9D,MAAMC,WAAW,CAACD;;YAA0BD;eAAD,UAACA,QAAAA,KAAKC,wBAALD,MAAUJ,KAAK,mBAAI,EAAE,EAAEO,MAAM,CAAC,CAACC,QAAU,OAAOA,UAAU,YAAY,CAAC,CAACJ,KAAKI;;IACzH,MAAMC,UAAU,IAAIC;IAEpB,uFAAuF,GACvF,MAAMC,QAAQ,IAAI1B;IAClB,mDAAmD,GACnD,MAAM2B,UAAU,CAACP;YAIqBQ;QAHpC,MAAMA,KAAKT,KAAKC;QAChB,IAAI,CAACQ,IAAI,OAAO;QAChB,IAAIA,GAAGC,MAAM,IAAIH,MAAMI,GAAG,CAACV,KAAK,OAAO;QACvC,IAAIrB,qBAAqB+B,GAAG,CAACC,QAAOH,kBAAAA,GAAGI,WAAW,YAAdJ,kBAAkB,MAAM,OAAO;QACnE,OAAO,CAAC,CAACA,GAAGK,KAAK,IAAIjD,qBAAqB6B,IAAI,CAAC,CAACqB;gBAASN;mBAAAA,EAAAA,YAAAA,GAAGK,KAAK,qBAARL,SAAU,CAACM,KAAK,MAAKC;;IAChF;IACA,gDAAgD,GAChD,MAAMC,SAAS,CAAChB;YAA4CD,UAAAA;eAApBQ,QAAQP,OAAOV,MAAKS,QAAAA,KAAKC,yBAALD,WAAAA,MAAUkB,EAAE,qBAAZlB,QAAc,CAAC,WAAW;;IACtF,uDAAuD,GACvD,MAAMmB,UAAU,CAAClB;;YACED,OAAsCA,UAAAA;eAAvDlB,MAAM6B,GAAG,CAACC,gBAAOZ,QAAAA,KAAKC,wBAALD,MAAUa,WAAW,mBAAI,QAAQtB,MAAKS,SAAAA,KAAKC,yBAALD,WAAAA,OAAUkB,EAAE,qBAAZlB,QAAc,CAAC,WAAW,KAAK,CAACQ,QAAQP;;IACjG;;;;;;GAMC,GACD,MAAMmB,UAAU,CAACnB;QACf,MAAMoB,OAAOnB,SAASD;QACtB,OACE,CAACgB,OAAOhB,OACRoB,KAAK3B,IAAI,CAACyB,YACVE,KAAKC,KAAK,CAAC,CAAClB;gBAA6BJ;mBAAnBmB,QAAQf,UAAWJ,EAAAA,QAAAA,KAAKI,2BAALJ,MAAaa,WAAW,MAAK,UAAU,CAACL,QAAQJ,UAAU,CAACF,SAASE,OAAOmB,MAAM;;IAE9H;IACA,MAAMC,cAAc,IAAIlB;IACxB,+EAA+E,GAC/E,MAAMmB,QAAQ,CAACxB;QACb,MAAMyB,QAAQF,YAAYG,GAAG,CAAC1B;QAC9B,IAAIyB,UAAUV,WAAW,OAAOU;QAChCF,YAAYI,GAAG,CAAC3B,IAAI;QACpB,MAAM4B,SAASZ,OAAOhB,OAAQ,CAACmB,QAAQnB,OAAOC,SAASD,IAAIP,IAAI,CAAC+B;QAChED,YAAYI,GAAG,CAAC3B,IAAI4B;QACpB,OAAOA;IACT;IACA,MAAMC,YAAY,IAAIxB;IACtB,MAAMyB,YAAY,IAAIzB;IACtB;;;;GAIC,GACD,MAAM0B,UAAU,CAAC/B;YAIYQ;YAItBA;QAPL,MAAMiB,QAAQI,UAAUH,GAAG,CAAC1B;QAC5B,IAAIyB,UAAUV,WAAW,OAAOU;QAChC,MAAMjB,KAAKT,KAAKC;QAChB,MAAMY,cAAcD,QAAOH,kBAAAA,GAAGI,WAAW,YAAdJ,kBAAkB;QAC7C,MAAMY,OAAOnB,SAASD;QACtB,IAAI4B;QACJ,iEAAiE;QACjE,IAAI,AAACpB,CAAAA,EAAAA,YAAAA,GAAGK,KAAK,qBAARL,SAAU,CAAC,gBAAgB,KAAI1B,MAAM4B,GAAG,CAACE,YAAW,KAAMQ,KAAKE,MAAM,EAAEM,SAAS;aAChF,IAAIR,KAAKE,MAAM,GAAG,GAAGM,SAAS;aAC9B;;gBACkB7B,OAEaS;YAFlC,MAAMwB,QAAQrB,gBAAOZ,QAAAA,KAAKqB,IAAI,CAAC,EAAE,sBAAZrB,MAAea,WAAW,mBAAI;YACnD,MAAMqB,QACJ,AAACrB,gBAAgB,aAAa,CAAC,GAACJ,aAAAA,GAAGK,KAAK,qBAARL,UAAU,CAAC,YAAY,KACtDzB,MAAM2B,GAAG,CAACsB,UAAUZ,KAAKC,KAAK,CAAC,CAAClB;oBAAUJ;uBAAAA,EAAAA,QAAAA,KAAKI,2BAALJ,MAAaa,WAAW,MAAKoB;;YAC1EJ,SAASK,SAAS,CAACC,WAAWlC;QAChC;QACA6B,UAAUF,GAAG,CAAC3B,IAAI4B;QAClB,OAAOA;IACT;IACA,+CAA+C,GAC/C,MAAMM,aAAa,CAAClC;QAClB,MAAMyB,QAAQK,UAAUJ,GAAG,CAAC1B;QAC5B,IAAIyB,UAAUV,WAAW,OAAOU;QAChC,MAAMG,SAAS3B,SAASD,IAAIP,IAAI,CAAC,CAACU,QAAU4B,QAAQ5B,UAAU+B,WAAW/B;QACzE2B,UAAUH,GAAG,CAAC3B,IAAI4B;QAClB,OAAOA;IACT;IACA,gGAAgG,GAChG,MAAMO,iBAAiB,CAACnC;QACtB,IAAIQ,KAAKR;QACT,IAAK,IAAIoC,QAAQ,GAAGA,QAAQ,IAAIA,SAAS,EAAG;;gBACrBrC;YAArB,IAAIlB,MAAM6B,GAAG,CAACC,gBAAOZ,QAAAA,KAAKS,wBAALT,MAAUa,WAAW,mBAAI,QAAQO,QAAQX,KAAK,OAAO;YAC1E,MAAMY,OAAOnB,SAASO;YACtB,IAAI,CAACY,KAAKE,MAAM,EAAE,OAAO;YACzBd,KAAKY,IAAI,CAAC,EAAE;QACd;QACA,OAAO;IACT;IACA,MAAMiB,WAAW,CAACrC,IAAYsC,OAAgBC;QAC5C,MAAMC,QAAQF,QAAQrC,SAASD,IAAIqB,KAAK,CAACc,kBAAkBA,eAAenC;QAC1E,OAAOwC,QAAQ3C,UAAU1B,WAAW,GAAGoE;IACzC;IAEA,MAAME,UAAU,CACdzC,IACA0C,SACAC,SACAJ,QAAyB1C,UAAU3B,MAAM,EACzC0E;QAEA,MAAMN,QAAQP,QAAQ/B;QACtB,MAAM9B,SAASmE,SAASrC,IAAIsC,OAAOA,SAASzC,UAAUgD,WAAW,GAAGhD,UAAUgD,WAAW,GAAGN;QAC5FnC,QAAQuB,GAAG,CAAC3B,IAAI;YACd,CAACnC,oBAAoB,EAAE+E,QAAQ,CAAC1E,OAAO4E,UAAU,CAAC,UAAUF,OAAO1E;YACnE,CAACF,4BAA4B,EAAE0E;YAC/B,CAAChF,6BAA6B,EAAEsB,MAAMa,UAAUxB,UAAU,EAAEd;WACxDoF,UAAU,IAAI;YAAE,CAAClF,0BAA0B,EAAEuB,MAAM2D,SAASrF;QAAwB,IAAI,CAAC;YAC7F,CAACK,yBAAyB,EAAEkC,UAAUzB,IAAI;WACtCkE,QACA;YACE,CAACxE,4BAA4B,EAAE;YAC/B,CAACC,iCAAiC,EAAEiB,MAAMa,UAAUvB,aAAa,EAAEd;QACrE,IACA,CAAC;IAET;IAEA,6EAA6E,GAC7E,MAAMuF,YAAY,CAAC/C;QACjB,IAAIQ,KAAKR;QACT,MAAO,CAACgB,OAAOR,OAAO,CAACuB,QAAQvB,IAAK;YAClC,MAAMY,OAAOnB,SAASO;YACtB,IAAIY,KAAKE,MAAM,KAAK,GAAG;YACvBd,KAAKY,IAAI,CAAC,EAAE;QACd;QACA,OAAOZ;IACT;IAEA;;;;GAIC,GACD,MAAMwC,UAAU,CAAChD;QACf,MAAMoB,OAAOnB,SAASD;QACtB,OAAOoB,KAAKE,MAAM,KAAK,KAAKa,eAAef,IAAI,CAAC,EAAE,MAAMe,eAAef,IAAI,CAAC,EAAE,KAAK,CAACA,KAAK3B,IAAI,CAAC+B;IAChG;IAEA,qGAAqG,GACrG,MAAMyB,QAAQ,CAACjD,IAAYuC;QACzB,IAAIvB,OAAOhB,KAAK;QAChB,IAAIgD,QAAQhD,KAAK;YACf,MAAM,CAACkD,OAAOC,IAAI,GAAGlD,SAASD;YAC9ByC,QAAQS,OAAO,UAAU,GAAGX,OAAO1C,UAAUpB,KAAK,GAAG,gBAAgBsC;YACrE0B,QAAQU,KAAK,UAAUtD,UAAUvB,aAAa,EAAEiE,OAAO1C,UAAUpB,KAAK,GAAG,eAAesC;YACxF;QACF;QACA,IAAIS,MAAMxB,OAAQ,CAAC+B,QAAQ/B,OAAOkC,WAAWlC,KAAM;YACjD,KAAK,MAAMG,SAASF,SAASD,IAAKiD,MAAM9C,OAAOoC;YAC/C;QACF;QACAE,QAAQzC,IAAI,UAAU,GAAGuC;IAC3B;IAEA,8DAA8D,GAC9D,MAAMa,UAAU,CAACpD;QACf,IAAIQ,KAAKR;QACT,IAAK,IAAIoB,OAAOnB,SAASO,KAAKY,KAAKE,MAAM,KAAK,KAAK,CAACN,OAAOI,IAAI,CAAC,EAAE,GAAGA,OAAOnB,SAASO,IAAKA,KAAKY,IAAI,CAAC,EAAE;QACtG,OAAOZ;IACT;IAEA;;;;GAIC,GACD,MAAM6C,SAAS,CAACrD;QACd,IAAIgB,OAAOhB,KAAK;QAChB,mEAAmE;QACnE,uEAAuE;QACvE,IAAI+B,QAAQ/B,OAAOC,SAASD,IAAIsB,MAAM,GAAG,GAAG,OAAOmB,QAAQzC,IAAI,QAAQ;QACvE,MAAMsD,SAAS,CAAC9C,KACdP,SAASO,IAAI+C,OAAO,CAAC,CAACpD,QAAWa,OAAOb,SAAS,EAAE,GAAGqB,MAAMrB,SAASmD,OAAOnD,SAAS;oBAACA;iBAAM;QAC9F,MAAMqD,OAAOvD,SAASD,IAAIsB,MAAM,GAAGgC,OAAOtD,MAAM;YAACA;SAAG;QACpD,0EAA0E;QAC1E,0CAA0C;QAC1C,MAAMyD,QAAkB,EAAE;QAC1BD,KAAKE,OAAO,CAAC,CAACC,MAAMC;YAClB,MAAMC,QAAQT,QAAQO;YACtB,MAAMvC,OAAOnB,SAAS4D;YACtB,MAAMC,QACJ1C,KAAKE,MAAM,IAAI,KACf,CAACS,QAAQ8B,UACT,CAAC1B,eAAe0B,UAChBJ,MAAMnC,MAAM,GAAGF,KAAKE,MAAM,GAAIkC,CAAAA,KAAKlC,MAAM,GAAGsC,QAAQ,CAAA,KAAMlF;YAC5D+E,MAAMM,IAAI,IAAKD,QAAQ1C,OAAO;gBAACuC;aAAK;QACtC;QACA,4FAA4F;QAC5FF,MACGvD,MAAM,CAAC,CAACyD,OAAS,CAAC3C,OAAO2C,OACzBD,OAAO,CAAC,CAACC,MAAMC,QAAUnB,QAAQkB,MAAM,QAAQxE,KAAKC,GAAG,CAACwE,OAAOlF,4BAA4B,KAAKmB,UAAUtB,UAAU;IACzH;IAEA;;;;;GAKC,GACD,MAAMyF,YAAY,CAACC;YAYDC;QAXhB,MAAMA,QAAkB,EAAE;QAC1B,MAAMC,OAAO,CAACnE;YACZkE,MAAMH,IAAI,CAAC/D;YACX,KAAK,MAAMG,SAASF,SAASD,IAAKmE,KAAKhE;QACzC;QACAgE,KAAKF;QACL,MAAMG,SAAS,CAACpE;;gBAEsCQ;YADpD,MAAMA,KAAKT,KAAKC;YAChB,OAAOQ,CAAAA,sBAAAA,GAAII,WAAW,MAAK,kBAAkBD,gBAAOH,YAAAA,GAAGK,KAAK,qBAARL,SAAU,CAAC,UAAU,mBAAI,MAAM;QACrF;QACA,MAAM6D,OAAO,CAACrE;gBAAqCoE;gBAA6CrE,aAAAA;mBAAnE,iBAAiBuE,IAAI,EAACF,UAAAA,OAAOpE,eAAPoE,UAAc,OAAQA,OAAOpE,QAAQ,QAAQD,EAAAA,QAAAA,KAAKC,yBAALD,cAAAA,MAAUc,KAAK,qBAAfd,WAAiB,CAAC,YAAY,MAAK;;QACnI,MAAMwE,WAAUL,cAAAA,MAAMM,IAAI,CAACH,iBAAXH,cAAoBA,MAAMM,IAAI,CAAC,CAACxE;gBAAiCoE;mBAA1B,qBAAqBE,IAAI,EAACF,UAAAA,OAAOpE,eAAPoE,UAAc;;QAC/F,IAAIG,SAASjE,MAAMmE,GAAG,CAACF;QACvB,MAAMG,OAAO,CAAC1E;gBAGSQ,WAA6BA,YAA4BA,QAA0BA;YAFxG,MAAMA,KAAKT,KAAKC;YAChB,MAAM2E,OAAO,CAAC1F,QAAoB,OAAOA,UAAU,WAAWA,QAAQ2F,WAAWjE,OAAO1B,gBAAAA,QAAS,QAAQ;YACzG,OAAOE,KAAKD,GAAG,CAACyF,KAAKnE,uBAAAA,YAAAA,GAAIK,KAAK,qBAATL,SAAW,CAAC,SAAS,GAAGmE,KAAKnE,uBAAAA,aAAAA,GAAIK,KAAK,qBAATL,UAAW,CAAC,QAAQ,GAAGmE,KAAKnE,uBAAAA,SAAAA,GAAIS,EAAE,qBAANT,MAAQ,CAAC,SAAS,GAAGmE,KAAKnE,uBAAAA,UAAAA,GAAIS,EAAE,qBAANT,OAAQ,CAAC,QAAQ;QAC3H;QACA,IAAIqE;QACJ,KAAK,MAAM7E,MAAMkE,MAAO;;gBACDnE;YAArB,IAAIlB,MAAM6B,GAAG,CAACC,gBAAOZ,QAAAA,KAAKC,wBAALD,MAAUa,WAAW,mBAAI,QAASiE,CAAAA,YAAY9D,aAAa2D,KAAK1E,MAAM0E,KAAKG,QAAO,GAAIA,UAAU7E;QACvH;QACA,IAAI6E,SAASvE,MAAMmE,GAAG,CAACI;IACzB;IAEA,MAAMC,WAAW,EAAClF,sBAAAA,QAAQmF,UAAU,YAAlBnF,sBAAsBK,SAASH,SAASI,MAAM,CAAC,CAACF,KAAO,CAAC,CAACD,KAAKC;IAChF,IAAI8E,QAAQ,CAAC,EAAE,EAAEd,UAAUc,QAAQ,CAAC,EAAE;IACtC,MAAME,QAAQnF,EAAAA,4BAAAA,UAAUrB,cAAc,qBAAxBqB,0BAA0ByB,MAAM,IAAGzB,UAAUrB,cAAc,GAAG;QAACqB,UAAU3B,MAAM;KAAC;IAC9F4G,SAASpB,OAAO,CAAC,CAAC1D,IAAI4D;YAMNoB;QALd,IAAIhE,OAAOhB,KAAK;QAChB,MAAMiF,UAAUlC,UAAU/C;QAC1B,uEAAuE;QACvE,+CAA+C;QAC/C,IAAIiF,YAAYjF,MAAM,CAACC,SAASD,IAAIsB,MAAM,EAAE;QAC5C,MAAMiB,SAAQyC,UAAAA,KAAK,CAAC,AAACpB,CAAAA,QAAQ,CAAA,IAAKoB,MAAM1D,MAAM,CAAC,YAAjC0D,UAAqCnF,UAAU3B,MAAM;QACnE,IAAI0F,UAAU,GAAGP,OAAO4B;aACnB,IAAIA,YAAYjF,MAAM,CAAC+B,QAAQ/B,OAAO,CAACgD,QAAQhD,KAClD,KAAK,MAAMG,SAASF,SAASD,IAAKiD,MAAM9C,OAAOoC;aAC5CU,MAAMgC,SAAS1C;IACtB;IAEA,IAAI,CAACnC,QAAQsE,IAAI,EAAE,OAAO/E;IAC1B,MAAMuF,OAA4B,aAAKvF;IACvC,KAAK,MAAM,CAACK,IAAIa,MAAM,IAAIT,QAAS;;YACOT;QAAxCuF,IAAI,CAAClF,GAAG,GAAG,aAAKL,KAAK,CAACK,GAAG;YAAEa,OAAO,sBAAMlB,YAAAA,KAAK,CAACK,GAAG,qBAATL,UAAWkB,KAAK,mBAAI,CAAC,GAAOA;;IACtE;IACA,OAAOqE;AACT"}
|
|
@@ -531,20 +531,51 @@ export declare function signupDroughtHealth(markers: SignupServedMarker[] | null
|
|
|
531
531
|
* 10/17's). The streak of two was red only on 10/11, AGL-3724, and nowhere
|
|
532
532
|
* else. A streak of three missed AGL-3724 entirely.
|
|
533
533
|
*
|
|
534
|
+
* ## A site is not the question; reaching the console is (2026-10-18)
|
|
535
|
+
*
|
|
536
|
+
* The streak rule went red on 2026-10-18 at 15:37Z for the same reason the
|
|
537
|
+
* count did: the two newest people had no site, and both had LEFT. One left
|
|
538
|
+
* nine seconds after the workspace appeared. The other reached the site list,
|
|
539
|
+
* walked four steps of the guided start, and stopped. Sign-ups on either side
|
|
540
|
+
* of them made sites on the same build. "No site" is an outcome that leaving
|
|
541
|
+
* produces as surely as a broken landing does, so no rule over sites alone
|
|
542
|
+
* can tell the two apart.
|
|
543
|
+
*
|
|
544
|
+
* What AGL-3724 broke was the landing itself: the person never REACHED the
|
|
545
|
+
* console after the workspace was made. That is recorded now. Every org is
|
|
546
|
+
* born with `landedAtMs: null` ({@link ORG_LANDED_AT_PATH}), and the org
|
|
547
|
+
* shell's activity beat, which fires the moment the workspace opens, sets it
|
|
548
|
+
* the first time (`stampOrgLastActivity`). So an org counts as stranded only
|
|
549
|
+
* when it has no site AND its owner never reached the console. A person who
|
|
550
|
+
* arrived and then left is a drop-off, which is not this check's business.
|
|
551
|
+
*
|
|
552
|
+
* An org without the field predates the marker, and the check has no
|
|
553
|
+
* evidence about it either way, so it ABSTAINS on it (`unmarkedOrgs`). It is
|
|
554
|
+
* never counted as stranded, the way a check that cannot see its subject
|
|
555
|
+
* says so instead of guessing.
|
|
556
|
+
*
|
|
534
557
|
* Pure, like its siblings: the route reads, this decides. The body carries
|
|
535
558
|
* counts only, never an org id, slug or visitor key, because the endpoint is
|
|
536
559
|
* public.
|
|
537
560
|
*/
|
|
538
561
|
export interface SignupLandingCheck extends HealthCheck {
|
|
539
|
-
/**
|
|
562
|
+
/**
|
|
563
|
+
* Orgs in the window with no site whose owner never reached the console.
|
|
564
|
+
* Null when the read failed.
|
|
565
|
+
*/
|
|
540
566
|
strandedOrgs: number | null;
|
|
541
|
-
/** People in the window
|
|
567
|
+
/** People in the window none of whose orgs has a site or a landing. */
|
|
542
568
|
strandedPeople: number | null;
|
|
543
569
|
/**
|
|
544
|
-
* How many of the newest people in the window, in a row,
|
|
570
|
+
* How many of the newest people in the window, in a row, are stranded:
|
|
545
571
|
* what the verdict reads. Null when the read failed.
|
|
546
572
|
*/
|
|
547
573
|
strandedStreak: number | null;
|
|
574
|
+
/**
|
|
575
|
+
* Orgs with no site that predate the landing marker, which the check
|
|
576
|
+
* abstains on. Null when the read failed.
|
|
577
|
+
*/
|
|
578
|
+
unmarkedOrgs: number | null;
|
|
548
579
|
/** Every org in the window, the context for the counts above. */
|
|
549
580
|
orgsInWindow: number | null;
|
|
550
581
|
/** How far back the window reaches. */
|
|
@@ -569,12 +600,21 @@ export declare const SIGNUP_LANDING_GRACE_MINUTES = 30;
|
|
|
569
600
|
*/
|
|
570
601
|
export declare const MAX_STRANDED_SIGNUPS = 2;
|
|
571
602
|
/**
|
|
572
|
-
*
|
|
573
|
-
*
|
|
574
|
-
*
|
|
603
|
+
* Where an org records that its owner reached the console (see
|
|
604
|
+
* {@link SignupLandingCheck}). `null` from birth, epoch millis once the
|
|
605
|
+
* workspace has been opened, absent on an org made before it existed.
|
|
606
|
+
*/
|
|
607
|
+
export declare const ORG_LANDED_AT_PATH = "landedAtMs";
|
|
608
|
+
/**
|
|
609
|
+
* One org as the landing check reads it: whether it has any site, whether
|
|
610
|
+
* its owner reached the console, when it was made, and who made it.
|
|
611
|
+
* `landed` is `undefined` for an org that predates the marker. `visitor` is
|
|
612
|
+
* an opaque key that never leaves the check; an org without one is its own
|
|
613
|
+
* person.
|
|
575
614
|
*/
|
|
576
615
|
export interface SignupLandingOrg {
|
|
577
616
|
hostCount: number;
|
|
617
|
+
landed?: boolean;
|
|
578
618
|
createdAtMs?: number | null;
|
|
579
619
|
visitor?: string | null;
|
|
580
620
|
}
|
|
@@ -1497,6 +1537,10 @@ export interface CronBeatStore {
|
|
|
1497
1537
|
collection(name: string): {
|
|
1498
1538
|
doc(id: string): {
|
|
1499
1539
|
set(data: Record<string, unknown>, options?: unknown): Promise<unknown>;
|
|
1540
|
+
/** Present on a real Firestore handle; {@link writeCronBeats} reads the summary through it. */
|
|
1541
|
+
get?(): Promise<{
|
|
1542
|
+
get(field: string): unknown;
|
|
1543
|
+
}>;
|
|
1500
1544
|
};
|
|
1501
1545
|
};
|
|
1502
1546
|
}
|
|
@@ -1514,6 +1558,34 @@ export declare const CRON_BEAT_EVERY_MINUTE_MIN_INTERVAL_MS: number;
|
|
|
1514
1558
|
/** Test seam. */
|
|
1515
1559
|
export declare function resetCronBeatThrottleForTests(): void;
|
|
1516
1560
|
export declare function writeCronBeat(store: CronBeatStore, jobId: string, now?: number): Promise<boolean>;
|
|
1561
|
+
/**
|
|
1562
|
+
* How long a job's own document may lag its summary entry when the jobs are
|
|
1563
|
+
* marked together (AGL-3932).
|
|
1564
|
+
*
|
|
1565
|
+
* The reader judges from the summary and opens a job's own document only to
|
|
1566
|
+
* confirm an entry the summary calls late, so a job marked every fifteen
|
|
1567
|
+
* minutes does not need its own document rewritten every fifteen minutes:
|
|
1568
|
+
* half an hour, under every plugin job's forty-five-minute grace, keeps the
|
|
1569
|
+
* confirming mark fresh enough to agree with a summary that has stopped.
|
|
1570
|
+
*/
|
|
1571
|
+
export declare const CRON_BEAT_OWN_MARK_REFRESH_MS: number;
|
|
1572
|
+
/**
|
|
1573
|
+
* The marks one run leaves for several jobs at once — the plugin-crons route
|
|
1574
|
+
* stamps twenty jobs a run, and {@link writeCronBeat} per job was forty
|
|
1575
|
+
* writes every fifteen minutes, half of everything the platform wrote
|
|
1576
|
+
* (AGL-3932).
|
|
1577
|
+
*
|
|
1578
|
+
* One summary write carries every job's entry (merged, like the single mark's).
|
|
1579
|
+
* A job's own document is rewritten only when the summary's current entry
|
|
1580
|
+
* for it is missing or older than {@link CRON_BEAT_OWN_MARK_REFRESH_MS},
|
|
1581
|
+
* which one read of the summary tells; a store without `get`, or a summary
|
|
1582
|
+
* that cannot be read or written, falls back to the single-mark shape for
|
|
1583
|
+
* every job, so no run can leave fewer marks than before.
|
|
1584
|
+
*
|
|
1585
|
+
* NEVER THROWS, as {@link writeCronBeat} never throws, and for the same
|
|
1586
|
+
* reason. Returns false only when nothing landed for some job.
|
|
1587
|
+
*/
|
|
1588
|
+
export declare function writeCronBeats(store: CronBeatStore, jobIds: readonly string[], now?: number): Promise<boolean>;
|
|
1517
1589
|
/**
|
|
1518
1590
|
* The beats a summary document holds, ignoring anything malformed.
|
|
1519
1591
|
*
|
|
@@ -548,6 +548,11 @@ export function signupDroughtHealth(markers, attemptMarkers, orgCreations, ms, m
|
|
|
548
548
|
* Red when the two newest people in the window are both stranded. See
|
|
549
549
|
* {@link SignupLandingCheck}.
|
|
550
550
|
*/ export const MAX_STRANDED_SIGNUPS = 2;
|
|
551
|
+
/**
|
|
552
|
+
* Where an org records that its owner reached the console (see
|
|
553
|
+
* {@link SignupLandingCheck}). `null` from birth, epoch millis once the
|
|
554
|
+
* workspace has been opened, absent on an org made before it existed.
|
|
555
|
+
*/ export const ORG_LANDED_AT_PATH = 'landedAtMs';
|
|
551
556
|
export function signupLandingHealth(orgs, ms, maxStranded = MAX_STRANDED_SIGNUPS, windowHours = SIGNUP_LANDING_WINDOW_HOURS, graceMinutes = SIGNUP_LANDING_GRACE_MINUTES) {
|
|
552
557
|
const shape = {
|
|
553
558
|
windowHours,
|
|
@@ -564,31 +569,41 @@ export function signupLandingHealth(orgs, ms, maxStranded = MAX_STRANDED_SIGNUPS
|
|
|
564
569
|
strandedOrgs: null,
|
|
565
570
|
strandedPeople: null,
|
|
566
571
|
strandedStreak: null,
|
|
572
|
+
unmarkedOrgs: null,
|
|
567
573
|
orgsInWindow: null
|
|
568
574
|
}, shape);
|
|
569
575
|
}
|
|
570
576
|
// `!(n > 0)` rather than `n === 0`, so a corrupt count (NaN, a string)
|
|
571
577
|
// reads as "no site" and cannot hide a stranded org.
|
|
572
578
|
const hasSite = (landing)=>Number(landing.hostCount) > 0;
|
|
573
|
-
|
|
579
|
+
// Each org is one of: arrived (a site, or its owner reached the console),
|
|
580
|
+
// stranded (neither, and the marker says so), or unknown (neither, and it
|
|
581
|
+
// predates the marker).
|
|
582
|
+
const arrived = (landing)=>hasSite(landing) || landing.landed === true;
|
|
583
|
+
const isStranded = (landing)=>!arrived(landing) && landing.landed === false;
|
|
584
|
+
const strandedOrgs = orgs.filter(isStranded).length;
|
|
585
|
+
const unmarkedOrgs = orgs.filter((landing)=>!arrived(landing) && landing.landed === undefined).length;
|
|
574
586
|
const people = new Map();
|
|
575
587
|
orgs.forEach((landing, index)=>{
|
|
576
|
-
var _ref, _ref1;
|
|
588
|
+
var _ref, _ref1, _ref2;
|
|
577
589
|
const key = landing.visitor ? `visitor:${landing.visitor}` : `org:${index}`;
|
|
578
590
|
const createdAtMs = Number(landing.createdAtMs);
|
|
579
591
|
const latestMs = Number.isFinite(createdAtMs) ? createdAtMs : 0;
|
|
580
592
|
const person = people.get(key);
|
|
581
593
|
people.set(key, {
|
|
582
594
|
latestMs: Math.max((_ref = person == null ? void 0 : person.latestMs) != null ? _ref : 0, latestMs),
|
|
583
|
-
|
|
595
|
+
arrived: ((_ref1 = person == null ? void 0 : person.arrived) != null ? _ref1 : false) || arrived(landing),
|
|
596
|
+
stranded: ((_ref2 = person == null ? void 0 : person.stranded) != null ? _ref2 : false) || isStranded(landing)
|
|
584
597
|
});
|
|
585
598
|
});
|
|
599
|
+
// A person with nothing but unmarked orgs is not graded at all: neither
|
|
600
|
+
// arrived nor stranded, so they neither start nor break a streak.
|
|
586
601
|
const newestFirst = [
|
|
587
602
|
...people.values()
|
|
588
|
-
].sort((a, b)=>b.latestMs - a.latestMs);
|
|
589
|
-
const strandedPeople = newestFirst.filter((person)=>!person.
|
|
603
|
+
].filter((person)=>person.arrived || person.stranded).sort((a, b)=>b.latestMs - a.latestMs);
|
|
604
|
+
const strandedPeople = newestFirst.filter((person)=>!person.arrived).length;
|
|
590
605
|
let strandedStreak = 0;
|
|
591
|
-
while(strandedStreak < newestFirst.length && !newestFirst[strandedStreak].
|
|
606
|
+
while(strandedStreak < newestFirst.length && !newestFirst[strandedStreak].arrived){
|
|
592
607
|
strandedStreak += 1;
|
|
593
608
|
}
|
|
594
609
|
const stranded = strandedStreak >= maxStranded;
|
|
@@ -601,6 +616,7 @@ export function signupLandingHealth(orgs, ms, maxStranded = MAX_STRANDED_SIGNUPS
|
|
|
601
616
|
strandedOrgs,
|
|
602
617
|
strandedPeople,
|
|
603
618
|
strandedStreak,
|
|
619
|
+
unmarkedOrgs,
|
|
604
620
|
orgsInWindow: orgs.length
|
|
605
621
|
}, shape);
|
|
606
622
|
}
|
|
@@ -1947,6 +1963,97 @@ export async function writeCronBeat(store, jobId, now = Date.now()) {
|
|
|
1947
1963
|
return false;
|
|
1948
1964
|
}
|
|
1949
1965
|
}
|
|
1966
|
+
/**
|
|
1967
|
+
* How long a job's own document may lag its summary entry when the jobs are
|
|
1968
|
+
* marked together (AGL-3932).
|
|
1969
|
+
*
|
|
1970
|
+
* The reader judges from the summary and opens a job's own document only to
|
|
1971
|
+
* confirm an entry the summary calls late, so a job marked every fifteen
|
|
1972
|
+
* minutes does not need its own document rewritten every fifteen minutes:
|
|
1973
|
+
* half an hour, under every plugin job's forty-five-minute grace, keeps the
|
|
1974
|
+
* confirming mark fresh enough to agree with a summary that has stopped.
|
|
1975
|
+
*/ export const CRON_BEAT_OWN_MARK_REFRESH_MS = 30 * 60000;
|
|
1976
|
+
/**
|
|
1977
|
+
* The marks one run leaves for several jobs at once — the plugin-crons route
|
|
1978
|
+
* stamps twenty jobs a run, and {@link writeCronBeat} per job was forty
|
|
1979
|
+
* writes every fifteen minutes, half of everything the platform wrote
|
|
1980
|
+
* (AGL-3932).
|
|
1981
|
+
*
|
|
1982
|
+
* One summary write carries every job's entry (merged, like the single mark's).
|
|
1983
|
+
* A job's own document is rewritten only when the summary's current entry
|
|
1984
|
+
* for it is missing or older than {@link CRON_BEAT_OWN_MARK_REFRESH_MS},
|
|
1985
|
+
* which one read of the summary tells; a store without `get`, or a summary
|
|
1986
|
+
* that cannot be read or written, falls back to the single-mark shape for
|
|
1987
|
+
* every job, so no run can leave fewer marks than before.
|
|
1988
|
+
*
|
|
1989
|
+
* NEVER THROWS, as {@link writeCronBeat} never throws, and for the same
|
|
1990
|
+
* reason. Returns false only when nothing landed for some job.
|
|
1991
|
+
*/ export async function writeCronBeats(store, jobIds, now = Date.now()) {
|
|
1992
|
+
const ids = [
|
|
1993
|
+
...new Set(jobIds)
|
|
1994
|
+
].filter((id)=>!CRON_BEAT_RESERVED_DOCS.has(id));
|
|
1995
|
+
if (!ids.length) return true;
|
|
1996
|
+
const beats = store.collection(CRON_BEAT_COLLECTION);
|
|
1997
|
+
const summary = beats.doc(CRON_BEAT_SUMMARY_DOC);
|
|
1998
|
+
let current = null;
|
|
1999
|
+
try {
|
|
2000
|
+
if (summary.get) {
|
|
2001
|
+
const snapshot = await summary.get();
|
|
2002
|
+
current = new Map(cronBeatsFromSummary(snapshot.get('beats')).map((beat)=>[
|
|
2003
|
+
beat.jobId,
|
|
2004
|
+
beat.atMs
|
|
2005
|
+
]));
|
|
2006
|
+
}
|
|
2007
|
+
} catch (unused) {
|
|
2008
|
+
// Unreadable: every job gets its own mark below, as before.
|
|
2009
|
+
}
|
|
2010
|
+
const refresh = current === null ? ids : ids.filter((id)=>{
|
|
2011
|
+
const last = current.get(id);
|
|
2012
|
+
return last === undefined || now - last >= CRON_BEAT_OWN_MARK_REFRESH_MS || now < last;
|
|
2013
|
+
});
|
|
2014
|
+
let landed = true;
|
|
2015
|
+
for (const id of refresh){
|
|
2016
|
+
try {
|
|
2017
|
+
await beats.doc(id).set({
|
|
2018
|
+
jobId: id,
|
|
2019
|
+
atMs: now,
|
|
2020
|
+
at: new Date(now).toISOString()
|
|
2021
|
+
}, {
|
|
2022
|
+
merge: true
|
|
2023
|
+
});
|
|
2024
|
+
} catch (unused) {
|
|
2025
|
+
landed = false;
|
|
2026
|
+
}
|
|
2027
|
+
}
|
|
2028
|
+
try {
|
|
2029
|
+
await summary.set({
|
|
2030
|
+
beats: Object.fromEntries(ids.map((id)=>[
|
|
2031
|
+
id,
|
|
2032
|
+
now
|
|
2033
|
+
]))
|
|
2034
|
+
}, {
|
|
2035
|
+
merge: true
|
|
2036
|
+
});
|
|
2037
|
+
} catch (unused) {
|
|
2038
|
+
// The summary is what the reader reads first; without it, give every job
|
|
2039
|
+
// the mark the reader confirms against, as the single mark would have.
|
|
2040
|
+
for (const id of ids){
|
|
2041
|
+
if (refresh.includes(id)) continue;
|
|
2042
|
+
try {
|
|
2043
|
+
await beats.doc(id).set({
|
|
2044
|
+
jobId: id,
|
|
2045
|
+
atMs: now,
|
|
2046
|
+
at: new Date(now).toISOString()
|
|
2047
|
+
}, {
|
|
2048
|
+
merge: true
|
|
2049
|
+
});
|
|
2050
|
+
} catch (unused) {
|
|
2051
|
+
landed = false;
|
|
2052
|
+
}
|
|
2053
|
+
}
|
|
2054
|
+
}
|
|
2055
|
+
return landed;
|
|
2056
|
+
}
|
|
1950
2057
|
/**
|
|
1951
2058
|
* The beats a summary document holds, ignoring anything malformed.
|
|
1952
2059
|
*
|