@instruments/materia-ui 0.4.7 → 0.4.9
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/agent-docs.md +4 -2
- package/dist/components/code/code-root.d.ts +1 -1
- package/dist/components/code/code-root.js +4 -1
- package/dist/components/combobox/combobox-root.js +3 -3
- package/dist/components/command/command-list.d.ts +1 -1
- package/dist/components/command/command-list.js +3 -2
- package/dist/components/country-combobox/country-combobox-root.d.ts +1 -1
- package/dist/components/input-group/input-group-button.d.ts +2 -2
- package/dist/components/location-input/location-input-root.js +9 -4
- package/dist/components/scroll-frame/scroll-frame-root.d.ts +18 -5
- package/dist/components/scroll-frame/scroll-frame-root.js +49 -17
- package/dist/components/scroll-frame.d.ts +1 -1
- package/dist/components/select/select-trigger.d.ts +1 -1
- package/dist/components/sidepanel/sidepanel-body.d.ts +4 -3
- package/dist/components/sidepanel/sidepanel-body.js +6 -4
- package/dist/components/swatch-group/swatch-group-item.d.ts +1 -1
- package/dist/components/swatch-selector/swatch-selector-item.d.ts +1 -1
- package/dist/components/table/table-root.d.ts +1 -1
- package/dist/components/table/table-root.js +3 -2
- package/dist/compositions/data-grid-column-filter/data-grid-column-filter-root.js +2 -1
- package/dist/compositions/data-grid-table/data-grid-table-root.js +2 -1
- package/dist/compositions/mega-menu/mega-menu-content.js +3 -2
- package/dist/compositions/mobile-navigation/mobile-navigation-header.d.ts +1 -1
- package/dist/compositions/quick-view/quick-view-body.d.ts +1 -1
- package/dist/compositions/sidebar/sidebar-content.d.ts +1 -1
- package/dist/compositions/sidebar/sidebar-content.js +3 -2
- package/dist/compositions/sidebar/sidebar-group.d.ts +1 -1
- package/dist/compositions/sidebar/sidebar-menu-sub.d.ts +2 -2
- package/dist/compositions/sidebar/sidebar-menu-sub.js +2 -1
- package/dist/compositions/sidebar/sidebar-trigger.d.ts +1 -1
- package/dist/compositions/tag-selector/tag-selector-list.js +4 -3
- package/dist/data/docs-manifest.js +270 -221
- package/package.json +1 -1
package/agent-docs.md
CHANGED
|
@@ -1478,6 +1478,8 @@ A scrolling region that shows where there is more: each edge fades only while co
|
|
|
1478
1478
|
- Give a vertical frame a bounded height (`className="h-80"`); a horizontal one needs only a bounded width, which a flex child with `min-w-0` already has.
|
|
1479
1479
|
- Match `fadeTheme` to the surface behind the frame (`card` inside a Card). Over an image or a blur, use `fadeMode="mask"` instead.
|
|
1480
1480
|
- For a horizontal strip used with a mouse, add `dragScroll` and `controls`; both stand aside on touch screens, and each control hides at the end it points to.
|
|
1481
|
+
- Every scrolling region in an app is a `ScrollFrame`, not a hand-rolled `overflow-y-auto` div: a pane, a list in a popover, a wide table (`orientation="both"`). A frame capped by `max-h-*` alone scrolls once its content passes the cap.
|
|
1482
|
+
- Hand a virtualiser the scrolling element through `viewportRef`.
|
|
1481
1483
|
|
|
1482
1484
|
## SearchInput
|
|
1483
1485
|
|
|
@@ -1688,7 +1690,7 @@ Root component for a sidepanel. Wraps Base UI Dialog.Root.
|
|
|
1688
1690
|
|
|
1689
1691
|
`@instruments/materia-ui/components/sidepanel`
|
|
1690
1692
|
|
|
1691
|
-
The scrolling region between a `SidepanelHeader` and `SidepanelFooter`: takes the remaining height of `SidepanelBody`, scrolls on its own and carries the panel's standard inset, so header and footer stay put while it moves.
|
|
1693
|
+
The scrolling region between a `SidepanelHeader` and `SidepanelFooter`, a `ScrollFrame`: takes the remaining height of `SidepanelBody`, scrolls on its own and carries the panel's standard inset, so header and footer stay put while it moves.
|
|
1692
1694
|
|
|
1693
1695
|
- Put exactly one `SidepanelScroll` inside `SidepanelBody`, between the header and footer, rather than a hand-rolled `flex-1 overflow-y-auto` div.
|
|
1694
1696
|
|
|
@@ -1887,7 +1889,7 @@ Renders a toggle switch for boolean on/off states.
|
|
|
1887
1889
|
|
|
1888
1890
|
`@instruments/materia-ui/components/table`
|
|
1889
1891
|
|
|
1890
|
-
Semantic `<table>` primitive, wrapped in a
|
|
1892
|
+
Semantic `<table>` primitive, wrapped in a horizontal `ScrollFrame` so wide tables overflow rather than break layout. Compose with the exported `TableHeader`, `TableBody`, `TableRow`, `TableHead`, `TableCell`, `TableFooter`, and `TableCaption` parts.
|
|
1891
1893
|
|
|
1892
1894
|
- Use `Table` for a small, static, hand-authored table; reach for `DataGrid` once you need sorting, filtering, pagination, column pinning/visibility, or row virtualisation - `Table` has no data model.
|
|
1893
1895
|
- Include a `TableCaption` describing the table's contents - it is the accessible name screen readers announce for the `<table>`.
|
|
@@ -2,7 +2,7 @@ import * as React from "react";
|
|
|
2
2
|
export interface CodeProps extends React.ComponentProps<"code"> {
|
|
3
3
|
/**
|
|
4
4
|
* Render a multi-line block (`<pre><code>`) instead of inline `<code>`. The
|
|
5
|
-
* block scrolls horizontally rather than wrapping. @default false
|
|
5
|
+
* block scrolls horizontally, in a `ScrollFrame`, rather than wrapping. @default false
|
|
6
6
|
*/
|
|
7
7
|
block?: boolean;
|
|
8
8
|
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
2
|
import { cn } from "@instruments/materia-ui/utils/cn";
|
|
3
3
|
import * as React from "react";
|
|
4
|
+
import { ScrollFrame } from "../scroll-frame.js";
|
|
4
5
|
/**
|
|
5
6
|
* Monospace code - inline `<code>` by default (a muted, rounded chip sized to
|
|
6
7
|
* surrounding text), or a scrollable `<pre><code>` block when `block` is set.
|
|
@@ -11,7 +12,9 @@ import * as React from "react";
|
|
|
11
12
|
*/
|
|
12
13
|
export const Code = ({ block = false, className, children, ...props }) => {
|
|
13
14
|
if (block) {
|
|
14
|
-
|
|
15
|
+
// The block is as wide as its longest line, so a long line scrolls the frame and the padding travels with it;
|
|
16
|
+
// with `whitespace-pre-wrap` it is the frame's width and wraps.
|
|
17
|
+
return (_jsx(ScrollFrame, { className: "border-border bg-muted rounded-lg border", contentClassName: "min-w-0!", fadeTheme: "muted", orientation: "horizontal", children: _jsx("pre", { className: cn("text-paragraph-sm w-fit min-w-full p-4 font-mono", className), "data-block": "true", "data-component": "code", children: _jsx("code", { ...props, children: children }) }) }));
|
|
15
18
|
}
|
|
16
19
|
return (_jsx("code", { className: cn("bg-muted text-paragraph-sm rounded-sm px-1 py-0.5 font-mono", className), "data-component": "code", ...props, children: children }));
|
|
17
20
|
};
|
|
@@ -10,6 +10,7 @@ import { CheckIcon, ChevronsUpDownIcon, SearchIcon } from "lucide-react";
|
|
|
10
10
|
import { useImperativeHandle, useRef, useState } from "react";
|
|
11
11
|
import { useControllableState } from "../../hooks/use-controllable-state.js";
|
|
12
12
|
import { POPOVER_CONTENT_ANIMATION, POPOVER_SURFACE, } from "../../tokens/overlay-styles.js";
|
|
13
|
+
import { ScrollFrame } from "../scroll-frame.js";
|
|
13
14
|
const OPTION_CLASSES = "data-highlighted:bg-muted data-highlighted:text-accent-foreground relative flex w-full cursor-default items-center rounded-full py-2.5 pr-8 pl-4 text-paragraph-sm outline-hidden select-none";
|
|
14
15
|
/** A single selectable option row, shared by virtualised and plain lists. */
|
|
15
16
|
const ComboboxOption = ({ item, className, ...props }) => (_jsxs(ComboboxPrimitive.Item, { className: cn(OPTION_CLASSES, className), "data-slot": "combobox-item", value: item, ...props, children: [item.label, _jsx(ComboboxPrimitive.ItemIndicator, { className: "absolute right-2 flex size-4 items-center justify-center", children: _jsx(Icon, { icon: CheckIcon, size: "xs" }) })] }));
|
|
@@ -33,7 +34,7 @@ const VirtualizedOptions = ({ virtualization, virtualizerRef, }) => {
|
|
|
33
34
|
overscan: virtualization.overscan ?? 8,
|
|
34
35
|
});
|
|
35
36
|
useImperativeHandle(virtualizerRef, () => rowVirtualizer);
|
|
36
|
-
return (_jsx(
|
|
37
|
+
return (_jsx(ScrollFrame, { fadeTheme: "popover", fades: "end", role: "presentation", style: { maxHeight: virtualization.maxHeightPx ?? 300 }, viewportRef: setScrollEl, children: _jsx("div", { className: "relative w-full", role: "presentation", style: { height: rowVirtualizer.getTotalSize() }, children: rowVirtualizer.getVirtualItems().map((virtualRow) => {
|
|
37
38
|
const item = filteredItems[virtualRow.index];
|
|
38
39
|
if (!item) {
|
|
39
40
|
return null;
|
|
@@ -105,6 +106,5 @@ export const Combobox = ({ items, value, onValueChange, placeholder = "Select…
|
|
|
105
106
|
// It renders the kit's Button, a native <button>.
|
|
106
107
|
, {
|
|
107
108
|
// It renders the kit's Button, a native <button>.
|
|
108
|
-
nativeButton: true, render: _jsx(ComboboxTriggerButton, { className: buttonClassName, "data-testid": dataTestId, size: size }), children: selected?.label ?? placeholder }), _jsx(ComboboxPrimitive.Portal, { children: _jsx(ComboboxPrimitive.Positioner, { align: "start", className: "z-50", sideOffset: 4, children: _jsxs(ComboboxPrimitive.Popup, { "aria-label": placeholder, className: cn(`${POPOVER_CONTENT_ANIMATION} ${POPOVER_SURFACE} w-(--anchor-width) min-w-32 origin-(--transform-origin) rounded-3xl`, contentClassName), "data-component": "combobox-content", "data-slot": "combobox-content", children: [_jsxs(Flex, { align: "center", className: "h-9 border-b px-3", "data-slot": "combobox-input-wrapper", gap: "xs", children: [_jsx(Icon, { className: "shrink-0 opacity-50", icon: SearchIcon, size: "xs" }), _jsx(ComboboxPrimitive.Input, { className: "placeholder:text-muted-foreground text-paragraph-sm flex h-10 w-full bg-transparent py-3 outline-hidden disabled:cursor-not-allowed disabled:opacity-50", "data-slot": "combobox-input", placeholder: "Search\u2026" })] }), _jsx(ComboboxPrimitive.Empty, { className: "text-paragraph-sm py-6 text-center empty:p-0", "data-slot": "combobox-empty", children: "No results found." }), _jsx(ComboboxPrimitive.List, { className:
|
|
109
|
-
"max-h-75 scroll-py-1 overflow-x-hidden overflow-y-auto"), children: shouldVirtualize && virtualization ? (_jsx(VirtualizedOptions, { virtualization: virtualization, virtualizerRef: virtualizerRef })) : ((item) => (_jsx(ComboboxOption, { item: item }, item.value))) })] }) }) })] }));
|
|
109
|
+
nativeButton: true, render: _jsx(ComboboxTriggerButton, { className: buttonClassName, "data-testid": dataTestId, size: size }), children: selected?.label ?? placeholder }), _jsx(ComboboxPrimitive.Portal, { children: _jsx(ComboboxPrimitive.Positioner, { align: "start", className: "z-50", sideOffset: 4, children: _jsxs(ComboboxPrimitive.Popup, { "aria-label": placeholder, className: cn(`${POPOVER_CONTENT_ANIMATION} ${POPOVER_SURFACE} w-(--anchor-width) min-w-32 origin-(--transform-origin) rounded-3xl`, contentClassName), "data-component": "combobox-content", "data-slot": "combobox-content", children: [_jsxs(Flex, { align: "center", className: "h-9 border-b px-3", "data-slot": "combobox-input-wrapper", gap: "xs", children: [_jsx(Icon, { className: "shrink-0 opacity-50", icon: SearchIcon, size: "xs" }), _jsx(ComboboxPrimitive.Input, { className: "placeholder:text-muted-foreground text-paragraph-sm flex h-10 w-full bg-transparent py-3 outline-hidden disabled:cursor-not-allowed disabled:opacity-50", "data-slot": "combobox-input", placeholder: "Search\u2026" })] }), _jsx(ComboboxPrimitive.Empty, { className: "text-paragraph-sm py-6 text-center empty:p-0", "data-slot": "combobox-empty", children: "No results found." }), shouldVirtualize && virtualization ? (_jsx(ComboboxPrimitive.List, { className: "p-1 empty:p-0", children: _jsx(VirtualizedOptions, { virtualization: virtualization, virtualizerRef: virtualizerRef }) })) : (_jsx(ScrollFrame, { className: "max-h-75", fadeTheme: "popover", fades: "end", children: _jsx(ComboboxPrimitive.List, { className: "scroll-py-1 p-1 empty:p-0", children: (item) => (_jsx(ComboboxOption, { item: item }, item.value)) }) }))] }) }) })] }));
|
|
110
110
|
};
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { Autocomplete as AutocompletePrimitive } from "@base-ui/react/autocomplete";
|
|
2
2
|
import type * as React from "react";
|
|
3
3
|
/**
|
|
4
|
-
* CommandList is the
|
|
4
|
+
* CommandList is the scrolling container for a Command's filtered items, in a `ScrollFrame` capped at 300px.
|
|
5
5
|
* Pass a function child: it receives each filtered item (or group, for grouped
|
|
6
6
|
* `items`) and returns a `CommandItem` (or `CommandGroup`).
|
|
7
7
|
* Import from "@instruments/materia-ui/components/command".
|
|
@@ -2,10 +2,11 @@
|
|
|
2
2
|
import { jsx as _jsx } from "react/jsx-runtime";
|
|
3
3
|
import { Autocomplete as AutocompletePrimitive } from "@base-ui/react/autocomplete";
|
|
4
4
|
import { cn } from "@instruments/materia-ui/utils/cn";
|
|
5
|
+
import { ScrollFrame } from "../scroll-frame.js";
|
|
5
6
|
/**
|
|
6
|
-
* CommandList is the
|
|
7
|
+
* CommandList is the scrolling container for a Command's filtered items, in a `ScrollFrame` capped at 300px.
|
|
7
8
|
* Pass a function child: it receives each filtered item (or group, for grouped
|
|
8
9
|
* `items`) and returns a `CommandItem` (or `CommandGroup`).
|
|
9
10
|
* Import from "@instruments/materia-ui/components/command".
|
|
10
11
|
*/
|
|
11
|
-
export const CommandList = ({ className, ...props }) => (_jsx(
|
|
12
|
+
export const CommandList = ({ className, ...props }) => (_jsx(ScrollFrame, { className: "max-h-75 shrink-0", fadeTheme: "popover", fades: "end", children: _jsx(AutocompletePrimitive.List, { className: cn("scroll-py-1 empty:hidden", className), "data-component": "command-list", "data-slot": "command-list", ...props }) }));
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { ComboboxProps } from "../combobox.js";
|
|
2
|
-
type CountryComboboxProps = Omit<ComboboxProps, "items" | "value"> & {
|
|
2
|
+
type CountryComboboxProps = Omit<ComboboxProps, "items" | "onValueChange" | "value"> & {
|
|
3
3
|
/** Current country code (ISO 3166-1 alpha-2) or country name */
|
|
4
4
|
value?: string;
|
|
5
5
|
/** Value change handler - receives ISO country code */
|
|
@@ -2,8 +2,8 @@ import type { LucideIcon } from "lucide-react";
|
|
|
2
2
|
import type * as React from "react";
|
|
3
3
|
import type { ButtonEmphasis, ButtonProps, ButtonTone } from "../button.js";
|
|
4
4
|
import { Button } from "../button.js";
|
|
5
|
-
export type InputGroupButtonProps = Omit<React.ComponentProps<typeof Button>, "size"> & {
|
|
6
|
-
/** Icon
|
|
5
|
+
export type InputGroupButtonProps = Omit<React.ComponentProps<typeof Button>, "emphasis" | "icon" | "radius" | "size" | "square" | "tone"> & {
|
|
6
|
+
/** Icon shown in the button, passed as a component (such as a Lucide icon); with no children the button is icon-only. */
|
|
7
7
|
icon?: LucideIcon | React.ComponentType<React.SVGProps<SVGSVGElement>>;
|
|
8
8
|
/** Semantic colour, as on Button. @default "neutral" */
|
|
9
9
|
tone?: ButtonTone;
|
|
@@ -9,6 +9,7 @@ import { getResponsiveSizeClasses } from "../../tokens/responsive.js";
|
|
|
9
9
|
import { Button } from "../button.js";
|
|
10
10
|
import { Icon } from "../icon.js";
|
|
11
11
|
import { MenuItem } from "../menu-item.js";
|
|
12
|
+
import { ScrollFrame } from "../scroll-frame.js";
|
|
12
13
|
import { useLocationInput } from "./use-location-input.js";
|
|
13
14
|
/** Default LocationInput size. */
|
|
14
15
|
const DEFAULT_LOCATION_INPUT_SIZE = "xl";
|
|
@@ -34,6 +35,11 @@ export const LOCATION_INPUT_SIZE_CLASSES = {
|
|
|
34
35
|
};
|
|
35
36
|
const inputWrapperVariants = cva(INPUT_WRAPPER);
|
|
36
37
|
const inputVariants = cva(INPUT_CONTROL_RESET);
|
|
38
|
+
/** The suggestions under the field, in a frame that scrolls past ten or so. */
|
|
39
|
+
const Suggestions = ({ onSelect, predictions, }) => (_jsx(ScrollFrame, { className: "bg-popover absolute inset-x-0 top-full z-50 max-h-60 rounded-3xl border shadow-xs", fadeTheme: "popover", fades: "end", children: _jsx("ul", { className: "m-0 list-none p-0", children: predictions.map((prediction) => (_jsx("li", { children: _jsx(MenuItem, { className: "hover:bg-muted hover:text-accent-foreground focus:bg-muted focus:text-accent-foreground rounded-full px-4 py-2.5", onClick: () => {
|
|
40
|
+
onSelect(prediction.placeId);
|
|
41
|
+
}, children: _jsxs("span", { children: [_jsx("span", { className: "font-medium", children: prediction.mainText }), prediction.secondaryText !== undefined &&
|
|
42
|
+
prediction.secondaryText !== "" ? (_jsx("span", { className: "text-muted-foreground ml-1", children: prediction.secondaryText })) : null] }) }) }, prediction.placeId))) }) }));
|
|
37
43
|
/**
|
|
38
44
|
* Location autocomplete input with a dropdown of suggestions for city or
|
|
39
45
|
* address search. The geocoder is injected: pass `search` (query to
|
|
@@ -79,8 +85,7 @@ export const LocationInput = ({ value, onChange, mode = "city", placeholder, siz
|
|
|
79
85
|
// Mirrors focusInput() default in focus-within form (see token JSDoc).
|
|
80
86
|
"focus-within:border-ring overflow-hidden rounded-md focus-within:ring-3 focus-within:ring-gray-500/80", hasError === true && hasErrorInput, disabled === true && "pointer-events-none opacity-50"), "data-component": "location-input", "data-testid": testId, children: [_jsx(Icon, { className: "text-muted-foreground shrink-0", icon: MapPin, size: "xs" }), _jsx("input", { "aria-label": "Search location", className: cn(inputVariants(), getResponsiveSizeClasses(size, LOCATION_INPUT_SIZE_CLASSES, DEFAULT_LOCATION_INPUT_SIZE)), disabled: disabled, id: id, onChange: (event) => {
|
|
81
87
|
handleInputChange(event.target.value);
|
|
82
|
-
}, onFocus: handleFocus, placeholder: effectivePlaceholder, type: "text", value: query }), hasValue && disabled !== true ? (_jsx(Button, { "aria-label": "Clear location", className: "text-muted-foreground hover:text-foreground", emphasis: "ghost", icon: X, onClick: handleClear, radius: "rounded", size: "icon-2xs", square: true, tone: "neutral", type: "button" })) : null, isLoading ? (_jsx("span", { className: "text-muted-foreground text-caption shrink-0", children: "\u2026" })) : null] }), isOpen && disabled !== true && predictions.length > 0 ? (_jsx(
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
prediction.secondaryText !== "" ? (_jsx("span", { className: "text-muted-foreground ml-1", children: prediction.secondaryText })) : null] }) }) }, prediction.placeId))) })) : null] }));
|
|
88
|
+
}, onFocus: handleFocus, placeholder: effectivePlaceholder, type: "text", value: query }), hasValue && disabled !== true ? (_jsx(Button, { "aria-label": "Clear location", className: "text-muted-foreground hover:text-foreground", emphasis: "ghost", icon: X, onClick: handleClear, radius: "rounded", size: "icon-2xs", square: true, tone: "neutral", type: "button" })) : null, isLoading ? (_jsx("span", { className: "text-muted-foreground text-caption shrink-0", children: "\u2026" })) : null] }), isOpen && disabled !== true && predictions.length > 0 ? (_jsx(Suggestions, { onSelect: (placeId) => {
|
|
89
|
+
void select(placeId);
|
|
90
|
+
}, predictions: predictions })) : null] }));
|
|
86
91
|
};
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { ComponentSize } from "@instruments/materia-ui/tokens/size";
|
|
2
|
-
import type { ReactNode } from "react";
|
|
2
|
+
import type { ComponentPropsWithoutRef, ReactNode, Ref } from "react";
|
|
3
3
|
import type { Orientation } from "../../tokens/orientation.js";
|
|
4
4
|
import type { ScrollFadeProps } from "../scroll-fade/scroll-fade-root.js";
|
|
5
5
|
/** Which edges may fade. */
|
|
@@ -11,11 +11,20 @@ export type ScrollFrameFades = "both" | "end" | "none" | "start";
|
|
|
11
11
|
* match.
|
|
12
12
|
*/
|
|
13
13
|
export type ScrollFrameFadeMode = "color" | "mask";
|
|
14
|
-
|
|
14
|
+
/** Which way the frame scrolls: one axis, or both, as for a wide table. */
|
|
15
|
+
export type ScrollFrameOrientation = Orientation | "both";
|
|
16
|
+
export interface ScrollFrameProps extends Omit<ComponentPropsWithoutRef<"div">, "aria-label" | "children" | "className"> {
|
|
15
17
|
/** Names the frame; it becomes a labelled region. */
|
|
16
18
|
"aria-label"?: string | undefined;
|
|
17
19
|
children: ReactNode;
|
|
18
20
|
className?: string | undefined;
|
|
21
|
+
/**
|
|
22
|
+
* Classes for the element holding the children, inside the scrolling one.
|
|
23
|
+
* A vertical frame's content is already as wide as the frame, so text wraps
|
|
24
|
+
* and `w-full` children fill it; a horizontal or two-way frame's content is
|
|
25
|
+
* as wide as its widest child.
|
|
26
|
+
*/
|
|
27
|
+
contentClassName?: string | undefined;
|
|
19
28
|
/**
|
|
20
29
|
* Previous and next buttons at the edges, each shown while there is more
|
|
21
30
|
* that way. They step most of a viewport, and stay hidden on touch screens,
|
|
@@ -56,12 +65,14 @@ export interface ScrollFrameProps {
|
|
|
56
65
|
*/
|
|
57
66
|
hideScrollbar?: boolean | undefined;
|
|
58
67
|
/**
|
|
59
|
-
* Scroll axis: `vertical` scrolls up and down, `horizontal` left and right, as for a row of chips.
|
|
68
|
+
* Scroll axis: `vertical` scrolls up and down, `horizontal` left and right, as for a row of chips, and `both` either way, as for a wide table. Controls and drag follow the vertical axis when both scroll.
|
|
60
69
|
* @default "vertical"
|
|
61
70
|
*/
|
|
62
|
-
orientation?:
|
|
71
|
+
orientation?: ScrollFrameOrientation | undefined;
|
|
63
72
|
/** Classes for the scrolling element. */
|
|
64
73
|
viewportClassName?: string | undefined;
|
|
74
|
+
/** The scrolling element, for a virtualiser or anything else that reads or sets its scroll position. */
|
|
75
|
+
viewportRef?: Ref<HTMLDivElement | null> | undefined;
|
|
65
76
|
}
|
|
66
77
|
/**
|
|
67
78
|
* A scrolling region that shows where there is more: each edge fades only
|
|
@@ -80,6 +91,8 @@ export interface ScrollFrameProps {
|
|
|
80
91
|
* @bestPractice Give a vertical frame a bounded height (`className="h-80"`); a horizontal one needs only a bounded width, which a flex child with `min-w-0` already has.
|
|
81
92
|
* @bestPractice Match `fadeTheme` to the surface behind the frame (`card` inside a Card). Over an image or a blur, use `fadeMode="mask"` instead.
|
|
82
93
|
* @bestPractice For a horizontal strip used with a mouse, add `dragScroll` and `controls`; both stand aside on touch screens, and each control hides at the end it points to.
|
|
94
|
+
* @bestPractice Every scrolling region in an app is a `ScrollFrame`, not a hand-rolled `overflow-y-auto` div: a pane, a list in a popover, a wide table (`orientation="both"`). A frame capped by `max-h-*` alone scrolls once its content passes the cap.
|
|
95
|
+
* @bestPractice Hand a virtualiser the scrolling element through `viewportRef`.
|
|
83
96
|
* @category layout
|
|
84
97
|
*/
|
|
85
|
-
export declare const ScrollFrame: ({ "aria-label": ariaLabel, children, className, controls, dragScroll, fadeMode, fades, fadeSize, fadeTheme, hideScrollbar, orientation, viewportClassName, }: ScrollFrameProps) => import("react").JSX.Element;
|
|
98
|
+
export declare const ScrollFrame: ({ "aria-label": ariaLabel, children, className, contentClassName, controls, dragScroll, fadeMode, fades, fadeSize, fadeTheme, hideScrollbar, orientation, viewportClassName, viewportRef, ...props }: ScrollFrameProps) => import("react").JSX.Element;
|
|
@@ -4,7 +4,7 @@ import { ScrollArea as ScrollAreaPrimitive } from "@base-ui/react/scroll-area";
|
|
|
4
4
|
import { focusRing } from "@instruments/materia-ui/tokens/focus-ring";
|
|
5
5
|
import { cn } from "@instruments/materia-ui/utils/cn";
|
|
6
6
|
import { ChevronDown, ChevronLeft, ChevronRight, ChevronUp, } from "lucide-react";
|
|
7
|
-
import { useState } from "react";
|
|
7
|
+
import { useImperativeHandle, useState } from "react";
|
|
8
8
|
import { useDragScroll } from "../../hooks/use-drag-scroll.js";
|
|
9
9
|
import { useScrollEdges } from "../../hooks/use-scroll-edges.js";
|
|
10
10
|
import { Button } from "../button.js";
|
|
@@ -23,12 +23,15 @@ const FADE_LENGTH = {
|
|
|
23
23
|
xs: "0.75rem",
|
|
24
24
|
};
|
|
25
25
|
const fadesEdge = (fades, edge) => fades === "both" || fades === edge;
|
|
26
|
-
|
|
27
|
-
|
|
26
|
+
/** The axes a frame scrolls along. */
|
|
27
|
+
const axesOf = (orientation) => orientation === "both" ? ["vertical", "horizontal"] : [orientation];
|
|
28
|
+
/** The one axis that controls and drag move: vertical when both scroll. */
|
|
29
|
+
const leadAxis = (orientation) => orientation === "horizontal" ? "horizontal" : "vertical";
|
|
30
|
+
const maskRamp = (axis, length, start, end) => {
|
|
31
|
+
const direction = axis === "horizontal" ? "to right" : "to bottom";
|
|
28
32
|
const from = start ? length : "0px";
|
|
29
33
|
const to = end ? length : "0px";
|
|
30
|
-
|
|
31
|
-
return { WebkitMaskImage: image, maskImage: image };
|
|
34
|
+
return `linear-gradient(${direction}, transparent, black ${from}, black calc(100% - ${to}), transparent)`;
|
|
32
35
|
};
|
|
33
36
|
const prefersReducedMotion = () => globalThis.matchMedia?.("(prefers-reduced-motion: reduce)").matches ?? false;
|
|
34
37
|
const CONTROL_ICONS = {
|
|
@@ -60,20 +63,39 @@ const moreOf = (edges) => ({
|
|
|
60
63
|
end: edges.scrollable && !edges.atEnd,
|
|
61
64
|
start: edges.scrollable && !edges.atStart,
|
|
62
65
|
});
|
|
63
|
-
/** The viewport's mask in mask mode; nothing in colour mode. */
|
|
64
|
-
const frameMask = (fadeMode, orientation, fadeSize, fades, more) =>
|
|
65
|
-
|
|
66
|
-
|
|
66
|
+
/** The viewport's mask in mask mode, one ramp per axis intersected; nothing in colour mode. */
|
|
67
|
+
const frameMask = (fadeMode, orientation, fadeSize, fades, more) => {
|
|
68
|
+
if (fadeMode !== "mask") {
|
|
69
|
+
return undefined;
|
|
70
|
+
}
|
|
71
|
+
const image = axesOf(orientation)
|
|
72
|
+
.map((axis) => maskRamp(axis, FADE_LENGTH[fadeSize], more[axis].start && fadesEdge(fades, "start"), more[axis].end && fadesEdge(fades, "end")))
|
|
73
|
+
.join(", ");
|
|
74
|
+
return orientation === "both"
|
|
75
|
+
? {
|
|
76
|
+
maskComposite: "intersect",
|
|
77
|
+
maskImage: image,
|
|
78
|
+
WebkitMaskComposite: "source-in",
|
|
79
|
+
WebkitMaskImage: image,
|
|
80
|
+
}
|
|
81
|
+
: { maskImage: image, WebkitMaskImage: image };
|
|
82
|
+
};
|
|
67
83
|
/** The colour fades, each shown while there is more beyond its edge. */
|
|
68
|
-
const Fades = ({ fades, fadeSize, fadeTheme, more, orientation, }) => EDGES.filter((edge) => fadesEdge(fades, edge)).map((edge) => (_jsx(ScrollFade, { className: cn("duration-normal transition-opacity motion-reduce:transition-none", !more[edge] && "opacity-0"), "data-slot": "scroll-frame-fade", orientation:
|
|
84
|
+
const Fades = ({ fades, fadeSize, fadeTheme, more, orientation, }) => axesOf(orientation).flatMap((axis) => EDGES.filter((edge) => fadesEdge(fades, edge)).map((edge) => (_jsx(ScrollFade, { className: cn("duration-normal transition-opacity motion-reduce:transition-none", !more[axis][edge] && "opacity-0"), "data-slot": "scroll-frame-fade", orientation: axis, position: edge, size: fadeSize, theme: fadeTheme }, `${axis}-${edge}`))));
|
|
69
85
|
/** Previous and next, each shown while there is more its way. */
|
|
70
86
|
const Controls = ({ more, orientation, viewport, }) => EDGES.map((edge) => (_jsx(Button, { "aria-label": edge === "start" ? "Scroll back" : "Scroll on", className: cn("duration-normal absolute z-20 transition-opacity after:absolute after:-inset-1.5 motion-reduce:transition-none pointer-coarse:hidden", CONTROL_PLACEMENT[orientation][edge], !more[edge] && "pointer-events-none opacity-0"), "data-slot": "scroll-frame-control", emphasis: "outline", icon: CONTROL_ICONS[orientation][edge], onClick: () => {
|
|
71
87
|
stepBy(viewport, orientation, edge);
|
|
72
88
|
}, radius: "full", size: "icon-sm", square: true, tabIndex: more[edge] ? undefined : -1, tone: "neutral", type: "button" }, edge)));
|
|
73
89
|
/** The scrolling element and its content, which a mouse can drag. */
|
|
74
|
-
const Viewport = ({ children, className, dragScroll, mask, onViewport, orientation, scrollable, viewport, }) => {
|
|
75
|
-
const drag = useDragScroll(viewport, orientation, {
|
|
76
|
-
|
|
90
|
+
const Viewport = ({ children, className, contentClassName, dragScroll, mask, onViewport, orientation, scrollable, viewport, }) => {
|
|
91
|
+
const drag = useDragScroll(viewport, leadAxis(orientation), {
|
|
92
|
+
enabled: dragScroll,
|
|
93
|
+
});
|
|
94
|
+
return (_jsx(ScrollAreaPrimitive.Viewport, { className: cn(focusRing(),
|
|
95
|
+
// A frame sized only by max-height still scrolls: the viewport takes that cap as its own.
|
|
96
|
+
"rounded-inherit size-full max-h-[inherit] focus-visible:ring-inset", className), "data-slot": "scroll-frame-viewport", ref: onViewport, style: mask, children: _jsx(ScrollAreaPrimitive.Content, { className: cn(
|
|
97
|
+
// Base UI sizes the content to its widest child; a vertical frame lays out at its own width.
|
|
98
|
+
orientation === "vertical" && "min-w-0!", dragScroll && scrollable && "cursor-grab", drag.dragging && "cursor-grabbing select-none", contentClassName), "data-dragging": drag.dragging ? "" : undefined, ...drag.handlers, children: children }) }));
|
|
77
99
|
};
|
|
78
100
|
/**
|
|
79
101
|
* A scrolling region that shows where there is more: each edge fades only
|
|
@@ -92,11 +114,21 @@ const Viewport = ({ children, className, dragScroll, mask, onViewport, orientati
|
|
|
92
114
|
* @bestPractice Give a vertical frame a bounded height (`className="h-80"`); a horizontal one needs only a bounded width, which a flex child with `min-w-0` already has.
|
|
93
115
|
* @bestPractice Match `fadeTheme` to the surface behind the frame (`card` inside a Card). Over an image or a blur, use `fadeMode="mask"` instead.
|
|
94
116
|
* @bestPractice For a horizontal strip used with a mouse, add `dragScroll` and `controls`; both stand aside on touch screens, and each control hides at the end it points to.
|
|
117
|
+
* @bestPractice Every scrolling region in an app is a `ScrollFrame`, not a hand-rolled `overflow-y-auto` div: a pane, a list in a popover, a wide table (`orientation="both"`). A frame capped by `max-h-*` alone scrolls once its content passes the cap.
|
|
118
|
+
* @bestPractice Hand a virtualiser the scrolling element through `viewportRef`.
|
|
95
119
|
* @category layout
|
|
96
120
|
*/
|
|
97
|
-
export const ScrollFrame = ({ "aria-label": ariaLabel, children, className, controls = false, dragScroll = false, fadeMode = "color", fades = "both", fadeSize = "base", fadeTheme = "background", hideScrollbar = false, orientation = "vertical", viewportClassName, }) => {
|
|
121
|
+
export const ScrollFrame = ({ "aria-label": ariaLabel, children, className, contentClassName, controls = false, dragScroll = false, fadeMode = "color", fades = "both", fadeSize = "base", fadeTheme = "background", hideScrollbar = false, orientation = "vertical", viewportClassName, viewportRef, ...props }) => {
|
|
98
122
|
const [viewport, setViewport] = useState(null);
|
|
99
|
-
const
|
|
100
|
-
const
|
|
101
|
-
|
|
123
|
+
const vertical = useScrollEdges(viewport, "vertical");
|
|
124
|
+
const horizontal = useScrollEdges(viewport, "horizontal");
|
|
125
|
+
const more = {
|
|
126
|
+
horizontal: moreOf(horizontal),
|
|
127
|
+
vertical: moreOf(vertical),
|
|
128
|
+
};
|
|
129
|
+
const lead = leadAxis(orientation);
|
|
130
|
+
useImperativeHandle(viewportRef, () => viewport, [viewport]);
|
|
131
|
+
return (_jsxs(ScrollAreaPrimitive.Root, { ...props, "aria-label": ariaLabel, className: cn("relative min-w-0 overflow-hidden", className), "data-component": "scroll-frame", "data-orientation": orientation, "data-slot": "scroll-frame", role: ariaLabel === undefined ? undefined : "region", children: [_jsx(Viewport, { className: viewportClassName, contentClassName: contentClassName, dragScroll: dragScroll, mask: frameMask(fadeMode, orientation, fadeSize, fades, more), onViewport: setViewport, orientation: orientation, scrollable: (lead === "vertical" ? vertical : horizontal).scrollable, viewport: viewport, children: children }), hideScrollbar
|
|
132
|
+
? null
|
|
133
|
+
: axesOf(orientation).map((axis) => (_jsx(ScrollBar, { orientation: axis }, axis))), orientation === "both" ? _jsx(ScrollAreaPrimitive.Corner, {}) : null, fadeMode === "color" ? (_jsx(Fades, { fadeSize: fadeSize, fadeTheme: fadeTheme, fades: fades, more: more, orientation: orientation })) : null, controls ? (_jsx(Controls, { more: more[lead], orientation: lead, viewport: viewport })) : null] }));
|
|
102
134
|
};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
export { ScrollFrame, type ScrollFrameFadeMode, type ScrollFrameFades, type ScrollFrameProps, } from "./scroll-frame/scroll-frame-root.js";
|
|
1
|
+
export { ScrollFrame, type ScrollFrameFadeMode, type ScrollFrameFades, type ScrollFrameOrientation, type ScrollFrameProps, } from "./scroll-frame/scroll-frame-root.js";
|
|
@@ -7,7 +7,7 @@ import type { ButtonProps } from "../button.js";
|
|
|
7
7
|
* SelectValue placeholder), with a trailing chevron; forwards Button styling
|
|
8
8
|
* props - defaults to `size="xl"`, `emphasis="outline"`, `radius="rounded"`.
|
|
9
9
|
*/
|
|
10
|
-
declare const SelectTrigger: ({ className, children, testId, tone, size, emphasis, radius, pressed, ...props }: WithTestId<Omit<SelectPrimitive.Trigger.Props, "className" | "render"> &
|
|
10
|
+
declare const SelectTrigger: ({ className, children, testId, tone, size, emphasis, radius, pressed, ...props }: WithTestId<Omit<SelectPrimitive.Trigger.Props, "className" | "render"> & {
|
|
11
11
|
className?: string;
|
|
12
12
|
/** Semantic colour of the trigger, as on Button. @default "neutral" */
|
|
13
13
|
tone?: ButtonProps["tone"];
|
|
@@ -7,9 +7,10 @@ type SidepanelBodyProps = ComponentProps<"div">;
|
|
|
7
7
|
declare const SidepanelBody: ({ className, ...props }: SidepanelBodyProps) => import("react").JSX.Element;
|
|
8
8
|
type SidepanelScrollProps = ComponentProps<"div">;
|
|
9
9
|
/**
|
|
10
|
-
* The scrolling region between a `SidepanelHeader` and `SidepanelFooter
|
|
11
|
-
* takes the remaining height of `SidepanelBody`, scrolls on its
|
|
12
|
-
* the panel's standard inset, so header and footer stay put
|
|
10
|
+
* The scrolling region between a `SidepanelHeader` and `SidepanelFooter`, a
|
|
11
|
+
* `ScrollFrame`: takes the remaining height of `SidepanelBody`, scrolls on its
|
|
12
|
+
* own and carries the panel's standard inset, so header and footer stay put
|
|
13
|
+
* while it moves.
|
|
13
14
|
*
|
|
14
15
|
* @bestPractice Put exactly one `SidepanelScroll` inside `SidepanelBody`, between the header and footer, rather than a hand-rolled `flex-1 overflow-y-auto` div.
|
|
15
16
|
*/
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
import { jsx as _jsx } from "react/jsx-runtime";
|
|
3
3
|
import { Flex } from "@instruments/materia-ui/components/flex";
|
|
4
4
|
import { cn } from "@instruments/materia-ui/utils/cn";
|
|
5
|
+
import { ScrollFrame } from "../scroll-frame.js";
|
|
5
6
|
/**
|
|
6
7
|
* Body container for sidepanel content. Provides vertical flex layout
|
|
7
8
|
* for header, scrollable content, and footer.
|
|
@@ -10,11 +11,12 @@ const SidepanelBody = ({ className, ...props }) => (_jsx(Flex, { className: cn("
|
|
|
10
11
|
// Allow text selection in the body, even inside a swipe-dismissable surface
|
|
11
12
|
"select-text", className), "data-component": "sidepanel-body", "data-slot": "sidepanel-body", direction: "column", ...props }));
|
|
12
13
|
/**
|
|
13
|
-
* The scrolling region between a `SidepanelHeader` and `SidepanelFooter
|
|
14
|
-
* takes the remaining height of `SidepanelBody`, scrolls on its
|
|
15
|
-
* the panel's standard inset, so header and footer stay put
|
|
14
|
+
* The scrolling region between a `SidepanelHeader` and `SidepanelFooter`, a
|
|
15
|
+
* `ScrollFrame`: takes the remaining height of `SidepanelBody`, scrolls on its
|
|
16
|
+
* own and carries the panel's standard inset, so header and footer stay put
|
|
17
|
+
* while it moves.
|
|
16
18
|
*
|
|
17
19
|
* @bestPractice Put exactly one `SidepanelScroll` inside `SidepanelBody`, between the header and footer, rather than a hand-rolled `flex-1 overflow-y-auto` div.
|
|
18
20
|
*/
|
|
19
|
-
const SidepanelScroll = ({ className, ...props }) => (_jsx(
|
|
21
|
+
const SidepanelScroll = ({ className, ...props }) => (_jsx(ScrollFrame, { className: "min-h-0 flex-1", fades: "end", children: _jsx(Flex, { className: cn("px-6 py-4", className), "data-component": "sidepanel-scroll", "data-slot": "sidepanel-scroll", direction: "column", gap: "base", ...props }) }));
|
|
20
22
|
export { SidepanelBody, SidepanelScroll, };
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { Radio as RadioPrimitive } from "@base-ui/react/radio";
|
|
2
2
|
import type React from "react";
|
|
3
3
|
import type { SwatchSize } from "../../tokens/swatch.js";
|
|
4
|
-
type SwatchGroupItemProps = Omit<RadioPrimitive.Root.Props<string>, "children"> & {
|
|
4
|
+
type SwatchGroupItemProps = Omit<RadioPrimitive.Root.Props<string>, "aria-label" | "children"> & {
|
|
5
5
|
/**
|
|
6
6
|
* CSS colour value (hex, rgb, hsl, etc.).
|
|
7
7
|
* If provided along with children, children render on top.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { ComponentProps } from "react";
|
|
2
2
|
import type { SwatchSize } from "../../tokens/swatch.js";
|
|
3
|
-
type SwatchSelectorItemProps = Omit<ComponentProps<"button">, "children" | "defaultValue" | "value"> & {
|
|
3
|
+
type SwatchSelectorItemProps = Omit<ComponentProps<"button">, "aria-label" | "children" | "defaultValue" | "value"> & {
|
|
4
4
|
/** Unique value for this swatch */
|
|
5
5
|
value: string;
|
|
6
6
|
/**
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type * as React from "react";
|
|
2
2
|
/**
|
|
3
|
-
* Semantic `<table>` primitive, wrapped in a
|
|
3
|
+
* Semantic `<table>` primitive, wrapped in a horizontal `ScrollFrame`
|
|
4
4
|
* so wide tables overflow rather than break layout. Compose with the exported
|
|
5
5
|
* `TableHeader`, `TableBody`, `TableRow`, `TableHead`, `TableCell`,
|
|
6
6
|
* `TableFooter`, and `TableCaption` parts.
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
import { jsx as _jsx } from "react/jsx-runtime";
|
|
3
3
|
import { cn } from "@instruments/materia-ui/utils/cn";
|
|
4
|
+
import { ScrollFrame } from "../scroll-frame.js";
|
|
4
5
|
/**
|
|
5
|
-
* Semantic `<table>` primitive, wrapped in a
|
|
6
|
+
* Semantic `<table>` primitive, wrapped in a horizontal `ScrollFrame`
|
|
6
7
|
* so wide tables overflow rather than break layout. Compose with the exported
|
|
7
8
|
* `TableHeader`, `TableBody`, `TableRow`, `TableHead`, `TableCell`,
|
|
8
9
|
* `TableFooter`, and `TableCaption` parts.
|
|
@@ -12,7 +13,7 @@ import { cn } from "@instruments/materia-ui/utils/cn";
|
|
|
12
13
|
* @bestPractice Compose real `TableHeader`/`TableBody`/`TableRow`/`TableHead`/`TableCell` parts (semantic `thead`/`tbody`/`th`/`td`); don't fake rows with styled divs or you lose the table semantics.
|
|
13
14
|
* @category data
|
|
14
15
|
*/
|
|
15
|
-
const Table = ({ className, ...props }) => (_jsx(
|
|
16
|
+
const Table = ({ className, ...props }) => (_jsx(ScrollFrame, { className: "w-full", contentClassName: "min-w-0!", orientation: "horizontal", children: _jsx("table", { className: cn("text-foreground text-paragraph-sm w-full caption-bottom", className), "data-component": "table", "data-slot": "table", ...props }) }));
|
|
16
17
|
/** table header section */
|
|
17
18
|
const TableHeader = ({ className, ...props }) => (_jsx("thead", { className: cn("[&_tr]:border-b", className), "data-component": "table-header", "data-slot": "table-header", ...props }));
|
|
18
19
|
/**
|
|
@@ -6,6 +6,7 @@ import { Button } from "@instruments/materia-ui/components/button";
|
|
|
6
6
|
import { Center } from "@instruments/materia-ui/components/center";
|
|
7
7
|
import { Flex } from "@instruments/materia-ui/components/flex";
|
|
8
8
|
import { Icon } from "@instruments/materia-ui/components/icon";
|
|
9
|
+
import { ScrollFrame } from "@instruments/materia-ui/components/scroll-frame";
|
|
9
10
|
import { Separator } from "@instruments/materia-ui/components/separator";
|
|
10
11
|
import { focusRing } from "@instruments/materia-ui/tokens/focus-ring";
|
|
11
12
|
import { cn } from "@instruments/materia-ui/utils/cn";
|
|
@@ -57,6 +58,6 @@ const DataGridColumnFilter = ({ column, title, options, }) => {
|
|
|
57
58
|
// It renders the kit's Button, a native <button>.
|
|
58
59
|
, {
|
|
59
60
|
// It renders the kit's Button, a native <button>.
|
|
60
|
-
nativeButton: true, "aria-label": title, render: _jsx(Button, { tone: "neutral", emphasis: "outline", "data-component": "data-grid-column-filter", pressed: false, size: "sm" }), children: [_jsx(Icon, { icon: CirclePlus, size: "xs" }), title, _jsx(FilterTriggerBadges, { options: options, selectedValues: selectedValues })] }), _jsx(ComboboxPrimitive.Portal, { children: _jsx(ComboboxPrimitive.Positioner, { align: "start", className: "z-50", sideOffset: 4, children: _jsx(ComboboxPrimitive.Popup, { "aria-label": title, className: `${POPOVER_CONTENT_ANIMATION} ${POPOVER_SURFACE} w-50 origin-(--transform-origin) rounded-3xl`, "data-slot": "data-grid-column-filter-content", children: _jsxs(Flex, { className: "bg-popover text-popover-foreground size-full overflow-hidden rounded-lg border shadow-xs", direction: "column", children: [_jsxs(Flex, { align: "center", className: "h-9 border-b px-3", gap: "xs", children: [_jsx(Icon, { className: "shrink-0 opacity-50", icon: Search, size: "xs" }), _jsx(ComboboxPrimitive.Input, { className: "placeholder:text-muted-foreground text-paragraph-sm flex h-10 w-full bg-transparent py-3 outline-hidden", placeholder: title })] }), _jsx(ComboboxPrimitive.Empty, { className: "text-paragraph-sm py-6 text-center empty:p-0", children: "No results found." }), _jsx(
|
|
61
|
+
nativeButton: true, "aria-label": title, render: _jsx(Button, { tone: "neutral", emphasis: "outline", "data-component": "data-grid-column-filter", pressed: false, size: "sm" }), children: [_jsx(Icon, { icon: CirclePlus, size: "xs" }), title, _jsx(FilterTriggerBadges, { options: options, selectedValues: selectedValues })] }), _jsx(ComboboxPrimitive.Portal, { children: _jsx(ComboboxPrimitive.Positioner, { align: "start", className: "z-50", sideOffset: 4, children: _jsx(ComboboxPrimitive.Popup, { "aria-label": title, className: `${POPOVER_CONTENT_ANIMATION} ${POPOVER_SURFACE} w-50 origin-(--transform-origin) rounded-3xl`, "data-slot": "data-grid-column-filter-content", children: _jsxs(Flex, { className: "bg-popover text-popover-foreground size-full overflow-hidden rounded-lg border shadow-xs", direction: "column", children: [_jsxs(Flex, { align: "center", className: "h-9 border-b px-3", gap: "xs", children: [_jsx(Icon, { className: "shrink-0 opacity-50", icon: Search, size: "xs" }), _jsx(ComboboxPrimitive.Input, { className: "placeholder:text-muted-foreground text-paragraph-sm flex h-10 w-full bg-transparent py-3 outline-hidden", placeholder: title })] }), _jsx(ComboboxPrimitive.Empty, { className: "text-paragraph-sm py-6 text-center empty:p-0", children: "No results found." }), _jsx(ScrollFrame, { className: "max-h-75", fadeTheme: "popover", fades: "end", children: _jsx(ComboboxPrimitive.List, { className: "scroll-py-1 p-1 empty:hidden", children: (option) => (_jsx(FilterOptionItem, { count: facets?.get(option.value) ?? 0, isSelected: selectedValues.has(option.value), option: option }, option.value)) }) }), selectedValues.size > 0 && (_jsx("div", { className: "border-t p-1", children: _jsx("button", { className: cn(focusRing(), "hover:bg-accent hover:text-accent-foreground text-paragraph-sm flex w-full cursor-default items-center justify-center rounded-sm px-2.5 py-1.5 select-none"), onClick: () => column?.setFilterValue(undefined), type: "button", children: "Clear filters" }) }))] }) }) }) })] }));
|
|
61
62
|
};
|
|
62
63
|
export { DataGridColumnFilter };
|
|
@@ -3,6 +3,7 @@ import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-run
|
|
|
3
3
|
import { DataGridFilterableHeader, useDataGrid, } from "@instruments/materia-ui/compositions/data-grid";
|
|
4
4
|
import { Fragment } from "react";
|
|
5
5
|
import { Center } from "../../components/center.js";
|
|
6
|
+
import { ScrollFrame } from "../../components/scroll-frame.js";
|
|
6
7
|
import { Spinner } from "../../components/spinner/spinner-root.js";
|
|
7
8
|
import { DataGridTableBody, DataGridTableBodyRow, DataGridTableBodyRowCell, DataGridTableBodyRowExpandded, DataGridTableBodyRowSkeleton, DataGridTableBodyRowSkeletonCell, DataGridTableEmpty, DataGridTableRowSpacer, flexRender, } from "../data-grid-table/data-grid-table-body.js";
|
|
8
9
|
import { DataGridTableBase, DataGridTableHead, DataGridTableHeadRow, DataGridTableHeadRowCell, DataGridTableHeadRowCellResize, } from "../data-grid-table/data-grid-table-header.js";
|
|
@@ -100,7 +101,7 @@ const DataGridTable = () => {
|
|
|
100
101
|
rowHeight,
|
|
101
102
|
rows,
|
|
102
103
|
});
|
|
103
|
-
return (_jsx(
|
|
104
|
+
return (_jsx(ScrollFrame, { contentClassName: "min-w-0!", fades: "none", orientation: "both", style: { maxHeight: "calc(100vh - 8rem)" }, viewportRef: tableContainerRef, children: _jsxs(DataGridTableBase, { children: [_jsx(DataGridTableHeadContent, { context: context }), (props.tableLayout?.striped === true ||
|
|
104
105
|
props.tableLayout?.rowBorder !== true) && _jsx(DataGridTableRowSpacer, {}), _jsx(DataGridTableBody, { children: _jsx(DataGridTableBodyContent, { context: context, measureRow: measureRow, paddingBottom: paddingBottom, paddingTop: paddingTop, pageSize: pagination.pageSize, renderedRowIndexes: renderedRowIndexes, rows: rows }) })] }) }));
|
|
105
106
|
};
|
|
106
107
|
export { DataGridTableBody, DataGridTableBodyRow, DataGridTableBodyRowCell, DataGridTableBodyRowExpandded, DataGridTableBodyRowSkeleton, DataGridTableBodyRowSkeletonCell, DataGridTableEmpty, DataGridTableLoader, DataGridTableRowSelect, DataGridTableRowSelectAll, DataGridTableRowSpacer, } from "../data-grid-table/data-grid-table-body.js";
|
|
@@ -3,6 +3,7 @@ import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-run
|
|
|
3
3
|
import { Box } from "@instruments/materia-ui/components/box";
|
|
4
4
|
import { Grid } from "@instruments/materia-ui/components/grid";
|
|
5
5
|
import { PopoverBackdrop, PopoverContent, } from "@instruments/materia-ui/components/popover";
|
|
6
|
+
import { ScrollFrame } from "@instruments/materia-ui/components/scroll-frame";
|
|
6
7
|
import { cn } from "@instruments/materia-ui/utils/cn";
|
|
7
8
|
// The window's top edge. The panel hangs from it, full width, in the shell's
|
|
8
9
|
// overlay layer: above the sidebar's nav and the page, beneath the chrome
|
|
@@ -33,6 +34,6 @@ const BACKDROP_MOTION = [
|
|
|
33
34
|
export const MegaMenuContent = ({ "aria-label": ariaLabel, children, className, ...props }) => (_jsxs(_Fragment, { children: [_jsx(PopoverBackdrop, { className: cn("z-30 bg-black/20 backdrop-blur-sm", BACKDROP_MOTION) }), _jsx(PopoverContent, { align: "start", anchor: WINDOW_TOP,
|
|
34
35
|
// It runs edge to edge across the window, so it is never nudged in from
|
|
35
36
|
// the viewport's edge.
|
|
36
|
-
collisionPadding: 0, className: cn("bg-background max-h-svh w-(--anchor-width) overflow-
|
|
37
|
+
collisionPadding: 0, className: cn("bg-background max-h-svh w-(--anchor-width) overflow-hidden rounded-none p-0 shadow-xl", PANEL_MOTION, className), "data-component": "mega-menu-content", "data-slot": "mega-menu-content",
|
|
37
38
|
// Beneath the chrome's layer, above the sidebar's.
|
|
38
|
-
positionerClassName: "z-30", side: "bottom", sideOffset: 0, ...props, children: _jsx(Box, { "aria-label": ariaLabel, as: "nav", className: "pt-header @container", children: _jsx(Grid, { align: "flex-start", className: "px-page-gutter pt-6 pb-10", columns: { base: 2, lg: 3 }, "data-slot": "mega-menu-grid", responsiveMode: "container", xGap: "2xl", yGap: "lg", children: children }) }) })] }));
|
|
39
|
+
positionerClassName: "z-30", side: "bottom", sideOffset: 0, ...props, children: _jsx(ScrollFrame, { className: "max-h-svh", fades: "none", children: _jsx(Box, { "aria-label": ariaLabel, as: "nav", className: "pt-header @container", children: _jsx(Grid, { align: "flex-start", className: "px-page-gutter pt-6 pb-10", columns: { base: 2, lg: 3 }, "data-slot": "mega-menu-grid", responsiveMode: "container", xGap: "2xl", yGap: "lg", children: children }) }) }) })] }));
|
|
@@ -38,7 +38,7 @@ export declare const MobileNavigationHeaderSearchSlot: ({ className, ...props }:
|
|
|
38
38
|
*
|
|
39
39
|
* @bestPractice Keep children to a few characters, such as a cart count, and give the action an `aria-label` that says what the number means.
|
|
40
40
|
*/
|
|
41
|
-
export declare const MobileNavigationHeaderAction: ({ children, className, emphasis, ...props }: React.ComponentProps<typeof Button> & {
|
|
41
|
+
export declare const MobileNavigationHeaderAction: ({ children, className, emphasis, ...props }: Omit<React.ComponentProps<typeof Button>, "emphasis"> & {
|
|
42
42
|
/** Visual weight, as on Button. @default "soft" */
|
|
43
43
|
emphasis?: React.ComponentProps<typeof Button>["emphasis"];
|
|
44
44
|
}) => React.JSX.Element;
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { ScrollArea } from "@instruments/materia-ui/components/scroll-area";
|
|
2
2
|
import type { ComponentProps } from "react";
|
|
3
|
-
export type QuickViewBodyProps = Omit<ComponentProps<typeof ScrollArea>, "as" | "orientation" | "render"> & {
|
|
3
|
+
export type QuickViewBodyProps = Omit<ComponentProps<typeof ScrollArea>, "aria-label" | "as" | "orientation" | "render"> & {
|
|
4
4
|
/** Names the scrolling region for assistive tech, e.g. "Product details". */
|
|
5
5
|
"aria-label": string;
|
|
6
6
|
};
|
|
@@ -1,3 +1,3 @@
|
|
|
1
1
|
import type React from "react";
|
|
2
|
-
/**
|
|
2
|
+
/** The sidebar's main region between header and footer, in a `ScrollFrame`; it stops scrolling when collapsed to an icon rail. */
|
|
3
3
|
export declare const SidebarContent: ({ className, ...props }: React.ComponentProps<"div">) => React.JSX.Element;
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
import { jsx as _jsx } from "react/jsx-runtime";
|
|
3
3
|
import { Flex } from "@instruments/materia-ui/components/flex";
|
|
4
|
+
import { ScrollFrame } from "@instruments/materia-ui/components/scroll-frame";
|
|
4
5
|
import { cn } from "@instruments/materia-ui/utils/cn";
|
|
5
|
-
/**
|
|
6
|
-
export const SidebarContent = ({ className, ...props }) => (_jsx(
|
|
6
|
+
/** The sidebar's main region between header and footer, in a `ScrollFrame`; it stops scrolling when collapsed to an icon rail. */
|
|
7
|
+
export const SidebarContent = ({ className, ...props }) => (_jsx(ScrollFrame, { className: "min-h-0 flex-1", fadeTheme: "gray", fades: "none", viewportClassName: "group-data-[collapsible=icon]:overflow-hidden!", children: _jsx(Flex, { className: cn("min-h-full", className), "data-component": "sidebar-content", "data-sidebar": "content", "data-slot": "sidebar-content", direction: "column", ...props }) }));
|
|
@@ -6,7 +6,7 @@ export declare const SidebarGroup: ({ className, ...props }: React.ComponentProp
|
|
|
6
6
|
/** Heading for a {@link SidebarGroup}; fades out (keeping layout) when collapsed to an icon rail. Pass `render` to use a custom element. */
|
|
7
7
|
export declare const SidebarGroupLabel: ({ className, render, ...props }: useRender.ComponentProps<"div">) => React.ReactElement<unknown, string | React.JSXElementConstructor<any>>;
|
|
8
8
|
/** Icon button pinned to a group's top-right corner (defaults to a `+`); hidden when collapsed unless `showWhenCollapsed`. */
|
|
9
|
-
export declare const SidebarGroupAction: ({ className, icon, size, radius, emphasis, showWhenCollapsed, ...props }: React.ComponentProps<typeof Button> & {
|
|
9
|
+
export declare const SidebarGroupAction: ({ className, icon, size, radius, emphasis, showWhenCollapsed, ...props }: Omit<React.ComponentProps<typeof Button>, "emphasis" | "size"> & {
|
|
10
10
|
/** Size token, as on Button. @default "icon-sm" */
|
|
11
11
|
size?: React.ComponentProps<typeof Button>["size"];
|
|
12
12
|
/** Visual weight, as on Button. @default "ghost" */
|
|
@@ -6,7 +6,7 @@ type SidebarMenuSubProps = React.ComponentProps<"ul">;
|
|
|
6
6
|
/** The `<ul>` container for a nested submenu under a menu item; hidden when the sidebar collapses to an icon rail. */
|
|
7
7
|
export declare const SidebarMenuSub: ({ className, ...props }: SidebarMenuSubProps) => React.JSX.Element;
|
|
8
8
|
type SidebarMenuSubScrollAreaProps = React.ComponentProps<"div"> & {
|
|
9
|
-
/**
|
|
9
|
+
/** Fade the bottom edge while more of the list is below. @default false */
|
|
10
10
|
fade?: boolean;
|
|
11
11
|
/** Cap the height: unconstrained, or clamped to the shared dropdown max. @default "none" */
|
|
12
12
|
maxHeight?: "none" | "dropdown";
|
|
@@ -19,7 +19,7 @@ export declare const SidebarMenuSubScrollArea: ({ children, className, fade, max
|
|
|
19
19
|
type SidebarMenuSubItemProps = React.ComponentProps<"li">;
|
|
20
20
|
/** The `<li>` wrapper for a single submenu row. */
|
|
21
21
|
export declare const SidebarMenuSubItem: ({ className, ...props }: SidebarMenuSubItemProps) => React.JSX.Element;
|
|
22
|
-
type SidebarMenuSubButtonProps = Omit<MenuItemProps, "icon" | "
|
|
22
|
+
type SidebarMenuSubButtonProps = Omit<MenuItemProps, "dot" | "icon" | "iconClassName" | "size" | "variant"> & {
|
|
23
23
|
/** Dot indicator in the left column (takes precedence over icon): `true` for a neutral dot, or a {@link Tone} to tint it. */
|
|
24
24
|
dot?: boolean | Tone;
|
|
25
25
|
/** Icon to display in the left icon column */
|