@cosxai/ui 0.25.0 → 1.0.0-alpha.0

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 (133) hide show
  1. package/CHANGELOG.md +577 -0
  2. package/NOTICE +11 -0
  3. package/README.md +52 -0
  4. package/package.json +33 -7
  5. package/src/fonts.css +20 -0
  6. package/src/index.ts +8 -25
  7. package/src/lib/cn.ts +23 -6
  8. package/src/styles.css +17 -0
  9. package/src/theme.css +132 -0
  10. package/src/tokens/colors.css +112 -0
  11. package/src/tokens/compat.css +36 -0
  12. package/src/tokens/elevation.css +14 -0
  13. package/src/tokens/motion.css +42 -0
  14. package/src/tokens/spacing.css +55 -0
  15. package/src/tokens/typography-cjk.css +42 -0
  16. package/src/tokens/typography.css +81 -0
  17. package/src/actionbar/ActionBar.tsx +0 -837
  18. package/src/actionbar/ActionBarButton.tsx +0 -111
  19. package/src/actionbar/ActionBarMenuGroup.tsx +0 -116
  20. package/src/actionbar/ActionBarModeHandle.tsx +0 -171
  21. package/src/actionbar/ActionBarProvider.tsx +0 -91
  22. package/src/actionbar/actionbar-context.ts +0 -45
  23. package/src/actionbar/index.ts +0 -28
  24. package/src/actionbar/types.ts +0 -147
  25. package/src/actionbar/useActionBarItems.ts +0 -60
  26. package/src/actionbar/useActionBarMode.ts +0 -144
  27. package/src/actionbar/useActionBarPanel.ts +0 -56
  28. package/src/actionbar/useActionBarStatusDot.ts +0 -49
  29. package/src/actionbar/useActionBarToast.ts +0 -33
  30. package/src/ambient/AmbientBackdrop.tsx +0 -74
  31. package/src/ambient/CommandInput.tsx +0 -107
  32. package/src/ambient/SuperbarStrip.tsx +0 -36
  33. package/src/ambient/index.ts +0 -6
  34. package/src/bento/BentoCell.tsx +0 -66
  35. package/src/bento/BentoGrid.tsx +0 -42
  36. package/src/bento/index.ts +0 -2
  37. package/src/command/CommandPalette.tsx +0 -329
  38. package/src/command/CommandProvider.tsx +0 -57
  39. package/src/command/command-context.ts +0 -12
  40. package/src/command/index.ts +0 -6
  41. package/src/command/rank.ts +0 -50
  42. package/src/command/types.ts +0 -37
  43. package/src/command/useCommandSource.ts +0 -37
  44. package/src/dialogs/DialogsProvider.tsx +0 -216
  45. package/src/dialogs/Modal.tsx +0 -260
  46. package/src/dialogs/Toast.tsx +0 -85
  47. package/src/dialogs/dialogs-context.ts +0 -6
  48. package/src/dialogs/index.ts +0 -10
  49. package/src/dialogs/types.ts +0 -37
  50. package/src/dialogs/useDialogs.ts +0 -8
  51. package/src/editorial/EditorialSpotlight.tsx +0 -63
  52. package/src/editorial/Folio.tsx +0 -52
  53. package/src/editorial/PlateMarker.tsx +0 -33
  54. package/src/editorial/RomanSection.tsx +0 -65
  55. package/src/editorial/RunningMarginalia.tsx +0 -65
  56. package/src/editorial/index.ts +0 -10
  57. package/src/hooks/index.ts +0 -5
  58. package/src/hooks/useKeyboardHotkey.ts +0 -93
  59. package/src/hooks/useReducedMotion.ts +0 -20
  60. package/src/hooks/useViewport.ts +0 -61
  61. package/src/layout/Breadcrumb.tsx +0 -74
  62. package/src/layout/LeftNavRail.tsx +0 -139
  63. package/src/layout/MobileTabBar.tsx +0 -101
  64. package/src/layout/NavItem.tsx +0 -128
  65. package/src/layout/NavSearchTrigger.tsx +0 -88
  66. package/src/layout/NavSection.tsx +0 -40
  67. package/src/layout/RightSidebarPanel.tsx +0 -111
  68. package/src/layout/Shell.tsx +0 -91
  69. package/src/layout/SidePanel.tsx +0 -316
  70. package/src/layout/StickyBanner.tsx +0 -83
  71. package/src/layout/Topbar.tsx +0 -68
  72. package/src/layout/index.ts +0 -24
  73. package/src/layout/useNavRailState.ts +0 -69
  74. package/src/lib/time-utils.ts +0 -44
  75. package/src/neobrutalism/Marquee.tsx +0 -81
  76. package/src/neobrutalism/Sticker.tsx +0 -71
  77. package/src/neobrutalism/index.ts +0 -4
  78. package/src/primitives/Avatar.tsx +0 -53
  79. package/src/primitives/Button.tsx +0 -67
  80. package/src/primitives/Card.tsx +0 -41
  81. package/src/primitives/Checkbox.tsx +0 -87
  82. package/src/primitives/Chip.tsx +0 -191
  83. package/src/primitives/Combobox.tsx +0 -507
  84. package/src/primitives/CopyField.tsx +0 -139
  85. package/src/primitives/CountBadge.tsx +0 -50
  86. package/src/primitives/Input.tsx +0 -204
  87. package/src/primitives/Kbd.tsx +0 -45
  88. package/src/primitives/MentionCombobox.tsx +0 -612
  89. package/src/primitives/MultiCombobox.tsx +0 -327
  90. package/src/primitives/PageHeader.tsx +0 -77
  91. package/src/primitives/Radio.tsx +0 -131
  92. package/src/primitives/SegmentedControl.tsx +0 -135
  93. package/src/primitives/Select.tsx +0 -779
  94. package/src/primitives/SignaturePad.tsx +0 -461
  95. package/src/primitives/Tag.tsx +0 -56
  96. package/src/primitives/Textarea.tsx +0 -62
  97. package/src/primitives/ToggleSwitch.tsx +0 -79
  98. package/src/primitives/Tooltip.tsx +0 -213
  99. package/src/primitives/index.ts +0 -53
  100. package/src/pwa/InstallPromptBanner.tsx +0 -132
  101. package/src/pwa/index.ts +0 -4
  102. package/src/pwa/manifest.template.json +0 -20
  103. package/src/pwa/registerSW.ts +0 -55
  104. package/src/riso/Halftone.tsx +0 -85
  105. package/src/riso/Misregister.tsx +0 -63
  106. package/src/riso/RisoStamp.tsx +0 -76
  107. package/src/riso/index.ts +0 -3
  108. package/src/sketch/HandUnderline.tsx +0 -53
  109. package/src/sketch/RoughArrow.tsx +0 -91
  110. package/src/sketch/RoughBox.tsx +0 -73
  111. package/src/sketch/StickyNote.tsx +0 -56
  112. package/src/sketch/index.ts +0 -4
  113. package/src/styles/base.css +0 -186
  114. package/src/styles/chrome-ambient.css +0 -235
  115. package/src/styles/chrome-bento.css +0 -192
  116. package/src/styles/chrome-editorial.css +0 -168
  117. package/src/styles/chrome-neobrutalism.css +0 -326
  118. package/src/styles/chrome-riso.css +0 -339
  119. package/src/styles/chrome-sketch.css +0 -362
  120. package/src/styles/chrome-swiss.css +0 -265
  121. package/src/styles/chrome-terminal.css +0 -272
  122. package/src/styles/fonts.css +0 -1034
  123. package/src/styles/index.css +0 -343
  124. package/src/styles/tokens.css +0 -941
  125. package/src/terminal/AsciiBox.tsx +0 -65
  126. package/src/terminal/BrailleSpinner.tsx +0 -46
  127. package/src/terminal/index.ts +0 -4
  128. package/src/theme/ThemeProvider.tsx +0 -99
  129. package/src/theme/index.ts +0 -6
  130. package/src/theme/inline-script.ts +0 -40
  131. package/src/theme/theme-context.ts +0 -7
  132. package/src/theme/types.ts +0 -39
  133. 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
- }