@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.
@@ -0,0 +1,313 @@
1
+ import {
2
+ cleanStyle,
3
+ luminaTheme,
4
+ normalizeWidgetArgs,
5
+ omitProps,
6
+ px,
7
+ } from "./utils.js";
8
+
9
+ export function Scaffold(props = {}) {
10
+ const children = [
11
+ props.appBar || null,
12
+ {
13
+ tag: "main",
14
+ props: {
15
+ style: cleanStyle({
16
+ flex: 1,
17
+ minWidth: 0,
18
+ minHeight: 0,
19
+ color: luminaTheme.colors.text,
20
+ ...props.bodyStyle,
21
+ }),
22
+ },
23
+ children: props.body ? [props.body] : [],
24
+ },
25
+ props.bottomNavigationBar || null,
26
+ props.drawer || null,
27
+ ];
28
+
29
+ return {
30
+ tag: "div",
31
+ props: {
32
+ ...omitProps(props, [
33
+ "appBar",
34
+ "body",
35
+ "bottomNavigationBar",
36
+ "drawer",
37
+ "bodyStyle",
38
+ ]),
39
+ style: cleanStyle({
40
+ display: "flex",
41
+ flexDirection: "column",
42
+ minHeight: px(props.minHeight, "100%"),
43
+ backgroundColor: props.backgroundColor || "transparent",
44
+ color: luminaTheme.colors.text,
45
+ ...props.style,
46
+ }),
47
+ },
48
+ children,
49
+ key: props.key,
50
+ };
51
+ }
52
+
53
+ export function AppBar(propsOrChildren = {}, maybeChildren = undefined) {
54
+ const [props, children] = normalizeWidgetArgs(propsOrChildren, maybeChildren);
55
+
56
+ return {
57
+ tag: "header",
58
+ props: {
59
+ ...omitProps(props, ["title", "leading", "actions", "height"]),
60
+ style: cleanStyle({
61
+ minHeight: px(props.height ?? 56),
62
+ display: "flex",
63
+ alignItems: "center",
64
+ gap: "12px",
65
+ padding: "0 16px",
66
+ borderBottom: `1px solid ${luminaTheme.colors.border}`,
67
+ backgroundColor: luminaTheme.colors.surface,
68
+ color: luminaTheme.colors.text,
69
+ boxShadow: luminaTheme.shadow.xs,
70
+ ...props.style,
71
+ }),
72
+ },
73
+ children: children.length
74
+ ? children
75
+ : [
76
+ props.leading || null,
77
+ {
78
+ tag: "div",
79
+ props: {
80
+ style: {
81
+ flex: 1,
82
+ minWidth: 0,
83
+ fontSize: "18px",
84
+ fontWeight: 800,
85
+ },
86
+ },
87
+ children: [props.title || ""],
88
+ },
89
+ ...(props.actions || []),
90
+ ],
91
+ key: props.key,
92
+ };
93
+ }
94
+
95
+ export function TabBar({
96
+ tabs = [],
97
+ value,
98
+ onChange,
99
+ color = luminaTheme.colors.primary,
100
+ style = {},
101
+ ...props
102
+ }) {
103
+ return {
104
+ tag: "div",
105
+ props: {
106
+ ...props,
107
+ role: "tablist",
108
+ style: cleanStyle({
109
+ display: "flex",
110
+ gap: "4px",
111
+ borderBottom: `1px solid ${luminaTheme.colors.border}`,
112
+ ...style,
113
+ }),
114
+ },
115
+ children: tabs.map((tab, index) => {
116
+ const selected = value === tab.value || value === index;
117
+ return {
118
+ tag: "button",
119
+ props: {
120
+ key: tab.value ?? index,
121
+ role: "tab",
122
+ "aria-selected": selected,
123
+ type: "button",
124
+ onClick: () => {
125
+ if (onChange) onChange(tab.value ?? index);
126
+ },
127
+ style: cleanStyle({
128
+ appearance: "none",
129
+ border: "1px solid transparent",
130
+ borderBottom: `2px solid ${selected ? color : "transparent"}`,
131
+ borderRadius: "8px 8px 0 0",
132
+ backgroundColor: selected ? luminaTheme.colors.primarySoft : "transparent",
133
+ color: selected ? color : luminaTheme.colors.muted,
134
+ padding: "10px 12px",
135
+ font: "inherit",
136
+ fontWeight: selected ? 800 : 600,
137
+ cursor: "pointer",
138
+ transition: `background-color ${luminaTheme.transition}, color ${luminaTheme.transition}, border-color ${luminaTheme.transition}`,
139
+ }),
140
+ },
141
+ children: [tab.label],
142
+ key: tab.value ?? index,
143
+ };
144
+ }),
145
+ key: props.key,
146
+ };
147
+ }
148
+
149
+ export function TabBarView({ tabs = [], value, style = {}, ...props }) {
150
+ const active =
151
+ tabs.find((tab, index) => tab.value === value || index === value) ||
152
+ tabs[0];
153
+
154
+ return {
155
+ tag: "div",
156
+ props: {
157
+ ...props,
158
+ style: cleanStyle({
159
+ minWidth: 0,
160
+ minHeight: 0,
161
+ ...style,
162
+ }),
163
+ },
164
+ children: active ? [active.child] : [],
165
+ key: props.key,
166
+ };
167
+ }
168
+
169
+ export function BottomNavigationBar({
170
+ items = [],
171
+ value,
172
+ onChange,
173
+ color = luminaTheme.colors.primary,
174
+ style = {},
175
+ ...props
176
+ }) {
177
+ return {
178
+ tag: "nav",
179
+ props: {
180
+ ...props,
181
+ "aria-label": props["aria-label"] || "Bottom navigation",
182
+ style: cleanStyle({
183
+ display: "grid",
184
+ gridTemplateColumns: `repeat(${Math.max(items.length, 1)}, 1fr)`,
185
+ borderTop: `1px solid ${luminaTheme.colors.border}`,
186
+ backgroundColor: luminaTheme.colors.surface,
187
+ boxShadow: "0 -1px 2px rgba(15, 23, 42, 0.04)",
188
+ ...style,
189
+ }),
190
+ },
191
+ children: items.map((item, index) => {
192
+ const selected = value === item.value || value === index;
193
+ return {
194
+ tag: "button",
195
+ props: {
196
+ key: item.value ?? index,
197
+ type: "button",
198
+ "aria-current": selected ? "page" : undefined,
199
+ onClick: () => {
200
+ if (onChange) onChange(item.value ?? index);
201
+ },
202
+ style: cleanStyle({
203
+ border: "none",
204
+ borderRadius: luminaTheme.radius.md,
205
+ backgroundColor: selected ? luminaTheme.colors.primarySoft : "transparent",
206
+ color: selected ? color : luminaTheme.colors.muted,
207
+ display: "flex",
208
+ flexDirection: "column",
209
+ alignItems: "center",
210
+ gap: "4px",
211
+ padding: "10px 8px",
212
+ font: "inherit",
213
+ fontSize: "12px",
214
+ fontWeight: selected ? 800 : 600,
215
+ cursor: "pointer",
216
+ transition: `background-color ${luminaTheme.transition}, color ${luminaTheme.transition}`,
217
+ }),
218
+ },
219
+ children: [item.icon || null, item.label],
220
+ key: item.value ?? index,
221
+ };
222
+ }),
223
+ key: props.key,
224
+ };
225
+ }
226
+
227
+ export function Drawer(propsOrChildren = {}, maybeChildren = undefined) {
228
+ const [props, children] = normalizeWidgetArgs(propsOrChildren, maybeChildren);
229
+ if (props.open === false) return null;
230
+
231
+ return {
232
+ tag: "aside",
233
+ props: {
234
+ ...omitProps(props, ["open", "width"]),
235
+ style: cleanStyle({
236
+ position: "fixed",
237
+ top: 0,
238
+ bottom: 0,
239
+ left: 0,
240
+ zIndex: props.zIndex ?? 1000,
241
+ width: px(props.width ?? 300),
242
+ maxWidth: "86vw",
243
+ backgroundColor: luminaTheme.colors.surface,
244
+ borderRight: `1px solid ${luminaTheme.colors.border}`,
245
+ boxShadow: luminaTheme.shadow.lg,
246
+ transform: props.open === false ? "translateX(-100%)" : "translateX(0)",
247
+ transition: "transform 180ms ease",
248
+ ...props.style,
249
+ }),
250
+ },
251
+ children,
252
+ key: props.key,
253
+ };
254
+ }
255
+
256
+ export function NavigationRail({
257
+ items = [],
258
+ value,
259
+ onChange,
260
+ color = luminaTheme.colors.primary,
261
+ style = {},
262
+ ...props
263
+ }) {
264
+ return {
265
+ tag: "nav",
266
+ props: {
267
+ ...props,
268
+ "aria-label": props["aria-label"] || "Navigation rail",
269
+ style: cleanStyle({
270
+ display: "flex",
271
+ flexDirection: "column",
272
+ gap: "6px",
273
+ width: px(props.width ?? 88),
274
+ padding: "8px",
275
+ borderRight: `1px solid ${luminaTheme.colors.border}`,
276
+ backgroundColor: luminaTheme.colors.surface,
277
+ ...style,
278
+ }),
279
+ },
280
+ children: items.map((item, index) => {
281
+ const selected = value === item.value || value === index;
282
+ return {
283
+ tag: "button",
284
+ props: {
285
+ key: item.value ?? index,
286
+ type: "button",
287
+ onClick: () => {
288
+ if (onChange) onChange(item.value ?? index);
289
+ },
290
+ style: cleanStyle({
291
+ border: "none",
292
+ borderRadius: luminaTheme.radius.md,
293
+ backgroundColor: selected ? luminaTheme.colors.primarySoft : "transparent",
294
+ color: selected ? color : luminaTheme.colors.muted,
295
+ display: "flex",
296
+ flexDirection: "column",
297
+ alignItems: "center",
298
+ gap: "4px",
299
+ padding: "10px 6px",
300
+ font: "inherit",
301
+ fontSize: "12px",
302
+ fontWeight: selected ? 800 : 600,
303
+ cursor: "pointer",
304
+ transition: `background-color ${luminaTheme.transition}, color ${luminaTheme.transition}`,
305
+ }),
306
+ },
307
+ children: [item.icon || null, item.label],
308
+ key: item.value ?? index,
309
+ };
310
+ }),
311
+ key: props.key,
312
+ };
313
+ }
@@ -0,0 +1,330 @@
1
+ import {
2
+ cleanStyle,
3
+ edgeInsets,
4
+ luminaTheme,
5
+ normalizeWidgetArgs,
6
+ omitProps,
7
+ px,
8
+ } from "./utils.js";
9
+
10
+ function dataKey(item) {
11
+ if (!item || typeof item !== "object") return null;
12
+ return item.id ?? item.key ?? null;
13
+ }
14
+
15
+ function withAutoKey(child, key) {
16
+ if (key == null) return child;
17
+ if (Array.isArray(child)) {
18
+ return child.map((entry, index) =>
19
+ withAutoKey(entry, `${key}:${index}`),
20
+ );
21
+ }
22
+ if (!child || typeof child !== "object" || !child.tag) return child;
23
+ if (child.key != null || child.props?.key != null) return child;
24
+ return { ...child, key };
25
+ }
26
+
27
+ export function SingleChildScrollView(
28
+ propsOrChildren = {},
29
+ maybeChildren = undefined,
30
+ ) {
31
+ const [props, children] = normalizeWidgetArgs(propsOrChildren, maybeChildren);
32
+ const direction = props.scrollDirection || props.direction || "vertical";
33
+ const isHorizontal = direction === "horizontal";
34
+
35
+ return {
36
+ tag: "div",
37
+ props: {
38
+ ...omitProps(props, ["scrollDirection", "direction", "padding"]),
39
+ style: cleanStyle({
40
+ overflowX: isHorizontal ? "auto" : "hidden",
41
+ overflowY: isHorizontal ? "hidden" : "auto",
42
+ WebkitOverflowScrolling: "touch",
43
+ maxWidth: "100%",
44
+ maxHeight: props.maxHeight ? px(props.maxHeight) : undefined,
45
+ padding: edgeInsets(props.padding),
46
+ scrollbarWidth: "thin",
47
+ scrollbarColor: `${luminaTheme.colors.borderStrong} transparent`,
48
+ ...props.style,
49
+ }),
50
+ },
51
+ children,
52
+ key: props.key,
53
+ };
54
+ }
55
+
56
+ export function ListView(propsOrChildren = {}, maybeChildren = undefined) {
57
+ const [props, givenChildren] = normalizeWidgetArgs(
58
+ propsOrChildren,
59
+ maybeChildren,
60
+ );
61
+ const {
62
+ items = [],
63
+ itemBuilder,
64
+ separatorBuilder,
65
+ direction = "vertical",
66
+ gap = 0,
67
+ padding,
68
+ empty,
69
+ style = {},
70
+ } = props;
71
+ const isHorizontal = direction === "horizontal";
72
+ const children = givenChildren.length
73
+ ? givenChildren
74
+ : items.length
75
+ ? items.flatMap((item, index) => {
76
+ const key = dataKey(item);
77
+ const child = withAutoKey(
78
+ itemBuilder ? itemBuilder(item, index) : item,
79
+ key,
80
+ );
81
+ if (!separatorBuilder || index === items.length - 1) return [child];
82
+ return [
83
+ child,
84
+ withAutoKey(separatorBuilder(item, index), key == null ? null : `${key}:separator`),
85
+ ];
86
+ })
87
+ : empty
88
+ ? [empty]
89
+ : [];
90
+
91
+ return {
92
+ tag: "div",
93
+ props: {
94
+ ...omitProps(props, [
95
+ "items",
96
+ "itemBuilder",
97
+ "separatorBuilder",
98
+ "direction",
99
+ "gap",
100
+ "padding",
101
+ "empty",
102
+ ]),
103
+ style: cleanStyle({
104
+ display: "flex",
105
+ flexDirection: isHorizontal ? "row" : "column",
106
+ gap: px(gap),
107
+ overflowX: isHorizontal ? "auto" : "hidden",
108
+ overflowY: isHorizontal ? "hidden" : "auto",
109
+ WebkitOverflowScrolling: "touch",
110
+ padding: edgeInsets(padding),
111
+ scrollbarWidth: "thin",
112
+ scrollbarColor: `${luminaTheme.colors.borderStrong} transparent`,
113
+ ...style,
114
+ }),
115
+ },
116
+ children,
117
+ key: props.key,
118
+ };
119
+ }
120
+
121
+ export function GridView(propsOrChildren = {}, maybeChildren = undefined) {
122
+ const [props, givenChildren] = normalizeWidgetArgs(
123
+ propsOrChildren,
124
+ maybeChildren,
125
+ );
126
+ const {
127
+ items = [],
128
+ itemBuilder,
129
+ columns,
130
+ minColumnWidth = 140,
131
+ gap = 12,
132
+ padding,
133
+ empty,
134
+ style = {},
135
+ } = props;
136
+ const children = givenChildren.length
137
+ ? givenChildren
138
+ : items.length
139
+ ? items.map((item, index) =>
140
+ withAutoKey(itemBuilder ? itemBuilder(item, index) : item, dataKey(item)),
141
+ )
142
+ : empty
143
+ ? [empty]
144
+ : [];
145
+
146
+ return {
147
+ tag: "div",
148
+ props: {
149
+ ...omitProps(props, [
150
+ "items",
151
+ "itemBuilder",
152
+ "columns",
153
+ "minColumnWidth",
154
+ "gap",
155
+ "padding",
156
+ "empty",
157
+ ]),
158
+ style: cleanStyle({
159
+ display: "grid",
160
+ gridTemplateColumns: columns
161
+ ? `repeat(${columns}, minmax(0, 1fr))`
162
+ : `repeat(auto-fit, minmax(${px(minColumnWidth)}, 1fr))`,
163
+ gap: px(gap),
164
+ overflow: "auto",
165
+ WebkitOverflowScrolling: "touch",
166
+ padding: edgeInsets(padding),
167
+ scrollbarWidth: "thin",
168
+ scrollbarColor: `${luminaTheme.colors.borderStrong} transparent`,
169
+ ...style,
170
+ }),
171
+ },
172
+ children,
173
+ key: props.key,
174
+ };
175
+ }
176
+
177
+ export function CustomScrollView(
178
+ propsOrChildren = {},
179
+ maybeChildren = undefined,
180
+ ) {
181
+ const [props, children] = normalizeWidgetArgs(propsOrChildren, maybeChildren);
182
+
183
+ return {
184
+ tag: "div",
185
+ props: {
186
+ ...omitProps(props, ["slivers", "direction", "padding"]),
187
+ style: cleanStyle({
188
+ display: "flex",
189
+ flexDirection: props.direction === "horizontal" ? "row" : "column",
190
+ overflow: "auto",
191
+ WebkitOverflowScrolling: "touch",
192
+ scrollBehavior: props.smooth ? "smooth" : undefined,
193
+ padding: edgeInsets(props.padding),
194
+ scrollbarWidth: "thin",
195
+ scrollbarColor: `${luminaTheme.colors.borderStrong} transparent`,
196
+ ...props.style,
197
+ }),
198
+ },
199
+ children: children.length ? children : props.slivers || [],
200
+ key: props.key,
201
+ };
202
+ }
203
+
204
+ export function NestedScrollView(props = {}) {
205
+ return {
206
+ tag: "div",
207
+ props: {
208
+ ...omitProps(props, ["header", "body"]),
209
+ style: cleanStyle({
210
+ display: "flex",
211
+ flexDirection: "column",
212
+ overflow: "auto",
213
+ WebkitOverflowScrolling: "touch",
214
+ scrollbarWidth: "thin",
215
+ scrollbarColor: `${luminaTheme.colors.borderStrong} transparent`,
216
+ ...props.style,
217
+ }),
218
+ },
219
+ children: [props.header || null, props.body || null],
220
+ key: props.key,
221
+ };
222
+ }
223
+
224
+ export function PageView(propsOrChildren = {}, maybeChildren = undefined) {
225
+ const [props, givenChildren] = normalizeWidgetArgs(
226
+ propsOrChildren,
227
+ maybeChildren,
228
+ );
229
+ const children = givenChildren.length ? givenChildren : props.pages || [];
230
+ const isVertical = props.direction === "vertical";
231
+
232
+ return {
233
+ tag: "div",
234
+ props: {
235
+ ...omitProps(props, ["pages", "direction", "gap"]),
236
+ style: cleanStyle({
237
+ display: "flex",
238
+ flexDirection: isVertical ? "column" : "row",
239
+ overflow: "auto",
240
+ scrollSnapType: isVertical ? "y mandatory" : "x mandatory",
241
+ WebkitOverflowScrolling: "touch",
242
+ gap: px(props.gap ?? 0),
243
+ scrollbarWidth: "thin",
244
+ scrollbarColor: `${luminaTheme.colors.borderStrong} transparent`,
245
+ ...props.style,
246
+ }),
247
+ },
248
+ children: children.map((child, index) => ({
249
+ tag: "div",
250
+ props: {
251
+ key: child?.key ?? index,
252
+ style: {
253
+ flex: "0 0 100%",
254
+ scrollSnapAlign: "start",
255
+ },
256
+ },
257
+ children: [child],
258
+ key: child?.key ?? index,
259
+ })),
260
+ key: props.key,
261
+ };
262
+ }
263
+
264
+ export function SliverAppBar(propsOrChildren = {}, maybeChildren = undefined) {
265
+ const [props, children] = normalizeWidgetArgs(propsOrChildren, maybeChildren);
266
+
267
+ return {
268
+ tag: "header",
269
+ props: {
270
+ ...omitProps(props, ["title", "expandedHeight", "floating", "pinned"]),
271
+ style: cleanStyle({
272
+ position: props.pinned || props.floating ? "sticky" : undefined,
273
+ top: props.pinned || props.floating ? 0 : undefined,
274
+ zIndex: props.pinned || props.floating ? 10 : undefined,
275
+ minHeight: px(props.expandedHeight ?? 56),
276
+ display: "flex",
277
+ alignItems: "center",
278
+ padding: "0 16px",
279
+ backgroundColor: luminaTheme.colors.surface,
280
+ borderBottom: `1px solid ${luminaTheme.colors.border}`,
281
+ boxShadow: luminaTheme.shadow.xs,
282
+ ...props.style,
283
+ }),
284
+ },
285
+ children: children.length ? children : [props.title || ""],
286
+ key: props.key,
287
+ };
288
+ }
289
+
290
+ export function SliverList(propsOrChildren = {}, maybeChildren = undefined) {
291
+ return ListView(propsOrChildren, maybeChildren);
292
+ }
293
+
294
+ export function SliverGrid(propsOrChildren = {}, maybeChildren = undefined) {
295
+ return GridView(propsOrChildren, maybeChildren);
296
+ }
297
+
298
+ export function SliverPadding(propsOrChildren = {}, maybeChildren = undefined) {
299
+ const [props, children] = normalizeWidgetArgs(propsOrChildren, maybeChildren);
300
+
301
+ return {
302
+ tag: "div",
303
+ props: {
304
+ ...omitProps(props, ["padding"]),
305
+ style: cleanStyle({
306
+ padding: edgeInsets(props.padding ?? 0),
307
+ ...props.style,
308
+ }),
309
+ },
310
+ children,
311
+ key: props.key,
312
+ };
313
+ }
314
+
315
+ export function SliverToBoxAdapter(
316
+ propsOrChildren = {},
317
+ maybeChildren = undefined,
318
+ ) {
319
+ const [props, children] = normalizeWidgetArgs(propsOrChildren, maybeChildren);
320
+
321
+ return {
322
+ tag: "div",
323
+ props: {
324
+ ...omitProps(props),
325
+ style: cleanStyle(props.style),
326
+ },
327
+ children,
328
+ key: props.key,
329
+ };
330
+ }