@arolariu/components 0.5.0 → 1.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/{changelog.md → CHANGELOG.md} +82 -0
- package/CONTRIBUTING.md +344 -265
- package/DEBUGGING.md +185 -103
- package/EXAMPLES.md +2976 -341
- package/{readme.md → README.md} +306 -203
- package/dist/components/ui/accordion.d.ts +157 -5
- package/dist/components/ui/accordion.d.ts.map +1 -1
- package/dist/components/ui/accordion.js +100 -22
- package/dist/components/ui/accordion.js.map +1 -1
- package/dist/components/ui/accordion.module.js +12 -0
- package/dist/components/ui/accordion.module.js.map +1 -0
- package/dist/components/ui/accordion_module.css +72 -0
- package/dist/components/ui/accordion_module.css.map +1 -0
- package/dist/components/ui/alert-dialog.d.ts +309 -18
- package/dist/components/ui/alert-dialog.d.ts.map +1 -1
- package/dist/components/ui/alert-dialog.js +149 -52
- package/dist/components/ui/alert-dialog.js.map +1 -1
- package/dist/components/ui/alert-dialog.module.js +13 -0
- package/dist/components/ui/alert-dialog.module.js.map +1 -0
- package/dist/components/ui/alert-dialog_module.css +89 -0
- package/dist/components/ui/alert-dialog_module.css.map +1 -0
- package/dist/components/ui/alert.d.ts +109 -6
- package/dist/components/ui/alert.d.ts.map +1 -1
- package/dist/components/ui/alert.js +12 -21
- package/dist/components/ui/alert.js.map +1 -1
- package/dist/components/ui/alert.module.js +11 -0
- package/dist/components/ui/alert.module.js.map +1 -0
- package/dist/components/ui/alert_module.css +59 -0
- package/dist/components/ui/alert_module.css.map +1 -0
- package/dist/components/ui/aspect-ratio.d.ts +24 -2
- package/dist/components/ui/aspect-ratio.d.ts.map +1 -1
- package/dist/components/ui/aspect-ratio.js +14 -3
- package/dist/components/ui/aspect-ratio.js.map +1 -1
- package/dist/components/ui/aspect-ratio.module.js +7 -0
- package/dist/components/ui/aspect-ratio.module.js.map +1 -0
- package/dist/components/ui/aspect-ratio_module.css +10 -0
- package/dist/components/ui/aspect-ratio_module.css.map +1 -0
- package/dist/components/ui/async-boundary.js +17 -0
- package/dist/components/ui/async-boundary.js.map +1 -0
- package/dist/components/ui/avatar.d.ts +88 -4
- package/dist/components/ui/avatar.d.ts.map +1 -1
- package/dist/components/ui/avatar.js +52 -20
- package/dist/components/ui/avatar.js.map +1 -1
- package/dist/components/ui/avatar.module.js +9 -0
- package/dist/components/ui/avatar.module.js.map +1 -0
- package/dist/components/ui/avatar_module.css +35 -0
- package/dist/components/ui/avatar_module.css.map +1 -0
- package/dist/components/ui/background-beams.d.ts +21 -3
- package/dist/components/ui/background-beams.d.ts.map +1 -1
- package/dist/components/ui/background-beams.js +22 -11
- package/dist/components/ui/background-beams.js.map +1 -1
- package/dist/components/ui/background-beams.module.js +8 -0
- package/dist/components/ui/background-beams.module.js.map +1 -0
- package/dist/components/ui/background-beams_module.css +22 -0
- package/dist/components/ui/background-beams_module.css.map +1 -0
- package/dist/components/ui/badge.d.ts +79 -6
- package/dist/components/ui/badge.d.ts.map +1 -1
- package/dist/components/ui/badge.js +19 -23
- package/dist/components/ui/badge.js.map +1 -1
- package/dist/components/ui/badge.module.js +11 -0
- package/dist/components/ui/badge.module.js.map +1 -0
- package/dist/components/ui/badge_module.css +63 -0
- package/dist/components/ui/badge_module.css.map +1 -0
- package/dist/components/ui/breadcrumb.d.ts +158 -13
- package/dist/components/ui/breadcrumb.d.ts.map +1 -1
- package/dist/components/ui/breadcrumb.js +39 -30
- package/dist/components/ui/breadcrumb.js.map +1 -1
- package/dist/components/ui/breadcrumb.module.js +14 -0
- package/dist/components/ui/breadcrumb.module.js.map +1 -0
- package/dist/components/ui/breadcrumb_module.css +90 -0
- package/dist/components/ui/breadcrumb_module.css.map +1 -0
- package/dist/components/ui/bubble-background.d.ts +38 -10
- package/dist/components/ui/bubble-background.d.ts.map +1 -1
- package/dist/components/ui/bubble-background.js +33 -35
- package/dist/components/ui/bubble-background.js.map +1 -1
- package/dist/components/ui/bubble-background.module.js +20 -0
- package/dist/components/ui/bubble-background.module.js.map +1 -0
- package/dist/components/ui/bubble-background_module.css +99 -0
- package/dist/components/ui/bubble-background_module.css.map +1 -0
- package/dist/components/ui/button-group.d.ts +88 -8
- package/dist/components/ui/button-group.d.ts.map +1 -1
- package/dist/components/ui/button-group.js +35 -32
- package/dist/components/ui/button-group.js.map +1 -1
- package/dist/components/ui/button-group.module.js +11 -0
- package/dist/components/ui/button-group.module.js.map +1 -0
- package/dist/components/ui/button-group_module.css +79 -0
- package/dist/components/ui/button-group_module.css.map +1 -0
- package/dist/components/ui/button.d.ts +82 -7
- package/dist/components/ui/button.d.ts.map +1 -1
- package/dist/components/ui/button.js +69 -34
- package/dist/components/ui/button.js.map +1 -1
- package/dist/components/ui/button.module.js +17 -0
- package/dist/components/ui/button.module.js.map +1 -0
- package/dist/components/ui/button_module.css +137 -0
- package/dist/components/ui/button_module.css.map +1 -0
- package/dist/components/ui/calendar.d.ts +36 -5
- package/dist/components/ui/calendar.d.ts.map +1 -1
- package/dist/components/ui/calendar.js +75 -64
- package/dist/components/ui/calendar.js.map +1 -1
- package/dist/components/ui/calendar.module.js +35 -0
- package/dist/components/ui/calendar.module.js.map +1 -0
- package/dist/components/ui/calendar_module.css +245 -0
- package/dist/components/ui/calendar_module.css.map +1 -0
- package/dist/components/ui/card-skeleton.js +41 -0
- package/dist/components/ui/card-skeleton.js.map +1 -0
- package/dist/components/ui/card-skeleton.module.js +15 -0
- package/dist/components/ui/card-skeleton.module.js.map +1 -0
- package/dist/components/ui/card-skeleton_module.css +54 -0
- package/dist/components/ui/card-skeleton_module.css.map +1 -0
- package/dist/components/ui/card.d.ts +162 -7
- package/dist/components/ui/card.d.ts.map +1 -1
- package/dist/components/ui/card.js +13 -13
- package/dist/components/ui/card.js.map +1 -1
- package/dist/components/ui/card.module.js +13 -0
- package/dist/components/ui/card.module.js.map +1 -0
- package/dist/components/ui/card_module.css +43 -0
- package/dist/components/ui/card_module.css.map +1 -0
- package/dist/components/ui/carousel.d.ts +127 -3
- package/dist/components/ui/carousel.d.ts.map +1 -1
- package/dist/components/ui/carousel.js +63 -36
- package/dist/components/ui/carousel.js.map +1 -1
- package/dist/components/ui/carousel.module.js +17 -0
- package/dist/components/ui/carousel.module.js.map +1 -0
- package/dist/components/ui/carousel_module.css +82 -0
- package/dist/components/ui/carousel_module.css.map +1 -0
- package/dist/components/ui/chart.d.ts +323 -13
- package/dist/components/ui/chart.d.ts.map +1 -1
- package/dist/components/ui/chart.js +224 -53
- package/dist/components/ui/chart.js.map +1 -1
- package/dist/components/ui/chart.module.js +27 -0
- package/dist/components/ui/chart.module.js.map +1 -0
- package/dist/components/ui/chart_module.css +159 -0
- package/dist/components/ui/chart_module.css.map +1 -0
- package/dist/components/ui/checkbox-group.d.ts +27 -0
- package/dist/components/ui/checkbox-group.d.ts.map +1 -0
- package/dist/components/ui/checkbox-group.js +26 -0
- package/dist/components/ui/checkbox-group.js.map +1 -0
- package/dist/components/ui/checkbox-group.module.js +7 -0
- package/dist/components/ui/checkbox-group.module.js.map +1 -0
- package/dist/components/ui/checkbox-group_module.css +11 -0
- package/dist/components/ui/checkbox-group_module.css.map +1 -0
- package/dist/components/ui/checkbox.d.ts +35 -2
- package/dist/components/ui/checkbox.d.ts.map +1 -1
- package/dist/components/ui/checkbox.js +40 -12
- package/dist/components/ui/checkbox.js.map +1 -1
- package/dist/components/ui/checkbox.module.js +8 -0
- package/dist/components/ui/checkbox.module.js.map +1 -0
- package/dist/components/ui/checkbox_module.css +45 -0
- package/dist/components/ui/checkbox_module.css.map +1 -0
- package/dist/components/ui/collapsible.d.ts +87 -3
- package/dist/components/ui/collapsible.d.ts.map +1 -1
- package/dist/components/ui/collapsible.js +46 -32
- package/dist/components/ui/collapsible.js.map +1 -1
- package/dist/components/ui/collapsible.module.js +8 -0
- package/dist/components/ui/collapsible.module.js.map +1 -0
- package/dist/components/ui/collapsible_module.css +26 -0
- package/dist/components/ui/collapsible_module.css.map +1 -0
- package/dist/components/ui/combobox.d.ts +335 -0
- package/dist/components/ui/combobox.d.ts.map +1 -0
- package/dist/components/ui/combobox.js +206 -0
- package/dist/components/ui/combobox.js.map +1 -0
- package/dist/components/ui/combobox.module.js +23 -0
- package/dist/components/ui/combobox.module.js.map +1 -0
- package/dist/components/ui/combobox_module.css +142 -0
- package/dist/components/ui/combobox_module.css.map +1 -0
- package/dist/components/ui/command.d.ts +284 -51
- package/dist/components/ui/command.d.ts.map +1 -1
- package/dist/components/ui/command.js +396 -51
- package/dist/components/ui/command.js.map +1 -1
- package/dist/components/ui/command.module.js +20 -0
- package/dist/components/ui/command.module.js.map +1 -0
- package/dist/components/ui/command_module.css +194 -0
- package/dist/components/ui/command_module.css.map +1 -0
- package/dist/components/ui/context-menu.d.ts +414 -21
- package/dist/components/ui/context-menu.d.ts.map +1 -1
- package/dist/components/ui/context-menu.js +190 -68
- package/dist/components/ui/context-menu.js.map +1 -1
- package/dist/components/ui/context-menu.module.js +19 -0
- package/dist/components/ui/context-menu.module.js.map +1 -0
- package/dist/components/ui/context-menu_module.css +114 -0
- package/dist/components/ui/context-menu_module.css.map +1 -0
- package/dist/components/ui/copy-button.d.ts +41 -0
- package/dist/components/ui/copy-button.d.ts.map +1 -0
- package/dist/components/ui/copy-button.js +51 -0
- package/dist/components/ui/copy-button.js.map +1 -0
- package/dist/components/ui/copy-button.module.js +8 -0
- package/dist/components/ui/copy-button.module.js.map +1 -0
- package/dist/components/ui/copy-button_module.css +37 -0
- package/dist/components/ui/copy-button_module.css.map +1 -0
- package/dist/components/ui/counting-number.d.ts +28 -2
- package/dist/components/ui/counting-number.d.ts.map +1 -1
- package/dist/components/ui/counting-number.js +31 -27
- package/dist/components/ui/counting-number.js.map +1 -1
- package/dist/components/ui/counting-number.module.js +7 -0
- package/dist/components/ui/counting-number.module.js.map +1 -0
- package/dist/components/ui/counting-number_module.css +7 -0
- package/dist/components/ui/counting-number_module.css.map +1 -0
- package/dist/components/ui/dialog.d.ts +287 -17
- package/dist/components/ui/dialog.d.ts.map +1 -1
- package/dist/components/ui/dialog.js +129 -52
- package/dist/components/ui/dialog.js.map +1 -1
- package/dist/components/ui/dialog.module.js +13 -0
- package/dist/components/ui/dialog.module.js.map +1 -0
- package/dist/components/ui/dialog_module.css +114 -0
- package/dist/components/ui/dialog_module.css.map +1 -0
- package/dist/components/ui/dot-background.d.ts +25 -35
- package/dist/components/ui/dot-background.d.ts.map +1 -1
- package/dist/components/ui/dot-background.js +31 -22
- package/dist/components/ui/dot-background.js.map +1 -1
- package/dist/components/ui/dot-background.module.js +8 -0
- package/dist/components/ui/dot-background.module.js.map +1 -0
- package/dist/components/ui/dot-background_module.css +15 -0
- package/dist/components/ui/dot-background_module.css.map +1 -0
- package/dist/components/ui/drawer.d.ts +287 -18
- package/dist/components/ui/drawer.d.ts.map +1 -1
- package/dist/components/ui/drawer.js +117 -40
- package/dist/components/ui/drawer.js.map +1 -1
- package/dist/components/ui/drawer.module.js +14 -0
- package/dist/components/ui/drawer.module.js.map +1 -0
- package/dist/components/ui/drawer_module.css +86 -0
- package/dist/components/ui/drawer_module.css.map +1 -0
- package/dist/components/ui/dropdown-menu.d.ts +414 -21
- package/dist/components/ui/dropdown-menu.d.ts.map +1 -1
- package/dist/components/ui/dropdown-menu.js +189 -68
- package/dist/components/ui/dropdown-menu.js.map +1 -1
- package/dist/components/ui/dropdown-menu.module.js +19 -0
- package/dist/components/ui/dropdown-menu.module.js.map +1 -0
- package/dist/components/ui/dropdown-menu_module.css +114 -0
- package/dist/components/ui/dropdown-menu_module.css.map +1 -0
- package/dist/components/ui/dropdrawer.d.ts +307 -17
- package/dist/components/ui/dropdrawer.d.ts.map +1 -1
- package/dist/components/ui/dropdrawer.js +438 -166
- package/dist/components/ui/dropdrawer.js.map +1 -1
- package/dist/components/ui/dropdrawer.module.js +43 -0
- package/dist/components/ui/dropdrawer.module.js.map +1 -0
- package/dist/components/ui/dropdrawer_module.css +307 -0
- package/dist/components/ui/dropdrawer_module.css.map +1 -0
- package/dist/components/ui/empty.d.ts +129 -10
- package/dist/components/ui/empty.d.ts.map +1 -1
- package/dist/components/ui/empty.js +32 -47
- package/dist/components/ui/empty.js.map +1 -1
- package/dist/components/ui/empty.module.js +13 -0
- package/dist/components/ui/empty.module.js.map +1 -0
- package/dist/components/ui/empty_module.css +85 -0
- package/dist/components/ui/empty_module.css.map +1 -0
- package/dist/components/ui/error-boundary.js +61 -0
- package/dist/components/ui/error-boundary.js.map +1 -0
- package/dist/components/ui/error-boundary.module.js +10 -0
- package/dist/components/ui/error-boundary.module.js.map +1 -0
- package/dist/components/ui/error-boundary_module.css +41 -0
- package/dist/components/ui/error-boundary_module.css.map +1 -0
- package/dist/components/ui/field.d.ts +222 -23
- package/dist/components/ui/field.d.ts.map +1 -1
- package/dist/components/ui/field.js +76 -86
- package/dist/components/ui/field.js.map +1 -1
- package/dist/components/ui/field.module.js +24 -0
- package/dist/components/ui/field.module.js.map +1 -0
- package/dist/components/ui/field_module.css +185 -0
- package/dist/components/ui/field_module.css.map +1 -0
- package/dist/components/ui/fireworks-background.d.ts +27 -3
- package/dist/components/ui/fireworks-background.d.ts.map +1 -1
- package/dist/components/ui/fireworks-background.js +36 -35
- package/dist/components/ui/fireworks-background.js.map +1 -1
- package/dist/components/ui/fireworks-background.module.js +8 -0
- package/dist/components/ui/fireworks-background.module.js.map +1 -0
- package/dist/components/ui/fireworks-background_module.css +17 -0
- package/dist/components/ui/fireworks-background_module.css.map +1 -0
- package/dist/components/ui/flip-button.d.ts +27 -3
- package/dist/components/ui/flip-button.d.ts.map +1 -1
- package/dist/components/ui/flip-button.js +27 -17
- package/dist/components/ui/flip-button.js.map +1 -1
- package/dist/components/ui/flip-button.module.js +11 -0
- package/dist/components/ui/flip-button.module.js.map +1 -0
- package/dist/components/ui/flip-button_module.css +47 -0
- package/dist/components/ui/flip-button_module.css.map +1 -0
- package/dist/components/ui/focus-scope.js +70 -0
- package/dist/components/ui/focus-scope.js.map +1 -0
- package/dist/components/ui/focus-scope.module.js +7 -0
- package/dist/components/ui/focus-scope.module.js.map +1 -0
- package/dist/components/ui/focus-scope_module.css +6 -0
- package/dist/components/ui/focus-scope_module.css.map +1 -0
- package/dist/components/ui/form-skeleton.js +32 -0
- package/dist/components/ui/form-skeleton.js.map +1 -0
- package/dist/components/ui/form-skeleton.module.js +11 -0
- package/dist/components/ui/form-skeleton.module.js.map +1 -0
- package/dist/components/ui/form-skeleton_module.css +30 -0
- package/dist/components/ui/form-skeleton_module.css.map +1 -0
- package/dist/components/ui/form.d.ts +143 -13
- package/dist/components/ui/form.d.ts.map +1 -1
- package/dist/components/ui/form.js +55 -21
- package/dist/components/ui/form.js.map +1 -1
- package/dist/components/ui/form.module.js +10 -0
- package/dist/components/ui/form.module.js.map +1 -0
- package/dist/components/ui/form_module.css +22 -0
- package/dist/components/ui/form_module.css.map +1 -0
- package/dist/components/ui/gradient-background.d.ts +21 -3
- package/dist/components/ui/gradient-background.d.ts.map +1 -1
- package/dist/components/ui/gradient-background.js +8 -4
- package/dist/components/ui/gradient-background.js.map +1 -1
- package/dist/components/ui/gradient-background.module.js +7 -0
- package/dist/components/ui/gradient-background.module.js.map +1 -0
- package/dist/components/ui/gradient-background_module.css +9 -0
- package/dist/components/ui/gradient-background_module.css.map +1 -0
- package/dist/components/ui/gradient-text.d.ts +23 -2
- package/dist/components/ui/gradient-text.d.ts.map +1 -1
- package/dist/components/ui/gradient-text.js +8 -6
- package/dist/components/ui/gradient-text.js.map +1 -1
- package/dist/components/ui/gradient-text.module.js +9 -0
- package/dist/components/ui/gradient-text.module.js.map +1 -0
- package/dist/components/ui/gradient-text_module.css +24 -0
- package/dist/components/ui/gradient-text_module.css.map +1 -0
- package/dist/components/ui/highlight-text.d.ts +24 -2
- package/dist/components/ui/highlight-text.d.ts.map +1 -1
- package/dist/components/ui/highlight-text.js +7 -10
- package/dist/components/ui/highlight-text.js.map +1 -1
- package/dist/components/ui/highlight-text.module.js +7 -0
- package/dist/components/ui/highlight-text.module.js.map +1 -0
- package/dist/components/ui/highlight-text_module.css +16 -0
- package/dist/components/ui/highlight-text_module.css.map +1 -0
- package/dist/components/ui/hole-background.d.ts +23 -2
- package/dist/components/ui/hole-background.d.ts.map +1 -1
- package/dist/components/ui/hole-background.js +155 -118
- package/dist/components/ui/hole-background.js.map +1 -1
- package/dist/components/ui/hole-background.module.js +10 -0
- package/dist/components/ui/hole-background.module.js.map +1 -0
- package/dist/components/ui/hole-background_module.css +85 -0
- package/dist/components/ui/hole-background_module.css.map +1 -0
- package/dist/components/ui/hover-card.d.ts +85 -4
- package/dist/components/ui/hover-card.d.ts.map +1 -1
- package/dist/components/ui/hover-card.js +52 -10
- package/dist/components/ui/hover-card.js.map +1 -1
- package/dist/components/ui/hover-card.module.js +8 -0
- package/dist/components/ui/hover-card.module.js.map +1 -0
- package/dist/components/ui/hover-card_module.css +23 -0
- package/dist/components/ui/hover-card_module.css.map +1 -0
- package/dist/components/ui/input-group.d.ts +132 -13
- package/dist/components/ui/input-group.d.ts.map +1 -1
- package/dist/components/ui/input-group.js +62 -66
- package/dist/components/ui/input-group.js.map +1 -1
- package/dist/components/ui/input-group.module.js +20 -0
- package/dist/components/ui/input-group.module.js.map +1 -0
- package/dist/components/ui/input-group_module.css +150 -0
- package/dist/components/ui/input-group_module.css.map +1 -0
- package/dist/components/ui/input-otp.d.ts +110 -30
- package/dist/components/ui/input-otp.d.ts.map +1 -1
- package/dist/components/ui/input-otp.js +24 -15
- package/dist/components/ui/input-otp.js.map +1 -1
- package/dist/components/ui/input-otp.module.js +17 -0
- package/dist/components/ui/input-otp.module.js.map +1 -0
- package/dist/components/ui/input-otp_module.css +89 -0
- package/dist/components/ui/input-otp_module.css.map +1 -0
- package/dist/components/ui/input.d.ts +31 -1
- package/dist/components/ui/input.d.ts.map +1 -1
- package/dist/components/ui/input.js +21 -8
- package/dist/components/ui/input.js.map +1 -1
- package/dist/components/ui/input.module.js +7 -0
- package/dist/components/ui/input.module.js.map +1 -0
- package/dist/components/ui/input_module.css +40 -0
- package/dist/components/ui/input_module.css.map +1 -0
- package/dist/components/ui/item.d.ts +221 -19
- package/dist/components/ui/item.d.ts.map +1 -1
- package/dist/components/ui/item.js +66 -90
- package/dist/components/ui/item.js.map +1 -1
- package/dist/components/ui/item.module.js +22 -0
- package/dist/components/ui/item.module.js.map +1 -0
- package/dist/components/ui/item_module.css +143 -0
- package/dist/components/ui/item_module.css.map +1 -0
- package/dist/components/ui/kbd.d.ts +43 -2
- package/dist/components/ui/kbd.d.ts.map +1 -1
- package/dist/components/ui/kbd.js +12 -12
- package/dist/components/ui/kbd.js.map +1 -1
- package/dist/components/ui/kbd.module.js +8 -0
- package/dist/components/ui/kbd.module.js.map +1 -0
- package/dist/components/ui/kbd_module.css +35 -0
- package/dist/components/ui/kbd_module.css.map +1 -0
- package/dist/components/ui/label.d.ts +30 -3
- package/dist/components/ui/label.d.ts.map +1 -1
- package/dist/components/ui/label.js +7 -8
- package/dist/components/ui/label.js.map +1 -1
- package/dist/components/ui/label.module.js +7 -0
- package/dist/components/ui/label.module.js.map +1 -0
- package/dist/components/ui/label_module.css +17 -0
- package/dist/components/ui/label_module.css.map +1 -0
- package/dist/components/ui/list-skeleton.js +35 -0
- package/dist/components/ui/list-skeleton.js.map +1 -0
- package/dist/components/ui/list-skeleton.module.js +12 -0
- package/dist/components/ui/list-skeleton.module.js.map +1 -0
- package/dist/components/ui/list-skeleton_module.css +39 -0
- package/dist/components/ui/list-skeleton_module.css.map +1 -0
- package/dist/components/ui/loading-overlay.js +21 -0
- package/dist/components/ui/loading-overlay.js.map +1 -0
- package/dist/components/ui/loading-overlay.module.js +9 -0
- package/dist/components/ui/loading-overlay.module.js.map +1 -0
- package/dist/components/ui/loading-overlay_module.css +22 -0
- package/dist/components/ui/loading-overlay_module.css.map +1 -0
- package/dist/components/ui/menubar.d.ts +420 -22
- package/dist/components/ui/menubar.d.ts.map +1 -1
- package/dist/components/ui/menubar.js +199 -100
- package/dist/components/ui/menubar.js.map +1 -1
- package/dist/components/ui/menubar.module.js +21 -0
- package/dist/components/ui/menubar.module.js.map +1 -0
- package/dist/components/ui/menubar_module.css +145 -0
- package/dist/components/ui/menubar_module.css.map +1 -0
- package/dist/components/ui/meter.d.ts +85 -0
- package/dist/components/ui/meter.d.ts.map +1 -0
- package/dist/components/ui/meter.js +75 -0
- package/dist/components/ui/meter.js.map +1 -0
- package/dist/components/ui/meter.module.js +10 -0
- package/dist/components/ui/meter.module.js.map +1 -0
- package/dist/components/ui/meter_module.css +31 -0
- package/dist/components/ui/meter_module.css.map +1 -0
- package/dist/components/ui/navigation-menu.d.ts +233 -11
- package/dist/components/ui/navigation-menu.d.ts.map +1 -1
- package/dist/components/ui/navigation-menu.js +138 -49
- package/dist/components/ui/navigation-menu.js.map +1 -1
- package/dist/components/ui/navigation-menu.module.js +18 -0
- package/dist/components/ui/navigation-menu.module.js.map +1 -0
- package/dist/components/ui/navigation-menu_module.css +112 -0
- package/dist/components/ui/navigation-menu_module.css.map +1 -0
- package/dist/components/ui/number-field.d.ts +138 -0
- package/dist/components/ui/number-field.d.ts.map +1 -0
- package/dist/components/ui/number-field.js +111 -0
- package/dist/components/ui/number-field.js.map +1 -0
- package/dist/components/ui/number-field.module.js +15 -0
- package/dist/components/ui/number-field.module.js.map +1 -0
- package/dist/components/ui/number-field_module.css +125 -0
- package/dist/components/ui/number-field_module.css.map +1 -0
- package/dist/components/ui/pagination.d.ts +150 -24
- package/dist/components/ui/pagination.d.ts.map +1 -1
- package/dist/components/ui/pagination.js +41 -38
- package/dist/components/ui/pagination.js.map +1 -1
- package/dist/components/ui/pagination.module.js +14 -0
- package/dist/components/ui/pagination.module.js.map +1 -0
- package/dist/components/ui/pagination_module.css +66 -0
- package/dist/components/ui/pagination_module.css.map +1 -0
- package/dist/components/ui/popover.d.ts +133 -5
- package/dist/components/ui/popover.d.ts.map +1 -1
- package/dist/components/ui/popover.js +68 -14
- package/dist/components/ui/popover.js.map +1 -1
- package/dist/components/ui/popover.module.js +9 -0
- package/dist/components/ui/popover.module.js.map +1 -0
- package/dist/components/ui/popover_module.css +28 -0
- package/dist/components/ui/popover_module.css.map +1 -0
- package/dist/components/ui/progress.d.ts +31 -2
- package/dist/components/ui/progress.d.ts.map +1 -1
- package/dist/components/ui/progress.js +22 -13
- package/dist/components/ui/progress.js.map +1 -1
- package/dist/components/ui/progress.module.js +8 -0
- package/dist/components/ui/progress.module.js.map +1 -0
- package/dist/components/ui/progress_module.css +20 -0
- package/dist/components/ui/progress_module.css.map +1 -0
- package/dist/components/ui/radio-group.d.ts +42 -3
- package/dist/components/ui/radio-group.d.ts.map +1 -1
- package/dist/components/ui/radio-group.js +38 -16
- package/dist/components/ui/radio-group.js.map +1 -1
- package/dist/components/ui/radio-group.module.js +10 -0
- package/dist/components/ui/radio-group.module.js.map +1 -0
- package/dist/components/ui/radio-group_module.css +44 -0
- package/dist/components/ui/radio-group_module.css.map +1 -0
- package/dist/components/ui/resizable.d.ts +78 -5
- package/dist/components/ui/resizable.d.ts.map +1 -1
- package/dist/components/ui/resizable.js +23 -13
- package/dist/components/ui/resizable.js.map +1 -1
- package/dist/components/ui/resizable.module.js +10 -0
- package/dist/components/ui/resizable.module.js.map +1 -0
- package/dist/components/ui/resizable_module.css +70 -0
- package/dist/components/ui/resizable_module.css.map +1 -0
- package/dist/components/ui/ripple-button.d.ts +23 -2
- package/dist/components/ui/ripple-button.d.ts.map +1 -1
- package/dist/components/ui/ripple-button.js +26 -11
- package/dist/components/ui/ripple-button.js.map +1 -1
- package/dist/components/ui/ripple-button.module.js +9 -0
- package/dist/components/ui/ripple-button.module.js.map +1 -0
- package/dist/components/ui/ripple-button_module.css +38 -0
- package/dist/components/ui/ripple-button_module.css.map +1 -0
- package/dist/components/ui/scratcher.d.ts +26 -3
- package/dist/components/ui/scratcher.d.ts.map +1 -1
- package/dist/components/ui/scratcher.js +89 -90
- package/dist/components/ui/scratcher.js.map +1 -1
- package/dist/components/ui/scratcher.module.js +8 -0
- package/dist/components/ui/scratcher.module.js.map +1 -0
- package/dist/components/ui/scratcher_module.css +13 -0
- package/dist/components/ui/scratcher_module.css.map +1 -0
- package/dist/components/ui/scroll-area.d.ts +44 -3
- package/dist/components/ui/scroll-area.d.ts.map +1 -1
- package/dist/components/ui/scroll-area.js +44 -19
- package/dist/components/ui/scroll-area.js.map +1 -1
- package/dist/components/ui/scroll-area.module.js +14 -0
- package/dist/components/ui/scroll-area.module.js.map +1 -0
- package/dist/components/ui/scroll-area_module.css +51 -0
- package/dist/components/ui/scroll-area_module.css.map +1 -0
- package/dist/components/ui/select.d.ts +269 -11
- package/dist/components/ui/select.d.ts.map +1 -1
- package/dist/components/ui/select.js +152 -67
- package/dist/components/ui/select.js.map +1 -1
- package/dist/components/ui/select.module.js +20 -0
- package/dist/components/ui/select.module.js.map +1 -0
- package/dist/components/ui/select_module.css +134 -0
- package/dist/components/ui/select_module.css.map +1 -0
- package/dist/components/ui/separator.d.ts +33 -2
- package/dist/components/ui/separator.d.ts.map +1 -1
- package/dist/components/ui/separator.js +20 -9
- package/dist/components/ui/separator.js.map +1 -1
- package/dist/components/ui/separator.module.js +9 -0
- package/dist/components/ui/separator.module.js.map +1 -0
- package/dist/components/ui/separator_module.css +17 -0
- package/dist/components/ui/separator_module.css.map +1 -0
- package/dist/components/ui/sheet.d.ts +297 -23
- package/dist/components/ui/sheet.d.ts.map +1 -1
- package/dist/components/ui/sheet.js +121 -63
- package/dist/components/ui/sheet.js.map +1 -1
- package/dist/components/ui/sheet.module.js +18 -0
- package/dist/components/ui/sheet.module.js.map +1 -0
- package/dist/components/ui/sheet_module.css +136 -0
- package/dist/components/ui/sheet_module.css.map +1 -0
- package/dist/components/ui/sidebar.d.ts +491 -23
- package/dist/components/ui/sidebar.d.ts.map +1 -1
- package/dist/components/ui/sidebar.js +214 -143
- package/dist/components/ui/sidebar.js.map +1 -1
- package/dist/components/ui/sidebar.module.js +50 -0
- package/dist/components/ui/sidebar.module.js.map +1 -0
- package/dist/components/ui/sidebar_module.css +569 -0
- package/dist/components/ui/sidebar_module.css.map +1 -0
- package/dist/components/ui/skeleton.d.ts +30 -1
- package/dist/components/ui/skeleton.d.ts.map +1 -1
- package/dist/components/ui/skeleton.js +7 -7
- package/dist/components/ui/skeleton.js.map +1 -1
- package/dist/components/ui/skeleton.module.js +8 -0
- package/dist/components/ui/skeleton.module.js.map +1 -0
- package/dist/components/ui/skeleton_module.css +18 -0
- package/dist/components/ui/skeleton_module.css.map +1 -0
- package/dist/components/ui/slider.d.ts +48 -2
- package/dist/components/ui/slider.d.ts.map +1 -1
- package/dist/components/ui/slider.js +44 -17
- package/dist/components/ui/slider.js.map +1 -1
- package/dist/components/ui/slider.module.js +11 -0
- package/dist/components/ui/slider.module.js.map +1 -0
- package/dist/components/ui/slider_module.css +55 -0
- package/dist/components/ui/slider_module.css.map +1 -0
- package/dist/components/ui/sonner.d.ts +138 -4
- package/dist/components/ui/sonner.d.ts.map +1 -1
- package/dist/components/ui/sonner.js +450 -17
- package/dist/components/ui/sonner.js.map +1 -1
- package/dist/components/ui/sonner.module.js +34 -0
- package/dist/components/ui/sonner.module.js.map +1 -0
- package/dist/components/ui/sonner_module.css +233 -0
- package/dist/components/ui/sonner_module.css.map +1 -0
- package/dist/components/ui/spinner.d.ts +20 -1
- package/dist/components/ui/spinner.d.ts.map +1 -1
- package/dist/components/ui/spinner.js +29 -11
- package/dist/components/ui/spinner.js.map +1 -1
- package/dist/components/ui/spinner.module.js +10 -0
- package/dist/components/ui/spinner.module.js.map +1 -0
- package/dist/components/ui/spinner_module.css +28 -0
- package/dist/components/ui/spinner_module.css.map +1 -0
- package/dist/components/ui/stepper.d.ts +48 -0
- package/dist/components/ui/stepper.d.ts.map +1 -0
- package/dist/components/ui/stepper.js +41 -0
- package/dist/components/ui/stepper.js.map +1 -0
- package/dist/components/ui/stepper.module.js +12 -0
- package/dist/components/ui/stepper.module.js.map +1 -0
- package/dist/components/ui/stepper_module.css +75 -0
- package/dist/components/ui/stepper_module.css.map +1 -0
- package/dist/components/ui/switch.d.ts +29 -2
- package/dist/components/ui/switch.d.ts.map +1 -1
- package/dist/components/ui/switch.js +21 -9
- package/dist/components/ui/switch.js.map +1 -1
- package/dist/components/ui/switch.module.js +8 -0
- package/dist/components/ui/switch.module.js.map +1 -0
- package/dist/components/ui/switch_module.css +45 -0
- package/dist/components/ui/switch_module.css.map +1 -0
- package/dist/components/ui/table-skeleton.js +34 -0
- package/dist/components/ui/table-skeleton.js.map +1 -0
- package/dist/components/ui/table-skeleton.module.js +11 -0
- package/dist/components/ui/table-skeleton.module.js.map +1 -0
- package/dist/components/ui/table-skeleton_module.css +32 -0
- package/dist/components/ui/table-skeleton_module.css.map +1 -0
- package/dist/components/ui/table.d.ts +170 -8
- package/dist/components/ui/table.d.ts.map +1 -1
- package/dist/components/ui/table.js +17 -17
- package/dist/components/ui/table.js.map +1 -1
- package/dist/components/ui/table.module.js +15 -0
- package/dist/components/ui/table.module.js.map +1 -0
- package/dist/components/ui/table_module.css +71 -0
- package/dist/components/ui/table_module.css.map +1 -0
- package/dist/components/ui/tabs.d.ts +114 -5
- package/dist/components/ui/tabs.d.ts.map +1 -1
- package/dist/components/ui/tabs.js +71 -20
- package/dist/components/ui/tabs.js.map +1 -1
- package/dist/components/ui/tabs.module.js +10 -0
- package/dist/components/ui/tabs.module.js.map +1 -0
- package/dist/components/ui/tabs_module.css +89 -0
- package/dist/components/ui/tabs_module.css.map +1 -0
- package/dist/components/ui/textarea.d.ts +24 -1
- package/dist/components/ui/textarea.d.ts.map +1 -1
- package/dist/components/ui/textarea.js +2 -2
- package/dist/components/ui/textarea.js.map +1 -1
- package/dist/components/ui/textarea.module.js +7 -0
- package/dist/components/ui/textarea.module.js.map +1 -0
- package/dist/components/ui/textarea_module.css +33 -0
- package/dist/components/ui/textarea_module.css.map +1 -0
- package/dist/components/ui/timeline.d.ts +111 -0
- package/dist/components/ui/timeline.d.ts.map +1 -0
- package/dist/components/ui/timeline.js +34 -0
- package/dist/components/ui/timeline.js.map +1 -0
- package/dist/components/ui/timeline.module.js +10 -0
- package/dist/components/ui/timeline.module.js.map +1 -0
- package/dist/components/ui/timeline_module.css +47 -0
- package/dist/components/ui/timeline_module.css.map +1 -0
- package/dist/components/ui/toggle-group.d.ts +75 -10
- package/dist/components/ui/toggle-group.d.ts.map +1 -1
- package/dist/components/ui/toggle-group.js +32 -21
- package/dist/components/ui/toggle-group.js.map +1 -1
- package/dist/components/ui/toggle-group.module.js +7 -0
- package/dist/components/ui/toggle-group.module.js.map +1 -0
- package/dist/components/ui/toggle-group_module.css +8 -0
- package/dist/components/ui/toggle-group_module.css.map +1 -0
- package/dist/components/ui/toggle.d.ts +60 -11
- package/dist/components/ui/toggle.d.ts.map +1 -1
- package/dist/components/ui/toggle.js +29 -29
- package/dist/components/ui/toggle.js.map +1 -1
- package/dist/components/ui/toggle.module.js +12 -0
- package/dist/components/ui/toggle.module.js.map +1 -0
- package/dist/components/ui/toggle_module.css +62 -0
- package/dist/components/ui/toggle_module.css.map +1 -0
- package/dist/components/ui/toolbar.d.ts +107 -0
- package/dist/components/ui/toolbar.d.ts.map +1 -0
- package/dist/components/ui/toolbar.js +90 -0
- package/dist/components/ui/toolbar.js.map +1 -0
- package/dist/components/ui/toolbar.module.js +12 -0
- package/dist/components/ui/toolbar.module.js.map +1 -0
- package/dist/components/ui/toolbar_module.css +115 -0
- package/dist/components/ui/toolbar_module.css.map +1 -0
- package/dist/components/ui/tooltip.d.ts +119 -5
- package/dist/components/ui/tooltip.d.ts.map +1 -1
- package/dist/components/ui/tooltip.js +48 -13
- package/dist/components/ui/tooltip.js.map +1 -1
- package/dist/components/ui/tooltip.module.js +9 -0
- package/dist/components/ui/tooltip.module.js.map +1 -0
- package/dist/components/ui/tooltip_module.css +35 -0
- package/dist/components/ui/tooltip_module.css.map +1 -0
- package/dist/components/ui/typewriter.d.ts +48 -13
- package/dist/components/ui/typewriter.d.ts.map +1 -1
- package/dist/components/ui/typewriter.js +46 -49
- package/dist/components/ui/typewriter.js.map +1 -1
- package/dist/components/ui/typewriter.module.js +16 -0
- package/dist/components/ui/typewriter.module.js.map +1 -0
- package/dist/components/ui/typewriter_module.css +102 -0
- package/dist/components/ui/typewriter_module.css.map +1 -0
- package/dist/components/ui/visually-hidden.d.ts +38 -0
- package/dist/components/ui/visually-hidden.d.ts.map +1 -0
- package/dist/components/ui/visually-hidden.js +13 -0
- package/dist/components/ui/visually-hidden.js.map +1 -0
- package/dist/components/ui/visually-hidden.module.js +7 -0
- package/dist/components/ui/visually-hidden.module.js.map +1 -0
- package/dist/components/ui/visually-hidden_module.css +14 -0
- package/dist/components/ui/visually-hidden_module.css.map +1 -0
- package/dist/hooks/useAnnounce.js +46 -0
- package/dist/hooks/useAnnounce.js.map +1 -0
- package/dist/hooks/useBreakpoint.d.ts +17 -0
- package/dist/hooks/useBreakpoint.d.ts.map +1 -0
- package/dist/hooks/useBreakpoint.js +16 -0
- package/dist/hooks/useBreakpoint.js.map +1 -0
- package/dist/hooks/useClipboard.d.ts +77 -0
- package/dist/hooks/useClipboard.d.ts.map +1 -0
- package/dist/hooks/useClipboard.js +42 -0
- package/dist/hooks/useClipboard.js.map +1 -0
- package/dist/hooks/useColorScheme.d.ts +14 -0
- package/dist/hooks/useColorScheme.d.ts.map +1 -0
- package/dist/hooks/useColorScheme.js +9 -0
- package/dist/hooks/useColorScheme.js.map +1 -0
- package/dist/hooks/useControllableState.d.ts +54 -0
- package/dist/hooks/useControllableState.d.ts.map +1 -0
- package/dist/hooks/useControllableState.js +29 -0
- package/dist/hooks/useControllableState.js.map +1 -0
- package/dist/hooks/useDebounce.d.ts +33 -0
- package/dist/hooks/useDebounce.d.ts.map +1 -0
- package/dist/hooks/useDebounce.js +20 -0
- package/dist/hooks/useDebounce.js.map +1 -0
- package/dist/hooks/useEventCallback.d.ts +34 -0
- package/dist/hooks/useEventCallback.d.ts.map +1 -0
- package/dist/hooks/useEventCallback.js +12 -0
- package/dist/hooks/useEventCallback.js.map +1 -0
- package/dist/hooks/useFocusManager.js +51 -0
- package/dist/hooks/useFocusManager.js.map +1 -0
- package/dist/hooks/useFocusVisible.d.ts +50 -0
- package/dist/hooks/useFocusVisible.d.ts.map +1 -0
- package/dist/hooks/useFocusVisible.js +35 -0
- package/dist/hooks/useFocusVisible.js.map +1 -0
- package/dist/hooks/useId.d.ts +30 -0
- package/dist/hooks/useId.d.ts.map +1 -0
- package/dist/hooks/useId.js +9 -0
- package/dist/hooks/useId.js.map +1 -0
- package/dist/hooks/useIntersectionObserver.d.ts +51 -0
- package/dist/hooks/useIntersectionObserver.d.ts.map +1 -0
- package/dist/hooks/useIntersectionObserver.js +25 -0
- package/dist/hooks/useIntersectionObserver.js.map +1 -0
- package/dist/hooks/useInterval.d.ts +55 -0
- package/dist/hooks/useInterval.d.ts.map +1 -0
- package/dist/hooks/useInterval.js +24 -0
- package/dist/hooks/useInterval.js.map +1 -0
- package/dist/hooks/useIsMobile.d.ts +5 -11
- package/dist/hooks/useIsMobile.d.ts.map +1 -1
- package/dist/hooks/useIsMobile.js +2 -13
- package/dist/hooks/useIsMobile.js.map +1 -1
- package/dist/hooks/useLocalStorage.d.ts +43 -0
- package/dist/hooks/useLocalStorage.d.ts.map +1 -0
- package/dist/hooks/useLocalStorage.js +53 -0
- package/dist/hooks/useLocalStorage.js.map +1 -0
- package/dist/hooks/useMediaQuery.d.ts +14 -0
- package/dist/hooks/useMediaQuery.d.ts.map +1 -0
- package/dist/hooks/useMediaQuery.js +20 -0
- package/dist/hooks/useMediaQuery.js.map +1 -0
- package/dist/hooks/useMergedRefs.d.ts +27 -0
- package/dist/hooks/useMergedRefs.d.ts.map +1 -0
- package/dist/hooks/useMergedRefs.js +11 -0
- package/dist/hooks/useMergedRefs.js.map +1 -0
- package/dist/hooks/useOnClickOutside.d.ts +32 -0
- package/dist/hooks/useOnClickOutside.d.ts.map +1 -0
- package/dist/hooks/useOnClickOutside.js +23 -0
- package/dist/hooks/useOnClickOutside.js.map +1 -0
- package/dist/hooks/usePrefersContrast.d.ts +19 -0
- package/dist/hooks/usePrefersContrast.d.ts.map +1 -0
- package/dist/hooks/usePrefersContrast.js +8 -0
- package/dist/hooks/usePrefersContrast.js.map +1 -0
- package/dist/hooks/usePrevious.d.ts +33 -0
- package/dist/hooks/usePrevious.d.ts.map +1 -0
- package/dist/hooks/usePrevious.js +14 -0
- package/dist/hooks/usePrevious.js.map +1 -0
- package/dist/hooks/useReducedMotion.d.ts +19 -0
- package/dist/hooks/useReducedMotion.d.ts.map +1 -0
- package/dist/hooks/useReducedMotion.js +2 -0
- package/dist/hooks/useThrottle.d.ts +37 -0
- package/dist/hooks/useThrottle.d.ts.map +1 -0
- package/dist/hooks/useThrottle.js +34 -0
- package/dist/hooks/useThrottle.js.map +1 -0
- package/dist/hooks/useTimeout.d.ts +28 -0
- package/dist/hooks/useTimeout.d.ts.map +1 -0
- package/dist/hooks/useTimeout.js +24 -0
- package/dist/hooks/useTimeout.js.map +1 -0
- package/dist/index.css +104 -5363
- package/dist/index.css.map +1 -1
- package/dist/index.d.ts +52 -8
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +103 -72
- package/dist/lib/utilities.d.ts +5 -5
- package/dist/lib/utilities.d.ts.map +1 -1
- package/dist/lib/utilities.js +1 -2
- package/dist/lib/utilities.js.map +1 -1
- package/dist/motion/Collapse.js +19 -0
- package/dist/motion/Collapse.js.map +1 -0
- package/dist/motion/Collapse.module.js +8 -0
- package/dist/motion/Collapse.module.js.map +1 -0
- package/dist/motion/Collapse_module.css +25 -0
- package/dist/motion/Collapse_module.css.map +1 -0
- package/dist/motion/Presence.js +14 -0
- package/dist/motion/Presence.js.map +1 -0
- package/dist/motion/index.js +5 -0
- package/dist/motion/presets.js +117 -0
- package/dist/motion/presets.js.map +1 -0
- package/dist/motion/tokens.js +41 -0
- package/dist/motion/tokens.js.map +1 -0
- package/dist/rslib-runtime.js +39 -0
- package/dist/rslib-runtime.js.map +1 -0
- package/package.json +233 -67
- package/src/components/ui/accordion.module.css +70 -0
- package/src/components/ui/accordion.tsx +278 -44
- package/src/components/ui/alert-dialog.module.css +87 -0
- package/src/components/ui/alert-dialog.tsx +481 -99
- package/src/components/ui/alert.module.css +57 -0
- package/src/components/ui/alert.tsx +136 -43
- package/src/components/ui/aspect-ratio.module.css +7 -0
- package/src/components/ui/aspect-ratio.tsx +38 -3
- package/src/components/ui/async-boundary.tsx +56 -0
- package/src/components/ui/avatar.module.css +31 -0
- package/src/components/ui/avatar.tsx +149 -36
- package/src/components/ui/background-beams.module.css +20 -0
- package/src/components/ui/background-beams.tsx +173 -134
- package/src/components/ui/badge.module.css +60 -0
- package/src/components/ui/badge.tsx +100 -32
- package/src/components/ui/breadcrumb.module.css +87 -0
- package/src/components/ui/breadcrumb.tsx +256 -74
- package/src/components/ui/bubble-background.module.css +97 -0
- package/src/components/ui/bubble-background.tsx +92 -52
- package/src/components/ui/button-group.module.css +76 -0
- package/src/components/ui/button-group.tsx +135 -46
- package/src/components/ui/button.module.css +138 -0
- package/src/components/ui/button.tsx +159 -41
- package/src/components/ui/calendar.module.css +250 -0
- package/src/components/ui/calendar.tsx +135 -111
- package/src/components/ui/card-skeleton.module.css +50 -0
- package/src/components/ui/card-skeleton.tsx +69 -0
- package/src/components/ui/card.module.css +41 -0
- package/src/components/ui/card.tsx +175 -22
- package/src/components/ui/carousel.module.css +80 -0
- package/src/components/ui/carousel.tsx +186 -43
- package/src/components/ui/chart.module.css +164 -0
- package/src/components/ui/chart.tsx +447 -102
- package/src/components/ui/checkbox-group.module.css +8 -0
- package/src/components/ui/checkbox-group.tsx +53 -0
- package/src/components/ui/checkbox.module.css +43 -0
- package/src/components/ui/checkbox.tsx +81 -19
- package/src/components/ui/collapsible.module.css +24 -0
- package/src/components/ui/collapsible.tsx +140 -3
- package/src/components/ui/combobox.module.css +158 -0
- package/src/components/ui/combobox.tsx +569 -0
- package/src/components/ui/command.module.css +193 -0
- package/src/components/ui/command.tsx +893 -114
- package/src/components/ui/context-menu.module.css +113 -0
- package/src/components/ui/context-menu.tsx +619 -157
- package/src/components/ui/copy-button.module.css +34 -0
- package/src/components/ui/copy-button.tsx +116 -0
- package/src/components/ui/counting-number.module.css +4 -0
- package/src/components/ui/counting-number.tsx +69 -31
- package/src/components/ui/dialog.module.css +113 -0
- package/src/components/ui/dialog.tsx +427 -81
- package/src/components/ui/dot-background.module.css +12 -0
- package/src/components/ui/dot-background.tsx +134 -126
- package/src/components/ui/drawer.module.css +85 -0
- package/src/components/ui/drawer.tsx +410 -80
- package/src/components/ui/dropdown-menu.module.css +113 -0
- package/src/components/ui/dropdown-menu.tsx +619 -159
- package/src/components/ui/dropdrawer.module.css +322 -0
- package/src/components/ui/dropdrawer.tsx +870 -400
- package/src/components/ui/empty.module.css +84 -0
- package/src/components/ui/empty.tsx +176 -52
- package/src/components/ui/error-boundary.module.css +36 -0
- package/src/components/ui/error-boundary.tsx +127 -0
- package/src/components/ui/field.module.css +179 -0
- package/src/components/ui/field.tsx +345 -139
- package/src/components/ui/fireworks-background.module.css +13 -0
- package/src/components/ui/fireworks-background.tsx +89 -51
- package/src/components/ui/flip-button.module.css +44 -0
- package/src/components/ui/flip-button.tsx +59 -28
- package/src/components/ui/focus-scope.module.css +3 -0
- package/src/components/ui/focus-scope.tsx +160 -0
- package/src/components/ui/form-skeleton.module.css +28 -0
- package/src/components/ui/form-skeleton.tsx +62 -0
- package/src/components/ui/form.module.css +20 -0
- package/src/components/ui/form.tsx +244 -33
- package/src/components/ui/gradient-background.module.css +6 -0
- package/src/components/ui/gradient-background.tsx +27 -5
- package/src/components/ui/gradient-text.module.css +23 -0
- package/src/components/ui/gradient-text.tsx +36 -9
- package/src/components/ui/highlight-text.module.css +14 -0
- package/src/components/ui/highlight-text.tsx +37 -15
- package/src/components/ui/hole-background.module.css +84 -0
- package/src/components/ui/hole-background.tsx +290 -162
- package/src/components/ui/hover-card.module.css +21 -0
- package/src/components/ui/hover-card.tsx +142 -21
- package/src/components/ui/input-group.module.css +148 -0
- package/src/components/ui/input-group.tsx +222 -98
- package/src/components/ui/input-otp.module.css +92 -0
- package/src/components/ui/input-otp.tsx +159 -33
- package/src/components/ui/input.module.css +37 -0
- package/src/components/ui/input.tsx +56 -14
- package/src/components/ui/item.module.css +143 -0
- package/src/components/ui/item.tsx +331 -104
- package/src/components/ui/kbd.module.css +31 -0
- package/src/components/ui/kbd.tsx +60 -17
- package/src/components/ui/label.module.css +13 -0
- package/src/components/ui/label.tsx +38 -14
- package/src/components/ui/list-skeleton.module.css +35 -0
- package/src/components/ui/list-skeleton.tsx +70 -0
- package/src/components/ui/loading-overlay.module.css +19 -0
- package/src/components/ui/loading-overlay.tsx +72 -0
- package/src/components/ui/menubar.module.css +144 -0
- package/src/components/ui/menubar.tsx +604 -192
- package/src/components/ui/meter.module.css +32 -0
- package/src/components/ui/meter.tsx +169 -0
- package/src/components/ui/navigation-menu.module.css +110 -0
- package/src/components/ui/navigation-menu.tsx +381 -99
- package/src/components/ui/number-field.module.css +126 -0
- package/src/components/ui/number-field.tsx +247 -0
- package/src/components/ui/pagination.module.css +64 -0
- package/src/components/ui/pagination.tsx +250 -72
- package/src/components/ui/popover.module.css +26 -0
- package/src/components/ui/popover.tsx +207 -25
- package/src/components/ui/progress.module.css +15 -0
- package/src/components/ui/progress.tsx +54 -16
- package/src/components/ui/radio-group.module.css +41 -0
- package/src/components/ui/radio-group.tsx +88 -34
- package/src/components/ui/resizable.module.css +67 -0
- package/src/components/ui/resizable.tsx +106 -26
- package/src/components/ui/ripple-button.module.css +35 -0
- package/src/components/ui/ripple-button.tsx +55 -21
- package/src/components/ui/scratcher.module.css +9 -0
- package/src/components/ui/scratcher.tsx +134 -108
- package/src/components/ui/scroll-area.module.css +47 -0
- package/src/components/ui/scroll-area.tsx +93 -34
- package/src/components/ui/select.module.css +131 -0
- package/src/components/ui/select.tsx +435 -124
- package/src/components/ui/separator.module.css +14 -0
- package/src/components/ui/separator.tsx +60 -18
- package/src/components/ui/sheet.module.css +138 -0
- package/src/components/ui/sheet.tsx +427 -95
- package/src/components/ui/sidebar.module.css +594 -0
- package/src/components/ui/sidebar.tsx +900 -425
- package/src/components/ui/skeleton.module.css +14 -0
- package/src/components/ui/skeleton.tsx +39 -9
- package/src/components/ui/slider.module.css +48 -0
- package/src/components/ui/slider.tsx +89 -16
- package/src/components/ui/sonner.module.css +246 -0
- package/src/components/ui/sonner.tsx +777 -25
- package/src/components/ui/spinner.module.css +25 -0
- package/src/components/ui/spinner.tsx +51 -12
- package/src/components/ui/stepper.module.css +72 -0
- package/src/components/ui/stepper.tsx +95 -0
- package/src/components/ui/switch.module.css +42 -0
- package/src/components/ui/switch.tsx +53 -20
- package/src/components/ui/table-skeleton.module.css +29 -0
- package/src/components/ui/table-skeleton.tsx +79 -0
- package/src/components/ui/table.module.css +66 -0
- package/src/components/ui/table.tsx +235 -61
- package/src/components/ui/tabs.module.css +89 -0
- package/src/components/ui/tabs.tsx +192 -45
- package/src/components/ui/textarea.module.css +30 -0
- package/src/components/ui/textarea.tsx +30 -10
- package/src/components/ui/timeline.module.css +43 -0
- package/src/components/ui/timeline.tsx +153 -0
- package/src/components/ui/toggle-group.module.css +5 -0
- package/src/components/ui/toggle-group.tsx +115 -32
- package/src/components/ui/toggle.module.css +57 -0
- package/src/components/ui/toggle.tsx +89 -33
- package/src/components/ui/toolbar.module.css +112 -0
- package/src/components/ui/toolbar.tsx +209 -0
- package/src/components/ui/tooltip.module.css +39 -0
- package/src/components/ui/tooltip.tsx +181 -24
- package/src/components/ui/typewriter.module.css +101 -0
- package/src/components/ui/typewriter.tsx +130 -128
- package/src/components/ui/visually-hidden.module.css +11 -0
- package/src/components/ui/visually-hidden.tsx +50 -0
- package/src/css-modules.d.ts +9 -0
- package/src/hooks/useAnnounce.tsx +73 -0
- package/src/hooks/useBreakpoint.tsx +41 -0
- package/src/hooks/useClipboard.tsx +137 -0
- package/src/hooks/useColorScheme.tsx +23 -0
- package/src/hooks/useControllableState.tsx +81 -0
- package/src/hooks/useDebounce.tsx +50 -0
- package/src/hooks/useEventCallback.tsx +47 -0
- package/src/hooks/useFocusManager.tsx +89 -0
- package/src/hooks/useFocusVisible.tsx +88 -0
- package/src/hooks/useId.tsx +36 -0
- package/src/hooks/useIntersectionObserver.tsx +81 -0
- package/src/hooks/useInterval.tsx +80 -0
- package/src/hooks/useIsMobile.tsx +7 -28
- package/src/hooks/useLocalStorage.tsx +111 -0
- package/src/hooks/useMediaQuery.tsx +34 -0
- package/src/hooks/useMergedRefs.tsx +48 -0
- package/src/hooks/useOnClickOutside.tsx +55 -0
- package/src/hooks/usePrefersContrast.tsx +24 -0
- package/src/hooks/usePrevious.tsx +44 -0
- package/src/hooks/useReducedMotion.tsx +20 -0
- package/src/hooks/useThrottle.tsx +78 -0
- package/src/hooks/useTimeout.tsx +51 -0
- package/src/index.css +127 -65
- package/src/index.ts +219 -18
- package/src/lib/utilities.ts +8 -7
- package/src/motion/Collapse.module.css +22 -0
- package/src/motion/Collapse.tsx +52 -0
- package/src/motion/Presence.tsx +44 -0
- package/src/motion/index.ts +13 -0
- package/src/motion/presets.ts +77 -0
- package/src/motion/tokens.ts +37 -0
- package/src/stories/DesignPrinciples.mdx +48 -0
- package/src/stories/GettingStarted.mdx +92 -0
- package/src/stories/Welcome.mdx +44 -0
- package/src/hooks/useIsMobile.test.tsx +0 -96
- package/src/hooks/useWindowSize.test.tsx +0 -57
- package/src/index.test.ts +0 -537
- package/src/lib/color-conversion-utilities.test.ts +0 -225
- package/src/lib/utilities.test.ts +0 -37
package/EXAMPLES.md
CHANGED
|
@@ -1,22 +1,61 @@
|
|
|
1
1
|
# 💡 Usage Examples for @arolariu/components
|
|
2
2
|
|
|
3
|
-
> **Real-world examples to get you building faster
|
|
3
|
+
> **Real-world examples to get you building faster.** Copy, paste, and customize these patterns for your projects.
|
|
4
4
|
|
|
5
5
|
## 🚀 Getting Started
|
|
6
6
|
|
|
7
7
|
### Installation & Setup
|
|
8
8
|
|
|
9
9
|
```bash
|
|
10
|
-
# Install the package
|
|
11
10
|
npm install @arolariu/components
|
|
12
11
|
|
|
13
|
-
#
|
|
14
|
-
npm install react react-dom
|
|
12
|
+
# Peer dependencies (install if not already in your project)
|
|
13
|
+
npm install react react-dom @base-ui/react motion
|
|
15
14
|
```
|
|
16
15
|
|
|
17
16
|
```tsx
|
|
18
|
-
//
|
|
17
|
+
// Import design tokens only once in your app entry point
|
|
19
18
|
import "@arolariu/components/styles";
|
|
19
|
+
|
|
20
|
+
// Components auto-load their CSS when imported
|
|
21
|
+
import { Button, Card } from "@arolariu/components";
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
`@arolariu/components/styles` provides design tokens only. Component CSS is loaded automatically when components are imported.
|
|
25
|
+
|
|
26
|
+
```tsx
|
|
27
|
+
// Use local CSS Modules for application-specific layout and composition
|
|
28
|
+
import styles from "./my-component.module.css";
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
`tailwindcss` is not a peer dependency in v1.0.0.
|
|
32
|
+
|
|
33
|
+
### Useful Subpath Imports
|
|
34
|
+
|
|
35
|
+
```tsx
|
|
36
|
+
import { Button } from "@arolariu/components/button";
|
|
37
|
+
import { useIsMobile } from "@arolariu/components/useIsMobile";
|
|
38
|
+
import { cn } from "@arolariu/components/utilities";
|
|
39
|
+
import { hexToHsl } from "@arolariu/components/color-conversion-utilities";
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
### Composition with the `render` Prop
|
|
43
|
+
|
|
44
|
+
```tsx
|
|
45
|
+
import { Button } from "@arolariu/components";
|
|
46
|
+
|
|
47
|
+
// Use render prop instead of asChild
|
|
48
|
+
<Button render={<a href="/dashboard" />}>
|
|
49
|
+
Go to Dashboard
|
|
50
|
+
</Button>
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
```css
|
|
54
|
+
/* my-component.module.css */
|
|
55
|
+
.page {
|
|
56
|
+
min-height: 100vh;
|
|
57
|
+
padding: 2rem;
|
|
58
|
+
}
|
|
20
59
|
```
|
|
21
60
|
|
|
22
61
|
---
|
|
@@ -26,51 +65,91 @@ import "@arolariu/components/styles";
|
|
|
26
65
|
### Simple Card Layout
|
|
27
66
|
|
|
28
67
|
```tsx
|
|
68
|
+
import { Badge } from "@arolariu/components/badge";
|
|
69
|
+
import { Button } from "@arolariu/components/button";
|
|
29
70
|
import {
|
|
30
71
|
Card,
|
|
31
|
-
CardHeader,
|
|
32
|
-
CardTitle,
|
|
33
72
|
CardContent,
|
|
34
73
|
CardFooter,
|
|
74
|
+
CardHeader,
|
|
75
|
+
CardTitle,
|
|
35
76
|
} from "@arolariu/components/card";
|
|
36
|
-
import
|
|
37
|
-
import { Badge } from "@arolariu/components/badge";
|
|
77
|
+
import styles from "./product-card.module.css";
|
|
38
78
|
|
|
39
79
|
export function ProductCard() {
|
|
40
80
|
return (
|
|
41
|
-
<Card className=
|
|
81
|
+
<Card className={styles.card}>
|
|
42
82
|
<CardHeader>
|
|
43
|
-
<div className=
|
|
83
|
+
<div className={styles.headerRow}>
|
|
44
84
|
<CardTitle>Premium Plan</CardTitle>
|
|
45
85
|
<Badge variant="secondary">Popular</Badge>
|
|
46
86
|
</div>
|
|
47
87
|
</CardHeader>
|
|
48
|
-
<CardContent>
|
|
49
|
-
<p className=
|
|
50
|
-
$29<span className=
|
|
88
|
+
<CardContent className={styles.content}>
|
|
89
|
+
<p className={styles.price}>
|
|
90
|
+
$29<span className={styles.priceSuffix}>/month</span>
|
|
51
91
|
</p>
|
|
52
|
-
<ul className=
|
|
92
|
+
<ul className={styles.featureList}>
|
|
53
93
|
<li>✅ Unlimited projects</li>
|
|
54
94
|
<li>✅ Priority support</li>
|
|
55
95
|
<li>✅ Advanced analytics</li>
|
|
56
96
|
</ul>
|
|
57
97
|
</CardContent>
|
|
58
98
|
<CardFooter>
|
|
59
|
-
<Button className=
|
|
99
|
+
<Button className={styles.primaryAction}>Subscribe Now</Button>
|
|
60
100
|
</CardFooter>
|
|
61
101
|
</Card>
|
|
62
102
|
);
|
|
63
103
|
}
|
|
64
104
|
```
|
|
65
105
|
|
|
106
|
+
```css
|
|
107
|
+
/* product-card.module.css */
|
|
108
|
+
.card {
|
|
109
|
+
width: 24rem;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
.headerRow {
|
|
113
|
+
display: flex;
|
|
114
|
+
align-items: center;
|
|
115
|
+
justify-content: space-between;
|
|
116
|
+
gap: 1rem;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
.content {
|
|
120
|
+
display: grid;
|
|
121
|
+
gap: 1rem;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
.price {
|
|
125
|
+
font-size: 1.875rem;
|
|
126
|
+
font-weight: 700;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
.priceSuffix {
|
|
130
|
+
font-size: 0.875rem;
|
|
131
|
+
font-weight: 400;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
.featureList {
|
|
135
|
+
display: grid;
|
|
136
|
+
gap: 0.5rem;
|
|
137
|
+
padding-left: 1.25rem;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
.primaryAction {
|
|
141
|
+
width: 100%;
|
|
142
|
+
}
|
|
143
|
+
```
|
|
144
|
+
|
|
66
145
|
### Dashboard Layout with Sidebar
|
|
67
146
|
|
|
68
147
|
```tsx
|
|
69
148
|
import {
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
} from "@arolariu/components/
|
|
149
|
+
Avatar,
|
|
150
|
+
AvatarFallback,
|
|
151
|
+
AvatarImage,
|
|
152
|
+
} from "@arolariu/components/avatar";
|
|
74
153
|
import {
|
|
75
154
|
Card,
|
|
76
155
|
CardContent,
|
|
@@ -79,61 +158,73 @@ import {
|
|
|
79
158
|
} from "@arolariu/components/card";
|
|
80
159
|
import { Progress } from "@arolariu/components/progress";
|
|
81
160
|
import {
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
161
|
+
Sidebar,
|
|
162
|
+
SidebarContent,
|
|
163
|
+
SidebarMenu,
|
|
164
|
+
SidebarMenuButton,
|
|
165
|
+
SidebarMenuItem,
|
|
166
|
+
SidebarProvider,
|
|
167
|
+
} from "@arolariu/components/sidebar";
|
|
168
|
+
import styles from "./dashboard.module.css";
|
|
86
169
|
|
|
87
170
|
export function Dashboard() {
|
|
88
171
|
return (
|
|
89
|
-
<
|
|
90
|
-
{
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
</
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
172
|
+
<SidebarProvider>
|
|
173
|
+
<div className={styles.layout}>
|
|
174
|
+
<Sidebar>
|
|
175
|
+
<SidebarContent>
|
|
176
|
+
<SidebarMenu>
|
|
177
|
+
<SidebarMenuItem>
|
|
178
|
+
<SidebarMenuButton isActive>Dashboard</SidebarMenuButton>
|
|
179
|
+
</SidebarMenuItem>
|
|
180
|
+
<SidebarMenuItem>
|
|
181
|
+
<SidebarMenuButton>Projects</SidebarMenuButton>
|
|
182
|
+
</SidebarMenuItem>
|
|
183
|
+
<SidebarMenuItem>
|
|
184
|
+
<SidebarMenuButton>Settings</SidebarMenuButton>
|
|
185
|
+
</SidebarMenuItem>
|
|
186
|
+
</SidebarMenu>
|
|
187
|
+
</SidebarContent>
|
|
188
|
+
</Sidebar>
|
|
189
|
+
|
|
190
|
+
<main className={styles.main}>
|
|
191
|
+
<h1 className={styles.title}>Dashboard</h1>
|
|
192
|
+
|
|
193
|
+
<div className={styles.cardGrid}>
|
|
194
|
+
<Card>
|
|
195
|
+
<CardHeader>
|
|
196
|
+
<CardTitle>Project Progress</CardTitle>
|
|
197
|
+
</CardHeader>
|
|
198
|
+
<CardContent className={styles.stack}>
|
|
199
|
+
<Progress value={75} />
|
|
200
|
+
<p className={styles.mutedText}>75% complete</p>
|
|
201
|
+
</CardContent>
|
|
202
|
+
</Card>
|
|
203
|
+
|
|
204
|
+
<Card>
|
|
205
|
+
<CardHeader>
|
|
206
|
+
<CardTitle>Team Members</CardTitle>
|
|
207
|
+
</CardHeader>
|
|
208
|
+
<CardContent>
|
|
209
|
+
<div className={styles.avatarRow}>
|
|
210
|
+
<Avatar className={styles.avatar}>
|
|
211
|
+
<AvatarImage src="/avatar1.jpg" />
|
|
212
|
+
<AvatarFallback>JD</AvatarFallback>
|
|
213
|
+
</Avatar>
|
|
214
|
+
<Avatar className={styles.avatar}>
|
|
215
|
+
<AvatarImage src="/avatar2.jpg" />
|
|
216
|
+
<AvatarFallback>SM</AvatarFallback>
|
|
217
|
+
</Avatar>
|
|
218
|
+
<Avatar className={styles.avatar}>
|
|
219
|
+
<AvatarFallback>+3</AvatarFallback>
|
|
220
|
+
</Avatar>
|
|
221
|
+
</div>
|
|
222
|
+
</CardContent>
|
|
223
|
+
</Card>
|
|
224
|
+
</div>
|
|
225
|
+
</main>
|
|
226
|
+
</div>
|
|
227
|
+
</SidebarProvider>
|
|
137
228
|
);
|
|
138
229
|
}
|
|
139
230
|
```
|
|
@@ -145,19 +236,21 @@ export function Dashboard() {
|
|
|
145
236
|
### Complete Login Form
|
|
146
237
|
|
|
147
238
|
```tsx
|
|
239
|
+
import { useState } from "react";
|
|
240
|
+
|
|
241
|
+
import { Alert, AlertDescription } from "@arolariu/components/alert";
|
|
242
|
+
import { Button } from "@arolariu/components/button";
|
|
148
243
|
import {
|
|
149
244
|
Card,
|
|
150
|
-
CardHeader,
|
|
151
|
-
CardTitle,
|
|
152
245
|
CardContent,
|
|
153
246
|
CardFooter,
|
|
247
|
+
CardHeader,
|
|
248
|
+
CardTitle,
|
|
154
249
|
} from "@arolariu/components/card";
|
|
250
|
+
import { Checkbox } from "@arolariu/components/checkbox";
|
|
155
251
|
import { Input } from "@arolariu/components/input";
|
|
156
|
-
import { Button } from "@arolariu/components/button";
|
|
157
252
|
import { Label } from "@arolariu/components/label";
|
|
158
|
-
import
|
|
159
|
-
import { Alert, AlertDescription } from "@arolariu/components/alert";
|
|
160
|
-
import { useState } from "react";
|
|
253
|
+
import styles from "./login-form.module.css";
|
|
161
254
|
|
|
162
255
|
export function LoginForm() {
|
|
163
256
|
const [email, setEmail] = useState("");
|
|
@@ -166,16 +259,15 @@ export function LoginForm() {
|
|
|
166
259
|
const [error, setError] = useState("");
|
|
167
260
|
const [loading, setLoading] = useState(false);
|
|
168
261
|
|
|
169
|
-
const handleSubmit = async (
|
|
170
|
-
|
|
262
|
+
const handleSubmit = async (event: React.FormEvent) => {
|
|
263
|
+
event.preventDefault();
|
|
171
264
|
setLoading(true);
|
|
172
265
|
setError("");
|
|
173
266
|
|
|
174
267
|
try {
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
} catch (err) {
|
|
268
|
+
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
269
|
+
console.log("Login successful");
|
|
270
|
+
} catch {
|
|
179
271
|
setError("Invalid email or password");
|
|
180
272
|
} finally {
|
|
181
273
|
setLoading(false);
|
|
@@ -183,43 +275,43 @@ export function LoginForm() {
|
|
|
183
275
|
};
|
|
184
276
|
|
|
185
277
|
return (
|
|
186
|
-
<div className=
|
|
187
|
-
<Card className=
|
|
278
|
+
<div className={styles.page}>
|
|
279
|
+
<Card className={styles.card}>
|
|
188
280
|
<CardHeader>
|
|
189
|
-
<CardTitle className=
|
|
281
|
+
<CardTitle className={styles.centeredTitle}>Welcome Back</CardTitle>
|
|
190
282
|
</CardHeader>
|
|
191
283
|
<form onSubmit={handleSubmit}>
|
|
192
|
-
<CardContent className=
|
|
193
|
-
{error
|
|
284
|
+
<CardContent className={styles.content}>
|
|
285
|
+
{error ? (
|
|
194
286
|
<Alert variant="destructive">
|
|
195
287
|
<AlertDescription>{error}</AlertDescription>
|
|
196
288
|
</Alert>
|
|
197
|
-
)}
|
|
289
|
+
) : null}
|
|
198
290
|
|
|
199
|
-
<div className=
|
|
291
|
+
<div className={styles.field}>
|
|
200
292
|
<Label htmlFor="email">Email</Label>
|
|
201
293
|
<Input
|
|
202
294
|
id="email"
|
|
203
295
|
type="email"
|
|
204
296
|
placeholder="you@example.com"
|
|
205
297
|
value={email}
|
|
206
|
-
onChange={(
|
|
298
|
+
onChange={(event) => setEmail(event.target.value)}
|
|
207
299
|
required
|
|
208
300
|
/>
|
|
209
301
|
</div>
|
|
210
302
|
|
|
211
|
-
<div className=
|
|
303
|
+
<div className={styles.field}>
|
|
212
304
|
<Label htmlFor="password">Password</Label>
|
|
213
305
|
<Input
|
|
214
306
|
id="password"
|
|
215
307
|
type="password"
|
|
216
308
|
value={password}
|
|
217
|
-
onChange={(
|
|
309
|
+
onChange={(event) => setPassword(event.target.value)}
|
|
218
310
|
required
|
|
219
311
|
/>
|
|
220
312
|
</div>
|
|
221
313
|
|
|
222
|
-
<div className=
|
|
314
|
+
<div className={styles.checkboxRow}>
|
|
223
315
|
<Checkbox
|
|
224
316
|
id="remember"
|
|
225
317
|
checked={rememberMe}
|
|
@@ -229,13 +321,20 @@ export function LoginForm() {
|
|
|
229
321
|
</div>
|
|
230
322
|
</CardContent>
|
|
231
323
|
|
|
232
|
-
<CardFooter className=
|
|
233
|
-
<Button
|
|
324
|
+
<CardFooter className={styles.footer}>
|
|
325
|
+
<Button
|
|
326
|
+
type="submit"
|
|
327
|
+
className={styles.submitButton}
|
|
328
|
+
disabled={loading}
|
|
329
|
+
>
|
|
234
330
|
{loading ? "Signing in..." : "Sign In"}
|
|
235
331
|
</Button>
|
|
236
|
-
<
|
|
332
|
+
<a
|
|
333
|
+
className={styles.link}
|
|
334
|
+
href="/forgot-password"
|
|
335
|
+
>
|
|
237
336
|
Forgot your password?
|
|
238
|
-
</
|
|
337
|
+
</a>
|
|
239
338
|
</CardFooter>
|
|
240
339
|
</form>
|
|
241
340
|
</Card>
|
|
@@ -247,9 +346,11 @@ export function LoginForm() {
|
|
|
247
346
|
### Advanced Form with Validation
|
|
248
347
|
|
|
249
348
|
```tsx
|
|
250
|
-
import { useForm } from "react-hook-form";
|
|
251
349
|
import { zodResolver } from "@hookform/resolvers/zod";
|
|
350
|
+
import { useForm } from "react-hook-form";
|
|
252
351
|
import * as z from "zod";
|
|
352
|
+
|
|
353
|
+
import { Button } from "@arolariu/components/button";
|
|
253
354
|
import {
|
|
254
355
|
Form,
|
|
255
356
|
FormControl,
|
|
@@ -260,7 +361,6 @@ import {
|
|
|
260
361
|
FormMessage,
|
|
261
362
|
} from "@arolariu/components/form";
|
|
262
363
|
import { Input } from "@arolariu/components/input";
|
|
263
|
-
import { Button } from "@arolariu/components/button";
|
|
264
364
|
import {
|
|
265
365
|
Select,
|
|
266
366
|
SelectContent,
|
|
@@ -269,6 +369,7 @@ import {
|
|
|
269
369
|
SelectValue,
|
|
270
370
|
} from "@arolariu/components/select";
|
|
271
371
|
import { Textarea } from "@arolariu/components/textarea";
|
|
372
|
+
import styles from "./profile-form.module.css";
|
|
272
373
|
|
|
273
374
|
const formSchema = z.object({
|
|
274
375
|
firstName: z.string().min(2, "First name must be at least 2 characters"),
|
|
@@ -298,9 +399,9 @@ export function ProfileForm() {
|
|
|
298
399
|
<Form {...form}>
|
|
299
400
|
<form
|
|
300
401
|
onSubmit={form.handleSubmit(onSubmit)}
|
|
301
|
-
className=
|
|
402
|
+
className={styles.form}
|
|
302
403
|
>
|
|
303
|
-
<div className=
|
|
404
|
+
<div className={styles.twoColumnGrid}>
|
|
304
405
|
<FormField
|
|
305
406
|
control={form.control}
|
|
306
407
|
name="firstName"
|
|
@@ -308,7 +409,10 @@ export function ProfileForm() {
|
|
|
308
409
|
<FormItem>
|
|
309
410
|
<FormLabel>First Name</FormLabel>
|
|
310
411
|
<FormControl>
|
|
311
|
-
<Input
|
|
412
|
+
<Input
|
|
413
|
+
placeholder="John"
|
|
414
|
+
{...field}
|
|
415
|
+
/>
|
|
312
416
|
</FormControl>
|
|
313
417
|
<FormMessage />
|
|
314
418
|
</FormItem>
|
|
@@ -322,7 +426,10 @@ export function ProfileForm() {
|
|
|
322
426
|
<FormItem>
|
|
323
427
|
<FormLabel>Last Name</FormLabel>
|
|
324
428
|
<FormControl>
|
|
325
|
-
<Input
|
|
429
|
+
<Input
|
|
430
|
+
placeholder="Doe"
|
|
431
|
+
{...field}
|
|
432
|
+
/>
|
|
326
433
|
</FormControl>
|
|
327
434
|
<FormMessage />
|
|
328
435
|
</FormItem>
|
|
@@ -337,10 +444,13 @@ export function ProfileForm() {
|
|
|
337
444
|
<FormItem>
|
|
338
445
|
<FormLabel>Email</FormLabel>
|
|
339
446
|
<FormControl>
|
|
340
|
-
<Input
|
|
447
|
+
<Input
|
|
448
|
+
placeholder="john.doe@example.com"
|
|
449
|
+
{...field}
|
|
450
|
+
/>
|
|
341
451
|
</FormControl>
|
|
342
452
|
<FormDescription>
|
|
343
|
-
We
|
|
453
|
+
We'll never share your email with anyone else.
|
|
344
454
|
</FormDescription>
|
|
345
455
|
<FormMessage />
|
|
346
456
|
</FormItem>
|
|
@@ -353,7 +463,10 @@ export function ProfileForm() {
|
|
|
353
463
|
render={({ field }) => (
|
|
354
464
|
<FormItem>
|
|
355
465
|
<FormLabel>Role</FormLabel>
|
|
356
|
-
<Select
|
|
466
|
+
<Select
|
|
467
|
+
defaultValue={field.value}
|
|
468
|
+
onValueChange={field.onChange}
|
|
469
|
+
>
|
|
357
470
|
<FormControl>
|
|
358
471
|
<SelectTrigger>
|
|
359
472
|
<SelectValue placeholder="Select a role" />
|
|
@@ -379,7 +492,7 @@ export function ProfileForm() {
|
|
|
379
492
|
<FormControl>
|
|
380
493
|
<Textarea
|
|
381
494
|
placeholder="Tell us about yourself"
|
|
382
|
-
className=
|
|
495
|
+
className={styles.textarea}
|
|
383
496
|
{...field}
|
|
384
497
|
/>
|
|
385
498
|
</FormControl>
|
|
@@ -402,17 +515,10 @@ export function ProfileForm() {
|
|
|
402
515
|
### Responsive Header Navigation
|
|
403
516
|
|
|
404
517
|
```tsx
|
|
405
|
-
import {
|
|
406
|
-
NavigationMenu,
|
|
407
|
-
NavigationMenuItem,
|
|
408
|
-
NavigationMenuList,
|
|
409
|
-
} from "@arolariu/components/navigation-menu";
|
|
410
|
-
import { Button } from "@arolariu/components/button";
|
|
411
|
-
import { Sheet, SheetContent, SheetTrigger } from "@arolariu/components/sheet";
|
|
412
518
|
import {
|
|
413
519
|
Avatar,
|
|
414
|
-
AvatarImage,
|
|
415
520
|
AvatarFallback,
|
|
521
|
+
AvatarImage,
|
|
416
522
|
} from "@arolariu/components/avatar";
|
|
417
523
|
import {
|
|
418
524
|
DropdownMenu,
|
|
@@ -420,101 +526,82 @@ import {
|
|
|
420
526
|
DropdownMenuItem,
|
|
421
527
|
DropdownMenuTrigger,
|
|
422
528
|
} from "@arolariu/components/dropdown-menu";
|
|
423
|
-
import {
|
|
529
|
+
import {
|
|
530
|
+
NavigationMenu,
|
|
531
|
+
NavigationMenuItem,
|
|
532
|
+
NavigationMenuLink,
|
|
533
|
+
NavigationMenuList,
|
|
534
|
+
} from "@arolariu/components/navigation-menu";
|
|
535
|
+
import { Sheet, SheetContent, SheetTrigger } from "@arolariu/components/sheet";
|
|
536
|
+
import { LogOut, MenuIcon, Settings, User } from "lucide-react";
|
|
537
|
+
import styles from "./app-header.module.css";
|
|
424
538
|
|
|
425
539
|
export function AppHeader() {
|
|
426
540
|
return (
|
|
427
|
-
<header className=
|
|
428
|
-
<div className=
|
|
429
|
-
<div className=
|
|
430
|
-
<a
|
|
431
|
-
|
|
541
|
+
<header className={styles.header}>
|
|
542
|
+
<div className={styles.headerInner}>
|
|
543
|
+
<div className={styles.desktopArea}>
|
|
544
|
+
<a
|
|
545
|
+
className={styles.brandLink}
|
|
546
|
+
href="/"
|
|
547
|
+
>
|
|
548
|
+
<span className={styles.brandName}>MyApp</span>
|
|
432
549
|
</a>
|
|
433
550
|
|
|
434
551
|
<NavigationMenu>
|
|
435
552
|
<NavigationMenuList>
|
|
436
553
|
<NavigationMenuItem>
|
|
437
|
-
<
|
|
438
|
-
Dashboard
|
|
439
|
-
</Button>
|
|
554
|
+
<NavigationMenuLink href="/dashboard">Dashboard</NavigationMenuLink>
|
|
440
555
|
</NavigationMenuItem>
|
|
441
556
|
<NavigationMenuItem>
|
|
442
|
-
<
|
|
443
|
-
Projects
|
|
444
|
-
</Button>
|
|
557
|
+
<NavigationMenuLink href="/projects">Projects</NavigationMenuLink>
|
|
445
558
|
</NavigationMenuItem>
|
|
446
559
|
<NavigationMenuItem>
|
|
447
|
-
<
|
|
448
|
-
Analytics
|
|
449
|
-
</Button>
|
|
560
|
+
<NavigationMenuLink href="/analytics">Analytics</NavigationMenuLink>
|
|
450
561
|
</NavigationMenuItem>
|
|
451
562
|
</NavigationMenuList>
|
|
452
563
|
</NavigationMenu>
|
|
453
564
|
</div>
|
|
454
565
|
|
|
455
|
-
{/* Mobile Navigation */}
|
|
456
566
|
<Sheet>
|
|
457
|
-
<SheetTrigger
|
|
458
|
-
<
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
>
|
|
462
|
-
<MenuIcon className="h-5 w-5" />
|
|
463
|
-
</Button>
|
|
567
|
+
<SheetTrigger
|
|
568
|
+
render={<button type="button" className={styles.mobileMenuButton} />}
|
|
569
|
+
>
|
|
570
|
+
<MenuIcon />
|
|
464
571
|
</SheetTrigger>
|
|
465
572
|
<SheetContent side="left">
|
|
466
|
-
<nav className=
|
|
467
|
-
<
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
href="/dashboard"
|
|
471
|
-
>
|
|
472
|
-
Dashboard
|
|
473
|
-
</Button>
|
|
474
|
-
<Button
|
|
475
|
-
variant="ghost"
|
|
476
|
-
className="justify-start"
|
|
477
|
-
href="/projects"
|
|
478
|
-
>
|
|
479
|
-
Projects
|
|
480
|
-
</Button>
|
|
481
|
-
<Button
|
|
482
|
-
variant="ghost"
|
|
483
|
-
className="justify-start"
|
|
484
|
-
href="/analytics"
|
|
485
|
-
>
|
|
486
|
-
Analytics
|
|
487
|
-
</Button>
|
|
573
|
+
<nav className={styles.mobileNav}>
|
|
574
|
+
<a href="/dashboard">Dashboard</a>
|
|
575
|
+
<a href="/projects">Projects</a>
|
|
576
|
+
<a href="/analytics">Analytics</a>
|
|
488
577
|
</nav>
|
|
489
578
|
</SheetContent>
|
|
490
579
|
</Sheet>
|
|
491
580
|
|
|
492
|
-
<div className=
|
|
493
|
-
<div className="w-full flex-1 md:w-auto md:flex-none">
|
|
494
|
-
{/* Search or other content can go here */}
|
|
495
|
-
</div>
|
|
496
|
-
|
|
497
|
-
{/* User Menu */}
|
|
581
|
+
<div className={styles.actions}>
|
|
498
582
|
<DropdownMenu>
|
|
499
|
-
<DropdownMenuTrigger
|
|
500
|
-
<
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
583
|
+
<DropdownMenuTrigger
|
|
584
|
+
render={<button type="button" className={styles.avatarButton} />}
|
|
585
|
+
>
|
|
586
|
+
<Avatar>
|
|
587
|
+
<AvatarImage
|
|
588
|
+
src="/avatars/01.png"
|
|
589
|
+
alt="User"
|
|
590
|
+
/>
|
|
591
|
+
<AvatarFallback>JD</AvatarFallback>
|
|
592
|
+
</Avatar>
|
|
506
593
|
</DropdownMenuTrigger>
|
|
507
|
-
<DropdownMenuContent
|
|
594
|
+
<DropdownMenuContent align="end">
|
|
508
595
|
<DropdownMenuItem>
|
|
509
|
-
<User
|
|
596
|
+
<User />
|
|
510
597
|
<span>Profile</span>
|
|
511
598
|
</DropdownMenuItem>
|
|
512
599
|
<DropdownMenuItem>
|
|
513
|
-
<Settings
|
|
600
|
+
<Settings />
|
|
514
601
|
<span>Settings</span>
|
|
515
602
|
</DropdownMenuItem>
|
|
516
603
|
<DropdownMenuItem>
|
|
517
|
-
<LogOut
|
|
604
|
+
<LogOut />
|
|
518
605
|
<span>Log out</span>
|
|
519
606
|
</DropdownMenuItem>
|
|
520
607
|
</DropdownMenuContent>
|
|
@@ -533,6 +620,13 @@ export function AppHeader() {
|
|
|
533
620
|
### Interactive Data Table
|
|
534
621
|
|
|
535
622
|
```tsx
|
|
623
|
+
import { Badge } from "@arolariu/components/badge";
|
|
624
|
+
import {
|
|
625
|
+
DropdownMenu,
|
|
626
|
+
DropdownMenuContent,
|
|
627
|
+
DropdownMenuItem,
|
|
628
|
+
DropdownMenuTrigger,
|
|
629
|
+
} from "@arolariu/components/dropdown-menu";
|
|
536
630
|
import {
|
|
537
631
|
Table,
|
|
538
632
|
TableBody,
|
|
@@ -542,15 +636,8 @@ import {
|
|
|
542
636
|
TableHeader,
|
|
543
637
|
TableRow,
|
|
544
638
|
} from "@arolariu/components/table";
|
|
545
|
-
import {
|
|
546
|
-
import
|
|
547
|
-
import {
|
|
548
|
-
DropdownMenu,
|
|
549
|
-
DropdownMenuContent,
|
|
550
|
-
DropdownMenuItem,
|
|
551
|
-
DropdownMenuTrigger,
|
|
552
|
-
} from "@arolariu/components/dropdown-menu";
|
|
553
|
-
import { MoreHorizontal, Eye, Edit, Trash } from "lucide-react";
|
|
639
|
+
import { Edit, Eye, MoreHorizontal, Trash } from "lucide-react";
|
|
640
|
+
import styles from "./users-table.module.css";
|
|
554
641
|
|
|
555
642
|
interface User {
|
|
556
643
|
id: string;
|
|
@@ -600,8 +687,8 @@ export function UsersTable() {
|
|
|
600
687
|
};
|
|
601
688
|
|
|
602
689
|
return (
|
|
603
|
-
<div className=
|
|
604
|
-
<div className=
|
|
690
|
+
<div className={styles.wrapper}>
|
|
691
|
+
<div className={styles.tableShell}>
|
|
605
692
|
<Table>
|
|
606
693
|
<TableCaption>A list of your team members.</TableCaption>
|
|
607
694
|
<TableHeader>
|
|
@@ -611,35 +698,35 @@ export function UsersTable() {
|
|
|
611
698
|
<TableHead>Role</TableHead>
|
|
612
699
|
<TableHead>Status</TableHead>
|
|
613
700
|
<TableHead>Last Login</TableHead>
|
|
614
|
-
<TableHead className=
|
|
701
|
+
<TableHead className={styles.actionsColumn}>Actions</TableHead>
|
|
615
702
|
</TableRow>
|
|
616
703
|
</TableHeader>
|
|
617
704
|
<TableBody>
|
|
618
705
|
{users.map((user) => (
|
|
619
706
|
<TableRow key={user.id}>
|
|
620
|
-
<TableCell className=
|
|
707
|
+
<TableCell className={styles.emphasisCell}>{user.name}</TableCell>
|
|
621
708
|
<TableCell>{user.email}</TableCell>
|
|
622
709
|
<TableCell>{user.role}</TableCell>
|
|
623
710
|
<TableCell>{getStatusBadge(user.status)}</TableCell>
|
|
624
711
|
<TableCell>{user.lastLogin}</TableCell>
|
|
625
712
|
<TableCell>
|
|
626
713
|
<DropdownMenu>
|
|
627
|
-
<DropdownMenuTrigger
|
|
628
|
-
<
|
|
629
|
-
|
|
630
|
-
|
|
714
|
+
<DropdownMenuTrigger
|
|
715
|
+
render={<button type="button" className={styles.iconButton} />}
|
|
716
|
+
>
|
|
717
|
+
<MoreHorizontal />
|
|
631
718
|
</DropdownMenuTrigger>
|
|
632
719
|
<DropdownMenuContent align="end">
|
|
633
720
|
<DropdownMenuItem>
|
|
634
|
-
<Eye
|
|
721
|
+
<Eye />
|
|
635
722
|
View
|
|
636
723
|
</DropdownMenuItem>
|
|
637
724
|
<DropdownMenuItem>
|
|
638
|
-
<Edit
|
|
725
|
+
<Edit />
|
|
639
726
|
Edit
|
|
640
727
|
</DropdownMenuItem>
|
|
641
|
-
<DropdownMenuItem className=
|
|
642
|
-
<Trash
|
|
728
|
+
<DropdownMenuItem className={styles.dangerItem}>
|
|
729
|
+
<Trash />
|
|
643
730
|
Delete
|
|
644
731
|
</DropdownMenuItem>
|
|
645
732
|
</DropdownMenuContent>
|
|
@@ -673,8 +760,8 @@ import {
|
|
|
673
760
|
AlertDialogTitle,
|
|
674
761
|
AlertDialogTrigger,
|
|
675
762
|
} from "@arolariu/components/alert-dialog";
|
|
676
|
-
import { Button } from "@arolariu/components/button";
|
|
677
763
|
import { Trash } from "lucide-react";
|
|
764
|
+
import styles from "./delete-confirmation.module.css";
|
|
678
765
|
|
|
679
766
|
export function DeleteConfirmation({
|
|
680
767
|
itemName,
|
|
@@ -685,26 +772,26 @@ export function DeleteConfirmation({
|
|
|
685
772
|
}) {
|
|
686
773
|
return (
|
|
687
774
|
<AlertDialog>
|
|
688
|
-
<AlertDialogTrigger
|
|
689
|
-
<
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
775
|
+
<AlertDialogTrigger
|
|
776
|
+
render={<button type="button" className={styles.triggerButton} />}
|
|
777
|
+
>
|
|
778
|
+
<Trash />
|
|
779
|
+
Delete
|
|
693
780
|
</AlertDialogTrigger>
|
|
694
781
|
<AlertDialogContent>
|
|
695
782
|
<AlertDialogHeader>
|
|
696
783
|
<AlertDialogTitle>Are you absolutely sure?</AlertDialogTitle>
|
|
697
784
|
<AlertDialogDescription>
|
|
698
|
-
This action cannot be undone. This will permanently delete
|
|
699
|
-
{
|
|
785
|
+
This action cannot be undone. This will permanently delete
|
|
786
|
+
{" "}
|
|
787
|
+
{itemName}
|
|
788
|
+
{" "}
|
|
789
|
+
and remove all associated data from our servers.
|
|
700
790
|
</AlertDialogDescription>
|
|
701
791
|
</AlertDialogHeader>
|
|
702
792
|
<AlertDialogFooter>
|
|
703
793
|
<AlertDialogCancel>Cancel</AlertDialogCancel>
|
|
704
|
-
<AlertDialogAction
|
|
705
|
-
onClick={onConfirm}
|
|
706
|
-
className="bg-destructive text-destructive-foreground hover:bg-destructive/90"
|
|
707
|
-
>
|
|
794
|
+
<AlertDialogAction onClick={onConfirm}>
|
|
708
795
|
Yes, delete it
|
|
709
796
|
</AlertDialogAction>
|
|
710
797
|
</AlertDialogFooter>
|
|
@@ -717,6 +804,7 @@ export function DeleteConfirmation({
|
|
|
717
804
|
### Settings Modal
|
|
718
805
|
|
|
719
806
|
```tsx
|
|
807
|
+
import { Button } from "@arolariu/components/button";
|
|
720
808
|
import {
|
|
721
809
|
Dialog,
|
|
722
810
|
DialogContent,
|
|
@@ -726,10 +814,8 @@ import {
|
|
|
726
814
|
DialogTitle,
|
|
727
815
|
DialogTrigger,
|
|
728
816
|
} from "@arolariu/components/dialog";
|
|
729
|
-
import { Button } from "@arolariu/components/button";
|
|
730
817
|
import { Input } from "@arolariu/components/input";
|
|
731
818
|
import { Label } from "@arolariu/components/label";
|
|
732
|
-
import { Switch } from "@arolariu/components/switch";
|
|
733
819
|
import {
|
|
734
820
|
Select,
|
|
735
821
|
SelectContent,
|
|
@@ -737,24 +823,21 @@ import {
|
|
|
737
823
|
SelectTrigger,
|
|
738
824
|
SelectValue,
|
|
739
825
|
} from "@arolariu/components/select";
|
|
740
|
-
import {
|
|
741
|
-
|
|
742
|
-
TabsContent,
|
|
743
|
-
TabsList,
|
|
744
|
-
TabsTrigger,
|
|
745
|
-
} from "@arolariu/components/tabs";
|
|
826
|
+
import { Switch } from "@arolariu/components/switch";
|
|
827
|
+
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@arolariu/components/tabs";
|
|
746
828
|
import { Settings } from "lucide-react";
|
|
829
|
+
import styles from "./settings-dialog.module.css";
|
|
747
830
|
|
|
748
831
|
export function SettingsDialog() {
|
|
749
832
|
return (
|
|
750
833
|
<Dialog>
|
|
751
|
-
<DialogTrigger
|
|
752
|
-
<
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
834
|
+
<DialogTrigger
|
|
835
|
+
render={<button type="button" className={styles.trigger} />}
|
|
836
|
+
>
|
|
837
|
+
<Settings />
|
|
838
|
+
Settings
|
|
756
839
|
</DialogTrigger>
|
|
757
|
-
<DialogContent className=
|
|
840
|
+
<DialogContent className={styles.dialogContent}>
|
|
758
841
|
<DialogHeader>
|
|
759
842
|
<DialogTitle>Settings</DialogTitle>
|
|
760
843
|
<DialogDescription>
|
|
@@ -762,23 +845,35 @@ export function SettingsDialog() {
|
|
|
762
845
|
</DialogDescription>
|
|
763
846
|
</DialogHeader>
|
|
764
847
|
|
|
765
|
-
<Tabs
|
|
766
|
-
|
|
848
|
+
<Tabs
|
|
849
|
+
defaultValue="general"
|
|
850
|
+
className={styles.tabs}
|
|
851
|
+
>
|
|
852
|
+
<TabsList className={styles.tabsList}>
|
|
767
853
|
<TabsTrigger value="general">General</TabsTrigger>
|
|
768
854
|
<TabsTrigger value="notifications">Notifications</TabsTrigger>
|
|
769
855
|
<TabsTrigger value="security">Security</TabsTrigger>
|
|
770
856
|
</TabsList>
|
|
771
857
|
|
|
772
|
-
<TabsContent
|
|
773
|
-
|
|
858
|
+
<TabsContent
|
|
859
|
+
value="general"
|
|
860
|
+
className={styles.panel}
|
|
861
|
+
>
|
|
862
|
+
<div className={styles.field}>
|
|
774
863
|
<Label htmlFor="name">Display Name</Label>
|
|
775
|
-
<Input
|
|
864
|
+
<Input
|
|
865
|
+
id="name"
|
|
866
|
+
defaultValue="John Doe"
|
|
867
|
+
/>
|
|
776
868
|
</div>
|
|
777
|
-
<div className=
|
|
869
|
+
<div className={styles.field}>
|
|
778
870
|
<Label htmlFor="email">Email</Label>
|
|
779
|
-
<Input
|
|
871
|
+
<Input
|
|
872
|
+
id="email"
|
|
873
|
+
defaultValue="john@example.com"
|
|
874
|
+
/>
|
|
780
875
|
</div>
|
|
781
|
-
<div className=
|
|
876
|
+
<div className={styles.field}>
|
|
782
877
|
<Label htmlFor="timezone">Timezone</Label>
|
|
783
878
|
<Select defaultValue="utc">
|
|
784
879
|
<SelectTrigger>
|
|
@@ -793,20 +888,23 @@ export function SettingsDialog() {
|
|
|
793
888
|
</div>
|
|
794
889
|
</TabsContent>
|
|
795
890
|
|
|
796
|
-
<TabsContent
|
|
797
|
-
|
|
798
|
-
|
|
891
|
+
<TabsContent
|
|
892
|
+
value="notifications"
|
|
893
|
+
className={styles.panel}
|
|
894
|
+
>
|
|
895
|
+
<div className={styles.switchRow}>
|
|
896
|
+
<div className={styles.switchText}>
|
|
799
897
|
<Label>Email Notifications</Label>
|
|
800
|
-
<div className=
|
|
898
|
+
<div className={styles.mutedText}>
|
|
801
899
|
Receive emails about your account activity.
|
|
802
900
|
</div>
|
|
803
901
|
</div>
|
|
804
902
|
<Switch />
|
|
805
903
|
</div>
|
|
806
|
-
<div className=
|
|
807
|
-
<div className=
|
|
904
|
+
<div className={styles.switchRow}>
|
|
905
|
+
<div className={styles.switchText}>
|
|
808
906
|
<Label>Push Notifications</Label>
|
|
809
|
-
<div className=
|
|
907
|
+
<div className={styles.mutedText}>
|
|
810
908
|
Receive push notifications on your devices.
|
|
811
909
|
</div>
|
|
812
910
|
</div>
|
|
@@ -814,18 +912,30 @@ export function SettingsDialog() {
|
|
|
814
912
|
</div>
|
|
815
913
|
</TabsContent>
|
|
816
914
|
|
|
817
|
-
<TabsContent
|
|
818
|
-
|
|
915
|
+
<TabsContent
|
|
916
|
+
value="security"
|
|
917
|
+
className={styles.panel}
|
|
918
|
+
>
|
|
919
|
+
<div className={styles.field}>
|
|
819
920
|
<Label htmlFor="current-password">Current Password</Label>
|
|
820
|
-
<Input
|
|
921
|
+
<Input
|
|
922
|
+
id="current-password"
|
|
923
|
+
type="password"
|
|
924
|
+
/>
|
|
821
925
|
</div>
|
|
822
|
-
<div className=
|
|
926
|
+
<div className={styles.field}>
|
|
823
927
|
<Label htmlFor="new-password">New Password</Label>
|
|
824
|
-
<Input
|
|
928
|
+
<Input
|
|
929
|
+
id="new-password"
|
|
930
|
+
type="password"
|
|
931
|
+
/>
|
|
825
932
|
</div>
|
|
826
|
-
<div className=
|
|
933
|
+
<div className={styles.field}>
|
|
827
934
|
<Label htmlFor="confirm-password">Confirm Password</Label>
|
|
828
|
-
<Input
|
|
935
|
+
<Input
|
|
936
|
+
id="confirm-password"
|
|
937
|
+
type="password"
|
|
938
|
+
/>
|
|
829
939
|
</div>
|
|
830
940
|
</TabsContent>
|
|
831
941
|
</Tabs>
|
|
@@ -842,81 +952,75 @@ export function SettingsDialog() {
|
|
|
842
952
|
|
|
843
953
|
---
|
|
844
954
|
|
|
845
|
-
## 🎨
|
|
955
|
+
## 🎨 Theming Examples
|
|
956
|
+
|
|
957
|
+
### App Theme with `--ac-*` Tokens
|
|
958
|
+
|
|
959
|
+
```css
|
|
960
|
+
:root {
|
|
961
|
+
--ac-primary: oklch(0.6 0.2 250);
|
|
962
|
+
--ac-radius: 0.5rem;
|
|
963
|
+
}
|
|
964
|
+
```
|
|
965
|
+
|
|
966
|
+
```css
|
|
967
|
+
/* app-theme.module.css */
|
|
968
|
+
.themeScope {
|
|
969
|
+
--ac-primary: oklch(0.68 0.2 258);
|
|
970
|
+
--ac-primary-foreground: oklch(0.98 0.01 258);
|
|
971
|
+
--ac-secondary: oklch(0.95 0.02 286);
|
|
972
|
+
--ac-background: oklch(0.99 0 0);
|
|
973
|
+
--ac-radius-md: 0.75rem;
|
|
974
|
+
--ac-radius-lg: 1rem;
|
|
975
|
+
}
|
|
846
976
|
|
|
847
|
-
|
|
977
|
+
.themeScope[data-theme="dark"] {
|
|
978
|
+
--ac-background: oklch(0.17 0.01 286);
|
|
979
|
+
--ac-foreground: oklch(0.98 0 0);
|
|
980
|
+
--ac-card: oklch(0.2 0.01 286);
|
|
981
|
+
}
|
|
982
|
+
```
|
|
848
983
|
|
|
849
984
|
```tsx
|
|
850
|
-
import { DotBackground } from "@arolariu/components/dot-background";
|
|
851
|
-
import { BubbleBackground } from "@arolariu/components/bubble-background";
|
|
852
|
-
import { GradientBackground } from "@arolariu/components/gradient-background";
|
|
853
985
|
import { Button } from "@arolariu/components/button";
|
|
854
|
-
import { Card, CardContent } from "@arolariu/components/card";
|
|
986
|
+
import { Card, CardContent, CardHeader, CardTitle } from "@arolariu/components/card";
|
|
987
|
+
import styles from "./app-theme.module.css";
|
|
855
988
|
|
|
856
|
-
export function
|
|
989
|
+
export function ThemePreview() {
|
|
857
990
|
return (
|
|
858
|
-
<
|
|
859
|
-
{
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
<
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
Create stunning user interfaces with our comprehensive React component
|
|
875
|
-
library. Built with accessibility, performance, and developer
|
|
876
|
-
experience in mind.
|
|
877
|
-
</p>
|
|
991
|
+
<section
|
|
992
|
+
className={styles.themeScope}
|
|
993
|
+
data-theme="dark"
|
|
994
|
+
>
|
|
995
|
+
<Card>
|
|
996
|
+
<CardHeader>
|
|
997
|
+
<CardTitle>Custom theme scope</CardTitle>
|
|
998
|
+
</CardHeader>
|
|
999
|
+
<CardContent>
|
|
1000
|
+
<Button>Token-driven button</Button>
|
|
1001
|
+
</CardContent>
|
|
1002
|
+
</Card>
|
|
1003
|
+
</section>
|
|
1004
|
+
);
|
|
1005
|
+
}
|
|
1006
|
+
```
|
|
878
1007
|
|
|
879
|
-
|
|
880
|
-
<Button size="lg" className="px-8">
|
|
881
|
-
Get Started
|
|
882
|
-
</Button>
|
|
883
|
-
<Button variant="outline" size="lg" className="px-8">
|
|
884
|
-
View Components
|
|
885
|
-
</Button>
|
|
886
|
-
</div>
|
|
1008
|
+
### Styling Base UI State Attributes
|
|
887
1009
|
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
</p>
|
|
896
|
-
</CardContent>
|
|
897
|
-
</Card>
|
|
1010
|
+
```css
|
|
1011
|
+
/* checkbox-demo.module.css */
|
|
1012
|
+
.checkboxRow {
|
|
1013
|
+
display: inline-flex;
|
|
1014
|
+
align-items: center;
|
|
1015
|
+
gap: 0.75rem;
|
|
1016
|
+
}
|
|
898
1017
|
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
<p className="text-sm text-muted-foreground">
|
|
903
|
-
Full type safety and excellent developer experience
|
|
904
|
-
</p>
|
|
905
|
-
</CardContent>
|
|
906
|
-
</Card>
|
|
1018
|
+
.checkboxRow :global([data-checked]) {
|
|
1019
|
+
box-shadow: 0 0 0 2px color-mix(in oklab, var(--ac-primary) 25%, transparent);
|
|
1020
|
+
}
|
|
907
1021
|
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
<h3 className="mb-2 text-lg font-semibold">Accessible</h3>
|
|
911
|
-
<p className="text-sm text-muted-foreground">
|
|
912
|
-
Built on Radix UI with WAI-ARIA compliance
|
|
913
|
-
</p>
|
|
914
|
-
</CardContent>
|
|
915
|
-
</Card>
|
|
916
|
-
</div>
|
|
917
|
-
</div>
|
|
918
|
-
</div>
|
|
919
|
-
);
|
|
1022
|
+
.checkboxRow :global([data-disabled]) {
|
|
1023
|
+
opacity: 0.5;
|
|
920
1024
|
}
|
|
921
1025
|
```
|
|
922
1026
|
|
|
@@ -927,6 +1031,9 @@ export function LandingPage() {
|
|
|
927
1031
|
### Mobile-Optimized Form
|
|
928
1032
|
|
|
929
1033
|
```tsx
|
|
1034
|
+
import { Button } from "@arolariu/components/button";
|
|
1035
|
+
import { Input } from "@arolariu/components/input";
|
|
1036
|
+
import { Label } from "@arolariu/components/label";
|
|
930
1037
|
import {
|
|
931
1038
|
Sheet,
|
|
932
1039
|
SheetContent,
|
|
@@ -935,21 +1042,22 @@ import {
|
|
|
935
1042
|
SheetTitle,
|
|
936
1043
|
SheetTrigger,
|
|
937
1044
|
} from "@arolariu/components/sheet";
|
|
938
|
-
import { Button } from "@arolariu/components/button";
|
|
939
|
-
import { Input } from "@arolariu/components/input";
|
|
940
|
-
import { Label } from "@arolariu/components/label";
|
|
941
1045
|
import { Textarea } from "@arolariu/components/textarea";
|
|
942
1046
|
import { Plus } from "lucide-react";
|
|
1047
|
+
import styles from "./mobile-add-form.module.css";
|
|
943
1048
|
|
|
944
1049
|
export function MobileAddForm() {
|
|
945
1050
|
return (
|
|
946
1051
|
<Sheet>
|
|
947
|
-
<SheetTrigger
|
|
948
|
-
<
|
|
949
|
-
|
|
950
|
-
|
|
1052
|
+
<SheetTrigger
|
|
1053
|
+
render={<button type="button" className={styles.fab} />}
|
|
1054
|
+
>
|
|
1055
|
+
<Plus />
|
|
951
1056
|
</SheetTrigger>
|
|
952
|
-
<SheetContent
|
|
1057
|
+
<SheetContent
|
|
1058
|
+
side="bottom"
|
|
1059
|
+
className={styles.sheetContent}
|
|
1060
|
+
>
|
|
953
1061
|
<SheetHeader>
|
|
954
1062
|
<SheetTitle>Add New Item</SheetTitle>
|
|
955
1063
|
<SheetDescription>
|
|
@@ -957,24 +1065,30 @@ export function MobileAddForm() {
|
|
|
957
1065
|
</SheetDescription>
|
|
958
1066
|
</SheetHeader>
|
|
959
1067
|
|
|
960
|
-
<div className=
|
|
961
|
-
<div className=
|
|
1068
|
+
<div className={styles.formStack}>
|
|
1069
|
+
<div className={styles.field}>
|
|
962
1070
|
<Label htmlFor="title">Title</Label>
|
|
963
|
-
<Input
|
|
1071
|
+
<Input
|
|
1072
|
+
id="title"
|
|
1073
|
+
placeholder="Enter title"
|
|
1074
|
+
/>
|
|
964
1075
|
</div>
|
|
965
1076
|
|
|
966
|
-
<div className=
|
|
1077
|
+
<div className={styles.field}>
|
|
967
1078
|
<Label htmlFor="description">Description</Label>
|
|
968
1079
|
<Textarea
|
|
969
1080
|
id="description"
|
|
970
1081
|
placeholder="Enter description"
|
|
971
|
-
className=
|
|
1082
|
+
className={styles.textarea}
|
|
972
1083
|
/>
|
|
973
1084
|
</div>
|
|
974
1085
|
|
|
975
|
-
<div className=
|
|
976
|
-
<Button className=
|
|
977
|
-
<Button
|
|
1086
|
+
<div className={styles.actionRow}>
|
|
1087
|
+
<Button className={styles.flexButton}>Save</Button>
|
|
1088
|
+
<Button
|
|
1089
|
+
variant="outline"
|
|
1090
|
+
className={styles.flexButton}
|
|
1091
|
+
>
|
|
978
1092
|
Cancel
|
|
979
1093
|
</Button>
|
|
980
1094
|
</div>
|
|
@@ -996,7 +1110,7 @@ export function MobileAddForm() {
|
|
|
996
1110
|
import { Button } from "@arolariu/components/button";
|
|
997
1111
|
import { Card } from "@arolariu/components/card";
|
|
998
1112
|
|
|
999
|
-
// ❌ Avoid barrel imports
|
|
1113
|
+
// ❌ Avoid barrel imports when bundle size matters
|
|
1000
1114
|
import { Button, Card } from "@arolariu/components";
|
|
1001
1115
|
```
|
|
1002
1116
|
|
|
@@ -1004,8 +1118,11 @@ import { Button, Card } from "@arolariu/components";
|
|
|
1004
1118
|
|
|
1005
1119
|
```tsx
|
|
1006
1120
|
// ✅ Always include proper labels and ARIA attributes
|
|
1007
|
-
<Button
|
|
1008
|
-
|
|
1121
|
+
<Button
|
|
1122
|
+
aria-label="Close dialog"
|
|
1123
|
+
onClick={handleClose}
|
|
1124
|
+
>
|
|
1125
|
+
<X />
|
|
1009
1126
|
</Button>
|
|
1010
1127
|
|
|
1011
1128
|
// ✅ Use semantic HTML structure
|
|
@@ -1019,17 +1136,2535 @@ import { Button, Card } from "@arolariu/components";
|
|
|
1019
1136
|
|
|
1020
1137
|
### Responsive Design
|
|
1021
1138
|
|
|
1022
|
-
```
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1139
|
+
```css
|
|
1140
|
+
/* responsive-layout.module.css */
|
|
1141
|
+
.grid {
|
|
1142
|
+
display: grid;
|
|
1143
|
+
gap: 1rem;
|
|
1144
|
+
grid-template-columns: 1fr;
|
|
1145
|
+
}
|
|
1146
|
+
|
|
1147
|
+
@media (min-width: 48rem) {
|
|
1148
|
+
.grid {
|
|
1149
|
+
grid-template-columns: repeat(2, minmax(0, 1fr));
|
|
1150
|
+
}
|
|
1151
|
+
}
|
|
1152
|
+
|
|
1153
|
+
@media (min-width: 64rem) {
|
|
1154
|
+
.grid {
|
|
1155
|
+
grid-template-columns: repeat(3, minmax(0, 1fr));
|
|
1156
|
+
}
|
|
1157
|
+
}
|
|
1031
1158
|
```
|
|
1032
1159
|
|
|
1160
|
+
Ready to build something amazing? **[🚀 Start with our Quick Start Guide](./README.md#-quick-start)**
|
|
1161
|
+
|
|
1033
1162
|
---
|
|
1034
1163
|
|
|
1164
|
+
## 🎓 Pattern Recipes
|
|
1165
|
+
|
|
1166
|
+
> **Real-world patterns ready to copy, paste, and customize.** These recipes demonstrate common UI patterns using @arolariu/components with best practices for forms, data, modals, and error handling.
|
|
1167
|
+
|
|
1168
|
+
### Recipe 1: Login Form with Validation
|
|
1169
|
+
|
|
1170
|
+
**Complete login form with zod validation, error handling, and loading states.**
|
|
1171
|
+
|
|
1172
|
+
```tsx
|
|
1173
|
+
import {zodResolver} from "@hookform/resolvers/zod";
|
|
1174
|
+
import {useForm} from "react-hook-form";
|
|
1175
|
+
import * as z from "zod";
|
|
1176
|
+
|
|
1177
|
+
import {Alert, AlertDescription} from "@arolariu/components/alert";
|
|
1178
|
+
import {Button} from "@arolariu/components/button";
|
|
1179
|
+
import {
|
|
1180
|
+
Card,
|
|
1181
|
+
CardContent,
|
|
1182
|
+
CardDescription,
|
|
1183
|
+
CardFooter,
|
|
1184
|
+
CardHeader,
|
|
1185
|
+
CardTitle,
|
|
1186
|
+
} from "@arolariu/components/card";
|
|
1187
|
+
import {Checkbox} from "@arolariu/components/checkbox";
|
|
1188
|
+
import {
|
|
1189
|
+
Form,
|
|
1190
|
+
FormControl,
|
|
1191
|
+
FormField,
|
|
1192
|
+
FormItem,
|
|
1193
|
+
FormLabel,
|
|
1194
|
+
FormMessage,
|
|
1195
|
+
} from "@arolariu/components/form";
|
|
1196
|
+
import {Input} from "@arolariu/components/input";
|
|
1197
|
+
import {toast} from "@arolariu/components/sonner";
|
|
1198
|
+
import styles from "./login-form.module.css";
|
|
1199
|
+
|
|
1200
|
+
// Define validation schema
|
|
1201
|
+
const loginSchema = z.object({
|
|
1202
|
+
email: z.string().email("Please enter a valid email address"),
|
|
1203
|
+
password: z.string().min(8, "Password must be at least 8 characters"),
|
|
1204
|
+
rememberMe: z.boolean().default(false),
|
|
1205
|
+
});
|
|
1206
|
+
|
|
1207
|
+
type LoginFormValues = z.infer<typeof loginSchema>;
|
|
1208
|
+
|
|
1209
|
+
export function LoginForm() {
|
|
1210
|
+
const form = useForm<LoginFormValues>({
|
|
1211
|
+
resolver: zodResolver(loginSchema),
|
|
1212
|
+
defaultValues: {
|
|
1213
|
+
email: "",
|
|
1214
|
+
password: "",
|
|
1215
|
+
rememberMe: false,
|
|
1216
|
+
},
|
|
1217
|
+
});
|
|
1218
|
+
|
|
1219
|
+
async function onSubmit(values: LoginFormValues) {
|
|
1220
|
+
try {
|
|
1221
|
+
// Simulate API call
|
|
1222
|
+
await new Promise((resolve) => setTimeout(resolve, 1500));
|
|
1223
|
+
|
|
1224
|
+
// Check credentials (mock)
|
|
1225
|
+
if (values.email === "demo@example.com" && values.password === "password123") {
|
|
1226
|
+
toast.success("Login successful! Redirecting...");
|
|
1227
|
+
// Redirect to dashboard
|
|
1228
|
+
window.location.href = "/dashboard";
|
|
1229
|
+
} else {
|
|
1230
|
+
throw new Error("Invalid credentials");
|
|
1231
|
+
}
|
|
1232
|
+
} catch (error) {
|
|
1233
|
+
toast.error("Login failed. Please check your credentials and try again.");
|
|
1234
|
+
}
|
|
1235
|
+
}
|
|
1236
|
+
|
|
1237
|
+
return (
|
|
1238
|
+
<div className={styles.page}>
|
|
1239
|
+
<Card className={styles.card}>
|
|
1240
|
+
<CardHeader className={styles.header}>
|
|
1241
|
+
<CardTitle>Welcome Back</CardTitle>
|
|
1242
|
+
<CardDescription>
|
|
1243
|
+
Sign in to your account to continue
|
|
1244
|
+
</CardDescription>
|
|
1245
|
+
</CardHeader>
|
|
1246
|
+
|
|
1247
|
+
<Form {...form}>
|
|
1248
|
+
<form onSubmit={form.handleSubmit(onSubmit)}>
|
|
1249
|
+
<CardContent className={styles.content}>
|
|
1250
|
+
{form.formState.errors.root ? (
|
|
1251
|
+
<Alert variant="destructive">
|
|
1252
|
+
<AlertDescription>
|
|
1253
|
+
{form.formState.errors.root.message}
|
|
1254
|
+
</AlertDescription>
|
|
1255
|
+
</Alert>
|
|
1256
|
+
) : null}
|
|
1257
|
+
|
|
1258
|
+
<FormField
|
|
1259
|
+
control={form.control}
|
|
1260
|
+
name="email"
|
|
1261
|
+
render={({field}) => (
|
|
1262
|
+
<FormItem>
|
|
1263
|
+
<FormLabel>Email</FormLabel>
|
|
1264
|
+
<FormControl>
|
|
1265
|
+
<Input
|
|
1266
|
+
type="email"
|
|
1267
|
+
placeholder="you@example.com"
|
|
1268
|
+
autoComplete="email"
|
|
1269
|
+
{...field}
|
|
1270
|
+
/>
|
|
1271
|
+
</FormControl>
|
|
1272
|
+
<FormMessage />
|
|
1273
|
+
</FormItem>
|
|
1274
|
+
)}
|
|
1275
|
+
/>
|
|
1276
|
+
|
|
1277
|
+
<FormField
|
|
1278
|
+
control={form.control}
|
|
1279
|
+
name="password"
|
|
1280
|
+
render={({field}) => (
|
|
1281
|
+
<FormItem>
|
|
1282
|
+
<FormLabel>Password</FormLabel>
|
|
1283
|
+
<FormControl>
|
|
1284
|
+
<Input
|
|
1285
|
+
type="password"
|
|
1286
|
+
placeholder="••••••••"
|
|
1287
|
+
autoComplete="current-password"
|
|
1288
|
+
{...field}
|
|
1289
|
+
/>
|
|
1290
|
+
</FormControl>
|
|
1291
|
+
<FormMessage />
|
|
1292
|
+
</FormItem>
|
|
1293
|
+
)}
|
|
1294
|
+
/>
|
|
1295
|
+
|
|
1296
|
+
<FormField
|
|
1297
|
+
control={form.control}
|
|
1298
|
+
name="rememberMe"
|
|
1299
|
+
render={({field}) => (
|
|
1300
|
+
<FormItem className={styles.checkboxItem}>
|
|
1301
|
+
<FormControl>
|
|
1302
|
+
<Checkbox
|
|
1303
|
+
checked={field.value}
|
|
1304
|
+
onCheckedChange={field.onChange}
|
|
1305
|
+
/>
|
|
1306
|
+
</FormControl>
|
|
1307
|
+
<FormLabel className={styles.checkboxLabel}>
|
|
1308
|
+
Remember me for 30 days
|
|
1309
|
+
</FormLabel>
|
|
1310
|
+
</FormItem>
|
|
1311
|
+
)}
|
|
1312
|
+
/>
|
|
1313
|
+
</CardContent>
|
|
1314
|
+
|
|
1315
|
+
<CardFooter className={styles.footer}>
|
|
1316
|
+
<Button
|
|
1317
|
+
type="submit"
|
|
1318
|
+
className={styles.submitButton}
|
|
1319
|
+
disabled={form.formState.isSubmitting}
|
|
1320
|
+
>
|
|
1321
|
+
{form.formState.isSubmitting ? "Signing in..." : "Sign In"}
|
|
1322
|
+
</Button>
|
|
1323
|
+
|
|
1324
|
+
<div className={styles.links}>
|
|
1325
|
+
<a href="/forgot-password" className={styles.link}>
|
|
1326
|
+
Forgot password?
|
|
1327
|
+
</a>
|
|
1328
|
+
<a href="/signup" className={styles.link}>
|
|
1329
|
+
Create account
|
|
1330
|
+
</a>
|
|
1331
|
+
</div>
|
|
1332
|
+
</CardFooter>
|
|
1333
|
+
</form>
|
|
1334
|
+
</Form>
|
|
1335
|
+
</Card>
|
|
1336
|
+
</div>
|
|
1337
|
+
);
|
|
1338
|
+
}
|
|
1339
|
+
```
|
|
1340
|
+
|
|
1341
|
+
```css
|
|
1342
|
+
/* login-form.module.css */
|
|
1343
|
+
.page {
|
|
1344
|
+
display: flex;
|
|
1345
|
+
align-items: center;
|
|
1346
|
+
justify-content: center;
|
|
1347
|
+
min-height: 100vh;
|
|
1348
|
+
padding: 1rem;
|
|
1349
|
+
background-color: var(--ac-muted);
|
|
1350
|
+
}
|
|
1351
|
+
|
|
1352
|
+
.card {
|
|
1353
|
+
width: min(28rem, 100%);
|
|
1354
|
+
}
|
|
1355
|
+
|
|
1356
|
+
.header {
|
|
1357
|
+
text-align: center;
|
|
1358
|
+
}
|
|
1359
|
+
|
|
1360
|
+
.content {
|
|
1361
|
+
display: grid;
|
|
1362
|
+
gap: 1rem;
|
|
1363
|
+
}
|
|
1364
|
+
|
|
1365
|
+
.checkboxItem {
|
|
1366
|
+
display: flex;
|
|
1367
|
+
flex-direction: row;
|
|
1368
|
+
align-items: center;
|
|
1369
|
+
gap: 0.5rem;
|
|
1370
|
+
}
|
|
1371
|
+
|
|
1372
|
+
.checkboxLabel {
|
|
1373
|
+
margin-top: 0;
|
|
1374
|
+
font-weight: 400;
|
|
1375
|
+
}
|
|
1376
|
+
|
|
1377
|
+
.footer {
|
|
1378
|
+
display: flex;
|
|
1379
|
+
flex-direction: column;
|
|
1380
|
+
gap: 1rem;
|
|
1381
|
+
}
|
|
1382
|
+
|
|
1383
|
+
.submitButton {
|
|
1384
|
+
width: 100%;
|
|
1385
|
+
}
|
|
1386
|
+
|
|
1387
|
+
.links {
|
|
1388
|
+
display: flex;
|
|
1389
|
+
justify-content: space-between;
|
|
1390
|
+
font-size: 0.875rem;
|
|
1391
|
+
}
|
|
1392
|
+
|
|
1393
|
+
.link {
|
|
1394
|
+
color: var(--ac-primary);
|
|
1395
|
+
text-decoration: none;
|
|
1396
|
+
}
|
|
1397
|
+
|
|
1398
|
+
.link:hover {
|
|
1399
|
+
text-decoration: underline;
|
|
1400
|
+
}
|
|
1401
|
+
```
|
|
1402
|
+
|
|
1403
|
+
---
|
|
1404
|
+
|
|
1405
|
+
### Recipe 2: Data Table with Sorting (TanStack Table)
|
|
1406
|
+
|
|
1407
|
+
**Sortable, filterable data table with row actions and pagination.**
|
|
1408
|
+
|
|
1409
|
+
```tsx
|
|
1410
|
+
import {
|
|
1411
|
+
createColumnHelper,
|
|
1412
|
+
flexRender,
|
|
1413
|
+
getCoreRowModel,
|
|
1414
|
+
getPaginationRowModel,
|
|
1415
|
+
getSortedRowModel,
|
|
1416
|
+
useReactTable,
|
|
1417
|
+
type SortingState,
|
|
1418
|
+
} from "@tanstack/react-table";
|
|
1419
|
+
import {ArrowUpDown, ChevronLeft, ChevronRight, MoreHorizontal} from "lucide-react";
|
|
1420
|
+
import {useState} from "react";
|
|
1421
|
+
|
|
1422
|
+
import {Badge} from "@arolariu/components/badge";
|
|
1423
|
+
import {Button} from "@arolariu/components/button";
|
|
1424
|
+
import {
|
|
1425
|
+
DropdownMenu,
|
|
1426
|
+
DropdownMenuContent,
|
|
1427
|
+
DropdownMenuItem,
|
|
1428
|
+
DropdownMenuLabel,
|
|
1429
|
+
DropdownMenuSeparator,
|
|
1430
|
+
DropdownMenuTrigger,
|
|
1431
|
+
} from "@arolariu/components/dropdown-menu";
|
|
1432
|
+
import {Input} from "@arolariu/components/input";
|
|
1433
|
+
import {
|
|
1434
|
+
Table,
|
|
1435
|
+
TableBody,
|
|
1436
|
+
TableCell,
|
|
1437
|
+
TableHead,
|
|
1438
|
+
TableHeader,
|
|
1439
|
+
TableRow,
|
|
1440
|
+
} from "@arolariu/components/table";
|
|
1441
|
+
import {toast} from "@arolariu/components/sonner";
|
|
1442
|
+
import styles from "./data-table.module.css";
|
|
1443
|
+
|
|
1444
|
+
interface User {
|
|
1445
|
+
id: string;
|
|
1446
|
+
name: string;
|
|
1447
|
+
email: string;
|
|
1448
|
+
role: "admin" | "user" | "guest";
|
|
1449
|
+
status: "active" | "inactive";
|
|
1450
|
+
createdAt: Date;
|
|
1451
|
+
}
|
|
1452
|
+
|
|
1453
|
+
const data: User[] = [
|
|
1454
|
+
{
|
|
1455
|
+
id: "1",
|
|
1456
|
+
name: "John Doe",
|
|
1457
|
+
email: "john@example.com",
|
|
1458
|
+
role: "admin",
|
|
1459
|
+
status: "active",
|
|
1460
|
+
createdAt: new Date("2024-01-15"),
|
|
1461
|
+
},
|
|
1462
|
+
{
|
|
1463
|
+
id: "2",
|
|
1464
|
+
name: "Jane Smith",
|
|
1465
|
+
email: "jane@example.com",
|
|
1466
|
+
role: "user",
|
|
1467
|
+
status: "active",
|
|
1468
|
+
createdAt: new Date("2024-02-20"),
|
|
1469
|
+
},
|
|
1470
|
+
{
|
|
1471
|
+
id: "3",
|
|
1472
|
+
name: "Bob Johnson",
|
|
1473
|
+
email: "bob@example.com",
|
|
1474
|
+
role: "guest",
|
|
1475
|
+
status: "inactive",
|
|
1476
|
+
createdAt: new Date("2024-03-10"),
|
|
1477
|
+
},
|
|
1478
|
+
];
|
|
1479
|
+
|
|
1480
|
+
const columnHelper = createColumnHelper<User>();
|
|
1481
|
+
|
|
1482
|
+
export function DataTableWithSorting() {
|
|
1483
|
+
const [sorting, setSorting] = useState<SortingState>([]);
|
|
1484
|
+
const [globalFilter, setGlobalFilter] = useState("");
|
|
1485
|
+
|
|
1486
|
+
const columns = [
|
|
1487
|
+
columnHelper.accessor("name", {
|
|
1488
|
+
header: ({column}) => (
|
|
1489
|
+
<button
|
|
1490
|
+
type="button"
|
|
1491
|
+
className={styles.sortButton}
|
|
1492
|
+
onClick={() => column.toggleSorting(column.getIsSorted() === "asc")}
|
|
1493
|
+
>
|
|
1494
|
+
Name
|
|
1495
|
+
<ArrowUpDown className={styles.sortIcon} />
|
|
1496
|
+
</button>
|
|
1497
|
+
),
|
|
1498
|
+
cell: (info) => <span className={styles.emphasisText}>{info.getValue()}</span>,
|
|
1499
|
+
}),
|
|
1500
|
+
columnHelper.accessor("email", {
|
|
1501
|
+
header: "Email",
|
|
1502
|
+
}),
|
|
1503
|
+
columnHelper.accessor("role", {
|
|
1504
|
+
header: "Role",
|
|
1505
|
+
cell: (info) => {
|
|
1506
|
+
const role = info.getValue();
|
|
1507
|
+
return (
|
|
1508
|
+
<Badge variant={role === "admin" ? "default" : "secondary"}>
|
|
1509
|
+
{role}
|
|
1510
|
+
</Badge>
|
|
1511
|
+
);
|
|
1512
|
+
},
|
|
1513
|
+
}),
|
|
1514
|
+
columnHelper.accessor("status", {
|
|
1515
|
+
header: "Status",
|
|
1516
|
+
cell: (info) => {
|
|
1517
|
+
const status = info.getValue();
|
|
1518
|
+
return (
|
|
1519
|
+
<Badge variant={status === "active" ? "default" : "outline"}>
|
|
1520
|
+
{status}
|
|
1521
|
+
</Badge>
|
|
1522
|
+
);
|
|
1523
|
+
},
|
|
1524
|
+
}),
|
|
1525
|
+
columnHelper.accessor("createdAt", {
|
|
1526
|
+
header: ({column}) => (
|
|
1527
|
+
<button
|
|
1528
|
+
type="button"
|
|
1529
|
+
className={styles.sortButton}
|
|
1530
|
+
onClick={() => column.toggleSorting(column.getIsSorted() === "asc")}
|
|
1531
|
+
>
|
|
1532
|
+
Created At
|
|
1533
|
+
<ArrowUpDown className={styles.sortIcon} />
|
|
1534
|
+
</button>
|
|
1535
|
+
),
|
|
1536
|
+
cell: (info) => info.getValue().toLocaleDateString(),
|
|
1537
|
+
}),
|
|
1538
|
+
columnHelper.display({
|
|
1539
|
+
id: "actions",
|
|
1540
|
+
cell: ({row}) => (
|
|
1541
|
+
<DropdownMenu>
|
|
1542
|
+
<DropdownMenuTrigger render={<button type="button" className={styles.iconButton} />}>
|
|
1543
|
+
<MoreHorizontal />
|
|
1544
|
+
</DropdownMenuTrigger>
|
|
1545
|
+
<DropdownMenuContent align="end">
|
|
1546
|
+
<DropdownMenuLabel>Actions</DropdownMenuLabel>
|
|
1547
|
+
<DropdownMenuItem
|
|
1548
|
+
onClick={() => {
|
|
1549
|
+
navigator.clipboard.writeText(row.original.id);
|
|
1550
|
+
toast.success("User ID copied to clipboard");
|
|
1551
|
+
}}
|
|
1552
|
+
>
|
|
1553
|
+
Copy ID
|
|
1554
|
+
</DropdownMenuItem>
|
|
1555
|
+
<DropdownMenuSeparator />
|
|
1556
|
+
<DropdownMenuItem onClick={() => toast.info(`Viewing user: ${row.original.name}`)}>
|
|
1557
|
+
View details
|
|
1558
|
+
</DropdownMenuItem>
|
|
1559
|
+
<DropdownMenuItem onClick={() => toast.info(`Editing user: ${row.original.name}`)}>
|
|
1560
|
+
Edit user
|
|
1561
|
+
</DropdownMenuItem>
|
|
1562
|
+
</DropdownMenuContent>
|
|
1563
|
+
</DropdownMenu>
|
|
1564
|
+
),
|
|
1565
|
+
}),
|
|
1566
|
+
];
|
|
1567
|
+
|
|
1568
|
+
const table = useReactTable({
|
|
1569
|
+
data,
|
|
1570
|
+
columns,
|
|
1571
|
+
state: {
|
|
1572
|
+
sorting,
|
|
1573
|
+
globalFilter,
|
|
1574
|
+
},
|
|
1575
|
+
onSortingChange: setSorting,
|
|
1576
|
+
onGlobalFilterChange: setGlobalFilter,
|
|
1577
|
+
getCoreRowModel: getCoreRowModel(),
|
|
1578
|
+
getSortedRowModel: getSortedRowModel(),
|
|
1579
|
+
getPaginationRowModel: getPaginationRowModel(),
|
|
1580
|
+
initialState: {
|
|
1581
|
+
pagination: {
|
|
1582
|
+
pageSize: 5,
|
|
1583
|
+
},
|
|
1584
|
+
},
|
|
1585
|
+
});
|
|
1586
|
+
|
|
1587
|
+
return (
|
|
1588
|
+
<div className={styles.container}>
|
|
1589
|
+
<div className={styles.toolbar}>
|
|
1590
|
+
<Input
|
|
1591
|
+
placeholder="Search users..."
|
|
1592
|
+
value={globalFilter}
|
|
1593
|
+
onChange={(e) => setGlobalFilter(e.target.value)}
|
|
1594
|
+
className={styles.searchInput}
|
|
1595
|
+
/>
|
|
1596
|
+
</div>
|
|
1597
|
+
|
|
1598
|
+
<div className={styles.tableWrapper}>
|
|
1599
|
+
<Table>
|
|
1600
|
+
<TableHeader>
|
|
1601
|
+
{table.getHeaderGroups().map((headerGroup) => (
|
|
1602
|
+
<TableRow key={headerGroup.id}>
|
|
1603
|
+
{headerGroup.headers.map((header) => (
|
|
1604
|
+
<TableHead key={header.id}>
|
|
1605
|
+
{header.isPlaceholder
|
|
1606
|
+
? null
|
|
1607
|
+
: flexRender(header.column.columnDef.header, header.getContext())}
|
|
1608
|
+
</TableHead>
|
|
1609
|
+
))}
|
|
1610
|
+
</TableRow>
|
|
1611
|
+
))}
|
|
1612
|
+
</TableHeader>
|
|
1613
|
+
<TableBody>
|
|
1614
|
+
{table.getRowModel().rows.length > 0 ? (
|
|
1615
|
+
table.getRowModel().rows.map((row) => (
|
|
1616
|
+
<TableRow key={row.id}>
|
|
1617
|
+
{row.getVisibleCells().map((cell) => (
|
|
1618
|
+
<TableCell key={cell.id}>
|
|
1619
|
+
{flexRender(cell.column.columnDef.cell, cell.getContext())}
|
|
1620
|
+
</TableCell>
|
|
1621
|
+
))}
|
|
1622
|
+
</TableRow>
|
|
1623
|
+
))
|
|
1624
|
+
) : (
|
|
1625
|
+
<TableRow>
|
|
1626
|
+
<TableCell colSpan={columns.length} className={styles.emptyCell}>
|
|
1627
|
+
No results found.
|
|
1628
|
+
</TableCell>
|
|
1629
|
+
</TableRow>
|
|
1630
|
+
)}
|
|
1631
|
+
</TableBody>
|
|
1632
|
+
</Table>
|
|
1633
|
+
</div>
|
|
1634
|
+
|
|
1635
|
+
<div className={styles.pagination}>
|
|
1636
|
+
<div className={styles.paginationInfo}>
|
|
1637
|
+
Page {table.getState().pagination.pageIndex + 1} of {table.getPageCount()}
|
|
1638
|
+
</div>
|
|
1639
|
+
<div className={styles.paginationButtons}>
|
|
1640
|
+
<Button
|
|
1641
|
+
variant="outline"
|
|
1642
|
+
size="sm"
|
|
1643
|
+
onClick={() => table.previousPage()}
|
|
1644
|
+
disabled={!table.getCanPreviousPage()}
|
|
1645
|
+
>
|
|
1646
|
+
<ChevronLeft />
|
|
1647
|
+
Previous
|
|
1648
|
+
</Button>
|
|
1649
|
+
<Button
|
|
1650
|
+
variant="outline"
|
|
1651
|
+
size="sm"
|
|
1652
|
+
onClick={() => table.nextPage()}
|
|
1653
|
+
disabled={!table.getCanNextPage()}
|
|
1654
|
+
>
|
|
1655
|
+
Next
|
|
1656
|
+
<ChevronRight />
|
|
1657
|
+
</Button>
|
|
1658
|
+
</div>
|
|
1659
|
+
</div>
|
|
1660
|
+
</div>
|
|
1661
|
+
);
|
|
1662
|
+
}
|
|
1663
|
+
```
|
|
1664
|
+
|
|
1665
|
+
```css
|
|
1666
|
+
/* data-table.module.css */
|
|
1667
|
+
.container {
|
|
1668
|
+
display: grid;
|
|
1669
|
+
gap: 1rem;
|
|
1670
|
+
}
|
|
1671
|
+
|
|
1672
|
+
.toolbar {
|
|
1673
|
+
display: flex;
|
|
1674
|
+
gap: 0.5rem;
|
|
1675
|
+
}
|
|
1676
|
+
|
|
1677
|
+
.searchInput {
|
|
1678
|
+
max-width: 20rem;
|
|
1679
|
+
}
|
|
1680
|
+
|
|
1681
|
+
.tableWrapper {
|
|
1682
|
+
border: 1px solid var(--ac-border);
|
|
1683
|
+
border-radius: var(--ac-radius-md);
|
|
1684
|
+
overflow: hidden;
|
|
1685
|
+
}
|
|
1686
|
+
|
|
1687
|
+
.sortButton {
|
|
1688
|
+
display: inline-flex;
|
|
1689
|
+
align-items: center;
|
|
1690
|
+
gap: 0.5rem;
|
|
1691
|
+
font-weight: 500;
|
|
1692
|
+
background: none;
|
|
1693
|
+
border: none;
|
|
1694
|
+
cursor: pointer;
|
|
1695
|
+
}
|
|
1696
|
+
|
|
1697
|
+
.sortIcon {
|
|
1698
|
+
width: 1rem;
|
|
1699
|
+
height: 1rem;
|
|
1700
|
+
opacity: 0.5;
|
|
1701
|
+
}
|
|
1702
|
+
|
|
1703
|
+
.emphasisText {
|
|
1704
|
+
font-weight: 500;
|
|
1705
|
+
}
|
|
1706
|
+
|
|
1707
|
+
.iconButton {
|
|
1708
|
+
display: inline-flex;
|
|
1709
|
+
align-items: center;
|
|
1710
|
+
justify-content: center;
|
|
1711
|
+
width: 2rem;
|
|
1712
|
+
height: 2rem;
|
|
1713
|
+
border: none;
|
|
1714
|
+
background: none;
|
|
1715
|
+
border-radius: var(--ac-radius-sm);
|
|
1716
|
+
cursor: pointer;
|
|
1717
|
+
}
|
|
1718
|
+
|
|
1719
|
+
.iconButton:hover {
|
|
1720
|
+
background-color: var(--ac-accent);
|
|
1721
|
+
}
|
|
1722
|
+
|
|
1723
|
+
.emptyCell {
|
|
1724
|
+
text-align: center;
|
|
1725
|
+
padding: 2rem;
|
|
1726
|
+
color: var(--ac-muted-foreground);
|
|
1727
|
+
}
|
|
1728
|
+
|
|
1729
|
+
.pagination {
|
|
1730
|
+
display: flex;
|
|
1731
|
+
align-items: center;
|
|
1732
|
+
justify-content: space-between;
|
|
1733
|
+
}
|
|
1734
|
+
|
|
1735
|
+
.paginationInfo {
|
|
1736
|
+
font-size: 0.875rem;
|
|
1737
|
+
color: var(--ac-muted-foreground);
|
|
1738
|
+
}
|
|
1739
|
+
|
|
1740
|
+
.paginationButtons {
|
|
1741
|
+
display: flex;
|
|
1742
|
+
gap: 0.5rem;
|
|
1743
|
+
}
|
|
1744
|
+
```
|
|
1745
|
+
|
|
1746
|
+
---
|
|
1747
|
+
|
|
1748
|
+
### Recipe 3: Modal Form (Dialog + Form + Validation)
|
|
1749
|
+
|
|
1750
|
+
**Dialog with form validation and async submission.**
|
|
1751
|
+
|
|
1752
|
+
```tsx
|
|
1753
|
+
import {zodResolver} from "@hookform/resolvers/zod";
|
|
1754
|
+
import {Plus} from "lucide-react";
|
|
1755
|
+
import {useState} from "react";
|
|
1756
|
+
import {useForm} from "react-hook-form";
|
|
1757
|
+
import * as z from "zod";
|
|
1758
|
+
|
|
1759
|
+
import {Button} from "@arolariu/components/button";
|
|
1760
|
+
import {
|
|
1761
|
+
Dialog,
|
|
1762
|
+
DialogContent,
|
|
1763
|
+
DialogDescription,
|
|
1764
|
+
DialogFooter,
|
|
1765
|
+
DialogHeader,
|
|
1766
|
+
DialogTitle,
|
|
1767
|
+
DialogTrigger,
|
|
1768
|
+
} from "@arolariu/components/dialog";
|
|
1769
|
+
import {
|
|
1770
|
+
Form,
|
|
1771
|
+
FormControl,
|
|
1772
|
+
FormDescription,
|
|
1773
|
+
FormField,
|
|
1774
|
+
FormItem,
|
|
1775
|
+
FormLabel,
|
|
1776
|
+
FormMessage,
|
|
1777
|
+
} from "@arolariu/components/form";
|
|
1778
|
+
import {Input} from "@arolariu/components/input";
|
|
1779
|
+
import {
|
|
1780
|
+
Select,
|
|
1781
|
+
SelectContent,
|
|
1782
|
+
SelectItem,
|
|
1783
|
+
SelectTrigger,
|
|
1784
|
+
SelectValue,
|
|
1785
|
+
} from "@arolariu/components/select";
|
|
1786
|
+
import {Textarea} from "@arolariu/components/textarea";
|
|
1787
|
+
import {toast} from "@arolariu/components/sonner";
|
|
1788
|
+
import styles from "./modal-form.module.css";
|
|
1789
|
+
|
|
1790
|
+
const projectSchema = z.object({
|
|
1791
|
+
name: z.string().min(3, "Project name must be at least 3 characters"),
|
|
1792
|
+
description: z.string().max(500, "Description must be less than 500 characters").optional(),
|
|
1793
|
+
category: z.enum(["web", "mobile", "desktop", "other"], {
|
|
1794
|
+
required_error: "Please select a category",
|
|
1795
|
+
}),
|
|
1796
|
+
budget: z.string().regex(/^\d+$/, "Budget must be a valid number"),
|
|
1797
|
+
});
|
|
1798
|
+
|
|
1799
|
+
type ProjectFormValues = z.infer<typeof projectSchema>;
|
|
1800
|
+
|
|
1801
|
+
export function CreateProjectModal() {
|
|
1802
|
+
const [open, setOpen] = useState(false);
|
|
1803
|
+
|
|
1804
|
+
const form = useForm<ProjectFormValues>({
|
|
1805
|
+
resolver: zodResolver(projectSchema),
|
|
1806
|
+
defaultValues: {
|
|
1807
|
+
name: "",
|
|
1808
|
+
description: "",
|
|
1809
|
+
category: undefined,
|
|
1810
|
+
budget: "",
|
|
1811
|
+
},
|
|
1812
|
+
});
|
|
1813
|
+
|
|
1814
|
+
async function onSubmit(values: ProjectFormValues) {
|
|
1815
|
+
try {
|
|
1816
|
+
// Simulate API call
|
|
1817
|
+
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
1818
|
+
|
|
1819
|
+
console.log("Project created:", values);
|
|
1820
|
+
toast.success("Project created successfully!");
|
|
1821
|
+
|
|
1822
|
+
// Close modal and reset form
|
|
1823
|
+
setOpen(false);
|
|
1824
|
+
form.reset();
|
|
1825
|
+
} catch (error) {
|
|
1826
|
+
toast.error("Failed to create project. Please try again.");
|
|
1827
|
+
}
|
|
1828
|
+
}
|
|
1829
|
+
|
|
1830
|
+
return (
|
|
1831
|
+
<Dialog open={open} onOpenChange={setOpen}>
|
|
1832
|
+
<DialogTrigger render={<Button />}>
|
|
1833
|
+
<Plus />
|
|
1834
|
+
Create Project
|
|
1835
|
+
</DialogTrigger>
|
|
1836
|
+
|
|
1837
|
+
<DialogContent className={styles.content}>
|
|
1838
|
+
<DialogHeader>
|
|
1839
|
+
<DialogTitle>Create New Project</DialogTitle>
|
|
1840
|
+
<DialogDescription>
|
|
1841
|
+
Fill in the details below to create a new project. Click save when you're done.
|
|
1842
|
+
</DialogDescription>
|
|
1843
|
+
</DialogHeader>
|
|
1844
|
+
|
|
1845
|
+
<Form {...form}>
|
|
1846
|
+
<form onSubmit={form.handleSubmit(onSubmit)} className={styles.form}>
|
|
1847
|
+
<FormField
|
|
1848
|
+
control={form.control}
|
|
1849
|
+
name="name"
|
|
1850
|
+
render={({field}) => (
|
|
1851
|
+
<FormItem>
|
|
1852
|
+
<FormLabel>Project Name</FormLabel>
|
|
1853
|
+
<FormControl>
|
|
1854
|
+
<Input placeholder="My Awesome Project" {...field} />
|
|
1855
|
+
</FormControl>
|
|
1856
|
+
<FormDescription>
|
|
1857
|
+
Choose a unique name for your project.
|
|
1858
|
+
</FormDescription>
|
|
1859
|
+
<FormMessage />
|
|
1860
|
+
</FormItem>
|
|
1861
|
+
)}
|
|
1862
|
+
/>
|
|
1863
|
+
|
|
1864
|
+
<FormField
|
|
1865
|
+
control={form.control}
|
|
1866
|
+
name="category"
|
|
1867
|
+
render={({field}) => (
|
|
1868
|
+
<FormItem>
|
|
1869
|
+
<FormLabel>Category</FormLabel>
|
|
1870
|
+
<Select onValueChange={field.onChange} defaultValue={field.value}>
|
|
1871
|
+
<FormControl>
|
|
1872
|
+
<SelectTrigger>
|
|
1873
|
+
<SelectValue placeholder="Select a category" />
|
|
1874
|
+
</SelectTrigger>
|
|
1875
|
+
</FormControl>
|
|
1876
|
+
<SelectContent>
|
|
1877
|
+
<SelectItem value="web">Web Application</SelectItem>
|
|
1878
|
+
<SelectItem value="mobile">Mobile App</SelectItem>
|
|
1879
|
+
<SelectItem value="desktop">Desktop Application</SelectItem>
|
|
1880
|
+
<SelectItem value="other">Other</SelectItem>
|
|
1881
|
+
</SelectContent>
|
|
1882
|
+
</Select>
|
|
1883
|
+
<FormMessage />
|
|
1884
|
+
</FormItem>
|
|
1885
|
+
)}
|
|
1886
|
+
/>
|
|
1887
|
+
|
|
1888
|
+
<FormField
|
|
1889
|
+
control={form.control}
|
|
1890
|
+
name="budget"
|
|
1891
|
+
render={({field}) => (
|
|
1892
|
+
<FormItem>
|
|
1893
|
+
<FormLabel>Budget (USD)</FormLabel>
|
|
1894
|
+
<FormControl>
|
|
1895
|
+
<Input type="text" placeholder="10000" {...field} />
|
|
1896
|
+
</FormControl>
|
|
1897
|
+
<FormMessage />
|
|
1898
|
+
</FormItem>
|
|
1899
|
+
)}
|
|
1900
|
+
/>
|
|
1901
|
+
|
|
1902
|
+
<FormField
|
|
1903
|
+
control={form.control}
|
|
1904
|
+
name="description"
|
|
1905
|
+
render={({field}) => (
|
|
1906
|
+
<FormItem>
|
|
1907
|
+
<FormLabel>Description</FormLabel>
|
|
1908
|
+
<FormControl>
|
|
1909
|
+
<Textarea
|
|
1910
|
+
placeholder="Describe your project..."
|
|
1911
|
+
className={styles.textarea}
|
|
1912
|
+
{...field}
|
|
1913
|
+
/>
|
|
1914
|
+
</FormControl>
|
|
1915
|
+
<FormMessage />
|
|
1916
|
+
</FormItem>
|
|
1917
|
+
)}
|
|
1918
|
+
/>
|
|
1919
|
+
|
|
1920
|
+
<DialogFooter>
|
|
1921
|
+
<Button
|
|
1922
|
+
type="button"
|
|
1923
|
+
variant="outline"
|
|
1924
|
+
onClick={() => setOpen(false)}
|
|
1925
|
+
>
|
|
1926
|
+
Cancel
|
|
1927
|
+
</Button>
|
|
1928
|
+
<Button type="submit" disabled={form.formState.isSubmitting}>
|
|
1929
|
+
{form.formState.isSubmitting ? "Creating..." : "Create Project"}
|
|
1930
|
+
</Button>
|
|
1931
|
+
</DialogFooter>
|
|
1932
|
+
</form>
|
|
1933
|
+
</Form>
|
|
1934
|
+
</DialogContent>
|
|
1935
|
+
</Dialog>
|
|
1936
|
+
);
|
|
1937
|
+
}
|
|
1938
|
+
```
|
|
1939
|
+
|
|
1940
|
+
```css
|
|
1941
|
+
/* modal-form.module.css */
|
|
1942
|
+
.content {
|
|
1943
|
+
max-width: 32rem;
|
|
1944
|
+
}
|
|
1945
|
+
|
|
1946
|
+
.form {
|
|
1947
|
+
display: grid;
|
|
1948
|
+
gap: 1rem;
|
|
1949
|
+
padding-block: 1rem;
|
|
1950
|
+
}
|
|
1951
|
+
|
|
1952
|
+
.textarea {
|
|
1953
|
+
min-height: 6rem;
|
|
1954
|
+
resize: vertical;
|
|
1955
|
+
}
|
|
1956
|
+
```
|
|
1957
|
+
|
|
1958
|
+
---
|
|
1959
|
+
|
|
1960
|
+
### Recipe 4: Toast Notifications (Sonner)
|
|
1961
|
+
|
|
1962
|
+
**Comprehensive toast notification patterns for all use cases.**
|
|
1963
|
+
|
|
1964
|
+
```tsx
|
|
1965
|
+
import {CheckCircle2, Info, Loader2, XCircle} from "lucide-react";
|
|
1966
|
+
|
|
1967
|
+
import {Button} from "@arolariu/components/button";
|
|
1968
|
+
import {Card, CardContent, CardHeader, CardTitle} from "@arolariu/components/card";
|
|
1969
|
+
import {toast, Toaster} from "@arolariu/components/sonner";
|
|
1970
|
+
import styles from "./toast-demo.module.css";
|
|
1971
|
+
|
|
1972
|
+
export function ToastDemo() {
|
|
1973
|
+
// Basic toasts
|
|
1974
|
+
const showSuccess = () => {
|
|
1975
|
+
toast.success("Operation completed successfully!");
|
|
1976
|
+
};
|
|
1977
|
+
|
|
1978
|
+
const showError = () => {
|
|
1979
|
+
toast.error("Something went wrong. Please try again.");
|
|
1980
|
+
};
|
|
1981
|
+
|
|
1982
|
+
const showInfo = () => {
|
|
1983
|
+
toast.info("This is an informational message.");
|
|
1984
|
+
};
|
|
1985
|
+
|
|
1986
|
+
const showWarning = () => {
|
|
1987
|
+
toast.warning("Warning: This action cannot be undone!");
|
|
1988
|
+
};
|
|
1989
|
+
|
|
1990
|
+
// Toast with action
|
|
1991
|
+
const showWithAction = () => {
|
|
1992
|
+
toast.success("File uploaded successfully", {
|
|
1993
|
+
action: {
|
|
1994
|
+
label: "View",
|
|
1995
|
+
onClick: () => console.log("View clicked"),
|
|
1996
|
+
},
|
|
1997
|
+
});
|
|
1998
|
+
};
|
|
1999
|
+
|
|
2000
|
+
// Toast with description
|
|
2001
|
+
const showWithDescription = () => {
|
|
2002
|
+
toast.success("Project created", {
|
|
2003
|
+
description: "Your project has been created and is now live.",
|
|
2004
|
+
});
|
|
2005
|
+
};
|
|
2006
|
+
|
|
2007
|
+
// Promise toast (loading → success/error)
|
|
2008
|
+
const showPromiseToast = () => {
|
|
2009
|
+
const uploadPromise = new Promise((resolve, reject) => {
|
|
2010
|
+
setTimeout(() => {
|
|
2011
|
+
Math.random() > 0.5 ? resolve({name: "document.pdf"}) : reject(new Error("Upload failed"));
|
|
2012
|
+
}, 2000);
|
|
2013
|
+
});
|
|
2014
|
+
|
|
2015
|
+
toast.promise(uploadPromise, {
|
|
2016
|
+
loading: "Uploading file...",
|
|
2017
|
+
success: (data: {name: string}) => `${data.name} uploaded successfully!`,
|
|
2018
|
+
error: "Failed to upload file.",
|
|
2019
|
+
});
|
|
2020
|
+
};
|
|
2021
|
+
|
|
2022
|
+
// Custom styled toast
|
|
2023
|
+
const showCustomToast = () => {
|
|
2024
|
+
toast.custom(
|
|
2025
|
+
<div className={styles.customToast}>
|
|
2026
|
+
<CheckCircle2 className={styles.customIcon} />
|
|
2027
|
+
<div className={styles.customContent}>
|
|
2028
|
+
<div className={styles.customTitle}>Custom Toast</div>
|
|
2029
|
+
<div className={styles.customDescription}>
|
|
2030
|
+
This is a fully customized toast notification.
|
|
2031
|
+
</div>
|
|
2032
|
+
</div>
|
|
2033
|
+
</div>
|
|
2034
|
+
);
|
|
2035
|
+
};
|
|
2036
|
+
|
|
2037
|
+
// Loading toast (manual control)
|
|
2038
|
+
const showLoadingToast = () => {
|
|
2039
|
+
const toastId = toast.loading("Processing your request...");
|
|
2040
|
+
|
|
2041
|
+
setTimeout(() => {
|
|
2042
|
+
toast.success("Request processed!", {id: toastId});
|
|
2043
|
+
}, 3000);
|
|
2044
|
+
};
|
|
2045
|
+
|
|
2046
|
+
return (
|
|
2047
|
+
<>
|
|
2048
|
+
<Toaster position="top-right" richColors />
|
|
2049
|
+
|
|
2050
|
+
<div className={styles.container}>
|
|
2051
|
+
<Card>
|
|
2052
|
+
<CardHeader>
|
|
2053
|
+
<CardTitle>Toast Notification Examples</CardTitle>
|
|
2054
|
+
</CardHeader>
|
|
2055
|
+
<CardContent className={styles.grid}>
|
|
2056
|
+
<div className={styles.section}>
|
|
2057
|
+
<h3 className={styles.sectionTitle}>Basic Toasts</h3>
|
|
2058
|
+
<div className={styles.buttonGroup}>
|
|
2059
|
+
<Button onClick={showSuccess} variant="default">
|
|
2060
|
+
<CheckCircle2 />
|
|
2061
|
+
Success Toast
|
|
2062
|
+
</Button>
|
|
2063
|
+
<Button onClick={showError} variant="destructive">
|
|
2064
|
+
<XCircle />
|
|
2065
|
+
Error Toast
|
|
2066
|
+
</Button>
|
|
2067
|
+
<Button onClick={showInfo} variant="outline">
|
|
2068
|
+
<Info />
|
|
2069
|
+
Info Toast
|
|
2070
|
+
</Button>
|
|
2071
|
+
<Button onClick={showWarning} variant="outline">
|
|
2072
|
+
Warning Toast
|
|
2073
|
+
</Button>
|
|
2074
|
+
</div>
|
|
2075
|
+
</div>
|
|
2076
|
+
|
|
2077
|
+
<div className={styles.section}>
|
|
2078
|
+
<h3 className={styles.sectionTitle}>Advanced Toasts</h3>
|
|
2079
|
+
<div className={styles.buttonGroup}>
|
|
2080
|
+
<Button onClick={showWithAction} variant="secondary">
|
|
2081
|
+
Toast with Action
|
|
2082
|
+
</Button>
|
|
2083
|
+
<Button onClick={showWithDescription} variant="secondary">
|
|
2084
|
+
Toast with Description
|
|
2085
|
+
</Button>
|
|
2086
|
+
<Button onClick={showPromiseToast} variant="secondary">
|
|
2087
|
+
<Loader2 />
|
|
2088
|
+
Promise Toast
|
|
2089
|
+
</Button>
|
|
2090
|
+
<Button onClick={showLoadingToast} variant="secondary">
|
|
2091
|
+
Loading Toast
|
|
2092
|
+
</Button>
|
|
2093
|
+
</div>
|
|
2094
|
+
</div>
|
|
2095
|
+
|
|
2096
|
+
<div className={styles.section}>
|
|
2097
|
+
<h3 className={styles.sectionTitle}>Custom Toast</h3>
|
|
2098
|
+
<Button onClick={showCustomToast} variant="outline">
|
|
2099
|
+
Show Custom Toast
|
|
2100
|
+
</Button>
|
|
2101
|
+
</div>
|
|
2102
|
+
</CardContent>
|
|
2103
|
+
</Card>
|
|
2104
|
+
</div>
|
|
2105
|
+
</>
|
|
2106
|
+
);
|
|
2107
|
+
}
|
|
2108
|
+
```
|
|
2109
|
+
|
|
2110
|
+
```css
|
|
2111
|
+
/* toast-demo.module.css */
|
|
2112
|
+
.container {
|
|
2113
|
+
display: flex;
|
|
2114
|
+
align-items: center;
|
|
2115
|
+
justify-content: center;
|
|
2116
|
+
min-height: 100vh;
|
|
2117
|
+
padding: 1rem;
|
|
2118
|
+
}
|
|
2119
|
+
|
|
2120
|
+
.grid {
|
|
2121
|
+
display: grid;
|
|
2122
|
+
gap: 2rem;
|
|
2123
|
+
}
|
|
2124
|
+
|
|
2125
|
+
.section {
|
|
2126
|
+
display: grid;
|
|
2127
|
+
gap: 1rem;
|
|
2128
|
+
}
|
|
2129
|
+
|
|
2130
|
+
.sectionTitle {
|
|
2131
|
+
font-size: 1rem;
|
|
2132
|
+
font-weight: 600;
|
|
2133
|
+
}
|
|
2134
|
+
|
|
2135
|
+
.buttonGroup {
|
|
2136
|
+
display: grid;
|
|
2137
|
+
gap: 0.5rem;
|
|
2138
|
+
grid-template-columns: repeat(auto-fit, minmax(10rem, 1fr));
|
|
2139
|
+
}
|
|
2140
|
+
|
|
2141
|
+
.customToast {
|
|
2142
|
+
display: flex;
|
|
2143
|
+
align-items: flex-start;
|
|
2144
|
+
gap: 0.75rem;
|
|
2145
|
+
padding: 1rem;
|
|
2146
|
+
background-color: var(--ac-card);
|
|
2147
|
+
border: 1px solid var(--ac-border);
|
|
2148
|
+
border-radius: var(--ac-radius-md);
|
|
2149
|
+
box-shadow: var(--ac-shadow-lg);
|
|
2150
|
+
}
|
|
2151
|
+
|
|
2152
|
+
.customIcon {
|
|
2153
|
+
flex-shrink: 0;
|
|
2154
|
+
width: 1.25rem;
|
|
2155
|
+
height: 1.25rem;
|
|
2156
|
+
color: var(--ac-primary);
|
|
2157
|
+
}
|
|
2158
|
+
|
|
2159
|
+
.customContent {
|
|
2160
|
+
display: grid;
|
|
2161
|
+
gap: 0.25rem;
|
|
2162
|
+
}
|
|
2163
|
+
|
|
2164
|
+
.customTitle {
|
|
2165
|
+
font-weight: 600;
|
|
2166
|
+
}
|
|
2167
|
+
|
|
2168
|
+
.customDescription {
|
|
2169
|
+
font-size: 0.875rem;
|
|
2170
|
+
color: var(--ac-muted-foreground);
|
|
2171
|
+
}
|
|
2172
|
+
```
|
|
2173
|
+
|
|
2174
|
+
---
|
|
2175
|
+
|
|
2176
|
+
### Recipe 5: Sidebar Navigation (with Keyboard Support)
|
|
2177
|
+
|
|
2178
|
+
**Responsive sidebar with keyboard navigation and active states.**
|
|
2179
|
+
|
|
2180
|
+
```tsx
|
|
2181
|
+
import {
|
|
2182
|
+
ChevronDown,
|
|
2183
|
+
FileText,
|
|
2184
|
+
Home,
|
|
2185
|
+
Settings,
|
|
2186
|
+
Users,
|
|
2187
|
+
} from "lucide-react";
|
|
2188
|
+
import {useState} from "react";
|
|
2189
|
+
|
|
2190
|
+
import {
|
|
2191
|
+
Collapsible,
|
|
2192
|
+
CollapsibleContent,
|
|
2193
|
+
CollapsibleTrigger,
|
|
2194
|
+
} from "@arolariu/components/collapsible";
|
|
2195
|
+
import {
|
|
2196
|
+
Sidebar,
|
|
2197
|
+
SidebarContent,
|
|
2198
|
+
SidebarGroup,
|
|
2199
|
+
SidebarGroupContent,
|
|
2200
|
+
SidebarGroupLabel,
|
|
2201
|
+
SidebarMenu,
|
|
2202
|
+
SidebarMenuButton,
|
|
2203
|
+
SidebarMenuItem,
|
|
2204
|
+
SidebarMenuSub,
|
|
2205
|
+
SidebarMenuSubButton,
|
|
2206
|
+
SidebarMenuSubItem,
|
|
2207
|
+
SidebarProvider,
|
|
2208
|
+
SidebarTrigger,
|
|
2209
|
+
} from "@arolariu/components/sidebar";
|
|
2210
|
+
import styles from "./app-sidebar.module.css";
|
|
2211
|
+
|
|
2212
|
+
const menuItems = [
|
|
2213
|
+
{
|
|
2214
|
+
title: "Dashboard",
|
|
2215
|
+
icon: Home,
|
|
2216
|
+
url: "/dashboard",
|
|
2217
|
+
},
|
|
2218
|
+
{
|
|
2219
|
+
title: "Team",
|
|
2220
|
+
icon: Users,
|
|
2221
|
+
url: "/team",
|
|
2222
|
+
submenu: [
|
|
2223
|
+
{title: "Members", url: "/team/members"},
|
|
2224
|
+
{title: "Roles", url: "/team/roles"},
|
|
2225
|
+
{title: "Invitations", url: "/team/invitations"},
|
|
2226
|
+
],
|
|
2227
|
+
},
|
|
2228
|
+
{
|
|
2229
|
+
title: "Projects",
|
|
2230
|
+
icon: FileText,
|
|
2231
|
+
url: "/projects",
|
|
2232
|
+
submenu: [
|
|
2233
|
+
{title: "Active", url: "/projects/active"},
|
|
2234
|
+
{title: "Archived", url: "/projects/archived"},
|
|
2235
|
+
{title: "Templates", url: "/projects/templates"},
|
|
2236
|
+
],
|
|
2237
|
+
},
|
|
2238
|
+
{
|
|
2239
|
+
title: "Settings",
|
|
2240
|
+
icon: Settings,
|
|
2241
|
+
url: "/settings",
|
|
2242
|
+
},
|
|
2243
|
+
];
|
|
2244
|
+
|
|
2245
|
+
export function AppSidebar() {
|
|
2246
|
+
const [activeItem, setActiveItem] = useState("/dashboard");
|
|
2247
|
+
|
|
2248
|
+
return (
|
|
2249
|
+
<SidebarProvider>
|
|
2250
|
+
<div className={styles.layout}>
|
|
2251
|
+
<Sidebar>
|
|
2252
|
+
<SidebarContent>
|
|
2253
|
+
<SidebarGroup>
|
|
2254
|
+
<SidebarGroupLabel>Application</SidebarGroupLabel>
|
|
2255
|
+
<SidebarGroupContent>
|
|
2256
|
+
<SidebarMenu>
|
|
2257
|
+
{menuItems.map((item) => {
|
|
2258
|
+
const isActive = activeItem === item.url || activeItem.startsWith(item.url + "/");
|
|
2259
|
+
|
|
2260
|
+
if (item.submenu) {
|
|
2261
|
+
return (
|
|
2262
|
+
<Collapsible key={item.title} defaultOpen={isActive}>
|
|
2263
|
+
<SidebarMenuItem>
|
|
2264
|
+
<CollapsibleTrigger asChild>
|
|
2265
|
+
<SidebarMenuButton isActive={isActive}>
|
|
2266
|
+
<item.icon />
|
|
2267
|
+
<span>{item.title}</span>
|
|
2268
|
+
<ChevronDown className={styles.chevron} />
|
|
2269
|
+
</SidebarMenuButton>
|
|
2270
|
+
</CollapsibleTrigger>
|
|
2271
|
+
<CollapsibleContent>
|
|
2272
|
+
<SidebarMenuSub>
|
|
2273
|
+
{item.submenu.map((subitem) => (
|
|
2274
|
+
<SidebarMenuSubItem key={subitem.title}>
|
|
2275
|
+
<SidebarMenuSubButton
|
|
2276
|
+
isActive={activeItem === subitem.url}
|
|
2277
|
+
onClick={() => setActiveItem(subitem.url)}
|
|
2278
|
+
>
|
|
2279
|
+
{subitem.title}
|
|
2280
|
+
</SidebarMenuSubButton>
|
|
2281
|
+
</SidebarMenuSubItem>
|
|
2282
|
+
))}
|
|
2283
|
+
</SidebarMenuSub>
|
|
2284
|
+
</CollapsibleContent>
|
|
2285
|
+
</SidebarMenuItem>
|
|
2286
|
+
</Collapsible>
|
|
2287
|
+
);
|
|
2288
|
+
}
|
|
2289
|
+
|
|
2290
|
+
return (
|
|
2291
|
+
<SidebarMenuItem key={item.title}>
|
|
2292
|
+
<SidebarMenuButton
|
|
2293
|
+
isActive={isActive}
|
|
2294
|
+
onClick={() => setActiveItem(item.url)}
|
|
2295
|
+
>
|
|
2296
|
+
<item.icon />
|
|
2297
|
+
<span>{item.title}</span>
|
|
2298
|
+
</SidebarMenuButton>
|
|
2299
|
+
</SidebarMenuItem>
|
|
2300
|
+
);
|
|
2301
|
+
})}
|
|
2302
|
+
</SidebarMenu>
|
|
2303
|
+
</SidebarGroupContent>
|
|
2304
|
+
</SidebarGroup>
|
|
2305
|
+
</SidebarContent>
|
|
2306
|
+
</Sidebar>
|
|
2307
|
+
|
|
2308
|
+
<main className={styles.main}>
|
|
2309
|
+
<div className={styles.header}>
|
|
2310
|
+
<SidebarTrigger />
|
|
2311
|
+
<h1 className={styles.title}>Welcome to Dashboard</h1>
|
|
2312
|
+
</div>
|
|
2313
|
+
|
|
2314
|
+
<div className={styles.content}>
|
|
2315
|
+
<p>Current route: {activeItem}</p>
|
|
2316
|
+
</div>
|
|
2317
|
+
</main>
|
|
2318
|
+
</div>
|
|
2319
|
+
</SidebarProvider>
|
|
2320
|
+
);
|
|
2321
|
+
}
|
|
2322
|
+
```
|
|
2323
|
+
|
|
2324
|
+
```css
|
|
2325
|
+
/* app-sidebar.module.css */
|
|
2326
|
+
.layout {
|
|
2327
|
+
display: flex;
|
|
2328
|
+
min-height: 100vh;
|
|
2329
|
+
}
|
|
2330
|
+
|
|
2331
|
+
.main {
|
|
2332
|
+
flex: 1;
|
|
2333
|
+
display: grid;
|
|
2334
|
+
grid-template-rows: auto 1fr;
|
|
2335
|
+
}
|
|
2336
|
+
|
|
2337
|
+
.header {
|
|
2338
|
+
display: flex;
|
|
2339
|
+
align-items: center;
|
|
2340
|
+
gap: 1rem;
|
|
2341
|
+
padding: 1rem;
|
|
2342
|
+
border-bottom: 1px solid var(--ac-border);
|
|
2343
|
+
}
|
|
2344
|
+
|
|
2345
|
+
.title {
|
|
2346
|
+
font-size: 1.5rem;
|
|
2347
|
+
font-weight: 600;
|
|
2348
|
+
}
|
|
2349
|
+
|
|
2350
|
+
.content {
|
|
2351
|
+
padding: 2rem;
|
|
2352
|
+
}
|
|
2353
|
+
|
|
2354
|
+
.chevron {
|
|
2355
|
+
margin-left: auto;
|
|
2356
|
+
transition: transform 150ms;
|
|
2357
|
+
}
|
|
2358
|
+
|
|
2359
|
+
:global([data-state="open"]) .chevron {
|
|
2360
|
+
transform: rotate(180deg);
|
|
2361
|
+
}
|
|
2362
|
+
```
|
|
2363
|
+
|
|
2364
|
+
---
|
|
2365
|
+
|
|
2366
|
+
### Recipe 6: Accessible Dropdown Menu (with Keyboard Nav)
|
|
2367
|
+
|
|
2368
|
+
**Fully accessible dropdown menu with keyboard shortcuts.**
|
|
2369
|
+
|
|
2370
|
+
```tsx
|
|
2371
|
+
import {
|
|
2372
|
+
Copy,
|
|
2373
|
+
Download,
|
|
2374
|
+
Edit,
|
|
2375
|
+
LogOut,
|
|
2376
|
+
MoreVertical,
|
|
2377
|
+
Share2,
|
|
2378
|
+
Trash2,
|
|
2379
|
+
User,
|
|
2380
|
+
} from "lucide-react";
|
|
2381
|
+
|
|
2382
|
+
import {Button} from "@arolariu/components/button";
|
|
2383
|
+
import {
|
|
2384
|
+
DropdownMenu,
|
|
2385
|
+
DropdownMenuContent,
|
|
2386
|
+
DropdownMenuGroup,
|
|
2387
|
+
DropdownMenuItem,
|
|
2388
|
+
DropdownMenuLabel,
|
|
2389
|
+
DropdownMenuSeparator,
|
|
2390
|
+
DropdownMenuShortcut,
|
|
2391
|
+
DropdownMenuTrigger,
|
|
2392
|
+
} from "@arolariu/components/dropdown-menu";
|
|
2393
|
+
import {toast} from "@arolariu/components/sonner";
|
|
2394
|
+
import styles from "./dropdown-demo.module.css";
|
|
2395
|
+
|
|
2396
|
+
export function AccessibleDropdownMenu() {
|
|
2397
|
+
const handleAction = (action: string) => {
|
|
2398
|
+
toast.info(`Action: ${action}`);
|
|
2399
|
+
};
|
|
2400
|
+
|
|
2401
|
+
return (
|
|
2402
|
+
<div className={styles.container}>
|
|
2403
|
+
<DropdownMenu>
|
|
2404
|
+
<DropdownMenuTrigger render={<Button variant="outline" />}>
|
|
2405
|
+
<MoreVertical />
|
|
2406
|
+
Actions
|
|
2407
|
+
</DropdownMenuTrigger>
|
|
2408
|
+
|
|
2409
|
+
<DropdownMenuContent align="end" className={styles.content}>
|
|
2410
|
+
<DropdownMenuLabel>My Account</DropdownMenuLabel>
|
|
2411
|
+
|
|
2412
|
+
<DropdownMenuSeparator />
|
|
2413
|
+
|
|
2414
|
+
<DropdownMenuGroup>
|
|
2415
|
+
<DropdownMenuItem onClick={() => handleAction("Profile")}>
|
|
2416
|
+
<User />
|
|
2417
|
+
<span>Profile</span>
|
|
2418
|
+
<DropdownMenuShortcut>⇧⌘P</DropdownMenuShortcut>
|
|
2419
|
+
</DropdownMenuItem>
|
|
2420
|
+
|
|
2421
|
+
<DropdownMenuItem onClick={() => handleAction("Edit")}>
|
|
2422
|
+
<Edit />
|
|
2423
|
+
<span>Edit</span>
|
|
2424
|
+
<DropdownMenuShortcut>⌘E</DropdownMenuShortcut>
|
|
2425
|
+
</DropdownMenuItem>
|
|
2426
|
+
|
|
2427
|
+
<DropdownMenuItem onClick={() => handleAction("Copy")}>
|
|
2428
|
+
<Copy />
|
|
2429
|
+
<span>Copy Link</span>
|
|
2430
|
+
<DropdownMenuShortcut>⌘C</DropdownMenuShortcut>
|
|
2431
|
+
</DropdownMenuItem>
|
|
2432
|
+
|
|
2433
|
+
<DropdownMenuItem onClick={() => handleAction("Share")}>
|
|
2434
|
+
<Share2 />
|
|
2435
|
+
<span>Share</span>
|
|
2436
|
+
<DropdownMenuShortcut>⌘S</DropdownMenuShortcut>
|
|
2437
|
+
</DropdownMenuItem>
|
|
2438
|
+
</DropdownMenuGroup>
|
|
2439
|
+
|
|
2440
|
+
<DropdownMenuSeparator />
|
|
2441
|
+
|
|
2442
|
+
<DropdownMenuGroup>
|
|
2443
|
+
<DropdownMenuItem onClick={() => handleAction("Download")}>
|
|
2444
|
+
<Download />
|
|
2445
|
+
<span>Download</span>
|
|
2446
|
+
<DropdownMenuShortcut>⌘D</DropdownMenuShortcut>
|
|
2447
|
+
</DropdownMenuItem>
|
|
2448
|
+
</DropdownMenuGroup>
|
|
2449
|
+
|
|
2450
|
+
<DropdownMenuSeparator />
|
|
2451
|
+
|
|
2452
|
+
<DropdownMenuItem
|
|
2453
|
+
onClick={() => handleAction("Delete")}
|
|
2454
|
+
className={styles.dangerItem}
|
|
2455
|
+
>
|
|
2456
|
+
<Trash2 />
|
|
2457
|
+
<span>Delete</span>
|
|
2458
|
+
<DropdownMenuShortcut>⌘⌫</DropdownMenuShortcut>
|
|
2459
|
+
</DropdownMenuItem>
|
|
2460
|
+
|
|
2461
|
+
<DropdownMenuSeparator />
|
|
2462
|
+
|
|
2463
|
+
<DropdownMenuItem onClick={() => handleAction("Logout")}>
|
|
2464
|
+
<LogOut />
|
|
2465
|
+
<span>Log out</span>
|
|
2466
|
+
<DropdownMenuShortcut>⇧⌘Q</DropdownMenuShortcut>
|
|
2467
|
+
</DropdownMenuItem>
|
|
2468
|
+
</DropdownMenuContent>
|
|
2469
|
+
</DropdownMenu>
|
|
2470
|
+
|
|
2471
|
+
<div className={styles.instructions}>
|
|
2472
|
+
<p className={styles.instructionTitle}>Keyboard Navigation:</p>
|
|
2473
|
+
<ul className={styles.instructionList}>
|
|
2474
|
+
<li><kbd>Enter</kbd> or <kbd>Space</kbd> - Open menu</li>
|
|
2475
|
+
<li><kbd>↑</kbd> <kbd>↓</kbd> - Navigate items</li>
|
|
2476
|
+
<li><kbd>Enter</kbd> - Select item</li>
|
|
2477
|
+
<li><kbd>Esc</kbd> - Close menu</li>
|
|
2478
|
+
</ul>
|
|
2479
|
+
</div>
|
|
2480
|
+
</div>
|
|
2481
|
+
);
|
|
2482
|
+
}
|
|
2483
|
+
```
|
|
2484
|
+
|
|
2485
|
+
```css
|
|
2486
|
+
/* dropdown-demo.module.css */
|
|
2487
|
+
.container {
|
|
2488
|
+
display: flex;
|
|
2489
|
+
flex-direction: column;
|
|
2490
|
+
align-items: center;
|
|
2491
|
+
gap: 2rem;
|
|
2492
|
+
padding: 4rem 1rem;
|
|
2493
|
+
}
|
|
2494
|
+
|
|
2495
|
+
.content {
|
|
2496
|
+
min-width: 14rem;
|
|
2497
|
+
}
|
|
2498
|
+
|
|
2499
|
+
.dangerItem {
|
|
2500
|
+
color: var(--ac-destructive);
|
|
2501
|
+
}
|
|
2502
|
+
|
|
2503
|
+
.instructions {
|
|
2504
|
+
display: grid;
|
|
2505
|
+
gap: 0.5rem;
|
|
2506
|
+
padding: 1rem;
|
|
2507
|
+
background-color: var(--ac-muted);
|
|
2508
|
+
border-radius: var(--ac-radius-md);
|
|
2509
|
+
}
|
|
2510
|
+
|
|
2511
|
+
.instructionTitle {
|
|
2512
|
+
font-weight: 600;
|
|
2513
|
+
}
|
|
2514
|
+
|
|
2515
|
+
.instructionList {
|
|
2516
|
+
display: grid;
|
|
2517
|
+
gap: 0.25rem;
|
|
2518
|
+
padding-left: 1.5rem;
|
|
2519
|
+
font-size: 0.875rem;
|
|
2520
|
+
color: var(--ac-muted-foreground);
|
|
2521
|
+
}
|
|
2522
|
+
|
|
2523
|
+
.instructionList kbd {
|
|
2524
|
+
display: inline-block;
|
|
2525
|
+
padding: 0.125rem 0.375rem;
|
|
2526
|
+
background-color: var(--ac-background);
|
|
2527
|
+
border: 1px solid var(--ac-border);
|
|
2528
|
+
border-radius: var(--ac-radius-sm);
|
|
2529
|
+
font-family: var(--ac-font-mono);
|
|
2530
|
+
font-size: 0.75rem;
|
|
2531
|
+
}
|
|
2532
|
+
```
|
|
2533
|
+
|
|
2534
|
+
---
|
|
2535
|
+
|
|
2536
|
+
### Recipe 7: Date Picker (Calendar Integration)
|
|
2537
|
+
|
|
2538
|
+
**Calendar-based date picker with range selection.**
|
|
2539
|
+
|
|
2540
|
+
```tsx
|
|
2541
|
+
import {format} from "date-fns";
|
|
2542
|
+
import {Calendar as CalendarIcon} from "lucide-react";
|
|
2543
|
+
import {useState} from "react";
|
|
2544
|
+
|
|
2545
|
+
import {Button} from "@arolariu/components/button";
|
|
2546
|
+
import {Calendar} from "@arolariu/components/calendar";
|
|
2547
|
+
import {
|
|
2548
|
+
Popover,
|
|
2549
|
+
PopoverContent,
|
|
2550
|
+
PopoverTrigger,
|
|
2551
|
+
} from "@arolariu/components/popover";
|
|
2552
|
+
import {cn} from "@arolariu/components/utilities";
|
|
2553
|
+
import styles from "./date-picker.module.css";
|
|
2554
|
+
|
|
2555
|
+
export function DatePicker() {
|
|
2556
|
+
const [date, setDate] = useState<Date | undefined>();
|
|
2557
|
+
|
|
2558
|
+
return (
|
|
2559
|
+
<div className={styles.container}>
|
|
2560
|
+
<div className={styles.field}>
|
|
2561
|
+
<label className={styles.label}>Select Date</label>
|
|
2562
|
+
<Popover>
|
|
2563
|
+
<PopoverTrigger render={<Button variant="outline" className={styles.trigger} />}>
|
|
2564
|
+
<CalendarIcon className={styles.icon} />
|
|
2565
|
+
{date ? format(date, "PPP") : <span>Pick a date</span>}
|
|
2566
|
+
</PopoverTrigger>
|
|
2567
|
+
<PopoverContent align="start" className={styles.popoverContent}>
|
|
2568
|
+
<Calendar
|
|
2569
|
+
mode="single"
|
|
2570
|
+
selected={date}
|
|
2571
|
+
onSelect={setDate}
|
|
2572
|
+
initialFocus
|
|
2573
|
+
/>
|
|
2574
|
+
</PopoverContent>
|
|
2575
|
+
</Popover>
|
|
2576
|
+
</div>
|
|
2577
|
+
</div>
|
|
2578
|
+
);
|
|
2579
|
+
}
|
|
2580
|
+
|
|
2581
|
+
export function DateRangePicker() {
|
|
2582
|
+
const [dateRange, setDateRange] = useState<{from: Date | undefined; to: Date | undefined}>({
|
|
2583
|
+
from: undefined,
|
|
2584
|
+
to: undefined,
|
|
2585
|
+
});
|
|
2586
|
+
|
|
2587
|
+
return (
|
|
2588
|
+
<div className={styles.container}>
|
|
2589
|
+
<div className={styles.field}>
|
|
2590
|
+
<label className={styles.label}>Select Date Range</label>
|
|
2591
|
+
<Popover>
|
|
2592
|
+
<PopoverTrigger render={<Button variant="outline" className={styles.trigger} />}>
|
|
2593
|
+
<CalendarIcon className={styles.icon} />
|
|
2594
|
+
{dateRange.from ? (
|
|
2595
|
+
dateRange.to ? (
|
|
2596
|
+
<>
|
|
2597
|
+
{format(dateRange.from, "LLL dd, y")} - {format(dateRange.to, "LLL dd, y")}
|
|
2598
|
+
</>
|
|
2599
|
+
) : (
|
|
2600
|
+
format(dateRange.from, "LLL dd, y")
|
|
2601
|
+
)
|
|
2602
|
+
) : (
|
|
2603
|
+
<span>Pick a date range</span>
|
|
2604
|
+
)}
|
|
2605
|
+
</PopoverTrigger>
|
|
2606
|
+
<PopoverContent align="start" className={styles.popoverContent}>
|
|
2607
|
+
<Calendar
|
|
2608
|
+
mode="range"
|
|
2609
|
+
selected={dateRange}
|
|
2610
|
+
onSelect={(range) =>
|
|
2611
|
+
setDateRange({
|
|
2612
|
+
from: range?.from,
|
|
2613
|
+
to: range?.to,
|
|
2614
|
+
})
|
|
2615
|
+
}
|
|
2616
|
+
numberOfMonths={2}
|
|
2617
|
+
initialFocus
|
|
2618
|
+
/>
|
|
2619
|
+
</PopoverContent>
|
|
2620
|
+
</Popover>
|
|
2621
|
+
</div>
|
|
2622
|
+
</div>
|
|
2623
|
+
);
|
|
2624
|
+
}
|
|
2625
|
+
```
|
|
2626
|
+
|
|
2627
|
+
```css
|
|
2628
|
+
/* date-picker.module.css */
|
|
2629
|
+
.container {
|
|
2630
|
+
display: flex;
|
|
2631
|
+
align-items: center;
|
|
2632
|
+
justify-content: center;
|
|
2633
|
+
min-height: 100vh;
|
|
2634
|
+
padding: 1rem;
|
|
2635
|
+
}
|
|
2636
|
+
|
|
2637
|
+
.field {
|
|
2638
|
+
display: grid;
|
|
2639
|
+
gap: 0.5rem;
|
|
2640
|
+
width: min(24rem, 100%);
|
|
2641
|
+
}
|
|
2642
|
+
|
|
2643
|
+
.label {
|
|
2644
|
+
font-weight: 500;
|
|
2645
|
+
}
|
|
2646
|
+
|
|
2647
|
+
.trigger {
|
|
2648
|
+
justify-content: flex-start;
|
|
2649
|
+
text-align: left;
|
|
2650
|
+
font-weight: 400;
|
|
2651
|
+
}
|
|
2652
|
+
|
|
2653
|
+
.icon {
|
|
2654
|
+
width: 1rem;
|
|
2655
|
+
height: 1rem;
|
|
2656
|
+
margin-right: 0.5rem;
|
|
2657
|
+
}
|
|
2658
|
+
|
|
2659
|
+
.popoverContent {
|
|
2660
|
+
width: auto;
|
|
2661
|
+
padding: 0;
|
|
2662
|
+
}
|
|
2663
|
+
```
|
|
2664
|
+
|
|
2665
|
+
---
|
|
2666
|
+
|
|
2667
|
+
### Recipe 8: File Upload Area (with Progress)
|
|
2668
|
+
|
|
2669
|
+
**Drag-and-drop file upload with progress tracking.**
|
|
2670
|
+
|
|
2671
|
+
```tsx
|
|
2672
|
+
import {Upload, X} from "lucide-react";
|
|
2673
|
+
import {useState} from "react";
|
|
2674
|
+
|
|
2675
|
+
import {Button} from "@arolariu/components/button";
|
|
2676
|
+
import {Card, CardContent, CardHeader, CardTitle} from "@arolariu/components/card";
|
|
2677
|
+
import {Progress} from "@arolariu/components/progress";
|
|
2678
|
+
import {toast} from "@arolariu/components/sonner";
|
|
2679
|
+
import styles from "./file-upload.module.css";
|
|
2680
|
+
|
|
2681
|
+
interface FileUpload {
|
|
2682
|
+
id: string;
|
|
2683
|
+
file: File;
|
|
2684
|
+
progress: number;
|
|
2685
|
+
status: "uploading" | "complete" | "error";
|
|
2686
|
+
}
|
|
2687
|
+
|
|
2688
|
+
export function FileUploadArea() {
|
|
2689
|
+
const [uploads, setUploads] = useState<FileUpload[]>([]);
|
|
2690
|
+
const [isDragging, setIsDragging] = useState(false);
|
|
2691
|
+
|
|
2692
|
+
const handleDragOver = (e: React.DragEvent) => {
|
|
2693
|
+
e.preventDefault();
|
|
2694
|
+
setIsDragging(true);
|
|
2695
|
+
};
|
|
2696
|
+
|
|
2697
|
+
const handleDragLeave = () => {
|
|
2698
|
+
setIsDragging(false);
|
|
2699
|
+
};
|
|
2700
|
+
|
|
2701
|
+
const handleDrop = (e: React.DragEvent) => {
|
|
2702
|
+
e.preventDefault();
|
|
2703
|
+
setIsDragging(false);
|
|
2704
|
+
|
|
2705
|
+
const files = Array.from(e.dataTransfer.files);
|
|
2706
|
+
handleFiles(files);
|
|
2707
|
+
};
|
|
2708
|
+
|
|
2709
|
+
const handleFileInput = (e: React.ChangeEvent<HTMLInputElement>) => {
|
|
2710
|
+
const files = e.target.files ? Array.from(e.target.files) : [];
|
|
2711
|
+
handleFiles(files);
|
|
2712
|
+
};
|
|
2713
|
+
|
|
2714
|
+
const handleFiles = (files: File[]) => {
|
|
2715
|
+
const newUploads: FileUpload[] = files.map((file) => ({
|
|
2716
|
+
id: Math.random().toString(36).substring(7),
|
|
2717
|
+
file,
|
|
2718
|
+
progress: 0,
|
|
2719
|
+
status: "uploading" as const,
|
|
2720
|
+
}));
|
|
2721
|
+
|
|
2722
|
+
setUploads((prev) => [...prev, ...newUploads]);
|
|
2723
|
+
|
|
2724
|
+
// Simulate upload progress
|
|
2725
|
+
newUploads.forEach((upload) => {
|
|
2726
|
+
simulateUpload(upload.id);
|
|
2727
|
+
});
|
|
2728
|
+
};
|
|
2729
|
+
|
|
2730
|
+
const simulateUpload = (id: string) => {
|
|
2731
|
+
const interval = setInterval(() => {
|
|
2732
|
+
setUploads((prev) =>
|
|
2733
|
+
prev.map((upload) => {
|
|
2734
|
+
if (upload.id === id) {
|
|
2735
|
+
const newProgress = Math.min(upload.progress + 10, 100);
|
|
2736
|
+
const newStatus = newProgress === 100 ? "complete" : upload.status;
|
|
2737
|
+
|
|
2738
|
+
if (newStatus === "complete") {
|
|
2739
|
+
clearInterval(interval);
|
|
2740
|
+
toast.success(`${upload.file.name} uploaded successfully`);
|
|
2741
|
+
}
|
|
2742
|
+
|
|
2743
|
+
return {...upload, progress: newProgress, status: newStatus};
|
|
2744
|
+
}
|
|
2745
|
+
return upload;
|
|
2746
|
+
})
|
|
2747
|
+
);
|
|
2748
|
+
}, 300);
|
|
2749
|
+
};
|
|
2750
|
+
|
|
2751
|
+
const removeUpload = (id: string) => {
|
|
2752
|
+
setUploads((prev) => prev.filter((upload) => upload.id !== id));
|
|
2753
|
+
};
|
|
2754
|
+
|
|
2755
|
+
return (
|
|
2756
|
+
<div className={styles.container}>
|
|
2757
|
+
<Card className={styles.card}>
|
|
2758
|
+
<CardHeader>
|
|
2759
|
+
<CardTitle>Upload Files</CardTitle>
|
|
2760
|
+
</CardHeader>
|
|
2761
|
+
<CardContent className={styles.content}>
|
|
2762
|
+
<div
|
|
2763
|
+
className={`${styles.dropZone} ${isDragging ? styles.dropZoneActive : ""}`}
|
|
2764
|
+
onDragOver={handleDragOver}
|
|
2765
|
+
onDragLeave={handleDragLeave}
|
|
2766
|
+
onDrop={handleDrop}
|
|
2767
|
+
>
|
|
2768
|
+
<Upload className={styles.uploadIcon} />
|
|
2769
|
+
<div className={styles.dropZoneText}>
|
|
2770
|
+
<p className={styles.dropZoneTitle}>Drop files here or click to upload</p>
|
|
2771
|
+
<p className={styles.dropZoneSubtitle}>
|
|
2772
|
+
Supports: Images, PDFs, Documents (Max 10MB)
|
|
2773
|
+
</p>
|
|
2774
|
+
</div>
|
|
2775
|
+
<input
|
|
2776
|
+
type="file"
|
|
2777
|
+
multiple
|
|
2778
|
+
className={styles.fileInput}
|
|
2779
|
+
onChange={handleFileInput}
|
|
2780
|
+
/>
|
|
2781
|
+
</div>
|
|
2782
|
+
|
|
2783
|
+
{uploads.length > 0 ? (
|
|
2784
|
+
<div className={styles.uploadList}>
|
|
2785
|
+
{uploads.map((upload) => (
|
|
2786
|
+
<div key={upload.id} className={styles.uploadItem}>
|
|
2787
|
+
<div className={styles.uploadInfo}>
|
|
2788
|
+
<div className={styles.uploadName}>{upload.file.name}</div>
|
|
2789
|
+
<div className={styles.uploadSize}>
|
|
2790
|
+
{(upload.file.size / 1024 / 1024).toFixed(2)} MB
|
|
2791
|
+
</div>
|
|
2792
|
+
</div>
|
|
2793
|
+
|
|
2794
|
+
<div className={styles.uploadProgress}>
|
|
2795
|
+
<Progress value={upload.progress} className={styles.progressBar} />
|
|
2796
|
+
<span className={styles.uploadPercent}>{upload.progress}%</span>
|
|
2797
|
+
</div>
|
|
2798
|
+
|
|
2799
|
+
{upload.status === "complete" ? (
|
|
2800
|
+
<Button
|
|
2801
|
+
variant="ghost"
|
|
2802
|
+
size="icon"
|
|
2803
|
+
onClick={() => removeUpload(upload.id)}
|
|
2804
|
+
className={styles.removeButton}
|
|
2805
|
+
>
|
|
2806
|
+
<X />
|
|
2807
|
+
</Button>
|
|
2808
|
+
) : null}
|
|
2809
|
+
</div>
|
|
2810
|
+
))}
|
|
2811
|
+
</div>
|
|
2812
|
+
) : null}
|
|
2813
|
+
</CardContent>
|
|
2814
|
+
</Card>
|
|
2815
|
+
</div>
|
|
2816
|
+
);
|
|
2817
|
+
}
|
|
2818
|
+
```
|
|
2819
|
+
|
|
2820
|
+
```css
|
|
2821
|
+
/* file-upload.module.css */
|
|
2822
|
+
.container {
|
|
2823
|
+
display: flex;
|
|
2824
|
+
align-items: center;
|
|
2825
|
+
justify-content: center;
|
|
2826
|
+
min-height: 100vh;
|
|
2827
|
+
padding: 1rem;
|
|
2828
|
+
}
|
|
2829
|
+
|
|
2830
|
+
.card {
|
|
2831
|
+
width: min(40rem, 100%);
|
|
2832
|
+
}
|
|
2833
|
+
|
|
2834
|
+
.content {
|
|
2835
|
+
display: grid;
|
|
2836
|
+
gap: 1.5rem;
|
|
2837
|
+
}
|
|
2838
|
+
|
|
2839
|
+
.dropZone {
|
|
2840
|
+
position: relative;
|
|
2841
|
+
display: flex;
|
|
2842
|
+
flex-direction: column;
|
|
2843
|
+
align-items: center;
|
|
2844
|
+
gap: 1rem;
|
|
2845
|
+
padding: 3rem 2rem;
|
|
2846
|
+
border: 2px dashed var(--ac-border);
|
|
2847
|
+
border-radius: var(--ac-radius-md);
|
|
2848
|
+
background-color: var(--ac-muted);
|
|
2849
|
+
cursor: pointer;
|
|
2850
|
+
transition: all 150ms;
|
|
2851
|
+
}
|
|
2852
|
+
|
|
2853
|
+
.dropZone:hover {
|
|
2854
|
+
border-color: var(--ac-primary);
|
|
2855
|
+
background-color: color-mix(in oklch, var(--ac-primary) 5%, var(--ac-muted));
|
|
2856
|
+
}
|
|
2857
|
+
|
|
2858
|
+
.dropZoneActive {
|
|
2859
|
+
border-color: var(--ac-primary);
|
|
2860
|
+
background-color: color-mix(in oklch, var(--ac-primary) 10%, var(--ac-muted));
|
|
2861
|
+
}
|
|
2862
|
+
|
|
2863
|
+
.uploadIcon {
|
|
2864
|
+
width: 3rem;
|
|
2865
|
+
height: 3rem;
|
|
2866
|
+
color: var(--ac-muted-foreground);
|
|
2867
|
+
}
|
|
2868
|
+
|
|
2869
|
+
.dropZoneText {
|
|
2870
|
+
text-align: center;
|
|
2871
|
+
}
|
|
2872
|
+
|
|
2873
|
+
.dropZoneTitle {
|
|
2874
|
+
font-weight: 600;
|
|
2875
|
+
}
|
|
2876
|
+
|
|
2877
|
+
.dropZoneSubtitle {
|
|
2878
|
+
margin-top: 0.25rem;
|
|
2879
|
+
font-size: 0.875rem;
|
|
2880
|
+
color: var(--ac-muted-foreground);
|
|
2881
|
+
}
|
|
2882
|
+
|
|
2883
|
+
.fileInput {
|
|
2884
|
+
position: absolute;
|
|
2885
|
+
inset: 0;
|
|
2886
|
+
width: 100%;
|
|
2887
|
+
height: 100%;
|
|
2888
|
+
opacity: 0;
|
|
2889
|
+
cursor: pointer;
|
|
2890
|
+
}
|
|
2891
|
+
|
|
2892
|
+
.uploadList {
|
|
2893
|
+
display: grid;
|
|
2894
|
+
gap: 1rem;
|
|
2895
|
+
}
|
|
2896
|
+
|
|
2897
|
+
.uploadItem {
|
|
2898
|
+
display: grid;
|
|
2899
|
+
grid-template-columns: 1fr auto;
|
|
2900
|
+
gap: 1rem;
|
|
2901
|
+
padding: 1rem;
|
|
2902
|
+
border: 1px solid var(--ac-border);
|
|
2903
|
+
border-radius: var(--ac-radius-md);
|
|
2904
|
+
}
|
|
2905
|
+
|
|
2906
|
+
.uploadInfo {
|
|
2907
|
+
display: grid;
|
|
2908
|
+
gap: 0.25rem;
|
|
2909
|
+
}
|
|
2910
|
+
|
|
2911
|
+
.uploadName {
|
|
2912
|
+
font-weight: 500;
|
|
2913
|
+
word-break: break-all;
|
|
2914
|
+
}
|
|
2915
|
+
|
|
2916
|
+
.uploadSize {
|
|
2917
|
+
font-size: 0.875rem;
|
|
2918
|
+
color: var(--ac-muted-foreground);
|
|
2919
|
+
}
|
|
2920
|
+
|
|
2921
|
+
.uploadProgress {
|
|
2922
|
+
grid-column: 1 / -1;
|
|
2923
|
+
display: flex;
|
|
2924
|
+
align-items: center;
|
|
2925
|
+
gap: 0.75rem;
|
|
2926
|
+
}
|
|
2927
|
+
|
|
2928
|
+
.progressBar {
|
|
2929
|
+
flex: 1;
|
|
2930
|
+
}
|
|
2931
|
+
|
|
2932
|
+
.uploadPercent {
|
|
2933
|
+
font-size: 0.875rem;
|
|
2934
|
+
font-weight: 500;
|
|
2935
|
+
color: var(--ac-muted-foreground);
|
|
2936
|
+
}
|
|
2937
|
+
|
|
2938
|
+
.removeButton {
|
|
2939
|
+
align-self: flex-start;
|
|
2940
|
+
}
|
|
2941
|
+
```
|
|
2942
|
+
|
|
2943
|
+
---
|
|
2944
|
+
|
|
2945
|
+
### Recipe 9: Settings Page (Tabs + Form + Switch)
|
|
2946
|
+
|
|
2947
|
+
**Complete settings page with tabs and form controls.**
|
|
2948
|
+
|
|
2949
|
+
```tsx
|
|
2950
|
+
import {zodResolver} from "@hookform/resolvers/zod";
|
|
2951
|
+
import {useForm} from "react-hook-form";
|
|
2952
|
+
import * as z from "zod";
|
|
2953
|
+
|
|
2954
|
+
import {Button} from "@arolariu/components/button";
|
|
2955
|
+
import {Card, CardContent, CardDescription, CardHeader, CardTitle} from "@arolariu/components/card";
|
|
2956
|
+
import {
|
|
2957
|
+
Form,
|
|
2958
|
+
FormControl,
|
|
2959
|
+
FormDescription,
|
|
2960
|
+
FormField,
|
|
2961
|
+
FormItem,
|
|
2962
|
+
FormLabel,
|
|
2963
|
+
FormMessage,
|
|
2964
|
+
} from "@arolariu/components/form";
|
|
2965
|
+
import {Input} from "@arolariu/components/input";
|
|
2966
|
+
import {
|
|
2967
|
+
Select,
|
|
2968
|
+
SelectContent,
|
|
2969
|
+
SelectItem,
|
|
2970
|
+
SelectTrigger,
|
|
2971
|
+
SelectValue,
|
|
2972
|
+
} from "@arolariu/components/select";
|
|
2973
|
+
import {Switch} from "@arolariu/components/switch";
|
|
2974
|
+
import {Tabs, TabsContent, TabsList, TabsTrigger} from "@arolariu/components/tabs";
|
|
2975
|
+
import {Textarea} from "@arolariu/components/textarea";
|
|
2976
|
+
import {toast} from "@arolariu/components/sonner";
|
|
2977
|
+
import styles from "./settings-page.module.css";
|
|
2978
|
+
|
|
2979
|
+
const profileSchema = z.object({
|
|
2980
|
+
username: z.string().min(3, "Username must be at least 3 characters"),
|
|
2981
|
+
email: z.string().email("Please enter a valid email address"),
|
|
2982
|
+
bio: z.string().max(500, "Bio must be less than 500 characters").optional(),
|
|
2983
|
+
});
|
|
2984
|
+
|
|
2985
|
+
const notificationSchema = z.object({
|
|
2986
|
+
emailNotifications: z.boolean(),
|
|
2987
|
+
pushNotifications: z.boolean(),
|
|
2988
|
+
weeklyDigest: z.boolean(),
|
|
2989
|
+
});
|
|
2990
|
+
|
|
2991
|
+
const appearanceSchema = z.object({
|
|
2992
|
+
theme: z.enum(["light", "dark", "system"]),
|
|
2993
|
+
language: z.string(),
|
|
2994
|
+
});
|
|
2995
|
+
|
|
2996
|
+
export function SettingsPage() {
|
|
2997
|
+
const profileForm = useForm<z.infer<typeof profileSchema>>({
|
|
2998
|
+
resolver: zodResolver(profileSchema),
|
|
2999
|
+
defaultValues: {
|
|
3000
|
+
username: "johndoe",
|
|
3001
|
+
email: "john@example.com",
|
|
3002
|
+
bio: "",
|
|
3003
|
+
},
|
|
3004
|
+
});
|
|
3005
|
+
|
|
3006
|
+
const notificationForm = useForm<z.infer<typeof notificationSchema>>({
|
|
3007
|
+
resolver: zodResolver(notificationSchema),
|
|
3008
|
+
defaultValues: {
|
|
3009
|
+
emailNotifications: true,
|
|
3010
|
+
pushNotifications: false,
|
|
3011
|
+
weeklyDigest: true,
|
|
3012
|
+
},
|
|
3013
|
+
});
|
|
3014
|
+
|
|
3015
|
+
const appearanceForm = useForm<z.infer<typeof appearanceSchema>>({
|
|
3016
|
+
resolver: zodResolver(appearanceSchema),
|
|
3017
|
+
defaultValues: {
|
|
3018
|
+
theme: "system",
|
|
3019
|
+
language: "en",
|
|
3020
|
+
},
|
|
3021
|
+
});
|
|
3022
|
+
|
|
3023
|
+
const onProfileSubmit = (values: z.infer<typeof profileSchema>) => {
|
|
3024
|
+
console.log("Profile updated:", values);
|
|
3025
|
+
toast.success("Profile updated successfully!");
|
|
3026
|
+
};
|
|
3027
|
+
|
|
3028
|
+
const onNotificationSubmit = (values: z.infer<typeof notificationSchema>) => {
|
|
3029
|
+
console.log("Notifications updated:", values);
|
|
3030
|
+
toast.success("Notification preferences updated!");
|
|
3031
|
+
};
|
|
3032
|
+
|
|
3033
|
+
const onAppearanceSubmit = (values: z.infer<typeof appearanceSchema>) => {
|
|
3034
|
+
console.log("Appearance updated:", values);
|
|
3035
|
+
toast.success("Appearance settings updated!");
|
|
3036
|
+
};
|
|
3037
|
+
|
|
3038
|
+
return (
|
|
3039
|
+
<div className={styles.container}>
|
|
3040
|
+
<div className={styles.header}>
|
|
3041
|
+
<h1 className={styles.title}>Settings</h1>
|
|
3042
|
+
<p className={styles.subtitle}>
|
|
3043
|
+
Manage your account settings and preferences.
|
|
3044
|
+
</p>
|
|
3045
|
+
</div>
|
|
3046
|
+
|
|
3047
|
+
<Tabs defaultValue="profile" className={styles.tabs}>
|
|
3048
|
+
<TabsList>
|
|
3049
|
+
<TabsTrigger value="profile">Profile</TabsTrigger>
|
|
3050
|
+
<TabsTrigger value="notifications">Notifications</TabsTrigger>
|
|
3051
|
+
<TabsTrigger value="appearance">Appearance</TabsTrigger>
|
|
3052
|
+
</TabsList>
|
|
3053
|
+
|
|
3054
|
+
<TabsContent value="profile" className={styles.tabContent}>
|
|
3055
|
+
<Card>
|
|
3056
|
+
<CardHeader>
|
|
3057
|
+
<CardTitle>Profile Information</CardTitle>
|
|
3058
|
+
<CardDescription>
|
|
3059
|
+
Update your profile details and public information.
|
|
3060
|
+
</CardDescription>
|
|
3061
|
+
</CardHeader>
|
|
3062
|
+
<CardContent>
|
|
3063
|
+
<Form {...profileForm}>
|
|
3064
|
+
<form
|
|
3065
|
+
onSubmit={profileForm.handleSubmit(onProfileSubmit)}
|
|
3066
|
+
className={styles.form}
|
|
3067
|
+
>
|
|
3068
|
+
<FormField
|
|
3069
|
+
control={profileForm.control}
|
|
3070
|
+
name="username"
|
|
3071
|
+
render={({field}) => (
|
|
3072
|
+
<FormItem>
|
|
3073
|
+
<FormLabel>Username</FormLabel>
|
|
3074
|
+
<FormControl>
|
|
3075
|
+
<Input placeholder="johndoe" {...field} />
|
|
3076
|
+
</FormControl>
|
|
3077
|
+
<FormDescription>
|
|
3078
|
+
This is your public display name.
|
|
3079
|
+
</FormDescription>
|
|
3080
|
+
<FormMessage />
|
|
3081
|
+
</FormItem>
|
|
3082
|
+
)}
|
|
3083
|
+
/>
|
|
3084
|
+
|
|
3085
|
+
<FormField
|
|
3086
|
+
control={profileForm.control}
|
|
3087
|
+
name="email"
|
|
3088
|
+
render={({field}) => (
|
|
3089
|
+
<FormItem>
|
|
3090
|
+
<FormLabel>Email</FormLabel>
|
|
3091
|
+
<FormControl>
|
|
3092
|
+
<Input type="email" placeholder="john@example.com" {...field} />
|
|
3093
|
+
</FormControl>
|
|
3094
|
+
<FormDescription>
|
|
3095
|
+
Your email address for account notifications.
|
|
3096
|
+
</FormDescription>
|
|
3097
|
+
<FormMessage />
|
|
3098
|
+
</FormItem>
|
|
3099
|
+
)}
|
|
3100
|
+
/>
|
|
3101
|
+
|
|
3102
|
+
<FormField
|
|
3103
|
+
control={profileForm.control}
|
|
3104
|
+
name="bio"
|
|
3105
|
+
render={({field}) => (
|
|
3106
|
+
<FormItem>
|
|
3107
|
+
<FormLabel>Bio</FormLabel>
|
|
3108
|
+
<FormControl>
|
|
3109
|
+
<Textarea
|
|
3110
|
+
placeholder="Tell us about yourself..."
|
|
3111
|
+
className={styles.textarea}
|
|
3112
|
+
{...field}
|
|
3113
|
+
/>
|
|
3114
|
+
</FormControl>
|
|
3115
|
+
<FormDescription>
|
|
3116
|
+
Brief description for your profile. Max 500 characters.
|
|
3117
|
+
</FormDescription>
|
|
3118
|
+
<FormMessage />
|
|
3119
|
+
</FormItem>
|
|
3120
|
+
)}
|
|
3121
|
+
/>
|
|
3122
|
+
|
|
3123
|
+
<Button type="submit">Save Changes</Button>
|
|
3124
|
+
</form>
|
|
3125
|
+
</Form>
|
|
3126
|
+
</CardContent>
|
|
3127
|
+
</Card>
|
|
3128
|
+
</TabsContent>
|
|
3129
|
+
|
|
3130
|
+
<TabsContent value="notifications" className={styles.tabContent}>
|
|
3131
|
+
<Card>
|
|
3132
|
+
<CardHeader>
|
|
3133
|
+
<CardTitle>Notification Preferences</CardTitle>
|
|
3134
|
+
<CardDescription>
|
|
3135
|
+
Choose how you want to receive notifications.
|
|
3136
|
+
</CardDescription>
|
|
3137
|
+
</CardHeader>
|
|
3138
|
+
<CardContent>
|
|
3139
|
+
<Form {...notificationForm}>
|
|
3140
|
+
<form
|
|
3141
|
+
onSubmit={notificationForm.handleSubmit(onNotificationSubmit)}
|
|
3142
|
+
className={styles.form}
|
|
3143
|
+
>
|
|
3144
|
+
<FormField
|
|
3145
|
+
control={notificationForm.control}
|
|
3146
|
+
name="emailNotifications"
|
|
3147
|
+
render={({field}) => (
|
|
3148
|
+
<FormItem className={styles.switchItem}>
|
|
3149
|
+
<div className={styles.switchContent}>
|
|
3150
|
+
<FormLabel>Email Notifications</FormLabel>
|
|
3151
|
+
<FormDescription>
|
|
3152
|
+
Receive email updates about your account activity.
|
|
3153
|
+
</FormDescription>
|
|
3154
|
+
</div>
|
|
3155
|
+
<FormControl>
|
|
3156
|
+
<Switch
|
|
3157
|
+
checked={field.value}
|
|
3158
|
+
onCheckedChange={field.onChange}
|
|
3159
|
+
/>
|
|
3160
|
+
</FormControl>
|
|
3161
|
+
</FormItem>
|
|
3162
|
+
)}
|
|
3163
|
+
/>
|
|
3164
|
+
|
|
3165
|
+
<FormField
|
|
3166
|
+
control={notificationForm.control}
|
|
3167
|
+
name="pushNotifications"
|
|
3168
|
+
render={({field}) => (
|
|
3169
|
+
<FormItem className={styles.switchItem}>
|
|
3170
|
+
<div className={styles.switchContent}>
|
|
3171
|
+
<FormLabel>Push Notifications</FormLabel>
|
|
3172
|
+
<FormDescription>
|
|
3173
|
+
Get push notifications on your devices.
|
|
3174
|
+
</FormDescription>
|
|
3175
|
+
</div>
|
|
3176
|
+
<FormControl>
|
|
3177
|
+
<Switch
|
|
3178
|
+
checked={field.value}
|
|
3179
|
+
onCheckedChange={field.onChange}
|
|
3180
|
+
/>
|
|
3181
|
+
</FormControl>
|
|
3182
|
+
</FormItem>
|
|
3183
|
+
)}
|
|
3184
|
+
/>
|
|
3185
|
+
|
|
3186
|
+
<FormField
|
|
3187
|
+
control={notificationForm.control}
|
|
3188
|
+
name="weeklyDigest"
|
|
3189
|
+
render={({field}) => (
|
|
3190
|
+
<FormItem className={styles.switchItem}>
|
|
3191
|
+
<div className={styles.switchContent}>
|
|
3192
|
+
<FormLabel>Weekly Digest</FormLabel>
|
|
3193
|
+
<FormDescription>
|
|
3194
|
+
Receive a weekly summary of your activity.
|
|
3195
|
+
</FormDescription>
|
|
3196
|
+
</div>
|
|
3197
|
+
<FormControl>
|
|
3198
|
+
<Switch
|
|
3199
|
+
checked={field.value}
|
|
3200
|
+
onCheckedChange={field.onChange}
|
|
3201
|
+
/>
|
|
3202
|
+
</FormControl>
|
|
3203
|
+
</FormItem>
|
|
3204
|
+
)}
|
|
3205
|
+
/>
|
|
3206
|
+
|
|
3207
|
+
<Button type="submit">Save Preferences</Button>
|
|
3208
|
+
</form>
|
|
3209
|
+
</Form>
|
|
3210
|
+
</CardContent>
|
|
3211
|
+
</Card>
|
|
3212
|
+
</TabsContent>
|
|
3213
|
+
|
|
3214
|
+
<TabsContent value="appearance" className={styles.tabContent}>
|
|
3215
|
+
<Card>
|
|
3216
|
+
<CardHeader>
|
|
3217
|
+
<CardTitle>Appearance Settings</CardTitle>
|
|
3218
|
+
<CardDescription>
|
|
3219
|
+
Customize how the application looks and feels.
|
|
3220
|
+
</CardDescription>
|
|
3221
|
+
</CardHeader>
|
|
3222
|
+
<CardContent>
|
|
3223
|
+
<Form {...appearanceForm}>
|
|
3224
|
+
<form
|
|
3225
|
+
onSubmit={appearanceForm.handleSubmit(onAppearanceSubmit)}
|
|
3226
|
+
className={styles.form}
|
|
3227
|
+
>
|
|
3228
|
+
<FormField
|
|
3229
|
+
control={appearanceForm.control}
|
|
3230
|
+
name="theme"
|
|
3231
|
+
render={({field}) => (
|
|
3232
|
+
<FormItem>
|
|
3233
|
+
<FormLabel>Theme</FormLabel>
|
|
3234
|
+
<Select
|
|
3235
|
+
onValueChange={field.onChange}
|
|
3236
|
+
defaultValue={field.value}
|
|
3237
|
+
>
|
|
3238
|
+
<FormControl>
|
|
3239
|
+
<SelectTrigger>
|
|
3240
|
+
<SelectValue placeholder="Select a theme" />
|
|
3241
|
+
</SelectTrigger>
|
|
3242
|
+
</FormControl>
|
|
3243
|
+
<SelectContent>
|
|
3244
|
+
<SelectItem value="light">Light</SelectItem>
|
|
3245
|
+
<SelectItem value="dark">Dark</SelectItem>
|
|
3246
|
+
<SelectItem value="system">System</SelectItem>
|
|
3247
|
+
</SelectContent>
|
|
3248
|
+
</Select>
|
|
3249
|
+
<FormDescription>
|
|
3250
|
+
Choose your preferred color scheme.
|
|
3251
|
+
</FormDescription>
|
|
3252
|
+
<FormMessage />
|
|
3253
|
+
</FormItem>
|
|
3254
|
+
)}
|
|
3255
|
+
/>
|
|
3256
|
+
|
|
3257
|
+
<FormField
|
|
3258
|
+
control={appearanceForm.control}
|
|
3259
|
+
name="language"
|
|
3260
|
+
render={({field}) => (
|
|
3261
|
+
<FormItem>
|
|
3262
|
+
<FormLabel>Language</FormLabel>
|
|
3263
|
+
<Select
|
|
3264
|
+
onValueChange={field.onChange}
|
|
3265
|
+
defaultValue={field.value}
|
|
3266
|
+
>
|
|
3267
|
+
<FormControl>
|
|
3268
|
+
<SelectTrigger>
|
|
3269
|
+
<SelectValue placeholder="Select a language" />
|
|
3270
|
+
</SelectTrigger>
|
|
3271
|
+
</FormControl>
|
|
3272
|
+
<SelectContent>
|
|
3273
|
+
<SelectItem value="en">English</SelectItem>
|
|
3274
|
+
<SelectItem value="es">Español</SelectItem>
|
|
3275
|
+
<SelectItem value="fr">Français</SelectItem>
|
|
3276
|
+
<SelectItem value="de">Deutsch</SelectItem>
|
|
3277
|
+
</SelectContent>
|
|
3278
|
+
</Select>
|
|
3279
|
+
<FormDescription>
|
|
3280
|
+
Select your preferred language.
|
|
3281
|
+
</FormDescription>
|
|
3282
|
+
<FormMessage />
|
|
3283
|
+
</FormItem>
|
|
3284
|
+
)}
|
|
3285
|
+
/>
|
|
3286
|
+
|
|
3287
|
+
<Button type="submit">Save Settings</Button>
|
|
3288
|
+
</form>
|
|
3289
|
+
</Form>
|
|
3290
|
+
</CardContent>
|
|
3291
|
+
</Card>
|
|
3292
|
+
</TabsContent>
|
|
3293
|
+
</Tabs>
|
|
3294
|
+
</div>
|
|
3295
|
+
);
|
|
3296
|
+
}
|
|
3297
|
+
```
|
|
3298
|
+
|
|
3299
|
+
```css
|
|
3300
|
+
/* settings-page.module.css */
|
|
3301
|
+
.container {
|
|
3302
|
+
max-width: 56rem;
|
|
3303
|
+
margin-inline: auto;
|
|
3304
|
+
padding: 2rem 1rem;
|
|
3305
|
+
}
|
|
3306
|
+
|
|
3307
|
+
.header {
|
|
3308
|
+
margin-bottom: 2rem;
|
|
3309
|
+
}
|
|
3310
|
+
|
|
3311
|
+
.title {
|
|
3312
|
+
font-size: 2rem;
|
|
3313
|
+
font-weight: 700;
|
|
3314
|
+
}
|
|
3315
|
+
|
|
3316
|
+
.subtitle {
|
|
3317
|
+
margin-top: 0.5rem;
|
|
3318
|
+
color: var(--ac-muted-foreground);
|
|
3319
|
+
}
|
|
3320
|
+
|
|
3321
|
+
.tabs {
|
|
3322
|
+
display: grid;
|
|
3323
|
+
gap: 1rem;
|
|
3324
|
+
}
|
|
3325
|
+
|
|
3326
|
+
.tabContent {
|
|
3327
|
+
margin-top: 1rem;
|
|
3328
|
+
}
|
|
3329
|
+
|
|
3330
|
+
.form {
|
|
3331
|
+
display: grid;
|
|
3332
|
+
gap: 1.5rem;
|
|
3333
|
+
}
|
|
3334
|
+
|
|
3335
|
+
.textarea {
|
|
3336
|
+
min-height: 6rem;
|
|
3337
|
+
resize: vertical;
|
|
3338
|
+
}
|
|
3339
|
+
|
|
3340
|
+
.switchItem {
|
|
3341
|
+
display: flex;
|
|
3342
|
+
flex-direction: row;
|
|
3343
|
+
align-items: center;
|
|
3344
|
+
justify-content: space-between;
|
|
3345
|
+
padding: 1rem;
|
|
3346
|
+
border: 1px solid var(--ac-border);
|
|
3347
|
+
border-radius: var(--ac-radius-md);
|
|
3348
|
+
}
|
|
3349
|
+
|
|
3350
|
+
.switchContent {
|
|
3351
|
+
display: grid;
|
|
3352
|
+
gap: 0.25rem;
|
|
3353
|
+
}
|
|
3354
|
+
```
|
|
3355
|
+
|
|
3356
|
+
---
|
|
3357
|
+
|
|
3358
|
+
### Recipe 10: Error Handling (ErrorBoundary + AsyncBoundary + Toast)
|
|
3359
|
+
|
|
3360
|
+
**Comprehensive error handling pattern with boundaries and toast notifications.**
|
|
3361
|
+
|
|
3362
|
+
```tsx
|
|
3363
|
+
import {AlertTriangle, RefreshCw} from "lucide-react";
|
|
3364
|
+
import {Component, Suspense, type ReactNode} from "react";
|
|
3365
|
+
|
|
3366
|
+
import {Alert, AlertDescription, AlertTitle} from "@arolariu/components/alert";
|
|
3367
|
+
import {AsyncBoundary} from "@arolariu/components/async-boundary";
|
|
3368
|
+
import {Button} from "@arolariu/components/button";
|
|
3369
|
+
import {Card, CardContent, CardHeader, CardTitle} from "@arolariu/components/card";
|
|
3370
|
+
import {ErrorBoundary} from "@arolariu/components/error-boundary";
|
|
3371
|
+
import {Skeleton} from "@arolariu/components/skeleton";
|
|
3372
|
+
import {toast} from "@arolariu/components/sonner";
|
|
3373
|
+
import styles from "./error-handling.module.css";
|
|
3374
|
+
|
|
3375
|
+
// Async data fetching component
|
|
3376
|
+
async function fetchUserData(userId: string) {
|
|
3377
|
+
await new Promise((resolve) => setTimeout(resolve, 1500));
|
|
3378
|
+
|
|
3379
|
+
// Simulate random error
|
|
3380
|
+
if (Math.random() > 0.7) {
|
|
3381
|
+
throw new Error("Failed to fetch user data");
|
|
3382
|
+
}
|
|
3383
|
+
|
|
3384
|
+
return {
|
|
3385
|
+
id: userId,
|
|
3386
|
+
name: "John Doe",
|
|
3387
|
+
email: "john@example.com",
|
|
3388
|
+
};
|
|
3389
|
+
}
|
|
3390
|
+
|
|
3391
|
+
function UserProfile({userId}: {userId: string}) {
|
|
3392
|
+
const user = use(fetchUserData(userId));
|
|
3393
|
+
|
|
3394
|
+
return (
|
|
3395
|
+
<Card>
|
|
3396
|
+
<CardHeader>
|
|
3397
|
+
<CardTitle>User Profile</CardTitle>
|
|
3398
|
+
</CardHeader>
|
|
3399
|
+
<CardContent className={styles.profile}>
|
|
3400
|
+
<div className={styles.profileField}>
|
|
3401
|
+
<span className={styles.profileLabel}>Name:</span>
|
|
3402
|
+
<span>{user.name}</span>
|
|
3403
|
+
</div>
|
|
3404
|
+
<div className={styles.profileField}>
|
|
3405
|
+
<span className={styles.profileLabel}>Email:</span>
|
|
3406
|
+
<span>{user.email}</span>
|
|
3407
|
+
</div>
|
|
3408
|
+
</CardContent>
|
|
3409
|
+
</Card>
|
|
3410
|
+
);
|
|
3411
|
+
}
|
|
3412
|
+
|
|
3413
|
+
// Loading fallback
|
|
3414
|
+
function ProfileSkeleton() {
|
|
3415
|
+
return (
|
|
3416
|
+
<Card>
|
|
3417
|
+
<CardHeader>
|
|
3418
|
+
<Skeleton className={styles.skeletonTitle} />
|
|
3419
|
+
</CardHeader>
|
|
3420
|
+
<CardContent className={styles.skeletonContent}>
|
|
3421
|
+
<Skeleton className={styles.skeletonField} />
|
|
3422
|
+
<Skeleton className={styles.skeletonField} />
|
|
3423
|
+
</CardContent>
|
|
3424
|
+
</Card>
|
|
3425
|
+
);
|
|
3426
|
+
}
|
|
3427
|
+
|
|
3428
|
+
// Error fallback
|
|
3429
|
+
function ProfileError({error, reset}: {error: Error; reset: () => void}) {
|
|
3430
|
+
return (
|
|
3431
|
+
<Alert variant="destructive">
|
|
3432
|
+
<AlertTriangle />
|
|
3433
|
+
<AlertTitle>Error Loading Profile</AlertTitle>
|
|
3434
|
+
<AlertDescription className={styles.errorDescription}>
|
|
3435
|
+
{error.message}
|
|
3436
|
+
<Button
|
|
3437
|
+
variant="outline"
|
|
3438
|
+
size="sm"
|
|
3439
|
+
onClick={reset}
|
|
3440
|
+
className={styles.retryButton}
|
|
3441
|
+
>
|
|
3442
|
+
<RefreshCw />
|
|
3443
|
+
Retry
|
|
3444
|
+
</Button>
|
|
3445
|
+
</AlertDescription>
|
|
3446
|
+
</Alert>
|
|
3447
|
+
);
|
|
3448
|
+
}
|
|
3449
|
+
|
|
3450
|
+
export function ErrorHandlingDemo() {
|
|
3451
|
+
const handleAPIError = async () => {
|
|
3452
|
+
try {
|
|
3453
|
+
// Simulate API call that fails
|
|
3454
|
+
await new Promise((_, reject) =>
|
|
3455
|
+
setTimeout(() => reject(new Error("API request failed")), 1000)
|
|
3456
|
+
);
|
|
3457
|
+
} catch (error) {
|
|
3458
|
+
toast.error("Failed to load data. Please try again.", {
|
|
3459
|
+
action: {
|
|
3460
|
+
label: "Retry",
|
|
3461
|
+
onClick: () => handleAPIError(),
|
|
3462
|
+
},
|
|
3463
|
+
});
|
|
3464
|
+
}
|
|
3465
|
+
};
|
|
3466
|
+
|
|
3467
|
+
const handleValidationError = () => {
|
|
3468
|
+
toast.error("Validation failed: Email is required");
|
|
3469
|
+
};
|
|
3470
|
+
|
|
3471
|
+
const handleNetworkError = () => {
|
|
3472
|
+
toast.error("Network error. Please check your connection.", {
|
|
3473
|
+
duration: 5000,
|
|
3474
|
+
});
|
|
3475
|
+
};
|
|
3476
|
+
|
|
3477
|
+
return (
|
|
3478
|
+
<div className={styles.container}>
|
|
3479
|
+
<div className={styles.header}>
|
|
3480
|
+
<h1 className={styles.title}>Error Handling Patterns</h1>
|
|
3481
|
+
<p className={styles.subtitle}>
|
|
3482
|
+
Comprehensive error handling with boundaries and toast notifications.
|
|
3483
|
+
</p>
|
|
3484
|
+
</div>
|
|
3485
|
+
|
|
3486
|
+
<div className={styles.grid}>
|
|
3487
|
+
{/* Pattern 1: AsyncBoundary (React 19 pattern) */}
|
|
3488
|
+
<div className={styles.section}>
|
|
3489
|
+
<h2 className={styles.sectionTitle}>1. AsyncBoundary (Suspense + ErrorBoundary)</h2>
|
|
3490
|
+
<AsyncBoundary
|
|
3491
|
+
suspenseFallback={<ProfileSkeleton />}
|
|
3492
|
+
errorFallback={({error, reset}) => <ProfileError error={error} reset={reset} />}
|
|
3493
|
+
>
|
|
3494
|
+
<UserProfile userId="123" />
|
|
3495
|
+
</AsyncBoundary>
|
|
3496
|
+
</div>
|
|
3497
|
+
|
|
3498
|
+
{/* Pattern 2: Toast for API errors */}
|
|
3499
|
+
<div className={styles.section}>
|
|
3500
|
+
<h2 className={styles.sectionTitle}>2. Toast Notifications for Errors</h2>
|
|
3501
|
+
<Card>
|
|
3502
|
+
<CardContent className={styles.buttonGroup}>
|
|
3503
|
+
<Button onClick={handleAPIError} variant="destructive">
|
|
3504
|
+
Trigger API Error
|
|
3505
|
+
</Button>
|
|
3506
|
+
<Button onClick={handleValidationError} variant="destructive">
|
|
3507
|
+
Trigger Validation Error
|
|
3508
|
+
</Button>
|
|
3509
|
+
<Button onClick={handleNetworkError} variant="destructive">
|
|
3510
|
+
Trigger Network Error
|
|
3511
|
+
</Button>
|
|
3512
|
+
</CardContent>
|
|
3513
|
+
</Card>
|
|
3514
|
+
</div>
|
|
3515
|
+
|
|
3516
|
+
{/* Pattern 3: Inline error display */}
|
|
3517
|
+
<div className={styles.section}>
|
|
3518
|
+
<h2 className={styles.sectionTitle}>3. Inline Error Display</h2>
|
|
3519
|
+
<Alert variant="destructive">
|
|
3520
|
+
<AlertTriangle />
|
|
3521
|
+
<AlertTitle>Authentication Error</AlertTitle>
|
|
3522
|
+
<AlertDescription>
|
|
3523
|
+
Your session has expired. Please log in again to continue.
|
|
3524
|
+
<Button
|
|
3525
|
+
variant="outline"
|
|
3526
|
+
size="sm"
|
|
3527
|
+
className={styles.loginButton}
|
|
3528
|
+
onClick={() => toast.info("Redirecting to login...")}
|
|
3529
|
+
>
|
|
3530
|
+
Log In
|
|
3531
|
+
</Button>
|
|
3532
|
+
</AlertDescription>
|
|
3533
|
+
</Alert>
|
|
3534
|
+
</div>
|
|
3535
|
+
</div>
|
|
3536
|
+
</div>
|
|
3537
|
+
);
|
|
3538
|
+
}
|
|
3539
|
+
|
|
3540
|
+
// React 19 use() hook polyfill (remove when React 19 is stable)
|
|
3541
|
+
function use<T>(promise: Promise<T>): T {
|
|
3542
|
+
if ((promise as any).status === "fulfilled") {
|
|
3543
|
+
return (promise as any).value;
|
|
3544
|
+
} else if ((promise as any).status === "rejected") {
|
|
3545
|
+
throw (promise as any).reason;
|
|
3546
|
+
} else {
|
|
3547
|
+
throw promise.then(
|
|
3548
|
+
(value) => {
|
|
3549
|
+
(promise as any).status = "fulfilled";
|
|
3550
|
+
(promise as any).value = value;
|
|
3551
|
+
},
|
|
3552
|
+
(reason) => {
|
|
3553
|
+
(promise as any).status = "rejected";
|
|
3554
|
+
(promise as any).reason = reason;
|
|
3555
|
+
}
|
|
3556
|
+
);
|
|
3557
|
+
}
|
|
3558
|
+
}
|
|
3559
|
+
```
|
|
3560
|
+
|
|
3561
|
+
```css
|
|
3562
|
+
/* error-handling.module.css */
|
|
3563
|
+
.container {
|
|
3564
|
+
max-width: 56rem;
|
|
3565
|
+
margin-inline: auto;
|
|
3566
|
+
padding: 2rem 1rem;
|
|
3567
|
+
}
|
|
3568
|
+
|
|
3569
|
+
.header {
|
|
3570
|
+
margin-bottom: 2rem;
|
|
3571
|
+
}
|
|
3572
|
+
|
|
3573
|
+
.title {
|
|
3574
|
+
font-size: 2rem;
|
|
3575
|
+
font-weight: 700;
|
|
3576
|
+
}
|
|
3577
|
+
|
|
3578
|
+
.subtitle {
|
|
3579
|
+
margin-top: 0.5rem;
|
|
3580
|
+
color: var(--ac-muted-foreground);
|
|
3581
|
+
}
|
|
3582
|
+
|
|
3583
|
+
.grid {
|
|
3584
|
+
display: grid;
|
|
3585
|
+
gap: 2rem;
|
|
3586
|
+
}
|
|
3587
|
+
|
|
3588
|
+
.section {
|
|
3589
|
+
display: grid;
|
|
3590
|
+
gap: 1rem;
|
|
3591
|
+
}
|
|
3592
|
+
|
|
3593
|
+
.sectionTitle {
|
|
3594
|
+
font-size: 1.25rem;
|
|
3595
|
+
font-weight: 600;
|
|
3596
|
+
}
|
|
3597
|
+
|
|
3598
|
+
.profile {
|
|
3599
|
+
display: grid;
|
|
3600
|
+
gap: 0.75rem;
|
|
3601
|
+
}
|
|
3602
|
+
|
|
3603
|
+
.profileField {
|
|
3604
|
+
display: flex;
|
|
3605
|
+
gap: 0.5rem;
|
|
3606
|
+
}
|
|
3607
|
+
|
|
3608
|
+
.profileLabel {
|
|
3609
|
+
font-weight: 600;
|
|
3610
|
+
}
|
|
3611
|
+
|
|
3612
|
+
.skeletonTitle {
|
|
3613
|
+
height: 1.5rem;
|
|
3614
|
+
width: 8rem;
|
|
3615
|
+
}
|
|
3616
|
+
|
|
3617
|
+
.skeletonContent {
|
|
3618
|
+
display: grid;
|
|
3619
|
+
gap: 0.75rem;
|
|
3620
|
+
}
|
|
3621
|
+
|
|
3622
|
+
.skeletonField {
|
|
3623
|
+
height: 1rem;
|
|
3624
|
+
}
|
|
3625
|
+
|
|
3626
|
+
.errorDescription {
|
|
3627
|
+
display: flex;
|
|
3628
|
+
align-items: center;
|
|
3629
|
+
gap: 1rem;
|
|
3630
|
+
margin-top: 0.5rem;
|
|
3631
|
+
}
|
|
3632
|
+
|
|
3633
|
+
.retryButton,
|
|
3634
|
+
.loginButton {
|
|
3635
|
+
margin-left: auto;
|
|
3636
|
+
}
|
|
3637
|
+
|
|
3638
|
+
.buttonGroup {
|
|
3639
|
+
display: grid;
|
|
3640
|
+
gap: 0.5rem;
|
|
3641
|
+
padding: 1.5rem;
|
|
3642
|
+
}
|
|
3643
|
+
```
|
|
3644
|
+
|
|
3645
|
+
---
|
|
3646
|
+
|
|
3647
|
+
## 🎓 Key Takeaways from Pattern Recipes
|
|
3648
|
+
|
|
3649
|
+
### ✅ Best Practices Demonstrated
|
|
3650
|
+
|
|
3651
|
+
1. **Form Validation**: Always use `zod` + `react-hook-form` for type-safe validation
|
|
3652
|
+
2. **Error Handling**: Combine boundaries, inline errors, and toast notifications
|
|
3653
|
+
3. **Loading States**: Use `Skeleton` components and `AsyncBoundary` for async data
|
|
3654
|
+
4. **Accessibility**: Keyboard navigation, ARIA attributes, and semantic HTML
|
|
3655
|
+
5. **Responsive Design**: CSS Modules with container queries and media queries
|
|
3656
|
+
6. **Type Safety**: Leverage namespace types (`Component.Props`, `Component.State`)
|
|
3657
|
+
7. **Composition**: Use `render` prop for element composition (Base UI pattern)
|
|
3658
|
+
8. **Toast Notifications**: Use `toast.promise()` for async operations
|
|
3659
|
+
9. **State Management**: Keep form state close to components, lift when needed
|
|
3660
|
+
10. **Progressive Enhancement**: Start with semantic HTML, enhance with JavaScript
|
|
3661
|
+
|
|
3662
|
+
### 📚 Additional Resources
|
|
3663
|
+
|
|
3664
|
+
- [Base UI Documentation](https://base-ui.com/react/components)
|
|
3665
|
+
- [React Hook Form](https://react-hook-form.com/)
|
|
3666
|
+
- [Zod Validation](https://zod.dev/)
|
|
3667
|
+
- [TanStack Table](https://tanstack.com/table)
|
|
3668
|
+
- [Date-fns](https://date-fns.org/)
|
|
3669
|
+
|
|
1035
3670
|
Ready to build something amazing? **[🚀 Start with our Quick Start Guide](./README.md#-quick-start)**
|