@bearmenu/ui 0.8.3 → 0.8.6
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-2WDJR6K7.js +76 -0
- package/dist/chunk-637L7XQ3.js +85 -0
- package/dist/chunk-6XMYEZVR.js +26 -0
- package/dist/chunk-DNSXMHYX.js +8 -0
- package/dist/chunk-FDITZ2VM.js +16 -0
- package/dist/chunk-GPH6L4DX.js +55 -0
- package/dist/{chunk-N2KNBPAQ.js → chunk-QXHMPJ35.js} +1 -0
- package/dist/chunk-TT3WFS6S.js +379 -0
- package/dist/chunk-UXWAOISG.js +170 -0
- package/dist/{chunk-UZ3PCIE5.js → chunk-XWMRABZW.js} +2 -2
- package/dist/{chunk-VBA45GLP.js → chunk-ZDAHKI3A.js} +12 -2
- 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/card.js +4 -127
- package/dist/components/category-deck.d.ts +50 -0
- package/dist/components/category-deck.js +128 -0
- package/dist/components/command.d.ts +15 -15
- package/dist/components/concept-card.d.ts +43 -0
- package/dist/components/concept-card.js +81 -0
- package/dist/components/coverflow.d.ts +123 -0
- package/dist/components/coverflow.js +300 -0
- package/dist/components/date-input.d.ts +1 -1
- package/dist/components/day-rail-chip.d.ts +82 -0
- package/dist/components/day-rail-chip.js +105 -0
- package/dist/components/drift-rail.d.ts +28 -0
- package/dist/components/drift-rail.js +3 -0
- package/dist/components/event-card.d.ts +97 -0
- package/dist/components/event-card.js +161 -0
- package/dist/components/event-compact-row.d.ts +72 -0
- package/dist/components/event-compact-row.js +133 -0
- package/dist/components/event-day-header.d.ts +65 -0
- package/dist/components/event-day-header.js +95 -0
- package/dist/components/event-day-separator.d.ts +46 -0
- package/dist/components/event-day-separator.js +50 -0
- package/dist/components/event-detail-panel.d.ts +80 -0
- package/dist/components/event-detail-panel.js +251 -0
- package/dist/components/event-hub-hero.d.ts +34 -0
- package/dist/components/event-hub-hero.js +57 -0
- package/dist/components/event-index-row.d.ts +68 -0
- package/dist/components/event-index-row.js +84 -0
- package/dist/components/event-list-row-wide.d.ts +101 -0
- package/dist/components/event-list-row-wide.js +218 -0
- package/dist/components/event-list-row.d.ts +82 -0
- package/dist/components/event-list-row.js +173 -0
- package/dist/components/event-poster-fallback.d.ts +20 -0
- package/dist/components/event-poster-fallback.js +3 -0
- package/dist/components/event-types.d.ts +118 -0
- package/dist/components/event-types.js +2 -0
- package/dist/components/events-empty-state.d.ts +44 -0
- package/dist/components/events-empty-state.js +73 -0
- package/dist/components/facet-group.d.ts +93 -0
- package/dist/components/facet-group.js +222 -0
- package/dist/components/featured-rail-item.d.ts +42 -0
- package/dist/components/featured-rail-item.js +88 -0
- package/dist/components/hour-band.d.ts +68 -0
- package/dist/components/hour-band.js +53 -0
- package/dist/components/hour-group.d.ts +67 -0
- package/dist/components/hour-group.js +59 -0
- package/dist/components/icon.d.ts +18 -0
- package/dist/components/icon.js +31 -0
- package/dist/components/item.d.ts +2 -2
- package/dist/components/motion-icon-controls.d.ts +26 -0
- package/dist/components/motion-icon-controls.js +4 -0
- package/dist/components/motion-icon-set.d.ts +27 -0
- package/dist/components/motion-icon-set.js +139 -0
- package/dist/components/motion-icon.d.ts +44 -0
- package/dist/components/motion-icon.js +3 -0
- package/dist/components/multi-select-combobox.js +1 -1
- package/dist/components/place-about-tab.js +3 -3
- package/dist/components/place-details-mobile.js +3 -3
- package/dist/components/poster-stack.d.ts +30 -0
- package/dist/components/poster-stack.js +104 -0
- package/dist/components/poster-tape.d.ts +36 -0
- package/dist/components/poster-tape.js +64 -0
- package/dist/components/poster-wall.d.ts +25 -0
- package/dist/components/poster-wall.js +183 -0
- package/dist/components/quiet-stretch.d.ts +43 -0
- package/dist/components/quiet-stretch.js +54 -0
- package/dist/components/section-header.d.ts +53 -0
- package/dist/components/section-header.js +63 -0
- package/dist/components/segmented-nav.d.ts +62 -0
- package/dist/components/segmented-nav.js +54 -0
- package/dist/components/skeleton-card.js +1 -1
- package/dist/components/skeleton.d.ts +17 -0
- package/dist/components/skeleton.js +1 -1
- package/dist/components/spinner.d.ts +2 -2
- package/dist/components/tag-group.js +3 -26
- package/dist/components/tag.d.ts +2 -2
- package/dist/components/tag.js +1 -1
- package/dist/components/text.d.ts +3 -3
- package/dist/components/timeline-band.d.ts +46 -0
- package/dist/components/timeline-band.js +5 -0
- package/dist/components/venue-events-block.d.ts +143 -0
- package/dist/components/venue-events-block.js +222 -0
- package/dist/components/weekend-panel.d.ts +44 -0
- package/dist/components/weekend-panel.js +231 -0
- package/dist/lib/icon-registry.d.ts +58 -0
- package/dist/lib/icon-registry.js +39 -0
- package/package.json +2 -1
- package/src/components/accordion.stories.tsx +1 -1
- package/src/components/card.tsx +44 -1
- package/src/components/category-deck.stories.tsx +96 -0
- package/src/components/category-deck.tsx +227 -0
- package/src/components/combobox.stories.tsx +1 -1
- package/src/components/concept-card.stories.tsx +80 -0
- package/src/components/concept-card.tsx +143 -0
- package/src/components/coverflow.stories.tsx +240 -0
- package/src/components/coverflow.test.tsx +193 -0
- package/src/components/coverflow.tsx +550 -0
- package/src/components/currency-input.stories.tsx +3 -3
- package/src/components/day-rail-chip.stories.tsx +145 -0
- package/src/components/day-rail-chip.tsx +201 -0
- package/src/components/drift-rail.stories.tsx +92 -0
- package/src/components/drift-rail.tsx +86 -0
- package/src/components/empty-state.stories.tsx +3 -3
- package/src/components/event-card.stories.tsx +248 -0
- package/src/components/event-card.test.tsx +177 -0
- package/src/components/event-card.tsx +321 -0
- package/src/components/event-compact-row.stories.tsx +221 -0
- package/src/components/event-compact-row.tsx +268 -0
- package/src/components/event-day-header.stories.tsx +124 -0
- package/src/components/event-day-header.tsx +180 -0
- package/src/components/event-day-separator.stories.tsx +70 -0
- package/src/components/event-day-separator.tsx +95 -0
- package/src/components/event-detail-panel.stories.tsx +191 -0
- package/src/components/event-detail-panel.tsx +360 -0
- package/src/components/event-hub-hero.stories.tsx +66 -0
- package/src/components/event-hub-hero.tsx +110 -0
- package/src/components/event-index-row.stories.tsx +116 -0
- package/src/components/event-index-row.tsx +181 -0
- package/src/components/event-list-row-wide.stories.tsx +204 -0
- package/src/components/event-list-row-wide.tsx +386 -0
- package/src/components/event-list-row.stories.tsx +159 -0
- package/src/components/event-list-row.tsx +335 -0
- package/src/components/event-poster-fallback.stories.tsx +52 -0
- package/src/components/event-poster-fallback.tsx +31 -0
- package/src/components/event-types.ts +133 -0
- package/src/components/events-empty-state.stories.tsx +59 -0
- package/src/components/events-empty-state.tsx +131 -0
- package/src/components/facet-group.stories.tsx +247 -0
- package/src/components/facet-group.test.tsx +225 -0
- package/src/components/facet-group.tsx +352 -0
- package/src/components/featured-rail-item.stories.tsx +86 -0
- package/src/components/featured-rail-item.tsx +153 -0
- package/src/components/form.stories.tsx +1 -1
- package/src/components/hour-band.stories.tsx +159 -0
- package/src/components/hour-band.tsx +126 -0
- package/src/components/hour-group.stories.tsx +100 -0
- package/src/components/hour-group.tsx +124 -0
- package/src/components/icon.tsx +57 -0
- package/src/components/input-otp.stories.tsx +1 -1
- package/src/components/markdown-renderer.stories.tsx +1 -1
- package/src/components/motion-icon-controls.test.tsx +78 -0
- package/src/components/motion-icon-controls.tsx +552 -0
- package/src/components/motion-icon-set.stories.tsx +185 -0
- package/src/components/motion-icon-set.tsx +219 -0
- package/src/components/motion-icon.test.tsx +159 -0
- package/src/components/motion-icon.tsx +163 -0
- package/src/components/multi-select-combobox.stories.tsx +1 -1
- package/src/components/phone-frame.stories.tsx +1 -1
- package/src/components/poster-stack.stories.tsx +68 -0
- package/src/components/poster-stack.tsx +155 -0
- package/src/components/poster-tape.stories.tsx +70 -0
- package/src/components/poster-tape.tsx +122 -0
- package/src/components/poster-wall.stories.tsx +95 -0
- package/src/components/poster-wall.tsx +220 -0
- package/src/components/quiet-stretch.stories.tsx +62 -0
- package/src/components/quiet-stretch.tsx +115 -0
- package/src/components/rating.stories.tsx +3 -3
- package/src/components/searchable-select.stories.tsx +1 -1
- package/src/components/section-header.stories.tsx +70 -0
- package/src/components/section-header.tsx +134 -0
- package/src/components/segmented-nav.stories.tsx +118 -0
- package/src/components/segmented-nav.tsx +113 -0
- package/src/components/selection-bar.stories.tsx +1 -1
- package/src/components/skeleton-card.stories.tsx +3 -3
- package/src/components/skeleton.tsx +18 -0
- package/src/components/sliding-panels.stories.tsx +1 -1
- package/src/components/stepper.stories.tsx +1 -1
- package/src/components/sticky-container.stories.tsx +3 -3
- package/src/components/tag.tsx +11 -1
- package/src/components/timeline-band.stories.tsx +206 -0
- package/src/components/timeline-band.tsx +207 -0
- package/src/components/toggle-group.stories.tsx +1 -1
- package/src/components/tooltip.stories.tsx +2 -2
- package/src/components/venue-events-block.stories.tsx +216 -0
- package/src/components/venue-events-block.tsx +408 -0
- package/src/components/weekend-panel.stories.tsx +92 -0
- package/src/components/weekend-panel.tsx +322 -0
- package/src/globals.css +19 -0
- package/src/lib/icon-registry.ts +139 -0
- package/src/test/setup.ts +5 -1
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import * as react_jsx_runtime from 'react/jsx-runtime';
|
|
2
|
+
import * as React$1 from 'react';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* ConceptCard — the recurring-series cover card (e.g. a weekly party
|
|
6
|
+
* concept), a wide 2:1 image with a bottom scrim carrying a kind pill,
|
|
7
|
+
* a composed date/venue/count line and the title.
|
|
8
|
+
*
|
|
9
|
+
* Same contract as `EventCard`: DS-owned props, plain `<img>`, anchor
|
|
10
|
+
* whenever `href` is given. Absent `coverUrl` falls back to a flat
|
|
11
|
+
* `bg-muted` block, same treatment `EventCard` gives a missing poster.
|
|
12
|
+
*/
|
|
13
|
+
type ConceptCardLinkProps = {
|
|
14
|
+
href: string;
|
|
15
|
+
className?: string;
|
|
16
|
+
children: React.ReactNode;
|
|
17
|
+
onClick?: (mouseEvent: React.MouseEvent) => void;
|
|
18
|
+
};
|
|
19
|
+
type ConceptCardProps = {
|
|
20
|
+
title: string;
|
|
21
|
+
/**
|
|
22
|
+
* The whole date/venue/count line, composed by the caller, e.g. "Every
|
|
23
|
+
* Saturday · Flying Circus · 4 dates" — clause order is locale-dependent.
|
|
24
|
+
*/
|
|
25
|
+
meta: string;
|
|
26
|
+
/** Label plus its icon, e.g. `<><PartyPopper .../> Party</>`. */
|
|
27
|
+
kind: React.ReactNode;
|
|
28
|
+
coverUrl?: string;
|
|
29
|
+
href?: string;
|
|
30
|
+
/** Defaults to a plain `<a>`. Pass `next/link` in a Next app. */
|
|
31
|
+
linkComponent?: React.ComponentType<ConceptCardLinkProps>;
|
|
32
|
+
onClick?: (mouseEvent: React.MouseEvent) => void;
|
|
33
|
+
className?: string;
|
|
34
|
+
/** Forwarded to the cover's `loading` attribute (`"eager"` when true). */
|
|
35
|
+
priority?: boolean;
|
|
36
|
+
};
|
|
37
|
+
declare const ConceptCard: React$1.NamedExoticComponent<ConceptCardProps>;
|
|
38
|
+
type ConceptCardSkeletonProps = {
|
|
39
|
+
className?: string;
|
|
40
|
+
};
|
|
41
|
+
declare function ConceptCardSkeleton({ className }: ConceptCardSkeletonProps): react_jsx_runtime.JSX.Element;
|
|
42
|
+
|
|
43
|
+
export { ConceptCard, type ConceptCardLinkProps, type ConceptCardProps, ConceptCardSkeleton, type ConceptCardSkeletonProps };
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import { Skeleton } from '../chunk-QXHMPJ35.js';
|
|
2
|
+
import { cn } from '../chunk-FEK5XGZW.js';
|
|
3
|
+
import { __objRest, __spreadValues } from '../chunk-3GWWZKX7.js';
|
|
4
|
+
import { memo, useState } from 'react';
|
|
5
|
+
import { Calendar } from 'lucide-react';
|
|
6
|
+
import { jsx, jsxs, Fragment } 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
|
+
var ConceptCard = memo(function ConceptCard2({
|
|
13
|
+
title,
|
|
14
|
+
meta,
|
|
15
|
+
kind,
|
|
16
|
+
coverUrl,
|
|
17
|
+
href,
|
|
18
|
+
linkComponent: LinkComponent = DefaultLink,
|
|
19
|
+
onClick,
|
|
20
|
+
className,
|
|
21
|
+
priority
|
|
22
|
+
}) {
|
|
23
|
+
const [imgError, setImgError] = useState(false);
|
|
24
|
+
const showCover = !!coverUrl && !imgError;
|
|
25
|
+
const body = /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
26
|
+
/* @__PURE__ */ jsx("div", { className: "absolute inset-0 bg-muted", children: showCover && // eslint-disable-next-line @next/next/no-img-element
|
|
27
|
+
/* @__PURE__ */ jsx(
|
|
28
|
+
"img",
|
|
29
|
+
{
|
|
30
|
+
src: coverUrl,
|
|
31
|
+
alt: title,
|
|
32
|
+
title,
|
|
33
|
+
loading: priority ? "eager" : "lazy",
|
|
34
|
+
decoding: "async",
|
|
35
|
+
className: "h-full w-full object-cover",
|
|
36
|
+
onError: () => setImgError(true)
|
|
37
|
+
}
|
|
38
|
+
) }),
|
|
39
|
+
/* @__PURE__ */ jsx("div", { className: "absolute inset-x-0 bottom-0 z-[1] h-[60%] bg-gradient-to-t from-black/90 via-black/60 to-transparent" }),
|
|
40
|
+
/* @__PURE__ */ jsx("span", { className: "absolute left-2 top-2 z-[2] inline-flex items-center gap-1 rounded-md bg-black/50 px-2 py-1 text-[11px] font-semibold text-white", children: kind }),
|
|
41
|
+
/* @__PURE__ */ jsxs("div", { className: "relative z-[2] mt-auto flex w-full flex-col gap-0.5 p-2.5 text-white", children: [
|
|
42
|
+
/* @__PURE__ */ jsxs("div", { className: "flex items-center gap-1 text-[11px] font-medium text-white/85", children: [
|
|
43
|
+
/* @__PURE__ */ jsx(Calendar, { className: "h-3 w-3 shrink-0", "aria-hidden": true }),
|
|
44
|
+
/* @__PURE__ */ jsx("span", { className: "truncate", children: meta })
|
|
45
|
+
] }),
|
|
46
|
+
/* @__PURE__ */ jsx("div", { className: "line-clamp-1 text-[14px] font-semibold leading-[1.25]", children: title })
|
|
47
|
+
] })
|
|
48
|
+
] });
|
|
49
|
+
const rootClasses = cn(
|
|
50
|
+
"relative flex aspect-[2/1] flex-col overflow-hidden rounded-xl text-left",
|
|
51
|
+
(href || onClick) && "cursor-pointer focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2",
|
|
52
|
+
className
|
|
53
|
+
);
|
|
54
|
+
if (href) {
|
|
55
|
+
return /* @__PURE__ */ jsx(LinkComponent, { href, className: rootClasses, onClick, children: body });
|
|
56
|
+
}
|
|
57
|
+
if (onClick) {
|
|
58
|
+
return /* @__PURE__ */ jsx("button", { type: "button", className: rootClasses, onClick, children: body });
|
|
59
|
+
}
|
|
60
|
+
return /* @__PURE__ */ jsx("div", { className: rootClasses, children: body });
|
|
61
|
+
});
|
|
62
|
+
function ConceptCardSkeleton({ className }) {
|
|
63
|
+
return /* @__PURE__ */ jsxs(
|
|
64
|
+
"div",
|
|
65
|
+
{
|
|
66
|
+
"aria-hidden": "true",
|
|
67
|
+
className: cn("relative flex aspect-[2/1] flex-col overflow-hidden rounded-xl", className),
|
|
68
|
+
children: [
|
|
69
|
+
/* @__PURE__ */ jsx(Skeleton, { className: "absolute inset-0 rounded-none" }),
|
|
70
|
+
/* @__PURE__ */ jsx("div", { className: "absolute inset-x-0 bottom-0 z-[1] h-[60%] bg-gradient-to-t from-black/90 via-black/60 to-transparent" }),
|
|
71
|
+
/* @__PURE__ */ jsx(Skeleton, { className: "absolute left-2 top-2 z-[2] h-[26px] w-16 rounded-md bg-white/20" }),
|
|
72
|
+
/* @__PURE__ */ jsxs("div", { className: "relative z-[2] mt-auto flex w-full flex-col gap-1 p-2.5", children: [
|
|
73
|
+
/* @__PURE__ */ jsx(Skeleton, { className: "h-[11px] w-24 bg-white/15" }),
|
|
74
|
+
/* @__PURE__ */ jsx(Skeleton, { className: "h-[14px] w-2/3 bg-white/20" })
|
|
75
|
+
] })
|
|
76
|
+
]
|
|
77
|
+
}
|
|
78
|
+
);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export { ConceptCard, ConceptCardSkeleton };
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
import * as react_jsx_runtime from 'react/jsx-runtime';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Coverflow — the busiest-events browser: a 3D perspective lane with a large
|
|
5
|
+
* lead poster flanked by up to three shrinking, desaturating posters on each
|
|
6
|
+
* side. The lane itself drifts a few pixels back and forth
|
|
7
|
+
* (`prefers-reduced-motion` stops this outright; an `IntersectionObserver`
|
|
8
|
+
* pauses it off-screen).
|
|
9
|
+
*
|
|
10
|
+
* IT IS A RING, NOT A LANE. A card's slot comes from `ringOffset` — the
|
|
11
|
+
* SIGNED SHORTEST WAY ROUND the list, not the raw `index - activeIndex`. With
|
|
12
|
+
* seven items and `activeIndex` at 0, item 6 renders at offset −1 (just left
|
|
13
|
+
* of the lead) rather than +6, so both flanks are full at every index instead
|
|
14
|
+
* of the stage emptying out at the two ends, and prev/next keep going past
|
|
15
|
+
* either end. `goTo` already reported a wrapped index; the visuals now agree
|
|
16
|
+
* with it.
|
|
17
|
+
*
|
|
18
|
+
* IDENTITY IS THE ANIMATION: a card renders keyed by `item.id`, and its
|
|
19
|
+
* appearance (position, rotation, scale, depth filter) is a pure function of
|
|
20
|
+
* that offset. Advancing `activeIndex` shifts every offset by one, and
|
|
21
|
+
* because the DOM node stays put the browser tweens it from its old slot to
|
|
22
|
+
* its new one instead of popping straight there.
|
|
23
|
+
*
|
|
24
|
+
* THE WRAP MUST NOT TWEEN. Exactly one card per step crosses the ring's seam
|
|
25
|
+
* — with seven items it goes from offset −3 to +3 — and animating that would
|
|
26
|
+
* drag a poster right across the face of the coverflow. The mechanism chosen:
|
|
27
|
+
* `Coverflow` keeps the previous and current active index in one piece of
|
|
28
|
+
* state (`step`), so a render can tell, per card, whether its offset moved by
|
|
29
|
+
* the same amount the active index did. A card that did not (the only way
|
|
30
|
+
* that happens is crossing the seam) renders WITHOUT its transition classes
|
|
31
|
+
* for that one commit, so the transform applies instantly; the next commit
|
|
32
|
+
* restores them and it tweens normally from its new slot. Two alternatives
|
|
33
|
+
* were rejected: re-keying the card to force a remount throws away the
|
|
34
|
+
* decoded `<img>` and flashes the poster, and an effect-based "clear the
|
|
35
|
+
* transition after paint" lands a frame too late — the slide has already
|
|
36
|
+
* started. The seam sits at the deepest tier, which is also the lowest
|
|
37
|
+
* `z-index`, so the teleport happens behind the stack and off-stage.
|
|
38
|
+
*
|
|
39
|
+
* ONLY THE VISIBLE TIERS ARE MOUNTED. Every item now resolves to some ring
|
|
40
|
+
* offset, so the component renders only `|offset| <= 3` and drops the rest
|
|
41
|
+
* entirely — a fifty-item list paints seven posters, not fifty. Cards enter
|
|
42
|
+
* and leave at tier 3, parked well outside the stage's horizontal clip, so the
|
|
43
|
+
* mount/unmount is never seen. The stage clips on the x axis ONLY; see the
|
|
44
|
+
* note on the container element for why the y axis must stay visible.
|
|
45
|
+
*
|
|
46
|
+
* DEGENERATE LENGTHS: with one item there is no ring, so the arrows are not
|
|
47
|
+
* rendered at all rather than shown inert — see the note below on controls
|
|
48
|
+
* that look operable and do nothing. With two items the shortest path ties at
|
|
49
|
+
* `n / 2`; `ringOffset` resolves the tie to the POSITIVE side, so the other
|
|
50
|
+
* card is always on the right flank instead of flipping between them.
|
|
51
|
+
*
|
|
52
|
+
* `prefers-reduced-motion` already removes the position transition outright
|
|
53
|
+
* (`motion-reduce:transition-none`), so under it every step is a jump and the
|
|
54
|
+
* wrap is not a special case — it cannot read as a flash because nothing else
|
|
55
|
+
* is animating either.
|
|
56
|
+
*
|
|
57
|
+
* ACCESSIBILITY: every item carries a `title`, so every card is a real,
|
|
58
|
+
* focusable control (the injected link when it carries an `href`, otherwise a
|
|
59
|
+
* `<button>`) named after the event it depicts — a keyboard or screen-reader
|
|
60
|
+
* user can jump straight to any visible card the way a mouse user clicks one
|
|
61
|
+
* directly, not just step with `prevLabel`/`nextLabel`. Selecting one calls
|
|
62
|
+
* `onActiveIndexChange` with its index. Cards beyond the visible tiers are
|
|
63
|
+
* not rendered at all, so a screen-reader/keyboard user never lands on an
|
|
64
|
+
* invisible control — there is nothing there to land on. The depth
|
|
65
|
+
* styling (scale/grayscale/opacity) stays purely decorative, applied to an
|
|
66
|
+
* inner media element rather than the focusable wrapper — `filter` creates a
|
|
67
|
+
* new stacking context that would otherwise dim a `box-shadow`-based focus
|
|
68
|
+
* ring along with the poster. The active item's title/meta stays
|
|
69
|
+
* `aria-live="polite"` so a screen-reader user is told what changed after any
|
|
70
|
+
* kind of jump.
|
|
71
|
+
*/
|
|
72
|
+
type CoverflowItem = {
|
|
73
|
+
/** Stable identity — this is what makes the position change animate rather than pop. */
|
|
74
|
+
id: string;
|
|
75
|
+
title: string;
|
|
76
|
+
/** Caller-composed, e.g. "Fri 4 · 90 going". */
|
|
77
|
+
meta: string;
|
|
78
|
+
posterUrl?: string;
|
|
79
|
+
href?: string;
|
|
80
|
+
/**
|
|
81
|
+
* Top-left overlay slot, rendered exactly as given — bring your own pill,
|
|
82
|
+
* icon and colour. Deliberately unstyled: a wrapper here would cap every
|
|
83
|
+
* caller at one look, and the first real consumer needed a colour-ramped
|
|
84
|
+
* badge the old black pill could not express.
|
|
85
|
+
*/
|
|
86
|
+
badge?: React.ReactNode;
|
|
87
|
+
isFree?: boolean;
|
|
88
|
+
};
|
|
89
|
+
type CoverflowLinkProps = {
|
|
90
|
+
href: string;
|
|
91
|
+
className?: string;
|
|
92
|
+
style?: React.CSSProperties;
|
|
93
|
+
children: React.ReactNode;
|
|
94
|
+
onClick?: (mouseEvent: React.MouseEvent) => void;
|
|
95
|
+
"aria-label"?: string;
|
|
96
|
+
"aria-hidden"?: boolean;
|
|
97
|
+
tabIndex?: number;
|
|
98
|
+
};
|
|
99
|
+
type CoverflowProps = {
|
|
100
|
+
items: CoverflowItem[];
|
|
101
|
+
activeIndex: number;
|
|
102
|
+
onActiveIndexChange?: (index: number) => void;
|
|
103
|
+
/**
|
|
104
|
+
* Optional centred caption under the stage, e.g. a pluralized item count
|
|
105
|
+
* ("7 events"). Omit it when `positionLabel` already says everything there
|
|
106
|
+
* is to say — passing a position string here as well renders it twice, with
|
|
107
|
+
* a separator dot between the two identical halves.
|
|
108
|
+
*/
|
|
109
|
+
caption?: string;
|
|
110
|
+
/** Required to render the free pill on an item — no label, no pill. */
|
|
111
|
+
freeLabel: string;
|
|
112
|
+
/** Accessible name for the previous-item control. */
|
|
113
|
+
prevLabel: string;
|
|
114
|
+
/** Accessible name for the next-item control. */
|
|
115
|
+
nextLabel: string;
|
|
116
|
+
/** e.g. "3 of 7" — caller-composed, locale-dependent. Shown next to `caption`. */
|
|
117
|
+
positionLabel?: string;
|
|
118
|
+
linkComponent?: React.ComponentType<CoverflowLinkProps>;
|
|
119
|
+
className?: string;
|
|
120
|
+
};
|
|
121
|
+
declare function Coverflow({ items, activeIndex, onActiveIndexChange, caption, freeLabel, prevLabel, nextLabel, positionLabel, linkComponent: LinkComponent, className, }: CoverflowProps): react_jsx_runtime.JSX.Element | null;
|
|
122
|
+
|
|
123
|
+
export { Coverflow, type CoverflowItem, type CoverflowLinkProps, type CoverflowProps };
|
|
@@ -0,0 +1,300 @@
|
|
|
1
|
+
import { EventPosterFallback } from '../chunk-FDITZ2VM.js';
|
|
2
|
+
import { cn } from '../chunk-FEK5XGZW.js';
|
|
3
|
+
import { __objRest, __spreadValues } from '../chunk-3GWWZKX7.js';
|
|
4
|
+
import { useId, useRef, useState, useEffect } from 'react';
|
|
5
|
+
import { ChevronLeft, ChevronRight, Gift } from 'lucide-react';
|
|
6
|
+
import { jsxs, jsx, Fragment } 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
|
+
var COVERFLOW_MAX_VISIBLE_TIER = 3;
|
|
13
|
+
var COVERFLOW_TIER_ROTATE_DEG = [0, 20, 30, 38];
|
|
14
|
+
var COVERFLOW_TIER_SCALE = [1, 0.92, 0.82, 0.72];
|
|
15
|
+
var COVERFLOW_TIER_GRAYSCALE = [0, 0, 0.35, 0.5];
|
|
16
|
+
var COVERFLOW_TIER_OPACITY = [1, 0.88, 0.7, 0.5];
|
|
17
|
+
var COVERFLOW_CARD_WIDTH_CLASS = "w-[149px] sm:w-[210px]";
|
|
18
|
+
var COVERFLOW_CARD_TRANSFORM_TRANSITION_CLASS = "transition-[transform,box-shadow] duration-[420ms] ease-[cubic-bezier(0.22,1,0.36,1)] motion-reduce:transition-none motion-reduce:duration-0";
|
|
19
|
+
var COVERFLOW_MEDIA_FILTER_TRANSITION_CLASS = "transition-[filter,opacity] duration-[420ms] ease-[cubic-bezier(0.22,1,0.36,1)] motion-reduce:transition-none motion-reduce:duration-0";
|
|
20
|
+
var COVERFLOW_CARD_FOCUS_CLASS = "focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2";
|
|
21
|
+
var COVERFLOW_SIDE_SHADOW_CLASS = "shadow-[0_11px_27px_oklch(0.3_0.02_60/0.22)]";
|
|
22
|
+
var COVERFLOW_LEAD_SHADOW_CLASS = "shadow-[0_24px_56px_oklch(0.28_0.03_45/0.32)]";
|
|
23
|
+
var COVERFLOW_PILL_CLASS = "inline-flex h-6 max-w-[85%] items-center gap-1 rounded-full px-2 text-[11px] font-semibold";
|
|
24
|
+
var COVERFLOW_DRIFT_DURATION_SECONDS = 26;
|
|
25
|
+
var COVERFLOW_DRIFT_PIXELS = 40;
|
|
26
|
+
var COVERFLOW_LEAD_Z_INDEX = 6;
|
|
27
|
+
var COVERFLOW_ARROW_CLASS = "absolute top-1/2 z-20 flex h-11 w-11 -translate-y-1/2 cursor-pointer items-center justify-center rounded-full bg-background/85 text-foreground shadow-[var(--shadow-overlay)] backdrop-blur-sm transition-colors duration-150 hover:bg-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 motion-reduce:transition-none";
|
|
28
|
+
function wrapIndex(index, length) {
|
|
29
|
+
return (index % length + length) % length;
|
|
30
|
+
}
|
|
31
|
+
function ringOffset(delta, length) {
|
|
32
|
+
const raw = wrapIndex(delta, length);
|
|
33
|
+
return raw > length / 2 ? raw - length : raw;
|
|
34
|
+
}
|
|
35
|
+
function CoverflowCard({
|
|
36
|
+
item,
|
|
37
|
+
index,
|
|
38
|
+
offset,
|
|
39
|
+
suppressTransition,
|
|
40
|
+
freeLabel,
|
|
41
|
+
onActivate,
|
|
42
|
+
linkComponent: LinkComponent
|
|
43
|
+
}) {
|
|
44
|
+
var _a;
|
|
45
|
+
const isLead = offset === 0;
|
|
46
|
+
const magnitude = Math.abs(offset);
|
|
47
|
+
const tier = Math.min(magnitude, COVERFLOW_MAX_VISIBLE_TIER);
|
|
48
|
+
const sign = Math.sign(offset);
|
|
49
|
+
const wrapperStyle = {
|
|
50
|
+
// The lead sits at `--cf-scale-lead`, which is now 1 — see the width class.
|
|
51
|
+
// Side tiers multiply their depth ratio by `--cf-tier-base`, the side/lead
|
|
52
|
+
// width ratio, because the shared box is the LEAD's size: without it every
|
|
53
|
+
// side card would render at nearly lead width. It is a CSS var rather than
|
|
54
|
+
// a constant here because the ratio differs per breakpoint, and calc()
|
|
55
|
+
// cannot divide two lengths to derive it live.
|
|
56
|
+
transform: isLead ? "translate(-50%, -50%) scale(var(--cf-scale-lead))" : `translate(-50%, -50%) translateX(calc(${sign} * var(--cf-x${tier}))) rotateY(${-sign * COVERFLOW_TIER_ROTATE_DEG[tier]}deg) scale(calc(var(--cf-tier-base) * ${COVERFLOW_TIER_SCALE[tier]}))`,
|
|
57
|
+
zIndex: Math.max(COVERFLOW_LEAD_Z_INDEX - magnitude, 0)
|
|
58
|
+
};
|
|
59
|
+
const mediaStyle = {
|
|
60
|
+
opacity: COVERFLOW_TIER_OPACITY[tier],
|
|
61
|
+
filter: COVERFLOW_TIER_GRAYSCALE[tier] > 0 ? `grayscale(${COVERFLOW_TIER_GRAYSCALE[tier] * 100}%)` : "none"
|
|
62
|
+
};
|
|
63
|
+
const media = /* @__PURE__ */ jsxs(
|
|
64
|
+
"div",
|
|
65
|
+
{
|
|
66
|
+
className: cn(
|
|
67
|
+
"relative aspect-[4/5] overflow-hidden rounded-[10px] bg-muted",
|
|
68
|
+
!suppressTransition && COVERFLOW_MEDIA_FILTER_TRANSITION_CLASS
|
|
69
|
+
),
|
|
70
|
+
style: mediaStyle,
|
|
71
|
+
children: [
|
|
72
|
+
item.posterUrl ? (
|
|
73
|
+
// eslint-disable-next-line @next/next/no-img-element
|
|
74
|
+
/* @__PURE__ */ jsx(
|
|
75
|
+
"img",
|
|
76
|
+
{
|
|
77
|
+
src: item.posterUrl,
|
|
78
|
+
alt: "",
|
|
79
|
+
loading: isLead ? "eager" : "lazy",
|
|
80
|
+
decoding: "async",
|
|
81
|
+
className: "h-full w-full object-cover"
|
|
82
|
+
}
|
|
83
|
+
)
|
|
84
|
+
) : /* @__PURE__ */ jsx(EventPosterFallback, {}),
|
|
85
|
+
/* @__PURE__ */ jsxs("div", { className: "absolute inset-x-2 top-2 flex items-start justify-between gap-2", children: [
|
|
86
|
+
(_a = item.badge) != null ? _a : /* @__PURE__ */ jsx("span", {}),
|
|
87
|
+
item.isFree && freeLabel && /* @__PURE__ */ jsxs("span", { className: cn(COVERFLOW_PILL_CLASS, "bg-success font-bold text-success-foreground"), children: [
|
|
88
|
+
/* @__PURE__ */ jsx(Gift, { className: "h-3 w-3", "aria-hidden": true }),
|
|
89
|
+
freeLabel
|
|
90
|
+
] })
|
|
91
|
+
] })
|
|
92
|
+
]
|
|
93
|
+
}
|
|
94
|
+
);
|
|
95
|
+
const wrapperClassName = cn(
|
|
96
|
+
"absolute left-1/2 top-1/2 block bg-transparent p-0 text-left",
|
|
97
|
+
COVERFLOW_CARD_WIDTH_CLASS,
|
|
98
|
+
isLead ? "cursor-default" : "cursor-pointer",
|
|
99
|
+
isLead ? COVERFLOW_LEAD_SHADOW_CLASS : COVERFLOW_SIDE_SHADOW_CLASS,
|
|
100
|
+
"rounded-[10px]",
|
|
101
|
+
!suppressTransition && COVERFLOW_CARD_TRANSFORM_TRANSITION_CLASS,
|
|
102
|
+
COVERFLOW_CARD_FOCUS_CLASS
|
|
103
|
+
);
|
|
104
|
+
const handleActivate = isLead ? void 0 : () => onActivate(index);
|
|
105
|
+
if (item.href) {
|
|
106
|
+
return /* @__PURE__ */ jsx(
|
|
107
|
+
LinkComponent,
|
|
108
|
+
{
|
|
109
|
+
href: item.href,
|
|
110
|
+
className: wrapperClassName,
|
|
111
|
+
style: wrapperStyle,
|
|
112
|
+
"aria-label": item.title,
|
|
113
|
+
onClick: handleActivate,
|
|
114
|
+
children: media
|
|
115
|
+
}
|
|
116
|
+
);
|
|
117
|
+
}
|
|
118
|
+
return /* @__PURE__ */ jsx(
|
|
119
|
+
"button",
|
|
120
|
+
{
|
|
121
|
+
type: "button",
|
|
122
|
+
className: wrapperClassName,
|
|
123
|
+
style: wrapperStyle,
|
|
124
|
+
"aria-label": item.title,
|
|
125
|
+
onClick: handleActivate,
|
|
126
|
+
children: media
|
|
127
|
+
}
|
|
128
|
+
);
|
|
129
|
+
}
|
|
130
|
+
function Coverflow({
|
|
131
|
+
items,
|
|
132
|
+
activeIndex,
|
|
133
|
+
onActiveIndexChange,
|
|
134
|
+
caption,
|
|
135
|
+
freeLabel,
|
|
136
|
+
prevLabel,
|
|
137
|
+
nextLabel,
|
|
138
|
+
positionLabel,
|
|
139
|
+
linkComponent: LinkComponent = DefaultLink,
|
|
140
|
+
className
|
|
141
|
+
}) {
|
|
142
|
+
const scopeId = useId().replace(/[^a-zA-Z0-9]/g, "");
|
|
143
|
+
const scopeClass = `coverflow-${scopeId}`;
|
|
144
|
+
const containerRef = useRef(null);
|
|
145
|
+
const length = items.length;
|
|
146
|
+
const active = length > 0 ? wrapIndex(activeIndex, length) : 0;
|
|
147
|
+
const [step, setStep] = useState({ from: active, to: active, length });
|
|
148
|
+
if (step.to !== active || step.length !== length) {
|
|
149
|
+
setStep(
|
|
150
|
+
step.length === length ? { from: step.to, to: active, length } : { from: active, to: active, length }
|
|
151
|
+
);
|
|
152
|
+
}
|
|
153
|
+
useEffect(() => {
|
|
154
|
+
const node = containerRef.current;
|
|
155
|
+
if (!node || typeof IntersectionObserver === "undefined") return void 0;
|
|
156
|
+
const observer = new IntersectionObserver(
|
|
157
|
+
([entry]) => {
|
|
158
|
+
node.dataset.offscreen = entry.isIntersecting ? "false" : "true";
|
|
159
|
+
},
|
|
160
|
+
{ threshold: 0 }
|
|
161
|
+
);
|
|
162
|
+
observer.observe(node);
|
|
163
|
+
return () => observer.disconnect();
|
|
164
|
+
}, []);
|
|
165
|
+
if (length === 0) return null;
|
|
166
|
+
const activeItem = items[active];
|
|
167
|
+
const goTo = (index) => onActiveIndexChange == null ? void 0 : onActiveIndexChange(wrapIndex(index, length));
|
|
168
|
+
const activeDelta = ringOffset(step.to - step.from, length);
|
|
169
|
+
const visibleCards = items.map((item, index) => {
|
|
170
|
+
const offset = ringOffset(index - active, length);
|
|
171
|
+
const suppressTransition = offset !== ringOffset(index - step.from, length) - activeDelta;
|
|
172
|
+
return { item, index, offset, suppressTransition };
|
|
173
|
+
}).filter(({ offset }) => Math.abs(offset) <= COVERFLOW_MAX_VISIBLE_TIER);
|
|
174
|
+
const hasRing = length > 1;
|
|
175
|
+
return /* @__PURE__ */ jsxs("div", { className: cn(scopeClass, "w-full", className), children: [
|
|
176
|
+
/* @__PURE__ */ jsx("style", { children: `
|
|
177
|
+
.${scopeClass} {
|
|
178
|
+
/*
|
|
179
|
+
* Every card shares one DOM box (COVERFLOW_CARD_WIDTH_CLASS above),
|
|
180
|
+
* and that box is now the LEAD's width. These are the visual
|
|
181
|
+
* footprints the JS transform math targets.
|
|
182
|
+
*
|
|
183
|
+
* --cf-scale-lead is therefore 1: the lead paints at its own size and
|
|
184
|
+
* is never resampled upward, which is what fixed it reading soft.
|
|
185
|
+
* --cf-tier-base is --cf-side-w / --cf-lead-w, the factor every side
|
|
186
|
+
* tier multiplies its depth ratio by; it must be kept in sync by hand
|
|
187
|
+
* if either width changes, because calc() cannot divide two lengths.
|
|
188
|
+
* 77 / 149 = 0.5168 120 / 210 = 0.5714
|
|
189
|
+
*
|
|
190
|
+
* Both footprints are 20% down from where they started (186/96 and
|
|
191
|
+
* 262/150): the lead was large enough that a poster meant for a feed
|
|
192
|
+
* thumbnail was being asked to carry a hero, and it showed.
|
|
193
|
+
*/
|
|
194
|
+
--cf-lead-w: 149px;
|
|
195
|
+
--cf-side-w: 77px;
|
|
196
|
+
--cf-gap: 21px;
|
|
197
|
+
--cf-scale-lead: 1;
|
|
198
|
+
--cf-tier-base: 0.5168;
|
|
199
|
+
--cf-x1: calc(var(--cf-lead-w) / 2 + var(--cf-gap) + var(--cf-side-w) / 2);
|
|
200
|
+
--cf-x2: calc(var(--cf-x1) + var(--cf-side-w) + var(--cf-gap));
|
|
201
|
+
--cf-x3: calc(var(--cf-x2) + var(--cf-side-w) + var(--cf-gap));
|
|
202
|
+
}
|
|
203
|
+
@media (min-width: 640px) {
|
|
204
|
+
.${scopeClass} {
|
|
205
|
+
--cf-lead-w: 210px;
|
|
206
|
+
--cf-side-w: 120px;
|
|
207
|
+
--cf-gap: 26px;
|
|
208
|
+
--cf-tier-base: 0.5714;
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
.${scopeClass} .coverflow-lane {
|
|
212
|
+
animation: ${scopeClass}-drift ${COVERFLOW_DRIFT_DURATION_SECONDS}s ease-in-out infinite alternate;
|
|
213
|
+
}
|
|
214
|
+
.${scopeClass}[data-offscreen="true"] .coverflow-lane {
|
|
215
|
+
animation-play-state: paused;
|
|
216
|
+
}
|
|
217
|
+
@keyframes ${scopeClass}-drift {
|
|
218
|
+
from { transform: translateX(-${COVERFLOW_DRIFT_PIXELS}px); }
|
|
219
|
+
to { transform: translateX(${COVERFLOW_DRIFT_PIXELS}px); }
|
|
220
|
+
}
|
|
221
|
+
@media (prefers-reduced-motion: reduce) {
|
|
222
|
+
.${scopeClass} .coverflow-lane {
|
|
223
|
+
animation: none;
|
|
224
|
+
transform: none;
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
` }),
|
|
228
|
+
/* @__PURE__ */ jsxs(
|
|
229
|
+
"div",
|
|
230
|
+
{
|
|
231
|
+
ref: containerRef,
|
|
232
|
+
className: "relative flex h-[300px] items-center justify-center overflow-x-clip overflow-y-visible [perspective:900px] sm:h-[380px] sm:[perspective:1400px]",
|
|
233
|
+
children: [
|
|
234
|
+
/* @__PURE__ */ jsx(
|
|
235
|
+
"div",
|
|
236
|
+
{
|
|
237
|
+
"aria-hidden": "true",
|
|
238
|
+
className: "pointer-events-none absolute inset-y-0 left-0 z-10 w-[120px] bg-gradient-to-r from-background to-transparent"
|
|
239
|
+
}
|
|
240
|
+
),
|
|
241
|
+
/* @__PURE__ */ jsx(
|
|
242
|
+
"div",
|
|
243
|
+
{
|
|
244
|
+
"aria-hidden": "true",
|
|
245
|
+
className: "pointer-events-none absolute inset-y-0 right-0 z-10 w-[120px] bg-gradient-to-l from-background to-transparent"
|
|
246
|
+
}
|
|
247
|
+
),
|
|
248
|
+
hasRing && /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
249
|
+
/* @__PURE__ */ jsx(
|
|
250
|
+
"button",
|
|
251
|
+
{
|
|
252
|
+
type: "button",
|
|
253
|
+
"aria-label": prevLabel,
|
|
254
|
+
onClick: () => goTo(active - 1),
|
|
255
|
+
className: cn(COVERFLOW_ARROW_CLASS, "left-2 sm:left-4"),
|
|
256
|
+
children: /* @__PURE__ */ jsx(ChevronLeft, { className: "h-5 w-5", "aria-hidden": true })
|
|
257
|
+
}
|
|
258
|
+
),
|
|
259
|
+
/* @__PURE__ */ jsx(
|
|
260
|
+
"button",
|
|
261
|
+
{
|
|
262
|
+
type: "button",
|
|
263
|
+
"aria-label": nextLabel,
|
|
264
|
+
onClick: () => goTo(active + 1),
|
|
265
|
+
className: cn(COVERFLOW_ARROW_CLASS, "right-2 sm:right-4"),
|
|
266
|
+
children: /* @__PURE__ */ jsx(ChevronRight, { className: "h-5 w-5", "aria-hidden": true })
|
|
267
|
+
}
|
|
268
|
+
)
|
|
269
|
+
] }),
|
|
270
|
+
/* @__PURE__ */ jsx("div", { className: "coverflow-lane relative h-full w-full [transform-style:preserve-3d] motion-reduce:[animation:none]", children: visibleCards.map(({ item, index, offset, suppressTransition }) => /* @__PURE__ */ jsx(
|
|
271
|
+
CoverflowCard,
|
|
272
|
+
{
|
|
273
|
+
item,
|
|
274
|
+
index,
|
|
275
|
+
offset,
|
|
276
|
+
suppressTransition,
|
|
277
|
+
freeLabel,
|
|
278
|
+
onActivate: goTo,
|
|
279
|
+
linkComponent: LinkComponent
|
|
280
|
+
},
|
|
281
|
+
item.id
|
|
282
|
+
)) })
|
|
283
|
+
]
|
|
284
|
+
}
|
|
285
|
+
),
|
|
286
|
+
/* @__PURE__ */ jsxs("div", { className: "mt-4 space-y-4 text-center", children: [
|
|
287
|
+
/* @__PURE__ */ jsxs("div", { "aria-live": "polite", children: [
|
|
288
|
+
/* @__PURE__ */ jsx("h3", { className: "text-[17px] font-extrabold", children: activeItem.title }),
|
|
289
|
+
/* @__PURE__ */ jsx("p", { className: "mt-[3px] text-[12.5px] text-muted-foreground", children: activeItem.meta })
|
|
290
|
+
] }),
|
|
291
|
+
(caption || positionLabel) && /* @__PURE__ */ jsxs("div", { className: "flex items-center justify-center gap-2 text-[12.5px] text-muted-foreground", children: [
|
|
292
|
+
caption && /* @__PURE__ */ jsx("span", { children: caption }),
|
|
293
|
+
caption && positionLabel && /* @__PURE__ */ jsx("span", { "aria-hidden": "true", children: "\xB7" }),
|
|
294
|
+
positionLabel && /* @__PURE__ */ jsx("span", { children: positionLabel })
|
|
295
|
+
] })
|
|
296
|
+
] })
|
|
297
|
+
] });
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
export { Coverflow };
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import * as React from 'react';
|
|
2
2
|
|
|
3
|
-
declare const DateInput: React.ForwardRefExoticComponent<Omit<React.InputHTMLAttributes<HTMLInputElement>, "
|
|
3
|
+
declare const DateInput: React.ForwardRefExoticComponent<Omit<React.InputHTMLAttributes<HTMLInputElement>, "size" | "type"> & {
|
|
4
4
|
/** Native input type. Defaults to "date". */
|
|
5
5
|
type?: "date" | "datetime-local" | "time" | "month" | "week";
|
|
6
6
|
size?: "sm" | "default" | "lg";
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import * as react_jsx_runtime from 'react/jsx-runtime';
|
|
2
|
+
import * as React from 'react';
|
|
3
|
+
import { DayRailChipLabels } from './event-types.js';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* DayRailChip — one day in the horizontally scrolling day rail.
|
|
7
|
+
*
|
|
8
|
+
* Three states the design distinguishes and that are easy to collapse by
|
|
9
|
+
* accident:
|
|
10
|
+
* - ACTIVE: FILLED — accent ground, accent-foreground label, count and dot.
|
|
11
|
+
* - HAS EVENTS: card ground, normal foreground, an accent density dot.
|
|
12
|
+
* - EMPTY: muted text, NO dot, an em dash instead of "0". A day with nothing
|
|
13
|
+
* on stays in the rail — ten of fourteen days routinely read empty, and
|
|
14
|
+
* hiding them makes the rail lie about how far it reaches — but the caller
|
|
15
|
+
* decides whether it is interactive, and a rail that NAVIGATES has nothing
|
|
16
|
+
* to send an empty day to.
|
|
17
|
+
*
|
|
18
|
+
* ── Why active is filled ──────────────────────────────────────────────────
|
|
19
|
+
* It was an accent border and accent text. Measured against the real tokens
|
|
20
|
+
* that is a 1px hairline plus a label at 1.16:1 against its inactive
|
|
21
|
+
* neighbours in light mode — and in dark, where `--primary` (0.66) is DIMMER
|
|
22
|
+
* than `--foreground` (0.96), the active chip read as the recessed one at
|
|
23
|
+
* 2.86:1. Survivable while a large pinned day header also announced the day;
|
|
24
|
+
* not survivable once the rail became the only indicator.
|
|
25
|
+
*
|
|
26
|
+
* An eye scanning fourteen near-identical 62px boxes horizontally, in
|
|
27
|
+
* peripheral vision, while the page moves, resolves AREA and LUMINANCE STEP
|
|
28
|
+
* long before it resolves the hue of a hairline or the weight of 11.5px type.
|
|
29
|
+
* The fill presents ~3,100px² at 5.97:1 (light) / 6.09:1 (dark). A tinted
|
|
30
|
+
* ground would not do it: `bg-accent/10` and `--accent-subtle` both measure
|
|
31
|
+
* ~1.15:1 against the chip's own card, which is no signal at all.
|
|
32
|
+
*
|
|
33
|
+
* `--accent`, deliberately, not `--gradient-primary`: white on that gradient's
|
|
34
|
+
* top stop is 4.45:1, under AA for text, and the gradient has no `.dark`
|
|
35
|
+
* override so the chip's ink direction would invert against the rest of the
|
|
36
|
+
* dark theme. `--accent` also already means "current" here — the bottom nav's
|
|
37
|
+
* active tab uses it.
|
|
38
|
+
*
|
|
39
|
+
* The border stays 1px in BOTH states. Thickening it on active would change
|
|
40
|
+
* the box model, and these chips sit in an equal-width grid that would reflow
|
|
41
|
+
* on every scroll-spy tick.
|
|
42
|
+
*
|
|
43
|
+
* Renders a link when `href` is given, a button when only `onClick` is, and a
|
|
44
|
+
* plain div otherwise. The button announces `aria-current`, not
|
|
45
|
+
* `aria-pressed`: this is the current item in a set of days, not a toggle.
|
|
46
|
+
*/
|
|
47
|
+
type DayRailChipLinkProps = {
|
|
48
|
+
href: string;
|
|
49
|
+
className?: string;
|
|
50
|
+
children: React.ReactNode;
|
|
51
|
+
"aria-current"?: "page";
|
|
52
|
+
onClick?: () => void;
|
|
53
|
+
};
|
|
54
|
+
type DayRailChipProps = {
|
|
55
|
+
/** Short weekday label, already localised — "Today", "Fri", "Sâm". */
|
|
56
|
+
label: string;
|
|
57
|
+
/** Day of month, as displayed. */
|
|
58
|
+
dayNumber: string;
|
|
59
|
+
/** Events on this day, after any active filter. */
|
|
60
|
+
count: number;
|
|
61
|
+
isActive?: boolean;
|
|
62
|
+
/** Keeps the bold weight when inactive. */
|
|
63
|
+
isWeekend?: boolean;
|
|
64
|
+
labels: DayRailChipLabels;
|
|
65
|
+
href?: string;
|
|
66
|
+
linkComponent?: React.ComponentType<DayRailChipLinkProps>;
|
|
67
|
+
onClick?: () => void;
|
|
68
|
+
className?: string;
|
|
69
|
+
};
|
|
70
|
+
declare function DayRailChip({ label, dayNumber, count, isActive, isWeekend, labels, href, linkComponent: LinkComponent, onClick, className, }: DayRailChipProps): react_jsx_runtime.JSX.Element;
|
|
71
|
+
type DayRailChipSkeletonProps = {
|
|
72
|
+
className?: string;
|
|
73
|
+
};
|
|
74
|
+
/**
|
|
75
|
+
* Mirrors one `<DayRailChip>`'s footprint (62×50, 12px radius — the source
|
|
76
|
+
* design's own day-rail skeleton geometry, which also happens to match this
|
|
77
|
+
* chip's rendered size: label + count rows inside `py-2` padding). The caller
|
|
78
|
+
* repeats it with the rail's own `gap-2` to build the loading rail.
|
|
79
|
+
*/
|
|
80
|
+
declare function DayRailChipSkeleton({ className }: DayRailChipSkeletonProps): react_jsx_runtime.JSX.Element;
|
|
81
|
+
|
|
82
|
+
export { DayRailChip, type DayRailChipLinkProps, type DayRailChipProps, DayRailChipSkeleton, type DayRailChipSkeletonProps };
|