gbs-add-block 1.2.8 → 1.2.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,198 @@
1
+ "use client";
2
+
3
+ import { useId, useImperativeHandle, useMemo, useRef, type Ref } from "react";
4
+ import type { ComboboxOption, OptionValue } from "../core/types";
5
+ import { defaultComboboxText } from "./locale";
6
+ import { ChevronIcon, XIcon } from "./icons";
7
+ import { Listbox } from "./Listbox";
8
+ import { ComboboxPopover } from "./Popover";
9
+ import { cx, type ComboboxHandle, type ComboboxSharedProps } from "./props";
10
+ import { useCombobox } from "./useCombobox";
11
+
12
+ export interface SelectProps<V extends OptionValue = string> extends ComboboxSharedProps<V> {
13
+ value?: V | null;
14
+ defaultValue?: V | null;
15
+ onChange?(value: NoInfer<V> | null, option: ComboboxOption<NoInfer<V>> | null): void;
16
+ /** Keep the list open after choosing. Default false. */
17
+ closeOnSelect?: boolean;
18
+ ref?: Ref<ComboboxHandle<V>>;
19
+ }
20
+
21
+ /** Single-select combobox: a value picker with option search. */
22
+ export function Select<V extends OptionValue = string>(props: SelectProps<V>) {
23
+ const {
24
+ ref,
25
+ value,
26
+ defaultValue,
27
+ onChange,
28
+ closeOnSelect = true,
29
+ options,
30
+ label,
31
+ placeholder = "Select…",
32
+ description,
33
+ error,
34
+ required = false,
35
+ disabled = false,
36
+ clearable = true,
37
+ size = "md",
38
+ name,
39
+ id: idProp,
40
+ className,
41
+ classNames,
42
+ style,
43
+ localeText,
44
+ maxHeight = 280,
45
+ virtualize = true,
46
+ emptyMessage,
47
+ searchable = true,
48
+ loading = false,
49
+ hasMore,
50
+ onLoadMore,
51
+ renderOption,
52
+ } = props;
53
+
54
+ const reactId = useId();
55
+ const id = idProp ?? reactId;
56
+ const text = useMemo(() => ({ ...defaultComboboxText, ...localeText }), [localeText]);
57
+
58
+ const controlRef = useRef<HTMLDivElement>(null);
59
+ const searchRef = useRef<HTMLInputElement>(null);
60
+ const listRef = useRef<HTMLDivElement>(null);
61
+
62
+ const combobox = useCombobox<V>({
63
+ controlRef,
64
+ searchRef,
65
+ listRef,
66
+ ...props,
67
+ multiple: false,
68
+ closeOnSelect,
69
+ value: value === undefined ? undefined : value === null ? [] : [value],
70
+ defaultValue: defaultValue === null || defaultValue === undefined ? [] : [defaultValue],
71
+ onValuesChange: (values, selectedOptions) =>
72
+ onChange?.(values[0] ?? null, selectedOptions[0] ?? null),
73
+ text,
74
+ id,
75
+ });
76
+
77
+ const selected = combobox.selectedOptions[0];
78
+
79
+ useImperativeHandle(ref, () => combobox.handle, [combobox.handle]);
80
+
81
+ return (
82
+ <div
83
+ className={cx("cb-root", classNames?.root, className)}
84
+ style={style}
85
+ data-size={size}
86
+ data-state={combobox.open ? "open" : "closed"}
87
+ data-invalid={error ? "" : undefined}
88
+ >
89
+ {label && (
90
+ <span id={`${id}-label`} className={cx("cb-label", classNames?.label)} data-required={required || undefined}>
91
+ {label}
92
+ </span>
93
+ )}
94
+
95
+ <div
96
+ ref={controlRef}
97
+ id={id}
98
+ role="combobox"
99
+ tabIndex={disabled ? -1 : 0}
100
+ aria-expanded={combobox.open}
101
+ aria-haspopup="listbox"
102
+ aria-controls={combobox.open ? combobox.listboxId : undefined}
103
+ aria-labelledby={label ? `${id}-label` : undefined}
104
+ aria-describedby={cx(description ? `${id}-description` : "", error ? `${id}-error` : "") || undefined}
105
+ aria-required={required || undefined}
106
+ aria-invalid={error ? true : undefined}
107
+ aria-disabled={disabled || undefined}
108
+ className={cx("cb-control", classNames?.control)}
109
+ data-disabled={disabled || undefined}
110
+ data-invalid={error ? "" : undefined}
111
+ onPointerDown={combobox.onControlPointerDown}
112
+ onClick={combobox.onControlClick}
113
+ onKeyDown={combobox.onKeyDown}
114
+ >
115
+ <span className={cx("cb-value", classNames?.value)}>
116
+ {selected ? (
117
+ <>
118
+ {selected.icon && <span className="cb-value-icon">{selected.icon}</span>}
119
+ {selected.label}
120
+ </>
121
+ ) : (
122
+ <span className="cb-placeholder">{placeholder}</span>
123
+ )}
124
+ </span>
125
+
126
+ <span className="cb-actions">
127
+ {clearable && selected && !disabled && (
128
+ <button
129
+ type="button"
130
+ tabIndex={-1}
131
+ className="cb-icon-button"
132
+ aria-label={text.clear}
133
+ onClick={(event) => {
134
+ event.stopPropagation();
135
+ combobox.clear();
136
+ }}
137
+ >
138
+ <XIcon width={14} height={14} />
139
+ </button>
140
+ )}
141
+ <ChevronIcon className="cb-chevron" />
142
+ </span>
143
+ </div>
144
+
145
+ {name && <input type="hidden" name={name} value={selected?.value ?? ""} />}
146
+ {description && (
147
+ <span id={`${id}-description`} className="cb-description">
148
+ {description}
149
+ </span>
150
+ )}
151
+ {error && (
152
+ <span id={`${id}-error`} className="cb-error" role="alert">
153
+ {error}
154
+ </span>
155
+ )}
156
+
157
+ {combobox.anchor && (
158
+ <ComboboxPopover
159
+ anchor={combobox.anchor}
160
+ onClose={combobox.onPopoverClose}
161
+ className={classNames?.popover}
162
+ >
163
+ <Listbox<V>
164
+ listboxId={combobox.listboxId}
165
+ labelledBy={label ? `${id}-label` : id}
166
+ items={combobox.items}
167
+ optionCount={combobox.entries.length}
168
+ activeIndex={combobox.activeIndex}
169
+ multiple={false}
170
+ size={size}
171
+ maxHeight={maxHeight}
172
+ virtualize={virtualize}
173
+ loading={loading}
174
+ hasMore={hasMore}
175
+ searchable={searchable}
176
+ search={combobox.search}
177
+ searchRef={searchRef}
178
+ listRef={listRef}
179
+ text={text}
180
+ classNames={classNames}
181
+ emptyMessage={emptyMessage}
182
+ createLabel={combobox.createLabel}
183
+ getOptionId={combobox.getOptionId}
184
+ isSelected={combobox.isSelected}
185
+ onSelect={combobox.selectOption}
186
+ onActiveIndexChange={combobox.setActiveIndex}
187
+ onSearchChange={combobox.setSearch}
188
+ onKeyDown={combobox.onKeyDown}
189
+ onLoadMore={onLoadMore}
190
+ onCreate={combobox.commitCreate}
191
+ renderOption={renderOption}
192
+ />
193
+ </ComboboxPopover>
194
+ )}
195
+ {options.length === 0 && loading && <span className="cb-sr-only">{text.loading}</span>}
196
+ </div>
197
+ );
198
+ }
@@ -0,0 +1,34 @@
1
+ import type { SVGProps } from "react";
2
+
3
+ type IconProps = SVGProps<SVGSVGElement>;
4
+
5
+ function Svg(props: IconProps) {
6
+ return (
7
+ <svg
8
+ width="16"
9
+ height="16"
10
+ viewBox="0 0 24 24"
11
+ fill="none"
12
+ stroke="currentColor"
13
+ strokeWidth="2"
14
+ strokeLinecap="round"
15
+ strokeLinejoin="round"
16
+ aria-hidden="true"
17
+ focusable="false"
18
+ {...props}
19
+ />
20
+ );
21
+ }
22
+
23
+ export const SearchIcon = (p: IconProps) => (
24
+ <Svg {...p}><circle cx="11" cy="11" r="7" /><path d="m20 20-3.5-3.5" /></Svg>
25
+ );
26
+ export const CheckIcon = (p: IconProps) => <Svg {...p}><path d="M20 6 9 17l-5-5" /></Svg>;
27
+ export const ChevronIcon = (p: IconProps) => <Svg {...p}><path d="m6 9 6 6 6-6" /></Svg>;
28
+ export const XIcon = (p: IconProps) => <Svg {...p}><path d="M18 6 6 18M6 6l12 12" /></Svg>;
29
+ export const PlusIcon = (p: IconProps) => <Svg {...p}><path d="M12 5v14M5 12h14" /></Svg>;
30
+ export const SpinnerIcon = (p: IconProps) => (
31
+ <Svg {...p} className={["cb-spinner", p.className].filter(Boolean).join(" ")}>
32
+ <path d="M12 3a9 9 0 1 0 9 9" />
33
+ </Svg>
34
+ );
@@ -0,0 +1,16 @@
1
+ import type { ComboboxLocaleText } from "../core/types";
2
+
3
+ export const defaultComboboxText: ComboboxLocaleText = {
4
+ searchPlaceholder: "Search…",
5
+ noResults: "No options found",
6
+ loading: "Loading…",
7
+ loadMore: "Load more",
8
+ clear: "Clear",
9
+ clearAll: "Clear all",
10
+ selectAll: "Select all",
11
+ createOption: (label) => `Create “${label}”`,
12
+ selectedCount: (count) => `${count} selected`,
13
+ moreCount: (count) => `+${count} more`,
14
+ removeOption: (label) => `Remove ${label}`,
15
+ maxReached: (max) => `Maximum ${max} selected`,
16
+ };
@@ -0,0 +1,86 @@
1
+ import type { CSSProperties, ReactNode } from "react";
2
+ import type {
3
+ ComboboxLocaleText,
4
+ ComboboxOption,
5
+ ComboboxSize,
6
+ OptionValue,
7
+ } from "../core/types";
8
+
9
+ export type ComboboxSlot =
10
+ | "root"
11
+ | "label"
12
+ | "control"
13
+ | "value"
14
+ | "tag"
15
+ | "popover"
16
+ | "search"
17
+ | "list"
18
+ | "option"
19
+ | "footer";
20
+
21
+ export interface ComboboxHandle<V extends OptionValue = string> {
22
+ open(): void;
23
+ close(): void;
24
+ toggle(): void;
25
+ focus(): void;
26
+ clear(): void;
27
+ getValue(): V[];
28
+ getSelectedOptions(): ComboboxOption<V>[];
29
+ }
30
+
31
+ export interface OptionState {
32
+ selected: boolean;
33
+ active: boolean;
34
+ }
35
+
36
+ export interface ComboboxSharedProps<V extends OptionValue = string> {
37
+ options: readonly ComboboxOption<V>[];
38
+ /** `client` filters `options` locally; `server` expects `options` to be the current results. */
39
+ mode?: "client" | "server";
40
+ /** Shows a loading row; keeps current options visible. */
41
+ loading?: boolean;
42
+ /** Called with the search text (debounced in server mode), and on open. */
43
+ onSearchChange?(search: string): void;
44
+ /** Debounce for `onSearchChange` in server mode. Default 250 ms. */
45
+ searchDebounce?: number;
46
+ /** Show the search box. Default true. When false, typing jumps to a matching option. */
47
+ searchable?: boolean;
48
+ /** More results are available; the list calls `onLoadMore` when scrolled near the end. */
49
+ hasMore?: boolean;
50
+ onLoadMore?(): void;
51
+ /**
52
+ * Replaces the built-in search matching (client mode).
53
+ * `NoInfer` keeps the option type coming from `options` and `value` only.
54
+ */
55
+ filterFn?(option: ComboboxOption<NoInfer<V>>, search: string): boolean;
56
+ renderOption?(option: ComboboxOption<NoInfer<V>>, state: OptionState): ReactNode;
57
+ /** Offers "Create …" when the search matches no option. */
58
+ allowCreate?: boolean;
59
+ onCreate?(label: string): void;
60
+ label?: ReactNode;
61
+ placeholder?: string;
62
+ description?: ReactNode;
63
+ /** Message shown below the control; also marks the control invalid. */
64
+ error?: ReactNode;
65
+ required?: boolean;
66
+ disabled?: boolean;
67
+ /** Show the clear button. Default true. */
68
+ clearable?: boolean;
69
+ size?: ComboboxSize;
70
+ /** Name for the hidden form input. */
71
+ name?: string;
72
+ id?: string;
73
+ /** Max height of the option list in pixels. Default 280. */
74
+ maxHeight?: number;
75
+ /** `true` / `false` to force, or a count above which virtualization turns on (default 80). */
76
+ virtualize?: boolean | number;
77
+ emptyMessage?: ReactNode;
78
+ className?: string;
79
+ classNames?: Partial<Record<ComboboxSlot, string>>;
80
+ style?: CSSProperties;
81
+ localeText?: Partial<ComboboxLocaleText>;
82
+ onOpenChange?(open: boolean): void;
83
+ }
84
+
85
+ export const cx = (...names: (string | false | null | undefined)[]) =>
86
+ names.filter(Boolean).join(" ");