@bearmenu/ui 0.8.8 → 0.8.10
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-2XCZJWTV.js +121 -0
- package/dist/chunk-5RWTOT5Y.js +72 -0
- package/dist/chunk-62ES6N32.js +27 -0
- package/dist/chunk-AKQ57NXY.js +58 -0
- 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-MZL7CGCL.js +109 -0
- package/dist/chunk-Q6QH52LJ.js +38 -0
- package/dist/chunk-SY6RV476.js +48 -0
- package/dist/chunk-TH6W2PWG.js +213 -0
- package/dist/chunk-UDN7UHJW.js +60 -0
- package/dist/{chunk-TT3WFS6S.js → chunk-YOKHBAIH.js} +11 -11
- package/dist/components/alert.d.ts +2 -2
- package/dist/components/badge.d.ts +1 -1
- 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 +110 -0
- package/dist/components/concept-compare.js +238 -0
- 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/date-input.d.ts +1 -1
- package/dist/components/event-card.js +1 -1
- package/dist/components/event-compact-row.d.ts +1 -1
- package/dist/components/event-compact-row.js +1 -1
- package/dist/components/event-detail-panel.d.ts +171 -25
- package/dist/components/event-detail-panel.js +472 -175
- 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/glass-ground.d.ts +66 -0
- package/dist/components/glass-ground.js +3 -0
- package/dist/components/hub-surface.d.ts +27 -0
- package/dist/components/hub-surface.js +30 -0
- package/dist/components/hue-chip.d.ts +70 -0
- package/dist/components/hue-chip.js +3 -0
- package/dist/components/kitchen-dossier.d.ts +88 -0
- package/dist/components/kitchen-dossier.js +187 -0
- 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 +99 -0
- package/dist/components/menu-family-block.js +155 -0
- package/dist/components/menu-family-card.d.ts +65 -0
- package/dist/components/menu-family-card.js +79 -0
- package/dist/components/menu-field.d.ts +57 -0
- package/dist/components/menu-field.js +93 -0
- 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.d.ts +65 -0
- package/dist/components/menu-hub-hero.js +103 -0
- 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-intent-tiles.d.ts +31 -0
- package/dist/components/menu-intent-tiles.js +50 -0
- package/dist/components/menu-item.d.ts +127 -0
- package/dist/components/menu-item.js +5 -0
- 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 +90 -0
- package/dist/components/menu-mocks.js +504 -0
- package/dist/components/menu-types.d.ts +421 -0
- package/dist/components/menu-types.js +1 -0
- package/dist/components/motion-icon-controls.js +2 -2
- package/dist/components/motion-icon-set.js +21 -9
- package/dist/components/motion-icon.d.ts +22 -1
- package/dist/components/motion-icon.js +1 -1
- package/dist/components/open-now-list.d.ts +40 -0
- package/dist/components/open-now-list.js +69 -0
- 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 +2 -2
- package/dist/components/place-types.d.ts +112 -1
- 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/signature-chip.d.ts +37 -0
- package/dist/components/signature-chip.js +4 -0
- package/dist/components/snap-rail.d.ts +48 -5
- package/dist/components/snap-rail.js +3 -51
- package/dist/components/stat-banner.d.ts +47 -0
- package/dist/components/stat-banner.js +61 -0
- package/dist/components/story.d.ts +34 -0
- package/dist/components/story.js +3 -0
- package/dist/components/tag.d.ts +2 -2
- package/dist/components/text.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 +38 -0
- package/dist/components/venue-grade-dimensions.js +3 -0
- package/dist/components/venue-quotes.d.ts +32 -0
- package/dist/components/venue-quotes.js +3 -0
- package/dist/components/venue-wall.d.ts +41 -0
- package/dist/components/venue-wall.js +118 -0
- package/dist/components/weekday-theme-banner.d.ts +70 -0
- package/dist/components/weekday-theme-banner.js +127 -0
- package/dist/components/weekend-panel.js +2 -2
- package/package.json +1 -1
- package/src/components/colour-dictionary.stories.tsx +227 -0
- package/src/components/concept-compare-rail.tsx +131 -0
- package/src/components/concept-compare.stories.tsx +172 -0
- package/src/components/concept-compare.tsx +443 -0
- 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/event-compact-row.tsx +1 -1
- package/src/components/event-detail-panel.stories.tsx +205 -97
- package/src/components/event-detail-panel.test.tsx +181 -0
- package/src/components/event-detail-panel.tsx +703 -185
- package/src/components/explainer-steps.stories.tsx +46 -0
- package/src/components/explainer-steps.tsx +74 -0
- package/src/components/glass-ground.tsx +129 -0
- package/src/components/hub-surface.stories.tsx +68 -0
- package/src/components/hub-surface.tsx +50 -0
- 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 +322 -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 +314 -0
- package/src/components/menu-family-card.stories.tsx +120 -0
- package/src/components/menu-family-card.tsx +191 -0
- package/src/components/menu-field.stories.tsx +74 -0
- package/src/components/menu-field.tsx +199 -0
- 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.stories.tsx +92 -0
- package/src/components/menu-hub-hero.tsx +187 -0
- 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-intent-tiles.stories.tsx +54 -0
- package/src/components/menu-intent-tiles.tsx +76 -0
- package/src/components/menu-item.tsx +416 -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 +592 -0
- package/src/components/menu-types.ts +442 -0
- package/src/components/motion-icon-controls.tsx +11 -11
- package/src/components/motion-icon-set.tsx +36 -8
- package/src/components/motion-icon.test.tsx +23 -1
- package/src/components/motion-icon.tsx +20 -0
- package/src/components/open-now-list.stories.tsx +76 -0
- package/src/components/open-now-list.tsx +127 -0
- package/src/components/photo-ground.tsx +147 -0
- package/src/components/place-types.ts +112 -0
- package/src/components/qa-block.stories.tsx +43 -0
- package/src/components/qa-block.tsx +60 -0
- package/src/components/signature-chip.tsx +77 -0
- package/src/components/snap-rail.tsx +133 -9
- package/src/components/stat-banner.stories.tsx +100 -0
- package/src/components/stat-banner.tsx +117 -0
- 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.stories.tsx +83 -0
- package/src/components/venue-grade-dimensions.tsx +91 -0
- package/src/components/venue-quotes.stories.tsx +59 -0
- package/src/components/venue-quotes.tsx +64 -0
- package/src/components/venue-wall.stories.tsx +78 -0
- package/src/components/venue-wall.tsx +185 -0
- package/src/components/weekday-theme-banner.stories.tsx +113 -0
- package/src/components/weekday-theme-banner.tsx +231 -0
- package/dist/{chunk-XWMRABZW.js → chunk-HSIYYA2E.js} +1 -1
|
@@ -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,129 @@
|
|
|
1
|
+
import type { CSSProperties } from "react";
|
|
2
|
+
import { cn } from "../lib/utils";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* GlassGround — a frosted, inked caption rising out of one edge of a
|
|
6
|
+
* photograph.
|
|
7
|
+
*
|
|
8
|
+
* Layers over a `PhotoGround`: two masked `backdrop-blur`s (the frost) and
|
|
9
|
+
* an ink gradient (the contrast), running in one direction — absent where the
|
|
10
|
+
* photograph is the subject, solid where the type is. `MenuFamilyBlock` drew
|
|
11
|
+
* this first; this is the same idea with the stops as data, so a card can say
|
|
12
|
+
* "clear for 128px, then glass" without a sixth copy of the gradient strings.
|
|
13
|
+
*
|
|
14
|
+
* ## Why two blurs, and why the ramps are eased
|
|
15
|
+
*
|
|
16
|
+
* One blur under a linear mask has an edge: the eye reads the line where
|
|
17
|
+
* sharp becomes soft, however gently the mask fades it in. Two blurs — a
|
|
18
|
+
* light one that starts early and a heavier one that starts late — make the
|
|
19
|
+
* softening itself a gradient (the "progressive blur" of a phone's lock
|
|
20
|
+
* screen), and every ramp here is an ease-in through five stops rather than
|
|
21
|
+
* a straight line, because a straight ramp has a visible crease at each end
|
|
22
|
+
* where its slope changes.
|
|
23
|
+
*
|
|
24
|
+
* ## Why the stops are custom properties
|
|
25
|
+
*
|
|
26
|
+
* Tailwind emits only the classes it can read verbatim, so the gradients
|
|
27
|
+
* cannot be assembled from props into a class. The classes below are literal
|
|
28
|
+
* and read `--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 frost starts, `full` where it and the ink are solid.
|
|
36
|
+
* Set `full` to the exact seam the content starts at — a ramp that runs past
|
|
37
|
+
* the seam puts the first row of type on half-frost.
|
|
38
|
+
*/
|
|
39
|
+
|
|
40
|
+
export type GlassStops = {
|
|
41
|
+
/** Which way the ink runs — toward the type. */
|
|
42
|
+
direction: "to bottom" | "to right";
|
|
43
|
+
/** Where the frost begins, e.g. `"0px"` or `"24%"`. */
|
|
44
|
+
clear: string;
|
|
45
|
+
/** Where frost and ink are 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.4` is a photograph the caption is
|
|
56
|
+
* drawn on — the type is lifted, the room stays a room.
|
|
57
|
+
*/
|
|
58
|
+
peak?: number;
|
|
59
|
+
/**
|
|
60
|
+
* How much the frost softens the photograph where it is solid. `xl` turns
|
|
61
|
+
* a room into colour; `sm` keeps it a room and only takes the edge off the
|
|
62
|
+
* detail behind the type.
|
|
63
|
+
*/
|
|
64
|
+
blur?: "sm" | "md" | "lg" | "xl";
|
|
65
|
+
className?: string;
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
/* Ease-in through the ramp: the fraction of the way from `clear` to `full`
|
|
69
|
+
at which each stop sits, and the share of the peak it carries. */
|
|
70
|
+
const STOPS = [0, 0.25, 0.5, 0.75, 1] as const;
|
|
71
|
+
const EASE = [0, 0.06, 0.25, 0.56, 1] as const;
|
|
72
|
+
|
|
73
|
+
/** The light blur starts at once and is solid halfway; the heavy one starts halfway. */
|
|
74
|
+
const FROST_SOFT =
|
|
75
|
+
"[mask-image:linear-gradient(var(--glass-dir),transparent_var(--glass-s0),rgba(0,0,0,0.25)_var(--glass-s1),black_var(--glass-s2))] lg:[mask-image:linear-gradient(var(--glass-lg-dir),transparent_var(--glass-lg-s0),rgba(0,0,0,0.25)_var(--glass-lg-s1),black_var(--glass-lg-s2))]";
|
|
76
|
+
const FROST_HEAVY =
|
|
77
|
+
"[mask-image:linear-gradient(var(--glass-dir),transparent_var(--glass-s1),rgba(0,0,0,0.25)_var(--glass-s2),rgba(0,0,0,0.6)_var(--glass-s3),black_var(--glass-s4))] lg:[mask-image:linear-gradient(var(--glass-lg-dir),transparent_var(--glass-lg-s1),rgba(0,0,0,0.25)_var(--glass-lg-s2),rgba(0,0,0,0.6)_var(--glass-lg-s3),black_var(--glass-lg-s4))]";
|
|
78
|
+
|
|
79
|
+
const INK =
|
|
80
|
+
"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%)]";
|
|
81
|
+
|
|
82
|
+
/** Literal, for the scanner: the light and the heavy blur per strength. */
|
|
83
|
+
const BLUR = {
|
|
84
|
+
sm: ["backdrop-blur-[2px]", "backdrop-blur-sm"],
|
|
85
|
+
md: ["backdrop-blur-sm", "backdrop-blur-md"],
|
|
86
|
+
lg: ["backdrop-blur-md", "backdrop-blur-lg"],
|
|
87
|
+
xl: ["backdrop-blur-lg", "backdrop-blur-xl"],
|
|
88
|
+
} as const;
|
|
89
|
+
|
|
90
|
+
/** "72px" → [72, "px"]; "24%" → [24, "%"]. */
|
|
91
|
+
function measure(value: string): [number, string] {
|
|
92
|
+
const match = value.trim().match(/^(-?\d*\.?\d+)\s*([a-z%]*)$/i);
|
|
93
|
+
if (!match) throw new Error(`GlassGround: cannot read stop "${value}"`);
|
|
94
|
+
return [Number(match[1]), match[2] || "px"];
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/** The five ramp positions between `clear` and `full`, as CSS lengths. */
|
|
98
|
+
function positions(clear: string, full: string): string[] {
|
|
99
|
+
const [from, unit] = measure(clear);
|
|
100
|
+
const [to] = measure(full);
|
|
101
|
+
return STOPS.map((fraction) => `${from + (to - from) * fraction}${unit}`);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
export function GlassGround({
|
|
105
|
+
stops,
|
|
106
|
+
lgStops = stops,
|
|
107
|
+
peak = 0.96,
|
|
108
|
+
blur = "xl",
|
|
109
|
+
className,
|
|
110
|
+
}: GlassGroundProps) {
|
|
111
|
+
const base = positions(stops.clear, stops.full);
|
|
112
|
+
const lg = positions(lgStops.clear, lgStops.full);
|
|
113
|
+
const style = {
|
|
114
|
+
"--glass-dir": stops.direction,
|
|
115
|
+
"--glass-lg-dir": lgStops.direction,
|
|
116
|
+
...Object.fromEntries(base.map((position, index) => [`--glass-s${index}`, position])),
|
|
117
|
+
...Object.fromEntries(lg.map((position, index) => [`--glass-lg-s${index}`, position])),
|
|
118
|
+
...Object.fromEntries(EASE.slice(1).map((share, index) => [`--glass-a${index + 1}`, String(peak * 0.8 * share)])),
|
|
119
|
+
"--glass-a-peak": String(peak),
|
|
120
|
+
} as CSSProperties;
|
|
121
|
+
|
|
122
|
+
return (
|
|
123
|
+
<span aria-hidden className={cn("pointer-events-none absolute inset-0 block", className)} style={style}>
|
|
124
|
+
<span className={cn("absolute inset-0", BLUR[blur][0], FROST_SOFT)} />
|
|
125
|
+
<span className={cn("absolute inset-0", BLUR[blur][1], FROST_HEAVY)} />
|
|
126
|
+
<span className={cn("absolute inset-0", INK)} />
|
|
127
|
+
</span>
|
|
128
|
+
);
|
|
129
|
+
}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from "@storybook/react";
|
|
2
|
+
import { HubSurface } from "./hub-surface";
|
|
3
|
+
import { SectionHeader } from "./section-header";
|
|
4
|
+
|
|
5
|
+
const meta: Meta<typeof HubSurface> = {
|
|
6
|
+
title: "Menu/HubSurface",
|
|
7
|
+
component: HubSurface,
|
|
8
|
+
tags: ["autodocs"],
|
|
9
|
+
decorators: [
|
|
10
|
+
(Story) => (
|
|
11
|
+
<div className="w-[390px] bg-[var(--surface-2,oklch(0.97_0.004_60))] p-3">
|
|
12
|
+
<Story />
|
|
13
|
+
</div>
|
|
14
|
+
),
|
|
15
|
+
],
|
|
16
|
+
parameters: {
|
|
17
|
+
docs: {
|
|
18
|
+
description: {
|
|
19
|
+
component:
|
|
20
|
+
"The surface a hub section sits on below `lg`. On a phone a hub is an app screen, not a page: each section is a white card on the tinted ground, 18px radius, one shadow, no border. From `lg` up the card dissolves entirely and sections sit flat the way the desktop design draws them — one element, two looks, identical markup inside. Hairlines live inside a card, never across the page.",
|
|
21
|
+
},
|
|
22
|
+
},
|
|
23
|
+
},
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
export default meta;
|
|
27
|
+
type Story = StoryObj<typeof HubSurface>;
|
|
28
|
+
|
|
29
|
+
export const Default: Story = {
|
|
30
|
+
render: () => (
|
|
31
|
+
<HubSurface>
|
|
32
|
+
<SectionHeader title="Open right now" />
|
|
33
|
+
<p className="text-sm text-muted-foreground">
|
|
34
|
+
Section content, inset by the surface.
|
|
35
|
+
</p>
|
|
36
|
+
</HubSurface>
|
|
37
|
+
),
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
/** `flush` drops the inset so a rail or a photograph can bleed to the edge. */
|
|
41
|
+
export const Flush: Story = {
|
|
42
|
+
render: () => (
|
|
43
|
+
<HubSurface flush>
|
|
44
|
+
<div className="px-3.5 pt-4">
|
|
45
|
+
<SectionHeader title="Browse by cuisine" />
|
|
46
|
+
</div>
|
|
47
|
+
<div className="flex gap-3 overflow-x-auto px-3.5 pb-4">
|
|
48
|
+
{[0, 1, 2].map((card) => (
|
|
49
|
+
<div key={card} className="h-24 w-40 shrink-0 rounded-xl bg-muted" />
|
|
50
|
+
))}
|
|
51
|
+
</div>
|
|
52
|
+
</HubSurface>
|
|
53
|
+
),
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
/** Stacked, at the 12px rhythm the hub uses between sections. */
|
|
57
|
+
export const Stacked: Story = {
|
|
58
|
+
render: () => (
|
|
59
|
+
<div className="flex flex-col gap-3">
|
|
60
|
+
{["Open right now", "One kitchen worth knowing", "Most named"].map((title) => (
|
|
61
|
+
<HubSurface key={title}>
|
|
62
|
+
<SectionHeader title={title} />
|
|
63
|
+
<div className="h-12 rounded-lg bg-muted" />
|
|
64
|
+
</HubSurface>
|
|
65
|
+
))}
|
|
66
|
+
</div>
|
|
67
|
+
),
|
|
68
|
+
};
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { cn } from "../lib/utils";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* HubSurface — the surface a hub section sits on BELOW `lg`.
|
|
5
|
+
*
|
|
6
|
+
* On a phone a hub is an app screen, not a page: each section is a white card
|
|
7
|
+
* on the tinted ground, 18px radius, one card shadow and no border, with 14px
|
|
8
|
+
* of inset. Hairlines live inside a card, never across the page. From `lg` up
|
|
9
|
+
* the card dissolves — no fill, no radius, no inset — and the sections sit flat
|
|
10
|
+
* the way the desktop design draws them. One element, two looks; the section
|
|
11
|
+
* markup inside is identical at every width.
|
|
12
|
+
*
|
|
13
|
+
* `flush` drops the inset so a section whose content bleeds to the card edge —
|
|
14
|
+
* a snap rail, a full-width photograph — can pad its own header and let the
|
|
15
|
+
* rest run.
|
|
16
|
+
*
|
|
17
|
+
* Generalised out of the events hub's `EventsHubCard`, which had the same job
|
|
18
|
+
* and lived in the app. Nothing here is menu- or event-shaped.
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
export type HubSurfaceProps = {
|
|
22
|
+
flush?: boolean;
|
|
23
|
+
className?: string;
|
|
24
|
+
children: React.ReactNode;
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
export function HubSurface({ flush, className, children }: HubSurfaceProps) {
|
|
28
|
+
return (
|
|
29
|
+
<section
|
|
30
|
+
className={cn(
|
|
31
|
+
"rounded-[18px] border border-border bg-card shadow-none",
|
|
32
|
+
flush ? "overflow-hidden" : "px-3.5 py-4",
|
|
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",
|
|
44
|
+
className
|
|
45
|
+
)}
|
|
46
|
+
>
|
|
47
|
+
{children}
|
|
48
|
+
</section>
|
|
49
|
+
);
|
|
50
|
+
}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from "@storybook/react";
|
|
2
|
+
import { Wallet } from "lucide-react";
|
|
3
|
+
import { HueChip } from "./hue-chip";
|
|
4
|
+
|
|
5
|
+
const meta: Meta<typeof HueChip> = {
|
|
6
|
+
title: "Menu/HueChip",
|
|
7
|
+
component: HueChip,
|
|
8
|
+
tags: ["autodocs"],
|
|
9
|
+
args: { children: "Sweet Pastry", hue: "oklch(0.62 0.14 82)" },
|
|
10
|
+
parameters: {
|
|
11
|
+
docs: {
|
|
12
|
+
description: {
|
|
13
|
+
component:
|
|
14
|
+
"A pill wearing a category's colour as a pale wash. It exists because the wash has one non-obvious rule — the mix must happen `in oklab`, never `in oklch` — and that rule has to live in one place. oklch is polar, so mixing a hue into an achromatic `--card` interpolates the hue ANGLE toward 0: honey 82° arrives at 16° (pink), a bar's 205° blue at 41° (orange). The hue is runtime data from ~40 cuisines, so it is an inline custom property rather than a Tailwind class — Tailwind can only emit what it sees at build time.",
|
|
15
|
+
},
|
|
16
|
+
},
|
|
17
|
+
},
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
export default meta;
|
|
21
|
+
type Story = StoryObj<typeof HueChip>;
|
|
22
|
+
|
|
23
|
+
export const Default: Story = {};
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* The bug this component was extracted to prevent. Every hue survives the mix
|
|
27
|
+
* with its own identity — the cool families especially, which under `in oklch`
|
|
28
|
+
* came out orange and mustard.
|
|
29
|
+
*/
|
|
30
|
+
export const EveryFamily: Story = {
|
|
31
|
+
render: () => (
|
|
32
|
+
<div className="flex flex-wrap gap-1.5">
|
|
33
|
+
{[
|
|
34
|
+
["Breakfast & Brunch", "oklch(0.62 0.14 82)"],
|
|
35
|
+
["Romanian", "oklch(0.48 0.16 25)"],
|
|
36
|
+
["Italian", "oklch(0.56 0.15 38)"],
|
|
37
|
+
["Vegetarian", "oklch(0.52 0.14 148)"],
|
|
38
|
+
["Bars", "oklch(0.47 0.10 205)"],
|
|
39
|
+
["Clubs", "oklch(0.42 0.11 300)"],
|
|
40
|
+
].map(([label, hue]) => (
|
|
41
|
+
<HueChip key={label} hue={hue}>
|
|
42
|
+
{label}
|
|
43
|
+
</HueChip>
|
|
44
|
+
))}
|
|
45
|
+
</div>
|
|
46
|
+
),
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
/** No hue — `--muted`, the right neutral for a facet with no family behind it. */
|
|
50
|
+
export const Untinted: Story = {
|
|
51
|
+
args: { hue: undefined },
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
/** A price band: an icon, a bare figure, and an accessible name for it. */
|
|
55
|
+
export const WithIcon: Story = {
|
|
56
|
+
args: {
|
|
57
|
+
hue: undefined,
|
|
58
|
+
className: "font-bold tabular-nums",
|
|
59
|
+
"aria-label": "kitchen price band: 22–86 lei",
|
|
60
|
+
icon: <Wallet className="h-3.5 w-3.5 shrink-0 text-muted-foreground" aria-hidden />,
|
|
61
|
+
children: "22–86 lei",
|
|
62
|
+
},
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
/** In a list, which is where most of these live. */
|
|
66
|
+
export const InAList: Story = {
|
|
67
|
+
render: () => (
|
|
68
|
+
<ul className="flex flex-wrap items-center gap-1.5">
|
|
69
|
+
{["Sweet Pastry", "Salad", "Cake"].map((label) => (
|
|
70
|
+
<HueChip key={label} as="li" hue="oklch(0.62 0.14 82)">
|
|
71
|
+
{label}
|
|
72
|
+
</HueChip>
|
|
73
|
+
))}
|
|
74
|
+
</ul>
|
|
75
|
+
),
|
|
76
|
+
};
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { render, screen } from "@testing-library/react";
|
|
2
|
+
import { describe, expect, it } from "vitest";
|
|
3
|
+
import { HueChip, hueTint } from "./hue-chip";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* The whole reason this component exists is one character of CSS, so that is
|
|
7
|
+
* what is pinned. `color-mix(in oklch, …)` interpolates the hue ANGLE, and the
|
|
8
|
+
* paper it mixes into is achromatic (`--card` is `oklch(1 0 0)`, hue 0), so
|
|
9
|
+
* every family colour was dragged a proportional fraction of the way to red:
|
|
10
|
+
* honey 82° arrived at 16°, pink. It is invisible in a diff and obvious on
|
|
11
|
+
* screen, which is exactly the kind of thing a test should hold.
|
|
12
|
+
*/
|
|
13
|
+
describe("hueTint", () => {
|
|
14
|
+
it("mixes in the rectangular space, never the polar one", () => {
|
|
15
|
+
const tint = hueTint("oklch(0.62 0.14 82)");
|
|
16
|
+
expect(tint).toContain("in oklab");
|
|
17
|
+
expect(tint).not.toContain("in oklch,");
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
it("mixes into the surface the chip sits on", () => {
|
|
21
|
+
expect(hueTint("oklch(0.62 0.14 82)")).toContain("var(--card)");
|
|
22
|
+
expect(hueTint("oklch(0.62 0.14 82)", "chip", "var(--background)")).toContain(
|
|
23
|
+
"var(--background)"
|
|
24
|
+
);
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
it("gives a subtle chip less of the hue than a plain one", () => {
|
|
28
|
+
expect(hueTint("oklch(0.62 0.14 82)", "chip")).toContain("20%");
|
|
29
|
+
expect(hueTint("oklch(0.62 0.14 82)", "subtle")).toContain("12%");
|
|
30
|
+
});
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
describe("HueChip", () => {
|
|
34
|
+
it("wears the tint when it has a hue", () => {
|
|
35
|
+
render(<HueChip hue="oklch(0.62 0.14 82)">Sweet Pastry</HueChip>);
|
|
36
|
+
const chip = screen.getByText("Sweet Pastry");
|
|
37
|
+
expect(chip.getAttribute("style")).toContain("oklab");
|
|
38
|
+
expect(chip.className).not.toContain("bg-muted");
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
it("falls back to the neutral rather than to a colour", () => {
|
|
42
|
+
// A facet with no family behind it must not borrow another family's colour.
|
|
43
|
+
render(<HueChip>Fast Food</HueChip>);
|
|
44
|
+
const chip = screen.getByText("Fast Food");
|
|
45
|
+
expect(chip.className).toContain("bg-muted");
|
|
46
|
+
expect(chip.getAttribute("style")).toBeNull();
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
it("renders as a list item when asked, for the lists it lives in", () => {
|
|
50
|
+
render(
|
|
51
|
+
<ul>
|
|
52
|
+
<HueChip as="li">Salad</HueChip>
|
|
53
|
+
</ul>
|
|
54
|
+
);
|
|
55
|
+
expect(screen.getByText("Salad").tagName).toBe("LI");
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
it("takes an accessible name for a chip whose text is a bare figure", () => {
|
|
59
|
+
render(<HueChip aria-label="kitchen price band: 22–86 lei">22–86 lei</HueChip>);
|
|
60
|
+
expect(screen.getByLabelText("kitchen price band: 22–86 lei")).toBeInTheDocument();
|
|
61
|
+
});
|
|
62
|
+
});
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import { cn } from "../lib/utils";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* HueChip — a pill that wears a category's colour as a pale wash.
|
|
5
|
+
*
|
|
6
|
+
* Menu surfaces label things with a family's hue: a cuisine's dish groups, a
|
|
7
|
+
* going-out family's tags, a price band. Every one of them was an inline
|
|
8
|
+
* `style` with a hand-written `color-mix` next to a hand-written pill class
|
|
9
|
+
* list, which is how the two below drifted apart and how the mix space bug
|
|
10
|
+
* survived as long as it did — it had to be found and fixed in each copy.
|
|
11
|
+
*
|
|
12
|
+
* ## The mix space is the whole reason this is a component
|
|
13
|
+
*
|
|
14
|
+
* **`in oklab`, and never `in oklch`.** They describe the same colours, but
|
|
15
|
+
* oklch is POLAR, so `color-mix(in oklch, …)` interpolates the hue ANGLE — and
|
|
16
|
+
* the paper being mixed into is achromatic (`--card` is `oklch(1 0 0)`, hue 0).
|
|
17
|
+
* Mixing 20% of a family colour into that lands at a fifth of the family's hue:
|
|
18
|
+
* honey 82° came out at 16°, which is pink, so a Breakfast & Brunch card wore
|
|
19
|
+
* pink chips under an amber mosaic. Cool families were worse — a bar's 205°
|
|
20
|
+
* blue mixes to 41°, orange; a club's 300° violet to 60°, mustard.
|
|
21
|
+
*
|
|
22
|
+
* oklab is rectangular: mixing toward an achromatic base scales chroma along
|
|
23
|
+
* the SAME hue instead of rotating it, which is what "a pale version of this
|
|
24
|
+
* colour" has to mean.
|
|
25
|
+
*
|
|
26
|
+
* ## Why an inline style and not a Tailwind class
|
|
27
|
+
*
|
|
28
|
+
* The hue is DATA — one of ~40 cuisine values resolved by the app at request
|
|
29
|
+
* time — not one of a fixed set of variants. Tailwind can only emit classes it
|
|
30
|
+
* can see at build time, so a class-based version would mean either a
|
|
31
|
+
* safelisted colour per cuisine (a palette that must be edited to add a
|
|
32
|
+
* cuisine) or an arbitrary-value class built from a runtime string, which
|
|
33
|
+
* Tailwind cannot compile. A CSS custom property set inline is the honest
|
|
34
|
+
* mechanism; this component is the one place that writes it.
|
|
35
|
+
*/
|
|
36
|
+
|
|
37
|
+
/** Mix strengths, so callers say what a chip IS rather than picking a number. */
|
|
38
|
+
const STRENGTH = {
|
|
39
|
+
/** A labelled facet: readable at 11.5px, still clearly the family's colour. */
|
|
40
|
+
chip: 20,
|
|
41
|
+
/** A quieter wash, for a chip that sits beside a stronger one. */
|
|
42
|
+
subtle: 12,
|
|
43
|
+
} as const;
|
|
44
|
+
|
|
45
|
+
export type HueChipTone = keyof typeof STRENGTH;
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* The tint itself, exported for the surfaces that need the colour rather than
|
|
49
|
+
* the pill — a section ground, a hover state. Same rule: `in oklab`.
|
|
50
|
+
*/
|
|
51
|
+
export function hueTint(hue: string, tone: HueChipTone = "chip", base = "var(--card)"): string {
|
|
52
|
+
return `color-mix(in oklab, ${hue} ${STRENGTH[tone]}%, ${base})`;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export type HueChipProps = {
|
|
56
|
+
/**
|
|
57
|
+
* A finished CSS colour — the DS owns no category table, so the app resolves
|
|
58
|
+
* it. Without one the chip falls back to `--muted`, which is the right
|
|
59
|
+
* neutral for a facet that has no family behind it.
|
|
60
|
+
*/
|
|
61
|
+
hue?: string;
|
|
62
|
+
tone?: HueChipTone;
|
|
63
|
+
/** The paper the wash is mixed into. Must be the surface the chip sits on. */
|
|
64
|
+
base?: string;
|
|
65
|
+
/** Drawn before the label, e.g. a wallet on a price band. */
|
|
66
|
+
icon?: React.ReactNode;
|
|
67
|
+
/** `li` inside a list, which is where most of these live. */
|
|
68
|
+
as?: "span" | "li" | "div";
|
|
69
|
+
children: React.ReactNode;
|
|
70
|
+
className?: string;
|
|
71
|
+
/** For a chip whose visible text is a bare figure, e.g. "22–86 lei". */
|
|
72
|
+
"aria-label"?: string;
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
export function HueChip({
|
|
76
|
+
hue,
|
|
77
|
+
tone = "chip",
|
|
78
|
+
base = "var(--card)",
|
|
79
|
+
icon,
|
|
80
|
+
as: Tag = "span",
|
|
81
|
+
children,
|
|
82
|
+
className,
|
|
83
|
+
"aria-label": ariaLabel,
|
|
84
|
+
}: HueChipProps) {
|
|
85
|
+
return (
|
|
86
|
+
<Tag
|
|
87
|
+
aria-label={ariaLabel}
|
|
88
|
+
className={cn(
|
|
89
|
+
"inline-flex h-7 items-center gap-1.5 rounded-full px-2.5 text-[11.5px] text-foreground",
|
|
90
|
+
!hue && "bg-muted",
|
|
91
|
+
className
|
|
92
|
+
)}
|
|
93
|
+
style={hue ? { backgroundColor: hueTint(hue, tone, base) } : undefined}
|
|
94
|
+
>
|
|
95
|
+
{icon}
|
|
96
|
+
{children}
|
|
97
|
+
</Tag>
|
|
98
|
+
);
|
|
99
|
+
}
|