@bearmenu/ui 0.8.24 → 0.8.26
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-5LTILG3L.js → chunk-5HHRVJOT.js} +14 -1
- package/dist/{chunk-SDWN6L4O.js → chunk-AHHDC7ML.js} +1 -1
- package/dist/{chunk-I4RUOAU7.js → chunk-AHL45DD2.js} +11 -38
- package/dist/{chunk-TIQNM4XD.js → chunk-F3XK34DV.js} +25 -1
- package/dist/{chunk-5XREWOBB.js → chunk-IEIJP73R.js} +14 -1
- package/dist/chunk-L3CDXVEP.js +56 -0
- package/dist/{chunk-BKKQ7GCZ.js → chunk-QG6I2ILT.js} +1 -1
- package/dist/{chunk-CZK2MRS2.js → chunk-VEFKZIK6.js} +13 -3
- package/dist/components/alert-dialog.js +1 -1
- package/dist/components/badge.d.ts +1 -1
- package/dist/components/badge.js +1 -1
- package/dist/components/banner.d.ts +36 -0
- package/dist/components/banner.js +29 -0
- package/dist/components/booking-row.d.ts +60 -0
- package/dist/components/booking-row.js +103 -0
- package/dist/components/booking-summary-card.d.ts +38 -0
- package/dist/components/booking-summary-card.js +67 -0
- package/dist/components/button.d.ts +2 -2
- package/dist/components/button.js +1 -1
- package/dist/components/calendar.d.ts +17 -3
- package/dist/components/calendar.js +100 -12
- package/dist/components/carousel.js +1 -1
- package/dist/components/checkbox.d.ts +9 -2
- package/dist/components/checkbox.js +1 -1
- package/dist/components/coach-marks.js +1 -1
- package/dist/components/combobox.js +1 -1
- package/dist/components/concept-convergence.js +2 -2
- package/dist/components/currency-input.js +1 -1
- package/dist/components/date-input.js +1 -1
- package/dist/components/drift-rail.d.ts +2 -29
- package/dist/components/drift-rail.js +1 -3
- package/dist/components/empty-state.js +1 -1
- package/dist/components/event-card.js +1 -1
- package/dist/components/event-compact-row.js +1 -1
- package/dist/components/event-detail-panel.js +2 -2
- package/dist/components/event-index-row.js +1 -1
- package/dist/components/event-list-row-wide.js +3 -3
- package/dist/components/event-list-row.js +2 -2
- package/dist/components/field.d.ts +15 -1
- package/dist/components/field.js +62 -21
- package/dist/components/filter-category-row.js +1 -1
- package/dist/components/hold-countdown.d.ts +43 -0
- package/dist/components/hold-countdown.js +90 -0
- package/dist/components/input.d.ts +1 -0
- package/dist/components/input.js +1 -1
- package/dist/components/link-button.js +1 -1
- package/dist/components/menu-family-block.js +1 -1
- package/dist/components/menu-index-list.js +1 -1
- package/dist/components/menu-item.js +3 -3
- package/dist/components/motion-icon-set.d.ts +2 -1
- package/dist/components/motion-icon-set.js +27 -1
- package/dist/components/multi-select-combobox.js +3 -3
- package/dist/components/multi-select-menu.js +1 -1
- package/dist/components/pagination.js +1 -1
- package/dist/components/party-counter.d.ts +36 -0
- package/dist/components/party-counter.js +61 -0
- package/dist/components/place-card.js +1 -1
- package/dist/components/place-details-mobile.js +1 -1
- package/dist/components/poster-tape.d.ts +2 -15
- package/dist/components/poster-tape.js +3 -12
- package/dist/components/searchable-select.js +1 -1
- package/dist/components/signature-chip.js +2 -2
- package/dist/components/sliding-panels.js +1 -1
- package/dist/components/textarea.d.ts +10 -3
- package/dist/components/textarea.js +21 -6
- package/dist/components/time-slot.d.ts +73 -0
- package/dist/components/time-slot.js +106 -0
- package/dist/components/toggle-group.js +15 -2
- package/dist/components/toggle.d.ts +1 -1
- package/dist/components/toggle.js +1 -1
- package/dist/components/top-ranking-entry.js +3 -3
- package/dist/components/weekday-chip.d.ts +33 -0
- package/dist/components/weekday-chip.js +48 -0
- package/package.json +207 -23
- package/src/components/badge.stories.tsx +17 -0
- package/src/components/badge.tsx +15 -0
- package/src/components/banner.stories.tsx +36 -0
- package/src/components/banner.tsx +58 -0
- package/src/components/booking-details-primitives.test.tsx +75 -0
- package/src/components/booking-primitives.test.tsx +180 -0
- package/src/components/booking-row.stories.tsx +66 -0
- package/src/components/booking-row.tsx +168 -0
- package/src/components/booking-summary-card.stories.tsx +41 -0
- package/src/components/booking-summary-card.tsx +109 -0
- package/src/components/button.stories.tsx +23 -0
- package/src/components/button.tsx +13 -0
- package/src/components/calendar.stories.tsx +27 -0
- package/src/components/calendar.tsx +110 -3
- package/src/components/checkbox.tsx +49 -23
- package/src/components/drift-rail.stories.tsx +1 -29
- package/src/components/drift-rail.tsx +4 -71
- package/src/components/field.stories.tsx +51 -1
- package/src/components/field.tsx +68 -9
- package/src/components/hold-countdown.stories.tsx +68 -0
- package/src/components/hold-countdown.tsx +140 -0
- package/src/components/input.tsx +13 -2
- package/src/components/motion-icon-set.stories.tsx +2 -0
- package/src/components/motion-icon-set.tsx +42 -0
- package/src/components/party-counter.stories.tsx +57 -0
- package/src/components/party-counter.tsx +93 -0
- package/src/components/poster-tape.stories.tsx +0 -29
- package/src/components/poster-tape.tsx +5 -21
- package/src/components/textarea.tsx +25 -7
- package/src/components/time-slot.stories.tsx +85 -0
- package/src/components/time-slot.tsx +186 -0
- package/src/components/toggle-group.stories.tsx +27 -0
- package/src/components/toggle-group.tsx +14 -1
- package/src/components/toggle.tsx +24 -0
- package/src/components/weekday-chip.stories.tsx +54 -0
- package/src/components/weekday-chip.tsx +75 -0
- package/dist/chunk-NMEVJT5Q.js +0 -26
- package/src/components/drift-rail.test.tsx +0 -125
- package/src/components/poster-tape.test.tsx +0 -42
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
import { CalendarDays, Clock, MapPin } from "lucide-react";
|
|
3
|
+
import { cn } from "../lib/utils";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* BookingSummaryCard — the booking at a glance on the confirmation screen
|
|
7
|
+
* ("Your table is booked"): the venue, then when, how many, where you asked
|
|
8
|
+
* to sit and the address, then the booking code.
|
|
9
|
+
*
|
|
10
|
+
* Measured from the booking design (`.cardx` + `.rowi`): a card with a 1px
|
|
11
|
+
* hairline, 14px radius, 15px inset and a 1px/3px shadow. The venue at 16px
|
|
12
|
+
* bold. Rows 11px below it, 7px apart: a 14px icon (1.8 stroke, muted) in a
|
|
13
|
+
* 16px column, 10px gap, text at 13px in the secondary ink. Then a
|
|
14
|
+
* `line-soft` divider 13px below with 13px above the code row: "BOOKING CODE"
|
|
15
|
+
* at 10.5px uppercase, muted, and the code in mono at 15px bold, pushed right.
|
|
16
|
+
*
|
|
17
|
+
* Every string arrives formatted and localised; the card only lays them out.
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
export type BookingSummaryCardLabels = {
|
|
21
|
+
/** "Booking code" */
|
|
22
|
+
code: string;
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
export type BookingSummaryCardProps = {
|
|
26
|
+
venueName: string;
|
|
27
|
+
/** "Fri 14 August" */
|
|
28
|
+
date: string;
|
|
29
|
+
/** "19:30 · 2 people" */
|
|
30
|
+
timeAndParty: string;
|
|
31
|
+
/** "Terrace (requested)" — omitted when there is no preference. */
|
|
32
|
+
seating?: string;
|
|
33
|
+
/** "Strada Iuliu Maniu 12" */
|
|
34
|
+
address?: string;
|
|
35
|
+
/** "BM-8421" */
|
|
36
|
+
code: string;
|
|
37
|
+
labels: BookingSummaryCardLabels;
|
|
38
|
+
className?: string;
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
function Row({ icon, children }: { icon: React.ReactNode; children: React.ReactNode }) {
|
|
42
|
+
return (
|
|
43
|
+
<div className="flex items-center gap-2.5 text-[13px] leading-4 text-ink-secondary [&+&]:mt-[7px]">
|
|
44
|
+
<span className="flex w-4 shrink-0 justify-center text-muted-foreground" aria-hidden>
|
|
45
|
+
{icon}
|
|
46
|
+
</span>
|
|
47
|
+
<span>{children}</span>
|
|
48
|
+
</div>
|
|
49
|
+
);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const ICON = "size-3.5 [stroke-width:1.8]";
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* The design's seating glyph, a low bench-sofa. Not in lucide (Armchair and
|
|
56
|
+
* Sofa both read as a different object), so drawn from the design's own paths.
|
|
57
|
+
*/
|
|
58
|
+
function SeatIcon({ className }: { className?: string }) {
|
|
59
|
+
return (
|
|
60
|
+
<svg
|
|
61
|
+
viewBox="0 0 24 24"
|
|
62
|
+
fill="none"
|
|
63
|
+
stroke="currentColor"
|
|
64
|
+
strokeLinecap="round"
|
|
65
|
+
strokeLinejoin="round"
|
|
66
|
+
className={className}
|
|
67
|
+
aria-hidden
|
|
68
|
+
>
|
|
69
|
+
<path d="M5 11V7a2 2 0 0 1 2-2h10a2 2 0 0 1 2 2v4" />
|
|
70
|
+
<path d="M5 11a2 2 0 0 0 0 4h14a2 2 0 0 0 0-4" />
|
|
71
|
+
<path d="M6 15v3M18 15v3M7 15h10" />
|
|
72
|
+
</svg>
|
|
73
|
+
);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export function BookingSummaryCard({
|
|
77
|
+
venueName,
|
|
78
|
+
date,
|
|
79
|
+
timeAndParty,
|
|
80
|
+
seating,
|
|
81
|
+
address,
|
|
82
|
+
code,
|
|
83
|
+
labels,
|
|
84
|
+
className,
|
|
85
|
+
}: BookingSummaryCardProps) {
|
|
86
|
+
return (
|
|
87
|
+
<div
|
|
88
|
+
data-slot="booking-summary-card"
|
|
89
|
+
className={cn(
|
|
90
|
+
"rounded-[14px] border border-border bg-card p-[15px] leading-[1.2] shadow-[0_1px_3px_oklch(0.5_0_0/0.1)]",
|
|
91
|
+
className
|
|
92
|
+
)}
|
|
93
|
+
>
|
|
94
|
+
<b className="text-base leading-5 font-bold text-foreground">{venueName}</b>
|
|
95
|
+
<div className="mt-[11px]">
|
|
96
|
+
<Row icon={<CalendarDays className={ICON} />}>{date}</Row>
|
|
97
|
+
<Row icon={<Clock className={ICON} />}>{timeAndParty}</Row>
|
|
98
|
+
{seating && <Row icon={<SeatIcon className={ICON} />}>{seating}</Row>}
|
|
99
|
+
{address && <Row icon={<MapPin className={ICON} />}>{address}</Row>}
|
|
100
|
+
</div>
|
|
101
|
+
<div className="mt-[13px] flex items-center justify-between border-t border-line-soft pt-[13px]">
|
|
102
|
+
<span className="text-[10.5px] tracking-[0.05em] text-muted-foreground uppercase">
|
|
103
|
+
{labels.code}
|
|
104
|
+
</span>
|
|
105
|
+
<b className="font-mono text-[15px] font-bold tabular-nums text-foreground">{code}</b>
|
|
106
|
+
</div>
|
|
107
|
+
</div>
|
|
108
|
+
);
|
|
109
|
+
}
|
|
@@ -127,6 +127,29 @@ export const AllVariants: Story = {
|
|
|
127
127
|
),
|
|
128
128
|
};
|
|
129
129
|
|
|
130
|
+
/**
|
|
131
|
+
* The committing action of the booking flow, on the brand gradient. `cta` in
|
|
132
|
+
* the footers ("Continue · 19:30", "Confirm booking"), `cta-sm` inline
|
|
133
|
+
* ("Confirm the table" beside the hold countdown). Beside it, the quiet
|
|
134
|
+
* `outline` for Back / Cancel.
|
|
135
|
+
*/
|
|
136
|
+
export const Cta: Story = {
|
|
137
|
+
render: () => (
|
|
138
|
+
<div className="flex flex-wrap items-center gap-3">
|
|
139
|
+
<Button variant="outline">Back</Button>
|
|
140
|
+
<Button variant="cta" size="cta">
|
|
141
|
+
Continue · 19:30
|
|
142
|
+
</Button>
|
|
143
|
+
<Button variant="cta" size="cta-sm">
|
|
144
|
+
Confirm the table
|
|
145
|
+
</Button>
|
|
146
|
+
<Button variant="cta" size="cta" disabled>
|
|
147
|
+
Continue
|
|
148
|
+
</Button>
|
|
149
|
+
</div>
|
|
150
|
+
),
|
|
151
|
+
};
|
|
152
|
+
|
|
130
153
|
export const AllSizes: Story = {
|
|
131
154
|
render: () => (
|
|
132
155
|
<div className="flex items-center gap-4">
|
|
@@ -62,6 +62,15 @@ const buttonVariants = cva(
|
|
|
62
62
|
/** An ON state that is not the primary — a toggled mode, a guide running. */
|
|
63
63
|
warning:
|
|
64
64
|
"bg-warning text-warning-foreground shadow-sm hover:bg-warning/90 hover:shadow",
|
|
65
|
+
/**
|
|
66
|
+
* The committing action of a flow: "Continue · 19:30", "Confirm
|
|
67
|
+
* booking", "Reserve", "See booking". The booking design draws every
|
|
68
|
+
* one of them on `--gradient-primary` with white type, in both themes —
|
|
69
|
+
* the gradient is warm in dark too, so the ink never inverts.
|
|
70
|
+
* `bg-[image:…]`, not `bg-[…]`: the token is a gradient, and only
|
|
71
|
+
* `background-image` accepts it. Pair with `size="cta"` / `"cta-sm"`.
|
|
72
|
+
*/
|
|
73
|
+
cta: "bg-[image:var(--gradient-primary)] text-white hover:brightness-[0.94] active:brightness-90",
|
|
65
74
|
},
|
|
66
75
|
size: {
|
|
67
76
|
xs: "h-8 rounded-lg px-3 text-xs [&_svg]:size-3.5",
|
|
@@ -70,6 +79,10 @@ const buttonVariants = cva(
|
|
|
70
79
|
lg: "h-12 rounded-2xl px-8",
|
|
71
80
|
/** The `ink` action's shape. Owns the radius so no variant fights it. */
|
|
72
81
|
pill: "h-11 rounded-full px-5 text-[13.5px] font-bold",
|
|
82
|
+
/** The booking design's action: 46px, 12px radius, 15px semibold. */
|
|
83
|
+
cta: "h-[46px] rounded-xl px-5 text-[15px] font-semibold",
|
|
84
|
+
/** Its compact form, in dense footers and inline rows: 38px, 10px radius. */
|
|
85
|
+
"cta-sm": "h-[38px] rounded-[10px] px-4 text-[13.5px] font-semibold",
|
|
73
86
|
icon: "h-11 w-11 rounded-full",
|
|
74
87
|
"icon-xs": "h-8 w-8 rounded-full [&_svg]:size-3.5",
|
|
75
88
|
"icon-sm": "h-9 w-9 rounded-full",
|
|
@@ -45,3 +45,30 @@ function CalendarRangeDemo() {
|
|
|
45
45
|
export const Range: Story = {
|
|
46
46
|
render: () => <CalendarRangeDemo />,
|
|
47
47
|
};
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* The booking flow's month grid (B1 desktop): 32px round cells in mono,
|
|
51
|
+
* the chosen day on the brand gradient, past days faded, a closed day struck
|
|
52
|
+
* through. Frozen on August 2026 so it matches the design frame.
|
|
53
|
+
*/
|
|
54
|
+
function BookingCalendarDemo() {
|
|
55
|
+
const [date, setDate] = useState<Date | undefined>(new Date(2026, 7, 14));
|
|
56
|
+
return (
|
|
57
|
+
<div className="w-[270px]">
|
|
58
|
+
<Calendar
|
|
59
|
+
appearance="booking"
|
|
60
|
+
mode="single"
|
|
61
|
+
weekStartsOn={1}
|
|
62
|
+
defaultMonth={new Date(2026, 7, 1)}
|
|
63
|
+
selected={date}
|
|
64
|
+
onSelect={setDate}
|
|
65
|
+
disabled={[{ before: new Date(2026, 7, 6) }, new Date(2026, 7, 25)]}
|
|
66
|
+
modifiers={{ closed: [new Date(2026, 7, 25)] }}
|
|
67
|
+
/>
|
|
68
|
+
</div>
|
|
69
|
+
);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export const Booking: Story = {
|
|
73
|
+
render: () => <BookingCalendarDemo />,
|
|
74
|
+
};
|
|
@@ -7,23 +7,93 @@ import { DayPicker, getDefaultClassNames, type DayButton } from "react-day-picke
|
|
|
7
7
|
import { cn } from "../lib/utils";
|
|
8
8
|
import { Button, buttonVariants } from "./button";
|
|
9
9
|
|
|
10
|
+
/**
|
|
11
|
+
* `appearance="booking"` is the month grid of the booking flow's "When are you
|
|
12
|
+
* coming?" (desktop), measured from its design (`.cal`): seven equal columns
|
|
13
|
+
* 3px apart; single-letter weekdays at 9.5px bold, muted; days as 32px round
|
|
14
|
+
* cells in mono 12.5px medium, secondary ink; the chosen day filled with
|
|
15
|
+
* `--gradient-primary`, white and bold; past days at 30%; a day the venue is
|
|
16
|
+
* closed struck through in a lighter ink. No outside days. Mark closed days
|
|
17
|
+
* with `modifiers={{ closed: … }}` — the appearance styles the `closed`
|
|
18
|
+
* modifier; `disabled` covers past days and the booking window.
|
|
19
|
+
*/
|
|
20
|
+
type CalendarAppearance = "default" | "booking";
|
|
21
|
+
|
|
22
|
+
const BOOKING_CLASSNAMES = {
|
|
23
|
+
root: "w-full",
|
|
24
|
+
months: "relative flex flex-col",
|
|
25
|
+
month: "flex w-full flex-col",
|
|
26
|
+
// The caption row is 15px with 7px under it; the chevrons sit in that row.
|
|
27
|
+
nav: "absolute inset-x-0 top-0 flex h-[15px] w-full items-center justify-between",
|
|
28
|
+
button_previous:
|
|
29
|
+
"grid size-6 -m-[4.5px] cursor-pointer place-items-center rounded-full text-muted-foreground hover:bg-surface-2 hover:text-foreground aria-disabled:pointer-events-none aria-disabled:opacity-30 [&_svg]:size-3.5",
|
|
30
|
+
button_next:
|
|
31
|
+
"grid size-6 -m-[4.5px] cursor-pointer place-items-center rounded-full text-muted-foreground hover:bg-surface-2 hover:text-foreground aria-disabled:pointer-events-none aria-disabled:opacity-30 [&_svg]:size-3.5",
|
|
32
|
+
month_caption: "mb-[7px] flex h-[15px] w-full items-center justify-center",
|
|
33
|
+
caption_label: "text-[12.5px] leading-[15px] font-bold text-foreground select-none",
|
|
34
|
+
month_grid: "w-full border-collapse",
|
|
35
|
+
weekdays: "grid grid-cols-7 gap-[3px]",
|
|
36
|
+
weekday: "pb-[3px] text-center text-[9.5px] font-bold text-muted-foreground select-none",
|
|
37
|
+
week: "mt-[3px] grid grid-cols-7 gap-[3px]",
|
|
38
|
+
day: "p-0 text-center select-none",
|
|
39
|
+
today: "",
|
|
40
|
+
outside: "invisible",
|
|
41
|
+
disabled: "",
|
|
42
|
+
hidden: "invisible",
|
|
43
|
+
} as const;
|
|
44
|
+
|
|
10
45
|
function Calendar({
|
|
11
46
|
className,
|
|
12
47
|
classNames,
|
|
13
|
-
showOutsideDays
|
|
48
|
+
showOutsideDays,
|
|
14
49
|
captionLayout = "label",
|
|
15
50
|
buttonVariant = "ghost",
|
|
51
|
+
appearance = "default",
|
|
16
52
|
formatters,
|
|
17
53
|
components,
|
|
54
|
+
modifiersClassNames,
|
|
18
55
|
...props
|
|
19
56
|
}: React.ComponentProps<typeof DayPicker> & {
|
|
20
57
|
buttonVariant?: React.ComponentProps<typeof Button>["variant"];
|
|
58
|
+
appearance?: CalendarAppearance;
|
|
21
59
|
}) {
|
|
22
60
|
const defaultClassNames = getDefaultClassNames();
|
|
23
61
|
|
|
62
|
+
if (appearance === "booking") {
|
|
63
|
+
return (
|
|
64
|
+
<DayPicker
|
|
65
|
+
showOutsideDays={showOutsideDays ?? false}
|
|
66
|
+
className={cn("group/calendar w-full", className)}
|
|
67
|
+
captionLayout="label"
|
|
68
|
+
formatters={{
|
|
69
|
+
formatWeekdayName: (date) =>
|
|
70
|
+
date.toLocaleString("default", { weekday: "narrow" }),
|
|
71
|
+
...formatters,
|
|
72
|
+
}}
|
|
73
|
+
classNames={{ ...BOOKING_CLASSNAMES, ...classNames }}
|
|
74
|
+
modifiersClassNames={{ closed: "is-closed", ...modifiersClassNames }}
|
|
75
|
+
components={{
|
|
76
|
+
Root: ({ className, rootRef, ...rootProps }) => (
|
|
77
|
+
<div data-slot="calendar" data-appearance="booking" ref={rootRef} className={cn(className)} {...rootProps} />
|
|
78
|
+
),
|
|
79
|
+
Chevron: ({ className, orientation, ...chevronProps }) =>
|
|
80
|
+
orientation === "left" ? (
|
|
81
|
+
<ChevronLeftIcon className={cn("size-4", className)} {...chevronProps} />
|
|
82
|
+
) : (
|
|
83
|
+
<ChevronRightIcon className={cn("size-4", className)} {...chevronProps} />
|
|
84
|
+
),
|
|
85
|
+
DayButton: BookingDayButton,
|
|
86
|
+
...components,
|
|
87
|
+
}}
|
|
88
|
+
{...props}
|
|
89
|
+
/>
|
|
90
|
+
);
|
|
91
|
+
}
|
|
92
|
+
|
|
24
93
|
return (
|
|
25
94
|
<DayPicker
|
|
26
|
-
showOutsideDays={showOutsideDays}
|
|
95
|
+
showOutsideDays={showOutsideDays ?? true}
|
|
96
|
+
modifiersClassNames={modifiersClassNames}
|
|
27
97
|
className={cn(
|
|
28
98
|
"group/calendar bg-background p-3 [--cell-size:--spacing(9)] [[data-slot=card-content]_&]:bg-transparent [[data-slot=popover-content]_&]:bg-transparent",
|
|
29
99
|
String.raw`rtl:**:[.rdp-button\_next>svg]:rotate-180`,
|
|
@@ -177,4 +247,41 @@ function CalendarDayButton({
|
|
|
177
247
|
);
|
|
178
248
|
}
|
|
179
249
|
|
|
180
|
-
|
|
250
|
+
/** The booking appearance's day: a 32px round cell, see `BOOKING_CLASSNAMES`. */
|
|
251
|
+
function BookingDayButton({
|
|
252
|
+
className,
|
|
253
|
+
day,
|
|
254
|
+
modifiers,
|
|
255
|
+
...props
|
|
256
|
+
}: React.ComponentProps<typeof DayButton>) {
|
|
257
|
+
const ref = React.useRef<HTMLButtonElement>(null);
|
|
258
|
+
React.useEffect(() => {
|
|
259
|
+
if (modifiers.focused) ref.current?.focus();
|
|
260
|
+
}, [modifiers.focused]);
|
|
261
|
+
|
|
262
|
+
const closed = Boolean(modifiers.closed);
|
|
263
|
+
|
|
264
|
+
return (
|
|
265
|
+
<button
|
|
266
|
+
ref={ref}
|
|
267
|
+
type="button"
|
|
268
|
+
data-day={day.date.toLocaleDateString()}
|
|
269
|
+
data-selected={modifiers.selected || undefined}
|
|
270
|
+
data-closed={closed || undefined}
|
|
271
|
+
className={cn(
|
|
272
|
+
"grid h-8 w-full cursor-pointer place-items-center rounded-full font-mono text-[12.5px] leading-none font-medium text-ink-secondary tabular-nums",
|
|
273
|
+
"transition-colors duration-150 hover:bg-surface-2 motion-reduce:transition-none",
|
|
274
|
+
"focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-1 focus-visible:ring-offset-background",
|
|
275
|
+
modifiers.selected &&
|
|
276
|
+
"bg-[image:var(--gradient-primary)] font-bold text-white hover:bg-transparent hover:brightness-[0.94]",
|
|
277
|
+
modifiers.disabled && !closed && "cursor-not-allowed opacity-30 hover:bg-transparent",
|
|
278
|
+
// A closed day is also disabled; it reads as closed, not as past.
|
|
279
|
+
closed && "cursor-not-allowed text-[oklch(0.7_0_0)] line-through hover:bg-transparent dark:text-ink-tertiary",
|
|
280
|
+
className
|
|
281
|
+
)}
|
|
282
|
+
{...props}
|
|
283
|
+
/>
|
|
284
|
+
);
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
export { Calendar, CalendarDayButton, BookingDayButton };
|
|
@@ -2,31 +2,57 @@
|
|
|
2
2
|
|
|
3
3
|
import * as React from "react";
|
|
4
4
|
import * as CheckboxPrimitive from "@radix-ui/react-checkbox";
|
|
5
|
+
import { cva, type VariantProps } from "class-variance-authority";
|
|
5
6
|
import { Check, Minus } from "lucide-react";
|
|
6
7
|
import { cn } from "../lib/utils";
|
|
7
8
|
|
|
8
|
-
const
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
9
|
+
const checkboxVariants = cva(
|
|
10
|
+
"peer size-5 shrink-0 rounded-md border border-border bg-background transition-all duration-150 ease-out 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=checked]:text-primary-foreground data-[state=checked]:border-primary data-[state=indeterminate]:bg-primary data-[state=indeterminate]:text-primary-foreground data-[state=indeterminate]:border-primary hover:border-primary/50 aria-invalid:border-destructive aria-invalid:focus-visible:ring-destructive/40",
|
|
11
|
+
{
|
|
12
|
+
variants: {
|
|
13
|
+
/**
|
|
14
|
+
* The booking flow's opt-in ("Save these details for next time",
|
|
15
|
+
* booking flow.html): a 17px box with a 5px radius, a hairline on the
|
|
16
|
+
* soft ground when off, the brand gradient with an 11px white check
|
|
17
|
+
* when on.
|
|
18
|
+
*/
|
|
19
|
+
appearance: {
|
|
20
|
+
default: "",
|
|
21
|
+
booking:
|
|
22
|
+
"size-[17px] rounded-[5px] bg-surface-2 data-[state=checked]:border-transparent data-[state=checked]:bg-[image:var(--gradient-primary)] data-[state=checked]:text-white [&_svg]:size-[11px]",
|
|
23
|
+
},
|
|
24
|
+
},
|
|
25
|
+
defaultVariants: { appearance: "default" },
|
|
26
|
+
}
|
|
27
|
+
);
|
|
28
|
+
|
|
29
|
+
type CheckboxProps = React.ComponentPropsWithoutRef<typeof CheckboxPrimitive.Root> &
|
|
30
|
+
VariantProps<typeof checkboxVariants>;
|
|
31
|
+
|
|
32
|
+
const Checkbox = React.forwardRef<React.ElementRef<typeof CheckboxPrimitive.Root>, CheckboxProps>(
|
|
33
|
+
({ className, appearance, ...props }, ref) => (
|
|
34
|
+
<CheckboxPrimitive.Root
|
|
35
|
+
ref={ref}
|
|
36
|
+
data-slot="checkbox"
|
|
37
|
+
className={cn(checkboxVariants({ appearance }), className)}
|
|
38
|
+
{...props}
|
|
39
|
+
>
|
|
40
|
+
<CheckboxPrimitive.Indicator className={cn("flex items-center justify-center text-current")}>
|
|
41
|
+
{props.checked === "indeterminate" ? (
|
|
42
|
+
<Minus
|
|
43
|
+
className="size-3.5 animate-in fade-in-0 zoom-in-50 duration-200 ease-out"
|
|
44
|
+
strokeWidth={3}
|
|
45
|
+
/>
|
|
46
|
+
) : (
|
|
47
|
+
<Check
|
|
48
|
+
className="size-3.5 animate-in fade-in-0 zoom-in-50 duration-200 ease-out"
|
|
49
|
+
strokeWidth={3}
|
|
50
|
+
/>
|
|
51
|
+
)}
|
|
52
|
+
</CheckboxPrimitive.Indicator>
|
|
53
|
+
</CheckboxPrimitive.Root>
|
|
54
|
+
)
|
|
55
|
+
);
|
|
30
56
|
Checkbox.displayName = CheckboxPrimitive.Root.displayName;
|
|
31
57
|
|
|
32
|
-
export { Checkbox };
|
|
58
|
+
export { Checkbox, checkboxVariants };
|
|
@@ -32,7 +32,7 @@ const meta: Meta<typeof DriftRail> = {
|
|
|
32
32
|
docs: {
|
|
33
33
|
description: {
|
|
34
34
|
component:
|
|
35
|
-
"An infinite horizontal marquee
|
|
35
|
+
"An infinite horizontal marquee. Duplicates its children once so a `translateX(-50%)` loop is seamless — pass a fixed, ordered list. Pauses on hover, pauses when scrolled off-screen (`IntersectionObserver`), and stops outright under `prefers-reduced-motion`.",
|
|
36
36
|
},
|
|
37
37
|
},
|
|
38
38
|
},
|
|
@@ -90,31 +90,3 @@ export const WithFeaturedItems: Story = {
|
|
|
90
90
|
</div>
|
|
91
91
|
),
|
|
92
92
|
};
|
|
93
|
-
|
|
94
|
-
/**
|
|
95
|
-
* With `labels`, a pause/play toggle renders under the rail's right edge —
|
|
96
|
-
* the same latch, glyph and 44px target as `WeekendPanel`'s (WCAG 2.2.2).
|
|
97
|
-
* Pressing it holds the rail still until play is pressed; hover and touch
|
|
98
|
-
* still pause it on their own. The control is not rendered under
|
|
99
|
-
* `prefers-reduced-motion`, where nothing drifts. The duplicate pass is
|
|
100
|
-
* `inert`, so Tab visits each item once.
|
|
101
|
-
*/
|
|
102
|
-
export const WithPauseControl: Story = {
|
|
103
|
-
render: () => (
|
|
104
|
-
<div className="max-w-[420px]">
|
|
105
|
-
<DriftRail durationSeconds={30} labels={{ pause: "Pause the rail", play: "Play the rail" }}>
|
|
106
|
-
{["Warehouse Rave", "Jazz on the Rooftop", "Open Mic Night", "Street Food Market"].map(
|
|
107
|
-
(title) => (
|
|
108
|
-
<FeaturedRailItem
|
|
109
|
-
key={title}
|
|
110
|
-
title={title}
|
|
111
|
-
venue="Atelier Café"
|
|
112
|
-
note="Sat 5 · 20:00"
|
|
113
|
-
href={`/events/${title.toLowerCase().replace(/\s+/g, "-")}`}
|
|
114
|
-
/>
|
|
115
|
-
)
|
|
116
|
-
)}
|
|
117
|
-
</DriftRail>
|
|
118
|
-
</div>
|
|
119
|
-
),
|
|
120
|
-
};
|
|
@@ -3,7 +3,6 @@
|
|
|
3
3
|
import { useEffect, useRef, useState } from "react";
|
|
4
4
|
import type { PointerEvent as ReactPointerEvent } from "react";
|
|
5
5
|
import { cn } from "../lib/utils";
|
|
6
|
-
import { MotionPlayPause } from "./motion-icon-controls";
|
|
7
6
|
|
|
8
7
|
/**
|
|
9
8
|
* DriftRail — an infinite horizontal marquee for logos, tag chips or small
|
|
@@ -31,29 +30,7 @@ import { MotionPlayPause } from "./motion-icon-controls";
|
|
|
31
30
|
* work nobody can see. `prefers-reduced-motion` removes the drift entirely;
|
|
32
31
|
* the rail stays scrollable by hand, which is more than the static strip it
|
|
33
32
|
* used to collapse to.
|
|
34
|
-
*
|
|
35
|
-
* THE READER CAN STOP IT FOR GOOD. Hover and touch are holds; a rail that
|
|
36
|
-
* drifts for longer than five seconds also needs a control the reader can
|
|
37
|
-
* press (WCAG 2.2.2), and a low-vision reader who cannot follow the motion
|
|
38
|
-
* needs it to stay stopped. Pass `labels` and the rail renders a pause/play
|
|
39
|
-
* toggle under its right edge — the same latch, glyph and 44px target as
|
|
40
|
-
* `WeekendPanel`'s, so the hub's rails all answer to one control. It is not
|
|
41
|
-
* rendered under `prefers-reduced-motion`, where there is nothing to stop.
|
|
42
|
-
*
|
|
43
|
-
* THE DUPLICATE IS INERT. The second copy is what makes the loop seamless,
|
|
44
|
-
* but its links are the same links again: `aria-hidden` kept them out of the
|
|
45
|
-
* accessibility tree and left them in the tab order, so a keyboard reader
|
|
46
|
-
* tabbed through every item twice. `inert` removes them from focus and
|
|
47
|
-
* hit-testing as well.
|
|
48
33
|
*/
|
|
49
|
-
/** Accessible names for the pause/play toggle, injected so the DS owns no copy. */
|
|
50
|
-
export type DriftRailLabels = {
|
|
51
|
-
/** Name announced while the rail drifts (the button pauses it). */
|
|
52
|
-
pause: string;
|
|
53
|
-
/** Name announced while the rail is paused (the button resumes it). */
|
|
54
|
-
play: string;
|
|
55
|
-
};
|
|
56
|
-
|
|
57
34
|
export type DriftRailProps = {
|
|
58
35
|
children: React.ReactNode;
|
|
59
36
|
/** Runs the loop right-to-left instead of left-to-right. */
|
|
@@ -62,12 +39,6 @@ export type DriftRailProps = {
|
|
|
62
39
|
durationSeconds?: number;
|
|
63
40
|
/** Pauses the loop while the pointer hovers the rail. */
|
|
64
41
|
pauseOnHover?: boolean;
|
|
65
|
-
/**
|
|
66
|
-
* Accessible names for the pause/play toggle. When given, the toggle
|
|
67
|
-
* renders whenever the rail drifts (never under `prefers-reduced-motion`);
|
|
68
|
-
* without it there is no control, as before.
|
|
69
|
-
*/
|
|
70
|
-
labels?: DriftRailLabels;
|
|
71
42
|
className?: string;
|
|
72
43
|
};
|
|
73
44
|
|
|
@@ -80,7 +51,6 @@ export function DriftRail({
|
|
|
80
51
|
reverse = false,
|
|
81
52
|
durationSeconds = DRIFT_RAIL_DEFAULT_DURATION_SECONDS,
|
|
82
53
|
pauseOnHover = true,
|
|
83
|
-
labels,
|
|
84
54
|
className,
|
|
85
55
|
}: DriftRailProps) {
|
|
86
56
|
const scrollerRef = useRef<HTMLDivElement>(null);
|
|
@@ -88,9 +58,6 @@ export function DriftRail({
|
|
|
88
58
|
const [isHovered, setIsHovered] = useState(false);
|
|
89
59
|
const [inView, setInView] = useState(true);
|
|
90
60
|
const [reducedMotion, setReducedMotion] = useState(false);
|
|
91
|
-
// A latch, not a hold: it only clears when the reader presses play.
|
|
92
|
-
const [isManuallyPaused, setIsManuallyPaused] = useState(false);
|
|
93
|
-
const [playPauseReplayToken, setPlayPauseReplayToken] = useState(0);
|
|
94
61
|
// `heldUntil` is a timestamp rather than a boolean so a native touch scroll
|
|
95
62
|
// that keeps going after the finger lifts is not fought by the drift.
|
|
96
63
|
const heldUntil = useRef(0);
|
|
@@ -131,8 +98,7 @@ export function DriftRail({
|
|
|
131
98
|
|
|
132
99
|
let frame = 0;
|
|
133
100
|
let last = performance.now();
|
|
134
|
-
const drifting =
|
|
135
|
-
!reducedMotion && !isManuallyPaused && inView && !(pauseOnHover && isHovered);
|
|
101
|
+
const drifting = !reducedMotion && inView && !(pauseOnHover && isHovered);
|
|
136
102
|
|
|
137
103
|
// Start the reverse loop inside the second copy so it has room to travel.
|
|
138
104
|
if (reverse && scroller.scrollLeft === 0) scroller.scrollLeft = track.scrollWidth / 2;
|
|
@@ -166,7 +132,7 @@ export function DriftRail({
|
|
|
166
132
|
};
|
|
167
133
|
frame = requestAnimationFrame(tick);
|
|
168
134
|
return () => cancelAnimationFrame(frame);
|
|
169
|
-
}, [durationSeconds, inView, isHovered,
|
|
135
|
+
}, [durationSeconds, inView, isHovered, pauseOnHover, reducedMotion, reverse]);
|
|
170
136
|
|
|
171
137
|
const hold = () => {
|
|
172
138
|
heldUntil.current = Number.POSITIVE_INFINITY;
|
|
@@ -212,7 +178,7 @@ export function DriftRail({
|
|
|
212
178
|
}
|
|
213
179
|
};
|
|
214
180
|
|
|
215
|
-
|
|
181
|
+
return (
|
|
216
182
|
<div
|
|
217
183
|
ref={scrollerRef}
|
|
218
184
|
onMouseEnter={() => setIsHovered(true)}
|
|
@@ -236,43 +202,10 @@ export function DriftRail({
|
|
|
236
202
|
>
|
|
237
203
|
<div ref={trackRef} className="flex w-max items-stretch gap-3.5">
|
|
238
204
|
{children}
|
|
239
|
-
|
|
240
|
-
tab order too, or a keyboard reader tabs every item twice. */}
|
|
241
|
-
<div className="contents" aria-hidden="true" inert>
|
|
205
|
+
<div className="contents" aria-hidden="true">
|
|
242
206
|
{children}
|
|
243
207
|
</div>
|
|
244
208
|
</div>
|
|
245
209
|
</div>
|
|
246
210
|
);
|
|
247
|
-
|
|
248
|
-
// The toggle exists to stop a drift; under reduced motion there is none.
|
|
249
|
-
// A sibling row, not an overlay: a button floated over the rail's corner
|
|
250
|
-
// would sit on an item and intercept the grab that starts there.
|
|
251
|
-
if (!labels || reducedMotion) return scroller;
|
|
252
|
-
|
|
253
|
-
return (
|
|
254
|
-
<>
|
|
255
|
-
{scroller}
|
|
256
|
-
<div className="flex justify-end">
|
|
257
|
-
{/* `MotionPlayPause`, as in `WeekendPanel`: the triangle folding into
|
|
258
|
-
two bars IS the state change. `active` is the bars, so it is
|
|
259
|
-
`!isManuallyPaused`; `replayToken` bumps on pointer down so the
|
|
260
|
-
gesture answers the touch. */}
|
|
261
|
-
<button
|
|
262
|
-
type="button"
|
|
263
|
-
onClick={() => setIsManuallyPaused((current) => !current)}
|
|
264
|
-
onPointerDown={() => setPlayPauseReplayToken((n) => n + 1)}
|
|
265
|
-
aria-label={isManuallyPaused ? labels.play : labels.pause}
|
|
266
|
-
aria-pressed={isManuallyPaused}
|
|
267
|
-
className="flex h-11 w-11 shrink-0 cursor-pointer items-center justify-center rounded-full text-muted-foreground transition-colors duration-150 hover:bg-muted hover:text-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 motion-reduce:transition-none"
|
|
268
|
-
>
|
|
269
|
-
<MotionPlayPause
|
|
270
|
-
active={!isManuallyPaused}
|
|
271
|
-
replayToken={playPauseReplayToken}
|
|
272
|
-
className="size-4"
|
|
273
|
-
/>
|
|
274
|
-
</button>
|
|
275
|
-
</div>
|
|
276
|
-
</>
|
|
277
|
-
);
|
|
278
211
|
}
|
|
@@ -1,6 +1,15 @@
|
|
|
1
1
|
import type { Meta, StoryObj } from "@storybook/react";
|
|
2
|
-
import {
|
|
2
|
+
import { Checkbox } from "./checkbox";
|
|
3
|
+
import {
|
|
4
|
+
Field,
|
|
5
|
+
FieldControl,
|
|
6
|
+
FieldDescription,
|
|
7
|
+
FieldError,
|
|
8
|
+
FieldLabel,
|
|
9
|
+
FieldOptional,
|
|
10
|
+
} from "./field";
|
|
3
11
|
import { Input } from "./input";
|
|
12
|
+
import { Textarea } from "./textarea";
|
|
4
13
|
|
|
5
14
|
const meta = {
|
|
6
15
|
title: "Components/Field",
|
|
@@ -34,3 +43,44 @@ export const Invalid: Story = {
|
|
|
34
43
|
</Field>
|
|
35
44
|
),
|
|
36
45
|
};
|
|
46
|
+
|
|
47
|
+
/** The booking flow's "Your details" fields (B2, S6): caption labels, 46px fields, the opt-in. */
|
|
48
|
+
export const Booking: Story = {
|
|
49
|
+
render: () => (
|
|
50
|
+
<div className="flex w-[335px] flex-col gap-3">
|
|
51
|
+
<Field appearance="booking">
|
|
52
|
+
<FieldLabel>Name</FieldLabel>
|
|
53
|
+
<FieldControl>
|
|
54
|
+
<Input appearance="booking" defaultValue="Adrian Pop" />
|
|
55
|
+
</FieldControl>
|
|
56
|
+
</Field>
|
|
57
|
+
<Field appearance="booking" invalid>
|
|
58
|
+
<FieldLabel>Phone</FieldLabel>
|
|
59
|
+
<FieldControl>
|
|
60
|
+
<Input appearance="booking" defaultValue="0712 34" />
|
|
61
|
+
</FieldControl>
|
|
62
|
+
<FieldError>That number looks too short.</FieldError>
|
|
63
|
+
</Field>
|
|
64
|
+
<Field appearance="booking">
|
|
65
|
+
<FieldLabel>
|
|
66
|
+
Email <FieldOptional>(optional)</FieldOptional>
|
|
67
|
+
</FieldLabel>
|
|
68
|
+
<FieldControl>
|
|
69
|
+
<Input appearance="booking" type="email" placeholder="you@example.com" />
|
|
70
|
+
</FieldControl>
|
|
71
|
+
</Field>
|
|
72
|
+
<Field appearance="booking">
|
|
73
|
+
<FieldLabel>
|
|
74
|
+
Anything we should know <FieldOptional>(optional)</FieldOptional>
|
|
75
|
+
</FieldLabel>
|
|
76
|
+
<FieldControl>
|
|
77
|
+
<Textarea appearance="booking" defaultValue="Birthday — a quieter corner if possible." />
|
|
78
|
+
</FieldControl>
|
|
79
|
+
</Field>
|
|
80
|
+
<label className="flex items-center gap-[9px] text-[12.5px] text-ink-secondary">
|
|
81
|
+
<Checkbox appearance="booking" defaultChecked />
|
|
82
|
+
Save these details for next time
|
|
83
|
+
</label>
|
|
84
|
+
</div>
|
|
85
|
+
),
|
|
86
|
+
};
|