@bunnix/components 0.10.3 → 0.11.0
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 +16 -2
- 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 +702 -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,352 @@ 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 || ""}`,
|
|
121
242
|
}),
|
|
122
243
|
);
|
|
123
244
|
};
|
|
124
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
|
+
},
|
|
314
|
+
}),
|
|
315
|
+
);
|
|
316
|
+
};
|
|
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
|
+
button(
|
|
402
|
+
{
|
|
403
|
+
...finalTriggerProps,
|
|
404
|
+
type: "button",
|
|
405
|
+
disabled: disabledValue,
|
|
406
|
+
click: () => {
|
|
407
|
+
if (isDisabled) return;
|
|
408
|
+
toggle();
|
|
409
|
+
},
|
|
410
|
+
class: `picker-trigger ${defaultClass} ${focusClass} ${
|
|
411
|
+
isDisabled ? "picker-trigger-disabled" : ""
|
|
412
|
+
} ${finalTriggerProps.class || ""}`.trim(),
|
|
413
|
+
},
|
|
414
|
+
Row(
|
|
415
|
+
{ fillWidth: true, alignItems: "center", gap: "small" },
|
|
416
|
+
div(
|
|
417
|
+
{ class: "picker-selection" },
|
|
418
|
+
...(selectedItem?.icon
|
|
419
|
+
? [Icon({ name: selectedItem.icon, size: 16 })]
|
|
420
|
+
: []),
|
|
421
|
+
...(selectedItem
|
|
422
|
+
? [Text({ weight: "heavy" }, selectedItem.text ?? selectedItem.key)]
|
|
423
|
+
: []),
|
|
424
|
+
),
|
|
425
|
+
Spacer(),
|
|
426
|
+
Icon({ name: "chevron_down", size: 16, color: "secondary" }),
|
|
427
|
+
),
|
|
428
|
+
),
|
|
429
|
+
})
|
|
430
|
+
)({ minHeight: 32, textSize: "1rem", ...triggerProps }),
|
|
431
|
+
),
|
|
432
|
+
),
|
|
433
|
+
);
|
|
434
|
+
};
|
|
435
|
+
|
|
436
|
+
const SegmentedPickerCore = (props, _) => {
|
|
437
|
+
const value =
|
|
438
|
+
props.value?.get && props.value?.set
|
|
439
|
+
? props.value
|
|
440
|
+
: useState(props.value ?? "");
|
|
441
|
+
const itemsValue = resolveCollectionState(props.items, []);
|
|
442
|
+
const focusClass = resolveInputFocusClass(props.outline);
|
|
443
|
+
const segmentedPickerState = Compute(
|
|
444
|
+
[value, itemsValue],
|
|
445
|
+
(selectedKey, resolvedItems) =>
|
|
446
|
+
({
|
|
447
|
+
selectedKey,
|
|
448
|
+
selectedItem: (resolvedItems ?? []).find((item) => item.key === selectedKey) ?? null,
|
|
449
|
+
segments: (resolvedItems ?? []).map((item) => ({
|
|
450
|
+
...item,
|
|
451
|
+
selected: item.key === selectedKey,
|
|
452
|
+
})),
|
|
453
|
+
}),
|
|
454
|
+
);
|
|
455
|
+
const segmentedPickerItems = segmentedPickerState.map((state) => state.segments);
|
|
456
|
+
|
|
457
|
+
useEffect(({ selectedKey, selectedItem }) => {
|
|
458
|
+
if (!selectedKey || selectedItem) return;
|
|
459
|
+
|
|
460
|
+
value.set("");
|
|
461
|
+
|
|
462
|
+
const eventLike = {
|
|
463
|
+
target: { value: "" },
|
|
464
|
+
currentTarget: { value: "" },
|
|
465
|
+
item: null,
|
|
466
|
+
};
|
|
467
|
+
|
|
468
|
+
props.change && props.change(eventLike);
|
|
469
|
+
props.input && props.input(eventLike);
|
|
470
|
+
}, segmentedPickerState);
|
|
471
|
+
|
|
472
|
+
delete props.outline;
|
|
473
|
+
delete props.items;
|
|
474
|
+
|
|
475
|
+
return wrapIntoLabel(
|
|
476
|
+
props,
|
|
477
|
+
div(
|
|
478
|
+
{
|
|
479
|
+
class: `segmented-picker border-primary bg-primary-dimmed radius-lg ${
|
|
480
|
+
props.disabled ? "segmented-picker-disabled" : ""
|
|
481
|
+
} ${focusClass} ${props.class || ""}`.trim(),
|
|
482
|
+
},
|
|
483
|
+
ForEach(segmentedPickerItems, "key", (item) =>
|
|
484
|
+
button(
|
|
485
|
+
{
|
|
486
|
+
type: "button",
|
|
487
|
+
disabled: !!props.disabled,
|
|
488
|
+
click: () => {
|
|
489
|
+
if (props.disabled || item.selected) return;
|
|
490
|
+
|
|
491
|
+
value.set(item.key);
|
|
492
|
+
|
|
493
|
+
const eventLike = {
|
|
494
|
+
target: { value: item.key },
|
|
495
|
+
currentTarget: { value: item.key },
|
|
496
|
+
item,
|
|
497
|
+
};
|
|
498
|
+
|
|
499
|
+
props.change && props.change(eventLike);
|
|
500
|
+
props.input && props.input(eventLike);
|
|
501
|
+
},
|
|
502
|
+
class: `segmented-picker-segment ${
|
|
503
|
+
item.selected ? "segmented-picker-segment-selected" : ""
|
|
504
|
+
}`,
|
|
505
|
+
},
|
|
506
|
+
Row(
|
|
507
|
+
{
|
|
508
|
+
class: "segmented-picker-segment-content",
|
|
509
|
+
alignItems: "center",
|
|
510
|
+
justifyContent: "center",
|
|
511
|
+
gap: item.icon ? "small" : 0,
|
|
512
|
+
width: "100%",
|
|
513
|
+
},
|
|
514
|
+
...(item.icon ? [Icon({ name: item.icon, size: 16 })] : []),
|
|
515
|
+
Text({ weight: "heavy" }, item.text),
|
|
516
|
+
),
|
|
517
|
+
),
|
|
518
|
+
),
|
|
519
|
+
),
|
|
520
|
+
);
|
|
521
|
+
};
|
|
522
|
+
|
|
125
523
|
/** Select core component and logic */
|
|
126
524
|
const SelectCore = (props, _) => {
|
|
127
525
|
let value =
|
|
128
526
|
props.value?.get && props.value?.set
|
|
129
527
|
? props.value
|
|
130
528
|
: useState(props.value ?? "");
|
|
131
|
-
let
|
|
132
|
-
let
|
|
529
|
+
let optionsValue = resolveCollectionState(props.options, []);
|
|
530
|
+
let focusClass = resolveInputFocusClass(props.outline);
|
|
531
|
+
const selectedOptionState = Compute([value, optionsValue], (selectedKey, resolvedOptions) => {
|
|
532
|
+
const options = resolvedOptions ?? [];
|
|
533
|
+
const selectedOption = options.find((option) => option.key === selectedKey);
|
|
534
|
+
return { selectedKey, selectedOption };
|
|
535
|
+
});
|
|
536
|
+
|
|
537
|
+
useEffect(({ selectedKey, selectedOption }) => {
|
|
538
|
+
if (!selectedKey || selectedOption) return;
|
|
539
|
+
|
|
540
|
+
value.set("");
|
|
541
|
+
props.input &&
|
|
542
|
+
props.input({
|
|
543
|
+
target: { value: "" },
|
|
544
|
+
currentTarget: { value: "" },
|
|
545
|
+
option: null,
|
|
546
|
+
});
|
|
547
|
+
}, selectedOptionState);
|
|
133
548
|
|
|
134
549
|
delete props.options;
|
|
135
550
|
delete props.outline;
|
|
136
551
|
|
|
137
|
-
|
|
138
|
-
|
|
552
|
+
// Use ForEach for reactive option rendering - updates when value changes
|
|
553
|
+
let childrenOptions = ForEach(optionsValue, "key", (o, index) =>
|
|
554
|
+
option(
|
|
139
555
|
{
|
|
140
556
|
value: o.key ?? `option ${index + 1}`,
|
|
141
557
|
selected: o.key === value.get(),
|
|
142
558
|
},
|
|
143
559
|
o.content ?? ``,
|
|
144
|
-
)
|
|
145
|
-
|
|
560
|
+
),
|
|
561
|
+
);
|
|
146
562
|
|
|
147
|
-
let defaultClass = `appearance-none padding-sm bg-primary border-primary radius-md flex-grow-1
|
|
563
|
+
let defaultClass = `appearance-none padding-sm bg-primary border-primary radius-md flex-grow-1 ${focusClass}`;
|
|
148
564
|
|
|
149
565
|
return wrapIntoLabel(
|
|
150
566
|
props,
|
|
151
567
|
select(
|
|
152
568
|
{
|
|
153
569
|
...props,
|
|
570
|
+
value: value,
|
|
154
571
|
input: (e) => {
|
|
155
572
|
value.set(e.target.value ?? "");
|
|
156
573
|
props.input && props.input(e);
|
|
@@ -165,14 +582,36 @@ const SelectCore = (props, _) => {
|
|
|
165
582
|
/** Checkbox core component and logic */
|
|
166
583
|
const CheckBoxCore = (props, _) => {
|
|
167
584
|
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";
|
|
585
|
+
let checked = resolveBooleanState(checkedValue);
|
|
586
|
+
let focusClass = resolveInputFocusClass(props.outline);
|
|
173
587
|
let defaultClass =
|
|
174
|
-
"cursor-pointer border-primary radius-md
|
|
588
|
+
"cursor-pointer border-primary radius-md bg-primary";
|
|
589
|
+
delete props.outline;
|
|
590
|
+
delete props.checked;
|
|
591
|
+
delete props.value;
|
|
592
|
+
|
|
593
|
+
return wrapCheckBoxIntoLabel(
|
|
594
|
+
props,
|
|
595
|
+
input({
|
|
596
|
+
...props,
|
|
597
|
+
type: "checkbox",
|
|
598
|
+
checked: !!checked.get(),
|
|
599
|
+
change: (e) => {
|
|
600
|
+
checked.set(!!e.target.checked);
|
|
601
|
+
props.change && props.change(e);
|
|
602
|
+
props.input && props.input(e);
|
|
603
|
+
},
|
|
604
|
+
class: `checkbox ${defaultClass} ${focusClass} ${props.class || ""}`,
|
|
605
|
+
}),
|
|
606
|
+
);
|
|
607
|
+
};
|
|
175
608
|
|
|
609
|
+
const SwitchCore = (props, _) => {
|
|
610
|
+
let checkedValue = "checked" in props ? props.checked : props.value;
|
|
611
|
+
let checked = resolveBooleanState(checkedValue);
|
|
612
|
+
let focusClass = resolveInputFocusClass(props.outline);
|
|
613
|
+
let defaultClass =
|
|
614
|
+
"cursor-pointer border-primary bg-primary";
|
|
176
615
|
delete props.outline;
|
|
177
616
|
delete props.checked;
|
|
178
617
|
delete props.value;
|
|
@@ -182,17 +621,150 @@ const CheckBoxCore = (props, _) => {
|
|
|
182
621
|
input({
|
|
183
622
|
...props,
|
|
184
623
|
type: "checkbox",
|
|
624
|
+
role: "switch",
|
|
185
625
|
checked: !!checked.get(),
|
|
186
626
|
change: (e) => {
|
|
187
627
|
checked.set(!!e.target.checked);
|
|
188
628
|
props.change && props.change(e);
|
|
189
629
|
props.input && props.input(e);
|
|
190
630
|
},
|
|
191
|
-
class: `
|
|
631
|
+
class: `switch ${defaultClass} ${focusClass} ${props.class || ""}`.trim(),
|
|
192
632
|
}),
|
|
193
633
|
);
|
|
194
634
|
};
|
|
195
635
|
|
|
636
|
+
const SliderCore = (props, _) => {
|
|
637
|
+
const focusClass = resolveInputFocusClass(props.outline);
|
|
638
|
+
const stepsValue = resolveCollectionState(props.steps, []);
|
|
639
|
+
const customStepsState = Compute(stepsValue, (steps) =>
|
|
640
|
+
isValidSliderSteps(steps) ? steps : null,
|
|
641
|
+
);
|
|
642
|
+
const minState = resolveNumericState(props.min, 0);
|
|
643
|
+
const maxState = resolveNumericState(props.max, 100);
|
|
644
|
+
const stepState = resolveNumericState(props.step, 1);
|
|
645
|
+
const initialCustomSteps = customStepsState.get();
|
|
646
|
+
const initialValue = initialCustomSteps
|
|
647
|
+
? initialCustomSteps[0].value
|
|
648
|
+
: toSliderNumber(props.value, toSliderNumber(minState.get(), 0));
|
|
649
|
+
const valueState = resolveNumericState(props.value, initialValue);
|
|
650
|
+
const sliderValue = useState(
|
|
651
|
+
initialCustomSteps
|
|
652
|
+
? findNearestSliderStepIndex(initialCustomSteps, valueState.get())
|
|
653
|
+
: toSliderNumber(valueState.get(), toSliderNumber(minState.get(), 0)),
|
|
654
|
+
);
|
|
655
|
+
const sliderBoundsState = Compute(
|
|
656
|
+
[customStepsState, minState, maxState, stepState],
|
|
657
|
+
(steps, min, max, step) => ({
|
|
658
|
+
min: steps ? 0 : min,
|
|
659
|
+
max: steps ? steps.length - 1 : max,
|
|
660
|
+
step: steps ? 1 : step,
|
|
661
|
+
}),
|
|
662
|
+
);
|
|
663
|
+
const sliderStepLabels = Compute(customStepsState, (steps) =>
|
|
664
|
+
steps && hasSliderStepLabels(steps)
|
|
665
|
+
? steps.map((step, index) => ({
|
|
666
|
+
...step,
|
|
667
|
+
position: steps.length === 1 ? 0 : (index / (steps.length - 1)) * 100,
|
|
668
|
+
alignment:
|
|
669
|
+
index === 0
|
|
670
|
+
? "slider-step-label-start"
|
|
671
|
+
: index === steps.length - 1
|
|
672
|
+
? "slider-step-label-end"
|
|
673
|
+
: "slider-step-label-center",
|
|
674
|
+
}))
|
|
675
|
+
: [],
|
|
676
|
+
);
|
|
677
|
+
const shouldRenderLabels = sliderStepLabels.map((steps) => steps.length > 0);
|
|
678
|
+
|
|
679
|
+
delete props.outline;
|
|
680
|
+
delete props.min;
|
|
681
|
+
delete props.max;
|
|
682
|
+
delete props.step;
|
|
683
|
+
delete props.steps;
|
|
684
|
+
delete props.value;
|
|
685
|
+
|
|
686
|
+
useEffect((nextValue) => {
|
|
687
|
+
const customSteps = customStepsState.get();
|
|
688
|
+
if (customSteps) {
|
|
689
|
+
sliderValue.set(findNearestSliderStepIndex(customSteps, nextValue));
|
|
690
|
+
return;
|
|
691
|
+
}
|
|
692
|
+
|
|
693
|
+
sliderValue.set(toSliderNumber(nextValue, toSliderNumber(minState.get(), 0)));
|
|
694
|
+
}, valueState);
|
|
695
|
+
|
|
696
|
+
useEffect((customSteps) => {
|
|
697
|
+
const currentValue = toSliderNumber(valueState.get(), toSliderNumber(minState.get(), 0));
|
|
698
|
+
|
|
699
|
+
if (customSteps) {
|
|
700
|
+
const nextStep = customSteps.find((step) => step.value === currentValue) ?? customSteps[0];
|
|
701
|
+
sliderValue.set(findNearestSliderStepIndex(customSteps, nextStep.value));
|
|
702
|
+
|
|
703
|
+
if (nextStep.value !== currentValue) {
|
|
704
|
+
valueState.set(nextStep.value);
|
|
705
|
+
props.input &&
|
|
706
|
+
props.input({
|
|
707
|
+
target: { value: nextStep.value },
|
|
708
|
+
currentTarget: { value: nextStep.value },
|
|
709
|
+
step: nextStep,
|
|
710
|
+
});
|
|
711
|
+
}
|
|
712
|
+
return;
|
|
713
|
+
}
|
|
714
|
+
|
|
715
|
+
sliderValue.set(currentValue);
|
|
716
|
+
}, customStepsState);
|
|
717
|
+
|
|
718
|
+
return wrapIntoLabel(
|
|
719
|
+
props,
|
|
720
|
+
Column(
|
|
721
|
+
{ gap: "small", class: "slider-field" },
|
|
722
|
+
input({
|
|
723
|
+
...props,
|
|
724
|
+
type: "range",
|
|
725
|
+
min: sliderBoundsState.map((bounds) => bounds.min),
|
|
726
|
+
max: sliderBoundsState.map((bounds) => bounds.max),
|
|
727
|
+
step: sliderBoundsState.map((bounds) => bounds.step),
|
|
728
|
+
value: sliderValue,
|
|
729
|
+
disabled: props.disabled,
|
|
730
|
+
class: `slider ${focusClass} ${props.class || ""}`.trim(),
|
|
731
|
+
input: (e) => {
|
|
732
|
+
const rawValue = toSliderNumber(e.target.value, sliderValue.get());
|
|
733
|
+
const customSteps = customStepsState.get();
|
|
734
|
+
const nextValue = customSteps
|
|
735
|
+
? getSliderStepValue(customSteps, rawValue)
|
|
736
|
+
: rawValue;
|
|
737
|
+
|
|
738
|
+
sliderValue.set(customSteps ? findNearestSliderStepIndex(customSteps, nextValue) : nextValue);
|
|
739
|
+
|
|
740
|
+
if (typeof valueState.set === "function") valueState.set(nextValue);
|
|
741
|
+
props.input && props.input(e);
|
|
742
|
+
},
|
|
743
|
+
}),
|
|
744
|
+
Show(shouldRenderLabels, () =>
|
|
745
|
+
div(
|
|
746
|
+
{
|
|
747
|
+
class: "slider-step-labels",
|
|
748
|
+
},
|
|
749
|
+
ForEach(sliderStepLabels, "value", (step) =>
|
|
750
|
+
div(
|
|
751
|
+
{
|
|
752
|
+
class: `slider-step-label ${step.alignment}`,
|
|
753
|
+
style: {
|
|
754
|
+
left: `${step.position}%`,
|
|
755
|
+
},
|
|
756
|
+
},
|
|
757
|
+
step.label
|
|
758
|
+
? Text({ weight: "heavy", color: "secondary" }, step.label)
|
|
759
|
+
: "",
|
|
760
|
+
),
|
|
761
|
+
),
|
|
762
|
+
),
|
|
763
|
+
),
|
|
764
|
+
),
|
|
765
|
+
);
|
|
766
|
+
};
|
|
767
|
+
|
|
196
768
|
// Exports
|
|
197
769
|
|
|
198
770
|
/**
|
|
@@ -217,6 +789,69 @@ export const TextInput = withNormalizedArgs((props, ...children) =>
|
|
|
217
789
|
)({ minHeight: 32, textSize: "1rem", ...props }, ...children),
|
|
218
790
|
);
|
|
219
791
|
|
|
792
|
+
/**
|
|
793
|
+
* Multiline text input with optional auto-growing height.
|
|
794
|
+
*
|
|
795
|
+
* @param {Object} props - Component props
|
|
796
|
+
* @param {Object|string|number} [props.value] - Textarea value (useState object or string)
|
|
797
|
+
* @param {string} [props.placeholder] - Placeholder text
|
|
798
|
+
* @param {string} [props.label] - Label text (wraps in Column with Heading)
|
|
799
|
+
* @param {boolean} [props.outline] - Show focus outline (default: false)
|
|
800
|
+
* @param {boolean} [props.disabled] - Disabled state
|
|
801
|
+
* @param {number} [props.minLines=3] - Minimum visible lines
|
|
802
|
+
* @param {number} [props.maxLines=3] - Maximum visible lines before scrolling
|
|
803
|
+
* Shift+Enter inserts a newline. Enter submits the parent form when available.
|
|
804
|
+
* @param {Function} [props.input] - Input event handler
|
|
805
|
+
* @param {string} [props.class] - Additional CSS classes
|
|
806
|
+
* @param {...*} children - Children elements (ignored)
|
|
807
|
+
* @returns {*} TextArea component
|
|
808
|
+
*/
|
|
809
|
+
export const TextArea = withNormalizedArgs((props, ...children) =>
|
|
810
|
+
withExtractedStyles((finalProps, ...children) =>
|
|
811
|
+
TextAreaCore(finalProps, ...children),
|
|
812
|
+
)({ textSize: "1rem", border: "primary", ...props }, ...children),
|
|
813
|
+
);
|
|
814
|
+
|
|
815
|
+
/**
|
|
816
|
+
* Menu-backed selector with input-like trigger rendering.
|
|
817
|
+
*
|
|
818
|
+
* @param {Object} props - Component props
|
|
819
|
+
* @param {Object|string} [props.value] - Selected item key (useState object or string)
|
|
820
|
+
* @param {Array<{key: string, text?: string, icon?: string, action?: Function, divider?: boolean}>} [props.options] - Menu-style options array
|
|
821
|
+
* @param {string} [props.label] - Label text (wraps in Column with Heading)
|
|
822
|
+
* @param {boolean} [props.outline] - Show focus outline
|
|
823
|
+
* @param {boolean} [props.disabled] - Disabled state
|
|
824
|
+
* @param {string} [props.anchor="bottom-left"] - Menu anchor position
|
|
825
|
+
* @param {Function} [props.input] - Called with an event-like object after selection
|
|
826
|
+
* @param {string} [props.class] - Additional CSS classes
|
|
827
|
+
* @param {...*} children - Children elements (ignored)
|
|
828
|
+
* @returns {*} Picker component
|
|
829
|
+
*/
|
|
830
|
+
export const Picker = withNormalizedArgs((props, ...children) =>
|
|
831
|
+
PickerCore(props, ...children),
|
|
832
|
+
);
|
|
833
|
+
|
|
834
|
+
/**
|
|
835
|
+
* iOS-style segmented picker with single keyed selection.
|
|
836
|
+
*
|
|
837
|
+
* @param {Object} props - Component props
|
|
838
|
+
* @param {Object|string} [props.value] - Selected segment key (useState object or string)
|
|
839
|
+
* @param {Array<{key: string, text: string, icon?: string}>} [props.items] - Segmented picker items
|
|
840
|
+
* @param {string} [props.label] - Label text (wraps in Column with Heading)
|
|
841
|
+
* @param {boolean} [props.outline] - Show focus outline
|
|
842
|
+
* @param {boolean} [props.disabled] - Disabled state
|
|
843
|
+
* @param {Function} [props.change] - Called with an event-like object after selection
|
|
844
|
+
* @param {Function} [props.input] - Called with an event-like object after selection
|
|
845
|
+
* @param {string} [props.class] - Additional CSS classes
|
|
846
|
+
* @param {...*} children - Children elements (ignored)
|
|
847
|
+
* @returns {*} SegmentedPicker component
|
|
848
|
+
*/
|
|
849
|
+
export const SegmentedPicker = withNormalizedArgs((props, ...children) =>
|
|
850
|
+
withExtractedStyles((finalProps, ...children) =>
|
|
851
|
+
SegmentedPickerCore(finalProps, ...children),
|
|
852
|
+
)({ textSize: "1rem", ...props }, ...children),
|
|
853
|
+
);
|
|
854
|
+
|
|
220
855
|
/**
|
|
221
856
|
* Dropdown select input with options.
|
|
222
857
|
*
|
|
@@ -257,3 +892,47 @@ export const CheckBox = withNormalizedArgs((props, ...children) =>
|
|
|
257
892
|
CheckBoxCore(finalProps, ...children),
|
|
258
893
|
)({ textSize: "1rem", ...props }, ...children),
|
|
259
894
|
);
|
|
895
|
+
|
|
896
|
+
/**
|
|
897
|
+
* OS-style switch toggle with state binding.
|
|
898
|
+
*
|
|
899
|
+
* @param {Object} props - Component props
|
|
900
|
+
* @param {Object|boolean} [props.checked] - Checked state (useState object or boolean)
|
|
901
|
+
* @param {Object|boolean} [props.value] - Alias for checked state
|
|
902
|
+
* @param {boolean} [props.outline] - Show focus outline
|
|
903
|
+
* @param {string} [props.label] - Label text (wraps in Row with Heading)
|
|
904
|
+
* @param {boolean} [props.disabled] - Disabled state
|
|
905
|
+
* @param {Function} [props.change] - Change event handler
|
|
906
|
+
* @param {Function} [props.input] - Input event handler
|
|
907
|
+
* @param {string} [props.class] - Additional CSS classes
|
|
908
|
+
* @param {...*} children - Children elements (ignored)
|
|
909
|
+
* @returns {*} Switch component
|
|
910
|
+
*/
|
|
911
|
+
export const Switch = withNormalizedArgs((props, ...children) =>
|
|
912
|
+
withExtractedStyles((finalProps, ...children) =>
|
|
913
|
+
SwitchCore(finalProps, ...children),
|
|
914
|
+
)({ textSize: "1rem", ...props }, ...children),
|
|
915
|
+
);
|
|
916
|
+
|
|
917
|
+
/**
|
|
918
|
+
* Range slider input with optional discrete step mapping.
|
|
919
|
+
*
|
|
920
|
+
* @param {Object} props - Component props
|
|
921
|
+
* @param {Object|number} [props.value] - Slider value (useState object or number)
|
|
922
|
+
* @param {Object|number} [props.min=0] - Minimum numeric value in native mode
|
|
923
|
+
* @param {Object|number} [props.max=100] - Maximum numeric value in native mode
|
|
924
|
+
* @param {Object|number} [props.step=1] - Step increment in native mode
|
|
925
|
+
* @param {Array<{value: number, label?: string}>} [props.steps] - Discrete step configuration; ignores min/max/step when valid
|
|
926
|
+
* @param {string} [props.label] - Label text (wraps in Column with Heading)
|
|
927
|
+
* @param {boolean} [props.outline] - Show focus outline
|
|
928
|
+
* @param {boolean} [props.disabled] - Disabled state
|
|
929
|
+
* @param {Function} [props.input] - Input event handler
|
|
930
|
+
* @param {string} [props.class] - Additional CSS classes
|
|
931
|
+
* @param {...*} children - Children elements (ignored)
|
|
932
|
+
* @returns {*} Slider component
|
|
933
|
+
*/
|
|
934
|
+
export const Slider = withNormalizedArgs((props, ...children) =>
|
|
935
|
+
withExtractedStyles((finalProps, ...children) =>
|
|
936
|
+
SliderCore(finalProps, ...children),
|
|
937
|
+
)({ fillWidth: true, ...props }, ...children),
|
|
938
|
+
);
|