@instruments/materia-ui 0.4.8 → 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/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/sidepanel/sidepanel-body.d.ts +4 -3
- package/dist/components/sidepanel/sidepanel-body.js +6 -4
- 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/sidebar/sidebar-content.d.ts +1 -1
- package/dist/compositions/sidebar/sidebar-content.js +3 -2
- package/dist/compositions/sidebar/sidebar-menu-sub.d.ts +1 -1
- package/dist/compositions/sidebar/sidebar-menu-sub.js +2 -1
- package/dist/compositions/tag-selector/tag-selector-list.js +4 -3
- package/dist/data/docs-manifest.js +65 -15
- 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 }) }));
|
|
@@ -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,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,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 }) }) }) })] }));
|
|
@@ -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 @@ 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";
|
|
@@ -5,6 +5,7 @@ import { Dot } from "@instruments/materia-ui/components/dot";
|
|
|
5
5
|
import { Flex } from "@instruments/materia-ui/components/flex";
|
|
6
6
|
import { Icon } from "@instruments/materia-ui/components/icon";
|
|
7
7
|
import { MenuItem } from "@instruments/materia-ui/components/menu-item";
|
|
8
|
+
import { ScrollFrame } from "@instruments/materia-ui/components/scroll-frame";
|
|
8
9
|
import { cn } from "@instruments/materia-ui/utils/cn";
|
|
9
10
|
import { isString } from "../../utils/type-guards.js";
|
|
10
11
|
/** The `<ul>` container for a nested submenu under a menu item; hidden when the sidebar collapses to an icon rail. */
|
|
@@ -13,7 +14,7 @@ export const SidebarMenuSub = ({ className, ...props }) => (_jsx(Flex, { as: "ul
|
|
|
13
14
|
* Scroll viewport for sidebar submenu content inside floating surfaces.
|
|
14
15
|
* Keeps menu list scrolling, scrollbar styling, and bottom fade treatment centralised.
|
|
15
16
|
*/
|
|
16
|
-
export const SidebarMenuSubScrollArea = ({ children, className, fade = false, maxHeight = "none", ...props }) => (
|
|
17
|
+
export const SidebarMenuSubScrollArea = ({ children, className, fade = false, maxHeight = "none", ...props }) => (_jsx(ScrollFrame, { ...props, className: cn("min-h-0", maxHeight === "dropdown" && "max-h-menu-sidebar-list", className), "data-component": "sidebar-menu-sub-scroll-area", "data-slot": "sidebar-menu-sub-scroll-area", fadeSize: "3xl", fadeTheme: "popover", fades: fade ? "end" : "none", children: children }));
|
|
17
18
|
/** The `<li>` wrapper for a single submenu row. */
|
|
18
19
|
export const SidebarMenuSubItem = ({ className, ...props }) => (_jsx("li", { className: cn("group/menu-sub-item relative", className), "data-component": "sidebar-menu-sub-item", "data-sidebar": "menu-sub-item", "data-slot": "sidebar-menu-sub-item", ...props }));
|
|
19
20
|
/**
|
|
@@ -7,6 +7,7 @@ import { Plus } from "lucide-react";
|
|
|
7
7
|
import { useMemo } from "react";
|
|
8
8
|
import { Button } from "../../components/button.js";
|
|
9
9
|
import { Flex } from "../../components/flex.js";
|
|
10
|
+
import { ScrollFrame } from "../../components/scroll-frame.js";
|
|
10
11
|
import { TagSelectorColorSwatches } from "../tag-selector/tag-selector-color-swatches.js";
|
|
11
12
|
import { useTagSelectorContext } from "../tag-selector/tag-selector-context.js";
|
|
12
13
|
import { TagSelectorItem } from "../tag-selector/tag-selector-item.js";
|
|
@@ -94,7 +95,7 @@ export const TagSelectorList = ({ emptyMessage = "No tags found.", className, })
|
|
|
94
95
|
const exactMatch = tags.some((tag) => tag.label.toLowerCase() === searchQuery.toLowerCase());
|
|
95
96
|
const showCreateOption = allowCreate && searchQuery !== "" && !exactMatch;
|
|
96
97
|
const selectedIds = new Set(value.map((t) => t.id));
|
|
97
|
-
return (_jsx(
|
|
98
|
-
|
|
99
|
-
|
|
98
|
+
return (_jsx(ScrollFrame, { className: cn("max-h-75", className), fadeTheme: "popover", fades: "end", children: _jsx(Flex, { className: "p-1", "data-component": "tag-selector-list", "data-slot": "tag-selector-list", direction: "column", id: listboxId,
|
|
99
|
+
// oxlint-disable-next-line jsx-a11y/prefer-tag-over-role, react-doctor/prefer-tag-over-role -- this is a custom listbox widget whose children are interactive TagSelectorItem rows, a drag-to-reorder DndContext, and a "Create" affordance with a colour palette; <datalist>/<select> can only hold plain <option>s, so they cannot represent this content.
|
|
100
|
+
role: "listbox", children: filteredTags.length === 0 && !showCreateOption ? (_jsx("div", { className: "text-muted-foreground text-paragraph-sm py-6 text-center", children: emptyMessage })) : (_jsxs(_Fragment, { children: [showCreateOption && _jsx(TagSelectorCreateOption, {}), _jsx(TagSelectorItems, { filteredTags: filteredTags, selectedIds: selectedIds })] })) }) }));
|
|
100
101
|
};
|
|
@@ -2403,7 +2403,7 @@ export const docsManifest = {
|
|
|
2403
2403
|
type: "boolean",
|
|
2404
2404
|
required: false,
|
|
2405
2405
|
defaultValue: "false",
|
|
2406
|
-
description: "Render a multi-line block (`<pre><code>`) instead of inline `<code>`. The\nblock scrolls horizontally rather than wrapping.",
|
|
2406
|
+
description: "Render a multi-line block (`<pre><code>`) instead of inline `<code>`. The\nblock scrolls horizontally, in a `ScrollFrame`, rather than wrapping.",
|
|
2407
2407
|
},
|
|
2408
2408
|
],
|
|
2409
2409
|
},
|
|
@@ -2418,7 +2418,7 @@ export const docsManifest = {
|
|
|
2418
2418
|
"`block` renders `<pre><code>` that scrolls horizontally; add `whitespace-pre-wrap` via `className` only when wrapping beats scrolling (long prose-like messages).",
|
|
2419
2419
|
],
|
|
2420
2420
|
keywords: [],
|
|
2421
|
-
uses: [],
|
|
2421
|
+
uses: ["scroll-frame"],
|
|
2422
2422
|
usedBy: [],
|
|
2423
2423
|
},
|
|
2424
2424
|
{
|
|
@@ -2634,7 +2634,7 @@ export const docsManifest = {
|
|
|
2634
2634
|
"Provide `items` as `{ label, value }[]` - the visible text comes from `label` while selection is tracked by `value`.",
|
|
2635
2635
|
],
|
|
2636
2636
|
keywords: ["autocomplete", "typeahead", "searchable select"],
|
|
2637
|
-
uses: ["button", "flex", "icon"],
|
|
2637
|
+
uses: ["button", "flex", "icon", "scroll-frame"],
|
|
2638
2638
|
usedBy: ["country-combobox"],
|
|
2639
2639
|
},
|
|
2640
2640
|
{
|
|
@@ -2845,7 +2845,7 @@ export const docsManifest = {
|
|
|
2845
2845
|
"A plain click-triggered action menu is `DropdownMenu`; a remembered selection is `Combobox`.",
|
|
2846
2846
|
],
|
|
2847
2847
|
keywords: ["command palette", "spotlight", "cmd+k", "quick search"],
|
|
2848
|
-
uses: ["dialog", "flex", "icon"],
|
|
2848
|
+
uses: ["dialog", "flex", "icon", "scroll-frame"],
|
|
2849
2849
|
usedBy: [],
|
|
2850
2850
|
anatomy: {
|
|
2851
2851
|
children: [
|
|
@@ -6914,7 +6914,7 @@ export const docsManifest = {
|
|
|
6914
6914
|
'Pick `mode="city"` vs `"address"` to scope the geocoding; drive it controlled via the `Location` `value` + `onChange` (which also fires on clear).',
|
|
6915
6915
|
],
|
|
6916
6916
|
keywords: [],
|
|
6917
|
-
uses: ["button", "icon", "menu-item"],
|
|
6917
|
+
uses: ["button", "icon", "menu-item", "scroll-frame"],
|
|
6918
6918
|
usedBy: [],
|
|
6919
6919
|
},
|
|
6920
6920
|
{
|
|
@@ -9480,6 +9480,12 @@ export const docsManifest = {
|
|
|
9480
9480
|
type: "string | undefined",
|
|
9481
9481
|
required: false,
|
|
9482
9482
|
},
|
|
9483
|
+
{
|
|
9484
|
+
name: "contentClassName",
|
|
9485
|
+
type: "string | undefined",
|
|
9486
|
+
required: false,
|
|
9487
|
+
description: "Classes for the element holding the children, inside the scrolling one.\nA vertical frame's content is already as wide as the frame, so text wraps\nand `w-full` children fill it; a horizontal or two-way frame's content is\nas wide as its widest child.",
|
|
9488
|
+
},
|
|
9483
9489
|
{
|
|
9484
9490
|
name: "controls",
|
|
9485
9491
|
type: "boolean | undefined",
|
|
@@ -9531,10 +9537,10 @@ export const docsManifest = {
|
|
|
9531
9537
|
},
|
|
9532
9538
|
{
|
|
9533
9539
|
name: "orientation",
|
|
9534
|
-
type: "
|
|
9540
|
+
type: "ScrollFrameOrientation | undefined",
|
|
9535
9541
|
required: false,
|
|
9536
9542
|
defaultValue: '"vertical"',
|
|
9537
|
-
description: "Scroll axis: `vertical` scrolls up and down, `horizontal` left and right, as for a row of chips.",
|
|
9543
|
+
description: "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.",
|
|
9538
9544
|
},
|
|
9539
9545
|
{
|
|
9540
9546
|
name: "viewportClassName",
|
|
@@ -9542,6 +9548,12 @@ export const docsManifest = {
|
|
|
9542
9548
|
required: false,
|
|
9543
9549
|
description: "Classes for the scrolling element.",
|
|
9544
9550
|
},
|
|
9551
|
+
{
|
|
9552
|
+
name: "viewportRef",
|
|
9553
|
+
type: "Ref<HTMLDivElement | null> | undefined",
|
|
9554
|
+
required: false,
|
|
9555
|
+
description: "The scrolling element, for a virtualiser or anything else that reads or sets its scroll position.",
|
|
9556
|
+
},
|
|
9545
9557
|
],
|
|
9546
9558
|
},
|
|
9547
9559
|
{
|
|
@@ -9554,6 +9566,11 @@ export const docsManifest = {
|
|
|
9554
9566
|
kind: "type",
|
|
9555
9567
|
props: [],
|
|
9556
9568
|
},
|
|
9569
|
+
{
|
|
9570
|
+
name: "ScrollFrameOrientation",
|
|
9571
|
+
kind: "type",
|
|
9572
|
+
props: [],
|
|
9573
|
+
},
|
|
9557
9574
|
{
|
|
9558
9575
|
name: "ScrollFrameProps",
|
|
9559
9576
|
kind: "interface",
|
|
@@ -9565,10 +9582,25 @@ export const docsManifest = {
|
|
|
9565
9582
|
'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.',
|
|
9566
9583
|
'Match `fadeTheme` to the surface behind the frame (`card` inside a Card). Over an image or a blur, use `fadeMode="mask"` instead.',
|
|
9567
9584
|
"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.",
|
|
9585
|
+
'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.',
|
|
9586
|
+
"Hand a virtualiser the scrolling element through `viewportRef`.",
|
|
9568
9587
|
],
|
|
9569
9588
|
keywords: [],
|
|
9570
9589
|
uses: ["button", "scroll-area", "scroll-fade"],
|
|
9571
|
-
usedBy: [
|
|
9590
|
+
usedBy: [
|
|
9591
|
+
"code",
|
|
9592
|
+
"combobox",
|
|
9593
|
+
"command",
|
|
9594
|
+
"data-grid-column-filter",
|
|
9595
|
+
"data-grid-table",
|
|
9596
|
+
"location-input",
|
|
9597
|
+
"mega-menu",
|
|
9598
|
+
"page-toolbar",
|
|
9599
|
+
"sidebar",
|
|
9600
|
+
"sidepanel",
|
|
9601
|
+
"table",
|
|
9602
|
+
"tag-selector",
|
|
9603
|
+
],
|
|
9572
9604
|
},
|
|
9573
9605
|
{
|
|
9574
9606
|
slug: "search-input",
|
|
@@ -10759,7 +10791,7 @@ export const docsManifest = {
|
|
|
10759
10791
|
"Compose nested panels with `SidepanelNestedRoot`, and always render a `SidepanelTitle` for the accessible name.",
|
|
10760
10792
|
],
|
|
10761
10793
|
keywords: [],
|
|
10762
|
-
uses: ["button", "flex", "stack"],
|
|
10794
|
+
uses: ["button", "flex", "scroll-frame", "stack"],
|
|
10763
10795
|
usedBy: [],
|
|
10764
10796
|
anatomy: {
|
|
10765
10797
|
children: [
|
|
@@ -12334,7 +12366,7 @@ export const docsManifest = {
|
|
|
12334
12366
|
module: "components/table",
|
|
12335
12367
|
title: "Table",
|
|
12336
12368
|
category: "data",
|
|
12337
|
-
description: "Semantic `<table>` primitive, wrapped in a
|
|
12369
|
+
description: "Semantic `<table>` primitive, wrapped in a horizontal `ScrollFrame`\nso wide tables overflow rather than break layout. Compose with the exported\n`TableHeader`, `TableBody`, `TableRow`, `TableHead`, `TableCell`,\n`TableFooter`, and `TableCaption` parts.",
|
|
12338
12370
|
exports: [
|
|
12339
12371
|
{
|
|
12340
12372
|
name: "Table",
|
|
@@ -12383,7 +12415,7 @@ export const docsManifest = {
|
|
|
12383
12415
|
"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.",
|
|
12384
12416
|
],
|
|
12385
12417
|
keywords: [],
|
|
12386
|
-
uses: [],
|
|
12418
|
+
uses: ["scroll-frame"],
|
|
12387
12419
|
usedBy: [],
|
|
12388
12420
|
},
|
|
12389
12421
|
{
|
|
@@ -14811,7 +14843,15 @@ export const docsManifest = {
|
|
|
14811
14843
|
"The typed search survives each pick, so several matches can be ticked from one query; it resets when the popup closes.",
|
|
14812
14844
|
],
|
|
14813
14845
|
keywords: [],
|
|
14814
|
-
uses: [
|
|
14846
|
+
uses: [
|
|
14847
|
+
"badge",
|
|
14848
|
+
"button",
|
|
14849
|
+
"center",
|
|
14850
|
+
"flex",
|
|
14851
|
+
"icon",
|
|
14852
|
+
"scroll-frame",
|
|
14853
|
+
"separator",
|
|
14854
|
+
],
|
|
14815
14855
|
usedBy: [],
|
|
14816
14856
|
},
|
|
14817
14857
|
{
|
|
@@ -15275,7 +15315,7 @@ export const docsManifest = {
|
|
|
15275
15315
|
"Use `DataGridTable` for a data-driven grid; for a small static table with hand-authored `<TableRow>`s reach for `Table` instead - no virtualisation or TanStack instance needed.",
|
|
15276
15316
|
],
|
|
15277
15317
|
keywords: [],
|
|
15278
|
-
uses: ["center", "data-grid", "flex", "spinner"],
|
|
15318
|
+
uses: ["center", "data-grid", "flex", "scroll-frame", "spinner"],
|
|
15279
15319
|
usedBy: [],
|
|
15280
15320
|
},
|
|
15281
15321
|
{
|
|
@@ -16728,7 +16768,15 @@ export const docsManifest = {
|
|
|
16728
16768
|
"Keep it to navigation. Every entry is a `MegaMenuLink`, which closes the menu as it navigates; actions belong in a `DropdownMenu`.",
|
|
16729
16769
|
],
|
|
16730
16770
|
keywords: [],
|
|
16731
|
-
uses: [
|
|
16771
|
+
uses: [
|
|
16772
|
+
"anchor",
|
|
16773
|
+
"box",
|
|
16774
|
+
"grid",
|
|
16775
|
+
"heading",
|
|
16776
|
+
"popover",
|
|
16777
|
+
"scroll-frame",
|
|
16778
|
+
"stack",
|
|
16779
|
+
],
|
|
16732
16780
|
usedBy: [],
|
|
16733
16781
|
},
|
|
16734
16782
|
{
|
|
@@ -21963,7 +22011,7 @@ export const docsManifest = {
|
|
|
21963
22011
|
type: "boolean",
|
|
21964
22012
|
required: false,
|
|
21965
22013
|
defaultValue: "false",
|
|
21966
|
-
description: "
|
|
22014
|
+
description: "Fade the bottom edge while more of the list is below.",
|
|
21967
22015
|
},
|
|
21968
22016
|
{
|
|
21969
22017
|
name: "maxHeight",
|
|
@@ -22277,6 +22325,7 @@ export const docsManifest = {
|
|
|
22277
22325
|
"input",
|
|
22278
22326
|
"kbd",
|
|
22279
22327
|
"menu-item",
|
|
22328
|
+
"scroll-frame",
|
|
22280
22329
|
"separator",
|
|
22281
22330
|
"sheet",
|
|
22282
22331
|
"skeleton",
|
|
@@ -23275,6 +23324,7 @@ export const docsManifest = {
|
|
|
23275
23324
|
"popover",
|
|
23276
23325
|
"scroll-area",
|
|
23277
23326
|
"scroll-fade",
|
|
23327
|
+
"scroll-frame",
|
|
23278
23328
|
"tag",
|
|
23279
23329
|
],
|
|
23280
23330
|
usedBy: [],
|