@sixthshift/design-system 0.6.0 → 0.8.0
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/components/Badge/Badge.d.ts +10 -2
- package/dist/components/Badge/Badge.d.ts.map +1 -1
- package/dist/components/Badge/Badge.js +6 -4
- package/dist/components/Badge/Badge.js.map +1 -1
- package/dist/components/Button/Button.d.ts +25 -2
- package/dist/components/Button/Button.d.ts.map +1 -1
- package/dist/components/Button/Button.js +13 -4
- package/dist/components/Button/Button.js.map +1 -1
- package/dist/components/DataTable/DataTable.d.ts +89 -0
- package/dist/components/DataTable/DataTable.d.ts.map +1 -0
- package/dist/components/DataTable/DataTable.js +107 -0
- package/dist/components/DataTable/DataTable.js.map +1 -0
- package/dist/components/DataTable/index.d.ts +2 -0
- package/dist/components/DataTable/index.d.ts.map +1 -0
- package/dist/components/DataTable/index.js +2 -0
- package/dist/components/DataTable/index.js.map +1 -0
- package/dist/components/DatePicker/DatePicker.d.ts.map +1 -1
- package/dist/components/DatePicker/DatePicker.js +2 -1
- package/dist/components/DatePicker/DatePicker.js.map +1 -1
- package/dist/components/DateTimePicker/DateTimePicker.d.ts.map +1 -1
- package/dist/components/DateTimePicker/DateTimePicker.js +2 -1
- package/dist/components/DateTimePicker/DateTimePicker.js.map +1 -1
- package/dist/components/DateTimeRangePicker/DateTimeRangePicker.d.ts.map +1 -1
- package/dist/components/DateTimeRangePicker/DateTimeRangePicker.js +2 -1
- package/dist/components/DateTimeRangePicker/DateTimeRangePicker.js.map +1 -1
- package/dist/components/FileDropzone/FileDropzone.d.ts +87 -0
- package/dist/components/FileDropzone/FileDropzone.d.ts.map +1 -0
- package/dist/components/FileDropzone/FileDropzone.js +146 -0
- package/dist/components/FileDropzone/FileDropzone.js.map +1 -0
- package/dist/components/FileDropzone/index.d.ts +2 -0
- package/dist/components/FileDropzone/index.d.ts.map +1 -0
- package/dist/components/FileDropzone/index.js +2 -0
- package/dist/components/FileDropzone/index.js.map +1 -0
- package/dist/components/HoverCard/HoverCard.d.ts.map +1 -1
- package/dist/components/HoverCard/HoverCard.js +3 -0
- package/dist/components/HoverCard/HoverCard.js.map +1 -1
- package/dist/components/HoverCard/components/HoverCardContent.d.ts +16 -2
- package/dist/components/HoverCard/components/HoverCardContent.d.ts.map +1 -1
- package/dist/components/HoverCard/components/HoverCardContent.js +3 -3
- package/dist/components/HoverCard/components/HoverCardContent.js.map +1 -1
- package/dist/components/HoverCard/components/HoverCardContext.d.ts +2 -0
- package/dist/components/HoverCard/components/HoverCardContext.d.ts.map +1 -1
- package/dist/components/HoverCard/components/HoverCardContext.js.map +1 -1
- package/dist/components/ImageGallery/ImageGallery.d.ts +93 -0
- package/dist/components/ImageGallery/ImageGallery.d.ts.map +1 -0
- package/dist/components/ImageGallery/ImageGallery.js +121 -0
- package/dist/components/ImageGallery/ImageGallery.js.map +1 -0
- package/dist/components/ImageGallery/index.d.ts +2 -0
- package/dist/components/ImageGallery/index.d.ts.map +1 -0
- package/dist/components/ImageGallery/index.js +2 -0
- package/dist/components/ImageGallery/index.js.map +1 -0
- package/dist/components/Message/Message.d.ts +4 -0
- package/dist/components/Message/Message.d.ts.map +1 -1
- package/dist/components/Message/Message.js +2 -2
- package/dist/components/Message/Message.js.map +1 -1
- package/dist/components/Modal/Modal.d.ts +12 -0
- package/dist/components/Modal/Modal.d.ts.map +1 -1
- package/dist/components/Modal/Modal.js +5 -2
- package/dist/components/Modal/Modal.js.map +1 -1
- package/dist/components/NumberStepper/NumberStepper.d.ts +78 -0
- package/dist/components/NumberStepper/NumberStepper.d.ts.map +1 -0
- package/dist/components/NumberStepper/NumberStepper.js +129 -0
- package/dist/components/NumberStepper/NumberStepper.js.map +1 -0
- package/dist/components/NumberStepper/index.d.ts +2 -0
- package/dist/components/NumberStepper/index.d.ts.map +1 -0
- package/dist/components/NumberStepper/index.js +2 -0
- package/dist/components/NumberStepper/index.js.map +1 -0
- package/dist/components/Popover/Popover.d.ts +3 -1
- package/dist/components/Popover/Popover.d.ts.map +1 -1
- package/dist/components/Popover/Popover.js +3 -0
- package/dist/components/Popover/Popover.js.map +1 -1
- package/dist/components/Popover/components/PopoverBody.d.ts +16 -2
- package/dist/components/Popover/components/PopoverBody.d.ts.map +1 -1
- package/dist/components/Popover/components/PopoverBody.js +3 -3
- package/dist/components/Popover/components/PopoverBody.js.map +1 -1
- package/dist/components/Popover/components/PopoverContext.d.ts +2 -0
- package/dist/components/Popover/components/PopoverContext.d.ts.map +1 -1
- package/dist/components/Popover/components/PopoverContext.js.map +1 -1
- package/dist/components/Select/Select.d.ts.map +1 -1
- package/dist/components/Select/Select.js +3 -2
- package/dist/components/Select/Select.js.map +1 -1
- package/dist/components/Select/SelectDropdown.d.ts +3 -1
- package/dist/components/Select/SelectDropdown.d.ts.map +1 -1
- package/dist/components/Select/SelectDropdown.js +2 -2
- package/dist/components/Select/SelectDropdown.js.map +1 -1
- package/dist/components/Sheet/Sheet.d.ts +12 -0
- package/dist/components/Sheet/Sheet.d.ts.map +1 -1
- package/dist/components/Sheet/Sheet.js +4 -2
- package/dist/components/Sheet/Sheet.js.map +1 -1
- package/dist/components/Steps/Steps.d.ts +65 -0
- package/dist/components/Steps/Steps.d.ts.map +1 -0
- package/dist/components/Steps/Steps.js +62 -0
- package/dist/components/Steps/Steps.js.map +1 -0
- package/dist/components/Steps/index.d.ts +2 -0
- package/dist/components/Steps/index.d.ts.map +1 -0
- package/dist/components/Steps/index.js +2 -0
- package/dist/components/Steps/index.js.map +1 -0
- package/dist/components/TimePicker/TimePicker.d.ts.map +1 -1
- package/dist/components/TimePicker/TimePicker.js +2 -1
- package/dist/components/TimePicker/TimePicker.js.map +1 -1
- package/dist/components/Toast/Toast.d.ts.map +1 -1
- package/dist/components/Toast/Toast.js +2 -2
- package/dist/components/Toast/Toast.js.map +1 -1
- package/dist/components/Toggle/Toggle.d.ts +4 -0
- package/dist/components/Toggle/Toggle.d.ts.map +1 -1
- package/dist/components/Toggle/Toggle.js +2 -2
- package/dist/components/Toggle/Toggle.js.map +1 -1
- package/dist/components/ToggleGroup/ToggleGroupMultiple.d.ts +1 -0
- package/dist/components/ToggleGroup/ToggleGroupMultiple.d.ts.map +1 -1
- package/dist/components/ToggleGroup/ToggleGroupMultiple.js +2 -2
- package/dist/components/ToggleGroup/ToggleGroupMultiple.js.map +1 -1
- package/dist/components/ToggleGroup/ToggleGroupSingle.d.ts +1 -0
- package/dist/components/ToggleGroup/ToggleGroupSingle.d.ts.map +1 -1
- package/dist/components/ToggleGroup/ToggleGroupSingle.js +2 -2
- package/dist/components/ToggleGroup/ToggleGroupSingle.js.map +1 -1
- package/dist/components/ToggleGroup/toggleGroup.types.d.ts +6 -0
- package/dist/components/ToggleGroup/toggleGroup.types.d.ts.map +1 -1
- package/dist/components/Tooltip/Tooltip.d.ts +3 -1
- package/dist/components/Tooltip/Tooltip.d.ts.map +1 -1
- package/dist/components/Tooltip/Tooltip.js +3 -0
- package/dist/components/Tooltip/Tooltip.js.map +1 -1
- package/dist/components/Tooltip/components/TooltipBody.d.ts +16 -2
- package/dist/components/Tooltip/components/TooltipBody.d.ts.map +1 -1
- package/dist/components/Tooltip/components/TooltipBody.js +3 -3
- package/dist/components/Tooltip/components/TooltipBody.js.map +1 -1
- package/dist/components/Tooltip/components/TooltipContext.d.ts +2 -0
- package/dist/components/Tooltip/components/TooltipContext.d.ts.map +1 -1
- package/dist/components/Tooltip/components/TooltipContext.js.map +1 -1
- package/dist/internal/brandScope.d.ts +25 -0
- package/dist/internal/brandScope.d.ts.map +1 -0
- package/dist/internal/brandScope.js +37 -0
- package/dist/internal/brandScope.js.map +1 -0
- package/package.json +21 -1
- package/src/components/Badge/Badge.tsx +10 -4
- package/src/components/Button/Button.tsx +24 -4
- package/src/components/DataTable/DataTable.tsx +261 -0
- package/src/components/DataTable/data-table.recipe.css +28 -0
- package/src/components/DataTable/index.ts +10 -0
- package/src/components/DatePicker/DatePicker.tsx +3 -0
- package/src/components/DateTimePicker/DateTimePicker.tsx +3 -0
- package/src/components/DateTimeRangePicker/DateTimeRangePicker.tsx +3 -0
- package/src/components/FileDropzone/FileDropzone.tsx +239 -0
- package/src/components/FileDropzone/file-dropzone.recipe.css +38 -0
- package/src/components/FileDropzone/index.ts +1 -0
- package/src/components/HoverCard/HoverCard.tsx +3 -0
- package/src/components/HoverCard/components/HoverCardContent.tsx +11 -3
- package/src/components/HoverCard/components/HoverCardContext.tsx +2 -0
- package/src/components/ImageGallery/ImageGallery.tsx +285 -0
- package/src/components/ImageGallery/image-gallery.recipe.css +50 -0
- package/src/components/ImageGallery/index.ts +7 -0
- package/src/components/Message/Message.tsx +26 -22
- package/src/components/Modal/Modal.tsx +11 -0
- package/src/components/NumberStepper/NumberStepper.tsx +227 -0
- package/src/components/NumberStepper/index.ts +1 -0
- package/src/components/NumberStepper/number-stepper.recipe.css +31 -0
- package/src/components/Popover/Popover.tsx +3 -0
- package/src/components/Popover/components/PopoverBody.tsx +11 -3
- package/src/components/Popover/components/PopoverContext.tsx +2 -0
- package/src/components/Select/Select.tsx +3 -1
- package/src/components/Select/SelectDropdown.tsx +4 -0
- package/src/components/Sheet/Sheet.tsx +10 -0
- package/src/components/Steps/Steps.tsx +149 -0
- package/src/components/Steps/index.ts +1 -0
- package/src/components/Steps/steps.recipe.css +51 -0
- package/src/components/TimePicker/TimePicker.tsx +3 -0
- package/src/components/Toast/Toast.tsx +5 -1
- package/src/components/Toggle/Toggle.tsx +17 -2
- package/src/components/ToggleGroup/ToggleGroupMultiple.tsx +2 -1
- package/src/components/ToggleGroup/ToggleGroupSingle.tsx +2 -0
- package/src/components/ToggleGroup/toggleGroup.types.ts +6 -0
- package/src/components/Tooltip/Tooltip.tsx +3 -0
- package/src/components/Tooltip/components/TooltipBody.tsx +11 -3
- package/src/components/Tooltip/components/TooltipContext.tsx +2 -0
- package/src/internal/brandScope.ts +39 -0
- package/src/theming/tailwind.css +5 -0
|
@@ -55,23 +55,29 @@ export type BadgeIntent = Loose<BadgeIntentName>;
|
|
|
55
55
|
export type BadgeRecipeProps = VariantProps<typeof badgeVariants> & {
|
|
56
56
|
variant?: BadgeVariant | undefined;
|
|
57
57
|
intent?: BadgeIntent | undefined;
|
|
58
|
+
/** Re-points this element's brand tokens to a brand the theme defines. See the Scoped brands docs. */
|
|
59
|
+
brand?: string | undefined;
|
|
58
60
|
className?: string | undefined;
|
|
59
61
|
};
|
|
60
62
|
|
|
61
63
|
/**
|
|
62
|
-
* The class string plus the
|
|
64
|
+
* The class string plus the attributes the recipe selects on. `data-brand` is
|
|
65
|
+
* omitted unless `brand` is set, same as `buttonRecipe`.
|
|
63
66
|
*/
|
|
64
|
-
export function badgeRecipe({ variant = "solid", intent = "brand", className }: BadgeRecipeProps) {
|
|
67
|
+
export function badgeRecipe({ variant = "solid", intent = "brand", brand, className }: BadgeRecipeProps) {
|
|
65
68
|
return {
|
|
66
69
|
className: cn(variantStructure[variant], badgeVariants({ className })),
|
|
67
70
|
"data-variant": variant,
|
|
68
71
|
"data-intent": intent,
|
|
72
|
+
"data-brand": brand,
|
|
69
73
|
};
|
|
70
74
|
}
|
|
71
75
|
|
|
72
76
|
export type BadgeProps = React.HTMLAttributes<HTMLSpanElement> & {
|
|
73
77
|
variant?: BadgeVariant | undefined;
|
|
74
78
|
intent?: BadgeIntent | undefined;
|
|
79
|
+
/** Name of a brand the theme defines, rendered as `data-brand`. Omitted, the badge follows the nearest `data-brand` ancestor. */
|
|
80
|
+
brand?: string | undefined;
|
|
75
81
|
};
|
|
76
82
|
|
|
77
83
|
/**
|
|
@@ -89,8 +95,8 @@ export type BadgeProps = React.HTMLAttributes<HTMLSpanElement> & {
|
|
|
89
95
|
* same seam Button uses, so a consumer can re-point a cell without a
|
|
90
96
|
* release.
|
|
91
97
|
*/
|
|
92
|
-
export const Badge = React.forwardRef<HTMLSpanElement, BadgeProps>(({ className, variant = "solid", intent = "brand", ...props }, ref) => {
|
|
93
|
-
return <span ref={ref} {...badgeRecipe({ variant, intent, className })} {...props} />;
|
|
98
|
+
export const Badge = React.forwardRef<HTMLSpanElement, BadgeProps>(({ className, variant = "solid", intent = "brand", brand, ...props }, ref) => {
|
|
99
|
+
return <span ref={ref} {...badgeRecipe({ variant, intent, brand, className })} {...props} />;
|
|
94
100
|
});
|
|
95
101
|
Badge.displayName = "Badge";
|
|
96
102
|
|
|
@@ -86,18 +86,23 @@ export type ButtonIntent = Loose<ButtonIntentName>;
|
|
|
86
86
|
export type ButtonRecipeProps = VariantProps<typeof buttonVariants> & {
|
|
87
87
|
variant?: ButtonVariant | undefined;
|
|
88
88
|
intent?: ButtonIntent | undefined;
|
|
89
|
+
/** Re-points this element's brand tokens to a brand the theme defines. See the Scoped brands docs. */
|
|
90
|
+
brand?: string | undefined;
|
|
89
91
|
iconOnly?: boolean | undefined;
|
|
90
92
|
className?: string | undefined;
|
|
91
93
|
};
|
|
92
94
|
|
|
93
95
|
/**
|
|
94
|
-
* The class string plus the
|
|
96
|
+
* The class string plus the attributes the recipe selects on.
|
|
97
|
+
*
|
|
98
|
+
* `data-brand` is only emitted when `brand` is set: `undefined` makes React
|
|
99
|
+
* omit the attribute, so the element takes its brand from its surroundings.
|
|
95
100
|
*
|
|
96
101
|
* Shared with Toggle and ToggleGroupItem, which are built on Button's look. The
|
|
97
102
|
* data attributes are half the contract now, so anything reusing that look has
|
|
98
103
|
* to emit them as well or it lands on the recipe's floor instead of a cell.
|
|
99
104
|
*/
|
|
100
|
-
export function buttonRecipe({ variant = "solid", intent = "neutral", size, iconOnly = false, className }: ButtonRecipeProps) {
|
|
105
|
+
export function buttonRecipe({ variant = "solid", intent = "neutral", brand, size, iconOnly = false, className }: ButtonRecipeProps) {
|
|
101
106
|
return {
|
|
102
107
|
className: cn(
|
|
103
108
|
variantStructure[variant],
|
|
@@ -106,6 +111,7 @@ export function buttonRecipe({ variant = "solid", intent = "neutral", size, icon
|
|
|
106
111
|
),
|
|
107
112
|
"data-variant": variant,
|
|
108
113
|
"data-intent": intent,
|
|
114
|
+
"data-brand": brand,
|
|
109
115
|
};
|
|
110
116
|
}
|
|
111
117
|
|
|
@@ -113,6 +119,12 @@ export type ButtonProps = React.ButtonHTMLAttributes<HTMLButtonElement> &
|
|
|
113
119
|
VariantProps<typeof buttonVariants> & {
|
|
114
120
|
variant?: ButtonVariant | undefined;
|
|
115
121
|
intent?: ButtonIntent | undefined;
|
|
122
|
+
/**
|
|
123
|
+
* Name of a brand the theme defines, rendered as `data-brand`. Re-points
|
|
124
|
+
* every brand token on this element, so `intent="brand"` takes that brand's
|
|
125
|
+
* colour. Omitted, the button follows the nearest `data-brand` ancestor.
|
|
126
|
+
*/
|
|
127
|
+
brand?: string | undefined;
|
|
116
128
|
/** Square the button at its current size, for an icon with no label. */
|
|
117
129
|
iconOnly?: boolean;
|
|
118
130
|
asChild?: boolean;
|
|
@@ -147,15 +159,23 @@ export type ButtonProps = React.ButtonHTMLAttributes<HTMLButtonElement> &
|
|
|
147
159
|
* `aria-label`, since there is no text to name it. It is a separate prop rather
|
|
148
160
|
* than a size, so `size="sm" iconOnly` is expressible.
|
|
149
161
|
*
|
|
162
|
+
* `brand` scopes the `brand` intent to one of the theme's named brands
|
|
163
|
+
* (`<Button intent="brand" brand="mint">`), for a theme with more than one
|
|
164
|
+
* brand colour. It renders `data-brand` and nothing else; the theme's CSS gives
|
|
165
|
+
* the name meaning, and an unknown name falls back to the surrounding brand.
|
|
166
|
+
*
|
|
150
167
|
* `asChild` renders the child element with Button's styling instead of a
|
|
151
168
|
* `<button>`, for links that should look like buttons. `loading` shows a spinner
|
|
152
169
|
* and disables the control, so it does not need `disabled` as well.
|
|
153
170
|
*/
|
|
154
171
|
const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
|
|
155
|
-
(
|
|
172
|
+
(
|
|
173
|
+
{ className, variant = "solid", intent = "neutral", brand, size, iconOnly = false, asChild = false, loading = false, children, disabled, ...props },
|
|
174
|
+
ref
|
|
175
|
+
) => {
|
|
156
176
|
const Comp = asChild ? Slot : "button";
|
|
157
177
|
return (
|
|
158
|
-
<Comp {...buttonRecipe({ variant, intent, size, iconOnly, className })} ref={ref} disabled={disabled || loading} {...props}>
|
|
178
|
+
<Comp {...buttonRecipe({ variant, intent, brand, size, iconOnly, className })} ref={ref} disabled={disabled || loading} {...props}>
|
|
159
179
|
{loading ? (
|
|
160
180
|
<>
|
|
161
181
|
<svg className="animate-spin" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" aria-hidden="true">
|
|
@@ -0,0 +1,261 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useControllableState } from "@sixthshift/design-system/hooks";
|
|
4
|
+
import { cn } from "@sixthshift/design-system/utils";
|
|
5
|
+
import { cva, type VariantProps } from "class-variance-authority";
|
|
6
|
+
import { ArrowDown, ArrowUp, ChevronsUpDown } from "lucide-react";
|
|
7
|
+
import { forwardRef, type HTMLAttributes, type KeyboardEvent, type ReactElement, type ReactNode, type Ref, useMemo } from "react";
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Geometry only. Every colour reads a `--data-table-*` component token whose
|
|
11
|
+
* value is decided by src/components/DataTable/data-table.recipe.css.
|
|
12
|
+
*
|
|
13
|
+
* `size` is cell density: `sm` for back-office lists scanned dozens of rows at
|
|
14
|
+
* a time, `md` for tables that are the page's main content.
|
|
15
|
+
*/
|
|
16
|
+
const cellVariants = cva("px-3 align-middle", {
|
|
17
|
+
variants: {
|
|
18
|
+
size: {
|
|
19
|
+
sm: "h-9 py-1.5 text-xs",
|
|
20
|
+
md: "h-11 py-2 text-sm",
|
|
21
|
+
},
|
|
22
|
+
},
|
|
23
|
+
defaultVariants: {
|
|
24
|
+
size: "md",
|
|
25
|
+
},
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
export type DataTableSize = NonNullable<VariantProps<typeof cellVariants>["size"]>;
|
|
29
|
+
|
|
30
|
+
export type DataTableAlign = "start" | "center" | "end";
|
|
31
|
+
|
|
32
|
+
export type DataTableColumn<Row> = {
|
|
33
|
+
/** Stable key — also what `sort.columnId` refers to. */
|
|
34
|
+
id: string;
|
|
35
|
+
/** Header content. */
|
|
36
|
+
header: ReactNode;
|
|
37
|
+
/** Cell content for one row. */
|
|
38
|
+
cell: (row: Row) => ReactNode;
|
|
39
|
+
/**
|
|
40
|
+
* Makes the column sortable. The returned value is what rows are compared
|
|
41
|
+
* by — return cents, not a formatted price, and an ISO date, not "Tue 3rd".
|
|
42
|
+
*/
|
|
43
|
+
sortValue?: (row: Row) => string | number;
|
|
44
|
+
/** Text alignment for header and cells. Numbers read best at `end`. */
|
|
45
|
+
align?: DataTableAlign;
|
|
46
|
+
/** A CSS width for the column, e.g. `"8rem"` or `"20%"`. */
|
|
47
|
+
width?: string;
|
|
48
|
+
/** Extra classes for this column's `<td>`s. */
|
|
49
|
+
className?: string;
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
export type DataTableSortDirection = "asc" | "desc";
|
|
53
|
+
|
|
54
|
+
export type DataTableSort = {
|
|
55
|
+
columnId: string;
|
|
56
|
+
direction: DataTableSortDirection;
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
export type DataTableProps<Row> = Omit<HTMLAttributes<HTMLDivElement>, "children"> &
|
|
60
|
+
VariantProps<typeof cellVariants> & {
|
|
61
|
+
columns: readonly DataTableColumn<Row>[];
|
|
62
|
+
rows: readonly Row[];
|
|
63
|
+
/** Stable id per row, used as the React key. */
|
|
64
|
+
getRowId: (row: Row) => string;
|
|
65
|
+
/** Controlled sort. `null` is unsorted — rows in the order given. */
|
|
66
|
+
sort?: DataTableSort | null;
|
|
67
|
+
/** Initial sort in uncontrolled mode. */
|
|
68
|
+
defaultSort?: DataTableSort | null;
|
|
69
|
+
/** Called when a header is clicked: ascending, then descending, then unsorted. */
|
|
70
|
+
onSortChange?: (sort: DataTableSort | null) => void;
|
|
71
|
+
/**
|
|
72
|
+
* Report sort changes without reordering `rows`, for data sorted where it
|
|
73
|
+
* is fetched. The headers still show the sort state.
|
|
74
|
+
*/
|
|
75
|
+
manualSorting?: boolean;
|
|
76
|
+
/** Makes every row activatable by pointer, `Enter` or `Space`. */
|
|
77
|
+
onRowClick?: (row: Row) => void;
|
|
78
|
+
/** Shown in place of the body when `rows` is empty. */
|
|
79
|
+
empty?: ReactNode;
|
|
80
|
+
/** Names the table for assistive tech. Visually hidden unless `showCaption`. */
|
|
81
|
+
caption?: ReactNode;
|
|
82
|
+
showCaption?: boolean;
|
|
83
|
+
/** Keep the header row visible while the wrapper scrolls vertically. Give the wrapper a height via `className`. */
|
|
84
|
+
stickyHeader?: boolean;
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
const alignClass: Record<DataTableAlign, string> = {
|
|
88
|
+
start: "text-start",
|
|
89
|
+
center: "text-center",
|
|
90
|
+
end: "text-end",
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
const ariaSort = { asc: "ascending", desc: "descending" } as const;
|
|
94
|
+
|
|
95
|
+
function nextSort(current: DataTableSort | null, columnId: string): DataTableSort | null {
|
|
96
|
+
if (current?.columnId !== columnId) return { columnId, direction: "asc" };
|
|
97
|
+
return current.direction === "asc" ? { columnId, direction: "desc" } : null;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function compare(a: string | number, b: string | number): number {
|
|
101
|
+
if (typeof a === "number" && typeof b === "number") return a - b;
|
|
102
|
+
return String(a).localeCompare(String(b), undefined, { numeric: true, sensitivity: "base" });
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* `forwardRef` erases the generic, so the implementation takes the ref as an
|
|
107
|
+
* argument and the export below restores `<Row>` — the same arrangement
|
|
108
|
+
* Select.tsx uses for `<T>`.
|
|
109
|
+
*/
|
|
110
|
+
const DataTableRoot = forwardRef(function DataTableRoot<Row>(
|
|
111
|
+
{
|
|
112
|
+
className,
|
|
113
|
+
columns,
|
|
114
|
+
rows,
|
|
115
|
+
getRowId,
|
|
116
|
+
size = "md",
|
|
117
|
+
sort: controlledSort,
|
|
118
|
+
defaultSort = null,
|
|
119
|
+
onSortChange,
|
|
120
|
+
manualSorting = false,
|
|
121
|
+
onRowClick,
|
|
122
|
+
empty,
|
|
123
|
+
caption,
|
|
124
|
+
showCaption = false,
|
|
125
|
+
stickyHeader = false,
|
|
126
|
+
...props
|
|
127
|
+
}: DataTableProps<Row>,
|
|
128
|
+
ref: Ref<HTMLDivElement>
|
|
129
|
+
) {
|
|
130
|
+
const [sort, setSort] = useControllableState<DataTableSort | null>({
|
|
131
|
+
value: controlledSort,
|
|
132
|
+
defaultValue: defaultSort,
|
|
133
|
+
onChange: onSortChange,
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
const sortedRows = useMemo(() => {
|
|
137
|
+
if (manualSorting || sort === null) return rows;
|
|
138
|
+
const sortValue = columns.find((column) => column.id === sort.columnId)?.sortValue;
|
|
139
|
+
if (!sortValue) return rows;
|
|
140
|
+
const factor = sort.direction === "asc" ? 1 : -1;
|
|
141
|
+
// Array.prototype.sort is stable, so equal keys keep the caller's order.
|
|
142
|
+
return [...rows].sort((a, b) => factor * compare(sortValue(a), sortValue(b)));
|
|
143
|
+
}, [rows, columns, sort, manualSorting]);
|
|
144
|
+
|
|
145
|
+
const interactive = onRowClick !== undefined;
|
|
146
|
+
|
|
147
|
+
const handleRowKeyDown = (event: KeyboardEvent<HTMLTableRowElement>, row: Row) => {
|
|
148
|
+
if (event.target !== event.currentTarget) return; // a button inside the row keeps its own keys
|
|
149
|
+
if (event.key === "Enter" || event.key === " ") {
|
|
150
|
+
event.preventDefault();
|
|
151
|
+
onRowClick?.(row);
|
|
152
|
+
}
|
|
153
|
+
};
|
|
154
|
+
|
|
155
|
+
return (
|
|
156
|
+
<div
|
|
157
|
+
ref={ref}
|
|
158
|
+
className={cn(
|
|
159
|
+
"data-table border-(color:--data-table-border) w-full overflow-auto rounded-lg border bg-(--data-table-bg) text-(--data-table-fg)",
|
|
160
|
+
className
|
|
161
|
+
)}
|
|
162
|
+
{...props}
|
|
163
|
+
>
|
|
164
|
+
<table className="w-full border-collapse">
|
|
165
|
+
{caption !== undefined && <caption className={cn(showCaption ? "px-3 py-2 text-start font-medium text-sm" : "sr-only")}>{caption}</caption>}
|
|
166
|
+
<thead className={cn("bg-(--data-table-header-bg)", stickyHeader && "sticky top-0 z-10")}>
|
|
167
|
+
<tr className="border-(color:--data-table-border) border-b">
|
|
168
|
+
{columns.map((column) => {
|
|
169
|
+
const align = column.align ?? "start";
|
|
170
|
+
const sorted = sort?.columnId === column.id ? sort.direction : undefined;
|
|
171
|
+
const Icon = sorted === "asc" ? ArrowUp : sorted === "desc" ? ArrowDown : ChevronsUpDown;
|
|
172
|
+
return (
|
|
173
|
+
<th
|
|
174
|
+
key={column.id}
|
|
175
|
+
scope="col"
|
|
176
|
+
aria-sort={column.sortValue ? (sorted ? ariaSort[sorted] : "none") : undefined}
|
|
177
|
+
style={column.width ? { width: column.width } : undefined}
|
|
178
|
+
className={cn(cellVariants({ size }), alignClass[align], "whitespace-nowrap font-medium text-(--data-table-header-fg)")}
|
|
179
|
+
>
|
|
180
|
+
{column.sortValue ? (
|
|
181
|
+
<button
|
|
182
|
+
type="button"
|
|
183
|
+
onClick={() => setSort(nextSort(sort, column.id))}
|
|
184
|
+
className={cn(
|
|
185
|
+
"focus-visible:ring-(color:--data-table-ring) -mx-1 inline-flex cursor-pointer items-center gap-1 rounded-sm px-1 hover:text-(--data-table-fg) focus-visible:outline-hidden focus-visible:ring-2",
|
|
186
|
+
align === "end" && "flex-row-reverse"
|
|
187
|
+
)}
|
|
188
|
+
>
|
|
189
|
+
{column.header}
|
|
190
|
+
<Icon aria-hidden="true" className={cn("size-3.5 shrink-0", sorted === undefined && "opacity-50")} />
|
|
191
|
+
</button>
|
|
192
|
+
) : (
|
|
193
|
+
column.header
|
|
194
|
+
)}
|
|
195
|
+
</th>
|
|
196
|
+
);
|
|
197
|
+
})}
|
|
198
|
+
</tr>
|
|
199
|
+
</thead>
|
|
200
|
+
<tbody>
|
|
201
|
+
{sortedRows.length === 0 ? (
|
|
202
|
+
<tr>
|
|
203
|
+
<td colSpan={columns.length} className="px-3 py-10 text-center text-(--data-table-empty-fg) text-sm">
|
|
204
|
+
{empty ?? "Nothing to show"}
|
|
205
|
+
</td>
|
|
206
|
+
</tr>
|
|
207
|
+
) : (
|
|
208
|
+
sortedRows.map((row) => (
|
|
209
|
+
<tr
|
|
210
|
+
key={getRowId(row)}
|
|
211
|
+
data-interactive={interactive || undefined}
|
|
212
|
+
onClick={interactive ? () => onRowClick(row) : undefined}
|
|
213
|
+
onKeyDown={interactive ? (event) => handleRowKeyDown(event, row) : undefined}
|
|
214
|
+
// A row is not a native control, but a clickable table row is the
|
|
215
|
+
// established back-office pattern. It stays a `row` so the table
|
|
216
|
+
// semantics survive, and gains a tab stop and Enter/Space like
|
|
217
|
+
// Card does when it is clickable.
|
|
218
|
+
tabIndex={interactive ? 0 : undefined}
|
|
219
|
+
className={cn(
|
|
220
|
+
"border-(color:--data-table-border) border-b last:border-b-0",
|
|
221
|
+
interactive &&
|
|
222
|
+
"focus-visible:outline-(color:--data-table-ring) cursor-pointer hover:bg-(--data-table-row-bg-hovered) focus-visible:outline-2 focus-visible:-outline-offset-2"
|
|
223
|
+
)}
|
|
224
|
+
>
|
|
225
|
+
{columns.map((column) => (
|
|
226
|
+
<td key={column.id} className={cn(cellVariants({ size }), alignClass[column.align ?? "start"], column.className)}>
|
|
227
|
+
{column.cell(row)}
|
|
228
|
+
</td>
|
|
229
|
+
))}
|
|
230
|
+
</tr>
|
|
231
|
+
))
|
|
232
|
+
)}
|
|
233
|
+
</tbody>
|
|
234
|
+
</table>
|
|
235
|
+
</div>
|
|
236
|
+
);
|
|
237
|
+
});
|
|
238
|
+
|
|
239
|
+
/**
|
|
240
|
+
* A data table for back-office lists — orders, products, customers.
|
|
241
|
+
*
|
|
242
|
+
* Props-based, per docs/component-api-design.md: rows are uniform, so they
|
|
243
|
+
* arrive as a `rows` array and a `columns` config, and each column's `cell`
|
|
244
|
+
* renders one row's content. Anything interactive in a cell — a status
|
|
245
|
+
* Badge, an action Button — is just what `cell` returns.
|
|
246
|
+
*
|
|
247
|
+
* A column with `sortValue` gets a header button cycling ascending →
|
|
248
|
+
* descending → unsorted, and `aria-sort` on its `<th>`. Sorting is
|
|
249
|
+
* client-side and stable; pass `manualSorting` to only report the change when
|
|
250
|
+
* the data is sorted where it is fetched. Sort state is controlled or
|
|
251
|
+
* uncontrolled through `sort`/`defaultSort`/`onSortChange`.
|
|
252
|
+
*
|
|
253
|
+
* `onRowClick` makes every row a tab stop activated by pointer, `Enter` or
|
|
254
|
+
* `Space` — keys pressed on a control inside the row are left to that control.
|
|
255
|
+
*
|
|
256
|
+
* The wrapper scrolls, so a wide table on a phone scrolls sideways inside its
|
|
257
|
+
* border rather than widening the page.
|
|
258
|
+
*/
|
|
259
|
+
export const DataTable = DataTableRoot as <Row>(props: DataTableProps<Row> & { ref?: Ref<HTMLDivElement> }) => ReactElement;
|
|
260
|
+
|
|
261
|
+
export { cellVariants as dataTableCellVariants };
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* DataTable recipe — the component-token layer for DataTable.
|
|
3
|
+
*
|
|
4
|
+
* No variant/intent axis: status colour inside a table belongs to what the
|
|
5
|
+
* cells render (a Badge), not to the table. The table itself is the Elevated
|
|
6
|
+
* tier's container — the same surface and rule colours as card.css — through
|
|
7
|
+
* its own tokens, so a consumer can re-point one without the other.
|
|
8
|
+
*
|
|
9
|
+
* The grammar is `--data-table-[{part}-]{context}[-{state}]`. Parts are the
|
|
10
|
+
* `header` row, a `row` (only its hover, which applies only when rows are
|
|
11
|
+
* clickable), and the `empty` state's text. `--data-table-border` draws the
|
|
12
|
+
* outer frame and the rules between rows alike.
|
|
13
|
+
*
|
|
14
|
+
* `@layer components` is load-bearing, not tidiness — see button.css.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
@layer components {
|
|
18
|
+
.data-table {
|
|
19
|
+
--data-table-bg: var(--bg-normal);
|
|
20
|
+
--data-table-fg: var(--fg-normal);
|
|
21
|
+
--data-table-border: var(--border-normal);
|
|
22
|
+
--data-table-ring: var(--focus-ring);
|
|
23
|
+
--data-table-header-bg: var(--bg-subtle);
|
|
24
|
+
--data-table-header-fg: var(--fg-subtle);
|
|
25
|
+
--data-table-row-bg-hovered: var(--bg-normal-hovered);
|
|
26
|
+
--data-table-empty-fg: var(--fg-subtle);
|
|
27
|
+
}
|
|
28
|
+
}
|
|
@@ -21,6 +21,7 @@ import {
|
|
|
21
21
|
toISODateRange,
|
|
22
22
|
} from "../../date-time";
|
|
23
23
|
import { PickerField } from "../../internal";
|
|
24
|
+
import { closestBrand } from "../../internal/brandScope";
|
|
24
25
|
import { useEscapeLayer } from "../../internal/escapeLayers";
|
|
25
26
|
import { CalendarView } from "../Calendar/CalendarView";
|
|
26
27
|
import { isDateDisabled } from "../Calendar/calendar.hooks";
|
|
@@ -539,6 +540,8 @@ export const DatePicker = React.forwardRef<HTMLDivElement, DatePickerProps>((pro
|
|
|
539
540
|
role="dialog"
|
|
540
541
|
aria-label="Choose date"
|
|
541
542
|
style={floatingStyles}
|
|
543
|
+
// Portalled out of the trigger's brand scope, so carry it across.
|
|
544
|
+
data-brand={closestBrand(context.elements.domReference)}
|
|
542
545
|
className="z-popover rounded-xl border border-border-normal bg-bg-normal p-4 shadow-lg"
|
|
543
546
|
{...getFloatingProps()}
|
|
544
547
|
>
|
|
@@ -16,6 +16,7 @@ import {
|
|
|
16
16
|
toISOInstantOrUndefined,
|
|
17
17
|
} from "../../date-time";
|
|
18
18
|
import { PickerField } from "../../internal";
|
|
19
|
+
import { closestBrand } from "../../internal/brandScope";
|
|
19
20
|
import { useEscapeLayer } from "../../internal/escapeLayers";
|
|
20
21
|
import { Button } from "../Button";
|
|
21
22
|
import { CalendarView } from "../Calendar/CalendarView";
|
|
@@ -410,6 +411,8 @@ export const DateTimePicker = React.forwardRef<HTMLDivElement, DateTimePickerPro
|
|
|
410
411
|
role="dialog"
|
|
411
412
|
aria-label="Choose date and time"
|
|
412
413
|
style={floatingStyles}
|
|
414
|
+
// Portalled out of the trigger's brand scope, so carry it across.
|
|
415
|
+
data-brand={closestBrand(context.elements.domReference)}
|
|
413
416
|
className="z-popover rounded-xl border border-border-normal bg-bg-normal p-4 shadow-lg"
|
|
414
417
|
{...getFloatingProps()}
|
|
415
418
|
>
|
|
@@ -19,6 +19,7 @@ import {
|
|
|
19
19
|
toISOInstantRange,
|
|
20
20
|
} from "../../date-time";
|
|
21
21
|
import { PickerField } from "../../internal";
|
|
22
|
+
import { closestBrand } from "../../internal/brandScope";
|
|
22
23
|
import { useEscapeLayer } from "../../internal/escapeLayers";
|
|
23
24
|
import { Button } from "../Button";
|
|
24
25
|
import { CalendarView } from "../Calendar/CalendarView";
|
|
@@ -630,6 +631,8 @@ export const DateTimeRangePicker = React.forwardRef<HTMLDivElement, DateTimeRang
|
|
|
630
631
|
role="dialog"
|
|
631
632
|
aria-label="Choose date and time range"
|
|
632
633
|
style={floatingStyles}
|
|
634
|
+
// Portalled out of the trigger's brand scope, so carry it across.
|
|
635
|
+
data-brand={closestBrand(context.elements.domReference)}
|
|
633
636
|
className="z-popover rounded-xl border border-border-normal bg-bg-normal p-4 shadow-lg"
|
|
634
637
|
{...getFloatingProps()}
|
|
635
638
|
>
|