gbs-add-block 1.2.13 → 1.2.14

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.
Files changed (53) hide show
  1. package/README.md +4 -12
  2. package/index.cjs +4 -0
  3. package/package.json +1 -1
  4. package/source/beta-components/dialog/README.md +39 -0
  5. package/source/beta-components/dialog/__tests__/core.test.ts +86 -0
  6. package/source/beta-components/dialog/core/api.ts +34 -0
  7. package/source/beta-components/dialog/core/dialog.ts +11 -0
  8. package/source/beta-components/dialog/core/index.ts +7 -0
  9. package/source/beta-components/dialog/core/store.ts +81 -0
  10. package/source/beta-components/dialog/core/types.ts +59 -0
  11. package/source/beta-components/dialog/index.ts +7 -0
  12. package/source/beta-components/dialog/react/Dialog.tsx +279 -0
  13. package/source/beta-components/dialog/react/DialogHost.tsx +45 -0
  14. package/source/beta-components/dialog/react/icons.tsx +51 -0
  15. package/source/beta-components/dialog/react/locale.ts +8 -0
  16. package/source/beta-components/dialog/react/props.ts +13 -0
  17. package/source/beta-components/dialog/styles.css +278 -0
  18. package/source/beta-components/input/README.md +44 -0
  19. package/source/beta-components/input/__tests__/core.test.ts +75 -0
  20. package/source/beta-components/input/core/count.ts +14 -0
  21. package/source/beta-components/input/core/index.ts +11 -0
  22. package/source/beta-components/input/core/otp.ts +64 -0
  23. package/source/beta-components/input/core/types.ts +14 -0
  24. package/source/beta-components/input/index.ts +8 -0
  25. package/source/beta-components/input/react/Input.tsx +219 -0
  26. package/source/beta-components/input/react/OtpInput.tsx +256 -0
  27. package/source/beta-components/input/react/dom.ts +10 -0
  28. package/source/beta-components/input/react/icons.tsx +35 -0
  29. package/source/beta-components/input/react/locale.ts +9 -0
  30. package/source/beta-components/input/react/props.ts +6 -0
  31. package/source/beta-components/input/styles.css +296 -0
  32. package/source/beta-components/modal/README.md +51 -0
  33. package/source/beta-components/modal/__tests__/core.test.ts +55 -0
  34. package/source/beta-components/modal/core/dismiss.ts +29 -0
  35. package/source/beta-components/modal/core/index.ts +3 -0
  36. package/source/beta-components/modal/core/types.ts +27 -0
  37. package/source/beta-components/modal/index.ts +5 -0
  38. package/source/beta-components/modal/react/Modal.tsx +238 -0
  39. package/source/beta-components/modal/react/icons.tsx +19 -0
  40. package/source/beta-components/modal/react/locale.ts +5 -0
  41. package/source/beta-components/modal/react/props.ts +17 -0
  42. package/source/beta-components/modal/styles.css +235 -0
  43. package/source/beta-components/textarea/README.md +39 -0
  44. package/source/beta-components/textarea/__tests__/core.test.ts +38 -0
  45. package/source/beta-components/textarea/core/count.ts +14 -0
  46. package/source/beta-components/textarea/core/index.ts +4 -0
  47. package/source/beta-components/textarea/core/size.ts +23 -0
  48. package/source/beta-components/textarea/core/types.ts +17 -0
  49. package/source/beta-components/textarea/index.ts +5 -0
  50. package/source/beta-components/textarea/react/Textarea.tsx +209 -0
  51. package/source/beta-components/textarea/react/locale.ts +5 -0
  52. package/source/beta-components/textarea/react/props.ts +4 -0
  53. package/source/beta-components/textarea/styles.css +159 -0
@@ -0,0 +1,219 @@
1
+ "use client";
2
+
3
+ import {
4
+ useId,
5
+ useImperativeHandle,
6
+ useMemo,
7
+ useRef,
8
+ useState,
9
+ type ChangeEvent,
10
+ type InputHTMLAttributes,
11
+ type ReactNode,
12
+ type Ref,
13
+ } from "react";
14
+ import { countCharacters } from "../core/count";
15
+ import type { FieldSize, InputLocaleText } from "../core/types";
16
+ import { setNativeValue } from "./dom";
17
+ import { EyeIcon, EyeOffIcon, XIcon } from "./icons";
18
+ import { defaultInputText } from "./locale";
19
+ import { cx, type InputSlot } from "./props";
20
+
21
+ export interface InputProps
22
+ extends Omit<InputHTMLAttributes<HTMLInputElement>, "size" | "prefix" | "value" | "defaultValue"> {
23
+ value?: string;
24
+ defaultValue?: string;
25
+ /** The new text on every change. The native `onChange` fires as well. */
26
+ onValueChange?(value: string): void;
27
+ label?: ReactNode;
28
+ description?: ReactNode;
29
+ /** Message below the field; also marks it invalid. */
30
+ error?: ReactNode;
31
+ size?: FieldSize;
32
+ /** Content before the text, e.g. an icon or "https://". */
33
+ leading?: ReactNode;
34
+ /** Content after the text, e.g. a unit such as "kg". */
35
+ trailing?: ReactNode;
36
+ /** A button that empties the field. Default false. */
37
+ clearable?: boolean;
38
+ /** For `type="password"`, a button that shows the text. Default true. */
39
+ revealPassword?: boolean;
40
+ /** A character counter under the field; shows `count / maxLength` when `maxLength` is set. */
41
+ showCount?: boolean;
42
+ classNames?: Partial<Record<InputSlot, string>>;
43
+ localeText?: Partial<InputLocaleText>;
44
+ /** The `<input>` element. */
45
+ ref?: Ref<HTMLInputElement>;
46
+ }
47
+
48
+ /**
49
+ * A text field with label, hint, error, adornments, clear and password-reveal
50
+ * buttons and a character counter. Every other prop goes to the `<input>`, so
51
+ * it works with native forms and form libraries unchanged.
52
+ */
53
+ export function Input(props: InputProps) {
54
+ const {
55
+ ref,
56
+ value: valueProp,
57
+ defaultValue,
58
+ onChange,
59
+ onValueChange,
60
+ label,
61
+ description,
62
+ error,
63
+ size = "md",
64
+ leading,
65
+ trailing,
66
+ clearable = false,
67
+ revealPassword = true,
68
+ showCount = false,
69
+ className,
70
+ classNames,
71
+ style,
72
+ localeText,
73
+ id: idProp,
74
+ type = "text",
75
+ disabled,
76
+ readOnly,
77
+ required,
78
+ maxLength,
79
+ "aria-describedby": describedByProp,
80
+ ...inputProps
81
+ } = props;
82
+
83
+ const reactId = useId();
84
+ const id = idProp ?? reactId;
85
+ const text = useMemo(() => ({ ...defaultInputText, ...localeText }), [localeText]);
86
+ const inputRef = useRef<HTMLInputElement>(null);
87
+ useImperativeHandle(ref, () => inputRef.current as HTMLInputElement, []);
88
+
89
+ // Mirrors the text even when uncontrolled, for the counter and the clear button.
90
+ const [internal, setInternal] = useState(defaultValue ?? "");
91
+ const current = valueProp ?? internal;
92
+ const [revealed, setRevealed] = useState(false);
93
+
94
+ const handleChange = (event: ChangeEvent<HTMLInputElement>) => {
95
+ if (valueProp === undefined) setInternal(event.target.value);
96
+ onChange?.(event);
97
+ onValueChange?.(event.target.value);
98
+ };
99
+
100
+ const clear = () => {
101
+ const input = inputRef.current;
102
+ if (!input) return;
103
+ // A real input event, so onChange and form libraries see the change too.
104
+ setNativeValue(input, "");
105
+ input.focus();
106
+ };
107
+
108
+ const invalid = Boolean(error);
109
+ const isPassword = type === "password";
110
+ const count = countCharacters(current);
111
+ const describedBy =
112
+ cx(describedByProp, description ? `${id}-description` : "", error ? `${id}-error` : "") || undefined;
113
+
114
+ return (
115
+ <div
116
+ className={cx("in-root", classNames?.root, className)}
117
+ style={style}
118
+ data-size={size}
119
+ data-disabled={disabled || undefined}
120
+ data-invalid={invalid ? "" : undefined}
121
+ >
122
+ {label && (
123
+ <label htmlFor={id} className={cx("in-label", classNames?.label)} data-required={required || undefined}>
124
+ {label}
125
+ </label>
126
+ )}
127
+
128
+ <div
129
+ className={cx("in-control", classNames?.control)}
130
+ data-disabled={disabled || undefined}
131
+ data-readonly={readOnly || undefined}
132
+ data-invalid={invalid ? "" : undefined}
133
+ // A press on the padding or an adornment focuses the text, like a native field.
134
+ onMouseDown={(event) => {
135
+ const target = event.target as Element;
136
+ if (target === inputRef.current || target.closest("button") || disabled) return;
137
+ event.preventDefault();
138
+ inputRef.current?.focus();
139
+ }}
140
+ >
141
+ {leading && (
142
+ <span className="in-adornment" data-side="leading">
143
+ {leading}
144
+ </span>
145
+ )}
146
+ <input
147
+ {...inputProps}
148
+ ref={inputRef}
149
+ id={id}
150
+ type={isPassword && revealed ? "text" : type}
151
+ className={cx("in-input", classNames?.input)}
152
+ value={valueProp}
153
+ defaultValue={valueProp === undefined ? defaultValue : undefined}
154
+ disabled={disabled}
155
+ readOnly={readOnly}
156
+ required={required}
157
+ maxLength={maxLength}
158
+ aria-invalid={invalid || undefined}
159
+ aria-describedby={describedBy}
160
+ onChange={handleChange}
161
+ />
162
+ {clearable && current !== "" && !disabled && !readOnly && (
163
+ <button
164
+ type="button"
165
+ tabIndex={-1}
166
+ className="in-icon-button"
167
+ aria-label={text.clear}
168
+ title={text.clear}
169
+ onClick={clear}
170
+ >
171
+ <XIcon />
172
+ </button>
173
+ )}
174
+ {isPassword && revealPassword && (
175
+ <button
176
+ type="button"
177
+ className="in-icon-button"
178
+ aria-label={revealed ? text.hidePassword : text.showPassword}
179
+ title={revealed ? text.hidePassword : text.showPassword}
180
+ aria-pressed={revealed}
181
+ aria-controls={id}
182
+ disabled={disabled}
183
+ onClick={() => setRevealed((shown) => !shown)}
184
+ >
185
+ {revealed ? <EyeOffIcon /> : <EyeIcon />}
186
+ </button>
187
+ )}
188
+ {trailing && (
189
+ <span className="in-adornment" data-side="trailing">
190
+ {trailing}
191
+ </span>
192
+ )}
193
+ </div>
194
+
195
+ {(description || showCount) && (
196
+ <div className="in-footer">
197
+ {description && (
198
+ <span id={`${id}-description`} className={cx("in-description", classNames?.description)}>
199
+ {description}
200
+ </span>
201
+ )}
202
+ {showCount && (
203
+ <span
204
+ className={cx("in-count", classNames?.count)}
205
+ data-over={maxLength !== undefined && count > maxLength ? "" : undefined}
206
+ >
207
+ {text.characterCount(String(count), maxLength !== undefined ? String(maxLength) : undefined)}
208
+ </span>
209
+ )}
210
+ </div>
211
+ )}
212
+ {error && (
213
+ <span id={`${id}-error`} className={cx("in-error", classNames?.error)} role="alert">
214
+ {error}
215
+ </span>
216
+ )}
217
+ </div>
218
+ );
219
+ }
@@ -0,0 +1,256 @@
1
+ "use client";
2
+
3
+ import {
4
+ Fragment,
5
+ useId,
6
+ useImperativeHandle,
7
+ useMemo,
8
+ useRef,
9
+ useState,
10
+ type ChangeEvent,
11
+ type InputHTMLAttributes,
12
+ type KeyboardEvent,
13
+ type ReactNode,
14
+ type Ref,
15
+ } from "react";
16
+ import {
17
+ activeCell,
18
+ cellAt,
19
+ otpInputMode,
20
+ otpPattern,
21
+ sanitizeOtp,
22
+ separatorsAfter,
23
+ } from "../core/otp";
24
+ import type { FieldSize, InputLocaleText, OtpMode } from "../core/types";
25
+ import { defaultInputText } from "./locale";
26
+ import { cx, type OtpSlot } from "./props";
27
+
28
+ export interface OtpInputProps
29
+ extends Omit<
30
+ InputHTMLAttributes<HTMLInputElement>,
31
+ "size" | "prefix" | "value" | "defaultValue" | "type" | "maxLength" | "minLength" | "pattern" | "inputMode"
32
+ > {
33
+ /** Number of characters. Default 6. */
34
+ length?: number;
35
+ /** Default `numeric`. */
36
+ mode?: OtpMode;
37
+ /** Turn letters into capitals as they are typed. */
38
+ uppercase?: boolean;
39
+ value?: string;
40
+ defaultValue?: string;
41
+ onValueChange?(value: string): void;
42
+ /** Every cell is filled. */
43
+ onComplete?(value: string): void;
44
+ /** Group sizes with a separator between them, e.g. `[3, 3]`. */
45
+ groups?: number[];
46
+ /** Show dots instead of the characters. */
47
+ mask?: boolean;
48
+ label?: ReactNode;
49
+ description?: ReactNode;
50
+ error?: ReactNode;
51
+ size?: FieldSize;
52
+ classNames?: Partial<Record<OtpSlot, string>>;
53
+ localeText?: Partial<InputLocaleText>;
54
+ /** The `<input>` element. */
55
+ ref?: Ref<HTMLInputElement>;
56
+ }
57
+
58
+ /**
59
+ * A one-time-code field. It is a single real `<input>` drawn as separate cells,
60
+ * so SMS autofill, paste, password managers and screen readers treat it as one
61
+ * field, the way they expect.
62
+ */
63
+ export function OtpInput(props: OtpInputProps) {
64
+ const {
65
+ ref,
66
+ length = 6,
67
+ mode = "numeric",
68
+ uppercase = false,
69
+ value: valueProp,
70
+ defaultValue = "",
71
+ onValueChange,
72
+ onComplete,
73
+ groups,
74
+ mask = false,
75
+ label,
76
+ description,
77
+ error,
78
+ size = "md",
79
+ className,
80
+ classNames,
81
+ style,
82
+ localeText,
83
+ id: idProp,
84
+ disabled,
85
+ readOnly,
86
+ required,
87
+ autoComplete = "one-time-code",
88
+ onChange,
89
+ onFocus,
90
+ onBlur,
91
+ onKeyDown,
92
+ onSelect,
93
+ onClick,
94
+ "aria-describedby": describedByProp,
95
+ "aria-label": ariaLabel,
96
+ ...inputProps
97
+ } = props;
98
+
99
+ const reactId = useId();
100
+ const id = idProp ?? reactId;
101
+ const text = useMemo(() => ({ ...defaultInputText, ...localeText }), [localeText]);
102
+ const inputRef = useRef<HTMLInputElement>(null);
103
+ const cellsRef = useRef<HTMLDivElement>(null);
104
+ useImperativeHandle(ref, () => inputRef.current as HTMLInputElement, []);
105
+
106
+ const [internal, setInternal] = useState(() => sanitizeOtp(defaultValue, length, mode, uppercase));
107
+ const value = valueProp !== undefined ? sanitizeOtp(valueProp, length, mode, uppercase) : internal;
108
+ const [focused, setFocused] = useState(false);
109
+ const [caret, setCaret] = useState(0);
110
+
111
+ /** Once the code is full, the caret selects one character so typing replaces it. */
112
+ const selectAt = (input: HTMLInputElement, index: number) => {
113
+ if (input.value.length >= length) {
114
+ const at = Math.max(0, Math.min(index, length - 1));
115
+ input.setSelectionRange(at, at + 1);
116
+ } else {
117
+ const at = Math.max(0, Math.min(index, input.value.length));
118
+ input.setSelectionRange(at, at);
119
+ }
120
+ };
121
+
122
+ const handleChange = (event: ChangeEvent<HTMLInputElement>) => {
123
+ const next = sanitizeOtp(event.target.value, length, mode, uppercase);
124
+ if (valueProp === undefined) setInternal(next);
125
+ onChange?.(event);
126
+ onValueChange?.(next);
127
+ if (next.length === length && next !== value) onComplete?.(next);
128
+ };
129
+
130
+ const handleKeyDown = (event: KeyboardEvent<HTMLInputElement>) => {
131
+ onKeyDown?.(event);
132
+ const input = event.currentTarget;
133
+ // Before the code is full the caret is collapsed, and native movement works.
134
+ if (event.defaultPrevented || event.shiftKey || input.value.length < length) return;
135
+ const start = input.selectionStart ?? 0;
136
+ const target: Record<string, number> = {
137
+ ArrowLeft: start - 1,
138
+ ArrowRight: start + 1,
139
+ Home: 0,
140
+ End: length - 1,
141
+ };
142
+ if (!(event.key in target)) return;
143
+ event.preventDefault();
144
+ selectAt(input, target[event.key]);
145
+ };
146
+
147
+ const invalid = Boolean(error);
148
+ const separators = separatorsAfter(groups, length);
149
+ const active = focused ? activeCell(value.length, length, caret) : -1;
150
+ const describedBy =
151
+ cx(describedByProp, description ? `${id}-description` : "", error ? `${id}-error` : "") || undefined;
152
+
153
+ return (
154
+ <div
155
+ className={cx("in-root", "in-otp-root", classNames?.root, className)}
156
+ style={style}
157
+ data-size={size}
158
+ data-disabled={disabled || undefined}
159
+ data-invalid={invalid ? "" : undefined}
160
+ >
161
+ {label && (
162
+ <label htmlFor={id} className={cx("in-label", classNames?.label)} data-required={required || undefined}>
163
+ {label}
164
+ </label>
165
+ )}
166
+
167
+ <div
168
+ ref={cellsRef}
169
+ className={cx("in-otp", classNames?.cells)}
170
+ dir="ltr"
171
+ data-focused={focused || undefined}
172
+ data-disabled={disabled || undefined}
173
+ data-invalid={invalid ? "" : undefined}
174
+ >
175
+ <input
176
+ {...inputProps}
177
+ ref={inputRef}
178
+ id={id}
179
+ className="in-otp-input"
180
+ type={mask ? "password" : "text"}
181
+ value={value}
182
+ inputMode={otpInputMode(mode)}
183
+ pattern={otpPattern(mode, length)}
184
+ autoComplete={autoComplete}
185
+ autoCorrect="off"
186
+ autoCapitalize={uppercase ? "characters" : "off"}
187
+ spellCheck={false}
188
+ disabled={disabled}
189
+ readOnly={readOnly}
190
+ required={required}
191
+ aria-label={ariaLabel ?? (label ? undefined : text.otpLabel)}
192
+ aria-invalid={invalid || undefined}
193
+ aria-describedby={describedBy}
194
+ onChange={handleChange}
195
+ onKeyDown={handleKeyDown}
196
+ onFocus={(event) => {
197
+ setFocused(true);
198
+ selectAt(event.currentTarget, event.currentTarget.value.length);
199
+ onFocus?.(event);
200
+ }}
201
+ onBlur={(event) => {
202
+ setFocused(false);
203
+ onBlur?.(event);
204
+ }}
205
+ onSelect={(event) => {
206
+ const input = event.currentTarget;
207
+ const start = input.selectionStart ?? input.value.length;
208
+ const end = input.selectionEnd ?? start;
209
+ if (input.value.length >= length && start === end) selectAt(input, start);
210
+ else setCaret(start);
211
+ onSelect?.(event);
212
+ }}
213
+ // The text is invisible, so place the caret by the cell that was clicked.
214
+ onClick={(event) => {
215
+ const cells = cellsRef.current?.querySelectorAll<HTMLElement>("[data-cell]");
216
+ if (cells) {
217
+ const index = cellAt(Array.from(cells, (cell) => cell.getBoundingClientRect()), event.clientX);
218
+ selectAt(event.currentTarget, index);
219
+ }
220
+ onClick?.(event);
221
+ }}
222
+ />
223
+ {Array.from({ length }, (_, index) => {
224
+ const char = value[index];
225
+ return (
226
+ <Fragment key={index}>
227
+ <span
228
+ data-cell=""
229
+ className={cx("in-otp-cell", classNames?.cell)}
230
+ data-active={index === active || undefined}
231
+ data-filled={char ? "" : undefined}
232
+ aria-hidden="true"
233
+ >
234
+ {char ? (mask ? "•" : char) : index === active ? <span className="in-otp-caret" /> : null}
235
+ </span>
236
+ {separators.has(index) && (
237
+ <span className={cx("in-otp-separator", classNames?.separator)} aria-hidden="true" />
238
+ )}
239
+ </Fragment>
240
+ );
241
+ })}
242
+ </div>
243
+
244
+ {description && (
245
+ <span id={`${id}-description`} className={cx("in-description", classNames?.description)}>
246
+ {description}
247
+ </span>
248
+ )}
249
+ {error && (
250
+ <span id={`${id}-error`} className={cx("in-error", classNames?.error)} role="alert">
251
+ {error}
252
+ </span>
253
+ )}
254
+ </div>
255
+ );
256
+ }
@@ -0,0 +1,10 @@
1
+ /**
2
+ * Sets an input's value the way typing does: through the prototype setter,
3
+ * followed by a bubbling `input` event. React's `onChange`, form libraries and
4
+ * native listeners all see the change, controlled or not.
5
+ */
6
+ export function setNativeValue(input: HTMLInputElement, value: string) {
7
+ const setter = Object.getOwnPropertyDescriptor(HTMLInputElement.prototype, "value")?.set;
8
+ setter?.call(input, value);
9
+ input.dispatchEvent(new Event("input", { bubbles: true }));
10
+ }
@@ -0,0 +1,35 @@
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 XIcon = (p: IconProps) => <Svg width={14} height={14} {...p}><path d="M18 6 6 18M6 6l12 12" /></Svg>;
24
+ export const EyeIcon = (p: IconProps) => (
25
+ <Svg {...p}>
26
+ <path d="M2 12s3.5-7 10-7 10 7 10 7-3.5 7-10 7S2 12 2 12Z" />
27
+ <circle cx="12" cy="12" r="3" />
28
+ </Svg>
29
+ );
30
+ export const EyeOffIcon = (p: IconProps) => (
31
+ <Svg {...p}>
32
+ <path d="M10.6 5.1A10.8 10.8 0 0 1 12 5c6.5 0 10 7 10 7a17.6 17.6 0 0 1-2.2 3.2M6.6 6.6C3.9 8.3 2 12 2 12s3.5 7 10 7a9.7 9.7 0 0 0 5.4-1.6" />
33
+ <path d="M9.9 9.9a3 3 0 0 0 4.2 4.2M2 2l20 20" />
34
+ </Svg>
35
+ );
@@ -0,0 +1,9 @@
1
+ import type { InputLocaleText } from "../core/types";
2
+
3
+ export const defaultInputText: InputLocaleText = {
4
+ clear: "Clear",
5
+ showPassword: "Show password",
6
+ hidePassword: "Hide password",
7
+ characterCount: (count, max) => (max === undefined ? count : `${count} / ${max}`),
8
+ otpLabel: "Verification code",
9
+ };
@@ -0,0 +1,6 @@
1
+ export type InputSlot = "root" | "label" | "control" | "input" | "description" | "error" | "count";
2
+
3
+ export type OtpSlot = "root" | "label" | "cells" | "cell" | "separator" | "description" | "error";
4
+
5
+ export const cx = (...names: (string | false | null | undefined)[]) =>
6
+ names.filter(Boolean).join(" ");