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

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 (62) hide show
  1. package/CHANGELOG.md +109 -0
  2. package/package.json +10 -4
  3. package/src/brand.css +1 -1
  4. package/src/components/ActionBar.tsx +607 -0
  5. package/src/components/ActivityTimeline.tsx +77 -0
  6. package/src/components/AppRail.tsx +141 -0
  7. package/src/components/AsyncSelect.tsx +205 -0
  8. package/src/components/AuthLayout.tsx +127 -0
  9. package/src/components/Avatar.tsx +75 -0
  10. package/src/components/BottomSheet.tsx +151 -0
  11. package/src/components/BottomTabs.tsx +102 -0
  12. package/src/components/BrandLoader.tsx +122 -0
  13. package/src/components/Breadcrumb.tsx +129 -0
  14. package/src/components/Button.tsx +48 -40
  15. package/src/components/ChoiceCards.tsx +68 -0
  16. package/src/components/CodeInput.tsx +150 -0
  17. package/src/components/CommandPalette.tsx +228 -0
  18. package/src/components/ConfirmDialog.tsx +294 -0
  19. package/src/components/CopyField.tsx +104 -0
  20. package/src/components/DateInput.tsx +366 -0
  21. package/src/components/Dialog.tsx +27 -3
  22. package/src/components/Dropzone.tsx +187 -0
  23. package/src/components/FileCard.tsx +233 -0
  24. package/src/components/FileRow.tsx +159 -0
  25. package/src/components/FolderTree.tsx +377 -0
  26. package/src/components/ListToolbar.tsx +177 -0
  27. package/src/components/MentionInput.tsx +233 -0
  28. package/src/components/Menu.tsx +109 -0
  29. package/src/components/PageHeader.tsx +62 -0
  30. package/src/components/PageState.tsx +84 -0
  31. package/src/components/Popover.tsx +46 -0
  32. package/src/components/Progress.tsx +97 -0
  33. package/src/components/PulseDot.tsx +31 -0
  34. package/src/components/RecipientsInput.tsx +252 -0
  35. package/src/components/SearchField.tsx +137 -0
  36. package/src/components/SegmentedControl.tsx +95 -0
  37. package/src/components/SidePanel.tsx +130 -0
  38. package/src/components/Skeleton.tsx +73 -0
  39. package/src/components/StageProgress.tsx +51 -0
  40. package/src/components/Steps.tsx +58 -0
  41. package/src/components/SyncStatus.tsx +56 -0
  42. package/src/components/ThemeSwitch.tsx +99 -0
  43. package/src/components/Toast.tsx +45 -8
  44. package/src/components/UploadCheck.tsx +118 -0
  45. package/src/components/UploadList.tsx +176 -0
  46. package/src/components/VirtualList.tsx +90 -0
  47. package/src/components/WindowDrop.tsx +131 -0
  48. package/src/components/WorkspaceRow.tsx +80 -0
  49. package/src/components/inputs-aria.ts +5 -0
  50. package/src/components/inputs-date.ts +92 -0
  51. package/src/components/inputs-listbox.tsx +154 -0
  52. package/src/components/inputs-search.ts +60 -0
  53. package/src/components/status-files.ts +124 -0
  54. package/src/components/status-motion.ts +30 -0
  55. package/src/components/useSelection.ts +142 -0
  56. package/src/index.ts +52 -1
  57. package/src/theme-base.css +123 -0
  58. package/src/theme-reset.css +14 -0
  59. package/src/theme.css +2 -125
  60. package/src/components/core.test.tsx +0 -242
  61. package/src/components/forms.test.tsx +0 -280
  62. package/src/components/overlays.test.tsx +0 -245
@@ -1,245 +0,0 @@
1
- import { act, render, screen, within } from '@testing-library/react';
2
- import userEvent from '@testing-library/user-event';
3
- import { useState } from 'react';
4
- import { afterEach, describe, expect, it, vi } from 'vitest';
5
-
6
- import { a11y } from '../../test/a11y';
7
- import { Button } from './Button';
8
- import { Dialog, DialogClose, DialogContent, DialogTrigger } from './Dialog';
9
- import { Table, type TableSort } from './Table';
10
- import { TabPanel, Tabs } from './Tabs';
11
- import { Toaster, toast } from './Toast';
12
- import { Tooltip } from './Tooltip';
13
-
14
-
15
- describe('Tabs', () => {
16
- const items = [
17
- { value: 'overview', label: 'Overview' },
18
- { value: 'documents', label: 'Documents', count: 3 },
19
- { value: 'parties', label: 'Parties', count: 0 },
20
- ];
21
-
22
- it('arrow keys move and aria-selected follows; onChange reports the value', async () => {
23
- const onChange = vi.fn();
24
- render(
25
- <Tabs items={items} defaultValue="overview" onChange={onChange} label="Project">
26
- <TabPanel value="overview">Overview panel</TabPanel>
27
- <TabPanel value="documents">Documents panel</TabPanel>
28
- <TabPanel value="parties">Parties panel</TabPanel>
29
- </Tabs>,
30
- );
31
- const tabs = screen.getAllByRole('tab');
32
- expect(tabs[0]).toHaveAttribute('aria-selected', 'true');
33
- await userEvent.click(tabs[0]!);
34
- await userEvent.keyboard('{ArrowRight}');
35
- expect(tabs[1]).toHaveAttribute('aria-selected', 'true');
36
- expect(onChange).toHaveBeenLastCalledWith('documents');
37
- expect(screen.getByRole('tabpanel')).toHaveTextContent('Documents panel');
38
- await userEvent.keyboard('{End}');
39
- expect(tabs[2]).toHaveAttribute('aria-selected', 'true');
40
- });
41
-
42
- it('count shows as a badge, hidden at zero', () => {
43
- render(<Tabs items={items} defaultValue="overview" label="Project" />);
44
- expect(screen.getByRole('tab', { name: 'Documents 3' })).toBeInTheDocument();
45
- expect(screen.getByRole('tab', { name: 'Parties' })).toBeInTheDocument();
46
- });
47
-
48
- it('passes axe (both variants)', async () => {
49
- const { container } = render(
50
- <div>
51
- <Tabs items={items} defaultValue="overview" label="Underline">
52
- <TabPanel value="overview">a</TabPanel>
53
- </Tabs>
54
- <Tabs variant="pills" items={items} defaultValue="documents" label="Pills">
55
- <TabPanel value="documents">b</TabPanel>
56
- </Tabs>
57
- </div>,
58
- );
59
- expect(await a11y(container)).toHaveNoViolations();
60
- });
61
- });
62
-
63
- describe('Dialog', () => {
64
- function Revoke() {
65
- return (
66
- <Dialog>
67
- <DialogTrigger asChild>
68
- <Button variant="secondary">Revoke share</Button>
69
- </DialogTrigger>
70
- <DialogContent
71
- size="sm"
72
- title="Revoke this share?"
73
- description="Anna loses access now."
74
- footer={
75
- <>
76
- <DialogClose asChild>
77
- <Button variant="secondary">Cancel</Button>
78
- </DialogClose>
79
- <Button variant="danger">Revoke share</Button>
80
- </>
81
- }
82
- />
83
- </Dialog>
84
- );
85
- }
86
-
87
- it('is labelled by its title and described by its consequence', async () => {
88
- render(<Revoke />);
89
- await userEvent.click(screen.getByRole('button', { name: 'Revoke share' }));
90
- const dialog = screen.getByRole('dialog', { name: 'Revoke this share?' });
91
- expect(dialog).toHaveAccessibleDescription('Anna loses access now.');
92
- });
93
-
94
- it('traps focus, closes on Esc and returns focus to the trigger', async () => {
95
- render(<Revoke />);
96
- const trigger = screen.getByRole('button', { name: 'Revoke share' });
97
- await userEvent.click(trigger);
98
- const dialog = screen.getByRole('dialog');
99
- expect(dialog.contains(document.activeElement)).toBe(true);
100
- for (let i = 0; i < 6; i++) {
101
- await userEvent.tab();
102
- expect(dialog.contains(document.activeElement)).toBe(true);
103
- }
104
- await userEvent.keyboard('{Escape}');
105
- expect(screen.queryByRole('dialog')).not.toBeInTheDocument();
106
- expect(trigger).toHaveFocus();
107
- });
108
-
109
- it('passes axe while open', async () => {
110
- render(<Revoke />);
111
- await userEvent.click(screen.getByRole('button', { name: 'Revoke share' }));
112
- expect(await a11y(document.body)).toHaveNoViolations();
113
- });
114
- });
115
-
116
- describe('Toast', () => {
117
- afterEach(() => {
118
- act(() => toast.reset());
119
- vi.useRealTimers();
120
- });
121
-
122
- it('is announced in a live region and leaves after 4s (8s with an action)', () => {
123
- vi.useFakeTimers();
124
- render(<Toaster />);
125
- act(() => {
126
- toast({ title: 'Link copied.' });
127
- toast({ title: '3 files moved to Archive.', status: 'complete', action: { label: 'Undo', onClick: () => {} } });
128
- });
129
- // Radix announces toasts through a status live region.
130
- expect(document.querySelector('[role="status"][aria-live]')).not.toBeNull();
131
- expect(screen.getByText('Link copied.')).toBeInTheDocument();
132
- act(() => vi.advanceTimersByTime(4100));
133
- expect(screen.queryByText('Link copied.')).not.toBeInTheDocument();
134
- expect(screen.getByText('3 files moved to Archive.')).toBeInTheDocument();
135
- act(() => vi.advanceTimersByTime(4000));
136
- expect(screen.queryByText('3 files moved to Archive.')).not.toBeInTheDocument();
137
- });
138
-
139
- it('shows the same error once per 30 seconds and at most three at once', () => {
140
- render(<Toaster />);
141
- act(() => {
142
- expect(toast({ title: "Couldn't rename the folder.", status: 'error' })).not.toBeNull();
143
- expect(toast({ title: "Couldn't rename the folder.", status: 'error' })).toBeNull();
144
- toast({ title: 'One.' });
145
- toast({ title: 'Two.' });
146
- toast({ title: 'Three.' });
147
- });
148
- expect(screen.queryByText("Couldn't rename the folder.")).not.toBeInTheDocument();
149
- expect(screen.getByText('Three.')).toBeInTheDocument();
150
- });
151
-
152
- it('never takes focus', () => {
153
- render(
154
- <>
155
- <button>Work</button>
156
- <Toaster />
157
- </>,
158
- );
159
- screen.getByRole('button', { name: 'Work' }).focus();
160
- act(() => void toast({ title: 'Link copied.' }));
161
- expect(screen.getByRole('button', { name: 'Work' })).toHaveFocus();
162
- });
163
- });
164
-
165
- describe('Tooltip', () => {
166
- it('opens on keyboard focus as well as hover', async () => {
167
- render(
168
- <Tooltip content="Downloads are off for this share">
169
- <button>Share</button>
170
- </Tooltip>,
171
- );
172
- await userEvent.tab();
173
- expect(screen.getByRole('button', { name: 'Share' })).toHaveFocus();
174
- expect(await screen.findByRole('tooltip')).toHaveTextContent('Downloads are off for this share');
175
- });
176
-
177
- it('opens on hover', async () => {
178
- render(
179
- <Tooltip content="Star" delay={0}>
180
- <button>☆</button>
181
- </Tooltip>,
182
- );
183
- await userEvent.hover(screen.getByRole('button'));
184
- expect(await screen.findByRole('tooltip')).toHaveTextContent('Star');
185
- });
186
- });
187
-
188
- describe('Table', () => {
189
- type Row = { id: string; status?: 'attention' | 'error'; task: string; docs: number };
190
- const rows: Row[] = [
191
- { id: 'T-119', task: 'Translate the term sheet', docs: 1 },
192
- { id: 'T-122', status: 'error', task: 'Prepare the data room index', docs: 20 },
193
- { id: 'T-131', status: 'attention', task: 'Sign the engagement letter', docs: 2 },
194
- ];
195
- const columns = [
196
- { key: 'task', label: 'Task', sortable: true },
197
- { key: 'docs', label: 'Docs', align: 'right' as const, sortable: true },
198
- ];
199
-
200
- function Sorted({ onRowClick }: { onRowClick: (r: Row) => void }) {
201
- const [sort, setSort] = useState<TableSort>({ key: 'task', direction: 'asc' });
202
- return <Table caption="Review queue" columns={columns} rows={rows} sort={sort} onSortChange={setSort} onRowClick={onRowClick} />;
203
- }
204
-
205
- it('sorting sets aria-sort on the header', async () => {
206
- render(<Sorted onRowClick={() => {}} />);
207
- const [task, docs] = screen.getAllByRole('columnheader');
208
- expect(task).toHaveAttribute('aria-sort', 'ascending');
209
- expect(docs).toHaveAttribute('aria-sort', 'none');
210
- await userEvent.click(within(docs!).getByRole('button', { name: 'Docs' }));
211
- expect(docs).toHaveAttribute('aria-sort', 'ascending');
212
- await userEvent.click(within(docs!).getByRole('button', { name: 'Docs' }));
213
- expect(docs).toHaveAttribute('aria-sort', 'descending');
214
- });
215
-
216
- it('rows open on click and on Enter from the keyboard', async () => {
217
- const onRowClick = vi.fn();
218
- render(<Sorted onRowClick={onRowClick} />);
219
- const row = screen.getByRole('row', { name: /Prepare the data room index/ });
220
- await userEvent.click(row);
221
- expect(onRowClick).toHaveBeenLastCalledWith(rows[1]);
222
- row.focus();
223
- await userEvent.keyboard('{Enter}');
224
- expect(onRowClick).toHaveBeenCalledTimes(2);
225
- });
226
-
227
- it('rows needing a person take a wash; the title can be a real link', () => {
228
- render(<Table columns={columns} rows={rows} rowHref={(r) => `/tasks/${r.id}`} />);
229
- expect(screen.getByRole('row', { name: /data room/ }).className).toMatch(/bg-error-wash/);
230
- expect(screen.getByRole('row', { name: /engagement letter/ }).className).toMatch(/bg-attention-wash/);
231
- expect(screen.getByRole('link', { name: 'Sign the engagement letter' })).toHaveAttribute('href', '/tasks/T-131');
232
- });
233
-
234
- it('empty and loading', () => {
235
- const { rerender } = render(<Table columns={columns} rows={[]} empty="No tasks yet." />);
236
- expect(screen.getByText('No tasks yet.')).toBeInTheDocument();
237
- rerender(<Table columns={columns} rows={[]} loading />);
238
- expect(screen.getAllByRole('columnheader')).toHaveLength(2);
239
- });
240
-
241
- it('passes axe', async () => {
242
- const { container } = render(<Sorted onRowClick={() => {}} />);
243
- expect(await a11y(container)).toHaveNoViolations();
244
- });
245
- });