@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 +739 -0
- package/dist/index.cjs +1534 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +342 -0
- package/dist/index.d.ts +342 -0
- package/dist/index.js +1458 -0
- package/dist/index.js.map +1 -0
- package/package.json +95 -0
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.
|