@aglyn/aglyn 1.0.0-beta.268 → 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.
Files changed (26) hide show
  1. package/package.json +11 -11
  2. package/src/lib/app-utils/docs-index.generated.js +1 -1
  3. package/src/lib/app-utils/docs-index.generated.js.map +1 -1
  4. package/src/lib/app-utils/element-motion.d.ts +14 -2
  5. package/src/lib/app-utils/element-motion.js +33 -13
  6. package/src/lib/app-utils/element-motion.js.map +1 -1
  7. package/src/lib/plugin-manager/operator-alerts.js +10 -1
  8. package/src/lib/plugin-manager/operator-alerts.js.map +1 -1
  9. package/src/lib/plugin-manager/plugin-config.js +10 -1
  10. package/src/lib/plugin-manager/plugin-config.js.map +1 -1
  11. package/src/lib/plugin-manager/plugin-consent-group-change.js +11 -2
  12. package/src/lib/plugin-manager/plugin-consent-group-change.js.map +1 -1
  13. package/src/lib/plugin-manager/plugin-console-crons.js +10 -1
  14. package/src/lib/plugin-manager/plugin-console-crons.js.map +1 -1
  15. package/src/lib/plugin-manager/plugin-events.js +11 -2
  16. package/src/lib/plugin-manager/plugin-events.js.map +1 -1
  17. package/src/lib/plugin-manager/plugin-lead-conversion.js +10 -1
  18. package/src/lib/plugin-manager/plugin-lead-conversion.js.map +1 -1
  19. package/src/lib/plugin-manager/plugin-membership-detach.js +10 -1
  20. package/src/lib/plugin-manager/plugin-membership-detach.js.map +1 -1
  21. package/src/lib/plugin-manager/plugin-person-matches.js +10 -1
  22. package/src/lib/plugin-manager/plugin-person-matches.js.map +1 -1
  23. package/src/lib/plugin-manager/plugin-usage-meters.js +10 -1
  24. package/src/lib/plugin-manager/plugin-usage-meters.js.map +1 -1
  25. package/src/lib/plugin-manager/usage-alert-contributors.js +10 -1
  26. 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, and so is everything inside it. */ const closed = (id)=>{
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
- if (at.props && NODE_ANIMATION_PROPS.some((prop)=>{
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
- })) return true;
149
- return pins((_at_sx = at.sx) == null ? void 0 : _at_sx['position']);
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 (character.sides && isSplit(id)) {
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) && !(character.sides && isSplit(id))) for (const child of children(id))place(child, words);
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":"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"}
@@ -1,4 +1,5 @@
1
1
  import { _ as _extends } from "@swc/helpers/_/_extends";
2
+ var _globalScope_REGISTRY_KEY;
2
3
  /**
3
4
  * @license
4
5
  * Copyright 2026 Aglyn LLC
@@ -28,7 +29,15 @@ import { _ as _extends } from "@swc/helpers/_/_extends";
28
29
  * two plugins, or a plugin and core, from claiming one settings row.
29
30
  * Registering the same type again replaces it in place, so a declarations
30
31
  * module evaluated twice registers once.
31
- */ const registrations = new Map();
32
+ */ /**
33
+ * On `globalThis` under `Symbol.for`, never at module scope (AGL-3947).
34
+ * The alert types are registered at boot, from `instrumentation.ts`, and Next
35
+ * compiles that file apart from the routes, so the boot and a route each hold
36
+ * their own copy of this module. At module scope, the boot filled its copy and
37
+ * every route read an empty one (AGL-3412, AGL-3868).
38
+ */ const REGISTRY_KEY = Symbol.for('@aglyn/aglyn:operator-alerts');
39
+ const globalScope = globalThis;
40
+ const registrations = (_globalScope_REGISTRY_KEY = globalScope[REGISTRY_KEY]) != null ? _globalScope_REGISTRY_KEY : globalScope[REGISTRY_KEY] = new Map();
32
41
  const CORE_TYPES = new Set(CORE_OPERATOR_ALERTS.map((entry)=>entry.type));
33
42
  /**
34
43
  * Registers a plugin's alert types. Throws on a type outside the plugin's
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../../../../libs/aglyn/src/lib/plugin-manager/operator-alerts.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 CORE_OPERATOR_ALERTS,\n type OperatorAlertDefinition,\n} from '../app-utils/operator-alerts'\n\n/**\n * Operator alerts a PLUGIN contributes (AGL-3377), beside core's own\n * `CORE_OPERATOR_ALERTS`.\n *\n * A plugin declares its alert types here, from its server declarations, in\n * the same shape core uses, and raises them through `raiseOperatorAlert`.\n * Core lists no plugin's types, so a plugin that is not installed adds\n * nothing to Staff → Operator alerts and a new plugin needs no core change.\n *\n * A plugin's types must start with its own id and a dot, which is what keeps\n * two plugins, or a plugin and core, from claiming one settings row.\n * Registering the same type again replaces it in place, so a declarations\n * module evaluated twice registers once.\n */\n\nconst registrations = new Map<string, OperatorAlertDefinition>()\n\nconst CORE_TYPES = new Set(CORE_OPERATOR_ALERTS.map((entry) => entry.type))\n\n/**\n * Registers a plugin's alert types. Throws on a type outside the plugin's\n * namespace or one core already owns: both are a programming error the\n * plugin's own tests should meet, not something to discover in production.\n */\nexport function registerOperatorAlerts(\n definitions: readonly OperatorAlertDefinition[],\n options: { pluginId: string },\n): void {\n const pluginId = String(options?.pluginId ?? '').trim()\n if (!pluginId) throw new Error('operator alerts need a pluginId')\n for (const definition of definitions) {\n const type = String(definition?.type ?? '').trim()\n if (!type.startsWith(`${pluginId}.`) || type.length <= pluginId.length + 1) {\n throw new Error(\n `operator alert \"${type}\" must be named \"${pluginId}.<name>\" by the plugin that registers it`,\n )\n }\n if (CORE_TYPES.has(type)) {\n throw new Error(`operator alert \"${type}\" is core's`)\n }\n registrations.set(type, { ...definition, type, pluginId })\n }\n}\n\n/** Every alert type: core's, then each plugin's in type order. */\nexport function listOperatorAlerts(): OperatorAlertDefinition[] {\n const contributed = [...registrations.values()].sort((a, b) =>\n a.type < b.type ? -1 : a.type > b.type ? 1 : 0,\n )\n return [...CORE_OPERATOR_ALERTS, ...contributed]\n}\n\n/** One alert type, core's or a registered plugin's. */\nexport function getOperatorAlert(\n type: string,\n): OperatorAlertDefinition | undefined {\n return (\n CORE_OPERATOR_ALERTS.find((entry) => entry.type === type) ??\n registrations.get(type)\n )\n}\n\n/**\n * The alert a staff notification of this type is, if any: the registry\n * entry that names it as its `notificationType`. How `notifyStaff` routes a\n * notification it was handed into the alert pipeline.\n */\nexport function operatorAlertForNotificationType(\n notificationType: string,\n): OperatorAlertDefinition | undefined {\n return listOperatorAlerts().find(\n (entry) => entry.notificationType === notificationType && entry.type === notificationType,\n )\n}\n\n/** Test seam: forget every plugin's registration. */\nexport function resetOperatorAlertsForTests(): void {\n registrations.clear()\n}\n"],"names":["CORE_OPERATOR_ALERTS","registrations","Map","CORE_TYPES","Set","map","entry","type","registerOperatorAlerts","definitions","options","pluginId","String","trim","Error","definition","startsWith","length","has","set","listOperatorAlerts","contributed","values","sort","a","b","getOperatorAlert","find","get","operatorAlertForNotificationType","notificationType","resetOperatorAlertsForTests","clear"],"mappings":";AAAA;;;;;;;;;;;;;;;CAeC,GAED,SACEA,oBAAoB,QAEf,kCAA8B;AAErC;;;;;;;;;;;;;CAaC,GAED,MAAMC,gBAAgB,IAAIC;AAE1B,MAAMC,aAAa,IAAIC,IAAIJ,qBAAqBK,GAAG,CAAC,CAACC,QAAUA,MAAMC,IAAI;AAEzE;;;;CAIC,GACD,OAAO,SAASC,uBACdC,WAA+C,EAC/CC,OAA6B;;IAE7B,MAAMC,WAAWC,eAAOF,2BAAAA,QAASC,QAAQ,mBAAI,IAAIE,IAAI;IACrD,IAAI,CAACF,UAAU,MAAM,IAAIG,MAAM;IAC/B,KAAK,MAAMC,cAAcN,YAAa;;QACpC,MAAMF,OAAOK,gBAAOG,8BAAAA,WAAYR,IAAI,oBAAI,IAAIM,IAAI;QAChD,IAAI,CAACN,KAAKS,UAAU,CAAC,GAAGL,SAAS,CAAC,CAAC,KAAKJ,KAAKU,MAAM,IAAIN,SAASM,MAAM,GAAG,GAAG;YAC1E,MAAM,IAAIH,MACR,CAAC,gBAAgB,EAAEP,KAAK,iBAAiB,EAAEI,SAAS,wCAAwC,CAAC;QAEjG;QACA,IAAIR,WAAWe,GAAG,CAACX,OAAO;YACxB,MAAM,IAAIO,MAAM,CAAC,gBAAgB,EAAEP,KAAK,WAAW,CAAC;QACtD;QACAN,cAAckB,GAAG,CAACZ,MAAM,aAAKQ;YAAYR;YAAMI;;IACjD;AACF;AAEA,gEAAgE,GAChE,OAAO,SAASS;IACd,MAAMC,cAAc;WAAIpB,cAAcqB,MAAM;KAAG,CAACC,IAAI,CAAC,CAACC,GAAGC,IACvDD,EAAEjB,IAAI,GAAGkB,EAAElB,IAAI,GAAG,CAAC,IAAIiB,EAAEjB,IAAI,GAAGkB,EAAElB,IAAI,GAAG,IAAI;IAE/C,OAAO;WAAIP;WAAyBqB;KAAY;AAClD;AAEA,qDAAqD,GACrD,OAAO,SAASK,iBACdnB,IAAY;QAGVP;IADF,QACEA,6BAAAA,qBAAqB2B,IAAI,CAAC,CAACrB,QAAUA,MAAMC,IAAI,KAAKA,iBAApDP,6BACAC,cAAc2B,GAAG,CAACrB;AAEtB;AAEA;;;;CAIC,GACD,OAAO,SAASsB,iCACdC,gBAAwB;IAExB,OAAOV,qBAAqBO,IAAI,CAC9B,CAACrB,QAAUA,MAAMwB,gBAAgB,KAAKA,oBAAoBxB,MAAMC,IAAI,KAAKuB;AAE7E;AAEA,mDAAmD,GACnD,OAAO,SAASC;IACd9B,cAAc+B,KAAK;AACrB"}
1
+ {"version":3,"sources":["../../../../../../libs/aglyn/src/lib/plugin-manager/operator-alerts.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 CORE_OPERATOR_ALERTS,\n type OperatorAlertDefinition,\n} from '../app-utils/operator-alerts'\n\n/**\n * Operator alerts a PLUGIN contributes (AGL-3377), beside core's own\n * `CORE_OPERATOR_ALERTS`.\n *\n * A plugin declares its alert types here, from its server declarations, in\n * the same shape core uses, and raises them through `raiseOperatorAlert`.\n * Core lists no plugin's types, so a plugin that is not installed adds\n * nothing to Staff → Operator alerts and a new plugin needs no core change.\n *\n * A plugin's types must start with its own id and a dot, which is what keeps\n * two plugins, or a plugin and core, from claiming one settings row.\n * Registering the same type again replaces it in place, so a declarations\n * module evaluated twice registers once.\n */\n\n/**\n * On `globalThis` under `Symbol.for`, never at module scope (AGL-3947).\n * The alert types are registered at boot, from `instrumentation.ts`, and Next\n * compiles that file apart from the routes, so the boot and a route each hold\n * their own copy of this module. At module scope, the boot filled its copy and\n * every route read an empty one (AGL-3412, AGL-3868).\n */\nconst REGISTRY_KEY = Symbol.for('@aglyn/aglyn:operator-alerts')\nconst globalScope = globalThis as typeof globalThis & {\n [REGISTRY_KEY]?: Map<string, OperatorAlertDefinition>\n}\nconst registrations: Map<string, OperatorAlertDefinition> =\n globalScope[REGISTRY_KEY] ?? (globalScope[REGISTRY_KEY] = new Map())\n\nconst CORE_TYPES = new Set(CORE_OPERATOR_ALERTS.map((entry) => entry.type))\n\n/**\n * Registers a plugin's alert types. Throws on a type outside the plugin's\n * namespace or one core already owns: both are a programming error the\n * plugin's own tests should meet, not something to discover in production.\n */\nexport function registerOperatorAlerts(\n definitions: readonly OperatorAlertDefinition[],\n options: { pluginId: string },\n): void {\n const pluginId = String(options?.pluginId ?? '').trim()\n if (!pluginId) throw new Error('operator alerts need a pluginId')\n for (const definition of definitions) {\n const type = String(definition?.type ?? '').trim()\n if (!type.startsWith(`${pluginId}.`) || type.length <= pluginId.length + 1) {\n throw new Error(\n `operator alert \"${type}\" must be named \"${pluginId}.<name>\" by the plugin that registers it`,\n )\n }\n if (CORE_TYPES.has(type)) {\n throw new Error(`operator alert \"${type}\" is core's`)\n }\n registrations.set(type, { ...definition, type, pluginId })\n }\n}\n\n/** Every alert type: core's, then each plugin's in type order. */\nexport function listOperatorAlerts(): OperatorAlertDefinition[] {\n const contributed = [...registrations.values()].sort((a, b) =>\n a.type < b.type ? -1 : a.type > b.type ? 1 : 0,\n )\n return [...CORE_OPERATOR_ALERTS, ...contributed]\n}\n\n/** One alert type, core's or a registered plugin's. */\nexport function getOperatorAlert(\n type: string,\n): OperatorAlertDefinition | undefined {\n return (\n CORE_OPERATOR_ALERTS.find((entry) => entry.type === type) ??\n registrations.get(type)\n )\n}\n\n/**\n * The alert a staff notification of this type is, if any: the registry\n * entry that names it as its `notificationType`. How `notifyStaff` routes a\n * notification it was handed into the alert pipeline.\n */\nexport function operatorAlertForNotificationType(\n notificationType: string,\n): OperatorAlertDefinition | undefined {\n return listOperatorAlerts().find(\n (entry) => entry.notificationType === notificationType && entry.type === notificationType,\n )\n}\n\n/** Test seam: forget every plugin's registration. */\nexport function resetOperatorAlertsForTests(): void {\n registrations.clear()\n}\n"],"names":["globalScope","CORE_OPERATOR_ALERTS","REGISTRY_KEY","Symbol","for","globalThis","registrations","Map","CORE_TYPES","Set","map","entry","type","registerOperatorAlerts","definitions","options","pluginId","String","trim","Error","definition","startsWith","length","has","set","listOperatorAlerts","contributed","values","sort","a","b","getOperatorAlert","find","get","operatorAlertForNotificationType","notificationType","resetOperatorAlertsForTests","clear"],"mappings":";IAiDEA;AAjDF;;;;;;;;;;;;;;;CAeC,GAED,SACEC,oBAAoB,QAEf,kCAA8B;AAErC;;;;;;;;;;;;;CAaC,GAED;;;;;;CAMC,GACD,MAAMC,eAAeC,OAAOC,GAAG,CAAC;AAChC,MAAMJ,cAAcK;AAGpB,MAAMC,iBACJN,4BAAAA,WAAW,CAACE,aAAa,YAAzBF,4BAA8BA,WAAW,CAACE,aAAa,GAAG,IAAIK;AAEhE,MAAMC,aAAa,IAAIC,IAAIR,qBAAqBS,GAAG,CAAC,CAACC,QAAUA,MAAMC,IAAI;AAEzE;;;;CAIC,GACD,OAAO,SAASC,uBACdC,WAA+C,EAC/CC,OAA6B;;IAE7B,MAAMC,WAAWC,eAAOF,2BAAAA,QAASC,QAAQ,mBAAI,IAAIE,IAAI;IACrD,IAAI,CAACF,UAAU,MAAM,IAAIG,MAAM;IAC/B,KAAK,MAAMC,cAAcN,YAAa;;QACpC,MAAMF,OAAOK,gBAAOG,8BAAAA,WAAYR,IAAI,oBAAI,IAAIM,IAAI;QAChD,IAAI,CAACN,KAAKS,UAAU,CAAC,GAAGL,SAAS,CAAC,CAAC,KAAKJ,KAAKU,MAAM,IAAIN,SAASM,MAAM,GAAG,GAAG;YAC1E,MAAM,IAAIH,MACR,CAAC,gBAAgB,EAAEP,KAAK,iBAAiB,EAAEI,SAAS,wCAAwC,CAAC;QAEjG;QACA,IAAIR,WAAWe,GAAG,CAACX,OAAO;YACxB,MAAM,IAAIO,MAAM,CAAC,gBAAgB,EAAEP,KAAK,WAAW,CAAC;QACtD;QACAN,cAAckB,GAAG,CAACZ,MAAM,aAAKQ;YAAYR;YAAMI;;IACjD;AACF;AAEA,gEAAgE,GAChE,OAAO,SAASS;IACd,MAAMC,cAAc;WAAIpB,cAAcqB,MAAM;KAAG,CAACC,IAAI,CAAC,CAACC,GAAGC,IACvDD,EAAEjB,IAAI,GAAGkB,EAAElB,IAAI,GAAG,CAAC,IAAIiB,EAAEjB,IAAI,GAAGkB,EAAElB,IAAI,GAAG,IAAI;IAE/C,OAAO;WAAIX;WAAyByB;KAAY;AAClD;AAEA,qDAAqD,GACrD,OAAO,SAASK,iBACdnB,IAAY;QAGVX;IADF,QACEA,6BAAAA,qBAAqB+B,IAAI,CAAC,CAACrB,QAAUA,MAAMC,IAAI,KAAKA,iBAApDX,6BACAK,cAAc2B,GAAG,CAACrB;AAEtB;AAEA;;;;CAIC,GACD,OAAO,SAASsB,iCACdC,gBAAwB;IAExB,OAAOV,qBAAqBO,IAAI,CAC9B,CAACrB,QAAUA,MAAMwB,gBAAgB,KAAKA,oBAAoBxB,MAAMC,IAAI,KAAKuB;AAE7E;AAEA,mDAAmD,GACnD,OAAO,SAASC;IACd9B,cAAc+B,KAAK;AACrB"}
@@ -31,7 +31,16 @@
31
31
  * there. Values are coerced defensively on merge — the settings doc is
32
32
  * client-writable (org managers), so readers never trust its types.
33
33
  */ import { _ as _extends } from "@swc/helpers/_/_extends";
34
- const schemas = new Map();
34
+ var _globalScope_REGISTRY_KEY;
35
+ /**
36
+ * On `globalThis` under `Symbol.for`, never at module scope (AGL-3947).
37
+ * The config schemas are registered at boot, from `instrumentation.ts`, and Next
38
+ * compiles that file apart from the routes, so the boot and a route each hold
39
+ * their own copy of this module. At module scope, the boot filled its copy and
40
+ * every route read an empty one (AGL-3412, AGL-3868).
41
+ */ const REGISTRY_KEY = Symbol.for('@aglyn/aglyn:plugin-config-schemas');
42
+ const globalScope = globalThis;
43
+ const schemas = (_globalScope_REGISTRY_KEY = globalScope[REGISTRY_KEY]) != null ? _globalScope_REGISTRY_KEY : globalScope[REGISTRY_KEY] = new Map();
35
44
  /** Idempotent by pluginId — re-registration replaces the schema. */ export function registerPluginConfigSchema(schema) {
36
45
  schemas.set(schema.pluginId, schema);
37
46
  }
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../../../../libs/aglyn/src/lib/plugin-manager/plugin-config.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\n/**\n * Per-plugin configuration (AGL-428) — Strapi `config` parity. A plugin\n * DECLARES its settings schema (fields + defaults + validation) at module\n * scope; the org stores overrides in `orgs/{orgId}/pluginSettings/\n * {pluginId}`; every consumer reads the DEFAULTS-MERGED view:\n *\n * - server handlers via `getPluginConfig(orgId, pluginId)`\n * (tenant-data-admin);\n * - client surfaces via `usePluginConfig(orgId, pluginId)`\n * (tenant-feature-instance);\n * - the console's Plugins & add-ons hub renders a generic settings form\n * from the schema, so a plugin gets a settings UI without writing one.\n *\n * Register at MODULE SCOPE in a file imported by both the client barrel\n * and the `/server` entry, so whichever surface loads first the schema is\n * there. Values are coerced defensively on merge — the settings doc is\n * client-writable (org managers), so readers never trust its types.\n */\n\nexport interface PluginConfigField {\n key: string\n label: string\n type: 'string' | 'number' | 'boolean' | 'select'\n description?: string\n /** For `select` fields. */\n options?: Array<{ value: string; label: string }>\n /** For `number` fields. */\n min?: number\n max?: number\n}\n\nexport interface PluginConfigSchema {\n pluginId: string\n fields: PluginConfigField[]\n defaults: Record<string, unknown>\n /**\n * What an operator must read before filling the form in (AGL-3700) — a\n * limit the settings cannot express, such as what a translation does NOT\n * do for search. Shown above the fields at both scopes.\n */\n notice?: string\n /**\n * The settings change what a published page serves (AGL-3700). A site-scope\n * save then drops that site's cached pages, so the change is live at once\n * rather than within the page cache's window.\n */\n affectsPublishedPages?: boolean\n /**\n * The settings card's own docs heading (AGL-3700): a docs help topic key and\n * one of that page's anchors. Each card's `?` names its own section, so a\n * plugin that documents its settings points here; without it the card links\n * the generic plugin-settings section. Names only — the console resolves\n * them against its registry, so no docs prose rides a site bundle.\n */\n help?: { topic: string; anchor: `#${string}` }\n /** Cross-field validation; returns an error message or null. */\n validate?: (values: Record<string, unknown>) => string | null\n}\n\nconst schemas = new Map<string, PluginConfigSchema>()\n\n/** Idempotent by pluginId — re-registration replaces the schema. */\nexport function registerPluginConfigSchema(schema: PluginConfigSchema): void {\n schemas.set(schema.pluginId, schema)\n}\n\nexport function getPluginConfigSchema(\n pluginId: string,\n): PluginConfigSchema | undefined {\n return schemas.get(pluginId)\n}\n\nexport function listPluginConfigSchemas(): PluginConfigSchema[] {\n return [...schemas.values()]\n}\n\nconst coerce = (\n field: PluginConfigField,\n raw: unknown,\n fallback: unknown,\n): unknown => {\n switch (field.type) {\n case 'boolean':\n return typeof raw === 'boolean' ? raw : fallback\n case 'number': {\n const value = typeof raw === 'number' && Number.isFinite(raw) ? raw : NaN\n if (Number.isNaN(value)) return fallback\n if (field.min != null && value < field.min) return field.min\n if (field.max != null && value > field.max) return field.max\n return value\n }\n case 'select': {\n const value = typeof raw === 'string' ? raw : undefined\n return field.options?.some((option) => option.value === value)\n ? value\n : fallback\n }\n default:\n return typeof raw === 'string' ? raw : fallback\n }\n}\n\n/**\n * The defaults-merged, type-coerced config view. Safe against a missing\n * or junk settings doc — unknown keys are dropped, wrong-typed values\n * fall back to the declared default.\n */\nexport function mergePluginConfig(\n schema: PluginConfigSchema,\n stored: Record<string, unknown> | null | undefined,\n): Record<string, unknown> {\n const merged: Record<string, unknown> = { ...schema.defaults }\n if (!stored) return merged\n for (const field of schema.fields) {\n if (field.key in stored) {\n merged[field.key] = coerce(\n field,\n stored[field.key],\n schema.defaults[field.key],\n )\n }\n }\n return merged\n}\n\n/** Pre-save validation for the settings UI (coerce + custom validate). */\nexport function validatePluginConfigValues(\n schema: PluginConfigSchema,\n values: Record<string, unknown>,\n): { ok: boolean; error?: string } {\n const merged = mergePluginConfig(schema, values)\n const error = schema.validate?.(merged) ?? null\n return error ? { ok: false, error } : { ok: true }\n}\n\nconst MANIFEST_CONFIG_FIELD_LIMIT = 50\nconst FIELD_TYPES: ReadonlySet<string> = new Set([\n 'string',\n 'number',\n 'boolean',\n 'select',\n])\n\n/**\n * A plugin's declared fields, read from its MANIFEST rather than the registry\n * (AGL-428).\n *\n * The registry is a module-scope map, filled by `registerPluginConfigSchema`\n * at import time. A marketplace plugin cannot fill it: its bundle runs in a\n * sandboxed frame on a separate origin and never executes in the console\n * process, so there is no moment at which its registration could happen. The\n * settings form therefore rendered NOTHING for every plugin a workspace\n * installed, and `resolvePluginConfig` — which is entirely plugin-agnostic and\n * would have layered a third-party plugin's org default under a per-site\n * override without changing a line — was never reached for one.\n *\n * The manifest is denormalized onto every install pin, so it is the one\n * declaration of a marketplace plugin the console already holds.\n *\n * Everything here is re-validated rather than trusted. The manifest is\n * publisher-authored, it is stored on a client-readable document, and a form\n * built from it is a form an org manager saves through — so a junk `type`\n * drops the field rather than rendering an unknown control, and the field\n * count is capped so one manifest cannot turn a settings page into thousands\n * of inputs. Being reviewed once is not the same as being safe to render\n * verbatim forever.\n *\n * Returns `undefined` when the manifest declares nothing usable, which callers\n * must treat as \"this plugin has no settings\" rather than as an error: most\n * plugins take none.\n */\nexport function pluginConfigSchemaFromManifest(\n pluginId: string,\n manifest: unknown,\n): PluginConfigSchema | undefined {\n if (!pluginId) return undefined\n const declared = (manifest as { config?: unknown } | null | undefined)?.config\n const rawFields = (declared as { fields?: unknown } | undefined)?.fields\n if (!Array.isArray(rawFields)) return undefined\n const seen = new Set<string>()\n const fields: PluginConfigField[] = []\n for (const raw of rawFields) {\n if (fields.length >= MANIFEST_CONFIG_FIELD_LIMIT) break\n const entry = raw as Record<string, unknown> | null\n const key = typeof entry?.['key'] === 'string' ? entry['key'].trim() : ''\n // A duplicate key would give two controls one storage slot, so the second\n // is dropped rather than allowed to shadow the first.\n if (!key || seen.has(key)) continue\n const type = String(entry?.['type'] ?? 'string')\n if (!FIELD_TYPES.has(type)) continue\n const options = Array.isArray(entry?.['options'])\n ? (entry['options'] as unknown[])\n .map((option) => option as Record<string, unknown> | null)\n .filter(\n (option) =>\n typeof option?.['value'] === 'string' &&\n typeof option?.['label'] === 'string',\n )\n .map((option) => ({\n value: String(option?.['value']),\n label: String(option?.['label']),\n }))\n : undefined\n // A `select` with nothing to select is a dropdown that cannot be answered,\n // so it is dropped rather than rendered empty.\n if (type === 'select' && !options?.length) continue\n seen.add(key)\n fields.push({\n key,\n label:\n typeof entry?.['label'] === 'string' && entry['label'].trim()\n ? String(entry['label']).slice(0, 120)\n : key,\n type: type as PluginConfigField['type'],\n description:\n typeof entry?.['description'] === 'string'\n ? String(entry['description']).slice(0, 400)\n : undefined,\n options,\n min: typeof entry?.['min'] === 'number' ? entry['min'] : undefined,\n max: typeof entry?.['max'] === 'number' ? entry['max'] : undefined,\n })\n }\n if (!fields.length) return undefined\n const rawDefaults = (declared as { defaults?: unknown } | undefined)?.defaults\n const defaults: Record<string, unknown> = {}\n for (const field of fields) {\n // Defaults are coerced through the same path stored values take, so a\n // manifest default of the wrong type cannot become the value a site\n // inherits. An absent one falls back to the type's empty value rather than\n // `undefined`, which `pluginConfigOverrides` reads as \"not answered\".\n const supplied =\n rawDefaults && typeof rawDefaults === 'object'\n ? (rawDefaults as Record<string, unknown>)[field.key]\n : undefined\n const empty =\n field.type === 'boolean'\n ? false\n : field.type === 'number'\n ? (field.min ?? 0)\n : field.type === 'select'\n ? (field.options?.[0]?.value ?? '')\n : ''\n defaults[field.key] = coerce(field, supplied, empty)\n }\n return { pluginId, fields, defaults }\n}\n\n/**\n * The three levels a plugin setting can be answered at.\n *\n * A workspace sets a value once and every site it enabled the plugin on\n * follows it; a site that needs a different answer overrides that one field\n * and keeps inheriting the rest. Bookings is the shape this is for — one\n * booking horizon across a chain, with the flagship branch taking bookings\n * further out — and it generalizes to every plugin because none of them get\n * to invent their own inheritance.\n */\nexport interface PluginConfigSources {\n /** `orgs/{orgId}/pluginSettings/{pluginId}` — the workspace's answer. */\n org?: Record<string, unknown> | null\n /** `hosts/{hostId}/pluginSettings/{pluginId}` — only the keys this site overrides. */\n host?: Record<string, unknown> | null\n}\n\n/**\n * Which keys a site is answering for itself.\n *\n * A key is an override when it is PRESENT on the host document, and inherited\n * when it is absent. `undefined` counts as absent, deliberately: a UI that\n * writes `{key: undefined}` to mean \"stop overriding\" is doing the same thing\n * as one that never wrote the key, and treating the two differently would make\n * \"revert to the workspace value\" depend on which code path cleared it.\n *\n * ⚠️ Which is also why clearing an override must DELETE the key rather than\n * write an empty value. `setDoc(..., {merge: true})` leaves an existing field\n * exactly as it is when the new object omits it, so a form that drops empty\n * inputs cannot clear anything by saving — the override survives, invisibly,\n * and the site keeps ignoring a workspace value the operator has since\n * changed. Deleting is the only write that returns a field to inherited.\n *\n * Keys the schema does not declare are ignored, so a stale field left behind\n * by a plugin update does not read as an override of a setting that no longer\n * exists.\n */\nexport function pluginConfigOverrides(\n schema: PluginConfigSchema,\n host: Record<string, unknown> | null | undefined,\n): string[] {\n if (!host) return []\n return schema.fields\n .filter((field) => field.key in host && host[field.key] !== undefined)\n .map((field) => field.key)\n}\n\n/**\n * What a plugin actually runs with on one site.\n *\n * Schema defaults, then the workspace's stored values, then only the keys the\n * site overrides — each layer narrowing the one before it. Type coercion\n * happens once at the end rather than per level, so a site override of the\n * wrong type falls back to the WORKSPACE value it was trying to replace\n * instead of skipping past it to the schema default. Getting that backwards\n * would have one malformed site setting silently discard a workspace answer\n * the operator can see in their own console.\n */\nexport function resolvePluginConfig(\n schema: PluginConfigSchema,\n sources: PluginConfigSources,\n): Record<string, unknown> {\n const org = mergePluginConfig(schema, sources.org)\n const overrides = pluginConfigOverrides(schema, sources.host)\n if (!overrides.length) return org\n const host = sources.host as Record<string, unknown>\n const merged: Record<string, unknown> = { ...org }\n for (const field of schema.fields) {\n if (!overrides.includes(field.key)) continue\n merged[field.key] = coerce(field, host[field.key], org[field.key])\n }\n return merged\n}\n"],"names":["schemas","Map","registerPluginConfigSchema","schema","set","pluginId","getPluginConfigSchema","get","listPluginConfigSchemas","values","coerce","field","raw","fallback","type","value","Number","isFinite","NaN","isNaN","min","max","undefined","options","some","option","mergePluginConfig","stored","merged","defaults","fields","key","validatePluginConfigValues","error","validate","ok","MANIFEST_CONFIG_FIELD_LIMIT","FIELD_TYPES","Set","pluginConfigSchemaFromManifest","manifest","declared","config","rawFields","Array","isArray","seen","length","entry","trim","has","String","map","filter","label","add","push","slice","description","rawDefaults","supplied","empty","pluginConfigOverrides","host","resolvePluginConfig","sources","org","overrides","includes"],"mappings":"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"}
1
+ {"version":3,"sources":["../../../../../../libs/aglyn/src/lib/plugin-manager/plugin-config.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\n/**\n * Per-plugin configuration (AGL-428) — Strapi `config` parity. A plugin\n * DECLARES its settings schema (fields + defaults + validation) at module\n * scope; the org stores overrides in `orgs/{orgId}/pluginSettings/\n * {pluginId}`; every consumer reads the DEFAULTS-MERGED view:\n *\n * - server handlers via `getPluginConfig(orgId, pluginId)`\n * (tenant-data-admin);\n * - client surfaces via `usePluginConfig(orgId, pluginId)`\n * (tenant-feature-instance);\n * - the console's Plugins & add-ons hub renders a generic settings form\n * from the schema, so a plugin gets a settings UI without writing one.\n *\n * Register at MODULE SCOPE in a file imported by both the client barrel\n * and the `/server` entry, so whichever surface loads first the schema is\n * there. Values are coerced defensively on merge — the settings doc is\n * client-writable (org managers), so readers never trust its types.\n */\n\nexport interface PluginConfigField {\n key: string\n label: string\n type: 'string' | 'number' | 'boolean' | 'select'\n description?: string\n /** For `select` fields. */\n options?: Array<{ value: string; label: string }>\n /** For `number` fields. */\n min?: number\n max?: number\n}\n\nexport interface PluginConfigSchema {\n pluginId: string\n fields: PluginConfigField[]\n defaults: Record<string, unknown>\n /**\n * What an operator must read before filling the form in (AGL-3700) — a\n * limit the settings cannot express, such as what a translation does NOT\n * do for search. Shown above the fields at both scopes.\n */\n notice?: string\n /**\n * The settings change what a published page serves (AGL-3700). A site-scope\n * save then drops that site's cached pages, so the change is live at once\n * rather than within the page cache's window.\n */\n affectsPublishedPages?: boolean\n /**\n * The settings card's own docs heading (AGL-3700): a docs help topic key and\n * one of that page's anchors. Each card's `?` names its own section, so a\n * plugin that documents its settings points here; without it the card links\n * the generic plugin-settings section. Names only — the console resolves\n * them against its registry, so no docs prose rides a site bundle.\n */\n help?: { topic: string; anchor: `#${string}` }\n /** Cross-field validation; returns an error message or null. */\n validate?: (values: Record<string, unknown>) => string | null\n}\n\n/**\n * On `globalThis` under `Symbol.for`, never at module scope (AGL-3947).\n * The config schemas are registered at boot, from `instrumentation.ts`, and Next\n * compiles that file apart from the routes, so the boot and a route each hold\n * their own copy of this module. At module scope, the boot filled its copy and\n * every route read an empty one (AGL-3412, AGL-3868).\n */\nconst REGISTRY_KEY = Symbol.for('@aglyn/aglyn:plugin-config-schemas')\nconst globalScope = globalThis as typeof globalThis & {\n [REGISTRY_KEY]?: Map<string, PluginConfigSchema>\n}\nconst schemas: Map<string, PluginConfigSchema> =\n globalScope[REGISTRY_KEY] ?? (globalScope[REGISTRY_KEY] = new Map())\n\n/** Idempotent by pluginId — re-registration replaces the schema. */\nexport function registerPluginConfigSchema(schema: PluginConfigSchema): void {\n schemas.set(schema.pluginId, schema)\n}\n\nexport function getPluginConfigSchema(\n pluginId: string,\n): PluginConfigSchema | undefined {\n return schemas.get(pluginId)\n}\n\nexport function listPluginConfigSchemas(): PluginConfigSchema[] {\n return [...schemas.values()]\n}\n\nconst coerce = (\n field: PluginConfigField,\n raw: unknown,\n fallback: unknown,\n): unknown => {\n switch (field.type) {\n case 'boolean':\n return typeof raw === 'boolean' ? raw : fallback\n case 'number': {\n const value = typeof raw === 'number' && Number.isFinite(raw) ? raw : NaN\n if (Number.isNaN(value)) return fallback\n if (field.min != null && value < field.min) return field.min\n if (field.max != null && value > field.max) return field.max\n return value\n }\n case 'select': {\n const value = typeof raw === 'string' ? raw : undefined\n return field.options?.some((option) => option.value === value)\n ? value\n : fallback\n }\n default:\n return typeof raw === 'string' ? raw : fallback\n }\n}\n\n/**\n * The defaults-merged, type-coerced config view. Safe against a missing\n * or junk settings doc — unknown keys are dropped, wrong-typed values\n * fall back to the declared default.\n */\nexport function mergePluginConfig(\n schema: PluginConfigSchema,\n stored: Record<string, unknown> | null | undefined,\n): Record<string, unknown> {\n const merged: Record<string, unknown> = { ...schema.defaults }\n if (!stored) return merged\n for (const field of schema.fields) {\n if (field.key in stored) {\n merged[field.key] = coerce(\n field,\n stored[field.key],\n schema.defaults[field.key],\n )\n }\n }\n return merged\n}\n\n/** Pre-save validation for the settings UI (coerce + custom validate). */\nexport function validatePluginConfigValues(\n schema: PluginConfigSchema,\n values: Record<string, unknown>,\n): { ok: boolean; error?: string } {\n const merged = mergePluginConfig(schema, values)\n const error = schema.validate?.(merged) ?? null\n return error ? { ok: false, error } : { ok: true }\n}\n\nconst MANIFEST_CONFIG_FIELD_LIMIT = 50\nconst FIELD_TYPES: ReadonlySet<string> = new Set([\n 'string',\n 'number',\n 'boolean',\n 'select',\n])\n\n/**\n * A plugin's declared fields, read from its MANIFEST rather than the registry\n * (AGL-428).\n *\n * The registry is a module-scope map, filled by `registerPluginConfigSchema`\n * at import time. A marketplace plugin cannot fill it: its bundle runs in a\n * sandboxed frame on a separate origin and never executes in the console\n * process, so there is no moment at which its registration could happen. The\n * settings form therefore rendered NOTHING for every plugin a workspace\n * installed, and `resolvePluginConfig` — which is entirely plugin-agnostic and\n * would have layered a third-party plugin's org default under a per-site\n * override without changing a line — was never reached for one.\n *\n * The manifest is denormalized onto every install pin, so it is the one\n * declaration of a marketplace plugin the console already holds.\n *\n * Everything here is re-validated rather than trusted. The manifest is\n * publisher-authored, it is stored on a client-readable document, and a form\n * built from it is a form an org manager saves through — so a junk `type`\n * drops the field rather than rendering an unknown control, and the field\n * count is capped so one manifest cannot turn a settings page into thousands\n * of inputs. Being reviewed once is not the same as being safe to render\n * verbatim forever.\n *\n * Returns `undefined` when the manifest declares nothing usable, which callers\n * must treat as \"this plugin has no settings\" rather than as an error: most\n * plugins take none.\n */\nexport function pluginConfigSchemaFromManifest(\n pluginId: string,\n manifest: unknown,\n): PluginConfigSchema | undefined {\n if (!pluginId) return undefined\n const declared = (manifest as { config?: unknown } | null | undefined)?.config\n const rawFields = (declared as { fields?: unknown } | undefined)?.fields\n if (!Array.isArray(rawFields)) return undefined\n const seen = new Set<string>()\n const fields: PluginConfigField[] = []\n for (const raw of rawFields) {\n if (fields.length >= MANIFEST_CONFIG_FIELD_LIMIT) break\n const entry = raw as Record<string, unknown> | null\n const key = typeof entry?.['key'] === 'string' ? entry['key'].trim() : ''\n // A duplicate key would give two controls one storage slot, so the second\n // is dropped rather than allowed to shadow the first.\n if (!key || seen.has(key)) continue\n const type = String(entry?.['type'] ?? 'string')\n if (!FIELD_TYPES.has(type)) continue\n const options = Array.isArray(entry?.['options'])\n ? (entry['options'] as unknown[])\n .map((option) => option as Record<string, unknown> | null)\n .filter(\n (option) =>\n typeof option?.['value'] === 'string' &&\n typeof option?.['label'] === 'string',\n )\n .map((option) => ({\n value: String(option?.['value']),\n label: String(option?.['label']),\n }))\n : undefined\n // A `select` with nothing to select is a dropdown that cannot be answered,\n // so it is dropped rather than rendered empty.\n if (type === 'select' && !options?.length) continue\n seen.add(key)\n fields.push({\n key,\n label:\n typeof entry?.['label'] === 'string' && entry['label'].trim()\n ? String(entry['label']).slice(0, 120)\n : key,\n type: type as PluginConfigField['type'],\n description:\n typeof entry?.['description'] === 'string'\n ? String(entry['description']).slice(0, 400)\n : undefined,\n options,\n min: typeof entry?.['min'] === 'number' ? entry['min'] : undefined,\n max: typeof entry?.['max'] === 'number' ? entry['max'] : undefined,\n })\n }\n if (!fields.length) return undefined\n const rawDefaults = (declared as { defaults?: unknown } | undefined)?.defaults\n const defaults: Record<string, unknown> = {}\n for (const field of fields) {\n // Defaults are coerced through the same path stored values take, so a\n // manifest default of the wrong type cannot become the value a site\n // inherits. An absent one falls back to the type's empty value rather than\n // `undefined`, which `pluginConfigOverrides` reads as \"not answered\".\n const supplied =\n rawDefaults && typeof rawDefaults === 'object'\n ? (rawDefaults as Record<string, unknown>)[field.key]\n : undefined\n const empty =\n field.type === 'boolean'\n ? false\n : field.type === 'number'\n ? (field.min ?? 0)\n : field.type === 'select'\n ? (field.options?.[0]?.value ?? '')\n : ''\n defaults[field.key] = coerce(field, supplied, empty)\n }\n return { pluginId, fields, defaults }\n}\n\n/**\n * The three levels a plugin setting can be answered at.\n *\n * A workspace sets a value once and every site it enabled the plugin on\n * follows it; a site that needs a different answer overrides that one field\n * and keeps inheriting the rest. Bookings is the shape this is for — one\n * booking horizon across a chain, with the flagship branch taking bookings\n * further out — and it generalizes to every plugin because none of them get\n * to invent their own inheritance.\n */\nexport interface PluginConfigSources {\n /** `orgs/{orgId}/pluginSettings/{pluginId}` — the workspace's answer. */\n org?: Record<string, unknown> | null\n /** `hosts/{hostId}/pluginSettings/{pluginId}` — only the keys this site overrides. */\n host?: Record<string, unknown> | null\n}\n\n/**\n * Which keys a site is answering for itself.\n *\n * A key is an override when it is PRESENT on the host document, and inherited\n * when it is absent. `undefined` counts as absent, deliberately: a UI that\n * writes `{key: undefined}` to mean \"stop overriding\" is doing the same thing\n * as one that never wrote the key, and treating the two differently would make\n * \"revert to the workspace value\" depend on which code path cleared it.\n *\n * ⚠️ Which is also why clearing an override must DELETE the key rather than\n * write an empty value. `setDoc(..., {merge: true})` leaves an existing field\n * exactly as it is when the new object omits it, so a form that drops empty\n * inputs cannot clear anything by saving — the override survives, invisibly,\n * and the site keeps ignoring a workspace value the operator has since\n * changed. Deleting is the only write that returns a field to inherited.\n *\n * Keys the schema does not declare are ignored, so a stale field left behind\n * by a plugin update does not read as an override of a setting that no longer\n * exists.\n */\nexport function pluginConfigOverrides(\n schema: PluginConfigSchema,\n host: Record<string, unknown> | null | undefined,\n): string[] {\n if (!host) return []\n return schema.fields\n .filter((field) => field.key in host && host[field.key] !== undefined)\n .map((field) => field.key)\n}\n\n/**\n * What a plugin actually runs with on one site.\n *\n * Schema defaults, then the workspace's stored values, then only the keys the\n * site overrides — each layer narrowing the one before it. Type coercion\n * happens once at the end rather than per level, so a site override of the\n * wrong type falls back to the WORKSPACE value it was trying to replace\n * instead of skipping past it to the schema default. Getting that backwards\n * would have one malformed site setting silently discard a workspace answer\n * the operator can see in their own console.\n */\nexport function resolvePluginConfig(\n schema: PluginConfigSchema,\n sources: PluginConfigSources,\n): Record<string, unknown> {\n const org = mergePluginConfig(schema, sources.org)\n const overrides = pluginConfigOverrides(schema, sources.host)\n if (!overrides.length) return org\n const host = sources.host as Record<string, unknown>\n const merged: Record<string, unknown> = { ...org }\n for (const field of schema.fields) {\n if (!overrides.includes(field.key)) continue\n merged[field.key] = coerce(field, host[field.key], org[field.key])\n }\n return merged\n}\n"],"names":["globalScope","REGISTRY_KEY","Symbol","for","globalThis","schemas","Map","registerPluginConfigSchema","schema","set","pluginId","getPluginConfigSchema","get","listPluginConfigSchemas","values","coerce","field","raw","fallback","type","value","Number","isFinite","NaN","isNaN","min","max","undefined","options","some","option","mergePluginConfig","stored","merged","defaults","fields","key","validatePluginConfigValues","error","validate","ok","MANIFEST_CONFIG_FIELD_LIMIT","FIELD_TYPES","Set","pluginConfigSchemaFromManifest","manifest","declared","config","rawFields","Array","isArray","seen","length","entry","trim","has","String","map","filter","label","add","push","slice","description","rawDefaults","supplied","empty","pluginConfigOverrides","host","resolvePluginConfig","sources","org","overrides","includes"],"mappings":"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"}
@@ -54,8 +54,17 @@
54
54
  *
55
55
  * Registered from a plugin's declarations, like the erasure seams, so the
56
56
  * participant is in place in a process that never loaded the plugin's API.
57
- */ import { getRegisteringPluginId } from "../app-utils/registering-plugin.js";
58
- const registrations = [];
57
+ */ var _globalScope_REGISTRY_KEY;
58
+ import { getRegisteringPluginId } from "../app-utils/registering-plugin.js";
59
+ /**
60
+ * On `globalThis` under `Symbol.for`, never at module scope (AGL-3947).
61
+ * The participants are registered at boot, from `instrumentation.ts`, and Next
62
+ * compiles that file apart from the routes, so the boot and a route each hold
63
+ * their own copy of this module. At module scope, the boot filled its copy and
64
+ * every route read an empty one (AGL-3412, AGL-3868).
65
+ */ const REGISTRY_KEY = Symbol.for('@aglyn/aglyn:plugin-consent-group-participants');
66
+ const globalScope = globalThis;
67
+ const registrations = (_globalScope_REGISTRY_KEY = globalScope[REGISTRY_KEY]) != null ? _globalScope_REGISTRY_KEY : globalScope[REGISTRY_KEY] = [];
59
68
  /**
60
69
  * Registers a plugin's participant. Owner = the loader's marker inside a
61
70
  * register fn, else `options.pluginId`; a participant with neither throws.
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../../../../libs/aglyn/src/lib/plugin-manager/plugin-consent-group-change.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\n/**\n * A plugin's share of a CONSENT GROUP CHANGE (AGL-3320).\n *\n * The executor in `libs/tenant/data/admin` carries the refusals the platform\n * stores per site — the suppression list, the topic opt-outs, the pace — and\n * flips the declaration. A plugin that keeps its own per-site refusals, or\n * records keyed by a consent group's id, registers a participant here, and\n * every change runs it at three points:\n *\n * - `carry`, BEFORE the flip: copy the plugin's refusals onto the sites the\n * plan's carries name, so no site stops honoring an opt-out it was reading\n * across its group. Run twice — once, and again as the catch-up.\n * - `rehome`, AFTER the flip: move the records keyed by the plan's holder\n * flows. After, because every writer keys by the declaration in force, so\n * new writes land on the final key while the move folds the old one in.\n * - `sweep`, once the sweep delay has passed: the whole job again,\n * idempotently, for anything a long job wrote under the old declaration.\n *\n * ## NOT ISOLATED, on purpose\n *\n * The erasure seams log a throw and go on (`plugin-person-erasure.ts`), because\n * a partial erasure is better than none. Here the opposite holds: a skipped\n * carry is a refusal nobody honors once the flip happens. So a participant's\n * throw propagates, the executor records it against the job, and the change\n * does not flip until the participant succeeds — five failures in a row mark\n * the job stalled, and it keeps retrying.\n *\n * The PREVIEW is the exception, because it decides nothing: a participant\n * whose preview throws shows `null` lines and the review step says so.\n *\n * ## The contract a `run` keeps\n *\n * Resumable and idempotent. The executor hands back the `cursor` the last\n * call returned (`null` to start), and stops calling once `done` is true. A\n * run must return before `deadlineMs`, keep every write create-if-absent or a\n * restrictive merge, and write nothing on `dryRun`. A second full run over a\n * finished change must write nothing, because the catch-up and the sweep ARE\n * second runs.\n *\n * Registered from a plugin's declarations, like the erasure seams, so the\n * participant is in place in a process that never loaded the plugin's API.\n */\n\nimport type {\n ConsentGroupChangePlan,\n ConsentGroupChangePreviewLine,\n} from '../app-utils/consent-group-change'\nimport { getRegisteringPluginId } from '../app-utils/registering-plugin'\n\n/** Which point of the change a `run` is called for. */\nexport type ConsentGroupChangeParticipantPhase = 'carry' | 'rehome' | 'sweep'\n\nexport interface ConsentGroupChangePreviewRequest {\n orgId: string\n plan: ConsentGroupChangePlan\n}\n\nexport interface ConsentGroupChangeRunRequest {\n orgId: string\n changeId: string\n plan: ConsentGroupChangePlan\n phase: ConsentGroupChangeParticipantPhase\n /** Where the previous call stopped, or `null` to start the phase. */\n cursor: string | null\n /** Return by this instant; the executor calls again from the cursor. */\n deadlineMs: number\n /** Count, write nothing. */\n dryRun: boolean\n}\n\nexport interface ConsentGroupChangeRunResult {\n done: boolean\n cursor: string | null\n /** What THIS call did — the executor adds them up across calls. */\n counts: Record<string, number>\n}\n\nexport interface ConsentGroupChangeParticipant {\n preview(request: ConsentGroupChangePreviewRequest): Promise<ConsentGroupChangePreviewLine[]>\n run(request: ConsentGroupChangeRunRequest): Promise<ConsentGroupChangeRunResult>\n /**\n * The participant's clause of the \"Finished a consent group change\" line,\n * from its counts totaled over the whole change, or `null` to add nothing.\n */\n summarize?(counts: Readonly<Record<string, number>>): string | null\n}\n\ninterface Registration {\n pluginId: string\n participant: ConsentGroupChangeParticipant\n}\n\nconst registrations: Registration[] = []\n\n/**\n * Registers a plugin's participant. Owner = the loader's marker inside a\n * register fn, else `options.pluginId`; a participant with neither throws.\n * One per plugin: registering again replaces it in place, so a module\n * evaluated twice does not run twice.\n */\nexport function registerPluginConsentGroupParticipant(\n participant: ConsentGroupChangeParticipant,\n options?: { pluginId?: string },\n): void {\n const pluginId = (getRegisteringPluginId() ?? options?.pluginId ?? '').trim()\n if (!pluginId) {\n throw new Error(\n 'a consent group participant was registered with no owner: pass ' +\n '{ pluginId } when registering outside a plugin register fn',\n )\n }\n const index = registrations.findIndex((entry) => entry.pluginId === pluginId)\n if (index >= 0) registrations[index] = { pluginId, participant }\n else registrations.push({ pluginId, participant })\n}\n\n/** Every participant, in registration order — the order they run in. */\nexport function listPluginConsentGroupParticipants(): Array<{\n pluginId: string\n participant: ConsentGroupChangeParticipant\n}> {\n return registrations.map(({ pluginId, participant }) => ({ pluginId, participant }))\n}\n\n/**\n * Every participant's preview, by plugin id, in registration order: its\n * lines, or `null` for one that threw. Never throws.\n */\nexport async function previewPluginConsentGroupChange(\n request: ConsentGroupChangePreviewRequest,\n): Promise<Array<{ pluginId: string; lines: ConsentGroupChangePreviewLine[] | null }>> {\n const out: Array<{ pluginId: string; lines: ConsentGroupChangePreviewLine[] | null }> = []\n for (const { pluginId, participant } of [...registrations]) {\n try {\n out.push({ pluginId, lines: await participant.preview(request) })\n } catch (error) {\n out.push({ pluginId, lines: null })\n console.error(\n `[plugins] ${pluginId} failed to preview a consent group change in org ${request.orgId}`,\n error,\n )\n }\n }\n return out\n}\n\n/** Test seam: forget every participant. */\nexport function resetPluginConsentGroupParticipantsForTests(): void {\n registrations.length = 0\n}\n"],"names":["getRegisteringPluginId","registrations","registerPluginConsentGroupParticipant","participant","options","pluginId","trim","Error","index","findIndex","entry","push","listPluginConsentGroupParticipants","map","previewPluginConsentGroupChange","request","out","lines","preview","error","console","orgId","resetPluginConsentGroupParticipantsForTests","length"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAyCC,GAMD,SAASA,sBAAsB,QAAQ,qCAAiC;AA6CxE,MAAMC,gBAAgC,EAAE;AAExC;;;;;CAKC,GACD,OAAO,SAASC,sCACdC,WAA0C,EAC1CC,OAA+B;QAEbJ,MAAAA;IAAlB,MAAMK,WAAW,EAACL,QAAAA,0BAAAA,oCAAAA,0BAA4BI,2BAAAA,QAASC,QAAQ,YAA7CL,OAAiD,IAAIM,IAAI;IAC3E,IAAI,CAACD,UAAU;QACb,MAAM,IAAIE,MACR,oEACE;IAEN;IACA,MAAMC,QAAQP,cAAcQ,SAAS,CAAC,CAACC,QAAUA,MAAML,QAAQ,KAAKA;IACpE,IAAIG,SAAS,GAAGP,aAAa,CAACO,MAAM,GAAG;QAAEH;QAAUF;IAAY;SAC1DF,cAAcU,IAAI,CAAC;QAAEN;QAAUF;IAAY;AAClD;AAEA,sEAAsE,GACtE,OAAO,SAASS;IAId,OAAOX,cAAcY,GAAG,CAAC,CAAC,EAAER,QAAQ,EAAEF,WAAW,EAAE,GAAM,CAAA;YAAEE;YAAUF;QAAY,CAAA;AACnF;AAEA;;;CAGC,GACD,OAAO,eAAeW,gCACpBC,OAAyC;IAEzC,MAAMC,MAAkF,EAAE;IAC1F,KAAK,MAAM,EAAEX,QAAQ,EAAEF,WAAW,EAAE,IAAI;WAAIF;KAAc,CAAE;QAC1D,IAAI;YACFe,IAAIL,IAAI,CAAC;gBAAEN;gBAAUY,OAAO,MAAMd,YAAYe,OAAO,CAACH;YAAS;QACjE,EAAE,OAAOI,OAAO;YACdH,IAAIL,IAAI,CAAC;gBAAEN;gBAAUY,OAAO;YAAK;YACjCG,QAAQD,KAAK,CACX,CAAC,UAAU,EAAEd,SAAS,iDAAiD,EAAEU,QAAQM,KAAK,EAAE,EACxFF;QAEJ;IACF;IACA,OAAOH;AACT;AAEA,yCAAyC,GACzC,OAAO,SAASM;IACdrB,cAAcsB,MAAM,GAAG;AACzB"}
1
+ {"version":3,"sources":["../../../../../../libs/aglyn/src/lib/plugin-manager/plugin-consent-group-change.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\n/**\n * A plugin's share of a CONSENT GROUP CHANGE (AGL-3320).\n *\n * The executor in `libs/tenant/data/admin` carries the refusals the platform\n * stores per site — the suppression list, the topic opt-outs, the pace — and\n * flips the declaration. A plugin that keeps its own per-site refusals, or\n * records keyed by a consent group's id, registers a participant here, and\n * every change runs it at three points:\n *\n * - `carry`, BEFORE the flip: copy the plugin's refusals onto the sites the\n * plan's carries name, so no site stops honoring an opt-out it was reading\n * across its group. Run twice — once, and again as the catch-up.\n * - `rehome`, AFTER the flip: move the records keyed by the plan's holder\n * flows. After, because every writer keys by the declaration in force, so\n * new writes land on the final key while the move folds the old one in.\n * - `sweep`, once the sweep delay has passed: the whole job again,\n * idempotently, for anything a long job wrote under the old declaration.\n *\n * ## NOT ISOLATED, on purpose\n *\n * The erasure seams log a throw and go on (`plugin-person-erasure.ts`), because\n * a partial erasure is better than none. Here the opposite holds: a skipped\n * carry is a refusal nobody honors once the flip happens. So a participant's\n * throw propagates, the executor records it against the job, and the change\n * does not flip until the participant succeeds — five failures in a row mark\n * the job stalled, and it keeps retrying.\n *\n * The PREVIEW is the exception, because it decides nothing: a participant\n * whose preview throws shows `null` lines and the review step says so.\n *\n * ## The contract a `run` keeps\n *\n * Resumable and idempotent. The executor hands back the `cursor` the last\n * call returned (`null` to start), and stops calling once `done` is true. A\n * run must return before `deadlineMs`, keep every write create-if-absent or a\n * restrictive merge, and write nothing on `dryRun`. A second full run over a\n * finished change must write nothing, because the catch-up and the sweep ARE\n * second runs.\n *\n * Registered from a plugin's declarations, like the erasure seams, so the\n * participant is in place in a process that never loaded the plugin's API.\n */\n\nimport type {\n ConsentGroupChangePlan,\n ConsentGroupChangePreviewLine,\n} from '../app-utils/consent-group-change'\nimport { getRegisteringPluginId } from '../app-utils/registering-plugin'\n\n/** Which point of the change a `run` is called for. */\nexport type ConsentGroupChangeParticipantPhase = 'carry' | 'rehome' | 'sweep'\n\nexport interface ConsentGroupChangePreviewRequest {\n orgId: string\n plan: ConsentGroupChangePlan\n}\n\nexport interface ConsentGroupChangeRunRequest {\n orgId: string\n changeId: string\n plan: ConsentGroupChangePlan\n phase: ConsentGroupChangeParticipantPhase\n /** Where the previous call stopped, or `null` to start the phase. */\n cursor: string | null\n /** Return by this instant; the executor calls again from the cursor. */\n deadlineMs: number\n /** Count, write nothing. */\n dryRun: boolean\n}\n\nexport interface ConsentGroupChangeRunResult {\n done: boolean\n cursor: string | null\n /** What THIS call did — the executor adds them up across calls. */\n counts: Record<string, number>\n}\n\nexport interface ConsentGroupChangeParticipant {\n preview(request: ConsentGroupChangePreviewRequest): Promise<ConsentGroupChangePreviewLine[]>\n run(request: ConsentGroupChangeRunRequest): Promise<ConsentGroupChangeRunResult>\n /**\n * The participant's clause of the \"Finished a consent group change\" line,\n * from its counts totaled over the whole change, or `null` to add nothing.\n */\n summarize?(counts: Readonly<Record<string, number>>): string | null\n}\n\ninterface Registration {\n pluginId: string\n participant: ConsentGroupChangeParticipant\n}\n\n/**\n * On `globalThis` under `Symbol.for`, never at module scope (AGL-3947).\n * The participants are registered at boot, from `instrumentation.ts`, and Next\n * compiles that file apart from the routes, so the boot and a route each hold\n * their own copy of this module. At module scope, the boot filled its copy and\n * every route read an empty one (AGL-3412, AGL-3868).\n */\nconst REGISTRY_KEY = Symbol.for('@aglyn/aglyn:plugin-consent-group-participants')\nconst globalScope = globalThis as typeof globalThis & {\n [REGISTRY_KEY]?: Registration[]\n}\nconst registrations: Registration[] =\n globalScope[REGISTRY_KEY] ?? (globalScope[REGISTRY_KEY] = [])\n\n/**\n * Registers a plugin's participant. Owner = the loader's marker inside a\n * register fn, else `options.pluginId`; a participant with neither throws.\n * One per plugin: registering again replaces it in place, so a module\n * evaluated twice does not run twice.\n */\nexport function registerPluginConsentGroupParticipant(\n participant: ConsentGroupChangeParticipant,\n options?: { pluginId?: string },\n): void {\n const pluginId = (getRegisteringPluginId() ?? options?.pluginId ?? '').trim()\n if (!pluginId) {\n throw new Error(\n 'a consent group participant was registered with no owner: pass ' +\n '{ pluginId } when registering outside a plugin register fn',\n )\n }\n const index = registrations.findIndex((entry) => entry.pluginId === pluginId)\n if (index >= 0) registrations[index] = { pluginId, participant }\n else registrations.push({ pluginId, participant })\n}\n\n/** Every participant, in registration order — the order they run in. */\nexport function listPluginConsentGroupParticipants(): Array<{\n pluginId: string\n participant: ConsentGroupChangeParticipant\n}> {\n return registrations.map(({ pluginId, participant }) => ({ pluginId, participant }))\n}\n\n/**\n * Every participant's preview, by plugin id, in registration order: its\n * lines, or `null` for one that threw. Never throws.\n */\nexport async function previewPluginConsentGroupChange(\n request: ConsentGroupChangePreviewRequest,\n): Promise<Array<{ pluginId: string; lines: ConsentGroupChangePreviewLine[] | null }>> {\n const out: Array<{ pluginId: string; lines: ConsentGroupChangePreviewLine[] | null }> = []\n for (const { pluginId, participant } of [...registrations]) {\n try {\n out.push({ pluginId, lines: await participant.preview(request) })\n } catch (error) {\n out.push({ pluginId, lines: null })\n console.error(\n `[plugins] ${pluginId} failed to preview a consent group change in org ${request.orgId}`,\n error,\n )\n }\n }\n return out\n}\n\n/** Test seam: forget every participant. */\nexport function resetPluginConsentGroupParticipantsForTests(): void {\n registrations.length = 0\n}\n"],"names":["globalScope","getRegisteringPluginId","REGISTRY_KEY","Symbol","for","globalThis","registrations","registerPluginConsentGroupParticipant","participant","options","pluginId","trim","Error","index","findIndex","entry","push","listPluginConsentGroupParticipants","map","previewPluginConsentGroupChange","request","out","lines","preview","error","console","orgId","resetPluginConsentGroupParticipantsForTests","length"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAyCC,OA+DCA;AAzDF,SAASC,sBAAsB,QAAQ,qCAAiC;AA6CxE;;;;;;CAMC,GACD,MAAMC,eAAeC,OAAOC,GAAG,CAAC;AAChC,MAAMJ,cAAcK;AAGpB,MAAMC,iBACJN,4BAAAA,WAAW,CAACE,aAAa,YAAzBF,4BAA8BA,WAAW,CAACE,aAAa,GAAG,EAAE;AAE9D;;;;;CAKC,GACD,OAAO,SAASK,sCACdC,WAA0C,EAC1CC,OAA+B;QAEbR,MAAAA;IAAlB,MAAMS,WAAW,EAACT,QAAAA,0BAAAA,oCAAAA,0BAA4BQ,2BAAAA,QAASC,QAAQ,YAA7CT,OAAiD,IAAIU,IAAI;IAC3E,IAAI,CAACD,UAAU;QACb,MAAM,IAAIE,MACR,oEACE;IAEN;IACA,MAAMC,QAAQP,cAAcQ,SAAS,CAAC,CAACC,QAAUA,MAAML,QAAQ,KAAKA;IACpE,IAAIG,SAAS,GAAGP,aAAa,CAACO,MAAM,GAAG;QAAEH;QAAUF;IAAY;SAC1DF,cAAcU,IAAI,CAAC;QAAEN;QAAUF;IAAY;AAClD;AAEA,sEAAsE,GACtE,OAAO,SAASS;IAId,OAAOX,cAAcY,GAAG,CAAC,CAAC,EAAER,QAAQ,EAAEF,WAAW,EAAE,GAAM,CAAA;YAAEE;YAAUF;QAAY,CAAA;AACnF;AAEA;;;CAGC,GACD,OAAO,eAAeW,gCACpBC,OAAyC;IAEzC,MAAMC,MAAkF,EAAE;IAC1F,KAAK,MAAM,EAAEX,QAAQ,EAAEF,WAAW,EAAE,IAAI;WAAIF;KAAc,CAAE;QAC1D,IAAI;YACFe,IAAIL,IAAI,CAAC;gBAAEN;gBAAUY,OAAO,MAAMd,YAAYe,OAAO,CAACH;YAAS;QACjE,EAAE,OAAOI,OAAO;YACdH,IAAIL,IAAI,CAAC;gBAAEN;gBAAUY,OAAO;YAAK;YACjCG,QAAQD,KAAK,CACX,CAAC,UAAU,EAAEd,SAAS,iDAAiD,EAAEU,QAAQM,KAAK,EAAE,EACxFF;QAEJ;IACF;IACA,OAAOH;AACT;AAEA,yCAAyC,GACzC,OAAO,SAASM;IACdrB,cAAcsB,MAAM,GAAG;AACzB"}
@@ -1,3 +1,4 @@
1
+ var _globalScope_REGISTRY_KEY;
1
2
  /**
2
3
  * @license
3
4
  * Copyright 2026 Aglyn LLC
@@ -66,7 +67,15 @@ import { getRegisteringPluginId } from "../app-utils/registering-plugin.js";
66
67
  * fifteen-minute sweep (`CONSOLE_FAST_CRON_SCHEDULE` in `cloud/functions`).
67
68
  */ export const PLUGIN_CONSOLE_CRON_SCHEDULE = '*/15 * * * *';
68
69
  /** How late a declared job may be before its row goes red: three missed ticks. */ export const PLUGIN_CONSOLE_CRON_GRACE_MINUTES = 45;
69
- const registrations = [];
70
+ /**
71
+ * On `globalThis` under `Symbol.for`, never at module scope (AGL-3947).
72
+ * The jobs are registered at boot, from `instrumentation.ts`, and Next
73
+ * compiles that file apart from the routes, so the boot and a route each hold
74
+ * their own copy of this module. At module scope, the boot filled its copy and
75
+ * every route read an empty one (AGL-3412, AGL-3868).
76
+ */ const REGISTRY_KEY = Symbol.for('@aglyn/aglyn:plugin-console-crons');
77
+ const globalScope = globalThis;
78
+ const registrations = (_globalScope_REGISTRY_KEY = globalScope[REGISTRY_KEY]) != null ? _globalScope_REGISTRY_KEY : globalScope[REGISTRY_KEY] = [];
70
79
  /** A job id as a beat document and a health row can carry it. */ const JOB_ID = /^[a-z][a-z0-9-]*$/;
71
80
  /** Job ids the platform's own inventory holds. */ const PLATFORM_JOB_IDS = new Set([
72
81
  ...SCHEDULED_JOBS.map((job)=>job.id),