@bearmenu/ui 0.8.18 → 0.8.20
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-PW247TG2.js → chunk-BRXI6EDO.js} +5 -2
- package/dist/{chunk-LYREUZH6.js → chunk-QRSIJC4X.js} +1 -1
- package/dist/components/alert.d.ts +2 -2
- package/dist/components/badge.d.ts +1 -1
- package/dist/components/button.d.ts +1 -1
- package/dist/components/card.d.ts +2 -2
- package/dist/components/command.d.ts +15 -15
- package/dist/components/event-hub-hero.d.ts +5 -3
- package/dist/components/event-hub-hero.js +11 -11
- package/dist/components/event-tonight-strip.js +2 -2
- package/dist/components/item.d.ts +2 -2
- package/dist/components/menu-family-block.js +4 -3
- package/dist/components/place-card.js +1 -1
- package/dist/components/poster-stack.d.ts +3 -1
- package/dist/components/poster-stack.js +71 -63
- package/dist/components/spinner.d.ts +2 -2
- package/dist/components/tag.d.ts +2 -2
- package/dist/components/text.d.ts +3 -3
- package/dist/components/timeline-day.js +2 -2
- package/dist/components/timeline-hour-puck.js +1 -1
- package/dist/components/timeline-hour.js +2 -2
- package/dist/components/timeline-playhead.js +1 -1
- package/package.json +1 -1
- package/src/components/event-hub-hero.tsx +18 -14
- package/src/components/event-tonight-strip.test.tsx +21 -11
- package/src/components/event-tonight-strip.tsx +2 -2
- package/src/components/menu-family-block.tsx +13 -6
- package/src/components/poster-stack.tsx +30 -11
- package/src/components/timeline-hour-puck.tsx +5 -2
- package/dist/components/event-door.d.ts +0 -41
- package/dist/components/event-door.js +0 -65
- package/src/components/event-door.stories.tsx +0 -88
- package/src/components/event-door.tsx +0 -97
|
@@ -53,8 +53,11 @@ function TimelineHourPuck({
|
|
|
53
53
|
variant === "responsive" && cn(
|
|
54
54
|
chipClasses,
|
|
55
55
|
// The station look from `lg`: bordered, background-filled,
|
|
56
|
-
//
|
|
57
|
-
|
|
56
|
+
// content-width. Every chip utility above has an `lg:` twin.
|
|
57
|
+
// `min-w-max`, NOT `min-w-0`: the host centres the puck inside
|
|
58
|
+
// a zero-width flex box, and a flex item allowed to shrink to 0
|
|
59
|
+
// collapses to a 24px square behind its own text.
|
|
60
|
+
"lg:h-auto lg:min-w-max lg:shrink-0 lg:gap-0 lg:border-border lg:bg-background lg:px-[7px] lg:py-[3px] lg:text-[12.5px] lg:text-foreground",
|
|
58
61
|
isLive && "lg:border-success/40 lg:bg-background lg:text-success"
|
|
59
62
|
)
|
|
60
63
|
),
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { TimelineHourPuck, TIMELINE_MOBILE_GUTTER_CLASS } from './chunk-
|
|
1
|
+
import { TimelineHourPuck, TIMELINE_MOBILE_GUTTER_CLASS } from './chunk-BRXI6EDO.js';
|
|
2
2
|
import { Separator } from './chunk-NWZGSLPU.js';
|
|
3
3
|
import { Skeleton } from './chunk-XBBEX4SF.js';
|
|
4
4
|
import { cn } from './chunk-FEK5XGZW.js';
|
|
@@ -3,10 +3,10 @@ import * as React from 'react';
|
|
|
3
3
|
import { VariantProps } from 'class-variance-authority';
|
|
4
4
|
|
|
5
5
|
declare const alertVariants: (props?: ({
|
|
6
|
-
variant?: "
|
|
6
|
+
variant?: "default" | "destructive" | "warning" | "success" | null | undefined;
|
|
7
7
|
} & class_variance_authority_types.ClassProp) | undefined) => string;
|
|
8
8
|
declare const Alert: React.ForwardRefExoticComponent<React.HTMLAttributes<HTMLDivElement> & VariantProps<(props?: ({
|
|
9
|
-
variant?: "
|
|
9
|
+
variant?: "default" | "destructive" | "warning" | "success" | null | undefined;
|
|
10
10
|
} & class_variance_authority_types.ClassProp) | undefined) => string> & React.RefAttributes<HTMLDivElement>>;
|
|
11
11
|
declare const AlertTitle: React.ForwardRefExoticComponent<React.HTMLAttributes<HTMLHeadingElement> & React.RefAttributes<HTMLParagraphElement>>;
|
|
12
12
|
declare const AlertDescription: React.ForwardRefExoticComponent<React.HTMLAttributes<HTMLParagraphElement> & React.RefAttributes<HTMLParagraphElement>>;
|
|
@@ -4,7 +4,7 @@ import * as React from 'react';
|
|
|
4
4
|
import { VariantProps } from 'class-variance-authority';
|
|
5
5
|
|
|
6
6
|
declare const badgeVariants: (props?: ({
|
|
7
|
-
variant?: "
|
|
7
|
+
variant?: "default" | "destructive" | "warning" | "success" | "outline" | "secondary" | "glass" | "overlay" | "brown" | "onPrimary" | null | undefined;
|
|
8
8
|
live?: boolean | null | undefined;
|
|
9
9
|
} & class_variance_authority_types.ClassProp) | undefined) => string;
|
|
10
10
|
interface BadgeProps extends React.HTMLAttributes<HTMLDivElement>, VariantProps<typeof badgeVariants> {
|
|
@@ -3,7 +3,7 @@ import * as React from 'react';
|
|
|
3
3
|
import { VariantProps } from 'class-variance-authority';
|
|
4
4
|
|
|
5
5
|
declare const buttonVariants: (props?: ({
|
|
6
|
-
variant?: "
|
|
6
|
+
variant?: "default" | "destructive" | "warning" | "link" | "outline" | "secondary" | "ghost" | "glass" | "overlay" | "ink" | "brown" | null | undefined;
|
|
7
7
|
size?: "default" | "xs" | "sm" | "lg" | "pill" | "icon" | "icon-xs" | "icon-sm" | "icon-lg" | null | undefined;
|
|
8
8
|
fullWidth?: boolean | null | undefined;
|
|
9
9
|
} & class_variance_authority_types.ClassProp) | undefined) => string;
|
|
@@ -3,12 +3,12 @@ import * as React from 'react';
|
|
|
3
3
|
import { VariantProps } from 'class-variance-authority';
|
|
4
4
|
|
|
5
5
|
declare const cardVariants: (props?: ({
|
|
6
|
-
variant?: "
|
|
6
|
+
variant?: "muted" | "outline" | "glass" | "surface" | "elevated" | "elevatedPanel" | null | undefined;
|
|
7
7
|
size?: "default" | "sm" | "lg" | null | undefined;
|
|
8
8
|
interactive?: boolean | null | undefined;
|
|
9
9
|
} & class_variance_authority_types.ClassProp) | undefined) => string;
|
|
10
10
|
declare const Card: React.ForwardRefExoticComponent<React.HTMLAttributes<HTMLDivElement> & VariantProps<(props?: ({
|
|
11
|
-
variant?: "
|
|
11
|
+
variant?: "muted" | "outline" | "glass" | "surface" | "elevated" | "elevatedPanel" | null | undefined;
|
|
12
12
|
size?: "default" | "sm" | "lg" | null | undefined;
|
|
13
13
|
interactive?: boolean | null | undefined;
|
|
14
14
|
} & class_variance_authority_types.ClassProp) | undefined) => string> & React.RefAttributes<HTMLDivElement>>;
|
|
@@ -4,11 +4,11 @@ import { DialogProps } from '@radix-ui/react-dialog';
|
|
|
4
4
|
|
|
5
5
|
declare const Command: React.ForwardRefExoticComponent<Omit<{
|
|
6
6
|
children?: React.ReactNode;
|
|
7
|
-
} & Pick<Pick<React.DetailedHTMLProps<React.HTMLAttributes<HTMLDivElement>, HTMLDivElement>,
|
|
7
|
+
} & Pick<Pick<React.DetailedHTMLProps<React.HTMLAttributes<HTMLDivElement>, HTMLDivElement>, keyof React.HTMLAttributes<HTMLDivElement> | "key"> & {
|
|
8
8
|
ref?: React.Ref<HTMLDivElement>;
|
|
9
9
|
} & {
|
|
10
10
|
asChild?: boolean;
|
|
11
|
-
},
|
|
11
|
+
}, keyof React.HTMLAttributes<HTMLDivElement> | "asChild" | "key"> & {
|
|
12
12
|
label?: string;
|
|
13
13
|
shouldFilter?: boolean;
|
|
14
14
|
filter?: (value: string, search: string, keywords?: string[]) => number;
|
|
@@ -28,61 +28,61 @@ declare const CommandInput: React.ForwardRefExoticComponent<Omit<Omit<Pick<Pick<
|
|
|
28
28
|
ref?: React.Ref<HTMLInputElement>;
|
|
29
29
|
} & {
|
|
30
30
|
asChild?: boolean;
|
|
31
|
-
}, "
|
|
31
|
+
}, "asChild" | "key" | keyof React.InputHTMLAttributes<HTMLInputElement>>, "onChange" | "type" | "value"> & {
|
|
32
32
|
value?: string;
|
|
33
33
|
onValueChange?: (search: string) => void;
|
|
34
34
|
} & React.RefAttributes<HTMLInputElement>, "ref"> & React.RefAttributes<HTMLInputElement>>;
|
|
35
35
|
declare const CommandList: React.ForwardRefExoticComponent<Omit<{
|
|
36
36
|
children?: React.ReactNode;
|
|
37
|
-
} & Pick<Pick<React.DetailedHTMLProps<React.HTMLAttributes<HTMLDivElement>, HTMLDivElement>,
|
|
37
|
+
} & Pick<Pick<React.DetailedHTMLProps<React.HTMLAttributes<HTMLDivElement>, HTMLDivElement>, keyof React.HTMLAttributes<HTMLDivElement> | "key"> & {
|
|
38
38
|
ref?: React.Ref<HTMLDivElement>;
|
|
39
39
|
} & {
|
|
40
40
|
asChild?: boolean;
|
|
41
|
-
},
|
|
41
|
+
}, keyof React.HTMLAttributes<HTMLDivElement> | "asChild" | "key"> & {
|
|
42
42
|
label?: string;
|
|
43
43
|
} & React.RefAttributes<HTMLDivElement>, "ref"> & React.RefAttributes<HTMLDivElement>>;
|
|
44
44
|
declare const CommandEmpty: React.ForwardRefExoticComponent<Omit<{
|
|
45
45
|
children?: React.ReactNode;
|
|
46
|
-
} & Pick<Pick<React.DetailedHTMLProps<React.HTMLAttributes<HTMLDivElement>, HTMLDivElement>,
|
|
46
|
+
} & Pick<Pick<React.DetailedHTMLProps<React.HTMLAttributes<HTMLDivElement>, HTMLDivElement>, keyof React.HTMLAttributes<HTMLDivElement> | "key"> & {
|
|
47
47
|
ref?: React.Ref<HTMLDivElement>;
|
|
48
48
|
} & {
|
|
49
49
|
asChild?: boolean;
|
|
50
|
-
},
|
|
50
|
+
}, keyof React.HTMLAttributes<HTMLDivElement> | "asChild" | "key"> & React.RefAttributes<HTMLDivElement>, "ref"> & React.RefAttributes<HTMLDivElement>>;
|
|
51
51
|
declare const CommandLoading: React.ForwardRefExoticComponent<Omit<{
|
|
52
52
|
children?: React.ReactNode;
|
|
53
|
-
} & Pick<Pick<React.DetailedHTMLProps<React.HTMLAttributes<HTMLDivElement>, HTMLDivElement>,
|
|
53
|
+
} & Pick<Pick<React.DetailedHTMLProps<React.HTMLAttributes<HTMLDivElement>, HTMLDivElement>, keyof React.HTMLAttributes<HTMLDivElement> | "key"> & {
|
|
54
54
|
ref?: React.Ref<HTMLDivElement>;
|
|
55
55
|
} & {
|
|
56
56
|
asChild?: boolean;
|
|
57
|
-
},
|
|
57
|
+
}, keyof React.HTMLAttributes<HTMLDivElement> | "asChild" | "key"> & {
|
|
58
58
|
progress?: number;
|
|
59
59
|
label?: string;
|
|
60
60
|
} & React.RefAttributes<HTMLDivElement>, "ref"> & React.RefAttributes<HTMLDivElement>>;
|
|
61
61
|
declare const CommandGroup: React.ForwardRefExoticComponent<Omit<{
|
|
62
62
|
children?: React.ReactNode;
|
|
63
|
-
} & Omit<Pick<Pick<React.DetailedHTMLProps<React.HTMLAttributes<HTMLDivElement>, HTMLDivElement>,
|
|
63
|
+
} & Omit<Pick<Pick<React.DetailedHTMLProps<React.HTMLAttributes<HTMLDivElement>, HTMLDivElement>, keyof React.HTMLAttributes<HTMLDivElement> | "key"> & {
|
|
64
64
|
ref?: React.Ref<HTMLDivElement>;
|
|
65
65
|
} & {
|
|
66
66
|
asChild?: boolean;
|
|
67
|
-
},
|
|
67
|
+
}, keyof React.HTMLAttributes<HTMLDivElement> | "asChild" | "key">, "heading" | "value"> & {
|
|
68
68
|
heading?: React.ReactNode;
|
|
69
69
|
value?: string;
|
|
70
70
|
forceMount?: boolean;
|
|
71
71
|
} & React.RefAttributes<HTMLDivElement>, "ref"> & React.RefAttributes<HTMLDivElement>>;
|
|
72
|
-
declare const CommandSeparator: React.ForwardRefExoticComponent<Omit<Pick<Pick<React.DetailedHTMLProps<React.HTMLAttributes<HTMLDivElement>, HTMLDivElement>,
|
|
72
|
+
declare const CommandSeparator: React.ForwardRefExoticComponent<Omit<Pick<Pick<React.DetailedHTMLProps<React.HTMLAttributes<HTMLDivElement>, HTMLDivElement>, keyof React.HTMLAttributes<HTMLDivElement> | "key"> & {
|
|
73
73
|
ref?: React.Ref<HTMLDivElement>;
|
|
74
74
|
} & {
|
|
75
75
|
asChild?: boolean;
|
|
76
|
-
},
|
|
76
|
+
}, keyof React.HTMLAttributes<HTMLDivElement> | "asChild" | "key"> & {
|
|
77
77
|
alwaysRender?: boolean;
|
|
78
78
|
} & React.RefAttributes<HTMLDivElement>, "ref"> & React.RefAttributes<HTMLDivElement>>;
|
|
79
79
|
declare const CommandItem: React.ForwardRefExoticComponent<Omit<{
|
|
80
80
|
children?: React.ReactNode;
|
|
81
|
-
} & Omit<Pick<Pick<React.DetailedHTMLProps<React.HTMLAttributes<HTMLDivElement>, HTMLDivElement>,
|
|
81
|
+
} & Omit<Pick<Pick<React.DetailedHTMLProps<React.HTMLAttributes<HTMLDivElement>, HTMLDivElement>, keyof React.HTMLAttributes<HTMLDivElement> | "key"> & {
|
|
82
82
|
ref?: React.Ref<HTMLDivElement>;
|
|
83
83
|
} & {
|
|
84
84
|
asChild?: boolean;
|
|
85
|
-
},
|
|
85
|
+
}, keyof React.HTMLAttributes<HTMLDivElement> | "asChild" | "key">, "onSelect" | "disabled" | "value"> & {
|
|
86
86
|
disabled?: boolean;
|
|
87
87
|
onSelect?: (value: string) => void;
|
|
88
88
|
value?: string;
|
|
@@ -43,9 +43,11 @@ type EventHubHeroProps = {
|
|
|
43
43
|
/** The right-column slot — the poster stack in the source design. */
|
|
44
44
|
aside?: React.ReactNode;
|
|
45
45
|
/**
|
|
46
|
-
* The
|
|
47
|
-
*
|
|
48
|
-
*
|
|
46
|
+
* The hub's tonight state strip. The figures say scale; this says what is
|
|
47
|
+
* on right now, and it is the one thing on the first screen a reader can
|
|
48
|
+
* tap into the timeline. On a phone it follows the aside — title, figures,
|
|
49
|
+
* posters, then the one line that says what is on — and from `lg` it is a
|
|
50
|
+
* content-width row under both columns.
|
|
49
51
|
*/
|
|
50
52
|
footer?: React.ReactNode;
|
|
51
53
|
className?: string;
|
|
@@ -17,10 +17,10 @@ function EventHubHero({
|
|
|
17
17
|
"div",
|
|
18
18
|
{
|
|
19
19
|
className: cn(
|
|
20
|
-
//
|
|
21
|
-
// tappable poster stack the aside
|
|
22
|
-
//
|
|
23
|
-
"grid grid-cols-1 gap-
|
|
20
|
+
// 12px between the phone's blocks — the page's rhythm, the same as
|
|
21
|
+
// between the sections below: with a tappable poster stack the aside
|
|
22
|
+
// is content, not decoration, and gets no section gap of its own.
|
|
23
|
+
"grid grid-cols-1 gap-3 lg:grid-cols-[1fr_620px] lg:items-center lg:gap-x-10 lg:gap-y-6",
|
|
24
24
|
className
|
|
25
25
|
),
|
|
26
26
|
children: [
|
|
@@ -86,10 +86,10 @@ function EventHubHero({
|
|
|
86
86
|
);
|
|
87
87
|
})
|
|
88
88
|
}
|
|
89
|
-
)
|
|
90
|
-
footer && /* @__PURE__ */ jsx("div", { className: "order-4 pt-1.5 lg:order-none lg:pt-2", children: footer })
|
|
89
|
+
)
|
|
91
90
|
] }),
|
|
92
|
-
aside && /* @__PURE__ */ jsx("div", { className: "min-w-0", children: aside })
|
|
91
|
+
aside && /* @__PURE__ */ jsx("div", { className: "min-w-0", children: aside }),
|
|
92
|
+
footer && /* @__PURE__ */ jsx("div", { className: "lg:col-span-2 lg:w-fit lg:min-w-[480px]", children: footer })
|
|
93
93
|
]
|
|
94
94
|
}
|
|
95
95
|
);
|
|
@@ -100,7 +100,7 @@ function EventHubHeroSkeleton({ aside, footer, className }) {
|
|
|
100
100
|
{
|
|
101
101
|
"aria-hidden": "true",
|
|
102
102
|
className: cn(
|
|
103
|
-
"grid grid-cols-1 gap-
|
|
103
|
+
"grid grid-cols-1 gap-3 lg:grid-cols-[1fr_620px] lg:items-center lg:gap-x-10 lg:gap-y-6",
|
|
104
104
|
className
|
|
105
105
|
),
|
|
106
106
|
children: [
|
|
@@ -121,10 +121,10 @@ function EventHubHeroSkeleton({ aside, footer, className }) {
|
|
|
121
121
|
]
|
|
122
122
|
},
|
|
123
123
|
index
|
|
124
|
-
)) })
|
|
125
|
-
footer && /* @__PURE__ */ jsx("div", { className: "order-4 pt-1.5 lg:order-none lg:pt-2", children: /* @__PURE__ */ jsx(Skeleton, { className: "h-[52px] w-full rounded-[14px]" }) })
|
|
124
|
+
)) })
|
|
126
125
|
] }),
|
|
127
|
-
aside && /* @__PURE__ */ jsx("div", { className: "min-w-0", children: aside })
|
|
126
|
+
aside && /* @__PURE__ */ jsx("div", { className: "min-w-0", children: aside }),
|
|
127
|
+
footer && /* @__PURE__ */ jsx("div", { className: "lg:col-span-2 lg:w-fit lg:min-w-[480px]", children: /* @__PURE__ */ jsx(Skeleton, { className: "h-[52px] w-full rounded-[14px]" }) })
|
|
128
128
|
]
|
|
129
129
|
}
|
|
130
130
|
);
|
|
@@ -2,7 +2,7 @@ import { Skeleton } from '../chunk-XBBEX4SF.js';
|
|
|
2
2
|
import { cn } from '../chunk-FEK5XGZW.js';
|
|
3
3
|
import { __objRest, __spreadValues } from '../chunk-MMUBH76A.js';
|
|
4
4
|
import * as React from 'react';
|
|
5
|
-
import { Clock, CalendarDays,
|
|
5
|
+
import { Clock, CalendarDays, ChevronRight } from 'lucide-react';
|
|
6
6
|
import { jsxs, jsx, Fragment } from 'react/jsx-runtime';
|
|
7
7
|
|
|
8
8
|
function DefaultLink(_a) {
|
|
@@ -44,7 +44,7 @@ function EventTonightStrip({
|
|
|
44
44
|
actionLabel
|
|
45
45
|
] })
|
|
46
46
|
] }),
|
|
47
|
-
interactive && /* @__PURE__ */ jsx(
|
|
47
|
+
interactive && /* @__PURE__ */ jsx(ChevronRight, { className: "h-4 w-4 shrink-0 text-muted-foreground", "aria-hidden": true })
|
|
48
48
|
] });
|
|
49
49
|
const rootClasses = cn(
|
|
50
50
|
"flex min-h-[52px] w-full items-center gap-2.5 rounded-[14px] border bg-card px-3.5 py-2 text-left",
|
|
@@ -3,12 +3,12 @@ import * as React from 'react';
|
|
|
3
3
|
import { VariantProps } from 'class-variance-authority';
|
|
4
4
|
|
|
5
5
|
declare const itemVariants: (props?: ({
|
|
6
|
-
variant?: "default" | "
|
|
6
|
+
variant?: "default" | "muted" | "outline" | null | undefined;
|
|
7
7
|
size?: "default" | "sm" | "lg" | null | undefined;
|
|
8
8
|
interactive?: boolean | null | undefined;
|
|
9
9
|
} & class_variance_authority_types.ClassProp) | undefined) => string;
|
|
10
10
|
declare const Item: React.ForwardRefExoticComponent<React.HTMLAttributes<HTMLDivElement> & VariantProps<(props?: ({
|
|
11
|
-
variant?: "default" | "
|
|
11
|
+
variant?: "default" | "muted" | "outline" | null | undefined;
|
|
12
12
|
size?: "default" | "sm" | "lg" | null | undefined;
|
|
13
13
|
interactive?: boolean | null | undefined;
|
|
14
14
|
} & class_variance_authority_types.ClassProp) | undefined) => string> & {
|
|
@@ -17,7 +17,7 @@ function DefaultLink(_a) {
|
|
|
17
17
|
}
|
|
18
18
|
var GROUND = PHOTO_GROUND;
|
|
19
19
|
var INK_PHONE = "bg-[linear-gradient(to_bottom,oklch(0.16_0.012_55_/_0.12)_0%,oklch(0.16_0.012_55_/_0.55)_38%,oklch(0.16_0.012_55_/_0.92)_62%,oklch(0.16_0.012_55_/_0.96)_100%)]";
|
|
20
|
-
var INK_WIDE = "lg:bg-[linear-gradient(to_right,oklch(0.16_0.012_55_/_0.
|
|
20
|
+
var INK_WIDE = "lg:bg-[linear-gradient(to_right,oklch(0.16_0.012_55_/_0.5)_0%,oklch(0.16_0.012_55_/_0.66)_34%,oklch(0.16_0.012_55_/_0.9)_52%,oklch(0.16_0.012_55_/_0.96)_100%)]";
|
|
21
21
|
var TEXT_SCRIM = (
|
|
22
22
|
/* `isolate` + `before:-z-10`: a positioned pseudo-element paints ABOVE the
|
|
23
23
|
column's in-flow text unless the column is its own stacking context and
|
|
@@ -70,8 +70,9 @@ function MenuFamilyBlock({
|
|
|
70
70
|
{
|
|
71
71
|
className: cn(
|
|
72
72
|
"relative flex min-h-[220px] flex-col justify-end p-3.5",
|
|
73
|
-
|
|
74
|
-
|
|
73
|
+
TEXT_SCRIM,
|
|
74
|
+
// The two-column card has ONE gradient from `lg` — see `INK_WIDE`.
|
|
75
|
+
wide && "lg:min-h-[320px] lg:p-5 lg:before:hidden"
|
|
75
76
|
),
|
|
76
77
|
children: [
|
|
77
78
|
pill && /* @__PURE__ */ jsx(
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { NEUTRAL_ICON_FILTER } from '../chunk-LQWGZIYA.js';
|
|
2
1
|
import { OVERLAY_PILL } from '../chunk-EZG2NELX.js';
|
|
3
2
|
import { HOVER_IMAGE_MOTION, HOVER_IMAGE_WRAPPER, HOVER_MOTION, HOVER_LIFT_GRID_GROUP } from '../chunk-XCB3F6SF.js';
|
|
3
|
+
import { NEUTRAL_ICON_FILTER } from '../chunk-LQWGZIYA.js';
|
|
4
4
|
import { GradeAtom } from '../chunk-DJD3EMPK.js';
|
|
5
5
|
import { showsGradeAtom } from '../chunk-I5V446BX.js';
|
|
6
6
|
import { Separator } from '../chunk-NWZGSLPU.js';
|
|
@@ -52,7 +52,9 @@ type PosterStackPoster = {
|
|
|
52
52
|
fallback?: React.ReactNode;
|
|
53
53
|
};
|
|
54
54
|
type PosterStackProps = {
|
|
55
|
-
/**
|
|
55
|
+
/** Candidates in order. The first three whose image loads are shown, so
|
|
56
|
+
* pass more than three: a poster whose asset fails yields its slot to
|
|
57
|
+
* the next candidate. */
|
|
56
58
|
posters: PosterStackPoster[];
|
|
57
59
|
/** Defaults to a plain `<a>`. Pass `next/link`, or an anchor that opens the app's preview. */
|
|
58
60
|
linkComponent?: React.ComponentType<PosterStackLinkProps>;
|
|
@@ -17,6 +17,7 @@ var POSTER_STACK_ANIMATION_STYLE = `
|
|
|
17
17
|
[data-ds-poster-stack-item] { animation: none !important; }
|
|
18
18
|
}
|
|
19
19
|
`;
|
|
20
|
+
var PHONE_STAGE_CLASS = "mx-auto w-[336px] max-w-full";
|
|
20
21
|
var POSTER_SLOTS = [
|
|
21
22
|
{
|
|
22
23
|
positionClassName: "left-0 top-[28px] w-[112px] md:left-0 md:top-[52px] md:w-[196px]",
|
|
@@ -64,72 +65,79 @@ function PosterStack({
|
|
|
64
65
|
observer.observe(container);
|
|
65
66
|
return () => observer.disconnect();
|
|
66
67
|
}, []);
|
|
67
|
-
const visiblePosters = posters.slice(0, POSTER_SLOTS.length);
|
|
68
|
-
return /* @__PURE__ */ jsxs(
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
68
|
+
const visiblePosters = posters.map((poster, index) => ({ poster, index })).filter(({ index }) => !failedIndices.has(index)).slice(0, POSTER_SLOTS.length);
|
|
69
|
+
return /* @__PURE__ */ jsxs(
|
|
70
|
+
"div",
|
|
71
|
+
{
|
|
72
|
+
ref: containerRef,
|
|
73
|
+
className: cn("relative h-[196px] md:h-[400px] md:w-auto md:mx-0", PHONE_STAGE_CLASS, className),
|
|
74
|
+
children: [
|
|
75
|
+
/* @__PURE__ */ jsx("style", { children: POSTER_STACK_ANIMATION_STYLE }),
|
|
76
|
+
visiblePosters.map(({ poster, index: candidateIndex }, index) => {
|
|
77
|
+
var _a, _b, _c;
|
|
78
|
+
const slot = POSTER_SLOTS[index];
|
|
79
|
+
const showPoster = !!poster.src;
|
|
80
|
+
const face = /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
81
|
+
showPoster ? (
|
|
82
|
+
// eslint-disable-next-line @next/next/no-img-element
|
|
83
|
+
/* @__PURE__ */ jsx(
|
|
84
|
+
"img",
|
|
85
|
+
{
|
|
86
|
+
src: poster.src,
|
|
87
|
+
alt: poster.caption ? "" : poster.alt,
|
|
88
|
+
loading: "lazy",
|
|
89
|
+
decoding: "async",
|
|
90
|
+
className: cn(
|
|
91
|
+
"h-full w-full object-cover",
|
|
92
|
+
poster.href && "transition-transform duration-300 group-hover/poster:scale-[1.03] motion-reduce:transition-none"
|
|
93
|
+
),
|
|
94
|
+
onError: () => setFailedIndices((current) => new Set(current).add(candidateIndex))
|
|
95
|
+
}
|
|
96
|
+
)
|
|
97
|
+
) : (_a = poster.fallback) != null ? _a : /* @__PURE__ */ jsx(EventPosterFallback, {}),
|
|
98
|
+
poster.caption && /* @__PURE__ */ jsx("span", { className: "absolute inset-x-0 bottom-0 bg-gradient-to-t from-black/70 to-transparent px-2 pb-2 pt-6", children: /* @__PURE__ */ jsx("span", { className: "line-clamp-2 text-[11px] font-semibold leading-[1.25] text-white md:text-[12.5px]", children: poster.caption }) })
|
|
99
|
+
] });
|
|
100
|
+
return /* @__PURE__ */ jsx(
|
|
101
|
+
"div",
|
|
79
102
|
{
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
103
|
+
ref: (node) => {
|
|
104
|
+
posterRefs.current[index] = node;
|
|
105
|
+
},
|
|
106
|
+
"data-ds-poster-stack-item": true,
|
|
84
107
|
className: cn(
|
|
85
|
-
"
|
|
86
|
-
|
|
108
|
+
"absolute aspect-[3/4] overflow-hidden rounded-xl bg-muted",
|
|
109
|
+
slot.positionClassName,
|
|
110
|
+
slot.zIndexClassName,
|
|
111
|
+
poster.href && "hover:z-[4] focus-within:z-[4]"
|
|
87
112
|
),
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
children: poster.href ? /* @__PURE__ */ jsx(
|
|
117
|
-
LinkComponent,
|
|
118
|
-
{
|
|
119
|
-
href: poster.href,
|
|
120
|
-
className: cn(
|
|
121
|
-
"group/poster relative block h-full w-full overflow-hidden rounded-xl",
|
|
122
|
-
"transition-transform duration-150 active:scale-[0.98] motion-reduce:transition-none",
|
|
123
|
-
"focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2"
|
|
124
|
-
),
|
|
125
|
-
children: face
|
|
126
|
-
}
|
|
127
|
-
) : /* @__PURE__ */ jsx("div", { className: "relative h-full w-full", children: face })
|
|
128
|
-
},
|
|
129
|
-
poster.alt || index
|
|
130
|
-
);
|
|
131
|
-
})
|
|
132
|
-
] });
|
|
113
|
+
style: {
|
|
114
|
+
transform: slot.restTransform,
|
|
115
|
+
boxShadow: POSTER_SHADOW,
|
|
116
|
+
animationName: slot.animationName,
|
|
117
|
+
animationDuration: `${slot.animationDurationMs}ms`,
|
|
118
|
+
animationTimingFunction: "ease-in-out",
|
|
119
|
+
animationIterationCount: "infinite",
|
|
120
|
+
animationDirection: "alternate"
|
|
121
|
+
},
|
|
122
|
+
children: poster.href ? /* @__PURE__ */ jsx(
|
|
123
|
+
LinkComponent,
|
|
124
|
+
{
|
|
125
|
+
href: poster.href,
|
|
126
|
+
className: cn(
|
|
127
|
+
"group/poster relative block h-full w-full overflow-hidden rounded-xl",
|
|
128
|
+
"transition-transform duration-150 active:scale-[0.98] motion-reduce:transition-none",
|
|
129
|
+
"focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2"
|
|
130
|
+
),
|
|
131
|
+
children: face
|
|
132
|
+
}
|
|
133
|
+
) : /* @__PURE__ */ jsx("div", { className: "relative h-full w-full", children: face })
|
|
134
|
+
},
|
|
135
|
+
(_c = (_b = poster.href) != null ? _b : poster.src) != null ? _c : candidateIndex
|
|
136
|
+
);
|
|
137
|
+
})
|
|
138
|
+
]
|
|
139
|
+
}
|
|
140
|
+
);
|
|
133
141
|
}
|
|
134
142
|
|
|
135
143
|
export { PosterStack };
|
|
@@ -4,11 +4,11 @@ import { VariantProps } from 'class-variance-authority';
|
|
|
4
4
|
|
|
5
5
|
declare const spinnerVariants: (props?: ({
|
|
6
6
|
size?: "default" | "xs" | "sm" | "lg" | "xl" | null | undefined;
|
|
7
|
-
tone?: "
|
|
7
|
+
tone?: "default" | "destructive" | "inherit" | "primary" | null | undefined;
|
|
8
8
|
} & class_variance_authority_types.ClassProp) | undefined) => string;
|
|
9
9
|
declare const Spinner: React.ForwardRefExoticComponent<React.HTMLAttributes<HTMLSpanElement> & VariantProps<(props?: ({
|
|
10
10
|
size?: "default" | "xs" | "sm" | "lg" | "xl" | null | undefined;
|
|
11
|
-
tone?: "
|
|
11
|
+
tone?: "default" | "destructive" | "inherit" | "primary" | null | undefined;
|
|
12
12
|
} & class_variance_authority_types.ClassProp) | undefined) => string> & {
|
|
13
13
|
/** Visually-hidden label for screen readers. Defaults to "Loading". */
|
|
14
14
|
label?: string;
|
package/dist/components/tag.d.ts
CHANGED
|
@@ -3,11 +3,11 @@ import * as React from 'react';
|
|
|
3
3
|
import { VariantProps } from 'class-variance-authority';
|
|
4
4
|
|
|
5
5
|
declare const tagVariants: (props?: ({
|
|
6
|
-
variant?: "
|
|
6
|
+
variant?: "default" | "destructive" | "warning" | "success" | "primary" | "outline" | "quiet" | null | undefined;
|
|
7
7
|
size?: "default" | "sm" | "lg" | null | undefined;
|
|
8
8
|
} & class_variance_authority_types.ClassProp) | undefined) => string;
|
|
9
9
|
declare const Tag: React.ForwardRefExoticComponent<React.HTMLAttributes<HTMLSpanElement> & VariantProps<(props?: ({
|
|
10
|
-
variant?: "
|
|
10
|
+
variant?: "default" | "destructive" | "warning" | "success" | "primary" | "outline" | "quiet" | null | undefined;
|
|
11
11
|
size?: "default" | "sm" | "lg" | null | undefined;
|
|
12
12
|
} & class_variance_authority_types.ClassProp) | undefined) => string> & {
|
|
13
13
|
onRemove?: () => void;
|
|
@@ -22,9 +22,9 @@ import { VariantProps } from 'class-variance-authority';
|
|
|
22
22
|
* Weights are 400 / 600 / 700. 500 is not in the system.
|
|
23
23
|
*/
|
|
24
24
|
declare const textVariants: (props?: ({
|
|
25
|
-
variant?: "
|
|
26
|
-
color?: "
|
|
27
|
-
weight?: "bold" | "
|
|
25
|
+
variant?: "small" | "display" | "h1" | "h2" | "h3" | "h4" | "body-lg" | "body" | "body-sm" | "caption" | "label" | "overline" | null | undefined;
|
|
26
|
+
color?: "default" | "destructive" | "inherit" | "muted" | "primary" | "primary-foreground" | null | undefined;
|
|
27
|
+
weight?: "bold" | "normal" | "medium" | "semibold" | null | undefined;
|
|
28
28
|
leading?: "none" | "normal" | "tight" | "snug" | "relaxed" | "loose" | null | undefined;
|
|
29
29
|
tracking?: "normal" | "tight" | "wide" | "wider" | null | undefined;
|
|
30
30
|
wrap?: "balance" | "pretty" | "nowrap" | null | undefined;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { TimelineHourSkeleton } from '../chunk-
|
|
2
|
-
import '../chunk-
|
|
1
|
+
import { TimelineHourSkeleton } from '../chunk-QRSIJC4X.js';
|
|
2
|
+
import '../chunk-BRXI6EDO.js';
|
|
3
3
|
import { Separator } from '../chunk-NWZGSLPU.js';
|
|
4
4
|
import { Skeleton } from '../chunk-XBBEX4SF.js';
|
|
5
5
|
import { cn } from '../chunk-FEK5XGZW.js';
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
export { TimelineHour, TimelineHourSkeleton } from '../chunk-
|
|
2
|
-
import '../chunk-
|
|
1
|
+
export { TimelineHour, TimelineHourSkeleton } from '../chunk-QRSIJC4X.js';
|
|
2
|
+
import '../chunk-BRXI6EDO.js';
|
|
3
3
|
import '../chunk-NWZGSLPU.js';
|
|
4
4
|
import '../chunk-XBBEX4SF.js';
|
|
5
5
|
import '../chunk-FEK5XGZW.js';
|
package/package.json
CHANGED
|
@@ -48,9 +48,11 @@ export type EventHubHeroProps = {
|
|
|
48
48
|
/** The right-column slot — the poster stack in the source design. */
|
|
49
49
|
aside?: React.ReactNode;
|
|
50
50
|
/**
|
|
51
|
-
* The
|
|
52
|
-
*
|
|
53
|
-
*
|
|
51
|
+
* The hub's tonight state strip. The figures say scale; this says what is
|
|
52
|
+
* on right now, and it is the one thing on the first screen a reader can
|
|
53
|
+
* tap into the timeline. On a phone it follows the aside — title, figures,
|
|
54
|
+
* posters, then the one line that says what is on — and from `lg` it is a
|
|
55
|
+
* content-width row under both columns.
|
|
54
56
|
*/
|
|
55
57
|
footer?: React.ReactNode;
|
|
56
58
|
className?: string;
|
|
@@ -69,10 +71,10 @@ export function EventHubHero({
|
|
|
69
71
|
return (
|
|
70
72
|
<div
|
|
71
73
|
className={cn(
|
|
72
|
-
//
|
|
73
|
-
// tappable poster stack the aside
|
|
74
|
-
//
|
|
75
|
-
"grid grid-cols-1 gap-
|
|
74
|
+
// 12px between the phone's blocks — the page's rhythm, the same as
|
|
75
|
+
// between the sections below: with a tappable poster stack the aside
|
|
76
|
+
// is content, not decoration, and gets no section gap of its own.
|
|
77
|
+
"grid grid-cols-1 gap-3 lg:grid-cols-[1fr_620px] lg:items-center lg:gap-x-10 lg:gap-y-6",
|
|
76
78
|
className
|
|
77
79
|
)}
|
|
78
80
|
>
|
|
@@ -141,10 +143,11 @@ export function EventHubHero({
|
|
|
141
143
|
);
|
|
142
144
|
})}
|
|
143
145
|
</div>
|
|
144
|
-
{footer && <div className="order-4 pt-1.5 lg:order-none lg:pt-2">{footer}</div>}
|
|
145
146
|
</div>
|
|
146
147
|
|
|
147
148
|
{aside && <div className="min-w-0">{aside}</div>}
|
|
149
|
+
|
|
150
|
+
{footer && <div className="lg:col-span-2 lg:w-fit lg:min-w-[480px]">{footer}</div>}
|
|
148
151
|
</div>
|
|
149
152
|
);
|
|
150
153
|
}
|
|
@@ -162,7 +165,7 @@ export function EventHubHeroSkeleton({ aside, footer, className }: EventHubHeroS
|
|
|
162
165
|
<div
|
|
163
166
|
aria-hidden="true"
|
|
164
167
|
className={cn(
|
|
165
|
-
"grid grid-cols-1 gap-
|
|
168
|
+
"grid grid-cols-1 gap-3 lg:grid-cols-[1fr_620px] lg:items-center lg:gap-x-10 lg:gap-y-6",
|
|
166
169
|
className
|
|
167
170
|
)}
|
|
168
171
|
>
|
|
@@ -187,14 +190,15 @@ export function EventHubHeroSkeleton({ aside, footer, className }: EventHubHeroS
|
|
|
187
190
|
</div>
|
|
188
191
|
))}
|
|
189
192
|
</div>
|
|
190
|
-
{footer && (
|
|
191
|
-
<div className="order-4 pt-1.5 lg:order-none lg:pt-2">
|
|
192
|
-
<Skeleton className="h-[52px] w-full rounded-[14px]" />
|
|
193
|
-
</div>
|
|
194
|
-
)}
|
|
195
193
|
</div>
|
|
196
194
|
|
|
197
195
|
{aside && <div className="min-w-0">{aside}</div>}
|
|
196
|
+
|
|
197
|
+
{footer && (
|
|
198
|
+
<div className="lg:col-span-2 lg:w-fit lg:min-w-[480px]">
|
|
199
|
+
<Skeleton className="h-[52px] w-full rounded-[14px]" />
|
|
200
|
+
</div>
|
|
201
|
+
)}
|
|
198
202
|
</div>
|
|
199
203
|
);
|
|
200
204
|
}
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import { describe, it, expect } from "vitest";
|
|
2
|
-
import { render, screen } from "@testing-library/react";
|
|
2
|
+
import { fireEvent, render, screen } from "@testing-library/react";
|
|
3
3
|
import { EventTonightStrip } from "./event-tonight-strip";
|
|
4
|
-
import { EventDoor } from "./event-door";
|
|
5
4
|
import { PosterStack } from "./poster-stack";
|
|
6
5
|
|
|
7
6
|
describe("EventTonightStrip", () => {
|
|
@@ -29,15 +28,6 @@ describe("EventTonightStrip", () => {
|
|
|
29
28
|
});
|
|
30
29
|
});
|
|
31
30
|
|
|
32
|
-
describe("EventDoor", () => {
|
|
33
|
-
it("is an anchor named by the category, with the image decorative", () => {
|
|
34
|
-
render(<EventDoor name="Concerts" href="/events/concerts" imageSrc="/stage.webp" />);
|
|
35
|
-
const link = screen.getByRole("link", { name: "Concerts" });
|
|
36
|
-
expect(link).toHaveAttribute("href", "/events/concerts");
|
|
37
|
-
expect(screen.getByRole("presentation", { hidden: true })).toHaveAttribute("alt", "");
|
|
38
|
-
});
|
|
39
|
-
});
|
|
40
|
-
|
|
41
31
|
describe("PosterStack", () => {
|
|
42
32
|
it("makes a captioned poster an anchor named by its caption", () => {
|
|
43
33
|
// The float's reduced-motion check reads matchMedia, which jsdom lacks.
|
|
@@ -56,4 +46,24 @@ describe("PosterStack", () => {
|
|
|
56
46
|
expect(screen.getAllByRole("link")).toHaveLength(1);
|
|
57
47
|
expect(screen.getByAltText("B poster")).toBeInTheDocument();
|
|
58
48
|
});
|
|
49
|
+
|
|
50
|
+
it("gives a failed poster's slot to the next candidate", () => {
|
|
51
|
+
window.matchMedia = () => ({ matches: true }) as MediaQueryList;
|
|
52
|
+
const poster = (id: string) => ({ src: `/${id}.jpg`, alt: id, caption: id, href: `/e/${id}` });
|
|
53
|
+
render(<PosterStack posters={[poster("a"), poster("b"), poster("c"), poster("d")]} />);
|
|
54
|
+
expect(screen.getAllByRole("link").map((l) => l.getAttribute("href"))).toEqual([
|
|
55
|
+
"/e/a",
|
|
56
|
+
"/e/b",
|
|
57
|
+
"/e/c",
|
|
58
|
+
]);
|
|
59
|
+
|
|
60
|
+
// The middle poster's asset dies at load time.
|
|
61
|
+
fireEvent.error(screen.getByRole("link", { name: "b" }).querySelector("img")!);
|
|
62
|
+
|
|
63
|
+
expect(screen.getAllByRole("link").map((l) => l.getAttribute("href"))).toEqual([
|
|
64
|
+
"/e/a",
|
|
65
|
+
"/e/c",
|
|
66
|
+
"/e/d",
|
|
67
|
+
]);
|
|
68
|
+
});
|
|
59
69
|
});
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
|
|
3
3
|
import * as React from "react";
|
|
4
|
-
import {
|
|
4
|
+
import { CalendarDays, ChevronRight, Clock } from "lucide-react";
|
|
5
5
|
import { cn } from "../lib/utils";
|
|
6
6
|
import { Skeleton } from "./skeleton";
|
|
7
7
|
|
|
@@ -98,7 +98,7 @@ export function EventTonightStrip({
|
|
|
98
98
|
{interactive && actionLabel && <span className="sr-only">. {actionLabel}</span>}
|
|
99
99
|
</span>
|
|
100
100
|
{interactive && (
|
|
101
|
-
<
|
|
101
|
+
<ChevronRight className="h-4 w-4 shrink-0 text-muted-foreground" aria-hidden />
|
|
102
102
|
)}
|
|
103
103
|
</>
|
|
104
104
|
);
|
|
@@ -123,12 +123,18 @@ const GROUND = PHOTO_GROUND;
|
|
|
123
123
|
const INK_PHONE =
|
|
124
124
|
"bg-[linear-gradient(to_bottom,oklch(0.16_0.012_55_/_0.12)_0%,oklch(0.16_0.012_55_/_0.55)_38%,oklch(0.16_0.012_55_/_0.92)_62%,oklch(0.16_0.012_55_/_0.96)_100%)]";
|
|
125
125
|
/**
|
|
126
|
-
* From `lg` the ramp runs to the right:
|
|
127
|
-
*
|
|
128
|
-
*
|
|
126
|
+
* From `lg` the ramp runs to the right: over the photo third, dark under the
|
|
127
|
+
* rows — the rows sit on the ink itself, no opaque paper, so the photograph
|
|
128
|
+
* still reads through the ramp's shoulder.
|
|
129
|
+
*
|
|
130
|
+
* THE ONLY GRADIENT FROM `lg`. The phone's local text scrim (below) is off
|
|
131
|
+
* at this width: a vertical scrim rising from the photo column's foot met
|
|
132
|
+
* this horizontal ramp at the column's bottom-right corner and the two read
|
|
133
|
+
* as a seam. Its contrast job moved here — the ramp opens at 0.5 rather
|
|
134
|
+
* than 0.32, which is what the name and count needed on a bright photograph.
|
|
129
135
|
*/
|
|
130
136
|
const INK_WIDE =
|
|
131
|
-
"lg:bg-[linear-gradient(to_right,oklch(0.16_0.012_55_/_0.
|
|
137
|
+
"lg:bg-[linear-gradient(to_right,oklch(0.16_0.012_55_/_0.5)_0%,oklch(0.16_0.012_55_/_0.66)_34%,oklch(0.16_0.012_55_/_0.9)_52%,oklch(0.16_0.012_55_/_0.96)_100%)]";
|
|
132
138
|
|
|
133
139
|
/**
|
|
134
140
|
* A second, LOCAL scrim under the name, count and action — the photo column's
|
|
@@ -200,8 +206,9 @@ export function MenuFamilyBlock({
|
|
|
200
206
|
<div
|
|
201
207
|
className={cn(
|
|
202
208
|
"relative flex min-h-[220px] flex-col justify-end p-3.5",
|
|
203
|
-
|
|
204
|
-
|
|
209
|
+
TEXT_SCRIM,
|
|
210
|
+
// The two-column card has ONE gradient from `lg` — see `INK_WIDE`.
|
|
211
|
+
wide && "lg:min-h-[320px] lg:p-5 lg:before:hidden"
|
|
205
212
|
)}
|
|
206
213
|
>
|
|
207
214
|
{pill && (
|
|
@@ -63,7 +63,9 @@ export type PosterStackPoster = {
|
|
|
63
63
|
};
|
|
64
64
|
|
|
65
65
|
export type PosterStackProps = {
|
|
66
|
-
/**
|
|
66
|
+
/** Candidates in order. The first three whose image loads are shown, so
|
|
67
|
+
* pass more than three: a poster whose asset fails yields its slot to
|
|
68
|
+
* the next candidate. */
|
|
67
69
|
posters: PosterStackPoster[];
|
|
68
70
|
/** Defaults to a plain `<a>`. Pass `next/link`, or an anchor that opens the app's preview. */
|
|
69
71
|
linkComponent?: React.ComponentType<PosterStackLinkProps>;
|
|
@@ -90,11 +92,14 @@ type PosterSlotConfig = {
|
|
|
90
92
|
};
|
|
91
93
|
|
|
92
94
|
/**
|
|
93
|
-
* Phone geometry
|
|
94
|
-
*
|
|
95
|
-
*
|
|
96
|
-
*
|
|
95
|
+
* Phone geometry: the three slots span 336px (112 / 128 / 112 wide, the
|
|
96
|
+
* outer two overlapping the middle by 8px each side), inside a stage the
|
|
97
|
+
* component centres in whatever column it is given — the stack used to hug
|
|
98
|
+
* the left gutter. 196px tall, the height the lowest slot actually reaches,
|
|
99
|
+
* so the block below sits at the page's rhythm rather than under 20px of
|
|
100
|
+
* empty stage. Touch targets are 112×149 and 128×171.
|
|
97
101
|
*/
|
|
102
|
+
const PHONE_STAGE_CLASS = "mx-auto w-[336px] max-w-full";
|
|
98
103
|
const POSTER_SLOTS: PosterSlotConfig[] = [
|
|
99
104
|
{
|
|
100
105
|
positionClassName: "left-0 top-[28px] w-[112px] md:left-0 md:top-[52px] md:w-[196px]",
|
|
@@ -151,15 +156,25 @@ export function PosterStack({
|
|
|
151
156
|
return () => observer.disconnect();
|
|
152
157
|
}, []);
|
|
153
158
|
|
|
154
|
-
|
|
159
|
+
// The first three candidates whose image has not failed. A poster whose
|
|
160
|
+
// asset 404s or 502s at load time gives its slot to the next candidate —
|
|
161
|
+
// the stack is the hub's first screen, and a fallback glyph in the middle
|
|
162
|
+
// of three posters is a hole in it. Pass more candidates than slots.
|
|
163
|
+
const visiblePosters = posters
|
|
164
|
+
.map((poster, index) => ({ poster, index }))
|
|
165
|
+
.filter(({ index }) => !failedIndices.has(index))
|
|
166
|
+
.slice(0, POSTER_SLOTS.length);
|
|
155
167
|
|
|
156
168
|
return (
|
|
157
|
-
<div
|
|
169
|
+
<div
|
|
170
|
+
ref={containerRef}
|
|
171
|
+
className={cn("relative h-[196px] md:h-[400px] md:w-auto md:mx-0", PHONE_STAGE_CLASS, className)}
|
|
172
|
+
>
|
|
158
173
|
<style>{POSTER_STACK_ANIMATION_STYLE}</style>
|
|
159
174
|
|
|
160
|
-
{visiblePosters.map((poster, index) => {
|
|
175
|
+
{visiblePosters.map(({ poster, index: candidateIndex }, index) => {
|
|
161
176
|
const slot = POSTER_SLOTS[index];
|
|
162
|
-
const showPoster = !!poster.src
|
|
177
|
+
const showPoster = !!poster.src;
|
|
163
178
|
const face = (
|
|
164
179
|
<>
|
|
165
180
|
{showPoster ? (
|
|
@@ -174,7 +189,9 @@ export function PosterStack({
|
|
|
174
189
|
poster.href &&
|
|
175
190
|
"transition-transform duration-300 group-hover/poster:scale-[1.03] motion-reduce:transition-none"
|
|
176
191
|
)}
|
|
177
|
-
onError={() =>
|
|
192
|
+
onError={() =>
|
|
193
|
+
setFailedIndices((current) => new Set(current).add(candidateIndex))
|
|
194
|
+
}
|
|
178
195
|
/>
|
|
179
196
|
) : (
|
|
180
197
|
(poster.fallback ?? <EventPosterFallback />)
|
|
@@ -191,7 +208,9 @@ export function PosterStack({
|
|
|
191
208
|
|
|
192
209
|
return (
|
|
193
210
|
<div
|
|
194
|
-
|
|
211
|
+
// Keyed by candidate, not slot: a replacement mounts fresh in the
|
|
212
|
+
// slot the failed poster vacated instead of inheriting its state.
|
|
213
|
+
key={poster.href ?? poster.src ?? candidateIndex}
|
|
195
214
|
ref={(node) => {
|
|
196
215
|
posterRefs.current[index] = node;
|
|
197
216
|
}}
|
|
@@ -112,8 +112,11 @@ export function TimelineHourPuck({
|
|
|
112
112
|
cn(
|
|
113
113
|
chipClasses,
|
|
114
114
|
// The station look from `lg`: bordered, background-filled,
|
|
115
|
-
//
|
|
116
|
-
|
|
115
|
+
// content-width. Every chip utility above has an `lg:` twin.
|
|
116
|
+
// `min-w-max`, NOT `min-w-0`: the host centres the puck inside
|
|
117
|
+
// a zero-width flex box, and a flex item allowed to shrink to 0
|
|
118
|
+
// collapses to a 24px square behind its own text.
|
|
119
|
+
"lg:h-auto lg:min-w-max lg:shrink-0 lg:gap-0 lg:border-border lg:bg-background lg:px-[7px] lg:py-[3px] lg:text-[12.5px] lg:text-foreground",
|
|
117
120
|
isLive && "lg:border-success/40 lg:bg-background lg:text-success"
|
|
118
121
|
)
|
|
119
122
|
)}
|
|
@@ -1,41 +0,0 @@
|
|
|
1
|
-
import * as react_jsx_runtime from 'react/jsx-runtime';
|
|
2
|
-
import * as React from 'react';
|
|
3
|
-
|
|
4
|
-
/**
|
|
5
|
-
* EventDoor — a square tile that is a category's entry point: a photograph of
|
|
6
|
-
* the KIND of night (a stage, a dance floor, a terrace), a scrim, the name.
|
|
7
|
-
*
|
|
8
|
-
* A door and an event row must not look alike. `CategoryDeck` fans four event
|
|
9
|
-
* posters and prints a count, which reads as "these four events"; a card with
|
|
10
|
-
* a single event's poster reads as "tap = that event" and changes daily. The
|
|
11
|
-
* 1:1 shape with the name INSIDE the image is what says "door, not event", and
|
|
12
|
-
* it carries no count — a count is a claim about scale the hub's figures
|
|
13
|
-
* already make, and on a door it competes with the name.
|
|
14
|
-
*
|
|
15
|
-
* Always an anchor: a door with no destination is not a door, so `href` is
|
|
16
|
-
* required. The scrim is a gradient over a photograph, which is mode-
|
|
17
|
-
* independent — the same measured treatment `DoorwayTiles` uses.
|
|
18
|
-
*/
|
|
19
|
-
type EventDoorLinkProps = {
|
|
20
|
-
href: string;
|
|
21
|
-
className?: string;
|
|
22
|
-
children: React.ReactNode;
|
|
23
|
-
onClick?: (mouseEvent: React.MouseEvent) => void;
|
|
24
|
-
};
|
|
25
|
-
type EventDoorProps = {
|
|
26
|
-
/** The category's name — the anchor's accessible name and the tile's label. */
|
|
27
|
-
name: string;
|
|
28
|
-
href: string;
|
|
29
|
-
imageSrc?: string;
|
|
30
|
-
/** Rendered when `imageSrc` is absent or fails. Defaults to `<EventPosterFallback />`. */
|
|
31
|
-
fallback?: React.ReactNode;
|
|
32
|
-
linkComponent?: React.ComponentType<EventDoorLinkProps>;
|
|
33
|
-
onClick?: (mouseEvent: React.MouseEvent) => void;
|
|
34
|
-
className?: string;
|
|
35
|
-
};
|
|
36
|
-
declare function EventDoor({ name, href, imageSrc, fallback, linkComponent: LinkComponent, onClick, className, }: EventDoorProps): react_jsx_runtime.JSX.Element;
|
|
37
|
-
declare function EventDoorSkeleton({ className }: {
|
|
38
|
-
className?: string;
|
|
39
|
-
}): react_jsx_runtime.JSX.Element;
|
|
40
|
-
|
|
41
|
-
export { EventDoor, type EventDoorLinkProps, type EventDoorProps, EventDoorSkeleton };
|
|
@@ -1,65 +0,0 @@
|
|
|
1
|
-
import { EventPosterFallback } from '../chunk-FDITZ2VM.js';
|
|
2
|
-
import { Skeleton } from '../chunk-XBBEX4SF.js';
|
|
3
|
-
import { cn } from '../chunk-FEK5XGZW.js';
|
|
4
|
-
import { __objRest, __spreadValues } from '../chunk-MMUBH76A.js';
|
|
5
|
-
import { useState } from 'react';
|
|
6
|
-
import { jsxs, jsx } from 'react/jsx-runtime';
|
|
7
|
-
|
|
8
|
-
function DefaultLink(_a) {
|
|
9
|
-
var _b = _a, { href } = _b, props = __objRest(_b, ["href"]);
|
|
10
|
-
return /* @__PURE__ */ jsx("a", __spreadValues({ href }, props));
|
|
11
|
-
}
|
|
12
|
-
function EventDoor({
|
|
13
|
-
name,
|
|
14
|
-
href,
|
|
15
|
-
imageSrc,
|
|
16
|
-
fallback = /* @__PURE__ */ jsx(EventPosterFallback, {}),
|
|
17
|
-
linkComponent: LinkComponent = DefaultLink,
|
|
18
|
-
onClick,
|
|
19
|
-
className
|
|
20
|
-
}) {
|
|
21
|
-
const [failed, setFailed] = useState(false);
|
|
22
|
-
const showImage = !!imageSrc && !failed;
|
|
23
|
-
return /* @__PURE__ */ jsxs(
|
|
24
|
-
LinkComponent,
|
|
25
|
-
{
|
|
26
|
-
href,
|
|
27
|
-
onClick,
|
|
28
|
-
className: cn(
|
|
29
|
-
"group relative block aspect-square overflow-hidden rounded-xl bg-muted",
|
|
30
|
-
"transition-transform duration-150 active:scale-[0.98] motion-reduce:transition-none",
|
|
31
|
-
"focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2",
|
|
32
|
-
className
|
|
33
|
-
),
|
|
34
|
-
children: [
|
|
35
|
-
showImage ? (
|
|
36
|
-
// eslint-disable-next-line @next/next/no-img-element
|
|
37
|
-
/* @__PURE__ */ jsx(
|
|
38
|
-
"img",
|
|
39
|
-
{
|
|
40
|
-
src: imageSrc,
|
|
41
|
-
alt: "",
|
|
42
|
-
loading: "lazy",
|
|
43
|
-
decoding: "async",
|
|
44
|
-
className: "h-full w-full object-cover transition-transform duration-300 group-hover:scale-[1.04] motion-reduce:transition-none",
|
|
45
|
-
onError: () => setFailed(true)
|
|
46
|
-
}
|
|
47
|
-
)
|
|
48
|
-
) : fallback,
|
|
49
|
-
/* @__PURE__ */ jsx(
|
|
50
|
-
"span",
|
|
51
|
-
{
|
|
52
|
-
"aria-hidden": true,
|
|
53
|
-
className: "absolute inset-x-0 bottom-0 h-3/4 bg-gradient-to-t from-black/65 via-black/45 to-transparent"
|
|
54
|
-
}
|
|
55
|
-
),
|
|
56
|
-
/* @__PURE__ */ jsx("span", { className: "absolute inset-x-2 bottom-2 line-clamp-2 text-[12.5px] font-bold leading-[1.2] tracking-[-0.01em] text-white", children: name })
|
|
57
|
-
]
|
|
58
|
-
}
|
|
59
|
-
);
|
|
60
|
-
}
|
|
61
|
-
function EventDoorSkeleton({ className }) {
|
|
62
|
-
return /* @__PURE__ */ jsx(Skeleton, { className: cn("aspect-square w-full rounded-xl", className) });
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
export { EventDoor, EventDoorSkeleton };
|
|
@@ -1,88 +0,0 @@
|
|
|
1
|
-
import type { Meta, StoryObj } from "@storybook/react";
|
|
2
|
-
import { EventDoor, EventDoorSkeleton } from "./event-door";
|
|
3
|
-
import { SnapRail } from "./snap-rail";
|
|
4
|
-
|
|
5
|
-
function photo(label: string, fill: string) {
|
|
6
|
-
return (
|
|
7
|
-
"data:image/svg+xml;utf8," +
|
|
8
|
-
encodeURIComponent(
|
|
9
|
-
`<svg xmlns="http://www.w3.org/2000/svg" width="320" height="320"><rect width="320" height="320" fill="${fill}"/><text x="50%" y="50%" font-family="monospace" font-size="22" fill="#fff" text-anchor="middle">${label}</text></svg>`
|
|
10
|
-
)
|
|
11
|
-
);
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
const DOORS = [
|
|
15
|
-
{ name: "Concerts", fill: "#7a5c46" },
|
|
16
|
-
{ name: "Parties", fill: "#4a6a5c" },
|
|
17
|
-
{ name: "Live music", fill: "#6a5c8a" },
|
|
18
|
-
{ name: "Stand-up", fill: "#8a6a4a" },
|
|
19
|
-
{ name: "Theatre", fill: "#4a5a7a" },
|
|
20
|
-
{ name: "Workshops", fill: "#5a7a4a" },
|
|
21
|
-
{ name: "Markets", fill: "#7a4a5a" },
|
|
22
|
-
{ name: "Film", fill: "#4a4a4a" },
|
|
23
|
-
];
|
|
24
|
-
|
|
25
|
-
const meta: Meta<typeof EventDoor> = {
|
|
26
|
-
title: "Events/EventDoor",
|
|
27
|
-
component: EventDoor,
|
|
28
|
-
tags: ["autodocs"],
|
|
29
|
-
args: {
|
|
30
|
-
name: "Concerts",
|
|
31
|
-
href: "#concerts",
|
|
32
|
-
imageSrc: photo("stage", "#7a5c46"),
|
|
33
|
-
},
|
|
34
|
-
decorators: [
|
|
35
|
-
(Story) => (
|
|
36
|
-
<div className="w-[104px]">
|
|
37
|
-
<Story />
|
|
38
|
-
</div>
|
|
39
|
-
),
|
|
40
|
-
],
|
|
41
|
-
parameters: {
|
|
42
|
-
docs: {
|
|
43
|
-
description: {
|
|
44
|
-
component:
|
|
45
|
-
"A category's entry point: a square photograph of the kind of night, a scrim, the name inside the image, no count. The 1:1 shape with the name inside is what says door rather than event. Always an anchor.",
|
|
46
|
-
},
|
|
47
|
-
},
|
|
48
|
-
},
|
|
49
|
-
};
|
|
50
|
-
|
|
51
|
-
export default meta;
|
|
52
|
-
type Story = StoryObj<typeof EventDoor>;
|
|
53
|
-
|
|
54
|
-
export const Default: Story = {};
|
|
55
|
-
|
|
56
|
-
/** No image — the poster fallback carries the tile. */
|
|
57
|
-
export const NoImage: Story = {
|
|
58
|
-
args: { imageSrc: undefined },
|
|
59
|
-
};
|
|
60
|
-
|
|
61
|
-
/** The hub's door row on a phone: a snap rail of squares, three and a third visible. */
|
|
62
|
-
export const Row: Story = {
|
|
63
|
-
decorators: [
|
|
64
|
-
(Story) => (
|
|
65
|
-
<div className="w-[375px] px-4" style={{ "--page-gutter": "1rem" } as React.CSSProperties}>
|
|
66
|
-
<Story />
|
|
67
|
-
</div>
|
|
68
|
-
),
|
|
69
|
-
],
|
|
70
|
-
render: () => (
|
|
71
|
-
<nav aria-label="Browse by kind">
|
|
72
|
-
<SnapRail itemWidth="xs" gap="sm" bleed="page">
|
|
73
|
-
{DOORS.map((door) => (
|
|
74
|
-
<EventDoor
|
|
75
|
-
key={door.name}
|
|
76
|
-
name={door.name}
|
|
77
|
-
href={`#${door.name}`}
|
|
78
|
-
imageSrc={photo(door.name.toLowerCase(), door.fill)}
|
|
79
|
-
/>
|
|
80
|
-
))}
|
|
81
|
-
</SnapRail>
|
|
82
|
-
</nav>
|
|
83
|
-
),
|
|
84
|
-
};
|
|
85
|
-
|
|
86
|
-
export const Skeleton: Story = {
|
|
87
|
-
render: () => <EventDoorSkeleton />,
|
|
88
|
-
};
|
|
@@ -1,97 +0,0 @@
|
|
|
1
|
-
"use client";
|
|
2
|
-
|
|
3
|
-
import * as React from "react";
|
|
4
|
-
import { useState } from "react";
|
|
5
|
-
import { cn } from "../lib/utils";
|
|
6
|
-
import { EventPosterFallback } from "./event-poster-fallback";
|
|
7
|
-
import { Skeleton } from "./skeleton";
|
|
8
|
-
|
|
9
|
-
/**
|
|
10
|
-
* EventDoor — a square tile that is a category's entry point: a photograph of
|
|
11
|
-
* the KIND of night (a stage, a dance floor, a terrace), a scrim, the name.
|
|
12
|
-
*
|
|
13
|
-
* A door and an event row must not look alike. `CategoryDeck` fans four event
|
|
14
|
-
* posters and prints a count, which reads as "these four events"; a card with
|
|
15
|
-
* a single event's poster reads as "tap = that event" and changes daily. The
|
|
16
|
-
* 1:1 shape with the name INSIDE the image is what says "door, not event", and
|
|
17
|
-
* it carries no count — a count is a claim about scale the hub's figures
|
|
18
|
-
* already make, and on a door it competes with the name.
|
|
19
|
-
*
|
|
20
|
-
* Always an anchor: a door with no destination is not a door, so `href` is
|
|
21
|
-
* required. The scrim is a gradient over a photograph, which is mode-
|
|
22
|
-
* independent — the same measured treatment `DoorwayTiles` uses.
|
|
23
|
-
*/
|
|
24
|
-
|
|
25
|
-
export type EventDoorLinkProps = {
|
|
26
|
-
href: string;
|
|
27
|
-
className?: string;
|
|
28
|
-
children: React.ReactNode;
|
|
29
|
-
onClick?: (mouseEvent: React.MouseEvent) => void;
|
|
30
|
-
};
|
|
31
|
-
|
|
32
|
-
function DefaultLink({ href, ...props }: EventDoorLinkProps) {
|
|
33
|
-
return <a href={href} {...props} />;
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
export type EventDoorProps = {
|
|
37
|
-
/** The category's name — the anchor's accessible name and the tile's label. */
|
|
38
|
-
name: string;
|
|
39
|
-
href: string;
|
|
40
|
-
imageSrc?: string;
|
|
41
|
-
/** Rendered when `imageSrc` is absent or fails. Defaults to `<EventPosterFallback />`. */
|
|
42
|
-
fallback?: React.ReactNode;
|
|
43
|
-
linkComponent?: React.ComponentType<EventDoorLinkProps>;
|
|
44
|
-
onClick?: (mouseEvent: React.MouseEvent) => void;
|
|
45
|
-
className?: string;
|
|
46
|
-
};
|
|
47
|
-
|
|
48
|
-
export function EventDoor({
|
|
49
|
-
name,
|
|
50
|
-
href,
|
|
51
|
-
imageSrc,
|
|
52
|
-
fallback = <EventPosterFallback />,
|
|
53
|
-
linkComponent: LinkComponent = DefaultLink,
|
|
54
|
-
onClick,
|
|
55
|
-
className,
|
|
56
|
-
}: EventDoorProps) {
|
|
57
|
-
const [failed, setFailed] = useState(false);
|
|
58
|
-
const showImage = !!imageSrc && !failed;
|
|
59
|
-
|
|
60
|
-
return (
|
|
61
|
-
<LinkComponent
|
|
62
|
-
href={href}
|
|
63
|
-
onClick={onClick}
|
|
64
|
-
className={cn(
|
|
65
|
-
"group relative block aspect-square overflow-hidden rounded-xl bg-muted",
|
|
66
|
-
"transition-transform duration-150 active:scale-[0.98] motion-reduce:transition-none",
|
|
67
|
-
"focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2",
|
|
68
|
-
className
|
|
69
|
-
)}
|
|
70
|
-
>
|
|
71
|
-
{showImage ? (
|
|
72
|
-
// eslint-disable-next-line @next/next/no-img-element
|
|
73
|
-
<img
|
|
74
|
-
src={imageSrc}
|
|
75
|
-
alt=""
|
|
76
|
-
loading="lazy"
|
|
77
|
-
decoding="async"
|
|
78
|
-
className="h-full w-full object-cover transition-transform duration-300 group-hover:scale-[1.04] motion-reduce:transition-none"
|
|
79
|
-
onError={() => setFailed(true)}
|
|
80
|
-
/>
|
|
81
|
-
) : (
|
|
82
|
-
fallback
|
|
83
|
-
)}
|
|
84
|
-
<span
|
|
85
|
-
aria-hidden
|
|
86
|
-
className="absolute inset-x-0 bottom-0 h-3/4 bg-gradient-to-t from-black/65 via-black/45 to-transparent"
|
|
87
|
-
/>
|
|
88
|
-
<span className="absolute inset-x-2 bottom-2 line-clamp-2 text-[12.5px] font-bold leading-[1.2] tracking-[-0.01em] text-white">
|
|
89
|
-
{name}
|
|
90
|
-
</span>
|
|
91
|
-
</LinkComponent>
|
|
92
|
-
);
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
export function EventDoorSkeleton({ className }: { className?: string }) {
|
|
96
|
-
return <Skeleton className={cn("aspect-square w-full rounded-xl", className)} />;
|
|
97
|
-
}
|