@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,112 @@
1
+ import {
2
+ cleanStyle,
3
+ normalizeWidgetArgs,
4
+ omitProps,
5
+ px,
6
+ } from "./utils.js";
7
+
8
+ function transition(duration = 200, curve = "ease", properties = "all") {
9
+ return `${properties} ${duration}ms ${curve}`;
10
+ }
11
+
12
+ export function AnimatedContainer(
13
+ propsOrChildren = {},
14
+ maybeChildren = undefined,
15
+ ) {
16
+ const [props, children] = normalizeWidgetArgs(propsOrChildren, maybeChildren);
17
+
18
+ return {
19
+ tag: "div",
20
+ props: {
21
+ ...omitProps(props, ["duration", "curve", "transition"]),
22
+ style: cleanStyle({
23
+ transition: props.transition || transition(props.duration, props.curve),
24
+ width: px(props.width),
25
+ height: px(props.height),
26
+ ...props.style,
27
+ }),
28
+ },
29
+ children,
30
+ key: props.key,
31
+ };
32
+ }
33
+
34
+ export function AnimatedOpacity(
35
+ propsOrChildren = {},
36
+ maybeChildren = undefined,
37
+ ) {
38
+ const [props, children] = normalizeWidgetArgs(propsOrChildren, maybeChildren);
39
+
40
+ return {
41
+ tag: "div",
42
+ props: {
43
+ ...omitProps(props, ["opacity", "duration", "curve"]),
44
+ style: cleanStyle({
45
+ opacity: props.opacity ?? 1,
46
+ transition: transition(props.duration, props.curve, "opacity"),
47
+ ...props.style,
48
+ }),
49
+ },
50
+ children,
51
+ key: props.key,
52
+ };
53
+ }
54
+
55
+ export function AnimatedScale(propsOrChildren = {}, maybeChildren = undefined) {
56
+ const [props, children] = normalizeWidgetArgs(propsOrChildren, maybeChildren);
57
+
58
+ return {
59
+ tag: "div",
60
+ props: {
61
+ ...omitProps(props, ["scale", "duration", "curve", "origin"]),
62
+ style: cleanStyle({
63
+ transform: `scale(${props.scale ?? 1})`,
64
+ transformOrigin: props.origin || "center",
65
+ transition: transition(props.duration, props.curve, "transform"),
66
+ ...props.style,
67
+ }),
68
+ },
69
+ children,
70
+ key: props.key,
71
+ };
72
+ }
73
+
74
+ export function AnimatedSlide(propsOrChildren = {}, maybeChildren = undefined) {
75
+ const [props, children] = normalizeWidgetArgs(propsOrChildren, maybeChildren);
76
+ const offset = props.offset || { x: 0, y: 0 };
77
+
78
+ return {
79
+ tag: "div",
80
+ props: {
81
+ ...omitProps(props, ["offset", "duration", "curve"]),
82
+ style: cleanStyle({
83
+ transform: `translate(${px(offset.x ?? 0)}, ${px(offset.y ?? 0)})`,
84
+ transition: transition(props.duration, props.curve, "transform"),
85
+ ...props.style,
86
+ }),
87
+ },
88
+ children,
89
+ key: props.key,
90
+ };
91
+ }
92
+
93
+ export function AnimatedSwitcher({
94
+ child,
95
+ duration = 180,
96
+ curve = "ease",
97
+ style = {},
98
+ ...props
99
+ }) {
100
+ return {
101
+ tag: "div",
102
+ props: {
103
+ ...props,
104
+ style: cleanStyle({
105
+ transition: transition(duration, curve, "opacity, transform"),
106
+ ...style,
107
+ }),
108
+ },
109
+ children: [child],
110
+ key: props.key,
111
+ };
112
+ }
@@ -0,0 +1,312 @@
1
+ import {
2
+ applyFieldFocus,
3
+ clearFieldFocus,
4
+ ensureGlobalStyle,
5
+ fieldStyle,
6
+ luminaTheme,
7
+ } from "./utils.js";
8
+
9
+ function ensureControlStyles() {
10
+ ensureGlobalStyle(
11
+ "lumina-control-styles",
12
+ `
13
+ .lumina-button:not(:disabled):hover {
14
+ filter: saturate(1.06);
15
+ transform: translateY(-1px);
16
+ }
17
+ .lumina-button:not(:disabled):active {
18
+ transform: translateY(0);
19
+ }
20
+ .lumina-button:focus-visible,
21
+ .lumina-switch:focus-visible,
22
+ .lumina-field:focus-visible {
23
+ outline: 2px solid ${luminaTheme.colors.focus};
24
+ outline-offset: 2px;
25
+ }
26
+ .lumina-field:hover:not(:disabled) {
27
+ border-color: ${luminaTheme.colors.primary} !important;
28
+ }
29
+ `,
30
+ );
31
+ }
32
+
33
+ function genId(prefix = "id") {
34
+ if (typeof crypto !== "undefined" && crypto.randomUUID)
35
+ return `${prefix}-${crypto.randomUUID()}`;
36
+ return `${prefix}-${Date.now()}-${Math.floor(Math.random() * 10000)}`;
37
+ }
38
+
39
+ export function Button({
40
+ text,
41
+ onClick,
42
+ variant = "primary",
43
+ disabled = false,
44
+ type = "button",
45
+ style = {},
46
+ ...props
47
+ }) {
48
+ ensureControlStyles();
49
+ const variants = {
50
+ primary: {
51
+ backgroundColor: luminaTheme.colors.primary,
52
+ color: "white",
53
+ border: `1px solid ${luminaTheme.colors.primary}`,
54
+ boxShadow: "0 8px 18px rgba(37, 99, 235, 0.20)",
55
+ },
56
+ secondary: {
57
+ backgroundColor: luminaTheme.colors.surface,
58
+ color: luminaTheme.colors.primary,
59
+ border: `1px solid ${luminaTheme.colors.borderStrong}`,
60
+ boxShadow: luminaTheme.shadow.xs,
61
+ },
62
+ text: {
63
+ backgroundColor: "transparent",
64
+ color: luminaTheme.colors.primary,
65
+ border: "1px solid transparent",
66
+ },
67
+ danger: {
68
+ backgroundColor: luminaTheme.colors.danger,
69
+ color: "white",
70
+ border: `1px solid ${luminaTheme.colors.danger}`,
71
+ boxShadow: "0 8px 18px rgba(220, 38, 38, 0.18)",
72
+ },
73
+ };
74
+
75
+ const finalStyle = {
76
+ minHeight: "36px",
77
+ display: "inline-flex",
78
+ alignItems: "center",
79
+ justifyContent: "center",
80
+ gap: "8px",
81
+ padding: "8px 14px",
82
+ borderRadius: luminaTheme.radius.md,
83
+ cursor: disabled ? "not-allowed" : "pointer",
84
+ fontSize: "14px",
85
+ fontWeight: 700,
86
+ fontFamily: "inherit",
87
+ lineHeight: 1,
88
+ whiteSpace: "nowrap",
89
+ userSelect: "none",
90
+ transition: `background-color ${luminaTheme.transition}, border-color ${luminaTheme.transition}, color ${luminaTheme.transition}, box-shadow ${luminaTheme.transition}, transform ${luminaTheme.transition}`,
91
+ opacity: disabled ? 0.5 : 1,
92
+ outline: "none",
93
+ ...variants[variant],
94
+ ...style,
95
+ };
96
+
97
+ return {
98
+ tag: "button",
99
+ props: {
100
+ type,
101
+ disabled,
102
+ onClick,
103
+ style: finalStyle,
104
+ ...props,
105
+ className: ["lumina-button", `lumina-button-${variant}`, props.className]
106
+ .filter(Boolean)
107
+ .join(" "),
108
+ },
109
+ children: [text],
110
+ };
111
+ }
112
+
113
+ export function Input({
114
+ value,
115
+ onChange,
116
+ onInput,
117
+ placeholder = "",
118
+ type = "text",
119
+ id,
120
+ style = {},
121
+ ...props
122
+ }) {
123
+ ensureControlStyles();
124
+ const isCheckbox = type === "checkbox";
125
+ const finalId = id || genId("input");
126
+ const finalStyle = isCheckbox
127
+ ? {
128
+ width: "16px",
129
+ height: "16px",
130
+ accentColor: luminaTheme.colors.primary,
131
+ cursor: props.disabled ? "not-allowed" : "pointer",
132
+ ...style,
133
+ }
134
+ : fieldStyle(style);
135
+
136
+ return {
137
+ tag: "input",
138
+ props: {
139
+ id: finalId,
140
+ type,
141
+ ...(isCheckbox ? { checked: !!value } : { value: value ?? "" }),
142
+ placeholder: placeholder || undefined,
143
+ ...(isCheckbox
144
+ ? {
145
+ onChange: (e) => {
146
+ if (onChange) onChange(e.target.checked);
147
+ },
148
+ }
149
+ : {
150
+ onInput: (e) => {
151
+ if (onInput) onInput(e);
152
+ if (onChange) onChange(e.target.value);
153
+ },
154
+ onChange: (e) => {
155
+ if (onChange) onChange(e.target.value);
156
+ },
157
+ }),
158
+ onFocus: (e) => {
159
+ if (props.onFocus) props.onFocus(e);
160
+ if (e.defaultPrevented) return;
161
+ if (!isCheckbox) applyFieldFocus(e, style);
162
+ },
163
+ onBlur: (e) => {
164
+ if (props.onBlur) props.onBlur(e);
165
+ if (e.defaultPrevented) return;
166
+ if (!isCheckbox) clearFieldFocus(e, style);
167
+ },
168
+ style: finalStyle,
169
+ ...Object.fromEntries(
170
+ Object.entries(props).filter(
171
+ ([key]) => key !== "onFocus" && key !== "onBlur",
172
+ ),
173
+ ),
174
+ className: [
175
+ isCheckbox ? "lumina-checkbox-input" : "lumina-field",
176
+ props.className,
177
+ ]
178
+ .filter(Boolean)
179
+ .join(" "),
180
+ },
181
+ children: [],
182
+ };
183
+ }
184
+
185
+ export function TextField(props) {
186
+ return Input({ ...props, type: props.type || "text" });
187
+ }
188
+
189
+ export function Checkbox({
190
+ checked = false,
191
+ onChange,
192
+ label = "",
193
+ id,
194
+ disabled = false,
195
+ style = {},
196
+ ...props
197
+ }) {
198
+ ensureControlStyles();
199
+ const finalId = id || genId("checkbox");
200
+
201
+ return {
202
+ tag: "label",
203
+ props: {
204
+ htmlFor: finalId,
205
+ style: {
206
+ display: "inline-flex",
207
+ alignItems: "center",
208
+ gap: "8px",
209
+ color: luminaTheme.colors.text,
210
+ fontSize: "14px",
211
+ lineHeight: 1.4,
212
+ cursor: disabled ? "not-allowed" : "pointer",
213
+ userSelect: "none",
214
+ opacity: disabled ? 0.6 : 1,
215
+ ...style,
216
+ },
217
+ ...props,
218
+ },
219
+ children: [
220
+ {
221
+ tag: "input",
222
+ props: {
223
+ id: finalId,
224
+ type: "checkbox",
225
+ checked: !!checked,
226
+ disabled,
227
+ onChange: (e) => {
228
+ if (!disabled && onChange) onChange(e.target.checked);
229
+ },
230
+ style: {
231
+ width: "16px",
232
+ height: "16px",
233
+ accentColor: luminaTheme.colors.primary,
234
+ cursor: disabled ? "not-allowed" : "pointer",
235
+ },
236
+ },
237
+ children: [],
238
+ },
239
+ label || "",
240
+ ],
241
+ };
242
+ }
243
+
244
+ export function Switch({
245
+ value = false,
246
+ onChange,
247
+ ariaLabel,
248
+ disabled = false,
249
+ style = {},
250
+ ...props
251
+ }) {
252
+ ensureControlStyles();
253
+ const base = {
254
+ width: "46px",
255
+ height: "24px",
256
+ borderRadius: luminaTheme.radius.pill,
257
+ backgroundColor: value ? luminaTheme.colors.primary : luminaTheme.colors.track,
258
+ border: `1px solid ${value ? luminaTheme.colors.primary : luminaTheme.colors.borderStrong}`,
259
+ cursor: disabled ? "not-allowed" : "pointer",
260
+ position: "relative",
261
+ transition: `background-color ${luminaTheme.transition}, border-color ${luminaTheme.transition}, box-shadow ${luminaTheme.transition}`,
262
+ outline: "none",
263
+ boxShadow: value ? "0 8px 18px rgba(37, 99, 235, 0.16)" : "none",
264
+ opacity: disabled ? 0.6 : 1,
265
+ ...style,
266
+ };
267
+
268
+ return {
269
+ tag: "button",
270
+ props: {
271
+ ...props,
272
+ role: "switch",
273
+ type: props.type || "button",
274
+ "aria-checked": !!value,
275
+ "aria-label": ariaLabel || "toggle",
276
+ disabled,
277
+ tabIndex: 0,
278
+ onClick: (event) => {
279
+ if (props.onClick) props.onClick(event);
280
+ if (!disabled && onChange) onChange(!value);
281
+ },
282
+ onKeyDown: (e) => {
283
+ if (props.onKeyDown) props.onKeyDown(e);
284
+ if (e.key === " " || e.key === "Enter") {
285
+ e.preventDefault();
286
+ if (!disabled && onChange) onChange(!value);
287
+ }
288
+ },
289
+ style: base,
290
+ className: ["lumina-switch", props.className].filter(Boolean).join(" "),
291
+ },
292
+ children: [
293
+ {
294
+ tag: "div",
295
+ props: {
296
+ style: {
297
+ width: "20px",
298
+ height: "20px",
299
+ borderRadius: "10px",
300
+ backgroundColor: luminaTheme.colors.surface,
301
+ position: "absolute",
302
+ top: "1px",
303
+ left: value ? "22px" : "1px",
304
+ transition: `left ${luminaTheme.transition}, transform ${luminaTheme.transition}`,
305
+ boxShadow: "0 2px 6px rgba(15, 23, 42, 0.22)",
306
+ },
307
+ },
308
+ children: [],
309
+ },
310
+ ],
311
+ };
312
+ }