@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,316 @@
1
+ import {
2
+ cleanStyle,
3
+ ensureGlobalStyle,
4
+ luminaTheme,
5
+ normalizeWidgetArgs,
6
+ omitProps,
7
+ px,
8
+ } from "./utils.js";
9
+
10
+ function ensureFeedbackStyles() {
11
+ ensureGlobalStyle(
12
+ "lumina-feedback-styles",
13
+ `
14
+ @keyframes lumina-spin {
15
+ to { transform: rotate(360deg); }
16
+ }
17
+ @keyframes lumina-progress {
18
+ 0% { transform: translateX(-100%); }
19
+ 50% { transform: translateX(0%); }
20
+ 100% { transform: translateX(100%); }
21
+ }
22
+ `,
23
+ );
24
+ }
25
+
26
+ export function ModalBarrier(props = {}) {
27
+ return {
28
+ tag: "div",
29
+ props: {
30
+ ...omitProps(props, ["color", "dismissible", "onDismiss"]),
31
+ onClick: props.dismissible === false ? undefined : props.onDismiss,
32
+ style: cleanStyle({
33
+ position: "fixed",
34
+ inset: 0,
35
+ backgroundColor: props.color || luminaTheme.colors.overlay,
36
+ backdropFilter: "blur(2px)",
37
+ zIndex: props.zIndex ?? 1000,
38
+ ...props.style,
39
+ }),
40
+ },
41
+ children: [],
42
+ key: props.key,
43
+ };
44
+ }
45
+
46
+ export function Dialog(propsOrChildren = {}, maybeChildren = undefined) {
47
+ const [props, children] = normalizeWidgetArgs(propsOrChildren, maybeChildren);
48
+ if (props.open === false) return null;
49
+
50
+ const zIndex = props.zIndex ?? 1000;
51
+
52
+ return {
53
+ tag: "div",
54
+ props: {
55
+ role: "presentation",
56
+ style: cleanStyle({
57
+ position: "fixed",
58
+ inset: 0,
59
+ zIndex,
60
+ display: "flex",
61
+ alignItems: "center",
62
+ justifyContent: "center",
63
+ padding: px(props.padding ?? 20),
64
+ ...props.overlayStyle,
65
+ }),
66
+ },
67
+ children: [
68
+ ModalBarrier({
69
+ color: props.barrierColor,
70
+ dismissible: props.dismissible,
71
+ onDismiss: props.onDismiss,
72
+ zIndex,
73
+ }),
74
+ {
75
+ tag: "div",
76
+ props: {
77
+ ...omitProps(props, [
78
+ "open",
79
+ "barrierColor",
80
+ "dismissible",
81
+ "onDismiss",
82
+ "padding",
83
+ "overlayStyle",
84
+ "width",
85
+ ]),
86
+ role: "dialog",
87
+ "aria-modal": "true",
88
+ style: cleanStyle({
89
+ position: "relative",
90
+ zIndex: zIndex + 1,
91
+ width: px(props.width, "min(100%, 420px)"),
92
+ maxWidth: "100%",
93
+ maxHeight: "calc(100vh - 40px)",
94
+ overflow: "auto",
95
+ border: `1px solid ${luminaTheme.colors.border}`,
96
+ borderRadius: luminaTheme.radius.xl,
97
+ backgroundColor: luminaTheme.colors.surface,
98
+ boxShadow: luminaTheme.shadow.lg,
99
+ ...props.style,
100
+ }),
101
+ },
102
+ children,
103
+ },
104
+ ],
105
+ key: props.key,
106
+ };
107
+ }
108
+
109
+ export function AlertDialog(props = {}) {
110
+ const actions = props.actions || [];
111
+ return Dialog(
112
+ {
113
+ open: props.open,
114
+ onDismiss: props.onDismiss,
115
+ dismissible: props.dismissible,
116
+ width: props.width,
117
+ style: props.style,
118
+ },
119
+ [
120
+ {
121
+ tag: "div",
122
+ props: {
123
+ style: {
124
+ padding: "22px",
125
+ display: "flex",
126
+ flexDirection: "column",
127
+ gap: "12px",
128
+ },
129
+ },
130
+ children: [
131
+ props.title
132
+ ? {
133
+ tag: "h2",
134
+ props: {
135
+ style: {
136
+ margin: 0,
137
+ fontSize: "20px",
138
+ lineHeight: 1.2,
139
+ color: luminaTheme.colors.text,
140
+ },
141
+ },
142
+ children: [props.title],
143
+ }
144
+ : null,
145
+ props.content
146
+ ? {
147
+ tag: "div",
148
+ props: {
149
+ style: {
150
+ lineHeight: 1.5,
151
+ color: luminaTheme.colors.muted,
152
+ },
153
+ },
154
+ children: Array.isArray(props.content)
155
+ ? props.content
156
+ : [props.content],
157
+ }
158
+ : null,
159
+ actions.length
160
+ ? {
161
+ tag: "div",
162
+ props: {
163
+ style: {
164
+ display: "flex",
165
+ justifyContent: "flex-end",
166
+ gap: "8px",
167
+ marginTop: "8px",
168
+ },
169
+ },
170
+ children: actions,
171
+ }
172
+ : null,
173
+ ],
174
+ },
175
+ ],
176
+ );
177
+ }
178
+
179
+ export function SnackBar(propsOrChildren = {}, maybeChildren = undefined) {
180
+ const [props, children] = normalizeWidgetArgs(propsOrChildren, maybeChildren);
181
+ if (props.open === false) return null;
182
+ const onClick = (event) => {
183
+ event.stopPropagation();
184
+ if (props.onClick) props.onClick(event);
185
+ };
186
+
187
+ return {
188
+ tag: "div",
189
+ props: {
190
+ ...omitProps(props, ["open", "message", "action", "position", "zIndex"]),
191
+ role: "status",
192
+ onClick,
193
+ style: cleanStyle({
194
+ position: "fixed",
195
+ left: "50%",
196
+ bottom: props.position === "top" ? undefined : "20px",
197
+ top: props.position === "top" ? "20px" : undefined,
198
+ transform: "translateX(-50%)",
199
+ zIndex: props.zIndex ?? 3000,
200
+ pointerEvents: "auto",
201
+ isolation: "isolate",
202
+ display: "flex",
203
+ alignItems: "center",
204
+ gap: "14px",
205
+ minWidth: "min(420px, calc(100vw - 32px))",
206
+ maxWidth: "calc(100vw - 32px)",
207
+ padding: "12px 14px",
208
+ border: "1px solid rgba(255, 255, 255, 0.10)",
209
+ borderRadius: luminaTheme.radius.lg,
210
+ backgroundColor: "#0f172a",
211
+ color: "#ffffff",
212
+ boxShadow: luminaTheme.shadow.md,
213
+ ...props.style,
214
+ }),
215
+ },
216
+ children: [
217
+ {
218
+ tag: "div",
219
+ props: { style: { flex: 1 } },
220
+ children: children.length ? children : [props.message || ""],
221
+ },
222
+ props.action || null,
223
+ ],
224
+ key: props.key,
225
+ };
226
+ }
227
+
228
+ export function Tooltip(propsOrChildren = {}, maybeChildren = undefined) {
229
+ const [props, children] = normalizeWidgetArgs(propsOrChildren, maybeChildren);
230
+ return {
231
+ tag: "span",
232
+ props: {
233
+ ...omitProps(props, ["message"]),
234
+ title: props.message,
235
+ style: cleanStyle({
236
+ display: "inline-flex",
237
+ ...props.style,
238
+ }),
239
+ },
240
+ children,
241
+ key: props.key,
242
+ };
243
+ }
244
+
245
+ export function LinearProgressIndicator(props = {}) {
246
+ ensureFeedbackStyles();
247
+ const value = props.value;
248
+ const determinate = typeof value === "number";
249
+
250
+ return {
251
+ tag: "div",
252
+ props: {
253
+ ...omitProps(props, ["value", "color", "trackColor", "height"]),
254
+ role: "progressbar",
255
+ "aria-valuemin": determinate ? 0 : undefined,
256
+ "aria-valuemax": determinate ? 1 : undefined,
257
+ "aria-valuenow": determinate ? value : undefined,
258
+ style: cleanStyle({
259
+ position: "relative",
260
+ overflow: "hidden",
261
+ width: "100%",
262
+ height: px(props.height ?? 6),
263
+ borderRadius: "999px",
264
+ backgroundColor: props.trackColor || luminaTheme.colors.track,
265
+ ...props.style,
266
+ }),
267
+ },
268
+ children: [
269
+ {
270
+ tag: "div",
271
+ props: {
272
+ style: cleanStyle({
273
+ position: "absolute",
274
+ inset: 0,
275
+ width: determinate
276
+ ? `${Math.max(0, Math.min(1, value)) * 100}%`
277
+ : "55%",
278
+ backgroundColor: props.color || luminaTheme.colors.primary,
279
+ borderRadius: "inherit",
280
+ animation: determinate
281
+ ? undefined
282
+ : "lumina-progress 1.3s ease-in-out infinite",
283
+ }),
284
+ },
285
+ children: [],
286
+ },
287
+ ],
288
+ key: props.key,
289
+ };
290
+ }
291
+
292
+ export function CircularProgressIndicator(props = {}) {
293
+ ensureFeedbackStyles();
294
+ const size = props.size ?? 32;
295
+ const strokeWidth = props.strokeWidth ?? 4;
296
+
297
+ return {
298
+ tag: "span",
299
+ props: {
300
+ ...omitProps(props, ["size", "strokeWidth", "color", "trackColor"]),
301
+ role: "progressbar",
302
+ style: cleanStyle({
303
+ display: "inline-block",
304
+ width: px(size),
305
+ height: px(size),
306
+ borderRadius: "50%",
307
+ border: `${px(strokeWidth)} solid ${props.trackColor || luminaTheme.colors.track}`,
308
+ borderTopColor: props.color || luminaTheme.colors.primary,
309
+ animation: "lumina-spin 0.8s linear infinite",
310
+ ...props.style,
311
+ }),
312
+ },
313
+ children: [],
314
+ key: props.key,
315
+ };
316
+ }
@@ -0,0 +1,342 @@
1
+ import {
2
+ applyFieldFocus,
3
+ cleanStyle,
4
+ clearFieldFocus,
5
+ ensureGlobalStyle,
6
+ fieldStyle,
7
+ luminaTheme,
8
+ normalizeWidgetArgs,
9
+ omitProps,
10
+ px,
11
+ } from "./utils.js";
12
+
13
+ function ensureFormStyles() {
14
+ ensureGlobalStyle(
15
+ "lumina-form-styles",
16
+ `
17
+ .lumina-field:focus-visible {
18
+ outline: 2px solid ${luminaTheme.colors.focus};
19
+ outline-offset: 2px;
20
+ }
21
+ .lumina-field:hover:not(:disabled) {
22
+ border-color: ${luminaTheme.colors.primary} !important;
23
+ }
24
+ `,
25
+ );
26
+ }
27
+
28
+ export function Form(propsOrChildren = {}, maybeChildren = undefined) {
29
+ const [props, children] = normalizeWidgetArgs(propsOrChildren, maybeChildren);
30
+
31
+ return {
32
+ tag: "form",
33
+ props: {
34
+ ...omitProps(props, ["onSubmit", "gap"]),
35
+ noValidate: props.noValidate ?? true,
36
+ onSubmit: (event) => {
37
+ event.preventDefault();
38
+ if (props.onSubmit) props.onSubmit(event);
39
+ },
40
+ style: cleanStyle({
41
+ display: "flex",
42
+ flexDirection: "column",
43
+ gap: px(props.gap ?? 12),
44
+ color: luminaTheme.colors.text,
45
+ ...props.style,
46
+ }),
47
+ },
48
+ children,
49
+ key: props.key,
50
+ };
51
+ }
52
+
53
+ export function FormField(propsOrChildren = {}, maybeChildren = undefined) {
54
+ const [props, children] = normalizeWidgetArgs(propsOrChildren, maybeChildren);
55
+ const {
56
+ label,
57
+ helperText,
58
+ errorText,
59
+ required = false,
60
+ style = {},
61
+ } = props;
62
+
63
+ return {
64
+ tag: "label",
65
+ props: {
66
+ ...omitProps(props, ["label", "helperText", "errorText", "required"]),
67
+ style: cleanStyle({
68
+ display: "flex",
69
+ flexDirection: "column",
70
+ gap: "6px",
71
+ color: "inherit",
72
+ ...style,
73
+ }),
74
+ },
75
+ children: [
76
+ label
77
+ ? {
78
+ tag: "span",
79
+ props: {
80
+ style: {
81
+ display: "inline-flex",
82
+ gap: "4px",
83
+ fontSize: "13px",
84
+ fontWeight: 700,
85
+ color: luminaTheme.colors.text,
86
+ },
87
+ },
88
+ children: [
89
+ label,
90
+ required
91
+ ? {
92
+ tag: "span",
93
+ props: { style: { color: luminaTheme.colors.danger } },
94
+ children: ["*"],
95
+ }
96
+ : "",
97
+ ],
98
+ }
99
+ : null,
100
+ ...children,
101
+ helperText || errorText
102
+ ? {
103
+ tag: "span",
104
+ props: {
105
+ style: {
106
+ minHeight: "16px",
107
+ fontSize: "12px",
108
+ color: errorText ? luminaTheme.colors.danger : luminaTheme.colors.muted,
109
+ },
110
+ },
111
+ children: [errorText || helperText],
112
+ }
113
+ : null,
114
+ ],
115
+ key: props.key,
116
+ };
117
+ }
118
+
119
+ export function Radio({
120
+ value,
121
+ groupValue,
122
+ onChange,
123
+ label = "",
124
+ name,
125
+ disabled = false,
126
+ style = {},
127
+ ...props
128
+ }) {
129
+ ensureFormStyles();
130
+ return {
131
+ tag: "label",
132
+ props: {
133
+ ...props,
134
+ style: cleanStyle({
135
+ display: "inline-flex",
136
+ alignItems: "center",
137
+ gap: "8px",
138
+ color: luminaTheme.colors.text,
139
+ fontSize: "14px",
140
+ lineHeight: 1.4,
141
+ cursor: disabled ? "not-allowed" : "pointer",
142
+ opacity: disabled ? 0.55 : 1,
143
+ userSelect: "none",
144
+ ...style,
145
+ }),
146
+ },
147
+ children: [
148
+ {
149
+ tag: "input",
150
+ props: {
151
+ type: "radio",
152
+ name,
153
+ value,
154
+ checked: value === groupValue,
155
+ disabled,
156
+ style: {
157
+ width: "16px",
158
+ height: "16px",
159
+ accentColor: luminaTheme.colors.primary,
160
+ cursor: disabled ? "not-allowed" : "pointer",
161
+ },
162
+ onChange: () => {
163
+ if (!disabled && onChange) onChange(value);
164
+ },
165
+ },
166
+ children: [],
167
+ },
168
+ label,
169
+ ],
170
+ key: props.key,
171
+ };
172
+ }
173
+
174
+ export function RadioGroup(propsOrChildren = {}, maybeChildren = undefined) {
175
+ const [props, givenChildren] = normalizeWidgetArgs(
176
+ propsOrChildren,
177
+ maybeChildren,
178
+ );
179
+ const options = props.options || [];
180
+ const children = givenChildren.length
181
+ ? givenChildren
182
+ : options.map((option) =>
183
+ Radio({
184
+ key: option.value,
185
+ name: props.name,
186
+ value: option.value,
187
+ groupValue: props.value,
188
+ label: option.label,
189
+ disabled: option.disabled,
190
+ onChange: props.onChange,
191
+ }),
192
+ );
193
+
194
+ return {
195
+ tag: "div",
196
+ props: {
197
+ ...omitProps(props, ["options", "value", "onChange", "name", "gap"]),
198
+ role: "radiogroup",
199
+ style: cleanStyle({
200
+ display: "flex",
201
+ flexDirection: props.direction === "horizontal" ? "row" : "column",
202
+ gap: px(props.gap ?? 8),
203
+ ...props.style,
204
+ }),
205
+ },
206
+ children,
207
+ key: props.key,
208
+ };
209
+ }
210
+
211
+ export function Slider({
212
+ value = 0,
213
+ min = 0,
214
+ max = 100,
215
+ step = 1,
216
+ onChange,
217
+ disabled = false,
218
+ style = {},
219
+ ...props
220
+ }) {
221
+ return {
222
+ tag: "input",
223
+ props: {
224
+ ...props,
225
+ type: "range",
226
+ min,
227
+ max,
228
+ step,
229
+ value,
230
+ disabled,
231
+ onInput: (event) => {
232
+ if (props.onInput) props.onInput(event);
233
+ if (onChange) onChange(Number(event.target.value));
234
+ },
235
+ style: cleanStyle({
236
+ width: "100%",
237
+ accentColor: luminaTheme.colors.primary,
238
+ cursor: disabled ? "not-allowed" : "pointer",
239
+ ...style,
240
+ }),
241
+ },
242
+ children: [],
243
+ key: props.key,
244
+ };
245
+ }
246
+
247
+ export function Dropdown({
248
+ value,
249
+ options = [],
250
+ onChange,
251
+ placeholder,
252
+ disabled = false,
253
+ style = {},
254
+ ...props
255
+ }) {
256
+ return {
257
+ tag: "select",
258
+ props: {
259
+ ...props,
260
+ value: value ?? "",
261
+ disabled,
262
+ onChange: (event) => {
263
+ if (onChange) onChange(event.target.value);
264
+ },
265
+ onFocus: (event) => {
266
+ if (props.onFocus) props.onFocus(event);
267
+ if (!event.defaultPrevented) applyFieldFocus(event, style);
268
+ },
269
+ onBlur: (event) => {
270
+ if (props.onBlur) props.onBlur(event);
271
+ if (!event.defaultPrevented) clearFieldFocus(event, style);
272
+ },
273
+ style: fieldStyle({
274
+ appearance: "auto",
275
+ cursor: disabled ? "not-allowed" : "pointer",
276
+ ...style,
277
+ }),
278
+ className: ["lumina-field", props.className].filter(Boolean).join(" "),
279
+ },
280
+ children: [
281
+ placeholder
282
+ ? {
283
+ tag: "option",
284
+ props: { value: "", disabled: true },
285
+ children: [placeholder],
286
+ }
287
+ : null,
288
+ ...options.map((option) => ({
289
+ tag: "option",
290
+ props: {
291
+ value: option.value,
292
+ disabled: option.disabled,
293
+ selected: option.value === value,
294
+ },
295
+ children: [option.label],
296
+ key: option.value,
297
+ })),
298
+ ],
299
+ key: props.key,
300
+ };
301
+ }
302
+
303
+ export function TextArea({
304
+ value = "",
305
+ onChange,
306
+ rows = 4,
307
+ placeholder = "",
308
+ style = {},
309
+ ...props
310
+ }) {
311
+ ensureFormStyles();
312
+ return {
313
+ tag: "textarea",
314
+ props: {
315
+ ...props,
316
+ value,
317
+ rows,
318
+ placeholder,
319
+ onInput: (event) => {
320
+ if (props.onInput) props.onInput(event);
321
+ if (onChange) onChange(event.target.value);
322
+ },
323
+ onFocus: (event) => {
324
+ if (props.onFocus) props.onFocus(event);
325
+ if (!event.defaultPrevented) applyFieldFocus(event, style);
326
+ },
327
+ onBlur: (event) => {
328
+ if (props.onBlur) props.onBlur(event);
329
+ if (!event.defaultPrevented) clearFieldFocus(event, style);
330
+ },
331
+ style: fieldStyle({
332
+ resize: "vertical",
333
+ minHeight: "96px",
334
+ lineHeight: 1.5,
335
+ ...style,
336
+ }),
337
+ className: ["lumina-field", props.className].filter(Boolean).join(" "),
338
+ },
339
+ children: [],
340
+ key: props.key,
341
+ };
342
+ }