@abuzareal/contour-ui 0.5.0 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +12 -0
- package/COMBOBOX.md +87 -0
- package/CONTRACTS.md +17 -17
- package/DOCS.md +80 -0
- package/FOUNDATION.md +35 -0
- package/GETTING-STARTED.md +63 -0
- package/NEXTJS.md +3 -1
- package/PUBLIC-API.md +174 -164
- package/README.md +7 -5
- package/RECIPES.md +43 -0
- package/RELEASING.md +2 -2
- package/SIZE.md +9 -3
- package/dist/components/ui/Combobox.d.ts +325 -0
- package/dist/components/ui/Combobox.js +120 -0
- package/dist/components/ui/Combobox.types.d.ts +46 -0
- package/dist/components/ui/Combobox.types.js +1 -0
- package/dist/components/ui/DataTable.js +5 -4
- package/dist/components/ui/Field.js +1 -1
- package/dist/components/ui/FloatingPortal.js +2 -40
- package/dist/components/ui/FloatingScope.d.ts +9 -0
- package/dist/components/ui/FloatingScope.js +43 -0
- package/dist/components/ui/Table.d.ts +20 -0
- package/dist/components/ui/Table.js +9 -0
- package/dist/hooks/useComboboxAnchor.d.ts +17 -0
- package/dist/hooks/useComboboxAnchor.js +61 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +2 -0
- package/dist/kit/data-display.css +6 -0
- package/dist/kit/forms.css +34 -0
- package/dist/lib/componentClasses.js +1 -1
- package/dist/scoped/kit/data-display.css +6 -0
- package/dist/scoped/kit/forms.css +34 -0
- package/examples/optional/LazyScene.tsx +78 -0
- package/examples/optional/OptionalSmoothScroll.tsx +25 -0
- package/examples/optional/SmoothScrollOwner.tsx +7 -0
- package/examples/optional/optional.css +25 -0
- package/examples/recipes/AsyncProjectPicker.tsx +82 -0
- package/examples/recipes/DeleteProjectConfirmation.tsx +97 -0
- package/examples/recipes/InputCompositions.tsx +49 -0
- package/examples/recipes/RemoteRecordsTable.tsx +256 -0
- package/examples/recipes/useRemoteResource.ts +81 -0
- package/examples/reproduction/README.md +9 -0
- package/examples/reproduction/index.html +14 -0
- package/examples/reproduction/package.json +27 -0
- package/examples/reproduction/src/main.tsx +33 -0
- package/examples/reproduction/src/style.css +19 -0
- package/examples/reproduction/tsconfig.json +18 -0
- package/examples/reproduction/vite.config.ts +19 -0
- package/examples/starters/next/README.md +18 -0
- package/examples/starters/next/app/Demo.tsx +65 -0
- package/examples/starters/next/app/layout.tsx +25 -0
- package/examples/starters/next/app/next-page/page.tsx +9 -0
- package/examples/starters/next/app/page.tsx +11 -0
- package/examples/starters/next/app/style.css +19 -0
- package/examples/starters/next/next-env.d.ts +2 -0
- package/examples/starters/next/next.config.mjs +1 -0
- package/examples/starters/next/package.json +27 -0
- package/examples/starters/next/proxy.ts +16 -0
- package/examples/starters/next/tsconfig.json +30 -0
- package/examples/starters/vite/README.md +16 -0
- package/examples/starters/vite/index.html +14 -0
- package/examples/starters/vite/package.json +27 -0
- package/examples/starters/vite/src/main.tsx +81 -0
- package/examples/starters/vite/src/style.css +19 -0
- package/examples/starters/vite/tsconfig.json +18 -0
- package/examples/starters/vite/vite.config.ts +19 -0
- package/package.json +19 -4
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
+
import { forwardRef, useContext, useId, useLayoutEffect, useMemo, useRef, useState, } from "react";
|
|
4
|
+
import { Combobox as BaseCombobox } from "@base-ui/react/combobox";
|
|
5
|
+
import { DirectionProvider } from "@base-ui/react/direction-provider";
|
|
6
|
+
import { Check, ChevronDown } from "lucide-react";
|
|
7
|
+
import { ccx } from "../../lib/classNames.js";
|
|
8
|
+
import { fieldDescribedBy } from "../../lib/a11y.js";
|
|
9
|
+
import { composeRefs } from "../../lib/composeRefs.js";
|
|
10
|
+
import { LayerVisibility } from "../../lib/layers.js";
|
|
11
|
+
import { floatingContainer } from "../../hooks/useFloatingPanel.js";
|
|
12
|
+
import useControlRef from "../../hooks/useControlRef.js";
|
|
13
|
+
import useFormReset from "../../hooks/useFormReset.js";
|
|
14
|
+
import useLayerDismiss from "../../hooks/useLayerDismiss.js";
|
|
15
|
+
import useOpenState from "../../hooks/useOpenState.js";
|
|
16
|
+
import useComboboxAnchor from "../../hooks/useComboboxAnchor.js";
|
|
17
|
+
import FloatingScope from "./FloatingScope.js";
|
|
18
|
+
import Field from "./Field.js";
|
|
19
|
+
/** Searchable allowed-value selection. The forwarded ref targets the visible input. */
|
|
20
|
+
const Combobox = forwardRef(function Combobox({ label, options, filteredOptions, value: controlledValue, defaultValue = null, onValueChange, inputValue, defaultInputValue, onInputValueChange, open: controlledOpen, defaultOpen = false, onOpenChange, id: providedId, name, form, required, disabled, readOnly, hint, error, hideLabel, loading = false, searchError, onRetry, loadingText = "Loading choices…", emptyText = "No matching choices.", retryLabel = "Retry search", triggerLabel = "Show choices", missingValueText = "Selected option is unavailable. Choose another.", side = "bottom", align = "start", sideOffset = 8, collisionPadding = 8, portal = true, container, inputProps, popupProps, rootProps, }, forwardedRef) {
|
|
21
|
+
const generatedId = useId();
|
|
22
|
+
const id = providedId ?? generatedId;
|
|
23
|
+
const [internalValue, setInternalValue] = useState(defaultValue);
|
|
24
|
+
const value = controlledValue === undefined ? internalValue : controlledValue;
|
|
25
|
+
const selected = options.find((option) => option.value === value);
|
|
26
|
+
const missing = value !== null && !selected;
|
|
27
|
+
const [query, setQuery] = useState(defaultInputValue ?? null);
|
|
28
|
+
const [open, changeOpen] = useOpenState(controlledOpen, defaultOpen, onOpenChange);
|
|
29
|
+
const available = useContext(LayerVisibility) && !disabled && !readOnly;
|
|
30
|
+
const visible = available && open;
|
|
31
|
+
const { ref: input, attach } = useControlRef(forwardedRef);
|
|
32
|
+
const [anchor, setAnchor] = useState(null);
|
|
33
|
+
const attachInput = useMemo(() => composeRefs(attach, inputProps?.ref, setAnchor), [attach, inputProps?.ref]);
|
|
34
|
+
const root = useRef(null);
|
|
35
|
+
const attachRoot = useMemo(() => composeRefs(root, rootProps?.ref), [rootProps?.ref]);
|
|
36
|
+
const positioner = useRef(null);
|
|
37
|
+
const [, mountedPositioner] = useState(null);
|
|
38
|
+
const attachPositioner = useMemo(() => composeRefs(positioner, mountedPositioner), []);
|
|
39
|
+
const [direction, setDirection] = useState("ltr");
|
|
40
|
+
const target = anchor && portal ? floatingContainer(anchor, { container }) : null;
|
|
41
|
+
const collection = useMemo(() => BaseCombobox.createItems(options, {
|
|
42
|
+
getValue: (option) => option.value,
|
|
43
|
+
getLabel: (option) => option.label,
|
|
44
|
+
}), [options]);
|
|
45
|
+
const close = () => {
|
|
46
|
+
changeOpen(false);
|
|
47
|
+
setQuery(null);
|
|
48
|
+
};
|
|
49
|
+
useLayerDismiss(root, visible, close, {
|
|
50
|
+
branches: [positioner, input],
|
|
51
|
+
layerElement: positioner,
|
|
52
|
+
});
|
|
53
|
+
useFormReset(input, () => {
|
|
54
|
+
if (controlledValue === undefined)
|
|
55
|
+
setInternalValue(defaultValue);
|
|
56
|
+
setQuery(null);
|
|
57
|
+
changeOpen(false);
|
|
58
|
+
});
|
|
59
|
+
useLayoutEffect(() => {
|
|
60
|
+
const element = input.current;
|
|
61
|
+
element?.setCustomValidity(missing ? missingValueText : "");
|
|
62
|
+
return () => element?.setCustomValidity("");
|
|
63
|
+
}, [input, missing, missingValueText]);
|
|
64
|
+
const fieldError = error ?? (missing ? missingValueText : undefined);
|
|
65
|
+
const results = loading || searchError ? [] : filteredOptions;
|
|
66
|
+
const statusId = `${id}-search-status`;
|
|
67
|
+
const positioning = useComboboxAnchor(anchor, positioner);
|
|
68
|
+
const popup = (_jsx(FloatingScope, { anchor: anchor, open: visible, target: target, onDirectionChange: setDirection, children: _jsx(BaseCombobox.Positioner, { ...positioning, ref: attachPositioner, className: ccx("combobox-positioner"), positionMethod: "fixed", side: side, align: align, sideOffset: Number.isFinite(sideOffset) ? sideOffset : 8, collisionPadding: Number.isFinite(collisionPadding) ? Math.max(0, collisionPadding) : 8, children: _jsxs(BaseCombobox.Popup, { ...popupProps, className: ccx("combobox-popup", popupProps?.className), children: [_jsx(BaseCombobox.Empty, { className: ccx("combobox-status"), children: loading ? loadingText : (searchError ?? emptyText) }), searchError && onRetry && (_jsx("button", { type: "button", className: ccx("combobox-retry"), onClick: onRetry, children: retryLabel })), _jsx(BaseCombobox.List, { className: ccx("combobox-list"), children: (option) => (_jsxs(BaseCombobox.Item, { className: ccx("combobox-option"), value: option.value, disabled: option.disabled, children: [_jsx("span", { children: option.label }), _jsx(BaseCombobox.ItemIndicator, { children: _jsx(Check, { size: 14, "aria-hidden": "true" }) })] }, option.value)) })] }) }) }));
|
|
69
|
+
return (_jsx(DirectionProvider, { direction: direction, children: _jsx("div", { ...rootProps, ref: attachRoot, className: ccx("combobox", rootProps?.className), children: _jsxs(BaseCombobox.Root, { items: collection, filteredItems: results, value: missing ? null : value, onValueChange: (next) => {
|
|
70
|
+
if (controlledValue === undefined)
|
|
71
|
+
setInternalValue(next);
|
|
72
|
+
setQuery(null);
|
|
73
|
+
onValueChange?.(next);
|
|
74
|
+
}, inputValue: inputValue ?? query ?? selected?.label ?? "", onInputValueChange: (next, details) => {
|
|
75
|
+
setQuery(details.reason === "input-change" ||
|
|
76
|
+
details.reason === "input-clear"
|
|
77
|
+
? next
|
|
78
|
+
: null);
|
|
79
|
+
onInputValueChange?.(next);
|
|
80
|
+
}, open: visible, onOpenChange: (next, details) => {
|
|
81
|
+
// Keep native observers informed; a prevented Escape must not dismiss the host layer.
|
|
82
|
+
details.allowPropagation();
|
|
83
|
+
if ((next && !available) ||
|
|
84
|
+
(details.reason === "escape-key" &&
|
|
85
|
+
details.event.defaultPrevented)) {
|
|
86
|
+
details.cancel();
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
if (!next)
|
|
90
|
+
setQuery(null);
|
|
91
|
+
changeOpen(next);
|
|
92
|
+
}, name: name, form: form, required: required, disabled: disabled, readOnly: readOnly, children: [_jsx(Field, { id: id, label: label, hint: hint, error: fieldError, required: required, disabled: disabled, hideLabel: hideLabel, children: _jsxs(BaseCombobox.InputGroup, { className: ccx("field-control combobox-control"), children: [_jsx(BaseCombobox.Input, { ...inputProps, onMouseDown: (event) => {
|
|
93
|
+
inputProps?.onMouseDown?.(event);
|
|
94
|
+
// Own activation on click; leave native focus/caret behavior intact and avoid stale touch timers.
|
|
95
|
+
event.preventBaseUIHandler();
|
|
96
|
+
}, onClick: (event) => {
|
|
97
|
+
inputProps?.onClick?.(event);
|
|
98
|
+
if (!event.defaultPrevented && available)
|
|
99
|
+
changeOpen(true);
|
|
100
|
+
}, onKeyDown: (event) => {
|
|
101
|
+
inputProps?.onKeyDown?.(event);
|
|
102
|
+
// Contour preserves a closed selection and lets the enclosing layer own Escape.
|
|
103
|
+
// Cancelling only Base UI's handler leaves the native document event intact.
|
|
104
|
+
if (event.defaultPrevented ||
|
|
105
|
+
(event.key === "Escape" && !visible))
|
|
106
|
+
event.preventBaseUIHandler();
|
|
107
|
+
}, ref: attachInput, id: id, form: form, "aria-labelledby": [
|
|
108
|
+
`${id}-label`,
|
|
109
|
+
inputProps?.["aria-labelledby"],
|
|
110
|
+
]
|
|
111
|
+
.filter(Boolean)
|
|
112
|
+
.join(" "), "aria-invalid": fieldError ? true : inputProps?.["aria-invalid"], "aria-busy": loading || undefined, "aria-describedby": [
|
|
113
|
+
inputProps?.["aria-describedby"],
|
|
114
|
+
fieldDescribedBy(id, hint, fieldError),
|
|
115
|
+
statusId,
|
|
116
|
+
]
|
|
117
|
+
.filter(Boolean)
|
|
118
|
+
.join(" ") }), _jsx(BaseCombobox.Trigger, { disabled: disabled || readOnly, className: ccx("combobox-trigger"), "aria-label": triggerLabel, children: _jsx(ChevronDown, { size: 16, "aria-hidden": "true" }) })] }) }), _jsx("span", { id: statusId, role: "status", className: ccx("visually-hidden"), children: loading ? loadingText : (searchError ?? "") }), _jsx(BaseCombobox.Portal, { container: portal ? (target ?? undefined) : root, children: popup })] }) }) }));
|
|
119
|
+
});
|
|
120
|
+
export default Combobox;
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import type { NativePartProps } from "../../lib/componentProps.js";
|
|
2
|
+
import type { FloatingOptions } from "../../hooks/useFloatingPanel.js";
|
|
3
|
+
export type ComboboxOption = {
|
|
4
|
+
/** Unique, nonempty form value. Labels are presentation, never identity. */
|
|
5
|
+
value: string;
|
|
6
|
+
label: string;
|
|
7
|
+
disabled?: boolean;
|
|
8
|
+
};
|
|
9
|
+
type InputPart = Omit<NativePartProps<"input">, "value" | "defaultValue" | "name" | "form" | "id" | "disabled" | "required" | "readOnly" | "type">;
|
|
10
|
+
export type ComboboxProps = FloatingOptions & {
|
|
11
|
+
label: string;
|
|
12
|
+
options: readonly ComboboxOption[];
|
|
13
|
+
/** Application-filtered results. Retain the selected option in options for async searches. */
|
|
14
|
+
filteredOptions?: readonly ComboboxOption[];
|
|
15
|
+
value?: string | null;
|
|
16
|
+
defaultValue?: string | null;
|
|
17
|
+
onValueChange?: (value: string | null) => void;
|
|
18
|
+
inputValue?: string;
|
|
19
|
+
defaultInputValue?: string;
|
|
20
|
+
onInputValueChange?: (query: string) => void;
|
|
21
|
+
open?: boolean;
|
|
22
|
+
defaultOpen?: boolean;
|
|
23
|
+
onOpenChange?: (open: boolean) => void;
|
|
24
|
+
id?: string;
|
|
25
|
+
name?: string;
|
|
26
|
+
form?: string;
|
|
27
|
+
required?: boolean;
|
|
28
|
+
disabled?: boolean;
|
|
29
|
+
readOnly?: boolean;
|
|
30
|
+
hint?: string;
|
|
31
|
+
error?: string;
|
|
32
|
+
hideLabel?: boolean;
|
|
33
|
+
loading?: boolean;
|
|
34
|
+
/** Search/service failure, distinct from field validation. Results are hidden until retry succeeds. */
|
|
35
|
+
searchError?: string;
|
|
36
|
+
onRetry?: () => void;
|
|
37
|
+
loadingText?: string;
|
|
38
|
+
emptyText?: string;
|
|
39
|
+
retryLabel?: string;
|
|
40
|
+
triggerLabel?: string;
|
|
41
|
+
missingValueText?: string;
|
|
42
|
+
inputProps?: InputPart;
|
|
43
|
+
popupProps?: NativePartProps<"div">;
|
|
44
|
+
rootProps?: NativePartProps<"div">;
|
|
45
|
+
};
|
|
46
|
+
export {};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -3,6 +3,7 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
|
3
3
|
import { ccx } from "../../lib/classNames.js";
|
|
4
4
|
/** Editorial data table with mono headers, optional column sorting, and horizontal scroll on small screens. */
|
|
5
5
|
import { useMemo, useState } from "react";
|
|
6
|
+
import Table from "./Table.js";
|
|
6
7
|
import { ArrowDown, ArrowUp } from "lucide-react";
|
|
7
8
|
export default function DataTable({ caption, columns, rows, rowKey, }) {
|
|
8
9
|
const [sort, setSort] = useState(null);
|
|
@@ -22,8 +23,8 @@ export default function DataTable({ caption, columns, rows, rowKey, }) {
|
|
|
22
23
|
const toggleSort = (key) => setSort((current) => current?.key === key && current.direction === "ascending"
|
|
23
24
|
? { key, direction: "descending" }
|
|
24
25
|
: { key, direction: "ascending" });
|
|
25
|
-
return (
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
26
|
+
return (_jsxs(Table, { caption: caption, children: [_jsx("thead", { className: ccx("mono"), children: _jsx("tr", { children: columns.map((column) => {
|
|
27
|
+
const direction = sort?.key === column.key ? sort.direction : undefined;
|
|
28
|
+
return (_jsx("th", { scope: "col", "aria-sort": column.sortable ? (direction ?? "none") : undefined, "data-align": column.align, children: column.sortable ? (_jsxs("button", { type: "button", onClick: () => toggleSort(column.key), children: [column.header, direction === "descending" ? (_jsx(ArrowDown, { size: 12, "aria-hidden": "true" })) : (_jsx(ArrowUp, { size: 12, "aria-hidden": "true", className: ccx(direction ? undefined : "data-table-idle") }))] })) : (column.header) }, column.key));
|
|
29
|
+
}) }) }), _jsx("tbody", { children: sortedRows.map((row) => (_jsx("tr", { children: columns.map((column) => (_jsx("td", { "data-align": column.align, children: column.render ? column.render(row) : row[column.key] }, column.key))) }, String(row[rowKey])))) })] }));
|
|
29
30
|
}
|
|
@@ -2,5 +2,5 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
|
2
2
|
import { AlertCircle } from "lucide-react";
|
|
3
3
|
import { ccx } from "../../lib/classNames.js";
|
|
4
4
|
export default function Field({ id, label, children, hint, error, required, hideLabel, counter, disabled, }) {
|
|
5
|
-
return (_jsxs("div", { className: ccx("field", error && "field-invalid", disabled && "field-disabled"), children: [_jsxs("label", { htmlFor: id, className: ccx("field-label mono", hideLabel && "visually-hidden"), children: [label, required && (_jsx("span", { className: ccx("field-required"), "aria-hidden": "true", children: "*" }))] }), children, (hint || error || counter) && (_jsxs("div", { className: ccx("field-footer mono"), children: [error ? (_jsxs("span", { id: `${id}-error`, className: ccx("field-error"), children: [_jsx(AlertCircle, { size: 13, "aria-hidden": "true" }), error] })) : (hint && _jsx("span", { id: `${id}-hint`, children: hint })), counter && _jsx("span", { className: ccx("field-counter"), children: counter })] }))] }));
|
|
5
|
+
return (_jsxs("div", { className: ccx("field", error && "field-invalid", disabled && "field-disabled"), children: [_jsxs("label", { htmlFor: id, className: ccx("field-label mono", hideLabel && "visually-hidden"), children: [_jsx("span", { id: `${id}-label`, children: label }), required && (_jsx("span", { className: ccx("field-required"), "aria-hidden": "true", children: "*" }))] }), children, (hint || error || counter) && (_jsxs("div", { className: ccx("field-footer mono"), children: [error ? (_jsxs("span", { id: `${id}-error`, className: ccx("field-error"), children: [_jsx(AlertCircle, { size: 13, "aria-hidden": "true" }), error] })) : (hint && _jsx("span", { id: `${id}-hint`, children: hint })), counter && _jsx("span", { className: ccx("field-counter"), children: counter })] }))] }));
|
|
6
6
|
}
|
|
@@ -1,51 +1,13 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
import { jsx as _jsx } from "react/jsx-runtime";
|
|
3
|
-
import { useLayoutEffect, useRef } from "react";
|
|
4
3
|
import { createPortal } from "react-dom";
|
|
5
4
|
import useFloatingPanel, { floatingContainer, } from "../../hooks/useFloatingPanel.js";
|
|
5
|
+
import FloatingScope from "./FloatingScope.js";
|
|
6
6
|
export default function FloatingPortal({ anchor, anchorElement, panel, open, options, children, }) {
|
|
7
|
-
const scope = useRef(null);
|
|
8
7
|
const target = anchorElement && options.portal !== false
|
|
9
8
|
? floatingContainer(anchorElement, options)
|
|
10
9
|
: null;
|
|
11
|
-
useLayoutEffect(() => {
|
|
12
|
-
const root = scope.current;
|
|
13
|
-
if (!open || !anchorElement || !root)
|
|
14
|
-
return;
|
|
15
|
-
const view = anchorElement.ownerDocument.defaultView;
|
|
16
|
-
if (!view)
|
|
17
|
-
return;
|
|
18
|
-
const synchronize = () => {
|
|
19
|
-
const computed = view.getComputedStyle(anchorElement);
|
|
20
|
-
// Copy inherited CSS variables without controlling the host document's theme.
|
|
21
|
-
for (let index = 0; index < computed.length; index++) {
|
|
22
|
-
const name = computed.item(index);
|
|
23
|
-
if (name.startsWith("--"))
|
|
24
|
-
root.style.setProperty(name, computed.getPropertyValue(name));
|
|
25
|
-
}
|
|
26
|
-
root.dir = computed.direction;
|
|
27
|
-
root.style.fontFamily = computed.fontFamily;
|
|
28
|
-
root.style.color = computed.color;
|
|
29
|
-
};
|
|
30
|
-
synchronize();
|
|
31
|
-
const observer = new view.MutationObserver(synchronize);
|
|
32
|
-
let ancestor = anchorElement;
|
|
33
|
-
while (ancestor) {
|
|
34
|
-
observer.observe(ancestor, {
|
|
35
|
-
attributes: true,
|
|
36
|
-
attributeFilter: [
|
|
37
|
-
"style",
|
|
38
|
-
"class",
|
|
39
|
-
"dir",
|
|
40
|
-
"data-theme",
|
|
41
|
-
"data-contour-theme",
|
|
42
|
-
],
|
|
43
|
-
});
|
|
44
|
-
ancestor = ancestor.parentElement;
|
|
45
|
-
}
|
|
46
|
-
return () => observer.disconnect();
|
|
47
|
-
}, [anchorElement, target, open]);
|
|
48
10
|
useFloatingPanel(anchor, panel, open, { ...options, container: target });
|
|
49
|
-
const content = (_jsx(
|
|
11
|
+
const content = (_jsx(FloatingScope, { anchor: anchorElement, open: open, target: target, children: children }));
|
|
50
12
|
return target ? createPortal(content, target) : content;
|
|
51
13
|
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { type ReactNode } from "react";
|
|
2
|
+
/** Carry the anchor's inherited theme into a portal without changing its host. */
|
|
3
|
+
export default function FloatingScope({ anchor, open, target, onDirectionChange, children, }: {
|
|
4
|
+
anchor: HTMLElement | null;
|
|
5
|
+
open: boolean;
|
|
6
|
+
target: Node | null;
|
|
7
|
+
onDirectionChange?: (direction: "ltr" | "rtl") => void;
|
|
8
|
+
children: ReactNode;
|
|
9
|
+
}): import("react").JSX.Element;
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
3
|
+
import { useLayoutEffect, useRef } from "react";
|
|
4
|
+
/** Carry the anchor's inherited theme into a portal without changing its host. */
|
|
5
|
+
export default function FloatingScope({ anchor, open, target, onDirectionChange, children, }) {
|
|
6
|
+
const scope = useRef(null);
|
|
7
|
+
useLayoutEffect(() => {
|
|
8
|
+
const root = scope.current;
|
|
9
|
+
const view = anchor?.ownerDocument.defaultView;
|
|
10
|
+
if (!open || !anchor || !root || !view)
|
|
11
|
+
return;
|
|
12
|
+
const synchronize = () => {
|
|
13
|
+
const computed = view.getComputedStyle(anchor);
|
|
14
|
+
for (let index = 0; index < computed.length; index++) {
|
|
15
|
+
const name = computed.item(index);
|
|
16
|
+
if (name.startsWith("--"))
|
|
17
|
+
root.style.setProperty(name, computed.getPropertyValue(name));
|
|
18
|
+
}
|
|
19
|
+
root.dir = computed.direction;
|
|
20
|
+
root.style.fontFamily = computed.fontFamily;
|
|
21
|
+
root.style.color = computed.color;
|
|
22
|
+
onDirectionChange?.(computed.direction === "rtl" ? "rtl" : "ltr");
|
|
23
|
+
};
|
|
24
|
+
synchronize();
|
|
25
|
+
const observer = new view.MutationObserver(synchronize);
|
|
26
|
+
let ancestor = anchor;
|
|
27
|
+
while (ancestor) {
|
|
28
|
+
observer.observe(ancestor, {
|
|
29
|
+
attributes: true,
|
|
30
|
+
attributeFilter: [
|
|
31
|
+
"style",
|
|
32
|
+
"class",
|
|
33
|
+
"dir",
|
|
34
|
+
"data-theme",
|
|
35
|
+
"data-contour-theme",
|
|
36
|
+
],
|
|
37
|
+
});
|
|
38
|
+
ancestor = ancestor.parentElement;
|
|
39
|
+
}
|
|
40
|
+
return () => observer.disconnect();
|
|
41
|
+
}, [anchor, target, open, onDirectionChange]);
|
|
42
|
+
return (_jsx("span", { ref: scope, className: "contour-floating-root contour-root", style: { display: "contents" }, children: children }));
|
|
43
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { type TableHTMLAttributes, type ReactNode } from "react";
|
|
2
|
+
import type { NativePartProps } from "../../lib/componentProps.js";
|
|
3
|
+
export type TableProps = Omit<TableHTMLAttributes<HTMLTableElement>, "children"> & {
|
|
4
|
+
/** Accessible table name. The component owns the caption element. */
|
|
5
|
+
caption: string;
|
|
6
|
+
children: ReactNode;
|
|
7
|
+
/** Show the caption visually; it remains accessible when false. @default false */
|
|
8
|
+
showCaption?: boolean;
|
|
9
|
+
scrollProps?: NativePartProps<"div">;
|
|
10
|
+
};
|
|
11
|
+
/** Semantic table styling for application-owned rows, cells and table engines. */
|
|
12
|
+
declare const Table: import("react").ForwardRefExoticComponent<Omit<TableHTMLAttributes<HTMLTableElement>, "children"> & {
|
|
13
|
+
/** Accessible table name. The component owns the caption element. */
|
|
14
|
+
caption: string;
|
|
15
|
+
children: ReactNode;
|
|
16
|
+
/** Show the caption visually; it remains accessible when false. @default false */
|
|
17
|
+
showCaption?: boolean;
|
|
18
|
+
scrollProps?: NativePartProps<"div">;
|
|
19
|
+
} & import("react").RefAttributes<HTMLTableElement>>;
|
|
20
|
+
export default Table;
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
+
import { forwardRef } from "react";
|
|
4
|
+
import { ccx } from "../../lib/classNames.js";
|
|
5
|
+
/** Semantic table styling for application-owned rows, cells and table engines. */
|
|
6
|
+
const Table = forwardRef(function Table({ caption, children, showCaption = false, scrollProps, className, ...rest }, ref) {
|
|
7
|
+
return (_jsx("div", { ...scrollProps, className: ccx("table-scroll", scrollProps?.className), tabIndex: scrollProps?.tabIndex ?? 0, role: scrollProps?.role ?? "region", "aria-label": scrollProps?.["aria-label"] ?? caption, children: _jsxs("table", { ...rest, ref: ref, className: ccx("data-table", className), children: [_jsx("caption", { className: showCaption ? undefined : ccx("visually-hidden"), children: caption }), children] }) }));
|
|
8
|
+
});
|
|
9
|
+
export default Table;
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { type RefObject } from "react";
|
|
2
|
+
/** Use public virtual-anchor/Rect extension points; keep upstream focus/selection and positioning. */
|
|
3
|
+
export default function useComboboxAnchor(input: HTMLInputElement | null, floating: RefObject<HTMLDivElement | null>): {
|
|
4
|
+
anchor?: undefined;
|
|
5
|
+
collisionBoundary?: undefined;
|
|
6
|
+
} | {
|
|
7
|
+
anchor: {
|
|
8
|
+
contextElement: HTMLElement;
|
|
9
|
+
getBoundingClientRect(): DOMRect;
|
|
10
|
+
};
|
|
11
|
+
collisionBoundary: {
|
|
12
|
+
readonly x: number;
|
|
13
|
+
readonly y: number;
|
|
14
|
+
readonly width: number;
|
|
15
|
+
readonly height: number;
|
|
16
|
+
};
|
|
17
|
+
};
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { useMemo } from "react";
|
|
3
|
+
import { platform } from "@floating-ui/dom";
|
|
4
|
+
/** Fixed children of a zoomed body need anchor and viewport in the same local CSS units. */
|
|
5
|
+
function windowScale(element) {
|
|
6
|
+
if (!element ||
|
|
7
|
+
platform.getOffsetParent?.(element) !== element.ownerDocument.defaultView)
|
|
8
|
+
return { x: 1, y: 1 };
|
|
9
|
+
const scale = platform.getScale?.(element);
|
|
10
|
+
if (!scale ||
|
|
11
|
+
!("x" in scale) ||
|
|
12
|
+
!("y" in scale) ||
|
|
13
|
+
!Number.isFinite(scale.x) ||
|
|
14
|
+
!Number.isFinite(scale.y) ||
|
|
15
|
+
scale.x <= 0 ||
|
|
16
|
+
scale.y <= 0)
|
|
17
|
+
return { x: 1, y: 1 };
|
|
18
|
+
return scale;
|
|
19
|
+
}
|
|
20
|
+
/** Use public virtual-anchor/Rect extension points; keep upstream focus/selection and positioning. */
|
|
21
|
+
export default function useComboboxAnchor(input, floating) {
|
|
22
|
+
return useMemo(() => {
|
|
23
|
+
const source = input?.parentElement ?? input;
|
|
24
|
+
if (!source)
|
|
25
|
+
return {};
|
|
26
|
+
return {
|
|
27
|
+
anchor: {
|
|
28
|
+
contextElement: source,
|
|
29
|
+
getBoundingClientRect() {
|
|
30
|
+
const rect = source.getBoundingClientRect(), scale = windowScale(floating.current);
|
|
31
|
+
rect.x /= scale.x;
|
|
32
|
+
rect.y /= scale.y;
|
|
33
|
+
rect.width /= scale.x;
|
|
34
|
+
rect.height /= scale.y;
|
|
35
|
+
return rect;
|
|
36
|
+
},
|
|
37
|
+
},
|
|
38
|
+
// Getters are evaluated on each upstream update, including scroll, zoom and mobile viewport resize.
|
|
39
|
+
collisionBoundary: {
|
|
40
|
+
get x() {
|
|
41
|
+
return ((source.ownerDocument.defaultView?.visualViewport?.offsetLeft ??
|
|
42
|
+
0) / windowScale(floating.current).x);
|
|
43
|
+
},
|
|
44
|
+
get y() {
|
|
45
|
+
return ((source.ownerDocument.defaultView?.visualViewport?.offsetTop ?? 0) /
|
|
46
|
+
windowScale(floating.current).y);
|
|
47
|
+
},
|
|
48
|
+
get width() {
|
|
49
|
+
return ((source.ownerDocument.defaultView?.visualViewport?.width ??
|
|
50
|
+
source.ownerDocument.documentElement.clientWidth) /
|
|
51
|
+
windowScale(floating.current).x);
|
|
52
|
+
},
|
|
53
|
+
get height() {
|
|
54
|
+
return ((source.ownerDocument.defaultView?.visualViewport?.height ??
|
|
55
|
+
source.ownerDocument.documentElement.clientHeight) /
|
|
56
|
+
windowScale(floating.current).y);
|
|
57
|
+
},
|
|
58
|
+
},
|
|
59
|
+
};
|
|
60
|
+
}, [input, floating]);
|
|
61
|
+
}
|
package/dist/index.d.ts
CHANGED
|
@@ -132,3 +132,5 @@ export type * from "./components/ui/ThemeScope.js";
|
|
|
132
132
|
export type { FloatingOptions } from "./hooks/useFloatingPanel.js";
|
|
133
133
|
export type { PanelFocusOptions } from "./hooks/usePanelFocus.js";
|
|
134
134
|
export type { DismissReason } from "./lib/layers.js";
|
|
135
|
+
export { default as Table, type TableProps } from "./components/ui/Table.js";
|
|
136
|
+
export { default as Combobox, type ComboboxProps, type ComboboxOption, } from "./components/ui/Combobox.js";
|
package/dist/index.js
CHANGED
|
@@ -67,3 +67,5 @@ export * from "./lib/motion.js";
|
|
|
67
67
|
export { default as SceneBoundary } from "./components/hero/SceneBoundary.js";
|
|
68
68
|
export { safeHref } from "./lib/safeHref.js";
|
|
69
69
|
export { default as ThemeScope } from "./components/ui/ThemeScope.js";
|
|
70
|
+
export { default as Table } from "./components/ui/Table.js";
|
|
71
|
+
export { default as Combobox, } from "./components/ui/Combobox.js";
|
|
@@ -277,12 +277,18 @@
|
|
|
277
277
|
}
|
|
278
278
|
|
|
279
279
|
.data-table {
|
|
280
|
+
color: var(--fg);
|
|
280
281
|
width: 100%;
|
|
281
282
|
min-width: 560px;
|
|
282
283
|
border-collapse: collapse;
|
|
283
284
|
font-size: 14px;
|
|
284
285
|
}
|
|
285
286
|
|
|
287
|
+
.data-table caption {
|
|
288
|
+
color: inherit;
|
|
289
|
+
text-align: left;
|
|
290
|
+
}
|
|
291
|
+
|
|
286
292
|
.data-table th {
|
|
287
293
|
padding: 14px 16px 14px 0;
|
|
288
294
|
border-bottom: 1px solid var(--line);
|
package/dist/kit/forms.css
CHANGED
|
@@ -370,3 +370,37 @@ textarea {
|
|
|
370
370
|
var(--line-strong) var(--progress)
|
|
371
371
|
);
|
|
372
372
|
}
|
|
373
|
+
|
|
374
|
+
/* Combobox uses the same native field frame; the popup is an allowed-value list. */
|
|
375
|
+
.combobox { min-width: 0; }
|
|
376
|
+
.combobox-control { display: flex; align-items: center; gap: 8px; }
|
|
377
|
+
.combobox-control input { min-width: 0; flex: 1; }
|
|
378
|
+
.combobox-trigger, .combobox-retry {
|
|
379
|
+
border: 0; background: transparent; color: inherit; font: inherit; cursor: pointer;
|
|
380
|
+
}
|
|
381
|
+
.combobox-trigger { display: grid; place-items: center; min-width: 36px; min-height: 36px; flex: none; }
|
|
382
|
+
.combobox-trigger:disabled { cursor: default; opacity: .45; }
|
|
383
|
+
.combobox-positioner { z-index: 30; }
|
|
384
|
+
.combobox-popup {
|
|
385
|
+
width: var(--anchor-width, 16rem);
|
|
386
|
+
max-width: var(--available-width, calc(100vw - 16px));
|
|
387
|
+
max-height: min(320px, var(--available-height, 320px));
|
|
388
|
+
overflow: auto; overscroll-behavior: contain;
|
|
389
|
+
border: 1px solid var(--line); border-radius: var(--radius-sm);
|
|
390
|
+
background: var(--bg); color: var(--fg); padding: 4px;
|
|
391
|
+
box-shadow: 0 8px 28px rgb(0 0 0 / .12);
|
|
392
|
+
}
|
|
393
|
+
.combobox-list { outline: none; }
|
|
394
|
+
.combobox-option { display: flex; justify-content: space-between; align-items: center; gap: 12px; padding: 10px 12px; overflow-wrap: anywhere; cursor: default; }
|
|
395
|
+
.combobox-option[data-highlighted] { background: var(--invert-bg); color: var(--invert-fg); outline: 2px solid currentColor; outline-offset: -2px; }
|
|
396
|
+
.combobox-option[data-disabled] { opacity: .45; }
|
|
397
|
+
.combobox-status { padding: 12px; font-size: 14px; }
|
|
398
|
+
.combobox-retry { padding: 10px 12px; text-decoration: underline; }
|
|
399
|
+
.combobox-trigger:focus-visible, .combobox-retry:focus-visible { outline: 2px solid currentColor; outline-offset: 2px; }
|
|
400
|
+
@media (forced-colors: active) {
|
|
401
|
+
.combobox-popup { border-color: CanvasText; background: Canvas; color: CanvasText; }
|
|
402
|
+
.combobox-option[data-highlighted] { background: Highlight; color: HighlightText; }
|
|
403
|
+
.combobox-option[data-disabled] { color: GrayText; opacity: 1; }
|
|
404
|
+
}
|
|
405
|
+
/* Keep the live region mounted without a blank row when there is no message. */
|
|
406
|
+
.combobox-status:empty { padding: 0; }
|
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
// Generated compatibility aliases; unknown consumer classes pass through unchanged.
|
|
2
|
-
export const componentClasses = new Set(["alert", "alert-accent", "alert-action", "alert-banner", "alert-body", "alert-content", "alert-danger", "alert-dismiss", "alert-icon", "alert-info", "alert-success", "alert-title", "alert-warning", "avatar", "avatar-group", "avatar-initials", "avatar-lg", "avatar-sm", "avatar-status", "avatar-status-away", "avatar-status-online", "avatar-xl", "badge", "badge-danger", "badge-dot", "badge-info", "badge-invert", "badge-neutral", "badge-success", "badge-warning", "brand", "breadcrumbs", "button", "button-accent", "button-danger", "button-ghost", "button-icon", "button-lg", "button-secondary", "button-sm", "card", "card-acid", "card-body", "card-footer", "card-icon", "card-ink", "card-interactive", "card-title", "card-top", "choice", "choice-box", "choice-control", "choice-description", "choice-error", "choice-input", "choice-invalid", "choice-label", "choice-radio", "choice-text", "count-up", "css", "data-table", "data-table-idle", "depth-button", "depth-button-face", "dialog", "dialog-body", "dialog-close", "dialog-description", "dialog-footer", "dialog-header", "dialog-left", "dialog-panel", "dialog-right", "disclosure", "disclosure-icon", "divider", "divider-labelled", "empty-state", "empty-state-icon", "field", "field-chevron", "field-control", "field-counter", "field-disabled", "field-error", "field-footer", "field-icon", "field-invalid", "field-label", "field-required", "field-select", "field-trailing", "flip-back", "flip-card", "flip-face", "flip-front", "flip-stage", "flip-toggle", "hero-letter", "icon-button", "icon-button-accent", "icon-button-lg", "icon-button-sm", "icon-button-solid", "icon-button-spin", "inline-link", "is-flipped", "is-open", "is-visible", "kbd", "magnetic", "marquee", "marquee-inner", "marquee-track", "menu", "menu-chevron", "menu-end", "menu-item", "menu-item-danger", "menu-list", "menu-start", "menu-trigger", "mono", "opening-sequence", "page-width", "pagination", "pagination-gap", "pagination-page", "pagination-step", "popover", "popover-end", "popover-panel", "popover-start", "popover-title", "progress", "progress-accent", "progress-danger", "progress-fill", "progress-header", "progress-indeterminate", "progress-success", "progress-track", "radio-group", "radio-group-inline", "reveal", "scroll-progress", "sculpture-canvas", "section-label", "segmented", "segmented-option", "site-shell", "skeleton", "skeleton-block", "skeleton-circle", "skeleton-line", "skip-link", "slider", "slider-header", "spinner", "stage-canvas", "stat", "stat-suffix", "stepper", "stepper-complete", "stepper-current", "stepper-description", "stepper-label", "stepper-marker", "stepper-step", "stepper-text", "stepper-upcoming", "switch", "switch-thumb", "switch-track", "table-scroll", "tabs", "tabs-list", "tabs-panel", "tabs-tab", "tag", "tag-removable", "tag-remove", "text-link", "theme-fading", "theme-icon-moon", "theme-icon-sun", "theme-toggle", "tilt-card", "tilt-glare", "tilt-stage", "timeline", "timeline-body", "timeline-current", "timeline-date", "timeline-entry", "timeline-title", "toast", "toast-bar", "toast-close", "toast-content", "toast-danger", "toast-info", "toast-success", "toast-viewport", "toast-warning", "tooltip", "tooltip-anchor", "tooltip-bottom", "tooltip-left", "tooltip-right", "tooltip-top", "ts", "type-line", "type-run", "visually-hidden"]);
|
|
2
|
+
export const componentClasses = new Set(["alert", "alert-accent", "alert-action", "alert-banner", "alert-body", "alert-content", "alert-danger", "alert-dismiss", "alert-icon", "alert-info", "alert-success", "alert-title", "alert-warning", "avatar", "avatar-group", "avatar-initials", "avatar-lg", "avatar-sm", "avatar-status", "avatar-status-away", "avatar-status-online", "avatar-xl", "badge", "badge-danger", "badge-dot", "badge-info", "badge-invert", "badge-neutral", "badge-success", "badge-warning", "brand", "breadcrumbs", "button", "button-accent", "button-danger", "button-ghost", "button-icon", "button-lg", "button-secondary", "button-sm", "card", "card-acid", "card-body", "card-footer", "card-icon", "card-ink", "card-interactive", "card-title", "card-top", "choice", "choice-box", "choice-control", "choice-description", "choice-error", "choice-input", "choice-invalid", "choice-label", "choice-radio", "choice-text", "combobox", "combobox-control", "combobox-list", "combobox-option", "combobox-popup", "combobox-positioner", "combobox-retry", "combobox-status", "combobox-trigger", "count-up", "css", "data-table", "data-table-idle", "depth-button", "depth-button-face", "dialog", "dialog-body", "dialog-close", "dialog-description", "dialog-footer", "dialog-header", "dialog-left", "dialog-panel", "dialog-right", "disclosure", "disclosure-icon", "divider", "divider-labelled", "empty-state", "empty-state-icon", "field", "field-chevron", "field-control", "field-counter", "field-disabled", "field-error", "field-footer", "field-icon", "field-invalid", "field-label", "field-required", "field-select", "field-trailing", "flip-back", "flip-card", "flip-face", "flip-front", "flip-stage", "flip-toggle", "hero-letter", "icon-button", "icon-button-accent", "icon-button-lg", "icon-button-sm", "icon-button-solid", "icon-button-spin", "inline-link", "is-flipped", "is-open", "is-visible", "kbd", "magnetic", "marquee", "marquee-inner", "marquee-track", "menu", "menu-chevron", "menu-end", "menu-item", "menu-item-danger", "menu-list", "menu-start", "menu-trigger", "mono", "opening-sequence", "page-width", "pagination", "pagination-gap", "pagination-page", "pagination-step", "popover", "popover-end", "popover-panel", "popover-start", "popover-title", "progress", "progress-accent", "progress-danger", "progress-fill", "progress-header", "progress-indeterminate", "progress-success", "progress-track", "radio-group", "radio-group-inline", "reveal", "scroll-progress", "sculpture-canvas", "section-label", "segmented", "segmented-option", "site-shell", "skeleton", "skeleton-block", "skeleton-circle", "skeleton-line", "skip-link", "slider", "slider-header", "spinner", "stage-canvas", "stat", "stat-suffix", "stepper", "stepper-complete", "stepper-current", "stepper-description", "stepper-label", "stepper-marker", "stepper-step", "stepper-text", "stepper-upcoming", "switch", "switch-thumb", "switch-track", "table-scroll", "tabs", "tabs-list", "tabs-panel", "tabs-tab", "tag", "tag-removable", "tag-remove", "text-link", "theme-fading", "theme-icon-moon", "theme-icon-sun", "theme-toggle", "tilt-card", "tilt-glare", "tilt-stage", "timeline", "timeline-body", "timeline-current", "timeline-date", "timeline-entry", "timeline-title", "toast", "toast-bar", "toast-close", "toast-content", "toast-danger", "toast-info", "toast-success", "toast-viewport", "toast-warning", "tooltip", "tooltip-anchor", "tooltip-bottom", "tooltip-left", "tooltip-right", "tooltip-top", "ts", "type-line", "type-run", "visually-hidden"]);
|
|
@@ -273,12 +273,18 @@
|
|
|
273
273
|
}
|
|
274
274
|
|
|
275
275
|
.contour-data-table {
|
|
276
|
+
color: var(--contour-fg);
|
|
276
277
|
width: 100%;
|
|
277
278
|
min-width: 560px;
|
|
278
279
|
border-collapse: collapse;
|
|
279
280
|
font-size: 14px;
|
|
280
281
|
}
|
|
281
282
|
|
|
283
|
+
.contour-data-table caption {
|
|
284
|
+
color: inherit;
|
|
285
|
+
text-align: start;
|
|
286
|
+
}
|
|
287
|
+
|
|
282
288
|
.contour-data-table th {
|
|
283
289
|
padding: 14px 16px 14px 0;
|
|
284
290
|
border-bottom: 1px solid var(--contour-line);
|
|
@@ -368,3 +368,37 @@
|
|
|
368
368
|
var(--contour-line-strong) var(--contour-progress, var(--progress))
|
|
369
369
|
);
|
|
370
370
|
}
|
|
371
|
+
|
|
372
|
+
/* Combobox uses the same native field frame; the popup is an allowed-value list. */
|
|
373
|
+
.contour-combobox { min-width: 0; }
|
|
374
|
+
.contour-combobox-control { display: flex; align-items: center; gap: 8px; }
|
|
375
|
+
.contour-combobox-control input { min-width: 0; flex: 1; }
|
|
376
|
+
.contour-combobox-trigger, .contour-combobox-retry {
|
|
377
|
+
border: 0; background: transparent; color: inherit; font: inherit; cursor: pointer;
|
|
378
|
+
}
|
|
379
|
+
.contour-combobox-trigger { display: grid; place-items: center; min-width: 36px; min-height: 36px; flex: none; }
|
|
380
|
+
.contour-combobox-trigger:disabled { cursor: default; opacity: .45; }
|
|
381
|
+
.contour-combobox-positioner { z-index: 30; }
|
|
382
|
+
.contour-combobox-popup {
|
|
383
|
+
width: var(--anchor-width, 16rem);
|
|
384
|
+
max-width: var(--available-width, calc(100vw - 16px));
|
|
385
|
+
max-height: min(320px, var(--available-height, 320px));
|
|
386
|
+
overflow: auto; overscroll-behavior: contain;
|
|
387
|
+
border: 1px solid var(--contour-line); border-radius: var(--contour-radius-sm);
|
|
388
|
+
background: var(--contour-bg); color: var(--contour-fg); padding: 4px;
|
|
389
|
+
box-shadow: 0 8px 28px rgb(0 0 0 / .12);
|
|
390
|
+
}
|
|
391
|
+
.contour-combobox-list { outline: none; }
|
|
392
|
+
.contour-combobox-option { display: flex; justify-content: space-between; align-items: center; gap: 12px; padding: 10px 12px; overflow-wrap: anywhere; cursor: default; }
|
|
393
|
+
.contour-combobox-option[data-highlighted] { background: var(--contour-invert-bg); color: var(--contour-invert-fg); outline: 2px solid currentColor; outline-offset: -2px; }
|
|
394
|
+
.contour-combobox-option[data-disabled] { opacity: .45; }
|
|
395
|
+
.contour-combobox-status { padding: 12px; font-size: 14px; }
|
|
396
|
+
.contour-combobox-retry { padding: 10px 12px; text-decoration: underline; }
|
|
397
|
+
.contour-combobox-trigger:focus-visible, .contour-combobox-retry:focus-visible { outline: 2px solid currentColor; outline-offset: 2px; }
|
|
398
|
+
@media (forced-colors: active) {
|
|
399
|
+
.contour-combobox-popup { border-color: CanvasText; background: Canvas; color: CanvasText; }
|
|
400
|
+
.contour-combobox-option[data-highlighted] { background: Highlight; color: HighlightText; }
|
|
401
|
+
.contour-combobox-option[data-disabled] { color: GrayText; opacity: 1; }
|
|
402
|
+
}
|
|
403
|
+
/* Keep the live region mounted without a blank row when there is no message. */
|
|
404
|
+
.contour-combobox-status:empty { padding: 0; }
|