@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
package/dist/theme-css.js CHANGED
@@ -1,509 +1,2 @@
1
- import { Result } from "better-result";
2
- //#region src/theme-css.ts
3
- const CHAR_TAB = 9;
4
- const CHAR_LINE_FEED = 10;
5
- const CHAR_CARRIAGE_RETURN = 13;
6
- const CHAR_SPACE = 32;
7
- const CHAR_STAR = 42;
8
- const CHAR_HYPHEN = 45;
9
- const CHAR_SLASH = 47;
10
- const CHAR_COLON = 58;
11
- const CHAR_SEMICOLON = 59;
12
- const CHAR_AT = 64;
13
- const CHAR_OPEN_PAREN = 40;
14
- const CHAR_CLOSE_PAREN = 41;
15
- const CHAR_OPEN_BRACE = 123;
16
- const CHAR_CLOSE_BRACE = 125;
17
- const CHAR_0 = 48;
18
- const CHAR_9 = 57;
19
- const CHAR_UPPER_A = 65;
20
- const CHAR_UPPER_Z = 90;
21
- const CHAR_LOWER_A = 97;
22
- const CHAR_LOWER_Z = 122;
23
- const isWhitespace = (code) => code === CHAR_SPACE || code === CHAR_TAB || code === CHAR_LINE_FEED || code === CHAR_CARRIAGE_RETURN;
24
- const isIdentChar = (code) => code >= CHAR_LOWER_A && code <= CHAR_LOWER_Z || code >= CHAR_UPPER_A && code <= CHAR_UPPER_Z || code >= CHAR_0 && code <= CHAR_9 || code === CHAR_HYPHEN;
25
- const HEX_PATTERN = /^#([0-9a-fA-F]{3}|[0-9a-fA-F]{6})$/;
26
- const NUM = String.raw`(?:\d+(?:\.\d+)?|\.\d+)`;
27
- const PX_PATTERN = new RegExp(`^(${NUM})px$`);
28
- const TEXT_PATTERN = new RegExp(`^(${NUM})px(?:\\s*/\\s*(${NUM})px)?$`);
29
- const UNITLESS_PATTERN = new RegExp(`^${NUM}$`);
30
- const EM_PATTERN = new RegExp(`^(-?${NUM})em$`);
31
- const INTEGER_PATTERN = /^\d+$/;
32
- /** #rgb/#rrggbb → 6-digit lowercase, or null. */
33
- const normalizeHex = (value) => {
34
- const match = HEX_PATTERN.exec(value);
35
- if (match === null) return null;
36
- const digits = match[1].toLowerCase();
37
- if (digits.length === 6) return `#${digits}`;
38
- return `#${digits[0]}${digits[0]}${digits[1]}${digits[1]}${digits[2]}${digits[2]}`;
39
- };
40
- const pxNumber = (value) => {
41
- const match = PX_PATTERN.exec(value);
42
- return match === null ? null : Number.parseFloat(match[1]);
43
- };
44
- const NAMESPACE_SUMMARY = "supported namespaces: --color-*, --radius-*, --font-*, --font-weight-*, --text-*, --leading-*, --spacing, --container-*, --tracking-*";
45
- const parseThemeCss = (css) => {
46
- const length = css.length;
47
- let position = 0;
48
- const diagnostics = [];
49
- const seen = /* @__PURE__ */ new Set();
50
- const accumulator = {
51
- colors: {},
52
- colorsDark: {},
53
- text: {},
54
- leading: {},
55
- radiusPx: {},
56
- fontWeights: {},
57
- fontFamilies: {},
58
- containerPx: {},
59
- tracking: {},
60
- spacingPx: void 0
61
- };
62
- let themeBlockCount = 0;
63
- const report = (code, message, span) => {
64
- diagnostics.push({
65
- code,
66
- message,
67
- span
68
- });
69
- };
70
- const skipTrivia = () => {
71
- while (position < length) {
72
- const code = css.charCodeAt(position);
73
- if (isWhitespace(code)) {
74
- position++;
75
- continue;
76
- }
77
- if (code === CHAR_SLASH && css.charCodeAt(position + 1) === CHAR_STAR) {
78
- const start = position;
79
- const close = css.indexOf("*/", position + 2);
80
- if (close === -1) {
81
- report("theme-unterminated-comment", "unterminated /* comment — add */", {
82
- start,
83
- end: length
84
- });
85
- position = length;
86
- return;
87
- }
88
- position = close + 2;
89
- continue;
90
- }
91
- return;
92
- }
93
- };
94
- /** `@word` at the current position, not followed by more ident chars. */
95
- const atKeyword = (word) => css.startsWith(word, position) && !isIdentChar(css.charCodeAt(position + word.length));
96
- /** Skips a balanced `{ ... }` region (position on or before the `{`). */
97
- const skipBalancedBlock = () => {
98
- while (position < length && css.charCodeAt(position) !== CHAR_OPEN_BRACE) position++;
99
- let depth = 0;
100
- while (position < length) {
101
- const code = css.charCodeAt(position);
102
- position++;
103
- if (code === CHAR_OPEN_BRACE) depth++;
104
- else if (code === CHAR_CLOSE_BRACE) {
105
- depth--;
106
- if (depth === 0) return;
107
- }
108
- }
109
- };
110
- /** Declaration-level recovery: past the next `;`, or up to the block's `}`. */
111
- const skipToDeclarationBoundary = () => {
112
- while (position < length) {
113
- const code = css.charCodeAt(position);
114
- if (code === CHAR_SEMICOLON) {
115
- position++;
116
- return;
117
- }
118
- if (code === CHAR_CLOSE_BRACE) return;
119
- position++;
120
- }
121
- };
122
- const record = (target, scope, field, name, value, propertySpan, property) => {
123
- const key = `${scope}:${field}:${name}`;
124
- if (seen.has(key)) {
125
- report("theme-duplicate-declaration", `duplicate declaration --${property} — declare each token once`, propertySpan);
126
- return;
127
- }
128
- seen.add(key);
129
- target[name] = value;
130
- };
131
- const parseDeclaration = (scope) => {
132
- const propertyStart = position;
133
- position += 2;
134
- while (position < length && isIdentChar(css.charCodeAt(position))) position++;
135
- const property = css.slice(propertyStart + 2, position);
136
- const propertySpan = {
137
- start: propertyStart,
138
- end: position
139
- };
140
- skipTrivia();
141
- if (css.charCodeAt(position) !== CHAR_COLON) {
142
- report("theme-expected-declaration", `expected : after --${property}`, {
143
- start: position,
144
- end: Math.min(position + 1, length)
145
- });
146
- skipToDeclarationBoundary();
147
- return;
148
- }
149
- position++;
150
- skipTrivia();
151
- const valueStart = position;
152
- while (position < length) {
153
- const code = css.charCodeAt(position);
154
- if (code === CHAR_SEMICOLON || code === CHAR_CLOSE_BRACE || code === CHAR_OPEN_BRACE) break;
155
- position++;
156
- }
157
- let valueEnd = position;
158
- while (valueEnd > valueStart && isWhitespace(css.charCodeAt(valueEnd - 1))) valueEnd--;
159
- const value = css.slice(valueStart, valueEnd).replace(/\s+/g, " ");
160
- const valueSpan = {
161
- start: valueStart,
162
- end: Math.max(valueEnd, valueStart + 1)
163
- };
164
- if (css.charCodeAt(position) === CHAR_SEMICOLON) position++;
165
- else report("theme-missing-semicolon", `missing ; after --${property} declaration`, propertySpan);
166
- routeDeclaration(scope, property, propertySpan, value, valueSpan);
167
- };
168
- const invalidValue = (property, value, expected, span) => {
169
- report("theme-invalid-value", `invalid value "${value}" for --${property} — ${expected}`, span);
170
- };
171
- const routeDeclaration = (scope, property, propertySpan, value, valueSpan) => {
172
- const named = (prefix) => {
173
- if (!property.startsWith(prefix)) return null;
174
- const name = property.slice(prefix.length);
175
- if (name.length === 0) {
176
- report("theme-invalid-value", `--${property} is missing a token name — write --${prefix}<name>`, propertySpan);
177
- return null;
178
- }
179
- if (name.includes("--")) {
180
- report("theme-invalid-value", `malformed token name in --${property}${prefix === "text-" ? " — use the size/line-height shorthand instead: --text-xl: 20px/28px" : " — token names use single hyphens"}`, propertySpan);
181
- return null;
182
- }
183
- return name;
184
- };
185
- if (value.length === 0) {
186
- invalidValue(property, value, "the value is empty", valueSpan);
187
- return;
188
- }
189
- if (property.startsWith("color-")) {
190
- const name = named("color-");
191
- if (name === null) return;
192
- const hex = normalizeHex(value);
193
- if (hex === null) {
194
- invalidValue(property, value, "expected a hex color like #4f46e5 or #fff (named colors, oklch(), and rgb() are outside the closed grammar)", valueSpan);
195
- return;
196
- }
197
- if (scope === "dark") record(accumulator.colorsDark, scope, "colors", name, hex, propertySpan, property);
198
- else record(accumulator.colors, scope, "colors", name, hex, propertySpan, property);
199
- return;
200
- }
201
- if (scope === "dark") {
202
- report("theme-dark-non-color", `--${property} is not allowed in the dark theme block — only --color-* tokens have dark counterparts`, propertySpan);
203
- return;
204
- }
205
- if (property.startsWith("radius-")) {
206
- const name = named("radius-");
207
- if (name === null) return;
208
- const px = pxNumber(value);
209
- if (px === null) {
210
- invalidValue(property, value, "expected a px length like \"12px\"", valueSpan);
211
- return;
212
- }
213
- record(accumulator.radiusPx, scope, "radiusPx", name, px, propertySpan, property);
214
- return;
215
- }
216
- if (property.startsWith("font-weight-")) {
217
- const name = named("font-weight-");
218
- if (name === null) return;
219
- const weight = INTEGER_PATTERN.test(value) ? Number.parseInt(value, 10) : null;
220
- if (weight === null || weight < 100 || weight > 900) {
221
- invalidValue(property, value, "expected a font weight between 100 and 900", valueSpan);
222
- return;
223
- }
224
- record(accumulator.fontWeights, scope, "fontWeights", name, weight, propertySpan, property);
225
- return;
226
- }
227
- if (property.startsWith("font-")) {
228
- const name = named("font-");
229
- if (name === null) return;
230
- if (value.includes("var(") || value.includes("calc(") || value.includes("oklch(")) {
231
- invalidValue(property, value, "expected a literal font stack — var()/calc() don't exist in compiled email", valueSpan);
232
- return;
233
- }
234
- const stack = value.replaceAll("\"", "'");
235
- record(accumulator.fontFamilies, scope, "fontFamilies", name, stack, propertySpan, property);
236
- return;
237
- }
238
- if (property.startsWith("text-")) {
239
- const name = named("text-");
240
- if (name === null) return;
241
- const match = TEXT_PATTERN.exec(value);
242
- if (match === null) {
243
- invalidValue(property, value, "expected \"16px\" or \"16px/24px\" (font-size/line-height)", valueSpan);
244
- return;
245
- }
246
- const sizePx = Number.parseFloat(match[1]);
247
- const lineHeightPx = match[2] === void 0 ? Math.round(sizePx * 1.5) : Number.parseFloat(match[2]);
248
- record(accumulator.text, scope, "text", name, {
249
- sizePx,
250
- lineHeightPx
251
- }, propertySpan, property);
252
- return;
253
- }
254
- if (property.startsWith("leading-")) {
255
- const name = named("leading-");
256
- if (name === null) return;
257
- if (!UNITLESS_PATTERN.test(value) || Number.parseFloat(value) <= 0) {
258
- invalidValue(property, value, "expected a unitless line-height ratio like \"1.5\"", valueSpan);
259
- return;
260
- }
261
- record(accumulator.leading, scope, "leading", name, Number.parseFloat(value), propertySpan, property);
262
- return;
263
- }
264
- if (property.startsWith("tracking-")) {
265
- const name = named("tracking-");
266
- if (name === null) return;
267
- const match = EM_PATTERN.exec(value);
268
- if (match === null) {
269
- invalidValue(property, value, "expected an em value like \"0.025em\" or \"-0.05em\"", valueSpan);
270
- return;
271
- }
272
- record(accumulator.tracking, scope, "tracking", name, Number.parseFloat(match[1]), propertySpan, property);
273
- return;
274
- }
275
- if (property.startsWith("container-")) {
276
- const name = named("container-");
277
- if (name === null) return;
278
- const px = pxNumber(value);
279
- if (px === null) {
280
- invalidValue(property, value, "expected a px length like \"600px\"", valueSpan);
281
- return;
282
- }
283
- record(accumulator.containerPx, scope, "containerPx", name, px, propertySpan, property);
284
- return;
285
- }
286
- if (property === "spacing") {
287
- const px = pxNumber(value);
288
- if (px === null || px <= 0) {
289
- invalidValue(property, value, "expected a positive px length like \"4px\"", valueSpan);
290
- return;
291
- }
292
- const key = "light:spacingPx";
293
- if (seen.has(key)) {
294
- report("theme-duplicate-declaration", "duplicate declaration --spacing — declare each token once", propertySpan);
295
- return;
296
- }
297
- seen.add(key);
298
- accumulator.spacingPx = px;
299
- return;
300
- }
301
- if (property.startsWith("spacing-")) {
302
- report("theme-unknown-namespace", `--${property} is not supported — only the bare --spacing multiplier is (spacing steps derive from it)`, propertySpan);
303
- return;
304
- }
305
- report("theme-unknown-namespace", `unknown namespace in --${property} — ${NAMESPACE_SUMMARY}`, propertySpan);
306
- };
307
- /** Position after the `@theme` keyword; parses `{ declarations }`. */
308
- const parseThemeBlock = (scope) => {
309
- skipTrivia();
310
- if (css.charCodeAt(position) !== CHAR_OPEN_BRACE) {
311
- report("theme-expected-block", "expected { after @theme", {
312
- start: position,
313
- end: Math.min(position + 1, length)
314
- });
315
- return;
316
- }
317
- const openBrace = position;
318
- position++;
319
- themeBlockCount++;
320
- while (true) {
321
- skipTrivia();
322
- if (position >= length) {
323
- report("theme-unbalanced-braces", "unclosed @theme block — add }", {
324
- start: openBrace,
325
- end: openBrace + 1
326
- });
327
- return;
328
- }
329
- const code = css.charCodeAt(position);
330
- if (code === CHAR_CLOSE_BRACE) {
331
- position++;
332
- return;
333
- }
334
- if (code === CHAR_HYPHEN && css.charCodeAt(position + 1) === CHAR_HYPHEN) {
335
- parseDeclaration(scope);
336
- continue;
337
- }
338
- report("theme-expected-declaration", "expected a --<namespace>-<name> declaration", {
339
- start: position,
340
- end: Math.min(position + 1, length)
341
- });
342
- skipToDeclarationBoundary();
343
- }
344
- };
345
- /** Position after the `@media` keyword; parses the dark wrapper block. */
346
- const parseMediaBlock = () => {
347
- const mediaStart = position - 6;
348
- const preludeError = () => {
349
- report("theme-unsupported-media", "only @media (prefers-color-scheme: dark) is supported — the dark theme wrapper", {
350
- start: mediaStart,
351
- end: Math.min(position + 1, length)
352
- });
353
- skipBalancedBlock();
354
- };
355
- skipTrivia();
356
- if (css.charCodeAt(position) !== CHAR_OPEN_PAREN) return preludeError();
357
- position++;
358
- skipTrivia();
359
- if (!css.startsWith("prefers-color-scheme", position)) return preludeError();
360
- position += 20;
361
- skipTrivia();
362
- if (css.charCodeAt(position) !== CHAR_COLON) return preludeError();
363
- position++;
364
- skipTrivia();
365
- if (!atKeyword("dark")) return preludeError();
366
- position += 4;
367
- skipTrivia();
368
- if (css.charCodeAt(position) !== CHAR_CLOSE_PAREN) return preludeError();
369
- position++;
370
- skipTrivia();
371
- if (css.charCodeAt(position) !== CHAR_OPEN_BRACE) {
372
- report("theme-expected-block", "expected { after the dark media query", {
373
- start: position,
374
- end: Math.min(position + 1, length)
375
- });
376
- return;
377
- }
378
- const openBrace = position;
379
- position++;
380
- let sawThemeBlock = false;
381
- while (true) {
382
- skipTrivia();
383
- if (position >= length) {
384
- report("theme-unbalanced-braces", "unclosed @media block — add }", {
385
- start: openBrace,
386
- end: openBrace + 1
387
- });
388
- return;
389
- }
390
- const code = css.charCodeAt(position);
391
- if (code === CHAR_CLOSE_BRACE) {
392
- position++;
393
- break;
394
- }
395
- if (code === CHAR_AT && atKeyword("@theme")) {
396
- position += 6;
397
- parseThemeBlock("dark");
398
- sawThemeBlock = true;
399
- continue;
400
- }
401
- report("theme-expected-block", "expected an @theme block inside the dark media query", {
402
- start: position,
403
- end: Math.min(position + 1, length)
404
- });
405
- skipBalancedBlock();
406
- }
407
- if (!sawThemeBlock) report("theme-expected-block", "the dark media query contains no @theme block", {
408
- start: openBrace,
409
- end: openBrace + 1
410
- });
411
- };
412
- while (position < length) {
413
- skipTrivia();
414
- if (position >= length) break;
415
- if (atKeyword("@theme")) {
416
- position += 6;
417
- parseThemeBlock("light");
418
- continue;
419
- }
420
- if (atKeyword("@media")) {
421
- position += 6;
422
- parseMediaBlock();
423
- continue;
424
- }
425
- report("theme-unexpected-content", "expected @theme or @media (prefers-color-scheme: dark) at the top level", {
426
- start: position,
427
- end: Math.min(position + 1, length)
428
- });
429
- const next = position;
430
- while (position < length) {
431
- const code = css.charCodeAt(position);
432
- if (code === CHAR_AT && position > next) break;
433
- if (code === CHAR_OPEN_BRACE) {
434
- skipBalancedBlock();
435
- break;
436
- }
437
- position++;
438
- }
439
- }
440
- if (themeBlockCount === 0 && diagnostics.length === 0) report("theme-missing-block", "no @theme block found — write @theme { --color-brand: #4f46e5; }", {
441
- start: 0,
442
- end: Math.min(1, length)
443
- });
444
- if (diagnostics.length > 0) return Result.err(diagnostics);
445
- const nonEmpty = (entries) => Object.keys(entries).length > 0 ? entries : void 0;
446
- const colors = nonEmpty(accumulator.colors);
447
- const colorsDark = nonEmpty(accumulator.colorsDark);
448
- const text = nonEmpty(accumulator.text);
449
- const leading = nonEmpty(accumulator.leading);
450
- const radiusPx = nonEmpty(accumulator.radiusPx);
451
- const fontWeights = nonEmpty(accumulator.fontWeights);
452
- const fontFamilies = nonEmpty(accumulator.fontFamilies);
453
- const containerPx = nonEmpty(accumulator.containerPx);
454
- const tracking = nonEmpty(accumulator.tracking);
455
- return Result.ok({
456
- colors,
457
- colorsDark,
458
- spacingPx: accumulator.spacingPx,
459
- text,
460
- leading,
461
- radiusPx,
462
- fontWeights,
463
- fontFamilies,
464
- containerPx,
465
- tracking
466
- });
467
- };
468
- /** Trim a JS number to a stable decimal string (no exponent, no trailing zeros). */
469
- const numberText = (value) => {
470
- const text = String(value);
471
- if (!text.includes("e") && !text.includes("E")) return text;
472
- const expanded = value.toLocaleString("en-US", {
473
- useGrouping: false,
474
- maximumFractionDigits: 20
475
- });
476
- if (!expanded.includes(".")) return expanded;
477
- return expanded.replace(/0+$/, "").replace(/\.$/, "");
478
- };
479
- const declaration = (property, value) => ` --${property}: ${value};`;
480
- const colorDeclarations = (colors) => colors === void 0 ? [] : Object.entries(colors).map(([name, hex]) => declaration(`color-${name}`, hex));
481
- /**
482
- * Serialize a ParsedTheme to `@theme` CSS. Light tokens go in the top-level
483
- * `@theme` block; `colorsDark` (colors-only, matching the parser) goes in the
484
- * `@media (prefers-color-scheme: dark) { @theme { … } }` wrapper.
485
- */
486
- const printThemeCss = (theme) => {
487
- const light = [
488
- ...colorDeclarations(theme.colors),
489
- ...theme.spacingPx !== void 0 ? [declaration("spacing", `${numberText(theme.spacingPx)}px`)] : [],
490
- ...theme.radiusPx === void 0 ? [] : Object.entries(theme.radiusPx).map(([name, px]) => declaration(`radius-${name}`, `${numberText(px)}px`)),
491
- ...theme.fontFamilies === void 0 ? [] : Object.entries(theme.fontFamilies).map(([name, stack]) => declaration(`font-${name}`, stack)),
492
- ...theme.fontWeights === void 0 ? [] : Object.entries(theme.fontWeights).map(([name, weight]) => declaration(`font-weight-${name}`, numberText(weight))),
493
- ...theme.text === void 0 ? [] : Object.entries(theme.text).map(([name, token]) => declaration(`text-${name}`, `${numberText(token.sizePx)}px/${numberText(token.lineHeightPx)}px`)),
494
- ...theme.leading === void 0 ? [] : Object.entries(theme.leading).map(([name, ratio]) => declaration(`leading-${name}`, numberText(ratio))),
495
- ...(() => {
496
- const trackingSource = theme.tracking ?? theme.trackingEm;
497
- return trackingSource === void 0 ? [] : Object.entries(trackingSource).map(([name, em]) => declaration(`tracking-${name}`, `${numberText(em)}em`));
498
- })(),
499
- ...theme.containerPx === void 0 ? [] : Object.entries(theme.containerPx).map(([name, px]) => declaration(`container-${name}`, `${numberText(px)}px`))
500
- ];
501
- const dark = colorDeclarations(theme.colorsDark);
502
- const blocks = [`@theme {\n${light.join("\n")}\n}`];
503
- if (dark.length > 0) blocks.push(`@media (prefers-color-scheme: dark) {\n @theme {\n ${dark.join("\n ")}\n }\n}`);
504
- return `${blocks.join("\n\n")}\n`;
505
- };
506
- //#endregion
507
- export { parseThemeCss, printThemeCss };
508
-
509
- //# sourceMappingURL=theme-css.js.map
1
+ import { a as brandThemeVariables, c as parseBrandCss, i as brandTailwindCss, l as printBrandCss, n as BRAND_FOREGROUND_VARIABLE, o as contrastForeground, r as brandForegroundDefaults, s as darkVariableName, t as BRAND_COLOR_VARIABLE } from "./theme-css-hFqcPSar.js";
2
+ export { BRAND_COLOR_VARIABLE, BRAND_FOREGROUND_VARIABLE, brandForegroundDefaults, brandTailwindCss, brandThemeVariables, contrastForeground, darkVariableName, parseBrandCss, printBrandCss };
@@ -0,0 +1,109 @@
1
+ import { buildLineMap, lineColumn } from "./diagnostics.js";
2
+ import { parse } from "css-tree";
3
+ //#region src/email/theme-imports.ts
4
+ /** The CSS and TSX specifier a template uses to reach its organization's brand. */
5
+ const BRAND_SPECIFIER = "samva:brand";
6
+ const BRAND_IMPORT = /^samva:brand(?:\/([a-z0-9](?:[a-z0-9-]{0,62}[a-z0-9])?))?$/;
7
+ /** The brand a `samva:brand` specifier names: `undefined` slug means the default brand. */
8
+ const parseBrandSpecifier = (specifier) => {
9
+ const match = BRAND_IMPORT.exec(specifier);
10
+ return match === null ? void 0 : { slug: match[1] };
11
+ };
12
+ /** The directory part of a project path, with no trailing slash (`""` at the root). */
13
+ const projectDirectory = (path) => path.includes("/") ? path.slice(0, path.lastIndexOf("/")) : "";
14
+ /** A relative or root-absolute stylesheet reference, resolved from a project directory. */
15
+ const resolveFromDirectory = (directory, reference) => {
16
+ const segments = reference.startsWith("/") || directory === "" ? [] : directory.split("/");
17
+ for (const segment of reference.split("/")) {
18
+ if (segment === "" || segment === ".") continue;
19
+ if (segment === "..") {
20
+ if (segments.length === 0) return void 0;
21
+ segments.pop();
22
+ } else segments.push(segment);
23
+ }
24
+ return segments.length === 0 ? void 0 : segments.join("/");
25
+ };
26
+ const importTarget = (node) => {
27
+ if (node.type !== "Atrule" || node.prelude === null || node.prelude.type !== "AtrulePrelude") return { extra: true };
28
+ const parts = [...node.prelude.children].filter((part) => part.type !== "WhiteSpace");
29
+ const first = parts[0];
30
+ const target = first?.type === "String" ? first.value : first?.type === "Url" ? first.value : void 0;
31
+ return target === void 0 ? { extra: true } : {
32
+ target,
33
+ extra: parts.length > 1
34
+ };
35
+ };
36
+ /**
37
+ * Walk the theme's `@import` graph. Project `.css` files resolve from the
38
+ * importing file's directory; `samva:brand` and `samva:brand/<slug>` are
39
+ * recorded for the brand the build supplies. Anything else is an error.
40
+ */
41
+ const resolveThemeImports = (themePath, themeCss, files) => {
42
+ const stylesheets = /* @__PURE__ */ new Map([[themePath, themeCss]]);
43
+ const brandSpecifiers = /* @__PURE__ */ new Set();
44
+ const diagnostics = [];
45
+ const visit = (path, source, chain) => {
46
+ const lineMap = buildLineMap(source);
47
+ const ast = parse(source, {
48
+ positions: true,
49
+ parseValue: false,
50
+ parseRulePrelude: false
51
+ });
52
+ if (ast.type !== "StyleSheet") return;
53
+ for (const node of ast.children) {
54
+ if (node.type !== "Atrule" || node.name.toLowerCase() !== "import") continue;
55
+ const position = lineColumn(lineMap, node.loc?.start.offset ?? 0);
56
+ const origins = [{
57
+ fileName: path,
58
+ lineNumber: position.line,
59
+ columnNumber: position.column
60
+ }];
61
+ const refuse = (message) => diagnostics.push({
62
+ code: "css-import-unresolved",
63
+ severity: "error",
64
+ message,
65
+ origins
66
+ });
67
+ const { target, extra } = importTarget(node);
68
+ if (target === void 0) {
69
+ refuse(`${path}: @import needs a quoted stylesheet path or "samva:brand".`);
70
+ continue;
71
+ }
72
+ if (extra) {
73
+ refuse(`${path}: @import ${JSON.stringify(target)} takes no layer, supports or media conditions; the import order is the layering.`);
74
+ continue;
75
+ }
76
+ if (parseBrandSpecifier(target) !== void 0) {
77
+ brandSpecifiers.add(target);
78
+ continue;
79
+ }
80
+ if (!/^\.{0,2}\//.test(target) || !/\.css$/i.test(target.replace(/[?#].*$/, ""))) {
81
+ refuse(`${path}: @import ${JSON.stringify(target)} is not a project stylesheet or "samva:brand". The compiler supplies Tailwind itself; import project .css files by relative path.`);
82
+ continue;
83
+ }
84
+ const resolved = resolveFromDirectory(projectDirectory(path), target.replace(/[?#].*$/, ""));
85
+ const content = resolved === void 0 ? void 0 : files[resolved];
86
+ if (resolved === void 0 || typeof content !== "string") {
87
+ refuse(`${path}: @import ${JSON.stringify(target)} names no stylesheet in the project.`);
88
+ continue;
89
+ }
90
+ if (chain.includes(resolved)) {
91
+ refuse(`${path}: @import ${JSON.stringify(target)} imports itself through ${chain.join(" → ")}.`);
92
+ continue;
93
+ }
94
+ if (stylesheets.has(resolved)) continue;
95
+ stylesheets.set(resolved, content);
96
+ visit(resolved, content, [...chain, resolved]);
97
+ }
98
+ };
99
+ visit(themePath, themeCss, [themePath]);
100
+ return {
101
+ stylesheets,
102
+ brandSpecifiers: [...brandSpecifiers].sort(),
103
+ diagnostics
104
+ };
105
+ };
106
+ //#endregion
107
+ export { resolveThemeImports as a, resolveFromDirectory as i, parseBrandSpecifier as n, projectDirectory as r, BRAND_SPECIFIER as t };
108
+
109
+ //# sourceMappingURL=theme-imports-lvBw3AD1.js.map