@aglyn/plugins-ai 1.0.0-beta.233 → 1.0.0-beta.235
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +17 -17
- package/src/lib/activity/ai-activity-actions.d.ts +25 -0
- package/src/lib/activity/ai-activity-actions.js +85 -4
- package/src/lib/activity/ai-activity-actions.js.map +1 -1
- package/src/lib/components/ai-brief-dialog.component.js +46 -2
- package/src/lib/components/ai-brief-dialog.component.js.map +1 -1
- package/src/lib/components/ai-campaign-create.component.js +1 -1
- package/src/lib/components/ai-campaign-create.component.js.map +1 -1
- package/src/lib/components/ai-credits-card.component.js +3 -3
- package/src/lib/components/ai-credits-card.component.js.map +1 -1
- package/src/lib/components/ai-crm-email-draft.component.js +1 -1
- package/src/lib/components/ai-crm-email-draft.component.js.map +1 -1
- package/src/lib/components/ai-crm-import-mapping.component.js +1 -1
- package/src/lib/components/ai-crm-import-mapping.component.js.map +1 -1
- package/src/lib/components/ai-crm-record-card.component.js +1 -1
- package/src/lib/components/ai-crm-record-card.component.js.map +1 -1
- package/src/lib/components/ai-describe-automation.component.js +4 -2
- package/src/lib/components/ai-describe-automation.component.js.map +1 -1
- package/src/lib/components/ai-describe-org-automation.component.js +1 -1
- package/src/lib/components/ai-describe-org-automation.component.js.map +1 -1
- package/src/lib/components/ai-experiment-cards.component.js +2 -2
- package/src/lib/components/ai-experiment-cards.component.js.map +1 -1
- package/src/lib/components/ai-explain-automation.component.js +1 -1
- package/src/lib/components/ai-explain-automation.component.js.map +1 -1
- package/src/lib/components/ai-insight-card.component.js +3 -1
- package/src/lib/components/ai-insight-card.component.js.map +1 -1
- 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-jobs-list-page.component.d.ts +6 -1
- package/src/lib/components/ai-jobs-list-page.component.js +15 -2
- package/src/lib/components/ai-jobs-list-page.component.js.map +1 -1
- package/src/lib/components/ai-logic.component.js +5 -3
- package/src/lib/components/ai-logic.component.js.map +1 -1
- package/src/lib/components/ai-media-create.component.js +88 -16
- package/src/lib/components/ai-media-create.component.js.map +1 -1
- package/src/lib/components/ai-model-selector.component.js +8 -0
- package/src/lib/components/ai-model-selector.component.js.map +1 -1
- package/src/lib/components/ai-overlay-cards.component.js +2 -2
- package/src/lib/components/ai-overlay-cards.component.js.map +1 -1
- package/src/lib/components/ai-product-copy-card.component.js +2 -1
- package/src/lib/components/ai-product-copy-card.component.js.map +1 -1
- package/src/lib/components/ai-products-hub-card.component.js +2 -1
- package/src/lib/components/ai-products-hub-card.component.js.map +1 -1
- package/src/lib/components/ai-revise-automation.component.js +1 -1
- package/src/lib/components/ai-revise-automation.component.js.map +1 -1
- package/src/lib/components/ai-seo-fields-card.component.js +7 -3
- package/src/lib/components/ai-seo-fields-card.component.js.map +1 -1
- package/src/lib/components/ai-site-batch-card.component.js +7 -4
- package/src/lib/components/ai-site-batch-card.component.js.map +1 -1
- package/src/lib/components/ai-site-build-page.component.js +48 -6
- package/src/lib/components/ai-site-build-page.component.js.map +1 -1
- package/src/lib/components/ai-site-seo-start-card.component.js +7 -4
- package/src/lib/components/ai-site-seo-start-card.component.js.map +1 -1
- package/src/lib/components/ai-site-start-card.component.js +152 -36
- package/src/lib/components/ai-site-start-card.component.js.map +1 -1
- package/src/lib/components/ai-theme-proposal-card.component.js +1 -0
- package/src/lib/components/ai-theme-proposal-card.component.js.map +1 -1
- package/src/lib/components/ai-upsell-dialog.component.js +17 -3
- package/src/lib/components/ai-upsell-dialog.component.js.map +1 -1
- package/src/lib/components/ai-usage-strip.component.js +13 -2
- package/src/lib/components/ai-usage-strip.component.js.map +1 -1
- package/src/lib/components/assist-build-card.component.js +20 -3
- package/src/lib/components/assist-build-card.component.js.map +1 -1
- package/src/lib/components/assist-jobs-drawer.component.js +7 -1
- package/src/lib/components/assist-jobs-drawer.component.js.map +1 -1
- package/src/lib/components/billing-ai-allotments.component.js +2 -2
- package/src/lib/components/billing-ai-allotments.component.js.map +1 -1
- package/src/lib/components/billing-ai-top-users.component.js +1 -0
- package/src/lib/components/billing-ai-top-users.component.js.map +1 -1
- package/src/lib/components/billing-assist-overage-card.component.js +2 -2
- package/src/lib/components/billing-assist-overage-card.component.js.map +1 -1
- package/src/lib/components/host-ai-allotments.component.js +2 -2
- package/src/lib/components/host-ai-allotments.component.js.map +1 -1
- package/src/lib/components/member-ai-allotment-card.component.js +2 -2
- package/src/lib/components/member-ai-allotment-card.component.js.map +1 -1
- package/src/lib/components/staff-org-ai-card.component.js +1 -0
- package/src/lib/components/staff-org-ai-card.component.js.map +1 -1
- package/src/lib/components/staff-org-ai-conversations.component.d.ts +9 -3
- package/src/lib/components/staff-org-ai-conversations.component.js +174 -83
- package/src/lib/components/staff-org-ai-conversations.component.js.map +1 -1
- package/src/lib/components/staff-user-ai-usage-card.component.js +2 -1
- package/src/lib/components/staff-user-ai-usage-card.component.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 +6 -1
- package/src/lib/jobs/ai-job-layout-language.js.map +1 -1
- package/src/lib/jobs/ai-job-layout-site-pages.d.ts +14 -0
- package/src/lib/jobs/ai-job-layout-site-pages.js +43 -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 +26 -1
- package/src/lib/jobs/ai-job-page-language.js +75 -13
- 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 +32 -7
- package/src/lib/jobs/ai-job-page-step.js.map +1 -1
- package/src/lib/jobs/ai-job-plan-step.d.ts +34 -0
- package/src/lib/jobs/ai-job-plan-step.js +106 -14
- package/src/lib/jobs/ai-job-plan-step.js.map +1 -1
- package/src/lib/jobs/ai-job-site-content.d.ts +81 -13
- package/src/lib/jobs/ai-job-site-content.js +210 -43
- package/src/lib/jobs/ai-job-site-content.js.map +1 -1
- package/src/lib/jobs/ai-job-site-step.d.ts +19 -0
- package/src/lib/jobs/ai-job-site-step.js +152 -51
- package/src/lib/jobs/ai-job-site-step.js.map +1 -1
- package/src/lib/jobs/ai-jobs.d.ts +8 -4
- package/src/lib/jobs/ai-jobs.js +19 -2
- 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 +37 -0
- package/src/lib/layout-language/ai-layout-compiler.js +1003 -19
- package/src/lib/layout-language/ai-layout-compiler.js.map +1 -1
- package/src/lib/layout-language/ai-layout-design.d.ts +114 -0
- package/src/lib/layout-language/ai-layout-design.js +209 -0
- package/src/lib/layout-language/ai-layout-design.js.map +1 -0
- package/src/lib/layout-language/ai-layout-frame.d.ts +12 -0
- package/src/lib/layout-language/ai-layout-frame.js +33 -8
- package/src/lib/layout-language/ai-layout-frame.js.map +1 -1
- package/src/lib/layout-language/ai-layout-gaps.js +18 -2
- package/src/lib/layout-language/ai-layout-gaps.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 +36 -0
- package/src/lib/layout-language/ai-layout-links.js +26 -2
- package/src/lib/layout-language/ai-layout-links.js.map +1 -1
- package/src/lib/layout-language/ai-layout-listings.d.ts +125 -0
- package/src/lib/layout-language/ai-layout-listings.js +187 -0
- package/src/lib/layout-language/ai-layout-listings.js.map +1 -0
- 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 +7 -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 +104 -15
- package/src/lib/model/ai-site-build-progress.js.map +1 -1
- package/src/lib/model/ai-site-job.d.ts +236 -24
- package/src/lib/model/ai-site-job.js +269 -22
- package/src/lib/model/ai-site-job.js.map +1 -1
- package/src/lib/model/ai-site-kinds.d.ts +30 -8
- package/src/lib/model/ai-site-kinds.js +735 -116
- 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/model/ai-site-start-seo.d.ts +30 -0
- package/src/lib/model/ai-site-start-seo.js +132 -11
- package/src/lib/model/ai-site-start-seo.js.map +1 -1
- package/src/lib/model/ai-site-start.d.ts +2 -2
- package/src/lib/model/ai-site-start.js +3 -3
- package/src/lib/model/ai-site-start.js.map +1 -1
- package/src/lib/plugin.js +15 -3
- package/src/lib/plugin.js.map +1 -1
- package/src/lib/runtime/ai-doctrine-validators.d.ts +32 -1
- package/src/lib/runtime/ai-doctrine-validators.js +227 -4
- 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 +31 -5
- package/src/lib/runtime/ai-palette.generated.js.map +1 -1
- package/src/lib/server/ai-admin-conversations.d.ts +9 -0
- package/src/lib/server/ai-admin-conversations.js +151 -9
- package/src/lib/server/ai-admin-conversations.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
- package/src/lib/usage/staff-org-ai-conversations.d.ts +23 -0
- package/src/lib/usage/staff-org-ai-conversations.js +45 -0
- package/src/lib/usage/staff-org-ai-conversations.js.map +1 -1
|
@@ -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
|
|
@@ -3692,9 +3901,12 @@ function slugKey(slug) {
|
|
|
3692
3901
|
}
|
|
3693
3902
|
];
|
|
3694
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;
|
|
3695
3906
|
const fits = sitePages !== undefined ? aiFreeSiteSectionsWithin({
|
|
3696
3907
|
layouts,
|
|
3697
|
-
pages
|
|
3908
|
+
pages,
|
|
3909
|
+
forms: Math.max(0, forms)
|
|
3698
3910
|
}, FREE_AI_TASTE_CREDITS_PER_MONTH) : aiFreePageSectionsWithin({
|
|
3699
3911
|
layouts,
|
|
3700
3912
|
pages
|
|
@@ -3702,11 +3914,22 @@ function slugKey(slug) {
|
|
|
3702
3914
|
if (asked <= fits) return [];
|
|
3703
3915
|
const job = pages > 1 ? `a Free plan of ${pages} pages` : 'a Free page';
|
|
3704
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.`;
|
|
3705
3928
|
return [
|
|
3706
3929
|
{
|
|
3707
3930
|
rule: null,
|
|
3708
3931
|
code: 'plan-over-free-wall',
|
|
3709
|
-
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}`,
|
|
3710
3933
|
paths: plan.screens.map((_, index)=>`screens[${index}].sections`)
|
|
3711
3934
|
}
|
|
3712
3935
|
];
|