@aglyn/aglyn 1.0.0-beta.290 → 1.0.0-beta.292

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.
@@ -52,6 +52,8 @@ import { ANIMATION_MAX_DELAY_MS, ANIMATION_MAX_DURATION_MS, ANIMATION_MAX_STAGGE
52
52
  'muiRating',
53
53
  'muiDatePicker',
54
54
  'muiTimePicker',
55
+ 'formFileUpload',
56
+ 'product-options-field',
55
57
  'newsletter-signup',
56
58
  'member-signin',
57
59
  'member-signup',
@@ -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. 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 'quote-page',\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
+ {"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 'formFileUpload',\n 'product-options-field',\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 'quote-page',\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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"}
@@ -31,6 +31,7 @@
31
31
  */
32
32
  import type { AglynNodeSchema, NodeId } from '../foundation/definitions/components.types';
33
33
  import type { PlacementKind } from './compose-reusable-components';
34
+ import { type VisitorUploadKind } from './visitor-upload';
34
35
  /**
35
36
  * The `componentId` of the node that RENDERS a form, and of the node that
36
37
  * renders one of its fields.
@@ -103,8 +104,13 @@ export declare function formPropsOnBind(previousProps: Record<string, unknown> |
103
104
  * constant.
104
105
  */
105
106
  export declare const FORMS_MAX_PER_HOST = 1000;
106
- /** Field types a `FormField` node offers; the form declares the same set. */
107
- export type FormFieldType = 'text' | 'email' | 'textarea' | 'select' | 'radio' | 'checkbox' | 'rating' | 'date' | 'time';
107
+ /**
108
+ * Field types a form declares: what a `FormField` node offers, MUI's form
109
+ * elements, and the answer elements other plugins draw inside a form
110
+ * ({@link FORM_PLUGIN_ANSWER_ELEMENTS}): `file`, files the person attaches,
111
+ * and `product`, a product and its options picked from the site's catalog.
112
+ */
113
+ export type FormFieldType = 'text' | 'email' | 'textarea' | 'select' | 'radio' | 'checkbox' | 'rating' | 'date' | 'time' | 'file' | 'product';
108
114
  /**
109
115
  * What a declared field MEANS, where the meaning is one the platform reads.
110
116
  *
@@ -126,6 +132,14 @@ export interface FormFieldDecl {
126
132
  /** Stable dataset model fieldId this value is stored under (AGL-556). */
127
133
  datasetFieldId?: string;
128
134
  role?: FormFieldRole;
135
+ /**
136
+ * For a `file` field: the kinds of file it takes (every kind when absent)
137
+ * and how many, at most {@link VISITOR_UPLOAD_MAX_FILES}. Read off the
138
+ * File upload element, so the upload route holds a visitor to what the
139
+ * author set (AGL-4098).
140
+ */
141
+ fileKinds?: VisitorUploadKind[];
142
+ maxFiles?: number;
129
143
  /**
130
144
  * The custom contact field this value is saved under (AGL-2601) — a
131
145
  * `ContactFieldDefinition.key` in the org that owns the site.
@@ -174,6 +188,21 @@ export interface FormDocument<N = AglynNodeSchema> {
174
188
  * records the opt-in and makes the person at least a subscriber.
175
189
  */
176
190
  optInOnSubmit?: boolean;
191
+ /**
192
+ * What the form asks for, when it is a request a business answers with
193
+ * something of its own (AGL-4098): `{ kind: 'quote' }` for a "Request a
194
+ * quote" form. Its submissions carry the same `request`, the Inbox marks
195
+ * them, and a plugin that answers that kind of request (a quote) offers to
196
+ * start one from the submission. Set on the form's page, never inferred.
197
+ */
198
+ request?: FormRequest | null;
199
+ /**
200
+ * Whether the person who sends the form is emailed a confirmation of what
201
+ * they sent (AGL-4098). Absent takes the default for the form's kind
202
+ * ({@link formSendsConfirmation}): on for a request, off for every other
203
+ * form, so no form that existed before this setting starts emailing.
204
+ */
205
+ confirmationEmail?: boolean;
177
206
  routing?: FormRouting;
178
207
  legacyMatch?: FormLegacyMatch;
179
208
  stats?: FormStats;
@@ -227,6 +256,24 @@ export interface FormDocument<N = AglynNodeSchema> {
227
256
  */
228
257
  versionId?: string;
229
258
  }
259
+ /** What a form asks for (AGL-4098); the one kind today is a quote. */
260
+ export type FormRequestKind = 'quote';
261
+ export interface FormRequest {
262
+ kind: FormRequestKind;
263
+ }
264
+ export declare const FORM_REQUEST_KIND_LABELS: Readonly<Record<FormRequestKind, string>>;
265
+ /** The form's request, read off a stored document, or null for an ordinary form. */
266
+ export declare function readFormRequest(form: {
267
+ request?: unknown;
268
+ } | null | undefined): FormRequest | null;
269
+ /**
270
+ * Whether a form emails the person who sends it a confirmation: what the
271
+ * form says, else on for a request and off for every other form.
272
+ */
273
+ export declare function formSendsConfirmation(form: {
274
+ confirmationEmail?: unknown;
275
+ request?: unknown;
276
+ } | null | undefined): boolean;
230
277
  /**
231
278
  * Whether a form has been retired — see {@link FormDocument.archivedAt}.
232
279
  *
@@ -635,6 +682,35 @@ export declare const FORM_INPUT_ELEMENT_IDS: {
635
682
  readonly formLabel: "muiFormLabel";
636
683
  readonly choiceLabel: "muiFormControlLabel";
637
684
  };
685
+ /**
686
+ * Answer elements OTHER plugins draw inside a form, by id, with the field type
687
+ * each declares (AGL-4098). Restated here for the reason MUI's are above: the
688
+ * foundation layer reads a form's fields off its nodes and may import no
689
+ * plugin. Each carries its own `name`, `label` and `required` props, and posts
690
+ * plain strings under its name like any input:
691
+ *
692
+ * - `formFileUpload` (forms): files the person attaches, uploaded before the
693
+ * form is sent; it posts their names, and the form sends the files by id.
694
+ * Its `kinds` and `maxFiles` are declared with it.
695
+ * - `product-options-field` (commerce): a product from the site's catalog and
696
+ * its options; it posts a readable line under its name and a machine value
697
+ * beside it ({@link FORM_FIELD_DATA_SUFFIX}).
698
+ *
699
+ * Persisted ids; never rename. Deliberately apart from
700
+ * {@link FORM_INPUT_ELEMENT_IDS}, which is MUI's set and what a generated
701
+ * form is built from.
702
+ */
703
+ export declare const FORM_PLUGIN_ANSWER_ELEMENTS: Readonly<Record<string, FormFieldType>>;
704
+ /**
705
+ * The suffix of a field that carries a machine value for the field it is
706
+ * named after (AGL-4098): `product__data` beside `product`. The form posts it
707
+ * like any field; the submit route keeps it apart from the answers a person
708
+ * reads (`fieldData`), so the Inbox, the notification email and an export
709
+ * show the readable line, and the plugin that wrote the value reads it back.
710
+ */
711
+ export declare const FORM_FIELD_DATA_SUFFIX = "__data";
712
+ /** The field a machine value belongs to, or null when the key is an ordinary answer. */
713
+ export declare function formFieldDataOwner(key: string): string | null;
638
714
  /** Whether a node carries an answer the form submits. */
639
715
  export declare function isFormInputNode(node: AglynNodeSchema | undefined | null, formFieldComponentId?: string): boolean;
640
716
  /**
@@ -678,6 +754,15 @@ export declare function collectFormFieldNodeIds(nodes: Record<NodeId, AglynNodeS
678
754
  * one key, so the second node contributes no separate value.
679
755
  */
680
756
  export declare function formFieldDeclsFromNodes(nodes: Record<NodeId, AglynNodeSchema | undefined> | undefined | null, formNodeId: NodeId, formFieldComponentId?: string): FormFieldDecl[];
757
+ /**
758
+ * What a File upload element takes, as its declaration carries it: the kinds
759
+ * the author picked (every kind when none) and at most how many files, from 1
760
+ * to {@link VISITOR_UPLOAD_MAX_FILES} (that many when unset).
761
+ */
762
+ export declare function formFileFieldLimits(props: Record<string, unknown>): {
763
+ fileKinds: VisitorUploadKind[];
764
+ maxFiles: number;
765
+ };
681
766
  /** One `form` node found by the discovery scan, with where it was found. */
682
767
  export interface DiscoveredFormNode {
683
768
  /** `screen` / `layout` / `component` — what kind of document holds it. */
@@ -31,6 +31,7 @@
31
31
  import { containerMembershipValue } from "./container-membership.js";
32
32
  import { utcMonthKey } from "./utc-month.js";
33
33
  import { displayNameSearchFields, nameSearchTokens } from "./name-search.js";
34
+ import { readVisitorUploadKinds, VISITOR_UPLOAD_MAX_FILES } from "./visitor-upload.js";
34
35
  /**
35
36
  * The `componentId` of the node that RENDERS a form, and of the node that
36
37
  * renders one of its fields.
@@ -115,6 +116,22 @@ export const FORM_FIELD_COMPONENT_ID = 'formField';
115
116
  * together. A catalog past even this needs real pagination, not a larger
116
117
  * constant.
117
118
  */ export const FORMS_MAX_PER_HOST = 1000;
119
+ export const FORM_REQUEST_KIND_LABELS = {
120
+ quote: 'Quote request'
121
+ };
122
+ /** The form's request, read off a stored document, or null for an ordinary form. */ export function readFormRequest(form) {
123
+ const request = form == null ? void 0 : form.request;
124
+ return (request == null ? void 0 : request.kind) === 'quote' ? {
125
+ kind: 'quote'
126
+ } : null;
127
+ }
128
+ /**
129
+ * Whether a form emails the person who sends it a confirmation: what the
130
+ * form says, else on for a request and off for every other form.
131
+ */ export function formSendsConfirmation(form) {
132
+ if (typeof (form == null ? void 0 : form.confirmationEmail) === 'boolean') return form.confirmationEmail;
133
+ return readFormRequest(form) !== null;
134
+ }
118
135
  /**
119
136
  * Whether a form has been retired — see {@link FormDocument.archivedAt}.
120
137
  *
@@ -537,7 +554,9 @@ const FORM_FIELD_TYPES = new Set([
537
554
  'checkbox',
538
555
  'rating',
539
556
  'date',
540
- 'time'
557
+ 'time',
558
+ 'file',
559
+ 'product'
541
560
  ]);
542
561
  /**
543
562
  * Splits a `FormField` node's newline- or comma-separated choice list.
@@ -573,6 +592,39 @@ const FORM_FIELD_TYPES = new Set([
573
592
  formLabel: 'muiFormLabel',
574
593
  choiceLabel: 'muiFormControlLabel'
575
594
  };
595
+ /**
596
+ * Answer elements OTHER plugins draw inside a form, by id, with the field type
597
+ * each declares (AGL-4098). Restated here for the reason MUI's are above: the
598
+ * foundation layer reads a form's fields off its nodes and may import no
599
+ * plugin. Each carries its own `name`, `label` and `required` props, and posts
600
+ * plain strings under its name like any input:
601
+ *
602
+ * - `formFileUpload` (forms): files the person attaches, uploaded before the
603
+ * form is sent; it posts their names, and the form sends the files by id.
604
+ * Its `kinds` and `maxFiles` are declared with it.
605
+ * - `product-options-field` (commerce): a product from the site's catalog and
606
+ * its options; it posts a readable line under its name and a machine value
607
+ * beside it ({@link FORM_FIELD_DATA_SUFFIX}).
608
+ *
609
+ * Persisted ids; never rename. Deliberately apart from
610
+ * {@link FORM_INPUT_ELEMENT_IDS}, which is MUI's set and what a generated
611
+ * form is built from.
612
+ */ export const FORM_PLUGIN_ANSWER_ELEMENTS = {
613
+ formFileUpload: 'file',
614
+ 'product-options-field': 'product'
615
+ };
616
+ /**
617
+ * The suffix of a field that carries a machine value for the field it is
618
+ * named after (AGL-4098): `product__data` beside `product`. The form posts it
619
+ * like any field; the submit route keeps it apart from the answers a person
620
+ * reads (`fieldData`), so the Inbox, the notification email and an export
621
+ * show the readable line, and the plugin that wrote the value reads it back.
622
+ */ export const FORM_FIELD_DATA_SUFFIX = '__data';
623
+ /** The field a machine value belongs to, or null when the key is an ordinary answer. */ export function formFieldDataOwner(key) {
624
+ if (!key.endsWith(FORM_FIELD_DATA_SUFFIX)) return null;
625
+ const owner = key.slice(0, -FORM_FIELD_DATA_SUFFIX.length);
626
+ return owner && !owner.startsWith('__') ? owner : null;
627
+ }
576
628
  const ANSWER_ELEMENT_IDS = new Set([
577
629
  FORM_INPUT_ELEMENT_IDS.input,
578
630
  FORM_INPUT_ELEMENT_IDS.select,
@@ -580,8 +632,13 @@ const ANSWER_ELEMENT_IDS = new Set([
580
632
  FORM_INPUT_ELEMENT_IDS.checkbox,
581
633
  FORM_INPUT_ELEMENT_IDS.rating,
582
634
  FORM_INPUT_ELEMENT_IDS.datePicker,
583
- FORM_INPUT_ELEMENT_IDS.timePicker
635
+ FORM_INPUT_ELEMENT_IDS.timePicker,
636
+ ...Object.keys(FORM_PLUGIN_ANSWER_ELEMENTS)
584
637
  ]);
638
+ /** Whether a node is one of the answer elements another plugin draws. */ function isPluginAnswerElement(node) {
639
+ var _ref;
640
+ return Object.prototype.hasOwnProperty.call(FORM_PLUGIN_ANSWER_ELEMENTS, String((_ref = node == null ? void 0 : node.componentId) != null ? _ref : ''));
641
+ }
585
642
  /** Whether a node carries an answer the form submits. */ export function isFormInputNode(node, formFieldComponentId = FORM_FIELD_COMPONENT_ID) {
586
643
  var _ref;
587
644
  const componentId = String((_ref = node == null ? void 0 : node.componentId) != null ? _ref : '');
@@ -617,6 +674,7 @@ const ANSWER_ELEMENT_IDS = new Set([
617
674
  default:
618
675
  {
619
676
  var _props_fieldType;
677
+ if (isPluginAnswerElement(node)) return FORM_PLUGIN_ANSWER_ELEMENTS[String(node == null ? void 0 : node.componentId)];
620
678
  const raw = String((_props_fieldType = props['fieldType']) != null ? _props_fieldType : 'text');
621
679
  return FORM_FIELD_TYPES.has(raw) ? raw : 'text';
622
680
  }
@@ -632,9 +690,13 @@ const ANSWER_ELEMENT_IDS = new Set([
632
690
  * Control around it (the question, for a group of boxes), else the Choice
633
691
  * Label around a lone checkbox, else its accessible label or placeholder.
634
692
  */ function answerElementLabel(nodes, id) {
635
- var _ref, _ref1, _props_ariaLabel;
693
+ var _ref, _props_label, _ref1, _props_ariaLabel;
636
694
  const node = nodes[id];
637
695
  const props = (_ref = node == null ? void 0 : node.props) != null ? _ref : {};
696
+ // An element another plugin draws carries its own words.
697
+ if (isPluginAnswerElement(node) && String((_props_label = props['label']) != null ? _props_label : '').trim()) {
698
+ return String(props['label']).trim();
699
+ }
638
700
  // A Choice Label names its own box; a Form Label in the Form Control
639
701
  // around a GROUP of boxes names the question, and wins.
640
702
  let choiceWords = '';
@@ -824,9 +886,10 @@ const ANSWER_ELEMENT_IDS = new Set([
824
886
  }
825
887
  const label = isFormField ? String((_props_label = props['label']) != null ? _props_label : '').trim() : answerElementLabel(nodes, id);
826
888
  const datasetFieldId = String((_props_datasetFieldId = props['datasetFieldId']) != null ? _props_datasetFieldId : '').trim();
889
+ const fieldType = formInputType(node);
827
890
  const declaration = _extends({
828
891
  fieldName,
829
- fieldType: formInputType(node)
892
+ fieldType
830
893
  }, label ? {
831
894
  label
832
895
  } : {}, (isFormField ? props['required'] === true : answerElementRequired(nodes, id)) ? {
@@ -835,12 +898,23 @@ const ANSWER_ELEMENT_IDS = new Set([
835
898
  options
836
899
  } : {}, datasetFieldId ? {
837
900
  datasetFieldId
838
- } : {});
901
+ } : {}, fieldType === 'file' && !isFormField ? formFileFieldLimits(props) : {});
839
902
  byName.set(fieldName, declaration);
840
903
  declarations.push(declaration);
841
904
  }
842
905
  return declarations;
843
906
  }
907
+ /**
908
+ * What a File upload element takes, as its declaration carries it: the kinds
909
+ * the author picked (every kind when none) and at most how many files, from 1
910
+ * to {@link VISITOR_UPLOAD_MAX_FILES} (that many when unset).
911
+ */ export function formFileFieldLimits(props) {
912
+ const max = Math.floor(Number(props['maxFiles']));
913
+ return {
914
+ fileKinds: readVisitorUploadKinds(props['kinds']),
915
+ maxFiles: Number.isFinite(max) && max >= 1 ? Math.min(max, VISITOR_UPLOAD_MAX_FILES) : VISITOR_UPLOAD_MAX_FILES
916
+ };
917
+ }
844
918
  /**
845
919
  * Finds every `form` node in one document's node map.
846
920
  *