@bearmenu/ui 0.8.9 → 0.8.11
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-24RFI4RQ.js +167 -0
- package/dist/{chunk-UKBNQO6Q.js → chunk-2CXU5WZO.js} +1 -1
- package/dist/chunk-2XCZJWTV.js +121 -0
- package/dist/{chunk-62ES6N32.js → chunk-43RC7MHY.js} +1 -1
- package/dist/chunk-5RWTOT5Y.js +72 -0
- package/dist/{chunk-Q6QH52LJ.js → chunk-6TEEY5YA.js} +2 -2
- package/dist/{chunk-AXV5O2IZ.js → chunk-AKQ57NXY.js} +3 -3
- package/dist/chunk-CLLY6XB3.js +41 -0
- package/dist/chunk-DTXNDLSE.js +125 -0
- package/dist/chunk-F6FMZJDG.js +113 -0
- package/dist/{chunk-2WDJR6K7.js → chunk-G7IFL4Z4.js} +4 -1
- package/dist/chunk-H3ZZDX5L.js +61 -0
- package/dist/{chunk-H3BV4VAG.js → chunk-JS3EINZD.js} +2 -2
- package/dist/chunk-MZL7CGCL.js +109 -0
- package/dist/{chunk-TH6W2PWG.js → chunk-UDTG3RSE.js} +1 -1
- package/dist/chunk-WCTHZKU7.js +37 -0
- package/dist/{chunk-TT3WFS6S.js → chunk-YOKHBAIH.js} +11 -11
- package/dist/components/alert-dialog.js +1 -1
- package/dist/components/alert.d.ts +2 -2
- package/dist/components/avatar.d.ts +3 -3
- package/dist/components/badge.d.ts +1 -1
- package/dist/components/badge.js +1 -1
- package/dist/components/button.d.ts +2 -2
- package/dist/components/button.js +1 -1
- package/dist/components/calendar.js +1 -1
- package/dist/components/card.d.ts +4 -4
- package/dist/components/carousel.js +1 -1
- package/dist/components/combobox.js +1 -1
- package/dist/components/command.d.ts +2 -2
- package/dist/components/concept-compare-rail.d.ts +50 -0
- package/dist/components/concept-compare-rail.js +5 -0
- package/dist/components/concept-compare.d.ts +75 -38
- package/dist/components/concept-compare.js +209 -133
- package/dist/components/concept-convergence.d.ts +41 -0
- package/dist/components/concept-convergence.js +53 -0
- package/dist/components/convergence.d.ts +25 -0
- package/dist/components/convergence.js +4 -0
- package/dist/components/dialog.d.ts +2 -2
- package/dist/components/empty-state.js +1 -1
- package/dist/components/event-card.js +1 -1
- package/dist/components/event-compact-row.js +1 -1
- package/dist/components/event-detail-panel.js +2 -2
- package/dist/components/event-index-row.js +1 -1
- package/dist/components/event-list-row-wide.js +2 -2
- package/dist/components/event-list-row.js +2 -2
- package/dist/components/explainer-steps.d.ts +34 -0
- package/dist/components/explainer-steps.js +36 -0
- package/dist/components/filter-category-row.js +1 -1
- package/dist/components/glass-ground.d.ts +61 -0
- package/dist/components/glass-ground.js +3 -0
- package/dist/components/haptic-button.js +1 -1
- package/dist/components/hub-surface.js +11 -1
- package/dist/components/hue-chip.d.ts +70 -0
- package/dist/components/hue-chip.js +3 -0
- package/dist/components/input-group.d.ts +2 -2
- package/dist/components/input.d.ts +1 -1
- package/dist/components/item.d.ts +4 -4
- package/dist/components/kbd.d.ts +2 -2
- package/dist/components/kitchen-dossier.d.ts +88 -0
- package/dist/components/kitchen-dossier.js +186 -0
- package/dist/components/link-button.js +1 -1
- package/dist/components/menu-dead-end.d.ts +56 -0
- package/dist/components/menu-dead-end.js +49 -0
- package/dist/components/menu-family-block.d.ts +98 -0
- package/dist/components/menu-family-block.js +153 -0
- package/dist/components/menu-family-card.d.ts +33 -27
- package/dist/components/menu-family-card.js +41 -85
- package/dist/components/menu-field.js +92 -2
- package/dist/components/menu-filter-chips.d.ts +86 -0
- package/dist/components/menu-filter-chips.js +122 -0
- package/dist/components/menu-group-rows.d.ts +53 -0
- package/dist/components/menu-group-rows.js +67 -0
- package/dist/components/menu-hub-hero.js +10 -5
- package/dist/components/menu-index-header.d.ts +37 -0
- package/dist/components/menu-index-header.js +68 -0
- package/dist/components/menu-index-list.d.ts +48 -0
- package/dist/components/menu-index-list.js +96 -0
- package/dist/components/menu-intent-hero.d.ts +69 -0
- package/dist/components/menu-intent-hero.js +87 -0
- package/dist/components/menu-item.js +3 -3
- package/dist/components/menu-kitchen-list.d.ts +82 -0
- package/dist/components/menu-kitchen-list.js +5 -0
- package/dist/components/menu-mocks.d.ts +37 -2
- package/dist/components/menu-mocks.js +152 -27
- package/dist/components/menu-types.d.ts +182 -6
- package/dist/components/motion-icon-controls.js +2 -2
- package/dist/components/motion-icon-set.js +6 -6
- package/dist/components/motion-icon.d.ts +22 -1
- package/dist/components/motion-icon.js +1 -1
- package/dist/components/multi-select-combobox.js +2 -2
- package/dist/components/native-select.d.ts +2 -2
- package/dist/components/pagination.js +1 -1
- package/dist/components/photo-ground.d.ts +64 -0
- package/dist/components/photo-ground.js +3 -0
- package/dist/components/place-about-tab.js +2 -2
- package/dist/components/place-details-mobile.js +3 -3
- package/dist/components/place-types.d.ts +2 -2
- package/dist/components/qa-block.d.ts +36 -0
- package/dist/components/qa-block.js +24 -0
- package/dist/components/scroll-fade.js +3 -121
- package/dist/components/searchable-select.js +1 -1
- package/dist/components/select.d.ts +1 -1
- package/dist/components/signature-chip.d.ts +2 -2
- package/dist/components/signature-chip.js +2 -2
- package/dist/components/sliding-panels.js +1 -1
- package/dist/components/snap-rail.d.ts +26 -3
- package/dist/components/snap-rail.js +3 -109
- package/dist/components/spinner.d.ts +4 -4
- package/dist/components/story.d.ts +34 -0
- package/dist/components/story.js +3 -0
- package/dist/components/tag.d.ts +4 -4
- package/dist/components/text.d.ts +5 -5
- package/dist/components/toggle.d.ts +1 -1
- package/dist/components/top-comparison.d.ts +28 -0
- package/dist/components/top-comparison.js +76 -0
- package/dist/components/top-cross-link.d.ts +34 -0
- package/dist/components/top-cross-link.js +101 -0
- package/dist/components/top-ranking-entry.d.ts +53 -0
- package/dist/components/top-ranking-entry.js +217 -0
- package/dist/components/top-ranking-hero.d.ts +61 -0
- package/dist/components/top-ranking-hero.js +99 -0
- package/dist/components/top-ranking-legend.d.ts +69 -0
- package/dist/components/top-ranking-legend.js +124 -0
- package/dist/components/top-types.d.ts +109 -0
- package/dist/components/top-types.js +1 -0
- package/dist/components/venue-grade-dimensions.d.ts +3 -1
- package/dist/components/venue-grade-dimensions.js +1 -1
- package/dist/components/venue-quotes.d.ts +1 -1
- package/dist/components/venue-quotes.js +1 -1
- package/dist/components/venue-wall.js +5 -3
- package/dist/components/weekday-theme-banner.js +1 -1
- package/dist/components/weekend-panel.js +2 -2
- package/package.json +1 -1
- package/src/components/badge.tsx +1 -1
- package/src/components/button.tsx +2 -2
- package/src/components/concept-compare-rail.tsx +132 -0
- package/src/components/concept-compare.stories.tsx +122 -54
- package/src/components/concept-compare.tsx +368 -196
- package/src/components/concept-convergence.stories.tsx +53 -0
- package/src/components/concept-convergence.tsx +90 -0
- package/src/components/convergence.stories.tsx +54 -0
- package/src/components/convergence.tsx +177 -0
- package/src/components/explainer-steps.stories.tsx +46 -0
- package/src/components/explainer-steps.tsx +74 -0
- package/src/components/glass-ground.tsx +104 -0
- package/src/components/hub-surface.tsx +11 -1
- package/src/components/hue-chip.stories.tsx +76 -0
- package/src/components/hue-chip.test.tsx +62 -0
- package/src/components/hue-chip.tsx +99 -0
- package/src/components/kitchen-dossier.stories.tsx +108 -0
- package/src/components/kitchen-dossier.tsx +321 -0
- package/src/components/menu-dead-end.stories.tsx +67 -0
- package/src/components/menu-dead-end.tsx +127 -0
- package/src/components/menu-family-block.stories.tsx +91 -0
- package/src/components/menu-family-block.tsx +303 -0
- package/src/components/menu-family-card.stories.tsx +43 -35
- package/src/components/menu-family-card.tsx +115 -142
- package/src/components/menu-filter-chips.stories.tsx +107 -0
- package/src/components/menu-filter-chips.tsx +238 -0
- package/src/components/menu-group-rows.stories.tsx +54 -0
- package/src/components/menu-group-rows.tsx +133 -0
- package/src/components/menu-hub-hero.test.tsx +39 -0
- package/src/components/menu-hub-hero.tsx +17 -5
- package/src/components/menu-index-header.stories.tsx +72 -0
- package/src/components/menu-index-header.tsx +125 -0
- package/src/components/menu-index-list.stories.tsx +50 -0
- package/src/components/menu-index-list.tsx +149 -0
- package/src/components/menu-intent-hero.stories.tsx +67 -0
- package/src/components/menu-intent-hero.tsx +189 -0
- package/src/components/menu-kitchen-list.stories.tsx +67 -0
- package/src/components/menu-kitchen-list.test.tsx +78 -0
- package/src/components/menu-kitchen-list.tsx +284 -0
- package/src/components/menu-mocks.ts +198 -23
- package/src/components/menu-types.ts +189 -6
- package/src/components/motion-icon-controls.tsx +11 -11
- package/src/components/motion-icon-set.tsx +6 -5
- package/src/components/motion-icon.test.tsx +23 -1
- package/src/components/motion-icon.tsx +20 -0
- package/src/components/photo-ground.tsx +147 -0
- package/src/components/place-types.ts +2 -2
- package/src/components/qa-block.stories.tsx +43 -0
- package/src/components/qa-block.tsx +60 -0
- package/src/components/signature-chip.tsx +3 -3
- package/src/components/snap-rail.tsx +58 -15
- package/src/components/story.tsx +225 -0
- package/src/components/top-comparison.stories.tsx +78 -0
- package/src/components/top-comparison.tsx +151 -0
- package/src/components/top-cross-link.stories.tsx +84 -0
- package/src/components/top-cross-link.tsx +171 -0
- package/src/components/top-ranking-entry.stories.tsx +171 -0
- package/src/components/top-ranking-entry.test.tsx +105 -0
- package/src/components/top-ranking-entry.tsx +425 -0
- package/src/components/top-ranking-hero.stories.tsx +76 -0
- package/src/components/top-ranking-hero.tsx +218 -0
- package/src/components/top-ranking-legend.stories.tsx +78 -0
- package/src/components/top-ranking-legend.tsx +239 -0
- package/src/components/top-types.ts +110 -0
- package/src/components/venue-grade-dimensions.tsx +2 -2
- package/src/components/venue-quotes.stories.tsx +1 -1
- package/src/components/venue-quotes.tsx +4 -2
- package/src/components/venue-wall.tsx +9 -4
- package/src/components/weekday-theme-banner.tsx +1 -1
- package/dist/chunk-THOVPZNN.js +0 -37
- package/dist/chunk-YS6BHKZO.js +0 -92
- package/dist/components/ranked-venue-list.d.ts +0 -49
- package/dist/components/ranked-venue-list.js +0 -87
- package/dist/components/signature-line-list.d.ts +0 -45
- package/dist/components/signature-line-list.js +0 -99
- package/dist/components/venue-busyness-strip.d.ts +0 -35
- package/dist/components/venue-busyness-strip.js +0 -37
- package/dist/components/venue-signature-block.d.ts +0 -39
- package/dist/components/venue-signature-block.js +0 -6
- package/dist/components/venue-spotlight.d.ts +0 -74
- package/dist/components/venue-spotlight.js +0 -138
- package/src/components/ranked-venue-list.stories.tsx +0 -80
- package/src/components/ranked-venue-list.tsx +0 -177
- package/src/components/signature-line-list.stories.tsx +0 -85
- package/src/components/signature-line-list.tsx +0 -136
- package/src/components/venue-busyness-strip.stories.tsx +0 -66
- package/src/components/venue-busyness-strip.tsx +0 -64
- package/src/components/venue-signature-block.stories.tsx +0 -68
- package/src/components/venue-signature-block.tsx +0 -71
- package/src/components/venue-spotlight.stories.tsx +0 -141
- package/src/components/venue-spotlight.tsx +0 -236
- package/dist/{chunk-XWMRABZW.js → chunk-HSIYYA2E.js} +1 -1
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from "@storybook/react";
|
|
2
|
+
import { ConceptConvergence } from "./concept-convergence";
|
|
3
|
+
|
|
4
|
+
function photo(hue: number) {
|
|
5
|
+
return (
|
|
6
|
+
"data:image/svg+xml;utf8," +
|
|
7
|
+
encodeURIComponent(
|
|
8
|
+
`<svg xmlns="http://www.w3.org/2000/svg" width="320" height="400"><defs><linearGradient id="g" x1="0" y1="0" x2="1" y2="1"><stop offset="0" stop-color="hsl(${hue} 30% 62%)"/><stop offset="1" stop-color="hsl(${hue} 20% 28%)"/></linearGradient></defs><rect width="320" height="400" fill="url(#g)"/><circle cx="90" cy="120" r="60" fill="hsl(${hue} 40% 80%)" opacity=".6"/></svg>`
|
|
9
|
+
)
|
|
10
|
+
);
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
const meta: Meta<typeof ConceptConvergence> = {
|
|
14
|
+
title: "Menu/ConceptConvergence",
|
|
15
|
+
component: ConceptConvergence,
|
|
16
|
+
tags: ["autodocs"],
|
|
17
|
+
decorators: [
|
|
18
|
+
(Story) => (
|
|
19
|
+
<div className="w-[360px] bg-[oklch(0.21_0.012_55)] p-4">
|
|
20
|
+
<Story />
|
|
21
|
+
</div>
|
|
22
|
+
),
|
|
23
|
+
],
|
|
24
|
+
args: {
|
|
25
|
+
aliases: [
|
|
26
|
+
{ label: "Papanași", price: "18 lei" },
|
|
27
|
+
{ label: "Papanasi cu smantana si dulceata", price: "24 lei" },
|
|
28
|
+
{ label: "Papanași de casă", price: "28 lei" },
|
|
29
|
+
{ label: "Papanasi (2 buc)", price: "32 lei" },
|
|
30
|
+
{ label: "Papanași cu fineți", price: "44 lei" },
|
|
31
|
+
],
|
|
32
|
+
concept: { title: "Papanași", photoUrl: photo(30), badge: "32 names" },
|
|
33
|
+
scale: ["8 lei", "45 kitchens", "44 lei"],
|
|
34
|
+
},
|
|
35
|
+
parameters: {
|
|
36
|
+
docs: {
|
|
37
|
+
description: {
|
|
38
|
+
component:
|
|
39
|
+
"Many names, one dish, drawn on the explainer's dark stage. The kitchens' own spellings of a dish, each with its price, converge on the concept they were matched into — the proof the concepts explainer opens with. Chips are white on ink; the concept card is a room's real photograph on the shared `PhotoGround`.",
|
|
40
|
+
},
|
|
41
|
+
},
|
|
42
|
+
},
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
export default meta;
|
|
46
|
+
type Story = StoryObj<typeof ConceptConvergence>;
|
|
47
|
+
|
|
48
|
+
export const Default: Story = {};
|
|
49
|
+
|
|
50
|
+
/** No photograph for the lead room: the ground with the initial. */
|
|
51
|
+
export const NoPhoto: Story = {
|
|
52
|
+
args: { concept: { title: "Ciorbă de burtă", badge: "12 names" } },
|
|
53
|
+
};
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import { cn } from "../lib/utils";
|
|
2
|
+
import { Badge } from "./badge";
|
|
3
|
+
import { Convergence } from "./convergence";
|
|
4
|
+
import { PhotoGround } from "./photo-ground";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* ConceptConvergence — many names, one dish, drawn on a dark stage.
|
|
8
|
+
*
|
|
9
|
+
* The explainer's proof: the kitchens' OWN spellings of a dish, each with its
|
|
10
|
+
* price, converging on the one concept they were matched into. Five visibly
|
|
11
|
+
* different strings under one photograph with one price column say what a
|
|
12
|
+
* paragraph about "matching" cannot.
|
|
13
|
+
*
|
|
14
|
+
* Drawn for the explainer's dark stage, and only there: the chips are white
|
|
15
|
+
* on ink, not a family's hue — the tint was doing no work once the stage
|
|
16
|
+
* went dark, and the spellings are the point, not the kitchens' cuisines.
|
|
17
|
+
* The concept card is the venue's real photograph, never the item's (which
|
|
18
|
+
* is generated), on the same ground as every photographed card.
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
export type ConceptConvergenceAlias = {
|
|
22
|
+
/** The kitchen's spelling, verbatim. */
|
|
23
|
+
label: string;
|
|
24
|
+
/** Pre-formatted, e.g. "32 lei". */
|
|
25
|
+
price: string;
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
export type ConceptConvergenceProps = {
|
|
29
|
+
aliases: ConceptConvergenceAlias[];
|
|
30
|
+
concept: {
|
|
31
|
+
title: string;
|
|
32
|
+
/** A room's photograph — venue photography only. */
|
|
33
|
+
photoUrl?: string;
|
|
34
|
+
/** "32 names" — the badge on the card. */
|
|
35
|
+
badge: string;
|
|
36
|
+
};
|
|
37
|
+
/**
|
|
38
|
+
* Low, middle and high of the ruler under the diagram, e.g.
|
|
39
|
+
* `["8 lei", "45 kitchens", "44 lei"]`. All three, or none.
|
|
40
|
+
*/
|
|
41
|
+
scale?: [string, string, string];
|
|
42
|
+
className?: string;
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
export function ConceptConvergence({ aliases, concept, scale, className }: ConceptConvergenceProps) {
|
|
46
|
+
return (
|
|
47
|
+
<div className={cn("flex flex-col gap-3 text-white", className)}>
|
|
48
|
+
<Convergence
|
|
49
|
+
linesClassName="text-white/30"
|
|
50
|
+
sources={aliases.map((alias, index) => (
|
|
51
|
+
<span
|
|
52
|
+
key={index}
|
|
53
|
+
className="inline-flex h-7 max-w-full items-center gap-1.5 rounded-full bg-white/10 px-2.5 text-[11.5px] text-white"
|
|
54
|
+
>
|
|
55
|
+
<span className="min-w-0 truncate">{alias.label}</span>
|
|
56
|
+
<span className="shrink-0 tabular-nums text-white/60">{alias.price}</span>
|
|
57
|
+
</span>
|
|
58
|
+
))}
|
|
59
|
+
target={
|
|
60
|
+
<span className="relative isolate block aspect-[4/5] w-[112px] overflow-hidden rounded-[16px] shadow-[0_8px_24px_oklch(0_0_0/0.35)]">
|
|
61
|
+
<PhotoGround
|
|
62
|
+
className="-z-10"
|
|
63
|
+
mode="stack"
|
|
64
|
+
photos={[concept.photoUrl]}
|
|
65
|
+
initials={concept.title.charAt(0).toLocaleUpperCase()}
|
|
66
|
+
overlay
|
|
67
|
+
loading="eager"
|
|
68
|
+
/>
|
|
69
|
+
<Badge variant="overlay" className="absolute left-2 top-2">
|
|
70
|
+
{concept.badge}
|
|
71
|
+
</Badge>
|
|
72
|
+
<span className="absolute inset-x-2 bottom-2 block text-balance font-serif text-[17px] font-semibold leading-tight tracking-[-0.02em] [text-shadow:0_1px_8px_oklch(0_0_0/0.5)]">
|
|
73
|
+
{concept.title}
|
|
74
|
+
</span>
|
|
75
|
+
</span>
|
|
76
|
+
}
|
|
77
|
+
/>
|
|
78
|
+
|
|
79
|
+
{scale && (
|
|
80
|
+
<div className="flex items-center gap-2 text-[11px] tabular-nums text-white/60">
|
|
81
|
+
<span>{scale[0]}</span>
|
|
82
|
+
<span aria-hidden className="h-px flex-1 bg-white/20" />
|
|
83
|
+
<span className="font-semibold text-white">{scale[1]}</span>
|
|
84
|
+
<span aria-hidden className="h-px flex-1 bg-white/20" />
|
|
85
|
+
<span>{scale[2]}</span>
|
|
86
|
+
</div>
|
|
87
|
+
)}
|
|
88
|
+
</div>
|
|
89
|
+
);
|
|
90
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from "@storybook/react";
|
|
2
|
+
import { MessageSquareQuote } from "lucide-react";
|
|
3
|
+
import { Convergence } from "./convergence";
|
|
4
|
+
|
|
5
|
+
const meta: Meta<typeof Convergence> = {
|
|
6
|
+
title: "Menu/Convergence",
|
|
7
|
+
component: Convergence,
|
|
8
|
+
tags: ["autodocs"],
|
|
9
|
+
decorators: [
|
|
10
|
+
(Story) => (
|
|
11
|
+
<div className="w-[360px] bg-background p-4">
|
|
12
|
+
<Story />
|
|
13
|
+
</div>
|
|
14
|
+
),
|
|
15
|
+
],
|
|
16
|
+
parameters: {
|
|
17
|
+
docs: {
|
|
18
|
+
description: {
|
|
19
|
+
component:
|
|
20
|
+
"Many things on the left becoming one thing on the right. The DS draws the shape — the fanned stack, the hairlines curving into one point — and the caller draws the nodes. `ConceptConvergence` is the dish version; this story is the reviews version, redacted lines converging on a mention count.",
|
|
21
|
+
},
|
|
22
|
+
},
|
|
23
|
+
},
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
export default meta;
|
|
27
|
+
type Story = StoryObj<typeof Convergence>;
|
|
28
|
+
|
|
29
|
+
export const Reviews: Story = {
|
|
30
|
+
render: () => (
|
|
31
|
+
<Convergence
|
|
32
|
+
linesClassName="text-white/30"
|
|
33
|
+
sources={Array.from({ length: 6 }, (_, index) => (
|
|
34
|
+
<span key={index} className="flex items-center gap-2" aria-hidden>
|
|
35
|
+
<MessageSquareQuote className="h-4 w-4 text-white/40" />
|
|
36
|
+
<span className="h-2 rounded-full bg-white/20" style={{ width: 48 + ((index * 37) % 60) }} />
|
|
37
|
+
</span>
|
|
38
|
+
))}
|
|
39
|
+
target={
|
|
40
|
+
<span className="flex flex-col items-center rounded-[18px] bg-white/10 px-5 py-4 text-white shadow-[0_8px_24px_oklch(0_0_0/0.35)]">
|
|
41
|
+
<span className="text-[40px] font-extrabold leading-none tabular-nums">56</span>
|
|
42
|
+
<span className="mt-1 text-[11px] text-white/70">mentions</span>
|
|
43
|
+
</span>
|
|
44
|
+
}
|
|
45
|
+
/>
|
|
46
|
+
),
|
|
47
|
+
decorators: [
|
|
48
|
+
(Story) => (
|
|
49
|
+
<div className="w-[360px] bg-[oklch(0.21_0.012_55)] p-4">
|
|
50
|
+
<Story />
|
|
51
|
+
</div>
|
|
52
|
+
),
|
|
53
|
+
],
|
|
54
|
+
};
|
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import type { ReactNode } from "react";
|
|
4
|
+
import { motion, useReducedMotion, type Variants } from "framer-motion";
|
|
5
|
+
import { cn } from "../lib/utils";
|
|
6
|
+
import { duration, ease, springSnappy, stagger } from "../lib/motion";
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Convergence — many things on the left becoming one thing on the right.
|
|
10
|
+
*
|
|
11
|
+
* The diagram the explainers are built on: several spellings of a dish
|
|
12
|
+
* converging on one concept; a column of reviews converging on one mention
|
|
13
|
+
* count. The DS draws the SHAPE — a stack of sources, hairlines curving into
|
|
14
|
+
* a single point, the target — and the caller draws the nodes, because what a
|
|
15
|
+
* source is (a chip, a redacted line, a thumbnail) is the caller's data.
|
|
16
|
+
*
|
|
17
|
+
* ## The motion tells the story
|
|
18
|
+
*
|
|
19
|
+
* Three beats on mount, overlapping, under 700ms: the many arrive (each
|
|
20
|
+
* source slides in from the left, staggered), the lines draw toward the one
|
|
21
|
+
* (each curve starts once its source is about half visible, so cause reads
|
|
22
|
+
* before effect), and the one lands (the target scales in on a snappy spring
|
|
23
|
+
* while the last lines are still in flight — it catches them rather than
|
|
24
|
+
* waiting for a clean hand-off). Self-contained: the sheet this sits in
|
|
25
|
+
* animates by CSS `data-state`, which nothing here could await, and the
|
|
26
|
+
* `delayChildren` already puts the content a beat behind the surface.
|
|
27
|
+
*
|
|
28
|
+
* `useReducedMotion` owns the fallback — the stylesheet's global rule only
|
|
29
|
+
* shortens CSS transitions and does not reach Framer's own scheduler. Under
|
|
30
|
+
* it the diagram renders settled and fades in once. `animate={false}` is a
|
|
31
|
+
* separate switch for static reuse (a docs frame), independent of the
|
|
32
|
+
* reader's setting.
|
|
33
|
+
*
|
|
34
|
+
* ## Why the lines are an SVG and not borders
|
|
35
|
+
*
|
|
36
|
+
* A curve from each source to one point cannot be a border on anything. The
|
|
37
|
+
* SVG stretches to the sources column's height (`preserveAspectRatio: none`)
|
|
38
|
+
* and keeps its hairlines at one pixel through `vector-effect`, so the curves
|
|
39
|
+
* follow however tall the stack is without the stroke thickening with it.
|
|
40
|
+
* `pathLength` is normalised by Framer to the path's own space, so the draw
|
|
41
|
+
* is unaffected by the stretch.
|
|
42
|
+
*/
|
|
43
|
+
|
|
44
|
+
/** The first line starts once the first source is about half visible. */
|
|
45
|
+
const LINE_LEAD = 0.15;
|
|
46
|
+
/** The target lands before the last line finishes — it catches them. */
|
|
47
|
+
const TARGET_DELAY = 0.38;
|
|
48
|
+
|
|
49
|
+
const sourcesContainer: Variants = {
|
|
50
|
+
hidden: {},
|
|
51
|
+
show: { transition: { delayChildren: 0.06, staggerChildren: stagger.tight } },
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
const sourceItem: Variants = {
|
|
55
|
+
hidden: { opacity: 0, x: -8 },
|
|
56
|
+
show: { opacity: 1, x: 0, transition: { duration: duration.quick, ease: ease.enter } },
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
const targetItem: Variants = {
|
|
60
|
+
hidden: { opacity: 0, scale: 0.92 },
|
|
61
|
+
show: { opacity: 1, scale: 1, transition: { delay: TARGET_DELAY, ...springSnappy } },
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
export type ConvergeLinesProps = {
|
|
65
|
+
/** How many sources the lines leave from. */
|
|
66
|
+
count: number;
|
|
67
|
+
/** Draw the curves in, source by source. Off: drawn already. */
|
|
68
|
+
animate?: boolean;
|
|
69
|
+
className?: string;
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
const LINES_WIDTH = 40;
|
|
73
|
+
const LINES_HEIGHT = 100;
|
|
74
|
+
|
|
75
|
+
export function ConvergeLines({ count, animate = true, className }: ConvergeLinesProps) {
|
|
76
|
+
const mid = LINES_HEIGHT / 2;
|
|
77
|
+
return (
|
|
78
|
+
<svg
|
|
79
|
+
aria-hidden
|
|
80
|
+
viewBox={`0 0 ${LINES_WIDTH} ${LINES_HEIGHT}`}
|
|
81
|
+
preserveAspectRatio="none"
|
|
82
|
+
className={cn("h-full w-10 text-border", className)}
|
|
83
|
+
>
|
|
84
|
+
{Array.from({ length: count }, (_, index) => {
|
|
85
|
+
const y = ((index + 0.5) / count) * LINES_HEIGHT;
|
|
86
|
+
const d = `M0 ${y} C ${LINES_WIDTH / 2} ${y}, ${LINES_WIDTH / 2} ${mid}, ${LINES_WIDTH} ${mid}`;
|
|
87
|
+
const stroke = {
|
|
88
|
+
d,
|
|
89
|
+
fill: "none",
|
|
90
|
+
stroke: "currentColor",
|
|
91
|
+
strokeWidth: 1.5,
|
|
92
|
+
strokeLinecap: "round" as const,
|
|
93
|
+
vectorEffect: "non-scaling-stroke" as const,
|
|
94
|
+
};
|
|
95
|
+
return animate ? (
|
|
96
|
+
<motion.path
|
|
97
|
+
key={index}
|
|
98
|
+
{...stroke}
|
|
99
|
+
initial={{ pathLength: 0, opacity: 0 }}
|
|
100
|
+
animate={{ pathLength: 1, opacity: 1 }}
|
|
101
|
+
transition={{
|
|
102
|
+
delay: LINE_LEAD + index * stagger.tight,
|
|
103
|
+
duration: duration.standard,
|
|
104
|
+
ease: ease.standard,
|
|
105
|
+
}}
|
|
106
|
+
/>
|
|
107
|
+
) : (
|
|
108
|
+
<path key={index} {...stroke} />
|
|
109
|
+
);
|
|
110
|
+
})}
|
|
111
|
+
</svg>
|
|
112
|
+
);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
export type ConvergenceProps = {
|
|
116
|
+
/** The many. One node each; the stack fans slightly so it reads as a spread, not a list. */
|
|
117
|
+
sources: ReactNode[];
|
|
118
|
+
/** The one. */
|
|
119
|
+
target: ReactNode;
|
|
120
|
+
/** The entrance choreography. Off for static reuse; the reader's reduced-motion setting is honoured either way. */
|
|
121
|
+
animate?: boolean;
|
|
122
|
+
/** The curves' colour, e.g. `"text-white/30"` on a dark stage. Defaults to the border. */
|
|
123
|
+
linesClassName?: string;
|
|
124
|
+
className?: string;
|
|
125
|
+
};
|
|
126
|
+
|
|
127
|
+
export function Convergence({
|
|
128
|
+
sources,
|
|
129
|
+
target,
|
|
130
|
+
animate = true,
|
|
131
|
+
linesClassName,
|
|
132
|
+
className,
|
|
133
|
+
}: ConvergenceProps) {
|
|
134
|
+
const reduce = useReducedMotion();
|
|
135
|
+
const run = animate && !reduce;
|
|
136
|
+
const fadeOnly = animate && reduce;
|
|
137
|
+
|
|
138
|
+
return (
|
|
139
|
+
<motion.div
|
|
140
|
+
className={cn("grid grid-cols-[minmax(0,1fr)_auto_auto] items-center", className)}
|
|
141
|
+
initial={fadeOnly ? { opacity: 0 } : false}
|
|
142
|
+
animate={fadeOnly ? { opacity: 1 } : undefined}
|
|
143
|
+
transition={{ duration: duration.quick, ease: ease.standard }}
|
|
144
|
+
>
|
|
145
|
+
<motion.div
|
|
146
|
+
className={cn(
|
|
147
|
+
"flex min-w-0 flex-col items-start justify-center gap-2 py-1",
|
|
148
|
+
/* The fan: the middle sources sit a little further right than the
|
|
149
|
+
ends, so five chips read as one hand of cards, not as a menu. */
|
|
150
|
+
"[&>*:nth-child(2)]:translate-x-1.5 [&>*:nth-child(3)]:translate-x-3 [&>*:nth-child(4)]:translate-x-1.5"
|
|
151
|
+
)}
|
|
152
|
+
variants={run ? sourcesContainer : undefined}
|
|
153
|
+
initial={run ? "hidden" : false}
|
|
154
|
+
animate={run ? "show" : undefined}
|
|
155
|
+
>
|
|
156
|
+
{sources.map((node, index) => (
|
|
157
|
+
<motion.div key={index} className="flex max-w-full" variants={run ? sourceItem : undefined}>
|
|
158
|
+
{node}
|
|
159
|
+
</motion.div>
|
|
160
|
+
))}
|
|
161
|
+
</motion.div>
|
|
162
|
+
|
|
163
|
+
<div className="self-stretch">
|
|
164
|
+
<ConvergeLines count={sources.length} animate={run} className={linesClassName} />
|
|
165
|
+
</div>
|
|
166
|
+
|
|
167
|
+
<motion.div
|
|
168
|
+
className="flex items-center"
|
|
169
|
+
variants={run ? targetItem : undefined}
|
|
170
|
+
initial={run ? "hidden" : false}
|
|
171
|
+
animate={run ? "show" : undefined}
|
|
172
|
+
>
|
|
173
|
+
{target}
|
|
174
|
+
</motion.div>
|
|
175
|
+
</motion.div>
|
|
176
|
+
);
|
|
177
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from "@storybook/react";
|
|
2
|
+
import { ExplainerSteps } from "./explainer-steps";
|
|
3
|
+
|
|
4
|
+
const meta: Meta<typeof ExplainerSteps> = {
|
|
5
|
+
title: "Menu/ExplainerSteps",
|
|
6
|
+
component: ExplainerSteps,
|
|
7
|
+
tags: ["autodocs"],
|
|
8
|
+
decorators: [
|
|
9
|
+
(Story) => (
|
|
10
|
+
<div className="w-[360px] bg-background p-4">
|
|
11
|
+
<Story />
|
|
12
|
+
</div>
|
|
13
|
+
),
|
|
14
|
+
],
|
|
15
|
+
args: {
|
|
16
|
+
steps: [
|
|
17
|
+
{ text: "We read 32 spellings of papanași and matched them to one dish.", figure: "32", unit: "names" },
|
|
18
|
+
{ text: "Prices compare like for like: every one is a portion of the same dish.", figure: "8–44", unit: "lei" },
|
|
19
|
+
{ text: "Open any dish and see what every other kitchen charges for it.", figure: "45", unit: "kitchens" },
|
|
20
|
+
],
|
|
21
|
+
},
|
|
22
|
+
parameters: {
|
|
23
|
+
docs: {
|
|
24
|
+
description: {
|
|
25
|
+
component:
|
|
26
|
+
"How something works, as numbered rules: a numeral in a ring, one line, and the live figure that proves it with its unit. A thread joins the numerals; the last is filled, because the rules end where the reader is asked to act. A step without a figure shows none.",
|
|
27
|
+
},
|
|
28
|
+
},
|
|
29
|
+
},
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
export default meta;
|
|
33
|
+
type Story = StoryObj<typeof ExplainerSteps>;
|
|
34
|
+
|
|
35
|
+
export const Default: Story = {};
|
|
36
|
+
|
|
37
|
+
/** The third rule has no number, so it shows none. */
|
|
38
|
+
export const UnevenFigures: Story = {
|
|
39
|
+
args: {
|
|
40
|
+
steps: [
|
|
41
|
+
{ text: "The reviews of every place in Cluj-Napoca, read in full.", figure: "791", unit: "places" },
|
|
42
|
+
{ text: "A mention is one review naming the item.", figure: "68", unit: "here" },
|
|
43
|
+
{ text: "The most-named lists are ranked on it, so the top is what the city keeps talking about." },
|
|
44
|
+
],
|
|
45
|
+
},
|
|
46
|
+
};
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import type { ReactNode } from "react";
|
|
2
|
+
import { cn } from "../lib/utils";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* ExplainerSteps — how something works, as numbered rules a reader can scan.
|
|
6
|
+
*
|
|
7
|
+
* Each step is a numeral in a ring, one line of prose, and the live figure
|
|
8
|
+
* that proves the line, right-aligned with its unit. A thread runs from one
|
|
9
|
+
* numeral to the next so the three read as a sequence, and the last numeral
|
|
10
|
+
* is filled: the rules end where the reader is asked to do something.
|
|
11
|
+
*
|
|
12
|
+
* A step without a figure shows none — an invented number in the column is
|
|
13
|
+
* worse than an empty one. But a list where no step has a figure should be
|
|
14
|
+
* prose, not steps.
|
|
15
|
+
*
|
|
16
|
+
* Server-safe. Data in, not children: a server component composes these from
|
|
17
|
+
* translated strings and numbers, and an array is easier to number than a
|
|
18
|
+
* child list is to clone.
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
export type ExplainerStepItem = {
|
|
22
|
+
/** One line. Two at most on a phone; a third is a paragraph and belongs elsewhere. */
|
|
23
|
+
text: ReactNode;
|
|
24
|
+
/** The live figure, pre-formatted. */
|
|
25
|
+
figure?: string;
|
|
26
|
+
/** A unit or caption beside the figure, e.g. "names". */
|
|
27
|
+
unit?: string;
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
export type ExplainerStepsProps = {
|
|
31
|
+
steps: ExplainerStepItem[];
|
|
32
|
+
className?: string;
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
export function ExplainerSteps({ steps, className }: ExplainerStepsProps) {
|
|
36
|
+
return (
|
|
37
|
+
<ol className={cn("flex flex-col", className)}>
|
|
38
|
+
{steps.map((step, index) => {
|
|
39
|
+
const isLast = index === steps.length - 1;
|
|
40
|
+
return (
|
|
41
|
+
<li key={index} className="relative flex gap-3.5 pb-5 last:pb-0">
|
|
42
|
+
{/* The thread, from this numeral to the next. */}
|
|
43
|
+
{!isLast && (
|
|
44
|
+
<span
|
|
45
|
+
aria-hidden
|
|
46
|
+
className="absolute bottom-0 left-[11px] top-7 w-px bg-border"
|
|
47
|
+
/>
|
|
48
|
+
)}
|
|
49
|
+
<span
|
|
50
|
+
aria-hidden
|
|
51
|
+
className={cn(
|
|
52
|
+
"relative flex h-6 w-6 shrink-0 items-center justify-center rounded-full text-[11px] font-bold tabular-nums",
|
|
53
|
+
isLast
|
|
54
|
+
? "bg-primary text-primary-foreground"
|
|
55
|
+
: "border border-border bg-background text-muted-foreground"
|
|
56
|
+
)}
|
|
57
|
+
>
|
|
58
|
+
{index + 1}
|
|
59
|
+
</span>
|
|
60
|
+
<p className="min-w-0 flex-1 pt-0.5 text-[14px] leading-[1.45] text-foreground">{step.text}</p>
|
|
61
|
+
{step.figure && (
|
|
62
|
+
<span className="shrink-0 pt-0.5 text-right text-[15px] font-bold leading-[1.45] tabular-nums text-foreground">
|
|
63
|
+
{step.figure}
|
|
64
|
+
{step.unit && (
|
|
65
|
+
<span className="ml-1 text-[11px] font-normal text-muted-foreground">{step.unit}</span>
|
|
66
|
+
)}
|
|
67
|
+
</span>
|
|
68
|
+
)}
|
|
69
|
+
</li>
|
|
70
|
+
);
|
|
71
|
+
})}
|
|
72
|
+
</ol>
|
|
73
|
+
);
|
|
74
|
+
}
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import type { CSSProperties } from "react";
|
|
2
|
+
import { cn } from "../lib/utils";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* GlassGround — an inked caption rising out of one edge of a photograph.
|
|
6
|
+
*
|
|
7
|
+
* One layer over a `PhotoGround`: an ink gradient running in one direction —
|
|
8
|
+
* absent where the photograph is the subject, solid where the type is.
|
|
9
|
+
* `MenuFamilyBlock` drew this first; this is the same idea with the stops as
|
|
10
|
+
* data, so a card can say "clear for 128px, then ink" without a sixth copy of
|
|
11
|
+
* the gradient string.
|
|
12
|
+
*
|
|
13
|
+
* ## Why there is no blur any more
|
|
14
|
+
*
|
|
15
|
+
* This used to be two masked `backdrop-blur`s under the ink — a progressive
|
|
16
|
+
* frost. Measured on `/menu` in production (2026-09-14): the page carried 73
|
|
17
|
+
* backdrop-filter composited layers, six dossiers' worth of these among them,
|
|
18
|
+
* and the main thread spent 9 ms per scrolled frame re-layerising them
|
|
19
|
+
* (2.7 s over a six-second fling; 56 ms with backdrop-filter disabled). A
|
|
20
|
+
* frost is not worth a page that cannot scroll on a phone, so the caption
|
|
21
|
+
* rests on the ink alone, eased through five stops so the ramp has no crease.
|
|
22
|
+
* See `docs/audits/audit-menu-page-performance-2026-09-14.md` in the app.
|
|
23
|
+
*
|
|
24
|
+
* ## Why the stops are custom properties
|
|
25
|
+
*
|
|
26
|
+
* Tailwind emits only the classes it can read verbatim, so the gradient
|
|
27
|
+
* cannot be assembled from props into a class. The class below is literal and
|
|
28
|
+
* reads `--glass-*` variables; the component writes those inline, having
|
|
29
|
+
* interpolated the quarter stops in JS (`clear` and `full` must share a
|
|
30
|
+
* unit). Same mechanism as `HueChip`'s hue. The ink's colour is the
|
|
31
|
+
* package's one scrim literal, `oklch(0.16 0.012 55)`, never `var(--scrim)`.
|
|
32
|
+
*
|
|
33
|
+
* ## The ramp ends where the type begins
|
|
34
|
+
*
|
|
35
|
+
* `clear` is where the ink starts, `full` where it is solid. Set `full` to
|
|
36
|
+
* the exact seam the content starts at — a ramp that runs past the seam puts
|
|
37
|
+
* the first row of type on half-ink.
|
|
38
|
+
*/
|
|
39
|
+
|
|
40
|
+
export type GlassStops = {
|
|
41
|
+
/** Which way the ink runs — toward the type. */
|
|
42
|
+
direction: "to bottom" | "to right";
|
|
43
|
+
/** Where the ink begins, e.g. `"0px"` or `"24%"`. */
|
|
44
|
+
clear: string;
|
|
45
|
+
/** Where the ink is solid — the seam the content starts at. Same unit as `clear`. */
|
|
46
|
+
full: string;
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
export type GlassGroundProps = {
|
|
50
|
+
stops: GlassStops;
|
|
51
|
+
/** Stops from `lg`. Omit to keep the same ramp at every width. */
|
|
52
|
+
lgStops?: GlassStops;
|
|
53
|
+
/**
|
|
54
|
+
* How dark the ink gets at the far edge, 0–1. `0.96` is a caption the
|
|
55
|
+
* photograph is only a texture under; `0.6` is a photograph the caption is
|
|
56
|
+
* drawn on — the type is lifted, the room stays a room.
|
|
57
|
+
*/
|
|
58
|
+
peak?: number;
|
|
59
|
+
/** Placement only. The ink is a `bg-*` on this same element, so a background here would replace it. */
|
|
60
|
+
className?: string;
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
/* Ease-in through the ramp: the fraction of the way from `clear` to `full`
|
|
64
|
+
at which each stop sits, and the share of the peak it carries. */
|
|
65
|
+
const STOPS = [0, 0.25, 0.5, 0.75, 1] as const;
|
|
66
|
+
const EASE = [0, 0.06, 0.25, 0.56, 1] as const;
|
|
67
|
+
|
|
68
|
+
const INK =
|
|
69
|
+
"bg-[linear-gradient(var(--glass-dir),oklch(0.16_0.012_55_/_0)_var(--glass-s0),oklch(0.16_0.012_55_/_var(--glass-a1))_var(--glass-s1),oklch(0.16_0.012_55_/_var(--glass-a2))_var(--glass-s2),oklch(0.16_0.012_55_/_var(--glass-a3))_var(--glass-s3),oklch(0.16_0.012_55_/_var(--glass-a4))_var(--glass-s4),oklch(0.16_0.012_55_/_var(--glass-a-peak))_100%)] lg:bg-[linear-gradient(var(--glass-lg-dir),oklch(0.16_0.012_55_/_0)_var(--glass-lg-s0),oklch(0.16_0.012_55_/_var(--glass-a1))_var(--glass-lg-s1),oklch(0.16_0.012_55_/_var(--glass-a2))_var(--glass-lg-s2),oklch(0.16_0.012_55_/_var(--glass-a3))_var(--glass-lg-s3),oklch(0.16_0.012_55_/_var(--glass-a4))_var(--glass-lg-s4),oklch(0.16_0.012_55_/_var(--glass-a-peak))_100%)]";
|
|
70
|
+
|
|
71
|
+
/** "72px" → [72, "px"]; "24%" → [24, "%"]. */
|
|
72
|
+
function measure(value: string): [number, string] {
|
|
73
|
+
const match = value.trim().match(/^(-?\d*\.?\d+)\s*([a-z%]*)$/i);
|
|
74
|
+
if (!match) throw new Error(`GlassGround: cannot read stop "${value}"`);
|
|
75
|
+
return [Number(match[1]), match[2] || "px"];
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/** The five ramp positions between `clear` and `full`, as CSS lengths. */
|
|
79
|
+
function positions(clear: string, full: string): string[] {
|
|
80
|
+
const [from, unit] = measure(clear);
|
|
81
|
+
const [to] = measure(full);
|
|
82
|
+
return STOPS.map((fraction) => `${from + (to - from) * fraction}${unit}`);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export function GlassGround({ stops, lgStops = stops, peak = 0.96, className }: GlassGroundProps) {
|
|
86
|
+
const base = positions(stops.clear, stops.full);
|
|
87
|
+
const lg = positions(lgStops.clear, lgStops.full);
|
|
88
|
+
const style = {
|
|
89
|
+
"--glass-dir": stops.direction,
|
|
90
|
+
"--glass-lg-dir": lgStops.direction,
|
|
91
|
+
...Object.fromEntries(base.map((position, index) => [`--glass-s${index}`, position])),
|
|
92
|
+
...Object.fromEntries(lg.map((position, index) => [`--glass-lg-s${index}`, position])),
|
|
93
|
+
...Object.fromEntries(EASE.slice(1).map((share, index) => [`--glass-a${index + 1}`, String(peak * 0.8 * share)])),
|
|
94
|
+
"--glass-a-peak": String(peak),
|
|
95
|
+
} as CSSProperties;
|
|
96
|
+
|
|
97
|
+
return (
|
|
98
|
+
<span
|
|
99
|
+
aria-hidden
|
|
100
|
+
className={cn("pointer-events-none absolute inset-0 block", INK, className)}
|
|
101
|
+
style={style}
|
|
102
|
+
/>
|
|
103
|
+
);
|
|
104
|
+
}
|
|
@@ -30,7 +30,17 @@ export function HubSurface({ flush, className, children }: HubSurfaceProps) {
|
|
|
30
30
|
className={cn(
|
|
31
31
|
"rounded-[18px] border border-border bg-card shadow-none",
|
|
32
32
|
flush ? "overflow-hidden" : "px-3.5 py-4",
|
|
33
|
-
|
|
33
|
+
/*
|
|
34
|
+
* `lg:border-0` is load-bearing and was missing.
|
|
35
|
+
*
|
|
36
|
+
* Above `lg` this surface stops being a card — the background, the
|
|
37
|
+
* radius, the padding and the shadow are all dropped so the section
|
|
38
|
+
* sits on the page. The border was not, so every section on every
|
|
39
|
+
* desktop page kept a 1px rectangle around content that no longer had
|
|
40
|
+
* a card behind it. A caller that genuinely wants an edge at desktop
|
|
41
|
+
* adds `lg:border` in `className`, which lands after this in `cn`.
|
|
42
|
+
*/
|
|
43
|
+
"lg:overflow-visible lg:rounded-none lg:border-0 lg:bg-transparent lg:p-0 lg:shadow-none",
|
|
34
44
|
className
|
|
35
45
|
)}
|
|
36
46
|
>
|