@symbo.ls/brender 3.4.11 → 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.
@@ -3,6 +3,61 @@ import { resetKeys, assignKeys, mapKeysToElements } from "./keys.js";
3
3
  import { extractMetadata, generateHeadHtml } from "./metadata.js";
4
4
  import { hydrate } from "./hydrate.js";
5
5
  import { parseHTML } from "linkedom";
6
+ const UIKIT_STUBS = {
7
+ Box: {},
8
+ Focusable: {},
9
+ Block: { display: "block" },
10
+ Inline: { display: "inline" },
11
+ Flex: { display: "flex" },
12
+ InlineFlex: { display: "inline-flex" },
13
+ Grid: { display: "grid" },
14
+ InlineGrid: { display: "inline-grid" },
15
+ Link: {
16
+ tag: "a",
17
+ attr: {
18
+ href: (el) => el.props?.href,
19
+ target: (el) => el.props?.target,
20
+ rel: (el) => el.props?.rel
21
+ }
22
+ },
23
+ A: { extends: "Link" },
24
+ RouteLink: { extends: "Link" },
25
+ Img: {
26
+ tag: "img",
27
+ attr: {
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
+ },
35
+ alt: (el) => el.props?.alt,
36
+ loading: (el) => el.props?.loading
37
+ }
38
+ },
39
+ Image: { extends: "Img" },
40
+ Button: { tag: "button" },
41
+ FocusableComponent: { tag: "button" },
42
+ Form: { tag: "form" },
43
+ Input: { tag: "input" },
44
+ TextArea: { tag: "textarea" },
45
+ Textarea: { tag: "textarea" },
46
+ Select: { tag: "select" },
47
+ Label: { tag: "label" },
48
+ Iframe: { tag: "iframe" },
49
+ Video: { tag: "video" },
50
+ Audio: { tag: "audio" },
51
+ Canvas: { tag: "canvas" },
52
+ Span: { tag: "span" },
53
+ P: { tag: "p" },
54
+ H1: { tag: "h1" },
55
+ H2: { tag: "h2" },
56
+ H3: { tag: "h3" },
57
+ H4: { tag: "h4" },
58
+ H5: { tag: "h5" },
59
+ H6: { tag: "h6" }
60
+ };
6
61
  const render = async (data, options = {}) => {
7
62
  const { route = "/", state: stateOverrides, context: contextOverrides } = options;
8
63
  const { window, document } = createEnv();
@@ -42,12 +97,23 @@ const renderElement = async (elementDef, options = {}) => {
42
97
  const { window, document } = createEnv();
43
98
  const body = document.body;
44
99
  const { create } = await import("@domql/element");
100
+ const domqlUtils = await import("@domql/utils");
101
+ const components = { ...UIKIT_STUBS, ...context.components || {} };
102
+ const utils = {
103
+ ...domqlUtils,
104
+ ...context.utils || {},
105
+ ...context.functions || {}
106
+ };
45
107
  resetKeys();
46
- const element = create(elementDef, { node: body }, "root", {
47
- context: { document, window, ...context }
48
- });
108
+ let element;
109
+ try {
110
+ element = create(elementDef, { node: body }, "root", {
111
+ context: { document, window, ...context, components, utils }
112
+ });
113
+ } catch (err) {
114
+ }
49
115
  assignKeys(body);
50
- const registry = mapKeysToElements(element);
116
+ const registry = element ? mapKeysToElements(element) : {};
51
117
  const html = body.innerHTML;
52
118
  return { html, registry, element };
53
119
  };
@@ -112,6 +178,138 @@ ${result.html}
112
178
  </html>`;
113
179
  return { html, route, brKeyCount: result.brKeyCount };
114
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
+ };
115
313
  const CSS_COLOR_PROPS = /* @__PURE__ */ new Set([
116
314
  "color",
117
315
  "background",
@@ -151,41 +349,96 @@ const NON_CSS_PROPS = /* @__PURE__ */ new Set([
151
349
  "autofocus",
152
350
  "theme",
153
351
  "__element",
154
- "update"
352
+ "update",
353
+ "childrenAs",
354
+ "childExtends",
355
+ "childProps",
356
+ "children"
155
357
  ]);
156
358
  const camelToKebab = (str) => str.replace(/[A-Z]/g, (m) => "-" + m.toLowerCase());
157
359
  const resolveShorthand = (key, val) => {
158
- if (key === "flexAlign" && typeof val === "string") {
360
+ if (typeof val === "undefined" || val === null) return null;
361
+ if (key === "flow" && typeof val === "string") {
362
+ let [direction, wrap] = (val || "row").split(" ");
363
+ if (val.startsWith("x") || val === "row") direction = "row";
364
+ if (val.startsWith("y") || val === "column") direction = "column";
365
+ return { display: "flex", "flex-flow": (direction || "") + " " + (wrap || "") };
366
+ }
367
+ if (key === "wrap") {
368
+ return { display: "flex", "flex-wrap": val };
369
+ }
370
+ if ((key === "align" || key === "flexAlign") && typeof val === "string") {
159
371
  const [alignItems, justifyContent] = val.split(" ");
160
- 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;
161
375
  }
162
376
  if (key === "gridAlign" && typeof val === "string") {
163
377
  const [alignItems, justifyContent] = val.split(" ");
164
- 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;
165
381
  }
166
- if (key === "round" && val) {
382
+ if (key === "flexFlow" && typeof val === "string") {
383
+ let [direction, wrap] = (val || "row").split(" ");
384
+ if (val.startsWith("x") || val === "row") direction = "row";
385
+ if (val.startsWith("y") || val === "column") direction = "column";
386
+ return { display: "flex", "flex-flow": (direction || "") + " " + (wrap || "") };
387
+ }
388
+ if (key === "flexWrap") {
389
+ return { display: "flex", "flex-wrap": val };
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
+ }
394
+ if (key === "round" || key === "borderRadius" && val) {
167
395
  return { "border-radius": typeof val === "number" ? val + "px" : val };
168
396
  }
169
- if (key === "boxSize" && val) {
170
- return { width: val, height: val };
397
+ if (key === "boxSize" && typeof val === "string") {
398
+ const [height, width] = val.split(" ");
399
+ return { height, width: width || height };
400
+ }
401
+ if (key === "widthRange" && typeof val === "string") {
402
+ const [minWidth, maxWidth] = val.split(" ");
403
+ return { "min-width": minWidth, "max-width": maxWidth || minWidth };
171
404
  }
405
+ if (key === "heightRange" && typeof val === "string") {
406
+ const [minHeight, maxHeight] = val.split(" ");
407
+ return { "min-height": minHeight, "max-height": maxHeight || minHeight };
408
+ }
409
+ if (key === "column") return { "grid-column": val };
410
+ if (key === "columns") return { "grid-template-columns": val };
411
+ if (key === "templateColumns") return { "grid-template-columns": val };
412
+ if (key === "row") return { "grid-row": val };
413
+ if (key === "rows") return { "grid-template-rows": val };
414
+ if (key === "templateRows") return { "grid-template-rows": val };
415
+ if (key === "area") return { "grid-area": val };
416
+ if (key === "template") return { "grid-template": val };
417
+ if (key === "templateAreas") return { "grid-template-areas": val };
418
+ if (key === "autoColumns") return { "grid-auto-columns": val };
419
+ if (key === "autoRows") return { "grid-auto-rows": val };
420
+ if (key === "autoFlow") return { "grid-auto-flow": val };
421
+ if (key === "columnStart") return { "grid-column-start": val };
422
+ if (key === "rowStart") return { "grid-row-start": val };
172
423
  return null;
173
424
  };
174
- const resolveInnerProps = (obj, colorMap) => {
425
+ const resolveInnerProps = (obj, ds) => {
175
426
  const result = {};
176
427
  for (const k in obj) {
177
428
  const v = obj[k];
178
429
  const expanded = resolveShorthand(k, v);
179
430
  if (expanded) {
180
- Object.assign(result, expanded);
431
+ for (const ek in expanded) {
432
+ result[ek] = resolveDSValue(ek, expanded[ek], ds);
433
+ }
181
434
  continue;
182
435
  }
183
436
  if (typeof v !== "string" && typeof v !== "number") continue;
184
- result[camelToKebab(k)] = CSS_COLOR_PROPS.has(k) && colorMap[v] ? colorMap[v] : v;
437
+ result[camelToKebab(k)] = resolveDSValue(k, v, ds);
185
438
  }
186
439
  return result;
187
440
  };
188
- const buildCSSFromProps = (props, colorMap, mediaMap) => {
441
+ const buildCSSFromProps = (props, ds, mediaMap) => {
189
442
  const base = {};
190
443
  const mediaRules = {};
191
444
  const pseudoRules = {};
@@ -194,13 +447,13 @@ const buildCSSFromProps = (props, colorMap, mediaMap) => {
194
447
  if (key.charCodeAt(0) === 64 && typeof val === "object") {
195
448
  const bp = mediaMap?.[key.slice(1)];
196
449
  if (bp) {
197
- const inner = resolveInnerProps(val, colorMap);
450
+ const inner = resolveInnerProps(val, ds);
198
451
  if (Object.keys(inner).length) mediaRules[bp] = inner;
199
452
  }
200
453
  continue;
201
454
  }
202
455
  if (key.charCodeAt(0) === 58 && typeof val === "object") {
203
- const inner = resolveInnerProps(val, colorMap);
456
+ const inner = resolveInnerProps(val, ds);
204
457
  if (Object.keys(inner).length) pseudoRules[key] = inner;
205
458
  continue;
206
459
  }
@@ -209,10 +462,12 @@ const buildCSSFromProps = (props, colorMap, mediaMap) => {
209
462
  if (NON_CSS_PROPS.has(key)) continue;
210
463
  const expanded = resolveShorthand(key, val);
211
464
  if (expanded) {
212
- Object.assign(base, expanded);
465
+ for (const ek in expanded) {
466
+ base[ek] = resolveDSValue(ek, expanded[ek], ds);
467
+ }
213
468
  continue;
214
469
  }
215
- base[camelToKebab(key)] = CSS_COLOR_PROPS.has(key) && colorMap[val] ? colorMap[val] : val;
470
+ base[camelToKebab(key)] = resolveDSValue(key, val, ds);
216
471
  }
217
472
  return { base, mediaRules, pseudoRules };
218
473
  };
@@ -231,8 +486,23 @@ const renderCSSRule = (selector, { base, mediaRules, pseudoRules }) => {
231
486
  }
232
487
  return lines.join("\n");
233
488
  };
489
+ const EXTENDS_CSS = {
490
+ Flex: { display: "flex" },
491
+ InlineFlex: { display: "inline-flex" },
492
+ Grid: { display: "grid" },
493
+ InlineGrid: { display: "inline-grid" },
494
+ Block: { display: "block" },
495
+ Inline: { display: "inline" }
496
+ };
497
+ const getExtendsCSS = (el) => {
498
+ const exts = el.__ref?.__extends;
499
+ if (!exts || !Array.isArray(exts)) return null;
500
+ for (const ext of exts) {
501
+ if (EXTENDS_CSS[ext]) return EXTENDS_CSS[ext];
502
+ }
503
+ return null;
504
+ };
234
505
  const extractCSS = (element, ds) => {
235
- const colorMap = ds?.color || {};
236
506
  const mediaMap = ds?.media || {};
237
507
  const animations = ds?.animation || {};
238
508
  const rules = [];
@@ -245,7 +515,14 @@ const extractCSS = (element, ds) => {
245
515
  const cls = el.node.getAttribute?.("class");
246
516
  if (cls && !seen.has(cls)) {
247
517
  seen.add(cls);
248
- const cssResult = buildCSSFromProps(props, colorMap, mediaMap);
518
+ const cssResult = buildCSSFromProps(props, ds, mediaMap);
519
+ const extsCss = getExtendsCSS(el);
520
+ if (extsCss) {
521
+ for (const [k, v] of Object.entries(extsCss)) {
522
+ const kebab = camelToKebab(k);
523
+ if (!cssResult.base[kebab]) cssResult.base[kebab] = v;
524
+ }
525
+ }
249
526
  const has = Object.keys(cssResult.base).length || Object.keys(cssResult.mediaRules).length || Object.keys(cssResult.pseudoRules).length;
250
527
  if (has) rules.push(renderCSSRule("." + cls.split(" ")[0], cssResult));
251
528
  const anim = props.animation || props.animationName;
@@ -279,6 +556,7 @@ const generateResetCSS = (reset) => {
279
556
  if (!reset) return "";
280
557
  const rules = [];
281
558
  for (const [selector, props] of Object.entries(reset)) {
559
+ if (!props || typeof props !== "object") continue;
282
560
  const decls = Object.entries(props).map(([k, v]) => `${camelToKebab(k)}: ${v}`).join("; ");
283
561
  if (decls) rules.push(`${selector} { ${decls}; }`);
284
562
  }
@@ -289,6 +567,7 @@ const generateFontLinks = (ds) => {
289
567
  const families = ds.font_family || ds.fontFamily || {};
290
568
  const fontNames = /* @__PURE__ */ new Set();
291
569
  for (const val of Object.values(families)) {
570
+ if (typeof val !== "string") continue;
292
571
  const match = val.match(/'([^']+)'/);
293
572
  if (match) fontNames.add(match[1]);
294
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/hydrate.js CHANGED
@@ -152,6 +152,14 @@ const renderCSS = (el, emotion, colorMap, mediaMap) => {
152
152
  hasCss = true
153
153
  }
154
154
 
155
+ // Inject CSS from extends chain (e.g. extends: 'Flex' → display: flex)
156
+ const extsCss = getExtendsCSS(el)
157
+ if (extsCss) {
158
+ for (const [k, v] of Object.entries(extsCss)) {
159
+ if (!css[k]) { css[k] = v; hasCss = true }
160
+ }
161
+ }
162
+
155
163
  // Handle element.style object
156
164
  if (el.style && typeof el.style === 'object') {
157
165
  Object.assign(css, el.style)
@@ -236,9 +244,40 @@ const NON_CSS_PROPS = new Set([
236
244
  'theme', '__element', 'update'
237
245
  ])
238
246
 
247
+ // Map of component names to their implicit CSS from extends
248
+ const EXTENDS_CSS = {
249
+ Flex: { display: 'flex' },
250
+ InlineFlex: { display: 'inline-flex' },
251
+ Grid: { display: 'grid' },
252
+ InlineGrid: { display: 'inline-grid' },
253
+ Block: { display: 'block' },
254
+ Inline: { display: 'inline' }
255
+ }
256
+
257
+ const getExtendsCSS = (el) => {
258
+ const exts = el.__ref?.__extends
259
+ if (!exts || !Array.isArray(exts)) return null
260
+ for (const ext of exts) {
261
+ if (EXTENDS_CSS[ext]) return EXTENDS_CSS[ext]
262
+ }
263
+ return null
264
+ }
265
+
239
266
  // DomQL shorthand props that expand to multiple CSS properties
240
267
  const resolveShorthand = (key, val) => {
241
- if (key === 'flexAlign' && typeof val === 'string') {
268
+ if (typeof val === 'undefined' || val === null) return null
269
+
270
+ // Flex shorthands
271
+ if (key === 'flow' && typeof val === 'string') {
272
+ let [direction, wrap] = (val || 'row').split(' ')
273
+ if (val.startsWith('x') || val === 'row') direction = 'row'
274
+ if (val.startsWith('y') || val === 'column') direction = 'column'
275
+ return { display: 'flex', flexFlow: (direction || '') + ' ' + (wrap || '') }
276
+ }
277
+ if (key === 'wrap') {
278
+ return { display: 'flex', flexWrap: val }
279
+ }
280
+ if ((key === 'align' || key === 'flexAlign') && typeof val === 'string') {
242
281
  const [alignItems, justifyContent] = val.split(' ')
243
282
  return { display: 'flex', alignItems, justifyContent }
244
283
  }
@@ -246,12 +285,49 @@ const resolveShorthand = (key, val) => {
246
285
  const [alignItems, justifyContent] = val.split(' ')
247
286
  return { display: 'grid', alignItems, justifyContent }
248
287
  }
249
- if (key === 'round' && val) {
288
+ if (key === 'flexFlow' && typeof val === 'string') {
289
+ let [direction, wrap] = (val || 'row').split(' ')
290
+ if (val.startsWith('x') || val === 'row') direction = 'row'
291
+ if (val.startsWith('y') || val === 'column') direction = 'column'
292
+ return { display: 'flex', flexFlow: (direction || '') + ' ' + (wrap || '') }
293
+ }
294
+ if (key === 'flexWrap') {
295
+ return { display: 'flex', flexWrap: val }
296
+ }
297
+
298
+ // Box/size shorthands
299
+ if (key === 'round' || (key === 'borderRadius' && val)) {
250
300
  return { borderRadius: typeof val === 'number' ? val + 'px' : val }
251
301
  }
252
- if (key === 'boxSize' && val) {
253
- return { width: val, height: val }
302
+ if (key === 'boxSize' && typeof val === 'string') {
303
+ const [height, width] = val.split(' ')
304
+ return { height, width: width || height }
254
305
  }
306
+ if (key === 'widthRange' && typeof val === 'string') {
307
+ const [minWidth, maxWidth] = val.split(' ')
308
+ return { minWidth, maxWidth: maxWidth || minWidth }
309
+ }
310
+ if (key === 'heightRange' && typeof val === 'string') {
311
+ const [minHeight, maxHeight] = val.split(' ')
312
+ return { minHeight, maxHeight: maxHeight || minHeight }
313
+ }
314
+
315
+ // Grid aliases
316
+ if (key === 'column') return { gridColumn: val }
317
+ if (key === 'columns') return { gridTemplateColumns: val }
318
+ if (key === 'templateColumns') return { gridTemplateColumns: val }
319
+ if (key === 'row') return { gridRow: val }
320
+ if (key === 'rows') return { gridTemplateRows: val }
321
+ if (key === 'templateRows') return { gridTemplateRows: val }
322
+ if (key === 'area') return { gridArea: val }
323
+ if (key === 'template') return { gridTemplate: val }
324
+ if (key === 'templateAreas') return { gridTemplateAreas: val }
325
+ if (key === 'autoColumns') return { gridAutoColumns: val }
326
+ if (key === 'autoRows') return { gridAutoRows: val }
327
+ if (key === 'autoFlow') return { gridAutoFlow: val }
328
+ if (key === 'columnStart') return { gridColumnStart: val }
329
+ if (key === 'rowStart') return { gridRowStart: val }
330
+
255
331
  return null
256
332
  }
257
333
 
@@ -289,7 +365,15 @@ const CSS_PROPERTIES = new Set([
289
365
  'gap', 'rowGap', 'columnGap',
290
366
  'gridTemplateColumns', 'gridTemplateRows', 'gridColumn', 'gridRow',
291
367
  'gridArea', 'gridAutoFlow', 'gridAutoColumns', 'gridAutoRows',
292
- 'transform', 'transformOrigin', 'transition', 'animation', 'animationDelay',
368
+ 'inset',
369
+ 'inlineSize', 'blockSize', 'minInlineSize', 'maxInlineSize', 'minBlockSize', 'maxBlockSize',
370
+ 'paddingBlockStart', 'paddingBlockEnd', 'paddingInlineStart', 'paddingInlineEnd',
371
+ 'marginBlockStart', 'marginBlockEnd', 'marginInlineStart', 'marginInlineEnd',
372
+ 'transform', 'transformOrigin', 'transition',
373
+ 'animation', 'animationName', 'animationDuration', 'animationDelay',
374
+ 'animationTimingFunction', 'animationFillMode', 'animationIterationCount',
375
+ 'animationPlayState', 'animationDirection',
376
+ 'gridTemplate', 'gridTemplateAreas', 'gridColumnStart', 'gridRowStart',
293
377
  'boxShadow', 'outline', 'outlineColor', 'outlineWidth', 'outlineStyle', 'outlineOffset',
294
378
  'whiteSpace', 'wordBreak', 'wordWrap', 'overflowWrap',
295
379
  'visibility', 'boxSizing', 'objectFit', 'objectPosition',
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 {