@neuralumina/lumina-ui 0.1.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/README.md ADDED
@@ -0,0 +1,1694 @@
1
+ # LuminaUI
2
+
3
+ LuminaUI is a lightweight, Flutter-inspired UI library for building web
4
+ interfaces with plain JavaScript, HTML, and the browser DOM.
5
+
6
+ The core idea is simple: build your UI as a tree of JavaScript widget
7
+ functions. Each widget returns a small virtual node object, and LuminaUI turns
8
+ that tree into real DOM.
9
+
10
+ No JSX. No build step. No runtime dependencies. Use it from npm, or open
11
+ `index.html` and run the local demo directly.
12
+
13
+ ```js
14
+ Column({ gap: 12 }, [
15
+ Text("Hello LuminaUI"),
16
+ Button({ text: "Click Me", onClick: () => console.log("clicked") }),
17
+ ])
18
+ ```
19
+
20
+ ## Why LuminaUI?
21
+
22
+ Traditional small web projects often spread one feature across HTML, CSS, and
23
+ JavaScript files. LuminaUI keeps the UI structure, behavior, and local styling
24
+ close together through a single composable model.
25
+
26
+ LuminaUI gives you:
27
+
28
+ - Widget-tree composition inspired by Flutter
29
+ - Vanilla JavaScript modules
30
+ - Real DOM output
31
+ - A tiny reactive state primitive
32
+ - Layout, forms, navigation, feedback, scrolling, display, and animation widgets
33
+ - No required bundler, compiler, or framework runtime
34
+ - A clean ESM package entry point for library usage
35
+
36
+ LuminaUI is still experimental, but it is now large enough for developers to
37
+ build small apps and understand how the framework is intended to grow.
38
+
39
+ ## Quick Start
40
+
41
+ After publishing the package, install it in an app:
42
+
43
+ ```bash
44
+ npm install @chimuka_amel/lumina-ui
45
+ ```
46
+
47
+ If you are using Vite, Parcel, Webpack, or another bundler/dev server, you can
48
+ import the package by name:
49
+
50
+ ```js
51
+ import { mount, Column, Text, Button } from "@chimuka_amel/lumina-ui";
52
+
53
+ function App() {
54
+ return Column({ gap: 12, padding: 16 }, [
55
+ Text("Hello from LuminaUI"),
56
+ Button({ text: "Click me", onClick: () => console.log("clicked") }),
57
+ ]);
58
+ }
59
+
60
+ mount(App, document.getElementById("root"));
61
+ ```
62
+
63
+ If you are serving plain files with `python3 -m http.server`, add an import map
64
+ to `index.html` because browsers cannot resolve npm package names by
65
+ themselves:
66
+
67
+ ```html
68
+ <!DOCTYPE html>
69
+ <html lang="en">
70
+ <head>
71
+ <meta charset="UTF-8" />
72
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
73
+ <title>LuminaUI App</title>
74
+ <script type="importmap">
75
+ {
76
+ "imports": {
77
+ "@chimuka_amel/lumina-ui": "./node_modules/@chimuka_amel/lumina-ui/lumina-ui.js"
78
+ }
79
+ }
80
+ </script>
81
+ </head>
82
+ <body>
83
+ <div id="root"></div>
84
+ <script type="module" src="./app.js"></script>
85
+ </body>
86
+ </html>
87
+ ```
88
+
89
+ Or clone this repository and open `index.html` in a browser.
90
+
91
+ ```bash
92
+ git clone https://github.com/<your-username>/lumina-ui.git
93
+ cd LuminaUI
94
+ ```
95
+
96
+ If your browser blocks ES module imports from local files, serve the folder:
97
+
98
+ ```bash
99
+ python3 -m http.server 4173
100
+ ```
101
+
102
+ Then open:
103
+
104
+ ```text
105
+ http://127.0.0.1:4173/
106
+ ```
107
+
108
+ The demo app is mounted from `index.html`:
109
+
110
+ ```html
111
+ <div id="root"></div>
112
+ <script type="module">
113
+ import { mount } from "./lumina-ui.js";
114
+ import { App } from "./lumina-ui/app/App.js";
115
+
116
+ const root = document.getElementById("root");
117
+ mount(App, root);
118
+ </script>
119
+ ```
120
+
121
+ ## Project Structure
122
+
123
+ ```text
124
+ LuminaUI/
125
+ ├── index.html
126
+ ├── lumina-ui.js
127
+ ├── package.json
128
+ ├── README.md
129
+ ├── scripts/
130
+ │ └── smoke-test.mjs
131
+ └── lumina-ui/
132
+ ├── app/
133
+ │ ├── App.js
134
+ │ └── ecommerce/
135
+ │ ├── catalog.json
136
+ │ ├── components.js
137
+ │ ├── data.js
138
+ │ ├── EcommerceApp.js
139
+ │ └── store.js
140
+ ├── core/
141
+ │ ├── element.js
142
+ │ ├── renderer.js
143
+ │ └── state.js
144
+ └── widgets/
145
+ ├── animation.js
146
+ ├── accessibility.js
147
+ ├── controls.js
148
+ ├── display.js
149
+ ├── feedback.js
150
+ ├── forms.js
151
+ ├── interaction.js
152
+ ├── layout.js
153
+ ├── navigation.js
154
+ ├── scrolling.js
155
+ ├── text.js
156
+ └── utils.js
157
+ ```
158
+
159
+ ### Important files
160
+
161
+ - `package.json`: npm package metadata, ESM exports, publish file list, and scripts.
162
+ - `lumina-ui.js`: public library entry point that re-exports the framework API.
163
+ - `scripts/smoke-test.mjs`: minimal DOM smoke test for renderer and widget behavior.
164
+ - `lumina-ui/app/App.js`: local demo application showing how widgets are used.
165
+ - `lumina-ui/app/ecommerce/*`: advanced ecommerce demo app. It is intentionally
166
+ not exported from the package entry point.
167
+ - `lumina-ui/core/renderer.js`: `mount()` and DOM patching.
168
+ - `lumina-ui/core/state.js`: `createState`, `useEffect`, and `createStore`.
169
+ - `lumina-ui/core/element.js`: low-level DOM element creation.
170
+ - `lumina-ui/widgets/*`: widget modules grouped by purpose.
171
+
172
+ ## Package Scripts
173
+
174
+ ```bash
175
+ npm run check
176
+ npm test
177
+ npm run pack:dry
178
+ ```
179
+
180
+ - `npm run check` syntax-checks every JavaScript module in the repository.
181
+ - `npm test` runs the syntax check and the DOM smoke test.
182
+ - `npm run pack:dry` previews the npm tarball contents without publishing.
183
+
184
+ The published package surface is framework-only: `lumina-ui.js`,
185
+ `lumina-ui/core/*`, and `lumina-ui/widgets/*`. The ecommerce app remains a demo
186
+ consumer inside the repository.
187
+
188
+ ## Publishing
189
+
190
+ Before the first publish, make sure the `name` in `package.json` matches an npm
191
+ scope you control. For the current scoped package name, publish publicly with:
192
+
193
+ ```bash
194
+ npm test
195
+ npm run pack:dry
196
+ npm publish --access public
197
+ ```
198
+
199
+ ## Core Mental Model
200
+
201
+ Every UI piece is a function that returns one of these:
202
+
203
+ - a virtual node object: `{ tag, props, children, key }`
204
+ - a string or number
205
+ - an array of children
206
+ - `null`, `undefined`, or `false` to render nothing
207
+ - a real DOM `Node`, when needed
208
+
209
+ Most widgets are called in one of two styles.
210
+
211
+ Compact children-first style:
212
+
213
+ ```js
214
+ Column([
215
+ Text("First"),
216
+ Text("Second"),
217
+ ])
218
+ ```
219
+
220
+ Configured `props, children` style:
221
+
222
+ ```js
223
+ Column({ gap: 12, padding: 16 }, [
224
+ Text("First"),
225
+ Text("Second"),
226
+ ])
227
+ ```
228
+
229
+ Single-child prop style:
230
+
231
+ ```js
232
+ Padding({
233
+ padding: 16,
234
+ child: Text("Inside padding"),
235
+ })
236
+ ```
237
+
238
+ LuminaUI uses plain JavaScript objects for style:
239
+
240
+ ```js
241
+ Container(
242
+ {
243
+ padding: 16,
244
+ decoration: {
245
+ color: "#ffffff",
246
+ border: "1px solid #e5e7eb",
247
+ borderRadius: 8,
248
+ },
249
+ },
250
+ [Text("Card-like content")],
251
+ )
252
+ ```
253
+
254
+ Numbers used for dimensions are generally converted to pixels by widget
255
+ helpers. Strings are passed through.
256
+
257
+ ```js
258
+ SizedBox({ width: 120, height: "50vh" })
259
+ ```
260
+
261
+ ## Importing
262
+
263
+ When installed as a package, import from the top-level entry point:
264
+
265
+ ```js
266
+ import {
267
+ mount,
268
+ useState,
269
+ Column,
270
+ Text,
271
+ Button,
272
+ } from "@chimuka_amel/lumina-ui";
273
+ ```
274
+
275
+ Package subpath imports are available for smaller, explicit imports:
276
+
277
+ ```js
278
+ import { mount } from "@chimuka_amel/lumina-ui/core/renderer";
279
+ import { createState } from "@chimuka_amel/lumina-ui/core/state";
280
+ import { Column, Row } from "@chimuka_amel/lumina-ui/widgets/layout";
281
+ import { Button } from "@chimuka_amel/lumina-ui/widgets/controls";
282
+ import { Text } from "@chimuka_amel/lumina-ui/widgets/text";
283
+ ```
284
+
285
+ When using this repository directly in the browser, import from local files:
286
+
287
+ ```js
288
+ import {
289
+ mount,
290
+ useState,
291
+ Column,
292
+ Text,
293
+ Button,
294
+ } from "./lumina-ui.js";
295
+ ```
296
+
297
+ Local direct module imports work too:
298
+
299
+ ```js
300
+ import { mount } from "./lumina-ui/core/renderer.js";
301
+ import { createState } from "./lumina-ui/core/state.js";
302
+ import { Column, Row } from "./lumina-ui/widgets/layout.js";
303
+ import { Button } from "./lumina-ui/widgets/controls.js";
304
+ import { Text } from "./lumina-ui/widgets/text.js";
305
+ ```
306
+
307
+ ## Rendering
308
+
309
+ Mount an app component with `mount(componentFn, container)`.
310
+
311
+ ```js
312
+ import { mount, Column, Text } from "./lumina-ui.js";
313
+
314
+ function App() {
315
+ return Column([
316
+ Text("Mounted with LuminaUI"),
317
+ ]);
318
+ }
319
+
320
+ mount(App, document.getElementById("root"));
321
+ ```
322
+
323
+ `mount()` passes a `forceUpdate` function into your app:
324
+
325
+ ```js
326
+ function App(forceUpdate) {
327
+ // Use forceUpdate with createState subscriptions.
328
+ }
329
+ ```
330
+
331
+ The returned update function can also unmount:
332
+
333
+ ```js
334
+ const update = mount(App, root);
335
+ update.unmount();
336
+ ```
337
+
338
+ ## State
339
+
340
+ LuminaUI state is explicit. The top-level entry point exports this as
341
+ `useState`, and the core state module exports the same primitive as
342
+ `createState`.
343
+
344
+ ```js
345
+ import { useState } from "./lumina-ui.js";
346
+
347
+ const [getValue, setValue, subscribe] = useState(initialValue);
348
+ ```
349
+
350
+ The direct core import is also available:
351
+
352
+ ```js
353
+ import { createState } from "./lumina-ui/core/state.js";
354
+
355
+ const [getValue, setValue, subscribe] = createState(initialValue);
356
+ ```
357
+
358
+ - `getValue()` reads the current value.
359
+ - `setValue(next)` updates the value.
360
+ - `subscribe(fn)` runs `fn` whenever the value changes.
361
+
362
+ Basic counter:
363
+
364
+ ```js
365
+ import { mount, useState, Column, Row, Text, Button } from "./lumina-ui.js";
366
+
367
+ const [count, setCount, subscribeCount] = useState(0);
368
+
369
+ function App(forceUpdate) {
370
+ subscribeCount(forceUpdate);
371
+
372
+ return Column({ gap: 12 }, [
373
+ Text(`Count: ${count()}`),
374
+ Row({ gap: 8 }, [
375
+ Button({ text: "-", onClick: () => setCount((value) => value - 1) }),
376
+ Button({ text: "+", onClick: () => setCount((value) => value + 1) }),
377
+ ]),
378
+ ]);
379
+ }
380
+
381
+ mount(App, document.getElementById("root"));
382
+ ```
383
+
384
+ Recommended pattern for app-level state:
385
+
386
+ ```js
387
+ const [getDarkMode, setDarkMode, subscribeDarkMode] = useState(false);
388
+ const subscribedUpdates = new WeakSet();
389
+
390
+ function bindState(forceUpdate) {
391
+ if (typeof forceUpdate !== "function" || subscribedUpdates.has(forceUpdate)) {
392
+ return;
393
+ }
394
+
395
+ subscribeDarkMode(forceUpdate);
396
+ subscribedUpdates.add(forceUpdate);
397
+ }
398
+
399
+ export function App(forceUpdate) {
400
+ bindState(forceUpdate);
401
+
402
+ return Switch({
403
+ value: getDarkMode(),
404
+ onChange: setDarkMode,
405
+ });
406
+ }
407
+ ```
408
+
409
+ This avoids repeatedly adding the same update function as a subscriber on every
410
+ render.
411
+
412
+ ## Store
413
+
414
+ For reducer-style state, use `createStore(reducer, initialState)`.
415
+
416
+ ```js
417
+ const store = createStore(
418
+ (state, action) => {
419
+ if (action.type === "increment") return { count: state.count + 1 };
420
+ return state;
421
+ },
422
+ { count: 0 },
423
+ );
424
+
425
+ store.subscribe(forceUpdate);
426
+ store.dispatch({ type: "increment" });
427
+ store.getState();
428
+ ```
429
+
430
+ ## Widget Reference
431
+
432
+ This section lists the current widget families and the most useful props. The
433
+ API is intentionally small and JavaScript-friendly.
434
+
435
+ ### Layout Widgets
436
+
437
+ Import:
438
+
439
+ ```js
440
+ import {
441
+ Column,
442
+ Row,
443
+ Container,
444
+ Center,
445
+ Align,
446
+ Padding,
447
+ SizedBox,
448
+ Flexible,
449
+ Expanded,
450
+ Spacer,
451
+ Wrap,
452
+ Stack,
453
+ Positioned,
454
+ Divider,
455
+ Card,
456
+ AspectRatio,
457
+ Baseline,
458
+ ConstrainedBox,
459
+ DecoratedBox,
460
+ FractionallySizedBox,
461
+ LayoutBuilder,
462
+ LimitedBox,
463
+ Offstage,
464
+ OverflowBox,
465
+ RotatedBox,
466
+ SizedOverflowBox,
467
+ Transform,
468
+ } from "./lumina-ui.js";
469
+ ```
470
+
471
+ #### `Column(props, children)`
472
+
473
+ Flex column layout.
474
+
475
+ Common props:
476
+
477
+ - `gap`
478
+ - `padding`
479
+ - `mainAxisAlignment`
480
+ - `crossAxisAlignment`
481
+ - `align`
482
+ - `style`
483
+
484
+ ```js
485
+ Column({ gap: 10, padding: 16 }, [
486
+ Text("Top"),
487
+ Text("Bottom"),
488
+ ])
489
+ ```
490
+
491
+ #### `Row(props, children)`
492
+
493
+ Flex row layout.
494
+
495
+ ```js
496
+ Row({ gap: 8, mainAxisAlignment: "spaceBetween" }, [
497
+ Text("Left"),
498
+ Button({ text: "Right" }),
499
+ ])
500
+ ```
501
+
502
+ #### `Container(props, children)`
503
+
504
+ General-purpose box.
505
+
506
+ Common props:
507
+
508
+ - `width`, `height`
509
+ - `minWidth`, `minHeight`, `maxWidth`, `maxHeight`
510
+ - `padding`, `margin`
511
+ - `color`
512
+ - `alignment`
513
+ - `decoration`
514
+ - `style`
515
+
516
+ ```js
517
+ Container(
518
+ {
519
+ width: 320,
520
+ padding: { vertical: 12, horizontal: 16 },
521
+ decoration: {
522
+ color: "#ffffff",
523
+ border: "1px solid #e5e7eb",
524
+ borderRadius: 8,
525
+ boxShadow: "0 1px 2px rgba(15, 23, 42, 0.08)",
526
+ },
527
+ },
528
+ [Text("Container content")],
529
+ )
530
+ ```
531
+
532
+ #### `Center(children)` and `Align(props, children)`
533
+
534
+ Use `Center` for centered content. Use `Align` for named alignments.
535
+
536
+ ```js
537
+ Center([Text("Centered")])
538
+
539
+ Align({ alignment: "bottomRight" }, [
540
+ Text("Bottom right"),
541
+ ])
542
+ ```
543
+
544
+ Supported alignment names include:
545
+
546
+ - `center`
547
+ - `topCenter`
548
+ - `bottomCenter`
549
+ - `centerLeft`
550
+ - `centerRight`
551
+ - `topLeft`
552
+ - `topRight`
553
+ - `bottomLeft`
554
+ - `bottomRight`
555
+
556
+ #### `Padding(props, children)`
557
+
558
+ ```js
559
+ Padding({ padding: { vertical: 8, horizontal: 12 } }, [
560
+ Text("Padded"),
561
+ ])
562
+ ```
563
+
564
+ Padding accepts:
565
+
566
+ - number: `16`
567
+ - string: `"1rem"`
568
+ - object: `{ all, vertical, horizontal, top, right, bottom, left }`
569
+
570
+ #### `SizedBox(props, children)`
571
+
572
+ Adds fixed width and/or height.
573
+
574
+ ```js
575
+ SizedBox({ height: 16 })
576
+ SizedBox({ width: 200, child: Text("Fixed width") })
577
+ ```
578
+
579
+ #### `Flexible`, `Expanded`, and `Spacer`
580
+
581
+ Use these inside `Row` or `Column`.
582
+
583
+ ```js
584
+ Row([
585
+ Expanded([Text("Takes available space")]),
586
+ Spacer(),
587
+ Button({ text: "Action" }),
588
+ ])
589
+ ```
590
+
591
+ #### `Wrap(props, children)`
592
+
593
+ Wrapping flex layout.
594
+
595
+ ```js
596
+ Wrap({ gap: 8 }, [
597
+ chip("Text"),
598
+ chip("Container"),
599
+ chip("Button"),
600
+ ])
601
+ ```
602
+
603
+ #### `Stack` and `Positioned`
604
+
605
+ Layer children relative to a container.
606
+
607
+ ```js
608
+ Stack({ height: 160 }, [
609
+ Container({ color: "#eef2ff", height: "100%" }),
610
+ Positioned({
611
+ right: 12,
612
+ bottom: 12,
613
+ child: Button({ text: "Floating" }),
614
+ }),
615
+ ])
616
+ ```
617
+
618
+ #### `Divider`
619
+
620
+ ```js
621
+ Divider()
622
+ Divider({ direction: "vertical", thickness: 1 })
623
+ ```
624
+
625
+ #### `Card`
626
+
627
+ Card is a styled `Container` shortcut.
628
+
629
+ ```js
630
+ Card({ elevation: 2, padding: 16 }, [
631
+ Text("Card content"),
632
+ ])
633
+ ```
634
+
635
+ #### Additional Flutter-inspired layout wrappers
636
+
637
+ These widgets map Flutter layout ideas onto browser CSS:
638
+
639
+ - `AspectRatio({ aspectRatio })`: uses CSS `aspect-ratio`.
640
+ - `Baseline({ baseline })`: aligns inline children on a text baseline.
641
+ - `ConstrainedBox({ minWidth, maxWidth, minHeight, maxHeight })`: applies CSS constraints.
642
+ - `DecoratedBox({ decoration })`: paints background, border, radius, shadow, or gradient.
643
+ - `FractionallySizedBox({ widthFactor, heightFactor })`: sizes by parent percentage.
644
+ - `LayoutBuilder({ constraints, builder })`: calls a builder with declared constraints.
645
+ - `LimitedBox({ maxWidth, maxHeight })`: applies max constraints.
646
+ - `Offstage({ offstage })`: hides children with `display: none`.
647
+ - `OverflowBox(...)`: allows visible overflow.
648
+ - `RotatedBox({ quarterTurns })`: rotates in 90-degree increments.
649
+ - `SizedOverflowBox({ width, height })`: fixed-size overflow wrapper.
650
+ - `Transform({ translate, rotate, scale, skew })`: applies CSS transforms.
651
+
652
+ ```js
653
+ AspectRatio({ aspectRatio: "16 / 9" }, [
654
+ Container({ color: "#eef2ff" }, [
655
+ Center([Text("16:9")]),
656
+ ]),
657
+ ])
658
+
659
+ Transform({ rotate: "-4deg", scale: 1.1 }, [
660
+ Card([Text("Transformed")]),
661
+ ])
662
+ ```
663
+
664
+ ### Text Widgets
665
+
666
+ Import:
667
+
668
+ ```js
669
+ import {
670
+ Text,
671
+ Heading,
672
+ Caption,
673
+ DefaultTextStyle,
674
+ RichText,
675
+ } from "./lumina-ui.js";
676
+ ```
677
+
678
+ #### `Text(content, props)`
679
+
680
+ Common props:
681
+
682
+ - `size`
683
+ - `weight`
684
+ - `align`
685
+ - `color`
686
+ - `lineHeight`
687
+ - `maxLines`
688
+ - `as`
689
+ - `style`
690
+
691
+ ```js
692
+ Text("Hello", { size: 18, weight: 700, color: "#2563eb" })
693
+ Text("Paragraph", { as: "p", lineHeight: 1.7 })
694
+ ```
695
+
696
+ #### `Heading(props, children)`
697
+
698
+ ```js
699
+ Heading({ level: 2 }, "Section title")
700
+ ```
701
+
702
+ #### `Caption(props, children)`
703
+
704
+ ```js
705
+ Caption({ color: "#6b7280" }, "Small helper text")
706
+ ```
707
+
708
+ #### `DefaultTextStyle(props, children)`
709
+
710
+ Applies inherited CSS text styles to descendants.
711
+
712
+ ```js
713
+ DefaultTextStyle({ color: "#6b7280", size: 14 }, [
714
+ Text("This inherits the default text color"),
715
+ ])
716
+ ```
717
+
718
+ #### `RichText(props)`
719
+
720
+ Renders multiple inline spans with different styles.
721
+
722
+ ```js
723
+ RichText({
724
+ spans: [
725
+ { text: "Rich ", style: { fontWeight: 800 } },
726
+ { text: "text", style: { color: "#2563eb" } },
727
+ ],
728
+ })
729
+ ```
730
+
731
+ ### Controls
732
+
733
+ Import:
734
+
735
+ ```js
736
+ import { Button, Input, TextField, Checkbox, Switch } from "./lumina-ui.js";
737
+ ```
738
+
739
+ #### `Button(props)`
740
+
741
+ Props:
742
+
743
+ - `text`
744
+ - `onClick`
745
+ - `variant`: `primary`, `secondary`, `text`, `danger`
746
+ - `disabled`
747
+ - `type`
748
+ - `style`
749
+
750
+ ```js
751
+ Button({
752
+ text: "Save",
753
+ variant: "primary",
754
+ onClick: save,
755
+ })
756
+ ```
757
+
758
+ #### `Input(props)` and `TextField(props)`
759
+
760
+ `Input` supports text inputs and checkbox input behavior. `TextField` is a
761
+ convenience wrapper for text input.
762
+
763
+ ```js
764
+ Input({
765
+ value: name(),
766
+ placeholder: "Your name",
767
+ onChange: setName,
768
+ })
769
+
770
+ Input({
771
+ type: "checkbox",
772
+ value: accepted(),
773
+ onChange: setAccepted,
774
+ })
775
+ ```
776
+
777
+ #### `Checkbox(props)`
778
+
779
+ ```js
780
+ Checkbox({
781
+ checked: done(),
782
+ label: "Completed",
783
+ onChange: setDone,
784
+ })
785
+ ```
786
+
787
+ #### `Switch(props)`
788
+
789
+ ```js
790
+ Switch({
791
+ value: enabled(),
792
+ onChange: setEnabled,
793
+ ariaLabel: "Enable notifications",
794
+ })
795
+ ```
796
+
797
+ ### Display Widgets
798
+
799
+ Import:
800
+
801
+ ```js
802
+ import {
803
+ Icon,
804
+ Image,
805
+ CircleAvatar,
806
+ Badge,
807
+ Placeholder,
808
+ ClipRRect,
809
+ ClipOval,
810
+ ClipRect,
811
+ ClipPath,
812
+ FittedBox,
813
+ Opacity,
814
+ PhysicalModel,
815
+ ShaderMask,
816
+ } from "./lumina-ui.js";
817
+ ```
818
+
819
+ #### `Icon(propsOrName, maybeProps)`
820
+
821
+ ```js
822
+ Icon("home")
823
+ Icon({ name: "settings", size: 28, color: "#2563eb" })
824
+ ```
825
+
826
+ Available built-in icon names are simple text symbols:
827
+
828
+ ```text
829
+ add, remove, close, check, search, menu, home, settings, person, info,
830
+ warning, error, delete, edit, save, star, favorite, arrowBack, arrowForward,
831
+ play, pause
832
+ ```
833
+
834
+ #### `Image(props)`
835
+
836
+ ```js
837
+ Image({
838
+ src: "/assets/photo.jpg",
839
+ alt: "Product",
840
+ height: 180,
841
+ fit: "cover",
842
+ radius: 8,
843
+ })
844
+ ```
845
+
846
+ #### `CircleAvatar(props, children)`
847
+
848
+ ```js
849
+ CircleAvatar({ initials: "LU", size: 48 })
850
+ CircleAvatar({ src: "/avatar.png", alt: "User", size: 48 })
851
+ ```
852
+
853
+ #### `Badge(props, children)`
854
+
855
+ ```js
856
+ Badge({ label: "3" }, [
857
+ Icon("star"),
858
+ ])
859
+ ```
860
+
861
+ #### `Placeholder`
862
+
863
+ ```js
864
+ Placeholder({ height: 120, label: "Image placeholder" })
865
+ ```
866
+
867
+ #### `ClipRRect`
868
+
869
+ Clips children with rounded corners.
870
+
871
+ ```js
872
+ ClipRRect({ radius: 12 }, [
873
+ Image({ src: "/photo.jpg", alt: "Photo" }),
874
+ ])
875
+ ```
876
+
877
+ #### Additional visual wrappers
878
+
879
+ - `ClipOval(children)`: clips children to an oval/circle.
880
+ - `ClipRect(children)`: clips rectangular overflow.
881
+ - `ClipPath({ clipPath })`: applies CSS `clip-path`.
882
+ - `FittedBox({ fit })`: constrains child media using object-fit-like behavior.
883
+ - `Opacity({ opacity })`: changes child opacity.
884
+ - `PhysicalModel({ elevation, color, shadowColor, borderRadius })`: adds material-like shadow and clipping.
885
+ - `ShaderMask({ shader, blendMode: "text" })`: useful for gradient text.
886
+
887
+ ```js
888
+ ShaderMask(
889
+ {
890
+ shader: "linear-gradient(135deg, #2563eb, #059669)",
891
+ blendMode: "text",
892
+ },
893
+ [Text("Gradient text", { weight: 900 })],
894
+ )
895
+ ```
896
+
897
+ ### Scrolling Widgets
898
+
899
+ Import:
900
+
901
+ ```js
902
+ import {
903
+ SingleChildScrollView,
904
+ ListView,
905
+ GridView,
906
+ CustomScrollView,
907
+ NestedScrollView,
908
+ PageView,
909
+ SliverAppBar,
910
+ SliverList,
911
+ SliverGrid,
912
+ SliverPadding,
913
+ SliverToBoxAdapter,
914
+ } from "./lumina-ui.js";
915
+ ```
916
+
917
+ #### `SingleChildScrollView(props, children)`
918
+
919
+ ```js
920
+ SingleChildScrollView({
921
+ maxHeight: 240,
922
+ child: Column([
923
+ Text("Scrollable content"),
924
+ ]),
925
+ })
926
+ ```
927
+
928
+ #### `ListView(props, children)`
929
+
930
+ Use direct children:
931
+
932
+ ```js
933
+ ListView({ gap: 8 }, [
934
+ Text("One"),
935
+ Text("Two"),
936
+ ])
937
+ ```
938
+
939
+ Use builder-style data:
940
+
941
+ ```js
942
+ ListView({
943
+ items: todos(),
944
+ gap: 8,
945
+ itemBuilder: (todo) => Text(todo.title),
946
+ })
947
+ ```
948
+
949
+ Optional props:
950
+
951
+ - `items`
952
+ - `itemBuilder`
953
+ - `separatorBuilder`
954
+ - `direction`
955
+ - `gap`
956
+ - `padding`
957
+ - `empty`
958
+
959
+ #### `GridView(props, children)`
960
+
961
+ ```js
962
+ GridView({
963
+ items: products,
964
+ minColumnWidth: 160,
965
+ gap: 12,
966
+ itemBuilder: (product) => Card([Text(product.name)]),
967
+ })
968
+ ```
969
+
970
+ Use `columns` for a fixed number of columns:
971
+
972
+ ```js
973
+ GridView({ columns: 3, gap: 12 }, cards)
974
+ ```
975
+
976
+ #### Advanced scrolling and sliver-style widgets
977
+
978
+ LuminaUI includes DOM equivalents for Flutter scroll composition:
979
+
980
+ - `CustomScrollView({ slivers })`
981
+ - `NestedScrollView({ header, body })`
982
+ - `PageView({ pages })`
983
+ - `SliverAppBar({ title, pinned, floating })`
984
+ - `SliverList(...)`
985
+ - `SliverGrid(...)`
986
+ - `SliverPadding({ padding }, children)`
987
+ - `SliverToBoxAdapter(child)`
988
+
989
+ These are not true Flutter slivers; they are composable DOM scroll sections that
990
+ use CSS overflow, sticky positioning, and scroll snapping.
991
+
992
+ ```js
993
+ CustomScrollView([
994
+ SliverAppBar({ title: "Pinned", pinned: true }),
995
+ SliverPadding({ padding: 12 }, [
996
+ SliverList({
997
+ items: ["One", "Two", "Three"],
998
+ itemBuilder: (item) => SliverToBoxAdapter([Text(item)]),
999
+ }),
1000
+ ]),
1001
+ ])
1002
+
1003
+ PageView({
1004
+ pages: [
1005
+ Center([Text("Page 1")]),
1006
+ Center([Text("Page 2")]),
1007
+ ],
1008
+ })
1009
+ ```
1010
+
1011
+ ### Interaction
1012
+
1013
+ Import:
1014
+
1015
+ ```js
1016
+ import {
1017
+ GestureDetector,
1018
+ AbsorbPointer,
1019
+ IgnorePointer,
1020
+ Dismissible,
1021
+ Draggable,
1022
+ DragTarget,
1023
+ } from "./lumina-ui.js";
1024
+ ```
1025
+
1026
+ #### `GestureDetector(props, children)`
1027
+
1028
+ Maps common Flutter gesture names to DOM pointer/click events.
1029
+
1030
+ ```js
1031
+ GestureDetector(
1032
+ {
1033
+ onTap: () => console.log("tap"),
1034
+ onDoubleTap: () => console.log("double"),
1035
+ },
1036
+ [Text("Tap me")],
1037
+ )
1038
+ ```
1039
+
1040
+ #### Pointer control
1041
+
1042
+ ```js
1043
+ AbsorbPointer([Button({ text: "Blocked" })])
1044
+ IgnorePointer([Button({ text: "Clicks pass through" })])
1045
+ ```
1046
+
1047
+ #### Dismiss and drag/drop
1048
+
1049
+ ```js
1050
+ Dismissible(
1051
+ {
1052
+ direction: "horizontal",
1053
+ onDismissed: () => removeItem(id),
1054
+ },
1055
+ [Text("Swipe or press Delete")],
1056
+ )
1057
+
1058
+ Draggable({ data: { id: 1 } }, [Text("Drag me")])
1059
+
1060
+ DragTarget(
1061
+ {
1062
+ onAccept: (data) => console.log(data),
1063
+ },
1064
+ [Text("Drop here")],
1065
+ )
1066
+ ```
1067
+
1068
+ ### Accessibility
1069
+
1070
+ Import:
1071
+
1072
+ ```js
1073
+ import { Semantics, ExcludeSemantics } from "./lumina-ui.js";
1074
+ ```
1075
+
1076
+ ```js
1077
+ Semantics(
1078
+ { label: "Save document", role: "button", hint: "Saves the current draft" },
1079
+ [Button({ text: "Save" })],
1080
+ )
1081
+
1082
+ ExcludeSemantics([
1083
+ Icon("star"),
1084
+ ])
1085
+ ```
1086
+
1087
+ ### Feedback Widgets
1088
+
1089
+ Import:
1090
+
1091
+ ```js
1092
+ import {
1093
+ Dialog,
1094
+ AlertDialog,
1095
+ ModalBarrier,
1096
+ SnackBar,
1097
+ Tooltip,
1098
+ LinearProgressIndicator,
1099
+ CircularProgressIndicator,
1100
+ } from "./lumina-ui.js";
1101
+ ```
1102
+
1103
+ #### `Dialog(props, children)`
1104
+
1105
+ Return `null` by setting `open: false`.
1106
+
1107
+ ```js
1108
+ Dialog(
1109
+ {
1110
+ open: dialogOpen(),
1111
+ onDismiss: () => setDialogOpen(false),
1112
+ },
1113
+ [
1114
+ Padding({ padding: 20 }, [
1115
+ Column({ gap: 12 }, [
1116
+ Heading({ level: 2 }, "Dialog"),
1117
+ Text("Dialog content"),
1118
+ Button({ text: "Close", onClick: () => setDialogOpen(false) }),
1119
+ ]),
1120
+ ]),
1121
+ ],
1122
+ )
1123
+ ```
1124
+
1125
+ #### `AlertDialog(props)`
1126
+
1127
+ ```js
1128
+ AlertDialog({
1129
+ open: confirmOpen(),
1130
+ title: "Delete item?",
1131
+ content: "This action cannot be undone.",
1132
+ actions: [
1133
+ Button({ text: "Cancel", variant: "text" }),
1134
+ Button({ text: "Delete", variant: "danger" }),
1135
+ ],
1136
+ })
1137
+ ```
1138
+
1139
+ #### `SnackBar(props, children)`
1140
+
1141
+ ```js
1142
+ SnackBar({
1143
+ open: snackbarOpen(),
1144
+ message: "Saved successfully",
1145
+ action: Button({
1146
+ text: "Dismiss",
1147
+ variant: "text",
1148
+ onClick: () => setSnackbarOpen(false),
1149
+ }),
1150
+ })
1151
+ ```
1152
+
1153
+ #### `Tooltip`
1154
+
1155
+ Uses the native browser `title` tooltip.
1156
+
1157
+ ```js
1158
+ Tooltip({ message: "More information" }, [
1159
+ Icon("info"),
1160
+ ])
1161
+ ```
1162
+
1163
+ #### Progress indicators
1164
+
1165
+ Determinate linear progress:
1166
+
1167
+ ```js
1168
+ LinearProgressIndicator({ value: 0.65 })
1169
+ ```
1170
+
1171
+ Indeterminate linear progress:
1172
+
1173
+ ```js
1174
+ LinearProgressIndicator()
1175
+ ```
1176
+
1177
+ Circular progress:
1178
+
1179
+ ```js
1180
+ CircularProgressIndicator({ size: 32 })
1181
+ ```
1182
+
1183
+ ### Forms
1184
+
1185
+ Import:
1186
+
1187
+ ```js
1188
+ import {
1189
+ Form,
1190
+ FormField,
1191
+ Radio,
1192
+ RadioGroup,
1193
+ Slider,
1194
+ Dropdown,
1195
+ TextArea,
1196
+ } from "./lumina-ui.js";
1197
+ ```
1198
+
1199
+ #### `Form(props, children)`
1200
+
1201
+ LuminaUI forms prevent default browser submit behavior by default and call your
1202
+ `onSubmit` handler.
1203
+
1204
+ ```js
1205
+ Form(
1206
+ {
1207
+ gap: 12,
1208
+ onSubmit: () => console.log("submit"),
1209
+ },
1210
+ [
1211
+ FormField({ label: "Name" }, [
1212
+ TextField({ value: name(), onChange: setName }),
1213
+ ]),
1214
+ Button({ text: "Submit", type: "submit" }),
1215
+ ],
1216
+ )
1217
+ ```
1218
+
1219
+ #### `FormField(props, children)`
1220
+
1221
+ Use it for label, helper text, required indicator, and error text.
1222
+
1223
+ ```js
1224
+ FormField(
1225
+ {
1226
+ label: "Notes",
1227
+ helperText: "Keep it short.",
1228
+ errorText: notes().length > 80 ? "Too long" : "",
1229
+ },
1230
+ [
1231
+ TextArea({ value: notes(), onChange: setNotes }),
1232
+ ],
1233
+ )
1234
+ ```
1235
+
1236
+ #### `Radio` and `RadioGroup`
1237
+
1238
+ ```js
1239
+ RadioGroup({
1240
+ value: role(),
1241
+ onChange: setRole,
1242
+ direction: "horizontal",
1243
+ options: [
1244
+ { label: "Designer", value: "designer" },
1245
+ { label: "Engineer", value: "engineer" },
1246
+ ],
1247
+ })
1248
+ ```
1249
+
1250
+ #### `Slider`
1251
+
1252
+ ```js
1253
+ Slider({
1254
+ value: volume(),
1255
+ min: 0,
1256
+ max: 100,
1257
+ onChange: setVolume,
1258
+ })
1259
+ ```
1260
+
1261
+ #### `Dropdown`
1262
+
1263
+ ```js
1264
+ Dropdown({
1265
+ value: plan(),
1266
+ onChange: setPlan,
1267
+ placeholder: "Choose plan",
1268
+ options: [
1269
+ { label: "Starter", value: "starter" },
1270
+ { label: "Studio", value: "studio" },
1271
+ ],
1272
+ })
1273
+ ```
1274
+
1275
+ #### `TextArea`
1276
+
1277
+ ```js
1278
+ TextArea({
1279
+ value: message(),
1280
+ onChange: setMessage,
1281
+ rows: 4,
1282
+ placeholder: "Write a message",
1283
+ })
1284
+ ```
1285
+
1286
+ ### Navigation
1287
+
1288
+ Import:
1289
+
1290
+ ```js
1291
+ import {
1292
+ Scaffold,
1293
+ AppBar,
1294
+ TabBar,
1295
+ TabBarView,
1296
+ BottomNavigationBar,
1297
+ NavigationRail,
1298
+ Drawer,
1299
+ } from "./lumina-ui.js";
1300
+ ```
1301
+
1302
+ #### `Scaffold`
1303
+
1304
+ `Scaffold` composes common application regions.
1305
+
1306
+ ```js
1307
+ Scaffold({
1308
+ appBar: AppBar({ title: "Dashboard" }),
1309
+ body: Padding({ padding: 16 }, [
1310
+ Text("Main content"),
1311
+ ]),
1312
+ bottomNavigationBar: BottomNavigationBar({
1313
+ value: page(),
1314
+ onChange: setPage,
1315
+ items: [
1316
+ { label: "Home", value: "home", icon: Icon("home") },
1317
+ { label: "Profile", value: "profile", icon: Icon("person") },
1318
+ ],
1319
+ }),
1320
+ })
1321
+ ```
1322
+
1323
+ #### `AppBar`
1324
+
1325
+ ```js
1326
+ AppBar({
1327
+ title: "LuminaUI",
1328
+ leading: Button({ text: "Menu" }),
1329
+ actions: [Icon("settings")],
1330
+ })
1331
+ ```
1332
+
1333
+ #### `TabBar` and `TabBarView`
1334
+
1335
+ ```js
1336
+ const tabs = [
1337
+ { label: "Overview", value: "overview", child: Text("Overview content") },
1338
+ { label: "Details", value: "details", child: Text("Details content") },
1339
+ ];
1340
+
1341
+ Column([
1342
+ TabBar({ tabs, value: activeTab(), onChange: setActiveTab }),
1343
+ TabBarView({ tabs, value: activeTab() }),
1344
+ ])
1345
+ ```
1346
+
1347
+ #### `BottomNavigationBar`
1348
+
1349
+ ```js
1350
+ BottomNavigationBar({
1351
+ value: page(),
1352
+ onChange: setPage,
1353
+ items: [
1354
+ { label: "Home", value: "home", icon: Icon("home") },
1355
+ { label: "Build", value: "build", icon: Icon("settings") },
1356
+ { label: "Profile", value: "profile", icon: Icon("person") },
1357
+ ],
1358
+ })
1359
+ ```
1360
+
1361
+ #### `Drawer`
1362
+
1363
+ ```js
1364
+ Drawer(
1365
+ { open: drawerOpen() },
1366
+ [
1367
+ Padding({ padding: 16 }, [
1368
+ Button({ text: "Close", onClick: () => setDrawerOpen(false) }),
1369
+ ]),
1370
+ ],
1371
+ )
1372
+ ```
1373
+
1374
+ ### Animation
1375
+
1376
+ Import:
1377
+
1378
+ ```js
1379
+ import {
1380
+ AnimatedContainer,
1381
+ AnimatedOpacity,
1382
+ AnimatedScale,
1383
+ AnimatedSlide,
1384
+ AnimatedSwitcher,
1385
+ } from "./lumina-ui.js";
1386
+ ```
1387
+
1388
+ The animation widgets are transition wrappers. They do not run a custom
1389
+ animation engine; they apply CSS transitions to DOM elements.
1390
+
1391
+ #### `AnimatedContainer`
1392
+
1393
+ ```js
1394
+ AnimatedContainer(
1395
+ {
1396
+ width: active() ? 160 : 80,
1397
+ height: 80,
1398
+ duration: 250,
1399
+ style: {
1400
+ backgroundColor: active() ? "#059669" : "#2563eb",
1401
+ borderRadius: active() ? "16px" : "50%",
1402
+ },
1403
+ },
1404
+ [],
1405
+ )
1406
+ ```
1407
+
1408
+ #### `AnimatedOpacity`
1409
+
1410
+ ```js
1411
+ AnimatedOpacity({ opacity: visible() ? 1 : 0, duration: 180 }, [
1412
+ Text("Fade me"),
1413
+ ])
1414
+ ```
1415
+
1416
+ #### `AnimatedScale`
1417
+
1418
+ ```js
1419
+ AnimatedScale({ scale: selected() ? 1.1 : 1 }, [
1420
+ Card([Text("Scale")]),
1421
+ ])
1422
+ ```
1423
+
1424
+ #### `AnimatedSlide`
1425
+
1426
+ ```js
1427
+ AnimatedSlide({ offset: { x: 40, y: 0 }, duration: 220 }, [
1428
+ Text("Slide"),
1429
+ ])
1430
+ ```
1431
+
1432
+ #### `AnimatedSwitcher`
1433
+
1434
+ ```js
1435
+ AnimatedSwitcher({
1436
+ child: selected() ? Text("A") : Text("B"),
1437
+ })
1438
+ ```
1439
+
1440
+ ## Complete Example
1441
+
1442
+ This example combines state, layout, navigation, and feedback.
1443
+
1444
+ ```js
1445
+ import {
1446
+ mount,
1447
+ useState,
1448
+ Column,
1449
+ Padding,
1450
+ Text,
1451
+ Button,
1452
+ Scaffold,
1453
+ AppBar,
1454
+ BottomNavigationBar,
1455
+ SnackBar,
1456
+ Icon,
1457
+ } from "./lumina-ui.js";
1458
+
1459
+ const [getPage, setPage, subscribePage] = useState("home");
1460
+ const [getSnack, setSnack, subscribeSnack] = useState(false);
1461
+ const updates = new WeakSet();
1462
+
1463
+ function bind(forceUpdate) {
1464
+ if (updates.has(forceUpdate)) return;
1465
+ subscribePage(forceUpdate);
1466
+ subscribeSnack(forceUpdate);
1467
+ updates.add(forceUpdate);
1468
+ }
1469
+
1470
+ function App(forceUpdate) {
1471
+ bind(forceUpdate);
1472
+
1473
+ return Column([
1474
+ Scaffold({
1475
+ appBar: AppBar({ title: "Example App" }),
1476
+ body: Padding({ padding: 16 }, [
1477
+ Column({ gap: 12 }, [
1478
+ Text(`Current page: ${getPage()}`),
1479
+ Button({
1480
+ text: "Show snackbar",
1481
+ onClick: () => setSnack(true),
1482
+ }),
1483
+ ]),
1484
+ ]),
1485
+ bottomNavigationBar: BottomNavigationBar({
1486
+ value: getPage(),
1487
+ onChange: setPage,
1488
+ items: [
1489
+ { label: "Home", value: "home", icon: Icon("home") },
1490
+ { label: "Build", value: "build", icon: Icon("settings") },
1491
+ ],
1492
+ }),
1493
+ }),
1494
+ SnackBar({
1495
+ open: getSnack(),
1496
+ message: "Hello from LuminaUI",
1497
+ action: Button({
1498
+ text: "Dismiss",
1499
+ variant: "text",
1500
+ onClick: () => setSnack(false),
1501
+ }),
1502
+ }),
1503
+ ]);
1504
+ }
1505
+
1506
+ mount(App, document.getElementById("root"));
1507
+ ```
1508
+
1509
+ ## Renderer Behavior
1510
+
1511
+ LuminaUI has a small renderer, not a full virtual DOM system.
1512
+
1513
+ What it currently does:
1514
+
1515
+ - Converts widget trees into real DOM nodes.
1516
+ - Patches existing DOM on state updates.
1517
+ - Updates props and event listeners.
1518
+ - Handles keyed children in a basic way.
1519
+ - Treats `null`, `undefined`, and `false` as empty widgets.
1520
+ - Cleans up removed or empty props during patching.
1521
+
1522
+ What it does not do yet:
1523
+
1524
+ - Advanced diffing like React, Vue, or Flutter.
1525
+ - Component-local hook state.
1526
+ - Lifecycle methods tied directly to mounted widgets.
1527
+ - Suspense, portals, async rendering, or hydration.
1528
+
1529
+ For now, keep state explicit and prefer small widget trees.
1530
+
1531
+ ## Styling
1532
+
1533
+ LuminaUI uses inline style objects by default.
1534
+
1535
+ ```js
1536
+ Text("Styled", {
1537
+ style: {
1538
+ letterSpacing: "0",
1539
+ textTransform: "uppercase",
1540
+ },
1541
+ })
1542
+ ```
1543
+
1544
+ You can still use CSS classes with `className`:
1545
+
1546
+ ```js
1547
+ Container({ className: "panel" }, [
1548
+ Text("Class-based styling works too"),
1549
+ ])
1550
+ ```
1551
+
1552
+ The low-level `createElement` utility supports:
1553
+
1554
+ - `style`
1555
+ - `className`
1556
+ - `dataset`
1557
+ - event handlers like `onClick`, `onInput`, `onKeyDown`
1558
+ - DOM props like `value`, `checked`, `disabled`, `selected`
1559
+ - ARIA attributes
1560
+
1561
+ ## Keys
1562
+
1563
+ Use `key` when rendering dynamic lists.
1564
+
1565
+ ```js
1566
+ Column(
1567
+ {},
1568
+ todos().map((todo) =>
1569
+ Row(
1570
+ { key: todo.id },
1571
+ [
1572
+ Text(todo.title),
1573
+ Button({ text: "Delete", onClick: () => removeTodo(todo.id) }),
1574
+ ],
1575
+ ),
1576
+ ),
1577
+ )
1578
+ ```
1579
+
1580
+ The keyed reconciliation is intentionally simple. Stable keys help LuminaUI keep
1581
+ DOM nodes aligned when arrays change.
1582
+
1583
+ ## Current Demo
1584
+
1585
+ `lumina-ui/app/App.js` demonstrates:
1586
+
1587
+ - a full ecommerce storefront backed by `lumina-ui/app/ecommerce/catalog.json`
1588
+ - product grid, product detail dialog, filters, sorting, search, and stock-aware
1589
+ cart actions
1590
+ - cart drawer with quantity controls and dismissible rows
1591
+ - checkout form with shipping and payment choices that creates demo orders
1592
+ - an admin interface for inventory, publish status, product editing, order
1593
+ status updates, and dashboard metrics
1594
+ - snackbars, dialogs, navigation, scrolling, forms, display widgets, and layout
1595
+ primitives working together
1596
+
1597
+ Use it as a living playground for new widgets.
1598
+
1599
+ ## Extending LuminaUI
1600
+
1601
+ To create a new widget:
1602
+
1603
+ 1. Add a function in the appropriate file under `lumina-ui/widgets/`.
1604
+ 2. Return a virtual node object: `{ tag, props, children, key }`.
1605
+ 3. Use helpers from `widgets/utils.js` for child normalization and pixel values.
1606
+ 4. Export it from `lumina-ui.js`.
1607
+ 5. Add a small example in `app/App.js`.
1608
+ 6. Document it in this README.
1609
+
1610
+ Small widget example:
1611
+
1612
+ ```js
1613
+ import { cleanStyle, normalizeWidgetArgs, omitProps } from "./utils.js";
1614
+
1615
+ export function Panel(propsOrChildren = {}, maybeChildren = undefined) {
1616
+ const [props, children] = normalizeWidgetArgs(propsOrChildren, maybeChildren);
1617
+
1618
+ return {
1619
+ tag: "section",
1620
+ props: {
1621
+ ...omitProps(props),
1622
+ style: cleanStyle({
1623
+ padding: "16px",
1624
+ border: "1px solid #e5e7eb",
1625
+ borderRadius: "8px",
1626
+ ...props.style,
1627
+ }),
1628
+ },
1629
+ children,
1630
+ key: props.key,
1631
+ };
1632
+ }
1633
+ ```
1634
+
1635
+ ## Current Limitations
1636
+
1637
+ LuminaUI is experimental. Important limitations:
1638
+
1639
+ - Rendering is simple and can still be improved.
1640
+ - There is no component-local hook system yet.
1641
+ - There is no global theme provider yet.
1642
+ - Routing is not implemented yet.
1643
+ - Accessibility coverage is partial and should be expanded widget by widget.
1644
+ - Most widgets use inline styles rather than a design token system.
1645
+ - Animation widgets use CSS transitions only.
1646
+
1647
+ ## Roadmap
1648
+
1649
+ Completed:
1650
+
1651
+ - Layout widgets: `Column`, `Row`, `Container`, `Center`, `Align`, `Padding`,
1652
+ `SizedBox`, `Flexible`, `Expanded`, `Spacer`, `Wrap`, `Stack`, `Positioned`,
1653
+ `Divider`, `Card`, `AspectRatio`, `Baseline`, `ConstrainedBox`,
1654
+ `DecoratedBox`, `FractionallySizedBox`, `LayoutBuilder`, `LimitedBox`,
1655
+ `Offstage`, `OverflowBox`, `RotatedBox`, `SizedOverflowBox`, `Transform`
1656
+ - Text widgets: `Text`, `Heading`, `Caption`, `DefaultTextStyle`, `RichText`
1657
+ - Controls: `Button`, `Input`, `TextField`, `Checkbox`, `Switch`
1658
+ - Display widgets: `Icon`, `Image`, `CircleAvatar`, `Badge`, `Placeholder`,
1659
+ `ClipRRect`, `ClipOval`, `ClipRect`, `ClipPath`, `FittedBox`, `Opacity`,
1660
+ `PhysicalModel`, `ShaderMask`
1661
+ - Scrolling widgets: `SingleChildScrollView`, `ListView`, `GridView`,
1662
+ `CustomScrollView`, `NestedScrollView`, `PageView`, `SliverAppBar`,
1663
+ `SliverGrid`, `SliverList`, `SliverPadding`, `SliverToBoxAdapter`
1664
+ - Interaction widgets: `GestureDetector`, `AbsorbPointer`, `IgnorePointer`,
1665
+ `Dismissible`, `Draggable`, `DragTarget`
1666
+ - Accessibility widgets: `Semantics`, `ExcludeSemantics`
1667
+ - Feedback widgets: `Dialog`, `AlertDialog`, `ModalBarrier`, `SnackBar`,
1668
+ `Tooltip`, `LinearProgressIndicator`, `CircularProgressIndicator`
1669
+ - Form widgets: `Form`, `FormField`, `Radio`, `RadioGroup`, `Slider`,
1670
+ `Dropdown`, `TextArea`
1671
+ - Navigation widgets: `Scaffold`, `AppBar`, `TabBar`, `TabBarView`,
1672
+ `BottomNavigationBar`, `NavigationRail`, `Drawer`
1673
+ - Animation widgets: `AnimatedContainer`, `AnimatedOpacity`, `AnimatedScale`,
1674
+ `AnimatedSlide`, `AnimatedSwitcher`
1675
+
1676
+ Next:
1677
+
1678
+ - Smarter rendering and diffing
1679
+ - Component isolation
1680
+ - Lifecycle hooks
1681
+ - Theme system
1682
+ - Router
1683
+ - Better keyboard accessibility
1684
+ - More Material-style components
1685
+
1686
+ ## License
1687
+
1688
+ MIT
1689
+
1690
+ ## Vision
1691
+
1692
+ LuminaUI is an experiment in bringing Flutter's widget model to the web without
1693
+ giving up vanilla JavaScript. The goal is a UI system that stays small enough to
1694
+ read, simple enough to modify, and expressive enough to build real interfaces.