@aglyn/aglyn 1.0.0-beta.260 → 1.0.0-beta.261
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 +11 -11
- 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 +17 -1
- package/src/lib/app-utils/element-motion.js +36 -10
- package/src/lib/app-utils/element-motion.js.map +1 -1
- package/src/lib/plugin-manager/stock-photo-provider.d.ts +5 -0
- package/src/lib/plugin-manager/stock-photo-provider.js.map +1 -1
|
@@ -36,7 +36,15 @@ import { type AnimationEase, type AnimationPreset } from './element-animation';
|
|
|
36
36
|
* page's Largest Contentful Paint, never move: they are painted where
|
|
37
37
|
* they stand from the first frame. Its band — a background color or a
|
|
38
38
|
* cover photo — never moves either.
|
|
39
|
-
* - Every later section's content slides in on `scroll
|
|
39
|
+
* - Every later section's content slides in on `scroll`, each section in
|
|
40
|
+
* turn taking the next of the character's {@link
|
|
41
|
+
* ElementMotionCharacter.sectionPresets}, so a page does not arrive as
|
|
42
|
+
* one repeated move.
|
|
43
|
+
* - A split — a column of words beside a picture — comes in from its
|
|
44
|
+
* sides when the character says {@link ElementMotionCharacter.sides}:
|
|
45
|
+
* the left column from the left, the right from the right, the second a
|
|
46
|
+
* stagger step after the first. A picture whose preset zooms keeps its
|
|
47
|
+
* zoom.
|
|
40
48
|
* - A group of like items — a grid of cards, a list, the questions of an
|
|
41
49
|
* FAQ, a dataset repeated over one card — STAGGERS its items, one after
|
|
42
50
|
* another, rather than arriving as one block.
|
|
@@ -71,6 +79,14 @@ export interface ElementMotionCharacter {
|
|
|
71
79
|
staggerStepMs: number;
|
|
72
80
|
/** Gap between the rungs of the first section's load ladder. */
|
|
73
81
|
heroStepMs: number;
|
|
82
|
+
/**
|
|
83
|
+
* The presets later sections take in turn, the second section the first of
|
|
84
|
+
* them; {@link preset} alone when absent. A section's pictures still take
|
|
85
|
+
* {@link mediaPreset}.
|
|
86
|
+
*/
|
|
87
|
+
sectionPresets?: readonly AnimationPreset[];
|
|
88
|
+
/** Whether a split's two columns come in from their own sides. */
|
|
89
|
+
sides?: boolean;
|
|
74
90
|
}
|
|
75
91
|
/** The motion a page gets when nothing says otherwise: a soft rise, as the attributes panel defaults to. */
|
|
76
92
|
export declare const ELEMENT_MOTION_DEFAULT: ElementMotionCharacter;
|
|
@@ -22,7 +22,12 @@ import { ANIMATION_MAX_DELAY_MS, ANIMATION_MAX_DURATION_MS, ANIMATION_MAX_STAGGE
|
|
|
22
22
|
ease: 'smooth',
|
|
23
23
|
durationMs: 600,
|
|
24
24
|
staggerStepMs: 90,
|
|
25
|
-
heroStepMs: 120
|
|
25
|
+
heroStepMs: 120,
|
|
26
|
+
sectionPresets: [
|
|
27
|
+
'slide-up',
|
|
28
|
+
'fade'
|
|
29
|
+
],
|
|
30
|
+
sides: true
|
|
26
31
|
};
|
|
27
32
|
/** The most rungs the first section's ladder climbs; later blocks share the last. */ export const ELEMENT_MOTION_HERO_RUNGS = 6;
|
|
28
33
|
/**
|
|
@@ -118,6 +123,7 @@ const clamp = (value, max)=>Math.min(Math.max(Math.round(value), 0), max);
|
|
|
118
123
|
* move.
|
|
119
124
|
*/ export function applyElementMotion(nodes, options = {}) {
|
|
120
125
|
var _options_character, _options_rootId, _options_sectionIds;
|
|
126
|
+
var _character_sectionPresets;
|
|
121
127
|
if (!nodes) return nodes;
|
|
122
128
|
const character = (_options_character = options.character) != null ? _options_character : ELEMENT_MOTION_DEFAULT;
|
|
123
129
|
const rootId = (_options_rootId = options.rootId) != null ? _options_rootId : CANVAS_ROOT_ELEMENT_ID;
|
|
@@ -201,14 +207,15 @@ const clamp = (value, max)=>Math.min(Math.max(Math.round(value), 0), max);
|
|
|
201
207
|
}
|
|
202
208
|
return false;
|
|
203
209
|
};
|
|
204
|
-
const presetOf = (id, group)=>{
|
|
210
|
+
const presetOf = (id, group, words)=>{
|
|
205
211
|
const media = group ? children(id).every(leadsWithMedia) : leadsWithMedia(id);
|
|
206
|
-
return media ? character.mediaPreset :
|
|
212
|
+
return media ? character.mediaPreset : words;
|
|
207
213
|
};
|
|
208
|
-
const animate = (id, trigger, delayMs)=>{
|
|
214
|
+
const animate = (id, trigger, delayMs, words = character.preset, side)=>{
|
|
209
215
|
const group = isGroup(id);
|
|
216
|
+
const preset = presetOf(id, group, words);
|
|
210
217
|
patches.set(id, _extends({
|
|
211
|
-
[NODE_ANIMATION_PROP]:
|
|
218
|
+
[NODE_ANIMATION_PROP]: side && !preset.startsWith('zoom') ? side : preset,
|
|
212
219
|
[NODE_ANIMATION_TRIGGER_PROP]: trigger,
|
|
213
220
|
[NODE_ANIMATION_DURATION_PROP]: clamp(character.durationMs, ANIMATION_MAX_DURATION_MS)
|
|
214
221
|
}, delayMs > 0 ? {
|
|
@@ -229,13 +236,27 @@ const clamp = (value, max)=>Math.min(Math.max(Math.round(value), 0), max);
|
|
|
229
236
|
}
|
|
230
237
|
return at;
|
|
231
238
|
};
|
|
232
|
-
/**
|
|
239
|
+
/**
|
|
240
|
+
* A column of words beside a picture: two blocks, one of which leads with a
|
|
241
|
+
* picture and one of which does not, neither left alone. Two cards that
|
|
242
|
+
* both lead with one are a group, and stagger.
|
|
243
|
+
*/ const isSplit = (id)=>{
|
|
244
|
+
const kids = children(id);
|
|
245
|
+
return kids.length === 2 && leadsWithMedia(kids[0]) !== leadsWithMedia(kids[1]) && !kids.some(holds);
|
|
246
|
+
};
|
|
247
|
+
/** A later section's content: on scroll, groups staggered, splits from their sides, nothing nested. */ const place = (id, words)=>{
|
|
233
248
|
if (closed(id)) return;
|
|
249
|
+
if (character.sides && isSplit(id)) {
|
|
250
|
+
const [start, end] = children(id);
|
|
251
|
+
animate(start, 'scroll', 0, words, 'slide-right');
|
|
252
|
+
animate(end, 'scroll', character.staggerStepMs, words, 'slide-left');
|
|
253
|
+
return;
|
|
254
|
+
}
|
|
234
255
|
if (holds(id) || !isGroup(id) && holdsGroup(id)) {
|
|
235
|
-
for (const child of children(id))place(child);
|
|
256
|
+
for (const child of children(id))place(child, words);
|
|
236
257
|
return;
|
|
237
258
|
}
|
|
238
|
-
animate(id, 'scroll', 0);
|
|
259
|
+
animate(id, 'scroll', 0, words);
|
|
239
260
|
};
|
|
240
261
|
/** Down through wrappers of one child that are free to move. */ const descend = (id)=>{
|
|
241
262
|
let at = id;
|
|
@@ -316,15 +337,20 @@ const clamp = (value, max)=>Math.min(Math.max(Math.round(value), 0), max);
|
|
|
316
337
|
};
|
|
317
338
|
const sections = ((_options_sectionIds = options.sectionIds) != null ? _options_sectionIds : children(rootId)).filter((id)=>!!node(id));
|
|
318
339
|
if (sections[0]) markStill(sections[0]);
|
|
340
|
+
const turns = ((_character_sectionPresets = character.sectionPresets) == null ? void 0 : _character_sectionPresets.length) ? character.sectionPresets : [
|
|
341
|
+
character.preset
|
|
342
|
+
];
|
|
319
343
|
sections.forEach((id, index)=>{
|
|
344
|
+
var _turns_;
|
|
320
345
|
if (closed(id)) return;
|
|
321
346
|
const content = contentOf(id);
|
|
322
347
|
// The band itself never moves — only what it holds — so a section that
|
|
323
348
|
// is nothing but its band has nothing to move.
|
|
324
349
|
if (content === id && !children(id).length) return;
|
|
350
|
+
const words = (_turns_ = turns[(index - 1) % turns.length]) != null ? _turns_ : character.preset;
|
|
325
351
|
if (index === 0) ladder(content);
|
|
326
|
-
else if (content === id && !isGroup(id)) for (const child of children(id))place(child);
|
|
327
|
-
else place(content);
|
|
352
|
+
else if (content === id && !isGroup(id) && !(character.sides && isSplit(id))) for (const child of children(id))place(child, words);
|
|
353
|
+
else place(content, words);
|
|
328
354
|
});
|
|
329
355
|
if (!patches.size) return nodes;
|
|
330
356
|
const next = _extends({}, 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`.\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\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}\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): AnimationPreset => {\n const media = group ? children(id).every(leadsWithMedia) : leadsWithMedia(id)\n return media ? character.mediaPreset : character.preset\n }\n\n const animate = (id: string, trigger: AnimationTrigger, delayMs: number): void => {\n const group = isGroup(id)\n patches.set(id, {\n [NODE_ANIMATION_PROP]: presetOf(id, group),\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 /** A later section's content: on scroll, groups staggered, nothing nested. */\n const place = (id: string): void => {\n if (closed(id)) return\n if (holds(id) || (!isGroup(id) && holdsGroup(id))) {\n for (const child of children(id)) place(child)\n return\n }\n animate(id, 'scroll', 0)\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 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 if (index === 0) ladder(content)\n else if (content === id && !isGroup(id)) for (const child of children(id)) place(child)\n else place(content)\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","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","media","animate","trigger","delayMs","contentOf","place","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","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.\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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"}
|
|
@@ -198,6 +198,11 @@ export interface StockPhotoPick {
|
|
|
198
198
|
* picture of last resort when the library is down or found nothing.
|
|
199
199
|
*/
|
|
200
200
|
origin: 'library' | 'starter';
|
|
201
|
+
/**
|
|
202
|
+
* What the photo shows, in words a page's `alt` takes as they stand
|
|
203
|
+
* (AGL-3856): the photo's own where the subject's words do not describe it.
|
|
204
|
+
*/
|
|
205
|
+
alt?: string;
|
|
201
206
|
}
|
|
202
207
|
export interface StockPhotoPicker {
|
|
203
208
|
/** A photo for the subject, or `null` when none can be had. Never throws. */
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../../../../libs/aglyn/src/lib/plugin-manager/stock-photo-provider.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport {\n definePluginServiceContract,\n registerPluginService,\n resolvePluginService,\n resolvePluginServices,\n} from './plugin-services'\n\n/**\n * STOCK PHOTOS (AGL-3660): a library of licensed photos outside this\n * platform that a server process searches and copies from.\n *\n * Core defines the contract and never names a library. A plugin registers a\n * provider from its server declarations, and a caller — the AI site build is\n * the first — asks for {@link stockPhotoProvider} and gets the first one\n * this deployment configured, or `null`. Nothing here stores a photo: a\n * caller that keeps one copies its bytes into the site's own media library\n * (`plugin-media-ingest.ts`), because a page that names a library's own\n * address would hotlink it, and most libraries' terms refuse that.\n *\n * What a provider owes its library's terms — caching searches, its rate\n * limit, the hosts it may fetch from — it keeps inside itself, so a caller\n * cannot get them wrong. What it owes the caller is {@link\n * StockPhotoProvider.credit}: the words and links the asset records about\n * where it came from, and whether the library requires them shown.\n *\n * A provider that is registered and not configured (its key unset) answers\n * as absent, so a deployment without the key behaves as one without the\n * plugin.\n */\n\n/** The shape a caller wants; a provider maps it onto its own filter. */\nexport type StockPhotoOrientation = 'horizontal' | 'vertical' | 'any'\n\nexport interface StockPhotoSearchRequest {\n /** Plain search words, at most {@link STOCK_PHOTO_QUERY_MAX_CHARS}. */\n query: string\n orientation?: StockPhotoOrientation\n /** The smallest original a hit may have, in pixels. */\n minWidth?: number\n minHeight?: number\n /** A hint that the photo should show people, where the library can filter by it. */\n people?: boolean\n}\n\n/** The longest query a caller sends; libraries refuse or truncate longer ones. */\nexport const STOCK_PHOTO_QUERY_MAX_CHARS = 100\n\n/** One photo a search found. */\nexport interface StockPhoto {\n /** The registering provider's id. */\n provider: string\n /** The library's own id for the photo, stable across searches. */\n id: string\n /** The size of the copy {@link StockPhotoProvider.download} fetches. */\n width: number\n height: number\n /** The photo's page at the library, for the credit. */\n pageUrl: string\n /** The contributor's name as the library shows it. */\n photographer: string\n /** The contributor's page at the library, when it has one. */\n photographerUrl?: string\n /** The library's tags, lowercase. */\n tags: string[]\n /**\n * The library's own description of the photo, where it writes one (a\n * Pexels hit's `alt`). A caller ranks hits by it with the tags.\n */\n alt?: string\n}\n\n/** What an asset copied from a library records about where it came from. */\nexport interface StockPhotoCredit {\n /** The library's name, as its terms ask it written. */\n providerLabel: string\n /** The license the photo is used under. */\n license: string\n licenseUrl: string\n /** Whether the license requires the credit shown wherever the photo is. */\n attributionRequired: boolean\n /** A sentence crediting the photo, for the asset's description. */\n text: string\n}\n\nexport interface StockPhotoSearchResult {\n photos: StockPhoto[]\n /** Whether the answer came from the provider's own cache. */\n cached: boolean\n}\n\nexport interface StockPhotoDownload {\n bytes: Uint8Array\n /** `image/jpeg`, `image/png` or `image/webp`. */\n contentType: string\n}\n\nexport interface StockPhotoProvider {\n /** A stable id, the prefix of every source key: `pixabay`. */\n id: string\n /** The library's name, in customer copy. */\n label: string\n /** Whether this deployment holds what the provider needs to search. */\n isConfigured(): boolean\n /**\n * The photos matching `request`, best first. `null` when the provider\n * cannot ask right now — its rate limit is spent, or the library failed —\n * which a caller treats as \"no photo\", never as an error to retry.\n */\n search(\n request: StockPhotoSearchRequest,\n options?: { signal?: AbortSignal },\n ): Promise<StockPhotoSearchResult | null>\n /**\n * The photo's bytes, fetched only from the library's own image hosts, or\n * `null` when they cannot be had within `maxBytes` and the signal.\n */\n download(\n photo: StockPhoto,\n options: { maxBytes: number; signal?: AbortSignal },\n ): Promise<StockPhotoDownload | null>\n /** How an asset copied from `photo` credits it. */\n credit(photo: StockPhoto): StockPhotoCredit\n}\n\n/**\n * Many plugins may each register a library; a caller is handed the first\n * configured one, highest priority first.\n */\nexport const STOCK_PHOTO_PROVIDERS = definePluginServiceContract<StockPhotoProvider>(\n 'core.stock-photos',\n { multiple: true },\n)\n\n/** The key an asset copied from a library is found again by: `{provider}:{id}`. */\nexport function stockPhotoSourceKey(photo: Pick<StockPhoto, 'provider' | 'id'>): string {\n return `${photo.provider}:${photo.id}`\n}\n\n/**\n * Registers a library. The owner is the loader's marker when a register fn\n * is running, else `options.pluginId`; the provider's own id tells two\n * libraries of one plugin apart.\n */\nexport function registerStockPhotoProvider(\n provider: StockPhotoProvider,\n options?: { pluginId?: string; priority?: number },\n): void {\n if (!provider?.id?.trim()) throw new Error('a stock photo provider needs an id')\n registerPluginService(STOCK_PHOTO_PROVIDERS, provider, {\n key: provider.id,\n ...(options?.pluginId ? { pluginId: options.pluginId } : {}),\n ...(options?.priority !== undefined ? { priority: options.priority } : {}),\n })\n}\n\n/**\n * The first registered library this deployment configured, or `null`.\n * Synchronous and free of I/O. A provider whose configuration check throws\n * answers as unconfigured: a stock photo is decoration, and its failure\n * leaves the caller's own fallback standing.\n */\nexport function stockPhotoProvider(): StockPhotoProvider | null {\n for (const { impl } of resolvePluginServices(STOCK_PHOTO_PROVIDERS)) {\n try {\n if (impl.isConfigured()) return impl\n } catch {\n // Unconfigured, as the note says.\n }\n }\n return null\n}\n\n/**\n * A PHOTO FOR A SUBJECT, KEPT IN THE SITE'S LIBRARY (AGL-3755).\n *\n * The providers above answer searches; choosing well among the hits is\n * work of its own — subject nouns rather than whole sentences, hits scored\n * by how their tags meet the subject and the business's world, nothing\n * placed twice in one act, the business's own world when the subject finds\n * nothing, and a starter photo when the library is down — and it lives in\n * the plugin that learned it on live site starts (the AI plugin). A plugin's\n * draft writer that wants a picture (an event's cover) asks\n * {@link stockPhotoPicker} for the deployment's one picker and gets a photo\n * the site can serve, or `null` where no picker is registered. It never\n * imports the plugin that picks.\n *\n * The picker copies a stock photo into the site's own library as the member\n * named, through `core.media-ingest`, and only once: a photo the library\n * already holds (by its source key) is reused, never stored a second time.\n */\nexport interface StockPhotoPickRequest {\n hostId: string\n /** The member the photo is stored as; their access, the site's lockdown and the plan's storage are checked as for an upload of theirs. */\n uid: string\n /** What the photo shows, in plain words: an event's title, a product's name. The picker reads the subject's nouns out of it. */\n subject: string\n /** The business in its own words (\"a sourdough bakery\"), which holds the photo to its world and is searched when the subject finds nothing. */\n business?: string\n /** The asset's description in the library; the subject where absent. */\n alt?: string\n /** The frame the photo fills; `horizontal` where absent. */\n orientation?: StockPhotoOrientation\n /**\n * The act the photo is one of (an AI job's id): no photo is placed twice\n * within it. Absent, only the site library's own reuse applies.\n */\n scope?: string\n /** Turns the choice among equally good photos; the same seed picks the same photo again. */\n seed?: string\n /** Source keys or asset srcs the act already shows. */\n avoid?: readonly string[]\n signal?: AbortSignal\n}\n\nexport interface StockPhotoPick {\n /** What an `image` node's `src` holds: the asset's media reference, or a starter photo's path. */\n src: string\n /** An address an `<img src>` or an image-address field takes as it stands: site-relative. */\n url: string\n width: number\n height: number\n /**\n * `library`: a stock photo in the site's own media library, copied there\n * now or reused. `starter`: one of the platform's starter photos, the\n * picture of last resort when the library is down or found nothing.\n */\n origin: 'library' | 'starter'\n}\n\nexport interface StockPhotoPicker {\n /** A photo for the subject, or `null` when none can be had. Never throws. */\n pick(request: StockPhotoPickRequest): Promise<StockPhotoPick | null>\n}\n\n/** One picker per deployment: the plugin whose choosing it is registers it. */\nexport const STOCK_PHOTO_PICKER = definePluginServiceContract<StockPhotoPicker>(\n 'core.stock-photo-picker',\n { multiple: false },\n)\n\n/** Registers the deployment's picker; registering again replaces it. */\nexport function registerStockPhotoPicker(\n picker: StockPhotoPicker,\n options?: { pluginId?: string },\n): void {\n if (typeof picker?.pick !== 'function') throw new Error('a stock photo picker needs pick')\n registerPluginService(STOCK_PHOTO_PICKER, picker, {\n ...(options?.pluginId ? { pluginId: options.pluginId } : {}),\n })\n}\n\n/** The registered picker, or `null` in a process that registered none. */\nexport function stockPhotoPicker(): StockPhotoPicker | null {\n return resolvePluginService(STOCK_PHOTO_PICKER) ?? null\n}\n"],"names":["definePluginServiceContract","registerPluginService","resolvePluginService","resolvePluginServices","STOCK_PHOTO_QUERY_MAX_CHARS","STOCK_PHOTO_PROVIDERS","multiple","stockPhotoSourceKey","photo","provider","id","registerStockPhotoProvider","options","trim","Error","key","pluginId","priority","undefined","stockPhotoProvider","impl","isConfigured","STOCK_PHOTO_PICKER","registerStockPhotoPicker","picker","pick","stockPhotoPicker"],"mappings":";AAAA;;;;;;;;;;;;;;;CAeC,GAED,SACEA,2BAA2B,EAC3BC,qBAAqB,EACrBC,oBAAoB,EACpBC,qBAAqB,QAChB,uBAAmB;AAuC1B,gFAAgF,GAChF,OAAO,MAAMC,8BAA8B,IAAG;AA+E9C;;;CAGC,GACD,OAAO,MAAMC,wBAAwBL,4BACnC,qBACA;IAAEM,UAAU;AAAK,GAClB;AAED,iFAAiF,GACjF,OAAO,SAASC,oBAAoBC,KAA0C;IAC5E,OAAO,GAAGA,MAAMC,QAAQ,CAAC,CAAC,EAAED,MAAME,EAAE,EAAE;AACxC;AAEA;;;;CAIC,GACD,OAAO,SAASC,2BACdF,QAA4B,EAC5BG,OAAkD;QAE7CH;IAAL,IAAI,EAACA,6BAAAA,eAAAA,SAAUC,EAAE,qBAAZD,aAAcI,IAAI,KAAI,MAAM,IAAIC,MAAM;IAC3Cb,sBAAsBI,uBAAuBI,UAAU;QACrDM,KAAKN,SAASC,EAAE;OACZE,CAAAA,2BAAAA,QAASI,QAAQ,IAAG;QAAEA,UAAUJ,QAAQI,QAAQ;IAAC,IAAI,CAAC,GACtDJ,CAAAA,2BAAAA,QAASK,QAAQ,MAAKC,YAAY;QAAED,UAAUL,QAAQK,QAAQ;IAAC,IAAI,CAAC;AAE5E;AAEA;;;;;CAKC,GACD,OAAO,SAASE;IACd,KAAK,MAAM,EAAEC,IAAI,EAAE,IAAIjB,sBAAsBE,uBAAwB;QACnE,IAAI;YACF,IAAIe,KAAKC,YAAY,IAAI,OAAOD;QAClC,EAAE,eAAM;QACN,kCAAkC;QACpC;IACF;IACA,OAAO;AACT;AAgEA,6EAA6E,GAC7E,OAAO,MAAME,qBAAqBtB,4BAChC,2BACA;IAAEM,UAAU;AAAM,GACnB;AAED,sEAAsE,GACtE,OAAO,SAASiB,yBACdC,MAAwB,EACxBZ,OAA+B;IAE/B,IAAI,QAAOY,0BAAAA,OAAQC,IAAI,MAAK,YAAY,MAAM,IAAIX,MAAM;IACxDb,sBAAsBqB,oBAAoBE,QAAQ,aAC5CZ,CAAAA,2BAAAA,QAASI,QAAQ,IAAG;QAAEA,UAAUJ,QAAQI,QAAQ;IAAC,IAAI,CAAC;AAE9D;AAEA,wEAAwE,GACxE,OAAO,SAASU;QACPxB;IAAP,QAAOA,wBAAAA,qBAAqBoB,+BAArBpB,wBAA4C;AACrD"}
|
|
1
|
+
{"version":3,"sources":["../../../../../../libs/aglyn/src/lib/plugin-manager/stock-photo-provider.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport {\n definePluginServiceContract,\n registerPluginService,\n resolvePluginService,\n resolvePluginServices,\n} from './plugin-services'\n\n/**\n * STOCK PHOTOS (AGL-3660): a library of licensed photos outside this\n * platform that a server process searches and copies from.\n *\n * Core defines the contract and never names a library. A plugin registers a\n * provider from its server declarations, and a caller — the AI site build is\n * the first — asks for {@link stockPhotoProvider} and gets the first one\n * this deployment configured, or `null`. Nothing here stores a photo: a\n * caller that keeps one copies its bytes into the site's own media library\n * (`plugin-media-ingest.ts`), because a page that names a library's own\n * address would hotlink it, and most libraries' terms refuse that.\n *\n * What a provider owes its library's terms — caching searches, its rate\n * limit, the hosts it may fetch from — it keeps inside itself, so a caller\n * cannot get them wrong. What it owes the caller is {@link\n * StockPhotoProvider.credit}: the words and links the asset records about\n * where it came from, and whether the library requires them shown.\n *\n * A provider that is registered and not configured (its key unset) answers\n * as absent, so a deployment without the key behaves as one without the\n * plugin.\n */\n\n/** The shape a caller wants; a provider maps it onto its own filter. */\nexport type StockPhotoOrientation = 'horizontal' | 'vertical' | 'any'\n\nexport interface StockPhotoSearchRequest {\n /** Plain search words, at most {@link STOCK_PHOTO_QUERY_MAX_CHARS}. */\n query: string\n orientation?: StockPhotoOrientation\n /** The smallest original a hit may have, in pixels. */\n minWidth?: number\n minHeight?: number\n /** A hint that the photo should show people, where the library can filter by it. */\n people?: boolean\n}\n\n/** The longest query a caller sends; libraries refuse or truncate longer ones. */\nexport const STOCK_PHOTO_QUERY_MAX_CHARS = 100\n\n/** One photo a search found. */\nexport interface StockPhoto {\n /** The registering provider's id. */\n provider: string\n /** The library's own id for the photo, stable across searches. */\n id: string\n /** The size of the copy {@link StockPhotoProvider.download} fetches. */\n width: number\n height: number\n /** The photo's page at the library, for the credit. */\n pageUrl: string\n /** The contributor's name as the library shows it. */\n photographer: string\n /** The contributor's page at the library, when it has one. */\n photographerUrl?: string\n /** The library's tags, lowercase. */\n tags: string[]\n /**\n * The library's own description of the photo, where it writes one (a\n * Pexels hit's `alt`). A caller ranks hits by it with the tags.\n */\n alt?: string\n}\n\n/** What an asset copied from a library records about where it came from. */\nexport interface StockPhotoCredit {\n /** The library's name, as its terms ask it written. */\n providerLabel: string\n /** The license the photo is used under. */\n license: string\n licenseUrl: string\n /** Whether the license requires the credit shown wherever the photo is. */\n attributionRequired: boolean\n /** A sentence crediting the photo, for the asset's description. */\n text: string\n}\n\nexport interface StockPhotoSearchResult {\n photos: StockPhoto[]\n /** Whether the answer came from the provider's own cache. */\n cached: boolean\n}\n\nexport interface StockPhotoDownload {\n bytes: Uint8Array\n /** `image/jpeg`, `image/png` or `image/webp`. */\n contentType: string\n}\n\nexport interface StockPhotoProvider {\n /** A stable id, the prefix of every source key: `pixabay`. */\n id: string\n /** The library's name, in customer copy. */\n label: string\n /** Whether this deployment holds what the provider needs to search. */\n isConfigured(): boolean\n /**\n * The photos matching `request`, best first. `null` when the provider\n * cannot ask right now — its rate limit is spent, or the library failed —\n * which a caller treats as \"no photo\", never as an error to retry.\n */\n search(\n request: StockPhotoSearchRequest,\n options?: { signal?: AbortSignal },\n ): Promise<StockPhotoSearchResult | null>\n /**\n * The photo's bytes, fetched only from the library's own image hosts, or\n * `null` when they cannot be had within `maxBytes` and the signal.\n */\n download(\n photo: StockPhoto,\n options: { maxBytes: number; signal?: AbortSignal },\n ): Promise<StockPhotoDownload | null>\n /** How an asset copied from `photo` credits it. */\n credit(photo: StockPhoto): StockPhotoCredit\n}\n\n/**\n * Many plugins may each register a library; a caller is handed the first\n * configured one, highest priority first.\n */\nexport const STOCK_PHOTO_PROVIDERS = definePluginServiceContract<StockPhotoProvider>(\n 'core.stock-photos',\n { multiple: true },\n)\n\n/** The key an asset copied from a library is found again by: `{provider}:{id}`. */\nexport function stockPhotoSourceKey(photo: Pick<StockPhoto, 'provider' | 'id'>): string {\n return `${photo.provider}:${photo.id}`\n}\n\n/**\n * Registers a library. The owner is the loader's marker when a register fn\n * is running, else `options.pluginId`; the provider's own id tells two\n * libraries of one plugin apart.\n */\nexport function registerStockPhotoProvider(\n provider: StockPhotoProvider,\n options?: { pluginId?: string; priority?: number },\n): void {\n if (!provider?.id?.trim()) throw new Error('a stock photo provider needs an id')\n registerPluginService(STOCK_PHOTO_PROVIDERS, provider, {\n key: provider.id,\n ...(options?.pluginId ? { pluginId: options.pluginId } : {}),\n ...(options?.priority !== undefined ? { priority: options.priority } : {}),\n })\n}\n\n/**\n * The first registered library this deployment configured, or `null`.\n * Synchronous and free of I/O. A provider whose configuration check throws\n * answers as unconfigured: a stock photo is decoration, and its failure\n * leaves the caller's own fallback standing.\n */\nexport function stockPhotoProvider(): StockPhotoProvider | null {\n for (const { impl } of resolvePluginServices(STOCK_PHOTO_PROVIDERS)) {\n try {\n if (impl.isConfigured()) return impl\n } catch {\n // Unconfigured, as the note says.\n }\n }\n return null\n}\n\n/**\n * A PHOTO FOR A SUBJECT, KEPT IN THE SITE'S LIBRARY (AGL-3755).\n *\n * The providers above answer searches; choosing well among the hits is\n * work of its own — subject nouns rather than whole sentences, hits scored\n * by how their tags meet the subject and the business's world, nothing\n * placed twice in one act, the business's own world when the subject finds\n * nothing, and a starter photo when the library is down — and it lives in\n * the plugin that learned it on live site starts (the AI plugin). A plugin's\n * draft writer that wants a picture (an event's cover) asks\n * {@link stockPhotoPicker} for the deployment's one picker and gets a photo\n * the site can serve, or `null` where no picker is registered. It never\n * imports the plugin that picks.\n *\n * The picker copies a stock photo into the site's own library as the member\n * named, through `core.media-ingest`, and only once: a photo the library\n * already holds (by its source key) is reused, never stored a second time.\n */\nexport interface StockPhotoPickRequest {\n hostId: string\n /** The member the photo is stored as; their access, the site's lockdown and the plan's storage are checked as for an upload of theirs. */\n uid: string\n /** What the photo shows, in plain words: an event's title, a product's name. The picker reads the subject's nouns out of it. */\n subject: string\n /** The business in its own words (\"a sourdough bakery\"), which holds the photo to its world and is searched when the subject finds nothing. */\n business?: string\n /** The asset's description in the library; the subject where absent. */\n alt?: string\n /** The frame the photo fills; `horizontal` where absent. */\n orientation?: StockPhotoOrientation\n /**\n * The act the photo is one of (an AI job's id): no photo is placed twice\n * within it. Absent, only the site library's own reuse applies.\n */\n scope?: string\n /** Turns the choice among equally good photos; the same seed picks the same photo again. */\n seed?: string\n /** Source keys or asset srcs the act already shows. */\n avoid?: readonly string[]\n signal?: AbortSignal\n}\n\nexport interface StockPhotoPick {\n /** What an `image` node's `src` holds: the asset's media reference, or a starter photo's path. */\n src: string\n /** An address an `<img src>` or an image-address field takes as it stands: site-relative. */\n url: string\n width: number\n height: number\n /**\n * `library`: a stock photo in the site's own media library, copied there\n * now or reused. `starter`: one of the platform's starter photos, the\n * picture of last resort when the library is down or found nothing.\n */\n origin: 'library' | 'starter'\n /**\n * What the photo shows, in words a page's `alt` takes as they stand\n * (AGL-3856): the photo's own where the subject's words do not describe it.\n */\n alt?: string\n}\n\nexport interface StockPhotoPicker {\n /** A photo for the subject, or `null` when none can be had. Never throws. */\n pick(request: StockPhotoPickRequest): Promise<StockPhotoPick | null>\n}\n\n/** One picker per deployment: the plugin whose choosing it is registers it. */\nexport const STOCK_PHOTO_PICKER = definePluginServiceContract<StockPhotoPicker>(\n 'core.stock-photo-picker',\n { multiple: false },\n)\n\n/** Registers the deployment's picker; registering again replaces it. */\nexport function registerStockPhotoPicker(\n picker: StockPhotoPicker,\n options?: { pluginId?: string },\n): void {\n if (typeof picker?.pick !== 'function') throw new Error('a stock photo picker needs pick')\n registerPluginService(STOCK_PHOTO_PICKER, picker, {\n ...(options?.pluginId ? { pluginId: options.pluginId } : {}),\n })\n}\n\n/** The registered picker, or `null` in a process that registered none. */\nexport function stockPhotoPicker(): StockPhotoPicker | null {\n return resolvePluginService(STOCK_PHOTO_PICKER) ?? null\n}\n"],"names":["definePluginServiceContract","registerPluginService","resolvePluginService","resolvePluginServices","STOCK_PHOTO_QUERY_MAX_CHARS","STOCK_PHOTO_PROVIDERS","multiple","stockPhotoSourceKey","photo","provider","id","registerStockPhotoProvider","options","trim","Error","key","pluginId","priority","undefined","stockPhotoProvider","impl","isConfigured","STOCK_PHOTO_PICKER","registerStockPhotoPicker","picker","pick","stockPhotoPicker"],"mappings":";AAAA;;;;;;;;;;;;;;;CAeC,GAED,SACEA,2BAA2B,EAC3BC,qBAAqB,EACrBC,oBAAoB,EACpBC,qBAAqB,QAChB,uBAAmB;AAuC1B,gFAAgF,GAChF,OAAO,MAAMC,8BAA8B,IAAG;AA+E9C;;;CAGC,GACD,OAAO,MAAMC,wBAAwBL,4BACnC,qBACA;IAAEM,UAAU;AAAK,GAClB;AAED,iFAAiF,GACjF,OAAO,SAASC,oBAAoBC,KAA0C;IAC5E,OAAO,GAAGA,MAAMC,QAAQ,CAAC,CAAC,EAAED,MAAME,EAAE,EAAE;AACxC;AAEA;;;;CAIC,GACD,OAAO,SAASC,2BACdF,QAA4B,EAC5BG,OAAkD;QAE7CH;IAAL,IAAI,EAACA,6BAAAA,eAAAA,SAAUC,EAAE,qBAAZD,aAAcI,IAAI,KAAI,MAAM,IAAIC,MAAM;IAC3Cb,sBAAsBI,uBAAuBI,UAAU;QACrDM,KAAKN,SAASC,EAAE;OACZE,CAAAA,2BAAAA,QAASI,QAAQ,IAAG;QAAEA,UAAUJ,QAAQI,QAAQ;IAAC,IAAI,CAAC,GACtDJ,CAAAA,2BAAAA,QAASK,QAAQ,MAAKC,YAAY;QAAED,UAAUL,QAAQK,QAAQ;IAAC,IAAI,CAAC;AAE5E;AAEA;;;;;CAKC,GACD,OAAO,SAASE;IACd,KAAK,MAAM,EAAEC,IAAI,EAAE,IAAIjB,sBAAsBE,uBAAwB;QACnE,IAAI;YACF,IAAIe,KAAKC,YAAY,IAAI,OAAOD;QAClC,EAAE,eAAM;QACN,kCAAkC;QACpC;IACF;IACA,OAAO;AACT;AAqEA,6EAA6E,GAC7E,OAAO,MAAME,qBAAqBtB,4BAChC,2BACA;IAAEM,UAAU;AAAM,GACnB;AAED,sEAAsE,GACtE,OAAO,SAASiB,yBACdC,MAAwB,EACxBZ,OAA+B;IAE/B,IAAI,QAAOY,0BAAAA,OAAQC,IAAI,MAAK,YAAY,MAAM,IAAIX,MAAM;IACxDb,sBAAsBqB,oBAAoBE,QAAQ,aAC5CZ,CAAAA,2BAAAA,QAASI,QAAQ,IAAG;QAAEA,UAAUJ,QAAQI,QAAQ;IAAC,IAAI,CAAC;AAE9D;AAEA,wEAAwE,GACxE,OAAO,SAASU;QACPxB;IAAP,QAAOA,wBAAAA,qBAAqBoB,+BAArBpB,wBAA4C;AACrD"}
|