@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.
Files changed (165) hide show
  1. package/CHANGELOG.md +597 -0
  2. package/NOTICE +11 -0
  3. package/README.md +52 -0
  4. package/package.json +57 -7
  5. package/src/brand.css +13 -0
  6. package/src/components/Badge.tsx +68 -0
  7. package/src/components/Button.tsx +178 -0
  8. package/src/components/Card.tsx +42 -0
  9. package/src/components/Checkbox.tsx +67 -0
  10. package/src/components/Dialog.tsx +111 -0
  11. package/src/components/Field.tsx +66 -0
  12. package/src/components/Figure.tsx +30 -0
  13. package/src/components/Icon.tsx +32 -0
  14. package/src/components/IconButton.tsx +55 -0
  15. package/src/components/Input.tsx +74 -0
  16. package/src/components/Logo.tsx +62 -0
  17. package/src/components/Marker.tsx +26 -0
  18. package/src/components/MetaLabel.tsx +38 -0
  19. package/src/components/Radio.tsx +72 -0
  20. package/src/components/Select.tsx +351 -0
  21. package/src/components/Spinner.tsx +20 -0
  22. package/src/components/Switch.tsx +67 -0
  23. package/src/components/Table.tsx +196 -0
  24. package/src/components/Tabs.tsx +124 -0
  25. package/src/components/Tag.tsx +35 -0
  26. package/src/components/Textarea.tsx +46 -0
  27. package/src/components/ThemeProvider.tsx +80 -0
  28. package/src/components/Toast.tsx +159 -0
  29. package/src/components/Tooltip.tsx +57 -0
  30. package/src/components/core.test.tsx +242 -0
  31. package/src/components/forms-control.ts +36 -0
  32. package/src/components/forms.test.tsx +280 -0
  33. package/src/components/logo-art.ts +4 -0
  34. package/src/components/overlays.test.tsx +245 -0
  35. package/src/components/useButtonAction.ts +57 -0
  36. package/src/fonts.css +20 -0
  37. package/src/index.ts +34 -24
  38. package/src/lib/cn.ts +23 -6
  39. package/src/modes.css +10 -0
  40. package/src/styles.css +19 -0
  41. package/src/theme.css +143 -0
  42. package/src/tokens/colors.css +112 -0
  43. package/src/tokens/compat.css +36 -0
  44. package/src/tokens/elevation.css +14 -0
  45. package/src/tokens/motion.css +42 -0
  46. package/src/tokens/spacing.css +55 -0
  47. package/src/tokens/typography-cjk.css +42 -0
  48. package/src/tokens/typography.css +81 -0
  49. package/src/actionbar/ActionBar.tsx +0 -837
  50. package/src/actionbar/ActionBarButton.tsx +0 -111
  51. package/src/actionbar/ActionBarMenuGroup.tsx +0 -116
  52. package/src/actionbar/ActionBarModeHandle.tsx +0 -171
  53. package/src/actionbar/ActionBarProvider.tsx +0 -91
  54. package/src/actionbar/actionbar-context.ts +0 -45
  55. package/src/actionbar/index.ts +0 -28
  56. package/src/actionbar/types.ts +0 -147
  57. package/src/actionbar/useActionBarItems.ts +0 -60
  58. package/src/actionbar/useActionBarMode.ts +0 -144
  59. package/src/actionbar/useActionBarPanel.ts +0 -56
  60. package/src/actionbar/useActionBarStatusDot.ts +0 -49
  61. package/src/actionbar/useActionBarToast.ts +0 -33
  62. package/src/ambient/AmbientBackdrop.tsx +0 -74
  63. package/src/ambient/CommandInput.tsx +0 -107
  64. package/src/ambient/SuperbarStrip.tsx +0 -36
  65. package/src/ambient/index.ts +0 -6
  66. package/src/bento/BentoCell.tsx +0 -66
  67. package/src/bento/BentoGrid.tsx +0 -42
  68. package/src/bento/index.ts +0 -2
  69. package/src/command/CommandPalette.tsx +0 -329
  70. package/src/command/CommandProvider.tsx +0 -57
  71. package/src/command/command-context.ts +0 -12
  72. package/src/command/index.ts +0 -6
  73. package/src/command/rank.ts +0 -50
  74. package/src/command/types.ts +0 -37
  75. package/src/command/useCommandSource.ts +0 -37
  76. package/src/dialogs/DialogsProvider.tsx +0 -216
  77. package/src/dialogs/Modal.tsx +0 -260
  78. package/src/dialogs/Toast.tsx +0 -85
  79. package/src/dialogs/dialogs-context.ts +0 -6
  80. package/src/dialogs/index.ts +0 -10
  81. package/src/dialogs/types.ts +0 -37
  82. package/src/dialogs/useDialogs.ts +0 -8
  83. package/src/editorial/EditorialSpotlight.tsx +0 -63
  84. package/src/editorial/Folio.tsx +0 -52
  85. package/src/editorial/PlateMarker.tsx +0 -33
  86. package/src/editorial/RomanSection.tsx +0 -65
  87. package/src/editorial/RunningMarginalia.tsx +0 -65
  88. package/src/editorial/index.ts +0 -10
  89. package/src/hooks/index.ts +0 -5
  90. package/src/hooks/useKeyboardHotkey.ts +0 -93
  91. package/src/hooks/useReducedMotion.ts +0 -20
  92. package/src/hooks/useViewport.ts +0 -61
  93. package/src/layout/Breadcrumb.tsx +0 -74
  94. package/src/layout/LeftNavRail.tsx +0 -139
  95. package/src/layout/MobileTabBar.tsx +0 -101
  96. package/src/layout/NavItem.tsx +0 -128
  97. package/src/layout/NavSearchTrigger.tsx +0 -88
  98. package/src/layout/NavSection.tsx +0 -40
  99. package/src/layout/RightSidebarPanel.tsx +0 -111
  100. package/src/layout/Shell.tsx +0 -91
  101. package/src/layout/SidePanel.tsx +0 -316
  102. package/src/layout/StickyBanner.tsx +0 -83
  103. package/src/layout/Topbar.tsx +0 -68
  104. package/src/layout/index.ts +0 -24
  105. package/src/layout/useNavRailState.ts +0 -69
  106. package/src/lib/time-utils.ts +0 -44
  107. package/src/neobrutalism/Marquee.tsx +0 -81
  108. package/src/neobrutalism/Sticker.tsx +0 -71
  109. package/src/neobrutalism/index.ts +0 -4
  110. package/src/primitives/Avatar.tsx +0 -53
  111. package/src/primitives/Button.tsx +0 -67
  112. package/src/primitives/Card.tsx +0 -41
  113. package/src/primitives/Checkbox.tsx +0 -87
  114. package/src/primitives/Chip.tsx +0 -191
  115. package/src/primitives/Combobox.tsx +0 -507
  116. package/src/primitives/CopyField.tsx +0 -139
  117. package/src/primitives/CountBadge.tsx +0 -50
  118. package/src/primitives/Input.tsx +0 -204
  119. package/src/primitives/Kbd.tsx +0 -45
  120. package/src/primitives/MentionCombobox.tsx +0 -612
  121. package/src/primitives/MultiCombobox.tsx +0 -327
  122. package/src/primitives/PageHeader.tsx +0 -77
  123. package/src/primitives/Radio.tsx +0 -131
  124. package/src/primitives/SegmentedControl.tsx +0 -135
  125. package/src/primitives/Select.tsx +0 -779
  126. package/src/primitives/SignaturePad.tsx +0 -461
  127. package/src/primitives/Tag.tsx +0 -56
  128. package/src/primitives/Textarea.tsx +0 -62
  129. package/src/primitives/ToggleSwitch.tsx +0 -79
  130. package/src/primitives/Tooltip.tsx +0 -213
  131. package/src/primitives/index.ts +0 -53
  132. package/src/pwa/InstallPromptBanner.tsx +0 -132
  133. package/src/pwa/index.ts +0 -4
  134. package/src/pwa/manifest.template.json +0 -20
  135. package/src/pwa/registerSW.ts +0 -55
  136. package/src/riso/Halftone.tsx +0 -85
  137. package/src/riso/Misregister.tsx +0 -63
  138. package/src/riso/RisoStamp.tsx +0 -76
  139. package/src/riso/index.ts +0 -3
  140. package/src/sketch/HandUnderline.tsx +0 -53
  141. package/src/sketch/RoughArrow.tsx +0 -91
  142. package/src/sketch/RoughBox.tsx +0 -73
  143. package/src/sketch/StickyNote.tsx +0 -56
  144. package/src/sketch/index.ts +0 -4
  145. package/src/styles/base.css +0 -186
  146. package/src/styles/chrome-ambient.css +0 -235
  147. package/src/styles/chrome-bento.css +0 -192
  148. package/src/styles/chrome-editorial.css +0 -168
  149. package/src/styles/chrome-neobrutalism.css +0 -326
  150. package/src/styles/chrome-riso.css +0 -339
  151. package/src/styles/chrome-sketch.css +0 -362
  152. package/src/styles/chrome-swiss.css +0 -265
  153. package/src/styles/chrome-terminal.css +0 -272
  154. package/src/styles/fonts.css +0 -1034
  155. package/src/styles/index.css +0 -343
  156. package/src/styles/tokens.css +0 -941
  157. package/src/terminal/AsciiBox.tsx +0 -65
  158. package/src/terminal/BrailleSpinner.tsx +0 -46
  159. package/src/terminal/index.ts +0 -4
  160. package/src/theme/ThemeProvider.tsx +0 -99
  161. package/src/theme/index.ts +0 -6
  162. package/src/theme/inline-script.ts +0 -40
  163. package/src/theme/theme-context.ts +0 -7
  164. package/src/theme/types.ts +0 -39
  165. 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
- }
@@ -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
- }
@@ -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);
@@ -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";
@@ -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
- }
@@ -1,8 +0,0 @@
1
- import { useContext } from "react";
2
- import { DialogsContext } from "./dialogs-context";
3
-
4
- export function useDialogs() {
5
- const ctx = useContext(DialogsContext);
6
- if (!ctx) throw new Error("useDialogs must be used within <DialogsProvider>");
7
- return ctx;
8
- }