@aglyn/plugins-ai 1.0.0-beta.233 → 1.0.0-beta.235

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 (199) hide show
  1. package/package.json +17 -17
  2. package/src/lib/activity/ai-activity-actions.d.ts +25 -0
  3. package/src/lib/activity/ai-activity-actions.js +85 -4
  4. package/src/lib/activity/ai-activity-actions.js.map +1 -1
  5. package/src/lib/components/ai-brief-dialog.component.js +46 -2
  6. package/src/lib/components/ai-brief-dialog.component.js.map +1 -1
  7. package/src/lib/components/ai-campaign-create.component.js +1 -1
  8. package/src/lib/components/ai-campaign-create.component.js.map +1 -1
  9. package/src/lib/components/ai-credits-card.component.js +3 -3
  10. package/src/lib/components/ai-credits-card.component.js.map +1 -1
  11. package/src/lib/components/ai-crm-email-draft.component.js +1 -1
  12. package/src/lib/components/ai-crm-email-draft.component.js.map +1 -1
  13. package/src/lib/components/ai-crm-import-mapping.component.js +1 -1
  14. package/src/lib/components/ai-crm-import-mapping.component.js.map +1 -1
  15. package/src/lib/components/ai-crm-record-card.component.js +1 -1
  16. package/src/lib/components/ai-crm-record-card.component.js.map +1 -1
  17. package/src/lib/components/ai-describe-automation.component.js +4 -2
  18. package/src/lib/components/ai-describe-automation.component.js.map +1 -1
  19. package/src/lib/components/ai-describe-org-automation.component.js +1 -1
  20. package/src/lib/components/ai-describe-org-automation.component.js.map +1 -1
  21. package/src/lib/components/ai-experiment-cards.component.js +2 -2
  22. package/src/lib/components/ai-experiment-cards.component.js.map +1 -1
  23. package/src/lib/components/ai-explain-automation.component.js +1 -1
  24. package/src/lib/components/ai-explain-automation.component.js.map +1 -1
  25. package/src/lib/components/ai-insight-card.component.js +3 -1
  26. package/src/lib/components/ai-insight-card.component.js.map +1 -1
  27. package/src/lib/components/ai-job-links.d.ts +6 -0
  28. package/src/lib/components/ai-job-links.js +9 -0
  29. package/src/lib/components/ai-job-links.js.map +1 -1
  30. package/src/lib/components/ai-jobs-list-page.component.d.ts +6 -1
  31. package/src/lib/components/ai-jobs-list-page.component.js +15 -2
  32. package/src/lib/components/ai-jobs-list-page.component.js.map +1 -1
  33. package/src/lib/components/ai-logic.component.js +5 -3
  34. package/src/lib/components/ai-logic.component.js.map +1 -1
  35. package/src/lib/components/ai-media-create.component.js +88 -16
  36. package/src/lib/components/ai-media-create.component.js.map +1 -1
  37. package/src/lib/components/ai-model-selector.component.js +8 -0
  38. package/src/lib/components/ai-model-selector.component.js.map +1 -1
  39. package/src/lib/components/ai-overlay-cards.component.js +2 -2
  40. package/src/lib/components/ai-overlay-cards.component.js.map +1 -1
  41. package/src/lib/components/ai-product-copy-card.component.js +2 -1
  42. package/src/lib/components/ai-product-copy-card.component.js.map +1 -1
  43. package/src/lib/components/ai-products-hub-card.component.js +2 -1
  44. package/src/lib/components/ai-products-hub-card.component.js.map +1 -1
  45. package/src/lib/components/ai-revise-automation.component.js +1 -1
  46. package/src/lib/components/ai-revise-automation.component.js.map +1 -1
  47. package/src/lib/components/ai-seo-fields-card.component.js +7 -3
  48. package/src/lib/components/ai-seo-fields-card.component.js.map +1 -1
  49. package/src/lib/components/ai-site-batch-card.component.js +7 -4
  50. package/src/lib/components/ai-site-batch-card.component.js.map +1 -1
  51. package/src/lib/components/ai-site-build-page.component.js +48 -6
  52. package/src/lib/components/ai-site-build-page.component.js.map +1 -1
  53. package/src/lib/components/ai-site-seo-start-card.component.js +7 -4
  54. package/src/lib/components/ai-site-seo-start-card.component.js.map +1 -1
  55. package/src/lib/components/ai-site-start-card.component.js +152 -36
  56. package/src/lib/components/ai-site-start-card.component.js.map +1 -1
  57. package/src/lib/components/ai-theme-proposal-card.component.js +1 -0
  58. package/src/lib/components/ai-theme-proposal-card.component.js.map +1 -1
  59. package/src/lib/components/ai-upsell-dialog.component.js +17 -3
  60. package/src/lib/components/ai-upsell-dialog.component.js.map +1 -1
  61. package/src/lib/components/ai-usage-strip.component.js +13 -2
  62. package/src/lib/components/ai-usage-strip.component.js.map +1 -1
  63. package/src/lib/components/assist-build-card.component.js +20 -3
  64. package/src/lib/components/assist-build-card.component.js.map +1 -1
  65. package/src/lib/components/assist-jobs-drawer.component.js +7 -1
  66. package/src/lib/components/assist-jobs-drawer.component.js.map +1 -1
  67. package/src/lib/components/billing-ai-allotments.component.js +2 -2
  68. package/src/lib/components/billing-ai-allotments.component.js.map +1 -1
  69. package/src/lib/components/billing-ai-top-users.component.js +1 -0
  70. package/src/lib/components/billing-ai-top-users.component.js.map +1 -1
  71. package/src/lib/components/billing-assist-overage-card.component.js +2 -2
  72. package/src/lib/components/billing-assist-overage-card.component.js.map +1 -1
  73. package/src/lib/components/host-ai-allotments.component.js +2 -2
  74. package/src/lib/components/host-ai-allotments.component.js.map +1 -1
  75. package/src/lib/components/member-ai-allotment-card.component.js +2 -2
  76. package/src/lib/components/member-ai-allotment-card.component.js.map +1 -1
  77. package/src/lib/components/staff-org-ai-card.component.js +1 -0
  78. package/src/lib/components/staff-org-ai-card.component.js.map +1 -1
  79. package/src/lib/components/staff-org-ai-conversations.component.d.ts +9 -3
  80. package/src/lib/components/staff-org-ai-conversations.component.js +174 -83
  81. package/src/lib/components/staff-org-ai-conversations.component.js.map +1 -1
  82. package/src/lib/components/staff-user-ai-usage-card.component.js +2 -1
  83. package/src/lib/components/staff-user-ai-usage-card.component.js.map +1 -1
  84. package/src/lib/jobs/ai-job-admission.d.ts +2 -1
  85. package/src/lib/jobs/ai-job-admission.js.map +1 -1
  86. package/src/lib/jobs/ai-job-email-step.d.ts +39 -0
  87. package/src/lib/jobs/ai-job-email-step.js +116 -5
  88. package/src/lib/jobs/ai-job-email-step.js.map +1 -1
  89. package/src/lib/jobs/ai-job-layout-language.d.ts +3 -0
  90. package/src/lib/jobs/ai-job-layout-language.js +6 -1
  91. package/src/lib/jobs/ai-job-layout-language.js.map +1 -1
  92. package/src/lib/jobs/ai-job-layout-site-pages.d.ts +14 -0
  93. package/src/lib/jobs/ai-job-layout-site-pages.js +43 -11
  94. package/src/lib/jobs/ai-job-layout-site-pages.js.map +1 -1
  95. package/src/lib/jobs/ai-job-layout-step.js +3 -2
  96. package/src/lib/jobs/ai-job-layout-step.js.map +1 -1
  97. package/src/lib/jobs/ai-job-page-language.d.ts +26 -1
  98. package/src/lib/jobs/ai-job-page-language.js +75 -13
  99. package/src/lib/jobs/ai-job-page-language.js.map +1 -1
  100. package/src/lib/jobs/ai-job-page-step.d.ts +3 -0
  101. package/src/lib/jobs/ai-job-page-step.js +32 -7
  102. package/src/lib/jobs/ai-job-page-step.js.map +1 -1
  103. package/src/lib/jobs/ai-job-plan-step.d.ts +34 -0
  104. package/src/lib/jobs/ai-job-plan-step.js +106 -14
  105. package/src/lib/jobs/ai-job-plan-step.js.map +1 -1
  106. package/src/lib/jobs/ai-job-site-content.d.ts +81 -13
  107. package/src/lib/jobs/ai-job-site-content.js +210 -43
  108. package/src/lib/jobs/ai-job-site-content.js.map +1 -1
  109. package/src/lib/jobs/ai-job-site-step.d.ts +19 -0
  110. package/src/lib/jobs/ai-job-site-step.js +152 -51
  111. package/src/lib/jobs/ai-job-site-step.js.map +1 -1
  112. package/src/lib/jobs/ai-jobs.d.ts +8 -4
  113. package/src/lib/jobs/ai-jobs.js +19 -2
  114. package/src/lib/jobs/ai-jobs.js.map +1 -1
  115. package/src/lib/jobs/ai-layout-stock-photos.d.ts +98 -0
  116. package/src/lib/jobs/ai-layout-stock-photos.js +248 -0
  117. package/src/lib/jobs/ai-layout-stock-photos.js.map +1 -0
  118. package/src/lib/jobs/ai-site-publish.d.ts +11 -0
  119. package/src/lib/jobs/ai-site-publish.js +31 -1
  120. package/src/lib/jobs/ai-site-publish.js.map +1 -1
  121. package/src/lib/jobs/fixtures/ai-juniper-portfolio-page-recording.d.ts +151 -0
  122. package/src/lib/jobs/fixtures/ai-juniper-portfolio-page-recording.js +183 -0
  123. package/src/lib/jobs/fixtures/ai-juniper-portfolio-page-recording.js.map +1 -0
  124. package/src/lib/jobs/fixtures/ai-slow-roads-site-plan-recording.d.ts +27 -0
  125. package/src/lib/jobs/fixtures/ai-slow-roads-site-plan-recording.js +192 -0
  126. package/src/lib/jobs/fixtures/ai-slow-roads-site-plan-recording.js.map +1 -0
  127. package/src/lib/jobs/fixtures/ai-yoga-site-plan-recording.d.ts +27 -0
  128. package/src/lib/jobs/fixtures/ai-yoga-site-plan-recording.js +222 -0
  129. package/src/lib/jobs/fixtures/ai-yoga-site-plan-recording.js.map +1 -0
  130. package/src/lib/layout-language/ai-layout-compiler.d.ts +37 -0
  131. package/src/lib/layout-language/ai-layout-compiler.js +1003 -19
  132. package/src/lib/layout-language/ai-layout-compiler.js.map +1 -1
  133. package/src/lib/layout-language/ai-layout-design.d.ts +114 -0
  134. package/src/lib/layout-language/ai-layout-design.js +209 -0
  135. package/src/lib/layout-language/ai-layout-design.js.map +1 -0
  136. package/src/lib/layout-language/ai-layout-frame.d.ts +12 -0
  137. package/src/lib/layout-language/ai-layout-frame.js +33 -8
  138. package/src/lib/layout-language/ai-layout-frame.js.map +1 -1
  139. package/src/lib/layout-language/ai-layout-gaps.js +18 -2
  140. package/src/lib/layout-language/ai-layout-gaps.js.map +1 -1
  141. package/src/lib/layout-language/ai-layout-language.d.ts +2 -0
  142. package/src/lib/layout-language/ai-layout-language.js +15 -1
  143. package/src/lib/layout-language/ai-layout-language.js.map +1 -1
  144. package/src/lib/layout-language/ai-layout-links.d.ts +36 -0
  145. package/src/lib/layout-language/ai-layout-links.js +26 -2
  146. package/src/lib/layout-language/ai-layout-links.js.map +1 -1
  147. package/src/lib/layout-language/ai-layout-listings.d.ts +125 -0
  148. package/src/lib/layout-language/ai-layout-listings.js +187 -0
  149. package/src/lib/layout-language/ai-layout-listings.js.map +1 -0
  150. package/src/lib/layout-language/ai-layout-pictures.d.ts +11 -5
  151. package/src/lib/layout-language/ai-layout-pictures.js +80 -17
  152. package/src/lib/layout-language/ai-layout-pictures.js.map +1 -1
  153. package/src/lib/model/ai-job-notice.d.ts +9 -1
  154. package/src/lib/model/ai-job-notice.js +23 -0
  155. package/src/lib/model/ai-job-notice.js.map +1 -1
  156. package/src/lib/model/ai-plan-capabilities.d.ts +7 -0
  157. package/src/lib/model/ai-plan-capabilities.js.map +1 -1
  158. package/src/lib/model/ai-site-build-progress.d.ts +12 -2
  159. package/src/lib/model/ai-site-build-progress.js +104 -15
  160. package/src/lib/model/ai-site-build-progress.js.map +1 -1
  161. package/src/lib/model/ai-site-job.d.ts +236 -24
  162. package/src/lib/model/ai-site-job.js +269 -22
  163. package/src/lib/model/ai-site-job.js.map +1 -1
  164. package/src/lib/model/ai-site-kinds.d.ts +30 -8
  165. package/src/lib/model/ai-site-kinds.js +735 -116
  166. package/src/lib/model/ai-site-kinds.js.map +1 -1
  167. package/src/lib/model/ai-site-look.d.ts +21 -0
  168. package/src/lib/model/ai-site-look.js +63 -14
  169. package/src/lib/model/ai-site-look.js.map +1 -1
  170. package/src/lib/model/ai-site-start-seo.d.ts +30 -0
  171. package/src/lib/model/ai-site-start-seo.js +132 -11
  172. package/src/lib/model/ai-site-start-seo.js.map +1 -1
  173. package/src/lib/model/ai-site-start.d.ts +2 -2
  174. package/src/lib/model/ai-site-start.js +3 -3
  175. package/src/lib/model/ai-site-start.js.map +1 -1
  176. package/src/lib/plugin.js +15 -3
  177. package/src/lib/plugin.js.map +1 -1
  178. package/src/lib/runtime/ai-doctrine-validators.d.ts +32 -1
  179. package/src/lib/runtime/ai-doctrine-validators.js +227 -4
  180. package/src/lib/runtime/ai-doctrine-validators.js.map +1 -1
  181. package/src/lib/runtime/ai-doctrine.js +5 -3
  182. package/src/lib/runtime/ai-doctrine.js.map +1 -1
  183. package/src/lib/runtime/ai-palette.generated.d.ts +2 -0
  184. package/src/lib/runtime/ai-palette.generated.js +31 -5
  185. package/src/lib/runtime/ai-palette.generated.js.map +1 -1
  186. package/src/lib/server/ai-admin-conversations.d.ts +9 -0
  187. package/src/lib/server/ai-admin-conversations.js +151 -9
  188. package/src/lib/server/ai-admin-conversations.js.map +1 -1
  189. package/src/lib/server/ai-jobs-resume.js.map +1 -1
  190. package/src/lib/server/ai-jobs-route.js +15 -3
  191. package/src/lib/server/ai-jobs-route.js.map +1 -1
  192. package/src/lib/server/assist-edit.js +5 -2
  193. package/src/lib/server/assist-edit.js.map +1 -1
  194. package/src/lib/usage/free-ai-credits-left.d.ts +54 -0
  195. package/src/lib/usage/free-ai-credits-left.js +62 -0
  196. package/src/lib/usage/free-ai-credits-left.js.map +1 -0
  197. package/src/lib/usage/staff-org-ai-conversations.d.ts +23 -0
  198. package/src/lib/usage/staff-org-ai-conversations.js +45 -0
  199. package/src/lib/usage/staff-org-ai-conversations.js.map +1 -1
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../../../../../libs/plugins/ai/src/lib/layout-language/ai-layout-links.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 { aiPageLinkSection } from '../runtime/ai-page-links'\n\n/**\n * Where a link the layout language names goes (AGL-3660), resolved by code to\n * a destination rule 10 admits: a page of this site by its id, a section of\n * this page by the platform's Scroll to element interaction, or an `https:`\n * address the brief itself gave. A destination that resolves to nothing, or\n * to the home page under words that do not say home, is no destination, and\n * the compiler leaves the button out rather than shipping a dead control.\n */\n\n/** A page of the site a link may go to: built, or minted on a guided start's plan. */\nexport interface AiLayoutPage {\n id: string\n label: string\n slug: string\n}\n\n/** Everything a page's links resolve against. */\nexport interface AiLayoutTargets {\n /** The page being built, which no link of its own goes to. */\n pageId?: string | null\n /** The site's pages a link may go to. */\n pages: readonly AiLayoutPage[]\n /** The site's home pages, which only words that say home link (rule 10). */\n homeIds: readonly string[]\n /** The saved forms a page may place. */\n forms: readonly { id: string; name: string }[]\n /** The page that places the site's form, where that is another page. */\n formPageId?: string | null\n /** The reusable components a page may place, with the props each fills. */\n components: readonly {\n id: string\n name: string\n props: Record<string, string>\n }[]\n /** Every word the job was given: the brief, the site's answers, its profile. */\n facts: string\n}\n\n/** Where a link resolved to. */\nexport type AiLayoutDestination =\n | { kind: 'page'; screenId: string }\n | { kind: 'section'; index: number }\n | { kind: 'href'; href: string }\n\n/** The words of a label or a name, as they are compared. */\nfunction wordsOf(text: string): string[] {\n return text\n .toLowerCase()\n .split(/[^\\p{L}\\p{N}]+/u)\n .filter(Boolean)\n}\n\nfunction slugOf(slug: string): string {\n return `/${slug.replace(/^\\/+|\\/+$/g, '')}`.toLowerCase()\n}\n\nconst HOME_WORDS = /\\bhome(?:\\s?page)?\\b/i\n\n/** Words that ask a visitor to get in touch, which the site's form answers. */\nconst CONTACT_WORDS =\n /\\b(?:contact|book|booking|call|quote|estimate|appointment|schedule|reserve|reservation|get in touch|request|visit|enquire|inquire|enquiry|inquiry|message|talk|consult|consultation|order|sign up|join|apply|start)\\b/i\n\nexport interface AiLayoutLinkScope {\n /** The page's section names in plan order. */\n sections: readonly string[]\n /** The section the link sits in, which it does not scroll to. */\n from: number\n /** The section of this page that places the site's form, if one does. */\n formSection: number | null\n}\n\n/** The page a word names: an id, a label or an address. */\nfunction pageNamed(\n word: string,\n targets: AiLayoutTargets,\n): AiLayoutPage | null {\n const value = word.trim()\n if (!value) return null\n const byId = targets.pages.find((page) => page.id === value)\n if (byId) return byId\n const lower = value.toLowerCase()\n if (lower.startsWith('/'))\n return (\n targets.pages.find((page) => slugOf(page.slug) === slugOf(lower)) ?? null\n )\n const words = wordsOf(value).join(' ')\n return (\n targets.pages.find((page) => wordsOf(page.label).join(' ') === words) ??\n null\n )\n}\n\n/** A destination rule 10 admits, from the page it names, for the words a link shows. */\nfunction pageDestination(\n page: AiLayoutPage | null,\n label: string,\n targets: AiLayoutTargets,\n): AiLayoutDestination | null {\n if (!page || page.id === targets.pageId) return null\n if (\n targets.homeIds.includes(page.id) &&\n !HOME_WORDS.test(label) &&\n !isHomeSlug(page.slug)\n )\n return null\n if (isHomeSlug(page.slug) && !HOME_WORDS.test(label)) return null\n return { kind: 'page', screenId: page.id }\n}\n\nfunction isHomeSlug(slug: string): boolean {\n return slugOf(slug) === '/'\n}\n\n/** Where the site's form is: a section of this page, else the page that places it. */\nfunction formDestination(\n scope: AiLayoutLinkScope,\n targets: AiLayoutTargets,\n): AiLayoutDestination | null {\n if (scope.formSection !== null && scope.formSection !== scope.from)\n return { kind: 'section', index: scope.formSection }\n if (targets.formPageId && targets.formPageId !== targets.pageId) {\n return { kind: 'page', screenId: targets.formPageId }\n }\n return null\n}\n\n/**\n * Where a link goes: the destination its `to` names, read the language's\n * way; failing that, for words that ask a visitor to get in touch, the site's\n * form; failing that, a page whose name the words say. `null` is a link with\n * nowhere to go, which is left out.\n */\nexport function aiLayoutResolveLink(\n to: string | undefined,\n label: string,\n scope: AiLayoutLinkScope,\n targets: AiLayoutTargets,\n): AiLayoutDestination | null {\n const named = to ? resolveNamed(to.trim(), label, scope, targets) : null\n if (named) return named\n if (CONTACT_WORDS.test(label)) {\n const form = formDestination(scope, targets)\n if (form) return form\n const contact = targets.pages.find(\n (page) =>\n page.id !== targets.pageId &&\n !isHomeSlug(page.slug) &&\n CONTACT_WORDS.test(page.label),\n )\n if (contact) return { kind: 'page', screenId: contact.id }\n }\n const labelWords = new Set(wordsOf(label))\n const said = targets.pages.filter(\n (page) =>\n page.id !== targets.pageId &&\n !isHomeSlug(page.slug) &&\n wordsOf(page.label).length > 0 &&\n wordsOf(page.label).every((word) => labelWords.has(word)),\n )\n return said.length === 1 ? { kind: 'page', screenId: said[0].id } : null\n}\n\nfunction resolveNamed(\n to: string,\n label: string,\n scope: AiLayoutLinkScope,\n targets: AiLayoutTargets,\n): AiLayoutDestination | null {\n const lower = to.toLowerCase()\n if (\n lower === 'form' ||\n lower.startsWith('form:') ||\n targets.forms.some((form) => form.id === to)\n ) {\n return formDestination(scope, targets)\n }\n if (lower.startsWith('https://')) {\n return targets.facts.includes(to) ? { kind: 'href', href: to } : null\n }\n const section = lower.startsWith('#')\n ? to.slice(1)\n : lower.startsWith('section:')\n ? to.slice(8)\n : null\n if (section !== null) {\n const number = Number(section.trim())\n const index =\n Number.isInteger(number) && number >= 1 && number <= scope.sections.length\n ? number - 1\n : aiPageLinkSection(section, scope.sections)\n return index !== null && index !== scope.from\n ? { kind: 'section', index }\n : null\n }\n const page = pageNamed(lower.startsWith('page:') ? to.slice(5) : to, targets)\n return pageDestination(page, label, targets)\n}\n"],"names":["aiPageLinkSection","wordsOf","text","toLowerCase","split","filter","Boolean","slugOf","slug","replace","HOME_WORDS","CONTACT_WORDS","pageNamed","word","targets","value","trim","byId","pages","find","page","id","lower","startsWith","words","join","label","pageDestination","pageId","homeIds","includes","test","isHomeSlug","kind","screenId","formDestination","scope","formSection","from","index","formPageId","aiLayoutResolveLink","to","named","resolveNamed","form","contact","labelWords","Set","said","length","every","has","forms","some","facts","href","section","slice","number","Number","isInteger","sections"],"mappings":"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"}
1
+ {"version":3,"sources":["../../../../../../../libs/plugins/ai/src/lib/layout-language/ai-layout-links.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 { aiPageLinkSection } from '../runtime/ai-page-links'\nimport type { AiLayoutListing } from './ai-layout-listings'\n\n/**\n * Where a link the layout language names goes (AGL-3660), resolved by code to\n * a destination rule 10 admits: a page of this site by its id, a section of\n * this page by the platform's Scroll to element interaction, or an `https:`\n * address the brief itself gave. A destination that resolves to nothing, or\n * to the home page under words that do not say home, is no destination, and\n * the compiler leaves the button out rather than shipping a dead control.\n */\n\n/** A page of the site a link may go to: built, or minted on a guided start's plan. */\nexport interface AiLayoutPage {\n id: string\n label: string\n slug: string\n /**\n * A destination on the site that is no page of its own, linked by its path:\n * the blog a guided start writes its posts into (AGL-3660), whose `id` is\n * no screen. Absent for every page.\n */\n href?: string\n /**\n * The title of a planned page merged into this destination (AGL-3676): a\n * page that stood in for the blog, kept only so links to it go to the blog.\n * A link to it is named for the blog, not for the page it replaced.\n */\n standsInFor?: string\n}\n\n/** Everything a page's links resolve against. */\nexport interface AiLayoutTargets {\n /** The page being built, which no link of its own goes to. */\n pageId?: string | null\n /** The site's pages a link may go to. */\n pages: readonly AiLayoutPage[]\n /** The site's home pages, which only words that say home link (rule 10). */\n homeIds: readonly string[]\n /** The saved forms a page may place. */\n forms: readonly { id: string; name: string }[]\n /** The page that places the site's form, where that is another page. */\n formPageId?: string | null\n /** The reusable components a page may place, with the props each fills. */\n components: readonly {\n id: string\n name: string\n props: Record<string, string>\n }[]\n /** Every word the job was given: the brief, the site's answers, its profile. */\n facts: string\n /**\n * The kinds of record the site keeps that its pages show — its catalog,\n * its blog — and the sections that place each (AGL-3676,\n * `ai-layout-listings.ts`). Absent or empty, a page shows only its words.\n */\n listings?: readonly AiLayoutListing[]\n}\n\n/** Where a link resolved to. */\nexport type AiLayoutDestination =\n | { kind: 'page'; screenId: string }\n | { kind: 'section'; index: number }\n | { kind: 'href'; href: string }\n /**\n * A path on this site that is no page of its own, such as the blog\n * (AGL-3660); reached through a planned page merged into it, the title\n * that page had and the name it now goes by (AGL-3676).\n */\n | { kind: 'path'; href: string; standsInFor?: string; label?: string }\n\n/**\n * A link's words for where it now goes (AGL-3676): a button that named a\n * page merged into the blog — \"Read the articles\" — names the blog instead,\n * \"Read the blog\". Words that name neither are kept.\n */\nexport function aiLayoutRenamedLabel(label: string, destination: AiLayoutDestination): string {\n if (destination.kind !== 'path' || !destination.standsInFor || !destination.label) return label\n const name = destination.label.toLowerCase()\n const words = wordsOf(destination.standsInFor)\n if (!words.length) return label\n // The page's own name, or its words alone, singular or plural.\n const stems = words.map((word) => word.replace(/s$/, ''))\n const pattern = new RegExp(`\\\\b(?:${stems.map((stem) => stem.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\$&')).join('\\\\s+')})s?\\\\b`, 'giu')\n const renamed = label.replace(pattern, (found) => (found[0] === found[0].toUpperCase() ? destination.label as string : name))\n return renamed\n}\n\n/** The words of a label or a name, as they are compared. */\nfunction wordsOf(text: string): string[] {\n return text\n .toLowerCase()\n .split(/[^\\p{L}\\p{N}]+/u)\n .filter(Boolean)\n}\n\nfunction slugOf(slug: string): string {\n return `/${slug.replace(/^\\/+|\\/+$/g, '')}`.toLowerCase()\n}\n\nconst HOME_WORDS = /\\bhome(?:\\s?page)?\\b/i\n\n/** Words that ask a visitor to get in touch, which the site's form answers. */\nconst CONTACT_WORDS =\n /\\b(?:contact|book|booking|call|quote|estimate|appointment|schedule|reserve|reservation|get in touch|request|visit|enquire|inquire|enquiry|inquiry|message|talk|consult|consultation|order|sign up|join|apply|start)\\b/i\n\nexport interface AiLayoutLinkScope {\n /** The page's section names in plan order. */\n sections: readonly string[]\n /** The section the link sits in, which it does not scroll to. */\n from: number\n /** The section of this page that places the site's form, if one does. */\n formSection: number | null\n}\n\n/** The page a word names: an id, a label or an address. */\nfunction pageNamed(\n word: string,\n targets: AiLayoutTargets,\n): AiLayoutPage | null {\n const value = word.trim()\n if (!value) return null\n const byId = targets.pages.find((page) => page.id === value)\n if (byId) return byId\n const lower = value.toLowerCase()\n if (lower.startsWith('/'))\n return (\n targets.pages.find((page) => slugOf(page.slug) === slugOf(lower)) ?? null\n )\n const words = wordsOf(value).join(' ')\n return (\n targets.pages.find((page) => wordsOf(page.label).join(' ') === words) ??\n // A page merged into the blog is still named by the title it had (AGL-3676).\n targets.pages.find((page) => page.standsInFor && wordsOf(page.standsInFor).join(' ') === words) ??\n null\n )\n}\n\n/** A destination rule 10 admits, from the page it names, for the words a link shows. */\nfunction pageDestination(\n page: AiLayoutPage | null,\n label: string,\n targets: AiLayoutTargets,\n): AiLayoutDestination | null {\n if (!page || page.id === targets.pageId) return null\n if (\n targets.homeIds.includes(page.id) &&\n !HOME_WORDS.test(label) &&\n !isHomeSlug(page.slug)\n )\n return null\n if (isHomeSlug(page.slug) && !HOME_WORDS.test(label)) return null\n if (page.href) return { kind: 'path', href: page.href, ...(page.standsInFor ? { standsInFor: page.standsInFor, label: page.label } : {}) }\n return { kind: 'page', screenId: page.id }\n}\n\nfunction isHomeSlug(slug: string): boolean {\n return slugOf(slug) === '/'\n}\n\n/** Where the site's form is: a section of this page, else the page that places it. */\nfunction formDestination(\n scope: AiLayoutLinkScope,\n targets: AiLayoutTargets,\n): AiLayoutDestination | null {\n if (scope.formSection !== null && scope.formSection !== scope.from)\n return { kind: 'section', index: scope.formSection }\n if (targets.formPageId && targets.formPageId !== targets.pageId) {\n return { kind: 'page', screenId: targets.formPageId }\n }\n return null\n}\n\n/**\n * Where a link goes: the destination its `to` names, read the language's\n * way; failing that, for words that ask a visitor to get in touch, the site's\n * form; failing that, a page whose name the words say. `null` is a link with\n * nowhere to go, which is left out.\n */\nexport function aiLayoutResolveLink(\n to: string | undefined,\n label: string,\n scope: AiLayoutLinkScope,\n targets: AiLayoutTargets,\n): AiLayoutDestination | null {\n const named = to ? resolveNamed(to.trim(), label, scope, targets) : null\n if (named) return named\n if (CONTACT_WORDS.test(label)) {\n const form = formDestination(scope, targets)\n if (form) return form\n const contact = targets.pages.find(\n (page) =>\n page.id !== targets.pageId &&\n !isHomeSlug(page.slug) &&\n CONTACT_WORDS.test(page.label),\n )\n if (contact) return { kind: 'page', screenId: contact.id }\n }\n const labelWords = new Set(wordsOf(label))\n const said = targets.pages.filter(\n (page) =>\n page.id !== targets.pageId &&\n !isHomeSlug(page.slug) &&\n wordsOf(page.label).length > 0 &&\n wordsOf(page.label).every((word) => labelWords.has(word)),\n )\n return said.length === 1 ? { kind: 'page', screenId: said[0].id } : null\n}\n\nfunction resolveNamed(\n to: string,\n label: string,\n scope: AiLayoutLinkScope,\n targets: AiLayoutTargets,\n): AiLayoutDestination | null {\n const lower = to.toLowerCase()\n if (\n lower === 'form' ||\n lower.startsWith('form:') ||\n targets.forms.some((form) => form.id === to)\n ) {\n return formDestination(scope, targets)\n }\n if (lower.startsWith('https://')) {\n return targets.facts.includes(to) ? { kind: 'href', href: to } : null\n }\n const section = lower.startsWith('#')\n ? to.slice(1)\n : lower.startsWith('section:')\n ? to.slice(8)\n : null\n if (section !== null) {\n const number = Number(section.trim())\n const index =\n Number.isInteger(number) && number >= 1 && number <= scope.sections.length\n ? number - 1\n : aiPageLinkSection(section, scope.sections)\n return index !== null && index !== scope.from\n ? { kind: 'section', index }\n : null\n }\n const page = pageNamed(lower.startsWith('page:') ? to.slice(5) : to, targets)\n return pageDestination(page, label, targets)\n}\n"],"names":["aiPageLinkSection","aiLayoutRenamedLabel","label","destination","kind","standsInFor","name","toLowerCase","words","wordsOf","length","stems","map","word","replace","pattern","RegExp","stem","join","renamed","found","toUpperCase","text","split","filter","Boolean","slugOf","slug","HOME_WORDS","CONTACT_WORDS","pageNamed","targets","value","trim","byId","pages","find","page","id","lower","startsWith","pageDestination","pageId","homeIds","includes","test","isHomeSlug","href","screenId","formDestination","scope","formSection","from","index","formPageId","aiLayoutResolveLink","to","named","resolveNamed","form","contact","labelWords","Set","said","every","has","forms","some","facts","section","slice","number","Number","isInteger","sections"],"mappings":";AAAA;;;;;;;;;;;;;;;CAeC,GAED,SAASA,iBAAiB,QAAQ,8BAA0B;AAuE5D;;;;CAIC,GACD,OAAO,SAASC,qBAAqBC,KAAa,EAAEC,WAAgC;IAClF,IAAIA,YAAYC,IAAI,KAAK,UAAU,CAACD,YAAYE,WAAW,IAAI,CAACF,YAAYD,KAAK,EAAE,OAAOA;IAC1F,MAAMI,OAAOH,YAAYD,KAAK,CAACK,WAAW;IAC1C,MAAMC,QAAQC,QAAQN,YAAYE,WAAW;IAC7C,IAAI,CAACG,MAAME,MAAM,EAAE,OAAOR;IAC1B,+DAA+D;IAC/D,MAAMS,QAAQH,MAAMI,GAAG,CAAC,CAACC,OAASA,KAAKC,OAAO,CAAC,MAAM;IACrD,MAAMC,UAAU,IAAIC,OAAO,CAAC,MAAM,EAAEL,MAAMC,GAAG,CAAC,CAACK,OAASA,KAAKH,OAAO,CAAC,uBAAuB,SAASI,IAAI,CAAC,QAAQ,MAAM,CAAC,EAAE;IAC3H,MAAMC,UAAUjB,MAAMY,OAAO,CAACC,SAAS,CAACK,QAAWA,KAAK,CAAC,EAAE,KAAKA,KAAK,CAAC,EAAE,CAACC,WAAW,KAAKlB,YAAYD,KAAK,GAAaI;IACvH,OAAOa;AACT;AAEA,0DAA0D,GAC1D,SAASV,QAAQa,IAAY;IAC3B,OAAOA,KACJf,WAAW,GACXgB,KAAK,CAAC,8maACNC,MAAM,CAACC;AACZ;AAEA,SAASC,OAAOC,IAAY;IAC1B,OAAO,CAAC,CAAC,EAAEA,KAAKb,OAAO,CAAC,cAAc,KAAK,CAACP,WAAW;AACzD;AAEA,MAAMqB,aAAa;AAEnB,6EAA6E,GAC7E,MAAMC,gBACJ;AAWF,yDAAyD,GACzD,SAASC,UACPjB,IAAY,EACZkB,OAAwB;QASpBA,qBAIFA,MAAAA;IAXF,MAAMC,QAAQnB,KAAKoB,IAAI;IACvB,IAAI,CAACD,OAAO,OAAO;IACnB,MAAME,OAAOH,QAAQI,KAAK,CAACC,IAAI,CAAC,CAACC,OAASA,KAAKC,EAAE,KAAKN;IACtD,IAAIE,MAAM,OAAOA;IACjB,MAAMK,QAAQP,MAAMzB,WAAW;IAC/B,IAAIgC,MAAMC,UAAU,CAAC,MACnB,QACET,sBAAAA,QAAQI,KAAK,CAACC,IAAI,CAAC,CAACC,OAASX,OAAOW,KAAKV,IAAI,MAAMD,OAAOa,mBAA1DR,sBAAqE;IAEzE,MAAMvB,QAAQC,QAAQuB,OAAOd,IAAI,CAAC;IAClC,QACEa,QAAAA,uBAAAA,QAAQI,KAAK,CAACC,IAAI,CAAC,CAACC,OAAS5B,QAAQ4B,KAAKnC,KAAK,EAAEgB,IAAI,CAAC,SAASV,kBAA/DuB,uBACA,6EAA6E;IAC7EA,QAAQI,KAAK,CAACC,IAAI,CAAC,CAACC,OAASA,KAAKhC,WAAW,IAAII,QAAQ4B,KAAKhC,WAAW,EAAEa,IAAI,CAAC,SAASV,kBAFzFuB,OAGA;AAEJ;AAEA,sFAAsF,GACtF,SAASU,gBACPJ,IAAyB,EACzBnC,KAAa,EACb6B,OAAwB;IAExB,IAAI,CAACM,QAAQA,KAAKC,EAAE,KAAKP,QAAQW,MAAM,EAAE,OAAO;IAChD,IACEX,QAAQY,OAAO,CAACC,QAAQ,CAACP,KAAKC,EAAE,KAChC,CAACV,WAAWiB,IAAI,CAAC3C,UACjB,CAAC4C,WAAWT,KAAKV,IAAI,GAErB,OAAO;IACT,IAAImB,WAAWT,KAAKV,IAAI,KAAK,CAACC,WAAWiB,IAAI,CAAC3C,QAAQ,OAAO;IAC7D,IAAImC,KAAKU,IAAI,EAAE,OAAO;QAAE3C,MAAM;QAAQ2C,MAAMV,KAAKU,IAAI;OAAMV,KAAKhC,WAAW,GAAG;QAAEA,aAAagC,KAAKhC,WAAW;QAAEH,OAAOmC,KAAKnC,KAAK;IAAC,IAAI,CAAC;IACtI,OAAO;QAAEE,MAAM;QAAQ4C,UAAUX,KAAKC,EAAE;IAAC;AAC3C;AAEA,SAASQ,WAAWnB,IAAY;IAC9B,OAAOD,OAAOC,UAAU;AAC1B;AAEA,oFAAoF,GACpF,SAASsB,gBACPC,KAAwB,EACxBnB,OAAwB;IAExB,IAAImB,MAAMC,WAAW,KAAK,QAAQD,MAAMC,WAAW,KAAKD,MAAME,IAAI,EAChE,OAAO;QAAEhD,MAAM;QAAWiD,OAAOH,MAAMC,WAAW;IAAC;IACrD,IAAIpB,QAAQuB,UAAU,IAAIvB,QAAQuB,UAAU,KAAKvB,QAAQW,MAAM,EAAE;QAC/D,OAAO;YAAEtC,MAAM;YAAQ4C,UAAUjB,QAAQuB,UAAU;QAAC;IACtD;IACA,OAAO;AACT;AAEA;;;;;CAKC,GACD,OAAO,SAASC,oBACdC,EAAsB,EACtBtD,KAAa,EACbgD,KAAwB,EACxBnB,OAAwB;IAExB,MAAM0B,QAAQD,KAAKE,aAAaF,GAAGvB,IAAI,IAAI/B,OAAOgD,OAAOnB,WAAW;IACpE,IAAI0B,OAAO,OAAOA;IAClB,IAAI5B,cAAcgB,IAAI,CAAC3C,QAAQ;QAC7B,MAAMyD,OAAOV,gBAAgBC,OAAOnB;QACpC,IAAI4B,MAAM,OAAOA;QACjB,MAAMC,UAAU7B,QAAQI,KAAK,CAACC,IAAI,CAChC,CAACC,OACCA,KAAKC,EAAE,KAAKP,QAAQW,MAAM,IAC1B,CAACI,WAAWT,KAAKV,IAAI,KACrBE,cAAcgB,IAAI,CAACR,KAAKnC,KAAK;QAEjC,IAAI0D,SAAS,OAAO;YAAExD,MAAM;YAAQ4C,UAAUY,QAAQtB,EAAE;QAAC;IAC3D;IACA,MAAMuB,aAAa,IAAIC,IAAIrD,QAAQP;IACnC,MAAM6D,OAAOhC,QAAQI,KAAK,CAACX,MAAM,CAC/B,CAACa,OACCA,KAAKC,EAAE,KAAKP,QAAQW,MAAM,IAC1B,CAACI,WAAWT,KAAKV,IAAI,KACrBlB,QAAQ4B,KAAKnC,KAAK,EAAEQ,MAAM,GAAG,KAC7BD,QAAQ4B,KAAKnC,KAAK,EAAE8D,KAAK,CAAC,CAACnD,OAASgD,WAAWI,GAAG,CAACpD;IAEvD,OAAOkD,KAAKrD,MAAM,KAAK,IAAI;QAAEN,MAAM;QAAQ4C,UAAUe,IAAI,CAAC,EAAE,CAACzB,EAAE;IAAC,IAAI;AACtE;AAEA,SAASoB,aACPF,EAAU,EACVtD,KAAa,EACbgD,KAAwB,EACxBnB,OAAwB;IAExB,MAAMQ,QAAQiB,GAAGjD,WAAW;IAC5B,IACEgC,UAAU,UACVA,MAAMC,UAAU,CAAC,YACjBT,QAAQmC,KAAK,CAACC,IAAI,CAAC,CAACR,OAASA,KAAKrB,EAAE,KAAKkB,KACzC;QACA,OAAOP,gBAAgBC,OAAOnB;IAChC;IACA,IAAIQ,MAAMC,UAAU,CAAC,aAAa;QAChC,OAAOT,QAAQqC,KAAK,CAACxB,QAAQ,CAACY,MAAM;YAAEpD,MAAM;YAAQ2C,MAAMS;QAAG,IAAI;IACnE;IACA,MAAMa,UAAU9B,MAAMC,UAAU,CAAC,OAC7BgB,GAAGc,KAAK,CAAC,KACT/B,MAAMC,UAAU,CAAC,cACfgB,GAAGc,KAAK,CAAC,KACT;IACN,IAAID,YAAY,MAAM;QACpB,MAAME,SAASC,OAAOH,QAAQpC,IAAI;QAClC,MAAMoB,QACJmB,OAAOC,SAAS,CAACF,WAAWA,UAAU,KAAKA,UAAUrB,MAAMwB,QAAQ,CAAChE,MAAM,GACtE6D,SAAS,IACTvE,kBAAkBqE,SAASnB,MAAMwB,QAAQ;QAC/C,OAAOrB,UAAU,QAAQA,UAAUH,MAAME,IAAI,GACzC;YAAEhD,MAAM;YAAWiD;QAAM,IACzB;IACN;IACA,MAAMhB,OAAOP,UAAUS,MAAMC,UAAU,CAAC,WAAWgB,GAAGc,KAAK,CAAC,KAAKd,IAAIzB;IACrE,OAAOU,gBAAgBJ,MAAMnC,OAAO6B;AACtC"}
@@ -0,0 +1,125 @@
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 SITE'S OWN RECORDS, PLACED BY THE PLATFORM (AGL-3676).
19
+ *
20
+ * A guided start writes a store's first products and a blog's first posts,
21
+ * then built the pages around them in the layout language — and the pages
22
+ * showed neither. "Featured candles" and "The candle range" on the live
23
+ * Ember & Wick start (2026-10-08) were cards the model wrote, a name and a
24
+ * line each: no photo, no price, no link, nothing a shopper could open; the
25
+ * Clay Notes blog's "Featured writing" was three cards that linked to no
26
+ * post. The real records were on the site, one click away from nowhere.
27
+ *
28
+ * A LISTING is one kind of record a site keeps — its catalog, its blog —
29
+ * placed by code in the sections whose plan says they show it, through the
30
+ * element the plugin that keeps the records renders them with, bound to the
31
+ * records themselves rather than to words about them. It is the form binding
32
+ * one step on (a section whose plan `uses` a form places that form by its
33
+ * id, whatever the answer remembered): the model writes the section's
34
+ * heading and words, the platform places the records.
35
+ *
36
+ * - `products` — the commerce plugin's Product grid (`product-grid`), over
37
+ * the site's whole active catalog: a photo, the name, the price (or
38
+ * "Price coming soon" where the owner cleared it), each card linking its
39
+ * product's page.
40
+ * - `posts` — the content plugin's Collection Entries (`collectionEntries`)
41
+ * over the blog's collection, its card a cover, the date and byline, the
42
+ * title and the excerpt, linking each post.
43
+ *
44
+ * The elements are named by their persisted ids, as the form binding names
45
+ * `form`: a plugin never imports another's internals, and
46
+ * `ai-layout-listings.spec.ts` holds the ids and props named here to the
47
+ * source of the plugins that declare them.
48
+ *
49
+ * Model only: shapes and the inputs reader; the compiler draws them.
50
+ */
51
+ export type AiLayoutListingKind = 'products' | 'posts';
52
+ /** How a section shows a listing: a few of its records on a page about something else, or all of them on its own page. */
53
+ export type AiLayoutListingRole = 'featured' | 'index';
54
+ /** One section of one page that places a listing. */
55
+ export interface AiLayoutListingPlacement {
56
+ /** The page, by its plan id. */
57
+ screenId: string;
58
+ /** The section, by plan index. */
59
+ section: number;
60
+ role: AiLayoutListingRole;
61
+ }
62
+ /** A kind of record the site keeps, and where its pages place it. */
63
+ export interface AiLayoutListing {
64
+ /** `listing:products`, `listing:posts`: what a section's placement names. */
65
+ id: string;
66
+ kind: AiLayoutListingKind;
67
+ /** What a visitor would call the whole list: "the shop", "the blog". */
68
+ name: string;
69
+ /** The records it shows today, by name, so a page's words can name them. */
70
+ records: string[];
71
+ /** Where the whole list lives, as a path on the site: the blog's `/blog`. */
72
+ href?: string;
73
+ /** The content collection a posts listing repeats, by its slug. */
74
+ collectionSlug?: string;
75
+ placements: AiLayoutListingPlacement[];
76
+ }
77
+ /** The unit input a site's listings travel in, to its layout and its pages. */
78
+ export declare const AI_LAYOUT_LISTINGS_INPUT = "siteListings";
79
+ /** The id a listing of a kind is placed by. */
80
+ export declare function aiLayoutListingId(kind: AiLayoutListingKind): string;
81
+ /** The persisted element a listing is drawn with, by kind (held to its plugin's source by a spec). */
82
+ export declare const AI_LAYOUT_LISTING_ELEMENTS: Readonly<Record<AiLayoutListingKind, string>>;
83
+ /** The header's cart button: the commerce plugin's Cart in its `button` variant. */
84
+ export declare const AI_LAYOUT_CART_ELEMENT = "cart";
85
+ /** The most records a featured band shows; a page of its own shows them all. */
86
+ export declare const AI_LAYOUT_FEATURED_RECORDS: {
87
+ readonly products: 4;
88
+ readonly posts: 3;
89
+ };
90
+ /** A site's listings as its unit inputs carry them; nothing that does not read is kept. */
91
+ export declare function aiLayoutListingsOf(inputs: Readonly<Record<string, unknown>> | null | undefined): AiLayoutListing[];
92
+ /** The listing a section of a page places, and how; `null` for a section that places none. */
93
+ export declare function aiLayoutListingAt(listings: readonly AiLayoutListing[], screenId: string | null | undefined, section: number): {
94
+ listing: AiLayoutListing;
95
+ role: AiLayoutListingRole;
96
+ } | null;
97
+ /** The plan of a page, as a placement is chosen from it. */
98
+ export interface AiLayoutListingScreen {
99
+ id?: string | null;
100
+ title: string;
101
+ slug: string;
102
+ sections: ReadonlyArray<{
103
+ name: string;
104
+ items: number;
105
+ }>;
106
+ }
107
+ /**
108
+ * The sections of a site's pages that show a listing, chosen by code from the
109
+ * plan (AGL-3676), never asked of the model:
110
+ *
111
+ * - PRODUCTS. The shop's own page — the page whose address or name says it
112
+ * sells (`/shop`, "Shop", "Products") — shows the whole catalog in the
113
+ * section the plan gave the most items, else its second; and the home
114
+ * page features a few in its first section after the hero whose name
115
+ * says products ("Featured candles", "Bestsellers"), else in the first
116
+ * section after the hero planned with items.
117
+ * - POSTS. The home features the latest in its first section of items after
118
+ * the hero whose name says writing ("Featured writing", "Latest posts"),
119
+ * else its first section of items after the hero; and any other page
120
+ * whose name says it is about the writing does the same. The blog's own
121
+ * index is the platform's.
122
+ *
123
+ * A section placed once is placed for one listing.
124
+ */
125
+ export declare function aiLayoutListingPlacements(kind: AiLayoutListingKind, screens: readonly AiLayoutListingScreen[]): AiLayoutListingPlacement[];
@@ -0,0 +1,187 @@
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 SITE'S OWN RECORDS, PLACED BY THE PLATFORM (AGL-3676).
18
+ *
19
+ * A guided start writes a store's first products and a blog's first posts,
20
+ * then built the pages around them in the layout language — and the pages
21
+ * showed neither. "Featured candles" and "The candle range" on the live
22
+ * Ember & Wick start (2026-10-08) were cards the model wrote, a name and a
23
+ * line each: no photo, no price, no link, nothing a shopper could open; the
24
+ * Clay Notes blog's "Featured writing" was three cards that linked to no
25
+ * post. The real records were on the site, one click away from nowhere.
26
+ *
27
+ * A LISTING is one kind of record a site keeps — its catalog, its blog —
28
+ * placed by code in the sections whose plan says they show it, through the
29
+ * element the plugin that keeps the records renders them with, bound to the
30
+ * records themselves rather than to words about them. It is the form binding
31
+ * one step on (a section whose plan `uses` a form places that form by its
32
+ * id, whatever the answer remembered): the model writes the section's
33
+ * heading and words, the platform places the records.
34
+ *
35
+ * - `products` — the commerce plugin's Product grid (`product-grid`), over
36
+ * the site's whole active catalog: a photo, the name, the price (or
37
+ * "Price coming soon" where the owner cleared it), each card linking its
38
+ * product's page.
39
+ * - `posts` — the content plugin's Collection Entries (`collectionEntries`)
40
+ * over the blog's collection, its card a cover, the date and byline, the
41
+ * title and the excerpt, linking each post.
42
+ *
43
+ * The elements are named by their persisted ids, as the form binding names
44
+ * `form`: a plugin never imports another's internals, and
45
+ * `ai-layout-listings.spec.ts` holds the ids and props named here to the
46
+ * source of the plugins that declare them.
47
+ *
48
+ * Model only: shapes and the inputs reader; the compiler draws them.
49
+ */ import { _ as _extends } from "@swc/helpers/_/_extends";
50
+ /** The unit input a site's listings travel in, to its layout and its pages. */ export const AI_LAYOUT_LISTINGS_INPUT = 'siteListings';
51
+ /** The id a listing of a kind is placed by. */ export function aiLayoutListingId(kind) {
52
+ return `listing:${kind}`;
53
+ }
54
+ /** The persisted element a listing is drawn with, by kind (held to its plugin's source by a spec). */ export const AI_LAYOUT_LISTING_ELEMENTS = {
55
+ products: 'product-grid',
56
+ posts: 'collectionEntries'
57
+ };
58
+ /** The header's cart button: the commerce plugin's Cart in its `button` variant. */ export const AI_LAYOUT_CART_ELEMENT = 'cart';
59
+ /** The most records a featured band shows; a page of its own shows them all. */ export const AI_LAYOUT_FEATURED_RECORDS = {
60
+ products: 4,
61
+ posts: 3
62
+ };
63
+ const PLACEMENT_ROLES = [
64
+ 'featured',
65
+ 'index'
66
+ ];
67
+ const KINDS = [
68
+ 'products',
69
+ 'posts'
70
+ ];
71
+ const text = (value)=>typeof value === 'string' ? value.trim() : '';
72
+ /** A site's listings as its unit inputs carry them; nothing that does not read is kept. */ export function aiLayoutListingsOf(inputs) {
73
+ const raw = inputs == null ? void 0 : inputs[AI_LAYOUT_LISTINGS_INPUT];
74
+ if (!Array.isArray(raw)) return [];
75
+ const listings = [];
76
+ for (const entry of raw){
77
+ if (!entry || typeof entry !== 'object') continue;
78
+ const record = entry;
79
+ const kind = record['kind'];
80
+ if (!KINDS.includes(kind) || listings.some((listing)=>listing.kind === kind)) continue;
81
+ const href = text(record['href']);
82
+ const collectionSlug = text(record['collectionSlug']);
83
+ // A posts listing with no collection to repeat shows nothing.
84
+ if (kind === 'posts' && !/^[a-z0-9-]{1,100}$/.test(collectionSlug)) continue;
85
+ const placements = (Array.isArray(record['placements']) ? record['placements'] : []).flatMap((value)=>{
86
+ const placement = value != null ? value : {};
87
+ const screenId = text(placement['screenId']);
88
+ const section = placement['section'];
89
+ const role = placement['role'];
90
+ return screenId && typeof section === 'number' && Number.isInteger(section) && section >= 0 && PLACEMENT_ROLES.includes(role) ? [
91
+ {
92
+ screenId,
93
+ section,
94
+ role
95
+ }
96
+ ] : [];
97
+ });
98
+ listings.push(_extends({
99
+ id: aiLayoutListingId(kind),
100
+ kind,
101
+ name: text(record['name']) || (kind === 'products' ? 'the shop' : 'the blog'),
102
+ records: (Array.isArray(record['records']) ? record['records'] : []).map(text).filter(Boolean).slice(0, 12)
103
+ }, /^\/[a-z0-9-]+(?:\/[a-z0-9-]+)*$/.test(href) ? {
104
+ href
105
+ } : {}, kind === 'posts' ? {
106
+ collectionSlug
107
+ } : {}, {
108
+ placements
109
+ }));
110
+ }
111
+ return listings;
112
+ }
113
+ /** The listing a section of a page places, and how; `null` for a section that places none. */ export function aiLayoutListingAt(listings, screenId, section) {
114
+ if (!screenId) return null;
115
+ for (const listing of listings){
116
+ const placement = listing.placements.find((entry)=>entry.screenId === screenId && entry.section === section);
117
+ if (placement) return {
118
+ listing,
119
+ role: placement.role
120
+ };
121
+ }
122
+ return null;
123
+ }
124
+ const PRODUCT_PAGE = /\b(shop|store|products?|catalog(?:ue)?|collections?|range|browse)\b/i;
125
+ const PRODUCT_SECTION = /\b(shop|store|products?|catalog(?:ue)?|collections?|range|bestsellers?|best[- ]sellers?|new arrivals|arrivals|featured|favou?rites|signature)\b/i;
126
+ const POST_SECTION = /\b(blog|posts?|articles?|writing|journal|stories|essays?|latest|recent|news|featured)\b/i;
127
+ const firstSegment = (slug)=>slug.trim().replace(/^\/+/, '').split('/')[0].toLowerCase();
128
+ const isHome = (slug)=>firstSegment(slug) === '';
129
+ /**
130
+ * The sections of a site's pages that show a listing, chosen by code from the
131
+ * plan (AGL-3676), never asked of the model:
132
+ *
133
+ * - PRODUCTS. The shop's own page — the page whose address or name says it
134
+ * sells (`/shop`, "Shop", "Products") — shows the whole catalog in the
135
+ * section the plan gave the most items, else its second; and the home
136
+ * page features a few in its first section after the hero whose name
137
+ * says products ("Featured candles", "Bestsellers"), else in the first
138
+ * section after the hero planned with items.
139
+ * - POSTS. The home features the latest in its first section of items after
140
+ * the hero whose name says writing ("Featured writing", "Latest posts"),
141
+ * else its first section of items after the hero; and any other page
142
+ * whose name says it is about the writing does the same. The blog's own
143
+ * index is the platform's.
144
+ *
145
+ * A section placed once is placed for one listing.
146
+ */ export function aiLayoutListingPlacements(kind, screens) {
147
+ const placements = [];
148
+ const place = (screen, section, role)=>{
149
+ if (typeof screen.id === 'string' && screen.id && section >= 0) placements.push({
150
+ screenId: screen.id,
151
+ section,
152
+ role
153
+ });
154
+ };
155
+ const afterHero = (screen, test)=>screen.sections.findIndex((section, index)=>index > 0 && test(section.name, section.items));
156
+ if (kind === 'products') {
157
+ for (const screen of screens){
158
+ if (isHome(screen.slug)) {
159
+ const named = afterHero(screen, (name)=>PRODUCT_SECTION.test(name));
160
+ place(screen, named !== -1 ? named : afterHero(screen, (_name, items)=>items > 0), 'featured');
161
+ continue;
162
+ }
163
+ if (!PRODUCT_PAGE.test(`${firstSegment(screen.slug)} ${screen.title}`)) continue;
164
+ const most = Math.max(0, ...screen.sections.map((section)=>section.items));
165
+ const byItems = most > 0 ? screen.sections.findIndex((section)=>section.items === most) : -1;
166
+ const index = byItems !== -1 ? byItems : screen.sections.length > 1 ? 1 : 0;
167
+ place(screen, index, 'index');
168
+ }
169
+ return placements;
170
+ }
171
+ for (const screen of screens){
172
+ if (isHome(screen.slug)) {
173
+ // On a blog's home the band that names the writing ("Featured posts
174
+ // cards", "Featured journeys") lists it; failing one, the first band of
175
+ // items after the hero. A live Clay Notes plan (2026-10-09) opened its
176
+ // home with "What you will find" before "Featured posts cards".
177
+ const named = afterHero(screen, (name, items)=>items > 0 && POST_SECTION.test(name));
178
+ place(screen, named !== -1 ? named : afterHero(screen, (_name, items)=>items > 0), 'featured');
179
+ continue;
180
+ }
181
+ if (!POST_SECTION.test(screen.title)) continue;
182
+ place(screen, afterHero(screen, (name)=>POST_SECTION.test(name)), 'featured');
183
+ }
184
+ return placements;
185
+ }
186
+
187
+ //# sourceMappingURL=ai-layout-listings.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../../../../../../libs/plugins/ai/src/lib/layout-language/ai-layout-listings.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 SITE'S OWN RECORDS, PLACED BY THE PLATFORM (AGL-3676).\n *\n * A guided start writes a store's first products and a blog's first posts,\n * then built the pages around them in the layout language — and the pages\n * showed neither. \"Featured candles\" and \"The candle range\" on the live\n * Ember & Wick start (2026-10-08) were cards the model wrote, a name and a\n * line each: no photo, no price, no link, nothing a shopper could open; the\n * Clay Notes blog's \"Featured writing\" was three cards that linked to no\n * post. The real records were on the site, one click away from nowhere.\n *\n * A LISTING is one kind of record a site keeps — its catalog, its blog —\n * placed by code in the sections whose plan says they show it, through the\n * element the plugin that keeps the records renders them with, bound to the\n * records themselves rather than to words about them. It is the form binding\n * one step on (a section whose plan `uses` a form places that form by its\n * id, whatever the answer remembered): the model writes the section's\n * heading and words, the platform places the records.\n *\n * - `products` — the commerce plugin's Product grid (`product-grid`), over\n * the site's whole active catalog: a photo, the name, the price (or\n * \"Price coming soon\" where the owner cleared it), each card linking its\n * product's page.\n * - `posts` — the content plugin's Collection Entries (`collectionEntries`)\n * over the blog's collection, its card a cover, the date and byline, the\n * title and the excerpt, linking each post.\n *\n * The elements are named by their persisted ids, as the form binding names\n * `form`: a plugin never imports another's internals, and\n * `ai-layout-listings.spec.ts` holds the ids and props named here to the\n * source of the plugins that declare them.\n *\n * Model only: shapes and the inputs reader; the compiler draws them.\n */\n\nexport type AiLayoutListingKind = 'products' | 'posts'\n\n/** How a section shows a listing: a few of its records on a page about something else, or all of them on its own page. */\nexport type AiLayoutListingRole = 'featured' | 'index'\n\n/** One section of one page that places a listing. */\nexport interface AiLayoutListingPlacement {\n /** The page, by its plan id. */\n screenId: string\n /** The section, by plan index. */\n section: number\n role: AiLayoutListingRole\n}\n\n/** A kind of record the site keeps, and where its pages place it. */\nexport interface AiLayoutListing {\n /** `listing:products`, `listing:posts`: what a section's placement names. */\n id: string\n kind: AiLayoutListingKind\n /** What a visitor would call the whole list: \"the shop\", \"the blog\". */\n name: string\n /** The records it shows today, by name, so a page's words can name them. */\n records: string[]\n /** Where the whole list lives, as a path on the site: the blog's `/blog`. */\n href?: string\n /** The content collection a posts listing repeats, by its slug. */\n collectionSlug?: string\n placements: AiLayoutListingPlacement[]\n}\n\n/** The unit input a site's listings travel in, to its layout and its pages. */\nexport const AI_LAYOUT_LISTINGS_INPUT = 'siteListings'\n\n/** The id a listing of a kind is placed by. */\nexport function aiLayoutListingId(kind: AiLayoutListingKind): string {\n return `listing:${kind}`\n}\n\n/** The persisted element a listing is drawn with, by kind (held to its plugin's source by a spec). */\nexport const AI_LAYOUT_LISTING_ELEMENTS: Readonly<Record<AiLayoutListingKind, string>> = {\n products: 'product-grid',\n posts: 'collectionEntries',\n}\n\n/** The header's cart button: the commerce plugin's Cart in its `button` variant. */\nexport const AI_LAYOUT_CART_ELEMENT = 'cart'\n\n/** The most records a featured band shows; a page of its own shows them all. */\nexport const AI_LAYOUT_FEATURED_RECORDS = { products: 4, posts: 3 } as const\n\nconst PLACEMENT_ROLES: readonly AiLayoutListingRole[] = ['featured', 'index']\nconst KINDS: readonly AiLayoutListingKind[] = ['products', 'posts']\n\nconst text = (value: unknown): string => (typeof value === 'string' ? value.trim() : '')\n\n/** A site's listings as its unit inputs carry them; nothing that does not read is kept. */\nexport function aiLayoutListingsOf(inputs: Readonly<Record<string, unknown>> | null | undefined): AiLayoutListing[] {\n const raw = inputs?.[AI_LAYOUT_LISTINGS_INPUT]\n if (!Array.isArray(raw)) return []\n const listings: AiLayoutListing[] = []\n for (const entry of raw) {\n if (!entry || typeof entry !== 'object') continue\n const record = entry as Record<string, unknown>\n const kind = record['kind'] as AiLayoutListingKind\n if (!KINDS.includes(kind) || listings.some((listing) => listing.kind === kind)) continue\n const href = text(record['href'])\n const collectionSlug = text(record['collectionSlug'])\n // A posts listing with no collection to repeat shows nothing.\n if (kind === 'posts' && !/^[a-z0-9-]{1,100}$/.test(collectionSlug)) continue\n const placements = (Array.isArray(record['placements']) ? record['placements'] : []).flatMap((value) => {\n const placement = (value ?? {}) as Record<string, unknown>\n const screenId = text(placement['screenId'])\n const section = placement['section']\n const role = placement['role'] as AiLayoutListingRole\n return screenId && typeof section === 'number' && Number.isInteger(section) && section >= 0 && PLACEMENT_ROLES.includes(role)\n ? [{ screenId, section, role }]\n : []\n })\n listings.push({\n id: aiLayoutListingId(kind),\n kind,\n name: text(record['name']) || (kind === 'products' ? 'the shop' : 'the blog'),\n records: (Array.isArray(record['records']) ? record['records'] : []).map(text).filter(Boolean).slice(0, 12),\n ...(/^\\/[a-z0-9-]+(?:\\/[a-z0-9-]+)*$/.test(href) ? { href } : {}),\n ...(kind === 'posts' ? { collectionSlug } : {}),\n placements,\n })\n }\n return listings\n}\n\n/** The listing a section of a page places, and how; `null` for a section that places none. */\nexport function aiLayoutListingAt(\n listings: readonly AiLayoutListing[],\n screenId: string | null | undefined,\n section: number,\n): { listing: AiLayoutListing; role: AiLayoutListingRole } | null {\n if (!screenId) return null\n for (const listing of listings) {\n const placement = listing.placements.find((entry) => entry.screenId === screenId && entry.section === section)\n if (placement) return { listing, role: placement.role }\n }\n return null\n}\n\n// ── Where a site's pages place them ────────────────────────────────────\n\n/** The plan of a page, as a placement is chosen from it. */\nexport interface AiLayoutListingScreen {\n id?: string | null\n title: string\n slug: string\n sections: ReadonlyArray<{ name: string; items: number }>\n}\n\nconst PRODUCT_PAGE = /\\b(shop|store|products?|catalog(?:ue)?|collections?|range|browse)\\b/i\nconst PRODUCT_SECTION =\n /\\b(shop|store|products?|catalog(?:ue)?|collections?|range|bestsellers?|best[- ]sellers?|new arrivals|arrivals|featured|favou?rites|signature)\\b/i\nconst POST_SECTION = /\\b(blog|posts?|articles?|writing|journal|stories|essays?|latest|recent|news|featured)\\b/i\n\nconst firstSegment = (slug: string) => slug.trim().replace(/^\\/+/, '').split('/')[0].toLowerCase()\nconst isHome = (slug: string) => firstSegment(slug) === ''\n\n/**\n * The sections of a site's pages that show a listing, chosen by code from the\n * plan (AGL-3676), never asked of the model:\n *\n * - PRODUCTS. The shop's own page — the page whose address or name says it\n * sells (`/shop`, \"Shop\", \"Products\") — shows the whole catalog in the\n * section the plan gave the most items, else its second; and the home\n * page features a few in its first section after the hero whose name\n * says products (\"Featured candles\", \"Bestsellers\"), else in the first\n * section after the hero planned with items.\n * - POSTS. The home features the latest in its first section of items after\n * the hero whose name says writing (\"Featured writing\", \"Latest posts\"),\n * else its first section of items after the hero; and any other page\n * whose name says it is about the writing does the same. The blog's own\n * index is the platform's.\n *\n * A section placed once is placed for one listing.\n */\nexport function aiLayoutListingPlacements(\n kind: AiLayoutListingKind,\n screens: readonly AiLayoutListingScreen[],\n): AiLayoutListingPlacement[] {\n const placements: AiLayoutListingPlacement[] = []\n const place = (screen: AiLayoutListingScreen, section: number, role: AiLayoutListingRole) => {\n if (typeof screen.id === 'string' && screen.id && section >= 0) placements.push({ screenId: screen.id, section, role })\n }\n const afterHero = (screen: AiLayoutListingScreen, test: (name: string, items: number) => boolean) =>\n screen.sections.findIndex((section, index) => index > 0 && test(section.name, section.items))\n if (kind === 'products') {\n for (const screen of screens) {\n if (isHome(screen.slug)) {\n const named = afterHero(screen, (name) => PRODUCT_SECTION.test(name))\n place(screen, named !== -1 ? named : afterHero(screen, (_name, items) => items > 0), 'featured')\n continue\n }\n if (!PRODUCT_PAGE.test(`${firstSegment(screen.slug)} ${screen.title}`)) continue\n const most = Math.max(0, ...screen.sections.map((section) => section.items))\n const byItems = most > 0 ? screen.sections.findIndex((section) => section.items === most) : -1\n const index = byItems !== -1 ? byItems : screen.sections.length > 1 ? 1 : 0\n place(screen, index, 'index')\n }\n return placements\n }\n for (const screen of screens) {\n if (isHome(screen.slug)) {\n // On a blog's home the band that names the writing (\"Featured posts\n // cards\", \"Featured journeys\") lists it; failing one, the first band of\n // items after the hero. A live Clay Notes plan (2026-10-09) opened its\n // home with \"What you will find\" before \"Featured posts cards\".\n const named = afterHero(screen, (name, items) => items > 0 && POST_SECTION.test(name))\n place(screen, named !== -1 ? named : afterHero(screen, (_name, items) => items > 0), 'featured')\n continue\n }\n if (!POST_SECTION.test(screen.title)) continue\n place(screen, afterHero(screen, (name) => POST_SECTION.test(name)), 'featured')\n }\n return placements\n}\n"],"names":["AI_LAYOUT_LISTINGS_INPUT","aiLayoutListingId","kind","AI_LAYOUT_LISTING_ELEMENTS","products","posts","AI_LAYOUT_CART_ELEMENT","AI_LAYOUT_FEATURED_RECORDS","PLACEMENT_ROLES","KINDS","text","value","trim","aiLayoutListingsOf","inputs","raw","Array","isArray","listings","entry","record","includes","some","listing","href","collectionSlug","test","placements","flatMap","placement","screenId","section","role","Number","isInteger","push","id","name","records","map","filter","Boolean","slice","aiLayoutListingAt","find","PRODUCT_PAGE","PRODUCT_SECTION","POST_SECTION","firstSegment","slug","replace","split","toLowerCase","isHome","aiLayoutListingPlacements","screens","place","screen","afterHero","sections","findIndex","index","items","named","_name","title","most","Math","max","byItems","length"],"mappings":"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"}
@@ -107,11 +107,17 @@ export declare const AI_LAYOUT_STARTER_PHOTOS: {
107
107
  /** A small stable number from a seed, so the same job always picks the same photos. */
108
108
  export declare function aiLayoutSeedNumber(seed: string): number;
109
109
  /**
110
- * A starter photo for each slot. The hero's and the gallery's pools are
111
- * rotated by the seed, so sites do not all open with the same photo, while an
112
- * about picture always leads with the owner; no photo repeats on a page
113
- * while one is still unused; and a page with more slots than photos starts
114
- * the round again from its role's pool.
110
+ * A starter photo for each slot (AGL-3660). No photo repeats on a page while
111
+ * an unused one remains, and every pool turns by the seed, so sites do not
112
+ * all open with the same photo while one job always picks the same.
113
+ *
114
+ * The slots are served by what they are for, not where they sit: the hero
115
+ * first, then the pictures of people (an about picture leads with the owner),
116
+ * then the rest. The live Juniper Clay start (2026-10-08) had six slots and
117
+ * five starters, and the owner's photo went to a stoneware card before the
118
+ * About section that described the artist asked for it. Only a page with more
119
+ * slots than starters repeats one: the photo placed longest ago, never the
120
+ * one in a slot beside it.
115
121
  */
116
122
  export declare function aiLayoutStarterPhotos(slots: readonly AiLayoutPictureSlot[], seed: string): AiLayoutPicturePhoto[];
117
123
  /**
@@ -16,11 +16,45 @@ import { _ as _extends } from "@swc/helpers/_/_extends";
16
16
  * limitations under the License.
17
17
  */ import { DEFAULT_SITE_IMAGES } from "@aglyn/aglyn/app-utils/default-site";
18
18
  /** Words that say a picture shows people, or a section is about them. */ const PEOPLE = /\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;
19
- /** A frame's `aspectRatio` (`'16 / 9'`) as a number. */ function aspectOf(value) {
19
+ /**
20
+ * An `aspectRatio` (`'16 / 9'`) as a number, or `null` where none is named.
21
+ * A responsive one (`{ xs: '4 / 3', md: '4 / 5' }`, as a designed page's
22
+ * pictures carry, AGL-3660) reads at the desktop breakpoint first: one photo
23
+ * serves every width, and the wide screen is where its shape shows most.
24
+ */ function aspectOf(value) {
25
+ if (value && typeof value === 'object') {
26
+ const byBreakpoint = value;
27
+ for (const breakpoint of [
28
+ 'md',
29
+ 'lg',
30
+ 'xl',
31
+ 'sm',
32
+ 'xs'
33
+ ]){
34
+ const ratio = aspectOf(byBreakpoint[breakpoint]);
35
+ if (ratio !== null) return ratio;
36
+ }
37
+ return null;
38
+ }
20
39
  const match = /^\s*(\d+(?:\.\d+)?)\s*\/\s*(\d+(?:\.\d+)?)\s*$/.exec(String(value != null ? value : ''));
21
- if (!match) return 1;
40
+ if (!match) return null;
22
41
  const ratio = Number(match[1]) / Number(match[2]);
23
- return Number.isFinite(ratio) && ratio > 0 ? ratio : 1;
42
+ return Number.isFinite(ratio) && ratio > 0 ? ratio : null;
43
+ }
44
+ /** The shape a full-bleed cover's photo is searched for: it fills a wide band. */ const COVER_ASPECT = 16 / 9;
45
+ /**
46
+ * A slot's shape (AGL-3660): the image's own `aspectRatio` (a designed
47
+ * picture card sets it on the image), else its frame's, else — for a frame
48
+ * laid absolutely over its whole section, a designed photo cover — a wide
49
+ * band's. 1 where nothing says.
50
+ */ function slotAspect(image, frame) {
51
+ var _aspectOf;
52
+ var _image_sx, _frame_sx;
53
+ const own = (_aspectOf = aspectOf(image == null ? void 0 : (_image_sx = image.sx) == null ? void 0 : _image_sx['aspectRatio'])) != null ? _aspectOf : aspectOf(frame == null ? void 0 : (_frame_sx = frame.sx) == null ? void 0 : _frame_sx['aspectRatio']);
54
+ if (own !== null) return own;
55
+ const sx = frame == null ? void 0 : frame.sx;
56
+ if ((sx == null ? void 0 : sx['position']) === 'absolute' && sx['width'] === '100%' && sx['height'] === '100%') return COVER_ASPECT;
57
+ return 1;
24
58
  }
25
59
  /** A slot's role: the first section's picture is the hero; people make an about picture. */ export function aiLayoutPictureRole(input) {
26
60
  if (input.sectionIndex === 0) return 'hero';
@@ -49,7 +83,7 @@ import { _ as _extends } from "@swc/helpers/_/_extends";
49
83
  const alt = typeof ((_node_props1 = node.props) == null ? void 0 : _node_props1['alt']) === 'string' ? node.props['alt'].trim() : '';
50
84
  if ((typeof src !== 'string' || !src.trim()) && alt) {
51
85
  var _ref, _sections_names_here;
52
- var _frame_nodes, _frame_sx;
86
+ var _frame_nodes;
53
87
  const frame = parentId ? map[parentId] : undefined;
54
88
  const iconId = (_ref = frame == null ? void 0 : (_frame_nodes = frame.nodes) == null ? void 0 : _frame_nodes.find((child)=>{
55
89
  var _map_child;
@@ -60,7 +94,7 @@ import { _ as _extends } from "@swc/helpers/_/_extends";
60
94
  frameId: frame ? parentId : null,
61
95
  iconId,
62
96
  alt,
63
- aspect: aspectOf(frame == null ? void 0 : (_frame_sx = frame.sx) == null ? void 0 : _frame_sx['aspectRatio']),
97
+ aspect: slotAspect(node, frame),
64
98
  sectionIndex: here,
65
99
  role: aiLayoutPictureRole({
66
100
  sectionIndex: here,
@@ -114,27 +148,56 @@ function rotate(list, by) {
114
148
  ];
115
149
  }
116
150
  /**
117
- * A starter photo for each slot. The hero's and the gallery's pools are
118
- * rotated by the seed, so sites do not all open with the same photo, while an
119
- * about picture always leads with the owner; no photo repeats on a page
120
- * while one is still unused; and a page with more slots than photos starts
121
- * the round again from its role's pool.
151
+ * A starter photo for each slot (AGL-3660). No photo repeats on a page while
152
+ * an unused one remains, and every pool turns by the seed, so sites do not
153
+ * all open with the same photo while one job always picks the same.
154
+ *
155
+ * The slots are served by what they are for, not where they sit: the hero
156
+ * first, then the pictures of people (an about picture leads with the owner),
157
+ * then the rest. The live Juniper Clay start (2026-10-08) had six slots and
158
+ * five starters, and the owner's photo went to a stoneware card before the
159
+ * About section that described the artist asked for it. Only a page with more
160
+ * slots than starters repeats one: the photo placed longest ago, never the
161
+ * one in a slot beside it.
122
162
  */ export function aiLayoutStarterPhotos(slots, seed) {
123
163
  const turn = aiLayoutSeedNumber(seed);
124
- const used = new Set();
125
- return slots.map((slot)=>{
126
- var _find;
164
+ const order = [
165
+ 'hero',
166
+ 'about',
167
+ 'gallery'
168
+ ];
169
+ const served = slots.map((slot, index)=>({
170
+ slot,
171
+ index
172
+ })).sort((a, b)=>order.indexOf(a.slot.role) - order.indexOf(b.slot.role) || a.index - b.index);
173
+ const names = slots.map(()=>undefined);
174
+ /** When each starter was last placed, by its turn in the serving order. */ const placedAt = new Map();
175
+ served.forEach(({ slot, index }, step)=>{
176
+ var _ref;
127
177
  // An about picture leads with the owner on every site; the others turn.
128
178
  const pool = slot.role === 'about' ? [
129
179
  ...STARTER_POOLS.about
130
180
  ] : rotate(STARTER_POOLS[slot.role], turn);
131
181
  const rest = rotate(ALL_STARTERS.filter((name)=>!pool.includes(name)), turn);
132
- if (used.size >= ALL_STARTERS.length) used.clear();
133
- const name = (_find = [
182
+ const candidates = [
134
183
  ...pool,
135
184
  ...rest
136
- ].find((candidate)=>!used.has(candidate))) != null ? _find : pool[0];
137
- used.add(name);
185
+ ];
186
+ const unused = candidates.find((candidate)=>!placedAt.has(candidate));
187
+ const beside = new Set([
188
+ names[index - 1],
189
+ names[index + 1]
190
+ ].filter(Boolean));
191
+ const name = (_ref = unused != null ? unused : [
192
+ ...candidates
193
+ ].sort((a, b)=>{
194
+ var _placedAt_get, _placedAt_get1;
195
+ return ((_placedAt_get = placedAt.get(a)) != null ? _placedAt_get : -1) - ((_placedAt_get1 = placedAt.get(b)) != null ? _placedAt_get1 : -1);
196
+ }).find((candidate)=>!beside.has(candidate))) != null ? _ref : candidates[0];
197
+ names[index] = name;
198
+ placedAt.set(name, step);
199
+ });
200
+ return names.map((name)=>{
138
201
  const photo = AI_LAYOUT_STARTER_PHOTOS[name];
139
202
  return {
140
203
  src: photo.src,