idml-ui 0.1.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.
package/dist/index.js ADDED
@@ -0,0 +1,2380 @@
1
+ "use client";
2
+
3
+ // src/schema/config.schema.ts
4
+ import { z as z6 } from "zod";
5
+
6
+ // src/schema/tokens.schema.ts
7
+ import { z } from "zod";
8
+ var ColorTokenSchema = z.object({
9
+ name: z.string().min(1),
10
+ value: z.string().min(1),
11
+ darkValue: z.string().optional()
12
+ }).strict();
13
+ var TypographyTokenSchema = z.object({
14
+ name: z.string().min(1),
15
+ fontFamily: z.string().optional(),
16
+ fontSize: z.string().min(1),
17
+ fontWeight: z.union([z.number(), z.string()]).optional(),
18
+ lineHeight: z.string().optional(),
19
+ letterSpacing: z.string().optional()
20
+ }).strict();
21
+ var SpacingTokenSchema = z.object({
22
+ name: z.string().min(1),
23
+ value: z.string().min(1)
24
+ }).strict();
25
+ var TokensDefSchema = z.object({
26
+ colors: z.array(ColorTokenSchema),
27
+ typography: z.array(TypographyTokenSchema),
28
+ spacing: z.array(SpacingTokenSchema)
29
+ }).strict();
30
+
31
+ // src/schema/page.schema.ts
32
+ import { z as z5 } from "zod";
33
+
34
+ // src/schema/layout.schema.ts
35
+ import { z as z2 } from "zod";
36
+ var percentageString = z2.string().regex(/^\d+(\.\d+)?%$/, {
37
+ message: 'All size values must be expressed as percentages (e.g. "100%", "33.3%"). px, rem, em, vw, vh are not allowed.'
38
+ });
39
+ var SizeDefSchema = z2.object({
40
+ width: percentageString.optional(),
41
+ height: percentageString.optional(),
42
+ minWidth: percentageString.optional(),
43
+ minHeight: percentageString.optional(),
44
+ maxWidth: percentageString.optional(),
45
+ maxHeight: percentageString.optional()
46
+ }).strict();
47
+ var VisibilitySchema = z2.object({ ref: z2.string(), negate: z2.boolean().optional() }).strict();
48
+ var ConditionalClassSchema = z2.object({ classes: z2.string(), ref: z2.string(), negate: z2.boolean().optional() }).strict();
49
+ var DynamicDimSchema = z2.object({
50
+ ref: z2.string(),
51
+ whenTrue: z2.string().optional(),
52
+ whenFalse: z2.string().optional()
53
+ }).strict();
54
+ var DynamicSizeSchema = z2.object({ width: DynamicDimSchema.optional(), height: DynamicDimSchema.optional() }).strict();
55
+ var LayoutDefSchema = z2.lazy(
56
+ () => z2.discriminatedUnion("type", [
57
+ z2.object({
58
+ type: z2.literal("flex"),
59
+ direction: z2.enum(["row", "column", "row-reverse", "column-reverse"]),
60
+ wrap: z2.enum(["nowrap", "wrap", "wrap-reverse"]).optional(),
61
+ justifyContent: z2.enum(["flex-start", "flex-end", "center", "space-between", "space-around", "space-evenly"]).optional(),
62
+ alignItems: z2.enum(["flex-start", "flex-end", "center", "stretch", "baseline"]).optional(),
63
+ gap: z2.string().optional(),
64
+ size: SizeDefSchema.optional(),
65
+ children: z2.array(LayoutDefSchema),
66
+ componentId: z2.string().optional(),
67
+ idmlStyle: z2.record(z2.string()).optional(),
68
+ className: z2.string().optional(),
69
+ classRefs: z2.array(z2.string()).optional(),
70
+ condClasses: z2.array(ConditionalClassSchema).optional(),
71
+ visibility: VisibilitySchema.optional(),
72
+ dynamicSize: DynamicSizeSchema.optional()
73
+ }).strict(),
74
+ z2.object({
75
+ type: z2.literal("grid"),
76
+ columns: z2.number().int().min(1).max(24),
77
+ rows: z2.number().int().min(1).optional(),
78
+ gap: z2.string().optional(),
79
+ size: SizeDefSchema.optional(),
80
+ children: z2.array(LayoutDefSchema),
81
+ componentId: z2.string().optional(),
82
+ idmlStyle: z2.record(z2.string()).optional(),
83
+ className: z2.string().optional(),
84
+ classRefs: z2.array(z2.string()).optional(),
85
+ condClasses: z2.array(ConditionalClassSchema).optional(),
86
+ visibility: VisibilitySchema.optional(),
87
+ dynamicSize: DynamicSizeSchema.optional()
88
+ }).strict()
89
+ ])
90
+ );
91
+
92
+ // src/schema/component.schema.ts
93
+ import { z as z4 } from "zod";
94
+
95
+ // src/schema/binding.schema.ts
96
+ import { z as z3 } from "zod";
97
+ var DataBindingDefSchema = z3.object({
98
+ prop: z3.string().min(1),
99
+ methodId: z3.string().min(1),
100
+ kind: z3.enum(["handler", "value", "model"]).optional()
101
+ }).strict();
102
+ var VisibilityDefSchema = z3.object({
103
+ methodId: z3.string().min(1),
104
+ negate: z3.boolean().optional()
105
+ }).strict();
106
+
107
+ // src/schema/component.schema.ts
108
+ var ComponentDefSchema = z4.lazy(
109
+ () => z4.object({
110
+ id: z4.string().min(1),
111
+ type: z4.string().min(1),
112
+ props: z4.record(z4.unknown()).optional(),
113
+ tokenProps: z4.object({
114
+ color: z4.string().optional(),
115
+ background: z4.string().optional(),
116
+ typography: z4.string().optional(),
117
+ padding: z4.string().optional()
118
+ }).strict().optional(),
119
+ bindings: z4.array(DataBindingDefSchema).optional(),
120
+ visibility: VisibilityDefSchema.optional(),
121
+ children: z4.array(ComponentDefSchema).optional(),
122
+ idmlStyle: z4.record(z4.string()).optional(),
123
+ className: z4.string().optional()
124
+ }).strict()
125
+ );
126
+
127
+ // src/schema/page.schema.ts
128
+ var PageDefSchema = z5.object({
129
+ route: z5.string().min(1),
130
+ title: z5.string().optional(),
131
+ layout: LayoutDefSchema,
132
+ components: z5.array(ComponentDefSchema),
133
+ meta: z5.object({
134
+ description: z5.string().optional(),
135
+ ogTitle: z5.string().optional()
136
+ }).strict().optional()
137
+ }).strict();
138
+
139
+ // src/schema/config.schema.ts
140
+ var UIConfigSchema = z6.object({
141
+ version: z6.literal("1"),
142
+ tokens: TokensDefSchema,
143
+ pages: z6.array(PageDefSchema).min(1),
144
+ userComponents: z6.array(z6.string()).optional()
145
+ }).strict();
146
+ function validateConfig(raw) {
147
+ return UIConfigSchema.parse(raw);
148
+ }
149
+ function safeValidateConfig(raw) {
150
+ return UIConfigSchema.safeParse(raw);
151
+ }
152
+
153
+ // src/renderer/ConfigProvider.tsx
154
+ import { createContext, useContext, useEffect, useState } from "react";
155
+
156
+ // src/renderer/tokens/token-resolver.ts
157
+ function injectTokenVars(tokens, darkMode) {
158
+ const vars = {};
159
+ for (const color of tokens.colors) {
160
+ const value = darkMode && color.darkValue ? color.darkValue : color.value;
161
+ vars[`--isd-color-${color.name}`] = value;
162
+ }
163
+ for (const spacing of tokens.spacing) {
164
+ vars[`--isd-spacing-${spacing.name}`] = spacing.value;
165
+ }
166
+ return vars;
167
+ }
168
+ function resolveTokenProps(tokenProps, tokens) {
169
+ if (!tokenProps) return {};
170
+ const style = {};
171
+ if (tokenProps.color) {
172
+ style.color = `var(--isd-color-${tokenProps.color})`;
173
+ }
174
+ if (tokenProps.background) {
175
+ style.backgroundColor = `var(--isd-color-${tokenProps.background})`;
176
+ }
177
+ if (tokenProps.typography) {
178
+ const token = tokens.typography.find((t) => t.name === tokenProps.typography);
179
+ if (token) {
180
+ style.fontSize = token.fontSize;
181
+ if (token.fontWeight) style.fontWeight = token.fontWeight;
182
+ if (token.lineHeight) style.lineHeight = token.lineHeight;
183
+ if (token.letterSpacing) style.letterSpacing = token.letterSpacing;
184
+ if (token.fontFamily) style.fontFamily = token.fontFamily;
185
+ }
186
+ }
187
+ if (tokenProps.padding) {
188
+ style.padding = `var(--isd-spacing-${tokenProps.padding})`;
189
+ }
190
+ return style;
191
+ }
192
+
193
+ // src/renderer/registry/method-registry.ts
194
+ var registry = /* @__PURE__ */ new Map();
195
+ function registerMethod(id, fn) {
196
+ if (registry.has(id)) {
197
+ console.warn(`[idml] registerMethod: overwriting existing method "${id}"`);
198
+ }
199
+ registry.set(id, fn);
200
+ }
201
+ function getMethod(id) {
202
+ return registry.get(id);
203
+ }
204
+ function clearRegistry() {
205
+ registry.clear();
206
+ }
207
+
208
+ // src/renderer/registry/component-registry.ts
209
+ var registry2 = /* @__PURE__ */ new Map();
210
+ function registerComponent(name, component) {
211
+ if (registry2.has(name)) {
212
+ console.warn(`[idml] registerComponent: overwriting existing component "${name}"`);
213
+ }
214
+ registry2.set(name, component);
215
+ }
216
+ function getComponent(name) {
217
+ return registry2.get(name);
218
+ }
219
+ function clearComponentRegistry() {
220
+ registry2.clear();
221
+ }
222
+
223
+ // src/renderer/ConfigProvider.tsx
224
+ import { jsx } from "react/jsx-runtime";
225
+ var ConfigContext = createContext(void 0);
226
+ function useConfigContext() {
227
+ const ctx = useContext(ConfigContext);
228
+ if (!ctx) throw new Error("useConfigContext must be used inside <ConfigProvider>");
229
+ return ctx;
230
+ }
231
+ function ConfigProvider({
232
+ config: rawConfig,
233
+ methods = [],
234
+ components = [],
235
+ children,
236
+ onConfigInvalid,
237
+ debug = false
238
+ }) {
239
+ const [validConfig, setValidConfig] = useState(null);
240
+ const [darkMode, setDarkMode] = useState(false);
241
+ useEffect(() => {
242
+ try {
243
+ const parsed = validateConfig(rawConfig);
244
+ setValidConfig(parsed);
245
+ } catch (err) {
246
+ const error = err instanceof Error ? err : new Error(String(err));
247
+ onConfigInvalid?.(error);
248
+ }
249
+ }, [rawConfig, onConfigInvalid]);
250
+ useEffect(() => {
251
+ clearRegistry();
252
+ for (const { id, fn } of methods) {
253
+ registerMethod(id, fn);
254
+ }
255
+ }, [methods]);
256
+ useEffect(() => {
257
+ clearComponentRegistry();
258
+ for (const { name, component } of components) {
259
+ registerComponent(name, component);
260
+ }
261
+ }, [components]);
262
+ useEffect(() => {
263
+ if (process.env.NODE_ENV !== "development") return;
264
+ const handler = (e) => {
265
+ const target = e.target.closest("[data-isd-id]");
266
+ if (target) {
267
+ const id = target.getAttribute("data-isd-id");
268
+ window.parent.postMessage({ type: "isd:select", componentId: id }, window.location.origin);
269
+ }
270
+ };
271
+ document.addEventListener("click", handler);
272
+ return () => document.removeEventListener("click", handler);
273
+ }, []);
274
+ if (!validConfig) return null;
275
+ const tokenVars = injectTokenVars(validConfig.tokens, darkMode);
276
+ return /* @__PURE__ */ jsx(ConfigContext.Provider, { value: { config: validConfig, darkMode, setDarkMode, tokenVars, debug }, children: /* @__PURE__ */ jsx("div", { style: tokenVars, children }) });
277
+ }
278
+
279
+ // src/renderer/LayoutRenderer.tsx
280
+ import React5 from "react";
281
+
282
+ // src/renderer/ComponentRenderer.tsx
283
+ import React4 from "react";
284
+
285
+ // src/renderer/hooks/useVisibility.ts
286
+ function useVisibility(rule) {
287
+ if (!rule) return true;
288
+ const method = getMethod(rule.methodId);
289
+ if (!method) {
290
+ console.warn(
291
+ `[idml] Visibility method "${rule.methodId}" not registered \u2014 defaulting to visible`
292
+ );
293
+ return true;
294
+ }
295
+ const result = Boolean(method());
296
+ return rule.negate ? !result : result;
297
+ }
298
+
299
+ // src/renderer/builtins/index.ts
300
+ import React3 from "react";
301
+ import { createPortal } from "react-dom";
302
+ import Link from "next/link";
303
+
304
+ // src/renderer/repeat-context.ts
305
+ import { createContext as createContext2 } from "react";
306
+ var RepeatItemContext = createContext2(void 0);
307
+
308
+ // src/renderer/form-context.tsx
309
+ import { createContext as createContext3, useCallback as useCallback2, useContext as useContext2, useState as useState2 } from "react";
310
+ import { jsx as jsx2 } from "react/jsx-runtime";
311
+ var FormStateContext = createContext3(void 0);
312
+ function FormStateProvider({
313
+ initial,
314
+ children
315
+ }) {
316
+ const [values, setValues] = useState2(initial ?? {});
317
+ const setValue = useCallback2((name, value) => {
318
+ setValues((prev) => ({ ...prev, [name]: value }));
319
+ }, []);
320
+ return /* @__PURE__ */ jsx2(FormStateContext.Provider, { value: { values, setValue }, children });
321
+ }
322
+
323
+ // src/renderer/builtins/index.ts
324
+ var Text = ({ text, children, ...props }) => React3.createElement("span", props, text || children);
325
+ var Heading = ({ level = 1, text, children, ...props }) => {
326
+ const Tag = `h${level}`;
327
+ return React3.createElement(Tag, props, text || children);
328
+ };
329
+ var BUTTON_BASE = {
330
+ border: "none",
331
+ outline: "none",
332
+ textDecoration: "none",
333
+ cursor: "pointer",
334
+ fontFamily: "inherit",
335
+ fontSize: "inherit"
336
+ };
337
+ var Button = ({ text, children, onClick, href, style, type = "button", ...props }) => {
338
+ const merged = { ...BUTTON_BASE, ...style };
339
+ const content = [text, children];
340
+ return href ? React3.createElement(Link, { href, style: merged, ...props }, content) : React3.createElement("button", { type, onClick, style: merged, ...props }, content);
341
+ };
342
+ var Image = ({ src, alt, ...props }) => React3.createElement("img", { src, alt: alt || "", ...props });
343
+ var List = ({ items = [], children, ...props }) => React3.createElement(
344
+ "ul",
345
+ props,
346
+ items.map(
347
+ (item, i) => React3.createElement("li", { key: i }, typeof item === "string" ? item : JSON.stringify(item))
348
+ ),
349
+ children
350
+ );
351
+ var Card = ({ children, ...props }) => React3.createElement("div", props, children);
352
+ var Divider = (props) => React3.createElement("hr", props);
353
+ var Spacer = (props) => React3.createElement("div", props);
354
+ var Icon = ({ name, ...props }) => React3.createElement("span", props, name || "\u25CF");
355
+ var Table = ({ children, ...props }) => React3.createElement("div", { "data-isd-table": "", ...props }, children ?? "Table");
356
+ var Input = ({ type = "text", value, onChange, placeholder, name, disabled, style, ...props }) => React3.createElement("input", { type, value, onChange, placeholder, name, disabled, style, ...props });
357
+ var Textarea = ({ value, onChange, placeholder, name, rows, style, ...props }) => React3.createElement("textarea", { value, onChange, placeholder, name, rows, style, ...props });
358
+ var Option = ({ value, label, children, ...props }) => React3.createElement("option", { value, ...props }, label ?? children);
359
+ var Select = ({ value, onChange, name, options, children, style, ...props }) => React3.createElement(
360
+ "select",
361
+ { value, onChange, name, style, ...props },
362
+ Array.isArray(options) ? options.map(
363
+ (opt, i) => React3.createElement("option", { key: i, value: opt.value }, opt.label)
364
+ ) : children
365
+ );
366
+ var Checkbox = ({ checked, onChange, name, disabled, style, ...props }) => React3.createElement("input", { type: "checkbox", checked, onChange, name, disabled, style, ...props });
367
+ var Radio = ({ checked, value, name, onChange, style, ...props }) => React3.createElement("input", { type: "radio", checked, value, name, onChange, style, ...props });
368
+ var Label = ({ htmlFor, text, children, style, ...props }) => React3.createElement("label", { htmlFor, style, ...props }, text ?? children);
369
+ var Children = ({ slot, children, ...props }) => React3.createElement(React3.Fragment, null, slot ?? children);
370
+ var Repeat = ({ data, children, style, ...props }) => {
371
+ const items = Array.isArray(data) ? data : [];
372
+ return React3.createElement(
373
+ "div",
374
+ { "data-isd-repeat": "", style, ...props },
375
+ items.map(
376
+ (item, i) => React3.createElement(RepeatItemContext.Provider, { key: i, value: item }, children)
377
+ )
378
+ );
379
+ };
380
+ var Form = ({ children, style, ...props }) => React3.createElement(
381
+ FormStateProvider,
382
+ null,
383
+ React3.createElement("form", { style, ...props }, children)
384
+ );
385
+ var MODAL_BACKDROP = {
386
+ position: "fixed",
387
+ inset: 0,
388
+ background: "rgba(0,0,0,0.5)",
389
+ display: "flex",
390
+ alignItems: "center",
391
+ justifyContent: "center",
392
+ zIndex: 1e3
393
+ };
394
+ var MODAL_PANEL = {
395
+ maxHeight: "90vh",
396
+ overflow: "auto"
397
+ };
398
+ var Modal = ({ open, onClose, children, style, ...props }) => {
399
+ if (!open) return null;
400
+ if (typeof document === "undefined") return null;
401
+ const panel = React3.createElement(
402
+ "div",
403
+ {
404
+ "data-isd-modal": "",
405
+ style: { ...MODAL_PANEL, ...style },
406
+ onClick: (e) => e.stopPropagation(),
407
+ ...props
408
+ },
409
+ children
410
+ );
411
+ return createPortal(
412
+ React3.createElement("div", { "data-isd-modal-backdrop": "", style: MODAL_BACKDROP, onClick: onClose }, panel),
413
+ document.body
414
+ );
415
+ };
416
+ var BUILTIN_COMPONENTS = {
417
+ Text,
418
+ Heading,
419
+ Button,
420
+ Image,
421
+ List,
422
+ Card,
423
+ Divider,
424
+ Spacer,
425
+ Icon,
426
+ Table,
427
+ Input,
428
+ Textarea,
429
+ Select,
430
+ Option,
431
+ Checkbox,
432
+ Radio,
433
+ Label,
434
+ Children,
435
+ Repeat,
436
+ Form,
437
+ Modal
438
+ };
439
+
440
+ // src/renderer/ComponentRenderer.tsx
441
+ import { jsx as jsx3 } from "react/jsx-runtime";
442
+ function ComponentRenderer({
443
+ component,
444
+ components,
445
+ slot
446
+ }) {
447
+ const { config } = useConfigContext();
448
+ const isVisible = useVisibility(component.visibility);
449
+ const boundProps = useBoundProps(component.bindings ?? []);
450
+ if (!isVisible) return null;
451
+ const FILL_HEIGHT = /* @__PURE__ */ new Set(["Button", "Image", "Card", "Divider", "Spacer"]);
452
+ const tokenStyle = {
453
+ width: "100%",
454
+ ...FILL_HEIGHT.has(component.type) ? { height: "100%" } : {},
455
+ boxSizing: "border-box",
456
+ ...resolveTokenProps(component.tokenProps, config.tokens),
457
+ ...component.idmlStyle ?? {}
458
+ };
459
+ const childElements = (component.children ?? []).map((child) => /* @__PURE__ */ jsx3(ComponentRenderer, { component: child, components }, child.id));
460
+ const Component = getComponent(component.type) ?? BUILTIN_COMPONENTS[component.type];
461
+ if (!Component) {
462
+ console.warn(`[idml] Unknown component type "${component.type}"`);
463
+ return null;
464
+ }
465
+ const content = slot ?? childElements;
466
+ const hasContent = Array.isArray(content) ? content.length > 0 : content != null;
467
+ const props = {
468
+ ...component.props,
469
+ ...boundProps,
470
+ style: tokenStyle,
471
+ "data-isd-id": component.id
472
+ };
473
+ const mergedClass = [component.className, boundProps.className].filter(Boolean).join(" ");
474
+ if (mergedClass) props.className = mergedClass;
475
+ else delete props.className;
476
+ return hasContent ? /* @__PURE__ */ jsx3(Component, { ...props, children: content }) : /* @__PURE__ */ jsx3(Component, { ...props });
477
+ }
478
+ function useBoundProps(bindings) {
479
+ const item = React4.useContext(RepeatItemContext);
480
+ const formStore = React4.useContext(FormStateContext);
481
+ const props = {};
482
+ for (const binding of bindings) {
483
+ if (binding.kind === "value") {
484
+ const resolved = resolveValueRef(binding.methodId, item, formStore?.values);
485
+ if (binding.prop === "className") {
486
+ props.className = [props.className, resolved].filter(Boolean).join(" ");
487
+ } else {
488
+ props[binding.prop] = resolved;
489
+ }
490
+ } else if (binding.kind === "model") {
491
+ const name = binding.methodId;
492
+ const isChecked = binding.prop === "checked";
493
+ const current = formStore?.values[name];
494
+ props[binding.prop] = isChecked ? Boolean(current) : current ?? "";
495
+ props.onChange = (e) => formStore?.setValue(name, isChecked ? e?.target?.checked : e?.target?.value);
496
+ } else {
497
+ const method = getMethod(binding.methodId);
498
+ if (typeof method === "function") {
499
+ const fn = method;
500
+ props[binding.prop] = (event) => fn(formStore?.values ?? {}, {
501
+ set: (name, value) => formStore?.setValue(name, value),
502
+ event,
503
+ item
504
+ });
505
+ }
506
+ }
507
+ }
508
+ return props;
509
+ }
510
+ function resolveValueRef(ref, item, state) {
511
+ const segments = ref.split(".");
512
+ let base;
513
+ if (segments[0] === "item") {
514
+ base = item;
515
+ } else if (segments[0] === "state") {
516
+ base = state;
517
+ } else {
518
+ const method = getMethod(segments[0]);
519
+ base = typeof method === "function" ? method(item, state) : void 0;
520
+ }
521
+ for (let i = 1; i < segments.length; i++) {
522
+ if (base == null) return void 0;
523
+ base = base[segments[i]];
524
+ }
525
+ return base;
526
+ }
527
+
528
+ // src/renderer/LayoutRenderer.tsx
529
+ import { jsx as jsx4 } from "react/jsx-runtime";
530
+ var DIM_ANIM = { duration: 300, easing: "ease-in-out" };
531
+ var toDimStr = (v) => v == null ? void 0 : typeof v === "number" ? `${v}%` : String(v);
532
+ function LayoutRenderer({ layout, components }) {
533
+ const { config, debug } = useConfigContext();
534
+ const item = React5.useContext(RepeatItemContext);
535
+ const formStore = React5.useContext(FormStateContext);
536
+ const resolveDyn = (d) => {
537
+ const v = resolveValueRef(d.ref, item, formStore?.values);
538
+ if (d.whenTrue !== void 0) return v ? d.whenTrue : d.whenFalse;
539
+ return toDimStr(v);
540
+ };
541
+ const dynW = layout.dynamicSize?.width ? resolveDyn(layout.dynamicSize.width) : void 0;
542
+ const dynH = layout.dynamicSize?.height ? resolveDyn(layout.dynamicSize.height) : void 0;
543
+ const elRef = React5.useRef(null);
544
+ const prevDimsRef = React5.useRef({ w: dynW, h: dynH });
545
+ React5.useLayoutEffect(() => {
546
+ const el = elRef.current;
547
+ const prev = prevDimsRef.current;
548
+ if (el && layout.dynamicSize) {
549
+ if (dynW !== void 0 && prev.w !== void 0 && prev.w !== dynW) {
550
+ el.animate([{ width: prev.w }, { width: dynW }], DIM_ANIM);
551
+ }
552
+ if (dynH !== void 0 && prev.h !== void 0 && prev.h !== dynH) {
553
+ el.animate([{ height: prev.h }, { height: dynH }], DIM_ANIM);
554
+ }
555
+ }
556
+ prevDimsRef.current = { w: dynW, h: dynH };
557
+ if (process.env.NODE_ENV !== "production" && el) {
558
+ const cs = getComputedStyle(el);
559
+ if (cs.overflowY === "hidden" && el.scrollHeight - el.clientHeight > 1) {
560
+ console.error(
561
+ `[idml] content overflows its box and is clipped \u2014 ${el.scrollHeight - el.clientHeight}px taller than its height. Reduce the content or its spacing.`,
562
+ el
563
+ );
564
+ }
565
+ }
566
+ }, [dynW, dynH]);
567
+ if (layout.visibility) {
568
+ const value = resolveValueRef(layout.visibility.ref, item, formStore?.values);
569
+ const visible = layout.visibility.negate ? !value : Boolean(value);
570
+ if (!visible) return null;
571
+ }
572
+ const sizeStyle = resolveSizeStyle(layout.size);
573
+ const gapValue = layout.gap ? resolveGap(layout.gap, config.tokens.spacing) : void 0;
574
+ const containerStyle = {
575
+ ...sizeStyle,
576
+ // Debug aid (opt-in): show bounding boxes for structural Row/Col containers
577
+ // (not component-bound leaf cells). Off by default so pages render cleanly.
578
+ ...debug && !layout.componentId ? { outline: "1px solid rgba(100,100,100,0.35)" } : {},
579
+ // Apply .idml inline styles (can override sizeStyle values, e.g. height: '30vh' for scroll pages)
580
+ ...layout.idmlStyle ?? {},
581
+ // Prevent flex children from shrinking so percentage/vh heights are respected and scroll works
582
+ flexShrink: 0
583
+ };
584
+ let containerClass = "";
585
+ if (layout.type === "flex") {
586
+ containerClass = "flex";
587
+ containerStyle.flexDirection = layout.direction;
588
+ if (layout.wrap) containerStyle.flexWrap = layout.wrap;
589
+ if (layout.justifyContent) containerStyle.justifyContent = layout.justifyContent;
590
+ if (layout.alignItems) containerStyle.alignItems = layout.alignItems;
591
+ if (gapValue) containerStyle.gap = gapValue;
592
+ } else {
593
+ containerClass = "grid";
594
+ containerStyle.gridTemplateColumns = `repeat(${layout.columns}, minmax(0, 1fr))`;
595
+ if (layout.rows) containerStyle.gridTemplateRows = `repeat(${layout.rows}, minmax(0, 1fr))`;
596
+ if (gapValue) containerStyle.gap = gapValue;
597
+ }
598
+ if (layout.dynamicSize) {
599
+ if (dynW !== void 0) {
600
+ containerStyle.width = dynW;
601
+ containerStyle.minWidth = 0;
602
+ }
603
+ if (dynH !== void 0) {
604
+ containerStyle.height = dynH;
605
+ containerStyle.minHeight = 0;
606
+ }
607
+ }
608
+ if (layout.className) containerClass += ` ${layout.className}`;
609
+ if (layout.classRefs?.length) {
610
+ for (const ref of layout.classRefs) {
611
+ const cls = resolveValueRef(ref, item, formStore?.values);
612
+ if (cls) containerClass += ` ${cls}`;
613
+ }
614
+ }
615
+ if (layout.condClasses?.length) {
616
+ for (const c of layout.condClasses) {
617
+ const v = resolveValueRef(c.ref, item, formStore?.values);
618
+ if (c.negate ? !v : Boolean(v)) containerClass += ` ${c.classes}`;
619
+ }
620
+ }
621
+ const boundComponent = layout.componentId ? components.find((c) => c.id === layout.componentId) : void 0;
622
+ const children = layout.children.map((child, i) => /* @__PURE__ */ jsx4(LayoutRenderer, { layout: child, components }, i));
623
+ return /* @__PURE__ */ jsx4("div", { ref: elRef, className: containerClass, style: containerStyle, "data-isd-layout": true, children: boundComponent ? /* @__PURE__ */ jsx4(
624
+ ComponentRenderer,
625
+ {
626
+ component: boundComponent,
627
+ components,
628
+ slot: layout.children.length > 0 ? children : void 0
629
+ }
630
+ ) : children });
631
+ }
632
+ function resolveSizeStyle(size) {
633
+ if (!size) return {};
634
+ const s = {};
635
+ if (size.width) s.width = size.width;
636
+ if (size.height) s.height = size.height;
637
+ if (size.minWidth) s.minWidth = size.minWidth;
638
+ if (size.minHeight) s.minHeight = size.minHeight;
639
+ if (size.maxWidth) s.maxWidth = size.maxWidth;
640
+ if (size.maxHeight) s.maxHeight = size.maxHeight;
641
+ return s;
642
+ }
643
+ function resolveGap(tokenName, spacingTokens) {
644
+ return spacingTokens.find((t) => t.name === tokenName)?.value;
645
+ }
646
+
647
+ // src/renderer/ConfigRenderer.tsx
648
+ import { jsx as jsx5 } from "react/jsx-runtime";
649
+ function ConfigRenderer({ page }) {
650
+ const { config } = useConfigContext();
651
+ const pageDef = config.pages.find((p) => p.route === page);
652
+ if (!pageDef) {
653
+ console.warn(`[idml] No page found for route "${page}"`);
654
+ return null;
655
+ }
656
+ return /* @__PURE__ */ jsx5(FormStateProvider, { children: /* @__PURE__ */ jsx5(
657
+ "div",
658
+ {
659
+ "data-isd-page": page,
660
+ style: { width: "100%", height: "100vh", display: "flex", flexDirection: "column" },
661
+ children: /* @__PURE__ */ jsx5(LayoutRenderer, { layout: pageDef.layout, components: pageDef.components })
662
+ }
663
+ ) });
664
+ }
665
+
666
+ // src/renderer/hooks/useRegisteredMethod.ts
667
+ function useRegisteredMethod(id) {
668
+ return getMethod(id);
669
+ }
670
+
671
+ // src/editor/EditorPage.tsx
672
+ import { useEffect as useEffect4, useState as useState6 } from "react";
673
+
674
+ // src/editor/hooks/useEditorState.ts
675
+ import { useState as useState3, useCallback as useCallback3 } from "react";
676
+ function useEditorState(initialConfig) {
677
+ const [state, setState] = useState3({
678
+ config: initialConfig,
679
+ selectedComponentId: null,
680
+ selectedPageRoute: initialConfig.pages[0]?.route ?? "/",
681
+ history: [],
682
+ future: []
683
+ });
684
+ const selectComponent = useCallback3((id) => {
685
+ setState((s) => ({ ...s, selectedComponentId: id }));
686
+ }, []);
687
+ const selectPage = useCallback3((route) => {
688
+ setState((s) => ({ ...s, selectedPageRoute: route, selectedComponentId: null }));
689
+ }, []);
690
+ const updateConfig = useCallback3((next) => {
691
+ setState((s) => ({
692
+ ...s,
693
+ config: next,
694
+ history: [...s.history, s.config].slice(-50),
695
+ future: []
696
+ }));
697
+ }, []);
698
+ const undo = useCallback3(() => {
699
+ setState((s) => {
700
+ if (s.history.length === 0) return s;
701
+ const prev = s.history[s.history.length - 1];
702
+ return {
703
+ ...s,
704
+ config: prev,
705
+ history: s.history.slice(0, -1),
706
+ future: [s.config, ...s.future]
707
+ };
708
+ });
709
+ }, []);
710
+ const redo = useCallback3(() => {
711
+ setState((s) => {
712
+ if (s.future.length === 0) return s;
713
+ const next = s.future[0];
714
+ return {
715
+ ...s,
716
+ config: next,
717
+ history: [...s.history, s.config],
718
+ future: s.future.slice(1)
719
+ };
720
+ });
721
+ }, []);
722
+ return { state, selectComponent, selectPage, updateConfig, undo, redo };
723
+ }
724
+
725
+ // src/editor/hooks/useSSEConfig.ts
726
+ import { useEffect as useEffect2, useCallback as useCallback4 } from "react";
727
+ function useSSEConfig(onConfigChange) {
728
+ const handleChange = useCallback4(() => {
729
+ fetch("/api/_isd/config").then((r) => r.json()).then((raw) => onConfigChange(raw)).catch((err) => console.error("[idml editor] Failed to reload config:", err));
730
+ }, [onConfigChange]);
731
+ useEffect2(() => {
732
+ const es = new EventSource("/api/_isd/events");
733
+ es.onmessage = (event) => {
734
+ try {
735
+ const data = JSON.parse(event.data);
736
+ if (data.type === "config:change") {
737
+ handleChange();
738
+ }
739
+ } catch {
740
+ }
741
+ };
742
+ es.onerror = () => {
743
+ console.warn("[idml editor] SSE connection lost, will retry automatically");
744
+ };
745
+ return () => es.close();
746
+ }, [handleChange]);
747
+ }
748
+
749
+ // src/editor/hooks/useSaveConfig.ts
750
+ import { useCallback as useCallback5, useState as useState4 } from "react";
751
+ function useSaveConfig() {
752
+ const [saving, setSaving] = useState4(false);
753
+ const [error, setError] = useState4(null);
754
+ const save = useCallback5(async (config) => {
755
+ setSaving(true);
756
+ setError(null);
757
+ try {
758
+ const res = await fetch("/api/_isd/config", {
759
+ method: "POST",
760
+ headers: { "Content-Type": "application/json" },
761
+ body: JSON.stringify(config)
762
+ });
763
+ if (!res.ok) {
764
+ const data = await res.json();
765
+ throw new Error(data.error || `Save failed: ${res.status}`);
766
+ }
767
+ } catch (err) {
768
+ setError(err instanceof Error ? err.message : "Unknown error");
769
+ } finally {
770
+ setSaving(false);
771
+ }
772
+ }, []);
773
+ return { save, saving, error };
774
+ }
775
+
776
+ // src/editor/panels/ComponentTree.tsx
777
+ import { jsx as jsx6, jsxs } from "react/jsx-runtime";
778
+ function ComponentTree({
779
+ pages,
780
+ selectedId,
781
+ onSelect,
782
+ onPageChange
783
+ }) {
784
+ return /* @__PURE__ */ jsxs("div", { style: { padding: "12px", overflow: "auto", height: "100%" }, children: [
785
+ /* @__PURE__ */ jsx6("div", { style: { fontSize: "12px", fontWeight: 600, marginBottom: "8px" }, children: "Pages" }),
786
+ pages.map((page) => /* @__PURE__ */ jsxs("div", { style: { marginBottom: "12px" }, children: [
787
+ /* @__PURE__ */ jsx6(
788
+ "button",
789
+ {
790
+ onClick: () => onPageChange(page.route),
791
+ style: {
792
+ display: "block",
793
+ width: "100%",
794
+ padding: "6px 8px",
795
+ textAlign: "left",
796
+ border: "1px solid #d1d5db",
797
+ background: "#f9fafb",
798
+ cursor: "pointer",
799
+ fontSize: "12px",
800
+ fontWeight: 500
801
+ },
802
+ children: page.route
803
+ }
804
+ ),
805
+ /* @__PURE__ */ jsx6("div", { style: { paddingLeft: "8px", marginTop: "4px" }, children: page.components.map((comp) => /* @__PURE__ */ jsx6(
806
+ ComponentTreeItem,
807
+ {
808
+ component: comp,
809
+ selected: selectedId === comp.id,
810
+ onSelect
811
+ },
812
+ comp.id
813
+ )) })
814
+ ] }, page.route))
815
+ ] });
816
+ }
817
+ function ComponentTreeItem({
818
+ component,
819
+ selected,
820
+ onSelect
821
+ }) {
822
+ return /* @__PURE__ */ jsxs("div", { children: [
823
+ /* @__PURE__ */ jsxs(
824
+ "button",
825
+ {
826
+ onClick: () => onSelect(component.id),
827
+ style: {
828
+ display: "block",
829
+ width: "100%",
830
+ padding: "4px 6px",
831
+ textAlign: "left",
832
+ border: selected ? "1px solid #1a56db" : "1px solid transparent",
833
+ background: selected ? "#eff6ff" : "transparent",
834
+ cursor: "pointer",
835
+ fontSize: "11px",
836
+ marginBottom: "2px"
837
+ },
838
+ children: [
839
+ /* @__PURE__ */ jsx6("span", { style: { fontFamily: "monospace" }, children: component.type }),
840
+ /* @__PURE__ */ jsxs("span", { style: { fontSize: "10px", color: "#6b7280" }, children: [
841
+ "#",
842
+ component.id
843
+ ] })
844
+ ]
845
+ }
846
+ ),
847
+ component.children?.map((child) => /* @__PURE__ */ jsx6("div", { style: { paddingLeft: "12px" }, children: /* @__PURE__ */ jsx6(ComponentTreeItem, { component: child, selected, onSelect }) }, child.id))
848
+ ] });
849
+ }
850
+
851
+ // src/editor/panels/LivePreview.tsx
852
+ import { useRef, useEffect as useEffect3 } from "react";
853
+ import { jsx as jsx7, jsxs as jsxs2 } from "react/jsx-runtime";
854
+ function LivePreview({
855
+ pageRoute,
856
+ onComponentSelect
857
+ }) {
858
+ const iframeRef = useRef(null);
859
+ useEffect3(() => {
860
+ const handler = (event) => {
861
+ if (event.data?.type === "isd:select" && typeof event.data.componentId === "string") {
862
+ onComponentSelect(event.data.componentId);
863
+ }
864
+ };
865
+ window.addEventListener("message", handler);
866
+ return () => window.removeEventListener("message", handler);
867
+ }, [onComponentSelect]);
868
+ return /* @__PURE__ */ jsxs2("div", { style: { height: "100%", display: "flex", flexDirection: "column", background: "#fff" }, children: [
869
+ /* @__PURE__ */ jsxs2("div", { style: { padding: "8px", borderBottom: "1px solid #e5e7eb", fontSize: "12px" }, children: [
870
+ "Preview: ",
871
+ pageRoute
872
+ ] }),
873
+ /* @__PURE__ */ jsx7(
874
+ "iframe",
875
+ {
876
+ ref: iframeRef,
877
+ src: pageRoute,
878
+ style: {
879
+ flex: 1,
880
+ border: "none",
881
+ width: "100%"
882
+ },
883
+ title: "Live preview"
884
+ }
885
+ )
886
+ ] });
887
+ }
888
+
889
+ // src/editor/panels/PropertyPanel.tsx
890
+ import { useState as useState5 } from "react";
891
+ import { Fragment, jsx as jsx8, jsxs as jsxs3 } from "react/jsx-runtime";
892
+ function PropertyPanel({
893
+ component,
894
+ tokens,
895
+ onChange
896
+ }) {
897
+ const [expandedSection, setExpandedSection] = useState5("props");
898
+ const updateComponent = (updates) => {
899
+ onChange({ ...component, ...updates });
900
+ };
901
+ return /* @__PURE__ */ jsxs3("div", { style: { padding: "12px", overflow: "auto", height: "100%", fontSize: "12px" }, children: [
902
+ /* @__PURE__ */ jsxs3("div", { style: { marginBottom: "16px" }, children: [
903
+ /* @__PURE__ */ jsx8("div", { style: { fontSize: "11px", color: "#6b7280", marginBottom: "4px" }, children: "ID" }),
904
+ /* @__PURE__ */ jsx8(
905
+ "div",
906
+ {
907
+ style: {
908
+ fontFamily: "monospace",
909
+ padding: "4px 6px",
910
+ background: "#f3f4f6",
911
+ borderRadius: "2px"
912
+ },
913
+ children: component.id
914
+ }
915
+ )
916
+ ] }),
917
+ /* @__PURE__ */ jsx8(
918
+ Section,
919
+ {
920
+ title: "Type",
921
+ expanded: expandedSection === "type",
922
+ onToggle: () => setExpandedSection(expandedSection === "type" ? null : "type"),
923
+ children: /* @__PURE__ */ jsx8(
924
+ "input",
925
+ {
926
+ type: "text",
927
+ value: component.type,
928
+ onChange: (e) => updateComponent({ type: e.target.value }),
929
+ style: { width: "100%", padding: "4px", fontSize: "12px" }
930
+ }
931
+ )
932
+ }
933
+ ),
934
+ /* @__PURE__ */ jsxs3(
935
+ Section,
936
+ {
937
+ title: "Token Props",
938
+ expanded: expandedSection === "tokenProps",
939
+ onToggle: () => setExpandedSection(expandedSection === "tokenProps" ? null : "tokenProps"),
940
+ children: [
941
+ /* @__PURE__ */ jsx8(
942
+ PropertyInput,
943
+ {
944
+ label: "Color",
945
+ value: component.tokenProps?.color ?? "",
946
+ onChange: (value) => updateComponent({
947
+ tokenProps: { ...component.tokenProps, color: value || void 0 }
948
+ }),
949
+ options: tokens.colors.map((c) => c.name)
950
+ }
951
+ ),
952
+ /* @__PURE__ */ jsx8(
953
+ PropertyInput,
954
+ {
955
+ label: "Background",
956
+ value: component.tokenProps?.background ?? "",
957
+ onChange: (value) => updateComponent({
958
+ tokenProps: { ...component.tokenProps, background: value || void 0 }
959
+ }),
960
+ options: tokens.colors.map((c) => c.name)
961
+ }
962
+ ),
963
+ /* @__PURE__ */ jsx8(
964
+ PropertyInput,
965
+ {
966
+ label: "Typography",
967
+ value: component.tokenProps?.typography ?? "",
968
+ onChange: (value) => updateComponent({
969
+ tokenProps: { ...component.tokenProps, typography: value || void 0 }
970
+ }),
971
+ options: tokens.typography.map((t) => t.name)
972
+ }
973
+ )
974
+ ]
975
+ }
976
+ ),
977
+ /* @__PURE__ */ jsx8(
978
+ Section,
979
+ {
980
+ title: "Visibility",
981
+ expanded: expandedSection === "visibility",
982
+ onToggle: () => setExpandedSection(expandedSection === "visibility" ? null : "visibility"),
983
+ children: component.visibility ? /* @__PURE__ */ jsxs3(Fragment, { children: [
984
+ /* @__PURE__ */ jsxs3("div", { style: { marginBottom: "8px" }, children: [
985
+ /* @__PURE__ */ jsx8("div", { style: { fontSize: "10px", color: "#6b7280" }, children: "Method" }),
986
+ /* @__PURE__ */ jsx8(
987
+ "input",
988
+ {
989
+ type: "text",
990
+ value: component.visibility.methodId,
991
+ onChange: (e) => updateComponent({
992
+ visibility: { ...component.visibility, methodId: e.target.value }
993
+ }),
994
+ style: { width: "100%", padding: "4px", fontSize: "12px" }
995
+ }
996
+ )
997
+ ] }),
998
+ /* @__PURE__ */ jsxs3("label", { style: { display: "flex", alignItems: "center", gap: "4px", fontSize: "11px" }, children: [
999
+ /* @__PURE__ */ jsx8(
1000
+ "input",
1001
+ {
1002
+ type: "checkbox",
1003
+ checked: component.visibility.negate ?? false,
1004
+ onChange: (e) => updateComponent({
1005
+ visibility: { ...component.visibility, negate: e.target.checked }
1006
+ })
1007
+ }
1008
+ ),
1009
+ "Negate (show when false)"
1010
+ ] }),
1011
+ /* @__PURE__ */ jsx8(
1012
+ "button",
1013
+ {
1014
+ onClick: () => updateComponent({ visibility: void 0 }),
1015
+ style: {
1016
+ marginTop: "8px",
1017
+ padding: "4px 8px",
1018
+ fontSize: "11px",
1019
+ background: "#fee2e2",
1020
+ color: "#dc2626",
1021
+ border: "none",
1022
+ cursor: "pointer"
1023
+ },
1024
+ children: "Remove visibility"
1025
+ }
1026
+ )
1027
+ ] }) : /* @__PURE__ */ jsx8(
1028
+ "button",
1029
+ {
1030
+ onClick: () => updateComponent({ visibility: { methodId: "", negate: false } }),
1031
+ style: {
1032
+ padding: "4px 8px",
1033
+ fontSize: "11px",
1034
+ background: "#dbeafe",
1035
+ color: "#1a56db",
1036
+ border: "none",
1037
+ cursor: "pointer"
1038
+ },
1039
+ children: "Add visibility rule"
1040
+ }
1041
+ )
1042
+ }
1043
+ )
1044
+ ] });
1045
+ }
1046
+ function Section({
1047
+ title,
1048
+ expanded,
1049
+ onToggle,
1050
+ children
1051
+ }) {
1052
+ return /* @__PURE__ */ jsxs3("div", { style: { marginBottom: "12px" }, children: [
1053
+ /* @__PURE__ */ jsxs3(
1054
+ "button",
1055
+ {
1056
+ onClick: onToggle,
1057
+ style: {
1058
+ width: "100%",
1059
+ padding: "6px 8px",
1060
+ textAlign: "left",
1061
+ background: "#f3f4f6",
1062
+ border: "1px solid #e5e7eb",
1063
+ cursor: "pointer",
1064
+ fontSize: "11px",
1065
+ fontWeight: 600
1066
+ },
1067
+ children: [
1068
+ expanded ? "\u25BC" : "\u25B6",
1069
+ " ",
1070
+ title
1071
+ ]
1072
+ }
1073
+ ),
1074
+ expanded && /* @__PURE__ */ jsx8("div", { style: { padding: "8px", background: "#fafafa" }, children })
1075
+ ] });
1076
+ }
1077
+ function PropertyInput({
1078
+ label,
1079
+ value,
1080
+ onChange,
1081
+ options
1082
+ }) {
1083
+ return /* @__PURE__ */ jsxs3("div", { style: { marginBottom: "8px" }, children: [
1084
+ /* @__PURE__ */ jsx8("label", { style: { display: "block", fontSize: "10px", color: "#6b7280", marginBottom: "2px" }, children: label }),
1085
+ options.length > 0 ? /* @__PURE__ */ jsxs3(
1086
+ "select",
1087
+ {
1088
+ value,
1089
+ onChange: (e) => onChange(e.target.value),
1090
+ style: {
1091
+ width: "100%",
1092
+ padding: "4px",
1093
+ fontSize: "12px",
1094
+ border: "1px solid #d1d5db"
1095
+ },
1096
+ children: [
1097
+ /* @__PURE__ */ jsx8("option", { value: "", children: "None" }),
1098
+ options.map((opt) => /* @__PURE__ */ jsx8("option", { value: opt, children: opt }, opt))
1099
+ ]
1100
+ }
1101
+ ) : /* @__PURE__ */ jsx8(
1102
+ "input",
1103
+ {
1104
+ type: "text",
1105
+ value,
1106
+ onChange: (e) => onChange(e.target.value),
1107
+ style: {
1108
+ width: "100%",
1109
+ padding: "4px",
1110
+ fontSize: "12px",
1111
+ border: "1px solid #d1d5db"
1112
+ }
1113
+ }
1114
+ )
1115
+ ] });
1116
+ }
1117
+
1118
+ // src/editor/EditorPage.tsx
1119
+ import { jsx as jsx9, jsxs as jsxs4 } from "react/jsx-runtime";
1120
+ function EditorPage() {
1121
+ const [initialConfig, setInitialConfig] = useState6(null);
1122
+ const [loading, setLoading] = useState6(true);
1123
+ useEffect4(() => {
1124
+ fetch("/api/_isd/config").then((r) => r.json()).then((config) => {
1125
+ setInitialConfig(config);
1126
+ setLoading(false);
1127
+ }).catch((err) => {
1128
+ console.error("[idml editor] Failed to load config:", err);
1129
+ setLoading(false);
1130
+ });
1131
+ }, []);
1132
+ if (loading) {
1133
+ return /* @__PURE__ */ jsx9("div", { style: { padding: "16px" }, children: "Loading editor..." });
1134
+ }
1135
+ if (!initialConfig) {
1136
+ return /* @__PURE__ */ jsx9("div", { style: { padding: "16px", color: "#dc2626" }, children: "Failed to load configuration" });
1137
+ }
1138
+ return /* @__PURE__ */ jsx9(EditorShell, { initialConfig });
1139
+ }
1140
+ function EditorShell({ initialConfig }) {
1141
+ const { state, selectComponent, selectPage, updateConfig, undo, redo } = useEditorState(initialConfig);
1142
+ const { save, saving, error } = useSaveConfig();
1143
+ useSSEConfig((fresh) => {
1144
+ updateConfig(fresh);
1145
+ });
1146
+ const activePage = state.config.pages.find((p) => p.route === state.selectedPageRoute);
1147
+ const selectedComponent = activePage?.components.find((c) => c.id === state.selectedComponentId);
1148
+ return /* @__PURE__ */ jsxs4("div", { style: { display: "flex", width: "100vw", height: "100vh", fontFamily: "system-ui" }, children: [
1149
+ /* @__PURE__ */ jsxs4("div", { style: { width: "20%", borderRight: "1px solid #e5e7eb", display: "flex", flexDirection: "column" }, children: [
1150
+ /* @__PURE__ */ jsx9("div", { style: { padding: "8px", borderBottom: "1px solid #e5e7eb", fontSize: "12px", fontWeight: 600 }, children: "Structure" }),
1151
+ /* @__PURE__ */ jsx9(
1152
+ ComponentTree,
1153
+ {
1154
+ pages: state.config.pages,
1155
+ selectedId: state.selectedComponentId,
1156
+ onSelect: selectComponent,
1157
+ onPageChange: selectPage
1158
+ }
1159
+ )
1160
+ ] }),
1161
+ /* @__PURE__ */ jsxs4("div", { style: { flex: 1, display: "flex", flexDirection: "column" }, children: [
1162
+ /* @__PURE__ */ jsxs4(
1163
+ "div",
1164
+ {
1165
+ style: {
1166
+ padding: "8px",
1167
+ borderBottom: "1px solid #e5e7eb",
1168
+ display: "flex",
1169
+ gap: "8px",
1170
+ alignItems: "center",
1171
+ fontSize: "12px"
1172
+ },
1173
+ children: [
1174
+ /* @__PURE__ */ jsx9(
1175
+ "button",
1176
+ {
1177
+ onClick: undo,
1178
+ disabled: state.history.length === 0,
1179
+ style: {
1180
+ padding: "4px 8px",
1181
+ fontSize: "11px",
1182
+ cursor: state.history.length === 0 ? "not-allowed" : "pointer",
1183
+ opacity: state.history.length === 0 ? 0.5 : 1
1184
+ },
1185
+ children: "\u21B6 Undo"
1186
+ }
1187
+ ),
1188
+ /* @__PURE__ */ jsx9(
1189
+ "button",
1190
+ {
1191
+ onClick: redo,
1192
+ disabled: state.future.length === 0,
1193
+ style: {
1194
+ padding: "4px 8px",
1195
+ fontSize: "11px",
1196
+ cursor: state.future.length === 0 ? "not-allowed" : "pointer",
1197
+ opacity: state.future.length === 0 ? 0.5 : 1
1198
+ },
1199
+ children: "\u21B7 Redo"
1200
+ }
1201
+ ),
1202
+ /* @__PURE__ */ jsx9(
1203
+ "button",
1204
+ {
1205
+ onClick: () => save(state.config),
1206
+ disabled: saving,
1207
+ style: {
1208
+ padding: "4px 8px",
1209
+ fontSize: "11px",
1210
+ background: "#dbeafe",
1211
+ color: "#1a56db",
1212
+ border: "none",
1213
+ cursor: saving ? "not-allowed" : "pointer",
1214
+ opacity: saving ? 0.7 : 1
1215
+ },
1216
+ children: saving ? "\u22EF Saving" : "\u{1F4BE} Save"
1217
+ }
1218
+ ),
1219
+ error && /* @__PURE__ */ jsxs4("div", { style: { fontSize: "10px", color: "#dc2626", flex: 1 }, children: [
1220
+ "Error: ",
1221
+ error
1222
+ ] })
1223
+ ]
1224
+ }
1225
+ ),
1226
+ activePage && /* @__PURE__ */ jsx9(LivePreview, { pageRoute: state.selectedPageRoute, onComponentSelect: selectComponent })
1227
+ ] }),
1228
+ /* @__PURE__ */ jsxs4("div", { style: { width: "25%", borderLeft: "1px solid #e5e7eb", display: "flex", flexDirection: "column" }, children: [
1229
+ /* @__PURE__ */ jsx9("div", { style: { padding: "8px", borderBottom: "1px solid #e5e7eb", fontSize: "12px", fontWeight: 600 }, children: "Properties" }),
1230
+ /* @__PURE__ */ jsx9("div", { style: { flex: 1, overflow: "auto" }, children: selectedComponent && activePage ? /* @__PURE__ */ jsx9(
1231
+ PropertyPanel,
1232
+ {
1233
+ component: selectedComponent,
1234
+ tokens: state.config.tokens,
1235
+ onChange: (updated) => {
1236
+ const newPages = state.config.pages.map(
1237
+ (p) => p.route === state.selectedPageRoute ? {
1238
+ ...p,
1239
+ components: p.components.map(
1240
+ (c) => c.id === updated.id ? updated : c
1241
+ )
1242
+ } : p
1243
+ );
1244
+ updateConfig({ ...state.config, pages: newPages });
1245
+ }
1246
+ }
1247
+ ) : /* @__PURE__ */ jsx9("div", { style: { padding: "12px", fontSize: "12px", color: "#6b7280" }, children: "Select a component to edit its properties" }) })
1248
+ ] })
1249
+ ] });
1250
+ }
1251
+
1252
+ // src/parser/idml-parser.ts
1253
+ var isDimRef = (d) => typeof d === "object" && d !== null && "ref" in d;
1254
+ var SINGLE_CHAR_TOKENS = {
1255
+ "(": "LPAREN",
1256
+ ")": "RPAREN",
1257
+ "[": "LBRACKET",
1258
+ "]": "RBRACKET",
1259
+ "{": "LBRACE",
1260
+ "}": "RBRACE",
1261
+ ",": "COMMA",
1262
+ ":": "COLON",
1263
+ "?": "QUESTION",
1264
+ "!": "BANG"
1265
+ };
1266
+ var MAX_LINE_WIDTH = 80;
1267
+ function validateSource(source) {
1268
+ const lines = source.split("\n");
1269
+ let codeStarted = false;
1270
+ lines.forEach((line, idx) => {
1271
+ const lineNo = idx + 1;
1272
+ if (line.length > MAX_LINE_WIDTH) {
1273
+ throw new Error(
1274
+ `[idml] line ${lineNo} is ${line.length} columns; the limit is ${MAX_LINE_WIDTH}`
1275
+ );
1276
+ }
1277
+ const trimmed = line.trim();
1278
+ if (trimmed === "") return;
1279
+ if (trimmed.startsWith("#")) {
1280
+ if (codeStarted) {
1281
+ throw new Error(
1282
+ `[idml] line ${lineNo}: comments are only allowed in the header block at the very top of the file, before any code`
1283
+ );
1284
+ }
1285
+ return;
1286
+ }
1287
+ codeStarted = true;
1288
+ });
1289
+ }
1290
+ function tokenize(source) {
1291
+ validateSource(source);
1292
+ const stripped = source.split("\n").map((line) => line.trimStart().startsWith("#") ? "" : line).join("\n");
1293
+ const tokens = [];
1294
+ let i = 0;
1295
+ while (i < stripped.length) {
1296
+ if (/\s/.test(stripped[i])) {
1297
+ i++;
1298
+ continue;
1299
+ }
1300
+ if (stripped[i] === "." && stripped[i + 1] === "/") {
1301
+ let j = i + 2;
1302
+ while (j < stripped.length && /[\w/-]/.test(stripped[j])) j++;
1303
+ tokens.push({ type: "ROUTE", value: "/" + stripped.slice(i + 2, j) });
1304
+ i = j;
1305
+ continue;
1306
+ }
1307
+ if (stripped[i] === "#" && /[0-9a-fA-F]/.test(stripped[i + 1] ?? "")) {
1308
+ let j = i + 1;
1309
+ while (j < stripped.length && /[0-9a-fA-F]/.test(stripped[j])) j++;
1310
+ tokens.push({ type: "COLOR", value: stripped.slice(i, j) });
1311
+ i = j;
1312
+ continue;
1313
+ }
1314
+ if (stripped[i] === "@" && /[a-zA-Z_]/.test(stripped[i + 1] ?? "")) {
1315
+ let j = i + 1;
1316
+ while (j < stripped.length && /[\w.-]/.test(stripped[j])) j++;
1317
+ tokens.push({ type: "VALUE_REF", value: stripped.slice(i + 1, j) });
1318
+ i = j;
1319
+ continue;
1320
+ }
1321
+ if (stripped[i] === "~" && /[a-zA-Z_]/.test(stripped[i + 1] ?? "")) {
1322
+ let j = i + 1;
1323
+ while (j < stripped.length && /[\w-]/.test(stripped[j])) j++;
1324
+ tokens.push({ type: "MODEL_REF", value: stripped.slice(i + 1, j) });
1325
+ i = j;
1326
+ continue;
1327
+ }
1328
+ if (stripped[i] === "<") {
1329
+ throw new Error(
1330
+ "[idml] inline `<...>` style blocks are no longer supported; declare a styled variant (Name:BaseType) and apply it instead"
1331
+ );
1332
+ }
1333
+ if (stripped[i] === "`") {
1334
+ let j = i + 1;
1335
+ while (j < stripped.length && stripped[j] !== "`") j++;
1336
+ tokens.push({ type: "CLASS_BLOCK", value: stripped.slice(i + 1, j).trim() });
1337
+ i = j + 1;
1338
+ continue;
1339
+ }
1340
+ if (stripped[i] in SINGLE_CHAR_TOKENS) {
1341
+ tokens.push({ type: SINGLE_CHAR_TOKENS[stripped[i]] });
1342
+ i++;
1343
+ continue;
1344
+ }
1345
+ if (stripped[i] === '"') {
1346
+ let j = i + 1;
1347
+ while (j < stripped.length && stripped[j] !== '"') {
1348
+ if (stripped[j] === "\\") j++;
1349
+ j++;
1350
+ }
1351
+ tokens.push({ type: "STRING", value: stripped.slice(i + 1, j) });
1352
+ i = j + 1;
1353
+ continue;
1354
+ }
1355
+ if (/\d/.test(stripped[i])) {
1356
+ let j = i;
1357
+ while (j < stripped.length && /[\d.]/.test(stripped[j])) j++;
1358
+ tokens.push({ type: "NUMBER", value: parseFloat(stripped.slice(i, j)) });
1359
+ i = j;
1360
+ continue;
1361
+ }
1362
+ if (/[a-zA-Z_]/.test(stripped[i])) {
1363
+ let j = i;
1364
+ while (j < stripped.length && /[\w-]/.test(stripped[j])) j++;
1365
+ tokens.push({ type: "IDENT", value: stripped.slice(i, j) });
1366
+ i = j;
1367
+ continue;
1368
+ }
1369
+ throw new Error(`[idml] Unexpected character '${stripped[i]}' at position ${i}`);
1370
+ }
1371
+ return tokens;
1372
+ }
1373
+ function applyStyleProp(key, val, result) {
1374
+ switch (key) {
1375
+ case "bg":
1376
+ result.backgroundColor = val;
1377
+ break;
1378
+ case "fg":
1379
+ result.color = val;
1380
+ break;
1381
+ case "size":
1382
+ result.fontSize = val.endsWith("vw") ? val : `${val}vw`;
1383
+ break;
1384
+ case "font":
1385
+ result.fontFamily = val;
1386
+ break;
1387
+ case "weight":
1388
+ result.fontWeight = val;
1389
+ break;
1390
+ case "style":
1391
+ if (val === "bold") result.fontWeight = "700";
1392
+ else if (val === "italic") result.fontStyle = "italic";
1393
+ break;
1394
+ case "pad":
1395
+ result.padding = val.endsWith("%") ? val : `${val}%`;
1396
+ break;
1397
+ case "radius":
1398
+ result.borderRadius = val.endsWith("px") ? val : `${val}px`;
1399
+ break;
1400
+ case "gap":
1401
+ result.gap = val.endsWith("vw") ? val : `${val}vw`;
1402
+ break;
1403
+ case "align":
1404
+ result.textAlign = val;
1405
+ break;
1406
+ case "overflow":
1407
+ result.overflowY = val;
1408
+ break;
1409
+ case "h":
1410
+ result.height = val;
1411
+ break;
1412
+ case "w":
1413
+ result.width = val;
1414
+ break;
1415
+ default:
1416
+ result[key] = val;
1417
+ }
1418
+ }
1419
+ var FN_REF_PREFIX = "\0fn:";
1420
+ var VALUE_REF_PREFIX = "\0val:";
1421
+ var MODEL_REF_PREFIX = "\0model:";
1422
+ var BUILTIN_NAMES = /* @__PURE__ */ new Set([
1423
+ "Text",
1424
+ "Heading",
1425
+ "Button",
1426
+ "Image",
1427
+ "List",
1428
+ "Card",
1429
+ "Divider",
1430
+ "Spacer",
1431
+ "Icon",
1432
+ "Table",
1433
+ "Children",
1434
+ "Row",
1435
+ "Col",
1436
+ "Repeat",
1437
+ "Form",
1438
+ "Modal",
1439
+ "Column",
1440
+ "Overlay",
1441
+ "Input",
1442
+ "Textarea",
1443
+ "Select",
1444
+ "Option",
1445
+ "Checkbox",
1446
+ "Radio",
1447
+ "Label"
1448
+ ]);
1449
+ var IdmlParser = class _IdmlParser {
1450
+ constructor(tokens) {
1451
+ this.pos = 0;
1452
+ this.styleRegistry = /* @__PURE__ */ new Map();
1453
+ // Reusable component definitions: name -> body template (item list). A `define`
1454
+ // block registers one; using the name as an item expands the body (macro-style),
1455
+ // substituting the call's children at the `Children` marker. See convertItem.
1456
+ this.defRegistry = /* @__PURE__ */ new Map();
1457
+ // Parameter names per definition (e.g. `define TopBar(title)` -> ['title']).
1458
+ // At expansion the call's positional args are bound to these names and any
1459
+ // matching references inside the body are substituted. See convertItem.
1460
+ this.defParamRegistry = /* @__PURE__ */ new Map();
1461
+ this.tokens = tokens;
1462
+ }
1463
+ peek(offset = 0) {
1464
+ return this.tokens[this.pos + offset];
1465
+ }
1466
+ consume(type) {
1467
+ const t = this.tokens[this.pos++];
1468
+ if (!t) throw new Error("[idml] Unexpected end of input");
1469
+ if (type && t.type !== type) {
1470
+ throw new Error(`[idml] Expected ${type}, got ${t.type} ("${t.value}")`);
1471
+ }
1472
+ return t;
1473
+ }
1474
+ // Entry point. resolve() is called to load imported .idml files.
1475
+ parseFile(resolve) {
1476
+ this.parseImports(resolve);
1477
+ this.parseTopDecls();
1478
+ const pages = [];
1479
+ while (this.peek()) {
1480
+ const route = this.consume("ROUTE").value;
1481
+ let scroll = false;
1482
+ if (this.peek()?.type === "LBRACKET") {
1483
+ this.consume("LBRACKET");
1484
+ const flag = this.consume("IDENT").value;
1485
+ if (flag === "scroll") scroll = true;
1486
+ this.consume("RBRACKET");
1487
+ }
1488
+ const items = [];
1489
+ while (this.peek() && this.peek()?.type !== "ROUTE") {
1490
+ const t = this.peek();
1491
+ if (t?.type === "IDENT" && t.value === "import") {
1492
+ this.parseImports(resolve);
1493
+ continue;
1494
+ }
1495
+ if (t?.type === "IDENT" && t.value === "define") {
1496
+ this.parseDefinition();
1497
+ continue;
1498
+ }
1499
+ if (t?.type === "IDENT" && this.peek(1)?.type === "COLON" && this.peek(2)?.type === "IDENT") {
1500
+ this.parseStyleDefs();
1501
+ continue;
1502
+ }
1503
+ items.push(this.parseItem());
1504
+ }
1505
+ pages.push({ route, scroll, items });
1506
+ }
1507
+ return pages;
1508
+ }
1509
+ // Consume import lines. Two forms:
1510
+ // import "path" (whole-file style/def import)
1511
+ // import Name, Name from "path" (named component/def import)
1512
+ // In both cases the referenced .idml file is parsed and its definitions +
1513
+ // style-defs are registered into the shared registries.
1514
+ parseImports(resolve) {
1515
+ while (this.peek(0)?.type === "IDENT" && this.peek(0)?.value === "import") {
1516
+ const next = this.peek(1);
1517
+ if (next?.type === "STRING") {
1518
+ this.pos++;
1519
+ const importPath = this.consume("STRING").value;
1520
+ this.resolveImport(importPath, [], resolve);
1521
+ continue;
1522
+ }
1523
+ if (next?.type === "IDENT") {
1524
+ this.pos++;
1525
+ const names = [this.consume("IDENT").value];
1526
+ while (this.peek()?.type === "COMMA") {
1527
+ this.pos++;
1528
+ names.push(this.consume("IDENT").value);
1529
+ }
1530
+ const fromTok = this.consume("IDENT");
1531
+ if (fromTok.value !== "from") {
1532
+ throw new Error(`[idml] Expected 'from' in import, got "${fromTok.value}"`);
1533
+ }
1534
+ const importPath = this.consume("STRING").value;
1535
+ this.resolveImport(importPath, names, resolve);
1536
+ continue;
1537
+ }
1538
+ break;
1539
+ }
1540
+ }
1541
+ // Resolve and parse an imported .idml file into the shared registries.
1542
+ // `names`, when non-empty, are validated against what the file actually defines.
1543
+ resolveImport(importPath, names, resolve) {
1544
+ const afterLastSlash = importPath.slice(importPath.lastIndexOf("/") + 1);
1545
+ const dotIdx = afterLastSlash.lastIndexOf(".");
1546
+ const ext = dotIdx >= 0 ? afterLastSlash.slice(dotIdx) : "";
1547
+ if (ext !== ".idml" && ext !== "") return;
1548
+ if (!resolve) return;
1549
+ const src = resolve(importPath);
1550
+ const sub = new _IdmlParser(tokenize(src));
1551
+ sub.styleRegistry = this.styleRegistry;
1552
+ sub.defRegistry = this.defRegistry;
1553
+ sub.defParamRegistry = this.defParamRegistry;
1554
+ sub.parseImports(resolve);
1555
+ sub.parseTopDecls();
1556
+ for (const name of names) {
1557
+ if (!this.defRegistry.has(name) && !this.styleRegistry.has(name) && !BUILTIN_NAMES.has(name)) {
1558
+ console.warn(`[idml] import: "${name}" is not defined in ${importPath}`);
1559
+ }
1560
+ }
1561
+ }
1562
+ // Parse the top-of-file declarations: `define` component definitions and
1563
+ // `Name:BaseType` style-defs, in any order, until a page route or EOF.
1564
+ parseTopDecls() {
1565
+ for (; ; ) {
1566
+ const t = this.peek();
1567
+ if (t?.type === "IDENT" && t.value === "define") {
1568
+ this.parseDefinition();
1569
+ continue;
1570
+ }
1571
+ if (t?.type === "IDENT" && this.peek(1)?.type === "COLON" && this.peek(2)?.type === "IDENT") {
1572
+ this.parseStyleDefs();
1573
+ continue;
1574
+ }
1575
+ break;
1576
+ }
1577
+ }
1578
+ // Consume `define Name(params?) { ...items including Children()... }`.
1579
+ parseDefinition() {
1580
+ this.pos++;
1581
+ const name = this.consume("IDENT").value;
1582
+ this.consume("LPAREN");
1583
+ const params = [];
1584
+ if (this.peek()?.type !== "RPAREN") {
1585
+ params.push(this.consume("IDENT").value);
1586
+ while (this.peek()?.type === "COMMA") {
1587
+ this.pos++;
1588
+ params.push(this.consume("IDENT").value);
1589
+ }
1590
+ }
1591
+ this.consume("RPAREN");
1592
+ this.consume("LBRACE");
1593
+ const body = [];
1594
+ while (this.peek()?.type !== "RBRACE") {
1595
+ body.push(this.parseItem());
1596
+ }
1597
+ this.consume("RBRACE");
1598
+ this.defRegistry.set(name, body);
1599
+ this.defParamRegistry.set(name, params);
1600
+ }
1601
+ // Consume `Name:BaseType("arg"...) { prop: value }` definitions.
1602
+ parseStyleDefs() {
1603
+ while (this.peek(0)?.type === "IDENT" && this.peek(1)?.type === "COLON" && this.peek(2)?.type === "IDENT") {
1604
+ const name = this.consume("IDENT").value;
1605
+ this.consume("COLON");
1606
+ const baseType = this.consume("IDENT").value;
1607
+ const defaultArgs = [];
1608
+ if (this.peek()?.type === "LPAREN") {
1609
+ this.consume("LPAREN");
1610
+ if (this.peek()?.type !== "RPAREN") defaultArgs.push(...this.parseArgList());
1611
+ this.consume("RPAREN");
1612
+ }
1613
+ let className;
1614
+ while (this.peek()?.type === "CLASS_BLOCK") {
1615
+ const cls = this.consume("CLASS_BLOCK").value;
1616
+ className = className ? `${className} ${cls}` : cls;
1617
+ }
1618
+ const style = this.peek()?.type === "LBRACE" ? this.parseStyleDefBody() : {};
1619
+ this.styleRegistry.set(name, { baseType, defaultArgs, style, className });
1620
+ }
1621
+ }
1622
+ // Parse `{ prop: value ... }` body of a style definition.
1623
+ parseStyleDefBody() {
1624
+ this.consume("LBRACE");
1625
+ const result = {};
1626
+ while (this.peek()?.type !== "RBRACE") {
1627
+ const key = this.consume("IDENT").value;
1628
+ this.consume("COLON");
1629
+ const val = this.parseStyleValue();
1630
+ applyStyleProp(key, val, result);
1631
+ }
1632
+ this.consume("RBRACE");
1633
+ return result;
1634
+ }
1635
+ // Parse a value token inside a style def body.
1636
+ parseStyleValue() {
1637
+ const t = this.peek();
1638
+ if (!t) throw new Error("[idml] Expected style value");
1639
+ if (t.type === "COLOR") {
1640
+ this.pos++;
1641
+ return t.value;
1642
+ }
1643
+ if (t.type === "NUMBER") {
1644
+ const n = this.consume("NUMBER").value;
1645
+ const next = this.peek();
1646
+ if (next?.type === "IDENT" && ["vh", "vw", "px", "rem", "em"].includes(next.value)) {
1647
+ this.pos++;
1648
+ return `${n}${next.value}`;
1649
+ }
1650
+ return String(n);
1651
+ }
1652
+ if (t.type === "IDENT") {
1653
+ this.pos++;
1654
+ return t.value;
1655
+ }
1656
+ throw new Error(`[idml] Unexpected token type ${t.type} as style value`);
1657
+ }
1658
+ parseItem() {
1659
+ const rawName = this.consume("IDENT").value;
1660
+ const regEntry = this.styleRegistry.get(rawName);
1661
+ const name = regEntry ? regEntry.baseType : rawName;
1662
+ const baseStyle = regEntry ? { ...regEntry.style } : {};
1663
+ this.consume("LPAREN");
1664
+ const parsedArgs = [];
1665
+ if (this.peek()?.type !== "RPAREN") parsedArgs.push(...this.parseArgList());
1666
+ this.consume("RPAREN");
1667
+ const inlineChildren = [];
1668
+ const valueArgs = [];
1669
+ for (const a of parsedArgs) {
1670
+ if (a && typeof a === "object" && Array.isArray(a.__idmlChildren)) {
1671
+ inlineChildren.push(...a.__idmlChildren);
1672
+ } else {
1673
+ valueArgs.push(a);
1674
+ }
1675
+ }
1676
+ const args = valueArgs.length > 0 ? valueArgs : regEntry?.defaultArgs ?? [];
1677
+ if (this.peek()?.type !== "LBRACKET") {
1678
+ throw new Error(
1679
+ `[idml] "${rawName}" is missing its required [height,width,anchor] dimensions`
1680
+ );
1681
+ }
1682
+ this.consume("LBRACKET");
1683
+ const height = this.parseDimension();
1684
+ this.consume("COMMA");
1685
+ const width = this.parseDimension();
1686
+ this.consume("COMMA");
1687
+ const anchor = this.consume("IDENT").value;
1688
+ let hug;
1689
+ if (this.peek()?.type === "COMMA") {
1690
+ this.consume("COMMA");
1691
+ const kw = this.consume("IDENT").value;
1692
+ if (kw === "hug") hug = { w: true, h: true };
1693
+ else if (kw === "hug-w") hug = { w: true, h: false };
1694
+ else if (kw === "hug-h") hug = { w: false, h: true };
1695
+ else
1696
+ throw new Error(
1697
+ `[idml] unknown sizing keyword "${kw}" for "${rawName}"; expected hug, hug-w, or hug-h`
1698
+ );
1699
+ }
1700
+ this.consume("RBRACKET");
1701
+ let visibility;
1702
+ if (this.peek()?.type === "QUESTION") {
1703
+ this.consume("QUESTION");
1704
+ let negate = false;
1705
+ if (this.peek()?.type === "BANG") {
1706
+ this.consume("BANG");
1707
+ negate = true;
1708
+ }
1709
+ const ref = this.consume("VALUE_REF").value;
1710
+ visibility = { ref, negate };
1711
+ }
1712
+ let className = regEntry?.className;
1713
+ const condClasses = [];
1714
+ while (this.peek()?.type === "CLASS_BLOCK") {
1715
+ const cls = this.consume("CLASS_BLOCK").value;
1716
+ if (this.peek()?.type === "QUESTION") {
1717
+ this.consume("QUESTION");
1718
+ let negate = false;
1719
+ if (this.peek()?.type === "BANG") {
1720
+ this.consume("BANG");
1721
+ negate = true;
1722
+ }
1723
+ const ref = this.consume("VALUE_REF").value;
1724
+ condClasses.push({ classes: cls, ref, negate });
1725
+ continue;
1726
+ }
1727
+ for (const tok of cls.split(/\s+/).filter(Boolean)) {
1728
+ if (!tok.startsWith("@")) {
1729
+ throw new Error(
1730
+ `[idml] literal class "${tok}" is not allowed at a use site; declare a styled variant (Name:BaseType) instead`
1731
+ );
1732
+ }
1733
+ }
1734
+ className = className ? `${className} ${cls}` : cls;
1735
+ }
1736
+ const style = { ...baseStyle };
1737
+ const classRefs = [];
1738
+ if (className) {
1739
+ const statics = [];
1740
+ for (const tok of className.split(/\s+/).filter(Boolean)) {
1741
+ if (tok.startsWith("@")) classRefs.push(tok.slice(1));
1742
+ else statics.push(tok);
1743
+ }
1744
+ className = statics.length ? statics.join(" ") : void 0;
1745
+ }
1746
+ this.consume("LBRACE");
1747
+ const children = [...inlineChildren];
1748
+ while (this.peek()?.type !== "RBRACE") {
1749
+ children.push(this.parseItem());
1750
+ }
1751
+ this.consume("RBRACE");
1752
+ return { name, args, height, width, anchor, children, style, className, classRefs, condClasses, hug, visibility };
1753
+ }
1754
+ parseDimension() {
1755
+ if (this.peek()?.type === "IDENT" && this.peek()?.value === "auto") {
1756
+ throw new Error(
1757
+ "[idml] the `auto` dimension is no longer supported; give an explicit percentage (use a Spacer for any intentional empty space)"
1758
+ );
1759
+ }
1760
+ if (this.peek()?.type === "VALUE_REF") {
1761
+ const ref = this.consume("VALUE_REF").value;
1762
+ if (this.peek()?.type === "QUESTION") {
1763
+ this.consume("QUESTION");
1764
+ const whenTrue = this.parseDimLiteral();
1765
+ this.consume("COLON");
1766
+ const whenFalse = this.parseDimLiteral();
1767
+ return { ref, whenTrue, whenFalse };
1768
+ }
1769
+ return { ref };
1770
+ }
1771
+ return this.consume("NUMBER").value;
1772
+ }
1773
+ /** A literal dimension value inside a `@ref ? A : B`: a number (→ `%`) or a
1774
+ * number with a unit (`3.4vw`, `64px`). Returns the final CSS string. */
1775
+ parseDimLiteral() {
1776
+ const n = this.consume("NUMBER").value;
1777
+ const next = this.peek();
1778
+ if (next?.type === "IDENT" && ["vw", "vh", "px", "rem", "em"].includes(next.value)) {
1779
+ this.pos++;
1780
+ return `${n}${next.value}`;
1781
+ }
1782
+ return `${n}%`;
1783
+ }
1784
+ parseArgList() {
1785
+ const args = [];
1786
+ args.push(this.parseArg());
1787
+ while (this.peek()?.type === "COMMA") {
1788
+ this.pos++;
1789
+ if (this.peek()?.type === "RPAREN") break;
1790
+ args.push(this.parseArg());
1791
+ }
1792
+ return args;
1793
+ }
1794
+ parseArg() {
1795
+ const t = this.peek();
1796
+ if (!t) throw new Error("[idml] Expected argument");
1797
+ if (t.type === "STRING") {
1798
+ this.pos++;
1799
+ return t.value;
1800
+ }
1801
+ if (t.type === "NUMBER") {
1802
+ this.pos++;
1803
+ return t.value;
1804
+ }
1805
+ if (t.type === "VALUE_REF") {
1806
+ this.pos++;
1807
+ return `${VALUE_REF_PREFIX}${t.value}`;
1808
+ }
1809
+ if (t.type === "MODEL_REF") {
1810
+ this.pos++;
1811
+ return `${MODEL_REF_PREFIX}${t.value}`;
1812
+ }
1813
+ if (t.type === "IDENT") {
1814
+ this.pos++;
1815
+ if (t.value === "null") return null;
1816
+ if (t.value === "true") return true;
1817
+ if (t.value === "false") return false;
1818
+ return `${FN_REF_PREFIX}${t.value}`;
1819
+ }
1820
+ if (t.type === "LBRACE") {
1821
+ this.pos++;
1822
+ const childItems = [];
1823
+ while (this.peek()?.type !== "RBRACE") {
1824
+ childItems.push(this.parseItem());
1825
+ }
1826
+ this.consume("RBRACE");
1827
+ return { __idmlChildren: childItems };
1828
+ }
1829
+ throw new Error(`[idml] Unexpected token type ${t.type} as argument`);
1830
+ }
1831
+ };
1832
+ var LAYOUT_ITEMS = /* @__PURE__ */ new Set(["Row", "Col"]);
1833
+ var ANCHOR_V = { top: "flex-start", center: "center", bottom: "flex-end" };
1834
+ var ANCHOR_H = { left: "flex-start", center: "center", right: "flex-end" };
1835
+ function anchorToAbsoluteInsets(anchor) {
1836
+ const parts = anchor.split("-");
1837
+ const v = parts.length === 1 ? parts[0] : parts[0] ?? "top";
1838
+ const h = parts.length === 1 ? parts[0] : parts[1] ?? "left";
1839
+ const css = {};
1840
+ if (v === "bottom") css.bottom = "0";
1841
+ else if (v === "center") css.top = "50%";
1842
+ else css.top = "0";
1843
+ if (h === "right") css.right = "0";
1844
+ else if (h === "center") css.left = "50%";
1845
+ else css.left = "0";
1846
+ if (v === "center" || h === "center") {
1847
+ css.transform = `translate(${h === "center" ? "-50%" : "0"}, ${v === "center" ? "-50%" : "0"})`;
1848
+ }
1849
+ return css;
1850
+ }
1851
+ function anchorToFlexProps(anchor, direction) {
1852
+ const parts = anchor.split("-");
1853
+ if (parts.length === 1) {
1854
+ const val = ANCHOR_V[parts[0]] ?? ANCHOR_H[parts[0]] ?? "flex-start";
1855
+ return { justifyContent: val, alignItems: val };
1856
+ }
1857
+ const [v = "top", h = "left"] = parts;
1858
+ const vVal = ANCHOR_V[v] ?? "flex-start";
1859
+ const hVal = ANCHOR_H[h] ?? "flex-start";
1860
+ if (direction === "row") {
1861
+ return { justifyContent: hVal, alignItems: vVal };
1862
+ }
1863
+ return { justifyContent: vVal, alignItems: hVal };
1864
+ }
1865
+ function pct(n) {
1866
+ return `${n}%`;
1867
+ }
1868
+ var _idCounter = 0;
1869
+ function genId(prefix) {
1870
+ return `${prefix}-${++_idCounter}`;
1871
+ }
1872
+ function substituteParams(item, bindings) {
1873
+ if (bindings.size === 0) return item;
1874
+ const subArg = (a) => {
1875
+ if (typeof a === "string") {
1876
+ for (const prefix of [FN_REF_PREFIX, VALUE_REF_PREFIX, MODEL_REF_PREFIX]) {
1877
+ if (a.startsWith(prefix)) {
1878
+ const name = a.slice(prefix.length);
1879
+ return bindings.has(name) ? bindings.get(name) : a;
1880
+ }
1881
+ }
1882
+ return a;
1883
+ }
1884
+ if (a && typeof a === "object" && Array.isArray(a.__idmlChildren)) {
1885
+ return {
1886
+ __idmlChildren: a.__idmlChildren.map((c) => substituteParams(c, bindings))
1887
+ };
1888
+ }
1889
+ return a;
1890
+ };
1891
+ return {
1892
+ ...item,
1893
+ args: item.args.map(subArg),
1894
+ children: item.children.map((c) => substituteParams(c, bindings))
1895
+ };
1896
+ }
1897
+ function containerDirection(name, defs) {
1898
+ if (name === "Row") return "row";
1899
+ if (name === "Col" || name === "Form") return "column";
1900
+ if (defs.has(name)) return "column";
1901
+ return null;
1902
+ }
1903
+ var OUT_OF_FLOW = /* @__PURE__ */ new Set(["Overlay", "Modal"]);
1904
+ function defIsOutOfFlow(name, defs, seen = /* @__PURE__ */ new Set()) {
1905
+ const body = defs.get(name);
1906
+ if (!body || seen.has(name)) return false;
1907
+ seen.add(name);
1908
+ return body.every(
1909
+ (c) => OUT_OF_FLOW.has(c.name) || defs.has(c.name) && defIsOutOfFlow(c.name, defs, seen)
1910
+ );
1911
+ }
1912
+ function makeOutOfFlowPredicate(defs) {
1913
+ return (name) => OUT_OF_FLOW.has(name) || defs.has(name) && defIsOutOfFlow(name, defs);
1914
+ }
1915
+ function validateTiling(children, direction, where, isOutOfFlow, containerHug) {
1916
+ children = children.filter((c) => !isOutOfFlow(c.name));
1917
+ if (children.length === 0) return;
1918
+ const main = direction === "row" ? "width" : "height";
1919
+ const cross = direction === "row" ? "height" : "width";
1920
+ const mainKey = direction === "row" ? "w" : "h";
1921
+ const crossKey = direction === "row" ? "h" : "w";
1922
+ const packsMain = !!containerHug?.[mainKey] || children.some((c) => c.hug?.[mainKey] || isDimRef(c[main]));
1923
+ let sum = 0;
1924
+ for (const c of children) {
1925
+ if (typeof c[main] === "number") sum += c[main];
1926
+ if (!c.hug?.[crossKey] && !isDimRef(c[cross]) && c[cross] !== 100) {
1927
+ throw new Error(
1928
+ `[idml] ${c.name} in ${where}: cross-axis ${cross} must be 100 (got ${c[cross]}); no vacant space is allowed`
1929
+ );
1930
+ }
1931
+ }
1932
+ if (!packsMain && sum !== 100) {
1933
+ throw new Error(
1934
+ `[idml] children of ${where} must tile to 100% along ${main}; got ${sum}. Add an explicit Spacer for any gap.`
1935
+ );
1936
+ }
1937
+ }
1938
+ function walkTiling(item, defs, isOutOfFlow) {
1939
+ const dir = containerDirection(item.name, defs);
1940
+ if (dir) validateTiling(item.children, dir, `<${item.name}>`, isOutOfFlow, item.hug);
1941
+ for (const child of item.children) walkTiling(child, defs, isOutOfFlow);
1942
+ }
1943
+ function sizeOf(item) {
1944
+ const size = {};
1945
+ if (typeof item.height === "number") size.height = pct(item.height);
1946
+ if (typeof item.width === "number") size.width = pct(item.width);
1947
+ return size;
1948
+ }
1949
+ function dynSizeOf(item) {
1950
+ const dyn = {};
1951
+ if (isDimRef(item.height)) dyn.height = dimRefToDynamic(item.height);
1952
+ if (isDimRef(item.width)) dyn.width = dimRefToDynamic(item.width);
1953
+ return dyn.width || dyn.height ? dyn : void 0;
1954
+ }
1955
+ function dimRefToDynamic(d) {
1956
+ return d.whenTrue !== void 0 ? { ref: d.ref, whenTrue: d.whenTrue, whenFalse: d.whenFalse } : { ref: d.ref };
1957
+ }
1958
+ function hugStyles(hug) {
1959
+ const s = {};
1960
+ if (hug.w) {
1961
+ s.width = "fit-content";
1962
+ s.maxWidth = "100%";
1963
+ s.minWidth = "0";
1964
+ s.overflow = "hidden";
1965
+ s.textOverflow = "ellipsis";
1966
+ s.whiteSpace = "nowrap";
1967
+ }
1968
+ if (hug.h) {
1969
+ s.height = "fit-content";
1970
+ s.maxHeight = "100%";
1971
+ }
1972
+ return s;
1973
+ }
1974
+ function hugContainerStyles(hug) {
1975
+ const s = {};
1976
+ if (hug.w) s.width = "fit-content";
1977
+ if (hug.h) s.height = "fit-content";
1978
+ return s;
1979
+ }
1980
+ var HUG_INVALID_ON = /* @__PURE__ */ new Set(["Overlay", "Modal", "Children"]);
1981
+ function assertHuggable(item, isDef) {
1982
+ if (!item.hug) return;
1983
+ if (HUG_INVALID_ON.has(item.name) || isDef) {
1984
+ throw new Error(
1985
+ `[idml] "${item.name}" cannot use hug \u2014 nothing to content-size here. hug applies to components (e.g. Button/Text) and layout containers (Row/Col/Form), not definitions, slots, tables, or out-of-flow layers.`
1986
+ );
1987
+ }
1988
+ }
1989
+ function mkItem(name, args, height, width, anchor, children, style = {}) {
1990
+ return { name, args, height, width, anchor, children, style };
1991
+ }
1992
+ function expandTable(item, ctx) {
1993
+ const dataArg = item.args.find(
1994
+ (a) => typeof a === "string" && a.startsWith(VALUE_REF_PREFIX)
1995
+ );
1996
+ const columns = item.children.filter((c) => c.name === "Column");
1997
+ const headerCells = columns.map((col) => {
1998
+ const label = mkItem(
1999
+ "Text",
2000
+ [String(col.args[0] ?? "")],
2001
+ "auto",
2002
+ 100,
2003
+ col.anchor,
2004
+ [],
2005
+ { fontSize: "0.63vw" }
2006
+ );
2007
+ label.className = "font-medium text-gray-500 uppercase tracking-wider";
2008
+ const cell = mkItem("Col", [], "auto", col.width, col.anchor, [label], {
2009
+ paddingLeft: "1.6vw",
2010
+ paddingRight: "1.6vw",
2011
+ paddingTop: "0.8vw",
2012
+ paddingBottom: "0.8vw"
2013
+ });
2014
+ return cell;
2015
+ });
2016
+ const headerRow = mkItem("Row", [], "auto", 100, "top-left", headerCells, {
2017
+ borderBottom: "0.07vw solid #e5e7eb"
2018
+ });
2019
+ headerRow.className = "bg-gray-50";
2020
+ const bodyCells = columns.map((col) => {
2021
+ const cell = mkItem("Col", [], "auto", col.width, col.anchor, col.children, {
2022
+ paddingLeft: "1.6vw",
2023
+ paddingRight: "1.6vw",
2024
+ paddingTop: "1vw",
2025
+ paddingBottom: "1vw"
2026
+ });
2027
+ cell.className = "whitespace-nowrap";
2028
+ return cell;
2029
+ });
2030
+ const bodyRowTemplate = mkItem("Row", [], "auto", 100, "top-left", bodyCells, {
2031
+ borderBottom: "0.07vw solid #e5e7eb"
2032
+ });
2033
+ const repeat = mkItem("Repeat", dataArg ? [dataArg] : [], "auto", 100, "top-left", [bodyRowTemplate]);
2034
+ const tableStyle = { ...item.style };
2035
+ if (item.hug?.w) tableStyle.width = "fit-content";
2036
+ const tableCol = mkItem(
2037
+ "Col",
2038
+ [],
2039
+ item.hug?.h ? "auto" : item.height,
2040
+ item.hug?.w ? "auto" : item.width,
2041
+ item.anchor,
2042
+ [headerRow, repeat],
2043
+ tableStyle
2044
+ );
2045
+ tableCol.className = item.className;
2046
+ return convertItem(tableCol, ctx);
2047
+ }
2048
+ function convertItem(item, ctx) {
2049
+ const def = convertNode(item, ctx);
2050
+ if (item.visibility) def.visibility = item.visibility;
2051
+ const dyn = dynSizeOf(item);
2052
+ if (dyn) def.dynamicSize = dyn;
2053
+ if (item.classRefs?.length && !def.componentId) def.classRefs = item.classRefs;
2054
+ if (item.condClasses?.length) def.condClasses = item.condClasses;
2055
+ return def;
2056
+ }
2057
+ function convertNode(item, ctx) {
2058
+ assertHuggable(item, ctx.defs.has(item.name));
2059
+ const size = sizeOf(item);
2060
+ const idmlStyle = Object.keys(item.style).length ? item.style : void 0;
2061
+ const colAnchor = anchorToFlexProps(item.anchor, "column");
2062
+ const outOfFlow = ctx.isOutOfFlow(item.name);
2063
+ const cellStyle = outOfFlow ? { ...idmlStyle ?? {}, display: "contents" } : idmlStyle;
2064
+ if (item.name === "Children") {
2065
+ const slot = ctx.slotChildren ?? [];
2066
+ const childCtx = { ...ctx, slotChildren: void 0 };
2067
+ return {
2068
+ type: "flex",
2069
+ direction: "column",
2070
+ ...colAnchor,
2071
+ size,
2072
+ children: slot.map((child) => convertItem(child, childCtx)),
2073
+ idmlStyle
2074
+ };
2075
+ }
2076
+ const defBody = ctx.defs.get(item.name);
2077
+ if (defBody && !ctx.expanding.has(item.name)) {
2078
+ const params = ctx.defParams.get(item.name) ?? [];
2079
+ const bindings = /* @__PURE__ */ new Map();
2080
+ params.forEach((p, i) => bindings.set(p, item.args[i] ?? ""));
2081
+ const body = bindings.size ? defBody.map((t) => substituteParams(t, bindings)) : defBody;
2082
+ const innerCtx = {
2083
+ ...ctx,
2084
+ slotChildren: item.children,
2085
+ expanding: new Set(ctx.expanding).add(item.name)
2086
+ };
2087
+ return {
2088
+ type: "flex",
2089
+ direction: "column",
2090
+ ...colAnchor,
2091
+ size,
2092
+ children: body.map((t) => convertItem(t, innerCtx)),
2093
+ idmlStyle: cellStyle
2094
+ };
2095
+ }
2096
+ if (item.name === "Overlay") {
2097
+ const children2 = item.children.map((child) => {
2098
+ const layout = convertItem(child, ctx);
2099
+ const hugStyle = {};
2100
+ if (child.hug?.w) {
2101
+ hugStyle.width = "fit-content";
2102
+ if (layout.size?.width) hugStyle.maxWidth = layout.size.width;
2103
+ }
2104
+ if (child.hug?.h) {
2105
+ hugStyle.height = "fit-content";
2106
+ if (layout.size?.height) hugStyle.maxHeight = layout.size.height;
2107
+ }
2108
+ return {
2109
+ ...layout,
2110
+ idmlStyle: {
2111
+ ...layout.idmlStyle ?? {},
2112
+ position: "absolute",
2113
+ pointerEvents: "auto",
2114
+ ...anchorToAbsoluteInsets(child.anchor),
2115
+ ...hugStyle
2116
+ }
2117
+ };
2118
+ });
2119
+ return {
2120
+ type: "flex",
2121
+ direction: "column",
2122
+ size: { width: "100%", height: "100%" },
2123
+ children: children2,
2124
+ idmlStyle: {
2125
+ position: "fixed",
2126
+ top: "0",
2127
+ left: "0",
2128
+ right: "0",
2129
+ bottom: "0",
2130
+ pointerEvents: "none",
2131
+ outline: "none",
2132
+ zIndex: "50",
2133
+ ...idmlStyle ?? {}
2134
+ },
2135
+ ...item.className ? { className: item.className } : {}
2136
+ };
2137
+ }
2138
+ if (item.name === "Table") {
2139
+ return expandTable(item, ctx);
2140
+ }
2141
+ if (LAYOUT_ITEMS.has(item.name)) {
2142
+ const direction = item.name === "Row" ? "row" : "column";
2143
+ const { justifyContent, alignItems } = anchorToFlexProps(item.anchor, direction);
2144
+ const children2 = item.children.map((child) => convertItem(child, ctx));
2145
+ const mainHug = direction === "column" ? item.hug?.h : item.hug?.w;
2146
+ if (mainHug) {
2147
+ for (const ch of children2) {
2148
+ if (!ch.size) continue;
2149
+ if (direction === "column") delete ch.size.height;
2150
+ else delete ch.size.width;
2151
+ }
2152
+ }
2153
+ const crossHug = direction === "column" ? item.hug?.w : item.hug?.h;
2154
+ const containerStyle = crossHug ? { ...idmlStyle ?? {}, ...hugContainerStyles({ w: direction === "column", h: direction === "row" }) } : idmlStyle;
2155
+ return {
2156
+ type: "flex",
2157
+ direction,
2158
+ justifyContent,
2159
+ alignItems,
2160
+ size,
2161
+ children: children2,
2162
+ idmlStyle: containerStyle,
2163
+ ...item.className ? { className: item.className } : {}
2164
+ };
2165
+ }
2166
+ if (item.name === "Select" && item.children.some((c) => c.name === "Option")) {
2167
+ const id2 = genId("select");
2168
+ const options = item.children.filter((c) => c.name === "Option").map((c) => ({ value: c.args[0] ?? "", label: c.args[1] ?? c.args[0] ?? "" }));
2169
+ const def = buildComponentDef(item, id2);
2170
+ def.props = { ...def.props, options };
2171
+ ctx.components.push(def);
2172
+ return { type: "flex", direction: "column", ...colAnchor, size, children: [], componentId: id2 };
2173
+ }
2174
+ const id = genId(item.name.toLowerCase());
2175
+ ctx.components.push(buildComponentDef(item, id));
2176
+ const children = item.children.map((child) => convertItem(child, ctx));
2177
+ const hugCell = {};
2178
+ if (item.hug?.w) {
2179
+ hugCell.width = "fit-content";
2180
+ if (typeof item.width === "number") hugCell.maxWidth = `${item.width}%`;
2181
+ }
2182
+ if (item.hug?.h) {
2183
+ hugCell.height = "fit-content";
2184
+ if (typeof item.height === "number") hugCell.maxHeight = `${item.height}%`;
2185
+ }
2186
+ const cellIdml = outOfFlow ? { ...cellStyle ?? {}, ...hugCell } : Object.keys(hugCell).length ? hugCell : void 0;
2187
+ return {
2188
+ type: "flex",
2189
+ direction: "column",
2190
+ ...colAnchor,
2191
+ size,
2192
+ children,
2193
+ componentId: id,
2194
+ ...cellIdml ? { idmlStyle: cellIdml } : {}
2195
+ };
2196
+ }
2197
+ function anchorToComponentStyle(anchor, componentType) {
2198
+ const parts = anchor.split("-");
2199
+ const h = parts.length === 1 ? parts[0] : parts[1] ?? parts[0];
2200
+ const v = parts.length === 1 ? parts[0] : parts[0];
2201
+ const css = {};
2202
+ if (componentType === "Text" || componentType === "Heading") {
2203
+ if (h === "center") css.textAlign = "center";
2204
+ else if (h === "right") css.textAlign = "right";
2205
+ }
2206
+ if (componentType === "Button") {
2207
+ css.display = "flex";
2208
+ css.justifyContent = h === "center" ? "center" : h === "right" ? "flex-end" : "flex-start";
2209
+ css.alignItems = v === "center" ? "center" : v === "bottom" ? "flex-end" : "flex-start";
2210
+ }
2211
+ return css;
2212
+ }
2213
+ function buildComponentDef(item, id) {
2214
+ const anchorStyle = anchorToComponentStyle(item.anchor, item.name);
2215
+ const hug = item.hug ? hugStyles(item.hug) : {};
2216
+ const merged = { ...anchorStyle, ...item.style, ...hug };
2217
+ const idmlStyle = Object.keys(merged).length ? merged : void 0;
2218
+ const valueRefs = [];
2219
+ const modelRefs = [];
2220
+ const handlerRefs = [];
2221
+ const literals = [];
2222
+ for (const a of item.args) {
2223
+ if (typeof a === "string" && a.startsWith(VALUE_REF_PREFIX)) valueRefs.push(a.slice(VALUE_REF_PREFIX.length));
2224
+ else if (typeof a === "string" && a.startsWith(MODEL_REF_PREFIX)) modelRefs.push(a.slice(MODEL_REF_PREFIX.length));
2225
+ else if (typeof a === "string" && a.startsWith(FN_REF_PREFIX)) handlerRefs.push(a.slice(FN_REF_PREFIX.length));
2226
+ else literals.push(a);
2227
+ }
2228
+ const primaryProp = PRIMARY_PROP[item.name] ?? "value";
2229
+ const bindings = [
2230
+ ...valueRefs.map((methodId) => ({ prop: primaryProp, methodId, kind: "value" })),
2231
+ ...modelRefs.map((methodId) => ({ prop: primaryProp, methodId, kind: "model" })),
2232
+ ...handlerRefs.map((methodId) => ({ prop: "onClick", methodId })),
2233
+ // Dynamic classes (`@method` tokens in a class block) resolve to strings that
2234
+ // are appended to className per render.
2235
+ ...(item.classRefs ?? []).map((methodId) => ({ prop: "className", methodId, kind: "value" }))
2236
+ ];
2237
+ const withBindings = (def) => {
2238
+ const withB = bindings.length ? { ...def, bindings } : def;
2239
+ return item.className ? { ...withB, className: item.className } : withB;
2240
+ };
2241
+ const [first, second] = literals;
2242
+ switch (item.name) {
2243
+ case "Text":
2244
+ return withBindings({ id, type: "Text", props: { text: String(first ?? "") }, idmlStyle });
2245
+ case "Heading":
2246
+ return withBindings({
2247
+ id,
2248
+ type: "Heading",
2249
+ props: { text: String(first ?? ""), level: typeof second === "number" ? second : 1 },
2250
+ idmlStyle
2251
+ });
2252
+ case "Button": {
2253
+ const route = literals.find((a) => typeof a === "string" && a.startsWith("/"));
2254
+ const label = literals.find((a) => typeof a === "string" && !a.startsWith("/"));
2255
+ const props = { text: String(label ?? "") };
2256
+ if (route) props.href = route;
2257
+ return withBindings({ id, type: "Button", props, idmlStyle });
2258
+ }
2259
+ case "Image":
2260
+ return withBindings({ id, type: "Image", props: { src: String(first ?? ""), alt: String(second ?? "") }, idmlStyle });
2261
+ case "Label":
2262
+ return withBindings({ id, type: "Label", props: { text: String(first ?? "") }, idmlStyle });
2263
+ case "Icon": {
2264
+ const props = { name: String(first ?? "") };
2265
+ for (const rest of literals.slice(1)) {
2266
+ if (typeof rest === "number") props.size = rest;
2267
+ else if (typeof rest === "string") props.color = rest;
2268
+ }
2269
+ return withBindings({ id, type: "Icon", props, idmlStyle });
2270
+ }
2271
+ case "Option": {
2272
+ const value = first ?? "";
2273
+ const label = second ?? first ?? "";
2274
+ return withBindings({ id, type: "Option", props: { value, label }, idmlStyle });
2275
+ }
2276
+ case "Input":
2277
+ case "Textarea":
2278
+ return withBindings({
2279
+ id,
2280
+ type: item.name,
2281
+ props: first != null ? { placeholder: String(first) } : {},
2282
+ idmlStyle
2283
+ });
2284
+ default:
2285
+ return withBindings({
2286
+ id,
2287
+ type: item.name,
2288
+ props: Object.fromEntries(literals.map((v, i) => [`arg${i}`, v])),
2289
+ idmlStyle
2290
+ });
2291
+ }
2292
+ }
2293
+ var PRIMARY_PROP = {
2294
+ Text: "text",
2295
+ Heading: "text",
2296
+ Button: "text",
2297
+ Image: "src",
2298
+ Input: "value",
2299
+ Textarea: "value",
2300
+ Select: "value",
2301
+ Checkbox: "checked",
2302
+ Table: "data",
2303
+ Repeat: "data",
2304
+ Modal: "open",
2305
+ Icon: "name"
2306
+ };
2307
+ var DEFAULT_TOKENS = {
2308
+ colors: [
2309
+ { name: "primary", value: "#1a56db", darkValue: "#60a5fa" },
2310
+ { name: "surface", value: "#ffffff", darkValue: "#1e1e2e" },
2311
+ { name: "on-surface", value: "#111827", darkValue: "#f9fafb" },
2312
+ { name: "danger", value: "#dc2626", darkValue: "#f87171" }
2313
+ ],
2314
+ typography: [
2315
+ { name: "heading-xl", fontSize: "2.25rem", fontWeight: 700, lineHeight: "1.25" },
2316
+ { name: "body-md", fontSize: "1rem", fontWeight: 400, lineHeight: "1.6" },
2317
+ { name: "label-sm", fontSize: "0.75rem", fontWeight: 500, lineHeight: "1.4" }
2318
+ ],
2319
+ spacing: [
2320
+ { name: "gap-sm", value: "0.5rem" },
2321
+ { name: "gap-md", value: "1rem" },
2322
+ { name: "gap-lg", value: "2rem" }
2323
+ ]
2324
+ };
2325
+ function parseIdml(source, options) {
2326
+ _idCounter = 0;
2327
+ const parser = new IdmlParser(tokenize(source));
2328
+ const parsedPages = parser.parseFile(options?.resolve);
2329
+ const isOutOfFlow = makeOutOfFlowPredicate(parser.defRegistry);
2330
+ for (const { route, items } of parsedPages) {
2331
+ validateTiling(items, "column", `page ${route}`, isOutOfFlow);
2332
+ items.forEach((it) => walkTiling(it, parser.defRegistry, isOutOfFlow));
2333
+ }
2334
+ for (const [name, body] of parser.defRegistry) {
2335
+ validateTiling(body, "column", `define ${name}`, isOutOfFlow);
2336
+ body.forEach((it) => walkTiling(it, parser.defRegistry, isOutOfFlow));
2337
+ }
2338
+ const pages = parsedPages.map(({ route, scroll, items }) => {
2339
+ const components = [];
2340
+ const ctx = {
2341
+ components,
2342
+ defs: parser.defRegistry,
2343
+ defParams: parser.defParamRegistry,
2344
+ expanding: /* @__PURE__ */ new Set(),
2345
+ isOutOfFlow
2346
+ };
2347
+ const layoutChildren = items.map((item) => convertItem(item, ctx));
2348
+ const rootLayout = {
2349
+ type: "flex",
2350
+ direction: "column",
2351
+ size: { width: "100%", height: "100%" },
2352
+ children: layoutChildren,
2353
+ ...scroll ? { idmlStyle: { overflowY: "auto" } } : {}
2354
+ };
2355
+ return { route, layout: rootLayout, components };
2356
+ });
2357
+ return { version: "1", tokens: DEFAULT_TOKENS, pages };
2358
+ }
2359
+ export {
2360
+ BUILTIN_COMPONENTS,
2361
+ ComponentRenderer,
2362
+ ConfigProvider,
2363
+ ConfigRenderer,
2364
+ EditorPage,
2365
+ LayoutRenderer,
2366
+ UIConfigSchema,
2367
+ clearComponentRegistry,
2368
+ clearRegistry,
2369
+ getComponent,
2370
+ getMethod,
2371
+ parseIdml,
2372
+ registerComponent,
2373
+ registerMethod,
2374
+ safeValidateConfig,
2375
+ useConfigContext,
2376
+ useRegisteredMethod,
2377
+ useVisibility,
2378
+ validateConfig
2379
+ };
2380
+ //# sourceMappingURL=index.js.map