@mirror-physics/fractal-ui 0.2.0-next.83 → 0.2.0-next.85

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/dist/index.d.cts CHANGED
@@ -304,6 +304,8 @@ interface DropdownProps {
304
304
  triggerLabel?: string;
305
305
  /** Floating field title. Empty selectedValue is the unfilled state. */
306
306
  fieldLabel?: string;
307
+ /** Show a clear action; defaults to true for floating fields. */
308
+ clearable?: boolean;
307
309
  direction?: 'up' | 'down' | 'left' | 'right';
308
310
  /** Preferred alignment; shifted as needed to stay inside visible bounds. */
309
311
  align?: 'left' | 'right';
@@ -331,7 +333,7 @@ interface DropdownProps {
331
333
  closeOnSelect?: boolean;
332
334
  disabled?: boolean;
333
335
  }
334
- declare function Dropdown({ items, selectedValue, onSelect, multiple, selectedValues, onSelectionChange, searchable, searchPlaceholder, emptyMessage, triggerLabel, fieldLabel, direction, align, maxHeight, collisionPadding, triggerOrientation, triggerContent, label, iconOnly, noChevron, size: sizeAlias, controlSize, triggerId, triggerVariant, className, triggerClassName, panelClassName, closeOnSelect, disabled, }: DropdownProps): React.JSX.Element | null;
336
+ declare function Dropdown({ items, selectedValue, onSelect, multiple, selectedValues, onSelectionChange, searchable, searchPlaceholder, emptyMessage, triggerLabel, fieldLabel, clearable, direction, align, maxHeight, collisionPadding, triggerOrientation, triggerContent, label, iconOnly, noChevron, size: sizeAlias, controlSize, triggerId, triggerVariant, className, triggerClassName, panelClassName, closeOnSelect, disabled, }: DropdownProps): React.JSX.Element | null;
335
337
 
336
338
  type LinkTheme = 'default' | 'muted' | 'accent';
337
339
  type LinkSize = 'sm' | 'md' | 'lg';
package/dist/index.d.ts CHANGED
@@ -304,6 +304,8 @@ interface DropdownProps {
304
304
  triggerLabel?: string;
305
305
  /** Floating field title. Empty selectedValue is the unfilled state. */
306
306
  fieldLabel?: string;
307
+ /** Show a clear action; defaults to true for floating fields. */
308
+ clearable?: boolean;
307
309
  direction?: 'up' | 'down' | 'left' | 'right';
308
310
  /** Preferred alignment; shifted as needed to stay inside visible bounds. */
309
311
  align?: 'left' | 'right';
@@ -331,7 +333,7 @@ interface DropdownProps {
331
333
  closeOnSelect?: boolean;
332
334
  disabled?: boolean;
333
335
  }
334
- declare function Dropdown({ items, selectedValue, onSelect, multiple, selectedValues, onSelectionChange, searchable, searchPlaceholder, emptyMessage, triggerLabel, fieldLabel, direction, align, maxHeight, collisionPadding, triggerOrientation, triggerContent, label, iconOnly, noChevron, size: sizeAlias, controlSize, triggerId, triggerVariant, className, triggerClassName, panelClassName, closeOnSelect, disabled, }: DropdownProps): React.JSX.Element | null;
336
+ declare function Dropdown({ items, selectedValue, onSelect, multiple, selectedValues, onSelectionChange, searchable, searchPlaceholder, emptyMessage, triggerLabel, fieldLabel, clearable, direction, align, maxHeight, collisionPadding, triggerOrientation, triggerContent, label, iconOnly, noChevron, size: sizeAlias, controlSize, triggerId, triggerVariant, className, triggerClassName, panelClassName, closeOnSelect, disabled, }: DropdownProps): React.JSX.Element | null;
335
337
 
336
338
  type LinkTheme = 'default' | 'muted' | 'accent';
337
339
  type LinkSize = 'sm' | 'md' | 'lg';
package/dist/index.js CHANGED
@@ -1424,7 +1424,7 @@ function DataTable({ columns, data, emptyMessage, header, size = "md", className
1424
1424
 
1425
1425
  // src/components/Dropdown/Dropdown.tsx
1426
1426
  import * as React11 from "react";
1427
- import { Check as Check2, MagnifyingGlass as MagnifyingGlass2, ChevronDown, ChevronLeft, ChevronRight as ChevronRight2, ChevronUp } from "@mirror-physics/fractal-icons";
1427
+ import { Close as Close2, Check as Check2, MagnifyingGlass as MagnifyingGlass2, ChevronDown, ChevronLeft, ChevronRight as ChevronRight2, ChevronUp } from "@mirror-physics/fractal-icons";
1428
1428
 
1429
1429
  // src/components/Dropdown/position.ts
1430
1430
  function positionDropdown(trigger, panel, bounds, direction, align) {
@@ -1475,6 +1475,7 @@ function Dropdown({
1475
1475
  emptyMessage = "No matching options.",
1476
1476
  triggerLabel,
1477
1477
  fieldLabel,
1478
+ clearable = Boolean(fieldLabel),
1478
1479
  direction = "down",
1479
1480
  align = "left",
1480
1481
  maxHeight = 320,
@@ -1496,8 +1497,12 @@ function Dropdown({
1496
1497
  }) {
1497
1498
  const [query, setQuery] = React11.useState("");
1498
1499
  const searchRef = React11.useRef(null);
1500
+ const searchContainerRef = React11.useRef(null);
1501
+ const optionsRef = React11.useRef(null);
1502
+ const [optionsScrolled, setOptionsScrolled] = React11.useState(false);
1499
1503
  const accessibleLabel = triggerLabel ?? fieldLabel ?? "Options";
1500
1504
  const size = controlSize ?? sizeAlias;
1505
+ const canClear = clearable && !disabled && (multiple ? selectedValues.length > 0 && Boolean(onSelectionChange) : Boolean(selectedValue) && Boolean(onSelect));
1501
1506
  const [open, setOpen] = React11.useState(false);
1502
1507
  React11.useEffect(() => {
1503
1508
  if (!open) setQuery("");
@@ -1526,12 +1531,17 @@ function Dropdown({
1526
1531
  return renderableItems.slice(index + 1, nextHeader < 0 ? void 0 : nextHeader).some((next) => matching.has(next.value));
1527
1532
  });
1528
1533
  const selectableItems = visibleItems.filter((i) => i.kind !== "header");
1529
- const selected = items.find((i) => i.value === selectedValue && i.kind !== "header") ?? selectableItems[0] ?? visibleItems[0];
1534
+ const selected = items.find((i) => i.value === selectedValue && i.kind !== "header") ?? (fieldLabel && !selectedValue && !multiple ? void 0 : selectableItems[0] ?? visibleItems[0]);
1530
1535
  const ChevronIcon = direction === "up" ? ChevronUp : direction === "left" ? ChevronLeft : direction === "right" ? ChevronRight2 : ChevronDown;
1531
1536
  useEscapeDismiss(100, () => {
1532
1537
  setOpen(false);
1533
1538
  triggerRef.current?.focus();
1534
1539
  }, open);
1540
+ React11.useLayoutEffect(() => {
1541
+ if (!open) return;
1542
+ if (optionsRef.current) optionsRef.current.scrollTop = 0;
1543
+ setOptionsScrolled(false);
1544
+ }, [open, query]);
1535
1545
  React11.useLayoutEffect(() => {
1536
1546
  const root = rootRef.current;
1537
1547
  const trigger = triggerRef.current;
@@ -1569,7 +1579,7 @@ function Dropdown({
1569
1579
  panel.style.maxWidth = `${availableWidth}px`;
1570
1580
  panel.style.minWidth = `${Math.min(triggerRect.width, availableWidth)}px`;
1571
1581
  const panelRect = panel.getBoundingClientRect();
1572
- const desiredHeight = Math.min(heightLimit, panel.scrollHeight + panel.offsetHeight - panel.clientHeight);
1582
+ const desiredHeight = Math.min(heightLimit, (optionsRef.current?.scrollHeight ?? 0) + (searchContainerRef.current?.offsetHeight ?? 0) + parseFloat(getComputedStyle(panel).paddingTop) + parseFloat(getComputedStyle(panel).paddingBottom) + panel.offsetHeight - panel.clientHeight);
1573
1583
  const position = positionDropdown(triggerRect, { width: panelRect.width, height: desiredHeight }, bounds, direction, align);
1574
1584
  const nextStyle = {
1575
1585
  left: position.left - rootRect.left - root.clientLeft + root.scrollLeft,
@@ -1594,6 +1604,7 @@ function Dropdown({
1594
1604
  const resize = new ResizeObserver(updatePosition);
1595
1605
  resize.observe(root);
1596
1606
  resize.observe(panel);
1607
+ if (optionsRef.current) resize.observe(optionsRef.current);
1597
1608
  for (let ancestor = root.parentElement; ancestor; ancestor = ancestor.parentElement) {
1598
1609
  resize.observe(ancestor);
1599
1610
  }
@@ -1620,7 +1631,7 @@ function Dropdown({
1620
1631
  return () => document.removeEventListener("mousedown", handleClickOutside);
1621
1632
  }, [open]);
1622
1633
  if (renderableItems.length === 0) return null;
1623
- return /* @__PURE__ */ jsxs17("div", { ref: rootRef, "data-control-size": size, "data-filled": multiple ? selectedValues.length > 0 : Boolean(selectedValue), "data-open": open, className: cn("fractal-dropdown", fieldLabel && "fractal-floating-field fractal-floating-field--dropdown", className), children: [
1634
+ return /* @__PURE__ */ jsxs17("div", { ref: rootRef, "data-control-size": size, "data-filled": multiple ? selectedValues.length > 0 : Boolean(selectedValue), "data-open": open, "data-clearable": canClear || void 0, className: cn("fractal-dropdown", fieldLabel && "fractal-floating-field fractal-floating-field--dropdown", className), children: [
1624
1635
  /* @__PURE__ */ jsxs17(
1625
1636
  "button",
1626
1637
  {
@@ -1665,6 +1676,13 @@ function Dropdown({
1665
1676
  ]
1666
1677
  }
1667
1678
  ),
1679
+ canClear && /* @__PURE__ */ jsx19("button", { type: "button", className: "fractal-dropdown-clear", "aria-label": `Clear ${accessibleLabel}`, onClick: () => {
1680
+ if (multiple) onSelectionChange?.([]);
1681
+ else onSelect?.("");
1682
+ setOpen(false);
1683
+ setQuery("");
1684
+ triggerRef.current?.focus();
1685
+ }, children: /* @__PURE__ */ jsx19(Close2, { "aria-hidden": "true" }) }),
1668
1686
  fieldLabel ? /* @__PURE__ */ jsx19("label", { className: "fractal-floating-field__label", htmlFor: triggerId ?? `${menuId}-trigger`, children: fieldLabel }) : null,
1669
1687
  open && /* @__PURE__ */ jsxs17(
1670
1688
  "div",
@@ -1686,11 +1704,11 @@ function Dropdown({
1686
1704
  className: cn("fractal-dropdown-panel", `fractal-dropdown-panel--${size}`, panelClassName),
1687
1705
  style: panelStyle,
1688
1706
  children: [
1689
- searchable && /* @__PURE__ */ jsxs17("div", { className: "fractal-dropdown-search", children: [
1707
+ searchable && /* @__PURE__ */ jsx19("div", { ref: searchContainerRef, className: "fractal-dropdown-search-container", "data-scrolled": optionsScrolled, children: /* @__PURE__ */ jsxs17("div", { className: "fractal-dropdown-search", children: [
1690
1708
  /* @__PURE__ */ jsx19(MagnifyingGlass2, { "aria-hidden": "true" }),
1691
1709
  /* @__PURE__ */ jsx19("input", { ref: searchRef, type: "search", "aria-label": searchPlaceholder, placeholder: searchPlaceholder, value: query, onChange: (event) => setQuery(event.target.value) })
1692
- ] }),
1693
- /* @__PURE__ */ jsxs17("div", { role: "listbox", id: `${menuId}-options`, "aria-label": accessibleLabel, "aria-multiselectable": multiple || void 0, children: [
1710
+ ] }) }),
1711
+ /* @__PURE__ */ jsxs17("div", { ref: optionsRef, className: "fractal-dropdown-options", onScroll: (event) => setOptionsScrolled(event.currentTarget.scrollTop > 0), role: "listbox", id: `${menuId}-options`, "aria-label": accessibleLabel, "aria-multiselectable": multiple || void 0, children: [
1694
1712
  label != null && /* @__PURE__ */ jsx19("div", { className: "fractal-dropdown-label", children: label }),
1695
1713
  visibleItems.map((item, index) => {
1696
1714
  const topDivider = item.border === "top" ? /* @__PURE__ */ jsx19("div", { className: "fractal-dropdown-separator" }) : null;
@@ -1730,9 +1748,9 @@ function Dropdown({
1730
1748
  ),
1731
1749
  bottomDivider
1732
1750
  ] }, item.value);
1733
- })
1734
- ] }),
1735
- !selectableItems.length && /* @__PURE__ */ jsx19("p", { role: "status", className: "fractal-dropdown-label", children: emptyMessage })
1751
+ }),
1752
+ !selectableItems.length && /* @__PURE__ */ jsx19("p", { role: "status", className: "fractal-dropdown-label", children: emptyMessage })
1753
+ ] })
1736
1754
  ]
1737
1755
  }
1738
1756
  )
@@ -2695,7 +2713,7 @@ SidebarFooter.displayName = "SidebarFooter";
2695
2713
  // src/components/SidePanel/SidePanel.tsx
2696
2714
  import * as React16 from "react";
2697
2715
  import { createPortal as createPortal4 } from "react-dom";
2698
- import { Close as Close2 } from "@mirror-physics/fractal-icons";
2716
+ import { Close as Close3 } from "@mirror-physics/fractal-icons";
2699
2717
  import { jsx as jsx31, jsxs as jsxs26 } from "react/jsx-runtime";
2700
2718
  function SidePanel({ open, onClose, title, description, children, footer, size = "md", loading = false, loadingLabel = "Loading", className }) {
2701
2719
  useEscapeDismiss(80, onClose, open);
@@ -2746,7 +2764,7 @@ function SidePanel({ open, onClose, title, description, children, footer, size =
2746
2764
  typeof title === "string" ? /* @__PURE__ */ jsx31("h2", { className: "fractal-side-panel__title", children: title }) : /* @__PURE__ */ jsx31("div", { className: "fractal-side-panel__title", children: title }),
2747
2765
  description && /* @__PURE__ */ jsx31("p", { className: "fractal-side-panel__description", children: description })
2748
2766
  ] }),
2749
- /* @__PURE__ */ jsx31("button", { type: "button", className: "fractal-side-panel__close", "aria-label": "Close panel", onClick: onClose, children: /* @__PURE__ */ jsx31(Close2, { size: 18 }) })
2767
+ /* @__PURE__ */ jsx31("button", { type: "button", className: "fractal-side-panel__close", "aria-label": "Close panel", onClick: onClose, children: /* @__PURE__ */ jsx31(Close3, { size: 18 }) })
2750
2768
  ] }),
2751
2769
  /* @__PURE__ */ jsx31("div", { ref: bodyRef, "aria-busy": loading || void 0, className: cn("fractal-side-panel__body", loading && "fractal-side-panel__body--loading"), children: loading ? /* @__PURE__ */ jsx31(UILoader, { label: loadingLabel }) : children }),
2752
2770
  footer && /* @__PURE__ */ jsx31("footer", { className: "fractal-side-panel__footer", "data-scroll-divider": scrollEdges.below, children: footer })
@@ -2806,7 +2824,7 @@ TextButton.displayName = "TextButton";
2806
2824
 
2807
2825
  // src/components/FileDropzone/FileDropzone.tsx
2808
2826
  import * as React19 from "react";
2809
- import { Close as Close3, FileArrowUp } from "@mirror-physics/fractal-icons";
2827
+ import { Close as Close4, FileArrowUp } from "@mirror-physics/fractal-icons";
2810
2828
  import { jsx as jsx34, jsxs as jsxs29 } from "react/jsx-runtime";
2811
2829
  function FileDropzone({
2812
2830
  files,
@@ -2886,7 +2904,7 @@ function FileDropzone({
2886
2904
  "aria-label": `Remove ${file.name}`,
2887
2905
  disabled,
2888
2906
  onClick: () => onFilesChange(files.filter((_, fileIndex) => fileIndex !== index)),
2889
- children: /* @__PURE__ */ jsx34(Close3, { "aria-hidden": "true", size: 14 })
2907
+ children: /* @__PURE__ */ jsx34(Close4, { "aria-hidden": "true", size: 14 })
2890
2908
  }
2891
2909
  )
2892
2910
  ] }, `${file.name}-${file.size}-${index}`)) })