@bearmenu/ui 0.8.32 → 0.8.34
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-5HHRVJOT.js → chunk-5SGKGFMN.js} +1 -1
- package/dist/{chunk-GESB7GCF.js → chunk-6W25S5CF.js} +1 -1
- package/dist/chunk-LY67SOFC.js +142 -0
- package/dist/{chunk-ZU4NAYN7.js → chunk-NIAL4ZU2.js} +13 -1
- package/dist/{chunk-JWTN7MNK.js → chunk-TVEIUTBQ.js} +2 -2
- package/dist/{chunk-ZJMCU7MI.js → chunk-VCRXL7H5.js} +1 -1
- package/dist/chunk-XLHMF6F5.js +127 -0
- package/dist/{chunk-VSRGEB36.js → chunk-XNBDACL7.js} +1 -1
- package/dist/components/action-bar.d.ts +67 -0
- package/dist/components/action-bar.js +43 -0
- package/dist/components/activity-row.d.ts +38 -0
- package/dist/components/activity-row.js +43 -0
- package/dist/components/alert-dialog.js +1 -1
- package/dist/components/alert.d.ts +2 -2
- package/dist/components/avatar.d.ts +4 -1
- package/dist/components/avatar.js +39 -5
- package/dist/components/badge.d.ts +1 -1
- package/dist/components/badge.js +1 -1
- package/dist/components/booking-row.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/carousel.js +1 -1
- package/dist/components/coach-marks.js +1 -1
- package/dist/components/combobox.js +3 -3
- package/dist/components/command.d.ts +15 -15
- package/dist/components/command.js +2 -2
- package/dist/components/concept-convergence.js +2 -2
- package/dist/components/currency-input.js +4 -142
- package/dist/components/dialog.d.ts +6 -0
- package/dist/components/dialog.js +1 -1
- package/dist/components/drawer.d.ts +6 -0
- package/dist/components/drawer.js +3 -127
- package/dist/components/empty-state.d.ts +2 -2
- package/dist/components/empty-state.js +1 -1
- package/dist/components/event-detail-panel.js +1 -1
- package/dist/components/event-list-row-wide.js +1 -1
- package/dist/components/filter-category-row.js +1 -1
- package/dist/components/hold-countdown.js +1 -1
- package/dist/components/link-button.js +1 -1
- package/dist/components/menu-family-block.js +1 -1
- package/dist/components/menu-index-list.js +1 -1
- package/dist/components/menu-item.js +3 -3
- package/dist/components/menu-pick-row.d.ts +47 -0
- package/dist/components/menu-pick-row.js +49 -0
- package/dist/components/multi-select-combobox.js +4 -4
- package/dist/components/pagination.js +1 -1
- package/dist/components/place-card.js +1 -1
- package/dist/components/place-details-mobile.js +1 -1
- package/dist/components/qr-panel.d.ts +31 -0
- package/dist/components/qr-panel.js +33 -0
- package/dist/components/reconcile-bar.d.ts +47 -0
- package/dist/components/reconcile-bar.js +60 -0
- package/dist/components/responsive-overlay.d.ts +59 -0
- package/dist/components/responsive-overlay.js +132 -0
- package/dist/components/searchable-select.js +3 -3
- package/dist/components/signature-chip.js +2 -2
- package/dist/components/sliding-panels.js +1 -1
- package/dist/components/spinner.d.ts +2 -2
- package/dist/components/split-share-row.d.ts +77 -0
- package/dist/components/split-share-row.js +101 -0
- package/dist/components/table-ledger-card.d.ts +80 -0
- package/dist/components/table-ledger-card.js +84 -0
- package/dist/components/table-trigger-band.d.ts +56 -0
- package/dist/components/table-trigger-band.js +43 -0
- package/dist/components/tag.d.ts +2 -2
- package/dist/components/text.d.ts +2 -2
- package/dist/components/toggle.d.ts +1 -1
- package/dist/components/top-ranking-entry.js +3 -3
- package/dist/components/venue-events-block.js +1 -1
- package/package.json +10 -1
- package/src/components/action-bar.stories.tsx +78 -0
- package/src/components/action-bar.tsx +80 -0
- package/src/components/activity-row.stories.tsx +78 -0
- package/src/components/activity-row.tsx +65 -0
- package/src/components/avatar.stories.tsx +55 -0
- package/src/components/avatar.tsx +63 -19
- package/src/components/badge.stories.tsx +41 -0
- package/src/components/badge.tsx +13 -0
- package/src/components/button.tsx +1 -1
- package/src/components/dialog.tsx +8 -2
- package/src/components/drawer.tsx +8 -2
- package/src/components/hold-countdown.tsx +1 -1
- package/src/components/menu-pick-row.stories.tsx +71 -0
- package/src/components/menu-pick-row.tsx +76 -0
- package/src/components/qr-panel.stories.tsx +39 -0
- package/src/components/qr-panel.tsx +52 -0
- package/src/components/reconcile-bar.stories.tsx +57 -0
- package/src/components/reconcile-bar.tsx +80 -0
- package/src/components/responsive-overlay.stories.tsx +91 -0
- package/src/components/responsive-overlay.test.tsx +162 -0
- package/src/components/responsive-overlay.tsx +232 -0
- package/src/components/shared-table-primitives.test.tsx +482 -0
- package/src/components/split-share-row.stories.tsx +114 -0
- package/src/components/split-share-row.tsx +150 -0
- package/src/components/table-ledger-card.stories.tsx +120 -0
- package/src/components/table-ledger-card.tsx +138 -0
- package/src/components/table-trigger-band.stories.tsx +76 -0
- package/src/components/table-trigger-band.tsx +75 -0
- package/src/globals.css +62 -0
- package/src/test/setup.ts +11 -0
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from "@storybook/react";
|
|
2
|
+
import { Users } from "lucide-react";
|
|
3
|
+
import { ActivityList, ActivityRow } from "./activity-row";
|
|
4
|
+
import { Avatar, AvatarFallback } from "./avatar";
|
|
5
|
+
|
|
6
|
+
const meta: Meta<typeof ActivityRow> = {
|
|
7
|
+
title: "Shared table/ActivityRow",
|
|
8
|
+
component: ActivityRow,
|
|
9
|
+
tags: ["autodocs"],
|
|
10
|
+
parameters: {
|
|
11
|
+
docs: {
|
|
12
|
+
description: {
|
|
13
|
+
component:
|
|
14
|
+
"What happened at the table, newest first. The leading slot is the actor's avatar when an event has one and a quiet glyph disc when it does not — a split belongs to an item, not to a person.",
|
|
15
|
+
},
|
|
16
|
+
},
|
|
17
|
+
},
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
export default meta;
|
|
21
|
+
type Story = StoryObj<typeof ActivityRow>;
|
|
22
|
+
|
|
23
|
+
function who(initial: string, tone: "brown" | "teal" | "green") {
|
|
24
|
+
return (
|
|
25
|
+
<Avatar size="sm" tone={tone}>
|
|
26
|
+
<AvatarFallback>{initial}</AvatarFallback>
|
|
27
|
+
</Avatar>
|
|
28
|
+
);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/** T9 — the whole feed, down to the event that opened the table. */
|
|
32
|
+
export const Feed: Story = {
|
|
33
|
+
render: () => (
|
|
34
|
+
<ActivityList aria-label="Table activity">
|
|
35
|
+
<ActivityRow
|
|
36
|
+
avatar={who("A", "green")}
|
|
37
|
+
body={
|
|
38
|
+
<>
|
|
39
|
+
<b>Ana</b> joined the table
|
|
40
|
+
</>
|
|
41
|
+
}
|
|
42
|
+
time="2 min ago"
|
|
43
|
+
/>
|
|
44
|
+
<ActivityRow
|
|
45
|
+
avatar={who("B", "teal")}
|
|
46
|
+
body={
|
|
47
|
+
<>
|
|
48
|
+
<b>Bruno</b> added Tochitură ardelenească
|
|
49
|
+
</>
|
|
50
|
+
}
|
|
51
|
+
time="9 min ago"
|
|
52
|
+
/>
|
|
53
|
+
<ActivityRow
|
|
54
|
+
avatar={who("A", "green")}
|
|
55
|
+
body={
|
|
56
|
+
<>
|
|
57
|
+
<b>Ana</b> moved Papanași to herself
|
|
58
|
+
</>
|
|
59
|
+
}
|
|
60
|
+
time="14 min ago"
|
|
61
|
+
/>
|
|
62
|
+
<ActivityRow
|
|
63
|
+
icon={<Users aria-hidden />}
|
|
64
|
+
body="Fetească Neagră split between 3"
|
|
65
|
+
time="18 min ago"
|
|
66
|
+
/>
|
|
67
|
+
<ActivityRow
|
|
68
|
+
avatar={who("A", "brown")}
|
|
69
|
+
body={
|
|
70
|
+
<>
|
|
71
|
+
<b>Adrian</b> opened the table
|
|
72
|
+
</>
|
|
73
|
+
}
|
|
74
|
+
time="19:42"
|
|
75
|
+
/>
|
|
76
|
+
</ActivityList>
|
|
77
|
+
),
|
|
78
|
+
};
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
import { cn } from "../lib/utils";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* ActivityRow / ActivityList — what happened at the table, newest first
|
|
6
|
+
* (16-shared-table.html T9): "Bruno added Tochitură ardelenească · 9 min ago",
|
|
7
|
+
* down to "Adrian opened the table · 19:42".
|
|
8
|
+
*
|
|
9
|
+
* The leading slot is the actor's avatar when an event has one and a quiet
|
|
10
|
+
* glyph disc when it does not — a split belongs to an item, not to a person.
|
|
11
|
+
* Both are 32px so the sentences stay on one left edge however the feed mixes.
|
|
12
|
+
*
|
|
13
|
+
* The rows come from an append-only log, not from the current state: state
|
|
14
|
+
* cannot say that Ana moved Papanași to herself, only that it is hers now.
|
|
15
|
+
*/
|
|
16
|
+
export type ActivityRowProps = Omit<React.LiHTMLAttributes<HTMLLIElement>, "children"> & {
|
|
17
|
+
/** The actor's `Avatar`. Wins over `icon` when both are given. */
|
|
18
|
+
avatar?: React.ReactNode;
|
|
19
|
+
/** A lucide glyph for an event with no actor. Sits in a muted disc. */
|
|
20
|
+
icon?: React.ReactNode;
|
|
21
|
+
/** The sentence — the caller bolds the actor inside it. */
|
|
22
|
+
body: React.ReactNode;
|
|
23
|
+
/** "9 min ago", "19:42". Formatted by the caller. */
|
|
24
|
+
time: React.ReactNode;
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
const ActivityRow = React.forwardRef<HTMLLIElement, ActivityRowProps>(
|
|
28
|
+
({ className, avatar, icon, body, time, ...props }, ref) => (
|
|
29
|
+
<li
|
|
30
|
+
ref={ref}
|
|
31
|
+
data-slot="activity-row"
|
|
32
|
+
className={cn("flex items-start gap-3 py-3", className)}
|
|
33
|
+
{...props}
|
|
34
|
+
>
|
|
35
|
+
<div className="shrink-0">
|
|
36
|
+
{avatar ?? (
|
|
37
|
+
<span className="grid size-8 place-items-center rounded-full bg-muted text-muted-foreground [&>svg]:size-4">
|
|
38
|
+
{icon}
|
|
39
|
+
</span>
|
|
40
|
+
)}
|
|
41
|
+
</div>
|
|
42
|
+
<div className="min-w-0 flex-1">
|
|
43
|
+
<div className="text-[14px] leading-snug">{body}</div>
|
|
44
|
+
<div className="mt-0.5 text-[12.5px] text-muted-foreground">{time}</div>
|
|
45
|
+
</div>
|
|
46
|
+
</li>
|
|
47
|
+
)
|
|
48
|
+
);
|
|
49
|
+
ActivityRow.displayName = "ActivityRow";
|
|
50
|
+
|
|
51
|
+
export type ActivityListProps = React.HTMLAttributes<HTMLUListElement>;
|
|
52
|
+
|
|
53
|
+
const ActivityList = React.forwardRef<HTMLUListElement, ActivityListProps>(
|
|
54
|
+
({ className, ...props }, ref) => (
|
|
55
|
+
<ul
|
|
56
|
+
ref={ref}
|
|
57
|
+
data-slot="activity-list"
|
|
58
|
+
className={cn("divide-y divide-border", className)}
|
|
59
|
+
{...props}
|
|
60
|
+
/>
|
|
61
|
+
)
|
|
62
|
+
);
|
|
63
|
+
ActivityList.displayName = "ActivityList";
|
|
64
|
+
|
|
65
|
+
export { ActivityRow, ActivityList };
|
|
@@ -79,6 +79,61 @@ export const GroupSizes: Story = {
|
|
|
79
79
|
),
|
|
80
80
|
};
|
|
81
81
|
|
|
82
|
+
/**
|
|
83
|
+
* `tone` — a person's fixed hue at a shared table, so the same face is the same
|
|
84
|
+
* colour on the ledger card, the organise chip, the split row and the activity
|
|
85
|
+
* feed. The caller assigns it; the design system never hashes a name into a
|
|
86
|
+
* colour, because a hash re-colours everyone when one person joins and collides
|
|
87
|
+
* often enough to matter at a table of four.
|
|
88
|
+
*
|
|
89
|
+
* `muted` is the non-person disc — "Shared between everyone", an event with no
|
|
90
|
+
* actor. Check the dark story: the three hues lift a notch there so a disc keeps
|
|
91
|
+
* an edge against the card without giving up the white initial.
|
|
92
|
+
*/
|
|
93
|
+
const TONES = ["brown", "teal", "green", "blue", "violet", "rose", "muted"] as const;
|
|
94
|
+
|
|
95
|
+
export const Tones: Story = {
|
|
96
|
+
render: () => (
|
|
97
|
+
<div className="flex items-center gap-3">
|
|
98
|
+
{TONES.map((tone) => (
|
|
99
|
+
<Avatar key={tone} tone={tone}>
|
|
100
|
+
<AvatarFallback>{tone.slice(0, 1).toUpperCase()}</AvatarFallback>
|
|
101
|
+
</Avatar>
|
|
102
|
+
))}
|
|
103
|
+
</div>
|
|
104
|
+
),
|
|
105
|
+
};
|
|
106
|
+
|
|
107
|
+
export const TonesDark: Story = {
|
|
108
|
+
parameters: { backgrounds: { default: "dark" } },
|
|
109
|
+
render: () => (
|
|
110
|
+
<div className="dark flex items-center gap-3 rounded-xl bg-card p-4">
|
|
111
|
+
{TONES.map((tone) => (
|
|
112
|
+
<Avatar key={tone} tone={tone}>
|
|
113
|
+
<AvatarFallback>{tone.slice(0, 1).toUpperCase()}</AvatarFallback>
|
|
114
|
+
</Avatar>
|
|
115
|
+
))}
|
|
116
|
+
</div>
|
|
117
|
+
),
|
|
118
|
+
};
|
|
119
|
+
|
|
120
|
+
/** The stack a shared table draws in its header and on its trigger band. */
|
|
121
|
+
export const TonedStack: Story = {
|
|
122
|
+
render: () => (
|
|
123
|
+
<AvatarGroup spacing="tight" size="sm">
|
|
124
|
+
<Avatar tone="brown">
|
|
125
|
+
<AvatarFallback>A</AvatarFallback>
|
|
126
|
+
</Avatar>
|
|
127
|
+
<Avatar tone="teal">
|
|
128
|
+
<AvatarFallback>B</AvatarFallback>
|
|
129
|
+
</Avatar>
|
|
130
|
+
<Avatar tone="green">
|
|
131
|
+
<AvatarFallback>A</AvatarFallback>
|
|
132
|
+
</Avatar>
|
|
133
|
+
</AvatarGroup>
|
|
134
|
+
),
|
|
135
|
+
};
|
|
136
|
+
|
|
82
137
|
/**
|
|
83
138
|
* An individual `Avatar` can still opt out of the group size — useful for
|
|
84
139
|
* highlighting "you" inside a roster.
|
|
@@ -16,6 +16,34 @@ const avatarVariants = cva(
|
|
|
16
16
|
lg: "size-12 text-base",
|
|
17
17
|
xl: "size-16 text-lg",
|
|
18
18
|
},
|
|
19
|
+
/**
|
|
20
|
+
* A person's fixed hue at a shared table: the same face is the same
|
|
21
|
+
* colour on the ledger card, the organise chip, the split row and the
|
|
22
|
+
* activity feed, so a reader tracks people by colour and never has to
|
|
23
|
+
* re-read a name (16-shared-table.html, T2/T4/T6/T9).
|
|
24
|
+
*
|
|
25
|
+
* The caller assigns it — from join order, from the wire, from wherever
|
|
26
|
+
* it keeps its participants. The design system never hashes a name into
|
|
27
|
+
* a colour: a hash re-colours everyone when one person joins, and gives
|
|
28
|
+
* two people the same hue often enough to matter at a table of four.
|
|
29
|
+
*
|
|
30
|
+
* `muted` is the non-person disc — the "Shared between everyone" card's
|
|
31
|
+
* people glyph, an activity row with no actor.
|
|
32
|
+
*
|
|
33
|
+
* Six hues, assigned in the order listed. The board draws the first
|
|
34
|
+
* three; the rest space across the arc they leave free. Past six, cycle —
|
|
35
|
+
* at that size the initial and the name are doing the identifying, and
|
|
36
|
+
* colour is a support cue, never the only one.
|
|
37
|
+
*/
|
|
38
|
+
tone: {
|
|
39
|
+
brown: "bg-avatar-brown",
|
|
40
|
+
teal: "bg-avatar-teal",
|
|
41
|
+
green: "bg-avatar-green",
|
|
42
|
+
blue: "bg-avatar-blue",
|
|
43
|
+
violet: "bg-avatar-violet",
|
|
44
|
+
rose: "bg-avatar-rose",
|
|
45
|
+
muted: "bg-muted",
|
|
46
|
+
},
|
|
19
47
|
},
|
|
20
48
|
defaultVariants: {
|
|
21
49
|
size: "default",
|
|
@@ -24,14 +52,17 @@ const avatarVariants = cva(
|
|
|
24
52
|
);
|
|
25
53
|
|
|
26
54
|
type AvatarSize = NonNullable<VariantProps<typeof avatarVariants>["size"]>;
|
|
55
|
+
type AvatarTone = NonNullable<VariantProps<typeof avatarVariants>["tone"]>;
|
|
27
56
|
|
|
28
57
|
const AvatarContext = React.createContext<AvatarSize>("default");
|
|
58
|
+
/** `null` = no tone set, so the fallback keeps its own muted ground. */
|
|
59
|
+
const AvatarToneContext = React.createContext<AvatarTone | null>(null);
|
|
29
60
|
|
|
30
61
|
const Avatar = React.forwardRef<
|
|
31
62
|
React.ElementRef<typeof AvatarPrimitive.Root>,
|
|
32
63
|
React.ComponentPropsWithoutRef<typeof AvatarPrimitive.Root> &
|
|
33
64
|
VariantProps<typeof avatarVariants>
|
|
34
|
-
>(({ className, size, ...props }, ref) => {
|
|
65
|
+
>(({ className, size, tone, ...props }, ref) => {
|
|
35
66
|
// An explicit `size` always wins; otherwise inherit from the nearest
|
|
36
67
|
// AvatarGroup (context defaults to "default", so a standalone Avatar with no
|
|
37
68
|
// size behaves exactly as before).
|
|
@@ -40,12 +71,15 @@ const Avatar = React.forwardRef<
|
|
|
40
71
|
|
|
41
72
|
return (
|
|
42
73
|
<AvatarContext.Provider value={resolvedSize}>
|
|
43
|
-
<
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
74
|
+
<AvatarToneContext.Provider value={tone ?? null}>
|
|
75
|
+
<AvatarPrimitive.Root
|
|
76
|
+
ref={ref}
|
|
77
|
+
data-slot="avatar"
|
|
78
|
+
data-tone={tone ?? undefined}
|
|
79
|
+
className={cn(avatarVariants({ size: resolvedSize, tone }), className)}
|
|
80
|
+
{...props}
|
|
81
|
+
/>
|
|
82
|
+
</AvatarToneContext.Provider>
|
|
49
83
|
</AvatarContext.Provider>
|
|
50
84
|
);
|
|
51
85
|
});
|
|
@@ -67,17 +101,27 @@ AvatarImage.displayName = AvatarPrimitive.Image.displayName;
|
|
|
67
101
|
const AvatarFallback = React.forwardRef<
|
|
68
102
|
React.ElementRef<typeof AvatarPrimitive.Fallback>,
|
|
69
103
|
React.ComponentPropsWithoutRef<typeof AvatarPrimitive.Fallback>
|
|
70
|
-
>(({ className, ...props }, ref) =>
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
104
|
+
>(({ className, ...props }, ref) => {
|
|
105
|
+
// The fallback paints its own ground, which would cover the Root's tone.
|
|
106
|
+
// With a tone set it steps aside and takes the ink that tone was measured
|
|
107
|
+
// for: white on the three hues (6.2–7.0:1 light, 4.8–5.4:1 dark), the muted
|
|
108
|
+
// ink on `muted`, which is not a person and stays quiet.
|
|
109
|
+
const tone = React.useContext(AvatarToneContext);
|
|
110
|
+
return (
|
|
111
|
+
<AvatarPrimitive.Fallback
|
|
112
|
+
ref={ref}
|
|
113
|
+
data-slot="avatar-fallback"
|
|
114
|
+
className={cn(
|
|
115
|
+
"flex size-full items-center justify-center rounded-full font-semibold",
|
|
116
|
+
tone === null && "bg-muted text-muted-foreground",
|
|
117
|
+
tone === "muted" && "text-muted-foreground",
|
|
118
|
+
tone !== null && tone !== "muted" && "text-white",
|
|
119
|
+
className
|
|
120
|
+
)}
|
|
121
|
+
{...props}
|
|
122
|
+
/>
|
|
123
|
+
);
|
|
124
|
+
});
|
|
81
125
|
AvatarFallback.displayName = AvatarPrimitive.Fallback.displayName;
|
|
82
126
|
|
|
83
127
|
const badgeOffset = {
|
|
@@ -195,4 +239,4 @@ export {
|
|
|
195
239
|
avatarVariants,
|
|
196
240
|
avatarGroupOverflowVariants,
|
|
197
241
|
};
|
|
198
|
-
export type { AvatarSize };
|
|
242
|
+
export type { AvatarSize, AvatarTone };
|
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import type { Meta, StoryObj } from "@storybook/react";
|
|
2
|
+
import { ChevronDown, Users } from "lucide-react";
|
|
3
|
+
import { Avatar, AvatarFallback, AvatarGroup } from "./avatar";
|
|
2
4
|
import { Badge } from "./badge";
|
|
3
5
|
import { Button } from "./button";
|
|
4
6
|
|
|
@@ -85,6 +87,45 @@ export const BookingStatus: Story = {
|
|
|
85
87
|
),
|
|
86
88
|
};
|
|
87
89
|
|
|
90
|
+
/**
|
|
91
|
+
* `assignee` — who an item belongs to, in the shared table's organise view. It
|
|
92
|
+
* is a control, not a status: the whole pill opens "Who had this?", so it keeps
|
|
93
|
+
* room either side for the avatar and the chevron the caller renders into it.
|
|
94
|
+
*/
|
|
95
|
+
export const Assignee: Story = {
|
|
96
|
+
render: () => (
|
|
97
|
+
<div className="flex flex-wrap items-center gap-3">
|
|
98
|
+
<Badge variant="assignee" as="span">
|
|
99
|
+
<Avatar size="xs" tone="brown">
|
|
100
|
+
<AvatarFallback>A</AvatarFallback>
|
|
101
|
+
</Avatar>
|
|
102
|
+
Adrian
|
|
103
|
+
<ChevronDown className="size-3.5 opacity-60" aria-hidden />
|
|
104
|
+
</Badge>
|
|
105
|
+
<Badge variant="assignee" as="span">
|
|
106
|
+
<AvatarGroup size="xs" spacing="tight">
|
|
107
|
+
<Avatar tone="brown">
|
|
108
|
+
<AvatarFallback>A</AvatarFallback>
|
|
109
|
+
</Avatar>
|
|
110
|
+
<Avatar tone="teal">
|
|
111
|
+
<AvatarFallback>B</AvatarFallback>
|
|
112
|
+
</Avatar>
|
|
113
|
+
<Avatar tone="green">
|
|
114
|
+
<AvatarFallback>A</AvatarFallback>
|
|
115
|
+
</Avatar>
|
|
116
|
+
</AvatarGroup>
|
|
117
|
+
Shared
|
|
118
|
+
</Badge>
|
|
119
|
+
<Badge variant="assignee" as="span">
|
|
120
|
+
<span className="grid size-6 place-items-center rounded-full bg-muted text-muted-foreground">
|
|
121
|
+
<Users className="size-3" aria-hidden />
|
|
122
|
+
</span>
|
|
123
|
+
Shared
|
|
124
|
+
</Badge>
|
|
125
|
+
</div>
|
|
126
|
+
),
|
|
127
|
+
};
|
|
128
|
+
|
|
88
129
|
export const AllVariants: Story = {
|
|
89
130
|
render: () => (
|
|
90
131
|
<div className="flex flex-wrap items-center gap-3">
|
package/src/components/badge.tsx
CHANGED
|
@@ -45,6 +45,19 @@ const badgeVariants = cva(
|
|
|
45
45
|
*/
|
|
46
46
|
completed:
|
|
47
47
|
"rounded-full border-0 bg-muted px-[9px] py-[3px] text-[10.5px] leading-[1.2] font-bold text-muted-foreground",
|
|
48
|
+
/**
|
|
49
|
+
* Who an item belongs to, in the organise table (T4 desktop): a soft
|
|
50
|
+
* pill carrying an avatar (or an `AvatarGroup` for "Shared") and a
|
|
51
|
+
* chevron, because the whole pill opens "Who had this?".
|
|
52
|
+
*
|
|
53
|
+
* It is a control, not a status, so it keeps 6px of inset either side
|
|
54
|
+
* of its slots and `gap-1.5` between them; the caller renders the
|
|
55
|
+
* avatar and the chevron as children. Sized to clear 44px of tap
|
|
56
|
+
* target when it is the row's own control — see `MenuPickRow` for the
|
|
57
|
+
* same rule on the add toggle.
|
|
58
|
+
*/
|
|
59
|
+
assignee:
|
|
60
|
+
"rounded-full border-0 bg-surface-2 py-1 pl-1 pr-2 gap-1.5 text-[12px] leading-[1.2] font-semibold text-foreground",
|
|
48
61
|
},
|
|
49
62
|
/**
|
|
50
63
|
* `status` is the taller pill the booking design draws beside a
|
|
@@ -80,7 +80,7 @@ const buttonVariants = cva(
|
|
|
80
80
|
/** The `ink` action's shape. Owns the radius so no variant fights it. */
|
|
81
81
|
pill: "h-11 rounded-full px-5 text-[13.5px] font-bold",
|
|
82
82
|
/** The booking design's action: 46px, 12px radius, 15px semibold. */
|
|
83
|
-
cta: "h-[46px] rounded-
|
|
83
|
+
cta: "h-[46px] rounded-[12px] px-5 text-[15px] font-semibold",
|
|
84
84
|
/** Its compact form, in dense footers and inline rows: 38px, 10px radius. */
|
|
85
85
|
"cta-sm": "h-[38px] rounded-[10px] px-4 text-[13.5px] font-semibold",
|
|
86
86
|
icon: "h-11 w-11 rounded-full",
|
|
@@ -53,6 +53,12 @@ const dialogContentVariants = cva(
|
|
|
53
53
|
type DialogContentProps = React.ComponentPropsWithoutRef<typeof DialogPrimitive.Content> &
|
|
54
54
|
VariantProps<typeof dialogContentVariants> & {
|
|
55
55
|
showCloseButton?: boolean;
|
|
56
|
+
/**
|
|
57
|
+
* Classes for the scrim behind the dialog. The default `bg-black/50` is
|
|
58
|
+
* kept for every existing caller; pass `bg-scrim-overlay` for the themed
|
|
59
|
+
* dim (see `--scrim-overlay` in globals.css, and why it is not `--scrim`).
|
|
60
|
+
*/
|
|
61
|
+
overlayClassName?: string;
|
|
56
62
|
};
|
|
57
63
|
|
|
58
64
|
const DialogContent = React.forwardRef<
|
|
@@ -60,11 +66,11 @@ const DialogContent = React.forwardRef<
|
|
|
60
66
|
DialogContentProps
|
|
61
67
|
>(
|
|
62
68
|
(
|
|
63
|
-
{ className, children, size, scrollBehavior, showCloseButton = true, ...props },
|
|
69
|
+
{ className, children, size, scrollBehavior, showCloseButton = true, overlayClassName, ...props },
|
|
64
70
|
ref
|
|
65
71
|
) => (
|
|
66
72
|
<DialogPortal>
|
|
67
|
-
<DialogOverlay />
|
|
73
|
+
<DialogOverlay className={overlayClassName} />
|
|
68
74
|
<DialogPrimitive.Content
|
|
69
75
|
ref={ref}
|
|
70
76
|
data-slot="dialog-content"
|
|
@@ -43,12 +43,18 @@ DrawerOverlay.displayName = DrawerPrimitive.Overlay.displayName;
|
|
|
43
43
|
type DrawerContentProps = React.ComponentPropsWithoutRef<typeof DrawerPrimitive.Content> & {
|
|
44
44
|
variant?: "flush" | "floating";
|
|
45
45
|
showHandle?: boolean;
|
|
46
|
+
/**
|
|
47
|
+
* Classes for the scrim behind the sheet. The default `bg-black/60` is kept
|
|
48
|
+
* for every existing caller; pass `bg-scrim-overlay` for the themed dim (see
|
|
49
|
+
* `--scrim-overlay` in globals.css, and why it is not `--scrim`).
|
|
50
|
+
*/
|
|
51
|
+
overlayClassName?: string;
|
|
46
52
|
};
|
|
47
53
|
|
|
48
54
|
const DrawerContent = React.forwardRef<
|
|
49
55
|
React.ElementRef<typeof DrawerPrimitive.Content>,
|
|
50
56
|
DrawerContentProps
|
|
51
|
-
>(({ className, variant = "flush", showHandle, children, ...props }, ref) => {
|
|
57
|
+
>(({ className, variant = "flush", showHandle, overlayClassName, children, ...props }, ref) => {
|
|
52
58
|
const direction = React.useContext(DrawerDirectionContext);
|
|
53
59
|
const isVertical = direction === "top" || direction === "bottom";
|
|
54
60
|
const handleEnabled = showHandle ?? (direction === "bottom");
|
|
@@ -64,7 +70,7 @@ const DrawerContent = React.forwardRef<
|
|
|
64
70
|
|
|
65
71
|
return (
|
|
66
72
|
<DrawerPortal>
|
|
67
|
-
<DrawerOverlay />
|
|
73
|
+
<DrawerOverlay className={overlayClassName} />
|
|
68
74
|
<DrawerPrimitive.Content
|
|
69
75
|
ref={ref}
|
|
70
76
|
data-slot="drawer-content"
|
|
@@ -49,7 +49,7 @@ export type HoldCountdownProps = {
|
|
|
49
49
|
* design's as a timer. Dark mode takes the lifted token, which already has
|
|
50
50
|
* the contrast against the dark card.
|
|
51
51
|
*/
|
|
52
|
-
const HOLD_BORDER = "border-
|
|
52
|
+
const HOLD_BORDER = "border-warning-deep";
|
|
53
53
|
|
|
54
54
|
function remainingSeconds(expiresAt: string | number, now: number): number {
|
|
55
55
|
const end = typeof expiresAt === "number" ? expiresAt : Date.parse(expiresAt);
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
import type { Meta, StoryObj } from "@storybook/react";
|
|
3
|
+
import { MenuPickRow } from "./menu-pick-row";
|
|
4
|
+
|
|
5
|
+
const meta: Meta<typeof MenuPickRow> = {
|
|
6
|
+
title: "Shared table/MenuPickRow",
|
|
7
|
+
component: MenuPickRow,
|
|
8
|
+
tags: ["autodocs"],
|
|
9
|
+
parameters: {
|
|
10
|
+
docs: {
|
|
11
|
+
description: {
|
|
12
|
+
component:
|
|
13
|
+
"A dish in the add-an-item overlay. The disc is the control, not the row: a whole-row target would make reading the menu and adding to the table the same gesture.",
|
|
14
|
+
},
|
|
15
|
+
},
|
|
16
|
+
},
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
export default meta;
|
|
20
|
+
type Story = StoryObj<typeof MenuPickRow>;
|
|
21
|
+
|
|
22
|
+
export const Unselected: Story = {
|
|
23
|
+
args: { label: "Mușchi de vită la grătar", price: "96 lei", addLabel: "Add Mușchi de vită la grătar" },
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
export const Selected: Story = {
|
|
27
|
+
args: {
|
|
28
|
+
label: "Tochitură ardelenească",
|
|
29
|
+
price: "54 lei",
|
|
30
|
+
selected: true,
|
|
31
|
+
removeLabel: "Remove Tochitură ardelenească",
|
|
32
|
+
},
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
/** T3 — a category of the venue's menu. */
|
|
36
|
+
export const Category: Story = {
|
|
37
|
+
render: () => {
|
|
38
|
+
const [picked, setPicked] = React.useState<string[]>(["Tochitură ardelenească"]);
|
|
39
|
+
const dishes = [
|
|
40
|
+
{ label: "Mușchi de vită la grătar", price: "96 lei" },
|
|
41
|
+
{ label: "Tochitură ardelenească", price: "54 lei" },
|
|
42
|
+
{ label: "Păstrăv la cuptor", price: "62 lei" },
|
|
43
|
+
];
|
|
44
|
+
return (
|
|
45
|
+
<div>
|
|
46
|
+
<p className="mb-1 font-mono text-[11px] font-bold uppercase tracking-wider text-muted-foreground">
|
|
47
|
+
Main courses
|
|
48
|
+
</p>
|
|
49
|
+
<div className="divide-y divide-border">
|
|
50
|
+
{dishes.map((dish) => (
|
|
51
|
+
<MenuPickRow
|
|
52
|
+
key={dish.label}
|
|
53
|
+
label={dish.label}
|
|
54
|
+
price={dish.price}
|
|
55
|
+
selected={picked.includes(dish.label)}
|
|
56
|
+
onToggle={() =>
|
|
57
|
+
setPicked((prev) =>
|
|
58
|
+
prev.includes(dish.label)
|
|
59
|
+
? prev.filter((d) => d !== dish.label)
|
|
60
|
+
: [...prev, dish.label]
|
|
61
|
+
)
|
|
62
|
+
}
|
|
63
|
+
addLabel={`Add ${dish.label}`}
|
|
64
|
+
removeLabel={`Remove ${dish.label}`}
|
|
65
|
+
/>
|
|
66
|
+
))}
|
|
67
|
+
</div>
|
|
68
|
+
</div>
|
|
69
|
+
);
|
|
70
|
+
},
|
|
71
|
+
};
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import * as React from "react";
|
|
4
|
+
import { Plus } from "lucide-react";
|
|
5
|
+
import { cn } from "../lib/utils";
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* MenuPickRow — a dish in the add-an-item overlay (16-shared-table.html T3):
|
|
9
|
+
* its name, its price, and a disc that adds it.
|
|
10
|
+
*
|
|
11
|
+
* The disc is the control and the row is not: a whole-row target would make
|
|
12
|
+
* "read the menu" and "add this" the same gesture, and a reader scrolling a
|
|
13
|
+
* long menu would add things by accident. It is 44×44 as the accessibility
|
|
14
|
+
* floor asks, with the drawn 28px disc inside it.
|
|
15
|
+
*
|
|
16
|
+
* A tap adds one unit. There is no stepper and no quantity anywhere in the
|
|
17
|
+
* shared table — one row per unit, so removing one is removing one thing and
|
|
18
|
+
* never an arithmetic edit (the board's ruling 3). `selected` is therefore
|
|
19
|
+
* "this dish is on the table", not a count; a caller showing a second unit
|
|
20
|
+
* shows a second row.
|
|
21
|
+
*/
|
|
22
|
+
export type MenuPickRowProps = Omit<React.HTMLAttributes<HTMLDivElement>, "onToggle"> & {
|
|
23
|
+
/** The dish. */
|
|
24
|
+
label: React.ReactNode;
|
|
25
|
+
/** Its price, formatted by the caller. */
|
|
26
|
+
price: React.ReactNode;
|
|
27
|
+
/** Is it on the table already? Fills the disc. */
|
|
28
|
+
selected?: boolean;
|
|
29
|
+
onToggle?: () => void;
|
|
30
|
+
/** The disc's accessible name when it would add — "Add Papanași". Injected. */
|
|
31
|
+
addLabel?: string;
|
|
32
|
+
/** Its name when it would remove — "Remove Papanași". Injected. */
|
|
33
|
+
removeLabel?: string;
|
|
34
|
+
disabled?: boolean;
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
const MenuPickRow = React.forwardRef<HTMLDivElement, MenuPickRowProps>(
|
|
38
|
+
(
|
|
39
|
+
{ className, label, price, selected = false, onToggle, addLabel, removeLabel, disabled = false, ...props },
|
|
40
|
+
ref
|
|
41
|
+
) => (
|
|
42
|
+
<div
|
|
43
|
+
ref={ref}
|
|
44
|
+
data-slot="menu-pick-row"
|
|
45
|
+
data-selected={selected ? "" : undefined}
|
|
46
|
+
className={cn("flex items-center gap-3 py-2.5", className)}
|
|
47
|
+
{...props}
|
|
48
|
+
>
|
|
49
|
+
<span className="min-w-0 flex-1 truncate text-[14px] font-bold">{label}</span>
|
|
50
|
+
{/* The numeral is the bold sans; only the caller's currency suffix is mono. */}
|
|
51
|
+
<span className="shrink-0 text-[14px] font-bold tabular-nums">{price}</span>
|
|
52
|
+
<button
|
|
53
|
+
type="button"
|
|
54
|
+
onClick={onToggle}
|
|
55
|
+
disabled={disabled}
|
|
56
|
+
aria-pressed={selected}
|
|
57
|
+
aria-label={selected ? removeLabel : addLabel}
|
|
58
|
+
className="grid size-11 shrink-0 place-items-center rounded-full focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50"
|
|
59
|
+
>
|
|
60
|
+
<span
|
|
61
|
+
className={cn(
|
|
62
|
+
"grid size-7 place-items-center rounded-full border transition-colors",
|
|
63
|
+
selected
|
|
64
|
+
? "border-transparent bg-primary text-primary-foreground"
|
|
65
|
+
: "border-border bg-background text-muted-foreground"
|
|
66
|
+
)}
|
|
67
|
+
>
|
|
68
|
+
<Plus className="size-4" aria-hidden strokeWidth={2.5} />
|
|
69
|
+
</span>
|
|
70
|
+
</button>
|
|
71
|
+
</div>
|
|
72
|
+
)
|
|
73
|
+
);
|
|
74
|
+
MenuPickRow.displayName = "MenuPickRow";
|
|
75
|
+
|
|
76
|
+
export { MenuPickRow };
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from "@storybook/react";
|
|
2
|
+
import { QrPanel } from "./qr-panel";
|
|
3
|
+
|
|
4
|
+
const meta: Meta<typeof QrPanel> = {
|
|
5
|
+
title: "Shared table/QrPanel",
|
|
6
|
+
component: QrPanel,
|
|
7
|
+
tags: ["autodocs"],
|
|
8
|
+
parameters: {
|
|
9
|
+
docs: {
|
|
10
|
+
description: {
|
|
11
|
+
component:
|
|
12
|
+
"The frame around the code that gets a phone onto a table. The code arrives as children — the design system imports no QR library. The plate stays white in dark mode because a reader needs the light module ground and the quiet zone to decode.",
|
|
13
|
+
},
|
|
14
|
+
},
|
|
15
|
+
},
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
export default meta;
|
|
19
|
+
type Story = StoryObj<typeof QrPanel>;
|
|
20
|
+
|
|
21
|
+
/** Stand-in for whatever the app's QR library renders. */
|
|
22
|
+
const code = (
|
|
23
|
+
<svg viewBox="0 0 21 21" shapeRendering="crispEdges" aria-hidden>
|
|
24
|
+
<rect width="21" height="21" fill="#fff" />
|
|
25
|
+
{Array.from({ length: 21 }, (_, y) =>
|
|
26
|
+
Array.from({ length: 21 }, (_, x) =>
|
|
27
|
+
(x * 7 + y * 3) % 5 < 2 ? <rect key={`${x}-${y}`} x={x} y={y} width="1" height="1" /> : null
|
|
28
|
+
)
|
|
29
|
+
)}
|
|
30
|
+
</svg>
|
|
31
|
+
);
|
|
32
|
+
|
|
33
|
+
export const Default: Story = {
|
|
34
|
+
args: { children: code, caption: "Scan with a phone camera to join" },
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
export const WithoutCaption: Story = {
|
|
38
|
+
args: { children: code },
|
|
39
|
+
};
|