@blankjs/react 0.1.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/dist/index.mjs ADDED
@@ -0,0 +1,1732 @@
1
+ import { FieldContext, useCollection, useControllableState, useFieldContext, useFieldControlProps, useFieldControlProps as useFieldControlProps$1, useFieldRoot } from "@blankjs/core";
2
+ import { Children, cloneElement, createContext, isValidElement, useCallback, useContext, useEffect, useId, useMemo, useRef, useState } from "react";
3
+ import { Fragment, jsx, jsxs } from "react/jsx-runtime";
4
+ import { autoUpdate, flip, offset, shift, size, useFloating } from "@floating-ui/react";
5
+ import { createPortal } from "react-dom";
6
+ //#region src/slot/merge-props.ts
7
+ const mergeProps = (slotProps, childProps) => {
8
+ const merged = {
9
+ ...slotProps,
10
+ ...childProps
11
+ };
12
+ for (const key in childProps) {
13
+ const slotValue = slotProps[key];
14
+ const childValue = childProps[key];
15
+ if (childValue === void 0) {
16
+ merged[key] = slotValue;
17
+ continue;
18
+ }
19
+ if (/^on[A-Z]/.test(key)) {
20
+ if (typeof slotValue === "function" && typeof childValue === "function") merged[key] = (...args) => {
21
+ childValue(...args);
22
+ slotValue(...args);
23
+ };
24
+ } else if (key === "className") merged[key] = [slotValue, childValue].filter(Boolean).join(" ");
25
+ else if (key === "style") merged[key] = {
26
+ ...slotValue,
27
+ ...childValue
28
+ };
29
+ }
30
+ return merged;
31
+ };
32
+ //#endregion
33
+ //#region src/slot/compose-refs.ts
34
+ const setRef = (ref, node) => {
35
+ if (typeof ref === "function") ref(node);
36
+ else if (ref) ref.current = node;
37
+ };
38
+ const composeRefs = (...refs) => {
39
+ return (node) => {
40
+ refs.forEach((ref) => setRef(ref, node));
41
+ };
42
+ };
43
+ //#endregion
44
+ //#region src/slot/slot.tsx
45
+ const Slot = ({ children, ...slotProps }) => {
46
+ if (!isValidElement(children)) return Children.count(children) > 1 ? Children.only(null) : null;
47
+ const childProps = children.props;
48
+ const childRef = childProps.ref;
49
+ const slotRef = slotProps.ref;
50
+ const merged = mergeProps(slotProps, childProps);
51
+ if (slotRef || childRef) merged.ref = composeRefs(slotRef, childRef);
52
+ return cloneElement(children, merged);
53
+ };
54
+ //#endregion
55
+ //#region src/form/context.ts
56
+ const FormContext = createContext(null);
57
+ //#endregion
58
+ //#region src/form/form.tsx
59
+ const Form = ({ onSubmit, errors, ref, children, ...rest }) => {
60
+ const innerRef = useRef(null);
61
+ const formContextValue = useMemo(() => ({ errors }), [errors]);
62
+ const handleSubmit = (e) => {
63
+ if (!onSubmit) return;
64
+ e.preventDefault();
65
+ onSubmit(new FormData(e.currentTarget), e);
66
+ };
67
+ useEffect(() => {
68
+ const form = innerRef.current;
69
+ if (!form) return;
70
+ let focused = false;
71
+ const onInvalid = (e) => {
72
+ if (focused) return;
73
+ focused = true;
74
+ queueMicrotask(() => {
75
+ focused = false;
76
+ });
77
+ e.target?.focus?.();
78
+ };
79
+ form.addEventListener("invalid", onInvalid, true);
80
+ return () => form.removeEventListener("invalid", onInvalid, true);
81
+ }, []);
82
+ return /* @__PURE__ */ jsx(FormContext, {
83
+ value: formContextValue,
84
+ children: /* @__PURE__ */ jsx("form", {
85
+ ref: composeRefs(innerRef, ref),
86
+ onSubmit: handleSubmit,
87
+ ...rest,
88
+ children
89
+ })
90
+ });
91
+ };
92
+ Form.displayName = "Form";
93
+ //#endregion
94
+ //#region src/form/serialize.ts
95
+ const serialize = (data) => {
96
+ const result = {};
97
+ for (const key of new Set(data.keys())) {
98
+ const values = data.getAll(key);
99
+ result[key] = values.length > 1 ? values : values[0] ?? "";
100
+ }
101
+ return result;
102
+ };
103
+ //#endregion
104
+ //#region src/field/root.tsx
105
+ const FieldRoot = ({ children, invalid, disabled, required, validationMode, name, ref, validate, ...props }) => {
106
+ const formContext = useContext(FormContext);
107
+ const serverError = name ? formContext?.errors?.[name] : void 0;
108
+ const [dismissed, setDismissed] = useState(false);
109
+ const activeServerError = dismissed ? void 0 : serverError;
110
+ const { onBlurCapture, onChangeCapture, onInvalidCapture, resetValidation, validateControl, ...contextValue } = useFieldRoot({
111
+ invalid: invalid ?? (activeServerError ? true : void 0),
112
+ disabled,
113
+ required,
114
+ validationMode,
115
+ validate
116
+ });
117
+ const innerRef = useRef(null);
118
+ useEffect(() => {
119
+ const control = innerRef.current?.querySelector("input:not([type=hidden]), textarea, select");
120
+ if (control) validateControl(control);
121
+ }, [validateControl]);
122
+ useEffect(() => {
123
+ setDismissed(false);
124
+ }, [formContext?.errors]);
125
+ useEffect(() => {
126
+ const form = innerRef.current?.closest("form");
127
+ if (!form) return;
128
+ form.addEventListener("reset", resetValidation);
129
+ return () => form.removeEventListener("reset", resetValidation);
130
+ }, [resetValidation]);
131
+ useEffect(() => {
132
+ const node = innerRef.current;
133
+ if (!node || !onChangeCapture) return;
134
+ const handler = (e) => {
135
+ onChangeCapture(e);
136
+ setDismissed(true);
137
+ };
138
+ node.addEventListener("change", handler, true);
139
+ node.addEventListener("input", handler, true);
140
+ return () => {
141
+ node.removeEventListener("change", handler, true);
142
+ node.removeEventListener("input", handler, true);
143
+ };
144
+ }, [onChangeCapture]);
145
+ return /* @__PURE__ */ jsx(FieldContext, {
146
+ value: useMemo(() => ({
147
+ ...contextValue,
148
+ serverError: activeServerError
149
+ }), [activeServerError, contextValue]),
150
+ children: /* @__PURE__ */ jsx("div", {
151
+ ...props,
152
+ ref: composeRefs(innerRef, ref),
153
+ "data-invalid": contextValue.invalid ? "" : void 0,
154
+ "data-disabled": disabled ? "" : void 0,
155
+ onBlurCapture,
156
+ onInvalidCapture,
157
+ children
158
+ })
159
+ });
160
+ };
161
+ FieldRoot.displayName = "Field.Root";
162
+ //#endregion
163
+ //#region src/field/label.tsx
164
+ const FieldLabel = ({ children, ...props }) => {
165
+ const { controlId, labelId, registerLabel, hasGroupControl } = useFieldContext();
166
+ useEffect(() => registerLabel(), [registerLabel]);
167
+ return /* @__PURE__ */ jsx("label", {
168
+ ...props,
169
+ id: labelId,
170
+ htmlFor: hasGroupControl ? void 0 : controlId,
171
+ children
172
+ });
173
+ };
174
+ FieldLabel.displayName = "Field.Label";
175
+ //#endregion
176
+ //#region src/field/description.tsx
177
+ const FieldDescription = ({ children, ...props }) => {
178
+ const { descriptionId, registerDescription } = useFieldContext();
179
+ useEffect(() => registerDescription(), [registerDescription]);
180
+ return /* @__PURE__ */ jsx("div", {
181
+ ...props,
182
+ id: descriptionId,
183
+ children
184
+ });
185
+ };
186
+ FieldDescription.displayName = "Field.Description";
187
+ //#endregion
188
+ //#region src/field/error.tsx
189
+ const FieldError = ({ children, match, ...props }) => {
190
+ const { errorId, registerError, invalid, validity, validationMessage, serverError } = useFieldContext();
191
+ const visible = match ? validity?.[match] === true : invalid;
192
+ useEffect(() => {
193
+ let cleanup = null;
194
+ if (visible) cleanup = registerError();
195
+ return () => cleanup?.();
196
+ }, [registerError, visible]);
197
+ if (!visible) return null;
198
+ return /* @__PURE__ */ jsx("div", {
199
+ ...props,
200
+ id: errorId,
201
+ children: children ?? serverError ?? validationMessage
202
+ });
203
+ };
204
+ FieldError.displayName = "Field.Error";
205
+ //#endregion
206
+ //#region src/field/control.tsx
207
+ const FieldControl = ({ children }) => {
208
+ return /* @__PURE__ */ jsx(Slot, {
209
+ ...useFieldControlProps$1(),
210
+ children
211
+ });
212
+ };
213
+ FieldControl.displayName = "Field.Control";
214
+ //#endregion
215
+ //#region src/field/index.ts
216
+ const Field = {
217
+ Control: FieldControl,
218
+ Root: FieldRoot,
219
+ Label: FieldLabel,
220
+ Description: FieldDescription,
221
+ Error: FieldError
222
+ };
223
+ //#endregion
224
+ //#region src/text-input/text-input.tsx
225
+ const TextInput = ({ className, size = "md", ...props }) => {
226
+ return /* @__PURE__ */ jsx("input", {
227
+ ...useFieldControlProps$1(),
228
+ ...props,
229
+ "data-size": size,
230
+ className: ["bk-input", className].filter(Boolean).join(" ")
231
+ });
232
+ };
233
+ TextInput.displayName = "TextInput";
234
+ //#endregion
235
+ //#region src/textarea/textarea.tsx
236
+ const Textarea = ({ className, size = "md", ...props }) => {
237
+ return /* @__PURE__ */ jsx("textarea", {
238
+ rows: 3,
239
+ ...useFieldControlProps$1(),
240
+ ...props,
241
+ "data-size": size,
242
+ className: ["bk-textarea", className].filter(Boolean).join(" ")
243
+ });
244
+ };
245
+ Textarea.displayName = "Textarea";
246
+ //#endregion
247
+ //#region src/password-field/password-field.tsx
248
+ const EyeIcon = ({ off }) => /* @__PURE__ */ jsxs("svg", {
249
+ width: "18",
250
+ height: "18",
251
+ viewBox: "0 0 24 24",
252
+ fill: "none",
253
+ stroke: "currentColor",
254
+ strokeWidth: "2",
255
+ "aria-hidden": "true",
256
+ children: [
257
+ /* @__PURE__ */ jsx("path", { d: "M2 12s3.5-7 10-7 10 7 10 7-3.5 7-10 7-10-7-10-7z" }),
258
+ /* @__PURE__ */ jsx("circle", {
259
+ cx: "12",
260
+ cy: "12",
261
+ r: "3"
262
+ }),
263
+ off && /* @__PURE__ */ jsx("line", {
264
+ x1: "3",
265
+ y1: "3",
266
+ x2: "21",
267
+ y2: "21"
268
+ })
269
+ ]
270
+ });
271
+ const PasswordField = ({ className, disabled, size = "md", ...rest }) => {
272
+ const fieldProps = useFieldControlProps$1();
273
+ const [visible, setVisible] = useState(false);
274
+ const isDisabled = disabled || fieldProps.disabled;
275
+ const mergedInput = ["bk-input", className].filter(Boolean).join(" ");
276
+ return /* @__PURE__ */ jsxs("div", {
277
+ className: "bk-password",
278
+ "data-size": size,
279
+ children: [/* @__PURE__ */ jsx("input", {
280
+ ...rest,
281
+ ...fieldProps,
282
+ type: visible ? "text" : "password",
283
+ disabled: isDisabled,
284
+ className: mergedInput
285
+ }), /* @__PURE__ */ jsx("button", {
286
+ className: "bk-password-toggle",
287
+ type: "button",
288
+ disabled: isDisabled,
289
+ "aria-label": visible ? "Hide password" : "Show password",
290
+ onClick: () => setVisible((v) => !v),
291
+ onMouseDown: (e) => e.preventDefault(),
292
+ children: /* @__PURE__ */ jsx(EyeIcon, { off: visible })
293
+ })]
294
+ });
295
+ };
296
+ //#endregion
297
+ //#region src/select/context.ts
298
+ const SelectContext = createContext(null);
299
+ const useSelectContext = () => {
300
+ const context = useContext(SelectContext);
301
+ if (!context) throw new Error("[blankjs] Select.* must be used within Select.Root");
302
+ return context;
303
+ };
304
+ //#endregion
305
+ //#region src/select/hidden-input.tsx
306
+ const SelectHiddenInput = ({ name, defaultValue, required }) => {
307
+ const { value, setValue, disabled, triggerElement } = useSelectContext();
308
+ const ref = useRef(null);
309
+ const controlProps = useFieldControlProps$1();
310
+ const isRequired = required ?? controlProps.required ?? false;
311
+ const mountedRef = useRef(false);
312
+ useEffect(() => {
313
+ if (!mountedRef.current) {
314
+ mountedRef.current = true;
315
+ return;
316
+ }
317
+ ref.current?.dispatchEvent(new Event("change", { bubbles: true }));
318
+ }, [value]);
319
+ useEffect(() => {
320
+ const form = ref.current?.form;
321
+ if (!form) return;
322
+ const onReset = () => setValue(defaultValue);
323
+ form.addEventListener("reset", onReset);
324
+ return () => form.removeEventListener("reset", onReset);
325
+ }, [setValue, defaultValue]);
326
+ return /* @__PURE__ */ jsx("input", {
327
+ ref,
328
+ type: "text",
329
+ name,
330
+ value: value ?? "",
331
+ disabled,
332
+ className: "bk-select-hidden-input",
333
+ tabIndex: -1,
334
+ "aria-hidden": "true",
335
+ autoComplete: "off",
336
+ onChange: () => {},
337
+ required: isRequired,
338
+ onFocus: () => triggerElement?.focus()
339
+ });
340
+ };
341
+ SelectHiddenInput.displayName = "Select.HiddenInput";
342
+ //#endregion
343
+ //#region src/select/use-select-root.ts
344
+ const useSelectRoot = (options = {}) => {
345
+ const [value, setValue] = useControllableState({
346
+ prop: options.value,
347
+ defaultProp: options.defaultValue,
348
+ onChange: options.onValueChange
349
+ });
350
+ const [open, setOpen] = useControllableState({
351
+ prop: options.open,
352
+ defaultProp: options.defaultOpen,
353
+ onChange: options.onOpenChange
354
+ });
355
+ const { getItems, registerItem } = useCollection();
356
+ const [activeItem, setActiveItem] = useState(void 0);
357
+ const [triggerElement, setTriggerElement] = useState(null);
358
+ const triggerId = useId();
359
+ const listboxId = useId();
360
+ return useMemo(() => ({
361
+ open: open ?? false,
362
+ setOpen,
363
+ activeItem,
364
+ setActiveItem,
365
+ value,
366
+ setValue,
367
+ triggerElement,
368
+ setTriggerElement,
369
+ listboxId,
370
+ triggerId,
371
+ getItems,
372
+ registerItem,
373
+ disabled: options.disabled ?? false,
374
+ size: options.size ?? "md"
375
+ }), [
376
+ activeItem,
377
+ getItems,
378
+ listboxId,
379
+ open,
380
+ options.disabled,
381
+ options.size,
382
+ registerItem,
383
+ setOpen,
384
+ setValue,
385
+ triggerElement,
386
+ triggerId,
387
+ value
388
+ ]);
389
+ };
390
+ //#endregion
391
+ //#region src/select/root.tsx
392
+ const SelectRoot = ({ children, required, ...options }) => {
393
+ const contextValue = useSelectRoot(options);
394
+ return /* @__PURE__ */ jsx(SelectContext, {
395
+ value: contextValue,
396
+ children: /* @__PURE__ */ jsxs("div", {
397
+ className: "bk-select-root",
398
+ "data-size": contextValue.size,
399
+ children: [children, options.name && /* @__PURE__ */ jsx(SelectHiddenInput, {
400
+ name: options.name,
401
+ defaultValue: options.defaultValue,
402
+ required
403
+ })]
404
+ })
405
+ });
406
+ };
407
+ SelectRoot.displayName = "Select.Root";
408
+ //#endregion
409
+ //#region src/internal/use-popover.ts
410
+ const matchWidthMap = {
411
+ exact: "width",
412
+ min: "minWidth"
413
+ };
414
+ const usePopover = ({ anchor, onDismiss, matchWidth }) => {
415
+ const { refs, floatingStyles, elements } = useFloating({
416
+ placement: "bottom-start",
417
+ middleware: [
418
+ offset(4),
419
+ flip(),
420
+ shift({ padding: 8 }),
421
+ size({ apply({ elements: floatingState, rects }) {
422
+ Object.assign(floatingState.floating.style, { [matchWidthMap[matchWidth ?? "exact"]]: `${rects.reference.width}px` });
423
+ } })
424
+ ],
425
+ whileElementsMounted: autoUpdate,
426
+ elements: { reference: anchor }
427
+ });
428
+ const { setFloating } = refs;
429
+ useEffect(() => {
430
+ const onKeyDown = (e) => {
431
+ if (e.key !== "Escape" || e.defaultPrevented) return;
432
+ e.preventDefault();
433
+ onDismiss("escape");
434
+ };
435
+ document.addEventListener("keydown", onKeyDown);
436
+ return () => document.removeEventListener("keydown", onKeyDown);
437
+ }, [onDismiss]);
438
+ useEffect(() => {
439
+ const onPointerDown = (e) => {
440
+ const target = e.target;
441
+ if (elements.floating?.contains(target)) return;
442
+ if (anchor?.contains(target)) return;
443
+ onDismiss("outside-press");
444
+ };
445
+ document.addEventListener("pointerdown", onPointerDown);
446
+ return () => document.removeEventListener("pointerdown", onPointerDown);
447
+ }, [
448
+ anchor,
449
+ elements.floating,
450
+ onDismiss
451
+ ]);
452
+ return {
453
+ setFloating,
454
+ floatingStyles
455
+ };
456
+ };
457
+ //#endregion
458
+ //#region src/internal/use-initial-active-item.ts
459
+ const useInitialActiveItem = ({ getItems, setActiveItem, isSelected }) => {
460
+ const [mounted, setMounted] = useState(false);
461
+ useEffect(() => setMounted(true), []);
462
+ useEffect(() => {
463
+ if (!mounted) return;
464
+ const items = getItems();
465
+ setActiveItem(items.find(isSelected) ?? items[0]);
466
+ return () => setActiveItem(void 0);
467
+ }, [mounted]);
468
+ return mounted;
469
+ };
470
+ //#endregion
471
+ //#region src/internal/use-listbox-keyboard.ts
472
+ const useListboxKeyboard = ({ open, setOpen, getItems, activeItem, setActiveItem, onCommit }) => {
473
+ const searchRef = useRef("");
474
+ const timerRef = useRef(null);
475
+ const onKeyDown = (e) => {
476
+ const key = e.key;
477
+ if (!open) {
478
+ switch (key) {
479
+ case "ArrowDown":
480
+ case "ArrowUp":
481
+ case "Enter":
482
+ case " ":
483
+ e.preventDefault();
484
+ setOpen(true);
485
+ break;
486
+ default: break;
487
+ }
488
+ return;
489
+ }
490
+ const items = getItems();
491
+ const index = items.findIndex((item) => item.value === activeItem?.value);
492
+ const isPrintable = key.length === 1 && !e.ctrlKey && !e.metaKey && !e.altKey;
493
+ const isSpaceSelect = key === " " && searchRef.current === "";
494
+ if (isPrintable && !isSpaceSelect) {
495
+ e.preventDefault();
496
+ searchRef.current += key;
497
+ if (timerRef.current) clearTimeout(timerRef.current);
498
+ timerRef.current = setTimeout(() => {
499
+ searchRef.current = "";
500
+ }, 500);
501
+ const query = searchRef.current.toLowerCase();
502
+ for (let offset = 1; offset <= items.length; offset++) {
503
+ const item = items[(index + offset) % items.length];
504
+ if (item && item.label.toLowerCase().startsWith(query)) {
505
+ setActiveItem(item);
506
+ break;
507
+ }
508
+ }
509
+ return;
510
+ }
511
+ switch (key) {
512
+ case "ArrowDown": {
513
+ e.preventDefault();
514
+ const next = items[Math.min(index + 1, items.length - 1)];
515
+ if (next) setActiveItem(next);
516
+ break;
517
+ }
518
+ case "ArrowUp": {
519
+ e.preventDefault();
520
+ const prev = items[Math.max(index - 1, 0)];
521
+ if (prev) setActiveItem(prev);
522
+ break;
523
+ }
524
+ case "Enter":
525
+ case " ":
526
+ e.preventDefault();
527
+ if (activeItem && searchRef.current === "") onCommit(activeItem);
528
+ break;
529
+ case "Tab":
530
+ setOpen(false);
531
+ break;
532
+ case "Home":
533
+ e.preventDefault();
534
+ setActiveItem(items[0]);
535
+ break;
536
+ case "End":
537
+ e.preventDefault();
538
+ setActiveItem(items[items.length - 1]);
539
+ break;
540
+ default: break;
541
+ }
542
+ };
543
+ return onKeyDown;
544
+ };
545
+ //#endregion
546
+ //#region src/select/trigger.tsx
547
+ const SelectArrow = () => /* @__PURE__ */ jsx("svg", {
548
+ className: "bk-select-arrow",
549
+ width: "16",
550
+ height: "16",
551
+ viewBox: "0 0 24 24",
552
+ fill: "none",
553
+ stroke: "currentColor",
554
+ strokeWidth: "2",
555
+ "aria-hidden": "true",
556
+ children: /* @__PURE__ */ jsx("path", { d: "M6 9l6 6 6-6" })
557
+ });
558
+ const SelectTrigger = ({ asChild, children, className, ...props }) => {
559
+ const { required: _, ...fieldProps } = useFieldControlProps$1();
560
+ const { open, setOpen, triggerId, listboxId, disabled, activeItem, setTriggerElement, getItems, setActiveItem, setValue } = useSelectContext();
561
+ const isDisabled = fieldProps.disabled || disabled;
562
+ const id = fieldProps.id ?? triggerId;
563
+ const activeDescendant = open ? activeItem?.id : void 0;
564
+ const onListboxKeyDown = useListboxKeyboard({
565
+ activeItem,
566
+ setActiveItem,
567
+ getItems,
568
+ open,
569
+ setOpen,
570
+ onCommit: (item) => {
571
+ setValue(item.value);
572
+ setOpen(false);
573
+ }
574
+ });
575
+ const triggerProps = {
576
+ type: "button",
577
+ ...props,
578
+ ...fieldProps,
579
+ className: ["bk-select-trigger", className].filter(Boolean).join(" "),
580
+ id,
581
+ role: "combobox",
582
+ "aria-disabled": isDisabled || void 0,
583
+ "aria-haspopup": "listbox",
584
+ "aria-expanded": open,
585
+ "aria-controls": open ? listboxId : void 0,
586
+ "aria-activedescendant": activeDescendant,
587
+ disabled: isDisabled,
588
+ onKeyDown: (e) => {
589
+ if (isDisabled) return;
590
+ props.onKeyDown?.(e);
591
+ onListboxKeyDown(e);
592
+ },
593
+ onClick: (e) => {
594
+ if (isDisabled) return;
595
+ props.onClick?.(e);
596
+ setOpen((o) => !o);
597
+ }
598
+ };
599
+ if (asChild) return /* @__PURE__ */ jsx(Slot, {
600
+ ...triggerProps,
601
+ ref: composeRefs(setTriggerElement, props.ref),
602
+ children
603
+ });
604
+ return /* @__PURE__ */ jsxs("button", {
605
+ ...triggerProps,
606
+ ref: composeRefs(setTriggerElement, props.ref),
607
+ children: [children, /* @__PURE__ */ jsx(SelectArrow, {})]
608
+ });
609
+ };
610
+ SelectTrigger.displayName = "Select.Trigger";
611
+ //#endregion
612
+ //#region src/select/content.tsx
613
+ const SelectContentInner = ({ children, style, className, container, ...props }) => {
614
+ const { triggerElement, listboxId, setOpen, getItems, value, setActiveItem, size } = useSelectContext();
615
+ const { setFloating, floatingStyles } = usePopover({
616
+ anchor: triggerElement,
617
+ onDismiss: useCallback(() => setOpen(false), [setOpen]),
618
+ matchWidth: "exact"
619
+ });
620
+ if (!useInitialActiveItem({
621
+ getItems,
622
+ setActiveItem,
623
+ isSelected: (item) => item.value === value
624
+ })) return null;
625
+ const target = container ?? document.body;
626
+ return createPortal(/* @__PURE__ */ jsx("div", {
627
+ ...props,
628
+ ref: setFloating,
629
+ style: {
630
+ ...style,
631
+ ...floatingStyles
632
+ },
633
+ className: ["bk-select-content", className].filter(Boolean).join(" "),
634
+ role: "listbox",
635
+ id: listboxId,
636
+ "data-size": size,
637
+ children
638
+ }), target);
639
+ };
640
+ const SelectContent = ({ children, ...props }) => {
641
+ const { open } = useSelectContext();
642
+ if (!open) return null;
643
+ return /* @__PURE__ */ jsx(SelectContentInner, {
644
+ ...props,
645
+ children
646
+ });
647
+ };
648
+ SelectContent.displayName = "Select.Content";
649
+ //#endregion
650
+ //#region src/select/item.tsx
651
+ const SelectCheck = () => /* @__PURE__ */ jsx("svg", {
652
+ className: "bk-select-check",
653
+ width: "16",
654
+ height: "16",
655
+ viewBox: "0 0 24 24",
656
+ fill: "none",
657
+ stroke: "currentColor",
658
+ strokeWidth: "2",
659
+ "aria-hidden": "true",
660
+ children: /* @__PURE__ */ jsx("path", { d: "M20 6L9 17l-5-5" })
661
+ });
662
+ const SelectItem = ({ value, textValue, children, className, ...props }) => {
663
+ const ref = useRef(null);
664
+ const optionId = useId();
665
+ const { value: selectedValue, activeItem, setValue, setOpen, registerItem, setActiveItem } = useSelectContext();
666
+ const isSelected = value === selectedValue;
667
+ const isActive = value === activeItem?.value;
668
+ useEffect(() => {
669
+ const node = ref.current;
670
+ if (!node) return;
671
+ const unregister = registerItem({
672
+ node,
673
+ value,
674
+ label: textValue ?? node.textContent ?? "",
675
+ id: optionId
676
+ });
677
+ return () => {
678
+ unregister();
679
+ setActiveItem((prev) => prev?.value === value ? void 0 : prev);
680
+ };
681
+ }, [
682
+ registerItem,
683
+ textValue,
684
+ value,
685
+ optionId,
686
+ setActiveItem
687
+ ]);
688
+ useEffect(() => {
689
+ if (isActive) ref.current?.scrollIntoView({ block: "nearest" });
690
+ }, [isActive]);
691
+ return /* @__PURE__ */ jsxs("div", {
692
+ ...props,
693
+ ref,
694
+ className: ["bk-select-item", className].filter(Boolean).join(" "),
695
+ role: "option",
696
+ id: optionId,
697
+ "aria-selected": isSelected,
698
+ "data-active": isActive ? "" : void 0,
699
+ onClick: (e) => {
700
+ props.onClick?.(e);
701
+ if (e.defaultPrevented) return;
702
+ setValue(value);
703
+ setOpen(false);
704
+ },
705
+ children: [children, /* @__PURE__ */ jsx(SelectCheck, {})]
706
+ });
707
+ };
708
+ SelectItem.displayName = "Select.Item";
709
+ //#endregion
710
+ //#region src/select/value.tsx
711
+ const SelectValue = ({ placeholder, children }) => {
712
+ const { value } = useSelectContext();
713
+ const getValue = () => {
714
+ if (value === void 0) return placeholder;
715
+ if (children) return children(value);
716
+ return value;
717
+ };
718
+ return /* @__PURE__ */ jsx("span", {
719
+ className: "bk-select-value",
720
+ "data-placeholder": value === void 0 ? "" : void 0,
721
+ children: getValue()
722
+ });
723
+ };
724
+ SelectValue.displayName = "Select.Value";
725
+ //#endregion
726
+ //#region src/select/clear.tsx
727
+ const SelectClearIcon = () => /* @__PURE__ */ jsx("svg", {
728
+ width: "14",
729
+ height: "14",
730
+ viewBox: "0 0 24 24",
731
+ fill: "none",
732
+ stroke: "currentColor",
733
+ strokeWidth: "2",
734
+ "aria-hidden": "true",
735
+ children: /* @__PURE__ */ jsx("path", { d: "M18 6L6 18M6 6l12 12" })
736
+ });
737
+ const SelectClear = ({ className, children, onClick, ...props }) => {
738
+ const { value, setValue, disabled, triggerElement } = useSelectContext();
739
+ if (!value || disabled) return null;
740
+ const handleClick = (e) => {
741
+ onClick?.(e);
742
+ if (e.defaultPrevented) return;
743
+ setValue(void 0);
744
+ triggerElement?.focus();
745
+ };
746
+ return /* @__PURE__ */ jsx("button", {
747
+ type: "button",
748
+ "aria-label": "Clear",
749
+ className: ["bk-select-clear", className].filter(Boolean).join(" "),
750
+ onClick: handleClick,
751
+ ...props,
752
+ children: children ?? /* @__PURE__ */ jsx(SelectClearIcon, {})
753
+ });
754
+ };
755
+ SelectClear.displayName = "Select.Clear";
756
+ //#endregion
757
+ //#region src/select/index.ts
758
+ const Select = {
759
+ Root: SelectRoot,
760
+ Trigger: SelectTrigger,
761
+ Content: SelectContent,
762
+ Item: SelectItem,
763
+ Value: SelectValue,
764
+ Clear: SelectClear
765
+ };
766
+ //#endregion
767
+ //#region src/button/button.tsx
768
+ const Button = ({ children, className, asChild, variant = "solid", size = "md", ...props }) => {
769
+ const buttonProps = {
770
+ type: "button",
771
+ ...props,
772
+ className: ["bk-button", className].filter(Boolean).join(" "),
773
+ "data-variant": variant,
774
+ "data-size": size
775
+ };
776
+ if (asChild) return /* @__PURE__ */ jsx(Slot, {
777
+ ...buttonProps,
778
+ children
779
+ });
780
+ return /* @__PURE__ */ jsx("button", {
781
+ ...buttonProps,
782
+ children
783
+ });
784
+ };
785
+ //#endregion
786
+ //#region src/checkbox/checkbox.tsx
787
+ const Checkbox = ({ checked, defaultChecked, onCheckedChange, indeterminate, onChange, disabled, className, size = "md", ...props }) => {
788
+ const fieldProps = useFieldControlProps$1();
789
+ const [value, setValue] = useControllableState({
790
+ prop: checked,
791
+ defaultProp: defaultChecked,
792
+ onChange: onCheckedChange
793
+ });
794
+ const ref = useRef(null);
795
+ useEffect(() => {
796
+ const form = ref.current?.form;
797
+ if (!form) return;
798
+ const onReset = () => setValue(defaultChecked ?? false);
799
+ form.addEventListener("reset", onReset);
800
+ return () => form.removeEventListener("reset", onReset);
801
+ }, [setValue, defaultChecked]);
802
+ useEffect(() => {
803
+ if (ref.current) ref.current.indeterminate = indeterminate ?? false;
804
+ }, [indeterminate, value]);
805
+ const isDisabled = disabled ?? fieldProps.disabled;
806
+ const handleChange = (e) => {
807
+ onChange?.(e);
808
+ if (e.defaultPrevented) return;
809
+ setValue(e.target.checked);
810
+ };
811
+ return /* @__PURE__ */ jsx("input", {
812
+ ...fieldProps,
813
+ ...props,
814
+ checked: value ?? false,
815
+ onChange: handleChange,
816
+ ref: composeRefs(ref, props.ref),
817
+ disabled: isDisabled,
818
+ "data-size": size,
819
+ className: ["bk-checkbox", className].filter(Boolean).join(" "),
820
+ type: "checkbox"
821
+ });
822
+ };
823
+ //#endregion
824
+ //#region src/switch/switch.tsx
825
+ const Switch = ({ checked, defaultChecked, onCheckedChange, disabled, className, style, onChange, size = "md", ...props }) => {
826
+ const fieldProps = useFieldControlProps$1();
827
+ const [value, setValue] = useControllableState({
828
+ prop: checked,
829
+ defaultProp: defaultChecked,
830
+ onChange: onCheckedChange
831
+ });
832
+ const ref = useRef(null);
833
+ useEffect(() => {
834
+ const form = ref.current?.form;
835
+ if (!form) return;
836
+ const onReset = () => setValue(defaultChecked ?? false);
837
+ form.addEventListener("reset", onReset);
838
+ return () => form.removeEventListener("reset", onReset);
839
+ }, [setValue, defaultChecked]);
840
+ const isDisabled = disabled ?? fieldProps.disabled;
841
+ const handleChange = (e) => {
842
+ onChange?.(e);
843
+ if (e.defaultPrevented) return;
844
+ setValue(e.target.checked);
845
+ };
846
+ return /* @__PURE__ */ jsxs("span", {
847
+ className: ["bk-switch", className].filter(Boolean).join(" "),
848
+ style,
849
+ "data-size": size,
850
+ children: [/* @__PURE__ */ jsx("input", {
851
+ ...fieldProps,
852
+ ...props,
853
+ role: "switch",
854
+ checked: value ?? false,
855
+ onChange: handleChange,
856
+ ref: composeRefs(ref, props.ref),
857
+ disabled: isDisabled,
858
+ className: "bk-switch-input",
859
+ type: "checkbox"
860
+ }), /* @__PURE__ */ jsx("span", {
861
+ className: "bk-switch-thumb",
862
+ "aria-hidden": "true"
863
+ })]
864
+ });
865
+ };
866
+ //#endregion
867
+ //#region src/radio/context.ts
868
+ const RadioGroupContext = createContext(null);
869
+ const useRadioGroupContext = () => {
870
+ const context = useContext(RadioGroupContext);
871
+ if (!context) throw new Error("[blankjs] RadioGroup.* must be used within RadioGroup.Root");
872
+ return context;
873
+ };
874
+ //#endregion
875
+ //#region src/radio/use-radio-group-root.ts
876
+ const useRadioGroupRoot = (options = {}) => {
877
+ const [value, setValue] = useControllableState({
878
+ prop: options.value,
879
+ defaultProp: options.defaultValue,
880
+ onChange: options.onValueChange
881
+ });
882
+ const fallbackName = useId();
883
+ const name = options.name ?? fallbackName;
884
+ return useMemo(() => ({
885
+ value,
886
+ setValue,
887
+ disabled: options.disabled ?? false,
888
+ name
889
+ }), [
890
+ name,
891
+ options.disabled,
892
+ setValue,
893
+ value
894
+ ]);
895
+ };
896
+ //#endregion
897
+ //#region src/radio/root.tsx
898
+ const RadioGroupRoot = ({ children, value, defaultValue, onValueChange, disabled, name, className, size = "md", ...props }) => {
899
+ const { disabled: fieldDisabled, ...fieldProps } = useFieldControlProps$1();
900
+ const fieldContext = useContext(FieldContext);
901
+ const labelledBy = fieldContext?.hasLabel ? fieldContext.labelId : void 0;
902
+ const registerGroupControl = fieldContext?.registerGroupControl;
903
+ useEffect(() => registerGroupControl?.(), [registerGroupControl]);
904
+ const contextValue = useRadioGroupRoot({
905
+ value,
906
+ defaultValue,
907
+ onValueChange,
908
+ disabled: disabled ?? fieldDisabled,
909
+ name
910
+ });
911
+ const { setValue } = contextValue;
912
+ const ref = useRef(null);
913
+ useEffect(() => {
914
+ const form = ref.current?.closest("form");
915
+ if (!form) return;
916
+ const onReset = () => setValue(defaultValue);
917
+ form.addEventListener("reset", onReset);
918
+ return () => form.removeEventListener("reset", onReset);
919
+ }, [setValue, defaultValue]);
920
+ return /* @__PURE__ */ jsx(RadioGroupContext, {
921
+ value: contextValue,
922
+ children: /* @__PURE__ */ jsx("div", {
923
+ ...fieldProps,
924
+ ...props,
925
+ ref: composeRefs(ref, props.ref),
926
+ role: "radiogroup",
927
+ "data-size": size,
928
+ "aria-labelledby": props["aria-labelledby"] ?? labelledBy,
929
+ className: ["bk-radio-group", className].filter(Boolean).join(" "),
930
+ children
931
+ })
932
+ });
933
+ };
934
+ RadioGroupRoot.displayName = "RadioGroup.Root";
935
+ //#endregion
936
+ //#region src/radio/item.tsx
937
+ const RadioGroupItem = ({ className, style, onChange, children, ...props }) => {
938
+ const { name, setValue, value: contextValue, disabled } = useRadioGroupContext();
939
+ const isDisabled = props.disabled ?? disabled;
940
+ const handleChange = (e) => {
941
+ onChange?.(e);
942
+ if (e.defaultPrevented) return;
943
+ setValue(e.target.value);
944
+ };
945
+ const input = /* @__PURE__ */ jsx("input", {
946
+ ...props,
947
+ checked: contextValue === props.value,
948
+ onChange: handleChange,
949
+ disabled: isDisabled,
950
+ type: "radio",
951
+ name,
952
+ className: children ? "bk-radio-item" : ["bk-radio-item", className].filter(Boolean).join(" "),
953
+ style: children ? void 0 : style
954
+ });
955
+ if (!children) return input;
956
+ return /* @__PURE__ */ jsxs("label", {
957
+ className: ["bk-radio-label", className].filter(Boolean).join(" "),
958
+ style,
959
+ children: [input, children]
960
+ });
961
+ };
962
+ RadioGroupItem.displayName = "RadioGroup.Item";
963
+ //#endregion
964
+ //#region src/radio/index.ts
965
+ const RadioGroup = {
966
+ Root: RadioGroupRoot,
967
+ Item: RadioGroupItem
968
+ };
969
+ //#endregion
970
+ //#region src/multi-select/use-multi-select-root.ts
971
+ const useMultiSelectRoot = (options = {}) => {
972
+ const [value, setValue] = useControllableState({
973
+ prop: options.value,
974
+ defaultProp: options.defaultValue ?? [],
975
+ onChange: options.onValueChange
976
+ });
977
+ const [open, setOpen] = useControllableState({
978
+ prop: options.open,
979
+ defaultProp: options.defaultOpen,
980
+ onChange: options.onOpenChange
981
+ });
982
+ const { getItems, registerItem } = useCollection();
983
+ const [activeItem, setActiveItem] = useState(void 0);
984
+ const [triggerElement, setTriggerElement] = useState(null);
985
+ const triggerId = useId();
986
+ const listboxId = useId();
987
+ const toggleValue = useCallback((v) => setValue((prev = []) => prev.includes(v) ? prev.filter((x) => x !== v) : [...prev, v]), [setValue]);
988
+ return useMemo(() => ({
989
+ open: open ?? false,
990
+ setOpen,
991
+ activeItem,
992
+ setActiveItem,
993
+ value: value ?? [],
994
+ setValue,
995
+ triggerElement,
996
+ setTriggerElement,
997
+ listboxId,
998
+ triggerId,
999
+ getItems,
1000
+ registerItem,
1001
+ toggleValue,
1002
+ disabled: options.disabled ?? false,
1003
+ size: options.size ?? "md"
1004
+ }), [
1005
+ activeItem,
1006
+ getItems,
1007
+ listboxId,
1008
+ open,
1009
+ options.disabled,
1010
+ options.size,
1011
+ registerItem,
1012
+ setOpen,
1013
+ setValue,
1014
+ toggleValue,
1015
+ triggerElement,
1016
+ triggerId,
1017
+ value
1018
+ ]);
1019
+ };
1020
+ //#endregion
1021
+ //#region src/multi-select/context.ts
1022
+ const MultiSelectContext = createContext(null);
1023
+ const useMultiSelectContext = () => {
1024
+ const context = useContext(MultiSelectContext);
1025
+ if (!context) throw new Error("[blankjs] MultiSelect.* must be used within MultiSelect.Root");
1026
+ return context;
1027
+ };
1028
+ //#endregion
1029
+ //#region src/multi-select/hidden-input.tsx
1030
+ const MultiSelectHiddenInput = ({ name, defaultValue, required }) => {
1031
+ const { value, setValue, disabled, triggerElement } = useMultiSelectContext();
1032
+ const ref = useRef(null);
1033
+ const controlProps = useFieldControlProps$1();
1034
+ const isRequired = required ?? controlProps.required ?? false;
1035
+ const defaultValueRef = useRef(defaultValue);
1036
+ useEffect(() => {
1037
+ defaultValueRef.current = defaultValue;
1038
+ }, [defaultValue]);
1039
+ const mountedRef = useRef(false);
1040
+ useEffect(() => {
1041
+ if (!mountedRef.current) {
1042
+ mountedRef.current = true;
1043
+ return;
1044
+ }
1045
+ ref.current?.dispatchEvent(new Event("change", { bubbles: true }));
1046
+ }, [value]);
1047
+ useEffect(() => {
1048
+ const form = ref.current?.form;
1049
+ if (!form) return;
1050
+ const onReset = () => setValue(defaultValueRef.current ?? []);
1051
+ form.addEventListener("reset", onReset);
1052
+ return () => form.removeEventListener("reset", onReset);
1053
+ }, [setValue]);
1054
+ return /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx("input", {
1055
+ ref,
1056
+ type: "text",
1057
+ value: value.join(","),
1058
+ disabled,
1059
+ className: "bk-multi-select-hidden-input",
1060
+ tabIndex: -1,
1061
+ "aria-hidden": "true",
1062
+ autoComplete: "off",
1063
+ onChange: () => {},
1064
+ required: isRequired,
1065
+ onFocus: () => triggerElement?.focus()
1066
+ }), value.map((v) => /* @__PURE__ */ jsx("input", {
1067
+ type: "hidden",
1068
+ name,
1069
+ value: v,
1070
+ disabled,
1071
+ readOnly: true
1072
+ }, v))] });
1073
+ };
1074
+ MultiSelectHiddenInput.displayName = "MultiSelect.HiddenInput";
1075
+ //#endregion
1076
+ //#region src/multi-select/root.tsx
1077
+ const MultiSelectRoot = ({ children, required, ...options }) => {
1078
+ const contextValue = useMultiSelectRoot(options);
1079
+ return /* @__PURE__ */ jsx(MultiSelectContext, {
1080
+ value: contextValue,
1081
+ children: /* @__PURE__ */ jsxs("div", {
1082
+ className: "bk-multi-select-root",
1083
+ "data-size": contextValue.size,
1084
+ children: [children, options.name && /* @__PURE__ */ jsx(MultiSelectHiddenInput, {
1085
+ name: options.name,
1086
+ defaultValue: options.defaultValue,
1087
+ required
1088
+ })]
1089
+ })
1090
+ });
1091
+ };
1092
+ MultiSelectRoot.displayName = "MultiSelect.Root";
1093
+ //#endregion
1094
+ //#region src/multi-select/trigger.tsx
1095
+ const MultiSelectArrow = () => /* @__PURE__ */ jsx("svg", {
1096
+ className: "bk-multi-select-arrow",
1097
+ width: "16",
1098
+ height: "16",
1099
+ viewBox: "0 0 24 24",
1100
+ fill: "none",
1101
+ stroke: "currentColor",
1102
+ strokeWidth: "2",
1103
+ "aria-hidden": "true",
1104
+ children: /* @__PURE__ */ jsx("path", { d: "M6 9l6 6 6-6" })
1105
+ });
1106
+ const MultiSelectTrigger = ({ asChild, children, className, ...props }) => {
1107
+ const { required: _required, ...fieldProps } = useFieldControlProps$1();
1108
+ const { open, setOpen, triggerId, listboxId, disabled, activeItem, setTriggerElement, getItems, setActiveItem, toggleValue } = useMultiSelectContext();
1109
+ const isDisabled = fieldProps.disabled || disabled;
1110
+ const id = fieldProps.id ?? triggerId;
1111
+ const activeDescendant = open ? activeItem?.id : void 0;
1112
+ const onListboxKeyDown = useListboxKeyboard({
1113
+ activeItem,
1114
+ setActiveItem,
1115
+ getItems,
1116
+ open,
1117
+ setOpen,
1118
+ onCommit: (item) => toggleValue(item.value)
1119
+ });
1120
+ const triggerProps = {
1121
+ type: "button",
1122
+ ...props,
1123
+ ...fieldProps,
1124
+ className: ["bk-multi-select-trigger", className].filter(Boolean).join(" "),
1125
+ id,
1126
+ role: "combobox",
1127
+ "aria-disabled": isDisabled || void 0,
1128
+ "aria-haspopup": "listbox",
1129
+ "aria-expanded": open,
1130
+ "aria-controls": open ? listboxId : void 0,
1131
+ "aria-activedescendant": activeDescendant,
1132
+ disabled: isDisabled,
1133
+ onKeyDown: (e) => {
1134
+ if (isDisabled) return;
1135
+ props.onKeyDown?.(e);
1136
+ onListboxKeyDown(e);
1137
+ },
1138
+ onClick: (e) => {
1139
+ if (isDisabled) return;
1140
+ props.onClick?.(e);
1141
+ setOpen((o) => !o);
1142
+ }
1143
+ };
1144
+ if (asChild) return /* @__PURE__ */ jsx(Slot, {
1145
+ ...triggerProps,
1146
+ ref: composeRefs(setTriggerElement, props.ref),
1147
+ children
1148
+ });
1149
+ return /* @__PURE__ */ jsxs("button", {
1150
+ ...triggerProps,
1151
+ ref: composeRefs(setTriggerElement, props.ref),
1152
+ children: [children, /* @__PURE__ */ jsx(MultiSelectArrow, {})]
1153
+ });
1154
+ };
1155
+ MultiSelectTrigger.displayName = "MultiSelect.Trigger";
1156
+ //#endregion
1157
+ //#region src/multi-select/content.tsx
1158
+ const MultiSelectContentInner = ({ children, style, className, container, ...props }) => {
1159
+ const { triggerElement, listboxId, setOpen, getItems, value, setActiveItem, size } = useMultiSelectContext();
1160
+ const { setFloating, floatingStyles } = usePopover({
1161
+ anchor: triggerElement,
1162
+ onDismiss: useCallback(() => setOpen(false), [setOpen])
1163
+ });
1164
+ if (!useInitialActiveItem({
1165
+ getItems,
1166
+ setActiveItem,
1167
+ isSelected: (item) => value.includes(item.value)
1168
+ })) return null;
1169
+ const target = container ?? document.body;
1170
+ return createPortal(/* @__PURE__ */ jsx("div", {
1171
+ ...props,
1172
+ ref: setFloating,
1173
+ style: {
1174
+ ...style,
1175
+ ...floatingStyles
1176
+ },
1177
+ className: ["bk-multi-select-content", className].filter(Boolean).join(" "),
1178
+ role: "listbox",
1179
+ "aria-multiselectable": "true",
1180
+ id: listboxId,
1181
+ "data-size": size,
1182
+ children
1183
+ }), target);
1184
+ };
1185
+ const MultiSelectContent = ({ children, ...props }) => {
1186
+ const { open } = useMultiSelectContext();
1187
+ if (!open) return null;
1188
+ return /* @__PURE__ */ jsx(MultiSelectContentInner, {
1189
+ ...props,
1190
+ children
1191
+ });
1192
+ };
1193
+ MultiSelectContent.displayName = "MultiSelect.Content";
1194
+ //#endregion
1195
+ //#region src/multi-select/item.tsx
1196
+ const MultiSelectCheck = () => /* @__PURE__ */ jsx("svg", {
1197
+ className: "bk-multi-select-check",
1198
+ width: "16",
1199
+ height: "16",
1200
+ viewBox: "0 0 24 24",
1201
+ fill: "none",
1202
+ stroke: "currentColor",
1203
+ strokeWidth: "2",
1204
+ "aria-hidden": "true",
1205
+ children: /* @__PURE__ */ jsx("path", { d: "M20 6L9 17l-5-5" })
1206
+ });
1207
+ const MultiSelectItem = ({ value, textValue, children, className, ...props }) => {
1208
+ const ref = useRef(null);
1209
+ const optionId = useId();
1210
+ const { value: selectedValue, activeItem, registerItem, setActiveItem, toggleValue } = useMultiSelectContext();
1211
+ const isSelected = selectedValue.includes(value);
1212
+ const isActive = value === activeItem?.value;
1213
+ useEffect(() => {
1214
+ const node = ref.current;
1215
+ if (!node) return;
1216
+ const unregister = registerItem({
1217
+ node,
1218
+ value,
1219
+ label: textValue ?? node.textContent ?? "",
1220
+ id: optionId
1221
+ });
1222
+ return () => {
1223
+ unregister();
1224
+ setActiveItem((prev) => prev?.value === value ? void 0 : prev);
1225
+ };
1226
+ }, [
1227
+ registerItem,
1228
+ textValue,
1229
+ value,
1230
+ optionId,
1231
+ setActiveItem
1232
+ ]);
1233
+ useEffect(() => {
1234
+ if (isActive) ref.current?.scrollIntoView({ block: "nearest" });
1235
+ }, [isActive]);
1236
+ return /* @__PURE__ */ jsxs("div", {
1237
+ ...props,
1238
+ ref,
1239
+ className: ["bk-multi-select-item", className].filter(Boolean).join(" "),
1240
+ role: "option",
1241
+ id: optionId,
1242
+ "aria-selected": isSelected,
1243
+ "data-active": isActive ? "" : void 0,
1244
+ onClick: (e) => {
1245
+ props.onClick?.(e);
1246
+ if (e.defaultPrevented) return;
1247
+ toggleValue(value);
1248
+ },
1249
+ children: [children, /* @__PURE__ */ jsx(MultiSelectCheck, {})]
1250
+ });
1251
+ };
1252
+ MultiSelectItem.displayName = "MultiSelect.Item";
1253
+ //#endregion
1254
+ //#region src/multi-select/value.tsx
1255
+ const MultiSelectValue = ({ placeholder, children }) => {
1256
+ const { value } = useMultiSelectContext();
1257
+ const isValueEmpty = value.length === 0;
1258
+ const getValue = () => {
1259
+ if (isValueEmpty) return placeholder;
1260
+ if (children) return children(value);
1261
+ return value.join(", ");
1262
+ };
1263
+ return /* @__PURE__ */ jsx("span", {
1264
+ className: "bk-multi-select-value",
1265
+ "data-placeholder": isValueEmpty ? "" : void 0,
1266
+ children: getValue()
1267
+ });
1268
+ };
1269
+ MultiSelectValue.displayName = "MultiSelect.Value";
1270
+ //#endregion
1271
+ //#region src/multi-select/clear.tsx
1272
+ const MultiSelectClearIcon = () => /* @__PURE__ */ jsx("svg", {
1273
+ width: "14",
1274
+ height: "14",
1275
+ viewBox: "0 0 24 24",
1276
+ fill: "none",
1277
+ stroke: "currentColor",
1278
+ strokeWidth: "2",
1279
+ "aria-hidden": "true",
1280
+ children: /* @__PURE__ */ jsx("path", { d: "M18 6L6 18M6 6l12 12" })
1281
+ });
1282
+ const MultiSelectClear = ({ className, children, onClick, ...props }) => {
1283
+ const { value, setValue, disabled, triggerElement } = useMultiSelectContext();
1284
+ if (!value.length || disabled) return null;
1285
+ const handleClick = (e) => {
1286
+ onClick?.(e);
1287
+ if (e.defaultPrevented) return;
1288
+ setValue([]);
1289
+ triggerElement?.focus();
1290
+ };
1291
+ return /* @__PURE__ */ jsx("button", {
1292
+ type: "button",
1293
+ "aria-label": "Clear",
1294
+ className: ["bk-multi-select-clear", className].filter(Boolean).join(" "),
1295
+ onClick: handleClick,
1296
+ ...props,
1297
+ children: children ?? /* @__PURE__ */ jsx(MultiSelectClearIcon, {})
1298
+ });
1299
+ };
1300
+ MultiSelectClear.displayName = "MultiSelect.Clear";
1301
+ //#endregion
1302
+ //#region src/multi-select/index.ts
1303
+ const MultiSelect = {
1304
+ Root: MultiSelectRoot,
1305
+ Trigger: MultiSelectTrigger,
1306
+ Content: MultiSelectContent,
1307
+ Item: MultiSelectItem,
1308
+ Value: MultiSelectValue,
1309
+ Clear: MultiSelectClear
1310
+ };
1311
+ //#endregion
1312
+ //#region src/combobox/use-combobox-root.ts
1313
+ const useComboboxRoot = (options = {}) => {
1314
+ const [value, setValue] = useControllableState({
1315
+ prop: options.value,
1316
+ defaultProp: options.defaultValue,
1317
+ onChange: options.onValueChange
1318
+ });
1319
+ const [open, setOpen] = useControllableState({
1320
+ prop: options.open,
1321
+ defaultProp: options.defaultOpen,
1322
+ onChange: options.onOpenChange
1323
+ });
1324
+ const [inputValue, setInputValue] = useControllableState({
1325
+ prop: options.inputValue,
1326
+ defaultProp: options.defaultInputValue ?? "",
1327
+ onChange: options.onInputValueChange
1328
+ });
1329
+ const committedLabelRef = useRef(options.defaultInputValue ?? "");
1330
+ const { getItems, registerItem } = useCollection();
1331
+ const [activeItem, setActiveItem] = useState();
1332
+ const [inputGroupElement, setInputGroupElement] = useState(null);
1333
+ const inputId = useId();
1334
+ const listboxId = useId();
1335
+ const revertInputValue = useCallback(() => {
1336
+ setInputValue(committedLabelRef.current);
1337
+ }, [setInputValue]);
1338
+ const commitItem = useCallback((item) => {
1339
+ setValue(item.value);
1340
+ setInputValue(item.label);
1341
+ setOpen(false);
1342
+ committedLabelRef.current = item.label;
1343
+ }, [
1344
+ setInputValue,
1345
+ setOpen,
1346
+ setValue
1347
+ ]);
1348
+ const resetToDefault = useCallback(() => {
1349
+ const defaultInputValue = options.defaultInputValue ?? "";
1350
+ setInputValue(defaultInputValue);
1351
+ setValue(options.defaultValue);
1352
+ setOpen(false);
1353
+ committedLabelRef.current = defaultInputValue;
1354
+ }, [
1355
+ options.defaultInputValue,
1356
+ options.defaultValue,
1357
+ setInputValue,
1358
+ setOpen,
1359
+ setValue
1360
+ ]);
1361
+ const clear = useCallback(() => {
1362
+ setValue(void 0);
1363
+ setInputValue("");
1364
+ committedLabelRef.current = "";
1365
+ }, [setInputValue, setValue]);
1366
+ return useMemo(() => ({
1367
+ open: open ?? false,
1368
+ setOpen,
1369
+ value,
1370
+ setValue,
1371
+ inputValue: inputValue ?? "",
1372
+ setInputValue,
1373
+ activeItem,
1374
+ setActiveItem,
1375
+ commitItem,
1376
+ registerItem,
1377
+ getItems,
1378
+ inputGroupElement,
1379
+ setInputGroupElement,
1380
+ inputId,
1381
+ listboxId,
1382
+ disabled: options.disabled ?? false,
1383
+ revertInputValue,
1384
+ resetToDefault,
1385
+ clear,
1386
+ size: options.size ?? "md"
1387
+ }), [
1388
+ activeItem,
1389
+ clear,
1390
+ options.size,
1391
+ commitItem,
1392
+ getItems,
1393
+ inputGroupElement,
1394
+ inputId,
1395
+ inputValue,
1396
+ listboxId,
1397
+ open,
1398
+ options.disabled,
1399
+ registerItem,
1400
+ resetToDefault,
1401
+ revertInputValue,
1402
+ setInputValue,
1403
+ setOpen,
1404
+ setValue,
1405
+ value
1406
+ ]);
1407
+ };
1408
+ //#endregion
1409
+ //#region src/combobox/context.ts
1410
+ const ComboboxContext = createContext(null);
1411
+ const useComboboxContext = () => {
1412
+ const context = useContext(ComboboxContext);
1413
+ if (!context) throw new Error("[blankjs] Combobox.* must be used within Combobox.Root");
1414
+ return context;
1415
+ };
1416
+ //#endregion
1417
+ //#region src/combobox/hidden-input.tsx
1418
+ const ComboboxHiddenInput = ({ name, required }) => {
1419
+ const { value, resetToDefault, disabled, inputGroupElement } = useComboboxContext();
1420
+ const ref = useRef(null);
1421
+ const controlProps = useFieldControlProps$1();
1422
+ const isRequired = required ?? controlProps.required ?? false;
1423
+ const mountedRef = useRef(false);
1424
+ useEffect(() => {
1425
+ if (!mountedRef.current) {
1426
+ mountedRef.current = true;
1427
+ return;
1428
+ }
1429
+ ref.current?.dispatchEvent(new Event("change", { bubbles: true }));
1430
+ }, [value]);
1431
+ useEffect(() => {
1432
+ const form = ref.current?.form;
1433
+ if (!form) return;
1434
+ form.addEventListener("reset", resetToDefault);
1435
+ return () => form.removeEventListener("reset", resetToDefault);
1436
+ }, [resetToDefault]);
1437
+ return /* @__PURE__ */ jsx("input", {
1438
+ ref,
1439
+ type: "text",
1440
+ name,
1441
+ value: value ?? "",
1442
+ disabled,
1443
+ className: "bk-combobox-hidden-input",
1444
+ tabIndex: -1,
1445
+ "aria-hidden": "true",
1446
+ autoComplete: "off",
1447
+ onChange: () => {},
1448
+ required: isRequired,
1449
+ onFocus: () => inputGroupElement?.querySelector("input")?.focus()
1450
+ });
1451
+ };
1452
+ ComboboxHiddenInput.displayName = "Combobox.HiddenInput";
1453
+ //#endregion
1454
+ //#region src/combobox/root.tsx
1455
+ const ComboboxRoot = ({ children, required, ...options }) => {
1456
+ const contextValue = useComboboxRoot(options);
1457
+ return /* @__PURE__ */ jsx(ComboboxContext, {
1458
+ value: contextValue,
1459
+ children: /* @__PURE__ */ jsxs("div", {
1460
+ className: "bk-combobox-root",
1461
+ "data-size": contextValue.size,
1462
+ children: [children, options.name && /* @__PURE__ */ jsx(ComboboxHiddenInput, {
1463
+ name: options.name,
1464
+ required
1465
+ })]
1466
+ })
1467
+ });
1468
+ };
1469
+ ComboboxRoot.displayName = "Combobox.Root";
1470
+ //#endregion
1471
+ //#region src/combobox/input.tsx
1472
+ const ComboboxArrow = () => /* @__PURE__ */ jsx("svg", {
1473
+ className: "bk-combobox-arrow",
1474
+ width: "16",
1475
+ height: "16",
1476
+ viewBox: "0 0 24 24",
1477
+ fill: "none",
1478
+ stroke: "currentColor",
1479
+ strokeWidth: "2",
1480
+ "aria-hidden": "true",
1481
+ children: /* @__PURE__ */ jsx("path", { d: "M6 9l6 6 6-6" })
1482
+ });
1483
+ const ComboboxInput = ({ className, ...props }) => {
1484
+ const { inputId, disabled, open, activeItem, inputValue, listboxId, setInputValue, setInputGroupElement, getItems, setActiveItem, setOpen, commitItem, revertInputValue } = useComboboxContext();
1485
+ const { required: _required, ...fieldProps } = useFieldControlProps$1();
1486
+ const id = fieldProps.id ?? inputId;
1487
+ const isDisabled = fieldProps.disabled || props.disabled || disabled;
1488
+ const activeDescendant = open ? activeItem?.id : void 0;
1489
+ const onBlur = (e) => {
1490
+ if (isDisabled) return;
1491
+ props.onBlur?.(e);
1492
+ if (e.defaultPrevented) return;
1493
+ revertInputValue();
1494
+ setOpen(false);
1495
+ };
1496
+ const onChange = (e) => {
1497
+ if (isDisabled) return;
1498
+ props.onChange?.(e);
1499
+ if (e.defaultPrevented) return;
1500
+ setInputValue(e.target.value);
1501
+ setOpen(true);
1502
+ };
1503
+ const onKeyDown = (e) => {
1504
+ if (isDisabled) return;
1505
+ props.onKeyDown?.(e);
1506
+ if (e.defaultPrevented) return;
1507
+ const key = e.key;
1508
+ if (!open) {
1509
+ switch (key) {
1510
+ case "ArrowDown":
1511
+ case "ArrowUp":
1512
+ e.preventDefault();
1513
+ setOpen(true);
1514
+ break;
1515
+ default: break;
1516
+ }
1517
+ return;
1518
+ }
1519
+ const items = getItems();
1520
+ const index = items.findIndex((item) => item.value === activeItem?.value);
1521
+ switch (key) {
1522
+ case "ArrowDown": {
1523
+ e.preventDefault();
1524
+ const next = items[Math.min(index + 1, items.length - 1)];
1525
+ if (next) setActiveItem(next);
1526
+ break;
1527
+ }
1528
+ case "ArrowUp": {
1529
+ e.preventDefault();
1530
+ const prev = items[Math.max(index - 1, 0)];
1531
+ if (prev) setActiveItem(prev);
1532
+ break;
1533
+ }
1534
+ case "Enter":
1535
+ e.preventDefault();
1536
+ if (activeItem) commitItem(activeItem);
1537
+ break;
1538
+ case "Tab":
1539
+ setOpen(false);
1540
+ break;
1541
+ default: break;
1542
+ }
1543
+ };
1544
+ const onClick = (e) => {
1545
+ if (isDisabled) return;
1546
+ props.onClick?.(e);
1547
+ if (e.defaultPrevented) return;
1548
+ setOpen(true);
1549
+ };
1550
+ return /* @__PURE__ */ jsxs("div", {
1551
+ className: "bk-combobox-group",
1552
+ ref: setInputGroupElement,
1553
+ children: [/* @__PURE__ */ jsx("input", {
1554
+ ...props,
1555
+ ...fieldProps,
1556
+ value: inputValue,
1557
+ onClick,
1558
+ onKeyDown,
1559
+ onChange,
1560
+ onBlur,
1561
+ className: ["bk-combobox-input", className].filter(Boolean).join(" "),
1562
+ role: "combobox",
1563
+ "aria-autocomplete": "list",
1564
+ "aria-expanded": open,
1565
+ "aria-controls": open ? listboxId : void 0,
1566
+ "aria-activedescendant": activeDescendant,
1567
+ disabled: isDisabled,
1568
+ id
1569
+ }), /* @__PURE__ */ jsx(ComboboxArrow, {})]
1570
+ });
1571
+ };
1572
+ ComboboxInput.displayName = "Combobox.Input";
1573
+ //#endregion
1574
+ //#region src/combobox/content.tsx
1575
+ const ComboboxContentInner = ({ children, style, className, container, ...props }) => {
1576
+ const { inputGroupElement, listboxId, setOpen, getItems, value, setActiveItem, revertInputValue, size } = useComboboxContext();
1577
+ const { setFloating, floatingStyles } = usePopover({
1578
+ anchor: inputGroupElement,
1579
+ onDismiss: useCallback((reason) => {
1580
+ if (reason === "escape") revertInputValue();
1581
+ setOpen(false);
1582
+ }, [revertInputValue, setOpen]),
1583
+ matchWidth: "min"
1584
+ });
1585
+ const onPointerDown = (e) => {
1586
+ props.onPointerDown?.(e);
1587
+ if (e.defaultPrevented) return;
1588
+ e.preventDefault();
1589
+ };
1590
+ if (!useInitialActiveItem({
1591
+ getItems,
1592
+ setActiveItem,
1593
+ isSelected: (item) => item.value === value
1594
+ })) return null;
1595
+ const target = container ?? document.body;
1596
+ return createPortal(/* @__PURE__ */ jsx("div", {
1597
+ ...props,
1598
+ onPointerDown,
1599
+ ref: setFloating,
1600
+ style: {
1601
+ ...style,
1602
+ ...floatingStyles
1603
+ },
1604
+ className: ["bk-combobox-content", className].filter(Boolean).join(" "),
1605
+ role: "listbox",
1606
+ id: listboxId,
1607
+ "data-size": size,
1608
+ "data-empty": Children.count(children) === 0 ? "" : void 0,
1609
+ children
1610
+ }), target);
1611
+ };
1612
+ const ComboboxContent = ({ children, ...props }) => {
1613
+ const { open } = useComboboxContext();
1614
+ if (!open) return null;
1615
+ return /* @__PURE__ */ jsx(ComboboxContentInner, {
1616
+ ...props,
1617
+ children
1618
+ });
1619
+ };
1620
+ ComboboxContent.displayName = "Combobox.Content";
1621
+ //#endregion
1622
+ //#region src/combobox/item.tsx
1623
+ const ComboboxCheck = () => /* @__PURE__ */ jsx("svg", {
1624
+ className: "bk-combobox-check",
1625
+ width: "16",
1626
+ height: "16",
1627
+ viewBox: "0 0 24 24",
1628
+ fill: "none",
1629
+ stroke: "currentColor",
1630
+ strokeWidth: "2",
1631
+ "aria-hidden": "true",
1632
+ children: /* @__PURE__ */ jsx("path", { d: "M20 6L9 17l-5-5" })
1633
+ });
1634
+ const ComboboxItem = ({ value, textValue, children, className, ...props }) => {
1635
+ const ref = useRef(null);
1636
+ const optionId = useId();
1637
+ const { value: selectedValue, activeItem, registerItem, commitItem, setActiveItem } = useComboboxContext();
1638
+ const isSelected = value === selectedValue;
1639
+ const isActive = value === activeItem?.value;
1640
+ useEffect(() => {
1641
+ const node = ref.current;
1642
+ if (!node) return;
1643
+ const unregister = registerItem({
1644
+ node,
1645
+ value,
1646
+ label: textValue ?? node.textContent ?? "",
1647
+ id: optionId
1648
+ });
1649
+ return () => {
1650
+ unregister();
1651
+ setActiveItem((prev) => prev?.value === value ? void 0 : prev);
1652
+ };
1653
+ }, [
1654
+ registerItem,
1655
+ textValue,
1656
+ value,
1657
+ optionId,
1658
+ setActiveItem
1659
+ ]);
1660
+ useEffect(() => {
1661
+ if (isActive) ref.current?.scrollIntoView({ block: "nearest" });
1662
+ }, [isActive]);
1663
+ const onPointerDown = (e) => {
1664
+ props.onPointerDown?.(e);
1665
+ if (e.defaultPrevented) return;
1666
+ e.preventDefault();
1667
+ const node = ref.current;
1668
+ if (!node) return;
1669
+ commitItem({
1670
+ id: optionId,
1671
+ value,
1672
+ label: textValue ?? node.textContent ?? "",
1673
+ node
1674
+ });
1675
+ };
1676
+ return /* @__PURE__ */ jsxs("div", {
1677
+ ...props,
1678
+ ref,
1679
+ className: ["bk-combobox-item", className].filter(Boolean).join(" "),
1680
+ role: "option",
1681
+ id: optionId,
1682
+ "aria-selected": isSelected,
1683
+ "data-active": isActive ? "" : void 0,
1684
+ onPointerDown,
1685
+ children: [children, /* @__PURE__ */ jsx(ComboboxCheck, {})]
1686
+ });
1687
+ };
1688
+ ComboboxItem.displayName = "Combobox.Item";
1689
+ //#endregion
1690
+ //#region src/combobox/clear.tsx
1691
+ const ComboboxClearIcon = () => /* @__PURE__ */ jsx("svg", {
1692
+ width: "14",
1693
+ height: "14",
1694
+ viewBox: "0 0 24 24",
1695
+ fill: "none",
1696
+ stroke: "currentColor",
1697
+ strokeWidth: "2",
1698
+ "aria-hidden": "true",
1699
+ children: /* @__PURE__ */ jsx("path", { d: "M18 6L6 18M6 6l12 12" })
1700
+ });
1701
+ const ComboboxClear = ({ children, className, onClick, ...props }) => {
1702
+ const { value, disabled, clear, inputGroupElement } = useComboboxContext();
1703
+ if (!value || disabled) return null;
1704
+ const handleClick = (e) => {
1705
+ onClick?.(e);
1706
+ if (e.defaultPrevented) return;
1707
+ clear();
1708
+ inputGroupElement?.querySelector("input")?.focus();
1709
+ };
1710
+ return /* @__PURE__ */ jsx("button", {
1711
+ type: "button",
1712
+ "aria-label": "Clear",
1713
+ className: ["bk-combobox-clear", className].filter(Boolean).join(" "),
1714
+ onClick: handleClick,
1715
+ ...props,
1716
+ children: children ?? /* @__PURE__ */ jsx(ComboboxClearIcon, {})
1717
+ });
1718
+ };
1719
+ ComboboxClear.displayName = "Combobox.Clear";
1720
+ //#endregion
1721
+ //#region src/combobox/index.ts
1722
+ const Combobox = {
1723
+ Root: ComboboxRoot,
1724
+ Input: ComboboxInput,
1725
+ Content: ComboboxContent,
1726
+ Item: ComboboxItem,
1727
+ Clear: ComboboxClear
1728
+ };
1729
+ //#endregion
1730
+ export { Button, Checkbox, Combobox, ComboboxClear, ComboboxContent, ComboboxInput, ComboboxItem, ComboboxRoot, Field, FieldControl, FieldDescription, FieldError, FieldLabel, FieldRoot, Form, FormContext, MultiSelect, MultiSelectClear, MultiSelectContent, MultiSelectItem, MultiSelectRoot, MultiSelectTrigger, MultiSelectValue, PasswordField, RadioGroup, RadioGroupItem, RadioGroupRoot, Select, SelectClear, SelectContent, SelectItem, SelectRoot, SelectTrigger, SelectValue, Switch, TextInput, Textarea, serialize, useFieldControlProps };
1731
+
1732
+ //# sourceMappingURL=index.mjs.map