@aglyn/plugins-ai 1.0.0-beta.236 → 1.0.0-beta.238

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 (164) hide show
  1. package/package.json +17 -17
  2. package/src/lib/components/ai-credits-prompt.component.d.ts +44 -0
  3. package/src/lib/components/ai-credits-prompt.component.js +117 -0
  4. package/src/lib/components/ai-credits-prompt.component.js.map +1 -0
  5. package/src/lib/components/ai-job-cancel.component.d.ts +38 -0
  6. package/src/lib/components/ai-job-cancel.component.js +156 -0
  7. package/src/lib/components/ai-job-cancel.component.js.map +1 -0
  8. package/src/lib/components/ai-job-plan.component.d.ts +5 -4
  9. package/src/lib/components/ai-job-plan.component.js +48 -12
  10. package/src/lib/components/ai-job-plan.component.js.map +1 -1
  11. package/src/lib/components/ai-job-requests.d.ts +10 -0
  12. package/src/lib/components/ai-job-requests.js +12 -4
  13. package/src/lib/components/ai-job-requests.js.map +1 -1
  14. package/src/lib/components/ai-jobs-list-page.component.js +141 -72
  15. package/src/lib/components/ai-jobs-list-page.component.js.map +1 -1
  16. package/src/lib/components/ai-site-build-page.component.js +48 -9
  17. package/src/lib/components/ai-site-build-page.component.js.map +1 -1
  18. package/src/lib/components/ai-site-start-card.component.js +68 -22
  19. package/src/lib/components/ai-site-start-card.component.js.map +1 -1
  20. package/src/lib/components/ai-usage-strip.component.d.ts +19 -2
  21. package/src/lib/components/ai-usage-strip.component.js +40 -9
  22. package/src/lib/components/ai-usage-strip.component.js.map +1 -1
  23. package/src/lib/components/assist-build-card.component.d.ts +3 -1
  24. package/src/lib/components/assist-build-card.component.js +58 -7
  25. package/src/lib/components/assist-build-card.component.js.map +1 -1
  26. package/src/lib/components/assist-jobs-drawer.component.js +66 -22
  27. package/src/lib/components/assist-jobs-drawer.component.js.map +1 -1
  28. package/src/lib/components/assist-panel.component.js +89 -52
  29. package/src/lib/components/assist-panel.component.js.map +1 -1
  30. package/src/lib/jobs/ai-job-admission.d.ts +22 -2
  31. package/src/lib/jobs/ai-job-admission.js.map +1 -1
  32. package/src/lib/jobs/ai-job-build-step.d.ts +24 -11
  33. package/src/lib/jobs/ai-job-build-step.js +54 -35
  34. package/src/lib/jobs/ai-job-build-step.js.map +1 -1
  35. package/src/lib/jobs/ai-job-cancel.d.ts +71 -0
  36. package/src/lib/jobs/ai-job-cancel.js +102 -0
  37. package/src/lib/jobs/ai-job-cancel.js.map +1 -0
  38. package/src/lib/jobs/ai-job-component-step.js +4 -1
  39. package/src/lib/jobs/ai-job-component-step.js.map +1 -1
  40. package/src/lib/jobs/ai-job-drafts.js +2 -0
  41. package/src/lib/jobs/ai-job-drafts.js.map +1 -1
  42. package/src/lib/jobs/ai-job-form-step.js +4 -1
  43. package/src/lib/jobs/ai-job-form-step.js.map +1 -1
  44. package/src/lib/jobs/ai-job-layout-language.d.ts +2 -0
  45. package/src/lib/jobs/ai-job-layout-language.js +13 -1
  46. package/src/lib/jobs/ai-job-layout-language.js.map +1 -1
  47. package/src/lib/jobs/ai-job-layout-step.js +6 -1
  48. package/src/lib/jobs/ai-job-layout-step.js.map +1 -1
  49. package/src/lib/jobs/ai-job-page-language.js +12 -0
  50. package/src/lib/jobs/ai-job-page-language.js.map +1 -1
  51. package/src/lib/jobs/ai-job-page-step.d.ts +13 -0
  52. package/src/lib/jobs/ai-job-page-step.js +46 -4
  53. package/src/lib/jobs/ai-job-page-step.js.map +1 -1
  54. package/src/lib/jobs/ai-job-plan-step.d.ts +42 -0
  55. package/src/lib/jobs/ai-job-plan-step.js +118 -18
  56. package/src/lib/jobs/ai-job-plan-step.js.map +1 -1
  57. package/src/lib/jobs/ai-job-site-content.d.ts +49 -3
  58. package/src/lib/jobs/ai-job-site-content.js +196 -37
  59. package/src/lib/jobs/ai-job-site-content.js.map +1 -1
  60. package/src/lib/jobs/ai-job-site-step.js +49 -16
  61. package/src/lib/jobs/ai-job-site-step.js.map +1 -1
  62. package/src/lib/jobs/ai-job-template-step.js +4 -1
  63. package/src/lib/jobs/ai-job-template-step.js.map +1 -1
  64. package/src/lib/jobs/ai-job-text-step.d.ts +8 -0
  65. package/src/lib/jobs/ai-job-text-step.js.map +1 -1
  66. package/src/lib/jobs/ai-jobs.d.ts +55 -13
  67. package/src/lib/jobs/ai-jobs.js +213 -41
  68. package/src/lib/jobs/ai-jobs.js.map +1 -1
  69. package/src/lib/jobs/ai-layout-stock-photos.d.ts +194 -34
  70. package/src/lib/jobs/ai-layout-stock-photos.js +861 -146
  71. package/src/lib/jobs/ai-layout-stock-photos.js.map +1 -1
  72. package/src/lib/jobs/fixtures/ai-ceramics-portfolio-site-plan.d.ts +30 -0
  73. package/src/lib/jobs/fixtures/ai-ceramics-portfolio-site-plan.js +220 -0
  74. package/src/lib/jobs/fixtures/ai-ceramics-portfolio-site-plan.js.map +1 -0
  75. package/src/lib/jobs/fixtures/ai-stock-photo-pixabay-recordings.d.ts +27 -0
  76. package/src/lib/jobs/fixtures/ai-stock-photo-pixabay-recordings.js +151 -0
  77. package/src/lib/jobs/fixtures/ai-stock-photo-pixabay-recordings.js.map +1 -0
  78. package/src/lib/layout-language/ai-layout-compiler.d.ts +33 -0
  79. package/src/lib/layout-language/ai-layout-compiler.js +381 -18
  80. package/src/lib/layout-language/ai-layout-compiler.js.map +1 -1
  81. package/src/lib/layout-language/ai-layout-design.d.ts +6 -3
  82. package/src/lib/layout-language/ai-layout-design.js +5 -3
  83. package/src/lib/layout-language/ai-layout-design.js.map +1 -1
  84. package/src/lib/layout-language/ai-layout-frame.d.ts +12 -0
  85. package/src/lib/layout-language/ai-layout-frame.js +58 -3
  86. package/src/lib/layout-language/ai-layout-frame.js.map +1 -1
  87. package/src/lib/layout-language/ai-layout-gaps.js +3 -1
  88. package/src/lib/layout-language/ai-layout-gaps.js.map +1 -1
  89. package/src/lib/layout-language/ai-layout-language.js +1 -1
  90. package/src/lib/layout-language/ai-layout-language.js.map +1 -1
  91. package/src/lib/layout-language/ai-layout-listings.d.ts +49 -1
  92. package/src/lib/layout-language/ai-layout-listings.js +117 -47
  93. package/src/lib/layout-language/ai-layout-listings.js.map +1 -1
  94. package/src/lib/layout-language/ai-layout-pictures.d.ts +10 -1
  95. package/src/lib/layout-language/ai-layout-pictures.js +5 -2
  96. package/src/lib/layout-language/ai-layout-pictures.js.map +1 -1
  97. package/src/lib/layout-language/ai-layout-storefront.d.ts +82 -0
  98. package/src/lib/layout-language/ai-layout-storefront.js +223 -0
  99. package/src/lib/layout-language/ai-layout-storefront.js.map +1 -0
  100. package/src/lib/model/ai-build-job.d.ts +39 -0
  101. package/src/lib/model/ai-build-job.js +100 -1
  102. package/src/lib/model/ai-build-job.js.map +1 -1
  103. package/src/lib/model/ai-build-progress.d.ts +8 -0
  104. package/src/lib/model/ai-build-progress.js +35 -0
  105. package/src/lib/model/ai-build-progress.js.map +1 -1
  106. package/src/lib/model/ai-credit-estimate.d.ts +249 -0
  107. package/src/lib/model/ai-credit-estimate.js +278 -0
  108. package/src/lib/model/ai-credit-estimate.js.map +1 -0
  109. package/src/lib/model/ai-job-cancel-copy.d.ts +44 -0
  110. package/src/lib/model/ai-job-cancel-copy.js +53 -0
  111. package/src/lib/model/ai-job-cancel-copy.js.map +1 -0
  112. package/src/lib/model/ai-job-failure-copy.d.ts +4 -1
  113. package/src/lib/model/ai-job-failure-copy.js +8 -3
  114. package/src/lib/model/ai-job-failure-copy.js.map +1 -1
  115. package/src/lib/model/ai-jobs.types.d.ts +35 -0
  116. package/src/lib/model/ai-jobs.types.js.map +1 -1
  117. package/src/lib/model/ai-site-build-progress.js +7 -4
  118. package/src/lib/model/ai-site-build-progress.js.map +1 -1
  119. package/src/lib/model/ai-site-job.d.ts +76 -22
  120. package/src/lib/model/ai-site-job.js +125 -23
  121. package/src/lib/model/ai-site-job.js.map +1 -1
  122. package/src/lib/model/ai-site-kinds.d.ts +4 -1
  123. package/src/lib/model/ai-site-kinds.js +35 -7
  124. package/src/lib/model/ai-site-kinds.js.map +1 -1
  125. package/src/lib/runtime/ai-doctrine-validators.d.ts +37 -1
  126. package/src/lib/runtime/ai-doctrine-validators.js +161 -7
  127. package/src/lib/runtime/ai-doctrine-validators.js.map +1 -1
  128. package/src/lib/runtime/ai-doctrine.js +4 -3
  129. package/src/lib/runtime/ai-doctrine.js.map +1 -1
  130. package/src/lib/runtime/ai-node-tree.d.ts +11 -0
  131. package/src/lib/runtime/ai-node-tree.js +26 -0
  132. package/src/lib/runtime/ai-node-tree.js.map +1 -1
  133. package/src/lib/runtime/ai-palette.generated.js +592 -14
  134. package/src/lib/runtime/ai-palette.generated.js.map +1 -1
  135. package/src/lib/server/ai-jobs-cancel.d.ts +13 -5
  136. package/src/lib/server/ai-jobs-cancel.js +27 -9
  137. package/src/lib/server/ai-jobs-cancel.js.map +1 -1
  138. package/src/lib/server/ai-jobs-gate.d.ts +12 -2
  139. package/src/lib/server/ai-jobs-gate.js +20 -9
  140. package/src/lib/server/ai-jobs-gate.js.map +1 -1
  141. package/src/lib/server/ai-jobs-resume.js +93 -35
  142. package/src/lib/server/ai-jobs-resume.js.map +1 -1
  143. package/src/lib/server/ai-jobs-route.js +45 -4
  144. package/src/lib/server/ai-jobs-route.js.map +1 -1
  145. package/src/lib/server/assist-chat.js +4 -1
  146. package/src/lib/server/assist-chat.js.map +1 -1
  147. package/src/lib/server/assist-edit.js +3 -1
  148. package/src/lib/server/assist-edit.js.map +1 -1
  149. package/src/lib/server/assist-music.d.ts +36 -0
  150. package/src/lib/server/assist-music.js +36 -0
  151. package/src/lib/server/assist-music.js.map +1 -0
  152. package/src/lib/server.js +6 -0
  153. package/src/lib/server.js.map +1 -1
  154. package/src/lib/usage/ai-usage-meter.d.ts +2 -1
  155. package/src/lib/usage/ai-usage-meter.js +17 -1
  156. package/src/lib/usage/ai-usage-meter.js.map +1 -1
  157. package/src/lib/usage/ai-usage-wire.d.ts +6 -0
  158. package/src/lib/usage/ai-usage-wire.js.map +1 -1
  159. package/src/lib/usage/assist-usage.d.ts +7 -0
  160. package/src/lib/usage/assist-usage.js +14 -4
  161. package/src/lib/usage/assist-usage.js.map +1 -1
  162. package/src/lib/usage/free-ai-credits-left.d.ts +7 -0
  163. package/src/lib/usage/free-ai-credits-left.js +4 -2
  164. package/src/lib/usage/free-ai-credits-left.js.map +1 -1
@@ -53,6 +53,14 @@ export interface AiLayoutPictureSlot {
53
53
  /** The plan index of the section it sits in, or -1. */
54
54
  sectionIndex: number;
55
55
  role: AiLayoutPictureRole;
56
+ /**
57
+ * A product's photo (AGL-3676): what it is of, most specific first — its
58
+ * name, then what its photo should show — searched for with the shop's
59
+ * category and held to it, so a wick trimmer is never a laptop on a desk.
60
+ */
61
+ product?: {
62
+ subjects: readonly string[];
63
+ };
56
64
  }
57
65
  /** A photo a slot is filled with. */
58
66
  export interface AiLayoutPicturePhoto {
@@ -117,7 +125,8 @@ export declare function aiLayoutSeedNumber(seed: string): number;
117
125
  * five starters, and the owner's photo went to a stoneware card before the
118
126
  * About section that described the artist asked for it. Only a page with more
119
127
  * slots than starters repeats one: the photo placed longest ago, never the
120
- * one in a slot beside it.
128
+ * one in a slot beside it. The owner's portrait goes only to a picture of
129
+ * people; a gallery picture never takes it, even to avoid a repeat.
121
130
  */
122
131
  export declare function aiLayoutStarterPhotos(slots: readonly AiLayoutPictureSlot[], seed: string): AiLayoutPicturePhoto[];
123
132
  /**
@@ -158,7 +158,8 @@ function rotate(list, by) {
158
158
  * five starters, and the owner's photo went to a stoneware card before the
159
159
  * About section that described the artist asked for it. Only a page with more
160
160
  * slots than starters repeats one: the photo placed longest ago, never the
161
- * one in a slot beside it.
161
+ * one in a slot beside it. The owner's portrait goes only to a picture of
162
+ * people; a gallery picture never takes it, even to avoid a repeat.
162
163
  */ export function aiLayoutStarterPhotos(slots, seed) {
163
164
  const turn = aiLayoutSeedNumber(seed);
164
165
  const order = [
@@ -179,10 +180,12 @@ function rotate(list, by) {
179
180
  ...STARTER_POOLS.about
180
181
  ] : rotate(STARTER_POOLS[slot.role], turn);
181
182
  const rest = rotate(ALL_STARTERS.filter((name)=>!pool.includes(name)), turn);
183
+ // The owner's portrait is a picture of a person, and only a picture of
184
+ // people takes it: never a vase in a gallery (AGL-3660).
182
185
  const candidates = [
183
186
  ...pool,
184
187
  ...rest
185
- ];
188
+ ].filter((name)=>slot.role === 'about' || name !== 'about');
186
189
  const unused = candidates.find((candidate)=>!placedAt.has(candidate));
187
190
  const beside = new Set([
188
191
  names[index - 1],
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../../../../../libs/plugins/ai/src/lib/layout-language/ai-layout-pictures.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 { DEFAULT_SITE_IMAGES } from '@aglyn/aglyn/app-utils/default-site'\nimport type { NodesMap } from '@aglyn/aglyn/types/nodes'\n\n/**\n * THE PICTURES OF A LANGUAGE PAGE (AGL-3660).\n *\n * The compiler writes a picture slot as a frame holding an icon and an\n * `image` with alt text and no source (`ai-layout-compiler.ts`), and stays\n * pure: it never looks a photo up. This module is what runs after it, on the\n * stored page — it finds each empty slot, decides what kind of picture the\n * slot is, and fills it with a photo the site can serve from its own origin.\n *\n * Two sources fill a slot, in order:\n *\n * 1. A stock photo searched for by the slot's own description and copied into\n * the site's media library (`jobs/ai-layout-stock-photos.ts`), when the\n * deployment has a stock-photo key.\n * 2. Otherwise, or when the search finds nothing fitting, one of the STARTER\n * PHOTOS every new site is born with (`DEFAULT_SITE_IMAGES`, served at\n * `/_static/starter/` by the tenant and the console alike, credited in the\n * CREDITS file beside them). They are the platform's own files, so a\n * visitor's browser asks no other host for them.\n *\n * The slot keeps the alt text the model wrote whichever source fills it.\n */\n\n/** What a picture slot is for, read from where it sits and what it says. */\nexport type AiLayoutPictureRole = 'hero' | 'about' | 'gallery'\n\n/** One empty picture slot of a stored page, in document order. */\nexport interface AiLayoutPictureSlot {\n /** The `image` node. */\n imageId: string\n /** The frame around it, which sets the slot's shape. */\n frameId: string | null\n /** The placeholder icon sharing the frame, taken out once a photo fills it. */\n iconId: string | null\n alt: string\n /** The frame's shape as width over height; 1 when it names none. */\n aspect: number\n /** The plan index of the section it sits in, or -1. */\n sectionIndex: number\n role: AiLayoutPictureRole\n}\n\n/** A photo a slot is filled with. */\nexport interface AiLayoutPicturePhoto {\n /** The site-served address: a starter path or the media library's own. */\n src: string\n width: number\n height: number\n}\n\n/** Words that say a picture shows people, or a section is about them. */\nconst PEOPLE =\n /\\b(about|team|story|owner|owners|founder|founders|staff|crew|people|person|portrait|meet|family|families|who we are|our (?:team|crew|people|story)|therapist|counsel(?:l)?or|coach|doctor|dentist|stylist|trainer|smil(?:e|es|ing)|headshot)\\b/i\n\n/**\n * An `aspectRatio` (`'16 / 9'`) as a number, or `null` where none is named.\n * A responsive one (`{ xs: '4 / 3', md: '4 / 5' }`, as a designed page's\n * pictures carry, AGL-3660) reads at the desktop breakpoint first: one photo\n * serves every width, and the wide screen is where its shape shows most.\n */\nfunction aspectOf(value: unknown): number | null {\n if (value && typeof value === 'object') {\n const byBreakpoint = value as Record<string, unknown>\n for (const breakpoint of ['md', 'lg', 'xl', 'sm', 'xs']) {\n const ratio = aspectOf(byBreakpoint[breakpoint])\n if (ratio !== null) return ratio\n }\n return null\n }\n const match = /^\\s*(\\d+(?:\\.\\d+)?)\\s*\\/\\s*(\\d+(?:\\.\\d+)?)\\s*$/.exec(String(value ?? ''))\n if (!match) return null\n const ratio = Number(match[1]) / Number(match[2])\n return Number.isFinite(ratio) && ratio > 0 ? ratio : null\n}\n\n/** The shape a full-bleed cover's photo is searched for: it fills a wide band. */\nconst COVER_ASPECT = 16 / 9\n\n/**\n * A slot's shape (AGL-3660): the image's own `aspectRatio` (a designed\n * picture card sets it on the image), else its frame's, else — for a frame\n * laid absolutely over its whole section, a designed photo cover — a wide\n * band's. 1 where nothing says.\n */\nfunction slotAspect(image: { sx?: Record<string, unknown> } | undefined, frame: { sx?: Record<string, unknown> } | undefined): number {\n const own = aspectOf(image?.sx?.['aspectRatio']) ?? aspectOf(frame?.sx?.['aspectRatio'])\n if (own !== null) return own\n const sx = frame?.sx\n if (sx?.['position'] === 'absolute' && sx['width'] === '100%' && sx['height'] === '100%') return COVER_ASPECT\n return 1\n}\n\n/** A slot's role: the first section's picture is the hero; people make an about picture. */\nexport function aiLayoutPictureRole(input: {\n sectionIndex: number\n sectionName: string\n alt: string\n}): AiLayoutPictureRole {\n if (input.sectionIndex === 0) return 'hero'\n if (PEOPLE.test(input.sectionName) || PEOPLE.test(input.alt)) return 'about'\n return 'gallery'\n}\n\n/**\n * Every empty picture slot of a stored page, walked from its root in\n * document order. A slot that already has a source — one the owner placed, or\n * an earlier pass filled — is not a slot.\n */\nexport function aiLayoutPictureSlots(\n nodes: NodesMap,\n rootId: string,\n sections: { ids: readonly string[]; names: readonly string[] },\n): AiLayoutPictureSlot[] {\n const map = nodes as unknown as Record<\n string,\n { componentId?: string; props?: Record<string, unknown>; sx?: Record<string, unknown>; nodes?: string[] }\n >\n const slots: AiLayoutPictureSlot[] = []\n const seen = new Set<string>()\n const visit = (id: string, parentId: string | null, sectionIndex: number) => {\n if (seen.has(id)) return\n seen.add(id)\n const node = map[id]\n if (!node) return\n const index = sections.ids.indexOf(id)\n const here = index >= 0 ? index : sectionIndex\n if (node.componentId === 'image') {\n const src = node.props?.['src']\n const alt = typeof node.props?.['alt'] === 'string' ? (node.props['alt'] as string).trim() : ''\n if ((typeof src !== 'string' || !src.trim()) && alt) {\n const frame = parentId ? map[parentId] : undefined\n const iconId =\n frame?.nodes?.find((child) => child !== id && map[child]?.componentId === 'icon') ?? null\n slots.push({\n imageId: id,\n frameId: frame ? parentId : null,\n iconId,\n alt,\n aspect: slotAspect(node, frame),\n sectionIndex: here,\n role: aiLayoutPictureRole({\n sectionIndex: here,\n sectionName: here >= 0 ? (sections.names[here] ?? '') : '',\n alt,\n }),\n })\n }\n }\n for (const child of node.nodes ?? []) visit(child, id, here)\n }\n visit(rootId, null, -1)\n return slots\n}\n\n/** The starter photos, by name. */\nexport const AI_LAYOUT_STARTER_PHOTOS = DEFAULT_SITE_IMAGES\n\ntype StarterName = keyof typeof AI_LAYOUT_STARTER_PHOTOS\n\n/**\n * The starter photos each role draws from, best first. A hero draws from the\n * wide photos that read as a page's opening, an about picture leads with the\n * owner, and the rest are the gallery's.\n */\nconst STARTER_POOLS: Readonly<Record<AiLayoutPictureRole, readonly StarterName[]>> = {\n hero: ['hero', 'desk', 'craft'],\n about: ['about', 'hero'],\n gallery: ['desk', 'plants', 'craft'],\n}\n\nconst ALL_STARTERS = Object.keys(AI_LAYOUT_STARTER_PHOTOS) as StarterName[]\n\n/** A small stable number from a seed, so the same job always picks the same photos. */\nexport function aiLayoutSeedNumber(seed: string): number {\n let hash = 2166136261\n for (let index = 0; index < seed.length; index += 1) {\n hash ^= seed.charCodeAt(index)\n hash = Math.imul(hash, 16777619)\n }\n return hash >>> 0\n}\n\nfunction rotate<T>(list: readonly T[], by: number): T[] {\n if (!list.length) return []\n const start = by % list.length\n return [...list.slice(start), ...list.slice(0, start)]\n}\n\n/**\n * A starter photo for each slot (AGL-3660). No photo repeats on a page while\n * an unused one remains, and every pool turns by the seed, so sites do not\n * all open with the same photo while one job always picks the same.\n *\n * The slots are served by what they are for, not where they sit: the hero\n * first, then the pictures of people (an about picture leads with the owner),\n * then the rest. The live Juniper Clay start (2026-10-08) had six slots and\n * five starters, and the owner's photo went to a stoneware card before the\n * About section that described the artist asked for it. Only a page with more\n * slots than starters repeats one: the photo placed longest ago, never the\n * one in a slot beside it.\n */\nexport function aiLayoutStarterPhotos(\n slots: readonly AiLayoutPictureSlot[],\n seed: string,\n): AiLayoutPicturePhoto[] {\n const turn = aiLayoutSeedNumber(seed)\n const order: AiLayoutPictureRole[] = ['hero', 'about', 'gallery']\n const served = slots\n .map((slot, index) => ({ slot, index }))\n .sort((a, b) => order.indexOf(a.slot.role) - order.indexOf(b.slot.role) || a.index - b.index)\n const names: Array<StarterName | undefined> = slots.map(() => undefined)\n /** When each starter was last placed, by its turn in the serving order. */\n const placedAt = new Map<StarterName, number>()\n served.forEach(({ slot, index }, step) => {\n // An about picture leads with the owner on every site; the others turn.\n const pool = slot.role === 'about' ? [...STARTER_POOLS.about] : rotate(STARTER_POOLS[slot.role], turn)\n const rest = rotate(\n ALL_STARTERS.filter((name) => !pool.includes(name)),\n turn,\n )\n const candidates = [...pool, ...rest]\n const unused = candidates.find((candidate) => !placedAt.has(candidate))\n const beside = new Set([names[index - 1], names[index + 1]].filter(Boolean))\n const name =\n unused ??\n [...candidates]\n .sort((a, b) => (placedAt.get(a) ?? -1) - (placedAt.get(b) ?? -1))\n .find((candidate) => !beside.has(candidate)) ??\n candidates[0]\n names[index] = name\n placedAt.set(name, step)\n })\n return names.map((name) => {\n const photo = AI_LAYOUT_STARTER_PHOTOS[name as StarterName]\n return { src: photo.src, width: photo.width, height: photo.height }\n })\n}\n\n/**\n * The page with each slot filled by its photo: the `image` given its source,\n * its intrinsic size and how it loads (the hero eagerly, as the starter home\n * page loads its own), and the placeholder icon taken out of the frame. The\n * alt text is kept as the model wrote it. A slot given no photo is left as\n * the compiler wrote it.\n */\nexport function aiLayoutPlacePictures(\n nodes: NodesMap,\n slots: readonly AiLayoutPictureSlot[],\n photos: ReadonlyArray<AiLayoutPicturePhoto | null>,\n): NodesMap {\n const next = { ...(nodes as unknown as Record<string, Record<string, unknown>>) }\n slots.forEach((slot, index) => {\n const photo = photos[index]\n const node = next[slot.imageId]\n if (!photo || !node) return\n next[slot.imageId] = {\n ...node,\n props: {\n ...((node['props'] as Record<string, unknown> | undefined) ?? {}),\n src: photo.src,\n intrinsicWidth: photo.width,\n intrinsicHeight: photo.height,\n loading: slot.role === 'hero' ? 'eager' : 'lazy',\n },\n }\n if (slot.iconId && slot.frameId && next[slot.frameId]) {\n const frame = next[slot.frameId]\n next[slot.frameId] = {\n ...frame,\n nodes: ((frame['nodes'] as string[] | undefined) ?? []).filter((child) => child !== slot.iconId),\n }\n delete next[slot.iconId]\n }\n })\n return next as unknown as NodesMap\n}\n\n/**\n * A source of photos found for the slots themselves — a stock search copied\n * into the site's library — answering one photo or `null` per slot, in order.\n * It may throw; a slot it leaves unfilled takes a starter photo.\n */\nexport type AiLayoutPictureSource = (\n slots: readonly AiLayoutPictureSlot[],\n) => Promise<ReadonlyArray<AiLayoutPicturePhoto | null>>\n\n/**\n * Fills a stored page's empty picture slots (AGL-3660): from `source` where\n * one is given and it answers, else from the starter photos. Never throws,\n * and never leaves a page worse than it came: a picture is decoration, and a\n * job never fails over one.\n */\nexport async function aiResolveLayoutPictures(\n nodes: NodesMap,\n input: {\n rootId: string\n sectionIds: readonly string[]\n sectionNames: readonly string[]\n /** A per-job word, so two sites do not open with the same photo. */\n seed: string\n source?: AiLayoutPictureSource | null\n },\n): Promise<NodesMap> {\n try {\n const slots = aiLayoutPictureSlots(nodes, input.rootId, {\n ids: input.sectionIds,\n names: input.sectionNames,\n })\n if (!slots.length) return nodes\n let found: ReadonlyArray<AiLayoutPicturePhoto | null> = []\n if (input.source) {\n try {\n found = await input.source(slots)\n } catch (error) {\n console.warn('ai layout pictures: the stock source failed; starter photos fill the page', { error })\n found = []\n }\n }\n const starters = aiLayoutStarterPhotos(slots, input.seed)\n return aiLayoutPlacePictures(\n nodes,\n slots,\n slots.map((_slot, index) => found[index] ?? starters[index]),\n )\n } catch (error) {\n console.error('ai layout pictures: the page keeps its empty picture slots', { error })\n return nodes\n }\n}\n"],"names":["DEFAULT_SITE_IMAGES","PEOPLE","aspectOf","value","byBreakpoint","breakpoint","ratio","match","exec","String","Number","isFinite","COVER_ASPECT","slotAspect","image","frame","own","sx","aiLayoutPictureRole","input","sectionIndex","test","sectionName","alt","aiLayoutPictureSlots","nodes","rootId","sections","map","slots","seen","Set","visit","id","parentId","node","has","add","index","ids","indexOf","here","componentId","src","props","trim","undefined","iconId","find","child","push","imageId","frameId","aspect","role","names","AI_LAYOUT_STARTER_PHOTOS","STARTER_POOLS","hero","about","gallery","ALL_STARTERS","Object","keys","aiLayoutSeedNumber","seed","hash","length","charCodeAt","Math","imul","rotate","list","by","start","slice","aiLayoutStarterPhotos","turn","order","served","slot","sort","a","b","placedAt","Map","forEach","step","unused","pool","rest","filter","name","includes","candidates","candidate","beside","Boolean","get","set","photo","width","height","aiLayoutPlacePictures","photos","next","intrinsicWidth","intrinsicHeight","loading","aiResolveLayoutPictures","sectionIds","sectionNames","found","source","error","console","warn","starters","_slot"],"mappings":"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"}
1
+ {"version":3,"sources":["../../../../../../../libs/plugins/ai/src/lib/layout-language/ai-layout-pictures.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 { DEFAULT_SITE_IMAGES } from '@aglyn/aglyn/app-utils/default-site'\nimport type { NodesMap } from '@aglyn/aglyn/types/nodes'\n\n/**\n * THE PICTURES OF A LANGUAGE PAGE (AGL-3660).\n *\n * The compiler writes a picture slot as a frame holding an icon and an\n * `image` with alt text and no source (`ai-layout-compiler.ts`), and stays\n * pure: it never looks a photo up. This module is what runs after it, on the\n * stored page — it finds each empty slot, decides what kind of picture the\n * slot is, and fills it with a photo the site can serve from its own origin.\n *\n * Two sources fill a slot, in order:\n *\n * 1. A stock photo searched for by the slot's own description and copied into\n * the site's media library (`jobs/ai-layout-stock-photos.ts`), when the\n * deployment has a stock-photo key.\n * 2. Otherwise, or when the search finds nothing fitting, one of the STARTER\n * PHOTOS every new site is born with (`DEFAULT_SITE_IMAGES`, served at\n * `/_static/starter/` by the tenant and the console alike, credited in the\n * CREDITS file beside them). They are the platform's own files, so a\n * visitor's browser asks no other host for them.\n *\n * The slot keeps the alt text the model wrote whichever source fills it.\n */\n\n/** What a picture slot is for, read from where it sits and what it says. */\nexport type AiLayoutPictureRole = 'hero' | 'about' | 'gallery'\n\n/** One empty picture slot of a stored page, in document order. */\nexport interface AiLayoutPictureSlot {\n /** The `image` node. */\n imageId: string\n /** The frame around it, which sets the slot's shape. */\n frameId: string | null\n /** The placeholder icon sharing the frame, taken out once a photo fills it. */\n iconId: string | null\n alt: string\n /** The frame's shape as width over height; 1 when it names none. */\n aspect: number\n /** The plan index of the section it sits in, or -1. */\n sectionIndex: number\n role: AiLayoutPictureRole\n /**\n * A product's photo (AGL-3676): what it is of, most specific first — its\n * name, then what its photo should show — searched for with the shop's\n * category and held to it, so a wick trimmer is never a laptop on a desk.\n */\n product?: { subjects: readonly string[] }\n}\n\n/** A photo a slot is filled with. */\nexport interface AiLayoutPicturePhoto {\n /** The site-served address: a starter path or the media library's own. */\n src: string\n width: number\n height: number\n}\n\n/** Words that say a picture shows people, or a section is about them. */\nconst PEOPLE =\n /\\b(about|team|story|owner|owners|founder|founders|staff|crew|people|person|portrait|meet|family|families|who we are|our (?:team|crew|people|story)|therapist|counsel(?:l)?or|coach|doctor|dentist|stylist|trainer|smil(?:e|es|ing)|headshot)\\b/i\n\n/**\n * An `aspectRatio` (`'16 / 9'`) as a number, or `null` where none is named.\n * A responsive one (`{ xs: '4 / 3', md: '4 / 5' }`, as a designed page's\n * pictures carry, AGL-3660) reads at the desktop breakpoint first: one photo\n * serves every width, and the wide screen is where its shape shows most.\n */\nfunction aspectOf(value: unknown): number | null {\n if (value && typeof value === 'object') {\n const byBreakpoint = value as Record<string, unknown>\n for (const breakpoint of ['md', 'lg', 'xl', 'sm', 'xs']) {\n const ratio = aspectOf(byBreakpoint[breakpoint])\n if (ratio !== null) return ratio\n }\n return null\n }\n const match = /^\\s*(\\d+(?:\\.\\d+)?)\\s*\\/\\s*(\\d+(?:\\.\\d+)?)\\s*$/.exec(String(value ?? ''))\n if (!match) return null\n const ratio = Number(match[1]) / Number(match[2])\n return Number.isFinite(ratio) && ratio > 0 ? ratio : null\n}\n\n/** The shape a full-bleed cover's photo is searched for: it fills a wide band. */\nconst COVER_ASPECT = 16 / 9\n\n/**\n * A slot's shape (AGL-3660): the image's own `aspectRatio` (a designed\n * picture card sets it on the image), else its frame's, else — for a frame\n * laid absolutely over its whole section, a designed photo cover — a wide\n * band's. 1 where nothing says.\n */\nfunction slotAspect(image: { sx?: Record<string, unknown> } | undefined, frame: { sx?: Record<string, unknown> } | undefined): number {\n const own = aspectOf(image?.sx?.['aspectRatio']) ?? aspectOf(frame?.sx?.['aspectRatio'])\n if (own !== null) return own\n const sx = frame?.sx\n if (sx?.['position'] === 'absolute' && sx['width'] === '100%' && sx['height'] === '100%') return COVER_ASPECT\n return 1\n}\n\n/** A slot's role: the first section's picture is the hero; people make an about picture. */\nexport function aiLayoutPictureRole(input: {\n sectionIndex: number\n sectionName: string\n alt: string\n}): AiLayoutPictureRole {\n if (input.sectionIndex === 0) return 'hero'\n if (PEOPLE.test(input.sectionName) || PEOPLE.test(input.alt)) return 'about'\n return 'gallery'\n}\n\n/**\n * Every empty picture slot of a stored page, walked from its root in\n * document order. A slot that already has a source — one the owner placed, or\n * an earlier pass filled — is not a slot.\n */\nexport function aiLayoutPictureSlots(\n nodes: NodesMap,\n rootId: string,\n sections: { ids: readonly string[]; names: readonly string[] },\n): AiLayoutPictureSlot[] {\n const map = nodes as unknown as Record<\n string,\n { componentId?: string; props?: Record<string, unknown>; sx?: Record<string, unknown>; nodes?: string[] }\n >\n const slots: AiLayoutPictureSlot[] = []\n const seen = new Set<string>()\n const visit = (id: string, parentId: string | null, sectionIndex: number) => {\n if (seen.has(id)) return\n seen.add(id)\n const node = map[id]\n if (!node) return\n const index = sections.ids.indexOf(id)\n const here = index >= 0 ? index : sectionIndex\n if (node.componentId === 'image') {\n const src = node.props?.['src']\n const alt = typeof node.props?.['alt'] === 'string' ? (node.props['alt'] as string).trim() : ''\n if ((typeof src !== 'string' || !src.trim()) && alt) {\n const frame = parentId ? map[parentId] : undefined\n const iconId =\n frame?.nodes?.find((child) => child !== id && map[child]?.componentId === 'icon') ?? null\n slots.push({\n imageId: id,\n frameId: frame ? parentId : null,\n iconId,\n alt,\n aspect: slotAspect(node, frame),\n sectionIndex: here,\n role: aiLayoutPictureRole({\n sectionIndex: here,\n sectionName: here >= 0 ? (sections.names[here] ?? '') : '',\n alt,\n }),\n })\n }\n }\n for (const child of node.nodes ?? []) visit(child, id, here)\n }\n visit(rootId, null, -1)\n return slots\n}\n\n/** The starter photos, by name. */\nexport const AI_LAYOUT_STARTER_PHOTOS = DEFAULT_SITE_IMAGES\n\ntype StarterName = keyof typeof AI_LAYOUT_STARTER_PHOTOS\n\n/**\n * The starter photos each role draws from, best first. A hero draws from the\n * wide photos that read as a page's opening, an about picture leads with the\n * owner, and the rest are the gallery's.\n */\nconst STARTER_POOLS: Readonly<Record<AiLayoutPictureRole, readonly StarterName[]>> = {\n hero: ['hero', 'desk', 'craft'],\n about: ['about', 'hero'],\n gallery: ['desk', 'plants', 'craft'],\n}\n\nconst ALL_STARTERS = Object.keys(AI_LAYOUT_STARTER_PHOTOS) as StarterName[]\n\n/** A small stable number from a seed, so the same job always picks the same photos. */\nexport function aiLayoutSeedNumber(seed: string): number {\n let hash = 2166136261\n for (let index = 0; index < seed.length; index += 1) {\n hash ^= seed.charCodeAt(index)\n hash = Math.imul(hash, 16777619)\n }\n return hash >>> 0\n}\n\nfunction rotate<T>(list: readonly T[], by: number): T[] {\n if (!list.length) return []\n const start = by % list.length\n return [...list.slice(start), ...list.slice(0, start)]\n}\n\n/**\n * A starter photo for each slot (AGL-3660). No photo repeats on a page while\n * an unused one remains, and every pool turns by the seed, so sites do not\n * all open with the same photo while one job always picks the same.\n *\n * The slots are served by what they are for, not where they sit: the hero\n * first, then the pictures of people (an about picture leads with the owner),\n * then the rest. The live Juniper Clay start (2026-10-08) had six slots and\n * five starters, and the owner's photo went to a stoneware card before the\n * About section that described the artist asked for it. Only a page with more\n * slots than starters repeats one: the photo placed longest ago, never the\n * one in a slot beside it. The owner's portrait goes only to a picture of\n * people; a gallery picture never takes it, even to avoid a repeat.\n */\nexport function aiLayoutStarterPhotos(\n slots: readonly AiLayoutPictureSlot[],\n seed: string,\n): AiLayoutPicturePhoto[] {\n const turn = aiLayoutSeedNumber(seed)\n const order: AiLayoutPictureRole[] = ['hero', 'about', 'gallery']\n const served = slots\n .map((slot, index) => ({ slot, index }))\n .sort((a, b) => order.indexOf(a.slot.role) - order.indexOf(b.slot.role) || a.index - b.index)\n const names: Array<StarterName | undefined> = slots.map(() => undefined)\n /** When each starter was last placed, by its turn in the serving order. */\n const placedAt = new Map<StarterName, number>()\n served.forEach(({ slot, index }, step) => {\n // An about picture leads with the owner on every site; the others turn.\n const pool = slot.role === 'about' ? [...STARTER_POOLS.about] : rotate(STARTER_POOLS[slot.role], turn)\n const rest = rotate(\n ALL_STARTERS.filter((name) => !pool.includes(name)),\n turn,\n )\n // The owner's portrait is a picture of a person, and only a picture of\n // people takes it: never a vase in a gallery (AGL-3660).\n const candidates = [...pool, ...rest].filter((name) => slot.role === 'about' || name !== 'about')\n const unused = candidates.find((candidate) => !placedAt.has(candidate))\n const beside = new Set([names[index - 1], names[index + 1]].filter(Boolean))\n const name =\n unused ??\n [...candidates]\n .sort((a, b) => (placedAt.get(a) ?? -1) - (placedAt.get(b) ?? -1))\n .find((candidate) => !beside.has(candidate)) ??\n candidates[0]\n names[index] = name\n placedAt.set(name, step)\n })\n return names.map((name) => {\n const photo = AI_LAYOUT_STARTER_PHOTOS[name as StarterName]\n return { src: photo.src, width: photo.width, height: photo.height }\n })\n}\n\n/**\n * The page with each slot filled by its photo: the `image` given its source,\n * its intrinsic size and how it loads (the hero eagerly, as the starter home\n * page loads its own), and the placeholder icon taken out of the frame. The\n * alt text is kept as the model wrote it. A slot given no photo is left as\n * the compiler wrote it.\n */\nexport function aiLayoutPlacePictures(\n nodes: NodesMap,\n slots: readonly AiLayoutPictureSlot[],\n photos: ReadonlyArray<AiLayoutPicturePhoto | null>,\n): NodesMap {\n const next = { ...(nodes as unknown as Record<string, Record<string, unknown>>) }\n slots.forEach((slot, index) => {\n const photo = photos[index]\n const node = next[slot.imageId]\n if (!photo || !node) return\n next[slot.imageId] = {\n ...node,\n props: {\n ...((node['props'] as Record<string, unknown> | undefined) ?? {}),\n src: photo.src,\n intrinsicWidth: photo.width,\n intrinsicHeight: photo.height,\n loading: slot.role === 'hero' ? 'eager' : 'lazy',\n },\n }\n if (slot.iconId && slot.frameId && next[slot.frameId]) {\n const frame = next[slot.frameId]\n next[slot.frameId] = {\n ...frame,\n nodes: ((frame['nodes'] as string[] | undefined) ?? []).filter((child) => child !== slot.iconId),\n }\n delete next[slot.iconId]\n }\n })\n return next as unknown as NodesMap\n}\n\n/**\n * A source of photos found for the slots themselves — a stock search copied\n * into the site's library — answering one photo or `null` per slot, in order.\n * It may throw; a slot it leaves unfilled takes a starter photo.\n */\nexport type AiLayoutPictureSource = (\n slots: readonly AiLayoutPictureSlot[],\n) => Promise<ReadonlyArray<AiLayoutPicturePhoto | null>>\n\n/**\n * Fills a stored page's empty picture slots (AGL-3660): from `source` where\n * one is given and it answers, else from the starter photos. Never throws,\n * and never leaves a page worse than it came: a picture is decoration, and a\n * job never fails over one.\n */\nexport async function aiResolveLayoutPictures(\n nodes: NodesMap,\n input: {\n rootId: string\n sectionIds: readonly string[]\n sectionNames: readonly string[]\n /** A per-job word, so two sites do not open with the same photo. */\n seed: string\n source?: AiLayoutPictureSource | null\n },\n): Promise<NodesMap> {\n try {\n const slots = aiLayoutPictureSlots(nodes, input.rootId, {\n ids: input.sectionIds,\n names: input.sectionNames,\n })\n if (!slots.length) return nodes\n let found: ReadonlyArray<AiLayoutPicturePhoto | null> = []\n if (input.source) {\n try {\n found = await input.source(slots)\n } catch (error) {\n console.warn('ai layout pictures: the stock source failed; starter photos fill the page', { error })\n found = []\n }\n }\n const starters = aiLayoutStarterPhotos(slots, input.seed)\n return aiLayoutPlacePictures(\n nodes,\n slots,\n slots.map((_slot, index) => found[index] ?? starters[index]),\n )\n } catch (error) {\n console.error('ai layout pictures: the page keeps its empty picture slots', { error })\n return nodes\n }\n}\n"],"names":["DEFAULT_SITE_IMAGES","PEOPLE","aspectOf","value","byBreakpoint","breakpoint","ratio","match","exec","String","Number","isFinite","COVER_ASPECT","slotAspect","image","frame","own","sx","aiLayoutPictureRole","input","sectionIndex","test","sectionName","alt","aiLayoutPictureSlots","nodes","rootId","sections","map","slots","seen","Set","visit","id","parentId","node","has","add","index","ids","indexOf","here","componentId","src","props","trim","undefined","iconId","find","child","push","imageId","frameId","aspect","role","names","AI_LAYOUT_STARTER_PHOTOS","STARTER_POOLS","hero","about","gallery","ALL_STARTERS","Object","keys","aiLayoutSeedNumber","seed","hash","length","charCodeAt","Math","imul","rotate","list","by","start","slice","aiLayoutStarterPhotos","turn","order","served","slot","sort","a","b","placedAt","Map","forEach","step","unused","pool","rest","filter","name","includes","candidates","candidate","beside","Boolean","get","set","photo","width","height","aiLayoutPlacePictures","photos","next","intrinsicWidth","intrinsicHeight","loading","aiResolveLayoutPictures","sectionIds","sectionNames","found","source","error","console","warn","starters","_slot"],"mappings":";AAAA;;;;;;;;;;;;;;;CAeC,GAED,SAASA,mBAAmB,QAAQ,sCAAqC;AA2DzE,uEAAuE,GACvE,MAAMC,SACJ;AAEF;;;;;CAKC,GACD,SAASC,SAASC,KAAc;IAC9B,IAAIA,SAAS,OAAOA,UAAU,UAAU;QACtC,MAAMC,eAAeD;QACrB,KAAK,MAAME,cAAc;YAAC;YAAM;YAAM;YAAM;YAAM;SAAK,CAAE;YACvD,MAAMC,QAAQJ,SAASE,YAAY,CAACC,WAAW;YAC/C,IAAIC,UAAU,MAAM,OAAOA;QAC7B;QACA,OAAO;IACT;IACA,MAAMC,QAAQ,iDAAiDC,IAAI,CAACC,OAAON,gBAAAA,QAAS;IACpF,IAAI,CAACI,OAAO,OAAO;IACnB,MAAMD,QAAQI,OAAOH,KAAK,CAAC,EAAE,IAAIG,OAAOH,KAAK,CAAC,EAAE;IAChD,OAAOG,OAAOC,QAAQ,CAACL,UAAUA,QAAQ,IAAIA,QAAQ;AACvD;AAEA,gFAAgF,GAChF,MAAMM,eAAe,KAAK;AAE1B;;;;;CAKC,GACD,SAASC,WAAWC,KAAmD,EAAEC,KAAmD;QAC9Gb;QAASY,WAAwCC;IAA7D,MAAMC,OAAMd,YAAAA,SAASY,0BAAAA,YAAAA,MAAOG,EAAE,qBAATH,SAAW,CAAC,cAAc,aAAnCZ,YAAwCA,SAASa,0BAAAA,YAAAA,MAAOE,EAAE,qBAATF,SAAW,CAAC,cAAc;IACvF,IAAIC,QAAQ,MAAM,OAAOA;IACzB,MAAMC,KAAKF,yBAAAA,MAAOE,EAAE;IACpB,IAAIA,CAAAA,sBAAAA,EAAI,CAAC,WAAW,MAAK,cAAcA,EAAE,CAAC,QAAQ,KAAK,UAAUA,EAAE,CAAC,SAAS,KAAK,QAAQ,OAAOL;IACjG,OAAO;AACT;AAEA,0FAA0F,GAC1F,OAAO,SAASM,oBAAoBC,KAInC;IACC,IAAIA,MAAMC,YAAY,KAAK,GAAG,OAAO;IACrC,IAAInB,OAAOoB,IAAI,CAACF,MAAMG,WAAW,KAAKrB,OAAOoB,IAAI,CAACF,MAAMI,GAAG,GAAG,OAAO;IACrE,OAAO;AACT;AAEA;;;;CAIC,GACD,OAAO,SAASC,qBACdC,KAAe,EACfC,MAAc,EACdC,QAA8D;IAE9D,MAAMC,MAAMH;IAIZ,MAAMI,QAA+B,EAAE;IACvC,MAAMC,OAAO,IAAIC;IACjB,MAAMC,QAAQ,CAACC,IAAYC,UAAyBd;YA6B9Be;QA5BpB,IAAIL,KAAKM,GAAG,CAACH,KAAK;QAClBH,KAAKO,GAAG,CAACJ;QACT,MAAME,OAAOP,GAAG,CAACK,GAAG;QACpB,IAAI,CAACE,MAAM;QACX,MAAMG,QAAQX,SAASY,GAAG,CAACC,OAAO,CAACP;QACnC,MAAMQ,OAAOH,SAAS,IAAIA,QAAQlB;QAClC,IAAIe,KAAKO,WAAW,KAAK,SAAS;gBACpBP,aACOA;YADnB,MAAMQ,OAAMR,cAAAA,KAAKS,KAAK,qBAAVT,WAAY,CAAC,MAAM;YAC/B,MAAMZ,MAAM,SAAOY,eAAAA,KAAKS,KAAK,qBAAVT,YAAY,CAAC,MAAM,MAAK,WAAW,AAACA,KAAKS,KAAK,CAAC,MAAM,CAAYC,IAAI,KAAK;YAC7F,IAAI,AAAC,CAAA,OAAOF,QAAQ,YAAY,CAACA,IAAIE,IAAI,EAAC,KAAMtB,KAAK;0BAarBI;oBAV5BZ;gBAFF,MAAMA,QAAQmB,WAAWN,GAAG,CAACM,SAAS,GAAGY;gBACzC,MAAMC,iBACJhC,0BAAAA,eAAAA,MAAOU,KAAK,qBAAZV,aAAciC,IAAI,CAAC,CAACC;wBAA0BrB;2BAAhBqB,UAAUhB,MAAML,EAAAA,aAAAA,GAAG,CAACqB,MAAM,qBAAVrB,WAAYc,WAAW,MAAK;qCAAW;gBACvFb,MAAMqB,IAAI,CAAC;oBACTC,SAASlB;oBACTmB,SAASrC,QAAQmB,WAAW;oBAC5Ba;oBACAxB;oBACA8B,QAAQxC,WAAWsB,MAAMpB;oBACzBK,cAAcqB;oBACda,MAAMpC,oBAAoB;wBACxBE,cAAcqB;wBACdnB,aAAamB,QAAQ,KAAKd,uBAAAA,SAAS4B,KAAK,CAACd,KAAK,YAApBd,uBAAwB,KAAM;wBACxDJ;oBACF;gBACF;YACF;QACF;QACA,KAAK,MAAM0B,UAASd,cAAAA,KAAKV,KAAK,YAAVU,cAAc,EAAE,CAAEH,MAAMiB,OAAOhB,IAAIQ;IACzD;IACAT,MAAMN,QAAQ,MAAM,CAAC;IACrB,OAAOG;AACT;AAEA,iCAAiC,GACjC,OAAO,MAAM2B,2BAA2BxD,oBAAmB;AAI3D;;;;CAIC,GACD,MAAMyD,gBAA+E;IACnFC,MAAM;QAAC;QAAQ;QAAQ;KAAQ;IAC/BC,OAAO;QAAC;QAAS;KAAO;IACxBC,SAAS;QAAC;QAAQ;QAAU;KAAQ;AACtC;AAEA,MAAMC,eAAeC,OAAOC,IAAI,CAACP;AAEjC,qFAAqF,GACrF,OAAO,SAASQ,mBAAmBC,IAAY;IAC7C,IAAIC,OAAO;IACX,IAAK,IAAI5B,QAAQ,GAAGA,QAAQ2B,KAAKE,MAAM,EAAE7B,SAAS,EAAG;QACnD4B,QAAQD,KAAKG,UAAU,CAAC9B;QACxB4B,OAAOG,KAAKC,IAAI,CAACJ,MAAM;IACzB;IACA,OAAOA,SAAS;AAClB;AAEA,SAASK,OAAUC,IAAkB,EAAEC,EAAU;IAC/C,IAAI,CAACD,KAAKL,MAAM,EAAE,OAAO,EAAE;IAC3B,MAAMO,QAAQD,KAAKD,KAAKL,MAAM;IAC9B,OAAO;WAAIK,KAAKG,KAAK,CAACD;WAAWF,KAAKG,KAAK,CAAC,GAAGD;KAAO;AACxD;AAEA;;;;;;;;;;;;;CAaC,GACD,OAAO,SAASE,sBACd/C,KAAqC,EACrCoC,IAAY;IAEZ,MAAMY,OAAOb,mBAAmBC;IAChC,MAAMa,QAA+B;QAAC;QAAQ;QAAS;KAAU;IACjE,MAAMC,SAASlD,MACZD,GAAG,CAAC,CAACoD,MAAM1C,QAAW,CAAA;YAAE0C;YAAM1C;QAAM,CAAA,GACpC2C,IAAI,CAAC,CAACC,GAAGC,IAAML,MAAMtC,OAAO,CAAC0C,EAAEF,IAAI,CAAC1B,IAAI,IAAIwB,MAAMtC,OAAO,CAAC2C,EAAEH,IAAI,CAAC1B,IAAI,KAAK4B,EAAE5C,KAAK,GAAG6C,EAAE7C,KAAK;IAC9F,MAAMiB,QAAwC1B,MAAMD,GAAG,CAAC,IAAMkB;IAC9D,yEAAyE,GACzE,MAAMsC,WAAW,IAAIC;IACrBN,OAAOO,OAAO,CAAC,CAAC,EAAEN,IAAI,EAAE1C,KAAK,EAAE,EAAEiD;YAa7BC;QAZF,wEAAwE;QACxE,MAAMC,OAAOT,KAAK1B,IAAI,KAAK,UAAU;eAAIG,cAAcE,KAAK;SAAC,GAAGY,OAAOd,aAAa,CAACuB,KAAK1B,IAAI,CAAC,EAAEuB;QACjG,MAAMa,OAAOnB,OACXV,aAAa8B,MAAM,CAAC,CAACC,OAAS,CAACH,KAAKI,QAAQ,CAACD,QAC7Cf;QAEF,uEAAuE;QACvE,yDAAyD;QACzD,MAAMiB,aAAa;eAAIL;eAASC;SAAK,CAACC,MAAM,CAAC,CAACC,OAASZ,KAAK1B,IAAI,KAAK,WAAWsC,SAAS;QACzF,MAAMJ,SAASM,WAAW9C,IAAI,CAAC,CAAC+C,YAAc,CAACX,SAAShD,GAAG,CAAC2D;QAC5D,MAAMC,SAAS,IAAIjE,IAAI;YAACwB,KAAK,CAACjB,QAAQ,EAAE;YAAEiB,KAAK,CAACjB,QAAQ,EAAE;SAAC,CAACqD,MAAM,CAACM;QACnE,MAAML,QACJJ,OAAAA,iBAAAA,SACA;eAAIM;SAAW,CACZb,IAAI,CAAC,CAACC,GAAGC;gBAAOC,eAA0BA;mBAA3B,EAACA,gBAAAA,SAASc,GAAG,CAAChB,cAAbE,gBAAmB,CAAC,OAAMA,iBAAAA,SAASc,GAAG,CAACf,cAAbC,iBAAmB,CAAC;WAC9DpC,IAAI,CAAC,CAAC+C,YAAc,CAACC,OAAO5D,GAAG,CAAC2D,uBAHnCP,OAIAM,UAAU,CAAC,EAAE;QACfvC,KAAK,CAACjB,MAAM,GAAGsD;QACfR,SAASe,GAAG,CAACP,MAAML;IACrB;IACA,OAAOhC,MAAM3B,GAAG,CAAC,CAACgE;QAChB,MAAMQ,QAAQ5C,wBAAwB,CAACoC,KAAoB;QAC3D,OAAO;YAAEjD,KAAKyD,MAAMzD,GAAG;YAAE0D,OAAOD,MAAMC,KAAK;YAAEC,QAAQF,MAAME,MAAM;QAAC;IACpE;AACF;AAEA;;;;;;CAMC,GACD,OAAO,SAASC,sBACd9E,KAAe,EACfI,KAAqC,EACrC2E,MAAkD;IAElD,MAAMC,OAAO,aAAMhF;IACnBI,MAAMyD,OAAO,CAAC,CAACN,MAAM1C;YAOVH;QANT,MAAMiE,QAAQI,MAAM,CAAClE,MAAM;QAC3B,MAAMH,OAAOsE,IAAI,CAACzB,KAAK7B,OAAO,CAAC;QAC/B,IAAI,CAACiD,SAAS,CAACjE,MAAM;QACrBsE,IAAI,CAACzB,KAAK7B,OAAO,CAAC,GAAG,aAChBhB;YACHS,OAAO,cACAT,cAAAA,IAAI,CAAC,QAAQ,YAAbA,cAAyD,CAAC;gBAC/DQ,KAAKyD,MAAMzD,GAAG;gBACd+D,gBAAgBN,MAAMC,KAAK;gBAC3BM,iBAAiBP,MAAME,MAAM;gBAC7BM,SAAS5B,KAAK1B,IAAI,KAAK,SAAS,UAAU;;;QAG9C,IAAI0B,KAAKjC,MAAM,IAAIiC,KAAK5B,OAAO,IAAIqD,IAAI,CAACzB,KAAK5B,OAAO,CAAC,EAAE;gBAI1CrC;YAHX,MAAMA,QAAQ0F,IAAI,CAACzB,KAAK5B,OAAO,CAAC;YAChCqD,IAAI,CAACzB,KAAK5B,OAAO,CAAC,GAAG,aAChBrC;gBACHU,OAAO,EAAEV,eAAAA,KAAK,CAAC,QAAQ,YAAdA,eAA2C,EAAE,EAAE4E,MAAM,CAAC,CAAC1C,QAAUA,UAAU+B,KAAKjC,MAAM;;YAEjG,OAAO0D,IAAI,CAACzB,KAAKjC,MAAM,CAAC;QAC1B;IACF;IACA,OAAO0D;AACT;AAWA;;;;;CAKC,GACD,OAAO,eAAeI,wBACpBpF,KAAe,EACfN,KAOC;IAED,IAAI;QACF,MAAMU,QAAQL,qBAAqBC,OAAON,MAAMO,MAAM,EAAE;YACtDa,KAAKpB,MAAM2F,UAAU;YACrBvD,OAAOpC,MAAM4F,YAAY;QAC3B;QACA,IAAI,CAAClF,MAAMsC,MAAM,EAAE,OAAO1C;QAC1B,IAAIuF,QAAoD,EAAE;QAC1D,IAAI7F,MAAM8F,MAAM,EAAE;YAChB,IAAI;gBACFD,QAAQ,MAAM7F,MAAM8F,MAAM,CAACpF;YAC7B,EAAE,OAAOqF,OAAO;gBACdC,QAAQC,IAAI,CAAC,6EAA6E;oBAAEF;gBAAM;gBAClGF,QAAQ,EAAE;YACZ;QACF;QACA,MAAMK,WAAWzC,sBAAsB/C,OAAOV,MAAM8C,IAAI;QACxD,OAAOsC,sBACL9E,OACAI,OACAA,MAAMD,GAAG,CAAC,CAAC0F,OAAOhF;gBAAU0E;oBAAAA,eAAAA,KAAK,CAAC1E,MAAM,YAAZ0E,eAAgBK,QAAQ,CAAC/E,MAAM;;IAE/D,EAAE,OAAO4E,OAAO;QACdC,QAAQD,KAAK,CAAC,8DAA8D;YAAEA;QAAM;QACpF,OAAOzF;IACT;AACF"}
@@ -0,0 +1,82 @@
1
+ /**
2
+ * @license
3
+ * Copyright 2026 Aglyn LLC
4
+ *
5
+ * Licensed under the Apache License, Version 2.0 (the "License");
6
+ * you may not use this file except in compliance with the License.
7
+ * You may obtain a copy of the License at
8
+ *
9
+ * http://www.apache.org/licenses/LICENSE-2.0
10
+ *
11
+ * Unless required by applicable law or agreed to in writing, software
12
+ * distributed under the License is distributed on an "AS IS" BASIS,
13
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
+ * See the License for the specific language governing permissions and
15
+ * limitations under the License.
16
+ */
17
+ /**
18
+ * A STORE'S STOREFRONT (AGL-3676, second round).
19
+ *
20
+ * Zach, 2026-10-09, on the beta.237 Willow Wick Candles start (job
21
+ * MtlXWjvt9P): "again this … site does not look like a store front". Its home
22
+ * was a lifestyle hero, ONE row of featured products and four text-only bands
23
+ * ("Why soy, why small batch", "How our candles are made", "Made to be
24
+ * loved", "Gifting and shipping"); its Shop page opened with a tall hero and
25
+ * the grid below the fold.
26
+ *
27
+ * A designer storefront (Shopify's Dawn, Craft, Sense; Squarespace's commerce
28
+ * templates) is composed of a handful of PARTS, in a familiar order: a hero
29
+ * that sells, the store's bestsellers, its collections as picture tiles, the
30
+ * brand's story beside a photo, why to buy here with icons, what customers
31
+ * say, and a newsletter sign-up. This module names those parts by the words a
32
+ * plan gives its sections, so the plan step can make sure a store's home has
33
+ * them and the compiler and the listings can draw each one its way.
34
+ *
35
+ * It is NOT a section library (Zach 10/7): the model still names, orders and
36
+ * writes every section, and designs each in the layout language; what code
37
+ * adds is a part the plan left out — named in plain words for the model to
38
+ * design — and the platform elements that show the store's own records.
39
+ *
40
+ * Pure: names and plans only.
41
+ */
42
+ import type { AiBuildPlanScreen } from '../model/ai-build-plan';
43
+ /** A part a storefront home is composed of. */
44
+ export type AiStorefrontPart = 'featured' | 'collections' | 'story' | 'values' | 'reviews' | 'signup';
45
+ /** The section names that say each part, as a plan or a page heading words it. */
46
+ export declare const AI_STOREFRONT_PART_WORDS: Readonly<Record<AiStorefrontPart, RegExp>>;
47
+ /** Whether a planned section's name says a part. */
48
+ export declare function aiStorefrontSays(part: AiStorefrontPart, name: string): boolean;
49
+ /**
50
+ * A store plan's screens with the storefront a shopper expects (AGL-3676):
51
+ *
52
+ * - THE HOME (paid starts only — the Free wall prices each section, and a
53
+ * Free store sells nothing yet): its hero first, then any part the plan
54
+ * left out, added under plain names while the page has room (eight
55
+ * sections), in order of need: the bestsellers the platform lists,
56
+ * right after the hero; the collections as picture tiles after them; the
57
+ * newsletter sign-up as the page's last band; and customer reviews (the
58
+ * store's real ones, which show only once shoppers leave them) before
59
+ * the close.
60
+ * - THE SHOP PAGE: grid first. Its product grid section moves to second,
61
+ * straight under the page's short title, so the products open above the
62
+ * fold; anything the plan put between them (an intro, a story) follows
63
+ * the grid.
64
+ *
65
+ * Every section the plan named stays, in its order otherwise; nothing is
66
+ * removed, and a page at eight sections is given nothing more.
67
+ */
68
+ export declare function aiStorefrontPlanScreens<S extends Pick<AiBuildPlanScreen, 'slug' | 'title' | 'sections'>>(screens: readonly S[], options: {
69
+ paid: boolean;
70
+ isShopPage: (screen: S) => boolean;
71
+ gridSection: (sections: S['sections']) => number;
72
+ }): S[];
73
+ /**
74
+ * The icon each of a store's reasons to buy shows (AGL-3676): its own where
75
+ * the design named one, else one its words suggest, else the next of a few
76
+ * neutral ones — never the same twice in one row.
77
+ */
78
+ export declare function aiStorefrontItemIcons(items: ReadonlyArray<{
79
+ title: string;
80
+ text: string;
81
+ icon?: string;
82
+ }>, known: (word: string | undefined) => boolean): string[];
@@ -0,0 +1,223 @@
1
+ /**
2
+ * @license
3
+ * Copyright 2026 Aglyn LLC
4
+ *
5
+ * Licensed under the Apache License, Version 2.0 (the "License");
6
+ * you may not use this file except in compliance with the License.
7
+ * You may obtain a copy of the License at
8
+ *
9
+ * http://www.apache.org/licenses/LICENSE-2.0
10
+ *
11
+ * Unless required by applicable law or agreed to in writing, software
12
+ * distributed under the License is distributed on an "AS IS" BASIS,
13
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
+ * See the License for the specific language governing permissions and
15
+ * limitations under the License.
16
+ */ /**
17
+ * A STORE'S STOREFRONT (AGL-3676, second round).
18
+ *
19
+ * Zach, 2026-10-09, on the beta.237 Willow Wick Candles start (job
20
+ * MtlXWjvt9P): "again this … site does not look like a store front". Its home
21
+ * was a lifestyle hero, ONE row of featured products and four text-only bands
22
+ * ("Why soy, why small batch", "How our candles are made", "Made to be
23
+ * loved", "Gifting and shipping"); its Shop page opened with a tall hero and
24
+ * the grid below the fold.
25
+ *
26
+ * A designer storefront (Shopify's Dawn, Craft, Sense; Squarespace's commerce
27
+ * templates) is composed of a handful of PARTS, in a familiar order: a hero
28
+ * that sells, the store's bestsellers, its collections as picture tiles, the
29
+ * brand's story beside a photo, why to buy here with icons, what customers
30
+ * say, and a newsletter sign-up. This module names those parts by the words a
31
+ * plan gives its sections, so the plan step can make sure a store's home has
32
+ * them and the compiler and the listings can draw each one its way.
33
+ *
34
+ * It is NOT a section library (Zach 10/7): the model still names, orders and
35
+ * writes every section, and designs each in the layout language; what code
36
+ * adds is a part the plan left out — named in plain words for the model to
37
+ * design — and the platform elements that show the store's own records.
38
+ *
39
+ * Pure: names and plans only.
40
+ */ import { _ as _extends } from "@swc/helpers/_/_extends";
41
+ /** The section names that say each part, as a plan or a page heading words it. */ export const AI_STOREFRONT_PART_WORDS = {
42
+ featured: /\b(bestsellers?|best[- ]sellers?|best[- ]selling|featured|new arrivals?|arrivals|just in|favou?rites?|signature|most loved|top picks?|our (?:candles|products|picks)|shop (?:all|now|the))\b/i,
43
+ collections: /\b(collections?|categor(?:y|ies)|shop by|browse by|by scent|scents?|ranges?|families|occasions?|edits?)\b/i,
44
+ story: /\b(story|about|our craft|craft|making|made|maker|process|how (?:we|it)|behind|studio|workshop)\b/i,
45
+ values: /\b(why|promise|promises|values?|difference|shipping|returns?|guarantee|perks?|benefits?|care|small[- ]batch|ingredients?|sustainab\w*)\b/i,
46
+ reviews: /\b(reviews?|testimonials?|customers? (?:say|love)|what (?:people|customers|our customers) say|kind words|praise|loved by|social proof)\b/i,
47
+ signup: /\b(newsletter|subscribe|sign[- ]?up|mailing list|join (?:us|the list|our list)|stay in touch|in the loop|first to know|inbox|letters? from)\b/i
48
+ };
49
+ /** Whether a planned section's name says a part. */ export function aiStorefrontSays(part, name) {
50
+ return AI_STOREFRONT_PART_WORDS[part].test(name);
51
+ }
52
+ /** The plan entry a part the plan left out is added as: plain words the page step designs. */ const ADDED = {
53
+ featured: {
54
+ name: 'Bestsellers',
55
+ uses: [],
56
+ items: 4
57
+ },
58
+ collections: {
59
+ name: 'Shop by collection',
60
+ uses: [],
61
+ items: 3
62
+ },
63
+ reviews: {
64
+ name: 'Customer reviews',
65
+ uses: [],
66
+ items: 0
67
+ },
68
+ signup: {
69
+ name: 'Newsletter sign-up',
70
+ uses: [],
71
+ items: 0
72
+ }
73
+ };
74
+ /** The most sections one page of a site start holds (`AI_SITE_MAX_SECTIONS`). */ const MOST_SECTIONS = 8;
75
+ const firstSegment = (slug)=>slug.trim().replace(/^\/+/, '').split('/')[0].toLowerCase();
76
+ /** A section that closes a page: its call to action, a contact or a sign-up band. */ const CLOSING = /\b(cta|call to action|closing|close|contact|get in touch|visit)\b/i;
77
+ /**
78
+ * A store plan's screens with the storefront a shopper expects (AGL-3676):
79
+ *
80
+ * - THE HOME (paid starts only — the Free wall prices each section, and a
81
+ * Free store sells nothing yet): its hero first, then any part the plan
82
+ * left out, added under plain names while the page has room (eight
83
+ * sections), in order of need: the bestsellers the platform lists,
84
+ * right after the hero; the collections as picture tiles after them; the
85
+ * newsletter sign-up as the page's last band; and customer reviews (the
86
+ * store's real ones, which show only once shoppers leave them) before
87
+ * the close.
88
+ * - THE SHOP PAGE: grid first. Its product grid section moves to second,
89
+ * straight under the page's short title, so the products open above the
90
+ * fold; anything the plan put between them (an intro, a story) follows
91
+ * the grid.
92
+ *
93
+ * Every section the plan named stays, in its order otherwise; nothing is
94
+ * removed, and a page at eight sections is given nothing more.
95
+ */ export function aiStorefrontPlanScreens(screens, options) {
96
+ return screens.map((screen)=>{
97
+ if (firstSegment(screen.slug) === '') return options.paid ? storefrontHome(screen) : screen;
98
+ if (!options.isShopPage(screen) || screen.sections.length < 3) return screen;
99
+ const grid = options.gridSection(screen.sections);
100
+ if (grid <= 1) return screen;
101
+ const sections = [
102
+ ...screen.sections
103
+ ];
104
+ const [moved] = sections.splice(grid, 1);
105
+ sections.splice(1, 0, moved);
106
+ return _extends({}, screen, {
107
+ sections
108
+ });
109
+ });
110
+ }
111
+ function storefrontHome(screen) {
112
+ const sections = [
113
+ ...screen.sections
114
+ ];
115
+ if (!sections.length) return screen;
116
+ // A section about the collections that also names the bestsellers is the bestsellers'.
117
+ const says = (part, name)=>aiStorefrontSays(part, name) && (part !== 'collections' || !aiStorefrontSays('featured', name));
118
+ const has = (part)=>sections.some((section, index)=>index > 0 && says(part, section.name));
119
+ const room = ()=>sections.length < MOST_SECTIONS;
120
+ // What a shopper meets first matters most, so the parts are added in that
121
+ // order while the page has room: the bestsellers, the collections, the
122
+ // sign-up, then the reviews — which show nothing until a shopper writes one.
123
+ if (!has('featured') && room()) sections.splice(1, 0, _extends({}, ADDED.featured));
124
+ if (!has('collections') && room()) {
125
+ // The collections follow the bestsellers.
126
+ const featured = sections.findIndex((section, index)=>index > 0 && says('featured', section.name));
127
+ sections.splice(featured > 0 ? featured + 1 : 1, 0, _extends({}, ADDED.collections));
128
+ }
129
+ // The newsletter is the page's last band, under any closing call to action.
130
+ if (!has('signup') && room()) sections.push(_extends({}, ADDED.signup));
131
+ if (!has('reviews') && room()) {
132
+ // The reviews come just before the page closes: above a closing sign-up
133
+ // and above a closing call to action.
134
+ let at = sections.length;
135
+ if (at - 1 > 0 && says('signup', sections[at - 1].name)) at -= 1;
136
+ if (at - 1 > 0 && CLOSING.test(sections[at - 1].name)) at -= 1;
137
+ sections.splice(at, 0, _extends({}, ADDED.reviews));
138
+ }
139
+ return sections.length === screen.sections.length ? screen : _extends({}, screen, {
140
+ sections
141
+ });
142
+ }
143
+ // ── Icons for a store's reasons to buy ────────────────────────────────────
144
+ /** Words that suggest an icon from the platform's icon library, first match wins. */ const ICON_WORDS = [
145
+ [
146
+ /\b(ship|shipping|shipped|deliver\w*|dispatch\w*|post(?:age|ed)?|courier)\b/i,
147
+ 'truck'
148
+ ],
149
+ [
150
+ /\b(returns?|refunds?|exchanges?)\b/i,
151
+ 'recycle'
152
+ ],
153
+ [
154
+ /\b(gifts?|gifting|wrapped|wrapping|boxed)\b/i,
155
+ 'gift'
156
+ ],
157
+ [
158
+ /\b(natural|soy|organic|plant|vegan|botanical|clean|non-toxic|eco|sustainab\w*|cotton|wood)\b/i,
159
+ 'leaf'
160
+ ],
161
+ [
162
+ /\b(hand|handmade|hand-poured|poured|small[- ]batch|batch|crafted|made|maker|artisan)\b/i,
163
+ 'care'
164
+ ],
165
+ [
166
+ /\b(guarantee\w*|secure|safe|safety|warranty|protected)\b/i,
167
+ 'shield'
168
+ ],
169
+ [
170
+ /\b(fast|quick|same[- ]day|next[- ]day)\b/i,
171
+ 'fast'
172
+ ],
173
+ [
174
+ /\b(quality|award\w*|premium|finest)\b/i,
175
+ 'award'
176
+ ],
177
+ [
178
+ /\b(scents?|fragrances?|aroma\w*|candles?|glow|light|burn\w*)\b/i,
179
+ 'sparkle'
180
+ ],
181
+ [
182
+ /\b(local|town|city|community)\b/i,
183
+ 'location'
184
+ ],
185
+ [
186
+ /\b(support|help|questions?|chat)\b/i,
187
+ 'chat'
188
+ ]
189
+ ];
190
+ /** The fallbacks an item takes in turn, so a row of icons never repeats one. */ const ICON_FALLBACKS = [
191
+ 'star',
192
+ 'heart',
193
+ 'sparkle',
194
+ 'check',
195
+ 'smile',
196
+ 'award'
197
+ ];
198
+ /**
199
+ * The icon each of a store's reasons to buy shows (AGL-3676): its own where
200
+ * the design named one, else one its words suggest, else the next of a few
201
+ * neutral ones — never the same twice in one row.
202
+ */ export function aiStorefrontItemIcons(items, known) {
203
+ const used = new Set();
204
+ const designed = items.map((item)=>{
205
+ if (item.icon && known(item.icon) && !used.has(item.icon)) {
206
+ used.add(item.icon);
207
+ return item.icon;
208
+ }
209
+ return '';
210
+ });
211
+ return designed.map((icon, index)=>{
212
+ var _ref;
213
+ var _ICON_WORDS_find;
214
+ if (icon) return icon;
215
+ const words = `${items[index].title} ${items[index].text}`;
216
+ const suggested = (_ICON_WORDS_find = ICON_WORDS.find(([pattern, word])=>pattern.test(words) && !used.has(word) && known(word))) == null ? void 0 : _ICON_WORDS_find[1];
217
+ const picked = (_ref = suggested != null ? suggested : ICON_FALLBACKS.find((word)=>!used.has(word) && known(word))) != null ? _ref : 'star';
218
+ used.add(picked);
219
+ return picked;
220
+ });
221
+ }
222
+
223
+ //# sourceMappingURL=ai-layout-storefront.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../../../../../../libs/plugins/ai/src/lib/layout-language/ai-layout-storefront.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 STORE'S STOREFRONT (AGL-3676, second round).\n *\n * Zach, 2026-10-09, on the beta.237 Willow Wick Candles start (job\n * MtlXWjvt9P): \"again this … site does not look like a store front\". Its home\n * was a lifestyle hero, ONE row of featured products and four text-only bands\n * (\"Why soy, why small batch\", \"How our candles are made\", \"Made to be\n * loved\", \"Gifting and shipping\"); its Shop page opened with a tall hero and\n * the grid below the fold.\n *\n * A designer storefront (Shopify's Dawn, Craft, Sense; Squarespace's commerce\n * templates) is composed of a handful of PARTS, in a familiar order: a hero\n * that sells, the store's bestsellers, its collections as picture tiles, the\n * brand's story beside a photo, why to buy here with icons, what customers\n * say, and a newsletter sign-up. This module names those parts by the words a\n * plan gives its sections, so the plan step can make sure a store's home has\n * them and the compiler and the listings can draw each one its way.\n *\n * It is NOT a section library (Zach 10/7): the model still names, orders and\n * writes every section, and designs each in the layout language; what code\n * adds is a part the plan left out — named in plain words for the model to\n * design — and the platform elements that show the store's own records.\n *\n * Pure: names and plans only.\n */\n\nimport type { AiBuildPlanScreen, AiBuildPlanSection } from '../model/ai-build-plan'\n\n/** A part a storefront home is composed of. */\nexport type AiStorefrontPart = 'featured' | 'collections' | 'story' | 'values' | 'reviews' | 'signup'\n\n/** The section names that say each part, as a plan or a page heading words it. */\nexport const AI_STOREFRONT_PART_WORDS: Readonly<Record<AiStorefrontPart, RegExp>> = {\n featured:\n /\\b(bestsellers?|best[- ]sellers?|best[- ]selling|featured|new arrivals?|arrivals|just in|favou?rites?|signature|most loved|top picks?|our (?:candles|products|picks)|shop (?:all|now|the))\\b/i,\n collections: /\\b(collections?|categor(?:y|ies)|shop by|browse by|by scent|scents?|ranges?|families|occasions?|edits?)\\b/i,\n story: /\\b(story|about|our craft|craft|making|made|maker|process|how (?:we|it)|behind|studio|workshop)\\b/i,\n values: /\\b(why|promise|promises|values?|difference|shipping|returns?|guarantee|perks?|benefits?|care|small[- ]batch|ingredients?|sustainab\\w*)\\b/i,\n reviews: /\\b(reviews?|testimonials?|customers? (?:say|love)|what (?:people|customers|our customers) say|kind words|praise|loved by|social proof)\\b/i,\n signup: /\\b(newsletter|subscribe|sign[- ]?up|mailing list|join (?:us|the list|our list)|stay in touch|in the loop|first to know|inbox|letters? from)\\b/i,\n}\n\n/** Whether a planned section's name says a part. */\nexport function aiStorefrontSays(part: AiStorefrontPart, name: string): boolean {\n return AI_STOREFRONT_PART_WORDS[part].test(name)\n}\n\n/** The plan entry a part the plan left out is added as: plain words the page step designs. */\nconst ADDED: Readonly<Record<'featured' | 'collections' | 'reviews' | 'signup', AiBuildPlanSection>> = {\n featured: { name: 'Bestsellers', uses: [], items: 4 },\n collections: { name: 'Shop by collection', uses: [], items: 3 },\n reviews: { name: 'Customer reviews', uses: [], items: 0 },\n signup: { name: 'Newsletter sign-up', uses: [], items: 0 },\n}\n\n/** The most sections one page of a site start holds (`AI_SITE_MAX_SECTIONS`). */\nconst MOST_SECTIONS = 8\n\nconst firstSegment = (slug: string) => slug.trim().replace(/^\\/+/, '').split('/')[0].toLowerCase()\n\n/** A section that closes a page: its call to action, a contact or a sign-up band. */\nconst CLOSING = /\\b(cta|call to action|closing|close|contact|get in touch|visit)\\b/i\n\n/**\n * A store plan's screens with the storefront a shopper expects (AGL-3676):\n *\n * - THE HOME (paid starts only — the Free wall prices each section, and a\n * Free store sells nothing yet): its hero first, then any part the plan\n * left out, added under plain names while the page has room (eight\n * sections), in order of need: the bestsellers the platform lists,\n * right after the hero; the collections as picture tiles after them; the\n * newsletter sign-up as the page's last band; and customer reviews (the\n * store's real ones, which show only once shoppers leave them) before\n * the close.\n * - THE SHOP PAGE: grid first. Its product grid section moves to second,\n * straight under the page's short title, so the products open above the\n * fold; anything the plan put between them (an intro, a story) follows\n * the grid.\n *\n * Every section the plan named stays, in its order otherwise; nothing is\n * removed, and a page at eight sections is given nothing more.\n */\nexport function aiStorefrontPlanScreens<S extends Pick<AiBuildPlanScreen, 'slug' | 'title' | 'sections'>>(\n screens: readonly S[],\n options: { paid: boolean; isShopPage: (screen: S) => boolean; gridSection: (sections: S['sections']) => number },\n): S[] {\n return screens.map((screen) => {\n if (firstSegment(screen.slug) === '') return options.paid ? storefrontHome(screen) : screen\n if (!options.isShopPage(screen) || screen.sections.length < 3) return screen\n const grid = options.gridSection(screen.sections)\n if (grid <= 1) return screen\n const sections = [...screen.sections]\n const [moved] = sections.splice(grid, 1)\n sections.splice(1, 0, moved)\n return { ...screen, sections }\n })\n}\n\nfunction storefrontHome<S extends Pick<AiBuildPlanScreen, 'sections'>>(screen: S): S {\n const sections = [...screen.sections]\n if (!sections.length) return screen\n // A section about the collections that also names the bestsellers is the bestsellers'.\n const says = (part: AiStorefrontPart, name: string) =>\n aiStorefrontSays(part, name) && (part !== 'collections' || !aiStorefrontSays('featured', name))\n const has = (part: AiStorefrontPart) => sections.some((section, index) => index > 0 && says(part, section.name))\n const room = () => sections.length < MOST_SECTIONS\n // What a shopper meets first matters most, so the parts are added in that\n // order while the page has room: the bestsellers, the collections, the\n // sign-up, then the reviews — which show nothing until a shopper writes one.\n if (!has('featured') && room()) sections.splice(1, 0, { ...ADDED.featured })\n if (!has('collections') && room()) {\n // The collections follow the bestsellers.\n const featured = sections.findIndex((section, index) => index > 0 && says('featured', section.name))\n sections.splice(featured > 0 ? featured + 1 : 1, 0, { ...ADDED.collections })\n }\n // The newsletter is the page's last band, under any closing call to action.\n if (!has('signup') && room()) sections.push({ ...ADDED.signup })\n if (!has('reviews') && room()) {\n // The reviews come just before the page closes: above a closing sign-up\n // and above a closing call to action.\n let at = sections.length\n if (at - 1 > 0 && says('signup', sections[at - 1].name)) at -= 1\n if (at - 1 > 0 && CLOSING.test(sections[at - 1].name)) at -= 1\n sections.splice(at, 0, { ...ADDED.reviews })\n }\n return sections.length === screen.sections.length ? screen : { ...screen, sections }\n}\n\n// ── Icons for a store's reasons to buy ────────────────────────────────────\n\n/** Words that suggest an icon from the platform's icon library, first match wins. */\nconst ICON_WORDS: ReadonlyArray<readonly [RegExp, string]> = [\n [/\\b(ship|shipping|shipped|deliver\\w*|dispatch\\w*|post(?:age|ed)?|courier)\\b/i, 'truck'],\n [/\\b(returns?|refunds?|exchanges?)\\b/i, 'recycle'],\n [/\\b(gifts?|gifting|wrapped|wrapping|boxed)\\b/i, 'gift'],\n [/\\b(natural|soy|organic|plant|vegan|botanical|clean|non-toxic|eco|sustainab\\w*|cotton|wood)\\b/i, 'leaf'],\n [/\\b(hand|handmade|hand-poured|poured|small[- ]batch|batch|crafted|made|maker|artisan)\\b/i, 'care'],\n [/\\b(guarantee\\w*|secure|safe|safety|warranty|protected)\\b/i, 'shield'],\n [/\\b(fast|quick|same[- ]day|next[- ]day)\\b/i, 'fast'],\n [/\\b(quality|award\\w*|premium|finest)\\b/i, 'award'],\n [/\\b(scents?|fragrances?|aroma\\w*|candles?|glow|light|burn\\w*)\\b/i, 'sparkle'],\n [/\\b(local|town|city|community)\\b/i, 'location'],\n [/\\b(support|help|questions?|chat)\\b/i, 'chat'],\n]\n\n/** The fallbacks an item takes in turn, so a row of icons never repeats one. */\nconst ICON_FALLBACKS = ['star', 'heart', 'sparkle', 'check', 'smile', 'award'] as const\n\n/**\n * The icon each of a store's reasons to buy shows (AGL-3676): its own where\n * the design named one, else one its words suggest, else the next of a few\n * neutral ones — never the same twice in one row.\n */\nexport function aiStorefrontItemIcons(\n items: ReadonlyArray<{ title: string; text: string; icon?: string }>,\n known: (word: string | undefined) => boolean,\n): string[] {\n const used = new Set<string>()\n const designed = items.map((item) => {\n if (item.icon && known(item.icon) && !used.has(item.icon)) {\n used.add(item.icon)\n return item.icon\n }\n return ''\n })\n return designed.map((icon, index) => {\n if (icon) return icon\n const words = `${items[index].title} ${items[index].text}`\n const suggested = ICON_WORDS.find(([pattern, word]) => pattern.test(words) && !used.has(word) && known(word))?.[1]\n const picked = suggested ?? ICON_FALLBACKS.find((word) => !used.has(word) && known(word)) ?? 'star'\n used.add(picked)\n return picked\n })\n}\n"],"names":["AI_STOREFRONT_PART_WORDS","featured","collections","story","values","reviews","signup","aiStorefrontSays","part","name","test","ADDED","uses","items","MOST_SECTIONS","firstSegment","slug","trim","replace","split","toLowerCase","CLOSING","aiStorefrontPlanScreens","screens","options","map","screen","paid","storefrontHome","isShopPage","sections","length","grid","gridSection","moved","splice","says","has","some","section","index","room","findIndex","push","at","ICON_WORDS","ICON_FALLBACKS","aiStorefrontItemIcons","known","used","Set","designed","item","icon","add","suggested","words","title","text","find","pattern","word","picked"],"mappings":"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"}
@@ -17,6 +17,7 @@
17
17
  import { type PluginAiCapability } from '@aglyn/aglyn/plugin-manager/plugin-ai-capabilities';
18
18
  import { type AiBuildItem, type AiBuildPlan, type AiBuildPlanCreate, type AiBuildPlanCreateKind, type AiBuildPlanScreen } from './ai-build-plan';
19
19
  import type { AiJobItemLedger } from './ai-jobs.types';
20
+ import { type AiCreditRange, type AiCreditsSmaller } from './ai-credit-estimate';
20
21
  /**
21
22
  * What a `build` job is (AGL-3616): one request — "a few pages, a contact
22
23
  * form and a way to book me" — planned as creations, pages and items, built
@@ -43,6 +44,14 @@ export declare const AI_BUILD_LIMITS: {
43
44
  /** A Free workspace's pages. */
44
45
  readonly freePages: 2;
45
46
  };
47
+ /**
48
+ * What a Free build's plan card says when it holds as many pages as a Free
49
+ * build makes (AGL-3722): the plan is told the cap and plans the first pages
50
+ * a request asks for, so a request for three — "About us, Custom cakes and
51
+ * Catering", 2026-10-10 — came back as two with nothing saying why. `null`
52
+ * when the plan holds fewer, or on a paid workspace.
53
+ */
54
+ export declare function aiBuildFreePageCapNote(plan: Pick<AiBuildPlan, 'screens'>, freeTaste: boolean): string | null;
46
55
  /** What a build creates through its `create` list; everything else beyond pages is an item. */
47
56
  export declare const AI_BUILD_CREATE_KINDS: readonly AiBuildPlanCreateKind[];
48
57
  /**
@@ -124,6 +133,36 @@ export declare function aiBuildCreditEstimate(plan: AiBuildPlan, options?: {
124
133
  * units, every other planned kind as `aiJobPlanCreditEstimate` counts it.
125
134
  */
126
135
  export declare function aiJobCreditEstimate(kind: string, plan: AiBuildPlan): number;
136
+ /**
137
+ * What one unit is likely to cost, its p90 and its ceiling (AGL-3722): a page
138
+ * as a page written on its own measured (a fixed cost and one per section), a creation at what its kind
139
+ * measured, an item at the measured pass times the passes its capability
140
+ * estimates (none for a draft another plugin writes, which asks no model).
141
+ * The ceiling is `aiBuildUnitCreditEstimate`, unchanged.
142
+ */
143
+ export declare function aiBuildUnitCreditRange(unit: AiBuildUnit, ops?: AiBuildOps): AiCreditRange;
144
+ /**
145
+ * What a build is likely to cost, its p90 and its ceiling (AGL-3722): every
146
+ * unit's range, or only the units named in `slots` — what Try again runs.
147
+ * The plan card, Try again and the Free admission all read this; the meter
148
+ * charges what actually runs.
149
+ */
150
+ export declare function aiBuildCreditRange(plan: AiBuildPlan, options?: {
151
+ ops?: AiBuildOps;
152
+ slots?: ReadonlySet<string>;
153
+ }): AiCreditRange;
154
+ /** `aiJobCreditEstimate` as a range (AGL-3722). */
155
+ export declare function aiJobCreditRange(kind: string, plan: AiBuildPlan): AiCreditRange;
156
+ /**
157
+ * A smaller first build (AGL-3722): the plan's home page — its first page
158
+ * where it has no home — and only what that page needs, transitively: the
159
+ * layout it is drawn in, the form and anything else its sections place.
160
+ * Every other page, and what only they needed, waits for a later request.
161
+ * `null` when the plan has no page, or when the smaller plan is the whole one.
162
+ */
163
+ export declare function aiBuildFirstPagePlan<T extends AiBuildPlan>(plan: T): T | null;
164
+ /** The smaller first build a build that does not fit is offered (AGL-3722), with its range; `null` when there is none. */
165
+ export declare function aiBuildSmaller(plan: AiBuildPlan, ops?: AiBuildOps): AiCreditsSmaller | null;
127
166
  /** A fresh ledger: one pending row per unit, first attempt. */
128
167
  export declare function aiBuildInitialLedger(units: readonly AiBuildUnit[]): AiJobItemLedger[];
129
168
  /** Whether an item is still to be built. */