@bearmenu/ui 0.8.33 → 0.8.35

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.
Files changed (86) hide show
  1. package/dist/{chunk-GESB7GCF.js → chunk-6W25S5CF.js} +1 -1
  2. package/dist/chunk-LY67SOFC.js +142 -0
  3. package/dist/{chunk-ZU4NAYN7.js → chunk-NIAL4ZU2.js} +13 -1
  4. package/dist/{chunk-JWTN7MNK.js → chunk-TVEIUTBQ.js} +2 -2
  5. package/dist/{chunk-ZJMCU7MI.js → chunk-VCRXL7H5.js} +1 -1
  6. package/dist/chunk-XLHMF6F5.js +127 -0
  7. package/dist/{chunk-VSRGEB36.js → chunk-XNBDACL7.js} +1 -1
  8. package/dist/components/action-bar.d.ts +67 -0
  9. package/dist/components/action-bar.js +43 -0
  10. package/dist/components/activity-row.d.ts +38 -0
  11. package/dist/components/activity-row.js +43 -0
  12. package/dist/components/alert.d.ts +2 -2
  13. package/dist/components/avatar.d.ts +4 -1
  14. package/dist/components/avatar.js +39 -5
  15. package/dist/components/badge.d.ts +1 -1
  16. package/dist/components/badge.js +1 -1
  17. package/dist/components/booking-row.d.ts +11 -1
  18. package/dist/components/booking-row.js +4 -1
  19. package/dist/components/button.d.ts +2 -2
  20. package/dist/components/combobox.js +2 -2
  21. package/dist/components/command.d.ts +15 -15
  22. package/dist/components/command.js +2 -2
  23. package/dist/components/concept-convergence.js +2 -2
  24. package/dist/components/currency-input.js +4 -142
  25. package/dist/components/dialog.d.ts +6 -0
  26. package/dist/components/dialog.js +1 -1
  27. package/dist/components/drawer.d.ts +6 -0
  28. package/dist/components/drawer.js +3 -127
  29. package/dist/components/empty-state.d.ts +2 -2
  30. package/dist/components/filter-category-row.js +1 -1
  31. package/dist/components/menu-item.js +3 -3
  32. package/dist/components/menu-pick-row.d.ts +47 -0
  33. package/dist/components/menu-pick-row.js +49 -0
  34. package/dist/components/multi-select-combobox.js +3 -3
  35. package/dist/components/place-card.js +1 -1
  36. package/dist/components/place-details-mobile.js +1 -1
  37. package/dist/components/qr-panel.d.ts +31 -0
  38. package/dist/components/qr-panel.js +33 -0
  39. package/dist/components/reconcile-bar.d.ts +47 -0
  40. package/dist/components/reconcile-bar.js +60 -0
  41. package/dist/components/responsive-overlay.d.ts +59 -0
  42. package/dist/components/responsive-overlay.js +132 -0
  43. package/dist/components/searchable-select.js +2 -2
  44. package/dist/components/signature-chip.js +2 -2
  45. package/dist/components/spinner.d.ts +2 -2
  46. package/dist/components/split-share-row.d.ts +77 -0
  47. package/dist/components/split-share-row.js +101 -0
  48. package/dist/components/table-ledger-card.d.ts +80 -0
  49. package/dist/components/table-ledger-card.js +84 -0
  50. package/dist/components/table-trigger-band.d.ts +56 -0
  51. package/dist/components/table-trigger-band.js +43 -0
  52. package/dist/components/tag.d.ts +2 -2
  53. package/dist/components/text.d.ts +2 -2
  54. package/dist/components/toggle.d.ts +1 -1
  55. package/dist/components/top-ranking-entry.js +3 -3
  56. package/dist/components/venue-events-block.js +1 -1
  57. package/package.json +23 -207
  58. package/src/components/action-bar.stories.tsx +78 -0
  59. package/src/components/action-bar.tsx +80 -0
  60. package/src/components/activity-row.stories.tsx +78 -0
  61. package/src/components/activity-row.tsx +65 -0
  62. package/src/components/avatar.stories.tsx +55 -0
  63. package/src/components/avatar.tsx +63 -19
  64. package/src/components/badge.stories.tsx +41 -0
  65. package/src/components/badge.tsx +13 -0
  66. package/src/components/booking-row.tsx +13 -0
  67. package/src/components/dialog.tsx +8 -2
  68. package/src/components/drawer.tsx +8 -2
  69. package/src/components/menu-pick-row.stories.tsx +71 -0
  70. package/src/components/menu-pick-row.tsx +76 -0
  71. package/src/components/qr-panel.stories.tsx +39 -0
  72. package/src/components/qr-panel.tsx +52 -0
  73. package/src/components/reconcile-bar.stories.tsx +57 -0
  74. package/src/components/reconcile-bar.tsx +80 -0
  75. package/src/components/responsive-overlay.stories.tsx +91 -0
  76. package/src/components/responsive-overlay.test.tsx +162 -0
  77. package/src/components/responsive-overlay.tsx +232 -0
  78. package/src/components/shared-table-primitives.test.tsx +482 -0
  79. package/src/components/split-share-row.stories.tsx +114 -0
  80. package/src/components/split-share-row.tsx +150 -0
  81. package/src/components/table-ledger-card.stories.tsx +120 -0
  82. package/src/components/table-ledger-card.tsx +138 -0
  83. package/src/components/table-trigger-band.stories.tsx +76 -0
  84. package/src/components/table-trigger-band.tsx +75 -0
  85. package/src/globals.css +44 -0
  86. package/src/test/setup.ts +11 -0
@@ -0,0 +1,78 @@
1
+ import type { Meta, StoryObj } from "@storybook/react";
2
+ import { ActionBar } from "./action-bar";
3
+ import { Button } from "./button";
4
+
5
+ const meta: Meta<typeof ActionBar> = {
6
+ title: "Shared table/ActionBar",
7
+ component: ActionBar,
8
+ tags: ["autodocs"],
9
+ parameters: {
10
+ docs: {
11
+ description: {
12
+ component:
13
+ "The band that closes a screen or an overlay. With a note, the note takes the left and the actions sit right; without one, the actions divide the width and the primary takes the trailing edge. The bar owns the home-indicator inset.",
14
+ },
15
+ },
16
+ },
17
+ };
18
+
19
+ export default meta;
20
+ type Story = StoryObj<typeof ActionBar>;
21
+
22
+ /** T2 — the table overview's pair, over the bottom nav. The one split bar. */
23
+ export const Split: Story = {
24
+ args: {
25
+ layout: "split",
26
+ secondary: <Button variant="outline">Add item</Button>,
27
+ primary: <Button variant="cta">Organise</Button>,
28
+ },
29
+ };
30
+
31
+ /** T6 desktop — two actions at their own width, on the trailing edge. */
32
+ export const TwoActions: Story = {
33
+ args: {
34
+ secondary: <Button variant="outline">Cancel</Button>,
35
+ primary: <Button variant="cta">Save split</Button>,
36
+ },
37
+ };
38
+
39
+ /** T3 — the add-item overlay: the qualifier is read before the commit. */
40
+ export const WithNote: Story = {
41
+ args: {
42
+ note: "Added items can be assigned afterwards.",
43
+ primary: <Button variant="cta">Add 1 item</Button>,
44
+ },
45
+ };
46
+
47
+ /** T6 mobile — the split sheet commits and nothing else. */
48
+ export const PrimaryOnly: Story = {
49
+ args: {
50
+ primary: <Button variant="cta">Save split</Button>,
51
+ },
52
+ };
53
+
54
+ /** X10 — a split that does not balance cannot be saved. */
55
+ export const DisabledPrimary: Story = {
56
+ args: {
57
+ note: "10 lei short of the item total.",
58
+ primary: (
59
+ <Button variant="cta" disabled className="bg-muted text-muted-foreground">
60
+ Save split
61
+ </Button>
62
+ ),
63
+ },
64
+ };
65
+
66
+ export const Sticky: Story = {
67
+ args: {
68
+ sticky: true,
69
+ secondary: <Button variant="outline">Cancel</Button>,
70
+ primary: <Button variant="cta">Review</Button>,
71
+ },
72
+ render: (args) => (
73
+ <div className="h-64 overflow-y-auto rounded-xl border border-border">
74
+ <div className="h-80 bg-surface-2 p-4 text-sm text-muted-foreground">Scroll me.</div>
75
+ <ActionBar {...args} />
76
+ </div>
77
+ ),
78
+ };
@@ -0,0 +1,80 @@
1
+ import * as React from "react";
2
+ import { cn } from "../lib/utils";
3
+
4
+ /**
5
+ * ActionBar — the band that closes a screen or an overlay: the note that
6
+ * qualifies the action on one side, the actions on the other.
7
+ *
8
+ * The shared-table board draws it three ways and they are one component
9
+ * (16-shared-table.html): the table overview's "Add item · Organise" pair
10
+ * over the bottom nav (T2), the add-item overlay's "Added items can be
11
+ * assigned afterwards." beside "Add 1 item" (T3), and the split sheet's lone
12
+ * "Save split" (T6). The booking flow draws the same band under its steps.
13
+ *
14
+ * Two rules the frames are consistent about:
15
+ * - The actions sit on the trailing edge at their own width, and `primary` is
16
+ * last. A `note` takes the left, so the qualifier is read before the commit.
17
+ * Only the table overview divides the bar between two equal buttons — that
18
+ * is `layout="split"`, not the default. (T5 and T6 draw a lone Save on the
19
+ * trailing edge; T6 and X10 draw Cancel + Save split right-aligned.)
20
+ * - The bar owns the home-indicator inset. `pb-[max(env(safe-area-inset-bottom),12px)]`
21
+ * is here and nowhere else, so no caller has to remember it.
22
+ *
23
+ * App-only offsets — bearmenu-fe's `--bottom-nav-offset`, `--page-gutter` —
24
+ * arrive through `className`. The design system does not know the app's
25
+ * chrome and must not guess at it.
26
+ */
27
+ export type ActionBarProps = Omit<React.HTMLAttributes<HTMLDivElement>, "children"> & {
28
+ /** The commit. Trailing edge, always last in the DOM. */
29
+ primary: React.ReactNode;
30
+ /** The way out — Cancel, Keep organising. Sits before the primary. */
31
+ secondary?: React.ReactNode;
32
+ /** The line that qualifies the action. Takes the left when present. */
33
+ note?: React.ReactNode;
34
+ /**
35
+ * `end` (the default) keeps the actions at their own width on the trailing
36
+ * edge. `split` divides the bar between them — the table overview's
37
+ * "Add item · Organise" pair, and nothing else the board draws.
38
+ */
39
+ layout?: "end" | "split";
40
+ /**
41
+ * Pins the bar to the bottom of its scroll container with the band's own
42
+ * ground and hairline. Off by default: inside an overlay the footer is
43
+ * already the last row of a flex column and does not need to stick.
44
+ */
45
+ sticky?: boolean;
46
+ };
47
+
48
+ const ActionBar = React.forwardRef<HTMLDivElement, ActionBarProps>(
49
+ ({ className, primary, secondary, note, layout = "end", sticky = false, ...props }, ref) => (
50
+ <div
51
+ ref={ref}
52
+ data-slot="action-bar"
53
+ className={cn(
54
+ "flex items-center gap-3 px-4 pt-3 pb-[max(env(safe-area-inset-bottom),12px)]",
55
+ sticky && "sticky bottom-0 z-10 border-t border-border bg-card",
56
+ className
57
+ )}
58
+ {...props}
59
+ >
60
+ {note ? (
61
+ <div data-slot="action-bar-note" className="min-w-0 flex-1 text-[12.5px] leading-normal text-muted-foreground">
62
+ {note}
63
+ </div>
64
+ ) : null}
65
+ <div
66
+ data-slot="action-bar-actions"
67
+ className={cn(
68
+ "flex items-center gap-3",
69
+ layout === "split" ? "flex-1 [&>*]:flex-1" : "ml-auto shrink-0"
70
+ )}
71
+ >
72
+ {secondary}
73
+ {primary}
74
+ </div>
75
+ </div>
76
+ )
77
+ );
78
+ ActionBar.displayName = "ActionBar";
79
+
80
+ export { ActionBar };
@@ -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
- <AvatarPrimitive.Root
44
- ref={ref}
45
- data-slot="avatar"
46
- className={cn(avatarVariants({ size: resolvedSize }), className)}
47
- {...props}
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
- <AvatarPrimitive.Fallback
72
- ref={ref}
73
- data-slot="avatar-fallback"
74
- className={cn(
75
- "flex size-full items-center justify-center rounded-full bg-muted font-semibold text-muted-foreground",
76
- className
77
- )}
78
- {...props}
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">
@@ -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
@@ -52,6 +52,16 @@ export type BookingRowProps = {
52
52
  href: string;
53
53
  /** The venue photo, rendered by the app; a quiet placeholder when absent. */
54
54
  thumbnail?: React.ReactNode;
55
+ /**
56
+ * One more chip about this booking, beside the status — "on this phone" for
57
+ * a booking the signed-in guest has not put in their account yet.
58
+ *
59
+ * A node rather than a string because the app owns what it says and how it
60
+ * is marked; the row only owns where it sits. It rides with the status on
61
+ * both layouts, so the two read as one cluster of facts about the booking
62
+ * rather than a second row of chrome.
63
+ */
64
+ note?: React.ReactNode;
55
65
  /** The next booking up — tinted on desktop. */
56
66
  highlighted?: boolean;
57
67
  linkComponent?: React.ComponentType<BookingRowLinkProps>;
@@ -84,6 +94,7 @@ export function BookingRow({
84
94
  code,
85
95
  href,
86
96
  thumbnail,
97
+ note,
87
98
  highlighted,
88
99
  linkComponent: LinkComponent = DefaultLink,
89
100
  className,
@@ -127,6 +138,7 @@ export function BookingRow({
127
138
  <div className="mt-2 flex flex-wrap items-center gap-[7px]">
128
139
  <span className="text-[12.5px] font-medium">{when}</span>
129
140
  <span className="text-xs text-muted-foreground">{party}</span>
141
+ {note}
130
142
  {statusPill}
131
143
  </div>
132
144
  </div>
@@ -142,6 +154,7 @@ export function BookingRow({
142
154
  </div>
143
155
  </div>
144
156
  <span className="hidden md:contents">
157
+ {note}
145
158
  {statusPill}
146
159
  {codeChip}
147
160
  <ChevronRight aria-hidden className="size-[15px] shrink-0 text-muted-foreground" />
@@ -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"