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
-
-
);
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 (
+ {
+ if (onSelectedItemChange) {
+ onSelectedItemChange(onItemChange(listItem));
+ return;
+ }
+ setSelectedItem(onItemChange(listItem));
+ }}
+ >
+
+
+ {listItem.label}
+
+
+ {isSelected && (
+
+ )}
+
+ );
+ },
+ [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 (
+
+
+ {children}
+
+
+ );
+}
+
+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 (
+
+ );
+});
+FormLabel.displayName = 'FormLabel';
+
+const FormDescription = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => {
+ const { formDescriptionNativeID } = useFormField();
+
+ return (
+
+ );
+});
+FormDescription.displayName = 'FormDescription';
+
+const FormMessage = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, children, ...props }, ref) => {
+ const { error, formMessageNativeID } = useFormField();
+ const body = error ? String(error?.message) : children;
+
+ if (!body) {
+ return null;
+ }
+
+ return (
+
+ {body}
+
+ );
+});
+FormMessage.displayName = 'FormMessage';
+
+type Override = Omit & U;
+
+interface FormFieldFieldProps {
+ name: string;
+ onBlur: Noop;
+ onChange: (val: T) => void;
+ value: T;
+ disabled?: boolean;
+}
+
+type FormItemProps, U> = Override<
+ React.ComponentPropsWithoutRef,
+ FormFieldFieldProps
+> & {
+ label?: string;
+ description?: string;
+};
+
+const FormInput = React.forwardRef<
+ React.ElementRef,
+ FormItemProps
+>(({ label, description, onChange, ...props }, ref) => {
+ const inputRef = React.useRef>(null);
+ const {
+ error,
+ formItemNativeID,
+ formDescriptionNativeID,
+ formMessageNativeID,
+ } = useFormField();
+
+ React.useImperativeHandle(
+ ref,
+ () => {
+ if (!inputRef.current) {
+ return {} as React.ComponentRef;
+ }
+ return inputRef.current;
+ },
+ [inputRef.current]
+ );
+
+ function handleOnLabelPress() {
+ if (!inputRef.current) {
+ return;
+ }
+ if (inputRef.current.isFocused()) {
+ inputRef.current?.blur();
+ } else {
+ inputRef.current?.focus();
+ }
+ }
+
+ return (
+
+ {!!label && (
+
+ {label}
+
+ )}
+
+
+ {!!description && {description}}
+
+
+ );
+});
+
+FormInput.displayName = 'FormInput';
+
+const FormTextarea = React.forwardRef<
+ React.ElementRef,
+ FormItemProps
+>(({ label, description, onChange, ...props }, ref) => {
+ const textareaRef = React.useRef>(null);
+ const {
+ error,
+ formItemNativeID,
+ formDescriptionNativeID,
+ formMessageNativeID,
+ } = useFormField();
+
+ React.useImperativeHandle(
+ ref,
+ () => {
+ if (!textareaRef.current) {
+ return {} as React.ComponentRef;
+ }
+ return textareaRef.current;
+ },
+ [textareaRef.current]
+ );
+
+ function handleOnLabelPress() {
+ if (!textareaRef.current) {
+ return;
+ }
+ if (textareaRef.current.isFocused()) {
+ textareaRef.current?.blur();
+ } else {
+ textareaRef.current?.focus();
+ }
+ }
+
+ return (
+
+ {!!label && (
+
+ {label}
+
+ )}
+
+
+ {!!description && {description}}
+
+
+ );
+});
+
+FormTextarea.displayName = 'FormTextarea';
+
+const FormCheckbox = React.forwardRef<
+ React.ElementRef,
+ FormItemProps
+>(({ label, description, value, onChange, ...props }, ref) => {
+ const checkboxRef = React.useRef>(null);
+ const {
+ error,
+ formItemNativeID,
+ formDescriptionNativeID,
+ formMessageNativeID,
+ } = useFormField();
+
+ React.useImperativeHandle(
+ ref,
+ () => {
+ if (!checkboxRef.current) {
+ return {} as React.ComponentRef;
+ }
+ return checkboxRef.current;
+ },
+ [checkboxRef.current]
+ );
+
+ function handleOnLabelPress() {
+ onChange?.(!value);
+ }
+
+ return (
+