@symbo.ls/brender 3.4.11 → 3.5.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.
@@ -115,6 +115,15 @@ const renderCSS = (el, emotion, colorMap, mediaMap) => {
115
115
  css[key] = resolveValue(key, val, colorMap);
116
116
  hasCss = true;
117
117
  }
118
+ const extsCss = getExtendsCSS(el);
119
+ if (extsCss) {
120
+ for (const [k, v] of Object.entries(extsCss)) {
121
+ if (!css[k]) {
122
+ css[k] = v;
123
+ hasCss = true;
124
+ }
125
+ }
126
+ }
118
127
  if (el.style && typeof el.style === "object") {
119
128
  Object.assign(css, el.style);
120
129
  hasCss = true;
@@ -213,8 +222,34 @@ const NON_CSS_PROPS = /* @__PURE__ */ new Set([
213
222
  "__element",
214
223
  "update"
215
224
  ]);
225
+ const EXTENDS_CSS = {
226
+ Flex: { display: "flex" },
227
+ InlineFlex: { display: "inline-flex" },
228
+ Grid: { display: "grid" },
229
+ InlineGrid: { display: "inline-grid" },
230
+ Block: { display: "block" },
231
+ Inline: { display: "inline" }
232
+ };
233
+ const getExtendsCSS = (el) => {
234
+ const exts = el.__ref?.__extends;
235
+ if (!exts || !Array.isArray(exts)) return null;
236
+ for (const ext of exts) {
237
+ if (EXTENDS_CSS[ext]) return EXTENDS_CSS[ext];
238
+ }
239
+ return null;
240
+ };
216
241
  const resolveShorthand = (key, val) => {
217
- if (key === "flexAlign" && typeof val === "string") {
242
+ if (typeof val === "undefined" || val === null) return null;
243
+ if (key === "flow" && typeof val === "string") {
244
+ let [direction, wrap] = (val || "row").split(" ");
245
+ if (val.startsWith("x") || val === "row") direction = "row";
246
+ if (val.startsWith("y") || val === "column") direction = "column";
247
+ return { display: "flex", flexFlow: (direction || "") + " " + (wrap || "") };
248
+ }
249
+ if (key === "wrap") {
250
+ return { display: "flex", flexWrap: val };
251
+ }
252
+ if ((key === "align" || key === "flexAlign") && typeof val === "string") {
218
253
  const [alignItems, justifyContent] = val.split(" ");
219
254
  return { display: "flex", alignItems, justifyContent };
220
255
  }
@@ -222,12 +257,44 @@ const resolveShorthand = (key, val) => {
222
257
  const [alignItems, justifyContent] = val.split(" ");
223
258
  return { display: "grid", alignItems, justifyContent };
224
259
  }
225
- if (key === "round" && val) {
260
+ if (key === "flexFlow" && typeof val === "string") {
261
+ let [direction, wrap] = (val || "row").split(" ");
262
+ if (val.startsWith("x") || val === "row") direction = "row";
263
+ if (val.startsWith("y") || val === "column") direction = "column";
264
+ return { display: "flex", flexFlow: (direction || "") + " " + (wrap || "") };
265
+ }
266
+ if (key === "flexWrap") {
267
+ return { display: "flex", flexWrap: val };
268
+ }
269
+ if (key === "round" || key === "borderRadius" && val) {
226
270
  return { borderRadius: typeof val === "number" ? val + "px" : val };
227
271
  }
228
- if (key === "boxSize" && val) {
229
- return { width: val, height: val };
272
+ if (key === "boxSize" && typeof val === "string") {
273
+ const [height, width] = val.split(" ");
274
+ return { height, width: width || height };
275
+ }
276
+ if (key === "widthRange" && typeof val === "string") {
277
+ const [minWidth, maxWidth] = val.split(" ");
278
+ return { minWidth, maxWidth: maxWidth || minWidth };
279
+ }
280
+ if (key === "heightRange" && typeof val === "string") {
281
+ const [minHeight, maxHeight] = val.split(" ");
282
+ return { minHeight, maxHeight: maxHeight || minHeight };
230
283
  }
284
+ if (key === "column") return { gridColumn: val };
285
+ if (key === "columns") return { gridTemplateColumns: val };
286
+ if (key === "templateColumns") return { gridTemplateColumns: val };
287
+ if (key === "row") return { gridRow: val };
288
+ if (key === "rows") return { gridTemplateRows: val };
289
+ if (key === "templateRows") return { gridTemplateRows: val };
290
+ if (key === "area") return { gridArea: val };
291
+ if (key === "template") return { gridTemplate: val };
292
+ if (key === "templateAreas") return { gridTemplateAreas: val };
293
+ if (key === "autoColumns") return { gridAutoColumns: val };
294
+ if (key === "autoRows") return { gridAutoRows: val };
295
+ if (key === "autoFlow") return { gridAutoFlow: val };
296
+ if (key === "columnStart") return { gridColumnStart: val };
297
+ if (key === "rowStart") return { gridRowStart: val };
231
298
  return null;
232
299
  };
233
300
  const isCSS = (key) => {
@@ -341,11 +408,37 @@ const CSS_PROPERTIES = /* @__PURE__ */ new Set([
341
408
  "gridAutoFlow",
342
409
  "gridAutoColumns",
343
410
  "gridAutoRows",
411
+ "inset",
412
+ "inlineSize",
413
+ "blockSize",
414
+ "minInlineSize",
415
+ "maxInlineSize",
416
+ "minBlockSize",
417
+ "maxBlockSize",
418
+ "paddingBlockStart",
419
+ "paddingBlockEnd",
420
+ "paddingInlineStart",
421
+ "paddingInlineEnd",
422
+ "marginBlockStart",
423
+ "marginBlockEnd",
424
+ "marginInlineStart",
425
+ "marginInlineEnd",
344
426
  "transform",
345
427
  "transformOrigin",
346
428
  "transition",
347
429
  "animation",
430
+ "animationName",
431
+ "animationDuration",
348
432
  "animationDelay",
433
+ "animationTimingFunction",
434
+ "animationFillMode",
435
+ "animationIterationCount",
436
+ "animationPlayState",
437
+ "animationDirection",
438
+ "gridTemplate",
439
+ "gridTemplateAreas",
440
+ "gridColumnStart",
441
+ "gridRowStart",
349
442
  "boxShadow",
350
443
  "outline",
351
444
  "outlineColor",
@@ -39,6 +39,55 @@ var import_metadata = require("./metadata.js");
39
39
  var import_hydrate = require("./hydrate.js");
40
40
  var import_linkedom = require("linkedom");
41
41
  const import_meta = {};
42
+ const UIKIT_STUBS = {
43
+ Box: {},
44
+ Focusable: {},
45
+ Block: { display: "block" },
46
+ Inline: { display: "inline" },
47
+ Flex: { display: "flex" },
48
+ InlineFlex: { display: "inline-flex" },
49
+ Grid: { display: "grid" },
50
+ InlineGrid: { display: "inline-grid" },
51
+ Link: {
52
+ tag: "a",
53
+ attr: {
54
+ href: (el) => el.props?.href,
55
+ target: (el) => el.props?.target,
56
+ rel: (el) => el.props?.rel
57
+ }
58
+ },
59
+ A: { extends: "Link" },
60
+ RouteLink: { extends: "Link" },
61
+ Img: {
62
+ tag: "img",
63
+ attr: {
64
+ src: (el) => el.props?.src,
65
+ alt: (el) => el.props?.alt,
66
+ loading: (el) => el.props?.loading
67
+ }
68
+ },
69
+ Image: { extends: "Img" },
70
+ Button: { tag: "button" },
71
+ FocusableComponent: { tag: "button" },
72
+ Form: { tag: "form" },
73
+ Input: { tag: "input" },
74
+ TextArea: { tag: "textarea" },
75
+ Textarea: { tag: "textarea" },
76
+ Select: { tag: "select" },
77
+ Label: { tag: "label" },
78
+ Iframe: { tag: "iframe" },
79
+ Video: { tag: "video" },
80
+ Audio: { tag: "audio" },
81
+ Canvas: { tag: "canvas" },
82
+ Span: { tag: "span" },
83
+ P: { tag: "p" },
84
+ H1: { tag: "h1" },
85
+ H2: { tag: "h2" },
86
+ H3: { tag: "h3" },
87
+ H4: { tag: "h4" },
88
+ H5: { tag: "h5" },
89
+ H6: { tag: "h6" }
90
+ };
42
91
  const render = async (data, options = {}) => {
43
92
  const { route = "/", state: stateOverrides, context: contextOverrides } = options;
44
93
  const { window, document } = (0, import_env.createEnv)();
@@ -78,9 +127,10 @@ const renderElement = async (elementDef, options = {}) => {
78
127
  const { window, document } = (0, import_env.createEnv)();
79
128
  const body = document.body;
80
129
  const { create } = await import("@domql/element");
130
+ const components = { ...UIKIT_STUBS, ...context.components || {} };
81
131
  (0, import_keys.resetKeys)();
82
132
  const element = create(elementDef, { node: body }, "root", {
83
- context: { document, window, ...context }
133
+ context: { document, window, ...context, components }
84
134
  });
85
135
  (0, import_keys.assignKeys)(body);
86
136
  const registry = (0, import_keys.mapKeysToElements)(element);
@@ -191,7 +241,17 @@ const NON_CSS_PROPS = /* @__PURE__ */ new Set([
191
241
  ]);
192
242
  const camelToKebab = (str) => str.replace(/[A-Z]/g, (m) => "-" + m.toLowerCase());
193
243
  const resolveShorthand = (key, val) => {
194
- if (key === "flexAlign" && typeof val === "string") {
244
+ if (typeof val === "undefined" || val === null) return null;
245
+ if (key === "flow" && typeof val === "string") {
246
+ let [direction, wrap] = (val || "row").split(" ");
247
+ if (val.startsWith("x") || val === "row") direction = "row";
248
+ if (val.startsWith("y") || val === "column") direction = "column";
249
+ return { display: "flex", "flex-flow": (direction || "") + " " + (wrap || "") };
250
+ }
251
+ if (key === "wrap") {
252
+ return { display: "flex", "flex-wrap": val };
253
+ }
254
+ if ((key === "align" || key === "flexAlign") && typeof val === "string") {
195
255
  const [alignItems, justifyContent] = val.split(" ");
196
256
  return { display: "flex", "align-items": alignItems, "justify-content": justifyContent };
197
257
  }
@@ -199,12 +259,44 @@ const resolveShorthand = (key, val) => {
199
259
  const [alignItems, justifyContent] = val.split(" ");
200
260
  return { display: "grid", "align-items": alignItems, "justify-content": justifyContent };
201
261
  }
202
- if (key === "round" && val) {
262
+ if (key === "flexFlow" && typeof val === "string") {
263
+ let [direction, wrap] = (val || "row").split(" ");
264
+ if (val.startsWith("x") || val === "row") direction = "row";
265
+ if (val.startsWith("y") || val === "column") direction = "column";
266
+ return { display: "flex", "flex-flow": (direction || "") + " " + (wrap || "") };
267
+ }
268
+ if (key === "flexWrap") {
269
+ return { display: "flex", "flex-wrap": val };
270
+ }
271
+ if (key === "round" || key === "borderRadius" && val) {
203
272
  return { "border-radius": typeof val === "number" ? val + "px" : val };
204
273
  }
205
- if (key === "boxSize" && val) {
206
- return { width: val, height: val };
274
+ if (key === "boxSize" && typeof val === "string") {
275
+ const [height, width] = val.split(" ");
276
+ return { height, width: width || height };
207
277
  }
278
+ if (key === "widthRange" && typeof val === "string") {
279
+ const [minWidth, maxWidth] = val.split(" ");
280
+ return { "min-width": minWidth, "max-width": maxWidth || minWidth };
281
+ }
282
+ if (key === "heightRange" && typeof val === "string") {
283
+ const [minHeight, maxHeight] = val.split(" ");
284
+ return { "min-height": minHeight, "max-height": maxHeight || minHeight };
285
+ }
286
+ if (key === "column") return { "grid-column": val };
287
+ if (key === "columns") return { "grid-template-columns": val };
288
+ if (key === "templateColumns") return { "grid-template-columns": val };
289
+ if (key === "row") return { "grid-row": val };
290
+ if (key === "rows") return { "grid-template-rows": val };
291
+ if (key === "templateRows") return { "grid-template-rows": val };
292
+ if (key === "area") return { "grid-area": val };
293
+ if (key === "template") return { "grid-template": val };
294
+ if (key === "templateAreas") return { "grid-template-areas": val };
295
+ if (key === "autoColumns") return { "grid-auto-columns": val };
296
+ if (key === "autoRows") return { "grid-auto-rows": val };
297
+ if (key === "autoFlow") return { "grid-auto-flow": val };
298
+ if (key === "columnStart") return { "grid-column-start": val };
299
+ if (key === "rowStart") return { "grid-row-start": val };
208
300
  return null;
209
301
  };
210
302
  const resolveInnerProps = (obj, colorMap) => {
@@ -267,6 +359,22 @@ const renderCSSRule = (selector, { base, mediaRules, pseudoRules }) => {
267
359
  }
268
360
  return lines.join("\n");
269
361
  };
362
+ const EXTENDS_CSS = {
363
+ Flex: { display: "flex" },
364
+ InlineFlex: { display: "inline-flex" },
365
+ Grid: { display: "grid" },
366
+ InlineGrid: { display: "inline-grid" },
367
+ Block: { display: "block" },
368
+ Inline: { display: "inline" }
369
+ };
370
+ const getExtendsCSS = (el) => {
371
+ const exts = el.__ref?.__extends;
372
+ if (!exts || !Array.isArray(exts)) return null;
373
+ for (const ext of exts) {
374
+ if (EXTENDS_CSS[ext]) return EXTENDS_CSS[ext];
375
+ }
376
+ return null;
377
+ };
270
378
  const extractCSS = (element, ds) => {
271
379
  const colorMap = ds?.color || {};
272
380
  const mediaMap = ds?.media || {};
@@ -282,6 +390,13 @@ const extractCSS = (element, ds) => {
282
390
  if (cls && !seen.has(cls)) {
283
391
  seen.add(cls);
284
392
  const cssResult = buildCSSFromProps(props, colorMap, mediaMap);
393
+ const extsCss = getExtendsCSS(el);
394
+ if (extsCss) {
395
+ for (const [k, v] of Object.entries(extsCss)) {
396
+ const kebab = camelToKebab(k);
397
+ if (!cssResult.base[kebab]) cssResult.base[kebab] = v;
398
+ }
399
+ }
285
400
  const has = Object.keys(cssResult.base).length || Object.keys(cssResult.mediaRules).length || Object.keys(cssResult.pseudoRules).length;
286
401
  if (has) rules.push(renderCSSRule("." + cls.split(" ")[0], cssResult));
287
402
  const anim = props.animation || props.animationName;
@@ -92,6 +92,15 @@ const renderCSS = (el, emotion, colorMap, mediaMap) => {
92
92
  css[key] = resolveValue(key, val, colorMap);
93
93
  hasCss = true;
94
94
  }
95
+ const extsCss = getExtendsCSS(el);
96
+ if (extsCss) {
97
+ for (const [k, v] of Object.entries(extsCss)) {
98
+ if (!css[k]) {
99
+ css[k] = v;
100
+ hasCss = true;
101
+ }
102
+ }
103
+ }
95
104
  if (el.style && typeof el.style === "object") {
96
105
  Object.assign(css, el.style);
97
106
  hasCss = true;
@@ -190,8 +199,34 @@ const NON_CSS_PROPS = /* @__PURE__ */ new Set([
190
199
  "__element",
191
200
  "update"
192
201
  ]);
202
+ const EXTENDS_CSS = {
203
+ Flex: { display: "flex" },
204
+ InlineFlex: { display: "inline-flex" },
205
+ Grid: { display: "grid" },
206
+ InlineGrid: { display: "inline-grid" },
207
+ Block: { display: "block" },
208
+ Inline: { display: "inline" }
209
+ };
210
+ const getExtendsCSS = (el) => {
211
+ const exts = el.__ref?.__extends;
212
+ if (!exts || !Array.isArray(exts)) return null;
213
+ for (const ext of exts) {
214
+ if (EXTENDS_CSS[ext]) return EXTENDS_CSS[ext];
215
+ }
216
+ return null;
217
+ };
193
218
  const resolveShorthand = (key, val) => {
194
- if (key === "flexAlign" && typeof val === "string") {
219
+ if (typeof val === "undefined" || val === null) return null;
220
+ if (key === "flow" && typeof val === "string") {
221
+ let [direction, wrap] = (val || "row").split(" ");
222
+ if (val.startsWith("x") || val === "row") direction = "row";
223
+ if (val.startsWith("y") || val === "column") direction = "column";
224
+ return { display: "flex", flexFlow: (direction || "") + " " + (wrap || "") };
225
+ }
226
+ if (key === "wrap") {
227
+ return { display: "flex", flexWrap: val };
228
+ }
229
+ if ((key === "align" || key === "flexAlign") && typeof val === "string") {
195
230
  const [alignItems, justifyContent] = val.split(" ");
196
231
  return { display: "flex", alignItems, justifyContent };
197
232
  }
@@ -199,12 +234,44 @@ const resolveShorthand = (key, val) => {
199
234
  const [alignItems, justifyContent] = val.split(" ");
200
235
  return { display: "grid", alignItems, justifyContent };
201
236
  }
202
- if (key === "round" && val) {
237
+ if (key === "flexFlow" && typeof val === "string") {
238
+ let [direction, wrap] = (val || "row").split(" ");
239
+ if (val.startsWith("x") || val === "row") direction = "row";
240
+ if (val.startsWith("y") || val === "column") direction = "column";
241
+ return { display: "flex", flexFlow: (direction || "") + " " + (wrap || "") };
242
+ }
243
+ if (key === "flexWrap") {
244
+ return { display: "flex", flexWrap: val };
245
+ }
246
+ if (key === "round" || key === "borderRadius" && val) {
203
247
  return { borderRadius: typeof val === "number" ? val + "px" : val };
204
248
  }
205
- if (key === "boxSize" && val) {
206
- return { width: val, height: val };
249
+ if (key === "boxSize" && typeof val === "string") {
250
+ const [height, width] = val.split(" ");
251
+ return { height, width: width || height };
252
+ }
253
+ if (key === "widthRange" && typeof val === "string") {
254
+ const [minWidth, maxWidth] = val.split(" ");
255
+ return { minWidth, maxWidth: maxWidth || minWidth };
256
+ }
257
+ if (key === "heightRange" && typeof val === "string") {
258
+ const [minHeight, maxHeight] = val.split(" ");
259
+ return { minHeight, maxHeight: maxHeight || minHeight };
207
260
  }
261
+ if (key === "column") return { gridColumn: val };
262
+ if (key === "columns") return { gridTemplateColumns: val };
263
+ if (key === "templateColumns") return { gridTemplateColumns: val };
264
+ if (key === "row") return { gridRow: val };
265
+ if (key === "rows") return { gridTemplateRows: val };
266
+ if (key === "templateRows") return { gridTemplateRows: val };
267
+ if (key === "area") return { gridArea: val };
268
+ if (key === "template") return { gridTemplate: val };
269
+ if (key === "templateAreas") return { gridTemplateAreas: val };
270
+ if (key === "autoColumns") return { gridAutoColumns: val };
271
+ if (key === "autoRows") return { gridAutoRows: val };
272
+ if (key === "autoFlow") return { gridAutoFlow: val };
273
+ if (key === "columnStart") return { gridColumnStart: val };
274
+ if (key === "rowStart") return { gridRowStart: val };
208
275
  return null;
209
276
  };
210
277
  const isCSS = (key) => {
@@ -318,11 +385,37 @@ const CSS_PROPERTIES = /* @__PURE__ */ new Set([
318
385
  "gridAutoFlow",
319
386
  "gridAutoColumns",
320
387
  "gridAutoRows",
388
+ "inset",
389
+ "inlineSize",
390
+ "blockSize",
391
+ "minInlineSize",
392
+ "maxInlineSize",
393
+ "minBlockSize",
394
+ "maxBlockSize",
395
+ "paddingBlockStart",
396
+ "paddingBlockEnd",
397
+ "paddingInlineStart",
398
+ "paddingInlineEnd",
399
+ "marginBlockStart",
400
+ "marginBlockEnd",
401
+ "marginInlineStart",
402
+ "marginInlineEnd",
321
403
  "transform",
322
404
  "transformOrigin",
323
405
  "transition",
324
406
  "animation",
407
+ "animationName",
408
+ "animationDuration",
325
409
  "animationDelay",
410
+ "animationTimingFunction",
411
+ "animationFillMode",
412
+ "animationIterationCount",
413
+ "animationPlayState",
414
+ "animationDirection",
415
+ "gridTemplate",
416
+ "gridTemplateAreas",
417
+ "gridColumnStart",
418
+ "gridRowStart",
326
419
  "boxShadow",
327
420
  "outline",
328
421
  "outlineColor",
@@ -3,6 +3,55 @@ 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) => el.props?.src,
29
+ alt: (el) => el.props?.alt,
30
+ loading: (el) => el.props?.loading
31
+ }
32
+ },
33
+ Image: { extends: "Img" },
34
+ Button: { tag: "button" },
35
+ FocusableComponent: { tag: "button" },
36
+ Form: { tag: "form" },
37
+ Input: { tag: "input" },
38
+ TextArea: { tag: "textarea" },
39
+ Textarea: { tag: "textarea" },
40
+ Select: { tag: "select" },
41
+ Label: { tag: "label" },
42
+ Iframe: { tag: "iframe" },
43
+ Video: { tag: "video" },
44
+ Audio: { tag: "audio" },
45
+ Canvas: { tag: "canvas" },
46
+ Span: { tag: "span" },
47
+ P: { tag: "p" },
48
+ H1: { tag: "h1" },
49
+ H2: { tag: "h2" },
50
+ H3: { tag: "h3" },
51
+ H4: { tag: "h4" },
52
+ H5: { tag: "h5" },
53
+ H6: { tag: "h6" }
54
+ };
6
55
  const render = async (data, options = {}) => {
7
56
  const { route = "/", state: stateOverrides, context: contextOverrides } = options;
8
57
  const { window, document } = createEnv();
@@ -42,9 +91,10 @@ const renderElement = async (elementDef, options = {}) => {
42
91
  const { window, document } = createEnv();
43
92
  const body = document.body;
44
93
  const { create } = await import("@domql/element");
94
+ const components = { ...UIKIT_STUBS, ...context.components || {} };
45
95
  resetKeys();
46
96
  const element = create(elementDef, { node: body }, "root", {
47
- context: { document, window, ...context }
97
+ context: { document, window, ...context, components }
48
98
  });
49
99
  assignKeys(body);
50
100
  const registry = mapKeysToElements(element);
@@ -155,7 +205,17 @@ const NON_CSS_PROPS = /* @__PURE__ */ new Set([
155
205
  ]);
156
206
  const camelToKebab = (str) => str.replace(/[A-Z]/g, (m) => "-" + m.toLowerCase());
157
207
  const resolveShorthand = (key, val) => {
158
- if (key === "flexAlign" && typeof val === "string") {
208
+ if (typeof val === "undefined" || val === null) return null;
209
+ if (key === "flow" && typeof val === "string") {
210
+ let [direction, wrap] = (val || "row").split(" ");
211
+ if (val.startsWith("x") || val === "row") direction = "row";
212
+ if (val.startsWith("y") || val === "column") direction = "column";
213
+ return { display: "flex", "flex-flow": (direction || "") + " " + (wrap || "") };
214
+ }
215
+ if (key === "wrap") {
216
+ return { display: "flex", "flex-wrap": val };
217
+ }
218
+ if ((key === "align" || key === "flexAlign") && typeof val === "string") {
159
219
  const [alignItems, justifyContent] = val.split(" ");
160
220
  return { display: "flex", "align-items": alignItems, "justify-content": justifyContent };
161
221
  }
@@ -163,12 +223,44 @@ const resolveShorthand = (key, val) => {
163
223
  const [alignItems, justifyContent] = val.split(" ");
164
224
  return { display: "grid", "align-items": alignItems, "justify-content": justifyContent };
165
225
  }
166
- if (key === "round" && val) {
226
+ if (key === "flexFlow" && typeof val === "string") {
227
+ let [direction, wrap] = (val || "row").split(" ");
228
+ if (val.startsWith("x") || val === "row") direction = "row";
229
+ if (val.startsWith("y") || val === "column") direction = "column";
230
+ return { display: "flex", "flex-flow": (direction || "") + " " + (wrap || "") };
231
+ }
232
+ if (key === "flexWrap") {
233
+ return { display: "flex", "flex-wrap": val };
234
+ }
235
+ if (key === "round" || key === "borderRadius" && val) {
167
236
  return { "border-radius": typeof val === "number" ? val + "px" : val };
168
237
  }
169
- if (key === "boxSize" && val) {
170
- return { width: val, height: val };
238
+ if (key === "boxSize" && typeof val === "string") {
239
+ const [height, width] = val.split(" ");
240
+ return { height, width: width || height };
171
241
  }
242
+ if (key === "widthRange" && typeof val === "string") {
243
+ const [minWidth, maxWidth] = val.split(" ");
244
+ return { "min-width": minWidth, "max-width": maxWidth || minWidth };
245
+ }
246
+ if (key === "heightRange" && typeof val === "string") {
247
+ const [minHeight, maxHeight] = val.split(" ");
248
+ return { "min-height": minHeight, "max-height": maxHeight || minHeight };
249
+ }
250
+ if (key === "column") return { "grid-column": val };
251
+ if (key === "columns") return { "grid-template-columns": val };
252
+ if (key === "templateColumns") return { "grid-template-columns": val };
253
+ if (key === "row") return { "grid-row": val };
254
+ if (key === "rows") return { "grid-template-rows": val };
255
+ if (key === "templateRows") return { "grid-template-rows": val };
256
+ if (key === "area") return { "grid-area": val };
257
+ if (key === "template") return { "grid-template": val };
258
+ if (key === "templateAreas") return { "grid-template-areas": val };
259
+ if (key === "autoColumns") return { "grid-auto-columns": val };
260
+ if (key === "autoRows") return { "grid-auto-rows": val };
261
+ if (key === "autoFlow") return { "grid-auto-flow": val };
262
+ if (key === "columnStart") return { "grid-column-start": val };
263
+ if (key === "rowStart") return { "grid-row-start": val };
172
264
  return null;
173
265
  };
174
266
  const resolveInnerProps = (obj, colorMap) => {
@@ -231,6 +323,22 @@ const renderCSSRule = (selector, { base, mediaRules, pseudoRules }) => {
231
323
  }
232
324
  return lines.join("\n");
233
325
  };
326
+ const EXTENDS_CSS = {
327
+ Flex: { display: "flex" },
328
+ InlineFlex: { display: "inline-flex" },
329
+ Grid: { display: "grid" },
330
+ InlineGrid: { display: "inline-grid" },
331
+ Block: { display: "block" },
332
+ Inline: { display: "inline" }
333
+ };
334
+ const getExtendsCSS = (el) => {
335
+ const exts = el.__ref?.__extends;
336
+ if (!exts || !Array.isArray(exts)) return null;
337
+ for (const ext of exts) {
338
+ if (EXTENDS_CSS[ext]) return EXTENDS_CSS[ext];
339
+ }
340
+ return null;
341
+ };
234
342
  const extractCSS = (element, ds) => {
235
343
  const colorMap = ds?.color || {};
236
344
  const mediaMap = ds?.media || {};
@@ -246,6 +354,13 @@ const extractCSS = (element, ds) => {
246
354
  if (cls && !seen.has(cls)) {
247
355
  seen.add(cls);
248
356
  const cssResult = buildCSSFromProps(props, colorMap, mediaMap);
357
+ const extsCss = getExtendsCSS(el);
358
+ if (extsCss) {
359
+ for (const [k, v] of Object.entries(extsCss)) {
360
+ const kebab = camelToKebab(k);
361
+ if (!cssResult.base[kebab]) cssResult.base[kebab] = v;
362
+ }
363
+ }
249
364
  const has = Object.keys(cssResult.base).length || Object.keys(cssResult.mediaRules).length || Object.keys(cssResult.pseudoRules).length;
250
365
  if (has) rules.push(renderCSSRule("." + cls.split(" ")[0], cssResult));
251
366
  const anim = props.animation || props.animationName;
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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@symbo.ls/brender",
3
- "version": "3.4.11",
3
+ "version": "3.5.0",
4
4
  "license": "MIT",
5
5
  "type": "module",
6
6
  "module": "./dist/esm/index.js",
package/render.js CHANGED
@@ -4,6 +4,59 @@ import { extractMetadata, generateHeadHtml } from './metadata.js'
4
4
  import { hydrate } from './hydrate.js'
5
5
  import { parseHTML } from 'linkedom'
6
6
 
7
+ // ── Minimal uikit stubs ──────────────────────────────────────────────────────
8
+ // Lightweight versions of uikit components so DOMQL can resolve extends chains
9
+ // (tag, display, attrs) without importing the full @symbo.ls/uikit package.
10
+ const UIKIT_STUBS = {
11
+ Box: {},
12
+ Focusable: {},
13
+ Block: { display: 'block' },
14
+ Inline: { display: 'inline' },
15
+ Flex: { display: 'flex' },
16
+ InlineFlex: { display: 'inline-flex' },
17
+ Grid: { display: 'grid' },
18
+ InlineGrid: { display: 'inline-grid' },
19
+ Link: {
20
+ tag: 'a',
21
+ attr: {
22
+ href: (el) => el.props?.href,
23
+ target: (el) => el.props?.target,
24
+ rel: (el) => el.props?.rel
25
+ }
26
+ },
27
+ A: { extends: 'Link' },
28
+ RouteLink: { extends: 'Link' },
29
+ Img: {
30
+ tag: 'img',
31
+ attr: {
32
+ src: (el) => el.props?.src,
33
+ alt: (el) => el.props?.alt,
34
+ loading: (el) => el.props?.loading
35
+ }
36
+ },
37
+ Image: { extends: 'Img' },
38
+ Button: { tag: 'button' },
39
+ FocusableComponent: { tag: 'button' },
40
+ Form: { tag: 'form' },
41
+ Input: { tag: 'input' },
42
+ TextArea: { tag: 'textarea' },
43
+ Textarea: { tag: 'textarea' },
44
+ Select: { tag: 'select' },
45
+ Label: { tag: 'label' },
46
+ Iframe: { tag: 'iframe' },
47
+ Video: { tag: 'video' },
48
+ Audio: { tag: 'audio' },
49
+ Canvas: { tag: 'canvas' },
50
+ Span: { tag: 'span' },
51
+ P: { tag: 'p' },
52
+ H1: { tag: 'h1' },
53
+ H2: { tag: 'h2' },
54
+ H3: { tag: 'h3' },
55
+ H4: { tag: 'h4' },
56
+ H5: { tag: 'h5' },
57
+ H6: { tag: 'h6' }
58
+ }
59
+
7
60
  /**
8
61
  * Renders a Symbols/DomQL project to HTML on the server.
9
62
  *
@@ -89,10 +142,14 @@ export const renderElement = async (elementDef, options = {}) => {
89
142
 
90
143
  const { create } = await import('@domql/element')
91
144
 
145
+ // Merge minimal uikit stubs so DOMQL resolves extends chains
146
+ // (e.g. extends: 'Link' → tag: 'a', extends: 'Flex' → display: flex)
147
+ const components = { ...UIKIT_STUBS, ...(context.components || {}) }
148
+
92
149
  resetKeys()
93
150
 
94
151
  const element = create(elementDef, { node: body }, 'root', {
95
- context: { document, window, ...context }
152
+ context: { document, window, ...context, components }
96
153
  })
97
154
 
98
155
  assignKeys(body)
@@ -216,7 +273,19 @@ const NON_CSS_PROPS = new Set([
216
273
  const camelToKebab = (str) => str.replace(/[A-Z]/g, m => '-' + m.toLowerCase())
217
274
 
218
275
  const resolveShorthand = (key, val) => {
219
- if (key === 'flexAlign' && typeof val === 'string') {
276
+ if (typeof val === 'undefined' || val === null) return null
277
+
278
+ // Flex shorthands
279
+ if (key === 'flow' && typeof val === 'string') {
280
+ let [direction, wrap] = (val || 'row').split(' ')
281
+ if (val.startsWith('x') || val === 'row') direction = 'row'
282
+ if (val.startsWith('y') || val === 'column') direction = 'column'
283
+ return { display: 'flex', 'flex-flow': (direction || '') + ' ' + (wrap || '') }
284
+ }
285
+ if (key === 'wrap') {
286
+ return { display: 'flex', 'flex-wrap': val }
287
+ }
288
+ if ((key === 'align' || key === 'flexAlign') && typeof val === 'string') {
220
289
  const [alignItems, justifyContent] = val.split(' ')
221
290
  return { display: 'flex', 'align-items': alignItems, 'justify-content': justifyContent }
222
291
  }
@@ -224,12 +293,49 @@ const resolveShorthand = (key, val) => {
224
293
  const [alignItems, justifyContent] = val.split(' ')
225
294
  return { display: 'grid', 'align-items': alignItems, 'justify-content': justifyContent }
226
295
  }
227
- if (key === 'round' && val) {
296
+ if (key === 'flexFlow' && typeof val === 'string') {
297
+ let [direction, wrap] = (val || 'row').split(' ')
298
+ if (val.startsWith('x') || val === 'row') direction = 'row'
299
+ if (val.startsWith('y') || val === 'column') direction = 'column'
300
+ return { display: 'flex', 'flex-flow': (direction || '') + ' ' + (wrap || '') }
301
+ }
302
+ if (key === 'flexWrap') {
303
+ return { display: 'flex', 'flex-wrap': val }
304
+ }
305
+
306
+ // Box/size shorthands
307
+ if (key === 'round' || (key === 'borderRadius' && val)) {
228
308
  return { 'border-radius': typeof val === 'number' ? val + 'px' : val }
229
309
  }
230
- if (key === 'boxSize' && val) {
231
- return { width: val, height: val }
310
+ if (key === 'boxSize' && typeof val === 'string') {
311
+ const [height, width] = val.split(' ')
312
+ return { height, width: width || height }
313
+ }
314
+ if (key === 'widthRange' && typeof val === 'string') {
315
+ const [minWidth, maxWidth] = val.split(' ')
316
+ return { 'min-width': minWidth, 'max-width': maxWidth || minWidth }
317
+ }
318
+ if (key === 'heightRange' && typeof val === 'string') {
319
+ const [minHeight, maxHeight] = val.split(' ')
320
+ return { 'min-height': minHeight, 'max-height': maxHeight || minHeight }
232
321
  }
322
+
323
+ // Grid aliases
324
+ if (key === 'column') return { 'grid-column': val }
325
+ if (key === 'columns') return { 'grid-template-columns': val }
326
+ if (key === 'templateColumns') return { 'grid-template-columns': val }
327
+ if (key === 'row') return { 'grid-row': val }
328
+ if (key === 'rows') return { 'grid-template-rows': val }
329
+ if (key === 'templateRows') return { 'grid-template-rows': val }
330
+ if (key === 'area') return { 'grid-area': val }
331
+ if (key === 'template') return { 'grid-template': val }
332
+ if (key === 'templateAreas') return { 'grid-template-areas': val }
333
+ if (key === 'autoColumns') return { 'grid-auto-columns': val }
334
+ if (key === 'autoRows') return { 'grid-auto-rows': val }
335
+ if (key === 'autoFlow') return { 'grid-auto-flow': val }
336
+ if (key === 'columnStart') return { 'grid-column-start': val }
337
+ if (key === 'rowStart') return { 'grid-row-start': val }
338
+
233
339
  return null
234
340
  }
235
341
 
@@ -300,6 +406,25 @@ const renderCSSRule = (selector, { base, mediaRules, pseudoRules }) => {
300
406
  return lines.join('\n')
301
407
  }
302
408
 
409
+ // Map of component names to their implicit CSS from extends
410
+ const EXTENDS_CSS = {
411
+ Flex: { display: 'flex' },
412
+ InlineFlex: { display: 'inline-flex' },
413
+ Grid: { display: 'grid' },
414
+ InlineGrid: { display: 'inline-grid' },
415
+ Block: { display: 'block' },
416
+ Inline: { display: 'inline' }
417
+ }
418
+
419
+ const getExtendsCSS = (el) => {
420
+ const exts = el.__ref?.__extends
421
+ if (!exts || !Array.isArray(exts)) return null
422
+ for (const ext of exts) {
423
+ if (EXTENDS_CSS[ext]) return EXTENDS_CSS[ext]
424
+ }
425
+ return null
426
+ }
427
+
303
428
  const extractCSS = (element, ds) => {
304
429
  const colorMap = ds?.color || {}
305
430
  const mediaMap = ds?.media || {}
@@ -316,6 +441,16 @@ const extractCSS = (element, ds) => {
316
441
  if (cls && !seen.has(cls)) {
317
442
  seen.add(cls)
318
443
  const cssResult = buildCSSFromProps(props, colorMap, mediaMap)
444
+
445
+ // Inject CSS from extends chain (e.g. extends: 'Flex' → display: flex)
446
+ const extsCss = getExtendsCSS(el)
447
+ if (extsCss) {
448
+ for (const [k, v] of Object.entries(extsCss)) {
449
+ const kebab = camelToKebab(k)
450
+ if (!cssResult.base[kebab]) cssResult.base[kebab] = v
451
+ }
452
+ }
453
+
319
454
  const has = Object.keys(cssResult.base).length || Object.keys(cssResult.mediaRules).length || Object.keys(cssResult.pseudoRules).length
320
455
  if (has) rules.push(renderCSSRule('.' + cls.split(' ')[0], cssResult))
321
456