@instruments/materia-ui 0.4.8 → 0.4.10
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 +15 -4
- 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/stat/stat-root.d.ts +32 -0
- package/dist/components/stat/stat-root.js +28 -0
- package/dist/components/stat.d.ts +2 -0
- package/dist/components/stat.js +1 -0
- package/dist/components/table/table-root.d.ts +1 -1
- package/dist/components/table/table-root.js +3 -2
- package/dist/compositions/attribute-card.d.ts +1 -1
- package/dist/compositions/attribute-card.js +1 -1
- 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.d.ts +1 -1
- package/dist/data/docs-manifest.js +137 -17
- package/package.json +5 -1
package/agent-docs.md
CHANGED
|
@@ -14,7 +14,7 @@ Do and don't guidance for coding agents, one section per component, taken from `
|
|
|
14
14
|
- Import icons through `Icon` and primitives through these components, never from `lucide-react` or `@base-ui/react` directly.
|
|
15
15
|
- Check your work with the `materia-ui-lint` class scanner (`--all` for every rule). The Oxlint preset is `@instruments/materia-ui/lint/oxlint`.
|
|
16
16
|
|
|
17
|
-
Documented components:
|
|
17
|
+
Documented components: 198
|
|
18
18
|
|
|
19
19
|
## AccordionRoot
|
|
20
20
|
|
|
@@ -88,7 +88,7 @@ Constrains its content to a fixed width-to-height ratio, sizing itself to the av
|
|
|
88
88
|
|
|
89
89
|
AttributeCard UI component. Import from "@instruments/materia-ui/compositions/attribute-card".
|
|
90
90
|
|
|
91
|
-
- Use `AttributeCard` for a single label + prominent value stat tile (a KPI/metric); for many key/value pairs about one entity reach for `DescriptionList` instead
|
|
91
|
+
- Use `AttributeCard` for a single label + prominent value stat tile (a KPI/metric); for many key/value pairs about one entity reach for `DescriptionList` instead, and for a bare figure without a card, `Stat`.
|
|
92
92
|
- Pass a Lucide component to `icon` (e.g. `RulerIcon`), not a rendered `<Icon/>` - it is handed to the internal `Icon` at a fixed `sm` size.
|
|
93
93
|
- This is a preset composition of `Card`; restyle via the surrounding layout rather than reaching inside - for a bespoke layout compose `Card`/`CardContent` directly.
|
|
94
94
|
|
|
@@ -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
|
|
|
@@ -1780,6 +1782,15 @@ Orchestrates staggered entrance animations for child `StaggerItem` components. U
|
|
|
1780
1782
|
- Tune rhythm with `stagger` (gap between items) and `delay` (lead-in) on the parent, rather than hand-setting per-child transition delays.
|
|
1781
1783
|
- Needs `ReducedMotionProvider` in the tree so the orchestrated entrance both runs and honours the reduced-motion setting.
|
|
1782
1784
|
|
|
1785
|
+
## Stat
|
|
1786
|
+
|
|
1787
|
+
`@instruments/materia-ui/components/stat`
|
|
1788
|
+
|
|
1789
|
+
A figure, what it measures and where it comes from, value first. The component owns the type of all three, so a page's figures match without setting a size or weight at the call site. Import from "@instruments/materia-ui/components/stat".
|
|
1790
|
+
|
|
1791
|
+
- Use `Stat` for a bare figure in running layout (a row of results, a cover, beside a chart); for a label-first tile in its own card reach for `AttributeCard`.
|
|
1792
|
+
- Keep `value` to the figure alone ("24", "1,284", "3–5") and put the sentence in `label`; `note` is for the source or date window, not a second label.
|
|
1793
|
+
|
|
1783
1794
|
## Stepper
|
|
1784
1795
|
|
|
1785
1796
|
`@instruments/materia-ui/components/stepper`
|
|
@@ -1887,7 +1898,7 @@ Renders a toggle switch for boolean on/off states.
|
|
|
1887
1898
|
|
|
1888
1899
|
`@instruments/materia-ui/components/table`
|
|
1889
1900
|
|
|
1890
|
-
Semantic `<table>` primitive, wrapped in a
|
|
1901
|
+
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
1902
|
|
|
1892
1903
|
- 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
1904
|
- 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, };
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import type * as React from "react";
|
|
2
|
+
/** The sizes a Stat comes in: `base` sits among other content, `lg` leads a page or a section. */
|
|
3
|
+
export declare const STAT_SIZES: readonly ["base", "lg"];
|
|
4
|
+
/** A size a Stat comes in; see {@link STAT_SIZES}. */
|
|
5
|
+
export type StatSize = (typeof STAT_SIZES)[number];
|
|
6
|
+
interface StatProps {
|
|
7
|
+
/** CSS class name */
|
|
8
|
+
className?: string;
|
|
9
|
+
/** What the value measures, set under it. */
|
|
10
|
+
label?: React.ReactNode;
|
|
11
|
+
/** Where the value comes from, set small and muted under the label. */
|
|
12
|
+
note?: React.ReactNode;
|
|
13
|
+
/**
|
|
14
|
+
* `base` for a figure among other content (a table cell, a side panel); `lg` for one that leads a page or section.
|
|
15
|
+
* @default "base"
|
|
16
|
+
*/
|
|
17
|
+
size?: StatSize;
|
|
18
|
+
/** The figure itself: a number, a percentage, a ratio. */
|
|
19
|
+
value: React.ReactNode;
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* A figure, what it measures and where it comes from, value first. The component owns the type of all three, so a
|
|
23
|
+
* page's figures match without setting a size or weight at the call site.
|
|
24
|
+
* Import from "@instruments/materia-ui/components/stat".
|
|
25
|
+
*
|
|
26
|
+
* @bestPractice Use `Stat` for a bare figure in running layout (a row of results, a cover, beside a chart); for a label-first tile in its own card reach for `AttributeCard`.
|
|
27
|
+
* @bestPractice Keep `value` to the figure alone ("24", "1,284", "3–5") and put the sentence in `label`; `note` is for the source or date window, not a second label.
|
|
28
|
+
* @category data
|
|
29
|
+
*/
|
|
30
|
+
declare const Stat: ({ value, label, note, size, className }: StatProps) => React.JSX.Element;
|
|
31
|
+
export type { StatProps };
|
|
32
|
+
export { Stat };
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { cn } from "@instruments/materia-ui/utils/cn";
|
|
3
|
+
import { Stack } from "../stack.js";
|
|
4
|
+
import { Text } from "../text.js";
|
|
5
|
+
/** The sizes a Stat comes in: `base` sits among other content, `lg` leads a page or a section. */
|
|
6
|
+
export const STAT_SIZES = ["base", "lg"];
|
|
7
|
+
/** The value's role at each size. `heading-lg` is past the sizes Text takes, so the value sets its own role. */
|
|
8
|
+
const VALUE_CLASSES = {
|
|
9
|
+
base: "text-subheading-lg",
|
|
10
|
+
lg: "text-heading-lg",
|
|
11
|
+
};
|
|
12
|
+
/** The label's role at each size. */
|
|
13
|
+
const LABEL_SIZES = {
|
|
14
|
+
base: "paragraph-sm",
|
|
15
|
+
lg: "paragraph",
|
|
16
|
+
};
|
|
17
|
+
const isSet = (node) => node !== undefined && node !== null && node !== "";
|
|
18
|
+
/**
|
|
19
|
+
* A figure, what it measures and where it comes from, value first. The component owns the type of all three, so a
|
|
20
|
+
* page's figures match without setting a size or weight at the call site.
|
|
21
|
+
* Import from "@instruments/materia-ui/components/stat".
|
|
22
|
+
*
|
|
23
|
+
* @bestPractice Use `Stat` for a bare figure in running layout (a row of results, a cover, beside a chart); for a label-first tile in its own card reach for `AttributeCard`.
|
|
24
|
+
* @bestPractice Keep `value` to the figure alone ("24", "1,284", "3–5") and put the sentence in `label`; `note` is for the source or date window, not a second label.
|
|
25
|
+
* @category data
|
|
26
|
+
*/
|
|
27
|
+
const Stat = ({ value, label, note, size = "base", className }) => (_jsxs(Stack, { className: className, "data-component": "stat", "data-slot": "stat", gap: "xs", children: [_jsx("p", { className: cn("trim-both font-medium text-pretty", VALUE_CLASSES[size]), "data-slot": "stat-value", children: value }), isSet(label) && (_jsx(Text, { "data-slot": "stat-label", size: LABEL_SIZES[size], children: label })), isSet(note) && (_jsx(Text, { "data-slot": "stat-note", size: "caption", variant: "muted", children: note }))] }));
|
|
28
|
+
export { Stat };
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { STAT_SIZES, Stat } from "./stat/stat-root.js";
|
|
@@ -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
|
/**
|
|
@@ -16,7 +16,7 @@ interface AttributeCardProps {
|
|
|
16
16
|
* AttributeCard UI component.
|
|
17
17
|
* Import from "@instruments/materia-ui/compositions/attribute-card".
|
|
18
18
|
*
|
|
19
|
-
* @bestPractice Use `AttributeCard` for a single label + prominent value stat tile (a KPI/metric); for many key/value pairs about one entity reach for `DescriptionList` instead
|
|
19
|
+
* @bestPractice Use `AttributeCard` for a single label + prominent value stat tile (a KPI/metric); for many key/value pairs about one entity reach for `DescriptionList` instead, and for a bare figure without a card, `Stat`.
|
|
20
20
|
* @bestPractice Pass a Lucide component to `icon` (e.g. `RulerIcon`), not a rendered `<Icon/>` - it is handed to the internal `Icon` at a fixed `sm` size.
|
|
21
21
|
* @bestPractice This is a preset composition of `Card`; restyle via the surrounding layout rather than reaching inside - for a bespoke layout compose `Card`/`CardContent` directly.
|
|
22
22
|
* @category catalogue
|
|
@@ -7,7 +7,7 @@ import { Icon } from "../components/icon.js";
|
|
|
7
7
|
* AttributeCard UI component.
|
|
8
8
|
* Import from "@instruments/materia-ui/compositions/attribute-card".
|
|
9
9
|
*
|
|
10
|
-
* @bestPractice Use `AttributeCard` for a single label + prominent value stat tile (a KPI/metric); for many key/value pairs about one entity reach for `DescriptionList` instead
|
|
10
|
+
* @bestPractice Use `AttributeCard` for a single label + prominent value stat tile (a KPI/metric); for many key/value pairs about one entity reach for `DescriptionList` instead, and for a bare figure without a card, `Stat`.
|
|
11
11
|
* @bestPractice Pass a Lucide component to `icon` (e.g. `RulerIcon`), not a rendered `<Icon/>` - it is handed to the internal `Icon` at a fixed `sm` size.
|
|
12
12
|
* @bestPractice This is a preset composition of `Card`; restyle via the surrounding layout rather than reaching inside - for a bespoke layout compose `Card`/`CardContent` directly.
|
|
13
13
|
* @category catalogue
|
|
@@ -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
|
};
|
|
@@ -74,5 +74,5 @@ export interface DocsManifest {
|
|
|
74
74
|
storybook: DocsStorybookInventory;
|
|
75
75
|
}
|
|
76
76
|
/** The slug of every documented component and composition. */
|
|
77
|
-
export type DocsUnitSlug = "accordion" | "alert" | "alert-dialog" | "anchor" | "animated-number" | "aspect-ratio" | "attribute-card" | "avatar" | "badge" | "border-beam" | "box" | "breadcrumb" | "button" | "button-group" | "calendar" | "card" | "card-grid" | "carousel" | "center" | "chart" | "checkbox" | "checkbox-card" | "checkbox-field" | "code" | "collapsible" | "color-palette" | "color-wheel" | "combobox" | "command" | "container" | "context-menu" | "cookie-banner" | "country-combobox" | "country-select" | "data-grid" | "data-grid-column-filter" | "data-grid-column-header" | "data-grid-column-visibility" | "data-grid-pagination" | "data-grid-table" | "delivery-banner" | "description-list" | "dialog" | "document-list" | "dot" | "dot-matrix-loader" | "drawer" | "dropdown-menu" | "easing-curve" | "editorial-card" | "empty" | "empty-state" | "error-boundary" | "error-state" | "expand-button" | "expandable-text" | "fade-overlay" | "field" | "file-upload" | "flex" | "folder-tile" | "gallery" | "grid" | "group" | "heading" | "hotspot-indicator" | "hover-card" | "icon" | "icon-box" | "icon-card" | "icon-morph" | "identified-surface" | "input" | "input-card" | "input-group" | "input-otp" | "item" | "kbd" | "label" | "loading-state" | "location-input" | "logo" | "logotype" | "marquee-text" | "media-frame" | "mega-menu" | "menu-item" | "menubar" | "mobile-navigation" | "month-year-picker" | "morph-dialog" | "motion-config" | "native-select" | "navigation-menu" | "page-header" | "page-toolbar" | "pagination" | "phone-input" | "pill" | "pixel-loader" | "popover" | "product-card" | "product-specifications-selector" | "progress" | "progress-dots" | "prompt-panel" | "quantity-control" | "quick-view" | "radio" | "radio-card" | "radio-group" | "requirement-item" | "resizable" | "scrim" | "scroll-area" | "scroll-fade" | "scroll-frame" | "search-input" | "section" | "section-header" | "segmented-control" | "select" | "separator" | "settings-card" | "sheet" | "shimmer" | "shine" | "sidebar" | "sidepanel" | "simple-grid" | "site-footer" | "skeleton" | "slider" | "social-icon" | "space" | "spinner" | "stack" | "stagger" | "stepper" | "streaming-text" | "sub-navigation" | "success-tick-ripple" | "swatch" | "swatch-fan" | "swatch-group" | "swatch-selector" | "switch" | "tab-navigation" | "table" | "tabs" | "tag" | "tag-selector" | "text" | "textarea" | "thinking-loader" | "thinking-orb" | "thinking-trace" | "time-slot-picker" | "toast" | "toggle" | "toggle-group" | "tooltip" | "variant-selector" | "verge" | "visually-hidden" | "voice";
|
|
77
|
+
export type DocsUnitSlug = "accordion" | "alert" | "alert-dialog" | "anchor" | "animated-number" | "aspect-ratio" | "attribute-card" | "avatar" | "badge" | "border-beam" | "box" | "breadcrumb" | "button" | "button-group" | "calendar" | "card" | "card-grid" | "carousel" | "center" | "chart" | "checkbox" | "checkbox-card" | "checkbox-field" | "code" | "collapsible" | "color-palette" | "color-wheel" | "combobox" | "command" | "container" | "context-menu" | "cookie-banner" | "country-combobox" | "country-select" | "data-grid" | "data-grid-column-filter" | "data-grid-column-header" | "data-grid-column-visibility" | "data-grid-pagination" | "data-grid-table" | "delivery-banner" | "description-list" | "dialog" | "document-list" | "dot" | "dot-matrix-loader" | "drawer" | "dropdown-menu" | "easing-curve" | "editorial-card" | "empty" | "empty-state" | "error-boundary" | "error-state" | "expand-button" | "expandable-text" | "fade-overlay" | "field" | "file-upload" | "flex" | "folder-tile" | "gallery" | "grid" | "group" | "heading" | "hotspot-indicator" | "hover-card" | "icon" | "icon-box" | "icon-card" | "icon-morph" | "identified-surface" | "input" | "input-card" | "input-group" | "input-otp" | "item" | "kbd" | "label" | "loading-state" | "location-input" | "logo" | "logotype" | "marquee-text" | "media-frame" | "mega-menu" | "menu-item" | "menubar" | "mobile-navigation" | "month-year-picker" | "morph-dialog" | "motion-config" | "native-select" | "navigation-menu" | "page-header" | "page-toolbar" | "pagination" | "phone-input" | "pill" | "pixel-loader" | "popover" | "product-card" | "product-specifications-selector" | "progress" | "progress-dots" | "prompt-panel" | "quantity-control" | "quick-view" | "radio" | "radio-card" | "radio-group" | "requirement-item" | "resizable" | "scrim" | "scroll-area" | "scroll-fade" | "scroll-frame" | "search-input" | "section" | "section-header" | "segmented-control" | "select" | "separator" | "settings-card" | "sheet" | "shimmer" | "shine" | "sidebar" | "sidepanel" | "simple-grid" | "site-footer" | "skeleton" | "slider" | "social-icon" | "space" | "spinner" | "stack" | "stagger" | "stat" | "stepper" | "streaming-text" | "sub-navigation" | "success-tick-ripple" | "swatch" | "swatch-fan" | "swatch-group" | "swatch-selector" | "switch" | "tab-navigation" | "table" | "tabs" | "tag" | "tag-selector" | "text" | "textarea" | "thinking-loader" | "thinking-orb" | "thinking-trace" | "time-slot-picker" | "toast" | "toggle" | "toggle-group" | "tooltip" | "variant-selector" | "verge" | "visually-hidden" | "voice";
|
|
78
78
|
export declare const docsManifest: DocsManifest;
|
|
@@ -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: [
|
|
@@ -11371,6 +11403,7 @@ export const docsManifest = {
|
|
|
11371
11403
|
"sidepanel",
|
|
11372
11404
|
"site-footer",
|
|
11373
11405
|
"skeleton",
|
|
11406
|
+
"stat",
|
|
11374
11407
|
],
|
|
11375
11408
|
},
|
|
11376
11409
|
{
|
|
@@ -11433,6 +11466,74 @@ export const docsManifest = {
|
|
|
11433
11466
|
uses: [],
|
|
11434
11467
|
usedBy: [],
|
|
11435
11468
|
},
|
|
11469
|
+
{
|
|
11470
|
+
slug: "stat",
|
|
11471
|
+
module: "components/stat",
|
|
11472
|
+
title: "Stat",
|
|
11473
|
+
category: "data",
|
|
11474
|
+
description: 'A figure, what it measures and where it comes from, value first. The component owns the type of all three, so a\npage\'s figures match without setting a size or weight at the call site.\nImport from "@instruments/materia-ui/components/stat".',
|
|
11475
|
+
exports: [
|
|
11476
|
+
{
|
|
11477
|
+
name: "Stat",
|
|
11478
|
+
kind: "component",
|
|
11479
|
+
props: [
|
|
11480
|
+
{
|
|
11481
|
+
name: "className",
|
|
11482
|
+
type: "string",
|
|
11483
|
+
required: false,
|
|
11484
|
+
description: "CSS class name",
|
|
11485
|
+
},
|
|
11486
|
+
{
|
|
11487
|
+
name: "label",
|
|
11488
|
+
type: "React.ReactNode",
|
|
11489
|
+
required: false,
|
|
11490
|
+
description: "What the value measures, set under it.",
|
|
11491
|
+
},
|
|
11492
|
+
{
|
|
11493
|
+
name: "note",
|
|
11494
|
+
type: "React.ReactNode",
|
|
11495
|
+
required: false,
|
|
11496
|
+
description: "Where the value comes from, set small and muted under the label.",
|
|
11497
|
+
},
|
|
11498
|
+
{
|
|
11499
|
+
name: "size",
|
|
11500
|
+
type: '"base" | "lg"',
|
|
11501
|
+
required: false,
|
|
11502
|
+
defaultValue: '"base"',
|
|
11503
|
+
description: "`base` for a figure among other content (a table cell, a side panel); `lg` for one that leads a page or section.",
|
|
11504
|
+
},
|
|
11505
|
+
{
|
|
11506
|
+
name: "value",
|
|
11507
|
+
type: "React.ReactNode",
|
|
11508
|
+
required: true,
|
|
11509
|
+
description: "The figure itself: a number, a percentage, a ratio.",
|
|
11510
|
+
},
|
|
11511
|
+
],
|
|
11512
|
+
},
|
|
11513
|
+
{
|
|
11514
|
+
name: "STAT_SIZES",
|
|
11515
|
+
kind: "const",
|
|
11516
|
+
props: [],
|
|
11517
|
+
},
|
|
11518
|
+
{
|
|
11519
|
+
name: "StatProps",
|
|
11520
|
+
kind: "interface",
|
|
11521
|
+
props: [],
|
|
11522
|
+
},
|
|
11523
|
+
{
|
|
11524
|
+
name: "StatSize",
|
|
11525
|
+
kind: "type",
|
|
11526
|
+
props: [],
|
|
11527
|
+
},
|
|
11528
|
+
],
|
|
11529
|
+
bestPractices: [
|
|
11530
|
+
"Use `Stat` for a bare figure in running layout (a row of results, a cover, beside a chart); for a label-first tile in its own card reach for `AttributeCard`.",
|
|
11531
|
+
'Keep `value` to the figure alone ("24", "1,284", "3–5") and put the sentence in `label`; `note` is for the source or date window, not a second label.',
|
|
11532
|
+
],
|
|
11533
|
+
keywords: [],
|
|
11534
|
+
uses: ["stack", "text"],
|
|
11535
|
+
usedBy: [],
|
|
11536
|
+
},
|
|
11436
11537
|
{
|
|
11437
11538
|
slug: "stepper",
|
|
11438
11539
|
module: "components/stepper",
|
|
@@ -12334,7 +12435,7 @@ export const docsManifest = {
|
|
|
12334
12435
|
module: "components/table",
|
|
12335
12436
|
title: "Table",
|
|
12336
12437
|
category: "data",
|
|
12337
|
-
description: "Semantic `<table>` primitive, wrapped in a
|
|
12438
|
+
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
12439
|
exports: [
|
|
12339
12440
|
{
|
|
12340
12441
|
name: "Table",
|
|
@@ -12383,7 +12484,7 @@ export const docsManifest = {
|
|
|
12383
12484
|
"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
12485
|
],
|
|
12385
12486
|
keywords: [],
|
|
12386
|
-
uses: [],
|
|
12487
|
+
uses: ["scroll-frame"],
|
|
12387
12488
|
usedBy: [],
|
|
12388
12489
|
},
|
|
12389
12490
|
{
|
|
@@ -12807,6 +12908,7 @@ export const docsManifest = {
|
|
|
12807
12908
|
"section-header",
|
|
12808
12909
|
"site-footer",
|
|
12809
12910
|
"slider",
|
|
12911
|
+
"stat",
|
|
12810
12912
|
"streaming-text",
|
|
12811
12913
|
"thinking-trace",
|
|
12812
12914
|
],
|
|
@@ -13844,7 +13946,7 @@ export const docsManifest = {
|
|
|
13844
13946
|
},
|
|
13845
13947
|
],
|
|
13846
13948
|
bestPractices: [
|
|
13847
|
-
"Use `AttributeCard` for a single label + prominent value stat tile (a KPI/metric); for many key/value pairs about one entity reach for `DescriptionList` instead
|
|
13949
|
+
"Use `AttributeCard` for a single label + prominent value stat tile (a KPI/metric); for many key/value pairs about one entity reach for `DescriptionList` instead, and for a bare figure without a card, `Stat`.",
|
|
13848
13950
|
"Pass a Lucide component to `icon` (e.g. `RulerIcon`), not a rendered `<Icon/>` - it is handed to the internal `Icon` at a fixed `sm` size.",
|
|
13849
13951
|
"This is a preset composition of `Card`; restyle via the surrounding layout rather than reaching inside - for a bespoke layout compose `Card`/`CardContent` directly.",
|
|
13850
13952
|
],
|
|
@@ -14811,7 +14913,15 @@ export const docsManifest = {
|
|
|
14811
14913
|
"The typed search survives each pick, so several matches can be ticked from one query; it resets when the popup closes.",
|
|
14812
14914
|
],
|
|
14813
14915
|
keywords: [],
|
|
14814
|
-
uses: [
|
|
14916
|
+
uses: [
|
|
14917
|
+
"badge",
|
|
14918
|
+
"button",
|
|
14919
|
+
"center",
|
|
14920
|
+
"flex",
|
|
14921
|
+
"icon",
|
|
14922
|
+
"scroll-frame",
|
|
14923
|
+
"separator",
|
|
14924
|
+
],
|
|
14815
14925
|
usedBy: [],
|
|
14816
14926
|
},
|
|
14817
14927
|
{
|
|
@@ -15275,7 +15385,7 @@ export const docsManifest = {
|
|
|
15275
15385
|
"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
15386
|
],
|
|
15277
15387
|
keywords: [],
|
|
15278
|
-
uses: ["center", "data-grid", "flex", "spinner"],
|
|
15388
|
+
uses: ["center", "data-grid", "flex", "scroll-frame", "spinner"],
|
|
15279
15389
|
usedBy: [],
|
|
15280
15390
|
},
|
|
15281
15391
|
{
|
|
@@ -16728,7 +16838,15 @@ export const docsManifest = {
|
|
|
16728
16838
|
"Keep it to navigation. Every entry is a `MegaMenuLink`, which closes the menu as it navigates; actions belong in a `DropdownMenu`.",
|
|
16729
16839
|
],
|
|
16730
16840
|
keywords: [],
|
|
16731
|
-
uses: [
|
|
16841
|
+
uses: [
|
|
16842
|
+
"anchor",
|
|
16843
|
+
"box",
|
|
16844
|
+
"grid",
|
|
16845
|
+
"heading",
|
|
16846
|
+
"popover",
|
|
16847
|
+
"scroll-frame",
|
|
16848
|
+
"stack",
|
|
16849
|
+
],
|
|
16732
16850
|
usedBy: [],
|
|
16733
16851
|
},
|
|
16734
16852
|
{
|
|
@@ -21963,7 +22081,7 @@ export const docsManifest = {
|
|
|
21963
22081
|
type: "boolean",
|
|
21964
22082
|
required: false,
|
|
21965
22083
|
defaultValue: "false",
|
|
21966
|
-
description: "
|
|
22084
|
+
description: "Fade the bottom edge while more of the list is below.",
|
|
21967
22085
|
},
|
|
21968
22086
|
{
|
|
21969
22087
|
name: "maxHeight",
|
|
@@ -22277,6 +22395,7 @@ export const docsManifest = {
|
|
|
22277
22395
|
"input",
|
|
22278
22396
|
"kbd",
|
|
22279
22397
|
"menu-item",
|
|
22398
|
+
"scroll-frame",
|
|
22280
22399
|
"separator",
|
|
22281
22400
|
"sheet",
|
|
22282
22401
|
"skeleton",
|
|
@@ -23275,6 +23394,7 @@ export const docsManifest = {
|
|
|
23275
23394
|
"popover",
|
|
23276
23395
|
"scroll-area",
|
|
23277
23396
|
"scroll-fade",
|
|
23397
|
+
"scroll-frame",
|
|
23278
23398
|
"tag",
|
|
23279
23399
|
],
|
|
23280
23400
|
usedBy: [],
|
|
@@ -23784,7 +23904,7 @@ export const docsManifest = {
|
|
|
23784
23904
|
storybook: {
|
|
23785
23905
|
addons: ["@storybook/addon-docs", "@storybook/react-vite"],
|
|
23786
23906
|
anatomyFiles: 31,
|
|
23787
|
-
storyFiles:
|
|
23907
|
+
storyFiles: 173,
|
|
23788
23908
|
storiesWithPlay: 63,
|
|
23789
23909
|
},
|
|
23790
23910
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@instruments/materia-ui",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.10",
|
|
4
4
|
"description": "Materia design system: React components, tokens and Tailwind v4 styles.",
|
|
5
5
|
"license": "SEE LICENSE IN LICENSE",
|
|
6
6
|
"repository": {
|
|
@@ -462,6 +462,10 @@
|
|
|
462
462
|
"types": "./dist/components/stagger.d.ts",
|
|
463
463
|
"default": "./dist/components/stagger.js"
|
|
464
464
|
},
|
|
465
|
+
"./components/stat": {
|
|
466
|
+
"types": "./dist/components/stat.d.ts",
|
|
467
|
+
"default": "./dist/components/stat.js"
|
|
468
|
+
},
|
|
465
469
|
"./components/stepper": {
|
|
466
470
|
"types": "./dist/components/stepper.d.ts",
|
|
467
471
|
"default": "./dist/components/stepper.js"
|