@bearmenu/ui 0.6.0 → 0.7.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-A5ONAJCE.js +102 -0
- package/dist/chunk-AV777R4G.js +43 -0
- package/dist/{chunk-WKMLZDUT.js → chunk-FRPPIXMY.js} +1 -1
- package/dist/{chunk-PSR7N36A.js → chunk-HCM6LKM5.js} +1 -1
- package/dist/{chunk-YJ6OBKZJ.js → chunk-TL4YU2O5.js} +10 -3
- package/dist/chunk-UX6SWAKH.js +320 -0
- package/dist/{chunk-7PPCH42H.js → chunk-VQNDZVI7.js} +1 -1
- package/dist/chunk-WSRWFA6K.js +26 -0
- package/dist/chunk-YLNYXPIH.js +61 -0
- package/dist/components/accordion.js +1 -1
- package/dist/components/alert.d.ts +2 -2
- package/dist/components/badge.d.ts +3 -2
- package/dist/components/badge.js +1 -1
- package/dist/components/button.d.ts +1 -1
- package/dist/components/card.d.ts +2 -2
- package/dist/components/card.js +127 -4
- package/dist/components/combobox.js +2 -2
- package/dist/components/command.d.ts +15 -15
- package/dist/components/command.js +2 -2
- package/dist/components/currency-input.js +1 -1
- package/dist/components/date-input.d.ts +1 -1
- package/dist/components/date-input.js +1 -1
- package/dist/components/dialog.js +1 -1
- package/dist/components/filter-category-row.d.ts +13 -0
- package/dist/components/filter-category-row.js +44 -0
- package/dist/components/input-group.d.ts +2 -0
- package/dist/components/input-group.js +10 -5
- package/dist/components/input.d.ts +1 -0
- package/dist/components/input.js +1 -1
- package/dist/components/item.d.ts +2 -2
- package/dist/components/multi-select-combobox.js +3 -3
- package/dist/components/phone-frame.js +1 -1
- package/dist/components/place-about-tab.d.ts +2 -2
- package/dist/components/place-about-tab.js +4 -3
- package/dist/components/place-details-mobile.js +5 -3
- package/dist/components/place-schedule-week.d.ts +19 -0
- package/dist/components/place-schedule-week.js +4 -0
- package/dist/components/place-types.d.ts +18 -9
- package/dist/components/scroll-fade.d.ts +15 -0
- package/dist/components/scroll-fade.js +121 -0
- package/dist/components/searchable-select.js +2 -2
- package/dist/components/separator.js +3 -26
- package/dist/components/sheet.js +1 -1
- package/dist/components/sliding-panels.d.ts +1 -0
- package/dist/components/sliding-panels.js +22 -6
- package/dist/components/sort-option-list.d.ts +16 -0
- package/dist/components/sort-option-list.js +46 -0
- package/dist/components/spinner.d.ts +2 -2
- package/dist/components/tabs.js +98 -17
- package/dist/components/tag.d.ts +2 -2
- package/dist/components/tag.js +3 -61
- package/dist/components/text.d.ts +3 -3
- package/dist/components/toast.js +1 -1
- package/dist/components/toaster.js +1 -1
- package/package.json +23 -112
- package/src/components/accordion.tsx +1 -1
- package/src/components/badge.tsx +9 -2
- package/src/components/card.tsx +22 -18
- package/src/components/dialog.tsx +1 -1
- package/src/components/filter-category-row.tsx +52 -0
- package/src/components/input-group.tsx +9 -4
- package/src/components/input.tsx +8 -3
- package/src/components/phone-frame.tsx +1 -1
- package/src/components/place-about-tab.stories.tsx +24 -13
- package/src/components/place-about-tab.test.tsx +92 -38
- package/src/components/place-about-tab.tsx +310 -344
- package/src/components/place-schedule-week.tsx +128 -0
- package/src/components/place-types.ts +27 -8
- package/src/components/scroll-fade.stories.tsx +98 -0
- package/src/components/scroll-fade.tsx +132 -0
- package/src/components/sheet.tsx +1 -1
- package/src/components/sliding-panels.tsx +33 -6
- package/src/components/sort-option-list.tsx +57 -0
- package/src/components/tabs.tsx +76 -11
- package/src/components/toast.tsx +1 -1
- package/src/globals.css +77 -19
- package/src/lib/motion.ts +137 -0
- package/dist/chunk-BAZFVSSG.js +0 -279
- package/dist/chunk-RIDXFY6M.js +0 -38
- package/dist/chunk-XK2ZDQVE.js +0 -122
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useEffect, useRef } from "react";
|
|
4
|
+
import { cn } from "../lib/utils";
|
|
5
|
+
import { Text } from "./text";
|
|
6
|
+
import type {
|
|
7
|
+
PlaceSchedule,
|
|
8
|
+
PlaceScheduleWeekLabels,
|
|
9
|
+
WeekdayKey,
|
|
10
|
+
} from "./place-types";
|
|
11
|
+
|
|
12
|
+
const WEEKDAY_KEYS: readonly WeekdayKey[] = [
|
|
13
|
+
"monday",
|
|
14
|
+
"tuesday",
|
|
15
|
+
"wednesday",
|
|
16
|
+
"thursday",
|
|
17
|
+
"friday",
|
|
18
|
+
"saturday",
|
|
19
|
+
"sunday",
|
|
20
|
+
] as const;
|
|
21
|
+
|
|
22
|
+
function formatHour(h: number): string {
|
|
23
|
+
return `${h.toString().padStart(2, "0")}:00`;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/** JS Date.getDay(): Sunday=0 ... Saturday=6 → WEEKDAY_KEYS index where Monday=0 */
|
|
27
|
+
function todayWeekdayIndex(): number {
|
|
28
|
+
const d = new Date().getDay();
|
|
29
|
+
return (d + 6) % 7;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export interface PlaceScheduleWeekProps {
|
|
33
|
+
schedule?: PlaceSchedule;
|
|
34
|
+
labels: PlaceScheduleWeekLabels;
|
|
35
|
+
/**
|
|
36
|
+
* When true, scrolls the today row into view on mount. Use inside a drawer/dialog
|
|
37
|
+
* where the schedule is the only content — auto-scrolling avoids weekend rows
|
|
38
|
+
* being clipped at the bottom of small viewports. Default false: do not auto-scroll
|
|
39
|
+
* when the schedule sits inline on a longer page, where the behaviour disorients
|
|
40
|
+
* the reader.
|
|
41
|
+
*/
|
|
42
|
+
autoScrollToToday?: boolean;
|
|
43
|
+
className?: string;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export function PlaceScheduleWeek({
|
|
47
|
+
schedule,
|
|
48
|
+
labels,
|
|
49
|
+
autoScrollToToday = false,
|
|
50
|
+
className,
|
|
51
|
+
}: PlaceScheduleWeekProps) {
|
|
52
|
+
const todayIdx = todayWeekdayIndex();
|
|
53
|
+
const todayRowRef = useRef<HTMLDivElement | null>(null);
|
|
54
|
+
|
|
55
|
+
useEffect(() => {
|
|
56
|
+
if (autoScrollToToday) {
|
|
57
|
+
todayRowRef.current?.scrollIntoView({ block: "nearest" });
|
|
58
|
+
}
|
|
59
|
+
}, [autoScrollToToday]);
|
|
60
|
+
|
|
61
|
+
if (!schedule) {
|
|
62
|
+
return (
|
|
63
|
+
<Text as="p" variant="body-sm" color="muted">
|
|
64
|
+
{labels.noHoursAvailable}
|
|
65
|
+
</Text>
|
|
66
|
+
);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
return (
|
|
70
|
+
<div className={cn(className)}>
|
|
71
|
+
{WEEKDAY_KEYS.map((dayKey, idx) => {
|
|
72
|
+
const slots = schedule[dayKey] ?? [];
|
|
73
|
+
const isToday = idx === todayIdx;
|
|
74
|
+
const isClosed = slots.length === 0;
|
|
75
|
+
return (
|
|
76
|
+
<div
|
|
77
|
+
key={dayKey}
|
|
78
|
+
ref={isToday ? todayRowRef : null}
|
|
79
|
+
className={cn(
|
|
80
|
+
"relative flex items-start justify-between gap-3 rounded-lg px-3 py-1.5 overflow-hidden",
|
|
81
|
+
isToday && "bg-primary/8"
|
|
82
|
+
)}
|
|
83
|
+
>
|
|
84
|
+
{isToday && (
|
|
85
|
+
<span
|
|
86
|
+
aria-hidden
|
|
87
|
+
className="absolute left-0 top-0 bottom-0 w-1 bg-primary"
|
|
88
|
+
/>
|
|
89
|
+
)}
|
|
90
|
+
<Text
|
|
91
|
+
as="span"
|
|
92
|
+
variant="body-sm"
|
|
93
|
+
weight={isToday ? "semibold" : "medium"}
|
|
94
|
+
className={isToday ? "text-foreground" : "text-muted-foreground"}
|
|
95
|
+
>
|
|
96
|
+
{labels.days[dayKey]}
|
|
97
|
+
</Text>
|
|
98
|
+
<div className="flex flex-col items-end gap-0.5">
|
|
99
|
+
{isClosed ? (
|
|
100
|
+
<Text
|
|
101
|
+
as="span"
|
|
102
|
+
variant="body-sm"
|
|
103
|
+
className="line-through text-muted-foreground/50"
|
|
104
|
+
>
|
|
105
|
+
{labels.closed}
|
|
106
|
+
</Text>
|
|
107
|
+
) : (
|
|
108
|
+
slots.map((slot, i) => (
|
|
109
|
+
<Text
|
|
110
|
+
key={i}
|
|
111
|
+
as="span"
|
|
112
|
+
variant="body-sm"
|
|
113
|
+
className={cn(
|
|
114
|
+
"tabular-nums",
|
|
115
|
+
isToday ? "text-foreground" : "text-muted-foreground"
|
|
116
|
+
)}
|
|
117
|
+
>
|
|
118
|
+
{formatHour(slot.opens_at)} – {formatHour(slot.closes_at)}
|
|
119
|
+
</Text>
|
|
120
|
+
))
|
|
121
|
+
)}
|
|
122
|
+
</div>
|
|
123
|
+
</div>
|
|
124
|
+
);
|
|
125
|
+
})}
|
|
126
|
+
</div>
|
|
127
|
+
);
|
|
128
|
+
}
|
|
@@ -15,6 +15,7 @@ export type PlacePreview = {
|
|
|
15
15
|
review_count?: number
|
|
16
16
|
hours?: PlaceHourBlock[]
|
|
17
17
|
today_schedule?: PlaceHourBlock[]
|
|
18
|
+
schedule?: PlaceSchedule
|
|
18
19
|
location?: {
|
|
19
20
|
address?: string
|
|
20
21
|
geolocation?: { lat: number | string; lon: number | string }
|
|
@@ -45,6 +46,17 @@ export type PlacePreview = {
|
|
|
45
46
|
|
|
46
47
|
export type PlaceHourBlock = { opens_at: number; closes_at: number }
|
|
47
48
|
|
|
49
|
+
export type WeekdayKey =
|
|
50
|
+
| 'monday'
|
|
51
|
+
| 'tuesday'
|
|
52
|
+
| 'wednesday'
|
|
53
|
+
| 'thursday'
|
|
54
|
+
| 'friday'
|
|
55
|
+
| 'saturday'
|
|
56
|
+
| 'sunday'
|
|
57
|
+
|
|
58
|
+
export type PlaceSchedule = Record<WeekdayKey, PlaceHourBlock[]>
|
|
59
|
+
|
|
48
60
|
export type PlacePriceRange = {
|
|
49
61
|
price_range_low?: number
|
|
50
62
|
price_range_high?: number
|
|
@@ -67,15 +79,14 @@ export type PlaceCardLabels = {
|
|
|
67
79
|
* to the raw enum value (with underscores → spaces).
|
|
68
80
|
*/
|
|
69
81
|
export type PlaceAboutTabLabels = {
|
|
70
|
-
|
|
82
|
+
/** Section heading for the editorial prose block (generative summary + review summary + description). */
|
|
71
83
|
description: string
|
|
72
|
-
whatPeopleSay: string
|
|
73
84
|
contactAndLocation: string
|
|
74
|
-
address: string
|
|
75
85
|
instagram: string
|
|
76
86
|
facebook: string
|
|
87
|
+
/** Section heading for the inline weekly schedule. */
|
|
77
88
|
todayHours: string
|
|
78
|
-
|
|
89
|
+
/** Used by the inline schedule's closed-day line-through. */
|
|
79
90
|
statusClosed: string
|
|
80
91
|
typicalBusyness: string
|
|
81
92
|
cuisines: string
|
|
@@ -84,10 +95,10 @@ export type PlaceAboutTabLabels = {
|
|
|
84
95
|
accessibility: string
|
|
85
96
|
parking: string
|
|
86
97
|
nearbyLandmarks: string
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
98
|
+
/** Weekday key → translated day name, consumed by the inline `<PlaceScheduleWeek>`. */
|
|
99
|
+
scheduleDays: Record<WeekdayKey, string>
|
|
100
|
+
/** Fallback shown by `<PlaceScheduleWeek>` when no schedule data is present. */
|
|
101
|
+
noHoursAvailable: string
|
|
91
102
|
amenityLabels: Record<string, string>
|
|
92
103
|
paymentLabels: Record<string, string>
|
|
93
104
|
accessibilityLabels: Record<string, string>
|
|
@@ -99,6 +110,14 @@ export type PlaceAboutTabLabels = {
|
|
|
99
110
|
/** Discriminator for `<PlaceAboutTab>`'s `onLinkClick` callback. */
|
|
100
111
|
export type PlaceLinkKind = 'phone' | 'website' | 'instagram' | 'facebook' | 'address'
|
|
101
112
|
|
|
113
|
+
/** Labels passed to `<PlaceScheduleWeek>`. */
|
|
114
|
+
export type PlaceScheduleWeekLabels = {
|
|
115
|
+
days: Record<WeekdayKey, string>
|
|
116
|
+
closed: string
|
|
117
|
+
/** Shown when the `schedule` prop is missing or empty. */
|
|
118
|
+
noHoursAvailable: string
|
|
119
|
+
}
|
|
120
|
+
|
|
102
121
|
/**
|
|
103
122
|
* Labels passed to `<PlaceDetailsMobile>`. Extends `PlaceAboutTabLabels` with
|
|
104
123
|
* the header/tab-strip strings unique to the mobile place-details surface.
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from "@storybook/react";
|
|
2
|
+
import { ScrollFade } from "./scroll-fade";
|
|
3
|
+
|
|
4
|
+
const meta = {
|
|
5
|
+
title: "Components/ScrollFade",
|
|
6
|
+
component: ScrollFade,
|
|
7
|
+
tags: ["autodocs"],
|
|
8
|
+
parameters: {
|
|
9
|
+
docs: {
|
|
10
|
+
description: {
|
|
11
|
+
component:
|
|
12
|
+
"Scroll-position-aware fade overlay. Hides when at the corresponding edge, fades in when there's more content to scroll. Works vertically or horizontally and matches a chosen surface color.",
|
|
13
|
+
},
|
|
14
|
+
},
|
|
15
|
+
},
|
|
16
|
+
} satisfies Meta<typeof ScrollFade>;
|
|
17
|
+
|
|
18
|
+
export default meta;
|
|
19
|
+
type Story = StoryObj<typeof meta>;
|
|
20
|
+
|
|
21
|
+
const longList = Array.from({ length: 30 });
|
|
22
|
+
const wideList = Array.from({ length: 20 });
|
|
23
|
+
|
|
24
|
+
export const Vertical: Story = {
|
|
25
|
+
args: {
|
|
26
|
+
orientation: "vertical",
|
|
27
|
+
fadeColor: "background",
|
|
28
|
+
size: "md",
|
|
29
|
+
},
|
|
30
|
+
render: (args) => (
|
|
31
|
+
<ScrollFade {...args} className="h-[320px] w-[360px] rounded-lg border" scrollClassName="p-4">
|
|
32
|
+
<ul className="space-y-3">
|
|
33
|
+
{longList.map((_, i) => (
|
|
34
|
+
<li key={i} className="text-sm text-muted-foreground">
|
|
35
|
+
Item #{i + 1} — scroll to reveal the fades at the edges.
|
|
36
|
+
</li>
|
|
37
|
+
))}
|
|
38
|
+
</ul>
|
|
39
|
+
</ScrollFade>
|
|
40
|
+
),
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
export const Horizontal: Story = {
|
|
44
|
+
args: {
|
|
45
|
+
orientation: "horizontal",
|
|
46
|
+
fadeColor: "background",
|
|
47
|
+
size: "md",
|
|
48
|
+
},
|
|
49
|
+
render: (args) => (
|
|
50
|
+
<ScrollFade {...args} className="w-[480px] rounded-lg border" scrollClassName="flex gap-3 p-4">
|
|
51
|
+
{wideList.map((_, i) => (
|
|
52
|
+
<div
|
|
53
|
+
key={i}
|
|
54
|
+
className="flex h-24 w-32 shrink-0 items-center justify-center rounded-md bg-muted text-sm"
|
|
55
|
+
>
|
|
56
|
+
Card {i + 1}
|
|
57
|
+
</div>
|
|
58
|
+
))}
|
|
59
|
+
</ScrollFade>
|
|
60
|
+
),
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
export const InsideCard: Story = {
|
|
64
|
+
args: {
|
|
65
|
+
orientation: "vertical",
|
|
66
|
+
fadeColor: "card",
|
|
67
|
+
size: "md",
|
|
68
|
+
},
|
|
69
|
+
render: (args) => (
|
|
70
|
+
<div className="bg-card text-card-foreground w-[360px] rounded-2xl border shadow-md">
|
|
71
|
+
<div className="border-border border-b p-4 font-semibold">Drawer header</div>
|
|
72
|
+
<ScrollFade {...args} className="h-[280px]" scrollClassName="p-4">
|
|
73
|
+
<ul className="space-y-3">
|
|
74
|
+
{longList.map((_, i) => (
|
|
75
|
+
<li key={i} className="text-sm text-muted-foreground">
|
|
76
|
+
Row #{i + 1} — fades use <code>fadeColor="card"</code> to match the surface.
|
|
77
|
+
</li>
|
|
78
|
+
))}
|
|
79
|
+
</ul>
|
|
80
|
+
</ScrollFade>
|
|
81
|
+
</div>
|
|
82
|
+
),
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
export const NoOverflow: Story = {
|
|
86
|
+
args: {
|
|
87
|
+
orientation: "vertical",
|
|
88
|
+
fadeColor: "background",
|
|
89
|
+
size: "md",
|
|
90
|
+
},
|
|
91
|
+
render: (args) => (
|
|
92
|
+
<ScrollFade {...args} className="h-[200px] w-[320px] rounded-lg border" scrollClassName="p-4">
|
|
93
|
+
<p className="text-sm text-muted-foreground">
|
|
94
|
+
Content fits inside the box — both fades stay hidden.
|
|
95
|
+
</p>
|
|
96
|
+
</ScrollFade>
|
|
97
|
+
),
|
|
98
|
+
};
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import * as React from "react";
|
|
4
|
+
import { cn } from "../lib/utils";
|
|
5
|
+
|
|
6
|
+
type Orientation = "vertical" | "horizontal";
|
|
7
|
+
type FadeColor = "background" | "card" | "muted";
|
|
8
|
+
type Size = "sm" | "md" | "lg";
|
|
9
|
+
|
|
10
|
+
interface ScrollFadeProps extends React.HTMLAttributes<HTMLDivElement> {
|
|
11
|
+
orientation?: Orientation;
|
|
12
|
+
fadeColor?: FadeColor;
|
|
13
|
+
size?: Size;
|
|
14
|
+
/** Class names applied to the inner scrollable element (where overflow lives). */
|
|
15
|
+
scrollClassName?: string;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
// Static maps so Tailwind can statically analyze the class names.
|
|
19
|
+
const fromMap: Record<FadeColor, string> = {
|
|
20
|
+
background: "from-background",
|
|
21
|
+
card: "from-card",
|
|
22
|
+
muted: "from-muted",
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
const sizeMap: Record<Orientation, Record<Size, string>> = {
|
|
26
|
+
vertical: { sm: "h-6", md: "h-12", lg: "h-20" },
|
|
27
|
+
horizontal: { sm: "w-8", md: "w-16", lg: "w-28" },
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
const ScrollFade = React.forwardRef<HTMLDivElement, ScrollFadeProps>(
|
|
31
|
+
(
|
|
32
|
+
{
|
|
33
|
+
orientation = "vertical",
|
|
34
|
+
fadeColor = "background",
|
|
35
|
+
size = "md",
|
|
36
|
+
className,
|
|
37
|
+
scrollClassName,
|
|
38
|
+
children,
|
|
39
|
+
...props
|
|
40
|
+
},
|
|
41
|
+
ref
|
|
42
|
+
) => {
|
|
43
|
+
const innerRef = React.useRef<HTMLDivElement>(null);
|
|
44
|
+
React.useImperativeHandle(ref, () => innerRef.current as HTMLDivElement);
|
|
45
|
+
|
|
46
|
+
const [atStart, setAtStart] = React.useState(true);
|
|
47
|
+
const [atEnd, setAtEnd] = React.useState(true);
|
|
48
|
+
|
|
49
|
+
const update = React.useCallback(() => {
|
|
50
|
+
const el = innerRef.current;
|
|
51
|
+
if (!el) return;
|
|
52
|
+
if (orientation === "vertical") {
|
|
53
|
+
const overflow = el.scrollHeight - el.clientHeight;
|
|
54
|
+
setAtStart(el.scrollTop <= 1);
|
|
55
|
+
setAtEnd(overflow <= 1 || el.scrollTop >= overflow - 1);
|
|
56
|
+
} else {
|
|
57
|
+
const overflow = el.scrollWidth - el.clientWidth;
|
|
58
|
+
setAtStart(el.scrollLeft <= 1);
|
|
59
|
+
setAtEnd(overflow <= 1 || el.scrollLeft >= overflow - 1);
|
|
60
|
+
}
|
|
61
|
+
}, [orientation]);
|
|
62
|
+
|
|
63
|
+
React.useEffect(() => {
|
|
64
|
+
const el = innerRef.current;
|
|
65
|
+
if (!el) return;
|
|
66
|
+
update();
|
|
67
|
+
el.addEventListener("scroll", update, { passive: true });
|
|
68
|
+
const ro = new ResizeObserver(update);
|
|
69
|
+
ro.observe(el);
|
|
70
|
+
return () => {
|
|
71
|
+
el.removeEventListener("scroll", update);
|
|
72
|
+
ro.disconnect();
|
|
73
|
+
};
|
|
74
|
+
}, [update]);
|
|
75
|
+
|
|
76
|
+
const fromClass = fromMap[fadeColor];
|
|
77
|
+
const fadeSize = sizeMap[orientation][size];
|
|
78
|
+
|
|
79
|
+
return (
|
|
80
|
+
<div
|
|
81
|
+
className={cn(
|
|
82
|
+
"relative isolate flex",
|
|
83
|
+
orientation === "vertical" ? "flex-col" : "flex-row",
|
|
84
|
+
className
|
|
85
|
+
)}
|
|
86
|
+
>
|
|
87
|
+
<div
|
|
88
|
+
ref={innerRef}
|
|
89
|
+
className={cn(
|
|
90
|
+
"min-h-0 min-w-0 flex-1",
|
|
91
|
+
orientation === "vertical" ? "overflow-y-auto" : "overflow-x-auto",
|
|
92
|
+
scrollClassName
|
|
93
|
+
)}
|
|
94
|
+
{...props}
|
|
95
|
+
>
|
|
96
|
+
{children}
|
|
97
|
+
</div>
|
|
98
|
+
<div
|
|
99
|
+
aria-hidden
|
|
100
|
+
className={cn(
|
|
101
|
+
"pointer-events-none absolute z-10 transition-opacity duration-300",
|
|
102
|
+
// `from-30%` adds a solid plateau before the gradient starts
|
|
103
|
+
// fading — without it the gradient is a pure linear ramp and only
|
|
104
|
+
// the very top pixel reads as solid, leaving the cut edge visible.
|
|
105
|
+
// -4px inset pulls the fade past the container edge so the very
|
|
106
|
+
// edge of clipped content is covered by the solid plateau.
|
|
107
|
+
orientation === "vertical"
|
|
108
|
+
? `inset-x-0 -top-1 ${fadeSize} bg-gradient-to-b from-30%`
|
|
109
|
+
: `inset-y-0 -left-1 ${fadeSize} bg-gradient-to-r from-30%`,
|
|
110
|
+
fromClass,
|
|
111
|
+
atStart ? "opacity-0" : "opacity-100"
|
|
112
|
+
)}
|
|
113
|
+
/>
|
|
114
|
+
<div
|
|
115
|
+
aria-hidden
|
|
116
|
+
className={cn(
|
|
117
|
+
"pointer-events-none absolute z-10 transition-opacity duration-300",
|
|
118
|
+
orientation === "vertical"
|
|
119
|
+
? `inset-x-0 -bottom-1 ${fadeSize} bg-gradient-to-t from-30%`
|
|
120
|
+
: `inset-y-0 -right-1 ${fadeSize} bg-gradient-to-l from-30%`,
|
|
121
|
+
fromClass,
|
|
122
|
+
atEnd ? "opacity-0" : "opacity-100"
|
|
123
|
+
)}
|
|
124
|
+
/>
|
|
125
|
+
</div>
|
|
126
|
+
);
|
|
127
|
+
}
|
|
128
|
+
);
|
|
129
|
+
ScrollFade.displayName = "ScrollFade";
|
|
130
|
+
|
|
131
|
+
export { ScrollFade };
|
|
132
|
+
export type { ScrollFadeProps };
|
package/src/components/sheet.tsx
CHANGED
|
@@ -30,7 +30,7 @@ const SheetOverlay = React.forwardRef<
|
|
|
30
30
|
SheetOverlay.displayName = SheetPrimitive.Overlay.displayName;
|
|
31
31
|
|
|
32
32
|
const sheetVariants = cva(
|
|
33
|
-
"fixed z-[1000] gap-4 bg-background border-border p-6 shadow-2xl
|
|
33
|
+
"fixed z-[1000] gap-4 bg-background border-border p-6 shadow-2xl data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=open]:duration-[var(--duration-deliberate)] data-[state=closed]:duration-[var(--duration-exit-standard)] data-[state=open]:ease-[var(--ease-enter)] data-[state=closed]:ease-[var(--ease-exit)]",
|
|
34
34
|
{
|
|
35
35
|
variants: {
|
|
36
36
|
side: {
|
|
@@ -12,6 +12,11 @@ export interface Panel {
|
|
|
12
12
|
showBackButton?: boolean;
|
|
13
13
|
backButtonLabel?: string;
|
|
14
14
|
onBack?: () => void;
|
|
15
|
+
// When set, the panel is treated as a child of `parentId`. Siblings (panels
|
|
16
|
+
// sharing the same parent) stay off-screen on the right when not active —
|
|
17
|
+
// they no longer slide through during non-adjacent transitions. The default
|
|
18
|
+
// back button navigates to `parentId` instead of the previous array index.
|
|
19
|
+
parentId?: string;
|
|
15
20
|
}
|
|
16
21
|
|
|
17
22
|
export interface SlidingPanelsProps {
|
|
@@ -32,6 +37,23 @@ export const SlidingPanels = ({
|
|
|
32
37
|
const panelRefs = useRef<(HTMLDivElement | null)[]>([]);
|
|
33
38
|
const [containerHeight, setContainerHeight] = useState<number | undefined>();
|
|
34
39
|
|
|
40
|
+
// Tree mode: if any panel declares parentId, position based on hierarchy
|
|
41
|
+
// (ancestors slide left, everything else stays right). Otherwise fall back
|
|
42
|
+
// to the legacy index-based stack.
|
|
43
|
+
const usesTreeModel = panels.some((p) => p.parentId);
|
|
44
|
+
|
|
45
|
+
const activeAncestors = React.useMemo(() => {
|
|
46
|
+
if (!usesTreeModel) return new Set<string>();
|
|
47
|
+
const ancestors = new Set<string>();
|
|
48
|
+
let cursor = panels.find((p) => p.id === currentPanel);
|
|
49
|
+
while (cursor?.parentId) {
|
|
50
|
+
ancestors.add(cursor.parentId);
|
|
51
|
+
const nextId: string = cursor.parentId;
|
|
52
|
+
cursor = panels.find((p) => p.id === nextId);
|
|
53
|
+
}
|
|
54
|
+
return ancestors;
|
|
55
|
+
}, [panels, currentPanel, usesTreeModel]);
|
|
56
|
+
|
|
35
57
|
// Measure active panel height on panel change and whenever content resizes
|
|
36
58
|
useLayoutEffect(() => {
|
|
37
59
|
const activeEl = panelRefs.current[currentIndex];
|
|
@@ -46,13 +68,16 @@ export const SlidingPanels = ({
|
|
|
46
68
|
}, [currentIndex]);
|
|
47
69
|
|
|
48
70
|
const handleBack = () => {
|
|
49
|
-
const previousPanel = panels[currentIndex - 1];
|
|
50
|
-
if (!previousPanel) return;
|
|
51
71
|
if (currentPanelData?.onBack) {
|
|
52
72
|
currentPanelData.onBack();
|
|
53
|
-
|
|
54
|
-
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
75
|
+
if (currentPanelData?.parentId) {
|
|
76
|
+
onPanelChange(currentPanelData.parentId);
|
|
77
|
+
return;
|
|
55
78
|
}
|
|
79
|
+
const previousPanel = panels[currentIndex - 1];
|
|
80
|
+
if (previousPanel) onPanelChange(previousPanel.id);
|
|
56
81
|
};
|
|
57
82
|
|
|
58
83
|
return (
|
|
@@ -65,8 +90,10 @@ export const SlidingPanels = ({
|
|
|
65
90
|
>
|
|
66
91
|
{panels.map((panel, index) => {
|
|
67
92
|
const isActive = panel.id === currentPanel;
|
|
68
|
-
const isLeft =
|
|
69
|
-
|
|
93
|
+
const isLeft = usesTreeModel
|
|
94
|
+
? activeAncestors.has(panel.id)
|
|
95
|
+
: index < currentIndex;
|
|
96
|
+
const isRight = !isActive && !isLeft;
|
|
70
97
|
|
|
71
98
|
return (
|
|
72
99
|
<div
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { Check } from "lucide-react";
|
|
4
|
+
import { cn } from "../lib/utils";
|
|
5
|
+
|
|
6
|
+
export type SortOptionListItem<T extends string> = {
|
|
7
|
+
value: T;
|
|
8
|
+
label: string;
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
interface SortOptionListProps<T extends string> {
|
|
12
|
+
options: SortOptionListItem<T>[];
|
|
13
|
+
value: T;
|
|
14
|
+
onChange: (value: T) => void;
|
|
15
|
+
ariaLabel: string;
|
|
16
|
+
className?: string;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function SortOptionList<T extends string>({
|
|
20
|
+
options,
|
|
21
|
+
value,
|
|
22
|
+
onChange,
|
|
23
|
+
ariaLabel,
|
|
24
|
+
className,
|
|
25
|
+
}: SortOptionListProps<T>) {
|
|
26
|
+
return (
|
|
27
|
+
<div
|
|
28
|
+
role="radiogroup"
|
|
29
|
+
aria-label={ariaLabel}
|
|
30
|
+
className={cn("px-2 pb-6", className)}
|
|
31
|
+
>
|
|
32
|
+
{options.map((option) => {
|
|
33
|
+
const isActive = option.value === value;
|
|
34
|
+
return (
|
|
35
|
+
<button
|
|
36
|
+
key={option.value}
|
|
37
|
+
type="button"
|
|
38
|
+
role="radio"
|
|
39
|
+
aria-checked={isActive}
|
|
40
|
+
onClick={() => onChange(option.value)}
|
|
41
|
+
className={cn(
|
|
42
|
+
"flex w-full items-center justify-between rounded-lg px-3 py-3 text-left text-sm cursor-pointer",
|
|
43
|
+
"transition-colors duration-150 motion-reduce:transition-none",
|
|
44
|
+
"focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary/30",
|
|
45
|
+
isActive
|
|
46
|
+
? "bg-accent/10 text-accent font-semibold"
|
|
47
|
+
: "text-foreground hover:bg-muted/50"
|
|
48
|
+
)}
|
|
49
|
+
>
|
|
50
|
+
<span>{option.label}</span>
|
|
51
|
+
{isActive && <Check className="h-4 w-4" aria-hidden />}
|
|
52
|
+
</button>
|
|
53
|
+
);
|
|
54
|
+
})}
|
|
55
|
+
</div>
|
|
56
|
+
);
|
|
57
|
+
}
|