@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 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
+ [![npm version](https://img.shields.io/npm/v/@andreyfedkovich/cozy-ui.svg?style=flat-square&color=0A84FF)](https://www.npmjs.com/package/@andreyfedkovich/cozy-ui)
10
+ [![bundle size](https://img.shields.io/bundlephobia/minzip/@andreyfedkovich/cozy-ui?style=flat-square&color=0A84FF)](https://bundlephobia.com/package/@andreyfedkovich/cozy-ui)
11
+ [![types included](https://img.shields.io/npm/types/@andreyfedkovich/cozy-ui?style=flat-square&color=0A84FF)](https://www.npmjs.com/package/@andreyfedkovich/cozy-ui)
12
+ [![license MIT](https://img.shields.io/npm/l/@andreyfedkovich/cozy-ui?style=flat-square&color=0A84FF)](./LICENSE)
13
+ [![react 18 / 19](https://img.shields.io/badge/react-18%20%7C%2019-0A84FF?style=flat-square)](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