gbs-add-block 1.2.13 → 1.2.15
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/README.md +4 -12
- package/index.cjs +9 -0
- package/package.json +1 -1
- package/source/beta-components/breadcrumb/README.md +34 -0
- package/source/beta-components/breadcrumb/__tests__/core.test.ts +58 -0
- package/source/beta-components/breadcrumb/core/index.ts +4 -0
- package/source/beta-components/breadcrumb/core/trail.ts +42 -0
- package/source/beta-components/breadcrumb/core/types.ts +20 -0
- package/source/beta-components/breadcrumb/index.ts +5 -0
- package/source/beta-components/breadcrumb/react/Breadcrumb.tsx +177 -0
- package/source/beta-components/breadcrumb/react/locale.ts +6 -0
- package/source/beta-components/breadcrumb/react/props.ts +14 -0
- package/source/beta-components/breadcrumb/styles.css +139 -0
- package/source/beta-components/button/README.md +32 -0
- package/source/beta-components/button/__tests__/core.test.ts +31 -0
- package/source/beta-components/button/core/index.ts +3 -0
- package/source/beta-components/button/core/state.ts +16 -0
- package/source/beta-components/button/core/types.ts +28 -0
- package/source/beta-components/button/index.ts +5 -0
- package/source/beta-components/button/react/Button.tsx +173 -0
- package/source/beta-components/button/react/locale.ts +5 -0
- package/source/beta-components/button/react/props.ts +23 -0
- package/source/beta-components/button/styles.css +233 -0
- package/source/beta-components/checkbox/README.md +36 -0
- package/source/beta-components/checkbox/__tests__/core.test.ts +40 -0
- package/source/beta-components/checkbox/core/group.ts +30 -0
- package/source/beta-components/checkbox/core/index.ts +3 -0
- package/source/beta-components/checkbox/core/types.ts +17 -0
- package/source/beta-components/checkbox/index.ts +9 -0
- package/source/beta-components/checkbox/react/Checkbox.tsx +146 -0
- package/source/beta-components/checkbox/react/CheckboxGroup.tsx +149 -0
- package/source/beta-components/checkbox/react/context.ts +16 -0
- package/source/beta-components/checkbox/react/locale.ts +5 -0
- package/source/beta-components/checkbox/react/props.ts +6 -0
- package/source/beta-components/checkbox/styles.css +212 -0
- package/source/beta-components/dialog/README.md +39 -0
- package/source/beta-components/dialog/__tests__/core.test.ts +86 -0
- package/source/beta-components/dialog/core/api.ts +34 -0
- package/source/beta-components/dialog/core/dialog.ts +11 -0
- package/source/beta-components/dialog/core/index.ts +7 -0
- package/source/beta-components/dialog/core/store.ts +81 -0
- package/source/beta-components/dialog/core/types.ts +59 -0
- package/source/beta-components/dialog/index.ts +7 -0
- package/source/beta-components/dialog/react/Dialog.tsx +279 -0
- package/source/beta-components/dialog/react/DialogHost.tsx +45 -0
- package/source/beta-components/dialog/react/icons.tsx +51 -0
- package/source/beta-components/dialog/react/locale.ts +8 -0
- package/source/beta-components/dialog/react/props.ts +13 -0
- package/source/beta-components/dialog/styles.css +278 -0
- package/source/beta-components/input/README.md +44 -0
- package/source/beta-components/input/__tests__/core.test.ts +75 -0
- package/source/beta-components/input/core/count.ts +14 -0
- package/source/beta-components/input/core/index.ts +11 -0
- package/source/beta-components/input/core/otp.ts +64 -0
- package/source/beta-components/input/core/types.ts +14 -0
- package/source/beta-components/input/index.ts +8 -0
- package/source/beta-components/input/react/Input.tsx +219 -0
- package/source/beta-components/input/react/OtpInput.tsx +256 -0
- package/source/beta-components/input/react/dom.ts +10 -0
- package/source/beta-components/input/react/icons.tsx +35 -0
- package/source/beta-components/input/react/locale.ts +9 -0
- package/source/beta-components/input/react/props.ts +6 -0
- package/source/beta-components/input/styles.css +296 -0
- package/source/beta-components/modal/README.md +51 -0
- package/source/beta-components/modal/__tests__/core.test.ts +55 -0
- package/source/beta-components/modal/core/dismiss.ts +29 -0
- package/source/beta-components/modal/core/index.ts +3 -0
- package/source/beta-components/modal/core/types.ts +27 -0
- package/source/beta-components/modal/index.ts +5 -0
- package/source/beta-components/modal/react/Modal.tsx +238 -0
- package/source/beta-components/modal/react/icons.tsx +19 -0
- package/source/beta-components/modal/react/locale.ts +5 -0
- package/source/beta-components/modal/react/props.ts +17 -0
- package/source/beta-components/modal/styles.css +235 -0
- package/source/beta-components/spinner/README.md +29 -0
- package/source/beta-components/spinner/__tests__/core.test.ts +48 -0
- package/source/beta-components/spinner/core/index.ts +3 -0
- package/source/beta-components/spinner/core/types.ts +28 -0
- package/source/beta-components/spinner/core/visibility.ts +34 -0
- package/source/beta-components/spinner/index.ts +6 -0
- package/source/beta-components/spinner/react/Spinner.tsx +96 -0
- package/source/beta-components/spinner/react/locale.ts +5 -0
- package/source/beta-components/spinner/react/props.ts +4 -0
- package/source/beta-components/spinner/react/useDelayedLoading.ts +39 -0
- package/source/beta-components/spinner/styles.css +167 -0
- package/source/beta-components/tabs/README.md +32 -0
- package/source/beta-components/tabs/__tests__/core.test.ts +51 -0
- package/source/beta-components/tabs/core/index.ts +3 -0
- package/source/beta-components/tabs/core/navigation.ts +54 -0
- package/source/beta-components/tabs/core/types.ts +16 -0
- package/source/beta-components/tabs/index.ts +5 -0
- package/source/beta-components/tabs/react/Tabs.tsx +260 -0
- package/source/beta-components/tabs/react/context.ts +21 -0
- package/source/beta-components/tabs/styles.css +257 -0
- package/source/beta-components/textarea/README.md +39 -0
- package/source/beta-components/textarea/__tests__/core.test.ts +38 -0
- package/source/beta-components/textarea/core/count.ts +14 -0
- package/source/beta-components/textarea/core/index.ts +4 -0
- package/source/beta-components/textarea/core/size.ts +23 -0
- package/source/beta-components/textarea/core/types.ts +17 -0
- package/source/beta-components/textarea/index.ts +5 -0
- package/source/beta-components/textarea/react/Textarea.tsx +209 -0
- package/source/beta-components/textarea/react/locale.ts +5 -0
- package/source/beta-components/textarea/react/props.ts +4 -0
- package/source/beta-components/textarea/styles.css +159 -0
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
use,
|
|
5
|
+
useId,
|
|
6
|
+
useImperativeHandle,
|
|
7
|
+
useLayoutEffect,
|
|
8
|
+
useRef,
|
|
9
|
+
useState,
|
|
10
|
+
type ChangeEvent,
|
|
11
|
+
type ChangeEventHandler,
|
|
12
|
+
type InputHTMLAttributes,
|
|
13
|
+
type ReactNode,
|
|
14
|
+
type Ref,
|
|
15
|
+
} from "react";
|
|
16
|
+
import type { CheckboxSize, CheckedState } from "../core/types";
|
|
17
|
+
import { CheckboxGroupContext } from "./context";
|
|
18
|
+
import { cx, type CheckboxSlot } from "./props";
|
|
19
|
+
|
|
20
|
+
export interface CheckboxProps
|
|
21
|
+
extends Omit<
|
|
22
|
+
InputHTMLAttributes<HTMLInputElement>,
|
|
23
|
+
"type" | "size" | "checked" | "defaultChecked" | "onChange" | "value"
|
|
24
|
+
> {
|
|
25
|
+
/** `true`, `false` or `"indeterminate"` (controlled). */
|
|
26
|
+
checked?: CheckedState;
|
|
27
|
+
defaultChecked?: CheckedState;
|
|
28
|
+
onCheckedChange?(checked: boolean): void;
|
|
29
|
+
onChange?: ChangeEventHandler<HTMLInputElement>;
|
|
30
|
+
/** Posted with the form when checked. Inside a CheckboxGroup, the value this box adds. */
|
|
31
|
+
value?: string;
|
|
32
|
+
label?: ReactNode;
|
|
33
|
+
description?: ReactNode;
|
|
34
|
+
error?: ReactNode;
|
|
35
|
+
size?: CheckboxSize;
|
|
36
|
+
classNames?: Partial<Record<CheckboxSlot, string>>;
|
|
37
|
+
/** The `<input>` element. */
|
|
38
|
+
ref?: Ref<HTMLInputElement>;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* A native checkbox, styled. It posts with forms, works with form libraries and
|
|
43
|
+
* reads as a checkbox to every assistive technology, including the mixed state.
|
|
44
|
+
*/
|
|
45
|
+
export function Checkbox(props: CheckboxProps) {
|
|
46
|
+
const {
|
|
47
|
+
ref,
|
|
48
|
+
checked: checkedProp,
|
|
49
|
+
defaultChecked = false,
|
|
50
|
+
onCheckedChange,
|
|
51
|
+
onChange,
|
|
52
|
+
value,
|
|
53
|
+
label,
|
|
54
|
+
description,
|
|
55
|
+
error,
|
|
56
|
+
size,
|
|
57
|
+
className,
|
|
58
|
+
classNames,
|
|
59
|
+
style,
|
|
60
|
+
id: idProp,
|
|
61
|
+
disabled,
|
|
62
|
+
required,
|
|
63
|
+
name,
|
|
64
|
+
"aria-describedby": describedByProp,
|
|
65
|
+
...inputProps
|
|
66
|
+
} = props;
|
|
67
|
+
|
|
68
|
+
const group = use(CheckboxGroupContext);
|
|
69
|
+
const reactId = useId();
|
|
70
|
+
const id = idProp ?? reactId;
|
|
71
|
+
const inputRef = useRef<HTMLInputElement>(null);
|
|
72
|
+
useImperativeHandle(ref, () => inputRef.current as HTMLInputElement, []);
|
|
73
|
+
|
|
74
|
+
const [internal, setInternal] = useState<CheckedState>(defaultChecked);
|
|
75
|
+
const inGroup = group !== null && value !== undefined;
|
|
76
|
+
const state: CheckedState = inGroup ? group.values.includes(value) : (checkedProp ?? internal);
|
|
77
|
+
|
|
78
|
+
// "Indeterminate" exists only as a DOM property, not an attribute.
|
|
79
|
+
useLayoutEffect(() => {
|
|
80
|
+
if (inputRef.current) inputRef.current.indeterminate = state === "indeterminate";
|
|
81
|
+
}, [state]);
|
|
82
|
+
|
|
83
|
+
const handleChange = (event: ChangeEvent<HTMLInputElement>) => {
|
|
84
|
+
const next = event.target.checked;
|
|
85
|
+
if (inGroup) group.toggle(value, next);
|
|
86
|
+
else if (checkedProp === undefined) setInternal(next);
|
|
87
|
+
onChange?.(event);
|
|
88
|
+
onCheckedChange?.(next);
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
const invalid = Boolean(error) || (group?.invalid ?? false);
|
|
92
|
+
const describedBy =
|
|
93
|
+
cx(describedByProp, description ? `${id}-description` : "", error ? `${id}-error` : "") || undefined;
|
|
94
|
+
|
|
95
|
+
return (
|
|
96
|
+
<div
|
|
97
|
+
className={cx("ck-root", classNames?.root, className)}
|
|
98
|
+
style={style}
|
|
99
|
+
data-size={size ?? group?.size ?? "md"}
|
|
100
|
+
data-disabled={disabled || group?.disabled || undefined}
|
|
101
|
+
data-invalid={invalid ? "" : undefined}
|
|
102
|
+
>
|
|
103
|
+
<span className={cx("ck-control", classNames?.control)}>
|
|
104
|
+
<input
|
|
105
|
+
{...inputProps}
|
|
106
|
+
ref={inputRef}
|
|
107
|
+
id={id}
|
|
108
|
+
type="checkbox"
|
|
109
|
+
className={cx("ck-input", classNames?.input)}
|
|
110
|
+
checked={state === true}
|
|
111
|
+
value={value}
|
|
112
|
+
name={name ?? group?.name}
|
|
113
|
+
disabled={disabled}
|
|
114
|
+
required={required}
|
|
115
|
+
aria-invalid={invalid || undefined}
|
|
116
|
+
aria-describedby={describedBy}
|
|
117
|
+
onChange={handleChange}
|
|
118
|
+
/>
|
|
119
|
+
<svg className="ck-icon" viewBox="0 0 16 16" aria-hidden="true" focusable="false">
|
|
120
|
+
<path className="ck-check" d="M3.5 8.5 6.5 11.5 12.5 4.5" pathLength={1} />
|
|
121
|
+
<path className="ck-dash" d="M4 8h8" />
|
|
122
|
+
</svg>
|
|
123
|
+
</span>
|
|
124
|
+
|
|
125
|
+
{(label || description || error) && (
|
|
126
|
+
<span className="ck-text">
|
|
127
|
+
{label && (
|
|
128
|
+
<label htmlFor={id} className={cx("ck-label", classNames?.label)} data-required={required || undefined}>
|
|
129
|
+
{label}
|
|
130
|
+
</label>
|
|
131
|
+
)}
|
|
132
|
+
{description && (
|
|
133
|
+
<span id={`${id}-description`} className={cx("ck-description", classNames?.description)}>
|
|
134
|
+
{description}
|
|
135
|
+
</span>
|
|
136
|
+
)}
|
|
137
|
+
{error && (
|
|
138
|
+
<span id={`${id}-error`} className={cx("ck-error", classNames?.error)} role="alert">
|
|
139
|
+
{error}
|
|
140
|
+
</span>
|
|
141
|
+
)}
|
|
142
|
+
</span>
|
|
143
|
+
)}
|
|
144
|
+
</div>
|
|
145
|
+
);
|
|
146
|
+
}
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useId, useMemo, useState, type CSSProperties, type ReactNode } from "react";
|
|
4
|
+
import { groupState, toggleAll, toggleValue } from "../core/group";
|
|
5
|
+
import type { CheckboxLocaleText, CheckboxOption, CheckboxSize } from "../core/types";
|
|
6
|
+
import { Checkbox } from "./Checkbox";
|
|
7
|
+
import { CheckboxGroupContext, type CheckboxGroupContextValue } from "./context";
|
|
8
|
+
import { defaultCheckboxText } from "./locale";
|
|
9
|
+
import { cx, type CheckboxGroupSlot } from "./props";
|
|
10
|
+
|
|
11
|
+
export interface CheckboxGroupProps {
|
|
12
|
+
/** Checked values (controlled). */
|
|
13
|
+
value?: string[];
|
|
14
|
+
defaultValue?: string[];
|
|
15
|
+
onValueChange?(values: string[]): void;
|
|
16
|
+
/** Render these options, or pass `<Checkbox value="…">` children. */
|
|
17
|
+
options?: readonly CheckboxOption[];
|
|
18
|
+
children?: ReactNode;
|
|
19
|
+
/** The group's name, shown as its legend. */
|
|
20
|
+
label?: ReactNode;
|
|
21
|
+
description?: ReactNode;
|
|
22
|
+
error?: ReactNode;
|
|
23
|
+
required?: boolean;
|
|
24
|
+
disabled?: boolean;
|
|
25
|
+
/** Form field name for every checkbox in the group. */
|
|
26
|
+
name?: string;
|
|
27
|
+
size?: CheckboxSize;
|
|
28
|
+
/** Default `vertical`. */
|
|
29
|
+
orientation?: "vertical" | "horizontal";
|
|
30
|
+
/** With `options`, a parent box that checks or clears them all. `true` uses the default label. */
|
|
31
|
+
selectAll?: boolean | ReactNode;
|
|
32
|
+
id?: string;
|
|
33
|
+
className?: string;
|
|
34
|
+
classNames?: Partial<Record<CheckboxGroupSlot, string>>;
|
|
35
|
+
style?: CSSProperties;
|
|
36
|
+
localeText?: Partial<CheckboxLocaleText>;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** A labelled set of checkboxes sharing one array of values. */
|
|
40
|
+
export function CheckboxGroup(props: CheckboxGroupProps) {
|
|
41
|
+
const {
|
|
42
|
+
value,
|
|
43
|
+
defaultValue,
|
|
44
|
+
onValueChange,
|
|
45
|
+
options,
|
|
46
|
+
children,
|
|
47
|
+
label,
|
|
48
|
+
description,
|
|
49
|
+
error,
|
|
50
|
+
required = false,
|
|
51
|
+
disabled = false,
|
|
52
|
+
name,
|
|
53
|
+
size,
|
|
54
|
+
orientation = "vertical",
|
|
55
|
+
selectAll,
|
|
56
|
+
id: idProp,
|
|
57
|
+
className,
|
|
58
|
+
classNames,
|
|
59
|
+
style,
|
|
60
|
+
localeText,
|
|
61
|
+
} = props;
|
|
62
|
+
|
|
63
|
+
const reactId = useId();
|
|
64
|
+
const id = idProp ?? reactId;
|
|
65
|
+
const text = useMemo(() => ({ ...defaultCheckboxText, ...localeText }), [localeText]);
|
|
66
|
+
const [internal, setInternal] = useState<string[]>(defaultValue ?? []);
|
|
67
|
+
const values = value ?? internal;
|
|
68
|
+
|
|
69
|
+
const context = useMemo<CheckboxGroupContextValue>(() => {
|
|
70
|
+
const commit = (next: string[]) => {
|
|
71
|
+
if (value === undefined) setInternal(next);
|
|
72
|
+
onValueChange?.(next);
|
|
73
|
+
};
|
|
74
|
+
return {
|
|
75
|
+
values,
|
|
76
|
+
toggle: (item, checked) => commit(toggleValue(values, item, checked)),
|
|
77
|
+
name,
|
|
78
|
+
disabled,
|
|
79
|
+
invalid: Boolean(error),
|
|
80
|
+
size,
|
|
81
|
+
};
|
|
82
|
+
}, [values, value, onValueChange, name, disabled, error, size]);
|
|
83
|
+
|
|
84
|
+
const showSelectAll = Boolean(selectAll) && options !== undefined && options.length > 0;
|
|
85
|
+
const describedBy =
|
|
86
|
+
cx(description ? `${id}-description` : "", error ? `${id}-error` : "") || undefined;
|
|
87
|
+
|
|
88
|
+
return (
|
|
89
|
+
<fieldset
|
|
90
|
+
id={id}
|
|
91
|
+
className={cx("ck-group", classNames?.root, className)}
|
|
92
|
+
style={style}
|
|
93
|
+
// A disabled fieldset disables every control inside it natively.
|
|
94
|
+
disabled={disabled}
|
|
95
|
+
aria-describedby={describedBy}
|
|
96
|
+
data-orientation={orientation}
|
|
97
|
+
data-select-all={showSelectAll || undefined}
|
|
98
|
+
data-invalid={error ? "" : undefined}
|
|
99
|
+
>
|
|
100
|
+
{label && (
|
|
101
|
+
<legend className={cx("ck-legend", classNames?.legend)} data-required={required || undefined}>
|
|
102
|
+
{label}
|
|
103
|
+
</legend>
|
|
104
|
+
)}
|
|
105
|
+
{description && (
|
|
106
|
+
<span id={`${id}-description`} className={cx("ck-description", classNames?.description)}>
|
|
107
|
+
{description}
|
|
108
|
+
</span>
|
|
109
|
+
)}
|
|
110
|
+
|
|
111
|
+
{showSelectAll && (
|
|
112
|
+
<Checkbox
|
|
113
|
+
className="ck-select-all"
|
|
114
|
+
label={selectAll === true ? text.selectAll : selectAll}
|
|
115
|
+
size={size}
|
|
116
|
+
disabled={disabled}
|
|
117
|
+
checked={groupState(values, options)}
|
|
118
|
+
onCheckedChange={() => {
|
|
119
|
+
const next = toggleAll(values, options);
|
|
120
|
+
if (value === undefined) setInternal(next);
|
|
121
|
+
onValueChange?.(next);
|
|
122
|
+
}}
|
|
123
|
+
/>
|
|
124
|
+
)}
|
|
125
|
+
|
|
126
|
+
<CheckboxGroupContext value={context}>
|
|
127
|
+
<div className={cx("ck-items", classNames?.items)}>
|
|
128
|
+
{options
|
|
129
|
+
? options.map((option) => (
|
|
130
|
+
<Checkbox
|
|
131
|
+
key={option.value}
|
|
132
|
+
value={option.value}
|
|
133
|
+
label={option.label}
|
|
134
|
+
description={option.description}
|
|
135
|
+
disabled={option.disabled}
|
|
136
|
+
/>
|
|
137
|
+
))
|
|
138
|
+
: children}
|
|
139
|
+
</div>
|
|
140
|
+
</CheckboxGroupContext>
|
|
141
|
+
|
|
142
|
+
{error && (
|
|
143
|
+
<span id={`${id}-error`} className={cx("ck-error", classNames?.error)} role="alert">
|
|
144
|
+
{error}
|
|
145
|
+
</span>
|
|
146
|
+
)}
|
|
147
|
+
</fieldset>
|
|
148
|
+
);
|
|
149
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { createContext } from "react";
|
|
4
|
+
import type { CheckboxSize } from "../core/types";
|
|
5
|
+
|
|
6
|
+
export interface CheckboxGroupContextValue {
|
|
7
|
+
values: readonly string[];
|
|
8
|
+
toggle(value: string, checked: boolean): void;
|
|
9
|
+
name?: string;
|
|
10
|
+
disabled: boolean;
|
|
11
|
+
invalid: boolean;
|
|
12
|
+
size?: CheckboxSize;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/** Lets each Checkbox inside a CheckboxGroup read and change the group's values. */
|
|
16
|
+
export const CheckboxGroupContext = createContext<CheckboxGroupContextValue | null>(null);
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
export type CheckboxSlot = "root" | "control" | "input" | "label" | "description" | "error";
|
|
2
|
+
|
|
3
|
+
export type CheckboxGroupSlot = "root" | "legend" | "description" | "items" | "error";
|
|
4
|
+
|
|
5
|
+
export const cx = (...names: (string | false | null | undefined)[]) =>
|
|
6
|
+
names.filter(Boolean).join(" ");
|
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Checkbox and CheckboxGroup styles.
|
|
3
|
+
*
|
|
4
|
+
* The box is the native <input> with `appearance: none`, so it keeps native
|
|
5
|
+
* behavior. Tokens read the DataGrid's --dg-* variables when they are set on an
|
|
6
|
+
* ancestor. Rules live in the `components` layer, so utility classes passed
|
|
7
|
+
* through `className` / `classNames` override them.
|
|
8
|
+
*/
|
|
9
|
+
@layer theme, base, components, utilities;
|
|
10
|
+
|
|
11
|
+
@layer components {
|
|
12
|
+
.ck-root,
|
|
13
|
+
.ck-group {
|
|
14
|
+
--ck-fg: var(--dg-fg, light-dark(#18181b, #f4f4f5));
|
|
15
|
+
--ck-muted: var(--dg-muted, light-dark(#71717a, #a1a1aa));
|
|
16
|
+
--ck-border: light-dark(#a1a1aa, #52525b);
|
|
17
|
+
--ck-input-bg: var(--dg-input-bg, light-dark(#ffffff, #121216));
|
|
18
|
+
--ck-accent: var(--dg-accent, light-dark(#2563eb, #60a5fa));
|
|
19
|
+
--ck-accent-fg: var(--dg-accent-fg, light-dark(#ffffff, #0b1220));
|
|
20
|
+
--ck-focus: var(--dg-focus, light-dark(#2563eb, #60a5fa));
|
|
21
|
+
--ck-danger: var(--dg-danger, light-dark(#dc2626, #f87171));
|
|
22
|
+
--ck-font-size: var(--dg-font-size, 13px);
|
|
23
|
+
--ck-size: 16px;
|
|
24
|
+
--ck-radius: 4px;
|
|
25
|
+
|
|
26
|
+
color-scheme: inherit;
|
|
27
|
+
font-size: var(--ck-font-size);
|
|
28
|
+
line-height: 1.4;
|
|
29
|
+
color: var(--ck-fg);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
:where(.dark, [data-theme="dark"]) :is(.ck-root, .ck-group) {
|
|
33
|
+
color-scheme: dark;
|
|
34
|
+
}
|
|
35
|
+
:where(.light, [data-theme="light"]) :is(.ck-root, .ck-group) {
|
|
36
|
+
color-scheme: light;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
:where(.ck-root, .ck-group) *,
|
|
40
|
+
:where(.ck-root, .ck-group) *::before,
|
|
41
|
+
:where(.ck-root, .ck-group) *::after {
|
|
42
|
+
box-sizing: border-box;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
.ck-root {
|
|
46
|
+
display: flex;
|
|
47
|
+
align-items: flex-start;
|
|
48
|
+
gap: 8px;
|
|
49
|
+
min-width: 0;
|
|
50
|
+
}
|
|
51
|
+
.ck-root[data-size="sm"] {
|
|
52
|
+
--ck-size: 14px;
|
|
53
|
+
--ck-font-size: 12px;
|
|
54
|
+
}
|
|
55
|
+
.ck-root[data-size="lg"] {
|
|
56
|
+
--ck-size: 20px;
|
|
57
|
+
--ck-font-size: 14px;
|
|
58
|
+
--ck-radius: 5px;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/* ---------------------------------------------------------------- box */
|
|
62
|
+
|
|
63
|
+
.ck-control {
|
|
64
|
+
position: relative;
|
|
65
|
+
display: grid;
|
|
66
|
+
flex: none;
|
|
67
|
+
width: var(--ck-size);
|
|
68
|
+
height: var(--ck-size);
|
|
69
|
+
/* Center the box on the first line of the label. */
|
|
70
|
+
margin-top: calc((1.4em - var(--ck-size)) / 2);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
.ck-input {
|
|
74
|
+
grid-area: 1 / 1;
|
|
75
|
+
width: 100%;
|
|
76
|
+
height: 100%;
|
|
77
|
+
margin: 0;
|
|
78
|
+
appearance: none;
|
|
79
|
+
border: 1.5px solid var(--ck-border);
|
|
80
|
+
border-radius: var(--ck-radius);
|
|
81
|
+
background: var(--ck-input-bg);
|
|
82
|
+
cursor: pointer;
|
|
83
|
+
transition:
|
|
84
|
+
background-color 120ms ease,
|
|
85
|
+
border-color 120ms ease;
|
|
86
|
+
}
|
|
87
|
+
.ck-input:hover:not(:disabled) {
|
|
88
|
+
border-color: color-mix(in oklab, var(--ck-border), var(--ck-fg) 35%);
|
|
89
|
+
}
|
|
90
|
+
.ck-input:focus-visible {
|
|
91
|
+
outline: 2px solid var(--ck-focus);
|
|
92
|
+
outline-offset: 2px;
|
|
93
|
+
}
|
|
94
|
+
.ck-input:is(:checked, :indeterminate) {
|
|
95
|
+
border-color: var(--ck-accent);
|
|
96
|
+
background: var(--ck-accent);
|
|
97
|
+
}
|
|
98
|
+
.ck-root[data-invalid] .ck-input:not(:checked, :indeterminate) {
|
|
99
|
+
border-color: var(--ck-danger);
|
|
100
|
+
}
|
|
101
|
+
.ck-input:disabled {
|
|
102
|
+
opacity: 0.5;
|
|
103
|
+
cursor: not-allowed;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
.ck-icon {
|
|
107
|
+
grid-area: 1 / 1;
|
|
108
|
+
width: 100%;
|
|
109
|
+
height: 100%;
|
|
110
|
+
fill: none;
|
|
111
|
+
stroke: var(--ck-accent-fg);
|
|
112
|
+
stroke-width: 2;
|
|
113
|
+
stroke-linecap: round;
|
|
114
|
+
stroke-linejoin: round;
|
|
115
|
+
pointer-events: none;
|
|
116
|
+
}
|
|
117
|
+
.ck-check {
|
|
118
|
+
stroke-dasharray: 1;
|
|
119
|
+
stroke-dashoffset: 1;
|
|
120
|
+
transition: stroke-dashoffset 160ms ease;
|
|
121
|
+
}
|
|
122
|
+
.ck-dash {
|
|
123
|
+
opacity: 0;
|
|
124
|
+
}
|
|
125
|
+
.ck-input:checked:not(:indeterminate) + .ck-icon .ck-check {
|
|
126
|
+
stroke-dashoffset: 0;
|
|
127
|
+
}
|
|
128
|
+
.ck-input:indeterminate + .ck-icon .ck-dash {
|
|
129
|
+
opacity: 1;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/* --------------------------------------------------------------- text */
|
|
133
|
+
|
|
134
|
+
.ck-text {
|
|
135
|
+
display: flex;
|
|
136
|
+
flex-direction: column;
|
|
137
|
+
gap: 1px;
|
|
138
|
+
min-width: 0;
|
|
139
|
+
}
|
|
140
|
+
.ck-label {
|
|
141
|
+
cursor: pointer;
|
|
142
|
+
}
|
|
143
|
+
.ck-root[data-disabled] .ck-label {
|
|
144
|
+
color: var(--ck-muted);
|
|
145
|
+
cursor: not-allowed;
|
|
146
|
+
}
|
|
147
|
+
.ck-label[data-required]::after,
|
|
148
|
+
.ck-legend[data-required]::after {
|
|
149
|
+
content: " *";
|
|
150
|
+
color: var(--ck-danger);
|
|
151
|
+
}
|
|
152
|
+
.ck-description {
|
|
153
|
+
color: var(--ck-muted);
|
|
154
|
+
font-size: 0.92em;
|
|
155
|
+
}
|
|
156
|
+
.ck-error {
|
|
157
|
+
color: var(--ck-danger);
|
|
158
|
+
font-size: 0.92em;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/* -------------------------------------------------------------- group */
|
|
162
|
+
|
|
163
|
+
.ck-group {
|
|
164
|
+
display: flex;
|
|
165
|
+
flex-direction: column;
|
|
166
|
+
gap: 8px;
|
|
167
|
+
min-width: 0;
|
|
168
|
+
margin: 0;
|
|
169
|
+
padding: 0;
|
|
170
|
+
border: 0;
|
|
171
|
+
}
|
|
172
|
+
.ck-legend {
|
|
173
|
+
/* A floated legend joins the flex gap like any other child. */
|
|
174
|
+
float: left;
|
|
175
|
+
width: 100%;
|
|
176
|
+
margin-bottom: 2px;
|
|
177
|
+
padding: 0;
|
|
178
|
+
font-weight: 500;
|
|
179
|
+
}
|
|
180
|
+
.ck-legend + * {
|
|
181
|
+
clear: both;
|
|
182
|
+
}
|
|
183
|
+
.ck-items {
|
|
184
|
+
display: flex;
|
|
185
|
+
flex-direction: column;
|
|
186
|
+
gap: 8px;
|
|
187
|
+
}
|
|
188
|
+
.ck-group[data-orientation="horizontal"] .ck-items {
|
|
189
|
+
flex-flow: row wrap;
|
|
190
|
+
gap: 8px 20px;
|
|
191
|
+
}
|
|
192
|
+
.ck-group[data-select-all] .ck-items {
|
|
193
|
+
padding-inline-start: calc(var(--ck-size) + 8px);
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
@media (prefers-reduced-motion: reduce) {
|
|
197
|
+
.ck-input,
|
|
198
|
+
.ck-check {
|
|
199
|
+
transition: none;
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
@media (forced-colors: active) {
|
|
204
|
+
.ck-input:is(:checked, :indeterminate) {
|
|
205
|
+
background: Highlight;
|
|
206
|
+
border-color: Highlight;
|
|
207
|
+
}
|
|
208
|
+
.ck-icon {
|
|
209
|
+
stroke: HighlightText;
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
# Dialog
|
|
2
|
+
|
|
3
|
+
Alert, confirm and prompt dialogs for React 19 that you can `await`, styled to
|
|
4
|
+
match the rest of the library. No runtime dependencies besides React.
|
|
5
|
+
|
|
6
|
+
## Setup
|
|
7
|
+
|
|
8
|
+
```ts
|
|
9
|
+
import { dialog, DialogHost } from "@/components/dialog";
|
|
10
|
+
import "@/components/dialog/styles.css";
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
Mount `<DialogHost />` once near the root, then:
|
|
14
|
+
|
|
15
|
+
```tsx
|
|
16
|
+
if (await dialog.confirm({ title: "Delete 3 invoices?", intent: "danger", confirmLabel: "Delete" })) {
|
|
17
|
+
await deleteInvoices();
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const name = await dialog.prompt({ title: "Rename", defaultValue: "report.pdf", required: true });
|
|
21
|
+
await dialog.alert("Export finished");
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
- `alert` resolves when closed; `confirm` resolves `true`/`false`; `prompt` resolves the text or `null`.
|
|
25
|
+
- `onConfirm` may be async: the dialog shows progress, and if it throws, shows the error and stays open.
|
|
26
|
+
- Requests queue and show one at a time. Where there is no document (a server render) they resolve as canceled.
|
|
27
|
+
- `<Dialog open onClose>` is the same dialog as a regular controlled component.
|
|
28
|
+
|
|
29
|
+
## Options
|
|
30
|
+
|
|
31
|
+
`title`, `description`, `intent` (`default` `info` `success` `warning` `danger`),
|
|
32
|
+
`icon`, `confirmLabel`, `cancelLabel`, `dismissible`, `size` (`sm` `md`),
|
|
33
|
+
`onConfirm`. Prompt adds `defaultValue`, `placeholder`, `inputLabel`, `inputType`,
|
|
34
|
+
`required` and `validate`.
|
|
35
|
+
|
|
36
|
+
## Headless use
|
|
37
|
+
|
|
38
|
+
`createDialogStore()` and `createDialogApi(store)` build separate instances; pass
|
|
39
|
+
the store to `<DialogHost store={store} />`. The core has no React imports.
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import { describe, expect, it, vi } from "vitest";
|
|
2
|
+
import { createDialogApi } from "../core/api";
|
|
3
|
+
import { dialog, dialogStore } from "../core/dialog";
|
|
4
|
+
import { createDialogStore, resultFor } from "../core/store";
|
|
5
|
+
|
|
6
|
+
describe("results", () => {
|
|
7
|
+
it("maps an answer to each kind's value", () => {
|
|
8
|
+
expect(resultFor("confirm", true)).toBe(true);
|
|
9
|
+
expect(resultFor("confirm", false)).toBe(false);
|
|
10
|
+
expect(resultFor("prompt", true, "Ada")).toBe("Ada");
|
|
11
|
+
expect(resultFor("prompt", true)).toBe("");
|
|
12
|
+
expect(resultFor("prompt", false, "ignored")).toBeNull();
|
|
13
|
+
expect(resultFor("alert", true)).toBeUndefined();
|
|
14
|
+
});
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
describe("queue", () => {
|
|
18
|
+
it("shows one dialog at a time, in order", async () => {
|
|
19
|
+
const store = createDialogStore();
|
|
20
|
+
const api = createDialogApi(store);
|
|
21
|
+
|
|
22
|
+
const first = api.confirm("Delete the file?");
|
|
23
|
+
const second = api.prompt({ title: "Rename", defaultValue: "report.pdf" });
|
|
24
|
+
const [a, b] = store.getSnapshot().queue;
|
|
25
|
+
expect(a).toMatchObject({ kind: "confirm", options: { title: "Delete the file?" } });
|
|
26
|
+
expect(b).toMatchObject({ kind: "prompt", options: { defaultValue: "report.pdf" } });
|
|
27
|
+
|
|
28
|
+
store.resolve(a.id, true);
|
|
29
|
+
expect(await first).toBe(true);
|
|
30
|
+
expect(store.getSnapshot().queue.map((item) => item.id)).toEqual([b.id]);
|
|
31
|
+
|
|
32
|
+
store.resolve(b.id, true, "summary.pdf");
|
|
33
|
+
expect(await second).toBe("summary.pdf");
|
|
34
|
+
expect(store.getSnapshot().queue).toHaveLength(0);
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
it("notifies subscribers and ignores unknown or repeated answers", async () => {
|
|
38
|
+
const store = createDialogStore();
|
|
39
|
+
const listener = vi.fn();
|
|
40
|
+
store.subscribe(listener);
|
|
41
|
+
|
|
42
|
+
const answer = store.open("alert", { title: "Saved" });
|
|
43
|
+
const [request] = store.getSnapshot().queue;
|
|
44
|
+
store.resolve("dialog-404", true);
|
|
45
|
+
store.resolve(request.id, true);
|
|
46
|
+
store.resolve(request.id, false);
|
|
47
|
+
|
|
48
|
+
expect(await answer).toBeUndefined();
|
|
49
|
+
expect(listener).toHaveBeenCalledTimes(2);
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
it("cancels everything with dismissAll", async () => {
|
|
53
|
+
const store = createDialogStore();
|
|
54
|
+
const api = createDialogApi(store);
|
|
55
|
+
const results = Promise.all([api.confirm("One?"), api.prompt("Two?"), api.alert("Three")]);
|
|
56
|
+
|
|
57
|
+
api.dismissAll();
|
|
58
|
+
expect(await results).toEqual([false, null, undefined]);
|
|
59
|
+
expect(store.getSnapshot().queue).toHaveLength(0);
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
it("keeps the snapshot object until something changes", () => {
|
|
63
|
+
const store = createDialogStore();
|
|
64
|
+
const before = store.getSnapshot();
|
|
65
|
+
expect(store.getSnapshot()).toBe(before);
|
|
66
|
+
void store.open("alert", { title: "Hi" });
|
|
67
|
+
expect(store.getSnapshot()).not.toBe(before);
|
|
68
|
+
});
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
describe("where nothing can be shown", () => {
|
|
72
|
+
it("answers as a cancel at once", async () => {
|
|
73
|
+
const store = createDialogStore();
|
|
74
|
+
const api = createDialogApi(store, () => false);
|
|
75
|
+
expect(await api.confirm("Sure?")).toBe(false);
|
|
76
|
+
expect(await api.prompt("Name?")).toBeNull();
|
|
77
|
+
expect(await api.alert("Hi")).toBeUndefined();
|
|
78
|
+
expect(store.getSnapshot().queue).toHaveLength(0);
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
it("does so for the default dialog on a server, where there is no document", async () => {
|
|
82
|
+
expect(typeof document).toBe("undefined");
|
|
83
|
+
expect(await dialog.confirm("Rendered on the server")).toBe(false);
|
|
84
|
+
expect(dialogStore.getSnapshot().queue).toHaveLength(0);
|
|
85
|
+
});
|
|
86
|
+
});
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import type { DialogStore } from "./store";
|
|
2
|
+
import type { AlertOptions, ConfirmOptions, PromptOptions } from "./types";
|
|
3
|
+
|
|
4
|
+
export interface DialogApi {
|
|
5
|
+
/** Resolves when the user presses OK (or dismisses it). */
|
|
6
|
+
alert(options: AlertOptions | string): Promise<void>;
|
|
7
|
+
/** Resolves `true` on Confirm, `false` on Cancel, Escape or a backdrop click. */
|
|
8
|
+
confirm(options: ConfirmOptions | string): Promise<boolean>;
|
|
9
|
+
/** Resolves with the entered text, or `null` when canceled. */
|
|
10
|
+
prompt(options: PromptOptions | string): Promise<string | null>;
|
|
11
|
+
dismissAll(): void;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* The `dialog` API for a store. `canShow` lets the default instance answer
|
|
16
|
+
* right away where nobody could see a dialog, such as during a server render.
|
|
17
|
+
*/
|
|
18
|
+
export function createDialogApi(store: DialogStore, canShow: () => boolean = () => true): DialogApi {
|
|
19
|
+
return {
|
|
20
|
+
alert(options) {
|
|
21
|
+
if (!canShow()) return Promise.resolve();
|
|
22
|
+
return store.open("alert", typeof options === "string" ? { title: options } : options);
|
|
23
|
+
},
|
|
24
|
+
confirm(options) {
|
|
25
|
+
if (!canShow()) return Promise.resolve(false);
|
|
26
|
+
return store.open("confirm", typeof options === "string" ? { title: options } : options);
|
|
27
|
+
},
|
|
28
|
+
prompt(options) {
|
|
29
|
+
if (!canShow()) return Promise.resolve(null);
|
|
30
|
+
return store.open("prompt", typeof options === "string" ? { title: options } : options);
|
|
31
|
+
},
|
|
32
|
+
dismissAll: () => store.dismissAll(),
|
|
33
|
+
};
|
|
34
|
+
}
|