@organcli/composed-cli 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/dist/index.js +540 -0
- package/dist/registry-bundle.json +1060 -0
- package/package.json +34 -0
|
@@ -0,0 +1,1060 @@
|
|
|
1
|
+
{
|
|
2
|
+
"registry": [
|
|
3
|
+
{
|
|
4
|
+
"name": "faq-accordion",
|
|
5
|
+
"type": "composed",
|
|
6
|
+
"slug": "accordion",
|
|
7
|
+
"files": [
|
|
8
|
+
{
|
|
9
|
+
"path": "src/components/ui/composed/accordion/faq-accordion.tsx",
|
|
10
|
+
"exports": [
|
|
11
|
+
"FaqAccordion"
|
|
12
|
+
]
|
|
13
|
+
}
|
|
14
|
+
],
|
|
15
|
+
"dependencies": {
|
|
16
|
+
"shadcn": [
|
|
17
|
+
"accordion"
|
|
18
|
+
],
|
|
19
|
+
"npm": [],
|
|
20
|
+
"local": [
|
|
21
|
+
"@/lib/utils"
|
|
22
|
+
]
|
|
23
|
+
},
|
|
24
|
+
"i18n": {
|
|
25
|
+
"required": false,
|
|
26
|
+
"keys": []
|
|
27
|
+
}
|
|
28
|
+
},
|
|
29
|
+
{
|
|
30
|
+
"name": "error-alert",
|
|
31
|
+
"type": "composed",
|
|
32
|
+
"slug": "alert",
|
|
33
|
+
"files": [
|
|
34
|
+
{
|
|
35
|
+
"path": "src/components/ui/composed/alert/error-alert.tsx",
|
|
36
|
+
"exports": [
|
|
37
|
+
"ErrorAlert"
|
|
38
|
+
]
|
|
39
|
+
}
|
|
40
|
+
],
|
|
41
|
+
"dependencies": {
|
|
42
|
+
"shadcn": [
|
|
43
|
+
"alert"
|
|
44
|
+
],
|
|
45
|
+
"npm": [
|
|
46
|
+
"lucide-react"
|
|
47
|
+
],
|
|
48
|
+
"local": [
|
|
49
|
+
"@/lib/utils"
|
|
50
|
+
]
|
|
51
|
+
},
|
|
52
|
+
"i18n": {
|
|
53
|
+
"required": false,
|
|
54
|
+
"keys": []
|
|
55
|
+
}
|
|
56
|
+
},
|
|
57
|
+
{
|
|
58
|
+
"name": "user-avatar",
|
|
59
|
+
"type": "composed",
|
|
60
|
+
"slug": "avatar",
|
|
61
|
+
"files": [
|
|
62
|
+
{
|
|
63
|
+
"path": "src/components/ui/composed/avatar/user-avatar.tsx",
|
|
64
|
+
"exports": [
|
|
65
|
+
"UserAvatar"
|
|
66
|
+
]
|
|
67
|
+
}
|
|
68
|
+
],
|
|
69
|
+
"dependencies": {
|
|
70
|
+
"shadcn": [
|
|
71
|
+
"avatar"
|
|
72
|
+
],
|
|
73
|
+
"npm": [],
|
|
74
|
+
"local": [
|
|
75
|
+
"@/lib/utils"
|
|
76
|
+
]
|
|
77
|
+
},
|
|
78
|
+
"i18n": {
|
|
79
|
+
"required": false,
|
|
80
|
+
"keys": []
|
|
81
|
+
}
|
|
82
|
+
},
|
|
83
|
+
{
|
|
84
|
+
"name": "status-badge",
|
|
85
|
+
"type": "composed",
|
|
86
|
+
"slug": "badge",
|
|
87
|
+
"files": [
|
|
88
|
+
{
|
|
89
|
+
"path": "src/components/ui/composed/badge/status-badge.tsx",
|
|
90
|
+
"exports": [
|
|
91
|
+
"StatusBadge"
|
|
92
|
+
]
|
|
93
|
+
}
|
|
94
|
+
],
|
|
95
|
+
"dependencies": {
|
|
96
|
+
"shadcn": [
|
|
97
|
+
"badge"
|
|
98
|
+
],
|
|
99
|
+
"npm": [],
|
|
100
|
+
"local": [
|
|
101
|
+
"@/lib/utils"
|
|
102
|
+
]
|
|
103
|
+
},
|
|
104
|
+
"i18n": {
|
|
105
|
+
"required": false,
|
|
106
|
+
"keys": []
|
|
107
|
+
}
|
|
108
|
+
},
|
|
109
|
+
{
|
|
110
|
+
"name": "loading-button",
|
|
111
|
+
"type": "composed",
|
|
112
|
+
"slug": "button",
|
|
113
|
+
"files": [
|
|
114
|
+
{
|
|
115
|
+
"path": "src/components/ui/composed/button/loading-button.tsx",
|
|
116
|
+
"exports": [
|
|
117
|
+
"LoadingButton"
|
|
118
|
+
]
|
|
119
|
+
}
|
|
120
|
+
],
|
|
121
|
+
"dependencies": {
|
|
122
|
+
"shadcn": [
|
|
123
|
+
"button"
|
|
124
|
+
],
|
|
125
|
+
"npm": [
|
|
126
|
+
"lucide-react"
|
|
127
|
+
],
|
|
128
|
+
"local": []
|
|
129
|
+
},
|
|
130
|
+
"i18n": {
|
|
131
|
+
"required": false,
|
|
132
|
+
"keys": []
|
|
133
|
+
}
|
|
134
|
+
},
|
|
135
|
+
{
|
|
136
|
+
"name": "theme-toggle",
|
|
137
|
+
"type": "composed",
|
|
138
|
+
"slug": "button",
|
|
139
|
+
"files": [
|
|
140
|
+
{
|
|
141
|
+
"path": "src/components/ui/composed/button/theme-toggle.tsx",
|
|
142
|
+
"exports": [
|
|
143
|
+
"ThemeToggle"
|
|
144
|
+
]
|
|
145
|
+
}
|
|
146
|
+
],
|
|
147
|
+
"dependencies": {
|
|
148
|
+
"shadcn": [
|
|
149
|
+
"button"
|
|
150
|
+
],
|
|
151
|
+
"npm": [
|
|
152
|
+
"lucide-react",
|
|
153
|
+
"next-themes"
|
|
154
|
+
],
|
|
155
|
+
"local": []
|
|
156
|
+
},
|
|
157
|
+
"i18n": {
|
|
158
|
+
"required": true,
|
|
159
|
+
"keys": [
|
|
160
|
+
"ui_toggle_theme"
|
|
161
|
+
]
|
|
162
|
+
}
|
|
163
|
+
},
|
|
164
|
+
{
|
|
165
|
+
"name": "stats-card",
|
|
166
|
+
"type": "composed",
|
|
167
|
+
"slug": "card",
|
|
168
|
+
"files": [
|
|
169
|
+
{
|
|
170
|
+
"path": "src/components/ui/composed/card/stats-card.tsx",
|
|
171
|
+
"exports": [
|
|
172
|
+
"StatsCard"
|
|
173
|
+
]
|
|
174
|
+
}
|
|
175
|
+
],
|
|
176
|
+
"dependencies": {
|
|
177
|
+
"shadcn": [
|
|
178
|
+
"card"
|
|
179
|
+
],
|
|
180
|
+
"npm": [
|
|
181
|
+
"lucide-react"
|
|
182
|
+
],
|
|
183
|
+
"local": [
|
|
184
|
+
"@/lib/utils"
|
|
185
|
+
]
|
|
186
|
+
},
|
|
187
|
+
"i18n": {
|
|
188
|
+
"required": false,
|
|
189
|
+
"keys": []
|
|
190
|
+
}
|
|
191
|
+
},
|
|
192
|
+
{
|
|
193
|
+
"name": "media-carousel",
|
|
194
|
+
"type": "composed",
|
|
195
|
+
"slug": "carousel",
|
|
196
|
+
"files": [
|
|
197
|
+
{
|
|
198
|
+
"path": "src/components/ui/composed/carousel/media-carousel.tsx",
|
|
199
|
+
"exports": [
|
|
200
|
+
"MediaCarousel"
|
|
201
|
+
]
|
|
202
|
+
}
|
|
203
|
+
],
|
|
204
|
+
"dependencies": {
|
|
205
|
+
"shadcn": [
|
|
206
|
+
"carousel",
|
|
207
|
+
"button"
|
|
208
|
+
],
|
|
209
|
+
"npm": [
|
|
210
|
+
"embla-carousel-react",
|
|
211
|
+
"lucide-react"
|
|
212
|
+
],
|
|
213
|
+
"local": [
|
|
214
|
+
"@/lib/utils"
|
|
215
|
+
]
|
|
216
|
+
},
|
|
217
|
+
"i18n": {
|
|
218
|
+
"required": false,
|
|
219
|
+
"keys": []
|
|
220
|
+
}
|
|
221
|
+
},
|
|
222
|
+
{
|
|
223
|
+
"name": "checkbox-field",
|
|
224
|
+
"type": "composed",
|
|
225
|
+
"slug": "checkbox",
|
|
226
|
+
"files": [
|
|
227
|
+
{
|
|
228
|
+
"path": "src/components/ui/composed/checkbox/checkbox-field.tsx",
|
|
229
|
+
"exports": [
|
|
230
|
+
"CheckboxField"
|
|
231
|
+
]
|
|
232
|
+
}
|
|
233
|
+
],
|
|
234
|
+
"dependencies": {
|
|
235
|
+
"shadcn": [
|
|
236
|
+
"checkbox",
|
|
237
|
+
"label"
|
|
238
|
+
],
|
|
239
|
+
"npm": [],
|
|
240
|
+
"local": [
|
|
241
|
+
"@/lib/utils"
|
|
242
|
+
]
|
|
243
|
+
},
|
|
244
|
+
"i18n": {
|
|
245
|
+
"required": false,
|
|
246
|
+
"keys": []
|
|
247
|
+
},
|
|
248
|
+
"controller": "CheckboxController"
|
|
249
|
+
},
|
|
250
|
+
{
|
|
251
|
+
"name": "checkbox-group",
|
|
252
|
+
"type": "composed",
|
|
253
|
+
"slug": "checkbox",
|
|
254
|
+
"files": [
|
|
255
|
+
{
|
|
256
|
+
"path": "src/components/ui/composed/checkbox/checkbox-group.tsx",
|
|
257
|
+
"exports": [
|
|
258
|
+
"CheckboxGroup"
|
|
259
|
+
]
|
|
260
|
+
}
|
|
261
|
+
],
|
|
262
|
+
"dependencies": {
|
|
263
|
+
"shadcn": [
|
|
264
|
+
"checkbox",
|
|
265
|
+
"label"
|
|
266
|
+
],
|
|
267
|
+
"npm": [],
|
|
268
|
+
"local": [
|
|
269
|
+
"@/lib/utils"
|
|
270
|
+
]
|
|
271
|
+
},
|
|
272
|
+
"i18n": {
|
|
273
|
+
"required": false,
|
|
274
|
+
"keys": []
|
|
275
|
+
},
|
|
276
|
+
"controller": "CheckboxGroupController"
|
|
277
|
+
},
|
|
278
|
+
{
|
|
279
|
+
"name": "searchable-select",
|
|
280
|
+
"type": "composed",
|
|
281
|
+
"slug": "combobox",
|
|
282
|
+
"files": [
|
|
283
|
+
{
|
|
284
|
+
"path": "src/components/ui/composed/combobox/searchable-select.tsx",
|
|
285
|
+
"exports": [
|
|
286
|
+
"SearchableSelect"
|
|
287
|
+
]
|
|
288
|
+
}
|
|
289
|
+
],
|
|
290
|
+
"dependencies": {
|
|
291
|
+
"shadcn": [
|
|
292
|
+
"command",
|
|
293
|
+
"popover",
|
|
294
|
+
"button",
|
|
295
|
+
"label",
|
|
296
|
+
"dialog",
|
|
297
|
+
"input-group"
|
|
298
|
+
],
|
|
299
|
+
"npm": [
|
|
300
|
+
"cmdk",
|
|
301
|
+
"lucide-react"
|
|
302
|
+
],
|
|
303
|
+
"local": [
|
|
304
|
+
"@/lib/utils"
|
|
305
|
+
]
|
|
306
|
+
},
|
|
307
|
+
"i18n": {
|
|
308
|
+
"required": true,
|
|
309
|
+
"keys": [
|
|
310
|
+
"ui_no_results",
|
|
311
|
+
"ui_search_placeholder",
|
|
312
|
+
"ui_select_placeholder"
|
|
313
|
+
]
|
|
314
|
+
},
|
|
315
|
+
"controller": "ComboboxController"
|
|
316
|
+
},
|
|
317
|
+
{
|
|
318
|
+
"name": "date-picker-field",
|
|
319
|
+
"type": "composed",
|
|
320
|
+
"slug": "date-picker",
|
|
321
|
+
"files": [
|
|
322
|
+
{
|
|
323
|
+
"path": "src/components/ui/composed/date-picker/date-picker-field.tsx",
|
|
324
|
+
"exports": [
|
|
325
|
+
"DatePickerField"
|
|
326
|
+
]
|
|
327
|
+
}
|
|
328
|
+
],
|
|
329
|
+
"dependencies": {
|
|
330
|
+
"shadcn": [
|
|
331
|
+
"calendar",
|
|
332
|
+
"button",
|
|
333
|
+
"label",
|
|
334
|
+
"popover"
|
|
335
|
+
],
|
|
336
|
+
"npm": [
|
|
337
|
+
"react-day-picker",
|
|
338
|
+
"lucide-react"
|
|
339
|
+
],
|
|
340
|
+
"local": [
|
|
341
|
+
"@/lib/utils"
|
|
342
|
+
]
|
|
343
|
+
},
|
|
344
|
+
"i18n": {
|
|
345
|
+
"required": true,
|
|
346
|
+
"keys": [
|
|
347
|
+
"ui_pick_date"
|
|
348
|
+
]
|
|
349
|
+
},
|
|
350
|
+
"controller": "DatePickerController"
|
|
351
|
+
},
|
|
352
|
+
{
|
|
353
|
+
"name": "confirmation-dialog",
|
|
354
|
+
"type": "composed",
|
|
355
|
+
"slug": "dialog",
|
|
356
|
+
"files": [
|
|
357
|
+
{
|
|
358
|
+
"path": "src/components/ui/composed/dialog/confirmation-dialog.tsx",
|
|
359
|
+
"exports": [
|
|
360
|
+
"ConfirmationDialog"
|
|
361
|
+
]
|
|
362
|
+
}
|
|
363
|
+
],
|
|
364
|
+
"dependencies": {
|
|
365
|
+
"shadcn": [
|
|
366
|
+
"dialog",
|
|
367
|
+
"button"
|
|
368
|
+
],
|
|
369
|
+
"npm": [
|
|
370
|
+
"lucide-react"
|
|
371
|
+
],
|
|
372
|
+
"local": []
|
|
373
|
+
},
|
|
374
|
+
"i18n": {
|
|
375
|
+
"required": true,
|
|
376
|
+
"keys": [
|
|
377
|
+
"ui_cancel",
|
|
378
|
+
"ui_confirm"
|
|
379
|
+
]
|
|
380
|
+
}
|
|
381
|
+
},
|
|
382
|
+
{
|
|
383
|
+
"name": "action-drawer",
|
|
384
|
+
"type": "composed",
|
|
385
|
+
"slug": "drawer",
|
|
386
|
+
"files": [
|
|
387
|
+
{
|
|
388
|
+
"path": "src/components/ui/composed/drawer/action-drawer.tsx",
|
|
389
|
+
"exports": [
|
|
390
|
+
"ActionDrawer"
|
|
391
|
+
]
|
|
392
|
+
}
|
|
393
|
+
],
|
|
394
|
+
"dependencies": {
|
|
395
|
+
"shadcn": [
|
|
396
|
+
"drawer"
|
|
397
|
+
],
|
|
398
|
+
"npm": [
|
|
399
|
+
"vaul"
|
|
400
|
+
],
|
|
401
|
+
"local": []
|
|
402
|
+
},
|
|
403
|
+
"i18n": {
|
|
404
|
+
"required": false,
|
|
405
|
+
"keys": []
|
|
406
|
+
}
|
|
407
|
+
},
|
|
408
|
+
{
|
|
409
|
+
"name": "actions-dropdown",
|
|
410
|
+
"type": "composed",
|
|
411
|
+
"slug": "dropdown-menu",
|
|
412
|
+
"files": [
|
|
413
|
+
{
|
|
414
|
+
"path": "src/components/ui/composed/dropdown-menu/actions-dropdown.tsx",
|
|
415
|
+
"exports": [
|
|
416
|
+
"ActionsDropdown"
|
|
417
|
+
]
|
|
418
|
+
}
|
|
419
|
+
],
|
|
420
|
+
"dependencies": {
|
|
421
|
+
"shadcn": [
|
|
422
|
+
"dropdown-menu",
|
|
423
|
+
"button"
|
|
424
|
+
],
|
|
425
|
+
"npm": [
|
|
426
|
+
"lucide-react"
|
|
427
|
+
],
|
|
428
|
+
"local": []
|
|
429
|
+
},
|
|
430
|
+
"i18n": {
|
|
431
|
+
"required": true,
|
|
432
|
+
"keys": [
|
|
433
|
+
"ui_open_actions"
|
|
434
|
+
]
|
|
435
|
+
}
|
|
436
|
+
},
|
|
437
|
+
{
|
|
438
|
+
"name": "empty-state",
|
|
439
|
+
"type": "composed",
|
|
440
|
+
"slug": "empty",
|
|
441
|
+
"files": [
|
|
442
|
+
{
|
|
443
|
+
"path": "src/components/ui/composed/empty/empty-state.tsx",
|
|
444
|
+
"exports": [
|
|
445
|
+
"EmptyState"
|
|
446
|
+
]
|
|
447
|
+
}
|
|
448
|
+
],
|
|
449
|
+
"dependencies": {
|
|
450
|
+
"shadcn": [
|
|
451
|
+
"empty"
|
|
452
|
+
],
|
|
453
|
+
"npm": [],
|
|
454
|
+
"local": [
|
|
455
|
+
"@/lib/utils"
|
|
456
|
+
]
|
|
457
|
+
},
|
|
458
|
+
"i18n": {
|
|
459
|
+
"required": false,
|
|
460
|
+
"keys": []
|
|
461
|
+
}
|
|
462
|
+
},
|
|
463
|
+
{
|
|
464
|
+
"name": "labeled-input",
|
|
465
|
+
"type": "composed",
|
|
466
|
+
"slug": "input",
|
|
467
|
+
"files": [
|
|
468
|
+
{
|
|
469
|
+
"path": "src/components/ui/composed/input/labeled-input.tsx",
|
|
470
|
+
"exports": [
|
|
471
|
+
"LabeledInput"
|
|
472
|
+
]
|
|
473
|
+
}
|
|
474
|
+
],
|
|
475
|
+
"dependencies": {
|
|
476
|
+
"shadcn": [
|
|
477
|
+
"input",
|
|
478
|
+
"label"
|
|
479
|
+
],
|
|
480
|
+
"npm": [],
|
|
481
|
+
"local": [
|
|
482
|
+
"@/lib/utils"
|
|
483
|
+
]
|
|
484
|
+
},
|
|
485
|
+
"i18n": {
|
|
486
|
+
"required": false,
|
|
487
|
+
"keys": []
|
|
488
|
+
},
|
|
489
|
+
"controller": "InputFieldController",
|
|
490
|
+
"knownIssues": [
|
|
491
|
+
"labeled-input.tsx imports `type InputProps` from '@/components/ui/input', but src/components/ui/input.tsx only exports `Input` (no InputProps type). This is a pre-existing type error in the boilerplate itself (reproduced by `bun run type-check`), not something introduced by the registry. A CLI copying this component will carry the same error until the source file is fixed upstream."
|
|
492
|
+
]
|
|
493
|
+
},
|
|
494
|
+
{
|
|
495
|
+
"name": "search-input",
|
|
496
|
+
"type": "composed",
|
|
497
|
+
"slug": "input-group",
|
|
498
|
+
"files": [
|
|
499
|
+
{
|
|
500
|
+
"path": "src/components/ui/composed/input-group/search-input.tsx",
|
|
501
|
+
"exports": [
|
|
502
|
+
"SearchInput"
|
|
503
|
+
]
|
|
504
|
+
}
|
|
505
|
+
],
|
|
506
|
+
"dependencies": {
|
|
507
|
+
"shadcn": [
|
|
508
|
+
"input-group",
|
|
509
|
+
"button",
|
|
510
|
+
"input",
|
|
511
|
+
"textarea"
|
|
512
|
+
],
|
|
513
|
+
"npm": [
|
|
514
|
+
"lucide-react"
|
|
515
|
+
],
|
|
516
|
+
"local": [
|
|
517
|
+
"@/lib/utils"
|
|
518
|
+
]
|
|
519
|
+
},
|
|
520
|
+
"i18n": {
|
|
521
|
+
"required": true,
|
|
522
|
+
"keys": [
|
|
523
|
+
"ui_clear_search",
|
|
524
|
+
"ui_search_placeholder"
|
|
525
|
+
]
|
|
526
|
+
}
|
|
527
|
+
},
|
|
528
|
+
{
|
|
529
|
+
"name": "otp-field",
|
|
530
|
+
"type": "composed",
|
|
531
|
+
"slug": "input-otp",
|
|
532
|
+
"files": [
|
|
533
|
+
{
|
|
534
|
+
"path": "src/components/ui/composed/input-otp/otp-field.tsx",
|
|
535
|
+
"exports": [
|
|
536
|
+
"OtpField"
|
|
537
|
+
]
|
|
538
|
+
}
|
|
539
|
+
],
|
|
540
|
+
"dependencies": {
|
|
541
|
+
"shadcn": [
|
|
542
|
+
"input-otp",
|
|
543
|
+
"label"
|
|
544
|
+
],
|
|
545
|
+
"npm": [
|
|
546
|
+
"input-otp"
|
|
547
|
+
],
|
|
548
|
+
"local": [
|
|
549
|
+
"@/lib/utils"
|
|
550
|
+
]
|
|
551
|
+
},
|
|
552
|
+
"i18n": {
|
|
553
|
+
"required": false,
|
|
554
|
+
"keys": []
|
|
555
|
+
},
|
|
556
|
+
"controller": "OtpFieldController"
|
|
557
|
+
},
|
|
558
|
+
{
|
|
559
|
+
"name": "site-nav",
|
|
560
|
+
"type": "composed",
|
|
561
|
+
"slug": "navigation-menu",
|
|
562
|
+
"files": [
|
|
563
|
+
{
|
|
564
|
+
"path": "src/components/ui/composed/navigation-menu/site-nav.tsx",
|
|
565
|
+
"exports": [
|
|
566
|
+
"SiteNav"
|
|
567
|
+
]
|
|
568
|
+
}
|
|
569
|
+
],
|
|
570
|
+
"dependencies": {
|
|
571
|
+
"shadcn": [
|
|
572
|
+
"navigation-menu"
|
|
573
|
+
],
|
|
574
|
+
"npm": [],
|
|
575
|
+
"local": [
|
|
576
|
+
"@/lib/utils"
|
|
577
|
+
]
|
|
578
|
+
},
|
|
579
|
+
"i18n": {
|
|
580
|
+
"required": false,
|
|
581
|
+
"keys": []
|
|
582
|
+
}
|
|
583
|
+
},
|
|
584
|
+
{
|
|
585
|
+
"name": "data-pagination",
|
|
586
|
+
"type": "composed",
|
|
587
|
+
"slug": "pagination",
|
|
588
|
+
"files": [
|
|
589
|
+
{
|
|
590
|
+
"path": "src/components/ui/composed/pagination/data-pagination.tsx",
|
|
591
|
+
"exports": [
|
|
592
|
+
"DataPagination"
|
|
593
|
+
]
|
|
594
|
+
}
|
|
595
|
+
],
|
|
596
|
+
"dependencies": {
|
|
597
|
+
"shadcn": [
|
|
598
|
+
"pagination",
|
|
599
|
+
"button"
|
|
600
|
+
],
|
|
601
|
+
"npm": [],
|
|
602
|
+
"local": [
|
|
603
|
+
"@/lib/utils"
|
|
604
|
+
]
|
|
605
|
+
},
|
|
606
|
+
"i18n": {
|
|
607
|
+
"required": true,
|
|
608
|
+
"keys": [
|
|
609
|
+
"ui_next",
|
|
610
|
+
"ui_next_page",
|
|
611
|
+
"ui_previous",
|
|
612
|
+
"ui_previous_page"
|
|
613
|
+
]
|
|
614
|
+
}
|
|
615
|
+
},
|
|
616
|
+
{
|
|
617
|
+
"name": "info-popover",
|
|
618
|
+
"type": "composed",
|
|
619
|
+
"slug": "popover",
|
|
620
|
+
"files": [
|
|
621
|
+
{
|
|
622
|
+
"path": "src/components/ui/composed/popover/info-popover.tsx",
|
|
623
|
+
"exports": [
|
|
624
|
+
"InfoPopover"
|
|
625
|
+
]
|
|
626
|
+
}
|
|
627
|
+
],
|
|
628
|
+
"dependencies": {
|
|
629
|
+
"shadcn": [
|
|
630
|
+
"popover",
|
|
631
|
+
"button"
|
|
632
|
+
],
|
|
633
|
+
"npm": [
|
|
634
|
+
"lucide-react"
|
|
635
|
+
],
|
|
636
|
+
"local": [
|
|
637
|
+
"@/lib/utils"
|
|
638
|
+
]
|
|
639
|
+
},
|
|
640
|
+
"i18n": {
|
|
641
|
+
"required": true,
|
|
642
|
+
"keys": [
|
|
643
|
+
"ui_more_information"
|
|
644
|
+
]
|
|
645
|
+
}
|
|
646
|
+
},
|
|
647
|
+
{
|
|
648
|
+
"name": "options-radio-group",
|
|
649
|
+
"type": "composed",
|
|
650
|
+
"slug": "radio-group",
|
|
651
|
+
"files": [
|
|
652
|
+
{
|
|
653
|
+
"path": "src/components/ui/composed/radio-group/options-radio-group.tsx",
|
|
654
|
+
"exports": [
|
|
655
|
+
"OptionsRadioGroup"
|
|
656
|
+
]
|
|
657
|
+
}
|
|
658
|
+
],
|
|
659
|
+
"dependencies": {
|
|
660
|
+
"shadcn": [
|
|
661
|
+
"radio-group",
|
|
662
|
+
"label"
|
|
663
|
+
],
|
|
664
|
+
"npm": [],
|
|
665
|
+
"local": [
|
|
666
|
+
"@/lib/utils"
|
|
667
|
+
]
|
|
668
|
+
},
|
|
669
|
+
"i18n": {
|
|
670
|
+
"required": false,
|
|
671
|
+
"keys": []
|
|
672
|
+
},
|
|
673
|
+
"controller": "RadioGroupController"
|
|
674
|
+
},
|
|
675
|
+
{
|
|
676
|
+
"name": "labeled-select",
|
|
677
|
+
"type": "composed",
|
|
678
|
+
"slug": "select",
|
|
679
|
+
"files": [
|
|
680
|
+
{
|
|
681
|
+
"path": "src/components/ui/composed/select/labeled-select.tsx",
|
|
682
|
+
"exports": [
|
|
683
|
+
"LabeledSelect"
|
|
684
|
+
]
|
|
685
|
+
}
|
|
686
|
+
],
|
|
687
|
+
"dependencies": {
|
|
688
|
+
"shadcn": [
|
|
689
|
+
"select",
|
|
690
|
+
"label"
|
|
691
|
+
],
|
|
692
|
+
"npm": [],
|
|
693
|
+
"local": [
|
|
694
|
+
"@/lib/utils"
|
|
695
|
+
]
|
|
696
|
+
},
|
|
697
|
+
"i18n": {
|
|
698
|
+
"required": true,
|
|
699
|
+
"keys": [
|
|
700
|
+
"ui_select_placeholder"
|
|
701
|
+
]
|
|
702
|
+
},
|
|
703
|
+
"controller": "SelectController"
|
|
704
|
+
},
|
|
705
|
+
{
|
|
706
|
+
"name": "select-groups",
|
|
707
|
+
"type": "composed",
|
|
708
|
+
"slug": "select",
|
|
709
|
+
"files": [
|
|
710
|
+
{
|
|
711
|
+
"path": "src/components/ui/composed/select/select-groups.tsx",
|
|
712
|
+
"exports": [
|
|
713
|
+
"SelectGroups"
|
|
714
|
+
]
|
|
715
|
+
}
|
|
716
|
+
],
|
|
717
|
+
"dependencies": {
|
|
718
|
+
"shadcn": [
|
|
719
|
+
"select",
|
|
720
|
+
"label"
|
|
721
|
+
],
|
|
722
|
+
"npm": [],
|
|
723
|
+
"local": [
|
|
724
|
+
"@/lib/utils"
|
|
725
|
+
]
|
|
726
|
+
},
|
|
727
|
+
"i18n": {
|
|
728
|
+
"required": true,
|
|
729
|
+
"keys": [
|
|
730
|
+
"ui_select_placeholder"
|
|
731
|
+
]
|
|
732
|
+
},
|
|
733
|
+
"controller": "SelectGroupsController"
|
|
734
|
+
},
|
|
735
|
+
{
|
|
736
|
+
"name": "filter-sheet",
|
|
737
|
+
"type": "composed",
|
|
738
|
+
"slug": "sheet",
|
|
739
|
+
"files": [
|
|
740
|
+
{
|
|
741
|
+
"path": "src/components/ui/composed/sheet/filter-sheet.tsx",
|
|
742
|
+
"exports": [
|
|
743
|
+
"FilterSheet"
|
|
744
|
+
]
|
|
745
|
+
}
|
|
746
|
+
],
|
|
747
|
+
"dependencies": {
|
|
748
|
+
"shadcn": [
|
|
749
|
+
"sheet"
|
|
750
|
+
],
|
|
751
|
+
"npm": [],
|
|
752
|
+
"local": []
|
|
753
|
+
},
|
|
754
|
+
"i18n": {
|
|
755
|
+
"required": false,
|
|
756
|
+
"keys": []
|
|
757
|
+
}
|
|
758
|
+
},
|
|
759
|
+
{
|
|
760
|
+
"name": "card-skeleton",
|
|
761
|
+
"type": "composed",
|
|
762
|
+
"slug": "skeleton",
|
|
763
|
+
"files": [
|
|
764
|
+
{
|
|
765
|
+
"path": "src/components/ui/composed/skeleton/card-skeleton.tsx",
|
|
766
|
+
"exports": [
|
|
767
|
+
"CardSkeleton"
|
|
768
|
+
]
|
|
769
|
+
}
|
|
770
|
+
],
|
|
771
|
+
"dependencies": {
|
|
772
|
+
"shadcn": [
|
|
773
|
+
"skeleton",
|
|
774
|
+
"card"
|
|
775
|
+
],
|
|
776
|
+
"npm": [],
|
|
777
|
+
"local": [
|
|
778
|
+
"@/lib/utils"
|
|
779
|
+
]
|
|
780
|
+
},
|
|
781
|
+
"i18n": {
|
|
782
|
+
"required": false,
|
|
783
|
+
"keys": []
|
|
784
|
+
}
|
|
785
|
+
},
|
|
786
|
+
{
|
|
787
|
+
"name": "simple-data-table",
|
|
788
|
+
"type": "composed",
|
|
789
|
+
"slug": "table",
|
|
790
|
+
"files": [
|
|
791
|
+
{
|
|
792
|
+
"path": "src/components/ui/composed/table/simple-data-table.tsx",
|
|
793
|
+
"exports": [
|
|
794
|
+
"SimpleDataTable"
|
|
795
|
+
]
|
|
796
|
+
}
|
|
797
|
+
],
|
|
798
|
+
"dependencies": {
|
|
799
|
+
"shadcn": [
|
|
800
|
+
"table"
|
|
801
|
+
],
|
|
802
|
+
"npm": [],
|
|
803
|
+
"local": [
|
|
804
|
+
"@/lib/utils"
|
|
805
|
+
]
|
|
806
|
+
},
|
|
807
|
+
"i18n": {
|
|
808
|
+
"required": true,
|
|
809
|
+
"keys": [
|
|
810
|
+
"ui_no_results"
|
|
811
|
+
]
|
|
812
|
+
}
|
|
813
|
+
},
|
|
814
|
+
{
|
|
815
|
+
"name": "icon-tabs",
|
|
816
|
+
"type": "composed",
|
|
817
|
+
"slug": "tabs",
|
|
818
|
+
"files": [
|
|
819
|
+
{
|
|
820
|
+
"path": "src/components/ui/composed/tabs/icon-tabs.tsx",
|
|
821
|
+
"exports": [
|
|
822
|
+
"IconTabs"
|
|
823
|
+
]
|
|
824
|
+
}
|
|
825
|
+
],
|
|
826
|
+
"dependencies": {
|
|
827
|
+
"shadcn": [
|
|
828
|
+
"tabs"
|
|
829
|
+
],
|
|
830
|
+
"npm": [],
|
|
831
|
+
"local": [
|
|
832
|
+
"@/lib/utils"
|
|
833
|
+
]
|
|
834
|
+
},
|
|
835
|
+
"i18n": {
|
|
836
|
+
"required": false,
|
|
837
|
+
"keys": []
|
|
838
|
+
}
|
|
839
|
+
},
|
|
840
|
+
{
|
|
841
|
+
"name": "labeled-textarea",
|
|
842
|
+
"type": "composed",
|
|
843
|
+
"slug": "textarea",
|
|
844
|
+
"files": [
|
|
845
|
+
{
|
|
846
|
+
"path": "src/components/ui/composed/textarea/labeled-textarea.tsx",
|
|
847
|
+
"exports": [
|
|
848
|
+
"LabeledTextarea"
|
|
849
|
+
]
|
|
850
|
+
}
|
|
851
|
+
],
|
|
852
|
+
"dependencies": {
|
|
853
|
+
"shadcn": [
|
|
854
|
+
"textarea",
|
|
855
|
+
"label"
|
|
856
|
+
],
|
|
857
|
+
"npm": [],
|
|
858
|
+
"local": [
|
|
859
|
+
"@/lib/utils"
|
|
860
|
+
]
|
|
861
|
+
},
|
|
862
|
+
"i18n": {
|
|
863
|
+
"required": false,
|
|
864
|
+
"keys": []
|
|
865
|
+
},
|
|
866
|
+
"controller": "TextareaController"
|
|
867
|
+
},
|
|
868
|
+
{
|
|
869
|
+
"name": "icon-tooltip",
|
|
870
|
+
"type": "composed",
|
|
871
|
+
"slug": "tooltip",
|
|
872
|
+
"files": [
|
|
873
|
+
{
|
|
874
|
+
"path": "src/components/ui/composed/tooltip/icon-tooltip.tsx",
|
|
875
|
+
"exports": [
|
|
876
|
+
"IconTooltip"
|
|
877
|
+
]
|
|
878
|
+
}
|
|
879
|
+
],
|
|
880
|
+
"dependencies": {
|
|
881
|
+
"shadcn": [
|
|
882
|
+
"tooltip"
|
|
883
|
+
],
|
|
884
|
+
"npm": [],
|
|
885
|
+
"local": [
|
|
886
|
+
"@/lib/utils"
|
|
887
|
+
]
|
|
888
|
+
},
|
|
889
|
+
"i18n": {
|
|
890
|
+
"required": false,
|
|
891
|
+
"keys": []
|
|
892
|
+
}
|
|
893
|
+
},
|
|
894
|
+
{
|
|
895
|
+
"name": "file-dropzone",
|
|
896
|
+
"type": "composed",
|
|
897
|
+
"slug": "upload",
|
|
898
|
+
"files": [
|
|
899
|
+
{
|
|
900
|
+
"path": "src/components/ui/composed/upload/file-dropzone.tsx",
|
|
901
|
+
"exports": [
|
|
902
|
+
"FileDropzone"
|
|
903
|
+
]
|
|
904
|
+
}
|
|
905
|
+
],
|
|
906
|
+
"dependencies": {
|
|
907
|
+
"shadcn": [
|
|
908
|
+
"button",
|
|
909
|
+
"input",
|
|
910
|
+
"label"
|
|
911
|
+
],
|
|
912
|
+
"npm": [
|
|
913
|
+
"lucide-react"
|
|
914
|
+
],
|
|
915
|
+
"local": [
|
|
916
|
+
"@/lib/utils"
|
|
917
|
+
]
|
|
918
|
+
},
|
|
919
|
+
"i18n": {
|
|
920
|
+
"required": true,
|
|
921
|
+
"keys": [
|
|
922
|
+
"ui_browse_files",
|
|
923
|
+
"ui_dropzone_prompt",
|
|
924
|
+
"ui_max_size",
|
|
925
|
+
"ui_remove_file"
|
|
926
|
+
]
|
|
927
|
+
},
|
|
928
|
+
"controller": "FileDropzoneController"
|
|
929
|
+
}
|
|
930
|
+
],
|
|
931
|
+
"files": {
|
|
932
|
+
"src/components/ui/composed/accordion/faq-accordion.tsx": "import * as React from 'react';\r\nimport {\r\n Accordion,\r\n AccordionContent,\r\n AccordionItem,\r\n AccordionTrigger,\r\n} from '@/components/ui/accordion';\r\nimport { cn } from '@/lib/utils';\r\n\r\ntype FaqItem = {\r\n id?: string;\r\n question: string;\r\n answer: React.ReactNode;\r\n};\r\n\r\ntype FaqAccordionProps = {\r\n items: FaqItem[];\r\n defaultValue?: string;\r\n className?: string;\r\n};\r\n\r\nexport function FaqAccordion({\r\n items,\r\n defaultValue,\r\n className,\r\n}: FaqAccordionProps) {\r\n return (\r\n <Accordion\r\n type='single'\r\n collapsible\r\n defaultValue={\r\n defaultValue ??\r\n (items.length > 0 ? (items[0].id ?? 'faq-0') : undefined)\r\n }\r\n className={cn('w-full', className)}\r\n >\r\n {items.map((item, index) => {\r\n const value = item.id ?? `faq-${index}`;\r\n return (\r\n <AccordionItem key={value} value={value}>\r\n <AccordionTrigger>{item.question}</AccordionTrigger>\r\n <AccordionContent>{item.answer}</AccordionContent>\r\n </AccordionItem>\r\n );\r\n })}\r\n </Accordion>\r\n );\r\n}\r\n",
|
|
933
|
+
"src/components/ui/composed/alert/error-alert.tsx": "import type { ReactNode } from 'react';\r\nimport { CircleAlertIcon } from 'lucide-react';\r\n\r\nimport {\r\n Alert,\r\n AlertAction,\r\n AlertDescription,\r\n AlertTitle,\r\n} from '@/components/ui/alert';\r\nimport { cn } from '@/lib/utils';\r\n\r\ntype ErrorAlertProps = {\r\n title: string;\r\n description?: string;\r\n action?: ReactNode;\r\n className?: string;\r\n};\r\n\r\nexport function ErrorAlert({\r\n title,\r\n description,\r\n action,\r\n className,\r\n}: ErrorAlertProps) {\r\n return (\r\n <Alert variant='destructive' className={cn('w-full', className)}>\r\n <CircleAlertIcon />\r\n <AlertTitle>{title}</AlertTitle>\r\n {description ? <AlertDescription>{description}</AlertDescription> : null}\r\n {action ? <AlertAction>{action}</AlertAction> : null}\r\n </Alert>\r\n );\r\n}\r\n",
|
|
934
|
+
"src/components/ui/composed/avatar/user-avatar.tsx": "import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar';\r\nimport { cn } from '@/lib/utils';\r\n\r\nconst sizeClasses = {\r\n sm: 'size-6 text-xs',\r\n md: 'size-8 text-sm',\r\n lg: 'size-12 text-base',\r\n xl: 'size-16 text-lg',\r\n} as const;\r\n\r\ntype UserAvatarProps = {\r\n src?: string;\r\n name: string;\r\n size?: keyof typeof sizeClasses;\r\n className?: string;\r\n};\r\n\r\nfunction getInitials(name: string): string {\r\n return name\r\n .trim()\r\n .split(/\\s+/)\r\n .map(n => n[0])\r\n .slice(0, 2)\r\n .join('')\r\n .toUpperCase();\r\n}\r\n\r\nexport function UserAvatar({\r\n src,\r\n name,\r\n size = 'md',\r\n className,\r\n}: UserAvatarProps) {\r\n return (\r\n <Avatar className={cn(sizeClasses[size], className)}>\r\n <AvatarImage src={src} alt={name} />\r\n <AvatarFallback>{getInitials(name)}</AvatarFallback>\r\n </Avatar>\r\n );\r\n}\r\n",
|
|
935
|
+
"src/components/ui/composed/badge/status-badge.tsx": "import * as React from 'react';\r\nimport { Badge } from '@/components/ui/badge';\r\nimport { cn } from '@/lib/utils';\r\n\r\nconst statusStyles = {\r\n success:\r\n 'border-green-200 bg-green-50 text-green-700 dark:border-green-800 dark:bg-green-950 dark:text-green-300',\r\n warning:\r\n 'border-yellow-200 bg-yellow-50 text-yellow-700 dark:border-yellow-800 dark:bg-yellow-950 dark:text-yellow-300',\r\n error:\r\n 'border-red-200 bg-red-50 text-red-700 dark:border-red-800 dark:bg-red-950 dark:text-red-300',\r\n info: 'border-blue-200 bg-blue-50 text-blue-700 dark:border-blue-800 dark:bg-blue-950 dark:text-blue-300',\r\n neutral: '',\r\n} as const;\r\n\r\ntype StatusBadgeProps = {\r\n status: keyof typeof statusStyles;\r\n label: string;\r\n icon?: React.ReactNode;\r\n className?: string;\r\n};\r\n\r\nexport function StatusBadge({\r\n status,\r\n label,\r\n icon,\r\n className,\r\n}: StatusBadgeProps) {\r\n return (\r\n <Badge variant='outline' className={cn(statusStyles[status], className)}>\r\n {icon}\r\n {label}\r\n </Badge>\r\n );\r\n}\r\n",
|
|
936
|
+
"src/components/ui/composed/button/loading-button.tsx": "import type * as React from 'react';\r\nimport { Button } from '@/components/ui/button';\r\nimport { Loader2Icon } from 'lucide-react';\r\n\r\ntype LoadingButtonProps = React.ComponentProps<typeof Button> & {\r\n loading?: boolean;\r\n loadingText?: string;\r\n};\r\n\r\nexport function LoadingButton({\r\n loading = false,\r\n loadingText,\r\n children,\r\n disabled,\r\n ...props\r\n}: LoadingButtonProps) {\r\n return (\r\n <Button disabled={disabled || loading} {...props}>\r\n {loading && <Loader2Icon className='animate-spin' />}\r\n {loading && loadingText !== undefined ? loadingText : children}\r\n </Button>\r\n );\r\n}\r\n",
|
|
937
|
+
"src/components/ui/composed/button/theme-toggle.tsx": "import { MoonIcon, SunIcon } from 'lucide-react';\r\nimport { useTheme } from 'next-themes';\r\n\r\nimport { Button } from '@/components/ui/button';\r\n\r\nimport { m } from '#/paraglide/messages';\r\n\r\nexport function ThemeToggle() {\r\n const { setTheme } = useTheme();\r\n\r\n return (\r\n <Button\r\n type='button'\r\n variant='ghost'\r\n size='icon'\r\n aria-label={m.ui_toggle_theme()}\r\n onClick={() => {\r\n const isDark = document.documentElement.classList.contains('dark');\r\n setTheme(isDark ? 'light' : 'dark');\r\n }}\r\n >\r\n <SunIcon className='dark:hidden' />\r\n <MoonIcon className='hidden dark:block' />\r\n </Button>\r\n );\r\n}\r\n",
|
|
938
|
+
"src/components/ui/composed/card/stats-card.tsx": "import * as React from 'react';\r\nimport {\r\n Card,\r\n CardContent,\r\n CardDescription,\r\n CardHeader,\r\n CardTitle,\r\n} from '@/components/ui/card';\r\nimport { TrendingDownIcon, TrendingUpIcon } from 'lucide-react';\r\nimport { cn } from '@/lib/utils';\r\n\r\ntype Trend = {\r\n value: number;\r\n direction: 'up' | 'down';\r\n label?: string;\r\n};\r\n\r\ntype StatsCardProps = {\r\n title: string;\r\n value: string | number;\r\n description?: string;\r\n icon?: React.ReactNode;\r\n trend?: Trend;\r\n className?: string;\r\n};\r\n\r\nexport function StatsCard({\r\n title,\r\n value,\r\n description,\r\n icon,\r\n trend,\r\n className,\r\n}: StatsCardProps) {\r\n return (\r\n <Card className={className}>\r\n <CardHeader>\r\n <div className='flex items-start justify-between gap-4'>\r\n <CardTitle className='text-sm font-medium text-muted-foreground'>\r\n {title}\r\n </CardTitle>\r\n {icon && <span className='text-muted-foreground'>{icon}</span>}\r\n </div>\r\n <div className='text-2xl font-bold'>{value}</div>\r\n {trend && (\r\n <div\r\n className={cn(\r\n 'flex items-center gap-1 text-xs',\r\n trend.direction === 'up'\r\n ? 'text-green-600 dark:text-green-400'\r\n : 'text-red-600 dark:text-red-400'\r\n )}\r\n >\r\n {trend.direction === 'up' ? (\r\n <TrendingUpIcon className='size-3.5' />\r\n ) : (\r\n <TrendingDownIcon className='size-3.5' />\r\n )}\r\n <span>{trend.value}%</span>\r\n {trend.label && (\r\n <span className='text-muted-foreground'>{trend.label}</span>\r\n )}\r\n </div>\r\n )}\r\n </CardHeader>\r\n {description && (\r\n <CardContent>\r\n <CardDescription>{description}</CardDescription>\r\n </CardContent>\r\n )}\r\n </Card>\r\n );\r\n}\r\n",
|
|
939
|
+
"src/components/ui/composed/carousel/media-carousel.tsx": "import * as React from 'react';\r\nimport {\r\n Carousel,\r\n CarouselContent,\r\n CarouselItem,\r\n CarouselNext,\r\n CarouselPrevious,\r\n} from '@/components/ui/carousel';\r\nimport { cn } from '@/lib/utils';\r\n\r\ntype MediaCarouselProps = {\r\n items: React.ReactNode[];\r\n itemClassName?: string;\r\n orientation?: 'horizontal' | 'vertical';\r\n loop?: boolean;\r\n className?: string;\r\n};\r\n\r\nexport function MediaCarousel({\r\n items,\r\n itemClassName,\r\n orientation = 'horizontal',\r\n loop = false,\r\n className,\r\n}: MediaCarouselProps) {\r\n return (\r\n <Carousel\r\n orientation={orientation}\r\n opts={{ loop }}\r\n className={cn('w-full', className)}\r\n >\r\n <CarouselContent>\r\n {items.map((item, index) => (\r\n <CarouselItem key={index} className={itemClassName}>\r\n {item}\r\n </CarouselItem>\r\n ))}\r\n </CarouselContent>\r\n <CarouselPrevious />\r\n <CarouselNext />\r\n </Carousel>\r\n );\r\n}\r\n",
|
|
940
|
+
"src/components/ui/composed/checkbox/checkbox-field.tsx": "import * as React from 'react';\r\n\r\nimport { Checkbox } from '@/components/ui/checkbox';\r\nimport { Label } from '@/components/ui/label';\r\nimport { cn } from '@/lib/utils';\r\n\r\ntype CheckboxFieldProps = {\r\n label: string;\r\n description?: string;\r\n error?: string;\r\n required?: boolean;\r\n id?: string;\r\n checked?: boolean | 'indeterminate';\r\n defaultChecked?: boolean;\r\n onCheckedChange?: (checked: boolean) => void;\r\n disabled?: boolean;\r\n name?: string;\r\n className?: string;\r\n};\r\n\r\nexport function CheckboxField({\r\n label,\r\n description,\r\n error,\r\n required,\r\n id: externalId,\r\n className,\r\n onCheckedChange,\r\n ...checkboxProps\r\n}: CheckboxFieldProps) {\r\n const generatedId = React.useId();\r\n const id = externalId ?? generatedId;\r\n\r\n return (\r\n <div className={cn('flex items-start gap-2', className)}>\r\n <Checkbox\r\n id={id}\r\n onCheckedChange={v => onCheckedChange?.(v === true)}\r\n aria-invalid={!!error}\r\n {...checkboxProps}\r\n />\r\n <div className='flex flex-col gap-0.5'>\r\n <Label\r\n htmlFor={id}\r\n className={cn(checkboxProps.disabled && 'opacity-50')}\r\n >\r\n {label}\r\n {required && <span className='text-destructive ms-0.5'>*</span>}\r\n </Label>\r\n {description && (\r\n <p className='text-muted-foreground text-xs'>{description}</p>\r\n )}\r\n {error && (\r\n <p role='alert' className='text-destructive text-sm'>\r\n {error}\r\n </p>\r\n )}\r\n </div>\r\n </div>\r\n );\r\n}\r\n",
|
|
941
|
+
"src/components/ui/composed/checkbox/checkbox-group.tsx": "import * as React from 'react';\r\nimport { Checkbox } from '@/components/ui/checkbox';\r\nimport { Label } from '@/components/ui/label';\r\nimport { cn } from '@/lib/utils';\r\n\r\ntype CheckboxOption = {\r\n label: string;\r\n value: string;\r\n description?: string;\r\n disabled?: boolean;\r\n};\r\n\r\ntype CheckboxGroupProps = {\r\n options: CheckboxOption[];\r\n value: string[];\r\n onValueChange: (value: string[]) => void;\r\n label?: string;\r\n description?: string;\r\n error?: string;\r\n orientation?: 'vertical' | 'horizontal';\r\n className?: string;\r\n};\r\n\r\nexport function CheckboxGroup({\r\n options,\r\n value,\r\n onValueChange,\r\n label,\r\n description,\r\n error,\r\n orientation = 'vertical',\r\n className,\r\n}: CheckboxGroupProps) {\r\n const groupId = React.useId();\r\n\r\n const toggle = (optionValue: string) => {\r\n if (value.includes(optionValue)) {\r\n onValueChange(value.filter(v => v !== optionValue));\r\n } else {\r\n onValueChange([...value, optionValue]);\r\n }\r\n };\r\n\r\n return (\r\n <fieldset className={cn('flex flex-col gap-2', className)}>\r\n {label && (\r\n <legend className='mb-1 text-sm font-medium leading-none'>\r\n {label}\r\n </legend>\r\n )}\r\n {description && (\r\n <p className='text-muted-foreground text-sm'>{description}</p>\r\n )}\r\n <div\r\n className={cn(\r\n 'flex gap-3',\r\n orientation === 'vertical' ? 'flex-col' : 'flex-row flex-wrap'\r\n )}\r\n >\r\n {options.map(option => {\r\n const id = `${groupId}-${option.value}`;\r\n return (\r\n <div key={option.value} className='flex items-start gap-2'>\r\n <Checkbox\r\n id={id}\r\n checked={value.includes(option.value)}\r\n onCheckedChange={() => toggle(option.value)}\r\n disabled={option.disabled}\r\n className='mt-0.5'\r\n />\r\n <div className='flex flex-col gap-0.5'>\r\n <Label\r\n htmlFor={id}\r\n className={cn(option.disabled && 'opacity-50')}\r\n >\r\n {option.label}\r\n </Label>\r\n {option.description && (\r\n <span className='text-muted-foreground text-xs'>\r\n {option.description}\r\n </span>\r\n )}\r\n </div>\r\n </div>\r\n );\r\n })}\r\n </div>\r\n {error && (\r\n <p role='alert' className='text-destructive text-sm'>\r\n {error}\r\n </p>\r\n )}\r\n </fieldset>\r\n );\r\n}\r\n",
|
|
942
|
+
"src/components/ui/composed/combobox/searchable-select.tsx": "import * as React from 'react';\r\n\r\nimport { CheckIcon, ChevronsUpDownIcon } from 'lucide-react';\r\n\r\nimport { Button } from '@/components/ui/button';\r\nimport {\r\n Command,\r\n CommandEmpty,\r\n CommandGroup,\r\n CommandInput,\r\n CommandItem,\r\n CommandList,\r\n} from '@/components/ui/command';\r\nimport { Label } from '@/components/ui/label';\r\nimport {\r\n Popover,\r\n PopoverContent,\r\n PopoverTrigger,\r\n} from '@/components/ui/popover';\r\nimport { cn } from '@/lib/utils';\r\n\r\nimport { m } from '#/paraglide/messages';\r\n\r\ntype ComboboxOption = {\r\n value: string;\r\n label: string;\r\n disabled?: boolean;\r\n};\r\n\r\ntype SearchableSelectProps = {\r\n options: ComboboxOption[];\r\n value?: string;\r\n onValueChange?: (value: string) => void;\r\n label?: string;\r\n description?: string;\r\n error?: string;\r\n required?: boolean;\r\n placeholder?: string;\r\n searchPlaceholder?: string;\r\n emptyMessage?: string;\r\n disabled?: boolean;\r\n className?: string;\r\n};\r\n\r\nexport function SearchableSelect({\r\n options,\r\n value,\r\n onValueChange,\r\n label,\r\n description,\r\n error,\r\n required,\r\n placeholder = m.ui_select_placeholder(),\r\n searchPlaceholder = m.ui_search_placeholder(),\r\n emptyMessage = m.ui_no_results(),\r\n disabled,\r\n className,\r\n}: SearchableSelectProps) {\r\n const id = React.useId();\r\n const [open, setOpen] = React.useState(false);\r\n\r\n const selected = options.find(o => o.value === value);\r\n\r\n return (\r\n <div className={cn('flex flex-col gap-1.5', className)}>\r\n {label && (\r\n <Label htmlFor={id}>\r\n {label}\r\n {required && <span className='text-destructive ms-0.5'>*</span>}\r\n </Label>\r\n )}\r\n {description && (\r\n <p className='text-muted-foreground text-sm'>{description}</p>\r\n )}\r\n <Popover open={open} onOpenChange={setOpen}>\r\n <PopoverTrigger asChild>\r\n <Button\r\n id={id}\r\n variant='outline'\r\n role='combobox'\r\n aria-expanded={open}\r\n aria-invalid={!!error}\r\n disabled={disabled}\r\n className='w-full justify-between font-normal'\r\n >\r\n {selected ? (\r\n selected.label\r\n ) : (\r\n <span className='text-muted-foreground'>{placeholder}</span>\r\n )}\r\n <ChevronsUpDownIcon className='ms-2 size-4 shrink-0 opacity-50' />\r\n </Button>\r\n </PopoverTrigger>\r\n <PopoverContent\r\n className='w-[--radix-popover-trigger-width] p-0'\r\n align='start'\r\n >\r\n <Command>\r\n <CommandInput placeholder={searchPlaceholder} />\r\n <CommandList>\r\n <CommandEmpty>{emptyMessage}</CommandEmpty>\r\n <CommandGroup>\r\n {options.map(option => (\r\n <CommandItem\r\n key={option.value}\r\n value={option.value}\r\n disabled={option.disabled}\r\n onSelect={current => {\r\n onValueChange?.(current === value ? '' : current);\r\n setOpen(false);\r\n }}\r\n >\r\n <CheckIcon\r\n className={cn(\r\n 'me-2 size-4',\r\n value === option.value ? 'opacity-100' : 'opacity-0'\r\n )}\r\n />\r\n {option.label}\r\n </CommandItem>\r\n ))}\r\n </CommandGroup>\r\n </CommandList>\r\n </Command>\r\n </PopoverContent>\r\n </Popover>\r\n {error && (\r\n <p role='alert' className='text-destructive text-sm'>\r\n {error}\r\n </p>\r\n )}\r\n </div>\r\n );\r\n}\r\n",
|
|
943
|
+
"src/components/ui/composed/date-picker/date-picker-field.tsx": "'use client';\r\n\r\nimport * as React from 'react';\r\nimport type { Matcher } from 'react-day-picker';\r\n\r\nimport { CalendarIcon } from 'lucide-react';\r\n\r\nimport { Button } from '@/components/ui/button';\r\nimport { Calendar } from '@/components/ui/calendar';\r\nimport { Label } from '@/components/ui/label';\r\nimport {\r\n Popover,\r\n PopoverContent,\r\n PopoverTrigger,\r\n} from '@/components/ui/popover';\r\nimport { cn } from '@/lib/utils';\r\n\r\nimport { m } from '#/paraglide/messages';\r\n\r\ntype DatePickerFieldProps = {\r\n value?: Date;\r\n onValueChange?: (date: Date | undefined) => void;\r\n label?: string;\r\n description?: string;\r\n error?: string;\r\n required?: boolean;\r\n placeholder?: string;\r\n disabled?: Matcher | Matcher[];\r\n id?: string;\r\n className?: string;\r\n};\r\n\r\nfunction formatDate(date: Date): string {\r\n return date.toLocaleDateString(undefined, {\r\n year: 'numeric',\r\n month: 'long',\r\n day: 'numeric',\r\n });\r\n}\r\n\r\nexport function DatePickerField({\r\n value,\r\n onValueChange,\r\n label,\r\n description,\r\n error,\r\n required,\r\n placeholder = m.ui_pick_date(),\r\n disabled,\r\n id: externalId,\r\n className,\r\n}: DatePickerFieldProps) {\r\n const generatedId = React.useId();\r\n const id = externalId ?? generatedId;\r\n const [open, setOpen] = React.useState(false);\r\n\r\n return (\r\n <div className={cn('flex flex-col gap-1.5', className)}>\r\n {label && (\r\n <Label htmlFor={id}>\r\n {label}\r\n {required && <span className='text-destructive ms-0.5'>*</span>}\r\n </Label>\r\n )}\r\n {description && (\r\n <p className='text-muted-foreground text-sm'>{description}</p>\r\n )}\r\n <Popover open={open} onOpenChange={setOpen}>\r\n <PopoverTrigger asChild>\r\n <Button\r\n id={id}\r\n variant='outline'\r\n aria-invalid={!!error}\r\n className='w-full justify-start text-start font-normal'\r\n >\r\n <CalendarIcon className='me-2 size-4' />\r\n {value ? (\r\n formatDate(value)\r\n ) : (\r\n <span className='text-muted-foreground'>{placeholder}</span>\r\n )}\r\n </Button>\r\n </PopoverTrigger>\r\n <PopoverContent className='w-auto p-0' align='start'>\r\n <Calendar\r\n mode='single'\r\n selected={value}\r\n onSelect={date => {\r\n onValueChange?.(date);\r\n setOpen(false);\r\n }}\r\n disabled={disabled}\r\n autoFocus\r\n />\r\n </PopoverContent>\r\n </Popover>\r\n {error && (\r\n <p role='alert' className='text-destructive text-sm'>\r\n {error}\r\n </p>\r\n )}\r\n </div>\r\n );\r\n}\r\n",
|
|
944
|
+
"src/components/ui/composed/dialog/confirmation-dialog.tsx": "import {\r\n Dialog,\r\n DialogContent,\r\n DialogDescription,\r\n DialogFooter,\r\n DialogHeader,\r\n DialogTitle,\r\n} from '@/components/ui/dialog';\r\nimport { Button } from '@/components/ui/button';\r\nimport { Loader2Icon } from 'lucide-react';\r\n\r\nimport { m } from '#/paraglide/messages';\r\n\r\ntype ConfirmationDialogProps = {\r\n open: boolean;\r\n onOpenChange: (open: boolean) => void;\r\n title: string;\r\n description?: string;\r\n confirmLabel?: string;\r\n cancelLabel?: string;\r\n onConfirm: () => void;\r\n onCancel?: () => void;\r\n variant?: 'default' | 'destructive';\r\n loading?: boolean;\r\n};\r\n\r\nexport function ConfirmationDialog({\r\n open,\r\n onOpenChange,\r\n title,\r\n description,\r\n confirmLabel = m.ui_confirm(),\r\n cancelLabel = m.ui_cancel(),\r\n onConfirm,\r\n onCancel,\r\n variant = 'default',\r\n loading = false,\r\n}: ConfirmationDialogProps) {\r\n const handleCancel = () => {\r\n onCancel?.();\r\n onOpenChange(false);\r\n };\r\n\r\n return (\r\n <Dialog open={open} onOpenChange={onOpenChange}>\r\n <DialogContent>\r\n <DialogHeader>\r\n <DialogTitle>{title}</DialogTitle>\r\n {description && <DialogDescription>{description}</DialogDescription>}\r\n </DialogHeader>\r\n <DialogFooter>\r\n <Button variant='outline' onClick={handleCancel} disabled={loading}>\r\n {cancelLabel}\r\n </Button>\r\n <Button variant={variant} onClick={onConfirm} disabled={loading}>\r\n {loading && <Loader2Icon className='animate-spin' />}\r\n {confirmLabel}\r\n </Button>\r\n </DialogFooter>\r\n </DialogContent>\r\n </Dialog>\r\n );\r\n}\r\n",
|
|
945
|
+
"src/components/ui/composed/drawer/action-drawer.tsx": "import * as React from 'react';\r\nimport {\r\n Drawer,\r\n DrawerContent,\r\n DrawerDescription,\r\n DrawerFooter,\r\n DrawerHeader,\r\n DrawerTitle,\r\n DrawerTrigger,\r\n} from '@/components/ui/drawer';\r\n\r\ntype ActionDrawerProps = {\r\n open?: boolean;\r\n onOpenChange?: (open: boolean) => void;\r\n trigger?: React.ReactNode;\r\n title: string;\r\n description?: string;\r\n children: React.ReactNode;\r\n footer?: React.ReactNode;\r\n direction?: 'bottom' | 'left' | 'right' | 'top';\r\n};\r\n\r\nexport function ActionDrawer({\r\n open,\r\n onOpenChange,\r\n trigger,\r\n title,\r\n description,\r\n children,\r\n footer,\r\n direction = 'bottom',\r\n}: ActionDrawerProps) {\r\n return (\r\n <Drawer open={open} onOpenChange={onOpenChange} direction={direction}>\r\n {trigger && <DrawerTrigger asChild>{trigger}</DrawerTrigger>}\r\n <DrawerContent>\r\n <DrawerHeader>\r\n <DrawerTitle>{title}</DrawerTitle>\r\n {description && <DrawerDescription>{description}</DrawerDescription>}\r\n </DrawerHeader>\r\n <div className='px-4 pb-2'>{children}</div>\r\n {footer && <DrawerFooter>{footer}</DrawerFooter>}\r\n </DrawerContent>\r\n </Drawer>\r\n );\r\n}\r\n",
|
|
946
|
+
"src/components/ui/composed/dropdown-menu/actions-dropdown.tsx": "import * as React from 'react';\r\nimport {\r\n DropdownMenu,\r\n DropdownMenuContent,\r\n DropdownMenuItem,\r\n DropdownMenuLabel,\r\n DropdownMenuSeparator,\r\n DropdownMenuTrigger,\r\n} from '@/components/ui/dropdown-menu';\r\nimport { Button } from '@/components/ui/button';\r\nimport { MoreHorizontalIcon } from 'lucide-react';\r\n\r\nimport { m } from '#/paraglide/messages';\r\n\r\ntype ActionItem = {\r\n label: string;\r\n icon?: React.ReactNode;\r\n onClick: () => void;\r\n disabled?: boolean;\r\n variant?: 'default' | 'destructive';\r\n};\r\n\r\ntype ActionGroup = {\r\n label?: string;\r\n items: ActionItem[];\r\n};\r\n\r\ntype ActionsDropdownProps = {\r\n groups: ActionGroup[];\r\n trigger?: React.ReactNode;\r\n triggerLabel?: string;\r\n align?: 'start' | 'center' | 'end';\r\n};\r\n\r\nexport function ActionsDropdown({\r\n groups,\r\n trigger,\r\n triggerLabel,\r\n align = 'end',\r\n}: ActionsDropdownProps) {\r\n return (\r\n <DropdownMenu>\r\n <DropdownMenuTrigger asChild>\r\n {trigger ?? (\r\n <Button\r\n variant='ghost'\r\n size='icon'\r\n aria-label={triggerLabel ?? m.ui_open_actions()}\r\n >\r\n <MoreHorizontalIcon />\r\n </Button>\r\n )}\r\n </DropdownMenuTrigger>\r\n <DropdownMenuContent align={align}>\r\n {groups.map((group, groupIndex) => (\r\n <React.Fragment key={groupIndex}>\r\n {groupIndex > 0 && <DropdownMenuSeparator />}\r\n {group.label && (\r\n <DropdownMenuLabel>{group.label}</DropdownMenuLabel>\r\n )}\r\n {group.items.map((item, itemIndex) => (\r\n <DropdownMenuItem\r\n key={itemIndex}\r\n onClick={item.onClick}\r\n disabled={item.disabled}\r\n variant={item.variant}\r\n >\r\n {item.icon}\r\n {item.label}\r\n </DropdownMenuItem>\r\n ))}\r\n </React.Fragment>\r\n ))}\r\n </DropdownMenuContent>\r\n </DropdownMenu>\r\n );\r\n}\r\n",
|
|
947
|
+
"src/components/ui/composed/empty/empty-state.tsx": "import type { ReactNode } from 'react';\r\n\r\nimport {\r\n Empty,\r\n EmptyContent,\r\n EmptyDescription,\r\n EmptyHeader,\r\n EmptyMedia,\r\n EmptyTitle,\r\n} from '@/components/ui/empty';\r\nimport { cn } from '@/lib/utils';\r\n\r\ntype EmptyStateProps = {\r\n title: string;\r\n description?: string;\r\n icon?: ReactNode;\r\n action?: ReactNode;\r\n className?: string;\r\n};\r\n\r\nexport function EmptyState({\r\n title,\r\n description,\r\n icon,\r\n action,\r\n className,\r\n}: EmptyStateProps) {\r\n return (\r\n <Empty className={cn('border', className)}>\r\n <EmptyHeader>\r\n {icon ? <EmptyMedia variant='icon'>{icon}</EmptyMedia> : null}\r\n <EmptyTitle>{title}</EmptyTitle>\r\n {description ? (\r\n <EmptyDescription>{description}</EmptyDescription>\r\n ) : null}\r\n </EmptyHeader>\r\n {action ? <EmptyContent>{action}</EmptyContent> : null}\r\n </Empty>\r\n );\r\n}\r\n",
|
|
948
|
+
"src/components/ui/composed/input/labeled-input.tsx": "import * as React from 'react';\r\nimport { Input, type InputProps } from '@/components/ui/input';\r\nimport { Label } from '@/components/ui/label';\r\nimport { cn } from '@/lib/utils';\r\n\r\n// Omit `error` from InputProps (which is boolean) so we can redefine it as\r\n// a string error message without a conflicting type intersection.\r\ntype LabeledInputProps = Omit<InputProps, 'error'> & {\r\n label?: string;\r\n description?: string;\r\n error?: string;\r\n required?: boolean;\r\n};\r\n\r\nexport function LabeledInput({\r\n label,\r\n description,\r\n error,\r\n required,\r\n id: externalId,\r\n className,\r\n ...props\r\n}: LabeledInputProps) {\r\n const generatedId = React.useId();\r\n const id = externalId ?? generatedId;\r\n const descId = description ? `${id}-desc` : undefined;\r\n const errId = error ? `${id}-err` : undefined;\r\n\r\n return (\r\n <div className={cn('flex flex-col gap-1.5', className)}>\r\n {label && (\r\n <Label htmlFor={id}>\r\n {label}\r\n {required && <span className='text-destructive ms-0.5'>*</span>}\r\n </Label>\r\n )}\r\n {description && (\r\n <p id={descId} className='text-muted-foreground text-sm'>\r\n {description}\r\n </p>\r\n )}\r\n <Input\r\n id={id}\r\n aria-describedby={\r\n [descId, errId].filter(Boolean).join(' ') || undefined\r\n }\r\n aria-invalid={!!error}\r\n error={!!error}\r\n {...props}\r\n />\r\n {error && (\r\n <p id={errId} role='alert' className='text-destructive text-sm'>\r\n {error}\r\n </p>\r\n )}\r\n </div>\r\n );\r\n}\r\n",
|
|
949
|
+
"src/components/ui/composed/input-group/search-input.tsx": "import { SearchIcon, XIcon } from 'lucide-react';\r\nimport {\r\n InputGroup,\r\n InputGroupAddon,\r\n InputGroupButton,\r\n InputGroupInput,\r\n} from '@/components/ui/input-group';\r\nimport { cn } from '@/lib/utils';\r\n\r\nimport { m } from '#/paraglide/messages';\r\n\r\ntype SearchInputProps = {\r\n value: string;\r\n onValueChange: (value: string) => void;\r\n placeholder?: string;\r\n clearable?: boolean;\r\n disabled?: boolean;\r\n className?: string;\r\n};\r\n\r\nexport function SearchInput({\r\n value,\r\n onValueChange,\r\n placeholder = m.ui_search_placeholder(),\r\n clearable = true,\r\n disabled,\r\n className,\r\n}: SearchInputProps) {\r\n return (\r\n <InputGroup className={cn('w-full', className)}>\r\n <InputGroupAddon align='inline-start'>\r\n <SearchIcon className='size-4' />\r\n </InputGroupAddon>\r\n <InputGroupInput\r\n value={value}\r\n onChange={e => onValueChange(e.target.value)}\r\n placeholder={placeholder}\r\n disabled={disabled}\r\n />\r\n {clearable && value && !disabled && (\r\n <InputGroupAddon align='inline-end'>\r\n <InputGroupButton\r\n onClick={() => onValueChange('')}\r\n aria-label={m.ui_clear_search()}\r\n >\r\n <XIcon />\r\n </InputGroupButton>\r\n </InputGroupAddon>\r\n )}\r\n </InputGroup>\r\n );\r\n}\r\n",
|
|
950
|
+
"src/components/ui/composed/input-otp/otp-field.tsx": "import * as React from 'react';\r\nimport {\r\n InputOTP,\r\n InputOTPGroup,\r\n InputOTPSeparator,\r\n InputOTPSlot,\r\n} from '@/components/ui/input-otp';\r\nimport { Label } from '@/components/ui/label';\r\nimport { cn } from '@/lib/utils';\r\n\r\ntype OtpFieldProps = {\r\n length?: 4 | 6;\r\n value: string;\r\n onChange: (value: string) => void;\r\n label?: string;\r\n error?: string;\r\n disabled?: boolean;\r\n className?: string;\r\n};\r\n\r\nexport function OtpField({\r\n length = 6,\r\n value,\r\n onChange,\r\n label,\r\n error,\r\n disabled,\r\n className,\r\n}: OtpFieldProps) {\r\n const id = React.useId();\r\n\r\n return (\r\n <div className={cn('flex flex-col items-start gap-2', className)}>\r\n {label && (\r\n <Label htmlFor={id} className='text-sm font-medium'>\r\n {label}\r\n </Label>\r\n )}\r\n <InputOTP\r\n id={id}\r\n maxLength={length}\r\n value={value}\r\n onChange={onChange}\r\n disabled={disabled}\r\n aria-invalid={!!error}\r\n >\r\n {length === 6 ? (\r\n <>\r\n <InputOTPGroup>\r\n <InputOTPSlot index={0} />\r\n <InputOTPSlot index={1} />\r\n <InputOTPSlot index={2} />\r\n </InputOTPGroup>\r\n <InputOTPSeparator />\r\n <InputOTPGroup>\r\n <InputOTPSlot index={3} />\r\n <InputOTPSlot index={4} />\r\n <InputOTPSlot index={5} />\r\n </InputOTPGroup>\r\n </>\r\n ) : (\r\n <InputOTPGroup>\r\n {Array.from({ length }).map((_, i) => (\r\n <InputOTPSlot key={i} index={i} />\r\n ))}\r\n </InputOTPGroup>\r\n )}\r\n </InputOTP>\r\n {error && (\r\n <p role='alert' className='text-destructive text-sm'>\r\n {error}\r\n </p>\r\n )}\r\n </div>\r\n );\r\n}\r\n",
|
|
951
|
+
"src/components/ui/composed/navigation-menu/site-nav.tsx": "import * as React from 'react';\r\nimport {\r\n NavigationMenu,\r\n NavigationMenuContent,\r\n NavigationMenuItem,\r\n NavigationMenuLink,\r\n NavigationMenuList,\r\n NavigationMenuTrigger,\r\n} from '@/components/ui/navigation-menu';\r\nimport { cn } from '@/lib/utils';\r\n\r\ntype NavChild = {\r\n label: string;\r\n href: string;\r\n description?: string;\r\n};\r\n\r\ntype NavItem = {\r\n label: string;\r\n href?: string;\r\n children?: NavChild[];\r\n};\r\n\r\ntype SiteNavProps = {\r\n items: NavItem[];\r\n className?: string;\r\n};\r\n\r\nexport function SiteNav({ items, className }: SiteNavProps) {\r\n return (\r\n <NavigationMenu className={className}>\r\n <NavigationMenuList>\r\n {items.map(item =>\r\n item.children?.length ? (\r\n <NavigationMenuItem key={item.label}>\r\n <NavigationMenuTrigger>{item.label}</NavigationMenuTrigger>\r\n <NavigationMenuContent>\r\n <ul className='grid w-[280px] gap-1 p-2 md:w-[360px]'>\r\n {item.children.map(child => (\r\n <li key={child.href}>\r\n <NavigationMenuLink\r\n href={child.href}\r\n className='flex flex-col gap-0.5 p-2'\r\n >\r\n <span className='text-sm font-medium'>\r\n {child.label}\r\n </span>\r\n {child.description && (\r\n <span className='text-muted-foreground line-clamp-1 text-xs'>\r\n {child.description}\r\n </span>\r\n )}\r\n </NavigationMenuLink>\r\n </li>\r\n ))}\r\n </ul>\r\n </NavigationMenuContent>\r\n </NavigationMenuItem>\r\n ) : (\r\n <NavigationMenuItem key={item.label}>\r\n <NavigationMenuLink href={item.href ?? '#'}>\r\n {item.label}\r\n </NavigationMenuLink>\r\n </NavigationMenuItem>\r\n )\r\n )}\r\n </NavigationMenuList>\r\n </NavigationMenu>\r\n );\r\n}\r\n",
|
|
952
|
+
"src/components/ui/composed/pagination/data-pagination.tsx": "import * as React from 'react';\r\nimport {\r\n Pagination,\r\n PaginationContent,\r\n PaginationEllipsis,\r\n PaginationItem,\r\n PaginationLink,\r\n PaginationNext,\r\n PaginationPrevious,\r\n} from '@/components/ui/pagination';\r\nimport { cn } from '@/lib/utils';\r\n\r\nimport { m } from '#/paraglide/messages';\r\n\r\ntype DataPaginationProps = {\r\n currentPage: number;\r\n totalPages: number;\r\n onPageChange: (page: number) => void;\r\n siblingCount?: number;\r\n className?: string;\r\n};\r\n\r\nfunction buildPageRange(\r\n current: number,\r\n total: number,\r\n siblings: number\r\n): (number | 'ellipsis')[] {\r\n if (total <= 1) return [1];\r\n\r\n const delta = siblings + 1;\r\n const innerStart = Math.max(2, current - delta);\r\n const innerEnd = Math.min(total - 1, current + delta);\r\n\r\n const inner: number[] = [];\r\n for (let i = innerStart; i <= innerEnd; i++) inner.push(i);\r\n\r\n const result: (number | 'ellipsis')[] = [1];\r\n if (inner[0] > 2) result.push('ellipsis');\r\n result.push(...inner);\r\n if (inner[inner.length - 1] < total - 1) result.push('ellipsis');\r\n if (total > 1) result.push(total);\r\n\r\n return result;\r\n}\r\n\r\nexport function DataPagination({\r\n currentPage,\r\n totalPages,\r\n onPageChange,\r\n siblingCount = 1,\r\n className,\r\n}: DataPaginationProps) {\r\n if (totalPages <= 1) return null;\r\n\r\n const pages = buildPageRange(currentPage, totalPages, siblingCount);\r\n\r\n return (\r\n <Pagination className={cn(className)}>\r\n <PaginationContent>\r\n <PaginationItem>\r\n <PaginationPrevious\r\n href='#'\r\n text={m.ui_previous()}\r\n aria-label={m.ui_previous_page()}\r\n onClick={e => {\r\n e.preventDefault();\r\n if (currentPage > 1) onPageChange(currentPage - 1);\r\n }}\r\n disabled={currentPage <= 1}\r\n />\r\n </PaginationItem>\r\n\r\n {pages.map((page, index) =>\r\n page === 'ellipsis' ? (\r\n <PaginationItem key={`ellipsis-${index}`}>\r\n <PaginationEllipsis />\r\n </PaginationItem>\r\n ) : (\r\n <PaginationItem key={page}>\r\n <PaginationLink\r\n href='#'\r\n isActive={page === currentPage}\r\n onClick={e => {\r\n e.preventDefault();\r\n onPageChange(page);\r\n }}\r\n >\r\n {page}\r\n </PaginationLink>\r\n </PaginationItem>\r\n )\r\n )}\r\n\r\n <PaginationItem>\r\n <PaginationNext\r\n href='#'\r\n text={m.ui_next()}\r\n aria-label={m.ui_next_page()}\r\n onClick={e => {\r\n e.preventDefault();\r\n if (currentPage < totalPages) onPageChange(currentPage + 1);\r\n }}\r\n disabled={currentPage >= totalPages}\r\n />\r\n </PaginationItem>\r\n </PaginationContent>\r\n </Pagination>\r\n );\r\n}\r\n",
|
|
953
|
+
"src/components/ui/composed/popover/info-popover.tsx": "import * as React from 'react';\r\nimport {\r\n Popover,\r\n PopoverContent,\r\n PopoverTrigger,\r\n} from '@/components/ui/popover';\r\nimport { Button } from '@/components/ui/button';\r\nimport { InfoIcon } from 'lucide-react';\r\nimport { cn } from '@/lib/utils';\r\n\r\nimport { m } from '#/paraglide/messages';\r\n\r\ntype InfoPopoverProps = {\r\n content: React.ReactNode;\r\n trigger?: React.ReactNode;\r\n side?: 'top' | 'right' | 'bottom' | 'left';\r\n align?: 'start' | 'center' | 'end';\r\n className?: string;\r\n};\r\n\r\nexport function InfoPopover({\r\n content,\r\n trigger,\r\n side = 'top',\r\n align = 'center',\r\n className,\r\n}: InfoPopoverProps) {\r\n return (\r\n <Popover>\r\n <PopoverTrigger asChild>\r\n {trigger ?? (\r\n <Button\r\n variant='ghost'\r\n size='icon'\r\n className='size-6 rounded-full'\r\n aria-label={m.ui_more_information()}\r\n >\r\n <InfoIcon className='size-3.5' />\r\n </Button>\r\n )}\r\n </PopoverTrigger>\r\n <PopoverContent\r\n side={side}\r\n align={align}\r\n className={cn('text-sm', className)}\r\n >\r\n {content}\r\n </PopoverContent>\r\n </Popover>\r\n );\r\n}\r\n",
|
|
954
|
+
"src/components/ui/composed/radio-group/options-radio-group.tsx": "import * as React from 'react';\r\nimport { RadioGroup, RadioGroupItem } from '@/components/ui/radio-group';\r\nimport { Label } from '@/components/ui/label';\r\nimport { cn } from '@/lib/utils';\r\n\r\ntype RadioOption = {\r\n label: string;\r\n value: string;\r\n description?: string;\r\n disabled?: boolean;\r\n};\r\n\r\ntype OptionsRadioGroupProps = {\r\n options: RadioOption[];\r\n value: string;\r\n onValueChange: (value: string) => void;\r\n label?: string;\r\n description?: string;\r\n error?: string;\r\n orientation?: 'vertical' | 'horizontal';\r\n className?: string;\r\n};\r\n\r\nexport function OptionsRadioGroup({\r\n options,\r\n value,\r\n onValueChange,\r\n label,\r\n description,\r\n error,\r\n orientation = 'vertical',\r\n className,\r\n}: OptionsRadioGroupProps) {\r\n const groupId = React.useId();\r\n\r\n return (\r\n <fieldset className={cn('flex flex-col gap-2', className)}>\r\n {label && (\r\n <legend className='mb-1 text-sm font-medium leading-none'>\r\n {label}\r\n </legend>\r\n )}\r\n {description && (\r\n <p className='text-muted-foreground text-sm'>{description}</p>\r\n )}\r\n <RadioGroup\r\n value={value}\r\n onValueChange={onValueChange}\r\n className={cn(\r\n orientation === 'horizontal' &&\r\n 'flex flex-row flex-wrap gap-x-6 gap-y-2'\r\n )}\r\n >\r\n {options.map(option => {\r\n const id = `${groupId}-${option.value}`;\r\n return (\r\n <div key={option.value} className='flex items-start gap-2'>\r\n <RadioGroupItem\r\n id={id}\r\n value={option.value}\r\n disabled={option.disabled}\r\n className='mt-0.5'\r\n />\r\n <div className='flex flex-col gap-0.5'>\r\n <Label\r\n htmlFor={id}\r\n className={cn(option.disabled && 'opacity-50')}\r\n >\r\n {option.label}\r\n </Label>\r\n {option.description && (\r\n <span className='text-muted-foreground text-xs'>\r\n {option.description}\r\n </span>\r\n )}\r\n </div>\r\n </div>\r\n );\r\n })}\r\n </RadioGroup>\r\n {error && (\r\n <p role='alert' className='text-destructive text-sm'>\r\n {error}\r\n </p>\r\n )}\r\n </fieldset>\r\n );\r\n}\r\n",
|
|
955
|
+
"src/components/ui/composed/select/labeled-select.tsx": "import * as React from 'react';\r\n\r\nimport { Label } from '@/components/ui/label';\r\nimport {\r\n Select,\r\n SelectContent,\r\n SelectItem,\r\n SelectTrigger,\r\n SelectValue,\r\n} from '@/components/ui/select';\r\nimport { cn } from '@/lib/utils';\r\n\r\nimport { m } from '#/paraglide/messages';\r\n\r\ntype SelectOption = {\r\n label: string;\r\n value: string;\r\n disabled?: boolean;\r\n};\r\n\r\ntype LabeledSelectProps = {\r\n options: SelectOption[];\r\n value?: string;\r\n onValueChange?: (value: string) => void;\r\n placeholder?: string;\r\n label?: string;\r\n description?: string;\r\n error?: string;\r\n required?: boolean;\r\n disabled?: boolean;\r\n className?: string;\r\n id?: string;\r\n};\r\n\r\nexport function LabeledSelect({\r\n options,\r\n value,\r\n onValueChange,\r\n placeholder = m.ui_select_placeholder(),\r\n label,\r\n description,\r\n error,\r\n required,\r\n disabled,\r\n className,\r\n id: externalId,\r\n}: LabeledSelectProps) {\r\n const generatedId = React.useId();\r\n const id = externalId ?? generatedId;\r\n const descId = description ? `${id}-desc` : undefined;\r\n const errId = error ? `${id}-err` : undefined;\r\n\r\n return (\r\n <div className={cn('flex flex-col gap-1.5', className)}>\r\n {label ? (\r\n <Label htmlFor={id}>\r\n {label}\r\n {required ? <span className='text-destructive ms-0.5'>*</span> : null}\r\n </Label>\r\n ) : null}\r\n {description ? (\r\n <p id={descId} className='text-muted-foreground text-sm'>\r\n {description}\r\n </p>\r\n ) : null}\r\n <Select\r\n value={value}\r\n onValueChange={next => {\r\n onValueChange?.(next ?? '');\r\n }}\r\n disabled={disabled}\r\n >\r\n <SelectTrigger\r\n id={id}\r\n className='w-full'\r\n aria-invalid={!!error}\r\n aria-describedby={\r\n [descId, errId].filter(Boolean).join(' ') || undefined\r\n }\r\n >\r\n <SelectValue placeholder={placeholder} />\r\n </SelectTrigger>\r\n <SelectContent>\r\n {options.map(option => (\r\n <SelectItem\r\n key={option.value}\r\n value={option.value}\r\n disabled={option.disabled}\r\n >\r\n {option.label}\r\n </SelectItem>\r\n ))}\r\n </SelectContent>\r\n </Select>\r\n {error ? (\r\n <p id={errId} role='alert' className='text-destructive text-sm'>\r\n {error}\r\n </p>\r\n ) : null}\r\n </div>\r\n );\r\n}\r\n",
|
|
956
|
+
"src/components/ui/composed/select/select-groups.tsx": "import * as React from 'react';\r\nimport {\r\n Select,\r\n SelectContent,\r\n SelectGroup,\r\n SelectItem,\r\n SelectLabel,\r\n SelectSeparator,\r\n SelectTrigger,\r\n SelectValue,\r\n} from '@/components/ui/select';\r\nimport { Label } from '@/components/ui/label';\r\nimport { cn } from '@/lib/utils';\r\n\r\nimport { m } from '#/paraglide/messages';\r\n\r\ntype SelectOption = {\r\n label: string;\r\n value: string;\r\n disabled?: boolean;\r\n};\r\n\r\ntype SelectOptionGroup = {\r\n label: string;\r\n options: SelectOption[];\r\n};\r\n\r\ntype SelectGroupsProps = {\r\n groups: SelectOptionGroup[];\r\n value?: string;\r\n onValueChange?: (value: string) => void;\r\n placeholder?: string;\r\n label?: string;\r\n error?: string;\r\n required?: boolean;\r\n disabled?: boolean;\r\n className?: string;\r\n};\r\n\r\nexport function SelectGroups({\r\n groups,\r\n value,\r\n onValueChange,\r\n placeholder = m.ui_select_placeholder(),\r\n label,\r\n error,\r\n required,\r\n disabled,\r\n className,\r\n}: SelectGroupsProps) {\r\n const id = React.useId();\r\n\r\n return (\r\n <div className={cn('flex flex-col gap-1.5', className)}>\r\n {label && (\r\n <Label htmlFor={id}>\r\n {label}\r\n {required && <span className='text-destructive ms-0.5'>*</span>}\r\n </Label>\r\n )}\r\n <Select\r\n value={value}\r\n onValueChange={next => {\r\n onValueChange?.(next ?? '');\r\n }}\r\n disabled={disabled}\r\n >\r\n <SelectTrigger id={id} className='w-full' aria-invalid={!!error}>\r\n <SelectValue placeholder={placeholder} />\r\n </SelectTrigger>\r\n <SelectContent>\r\n {groups.map((group, index) => (\r\n <React.Fragment key={group.label}>\r\n {index > 0 && <SelectSeparator />}\r\n <SelectGroup>\r\n <SelectLabel>{group.label}</SelectLabel>\r\n {group.options.map(option => (\r\n <SelectItem\r\n key={option.value}\r\n value={option.value}\r\n disabled={option.disabled}\r\n >\r\n {option.label}\r\n </SelectItem>\r\n ))}\r\n </SelectGroup>\r\n </React.Fragment>\r\n ))}\r\n </SelectContent>\r\n </Select>\r\n {error && (\r\n <p role='alert' className='text-destructive text-sm'>\r\n {error}\r\n </p>\r\n )}\r\n </div>\r\n );\r\n}\r\n",
|
|
957
|
+
"src/components/ui/composed/sheet/filter-sheet.tsx": "import * as React from 'react';\r\nimport {\r\n Sheet,\r\n SheetContent,\r\n SheetDescription,\r\n SheetFooter,\r\n SheetHeader,\r\n SheetTitle,\r\n SheetTrigger,\r\n} from '@/components/ui/sheet';\r\n\r\ntype FilterSheetProps = {\r\n open?: boolean;\r\n onOpenChange?: (open: boolean) => void;\r\n trigger?: React.ReactNode;\r\n title: string;\r\n description?: string;\r\n children: React.ReactNode;\r\n footer?: React.ReactNode;\r\n side?: 'left' | 'right' | 'top' | 'bottom';\r\n};\r\n\r\nexport function FilterSheet({\r\n open,\r\n onOpenChange,\r\n trigger,\r\n title,\r\n description,\r\n children,\r\n footer,\r\n side = 'right',\r\n}: FilterSheetProps) {\r\n return (\r\n <Sheet open={open} onOpenChange={onOpenChange}>\r\n {trigger && <SheetTrigger asChild>{trigger}</SheetTrigger>}\r\n <SheetContent side={side} className='flex flex-col'>\r\n <SheetHeader>\r\n <SheetTitle>{title}</SheetTitle>\r\n {description && <SheetDescription>{description}</SheetDescription>}\r\n </SheetHeader>\r\n <div className='flex-1 overflow-y-auto px-4 py-4'>{children}</div>\r\n {footer && <SheetFooter className='px-4 pb-4'>{footer}</SheetFooter>}\r\n </SheetContent>\r\n </Sheet>\r\n );\r\n}\r\n",
|
|
958
|
+
"src/components/ui/composed/skeleton/card-skeleton.tsx": "import { Skeleton } from '@/components/ui/skeleton';\r\nimport { Card, CardContent, CardHeader } from '@/components/ui/card';\r\nimport { cn } from '@/lib/utils';\r\n\r\ntype CardSkeletonProps = {\r\n lines?: number;\r\n showHeader?: boolean;\r\n showAvatar?: boolean;\r\n className?: string;\r\n};\r\n\r\nexport function CardSkeleton({\r\n lines = 3,\r\n showHeader = true,\r\n showAvatar = false,\r\n className,\r\n}: CardSkeletonProps) {\r\n return (\r\n <Card className={cn('overflow-hidden', className)}>\r\n {showHeader && (\r\n <CardHeader className='gap-3'>\r\n <div className='flex items-center gap-3'>\r\n {showAvatar && (\r\n <Skeleton className='size-10 shrink-0 rounded-full' />\r\n )}\r\n <div className='flex flex-1 flex-col gap-2'>\r\n <Skeleton className='h-4 w-1/3' />\r\n <Skeleton className='h-3 w-1/4' />\r\n </div>\r\n </div>\r\n </CardHeader>\r\n )}\r\n <CardContent className='flex flex-col gap-2'>\r\n {Array.from({ length: lines }).map((_, i) => (\r\n <Skeleton\r\n key={i}\r\n className={cn('h-3', i === lines - 1 ? 'w-2/3' : 'w-full')}\r\n />\r\n ))}\r\n </CardContent>\r\n </Card>\r\n );\r\n}\r\n",
|
|
959
|
+
"src/components/ui/composed/table/simple-data-table.tsx": "import * as React from 'react';\r\nimport {\r\n Table,\r\n TableBody,\r\n TableCaption,\r\n TableCell,\r\n TableHead,\r\n TableHeader,\r\n TableRow,\r\n} from '@/components/ui/table';\r\nimport { cn } from '@/lib/utils';\r\n\r\nimport { m } from '#/paraglide/messages';\r\n\r\ntype Column<TData> = {\r\n key: string;\r\n header: string;\r\n cell: (row: TData) => React.ReactNode;\r\n headerClassName?: string;\r\n cellClassName?: string;\r\n};\r\n\r\ntype SimpleDataTableProps<TData extends { id: string | number }> = {\r\n columns: Column<TData>[];\r\n data: TData[];\r\n caption?: string;\r\n emptyMessage?: string;\r\n className?: string;\r\n};\r\n\r\nexport function SimpleDataTable<TData extends { id: string | number }>({\r\n columns,\r\n data,\r\n caption,\r\n emptyMessage = m.ui_no_results(),\r\n className,\r\n}: SimpleDataTableProps<TData>) {\r\n return (\r\n <Table className={className}>\r\n {caption && <TableCaption>{caption}</TableCaption>}\r\n <TableHeader>\r\n <TableRow>\r\n {columns.map(col => (\r\n <TableHead key={col.key} className={cn(col.headerClassName)}>\r\n {col.header}\r\n </TableHead>\r\n ))}\r\n </TableRow>\r\n </TableHeader>\r\n <TableBody>\r\n {data.length === 0 ? (\r\n <TableRow>\r\n <TableCell\r\n colSpan={columns.length}\r\n className='h-24 text-center text-muted-foreground'\r\n >\r\n {emptyMessage}\r\n </TableCell>\r\n </TableRow>\r\n ) : (\r\n data.map(row => (\r\n <TableRow key={row.id}>\r\n {columns.map(col => (\r\n <TableCell key={col.key} className={cn(col.cellClassName)}>\r\n {col.cell(row)}\r\n </TableCell>\r\n ))}\r\n </TableRow>\r\n ))\r\n )}\r\n </TableBody>\r\n </Table>\r\n );\r\n}\r\n",
|
|
960
|
+
"src/components/ui/composed/tabs/icon-tabs.tsx": "import * as React from 'react';\r\nimport { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';\r\nimport { cn } from '@/lib/utils';\r\n\r\ntype TabItem = {\r\n value: string;\r\n label: string;\r\n icon?: React.ReactNode;\r\n content: React.ReactNode;\r\n disabled?: boolean;\r\n};\r\n\r\ntype IconTabsProps = {\r\n items: TabItem[];\r\n defaultValue?: string;\r\n value?: string;\r\n onValueChange?: (value: string) => void;\r\n className?: string;\r\n listClassName?: string;\r\n};\r\n\r\nexport function IconTabs({\r\n items,\r\n defaultValue,\r\n value,\r\n onValueChange,\r\n className,\r\n listClassName,\r\n}: IconTabsProps) {\r\n return (\r\n <Tabs\r\n defaultValue={defaultValue ?? items[0]?.value}\r\n value={value}\r\n onValueChange={onValueChange}\r\n className={className}\r\n >\r\n <TabsList className={cn(listClassName)}>\r\n {items.map(item => (\r\n <TabsTrigger\r\n key={item.value}\r\n value={item.value}\r\n disabled={item.disabled}\r\n >\r\n {item.icon}\r\n {item.label}\r\n </TabsTrigger>\r\n ))}\r\n </TabsList>\r\n {items.map(item => (\r\n <TabsContent key={item.value} value={item.value}>\r\n {item.content}\r\n </TabsContent>\r\n ))}\r\n </Tabs>\r\n );\r\n}\r\n",
|
|
961
|
+
"src/components/ui/composed/textarea/labeled-textarea.tsx": "import * as React from 'react';\r\nimport { Textarea } from '@/components/ui/textarea';\r\nimport { Label } from '@/components/ui/label';\r\nimport { cn } from '@/lib/utils';\r\n\r\ntype LabeledTextareaProps = React.ComponentProps<typeof Textarea> & {\r\n label?: string;\r\n description?: string;\r\n error?: string;\r\n required?: boolean;\r\n maxLength?: number;\r\n showCharCount?: boolean;\r\n};\r\n\r\nexport function LabeledTextarea({\r\n label,\r\n description,\r\n error,\r\n required,\r\n maxLength,\r\n showCharCount = false,\r\n value,\r\n id: externalId,\r\n className,\r\n ...props\r\n}: LabeledTextareaProps) {\r\n const generatedId = React.useId();\r\n const id = externalId ?? generatedId;\r\n const descId = description ? `${id}-desc` : undefined;\r\n const errId = error ? `${id}-err` : undefined;\r\n const charCount = typeof value === 'string' ? value.length : 0;\r\n\r\n return (\r\n <div className={cn('flex flex-col gap-1.5', className)}>\r\n <div className='flex items-baseline justify-between gap-2'>\r\n {label && (\r\n <Label htmlFor={id}>\r\n {label}\r\n {required && <span className='text-destructive ms-0.5'>*</span>}\r\n </Label>\r\n )}\r\n {showCharCount && maxLength && (\r\n <span className='text-muted-foreground text-xs tabular-nums'>\r\n {charCount}/{maxLength}\r\n </span>\r\n )}\r\n </div>\r\n {description && (\r\n <p id={descId} className='text-muted-foreground text-sm'>\r\n {description}\r\n </p>\r\n )}\r\n <Textarea\r\n id={id}\r\n value={value}\r\n maxLength={maxLength}\r\n aria-describedby={\r\n [descId, errId].filter(Boolean).join(' ') || undefined\r\n }\r\n aria-invalid={!!error}\r\n {...props}\r\n />\r\n {error && (\r\n <p id={errId} role='alert' className='text-destructive text-sm'>\r\n {error}\r\n </p>\r\n )}\r\n </div>\r\n );\r\n}\r\n",
|
|
962
|
+
"src/components/ui/composed/tooltip/icon-tooltip.tsx": "import * as React from 'react';\r\nimport {\r\n Tooltip,\r\n TooltipContent,\r\n TooltipTrigger,\r\n} from '@/components/ui/tooltip';\r\nimport { cn } from '@/lib/utils';\r\n\r\ntype IconTooltipProps = {\r\n content: React.ReactNode;\r\n children: React.ReactNode;\r\n side?: 'top' | 'right' | 'bottom' | 'left';\r\n align?: 'start' | 'center' | 'end';\r\n className?: string;\r\n};\r\n\r\nexport function IconTooltip({\r\n content,\r\n children,\r\n side = 'top',\r\n align = 'center',\r\n className,\r\n}: IconTooltipProps) {\r\n return (\r\n <Tooltip>\r\n <TooltipTrigger asChild>{children}</TooltipTrigger>\r\n <TooltipContent side={side} align={align} className={className}>\r\n {content}\r\n </TooltipContent>\r\n </Tooltip>\r\n );\r\n}\r\n",
|
|
963
|
+
"src/components/ui/composed/upload/file-dropzone.tsx": "import * as React from 'react';\r\nimport { UploadIcon, XIcon } from 'lucide-react';\r\n\r\nimport { Button } from '@/components/ui/button';\r\nimport { Input } from '@/components/ui/input';\r\nimport { Label } from '@/components/ui/label';\r\nimport { cn } from '@/lib/utils';\r\n\r\nimport { m } from '#/paraglide/messages';\r\n\r\ntype FileDropzoneProps = {\r\n value?: File[];\r\n onValueChange?: (files: File[]) => void;\r\n label?: string;\r\n description?: string;\r\n error?: string;\r\n required?: boolean;\r\n disabled?: boolean;\r\n multiple?: boolean;\r\n accept?: string;\r\n maxSize?: number;\r\n id?: string;\r\n className?: string;\r\n};\r\n\r\nconst formatBytes = (bytes: number) => {\r\n if (bytes < 1024) return `${bytes} B`;\r\n if (bytes < 1024 * 1024) return `${Math.round(bytes / 1024)} KB`;\r\n return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;\r\n};\r\n\r\nconst fileKey = (file: File) =>\r\n `${file.name}-${file.size}-${file.lastModified}`;\r\n\r\nexport function FileDropzone({\r\n value = [],\r\n onValueChange,\r\n label,\r\n description,\r\n error,\r\n required,\r\n disabled,\r\n multiple = false,\r\n accept,\r\n maxSize,\r\n id: externalId,\r\n className,\r\n}: FileDropzoneProps) {\r\n const generatedId = React.useId();\r\n const id = externalId ?? generatedId;\r\n const descId = description ? `${id}-desc` : undefined;\r\n const errId = error ? `${id}-err` : undefined;\r\n const [isDragging, setIsDragging] = React.useState(false);\r\n\r\n const addFiles = (incoming: File[]) => {\r\n const allowed = maxSize\r\n ? incoming.filter(file => file.size <= maxSize)\r\n : incoming;\r\n if (allowed.length === 0) return;\r\n\r\n if (!multiple) {\r\n onValueChange?.(allowed.slice(0, 1));\r\n return;\r\n }\r\n\r\n const existing = new Set(value.map(fileKey));\r\n onValueChange?.([\r\n ...value,\r\n ...allowed.filter(file => !existing.has(fileKey(file))),\r\n ]);\r\n };\r\n\r\n return (\r\n <div className={cn('flex flex-col gap-1.5', className)}>\r\n {label ? (\r\n <Label htmlFor={id}>\r\n {label}\r\n {required ? <span className='text-destructive ms-0.5'>*</span> : null}\r\n </Label>\r\n ) : null}\r\n {description ? (\r\n <p id={descId} className='text-muted-foreground text-sm'>\r\n {description}\r\n </p>\r\n ) : null}\r\n <label\r\n htmlFor={id}\r\n onDragOver={event => {\r\n event.preventDefault();\r\n if (!disabled) setIsDragging(true);\r\n }}\r\n onDragLeave={() => setIsDragging(false)}\r\n onDrop={event => {\r\n event.preventDefault();\r\n setIsDragging(false);\r\n if (disabled) return;\r\n addFiles(Array.from(event.dataTransfer.files));\r\n }}\r\n className={cn(\r\n 'border-input bg-background flex min-h-32 cursor-pointer flex-col items-center justify-center gap-2 rounded-lg border border-dashed px-4 py-6 text-center transition-colors',\r\n isDragging && 'border-ring bg-muted/50',\r\n error && 'border-destructive',\r\n disabled && 'pointer-events-none opacity-50'\r\n )}\r\n >\r\n <UploadIcon className='text-muted-foreground size-6' />\r\n <span className='text-sm'>{m.ui_dropzone_prompt()}</span>\r\n <span className='text-primary text-sm font-medium'>\r\n {m.ui_browse_files()}\r\n </span>\r\n {maxSize ? (\r\n <span className='text-muted-foreground text-xs'>\r\n {m.ui_max_size({ size: formatBytes(maxSize) })}\r\n </span>\r\n ) : null}\r\n <Input\r\n id={id}\r\n type='file'\r\n accept={accept}\r\n multiple={multiple}\r\n disabled={disabled}\r\n className='sr-only'\r\n aria-describedby={\r\n [descId, errId].filter(Boolean).join(' ') || undefined\r\n }\r\n aria-invalid={!!error}\r\n onChange={event => {\r\n addFiles(Array.from(event.target.files ?? []));\r\n event.target.value = '';\r\n }}\r\n />\r\n </label>\r\n {value.length > 0 ? (\r\n <ul className='flex flex-col gap-1'>\r\n {value.map(file => (\r\n <li\r\n key={fileKey(file)}\r\n className='bg-muted flex items-center gap-2 rounded-md px-2 py-1.5 text-sm'\r\n >\r\n <span className='min-w-0 flex-1 truncate text-start'>\r\n {file.name}\r\n </span>\r\n <span className='text-muted-foreground shrink-0 text-xs'>\r\n {formatBytes(file.size)}\r\n </span>\r\n <Button\r\n type='button'\r\n variant='ghost'\r\n size='icon-xs'\r\n disabled={disabled}\r\n aria-label={m.ui_remove_file()}\r\n onClick={() =>\r\n onValueChange?.(\r\n value.filter(item => fileKey(item) !== fileKey(file))\r\n )\r\n }\r\n >\r\n <XIcon />\r\n </Button>\r\n </li>\r\n ))}\r\n </ul>\r\n ) : null}\r\n {error ? (\r\n <p id={errId} role='alert' className='text-destructive text-sm'>\r\n {error}\r\n </p>\r\n ) : null}\r\n </div>\r\n );\r\n}\r\n",
|
|
964
|
+
"src/lib/utils.ts": "import { clsx } from 'clsx';\r\nimport type { ClassValue } from 'clsx';\r\nimport { twMerge } from 'tailwind-merge';\r\n\r\nexport function cn(...inputs: ClassValue[]) {\r\n return twMerge(clsx(inputs));\r\n}\r\n"
|
|
965
|
+
},
|
|
966
|
+
"messages": {
|
|
967
|
+
"en": {
|
|
968
|
+
"about_page": "About page",
|
|
969
|
+
"about_title": "About us",
|
|
970
|
+
"about_intro": "Learn more about who we are and what we do.",
|
|
971
|
+
"contact_page": "Contact page",
|
|
972
|
+
"contact_title": "Contact us",
|
|
973
|
+
"contact_intro": "Get in touch and we'll get back to you shortly.",
|
|
974
|
+
"dashboard_page": "Dashboard",
|
|
975
|
+
"dashboard_title": "Dashboard",
|
|
976
|
+
"dashboard_intro": "Welcome back. Here's an overview of your account.",
|
|
977
|
+
"dashboard_signed_in_as": "Signed in as {email}",
|
|
978
|
+
"dashboard_logout": "Log out",
|
|
979
|
+
"login_page": "Log in",
|
|
980
|
+
"login_title": "Log in",
|
|
981
|
+
"login_intro": "Sign in to continue to your account.",
|
|
982
|
+
"login_email_label": "Email",
|
|
983
|
+
"login_email_placeholder": "Enter your email",
|
|
984
|
+
"login_password_label": "Password",
|
|
985
|
+
"login_password_placeholder": "Enter your password",
|
|
986
|
+
"login_submit": "Log in",
|
|
987
|
+
"login_submitting": "Logging in...",
|
|
988
|
+
"login_success": "Welcome! You have successfully logged in.",
|
|
989
|
+
"status_error_404": "The requested resource was not found.",
|
|
990
|
+
"status_error_500": "Something went wrong. Please try again.",
|
|
991
|
+
"ui_select_placeholder": "Select an option",
|
|
992
|
+
"ui_search_placeholder": "Search...",
|
|
993
|
+
"ui_no_results": "No results found.",
|
|
994
|
+
"ui_pick_date": "Pick a date",
|
|
995
|
+
"ui_confirm": "Confirm",
|
|
996
|
+
"ui_cancel": "Cancel",
|
|
997
|
+
"ui_open_actions": "Open actions",
|
|
998
|
+
"ui_more_information": "More information",
|
|
999
|
+
"ui_clear_search": "Clear search",
|
|
1000
|
+
"ui_previous": "Previous",
|
|
1001
|
+
"ui_next": "Next",
|
|
1002
|
+
"ui_previous_page": "Go to previous page",
|
|
1003
|
+
"ui_next_page": "Go to next page",
|
|
1004
|
+
"ui_toggle_theme": "Toggle theme",
|
|
1005
|
+
"ui_dropzone_prompt": "Drag and drop a file here, or browse",
|
|
1006
|
+
"ui_browse_files": "Browse files",
|
|
1007
|
+
"ui_remove_file": "Remove file",
|
|
1008
|
+
"ui_max_size": "Maximum size {size}",
|
|
1009
|
+
"validation_email_required": "Email field is required",
|
|
1010
|
+
"validation_email_invalid": "Email is invalid",
|
|
1011
|
+
"validation_password_required": "Password field is required"
|
|
1012
|
+
},
|
|
1013
|
+
"ar": {
|
|
1014
|
+
"about_page": "صفحة من نحن",
|
|
1015
|
+
"about_title": "من نحن",
|
|
1016
|
+
"about_intro": "تعرّف أكثر على من نحن وما نقوم به.",
|
|
1017
|
+
"contact_page": "صفحة التواصل",
|
|
1018
|
+
"contact_title": "تواصل معنا",
|
|
1019
|
+
"contact_intro": "تواصل معنا وسنردّ عليك في أقرب وقت.",
|
|
1020
|
+
"dashboard_page": "لوحة التحكم",
|
|
1021
|
+
"dashboard_title": "لوحة التحكم",
|
|
1022
|
+
"dashboard_intro": "مرحبًا بعودتك. إليك نظرة عامة على حسابك.",
|
|
1023
|
+
"dashboard_signed_in_as": "تم تسجيل الدخول باسم {email}",
|
|
1024
|
+
"dashboard_logout": "تسجيل الخروج",
|
|
1025
|
+
"login_page": "تسجيل الدخول",
|
|
1026
|
+
"login_title": "تسجيل الدخول",
|
|
1027
|
+
"login_intro": "سجّل الدخول للمتابعة إلى حسابك.",
|
|
1028
|
+
"login_email_label": "البريد الإلكتروني",
|
|
1029
|
+
"login_email_placeholder": "أدخل بريدك الإلكتروني",
|
|
1030
|
+
"login_password_label": "كلمة المرور",
|
|
1031
|
+
"login_password_placeholder": "أدخل كلمة المرور",
|
|
1032
|
+
"login_submit": "تسجيل الدخول",
|
|
1033
|
+
"login_submitting": "جارٍ تسجيل الدخول...",
|
|
1034
|
+
"login_success": "مرحبًا! لقد سجّلت الدخول بنجاح.",
|
|
1035
|
+
"status_error_404": "تعذّر العثور على المورد المطلوب.",
|
|
1036
|
+
"status_error_500": "حدث خطأ ما. يُرجى المحاولة مرة أخرى.",
|
|
1037
|
+
"ui_select_placeholder": "اختر خيارًا",
|
|
1038
|
+
"ui_search_placeholder": "بحث...",
|
|
1039
|
+
"ui_no_results": "لا توجد نتائج.",
|
|
1040
|
+
"ui_pick_date": "اختر تاريخًا",
|
|
1041
|
+
"ui_confirm": "تأكيد",
|
|
1042
|
+
"ui_cancel": "إلغاء",
|
|
1043
|
+
"ui_open_actions": "فتح الإجراءات",
|
|
1044
|
+
"ui_more_information": "مزيد من المعلومات",
|
|
1045
|
+
"ui_clear_search": "مسح البحث",
|
|
1046
|
+
"ui_previous": "السابق",
|
|
1047
|
+
"ui_next": "التالي",
|
|
1048
|
+
"ui_previous_page": "الانتقال إلى الصفحة السابقة",
|
|
1049
|
+
"ui_next_page": "الانتقال إلى الصفحة التالية",
|
|
1050
|
+
"ui_toggle_theme": "تبديل المظهر",
|
|
1051
|
+
"ui_dropzone_prompt": "اسحب الملف وأفلته هنا، أو تصفح",
|
|
1052
|
+
"ui_browse_files": "تصفح الملفات",
|
|
1053
|
+
"ui_remove_file": "إزالة الملف",
|
|
1054
|
+
"ui_max_size": "الحد الأقصى للحجم {size}",
|
|
1055
|
+
"validation_email_required": "حقل البريد الإلكتروني مطلوب",
|
|
1056
|
+
"validation_email_invalid": "البريد الإلكتروني غير صالح",
|
|
1057
|
+
"validation_password_required": "حقل كلمة المرور مطلوب"
|
|
1058
|
+
}
|
|
1059
|
+
}
|
|
1060
|
+
}
|