@aglyn/plugins-ai 1.0.0-beta.246 → 1.0.0-beta.247
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.
- package/package.json +17 -17
- package/src/lib/activity/ai-activity-actions.js +5 -1
- package/src/lib/activity/ai-activity-actions.js.map +1 -1
- package/src/lib/components/ai-job-links.js +14 -0
- package/src/lib/components/ai-job-links.js.map +1 -1
- package/src/lib/components/ai-site-start-card.component.d.ts +1 -1
- package/src/lib/components/ai-site-start-card.component.js +92 -19
- package/src/lib/components/ai-site-start-card.component.js.map +1 -1
- package/src/lib/declarations.server.js +12 -0
- package/src/lib/declarations.server.js.map +1 -1
- package/src/lib/jobs/ai-build-capabilities.js +56 -2
- package/src/lib/jobs/ai-build-capabilities.js.map +1 -1
- package/src/lib/jobs/ai-job-build-step.d.ts +3 -8
- package/src/lib/jobs/ai-job-build-step.js +49 -12
- package/src/lib/jobs/ai-job-build-step.js.map +1 -1
- package/src/lib/jobs/ai-job-form-dataset.d.ts +8 -5
- package/src/lib/jobs/ai-job-form-dataset.js +28 -10
- package/src/lib/jobs/ai-job-form-dataset.js.map +1 -1
- package/src/lib/jobs/ai-job-form-step.d.ts +20 -4
- package/src/lib/jobs/ai-job-form-step.js +98 -28
- package/src/lib/jobs/ai-job-form-step.js.map +1 -1
- package/src/lib/jobs/ai-job-layout-language.d.ts +6 -2
- package/src/lib/jobs/ai-job-layout-language.js +16 -5
- package/src/lib/jobs/ai-job-layout-language.js.map +1 -1
- package/src/lib/jobs/ai-job-layout-step.js +4 -1
- package/src/lib/jobs/ai-job-layout-step.js.map +1 -1
- package/src/lib/jobs/ai-job-media-step.d.ts +239 -0
- package/src/lib/jobs/ai-job-media-step.js +610 -0
- package/src/lib/jobs/ai-job-media-step.js.map +1 -0
- package/src/lib/jobs/ai-job-page-language.d.ts +2 -0
- package/src/lib/jobs/ai-job-page-language.js +9 -0
- package/src/lib/jobs/ai-job-page-language.js.map +1 -1
- package/src/lib/jobs/ai-job-page-sections.js +4 -3
- package/src/lib/jobs/ai-job-page-sections.js.map +1 -1
- package/src/lib/jobs/ai-job-page-step.js +3 -2
- package/src/lib/jobs/ai-job-page-step.js.map +1 -1
- package/src/lib/jobs/ai-job-plan-step.d.ts +10 -1
- package/src/lib/jobs/ai-job-plan-step.js +73 -15
- package/src/lib/jobs/ai-job-plan-step.js.map +1 -1
- package/src/lib/jobs/ai-job-settings-step.d.ts +0 -4
- package/src/lib/jobs/ai-job-settings-step.js +2 -7
- package/src/lib/jobs/ai-job-settings-step.js.map +1 -1
- package/src/lib/jobs/ai-job-site-content.d.ts +2 -2
- package/src/lib/jobs/ai-job-site-content.js +5 -3
- package/src/lib/jobs/ai-job-site-content.js.map +1 -1
- package/src/lib/jobs/ai-job-site-datasets.d.ts +1 -1
- package/src/lib/jobs/ai-job-site-datasets.js +3 -2
- package/src/lib/jobs/ai-job-site-datasets.js.map +1 -1
- package/src/lib/jobs/ai-job-site-events.d.ts +148 -0
- package/src/lib/jobs/ai-job-site-events.js +313 -0
- package/src/lib/jobs/ai-job-site-events.js.map +1 -0
- package/src/lib/jobs/ai-job-site-step.d.ts +9 -3
- package/src/lib/jobs/ai-job-site-step.js +63 -17
- package/src/lib/jobs/ai-job-site-step.js.map +1 -1
- package/src/lib/jobs/ai-job-tracking-step.d.ts +184 -0
- package/src/lib/jobs/ai-job-tracking-step.js +501 -0
- package/src/lib/jobs/ai-job-tracking-step.js.map +1 -0
- package/src/lib/jobs/ai-layout-stock-photos.d.ts +21 -0
- package/src/lib/jobs/ai-layout-stock-photos.js +41 -4
- package/src/lib/jobs/ai-layout-stock-photos.js.map +1 -1
- package/src/lib/jobs/ai-site-publish.js +2 -3
- package/src/lib/jobs/ai-site-publish.js.map +1 -1
- package/src/lib/jobs/ai-stock-photo-picker.d.ts +24 -0
- package/src/lib/jobs/ai-stock-photo-picker.js +108 -0
- package/src/lib/jobs/ai-stock-photo-picker.js.map +1 -0
- package/src/lib/jobs/fixtures/ai-jazz-events-build-plan-recording.d.ts +37 -0
- package/src/lib/jobs/fixtures/ai-jazz-events-build-plan-recording.js +47 -0
- package/src/lib/jobs/fixtures/ai-jazz-events-build-plan-recording.js.map +1 -0
- package/src/lib/jobs/fixtures/ai-stock-photo-agl3760-recordings.d.ts +21 -0
- package/src/lib/jobs/fixtures/ai-stock-photo-agl3760-recordings.js +128 -0
- package/src/lib/jobs/fixtures/ai-stock-photo-agl3760-recordings.js.map +1 -0
- package/src/lib/layout-language/ai-layout-compiler.d.ts +10 -4
- package/src/lib/layout-language/ai-layout-compiler.js +38 -19
- package/src/lib/layout-language/ai-layout-compiler.js.map +1 -1
- package/src/lib/layout-language/ai-layout-frame.d.ts +7 -5
- package/src/lib/layout-language/ai-layout-frame.js +12 -9
- package/src/lib/layout-language/ai-layout-frame.js.map +1 -1
- package/src/lib/layout-language/ai-layout-gaps.js +3 -1
- package/src/lib/layout-language/ai-layout-gaps.js.map +1 -1
- package/src/lib/layout-language/ai-layout-labels.d.ts +70 -0
- package/src/lib/layout-language/ai-layout-labels.js +162 -0
- package/src/lib/layout-language/ai-layout-labels.js.map +1 -0
- package/src/lib/layout-language/ai-layout-links.js +5 -2
- package/src/lib/layout-language/ai-layout-links.js.map +1 -1
- package/src/lib/layout-language/ai-layout-listings.d.ts +14 -1
- package/src/lib/layout-language/ai-layout-listings.js +32 -5
- package/src/lib/layout-language/ai-layout-listings.js.map +1 -1
- package/src/lib/model/ai-build-event-page.d.ts +70 -0
- package/src/lib/model/ai-build-event-page.js +145 -0
- package/src/lib/model/ai-build-event-page.js.map +1 -0
- package/src/lib/model/ai-build-job.d.ts +21 -0
- package/src/lib/model/ai-build-job.js +39 -0
- package/src/lib/model/ai-build-job.js.map +1 -1
- package/src/lib/model/ai-build-plan.d.ts +25 -14
- package/src/lib/model/ai-build-plan.js +33 -23
- package/src/lib/model/ai-build-plan.js.map +1 -1
- package/src/lib/model/ai-job-activity.js +3 -1
- package/src/lib/model/ai-job-activity.js.map +1 -1
- package/src/lib/model/ai-jobs.types.d.ts +10 -2
- package/src/lib/model/ai-jobs.types.js +3 -1
- package/src/lib/model/ai-jobs.types.js.map +1 -1
- package/src/lib/model/ai-plan-capabilities.d.ts +8 -0
- package/src/lib/model/ai-plan-capabilities.js.map +1 -1
- package/src/lib/model/ai-site-inventory.d.ts +2 -0
- package/src/lib/model/ai-site-inventory.js +5 -0
- package/src/lib/model/ai-site-inventory.js.map +1 -1
- package/src/lib/model/ai-site-job.d.ts +2 -2
- package/src/lib/model/ai-site-job.js +3 -3
- package/src/lib/model/ai-site-job.js.map +1 -1
- package/src/lib/runtime/ai-doctrine-validators.d.ts +25 -3
- package/src/lib/runtime/ai-doctrine-validators.js +53 -6
- package/src/lib/runtime/ai-doctrine-validators.js.map +1 -1
- package/src/lib/runtime/ai-doctrine.d.ts +6 -1
- package/src/lib/runtime/ai-doctrine.js +13 -9
- package/src/lib/runtime/ai-doctrine.js.map +1 -1
- package/src/lib/runtime/ai-eval-live.d.ts +1 -1
- package/src/lib/runtime/ai-eval-live.js +1 -1
- package/src/lib/runtime/ai-eval-live.js.map +1 -1
- package/src/lib/runtime/ai-palette.d.ts +3 -1
- package/src/lib/runtime/ai-palette.generated.js +498 -10
- package/src/lib/runtime/ai-palette.generated.js.map +1 -1
- package/src/lib/runtime/ai-palette.js +5 -3
- package/src/lib/runtime/ai-palette.js.map +1 -1
- package/src/lib/runtime/ai-template-examples.js +8 -2
- package/src/lib/runtime/ai-template-examples.js.map +1 -1
- package/src/lib/server.js +8 -0
- package/src/lib/server.js.map +1 -1
|
@@ -0,0 +1,162 @@
|
|
|
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
|
+
* THE WORDS THE PLATFORM WRITES ON A PAGE ITSELF, IN THE SITE'S LANGUAGE (AGL-3760).
|
|
18
|
+
*
|
|
19
|
+
* The model writes a page's copy in the language its brief is in; a few
|
|
20
|
+
* labels are the compiler's own — the button under a featured list that opens
|
|
21
|
+
* the whole of it, a post card's link, a store's fixed lines. A Spanish
|
|
22
|
+
* guided start (beta.246) showed "See all" under "Lo que horneamos". Each
|
|
23
|
+
* label is English, then the same words in the languages below; a site in a
|
|
24
|
+
* language not listed keeps the English.
|
|
25
|
+
*/ /** The labels the compiler and the frame place, in English. */ export const AI_LAYOUT_LABELS_EN = {
|
|
26
|
+
/** The button under a featured band of the site's records that opens the whole list. */ seeAll: 'See all',
|
|
27
|
+
/** The same, for the blog's posts. */ allPosts: 'All posts',
|
|
28
|
+
/** The same, for the site's music. */ allMusic: 'All music',
|
|
29
|
+
/** The same, for the site's events. */ allEvents: 'All events',
|
|
30
|
+
/** The button a storefront's hero, or a featured band of products, opens the shop with. */ shopAll: 'Shop all',
|
|
31
|
+
/** A post card's link to the post. */ readPost: 'Read the post',
|
|
32
|
+
/** A record card's link to the record's own page. */ viewDetails: 'View details',
|
|
33
|
+
/** A store's newsletter sign-up button. */ subscribe: 'Subscribe',
|
|
34
|
+
/** A store's reviews band, where neither the design nor the plan heads it. */ reviewsHeading: 'What our customers say',
|
|
35
|
+
/** A store's sign-up band, where neither the design nor the plan heads it. */ signupHeading: 'Join our newsletter',
|
|
36
|
+
/** The header's link to a store's account page. */ account: 'Account',
|
|
37
|
+
/** The footer's heading over a store's account and policy links. */ customerCare: 'Customer care',
|
|
38
|
+
/** A store's announcement bar, where its design gave it no line. */ storeAnnouncement: 'Shop the full collection',
|
|
39
|
+
/** The way into the shop beside the design's own announcement. */ shopNow: 'Shop now',
|
|
40
|
+
/** A store's Product grid while the store lists nothing yet. */ storeEmptyTitle: 'New pieces are on the way',
|
|
41
|
+
storeEmptyText: 'Our first pieces are being finished now. Get in touch and we will let you know the moment they land.',
|
|
42
|
+
storeEmptyTextAlone: 'Our first pieces are being finished now. Check back soon.',
|
|
43
|
+
/** The way to get in touch under that empty grid, where the site has a page for it. */ getInTouch: 'Get in touch'
|
|
44
|
+
};
|
|
45
|
+
/** The same labels in each language listed, by its two-letter code. */ const LABELS = {
|
|
46
|
+
es: {
|
|
47
|
+
seeAll: 'Ver todo',
|
|
48
|
+
allPosts: 'Todas las entradas',
|
|
49
|
+
allMusic: 'Toda la música',
|
|
50
|
+
allEvents: 'Todos los eventos',
|
|
51
|
+
shopAll: 'Ver la tienda',
|
|
52
|
+
readPost: 'Leer la entrada',
|
|
53
|
+
viewDetails: 'Ver detalles',
|
|
54
|
+
subscribe: 'Suscribirme',
|
|
55
|
+
reviewsHeading: 'Lo que dicen nuestros clientes',
|
|
56
|
+
signupHeading: 'Suscríbete a nuestro boletín',
|
|
57
|
+
account: 'Mi cuenta',
|
|
58
|
+
customerCare: 'Atención al cliente',
|
|
59
|
+
storeAnnouncement: 'Descubre toda la colección',
|
|
60
|
+
shopNow: 'Comprar ahora',
|
|
61
|
+
storeEmptyTitle: 'Pronto llegarán nuevas piezas',
|
|
62
|
+
storeEmptyText: 'Estamos terminando nuestras primeras piezas. Escríbenos y te avisaremos en cuanto lleguen.',
|
|
63
|
+
storeEmptyTextAlone: 'Estamos terminando nuestras primeras piezas. Vuelve pronto.',
|
|
64
|
+
getInTouch: 'Escríbenos'
|
|
65
|
+
},
|
|
66
|
+
fr: {
|
|
67
|
+
seeAll: 'Tout voir',
|
|
68
|
+
allPosts: 'Tous les articles',
|
|
69
|
+
allMusic: 'Toute la musique',
|
|
70
|
+
allEvents: 'Tous les événements',
|
|
71
|
+
shopAll: 'Voir la boutique',
|
|
72
|
+
readPost: 'Lire l’article',
|
|
73
|
+
viewDetails: 'Voir les détails',
|
|
74
|
+
subscribe: 'S’abonner',
|
|
75
|
+
reviewsHeading: 'Ce que disent nos clients',
|
|
76
|
+
signupHeading: 'Abonnez-vous à notre newsletter',
|
|
77
|
+
account: 'Mon compte',
|
|
78
|
+
customerCare: 'Service client',
|
|
79
|
+
storeAnnouncement: 'Découvrez toute la collection',
|
|
80
|
+
shopNow: 'Acheter',
|
|
81
|
+
storeEmptyTitle: 'De nouvelles pièces arrivent bientôt',
|
|
82
|
+
storeEmptyText: 'Nous terminons nos premières pièces. Écrivez-nous et nous vous préviendrons dès leur arrivée.',
|
|
83
|
+
storeEmptyTextAlone: 'Nous terminons nos premières pièces. Revenez bientôt.',
|
|
84
|
+
getInTouch: 'Écrivez-nous'
|
|
85
|
+
},
|
|
86
|
+
pt: {
|
|
87
|
+
seeAll: 'Ver tudo',
|
|
88
|
+
allPosts: 'Todos os posts',
|
|
89
|
+
allMusic: 'Todas as músicas',
|
|
90
|
+
allEvents: 'Todos os eventos',
|
|
91
|
+
shopAll: 'Ver a loja',
|
|
92
|
+
readPost: 'Ler o post',
|
|
93
|
+
viewDetails: 'Ver detalhes',
|
|
94
|
+
subscribe: 'Inscrever-me',
|
|
95
|
+
reviewsHeading: 'O que dizem nossos clientes',
|
|
96
|
+
signupHeading: 'Assine nossa newsletter',
|
|
97
|
+
account: 'Minha conta',
|
|
98
|
+
customerCare: 'Atendimento ao cliente',
|
|
99
|
+
storeAnnouncement: 'Conheça toda a coleção',
|
|
100
|
+
shopNow: 'Comprar agora',
|
|
101
|
+
storeEmptyTitle: 'Novas peças estão a caminho',
|
|
102
|
+
storeEmptyText: 'Estamos finalizando nossas primeiras peças. Fale conosco e avisaremos assim que chegarem.',
|
|
103
|
+
storeEmptyTextAlone: 'Estamos finalizando nossas primeiras peças. Volte em breve.',
|
|
104
|
+
getInTouch: 'Fale conosco'
|
|
105
|
+
},
|
|
106
|
+
de: {
|
|
107
|
+
seeAll: 'Alle ansehen',
|
|
108
|
+
allPosts: 'Alle Beiträge',
|
|
109
|
+
allMusic: 'Alle Musik',
|
|
110
|
+
allEvents: 'Alle Veranstaltungen',
|
|
111
|
+
shopAll: 'Zum Shop',
|
|
112
|
+
readPost: 'Beitrag lesen',
|
|
113
|
+
viewDetails: 'Details ansehen',
|
|
114
|
+
subscribe: 'Abonnieren',
|
|
115
|
+
reviewsHeading: 'Was unsere Kunden sagen',
|
|
116
|
+
signupHeading: 'Abonnieren Sie unseren Newsletter',
|
|
117
|
+
account: 'Mein Konto',
|
|
118
|
+
customerCare: 'Kundenservice',
|
|
119
|
+
storeAnnouncement: 'Entdecken Sie die ganze Kollektion',
|
|
120
|
+
shopNow: 'Jetzt kaufen',
|
|
121
|
+
storeEmptyTitle: 'Neue Stücke sind unterwegs',
|
|
122
|
+
storeEmptyText: 'Unsere ersten Stücke werden gerade fertig. Schreiben Sie uns, und wir melden uns, sobald sie da sind.',
|
|
123
|
+
storeEmptyTextAlone: 'Unsere ersten Stücke werden gerade fertig. Schauen Sie bald wieder vorbei.',
|
|
124
|
+
getInTouch: 'Kontakt aufnehmen'
|
|
125
|
+
},
|
|
126
|
+
it: {
|
|
127
|
+
seeAll: 'Vedi tutto',
|
|
128
|
+
allPosts: 'Tutti gli articoli',
|
|
129
|
+
allMusic: 'Tutta la musica',
|
|
130
|
+
allEvents: 'Tutti gli eventi',
|
|
131
|
+
shopAll: 'Vai al negozio',
|
|
132
|
+
readPost: 'Leggi l’articolo',
|
|
133
|
+
viewDetails: 'Vedi dettagli',
|
|
134
|
+
subscribe: 'Iscriviti',
|
|
135
|
+
reviewsHeading: 'Cosa dicono i nostri clienti',
|
|
136
|
+
signupHeading: 'Iscriviti alla nostra newsletter',
|
|
137
|
+
account: 'Il mio account',
|
|
138
|
+
customerCare: 'Assistenza clienti',
|
|
139
|
+
storeAnnouncement: 'Scopri tutta la collezione',
|
|
140
|
+
shopNow: 'Acquista ora',
|
|
141
|
+
storeEmptyTitle: 'Nuovi pezzi in arrivo',
|
|
142
|
+
storeEmptyText: 'Stiamo finendo i nostri primi pezzi. Scrivici e ti avviseremo appena arrivano.',
|
|
143
|
+
storeEmptyTextAlone: 'Stiamo finendo i nostri primi pezzi. Torna presto.',
|
|
144
|
+
getInTouch: 'Scrivici'
|
|
145
|
+
}
|
|
146
|
+
};
|
|
147
|
+
/** A language code's two-letter language, lowercase: `es` of `es-MX`; empty for none. */ export function aiLayoutLanguageOf(language) {
|
|
148
|
+
var _ref;
|
|
149
|
+
var _exec;
|
|
150
|
+
return typeof language === 'string' ? ((_ref = (_exec = /^([a-z]{2})(?:[-_]|$)/i.exec(language.trim())) == null ? void 0 : _exec[1]) != null ? _ref : '').toLowerCase() : '';
|
|
151
|
+
}
|
|
152
|
+
/** Whether a site's language is one other than English: a code that names one. */ export function aiLayoutSpeaksOtherThanEnglish(language) {
|
|
153
|
+
const code = aiLayoutLanguageOf(language);
|
|
154
|
+
return code !== '' && code !== 'en';
|
|
155
|
+
}
|
|
156
|
+
/** A label the platform places, in the site's language where it is listed, else in English. */ export function aiLayoutLabel(key, language) {
|
|
157
|
+
var _ref;
|
|
158
|
+
var _LABELS_aiLayoutLanguageOf;
|
|
159
|
+
return (_ref = (_LABELS_aiLayoutLanguageOf = LABELS[aiLayoutLanguageOf(language)]) == null ? void 0 : _LABELS_aiLayoutLanguageOf[key]) != null ? _ref : AI_LAYOUT_LABELS_EN[key];
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
//# sourceMappingURL=ai-layout-labels.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../../../../../../libs/plugins/ai/src/lib/layout-language/ai-layout-labels.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 * THE WORDS THE PLATFORM WRITES ON A PAGE ITSELF, IN THE SITE'S LANGUAGE (AGL-3760).\n *\n * The model writes a page's copy in the language its brief is in; a few\n * labels are the compiler's own — the button under a featured list that opens\n * the whole of it, a post card's link, a store's fixed lines. A Spanish\n * guided start (beta.246) showed \"See all\" under \"Lo que horneamos\". Each\n * label is English, then the same words in the languages below; a site in a\n * language not listed keeps the English.\n */\n\n/** The labels the compiler and the frame place, in English. */\nexport const AI_LAYOUT_LABELS_EN = {\n /** The button under a featured band of the site's records that opens the whole list. */\n seeAll: 'See all',\n /** The same, for the blog's posts. */\n allPosts: 'All posts',\n /** The same, for the site's music. */\n allMusic: 'All music',\n /** The same, for the site's events. */\n allEvents: 'All events',\n /** The button a storefront's hero, or a featured band of products, opens the shop with. */\n shopAll: 'Shop all',\n /** A post card's link to the post. */\n readPost: 'Read the post',\n /** A record card's link to the record's own page. */\n viewDetails: 'View details',\n /** A store's newsletter sign-up button. */\n subscribe: 'Subscribe',\n /** A store's reviews band, where neither the design nor the plan heads it. */\n reviewsHeading: 'What our customers say',\n /** A store's sign-up band, where neither the design nor the plan heads it. */\n signupHeading: 'Join our newsletter',\n /** The header's link to a store's account page. */\n account: 'Account',\n /** The footer's heading over a store's account and policy links. */\n customerCare: 'Customer care',\n /** A store's announcement bar, where its design gave it no line. */\n storeAnnouncement: 'Shop the full collection',\n /** The way into the shop beside the design's own announcement. */\n shopNow: 'Shop now',\n /** A store's Product grid while the store lists nothing yet. */\n storeEmptyTitle: 'New pieces are on the way',\n storeEmptyText: 'Our first pieces are being finished now. Get in touch and we will let you know the moment they land.',\n storeEmptyTextAlone: 'Our first pieces are being finished now. Check back soon.',\n /** The way to get in touch under that empty grid, where the site has a page for it. */\n getInTouch: 'Get in touch',\n} as const\n\nexport type AiLayoutLabel = keyof typeof AI_LAYOUT_LABELS_EN\n\ntype Labels = Record<AiLayoutLabel, string>\n\n/** The same labels in each language listed, by its two-letter code. */\nconst LABELS: Readonly<Record<string, Labels>> = {\n es: {\n seeAll: 'Ver todo',\n allPosts: 'Todas las entradas',\n allMusic: 'Toda la música',\n allEvents: 'Todos los eventos',\n shopAll: 'Ver la tienda',\n readPost: 'Leer la entrada',\n viewDetails: 'Ver detalles',\n subscribe: 'Suscribirme',\n reviewsHeading: 'Lo que dicen nuestros clientes',\n signupHeading: 'Suscríbete a nuestro boletín',\n account: 'Mi cuenta',\n customerCare: 'Atención al cliente',\n storeAnnouncement: 'Descubre toda la colección',\n shopNow: 'Comprar ahora',\n storeEmptyTitle: 'Pronto llegarán nuevas piezas',\n storeEmptyText: 'Estamos terminando nuestras primeras piezas. Escríbenos y te avisaremos en cuanto lleguen.',\n storeEmptyTextAlone: 'Estamos terminando nuestras primeras piezas. Vuelve pronto.',\n getInTouch: 'Escríbenos',\n },\n fr: {\n seeAll: 'Tout voir',\n allPosts: 'Tous les articles',\n allMusic: 'Toute la musique',\n allEvents: 'Tous les événements',\n shopAll: 'Voir la boutique',\n readPost: 'Lire l’article',\n viewDetails: 'Voir les détails',\n subscribe: 'S’abonner',\n reviewsHeading: 'Ce que disent nos clients',\n signupHeading: 'Abonnez-vous à notre newsletter',\n account: 'Mon compte',\n customerCare: 'Service client',\n storeAnnouncement: 'Découvrez toute la collection',\n shopNow: 'Acheter',\n storeEmptyTitle: 'De nouvelles pièces arrivent bientôt',\n storeEmptyText: 'Nous terminons nos premières pièces. Écrivez-nous et nous vous préviendrons dès leur arrivée.',\n storeEmptyTextAlone: 'Nous terminons nos premières pièces. Revenez bientôt.',\n getInTouch: 'Écrivez-nous',\n },\n pt: {\n seeAll: 'Ver tudo',\n allPosts: 'Todos os posts',\n allMusic: 'Todas as músicas',\n allEvents: 'Todos os eventos',\n shopAll: 'Ver a loja',\n readPost: 'Ler o post',\n viewDetails: 'Ver detalhes',\n subscribe: 'Inscrever-me',\n reviewsHeading: 'O que dizem nossos clientes',\n signupHeading: 'Assine nossa newsletter',\n account: 'Minha conta',\n customerCare: 'Atendimento ao cliente',\n storeAnnouncement: 'Conheça toda a coleção',\n shopNow: 'Comprar agora',\n storeEmptyTitle: 'Novas peças estão a caminho',\n storeEmptyText: 'Estamos finalizando nossas primeiras peças. Fale conosco e avisaremos assim que chegarem.',\n storeEmptyTextAlone: 'Estamos finalizando nossas primeiras peças. Volte em breve.',\n getInTouch: 'Fale conosco',\n },\n de: {\n seeAll: 'Alle ansehen',\n allPosts: 'Alle Beiträge',\n allMusic: 'Alle Musik',\n allEvents: 'Alle Veranstaltungen',\n shopAll: 'Zum Shop',\n readPost: 'Beitrag lesen',\n viewDetails: 'Details ansehen',\n subscribe: 'Abonnieren',\n reviewsHeading: 'Was unsere Kunden sagen',\n signupHeading: 'Abonnieren Sie unseren Newsletter',\n account: 'Mein Konto',\n customerCare: 'Kundenservice',\n storeAnnouncement: 'Entdecken Sie die ganze Kollektion',\n shopNow: 'Jetzt kaufen',\n storeEmptyTitle: 'Neue Stücke sind unterwegs',\n storeEmptyText: 'Unsere ersten Stücke werden gerade fertig. Schreiben Sie uns, und wir melden uns, sobald sie da sind.',\n storeEmptyTextAlone: 'Unsere ersten Stücke werden gerade fertig. Schauen Sie bald wieder vorbei.',\n getInTouch: 'Kontakt aufnehmen',\n },\n it: {\n seeAll: 'Vedi tutto',\n allPosts: 'Tutti gli articoli',\n allMusic: 'Tutta la musica',\n allEvents: 'Tutti gli eventi',\n shopAll: 'Vai al negozio',\n readPost: 'Leggi l’articolo',\n viewDetails: 'Vedi dettagli',\n subscribe: 'Iscriviti',\n reviewsHeading: 'Cosa dicono i nostri clienti',\n signupHeading: 'Iscriviti alla nostra newsletter',\n account: 'Il mio account',\n customerCare: 'Assistenza clienti',\n storeAnnouncement: 'Scopri tutta la collezione',\n shopNow: 'Acquista ora',\n storeEmptyTitle: 'Nuovi pezzi in arrivo',\n storeEmptyText: 'Stiamo finendo i nostri primi pezzi. Scrivici e ti avviseremo appena arrivano.',\n storeEmptyTextAlone: 'Stiamo finendo i nostri primi pezzi. Torna presto.',\n getInTouch: 'Scrivici',\n },\n}\n\n/** A language code's two-letter language, lowercase: `es` of `es-MX`; empty for none. */\nexport function aiLayoutLanguageOf(language: string | null | undefined): string {\n return typeof language === 'string' ? (/^([a-z]{2})(?:[-_]|$)/i.exec(language.trim())?.[1] ?? '').toLowerCase() : ''\n}\n\n/** Whether a site's language is one other than English: a code that names one. */\nexport function aiLayoutSpeaksOtherThanEnglish(language: string | null | undefined): boolean {\n const code = aiLayoutLanguageOf(language)\n return code !== '' && code !== 'en'\n}\n\n/** A label the platform places, in the site's language where it is listed, else in English. */\nexport function aiLayoutLabel(key: AiLayoutLabel, language?: string | null): string {\n return LABELS[aiLayoutLanguageOf(language)]?.[key] ?? AI_LAYOUT_LABELS_EN[key]\n}\n"],"names":["AI_LAYOUT_LABELS_EN","seeAll","allPosts","allMusic","allEvents","shopAll","readPost","viewDetails","subscribe","reviewsHeading","signupHeading","account","customerCare","storeAnnouncement","shopNow","storeEmptyTitle","storeEmptyText","storeEmptyTextAlone","getInTouch","LABELS","es","fr","pt","de","it","aiLayoutLanguageOf","language","exec","trim","toLowerCase","aiLayoutSpeaksOtherThanEnglish","code","aiLayoutLabel","key"],"mappings":"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"}
|
|
@@ -55,8 +55,11 @@ const HOME_WORDS = /\bhome(?:\s?page)?\b/i;
|
|
|
55
55
|
}
|
|
56
56
|
/** A destination rule 10 admits, from the page it names, for the words a link shows. */ function pageDestination(page, label, targets) {
|
|
57
57
|
if (!page || page.id === targets.pageId) return null;
|
|
58
|
-
|
|
59
|
-
|
|
58
|
+
// A link home says so, in English or by the home page's own title: a
|
|
59
|
+
// Spanish site's home is "Inicio" (AGL-3760).
|
|
60
|
+
const saysHome = HOME_WORDS.test(label) || wordsOf(label).join(' ') !== '' && wordsOf(label).join(' ') === wordsOf(page.label).join(' ');
|
|
61
|
+
if (targets.homeIds.includes(page.id) && !saysHome && !isHomeSlug(page.slug)) return null;
|
|
62
|
+
if (isHomeSlug(page.slug) && !saysHome) return null;
|
|
60
63
|
if (page.href) return _extends({
|
|
61
64
|
kind: 'path',
|
|
62
65
|
href: page.href
|
|
@@ -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 { aiAsksToGetInTouch } from '../runtime/ai-doctrine-validators'\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: rule 10's own list (AGL-3729). */\nconst CONTACT_WORDS = { test: aiAsksToGetInTouch }\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":["aiAsksToGetInTouch","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","test","pageNamed","targets","value","trim","byId","pages","find","page","id","lower","startsWith","pageDestination","pageId","homeIds","includes","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":"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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 { aiAsksToGetInTouch } from '../runtime/ai-doctrine-validators'\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: rule 10's own list (AGL-3729). */\nconst CONTACT_WORDS = { test: aiAsksToGetInTouch }\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 // A link home says so, in English or by the home page's own title: a\n // Spanish site's home is \"Inicio\" (AGL-3760).\n const saysHome = HOME_WORDS.test(label) || (wordsOf(label).join(' ') !== '' && wordsOf(label).join(' ') === wordsOf(page.label).join(' '))\n if (targets.homeIds.includes(page.id) && !saysHome && !isHomeSlug(page.slug)) return null\n if (isHomeSlug(page.slug) && !saysHome) 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":["aiAsksToGetInTouch","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","test","pageNamed","targets","value","trim","byId","pages","find","page","id","lower","startsWith","pageDestination","pageId","saysHome","homeIds","includes","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":"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"}
|
|
@@ -60,6 +60,11 @@
|
|
|
60
60
|
* repeat), its card binding the record's fields as `{{item.<field>}}`, in
|
|
61
61
|
* the sections whose plan names the dataset in its `uses`. A site may list
|
|
62
62
|
* several, one listing a dataset.
|
|
63
|
+
* - `events` — the site's dated events (AGL-3755), which a guided start of
|
|
64
|
+
* an event-led business drafts through the events-calendar plugin, listed
|
|
65
|
+
* by its Event List (`eventList`) in the section whose plan names them in
|
|
66
|
+
* its `uses`: each published event with its date, place and cover, and
|
|
67
|
+
* its search-engine Event markup.
|
|
63
68
|
*
|
|
64
69
|
* The elements are named by their persisted ids, as the form binding names
|
|
65
70
|
* `form`: a plugin never imports another's internals, and
|
|
@@ -68,7 +73,7 @@
|
|
|
68
73
|
*
|
|
69
74
|
* Model only: shapes and the inputs reader; the compiler draws them.
|
|
70
75
|
*/
|
|
71
|
-
export type AiLayoutListingKind = 'products' | 'posts' | 'tracks' | 'reviews' | 'signup' | 'records';
|
|
76
|
+
export type AiLayoutListingKind = 'products' | 'posts' | 'tracks' | 'reviews' | 'signup' | 'records' | 'events';
|
|
72
77
|
/** 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. */
|
|
73
78
|
export type AiLayoutListingRole = 'featured' | 'index';
|
|
74
79
|
/** One section of one page that places a listing. */
|
|
@@ -155,6 +160,7 @@ export declare const AI_LAYOUT_FEATURED_RECORDS: {
|
|
|
155
160
|
readonly reviews: 3;
|
|
156
161
|
readonly signup: 0;
|
|
157
162
|
readonly records: 3;
|
|
163
|
+
readonly events: 3;
|
|
158
164
|
};
|
|
159
165
|
/**
|
|
160
166
|
* The link each card of a records listing opens (AGL-3727): the record's own
|
|
@@ -241,3 +247,10 @@ export declare function aiLayoutRecordsPlacements(dataset: {
|
|
|
241
247
|
id: string;
|
|
242
248
|
name: string;
|
|
243
249
|
}, screens: readonly AiLayoutListingScreen[]): AiLayoutListingPlacement[];
|
|
250
|
+
/**
|
|
251
|
+
* The sections that list the site's events (AGL-3755): every section whose
|
|
252
|
+
* plan names one of them in its `uses` (`new:<its name>`), once however many
|
|
253
|
+
* it names. The home page features the next few; any other page lists them
|
|
254
|
+
* all. Chosen from the plan, never asked of the model.
|
|
255
|
+
*/
|
|
256
|
+
export declare function aiLayoutEventsPlacements(names: readonly string[], screens: readonly AiLayoutListingScreen[]): AiLayoutListingPlacement[];
|
|
@@ -26,7 +26,8 @@ import { _ as _extends } from "@swc/helpers/_/_extends";
|
|
|
26
26
|
reviews: 'product-reviews',
|
|
27
27
|
signup: 'newsletter-signup',
|
|
28
28
|
// The platform's own Box, repeating its card over the dataset.
|
|
29
|
-
records: 'muiBox'
|
|
29
|
+
records: 'muiBox',
|
|
30
|
+
events: 'eventList'
|
|
30
31
|
};
|
|
31
32
|
/** The header's cart button: the commerce plugin's Cart in its `button` variant. */ export const AI_LAYOUT_CART_ELEMENT = 'cart';
|
|
32
33
|
/** The most records a featured band shows; a page of its own shows them all. */ export const AI_LAYOUT_FEATURED_RECORDS = {
|
|
@@ -35,7 +36,8 @@ import { _ as _extends } from "@swc/helpers/_/_extends";
|
|
|
35
36
|
tracks: 0,
|
|
36
37
|
reviews: 3,
|
|
37
38
|
signup: 0,
|
|
38
|
-
records: 3
|
|
39
|
+
records: 3,
|
|
40
|
+
events: 3
|
|
39
41
|
};
|
|
40
42
|
const PLACEMENT_ROLES = [
|
|
41
43
|
'featured',
|
|
@@ -47,7 +49,8 @@ const KINDS = [
|
|
|
47
49
|
'tracks',
|
|
48
50
|
'reviews',
|
|
49
51
|
'signup',
|
|
50
|
-
'records'
|
|
52
|
+
'records',
|
|
53
|
+
'events'
|
|
51
54
|
];
|
|
52
55
|
const text = (value)=>typeof value === 'string' ? value.trim() : '';
|
|
53
56
|
/** What a visitor calls each kind's whole list, where a unit's input names none. */ const LISTING_NAMES = {
|
|
@@ -56,7 +59,8 @@ const text = (value)=>typeof value === 'string' ? value.trim() : '';
|
|
|
56
59
|
tracks: 'the music',
|
|
57
60
|
reviews: 'the store',
|
|
58
61
|
signup: 'the newsletter',
|
|
59
|
-
records: 'the list'
|
|
62
|
+
records: 'the list',
|
|
63
|
+
events: 'the events'
|
|
60
64
|
};
|
|
61
65
|
/** A path on the site, as a listing's links are kept. */ const SITE_PATH = /^\/[a-z0-9-]+(?:\/[a-z0-9-]+)*$/;
|
|
62
66
|
/** A dataset id and a field id, as the data plugin mints them. */ const DATASET_ID = /^[A-Za-z0-9_-]{1,128}$/;
|
|
@@ -133,7 +137,7 @@ const FIELD_ID = /^[A-Za-z][A-Za-z0-9_]{0,63}$/;
|
|
|
133
137
|
name: text(record['name']) || LISTING_NAMES[kind],
|
|
134
138
|
// A tracks listing names no records: no job sources a recording (AGL-3716);
|
|
135
139
|
// nor do a store's reviews and its sign-up, which shoppers fill (AGL-3676).
|
|
136
|
-
records: kind !== 'products' && kind !== 'posts' && kind !== 'records' ? [] : (Array.isArray(record['records']) ? record['records'] : []).map(text).filter(Boolean).slice(0, 12)
|
|
140
|
+
records: kind !== 'products' && kind !== 'posts' && kind !== 'records' && kind !== 'events' ? [] : (Array.isArray(record['records']) ? record['records'] : []).map(text).filter(Boolean).slice(0, 12)
|
|
137
141
|
}, SITE_PATH.test(href) ? {
|
|
138
142
|
href
|
|
139
143
|
} : {}, kind === 'posts' ? {
|
|
@@ -343,5 +347,28 @@ const isHome = (slug)=>firstSegment(slug) === '';
|
|
|
343
347
|
}
|
|
344
348
|
return placements;
|
|
345
349
|
}
|
|
350
|
+
/**
|
|
351
|
+
* The sections that list the site's events (AGL-3755): every section whose
|
|
352
|
+
* plan names one of them in its `uses` (`new:<its name>`), once however many
|
|
353
|
+
* it names. The home page features the next few; any other page lists them
|
|
354
|
+
* all. Chosen from the plan, never asked of the model.
|
|
355
|
+
*/ export function aiLayoutEventsPlacements(names, screens) {
|
|
356
|
+
const named = new Set(names.map((name)=>`new:${name}`.trim().toLowerCase()));
|
|
357
|
+
const placements = [];
|
|
358
|
+
for (const screen of screens){
|
|
359
|
+
if (typeof screen.id !== 'string' || !screen.id) continue;
|
|
360
|
+
screen.sections.forEach((section, index)=>{
|
|
361
|
+
var _section_uses;
|
|
362
|
+
if (((_section_uses = section.uses) != null ? _section_uses : []).some((ref)=>named.has(ref.trim().toLowerCase()))) {
|
|
363
|
+
placements.push({
|
|
364
|
+
screenId: screen.id,
|
|
365
|
+
section: index,
|
|
366
|
+
role: isHome(screen.slug) ? 'featured' : 'index'
|
|
367
|
+
});
|
|
368
|
+
}
|
|
369
|
+
});
|
|
370
|
+
}
|
|
371
|
+
return placements;
|
|
372
|
+
}
|
|
346
373
|
|
|
347
374
|
//# sourceMappingURL=ai-layout-listings.js.map
|
|
@@ -1 +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\nimport { aiStorefrontSays } from './ai-layout-storefront'\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 * - `tracks` — the music plugin's Music player (`musicPlayer`, AGL-3716) on a\n * music site, placed EMPTY: its \"add your tracks\" state, for the owner to\n * fill from their own media library. No job ever sources a recording, so\n * a tracks listing has no records and the player no source.\n * - `reviews` — a selling store's own customer reviews, through the commerce\n * plugin's Product reviews in its store-wide scope (`product-reviews`,\n * AGL-3676): approved reviews across the catalog with their star ratings.\n * A store that opens has none, and the element then shows nothing at all\n * on the published page, so no review is ever written for a shopper who\n * did not leave one (the FTC's fake-review rule; rule 14's \"never invent\n * a customer\").\n * - `signup` — a selling store's newsletter sign-up, through the commerce\n * plugin's Newsletter signup (`newsletter-signup`, AGL-3676): a consented\n * email field into the store's contacts, where a section says newsletter.\n * - `records` — one of the site's datasets (AGL-3616): a menu, a team, its\n * services, built by the site start and kept by the data plugin. Drawn as\n * a Box that repeats over the dataset (`repeatDataset`, the platform's own\n * repeat), its card binding the record's fields as `{{item.<field>}}`, in\n * the sections whose plan names the dataset in its `uses`. A site may list\n * several, one listing a dataset.\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' | 'tracks' | 'reviews' | 'signup' | 'records'\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/** One field of a dataset a `records` listing shows, as its card binds it. */\nexport interface AiLayoutListingField {\n /** The field's id, which `{{item.<id>}}` reads. */\n id: string\n name: string\n /** The data plugin's stored type: `text`, `float`, `int32`, `bool`, `sorted`. */\n type: string\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 /** The dataset a records listing repeats over, by id (AGL-3616). */\n datasetId?: string\n /** The dataset's fields, in its order, which the card binds. */\n fields?: AiLayoutListingField[]\n /**\n * The field holding each record's photo (AGL-3616), which the card leads\n * with as its Image's `src`, so a portfolio stays a gallery. Never among\n * `fields`: it is the card's picture, not its words.\n */\n imageField?: string\n /**\n * Where each record's own page lives (AGL-3727): the base path of the\n * dataset's record template (AGL-3475), without slashes at either end —\n * `services` for `/services/roofing`. Set by code where the plan makes or\n * the site keeps a record template for the dataset, never by the model.\n */\n recordBase?: string\n /**\n * The record's page-address field (AGL-3727), which the record's page is\n * served at under {@link recordBase}. Never among `fields`: it is the card's\n * link, not its words.\n */\n addressField?: string\n /**\n * Where a visitor goes from a store with nothing listed yet (AGL-3676): the\n * site's contact page, by its path. The empty state names it as its one\n * action, so a shop that opens empty still answers a visitor.\n */\n emptyAction?: { label: string; href: string }\n /**\n * `false` for a store that lists its catalog but cannot sell yet (its first\n * products were skipped or failed, AGL-3676): the header carries no cart.\n */\n cart?: boolean\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; a records listing is one dataset's. */\nexport function aiLayoutListingId(kind: AiLayoutListingKind, datasetId?: string): string {\n return kind === 'records' && datasetId ? `listing:records:${datasetId}` : `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 tracks: 'musicPlayer',\n reviews: 'product-reviews',\n signup: 'newsletter-signup',\n // The platform's own Box, repeating its card over the dataset.\n records: 'muiBox',\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, tracks: 0, reviews: 3, signup: 0, records: 3 } as const\n\nconst PLACEMENT_ROLES: readonly AiLayoutListingRole[] = ['featured', 'index']\nconst KINDS: readonly AiLayoutListingKind[] = ['products', 'posts', 'tracks', 'reviews', 'signup', 'records']\n\nconst text = (value: unknown): string => (typeof value === 'string' ? value.trim() : '')\n\n/** What a visitor calls each kind's whole list, where a unit's input names none. */\nconst LISTING_NAMES: Readonly<Record<AiLayoutListingKind, string>> = {\n products: 'the shop',\n posts: 'the blog',\n tracks: 'the music',\n reviews: 'the store',\n signup: 'the newsletter',\n records: 'the list',\n}\n\n/** A path on the site, as a listing's links are kept. */\nconst SITE_PATH = /^\\/[a-z0-9-]+(?:\\/[a-z0-9-]+)*$/\n\n/** A dataset id and a field id, as the data plugin mints them. */\nconst DATASET_ID = /^[A-Za-z0-9_-]{1,128}$/\nconst FIELD_ID = /^[A-Za-z][A-Za-z0-9_]{0,63}$/\n\n/** A record template's base: one to four URL-safe segments, as the data plugin keeps it. */\nconst RECORD_BASE = /^[a-z0-9]+(?:-[a-z0-9]+)*(?:\\/[a-z0-9]+(?:-[a-z0-9]+)*){0,3}$/\n\n/**\n * The link each card of a records listing opens (AGL-3727): the record's own\n * page, `/{base}/{{item.<address field>}}`, which the page fills per record\n * as it repeats the card. `null` where the dataset has no record page, or the\n * listing names no address to reach it by.\n */\nexport function aiLayoutRecordHref(listing: Pick<AiLayoutListing, 'kind' | 'recordBase' | 'addressField'>): string | null {\n if (listing.kind !== 'records') return null\n const base = text(listing.recordBase).replace(/^\\/+|\\/+$/g, '')\n const field = text(listing.addressField)\n return RECORD_BASE.test(base) && FIELD_ID.test(field) ? `/${base}/{{item.${field}}}` : null\n}\n\n/** A records listing's fields as its input carries them; nothing that does not read is kept. */\nfunction listingFieldsOf(raw: unknown): AiLayoutListingField[] {\n if (!Array.isArray(raw)) return []\n return raw\n .flatMap((value) => {\n const field = (value ?? {}) as Record<string, unknown>\n const id = text(field['id'])\n return FIELD_ID.test(id) ? [{ id, name: text(field['name']).slice(0, 60) || id, type: text(field['type']) || 'text' }] : []\n })\n .slice(0, 16)\n}\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 // A records listing is one dataset's (AGL-3616): a site may list several.\n const datasetId = kind === 'records' ? text(record['datasetId']) : ''\n const fields = kind === 'records' ? listingFieldsOf(record['fields']) : []\n const imageField = kind === 'records' ? text(record['imageField']) : ''\n const recordBase = kind === 'records' ? text(record['recordBase']).replace(/^\\/+|\\/+$/g, '') : ''\n const addressField = kind === 'records' ? text(record['addressField']) : ''\n // Its fields may be left for the page to read off the site's inventory.\n if (kind === 'records' && !DATASET_ID.test(datasetId)) continue\n if (\n !KINDS.includes(kind) ||\n listings.some((listing) => listing.kind === kind && (kind !== 'records' || listing.datasetId === datasetId))\n ) {\n continue\n }\n const href = text(record['href'])\n const action = (record['emptyAction'] ?? {}) as Record<string, unknown>\n const actionLabel = text(action['label']).slice(0, 40)\n const actionHref = text(action['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, datasetId),\n kind,\n name: text(record['name']) || LISTING_NAMES[kind],\n // A tracks listing names no records: no job sources a recording (AGL-3716);\n // nor do a store's reviews and its sign-up, which shoppers fill (AGL-3676).\n records:\n kind !== 'products' && kind !== 'posts' && kind !== 'records'\n ? []\n : (Array.isArray(record['records']) ? record['records'] : []).map(text).filter(Boolean).slice(0, 12),\n ...(SITE_PATH.test(href) ? { href } : {}),\n ...(kind === 'posts' ? { collectionSlug } : {}),\n ...(kind === 'records' ? { datasetId, fields } : {}),\n ...(kind === 'records' && FIELD_ID.test(imageField) ? { imageField } : {}),\n // A record page's base, and the address field where the unit knew it; a\n // build's page reads the field off the site's inventory (AGL-3727).\n ...(kind === 'records' && RECORD_BASE.test(recordBase) ? { recordBase } : {}),\n ...(kind === 'records' && RECORD_BASE.test(recordBase) && FIELD_ID.test(addressField) ? { addressField } : {}),\n ...(kind === 'products' && record['cart'] === false ? { cart: false } : {}),\n ...(kind === 'products' && actionLabel && SITE_PATH.test(actionHref) ? { emptyAction: { label: actionLabel, href: actionHref } } : {}),\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; uses?: readonly string[] }>\n /** The dataset this page is the record template of, and its base (AGL-3475); absent for a page that is one page. */\n record?: { dataset: string; base: string } | null\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/** A music site's page or section about its recordings (AGL-3716). */\nconst MUSIC_PAGE = /\\b(music|listen|tracks?|songs?|releases?|discography|albums?|eps?|singles?|sounds?)\\b/i\n\n/**\n * The section of a shop's own page the whole catalog fills (AGL-3676): the\n * first after its opening whose name says products (\"Product grid\", \"The\n * range\"), else the section planned with the most items, else its second.\n * The live Hearth & Wick start (2026-10-09) planned \"Product range image\n * cards\" — six cards naming kinds of candle, with no product, price or cart.\n */\nexport function aiLayoutShopGridSection(sections: ReadonlyArray<{ name: string; items: number }>): number {\n const named = sections.findIndex((section, index) => index > 0 && PRODUCT_SECTION.test(section.name) && !SHOP_FRAMING.test(section.name))\n if (named !== -1) return named\n const most = Math.max(0, ...sections.map((section) => section.items))\n const byItems = most > 0 ? sections.findIndex((section) => section.items === most) : -1\n return byItems !== -1 ? byItems : sections.length > 1 ? 1 : 0\n}\n\n/**\n * The section of a store's home its bestsellers fill (AGL-3676): the first\n * after the hero whose name says them (\"Bestsellers\", \"Featured candles\", \"New\n * arrivals\"), never one about the store's collections, its reviews or its\n * sign-up; else the first after the hero naming products at all; else the\n * first after the hero planned with items that is none of those. The\n * beta.237 Willow Wick home would otherwise have listed products in its\n * \"Shop by collection\" tiles.\n */\nfunction aiLayoutFeaturedSection(screen: AiLayoutListingScreen): number {\n // A section about the collections that does not also name the bestsellers.\n const other = (name: string) =>\n (aiStorefrontSays('collections', name) && !aiStorefrontSays('featured', name)) ||\n aiStorefrontSays('reviews', name) ||\n aiStorefrontSays('signup', name)\n const first = (test: (name: string, items: number) => boolean) =>\n screen.sections.findIndex((section, index) => index > 0 && test(section.name, section.items))\n for (const test of [\n (name: string) => aiStorefrontSays('featured', name) && !other(name),\n (name: string) => PRODUCT_SECTION.test(name) && !other(name),\n (name: string, items: number) => items > 0 && !other(name),\n (name: string) => PRODUCT_SECTION.test(name),\n ]) {\n const found = first(test)\n if (found !== -1) return found\n }\n return -1\n}\n\n/** A section of a shop page that only opens or closes it: \"Shop intro heading\", \"Gift help call to action\". */\nconst SHOP_FRAMING = /\\b(hero|intro(?:duction)?|heading|banner|cta|call to action|contact)\\b/i\n\n/** Whether a planned page is the store's own Shop page: its address or its name says it sells. */\nexport function aiLayoutIsShopPage(screen: { slug: string; title: string }): boolean {\n return !isHome(screen.slug) && PRODUCT_PAGE.test(`${firstSegment(screen.slug)} ${screen.title}`)\n}\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 === 'tracks') {\n // A music site's player (AGL-3716): on its Music page, in the section that\n // says it is about the recordings, else the one after the opening; on the\n // home, only in a section named for them. A site with neither gets it on\n // its home, after the opening.\n for (const screen of screens) {\n if (isHome(screen.slug)) {\n place(screen, afterHero(screen, (name) => MUSIC_PAGE.test(name)), 'featured')\n continue\n }\n if (!MUSIC_PAGE.test(`${firstSegment(screen.slug)} ${screen.title}`)) continue\n const named = afterHero(screen, (name) => MUSIC_PAGE.test(name))\n place(screen, named !== -1 ? named : screen.sections.length > 1 ? 1 : 0, 'index')\n }\n if (!placements.length) {\n const home = screens.find((screen) => isHome(screen.slug))\n if (home) place(home, home.sections.length > 1 ? 1 : 0, 'featured')\n }\n return placements\n }\n if (kind === 'reviews' || kind === 'signup') {\n // A store's own reviews, on its home, in the section named for them; its\n // newsletter sign-up in any page's section named for it that places no\n // form of the plan's (AGL-3676). Neither is ever a page's opening.\n for (const screen of screens) {\n if (kind === 'reviews' && !isHome(screen.slug)) continue\n place(\n screen,\n afterHero(\n screen,\n (name) => aiStorefrontSays(kind, name),\n ),\n kind === 'reviews' ? 'featured' : 'index',\n )\n }\n return kind === 'signup'\n ? placements.filter((placement) => {\n const screen = screens.find((entry) => entry.id === placement.screenId)\n return !(screen?.sections[placement.section]?.uses ?? []).some((use) => /form/i.test(use))\n })\n : placements\n }\n if (kind === 'products') {\n for (const screen of screens) {\n if (isHome(screen.slug)) {\n place(screen, aiLayoutFeaturedSection(screen), 'featured')\n continue\n }\n if (!aiLayoutIsShopPage(screen)) continue\n place(screen, aiLayoutShopGridSection(screen.sections), '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\n/**\n * Where a dataset's records have pages of their own (AGL-3727): the base of\n * the page the plan makes its record template (AGL-3475), named by its id or\n * as `new:<its name>`; `null` where no page is. Read off the plan, never\n * asked of the model, so every card that lists the dataset links its record.\n */\nexport function aiLayoutRecordBaseOf(\n dataset: { id: string; name: string },\n screens: readonly AiLayoutListingScreen[],\n): string | null {\n const named = `new:${dataset.name}`.trim().toLowerCase()\n for (const screen of screens) {\n const ref = text(screen.record?.dataset)\n if (!ref || (ref !== dataset.id && ref.toLowerCase() !== named)) continue\n const base = text(screen.record?.base).replace(/^\\/+|\\/+$/g, '')\n if (RECORD_BASE.test(base)) return base\n }\n return null\n}\n\n/**\n * The sections that list one of the site's datasets (AGL-3616): every section\n * whose plan names it in its `uses` — `new:<its name>` as the plan wrote it,\n * or its id once built. The home page features a few of its records; any\n * other page lists them all. Chosen from the plan, never asked of the model.\n */\nexport function aiLayoutRecordsPlacements(\n dataset: { id: string; name: string },\n screens: readonly AiLayoutListingScreen[],\n): AiLayoutListingPlacement[] {\n const named = `new:${dataset.name}`.trim().toLowerCase()\n const placements: AiLayoutListingPlacement[] = []\n for (const screen of screens) {\n if (typeof screen.id !== 'string' || !screen.id) continue\n screen.sections.forEach((section, index) => {\n const lists = (section.uses ?? []).some((ref) => ref === dataset.id || ref.trim().toLowerCase() === named)\n if (lists) placements.push({ screenId: screen.id as string, section: index, role: isHome(screen.slug) ? 'featured' : 'index' })\n })\n }\n return placements\n}\n"],"names":["aiStorefrontSays","AI_LAYOUT_LISTINGS_INPUT","aiLayoutListingId","kind","datasetId","AI_LAYOUT_LISTING_ELEMENTS","products","posts","tracks","reviews","signup","records","AI_LAYOUT_CART_ELEMENT","AI_LAYOUT_FEATURED_RECORDS","PLACEMENT_ROLES","KINDS","text","value","trim","LISTING_NAMES","SITE_PATH","DATASET_ID","FIELD_ID","RECORD_BASE","aiLayoutRecordHref","listing","base","recordBase","replace","field","addressField","test","listingFieldsOf","raw","Array","isArray","flatMap","id","name","slice","type","aiLayoutListingsOf","inputs","listings","entry","record","fields","imageField","includes","some","href","action","actionLabel","actionHref","collectionSlug","placements","placement","screenId","section","role","Number","isInteger","push","map","filter","Boolean","cart","emptyAction","label","aiLayoutListingAt","find","PRODUCT_PAGE","PRODUCT_SECTION","POST_SECTION","MUSIC_PAGE","aiLayoutShopGridSection","sections","named","findIndex","index","SHOP_FRAMING","most","Math","max","items","byItems","length","aiLayoutFeaturedSection","screen","other","first","found","aiLayoutIsShopPage","isHome","slug","firstSegment","title","split","toLowerCase","aiLayoutListingPlacements","screens","place","afterHero","home","uses","use","_name","aiLayoutRecordBaseOf","dataset","ref","aiLayoutRecordsPlacements","forEach","lists"],"mappings":"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"}
|
|
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\nimport { aiStorefrontSays } from './ai-layout-storefront'\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 * - `tracks` — the music plugin's Music player (`musicPlayer`, AGL-3716) on a\n * music site, placed EMPTY: its \"add your tracks\" state, for the owner to\n * fill from their own media library. No job ever sources a recording, so\n * a tracks listing has no records and the player no source.\n * - `reviews` — a selling store's own customer reviews, through the commerce\n * plugin's Product reviews in its store-wide scope (`product-reviews`,\n * AGL-3676): approved reviews across the catalog with their star ratings.\n * A store that opens has none, and the element then shows nothing at all\n * on the published page, so no review is ever written for a shopper who\n * did not leave one (the FTC's fake-review rule; rule 14's \"never invent\n * a customer\").\n * - `signup` — a selling store's newsletter sign-up, through the commerce\n * plugin's Newsletter signup (`newsletter-signup`, AGL-3676): a consented\n * email field into the store's contacts, where a section says newsletter.\n * - `records` — one of the site's datasets (AGL-3616): a menu, a team, its\n * services, built by the site start and kept by the data plugin. Drawn as\n * a Box that repeats over the dataset (`repeatDataset`, the platform's own\n * repeat), its card binding the record's fields as `{{item.<field>}}`, in\n * the sections whose plan names the dataset in its `uses`. A site may list\n * several, one listing a dataset.\n * - `events` — the site's dated events (AGL-3755), which a guided start of\n * an event-led business drafts through the events-calendar plugin, listed\n * by its Event List (`eventList`) in the section whose plan names them in\n * its `uses`: each published event with its date, place and cover, and\n * its search-engine Event markup.\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' | 'tracks' | 'reviews' | 'signup' | 'records' | 'events'\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/** One field of a dataset a `records` listing shows, as its card binds it. */\nexport interface AiLayoutListingField {\n /** The field's id, which `{{item.<id>}}` reads. */\n id: string\n name: string\n /** The data plugin's stored type: `text`, `float`, `int32`, `bool`, `sorted`. */\n type: string\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 /** The dataset a records listing repeats over, by id (AGL-3616). */\n datasetId?: string\n /** The dataset's fields, in its order, which the card binds. */\n fields?: AiLayoutListingField[]\n /**\n * The field holding each record's photo (AGL-3616), which the card leads\n * with as its Image's `src`, so a portfolio stays a gallery. Never among\n * `fields`: it is the card's picture, not its words.\n */\n imageField?: string\n /**\n * Where each record's own page lives (AGL-3727): the base path of the\n * dataset's record template (AGL-3475), without slashes at either end —\n * `services` for `/services/roofing`. Set by code where the plan makes or\n * the site keeps a record template for the dataset, never by the model.\n */\n recordBase?: string\n /**\n * The record's page-address field (AGL-3727), which the record's page is\n * served at under {@link recordBase}. Never among `fields`: it is the card's\n * link, not its words.\n */\n addressField?: string\n /**\n * Where a visitor goes from a store with nothing listed yet (AGL-3676): the\n * site's contact page, by its path. The empty state names it as its one\n * action, so a shop that opens empty still answers a visitor.\n */\n emptyAction?: { label: string; href: string }\n /**\n * `false` for a store that lists its catalog but cannot sell yet (its first\n * products were skipped or failed, AGL-3676): the header carries no cart.\n */\n cart?: boolean\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; a records listing is one dataset's. */\nexport function aiLayoutListingId(kind: AiLayoutListingKind, datasetId?: string): string {\n return kind === 'records' && datasetId ? `listing:records:${datasetId}` : `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 tracks: 'musicPlayer',\n reviews: 'product-reviews',\n signup: 'newsletter-signup',\n // The platform's own Box, repeating its card over the dataset.\n records: 'muiBox',\n events: 'eventList',\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, tracks: 0, reviews: 3, signup: 0, records: 3, events: 3 } as const\n\nconst PLACEMENT_ROLES: readonly AiLayoutListingRole[] = ['featured', 'index']\nconst KINDS: readonly AiLayoutListingKind[] = ['products', 'posts', 'tracks', 'reviews', 'signup', 'records', 'events']\n\nconst text = (value: unknown): string => (typeof value === 'string' ? value.trim() : '')\n\n/** What a visitor calls each kind's whole list, where a unit's input names none. */\nconst LISTING_NAMES: Readonly<Record<AiLayoutListingKind, string>> = {\n products: 'the shop',\n posts: 'the blog',\n tracks: 'the music',\n reviews: 'the store',\n signup: 'the newsletter',\n records: 'the list',\n events: 'the events',\n}\n\n/** A path on the site, as a listing's links are kept. */\nconst SITE_PATH = /^\\/[a-z0-9-]+(?:\\/[a-z0-9-]+)*$/\n\n/** A dataset id and a field id, as the data plugin mints them. */\nconst DATASET_ID = /^[A-Za-z0-9_-]{1,128}$/\nconst FIELD_ID = /^[A-Za-z][A-Za-z0-9_]{0,63}$/\n\n/** A record template's base: one to four URL-safe segments, as the data plugin keeps it. */\nconst RECORD_BASE = /^[a-z0-9]+(?:-[a-z0-9]+)*(?:\\/[a-z0-9]+(?:-[a-z0-9]+)*){0,3}$/\n\n/**\n * The link each card of a records listing opens (AGL-3727): the record's own\n * page, `/{base}/{{item.<address field>}}`, which the page fills per record\n * as it repeats the card. `null` where the dataset has no record page, or the\n * listing names no address to reach it by.\n */\nexport function aiLayoutRecordHref(listing: Pick<AiLayoutListing, 'kind' | 'recordBase' | 'addressField'>): string | null {\n if (listing.kind !== 'records') return null\n const base = text(listing.recordBase).replace(/^\\/+|\\/+$/g, '')\n const field = text(listing.addressField)\n return RECORD_BASE.test(base) && FIELD_ID.test(field) ? `/${base}/{{item.${field}}}` : null\n}\n\n/** A records listing's fields as its input carries them; nothing that does not read is kept. */\nfunction listingFieldsOf(raw: unknown): AiLayoutListingField[] {\n if (!Array.isArray(raw)) return []\n return raw\n .flatMap((value) => {\n const field = (value ?? {}) as Record<string, unknown>\n const id = text(field['id'])\n return FIELD_ID.test(id) ? [{ id, name: text(field['name']).slice(0, 60) || id, type: text(field['type']) || 'text' }] : []\n })\n .slice(0, 16)\n}\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 // A records listing is one dataset's (AGL-3616): a site may list several.\n const datasetId = kind === 'records' ? text(record['datasetId']) : ''\n const fields = kind === 'records' ? listingFieldsOf(record['fields']) : []\n const imageField = kind === 'records' ? text(record['imageField']) : ''\n const recordBase = kind === 'records' ? text(record['recordBase']).replace(/^\\/+|\\/+$/g, '') : ''\n const addressField = kind === 'records' ? text(record['addressField']) : ''\n // Its fields may be left for the page to read off the site's inventory.\n if (kind === 'records' && !DATASET_ID.test(datasetId)) continue\n if (\n !KINDS.includes(kind) ||\n listings.some((listing) => listing.kind === kind && (kind !== 'records' || listing.datasetId === datasetId))\n ) {\n continue\n }\n const href = text(record['href'])\n const action = (record['emptyAction'] ?? {}) as Record<string, unknown>\n const actionLabel = text(action['label']).slice(0, 40)\n const actionHref = text(action['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, datasetId),\n kind,\n name: text(record['name']) || LISTING_NAMES[kind],\n // A tracks listing names no records: no job sources a recording (AGL-3716);\n // nor do a store's reviews and its sign-up, which shoppers fill (AGL-3676).\n records:\n kind !== 'products' && kind !== 'posts' && kind !== 'records' && kind !== 'events'\n ? []\n : (Array.isArray(record['records']) ? record['records'] : []).map(text).filter(Boolean).slice(0, 12),\n ...(SITE_PATH.test(href) ? { href } : {}),\n ...(kind === 'posts' ? { collectionSlug } : {}),\n ...(kind === 'records' ? { datasetId, fields } : {}),\n ...(kind === 'records' && FIELD_ID.test(imageField) ? { imageField } : {}),\n // A record page's base, and the address field where the unit knew it; a\n // build's page reads the field off the site's inventory (AGL-3727).\n ...(kind === 'records' && RECORD_BASE.test(recordBase) ? { recordBase } : {}),\n ...(kind === 'records' && RECORD_BASE.test(recordBase) && FIELD_ID.test(addressField) ? { addressField } : {}),\n ...(kind === 'products' && record['cart'] === false ? { cart: false } : {}),\n ...(kind === 'products' && actionLabel && SITE_PATH.test(actionHref) ? { emptyAction: { label: actionLabel, href: actionHref } } : {}),\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; uses?: readonly string[] }>\n /** The dataset this page is the record template of, and its base (AGL-3475); absent for a page that is one page. */\n record?: { dataset: string; base: string } | null\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/** A music site's page or section about its recordings (AGL-3716). */\nconst MUSIC_PAGE = /\\b(music|listen|tracks?|songs?|releases?|discography|albums?|eps?|singles?|sounds?)\\b/i\n\n/**\n * The section of a shop's own page the whole catalog fills (AGL-3676): the\n * first after its opening whose name says products (\"Product grid\", \"The\n * range\"), else the section planned with the most items, else its second.\n * The live Hearth & Wick start (2026-10-09) planned \"Product range image\n * cards\" — six cards naming kinds of candle, with no product, price or cart.\n */\nexport function aiLayoutShopGridSection(sections: ReadonlyArray<{ name: string; items: number }>): number {\n const named = sections.findIndex((section, index) => index > 0 && PRODUCT_SECTION.test(section.name) && !SHOP_FRAMING.test(section.name))\n if (named !== -1) return named\n const most = Math.max(0, ...sections.map((section) => section.items))\n const byItems = most > 0 ? sections.findIndex((section) => section.items === most) : -1\n return byItems !== -1 ? byItems : sections.length > 1 ? 1 : 0\n}\n\n/**\n * The section of a store's home its bestsellers fill (AGL-3676): the first\n * after the hero whose name says them (\"Bestsellers\", \"Featured candles\", \"New\n * arrivals\"), never one about the store's collections, its reviews or its\n * sign-up; else the first after the hero naming products at all; else the\n * first after the hero planned with items that is none of those. The\n * beta.237 Willow Wick home would otherwise have listed products in its\n * \"Shop by collection\" tiles.\n */\nfunction aiLayoutFeaturedSection(screen: AiLayoutListingScreen): number {\n // A section about the collections that does not also name the bestsellers.\n const other = (name: string) =>\n (aiStorefrontSays('collections', name) && !aiStorefrontSays('featured', name)) ||\n aiStorefrontSays('reviews', name) ||\n aiStorefrontSays('signup', name)\n const first = (test: (name: string, items: number) => boolean) =>\n screen.sections.findIndex((section, index) => index > 0 && test(section.name, section.items))\n for (const test of [\n (name: string) => aiStorefrontSays('featured', name) && !other(name),\n (name: string) => PRODUCT_SECTION.test(name) && !other(name),\n (name: string, items: number) => items > 0 && !other(name),\n (name: string) => PRODUCT_SECTION.test(name),\n ]) {\n const found = first(test)\n if (found !== -1) return found\n }\n return -1\n}\n\n/** A section of a shop page that only opens or closes it: \"Shop intro heading\", \"Gift help call to action\". */\nconst SHOP_FRAMING = /\\b(hero|intro(?:duction)?|heading|banner|cta|call to action|contact)\\b/i\n\n/** Whether a planned page is the store's own Shop page: its address or its name says it sells. */\nexport function aiLayoutIsShopPage(screen: { slug: string; title: string }): boolean {\n return !isHome(screen.slug) && PRODUCT_PAGE.test(`${firstSegment(screen.slug)} ${screen.title}`)\n}\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 === 'tracks') {\n // A music site's player (AGL-3716): on its Music page, in the section that\n // says it is about the recordings, else the one after the opening; on the\n // home, only in a section named for them. A site with neither gets it on\n // its home, after the opening.\n for (const screen of screens) {\n if (isHome(screen.slug)) {\n place(screen, afterHero(screen, (name) => MUSIC_PAGE.test(name)), 'featured')\n continue\n }\n if (!MUSIC_PAGE.test(`${firstSegment(screen.slug)} ${screen.title}`)) continue\n const named = afterHero(screen, (name) => MUSIC_PAGE.test(name))\n place(screen, named !== -1 ? named : screen.sections.length > 1 ? 1 : 0, 'index')\n }\n if (!placements.length) {\n const home = screens.find((screen) => isHome(screen.slug))\n if (home) place(home, home.sections.length > 1 ? 1 : 0, 'featured')\n }\n return placements\n }\n if (kind === 'reviews' || kind === 'signup') {\n // A store's own reviews, on its home, in the section named for them; its\n // newsletter sign-up in any page's section named for it that places no\n // form of the plan's (AGL-3676). Neither is ever a page's opening.\n for (const screen of screens) {\n if (kind === 'reviews' && !isHome(screen.slug)) continue\n place(\n screen,\n afterHero(\n screen,\n (name) => aiStorefrontSays(kind, name),\n ),\n kind === 'reviews' ? 'featured' : 'index',\n )\n }\n return kind === 'signup'\n ? placements.filter((placement) => {\n const screen = screens.find((entry) => entry.id === placement.screenId)\n return !(screen?.sections[placement.section]?.uses ?? []).some((use) => /form/i.test(use))\n })\n : placements\n }\n if (kind === 'products') {\n for (const screen of screens) {\n if (isHome(screen.slug)) {\n place(screen, aiLayoutFeaturedSection(screen), 'featured')\n continue\n }\n if (!aiLayoutIsShopPage(screen)) continue\n place(screen, aiLayoutShopGridSection(screen.sections), '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\n/**\n * Where a dataset's records have pages of their own (AGL-3727): the base of\n * the page the plan makes its record template (AGL-3475), named by its id or\n * as `new:<its name>`; `null` where no page is. Read off the plan, never\n * asked of the model, so every card that lists the dataset links its record.\n */\nexport function aiLayoutRecordBaseOf(\n dataset: { id: string; name: string },\n screens: readonly AiLayoutListingScreen[],\n): string | null {\n const named = `new:${dataset.name}`.trim().toLowerCase()\n for (const screen of screens) {\n const ref = text(screen.record?.dataset)\n if (!ref || (ref !== dataset.id && ref.toLowerCase() !== named)) continue\n const base = text(screen.record?.base).replace(/^\\/+|\\/+$/g, '')\n if (RECORD_BASE.test(base)) return base\n }\n return null\n}\n\n/**\n * The sections that list one of the site's datasets (AGL-3616): every section\n * whose plan names it in its `uses` — `new:<its name>` as the plan wrote it,\n * or its id once built. The home page features a few of its records; any\n * other page lists them all. Chosen from the plan, never asked of the model.\n */\nexport function aiLayoutRecordsPlacements(\n dataset: { id: string; name: string },\n screens: readonly AiLayoutListingScreen[],\n): AiLayoutListingPlacement[] {\n const named = `new:${dataset.name}`.trim().toLowerCase()\n const placements: AiLayoutListingPlacement[] = []\n for (const screen of screens) {\n if (typeof screen.id !== 'string' || !screen.id) continue\n screen.sections.forEach((section, index) => {\n const lists = (section.uses ?? []).some((ref) => ref === dataset.id || ref.trim().toLowerCase() === named)\n if (lists) placements.push({ screenId: screen.id as string, section: index, role: isHome(screen.slug) ? 'featured' : 'index' })\n })\n }\n return placements\n}\n\n/**\n * The sections that list the site's events (AGL-3755): every section whose\n * plan names one of them in its `uses` (`new:<its name>`), once however many\n * it names. The home page features the next few; any other page lists them\n * all. Chosen from the plan, never asked of the model.\n */\nexport function aiLayoutEventsPlacements(\n names: readonly string[],\n screens: readonly AiLayoutListingScreen[],\n): AiLayoutListingPlacement[] {\n const named = new Set(names.map((name) => `new:${name}`.trim().toLowerCase()))\n const placements: AiLayoutListingPlacement[] = []\n for (const screen of screens) {\n if (typeof screen.id !== 'string' || !screen.id) continue\n screen.sections.forEach((section, index) => {\n if ((section.uses ?? []).some((ref) => named.has(ref.trim().toLowerCase()))) {\n placements.push({ screenId: screen.id as string, section: index, role: isHome(screen.slug) ? 'featured' : 'index' })\n }\n })\n }\n return placements\n}\n"],"names":["aiStorefrontSays","AI_LAYOUT_LISTINGS_INPUT","aiLayoutListingId","kind","datasetId","AI_LAYOUT_LISTING_ELEMENTS","products","posts","tracks","reviews","signup","records","events","AI_LAYOUT_CART_ELEMENT","AI_LAYOUT_FEATURED_RECORDS","PLACEMENT_ROLES","KINDS","text","value","trim","LISTING_NAMES","SITE_PATH","DATASET_ID","FIELD_ID","RECORD_BASE","aiLayoutRecordHref","listing","base","recordBase","replace","field","addressField","test","listingFieldsOf","raw","Array","isArray","flatMap","id","name","slice","type","aiLayoutListingsOf","inputs","listings","entry","record","fields","imageField","includes","some","href","action","actionLabel","actionHref","collectionSlug","placements","placement","screenId","section","role","Number","isInteger","push","map","filter","Boolean","cart","emptyAction","label","aiLayoutListingAt","find","PRODUCT_PAGE","PRODUCT_SECTION","POST_SECTION","MUSIC_PAGE","aiLayoutShopGridSection","sections","named","findIndex","index","SHOP_FRAMING","most","Math","max","items","byItems","length","aiLayoutFeaturedSection","screen","other","first","found","aiLayoutIsShopPage","isHome","slug","firstSegment","title","split","toLowerCase","aiLayoutListingPlacements","screens","place","afterHero","home","uses","use","_name","aiLayoutRecordBaseOf","dataset","ref","aiLayoutRecordsPlacements","forEach","lists","aiLayoutEventsPlacements","names","Set","has"],"mappings":"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"}
|