@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
@@ -0,0 +1,245 @@
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
+ });
@@ -0,0 +1,57 @@
1
+ import { useCallback, useEffect, useRef, useState } from 'react';
2
+
3
+ import type { ButtonState } from './Button';
4
+
5
+ export type ButtonAction<A extends unknown[]> = {
6
+ /** Pass to <Button state>. */
7
+ state: ButtonState;
8
+ /** The last failure, to explain beside the button (with a Retry). Cleared on the next run. */
9
+ error: unknown;
10
+ /** Run the action: busy → done for `doneFor` ms → idle; on failure → idle with `error` set. */
11
+ run: (...args: A) => Promise<void>;
12
+ };
13
+
14
+ /**
15
+ * useButtonAction — the async button of the spec: busy while the action
16
+ * runs, the result confirmed in place for 1.6s, a failure kept for the
17
+ * caller to explain in place. Repeat runs while busy are ignored.
18
+ */
19
+ export function useButtonAction<A extends unknown[]>(action: (...args: A) => Promise<unknown>, { doneFor = 1600 }: { doneFor?: number } = {}): ButtonAction<A> {
20
+ const [state, setState] = useState<ButtonState>('idle');
21
+ const [error, setError] = useState<unknown>(null);
22
+ const busy = useRef(false);
23
+ const timer = useRef<ReturnType<typeof setTimeout> | null>(null);
24
+ const alive = useRef(true);
25
+
26
+ useEffect(
27
+ () => () => {
28
+ alive.current = false;
29
+ if (timer.current) clearTimeout(timer.current);
30
+ },
31
+ [],
32
+ );
33
+
34
+ const run = useCallback(
35
+ async (...args: A) => {
36
+ if (busy.current) return;
37
+ busy.current = true;
38
+ setError(null);
39
+ setState('busy');
40
+ try {
41
+ await action(...args);
42
+ if (!alive.current) return;
43
+ setState('done');
44
+ timer.current = setTimeout(() => alive.current && setState('idle'), doneFor);
45
+ } catch (e) {
46
+ if (!alive.current) return;
47
+ setError(e);
48
+ setState('idle');
49
+ } finally {
50
+ busy.current = false;
51
+ }
52
+ },
53
+ [action, doneFor],
54
+ );
55
+
56
+ return { state, error, run };
57
+ }
package/src/fonts.css ADDED
@@ -0,0 +1,20 @@
1
+ /* @cosxai/ui — self-hosted fonts.
2
+ *
3
+ * The 3.0 tokens load Geist and Noto Sans SC from Google Fonts
4
+ * (tokens/fonts.css in the design system, not shipped here). The kit
5
+ * serves them from the consumer's own bundle instead: Fontsource
6
+ * variable builds, OFL-1.1, split by unicode range so a page downloads
7
+ * only the glyphs it shows.
8
+ *
9
+ * Fontsource names the families "… Variable"; the stacks below put those
10
+ * first and keep the design system's own names after them. Loaded after
11
+ * the tokens (styles.css), so these stacks are the ones in effect.
12
+ */
13
+ @import "@fontsource-variable/geist";
14
+ @import "@fontsource-variable/noto-sans-sc";
15
+
16
+ :root {
17
+ --font-sans: "Geist Variable", "Geist", "Helvetica Neue", Helvetica, Arial, sans-serif;
18
+ --font-cjk: "Noto Sans SC Variable", "Noto Sans SC", "PingFang SC", "Microsoft YaHei", sans-serif;
19
+ --font-sans-cjk: "Geist Variable", "Geist", "Noto Sans SC Variable", "Noto Sans SC", "PingFang SC", "Microsoft YaHei", sans-serif;
20
+ }
package/src/index.ts CHANGED
@@ -1,25 +1,35 @@
1
- // Entry point — re-exports everything consumers need.
2
- // Components, hooks, theme, and helpers are organised under
3
- // /primitives, /layout, /actionbar, /command, /dialogs, /hooks,
4
- // /theme, /pwa, /lib. As the kit grows, each phase adds exports
5
- // here. Consumers pull individual modules (`import { Button }
6
- // from "@cosxai/ui"`) or copy source files directly per the
7
- // shadcn-style distribution model.
1
+ /**
2
+ * @cosxai/ui 1.x — the COSX Design System 3.0 in React.
3
+ *
4
+ * Foundations: import "@cosxai/ui/styles.css" (tokens + fonts) and, in a
5
+ * Tailwind v4 app, "@cosxai/ui/theme.css". Plan and status:
6
+ * docs/workdocs/2026-09-29_feature-ui-1.0.
7
+ */
8
+ export { cn } from './lib/cn';
8
9
 
9
- export * from "./primitives";
10
- export * from "./lib/cn";
11
- export * from "./lib/time-utils";
12
- export * from "./theme";
13
- export * from "./hooks";
14
- export * from "./layout";
15
- export * from "./dialogs";
16
- export * from "./actionbar";
17
- export * from "./command";
18
- export * from "./pwa";
19
- export * from "./editorial";
20
- export * from "./neobrutalism";
21
- export * from "./ambient";
22
- export * from "./terminal";
23
- export * from "./bento";
24
- export * from "./riso";
25
- export * from "./sketch";
10
+ export { Badge, type BadgeAppearance, type BadgeProps, type BadgeStatus } from './components/Badge';
11
+ export { Button, formatCount, type ButtonGround, type ButtonProps, type ButtonState, type ButtonVariant } from './components/Button';
12
+ export { Card, type CardProps } from './components/Card';
13
+ export { Figure, type FigureProps } from './components/Figure';
14
+ export { Icon, type IconProps } from './components/Icon';
15
+ export { IconButton, type IconButtonProps } from './components/IconButton';
16
+ export { Logo, type LogoProps } from './components/Logo';
17
+ export { Marker, type MarkerProps } from './components/Marker';
18
+ export { MetaLabel, type MetaLabelProps } from './components/MetaLabel';
19
+ export { Spinner, type SpinnerProps } from './components/Spinner';
20
+ export { Tag, type TagProps } from './components/Tag';
21
+ export { ThemeProvider, useTheme, type ThemeMode, type ThemeProviderProps } from './components/ThemeProvider';
22
+ export { useButtonAction, type ButtonAction } from './components/useButtonAction';
23
+
24
+ export { Checkbox, type CheckboxProps } from './components/Checkbox';
25
+ export { Dialog, DialogClose, DialogContent, DialogTrigger, type DialogContentProps } from './components/Dialog';
26
+ export { Field, useField, type FieldContextValue, type FieldProps } from './components/Field';
27
+ export { Input, type InputProps } from './components/Input';
28
+ export { RadioGroup, type RadioGroupProps, type RadioOption } from './components/Radio';
29
+ export { Select, type SelectOption, type SelectProps } from './components/Select';
30
+ export { Switch, type SwitchProps } from './components/Switch';
31
+ export { Table, type TableColumn, type TableProps, type TableRowBase, type TableSort, type TableSortDirection } from './components/Table';
32
+ export { Tab, TabList, TabPanel, Tabs, type TabItem, type TabListProps, type TabPanelProps, type TabProps, type TabsProps, type TabsVariant } from './components/Tabs';
33
+ export { Textarea, type TextareaProps } from './components/Textarea';
34
+ export { Toaster, toast, type ToasterProps, type ToastLabels, type ToastOptions, type ToastStatus } from './components/Toast';
35
+ export { Tooltip, TooltipProvider, type TooltipProps } from './components/Tooltip';
package/src/lib/cn.ts CHANGED
@@ -1,7 +1,24 @@
1
- // Lightweight className merge. Filters falsy values and joins with a
2
- // single space. Sufficient for component composition; if a consumer
3
- // needs tailwind-merge / clsx semantics they can swap implementations
4
- // without touching call sites.
5
- export function cn(...inputs: Array<string | false | null | undefined>): string {
6
- return inputs.filter(Boolean).join(" ");
1
+ import { clsx, type ClassValue } from 'clsx';
2
+ import { extendTailwindMerge } from 'tailwind-merge';
3
+
4
+ /**
5
+ * cn — join class names and let the later Tailwind class win on conflict
6
+ * (`cn('p-2', cond && 'p-4')` → `p-4`).
7
+ *
8
+ * tailwind-merge only knows Tailwind's default scales; the kit's theme
9
+ * (theme.css) replaces font sizes with its own names, taught here so
10
+ * `text-ui` and `text-fg` are not mistaken for each other (one is a
11
+ * size, one a colour).
12
+ */
13
+ const merge = extendTailwindMerge({
14
+ extend: {
15
+ theme: {
16
+ text: ['meta', 'small', 'ui', 'body', 'h3', 'lede', 'title', 'figure', 'h2', 'h1', 'display'],
17
+ radius: ['xs', 'sm', 'md', 'lg', 'xl', 'pill'],
18
+ },
19
+ },
20
+ });
21
+
22
+ export function cn(...inputs: ClassValue[]): string {
23
+ return merge(clsx(inputs));
7
24
  }
package/src/modes.css ADDED
@@ -0,0 +1,10 @@
1
+ /* @cosxai/ui — ink mode, completed.
2
+ *
3
+ * 3.0 switches its colours on either data-mode="ink" (root) or .ink-mode
4
+ * (subtree) — colors.css — but its marker rules (typography.css,
5
+ * typography-cjk.css) only know .ink-mode. Same rules for data-mode, so a
6
+ * page in ink mode gets the ink marker: no band, the finding in accent
7
+ * yellow. Reported to the design system; drop this when 3.0 covers it.
8
+ */
9
+ [data-mode="ink"] .marker { color: var(--yellow-accent); background: none; padding: 0; margin: 0; border-radius: 0; }
10
+ [data-mode="ink"] :lang(zh) .marker, :lang(zh)[data-mode="ink"] .marker { background: none; }
package/src/styles.css ADDED
@@ -0,0 +1,19 @@
1
+ /* @cosxai/ui — the COSX Design System 3.0 foundations.
2
+ *
3
+ * tokens/ are the design system's own files, copied unchanged from the
4
+ * "COSX Design System 3.0" project. Edit them there and copy again; the
5
+ * kit's additions live in fonts.css and theme.css.
6
+ *
7
+ * Ink (dark) mode: set data-mode="ink" on <html>, or put .ink-mode on a
8
+ * subtree (colors.css).
9
+ */
10
+ @import "./tokens/colors.css";
11
+ @import "./tokens/typography.css";
12
+ @import "./tokens/typography-cjk.css";
13
+ @import "./tokens/spacing.css";
14
+ @import "./tokens/elevation.css";
15
+ @import "./tokens/motion.css";
16
+ @import "./tokens/compat.css";
17
+ @import "./brand.css";
18
+ @import "./modes.css";
19
+ @import "./fonts.css";
package/src/theme.css ADDED
@@ -0,0 +1,143 @@
1
+ /* @cosxai/ui — Tailwind v4 theme for the COSX Design System 3.0.
2
+ *
3
+ * @import "tailwindcss";
4
+ * @import "@cosxai/ui/theme.css";
5
+ * @import "@cosxai/ui/styles.css";
6
+ * @source "../node_modules/@cosxai/ui/src"; (so the kit's classes are generated)
7
+ *
8
+ * Utilities point at the 3.0 variables ("inline"), so ink mode —
9
+ * data-mode="ink" / .ink-mode — switches them with no dark: variants.
10
+ *
11
+ * Tailwind's own palette, radii, shadows, fonts and easings are cleared:
12
+ * only what the design system defines exists (no bg-blue-500, no
13
+ * shadow-lg — 3.0 has no shadows).
14
+ *
15
+ * Names never collide with a 3.0 variable unless the value is the same
16
+ * (theme.test.ts). In particular font sizes avoid --text-primary /
17
+ * --text-secondary / --text-label, which are colours in 3.0.
18
+ */
19
+ /* ink: — styles for ink mode (data-mode="ink" on <html>, or inside
20
+ .ink-mode). The semantic colours switch by themselves; this is for the
21
+ rules that flip on ink (the primary becomes the yellow). */
22
+ @custom-variant ink (&:where([data-mode="ink"], [data-mode="ink"] *, .ink-mode, .ink-mode *));
23
+
24
+ @theme inline {
25
+ --color-*: initial;
26
+ --radius-*: initial;
27
+ --shadow-*: initial;
28
+ --inset-shadow-*: initial;
29
+ --drop-shadow-*: initial;
30
+ --font-*: initial;
31
+ --ease-*: initial;
32
+ --text-*: initial;
33
+ --font-weight-*: initial;
34
+ --tracking-*: initial;
35
+
36
+ /* Materials — fixed, do not switch with ink mode */
37
+ --color-paper: var(--paper);
38
+ --color-linen: var(--linen);
39
+ --color-sunk-2: var(--sunk-2);
40
+ --color-sunk-3: var(--sunk-3);
41
+ --color-ink: var(--ink);
42
+ --color-ink-raised: var(--ink-raised);
43
+ --color-grey: var(--grey);
44
+ --color-grey-inverse: var(--grey-inverse);
45
+
46
+ /* The yellow — one hue, by area */
47
+ --color-yellow: var(--yellow);
48
+ --color-yellow-accent: var(--yellow-accent);
49
+ --color-yellow-accent-hover: var(--yellow-accent-hover);
50
+ --color-yellow-light: var(--yellow-light);
51
+ --color-yellow-hover: var(--yellow-hover);
52
+ --color-yellow-1: var(--yellow-1);
53
+ --color-yellow-2: var(--yellow-2);
54
+ --color-yellow-3: var(--yellow-3);
55
+ --color-yellow-4: var(--yellow-4);
56
+ --color-yellow-5: var(--yellow-5);
57
+ --color-yellow-wash-12: var(--yellow-12);
58
+ --color-yellow-wash-20: var(--yellow-20);
59
+ --color-yellow-wash-32: var(--yellow-32);
60
+ --color-yellow-wash-50: var(--yellow-50);
61
+ --color-marker: var(--marker);
62
+
63
+ /* Semantic — switch with ink mode. Product code uses these. */
64
+ --color-page: var(--bg-page);
65
+ --color-sunk: var(--bg-sunk);
66
+ --color-well: var(--bg-well);
67
+ --color-chrome: var(--bg-chrome);
68
+ --color-field: var(--bg-field);
69
+ --color-fg: var(--text-primary);
70
+ --color-fg-secondary: var(--text-secondary);
71
+ --color-fg-label: var(--text-label);
72
+ --color-fg-on-yellow-secondary: var(--text-on-yellow-secondary);
73
+ --color-rule: var(--rule);
74
+ --color-rule-soft: var(--rule-soft);
75
+ --color-hover: var(--hover);
76
+ --color-scrim: var(--scrim);
77
+
78
+ /* Workspace brand (brand.css): the COSX yellow unless a customer's
79
+ colour is injected. Selection and grounds use field; dots and
80
+ progress use mark. Never for text. */
81
+ --color-brand-field: var(--brand-field);
82
+ --color-brand-mark: var(--brand-mark);
83
+
84
+ /* On an ink surface inside a paper page */
85
+ --color-inv: var(--inv-bg);
86
+ --color-inv-raised: var(--inv-bg-raised);
87
+ --color-inv-fg: var(--inv-text);
88
+ --color-inv-fg-secondary: var(--inv-text-secondary);
89
+ --color-inv-rule: var(--inv-rule);
90
+
91
+ /* Product status: yellow, ink and one red. Progress is an ink outline and
92
+ complete a grey dot — they take no colour of their own. */
93
+ --color-attention: var(--status-attention);
94
+ --color-attention-wash: var(--status-attention-wash);
95
+ --color-error: var(--status-error);
96
+ --color-error-text: var(--status-error-text);
97
+ --color-error-wash: var(--status-error-wash);
98
+
99
+ --color-transparent: transparent;
100
+ --color-current: currentColor;
101
+ --color-white: #fff;
102
+
103
+ /* Radius ladder — same names and values as 3.0 spacing.css */
104
+ --radius-xs: 4px;
105
+ --radius-sm: 6px;
106
+ --radius-md: 8px;
107
+ --radius-lg: 16px;
108
+ --radius-xl: 24px;
109
+ --radius-pill: 999px;
110
+
111
+ /* One family. Same stacks as fonts.css (the ones in effect): font-sans
112
+ for Latin, font-sans-cjk for Chinese pages (Geist for Latin and
113
+ figures, Noto Sans SC for hanzi). */
114
+ --font-sans: "Geist Variable", "Geist", "Helvetica Neue", Helvetica, Arial, sans-serif;
115
+ --font-sans-cjk: "Geist Variable", "Geist", "Noto Sans SC Variable", "Noto Sans SC", "PingFang SC", "Microsoft YaHei", sans-serif;
116
+
117
+ --font-weight-normal: 400;
118
+ --font-weight-medium: 500;
119
+ --font-weight-semibold: 600;
120
+ --font-weight-bold: 700;
121
+
122
+ /* Type scale (typography.css). Product UI body is 14px, content 15px. */
123
+ --text-meta: 12px;
124
+ --text-small: 13.5px;
125
+ --text-ui: 14px;
126
+ --text-body: 15px;
127
+ --text-h3: 18px;
128
+ --text-lede: 20px;
129
+ --text-title: 22px;
130
+ --text-figure: 32px;
131
+ --text-h2: var(--size-h2);
132
+ --text-h1: var(--size-h1);
133
+ --text-display: var(--size-display);
134
+
135
+ --tracking-display: -0.025em;
136
+ --tracking-heading: -0.015em;
137
+ --tracking-figure: -0.03em;
138
+
139
+ /* Motion — flat for state, ease-out for entry; nothing bounces */
140
+ --ease-standard: cubic-bezier(.2,0,.2,1);
141
+ --ease-out: cubic-bezier(.16,1,.3,1);
142
+ --ease-in: cubic-bezier(.4,0,1,1);
143
+ }