@andreyfedkovich/cozy-ui 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +718 -0
- package/dist/index.d.ts +661 -0
- package/dist/styles.css +1 -0
- package/dist/ui-library.cjs.js +48 -0
- package/dist/ui-library.cjs.js.map +1 -0
- package/dist/ui-library.es.js +10518 -0
- package/dist/ui-library.es.js.map +1 -0
- package/package.json +149 -0
package/README.md
ADDED
|
@@ -0,0 +1,718 @@
|
|
|
1
|
+
<div align="center">
|
|
2
|
+
|
|
3
|
+
# Cozy UI
|
|
4
|
+
|
|
5
|
+
**A premium, opinionated React component library for crafted product UIs.**
|
|
6
|
+
|
|
7
|
+
Typed end-to-end · SCSS-modules with design tokens · SSR-safe · Tree-shakeable ESM + CJS
|
|
8
|
+
|
|
9
|
+
[](https://www.npmjs.com/package/@andreyfedkovich/cozy-ui)
|
|
10
|
+
[](https://bundlephobia.com/package/@andreyfedkovich/cozy-ui)
|
|
11
|
+
[](https://www.npmjs.com/package/@andreyfedkovich/cozy-ui)
|
|
12
|
+
[](./LICENSE)
|
|
13
|
+
[](https://react.dev)
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
npm i @andreyfedkovich/cozy-ui
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
</div>
|
|
20
|
+
|
|
21
|
+
---
|
|
22
|
+
|
|
23
|
+
## Table of contents
|
|
24
|
+
|
|
25
|
+
- [Why Cozy UI](#why-cozy-ui)
|
|
26
|
+
- [Installation](#installation)
|
|
27
|
+
- [Quick start](#quick-start)
|
|
28
|
+
- [Design tokens](#design-tokens)
|
|
29
|
+
- [Component API](#component-api)
|
|
30
|
+
- [Layout & content](#layout--content) — `BaseBlock`, `Card`, `CollapsableBlock`, `Collapse`, `Carousel`, `EmptyComponent`, `Spinner`
|
|
31
|
+
- [Inputs & forms](#inputs--forms) — `Button`, `RadioGroupButton`, `Select`, `DialogSelect`, `TreeDialogSelect`, `InputCaption`, `Label`
|
|
32
|
+
- [Navigation](#navigation) — `Tabs`, `TabsRounded`, `Stepper`
|
|
33
|
+
- [Overlays](#overlays) — `Popover`, `TooltipDark`, `TooltipLight`
|
|
34
|
+
- [Utility](#utility) — `Tag`, `CopyTextTrigger`
|
|
35
|
+
- [Workflow](#workflow) — `ApprovalRoute`
|
|
36
|
+
- [Hooks & helpers](#hooks--helpers)
|
|
37
|
+
- [Icons](#icons)
|
|
38
|
+
- [TypeScript](#typescript)
|
|
39
|
+
- [SSR & framework support](#ssr--framework-support)
|
|
40
|
+
- [Theming](#theming)
|
|
41
|
+
- [Accessibility](#accessibility)
|
|
42
|
+
- [Local development](#local-development)
|
|
43
|
+
- [Publishing](#publishing)
|
|
44
|
+
- [Contributing](#contributing)
|
|
45
|
+
- [License](#license)
|
|
46
|
+
|
|
47
|
+
---
|
|
48
|
+
|
|
49
|
+
## Why Cozy UI
|
|
50
|
+
|
|
51
|
+
- **Premium defaults out of the box.** Soft shadows, generous spacing, calm motion — no theming required to look polished.
|
|
52
|
+
- **Tokens you can trust.** Colors, radii, and surfaces are exported as both CSS custom properties and TypeScript constants.
|
|
53
|
+
- **Typed end-to-end.** Generics on `Select`, `DialogSelect`, `TreeDialogSelect`, `Carousel`, and `RadioGroupButton` — your data, your types.
|
|
54
|
+
- **Headless where it matters.** Dialogs and labels are powered by Radix primitives; positioning by `@floating-ui/react`.
|
|
55
|
+
- **SSR-safe.** Works in Next.js, TanStack Start, Remix, and any Vite SPA. Portals are guarded.
|
|
56
|
+
- **Zero global CSS leakage.** SCSS modules everywhere. One stylesheet to import, no surprises.
|
|
57
|
+
- **Tree-shakeable.** Ships ESM + CJS + `.d.ts`. Pay only for what you import.
|
|
58
|
+
|
|
59
|
+
---
|
|
60
|
+
|
|
61
|
+
## Installation
|
|
62
|
+
|
|
63
|
+
```bash
|
|
64
|
+
npm i @andreyfedkovich/cozy-ui
|
|
65
|
+
pnpm add @andreyfedkovich/cozy-ui
|
|
66
|
+
bun add @andreyfedkovich/cozy-ui
|
|
67
|
+
yarn add @andreyfedkovich/cozy-ui
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
Peer dependencies: **React ≥ 18** and **react-dom ≥ 18** (React 19 supported).
|
|
71
|
+
|
|
72
|
+
Import the stylesheet **once** at your app root:
|
|
73
|
+
|
|
74
|
+
```ts
|
|
75
|
+
import "@andreyfedkovich/cozy-ui/styles.css";
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
---
|
|
79
|
+
|
|
80
|
+
## Quick start
|
|
81
|
+
|
|
82
|
+
```tsx
|
|
83
|
+
import { Button, Card, Tag } from "@andreyfedkovich/cozy-ui";
|
|
84
|
+
import "@andreyfedkovich/cozy-ui/styles.css";
|
|
85
|
+
|
|
86
|
+
export default function App() {
|
|
87
|
+
return (
|
|
88
|
+
<div style={{ display: "grid", gap: 16, padding: 24 }}>
|
|
89
|
+
<Card text="Welcome to Cozy UI" height={160} />
|
|
90
|
+
<div style={{ display: "flex", gap: 8, alignItems: "center" }}>
|
|
91
|
+
<Tag>New</Tag>
|
|
92
|
+
<Button variant="primary" onClick={() => console.log("hi")}>
|
|
93
|
+
Get started
|
|
94
|
+
</Button>
|
|
95
|
+
</div>
|
|
96
|
+
</div>
|
|
97
|
+
);
|
|
98
|
+
}
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
---
|
|
102
|
+
|
|
103
|
+
## Design tokens
|
|
104
|
+
|
|
105
|
+
Tokens ship two ways:
|
|
106
|
+
|
|
107
|
+
1. **CSS custom properties** — applied globally by `styles.css` and consumable from any stylesheet.
|
|
108
|
+
2. **TypeScript constants** — re-exported from the package root, ideal for inline styles or chart libraries.
|
|
109
|
+
|
|
110
|
+
```ts
|
|
111
|
+
import { colors } from "@andreyfedkovich/cozy-ui";
|
|
112
|
+
|
|
113
|
+
const accent = colors.blue03; // typed string
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
| Group | Tokens (excerpt) |
|
|
117
|
+
| ----------- | ------------------------------------------------- |
|
|
118
|
+
| Brand | `blue01` … `blue07` |
|
|
119
|
+
| Neutrals | `gray01` … `gray09`, `white`, `black` |
|
|
120
|
+
| Status | `green`, `red`, `yellow` |
|
|
121
|
+
| Surfaces | `surfacePrimary`, `surfaceMuted`, `surfaceRaised` |
|
|
122
|
+
|
|
123
|
+
Override a token in your app's CSS:
|
|
124
|
+
|
|
125
|
+
```css
|
|
126
|
+
:root {
|
|
127
|
+
--cozy-blue-03: #2563eb;
|
|
128
|
+
}
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
---
|
|
132
|
+
|
|
133
|
+
## Component API
|
|
134
|
+
|
|
135
|
+
Every snippet below is copy-paste runnable against the real exports.
|
|
136
|
+
|
|
137
|
+
### Layout & content
|
|
138
|
+
|
|
139
|
+
#### `BaseBlock`
|
|
140
|
+
|
|
141
|
+
A titled section wrapper with optional subtitle. Use it as the building block of dashboards and forms.
|
|
142
|
+
|
|
143
|
+
| Prop | Type | Default | Description |
|
|
144
|
+
| ---------- | ----------------- | ------- | ------------------------------------ |
|
|
145
|
+
| `id` | `string` | — | Anchor id for in-page navigation. |
|
|
146
|
+
| `title` | `ReactNode` | — | Section title. |
|
|
147
|
+
| `subtitle` | `ReactNode` | — | Supporting copy under the title. |
|
|
148
|
+
| `children` | `ReactNode` | — | Section content. |
|
|
149
|
+
| `className`| `string` | — | Additional class on the root. |
|
|
150
|
+
|
|
151
|
+
```tsx
|
|
152
|
+
import { BaseBlock } from "@andreyfedkovich/cozy-ui";
|
|
153
|
+
|
|
154
|
+
<BaseBlock title="Profile" subtitle="Public information visible to teammates">
|
|
155
|
+
{/* form content */}
|
|
156
|
+
</BaseBlock>;
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
#### `Card`
|
|
160
|
+
|
|
161
|
+
A premium content tile with optional background image and link behavior.
|
|
162
|
+
|
|
163
|
+
| Prop | Type | Default | Description |
|
|
164
|
+
| ----------------- | ------------------- | ------- | ------------------------------------ |
|
|
165
|
+
| `text` | `string` | — | Title rendered inside the card. |
|
|
166
|
+
| `width` | `number` | — | Fixed width in px. |
|
|
167
|
+
| `height` | `number` | — | Fixed height in px. |
|
|
168
|
+
| `backgroundColor` | `string` | — | CSS color for the surface. |
|
|
169
|
+
| `imageUrl` | `string` | — | Background image URL. |
|
|
170
|
+
| `textColor` | `string` | — | Title color override. |
|
|
171
|
+
| `link` | `string` | — | If provided, renders as a `<Link>`. |
|
|
172
|
+
| `className` | `string` | — | Extra class. |
|
|
173
|
+
|
|
174
|
+
```tsx
|
|
175
|
+
import { Card } from "@andreyfedkovich/cozy-ui";
|
|
176
|
+
|
|
177
|
+
<Card
|
|
178
|
+
text="Q4 highlights"
|
|
179
|
+
imageUrl="/covers/q4.jpg"
|
|
180
|
+
height={220}
|
|
181
|
+
link="/reports/q4"
|
|
182
|
+
/>;
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
#### `CollapsableBlock`
|
|
186
|
+
|
|
187
|
+
A block with a header that expands and collapses its content.
|
|
188
|
+
|
|
189
|
+
```tsx
|
|
190
|
+
import { CollapsableBlock } from "@andreyfedkovich/cozy-ui";
|
|
191
|
+
|
|
192
|
+
<CollapsableBlock title="Advanced settings">
|
|
193
|
+
{/* hidden by default */}
|
|
194
|
+
</CollapsableBlock>;
|
|
195
|
+
```
|
|
196
|
+
|
|
197
|
+
#### `Collapse`
|
|
198
|
+
|
|
199
|
+
Low-level animated open/close primitive — give it `isOpen` and children.
|
|
200
|
+
|
|
201
|
+
| Prop | Type | Default | Description |
|
|
202
|
+
| --------- | --------- | ------- | --------------------------------- |
|
|
203
|
+
| `isOpen` | `boolean` | `false` | Controls expansion. |
|
|
204
|
+
| `children`| `ReactNode` | — | Collapsible content. |
|
|
205
|
+
|
|
206
|
+
```tsx
|
|
207
|
+
import { Collapse } from "@andreyfedkovich/cozy-ui";
|
|
208
|
+
import { useState } from "react";
|
|
209
|
+
|
|
210
|
+
const [open, setOpen] = useState(false);
|
|
211
|
+
|
|
212
|
+
<>
|
|
213
|
+
<button onClick={() => setOpen((v) => !v)}>Toggle</button>
|
|
214
|
+
<Collapse isOpen={open}>Hidden content</Collapse>
|
|
215
|
+
</>;
|
|
216
|
+
```
|
|
217
|
+
|
|
218
|
+
#### `Carousel`
|
|
219
|
+
|
|
220
|
+
Generic, typed carousel with captions. Items must have an `id`.
|
|
221
|
+
|
|
222
|
+
```tsx
|
|
223
|
+
import { Carousel } from "@andreyfedkovich/cozy-ui";
|
|
224
|
+
|
|
225
|
+
const slides = [
|
|
226
|
+
{ id: 1, src: "/a.jpg", caption: "Atlas" },
|
|
227
|
+
{ id: 2, src: "/b.jpg", caption: "Borealis" },
|
|
228
|
+
];
|
|
229
|
+
|
|
230
|
+
<Carousel
|
|
231
|
+
items={slides}
|
|
232
|
+
renderItem={(s) => <img src={s.src} alt={s.caption} />}
|
|
233
|
+
/>;
|
|
234
|
+
```
|
|
235
|
+
|
|
236
|
+
#### `EmptyComponent`
|
|
237
|
+
|
|
238
|
+
Friendly empty state with illustration, title, and description.
|
|
239
|
+
|
|
240
|
+
```tsx
|
|
241
|
+
import { EmptyComponent } from "@andreyfedkovich/cozy-ui";
|
|
242
|
+
|
|
243
|
+
<EmptyComponent title="Nothing here yet" description="Create your first item to get started." />;
|
|
244
|
+
```
|
|
245
|
+
|
|
246
|
+
#### `Spinner`
|
|
247
|
+
|
|
248
|
+
Loading indicator with sizes `extraSmall | small | medium | large`.
|
|
249
|
+
|
|
250
|
+
```tsx
|
|
251
|
+
import { Spinner } from "@andreyfedkovich/cozy-ui";
|
|
252
|
+
|
|
253
|
+
<Spinner size="medium" />;
|
|
254
|
+
```
|
|
255
|
+
|
|
256
|
+
---
|
|
257
|
+
|
|
258
|
+
### Inputs & forms
|
|
259
|
+
|
|
260
|
+
#### `Button`
|
|
261
|
+
|
|
262
|
+
| Prop | Type | Default | Description |
|
|
263
|
+
| --------- | ----------------------------------------------------------------------- | ----------- | ------------------------ |
|
|
264
|
+
| `variant` | `"default" \| "primary" \| "secondary" \| "text" \| "link" \| "danger"` | `"default"` | Visual style. |
|
|
265
|
+
| `size` | `"small" \| "medium" \| "large"` | `"medium"` | Control size. |
|
|
266
|
+
| `loading` | `boolean` | `false` | Shows inline spinner. |
|
|
267
|
+
| `disabled`| `boolean` | `false` | Disabled state. |
|
|
268
|
+
| `...rest` | `ButtonHTMLAttributes<HTMLButtonElement>` | — | All native button props. |
|
|
269
|
+
|
|
270
|
+
```tsx
|
|
271
|
+
import { Button } from "@andreyfedkovich/cozy-ui";
|
|
272
|
+
|
|
273
|
+
<Button variant="primary" size="large" loading>
|
|
274
|
+
Saving…
|
|
275
|
+
</Button>;
|
|
276
|
+
```
|
|
277
|
+
|
|
278
|
+
#### `RadioGroupButton`
|
|
279
|
+
|
|
280
|
+
Segmented radio group, generic over its option value.
|
|
281
|
+
|
|
282
|
+
```tsx
|
|
283
|
+
import { RadioGroupButton } from "@andreyfedkovich/cozy-ui";
|
|
284
|
+
import { useState } from "react";
|
|
285
|
+
|
|
286
|
+
const [view, setView] = useState<"grid" | "list">("grid");
|
|
287
|
+
|
|
288
|
+
<RadioGroupButton
|
|
289
|
+
value={view}
|
|
290
|
+
onChange={setView}
|
|
291
|
+
options={[
|
|
292
|
+
{ value: "grid", label: "Grid" },
|
|
293
|
+
{ value: "list", label: "List" },
|
|
294
|
+
]}
|
|
295
|
+
/>;
|
|
296
|
+
```
|
|
297
|
+
|
|
298
|
+
#### `Select`
|
|
299
|
+
|
|
300
|
+
Powerful, virtualized-friendly select with `single` and `multiple` modes, search, custom rendering, and table layout.
|
|
301
|
+
|
|
302
|
+
| Prop | Type | Default | Description |
|
|
303
|
+
| ------------- | ------------------------------------- | ---------- | ------------------------------------ |
|
|
304
|
+
| `mode` | `"single" \| "multiple"` | — | Selection mode. |
|
|
305
|
+
| `value` | `CustomOption \| CustomOption[]` | — | Current value. |
|
|
306
|
+
| `options` | `CustomOption[]` | — | Available options. |
|
|
307
|
+
| `onChange` | `(option) => void` | — | Selection callback. |
|
|
308
|
+
| `onSearch` | `(value: string) => void` | — | Async search hook. |
|
|
309
|
+
| `template` | `"list" \| "table"` | `"list"` | Dropdown layout. |
|
|
310
|
+
| `columns` | `SelectColumn[]` | — | Required when `template="table"`. |
|
|
311
|
+
| `isLoading` | `boolean` | `false` | Show loading state in dropdown. |
|
|
312
|
+
| `error` | `string \| null` | — | Validation message. |
|
|
313
|
+
| `label` | `ReactNode` | — | Field label. |
|
|
314
|
+
|
|
315
|
+
```tsx
|
|
316
|
+
import { Select, type CustomOption } from "@andreyfedkovich/cozy-ui";
|
|
317
|
+
import { useState } from "react";
|
|
318
|
+
|
|
319
|
+
const options: CustomOption<unknown, string>[] = [
|
|
320
|
+
{ value: "design", label: "Design" },
|
|
321
|
+
{ value: "engineering", label: "Engineering" },
|
|
322
|
+
];
|
|
323
|
+
|
|
324
|
+
const [value, setValue] = useState<CustomOption<unknown, string> | null>(null);
|
|
325
|
+
|
|
326
|
+
<Select
|
|
327
|
+
mode="single"
|
|
328
|
+
label="Department"
|
|
329
|
+
placeholder="Pick one"
|
|
330
|
+
value={value}
|
|
331
|
+
options={options}
|
|
332
|
+
onChange={setValue}
|
|
333
|
+
/>;
|
|
334
|
+
```
|
|
335
|
+
|
|
336
|
+
#### `DialogSelect`
|
|
337
|
+
|
|
338
|
+
Dialog-based picker for large datasets — search + paginated loading + multi-select.
|
|
339
|
+
|
|
340
|
+
```tsx
|
|
341
|
+
import { DialogSelect } from "@andreyfedkovich/cozy-ui";
|
|
342
|
+
|
|
343
|
+
<DialogSelect
|
|
344
|
+
title="Add reviewer"
|
|
345
|
+
placeholder="Choose a person"
|
|
346
|
+
loadOptions={async ({ search, page, pageSize }) => {
|
|
347
|
+
const res = await fetch(`/api/people?q=${search}&page=${page}&size=${pageSize}`);
|
|
348
|
+
const { items, total } = await res.json();
|
|
349
|
+
return { options: items.map((p) => ({ value: p.id, label: p.name })), total };
|
|
350
|
+
}}
|
|
351
|
+
onSelect={(opt) => console.log(opt)}
|
|
352
|
+
/>;
|
|
353
|
+
```
|
|
354
|
+
|
|
355
|
+
#### `TreeDialogSelect`
|
|
356
|
+
|
|
357
|
+
Hierarchical picker with lazy-loaded branches and search.
|
|
358
|
+
|
|
359
|
+
```tsx
|
|
360
|
+
import { TreeDialogSelect } from "@andreyfedkovich/cozy-ui";
|
|
361
|
+
|
|
362
|
+
<TreeDialogSelect
|
|
363
|
+
title="Pick a department"
|
|
364
|
+
loadNodes={async ({ parentId }) => ({ nodes: await fetchChildren(parentId) })}
|
|
365
|
+
searchNodes={async ({ search }) => ({ nodes: await searchTree(search) })}
|
|
366
|
+
onSelect={(node) => console.log(node)}
|
|
367
|
+
/>;
|
|
368
|
+
```
|
|
369
|
+
|
|
370
|
+
#### `InputCaption`
|
|
371
|
+
|
|
372
|
+
Small caption row under an input — supports neutral, error, and success tones.
|
|
373
|
+
|
|
374
|
+
```tsx
|
|
375
|
+
import { InputCaption } from "@andreyfedkovich/cozy-ui";
|
|
376
|
+
|
|
377
|
+
<InputCaption type="error">Email is required.</InputCaption>;
|
|
378
|
+
```
|
|
379
|
+
|
|
380
|
+
#### `Label`
|
|
381
|
+
|
|
382
|
+
Accessible label, pairs with any input via `htmlFor`.
|
|
383
|
+
|
|
384
|
+
```tsx
|
|
385
|
+
import { Label } from "@andreyfedkovich/cozy-ui";
|
|
386
|
+
|
|
387
|
+
<Label htmlFor="email">Email</Label>;
|
|
388
|
+
```
|
|
389
|
+
|
|
390
|
+
---
|
|
391
|
+
|
|
392
|
+
### Navigation
|
|
393
|
+
|
|
394
|
+
#### `Tabs`
|
|
395
|
+
|
|
396
|
+
Classic underlined tabs.
|
|
397
|
+
|
|
398
|
+
```tsx
|
|
399
|
+
import { Tabs } from "@andreyfedkovich/cozy-ui";
|
|
400
|
+
import { useState } from "react";
|
|
401
|
+
|
|
402
|
+
const [tab, setTab] = useState("overview");
|
|
403
|
+
|
|
404
|
+
<Tabs
|
|
405
|
+
value={tab}
|
|
406
|
+
onChange={setTab}
|
|
407
|
+
items={[
|
|
408
|
+
{ value: "overview", label: "Overview" },
|
|
409
|
+
{ value: "activity", label: "Activity" },
|
|
410
|
+
]}
|
|
411
|
+
/>;
|
|
412
|
+
```
|
|
413
|
+
|
|
414
|
+
#### `TabsRounded`
|
|
415
|
+
|
|
416
|
+
Pill-shaped variant — great for filter bars.
|
|
417
|
+
|
|
418
|
+
```tsx
|
|
419
|
+
import { TabsRounded } from "@andreyfedkovich/cozy-ui";
|
|
420
|
+
|
|
421
|
+
<TabsRounded
|
|
422
|
+
value="all"
|
|
423
|
+
onChange={(v) => console.log(v)}
|
|
424
|
+
items={[
|
|
425
|
+
{ value: "all", label: "All" },
|
|
426
|
+
{ value: "open", label: "Open" },
|
|
427
|
+
{ value: "closed", label: "Closed" },
|
|
428
|
+
]}
|
|
429
|
+
/>;
|
|
430
|
+
```
|
|
431
|
+
|
|
432
|
+
#### `Stepper`
|
|
433
|
+
|
|
434
|
+
Linear, numbered progress for multi-step flows.
|
|
435
|
+
|
|
436
|
+
| Prop | Type | Default | Description |
|
|
437
|
+
| --------- | ---------------- | ------- | --------------------------------------------- |
|
|
438
|
+
| `items` | `StepperItem[]` | — | Step definitions. |
|
|
439
|
+
| `current` | `number` | `0` | Index of the active step. |
|
|
440
|
+
| `onStepClick` | `(index) => void` | — | Optional click handler for completed steps. |
|
|
441
|
+
|
|
442
|
+
```tsx
|
|
443
|
+
import { Stepper } from "@andreyfedkovich/cozy-ui";
|
|
444
|
+
|
|
445
|
+
<Stepper
|
|
446
|
+
current={1}
|
|
447
|
+
items={[
|
|
448
|
+
{ title: "Account" },
|
|
449
|
+
{ title: "Profile" },
|
|
450
|
+
{ title: "Review" },
|
|
451
|
+
]}
|
|
452
|
+
/>;
|
|
453
|
+
```
|
|
454
|
+
|
|
455
|
+
---
|
|
456
|
+
|
|
457
|
+
### Overlays
|
|
458
|
+
|
|
459
|
+
#### `Popover`
|
|
460
|
+
|
|
461
|
+
Floating panel anchored to a trigger element. Positioning powered by `@floating-ui/react`.
|
|
462
|
+
|
|
463
|
+
```tsx
|
|
464
|
+
import { Popover, Button } from "@andreyfedkovich/cozy-ui";
|
|
465
|
+
|
|
466
|
+
<Popover trigger={<Button>Open</Button>} placement="bottom-start">
|
|
467
|
+
<div style={{ padding: 12 }}>Anchored content</div>
|
|
468
|
+
</Popover>;
|
|
469
|
+
```
|
|
470
|
+
|
|
471
|
+
#### `TooltipDark` / `TooltipLight`
|
|
472
|
+
|
|
473
|
+
Two tonal variants of the same tooltip primitive.
|
|
474
|
+
|
|
475
|
+
| Prop | Type | Default | Description |
|
|
476
|
+
| ----------- | ----------------------------------- | ----------- | -------------------------- |
|
|
477
|
+
| `content` | `ReactNode` | — | Tooltip body. |
|
|
478
|
+
| `placement` | `TooltipPlacement` | `"top"` | Floating placement. |
|
|
479
|
+
| `trigger` | `"hover" \| "click"` | `"hover"` | Activation trigger. |
|
|
480
|
+
| `children` | `ReactNode` | — | The anchor element. |
|
|
481
|
+
|
|
482
|
+
```tsx
|
|
483
|
+
import { TooltipDark } from "@andreyfedkovich/cozy-ui";
|
|
484
|
+
|
|
485
|
+
<TooltipDark content="Copy to clipboard" placement="top">
|
|
486
|
+
<button aria-label="copy">⧉</button>
|
|
487
|
+
</TooltipDark>;
|
|
488
|
+
```
|
|
489
|
+
|
|
490
|
+
---
|
|
491
|
+
|
|
492
|
+
### Utility
|
|
493
|
+
|
|
494
|
+
#### `Tag`
|
|
495
|
+
|
|
496
|
+
Compact label for status, categories, counts.
|
|
497
|
+
|
|
498
|
+
```tsx
|
|
499
|
+
import { Tag } from "@andreyfedkovich/cozy-ui";
|
|
500
|
+
|
|
501
|
+
<Tag isSmall onClick={() => {}}>Beta</Tag>;
|
|
502
|
+
```
|
|
503
|
+
|
|
504
|
+
#### `CopyTextTrigger`
|
|
505
|
+
|
|
506
|
+
Wraps any element to copy a string to clipboard, with built-in feedback.
|
|
507
|
+
|
|
508
|
+
```tsx
|
|
509
|
+
import { CopyTextTrigger } from "@andreyfedkovich/cozy-ui";
|
|
510
|
+
|
|
511
|
+
<CopyTextTrigger text="cozy-ui">
|
|
512
|
+
<button>Copy package name</button>
|
|
513
|
+
</CopyTextTrigger>;
|
|
514
|
+
```
|
|
515
|
+
|
|
516
|
+
---
|
|
517
|
+
|
|
518
|
+
### Workflow
|
|
519
|
+
|
|
520
|
+
#### `ApprovalRoute`
|
|
521
|
+
|
|
522
|
+
The flagship workflow component. Renders a premium vertical timeline of **levels → stages → approvers** with statuses, rejection reasons, current-level highlight, empty-approver hints, and an optional editing mode.
|
|
523
|
+
|
|
524
|
+
| Prop | Type | Default | Description |
|
|
525
|
+
| ----------------- | ------------------------------------------------- | ------- | -------------------------------------------------------- |
|
|
526
|
+
| `levels` | `ApprovalLevel[]` | — | Sequential levels; each contains parallel `stages`. |
|
|
527
|
+
| `editable` | `boolean` | `false` | Enables add/remove controls. |
|
|
528
|
+
| `title` | `string` | — | Header title. |
|
|
529
|
+
| `eyebrow` | `string` | — | Small label above the title. |
|
|
530
|
+
| `loadApprovers` | `(params) => Promise<{ options, total? }>` | — | Async source for the "add approver" dialog. |
|
|
531
|
+
| `onAddLevel` | `(name) => void` | — | Edit callback. |
|
|
532
|
+
| `onRemoveLevel` | `(levelId) => void` | — | Edit callback. |
|
|
533
|
+
| `onAddStage` | `(levelId, name) => void` | — | Edit callback. |
|
|
534
|
+
| `onRemoveStage` | `(levelId, stageId) => void` | — | Edit callback. |
|
|
535
|
+
| `onAddApprover` | `(levelId, stageId, person) => void` | — | Edit callback. |
|
|
536
|
+
| `onRemoveApprover`| `(levelId, stageId, approverId) => void` | — | Edit callback. |
|
|
537
|
+
|
|
538
|
+
View mode — covers the three approver states (rejected, current, pending):
|
|
539
|
+
|
|
540
|
+
```tsx
|
|
541
|
+
import { ApprovalRoute, type ApprovalLevel } from "@andreyfedkovich/cozy-ui";
|
|
542
|
+
|
|
543
|
+
const levels: ApprovalLevel[] = [
|
|
544
|
+
{
|
|
545
|
+
id: "l1",
|
|
546
|
+
name: "Manager review",
|
|
547
|
+
status: "completed",
|
|
548
|
+
stages: [{
|
|
549
|
+
id: "s1", name: "Direct manager",
|
|
550
|
+
approvers: [{ id: "u1", fullName: "A. Ivanova", status: "approved", actedAt: "2026-04-28" }],
|
|
551
|
+
}],
|
|
552
|
+
},
|
|
553
|
+
{
|
|
554
|
+
id: "l2",
|
|
555
|
+
name: "Finance",
|
|
556
|
+
status: "current",
|
|
557
|
+
stages: [
|
|
558
|
+
{ id: "s2", name: "Budget owner",
|
|
559
|
+
approvers: [{ id: "u2", fullName: "M. Petrov", status: "pending" }] },
|
|
560
|
+
{ id: "s3", name: "Controller",
|
|
561
|
+
approvers: [{ id: "u3", fullName: "S. Orlov", status: "rejected", actedAt: "2026-05-01", rejectReason: "Out of budget" }] },
|
|
562
|
+
],
|
|
563
|
+
},
|
|
564
|
+
{
|
|
565
|
+
id: "l3", name: "Director sign-off", status: "pending",
|
|
566
|
+
stages: [{ id: "s4", name: "Director", approvers: [] }], // empty → "approver not assigned"
|
|
567
|
+
},
|
|
568
|
+
];
|
|
569
|
+
|
|
570
|
+
<ApprovalRoute title="Purchase request #4821" eyebrow="Approval" levels={levels} />;
|
|
571
|
+
```
|
|
572
|
+
|
|
573
|
+
Edit mode:
|
|
574
|
+
|
|
575
|
+
```tsx
|
|
576
|
+
<ApprovalRoute
|
|
577
|
+
title="Route editor"
|
|
578
|
+
editable
|
|
579
|
+
levels={levels}
|
|
580
|
+
loadApprovers={async ({ search, page, pageSize }) => {
|
|
581
|
+
const res = await fetch(`/api/people?q=${search}&page=${page}&size=${pageSize}`);
|
|
582
|
+
const { items, total } = await res.json();
|
|
583
|
+
return { options: items.map((p) => ({ value: p.id, label: p.fullName })), total };
|
|
584
|
+
}}
|
|
585
|
+
onAddLevel={(name) => /* ... */ undefined}
|
|
586
|
+
onAddStage={(levelId, name) => /* ... */ undefined}
|
|
587
|
+
onAddApprover={(levelId, stageId, person) => /* ... */ undefined}
|
|
588
|
+
onRemoveApprover={(levelId, stageId, approverId) => /* ... */ undefined}
|
|
589
|
+
/>;
|
|
590
|
+
```
|
|
591
|
+
|
|
592
|
+
---
|
|
593
|
+
|
|
594
|
+
## Hooks & helpers
|
|
595
|
+
|
|
596
|
+
### `useMeasureElement`
|
|
597
|
+
|
|
598
|
+
Tracks the size of a DOM element via `ResizeObserver`.
|
|
599
|
+
|
|
600
|
+
```ts
|
|
601
|
+
import { useMeasureElement } from "@andreyfedkovich/cozy-ui";
|
|
602
|
+
|
|
603
|
+
const { ref, width, height } = useMeasureElement<HTMLDivElement>();
|
|
604
|
+
|
|
605
|
+
<div ref={ref}>{width} × {height}</div>;
|
|
606
|
+
```
|
|
607
|
+
|
|
608
|
+
### `useDropdownPosition`
|
|
609
|
+
|
|
610
|
+
Calculates a flip-aware dropdown position relative to a trigger. Used internally by `Select`.
|
|
611
|
+
|
|
612
|
+
---
|
|
613
|
+
|
|
614
|
+
## Icons
|
|
615
|
+
|
|
616
|
+
The library ships its SVG icon set as React components. Tree-shaken, currentColor-aware.
|
|
617
|
+
|
|
618
|
+
```ts
|
|
619
|
+
import { DoneIcon, WarnIcon, CrossIcon, SearchIcon, ArrowDownIcon } from "@andreyfedkovich/cozy-ui";
|
|
620
|
+
```
|
|
621
|
+
|
|
622
|
+
Available icons include: `ArrowDownIcon`, `ArrowRightIcon`, `CameraIcon`, `CancelIcon`, `ChartIcon`, `ChatIcon`, `CheckGreenIcon`, `ClockIcon`, `CloseRedIcon`, `CopyIcon`, `CrossIcon`, `DoneIcon`, `DownloadIcon`, `EditIcon`, `EmptyIcon`, `EnvelopIcon`, `FeedbackIcon`, `FilterIcon`, `GridIcon`, `HeartIcon`, `HelpIcon`, `HomeIcon`, `InfoIcon`, `ListIcon`, `MarketIcon`, `MessageIcon`, `PhoneIcon`, `PlaneIcon`, `ProfileIcon`, `ReloadIcon`, `SearchIcon`, `SettingsIcon`, `WalletIcon`, `WarnIcon`, and more.
|
|
623
|
+
|
|
624
|
+
---
|
|
625
|
+
|
|
626
|
+
## TypeScript
|
|
627
|
+
|
|
628
|
+
Cozy UI is written in TypeScript and ships `.d.ts` declarations. All public types are re-exported from the package root:
|
|
629
|
+
|
|
630
|
+
```ts
|
|
631
|
+
import type {
|
|
632
|
+
ButtonVariant, ButtonSize,
|
|
633
|
+
CustomOption, SelectColumn,
|
|
634
|
+
DialogSelectProps, DialogSelectColumn,
|
|
635
|
+
TreeDialogSelectProps, TreeNode,
|
|
636
|
+
StepperItem, StepperProps,
|
|
637
|
+
TooltipProps, TooltipPlacement, TooltipTrigger,
|
|
638
|
+
CarouselProps,
|
|
639
|
+
CopyTextTriggerProps,
|
|
640
|
+
ApprovalRouteProps, ApprovalLevel, ApprovalStage, Approver, ApprovalStatus,
|
|
641
|
+
} from "@andreyfedkovich/cozy-ui";
|
|
642
|
+
```
|
|
643
|
+
|
|
644
|
+
---
|
|
645
|
+
|
|
646
|
+
## SSR & framework support
|
|
647
|
+
|
|
648
|
+
Cozy UI runs in **Next.js (App / Pages router)**, **TanStack Start**, **Remix**, and any **Vite SPA**.
|
|
649
|
+
|
|
650
|
+
Components that use portals — `Select`, `DialogSelect`, `TreeDialogSelect`, `Popover`, `TooltipDark`, `TooltipLight` — render on the client. In Next.js App Router, mark consuming files with `"use client"` (or import them through a client boundary). In TanStack Start they work out of the box inside route components.
|
|
651
|
+
|
|
652
|
+
---
|
|
653
|
+
|
|
654
|
+
## Theming
|
|
655
|
+
|
|
656
|
+
Override CSS variables in your global stylesheet, after Cozy UI's import:
|
|
657
|
+
|
|
658
|
+
```css
|
|
659
|
+
@import "@andreyfedkovich/cozy-ui/styles.css";
|
|
660
|
+
|
|
661
|
+
:root {
|
|
662
|
+
--cozy-blue-03: #2563eb;
|
|
663
|
+
--cozy-radius-md: 14px;
|
|
664
|
+
--cozy-shadow-raised: 0 12px 32px -12px rgb(15 23 42 / 0.18);
|
|
665
|
+
}
|
|
666
|
+
```
|
|
667
|
+
|
|
668
|
+
For per-component overrides, every component accepts a `className` prop and uses CSS modules — your class wins over module hashes thanks to a single trailing `className` slot.
|
|
669
|
+
|
|
670
|
+
---
|
|
671
|
+
|
|
672
|
+
## Accessibility
|
|
673
|
+
|
|
674
|
+
- Dialogs (`DialogSelect`, `TreeDialogSelect`, internal name dialogs) are built on **Radix Dialog** — focus trap, ESC to close, scroll lock.
|
|
675
|
+
- `Label` is built on **Radix Label** with proper `for`/`id` association.
|
|
676
|
+
- `Stepper` and `Tabs` are keyboard navigable.
|
|
677
|
+
- Focus rings respect `:focus-visible`, never blanket-suppressed.
|
|
678
|
+
- Color tokens meet WCAG AA contrast for text-on-surface combinations.
|
|
679
|
+
|
|
680
|
+
---
|
|
681
|
+
|
|
682
|
+
## Local development
|
|
683
|
+
|
|
684
|
+
```bash
|
|
685
|
+
bun install
|
|
686
|
+
bun run dev # demo playground at http://localhost:5173
|
|
687
|
+
bun run build:lib # produce dist/ (ESM + CJS + .d.ts + styles.css)
|
|
688
|
+
bun run lint
|
|
689
|
+
bun run format
|
|
690
|
+
```
|
|
691
|
+
|
|
692
|
+
The demo playground (`src/routes/index.tsx`) showcases every exported component and is the easiest place to iterate on a new variant.
|
|
693
|
+
|
|
694
|
+
---
|
|
695
|
+
|
|
696
|
+
## Publishing
|
|
697
|
+
|
|
698
|
+
```bash
|
|
699
|
+
npm publish --access public
|
|
700
|
+
```
|
|
701
|
+
|
|
702
|
+
`prepublishOnly` runs `build:lib` automatically, so you publish exactly what's in `dist/`. Bump the version in `package.json` (semver) before each release.
|
|
703
|
+
|
|
704
|
+
---
|
|
705
|
+
|
|
706
|
+
## Contributing
|
|
707
|
+
|
|
708
|
+
PRs are welcome. Please:
|
|
709
|
+
|
|
710
|
+
1. Run `bun run lint && bun run format` before pushing.
|
|
711
|
+
2. Add the new component to `src/lib/components/index.ts` and demo it in `src/routes/index.tsx`.
|
|
712
|
+
3. Document any new prop in this README.
|
|
713
|
+
|
|
714
|
+
---
|
|
715
|
+
|
|
716
|
+
## License
|
|
717
|
+
|
|
718
|
+
MIT © Andrey Fedkovich
|