From b740ac0950462cb1d58e73f7f26604bc9c3f72f1 Mon Sep 17 00:00:00 2001 From: Rajat Sandeep <93584596+rajatsandeepsen@users.noreply.github.com> Date: Thu, 25 Jan 2024 19:32:36 +0530 Subject: [PATCH] feat: added shadcn ui in native --- .vscode/project.code-workspace | 22 +- .vscode/settings.json | 4 +- apps/native/babel.config.js | 2 +- apps/native/package.json | 9 +- apps/native/src/app/_layout.tsx | 15 +- apps/native/src/app/index.tsx | 135 +--- apps/native/src/components/DrawerToggle.tsx | 31 + apps/native/src/components/ThemeToggle.tsx | 40 + apps/native/src/components/ui/accordion.tsx | 205 +++++ .../native/src/components/ui/alert-dialog.tsx | 300 +++++++ apps/native/src/components/ui/alert.tsx | 129 +++ apps/native/src/components/ui/avatar.tsx | 69 ++ apps/native/src/components/ui/badge.tsx | 62 ++ .../src/components/ui/bottom-sheet.native.tsx | 356 ++++++++ .../native/src/components/ui/bottom-sheet.tsx | 257 ++++++ apps/native/src/components/ui/button.tsx | 139 ++++ apps/native/src/components/ui/calendar.tsx | 147 ++++ apps/native/src/components/ui/card.tsx | 126 +++ apps/native/src/components/ui/checkbox.tsx | 51 ++ apps/native/src/components/ui/collapsible.tsx | 150 ++++ apps/native/src/components/ui/combobox.tsx | 229 ++++++ apps/native/src/components/ui/command.tsx | 444 ++++++++++ .../native/src/components/ui/context-menu.tsx | 5 + apps/native/src/components/ui/data-table.tsx | 152 ++++ apps/native/src/components/ui/dialog.tsx | 276 +++++++ .../src/components/ui/dropdown-menu.tsx | 5 + apps/native/src/components/ui/form.tsx | 684 ++++++++++++++++ apps/native/src/components/ui/input.tsx | 25 + apps/native/src/components/ui/label.tsx | 33 + apps/native/src/components/ui/menubar.tsx | 5 + apps/native/src/components/ui/popover.tsx | 314 ++++++++ apps/native/src/components/ui/progress.tsx | 56 ++ apps/native/src/components/ui/radio-group.tsx | 130 +++ .../native/src/components/ui/section-list.tsx | 127 +++ apps/native/src/components/ui/select.tsx | 216 +++++ apps/native/src/components/ui/separator.tsx | 21 + apps/native/src/components/ui/skeleton.tsx | 32 + apps/native/src/components/ui/slider.tsx | 31 + apps/native/src/components/ui/switch.tsx | 35 + apps/native/src/components/ui/table.tsx | 210 +++++ apps/native/src/components/ui/tabs.tsx | 173 ++++ apps/native/src/components/ui/textarea.tsx | 41 + apps/native/src/components/ui/toast.tsx | 51 ++ .../native/src/components/ui/toggle-group.tsx | 187 +++++ apps/native/src/components/ui/toggle.tsx | 98 +++ .../src/components/universal-ui/accordion.tsx | 130 +++ .../components/universal-ui/alert-dialog.tsx | 191 +++++ .../src/components/universal-ui/alert.tsx | 105 +++ .../components/universal-ui/aspect-ratio.tsx | 5 + .../src/components/universal-ui/avatar.tsx | 48 ++ .../src/components/universal-ui/badge.tsx | 58 ++ .../src/components/universal-ui/button.tsx | 96 +++ .../src/components/universal-ui/card.tsx | 92 +++ .../src/components/universal-ui/checkbox.tsx | 36 + .../components/universal-ui/collapsible.tsx | 9 + .../components/universal-ui/context-menu.tsx | 258 ++++++ .../src/components/universal-ui/dialog.tsx | 172 ++++ .../components/universal-ui/dropdown-menu.tsx | 260 ++++++ .../components/universal-ui/hover-card.tsx | 45 ++ .../src/components/universal-ui/input.tsx | 26 + .../src/components/universal-ui/label.tsx | 35 + .../src/components/universal-ui/menubar.tsx | 269 +++++++ .../universal-ui/navigation-menu.tsx | 186 +++++ .../src/components/universal-ui/popover.tsx | 41 + .../src/components/universal-ui/progress.tsx | 65 ++ .../components/universal-ui/radio-group.tsx | 42 + .../src/components/universal-ui/select.tsx | 194 +++++ .../src/components/universal-ui/separator.tsx | 28 + .../src/components/universal-ui/tabs.tsx | 65 ++ .../src/components/universal-ui/textarea.tsx | 40 + .../components/universal-ui/toggle-group.tsx | 97 +++ .../src/components/universal-ui/toggle.tsx | 95 +++ .../src/components/universal-ui/tooltip.tsx | 38 + .../components/universal-ui/typography.tsx | 257 ++++++ apps/native/src/lib/android-navigation-bar.ts | 11 + apps/native/src/lib/constants.ts | 64 ++ apps/native/src/lib/keyboard.tsx | 34 + apps/native/src/lib/rn-primitives/README.md | 15 + .../accordion/accordion-native.tsx | 240 ++++++ .../rn-primitives/accordion/accordion-web.tsx | 282 +++++++ .../src/lib/rn-primitives/accordion/index.ts | 38 + .../src/lib/rn-primitives/accordion/types.ts | 44 + .../alert-dialog/alert-dialog-native.tsx | 261 ++++++ .../alert-dialog/alert-dialog-web.tsx | 277 +++++++ .../lib/rn-primitives/alert-dialog/index.ts | 53 ++ .../lib/rn-primitives/alert-dialog/types.ts | 42 + .../aspect-ratio/aspect-ratio-native.tsx | 19 + .../lib/rn-primitives/aspect-ratio/index.ts | 1 + .../lib/rn-primitives/aspect-ratio/types.ts | 8 + .../rn-primitives/avatar/avatar-native.tsx | 140 ++++ .../src/lib/rn-primitives/avatar/index.ts | 1 + .../src/lib/rn-primitives/avatar/types.ts | 10 + .../checkbox/checkbox-native.tsx | 112 +++ .../rn-primitives/checkbox/checkbox-web.tsx | 131 +++ .../src/lib/rn-primitives/checkbox/index.ts | 13 + .../src/lib/rn-primitives/checkbox/types.ts | 11 + .../collapsible/collapsible-native.tsx | 111 +++ .../collapsible/collapsible-web.tsx | 152 ++++ .../lib/rn-primitives/collapsible/index.ts | 18 + .../lib/rn-primitives/collapsible/types.ts | 11 + .../context-menu/context-menu-native.tsx | 745 +++++++++++++++++ .../context-menu/context-menu-web.tsx | 546 +++++++++++++ .../lib/rn-primitives/context-menu/index.ts | 93 +++ .../lib/rn-primitives/context-menu/types.ts | 79 ++ .../rn-primitives/dialog/dialog-native.tsx | 244 ++++++ .../lib/rn-primitives/dialog/dialog-web.tsx | 225 ++++++ .../src/lib/rn-primitives/dialog/index.ts | 48 ++ .../src/lib/rn-primitives/dialog/types.ts | 53 ++ .../dropdown-menu/dropdown-menu-native.tsx | 706 ++++++++++++++++ .../dropdown-menu/dropdown-menu-web.tsx | 555 +++++++++++++ .../lib/rn-primitives/dropdown-menu/index.ts | 93 +++ .../lib/rn-primitives/dropdown-menu/types.ts | 71 ++ .../rn-primitives/hooks/useAugmentedRef.tsx | 30 + .../hooks/useRelativePosition.tsx | 235 ++++++ .../lib/rn-primitives/hooks/useTrigger.tsx | 59 ++ .../hover-card/hover-card-native.tsx | 10 + .../hover-card/hover-card-web.tsx | 131 +++ .../src/lib/rn-primitives/hover-card/index.ts | 33 + .../src/lib/rn-primitives/hover-card/types.ts | 24 + .../src/lib/rn-primitives/label/index.ts | 13 + .../lib/rn-primitives/label/label-native.tsx | 31 + .../src/lib/rn-primitives/label/label-web.tsx | 36 + .../src/lib/rn-primitives/label/types.ts | 15 + .../src/lib/rn-primitives/menubar/index.ts | 102 +++ .../rn-primitives/menubar/menubar-native.tsx | 757 ++++++++++++++++++ .../lib/rn-primitives/menubar/menubar-web.tsx | 577 +++++++++++++ .../src/lib/rn-primitives/menubar/types.ts | 76 ++ .../rn-primitives/navigation-menu/index.ts | 58 ++ .../navigation-menu-native.tsx | 369 +++++++++ .../navigation-menu/navigation-menu-web.tsx | 297 +++++++ .../rn-primitives/navigation-menu/types.ts | 49 ++ .../src/lib/rn-primitives/popover/index.ts | 38 + .../rn-primitives/popover/popover-native.tsx | 352 ++++++++ .../lib/rn-primitives/popover/popover-web.tsx | 187 +++++ .../src/lib/rn-primitives/popover/types.ts | 24 + .../src/lib/rn-primitives/portal/index.ts | 1 + .../rn-primitives/portal/portal-native.tsx | 67 ++ .../src/lib/rn-primitives/progress/index.ts | 13 + .../progress/progress-native.tsx | 69 ++ .../rn-primitives/progress/progress-web.tsx | 43 + .../src/lib/rn-primitives/progress/types.ts | 7 + .../lib/rn-primitives/radio-group/index.ts | 17 + .../radio-group/radio-group-native.tsx | 127 +++ .../radio-group/radio-group-web.tsx | 86 ++ .../lib/rn-primitives/radio-group/types.ts | 15 + .../src/lib/rn-primitives/select/index.ts | 90 +++ .../rn-primitives/select/select-native.tsx | 526 ++++++++++++ .../lib/rn-primitives/select/select-web.tsx | 317 ++++++++ .../src/lib/rn-primitives/select/types.ts | 76 ++ .../src/lib/rn-primitives/separator/index.ts | 1 + .../separator/separator-native.tsx | 23 + .../src/lib/rn-primitives/separator/types.ts | 6 + .../src/lib/rn-primitives/slider/index.ts | 24 + .../rn-primitives/slider/slider-native.tsx | 86 ++ .../lib/rn-primitives/slider/slider-web.tsx | 84 ++ .../src/lib/rn-primitives/slider/types.ts | 24 + .../src/lib/rn-primitives/slot/index.ts | 1 + .../lib/rn-primitives/slot/slot-native.tsx | 205 +++++ .../src/lib/rn-primitives/switch/index.ts | 14 + .../rn-primitives/switch/switch-native.tsx | 65 ++ .../lib/rn-primitives/switch/switch-web.tsx | 77 ++ .../src/lib/rn-primitives/switch/types.ts | 11 + .../src/lib/rn-primitives/table/index.ts | 1 + .../lib/rn-primitives/table/table-native.tsx | 67 ++ .../src/lib/rn-primitives/tabs/index.ts | 34 + .../lib/rn-primitives/tabs/tabs-native.tsx | 147 ++++ .../src/lib/rn-primitives/tabs/tabs-web.tsx | 117 +++ .../src/lib/rn-primitives/tabs/types.ts | 24 + .../src/lib/rn-primitives/toast/index.ts | 1 + .../lib/rn-primitives/toast/toast-native.tsx | 156 ++++ .../src/lib/rn-primitives/toast/types.ts | 8 + .../lib/rn-primitives/toggle-group/index.ts | 27 + .../toggle-group/toggle-group-native.tsx | 141 ++++ .../toggle-group/toggle-group-web.tsx | 136 ++++ .../lib/rn-primitives/toggle-group/types.ts | 37 + .../src/lib/rn-primitives/toggle/index.ts | 9 + .../rn-primitives/toggle/toggle-native.tsx | 50 ++ .../lib/rn-primitives/toggle/toggle-web.tsx | 50 ++ .../src/lib/rn-primitives/toggle/types.ts | 7 + .../src/lib/rn-primitives/toolbar/index.ts | 30 + .../rn-primitives/toolbar/toolbar-native.tsx | 155 ++++ .../lib/rn-primitives/toolbar/toolbar-web.tsx | 152 ++++ .../src/lib/rn-primitives/toolbar/types.ts | 39 + .../src/lib/rn-primitives/tooltip/index.ts | 28 + .../rn-primitives/tooltip/tooltip-native.tsx | 329 ++++++++ .../lib/rn-primitives/tooltip/tooltip-web.tsx | 172 ++++ .../src/lib/rn-primitives/tooltip/types.ts | 38 + apps/native/src/lib/rn-primitives/types.ts | 109 +++ .../src/lib/rn-primitives/utils/index.ts | 67 ++ apps/native/src/lib/useAugmentedRef.tsx | 29 + apps/native/src/lib/utils.ts | 15 + apps/native/src/styles.css | 73 ++ apps/native/tailwind.config.ts | 4 +- apps/web/src/env.mjs | 68 +- biome.json | 15 + pnpm-lock.yaml | 342 +++++++- tooling/tailwind/index.ts | 76 +- tooling/tailwind/package.json | 3 +- tooling/typescript/base.json | 17 +- 199 files changed, 22493 insertions(+), 227 deletions(-) create mode 100644 apps/native/src/components/DrawerToggle.tsx create mode 100644 apps/native/src/components/ThemeToggle.tsx create mode 100644 apps/native/src/components/ui/accordion.tsx create mode 100644 apps/native/src/components/ui/alert-dialog.tsx create mode 100644 apps/native/src/components/ui/alert.tsx create mode 100644 apps/native/src/components/ui/avatar.tsx create mode 100644 apps/native/src/components/ui/badge.tsx create mode 100644 apps/native/src/components/ui/bottom-sheet.native.tsx create mode 100644 apps/native/src/components/ui/bottom-sheet.tsx create mode 100644 apps/native/src/components/ui/button.tsx create mode 100644 apps/native/src/components/ui/calendar.tsx create mode 100644 apps/native/src/components/ui/card.tsx create mode 100644 apps/native/src/components/ui/checkbox.tsx create mode 100644 apps/native/src/components/ui/collapsible.tsx create mode 100644 apps/native/src/components/ui/combobox.tsx create mode 100644 apps/native/src/components/ui/command.tsx create mode 100644 apps/native/src/components/ui/context-menu.tsx create mode 100644 apps/native/src/components/ui/data-table.tsx create mode 100644 apps/native/src/components/ui/dialog.tsx create mode 100644 apps/native/src/components/ui/dropdown-menu.tsx create mode 100644 apps/native/src/components/ui/form.tsx create mode 100644 apps/native/src/components/ui/input.tsx create mode 100644 apps/native/src/components/ui/label.tsx create mode 100644 apps/native/src/components/ui/menubar.tsx create mode 100644 apps/native/src/components/ui/popover.tsx create mode 100644 apps/native/src/components/ui/progress.tsx create mode 100644 apps/native/src/components/ui/radio-group.tsx create mode 100644 apps/native/src/components/ui/section-list.tsx create mode 100644 apps/native/src/components/ui/select.tsx create mode 100644 apps/native/src/components/ui/separator.tsx create mode 100644 apps/native/src/components/ui/skeleton.tsx create mode 100644 apps/native/src/components/ui/slider.tsx create mode 100644 apps/native/src/components/ui/switch.tsx create mode 100644 apps/native/src/components/ui/table.tsx create mode 100644 apps/native/src/components/ui/tabs.tsx create mode 100644 apps/native/src/components/ui/textarea.tsx create mode 100644 apps/native/src/components/ui/toast.tsx create mode 100644 apps/native/src/components/ui/toggle-group.tsx create mode 100644 apps/native/src/components/ui/toggle.tsx create mode 100644 apps/native/src/components/universal-ui/accordion.tsx create mode 100644 apps/native/src/components/universal-ui/alert-dialog.tsx create mode 100644 apps/native/src/components/universal-ui/alert.tsx create mode 100644 apps/native/src/components/universal-ui/aspect-ratio.tsx create mode 100644 apps/native/src/components/universal-ui/avatar.tsx create mode 100644 apps/native/src/components/universal-ui/badge.tsx create mode 100644 apps/native/src/components/universal-ui/button.tsx create mode 100644 apps/native/src/components/universal-ui/card.tsx create mode 100644 apps/native/src/components/universal-ui/checkbox.tsx create mode 100644 apps/native/src/components/universal-ui/collapsible.tsx create mode 100644 apps/native/src/components/universal-ui/context-menu.tsx create mode 100644 apps/native/src/components/universal-ui/dialog.tsx create mode 100644 apps/native/src/components/universal-ui/dropdown-menu.tsx create mode 100644 apps/native/src/components/universal-ui/hover-card.tsx create mode 100644 apps/native/src/components/universal-ui/input.tsx create mode 100644 apps/native/src/components/universal-ui/label.tsx create mode 100644 apps/native/src/components/universal-ui/menubar.tsx create mode 100644 apps/native/src/components/universal-ui/navigation-menu.tsx create mode 100644 apps/native/src/components/universal-ui/popover.tsx create mode 100644 apps/native/src/components/universal-ui/progress.tsx create mode 100644 apps/native/src/components/universal-ui/radio-group.tsx create mode 100644 apps/native/src/components/universal-ui/select.tsx create mode 100644 apps/native/src/components/universal-ui/separator.tsx create mode 100644 apps/native/src/components/universal-ui/tabs.tsx create mode 100644 apps/native/src/components/universal-ui/textarea.tsx create mode 100644 apps/native/src/components/universal-ui/toggle-group.tsx create mode 100644 apps/native/src/components/universal-ui/toggle.tsx create mode 100644 apps/native/src/components/universal-ui/tooltip.tsx create mode 100644 apps/native/src/components/universal-ui/typography.tsx create mode 100644 apps/native/src/lib/android-navigation-bar.ts create mode 100644 apps/native/src/lib/constants.ts create mode 100644 apps/native/src/lib/keyboard.tsx create mode 100644 apps/native/src/lib/rn-primitives/README.md create mode 100644 apps/native/src/lib/rn-primitives/accordion/accordion-native.tsx create mode 100644 apps/native/src/lib/rn-primitives/accordion/accordion-web.tsx create mode 100644 apps/native/src/lib/rn-primitives/accordion/index.ts create mode 100644 apps/native/src/lib/rn-primitives/accordion/types.ts create mode 100644 apps/native/src/lib/rn-primitives/alert-dialog/alert-dialog-native.tsx create mode 100644 apps/native/src/lib/rn-primitives/alert-dialog/alert-dialog-web.tsx create mode 100644 apps/native/src/lib/rn-primitives/alert-dialog/index.ts create mode 100644 apps/native/src/lib/rn-primitives/alert-dialog/types.ts create mode 100644 apps/native/src/lib/rn-primitives/aspect-ratio/aspect-ratio-native.tsx create mode 100644 apps/native/src/lib/rn-primitives/aspect-ratio/index.ts create mode 100644 apps/native/src/lib/rn-primitives/aspect-ratio/types.ts create mode 100644 apps/native/src/lib/rn-primitives/avatar/avatar-native.tsx create mode 100644 apps/native/src/lib/rn-primitives/avatar/index.ts create mode 100644 apps/native/src/lib/rn-primitives/avatar/types.ts create mode 100644 apps/native/src/lib/rn-primitives/checkbox/checkbox-native.tsx create mode 100644 apps/native/src/lib/rn-primitives/checkbox/checkbox-web.tsx create mode 100644 apps/native/src/lib/rn-primitives/checkbox/index.ts create mode 100644 apps/native/src/lib/rn-primitives/checkbox/types.ts create mode 100644 apps/native/src/lib/rn-primitives/collapsible/collapsible-native.tsx create mode 100644 apps/native/src/lib/rn-primitives/collapsible/collapsible-web.tsx create mode 100644 apps/native/src/lib/rn-primitives/collapsible/index.ts create mode 100644 apps/native/src/lib/rn-primitives/collapsible/types.ts create mode 100644 apps/native/src/lib/rn-primitives/context-menu/context-menu-native.tsx create mode 100644 apps/native/src/lib/rn-primitives/context-menu/context-menu-web.tsx create mode 100644 apps/native/src/lib/rn-primitives/context-menu/index.ts create mode 100644 apps/native/src/lib/rn-primitives/context-menu/types.ts create mode 100644 apps/native/src/lib/rn-primitives/dialog/dialog-native.tsx create mode 100644 apps/native/src/lib/rn-primitives/dialog/dialog-web.tsx create mode 100644 apps/native/src/lib/rn-primitives/dialog/index.ts create mode 100644 apps/native/src/lib/rn-primitives/dialog/types.ts create mode 100644 apps/native/src/lib/rn-primitives/dropdown-menu/dropdown-menu-native.tsx create mode 100644 apps/native/src/lib/rn-primitives/dropdown-menu/dropdown-menu-web.tsx create mode 100644 apps/native/src/lib/rn-primitives/dropdown-menu/index.ts create mode 100644 apps/native/src/lib/rn-primitives/dropdown-menu/types.ts create mode 100644 apps/native/src/lib/rn-primitives/hooks/useAugmentedRef.tsx create mode 100644 apps/native/src/lib/rn-primitives/hooks/useRelativePosition.tsx create mode 100644 apps/native/src/lib/rn-primitives/hooks/useTrigger.tsx create mode 100644 apps/native/src/lib/rn-primitives/hover-card/hover-card-native.tsx create mode 100644 apps/native/src/lib/rn-primitives/hover-card/hover-card-web.tsx create mode 100644 apps/native/src/lib/rn-primitives/hover-card/index.ts create mode 100644 apps/native/src/lib/rn-primitives/hover-card/types.ts create mode 100644 apps/native/src/lib/rn-primitives/label/index.ts create mode 100644 apps/native/src/lib/rn-primitives/label/label-native.tsx create mode 100644 apps/native/src/lib/rn-primitives/label/label-web.tsx create mode 100644 apps/native/src/lib/rn-primitives/label/types.ts create mode 100644 apps/native/src/lib/rn-primitives/menubar/index.ts create mode 100644 apps/native/src/lib/rn-primitives/menubar/menubar-native.tsx create mode 100644 apps/native/src/lib/rn-primitives/menubar/menubar-web.tsx create mode 100644 apps/native/src/lib/rn-primitives/menubar/types.ts create mode 100644 apps/native/src/lib/rn-primitives/navigation-menu/index.ts create mode 100644 apps/native/src/lib/rn-primitives/navigation-menu/navigation-menu-native.tsx create mode 100644 apps/native/src/lib/rn-primitives/navigation-menu/navigation-menu-web.tsx create mode 100644 apps/native/src/lib/rn-primitives/navigation-menu/types.ts create mode 100644 apps/native/src/lib/rn-primitives/popover/index.ts create mode 100644 apps/native/src/lib/rn-primitives/popover/popover-native.tsx create mode 100644 apps/native/src/lib/rn-primitives/popover/popover-web.tsx create mode 100644 apps/native/src/lib/rn-primitives/popover/types.ts create mode 100644 apps/native/src/lib/rn-primitives/portal/index.ts create mode 100644 apps/native/src/lib/rn-primitives/portal/portal-native.tsx create mode 100644 apps/native/src/lib/rn-primitives/progress/index.ts create mode 100644 apps/native/src/lib/rn-primitives/progress/progress-native.tsx create mode 100644 apps/native/src/lib/rn-primitives/progress/progress-web.tsx create mode 100644 apps/native/src/lib/rn-primitives/progress/types.ts create mode 100644 apps/native/src/lib/rn-primitives/radio-group/index.ts create mode 100644 apps/native/src/lib/rn-primitives/radio-group/radio-group-native.tsx create mode 100644 apps/native/src/lib/rn-primitives/radio-group/radio-group-web.tsx create mode 100644 apps/native/src/lib/rn-primitives/radio-group/types.ts create mode 100644 apps/native/src/lib/rn-primitives/select/index.ts create mode 100644 apps/native/src/lib/rn-primitives/select/select-native.tsx create mode 100644 apps/native/src/lib/rn-primitives/select/select-web.tsx create mode 100644 apps/native/src/lib/rn-primitives/select/types.ts create mode 100644 apps/native/src/lib/rn-primitives/separator/index.ts create mode 100644 apps/native/src/lib/rn-primitives/separator/separator-native.tsx create mode 100644 apps/native/src/lib/rn-primitives/separator/types.ts create mode 100644 apps/native/src/lib/rn-primitives/slider/index.ts create mode 100644 apps/native/src/lib/rn-primitives/slider/slider-native.tsx create mode 100644 apps/native/src/lib/rn-primitives/slider/slider-web.tsx create mode 100644 apps/native/src/lib/rn-primitives/slider/types.ts create mode 100644 apps/native/src/lib/rn-primitives/slot/index.ts create mode 100644 apps/native/src/lib/rn-primitives/slot/slot-native.tsx create mode 100644 apps/native/src/lib/rn-primitives/switch/index.ts create mode 100644 apps/native/src/lib/rn-primitives/switch/switch-native.tsx create mode 100644 apps/native/src/lib/rn-primitives/switch/switch-web.tsx create mode 100644 apps/native/src/lib/rn-primitives/switch/types.ts create mode 100644 apps/native/src/lib/rn-primitives/table/index.ts create mode 100644 apps/native/src/lib/rn-primitives/table/table-native.tsx create mode 100644 apps/native/src/lib/rn-primitives/tabs/index.ts create mode 100644 apps/native/src/lib/rn-primitives/tabs/tabs-native.tsx create mode 100644 apps/native/src/lib/rn-primitives/tabs/tabs-web.tsx create mode 100644 apps/native/src/lib/rn-primitives/tabs/types.ts create mode 100644 apps/native/src/lib/rn-primitives/toast/index.ts create mode 100644 apps/native/src/lib/rn-primitives/toast/toast-native.tsx create mode 100644 apps/native/src/lib/rn-primitives/toast/types.ts create mode 100644 apps/native/src/lib/rn-primitives/toggle-group/index.ts create mode 100644 apps/native/src/lib/rn-primitives/toggle-group/toggle-group-native.tsx create mode 100644 apps/native/src/lib/rn-primitives/toggle-group/toggle-group-web.tsx create mode 100644 apps/native/src/lib/rn-primitives/toggle-group/types.ts create mode 100644 apps/native/src/lib/rn-primitives/toggle/index.ts create mode 100644 apps/native/src/lib/rn-primitives/toggle/toggle-native.tsx create mode 100644 apps/native/src/lib/rn-primitives/toggle/toggle-web.tsx create mode 100644 apps/native/src/lib/rn-primitives/toggle/types.ts create mode 100644 apps/native/src/lib/rn-primitives/toolbar/index.ts create mode 100644 apps/native/src/lib/rn-primitives/toolbar/toolbar-native.tsx create mode 100644 apps/native/src/lib/rn-primitives/toolbar/toolbar-web.tsx create mode 100644 apps/native/src/lib/rn-primitives/toolbar/types.ts create mode 100644 apps/native/src/lib/rn-primitives/tooltip/index.ts create mode 100644 apps/native/src/lib/rn-primitives/tooltip/tooltip-native.tsx create mode 100644 apps/native/src/lib/rn-primitives/tooltip/tooltip-web.tsx create mode 100644 apps/native/src/lib/rn-primitives/tooltip/types.ts create mode 100644 apps/native/src/lib/rn-primitives/types.ts create mode 100644 apps/native/src/lib/rn-primitives/utils/index.ts create mode 100644 apps/native/src/lib/useAugmentedRef.tsx create mode 100644 apps/native/src/lib/utils.ts create mode 100644 biome.json diff --git a/.vscode/project.code-workspace b/.vscode/project.code-workspace index 3a6a940..9f08745 100644 --- a/.vscode/project.code-workspace +++ b/.vscode/project.code-workspace @@ -2,7 +2,7 @@ "folders": [ { "path": "..", - "name": "root", + "name": "root" }, { "name": "native", @@ -12,5 +12,25 @@ "name": "web", "path": "../apps/web/" }, + { + "name": "auth-proxy", + "path": "../apps/auth-proxy/" + }, + { + "name": "api", + "path": "../packages/api/" + }, + { + "name": "db", + "path": "../packages/db/" + }, + { + "name": "auth", + "path": "../packages/auth/" + }, + { + "name": "tooling", + "path": "../tooling/" + } ] } \ No newline at end of file diff --git a/.vscode/settings.json b/.vscode/settings.json index 729802a..b4c2709 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,5 +1,7 @@ { - "editor.codeActionsOnSave": { "source.fixAll.eslint": true }, + "editor.codeActionsOnSave": { + "source.fixAll.eslint": "explicit" + }, "editor.defaultFormatter": "esbenp.prettier-vscode", "editor.formatOnSave": true, "eslint.rules.customizations": [{ "rule": "*", "severity": "warn" }], diff --git a/apps/native/babel.config.js b/apps/native/babel.config.js index 143cb08..64a3aba 100644 --- a/apps/native/babel.config.js +++ b/apps/native/babel.config.js @@ -1,5 +1,5 @@ /** @type {import("@babel/core").ConfigFunction} */ -module.exports = function (api) { +module.exports = (api) => { api.cache.forever(); return { diff --git a/apps/native/package.json b/apps/native/package.json index 139bbdb..2cd0ef2 100644 --- a/apps/native/package.json +++ b/apps/native/package.json @@ -5,7 +5,7 @@ "main": "expo-router/entry", "scripts": { "clean": "git clean -xdf .expo .turbo node_modules", - "dev": "expo start --ios", + "dev": "expo start --go", "dev:android": "expo start --android", "dev:ios": "expo start --ios", "lint": "eslint .", @@ -21,6 +21,8 @@ "@trpc/client": "next", "@trpc/react-query": "next", "@trpc/server": "next", + "class-variance-authority": "^0.7.0", + "clsx": "^2.1.0", "expo": "^49.0.18", "expo-constants": "~14.4.2", "expo-linking": "~5.0.2", @@ -35,7 +37,8 @@ "react-native-reanimated": "~3.3.0", "react-native-safe-area-context": "4.6.3", "react-native-screens": "~3.22.1", - "superjson": "2.2.0" + "superjson": "2.2.0", + "tailwind-merge": "^2.2.1" }, "devDependencies": { "@acme/api": "workspace:^0.1.0", @@ -65,4 +68,4 @@ ] }, "prettier": "@acme/prettier-config" -} +} \ No newline at end of file diff --git a/apps/native/src/app/_layout.tsx b/apps/native/src/app/_layout.tsx index 74a51d0..4af7c90 100644 --- a/apps/native/src/app/_layout.tsx +++ b/apps/native/src/app/_layout.tsx @@ -6,23 +6,10 @@ import { TRPCProvider } from "~/utils/api"; import "../styles.css"; -// This is the main layout of the app -// It wraps your pages with the providers they need const RootLayout = () => { return ( - {/* - The Stack component displays the current page. - It also allows you to configure your screens - */} - - + ); }; diff --git a/apps/native/src/app/index.tsx b/apps/native/src/app/index.tsx index 3945068..1ca63ce 100644 --- a/apps/native/src/app/index.tsx +++ b/apps/native/src/app/index.tsx @@ -1,144 +1,25 @@ import React from "react"; -import { Button, Pressable, Text, TextInput, View } from "react-native"; +import { Text, View } from "react-native"; import { SafeAreaView } from "react-native-safe-area-context"; -import { Link, Stack } from "expo-router"; -import { FlashList } from "@shopify/flash-list"; +import { Button } from "~/components/ui/button"; +import { Card } from "~/components/ui/card"; -import { api } from "~/utils/api"; -import type { RouterOutputs } from "~/utils/api"; - -function PostCard(props: { - post: RouterOutputs["post"]["all"][number]; - onDelete: () => void; -}) { - return ( - - - - - - {props.post.title} - - {props.post.content} - - - - - Delete - - - ); -} - -function CreatePost() { - const utils = api.useContext(); - - const [title, setTitle] = React.useState(""); - const [content, setContent] = React.useState(""); - - const { mutate, error } = api.post.create.useMutation({ - async onSuccess() { - setTitle(""); - setContent(""); - await utils.post.all.invalidate(); - }, - }); - - return ( - - - {error?.data?.zodError?.fieldErrors.title && ( - - {error.data.zodError.fieldErrors.title} - - )} - - {error?.data?.zodError?.fieldErrors.content && ( - - {error.data.zodError.fieldErrors.content} - - )} - { - mutate({ - title, - content, - }); - }} - > - Publish post - - {error?.data?.code === "UNAUTHORIZED" && ( - - You need to be logged in to create a post - - )} - - ); -} const Index = () => { - const utils = api.useContext(); - - const postQuery = api.post.all.useQuery(); - - const deletePostMutation = api.post.delete.useMutation({ - onSettled: () => utils.post.all.invalidate(), - }); return ( - {/* Changes page title visible on the header */} - - - Create T3 Turbo - - + - } - renderItem={(p) => ( - deletePostMutation.mutate(p.item.id)} - /> - )} - /> - - ); diff --git a/apps/native/src/components/DrawerToggle.tsx b/apps/native/src/components/DrawerToggle.tsx new file mode 100644 index 0000000..2a4ee48 --- /dev/null +++ b/apps/native/src/components/DrawerToggle.tsx @@ -0,0 +1,31 @@ +import { DrawerNavigationProp } from '@react-navigation/drawer'; +import { useNavigation } from 'expo-router'; +import { AlignJustify } from 'lucide-react-native'; +import { Pressable, View } from 'react-native'; +import { cn } from '~/lib/utils'; + +export function DrawerToggle() { + const navigation = useNavigation>(); + + return ( + + {({ pressed }) => ( + + + + )} + + ); +} diff --git a/apps/native/src/components/ThemeToggle.tsx b/apps/native/src/components/ThemeToggle.tsx new file mode 100644 index 0000000..546ccf0 --- /dev/null +++ b/apps/native/src/components/ThemeToggle.tsx @@ -0,0 +1,40 @@ +import AsyncStorage from '@react-native-async-storage/async-storage'; +import { MoonStar, Sun } from 'lucide-react-native'; +import { useColorScheme } from 'nativewind'; +import { Pressable, View } from 'react-native'; +import { setAndroidNavigationBar } from '~/lib/android-navigation-bar'; +import { cn } from '~/lib/utils'; + +export function ThemeToggle() { + const { colorScheme, setColorScheme } = useColorScheme(); + return ( + { + const newTheme = colorScheme === 'dark' ? 'light' : 'dark'; + setColorScheme(newTheme); + setAndroidNavigationBar(newTheme); + AsyncStorage.setItem('theme', newTheme); + }} + className='ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50' + > + {({ pressed }) => ( + + {colorScheme === 'light' ? ( + + ) : ( + + )} + + )} + + ); +} diff --git a/apps/native/src/components/ui/accordion.tsx b/apps/native/src/components/ui/accordion.tsx new file mode 100644 index 0000000..35e46f7 --- /dev/null +++ b/apps/native/src/components/ui/accordion.tsx @@ -0,0 +1,205 @@ +import { ChevronDown } from 'lucide-react-native'; +import React from 'react'; +import { LayoutChangeEvent, Pressable, View } from 'react-native'; +import Animated, { + Extrapolate, + SharedValue, + interpolate, + measure, + runOnUI, + useAnimatedStyle, + useDerivedValue, + useSharedValue, + withTiming, +} from 'react-native-reanimated'; +import { Separator } from '~/components/ui/separator'; +import { cn } from '~/lib/utils'; + +const Accordion = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)); + +Accordion.displayName = 'Accordion'; + +interface AccordionItemProps { + disabled?: boolean; + defaultOpen?: boolean; + onChange?: (isOpen: boolean) => void; +} + +interface AccordionItemContext extends AccordionItemProps { + innerContentRef: React.RefObject; + contentHeight: SharedValue; + progress: SharedValue; + open: SharedValue; + nativeID: string; +} + +const AccordionItemContext = React.createContext( + {} as AccordionItemContext +); + +const AccordionItem = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef & AccordionItemProps +>( + ( + { className, disabled = false, defaultOpen = false, onChange, ...props }, + ref + ) => { + const nativeID = React.useId(); + const open = useSharedValue(defaultOpen); + const contentHeight = useSharedValue(0); + const innerContentRef = React.useRef(null); + const progress = useDerivedValue(() => + open.value ? withTiming(1) : withTiming(0) + ); + return ( + + + + ); + } +); + +AccordionItem.displayName = 'AccordionItem'; + +function useAccordionItemContext() { + const context = React.useContext(AccordionItemContext); + if (!context) { + throw new Error( + 'AccordionItem compound components cannot be rendered outside the AccordionItem component' + ); + } + return context; +} + +const AccordionTrigger = React.forwardRef< + React.ElementRef, + Omit, 'onPress'> & { + children: React.ReactNode; + } +>(({ children, className, ...props }, ref) => { + const { + contentHeight, + open, + innerContentRef, + onChange, + disabled, + nativeID, + progress, + } = useAccordionItemContext(); + const [isOpen, setIsOpen] = React.useState(open.value); + + const chevronAnimationStyle = useAnimatedStyle(() => ({ + transform: [ + { + rotate: + contentHeight.value === 0 ? '0deg' : `${progress.value * 180}deg`, + }, + ], + opacity: interpolate(progress.value, [0, 1], [1, 0.8], Extrapolate.CLAMP), + })); + + function onPress() { + if (disabled) return; + if (contentHeight.value === 0) { + runOnUI(() => { + 'worklet'; + contentHeight.value = measure(innerContentRef).height; + })(); + } + open.value = !open.value; + setIsOpen(open.value); + onChange?.(open.value); + } + + return ( + <> + + {children} + + + + + + + ); +}); + +AccordionTrigger.displayName = 'AccordionTrigger'; + +const AccordionContent = React.forwardRef< + React.ElementRef, + Omit, 'onLayout'> & { + children: React.ReactNode; + } +>(({ children, className, ...props }, ref) => { + const { contentHeight, innerContentRef, nativeID, progress } = + useAccordionItemContext(); + + const heightAnimationStyle = useAnimatedStyle(() => ({ + height: interpolate( + progress.value, + [0, 1], + [0, contentHeight.value], + Extrapolate.CLAMP + ), + })); + + const onLayout = React.useCallback( + ({ + nativeEvent: { + layout: { height }, + }, + }: LayoutChangeEvent) => { + contentHeight.value = height; + }, + [contentHeight] + ); + + return ( + + + + {children} + + + + ); +}); + +AccordionContent.displayName = 'AccordionContent'; + +export { Accordion, AccordionContent, AccordionItem, AccordionTrigger }; diff --git a/apps/native/src/components/ui/alert-dialog.tsx b/apps/native/src/components/ui/alert-dialog.tsx new file mode 100644 index 0000000..d958671 --- /dev/null +++ b/apps/native/src/components/ui/alert-dialog.tsx @@ -0,0 +1,300 @@ +import { useColorScheme } from 'nativewind'; +import React from 'react'; +import { + GestureResponderEvent, + Modal, + Pressable, + StyleSheet, + Text, + View, + ViewStyle, +} from 'react-native'; +import { cn } from '~/lib/utils'; +import { Button } from '~/components/ui/button'; +import * as Slot from '~/lib/rn-primitives/slot/slot-native'; + +interface AlertDialogProps { + children: React.ReactNode; + closeOnOverlayPress?: boolean; + defaultOpen?: boolean; + open?: boolean; + setOpen?: React.Dispatch>; +} +interface AlertDialogContext { + visible: boolean; + setVisible: React.Dispatch>; + closeOnOverlayPress: boolean; +} + +const AlertDialogContext = React.createContext( + {} as AlertDialogContext +); + +const AlertDialog = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef & AlertDialogProps +>( + ( + { + open, + setOpen, + closeOnOverlayPress = false, + defaultOpen = false, + ...props + }, + ref + ) => { + const [visible, setVisible] = React.useState(defaultOpen ?? false); + return ( + + + + ); + } +); + +AlertDialog.displayName = 'AlertDialog'; + +function useAlertDialogContext() { + const context = React.useContext(AlertDialogContext); + if (!context) { + throw new Error( + 'AlertDialog compound components cannot be rendered outside the AlertDialog component' + ); + } + return context; +} + +const AlertDialogTrigger = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef & { + asChild?: boolean; + } +>(({ onPress, asChild = false, ...props }, ref) => { + const { setVisible } = useAlertDialogContext(); + function handleOnPress(event: GestureResponderEvent) { + setVisible(true); + onPress?.(event); + } + + const Trigger = asChild ? Slot.Pressable : Button; + return ; +}); + +AlertDialogTrigger.displayName = 'AlertDialogTrigger'; + +const AlertDialogContent = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef & { overlayClass?: string } +>( + ( + { + className, + children, + animationType = 'fade', + style: styleProp, + overlayClass, + ...props + }, + ref + ) => { + const { colorScheme } = useColorScheme(); + const { visible, setVisible, closeOnOverlayPress } = + useAlertDialogContext(); + const [style, setStyle] = React.useState( + StyleSheet.flatten(styleProp) + ); + + React.useEffect(() => { + setStyle( + StyleSheet.flatten([ + colorScheme === 'dark' ? styles.shadowDark : styles.shadowLight, + styleProp, + ]) + ); + }, [styleProp, colorScheme]); + + return ( + { + setVisible((prev) => !prev); + }} + statusBarTranslucent + {...props} + > + { + setVisible(false); + } + : undefined + } + className={cn( + 'flex-1 justify-center items-center p-2', + animationType !== 'slide' && 'bg-zinc-50/80 dark:bg-zinc-900/80', + overlayClass + )} + > + + {children} + + + + ); + } +); + +AlertDialogContent.displayName = 'AlertDialogContent'; + +const AlertDialogHeader = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => { + return ; +}); + +AlertDialogHeader.displayName = 'AlertDialogHeader'; + +const AlertDialogTitle = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => { + return ( + + ); +}); + +AlertDialogTitle.displayName = 'AlertDialogTitle'; + +const AlertDialogDescription = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => { + return ( + + ); +}); + +AlertDialogDescription.displayName = 'AlertDialogDescription'; + +const AlertDialogFooter = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => { + return ( + + ); +}); + +AlertDialogFooter.displayName = 'AlertDialogFooter'; + +const AlertDialogCancel = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef & { + asChild?: boolean; + } +>(({ variant = 'outline', asChild = false, ...props }, ref) => { + const { setVisible } = useAlertDialogContext(); + const Trigger = asChild ? Slot.Pressable : Button; + return ( + { + setVisible(false); + }} + ref={ref} + {...props} + /> + ); +}); + +AlertDialogCancel.displayName = 'AlertDialogCancel'; + +type ButtonProps = React.ComponentPropsWithoutRef; + +const AlertDialogAction = React.forwardRef< + React.ElementRef, + Omit & { + asChild?: boolean; + onPress?: + | ((event: GestureResponderEvent) => void) + | ((event: GestureResponderEvent) => Promise); + } +>(({ onPress, asChild, ...props }, ref) => { + const { setVisible } = useAlertDialogContext(); + async function onPressAction(ev: GestureResponderEvent) { + await onPress?.(ev); + setVisible(false); + } + + const Trigger = asChild ? Slot.Pressable : Button; + return ; +}); + +AlertDialogAction.displayName = 'AlertDialogAction'; + +export { + AlertDialog, + AlertDialogAction, + AlertDialogCancel, + AlertDialogContent, + AlertDialogDescription, + AlertDialogFooter, + AlertDialogHeader, + AlertDialogTitle, + AlertDialogTrigger, +}; + +const styles = StyleSheet.create({ + shadowLight: { + shadowColor: '#000000', + shadowOffset: { + width: 0, + height: 2, + }, + shadowOpacity: 0.1, + shadowRadius: 8, + elevation: 5, + }, + shadowDark: { + shadowColor: '#000000', + shadowOffset: { + width: 0, + height: 2, + }, + shadowOpacity: 0.25, + shadowRadius: 8, + elevation: 5, + }, +}); diff --git a/apps/native/src/components/ui/alert.tsx b/apps/native/src/components/ui/alert.tsx new file mode 100644 index 0000000..c402304 --- /dev/null +++ b/apps/native/src/components/ui/alert.tsx @@ -0,0 +1,129 @@ +import * as React from 'react'; +import { cva, type VariantProps } from 'class-variance-authority'; + +import { cn } from '~/lib/utils'; +import { View, Text, StyleSheet, ViewStyle } from 'react-native'; +import * as LucideIcon from 'lucide-react-native'; +import { useColorScheme } from 'nativewind'; + +const alertVariants = cva( + 'bg-background relative w-full rounded-lg border p-5', + { + variants: { + variant: { + default: 'border-muted-foreground', + destructive: 'border-destructive', + success: 'border-emerald-500', + }, + }, + defaultVariants: { + variant: 'default', + }, + } +); + +const Alert = React.forwardRef< + React.ElementRef, + Omit, 'style'> & + VariantProps & { + icon?: keyof typeof LucideIcon; + style?: ViewStyle; + } +>(({ children, icon, className, variant, style: styleProp, ...props }, ref) => { + const { colorScheme } = useColorScheme(); + const [style, setStyle] = React.useState( + StyleSheet.flatten(styleProp) + ); + + React.useEffect(() => { + setStyle( + StyleSheet.flatten([ + colorScheme === 'dark' ? styles.shadowDark : styles.shadowLight, + styleProp, + ]) + ); + }, [styleProp, colorScheme]); + + const Icon = LucideIcon[icon ?? 'AlertTriangle'] as LucideIcon.Icon; + return ( + + {icon && ( + + )} + {children} + + ); +}); +Alert.displayName = 'Alert'; + +const AlertTitle = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)); +AlertTitle.displayName = 'AlertTitle'; + +const AlertDescription = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)); +AlertDescription.displayName = 'AlertDescription'; + +export { Alert, AlertTitle, AlertDescription }; + +const styles = StyleSheet.create({ + shadowLight: { + shadowColor: '#000000', + shadowOffset: { + width: 0, + height: 2, + }, + shadowOpacity: 0.1, + shadowRadius: 8, + elevation: 2, + }, + shadowDark: { + shadowColor: '#FFFFFF', + shadowOffset: { + width: 0, + height: 2, + }, + shadowOpacity: 0.05, + shadowRadius: 8, + elevation: 1, + }, +}); diff --git a/apps/native/src/components/ui/avatar.tsx b/apps/native/src/components/ui/avatar.tsx new file mode 100644 index 0000000..84599b2 --- /dev/null +++ b/apps/native/src/components/ui/avatar.tsx @@ -0,0 +1,69 @@ +import React from 'react'; +import { + Image, + ImageErrorEventData, + NativeSyntheticEvent, + Text, + View, +} from 'react-native'; + +import { cn } from '~/lib/utils'; + +const Avatar = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)); +Avatar.displayName = 'Avatar'; + +const AvatarImage = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => { + const [hasError, setHasError] = React.useState(false); + + function onError(error: NativeSyntheticEvent) { + setHasError(!!error); + } + + if (hasError) { + return null; + } + return ( + + ); +}); +AvatarImage.displayName = 'AvatarImage'; + +const AvatarFallback = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef & { textClass?: string } +>(({ children, textClass, className, ...props }, ref) => ( + + {children} + +)); +AvatarFallback.displayName = 'AvatarFallback'; + +export { Avatar, AvatarImage, AvatarFallback }; diff --git a/apps/native/src/components/ui/badge.tsx b/apps/native/src/components/ui/badge.tsx new file mode 100644 index 0000000..6ffbe33 --- /dev/null +++ b/apps/native/src/components/ui/badge.tsx @@ -0,0 +1,62 @@ +import { cva, type VariantProps } from 'class-variance-authority'; +import React from 'react'; +import { Text, View } from 'react-native'; + +const badgeRootVariants = cva( + 'items-center rounded-full border px-2.5 py-0.5', + { + variants: { + variant: { + default: 'border-transparent bg-primary ', + secondary: 'border-transparent bg-secondary ', + destructive: 'border-transparent bg-destructive ', + outline: 'border-border', + }, + }, + defaultVariants: { + variant: 'default', + }, + } +); + +const badgeTextVariants = cva('font-semibold', { + variants: { + variant: { + default: 'text-primary-foreground', + secondary: 'text-secondary-foreground', + destructive: 'text-destructive-foreground', + outline: 'text-foreground', + }, + size: { + sm: 'text-xs native:text-sm', + md: 'text-sm native:text-base', + lg: 'text-base native:text-lg', + }, + }, + defaultVariants: { + variant: 'default', + size: 'md', + }, +}); + +const Badge = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef & + VariantProps & { textClass?: string } +>(({ className, children, textClass, variant, size, ...props }, ref) => { + return ( + + + {children} + + + ); +}); + +export { Badge, badgeRootVariants, badgeTextVariants }; diff --git a/apps/native/src/components/ui/bottom-sheet.native.tsx b/apps/native/src/components/ui/bottom-sheet.native.tsx new file mode 100644 index 0000000..c9c7022 --- /dev/null +++ b/apps/native/src/components/ui/bottom-sheet.native.tsx @@ -0,0 +1,356 @@ +import type { + BottomSheetBackdropProps, + BottomSheetFooterProps as GBottomSheetFooterProps, +} from '@gorhom/bottom-sheet'; +import { + BottomSheetBackdrop, + BottomSheetModal, + BottomSheetFlatList as GBottomSheetFlatList, + BottomSheetFooter as GBottomSheetFooter, + BottomSheetTextInput as GBottomSheetTextInput, + BottomSheetView as GBottomSheetView, + useBottomSheetModal, +} from '@gorhom/bottom-sheet'; +import type { BottomSheetModalMethods } from '@gorhom/bottom-sheet/lib/typescript/types'; +import { X } from 'lucide-react-native'; +import { useColorScheme } from 'nativewind'; +import React, { useCallback, useImperativeHandle } from 'react'; +import { + GestureResponderEvent, + Keyboard, + Pressable, + View, + ViewStyle, +} from 'react-native'; +import { useSafeAreaInsets } from 'react-native-safe-area-context'; +import { Button } from '~/components/ui/button'; +import { NAV_THEME } from '~/lib/constants'; +import * as Slot from '~/lib/rn-primitives/slot/slot-native'; +import { cn } from '~/lib/utils'; + +type BottomSheetRef = React.ElementRef; +type BottomSheetProps = React.ComponentPropsWithoutRef; + +interface BottomSheetContext { + sheetRef: React.RefObject; +} + +const BottomSheetContext = React.createContext({} as BottomSheetContext); + +const BottomSheet = React.forwardRef( + ({ ...props }, ref) => { + const sheetRef = React.useRef(null); + + return ( + + + + ); + } +); + +function useBottomSheetContext() { + const context = React.useContext(BottomSheetContext); + if (!context) { + throw new Error( + 'BottomSheet compound components cannot be rendered outside the BottomSheet component' + ); + } + return context; +} + +const CLOSED_INDEX = -1; + +type BottomSheetContentRef = React.ElementRef; + +type BottomSheetContentProps = Omit< + React.ComponentPropsWithoutRef, + 'backdropComponent' +> & { + backdropProps?: Partial< + React.ComponentPropsWithoutRef + >; +}; + +const BottomSheetContent = React.forwardRef< + BottomSheetContentRef, + BottomSheetContentProps +>( + ( + { + enablePanDownToClose = true, + enableDynamicSizing = true, + index = 0, + backdropProps, + backgroundStyle, + android_keyboardInputMode = 'adjustResize', + ...props + }, + ref + ) => { + const insets = useSafeAreaInsets(); + const { colorScheme } = useColorScheme(); + const { sheetRef } = useBottomSheetContext(); + + useImperativeHandle( + ref, + () => { + if (!sheetRef.current) { + return {} as BottomSheetModalMethods; + } + return sheetRef.current; + }, + [sheetRef.current] + ); + + const renderBackdrop = useCallback( + (props: BottomSheetBackdropProps) => { + const { + pressBehavior = 'close', + opacity = colorScheme === 'dark' ? 0.3 : 0.7, + disappearsOnIndex = CLOSED_INDEX, + style, + onPress, + ...rest + } = { + ...props, + ...backdropProps, + }; + return ( + { + if (Keyboard.isVisible()) { + Keyboard.dismiss(); + } + onPress?.(); + }} + {...rest} + /> + ); + }, + [backdropProps, colorScheme] + ); + + return ( + + ); + } +); + +const BottomSheetOpenTrigger = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef & { + asChild?: boolean; + } +>(({ onPress, asChild = false, ...props }, ref) => { + const { sheetRef } = useBottomSheetContext(); + function handleOnPress(ev: GestureResponderEvent) { + sheetRef.current?.present(); + onPress?.(ev); + } + const Trigger = asChild ? Slot.Pressable : Pressable; + return ; +}); + +const BottomSheetCloseTrigger = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef & { + asChild?: boolean; + } +>(({ onPress, asChild = false, ...props }, ref) => { + const { dismiss } = useBottomSheetModal(); + function handleOnPress(ev: GestureResponderEvent) { + dismiss(); + if (Keyboard.isVisible()) { + Keyboard.dismiss(); + } + onPress?.(ev); + } + const Trigger = asChild ? Slot.Pressable : Pressable; + return ; +}); + +const BOTTOM_SHEET_HEADER_HEIGHT = 60; // BottomSheetHeader height + +type BottomSheetViewProps = Omit< + React.ComponentPropsWithoutRef, + 'style' +> & { + hadHeader?: boolean; + style?: ViewStyle; +}; + +function BottomSheetView({ + className, + children, + hadHeader = true, + style, + ...props +}: BottomSheetViewProps) { + const insets = useSafeAreaInsets(); + return ( + + {children} + + ); +} + +type BottomSheetTextInputRef = React.ElementRef; +type BottomSheetTextInputProps = React.ComponentPropsWithoutRef< + typeof GBottomSheetTextInput +>; +const BottomSheetTextInput = React.forwardRef< + BottomSheetTextInputRef, + BottomSheetTextInputProps +>(({ className, placeholderClassName, ...props }, ref) => { + return ( + + ); +}); + +type BottomSheetFlatListRef = React.ElementRef; +type BottomSheetFlatListProps = React.ComponentPropsWithoutRef< + typeof GBottomSheetFlatList +>; +const BottomSheetFlatList = React.forwardRef< + BottomSheetFlatListRef, + BottomSheetFlatListProps +>(({ className, ...props }, ref) => { + const insets = useSafeAreaInsets(); + return ( + + ); +}); + +type BottomSheetHeaderRef = React.ElementRef; +type BottomSheetHeaderProps = React.ComponentPropsWithoutRef; +const BottomSheetHeader = React.forwardRef< + BottomSheetHeaderRef, + BottomSheetHeaderProps +>(({ className, children, ...props }, ref) => { + const { dismiss } = useBottomSheetModal(); + function close() { + if (Keyboard.isVisible()) { + Keyboard.dismiss(); + } + dismiss(); + } + return ( + + {children} + + + ); +}); + +type BottomSheetFooterRef = React.ElementRef; +type BottomSheetFooterProps = Omit< + React.ComponentPropsWithoutRef, + 'style' +> & { + bottomSheetFooterProps: GBottomSheetFooterProps; + children?: React.ReactNode; + style?: ViewStyle; +}; + +/** + * To be used in a useCallback function as a props to BottomSheetContent + */ +const BottomSheetFooter = React.forwardRef< + BottomSheetFooterRef, + BottomSheetFooterProps +>(({ bottomSheetFooterProps, children, className, style, ...props }, ref) => { + const insets = useSafeAreaInsets(); + return ( + + + {children} + + + ); +}); + +function useBottomSheet() { + const ref = React.useRef(null); + + const open = useCallback(() => { + ref.current?.present(); + }, []); + + const close = useCallback(() => { + ref.current?.dismiss(); + }, []); + + return { ref, open, close }; +} + +export { + BottomSheet, + BottomSheetCloseTrigger, + BottomSheetContent, + BottomSheetFlatList, + BottomSheetFooter, + BottomSheetHeader, + BottomSheetOpenTrigger, + BottomSheetTextInput, + BottomSheetView, + useBottomSheet, +}; diff --git a/apps/native/src/components/ui/bottom-sheet.tsx b/apps/native/src/components/ui/bottom-sheet.tsx new file mode 100644 index 0000000..59dd9ed --- /dev/null +++ b/apps/native/src/components/ui/bottom-sheet.tsx @@ -0,0 +1,257 @@ +import type { BottomSheetFooterProps as GBottomSheetFooterProps } from '@gorhom/bottom-sheet'; +import { + BottomSheetBackdrop, + BottomSheetModal, + BottomSheetFlatList as GBottomSheetFlatList, + BottomSheetFooter as GBottomSheetFooter, + BottomSheetTextInput as GBottomSheetTextInput, + BottomSheetView as GBottomSheetView, + useBottomSheetModal, +} from '@gorhom/bottom-sheet'; +import { X } from 'lucide-react-native'; +import React, { useCallback } from 'react'; +import { + GestureResponderEvent, + Keyboard, + Pressable, + View, + ViewStyle, +} from 'react-native'; +import { useSafeAreaInsets } from 'react-native-safe-area-context'; +import * as Slot from '~/lib/rn-primitives/slot/slot-native'; +import { Button } from '~/components/ui/button'; +import { cn } from '~/lib/utils'; + +// !IMPORTANT: This file is only for web. BottomSheet is not available for web yet. +// Should be available in v5 which is in alpha: components/ui/bottom-sheet.tsx + +type BottomSheetRef = React.ElementRef; +type BottomSheetProps = React.ComponentPropsWithoutRef; + +interface BottomSheetContext { + sheetRef: React.RefObject; +} + +const BottomSheetContext = React.createContext({} as BottomSheetContext); + +const BottomSheet = React.forwardRef( + ({ ...props }, ref) => { + return ; + } +); + +type BottomSheetContentRef = React.ElementRef; + +type BottomSheetContentProps = Omit< + React.ComponentPropsWithoutRef, + 'backdropComponent' +> & { + backdropProps?: Partial< + React.ComponentPropsWithoutRef + >; +}; + +const BottomSheetContent = React.forwardRef< + BottomSheetContentRef, + BottomSheetContentProps +>(() => { + return null; +}); + +const BottomSheetOpenTrigger = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef & { + asChild?: boolean; + } +>(({ onPress, asChild = false, ...props }, ref) => { + function handleOnPress() { + window.alert( + 'Not implemented for web yet. Check `bottom-sheet.tsx` for more info.' + ); + } + const Trigger = asChild ? Slot.Pressable : Pressable; + return ; +}); + +const BottomSheetCloseTrigger = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef & { + asChild?: boolean; + } +>(({ onPress, asChild = false, ...props }, ref) => { + const { dismiss } = useBottomSheetModal(); + function handleOnPress(ev: GestureResponderEvent) { + dismiss(); + if (Keyboard.isVisible()) { + Keyboard.dismiss(); + } + onPress?.(ev); + } + const Trigger = asChild ? Slot.Pressable : Pressable; + return ; +}); + +const BOTTOM_SHEET_HEADER_HEIGHT = 60; // BottomSheetHeader height + +type BottomSheetViewProps = Omit< + React.ComponentPropsWithoutRef, + 'style' +> & { + hadHeader?: boolean; + style?: ViewStyle; +}; + +function BottomSheetView({ + className, + children, + hadHeader = true, + style, + ...props +}: BottomSheetViewProps) { + const insets = useSafeAreaInsets(); + return ( + + {children} + + ); +} + +type BottomSheetTextInputRef = React.ElementRef; +type BottomSheetTextInputProps = React.ComponentPropsWithoutRef< + typeof GBottomSheetTextInput +>; +const BottomSheetTextInput = React.forwardRef< + BottomSheetTextInputRef, + BottomSheetTextInputProps +>(({ className, placeholderClassName, ...props }, ref) => { + return ( + + ); +}); + +type BottomSheetFlatListRef = React.ElementRef; +type BottomSheetFlatListProps = React.ComponentPropsWithoutRef< + typeof GBottomSheetFlatList +>; +const BottomSheetFlatList = React.forwardRef< + BottomSheetFlatListRef, + BottomSheetFlatListProps +>(({ className, ...props }, ref) => { + const insets = useSafeAreaInsets(); + return ( + + ); +}); + +type BottomSheetHeaderRef = React.ElementRef; +type BottomSheetHeaderProps = React.ComponentPropsWithoutRef; +const BottomSheetHeader = React.forwardRef< + BottomSheetHeaderRef, + BottomSheetHeaderProps +>(({ className, children, ...props }, ref) => { + const { dismiss } = useBottomSheetModal(); + function close() { + if (Keyboard.isVisible()) { + Keyboard.dismiss(); + } + dismiss(); + } + return ( + + {children} + + + ); +}); + +type BottomSheetFooterRef = React.ElementRef; +type BottomSheetFooterProps = Omit< + React.ComponentPropsWithoutRef, + 'style' +> & { + bottomSheetFooterProps: GBottomSheetFooterProps; + children?: React.ReactNode; + style?: ViewStyle; +}; + +/** + * To be used in a useCallback function as a props to BottomSheetContent + */ +const BottomSheetFooter = React.forwardRef< + BottomSheetFooterRef, + BottomSheetFooterProps +>(({ bottomSheetFooterProps, children, className, style, ...props }, ref) => { + const insets = useSafeAreaInsets(); + return ( + + + {children} + + + ); +}); + +function useBottomSheet() { + const ref = React.useRef(null); + + const open = useCallback(() => { + ref.current?.present(); + }, []); + + const close = useCallback(() => { + ref.current?.dismiss(); + }, []); + + return { ref, open, close }; +} + +export { + BottomSheet, + BottomSheetCloseTrigger, + BottomSheetContent, + BottomSheetFlatList, + BottomSheetFooter, + BottomSheetHeader, + BottomSheetOpenTrigger, + BottomSheetTextInput, + BottomSheetView, + useBottomSheet, +}; diff --git a/apps/native/src/components/ui/button.tsx b/apps/native/src/components/ui/button.tsx new file mode 100644 index 0000000..1948bce --- /dev/null +++ b/apps/native/src/components/ui/button.tsx @@ -0,0 +1,139 @@ +import { cva } from 'class-variance-authority'; +import type { VariantProps } from 'class-variance-authority'; +import * as React from 'react'; + +import { useColorScheme } from 'nativewind'; +import { Platform, Pressable, Text, View } from 'react-native'; +import { cn, isTextChildren } from '~/lib/utils'; +import * as Slot from '~/lib/rn-primitives/slot/slot-native'; + +const buttonVariants = cva( + 'flex-row items-center justify-center rounded-lg web:ring-offset-background web:transition-colors web:focus-visible:outline-none web:focus-visible:ring-2 web:focus-visible:ring-ring web:focus-visible:ring-offset-2', + { + variants: { + variant: { + default: 'bg-primary', + destructive: 'bg-destructive', + outline: 'border border-input bg-background', + secondary: 'bg-secondary', + ghost: '', + link: '', + }, + size: { + default: 'px-4 py-2 native:px-6 native:py-3.5', + sm: 'px-3 py-1 native:py-2', + lg: 'px-8 py-1.5 native:py-4', + }, + }, + defaultVariants: { + variant: 'default', + size: 'default', + }, + } +); + +const buttonTextVariants = cva('font-medium', { + variants: { + variant: { + default: 'text-primary-foreground', + destructive: 'text-destructive-foreground', + outline: 'text-foreground', + secondary: 'text-secondary-foreground', + ghost: 'text-foreground', + link: 'text-primary underline', + }, + size: { + default: 'text-sm native:text-xl', + sm: 'text-xs native:text-lg', + lg: 'text-base native:text-2xl', + }, + }, + defaultVariants: { + variant: 'default', + size: 'default', + }, +}); + +const rippleColor = (isThemeDark: boolean) => { + const secondary = isThemeDark ? 'hsl(240 4% 16%)' : 'hsl(240 5% 96%)'; + return { + default: isThemeDark ? '#d4d4d8' : '#3f3f46', + destructive: isThemeDark ? '#b91c1c' : '#f87171', + outline: secondary, + secondary: isThemeDark ? '#3f3f46' : '#e4e4e7', + ghost: secondary, + link: secondary, + }; +}; + +const Button = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef & + VariantProps & { + textClass?: string; + androidRootClass?: string; + } +>( + ( + { + className, + textClass, + variant = 'default', + size, + children, + androidRootClass, + disabled, + ...props + }, + ref + ) => { + const { colorScheme } = useColorScheme(); + const Root = Platform.OS === 'android' ? View : Slot.Pressable; + return ( + + + {isTextChildren(children) + ? ({ pressed, hovered }) => ( + + {children as string | string[]} + + ) + : children} + + + ); + } +); +Button.displayName = 'Button'; + +export { Button, buttonTextVariants, buttonVariants }; diff --git a/apps/native/src/components/ui/calendar.tsx b/apps/native/src/components/ui/calendar.tsx new file mode 100644 index 0000000..2d02028 --- /dev/null +++ b/apps/native/src/components/ui/calendar.tsx @@ -0,0 +1,147 @@ +import { useColorScheme } from 'nativewind'; +import React from 'react'; +import { Calendar as RNCalendar, LocaleConfig } from 'react-native-calendars'; +import { NAV_THEME } from '~/lib/constants'; + +/** + * @docs https://github.com/wix/react-native-calendars + */ +function Calendar({ + theme, + ...props +}: React.ComponentProps) { + const { colorScheme } = useColorScheme(); + const id = React.useId(); + + return ( + + ); +} + +const SKY_500 = '#0ea5e9'; +const SKY_600 = '#0284c7'; + +function getTheme( + isThemeDark: boolean, + customTheme?: React.ComponentProps['theme'] +): React.ComponentProps['theme'] { + if (isThemeDark) { + return { + backgroundColor: NAV_THEME.dark.background, + calendarBackground: NAV_THEME.dark.card, + textSectionTitleColor: NAV_THEME.dark.text, + selectedDayBackgroundColor: SKY_500, + selectedDayTextColor: '#000000', + todayTextColor: SKY_500, + dayTextColor: NAV_THEME.dark.text, + textDisabledColor: '#ffffff30', + monthTextColor: NAV_THEME.dark.text, + textMonthFontWeight: '500', + arrowColor: SKY_500, + ...customTheme, + }; + } + return { + backgroundColor: NAV_THEME.light.background, + calendarBackground: NAV_THEME.light.card, + textSectionTitleColor: NAV_THEME.light.text, + selectedDayBackgroundColor: SKY_600, + selectedDayTextColor: '#ffffff', + todayTextColor: SKY_600, + dayTextColor: '#2d4150', + monthTextColor: NAV_THEME.light.text, + textMonthFontWeight: '500', + arrowColor: SKY_600, + ...customTheme, + }; +} + +LocaleConfig.locales['en'] = { + monthNames: [ + 'January', + 'Febuary', + 'March', + 'April', + 'May', + 'June', + 'July', + 'August', + 'Septemeber', + 'October', + 'November', + 'December', + ], + monthNamesShort: [ + 'Jan', + 'Feb', + 'Mar', + 'Apr', + 'May', + 'Jun', + 'Jul', + 'Aug', + 'Sept', + 'Oct', + 'Nov', + 'Dec', + ], + dayNames: [ + 'Sunday', + 'Monday', + 'Tuesday', + 'Wednesday', + 'Thursday', + 'Friday', + 'Saturday', + ], + dayNamesShort: ['Sun', 'Mon', 'Tue', 'Wed', 'Thur', 'Fri', 'Sat'], + today: 'Today', +}; + +LocaleConfig.locales['fr'] = { + monthNames: [ + 'Janvier', + 'Février', + 'Mars', + 'Avril', + 'Mai', + 'Juin', + 'Juillet', + 'Août', + 'Septembre', + 'Octobre', + 'Novembre', + 'Décembre', + ], + monthNamesShort: [ + 'Janv.', + 'Févr.', + 'Mars', + 'Avril', + 'Mai', + 'Juin', + 'Juil.', + 'Août', + 'Sept.', + 'Oct.', + 'Nov.', + 'Déc.', + ], + dayNames: [ + 'Dimanche', + 'Lundi', + 'Mardi', + 'Mercredi', + 'Jeudi', + 'Vendredi', + 'Samedi', + ], + dayNamesShort: ['Dim.', 'Lun.', 'Mar.', 'Mer.', 'Jeu.', 'Ven.', 'Sam.'], + today: "Aujourd'hui", +}; + +export { Calendar, LocaleConfig }; diff --git a/apps/native/src/components/ui/card.tsx b/apps/native/src/components/ui/card.tsx new file mode 100644 index 0000000..2660fa6 --- /dev/null +++ b/apps/native/src/components/ui/card.tsx @@ -0,0 +1,126 @@ +import { useColorScheme } from 'nativewind'; +import React from 'react'; +import type { ViewStyle } from 'react-native'; +import { Text, View, StyleSheet } from 'react-native'; + +import { cn } from '~/lib/utils'; + +const Card = React.forwardRef< + React.ElementRef, + Omit, 'style'> & { + style?: ViewStyle; + } +>(({ className, style: styleProp, ...props }, ref) => { + const { colorScheme } = useColorScheme(); + const [style, setStyle] = React.useState(styleProp ?? {}); + + React.useEffect(() => { + setStyle( + StyleSheet.flatten([ + colorScheme === 'dark' ? styles.shadowDark : styles.shadowLight, + styleProp, + ]) + ); + }, [styleProp, colorScheme]); + + return ( + + ); +}); + +Card.displayName = 'Card'; + +const CardHeader = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)); +CardHeader.displayName = 'CardHeader'; + +const CardTitle = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)); +CardTitle.displayName = 'CardTitle'; + +const CardDescription = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)); +CardDescription.displayName = 'CardDescription'; + +const CardContent = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)); +CardContent.displayName = 'CardContent'; + +const CardFooter = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)); +CardFooter.displayName = 'CardFooter'; + +export { + Card, + CardHeader, + CardFooter, + CardTitle, + CardDescription, + CardContent, +}; + +const styles = StyleSheet.create({ + shadowLight: { + shadowColor: '#000000', + shadowOffset: { + width: 0, + height: 2, + }, + shadowOpacity: 0.05, + shadowRadius: 8, + elevation: 2, + }, + shadowDark: { + shadowColor: '#FFFFFF', + shadowOffset: { + width: 0, + height: 2, + }, + shadowOpacity: 0.03, + shadowRadius: 8, + elevation: 1, + }, +}); diff --git a/apps/native/src/components/ui/checkbox.tsx b/apps/native/src/components/ui/checkbox.tsx new file mode 100644 index 0000000..f0f9d40 --- /dev/null +++ b/apps/native/src/components/ui/checkbox.tsx @@ -0,0 +1,51 @@ +import React from 'react'; +import { Check } from 'lucide-react-native'; + +import { cn } from '~/lib/utils'; +import { Pressable, View } from 'react-native'; +import Animated, { FadeIn, FadeOut } from 'react-native-reanimated'; + +interface CheckboxProps { + value: boolean; + onChange: (checked: boolean) => void; + iconClass?: string; + iconSize?: number; +} + +const AnimatedCheck = Animated.createAnimatedComponent(Check); + +const Checkbox = React.forwardRef< + React.ElementRef, + Omit, 'onPress'> & + CheckboxProps +>(({ className, value, onChange, iconClass, iconSize = 16, ...props }, ref) => { + return ( + { + onChange(!value); + }} + {...props} + > + + {value && ( + + )} + + ); +}); + +Checkbox.displayName = 'Checkbox'; + +export { Checkbox }; diff --git a/apps/native/src/components/ui/collapsible.tsx b/apps/native/src/components/ui/collapsible.tsx new file mode 100644 index 0000000..5b0f280 --- /dev/null +++ b/apps/native/src/components/ui/collapsible.tsx @@ -0,0 +1,150 @@ +import { VariantProps } from 'class-variance-authority'; +import React from 'react'; +import { GestureResponderEvent, Pressable, View } from 'react-native'; +import Animated, { FadeInDown, FadeOutUp } from 'react-native-reanimated'; +import { buttonVariants } from '~/components/ui/button'; +import * as Slot from '~/lib/rn-primitives/slot/slot-native'; +import { cn } from '~/lib/utils'; + +interface CollapsibleProps { + open?: boolean; + setOpen?: React.Dispatch>; + defaultOpen?: boolean; + disabled?: boolean; +} + +interface CollapsibleContext { + visible: boolean; + setVisible: React.Dispatch>; + nativeID: string; + disabled: boolean; +} + +const CollapsibleContext = React.createContext({} as CollapsibleContext); + +const Collapsible = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef & CollapsibleProps +>( + ( + { open, setOpen, defaultOpen, className, disabled = false, ...props }, + ref + ) => { + const [visible, setVisible] = React.useState(defaultOpen ?? false); + const nativeID = React.useId(); + + return ( + + + + ); + } +); + +function useCollapsibleContext() { + const context = React.useContext(CollapsibleContext); + if (!context) { + throw new Error( + 'Collapsible compound components cannot be rendered outside the Collapsible component' + ); + } + return context; +} + +const CollapsibleHeader = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => { + return ( + + ); +}); + +const CollapsibleTrigger = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef & + VariantProps & { + asChild?: boolean; + } +>( + ( + { + className, + onPress, + variant = 'outline', + size = 'sm', + asChild = false, + ...props + }, + ref + ) => { + const { nativeID, visible, setVisible, disabled } = useCollapsibleContext(); + + function handleOnPress(event: GestureResponderEvent) { + setVisible((prev) => !prev); + onPress?.(event); + } + + const Trigger = asChild ? Slot.Pressable : Pressable; + return ( + + ); + } +); + +const CollapsibleContent = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => { + const { nativeID, visible } = useCollapsibleContext(); + + if (!visible) return null; + return ( + + ); +}); + +export { + Collapsible, + CollapsibleContent, + CollapsibleHeader, + CollapsibleTrigger, +}; diff --git a/apps/native/src/components/ui/combobox.tsx b/apps/native/src/components/ui/combobox.tsx new file mode 100644 index 0000000..8bb5fea --- /dev/null +++ b/apps/native/src/components/ui/combobox.tsx @@ -0,0 +1,229 @@ +import { Check, ChevronsUpDown, Search } from 'lucide-react-native'; +import React from 'react'; +import { ListRenderItemInfo, Text, View } from 'react-native'; +import { useSafeAreaInsets } from 'react-native-safe-area-context'; +import { + BottomSheet, + BottomSheetContent, + BottomSheetFlatList, + BottomSheetHeader, + BottomSheetOpenTrigger, + BottomSheetTextInput, + useBottomSheet, +} from '~/components/ui/bottom-sheet'; +import { + Button, + buttonTextVariants, + buttonVariants, +} from '~/components/ui/button'; +import { cn } from '~/lib/utils'; + +// TODO: Fix bottom sheet content UI - Too big/too much space + +const HEADER_HEIGHT = 130; + +interface ComboboxOption { + label: string; + value: string; +} + +const Combobox = React.forwardRef< + React.ElementRef, + Omit, 'children'> & { + items: ComboboxOption[]; + placeholder?: string; + inputProps?: React.ComponentPropsWithoutRef; + emptyText?: string; + defaultSelectedItem?: ComboboxOption | null; + selectedItem?: ComboboxOption | null; + onSelectedItemChange?: (option: ComboboxOption | null) => void; + } +>( + ( + { + className, + textClass, + variant = 'outline', + size = 'sm', + inputProps, + placeholder, + items, + emptyText = 'Nothing found...', + defaultSelectedItem = null, + selectedItem: selectedItemProp, + onSelectedItemChange, + ...props + }, + ref + ) => { + const insets = useSafeAreaInsets(); + const [search, setSearch] = React.useState(''); + const [selectedItem, setSelectedItem] = + React.useState(defaultSelectedItem); + const bottomSheet = useBottomSheet(); + const inputRef = + React.useRef>(null); + + const listItems = React.useMemo(() => { + return search + ? items.filter((item) => { + return item.label + .toLocaleLowerCase() + .includes(search.toLocaleLowerCase()); + }) + : items; + }, [items, search]); + + function onItemChange(listItem: ComboboxOption) { + if (selectedItemProp?.value === listItem.value) { + return null; + } + setSearch(''); + bottomSheet.close(); + return listItem; + } + + const renderItem = React.useCallback( + ({ item }: ListRenderItemInfo) => { + const listItem = item as ComboboxOption; + const isSelected = onSelectedItemChange + ? selectedItemProp?.value === listItem.value + : selectedItem?.value === listItem.value; + return ( + + ); + }, + [selectedItem, selectedItemProp] + ); + + function onSubmitEditing() { + const firstItem = listItems[0]; + if (!firstItem) return; + if (onSelectedItemChange) { + onSelectedItemChange(firstItem); + } else { + setSelectedItem(firstItem); + } + bottomSheet.close(); + } + + function onSearchIconPress() { + if (!inputRef.current) return; + const input = inputRef.current; + if (input && 'focus' in input && typeof input.focus === 'function') { + input.focus(); + } + } + + const itemSelected = onSelectedItemChange ? selectedItemProp : selectedItem; + + return ( + + + + + {itemSelected ? itemSelected.label : placeholder ?? ''} + + + + + { + setSearch(''); + }} + > + + + {placeholder} + + + + + + + (item as ComboboxOption).value} + className={'px-4'} + keyboardShouldPersistTaps='handled' + ListEmptyComponent={() => { + return ( + + + {emptyText} + + + ); + }} + /> + + + ); + } +); + +Combobox.displayName = 'Combobox'; + +export { Combobox, type ComboboxOption }; diff --git a/apps/native/src/components/ui/command.tsx b/apps/native/src/components/ui/command.tsx new file mode 100644 index 0000000..256f4f3 --- /dev/null +++ b/apps/native/src/components/ui/command.tsx @@ -0,0 +1,444 @@ +import { type ListRenderItemInfo } from '@shopify/flash-list'; +import { Search, X } from 'lucide-react-native'; +import React, { useImperativeHandle } from 'react'; +import { + GestureResponderEvent, + Modal, + Pressable, + Text, + View, +} from 'react-native'; +import Animated, { FadeInUp, SlideInUp } from 'react-native-reanimated'; +import { useSafeAreaInsets } from 'react-native-safe-area-context'; +import { useKeyboard } from '~/lib/keyboard'; +import * as Slot from '~/lib/rn-primitives/slot/slot-native'; +import { cn, isTextChildren } from '~/lib/utils'; +import { Button } from './button'; +import { Input } from './input'; +import { SectionList } from './section-list'; + +type Data = Record | string; + +interface CommandProps { + data: T[]; + onItemSelected: (item: Exclude) => void; + filterFn: (search: string, item: Exclude) => boolean; + defaultOpen?: boolean; + onOpenChange?: (isOpen: boolean) => void; + onSearch?: (search: string) => void; +} + +interface CommandContext { + data: T[]; + onItemSelected: (item: Exclude) => void; + isOpen: boolean; + toggleIsOpen: () => void; + search: string; + handleOnSearch: (search: string) => void; +} + +const CommandContext = React.createContext>( + {} as CommandContext +); + +type CommandWrapperProps = React.ComponentPropsWithoutRef< + typeof View +> & + CommandProps; + +function CommandWrapper( + { + data: dataFromProps, + defaultOpen = false, + onOpenChange, + onSearch, + onItemSelected, + filterFn, + ...props + }: CommandWrapperProps, + ref?: React.ForwardedRef +) { + const [isOpen, setIsOpen] = React.useState(defaultOpen); + const [search, setSearch] = React.useState(''); + + const data = React.useMemo(() => { + const items = dataFromProps.filter((item) => { + if (typeof item === 'string') return true; + return filterFn(search, item as Exclude); + }); + return items.filter((item, index) => { + if (typeof item === 'string') { + const nextItem = items[index + 1]; + return nextItem && typeof nextItem !== 'string'; + } + return true; + }); + }, [search, dataFromProps, filterFn]); + + function toggleIsOpen() { + setIsOpen((prev) => { + const newVal = !prev; + onOpenChange?.(newVal); + return newVal; + }); + if (search) { + setSearch(''); + } + } + + function handleOnSearch(search: string) { + setSearch(search); + onSearch?.(search); + } + + return ( + + + + ); +} + +interface WithForwardRefCommand extends React.FC> { + (props: CommandWrapperProps): ReturnType< + React.FC> + >; +} + +const Command: WithForwardRefCommand = React.forwardRef(CommandWrapper); + +Command.displayName = 'Command'; + +function useCommandContext() { + const context = React.useContext>(CommandContext); + if (!context) { + throw new Error( + 'Command compound components cannot be rendered outside the Command component' + ); + } + return context; +} + +function CommandPressable( + { + onPress, + asChild = false, + ...props + }: React.ComponentPropsWithoutRef & { + asChild?: boolean; + }, + ref: React.ForwardedRef> +) { + const { toggleIsOpen } = useCommandContext(); + + function handleOnPress(event: GestureResponderEvent) { + toggleIsOpen(); + onPress?.(event); + } + + const Trigger = asChild ? Slot.Pressable : Pressable; + return ; +} + +const CommandTrigger = React.forwardRef(CommandPressable); + +CommandTrigger.displayName = 'CommandTrigger'; + +function CommandModal( + { + className, + children, + animationType = 'fade', + overlayClass, + style, + ...props + }: React.ComponentPropsWithoutRef & { + overlayClass?: string; + }, + ref: React.ForwardedRef> +) { + const insets = useSafeAreaInsets(); + const { keyboardHeight } = useKeyboard(); + const { toggleIsOpen, isOpen } = useCommandContext(); + + return ( + + + + + {children} + + + + + + ); +} + +const CommandContent = React.forwardRef(CommandModal); +CommandContent.displayName = 'CommandContent'; + +function CommandTextInput( + { + className, + placeholderClassName, + ...props + }: React.ComponentPropsWithoutRef, + ref: React.ForwardedRef> +) { + const inputRef = React.useRef>(null); + const { search, handleOnSearch, data, onItemSelected, toggleIsOpen } = + useCommandContext(); + + useImperativeHandle( + ref, + () => { + if (!inputRef.current) { + return {} as React.ComponentRef; + } + return inputRef.current; + }, + [inputRef.current] + ); + + function onSubmitEditing() { + const firstItem = data.find((item) => typeof item !== 'string'); + if (firstItem && typeof firstItem !== 'string') { + onItemSelected(firstItem as Exclude); + } + toggleIsOpen(); + } + + function onSearchIconPress() { + inputRef.current?.focus(); + } + + return ( + + + + + + ); +} + +const CommandInput = React.forwardRef(CommandTextInput); +CommandInput.displayName = 'CommandInput'; + +type CommandSectionListProps = Omit< + React.ComponentPropsWithoutRef>, + 'data' +> & { + headerHeight?: number; + itemHeight?: number; +}; + +function CommandSectionList( + { + headerHeight = 43, + itemHeight = 57, + className, + extraData, + ...props + }: CommandSectionListProps, + ref: React.ForwardedRef>> +) { + const { data, search } = useCommandContext(); + + function overrideItemLayout(layout: any, item: any) { + if (typeof item === 'string') { + layout.size = headerHeight; + } + layout.size = itemHeight; + } + + return ( + + + + ref={ref} + data={data} + extraData={[search, extraData]} + estimatedItemSize={itemHeight} + overrideItemLayout={overrideItemLayout} + keyboardShouldPersistTaps='handled' + role='menu' + {...props} + /> + + ); +} +interface WithForwardRefCommandList + extends React.FC> { + (props: CommandSectionListProps): ReturnType< + React.FC< + Omit>, 'data'> & { + headerHeight?: number; + itemHeight?: number; + } + > + >; +} + +const CommandList: WithForwardRefCommandList = + React.forwardRef(CommandSectionList); +CommandList.displayName = 'CommandList'; + +const CommandListHeader = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef & { textClass?: string } +>(({ className, textClass, children, ...props }, ref) => { + return ( + + {isTextChildren(children) ? ( + + {children} + + ) : ( + children + )} + + ); +}); + +type CommandListHeaderProps = ListRenderItemInfo; + +CommandListHeader.displayName = 'CommandListHeader'; + +function CommandSectionListItem( + { + className, + index, + containnerClass, + onPress, + children, + ...props + }: React.ComponentPropsWithoutRef & { + index: number; + containnerClass?: string; + }, + ref: React.ForwardedRef> +) { + const { data, onItemSelected, toggleIsOpen } = useCommandContext(); + + function handleOnPress(event: GestureResponderEvent) { + const item = data[index]; + if (typeof item === 'string' || !item) return; + onItemSelected(item as Exclude); + onPress?.(event); + toggleIsOpen(); + } + + return ( + + + + ); +} + +type CommandListItemProps = ListRenderItemInfo; + +const CommandListItem = React.forwardRef(CommandSectionListItem); + +CommandListItem.displayName = 'CommandListItem'; + +export { + Command, + CommandContent, + CommandInput, + CommandList, + CommandListHeader, + CommandListItem, + CommandTrigger, + type CommandListHeaderProps, + type CommandListItemProps, +}; diff --git a/apps/native/src/components/ui/context-menu.tsx b/apps/native/src/components/ui/context-menu.tsx new file mode 100644 index 0000000..341da19 --- /dev/null +++ b/apps/native/src/components/ui/context-menu.tsx @@ -0,0 +1,5 @@ +import { Text } from 'react-native'; + +export function ContextMenu() { + return Context Menu (soon); +} diff --git a/apps/native/src/components/ui/data-table.tsx b/apps/native/src/components/ui/data-table.tsx new file mode 100644 index 0000000..5b6be2f --- /dev/null +++ b/apps/native/src/components/ui/data-table.tsx @@ -0,0 +1,152 @@ +import { + ColumnDef, + Row, + SortingState, + flexRender, + getCoreRowModel, + getSortedRowModel, + useReactTable, +} from '@tanstack/react-table'; +import React from 'react'; +import { ActivityIndicator, Dimensions, RefreshControl } from 'react-native'; +import Animated, { FadeInUp, FadeOutUp } from 'react-native-reanimated'; +import { cn } from '~/lib/utils'; +import { + Table, + TableBody, + TableCell, + TableHead, + TableHeader, + TableRow, + TableRowsFlashListProps, + TableRowsList, +} from './table'; + +interface DataTableProps { + columns: ColumnDef[]; + data: TData[]; + onRowPress?: (row: Row) => void; + estimatedItemSize?: number; + ListEmptyComponent?: TableRowsFlashListProps['ListEmptyComponent']; + ListFooterComponent?: TableRowsFlashListProps['ListFooterComponent']; + isRefreshing?: boolean; + onRefresh?: () => void; +} + +/** + * @docs https://tanstack.com/table + */ + +export function DataTable({ + columns, + data, + onRowPress, + estimatedItemSize = 45, + ListEmptyComponent, + ListFooterComponent, + isRefreshing = false, + onRefresh, +}: DataTableProps) { + const [sorting, setSorting] = React.useState([]); + const table = useReactTable({ + data, + columns, + getCoreRowModel: getCoreRowModel(), + onSortingChange: setSorting, + getSortedRowModel: getSortedRowModel(), + state: { + sorting, + }, + }); + + return ( + <> + {isRefreshing && ( + + + + )} + + + {table.getHeaderGroups().map((headerGroup) => ( + + {headerGroup.headers.map((header) => { + return ( + + {header.isPlaceholder + ? null + : flexRender( + header.column.columnDef.header, + header.getContext() + )} + + ); + })} + + ))} + + + + } + renderItem={({ item: row, index }) => { + return ( + { + onRowPress(row); + } + : undefined + } + > + {row.getVisibleCells().map((cell) => ( + + {flexRender( + cell.column.columnDef.cell, + cell.getContext() + )} + + ))} + + ); + }} + /> + +
+ + ); +} + +const { width } = Dimensions.get('window'); + +function getColumnWidth(size: number, length: number) { + const evenWidth = width / length; + return evenWidth > size ? evenWidth : size; +} diff --git a/apps/native/src/components/ui/dialog.tsx b/apps/native/src/components/ui/dialog.tsx new file mode 100644 index 0000000..28875d3 --- /dev/null +++ b/apps/native/src/components/ui/dialog.tsx @@ -0,0 +1,276 @@ +import { useColorScheme } from 'nativewind'; +import React from 'react'; +import { + GestureResponderEvent, + Modal, + Pressable, + StyleSheet, + Text, + View, + ViewStyle, +} from 'react-native'; +import * as Slot from '~/lib/rn-primitives/slot/slot-native'; +import { cn } from '~/lib/utils'; +import { Button } from './button'; + +interface DialogProps { + children: React.ReactNode; + closeOnOverlayPress?: boolean; + defaultOpen?: boolean; + open?: boolean; + setOpen?: React.Dispatch>; +} +interface DialogContext { + visible: boolean; + setVisible: React.Dispatch>; + closeOnOverlayPress: boolean; +} + +const DialogContext = React.createContext({} as DialogContext); + +const Dialog = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef & DialogProps +>( + ( + { + open, + setOpen, + closeOnOverlayPress = true, + defaultOpen = false, + ...props + }, + ref + ) => { + const [visible, setVisible] = React.useState(defaultOpen ?? false); + return ( + + + + ); + } +); + +Dialog.displayName = 'Dialog'; + +function useDialogContext() { + const context = React.useContext(DialogContext); + if (!context) { + throw new Error( + 'Dialog compound components cannot be rendered outside the Dialog component' + ); + } + return context; +} + +const DialogTrigger = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef & { + asChild?: boolean; + } +>(({ onPress, asChild = false, ...props }, ref) => { + const { setVisible } = useDialogContext(); + function handleOnPress(event: GestureResponderEvent) { + setVisible(true); + onPress?.(event); + } + + const Trigger = asChild ? Slot.Pressable : Button; + return ; +}); + +DialogTrigger.displayName = 'DialogTrigger'; + +const DialogContent = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef & { overlayClass?: string } +>( + ( + { + className, + children, + animationType = 'fade', + style: styleProp, + overlayClass, + ...props + }, + ref + ) => { + const { colorScheme } = useColorScheme(); + const { visible, setVisible, closeOnOverlayPress } = useDialogContext(); + const [style, setStyle] = React.useState( + StyleSheet.flatten(styleProp) + ); + + React.useEffect(() => { + setStyle( + StyleSheet.flatten([ + colorScheme === 'dark' ? styles.shadowDark : styles.shadowLight, + styleProp, + ]) + ); + }, [styleProp, colorScheme]); + + return ( + { + setVisible((prev) => !prev); + }} + statusBarTranslucent + {...props} + > + { + setVisible(false); + } + : undefined + } + className={cn( + 'flex-1 justify-center items-center p-2', + animationType !== 'slide' && 'bg-zinc-50/80 dark:bg-zinc-900/80', + overlayClass + )} + > + + {children} + + + + ); + } +); + +DialogContent.displayName = 'DialogContent'; + +const DialogHeader = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => { + return ; +}); + +DialogHeader.displayName = 'DialogHeader'; + +const DialogTitle = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => { + return ( + + ); +}); + +DialogTitle.displayName = 'DialogTitle'; + +const DialogDescription = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => { + return ( + + ); +}); + +DialogDescription.displayName = 'DialogDescription'; + +const DialogFooter = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => { + return ( + + ); +}); + +DialogFooter.displayName = 'DialogFooter'; + +const DialogClose = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef & { + asChild?: boolean; + } +>(({ variant = 'outline', asChild, ...props }, ref) => { + const { setVisible } = useDialogContext(); + const Trigger = asChild ? Slot.Pressable : Button; + return ( + { + setVisible(false); + }} + ref={ref} + {...props} + /> + ); +}); + +DialogClose.displayName = 'DialogClose'; + +export { + Dialog, + DialogClose, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogTitle, + DialogTrigger, +}; + +const styles = StyleSheet.create({ + shadowLight: { + shadowColor: '#000000', + shadowOffset: { + width: 0, + height: 2, + }, + shadowOpacity: 0.1, + shadowRadius: 8, + elevation: 5, + }, + shadowDark: { + shadowColor: '#000000', + shadowOffset: { + width: 0, + height: 2, + }, + shadowOpacity: 0.25, + shadowRadius: 8, + elevation: 5, + }, +}); diff --git a/apps/native/src/components/ui/dropdown-menu.tsx b/apps/native/src/components/ui/dropdown-menu.tsx new file mode 100644 index 0000000..36c4cac --- /dev/null +++ b/apps/native/src/components/ui/dropdown-menu.tsx @@ -0,0 +1,5 @@ +import { Text } from 'react-native'; + +export function DropdownMenu() { + return Dropdown Menu (soon); +} diff --git a/apps/native/src/components/ui/form.tsx b/apps/native/src/components/ui/form.tsx new file mode 100644 index 0000000..39fe8c5 --- /dev/null +++ b/apps/native/src/components/ui/form.tsx @@ -0,0 +1,684 @@ +// This project uses code from shadcn/ui. +// The code is licensed under the MIT License. +// https://github.com/shadcn-ui/ui + +import { CalendarIcon, X } from 'lucide-react-native'; +import React from 'react'; +import { + Controller, + ControllerProps, + FieldPath, + FieldValues, + FormProvider, + Noop, + useFormContext, +} from 'react-hook-form'; +import { Text, View } from 'react-native'; +import Animated, { FadeInDown, FadeOut } from 'react-native-reanimated'; +import { + BottomSheet, + BottomSheetCloseTrigger, + BottomSheetContent, + BottomSheetOpenTrigger, + BottomSheetView, +} from '~/components/ui/bottom-sheet'; +import { Button, buttonTextVariants } from '~/components/ui/button'; +import { Calendar } from '~/components/ui/calendar'; +import { Combobox, ComboboxOption } from '~/components/ui/combobox'; +import { Input } from '~/components/ui/input'; +import { Label } from '~/components/ui/label'; +import { RadioGroup } from '~/components/ui/radio-group'; +import { + RenderSelectItem, + Select, + SelectItem, + SelectList, + SelectOption, + SelectTrigger, +} from '~/components/ui/select'; +import { Switch } from '~/components/ui/switch'; +import { Textarea } from '~/components/ui/textarea'; +import { cn } from '~/lib/utils'; +import { Checkbox } from './checkbox'; + +const Form = FormProvider; + +type FormFieldContextValue< + TFieldValues extends FieldValues = FieldValues, + TName extends FieldPath = FieldPath +> = { + name: TName; +}; + +const FormFieldContext = React.createContext( + {} as FormFieldContextValue +); + +const FormField = < + TFieldValues extends FieldValues = FieldValues, + TName extends FieldPath = FieldPath +>({ + ...props +}: ControllerProps) => { + return ( + + + + ); +}; + +const useFormField = () => { + const fieldContext = React.useContext(FormFieldContext); + const itemContext = React.useContext(FormItemContext); + const { getFieldState, formState, handleSubmit } = useFormContext(); + + const fieldState = getFieldState(fieldContext.name, formState); + + if (!fieldContext) { + throw new Error('useFormField should be used within '); + } + + const { nativeID } = itemContext; + + return { + nativeID, + name: fieldContext.name, + formItemNativeID: `${nativeID}-form-item`, + formDescriptionNativeID: `${nativeID}-form-item-description`, + formMessageNativeID: `${nativeID}-form-item-message`, + handleSubmit, + ...fieldState, + }; +}; + +type FormItemContextValue = { + nativeID: string; +}; + +const FormItemContext = React.createContext( + {} as FormItemContextValue +); + +const FormItem = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => { + const nativeID = React.useId(); + + return ( + + + + ); +}); +FormItem.displayName = 'FormItem'; + +const FormLabel = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => { + const { error, formItemNativeID } = useFormField(); + + return ( +