@bunnix/components 0.10.3 → 0.11.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/@types/index.d.ts +179 -15
- package/README.md +41 -4
- package/package.json +3 -8
- package/src/core/buttons.css +1 -0
- package/src/core/core.css +17 -3
- package/src/core/dialog.css +3 -1
- package/src/core/dialog.mjs +101 -16
- package/src/core/input.css +202 -0
- package/src/core/inputs.mjs +723 -23
- package/src/core/layout.mjs +1 -2
- package/src/core/media.css +36 -1
- package/src/core/media.mjs +13 -13
- package/src/core/menu.css +10 -29
- package/src/core/menu.mjs +159 -70
- package/src/core/outline.mjs +100 -0
- package/src/core/sidebar.mjs +189 -68
- package/src/core/sliderUtils.mjs +51 -0
- package/src/core/table.css +23 -0
- package/src/core/table.mjs +35 -20
- package/src/core/textareaUtils.mjs +31 -0
- package/src/core/utils.mjs +105 -0
- package/src/font-face/Framework7Icons-Regular.woff2 +0 -0
- package/src/index.mjs +3 -1
- package/src/icons/add-circle.svg +0 -1
- package/src/icons/add.svg +0 -1
- package/src/icons/alt.svg +0 -1
- package/src/icons/archive.svg +0 -1
- package/src/icons/arrow-down.svg +0 -1
- package/src/icons/arrow-left.svg +0 -1
- package/src/icons/arrow-right.svg +0 -1
- package/src/icons/arrow-up.svg +0 -1
- package/src/icons/at.svg +0 -1
- package/src/icons/attestation.svg +0 -1
- package/src/icons/battery-25.svg +0 -1
- package/src/icons/bell.svg +0 -3
- package/src/icons/bookmark.svg +0 -1
- package/src/icons/bot.svg +0 -1
- package/src/icons/bubble.svg +0 -1
- package/src/icons/building.svg +0 -3
- package/src/icons/button.svg +0 -1
- package/src/icons/calculate.svg +0 -1
- package/src/icons/calendar.svg +0 -1
- package/src/icons/captions-bubble.svg +0 -1
- package/src/icons/cart.svg +0 -1
- package/src/icons/chart.svg +0 -1
- package/src/icons/check.svg +0 -1
- package/src/icons/chevron-down.svg +0 -1
- package/src/icons/chevron-left.svg +0 -1
- package/src/icons/chevron-right.svg +0 -1
- package/src/icons/clip.svg +0 -1
- package/src/icons/clock.svg +0 -3
- package/src/icons/close-circle.svg +0 -3
- package/src/icons/close.svg +0 -1
- package/src/icons/cloud-download.svg +0 -1
- package/src/icons/cloud-upload.svg +0 -1
- package/src/icons/cloud.svg +0 -1
- package/src/icons/columns-layout.svg +0 -1
- package/src/icons/command.svg +0 -1
- package/src/icons/cube.svg +0 -1
- package/src/icons/delete.svg +0 -3
- package/src/icons/dollar.svg +0 -3
- package/src/icons/download.svg +0 -1
- package/src/icons/draw.svg +0 -1
- package/src/icons/duplicate.svg +0 -3
- package/src/icons/ear.svg +0 -1
- package/src/icons/edit.svg +0 -1
- package/src/icons/exclamation-mark.svg +0 -1
- package/src/icons/eye-open.svg +0 -1
- package/src/icons/eye.svg +0 -1
- package/src/icons/file-html.svg +0 -1
- package/src/icons/file.svg +0 -3
- package/src/icons/finger.svg +0 -1
- package/src/icons/flag.svg +0 -1
- package/src/icons/folder.svg +0 -1
- package/src/icons/function.svg +0 -1
- package/src/icons/gear.svg +0 -1
- package/src/icons/gift.svg +0 -1
- package/src/icons/globe.svg +0 -3
- package/src/icons/grid.svg +0 -1
- package/src/icons/hammer.svg +0 -1
- package/src/icons/hand.svg +0 -1
- package/src/icons/hare.svg +0 -1
- package/src/icons/heart.svg +0 -3
- package/src/icons/home.svg +0 -3
- package/src/icons/image.svg +0 -1
- package/src/icons/inbox.svg +0 -3
- package/src/icons/info.svg +0 -1
- package/src/icons/key.svg +0 -1
- package/src/icons/lamp.svg +0 -1
- package/src/icons/link.svg +0 -1
- package/src/icons/location.svg +0 -1
- package/src/icons/locker.svg +0 -1
- package/src/icons/login.svg +0 -1
- package/src/icons/logout.svg +0 -3
- package/src/icons/mail.svg +0 -3
- package/src/icons/map.svg +0 -3
- package/src/icons/markup.svg +0 -1
- package/src/icons/merge.svg +0 -1
- package/src/icons/more-horizontal.svg +0 -5
- package/src/icons/more-vertical.svg +0 -5
- package/src/icons/mouse.svg +0 -1
- package/src/icons/music-mic.svg +0 -1
- package/src/icons/paintbrush.svg +0 -1
- package/src/icons/palette.svg +0 -1
- package/src/icons/password.svg +0 -1
- package/src/icons/pencil.svg +0 -1
- package/src/icons/people.svg +0 -3
- package/src/icons/percent.svg +0 -1
- package/src/icons/person-add.svg +0 -1
- package/src/icons/person-remove.svg +0 -1
- package/src/icons/person.svg +0 -4
- package/src/icons/phone.svg +0 -1
- package/src/icons/pin.svg +0 -1
- package/src/icons/question-circle.svg +0 -3
- package/src/icons/remove-circle.svg +0 -1
- package/src/icons/return-arrow.svg +0 -1
- package/src/icons/save.svg +0 -1
- package/src/icons/search.svg +0 -1
- package/src/icons/sections.svg +0 -1
- package/src/icons/send.svg +0 -1
- package/src/icons/share.svg +0 -1
- package/src/icons/shine.svg +0 -1
- package/src/icons/sliders.svg +0 -1
- package/src/icons/star.svg +0 -3
- package/src/icons/staroflife.svg +0 -1
- package/src/icons/storage.svg +0 -1
- package/src/icons/success-circle.svg +0 -3
- package/src/icons/swap.svg +0 -1
- package/src/icons/switch.svg +0 -1
- package/src/icons/sync.svg +0 -3
- package/src/icons/table.svg +0 -3
- package/src/icons/tag.svg +0 -3
- package/src/icons/terminal.svg +0 -1
- package/src/icons/text.svg +0 -1
- package/src/icons/thumb-down.svg +0 -1
- package/src/icons/thumb-up.svg +0 -1
- package/src/icons/timer.svg +0 -3
- package/src/icons/toggle.svg +0 -1
- package/src/icons/trash.svg +0 -1
- package/src/icons/tv-music.svg +0 -1
- package/src/icons/update-page.svg +0 -1
- package/src/icons/upload.svg +0 -1
- package/src/icons/video.svg +0 -1
- package/src/icons/wallet.svg +0 -1
- package/src/icons/wand-stars.svg +0 -1
- package/src/icons/waveform.svg +0 -1
- package/src/icons/window.svg +0 -1
- package/src/utils/iconRegistry.generated.mjs +0 -187
- package/src/utils/iconRegistry.mjs +0 -34
package/src/core/inputs.mjs
CHANGED
|
@@ -5,8 +5,11 @@
|
|
|
5
5
|
*
|
|
6
6
|
* Components:
|
|
7
7
|
* - TextInput: Single-line text input with optional placeholder and state binding
|
|
8
|
+
* - Picker: Menu-backed selection input with a selector-style trigger
|
|
9
|
+
* - SegmentedPicker: iOS-style segmented selection control with optional icons
|
|
8
10
|
* - Select: Dropdown input with mapped options
|
|
9
11
|
* - CheckBox: Simple checkbox input with optional state binding
|
|
12
|
+
* - Switch: OS-style boolean toggle with optional state binding
|
|
10
13
|
*
|
|
11
14
|
* Features:
|
|
12
15
|
* - Two-way binding with useState objects
|
|
@@ -14,13 +17,31 @@
|
|
|
14
17
|
* - Flexible props normalization (supports both props object and direct children)
|
|
15
18
|
* - Outline focus states via core.css utilities
|
|
16
19
|
*/
|
|
17
|
-
import Bunnix, { useState, useEffect } from "@bunnix/core";
|
|
18
|
-
import {
|
|
19
|
-
|
|
20
|
-
|
|
20
|
+
import Bunnix, { Compute, useState, useEffect, useRef, ForEach, Show } from "@bunnix/core";
|
|
21
|
+
import {
|
|
22
|
+
withNormalizedArgs,
|
|
23
|
+
withExtractedStyles,
|
|
24
|
+
isStateLike,
|
|
25
|
+
resolveCollectionState,
|
|
26
|
+
} from "./utils.mjs";
|
|
27
|
+
import { Column, Row, Spacer } from "./layout.mjs";
|
|
28
|
+
import { Heading, Text } from "./typography.mjs";
|
|
29
|
+
import { Icon } from "./media.mjs";
|
|
30
|
+
import { Menu } from "./menu.mjs";
|
|
31
|
+
import {
|
|
32
|
+
findNearestSliderStepIndex,
|
|
33
|
+
getSliderStepValue,
|
|
34
|
+
hasSliderStepLabels,
|
|
35
|
+
isValidSliderSteps,
|
|
36
|
+
toSliderNumber,
|
|
37
|
+
} from "./sliderUtils.mjs";
|
|
38
|
+
import {
|
|
39
|
+
getTextAreaHeightMetrics,
|
|
40
|
+
resolveTextAreaLines,
|
|
41
|
+
} from "./textareaUtils.mjs";
|
|
21
42
|
import "./input.css";
|
|
22
43
|
|
|
23
|
-
const { input, select, option } = Bunnix;
|
|
44
|
+
const { input, textarea, select, option, div, button } = Bunnix;
|
|
24
45
|
|
|
25
46
|
/**
|
|
26
47
|
* Wraps a component in a Column with a Heading label if props.label exists.
|
|
@@ -64,18 +85,104 @@ function wrapCheckBoxIntoLabel(props, component) {
|
|
|
64
85
|
return component;
|
|
65
86
|
}
|
|
66
87
|
|
|
88
|
+
function resolveNumericState(propValue, fallback) {
|
|
89
|
+
return isStateLike(propValue)
|
|
90
|
+
? propValue
|
|
91
|
+
: useState(toSliderNumber(propValue, fallback));
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function resolveBooleanState(propValue) {
|
|
95
|
+
return isStateLike(propValue)
|
|
96
|
+
? propValue
|
|
97
|
+
: useState(!!propValue);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function resolveInputFocusClass(outline) {
|
|
101
|
+
return outline ? "focus-border-outline focus-outline-dimmed" : "no-outline";
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function syncFocusedNode(node, shouldFocus) {
|
|
105
|
+
if (!node || typeof node.focus !== "function" || typeof node.blur !== "function") return;
|
|
106
|
+
if (shouldFocus) {
|
|
107
|
+
if (typeof document === "undefined" || document.activeElement !== node) {
|
|
108
|
+
node.focus();
|
|
109
|
+
}
|
|
110
|
+
return;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
if (typeof document === "undefined" || document.activeElement === node) {
|
|
114
|
+
node.blur();
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function getLineHeightPx(node) {
|
|
119
|
+
if (!node || typeof window === "undefined") return 20;
|
|
120
|
+
|
|
121
|
+
const computed = window.getComputedStyle(node);
|
|
122
|
+
const lineHeight = Number.parseFloat(computed.lineHeight);
|
|
123
|
+
|
|
124
|
+
if (Number.isFinite(lineHeight)) return lineHeight;
|
|
125
|
+
|
|
126
|
+
const fontSize = Number.parseFloat(computed.fontSize);
|
|
127
|
+
if (Number.isFinite(fontSize)) return fontSize * 1.5;
|
|
128
|
+
|
|
129
|
+
return 20;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function getTextAreaVerticalInset(node) {
|
|
133
|
+
if (!node || typeof window === "undefined") return 0;
|
|
134
|
+
|
|
135
|
+
const computed = window.getComputedStyle(node);
|
|
136
|
+
const values = [
|
|
137
|
+
computed.paddingTop,
|
|
138
|
+
computed.paddingBottom,
|
|
139
|
+
computed.borderTopWidth,
|
|
140
|
+
computed.borderBottomWidth,
|
|
141
|
+
];
|
|
142
|
+
|
|
143
|
+
return values.reduce((total, value) => {
|
|
144
|
+
const nextValue = Number.parseFloat(value);
|
|
145
|
+
return total + (Number.isFinite(nextValue) ? nextValue : 0);
|
|
146
|
+
}, 0);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
function resizeTextArea(node, minLines, maxLines) {
|
|
150
|
+
if (!node) return;
|
|
151
|
+
|
|
152
|
+
const metrics = getTextAreaHeightMetrics({
|
|
153
|
+
lineHeight: getLineHeightPx(node),
|
|
154
|
+
scrollHeight: node.scrollHeight,
|
|
155
|
+
minLines,
|
|
156
|
+
maxLines,
|
|
157
|
+
verticalInset: getTextAreaVerticalInset(node),
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
node.style.height = "auto";
|
|
161
|
+
node.style.height = `${metrics.nextHeight}px`;
|
|
162
|
+
node.style.overflowY = metrics.shouldScroll ? "auto" : "hidden";
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
function shouldInsertTextAreaNewline(event) {
|
|
166
|
+
if (event.key !== "Enter" || event.isComposing) return true;
|
|
167
|
+
return !!event.shiftKey && !event.metaKey && !event.ctrlKey && !event.altKey;
|
|
168
|
+
}
|
|
169
|
+
|
|
67
170
|
/** Text Input core component and logic */
|
|
68
171
|
const TextInputCore = (props, _) => {
|
|
69
172
|
let value =
|
|
70
173
|
props.value?.get && props.value?.set
|
|
71
174
|
? props.value
|
|
72
175
|
: useState(props.value ?? "");
|
|
176
|
+
let focusedValue = isStateLike(props.focused) ? props.focused : null;
|
|
177
|
+
let shouldAutoFocus = !focusedValue && !!props.focused;
|
|
178
|
+
let inputRef = useRef(null);
|
|
73
179
|
let placeholder = props.placeholder ?? "";
|
|
74
|
-
let
|
|
180
|
+
let focusClass = resolveInputFocusClass(props.outline);
|
|
75
181
|
let defaultClass =
|
|
76
|
-
"padding-sm border-primary radius-md flex-grow-1
|
|
182
|
+
"padding-sm border-primary radius-md flex-grow-1 bg-primary text-default";
|
|
77
183
|
|
|
78
184
|
delete props.outline;
|
|
185
|
+
delete props.focused;
|
|
79
186
|
|
|
80
187
|
let rawValue = useState("");
|
|
81
188
|
|
|
@@ -87,6 +194,12 @@ const TextInputCore = (props, _) => {
|
|
|
87
194
|
}
|
|
88
195
|
}, value);
|
|
89
196
|
|
|
197
|
+
if (focusedValue) {
|
|
198
|
+
useEffect((isFocused) => {
|
|
199
|
+
syncFocusedNode(inputRef.current, !!isFocused);
|
|
200
|
+
}, focusedValue);
|
|
201
|
+
}
|
|
202
|
+
|
|
90
203
|
const convertRawValue = (val) => {
|
|
91
204
|
rawValue.set(val);
|
|
92
205
|
let prevVal = value.get();
|
|
@@ -109,48 +222,373 @@ const TextInputCore = (props, _) => {
|
|
|
109
222
|
props,
|
|
110
223
|
input({
|
|
111
224
|
...props,
|
|
112
|
-
|
|
225
|
+
ref: inputRef,
|
|
226
|
+
autofocus: shouldAutoFocus,
|
|
227
|
+
value: rawValue,
|
|
113
228
|
disabled: props.disabled,
|
|
229
|
+
focus: () => {
|
|
230
|
+
if (focusedValue?.set) focusedValue.set(true);
|
|
231
|
+
},
|
|
232
|
+
blur: () => {
|
|
233
|
+
if (focusedValue?.set) focusedValue.set(false);
|
|
234
|
+
},
|
|
114
235
|
input: (e) => {
|
|
115
236
|
convertRawValue(e.target.value ?? "");
|
|
116
237
|
// value.set(e.target.value ?? "");
|
|
117
238
|
props.input && props.input(e);
|
|
118
239
|
},
|
|
119
240
|
placeholder: placeholder,
|
|
120
|
-
class: `${defaultClass} ${
|
|
241
|
+
class: `${defaultClass} ${focusClass} ${props.class || ""}`,
|
|
242
|
+
}),
|
|
243
|
+
);
|
|
244
|
+
};
|
|
245
|
+
|
|
246
|
+
const TextAreaCore = (props, _) => {
|
|
247
|
+
const value =
|
|
248
|
+
props.value?.get && props.value?.set
|
|
249
|
+
? props.value
|
|
250
|
+
: useState(props.value ?? "");
|
|
251
|
+
const focusedValue = isStateLike(props.focused) ? props.focused : null;
|
|
252
|
+
const shouldAutoFocus = !focusedValue && !!props.focused;
|
|
253
|
+
const rawValue = useState(value.get?.() ?? props.value ?? "");
|
|
254
|
+
const textAreaRef = useRef(null);
|
|
255
|
+
const minLines = resolveTextAreaLines(props.minLines, 3);
|
|
256
|
+
const maxLines = Math.max(minLines, resolveTextAreaLines(props.maxLines, 3));
|
|
257
|
+
const placeholder = props.placeholder ?? "";
|
|
258
|
+
const focusClass = resolveInputFocusClass(props.outline);
|
|
259
|
+
const defaultClass =
|
|
260
|
+
"padding-sm radius-md flex-grow-1 bg-primary text-default";
|
|
261
|
+
|
|
262
|
+
delete props.outline;
|
|
263
|
+
delete props.focused;
|
|
264
|
+
delete props.minLines;
|
|
265
|
+
delete props.maxLines;
|
|
266
|
+
|
|
267
|
+
useEffect((nextValue) => {
|
|
268
|
+
rawValue.set(nextValue ?? "");
|
|
269
|
+
resizeTextArea(textAreaRef.current, minLines, maxLines);
|
|
270
|
+
}, value);
|
|
271
|
+
|
|
272
|
+
useEffect(() => {
|
|
273
|
+
resizeTextArea(textAreaRef.current, minLines, maxLines);
|
|
274
|
+
}, []);
|
|
275
|
+
|
|
276
|
+
if (focusedValue) {
|
|
277
|
+
useEffect((isFocused) => {
|
|
278
|
+
syncFocusedNode(textAreaRef.current, !!isFocused);
|
|
279
|
+
}, focusedValue);
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
return wrapIntoLabel(
|
|
283
|
+
props,
|
|
284
|
+
textarea({
|
|
285
|
+
...props,
|
|
286
|
+
ref: textAreaRef,
|
|
287
|
+
autofocus: shouldAutoFocus,
|
|
288
|
+
rows: minLines,
|
|
289
|
+
value: rawValue,
|
|
290
|
+
disabled: props.disabled,
|
|
291
|
+
placeholder,
|
|
292
|
+
class: `textarea ${defaultClass} ${focusClass} ${props.class || ""}`.trim(),
|
|
293
|
+
focus: () => {
|
|
294
|
+
if (focusedValue?.set) focusedValue.set(true);
|
|
295
|
+
},
|
|
296
|
+
blur: () => {
|
|
297
|
+
if (focusedValue?.set) focusedValue.set(false);
|
|
298
|
+
},
|
|
299
|
+
keydown: (e) => {
|
|
300
|
+
if (e.key === "Enter" && !shouldInsertTextAreaNewline(e)) {
|
|
301
|
+
e.preventDefault();
|
|
302
|
+
e.target?.form?.requestSubmit?.();
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
props.keydown && props.keydown(e);
|
|
306
|
+
},
|
|
307
|
+
input: (e) => {
|
|
308
|
+
const nextValue = e.target.value ?? "";
|
|
309
|
+
rawValue.set(nextValue);
|
|
310
|
+
value.set(nextValue);
|
|
311
|
+
resizeTextArea(e.target, minLines, maxLines);
|
|
312
|
+
props.input && props.input(e);
|
|
313
|
+
},
|
|
121
314
|
}),
|
|
122
315
|
);
|
|
123
316
|
};
|
|
124
317
|
|
|
318
|
+
const PickerCore = (props, _) => {
|
|
319
|
+
const value =
|
|
320
|
+
props.value?.get && props.value?.set
|
|
321
|
+
? props.value
|
|
322
|
+
: useState(props.value ?? "");
|
|
323
|
+
const optionsValue = resolveCollectionState(props.options ?? props.items, []);
|
|
324
|
+
const disabledValue = resolveBooleanState(props.disabled);
|
|
325
|
+
const labelProps = props.label ? { label: props.label } : {};
|
|
326
|
+
const anchor = props.anchor;
|
|
327
|
+
const focusClass = resolveInputFocusClass(props.outline);
|
|
328
|
+
const defaultClass =
|
|
329
|
+
"padding-sm border-primary radius-md flex-grow-1 bg-primary text-default";
|
|
330
|
+
const pickerState = Compute([value, optionsValue, disabledValue], (selectedKey, resolvedOptions, isDisabled) => {
|
|
331
|
+
const firstSelectableOption = (resolvedOptions ?? []).find(
|
|
332
|
+
(option) => !option.divider && option.key !== undefined && option.key !== null,
|
|
333
|
+
);
|
|
334
|
+
const selectedItem = (resolvedOptions ?? []).find(
|
|
335
|
+
(option) => !option.divider && option.key === selectedKey,
|
|
336
|
+
);
|
|
337
|
+
|
|
338
|
+
const menuOptions = (resolvedOptions ?? []).map((option) => {
|
|
339
|
+
if (option.divider) return option;
|
|
340
|
+
|
|
341
|
+
return {
|
|
342
|
+
...option,
|
|
343
|
+
action: () => {
|
|
344
|
+
value.set(option.key);
|
|
345
|
+
props.input &&
|
|
346
|
+
props.input({
|
|
347
|
+
target: { value: option.key },
|
|
348
|
+
currentTarget: { value: option.key },
|
|
349
|
+
option,
|
|
350
|
+
});
|
|
351
|
+
},
|
|
352
|
+
};
|
|
353
|
+
});
|
|
354
|
+
|
|
355
|
+
return { selectedItem, firstSelectableOption, menuOptions, isDisabled: !!isDisabled };
|
|
356
|
+
});
|
|
357
|
+
|
|
358
|
+
useEffect(({ selectedItem, firstSelectableOption }) => {
|
|
359
|
+
const selectedKey = value.get();
|
|
360
|
+
if (!selectedKey || selectedItem) return;
|
|
361
|
+
if (!firstSelectableOption) {
|
|
362
|
+
value.set("");
|
|
363
|
+
props.input &&
|
|
364
|
+
props.input({
|
|
365
|
+
target: { value: "" },
|
|
366
|
+
currentTarget: { value: "" },
|
|
367
|
+
option: null,
|
|
368
|
+
});
|
|
369
|
+
return;
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
value.set(firstSelectableOption.key);
|
|
373
|
+
props.input &&
|
|
374
|
+
props.input({
|
|
375
|
+
target: { value: firstSelectableOption.key },
|
|
376
|
+
currentTarget: { value: firstSelectableOption.key },
|
|
377
|
+
option: firstSelectableOption,
|
|
378
|
+
});
|
|
379
|
+
}, pickerState);
|
|
380
|
+
|
|
381
|
+
const triggerProps = { ...props };
|
|
382
|
+
delete triggerProps.value;
|
|
383
|
+
delete triggerProps.options;
|
|
384
|
+
delete triggerProps.items;
|
|
385
|
+
delete triggerProps.label;
|
|
386
|
+
delete triggerProps.input;
|
|
387
|
+
delete triggerProps.anchor;
|
|
388
|
+
delete triggerProps.disabled;
|
|
389
|
+
delete triggerProps.outline;
|
|
390
|
+
|
|
391
|
+
return wrapIntoLabel(
|
|
392
|
+
labelProps,
|
|
393
|
+
div(
|
|
394
|
+
{},
|
|
395
|
+
Show(pickerState, ({ selectedItem, menuOptions, isDisabled }) =>
|
|
396
|
+
withExtractedStyles((finalTriggerProps) =>
|
|
397
|
+
Menu({
|
|
398
|
+
...(anchor ? { anchor } : {}),
|
|
399
|
+
items: menuOptions,
|
|
400
|
+
trigger: ({ toggle }) => {
|
|
401
|
+
const triggerColor = finalTriggerProps.style?.color;
|
|
402
|
+
|
|
403
|
+
return button(
|
|
404
|
+
{
|
|
405
|
+
...finalTriggerProps,
|
|
406
|
+
type: "button",
|
|
407
|
+
disabled: disabledValue,
|
|
408
|
+
click: () => {
|
|
409
|
+
if (isDisabled) return;
|
|
410
|
+
toggle();
|
|
411
|
+
},
|
|
412
|
+
class: `picker-trigger ${defaultClass} ${focusClass} ${
|
|
413
|
+
isDisabled ? "picker-trigger-disabled" : ""
|
|
414
|
+
} ${finalTriggerProps.class || ""}`.trim(),
|
|
415
|
+
},
|
|
416
|
+
Row(
|
|
417
|
+
{ fillWidth: true, alignItems: "center", gap: "small" },
|
|
418
|
+
div(
|
|
419
|
+
{ class: "picker-selection" },
|
|
420
|
+
...(selectedItem?.icon
|
|
421
|
+
? [
|
|
422
|
+
Icon({
|
|
423
|
+
name: selectedItem.icon,
|
|
424
|
+
size: 16,
|
|
425
|
+
...(triggerColor ? { color: triggerColor } : {}),
|
|
426
|
+
}),
|
|
427
|
+
]
|
|
428
|
+
: []),
|
|
429
|
+
...(selectedItem
|
|
430
|
+
? [
|
|
431
|
+
Text(
|
|
432
|
+
{
|
|
433
|
+
weight: "heavy",
|
|
434
|
+
...(triggerColor ? { color: triggerColor } : {}),
|
|
435
|
+
},
|
|
436
|
+
selectedItem.text ?? selectedItem.key,
|
|
437
|
+
),
|
|
438
|
+
]
|
|
439
|
+
: []),
|
|
440
|
+
),
|
|
441
|
+
Spacer(),
|
|
442
|
+
Icon({
|
|
443
|
+
name: "chevron_down",
|
|
444
|
+
size: 16,
|
|
445
|
+
...(triggerColor ? { color: triggerColor } : { color: "secondary" }),
|
|
446
|
+
}),
|
|
447
|
+
),
|
|
448
|
+
);
|
|
449
|
+
},
|
|
450
|
+
})
|
|
451
|
+
)({ minHeight: 32, textSize: "1rem", ...triggerProps }),
|
|
452
|
+
),
|
|
453
|
+
),
|
|
454
|
+
);
|
|
455
|
+
};
|
|
456
|
+
|
|
457
|
+
const SegmentedPickerCore = (props, _) => {
|
|
458
|
+
const value =
|
|
459
|
+
props.value?.get && props.value?.set
|
|
460
|
+
? props.value
|
|
461
|
+
: useState(props.value ?? "");
|
|
462
|
+
const itemsValue = resolveCollectionState(props.items, []);
|
|
463
|
+
const focusClass = resolveInputFocusClass(props.outline);
|
|
464
|
+
const segmentedPickerState = Compute(
|
|
465
|
+
[value, itemsValue],
|
|
466
|
+
(selectedKey, resolvedItems) =>
|
|
467
|
+
({
|
|
468
|
+
selectedKey,
|
|
469
|
+
selectedItem: (resolvedItems ?? []).find((item) => item.key === selectedKey) ?? null,
|
|
470
|
+
segments: (resolvedItems ?? []).map((item) => ({
|
|
471
|
+
...item,
|
|
472
|
+
selected: item.key === selectedKey,
|
|
473
|
+
})),
|
|
474
|
+
}),
|
|
475
|
+
);
|
|
476
|
+
const segmentedPickerItems = segmentedPickerState.map((state) => state.segments);
|
|
477
|
+
|
|
478
|
+
useEffect(({ selectedKey, selectedItem }) => {
|
|
479
|
+
if (!selectedKey || selectedItem) return;
|
|
480
|
+
|
|
481
|
+
value.set("");
|
|
482
|
+
|
|
483
|
+
const eventLike = {
|
|
484
|
+
target: { value: "" },
|
|
485
|
+
currentTarget: { value: "" },
|
|
486
|
+
item: null,
|
|
487
|
+
};
|
|
488
|
+
|
|
489
|
+
props.change && props.change(eventLike);
|
|
490
|
+
props.input && props.input(eventLike);
|
|
491
|
+
}, segmentedPickerState);
|
|
492
|
+
|
|
493
|
+
delete props.outline;
|
|
494
|
+
delete props.items;
|
|
495
|
+
|
|
496
|
+
return wrapIntoLabel(
|
|
497
|
+
props,
|
|
498
|
+
div(
|
|
499
|
+
{
|
|
500
|
+
class: `segmented-picker border-primary bg-primary-dimmed radius-lg ${
|
|
501
|
+
props.disabled ? "segmented-picker-disabled" : ""
|
|
502
|
+
} ${focusClass} ${props.class || ""}`.trim(),
|
|
503
|
+
},
|
|
504
|
+
ForEach(segmentedPickerItems, "key", (item) =>
|
|
505
|
+
button(
|
|
506
|
+
{
|
|
507
|
+
type: "button",
|
|
508
|
+
disabled: !!props.disabled,
|
|
509
|
+
click: () => {
|
|
510
|
+
if (props.disabled || item.selected) return;
|
|
511
|
+
|
|
512
|
+
value.set(item.key);
|
|
513
|
+
|
|
514
|
+
const eventLike = {
|
|
515
|
+
target: { value: item.key },
|
|
516
|
+
currentTarget: { value: item.key },
|
|
517
|
+
item,
|
|
518
|
+
};
|
|
519
|
+
|
|
520
|
+
props.change && props.change(eventLike);
|
|
521
|
+
props.input && props.input(eventLike);
|
|
522
|
+
},
|
|
523
|
+
class: `segmented-picker-segment ${
|
|
524
|
+
item.selected ? "segmented-picker-segment-selected" : ""
|
|
525
|
+
}`,
|
|
526
|
+
},
|
|
527
|
+
Row(
|
|
528
|
+
{
|
|
529
|
+
class: "segmented-picker-segment-content",
|
|
530
|
+
alignItems: "center",
|
|
531
|
+
justifyContent: "center",
|
|
532
|
+
gap: item.icon ? "small" : 0,
|
|
533
|
+
width: "100%",
|
|
534
|
+
},
|
|
535
|
+
...(item.icon ? [Icon({ name: item.icon, size: 16 })] : []),
|
|
536
|
+
Text({ weight: "heavy" }, item.text),
|
|
537
|
+
),
|
|
538
|
+
),
|
|
539
|
+
),
|
|
540
|
+
),
|
|
541
|
+
);
|
|
542
|
+
};
|
|
543
|
+
|
|
125
544
|
/** Select core component and logic */
|
|
126
545
|
const SelectCore = (props, _) => {
|
|
127
546
|
let value =
|
|
128
547
|
props.value?.get && props.value?.set
|
|
129
548
|
? props.value
|
|
130
549
|
: useState(props.value ?? "");
|
|
131
|
-
let
|
|
132
|
-
let
|
|
550
|
+
let optionsValue = resolveCollectionState(props.options, []);
|
|
551
|
+
let focusClass = resolveInputFocusClass(props.outline);
|
|
552
|
+
const selectedOptionState = Compute([value, optionsValue], (selectedKey, resolvedOptions) => {
|
|
553
|
+
const options = resolvedOptions ?? [];
|
|
554
|
+
const selectedOption = options.find((option) => option.key === selectedKey);
|
|
555
|
+
return { selectedKey, selectedOption };
|
|
556
|
+
});
|
|
557
|
+
|
|
558
|
+
useEffect(({ selectedKey, selectedOption }) => {
|
|
559
|
+
if (!selectedKey || selectedOption) return;
|
|
560
|
+
|
|
561
|
+
value.set("");
|
|
562
|
+
props.input &&
|
|
563
|
+
props.input({
|
|
564
|
+
target: { value: "" },
|
|
565
|
+
currentTarget: { value: "" },
|
|
566
|
+
option: null,
|
|
567
|
+
});
|
|
568
|
+
}, selectedOptionState);
|
|
133
569
|
|
|
134
570
|
delete props.options;
|
|
135
571
|
delete props.outline;
|
|
136
572
|
|
|
137
|
-
|
|
138
|
-
|
|
573
|
+
// Use ForEach for reactive option rendering - updates when value changes
|
|
574
|
+
let childrenOptions = ForEach(optionsValue, "key", (o, index) =>
|
|
575
|
+
option(
|
|
139
576
|
{
|
|
140
577
|
value: o.key ?? `option ${index + 1}`,
|
|
141
578
|
selected: o.key === value.get(),
|
|
142
579
|
},
|
|
143
580
|
o.content ?? ``,
|
|
144
|
-
)
|
|
145
|
-
|
|
581
|
+
),
|
|
582
|
+
);
|
|
146
583
|
|
|
147
|
-
let defaultClass = `appearance-none padding-sm bg-primary border-primary radius-md flex-grow-1
|
|
584
|
+
let defaultClass = `appearance-none padding-sm bg-primary border-primary radius-md flex-grow-1 ${focusClass}`;
|
|
148
585
|
|
|
149
586
|
return wrapIntoLabel(
|
|
150
587
|
props,
|
|
151
588
|
select(
|
|
152
589
|
{
|
|
153
590
|
...props,
|
|
591
|
+
value: value,
|
|
154
592
|
input: (e) => {
|
|
155
593
|
value.set(e.target.value ?? "");
|
|
156
594
|
props.input && props.input(e);
|
|
@@ -165,14 +603,36 @@ const SelectCore = (props, _) => {
|
|
|
165
603
|
/** Checkbox core component and logic */
|
|
166
604
|
const CheckBoxCore = (props, _) => {
|
|
167
605
|
let checkedValue = "checked" in props ? props.checked : props.value;
|
|
168
|
-
let checked =
|
|
169
|
-
|
|
170
|
-
? checkedValue
|
|
171
|
-
: useState(!!checkedValue);
|
|
172
|
-
let outlineClass = props.outline ? "focus-outline-dimmed" : "no-outline";
|
|
606
|
+
let checked = resolveBooleanState(checkedValue);
|
|
607
|
+
let focusClass = resolveInputFocusClass(props.outline);
|
|
173
608
|
let defaultClass =
|
|
174
|
-
"cursor-pointer border-primary radius-md
|
|
609
|
+
"cursor-pointer border-primary radius-md bg-primary";
|
|
610
|
+
delete props.outline;
|
|
611
|
+
delete props.checked;
|
|
612
|
+
delete props.value;
|
|
175
613
|
|
|
614
|
+
return wrapCheckBoxIntoLabel(
|
|
615
|
+
props,
|
|
616
|
+
input({
|
|
617
|
+
...props,
|
|
618
|
+
type: "checkbox",
|
|
619
|
+
checked: !!checked.get(),
|
|
620
|
+
change: (e) => {
|
|
621
|
+
checked.set(!!e.target.checked);
|
|
622
|
+
props.change && props.change(e);
|
|
623
|
+
props.input && props.input(e);
|
|
624
|
+
},
|
|
625
|
+
class: `checkbox ${defaultClass} ${focusClass} ${props.class || ""}`,
|
|
626
|
+
}),
|
|
627
|
+
);
|
|
628
|
+
};
|
|
629
|
+
|
|
630
|
+
const SwitchCore = (props, _) => {
|
|
631
|
+
let checkedValue = "checked" in props ? props.checked : props.value;
|
|
632
|
+
let checked = resolveBooleanState(checkedValue);
|
|
633
|
+
let focusClass = resolveInputFocusClass(props.outline);
|
|
634
|
+
let defaultClass =
|
|
635
|
+
"cursor-pointer border-primary bg-primary";
|
|
176
636
|
delete props.outline;
|
|
177
637
|
delete props.checked;
|
|
178
638
|
delete props.value;
|
|
@@ -182,17 +642,150 @@ const CheckBoxCore = (props, _) => {
|
|
|
182
642
|
input({
|
|
183
643
|
...props,
|
|
184
644
|
type: "checkbox",
|
|
645
|
+
role: "switch",
|
|
185
646
|
checked: !!checked.get(),
|
|
186
647
|
change: (e) => {
|
|
187
648
|
checked.set(!!e.target.checked);
|
|
188
649
|
props.change && props.change(e);
|
|
189
650
|
props.input && props.input(e);
|
|
190
651
|
},
|
|
191
|
-
class: `
|
|
652
|
+
class: `switch ${defaultClass} ${focusClass} ${props.class || ""}`.trim(),
|
|
192
653
|
}),
|
|
193
654
|
);
|
|
194
655
|
};
|
|
195
656
|
|
|
657
|
+
const SliderCore = (props, _) => {
|
|
658
|
+
const focusClass = resolveInputFocusClass(props.outline);
|
|
659
|
+
const stepsValue = resolveCollectionState(props.steps, []);
|
|
660
|
+
const customStepsState = Compute(stepsValue, (steps) =>
|
|
661
|
+
isValidSliderSteps(steps) ? steps : null,
|
|
662
|
+
);
|
|
663
|
+
const minState = resolveNumericState(props.min, 0);
|
|
664
|
+
const maxState = resolveNumericState(props.max, 100);
|
|
665
|
+
const stepState = resolveNumericState(props.step, 1);
|
|
666
|
+
const initialCustomSteps = customStepsState.get();
|
|
667
|
+
const initialValue = initialCustomSteps
|
|
668
|
+
? initialCustomSteps[0].value
|
|
669
|
+
: toSliderNumber(props.value, toSliderNumber(minState.get(), 0));
|
|
670
|
+
const valueState = resolveNumericState(props.value, initialValue);
|
|
671
|
+
const sliderValue = useState(
|
|
672
|
+
initialCustomSteps
|
|
673
|
+
? findNearestSliderStepIndex(initialCustomSteps, valueState.get())
|
|
674
|
+
: toSliderNumber(valueState.get(), toSliderNumber(minState.get(), 0)),
|
|
675
|
+
);
|
|
676
|
+
const sliderBoundsState = Compute(
|
|
677
|
+
[customStepsState, minState, maxState, stepState],
|
|
678
|
+
(steps, min, max, step) => ({
|
|
679
|
+
min: steps ? 0 : min,
|
|
680
|
+
max: steps ? steps.length - 1 : max,
|
|
681
|
+
step: steps ? 1 : step,
|
|
682
|
+
}),
|
|
683
|
+
);
|
|
684
|
+
const sliderStepLabels = Compute(customStepsState, (steps) =>
|
|
685
|
+
steps && hasSliderStepLabels(steps)
|
|
686
|
+
? steps.map((step, index) => ({
|
|
687
|
+
...step,
|
|
688
|
+
position: steps.length === 1 ? 0 : (index / (steps.length - 1)) * 100,
|
|
689
|
+
alignment:
|
|
690
|
+
index === 0
|
|
691
|
+
? "slider-step-label-start"
|
|
692
|
+
: index === steps.length - 1
|
|
693
|
+
? "slider-step-label-end"
|
|
694
|
+
: "slider-step-label-center",
|
|
695
|
+
}))
|
|
696
|
+
: [],
|
|
697
|
+
);
|
|
698
|
+
const shouldRenderLabels = sliderStepLabels.map((steps) => steps.length > 0);
|
|
699
|
+
|
|
700
|
+
delete props.outline;
|
|
701
|
+
delete props.min;
|
|
702
|
+
delete props.max;
|
|
703
|
+
delete props.step;
|
|
704
|
+
delete props.steps;
|
|
705
|
+
delete props.value;
|
|
706
|
+
|
|
707
|
+
useEffect((nextValue) => {
|
|
708
|
+
const customSteps = customStepsState.get();
|
|
709
|
+
if (customSteps) {
|
|
710
|
+
sliderValue.set(findNearestSliderStepIndex(customSteps, nextValue));
|
|
711
|
+
return;
|
|
712
|
+
}
|
|
713
|
+
|
|
714
|
+
sliderValue.set(toSliderNumber(nextValue, toSliderNumber(minState.get(), 0)));
|
|
715
|
+
}, valueState);
|
|
716
|
+
|
|
717
|
+
useEffect((customSteps) => {
|
|
718
|
+
const currentValue = toSliderNumber(valueState.get(), toSliderNumber(minState.get(), 0));
|
|
719
|
+
|
|
720
|
+
if (customSteps) {
|
|
721
|
+
const nextStep = customSteps.find((step) => step.value === currentValue) ?? customSteps[0];
|
|
722
|
+
sliderValue.set(findNearestSliderStepIndex(customSteps, nextStep.value));
|
|
723
|
+
|
|
724
|
+
if (nextStep.value !== currentValue) {
|
|
725
|
+
valueState.set(nextStep.value);
|
|
726
|
+
props.input &&
|
|
727
|
+
props.input({
|
|
728
|
+
target: { value: nextStep.value },
|
|
729
|
+
currentTarget: { value: nextStep.value },
|
|
730
|
+
step: nextStep,
|
|
731
|
+
});
|
|
732
|
+
}
|
|
733
|
+
return;
|
|
734
|
+
}
|
|
735
|
+
|
|
736
|
+
sliderValue.set(currentValue);
|
|
737
|
+
}, customStepsState);
|
|
738
|
+
|
|
739
|
+
return wrapIntoLabel(
|
|
740
|
+
props,
|
|
741
|
+
Column(
|
|
742
|
+
{ gap: "small", class: "slider-field" },
|
|
743
|
+
input({
|
|
744
|
+
...props,
|
|
745
|
+
type: "range",
|
|
746
|
+
min: sliderBoundsState.map((bounds) => bounds.min),
|
|
747
|
+
max: sliderBoundsState.map((bounds) => bounds.max),
|
|
748
|
+
step: sliderBoundsState.map((bounds) => bounds.step),
|
|
749
|
+
value: sliderValue,
|
|
750
|
+
disabled: props.disabled,
|
|
751
|
+
class: `slider ${focusClass} ${props.class || ""}`.trim(),
|
|
752
|
+
input: (e) => {
|
|
753
|
+
const rawValue = toSliderNumber(e.target.value, sliderValue.get());
|
|
754
|
+
const customSteps = customStepsState.get();
|
|
755
|
+
const nextValue = customSteps
|
|
756
|
+
? getSliderStepValue(customSteps, rawValue)
|
|
757
|
+
: rawValue;
|
|
758
|
+
|
|
759
|
+
sliderValue.set(customSteps ? findNearestSliderStepIndex(customSteps, nextValue) : nextValue);
|
|
760
|
+
|
|
761
|
+
if (typeof valueState.set === "function") valueState.set(nextValue);
|
|
762
|
+
props.input && props.input(e);
|
|
763
|
+
},
|
|
764
|
+
}),
|
|
765
|
+
Show(shouldRenderLabels, () =>
|
|
766
|
+
div(
|
|
767
|
+
{
|
|
768
|
+
class: "slider-step-labels",
|
|
769
|
+
},
|
|
770
|
+
ForEach(sliderStepLabels, "value", (step) =>
|
|
771
|
+
div(
|
|
772
|
+
{
|
|
773
|
+
class: `slider-step-label ${step.alignment}`,
|
|
774
|
+
style: {
|
|
775
|
+
left: `${step.position}%`,
|
|
776
|
+
},
|
|
777
|
+
},
|
|
778
|
+
step.label
|
|
779
|
+
? Text({ weight: "heavy", color: "secondary" }, step.label)
|
|
780
|
+
: "",
|
|
781
|
+
),
|
|
782
|
+
),
|
|
783
|
+
),
|
|
784
|
+
),
|
|
785
|
+
),
|
|
786
|
+
);
|
|
787
|
+
};
|
|
788
|
+
|
|
196
789
|
// Exports
|
|
197
790
|
|
|
198
791
|
/**
|
|
@@ -217,6 +810,69 @@ export const TextInput = withNormalizedArgs((props, ...children) =>
|
|
|
217
810
|
)({ minHeight: 32, textSize: "1rem", ...props }, ...children),
|
|
218
811
|
);
|
|
219
812
|
|
|
813
|
+
/**
|
|
814
|
+
* Multiline text input with optional auto-growing height.
|
|
815
|
+
*
|
|
816
|
+
* @param {Object} props - Component props
|
|
817
|
+
* @param {Object|string|number} [props.value] - Textarea value (useState object or string)
|
|
818
|
+
* @param {string} [props.placeholder] - Placeholder text
|
|
819
|
+
* @param {string} [props.label] - Label text (wraps in Column with Heading)
|
|
820
|
+
* @param {boolean} [props.outline] - Show focus outline (default: false)
|
|
821
|
+
* @param {boolean} [props.disabled] - Disabled state
|
|
822
|
+
* @param {number} [props.minLines=3] - Minimum visible lines
|
|
823
|
+
* @param {number} [props.maxLines=3] - Maximum visible lines before scrolling
|
|
824
|
+
* Shift+Enter inserts a newline. Enter submits the parent form when available.
|
|
825
|
+
* @param {Function} [props.input] - Input event handler
|
|
826
|
+
* @param {string} [props.class] - Additional CSS classes
|
|
827
|
+
* @param {...*} children - Children elements (ignored)
|
|
828
|
+
* @returns {*} TextArea component
|
|
829
|
+
*/
|
|
830
|
+
export const TextArea = withNormalizedArgs((props, ...children) =>
|
|
831
|
+
withExtractedStyles((finalProps, ...children) =>
|
|
832
|
+
TextAreaCore(finalProps, ...children),
|
|
833
|
+
)({ textSize: "1rem", border: "primary", ...props }, ...children),
|
|
834
|
+
);
|
|
835
|
+
|
|
836
|
+
/**
|
|
837
|
+
* Menu-backed selector with input-like trigger rendering.
|
|
838
|
+
*
|
|
839
|
+
* @param {Object} props - Component props
|
|
840
|
+
* @param {Object|string} [props.value] - Selected item key (useState object or string)
|
|
841
|
+
* @param {Array<{key: string, text?: string, icon?: string, action?: Function, divider?: boolean}>} [props.options] - Menu-style options array
|
|
842
|
+
* @param {string} [props.label] - Label text (wraps in Column with Heading)
|
|
843
|
+
* @param {boolean} [props.outline] - Show focus outline
|
|
844
|
+
* @param {boolean} [props.disabled] - Disabled state
|
|
845
|
+
* @param {string} [props.anchor="bottom-left"] - Menu anchor position
|
|
846
|
+
* @param {Function} [props.input] - Called with an event-like object after selection
|
|
847
|
+
* @param {string} [props.class] - Additional CSS classes
|
|
848
|
+
* @param {...*} children - Children elements (ignored)
|
|
849
|
+
* @returns {*} Picker component
|
|
850
|
+
*/
|
|
851
|
+
export const Picker = withNormalizedArgs((props, ...children) =>
|
|
852
|
+
PickerCore(props, ...children),
|
|
853
|
+
);
|
|
854
|
+
|
|
855
|
+
/**
|
|
856
|
+
* iOS-style segmented picker with single keyed selection.
|
|
857
|
+
*
|
|
858
|
+
* @param {Object} props - Component props
|
|
859
|
+
* @param {Object|string} [props.value] - Selected segment key (useState object or string)
|
|
860
|
+
* @param {Array<{key: string, text: string, icon?: string}>} [props.items] - Segmented picker items
|
|
861
|
+
* @param {string} [props.label] - Label text (wraps in Column with Heading)
|
|
862
|
+
* @param {boolean} [props.outline] - Show focus outline
|
|
863
|
+
* @param {boolean} [props.disabled] - Disabled state
|
|
864
|
+
* @param {Function} [props.change] - Called with an event-like object after selection
|
|
865
|
+
* @param {Function} [props.input] - Called with an event-like object after selection
|
|
866
|
+
* @param {string} [props.class] - Additional CSS classes
|
|
867
|
+
* @param {...*} children - Children elements (ignored)
|
|
868
|
+
* @returns {*} SegmentedPicker component
|
|
869
|
+
*/
|
|
870
|
+
export const SegmentedPicker = withNormalizedArgs((props, ...children) =>
|
|
871
|
+
withExtractedStyles((finalProps, ...children) =>
|
|
872
|
+
SegmentedPickerCore(finalProps, ...children),
|
|
873
|
+
)({ textSize: "1rem", ...props }, ...children),
|
|
874
|
+
);
|
|
875
|
+
|
|
220
876
|
/**
|
|
221
877
|
* Dropdown select input with options.
|
|
222
878
|
*
|
|
@@ -257,3 +913,47 @@ export const CheckBox = withNormalizedArgs((props, ...children) =>
|
|
|
257
913
|
CheckBoxCore(finalProps, ...children),
|
|
258
914
|
)({ textSize: "1rem", ...props }, ...children),
|
|
259
915
|
);
|
|
916
|
+
|
|
917
|
+
/**
|
|
918
|
+
* OS-style switch toggle with state binding.
|
|
919
|
+
*
|
|
920
|
+
* @param {Object} props - Component props
|
|
921
|
+
* @param {Object|boolean} [props.checked] - Checked state (useState object or boolean)
|
|
922
|
+
* @param {Object|boolean} [props.value] - Alias for checked state
|
|
923
|
+
* @param {boolean} [props.outline] - Show focus outline
|
|
924
|
+
* @param {string} [props.label] - Label text (wraps in Row with Heading)
|
|
925
|
+
* @param {boolean} [props.disabled] - Disabled state
|
|
926
|
+
* @param {Function} [props.change] - Change event handler
|
|
927
|
+
* @param {Function} [props.input] - Input event handler
|
|
928
|
+
* @param {string} [props.class] - Additional CSS classes
|
|
929
|
+
* @param {...*} children - Children elements (ignored)
|
|
930
|
+
* @returns {*} Switch component
|
|
931
|
+
*/
|
|
932
|
+
export const Switch = withNormalizedArgs((props, ...children) =>
|
|
933
|
+
withExtractedStyles((finalProps, ...children) =>
|
|
934
|
+
SwitchCore(finalProps, ...children),
|
|
935
|
+
)({ textSize: "1rem", ...props }, ...children),
|
|
936
|
+
);
|
|
937
|
+
|
|
938
|
+
/**
|
|
939
|
+
* Range slider input with optional discrete step mapping.
|
|
940
|
+
*
|
|
941
|
+
* @param {Object} props - Component props
|
|
942
|
+
* @param {Object|number} [props.value] - Slider value (useState object or number)
|
|
943
|
+
* @param {Object|number} [props.min=0] - Minimum numeric value in native mode
|
|
944
|
+
* @param {Object|number} [props.max=100] - Maximum numeric value in native mode
|
|
945
|
+
* @param {Object|number} [props.step=1] - Step increment in native mode
|
|
946
|
+
* @param {Array<{value: number, label?: string}>} [props.steps] - Discrete step configuration; ignores min/max/step when valid
|
|
947
|
+
* @param {string} [props.label] - Label text (wraps in Column with Heading)
|
|
948
|
+
* @param {boolean} [props.outline] - Show focus outline
|
|
949
|
+
* @param {boolean} [props.disabled] - Disabled state
|
|
950
|
+
* @param {Function} [props.input] - Input event handler
|
|
951
|
+
* @param {string} [props.class] - Additional CSS classes
|
|
952
|
+
* @param {...*} children - Children elements (ignored)
|
|
953
|
+
* @returns {*} Slider component
|
|
954
|
+
*/
|
|
955
|
+
export const Slider = withNormalizedArgs((props, ...children) =>
|
|
956
|
+
withExtractedStyles((finalProps, ...children) =>
|
|
957
|
+
SliderCore(finalProps, ...children),
|
|
958
|
+
)({ fillWidth: true, ...props }, ...children),
|
|
959
|
+
);
|