@aglyn/plugins-ai 1.0.0-beta.291 → 1.0.0-beta.293

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 (149) hide show
  1. package/package.json +17 -17
  2. package/src/lib/components/ai-site-import-job.component.js +5 -4
  3. package/src/lib/components/ai-site-import-job.component.js.map +1 -1
  4. package/src/lib/components/ai-site-import-start.component.js +3 -3
  5. package/src/lib/components/ai-site-import-start.component.js.map +1 -1
  6. package/src/lib/components/ai-site-start-card.component.d.ts +1 -1
  7. package/src/lib/components/ai-site-start-card.component.js +129 -29
  8. package/src/lib/components/ai-site-start-card.component.js.map +1 -1
  9. package/src/lib/components/ai-site-start-steps.component.d.ts +15 -3
  10. package/src/lib/components/ai-site-start-steps.component.js +42 -25
  11. package/src/lib/components/ai-site-start-steps.component.js.map +1 -1
  12. package/src/lib/components/ai-site-style-previews.d.ts +17 -0
  13. package/src/lib/components/ai-site-style-previews.js +26 -95
  14. package/src/lib/components/ai-site-style-previews.js.map +1 -1
  15. package/src/lib/declarations.server.js +22 -0
  16. package/src/lib/declarations.server.js.map +1 -1
  17. package/src/lib/jobs/ai-job-import-step.d.ts +21 -2
  18. package/src/lib/jobs/ai-job-import-step.js +179 -49
  19. package/src/lib/jobs/ai-job-import-step.js.map +1 -1
  20. package/src/lib/jobs/ai-job-layout-language.d.ts +27 -3
  21. package/src/lib/jobs/ai-job-layout-language.js +156 -113
  22. package/src/lib/jobs/ai-job-layout-language.js.map +1 -1
  23. package/src/lib/jobs/ai-job-layout-step.js +56 -17
  24. package/src/lib/jobs/ai-job-layout-step.js.map +1 -1
  25. package/src/lib/jobs/ai-job-page-language.d.ts +25 -15
  26. package/src/lib/jobs/ai-job-page-language.js +120 -66
  27. package/src/lib/jobs/ai-job-page-language.js.map +1 -1
  28. package/src/lib/jobs/ai-job-page-step.js +27 -7
  29. package/src/lib/jobs/ai-job-page-step.js.map +1 -1
  30. package/src/lib/jobs/ai-job-site-datasets.d.ts +12 -4
  31. package/src/lib/jobs/ai-job-site-datasets.js +36 -16
  32. package/src/lib/jobs/ai-job-site-datasets.js.map +1 -1
  33. package/src/lib/jobs/ai-jobs.js +6 -2
  34. package/src/lib/jobs/ai-jobs.js.map +1 -1
  35. package/src/lib/jobs/ai-layout-stock-photos.d.ts +50 -1
  36. package/src/lib/jobs/ai-layout-stock-photos.js +586 -35
  37. package/src/lib/jobs/ai-layout-stock-photos.js.map +1 -1
  38. package/src/lib/jobs/ai-site-import-compile.d.ts +147 -0
  39. package/src/lib/jobs/ai-site-import-compile.js +1143 -0
  40. package/src/lib/jobs/ai-site-import-compile.js.map +1 -0
  41. package/src/lib/jobs/ai-site-import-document.d.ts +9 -36
  42. package/src/lib/jobs/ai-site-import-document.js +23 -489
  43. package/src/lib/jobs/ai-site-import-document.js.map +1 -1
  44. package/src/lib/jobs/ai-site-import-look.d.ts +54 -7
  45. package/src/lib/jobs/ai-site-import-look.js +163 -52
  46. package/src/lib/jobs/ai-site-import-look.js.map +1 -1
  47. package/src/lib/jobs/ai-site-import-publish.d.ts +7 -1
  48. package/src/lib/jobs/ai-site-import-publish.js +19 -5
  49. package/src/lib/jobs/ai-site-import-publish.js.map +1 -1
  50. package/src/lib/jobs/ai-site-late-photos.js +4 -2
  51. package/src/lib/jobs/ai-site-late-photos.js.map +1 -1
  52. package/src/lib/jobs/ai-site-publish.d.ts +10 -1
  53. package/src/lib/jobs/ai-site-publish.js +8 -3
  54. package/src/lib/jobs/ai-site-publish.js.map +1 -1
  55. package/src/lib/jobs/fixtures/ai-stock-photo-agl4092-1029-recordings.d.ts +23 -0
  56. package/src/lib/jobs/fixtures/ai-stock-photo-agl4092-1029-recordings.js +677 -0
  57. package/src/lib/jobs/fixtures/ai-stock-photo-agl4092-1029-recordings.js.map +1 -0
  58. package/src/lib/layout-language/ai-layout-compiler.d.ts +26 -0
  59. package/src/lib/layout-language/ai-layout-compiler.js +677 -33
  60. package/src/lib/layout-language/ai-layout-compiler.js.map +1 -1
  61. package/src/lib/layout-language/ai-layout-frame-hollows.js +5 -1
  62. package/src/lib/layout-language/ai-layout-frame-hollows.js.map +1 -1
  63. package/src/lib/layout-language/ai-layout-frame-source.d.ts +196 -0
  64. package/src/lib/layout-language/ai-layout-frame-source.js +726 -0
  65. package/src/lib/layout-language/ai-layout-frame-source.js.map +1 -0
  66. package/src/lib/layout-language/ai-layout-frame.d.ts +22 -0
  67. package/src/lib/layout-language/ai-layout-frame.js +121 -29
  68. package/src/lib/layout-language/ai-layout-frame.js.map +1 -1
  69. package/src/lib/layout-language/ai-layout-language.d.ts +103 -37
  70. package/src/lib/layout-language/ai-layout-language.js +85 -120
  71. package/src/lib/layout-language/ai-layout-language.js.map +1 -1
  72. package/src/lib/model/ai-credit-estimate.d.ts +11 -7
  73. package/src/lib/model/ai-credit-estimate.js +11 -7
  74. package/src/lib/model/ai-credit-estimate.js.map +1 -1
  75. package/src/lib/model/ai-site-import.d.ts +307 -13
  76. package/src/lib/model/ai-site-import.js +227 -19
  77. package/src/lib/model/ai-site-import.js.map +1 -1
  78. package/src/lib/model/ai-site-look.d.ts +65 -1
  79. package/src/lib/model/ai-site-look.js +141 -23
  80. package/src/lib/model/ai-site-look.js.map +1 -1
  81. package/src/lib/model/ai-site-photos.d.ts +2 -0
  82. package/src/lib/model/ai-site-photos.js.map +1 -1
  83. package/src/lib/model/ai-site-start-steps.d.ts +4 -3
  84. package/src/lib/model/ai-site-start-steps.js +11 -3
  85. package/src/lib/model/ai-site-start-steps.js.map +1 -1
  86. package/src/lib/model/ai-site-start.d.ts +23 -2
  87. package/src/lib/model/ai-site-start.js +25 -3
  88. package/src/lib/model/ai-site-start.js.map +1 -1
  89. package/src/lib/providers/routing.d.ts +11 -0
  90. package/src/lib/providers/routing.js +23 -0
  91. package/src/lib/providers/routing.js.map +1 -1
  92. package/src/lib/runtime/ai-doctrine-validators.d.ts +4 -2
  93. package/src/lib/runtime/ai-doctrine-validators.js +7 -2
  94. package/src/lib/runtime/ai-doctrine-validators.js.map +1 -1
  95. package/src/lib/runtime/ai-icon-library.d.ts +4 -0
  96. package/src/lib/runtime/ai-icon-library.js +4 -0
  97. package/src/lib/runtime/ai-icon-library.js.map +1 -1
  98. package/src/lib/runtime/ai-node-tree.js +5 -1
  99. package/src/lib/runtime/ai-node-tree.js.map +1 -1
  100. package/src/lib/runtime/ai-palette.d.ts +2 -0
  101. package/src/lib/runtime/ai-palette.generated.js +19 -3
  102. package/src/lib/runtime/ai-palette.generated.js.map +1 -1
  103. package/src/lib/runtime/ai-palette.js +2 -1
  104. package/src/lib/runtime/ai-palette.js.map +1 -1
  105. package/src/lib/site-import/fixtures/site-layout-recordings.d.ts +50 -0
  106. package/src/lib/site-import/fixtures/site-layout-recordings.js +86 -0
  107. package/src/lib/site-import/fixtures/site-layout-recordings.js.map +1 -0
  108. package/src/lib/site-import/site-import-crawl.d.ts +18 -1
  109. package/src/lib/site-import/site-import-crawl.js +70 -10
  110. package/src/lib/site-import/site-import-crawl.js.map +1 -1
  111. package/src/lib/site-import/site-import-css.d.ts +110 -0
  112. package/src/lib/site-import/site-import-css.js +826 -0
  113. package/src/lib/site-import/site-import-css.js.map +1 -0
  114. package/src/lib/site-import/site-import-html.d.ts +116 -1
  115. package/src/lib/site-import/site-import-html.js +538 -46
  116. package/src/lib/site-import/site-import-html.js.map +1 -1
  117. package/src/lib/site-import/site-import-layout.d.ts +79 -0
  118. package/src/lib/site-import/site-import-layout.js +1608 -0
  119. package/src/lib/site-import/site-import-layout.js.map +1 -0
  120. package/src/lib/site-import/site-import-nav.d.ts +40 -0
  121. package/src/lib/site-import/site-import-nav.js +437 -0
  122. package/src/lib/site-import/site-import-nav.js.map +1 -0
  123. package/src/lib/site-import/site-import-page.d.ts +8 -1
  124. package/src/lib/site-import/site-import-page.js +24 -2
  125. package/src/lib/site-import/site-import-page.js.map +1 -1
  126. package/src/lib/assets/site-styles/beauty.webp +0 -0
  127. package/src/lib/assets/site-styles/blog.webp +0 -0
  128. package/src/lib/assets/site-styles/business.webp +0 -0
  129. package/src/lib/assets/site-styles/coming-soon.webp +0 -0
  130. package/src/lib/assets/site-styles/education.webp +0 -0
  131. package/src/lib/assets/site-styles/events.webp +0 -0
  132. package/src/lib/assets/site-styles/fitness.webp +0 -0
  133. package/src/lib/assets/site-styles/landing.webp +0 -0
  134. package/src/lib/assets/site-styles/music.webp +0 -0
  135. package/src/lib/assets/site-styles/nonprofit.webp +0 -0
  136. package/src/lib/assets/site-styles/personal.webp +0 -0
  137. package/src/lib/assets/site-styles/photography.webp +0 -0
  138. package/src/lib/assets/site-styles/portfolio.webp +0 -0
  139. package/src/lib/assets/site-styles/professional.webp +0 -0
  140. package/src/lib/assets/site-styles/realestate.webp +0 -0
  141. package/src/lib/assets/site-styles/restaurant.webp +0 -0
  142. package/src/lib/assets/site-styles/store.webp +0 -0
  143. package/src/lib/assets/site-styles/studio.webp +0 -0
  144. package/src/lib/assets/site-styles/trades.webp +0 -0
  145. package/src/lib/assets/site-styles/wellness.webp +0 -0
  146. package/src/lib/assets/site-styles/yoga.webp +0 -0
  147. package/src/lib/site-import/site-import-review.d.ts +0 -40
  148. package/src/lib/site-import/site-import-review.js +0 -149
  149. package/src/lib/site-import/site-import-review.js.map +0 -1
@@ -0,0 +1,726 @@
1
+ import { _ as _extends } from "@swc/helpers/_/_extends";
2
+ /**
3
+ * @license
4
+ * Copyright 2026 Aglyn LLC
5
+ *
6
+ * Licensed under the Apache License, Version 2.0 (the "License");
7
+ * you may not use this file except in compliance with the License.
8
+ * You may obtain a copy of the License at
9
+ *
10
+ * http://www.apache.org/licenses/LICENSE-2.0
11
+ *
12
+ * Unless required by applicable law or agreed to in writing, software
13
+ * distributed under the License is distributed on an "AS IS" BASIS,
14
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15
+ * See the License for the specific language governing permissions and
16
+ * limitations under the License.
17
+ */ /*==========================================
18
+ * A HEADER AND FOOTER BROUGHT IN AS THE SOURCE BUILDS THEM (AGL-4108).
19
+ *
20
+ * On 10/29 an imported header was the wave's page titles, seven of them cut
21
+ * to 28 characters, wrapping onto two and three lines from 900 px up, with
22
+ * no menus, its logo over "Home" at 1280 and its "Start free" sent to the
23
+ * Forms page. The source's own header was five items, two of them menus,
24
+ * and two buttons that leave for the app.
25
+ *
26
+ * So a site brought in from elsewhere is given its source's header and
27
+ * footer as the reader found them (`site-import-nav.ts`), and they are drawn
28
+ * here by code, with no model:
29
+ *
30
+ * - **The menu.** Each top-level item as the source has it: a link, or a
31
+ * menu of its entries — the Besigner's Mega Menu where its entries carry a
32
+ * line under them or sit in headed columns, its Dropdown Menu otherwise.
33
+ * Every label on one line. The row shows from the screen width it fits at
34
+ * (`aiLayoutNavBreakpoint`), and below it the phone menu's button.
35
+ * - **The buttons.** The source's own, with their own addresses: an address
36
+ * off the site stays off it, as the source sends it. Drawn as the source
37
+ * draws them: filled, outlined or as text.
38
+ * - **The phone menu.** Each menu as a heading over its entries, then the
39
+ * links, then the buttons: every part of the menu as present as the rest.
40
+ * - **The footer.** The source's columns under their headings, its words,
41
+ * its profiles, its address, phone and email, and its © line word for word.
42
+ *
43
+ * Every word is the source's, as written; nothing here is a model's.
44
+ *=========================================*/ import { aiIsIconPathData } from "../runtime/ai-icon-library.js";
45
+ import { isContactHref, isHttpsUrl, isRootRelativePath } from "../runtime/ai-node-tree.js";
46
+ /** The most kept of each part: the reader's own limits, held again on the way in. */ export const AI_LAYOUT_FRAME_SOURCE_LIMITS = {
47
+ groups: 12,
48
+ entries: 40,
49
+ ctas: 4,
50
+ columns: 12,
51
+ links: 60,
52
+ text: 12,
53
+ social: 20,
54
+ label: 120,
55
+ line: 300
56
+ };
57
+ const SCREEN_ID = /^[A-Za-z0-9_-]{1,64}$/;
58
+ const STYLES = [
59
+ 'primary',
60
+ 'secondary',
61
+ 'quiet'
62
+ ];
63
+ const GROUNDS = [
64
+ 'page',
65
+ 'paper',
66
+ 'brand',
67
+ 'dark'
68
+ ];
69
+ /** Whether a destination is one a link of the frame may carry. */ export function aiLayoutFrameSourceToAllowed(to) {
70
+ if (typeof to !== 'string' || !to || to.length > 2048) return false;
71
+ if (to.startsWith('page:')) return SCREEN_ID.test(to.slice(5));
72
+ return isHttpsUrl(to) || isRootRelativePath(to) || isContactHref(to);
73
+ }
74
+ const isRecord = (value)=>!!value && typeof value === 'object' && !Array.isArray(value);
75
+ const words = (value, max)=>{
76
+ const line = typeof value === 'string' ? value.replace(/\s+/g, ' ').trim() : '';
77
+ return line.length > max ? `${line.slice(0, max - 1).trimEnd()}…` : line;
78
+ };
79
+ const listOf = (value)=>Array.isArray(value) ? value : [];
80
+ function linkOf(raw) {
81
+ if (!isRecord(raw)) return null;
82
+ const label = words(raw['label'], AI_LAYOUT_FRAME_SOURCE_LIMITS.label);
83
+ return label && aiLayoutFrameSourceToAllowed(raw['to']) ? {
84
+ label,
85
+ to: raw['to']
86
+ } : null;
87
+ }
88
+ const present = (value)=>value !== null;
89
+ /**
90
+ * A source frame as a job carries it, read defensively: every label cut to
91
+ * its limit, every destination one a link may carry, every icon one path.
92
+ * `null` where it holds no menu item and no button to draw.
93
+ */ export function aiLayoutFrameSourceOf(raw) {
94
+ if (!isRecord(raw)) return null;
95
+ const limits = AI_LAYOUT_FRAME_SOURCE_LIMITS;
96
+ const nav = listOf(raw['nav']).slice(0, limits.groups).map((group)=>{
97
+ if (!isRecord(group)) return null;
98
+ const label = words(group['label'], limits.label);
99
+ if (!label) return null;
100
+ const items = listOf(group['items']).slice(0, limits.entries).map((entry)=>{
101
+ const link = linkOf(entry);
102
+ if (!link || !isRecord(entry)) return null;
103
+ const text = words(entry['text'], limits.line);
104
+ const column = words(entry['column'], limits.label);
105
+ return _extends({}, link, text ? {
106
+ text
107
+ } : {}, aiIsIconPathData(entry['iconPath']) ? {
108
+ iconPath: entry['iconPath']
109
+ } : {}, column ? {
110
+ column
111
+ } : {});
112
+ }).filter(present);
113
+ const to = aiLayoutFrameSourceToAllowed(group['to']) ? group['to'] : null;
114
+ // A name that opens nothing and goes nowhere is no item.
115
+ return items.length || to ? {
116
+ label,
117
+ to,
118
+ items
119
+ } : null;
120
+ }).filter(present);
121
+ const ctas = listOf(raw['ctas']).slice(0, limits.ctas).map((cta)=>{
122
+ const link = linkOf(cta);
123
+ if (!link || !isRecord(cta)) return null;
124
+ const style = STYLES.includes(cta['style']) ? cta['style'] : 'primary';
125
+ return _extends({}, link, {
126
+ style
127
+ });
128
+ }).filter(present);
129
+ if (!nav.length && !ctas.length) return null;
130
+ const header = GROUNDS.includes(raw['header']) ? raw['header'] : 'paper';
131
+ return {
132
+ nav,
133
+ ctas,
134
+ header,
135
+ footer: footerOf(raw['footer'])
136
+ };
137
+ }
138
+ function footerOf(raw) {
139
+ if (!isRecord(raw)) return null;
140
+ const limits = AI_LAYOUT_FRAME_SOURCE_LIMITS;
141
+ const columns = listOf(raw['columns']).slice(0, limits.columns).map((column)=>{
142
+ if (!isRecord(column)) return null;
143
+ const heading = words(column['heading'], limits.label);
144
+ const links = listOf(column['links']).slice(0, limits.links).map(linkOf).filter(present);
145
+ return heading && links.length ? {
146
+ heading,
147
+ links
148
+ } : null;
149
+ }).filter(present);
150
+ const links = listOf(raw['links']).slice(0, limits.links).map(linkOf).filter(present);
151
+ const text = listOf(raw['text']).slice(0, limits.text).map((line)=>words(line, limits.line)).filter(Boolean);
152
+ const social = listOf(raw['social']).slice(0, limits.social).map((entry)=>{
153
+ const link = linkOf(entry);
154
+ const network = isRecord(entry) ? words(entry['network'], 40) : '';
155
+ return link && network && isHttpsUrl(link.to) ? _extends({}, link, {
156
+ network
157
+ }) : null;
158
+ }).filter(present);
159
+ const fact = (key)=>words(raw[key], limits.line) || null;
160
+ const footer = {
161
+ columns,
162
+ links,
163
+ text,
164
+ social,
165
+ address: fact('address'),
166
+ phone: fact('phone'),
167
+ email: fact('email'),
168
+ copyright: fact('copyright')
169
+ };
170
+ const empty = !columns.length && !links.length && !text.length && !social.length && !footer.address && !footer.phone && !footer.email && !footer.copyright;
171
+ return empty ? null : footer;
172
+ }
173
+ /** The pages a source frame links by id, each once. */ export function aiLayoutFrameSourcePageIds(source) {
174
+ const all = [
175
+ ...source.nav.flatMap((group)=>[
176
+ group.to,
177
+ ...group.items.map((entry)=>entry.to)
178
+ ]),
179
+ ...source.ctas.map((cta)=>cta.to),
180
+ ...source.footer ? [
181
+ ...source.footer.columns.flatMap((column)=>column.links.map((entry)=>entry.to)),
182
+ ...source.footer.links.map((entry)=>entry.to)
183
+ ] : []
184
+ ];
185
+ return [
186
+ ...new Set(all.filter((to)=>typeof to === 'string' && to.startsWith('page:')).map((to)=>to.slice('page:'.length)))
187
+ ];
188
+ }
189
+ /** A link's props for where it goes: a page by its id, anywhere else by its address. */ export function aiLayoutFrameSourceLinkProps(to) {
190
+ return to.startsWith('page:') ? {
191
+ screenId: to.slice('page:'.length)
192
+ } : {
193
+ href: to
194
+ };
195
+ }
196
+ /*------------------------------------------
197
+ * Where the menu row fits
198
+ *-----------------------------------------*/ /**
199
+ * The widths the header row is held to, in CSS pixels: a character of a
200
+ * label at the body size, the chevron a menu's trigger carries, the gap
201
+ * between the row's items and between its buttons, a button's padding, and
202
+ * the row's room at each breakpoint (the Container's `lg` width less its
203
+ * gutters).
204
+ */ export const AI_LAYOUT_HEADER_MEASURE = {
205
+ char: 9,
206
+ chevron: 28,
207
+ gap: 24,
208
+ buttonGap: 8,
209
+ padding: {
210
+ primary: 32,
211
+ secondary: 32,
212
+ quiet: 16
213
+ },
214
+ room: {
215
+ md: 900 - 48,
216
+ lg: 1200 - 48
217
+ },
218
+ /** What share of the room the row may fill: the estimate is a character's average, so it keeps a margin. */ fill: 0.9,
219
+ /** A logo's width where its picture's shape is unknown. */ logo: 160
220
+ };
221
+ /** The header row's width in CSS pixels, its brand, its menu and its buttons side by side. */ export function aiLayoutHeaderRowWidth(input) {
222
+ const measure = AI_LAYOUT_HEADER_MEASURE;
223
+ const navWidth = input.nav.reduce((sum, item)=>sum + item.label.length * measure.char + (item.menu ? measure.chevron : 0), 0) + Math.max(0, input.nav.length - 1) * measure.gap;
224
+ const ctaWidth = input.ctas.reduce((sum, cta)=>sum + cta.label.length * measure.char + measure.padding[cta.style], 0) + Math.max(0, input.ctas.length - 1) * measure.buttonGap;
225
+ const parts = [
226
+ input.brandWidth,
227
+ navWidth,
228
+ ctaWidth
229
+ ].filter((width)=>width > 0);
230
+ return parts.reduce((sum, width)=>sum + width, 0) + Math.max(0, parts.length - 1) * measure.gap;
231
+ }
232
+ /**
233
+ * The screen width the menu row shows from (AGL-4108): `md` where it fits a
234
+ * 900-pixel screen with room to spare, else `lg`; and `lg` whatever it
235
+ * measures for a row of more than five items or more than 60 characters.
236
+ * Below it the row is the phone menu's button.
237
+ */ export function aiLayoutNavBreakpoint(input) {
238
+ const chars = input.nav.reduce((sum, item)=>sum + item.label.length, 0);
239
+ if (input.nav.length > 5 || chars > 60) return 'lg';
240
+ const measure = AI_LAYOUT_HEADER_MEASURE;
241
+ return aiLayoutHeaderRowWidth(input) <= measure.room.md * measure.fill ? 'md' : 'lg';
242
+ }
243
+ /** A source's header row as `aiLayoutHeaderRowWidth` measures it. */ export function aiLayoutFrameSourceRow(source, brandWidth) {
244
+ return {
245
+ brandWidth,
246
+ nav: source.nav.map((group)=>({
247
+ label: group.label,
248
+ menu: group.items.length > 0
249
+ })),
250
+ ctas: source.ctas.map((cta)=>({
251
+ label: cta.label,
252
+ style: cta.style
253
+ }))
254
+ };
255
+ }
256
+ /*------------------------------------------
257
+ * The header
258
+ *-----------------------------------------*/ const VARIANT = {
259
+ primary: 'contained',
260
+ secondary: 'outlined',
261
+ quiet: 'text'
262
+ };
263
+ /** The most entries one unheaded column of a mega menu holds before it is drawn as two. */ const COLUMN_ENTRIES = 7;
264
+ /** A menu's entries in the columns the source sets them in, in order; a long unheaded run in two. */ export function aiLayoutMenuColumns(entries) {
265
+ const columns = [];
266
+ for (const entry of entries){
267
+ var _entry_column;
268
+ const heading = (_entry_column = entry.column) != null ? _entry_column : null;
269
+ const last = columns[columns.length - 1];
270
+ if (last && last.heading === heading) last.entries.push(entry);
271
+ else columns.push({
272
+ heading,
273
+ entries: [
274
+ entry
275
+ ]
276
+ });
277
+ }
278
+ return columns.flatMap((column)=>{
279
+ if (column.heading || column.entries.length <= COLUMN_ENTRIES) return [
280
+ column
281
+ ];
282
+ const half = Math.ceil(column.entries.length / 2);
283
+ return [
284
+ {
285
+ heading: null,
286
+ entries: column.entries.slice(0, half)
287
+ },
288
+ {
289
+ heading: null,
290
+ entries: column.entries.slice(half)
291
+ }
292
+ ];
293
+ });
294
+ }
295
+ /** Whether a menu is drawn as a Mega Menu: its entries carry a line under them, or sit in more than one headed column. */ export function aiLayoutMenuIsMega(group) {
296
+ if (group.items.some((entry)=>entry.text)) return true;
297
+ return new Set(group.items.map((entry)=>entry.column).filter(Boolean)).size >= 2 || group.items.length > COLUMN_ENTRIES * 2;
298
+ }
299
+ /**
300
+ * The source's header drawn (AGL-4108): its menu row, its buttons and its
301
+ * phone menu. `close` is the interaction a link of the phone menu closes it
302
+ * with.
303
+ */ export function aiLayoutSourceHeader(tree, source, input) {
304
+ const breakpoint = aiLayoutNavBreakpoint(aiLayoutFrameSourceRow(source, input.brandWidth));
305
+ // A link's look is its parent's, which every link under it inherits: a
306
+ // style repeated on each is one the doctrine asks be carried once.
307
+ const link = (entry, role, interactions, sx = {})=>{
308
+ const id = tree.add('muiScreenLink', _extends({
309
+ children: entry.label
310
+ }, aiLayoutFrameSourceLinkProps(entry.to), {
311
+ renderAs: 'link',
312
+ color: 'inherit'
313
+ }), _extends({
314
+ textDecoration: 'none'
315
+ }, sx), null, role);
316
+ if (interactions) tree.nodes[id].interactions = interactions;
317
+ return id;
318
+ };
319
+ const items = source.nav.map((group)=>group.items.length ? menu(tree, group, link) : group.to ? link({
320
+ label: group.label,
321
+ to: group.to
322
+ }, 'navLink') : null);
323
+ const nav = items.some(Boolean) ? tree.add('muiStack', {
324
+ component: 'nav',
325
+ ariaLabel: 'Main',
326
+ direction: 'row',
327
+ spacing: '3',
328
+ alignItems: 'center'
329
+ }, // Every label on one line, and the row never narrower than its labels.
330
+ {
331
+ display: {
332
+ xs: 'none',
333
+ [breakpoint]: 'flex'
334
+ },
335
+ whiteSpace: 'nowrap',
336
+ flexShrink: 0
337
+ }, items, 'nav') : null;
338
+ const button = (cta, role, extra = {})=>tree.add('muiButton', _extends({
339
+ children: cta.label,
340
+ variant: VARIANT[cta.style],
341
+ color: input.onBrand ? 'secondary' : 'primary'
342
+ }, aiLayoutFrameSourceLinkProps(cta.to), extra), null, null, role);
343
+ // On the smallest phones the buttons move into the menu. Their words on one line, as the row's.
344
+ const ctas = source.ctas.length ? tree.add('muiStack', {
345
+ direction: 'row',
346
+ spacing: '1',
347
+ alignItems: 'center'
348
+ }, {
349
+ display: {
350
+ xs: 'none',
351
+ sm: 'flex'
352
+ },
353
+ flexShrink: 0,
354
+ whiteSpace: 'nowrap',
355
+ ml: 'auto'
356
+ }, source.ctas.map((cta)=>button(cta, 'cta')), 'ctas') : null;
357
+ // The phone menu: each menu as a heading over its entries, its columns'
358
+ // headings among them, the links, then the buttons.
359
+ const drawer = [];
360
+ let links = [];
361
+ const flushLinks = ()=>{
362
+ if (!links.length) return;
363
+ drawer.push(tree.add('muiStack', {
364
+ spacing: '1.5'
365
+ }, {
366
+ typography: 'subtitle1'
367
+ }, links, 'menuLinks'));
368
+ links = [];
369
+ };
370
+ for (const group of source.nav){
371
+ if (!group.items.length) {
372
+ if (group.to) links.push(link({
373
+ label: group.label,
374
+ to: group.to
375
+ }, 'menuLink', input.close));
376
+ continue;
377
+ }
378
+ flushLinks();
379
+ // The menu's name heads its entries; it links where the source's name opens a page and no entry does.
380
+ const own = aiLayoutMenuOwnPage(group);
381
+ const heading = own ? link({
382
+ label: group.label,
383
+ to: own
384
+ }, 'menuHeading', input.close, {
385
+ typography: 'overline'
386
+ }) : tree.add('muiTypography', {
387
+ children: group.label,
388
+ variant: 'overline',
389
+ component: 'p'
390
+ }, {
391
+ color: 'text.secondary'
392
+ }, null, 'menuHeading');
393
+ const entries = [];
394
+ let column = null;
395
+ for (const entry of group.items){
396
+ var _entry_column;
397
+ const under = (_entry_column = entry.column) != null ? _entry_column : null;
398
+ if (under && under !== column) entries.push(tree.add('muiTypography', {
399
+ children: under,
400
+ variant: 'caption',
401
+ component: 'p'
402
+ }, {
403
+ color: 'text.secondary'
404
+ }, null, 'menuColumn'));
405
+ column = under;
406
+ entries.push(link(entry, 'menuEntry', input.close));
407
+ }
408
+ drawer.push(tree.add('muiStack', {
409
+ spacing: '1'
410
+ }, {
411
+ typography: 'body1'
412
+ }, [
413
+ heading,
414
+ ...entries
415
+ ], 'menuGroup'));
416
+ }
417
+ flushLinks();
418
+ if (source.ctas.length) {
419
+ drawer.push(tree.add('muiStack', {
420
+ spacing: '1'
421
+ }, {
422
+ mt: 1
423
+ }, source.ctas.map((cta)=>button(cta, 'menuCta', {
424
+ fullWidth: true
425
+ })), 'menuCtas'));
426
+ }
427
+ return {
428
+ nav,
429
+ ctas,
430
+ drawer,
431
+ breakpoint
432
+ };
433
+ }
434
+ /**
435
+ * The page a menu's own name opens, where none of its entries does: a page
436
+ * of the site, never an address elsewhere (a Squarespace folder's address
437
+ * is no page). `null` otherwise.
438
+ */ export function aiLayoutMenuOwnPage(group) {
439
+ const to = group.to;
440
+ if (!to || !(to.startsWith('page:') || isRootRelativePath(to))) return null;
441
+ return group.items.some((entry)=>entry.to === to) ? null : to;
442
+ }
443
+ /**
444
+ * A top-level item with entries: the Mega Menu, its entries in the source's
445
+ * columns, each with its icon and the line under it; or the Dropdown Menu.
446
+ * Where the item's own name goes somewhere none of its entries does, the menu
447
+ * opens with that link, so the page the source's name opens stays reachable.
448
+ */ function menu(tree, group, link) {
449
+ const own = aiLayoutMenuOwnPage(group);
450
+ const overview = own ? link({
451
+ label: group.label,
452
+ to: own
453
+ }, 'menuOverview', undefined, {
454
+ fontWeight: 700
455
+ }) : null;
456
+ if (!aiLayoutMenuIsMega(group)) {
457
+ // Its entries' words wrap as the source's do; only the row's labels are held to one line.
458
+ return tree.add('muiNavMenu', {
459
+ label: group.label
460
+ }, {
461
+ flexShrink: 0
462
+ }, [
463
+ tree.add('muiStack', {
464
+ spacing: '1'
465
+ }, {
466
+ whiteSpace: 'normal'
467
+ }, [
468
+ overview,
469
+ ...group.items.map((entry)=>link(entry, 'menuItem'))
470
+ ], 'menuItems')
471
+ ], 'navMenu');
472
+ }
473
+ const columns = aiLayoutMenuColumns(group.items);
474
+ // Its columns side by side from a tablet up, as many to a row as the source sets, three at most.
475
+ const span = 12 / Math.min(3, Math.max(1, columns.length));
476
+ const cells = columns.map((column)=>tree.add('muiGrid', {
477
+ size: `xs:12 sm:${span}`
478
+ }, null, [
479
+ tree.add('muiStack', {
480
+ spacing: '1'
481
+ }, null, [
482
+ column.heading ? tree.add('muiTypography', {
483
+ children: column.heading,
484
+ variant: 'overline',
485
+ component: 'p'
486
+ }, {
487
+ color: 'text.secondary'
488
+ }, null, 'menuColumnHeading') : null,
489
+ ...column.entries.map((entry)=>entry.text || entry.iconPath ? described(tree, entry) : link(entry, 'menuItem'))
490
+ ], 'menuColumn')
491
+ ], 'menuCell'));
492
+ const grid = tree.add('muiGrid', {
493
+ container: true,
494
+ spacing: '2'
495
+ }, null, cells, 'menuColumns');
496
+ // Words in the panel wrap as the source's do; only the row's labels are held to one line.
497
+ const panel = tree.add('muiStack', {
498
+ spacing: '2'
499
+ }, {
500
+ whiteSpace: 'normal'
501
+ }, [
502
+ overview,
503
+ grid
504
+ ], 'menuPanel');
505
+ return tree.add('muiMegaMenu', {
506
+ label: group.label,
507
+ panelWidth: 'wide'
508
+ }, {
509
+ flexShrink: 0
510
+ }, [
511
+ panel
512
+ ], 'megaMenu');
513
+ }
514
+ /** A mega menu's entry: the whole of it one link, its icon beside its name and the line under it. */ function described(tree, entry) {
515
+ // In the brand's color, as the source draws its menu's icons.
516
+ const icon = entry.iconPath ? tree.add('icon', {
517
+ iconPath: entry.iconPath,
518
+ size: '24',
519
+ color: 'primary.main'
520
+ }, null, null, 'menuIcon') : null;
521
+ const name = tree.add('muiTypography', {
522
+ children: entry.label,
523
+ variant: 'subtitle2',
524
+ component: 'p'
525
+ }, {
526
+ fontWeight: 600
527
+ }, null, 'menuEntryName');
528
+ const line = entry.text ? tree.add('muiTypography', {
529
+ children: entry.text,
530
+ variant: 'body2',
531
+ component: 'p'
532
+ }, {
533
+ color: 'text.secondary'
534
+ }, null, 'menuEntryText') : null;
535
+ const words = tree.add('muiStack', {
536
+ spacing: '0.25'
537
+ }, null, [
538
+ name,
539
+ line
540
+ ], 'menuEntryWords');
541
+ const row = tree.add('muiStack', {
542
+ direction: 'row',
543
+ spacing: '1.5',
544
+ alignItems: 'flex-start'
545
+ }, null, [
546
+ icon,
547
+ words
548
+ ], 'menuEntryRow');
549
+ return tree.add('muiLinkBox', _extends({}, aiLayoutFrameSourceLinkProps(entry.to), {
550
+ ariaLabel: entry.label
551
+ }), {
552
+ color: 'inherit'
553
+ }, [
554
+ row
555
+ ], 'menuEntry');
556
+ }
557
+ /*------------------------------------------
558
+ * The footer
559
+ *-----------------------------------------*/ /** The networks the Social Links element draws, by the reader's name for each. */ const SOCIAL_FIELDS = {
560
+ x: 'twitter',
561
+ twitter: 'twitter',
562
+ instagram: 'instagram',
563
+ facebook: 'facebook',
564
+ linkedin: 'linkedin',
565
+ youtube: 'youtube',
566
+ github: 'github'
567
+ };
568
+ /** A phone number as a link that calls it: its digits, a `+` only first; `null` where it is no number. */ export function aiLayoutTelHref(phone) {
569
+ const digits = phone.replace(/[^\d+]/g, '').replace(/(?!^)\+/g, '');
570
+ const href = `tel:${digits}`;
571
+ return isContactHref(href) ? href : null;
572
+ }
573
+ /** The source's footer drawn (AGL-4108): its words and profiles beside its columns, its © line and other links under a rule. */ export function aiLayoutSourceFooter(tree, footer, input) {
574
+ var _footer_copyright;
575
+ // The footer's words are quiet and small, set once on the footer for every line and link under it.
576
+ const quiet = {
577
+ color: 'text.secondary'
578
+ };
579
+ const footerLink = (entry, role)=>tree.add('muiScreenLink', _extends({
580
+ children: entry.label
581
+ }, aiLayoutFrameSourceLinkProps(entry.to), {
582
+ renderAs: 'link',
583
+ color: 'inherit'
584
+ }), {
585
+ textDecoration: 'none'
586
+ }, null, role);
587
+ // Its words, its profiles and how to reach it, beside the columns.
588
+ const lines = footer.text.map((line)=>tree.add('muiTypography', {
589
+ children: line,
590
+ variant: 'body2',
591
+ component: 'p'
592
+ }, null, null, 'footerText'));
593
+ const drawn = {};
594
+ const others = [];
595
+ for (const profile of footer.social){
596
+ const field = SOCIAL_FIELDS[profile.network.toLowerCase()];
597
+ if (field && !drawn[field]) drawn[field] = profile.to;
598
+ else if (!field) others.push(profile);
599
+ }
600
+ const icons = Object.keys(drawn).length ? tree.add('socialLinks', drawn, {
601
+ ml: -1
602
+ }, null, 'social') : null;
603
+ // A profile the Social Links element has no icon for is linked by its name, beside the icons.
604
+ const named = others.map((profile)=>footerLink(profile, 'socialLink'));
605
+ const profiles = icons || named.length ? tree.add('muiStack', {
606
+ direction: 'row',
607
+ spacing: '1.5',
608
+ alignItems: 'center',
609
+ useFlexGap: true,
610
+ flexWrap: 'wrap'
611
+ }, null, [
612
+ icons,
613
+ ...named
614
+ ], 'profiles') : null;
615
+ const tel = footer.phone ? aiLayoutTelHref(footer.phone) : null;
616
+ const mail = footer.email ? `mailto:${footer.email}` : null;
617
+ const reach = [
618
+ footer.address ? tree.add('muiTypography', {
619
+ children: footer.address,
620
+ variant: 'body2',
621
+ component: 'p'
622
+ }, null, null, 'address') : null,
623
+ footer.phone && tel ? footerLink({
624
+ label: footer.phone,
625
+ to: tel
626
+ }, 'phone') : null,
627
+ footer.email && mail && isContactHref(mail) ? footerLink({
628
+ label: footer.email,
629
+ to: mail
630
+ }, 'email') : null
631
+ ].filter((id)=>!!id);
632
+ const about = [
633
+ ...lines,
634
+ profiles,
635
+ ...reach.length ? [
636
+ tree.add('muiStack', {
637
+ spacing: '0.5'
638
+ }, null, reach, 'reach')
639
+ ] : []
640
+ ].filter((id)=>!!id);
641
+ // Its columns, each under its heading.
642
+ const count = footer.columns.length;
643
+ const span = Math.max(2, Math.min(6, Math.floor(12 / Math.max(1, count))));
644
+ const columns = footer.columns.map((column)=>tree.add('muiGrid', {
645
+ size: `xs:12 sm:4 md:${span}`
646
+ }, null, [
647
+ tree.add('muiStack', {
648
+ spacing: '1'
649
+ }, null, [
650
+ // A heading in the text's own color over its quiet links.
651
+ tree.add('muiTypography', {
652
+ children: column.heading,
653
+ variant: 'subtitle2',
654
+ component: 'p'
655
+ }, {
656
+ color: 'text.primary'
657
+ }, null, 'footerHeading'),
658
+ ...column.links.map((entry)=>footerLink(entry, 'footerLink'))
659
+ ], 'footerColumn')
660
+ ], 'footerCell'));
661
+ const grid = columns.length ? tree.add('muiGrid', {
662
+ container: true,
663
+ spacing: '3'
664
+ }, null, columns, 'footerColumns') : null;
665
+ const aboutStack = about.length ? tree.add('muiStack', {
666
+ spacing: '2'
667
+ }, null, about, 'footerAbout') : null;
668
+ const top = aboutStack && grid ? tree.add('muiGrid', {
669
+ container: true,
670
+ spacing: '4'
671
+ }, {
672
+ alignItems: 'flex-start'
673
+ }, [
674
+ tree.add('muiGrid', {
675
+ size: 'xs:12 md:3'
676
+ }, null, [
677
+ aboutStack
678
+ ], 'footerAboutCell'),
679
+ tree.add('muiGrid', {
680
+ size: 'xs:12 md:9'
681
+ }, null, [
682
+ grid
683
+ ], 'footerColumnsCell')
684
+ ], 'footerTop') : aboutStack != null ? aboutStack : grid;
685
+ // The © line word for word, and the links outside the columns beside it.
686
+ const copyright = tree.add('muiTypography', {
687
+ children: (_footer_copyright = footer.copyright) != null ? _footer_copyright : `© ${input.name}`,
688
+ variant: 'body2',
689
+ component: 'p'
690
+ }, null, null, 'copyright');
691
+ const legal = footer.links.length ? tree.add('muiStack', {
692
+ direction: 'row',
693
+ spacing: '2',
694
+ useFlexGap: true,
695
+ flexWrap: 'wrap'
696
+ }, null, footer.links.map((entry)=>footerLink(entry, 'footerLegal')), 'footerLinks') : null;
697
+ const bottom = tree.add('muiStack', {
698
+ spacing: '2',
699
+ useFlexGap: true
700
+ }, {
701
+ pt: 3,
702
+ borderTop: 1,
703
+ borderColor: 'divider',
704
+ flexDirection: {
705
+ xs: 'column',
706
+ md: 'row'
707
+ },
708
+ justifyContent: 'space-between',
709
+ alignItems: {
710
+ xs: 'flex-start',
711
+ md: 'center'
712
+ }
713
+ }, [
714
+ copyright,
715
+ legal
716
+ ], 'footerBottom');
717
+ return {
718
+ top,
719
+ bottom,
720
+ sx: _extends({}, quiet, {
721
+ typography: 'body2'
722
+ })
723
+ };
724
+ }
725
+
726
+ //# sourceMappingURL=ai-layout-frame-source.js.map