@samva/markup 0.13.1 → 0.15.0
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/AGENTS.md +12 -2
- package/CHANGELOG.md +58 -0
- package/README.md +1 -1
- package/dist/{assets-DCULM6QD.d.ts → assets-tISHaQNF.d.ts} +1 -1
- package/dist/{brand-C45QBKX9.js → brand-nreeKWxF.js} +5 -5
- package/dist/brand.d.ts +48 -25
- package/dist/brand.js +4 -4
- package/dist/compiler.d.ts +23 -6
- package/dist/compiler.js +658 -200
- package/dist/components-3XHjYD3i.js +401 -0
- package/dist/dark-scheme-CbCwGDWH.js +12 -0
- package/dist/{diagnostic-codes-DdZ4tka5.js → diagnostic-codes-UXwOO3A-.js} +22 -0
- package/dist/diagnostics.d.ts +17 -0
- package/dist/diagnostics.js +1 -1
- package/dist/edit.js +2 -2
- package/dist/email.d.ts +52 -9
- package/dist/email.js +1 -1
- package/dist/{html-eOZfDwVf.js → html-BvRFepeO.js} +9 -0
- package/dist/{ir-C-ubz6N3.js → ir-DNL6H7jA.js} +203 -79
- package/dist/{profile-CXnkfEMi.js → profile-AatO_SvP.js} +135 -13
- package/dist/{render-C6OTH8BQ.d.ts → render-BdvewoQt.d.ts} +19 -0
- package/dist/{render-Cja448i0.js → render-DpYouYYG.js} +8 -6
- package/dist/render.d.ts +1 -1
- package/dist/render.js +3 -3
- package/dist/{theme-css-CEUrDpqq.js → theme-css-s9QwD5Ko.js} +108 -46
- package/dist/whatsapp.d.ts +20 -2
- package/dist/whatsapp.js +3 -2
- package/generated/contract.json +206 -2
- package/generated/reference.md +165 -110
- package/package.json +1 -1
- package/skills/sml/SKILL.md +63 -5
- package/dist/components-DycSEKva.js +0 -273
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
import { n as isObject } from "./guards-eV1qLbf2.js";
|
|
2
2
|
import { n as inputSchema, r as jsonSchema } from "./input-schema-zjkgrXn2.js";
|
|
3
3
|
import { n as TEMPLATE_NAME_PATTERN, t as TEMPLATE_ID_PATTERN } from "./template-zLUjcwfF.js";
|
|
4
|
-
import { D as samvaBrandPlugin, E as reservedBrandAttributes, O as BRAND_FOOTER_MARKER, S as emailElement, T as parseBrandSpecifier, b as EMAIL_ELEMENTS, s as serializeStyle, t as checkAttribute } from "./html-
|
|
4
|
+
import { D as samvaBrandPlugin, E as reservedBrandAttributes, O as BRAND_FOOTER_MARKER, S as emailElement, T as parseBrandSpecifier, b as EMAIL_ELEMENTS, s as serializeStyle, t as checkAttribute } from "./html-BvRFepeO.js";
|
|
5
5
|
import { FORMATTER_NAMES } from "./fmt.js";
|
|
6
6
|
import { i as drainEmailDiagnostics, t as EmailNode } from "./jsx-runtime-CBlexoeR.js";
|
|
7
|
-
import { a as Email, d as equalColumnWidth, i as Divider, l as Section, n as Column, o as Image, r as Columns, s as Link, t as Button, u as Spacer } from "./components-
|
|
8
|
-
import { n as applyStylesheet } from "./ir-
|
|
9
|
-
import { i as buildLineMap, s as lineColumn, t as DIAGNOSTIC_CODES } from "./diagnostic-codes-
|
|
10
|
-
import { l as isAssetPath, n as brandComponents } from "./brand-
|
|
7
|
+
import { a as Email, d as equalColumnWidth, i as Divider, l as Section, n as Column, o as Image, p as PRIMITIVE_CLASSES, r as Columns, s as Link, t as Button, u as Spacer } from "./components-3XHjYD3i.js";
|
|
8
|
+
import { i as renderDarkRules, n as DARK_CLASS_PREFIX, r as applyStylesheet } from "./ir-DNL6H7jA.js";
|
|
9
|
+
import { i as buildLineMap, s as lineColumn, t as DIAGNOSTIC_CODES } from "./diagnostic-codes-UXwOO3A-.js";
|
|
10
|
+
import { l as isAssetPath, n as brandComponents } from "./brand-nreeKWxF.js";
|
|
11
11
|
import { parse } from "css-tree";
|
|
12
12
|
import { parse as parse$1 } from "@babel/parser";
|
|
13
13
|
//#region src/email/theme-imports.ts
|
|
@@ -156,11 +156,13 @@ const mergeStylesheets = (sheets) => {
|
|
|
156
156
|
}));
|
|
157
157
|
});
|
|
158
158
|
const variables = {};
|
|
159
|
+
const darkVariables = {};
|
|
159
160
|
const headAtRules = [];
|
|
160
161
|
const fontFaces = /* @__PURE__ */ new Map();
|
|
161
162
|
const diagnostics = sheets.flatMap((sheet) => sheet.diagnostics);
|
|
162
163
|
for (const sheet of sheets) {
|
|
163
164
|
Object.assign(variables, sheet.variables);
|
|
165
|
+
Object.assign(darkVariables, sheet.darkVariables);
|
|
164
166
|
headAtRules.push(...sheet.headAtRules);
|
|
165
167
|
for (const face of sheet.fontFaces) fontFaces.set(face.css, face);
|
|
166
168
|
}
|
|
@@ -169,6 +171,7 @@ const mergeStylesheets = (sheets) => {
|
|
|
169
171
|
headAtRules,
|
|
170
172
|
fontFaces: [...fontFaces.values()],
|
|
171
173
|
variables,
|
|
174
|
+
darkVariables,
|
|
172
175
|
diagnostics
|
|
173
176
|
};
|
|
174
177
|
};
|
|
@@ -974,6 +977,7 @@ const emailNodeToIr = (node, tokens, source) => {
|
|
|
974
977
|
//#endregion
|
|
975
978
|
//#region src/sml/components.ts
|
|
976
979
|
const NUMERIC = {
|
|
980
|
+
Email: ["gutter"],
|
|
977
981
|
Button: [
|
|
978
982
|
"height",
|
|
979
983
|
"paddingX",
|
|
@@ -983,6 +987,7 @@ const NUMERIC = {
|
|
|
983
987
|
"width"
|
|
984
988
|
],
|
|
985
989
|
Spacer: ["height"],
|
|
990
|
+
Columns: ["gap"],
|
|
986
991
|
Divider: ["thickness"],
|
|
987
992
|
BrandLogo: ["width"]
|
|
988
993
|
};
|
|
@@ -1043,7 +1048,7 @@ const equalWidth = (per, list) => {
|
|
|
1043
1048
|
* items (all in one row without `per`), each cell from the callback. Returns the nodes to place
|
|
1044
1049
|
* inside the Columns table, and a wrapper that repeats that table per chunk.
|
|
1045
1050
|
*/
|
|
1046
|
-
const columnsEach = (call, bindings) => {
|
|
1051
|
+
const columnsEach = (call, bindings, stack) => {
|
|
1047
1052
|
const { lowerer, ctx, node } = call;
|
|
1048
1053
|
const listValue = bindings.get("each");
|
|
1049
1054
|
const list = listValue === void 0 ? void 0 : bindingValue(listValue);
|
|
@@ -1070,7 +1075,8 @@ const columnsEach = (call, bindings) => {
|
|
|
1070
1075
|
const cellCtx = {
|
|
1071
1076
|
...ctx,
|
|
1072
1077
|
loops: row === void 0 ? ctx.loops : [...ctx.loops, row],
|
|
1073
|
-
columnWidth: equalWidth(per, list)
|
|
1078
|
+
columnWidth: equalWidth(per, list),
|
|
1079
|
+
columnStack: stack
|
|
1074
1080
|
};
|
|
1075
1081
|
const cells = lowerer.eachOver(cellCtx, node, row === void 0 ? list : { bind: `$${row}` }, fn);
|
|
1076
1082
|
if (cells === void 0) return void 0;
|
|
@@ -1124,17 +1130,32 @@ const runBuiltinComponent = (call) => {
|
|
|
1124
1130
|
let bindings = declared;
|
|
1125
1131
|
let wrap = (table) => table;
|
|
1126
1132
|
let kids;
|
|
1133
|
+
const stackBinding = declared.get("stack");
|
|
1134
|
+
const stack = stackBinding === void 0 ? true : bindingValue(stackBinding);
|
|
1135
|
+
if (exported === "Columns" && typeof stack !== "boolean") {
|
|
1136
|
+
lowerer.error(ctx, node, "invalid-columns-stack", "<Columns stack> decides the layout at compile time, so it must be true or false.", "Write stack={false}, or leave it out to stack on narrow screens.");
|
|
1137
|
+
return [];
|
|
1138
|
+
}
|
|
1127
1139
|
const childCtx = {
|
|
1128
1140
|
...ctx,
|
|
1129
|
-
columnWidth: void 0
|
|
1141
|
+
columnWidth: void 0,
|
|
1142
|
+
columnStack: exported === "Columns" ? stack === true : void 0
|
|
1130
1143
|
};
|
|
1144
|
+
if (exported === "Columns" && declared.has("each") && declared.has("gap")) {
|
|
1145
|
+
lowerer.error(ctx, node, "invalid-columns-gap", "<Columns gap> spaces Column children written out in the row; cells repeated with each cannot take it.", "Write the Columns out, or pad each repeated cell (className=\"px-2\").");
|
|
1146
|
+
return [];
|
|
1147
|
+
}
|
|
1131
1148
|
if (exported === "Columns" && declared.has("each")) {
|
|
1132
|
-
const chunked = columnsEach(call, declared);
|
|
1149
|
+
const chunked = columnsEach(call, declared, stack === true);
|
|
1133
1150
|
if (chunked === void 0) return [];
|
|
1134
|
-
bindings = new Map([...declared].filter(([name]) => name !== "each" && name !== "per"));
|
|
1151
|
+
bindings = new Map([...declared].filter(([name]) => name !== "each" && name !== "per" && name !== "stack"));
|
|
1135
1152
|
kids = [chunked.cells];
|
|
1136
1153
|
wrap = chunked.wrap;
|
|
1137
1154
|
} else kids = lowerer.jsxChildren(childCtx, node);
|
|
1155
|
+
if (exported === "Columns" && declared.has("gap") && kids.some((kid) => !isValueNode(kid) && !("el" in kid))) {
|
|
1156
|
+
lowerer.error(ctx, node, "invalid-columns-gap", "<Columns gap> spaces Column children that always render; a conditional or repeated Column would leave a stray gap.", "Pad the cells instead (className=\"pr-6\"), or write each Column unconditionally.");
|
|
1157
|
+
return [];
|
|
1158
|
+
}
|
|
1138
1159
|
const tokens = new Tokens();
|
|
1139
1160
|
const props = {};
|
|
1140
1161
|
for (const [name, bound] of bindings) props[name] = bindingToProp(call, tokens, name, bound);
|
|
@@ -1146,6 +1167,7 @@ const runBuiltinComponent = (call) => {
|
|
|
1146
1167
|
}
|
|
1147
1168
|
}
|
|
1148
1169
|
if (exported === "Column" && props.width === void 0 && ctx.columnWidth !== void 0) props.width = propFor(tokens, ctx.columnWidth);
|
|
1170
|
+
if (exported === "Column" && ctx.columnStack === true) props.className = typeof props.className === "string" && props.className !== "" ? `${PRIMITIVE_CLASSES.stack} ${props.className}` : PRIMITIVE_CLASSES.stack;
|
|
1149
1171
|
if (kids.length > 0) props.children = childrenProp(tokens, kids);
|
|
1150
1172
|
if (exported === "Button" && typeof props.children === "string" && props.children !== "" && props.width === void 0 && props.height !== void 0 && tokens.toValue(props.children) !== props.children) {
|
|
1151
1173
|
lowerer.error(ctx, node, "button-dynamic-label", "Classic Outlook draws the button from a fixed box, and a label read at render time cannot size it.", "Give the button a width: <Button width={200} …>.");
|
|
@@ -2508,7 +2530,12 @@ const BUTTON_TEXT_LIMIT = 25;
|
|
|
2508
2530
|
const URL_BUTTON_LIMIT = 2;
|
|
2509
2531
|
const PHONE_BUTTON_LIMIT = 1;
|
|
2510
2532
|
const QUICK_REPLY_LIMIT = 10;
|
|
2533
|
+
const FLOW_BUTTON_LIMIT = 1;
|
|
2511
2534
|
const BUTTON_LIMIT = 10;
|
|
2535
|
+
const FLOW_HANDLE_PATTERN = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
|
|
2536
|
+
const FLOW_HANDLE_LIMIT = 64;
|
|
2537
|
+
const FLOW_SCREEN_PATTERN = /^[A-Za-z_]{1,100}$/;
|
|
2538
|
+
const FLOW_ACTIONS = ["navigate", "data_exchange"];
|
|
2512
2539
|
const jsxName = (name) => {
|
|
2513
2540
|
if (name.type === "JSXIdentifier") return text(name, "name") ?? "";
|
|
2514
2541
|
if (name.type === "JSXMemberExpression") return `${jsxName(child(name, "object") ?? name)}.${text(child(name, "property") ?? name, "name") ?? ""}`;
|
|
@@ -2614,6 +2641,7 @@ const ZERO = {
|
|
|
2614
2641
|
url: 0,
|
|
2615
2642
|
phone: 0,
|
|
2616
2643
|
quickReply: 0,
|
|
2644
|
+
flow: 0,
|
|
2617
2645
|
total: 0
|
|
2618
2646
|
};
|
|
2619
2647
|
/** The most buttons of each kind any combination of the conditions can produce. */
|
|
@@ -2628,18 +2656,21 @@ const maximalCounts = (nodes) => {
|
|
|
2628
2656
|
url: Math.max(then.url, otherwise.url),
|
|
2629
2657
|
phone: Math.max(then.phone, otherwise.phone),
|
|
2630
2658
|
quickReply: Math.max(then.quickReply, otherwise.quickReply),
|
|
2659
|
+
flow: Math.max(then.flow, otherwise.flow),
|
|
2631
2660
|
total: Math.max(then.total, otherwise.total)
|
|
2632
2661
|
};
|
|
2633
2662
|
} else add = {
|
|
2634
2663
|
url: node.type === "url" ? 1 : 0,
|
|
2635
2664
|
phone: node.type === "phone" ? 1 : 0,
|
|
2636
2665
|
quickReply: node.type === "quick-reply" ? 1 : 0,
|
|
2666
|
+
flow: node.type === "flow" ? 1 : 0,
|
|
2637
2667
|
total: 1
|
|
2638
2668
|
};
|
|
2639
2669
|
counts = {
|
|
2640
2670
|
url: counts.url + add.url,
|
|
2641
2671
|
phone: counts.phone + add.phone,
|
|
2642
2672
|
quickReply: counts.quickReply + add.quickReply,
|
|
2673
|
+
flow: counts.flow + add.flow,
|
|
2643
2674
|
total: counts.total + add.total
|
|
2644
2675
|
};
|
|
2645
2676
|
}
|
|
@@ -2657,6 +2688,7 @@ const buttonNodes = (buttons) => buttons.flatMap((button) => {
|
|
|
2657
2688
|
case "url": return [button.text, button.url];
|
|
2658
2689
|
case "phone": return [button.text, button.phone];
|
|
2659
2690
|
case "copy-code": return [button.code];
|
|
2691
|
+
case "flow": return [button.text];
|
|
2660
2692
|
}
|
|
2661
2693
|
});
|
|
2662
2694
|
const whatsappChannel = (input, field) => {
|
|
@@ -2750,6 +2782,7 @@ const whatsappChannel = (input, field) => {
|
|
|
2750
2782
|
over("URL buttons", counts.url, URL_BUTTON_LIMIT);
|
|
2751
2783
|
over("phone buttons", counts.phone, PHONE_BUTTON_LIMIT);
|
|
2752
2784
|
over("quick-reply buttons", counts.quickReply, QUICK_REPLY_LIMIT);
|
|
2785
|
+
over("flow buttons", counts.flow, FLOW_BUTTON_LIMIT);
|
|
2753
2786
|
over("buttons in all", counts.total, BUTTON_LIMIT);
|
|
2754
2787
|
roots.push(...buttonNodes(buttons));
|
|
2755
2788
|
}
|
|
@@ -2768,7 +2801,8 @@ const BUTTON_TAGS = [
|
|
|
2768
2801
|
"UrlButton",
|
|
2769
2802
|
"QuickReplyButton",
|
|
2770
2803
|
"PhoneButton",
|
|
2771
|
-
"CopyCodeButton"
|
|
2804
|
+
"CopyCodeButton",
|
|
2805
|
+
"FlowButton"
|
|
2772
2806
|
];
|
|
2773
2807
|
const buttonList = (input, ctx, local, parent) => {
|
|
2774
2808
|
const { lowerer } = input;
|
|
@@ -2793,6 +2827,54 @@ const buttonList = (input, ctx, local, parent) => {
|
|
|
2793
2827
|
if (value === void 0) lowerer.error(ctx, element, "missing-attribute", `This button needs \`${name}\`.`, `Write ${name}="…" or ${name}={input.field}.`);
|
|
2794
2828
|
return value;
|
|
2795
2829
|
};
|
|
2830
|
+
const flowButton = (element) => {
|
|
2831
|
+
const label_ = label(element, "label");
|
|
2832
|
+
const values = /* @__PURE__ */ new Map();
|
|
2833
|
+
let valid = true;
|
|
2834
|
+
const refuse = (node, code, message, fix) => {
|
|
2835
|
+
valid = false;
|
|
2836
|
+
lowerer.error(ctx, node, code, message, fix);
|
|
2837
|
+
};
|
|
2838
|
+
for (const item of attributesOf(element)) {
|
|
2839
|
+
if (item.type === "JSXSpreadAttribute") {
|
|
2840
|
+
refuse(item, "invalid-attribute", "A spread attribute runs code, and templates render without running code.", "Write flow, action and screen as string literals.");
|
|
2841
|
+
continue;
|
|
2842
|
+
}
|
|
2843
|
+
const nameNode = child(item, "name");
|
|
2844
|
+
const name = nameNode === void 0 ? "" : jsxName(nameNode);
|
|
2845
|
+
if (name === "key") continue;
|
|
2846
|
+
if (name !== "flow" && name !== "action" && name !== "screen") {
|
|
2847
|
+
refuse(item, "invalid-attribute", `\`${name}\` is not an attribute of this button.`, "The attributes are flow, action and screen.");
|
|
2848
|
+
continue;
|
|
2849
|
+
}
|
|
2850
|
+
const raw = child(item, "value");
|
|
2851
|
+
const literal = raw?.type === "JSXExpressionContainer" ? child(raw, "expression") : raw;
|
|
2852
|
+
if (literal?.type !== "StringLiteral") {
|
|
2853
|
+
refuse(item, "invalid-attribute", `\`${name}\` is fixed when the template is registered, so it is a string literal, not a binding.`, `Write ${name}="…".`);
|
|
2854
|
+
continue;
|
|
2855
|
+
}
|
|
2856
|
+
values.set(name, text(literal, "value") ?? "");
|
|
2857
|
+
}
|
|
2858
|
+
const flow = values.get("flow");
|
|
2859
|
+
if (flow === void 0) {
|
|
2860
|
+
if (valid) refuse(element, "missing-attribute", "This button needs `flow`.", "Write flow=\"handle\".");
|
|
2861
|
+
} else if (flow.length > FLOW_HANDLE_LIMIT || !FLOW_HANDLE_PATTERN.test(flow)) refuse(element, "invalid-attribute", `\`${flow}\` is not a flow handle.`, `Use the lowercase kebab-case handle of flows/<handle>.json, at most ${FLOW_HANDLE_LIMIT} characters.`);
|
|
2862
|
+
const action = values.get("action") ?? "navigate";
|
|
2863
|
+
if (!FLOW_ACTIONS.includes(action)) refuse(element, "invalid-attribute", `\`${action}\` is not a flow action.`, `Use one of ${FLOW_ACTIONS.join(", ")}.`);
|
|
2864
|
+
const screen = values.get("screen");
|
|
2865
|
+
if (screen !== void 0) {
|
|
2866
|
+
if (action === "data_exchange") refuse(element, "invalid-attribute", "A data_exchange flow has its endpoint choose the first screen, so it takes no `screen`.", "Remove screen, or use action=\"navigate\".");
|
|
2867
|
+
else if (!FLOW_SCREEN_PATTERN.test(screen) || screen === "SUCCESS") refuse(element, "invalid-attribute", `\`${screen}\` is not a flow screen id.`, "Use the screen's id: letters and underscores, at most 100 characters, and not SUCCESS.");
|
|
2868
|
+
}
|
|
2869
|
+
if (!valid || flow === void 0 || label_ === void 0) return void 0;
|
|
2870
|
+
return {
|
|
2871
|
+
type: "flow",
|
|
2872
|
+
text: label_,
|
|
2873
|
+
flow,
|
|
2874
|
+
action,
|
|
2875
|
+
...screen === void 0 ? {} : { screen }
|
|
2876
|
+
};
|
|
2877
|
+
};
|
|
2796
2878
|
const single = (element) => {
|
|
2797
2879
|
const tag = tagOf(element);
|
|
2798
2880
|
switch (tag.startsWith(`${local}.`) ? tag.slice(local.length + 1) : "") {
|
|
@@ -2829,6 +2911,7 @@ const buttonList = (input, ctx, local, parent) => {
|
|
|
2829
2911
|
code
|
|
2830
2912
|
};
|
|
2831
2913
|
}
|
|
2914
|
+
case "FlowButton": return flowButton(element);
|
|
2832
2915
|
default:
|
|
2833
2916
|
lowerer.error(ctx, element, "whatsapp-structure", `<WhatsApp.Buttons> holds ${BUTTON_TAGS.map((name) => `<WhatsApp.${name}>`).join(", ")}.`, "Use one of the button components.");
|
|
2834
2917
|
return;
|
|
@@ -2951,6 +3034,35 @@ const mapTree = (tree, leafStyle, counter = { next: 0 }) => {
|
|
|
2951
3034
|
else: otherwise
|
|
2952
3035
|
};
|
|
2953
3036
|
};
|
|
3037
|
+
/** A class tree as the IR value it was read from: a literal, or a choice between literals. */
|
|
3038
|
+
const treeValue = (tree) => "leaf" in tree ? tree.leaf : {
|
|
3039
|
+
if: tree.pred,
|
|
3040
|
+
then: treeValue(tree.then),
|
|
3041
|
+
else: treeValue(tree.else)
|
|
3042
|
+
};
|
|
3043
|
+
const appendClass = (classes, name) => name === void 0 ? classes : classes.trim() === "" ? name : `${classes} ${name}`;
|
|
3044
|
+
/**
|
|
3045
|
+
* An element's class with the dark-mode class each pass gave it. A choice between literal class
|
|
3046
|
+
* lists resolved one list per pass, so each list gains its own pass's class; any other class
|
|
3047
|
+
* value gains the first pass's. `undefined` when no pass gave the element one.
|
|
3048
|
+
*/
|
|
3049
|
+
const withDarkClass = (value, tree, perPass) => {
|
|
3050
|
+
if (perPass.every((name) => name === void 0)) return void 0;
|
|
3051
|
+
if (tree !== void 0 && leavesOf(tree).length > 1) {
|
|
3052
|
+
let leaf = 0;
|
|
3053
|
+
const extend = (node) => "leaf" in node ? { leaf: appendClass(node.leaf, perPass[Math.min(leaf++, perPass.length - 1)]) } : {
|
|
3054
|
+
pred: node.pred,
|
|
3055
|
+
then: extend(node.then),
|
|
3056
|
+
else: extend(node.else)
|
|
3057
|
+
};
|
|
3058
|
+
return treeValue(extend(tree));
|
|
3059
|
+
}
|
|
3060
|
+
const name = perPass[0];
|
|
3061
|
+
if (name === void 0) return value;
|
|
3062
|
+
if (value === void 0) return name;
|
|
3063
|
+
if (typeof value === "string") return appendClass(value, name);
|
|
3064
|
+
return { concat: [value, ` ${name}`] };
|
|
3065
|
+
};
|
|
2954
3066
|
const collect = (nodes, into) => {
|
|
2955
3067
|
for (const node of nodes) {
|
|
2956
3068
|
if (isValueNode(node)) continue;
|
|
@@ -3057,6 +3169,8 @@ const applyStyles = (root, sheet, options) => {
|
|
|
3057
3169
|
for (const node of nodes) if (node.type === "Fragment") read(node.children, into);
|
|
3058
3170
|
else if (node.type === "Raw") into.raws.push(node.html);
|
|
3059
3171
|
else if (node.type === "Element") {
|
|
3172
|
+
const classes = typeof node.props.class === "string" ? node.props.class.split(" ") : [];
|
|
3173
|
+
into.darkClasses.push(classes.find((name) => name.startsWith(DARK_CLASS_PREFIX)));
|
|
3060
3174
|
into.styles.push(node.props.style === void 0 ? void 0 : node.props.style);
|
|
3061
3175
|
into.bgcolors.push(typeof node.props.bgcolor === "string" ? node.props.bgcolor : void 0);
|
|
3062
3176
|
read(node.children, into);
|
|
@@ -3065,6 +3179,7 @@ const applyStyles = (root, sheet, options) => {
|
|
|
3065
3179
|
const results = [];
|
|
3066
3180
|
let headCss = "";
|
|
3067
3181
|
let fontFaceCss = "";
|
|
3182
|
+
const darkRules = /* @__PURE__ */ new Map();
|
|
3068
3183
|
const seen = /* @__PURE__ */ new Set();
|
|
3069
3184
|
for (let pass = 0; pass < passes; pass++) {
|
|
3070
3185
|
const tree = shadow([root], pass, { next: 0 });
|
|
@@ -3073,6 +3188,7 @@ const applyStyles = (root, sheet, options) => {
|
|
|
3073
3188
|
headCss = applied.headCss;
|
|
3074
3189
|
fontFaceCss = applied.fontFaceCss;
|
|
3075
3190
|
}
|
|
3191
|
+
for (const [name, body] of applied.darkRules) darkRules.set(name, body);
|
|
3076
3192
|
for (const diagnostic of applied.diagnostics) {
|
|
3077
3193
|
const key = `${diagnostic.code}\0${diagnostic.message}`;
|
|
3078
3194
|
if (seen.has(key)) continue;
|
|
@@ -3082,6 +3198,7 @@ const applyStyles = (root, sheet, options) => {
|
|
|
3082
3198
|
const result = {
|
|
3083
3199
|
styles: [],
|
|
3084
3200
|
bgcolors: [],
|
|
3201
|
+
darkClasses: [],
|
|
3085
3202
|
raws: []
|
|
3086
3203
|
};
|
|
3087
3204
|
read([applied.tree], result);
|
|
@@ -3114,10 +3231,15 @@ const applyStyles = (root, sheet, options) => {
|
|
|
3114
3231
|
const baseStyle = first?.styles[ordinal];
|
|
3115
3232
|
const style = tree !== void 0 && leavesOf(tree).length > 1 ? mapTree(tree, (leaf) => styleOf(results[Math.min(leaf, results.length - 1)]?.styles[ordinal], node)) : styleOf(baseStyle, node);
|
|
3116
3233
|
const bgcolor = first?.bgcolors[ordinal];
|
|
3117
|
-
const
|
|
3234
|
+
const painted = typeof node.attrs?.bgcolor === "string" && bgcolor !== void 0 && bgcolor !== node.attrs.bgcolor ? {
|
|
3118
3235
|
...node.attrs,
|
|
3119
3236
|
bgcolor: tokens.toValue(bgcolor) ?? bgcolor
|
|
3120
3237
|
} : node.attrs;
|
|
3238
|
+
const darkClass = withDarkClass(node.attrs?.class, tree, results.map((result) => result.darkClasses[ordinal]));
|
|
3239
|
+
const attrs = darkClass === void 0 ? painted : {
|
|
3240
|
+
...painted,
|
|
3241
|
+
class: darkClass
|
|
3242
|
+
};
|
|
3121
3243
|
const kids = rebuild(node.children ?? []);
|
|
3122
3244
|
const { style: _authored, children: _children, attrs: _attrs, ...rest } = node;
|
|
3123
3245
|
return {
|
|
@@ -3152,7 +3274,7 @@ const applyStyles = (root, sheet, options) => {
|
|
|
3152
3274
|
});
|
|
3153
3275
|
return {
|
|
3154
3276
|
root: rebuild([root])[0] ?? root,
|
|
3155
|
-
headCss
|
|
3277
|
+
headCss: `${renderDarkRules(darkRules)}${headCss}`,
|
|
3156
3278
|
fontFaceCss
|
|
3157
3279
|
};
|
|
3158
3280
|
};
|
|
@@ -167,6 +167,14 @@ type IrWhatsAppButton = {
|
|
|
167
167
|
} | {
|
|
168
168
|
readonly type: "copy-code";
|
|
169
169
|
readonly code: IrValue;
|
|
170
|
+
} | {
|
|
171
|
+
readonly type: "flow";
|
|
172
|
+
readonly text: IrValue;
|
|
173
|
+
/** The Samva flow handle, resolved to Meta's flow id when the template is registered. */
|
|
174
|
+
readonly flow: string;
|
|
175
|
+
readonly action: "navigate" | "data_exchange";
|
|
176
|
+
/** Meta's `navigate_screen`; absent opens the flow's first entry screen. */
|
|
177
|
+
readonly screen?: string;
|
|
170
178
|
};
|
|
171
179
|
/** A button, or buttons that appear when a condition over the input holds. */
|
|
172
180
|
type IrWhatsAppButtonNode = IrWhatsAppButton | {
|
|
@@ -243,6 +251,12 @@ type RenderedWhatsAppButton = {
|
|
|
243
251
|
} | {
|
|
244
252
|
readonly type: "copy-code";
|
|
245
253
|
readonly code: string;
|
|
254
|
+
} | {
|
|
255
|
+
readonly type: "flow";
|
|
256
|
+
readonly text: string;
|
|
257
|
+
readonly flow: string;
|
|
258
|
+
readonly action: "navigate" | "data_exchange";
|
|
259
|
+
readonly screen?: string;
|
|
246
260
|
};
|
|
247
261
|
/** A WhatsApp message with every text rendered and every conditional button resolved. */
|
|
248
262
|
interface RenderedIrWhatsApp {
|
|
@@ -441,6 +455,11 @@ interface Stylesheet {
|
|
|
441
455
|
readonly fontFaces: readonly FontFaceRule[];
|
|
442
456
|
/** Unconditional custom properties, for resolving `var()` before inlining. */
|
|
443
457
|
readonly variables: Readonly<Record<string, string>>;
|
|
458
|
+
/**
|
|
459
|
+
* The custom properties the dark-mode query redefines (a theme's dark-mode
|
|
460
|
+
* block). A declaration that reads one switches in dark mode.
|
|
461
|
+
*/
|
|
462
|
+
readonly darkVariables: Readonly<Record<string, string>>;
|
|
444
463
|
readonly diagnostics: readonly EmailDiagnostic[];
|
|
445
464
|
}
|
|
446
465
|
/** Compile a selector into the plain-data matcher the render lane uses. */
|
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import { S as emailElement, o as serializeEmailHtml } from "./html-
|
|
1
|
+
import { S as emailElement, o as serializeEmailHtml } from "./html-BvRFepeO.js";
|
|
2
2
|
import { i as drainEmailDiagnostics, t as EmailNode } from "./jsx-runtime-CBlexoeR.js";
|
|
3
|
-
import { f as preheaderNode } from "./components-
|
|
4
|
-
import {
|
|
3
|
+
import { f as preheaderNode } from "./components-3XHjYD3i.js";
|
|
4
|
+
import { i as renderDarkRules, r as applyStylesheet } from "./ir-DNL6H7jA.js";
|
|
5
5
|
import { r as hasBlockingDiagnostic, t as EmailCompileError } from "./diagnostics-C0Tr8-eQ.js";
|
|
6
6
|
//#region src/email/text.ts
|
|
7
7
|
const PARAGRAPH = "\n\n";
|
|
@@ -219,12 +219,14 @@ const renderEmailContent = (stylesheet, render, options = {}) => {
|
|
|
219
219
|
tree: base,
|
|
220
220
|
headCss: "",
|
|
221
221
|
fontFaceCss: "",
|
|
222
|
-
diagnostics: []
|
|
222
|
+
diagnostics: [],
|
|
223
|
+
darkRules: /* @__PURE__ */ new Map()
|
|
223
224
|
} : applyStylesheet(base, stylesheet);
|
|
225
|
+
const headCss = `${renderDarkRules(styled.darkRules)}${styled.headCss}`;
|
|
224
226
|
const style = [...styled.diagnostics];
|
|
225
227
|
let tree = styled.tree;
|
|
226
|
-
if (
|
|
227
|
-
const placed = withHeadCss(tree,
|
|
228
|
+
if (headCss !== "" || styled.fontFaceCss !== "") {
|
|
229
|
+
const placed = withHeadCss(tree, headCss, styled.fontFaceCss);
|
|
228
230
|
if (placed === void 0) style.push({
|
|
229
231
|
code: "missing-head",
|
|
230
232
|
severity: "error",
|
package/dist/render.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { C as isUrlAttribute, S as emailElement, _ as EmailGlobalProps, b as EmailStyle, g as EmailElementSpec, h as EmailAttributeSpec, m as EMAIL_GLOBAL_ATTRIBUTES, p as EMAIL_ELEMENTS, r as EmailNode, v as EmailIntrinsicElements, x as EmailTag, y as EmailLayout } from "./jsx-runtime-DC7xG0aA.js";
|
|
2
|
-
import { $ as IrWhatsApp, A as emailAuthoringTarget, B as IrElement, D as EmittedPosition, E as EmailElementSelection, F as emailSelections, G as IrPredicate, H as IrFormatter, I as renderIrPreview, J as IrRawPart, K as IrPreheader, L as IrArithmetic, M as emailSelectionAncestry, N as emailSelectionIndex, O as INSTANCE_PATH_ATTRIBUTE, P as emailSelectionSiblings, Q as IrValue, R as IrComparison, U as IrIf, V as IrEmail, W as IrNode, X as IrSource, Y as IrSms, Z as IrStyle, a as renderEmailContent, at as RenderedPosition, c as checkAttribute, ct as TemplateIr, d as escapeText, et as IrWhatsAppButton, f as kebab, i as compileEmail, it as RenderedIrWhatsApp, j as emailDefinitionInstances, k as IrPreview, l as declarationValue, m as serializeStyle, n as EmailCompileOutput, nt as RenderOptions, o as EmailHtmlOutput, ot as RenderedWhatsAppButton, p as serializeEmailHtml, q as IrRaw, r as RenderedEmail, rt as RenderedIrEmail, s as SerializeEmailOptions, st as SML_IR_VERSION, t as EMAIL_DOCTYPE, tt as IrWhatsAppButtonNode, u as escapeAttribute, z as IrEach } from "./render-
|
|
2
|
+
import { $ as IrWhatsApp, A as emailAuthoringTarget, B as IrElement, D as EmittedPosition, E as EmailElementSelection, F as emailSelections, G as IrPredicate, H as IrFormatter, I as renderIrPreview, J as IrRawPart, K as IrPreheader, L as IrArithmetic, M as emailSelectionAncestry, N as emailSelectionIndex, O as INSTANCE_PATH_ATTRIBUTE, P as emailSelectionSiblings, Q as IrValue, R as IrComparison, U as IrIf, V as IrEmail, W as IrNode, X as IrSource, Y as IrSms, Z as IrStyle, a as renderEmailContent, at as RenderedPosition, c as checkAttribute, ct as TemplateIr, d as escapeText, et as IrWhatsAppButton, f as kebab, i as compileEmail, it as RenderedIrWhatsApp, j as emailDefinitionInstances, k as IrPreview, l as declarationValue, m as serializeStyle, n as EmailCompileOutput, nt as RenderOptions, o as EmailHtmlOutput, ot as RenderedWhatsAppButton, p as serializeEmailHtml, q as IrRaw, r as RenderedEmail, rt as RenderedIrEmail, s as SerializeEmailOptions, st as SML_IR_VERSION, t as EMAIL_DOCTYPE, tt as IrWhatsAppButtonNode, u as escapeAttribute, z as IrEach } from "./render-BdvewoQt.js";
|
|
3
3
|
//#region src/render-ir.d.ts
|
|
4
4
|
interface IrRenderDiagnostic {
|
|
5
5
|
readonly code: string;
|
package/dist/render.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { C as isUrlAttribute, S as emailElement, _ as renderIr, a as kebab, b as EMAIL_ELEMENTS, c as INSTANCE_PATH_ATTRIBUTE, d as emailSelectionAncestry, f as emailSelectionIndex, g as IrRenderError, h as renderIrPreview, i as escapeText, l as emailAuthoringTarget, m as emailSelections, n as declarationValue, o as serializeEmailHtml, p as emailSelectionSiblings, r as escapeAttribute, s as serializeStyle, t as checkAttribute, u as emailDefinitionInstances, v as renderIrSms, x as EMAIL_GLOBAL_ATTRIBUTES, y as renderIrWhatsApp } from "./html-
|
|
2
|
-
import { t as SML_IR_VERSION } from "./ir-
|
|
3
|
-
import { i as deriveEmailText, n as compileEmail, r as renderEmailContent, t as EMAIL_DOCTYPE } from "./render-
|
|
1
|
+
import { C as isUrlAttribute, S as emailElement, _ as renderIr, a as kebab, b as EMAIL_ELEMENTS, c as INSTANCE_PATH_ATTRIBUTE, d as emailSelectionAncestry, f as emailSelectionIndex, g as IrRenderError, h as renderIrPreview, i as escapeText, l as emailAuthoringTarget, m as emailSelections, n as declarationValue, o as serializeEmailHtml, p as emailSelectionSiblings, r as escapeAttribute, s as serializeStyle, t as checkAttribute, u as emailDefinitionInstances, v as renderIrSms, x as EMAIL_GLOBAL_ATTRIBUTES, y as renderIrWhatsApp } from "./html-BvRFepeO.js";
|
|
2
|
+
import { t as SML_IR_VERSION } from "./ir-DNL6H7jA.js";
|
|
3
|
+
import { i as deriveEmailText, n as compileEmail, r as renderEmailContent, t as EMAIL_DOCTYPE } from "./render-DpYouYYG.js";
|
|
4
4
|
//#region src/sms-segments.ts
|
|
5
5
|
const GSM7_EXTENDED = /* @__PURE__ */ new Set([
|
|
6
6
|
"[",
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { r as isDarkSchemeQuery } from "./dark-scheme-CbCwGDWH.js";
|
|
1
2
|
import { generate, parse, walk } from "css-tree";
|
|
2
3
|
import { Result } from "better-result";
|
|
3
4
|
//#region src/email/css-color.ts
|
|
@@ -322,14 +323,15 @@ const BRAND_NAMESPACES = [
|
|
|
322
323
|
"--text-",
|
|
323
324
|
"--leading-",
|
|
324
325
|
"--tracking-",
|
|
325
|
-
"--radius
|
|
326
|
+
"--radius",
|
|
326
327
|
"--spacing",
|
|
327
328
|
"--container-",
|
|
328
329
|
"--shadow-"
|
|
329
330
|
];
|
|
331
|
+
/** `--color-` names a prefix; `--radius` is the variable itself or a `--radius-*` prefix. */
|
|
332
|
+
const inNamespace = (property, namespace) => namespace.endsWith("-") ? property.startsWith(namespace) : property === namespace || property.startsWith(`${namespace}-`);
|
|
330
333
|
const BRAND_PROPERTY = /^--[a-z][a-z0-9-]*$/;
|
|
331
334
|
const BRAND_UNSAFE_VALUE = /[{};<>\\@]|url\s*\(|expression\s*\(|\/\*/i;
|
|
332
|
-
const BRAND_DARK_MEDIA = /^\(\s*prefers-color-scheme\s*:\s*dark\s*\)$/i;
|
|
333
335
|
const BRAND_FONT_DESCRIPTORS = /* @__PURE__ */ new Set([
|
|
334
336
|
"font-family",
|
|
335
337
|
"src",
|
|
@@ -338,8 +340,6 @@ const BRAND_FONT_DESCRIPTORS = /* @__PURE__ */ new Set([
|
|
|
338
340
|
"font-display",
|
|
339
341
|
"unicode-range"
|
|
340
342
|
]);
|
|
341
|
-
/** The theme variable a dark-mode color lowers to: `--color-brand` → `--color-brand-dark`. */
|
|
342
|
-
const darkVariableName = (property) => `${property}-dark`;
|
|
343
343
|
const spanOfNode = (node) => ({
|
|
344
344
|
start: node.loc?.start.offset ?? 0,
|
|
345
345
|
end: node.loc?.end.offset ?? 0
|
|
@@ -390,7 +390,7 @@ const parseBrandCss = (css) => {
|
|
|
390
390
|
report("brand-css-property", `${property} is not a theme variable; name theme variables --namespace-name.`, span);
|
|
391
391
|
continue;
|
|
392
392
|
}
|
|
393
|
-
if (!BRAND_NAMESPACES.some((namespace) => property
|
|
393
|
+
if (!BRAND_NAMESPACES.some((namespace) => inNamespace(property, namespace))) {
|
|
394
394
|
report("brand-css-namespace", `${property} is outside the brand namespaces (${BRAND_NAMESPACES.map((namespace) => namespace.endsWith("-") ? `${namespace}*` : namespace).join(", ")}).`, span);
|
|
395
395
|
continue;
|
|
396
396
|
}
|
|
@@ -476,7 +476,7 @@ const parseBrandCss = (css) => {
|
|
|
476
476
|
readFontFace(node);
|
|
477
477
|
continue;
|
|
478
478
|
}
|
|
479
|
-
if (name === "media" && node.prelude !== null &&
|
|
479
|
+
if (name === "media" && node.prelude !== null && isDarkSchemeQuery(generate(node.prelude))) {
|
|
480
480
|
for (const child of node.block.children) if (child.type === "Atrule" && child.name.toLowerCase() === "theme") readTheme(child, dark, "dark");
|
|
481
481
|
else report("brand-css-unsupported", "The dark-mode block may contain only @theme.", spanOfNode(child));
|
|
482
482
|
continue;
|
|
@@ -484,10 +484,6 @@ const parseBrandCss = (css) => {
|
|
|
484
484
|
}
|
|
485
485
|
report("brand-css-unsupported", "A brand theme contains only @theme, a dark-mode @theme (@media (prefers-color-scheme: dark)), and @font-face.", spanOfNode(node));
|
|
486
486
|
}
|
|
487
|
-
for (const property of Object.keys(dark)) if (darkVariableName(property) in variables) report("brand-css-dark-conflict", `${darkVariableName(property)} and the dark-mode ${property} name the same theme variable; keep one.`, {
|
|
488
|
-
start: 0,
|
|
489
|
-
end: 0
|
|
490
|
-
});
|
|
491
487
|
return diagnostics.length > 0 ? Result.err(diagnostics) : Result.ok({
|
|
492
488
|
variables,
|
|
493
489
|
dark,
|
|
@@ -518,9 +514,92 @@ const printBrandCss = (brand) => {
|
|
|
518
514
|
];
|
|
519
515
|
return blocks.length === 0 ? "" : `${blocks.join("\n\n")}\n`;
|
|
520
516
|
};
|
|
521
|
-
/**
|
|
522
|
-
|
|
523
|
-
|
|
517
|
+
/**
|
|
518
|
+
* The semantic color tokens, as Tailwind color names (`bg-card`,
|
|
519
|
+
* `text-primary-foreground`, `border-border`), each with the role a block uses
|
|
520
|
+
* it for. A block reads only these, so swapping the theme reskins it.
|
|
521
|
+
*/
|
|
522
|
+
const THEME_COLOR_ROLES = {
|
|
523
|
+
background: "The page, behind everything.",
|
|
524
|
+
foreground: "Text on the page.",
|
|
525
|
+
card: "A surface that groups the message's content.",
|
|
526
|
+
"card-foreground": "Text on a card.",
|
|
527
|
+
primary: "The main action's fill (a button), and links.",
|
|
528
|
+
"primary-foreground": "Text on a primary fill.",
|
|
529
|
+
secondary: "A quieter action's fill.",
|
|
530
|
+
"secondary-foreground": "Text on a secondary fill.",
|
|
531
|
+
muted: "A subdued surface: a code block, a table header.",
|
|
532
|
+
"muted-foreground": "Secondary text on the page, a card or a muted surface: labels, captions, the footer.",
|
|
533
|
+
accent: "A highlighted surface: a callout, a selected row.",
|
|
534
|
+
"accent-foreground": "Text on an accent surface.",
|
|
535
|
+
border: "Rules, dividers and outlines."
|
|
536
|
+
};
|
|
537
|
+
const THEME_COLORS = Object.keys(THEME_COLOR_ROLES);
|
|
538
|
+
/**
|
|
539
|
+
* The non-color tokens: the corner radius (`rounded`; `rounded-sm` to `rounded-xl` follow it) and
|
|
540
|
+
* the body and heading families (`font-sans`, `font-heading`).
|
|
541
|
+
*/
|
|
542
|
+
const THEME_TOKENS = [
|
|
543
|
+
"--radius",
|
|
544
|
+
"--font-sans",
|
|
545
|
+
"--font-heading"
|
|
546
|
+
];
|
|
547
|
+
const colorVariables = (colors) => Object.fromEntries(THEME_COLORS.map((name) => [`--color-${name}`, colors[name]]));
|
|
548
|
+
/**
|
|
549
|
+
* The theme every project compiles with, beneath its own: a neutral light and
|
|
550
|
+
* dark palette, an 8px radius and the system sans-serif stack. A project's
|
|
551
|
+
* `theme.css`, its imported stylesheets and its brand layer over it.
|
|
552
|
+
*/
|
|
553
|
+
const DEFAULT_THEME = {
|
|
554
|
+
variables: {
|
|
555
|
+
...colorVariables({
|
|
556
|
+
background: "#ffffff",
|
|
557
|
+
foreground: "#0a0a0a",
|
|
558
|
+
card: "#ffffff",
|
|
559
|
+
"card-foreground": "#0a0a0a",
|
|
560
|
+
primary: "#171717",
|
|
561
|
+
"primary-foreground": "#fafafa",
|
|
562
|
+
secondary: "#f5f5f5",
|
|
563
|
+
"secondary-foreground": "#171717",
|
|
564
|
+
muted: "#f5f5f5",
|
|
565
|
+
"muted-foreground": "#737373",
|
|
566
|
+
accent: "#f5f5f5",
|
|
567
|
+
"accent-foreground": "#171717",
|
|
568
|
+
border: "#e5e5e5"
|
|
569
|
+
}),
|
|
570
|
+
"--radius": "8px",
|
|
571
|
+
"--radius-sm": "calc(var(--radius) - 4px)",
|
|
572
|
+
"--radius-md": "calc(var(--radius) - 2px)",
|
|
573
|
+
"--radius-lg": "var(--radius)",
|
|
574
|
+
"--radius-xl": "calc(var(--radius) + 4px)",
|
|
575
|
+
"--font-sans": "-apple-system, BlinkMacSystemFont, \"Segoe UI\", Roboto, Helvetica, Arial, sans-serif",
|
|
576
|
+
"--font-heading": "var(--font-sans)"
|
|
577
|
+
},
|
|
578
|
+
dark: colorVariables({
|
|
579
|
+
background: "#0a0a0a",
|
|
580
|
+
foreground: "#fafafa",
|
|
581
|
+
card: "#171717",
|
|
582
|
+
"card-foreground": "#fafafa",
|
|
583
|
+
primary: "#fafafa",
|
|
584
|
+
"primary-foreground": "#171717",
|
|
585
|
+
secondary: "#262626",
|
|
586
|
+
"secondary-foreground": "#fafafa",
|
|
587
|
+
muted: "#262626",
|
|
588
|
+
"muted-foreground": "#a3a3a3",
|
|
589
|
+
accent: "#262626",
|
|
590
|
+
"accent-foreground": "#fafafa",
|
|
591
|
+
border: "#2e2e2e"
|
|
592
|
+
}),
|
|
593
|
+
fontFaces: []
|
|
594
|
+
};
|
|
595
|
+
/** `DEFAULT_THEME` as theme CSS. */
|
|
596
|
+
const DEFAULT_THEME_CSS = printBrandCss(DEFAULT_THEME);
|
|
597
|
+
/** The fills whose text color a brand may leave out: `--color-primary` and `--color-primary-foreground`, and so on. */
|
|
598
|
+
const FILL_COLORS = [
|
|
599
|
+
"primary",
|
|
600
|
+
"secondary",
|
|
601
|
+
"accent"
|
|
602
|
+
];
|
|
524
603
|
/** WCAG relative luminance of an sRGB channel triple in 0–1. */
|
|
525
604
|
const relativeLuminance = (r, g, b) => {
|
|
526
605
|
const linear = (channel) => {
|
|
@@ -545,41 +624,24 @@ const contrastForeground = (color) => {
|
|
|
545
624
|
return 1.05 / (luminance + .05) > (luminance + .05) / .05 ? "#ffffff" : "#000000";
|
|
546
625
|
};
|
|
547
626
|
/**
|
|
548
|
-
* The text
|
|
549
|
-
*
|
|
550
|
-
*
|
|
551
|
-
* the foreground, or whose
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
const light = brand.variables["--color-brand-foreground"] === void 0 && brand.variables["--color-brand"] !== void 0 ? contrastForeground(brand.variables[BRAND_COLOR_VARIABLE]) : void 0;
|
|
555
|
-
const darkBrand = brand.dark[BRAND_COLOR_VARIABLE];
|
|
556
|
-
return {
|
|
557
|
-
light,
|
|
558
|
-
dark: darkBrand !== void 0 && brand.dark["--color-brand-foreground"] === void 0 && brand.variables[darkVariableName("--color-brand-foreground")] === void 0 ? contrastForeground(darkBrand) : void 0
|
|
559
|
-
};
|
|
560
|
-
};
|
|
561
|
-
/**
|
|
562
|
-
* A brand theme as flat theme variables, the way a build compiles it. Email
|
|
563
|
-
* inlining resolves `var()` once, so a dark-mode `@theme` cannot switch a
|
|
564
|
-
* value at run time; each dark color lowers to its `-dark` theme variable,
|
|
565
|
-
* which `dark:` utilities read (`dark:bg-brand-dark`). A text-on-brand color
|
|
566
|
-
* the brand leaves out is filled in by `brandForegroundDefaults`, so every
|
|
567
|
-
* build compiles the same default and the stored CSS never carries it.
|
|
627
|
+
* The text colors a theme layer gets for the fills it sets without one: for
|
|
628
|
+
* each of `FILL_COLORS`, the `contrastForeground` of the fill, in light mode
|
|
629
|
+
* and, where the layer's dark-mode block sets the fill, in dark mode. A layer
|
|
630
|
+
* that sets the foreground, or whose fill does not evaluate, gets no default.
|
|
631
|
+
* Every build lowers each theme layer with these, so stored theme CSS never
|
|
632
|
+
* carries them.
|
|
568
633
|
*/
|
|
569
|
-
const
|
|
570
|
-
const defaults =
|
|
634
|
+
const foregroundDefaults = (light, dark) => {
|
|
635
|
+
const defaults = (colors) => Object.fromEntries(FILL_COLORS.flatMap((fill) => {
|
|
636
|
+
const color = colors[`--color-${fill}`];
|
|
637
|
+
const foreground = `--color-${fill}-foreground`;
|
|
638
|
+
const picked = color === void 0 || colors[foreground] !== void 0 ? void 0 : contrastForeground(color);
|
|
639
|
+
return picked === void 0 ? [] : [[foreground, picked]];
|
|
640
|
+
}));
|
|
571
641
|
return {
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
...Object.fromEntries(Object.entries(brand.dark).map(([property, value]) => [darkVariableName(property), value])),
|
|
575
|
-
...defaults.dark === void 0 ? {} : { [darkVariableName(BRAND_FOREGROUND_VARIABLE)]: defaults.dark }
|
|
642
|
+
light: defaults(light),
|
|
643
|
+
dark: defaults(dark)
|
|
576
644
|
};
|
|
577
645
|
};
|
|
578
|
-
/** The stylesheet Tailwind compiles for the theme's brand import: `brandThemeVariables` and the font faces. */
|
|
579
|
-
const brandTailwindCss = (brand) => printBrandCss({
|
|
580
|
-
fontFaces: brand.fontFaces,
|
|
581
|
-
variables: brandThemeVariables(brand),
|
|
582
|
-
dark: {}
|
|
583
|
-
});
|
|
584
646
|
//#endregion
|
|
585
|
-
export {
|
|
647
|
+
export { THEME_COLOR_ROLES as a, foregroundDefaults as c, downlevelColors as d, formatColor as f, THEME_COLORS as i, parseBrandCss as l, DEFAULT_THEME_CSS as n, THEME_TOKENS as o, parseColor as p, FILL_COLORS as r, contrastForeground as s, DEFAULT_THEME as t, printBrandCss as u };
|