@assassin1717/aifelib 1.0.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,739 @@
1
+ # @assassin1717/aifelib
2
+
3
+ Private UI component library — React + TypeScript + Tailwind CSS + Lucide Icons.
4
+
5
+ Built to standardize frontend across all internal apps: consistent visuals, mobile-first, accessible, and easy for AI agents to use.
6
+
7
+ ---
8
+
9
+ ## Install
10
+
11
+ ```bash
12
+ npm install @assassin1717/aifelib
13
+ ```
14
+
15
+ Peer dependencies (must be installed in the consuming app):
16
+
17
+ ```bash
18
+ npm install react react-dom
19
+ ```
20
+
21
+ Tailwind CSS must be configured in the consuming app. Add the library path to your `content` array so Tailwind scans its classes:
22
+
23
+ ```js
24
+ // tailwind.config.js / tailwind.config.ts
25
+ content: [
26
+ "./src/**/*.{ts,tsx}",
27
+ "./node_modules/@assassin1717/aifelib/dist/**/*.js",
28
+ ]
29
+ ```
30
+
31
+ ---
32
+
33
+ ## Setup
34
+
35
+ Wrap your app with `ToastProvider` (required for `useToast` to work):
36
+
37
+ ```tsx
38
+ import { ToastProvider } from "@assassin1717/aifelib";
39
+
40
+ function App() {
41
+ return (
42
+ <ToastProvider>
43
+ <YourApp />
44
+ </ToastProvider>
45
+ );
46
+ }
47
+ ```
48
+
49
+ ---
50
+
51
+ ## Components
52
+
53
+ ### Phase 1 — Form
54
+
55
+ #### Button
56
+
57
+ ```tsx
58
+ import { Button } from "@assassin1717/aifelib";
59
+
60
+ <Button>Save</Button>
61
+ <Button variant="destructive" size="lg">Delete</Button>
62
+ <Button loading>Saving…</Button>
63
+ <Button fullWidth>Submit</Button>
64
+ ```
65
+
66
+ Props:
67
+ - `variant`: `"primary"` | `"secondary"` | `"ghost"` | `"destructive"` | `"outline"` — default `"primary"`
68
+ - `size`: `"sm"` | `"md"` | `"lg"` — default `"md"`
69
+ - `loading`: `boolean` — shows spinner, disables button
70
+ - `fullWidth`: `boolean` — `w-full`
71
+ - All native `<button>` props
72
+
73
+ #### Input
74
+
75
+ ```tsx
76
+ import { Input } from "@assassin1717/aifelib";
77
+ import { Search } from "lucide-react";
78
+
79
+ <Input placeholder="Email" type="email" />
80
+ <Input error placeholder="Invalid value" />
81
+ <Input startIcon={<Search size={16} />} placeholder="Search…" />
82
+ ```
83
+
84
+ Props:
85
+ - `error`: `boolean` — red border + `aria-invalid`
86
+ - `startIcon`: `ReactNode` — icon on the left
87
+ - `endIcon`: `ReactNode` — icon on the right
88
+ - All native `<input>` props
89
+
90
+ #### Textarea
91
+
92
+ ```tsx
93
+ import { Textarea } from "@assassin1717/aifelib";
94
+
95
+ <Textarea placeholder="Description" rows={4} />
96
+ <Textarea error />
97
+ ```
98
+
99
+ Props:
100
+ - `error`: `boolean`
101
+ - All native `<textarea>` props
102
+
103
+ #### Select
104
+
105
+ ```tsx
106
+ import { Select } from "@assassin1717/aifelib";
107
+
108
+ <Select
109
+ options={[
110
+ { value: "admin", label: "Admin" },
111
+ { value: "user", label: "User" },
112
+ ]}
113
+ placeholder="Choose role…"
114
+ />
115
+ ```
116
+
117
+ Props:
118
+ - `options`: `{ value: string; label: string; disabled?: boolean }[]`
119
+ - `placeholder`: `string` — disabled first option
120
+ - `error`: `boolean`
121
+ - All native `<select>` props
122
+
123
+ #### Checkbox
124
+
125
+ ```tsx
126
+ import { Checkbox } from "@assassin1717/aifelib";
127
+
128
+ <Checkbox label="Accept terms" />
129
+ <Checkbox label="Required" error />
130
+ ```
131
+
132
+ Props:
133
+ - `label`: `string` — inline label (renders its own `<label>`)
134
+ - `error`: `boolean`
135
+ - All native `<input type="checkbox">` props except `type`
136
+
137
+ #### Label
138
+
139
+ ```tsx
140
+ import { Label } from "@assassin1717/aifelib";
141
+
142
+ <Label htmlFor="email" required>Email</Label>
143
+ ```
144
+
145
+ Props:
146
+ - `required`: `boolean` — appends a red `*`
147
+ - All native `<label>` props
148
+
149
+ #### FormField
150
+
151
+ Composes Label + any input child. Automatically injects `id`, `error`, and `aria-describedby` into the child.
152
+
153
+ ```tsx
154
+ import { FormField, Input } from "@assassin1717/aifelib";
155
+
156
+ <FormField label="Email" htmlFor="email" required error="Invalid email">
157
+ <Input id="email" type="email" />
158
+ </FormField>
159
+
160
+ <FormField label="Bio" htmlFor="bio" hint="Max 200 characters">
161
+ <Textarea id="bio" />
162
+ </FormField>
163
+ ```
164
+
165
+ Props:
166
+ - `label`: `string`
167
+ - `htmlFor`: `string` — links label to input
168
+ - `required`: `boolean`
169
+ - `hint`: `string` — helper text shown below input (hidden when error is set)
170
+ - `error`: `string` — error message shown below input with `role="alert"`
171
+
172
+ ---
173
+
174
+ ### Phase 2 — Feedback & Overlays
175
+
176
+ #### Spinner
177
+
178
+ ```tsx
179
+ import { Spinner } from "@assassin1717/aifelib";
180
+
181
+ <Spinner />
182
+ <Spinner size="lg" label="Loading users…" />
183
+ ```
184
+
185
+ Props:
186
+ - `size`: `"sm"` | `"md"` | `"lg"` — default `"md"`
187
+ - `label`: `string` — aria-label for screen readers — default `"Loading…"`
188
+
189
+ #### Badge
190
+
191
+ ```tsx
192
+ import { Badge } from "@assassin1717/aifelib";
193
+
194
+ <Badge>Default</Badge>
195
+ <Badge variant="success">Active</Badge>
196
+ <Badge variant="destructive">Error</Badge>
197
+ ```
198
+
199
+ Props:
200
+ - `variant`: `"default"` | `"success"` | `"warning"` | `"destructive"` | `"info"` | `"outline"` — default `"default"`
201
+ - All native `<span>` props
202
+
203
+ #### Alert
204
+
205
+ ```tsx
206
+ import { Alert } from "@assassin1717/aifelib";
207
+
208
+ <Alert variant="success" title="Saved" onDismiss={() => {}}>
209
+ Your changes have been saved.
210
+ </Alert>
211
+ ```
212
+
213
+ Props:
214
+ - `variant`: `"info"` | `"success"` | `"warning"` | `"destructive"` — default `"info"`
215
+ - `title`: `string`
216
+ - `onDismiss`: `() => void` — shows dismiss button
217
+ - `children`: message body
218
+
219
+ #### Card
220
+
221
+ ```tsx
222
+ import { Card, CardHeader, CardTitle, CardDescription, CardContent, CardFooter } from "@assassin1717/aifelib";
223
+
224
+ <Card>
225
+ <CardHeader>
226
+ <CardTitle>Users</CardTitle>
227
+ <CardDescription>Manage your team members.</CardDescription>
228
+ </CardHeader>
229
+ <CardContent>Content here</CardContent>
230
+ <CardFooter>
231
+ <Button>Save</Button>
232
+ </CardFooter>
233
+ </Card>
234
+ ```
235
+
236
+ `Card` props:
237
+ - `padding`: `"none"` | `"sm"` | `"md"` | `"lg"` — default `"md"`
238
+
239
+ #### Modal
240
+
241
+ ```tsx
242
+ import { Modal, Button } from "@assassin1717/aifelib";
243
+
244
+ const [open, setOpen] = useState(false);
245
+
246
+ <Modal
247
+ open={open}
248
+ onClose={() => setOpen(false)}
249
+ title="Edit user"
250
+ description="Update the user details below."
251
+ size="md"
252
+ footer={
253
+ <>
254
+ <Button variant="ghost" onClick={() => setOpen(false)}>Cancel</Button>
255
+ <Button onClick={handleSave}>Save</Button>
256
+ </>
257
+ }
258
+ >
259
+ <FormField label="Name" htmlFor="name">
260
+ <Input id="name" />
261
+ </FormField>
262
+ </Modal>
263
+ ```
264
+
265
+ Props:
266
+ - `open`: `boolean`
267
+ - `onClose`: `() => void` — called on backdrop click and ESC key
268
+ - `title`: `string`
269
+ - `description`: `string`
270
+ - `size`: `"sm"` | `"md"` | `"lg"` | `"xl"` | `"full"` — default `"md"`
271
+ - `footer`: `ReactNode` — action buttons slot (rendered inside the modal)
272
+ - `children`: modal body
273
+
274
+ Behaviour: focus trap, ESC to close, body scroll lock, bottom sheet on mobile / centered dialog on `sm+`.
275
+
276
+ #### ConfirmDialog
277
+
278
+ ```tsx
279
+ import { ConfirmDialog } from "@assassin1717/aifelib";
280
+
281
+ <ConfirmDialog
282
+ open={open}
283
+ onClose={() => setOpen(false)}
284
+ onConfirm={handleDelete}
285
+ title="Delete user"
286
+ description="This action cannot be undone."
287
+ variant="destructive"
288
+ confirmLabel="Delete"
289
+ loading={isDeleting}
290
+ />
291
+ ```
292
+
293
+ Props:
294
+ - `open`: `boolean`
295
+ - `onClose`: `() => void`
296
+ - `onConfirm`: `() => void`
297
+ - `title`: `string`
298
+ - `description`: `string`
299
+ - `variant`: `"primary"` | `"destructive"` — default `"primary"`
300
+ - `confirmLabel`: `string` — default `"Confirm"`
301
+ - `cancelLabel`: `string` — default `"Cancel"`
302
+ - `loading`: `boolean`
303
+
304
+ #### ToastMessage
305
+
306
+ ```tsx
307
+ import { useToast } from "@assassin1717/aifelib";
308
+
309
+ function MyComponent() {
310
+ const { addToast } = useToast();
311
+
312
+ return (
313
+ <Button onClick={() => addToast({ type: "success", title: "Saved", description: "Changes saved." })}>
314
+ Save
315
+ </Button>
316
+ );
317
+ }
318
+ ```
319
+
320
+ `addToast` options:
321
+ - `type`: `"success"` | `"error"` | `"warning"` | `"info"`
322
+ - `title`: `string`
323
+ - `description`: `string`
324
+ - `duration`: `number` — ms before auto-dismiss, default `4000`. Pass `0` to disable auto-dismiss.
325
+
326
+ Requires `<ToastProvider>` at the root of the app.
327
+
328
+ ---
329
+
330
+ ### Phase 3 — Data Display
331
+
332
+ #### Table
333
+
334
+ Mobile: each row renders as a card with `label: value` pairs. Desktop: standard table with horizontal scroll.
335
+
336
+ ```tsx
337
+ import {
338
+ Table, TableHeader, TableBody, TableRow,
339
+ TableHead, TableCell, TableToolbar, TableEmptyState
340
+ } from "@assassin1717/aifelib";
341
+
342
+ <TableToolbar
343
+ filters={<Input placeholder="Search…" />}
344
+ actions={<Button>Add user</Button>}
345
+ />
346
+
347
+ <Table>
348
+ <TableHeader>
349
+ <TableRow>
350
+ <TableHead>Name</TableHead>
351
+ <TableHead>Status</TableHead>
352
+ <TableHead align="right">Actions</TableHead>
353
+ </TableRow>
354
+ </TableHeader>
355
+ <TableBody>
356
+ {users.length === 0 ? (
357
+ <TableEmptyState colSpan={3} title="No users found" description="Add your first user." />
358
+ ) : (
359
+ users.map(user => (
360
+ <TableRow key={user.id}>
361
+ <TableCell label="Name">{user.name}</TableCell>
362
+ <TableCell label="Status"><Badge variant="success">Active</Badge></TableCell>
363
+ <TableCell label="Actions" align="right"><Button size="sm">Edit</Button></TableCell>
364
+ </TableRow>
365
+ ))
366
+ )}
367
+ </TableBody>
368
+ </Table>
369
+ ```
370
+
371
+ **Important:** Always pass `label` to `TableCell` — it's the column header shown on mobile cards.
372
+
373
+ `TableHead` / `TableCell` props:
374
+ - `align`: `"left"` | `"center"` | `"right"` — default `"left"`
375
+
376
+ `TableCell` extra props:
377
+ - `label`: `string` — column name shown on mobile
378
+
379
+ `TableEmptyState` props:
380
+ - `colSpan`: `number` — must match number of columns
381
+ - `title`: `string`
382
+ - `description`: `string`
383
+ - `icon`: `ReactNode`
384
+ - `action`: `ReactNode` — action button
385
+
386
+ `TableToolbar` props:
387
+ - `filters`: `ReactNode` — left slot (search, selects)
388
+ - `actions`: `ReactNode` — right slot (buttons)
389
+
390
+ #### PageHeader
391
+
392
+ ```tsx
393
+ import { PageHeader, Button } from "@assassin1717/aifelib";
394
+
395
+ <PageHeader
396
+ title="Users"
397
+ description="Manage your team members."
398
+ actions={
399
+ <>
400
+ <Button variant="outline">Export</Button>
401
+ <Button>Add user</Button>
402
+ </>
403
+ }
404
+ />
405
+ ```
406
+
407
+ Props:
408
+ - `title`: `string`
409
+ - `description`: `string`
410
+ - `prefix`: `ReactNode` — breadcrumb or back link
411
+ - `actions`: `ReactNode` — right slot, stacks on mobile
412
+
413
+ #### EmptyState
414
+
415
+ ```tsx
416
+ import { EmptyState, Button } from "@assassin1717/aifelib";
417
+ import { Users } from "lucide-react";
418
+
419
+ <EmptyState
420
+ icon={<Users size={48} />}
421
+ title="No users yet"
422
+ description="Add your first team member to get started."
423
+ action={<Button>Add user</Button>}
424
+ secondaryAction={<Button variant="ghost">Learn more</Button>}
425
+ />
426
+ ```
427
+
428
+ Props:
429
+ - `icon`: `ReactNode`
430
+ - `title`: `string`
431
+ - `description`: `string`
432
+ - `action`: `ReactNode` — primary button
433
+ - `secondaryAction`: `ReactNode` — secondary button
434
+
435
+ ---
436
+
437
+ ### Phase 4 — Navigation & Extras
438
+
439
+ #### Tabs
440
+
441
+ ```tsx
442
+ import { Tabs, TabPanel } from "@assassin1717/aifelib";
443
+
444
+ const [tab, setTab] = useState("overview");
445
+
446
+ <Tabs
447
+ tabs={[
448
+ { value: "overview", label: "Overview" },
449
+ { value: "members", label: "Members" },
450
+ { value: "settings", label: "Settings", disabled: true },
451
+ ]}
452
+ value={tab}
453
+ onChange={setTab}
454
+ >
455
+ <TabPanel value="overview" activeValue={tab}>Overview content</TabPanel>
456
+ <TabPanel value="members" activeValue={tab}>Members content</TabPanel>
457
+ </Tabs>
458
+ ```
459
+
460
+ `Tabs` props:
461
+ - `tabs`: `{ value: string; label: string; disabled?: boolean }[]`
462
+ - `value`: `string` — controlled active tab
463
+ - `onChange`: `(value: string) => void`
464
+
465
+ `TabPanel` props:
466
+ - `value`: `string` — this panel's tab value
467
+ - `activeValue`: `string` — currently active tab value
468
+
469
+ Behaviour: horizontal scroll when tabs overflow on mobile, arrow key navigation.
470
+
471
+ #### DropdownMenu
472
+
473
+ ```tsx
474
+ import { DropdownMenu, Button } from "@assassin1717/aifelib";
475
+ import { Edit, Trash2, MoreHorizontal } from "lucide-react";
476
+
477
+ <DropdownMenu
478
+ trigger={<Button variant="ghost" size="sm"><MoreHorizontal size={16} /></Button>}
479
+ align="right"
480
+ items={[
481
+ { label: "Edit", icon: <Edit size={14} />, onClick: handleEdit },
482
+ { label: "Delete", icon: <Trash2 size={14} />, destructive: true, divider: true, onClick: handleDelete },
483
+ ]}
484
+ />
485
+ ```
486
+
487
+ Props:
488
+ - `trigger`: `ReactNode` — the button that opens the menu
489
+ - `items`: `DropdownMenuItem[]`
490
+ - `align`: `"left"` | `"right"` — default `"right"`
491
+
492
+ `DropdownMenuItem`:
493
+ - `label`: `string`
494
+ - `onClick`: `() => void`
495
+ - `icon`: `ReactNode`
496
+ - `disabled`: `boolean`
497
+ - `destructive`: `boolean` — red styling
498
+ - `divider`: `boolean` — visual separator above item
499
+
500
+ Behaviour: closes on outside click, ESC, or item selection. Focus returns to trigger on ESC.
501
+
502
+ #### Pagination
503
+
504
+ ```tsx
505
+ import { Pagination } from "@assassin1717/aifelib";
506
+
507
+ const [page, setPage] = useState(1);
508
+
509
+ <Pagination page={page} totalPages={20} onChange={setPage} />
510
+ ```
511
+
512
+ Props:
513
+ - `page`: `number`
514
+ - `totalPages`: `number`
515
+ - `onChange`: `(page: number) => void`
516
+ - `siblingCount`: `number` — page buttons around current, default `1`
517
+
518
+ Mobile: shows `"X / Y"` compact label instead of page buttons. Returns `null` when `totalPages <= 1`.
519
+
520
+ #### Drawer
521
+
522
+ ```tsx
523
+ import { Drawer, Button } from "@assassin1717/aifelib";
524
+
525
+ <Drawer
526
+ open={open}
527
+ onClose={() => setOpen(false)}
528
+ title="Filters"
529
+ side="right"
530
+ footer={
531
+ <>
532
+ <Button variant="ghost" onClick={() => setOpen(false)}>Cancel</Button>
533
+ <Button onClick={applyFilters}>Apply</Button>
534
+ </>
535
+ }
536
+ >
537
+ <FormField label="Status" htmlFor="status">
538
+ <Select id="status" options={statusOptions} />
539
+ </FormField>
540
+ </Drawer>
541
+ ```
542
+
543
+ Props:
544
+ - `open`: `boolean`
545
+ - `onClose`: `() => void`
546
+ - `title`: `string`
547
+ - `description`: `string`
548
+ - `side`: `"right"` | `"left"` — default `"right"`
549
+ - `widthClass`: `string` — Tailwind width class for desktop, default `"sm:w-96"`
550
+ - `footer`: `ReactNode` — action buttons slot
551
+
552
+ Behaviour: same as Modal (focus trap, ESC, scroll lock). Bottom sheet on mobile, side panel on `sm+`.
553
+
554
+ #### Tooltip
555
+
556
+ ```tsx
557
+ import { Tooltip, Button } from "@assassin1717/aifelib";
558
+
559
+ <Tooltip content="Save your changes" side="top">
560
+ <Button>Save</Button>
561
+ </Tooltip>
562
+ ```
563
+
564
+ Props:
565
+ - `content`: `string`
566
+ - `side`: `"top"` | `"bottom"` | `"left"` | `"right"` — default `"top"`
567
+ - `children`: single `ReactElement` — must accept `onMouseEnter`, `onMouseLeave`, `onFocus`, `onBlur`
568
+
569
+ Behaviour: visible on hover and keyboard focus.
570
+
571
+ ---
572
+
573
+ ### Phase 5 — App Shell
574
+
575
+ #### AppShell
576
+
577
+ Full-page layout with fixed sidebar (desktop) + Drawer sidebar (mobile).
578
+
579
+ ```tsx
580
+ import { AppShell } from "@assassin1717/aifelib";
581
+ import { LayoutDashboard, Users, Settings } from "lucide-react";
582
+
583
+ <AppShell
584
+ sidebar={{
585
+ logo: <span className="font-bold text-blue-600">MyApp</span>,
586
+ groups: [
587
+ {
588
+ items: [
589
+ { label: "Dashboard", href: "/", icon: <LayoutDashboard size={18} />, active: true },
590
+ { label: "Users", href: "/users", icon: <Users size={18} /> },
591
+ ],
592
+ },
593
+ {
594
+ title: "Config",
595
+ items: [
596
+ { label: "Settings", href: "/settings", icon: <Settings size={18} /> },
597
+ ],
598
+ },
599
+ ],
600
+ footer: <div className="text-sm text-gray-500">v1.0.0</div>,
601
+ }}
602
+ topbar={{ title: "Dashboard" }}
603
+ >
604
+ <PageHeader title="Dashboard" />
605
+ {/* page content */}
606
+ </AppShell>
607
+ ```
608
+
609
+ `AppShell` props:
610
+ - `sidebar`: `SidebarProps` — see Sidebar below
611
+ - `topbar`: `TopbarProps` (without `onMenuOpen`) — omit to hide topbar entirely
612
+ - `children`: page content rendered in the scrollable main area
613
+
614
+ #### Sidebar
615
+
616
+ Can be used standalone (e.g. inside a custom layout).
617
+
618
+ ```tsx
619
+ import { Sidebar } from "@assassin1717/aifelib";
620
+
621
+ <Sidebar
622
+ logo={<img src="/logo.svg" alt="MyApp" className="h-8" />}
623
+ groups={[
624
+ {
625
+ items: [
626
+ { label: "Dashboard", href: "/", active: true },
627
+ { label: "Users", href: "/users", badge: 3 },
628
+ { label: "Archived", href: "/archived", disabled: true },
629
+ ],
630
+ },
631
+ ]}
632
+ footer={<Button variant="ghost" fullWidth>Logout</Button>}
633
+ />
634
+ ```
635
+
636
+ `SidebarNavItem`:
637
+ - `label`: `string`
638
+ - `href`: `string` — renders as `<a>`, omit to render as `<button>`
639
+ - `icon`: `ReactNode`
640
+ - `active`: `boolean` — highlights item, sets `aria-current="page"`
641
+ - `disabled`: `boolean`
642
+ - `onClick`: `() => void`
643
+ - `badge`: `string | number` — shown on the right
644
+
645
+ `SidebarNavGroup`:
646
+ - `title`: `string` — optional group heading
647
+ - `items`: `SidebarNavItem[]`
648
+
649
+ #### Topbar
650
+
651
+ ```tsx
652
+ import { Topbar } from "@assassin1717/aifelib";
653
+
654
+ <Topbar
655
+ onMenuOpen={() => setSidebarOpen(true)}
656
+ title="Users"
657
+ actions={<Avatar name="Tiago" />}
658
+ />
659
+ ```
660
+
661
+ Props:
662
+ - `onMenuOpen`: `() => void` — hamburger button handler (button hidden on `sm+`)
663
+ - `title`: `ReactNode`
664
+ - `actions`: `ReactNode` — right slot
665
+ - `hideMenuButton`: `boolean` — hides the hamburger
666
+
667
+ ---
668
+
669
+ ## Utilities
670
+
671
+ ### cn
672
+
673
+ Merges Tailwind classes safely (clsx + tailwind-merge).
674
+
675
+ ```tsx
676
+ import { cn } from "@assassin1717/aifelib";
677
+
678
+ <div className={cn("px-4 py-2", isActive && "bg-blue-50", className)} />
679
+ ```
680
+
681
+ ---
682
+
683
+ ## Development
684
+
685
+ ```bash
686
+ # Install dependencies
687
+ npm install
688
+
689
+ # Run Storybook (component explorer)
690
+ npm run dev
691
+
692
+ # Run tests
693
+ npm test
694
+ npm run test:watch
695
+
696
+ # Type check
697
+ npm run type-check
698
+
699
+ # Lint
700
+ npm run lint
701
+
702
+ # Build library
703
+ npm run build
704
+ ```
705
+
706
+ ---
707
+
708
+ ## Release process
709
+
710
+ 1. Work on a feature branch (`dev/<name>`)
711
+ 2. Merge to `staging` — pipeline runs lint + type-check + test + build
712
+ 3. Bump version in `package.json` (`npm version patch|minor|major`)
713
+ 4. Merge `staging` → `main` — pipeline publishes to npm automatically
714
+
715
+ ---
716
+
717
+ ## CI/CD
718
+
719
+ Bitbucket Pipelines (`bitbucket-pipelines.yml`):
720
+
721
+ | Branch | Steps |
722
+ |---|---|
723
+ | Any push | install → lint + type-check + test (parallel) → build |
724
+ | `staging` | Same as above |
725
+ | `main` | Above + publish to npm |
726
+
727
+ Required pipeline variable (set in Bitbucket repository Settings → Pipelines → Variables):
728
+ - `NPM_TOKEN` — npm access token with read+write on `@assassin1717` scope. Mark as **Secured**.
729
+
730
+ ---
731
+
732
+ ## Design decisions
733
+
734
+ - **No CSS-in-JS** — Tailwind only. No runtime style injection.
735
+ - **No headless UI library** — all components are hand-rolled to keep the bundle small and predictable.
736
+ - **Composition over configuration** — `FormField` injects props into children via `cloneElement`. `Modal`/`Drawer`/`ConfirmDialog` define their own action buttons internally — callers pass callbacks, not buttons.
737
+ - **Mobile-first** — touch targets minimum 44px, bottom sheets on mobile, horizontal scroll on tables replaced by card layout.
738
+ - **Accessibility** — `aria-invalid`, `aria-describedby`, `aria-current`, `role="alert"`, `role="status"`, focus traps in overlays, keyboard navigation in Tabs and DropdownMenu.
739
+ - **`exactOptionalPropertyTypes: true`** — optional props are spread conditionally, never passed as `undefined` explicitly.