@bearmenu/ui 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/chunk-2CUFS5LF.js +80 -0
- package/dist/chunk-3GWWZKX7.js +33 -0
- package/dist/chunk-3VMDCCCX.js +10 -0
- package/dist/chunk-4AVS6CV4.js +89 -0
- package/dist/chunk-4JOV7FXW.js +17 -0
- package/dist/chunk-4KOMHDZD.js +89 -0
- package/dist/chunk-4UO73SKC.js +26 -0
- package/dist/chunk-7H54PTVH.js +116 -0
- package/dist/chunk-CW24PLDW.js +24 -0
- package/dist/chunk-EBAMCZDW.js +112 -0
- package/dist/chunk-ERMIA7NP.js +106 -0
- package/dist/chunk-ESO2EM3L.js +46 -0
- package/dist/chunk-EUHEIWHZ.js +32 -0
- package/dist/chunk-FEK5XGZW.js +8 -0
- package/dist/chunk-NKHH23YK.js +31 -0
- package/dist/chunk-R47NEOSI.js +78 -0
- package/dist/chunk-UXAYXRFN.js +69 -0
- package/dist/chunk-X6GATUI2.js +19 -0
- package/dist/components/accordion.d.ts +9 -0
- package/dist/components/accordion.js +53 -0
- package/dist/components/alert-dialog.d.ts +23 -0
- package/dist/components/alert-dialog.js +98 -0
- package/dist/components/alert.d.ts +11 -0
- package/dist/components/alert.js +52 -0
- package/dist/components/aspect-ratio.d.ts +6 -0
- package/dist/components/aspect-ratio.js +2 -0
- package/dist/components/avatar.d.ts +8 -0
- package/dist/components/avatar.js +47 -0
- package/dist/components/badge.d.ts +13 -0
- package/dist/components/badge.js +3 -0
- package/dist/components/breadcrumb.d.ts +14 -0
- package/dist/components/breadcrumb.js +96 -0
- package/dist/components/button.d.ts +16 -0
- package/dist/components/button.js +4 -0
- package/dist/components/calendar.d.ts +11 -0
- package/dist/components/calendar.js +56 -0
- package/dist/components/card.d.ts +13 -0
- package/dist/components/card.js +4 -0
- package/dist/components/carousel.d.ts +24 -0
- package/dist/components/carousel.js +198 -0
- package/dist/components/checkbox.d.ts +6 -0
- package/dist/components/checkbox.js +25 -0
- package/dist/components/collapsible.d.ts +8 -0
- package/dist/components/collapsible.js +26 -0
- package/dist/components/command.d.ts +84 -0
- package/dist/components/command.js +4 -0
- package/dist/components/currency-input.d.ts +17 -0
- package/dist/components/currency-input.js +121 -0
- package/dist/components/dialog.d.ts +22 -0
- package/dist/components/dialog.js +3 -0
- package/dist/components/drawer.d.ts +27 -0
- package/dist/components/drawer.js +85 -0
- package/dist/components/dropdown-menu.d.ts +28 -0
- package/dist/components/dropdown-menu.js +211 -0
- package/dist/components/empty-state.d.ts +22 -0
- package/dist/components/empty-state.js +81 -0
- package/dist/components/form.d.ts +28 -0
- package/dist/components/form.js +97 -0
- package/dist/components/haptic-button.d.ts +12 -0
- package/dist/components/haptic-button.js +17 -0
- package/dist/components/input-otp.d.ts +14 -0
- package/dist/components/input-otp.js +62 -0
- package/dist/components/input.d.ts +6 -0
- package/dist/components/input.js +3 -0
- package/dist/components/label.d.ts +8 -0
- package/dist/components/label.js +3 -0
- package/dist/components/lazy-markdown.d.ts +11 -0
- package/dist/components/lazy-markdown.js +13 -0
- package/dist/components/link-button.d.ts +30 -0
- package/dist/components/link-button.js +17 -0
- package/dist/components/markdown-renderer.d.ts +11 -0
- package/dist/components/markdown-renderer.js +10 -0
- package/dist/components/multi-select-combobox.d.ts +23 -0
- package/dist/components/multi-select-combobox.js +114 -0
- package/dist/components/pagination.d.ts +18 -0
- package/dist/components/pagination.js +101 -0
- package/dist/components/popover.d.ts +8 -0
- package/dist/components/popover.js +3 -0
- package/dist/components/progress.d.ts +6 -0
- package/dist/components/progress.js +30 -0
- package/dist/components/radio-group.d.ts +7 -0
- package/dist/components/radio-group.js +30 -0
- package/dist/components/rating.d.ts +15 -0
- package/dist/components/rating.js +120 -0
- package/dist/components/scroll-area.d.ts +7 -0
- package/dist/components/scroll-area.js +44 -0
- package/dist/components/search-bar.d.ts +17 -0
- package/dist/components/search-bar.js +92 -0
- package/dist/components/searchable-select.d.ts +22 -0
- package/dist/components/searchable-select.js +104 -0
- package/dist/components/select.d.ts +15 -0
- package/dist/components/select.js +129 -0
- package/dist/components/separator.d.ts +6 -0
- package/dist/components/separator.js +25 -0
- package/dist/components/sheet.d.ts +29 -0
- package/dist/components/sheet.js +96 -0
- package/dist/components/skeleton-card.d.ts +13 -0
- package/dist/components/skeleton-card.js +154 -0
- package/dist/components/skeleton.d.ts +6 -0
- package/dist/components/skeleton.js +3 -0
- package/dist/components/slider.d.ts +6 -0
- package/dist/components/slider.js +24 -0
- package/dist/components/sliding-panels.d.ts +19 -0
- package/dist/components/sliding-panels.js +75 -0
- package/dist/components/sonner.d.ts +6 -0
- package/dist/components/sonner.js +23 -0
- package/dist/components/sticky-container.d.ts +44 -0
- package/dist/components/sticky-container.js +39 -0
- package/dist/components/switch.d.ts +6 -0
- package/dist/components/switch.js +31 -0
- package/dist/components/table.d.ts +12 -0
- package/dist/components/table.js +81 -0
- package/dist/components/tabs.d.ts +12 -0
- package/dist/components/tabs.js +55 -0
- package/dist/components/text.d.ts +31 -0
- package/dist/components/text.js +3 -0
- package/dist/components/textarea.d.ts +6 -0
- package/dist/components/textarea.js +23 -0
- package/dist/components/toast.d.ts +18 -0
- package/dist/components/toast.js +3 -0
- package/dist/components/toaster.d.ts +5 -0
- package/dist/components/toaster.js +25 -0
- package/dist/components/toggle-group.d.ts +12 -0
- package/dist/components/toggle-group.js +72 -0
- package/dist/components/toggle.d.ts +13 -0
- package/dist/components/toggle.js +3 -0
- package/dist/components/tooltip.d.ts +9 -0
- package/dist/components/tooltip.js +26 -0
- package/dist/components/use-mobile.d.ts +3 -0
- package/dist/components/use-mobile.js +19 -0
- package/dist/components/use-toast.d.ts +42 -0
- package/dist/components/use-toast.js +2 -0
- package/dist/hooks/use-haptic.d.ts +22 -0
- package/dist/hooks/use-haptic.js +2 -0
- package/dist/hooks/use-mobile.d.ts +3 -0
- package/dist/hooks/use-mobile.js +19 -0
- package/dist/hooks/use-mounted-theme.d.ts +6 -0
- package/dist/hooks/use-mounted-theme.js +18 -0
- package/dist/hooks/use-sticky.d.ts +18 -0
- package/dist/hooks/use-sticky.js +2 -0
- package/dist/lib/utils.d.ts +5 -0
- package/dist/lib/utils.js +2 -0
- package/package.json +203 -0
- package/src/components/accordion.tsx +57 -0
- package/src/components/alert-dialog.tsx +116 -0
- package/src/components/alert.tsx +57 -0
- package/src/components/aspect-ratio.tsx +9 -0
- package/src/components/avatar.tsx +49 -0
- package/src/components/badge.tsx +38 -0
- package/src/components/breadcrumb.tsx +102 -0
- package/src/components/button.tsx +94 -0
- package/src/components/calendar.tsx +60 -0
- package/src/components/card.tsx +67 -0
- package/src/components/carousel.tsx +230 -0
- package/src/components/checkbox.tsx +27 -0
- package/src/components/collapsible.tsx +28 -0
- package/src/components/command.tsx +148 -0
- package/src/components/currency-input.tsx +158 -0
- package/src/components/dialog.tsx +103 -0
- package/src/components/drawer.tsx +99 -0
- package/src/components/dropdown-menu.tsx +228 -0
- package/src/components/empty-state.tsx +118 -0
- package/src/components/form.tsx +152 -0
- package/src/components/haptic-button.tsx +19 -0
- package/src/components/input-otp.tsx +70 -0
- package/src/components/input.tsx +23 -0
- package/src/components/label.tsx +20 -0
- package/src/components/lazy-markdown.tsx +20 -0
- package/src/components/link-button.tsx +40 -0
- package/src/components/markdown-renderer.tsx +20 -0
- package/src/components/multi-select-combobox.tsx +148 -0
- package/src/components/pagination.tsx +106 -0
- package/src/components/popover.tsx +30 -0
- package/src/components/progress.tsx +27 -0
- package/src/components/radio-group.tsx +37 -0
- package/src/components/rating.tsx +136 -0
- package/src/components/scroll-area.tsx +45 -0
- package/src/components/search-bar.tsx +113 -0
- package/src/components/searchable-select.tsx +134 -0
- package/src/components/select.tsx +152 -0
- package/src/components/separator.tsx +25 -0
- package/src/components/sheet.tsx +121 -0
- package/src/components/skeleton-card.tsx +282 -0
- package/src/components/skeleton.tsx +17 -0
- package/src/components/slider.tsx +24 -0
- package/src/components/sliding-panels.tsx +99 -0
- package/src/components/sonner.tsx +25 -0
- package/src/components/sticky-container.tsx +88 -0
- package/src/components/switch.tsx +28 -0
- package/src/components/table.tsx +90 -0
- package/src/components/tabs.tsx +72 -0
- package/src/components/text.test.tsx +192 -0
- package/src/components/text.tsx +114 -0
- package/src/components/textarea.tsx +22 -0
- package/src/components/toast.tsx +129 -0
- package/src/components/toaster.tsx +31 -0
- package/src/components/toggle-group.tsx +69 -0
- package/src/components/toggle.tsx +46 -0
- package/src/components/tooltip.tsx +29 -0
- package/src/components/use-mobile.tsx +19 -0
- package/src/components/use-toast.ts +180 -0
- package/src/globals.css +409 -0
- package/src/hooks/use-haptic.ts +118 -0
- package/src/hooks/use-mobile.ts +19 -0
- package/src/hooks/use-mounted-theme.ts +20 -0
- package/src/hooks/use-sticky.ts +47 -0
- package/src/lib/utils.ts +6 -0
|
@@ -0,0 +1,282 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import * as React from "react";
|
|
4
|
+
import { cn } from "../lib/utils";
|
|
5
|
+
import { Skeleton } from "./skeleton";
|
|
6
|
+
import { AspectRatio } from "./aspect-ratio";
|
|
7
|
+
|
|
8
|
+
interface SkeletonCardProps {
|
|
9
|
+
variant?: "experience" | "restaurant" | "generic" | "place" | "dish" | "drink" | "event";
|
|
10
|
+
showImage?: boolean;
|
|
11
|
+
showRating?: boolean;
|
|
12
|
+
showPrice?: boolean;
|
|
13
|
+
lines?: number; // for description lines
|
|
14
|
+
className?: string;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const SkeletonCard = React.forwardRef<HTMLDivElement, SkeletonCardProps>(
|
|
18
|
+
(
|
|
19
|
+
{
|
|
20
|
+
variant = "experience",
|
|
21
|
+
showImage = true,
|
|
22
|
+
showRating = true,
|
|
23
|
+
showPrice = true,
|
|
24
|
+
lines = 2,
|
|
25
|
+
className,
|
|
26
|
+
...props
|
|
27
|
+
},
|
|
28
|
+
ref
|
|
29
|
+
) => {
|
|
30
|
+
const renderPlaceCardSkeleton = () => (
|
|
31
|
+
<div className="glass transition-all duration-300 ease-in-out h-full flex flex-col w-full rounded-3xl border border-transparent">
|
|
32
|
+
<div className="p-4 flex-1 flex flex-col">
|
|
33
|
+
{/* SECTION 1 - PLACE HEADER */}
|
|
34
|
+
<div className="flex items-start gap-3 mb-3">
|
|
35
|
+
{/* Place Photo or Icon - 48x48px */}
|
|
36
|
+
<Skeleton className="w-12 h-12 rounded-lg flex-shrink-0" />
|
|
37
|
+
|
|
38
|
+
{/* Place Name & Rating */}
|
|
39
|
+
<div className="flex-1 min-w-0">
|
|
40
|
+
{/* Title - matches font-bold text-lg leading-tight (lh ~22.5px) */}
|
|
41
|
+
<Skeleton className="h-5 w-3/4 mb-2" />
|
|
42
|
+
{/* Rating row - matches text-sm */}
|
|
43
|
+
<div className="flex items-center gap-2">
|
|
44
|
+
<Skeleton className="h-4 w-16" />
|
|
45
|
+
<Skeleton className="h-3 w-1" />
|
|
46
|
+
<Skeleton className="h-4 w-12" />
|
|
47
|
+
</div>
|
|
48
|
+
</div>
|
|
49
|
+
</div>
|
|
50
|
+
|
|
51
|
+
{/* SECTION 2 - PRICE RANGES */}
|
|
52
|
+
<div className="mb-3">
|
|
53
|
+
<div className="flex items-center gap-4 text-xs">
|
|
54
|
+
<Skeleton className="h-3 w-20" />
|
|
55
|
+
<Skeleton className="h-3 w-20" />
|
|
56
|
+
</div>
|
|
57
|
+
</div>
|
|
58
|
+
|
|
59
|
+
{/* SECTION 3 - LOCATION & STATUS */}
|
|
60
|
+
<div className="mb-3">
|
|
61
|
+
<div className="flex items-center gap-2">
|
|
62
|
+
<Skeleton className="h-3.5 w-16" />
|
|
63
|
+
<Skeleton className="h-3.5 w-1" />
|
|
64
|
+
<Skeleton className="h-3.5 w-20" />
|
|
65
|
+
</div>
|
|
66
|
+
</div>
|
|
67
|
+
|
|
68
|
+
{/* SECTION 4 - DESCRIPTION PREVIEW */}
|
|
69
|
+
<div className="mb-4">
|
|
70
|
+
<Skeleton className="h-3 w-full mb-1" />
|
|
71
|
+
<Skeleton className="h-3 w-2/3" />
|
|
72
|
+
</div>
|
|
73
|
+
</div>
|
|
74
|
+
</div>
|
|
75
|
+
);
|
|
76
|
+
|
|
77
|
+
const renderDishCardSkeleton = () => (
|
|
78
|
+
<div className="glass transition-all duration-300 ease-in-out h-full flex flex-col w-full border rounded-lg">
|
|
79
|
+
<div className="p-3 h-full flex flex-col">
|
|
80
|
+
{/* SECTION 1 - DISH HEADER (icon + name + heart button) */}
|
|
81
|
+
<div className="flex items-start gap-3 mb-3">
|
|
82
|
+
{/* Dish Icon - 48x48px */}
|
|
83
|
+
<Skeleton className="w-12 h-12 rounded-lg flex-shrink-0" />
|
|
84
|
+
|
|
85
|
+
{/* Dish Name */}
|
|
86
|
+
<div className="flex-1 min-w-0">
|
|
87
|
+
{/* Title - matches font-bold text-lg leading-tight line-clamp-2 */}
|
|
88
|
+
<Skeleton className="h-5 w-3/4 mb-1" />
|
|
89
|
+
<Skeleton className="h-4 w-1/2" />
|
|
90
|
+
</div>
|
|
91
|
+
|
|
92
|
+
{/* Heart button placeholder - matches h-8 w-8 min-h-[44px] min-w-[44px] */}
|
|
93
|
+
<Skeleton className="h-8 w-8 rounded-md flex-shrink-0" />
|
|
94
|
+
</div>
|
|
95
|
+
|
|
96
|
+
{/* SECTION 2 - LOCATION BADGE */}
|
|
97
|
+
<div className="mb-3 flex items-center gap-2">
|
|
98
|
+
<Skeleton className="h-5 w-24 rounded-full" />
|
|
99
|
+
</div>
|
|
100
|
+
|
|
101
|
+
{/* SECTION 3 - BADGES ROW (For You / Rare / Qty) */}
|
|
102
|
+
<div className="flex items-center gap-2 mb-2">
|
|
103
|
+
<Skeleton className="h-5 w-16 rounded-full" />
|
|
104
|
+
<Skeleton className="h-5 w-12 rounded-full" />
|
|
105
|
+
</div>
|
|
106
|
+
|
|
107
|
+
{/* SECTION 4 - INGREDIENTS PREVIEW */}
|
|
108
|
+
<div className="mb-3">
|
|
109
|
+
<Skeleton className="h-3 w-full mb-1" />
|
|
110
|
+
<Skeleton className="h-3 w-2/3" />
|
|
111
|
+
</div>
|
|
112
|
+
|
|
113
|
+
{/* SECTION 5 - FOOTER (PRICE AND BUTTON) */}
|
|
114
|
+
<div className="flex items-center justify-between mt-auto">
|
|
115
|
+
<Skeleton className="h-6 w-16" />
|
|
116
|
+
<Skeleton className="h-8 w-24 rounded-md" />
|
|
117
|
+
</div>
|
|
118
|
+
</div>
|
|
119
|
+
</div>
|
|
120
|
+
);
|
|
121
|
+
|
|
122
|
+
const renderDrinkCardSkeleton = () => (
|
|
123
|
+
<div className="glass transition-all duration-300 ease-in-out w-full h-full border rounded-lg">
|
|
124
|
+
<div className="p-3 h-full flex flex-col">
|
|
125
|
+
{/* HEADER (icon + name + heart button) */}
|
|
126
|
+
<div className="flex items-start gap-3 mb-3">
|
|
127
|
+
{/* Drink Icon - 48x48px */}
|
|
128
|
+
<Skeleton className="w-12 h-12 rounded-lg flex-shrink-0" />
|
|
129
|
+
|
|
130
|
+
{/* Drink Name */}
|
|
131
|
+
<div className="flex-1 min-w-0">
|
|
132
|
+
{/* Title - matches font-bold text-lg leading-tight */}
|
|
133
|
+
<Skeleton className="h-5 w-3/4 mb-1" />
|
|
134
|
+
<Skeleton className="h-4 w-1/2" />
|
|
135
|
+
</div>
|
|
136
|
+
|
|
137
|
+
{/* Heart button placeholder - matches h-8 w-8 min-h-[44px] min-w-[44px] */}
|
|
138
|
+
<Skeleton className="h-8 w-8 rounded-md flex-shrink-0" />
|
|
139
|
+
</div>
|
|
140
|
+
|
|
141
|
+
{/* LOCATION BADGE */}
|
|
142
|
+
<div className="mb-3 flex items-center gap-2">
|
|
143
|
+
<Skeleton className="h-5 w-24 rounded-full" />
|
|
144
|
+
</div>
|
|
145
|
+
|
|
146
|
+
{/* BADGES ROW */}
|
|
147
|
+
<div className="flex items-center gap-2 mb-2">
|
|
148
|
+
<Skeleton className="h-5 w-16 rounded-full" />
|
|
149
|
+
<Skeleton className="h-5 w-12 rounded-full" />
|
|
150
|
+
</div>
|
|
151
|
+
|
|
152
|
+
{/* INGREDIENTS PREVIEW */}
|
|
153
|
+
<div className="mb-3">
|
|
154
|
+
<Skeleton className="h-3 w-full mb-1" />
|
|
155
|
+
<Skeleton className="h-3 w-2/3" />
|
|
156
|
+
</div>
|
|
157
|
+
|
|
158
|
+
{/* FOOTER WITH PRICE AND BUTTON */}
|
|
159
|
+
<div className="flex items-center justify-between mt-auto">
|
|
160
|
+
<Skeleton className="h-6 w-16" />
|
|
161
|
+
<Skeleton className="h-8 w-24 rounded-md" />
|
|
162
|
+
</div>
|
|
163
|
+
</div>
|
|
164
|
+
</div>
|
|
165
|
+
);
|
|
166
|
+
|
|
167
|
+
const renderEventCardSkeleton = () => (
|
|
168
|
+
<div className="glass transition-all duration-300 ease-in-out w-full overflow-hidden rounded-xl">
|
|
169
|
+
{/* Image area - matches aspect-[16/10] of EventCarouselCard */}
|
|
170
|
+
<div className="relative aspect-[16/10] overflow-hidden bg-white/[0.06]">
|
|
171
|
+
<Skeleton className="absolute inset-0 w-full h-full" />
|
|
172
|
+
</div>
|
|
173
|
+
{/* Content area - matches flex flex-col gap-1 p-3 */}
|
|
174
|
+
<div className="flex flex-col gap-1 p-3">
|
|
175
|
+
<Skeleton className="h-4 w-3/4" />
|
|
176
|
+
<Skeleton className="h-3 w-1/2" />
|
|
177
|
+
</div>
|
|
178
|
+
</div>
|
|
179
|
+
);
|
|
180
|
+
|
|
181
|
+
const renderExperienceSkeleton = () => (
|
|
182
|
+
<div className="glass rounded-xl overflow-hidden transition-all duration-300">
|
|
183
|
+
{/* Image skeleton - 16:9 aspect ratio */}
|
|
184
|
+
{showImage && (
|
|
185
|
+
<AspectRatio ratio={16 / 9}>
|
|
186
|
+
<Skeleton className="w-full h-full" />
|
|
187
|
+
</AspectRatio>
|
|
188
|
+
)}
|
|
189
|
+
|
|
190
|
+
{/* Content skeleton */}
|
|
191
|
+
<div className="p-6 space-y-4">
|
|
192
|
+
{/* Restaurant name */}
|
|
193
|
+
<Skeleton className="h-6 w-3/4" />
|
|
194
|
+
|
|
195
|
+
{/* Description lines */}
|
|
196
|
+
<div className="space-y-2">
|
|
197
|
+
{Array.from({ length: lines }).map((_, i) => (
|
|
198
|
+
<Skeleton key={i} className={cn("h-4", i === lines - 1 ? "w-1/2" : "w-full")} />
|
|
199
|
+
))}
|
|
200
|
+
</div>
|
|
201
|
+
|
|
202
|
+
{/* Price and rating row */}
|
|
203
|
+
<div className="flex items-center justify-between">
|
|
204
|
+
{showPrice && <Skeleton className="h-5 w-20" />}
|
|
205
|
+
{showRating && <Skeleton className="h-4 w-16" />}
|
|
206
|
+
</div>
|
|
207
|
+
|
|
208
|
+
{/* Quick details */}
|
|
209
|
+
<div className="flex items-center justify-between text-sm">
|
|
210
|
+
<Skeleton className="h-4 w-24" />
|
|
211
|
+
<Skeleton className="h-4 w-16" />
|
|
212
|
+
</div>
|
|
213
|
+
</div>
|
|
214
|
+
</div>
|
|
215
|
+
);
|
|
216
|
+
|
|
217
|
+
const renderRestaurantSkeleton = () => (
|
|
218
|
+
<div className="glass rounded-xl overflow-hidden transition-all duration-300">
|
|
219
|
+
{/* Image skeleton */}
|
|
220
|
+
{showImage && (
|
|
221
|
+
<AspectRatio ratio={16 / 9}>
|
|
222
|
+
<Skeleton className="w-full h-full" />
|
|
223
|
+
</AspectRatio>
|
|
224
|
+
)}
|
|
225
|
+
|
|
226
|
+
{/* Content skeleton */}
|
|
227
|
+
<div className="p-6 space-y-4">
|
|
228
|
+
{/* Restaurant name */}
|
|
229
|
+
<Skeleton className="h-6 w-2/3" />
|
|
230
|
+
|
|
231
|
+
{/* Description */}
|
|
232
|
+
<div className="space-y-2">
|
|
233
|
+
<Skeleton className="h-4 w-full" />
|
|
234
|
+
<Skeleton className="h-4 w-3/4" />
|
|
235
|
+
</div>
|
|
236
|
+
|
|
237
|
+
{/* Rating and price */}
|
|
238
|
+
<div className="flex items-center justify-between">
|
|
239
|
+
{showRating && <Skeleton className="h-4 w-20" />}
|
|
240
|
+
{showPrice && <Skeleton className="h-5 w-16" />}
|
|
241
|
+
</div>
|
|
242
|
+
</div>
|
|
243
|
+
</div>
|
|
244
|
+
);
|
|
245
|
+
|
|
246
|
+
const renderGenericSkeleton = () => (
|
|
247
|
+
<div className="glass rounded-xl p-6 space-y-4">
|
|
248
|
+
{/* Title */}
|
|
249
|
+
<Skeleton className="h-6 w-1/2" />
|
|
250
|
+
|
|
251
|
+
{/* Content lines */}
|
|
252
|
+
<div className="space-y-2">
|
|
253
|
+
{Array.from({ length: lines }).map((_, i) => (
|
|
254
|
+
<Skeleton key={i} className={cn("h-4", i === lines - 1 ? "w-1/3" : "w-full")} />
|
|
255
|
+
))}
|
|
256
|
+
</div>
|
|
257
|
+
|
|
258
|
+
{/* Footer content */}
|
|
259
|
+
<div className="flex items-center justify-between">
|
|
260
|
+
<Skeleton className="h-4 w-20" />
|
|
261
|
+
<Skeleton className="h-4 w-16" />
|
|
262
|
+
</div>
|
|
263
|
+
</div>
|
|
264
|
+
);
|
|
265
|
+
|
|
266
|
+
return (
|
|
267
|
+
<div ref={ref} className={cn("w-full", className)} {...props}>
|
|
268
|
+
{variant === "place" && renderPlaceCardSkeleton()}
|
|
269
|
+
{variant === "dish" && renderDishCardSkeleton()}
|
|
270
|
+
{variant === "drink" && renderDrinkCardSkeleton()}
|
|
271
|
+
{variant === "event" && renderEventCardSkeleton()}
|
|
272
|
+
{variant === "experience" && renderExperienceSkeleton()}
|
|
273
|
+
{variant === "restaurant" && renderRestaurantSkeleton()}
|
|
274
|
+
{variant === "generic" && renderGenericSkeleton()}
|
|
275
|
+
</div>
|
|
276
|
+
);
|
|
277
|
+
}
|
|
278
|
+
);
|
|
279
|
+
|
|
280
|
+
SkeletonCard.displayName = "SkeletonCard";
|
|
281
|
+
|
|
282
|
+
export { SkeletonCard };
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type React from "react";
|
|
2
|
+
import { cn } from "../lib/utils";
|
|
3
|
+
|
|
4
|
+
function Skeleton({ className, ...props }: React.HTMLAttributes<HTMLDivElement>) {
|
|
5
|
+
return (
|
|
6
|
+
<div
|
|
7
|
+
className={cn(
|
|
8
|
+
"animate-pulse rounded-xl glass border border-border/30 relative overflow-hidden",
|
|
9
|
+
"before:absolute before:inset-0 before:-translate-x-full before:animate-[shimmer_2s_infinite] before:bg-gradient-to-r before:from-transparent before:via-white/10 before:to-transparent",
|
|
10
|
+
className
|
|
11
|
+
)}
|
|
12
|
+
{...props}
|
|
13
|
+
/>
|
|
14
|
+
);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export { Skeleton };
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import * as React from "react";
|
|
4
|
+
import * as SliderPrimitive from "@radix-ui/react-slider";
|
|
5
|
+
import { cn } from "../lib/utils";
|
|
6
|
+
|
|
7
|
+
const Slider = React.forwardRef<
|
|
8
|
+
React.ElementRef<typeof SliderPrimitive.Root>,
|
|
9
|
+
React.ComponentPropsWithoutRef<typeof SliderPrimitive.Root>
|
|
10
|
+
>(({ className, ...props }, ref) => (
|
|
11
|
+
<SliderPrimitive.Root
|
|
12
|
+
ref={ref}
|
|
13
|
+
className={cn("relative flex w-full touch-none select-none items-center", className)}
|
|
14
|
+
{...props}
|
|
15
|
+
>
|
|
16
|
+
<SliderPrimitive.Track className="relative h-2 w-full grow overflow-hidden rounded-full glass border border-border/50">
|
|
17
|
+
<SliderPrimitive.Range className="absolute h-full bg-white shadow-lg" />
|
|
18
|
+
</SliderPrimitive.Track>
|
|
19
|
+
<SliderPrimitive.Thumb className="block h-5 w-5 rounded-full border-2 border-primary bg-primary ring-offset-background transition-all duration-300 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 hover:shadow-lg hover:shadow-primary/20 hover:scale-110" />
|
|
20
|
+
</SliderPrimitive.Root>
|
|
21
|
+
));
|
|
22
|
+
Slider.displayName = SliderPrimitive.Root.displayName;
|
|
23
|
+
|
|
24
|
+
export { Slider };
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import * as React from "react";
|
|
4
|
+
import { useLayoutEffect, useRef, useState } from "react";
|
|
5
|
+
import { Button } from "./button";
|
|
6
|
+
import { ChevronLeft } from "lucide-react";
|
|
7
|
+
import { cn } from "../lib/utils";
|
|
8
|
+
|
|
9
|
+
export interface Panel {
|
|
10
|
+
id: string;
|
|
11
|
+
content: React.ReactNode;
|
|
12
|
+
showBackButton?: boolean;
|
|
13
|
+
backButtonLabel?: string;
|
|
14
|
+
onBack?: () => void;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export interface SlidingPanelsProps {
|
|
18
|
+
panels: Panel[];
|
|
19
|
+
currentPanel: string;
|
|
20
|
+
onPanelChange: (panelId: string) => void;
|
|
21
|
+
className?: string;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export const SlidingPanels = ({
|
|
25
|
+
panels,
|
|
26
|
+
currentPanel,
|
|
27
|
+
onPanelChange,
|
|
28
|
+
className,
|
|
29
|
+
}: SlidingPanelsProps) => {
|
|
30
|
+
const currentIndex = panels.findIndex((p) => p.id === currentPanel);
|
|
31
|
+
const currentPanelData = panels[currentIndex];
|
|
32
|
+
const panelRefs = useRef<(HTMLDivElement | null)[]>([]);
|
|
33
|
+
const [containerHeight, setContainerHeight] = useState<number | undefined>();
|
|
34
|
+
|
|
35
|
+
// Measure active panel height on panel change and whenever content resizes
|
|
36
|
+
useLayoutEffect(() => {
|
|
37
|
+
const activeEl = panelRefs.current[currentIndex];
|
|
38
|
+
if (!activeEl) return;
|
|
39
|
+
|
|
40
|
+
const updateHeight = () => setContainerHeight(activeEl.scrollHeight);
|
|
41
|
+
updateHeight();
|
|
42
|
+
|
|
43
|
+
const ro = new ResizeObserver(updateHeight);
|
|
44
|
+
ro.observe(activeEl);
|
|
45
|
+
return () => ro.disconnect();
|
|
46
|
+
}, [currentIndex]);
|
|
47
|
+
|
|
48
|
+
const handleBack = () => {
|
|
49
|
+
if (currentIndex > 0) {
|
|
50
|
+
const previousPanel = panels[currentIndex - 1];
|
|
51
|
+
if (currentPanelData?.onBack) {
|
|
52
|
+
currentPanelData.onBack();
|
|
53
|
+
} else {
|
|
54
|
+
onPanelChange(previousPanel.id);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
return (
|
|
60
|
+
<div
|
|
61
|
+
className={cn("relative overflow-hidden transition-[height] duration-300", className)}
|
|
62
|
+
style={{ height: containerHeight }}
|
|
63
|
+
>
|
|
64
|
+
{panels.map((panel, index) => {
|
|
65
|
+
const isActive = panel.id === currentPanel;
|
|
66
|
+
const isLeft = index < currentIndex;
|
|
67
|
+
const isRight = index > currentIndex;
|
|
68
|
+
|
|
69
|
+
return (
|
|
70
|
+
<div
|
|
71
|
+
key={panel.id}
|
|
72
|
+
ref={(el) => {
|
|
73
|
+
panelRefs.current[index] = el;
|
|
74
|
+
}}
|
|
75
|
+
className={cn(
|
|
76
|
+
"absolute top-0 left-0 w-full transition-transform duration-300",
|
|
77
|
+
isActive && "translate-x-0",
|
|
78
|
+
isLeft && "-translate-x-full",
|
|
79
|
+
isRight && "translate-x-full"
|
|
80
|
+
)}
|
|
81
|
+
aria-hidden={!isActive}
|
|
82
|
+
>
|
|
83
|
+
{panel.showBackButton && (
|
|
84
|
+
<div className="flex items-center gap-2 mb-4">
|
|
85
|
+
<Button variant="ghost" size="icon" onClick={handleBack}>
|
|
86
|
+
<ChevronLeft className="h-4 w-4" />
|
|
87
|
+
</Button>
|
|
88
|
+
{panel.backButtonLabel && (
|
|
89
|
+
<h3 className="font-semibold">{panel.backButtonLabel}</h3>
|
|
90
|
+
)}
|
|
91
|
+
</div>
|
|
92
|
+
)}
|
|
93
|
+
{panel.content}
|
|
94
|
+
</div>
|
|
95
|
+
);
|
|
96
|
+
})}
|
|
97
|
+
</div>
|
|
98
|
+
);
|
|
99
|
+
};
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useTheme } from "next-themes";
|
|
4
|
+
import { Toaster as Sonner, ToasterProps } from "sonner";
|
|
5
|
+
|
|
6
|
+
const Toaster = ({ ...props }: ToasterProps) => {
|
|
7
|
+
const { theme = "system" } = useTheme();
|
|
8
|
+
|
|
9
|
+
return (
|
|
10
|
+
<Sonner
|
|
11
|
+
theme={theme as ToasterProps["theme"]}
|
|
12
|
+
className="toaster group"
|
|
13
|
+
style={
|
|
14
|
+
{
|
|
15
|
+
"--normal-bg": "var(--popover)",
|
|
16
|
+
"--normal-text": "var(--popover-foreground)",
|
|
17
|
+
"--normal-border": "var(--border)",
|
|
18
|
+
} as React.CSSProperties
|
|
19
|
+
}
|
|
20
|
+
{...props}
|
|
21
|
+
/>
|
|
22
|
+
);
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
export { Toaster };
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useSticky } from "../hooks/use-sticky";
|
|
4
|
+
import { cn } from "../lib/utils";
|
|
5
|
+
|
|
6
|
+
interface StickyContainerProps {
|
|
7
|
+
/** Content to render inside the sticky container */
|
|
8
|
+
children: React.ReactNode;
|
|
9
|
+
/** Additional className for the sticky element */
|
|
10
|
+
className?: string;
|
|
11
|
+
/** Additional className for the inner container wrapper */
|
|
12
|
+
containerClassName?: string;
|
|
13
|
+
/**
|
|
14
|
+
* Top offset when sticky.
|
|
15
|
+
* Defaults to "sticky-header-safe" which uses the global CSS class
|
|
16
|
+
* to handle safe-area-inset-top correctly on all devices.
|
|
17
|
+
*/
|
|
18
|
+
topOffset?: string;
|
|
19
|
+
/** z-index when sticky (default: z-40) */
|
|
20
|
+
zIndex?: string;
|
|
21
|
+
/** Whether to show the background/border/shadow when sticky (default: true) */
|
|
22
|
+
showStickyStyles?: boolean;
|
|
23
|
+
/** Custom className for when sticky */
|
|
24
|
+
stickyClassName?: string;
|
|
25
|
+
/** Hide children until sticky state is reached */
|
|
26
|
+
hideUntilSticky?: boolean;
|
|
27
|
+
/** Optional ID for the sticky container (useful for measuring height for nested sticky elements) */
|
|
28
|
+
id?: string;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Reusable sticky container component with automatic background/shadow when stuck
|
|
33
|
+
* Uses native CSS position: sticky with IntersectionObserver to detect sticky state
|
|
34
|
+
*
|
|
35
|
+
* Supports nested sticky elements - children can use `sticky bottom-0` to stick
|
|
36
|
+
* to the bottom of this container when scrolling.
|
|
37
|
+
*
|
|
38
|
+
* Automatically handles PWA safe area insets via CSS class.
|
|
39
|
+
*
|
|
40
|
+
* @example
|
|
41
|
+
* <StickyContainer>
|
|
42
|
+
* <SearchBar />
|
|
43
|
+
* <div className="sticky bottom-0">Categories</div>
|
|
44
|
+
* </StickyContainer>
|
|
45
|
+
*/
|
|
46
|
+
export const StickyContainer = ({
|
|
47
|
+
children,
|
|
48
|
+
className,
|
|
49
|
+
containerClassName,
|
|
50
|
+
topOffset = "sticky-header-safe",
|
|
51
|
+
zIndex = "z-40",
|
|
52
|
+
showStickyStyles = true,
|
|
53
|
+
stickyClassName,
|
|
54
|
+
hideUntilSticky = false,
|
|
55
|
+
id,
|
|
56
|
+
}: StickyContainerProps) => {
|
|
57
|
+
const { isSticky, ref, sentinelRef } = useSticky();
|
|
58
|
+
|
|
59
|
+
return (
|
|
60
|
+
<>
|
|
61
|
+
{/* Sentinel element - placed 1px above sticky element to detect when it becomes stuck */}
|
|
62
|
+
<div ref={sentinelRef} className="h-px w-full" aria-hidden="true" />
|
|
63
|
+
|
|
64
|
+
{/* Sticky container */}
|
|
65
|
+
<div
|
|
66
|
+
ref={ref}
|
|
67
|
+
id={id}
|
|
68
|
+
className={cn(
|
|
69
|
+
"sticky px-4 transition-all duration-200",
|
|
70
|
+
hideUntilSticky
|
|
71
|
+
? isSticky
|
|
72
|
+
? "py-4 opacity-100 translate-y-0"
|
|
73
|
+
: "py-0 opacity-0 -translate-y-2 pointer-events-none"
|
|
74
|
+
: "py-4",
|
|
75
|
+
topOffset,
|
|
76
|
+
zIndex,
|
|
77
|
+
showStickyStyles && isSticky
|
|
78
|
+
? stickyClassName || "bg-background border-b border-border"
|
|
79
|
+
: "bg-transparent border-b border-transparent",
|
|
80
|
+
className
|
|
81
|
+
)}
|
|
82
|
+
>
|
|
83
|
+
{/* Inner container wrapper - allows passing custom container classes */}
|
|
84
|
+
<div className={cn(containerClassName, "lg:px-4")}>{children}</div>
|
|
85
|
+
</div>
|
|
86
|
+
</>
|
|
87
|
+
);
|
|
88
|
+
};
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import * as React from "react";
|
|
4
|
+
import * as SwitchPrimitive from "@radix-ui/react-switch";
|
|
5
|
+
import { cn } from "../lib/utils";
|
|
6
|
+
|
|
7
|
+
const Switch = React.forwardRef<
|
|
8
|
+
React.ElementRef<typeof SwitchPrimitive.Root>,
|
|
9
|
+
React.ComponentPropsWithoutRef<typeof SwitchPrimitive.Root>
|
|
10
|
+
>(({ className, ...props }, ref) => (
|
|
11
|
+
<SwitchPrimitive.Root
|
|
12
|
+
className={cn(
|
|
13
|
+
"peer inline-flex h-6 w-11 shrink-0 cursor-pointer items-center rounded-full border-2 border-transparent transition-all duration-300 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:bg-primary data-[state=unchecked]:bg-input glass data-[state=checked]:shadow-lg data-[state=checked]:shadow-primary/20 hover:shadow-md",
|
|
14
|
+
className
|
|
15
|
+
)}
|
|
16
|
+
{...props}
|
|
17
|
+
ref={ref}
|
|
18
|
+
>
|
|
19
|
+
<SwitchPrimitive.Thumb
|
|
20
|
+
className={cn(
|
|
21
|
+
"pointer-events-none block h-5 w-5 rounded-full bg-background shadow-lg ring-0 transition-all duration-300 data-[state=checked]:translate-x-5 data-[state=unchecked]:translate-x-0 data-[state=checked]:bg-primary-foreground backdrop-blur-md border border-border/50"
|
|
22
|
+
)}
|
|
23
|
+
/>
|
|
24
|
+
</SwitchPrimitive.Root>
|
|
25
|
+
));
|
|
26
|
+
Switch.displayName = SwitchPrimitive.Root.displayName;
|
|
27
|
+
|
|
28
|
+
export { Switch };
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
import { cn } from "../lib/utils";
|
|
3
|
+
|
|
4
|
+
const Table = React.forwardRef<HTMLTableElement, React.HTMLAttributes<HTMLTableElement>>(
|
|
5
|
+
({ className, ...props }, ref) => (
|
|
6
|
+
<div className="relative w-full overflow-auto">
|
|
7
|
+
<table ref={ref} className={cn("w-full caption-bottom text-sm", className)} {...props} />
|
|
8
|
+
</div>
|
|
9
|
+
)
|
|
10
|
+
);
|
|
11
|
+
Table.displayName = "Table";
|
|
12
|
+
|
|
13
|
+
const TableHeader = React.forwardRef<
|
|
14
|
+
HTMLTableSectionElement,
|
|
15
|
+
React.HTMLAttributes<HTMLTableSectionElement>
|
|
16
|
+
>(({ className, ...props }, ref) => (
|
|
17
|
+
<thead ref={ref} className={cn("[&_tr]:border-b", className)} {...props} />
|
|
18
|
+
));
|
|
19
|
+
TableHeader.displayName = "TableHeader";
|
|
20
|
+
|
|
21
|
+
const TableBody = React.forwardRef<
|
|
22
|
+
HTMLTableSectionElement,
|
|
23
|
+
React.HTMLAttributes<HTMLTableSectionElement>
|
|
24
|
+
>(({ className, ...props }, ref) => (
|
|
25
|
+
<tbody ref={ref} className={cn("[&_tr:last-child]:border-0", className)} {...props} />
|
|
26
|
+
));
|
|
27
|
+
TableBody.displayName = "TableBody";
|
|
28
|
+
|
|
29
|
+
const TableFooter = React.forwardRef<
|
|
30
|
+
HTMLTableSectionElement,
|
|
31
|
+
React.HTMLAttributes<HTMLTableSectionElement>
|
|
32
|
+
>(({ className, ...props }, ref) => (
|
|
33
|
+
<tfoot
|
|
34
|
+
ref={ref}
|
|
35
|
+
className={cn("border-t bg-muted/50 font-medium [&>tr]:last:border-b-0", className)}
|
|
36
|
+
{...props}
|
|
37
|
+
/>
|
|
38
|
+
));
|
|
39
|
+
TableFooter.displayName = "TableFooter";
|
|
40
|
+
|
|
41
|
+
const TableRow = React.forwardRef<HTMLTableRowElement, React.HTMLAttributes<HTMLTableRowElement>>(
|
|
42
|
+
({ className, ...props }, ref) => (
|
|
43
|
+
<tr
|
|
44
|
+
ref={ref}
|
|
45
|
+
className={cn(
|
|
46
|
+
"border-b border-border/50 transition-all duration-300 hover:bg-muted/30 hover:backdrop-blur-sm data-[state=selected]:bg-muted/50",
|
|
47
|
+
className
|
|
48
|
+
)}
|
|
49
|
+
{...props}
|
|
50
|
+
/>
|
|
51
|
+
)
|
|
52
|
+
);
|
|
53
|
+
TableRow.displayName = "TableRow";
|
|
54
|
+
|
|
55
|
+
const TableHead = React.forwardRef<
|
|
56
|
+
HTMLTableCellElement,
|
|
57
|
+
React.ThHTMLAttributes<HTMLTableCellElement>
|
|
58
|
+
>(({ className, ...props }, ref) => (
|
|
59
|
+
<th
|
|
60
|
+
ref={ref}
|
|
61
|
+
className={cn(
|
|
62
|
+
"h-12 px-4 text-left align-middle font-medium text-muted-foreground [&:has([role=checkbox])]:pr-0",
|
|
63
|
+
className
|
|
64
|
+
)}
|
|
65
|
+
{...props}
|
|
66
|
+
/>
|
|
67
|
+
));
|
|
68
|
+
TableHead.displayName = "TableHead";
|
|
69
|
+
|
|
70
|
+
const TableCell = React.forwardRef<
|
|
71
|
+
HTMLTableCellElement,
|
|
72
|
+
React.TdHTMLAttributes<HTMLTableCellElement>
|
|
73
|
+
>(({ className, ...props }, ref) => (
|
|
74
|
+
<td
|
|
75
|
+
ref={ref}
|
|
76
|
+
className={cn("p-4 align-middle [&:has([role=checkbox])]:pr-0", className)}
|
|
77
|
+
{...props}
|
|
78
|
+
/>
|
|
79
|
+
));
|
|
80
|
+
TableCell.displayName = "TableCell";
|
|
81
|
+
|
|
82
|
+
const TableCaption = React.forwardRef<
|
|
83
|
+
HTMLTableCaptionElement,
|
|
84
|
+
React.HTMLAttributes<HTMLTableCaptionElement>
|
|
85
|
+
>(({ className, ...props }, ref) => (
|
|
86
|
+
<caption ref={ref} className={cn("mt-4 text-sm text-muted-foreground", className)} {...props} />
|
|
87
|
+
));
|
|
88
|
+
TableCaption.displayName = "TableCaption";
|
|
89
|
+
|
|
90
|
+
export { Table, TableHeader, TableBody, TableFooter, TableHead, TableRow, TableCell, TableCaption };
|