@cosxai/ui 1.0.0-alpha.1 → 1.0.0-alpha.2

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 (48) hide show
  1. package/CHANGELOG.md +22 -0
  2. package/package.json +6 -1
  3. package/src/components/ActionBar.tsx +607 -0
  4. package/src/components/ActivityTimeline.tsx +77 -0
  5. package/src/components/AsyncSelect.tsx +205 -0
  6. package/src/components/Breadcrumb.tsx +129 -0
  7. package/src/components/Button.tsx +17 -2
  8. package/src/components/ChoiceCards.tsx +68 -0
  9. package/src/components/CodeInput.tsx +150 -0
  10. package/src/components/CommandPalette.tsx +228 -0
  11. package/src/components/ConfirmDialog.tsx +294 -0
  12. package/src/components/CopyField.tsx +104 -0
  13. package/src/components/DateInput.tsx +366 -0
  14. package/src/components/Dialog.tsx +27 -3
  15. package/src/components/Dropzone.tsx +187 -0
  16. package/src/components/FileCard.tsx +233 -0
  17. package/src/components/FileRow.tsx +159 -0
  18. package/src/components/FolderTree.tsx +377 -0
  19. package/src/components/ListToolbar.tsx +177 -0
  20. package/src/components/MentionInput.tsx +233 -0
  21. package/src/components/Menu.tsx +109 -0
  22. package/src/components/PageHeader.tsx +62 -0
  23. package/src/components/PageState.tsx +84 -0
  24. package/src/components/Progress.tsx +97 -0
  25. package/src/components/PulseDot.tsx +31 -0
  26. package/src/components/RecipientsInput.tsx +252 -0
  27. package/src/components/SearchField.tsx +137 -0
  28. package/src/components/SegmentedControl.tsx +95 -0
  29. package/src/components/SidePanel.tsx +130 -0
  30. package/src/components/Skeleton.tsx +73 -0
  31. package/src/components/StageProgress.tsx +51 -0
  32. package/src/components/Steps.tsx +58 -0
  33. package/src/components/SyncStatus.tsx +56 -0
  34. package/src/components/UploadCheck.tsx +118 -0
  35. package/src/components/UploadList.tsx +176 -0
  36. package/src/components/VirtualList.tsx +90 -0
  37. package/src/components/WindowDrop.tsx +131 -0
  38. package/src/components/inputs-aria.ts +5 -0
  39. package/src/components/inputs-date.ts +92 -0
  40. package/src/components/inputs-listbox.tsx +154 -0
  41. package/src/components/inputs-search.ts +60 -0
  42. package/src/components/status-files.ts +124 -0
  43. package/src/components/status-motion.ts +30 -0
  44. package/src/components/useSelection.ts +142 -0
  45. package/src/index.ts +38 -0
  46. package/src/components/core.test.tsx +0 -242
  47. package/src/components/forms.test.tsx +0 -280
  48. package/src/components/overlays.test.tsx +0 -245
@@ -0,0 +1,233 @@
1
+ import * as CheckboxPrimitive from '@radix-ui/react-checkbox';
2
+ import { Check, FileText, FileX, Folder, Link2, MessageSquare, Star, type LucideIcon } from 'lucide-react';
3
+ import type { ComponentProps, MouseEvent, ReactNode } from 'react';
4
+
5
+ import { cn } from '../lib/cn';
6
+ import { Badge, type BadgeStatus } from './Badge';
7
+ import { Spinner } from './Spinner';
8
+ import { Tag } from './Tag';
9
+
10
+ export type FileCardState = 'ready' | 'converting' | 'failed';
11
+
12
+ export type FileCardProps = Omit<ComponentProps<'div'>, 'title' | 'onClick'> & {
13
+ title: ReactNode;
14
+ /** file · folder · link (a card that points elsewhere: dashed edge). @default "file" */
15
+ kind?: 'file' | 'folder' | 'link' | undefined;
16
+ /** The thumbnail. Without one the icon sits on linen. */
17
+ preview?: ReactNode;
18
+ /** Icon when there is no preview. Defaults by kind. */
19
+ icon?: LucideIcon | undefined;
20
+ /** Format tag: PDF, Word. */
21
+ type?: ReactNode;
22
+ /** Status in words; `status` picks the badge. */
23
+ statusLabel?: ReactNode;
24
+ status?: BadgeStatus | undefined;
25
+ /** The last line: "Updated 2 hours ago", "in Harbour / Site / 2026". */
26
+ meta?: ReactNode;
27
+ /** Comment count after the status; hidden at zero. */
28
+ comments?: number | undefined;
29
+ /** A small source label on the preview: "Dropbox". */
30
+ source?: ReactNode;
31
+ /** ready · converting (spinner and label) · failed (red wash, a Re-render action). */
32
+ state?: FileCardState | undefined;
33
+ stateLabel?: ReactNode;
34
+ onRerender?: (() => void) | undefined;
35
+ /** @default "Re-render" */
36
+ rerenderLabel?: string | undefined;
37
+ /** Link cards: the Remove link action beside the target. */
38
+ onRemoveLink?: (() => void) | undefined;
39
+ /** @default "Remove link" */
40
+ removeLinkLabel?: string | undefined;
41
+
42
+ selected?: boolean | undefined;
43
+ /** In selection mode the whole card toggles instead of opening. */
44
+ selectionMode?: boolean | undefined;
45
+ onSelectedChange?: ((selected: boolean) => void) | undefined;
46
+ /** Accessible name of the tick box. @default "Select" */
47
+ selectLabel?: string | undefined;
48
+
49
+ starred?: boolean | undefined;
50
+ onStarredChange?: ((starred: boolean) => void) | undefined;
51
+ /** @default "Star" */
52
+ starLabel?: string | undefined;
53
+
54
+ /** Opens the item (not in selection mode). */
55
+ onOpen?: (() => void) | undefined;
56
+ /** Every press of the card, with its modifiers — wire useSelection.handleClick here; replaces the default open / toggle. */
57
+ onPress?: ((event: MouseEvent<HTMLButtonElement>) => void) | undefined;
58
+ };
59
+
60
+ const KIND_ICON: Record<NonNullable<FileCardProps['kind']>, LucideIcon> = { file: FileText, folder: Folder, link: Link2 };
61
+
62
+ /**
63
+ * FileCard — the library card shared by every grid. The title is the one
64
+ * real button, stretched over the card; the tick box, the star and inline
65
+ * actions sit above it and never open the card. In selection mode the
66
+ * whole card is the checkbox.
67
+ */
68
+ export function FileCard({
69
+ title,
70
+ kind = 'file',
71
+ preview,
72
+ icon,
73
+ type,
74
+ statusLabel,
75
+ status,
76
+ meta,
77
+ comments,
78
+ source,
79
+ state = 'ready',
80
+ stateLabel,
81
+ onRerender,
82
+ rerenderLabel = 'Re-render',
83
+ onRemoveLink,
84
+ removeLinkLabel = 'Remove link',
85
+ selected = false,
86
+ selectionMode = false,
87
+ onSelectedChange,
88
+ selectLabel = 'Select',
89
+ starred,
90
+ onStarredChange,
91
+ starLabel = 'Star',
92
+ onOpen,
93
+ onPress,
94
+ className,
95
+ ...rest
96
+ }: FileCardProps) {
97
+ const Glyph = icon ?? KIND_ICON[kind];
98
+ const press = (e: MouseEvent<HTMLButtonElement>) => {
99
+ if (onPress) onPress(e);
100
+ else if (selectionMode) onSelectedChange?.(!selected);
101
+ else onOpen?.();
102
+ };
103
+ const showTick = Boolean(onSelectedChange) && (selectionMode || selected);
104
+
105
+ return (
106
+ <div
107
+ data-selected={selected || undefined}
108
+ className={cn(
109
+ 'group relative flex flex-col overflow-hidden rounded-lg bg-page text-fg',
110
+ 'transition-[border-color] duration-[120ms] ease-standard',
111
+ kind === 'link' ? 'border border-dashed border-rule' : 'border border-rule',
112
+ selected ? 'border-2 border-fg' : 'hover:border-fg focus-within:border-fg',
113
+ className,
114
+ )}
115
+ {...rest}
116
+ >
117
+ {/* Preview */}
118
+ <div
119
+ className={cn(
120
+ 'relative grid h-[180px] place-items-center text-fg',
121
+ selected ? 'bg-brand-field text-ink' : state === 'failed' ? 'bg-error-wash text-ink ink:bg-error/20 ink:text-fg' : kind === 'link' ? 'bg-page' : 'bg-sunk',
122
+ )}
123
+ >
124
+ {state === 'converting' ? (
125
+ <div className="flex flex-col items-center gap-2 text-small text-fg-secondary">
126
+ <Spinner size={16} />
127
+ {stateLabel}
128
+ </div>
129
+ ) : state === 'failed' ? (
130
+ <div className="flex flex-col items-center gap-2 text-small">
131
+ <FileX size={20} strokeWidth={1.75} aria-hidden />
132
+ <span>{stateLabel}</span>
133
+ {onRerender && (
134
+ <button
135
+ type="button"
136
+ onClick={onRerender}
137
+ className="relative z-10 cursor-pointer font-semibold underline underline-offset-2 outline-none focus-visible:shadow-(--focus-ring)"
138
+ >
139
+ {rerenderLabel}
140
+ </button>
141
+ )}
142
+ </div>
143
+ ) : (
144
+ (preview ?? <Glyph size={32} strokeWidth={1.5} aria-hidden />)
145
+ )}
146
+
147
+ {source && !showTick && (
148
+ <span className="absolute top-2.5 left-2.5 inline-flex items-center gap-1.5 rounded-sm bg-page px-1.5 py-1 text-[11px] font-semibold text-fg">
149
+ <span aria-hidden className="size-1.5 rounded-pill bg-brand-mark" />
150
+ {source}
151
+ </span>
152
+ )}
153
+
154
+ {onSelectedChange && (
155
+ <CheckboxPrimitive.Root
156
+ checked={selected}
157
+ onCheckedChange={(v) => onSelectedChange(v === true)}
158
+ aria-label={selectLabel}
159
+ className={cn(
160
+ 'absolute top-2.5 left-2.5 z-10 grid size-5 cursor-pointer place-items-center rounded-xs border border-rule bg-page outline-none',
161
+ 'data-[state=checked]:border-fg data-[state=checked]:bg-fg data-[state=checked]:text-page focus-visible:shadow-(--focus-ring)',
162
+ showTick ? 'opacity-100' : 'opacity-0 group-hover:opacity-100 focus-visible:opacity-100',
163
+ )}
164
+ >
165
+ <CheckboxPrimitive.Indicator>
166
+ <Check size={13} strokeWidth={2.5} aria-hidden />
167
+ </CheckboxPrimitive.Indicator>
168
+ </CheckboxPrimitive.Root>
169
+ )}
170
+
171
+ {onStarredChange && (
172
+ <button
173
+ type="button"
174
+ aria-label={starLabel}
175
+ aria-pressed={Boolean(starred)}
176
+ onClick={() => onStarredChange(!starred)}
177
+ className={cn(
178
+ 'absolute top-2 right-2 z-10 grid size-7 cursor-pointer place-items-center rounded-md text-fg outline-none hover:bg-hover focus-visible:shadow-(--focus-ring)',
179
+ starred ? 'opacity-100' : 'opacity-0 group-hover:opacity-100 focus-visible:opacity-100',
180
+ )}
181
+ >
182
+ <Star size={15} strokeWidth={1.75} aria-hidden className={starred ? 'fill-brand-mark text-fg' : undefined} />
183
+ </button>
184
+ )}
185
+ </div>
186
+
187
+ {/* Body */}
188
+ <div className="flex min-w-0 flex-col gap-1.5 px-3.5 pt-3 pb-3.5">
189
+ <button
190
+ type="button"
191
+ onClick={press}
192
+ aria-pressed={selectionMode ? selected : undefined}
193
+ className={cn(
194
+ 'min-w-0 cursor-pointer truncate text-left text-ui font-medium text-fg outline-none',
195
+ // Stretched: the whole card is this button's target.
196
+ 'after:absolute after:inset-0 after:content-[""] focus-visible:after:rounded-lg focus-visible:after:shadow-(--focus-ring)',
197
+ )}
198
+ >
199
+ {title}
200
+ </button>
201
+ {(type || statusLabel || (comments ?? 0) > 0) && (
202
+ <div className="flex min-w-0 items-center gap-2">
203
+ {type && <Tag className="px-1.5 py-0.5 font-semibold text-fg">{type}</Tag>}
204
+ {statusLabel && <Badge status={status}>{statusLabel}</Badge>}
205
+ {(comments ?? 0) > 0 && (
206
+ <span className="ml-auto inline-flex items-center gap-1 text-meta text-fg-secondary">
207
+ <MessageSquare size={13} strokeWidth={1.75} aria-hidden />
208
+ <span className="tabular-nums">{comments}</span>
209
+ </span>
210
+ )}
211
+ </div>
212
+ )}
213
+ {(meta || onRemoveLink) && (
214
+ <div className="flex min-w-0 items-center gap-1 text-meta text-fg-secondary">
215
+ {meta && <span className="truncate">{meta}</span>}
216
+ {onRemoveLink && (
217
+ <>
218
+ {meta && <span aria-hidden>·</span>}
219
+ <button
220
+ type="button"
221
+ onClick={onRemoveLink}
222
+ className="relative z-10 cursor-pointer text-fg-secondary outline-none hover:text-fg hover:underline focus-visible:shadow-(--focus-ring)"
223
+ >
224
+ {removeLinkLabel}
225
+ </button>
226
+ </>
227
+ )}
228
+ </div>
229
+ )}
230
+ </div>
231
+ </div>
232
+ );
233
+ }
@@ -0,0 +1,159 @@
1
+ import * as CheckboxPrimitive from '@radix-ui/react-checkbox';
2
+ import { Check, FileText, Star, type LucideIcon } from 'lucide-react';
3
+ import type { ComponentProps, MouseEvent, ReactNode } from 'react';
4
+
5
+ import { cn } from '../lib/cn';
6
+ import { Badge, type BadgeStatus } from './Badge';
7
+
8
+ /** The shared column layout of a file list: [tick] icon · name · state · updated · [actions]. */
9
+ const GRID = 'grid grid-cols-[20px_minmax(0,1fr)_minmax(96px,120px)_minmax(96px,160px)_32px] items-center gap-x-3';
10
+ const GRID_SELECTABLE = 'grid grid-cols-[20px_20px_minmax(0,1fr)_minmax(96px,120px)_minmax(96px,160px)_32px] items-center gap-x-3';
11
+
12
+ export type FileListProps = ComponentProps<'div'> & {
13
+ /** Column headings: name, state, updated. Omit for a headless list. */
14
+ columns?: { name: ReactNode; state: ReactNode; updated: ReactNode } | undefined;
15
+ /** Leave room for the tick column. */
16
+ selectable?: boolean | undefined;
17
+ /** Accessible name of the list. */
18
+ label: string;
19
+ };
20
+
21
+ /** FileList — the row container: optional headings, then FileRows. */
22
+ export function FileList({ columns, selectable = false, label, className, children, ...rest }: FileListProps) {
23
+ return (
24
+ <div className={cn('flex flex-col', className)} {...rest}>
25
+ {columns && (
26
+ <div aria-hidden className={cn(selectable ? GRID_SELECTABLE : GRID, 'border-b border-rule px-3 pb-2 text-meta font-medium text-fg-secondary')}>
27
+ {selectable && <span />}
28
+ <span />
29
+ <span>{columns.name}</span>
30
+ <span>{columns.state}</span>
31
+ <span>{columns.updated}</span>
32
+ <span />
33
+ </div>
34
+ )}
35
+ <ul aria-label={label} className="m-0 flex list-none flex-col p-0">
36
+ {children}
37
+ </ul>
38
+ </div>
39
+ );
40
+ }
41
+
42
+ export type FileRowProps = Omit<ComponentProps<'li'>, 'title' | 'onClick'> & {
43
+ name: ReactNode;
44
+ icon?: LucideIcon | undefined;
45
+ statusLabel?: ReactNode;
46
+ status?: BadgeStatus | undefined;
47
+ /** "2 hours ago" — or, in search results, the location: "in Legal / 2026". */
48
+ updated?: ReactNode;
49
+ /** A wash for rows that need someone (attention) or are overdue (error). */
50
+ tone?: 'attention' | 'error' | undefined;
51
+ /** Match the FileList's selectable. */
52
+ selectable?: boolean | undefined;
53
+ selected?: boolean | undefined;
54
+ selectionMode?: boolean | undefined;
55
+ onSelectedChange?: ((selected: boolean) => void) | undefined;
56
+ /** @default "Select" */
57
+ selectLabel?: string | undefined;
58
+ starred?: boolean | undefined;
59
+ onStarredChange?: ((starred: boolean) => void) | undefined;
60
+ /** @default "Star" */
61
+ starLabel?: string | undefined;
62
+ onOpen?: (() => void) | undefined;
63
+ /** Every press with its modifiers — wire useSelection.handleClick; replaces the default open / toggle. */
64
+ onPress?: ((event: MouseEvent<HTMLButtonElement>) => void) | undefined;
65
+ };
66
+
67
+ /**
68
+ * FileRow — a list row: icon, name, state, updated or location, a star on
69
+ * hover. Like FileCard, the name is the one button stretched over the row;
70
+ * the tick box and the star sit above it and never open the row.
71
+ */
72
+ export function FileRow({
73
+ name,
74
+ icon: Glyph = FileText,
75
+ statusLabel,
76
+ status,
77
+ updated,
78
+ tone,
79
+ selectable = false,
80
+ selected = false,
81
+ selectionMode = false,
82
+ onSelectedChange,
83
+ selectLabel = 'Select',
84
+ starred,
85
+ onStarredChange,
86
+ starLabel = 'Star',
87
+ onOpen,
88
+ onPress,
89
+ className,
90
+ ...rest
91
+ }: FileRowProps) {
92
+ const press = (e: MouseEvent<HTMLButtonElement>) => {
93
+ if (onPress) onPress(e);
94
+ else if (selectionMode) onSelectedChange?.(!selected);
95
+ else onOpen?.();
96
+ };
97
+ return (
98
+ <li
99
+ data-selected={selected || undefined}
100
+ className={cn(
101
+ selectable ? GRID_SELECTABLE : GRID,
102
+ 'group relative min-h-[42px] border-b border-rule-soft px-3 py-2 text-ui text-fg',
103
+ selected
104
+ ? 'bg-brand-field text-ink'
105
+ : tone === 'attention'
106
+ ? 'bg-yellow-wash-32 ink:bg-yellow-accent/15'
107
+ : tone === 'error'
108
+ ? 'bg-error-wash ink:bg-error/20'
109
+ : 'hover:bg-hover',
110
+ className,
111
+ )}
112
+ {...rest}
113
+ >
114
+ {selectable && (
115
+ <CheckboxPrimitive.Root
116
+ checked={selected}
117
+ onCheckedChange={(v) => onSelectedChange?.(v === true)}
118
+ aria-label={selectLabel}
119
+ className={cn(
120
+ 'relative z-10 grid size-4 cursor-pointer place-items-center rounded-xs border border-rule bg-page outline-none',
121
+ 'data-[state=checked]:border-fg data-[state=checked]:bg-fg data-[state=checked]:text-page focus-visible:shadow-(--focus-ring)',
122
+ selectionMode || selected ? 'opacity-100' : 'opacity-0 group-hover:opacity-100 focus-visible:opacity-100',
123
+ )}
124
+ >
125
+ <CheckboxPrimitive.Indicator>
126
+ <Check size={12} strokeWidth={2.5} aria-hidden />
127
+ </CheckboxPrimitive.Indicator>
128
+ </CheckboxPrimitive.Root>
129
+ )}
130
+ <Glyph size={16} strokeWidth={1.75} aria-hidden className="text-fg" />
131
+ <button
132
+ type="button"
133
+ onClick={press}
134
+ aria-pressed={selectionMode ? selected : undefined}
135
+ className="min-w-0 cursor-pointer truncate text-left outline-none after:absolute after:inset-0 after:content-[''] focus-visible:after:shadow-(--focus-ring)"
136
+ >
137
+ {name}
138
+ </button>
139
+ <span className="min-w-0">{statusLabel ? <Badge status={status}>{statusLabel}</Badge> : null}</span>
140
+ <span className={cn('min-w-0 truncate text-small', selected ? 'text-fg-on-yellow-secondary' : 'text-fg-secondary')}>{updated}</span>
141
+ <span className="flex justify-end">
142
+ {onStarredChange && (
143
+ <button
144
+ type="button"
145
+ aria-label={starLabel}
146
+ aria-pressed={Boolean(starred)}
147
+ onClick={() => onStarredChange(!starred)}
148
+ className={cn(
149
+ 'relative z-10 grid size-7 cursor-pointer place-items-center rounded-md outline-none hover:bg-hover focus-visible:shadow-(--focus-ring)',
150
+ starred ? 'opacity-100' : 'opacity-0 group-hover:opacity-100 focus-visible:opacity-100',
151
+ )}
152
+ >
153
+ <Star size={15} strokeWidth={1.75} aria-hidden className={starred ? 'fill-brand-mark' : undefined} />
154
+ </button>
155
+ )}
156
+ </span>
157
+ </li>
158
+ );
159
+ }