@cosxai/ui 0.25.0 → 1.0.0-alpha.1
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/CHANGELOG.md +597 -0
- package/NOTICE +11 -0
- package/README.md +52 -0
- package/package.json +57 -7
- package/src/brand.css +13 -0
- package/src/components/Badge.tsx +68 -0
- package/src/components/Button.tsx +178 -0
- package/src/components/Card.tsx +42 -0
- package/src/components/Checkbox.tsx +67 -0
- package/src/components/Dialog.tsx +111 -0
- package/src/components/Field.tsx +66 -0
- package/src/components/Figure.tsx +30 -0
- package/src/components/Icon.tsx +32 -0
- package/src/components/IconButton.tsx +55 -0
- package/src/components/Input.tsx +74 -0
- package/src/components/Logo.tsx +62 -0
- package/src/components/Marker.tsx +26 -0
- package/src/components/MetaLabel.tsx +38 -0
- package/src/components/Radio.tsx +72 -0
- package/src/components/Select.tsx +351 -0
- package/src/components/Spinner.tsx +20 -0
- package/src/components/Switch.tsx +67 -0
- package/src/components/Table.tsx +196 -0
- package/src/components/Tabs.tsx +124 -0
- package/src/components/Tag.tsx +35 -0
- package/src/components/Textarea.tsx +46 -0
- package/src/components/ThemeProvider.tsx +80 -0
- package/src/components/Toast.tsx +159 -0
- package/src/components/Tooltip.tsx +57 -0
- package/src/components/core.test.tsx +242 -0
- package/src/components/forms-control.ts +36 -0
- package/src/components/forms.test.tsx +280 -0
- package/src/components/logo-art.ts +4 -0
- package/src/components/overlays.test.tsx +245 -0
- package/src/components/useButtonAction.ts +57 -0
- package/src/fonts.css +20 -0
- package/src/index.ts +34 -24
- package/src/lib/cn.ts +23 -6
- package/src/modes.css +10 -0
- package/src/styles.css +19 -0
- package/src/theme.css +143 -0
- package/src/tokens/colors.css +112 -0
- package/src/tokens/compat.css +36 -0
- package/src/tokens/elevation.css +14 -0
- package/src/tokens/motion.css +42 -0
- package/src/tokens/spacing.css +55 -0
- package/src/tokens/typography-cjk.css +42 -0
- package/src/tokens/typography.css +81 -0
- package/src/actionbar/ActionBar.tsx +0 -837
- package/src/actionbar/ActionBarButton.tsx +0 -111
- package/src/actionbar/ActionBarMenuGroup.tsx +0 -116
- package/src/actionbar/ActionBarModeHandle.tsx +0 -171
- package/src/actionbar/ActionBarProvider.tsx +0 -91
- package/src/actionbar/actionbar-context.ts +0 -45
- package/src/actionbar/index.ts +0 -28
- package/src/actionbar/types.ts +0 -147
- package/src/actionbar/useActionBarItems.ts +0 -60
- package/src/actionbar/useActionBarMode.ts +0 -144
- package/src/actionbar/useActionBarPanel.ts +0 -56
- package/src/actionbar/useActionBarStatusDot.ts +0 -49
- package/src/actionbar/useActionBarToast.ts +0 -33
- package/src/ambient/AmbientBackdrop.tsx +0 -74
- package/src/ambient/CommandInput.tsx +0 -107
- package/src/ambient/SuperbarStrip.tsx +0 -36
- package/src/ambient/index.ts +0 -6
- package/src/bento/BentoCell.tsx +0 -66
- package/src/bento/BentoGrid.tsx +0 -42
- package/src/bento/index.ts +0 -2
- package/src/command/CommandPalette.tsx +0 -329
- package/src/command/CommandProvider.tsx +0 -57
- package/src/command/command-context.ts +0 -12
- package/src/command/index.ts +0 -6
- package/src/command/rank.ts +0 -50
- package/src/command/types.ts +0 -37
- package/src/command/useCommandSource.ts +0 -37
- package/src/dialogs/DialogsProvider.tsx +0 -216
- package/src/dialogs/Modal.tsx +0 -260
- package/src/dialogs/Toast.tsx +0 -85
- package/src/dialogs/dialogs-context.ts +0 -6
- package/src/dialogs/index.ts +0 -10
- package/src/dialogs/types.ts +0 -37
- package/src/dialogs/useDialogs.ts +0 -8
- package/src/editorial/EditorialSpotlight.tsx +0 -63
- package/src/editorial/Folio.tsx +0 -52
- package/src/editorial/PlateMarker.tsx +0 -33
- package/src/editorial/RomanSection.tsx +0 -65
- package/src/editorial/RunningMarginalia.tsx +0 -65
- package/src/editorial/index.ts +0 -10
- package/src/hooks/index.ts +0 -5
- package/src/hooks/useKeyboardHotkey.ts +0 -93
- package/src/hooks/useReducedMotion.ts +0 -20
- package/src/hooks/useViewport.ts +0 -61
- package/src/layout/Breadcrumb.tsx +0 -74
- package/src/layout/LeftNavRail.tsx +0 -139
- package/src/layout/MobileTabBar.tsx +0 -101
- package/src/layout/NavItem.tsx +0 -128
- package/src/layout/NavSearchTrigger.tsx +0 -88
- package/src/layout/NavSection.tsx +0 -40
- package/src/layout/RightSidebarPanel.tsx +0 -111
- package/src/layout/Shell.tsx +0 -91
- package/src/layout/SidePanel.tsx +0 -316
- package/src/layout/StickyBanner.tsx +0 -83
- package/src/layout/Topbar.tsx +0 -68
- package/src/layout/index.ts +0 -24
- package/src/layout/useNavRailState.ts +0 -69
- package/src/lib/time-utils.ts +0 -44
- package/src/neobrutalism/Marquee.tsx +0 -81
- package/src/neobrutalism/Sticker.tsx +0 -71
- package/src/neobrutalism/index.ts +0 -4
- package/src/primitives/Avatar.tsx +0 -53
- package/src/primitives/Button.tsx +0 -67
- package/src/primitives/Card.tsx +0 -41
- package/src/primitives/Checkbox.tsx +0 -87
- package/src/primitives/Chip.tsx +0 -191
- package/src/primitives/Combobox.tsx +0 -507
- package/src/primitives/CopyField.tsx +0 -139
- package/src/primitives/CountBadge.tsx +0 -50
- package/src/primitives/Input.tsx +0 -204
- package/src/primitives/Kbd.tsx +0 -45
- package/src/primitives/MentionCombobox.tsx +0 -612
- package/src/primitives/MultiCombobox.tsx +0 -327
- package/src/primitives/PageHeader.tsx +0 -77
- package/src/primitives/Radio.tsx +0 -131
- package/src/primitives/SegmentedControl.tsx +0 -135
- package/src/primitives/Select.tsx +0 -779
- package/src/primitives/SignaturePad.tsx +0 -461
- package/src/primitives/Tag.tsx +0 -56
- package/src/primitives/Textarea.tsx +0 -62
- package/src/primitives/ToggleSwitch.tsx +0 -79
- package/src/primitives/Tooltip.tsx +0 -213
- package/src/primitives/index.ts +0 -53
- package/src/pwa/InstallPromptBanner.tsx +0 -132
- package/src/pwa/index.ts +0 -4
- package/src/pwa/manifest.template.json +0 -20
- package/src/pwa/registerSW.ts +0 -55
- package/src/riso/Halftone.tsx +0 -85
- package/src/riso/Misregister.tsx +0 -63
- package/src/riso/RisoStamp.tsx +0 -76
- package/src/riso/index.ts +0 -3
- package/src/sketch/HandUnderline.tsx +0 -53
- package/src/sketch/RoughArrow.tsx +0 -91
- package/src/sketch/RoughBox.tsx +0 -73
- package/src/sketch/StickyNote.tsx +0 -56
- package/src/sketch/index.ts +0 -4
- package/src/styles/base.css +0 -186
- package/src/styles/chrome-ambient.css +0 -235
- package/src/styles/chrome-bento.css +0 -192
- package/src/styles/chrome-editorial.css +0 -168
- package/src/styles/chrome-neobrutalism.css +0 -326
- package/src/styles/chrome-riso.css +0 -339
- package/src/styles/chrome-sketch.css +0 -362
- package/src/styles/chrome-swiss.css +0 -265
- package/src/styles/chrome-terminal.css +0 -272
- package/src/styles/fonts.css +0 -1034
- package/src/styles/index.css +0 -343
- package/src/styles/tokens.css +0 -941
- package/src/terminal/AsciiBox.tsx +0 -65
- package/src/terminal/BrailleSpinner.tsx +0 -46
- package/src/terminal/index.ts +0 -4
- package/src/theme/ThemeProvider.tsx +0 -99
- package/src/theme/index.ts +0 -6
- package/src/theme/inline-script.ts +0 -40
- package/src/theme/theme-context.ts +0 -7
- package/src/theme/types.ts +0 -39
- package/src/theme/useTheme.ts +0 -8
|
@@ -1,37 +0,0 @@
|
|
|
1
|
-
import { useContext, useEffect, useMemo } from "react";
|
|
2
|
-
import { CommandContext } from "./command-context";
|
|
3
|
-
import type { CommandItem } from "./types";
|
|
4
|
-
|
|
5
|
-
// Register a set of commands. Same lifecycle pattern as
|
|
6
|
-
// useActionBarItems — auto-unregister on unmount / sourceKey change.
|
|
7
|
-
//
|
|
8
|
-
// **Important**: same `ctx`-in-deps caveat as useActionBarItems —
|
|
9
|
-
// depending on the whole ctx object would cause an infinite render
|
|
10
|
-
// loop (register mutates provider state → ctx ref changes → effect
|
|
11
|
-
// re-runs → loop). Destructure register/unregister out so we depend
|
|
12
|
-
// on the stable useCallback'd refs instead.
|
|
13
|
-
|
|
14
|
-
export function useCommandSource(sourceKey: string, items: CommandItem[]) {
|
|
15
|
-
const ctx = useContext(CommandContext);
|
|
16
|
-
if (!ctx) {
|
|
17
|
-
throw new Error("useCommandSource must be used within <CommandProvider>");
|
|
18
|
-
}
|
|
19
|
-
const { register, unregister } = ctx;
|
|
20
|
-
const memo = useMemo(
|
|
21
|
-
() => items,
|
|
22
|
-
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
23
|
-
[items.length, items.map((i) => i.key).join("|")],
|
|
24
|
-
);
|
|
25
|
-
useEffect(() => {
|
|
26
|
-
register(sourceKey, memo);
|
|
27
|
-
return () => unregister(sourceKey);
|
|
28
|
-
}, [register, unregister, sourceKey, memo]);
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
export function useCommandPalette() {
|
|
32
|
-
const ctx = useContext(CommandContext);
|
|
33
|
-
if (!ctx) {
|
|
34
|
-
throw new Error("Command palette context not found");
|
|
35
|
-
}
|
|
36
|
-
return { open: ctx.open, setOpen: ctx.setOpen };
|
|
37
|
-
}
|
|
@@ -1,216 +0,0 @@
|
|
|
1
|
-
import { useCallback, useMemo, useRef, useState, type ReactNode } from "react";
|
|
2
|
-
import { DialogsContext } from "./dialogs-context";
|
|
3
|
-
import { Modal, ModalHeader, ModalBody, ModalFooter } from "./Modal";
|
|
4
|
-
import { Button } from "../primitives/Button";
|
|
5
|
-
import { Input } from "../primitives/Input";
|
|
6
|
-
import { ToastStack, type ToastItem } from "./Toast";
|
|
7
|
-
import type {
|
|
8
|
-
ConfirmOptions,
|
|
9
|
-
PromptOptions,
|
|
10
|
-
ToastOptions,
|
|
11
|
-
DialogsApi,
|
|
12
|
-
} from "./types";
|
|
13
|
-
|
|
14
|
-
// Single mount point. Hosts the imperative confirm/prompt/toast
|
|
15
|
-
// API used by useDialogs(). Promise-based so call sites flow
|
|
16
|
-
// naturally:
|
|
17
|
-
//
|
|
18
|
-
// const ok = await dialogs.confirm({ title: "Delete?", danger: true });
|
|
19
|
-
// if (!ok) return;
|
|
20
|
-
// await api.deleteThing();
|
|
21
|
-
//
|
|
22
|
-
// Toast queue handles its own dismiss timer (see Toast.tsx).
|
|
23
|
-
|
|
24
|
-
interface ConfirmState extends ConfirmOptions {
|
|
25
|
-
resolve: (value: boolean) => void;
|
|
26
|
-
}
|
|
27
|
-
interface PromptState extends PromptOptions {
|
|
28
|
-
resolve: (value: string | null) => void;
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
export function DialogsProvider({ children }: { children: ReactNode }) {
|
|
32
|
-
const [confirmState, setConfirmState] = useState<ConfirmState | null>(null);
|
|
33
|
-
const [promptState, setPromptState] = useState<PromptState | null>(null);
|
|
34
|
-
const [toasts, setToasts] = useState<ToastItem[]>([]);
|
|
35
|
-
const toastIdRef = useRef(0);
|
|
36
|
-
|
|
37
|
-
const confirm = useCallback(
|
|
38
|
-
(opts: ConfirmOptions) =>
|
|
39
|
-
new Promise<boolean>((resolve) => {
|
|
40
|
-
setConfirmState({ ...opts, resolve });
|
|
41
|
-
}),
|
|
42
|
-
[],
|
|
43
|
-
);
|
|
44
|
-
|
|
45
|
-
const prompt = useCallback(
|
|
46
|
-
(opts: PromptOptions) =>
|
|
47
|
-
new Promise<string | null>((resolve) => {
|
|
48
|
-
setPromptState({ ...opts, resolve });
|
|
49
|
-
}),
|
|
50
|
-
[],
|
|
51
|
-
);
|
|
52
|
-
|
|
53
|
-
const toast = useCallback((opts: ToastOptions) => {
|
|
54
|
-
const id = ++toastIdRef.current;
|
|
55
|
-
setToasts((t) => [...t, { ...opts, id }]);
|
|
56
|
-
}, []);
|
|
57
|
-
|
|
58
|
-
const dismissToast = useCallback((id: number) => {
|
|
59
|
-
setToasts((t) => t.filter((x) => x.id !== id));
|
|
60
|
-
}, []);
|
|
61
|
-
|
|
62
|
-
const api = useMemo<DialogsApi>(
|
|
63
|
-
() => ({ confirm, prompt, toast }),
|
|
64
|
-
[confirm, prompt, toast],
|
|
65
|
-
);
|
|
66
|
-
|
|
67
|
-
return (
|
|
68
|
-
<DialogsContext.Provider value={api}>
|
|
69
|
-
{children}
|
|
70
|
-
{confirmState && (
|
|
71
|
-
<ConfirmDialog
|
|
72
|
-
state={confirmState}
|
|
73
|
-
onDone={(v) => {
|
|
74
|
-
confirmState.resolve(v);
|
|
75
|
-
setConfirmState(null);
|
|
76
|
-
}}
|
|
77
|
-
/>
|
|
78
|
-
)}
|
|
79
|
-
{promptState && (
|
|
80
|
-
<PromptDialog
|
|
81
|
-
state={promptState}
|
|
82
|
-
onDone={(v) => {
|
|
83
|
-
promptState.resolve(v);
|
|
84
|
-
setPromptState(null);
|
|
85
|
-
}}
|
|
86
|
-
/>
|
|
87
|
-
)}
|
|
88
|
-
<ToastStack toasts={toasts} onDismiss={dismissToast} />
|
|
89
|
-
</DialogsContext.Provider>
|
|
90
|
-
);
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
// ---------- Confirm dialog ----------
|
|
94
|
-
|
|
95
|
-
function ConfirmDialog({
|
|
96
|
-
state,
|
|
97
|
-
onDone,
|
|
98
|
-
}: {
|
|
99
|
-
state: ConfirmState;
|
|
100
|
-
onDone: (value: boolean) => void;
|
|
101
|
-
}) {
|
|
102
|
-
const [typed, setTyped] = useState("");
|
|
103
|
-
const needsTyped = !!state.confirmationText;
|
|
104
|
-
const canConfirm =
|
|
105
|
-
!needsTyped ||
|
|
106
|
-
typed.trim().toLowerCase() === state.confirmationText!.trim().toLowerCase();
|
|
107
|
-
return (
|
|
108
|
-
<Modal open onClose={() => onDone(false)}>
|
|
109
|
-
<ModalHeader title={state.title} onClose={() => onDone(false)} />
|
|
110
|
-
{(state.message || needsTyped) && (
|
|
111
|
-
<ModalBody>
|
|
112
|
-
{state.message && (
|
|
113
|
-
<div
|
|
114
|
-
style={{
|
|
115
|
-
font: "400 14px/1.5 var(--ck-font-sans)",
|
|
116
|
-
color: "var(--ck-text-secondary)",
|
|
117
|
-
}}
|
|
118
|
-
>
|
|
119
|
-
{state.message}
|
|
120
|
-
</div>
|
|
121
|
-
)}
|
|
122
|
-
{needsTyped && (
|
|
123
|
-
<div style={{ marginTop: 16 }}>
|
|
124
|
-
<Input
|
|
125
|
-
value={typed}
|
|
126
|
-
onChange={(e) => setTyped(e.target.value)}
|
|
127
|
-
placeholder={state.confirmationText}
|
|
128
|
-
helper={`Type "${state.confirmationText}" to confirm.`}
|
|
129
|
-
/>
|
|
130
|
-
</div>
|
|
131
|
-
)}
|
|
132
|
-
</ModalBody>
|
|
133
|
-
)}
|
|
134
|
-
<ModalFooter>
|
|
135
|
-
<Button variant="ghost" onClick={() => onDone(false)}>
|
|
136
|
-
{state.cancelLabel ?? "Cancel"}
|
|
137
|
-
</Button>
|
|
138
|
-
<Button
|
|
139
|
-
variant="primary"
|
|
140
|
-
onClick={() => onDone(true)}
|
|
141
|
-
disabled={!canConfirm}
|
|
142
|
-
style={
|
|
143
|
-
state.danger
|
|
144
|
-
? {
|
|
145
|
-
background: "var(--ck-critical)",
|
|
146
|
-
color: "var(--ck-text-inverse)",
|
|
147
|
-
}
|
|
148
|
-
: undefined
|
|
149
|
-
}
|
|
150
|
-
>
|
|
151
|
-
{state.confirmLabel ?? "Confirm"}
|
|
152
|
-
</Button>
|
|
153
|
-
</ModalFooter>
|
|
154
|
-
</Modal>
|
|
155
|
-
);
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
// ---------- Prompt dialog ----------
|
|
159
|
-
|
|
160
|
-
function PromptDialog({
|
|
161
|
-
state,
|
|
162
|
-
onDone,
|
|
163
|
-
}: {
|
|
164
|
-
state: PromptState;
|
|
165
|
-
onDone: (value: string | null) => void;
|
|
166
|
-
}) {
|
|
167
|
-
const [value, setValue] = useState(state.defaultValue ?? "");
|
|
168
|
-
const [error, setError] = useState<string | null>(null);
|
|
169
|
-
const submit = () => {
|
|
170
|
-
const err = state.validate?.(value);
|
|
171
|
-
if (err) {
|
|
172
|
-
setError(err);
|
|
173
|
-
return;
|
|
174
|
-
}
|
|
175
|
-
onDone(value);
|
|
176
|
-
};
|
|
177
|
-
return (
|
|
178
|
-
<Modal open onClose={() => onDone(null)}>
|
|
179
|
-
<ModalHeader title={state.title} onClose={() => onDone(null)} />
|
|
180
|
-
<ModalBody>
|
|
181
|
-
{state.message && (
|
|
182
|
-
<div
|
|
183
|
-
style={{
|
|
184
|
-
font: "400 14px/1.5 var(--ck-font-sans)",
|
|
185
|
-
color: "var(--ck-text-secondary)",
|
|
186
|
-
marginBottom: 12,
|
|
187
|
-
}}
|
|
188
|
-
>
|
|
189
|
-
{state.message}
|
|
190
|
-
</div>
|
|
191
|
-
)}
|
|
192
|
-
<Input
|
|
193
|
-
value={value}
|
|
194
|
-
onChange={(e) => {
|
|
195
|
-
setValue(e.target.value);
|
|
196
|
-
if (error) setError(null);
|
|
197
|
-
}}
|
|
198
|
-
placeholder={state.placeholder}
|
|
199
|
-
error={error}
|
|
200
|
-
onKeyDown={(e) => {
|
|
201
|
-
if (e.key === "Enter") submit();
|
|
202
|
-
}}
|
|
203
|
-
autoFocus
|
|
204
|
-
/>
|
|
205
|
-
</ModalBody>
|
|
206
|
-
<ModalFooter>
|
|
207
|
-
<Button variant="ghost" onClick={() => onDone(null)}>
|
|
208
|
-
{state.cancelLabel ?? "Cancel"}
|
|
209
|
-
</Button>
|
|
210
|
-
<Button variant="primary" onClick={submit}>
|
|
211
|
-
{state.confirmLabel ?? "OK"}
|
|
212
|
-
</Button>
|
|
213
|
-
</ModalFooter>
|
|
214
|
-
</Modal>
|
|
215
|
-
);
|
|
216
|
-
}
|
package/src/dialogs/Modal.tsx
DELETED
|
@@ -1,260 +0,0 @@
|
|
|
1
|
-
import { useEffect, type ReactNode, type CSSProperties } from "react";
|
|
2
|
-
import { createPortal } from "react-dom";
|
|
3
|
-
|
|
4
|
-
import { useViewport } from "../hooks/useViewport";
|
|
5
|
-
|
|
6
|
-
// Modal base. Portals to document.body so it escapes ancestor
|
|
7
|
-
// stacking contexts. Closes on Esc + backdrop click (both opt-out).
|
|
8
|
-
// Caller owns open state and slots in the header/body/footer.
|
|
9
|
-
//
|
|
10
|
-
// Provides three sub-components for compositional layout:
|
|
11
|
-
// - <Modal> — the host (backdrop + card + close binding)
|
|
12
|
-
// - <ModalHeader> — sticky-ish top with title + optional close X
|
|
13
|
-
// - <ModalBody> — scrollable middle region
|
|
14
|
-
// - <ModalFooter> — sticky-ish bottom (buttons)
|
|
15
|
-
|
|
16
|
-
export interface ModalProps {
|
|
17
|
-
open: boolean;
|
|
18
|
-
onClose: () => void;
|
|
19
|
-
// Disables close on backdrop click + Escape. Useful for blocking
|
|
20
|
-
// dialogs that must be resolved via an explicit button.
|
|
21
|
-
dismissable?: boolean;
|
|
22
|
-
// Width preset OR raw px. Default "md" (440 px).
|
|
23
|
-
maxWidth?: "sm" | "md" | "lg" | "xl" | "2xl" | "3xl" | "4xl" | "5xl" | number;
|
|
24
|
-
// Inline style override for the card.
|
|
25
|
-
cardStyle?: CSSProperties;
|
|
26
|
-
children: ReactNode;
|
|
27
|
-
// Optional aria-labelled-by for screen readers (point at the
|
|
28
|
-
// ModalHeader title's id).
|
|
29
|
-
labelledBy?: string;
|
|
30
|
-
// How the modal presents on PHONE viewports (no effect elsewhere):
|
|
31
|
-
// "center" (default) — the desktop card, scaled to fit
|
|
32
|
-
// "page" — full-screen, slides in from the right like
|
|
33
|
-
// a pushed native page. For content dialogs
|
|
34
|
-
// the user enters to work in.
|
|
35
|
-
// "sheet" — bottom sheet rising from the lower edge.
|
|
36
|
-
// For light pickers / short forms.
|
|
37
|
-
// Confirm-style dialogs should stay "center" — native alerts are
|
|
38
|
-
// centered cards too.
|
|
39
|
-
phonePresentation?: "center" | "page" | "sheet";
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
const WIDTHS = { sm: 340, md: 440, lg: 560, xl: 720, "2xl": 880, "3xl": 1024, "4xl": 1200, "5xl": 1440 };
|
|
43
|
-
|
|
44
|
-
export function Modal({
|
|
45
|
-
open,
|
|
46
|
-
onClose,
|
|
47
|
-
dismissable = true,
|
|
48
|
-
maxWidth = "md",
|
|
49
|
-
cardStyle,
|
|
50
|
-
children,
|
|
51
|
-
labelledBy,
|
|
52
|
-
phonePresentation = "center",
|
|
53
|
-
}: ModalProps) {
|
|
54
|
-
const vp = useViewport();
|
|
55
|
-
const mode = vp.isPhone ? phonePresentation : "center";
|
|
56
|
-
useEffect(() => {
|
|
57
|
-
if (!open || !dismissable) return;
|
|
58
|
-
const onKey = (e: KeyboardEvent) => {
|
|
59
|
-
if (e.key === "Escape") {
|
|
60
|
-
e.preventDefault();
|
|
61
|
-
onClose();
|
|
62
|
-
}
|
|
63
|
-
};
|
|
64
|
-
window.addEventListener("keydown", onKey);
|
|
65
|
-
return () => window.removeEventListener("keydown", onKey);
|
|
66
|
-
}, [open, dismissable, onClose]);
|
|
67
|
-
|
|
68
|
-
// Lock body scroll while open so the backdrop doesn't reveal
|
|
69
|
-
// a scrolling page underneath.
|
|
70
|
-
useEffect(() => {
|
|
71
|
-
if (!open) return;
|
|
72
|
-
const prev = document.body.style.overflow;
|
|
73
|
-
document.body.style.overflow = "hidden";
|
|
74
|
-
return () => {
|
|
75
|
-
document.body.style.overflow = prev;
|
|
76
|
-
};
|
|
77
|
-
}, [open]);
|
|
78
|
-
|
|
79
|
-
if (!open) return null;
|
|
80
|
-
const width = typeof maxWidth === "number" ? maxWidth : WIDTHS[maxWidth];
|
|
81
|
-
return createPortal(
|
|
82
|
-
<div
|
|
83
|
-
role="dialog"
|
|
84
|
-
aria-modal="true"
|
|
85
|
-
aria-labelledby={labelledBy}
|
|
86
|
-
style={{
|
|
87
|
-
position: "fixed",
|
|
88
|
-
inset: 0,
|
|
89
|
-
background: "rgba(10, 14, 26, 0.6)",
|
|
90
|
-
// Pull the page surface noticeably out of focus behind the
|
|
91
|
-
// modal — agent-dataroom's `backdrop-blur-sm` (4px) reads as
|
|
92
|
-
// a hint; 8px reads as a clear "the world stopped". The card
|
|
93
|
-
// sits above the blurred layer, so the content stays crisp.
|
|
94
|
-
backdropFilter: "blur(8px)",
|
|
95
|
-
WebkitBackdropFilter: "blur(8px)",
|
|
96
|
-
display: "flex",
|
|
97
|
-
alignItems: mode === "sheet" ? "flex-end" : "center",
|
|
98
|
-
justifyContent: "center",
|
|
99
|
-
padding: mode === "center" ? 16 : 0,
|
|
100
|
-
zIndex: 100,
|
|
101
|
-
}}
|
|
102
|
-
onMouseDown={(e) => {
|
|
103
|
-
// Backdrop click closes; clicks inside the card stopPropagation.
|
|
104
|
-
if (dismissable && e.target === e.currentTarget) onClose();
|
|
105
|
-
}}
|
|
106
|
-
className="ck-anim-fade"
|
|
107
|
-
>
|
|
108
|
-
<div
|
|
109
|
-
style={{
|
|
110
|
-
width: "100%",
|
|
111
|
-
maxWidth: mode === "center" ? width : "100%",
|
|
112
|
-
maxHeight:
|
|
113
|
-
mode === "page" ? "100dvh" : mode === "sheet" ? "92dvh" : "calc(100vh - 32px)",
|
|
114
|
-
...(mode === "page" ? { height: "100dvh" } : {}),
|
|
115
|
-
background: "var(--ck-bg-surface)",
|
|
116
|
-
border: "1px solid var(--ck-border-subtle)",
|
|
117
|
-
// Modal cards lift on the `-lg` radius rather than the
|
|
118
|
-
// page's default `-md` so the corners read as softer than
|
|
119
|
-
// the cards underneath them — supports the "this took
|
|
120
|
-
// over" cue alongside the deeper overlay shadow.
|
|
121
|
-
borderRadius:
|
|
122
|
-
mode === "page"
|
|
123
|
-
? 0
|
|
124
|
-
: mode === "sheet"
|
|
125
|
-
? "var(--ck-radius-lg) var(--ck-radius-lg) 0 0"
|
|
126
|
-
: "var(--ck-radius-lg)",
|
|
127
|
-
...(mode === "page" ? { border: "none" } : {}),
|
|
128
|
-
// Overlay shadow > shadow-3. shadow-3 was tuned for cards
|
|
129
|
-
// sitting on a page; a modal needs to read as physically
|
|
130
|
-
// lifted off the page, not as another tier of card.
|
|
131
|
-
boxShadow: "var(--ck-shadow-overlay)",
|
|
132
|
-
display: "flex",
|
|
133
|
-
flexDirection: "column",
|
|
134
|
-
overflow: "hidden",
|
|
135
|
-
fontFamily: "var(--ck-font-sans)",
|
|
136
|
-
color: "var(--ck-text-primary)",
|
|
137
|
-
...cardStyle,
|
|
138
|
-
}}
|
|
139
|
-
className={
|
|
140
|
-
mode === "page"
|
|
141
|
-
? "ck-anim-page-push"
|
|
142
|
-
: mode === "sheet"
|
|
143
|
-
? "ck-anim-sheet-up"
|
|
144
|
-
: "ck-anim-popover"
|
|
145
|
-
}
|
|
146
|
-
>
|
|
147
|
-
{children}
|
|
148
|
-
</div>
|
|
149
|
-
</div>,
|
|
150
|
-
document.body,
|
|
151
|
-
);
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
// ---------- Slot components ----------
|
|
155
|
-
|
|
156
|
-
export interface ModalHeaderProps {
|
|
157
|
-
title?: ReactNode;
|
|
158
|
-
subtitle?: ReactNode;
|
|
159
|
-
onClose?: () => void;
|
|
160
|
-
titleId?: string;
|
|
161
|
-
}
|
|
162
|
-
|
|
163
|
-
export function ModalHeader({ title, subtitle, onClose, titleId }: ModalHeaderProps) {
|
|
164
|
-
if (!title && !subtitle && !onClose) return null;
|
|
165
|
-
return (
|
|
166
|
-
<header
|
|
167
|
-
style={{
|
|
168
|
-
// Editorial-modal breathing room: matches the 24-px gutter
|
|
169
|
-
// ModalBody / ModalFooter ship so a Modal renders as one
|
|
170
|
-
// coherent column of 24-px insets rather than three slots
|
|
171
|
-
// with different paddings.
|
|
172
|
-
padding: "20px 24px",
|
|
173
|
-
borderBottom: "1px solid var(--ck-border-subtle)",
|
|
174
|
-
display: "flex",
|
|
175
|
-
alignItems: "flex-start",
|
|
176
|
-
gap: 12,
|
|
177
|
-
}}
|
|
178
|
-
>
|
|
179
|
-
<div style={{ flex: 1, minWidth: 0 }}>
|
|
180
|
-
{title && (
|
|
181
|
-
<h2
|
|
182
|
-
id={titleId}
|
|
183
|
-
style={{
|
|
184
|
-
font: "500 16px/1.3 var(--ck-font-sans)",
|
|
185
|
-
margin: 0,
|
|
186
|
-
color: "var(--ck-text-primary)",
|
|
187
|
-
}}
|
|
188
|
-
>
|
|
189
|
-
{title}
|
|
190
|
-
</h2>
|
|
191
|
-
)}
|
|
192
|
-
{subtitle && (
|
|
193
|
-
<p
|
|
194
|
-
style={{
|
|
195
|
-
font: "400 12px/1.4 var(--ck-font-sans)",
|
|
196
|
-
color: "var(--ck-text-tertiary)",
|
|
197
|
-
margin: "4px 0 0",
|
|
198
|
-
}}
|
|
199
|
-
>
|
|
200
|
-
{subtitle}
|
|
201
|
-
</p>
|
|
202
|
-
)}
|
|
203
|
-
</div>
|
|
204
|
-
{onClose && (
|
|
205
|
-
<button
|
|
206
|
-
type="button"
|
|
207
|
-
onClick={onClose}
|
|
208
|
-
aria-label="Close"
|
|
209
|
-
onMouseEnter={(e) => {
|
|
210
|
-
(e.currentTarget as HTMLButtonElement).style.background =
|
|
211
|
-
"var(--ck-bg-muted)";
|
|
212
|
-
}}
|
|
213
|
-
onMouseLeave={(e) => {
|
|
214
|
-
(e.currentTarget as HTMLButtonElement).style.background =
|
|
215
|
-
"transparent";
|
|
216
|
-
}}
|
|
217
|
-
style={{
|
|
218
|
-
border: "none",
|
|
219
|
-
background: "transparent",
|
|
220
|
-
color: "var(--ck-text-tertiary)",
|
|
221
|
-
cursor: "pointer",
|
|
222
|
-
fontSize: 18,
|
|
223
|
-
lineHeight: 1,
|
|
224
|
-
padding: 4,
|
|
225
|
-
borderRadius: "var(--ck-radius-sm, 4px)",
|
|
226
|
-
transition:
|
|
227
|
-
"background var(--ck-dur-fast) var(--ck-ease)",
|
|
228
|
-
}}
|
|
229
|
-
>
|
|
230
|
-
×
|
|
231
|
-
</button>
|
|
232
|
-
)}
|
|
233
|
-
</header>
|
|
234
|
-
);
|
|
235
|
-
}
|
|
236
|
-
|
|
237
|
-
export function ModalBody({ children }: { children: ReactNode }) {
|
|
238
|
-
return (
|
|
239
|
-
<div style={{ flex: 1, minHeight: 0, overflowY: "auto", padding: "20px 24px 24px" }}>
|
|
240
|
-
{children}
|
|
241
|
-
</div>
|
|
242
|
-
);
|
|
243
|
-
}
|
|
244
|
-
|
|
245
|
-
export function ModalFooter({ children }: { children: ReactNode }) {
|
|
246
|
-
return (
|
|
247
|
-
<footer
|
|
248
|
-
style={{
|
|
249
|
-
padding: "16px 24px",
|
|
250
|
-
borderTop: "1px solid var(--ck-border-subtle)",
|
|
251
|
-
display: "flex",
|
|
252
|
-
justifyContent: "flex-end",
|
|
253
|
-
gap: 8,
|
|
254
|
-
background: "var(--ck-bg-surface-2)",
|
|
255
|
-
}}
|
|
256
|
-
>
|
|
257
|
-
{children}
|
|
258
|
-
</footer>
|
|
259
|
-
);
|
|
260
|
-
}
|
package/src/dialogs/Toast.tsx
DELETED
|
@@ -1,85 +0,0 @@
|
|
|
1
|
-
import { useEffect } from "react";
|
|
2
|
-
import { createPortal } from "react-dom";
|
|
3
|
-
import type { ToastOptions } from "./types";
|
|
4
|
-
|
|
5
|
-
interface ToastItem extends ToastOptions {
|
|
6
|
-
id: number;
|
|
7
|
-
}
|
|
8
|
-
|
|
9
|
-
// Toast stack — fixed bottom-right corner. Each toast auto-dismisses
|
|
10
|
-
// after durationMs (default 4000). Stack grows upward.
|
|
11
|
-
|
|
12
|
-
const KIND_BG = {
|
|
13
|
-
info: "var(--ck-bg-surface)",
|
|
14
|
-
success: "var(--ck-success-muted)",
|
|
15
|
-
error: "var(--ck-critical-muted)",
|
|
16
|
-
};
|
|
17
|
-
const KIND_BORDER = {
|
|
18
|
-
info: "var(--ck-border-strong)",
|
|
19
|
-
success: "var(--ck-success)",
|
|
20
|
-
error: "var(--ck-critical)",
|
|
21
|
-
};
|
|
22
|
-
const KIND_TEXT = {
|
|
23
|
-
info: "var(--ck-text-primary)",
|
|
24
|
-
success: "var(--ck-success)",
|
|
25
|
-
error: "var(--ck-critical)",
|
|
26
|
-
};
|
|
27
|
-
|
|
28
|
-
export function ToastStack({
|
|
29
|
-
toasts,
|
|
30
|
-
onDismiss,
|
|
31
|
-
}: {
|
|
32
|
-
toasts: ToastItem[];
|
|
33
|
-
onDismiss: (id: number) => void;
|
|
34
|
-
}) {
|
|
35
|
-
return createPortal(
|
|
36
|
-
<div
|
|
37
|
-
style={{
|
|
38
|
-
position: "fixed",
|
|
39
|
-
bottom: 16,
|
|
40
|
-
right: 16,
|
|
41
|
-
zIndex: 110,
|
|
42
|
-
display: "flex",
|
|
43
|
-
flexDirection: "column",
|
|
44
|
-
gap: 8,
|
|
45
|
-
maxWidth: 360,
|
|
46
|
-
pointerEvents: "none",
|
|
47
|
-
}}
|
|
48
|
-
>
|
|
49
|
-
{toasts.map((t) => (
|
|
50
|
-
<ToastRow key={t.id} item={t} onDismiss={() => onDismiss(t.id)} />
|
|
51
|
-
))}
|
|
52
|
-
</div>,
|
|
53
|
-
document.body,
|
|
54
|
-
);
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
function ToastRow({ item, onDismiss }: { item: ToastItem; onDismiss: () => void }) {
|
|
58
|
-
const kind = item.kind ?? "info";
|
|
59
|
-
const duration = item.durationMs ?? 4000;
|
|
60
|
-
useEffect(() => {
|
|
61
|
-
const timer = setTimeout(onDismiss, duration);
|
|
62
|
-
return () => clearTimeout(timer);
|
|
63
|
-
}, [duration, onDismiss]);
|
|
64
|
-
return (
|
|
65
|
-
<div
|
|
66
|
-
className="ck-anim-slide-left"
|
|
67
|
-
style={{
|
|
68
|
-
pointerEvents: "auto",
|
|
69
|
-
padding: "12px 14px",
|
|
70
|
-
background: KIND_BG[kind],
|
|
71
|
-
border: `1px solid ${KIND_BORDER[kind]}`,
|
|
72
|
-
borderRadius: "var(--ck-radius-md)",
|
|
73
|
-
boxShadow: "var(--ck-shadow-2)",
|
|
74
|
-
color: KIND_TEXT[kind],
|
|
75
|
-
font: "400 13px/1.4 var(--ck-font-sans)",
|
|
76
|
-
cursor: "pointer",
|
|
77
|
-
}}
|
|
78
|
-
onClick={onDismiss}
|
|
79
|
-
>
|
|
80
|
-
{item.message}
|
|
81
|
-
</div>
|
|
82
|
-
);
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
export type { ToastItem };
|
|
@@ -1,6 +0,0 @@
|
|
|
1
|
-
import { createContext } from "react";
|
|
2
|
-
import type { DialogsApi } from "./types";
|
|
3
|
-
|
|
4
|
-
// React Fast Refresh requires component modules to export only
|
|
5
|
-
// components — same reason ThemeContext lives in its own file.
|
|
6
|
-
export const DialogsContext = createContext<DialogsApi | null>(null);
|
package/src/dialogs/index.ts
DELETED
|
@@ -1,10 +0,0 @@
|
|
|
1
|
-
export { Modal, ModalHeader, ModalBody, ModalFooter } from "./Modal";
|
|
2
|
-
export type { ModalProps, ModalHeaderProps } from "./Modal";
|
|
3
|
-
export { DialogsProvider } from "./DialogsProvider";
|
|
4
|
-
export { useDialogs } from "./useDialogs";
|
|
5
|
-
export type {
|
|
6
|
-
ConfirmOptions,
|
|
7
|
-
PromptOptions,
|
|
8
|
-
ToastOptions,
|
|
9
|
-
DialogsApi,
|
|
10
|
-
} from "./types";
|
package/src/dialogs/types.ts
DELETED
|
@@ -1,37 +0,0 @@
|
|
|
1
|
-
import type { ReactNode } from "react";
|
|
2
|
-
|
|
3
|
-
export interface ConfirmOptions {
|
|
4
|
-
title: string;
|
|
5
|
-
message?: ReactNode;
|
|
6
|
-
confirmLabel?: string;
|
|
7
|
-
cancelLabel?: string;
|
|
8
|
-
// Renders the confirm button as the critical tone.
|
|
9
|
-
danger?: boolean;
|
|
10
|
-
// When set, the user must type this string into a confirmation
|
|
11
|
-
// field before confirm is enabled. Case-insensitive.
|
|
12
|
-
confirmationText?: string;
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
export interface PromptOptions {
|
|
16
|
-
title: string;
|
|
17
|
-
message?: ReactNode;
|
|
18
|
-
defaultValue?: string;
|
|
19
|
-
placeholder?: string;
|
|
20
|
-
confirmLabel?: string;
|
|
21
|
-
cancelLabel?: string;
|
|
22
|
-
// Returning a string treats it as a validation error message;
|
|
23
|
-
// returning null/undefined accepts the input.
|
|
24
|
-
validate?: (value: string) => string | null | undefined;
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
export interface ToastOptions {
|
|
28
|
-
kind?: "info" | "success" | "error";
|
|
29
|
-
message: ReactNode;
|
|
30
|
-
durationMs?: number;
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
export interface DialogsApi {
|
|
34
|
-
confirm: (opts: ConfirmOptions) => Promise<boolean>;
|
|
35
|
-
prompt: (opts: PromptOptions) => Promise<string | null>;
|
|
36
|
-
toast: (opts: ToastOptions) => void;
|
|
37
|
-
}
|