@symbo.ls/brender 3.5.0 → 3.5.1

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/dist/cjs/env.js CHANGED
@@ -48,6 +48,29 @@ const createEnv = (html = "<!DOCTYPE html><html><head></head><body></body></html
48
48
  window.scrollTo = () => {
49
49
  };
50
50
  }
51
+ const createStorage = () => {
52
+ const store = {};
53
+ return {
54
+ getItem: (k) => store[k] ?? null,
55
+ setItem: (k, v) => {
56
+ store[k] = String(v);
57
+ },
58
+ removeItem: (k) => {
59
+ delete store[k];
60
+ },
61
+ clear: () => {
62
+ for (const k in store) delete store[k];
63
+ },
64
+ get length() {
65
+ return Object.keys(store).length;
66
+ },
67
+ key: (i) => Object.keys(store)[i] ?? null
68
+ };
69
+ };
70
+ if (!window.localStorage) window.localStorage = createStorage();
71
+ if (!window.sessionStorage) window.sessionStorage = createStorage();
72
+ if (!globalThis.localStorage) globalThis.localStorage = window.localStorage;
73
+ if (!globalThis.sessionStorage) globalThis.sessionStorage = window.sessionStorage;
51
74
  globalThis.window = window;
52
75
  globalThis.document = document;
53
76
  globalThis.Node = window.Node || globalThis.Node;
package/dist/cjs/load.js CHANGED
@@ -1,6 +1,8 @@
1
+ var __create = Object.create;
1
2
  var __defProp = Object.defineProperty;
2
3
  var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
3
4
  var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __getProtoOf = Object.getPrototypeOf;
4
6
  var __hasOwnProp = Object.prototype.hasOwnProperty;
5
7
  var __export = (target, all) => {
6
8
  for (var name in all)
@@ -14,6 +16,14 @@ var __copyProps = (to, from, except, desc) => {
14
16
  }
15
17
  return to;
16
18
  };
19
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
20
+ // If the importer is in node compatibility mode or this is not an ESM
21
+ // file that has been converted to a CommonJS file using a Babel-
22
+ // compatible transform (i.e. "__esModule" has not been set), then set
23
+ // "default" to the CommonJS "module.exports" for node compatibility.
24
+ isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
25
+ mod
26
+ ));
17
27
  var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
18
28
  var load_exports = {};
19
29
  __export(load_exports, {
@@ -22,15 +32,51 @@ __export(load_exports, {
22
32
  });
23
33
  module.exports = __toCommonJS(load_exports);
24
34
  var import_path = require("path");
25
- const loadProject = async (projectPath) => {
26
- const symbolsDir = (0, import_path.resolve)(projectPath, "symbols");
27
- const tryImport = async (modulePath) => {
35
+ var import_fs = require("fs");
36
+ var import_os = require("os");
37
+ var import_crypto = require("crypto");
38
+ const bundleAndImport = async (entryPath) => {
39
+ if (!(0, import_fs.existsSync)(entryPath)) return null;
40
+ let esbuild;
41
+ try {
42
+ esbuild = await import("esbuild");
43
+ } catch {
28
44
  try {
29
- return await import(modulePath);
45
+ return await import(entryPath);
30
46
  } catch {
31
47
  return null;
32
48
  }
33
- };
49
+ }
50
+ const outFile = (0, import_path.join)((0, import_os.tmpdir)(), `brender_${(0, import_crypto.randomBytes)(8).toString("hex")}.mjs`);
51
+ try {
52
+ await esbuild.build({
53
+ entryPoints: [entryPath],
54
+ bundle: true,
55
+ format: "esm",
56
+ platform: "node",
57
+ outfile: outFile,
58
+ write: true,
59
+ logLevel: "silent",
60
+ // Mark node builtins as external
61
+ external: ["fs", "path", "os", "crypto", "url", "http", "https", "stream", "util", "events", "buffer", "child_process", "worker_threads", "net", "tls", "dns", "dgram", "zlib", "assert", "querystring", "string_decoder", "readline", "perf_hooks", "async_hooks", "v8", "vm", "cluster", "inspector", "module", "process", "tty"]
62
+ });
63
+ const mod = await import(`file://${outFile}`);
64
+ return mod;
65
+ } catch {
66
+ try {
67
+ return await import(entryPath);
68
+ } catch {
69
+ return null;
70
+ }
71
+ } finally {
72
+ try {
73
+ (0, import_fs.unlinkSync)(outFile);
74
+ } catch {
75
+ }
76
+ }
77
+ };
78
+ const loadProject = async (projectPath) => {
79
+ const symbolsDir = (0, import_path.resolve)(projectPath, "symbols");
34
80
  const [
35
81
  appModule,
36
82
  stateModule,
@@ -44,17 +90,17 @@ const loadProject = async (projectPath) => {
44
90
  designSystemModule,
45
91
  filesModule
46
92
  ] = await Promise.all([
47
- tryImport((0, import_path.join)(symbolsDir, "app.js")),
48
- tryImport((0, import_path.join)(symbolsDir, "state.js")),
49
- tryImport((0, import_path.join)(symbolsDir, "config.js")),
50
- tryImport((0, import_path.join)(symbolsDir, "dependencies.js")),
51
- tryImport((0, import_path.join)(symbolsDir, "components", "index.js")),
52
- tryImport((0, import_path.join)(symbolsDir, "snippets", "index.js")),
53
- tryImport((0, import_path.join)(symbolsDir, "pages", "index.js")),
54
- tryImport((0, import_path.join)(symbolsDir, "functions", "index.js")),
55
- tryImport((0, import_path.join)(symbolsDir, "methods", "index.js")),
56
- tryImport((0, import_path.join)(symbolsDir, "designSystem", "index.js")),
57
- tryImport((0, import_path.join)(symbolsDir, "files", "index.js"))
93
+ bundleAndImport((0, import_path.join)(symbolsDir, "app.js")),
94
+ bundleAndImport((0, import_path.join)(symbolsDir, "state.js")),
95
+ bundleAndImport((0, import_path.join)(symbolsDir, "config.js")),
96
+ bundleAndImport((0, import_path.join)(symbolsDir, "dependencies.js")),
97
+ bundleAndImport((0, import_path.join)(symbolsDir, "components", "index.js")),
98
+ bundleAndImport((0, import_path.join)(symbolsDir, "snippets", "index.js")),
99
+ bundleAndImport((0, import_path.join)(symbolsDir, "pages", "index.js")),
100
+ bundleAndImport((0, import_path.join)(symbolsDir, "functions", "index.js")),
101
+ bundleAndImport((0, import_path.join)(symbolsDir, "methods", "index.js")),
102
+ bundleAndImport((0, import_path.join)(symbolsDir, "designSystem", "index.js")),
103
+ bundleAndImport((0, import_path.join)(symbolsDir, "files", "index.js"))
58
104
  ]);
59
105
  return {
60
106
  app: appModule?.default || {},
@@ -61,7 +61,13 @@ const UIKIT_STUBS = {
61
61
  Img: {
62
62
  tag: "img",
63
63
  attr: {
64
- src: (el) => el.props?.src,
64
+ src: (el) => {
65
+ let src = el.props?.src;
66
+ if (typeof src === "string" && src.includes("{{")) {
67
+ src = el.call("replaceLiteralsWithObjectFields", src, el.state);
68
+ }
69
+ return src;
70
+ },
65
71
  alt: (el) => el.props?.alt,
66
72
  loading: (el) => el.props?.loading
67
73
  }
@@ -127,13 +133,23 @@ const renderElement = async (elementDef, options = {}) => {
127
133
  const { window, document } = (0, import_env.createEnv)();
128
134
  const body = document.body;
129
135
  const { create } = await import("@domql/element");
136
+ const domqlUtils = await import("@domql/utils");
130
137
  const components = { ...UIKIT_STUBS, ...context.components || {} };
138
+ const utils = {
139
+ ...domqlUtils,
140
+ ...context.utils || {},
141
+ ...context.functions || {}
142
+ };
131
143
  (0, import_keys.resetKeys)();
132
- const element = create(elementDef, { node: body }, "root", {
133
- context: { document, window, ...context, components }
134
- });
144
+ let element;
145
+ try {
146
+ element = create(elementDef, { node: body }, "root", {
147
+ context: { document, window, ...context, components, utils }
148
+ });
149
+ } catch (err) {
150
+ }
135
151
  (0, import_keys.assignKeys)(body);
136
- const registry = (0, import_keys.mapKeysToElements)(element);
152
+ const registry = element ? (0, import_keys.mapKeysToElements)(element) : {};
137
153
  const html = body.innerHTML;
138
154
  return { html, registry, element };
139
155
  };
@@ -198,6 +214,138 @@ ${result.html}
198
214
  </html>`;
199
215
  return { html, route, brKeyCount: result.brKeyCount };
200
216
  };
217
+ const LETTER_TO_INDEX = {
218
+ U: -6,
219
+ V: -5,
220
+ W: -4,
221
+ X: -3,
222
+ Y: -2,
223
+ Z: -1,
224
+ A: 0,
225
+ B: 1,
226
+ C: 2,
227
+ D: 3,
228
+ E: 4,
229
+ F: 5,
230
+ G: 6,
231
+ H: 7,
232
+ I: 8,
233
+ J: 9,
234
+ K: 10,
235
+ L: 11,
236
+ M: 12,
237
+ N: 13,
238
+ O: 14,
239
+ P: 15
240
+ };
241
+ const SPACING_PROPS = /* @__PURE__ */ new Set([
242
+ "padding",
243
+ "paddingTop",
244
+ "paddingRight",
245
+ "paddingBottom",
246
+ "paddingLeft",
247
+ "paddingBlock",
248
+ "paddingInline",
249
+ "paddingBlockStart",
250
+ "paddingBlockEnd",
251
+ "paddingInlineStart",
252
+ "paddingInlineEnd",
253
+ "margin",
254
+ "marginTop",
255
+ "marginRight",
256
+ "marginBottom",
257
+ "marginLeft",
258
+ "marginBlock",
259
+ "marginInline",
260
+ "marginBlockStart",
261
+ "marginBlockEnd",
262
+ "marginInlineStart",
263
+ "marginInlineEnd",
264
+ "gap",
265
+ "rowGap",
266
+ "columnGap",
267
+ "top",
268
+ "right",
269
+ "bottom",
270
+ "left",
271
+ "width",
272
+ "height",
273
+ "minWidth",
274
+ "maxWidth",
275
+ "minHeight",
276
+ "maxHeight",
277
+ "flexBasis",
278
+ "fontSize",
279
+ "lineHeight",
280
+ "letterSpacing",
281
+ "borderWidth",
282
+ "borderRadius",
283
+ "outlineWidth",
284
+ "outlineOffset",
285
+ "inset",
286
+ "insetBlock",
287
+ "insetInline",
288
+ "boxSize",
289
+ "round"
290
+ ]);
291
+ const resolveSpacingToken = (token, spacingConfig) => {
292
+ if (!token || typeof token !== "string") return null;
293
+ if (!spacingConfig) return null;
294
+ const base = spacingConfig.base || 16;
295
+ const ratio = spacingConfig.ratio || 1.618;
296
+ const unit = spacingConfig.unit || "px";
297
+ const hasSubSequence = spacingConfig.subSequence !== false;
298
+ if (token.includes(" ")) {
299
+ const parts = token.split(" ").map((part) => {
300
+ if (part === "-" || part === "") return part;
301
+ return resolveSpacingToken(part, spacingConfig) || part;
302
+ });
303
+ return parts.join(" ");
304
+ }
305
+ if (/^(none|auto|inherit|initial|unset|0)$/i.test(token)) return null;
306
+ if (/\d+(px|em|rem|%|vh|vw|vmin|vmax|ch|ex|cm|mm|in|pt|pc|fr|s|ms)$/i.test(token)) return null;
307
+ if (/^(#|rgb|hsl|var\()/i.test(token)) return null;
308
+ const isNegative = token.startsWith("-");
309
+ const abs = isNegative ? token.slice(1) : token;
310
+ const m = abs.match(/^([A-Z])(\d)?$/i);
311
+ if (!m) return null;
312
+ const letter = m[1].toUpperCase();
313
+ const subStep = m[2] ? parseInt(m[2]) : 0;
314
+ const idx = LETTER_TO_INDEX[letter];
315
+ if (idx === void 0) return null;
316
+ let value = base * Math.pow(ratio, idx);
317
+ if (subStep > 0 && hasSubSequence) {
318
+ const next = base * Math.pow(ratio, idx + 1);
319
+ const diff = next - value;
320
+ const subRatio = diff / ratio;
321
+ const first = next - subRatio;
322
+ const second = value + subRatio;
323
+ const middle = (first + second) / 2;
324
+ const subs = ~~next - ~~value > 16 ? [first, middle, second] : [first, second];
325
+ if (subStep <= subs.length) {
326
+ value = subs[subStep - 1];
327
+ }
328
+ }
329
+ const rounded = Math.round(value * 100) / 100;
330
+ const sign = isNegative ? "-" : "";
331
+ return `${sign}${rounded}${unit}`;
332
+ };
333
+ const SPACING_PROPS_KEBAB = new Set(
334
+ [...SPACING_PROPS].map((k) => k.replace(/[A-Z]/g, (m) => "-" + m.toLowerCase()))
335
+ );
336
+ const resolveDSValue = (key, val, ds) => {
337
+ if (typeof val !== "string") return val;
338
+ if (CSS_COLOR_PROPS.has(key)) {
339
+ const colorMap = ds?.color || {};
340
+ if (colorMap[val]) return colorMap[val];
341
+ }
342
+ if (SPACING_PROPS.has(key) || SPACING_PROPS_KEBAB.has(key)) {
343
+ const spacing = ds?.spacing || {};
344
+ const resolved = resolveSpacingToken(val, spacing);
345
+ if (resolved) return resolved;
346
+ }
347
+ return val;
348
+ };
201
349
  const CSS_COLOR_PROPS = /* @__PURE__ */ new Set([
202
350
  "color",
203
351
  "background",
@@ -237,7 +385,11 @@ const NON_CSS_PROPS = /* @__PURE__ */ new Set([
237
385
  "autofocus",
238
386
  "theme",
239
387
  "__element",
240
- "update"
388
+ "update",
389
+ "childrenAs",
390
+ "childExtends",
391
+ "childProps",
392
+ "children"
241
393
  ]);
242
394
  const camelToKebab = (str) => str.replace(/[A-Z]/g, (m) => "-" + m.toLowerCase());
243
395
  const resolveShorthand = (key, val) => {
@@ -253,11 +405,15 @@ const resolveShorthand = (key, val) => {
253
405
  }
254
406
  if ((key === "align" || key === "flexAlign") && typeof val === "string") {
255
407
  const [alignItems, justifyContent] = val.split(" ");
256
- return { display: "flex", "align-items": alignItems, "justify-content": justifyContent };
408
+ const result = { display: "flex", "align-items": alignItems };
409
+ if (justifyContent) result["justify-content"] = justifyContent;
410
+ return result;
257
411
  }
258
412
  if (key === "gridAlign" && typeof val === "string") {
259
413
  const [alignItems, justifyContent] = val.split(" ");
260
- return { display: "grid", "align-items": alignItems, "justify-content": justifyContent };
414
+ const result = { display: "grid", "align-items": alignItems };
415
+ if (justifyContent) result["justify-content"] = justifyContent;
416
+ return result;
261
417
  }
262
418
  if (key === "flexFlow" && typeof val === "string") {
263
419
  let [direction, wrap] = (val || "row").split(" ");
@@ -268,6 +424,9 @@ const resolveShorthand = (key, val) => {
268
424
  if (key === "flexWrap") {
269
425
  return { display: "flex", "flex-wrap": val };
270
426
  }
427
+ if (key === "backgroundImage" && typeof val === "string" && !val.startsWith("url(") && !val.startsWith("linear-gradient") && !val.startsWith("radial-gradient") && !val.startsWith("none")) {
428
+ return { "background-image": `url(${val})` };
429
+ }
271
430
  if (key === "round" || key === "borderRadius" && val) {
272
431
  return { "border-radius": typeof val === "number" ? val + "px" : val };
273
432
  }
@@ -299,21 +458,23 @@ const resolveShorthand = (key, val) => {
299
458
  if (key === "rowStart") return { "grid-row-start": val };
300
459
  return null;
301
460
  };
302
- const resolveInnerProps = (obj, colorMap) => {
461
+ const resolveInnerProps = (obj, ds) => {
303
462
  const result = {};
304
463
  for (const k in obj) {
305
464
  const v = obj[k];
306
465
  const expanded = resolveShorthand(k, v);
307
466
  if (expanded) {
308
- Object.assign(result, expanded);
467
+ for (const ek in expanded) {
468
+ result[ek] = resolveDSValue(ek, expanded[ek], ds);
469
+ }
309
470
  continue;
310
471
  }
311
472
  if (typeof v !== "string" && typeof v !== "number") continue;
312
- result[camelToKebab(k)] = CSS_COLOR_PROPS.has(k) && colorMap[v] ? colorMap[v] : v;
473
+ result[camelToKebab(k)] = resolveDSValue(k, v, ds);
313
474
  }
314
475
  return result;
315
476
  };
316
- const buildCSSFromProps = (props, colorMap, mediaMap) => {
477
+ const buildCSSFromProps = (props, ds, mediaMap) => {
317
478
  const base = {};
318
479
  const mediaRules = {};
319
480
  const pseudoRules = {};
@@ -322,13 +483,13 @@ const buildCSSFromProps = (props, colorMap, mediaMap) => {
322
483
  if (key.charCodeAt(0) === 64 && typeof val === "object") {
323
484
  const bp = mediaMap?.[key.slice(1)];
324
485
  if (bp) {
325
- const inner = resolveInnerProps(val, colorMap);
486
+ const inner = resolveInnerProps(val, ds);
326
487
  if (Object.keys(inner).length) mediaRules[bp] = inner;
327
488
  }
328
489
  continue;
329
490
  }
330
491
  if (key.charCodeAt(0) === 58 && typeof val === "object") {
331
- const inner = resolveInnerProps(val, colorMap);
492
+ const inner = resolveInnerProps(val, ds);
332
493
  if (Object.keys(inner).length) pseudoRules[key] = inner;
333
494
  continue;
334
495
  }
@@ -337,10 +498,12 @@ const buildCSSFromProps = (props, colorMap, mediaMap) => {
337
498
  if (NON_CSS_PROPS.has(key)) continue;
338
499
  const expanded = resolveShorthand(key, val);
339
500
  if (expanded) {
340
- Object.assign(base, expanded);
501
+ for (const ek in expanded) {
502
+ base[ek] = resolveDSValue(ek, expanded[ek], ds);
503
+ }
341
504
  continue;
342
505
  }
343
- base[camelToKebab(key)] = CSS_COLOR_PROPS.has(key) && colorMap[val] ? colorMap[val] : val;
506
+ base[camelToKebab(key)] = resolveDSValue(key, val, ds);
344
507
  }
345
508
  return { base, mediaRules, pseudoRules };
346
509
  };
@@ -376,7 +539,6 @@ const getExtendsCSS = (el) => {
376
539
  return null;
377
540
  };
378
541
  const extractCSS = (element, ds) => {
379
- const colorMap = ds?.color || {};
380
542
  const mediaMap = ds?.media || {};
381
543
  const animations = ds?.animation || {};
382
544
  const rules = [];
@@ -389,7 +551,7 @@ const extractCSS = (element, ds) => {
389
551
  const cls = el.node.getAttribute?.("class");
390
552
  if (cls && !seen.has(cls)) {
391
553
  seen.add(cls);
392
- const cssResult = buildCSSFromProps(props, colorMap, mediaMap);
554
+ const cssResult = buildCSSFromProps(props, ds, mediaMap);
393
555
  const extsCss = getExtendsCSS(el);
394
556
  if (extsCss) {
395
557
  for (const [k, v] of Object.entries(extsCss)) {
@@ -430,6 +592,7 @@ const generateResetCSS = (reset) => {
430
592
  if (!reset) return "";
431
593
  const rules = [];
432
594
  for (const [selector, props] of Object.entries(reset)) {
595
+ if (!props || typeof props !== "object") continue;
433
596
  const decls = Object.entries(props).map(([k, v]) => `${camelToKebab(k)}: ${v}`).join("; ");
434
597
  if (decls) rules.push(`${selector} { ${decls}; }`);
435
598
  }
@@ -440,6 +603,7 @@ const generateFontLinks = (ds) => {
440
603
  const families = ds.font_family || ds.fontFamily || {};
441
604
  const fontNames = /* @__PURE__ */ new Set();
442
605
  for (const val of Object.values(families)) {
606
+ if (typeof val !== "string") continue;
443
607
  const match = val.match(/'([^']+)'/);
444
608
  if (match) fontNames.add(match[1]);
445
609
  }
package/dist/esm/env.js CHANGED
@@ -26,6 +26,29 @@ const createEnv = (html = "<!DOCTYPE html><html><head></head><body></body></html
26
26
  window.scrollTo = () => {
27
27
  };
28
28
  }
29
+ const createStorage = () => {
30
+ const store = {};
31
+ return {
32
+ getItem: (k) => store[k] ?? null,
33
+ setItem: (k, v) => {
34
+ store[k] = String(v);
35
+ },
36
+ removeItem: (k) => {
37
+ delete store[k];
38
+ },
39
+ clear: () => {
40
+ for (const k in store) delete store[k];
41
+ },
42
+ get length() {
43
+ return Object.keys(store).length;
44
+ },
45
+ key: (i) => Object.keys(store)[i] ?? null
46
+ };
47
+ };
48
+ if (!window.localStorage) window.localStorage = createStorage();
49
+ if (!window.sessionStorage) window.sessionStorage = createStorage();
50
+ if (!globalThis.localStorage) globalThis.localStorage = window.localStorage;
51
+ if (!globalThis.sessionStorage) globalThis.sessionStorage = window.sessionStorage;
29
52
  globalThis.window = window;
30
53
  globalThis.document = document;
31
54
  globalThis.Node = window.Node || globalThis.Node;
package/dist/esm/load.js CHANGED
@@ -1,13 +1,49 @@
1
1
  import { resolve, join } from "path";
2
- const loadProject = async (projectPath) => {
3
- const symbolsDir = resolve(projectPath, "symbols");
4
- const tryImport = async (modulePath) => {
2
+ import { existsSync, mkdirSync, writeFileSync, unlinkSync } from "fs";
3
+ import { tmpdir } from "os";
4
+ import { randomBytes } from "crypto";
5
+ const bundleAndImport = async (entryPath) => {
6
+ if (!existsSync(entryPath)) return null;
7
+ let esbuild;
8
+ try {
9
+ esbuild = await import("esbuild");
10
+ } catch {
5
11
  try {
6
- return await import(modulePath);
12
+ return await import(entryPath);
7
13
  } catch {
8
14
  return null;
9
15
  }
10
- };
16
+ }
17
+ const outFile = join(tmpdir(), `brender_${randomBytes(8).toString("hex")}.mjs`);
18
+ try {
19
+ await esbuild.build({
20
+ entryPoints: [entryPath],
21
+ bundle: true,
22
+ format: "esm",
23
+ platform: "node",
24
+ outfile: outFile,
25
+ write: true,
26
+ logLevel: "silent",
27
+ // Mark node builtins as external
28
+ external: ["fs", "path", "os", "crypto", "url", "http", "https", "stream", "util", "events", "buffer", "child_process", "worker_threads", "net", "tls", "dns", "dgram", "zlib", "assert", "querystring", "string_decoder", "readline", "perf_hooks", "async_hooks", "v8", "vm", "cluster", "inspector", "module", "process", "tty"]
29
+ });
30
+ const mod = await import(`file://${outFile}`);
31
+ return mod;
32
+ } catch {
33
+ try {
34
+ return await import(entryPath);
35
+ } catch {
36
+ return null;
37
+ }
38
+ } finally {
39
+ try {
40
+ unlinkSync(outFile);
41
+ } catch {
42
+ }
43
+ }
44
+ };
45
+ const loadProject = async (projectPath) => {
46
+ const symbolsDir = resolve(projectPath, "symbols");
11
47
  const [
12
48
  appModule,
13
49
  stateModule,
@@ -21,17 +57,17 @@ const loadProject = async (projectPath) => {
21
57
  designSystemModule,
22
58
  filesModule
23
59
  ] = await Promise.all([
24
- tryImport(join(symbolsDir, "app.js")),
25
- tryImport(join(symbolsDir, "state.js")),
26
- tryImport(join(symbolsDir, "config.js")),
27
- tryImport(join(symbolsDir, "dependencies.js")),
28
- tryImport(join(symbolsDir, "components", "index.js")),
29
- tryImport(join(symbolsDir, "snippets", "index.js")),
30
- tryImport(join(symbolsDir, "pages", "index.js")),
31
- tryImport(join(symbolsDir, "functions", "index.js")),
32
- tryImport(join(symbolsDir, "methods", "index.js")),
33
- tryImport(join(symbolsDir, "designSystem", "index.js")),
34
- tryImport(join(symbolsDir, "files", "index.js"))
60
+ bundleAndImport(join(symbolsDir, "app.js")),
61
+ bundleAndImport(join(symbolsDir, "state.js")),
62
+ bundleAndImport(join(symbolsDir, "config.js")),
63
+ bundleAndImport(join(symbolsDir, "dependencies.js")),
64
+ bundleAndImport(join(symbolsDir, "components", "index.js")),
65
+ bundleAndImport(join(symbolsDir, "snippets", "index.js")),
66
+ bundleAndImport(join(symbolsDir, "pages", "index.js")),
67
+ bundleAndImport(join(symbolsDir, "functions", "index.js")),
68
+ bundleAndImport(join(symbolsDir, "methods", "index.js")),
69
+ bundleAndImport(join(symbolsDir, "designSystem", "index.js")),
70
+ bundleAndImport(join(symbolsDir, "files", "index.js"))
35
71
  ]);
36
72
  return {
37
73
  app: appModule?.default || {},
@@ -25,7 +25,13 @@ const UIKIT_STUBS = {
25
25
  Img: {
26
26
  tag: "img",
27
27
  attr: {
28
- src: (el) => el.props?.src,
28
+ src: (el) => {
29
+ let src = el.props?.src;
30
+ if (typeof src === "string" && src.includes("{{")) {
31
+ src = el.call("replaceLiteralsWithObjectFields", src, el.state);
32
+ }
33
+ return src;
34
+ },
29
35
  alt: (el) => el.props?.alt,
30
36
  loading: (el) => el.props?.loading
31
37
  }
@@ -91,13 +97,23 @@ const renderElement = async (elementDef, options = {}) => {
91
97
  const { window, document } = createEnv();
92
98
  const body = document.body;
93
99
  const { create } = await import("@domql/element");
100
+ const domqlUtils = await import("@domql/utils");
94
101
  const components = { ...UIKIT_STUBS, ...context.components || {} };
102
+ const utils = {
103
+ ...domqlUtils,
104
+ ...context.utils || {},
105
+ ...context.functions || {}
106
+ };
95
107
  resetKeys();
96
- const element = create(elementDef, { node: body }, "root", {
97
- context: { document, window, ...context, components }
98
- });
108
+ let element;
109
+ try {
110
+ element = create(elementDef, { node: body }, "root", {
111
+ context: { document, window, ...context, components, utils }
112
+ });
113
+ } catch (err) {
114
+ }
99
115
  assignKeys(body);
100
- const registry = mapKeysToElements(element);
116
+ const registry = element ? mapKeysToElements(element) : {};
101
117
  const html = body.innerHTML;
102
118
  return { html, registry, element };
103
119
  };
@@ -162,6 +178,138 @@ ${result.html}
162
178
  </html>`;
163
179
  return { html, route, brKeyCount: result.brKeyCount };
164
180
  };
181
+ const LETTER_TO_INDEX = {
182
+ U: -6,
183
+ V: -5,
184
+ W: -4,
185
+ X: -3,
186
+ Y: -2,
187
+ Z: -1,
188
+ A: 0,
189
+ B: 1,
190
+ C: 2,
191
+ D: 3,
192
+ E: 4,
193
+ F: 5,
194
+ G: 6,
195
+ H: 7,
196
+ I: 8,
197
+ J: 9,
198
+ K: 10,
199
+ L: 11,
200
+ M: 12,
201
+ N: 13,
202
+ O: 14,
203
+ P: 15
204
+ };
205
+ const SPACING_PROPS = /* @__PURE__ */ new Set([
206
+ "padding",
207
+ "paddingTop",
208
+ "paddingRight",
209
+ "paddingBottom",
210
+ "paddingLeft",
211
+ "paddingBlock",
212
+ "paddingInline",
213
+ "paddingBlockStart",
214
+ "paddingBlockEnd",
215
+ "paddingInlineStart",
216
+ "paddingInlineEnd",
217
+ "margin",
218
+ "marginTop",
219
+ "marginRight",
220
+ "marginBottom",
221
+ "marginLeft",
222
+ "marginBlock",
223
+ "marginInline",
224
+ "marginBlockStart",
225
+ "marginBlockEnd",
226
+ "marginInlineStart",
227
+ "marginInlineEnd",
228
+ "gap",
229
+ "rowGap",
230
+ "columnGap",
231
+ "top",
232
+ "right",
233
+ "bottom",
234
+ "left",
235
+ "width",
236
+ "height",
237
+ "minWidth",
238
+ "maxWidth",
239
+ "minHeight",
240
+ "maxHeight",
241
+ "flexBasis",
242
+ "fontSize",
243
+ "lineHeight",
244
+ "letterSpacing",
245
+ "borderWidth",
246
+ "borderRadius",
247
+ "outlineWidth",
248
+ "outlineOffset",
249
+ "inset",
250
+ "insetBlock",
251
+ "insetInline",
252
+ "boxSize",
253
+ "round"
254
+ ]);
255
+ const resolveSpacingToken = (token, spacingConfig) => {
256
+ if (!token || typeof token !== "string") return null;
257
+ if (!spacingConfig) return null;
258
+ const base = spacingConfig.base || 16;
259
+ const ratio = spacingConfig.ratio || 1.618;
260
+ const unit = spacingConfig.unit || "px";
261
+ const hasSubSequence = spacingConfig.subSequence !== false;
262
+ if (token.includes(" ")) {
263
+ const parts = token.split(" ").map((part) => {
264
+ if (part === "-" || part === "") return part;
265
+ return resolveSpacingToken(part, spacingConfig) || part;
266
+ });
267
+ return parts.join(" ");
268
+ }
269
+ if (/^(none|auto|inherit|initial|unset|0)$/i.test(token)) return null;
270
+ if (/\d+(px|em|rem|%|vh|vw|vmin|vmax|ch|ex|cm|mm|in|pt|pc|fr|s|ms)$/i.test(token)) return null;
271
+ if (/^(#|rgb|hsl|var\()/i.test(token)) return null;
272
+ const isNegative = token.startsWith("-");
273
+ const abs = isNegative ? token.slice(1) : token;
274
+ const m = abs.match(/^([A-Z])(\d)?$/i);
275
+ if (!m) return null;
276
+ const letter = m[1].toUpperCase();
277
+ const subStep = m[2] ? parseInt(m[2]) : 0;
278
+ const idx = LETTER_TO_INDEX[letter];
279
+ if (idx === void 0) return null;
280
+ let value = base * Math.pow(ratio, idx);
281
+ if (subStep > 0 && hasSubSequence) {
282
+ const next = base * Math.pow(ratio, idx + 1);
283
+ const diff = next - value;
284
+ const subRatio = diff / ratio;
285
+ const first = next - subRatio;
286
+ const second = value + subRatio;
287
+ const middle = (first + second) / 2;
288
+ const subs = ~~next - ~~value > 16 ? [first, middle, second] : [first, second];
289
+ if (subStep <= subs.length) {
290
+ value = subs[subStep - 1];
291
+ }
292
+ }
293
+ const rounded = Math.round(value * 100) / 100;
294
+ const sign = isNegative ? "-" : "";
295
+ return `${sign}${rounded}${unit}`;
296
+ };
297
+ const SPACING_PROPS_KEBAB = new Set(
298
+ [...SPACING_PROPS].map((k) => k.replace(/[A-Z]/g, (m) => "-" + m.toLowerCase()))
299
+ );
300
+ const resolveDSValue = (key, val, ds) => {
301
+ if (typeof val !== "string") return val;
302
+ if (CSS_COLOR_PROPS.has(key)) {
303
+ const colorMap = ds?.color || {};
304
+ if (colorMap[val]) return colorMap[val];
305
+ }
306
+ if (SPACING_PROPS.has(key) || SPACING_PROPS_KEBAB.has(key)) {
307
+ const spacing = ds?.spacing || {};
308
+ const resolved = resolveSpacingToken(val, spacing);
309
+ if (resolved) return resolved;
310
+ }
311
+ return val;
312
+ };
165
313
  const CSS_COLOR_PROPS = /* @__PURE__ */ new Set([
166
314
  "color",
167
315
  "background",
@@ -201,7 +349,11 @@ const NON_CSS_PROPS = /* @__PURE__ */ new Set([
201
349
  "autofocus",
202
350
  "theme",
203
351
  "__element",
204
- "update"
352
+ "update",
353
+ "childrenAs",
354
+ "childExtends",
355
+ "childProps",
356
+ "children"
205
357
  ]);
206
358
  const camelToKebab = (str) => str.replace(/[A-Z]/g, (m) => "-" + m.toLowerCase());
207
359
  const resolveShorthand = (key, val) => {
@@ -217,11 +369,15 @@ const resolveShorthand = (key, val) => {
217
369
  }
218
370
  if ((key === "align" || key === "flexAlign") && typeof val === "string") {
219
371
  const [alignItems, justifyContent] = val.split(" ");
220
- return { display: "flex", "align-items": alignItems, "justify-content": justifyContent };
372
+ const result = { display: "flex", "align-items": alignItems };
373
+ if (justifyContent) result["justify-content"] = justifyContent;
374
+ return result;
221
375
  }
222
376
  if (key === "gridAlign" && typeof val === "string") {
223
377
  const [alignItems, justifyContent] = val.split(" ");
224
- return { display: "grid", "align-items": alignItems, "justify-content": justifyContent };
378
+ const result = { display: "grid", "align-items": alignItems };
379
+ if (justifyContent) result["justify-content"] = justifyContent;
380
+ return result;
225
381
  }
226
382
  if (key === "flexFlow" && typeof val === "string") {
227
383
  let [direction, wrap] = (val || "row").split(" ");
@@ -232,6 +388,9 @@ const resolveShorthand = (key, val) => {
232
388
  if (key === "flexWrap") {
233
389
  return { display: "flex", "flex-wrap": val };
234
390
  }
391
+ if (key === "backgroundImage" && typeof val === "string" && !val.startsWith("url(") && !val.startsWith("linear-gradient") && !val.startsWith("radial-gradient") && !val.startsWith("none")) {
392
+ return { "background-image": `url(${val})` };
393
+ }
235
394
  if (key === "round" || key === "borderRadius" && val) {
236
395
  return { "border-radius": typeof val === "number" ? val + "px" : val };
237
396
  }
@@ -263,21 +422,23 @@ const resolveShorthand = (key, val) => {
263
422
  if (key === "rowStart") return { "grid-row-start": val };
264
423
  return null;
265
424
  };
266
- const resolveInnerProps = (obj, colorMap) => {
425
+ const resolveInnerProps = (obj, ds) => {
267
426
  const result = {};
268
427
  for (const k in obj) {
269
428
  const v = obj[k];
270
429
  const expanded = resolveShorthand(k, v);
271
430
  if (expanded) {
272
- Object.assign(result, expanded);
431
+ for (const ek in expanded) {
432
+ result[ek] = resolveDSValue(ek, expanded[ek], ds);
433
+ }
273
434
  continue;
274
435
  }
275
436
  if (typeof v !== "string" && typeof v !== "number") continue;
276
- result[camelToKebab(k)] = CSS_COLOR_PROPS.has(k) && colorMap[v] ? colorMap[v] : v;
437
+ result[camelToKebab(k)] = resolveDSValue(k, v, ds);
277
438
  }
278
439
  return result;
279
440
  };
280
- const buildCSSFromProps = (props, colorMap, mediaMap) => {
441
+ const buildCSSFromProps = (props, ds, mediaMap) => {
281
442
  const base = {};
282
443
  const mediaRules = {};
283
444
  const pseudoRules = {};
@@ -286,13 +447,13 @@ const buildCSSFromProps = (props, colorMap, mediaMap) => {
286
447
  if (key.charCodeAt(0) === 64 && typeof val === "object") {
287
448
  const bp = mediaMap?.[key.slice(1)];
288
449
  if (bp) {
289
- const inner = resolveInnerProps(val, colorMap);
450
+ const inner = resolveInnerProps(val, ds);
290
451
  if (Object.keys(inner).length) mediaRules[bp] = inner;
291
452
  }
292
453
  continue;
293
454
  }
294
455
  if (key.charCodeAt(0) === 58 && typeof val === "object") {
295
- const inner = resolveInnerProps(val, colorMap);
456
+ const inner = resolveInnerProps(val, ds);
296
457
  if (Object.keys(inner).length) pseudoRules[key] = inner;
297
458
  continue;
298
459
  }
@@ -301,10 +462,12 @@ const buildCSSFromProps = (props, colorMap, mediaMap) => {
301
462
  if (NON_CSS_PROPS.has(key)) continue;
302
463
  const expanded = resolveShorthand(key, val);
303
464
  if (expanded) {
304
- Object.assign(base, expanded);
465
+ for (const ek in expanded) {
466
+ base[ek] = resolveDSValue(ek, expanded[ek], ds);
467
+ }
305
468
  continue;
306
469
  }
307
- base[camelToKebab(key)] = CSS_COLOR_PROPS.has(key) && colorMap[val] ? colorMap[val] : val;
470
+ base[camelToKebab(key)] = resolveDSValue(key, val, ds);
308
471
  }
309
472
  return { base, mediaRules, pseudoRules };
310
473
  };
@@ -340,7 +503,6 @@ const getExtendsCSS = (el) => {
340
503
  return null;
341
504
  };
342
505
  const extractCSS = (element, ds) => {
343
- const colorMap = ds?.color || {};
344
506
  const mediaMap = ds?.media || {};
345
507
  const animations = ds?.animation || {};
346
508
  const rules = [];
@@ -353,7 +515,7 @@ const extractCSS = (element, ds) => {
353
515
  const cls = el.node.getAttribute?.("class");
354
516
  if (cls && !seen.has(cls)) {
355
517
  seen.add(cls);
356
- const cssResult = buildCSSFromProps(props, colorMap, mediaMap);
518
+ const cssResult = buildCSSFromProps(props, ds, mediaMap);
357
519
  const extsCss = getExtendsCSS(el);
358
520
  if (extsCss) {
359
521
  for (const [k, v] of Object.entries(extsCss)) {
@@ -394,6 +556,7 @@ const generateResetCSS = (reset) => {
394
556
  if (!reset) return "";
395
557
  const rules = [];
396
558
  for (const [selector, props] of Object.entries(reset)) {
559
+ if (!props || typeof props !== "object") continue;
397
560
  const decls = Object.entries(props).map(([k, v]) => `${camelToKebab(k)}: ${v}`).join("; ");
398
561
  if (decls) rules.push(`${selector} { ${decls}; }`);
399
562
  }
@@ -404,6 +567,7 @@ const generateFontLinks = (ds) => {
404
567
  const families = ds.font_family || ds.fontFamily || {};
405
568
  const fontNames = /* @__PURE__ */ new Set();
406
569
  for (const val of Object.values(families)) {
570
+ if (typeof val !== "string") continue;
407
571
  const match = val.match(/'([^']+)'/);
408
572
  if (match) fontNames.add(match[1]);
409
573
  }
package/env.js CHANGED
@@ -31,6 +31,23 @@ export const createEnv = (html = '<!DOCTYPE html><html><head></head><body></body
31
31
  window.scrollTo = () => {}
32
32
  }
33
33
 
34
+ // Storage stubs
35
+ const createStorage = () => {
36
+ const store = {}
37
+ return {
38
+ getItem: (k) => store[k] ?? null,
39
+ setItem: (k, v) => { store[k] = String(v) },
40
+ removeItem: (k) => { delete store[k] },
41
+ clear: () => { for (const k in store) delete store[k] },
42
+ get length () { return Object.keys(store).length },
43
+ key: (i) => Object.keys(store)[i] ?? null
44
+ }
45
+ }
46
+ if (!window.localStorage) window.localStorage = createStorage()
47
+ if (!window.sessionStorage) window.sessionStorage = createStorage()
48
+ if (!globalThis.localStorage) globalThis.localStorage = window.localStorage
49
+ if (!globalThis.sessionStorage) globalThis.sessionStorage = window.sessionStorage
50
+
34
51
  // Expose linkedom constructors on globalThis so @domql/utils isDOMNode
35
52
  // can use instanceof checks (it reads from globalThis.Node, etc.)
36
53
  globalThis.window = window
package/load.js CHANGED
@@ -1,9 +1,56 @@
1
1
  import { resolve, join } from 'path'
2
+ import { existsSync, mkdirSync, writeFileSync, unlinkSync } from 'fs'
3
+ import { tmpdir } from 'os'
4
+ import { randomBytes } from 'crypto'
5
+
6
+ /**
7
+ * Bundles a module entry point with esbuild so that extensionless imports,
8
+ * bare specifiers, and other bundler conventions resolve correctly.
9
+ * Returns the default + named exports of the bundled module, or null on failure.
10
+ */
11
+ const bundleAndImport = async (entryPath) => {
12
+ if (!existsSync(entryPath)) return null
13
+
14
+ let esbuild
15
+ try {
16
+ esbuild = await import('esbuild')
17
+ } catch {
18
+ // Fallback: try raw import if esbuild is not available
19
+ try { return await import(entryPath) } catch { return null }
20
+ }
21
+
22
+ const outFile = join(tmpdir(), `brender_${randomBytes(8).toString('hex')}.mjs`)
23
+
24
+ try {
25
+ await esbuild.build({
26
+ entryPoints: [entryPath],
27
+ bundle: true,
28
+ format: 'esm',
29
+ platform: 'node',
30
+ outfile: outFile,
31
+ write: true,
32
+ logLevel: 'silent',
33
+ // Mark node builtins as external
34
+ external: ['fs', 'path', 'os', 'crypto', 'url', 'http', 'https', 'stream', 'util', 'events', 'buffer', 'child_process', 'worker_threads', 'net', 'tls', 'dns', 'dgram', 'zlib', 'assert', 'querystring', 'string_decoder', 'readline', 'perf_hooks', 'async_hooks', 'v8', 'vm', 'cluster', 'inspector', 'module', 'process', 'tty'],
35
+ })
36
+
37
+ const mod = await import(`file://${outFile}`)
38
+ return mod
39
+ } catch {
40
+ // Fallback: try raw import
41
+ try { return await import(entryPath) } catch { return null }
42
+ } finally {
43
+ try { unlinkSync(outFile) } catch {}
44
+ }
45
+ }
2
46
 
3
47
  /**
4
48
  * Loads a Symbols project from a filesystem path.
5
49
  * Expects the standard symbols/ directory structure.
6
50
  *
51
+ * Uses esbuild to bundle each module so that extensionless imports
52
+ * and other bundler conventions work in Node.js.
53
+ *
7
54
  * Used for prebuild scenarios where brender runs locally
8
55
  * against a project directory (e.g. `smbls build --prerender`).
9
56
  *
@@ -13,14 +60,6 @@ import { resolve, join } from 'path'
13
60
  export const loadProject = async (projectPath) => {
14
61
  const symbolsDir = resolve(projectPath, 'symbols')
15
62
 
16
- const tryImport = async (modulePath) => {
17
- try {
18
- return await import(modulePath)
19
- } catch {
20
- return null
21
- }
22
- }
23
-
24
63
  const [
25
64
  appModule,
26
65
  stateModule,
@@ -34,17 +73,17 @@ export const loadProject = async (projectPath) => {
34
73
  designSystemModule,
35
74
  filesModule
36
75
  ] = await Promise.all([
37
- tryImport(join(symbolsDir, 'app.js')),
38
- tryImport(join(symbolsDir, 'state.js')),
39
- tryImport(join(symbolsDir, 'config.js')),
40
- tryImport(join(symbolsDir, 'dependencies.js')),
41
- tryImport(join(symbolsDir, 'components', 'index.js')),
42
- tryImport(join(symbolsDir, 'snippets', 'index.js')),
43
- tryImport(join(symbolsDir, 'pages', 'index.js')),
44
- tryImport(join(symbolsDir, 'functions', 'index.js')),
45
- tryImport(join(symbolsDir, 'methods', 'index.js')),
46
- tryImport(join(symbolsDir, 'designSystem', 'index.js')),
47
- tryImport(join(symbolsDir, 'files', 'index.js'))
76
+ bundleAndImport(join(symbolsDir, 'app.js')),
77
+ bundleAndImport(join(symbolsDir, 'state.js')),
78
+ bundleAndImport(join(symbolsDir, 'config.js')),
79
+ bundleAndImport(join(symbolsDir, 'dependencies.js')),
80
+ bundleAndImport(join(symbolsDir, 'components', 'index.js')),
81
+ bundleAndImport(join(symbolsDir, 'snippets', 'index.js')),
82
+ bundleAndImport(join(symbolsDir, 'pages', 'index.js')),
83
+ bundleAndImport(join(symbolsDir, 'functions', 'index.js')),
84
+ bundleAndImport(join(symbolsDir, 'methods', 'index.js')),
85
+ bundleAndImport(join(symbolsDir, 'designSystem', 'index.js')),
86
+ bundleAndImport(join(symbolsDir, 'files', 'index.js'))
48
87
  ])
49
88
 
50
89
  return {
package/package.json CHANGED
@@ -1,15 +1,15 @@
1
1
  {
2
2
  "name": "@symbo.ls/brender",
3
- "version": "3.5.0",
3
+ "version": "3.5.1",
4
4
  "license": "MIT",
5
5
  "type": "module",
6
6
  "module": "./dist/esm/index.js",
7
7
  "main": "./dist/cjs/index.js",
8
8
  "exports": {
9
9
  ".": {
10
- "import": "./dist/esm/index.js",
10
+ "import": "./index.js",
11
11
  "require": "./dist/cjs/index.js",
12
- "default": "./dist/esm/index.js"
12
+ "default": "./index.js"
13
13
  },
14
14
  "./hydrate": {
15
15
  "import": "./hydrate.js",
package/render.js CHANGED
@@ -29,7 +29,13 @@ const UIKIT_STUBS = {
29
29
  Img: {
30
30
  tag: 'img',
31
31
  attr: {
32
- src: (el) => el.props?.src,
32
+ src: (el) => {
33
+ let src = el.props?.src
34
+ if (typeof src === 'string' && src.includes('{{')) {
35
+ src = el.call('replaceLiteralsWithObjectFields', src, el.state)
36
+ }
37
+ return src
38
+ },
33
39
  alt: (el) => el.props?.alt,
34
40
  loading: (el) => el.props?.loading
35
41
  }
@@ -141,19 +147,35 @@ export const renderElement = async (elementDef, options = {}) => {
141
147
  const body = document.body
142
148
 
143
149
  const { create } = await import('@domql/element')
150
+ const domqlUtils = await import('@domql/utils')
144
151
 
145
152
  // Merge minimal uikit stubs so DOMQL resolves extends chains
146
153
  // (e.g. extends: 'Link' → tag: 'a', extends: 'Flex' → display: flex)
147
154
  const components = { ...UIKIT_STUBS, ...(context.components || {}) }
148
155
 
156
+ // Register utility functions so element.call() can resolve them
157
+ // (e.g. replaceLiteralsWithObjectFields for {{ }} templates)
158
+ const utils = {
159
+ ...domqlUtils,
160
+ ...(context.utils || {}),
161
+ ...(context.functions || {})
162
+ }
163
+
149
164
  resetKeys()
150
165
 
151
- const element = create(elementDef, { node: body }, 'root', {
152
- context: { document, window, ...context, components }
153
- })
166
+ let element
167
+ try {
168
+ element = create(elementDef, { node: body }, 'root', {
169
+ context: { document, window, ...context, components, utils }
170
+ })
171
+ } catch (err) {
172
+ // Lifecycle events (onRender, onDone, etc.) may throw in SSR
173
+ // because they access browser-only APIs. The DOM tree is built
174
+ // before these fire, so we can still extract HTML.
175
+ }
154
176
 
155
177
  assignKeys(body)
156
- const registry = mapKeysToElements(element)
178
+ const registry = element ? mapKeysToElements(element) : {}
157
179
  const html = body.innerHTML
158
180
 
159
181
  return { html, registry, element }
@@ -255,6 +277,120 @@ ${result.html}
255
277
  return { html, route, brKeyCount: result.brKeyCount }
256
278
  }
257
279
 
280
+ // ── Design system token resolution ──────────────────────────────────────────
281
+
282
+ const LETTER_TO_INDEX = {
283
+ U: -6, V: -5, W: -4, X: -3, Y: -2, Z: -1,
284
+ A: 0, B: 1, C: 2, D: 3, E: 4, F: 5, G: 6, H: 7, I: 8, J: 9,
285
+ K: 10, L: 11, M: 12, N: 13, O: 14, P: 15
286
+ }
287
+
288
+ const SPACING_PROPS = new Set([
289
+ 'padding', 'paddingTop', 'paddingRight', 'paddingBottom', 'paddingLeft',
290
+ 'paddingBlock', 'paddingInline', 'paddingBlockStart', 'paddingBlockEnd',
291
+ 'paddingInlineStart', 'paddingInlineEnd',
292
+ 'margin', 'marginTop', 'marginRight', 'marginBottom', 'marginLeft',
293
+ 'marginBlock', 'marginInline', 'marginBlockStart', 'marginBlockEnd',
294
+ 'marginInlineStart', 'marginInlineEnd',
295
+ 'gap', 'rowGap', 'columnGap',
296
+ 'top', 'right', 'bottom', 'left',
297
+ 'width', 'height', 'minWidth', 'maxWidth', 'minHeight', 'maxHeight',
298
+ 'flexBasis', 'fontSize', 'lineHeight', 'letterSpacing',
299
+ 'borderWidth', 'borderRadius', 'outlineWidth', 'outlineOffset',
300
+ 'inset', 'insetBlock', 'insetInline',
301
+ 'boxSize', 'round'
302
+ ])
303
+
304
+ /**
305
+ * Resolves a spacing token like 'B2', 'A', 'E3' to a px/em value.
306
+ * Uses base * ratio^index for main steps (A=0, B=1, etc.)
307
+ * and sub-ratio interpolation for sub-steps (B1, B2, B3).
308
+ */
309
+ const resolveSpacingToken = (token, spacingConfig) => {
310
+ if (!token || typeof token !== 'string') return null
311
+ if (!spacingConfig) return null
312
+
313
+ const base = spacingConfig.base || 16
314
+ const ratio = spacingConfig.ratio || 1.618
315
+ const unit = spacingConfig.unit || 'px'
316
+ const hasSubSequence = spacingConfig.subSequence !== false
317
+
318
+ // Handle compound values like 'B2 - -' or 'A1 B C1'
319
+ if (token.includes(' ')) {
320
+ const parts = token.split(' ').map(part => {
321
+ if (part === '-' || part === '') return part
322
+ return resolveSpacingToken(part, spacingConfig) || part
323
+ })
324
+ return parts.join(' ')
325
+ }
326
+
327
+ // Skip CSS keywords and values with units
328
+ if (/^(none|auto|inherit|initial|unset|0)$/i.test(token)) return null
329
+ if (/\d+(px|em|rem|%|vh|vw|vmin|vmax|ch|ex|cm|mm|in|pt|pc|fr|s|ms)$/i.test(token)) return null
330
+ // Skip hex colors, rgb(), etc.
331
+ if (/^(#|rgb|hsl|var\()/i.test(token)) return null
332
+
333
+ const isNegative = token.startsWith('-')
334
+ const abs = isNegative ? token.slice(1) : token
335
+
336
+ // Match letter + optional digit: A, B, B2, E3, etc.
337
+ const m = abs.match(/^([A-Z])(\d)?$/i)
338
+ if (!m) return null
339
+
340
+ const letter = m[1].toUpperCase()
341
+ const subStep = m[2] ? parseInt(m[2]) : 0
342
+ const idx = LETTER_TO_INDEX[letter]
343
+ if (idx === undefined) return null
344
+
345
+ let value = base * Math.pow(ratio, idx)
346
+
347
+ if (subStep > 0 && hasSubSequence) {
348
+ const next = base * Math.pow(ratio, idx + 1)
349
+ const diff = next - value
350
+ const subRatio = diff / ratio
351
+ // Sub-steps: 1 = value + (diff - subRatio), 2 = midpoint, 3 = value + subRatio
352
+ const first = next - subRatio
353
+ const second = value + subRatio
354
+ const middle = (first + second) / 2
355
+ const subs = (~~next - ~~value > 16) ? [first, middle, second] : [first, second]
356
+ if (subStep <= subs.length) {
357
+ value = subs[subStep - 1]
358
+ }
359
+ }
360
+
361
+ const rounded = Math.round(value * 100) / 100
362
+ const sign = isNegative ? '-' : ''
363
+ return `${sign}${rounded}${unit}`
364
+ }
365
+
366
+ // Kebab-case versions of spacing props for post-shorthand resolution
367
+ const SPACING_PROPS_KEBAB = new Set(
368
+ [...SPACING_PROPS].map(k => k.replace(/[A-Z]/g, m => '-' + m.toLowerCase()))
369
+ )
370
+
371
+ /**
372
+ * Try to resolve a CSS value through the design system.
373
+ * Returns the resolved value or the original if not a token.
374
+ */
375
+ const resolveDSValue = (key, val, ds) => {
376
+ if (typeof val !== 'string') return val
377
+
378
+ // Color resolution
379
+ if (CSS_COLOR_PROPS.has(key)) {
380
+ const colorMap = ds?.color || {}
381
+ if (colorMap[val]) return colorMap[val]
382
+ }
383
+
384
+ // Spacing resolution (check both camelCase and kebab-case keys)
385
+ if (SPACING_PROPS.has(key) || SPACING_PROPS_KEBAB.has(key)) {
386
+ const spacing = ds?.spacing || {}
387
+ const resolved = resolveSpacingToken(val, spacing)
388
+ if (resolved) return resolved
389
+ }
390
+
391
+ return val
392
+ }
393
+
258
394
  // ── CSS helpers ─────────────────────────────────────────────────────────────
259
395
 
260
396
  const CSS_COLOR_PROPS = new Set([
@@ -267,7 +403,8 @@ const NON_CSS_PROPS = new Set([
267
403
  'href', 'src', 'alt', 'title', 'id', 'name', 'type', 'value', 'placeholder',
268
404
  'target', 'rel', 'loading', 'srcset', 'sizes', 'media', 'role', 'tabindex',
269
405
  'for', 'action', 'method', 'enctype', 'autocomplete', 'autofocus',
270
- 'theme', '__element', 'update'
406
+ 'theme', '__element', 'update',
407
+ 'childrenAs', 'childExtends', 'childProps', 'children'
271
408
  ])
272
409
 
273
410
  const camelToKebab = (str) => str.replace(/[A-Z]/g, m => '-' + m.toLowerCase())
@@ -287,11 +424,15 @@ const resolveShorthand = (key, val) => {
287
424
  }
288
425
  if ((key === 'align' || key === 'flexAlign') && typeof val === 'string') {
289
426
  const [alignItems, justifyContent] = val.split(' ')
290
- return { display: 'flex', 'align-items': alignItems, 'justify-content': justifyContent }
427
+ const result = { display: 'flex', 'align-items': alignItems }
428
+ if (justifyContent) result['justify-content'] = justifyContent
429
+ return result
291
430
  }
292
431
  if (key === 'gridAlign' && typeof val === 'string') {
293
432
  const [alignItems, justifyContent] = val.split(' ')
294
- return { display: 'grid', 'align-items': alignItems, 'justify-content': justifyContent }
433
+ const result = { display: 'grid', 'align-items': alignItems }
434
+ if (justifyContent) result['justify-content'] = justifyContent
435
+ return result
295
436
  }
296
437
  if (key === 'flexFlow' && typeof val === 'string') {
297
438
  let [direction, wrap] = (val || 'row').split(' ')
@@ -303,6 +444,11 @@ const resolveShorthand = (key, val) => {
303
444
  return { display: 'flex', 'flex-wrap': val }
304
445
  }
305
446
 
447
+ // Background image shorthand
448
+ if (key === 'backgroundImage' && typeof val === 'string' && !val.startsWith('url(') && !val.startsWith('linear-gradient') && !val.startsWith('radial-gradient') && !val.startsWith('none')) {
449
+ return { 'background-image': `url(${val})` }
450
+ }
451
+
306
452
  // Box/size shorthands
307
453
  if (key === 'round' || (key === 'borderRadius' && val)) {
308
454
  return { 'border-radius': typeof val === 'number' ? val + 'px' : val }
@@ -339,19 +485,24 @@ const resolveShorthand = (key, val) => {
339
485
  return null
340
486
  }
341
487
 
342
- const resolveInnerProps = (obj, colorMap) => {
488
+ const resolveInnerProps = (obj, ds) => {
343
489
  const result = {}
344
490
  for (const k in obj) {
345
491
  const v = obj[k]
346
492
  const expanded = resolveShorthand(k, v)
347
- if (expanded) { Object.assign(result, expanded); continue }
493
+ if (expanded) {
494
+ for (const ek in expanded) {
495
+ result[ek] = resolveDSValue(ek, expanded[ek], ds)
496
+ }
497
+ continue
498
+ }
348
499
  if (typeof v !== 'string' && typeof v !== 'number') continue
349
- result[camelToKebab(k)] = CSS_COLOR_PROPS.has(k) && colorMap[v] ? colorMap[v] : v
500
+ result[camelToKebab(k)] = resolveDSValue(k, v, ds)
350
501
  }
351
502
  return result
352
503
  }
353
504
 
354
- const buildCSSFromProps = (props, colorMap, mediaMap) => {
505
+ const buildCSSFromProps = (props, ds, mediaMap) => {
355
506
  const base = {}
356
507
  const mediaRules = {}
357
508
  const pseudoRules = {}
@@ -362,14 +513,14 @@ const buildCSSFromProps = (props, colorMap, mediaMap) => {
362
513
  if (key.charCodeAt(0) === 64 && typeof val === 'object') {
363
514
  const bp = mediaMap?.[key.slice(1)]
364
515
  if (bp) {
365
- const inner = resolveInnerProps(val, colorMap)
516
+ const inner = resolveInnerProps(val, ds)
366
517
  if (Object.keys(inner).length) mediaRules[bp] = inner
367
518
  }
368
519
  continue
369
520
  }
370
521
 
371
522
  if (key.charCodeAt(0) === 58 && typeof val === 'object') {
372
- const inner = resolveInnerProps(val, colorMap)
523
+ const inner = resolveInnerProps(val, ds)
373
524
  if (Object.keys(inner).length) pseudoRules[key] = inner
374
525
  continue
375
526
  }
@@ -379,9 +530,14 @@ const buildCSSFromProps = (props, colorMap, mediaMap) => {
379
530
  if (NON_CSS_PROPS.has(key)) continue
380
531
 
381
532
  const expanded = resolveShorthand(key, val)
382
- if (expanded) { Object.assign(base, expanded); continue }
533
+ if (expanded) {
534
+ for (const ek in expanded) {
535
+ base[ek] = resolveDSValue(ek, expanded[ek], ds)
536
+ }
537
+ continue
538
+ }
383
539
 
384
- base[camelToKebab(key)] = CSS_COLOR_PROPS.has(key) && colorMap[val] ? colorMap[val] : val
540
+ base[camelToKebab(key)] = resolveDSValue(key, val, ds)
385
541
  }
386
542
 
387
543
  return { base, mediaRules, pseudoRules }
@@ -426,7 +582,6 @@ const getExtendsCSS = (el) => {
426
582
  }
427
583
 
428
584
  const extractCSS = (element, ds) => {
429
- const colorMap = ds?.color || {}
430
585
  const mediaMap = ds?.media || {}
431
586
  const animations = ds?.animation || {}
432
587
  const rules = []
@@ -440,7 +595,7 @@ const extractCSS = (element, ds) => {
440
595
  const cls = el.node.getAttribute?.('class')
441
596
  if (cls && !seen.has(cls)) {
442
597
  seen.add(cls)
443
- const cssResult = buildCSSFromProps(props, colorMap, mediaMap)
598
+ const cssResult = buildCSSFromProps(props, ds, mediaMap)
444
599
 
445
600
  // Inject CSS from extends chain (e.g. extends: 'Flex' → display: flex)
446
601
  const extsCss = getExtendsCSS(el)
@@ -486,6 +641,7 @@ const generateResetCSS = (reset) => {
486
641
  if (!reset) return ''
487
642
  const rules = []
488
643
  for (const [selector, props] of Object.entries(reset)) {
644
+ if (!props || typeof props !== 'object') continue
489
645
  const decls = Object.entries(props)
490
646
  .map(([k, v]) => `${camelToKebab(k)}: ${v}`)
491
647
  .join('; ')
@@ -501,6 +657,7 @@ const generateFontLinks = (ds) => {
501
657
 
502
658
  // Collect font family names from the design system
503
659
  for (const val of Object.values(families)) {
660
+ if (typeof val !== 'string') continue
504
661
  const match = val.match(/'([^']+)'/)
505
662
  if (match) fontNames.add(match[1])
506
663
  }