@judarn/ui 0.0.0-stage → 0.7.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/index.mjs ADDED
@@ -0,0 +1,650 @@
1
+ var __defProp = Object.defineProperty;
2
+ var __export = (target, all) => {
3
+ for (var name in all)
4
+ __defProp(target, name, { get: all[name], enumerable: true });
5
+ };
6
+
7
+ // src/internal/cx.js
8
+ function cx(...parts) {
9
+ return parts.filter(Boolean).join(" ");
10
+ }
11
+
12
+ // src/Badge/Badge.jsx
13
+ import { jsx } from "react/jsx-runtime";
14
+ var linearize = (c) => c <= 0.04045 ? c / 12.92 : Math.pow((c + 0.055) / 1.055, 2.4);
15
+ function oklchToLinearSrgb(l, c, h, clip) {
16
+ const hr = h * Math.PI / 180;
17
+ const a = c * Math.cos(hr);
18
+ const b = c * Math.sin(hr);
19
+ const l_ = Math.pow(l + 0.3963377774 * a + 0.2158037573 * b, 3);
20
+ const m_ = Math.pow(l - 0.1055613458 * a - 0.0638541728 * b, 3);
21
+ const s_ = Math.pow(l - 0.0894841775 * a - 1.291485548 * b, 3);
22
+ const rgb = [
23
+ 4.0767416621 * l_ - 3.3077115913 * m_ + 0.2309699292 * s_,
24
+ -1.2684380046 * l_ + 2.6097574011 * m_ - 0.3413193965 * s_,
25
+ -0.0041960863 * l_ - 0.7034186147 * m_ + 1.707614701 * s_
26
+ ];
27
+ return clip ? rgb.map((v) => Math.min(1, Math.max(0, v))) : rgb;
28
+ }
29
+ function oklchToLuminance(l, c, h) {
30
+ if (l <= 0) return 0;
31
+ if (l >= 1) return 1;
32
+ const inGamut = (rgb2) => rgb2.every((v) => v >= -1e-6 && v <= 1 + 1e-6);
33
+ let rgb = oklchToLinearSrgb(l, c, h, false);
34
+ if (!inGamut(rgb)) {
35
+ let low = 0;
36
+ let high = c;
37
+ for (let i = 0; i < 24; i++) {
38
+ const mid = 0.5 * (low + high);
39
+ if (inGamut(oklchToLinearSrgb(l, mid, h, false))) low = mid;
40
+ else high = mid;
41
+ }
42
+ rgb = oklchToLinearSrgb(l, low, h, false);
43
+ }
44
+ const [r, g, b] = rgb.map((v) => Math.min(1, Math.max(0, v)));
45
+ return 0.2126 * r + 0.7152 * g + 0.0722 * b;
46
+ }
47
+ function parseLuminance(input) {
48
+ const value = input.trim().toLowerCase();
49
+ if (value.startsWith("#")) {
50
+ const hex = value.slice(1);
51
+ if (!/^([0-9a-f]{3}|[0-9a-f]{4}|[0-9a-f]{6}|[0-9a-f]{8})$/.test(hex)) return null;
52
+ const full = hex.length <= 4 ? [...hex].map((ch) => ch + ch).join("") : hex;
53
+ const [r, g, b] = [0, 2, 4].map((i) => linearize(parseInt(full.slice(i, i + 2), 16) / 255));
54
+ return 0.2126 * r + 0.7152 * g + 0.0722 * b;
55
+ }
56
+ if (value.startsWith("rgb")) {
57
+ const nums = value.replace(/\/.*$/, "").match(/\d+(\.\d+)?/g);
58
+ if (!nums || nums.length < 3) return null;
59
+ const [r, g, b] = nums.slice(0, 3).map((n) => linearize(parseFloat(n) / 255));
60
+ return 0.2126 * r + 0.7152 * g + 0.0722 * b;
61
+ }
62
+ if (value.startsWith("oklch(")) {
63
+ const m = value.match(/^oklch\(\s*([\d.]+)(%?)\s+([\d.]+)\s+([\d.]+)(?:deg)?\s*(?:\/[^)]*)?\)$/);
64
+ if (!m) return null;
65
+ const l = parseFloat(m[1]) / (m[2] ? 100 : 1);
66
+ return oklchToLuminance(l, parseFloat(m[3]), parseFloat(m[4]));
67
+ }
68
+ return null;
69
+ }
70
+ function getContrastingInk(color) {
71
+ const lum = typeof color === "string" ? parseLuminance(color) : null;
72
+ if (lum === null) {
73
+ throw new TypeError(`getContrastingInk: unsupported colour "${String(color)}". Use #hex, rgb() or oklch().`);
74
+ }
75
+ const perceptualCrossover = 0.5;
76
+ return lum > perceptualCrossover ? "#000000" : "#ffffff";
77
+ }
78
+ function Badge({ children, variant = "outline", accentColor, style, className, ...rest }) {
79
+ const cls = cx(
80
+ "badge",
81
+ variant === "solid" && "badge--solid",
82
+ variant === "accent" && "badge--accent",
83
+ variant === "accent-outline" && "badge--accent-outline",
84
+ className
85
+ );
86
+ const inlineStyle = { ...style };
87
+ if (accentColor) {
88
+ if (variant === "accent") {
89
+ inlineStyle.backgroundColor = accentColor;
90
+ inlineStyle.borderColor = accentColor;
91
+ inlineStyle.color = getContrastingInk(accentColor);
92
+ } else if (variant === "accent-outline") {
93
+ inlineStyle.borderColor = accentColor;
94
+ inlineStyle.color = accentColor;
95
+ }
96
+ }
97
+ return /* @__PURE__ */ jsx("span", { ...rest, className: cls, style: Object.keys(inlineStyle).length > 0 ? inlineStyle : void 0, children });
98
+ }
99
+ function Tag({ children, className, ...rest }) {
100
+ return /* @__PURE__ */ jsx("span", { ...rest, className: cx("tag", className), children });
101
+ }
102
+ var Badge_default = Badge;
103
+
104
+ // src/Button/Button.jsx
105
+ import { jsx as jsx2 } from "react/jsx-runtime";
106
+ function Button({
107
+ children,
108
+ variant = "primary",
109
+ size = "md",
110
+ block = false,
111
+ disabled = false,
112
+ type = "button",
113
+ onClick,
114
+ className,
115
+ ...rest
116
+ }) {
117
+ const cls = cx(
118
+ "btn",
119
+ variant === "outline" && "btn--outline",
120
+ variant === "secondary" && "btn--secondary",
121
+ variant === "ghost" && "btn--ghost",
122
+ variant === "accent" && "btn--accent",
123
+ size === "sm" && "btn--sm",
124
+ size === "lg" && "btn--lg",
125
+ block && "btn--block",
126
+ className
127
+ );
128
+ return /* @__PURE__ */ jsx2("button", { ...rest, className: cls, type, disabled, onClick, children });
129
+ }
130
+ var Button_default = Button;
131
+
132
+ // src/Card/Card.jsx
133
+ import { jsx as jsx3, jsxs } from "react/jsx-runtime";
134
+ function Card({
135
+ eyebrow,
136
+ eyebrowCasing = "uppercase",
137
+ title,
138
+ children,
139
+ media,
140
+ actions,
141
+ flat = false,
142
+ compact = false,
143
+ className,
144
+ ...rest
145
+ }) {
146
+ const cls = cx("card", flat && "card--flat", compact && "card--compact", className);
147
+ return /* @__PURE__ */ jsxs("div", { ...rest, className: cls, children: [
148
+ media ? /* @__PURE__ */ jsx3("div", { className: "card__media", children: media }) : null,
149
+ eyebrow ? /* @__PURE__ */ jsx3("span", { className: cx("card__eyebrow", "t-label", eyebrowCasing === "asWritten" && "t-as-written"), children: eyebrow }) : null,
150
+ title ? /* @__PURE__ */ jsx3("div", { className: "card__title", children: title }) : null,
151
+ children ? /* @__PURE__ */ jsx3("div", { className: "card__body", children }) : null,
152
+ actions ? /* @__PURE__ */ jsx3("div", { style: { display: "flex", gap: "var(--gap-tight)" }, children: actions }) : null
153
+ ] });
154
+ }
155
+ var Card_default = Card;
156
+
157
+ // src/DataMatrix/DataMatrix.jsx
158
+ import { jsx as jsx4, jsxs as jsxs2 } from "react/jsx-runtime";
159
+ function DataMatrix({ columns: columns2 = [], rows: rows2 = [], label = "Data Matrix", className, ...rest }) {
160
+ const cols = columns2.map((c) => typeof c === "string" ? { key: c, label: c } : c);
161
+ const totalColumns = cols.length;
162
+ const totalRows = rows2.length;
163
+ return /* @__PURE__ */ jsxs2(
164
+ "table",
165
+ {
166
+ ...rest,
167
+ className: cx("matrix", className),
168
+ role: "table",
169
+ "aria-label": label,
170
+ "aria-colcount": totalColumns,
171
+ "aria-rowcount": totalRows + 1,
172
+ children: [
173
+ /* @__PURE__ */ jsx4("thead", { role: "rowgroup", children: /* @__PURE__ */ jsx4("tr", { role: "row", "aria-rowindex": 1, children: cols.map((c, colIndex) => {
174
+ const widthStyle = c.width != null ? {
175
+ width: typeof c.width === "number" ? `${c.width}px` : c.width,
176
+ minWidth: typeof c.width === "number" ? `${c.width}px` : c.width,
177
+ maxWidth: typeof c.width === "number" ? `${c.width}px` : c.width
178
+ } : void 0;
179
+ return /* @__PURE__ */ jsx4(
180
+ "th",
181
+ {
182
+ scope: "col",
183
+ role: "columnheader",
184
+ "aria-colindex": colIndex + 1,
185
+ "aria-label": c.label,
186
+ className: [c.numeric && "num", c.casing === "asWritten" && "t-as-written"].filter(Boolean).join(" ") || void 0,
187
+ style: widthStyle,
188
+ children: c.label
189
+ },
190
+ c.key
191
+ );
192
+ }) }) }),
193
+ /* @__PURE__ */ jsx4("tbody", { role: "rowgroup", children: rows2.map((row, rowIndex) => {
194
+ const rowNum = rowIndex + 2;
195
+ return /* @__PURE__ */ jsx4(
196
+ "tr",
197
+ {
198
+ role: "row",
199
+ "aria-rowindex": rowNum,
200
+ "aria-label": `Row ${rowIndex + 1} of ${totalRows}`,
201
+ children: cols.map((c, colIndex) => {
202
+ const rawVal = row[c.key];
203
+ const isEmpty = rawVal == null || rawVal === "";
204
+ const displayVal = isEmpty ? "\u200B" : rawVal;
205
+ const widthStyle = c.width != null ? {
206
+ width: typeof c.width === "number" ? `${c.width}px` : c.width,
207
+ minWidth: typeof c.width === "number" ? `${c.width}px` : c.width,
208
+ maxWidth: typeof c.width === "number" ? `${c.width}px` : c.width
209
+ } : void 0;
210
+ return /* @__PURE__ */ jsx4(
211
+ "td",
212
+ {
213
+ role: "cell",
214
+ "aria-colindex": colIndex + 1,
215
+ "aria-label": isEmpty ? `${c.label}: Empty` : void 0,
216
+ className: c.numeric ? "num" : void 0,
217
+ style: widthStyle,
218
+ children: displayVal
219
+ },
220
+ c.key
221
+ );
222
+ })
223
+ },
224
+ rowIndex
225
+ );
226
+ }) })
227
+ ]
228
+ }
229
+ );
230
+ }
231
+ var DataMatrix_default = DataMatrix;
232
+
233
+ // src/DataTable/DataTable.jsx
234
+ import { jsx as jsx5, jsxs as jsxs3 } from "react/jsx-runtime";
235
+ var headerClass = (c) => [c.numeric && "num", c.casing === "asWritten" && "t-as-written"].filter(Boolean).join(" ") || void 0;
236
+ function DataTable({ columns: columns2 = [], rows: rows2 = [], className, ...rest }) {
237
+ const cols = columns2.map((c) => typeof c === "string" ? { key: c, label: c } : c);
238
+ return /* @__PURE__ */ jsxs3("table", { ...rest, className: cx("table", className), children: [
239
+ /* @__PURE__ */ jsx5("thead", { children: /* @__PURE__ */ jsx5("tr", { children: cols.map((c) => /* @__PURE__ */ jsx5("th", { className: headerClass(c), children: c.label }, c.key)) }) }),
240
+ /* @__PURE__ */ jsx5("tbody", { children: rows2.map((row, i) => /* @__PURE__ */ jsx5("tr", { children: cols.map((c) => /* @__PURE__ */ jsx5("td", { className: c.numeric ? "num" : void 0, children: row[c.key] }, c.key)) }, i)) })
241
+ ] });
242
+ }
243
+ var DataTable_default = DataTable;
244
+
245
+ // src/Footer/Footer.jsx
246
+ import { jsx as jsx6, jsxs as jsxs4 } from "react/jsx-runtime";
247
+ function Footer({ brand = "JUDARN", tagline, columns: columns2 = [], headCasing = "uppercase", className, ...rest }) {
248
+ const headClass = headCasing === "asWritten" ? "footer__head t-label t-as-written" : "footer__head t-label";
249
+ return /* @__PURE__ */ jsxs4("footer", { ...rest, className: cx("footer", className), children: [
250
+ /* @__PURE__ */ jsxs4("div", { className: "footer__col", children: [
251
+ /* @__PURE__ */ jsx6("div", { className: "navbar__brand", children: brand }),
252
+ tagline ? /* @__PURE__ */ jsx6("p", { className: "footer__link", style: { marginTop: "8px" }, children: tagline }) : null
253
+ ] }),
254
+ columns2.map((col) => /* @__PURE__ */ jsxs4("div", { className: "footer__col", children: [
255
+ /* @__PURE__ */ jsx6("h2", { className: headClass, children: col.head }),
256
+ (col.links || []).map((l) => {
257
+ const label = typeof l === "string" ? l : l.label;
258
+ const href = typeof l === "string" ? "#" : l.href || "#";
259
+ return /* @__PURE__ */ jsx6("a", { className: "footer__link", href, children: label }, label);
260
+ })
261
+ ] }, col.head))
262
+ ] });
263
+ }
264
+ var Footer_default = Footer;
265
+
266
+ // src/LogoLockup/LogoLockup.jsx
267
+ import { jsx as jsx7, jsxs as jsxs5 } from "react/jsx-runtime";
268
+ function PlaceholderMark({ size = 48 }) {
269
+ return /* @__PURE__ */ jsxs5("svg", { width: size, height: size, viewBox: "0 0 48 48", "aria-hidden": "true", children: [
270
+ /* @__PURE__ */ jsx7("rect", { x: "0", y: "0", width: "48", height: "24", fill: "var(--black)" }),
271
+ /* @__PURE__ */ jsx7("rect", { x: "0", y: "24", width: "24", height: "24", fill: "var(--ink-60)" }),
272
+ /* @__PURE__ */ jsx7("polygon", { points: "24,24 48,24 48,48", fill: "var(--accent)" })
273
+ ] });
274
+ }
275
+ function LogoLockup({
276
+ name = "JUDARN",
277
+ tagline,
278
+ mark,
279
+ variant = "horizontal",
280
+ markSize,
281
+ taglineCasing = "uppercase"
282
+ }) {
283
+ const isIcon = variant === "icon";
284
+ const cls = [
285
+ "lockup",
286
+ variant === "vertical" && "lockup--vertical",
287
+ variant === "reversed" && "lockup--reversed"
288
+ ].filter(Boolean).join(" ");
289
+ const theMark = mark || /* @__PURE__ */ jsx7(PlaceholderMark, { size: markSize || (variant === "vertical" ? 64 : 48) });
290
+ if (isIcon) {
291
+ return /* @__PURE__ */ jsx7("span", { className: "lockup__mark", role: "img", "aria-label": name, children: mark || /* @__PURE__ */ jsx7(PlaceholderMark, { size: markSize || 64 }) });
292
+ }
293
+ return /* @__PURE__ */ jsxs5("div", { className: cls, role: "img", "aria-label": tagline ? `${name} \u2013 ${tagline}` : name, children: [
294
+ /* @__PURE__ */ jsx7("span", { className: "lockup__mark", children: theMark }),
295
+ /* @__PURE__ */ jsxs5("span", { className: "lockup__words", children: [
296
+ /* @__PURE__ */ jsx7("span", { className: "lockup__name", children: name }),
297
+ tagline ? /* @__PURE__ */ jsx7("span", { className: taglineCasing === "asWritten" ? "lockup__tag t-label t-as-written" : "lockup__tag t-label", children: tagline }) : null
298
+ ] })
299
+ ] });
300
+ }
301
+ var LogoLockup_default = LogoLockup;
302
+
303
+ // src/Navbar/Navbar.jsx
304
+ import { jsx as jsx8, jsxs as jsxs6 } from "react/jsx-runtime";
305
+ function Navbar({ brand = "JUDARN", links = [], current, actions, className, ...rest }) {
306
+ return /* @__PURE__ */ jsxs6("nav", { ...rest, className: cx("navbar", className), children: [
307
+ /* @__PURE__ */ jsx8("div", { className: "navbar__brand", children: brand }),
308
+ /* @__PURE__ */ jsxs6("ul", { className: "navbar__links", children: [
309
+ links.map((l) => {
310
+ const label = typeof l === "string" ? l : l.label;
311
+ const href = typeof l === "string" ? "#" : l.href || "#";
312
+ return /* @__PURE__ */ jsx8("li", { children: /* @__PURE__ */ jsx8(
313
+ "a",
314
+ {
315
+ className: "navbar__link",
316
+ href,
317
+ "aria-current": label === current ? "page" : void 0,
318
+ children: label
319
+ }
320
+ ) }, label);
321
+ }),
322
+ actions ? /* @__PURE__ */ jsx8("li", { children: actions }) : null
323
+ ] })
324
+ ] });
325
+ }
326
+ var Navbar_default = Navbar;
327
+
328
+ // src/TextField/TextField.jsx
329
+ import * as React from "react";
330
+ import { jsx as jsx9, jsxs as jsxs7 } from "react/jsx-runtime";
331
+ function TextField({
332
+ label,
333
+ hint,
334
+ error,
335
+ as = "input",
336
+ id,
337
+ invalid = false,
338
+ disabled = false,
339
+ labelCasing = "uppercase",
340
+ className,
341
+ children,
342
+ "aria-describedby": describedByProp,
343
+ ...rest
344
+ }) {
345
+ const generated = React.useId();
346
+ const fid = id || generated;
347
+ const hintId = hint ? `${fid}-hint` : void 0;
348
+ const errorId = error ? `${fid}-error` : void 0;
349
+ const describedBy = [describedByProp, hintId, errorId].filter(Boolean).join(" ") || void 0;
350
+ const common = {
351
+ ...rest,
352
+ id: fid,
353
+ className: cx("field__input", className),
354
+ "aria-invalid": invalid || error ? true : void 0,
355
+ "aria-describedby": describedBy,
356
+ disabled
357
+ };
358
+ let control;
359
+ if (as === "textarea") control = /* @__PURE__ */ jsx9("textarea", { ...common });
360
+ else if (as === "select") control = /* @__PURE__ */ jsx9("select", { ...common, children });
361
+ else control = /* @__PURE__ */ jsx9("input", { ...common });
362
+ return /* @__PURE__ */ jsxs7("div", { className: "field", children: [
363
+ label ? /* @__PURE__ */ jsx9("label", { className: cx("field__label", labelCasing === "asWritten" && "t-as-written"), htmlFor: fid, children: label }) : null,
364
+ control,
365
+ hint ? /* @__PURE__ */ jsx9("span", { className: "field__hint", id: hintId, children: hint }) : null,
366
+ error ? /* @__PURE__ */ jsx9("span", { className: "field__hint is-error", id: errorId, children: error }) : null
367
+ ] });
368
+ }
369
+ var TextField_default = TextField;
370
+
371
+ // src/sdui/index.js
372
+ var sdui_exports = {};
373
+ __export(sdui_exports, {
374
+ SDUI_PROP_ALLOWLIST: () => SDUI_PROP_ALLOWLIST,
375
+ VALID_NODE_TYPES: () => VALID_NODE_TYPES,
376
+ VALID_TOKEN_NAMES: () => VALID_TOKEN_NAMES,
377
+ deserializeComponent: () => deserializeComponent,
378
+ serializeComponent: () => serializeComponent,
379
+ serializeTokens: () => serializeTokens,
380
+ validateSDUINode: () => validateSDUINode
381
+ });
382
+
383
+ // src/sdui/serializer.js
384
+ import * as React2 from "react";
385
+ var VALID_NODE_TYPES = /* @__PURE__ */ new Set([
386
+ "button",
387
+ "badge",
388
+ "tag",
389
+ "card",
390
+ "data-matrix",
391
+ "data-table",
392
+ "text-field",
393
+ "container",
394
+ "text"
395
+ ]);
396
+ var BUTTON_VARIANTS = ["primary", "outline", "secondary", "ghost", "accent"];
397
+ var BADGE_VARIANTS = ["outline", "solid", "accent", "accent-outline"];
398
+ var SIZES = ["sm", "md", "lg"];
399
+ var CASINGS = ["uppercase", "asWritten"];
400
+ var VALID_TOKEN_NAMES = /* @__PURE__ */ new Set([
401
+ // color
402
+ "substrate",
403
+ "surfaceElevated",
404
+ "surfaceRecessed",
405
+ "ink100",
406
+ "ink60",
407
+ "ink30",
408
+ "signalError",
409
+ // radius
410
+ "subtle",
411
+ "control",
412
+ "card",
413
+ "badge",
414
+ // space
415
+ "half",
416
+ "1",
417
+ "2",
418
+ "3",
419
+ "4",
420
+ "6",
421
+ "8",
422
+ "12",
423
+ "16",
424
+ "touch",
425
+ // measure
426
+ "body",
427
+ "compact",
428
+ "sidebar"
429
+ ]);
430
+ var string = { kind: "string" };
431
+ var boolean = { kind: "boolean" };
432
+ var oneOf = (values) => ({ kind: "enum", values });
433
+ var columns = { kind: "columns" };
434
+ var rows = { kind: "rows" };
435
+ var colorValue = { kind: "color" };
436
+ var SDUI_PROP_ALLOWLIST = {
437
+ button: { variant: oneOf(BUTTON_VARIANTS), size: oneOf(SIZES), block: boolean, disabled: boolean, type: oneOf(["button", "submit", "reset"]), token: string },
438
+ badge: { variant: oneOf(BADGE_VARIANTS), accentColor: colorValue, token: string },
439
+ tag: { token: string },
440
+ card: { eyebrow: string, eyebrowCasing: oneOf(CASINGS), title: string, flat: boolean, compact: boolean, token: string },
441
+ "data-matrix": { columns, rows, label: string, token: string },
442
+ "data-table": { columns, rows, token: string },
443
+ "text-field": {
444
+ label: string,
445
+ hint: string,
446
+ error: string,
447
+ as: oneOf(["input", "textarea", "select"]),
448
+ invalid: boolean,
449
+ disabled: boolean,
450
+ labelCasing: oneOf(CASINGS),
451
+ placeholder: string,
452
+ name: string,
453
+ type: oneOf(["text", "email", "number", "password", "search", "tel", "url"]),
454
+ defaultValue: string,
455
+ token: string
456
+ },
457
+ container: { token: string },
458
+ text: { token: string }
459
+ };
460
+ var COLOR_PATTERN = /^(#[0-9a-f]{3,8}|rgba?\([\d\s.,%/]+\)|oklch\([\d\s.%/deg]+\))$/i;
461
+ var COLUMN_KEYS = /* @__PURE__ */ new Set(["key", "label", "numeric", "width", "casing"]);
462
+ var fail = (message) => {
463
+ throw new Error(message);
464
+ };
465
+ function validateColumns(value, where) {
466
+ if (!Array.isArray(value)) fail(`${where}: "columns" must be an array.`);
467
+ for (const col of value) {
468
+ if (typeof col === "string") continue;
469
+ if (!col || typeof col !== "object" || Array.isArray(col)) fail(`${where}: a column must be a string or an object.`);
470
+ for (const [k, v] of Object.entries(col)) {
471
+ if (!COLUMN_KEYS.has(k)) fail(`${where}: unsupported column key "${k}".`);
472
+ if ((k === "key" || k === "label") && typeof v !== "string") fail(`${where}: column "${k}" must be a string.`);
473
+ if (k === "numeric" && typeof v !== "boolean") fail(`${where}: column "numeric" must be a boolean.`);
474
+ if (k === "width" && typeof v !== "number" && typeof v !== "string") fail(`${where}: column "width" must be a number or string.`);
475
+ if (k === "casing" && !CASINGS.includes(v)) fail(`${where}: column "casing" must be one of ${CASINGS.join(", ")}.`);
476
+ }
477
+ if (typeof col.key !== "string" || typeof col.label !== "string") fail(`${where}: a column needs "key" and "label" strings.`);
478
+ }
479
+ }
480
+ function validateRows(value, where) {
481
+ if (!Array.isArray(value)) fail(`${where}: "rows" must be an array.`);
482
+ for (const row of value) {
483
+ if (!row || typeof row !== "object" || Array.isArray(row)) fail(`${where}: a row must be an object.`);
484
+ for (const cell of Object.values(row)) {
485
+ if (cell !== null && !["string", "number", "boolean"].includes(typeof cell)) fail(`${where}: row cells must be strings, numbers, booleans or null.`);
486
+ }
487
+ }
488
+ }
489
+ function validateProp(type, name, spec, value) {
490
+ const where = `SDUI ${type} prop "${name}"`;
491
+ switch (spec.kind) {
492
+ case "string":
493
+ if (typeof value !== "string") fail(`${where} must be a string.`);
494
+ break;
495
+ case "boolean":
496
+ if (typeof value !== "boolean") fail(`${where} must be a boolean.`);
497
+ break;
498
+ case "enum":
499
+ if (!spec.values.includes(value)) fail(`Unsupported ${where}: "${value}". Allowed: ${spec.values.join(", ")}`);
500
+ break;
501
+ case "color":
502
+ if (typeof value !== "string" || !COLOR_PATTERN.test(value)) fail(`${where} must be a #hex, rgb() or oklch() colour.`);
503
+ break;
504
+ case "columns":
505
+ validateColumns(value, where);
506
+ break;
507
+ case "rows":
508
+ validateRows(value, where);
509
+ break;
510
+ default:
511
+ fail(`${where} has an unknown spec.`);
512
+ }
513
+ if (name === "token" && !VALID_TOKEN_NAMES.has(value)) fail(`Unsupported SDUI token reference: "${value}".`);
514
+ }
515
+ function serializeTokens(tokens) {
516
+ if (!tokens || typeof tokens !== "object") {
517
+ throw new Error("Invalid tokens object provided for SDUI serialization.");
518
+ }
519
+ const sduiTokens = {
520
+ $schema: "https://judarn.design/schemas/sdui/tokens/v1.json",
521
+ space: {},
522
+ measure: {},
523
+ border: {},
524
+ radius: {},
525
+ color: {}
526
+ };
527
+ if (tokens.space) {
528
+ for (const [key, val] of Object.entries(tokens.space)) {
529
+ sduiTokens.space[key] = val.$value;
530
+ }
531
+ }
532
+ if (tokens.measure) {
533
+ for (const [key, val] of Object.entries(tokens.measure)) {
534
+ sduiTokens.measure[key] = val.$value;
535
+ }
536
+ }
537
+ if (tokens.border) {
538
+ for (const [key, val] of Object.entries(tokens.border)) {
539
+ sduiTokens.border[key] = val.$value;
540
+ }
541
+ }
542
+ if (tokens.radius) {
543
+ for (const [key, val] of Object.entries(tokens.radius)) {
544
+ sduiTokens.radius[key] = val.$value;
545
+ }
546
+ }
547
+ if (tokens.color) {
548
+ for (const [key, val] of Object.entries(tokens.color)) {
549
+ sduiTokens.color[key] = {
550
+ light: val.light?.$value,
551
+ dark: val.dark?.$value,
552
+ lightHighContrast: val.lightHighContrast?.$value,
553
+ darkHighContrast: val.darkHighContrast?.$value
554
+ };
555
+ }
556
+ }
557
+ return sduiTokens;
558
+ }
559
+ function validateSDUINode(node) {
560
+ if (!node || typeof node !== "object" || Array.isArray(node)) {
561
+ fail("SDUI Node must be a non-null object.");
562
+ }
563
+ if (!node.type || typeof node.type !== "string") {
564
+ fail('SDUI Node missing required "type" string property.');
565
+ }
566
+ if (!VALID_NODE_TYPES.has(node.type)) {
567
+ fail(`Unsupported SDUI node type: "${node.type}". Allowed: ${Array.from(VALID_NODE_TYPES).join(", ")}`);
568
+ }
569
+ if (node.props !== void 0) {
570
+ if (!node.props || typeof node.props !== "object" || Array.isArray(node.props)) fail("SDUI Node props must be an object.");
571
+ const allowed = SDUI_PROP_ALLOWLIST[node.type];
572
+ for (const [name, value] of Object.entries(node.props)) {
573
+ if (!Object.prototype.hasOwnProperty.call(allowed, name)) {
574
+ fail(`Unsupported SDUI prop "${name}" on "${node.type}". Allowed: ${Object.keys(allowed).join(", ") || "none"}`);
575
+ }
576
+ validateProp(node.type, name, allowed[name], value);
577
+ }
578
+ }
579
+ if (node.content !== void 0 && typeof node.content !== "string") {
580
+ fail("SDUI Node content must be a string.");
581
+ }
582
+ if (node.children !== void 0) {
583
+ if (!Array.isArray(node.children)) fail("SDUI Node children must be an array.");
584
+ for (const child of node.children) validateSDUINode(child);
585
+ }
586
+ return true;
587
+ }
588
+ function serializeComponent(type, props = {}, children = [], content = void 0) {
589
+ const node = { type, props: { ...props } };
590
+ if (content !== void 0) node.content = String(content);
591
+ if (Array.isArray(children) && children.length > 0) node.children = children;
592
+ validateSDUINode(node);
593
+ return node;
594
+ }
595
+ function deserializeComponent(node, customComponents = {}, reactInstance = React2) {
596
+ validateSDUINode(node);
597
+ const components = {
598
+ Button,
599
+ Badge,
600
+ Tag,
601
+ Card,
602
+ DataMatrix,
603
+ DataTable,
604
+ TextField,
605
+ ...customComponents
606
+ };
607
+ const h = (type, props2, children) => reactInstance.createElement(type, props2, ...children);
608
+ const childElements = node.children ? node.children.map((child, i) => {
609
+ const el = deserializeComponent(child, customComponents, reactInstance);
610
+ return reactInstance.isValidElement(el) ? reactInstance.cloneElement(el, { key: i }) : el;
611
+ }) : [];
612
+ const contentOrChildren = node.content !== void 0 ? [node.content] : childElements;
613
+ const { token: _token, ...props } = node.props || {};
614
+ switch (node.type) {
615
+ case "button":
616
+ return h(components.Button, props, contentOrChildren);
617
+ case "badge":
618
+ return h(components.Badge, props, contentOrChildren);
619
+ case "tag":
620
+ return h(components.Tag, props, contentOrChildren);
621
+ case "card":
622
+ return h(components.Card, props, contentOrChildren);
623
+ case "data-matrix":
624
+ return h(components.DataMatrix, props, []);
625
+ case "data-table":
626
+ return h(components.DataTable, props, []);
627
+ case "text-field":
628
+ return h(components.TextField, props, []);
629
+ case "container":
630
+ return h("div", props, contentOrChildren);
631
+ case "text":
632
+ return h("span", props, [node.content || ""]);
633
+ default:
634
+ return fail(`Unhandled SDUI node type: ${node.type}`);
635
+ }
636
+ }
637
+ export {
638
+ Badge_default as Badge,
639
+ Button_default as Button,
640
+ Card_default as Card,
641
+ DataMatrix_default as DataMatrix,
642
+ DataTable_default as DataTable,
643
+ Footer_default as Footer,
644
+ LogoLockup_default as LogoLockup,
645
+ Navbar_default as Navbar,
646
+ Tag,
647
+ TextField_default as TextField,
648
+ getContrastingInk,
649
+ sdui_exports as sdui
650
+ };