@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,327 +0,0 @@
|
|
|
1
|
-
import {
|
|
2
|
-
forwardRef,
|
|
3
|
-
useId,
|
|
4
|
-
useImperativeHandle,
|
|
5
|
-
useRef,
|
|
6
|
-
type CSSProperties,
|
|
7
|
-
type KeyboardEvent,
|
|
8
|
-
type MouseEvent,
|
|
9
|
-
type ReactNode,
|
|
10
|
-
} from "react";
|
|
11
|
-
|
|
12
|
-
import { Chip, type ChipTone } from "./Chip";
|
|
13
|
-
import {
|
|
14
|
-
COMBOBOX_DEFAULT_INVALID_HINT,
|
|
15
|
-
Combobox,
|
|
16
|
-
type ComboboxHandle,
|
|
17
|
-
type ComboboxProps,
|
|
18
|
-
} from "./Combobox";
|
|
19
|
-
|
|
20
|
-
/**
|
|
21
|
-
* MultiCombobox — a Combobox that collects MANY values. The embedded
|
|
22
|
-
* search input clears after every commit (`clearOnCommit`) and the
|
|
23
|
-
* parent renders what it collected as a row of removable Chips below
|
|
24
|
-
* the field. Exists so recipient lists (group share links: one URL,
|
|
25
|
-
* N emails) and similar "pick several people / things" fields share
|
|
26
|
-
* one interaction model with the single-value Combobox — same async
|
|
27
|
-
* search, same free-entry gate, same blur auto-commit.
|
|
28
|
-
*
|
|
29
|
-
* Like Combobox, the value is PARENT-OWNED: `onCommit` hands the
|
|
30
|
-
* parent each pick / free entry, the parent maps it onto its own
|
|
31
|
-
* domain state and passes back `entries` for display. Splitting a
|
|
32
|
-
* pasted "a@x.co, b@x.co" into several entries is the parent's job
|
|
33
|
-
* inside `onCommit` (`allowFreeEntry` sees the whole raw string).
|
|
34
|
-
*
|
|
35
|
-
* Per entry: `title` (chip text), `subtitle` (native tooltip),
|
|
36
|
-
* `tone` (e.g. `warning` for "still needs a name"), `editing` — the
|
|
37
|
-
* one entry currently being edited renders `selected` and, when
|
|
38
|
-
* `renderEntryEditor` is given, its editor mounts under the chip row
|
|
39
|
-
* (the parent owns the editor's state; use the forwarded handle's
|
|
40
|
-
* `focus()` to hand focus back to the search input when it closes).
|
|
41
|
-
*
|
|
42
|
-
* Keyboard: Backspace in the EMPTY search input removes the last
|
|
43
|
-
* entry (`backspaceRemovesLast`, default true). The check is scoped
|
|
44
|
-
* to the embedded Combobox's input, so Backspace inside an entry
|
|
45
|
-
* editor never removes anything.
|
|
46
|
-
*
|
|
47
|
-
* Layouts (`layout`): `stacked` (default) — the search field, then
|
|
48
|
-
* the chip row underneath. `inline` — a mail-client "To" field: one
|
|
49
|
-
* Input-like frame (`.ck-multi-combobox-field`) whose `<ul>` is the
|
|
50
|
-
* flex-wrap row: chips as `<li>`s and the bare search input in a last
|
|
51
|
-
* presentational `<li>`, so typing continues after the last chip and
|
|
52
|
-
* wraps with it. The label renders above as an eyebrow; the invalid
|
|
53
|
-
* hint (aria-describedby on the input), editor and hint stay below.
|
|
54
|
-
*
|
|
55
|
-
* Compose with: Chip (entries), Input (inside `renderEntryEditor`),
|
|
56
|
-
* dialogs collecting recipients.
|
|
57
|
-
*
|
|
58
|
-
* Visual: see docs/components/multi-combobox.
|
|
59
|
-
*
|
|
60
|
-
* Forwards a `MultiComboboxHandle` (= `ComboboxHandle`): `focus()`
|
|
61
|
-
* the search input, `clear()` its text (e.g. after rejecting a
|
|
62
|
-
* duplicate). Testids (from `testid`, default "multi-combobox"):
|
|
63
|
-
* `${testid}` wrapper, `${testid}-combobox-input` / `-dropdown` /
|
|
64
|
-
* `-free-entry` (the embedded Combobox), `${testid}-entries` (the
|
|
65
|
-
* list), `${testid}-entry-${key}` (each chip), `${testid}-editor`.
|
|
66
|
-
*/
|
|
67
|
-
|
|
68
|
-
export type MultiComboboxEntry = {
|
|
69
|
-
// Stable identity — list key, testid suffix, and the argument to
|
|
70
|
-
// `onRemoveEntry` / `onEntryClick`.
|
|
71
|
-
key: string;
|
|
72
|
-
// Chip text.
|
|
73
|
-
title: ReactNode;
|
|
74
|
-
// Native tooltip on the chip (e.g. the email behind a name).
|
|
75
|
-
subtitle?: string | undefined;
|
|
76
|
-
tone?: ChipTone | undefined;
|
|
77
|
-
// Marks the entry being edited: chip renders `selected`, and
|
|
78
|
-
// `renderEntryEditor(entry)` mounts below the chip row.
|
|
79
|
-
editing?: boolean | undefined;
|
|
80
|
-
};
|
|
81
|
-
|
|
82
|
-
export type MultiComboboxHandle = ComboboxHandle;
|
|
83
|
-
|
|
84
|
-
export type MultiComboboxLayout = "stacked" | "inline";
|
|
85
|
-
|
|
86
|
-
export type MultiComboboxProps = Omit<
|
|
87
|
-
ComboboxProps,
|
|
88
|
-
| "committed"
|
|
89
|
-
| "committedExtra"
|
|
90
|
-
| "onUncommit"
|
|
91
|
-
| "clearOnCommit"
|
|
92
|
-
| "bare"
|
|
93
|
-
| "inputId"
|
|
94
|
-
| "inputDescribedBy"
|
|
95
|
-
> & {
|
|
96
|
-
entries: MultiComboboxEntry[];
|
|
97
|
-
// `stacked` (default): field, then chips underneath. `inline`:
|
|
98
|
-
// chips inside the field, input after the last chip.
|
|
99
|
-
layout?: MultiComboboxLayout | undefined;
|
|
100
|
-
onRemoveEntry: (key: string) => void;
|
|
101
|
-
// When set, chip labels become buttons (e.g. click-to-edit).
|
|
102
|
-
onEntryClick?: ((key: string) => void) | undefined;
|
|
103
|
-
// Inline editor for the entry with `editing: true`.
|
|
104
|
-
renderEntryEditor?: ((entry: MultiComboboxEntry) => ReactNode) | undefined;
|
|
105
|
-
// aria-label of the entries list. Default "Selected items".
|
|
106
|
-
entriesLabel?: string | undefined;
|
|
107
|
-
// Per-entry aria-label for the chip's × button. Default "Remove".
|
|
108
|
-
removeLabel?: ((entry: MultiComboboxEntry) => string) | undefined;
|
|
109
|
-
// Backspace in the empty search input removes the last entry.
|
|
110
|
-
// Default true.
|
|
111
|
-
backspaceRemovesLast?: boolean | undefined;
|
|
112
|
-
// Small tertiary line under the entries (counts, duplicate notices).
|
|
113
|
-
hint?: ReactNode | undefined;
|
|
114
|
-
};
|
|
115
|
-
|
|
116
|
-
// Inline layout: the host-drawn field mirrors Input's chrome. Focus
|
|
117
|
-
// ring / invalid border / disabled dim come from styles/index.css
|
|
118
|
-
// (`.ck-multi-combobox-field`).
|
|
119
|
-
const INLINE_FIELD_STYLE: CSSProperties = {
|
|
120
|
-
position: "relative",
|
|
121
|
-
display: "flex",
|
|
122
|
-
flexWrap: "wrap",
|
|
123
|
-
alignItems: "center",
|
|
124
|
-
gap: 6,
|
|
125
|
-
minHeight: 36,
|
|
126
|
-
padding: "4px 8px",
|
|
127
|
-
boxSizing: "border-box",
|
|
128
|
-
background: "var(--ck-bg-surface, #fff)",
|
|
129
|
-
border: "1px solid var(--ck-border-strong, #C5C9D2)",
|
|
130
|
-
borderRadius: "var(--ck-radius-sm, 6px)",
|
|
131
|
-
transition: "border-color var(--ck-dur-fast, 120ms) var(--ck-ease, ease)",
|
|
132
|
-
};
|
|
133
|
-
// The `<ul>` inside the frame is the actual flex-wrap row.
|
|
134
|
-
const INLINE_LIST_STYLE: CSSProperties = {
|
|
135
|
-
listStyle: "none",
|
|
136
|
-
margin: 0,
|
|
137
|
-
padding: 0,
|
|
138
|
-
width: "100%",
|
|
139
|
-
display: "flex",
|
|
140
|
-
flexWrap: "wrap",
|
|
141
|
-
alignItems: "center",
|
|
142
|
-
gap: 6,
|
|
143
|
-
};
|
|
144
|
-
// Last <li>: holds the bare Combobox (its root is `position: static`,
|
|
145
|
-
// so the dropdown anchors to the frame) and takes the remaining width.
|
|
146
|
-
const INLINE_COMBOBOX_ITEM: CSSProperties = {
|
|
147
|
-
flex: "1 1 140px",
|
|
148
|
-
minWidth: 140,
|
|
149
|
-
display: "flex",
|
|
150
|
-
};
|
|
151
|
-
|
|
152
|
-
export const MultiCombobox = forwardRef<MultiComboboxHandle, MultiComboboxProps>(
|
|
153
|
-
function MultiCombobox(
|
|
154
|
-
{
|
|
155
|
-
entries,
|
|
156
|
-
onRemoveEntry,
|
|
157
|
-
onEntryClick,
|
|
158
|
-
renderEntryEditor,
|
|
159
|
-
entriesLabel,
|
|
160
|
-
removeLabel,
|
|
161
|
-
backspaceRemovesLast = true,
|
|
162
|
-
hint,
|
|
163
|
-
layout = "stacked",
|
|
164
|
-
testid = "multi-combobox",
|
|
165
|
-
label,
|
|
166
|
-
...comboboxProps
|
|
167
|
-
},
|
|
168
|
-
ref,
|
|
169
|
-
) {
|
|
170
|
-
// Wraps ONLY the embedded Combobox so the Backspace handler can
|
|
171
|
-
// tell its input apart from any input inside `renderEntryEditor`.
|
|
172
|
-
// A <div> in stacked mode, the last <li> in inline mode.
|
|
173
|
-
const comboboxWrapRef = useRef<HTMLElement | null>(null);
|
|
174
|
-
const setComboboxWrap = (el: HTMLElement | null): void => {
|
|
175
|
-
comboboxWrapRef.current = el;
|
|
176
|
-
};
|
|
177
|
-
const listRef = useRef<HTMLUListElement | null>(null);
|
|
178
|
-
const handleRef = useRef<ComboboxHandle | null>(null);
|
|
179
|
-
useImperativeHandle(ref, () => ({
|
|
180
|
-
focus: () => handleRef.current?.focus(),
|
|
181
|
-
clear: () => handleRef.current?.clear(),
|
|
182
|
-
}));
|
|
183
|
-
const inputId = useId();
|
|
184
|
-
const hintId = useId();
|
|
185
|
-
const inline = layout === "inline";
|
|
186
|
-
const editing = entries.find((entry) => entry.editing);
|
|
187
|
-
|
|
188
|
-
const handleKeyDownCapture = (e: KeyboardEvent<HTMLDivElement>): void => {
|
|
189
|
-
if (!backspaceRemovesLast || e.key !== "Backspace") return;
|
|
190
|
-
const target = e.target;
|
|
191
|
-
if (!(target instanceof HTMLInputElement)) return;
|
|
192
|
-
if (!comboboxWrapRef.current?.contains(target)) return;
|
|
193
|
-
if (target.value !== "") return;
|
|
194
|
-
const last = entries[entries.length - 1];
|
|
195
|
-
if (!last) return;
|
|
196
|
-
onRemoveEntry(last.key);
|
|
197
|
-
};
|
|
198
|
-
|
|
199
|
-
// Clicking the field's empty area (inline) focuses the input, like
|
|
200
|
-
// a mail client's To field. Chip / input clicks handle themselves.
|
|
201
|
-
const handleFieldMouseDown = (e: MouseEvent<HTMLDivElement>): void => {
|
|
202
|
-
if (e.target !== e.currentTarget && e.target !== listRef.current) return;
|
|
203
|
-
e.preventDefault();
|
|
204
|
-
handleRef.current?.focus();
|
|
205
|
-
};
|
|
206
|
-
|
|
207
|
-
const comboboxEl = (
|
|
208
|
-
<Combobox
|
|
209
|
-
ref={handleRef}
|
|
210
|
-
{...comboboxProps}
|
|
211
|
-
label={inline ? undefined : label}
|
|
212
|
-
bare={inline}
|
|
213
|
-
inputId={inline ? inputId : undefined}
|
|
214
|
-
inputDescribedBy={inline ? hintId : undefined}
|
|
215
|
-
clearOnCommit
|
|
216
|
-
committed={null}
|
|
217
|
-
testid={`${testid}-combobox`}
|
|
218
|
-
/>
|
|
219
|
-
);
|
|
220
|
-
|
|
221
|
-
const entriesList = (
|
|
222
|
-
<ul
|
|
223
|
-
ref={listRef}
|
|
224
|
-
role="list"
|
|
225
|
-
aria-label={entriesLabel ?? "Selected items"}
|
|
226
|
-
data-testid={`${testid}-entries`}
|
|
227
|
-
style={
|
|
228
|
-
inline
|
|
229
|
-
? INLINE_LIST_STYLE
|
|
230
|
-
: {
|
|
231
|
-
listStyle: "none",
|
|
232
|
-
margin: entries.length > 0 ? "8px 0 0" : 0,
|
|
233
|
-
padding: 0,
|
|
234
|
-
display: "flex",
|
|
235
|
-
flexWrap: "wrap",
|
|
236
|
-
gap: 6,
|
|
237
|
-
}
|
|
238
|
-
}
|
|
239
|
-
>
|
|
240
|
-
{entries.map((entry) => (
|
|
241
|
-
<li key={entry.key} style={{ minWidth: 0, maxWidth: "100%" }}>
|
|
242
|
-
<Chip
|
|
243
|
-
data-testid={`${testid}-entry-${entry.key}`}
|
|
244
|
-
title={entry.subtitle}
|
|
245
|
-
tone={entry.tone}
|
|
246
|
-
selected={entry.editing}
|
|
247
|
-
disabled={comboboxProps.disabled}
|
|
248
|
-
onClick={onEntryClick ? () => onEntryClick(entry.key) : undefined}
|
|
249
|
-
onRemove={() => onRemoveEntry(entry.key)}
|
|
250
|
-
removeLabel={removeLabel?.(entry)}
|
|
251
|
-
>
|
|
252
|
-
{entry.title}
|
|
253
|
-
</Chip>
|
|
254
|
-
</li>
|
|
255
|
-
))}
|
|
256
|
-
{inline ? (
|
|
257
|
-
// Not an entry: presentational so the list still counts N
|
|
258
|
-
// chips for assistive tech.
|
|
259
|
-
<li ref={setComboboxWrap} role="presentation" style={INLINE_COMBOBOX_ITEM}>
|
|
260
|
-
{comboboxEl}
|
|
261
|
-
</li>
|
|
262
|
-
) : null}
|
|
263
|
-
</ul>
|
|
264
|
-
);
|
|
265
|
-
|
|
266
|
-
return (
|
|
267
|
-
<div data-testid={testid} onKeyDownCapture={handleKeyDownCapture}>
|
|
268
|
-
{inline ? (
|
|
269
|
-
<>
|
|
270
|
-
{label ? (
|
|
271
|
-
<label
|
|
272
|
-
htmlFor={inputId}
|
|
273
|
-
className="ck-eyebrow"
|
|
274
|
-
style={{ color: "var(--ck-text-secondary)", display: "block", marginBottom: 6 }}
|
|
275
|
-
>
|
|
276
|
-
{label}
|
|
277
|
-
</label>
|
|
278
|
-
) : null}
|
|
279
|
-
<div
|
|
280
|
-
className="ck-multi-combobox-field"
|
|
281
|
-
data-testid={`${testid}-field`}
|
|
282
|
-
style={INLINE_FIELD_STYLE}
|
|
283
|
-
onMouseDown={handleFieldMouseDown}
|
|
284
|
-
>
|
|
285
|
-
{entriesList}
|
|
286
|
-
</div>
|
|
287
|
-
<div
|
|
288
|
-
id={hintId}
|
|
289
|
-
className="ck-multi-combobox-invalid-hint"
|
|
290
|
-
style={{
|
|
291
|
-
marginTop: 6,
|
|
292
|
-
font: "400 11px/1.4 var(--ck-font-sans)",
|
|
293
|
-
color: "var(--ck-critical)",
|
|
294
|
-
}}
|
|
295
|
-
>
|
|
296
|
-
{comboboxProps.invalidHint ?? COMBOBOX_DEFAULT_INVALID_HINT}
|
|
297
|
-
</div>
|
|
298
|
-
</>
|
|
299
|
-
) : (
|
|
300
|
-
<>
|
|
301
|
-
<div ref={setComboboxWrap}>{comboboxEl}</div>
|
|
302
|
-
{entriesList}
|
|
303
|
-
</>
|
|
304
|
-
)}
|
|
305
|
-
|
|
306
|
-
{editing && renderEntryEditor ? (
|
|
307
|
-
<div data-testid={`${testid}-editor`} style={{ marginTop: 8 }}>
|
|
308
|
-
{renderEntryEditor(editing)}
|
|
309
|
-
</div>
|
|
310
|
-
) : null}
|
|
311
|
-
|
|
312
|
-
{hint ? (
|
|
313
|
-
<div
|
|
314
|
-
style={{
|
|
315
|
-
marginTop: 6,
|
|
316
|
-
fontSize: 11,
|
|
317
|
-
lineHeight: 1.4,
|
|
318
|
-
color: "var(--ck-text-tertiary, #8B92A3)",
|
|
319
|
-
}}
|
|
320
|
-
>
|
|
321
|
-
{hint}
|
|
322
|
-
</div>
|
|
323
|
-
) : null}
|
|
324
|
-
</div>
|
|
325
|
-
);
|
|
326
|
-
},
|
|
327
|
-
);
|
|
@@ -1,77 +0,0 @@
|
|
|
1
|
-
import type { ReactNode, CSSProperties } from "react";
|
|
2
|
-
|
|
3
|
-
// Standard list-page header — eyebrow + title + optional
|
|
4
|
-
// description + right-aligned action cluster. Wrap the top of
|
|
5
|
-
// list / overview pages so visual rhythm stays consistent across
|
|
6
|
-
// the app.
|
|
7
|
-
|
|
8
|
-
export interface PageHeaderProps {
|
|
9
|
-
// Small uppercase mono cap above the title — "ACCESS · SHARES".
|
|
10
|
-
eyebrow?: string;
|
|
11
|
-
// Main heading (typically a sentence: "Bundles you've sent.").
|
|
12
|
-
title: ReactNode;
|
|
13
|
-
// Optional one-or-two-line caption under the title.
|
|
14
|
-
description?: ReactNode;
|
|
15
|
-
// Right-aligned cluster of buttons / chips / status items.
|
|
16
|
-
actions?: ReactNode;
|
|
17
|
-
className?: string;
|
|
18
|
-
style?: CSSProperties;
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
export function PageHeader({
|
|
22
|
-
eyebrow,
|
|
23
|
-
title,
|
|
24
|
-
description,
|
|
25
|
-
actions,
|
|
26
|
-
className,
|
|
27
|
-
style,
|
|
28
|
-
}: PageHeaderProps) {
|
|
29
|
-
return (
|
|
30
|
-
<header
|
|
31
|
-
className={className}
|
|
32
|
-
style={{
|
|
33
|
-
display: "flex",
|
|
34
|
-
alignItems: "flex-end",
|
|
35
|
-
gap: 16,
|
|
36
|
-
marginBottom: 24,
|
|
37
|
-
paddingBottom: 16,
|
|
38
|
-
borderBottom: "1px solid var(--ck-border-subtle)",
|
|
39
|
-
fontFamily: "var(--ck-font-sans)",
|
|
40
|
-
...style,
|
|
41
|
-
}}
|
|
42
|
-
>
|
|
43
|
-
<div style={{ flex: 1, minWidth: 0 }}>
|
|
44
|
-
{eyebrow && (
|
|
45
|
-
<div className="ck-eyebrow" style={{ marginBottom: 6 }}>
|
|
46
|
-
{eyebrow}
|
|
47
|
-
</div>
|
|
48
|
-
)}
|
|
49
|
-
<h1
|
|
50
|
-
style={{
|
|
51
|
-
font: "500 22px/1.3 var(--ck-font-sans)",
|
|
52
|
-
letterSpacing: "-0.005em",
|
|
53
|
-
color: "var(--ck-text-primary)",
|
|
54
|
-
margin: 0,
|
|
55
|
-
}}
|
|
56
|
-
>
|
|
57
|
-
{title}
|
|
58
|
-
</h1>
|
|
59
|
-
{description && (
|
|
60
|
-
<p
|
|
61
|
-
style={{
|
|
62
|
-
font: "400 13px/1.55 var(--ck-font-sans)",
|
|
63
|
-
color: "var(--ck-text-tertiary)",
|
|
64
|
-
margin: "6px 0 0",
|
|
65
|
-
maxWidth: "44rem",
|
|
66
|
-
}}
|
|
67
|
-
>
|
|
68
|
-
{description}
|
|
69
|
-
</p>
|
|
70
|
-
)}
|
|
71
|
-
</div>
|
|
72
|
-
{actions && (
|
|
73
|
-
<div style={{ display: "flex", gap: 8, flex: "none" }}>{actions}</div>
|
|
74
|
-
)}
|
|
75
|
-
</header>
|
|
76
|
-
);
|
|
77
|
-
}
|
package/src/primitives/Radio.tsx
DELETED
|
@@ -1,131 +0,0 @@
|
|
|
1
|
-
import { cn } from "../lib/cn";
|
|
2
|
-
|
|
3
|
-
// Custom-styled radio — accent-bordered circle that becomes filled
|
|
4
|
-
// accent + white dot when checked. Paired with Checkbox: same
|
|
5
|
-
// disabled/label/suffix contract, same 16px hit target size. Use
|
|
6
|
-
// this anywhere you'd reach for a `<input type="radio">` — the
|
|
7
|
-
// native control renders as the OS accent (blue on macOS by
|
|
8
|
-
// default) which doesn't respect --ck-accent per workspace, and the
|
|
9
|
-
// unstyled defaults look inconsistent alongside our Checkbox.
|
|
10
|
-
//
|
|
11
|
-
// Radio is a single option in a group; the caller owns the group's
|
|
12
|
-
// selection state and passes `checked` + `onChange` per option.
|
|
13
|
-
// Radios with the same `name` still get the browser's native "one
|
|
14
|
-
// selected at a time" behaviour if you need form-post semantics; we
|
|
15
|
-
// forward it to a hidden native input so aria-pressed on the visual
|
|
16
|
-
// button doesn't drift from the actual form value.
|
|
17
|
-
|
|
18
|
-
export interface RadioProps {
|
|
19
|
-
checked: boolean;
|
|
20
|
-
onChange: (next: boolean) => void;
|
|
21
|
-
label?: React.ReactNode;
|
|
22
|
-
suffix?: React.ReactNode;
|
|
23
|
-
disabled?: boolean;
|
|
24
|
-
/**
|
|
25
|
-
* Native radio-group name. Optional but recommended: browsers
|
|
26
|
-
* enforce "only one checked at a time" for same-name radios, so
|
|
27
|
-
* passing a shared name is a belt-and-braces guard against a
|
|
28
|
-
* stale onChange handler leaving two visually-checked radios.
|
|
29
|
-
*/
|
|
30
|
-
name?: string;
|
|
31
|
-
/**
|
|
32
|
-
* Value that lands in the form-post payload when this radio is
|
|
33
|
-
* checked. Only meaningful when `name` is also set.
|
|
34
|
-
*/
|
|
35
|
-
value?: string;
|
|
36
|
-
className?: string;
|
|
37
|
-
/** Test hook forwarded to the visual button — see Checkbox. */
|
|
38
|
-
"data-testid"?: string;
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
export function Radio({
|
|
42
|
-
checked,
|
|
43
|
-
onChange,
|
|
44
|
-
label,
|
|
45
|
-
suffix,
|
|
46
|
-
disabled,
|
|
47
|
-
name,
|
|
48
|
-
value,
|
|
49
|
-
className,
|
|
50
|
-
"data-testid": testId,
|
|
51
|
-
}: RadioProps) {
|
|
52
|
-
return (
|
|
53
|
-
<label
|
|
54
|
-
className={cn("ck-radio", className)}
|
|
55
|
-
style={{
|
|
56
|
-
display: "inline-flex",
|
|
57
|
-
alignItems: "center",
|
|
58
|
-
gap: 8,
|
|
59
|
-
font: "400 13px/1 var(--ck-font-sans)",
|
|
60
|
-
color: "var(--ck-text-secondary)",
|
|
61
|
-
cursor: disabled ? "not-allowed" : "pointer",
|
|
62
|
-
opacity: disabled ? 0.5 : 1,
|
|
63
|
-
userSelect: "none",
|
|
64
|
-
}}
|
|
65
|
-
>
|
|
66
|
-
<button
|
|
67
|
-
type="button"
|
|
68
|
-
role="radio"
|
|
69
|
-
aria-checked={checked}
|
|
70
|
-
disabled={disabled}
|
|
71
|
-
onClick={() => !disabled && !checked && onChange(true)}
|
|
72
|
-
data-testid={testId}
|
|
73
|
-
style={{
|
|
74
|
-
width: 16,
|
|
75
|
-
height: 16,
|
|
76
|
-
borderRadius: "50%",
|
|
77
|
-
border: "1.5px solid var(--ck-accent)",
|
|
78
|
-
background: "transparent",
|
|
79
|
-
display: "inline-flex",
|
|
80
|
-
alignItems: "center",
|
|
81
|
-
justifyContent: "center",
|
|
82
|
-
padding: 0,
|
|
83
|
-
cursor: disabled ? "not-allowed" : "pointer",
|
|
84
|
-
flexShrink: 0,
|
|
85
|
-
}}
|
|
86
|
-
>
|
|
87
|
-
{checked && (
|
|
88
|
-
<span
|
|
89
|
-
aria-hidden
|
|
90
|
-
style={{
|
|
91
|
-
width: 8,
|
|
92
|
-
height: 8,
|
|
93
|
-
borderRadius: "50%",
|
|
94
|
-
background: "var(--ck-accent)",
|
|
95
|
-
display: "inline-block",
|
|
96
|
-
}}
|
|
97
|
-
/>
|
|
98
|
-
)}
|
|
99
|
-
</button>
|
|
100
|
-
{/* Hidden native input keeps the browser's same-name enforcement
|
|
101
|
-
+ lets consumers use the surrounding <form> for post payload. */}
|
|
102
|
-
{name && (
|
|
103
|
-
<input
|
|
104
|
-
type="radio"
|
|
105
|
-
name={name}
|
|
106
|
-
value={value}
|
|
107
|
-
checked={checked}
|
|
108
|
-
onChange={(e) => !disabled && onChange(e.target.checked)}
|
|
109
|
-
disabled={disabled}
|
|
110
|
-
tabIndex={-1}
|
|
111
|
-
aria-hidden
|
|
112
|
-
style={{
|
|
113
|
-
position: "absolute",
|
|
114
|
-
width: 1,
|
|
115
|
-
height: 1,
|
|
116
|
-
padding: 0,
|
|
117
|
-
margin: -1,
|
|
118
|
-
overflow: "hidden",
|
|
119
|
-
clip: "rect(0,0,0,0)",
|
|
120
|
-
whiteSpace: "nowrap",
|
|
121
|
-
border: 0,
|
|
122
|
-
}}
|
|
123
|
-
/>
|
|
124
|
-
)}
|
|
125
|
-
{label}
|
|
126
|
-
{suffix && (
|
|
127
|
-
<span style={{ color: "var(--ck-text-tertiary)" }}>{suffix}</span>
|
|
128
|
-
)}
|
|
129
|
-
</label>
|
|
130
|
-
);
|
|
131
|
-
}
|
|
@@ -1,135 +0,0 @@
|
|
|
1
|
-
import { cn } from "../lib/cn";
|
|
2
|
-
|
|
3
|
-
// SegmentedControl — a horizontal set of mutually-exclusive options
|
|
4
|
-
// rendered as one pill-shaped track with a raised "active" segment.
|
|
5
|
-
// Use it where the choice swaps the content below it (mode switch,
|
|
6
|
-
// filter scope), i.e. wherever a pair of radio buttons would be
|
|
7
|
-
// semantically right but visually too quiet. For true page-level
|
|
8
|
-
// navigation reach for router links instead; for on/off state use
|
|
9
|
-
// ToggleSwitch.
|
|
10
|
-
//
|
|
11
|
-
// The caller owns the selection: pass `value` + `onChange`, one
|
|
12
|
-
// option per segment. Per-option `disabled` renders the segment
|
|
13
|
-
// non-interactive with a hint via `title` (a disabled option should
|
|
14
|
-
// tell the user why — pass `disabledHint`).
|
|
15
|
-
//
|
|
16
|
-
// Semantics are a radiogroup (not tabs): arrow keys move selection
|
|
17
|
-
// directly, matching native radio behaviour, and screen readers
|
|
18
|
-
// announce "x of y" positioning for free.
|
|
19
|
-
|
|
20
|
-
export interface SegmentedControlOption<V extends string = string> {
|
|
21
|
-
value: V;
|
|
22
|
-
label: React.ReactNode;
|
|
23
|
-
disabled?: boolean;
|
|
24
|
-
/** Shown as the native tooltip on a disabled segment — say WHY. */
|
|
25
|
-
disabledHint?: string;
|
|
26
|
-
"data-testid"?: string;
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
export interface SegmentedControlProps<V extends string = string> {
|
|
30
|
-
value: V;
|
|
31
|
-
onChange: (next: V) => void;
|
|
32
|
-
options: SegmentedControlOption<V>[];
|
|
33
|
-
/** Disables the whole control (per-option disabled still applies). */
|
|
34
|
-
disabled?: boolean;
|
|
35
|
-
/** "full" stretches the track and gives segments equal width. */
|
|
36
|
-
fit?: "auto" | "full";
|
|
37
|
-
"aria-label"?: string;
|
|
38
|
-
className?: string;
|
|
39
|
-
"data-testid"?: string;
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
export function SegmentedControl<V extends string = string>({
|
|
43
|
-
value,
|
|
44
|
-
onChange,
|
|
45
|
-
options,
|
|
46
|
-
disabled,
|
|
47
|
-
fit = "auto",
|
|
48
|
-
"aria-label": ariaLabel,
|
|
49
|
-
className,
|
|
50
|
-
"data-testid": testId,
|
|
51
|
-
}: SegmentedControlProps<V>) {
|
|
52
|
-
// Arrow keys move selection to the nearest enabled neighbour,
|
|
53
|
-
// wrapping — the native radio-group keyboard model.
|
|
54
|
-
const move = (from: number, step: 1 | -1) => {
|
|
55
|
-
for (let i = 1; i <= options.length; i++) {
|
|
56
|
-
const next = options[(from + step * i + options.length * i) % options.length];
|
|
57
|
-
if (next && !next.disabled) {
|
|
58
|
-
onChange(next.value);
|
|
59
|
-
return;
|
|
60
|
-
}
|
|
61
|
-
}
|
|
62
|
-
};
|
|
63
|
-
|
|
64
|
-
return (
|
|
65
|
-
<div
|
|
66
|
-
role="radiogroup"
|
|
67
|
-
aria-label={ariaLabel}
|
|
68
|
-
className={cn("ck-segmented", className)}
|
|
69
|
-
data-testid={testId}
|
|
70
|
-
// The track is deliberately unfilled (owner feedback,
|
|
71
|
-
// 2026-08-19): a muted-filled track reads as a heavy block on
|
|
72
|
-
// warm canvases — the hairline border delimits the group and
|
|
73
|
-
// the raised selected segment carries ALL the contrast. This
|
|
74
|
-
// also makes the control look right on any surface (canvas,
|
|
75
|
-
// card, modal) without per-context tuning.
|
|
76
|
-
style={{
|
|
77
|
-
display: fit === "full" ? "flex" : "inline-flex",
|
|
78
|
-
alignItems: "stretch",
|
|
79
|
-
gap: 2,
|
|
80
|
-
padding: 2,
|
|
81
|
-
borderRadius: 8,
|
|
82
|
-
border: "1px solid var(--ck-border-subtle)",
|
|
83
|
-
background: "transparent",
|
|
84
|
-
opacity: disabled ? 0.5 : 1,
|
|
85
|
-
}}
|
|
86
|
-
>
|
|
87
|
-
{options.map((opt, i) => {
|
|
88
|
-
const selected = opt.value === value;
|
|
89
|
-
const optDisabled = disabled || opt.disabled;
|
|
90
|
-
return (
|
|
91
|
-
<button
|
|
92
|
-
key={opt.value}
|
|
93
|
-
type="button"
|
|
94
|
-
role="radio"
|
|
95
|
-
aria-checked={selected}
|
|
96
|
-
disabled={optDisabled}
|
|
97
|
-
title={opt.disabled ? opt.disabledHint : undefined}
|
|
98
|
-
data-testid={opt["data-testid"]}
|
|
99
|
-
// Roving tabindex: the group is one tab stop; arrows walk it.
|
|
100
|
-
tabIndex={selected ? 0 : -1}
|
|
101
|
-
onClick={() => !optDisabled && !selected && onChange(opt.value)}
|
|
102
|
-
onKeyDown={(e) => {
|
|
103
|
-
if (e.key === "ArrowRight" || e.key === "ArrowDown") {
|
|
104
|
-
e.preventDefault();
|
|
105
|
-
move(i, 1);
|
|
106
|
-
} else if (e.key === "ArrowLeft" || e.key === "ArrowUp") {
|
|
107
|
-
e.preventDefault();
|
|
108
|
-
move(i, -1);
|
|
109
|
-
}
|
|
110
|
-
}}
|
|
111
|
-
style={{
|
|
112
|
-
flex: fit === "full" ? 1 : undefined,
|
|
113
|
-
padding: "6px 14px",
|
|
114
|
-
borderRadius: 6,
|
|
115
|
-
border: "1px solid transparent",
|
|
116
|
-
font: `500 13px/1.2 var(--ck-font-sans)`,
|
|
117
|
-
color: selected
|
|
118
|
-
? "var(--ck-text-primary)"
|
|
119
|
-
: "var(--ck-text-secondary)",
|
|
120
|
-
background: selected ? "var(--ck-bg-surface)" : "transparent",
|
|
121
|
-
boxShadow: selected ? "0 1px 2px rgba(0,0,0,0.06)" : "none",
|
|
122
|
-
borderColor: selected ? "var(--ck-border-subtle)" : "transparent",
|
|
123
|
-
cursor: optDisabled ? "not-allowed" : "pointer",
|
|
124
|
-
opacity: opt.disabled && !disabled ? 0.45 : 1,
|
|
125
|
-
whiteSpace: "nowrap",
|
|
126
|
-
transition: "background 120ms ease, color 120ms ease",
|
|
127
|
-
}}
|
|
128
|
-
>
|
|
129
|
-
{opt.label}
|
|
130
|
-
</button>
|
|
131
|
-
);
|
|
132
|
-
})}
|
|
133
|
-
</div>
|
|
134
|
-
);
|
|
135
|
-
}
|