ryzen-ui 0.1.4

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,1063 @@
1
+ # Ryzen UI
2
+
3
+ A lightweight, standalone Angular component library built with **signals**, **OnPush** change detection, **pure component-scoped CSS** with oklch color theming.
4
+
5
+ - **27 components** across Form, Data Display, Feedback, Navigation, and Overlay categories
6
+ - Zero runtime dependencies beyond `@angular/core` and `primeicons`
7
+ - No `@angular/animations` — CSS keyframes for all animations
8
+ - No CDK — manual document listeners and `position: fixed / absolute` for overlays
9
+ - Tree-shakeable, side-effect-free
10
+
11
+ ---
12
+
13
+ ## Installation
14
+
15
+ ```bash
16
+ pnpm add ryzen-ui primeicons
17
+ # or
18
+ npm install ryzen-ui primeicons
19
+ ```
20
+
21
+ ### Optional peer dependencies (for Table export)
22
+
23
+ ```bash
24
+ pnpm add exceljs jspdf html2canvas
25
+ # or
26
+ npm install exceljs jspdf html2canvas
27
+ ```
28
+
29
+ ---
30
+
31
+ ## Quick Start
32
+
33
+ ```typescript
34
+ import { Component } from '@angular/core';
35
+ import { ToastService } from 'ryzen-ui';
36
+ import { RzButtonComponent } from 'ryzen-ui';
37
+ // etc. — all components are standalone and tree-shakeable
38
+ ```
39
+
40
+ ### Global styles
41
+
42
+ ```css
43
+ /* styles.css */
44
+ @import 'primeicons/primeicons.css';
45
+
46
+ :root {
47
+ --color-primary: oklch(0.32 0.09 258);
48
+ --color-surface: oklch(0.99 0 0);
49
+ --color-border: oklch(0.83 0.015 260);
50
+ --color-text: oklch(0.14 0.01 260);
51
+ --color-text-muted: oklch(0.48 0.01 260);
52
+ --color-surface-alt: oklch(0.975 0.005 260);
53
+ --color-secondary: oklch(0.55 0.12 40);
54
+ --color-accent: oklch(0.64 0.2 50);
55
+ --color-success: oklch(0.55 0.18 145);
56
+ --color-warning: oklch(0.68 0.18 75);
57
+ --color-danger: oklch(0.55 0.22 25);
58
+ --color-info: oklch(0.55 0.15 235);
59
+ --radius-md: 0.5rem;
60
+ --font-sans: ui-sans-serif, system-ui, sans-serif;
61
+ }
62
+
63
+ .dark {
64
+ --color-primary: oklch(0.65 0.15 258);
65
+ --color-surface: oklch(0.18 0.01 260);
66
+ --color-border: oklch(0.3 0.01 260);
67
+ --color-text: oklch(0.95 0.005 260);
68
+ --color-text-muted: oklch(0.65 0.01 260);
69
+ --color-surface-alt: oklch(0.22 0.01 260);
70
+ }
71
+ ```
72
+
73
+ ---
74
+
75
+ ## Theming
76
+
77
+ All components inherit colors through CSS custom properties. Each component's `:host` maps theme variables into internal `--rz-*` variables with oklch fallbacks, so components work **without any theme setup**:
78
+
79
+ | Variable | Default | Description |
80
+ |----------|---------|-------------|
81
+ | `--rz-accent` | `oklch(0.32 0.09 258)` | Accent/highlight color |
82
+ | `--rz-bg` | `oklch(0.99 0 0)` | Input/component background |
83
+ | `--rz-border` | `oklch(0.83 0.015 260)` | Border color |
84
+ | `--rz-text` | `oklch(0.14 0.01 260)` | Text color |
85
+ | `--rz-muted` | `oklch(0.48 0.01 260)` | Muted/secondary text |
86
+ | `--rz-radius` | `0.5rem` | Border radius |
87
+
88
+ ### Dark mode
89
+
90
+ Add `.dark` class to `<html>`:
91
+
92
+ ```html
93
+ <html class="dark">...</html>
94
+ ```
95
+
96
+ The library uses `@custom-variant dark (&:where(.dark, .dark *))` — no media query, fully manual toggle.
97
+
98
+ ---
99
+
100
+ ## Components
101
+
102
+ ### Common Conventions
103
+
104
+ - **Size**: `'sm' | 'md' | 'lg'` — component `size` input, defaults to `'md'`
105
+ - **Accent**: applies via `accentColor` input (named `ThemeColor` or any raw CSS color string) or `config.accentColor` where available
106
+ - **Error/Hint**: every form component supports `error`, `errorMessage`, and `hint` inputs
107
+ - **Animations**: CSS `@keyframes` prefixed with `rz-*`, triggered by Angular animation triggers (`[animate.enter]`, `[animate.leave]`)
108
+ - **Panel positioning**: panels (dropdown, datepicker) use `position: fixed` with manual `(document:click)` listener — no CDK
109
+
110
+ ---
111
+
112
+ ### Form
113
+
114
+ #### `<app-text-input>`
115
+
116
+ Standard text input field.
117
+
118
+ | Input | Type | Default |
119
+ |-------|------|---------|
120
+ | `value` | `model<string>` | `''` |
121
+ | `placeholder` | `string` | `''` |
122
+ | `size` | `FieldSize` | `'md'` |
123
+ | `disabled` | `boolean` | `false` |
124
+ | `accentColor` | `string \| null` | `null` |
125
+ | `hint` | `string` | `''` |
126
+ | `errorMessage` | `string` | `''` |
127
+ | `error` | `boolean` | `false` |
128
+ | `width` | `string` | `''` |
129
+
130
+ ```html
131
+ <app-text-input [(value)]="name" placeholder="Enter name" [accentColor]="'primary'" />
132
+ ```
133
+
134
+ ---
135
+
136
+ #### `<app-password-input>`
137
+
138
+ Password field with visibility toggle.
139
+
140
+ | Input | Type | Default |
141
+ |-------|------|---------|
142
+ | `value` | `model<string>` | `''` |
143
+ | `placeholder` | `string` | `''` |
144
+ | `size` | `FieldSize` | `'md'` |
145
+ | `disabled` | `boolean` | `false` |
146
+ | `accentColor` | `string \| null` | `null` |
147
+ | `hint` | `string` | `''` |
148
+ | `errorMessage` | `string` | `''` |
149
+ | `error` | `boolean` | `false` |
150
+ | `hideToggle` | `boolean` | `false` |
151
+ | `width` | `string` | `''` |
152
+
153
+ **Methods**: `toggleVisibility()`
154
+
155
+ ```html
156
+ <app-password-input [(value)]="password" placeholder="Password" />
157
+ ```
158
+
159
+ ---
160
+
161
+ #### `<app-search-input>`
162
+
163
+ Search field with search icon and clear button.
164
+
165
+ | Input | Type | Default |
166
+ |-------|------|---------|
167
+ | `value` | `model<string>` | `''` |
168
+ | `placeholder` | `string` | `'Search...'` |
169
+ | `size` | `FieldSize` | `'md'` |
170
+ | `disabled` | `boolean` | `false` |
171
+ | `accentColor` | `string \| null` | `null` |
172
+ | `hint` | `string` | `''` |
173
+ | `errorMessage` | `string` | `''` |
174
+ | `error` | `boolean` | `false` |
175
+ | `width` | `string` | `''` |
176
+
177
+ ```html
178
+ <app-search-input [(value)]="query" placeholder="Search users..." />
179
+ ```
180
+
181
+ ---
182
+
183
+ #### `<app-checkbox>`
184
+
185
+ Checkbox with optional label, indeterminate state.
186
+
187
+ | Input | Type | Default |
188
+ |-------|------|---------|
189
+ | `checked` | `model<boolean>` | `false` |
190
+ | `label` | `string` | `''` |
191
+ | `size` | `FieldSize` | `'md'` |
192
+ | `disabled` | `boolean` | `false` |
193
+ | `accentColor` | `string \| null` | `null` |
194
+ | `indeterminate` | `boolean` | `false` |
195
+ | `hint` | `string` | `''` |
196
+ | `errorMessage` | `string` | `''` |
197
+ | `error` | `boolean` | `false` |
198
+ | `width` | `string` | `''` |
199
+
200
+ **Methods**: `toggle()`
201
+
202
+ ```html
203
+ <app-checkbox [(checked)]="agreed" label="I agree to the terms" />
204
+ <app-checkbox [indeterminate]="true" label="Select all" />
205
+ ```
206
+
207
+ ---
208
+
209
+ #### `<app-toggle>`
210
+
211
+ Switch/toggle control.
212
+
213
+ | Input | Type | Default |
214
+ |-------|------|---------|
215
+ | `checked` | `model<boolean>` | `false` |
216
+ | `label` | `string` | `''` |
217
+ | `size` | `FieldSize` | `'md'` |
218
+ | `disabled` | `boolean` | `false` |
219
+ | `accentColor` | `string \| null` | `null` |
220
+ | `hint` | `string` | `''` |
221
+ | `errorMessage` | `string` | `''` |
222
+ | `error` | `boolean` | `false` |
223
+ | `width` | `string` | `''` |
224
+
225
+ **Methods**: `toggle()`
226
+
227
+ ```html
228
+ <app-toggle [(checked)]="notifications" label="Enable notifications" />
229
+ ```
230
+
231
+ ---
232
+
233
+ #### `<app-date-picker>`
234
+
235
+ Inline calendar with month/year navigation, today button.
236
+
237
+ | Input | Type | Default |
238
+ |-------|------|---------|
239
+ | `value` | `model<Date \| null>` | `null` |
240
+ | `placeholder` | `string` | `'Select date'` |
241
+ | `size` | `FieldSize` | `'md'` |
242
+ | `disabled` | `boolean` | `false` |
243
+ | `accentColor` | `string \| null` | `null` |
244
+ | `hint` | `string` | `''` |
245
+ | `errorMessage` | `string` | `''` |
246
+ | `error` | `boolean` | `false` |
247
+ | `format` | `'dd/MM/yyyy' \| 'MM/dd/yyyy' \| 'yyyy-MM-dd'` | `'dd/MM/yyyy'` |
248
+ | `width` | `string` | `''` |
249
+ | `config` | `DatePickerConfig` | `{}` |
250
+
251
+ **`DatePickerConfig`:**
252
+
253
+ ```typescript
254
+ interface DatePickerConfig {
255
+ minDate?: Date;
256
+ maxDate?: Date;
257
+ disabledDates?: Date[];
258
+ highlightedDates?: Date[];
259
+ firstDayOfWeek?: 0 | 1; // 0=Sunday, 1=Monday
260
+ showTodayButton?: boolean; // default true
261
+ showYearNavigation?: boolean; // default true
262
+ appendToParent?: boolean; // default false
263
+ }
264
+ ```
265
+
266
+ ```html
267
+ <app-date-picker [(value)]="dob" placeholder="Date of birth" />
268
+ <app-date-picker [(value)]="range" [config]="{ minDate: today, firstDayOfWeek: 1 }" />
269
+ ```
270
+
271
+ ---
272
+
273
+ #### `<app-drop-down>`
274
+
275
+ Searchable dropdown with smart panel positioning.
276
+
277
+ | Input | Type | Default |
278
+ |-------|------|---------|
279
+ | `value` | `model<string \| null>` | `null` |
280
+ | `options` | `(DropdownOption \| string)[]` | `[]` |
281
+ | `placeholder` | `string` | `'Select...'` |
282
+ | `size` | `FieldSize` | `'md'` |
283
+ | `disabled` | `boolean` | `false` |
284
+ | `accentColor` | `string \| null` | `null` |
285
+ | `searchable` | `boolean` | `false` |
286
+ | `hint` | `string` | `''` |
287
+ | `errorMessage` | `string` | `''` |
288
+ | `error` | `boolean` | `false` |
289
+ | `width` | `string` | `''` |
290
+ | `config` | `DropdownConfig` | `{}` |
291
+
292
+ **`DropdownOption<T>`:**
293
+
294
+ ```typescript
295
+ interface DropdownOption<T = unknown> {
296
+ label: string;
297
+ value: T;
298
+ }
299
+ ```
300
+
301
+ **`DropdownConfig`:**
302
+
303
+ ```typescript
304
+ interface DropdownConfig {
305
+ appendToParent?: boolean;
306
+ maxHeight?: string;
307
+ direction?: 'up' | 'down' | 'auto';
308
+ }
309
+ ```
310
+
311
+ ```html
312
+ <app-drop-down [(value)]="country" :options="['Egypt', 'UAE', 'KSA']" />
313
+ <app-drop-down
314
+ [(value)]="user"
315
+ [options]="[{ label: 'Alice', value: 1 }, { label: 'Bob', value: 2 }]"
316
+ searchable
317
+ />
318
+ ```
319
+
320
+ ---
321
+
322
+ #### `<app-multi-select>`
323
+
324
+ Multi-select with chips, search, select all.
325
+
326
+ | Input | Type | Default |
327
+ |-------|------|---------|
328
+ | `value` | `model<string[]>` | `[]` |
329
+ | `options` | `(DropdownOption \| string)[]` | `[]` |
330
+ | `placeholder` | `string` | `'Select...'` |
331
+ | `size` | `FieldSize` | `'md'` |
332
+ | `disabled` | `boolean` | `false` |
333
+ | `selectAll` | `boolean` | `true` |
334
+ | `selectAllLabel` | `string` | `'Select All'` |
335
+ | `accentColor` | `string \| null` | `null` |
336
+ | `searchable` | `boolean` | `false` |
337
+ | `hint` | `string` | `''` |
338
+ | `errorMessage` | `string` | `''` |
339
+ | `error` | `boolean` | `false` |
340
+ | `width` | `string` | `''` |
341
+ | `config` | `MultiSelectConfig` | `{}` |
342
+
343
+ **`MultiSelectConfig`:**
344
+
345
+ ```typescript
346
+ interface MultiSelectConfig {
347
+ appendToParent?: boolean;
348
+ maxHeight?: string;
349
+ direction?: 'up' | 'down' | 'auto';
350
+ }
351
+ ```
352
+
353
+ ```html
354
+ <app-multi-select
355
+ [(value)]="roles"
356
+ [options]="['Admin', 'Editor', 'Viewer']"
357
+ searchable
358
+ />
359
+ ```
360
+
361
+ ---
362
+
363
+ #### `<app-file-upload>`
364
+
365
+ Drag-and-drop file upload with validation.
366
+
367
+ | Input | Type | Default |
368
+ |-------|------|---------|
369
+ | `files` | `model<File[]>` | `[]` |
370
+ | `accept` | `string` | `''` |
371
+ | `multiple` | `boolean` | `true` |
372
+ | `maxSize` | `number` (bytes) | `0` (unlimited) |
373
+ | `maxFiles` | `number` | `0` (unlimited) |
374
+ | `disabled` | `boolean` | `false` |
375
+ | `placeholder` | `string` | `'Drop files here or click to browse'` |
376
+ | `size` | `FieldSize` | `'md'` |
377
+ | `hint` | `string` | `''` |
378
+ | `errorMessage` | `string` | `''` |
379
+ | `error` | `boolean` | `false` |
380
+
381
+ ```html
382
+ <app-file-upload [(files)]="docs" accept=".pdf,.docx" [maxSize]="5 * 1024 * 1024" />
383
+ ```
384
+
385
+ ---
386
+
387
+ ### Data Display
388
+
389
+ #### `<app-badge>`
390
+
391
+ Small label/badge with variants.
392
+
393
+ | Input | Type | Default |
394
+ |-------|------|---------|
395
+ | `accentColor` | `string` | `'primary'` |
396
+ | `variant` | `'filled' \| 'outlined' \| 'subtle'` | `'filled'` |
397
+ | `size` | `FieldSize` | `'sm'` |
398
+ | `dot` | `boolean` | `false` |
399
+
400
+ ```html
401
+ <app-badge variant="subtle" accentColor="success">Active</app-badge>
402
+ <app-badge [dot]="true" accentColor="danger" />
403
+ ```
404
+
405
+ ---
406
+
407
+ #### `<app-skeleton>`
408
+
409
+ Placeholder loader.
410
+
411
+ | Input | Type | Default |
412
+ |-------|------|---------|
413
+ | `variant` | `'text' \| 'circle' \| 'rect'` | `'text'` |
414
+ | `width` | `string` | `''` |
415
+ | `height` | `string` | `''` |
416
+ | `count` | `number` | `1` |
417
+ | `borderRadius` | `string` | `''` |
418
+ | `accentColor` | `string` | `''` |
419
+
420
+ ```html
421
+ <app-skeleton variant="circle" width="40px" height="40px" />
422
+ <app-skeleton variant="text" count="3" />
423
+ ```
424
+
425
+ ---
426
+
427
+ #### `<app-table-skeleton>`
428
+
429
+ Table skeleton loader.
430
+
431
+ | Input | Type | Default |
432
+ |-------|------|---------|
433
+ | `rows` | `number` | `5` |
434
+ | `columns` | `number \| TableSkeletonColumn[]` | `4` |
435
+
436
+ ```html
437
+ <app-table-skeleton [rows]="8" [columns]="6" />
438
+ ```
439
+
440
+ ---
441
+
442
+ #### `<app-tooltip>`
443
+
444
+ Overflow-detection tooltip.
445
+
446
+ | Input | Type | Default |
447
+ |-------|------|---------|
448
+ | `text` | `string` | `''` |
449
+ | `position` | `'top' \| 'bottom' \| 'left' \| 'right'` | `'top'` |
450
+ | `disabled` | `boolean` | `false` |
451
+ | `alwaysShow` | `boolean` | `false` |
452
+
453
+ ```html
454
+ <app-tooltip text="This is a tooltip" position="top">
455
+ <span>Hover me</span>
456
+ </app-tooltip>
457
+ ```
458
+
459
+ ---
460
+
461
+ #### `<app-table>`
462
+
463
+ Full-featured data table with sorting, column filtering, text search, pagination, row selection, column resize, column toggle, Excel/PDF export, row expansion, sticky header, and skeleton loading.
464
+
465
+ | Input | Type | Default | Description |
466
+ |-------|------|---------|-------------|
467
+ | `columns` | `ColumnDef[]` | `[]` | Column definitions |
468
+ | `rows` | `Record<string, unknown>[]` | `[]` | Data rows |
469
+ | `striped` | `boolean` | `false` | Alternating row colors |
470
+ | `hoverable` | `boolean` | `true` | Highlight row on hover |
471
+ | `sortable` | `boolean` | `false` | Enable column sorting |
472
+ | `emptyMessage` | `string` | `'No data available'` | Text when no rows match |
473
+ | `width` | `string` | `''` | CSS width |
474
+ | `loading` | `boolean` | `false` | Show skeleton loader |
475
+ | `searchable` | `boolean` | `false` | Global text search bar |
476
+ | `filterable` | `boolean` | `false` | Per-column filter dropdowns |
477
+ | `skeletonRows` | `number` | `5` | Skeleton row count during loading |
478
+ | `resizable` | `boolean` | `false` | Drag column borders to resize |
479
+ | `pageable` | `boolean` | `false` | Pagination controls |
480
+ | `pageSize` | `number` | `10` | Rows per page |
481
+ | `pageSizeOptions` | `number[]` | `[5, 10, 20, 50]` | Page size choices |
482
+ | `selectable` | `boolean` | `false` | Checkbox row selection |
483
+ | `selectionMode` | `'single' \| 'multiple'` | `'multiple'` | Single or multi select |
484
+ | `selectionKey` | `string` | `''` | Row property for selection identity |
485
+ | `stickyHeader` | `boolean` | `false` | Fixed header on scroll |
486
+ | `exportable` | `boolean` | `false` | Excel (xlsx) + PDF export buttons |
487
+ | `columnToggle` | `boolean` | `false` | Show/hide columns menu |
488
+ | `expandable` | `boolean` | `false` | Expandable row detail |
489
+
490
+ | Output | Emit Type | Description |
491
+ |--------|-----------|-------------|
492
+ | `sortChange` | `SortChange` | Emitted when sort column/direction changes |
493
+ | `pageChange` | `number` | Emitted on page navigation |
494
+ | `selectedRows` | `Record<string, unknown>[]` | Emitted when selection changes |
495
+
496
+ ```typescript
497
+ interface SortChange {
498
+ key: string;
499
+ direction: 'asc' | 'desc' | '';
500
+ }
501
+ ```
502
+
503
+ **`ColumnDef`:**
504
+
505
+ ```typescript
506
+ interface ColumnDef {
507
+ key: string; // Property name in row data
508
+ header: string; // Display header text
509
+ sortable?: boolean; // Allow sorting on this column
510
+ width?: string; // CSS width (e.g. '120px', '2fr')
511
+ align?: 'left' | 'center' | 'right'; // Text alignment
512
+ cell?: (row: Record<string, unknown>) => string; // Custom cell renderer
513
+ rowSpan?: (row, index, rows) => number; // Dynamic rowspan (0=hidden, >1=span)
514
+ actions?: ActionDef[]; // Action buttons per row
515
+ filterable?: boolean; // Show filter dropdown
516
+ filterOptions?: { label: string; value: string }[]; // Filter choices
517
+ footer?: (rows: Record<string, unknown>[]) => string; // Footer text aggregator
518
+ truncate?: boolean; // Truncate long text
519
+ maxChars?: number; // Max chars before truncation
520
+ }
521
+ ```
522
+
523
+ **`ActionDef`:**
524
+
525
+ ```typescript
526
+ interface ActionDef {
527
+ icon?: string; // PrimeIcon class (e.g. 'pi pi-pencil')
528
+ label?: string; // Button text (falls back to icon-only)
529
+ accentColor?: string; // Button color
530
+ visible?: (row: Record<string, unknown>) => boolean; // Conditional visibility
531
+ disabled?: (row: Record<string, unknown>) => boolean; // Conditional disable
532
+ click: (row: Record<string, unknown>) => void; // Click handler
533
+ }
534
+ ```
535
+
536
+ **Complete usage example:**
537
+
538
+ ```typescript
539
+ import { Component } from '@angular/core';
540
+ import { ColumnDef, SortChange } from 'ryzen-ui';
541
+
542
+ @Component({ ... })
543
+ export class MyComponent {
544
+ cols: ColumnDef[] = [
545
+ { key: 'name', header: 'Name', sortable: true, width: '150px' },
546
+ { key: 'email', header: 'Email', sortable: true, filterable: true, filterOptions: [] },
547
+ { key: 'role', header: 'Role', sortable: true },
548
+ {
549
+ key: 'actions',
550
+ header: '',
551
+ width: '100px',
552
+ align: 'center',
553
+ actions: [
554
+ {
555
+ icon: 'pi pi-pencil',
556
+ accentColor: 'primary',
557
+ click: row => this.edit(row),
558
+ },
559
+ {
560
+ icon: 'pi pi-trash',
561
+ accentColor: 'danger',
562
+ visible: row => row['status'] !== 'archived',
563
+ click: row => this.delete(row),
564
+ },
565
+ ],
566
+ },
567
+ ];
568
+
569
+ data = [
570
+ { name: 'Alice', email: 'alice@example.com', role: 'Admin', status: 'active' },
571
+ { name: 'Bob', email: 'bob@example.com', role: 'User', status: 'active' },
572
+ ];
573
+
574
+ onSort(ev: SortChange) { console.log('sort', ev); }
575
+ onPage(page: number) { console.log('page', page); }
576
+ onSelect(rows: Record<string, unknown>[]) { console.log('selected', rows); }
577
+ edit(row: any) { /* ... */ }
578
+ delete(row: any) { /* ... */ }
579
+ }
580
+ ```
581
+
582
+ ```html
583
+ <app-table
584
+ [columns]="cols"
585
+ [rows]="data"
586
+ sortable
587
+ pageable
588
+ searchable
589
+ filterable
590
+ selectable
591
+ selectionKey="email"
592
+ exportable
593
+ columnToggle
594
+ resizable
595
+ striped
596
+ stickyHeader
597
+ (sortChange)="onSort($event)"
598
+ (pageChange)="onPage($event)"
599
+ (selectedRows)="onSelect($event)"
600
+ />
601
+
602
+ <!-- With expandable rows -->
603
+ <app-table [columns]="cols" [rows]="data" expandable>
604
+ <ng-template #rowDetail let-row>
605
+ <div style="padding: 1rem">
606
+ <p><strong>Email:</strong> {{ row['email'] }}</p>
607
+ <p><strong>Role:</strong> {{ row['role'] }}</p>
608
+ </div>
609
+ </ng-template>
610
+ </app-table>
611
+ ```
612
+
613
+ ---
614
+
615
+ #### `<app-image-upload>`
616
+
617
+ Image uploader with thumbnail previews.
618
+
619
+ | Input | Type | Default |
620
+ |-------|------|---------|
621
+ | `images` | `model<ImageFile[]>` | `[]` |
622
+ | `multiple` | `boolean` | `true` |
623
+ | `maxFiles` | `number` | `4` |
624
+ | `maxSize` | `number` (bytes) | `2097152` (2 MB) |
625
+ | `disabled` | `boolean` | `false` |
626
+ | `size` | `FieldSize` | `'md'` |
627
+ | `placeholder` | `string` | `'Drop images or click'` |
628
+ | `errorMessage` | `string` | `''` |
629
+ | `error` | `boolean` | `false` |
630
+
631
+ ```typescript
632
+ interface ImageFile {
633
+ file: File;
634
+ url: string;
635
+ }
636
+ ```
637
+
638
+ ```html
639
+ <app-image-upload [(images)]="photos" [maxFiles]="6" />
640
+ ```
641
+
642
+ ---
643
+
644
+ #### `<app-carousel>`
645
+
646
+ Image carousel with autoplay, dots, arrows.
647
+
648
+ | Input | Type | Default |
649
+ |-------|------|---------|
650
+ | `items` | `CarouselItem[]` | **required** |
651
+ | `height` | `string` | `'320px'` |
652
+ | `autoPlay` | `boolean` | `true` |
653
+ | `interval` | `number` (ms) | `4000` |
654
+ | `showArrows` | `boolean` | `true` |
655
+ | `showDots` | `boolean` | `true` |
656
+
657
+ ```typescript
658
+ interface CarouselItem {
659
+ src: string;
660
+ alt?: string;
661
+ title?: string;
662
+ description?: string;
663
+ }
664
+ ```
665
+
666
+ ```html
667
+ <app-carousel [items]="slides" [autoPlay]="false" />
668
+ ```
669
+
670
+ ---
671
+
672
+ #### `<app-banner-slider>`
673
+
674
+ Hero banner with CTA button and text overlays.
675
+
676
+ | Input | Type | Default |
677
+ |-------|------|---------|
678
+ | `items` | `BannerItem[]` | **required** |
679
+ | `height` | `string` | `'400px'` |
680
+ | `autoPlay` | `boolean` | `true` |
681
+ | `interval` | `number` (ms) | `5000` |
682
+ | `showArrows` | `boolean` | `true` |
683
+ | `showDots` | `boolean` | `true` |
684
+ | `overlayMode` | `'left' \| 'center' \| 'right'` | `'left'` |
685
+
686
+ | Output | Emit Type |
687
+ |--------|-----------|
688
+ | `ctaClick` | `BannerItem` |
689
+
690
+ ```typescript
691
+ interface BannerItem {
692
+ src: string;
693
+ alt?: string;
694
+ title?: string;
695
+ subtitle?: string;
696
+ description?: string;
697
+ ctaLabel?: string;
698
+ ctaLink?: string;
699
+ }
700
+ ```
701
+
702
+ ```html
703
+ <app-banner-slider [items]="banners" (ctaClick)="onCtaClick($event)" />
704
+ ```
705
+
706
+ ---
707
+
708
+ #### `<app-empty-state>`
709
+
710
+ Centered placeholder with icon and action.
711
+
712
+ | Input | Type | Default |
713
+ |-------|------|---------|
714
+ | `icon` | `string` | `''` |
715
+ | `title` | `string` | `''` |
716
+ | `message` | `string` | `''` |
717
+ | `width` | `string` | `''` |
718
+
719
+ ```html
720
+ <app-empty-state icon="pi pi-inbox" title="No messages" message="You have no unread messages" />
721
+ ```
722
+
723
+ ---
724
+
725
+ ### Feedback
726
+
727
+ #### `<app-alert>`
728
+
729
+ Colored alert banner with icon and dismiss.
730
+
731
+ | Input | Type | Default |
732
+ |-------|------|---------|
733
+ | `type` | `'success' \| 'warning' \| 'danger' \| 'info'` | `'info'` |
734
+ | `title` | `string` | `''` |
735
+ | `dismissible` | `boolean` | `true` |
736
+ | `showIcon` | `boolean` | `true` |
737
+ | `width` | `string` | `''` |
738
+
739
+ | Output | Emit Type |
740
+ |--------|-----------|
741
+ | `dismiss` | `void` |
742
+
743
+ ```html
744
+ <app-alert type="success" title="Operation completed successfully" (dismiss)="onDismiss()" />
745
+ <app-alert type="danger" [dismissible]="false" title="Critical error" />
746
+ ```
747
+
748
+ ---
749
+
750
+ #### `<app-spinner>`
751
+
752
+ Loading spinner with 5 variants, image/text support, overlay mode.
753
+
754
+ | Input | Type | Default |
755
+ |-------|------|---------|
756
+ | `variant` | `'circle' \| 'dual-ring' \| 'dots' \| 'pulse' \| 'bars'` | `'dual-ring'` |
757
+ | `size` | `FieldSize` | `'md'` |
758
+ | `accentColor` | `string` | `'primary'` |
759
+ | `overlay` | `boolean` | `false` |
760
+ | `text` | `string` | `''` |
761
+ | `imageUrl` | `string` | `''` |
762
+
763
+ ```html
764
+ <app-spinner variant="dots" text="Loading..." />
765
+ <app-spinner [overlay]="true" variant="bars" />
766
+ <app-spinner variant="circle" [imageUrl]="'/assets/logo.png'" />
767
+ ```
768
+
769
+ ---
770
+
771
+ #### `<app-toast>`
772
+
773
+ Toast notification container (used with `ToastService`).
774
+
775
+ | Input | Type | Default |
776
+ |-------|------|---------|
777
+ | `position` | `'top-right' \| 'top-left' \| 'bottom-right' \| 'bottom-left'` | `'top-right'` |
778
+ | `maxVisible` | `number` | `5` |
779
+
780
+ ```html
781
+ <app-toast position="top-right" />
782
+ ```
783
+
784
+ **`ToastService`** (providedIn: 'root'):
785
+
786
+ ```typescript
787
+ class ToastService {
788
+ readonly toasts: Signal<ToastMessage[]>;
789
+
790
+ show(type: ToastType, message: string, config?: { title?: string; duration?: number }): void;
791
+ success(message: string, config?: { title?: string; duration?: number }): void;
792
+ error(message: string, config?: { title?: string; duration?: number }): void;
793
+ warning(message: string, config?: { title?: string; duration?: number }): void;
794
+ info(message: string, config?: { title?: string; duration?: number }): void;
795
+ remove(id: number): void;
796
+ clear(): void;
797
+ }
798
+ ```
799
+
800
+ ```typescript
801
+ @Component({ ... })
802
+ export class MyComponent {
803
+ private toast = inject(ToastService);
804
+
805
+ save() {
806
+ this.toast.success('Data saved', { title: 'Success', duration: 3000 });
807
+ }
808
+ }
809
+ ```
810
+
811
+ ---
812
+
813
+ ### Navigation
814
+
815
+ #### `<app-accordion>` + `<app-accordion-panel>`
816
+
817
+ Collapsible accordion with single/multi mode.
818
+
819
+ **`<app-accordion>`:**
820
+
821
+ | Input | Type | Default |
822
+ |-------|------|---------|
823
+ | `multi` | `boolean` | `false` |
824
+
825
+ **`<app-accordion-panel>`:**
826
+
827
+ | Input | Type | Default |
828
+ |-------|------|---------|
829
+ | `expanded` | `model<boolean>` | `false` |
830
+ | `title` | `string` | `''` |
831
+ | `disabled` | `boolean` | `false` |
832
+
833
+ ```html
834
+ <app-accordion [multi]="true">
835
+ <app-accordion-panel title="Section 1" [(expanded)]="sec1">
836
+ Content for section 1
837
+ </app-accordion-panel>
838
+ <app-accordion-panel title="Section 2" [disabled]="true">
839
+ Disabled content
840
+ </app-accordion-panel>
841
+ </app-accordion>
842
+ ```
843
+
844
+ ---
845
+
846
+ #### `<app-sidebar>`
847
+
848
+ Responsive sidebar with collapse, submenus, logout.
849
+
850
+ | Input | Type | Default |
851
+ |-------|------|---------|
852
+ | `isOpen` | `boolean` | `false` |
853
+ | `isCollapsed` | `boolean` | `false` |
854
+ | `items` | `SidebarItem[]` | `[]` |
855
+ | `title` | `string` | `'Menu'` |
856
+ | `width` | `string` | `'280px'` |
857
+ | `activeId` | `string \| null` | `null` |
858
+
859
+ | Output | Emit Type |
860
+ |--------|-----------|
861
+ | `toggleOpen` | `void` |
862
+ | `toggleCollapse` | `void` |
863
+ | `itemClick` | `SidebarItem` |
864
+ | `logout` | `void` |
865
+
866
+ ```typescript
867
+ interface SidebarItem {
868
+ id: string;
869
+ label: string;
870
+ icon?: string;
871
+ route?: string;
872
+ children?: SidebarItem[];
873
+ }
874
+ ```
875
+
876
+ ```html
877
+ <app-sidebar
878
+ [isOpen]="sidebarOpen"
879
+ [isCollapsed]="sidebarCollapsed"
880
+ [items]="menuItems"
881
+ [activeId]="currentRoute"
882
+ (itemClick)="onNav($event)"
883
+ (toggleCollapse)="sidebarCollapsed = !sidebarCollapsed"
884
+ (logout)="onLogout()"
885
+ />
886
+ ```
887
+
888
+ ---
889
+
890
+ #### `<app-nav>`
891
+
892
+ Top navbar with desktop dropdowns, mobile hamburger menu.
893
+
894
+ | Input | Type | Default |
895
+ |-------|------|---------|
896
+ | `items` | `NavItem[]` | `[]` |
897
+ | `brandText` | `string` | `'Brand'` |
898
+ | `brandIcon` | `string` | `'pi pi-box'` |
899
+ | `fixed` | `boolean` | `true` |
900
+
901
+ | Output | Emit Type |
902
+ |--------|-----------|
903
+ | `logout` | `void` |
904
+
905
+ ```typescript
906
+ interface NavItem {
907
+ id: string;
908
+ label: string;
909
+ icon?: string;
910
+ route?: string;
911
+ children?: NavItem[];
912
+ }
913
+ ```
914
+
915
+ ```html
916
+ <app-nav
917
+ [items]="navItems"
918
+ brandText="MyApp"
919
+ brandIcon="pi pi-cog"
920
+ (logout)="onLogout()"
921
+ />
922
+ ```
923
+
924
+ ---
925
+
926
+ ### Overlay
927
+
928
+ #### `<app-modal>`
929
+
930
+ Modal dialog with title slot, footer slot, close on backdrop/escape.
931
+
932
+ | Input | Type | Default |
933
+ |-------|------|---------|
934
+ | `open` | `model<boolean>` | `false` |
935
+ | `title` | `string` | `''` |
936
+ | `width` | `string` | `''` |
937
+ | `config` | `ModalConfig` | `{}` |
938
+
939
+ | Output | Emit Type |
940
+ |--------|-----------|
941
+ | `closed` | `void` |
942
+
943
+ **Methods**: `close()`
944
+
945
+ ```typescript
946
+ interface ModalConfig {
947
+ closeOnBackdrop?: boolean; // default true
948
+ closeOnEscape?: boolean; // default true
949
+ showCloseButton?: boolean; // default true
950
+ maxWidth?: string; // default '500px'
951
+ }
952
+ ```
953
+
954
+ ```html
955
+ <app-modal [(open)]="modalOpen" title="Edit User" [config]="{ maxWidth: '600px' }">
956
+ <p>Modal body content</p>
957
+ <ng-template #modalFooter>
958
+ <button (click)="save()">Save</button>
959
+ </ng-template>
960
+ </app-modal>
961
+ ```
962
+
963
+ ---
964
+
965
+ #### `<app-confirm-dialog>`
966
+
967
+ Pre-built confirmation dialog with customizable icon, accent, and button text.
968
+
969
+ | Input | Type | Default |
970
+ |-------|------|---------|
971
+ | `open` | `model<boolean>` | `false` |
972
+ | `message` | `string` | `''` |
973
+ | `config` | `ConfirmDialogConfig` | `{}` |
974
+
975
+ | Output | Emit Type |
976
+ |--------|-----------|
977
+ | `confirm` | `void` |
978
+ | `cancel` | `void` |
979
+
980
+ ```typescript
981
+ interface ConfirmDialogConfig {
982
+ title?: string;
983
+ confirmText?: string;
984
+ cancelText?: string;
985
+ icon?: string;
986
+ confirmAccent?: ThemeColor | string;
987
+ closeOnBackdrop?: boolean;
988
+ closeOnEscape?: boolean;
989
+ }
990
+ ```
991
+
992
+ ```html
993
+ <app-confirm-dialog
994
+ [(open)]="confirmOpen"
995
+ message="Are you sure you want to delete this item?"
996
+ [config]="{ title: 'Confirm Delete', confirmAccent: 'danger', confirmText: 'Delete' }"
997
+ (confirm)="deleteItem()"
998
+ (cancel)="confirmOpen = false"
999
+ />
1000
+ ```
1001
+
1002
+ ---
1003
+
1004
+ ## Shared Types
1005
+
1006
+ ```typescript
1007
+ type FieldSize = 'sm' | 'md' | 'lg';
1008
+ type ThemeColor = 'primary' | 'secondary' | 'accent' | 'success' | 'warning' | 'danger' | 'info';
1009
+ ```
1010
+
1011
+ ---
1012
+
1013
+ ## Development
1014
+
1015
+ ```bash
1016
+ # Build the library
1017
+ npm run build
1018
+
1019
+ # Watch mode
1020
+ npm run watch
1021
+
1022
+ # Publish
1023
+ npm run publish
1024
+ ```
1025
+
1026
+ Build artifacts go to `dist/rz-lib/`.
1027
+
1028
+ ---
1029
+
1030
+ ## Conventions
1031
+
1032
+ | Convention | Value |
1033
+ |------------|-------|
1034
+ | Change detection | `ChangeDetectionStrategy.OnPush` |
1035
+ | State management | Angular `signal()` / `model()` / `computed()` / `input()` |
1036
+ | Styling | Pure CSS custom properties — no `@angular/animations` |
1037
+ | Theming | CSS variables with oklch fallbacks (light + dark) |
1038
+ | Positioning | CSS `position: fixed` / `absolute` — no CDK overlay |
1039
+ | Animation | `@keyframes` + `setTimeout` for close sequence |
1040
+ | Panel pattern | Open: `setTimeout(0)` → enter animation. Close: `_isClosing` signal → wait 150ms → remove |
1041
+ | `document:click` | Manually bound/unbound on open/close |
1042
+ | Timer cleanup | `_closeTimer` field + `ngOnDestroy` |
1043
+ | Field sizes | `sm` / `md` / `lg` |
1044
+ | Accent colors | Named theme colors or any raw CSS color string |
1045
+
1046
+ ---
1047
+
1048
+ ## Dependencies
1049
+
1050
+ | Dependency | Type | Required |
1051
+ |------------|------|----------|
1052
+ | `@angular/core` | peer | Yes |
1053
+ | `@angular/common` | peer | Yes |
1054
+ | `primeicons` | peer | Yes |
1055
+ | `exceljs` | optional peer | Table export (XLSX) |
1056
+ | `jspdf` | optional peer | Table export (PDF) |
1057
+ | `html2canvas` | optional peer | Table export (PDF) |
1058
+
1059
+ ---
1060
+
1061
+ ## License
1062
+
1063
+ MIT