@abuzareal/contour-ui 0.5.0 → 0.6.1

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.
Files changed (87) hide show
  1. package/CHANGELOG.md +16 -0
  2. package/COMBOBOX.md +87 -0
  3. package/COMPOSITION.md +4 -0
  4. package/CONTRACTS.md +27 -27
  5. package/DOCS.md +86 -0
  6. package/FORMS.md +4 -0
  7. package/FOUNDATION.md +35 -0
  8. package/GETTING-STARTED.md +67 -0
  9. package/HELPERS.md +33 -0
  10. package/HOOKS.md +67 -0
  11. package/MANUAL-QA.md +38 -0
  12. package/MIGRATION.md +44 -0
  13. package/MODELS.md +18 -0
  14. package/NEXTJS.md +3 -1
  15. package/OVERLAYS.md +4 -0
  16. package/PROP-REFERENCE.md +37 -0
  17. package/PUBLIC-API.md +174 -164
  18. package/README.md +9 -5
  19. package/RECIPES.md +43 -0
  20. package/REFERENCE.md +193 -0
  21. package/RELEASING.md +8 -2
  22. package/SIZE.md +9 -3
  23. package/STYLING.md +4 -0
  24. package/SUPPORT.md +36 -0
  25. package/THREE.md +40 -0
  26. package/dist/components/ui/Combobox.d.ts +325 -0
  27. package/dist/components/ui/Combobox.js +135 -0
  28. package/dist/components/ui/Combobox.types.d.ts +46 -0
  29. package/dist/components/ui/Combobox.types.js +1 -0
  30. package/dist/components/ui/DataTable.js +5 -4
  31. package/dist/components/ui/Field.js +1 -1
  32. package/dist/components/ui/FloatingPortal.js +2 -40
  33. package/dist/components/ui/FloatingScope.d.ts +9 -0
  34. package/dist/components/ui/FloatingScope.js +43 -0
  35. package/dist/components/ui/Table.d.ts +20 -0
  36. package/dist/components/ui/Table.js +9 -0
  37. package/dist/hooks/useComboboxAnchor.d.ts +17 -0
  38. package/dist/hooks/useComboboxAnchor.js +61 -0
  39. package/dist/hooks/useInitialHashScroll.js +5 -1
  40. package/dist/hooks/usePrintPreparation.js +12 -1
  41. package/dist/hooks/useScrollSpy.js +6 -3
  42. package/dist/index.d.ts +2 -0
  43. package/dist/index.js +2 -0
  44. package/dist/kit/data-display.css +6 -0
  45. package/dist/kit/forms.css +34 -0
  46. package/dist/lib/componentClasses.js +1 -1
  47. package/dist/scoped/kit/data-display.css +6 -0
  48. package/dist/scoped/kit/forms.css +34 -0
  49. package/docs/prop-metadata.json +2668 -0
  50. package/examples/onboarding/SettingsForm.jsx +30 -0
  51. package/examples/onboarding/SettingsForm.tsx +50 -0
  52. package/examples/optional/LazyScene.tsx +78 -0
  53. package/examples/optional/OptionalSmoothScroll.tsx +25 -0
  54. package/examples/optional/SmoothScrollOwner.tsx +7 -0
  55. package/examples/optional/optional.css +25 -0
  56. package/examples/recipes/AsyncProjectPicker.tsx +82 -0
  57. package/examples/recipes/DeleteProjectConfirmation.tsx +97 -0
  58. package/examples/recipes/InputCompositions.tsx +49 -0
  59. package/examples/recipes/RemoteRecordsTable.tsx +256 -0
  60. package/examples/recipes/useRemoteResource.ts +81 -0
  61. package/examples/reference/HookExamples.tsx +146 -0
  62. package/examples/reproduction/README.md +9 -0
  63. package/examples/reproduction/index.html +14 -0
  64. package/examples/reproduction/package.json +27 -0
  65. package/examples/reproduction/src/main.tsx +33 -0
  66. package/examples/reproduction/src/style.css +19 -0
  67. package/examples/reproduction/tsconfig.json +18 -0
  68. package/examples/reproduction/vite.config.ts +19 -0
  69. package/examples/starters/next/README.md +18 -0
  70. package/examples/starters/next/app/Demo.tsx +65 -0
  71. package/examples/starters/next/app/layout.tsx +25 -0
  72. package/examples/starters/next/app/next-page/page.tsx +9 -0
  73. package/examples/starters/next/app/page.tsx +11 -0
  74. package/examples/starters/next/app/style.css +19 -0
  75. package/examples/starters/next/next-env.d.ts +2 -0
  76. package/examples/starters/next/next.config.mjs +1 -0
  77. package/examples/starters/next/package.json +27 -0
  78. package/examples/starters/next/proxy.ts +16 -0
  79. package/examples/starters/next/tsconfig.json +30 -0
  80. package/examples/starters/vite/README.md +16 -0
  81. package/examples/starters/vite/index.html +14 -0
  82. package/examples/starters/vite/package.json +27 -0
  83. package/examples/starters/vite/src/main.tsx +81 -0
  84. package/examples/starters/vite/src/style.css +19 -0
  85. package/examples/starters/vite/tsconfig.json +18 -0
  86. package/examples/starters/vite/vite.config.ts +19 -0
  87. package/package.json +37 -4
@@ -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
+ }
@@ -24,6 +24,10 @@ export default function useInitialHashScroll() {
24
24
  align();
25
25
  userScrollEvents.forEach((event) => window.addEventListener(event, markScrolled, { passive: true }));
26
26
  document.fonts.ready.then(align);
27
- return () => userScrollEvents.forEach((event) => window.removeEventListener(event, markScrolled));
27
+ return () => {
28
+ // Font readiness cannot be cancelled; an unmounted owner must not scroll a later page.
29
+ userScrolled = true;
30
+ userScrollEvents.forEach((event) => window.removeEventListener(event, markScrolled));
31
+ };
28
32
  }, []);
29
33
  }
@@ -6,7 +6,12 @@ export default function usePrintPreparation() {
6
6
  const root = document.documentElement;
7
7
  let expanded = [];
8
8
  let screenTheme;
9
+ let printing = false;
9
10
  const prepare = () => {
11
+ // Some browsers repeat beforeprint while the same print preview is open.
12
+ if (printing)
13
+ return;
14
+ printing = true;
10
15
  expanded = [
11
16
  ...document.querySelectorAll("details:not([open])"),
12
17
  ];
@@ -15,9 +20,14 @@ export default function usePrintPreparation() {
15
20
  root.dataset.theme = "light";
16
21
  };
17
22
  const restore = () => {
23
+ if (!printing)
24
+ return;
25
+ printing = false;
18
26
  expanded.forEach((details) => (details.open = false));
19
27
  expanded = [];
20
- if (screenTheme)
28
+ if (screenTheme === undefined)
29
+ delete root.dataset.theme;
30
+ else
21
31
  root.dataset.theme = screenTheme;
22
32
  };
23
33
  window.addEventListener("beforeprint", prepare);
@@ -25,6 +35,7 @@ export default function usePrintPreparation() {
25
35
  return () => {
26
36
  window.removeEventListener("beforeprint", prepare);
27
37
  window.removeEventListener("afterprint", restore);
38
+ restore();
28
39
  };
29
40
  }, []);
30
41
  }
@@ -5,10 +5,13 @@ import { useEffect, useState } from "react";
5
5
  const activationLine = 0.3;
6
6
  export default function useScrollSpy(ids) {
7
7
  const [activeId, setActiveId] = useState("");
8
- const key = ids.join("|");
8
+ const key = JSON.stringify(ids);
9
9
  useEffect(() => {
10
- const elements = key
11
- .split("|")
10
+ const parsed = JSON.parse(key);
11
+ if (!Array.isArray(parsed) ||
12
+ !parsed.every((id) => typeof id === "string"))
13
+ return;
14
+ const elements = parsed
12
15
  .map((id) => document.getElementById(id))
13
16
  .filter((element) => element !== null);
14
17
  let frame = 0;
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);
@@ -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; }