@aglyn/plugins-ai 1.0.0-beta.232 → 1.0.0-beta.234
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/components/ai-job-links.d.ts +6 -0
- package/src/lib/components/ai-job-links.js +9 -0
- package/src/lib/components/ai-job-links.js.map +1 -1
- package/src/lib/components/ai-site-build-page.component.js +41 -3
- package/src/lib/components/ai-site-build-page.component.js.map +1 -1
- package/src/lib/components/ai-site-start-card.component.js +91 -8
- package/src/lib/components/ai-site-start-card.component.js.map +1 -1
- package/src/lib/declarations.server.js +7 -4
- package/src/lib/declarations.server.js.map +1 -1
- package/src/lib/jobs/ai-build-capabilities.js +8 -2
- package/src/lib/jobs/ai-build-capabilities.js.map +1 -1
- package/src/lib/jobs/ai-job-admission.d.ts +2 -1
- package/src/lib/jobs/ai-job-admission.js.map +1 -1
- package/src/lib/jobs/ai-job-email-step.d.ts +39 -0
- package/src/lib/jobs/ai-job-email-step.js +116 -5
- package/src/lib/jobs/ai-job-email-step.js.map +1 -1
- package/src/lib/jobs/ai-job-layout-language.d.ts +3 -0
- package/src/lib/jobs/ai-job-layout-language.js +3 -1
- package/src/lib/jobs/ai-job-layout-language.js.map +1 -1
- package/src/lib/jobs/ai-job-layout-site-pages.d.ts +2 -0
- package/src/lib/jobs/ai-job-layout-site-pages.js +23 -11
- package/src/lib/jobs/ai-job-layout-site-pages.js.map +1 -1
- package/src/lib/jobs/ai-job-layout-step.js +3 -2
- package/src/lib/jobs/ai-job-layout-step.js.map +1 -1
- package/src/lib/jobs/ai-job-page-language.d.ts +16 -0
- package/src/lib/jobs/ai-job-page-language.js +35 -6
- package/src/lib/jobs/ai-job-page-language.js.map +1 -1
- package/src/lib/jobs/ai-job-page-step.d.ts +3 -0
- package/src/lib/jobs/ai-job-page-step.js +18 -4
- package/src/lib/jobs/ai-job-page-step.js.map +1 -1
- package/src/lib/jobs/ai-job-plan-step.d.ts +40 -1
- package/src/lib/jobs/ai-job-plan-step.js +121 -16
- package/src/lib/jobs/ai-job-plan-step.js.map +1 -1
- package/src/lib/jobs/ai-job-site-content.d.ts +6 -6
- package/src/lib/jobs/ai-job-site-content.js +6 -12
- package/src/lib/jobs/ai-job-site-content.js.map +1 -1
- package/src/lib/jobs/ai-job-site-step.d.ts +11 -0
- package/src/lib/jobs/ai-job-site-step.js +96 -44
- package/src/lib/jobs/ai-job-site-step.js.map +1 -1
- package/src/lib/jobs/ai-jobs-notify.d.ts +14 -4
- package/src/lib/jobs/ai-jobs-notify.js +70 -4
- package/src/lib/jobs/ai-jobs-notify.js.map +1 -1
- package/src/lib/jobs/ai-jobs.d.ts +22 -4
- package/src/lib/jobs/ai-jobs.js +47 -12
- package/src/lib/jobs/ai-jobs.js.map +1 -1
- package/src/lib/jobs/ai-layout-stock-photos.d.ts +98 -0
- package/src/lib/jobs/ai-layout-stock-photos.js +248 -0
- package/src/lib/jobs/ai-layout-stock-photos.js.map +1 -0
- package/src/lib/jobs/ai-site-publish.d.ts +11 -0
- package/src/lib/jobs/ai-site-publish.js +31 -1
- package/src/lib/jobs/ai-site-publish.js.map +1 -1
- package/src/lib/jobs/fixtures/ai-juniper-portfolio-page-recording.d.ts +151 -0
- package/src/lib/jobs/fixtures/ai-juniper-portfolio-page-recording.js +183 -0
- package/src/lib/jobs/fixtures/ai-juniper-portfolio-page-recording.js.map +1 -0
- package/src/lib/jobs/fixtures/ai-slow-roads-site-plan-recording.d.ts +27 -0
- package/src/lib/jobs/fixtures/ai-slow-roads-site-plan-recording.js +192 -0
- package/src/lib/jobs/fixtures/ai-slow-roads-site-plan-recording.js.map +1 -0
- package/src/lib/jobs/fixtures/ai-yoga-site-plan-recording.d.ts +27 -0
- package/src/lib/jobs/fixtures/ai-yoga-site-plan-recording.js +222 -0
- package/src/lib/jobs/fixtures/ai-yoga-site-plan-recording.js.map +1 -0
- package/src/lib/layout-language/ai-layout-compiler.d.ts +29 -0
- package/src/lib/layout-language/ai-layout-compiler.js +698 -12
- package/src/lib/layout-language/ai-layout-compiler.js.map +1 -1
- package/src/lib/layout-language/ai-layout-design.d.ts +107 -0
- package/src/lib/layout-language/ai-layout-design.js +197 -0
- package/src/lib/layout-language/ai-layout-design.js.map +1 -0
- package/src/lib/layout-language/ai-layout-frame.d.ts +6 -0
- package/src/lib/layout-language/ai-layout-frame.js +22 -6
- package/src/lib/layout-language/ai-layout-frame.js.map +1 -1
- package/src/lib/layout-language/ai-layout-language.d.ts +2 -0
- package/src/lib/layout-language/ai-layout-language.js +15 -1
- package/src/lib/layout-language/ai-layout-language.js.map +1 -1
- package/src/lib/layout-language/ai-layout-links.d.ts +11 -0
- package/src/lib/layout-language/ai-layout-links.js +4 -0
- package/src/lib/layout-language/ai-layout-links.js.map +1 -1
- package/src/lib/layout-language/ai-layout-pictures.d.ts +11 -5
- package/src/lib/layout-language/ai-layout-pictures.js +80 -17
- package/src/lib/layout-language/ai-layout-pictures.js.map +1 -1
- package/src/lib/model/ai-job-notice.d.ts +9 -1
- package/src/lib/model/ai-job-notice.js +23 -0
- package/src/lib/model/ai-job-notice.js.map +1 -1
- package/src/lib/model/ai-plan-capabilities.d.ts +23 -0
- package/src/lib/model/ai-plan-capabilities.js +5 -0
- package/src/lib/model/ai-plan-capabilities.js.map +1 -1
- package/src/lib/model/ai-site-build-progress.d.ts +12 -2
- package/src/lib/model/ai-site-build-progress.js +78 -4
- package/src/lib/model/ai-site-build-progress.js.map +1 -1
- package/src/lib/model/ai-site-job.d.ts +209 -22
- package/src/lib/model/ai-site-job.js +247 -20
- package/src/lib/model/ai-site-job.js.map +1 -1
- package/src/lib/model/ai-site-kinds.js +6 -1
- package/src/lib/model/ai-site-kinds.js.map +1 -1
- package/src/lib/model/ai-site-look.d.ts +21 -0
- package/src/lib/model/ai-site-look.js +63 -14
- package/src/lib/model/ai-site-look.js.map +1 -1
- package/src/lib/operator-alerts.d.ts +18 -0
- package/src/lib/operator-alerts.js +42 -0
- package/src/lib/operator-alerts.js.map +1 -1
- package/src/lib/runtime/ai-doctrine-validators.d.ts +39 -1
- package/src/lib/runtime/ai-doctrine-validators.js +236 -6
- package/src/lib/runtime/ai-doctrine-validators.js.map +1 -1
- package/src/lib/runtime/ai-doctrine.js +5 -3
- package/src/lib/runtime/ai-doctrine.js.map +1 -1
- package/src/lib/runtime/ai-palette.generated.d.ts +2 -0
- package/src/lib/runtime/ai-palette.generated.js +18 -2
- package/src/lib/runtime/ai-palette.generated.js.map +1 -1
- package/src/lib/server/ai-jobs-resume.js.map +1 -1
- package/src/lib/server/ai-jobs-route.js +15 -3
- package/src/lib/server/ai-jobs-route.js.map +1 -1
- package/src/lib/server/assist-edit.js +5 -2
- package/src/lib/server/assist-edit.js.map +1 -1
- package/src/lib/usage/free-ai-credits-left.d.ts +54 -0
- package/src/lib/usage/free-ai-credits-left.js +62 -0
- package/src/lib/usage/free-ai-credits-left.js.map +1 -0
|
@@ -114,6 +114,17 @@ export interface AiDoctrineTreeContext extends AiNodeTreeContext {
|
|
|
114
114
|
* every plan, counted against `formsPerHost`, or none (AGL-3596).
|
|
115
115
|
*/
|
|
116
116
|
reusableComponents?: boolean;
|
|
117
|
+
/**
|
|
118
|
+
* A page the layout language compiled (AGL-3660), whose picture cards — a
|
|
119
|
+
* picture over its title and words, one to a cell of a grid — the compiler
|
|
120
|
+
* draws from the site's design. Rule 1 asks no component of those cards;
|
|
121
|
+
* every other repeat on the page is still held to it. The page half of the
|
|
122
|
+
* plan capability of the same name (`AiPlanCapabilities.repeatsCompiled`,
|
|
123
|
+
* AGL-3616), which spares a build's plan a component for a section's items:
|
|
124
|
+
* the plan leaves them to the compiler, and this lets the page it compiles
|
|
125
|
+
* draw them.
|
|
126
|
+
*/
|
|
127
|
+
repeatsCompiled?: boolean;
|
|
117
128
|
/**
|
|
118
129
|
* The site's home screens, by id (AGL-3056): the screen a link falls back
|
|
119
130
|
* to when the site has none for what it promises. Absent: none is known.
|
|
@@ -173,7 +184,7 @@ export declare const AI_TYPED_LIST_MIN_ITEMS = 8;
|
|
|
173
184
|
* A workspace that keeps no reusable components can place no instance, so
|
|
174
185
|
* there a repeated block is drawn where it repeats, and nothing is refused.
|
|
175
186
|
*/
|
|
176
|
-
export declare function detectRepeatedSubtrees(tree: AiDoctrineTree, otherPages?: readonly AiDoctrineTree[], context?: Pick<AiDoctrineTreeContext, 'reusableComponents'>): AiDoctrineViolation[];
|
|
187
|
+
export declare function detectRepeatedSubtrees(tree: AiDoctrineTree, otherPages?: readonly AiDoctrineTree[], context?: Pick<AiDoctrineTreeContext, 'reusableComponents' | 'repeatsCompiled'>): AiDoctrineViolation[];
|
|
177
188
|
/**
|
|
178
189
|
* How many copies of one repeated thing a document shows, read as
|
|
179
190
|
* generously as the tree allows (AGL-3024): the largest group of nodes the
|
|
@@ -272,6 +283,26 @@ export declare function detectLiteralStyles(tree: AiDoctrineTree, outputKind: Ai
|
|
|
272
283
|
* with no brand recorded, the blocks' own defaults.
|
|
273
284
|
*/
|
|
274
285
|
export declare function detectOffBrandEmail(tree: AiDoctrineTree, outputKind: AiOutputKind, brand: AiDoctrineTreeContext['brand']): AiDoctrineViolation[];
|
|
286
|
+
/**
|
|
287
|
+
* An email's off-brand colors, settled where each has one reading (AGL-3676).
|
|
288
|
+
* Rule 6 holds an email to the brand's own values, and the guided start's
|
|
289
|
+
* welcome email kept writing tints of them — a pale wash of the primary color
|
|
290
|
+
* behind a section — and stopped on rule 6 after its re-ask. Before the tree
|
|
291
|
+
* is checked, each `color` and `backgroundColor` outside the brand is:
|
|
292
|
+
*
|
|
293
|
+
* - a palette path the brand lists, such as `primary.main`: its value;
|
|
294
|
+
* - a color that parses: the NEAREST opaque brand color, so a pale tint
|
|
295
|
+
* becomes the brand's pale surface and a deep one its deep color;
|
|
296
|
+
* - with no brand color recorded at all: dropped, leaving the block's own
|
|
297
|
+
* default, which is what rule 6 asks of an email with no brand.
|
|
298
|
+
*
|
|
299
|
+
* A value that does not parse (a name, a CSS variable) is left for the
|
|
300
|
+
* re-ask, and so is the whole tree when a snapped color would leave words
|
|
301
|
+
* below 3:1 against what they sit on. Nothing is relaxed: the settled tree
|
|
302
|
+
* is the one rule 6 checks.
|
|
303
|
+
* Reads and returns the tree as the model wrote it.
|
|
304
|
+
*/
|
|
305
|
+
export declare function aiSettleOffBrandEmailColors(input: unknown, brand: AiDoctrineTreeContext['brand']): unknown;
|
|
275
306
|
/**
|
|
276
307
|
* Rule 8 (tree). A long list typed out item by item is data: bound to a
|
|
277
308
|
* dataset or a content collection it stays current; typed, it is a copy that
|
|
@@ -683,6 +714,13 @@ export declare function aiNamesMatch(left: string, right: string): boolean;
|
|
|
683
714
|
* Rule 1 (plan): repeated items place a component, and a section two screens
|
|
684
715
|
* share is one. A workspace that keeps no reusable components draws them in
|
|
685
716
|
* their sections instead (AGL-3030), and nothing is refused.
|
|
717
|
+
*
|
|
718
|
+
* A job whose pages the layout language compiles (`repeatsCompiled`, a
|
|
719
|
+
* build) is not asked for a component for a section's repeated items: the
|
|
720
|
+
* compiler draws them in the section, compact where the plan places none, so
|
|
721
|
+
* the page answers rule 1 itself (AGL-3660). Held to the first half anyway,
|
|
722
|
+
* the canonical build request was refused twice for an About page's three
|
|
723
|
+
* values (AGL-3616). A section two pages share is still one component.
|
|
686
724
|
*/
|
|
687
725
|
export declare function detectPlanRepeats(plan: AiBuildPlan, inventory: AiSiteInventory | null, capabilities?: AiPlanCapabilities | null): AiDoctrineViolation[];
|
|
688
726
|
/** The fewest sections of one item, side by side and of one kind, that are one list split apart. */
|
|
@@ -25,7 +25,7 @@ import { parseBreakpointSpan } from "@aglyn/shared-data-enums/breakpoint-span";
|
|
|
25
25
|
import { renderEmailHtml } from "@aglyn/shared-util-email/email-render";
|
|
26
26
|
import { AI_BUILD_PLAN_CREATION_NOUNS, AI_BUILD_PLAN_EMBED_HOST_NAMES, AI_PLAN_ASKS_FOR_VIDEO, aiEmbedHostOf, aiEmbedVideoKey, aiPlanCreateFor, aiPlanSlugKey, aiPlanUndeclaredRefs, isAiPlanNewRef } from "../model/ai-build-plan.js";
|
|
27
27
|
import { aiCreationNoun, aiPlanCanPlaceForm, aiPlanUncreatable, aiPlanUncreatableKind } from "../model/ai-plan-capabilities.js";
|
|
28
|
-
import { aiFreeSiteSectionsWithin } from "../model/ai-site-job.js";
|
|
28
|
+
import { aiFreeSiteSectionsWithin, aiSiteHomeMinSections, aiSitePlanIsHome } from "../model/ai-site-job.js";
|
|
29
29
|
import { AI_INSTANCE_REF_PROP, validateAiNodeTree } from "./ai-node-tree.js";
|
|
30
30
|
import { AI_PAGE_SCROLL_TO_PROP, aiPageLinkTarget, aiStoredScrollTargets } from "./ai-page-links.js";
|
|
31
31
|
import { AI_EMAIL_CLIP_BYTES, AI_OUTPUT_BUDGETS, AI_OUTPUT_SURFACE, AI_TEXT_LIMITS, isHeadlineVariant } from "./ai-palette.js";
|
|
@@ -277,6 +277,22 @@ function listNames(ids, tree) {
|
|
|
277
277
|
*/ export function detectRepeatedSubtrees(tree, otherPages = [], context = {}) {
|
|
278
278
|
if (context.reusableComponents === false) return [];
|
|
279
279
|
const index = indexShapes(tree);
|
|
280
|
+
/**
|
|
281
|
+
* A compiled picture card (AGL-3660): a Stack that opens with its picture,
|
|
282
|
+
* in a cell of a grid — or that cell, holding only the card.
|
|
283
|
+
*/ const card = (id)=>{
|
|
284
|
+
var _node_nodes, _tree_nodes_first;
|
|
285
|
+
const node = id === undefined ? undefined : tree.nodes[id];
|
|
286
|
+
const first = node == null ? void 0 : (_node_nodes = node.nodes) == null ? void 0 : _node_nodes[0];
|
|
287
|
+
return (node == null ? void 0 : node.componentId) === 'muiStack' && first !== undefined && ((_tree_nodes_first = tree.nodes[first]) == null ? void 0 : _tree_nodes_first.componentId) === 'image';
|
|
288
|
+
};
|
|
289
|
+
const pictureCard = (id)=>{
|
|
290
|
+
var _node_props, _node_nodes, _index_visits_find, _tree_nodes_parentId;
|
|
291
|
+
const node = tree.nodes[id];
|
|
292
|
+
if ((node == null ? void 0 : node.componentId) === 'muiGrid' && ((_node_props = node.props) == null ? void 0 : _node_props['container']) !== true) return ((_node_nodes = node.nodes) == null ? void 0 : _node_nodes.length) === 1 && card(node.nodes[0]);
|
|
293
|
+
const parentId = (_index_visits_find = index.visits.find((visit)=>visit.id === id)) == null ? void 0 : _index_visits_find.ancestors.at(-1);
|
|
294
|
+
return card(id) && parentId !== undefined && ((_tree_nodes_parentId = tree.nodes[parentId]) == null ? void 0 : _tree_nodes_parentId.componentId) === 'muiGrid';
|
|
295
|
+
};
|
|
280
296
|
const violations = repeatedShapes(tree, index, AI_REPEAT_MIN_NODES, AI_REPEAT_MIN_COUNT, // Counted within the band of the page it sits in (AGL-3660), as rule 8
|
|
281
297
|
// counts a typed list: a heading over its words in the About band and in
|
|
282
298
|
// the Services band are two parts of two stories, not one block placed twice.
|
|
@@ -288,7 +304,7 @@ function listNames(ids, tree) {
|
|
|
288
304
|
var _tree_nodes_id;
|
|
289
305
|
return ((_tree_nodes_id = tree.nodes[id]) == null ? void 0 : _tree_nodes_id.componentId) === 'section';
|
|
290
306
|
})) != null ? _reverse_find : tree.rootId;
|
|
291
|
-
}, aiRepeatFrameOrSection(tree)).map(({ ids })=>{
|
|
307
|
+
}, aiRepeatFrameOrSection(tree)).filter(({ ids })=>!(context.repeatsCompiled && ids.every(pictureCard))).map(({ ids })=>{
|
|
292
308
|
const name = displayName(tree.nodes[ids[0]].componentId);
|
|
293
309
|
return {
|
|
294
310
|
rule: 1,
|
|
@@ -729,6 +745,199 @@ function normalizeHex(value) {
|
|
|
729
745
|
}
|
|
730
746
|
];
|
|
731
747
|
}
|
|
748
|
+
/** The props an email block is colored by, the ones rule 6 reads. */ const EMAIL_COLOR_PROPS = [
|
|
749
|
+
'color',
|
|
750
|
+
'backgroundColor'
|
|
751
|
+
];
|
|
752
|
+
/**
|
|
753
|
+
* A color as opaque RGB: hex of 3, 4, 6 or 8 digits, or `rgb()`/`rgba()`. A
|
|
754
|
+
* translucent value is laid over white, the page an email is read on. `null`
|
|
755
|
+
* for anything else, a name or a token included.
|
|
756
|
+
*/ function emailRgbOf(value) {
|
|
757
|
+
var _exec;
|
|
758
|
+
const text = value.trim().toLowerCase();
|
|
759
|
+
const hex = (_exec = /^#([0-9a-f]{3,4}|[0-9a-f]{6}|[0-9a-f]{8})$/.exec(text)) == null ? void 0 : _exec[1];
|
|
760
|
+
let r;
|
|
761
|
+
let g;
|
|
762
|
+
let b;
|
|
763
|
+
let alpha = 1;
|
|
764
|
+
if (hex) {
|
|
765
|
+
const full = hex.length <= 4 ? [
|
|
766
|
+
...hex
|
|
767
|
+
].map((digit)=>digit + digit).join('') : hex;
|
|
768
|
+
r = parseInt(full.slice(0, 2), 16);
|
|
769
|
+
g = parseInt(full.slice(2, 4), 16);
|
|
770
|
+
b = parseInt(full.slice(4, 6), 16);
|
|
771
|
+
if (full.length === 8) alpha = parseInt(full.slice(6, 8), 16) / 255;
|
|
772
|
+
} else {
|
|
773
|
+
const fn = /^rgba?\(\s*([\d.]+)\s*,\s*([\d.]+)\s*,\s*([\d.]+)\s*(?:,\s*([\d.]+)\s*)?\)$/.exec(text);
|
|
774
|
+
if (!fn) return null;
|
|
775
|
+
r = Number(fn[1]);
|
|
776
|
+
g = Number(fn[2]);
|
|
777
|
+
b = Number(fn[3]);
|
|
778
|
+
if (fn[4] !== undefined) alpha = Number(fn[4]);
|
|
779
|
+
if (![
|
|
780
|
+
r,
|
|
781
|
+
g,
|
|
782
|
+
b,
|
|
783
|
+
alpha
|
|
784
|
+
].every(Number.isFinite) || r > 255 || g > 255 || b > 255 || alpha > 1) return null;
|
|
785
|
+
}
|
|
786
|
+
const over = (channel)=>Math.round(channel * alpha + 255 * (1 - alpha));
|
|
787
|
+
return {
|
|
788
|
+
r: over(r),
|
|
789
|
+
g: over(g),
|
|
790
|
+
b: over(b),
|
|
791
|
+
alpha
|
|
792
|
+
};
|
|
793
|
+
}
|
|
794
|
+
/** How far apart two colors look: the weighted "redmean" RGB distance. */ function emailColorDistance(a, b) {
|
|
795
|
+
const mean = (a.r + b.r) / 2;
|
|
796
|
+
const dr = a.r - b.r;
|
|
797
|
+
const dg = a.g - b.g;
|
|
798
|
+
const db = a.b - b.b;
|
|
799
|
+
return Math.sqrt((2 + mean / 256) * dr * dr + 4 * dg * dg + (2 + (255 - mean) / 256) * db * db);
|
|
800
|
+
}
|
|
801
|
+
/**
|
|
802
|
+
* An email's off-brand colors, settled where each has one reading (AGL-3676).
|
|
803
|
+
* Rule 6 holds an email to the brand's own values, and the guided start's
|
|
804
|
+
* welcome email kept writing tints of them — a pale wash of the primary color
|
|
805
|
+
* behind a section — and stopped on rule 6 after its re-ask. Before the tree
|
|
806
|
+
* is checked, each `color` and `backgroundColor` outside the brand is:
|
|
807
|
+
*
|
|
808
|
+
* - a palette path the brand lists, such as `primary.main`: its value;
|
|
809
|
+
* - a color that parses: the NEAREST opaque brand color, so a pale tint
|
|
810
|
+
* becomes the brand's pale surface and a deep one its deep color;
|
|
811
|
+
* - with no brand color recorded at all: dropped, leaving the block's own
|
|
812
|
+
* default, which is what rule 6 asks of an email with no brand.
|
|
813
|
+
*
|
|
814
|
+
* A value that does not parse (a name, a CSS variable) is left for the
|
|
815
|
+
* re-ask, and so is the whole tree when a snapped color would leave words
|
|
816
|
+
* below 3:1 against what they sit on. Nothing is relaxed: the settled tree
|
|
817
|
+
* is the one rule 6 checks.
|
|
818
|
+
* Reads and returns the tree as the model wrote it.
|
|
819
|
+
*/ export function aiSettleOffBrandEmailColors(input, brand) {
|
|
820
|
+
var _ref;
|
|
821
|
+
if (!isRecord(input) || !isRecord(input['nodes'])) return input;
|
|
822
|
+
const entries = Object.entries((_ref = brand == null ? void 0 : brand.colors) != null ? _ref : {}).filter((entry)=>typeof entry[1] === 'string' && entry[1].trim() !== '');
|
|
823
|
+
const allowed = new Set(entries.map(([, value])=>normalizeHex(value)));
|
|
824
|
+
const byPath = new Map(entries.map(([path, value])=>[
|
|
825
|
+
path.toLowerCase(),
|
|
826
|
+
value.trim()
|
|
827
|
+
]));
|
|
828
|
+
const targets = entries.flatMap(([, value])=>{
|
|
829
|
+
const rgb = emailRgbOf(value);
|
|
830
|
+
return rgb && rgb.alpha === 1 ? [
|
|
831
|
+
{
|
|
832
|
+
value: value.trim(),
|
|
833
|
+
rgb
|
|
834
|
+
}
|
|
835
|
+
] : [];
|
|
836
|
+
});
|
|
837
|
+
const settle = (value)=>{
|
|
838
|
+
if (!entries.length) return null;
|
|
839
|
+
const named = byPath.get(value.trim().toLowerCase());
|
|
840
|
+
if (named) return named;
|
|
841
|
+
const rgb = emailRgbOf(value);
|
|
842
|
+
if (!rgb || !targets.length) return undefined;
|
|
843
|
+
let best = targets[0];
|
|
844
|
+
for (const target of targets){
|
|
845
|
+
if (emailColorDistance(rgb, target.rgb) < emailColorDistance(rgb, best.rgb)) best = target;
|
|
846
|
+
}
|
|
847
|
+
return best.value;
|
|
848
|
+
};
|
|
849
|
+
let nodes = null;
|
|
850
|
+
for (const [id, node] of Object.entries(input['nodes'])){
|
|
851
|
+
if (!isRecord(node) || !isRecord(node['props'])) continue;
|
|
852
|
+
let props = null;
|
|
853
|
+
for (const name of EMAIL_COLOR_PROPS){
|
|
854
|
+
const value = node['props'][name];
|
|
855
|
+
if (typeof value !== 'string' || !value.trim() || allowed.has(normalizeHex(value))) continue;
|
|
856
|
+
const settled = settle(value);
|
|
857
|
+
if (settled === undefined) continue;
|
|
858
|
+
props != null ? props : props = _extends({}, node['props']);
|
|
859
|
+
if (settled === null) delete props[name];
|
|
860
|
+
else props[name] = settled;
|
|
861
|
+
}
|
|
862
|
+
if (!props) continue;
|
|
863
|
+
nodes != null ? nodes : nodes = _extends({}, input['nodes']);
|
|
864
|
+
nodes[id] = _extends({}, node, {
|
|
865
|
+
props
|
|
866
|
+
});
|
|
867
|
+
}
|
|
868
|
+
if (!nodes || !targets.length) return nodes ? _extends({}, input, {
|
|
869
|
+
nodes
|
|
870
|
+
}) : input;
|
|
871
|
+
// Two colors snapped onto one brand value can leave words the color of what
|
|
872
|
+
// they sit on — a pale green heading on a cream band both become the cream.
|
|
873
|
+
// A settle that leaves any color it touched unreadable is not one reading,
|
|
874
|
+
// so the tree goes back as written and rule 6 asks the model.
|
|
875
|
+
return emailSettleKeepsContrast(input['nodes'], nodes) ? _extends({}, input, {
|
|
876
|
+
nodes
|
|
877
|
+
}) : input;
|
|
878
|
+
}
|
|
879
|
+
/** The least contrast words keep on what they sit on once settled: WCAG's large-text floor. */ const EMAIL_SETTLED_MIN_CONTRAST = 3;
|
|
880
|
+
function emailLuminance({ r, g, b }) {
|
|
881
|
+
const channel = (value)=>{
|
|
882
|
+
const c = value / 255;
|
|
883
|
+
return c <= 0.03928 ? c / 12.92 : ((c + 0.055) / 1.055) ** 2.4;
|
|
884
|
+
};
|
|
885
|
+
return 0.2126 * channel(r) + 0.7152 * channel(g) + 0.0722 * channel(b);
|
|
886
|
+
}
|
|
887
|
+
function emailContrast(a, b) {
|
|
888
|
+
const [light, dark] = [
|
|
889
|
+
emailLuminance(a),
|
|
890
|
+
emailLuminance(b)
|
|
891
|
+
].sort((x, y)=>y - x);
|
|
892
|
+
return (light + 0.05) / (dark + 0.05);
|
|
893
|
+
}
|
|
894
|
+
/**
|
|
895
|
+
* Whether every block whose words or ground the settle changed can still be
|
|
896
|
+
* read: its `color` against its own `backgroundColor` (a button's fill), else
|
|
897
|
+
* the nearest enclosing one, else the white an email is read on. A button
|
|
898
|
+
* with no fill of its own is left out: its fill is the block's default.
|
|
899
|
+
*/ function emailSettleKeepsContrast(written, settled) {
|
|
900
|
+
const parentOf = new Map();
|
|
901
|
+
for (const [id, node] of Object.entries(settled)){
|
|
902
|
+
if (!isRecord(node) || !Array.isArray(node['nodes'])) continue;
|
|
903
|
+
for (const child of node['nodes'])if (typeof child === 'string') parentOf.set(child, id);
|
|
904
|
+
}
|
|
905
|
+
const propOf = (nodes, id, name)=>{
|
|
906
|
+
const node = nodes[id];
|
|
907
|
+
return isRecord(node) && isRecord(node['props']) ? node['props'][name] : undefined;
|
|
908
|
+
};
|
|
909
|
+
/** The id whose background a node's words sit on, or `null` for the email's white. */ const groundOf = (id)=>{
|
|
910
|
+
if (typeof propOf(settled, id, 'backgroundColor') === 'string') return id;
|
|
911
|
+
const node = settled[id];
|
|
912
|
+
if (isRecord(node) && node['componentId'] === 'emailButton') return undefined;
|
|
913
|
+
const seen = new Set([
|
|
914
|
+
id
|
|
915
|
+
]);
|
|
916
|
+
for(let at = parentOf.get(id); at && !seen.has(at); at = parentOf.get(at)){
|
|
917
|
+
seen.add(at);
|
|
918
|
+
if (typeof propOf(settled, at, 'backgroundColor') === 'string') return at;
|
|
919
|
+
}
|
|
920
|
+
return null;
|
|
921
|
+
};
|
|
922
|
+
const WHITE = {
|
|
923
|
+
r: 255,
|
|
924
|
+
g: 255,
|
|
925
|
+
b: 255
|
|
926
|
+
};
|
|
927
|
+
for (const id of Object.keys(settled)){
|
|
928
|
+
const color = propOf(settled, id, 'color');
|
|
929
|
+
if (typeof color !== 'string') continue;
|
|
930
|
+
const ground = groundOf(id);
|
|
931
|
+
if (ground === undefined) continue;
|
|
932
|
+
const touched = color !== propOf(written, id, 'color') || ground !== null && propOf(settled, ground, 'backgroundColor') !== propOf(written, ground, 'backgroundColor');
|
|
933
|
+
if (!touched) continue;
|
|
934
|
+
const text = emailRgbOf(color);
|
|
935
|
+
const background = ground === null ? WHITE : emailRgbOf(String(propOf(settled, ground, 'backgroundColor')));
|
|
936
|
+
if (!text || !background) continue;
|
|
937
|
+
if (emailContrast(text, background) < EMAIL_SETTLED_MIN_CONTRAST) return false;
|
|
938
|
+
}
|
|
939
|
+
return true;
|
|
940
|
+
}
|
|
732
941
|
/**
|
|
733
942
|
* Rule 8 (tree). A long list typed out item by item is data: bound to a
|
|
734
943
|
* dataset or a content collection it stays current; typed, it is a copy that
|
|
@@ -2912,6 +3121,13 @@ const FORM_SECTION_NAME = /\bform\b|\bsign[- ]?up\b|\bsubscribe\b|\bnewsletter\b
|
|
|
2912
3121
|
* Rule 1 (plan): repeated items place a component, and a section two screens
|
|
2913
3122
|
* share is one. A workspace that keeps no reusable components draws them in
|
|
2914
3123
|
* their sections instead (AGL-3030), and nothing is refused.
|
|
3124
|
+
*
|
|
3125
|
+
* A job whose pages the layout language compiles (`repeatsCompiled`, a
|
|
3126
|
+
* build) is not asked for a component for a section's repeated items: the
|
|
3127
|
+
* compiler draws them in the section, compact where the plan places none, so
|
|
3128
|
+
* the page answers rule 1 itself (AGL-3660). Held to the first half anyway,
|
|
3129
|
+
* the canonical build request was refused twice for an About page's three
|
|
3130
|
+
* values (AGL-3616). A section two pages share is still one component.
|
|
2915
3131
|
*/ export function detectPlanRepeats(plan, inventory, capabilities = null) {
|
|
2916
3132
|
if ((capabilities == null ? void 0 : capabilities.reusableComponents) === false) return [];
|
|
2917
3133
|
const kinds = inventoryKinds(inventory);
|
|
@@ -2921,7 +3137,7 @@ const FORM_SECTION_NAME = /\bform\b|\bsign[- ]?up\b|\bsubscribe\b|\bnewsletter\b
|
|
|
2921
3137
|
'dataset',
|
|
2922
3138
|
'collection'
|
|
2923
3139
|
].includes(String(refKind(ref, plan, kinds))));
|
|
2924
|
-
const repeated = sectionPaths(plan).filter(({ section })=>section.items >= AI_REPEAT_MIN_COUNT && !placesComponent(section.uses) && !bound(section.uses));
|
|
3140
|
+
const repeated = (capabilities == null ? void 0 : capabilities.repeatsCompiled) ? [] : sectionPaths(plan).filter(({ section })=>section.items >= AI_REPEAT_MIN_COUNT && !placesComponent(section.uses) && !bound(section.uses));
|
|
2925
3141
|
if (repeated.length) {
|
|
2926
3142
|
violations.push({
|
|
2927
3143
|
rule: 1,
|
|
@@ -3017,7 +3233,7 @@ const FORM_SECTION_NAME = /\bform\b|\bsign[- ]?up\b|\bsubscribe\b|\bnewsletter\b
|
|
|
3017
3233
|
const listed = `${names.slice(0, -1).join(', ')} and ${names[names.length - 1]}`;
|
|
3018
3234
|
const [{ kind }] = run;
|
|
3019
3235
|
// Rule 1 asks a list this long on a workspace that keeps components to place one for its item.
|
|
3020
|
-
const component = !kind.components.length && (capabilities == null ? void 0 : capabilities.reusableComponents) !== false && run.length >= AI_REPEAT_MIN_COUNT;
|
|
3236
|
+
const component = !kind.components.length && (capabilities == null ? void 0 : capabilities.reusableComponents) !== false && !(capabilities == null ? void 0 : capabilities.repeatsCompiled) && run.length >= AI_REPEAT_MIN_COUNT;
|
|
3021
3237
|
const section = {
|
|
3022
3238
|
name: kind.name,
|
|
3023
3239
|
uses: component ? [
|
|
@@ -3685,9 +3901,12 @@ function slugKey(slug) {
|
|
|
3685
3901
|
}
|
|
3686
3902
|
];
|
|
3687
3903
|
}
|
|
3904
|
+
// A site start's saved form is a step of its own (AGL-3596), counted on the wall since AGL-3660.
|
|
3905
|
+
const forms = plan.create.filter((entry)=>entry.kind === 'form').length - aiPlanUncreatable(plan, capabilities).filter((entry)=>entry.kind === 'form').length;
|
|
3688
3906
|
const fits = sitePages !== undefined ? aiFreeSiteSectionsWithin({
|
|
3689
3907
|
layouts,
|
|
3690
|
-
pages
|
|
3908
|
+
pages,
|
|
3909
|
+
forms: Math.max(0, forms)
|
|
3691
3910
|
}, FREE_AI_TASTE_CREDITS_PER_MONTH) : aiFreePageSectionsWithin({
|
|
3692
3911
|
layouts,
|
|
3693
3912
|
pages
|
|
@@ -3695,11 +3914,22 @@ function slugKey(slug) {
|
|
|
3695
3914
|
if (asked <= fits) return [];
|
|
3696
3915
|
const job = pages > 1 ? `a Free plan of ${pages} pages` : 'a Free page';
|
|
3697
3916
|
const beside = layouts ? ` that creates ${layouts > 1 ? `${layouts} layouts` : 'its layout'}` : '';
|
|
3917
|
+
// A site start's home keeps its full five (AGL-3660): told only "plan at
|
|
3918
|
+
// most 8", the live yoga start of 2026-10-08 cut a home of 6 to 4 and kept
|
|
3919
|
+
// its other page, so the redo names where the sections come from.
|
|
3920
|
+
const homeMin = sitePages !== undefined && plan.screens.some(aiSitePlanIsHome) ? aiSiteHomeMinSections({
|
|
3921
|
+
pages,
|
|
3922
|
+
across: fits
|
|
3923
|
+
}) : 0;
|
|
3924
|
+
const keepHome = homeMin ? ` Keep the home page at / at ${homeMin} or more sections, and take the rest from the other ${pages > 2 ? 'pages' : 'page'}.` : '';
|
|
3925
|
+
// A site start's redo keeps the plan full (AGL-3660): the wall's sections
|
|
3926
|
+
// are its budget to use, and a redo told only "at most" cuts far under it.
|
|
3927
|
+
const redo = sitePages !== undefined && fits >= 2 ? `Plan ${fits - 1} to ${fits}: the ${fits} are the budget to use, not a ceiling to stay under. Draw a list's repeated items in one section, and take out only the sections the brief needs least.` : `Plan at most ${fits}: draw a list's repeated items in one section, and leave out a section the brief does not ask for.`;
|
|
3698
3928
|
return [
|
|
3699
3929
|
{
|
|
3700
3930
|
rule: null,
|
|
3701
3931
|
code: 'plan-over-free-wall',
|
|
3702
|
-
message: `This plan asks for ${asked} sections, and ${job}${beside} fits ${fits} in the ${FREE_AI_TASTE_CREDITS_PER_MONTH} AI credits a Free workspace has a month.
|
|
3932
|
+
message: `This plan asks for ${asked} sections, and ${job}${beside} fits ${fits} in the ${FREE_AI_TASTE_CREDITS_PER_MONTH} AI credits a Free workspace has a month. ${redo}${keepHome}`,
|
|
3703
3933
|
paths: plan.screens.map((_, index)=>`screens[${index}].sections`)
|
|
3704
3934
|
}
|
|
3705
3935
|
];
|