@samva/markup 0.7.0 → 0.9.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.
Files changed (176) hide show
  1. package/AGENTS.md +104 -37
  2. package/README.md +188 -52
  3. package/dist/assets-C5duwTya.d.ts +40 -0
  4. package/dist/brand-B-0bN_Ir.d.ts +88 -0
  5. package/dist/{render-aaJBnsXK.js → cascade-t6kxlgEI.js} +130 -160
  6. package/dist/channel-edit.d.ts +121 -0
  7. package/dist/channel-edit.js +609 -0
  8. package/dist/channel-specs-B6M_HlaV.js +29 -0
  9. package/dist/channel-specs-C2feJwuP.d.ts +13 -0
  10. package/dist/compatibility-DbISoFIq.js +589 -0
  11. package/dist/compiler.d.ts +19 -249
  12. package/dist/compiler.js +10 -1408
  13. package/dist/{css-COcFOT1y.d.ts → css-BVQKICKC.d.ts} +30 -4
  14. package/dist/diagnostic-codes.d.ts +403 -0
  15. package/dist/diagnostic-codes.js +399 -0
  16. package/dist/{diagnostics-BMYtBCQk.d.ts → diagnostics-B-Gym34R.d.ts} +8 -4
  17. package/dist/diagnostics.d.ts +31 -2
  18. package/dist/diagnostics.js +1 -19
  19. package/dist/{elements-BzjjnoCS.d.ts → elements-cqldpaJt.d.ts} +3 -3
  20. package/dist/email/assets.d.ts +2 -0
  21. package/dist/email/assets.js +63 -0
  22. package/dist/email/brand-footer.d.ts +34 -0
  23. package/dist/email/brand-footer.js +63 -0
  24. package/dist/email/brand.d.ts +2 -0
  25. package/dist/email/brand.js +148 -0
  26. package/dist/email/components.d.ts +21 -10
  27. package/dist/email/components.js +46 -11
  28. package/dist/email/diagnostics.d.ts +2 -2
  29. package/dist/email/diagnostics.js +2 -1
  30. package/dist/email/elements.d.ts +1 -1
  31. package/dist/email/html.d.ts +2 -42
  32. package/dist/email/html.js +56 -2
  33. package/dist/email/jsx-dev-runtime.d.ts +2 -2
  34. package/dist/email/jsx-runtime.d.ts +1 -1
  35. package/dist/email/render.d.ts +10 -13
  36. package/dist/email/render.js +163 -6
  37. package/dist/email/text.d.ts +1 -1
  38. package/dist/fmt.d.ts +32 -0
  39. package/dist/fmt.js +174 -0
  40. package/dist/html-BJepOwZ4.d.ts +56 -0
  41. package/dist/index.d.ts +2 -0
  42. package/dist/index.js +2 -0
  43. package/dist/ir-aaqNE987.d.ts +252 -0
  44. package/dist/ir.d.ts +2 -0
  45. package/dist/ir.js +7 -0
  46. package/dist/preview-Ekdg1dXG.d.ts +100 -0
  47. package/dist/preview.d.ts +2 -0
  48. package/dist/preview.js +160 -0
  49. package/dist/profile-BH-OtdDA.js +3670 -0
  50. package/dist/render-ir.d.ts +19 -0
  51. package/dist/render-ir.js +826 -0
  52. package/dist/sms-segments.d.ts +13 -2
  53. package/dist/sms.d.ts +16 -0
  54. package/dist/sms.js +12 -0
  55. package/dist/{email/source-edit.d.ts → source-edit.d.ts} +55 -6
  56. package/dist/source-edit.js +343 -0
  57. package/dist/source-locations-C8wCsiAy.d.ts +23 -0
  58. package/dist/source-locations.d.ts +2 -2
  59. package/dist/source-locations.js +3 -89
  60. package/dist/static.d.ts +57 -0
  61. package/dist/static.js +69 -0
  62. package/dist/tailwind-CMYpZf1v.js +614 -0
  63. package/dist/template-_QE_rQND.d.ts +65 -0
  64. package/dist/template.d.ts +2 -2
  65. package/dist/template.js +15 -10
  66. package/dist/theme-css-hFqcPSar.js +587 -0
  67. package/dist/theme-css.d.ts +57 -16
  68. package/dist/theme-css.js +2 -509
  69. package/dist/theme-imports-lvBw3AD1.js +109 -0
  70. package/dist/transform-Ct2OMkFN.d.ts +153 -0
  71. package/dist/unsubscribe-link-CoesnQDU.js +26 -0
  72. package/dist/whatsapp.d.ts +57 -0
  73. package/dist/whatsapp.js +22 -0
  74. package/generated/contract.json +1782 -0
  75. package/generated/reference.md +353 -0
  76. package/package.json +56 -154
  77. package/samva-brand.d.ts +14 -0
  78. package/skills/sml/SKILL.md +123 -0
  79. package/dist/ast-BrDtApwS.d.ts +0 -107
  80. package/dist/ast.d.ts +0 -2
  81. package/dist/ast.js +0 -124
  82. package/dist/candidate.d.ts +0 -29
  83. package/dist/candidate.js +0 -88
  84. package/dist/catalog-60eaYC7b.d.ts +0 -356
  85. package/dist/checks-DBweSIPN.js +0 -58954
  86. package/dist/checks.d.ts +0 -33
  87. package/dist/checks.js +0 -2
  88. package/dist/classes.d.ts +0 -27
  89. package/dist/classes.js +0 -184
  90. package/dist/compile-sms.d.ts +0 -28
  91. package/dist/compile-sms.js +0 -44
  92. package/dist/compile-whatsapp.d.ts +0 -56
  93. package/dist/compile-whatsapp.js +0 -186
  94. package/dist/compile.d.ts +0 -18
  95. package/dist/compile.js +0 -29
  96. package/dist/components.d.ts +0 -289
  97. package/dist/components.js +0 -106
  98. package/dist/conditionals.d.ts +0 -10
  99. package/dist/conditionals.js +0 -74
  100. package/dist/corpus-BjYMqOoY.d.ts +0 -106
  101. package/dist/diagnostics-OcQKQYSY.d.ts +0 -49
  102. package/dist/email/selection.d.ts +0 -91
  103. package/dist/email/selection.js +0 -91
  104. package/dist/email/source-edit.js +0 -185
  105. package/dist/email/stylesheets.d.ts +0 -15
  106. package/dist/email/stylesheets.js +0 -45
  107. package/dist/emit.d.ts +0 -51
  108. package/dist/emit.js +0 -151
  109. package/dist/footer.d.ts +0 -66
  110. package/dist/footer.js +0 -93
  111. package/dist/interpolate.d.ts +0 -24
  112. package/dist/interpolate.js +0 -43
  113. package/dist/jsx-dev-runtime.d.ts +0 -11
  114. package/dist/jsx-dev-runtime.js +0 -15
  115. package/dist/jsx-runtime-I-1nCtl2.d.ts +0 -57
  116. package/dist/jsx-runtime.d.ts +0 -2
  117. package/dist/jsx-runtime.js +0 -121
  118. package/dist/lexer.d.ts +0 -60
  119. package/dist/lexer.js +0 -281
  120. package/dist/model.d.ts +0 -24
  121. package/dist/model.js +0 -77
  122. package/dist/ops-2aiiYO7m.js +0 -630
  123. package/dist/ops.d.ts +0 -114
  124. package/dist/ops.js +0 -2
  125. package/dist/parser-t29cHLVL.d.ts +0 -396
  126. package/dist/parser.d.ts +0 -2
  127. package/dist/parser.js +0 -859
  128. package/dist/predicate-BI6uRRM-.js +0 -549
  129. package/dist/predicate-M-05Nd7R.d.ts +0 -56
  130. package/dist/predicate.d.ts +0 -2
  131. package/dist/predicate.js +0 -2
  132. package/dist/presets/category-mapping.d.ts +0 -13
  133. package/dist/presets/category-mapping.js +0 -49
  134. package/dist/presets/corpus.d.ts +0 -2
  135. package/dist/presets/corpus.js +0 -194
  136. package/dist/presets/public-gallery.d.ts +0 -266
  137. package/dist/presets/public-gallery.js +0 -916
  138. package/dist/presets/public-handoff.d.ts +0 -164
  139. package/dist/presets/public-handoff.js +0 -361
  140. package/dist/presets/public-preview-artifacts.d.ts +0 -43
  141. package/dist/presets/public-preview-artifacts.js +0 -104
  142. package/dist/presets/public-preview.d.ts +0 -61
  143. package/dist/presets/public-preview.js +0 -83
  144. package/dist/presets/showcase.d.ts +0 -2
  145. package/dist/presets/showcase.js +0 -627
  146. package/dist/presets/sources.d.ts +0 -38
  147. package/dist/presets/sources.js +0 -6083
  148. package/dist/presets.d.ts +0 -2
  149. package/dist/presets.js +0 -881
  150. package/dist/printer.d.ts +0 -6
  151. package/dist/printer.js +0 -122
  152. package/dist/registry.d.ts +0 -42
  153. package/dist/registry.js +0 -646
  154. package/dist/render-template.d.ts +0 -45
  155. package/dist/render-template.js +0 -76
  156. package/dist/showcase-DGZAWn4Z.d.ts +0 -456
  157. package/dist/sms-segments-y4FpPilJ.d.ts +0 -13
  158. package/dist/source-locations-DrCM82Ym.d.ts +0 -67
  159. package/dist/template-DWftqh4x.d.ts +0 -60
  160. package/dist/theme-6ApjjiBo.d.ts +0 -116
  161. package/dist/theme.d.ts +0 -2
  162. package/dist/theme.js +0 -118
  163. package/dist/tokens.d.ts +0 -6
  164. package/dist/tokens.js +0 -417
  165. package/dist/tsx-source-instrumentation.d.ts +0 -30
  166. package/dist/tsx-source-instrumentation.js +0 -115
  167. package/dist/tsx-tracer.d.ts +0 -112
  168. package/dist/tsx-tracer.js +0 -985
  169. package/dist/variables-J_L3qxRa.d.ts +0 -77
  170. package/dist/variables.d.ts +0 -2
  171. package/dist/variables.js +0 -141
  172. package/dist/version-BITr58kk.js +0 -96
  173. package/generated/sml-agent-contract.json +0 -709
  174. package/generated/sml-email-agent-contract.json +0 -615
  175. package/generated/sml-email-language-reference.md +0 -94
  176. package/generated/sml-language-reference.md +0 -195
@@ -0,0 +1,587 @@
1
+ import { Result } from "better-result";
2
+ import { generate, parse, walk } from "css-tree";
3
+ //#region src/email/css-color.ts
4
+ const clamp = (value, low = 0, high = 1) => value < low ? low : value > high ? high : value;
5
+ const toGamma = (channel) => channel <= .0031308 ? 12.92 * channel : 1.055 * channel ** (1 / 2.4) - .055;
6
+ const toLinear = (channel) => channel <= .04045 ? channel / 12.92 : ((channel + .055) / 1.055) ** 2.4;
7
+ /** OKLab is the interpolation space Tailwind mixes in, so conversions run both ways. */
8
+ const oklabToRgb = (lightness, a, b, alpha) => {
9
+ const l = (lightness + .3963377774 * a + .2158037573 * b) ** 3;
10
+ const m = (lightness - .1055613458 * a - .0638541728 * b) ** 3;
11
+ const s = (lightness - .0894841775 * a - 1.291485548 * b) ** 3;
12
+ return {
13
+ r: toGamma(4.0767416621 * l - 3.3077115913 * m + .2309699292 * s),
14
+ g: toGamma(-1.2684380046 * l + 2.6097574011 * m - .3413193965 * s),
15
+ b: toGamma(-.0041960863 * l - .7034186147 * m + 1.707614701 * s),
16
+ a: alpha
17
+ };
18
+ };
19
+ const rgbToOklab = (color) => {
20
+ const r = toLinear(color.r);
21
+ const g = toLinear(color.g);
22
+ const b = toLinear(color.b);
23
+ const l = Math.cbrt(.4122214708 * r + .5363325363 * g + .0514459929 * b);
24
+ const m = Math.cbrt(.2119034982 * r + .6806995451 * g + .1073969566 * b);
25
+ const s = Math.cbrt(.0883024619 * r + .2817188376 * g + .6299787005 * b);
26
+ return {
27
+ L: .2104542553 * l + .793617785 * m - .0040720468 * s,
28
+ a: 1.9779984951 * l - 2.428592205 * m + .4505937099 * s,
29
+ b: .0259040371 * l + .7827717662 * m - .808675766 * s,
30
+ alpha: color.a
31
+ };
32
+ };
33
+ const hslToRgb = (hue, saturation, lightness, alpha) => {
34
+ const h = (hue % 360 + 360) % 360;
35
+ const c = (1 - Math.abs(2 * lightness - 1)) * saturation;
36
+ const x = c * (1 - Math.abs(h / 60 % 2 - 1));
37
+ const m = lightness - c / 2;
38
+ const [r, g, b] = [
39
+ [
40
+ c,
41
+ x,
42
+ 0
43
+ ],
44
+ [
45
+ x,
46
+ c,
47
+ 0
48
+ ],
49
+ [
50
+ 0,
51
+ c,
52
+ x
53
+ ],
54
+ [
55
+ 0,
56
+ x,
57
+ c
58
+ ],
59
+ [
60
+ x,
61
+ 0,
62
+ c
63
+ ],
64
+ [
65
+ c,
66
+ 0,
67
+ x
68
+ ]
69
+ ][Math.min(5, Math.floor(h / 60))];
70
+ return {
71
+ r: r + m,
72
+ g: g + m,
73
+ b: b + m,
74
+ a: alpha
75
+ };
76
+ };
77
+ /** The named colors an email template actually reaches for; anything else stays literal. */
78
+ const NAMED = {
79
+ transparent: "#00000000",
80
+ black: "#000000",
81
+ white: "#ffffff",
82
+ red: "#ff0000",
83
+ green: "#008000",
84
+ blue: "#0000ff",
85
+ gray: "#808080",
86
+ grey: "#808080",
87
+ silver: "#c0c0c0",
88
+ yellow: "#ffff00",
89
+ orange: "#ffa500",
90
+ purple: "#800080",
91
+ navy: "#000080",
92
+ teal: "#008080",
93
+ olive: "#808000",
94
+ maroon: "#800000",
95
+ lime: "#00ff00",
96
+ aqua: "#00ffff",
97
+ cyan: "#00ffff",
98
+ fuchsia: "#ff00ff",
99
+ magenta: "#ff00ff"
100
+ };
101
+ const number = (token, reference = 1) => {
102
+ const text = token.trim();
103
+ if (text === "none") return 0;
104
+ if (text.endsWith("%")) {
105
+ const value = Number.parseFloat(text.slice(0, -1));
106
+ return Number.isFinite(value) ? value / 100 * reference : void 0;
107
+ }
108
+ const value = Number.parseFloat(text);
109
+ return Number.isFinite(value) ? value : void 0;
110
+ };
111
+ const angle = (token) => {
112
+ const text = token.trim();
113
+ const value = Number.parseFloat(text);
114
+ if (!Number.isFinite(value)) return void 0;
115
+ if (text.endsWith("turn")) return value * 360;
116
+ if (text.endsWith("rad")) return value * 180 / Math.PI;
117
+ if (text.endsWith("grad")) return value * .9;
118
+ return value;
119
+ };
120
+ /** Split a function's arguments on top-level commas and `/`, keeping nested calls intact. */
121
+ const argumentTokens = (body) => {
122
+ const parts = [];
123
+ let alpha;
124
+ let depth = 0;
125
+ let current = "";
126
+ let afterSlash = false;
127
+ for (const character of body) {
128
+ if (character === "(") depth += 1;
129
+ if (character === ")") depth -= 1;
130
+ if (depth === 0 && (character === "," || character === " " || character === "/")) {
131
+ if (character === "/") {
132
+ if (current.trim() !== "") parts.push(current.trim());
133
+ current = "";
134
+ afterSlash = true;
135
+ continue;
136
+ }
137
+ if (current.trim() !== "") {
138
+ if (afterSlash) alpha = current.trim();
139
+ else parts.push(current.trim());
140
+ }
141
+ current = "";
142
+ continue;
143
+ }
144
+ current += character;
145
+ }
146
+ if (current.trim() !== "") {
147
+ if (afterSlash) alpha = current.trim();
148
+ else parts.push(current.trim());
149
+ }
150
+ return alpha === void 0 ? { parts } : {
151
+ parts,
152
+ alpha
153
+ };
154
+ };
155
+ /** Read one function call at the start of `value`, returning its name and body. */
156
+ const functionCall = (value) => {
157
+ const match = /^([a-zA-Z-]+)\(/.exec(value.trim());
158
+ if (match === null) return void 0;
159
+ const text = value.trim();
160
+ let depth = 0;
161
+ for (let index = match[0].length - 1; index < text.length; index++) if (text[index] === "(") depth += 1;
162
+ else if (text[index] === ")") {
163
+ depth -= 1;
164
+ if (depth === 0) return {
165
+ name: match[1].toLowerCase(),
166
+ body: text.slice(match[0].length, index)
167
+ };
168
+ }
169
+ };
170
+ /** Parse any supported color notation into extended sRGB. `undefined` means "leave it alone". */
171
+ const parseColor = (value) => {
172
+ const text = value.trim();
173
+ const named = NAMED[text.toLowerCase()];
174
+ if (named !== void 0) return parseColor(named);
175
+ if (text.startsWith("#")) {
176
+ const hex = text.slice(1);
177
+ const expand = (part) => Number.parseInt(part.repeat(2 / part.length), 16) / 255;
178
+ if (hex.length === 3 || hex.length === 4) return {
179
+ r: expand(hex[0]),
180
+ g: expand(hex[1]),
181
+ b: expand(hex[2]),
182
+ a: hex.length === 4 ? expand(hex[3]) : 1
183
+ };
184
+ if (hex.length === 6 || hex.length === 8) return {
185
+ r: Number.parseInt(hex.slice(0, 2), 16) / 255,
186
+ g: Number.parseInt(hex.slice(2, 4), 16) / 255,
187
+ b: Number.parseInt(hex.slice(4, 6), 16) / 255,
188
+ a: hex.length === 8 ? Number.parseInt(hex.slice(6, 8), 16) / 255 : 1
189
+ };
190
+ return;
191
+ }
192
+ const call = functionCall(text);
193
+ if (call === void 0) return void 0;
194
+ const { parts, alpha } = argumentTokens(call.body);
195
+ const opacity = alpha === void 0 ? 1 : number(alpha) ?? 1;
196
+ switch (call.name) {
197
+ case "rgb":
198
+ case "rgba": {
199
+ const [r, g, b, legacy] = parts;
200
+ if (r === void 0 || g === void 0 || b === void 0) return void 0;
201
+ const channel = (token) => {
202
+ const parsed = number(token, 255);
203
+ return parsed === void 0 ? void 0 : parsed / 255;
204
+ };
205
+ const [red, green, blue] = [
206
+ channel(r),
207
+ channel(g),
208
+ channel(b)
209
+ ];
210
+ if (red === void 0 || green === void 0 || blue === void 0) return void 0;
211
+ return {
212
+ r: red,
213
+ g: green,
214
+ b: blue,
215
+ a: legacy === void 0 ? opacity : number(legacy) ?? 1
216
+ };
217
+ }
218
+ case "hsl":
219
+ case "hsla": {
220
+ const [h, s, l, legacy] = parts;
221
+ if (h === void 0 || s === void 0 || l === void 0) return void 0;
222
+ const hue = angle(h);
223
+ const saturation = number(s);
224
+ const lightness = number(l);
225
+ if (hue === void 0 || saturation === void 0 || lightness === void 0) return void 0;
226
+ return hslToRgb(hue, saturation, lightness, legacy === void 0 ? opacity : number(legacy) ?? 1);
227
+ }
228
+ case "oklch": {
229
+ const [l, c, h] = parts;
230
+ if (l === void 0 || c === void 0 || h === void 0) return void 0;
231
+ const lightness = number(l);
232
+ const chroma = number(c, .4);
233
+ const hue = angle(h);
234
+ if (lightness === void 0 || chroma === void 0 || hue === void 0) return void 0;
235
+ const radians = hue * Math.PI / 180;
236
+ return oklabToRgb(lightness, chroma * Math.cos(radians), chroma * Math.sin(radians), opacity);
237
+ }
238
+ case "oklab": {
239
+ const [l, a, b] = parts;
240
+ if (l === void 0 || a === void 0 || b === void 0) return void 0;
241
+ const lightness = number(l);
242
+ const green = number(a, .4);
243
+ const blue = number(b, .4);
244
+ if (lightness === void 0 || green === void 0 || blue === void 0) return void 0;
245
+ return oklabToRgb(lightness, green, blue, opacity);
246
+ }
247
+ case "color-mix": return mixColors(call.body);
248
+ default: return;
249
+ }
250
+ };
251
+ /** `color-mix(in <space>, <color> <pct>?, <color> <pct>?)` — Tailwind's opacity modifier. */
252
+ const mixColors = (body) => {
253
+ const stops = body.split(",").map((part) => part.trim());
254
+ const [space, first, second] = stops;
255
+ if (space === void 0 || first === void 0 || second === void 0 || stops.length !== 3) return void 0;
256
+ const method = /^in\s+([a-z-]+)/i.exec(space)?.[1]?.toLowerCase();
257
+ if (method === void 0) return void 0;
258
+ const stop = (text) => {
259
+ const percentage = /\s([\d.]+)%$/.exec(text);
260
+ const color = parseColor(percentage === null ? text : text.slice(0, percentage.index));
261
+ if (color === void 0) return void 0;
262
+ return {
263
+ color,
264
+ weight: percentage === null ? void 0 : Number.parseFloat(percentage[1]) / 100
265
+ };
266
+ };
267
+ const left = stop(first);
268
+ const right = stop(second);
269
+ if (left === void 0 || right === void 0) return void 0;
270
+ const leftWeight = left.weight ?? (right.weight === void 0 ? .5 : 1 - right.weight);
271
+ const total = leftWeight + (right.weight ?? 1 - leftWeight);
272
+ if (total === 0) return void 0;
273
+ const p = leftWeight / total;
274
+ const alpha = left.color.a * p + right.color.a * (1 - p);
275
+ const mix = (l, r) => alpha === 0 ? 0 : (l * left.color.a * p + r * right.color.a * (1 - p)) / alpha;
276
+ if (method === "srgb") return {
277
+ r: mix(left.color.r, right.color.r),
278
+ g: mix(left.color.g, right.color.g),
279
+ b: mix(left.color.b, right.color.b),
280
+ a: alpha
281
+ };
282
+ const l = rgbToOklab(left.color);
283
+ const r = rgbToOklab(right.color);
284
+ const channel = (a, b) => alpha === 0 ? 0 : (a * left.color.a * p + b * right.color.a * (1 - p)) / alpha;
285
+ return oklabToRgb(channel(l.L, r.L), channel(l.a, r.a), channel(l.b, r.b), alpha);
286
+ };
287
+ const byte = (channel) => Math.round(clamp(channel) * 255).toString(16).padStart(2, "0");
288
+ /** sRGB hex, or `rgba()` when the color is not opaque. */
289
+ const formatColor = (color) => color.a >= 1 ? `#${byte(color.r)}${byte(color.g)}${byte(color.b)}` : `rgba(${Math.round(clamp(color.r) * 255)},${Math.round(clamp(color.g) * 255)},${Math.round(clamp(color.b) * 255)},${Math.round(clamp(color.a) * 1e3) / 1e3})`;
290
+ const MODERN_COLOR = /\b(oklch|oklab|color-mix|lab|lch)\(/i;
291
+ /** Rewrite every modern color function in a declaration value; other text is untouched. */
292
+ const downlevelColors = (value) => {
293
+ if (!MODERN_COLOR.test(value)) return value;
294
+ let result = "";
295
+ let rest = value;
296
+ while (rest.length > 0) {
297
+ const match = MODERN_COLOR.exec(rest);
298
+ if (match === null || match.index === void 0) {
299
+ result += rest;
300
+ break;
301
+ }
302
+ result += rest.slice(0, match.index);
303
+ const remainder = rest.slice(match.index);
304
+ const call = functionCall(remainder);
305
+ if (call === void 0) {
306
+ result += remainder;
307
+ break;
308
+ }
309
+ const consumed = remainder.indexOf("(") + call.body.length + 2;
310
+ const color = parseColor(remainder.slice(0, consumed));
311
+ result += color === void 0 ? remainder.slice(0, consumed) : formatColor(color);
312
+ rest = remainder.slice(consumed);
313
+ }
314
+ return result;
315
+ };
316
+ //#endregion
317
+ //#region src/theme-css.ts
318
+ /** Theme-variable namespaces a brand may set: Tailwind's, minus anything that is not a look. */
319
+ const BRAND_NAMESPACES = [
320
+ "--color-",
321
+ "--font-",
322
+ "--text-",
323
+ "--leading-",
324
+ "--tracking-",
325
+ "--radius-",
326
+ "--spacing",
327
+ "--container-",
328
+ "--shadow-"
329
+ ];
330
+ const BRAND_PROPERTY = /^--[a-z][a-z0-9-]*$/;
331
+ const BRAND_UNSAFE_VALUE = /[{};<>\\@]|url\s*\(|expression\s*\(|\/\*/i;
332
+ const BRAND_DARK_MEDIA = /^\(\s*prefers-color-scheme\s*:\s*dark\s*\)$/i;
333
+ const BRAND_FONT_DESCRIPTORS = /* @__PURE__ */ new Set([
334
+ "font-family",
335
+ "src",
336
+ "font-weight",
337
+ "font-style",
338
+ "font-display",
339
+ "unicode-range"
340
+ ]);
341
+ /** The theme variable a dark-mode color lowers to: `--color-brand` → `--color-brand-dark`. */
342
+ const darkVariableName = (property) => `${property}-dark`;
343
+ const spanOfNode = (node) => ({
344
+ start: node.loc?.start.offset ?? 0,
345
+ end: node.loc?.end.offset ?? 0
346
+ });
347
+ const unquoteFamily = (value) => {
348
+ const trimmed = value.trim();
349
+ const quoted = /^(['"])(.*)\1$/s.exec(trimmed);
350
+ return quoted === null ? trimmed : quoted[2].replace(/\\(.)/g, "$1");
351
+ };
352
+ const quoteFamily = (family) => `"${family.replace(/["\\]/g, "\\$&")}"`;
353
+ /** Parse brand theme CSS against the closed grammar above. */
354
+ const parseBrandCss = (css) => {
355
+ const diagnostics = [];
356
+ const report = (code, message, span) => {
357
+ diagnostics.push({
358
+ code,
359
+ message,
360
+ span
361
+ });
362
+ };
363
+ const variables = {};
364
+ const dark = {};
365
+ const fontFaces = [];
366
+ const ast = parse(css, {
367
+ positions: true,
368
+ parseCustomProperty: false,
369
+ onParseError: (error) => {
370
+ const offset = error.offset ?? 0;
371
+ report("brand-css-syntax", `CSS syntax error: ${error.message}`, {
372
+ start: offset,
373
+ end: offset
374
+ });
375
+ }
376
+ });
377
+ const sourceOf = (node) => node.loc === void 0 || node.loc === null ? generate(node).trim() : css.slice(node.loc.start.offset, node.loc.end.offset).trim();
378
+ const readTheme = (node, into, scope) => {
379
+ if (node.type !== "Atrule" || node.block === null) return;
380
+ if (node.prelude !== null && generate(node.prelude).trim() !== "") report("brand-css-theme-prelude", "@theme takes no options in a brand theme.", spanOfNode(node));
381
+ for (const child of node.block.children) {
382
+ if (child.type !== "Declaration") {
383
+ report("brand-css-unsupported", "@theme may contain only theme variables (--name: value;).", spanOfNode(child));
384
+ continue;
385
+ }
386
+ const property = child.property;
387
+ const value = sourceOf(child.value);
388
+ const span = spanOfNode(child);
389
+ if (!BRAND_PROPERTY.test(property)) {
390
+ report("brand-css-property", `${property} is not a theme variable; name theme variables --namespace-name.`, span);
391
+ continue;
392
+ }
393
+ if (!BRAND_NAMESPACES.some((namespace) => property.startsWith(namespace))) {
394
+ report("brand-css-namespace", `${property} is outside the brand namespaces (${BRAND_NAMESPACES.map((namespace) => namespace.endsWith("-") ? `${namespace}*` : namespace).join(", ")}).`, span);
395
+ continue;
396
+ }
397
+ if (scope === "dark" && !property.startsWith("--color-")) {
398
+ report("brand-css-dark-namespace", `${property}: the dark-mode @theme sets colors (--color-*) only.`, span);
399
+ continue;
400
+ }
401
+ if (value === "" || BRAND_UNSAFE_VALUE.test(value) || child.important !== false) {
402
+ report("brand-css-value", `${property} has a value a theme variable cannot take; write a plain value without braces, semicolons, url() or !important.`, span);
403
+ continue;
404
+ }
405
+ if (property in into) {
406
+ report("brand-css-duplicate", `${property} is declared twice.`, span);
407
+ continue;
408
+ }
409
+ into[property] = value;
410
+ }
411
+ };
412
+ const readFontFace = (node) => {
413
+ if (node.type !== "Atrule" || node.block === null) return;
414
+ const face = {};
415
+ for (const child of node.block.children) {
416
+ if (child.type !== "Declaration") {
417
+ report("brand-css-unsupported", "@font-face may contain only descriptors.", spanOfNode(child));
418
+ continue;
419
+ }
420
+ const property = child.property.toLowerCase();
421
+ const value = sourceOf(child.value);
422
+ const span = spanOfNode(child);
423
+ if (!BRAND_FONT_DESCRIPTORS.has(property)) {
424
+ report("brand-css-font-descriptor", `@font-face ${property} is not supported; use ${[...BRAND_FONT_DESCRIPTORS].join(", ")}.`, span);
425
+ continue;
426
+ }
427
+ if (property === "src") {
428
+ let urls = 0;
429
+ let invalid = false;
430
+ walk(child.value, (part) => {
431
+ if (part.type === "Url") {
432
+ urls++;
433
+ const path = part.value.replace(/[?#].*$/, "");
434
+ if (!/^https:\/\/[^\s]+$/i.test(part.value)) invalid = true;
435
+ else if (!/\.woff2$/i.test(path) && !/format\(\s*['"]?woff2/i.test(value)) invalid = true;
436
+ } else if (part.type === "Function" && part.name.toLowerCase() === "local") invalid = true;
437
+ });
438
+ if (urls === 0 || invalid) {
439
+ report("brand-css-font-src", "A brand @font-face src must be one or more https url()s to WOFF2 files.", span);
440
+ continue;
441
+ }
442
+ face.src = value;
443
+ continue;
444
+ }
445
+ if (BRAND_UNSAFE_VALUE.test(value)) {
446
+ report("brand-css-value", `@font-face ${property} has an unsupported value.`, span);
447
+ continue;
448
+ }
449
+ if (property === "font-family") face.family = unquoteFamily(value);
450
+ if (property === "font-weight") face.weight = value;
451
+ if (property === "font-style") face.style = value;
452
+ if (property === "font-display") face.display = value;
453
+ if (property === "unicode-range") face.unicodeRange = value;
454
+ }
455
+ if (face.family === void 0 || face.family === "" || face.src === void 0) {
456
+ report("brand-css-font-incomplete", "A brand @font-face needs a font-family and an https WOFF2 src.", spanOfNode(node));
457
+ return;
458
+ }
459
+ fontFaces.push({
460
+ family: face.family,
461
+ src: face.src,
462
+ ...face.weight === void 0 ? {} : { weight: face.weight },
463
+ ...face.style === void 0 ? {} : { style: face.style },
464
+ ...face.display === void 0 ? {} : { display: face.display },
465
+ ...face.unicodeRange === void 0 ? {} : { unicodeRange: face.unicodeRange }
466
+ });
467
+ };
468
+ if (ast.type === "StyleSheet") for (const node of ast.children) {
469
+ if (node.type === "Atrule" && node.block !== null) {
470
+ const name = node.name.toLowerCase();
471
+ if (name === "theme") {
472
+ readTheme(node, variables, "light");
473
+ continue;
474
+ }
475
+ if (name === "font-face") {
476
+ readFontFace(node);
477
+ continue;
478
+ }
479
+ if (name === "media" && node.prelude !== null && BRAND_DARK_MEDIA.test(generate(node.prelude).trim())) {
480
+ for (const child of node.block.children) if (child.type === "Atrule" && child.name.toLowerCase() === "theme") readTheme(child, dark, "dark");
481
+ else report("brand-css-unsupported", "The dark-mode block may contain only @theme.", spanOfNode(child));
482
+ continue;
483
+ }
484
+ }
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
+ }
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
+ return diagnostics.length > 0 ? Result.err(diagnostics) : Result.ok({
492
+ variables,
493
+ dark,
494
+ fontFaces
495
+ });
496
+ };
497
+ const printDeclarations = (entries, indent) => Object.entries(entries).map(([property, value]) => `${indent}${property}: ${value};`).join("\n");
498
+ const printFontFace = (face) => [
499
+ "@font-face {",
500
+ ` font-family: ${quoteFamily(face.family)};`,
501
+ ` src: ${face.src};`,
502
+ ...face.weight === void 0 ? [] : [` font-weight: ${face.weight};`],
503
+ ...face.style === void 0 ? [] : [` font-style: ${face.style};`],
504
+ ...face.display === void 0 ? [] : [` font-display: ${face.display};`],
505
+ ...face.unicodeRange === void 0 ? [] : [` unicode-range: ${face.unicodeRange};`],
506
+ "}"
507
+ ].join("\n");
508
+ /**
509
+ * Serialize a brand theme in canonical form: font faces, then the light
510
+ * `@theme`, then the dark-mode `@theme`. Empty sections are omitted, and the
511
+ * output always parses back to the same value.
512
+ */
513
+ const printBrandCss = (brand) => {
514
+ const blocks = [
515
+ ...brand.fontFaces.map(printFontFace),
516
+ ...Object.keys(brand.variables).length === 0 ? [] : [`@theme {\n${printDeclarations(brand.variables, " ")}\n}`],
517
+ ...Object.keys(brand.dark).length === 0 ? [] : [`@media (prefers-color-scheme: dark) {\n @theme {\n${printDeclarations(brand.dark, " ")}\n }\n}`]
518
+ ];
519
+ return blocks.length === 0 ? "" : `${blocks.join("\n\n")}\n`;
520
+ };
521
+ /** The brand color, and the text color that sits on it. */
522
+ const BRAND_COLOR_VARIABLE = "--color-brand";
523
+ const BRAND_FOREGROUND_VARIABLE = "--color-brand-foreground";
524
+ /** WCAG relative luminance of an sRGB channel triple in 0–1. */
525
+ const relativeLuminance = (r, g, b) => {
526
+ const linear = (channel) => {
527
+ const clamped = Math.min(1, Math.max(0, channel));
528
+ return clamped <= .04045 ? clamped / 12.92 : ((clamped + .055) / 1.055) ** 2.4;
529
+ };
530
+ return .2126 * linear(r) + .7152 * linear(g) + .0722 * linear(b);
531
+ };
532
+ /**
533
+ * Black or white, whichever has the higher WCAG contrast ratio against
534
+ * `color`. A translucent color is judged over white, the page an email sits on.
535
+ * `undefined` when the color is not one `parseColor` evaluates (a `var()`,
536
+ * `currentColor`, `lab()`, a named color outside its table): nothing is
537
+ * guessed for it.
538
+ */
539
+ const contrastForeground = (color) => {
540
+ const parsed = parseColor(color);
541
+ if (parsed === void 0) return void 0;
542
+ const alpha = Math.min(1, Math.max(0, parsed.a));
543
+ const over = (channel) => channel * alpha + (1 - alpha);
544
+ const luminance = relativeLuminance(over(parsed.r), over(parsed.g), over(parsed.b));
545
+ return 1.05 / (luminance + .05) > (luminance + .05) / .05 ? "#ffffff" : "#000000";
546
+ };
547
+ /**
548
+ * The text-on-brand colors a brand gets when it leaves them out: for the light
549
+ * theme and, when the brand sets a dark-mode brand color, for dark mode. Each
550
+ * is the `contrastForeground` of the brand color it sits on. A brand that sets
551
+ * the foreground, or whose brand color does not evaluate, gets no default.
552
+ */
553
+ const brandForegroundDefaults = (brand) => {
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.
568
+ */
569
+ const brandThemeVariables = (brand) => {
570
+ const defaults = brandForegroundDefaults(brand);
571
+ return {
572
+ ...brand.variables,
573
+ ...defaults.light === void 0 ? {} : { [BRAND_FOREGROUND_VARIABLE]: defaults.light },
574
+ ...Object.fromEntries(Object.entries(brand.dark).map(([property, value]) => [darkVariableName(property), value])),
575
+ ...defaults.dark === void 0 ? {} : { [darkVariableName(BRAND_FOREGROUND_VARIABLE)]: defaults.dark }
576
+ };
577
+ };
578
+ /** The stylesheet Tailwind compiles for `@import "samva:brand"`: `brandThemeVariables` and the font faces. */
579
+ const brandTailwindCss = (brand) => printBrandCss({
580
+ fontFaces: brand.fontFaces,
581
+ variables: brandThemeVariables(brand),
582
+ dark: {}
583
+ });
584
+ //#endregion
585
+ export { brandThemeVariables as a, parseBrandCss as c, formatColor as d, parseColor as f, brandTailwindCss as i, printBrandCss as l, BRAND_FOREGROUND_VARIABLE as n, contrastForeground as o, brandForegroundDefaults as r, darkVariableName as s, BRAND_COLOR_VARIABLE as t, downlevelColors as u };
586
+
587
+ //# sourceMappingURL=theme-css-hFqcPSar.js.map
@@ -1,24 +1,65 @@
1
- import { t as Diagnostic } from "./diagnostics-OcQKQYSY.js";
2
- import { o as TokenOverrides } from "./theme-6ApjjiBo.js";
1
+ import { Diagnostic } from "./diagnostics.js";
3
2
  import { Result } from "better-result";
4
3
  //#region src/theme-css.d.ts
4
+ /** One `@font-face` a brand declares. `src` is the declaration value as authored. */
5
+ interface BrandFontFace {
6
+ readonly family: string;
7
+ readonly src: string;
8
+ readonly weight?: string | undefined;
9
+ readonly style?: string | undefined;
10
+ readonly display?: string | undefined;
11
+ readonly unicodeRange?: string | undefined;
12
+ }
13
+ /** A brand theme, parsed. Property names keep their leading `--`. */
14
+ interface BrandCss {
15
+ /** Light `@theme` variables. */
16
+ readonly variables: Readonly<Record<string, string>>;
17
+ /** Dark-mode `@theme` colors, keyed by the light name they replace in dark mode. */
18
+ readonly dark: Readonly<Record<string, string>>;
19
+ readonly fontFaces: readonly BrandFontFace[];
20
+ }
21
+ /** The theme variable a dark-mode color lowers to: `--color-brand` → `--color-brand-dark`. */
22
+ declare const darkVariableName: (property: string) => string;
23
+ /** Parse brand theme CSS against the closed grammar above. */
24
+ declare const parseBrandCss: (css: string) => Result<BrandCss, ReadonlyArray<Diagnostic>>;
5
25
  /**
6
- * `TokenOverrides` plus `tracking`. The compiler's `TokenOverrides` names its
7
- * letter-spacing slot `trackingEm`; the `@theme` CSS namespace and the persisted
8
- * theme schema both call it `tracking`, so the parser surfaces it under that
9
- * name and apps/api maps `tracking → trackingEm` at the compile call sites.
26
+ * Serialize a brand theme in canonical form: font faces, then the light
27
+ * `@theme`, then the dark-mode `@theme`. Empty sections are omitted, and the
28
+ * output always parses back to the same value.
10
29
  */
11
- interface ParsedTheme extends TokenOverrides {
12
- /** tracking-{name} → letter-spacing in em (negative allowed). */
13
- readonly tracking?: Readonly<Record<string, number>> | undefined;
14
- }
15
- declare const parseThemeCss: (css: string) => Result<ParsedTheme, ReadonlyArray<Diagnostic>>;
30
+ declare const printBrandCss: (brand: BrandCss) => string;
31
+ /** The brand color, and the text color that sits on it. */
32
+ declare const BRAND_COLOR_VARIABLE = "--color-brand";
33
+ declare const BRAND_FOREGROUND_VARIABLE = "--color-brand-foreground";
34
+ /**
35
+ * Black or white, whichever has the higher WCAG contrast ratio against
36
+ * `color`. A translucent color is judged over white, the page an email sits on.
37
+ * `undefined` when the color is not one `parseColor` evaluates (a `var()`,
38
+ * `currentColor`, `lab()`, a named color outside its table): nothing is
39
+ * guessed for it.
40
+ */
41
+ declare const contrastForeground: (color: string) => "#000000" | "#ffffff" | undefined;
42
+ /**
43
+ * The text-on-brand colors a brand gets when it leaves them out: for the light
44
+ * theme and, when the brand sets a dark-mode brand color, for dark mode. Each
45
+ * is the `contrastForeground` of the brand color it sits on. A brand that sets
46
+ * the foreground, or whose brand color does not evaluate, gets no default.
47
+ */
48
+ declare const brandForegroundDefaults: (brand: BrandCss) => {
49
+ readonly light?: string | undefined;
50
+ readonly dark?: string | undefined;
51
+ };
16
52
  /**
17
- * Serialize a ParsedTheme to `@theme` CSS. Light tokens go in the top-level
18
- * `@theme` block; `colorsDark` (colors-only, matching the parser) goes in the
19
- * `@media (prefers-color-scheme: dark) { @theme { … } }` wrapper.
53
+ * A brand theme as flat theme variables, the way a build compiles it. Email
54
+ * inlining resolves `var()` once, so a dark-mode `@theme` cannot switch a
55
+ * value at run time; each dark color lowers to its `-dark` theme variable,
56
+ * which `dark:` utilities read (`dark:bg-brand-dark`). A text-on-brand color
57
+ * the brand leaves out is filled in by `brandForegroundDefaults`, so every
58
+ * build compiles the same default and the stored CSS never carries it.
20
59
  */
21
- declare const printThemeCss: (theme: ParsedTheme) => string;
60
+ declare const brandThemeVariables: (brand: BrandCss) => Readonly<Record<string, string>>;
61
+ /** The stylesheet Tailwind compiles for `@import "samva:brand"`: `brandThemeVariables` and the font faces. */
62
+ declare const brandTailwindCss: (brand: BrandCss) => string;
22
63
  //#endregion
23
- export { ParsedTheme, parseThemeCss, printThemeCss };
64
+ export { BRAND_COLOR_VARIABLE, BRAND_FOREGROUND_VARIABLE, BrandCss, BrandFontFace, brandForegroundDefaults, brandTailwindCss, brandThemeVariables, contrastForeground, darkVariableName, parseBrandCss, printBrandCss };
24
65
  //# sourceMappingURL=theme-css.d.ts.map