@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/LICENSE +21 -0
- package/README.md +1694 -0
- package/lumina-ui/core/element.js +118 -0
- package/lumina-ui/core/renderer.js +376 -0
- package/lumina-ui/core/state.js +99 -0
- package/lumina-ui/widgets/accessibility.js +45 -0
- package/lumina-ui/widgets/animation.js +112 -0
- package/lumina-ui/widgets/controls.js +312 -0
- package/lumina-ui/widgets/display.js +443 -0
- package/lumina-ui/widgets/feedback.js +316 -0
- package/lumina-ui/widgets/forms.js +342 -0
- package/lumina-ui/widgets/interaction.js +254 -0
- package/lumina-ui/widgets/layout.js +624 -0
- package/lumina-ui/widgets/navigation.js +313 -0
- package/lumina-ui/widgets/scrolling.js +330 -0
- package/lumina-ui/widgets/text.js +121 -0
- package/lumina-ui/widgets/utils.js +221 -0
- package/lumina-ui.js +154 -0
- package/package.json +38 -0
- package/scripts/smoke-test.mjs +256 -0
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.
|