@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.
- 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.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.d.ts +11 -1
- package/dist/components/booking-row.js +4 -1
- package/dist/components/button.d.ts +2 -2
- package/dist/components/combobox.js +2 -2
- 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/filter-category-row.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 +3 -3
- 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 +2 -2
- package/dist/components/signature-chip.js +2 -2
- 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 +23 -207
- 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/booking-row.tsx +13 -0
- package/src/components/dialog.tsx +8 -2
- package/src/components/drawer.tsx +8 -2
- 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 +44 -0
- package/src/test/setup.ts +11 -0
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import * as React from "react";
|
|
4
|
+
import { cn } from "../lib/utils";
|
|
5
|
+
import { Checkbox } from "./checkbox";
|
|
6
|
+
import { CurrencyInput } from "./currency-input";
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* SplitShareRow — one person's share of a split item (16-shared-table.html,
|
|
10
|
+
* T6 and X10). Include them or leave them out, and in the custom mode set what
|
|
11
|
+
* they had.
|
|
12
|
+
*
|
|
13
|
+
* The board draws one row two ways and they are the same component: under md a
|
|
14
|
+
* full-width row with the check on the trailing edge, where a thumb is; from md
|
|
15
|
+
* a tile in a three-up grid with a square checkbox leading, where an eye scans
|
|
16
|
+
* a column. Both are CSS on one DOM — the order flips with `order`, the check's
|
|
17
|
+
* radius with a breakpoint — so there is no JS measurement and no flash.
|
|
18
|
+
*
|
|
19
|
+
* The amount sits in a soft pill under md whether or not it is editable, as the
|
|
20
|
+
* frames draw it; from md a fixed amount is plain text and only an editable one
|
|
21
|
+
* keeps the pill, plus a hairline and a focus ring, because on a wide screen
|
|
22
|
+
* nothing else would say "you can type here".
|
|
23
|
+
*
|
|
24
|
+
* The numeral is the bold sans and only the caller's currency suffix is mono,
|
|
25
|
+
* as the board draws money everywhere.
|
|
26
|
+
*
|
|
27
|
+
* The parent owns the grid (`grid md:grid-cols-3`) and the arithmetic. This row
|
|
28
|
+
* reports what it was given and never rounds: allocations are integer minor
|
|
29
|
+
* units and must sum to the line total exactly.
|
|
30
|
+
*/
|
|
31
|
+
export type SplitShareRowProps = Omit<
|
|
32
|
+
React.HTMLAttributes<HTMLDivElement>,
|
|
33
|
+
"onChange" | "children"
|
|
34
|
+
> & {
|
|
35
|
+
/** Avatar and name, composed by the caller — the design system holds no participant shape. */
|
|
36
|
+
person: React.ReactNode;
|
|
37
|
+
/** The share as the caller formatted it. Shown when `editable` is false. */
|
|
38
|
+
amount: React.ReactNode;
|
|
39
|
+
/** Swaps the amount for a `CurrencyInput`. The custom mode, and only it. */
|
|
40
|
+
editable?: boolean;
|
|
41
|
+
/** The editable value in major units, as `CurrencyInput` takes it. */
|
|
42
|
+
amountValue?: number | null;
|
|
43
|
+
onAmountChange?: (value: number | null) => void;
|
|
44
|
+
/** Passed through to `CurrencyInput` for the editable amount. */
|
|
45
|
+
currency?: string;
|
|
46
|
+
locale?: string;
|
|
47
|
+
/** Is this person in the split? Omit the handler for a row that cannot be excluded. */
|
|
48
|
+
checked?: boolean;
|
|
49
|
+
onCheckedChange?: (checked: boolean) => void;
|
|
50
|
+
/**
|
|
51
|
+
* The checkbox's accessible name — "Include Ana in the split". Injected;
|
|
52
|
+
* the design system holds no copy.
|
|
53
|
+
*/
|
|
54
|
+
includeLabel?: string;
|
|
55
|
+
/** The editable amount's accessible name — "Ana's share". */
|
|
56
|
+
amountLabel?: string;
|
|
57
|
+
disabled?: boolean;
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
const SplitShareRow = React.forwardRef<HTMLDivElement, SplitShareRowProps>(
|
|
61
|
+
(
|
|
62
|
+
{
|
|
63
|
+
className,
|
|
64
|
+
person,
|
|
65
|
+
amount,
|
|
66
|
+
editable = false,
|
|
67
|
+
amountValue = null,
|
|
68
|
+
onAmountChange,
|
|
69
|
+
currency = "lei",
|
|
70
|
+
locale,
|
|
71
|
+
checked,
|
|
72
|
+
onCheckedChange,
|
|
73
|
+
includeLabel,
|
|
74
|
+
amountLabel,
|
|
75
|
+
disabled = false,
|
|
76
|
+
...props
|
|
77
|
+
},
|
|
78
|
+
ref
|
|
79
|
+
) => {
|
|
80
|
+
const amountId = React.useId();
|
|
81
|
+
|
|
82
|
+
return (
|
|
83
|
+
<div
|
|
84
|
+
ref={ref}
|
|
85
|
+
data-slot="split-share-row"
|
|
86
|
+
data-checked={checked ? "" : undefined}
|
|
87
|
+
className={cn(
|
|
88
|
+
"flex items-center gap-3 rounded-xl border border-border bg-card px-3 py-2.5",
|
|
89
|
+
className
|
|
90
|
+
)}
|
|
91
|
+
{...props}
|
|
92
|
+
>
|
|
93
|
+
<div className="min-w-0 flex-1 order-2">{person}</div>
|
|
94
|
+
|
|
95
|
+
{editable ? (
|
|
96
|
+
<span className="order-3 shrink-0">
|
|
97
|
+
{/* `CurrencyInput` takes a closed prop set with no `aria-label`,
|
|
98
|
+
so the name arrives the HTML way. */}
|
|
99
|
+
<label htmlFor={amountId} className="sr-only">
|
|
100
|
+
{amountLabel}
|
|
101
|
+
</label>
|
|
102
|
+
<CurrencyInput
|
|
103
|
+
id={amountId}
|
|
104
|
+
value={amountValue}
|
|
105
|
+
onChange={(next) => onAmountChange?.(next)}
|
|
106
|
+
currency={currency}
|
|
107
|
+
locale={locale}
|
|
108
|
+
disabled={disabled}
|
|
109
|
+
// An editable amount has to look editable at both widths, which
|
|
110
|
+
// the soft pill alone does not do on a wide screen where the
|
|
111
|
+
// fixed amount is plain text: it takes a hairline and a focus ring.
|
|
112
|
+
className="h-9 w-24 rounded-lg border border-border bg-surface-2 text-right text-[13px] font-bold tabular-nums focus-within:ring-2 focus-within:ring-ring"
|
|
113
|
+
/>
|
|
114
|
+
</span>
|
|
115
|
+
) : (
|
|
116
|
+
<span
|
|
117
|
+
data-slot="split-share-amount"
|
|
118
|
+
className={cn(
|
|
119
|
+
"order-3 shrink-0 rounded-lg bg-surface-2 px-2.5 py-1 text-[13px] font-bold tabular-nums md:bg-transparent md:px-0",
|
|
120
|
+
// Only the amount goes quiet when someone is out of the split.
|
|
121
|
+
// Dimming the whole row takes the name under 4.5:1 with it.
|
|
122
|
+
checked === false && "text-muted-foreground"
|
|
123
|
+
)}
|
|
124
|
+
>
|
|
125
|
+
{amount}
|
|
126
|
+
</span>
|
|
127
|
+
)}
|
|
128
|
+
|
|
129
|
+
{onCheckedChange ? (
|
|
130
|
+
<Checkbox
|
|
131
|
+
checked={checked}
|
|
132
|
+
onCheckedChange={(next) => onCheckedChange(next === true)}
|
|
133
|
+
disabled={disabled}
|
|
134
|
+
aria-label={includeLabel}
|
|
135
|
+
// Last in the DOM because that is where the sheet paints it, so
|
|
136
|
+
// tab order follows the eye; `md:order-first` moves it to the
|
|
137
|
+
// leading edge for the dialog's three-up tiles. 44px either way.
|
|
138
|
+
className={cn(
|
|
139
|
+
"relative order-4 size-6 rounded-full md:order-1 md:size-5 md:rounded-md",
|
|
140
|
+
"before:absolute before:-inset-2.5 before:content-[''] md:before:-inset-3"
|
|
141
|
+
)}
|
|
142
|
+
/>
|
|
143
|
+
) : null}
|
|
144
|
+
</div>
|
|
145
|
+
);
|
|
146
|
+
}
|
|
147
|
+
);
|
|
148
|
+
SplitShareRow.displayName = "SplitShareRow";
|
|
149
|
+
|
|
150
|
+
export { SplitShareRow };
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from "@storybook/react";
|
|
2
|
+
import { Users } from "lucide-react";
|
|
3
|
+
import { Avatar, AvatarFallback, AvatarGroup } from "./avatar";
|
|
4
|
+
import { Badge } from "./badge";
|
|
5
|
+
import { TableLedgerCard } from "./table-ledger-card";
|
|
6
|
+
|
|
7
|
+
const meta: Meta<typeof TableLedgerCard> = {
|
|
8
|
+
title: "Shared table/TableLedgerCard",
|
|
9
|
+
component: TableLedgerCard,
|
|
10
|
+
tags: ["autodocs"],
|
|
11
|
+
parameters: {
|
|
12
|
+
docs: {
|
|
13
|
+
description: {
|
|
14
|
+
component:
|
|
15
|
+
"What one party at a shared table has against their name. One component for the person card, the Shared card, the card a remote write just changed, and the drop targets on Organise.",
|
|
16
|
+
},
|
|
17
|
+
},
|
|
18
|
+
},
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
export default meta;
|
|
22
|
+
type Story = StoryObj<typeof TableLedgerCard>;
|
|
23
|
+
|
|
24
|
+
const adrian = (
|
|
25
|
+
<Avatar size="sm" tone="brown">
|
|
26
|
+
<AvatarFallback>A</AvatarFallback>
|
|
27
|
+
</Avatar>
|
|
28
|
+
);
|
|
29
|
+
|
|
30
|
+
const rows = [
|
|
31
|
+
{ id: "1", label: "Ciorbă de burtă", amount: "32 lei" },
|
|
32
|
+
{ id: "2", label: "Zacuscă de casă", amount: "24 lei" },
|
|
33
|
+
];
|
|
34
|
+
|
|
35
|
+
/** T2 — a person and what they had. */
|
|
36
|
+
export const Person: Story = {
|
|
37
|
+
args: {
|
|
38
|
+
lead: adrian,
|
|
39
|
+
person: (
|
|
40
|
+
<>
|
|
41
|
+
Adrian <span className="font-normal text-muted-foreground">· host</span>
|
|
42
|
+
</>
|
|
43
|
+
),
|
|
44
|
+
meta: "2 items + share",
|
|
45
|
+
total: "96 lei",
|
|
46
|
+
rows,
|
|
47
|
+
labels: { rows: "Adrian's items" },
|
|
48
|
+
},
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
/** T2 — the Shared card: a glyph for a lead, and rows priced per head. */
|
|
52
|
+
export const Shared: Story = {
|
|
53
|
+
args: {
|
|
54
|
+
lead: (
|
|
55
|
+
<span className="grid size-8 place-items-center rounded-full bg-muted text-muted-foreground">
|
|
56
|
+
<Users className="size-4" aria-hidden />
|
|
57
|
+
</span>
|
|
58
|
+
),
|
|
59
|
+
person: "Shared between everyone",
|
|
60
|
+
total: "120 lei",
|
|
61
|
+
rows: [{ id: "w", label: "Fetească Neagră, Recaș", amount: "", trailing: "40 lei each" }],
|
|
62
|
+
},
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
/** X5 — someone else just added to it, marked in place rather than re-sorted. */
|
|
66
|
+
export const Updated: Story = {
|
|
67
|
+
args: {
|
|
68
|
+
state: "updated",
|
|
69
|
+
lead: (
|
|
70
|
+
<Avatar size="sm" tone="teal">
|
|
71
|
+
<AvatarFallback>B</AvatarFallback>
|
|
72
|
+
</Avatar>
|
|
73
|
+
),
|
|
74
|
+
person: "Bruno",
|
|
75
|
+
total: "150 lei",
|
|
76
|
+
rows: [
|
|
77
|
+
{
|
|
78
|
+
id: "t",
|
|
79
|
+
label: "Tochitură ardelenească",
|
|
80
|
+
amount: "54 lei",
|
|
81
|
+
badge: <Badge variant="pending">just now</Badge>,
|
|
82
|
+
},
|
|
83
|
+
],
|
|
84
|
+
},
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
/** T4 desktop — the drop targets, header only. */
|
|
88
|
+
export const DropTargets: Story = {
|
|
89
|
+
render: () => (
|
|
90
|
+
<div className="grid gap-2">
|
|
91
|
+
<TableLedgerCard state="drop-idle" lead={adrian} person="Adrian" meta="2 items" total="56 lei" />
|
|
92
|
+
<TableLedgerCard
|
|
93
|
+
state="drop-active"
|
|
94
|
+
lead={
|
|
95
|
+
<Avatar size="sm" tone="teal">
|
|
96
|
+
<AvatarFallback>B</AvatarFallback>
|
|
97
|
+
</Avatar>
|
|
98
|
+
}
|
|
99
|
+
person="Bruno"
|
|
100
|
+
meta="2 items"
|
|
101
|
+
total="150 lei"
|
|
102
|
+
/>
|
|
103
|
+
<TableLedgerCard
|
|
104
|
+
state="drop-idle"
|
|
105
|
+
lead={
|
|
106
|
+
<AvatarGroup size="sm" spacing="tight">
|
|
107
|
+
<Avatar tone="brown">
|
|
108
|
+
<AvatarFallback>A</AvatarFallback>
|
|
109
|
+
</Avatar>
|
|
110
|
+
<Avatar tone="teal">
|
|
111
|
+
<AvatarFallback>B</AvatarFallback>
|
|
112
|
+
</Avatar>
|
|
113
|
+
</AvatarGroup>
|
|
114
|
+
}
|
|
115
|
+
person="Shared"
|
|
116
|
+
meta="Split between everyone"
|
|
117
|
+
/>
|
|
118
|
+
</div>
|
|
119
|
+
),
|
|
120
|
+
};
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
import { cva, type VariantProps } from "class-variance-authority";
|
|
3
|
+
import { cn } from "../lib/utils";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* TableLedgerCard — what one party at a shared table has against their name.
|
|
7
|
+
*
|
|
8
|
+
* The board draws this shape four times and it is one component
|
|
9
|
+
* (16-shared-table.html):
|
|
10
|
+
* - T2, a person's card — avatar, "Adrian · host", "2 items + share", their
|
|
11
|
+
* total, and the items under it.
|
|
12
|
+
* - T2, the Shared card — the same card whose `lead` is a people glyph or an
|
|
13
|
+
* `AvatarGroup` instead of one avatar, and whose rows carry "40 lei each".
|
|
14
|
+
* - X5, the same card the moment someone else changed it: `updated` rings it
|
|
15
|
+
* and tints the header, so a remote write is marked in place and the ledger
|
|
16
|
+
* never silently re-sorts under a reader.
|
|
17
|
+
* - T4 desktop, the drop targets — the header alone, no rows, `drop-idle`
|
|
18
|
+
* until a drag is over it and `drop-active` while it is.
|
|
19
|
+
*
|
|
20
|
+
* Money slots are `font-bold tabular-nums`, so a column of totals lines up and
|
|
21
|
+
* does not shuffle when one changes. The board sets the NUMERAL in the bold
|
|
22
|
+
* sans and only the currency suffix in mono, small and muted — so a caller
|
|
23
|
+
* passes `96` followed by a `font-mono text-[11px] text-muted-foreground`
|
|
24
|
+
* "lei", and never wraps the whole amount in `font-mono`.
|
|
25
|
+
*
|
|
26
|
+
* Drag is an accelerator only. A card in `drop-active` must have a tap and
|
|
27
|
+
* keyboard path to the same outcome — every T4 row opens "Who had this?" —
|
|
28
|
+
* because a pointer that can drag is not a pointer everyone has.
|
|
29
|
+
*/
|
|
30
|
+
const tableLedgerCardVariants = cva(
|
|
31
|
+
"overflow-hidden rounded-2xl border bg-card transition-colors",
|
|
32
|
+
{
|
|
33
|
+
variants: {
|
|
34
|
+
state: {
|
|
35
|
+
default: "border-border",
|
|
36
|
+
updated: "border-primary",
|
|
37
|
+
"drop-idle": "border-dashed border-border",
|
|
38
|
+
"drop-active": "border-dashed border-primary bg-accent-subtle",
|
|
39
|
+
},
|
|
40
|
+
},
|
|
41
|
+
defaultVariants: { state: "default" },
|
|
42
|
+
}
|
|
43
|
+
);
|
|
44
|
+
|
|
45
|
+
export type TableLedgerRow = {
|
|
46
|
+
/** React key. Falls back to the index when the caller has no id. */
|
|
47
|
+
id?: string;
|
|
48
|
+
/** The dish or drink. */
|
|
49
|
+
label: React.ReactNode;
|
|
50
|
+
/** Its price, formatted by the caller. */
|
|
51
|
+
amount: React.ReactNode;
|
|
52
|
+
/** The quiet trailing line — "40 lei each" on the Shared card. Replaces `amount` when set. */
|
|
53
|
+
trailing?: React.ReactNode;
|
|
54
|
+
/** A chip on the row — "just now" on the X5 card, "Queued" on X6. */
|
|
55
|
+
badge?: React.ReactNode;
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
export type TableLedgerCardProps = Omit<React.HTMLAttributes<HTMLDivElement>, "children"> &
|
|
59
|
+
VariantProps<typeof tableLedgerCardVariants> & {
|
|
60
|
+
/** The leading slot: an `Avatar`, an `AvatarGroup`, or a glyph for "Shared". */
|
|
61
|
+
lead?: React.ReactNode;
|
|
62
|
+
/** The name — "Adrian · host", "Shared between everyone". */
|
|
63
|
+
person?: React.ReactNode;
|
|
64
|
+
/** The line under the name — "2 items + share". */
|
|
65
|
+
meta?: React.ReactNode;
|
|
66
|
+
/** Their running total, formatted by the caller. */
|
|
67
|
+
total?: React.ReactNode;
|
|
68
|
+
/** The items. Empty or omitted for a T4 drop target. */
|
|
69
|
+
rows?: TableLedgerRow[];
|
|
70
|
+
/** Accessible names. Injected; the design system holds no copy. */
|
|
71
|
+
labels?: { rows?: string };
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
const TableLedgerCard = React.forwardRef<HTMLDivElement, TableLedgerCardProps>(
|
|
75
|
+
({ className, state, lead, person, meta, total, rows, labels, ...props }, ref) => (
|
|
76
|
+
<div
|
|
77
|
+
ref={ref}
|
|
78
|
+
data-slot="table-ledger-card"
|
|
79
|
+
data-state={state ?? "default"}
|
|
80
|
+
className={cn(tableLedgerCardVariants({ state }), className)}
|
|
81
|
+
{...props}
|
|
82
|
+
>
|
|
83
|
+
<div
|
|
84
|
+
data-slot="table-ledger-header"
|
|
85
|
+
className={cn(
|
|
86
|
+
"flex items-center gap-3 px-3.5 py-3",
|
|
87
|
+
// The header takes its own ground only when there are items under it,
|
|
88
|
+
// so it reads as their heading. A card with no rows — a drop target,
|
|
89
|
+
// a person who has had nothing yet — is one plain block, and tinting
|
|
90
|
+
// it would also paint over `drop-active`'s accent.
|
|
91
|
+
rows?.length
|
|
92
|
+
? state === "updated"
|
|
93
|
+
? "bg-accent-subtle"
|
|
94
|
+
: "bg-surface-2/60"
|
|
95
|
+
: undefined
|
|
96
|
+
)}
|
|
97
|
+
>
|
|
98
|
+
{lead ? <div className="shrink-0">{lead}</div> : null}
|
|
99
|
+
<div className="min-w-0 flex-1">
|
|
100
|
+
{person ? <div className="truncate text-[15px] font-bold leading-tight">{person}</div> : null}
|
|
101
|
+
{meta ? <div className="mt-0.5 truncate text-[12.5px] text-muted-foreground">{meta}</div> : null}
|
|
102
|
+
</div>
|
|
103
|
+
{total ? (
|
|
104
|
+
<div
|
|
105
|
+
data-slot="table-ledger-total"
|
|
106
|
+
className="shrink-0 text-[15px] font-bold tabular-nums"
|
|
107
|
+
>
|
|
108
|
+
{total}
|
|
109
|
+
</div>
|
|
110
|
+
) : null}
|
|
111
|
+
</div>
|
|
112
|
+
|
|
113
|
+
{rows && rows.length > 0 ? (
|
|
114
|
+
<ul data-slot="table-ledger-rows" aria-label={labels?.rows} className="divide-y divide-border">
|
|
115
|
+
{rows.map((row, index) => (
|
|
116
|
+
<li
|
|
117
|
+
key={row.id ?? index}
|
|
118
|
+
className="flex items-center gap-3 px-3.5 py-2.5 text-[14px]"
|
|
119
|
+
>
|
|
120
|
+
<span className="min-w-0 flex-1 truncate">{row.label}</span>
|
|
121
|
+
{row.badge}
|
|
122
|
+
{row.trailing ? (
|
|
123
|
+
<span className="shrink-0 text-[12.5px] text-muted-foreground">{row.trailing}</span>
|
|
124
|
+
) : (
|
|
125
|
+
<span className="shrink-0 text-[14px] font-bold tabular-nums">
|
|
126
|
+
{row.amount}
|
|
127
|
+
</span>
|
|
128
|
+
)}
|
|
129
|
+
</li>
|
|
130
|
+
))}
|
|
131
|
+
</ul>
|
|
132
|
+
) : null}
|
|
133
|
+
</div>
|
|
134
|
+
)
|
|
135
|
+
);
|
|
136
|
+
TableLedgerCard.displayName = "TableLedgerCard";
|
|
137
|
+
|
|
138
|
+
export { TableLedgerCard, tableLedgerCardVariants };
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from "@storybook/react";
|
|
2
|
+
import { Avatar, AvatarFallback, AvatarGroup } from "./avatar";
|
|
3
|
+
import { Button } from "./button";
|
|
4
|
+
import { TableTriggerBand } from "./table-trigger-band";
|
|
5
|
+
|
|
6
|
+
const meta: Meta<typeof TableTriggerBand> = {
|
|
7
|
+
title: "Shared table/TableTriggerBand",
|
|
8
|
+
component: TableTriggerBand,
|
|
9
|
+
tags: ["autodocs"],
|
|
10
|
+
parameters: {
|
|
11
|
+
docs: {
|
|
12
|
+
description: {
|
|
13
|
+
component:
|
|
14
|
+
"The way back to a live table from the venue's own pages. One target at both widths: a chevron under md, the action inside the link from md — a real button nested in a link would give a screen reader two controls where a reader sees one.",
|
|
15
|
+
},
|
|
16
|
+
},
|
|
17
|
+
},
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
export default meta;
|
|
21
|
+
type Story = StoryObj<typeof TableTriggerBand>;
|
|
22
|
+
|
|
23
|
+
const stack = (
|
|
24
|
+
<AvatarGroup size="sm" spacing="tight">
|
|
25
|
+
<Avatar tone="brown">
|
|
26
|
+
<AvatarFallback>A</AvatarFallback>
|
|
27
|
+
</Avatar>
|
|
28
|
+
<Avatar tone="teal">
|
|
29
|
+
<AvatarFallback>B</AvatarFallback>
|
|
30
|
+
</Avatar>
|
|
31
|
+
<Avatar tone="green">
|
|
32
|
+
<AvatarFallback>A</AvatarFallback>
|
|
33
|
+
</Avatar>
|
|
34
|
+
</AvatarGroup>
|
|
35
|
+
);
|
|
36
|
+
|
|
37
|
+
/** T0c — a session is live on this device. */
|
|
38
|
+
export const AtTheTable: Story = {
|
|
39
|
+
args: {
|
|
40
|
+
href: "/t/8f3c1a",
|
|
41
|
+
people: stack,
|
|
42
|
+
title: "You're at Table 18",
|
|
43
|
+
meta: "Adrian, Bruno and you · opened 19:42 · 367 lei so far",
|
|
44
|
+
// `asChild`: the band is the link, so its action must not be a real
|
|
45
|
+
// <button> nested inside an <a>.
|
|
46
|
+
action: (
|
|
47
|
+
<Button asChild variant="cta">
|
|
48
|
+
<span>Open the table</span>
|
|
49
|
+
</Button>
|
|
50
|
+
),
|
|
51
|
+
},
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
/** T0e — none is, so the band offers to start one. No naming step. */
|
|
55
|
+
export const StartATable: Story = {
|
|
56
|
+
args: {
|
|
57
|
+
href: "/t/new",
|
|
58
|
+
title: "Start a table",
|
|
59
|
+
meta: "Track who had what, then invite the others",
|
|
60
|
+
action: (
|
|
61
|
+
<Button asChild variant="cta">
|
|
62
|
+
<span>Start a table</span>
|
|
63
|
+
</Button>
|
|
64
|
+
),
|
|
65
|
+
},
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
/** Without an action the chevron carries both widths. */
|
|
69
|
+
export const ChevronOnly: Story = {
|
|
70
|
+
args: {
|
|
71
|
+
href: "/t/8f3c1a",
|
|
72
|
+
people: stack,
|
|
73
|
+
title: "You're at Table 18",
|
|
74
|
+
meta: "3 people · 367 lei so far",
|
|
75
|
+
},
|
|
76
|
+
};
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
import { ChevronRight } from "lucide-react";
|
|
3
|
+
import { cn } from "../lib/utils";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* TableTriggerBand — the way back to a live shared table from the venue's own
|
|
7
|
+
* pages: "You're at Table 18 · 3 people · 367 lei so far" (16-shared-table.html
|
|
8
|
+
* T0c), and the same band offering to start one when none is live (T0e).
|
|
9
|
+
*
|
|
10
|
+
* It is one target, not a card with a button in it. The frames draw a chevron
|
|
11
|
+
* under md and an "Open the table" button from md, but both mean "this band
|
|
12
|
+
* goes to the table", so the band is the link at both widths and `action`
|
|
13
|
+
* renders inside it as the wide screen's affordance — a span that looks like a
|
|
14
|
+
* button, `aria-hidden`, because the link already carries the name. A real
|
|
15
|
+
* button nested in a link is invalid HTML and gives a screen reader two
|
|
16
|
+
* controls where a reader sees one.
|
|
17
|
+
*
|
|
18
|
+
* `linkComponent` takes the app's router link (`next/link`); it falls back to
|
|
19
|
+
* `a`, so the design system imports no router.
|
|
20
|
+
*/
|
|
21
|
+
export type TableTriggerBandProps = {
|
|
22
|
+
/** "You're at Table 18", "Start a table". */
|
|
23
|
+
title: React.ReactNode;
|
|
24
|
+
/** "3 people · 367 lei so far"; desktop draws the longer "Adrian, Bruno and you · opened 19:42". */
|
|
25
|
+
meta?: React.ReactNode;
|
|
26
|
+
/** The avatar stack — an `AvatarGroup`, or nothing when no table is live. */
|
|
27
|
+
people?: React.ReactNode;
|
|
28
|
+
/**
|
|
29
|
+
* The wide screen's call to action. Decorative: the band is the control, so
|
|
30
|
+
* this must NOT be a real `button` — a button inside an anchor is invalid
|
|
31
|
+
* HTML. Pass `<Button asChild><span>…</span></Button>`.
|
|
32
|
+
*/
|
|
33
|
+
action?: React.ReactNode;
|
|
34
|
+
href: string;
|
|
35
|
+
/** The app's link component. Defaults to `a`. */
|
|
36
|
+
linkComponent?: React.ElementType;
|
|
37
|
+
className?: string;
|
|
38
|
+
} & Omit<React.AnchorHTMLAttributes<HTMLAnchorElement>, "href" | "title" | "action">;
|
|
39
|
+
|
|
40
|
+
const TableTriggerBand = React.forwardRef<HTMLAnchorElement, TableTriggerBandProps>(
|
|
41
|
+
({ title, meta, people, action, href, linkComponent, className, ...props }, ref) => {
|
|
42
|
+
const Link = linkComponent ?? "a";
|
|
43
|
+
|
|
44
|
+
return (
|
|
45
|
+
<Link
|
|
46
|
+
ref={ref}
|
|
47
|
+
href={href}
|
|
48
|
+
data-slot="table-trigger-band"
|
|
49
|
+
className={cn(
|
|
50
|
+
"flex items-center gap-3 rounded-2xl border border-primary bg-card px-3.5 py-3 transition-colors hover:bg-accent-subtle focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2",
|
|
51
|
+
className
|
|
52
|
+
)}
|
|
53
|
+
{...props}
|
|
54
|
+
>
|
|
55
|
+
{people ? <div className="shrink-0">{people}</div> : null}
|
|
56
|
+
<div className="min-w-0 flex-1">
|
|
57
|
+
<div className="truncate text-[15px] font-bold leading-tight">{title}</div>
|
|
58
|
+
{meta ? <div className="mt-0.5 truncate text-[12.5px] text-muted-foreground">{meta}</div> : null}
|
|
59
|
+
</div>
|
|
60
|
+
{action ? (
|
|
61
|
+
<span data-slot="table-trigger-action" aria-hidden className="hidden shrink-0 md:block">
|
|
62
|
+
{action}
|
|
63
|
+
</span>
|
|
64
|
+
) : null}
|
|
65
|
+
<ChevronRight
|
|
66
|
+
aria-hidden
|
|
67
|
+
className={cn("size-5 shrink-0 text-muted-foreground", action && "md:hidden")}
|
|
68
|
+
/>
|
|
69
|
+
</Link>
|
|
70
|
+
);
|
|
71
|
+
}
|
|
72
|
+
);
|
|
73
|
+
TableTriggerBand.displayName = "TableTriggerBand";
|
|
74
|
+
|
|
75
|
+
export { TableTriggerBand };
|
package/src/globals.css
CHANGED
|
@@ -190,6 +190,31 @@ h6 {
|
|
|
190
190
|
--brown: oklch(0.31 0.05 45);
|
|
191
191
|
--brown-foreground: oklch(0.985 0 0);
|
|
192
192
|
|
|
193
|
+
/* Avatar tones — one fixed hue per person at a shared table, so the same
|
|
194
|
+
face reads the same on the ledger card, the organise chip, the split row
|
|
195
|
+
and the activity feed. Sampled from the shared-table board
|
|
196
|
+
(16-shared-table.html): #914F2F, #006469, #196632. All three sit near
|
|
197
|
+
L 0.45–0.50, which carries a white initial at 6.2:1, 6.9:1 and 7.0:1 —
|
|
198
|
+
AA with room, because the initial is small and semibold.
|
|
199
|
+
|
|
200
|
+
They are NOT `--primary` / `--success`: those move with the brand and
|
|
201
|
+
with status, and a person's colour must not. `avatar-brown` is deliberately
|
|
202
|
+
lighter than `--brown` so a disc separates from a brown control behind it. */
|
|
203
|
+
--avatar-brown: oklch(0.5 0.1 45.5);
|
|
204
|
+
--avatar-teal: oklch(0.458 0.078 201);
|
|
205
|
+
--avatar-green: oklch(0.451 0.111 150);
|
|
206
|
+
/* The board draws three people; a table can seat more. The drawn hues sit at
|
|
207
|
+
45.5°, 150° and 201°, leaving the 205°–405° arc free, so the next three are
|
|
208
|
+
spaced across it at the same step the teal/green pair uses. White initials
|
|
209
|
+
measure 6.57 / 7.30 / 7.12:1 here. Assign in this order: rose (357°) then
|
|
210
|
+
lands furthest from brown (45.5°) in join order. Past six, cycle — at a
|
|
211
|
+
table of seven the initial and the name are doing the identifying, and
|
|
212
|
+
colour is a support cue, never the only one, which is also the answer to
|
|
213
|
+
teal and green reading alike under deuteranopia. */
|
|
214
|
+
--avatar-blue: oklch(0.48 0.13 253);
|
|
215
|
+
--avatar-violet: oklch(0.47 0.14 305);
|
|
216
|
+
--avatar-rose: oklch(0.48 0.15 357);
|
|
217
|
+
|
|
193
218
|
/* Shadow tokens — color + per-step alpha (themed in .dark) */
|
|
194
219
|
--shadow-color: 0 0% 0%;
|
|
195
220
|
--shadow-alpha-2xs: 0.05;
|
|
@@ -359,6 +384,18 @@ h6 {
|
|
|
359
384
|
--brown: oklch(0.31 0.05 45);
|
|
360
385
|
--brown-foreground: oklch(0.985 0 0);
|
|
361
386
|
|
|
387
|
+
/* Avatar tones, lifted one notch. The light values sit at 2.6–2.9:1 against
|
|
388
|
+
a 0.205 card, so a disc has no edge in dark; +0.06 lightness buys 3.3–3.7:1
|
|
389
|
+
(AA non-text) while the white initial still holds 4.8–5.4:1 (AA text).
|
|
390
|
+
Lifting further trades the initial away for the edge — this is the ceiling. */
|
|
391
|
+
--avatar-brown: oklch(0.56 0.1 45.5);
|
|
392
|
+
--avatar-teal: oklch(0.518 0.078 201);
|
|
393
|
+
--avatar-green: oklch(0.511 0.111 150);
|
|
394
|
+
/* Same +0.06 lift: disc-vs-card 3.54 / 3.19 / 3.26:1, white at 5.07 / 5.63 / 5.50:1. */
|
|
395
|
+
--avatar-blue: oklch(0.54 0.13 253);
|
|
396
|
+
--avatar-violet: oklch(0.53 0.14 305);
|
|
397
|
+
--avatar-rose: oklch(0.54 0.15 357);
|
|
398
|
+
|
|
362
399
|
/* Heavier shadow alphas — default Tailwind tints disappear on dark surfaces */
|
|
363
400
|
--shadow-alpha-2xs: 0.25;
|
|
364
401
|
--shadow-alpha-xs: 0.3;
|
|
@@ -424,6 +461,13 @@ h6 {
|
|
|
424
461
|
--color-chart-5: var(--chart-5);
|
|
425
462
|
--color-brown: var(--brown);
|
|
426
463
|
--color-brown-foreground: var(--brown-foreground);
|
|
464
|
+
/* `bg-avatar-brown` … — a person's fixed hue at a shared table. */
|
|
465
|
+
--color-avatar-brown: var(--avatar-brown);
|
|
466
|
+
--color-avatar-teal: var(--avatar-teal);
|
|
467
|
+
--color-avatar-green: var(--avatar-green);
|
|
468
|
+
--color-avatar-blue: var(--avatar-blue);
|
|
469
|
+
--color-avatar-violet: var(--avatar-violet);
|
|
470
|
+
--color-avatar-rose: var(--avatar-rose);
|
|
427
471
|
|
|
428
472
|
/* Radius scale */
|
|
429
473
|
--radius-sm: calc(var(--radius) - 4px);
|
package/src/test/setup.ts
CHANGED
|
@@ -11,3 +11,14 @@ globalThis.ResizeObserver = class ResizeObserver {
|
|
|
11
11
|
};
|
|
12
12
|
|
|
13
13
|
window.HTMLElement.prototype.scrollIntoView = function () {};
|
|
14
|
+
|
|
15
|
+
// jsdom implements no Pointer Events capture API. vaul calls
|
|
16
|
+
// `setPointerCapture` on the first pointerdown inside a Drawer, so without
|
|
17
|
+
// these any test that clicks inside a sheet fails with an unhandled
|
|
18
|
+
// TypeError — and the failure lands on whichever test ran last, not on the
|
|
19
|
+
// one that caused it.
|
|
20
|
+
window.HTMLElement.prototype.setPointerCapture = function () {};
|
|
21
|
+
window.HTMLElement.prototype.releasePointerCapture = function () {};
|
|
22
|
+
window.HTMLElement.prototype.hasPointerCapture = function () {
|
|
23
|
+
return false;
|
|
24
|
+
};
|