@stackone/malachite 2.52.0 → 2.54.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/COMPONENTS.md CHANGED
@@ -12,7 +12,7 @@ The catalog of components exported from `@stackone/malachite`. **Reach for one o
12
12
  - **`SurfaceExpandableRow`** — A row that opens to reveal a body (used as `Surface.ExpandableRow`), for a line that carries its own controls. Not `Accordion`, which puts its header inside the toggle button, so a header holding a dropdown or a second button would be invalid: here the row surface takes the click and a cell opts out by wrapping its controls in `ExpandableRow.Controls`, while the arrow stays a real button so the keyboard reaches it. That opt-out is explicit rather than sniffing the event target for an interactive ancestor, because a control rendering into a shadow root retargets the event to its host. `sticky` pins the row while its body scrolls, offset by `stickyTopOffset` for whatever is already pinned above it; it only pins while open, since a closed row has nothing to scroll past. _Key props:_ `header`, `children`, `expanded`, `onToggle`, `sticky`, `stickyTopOffset`, `divider`, `tone`, `aria-label`, `className`, `style`.
13
13
  - **`DisclosureArrow`** — The expand/collapse triangle disclosure UI shares (`Accordion`, `Surface.ExpandableRow`). Owns its colour states so they never drift between usages: `text-primary` when expanded, `text-placeholder` otherwise, and dark on hover inside a `group` ancestor — put `group` on the clickable header or row so that hover state activates. _Key props:_ `expanded`, `size`, `className`.
14
14
  - **`Card`** — Generic content card.
15
- - **`SelectableCard`** — A card that is one option in a radio group — eyebrow, optional status tag, title, description, a count label and a top-right action slot. Use inside a `CardStrip`. Selection is controlled by the consumer: pass `selected`, which drives both the visual treatment and `aria-checked`. A selected card meets the sheet below the way a browser tab meets its toolbar — its bottom corners flare outward onto the strip’s rule — so its outline is one SVG path rather than borders, which would split into two lines at the tangent. `variant` picks the palette: `profile` (green), `library` (neutral) or `editing` (amber, for a profile with unsaved changes). `actions` stands in the card’s top-right corner, on the eyebrow row, and renders outside the control so a menu or icon button inside it stays clickable and keeps its own tab stop. Nothing else marks that corner: the flared outline, the tint and the green eyebrow already say which card is selected, so a radio there would only repeat them. `onDismiss` adds an x after `actions`, tinted like the eyebrow so it reads as part of the card — for discarding a draft card; `dismissDisabledReason` disables it and explains why in its tooltip. `isDefault` marks the card as the account default with a pin beside the title, named for assistive tech and tooltipped for everyone else — it belongs with the name it qualifies rather than among the actions, which read as things to press. `address` puts a one-line chip above the count for a copyable address such as a server URL: its `label` is cut short with an ellipsis, and its full `value` shows in a tooltip and is copied on click, without selecting the card. It takes the place of `description`, since the card has room for one or the other. For a row-shaped selectable use `Surface.Row` instead. _Key props:_ `value`, `title`, `titleIcon`, `isDefault`, `variant` (`profile` | `library` | `editing`), `eyebrow`, `status` (`label`, `type`), `description`, `address` (`label`, `value`), `countLabel`, `actions`, `onDismiss` (`dismissLabel`, `dismissDisabledReason`), `selected`, `loading` (`loadingLabel`, `loadingHint`), `disabled`, `dimmed`, `onSelect`, `tabIndex`, `aria-label`, `className`, `style`.
15
+ - **`SelectableCard`** — A card that is one option in a radio group — eyebrow, optional status tag, title, description, a count label and a top-right action slot. Use inside a `CardStrip`. Selection is controlled by the consumer: pass `selected`, which drives both the visual treatment and `aria-checked`. A selected card meets the sheet below the way a browser tab meets its toolbar — its bottom corners flare outward onto the strip’s rule — so its outline is one SVG path rather than borders, which would split into two lines at the tangent. `variant` picks the palette: `profile` (green), `library` (neutral) or `editing` (amber, for a profile with unsaved changes). `actions` stands in the card’s top-right corner, on the eyebrow row, and renders outside the control so a menu or icon button inside it stays clickable and keeps its own tab stop. Nothing else marks that corner: the flared outline, the tint and the green eyebrow already say which card is selected, so a radio there would only repeat them. `onDismiss` adds an x after `actions`, tinted like the eyebrow so it reads as part of the card — for discarding a draft card; `dismissDisabledReason` disables it and explains why in its tooltip. `isDefault` marks the card as the account default with a pin beside the title, named for assistive tech and tooltipped for everyone else — it belongs with the name it qualifies rather than among the actions, which read as things to press. `address` puts a one-line chip above the count for a copyable address such as a server URL: its `label` is cut short with an ellipsis, and its full `value` shows in a tooltip and is copied on click, without selecting the card. It takes the place of `description`, since the card has room for one or the other. For a row-shaped selectable use `Surface.Row` instead. _Key props:_ `value`, `title`, `titleIcon`, `isDefault`, `variant` (`profile` | `library` | `editing`), `eyebrow`, `status` (`label`, `type`, `tooltip`), `description`, `address` (`label`, `value`), `countLabel`, `actions`, `onDismiss` (`dismissLabel`, `dismissDisabledReason`), `selected`, `loading` (`loadingLabel`, `loadingHint`), `disabled`, `dimmed`, `onSelect`, `tabIndex`, `aria-label`, `className`, `style`.
16
16
  - **`CardStrip`** — Horizontally scrolling radio group of cards, with edge arrows that appear only when the content overflows, an edge fade, scroll-snap and arrow-key navigation matching `SegmentedControl`. Owns interaction only: it finds its options by looking for `role="radio"` descendants carrying `data-value`, so any such child works (`SelectableCard` emits both). Pass a non-selectable terminal tile as `CardStrip.AddTile` — it sits inside the group but outside the radio set, and must be a direct child or it scrolls with the cards. Use `leading` to pin a card that should not scroll away; the strip tells the cards which slot they are in, so a pinned `SelectableCard` drops the left rule the scrolling ones draw. A strip too narrow to show a card beside `leading` scrolls it with the cards rather than squeezing them, so the selected card always has its full width. Set `bleed` when the strip sits in a `PageLayout`: the strip stretches past the horizontal padding of the layout so its bottom rule runs the full width of the page card, while the cards stay aligned to the gutter. With no cards at all the strip degenerates to its pinned slots alone — no trough, no scroller and no arrows — with the add tile standing beside `leading` and taking `primary` emphasis automatically, since adding the first card is the only thing on offer. _Key props:_ `value`, `tone` (`profile` | `library` | `editing` — read off the selected card unless set), `onChange`, `children`, `leading`, `arrows`, `fadeEdges`, `snap`, `bleed`, `scrollSelectedIntoView`, `aria-label`, `aria-labelledby`, `className`, `style`. `CardStrip.AddTile`: `label`, `icon`, `onClick`, `disabled`, `emphasis` (`primary` | `secondary` — left to the strip unless set: `primary` while there are no cards, `secondary` once there are), `className`.
17
17
  - **`SplitScreenLayout`** — Two-pane full-screen layout (content + preview/aside) with an optional dismiss button.
18
18
  - **`PageLayout`** — Standard page chrome: a bordered card holding a breadcrumb bar, an optional alert strip, and a header block (subtitle, title + suffix, description) above the page body. Used as `PageLayout.Regular`; `PageLayout.Empty` is the chrome-only variant for pre-auth and error screens, with `headerActions` and `background` slots. Actions are declared as data (`buttons`) rather than children, and any that do not fit collapse into an overflow `ActionMenu` via `OverflowRow`. A client-side-routed app must pass `renderLink` so crumbs and the back button go through its router instead of reloading the document. Both layouts render a `main` landmark, demoted via `role` when the app owns its own. Deliberately not a `Surface` — the page card is 16px and carries no shadow. _Key props:_ `PageLayout.Regular`: `breadcrumbLinks`, `breadcrumbActions`, `renderLink`, `title`, `titleSuffix`, `subtitle`, `description`, `buttons`, `loading`, `centered`, `noPadding`, `contentGap` (`small` | `medium` | `large` | `xlarge`, default `small`), `alert`, `role`, `aria-label`, `aria-labelledby`, `className`, `style`. `PageLayout.Empty`: `title`, `centered`, `uniformPadding`, `headerActions`, `background`, `role`, `aria-label`, `aria-labelledby`, `className`, `style`.
@@ -38,7 +38,7 @@ The catalog of components exported from `@stackone/malachite`. **Reach for one o
38
38
  - **`IconIndicator`** — A non-interactive icon that carries a meaning, such as a capability shown on a page that cannot switch it on. Same square as an icon-only `Button` at each `size`, so it lines up where a sibling view shows the button. Renders `role="img"`, named by `aria-label`; with a `tooltip` it takes focus so the tooltip is reachable by keyboard. Reach for it instead of a hand-sized `<span>` around an icon. _Key props:_ `icon`, `aria-label`, `tooltip`, `size`.
39
39
  - **`StatusBadge`** — Large status indicator (e.g. success checkmark) for terminal/confirmation screens. _Key props:_ `type`.
40
40
  - **`StatBadge`** — Pill showing a numeric value with a label (e.g. "3 accounts selected"). _Key props:_ `count`, `label`.
41
- - **`Tag`** — Small status/label tag. Types: success, error, warning, default. _Key props:_ `value`, `type`, `paddingSize`, `textTransform`, `WidthSize`.
41
+ - **`Tag`** — Small status/label tag. Types: success, error, warning, default. _Key props:_ `value`, `type`, `paddingSize`, `textTransform`, `WidthSize`, `tooltip`, `tooltipDescribesTag`.
42
42
  - **`NumberTag`** — Compact numeric badge/counter.
43
43
  - **`UsageCard`** — A small metered card: a title (optionally a link via `titleLink`, drawn with a trailing arrow), a right-aligned `detail` such as "700/1,000", and a body, usually a `Progress`. Used by `Sidebar.Usage`, and fine on its own, e.g. on a billing page. _Key props:_ `title`, `titleLink`, `detail`, `children`, `aria-label` (required; names the group), `busy` (sets `aria-busy` only).
44
44
  - **`Progress`** — Progress bar.
@@ -88,6 +88,7 @@ The catalog of components exported from `@stackone/malachite`. **Reach for one o
88
88
  - **`Toggle`** — On/off switch. Add `label` for a visible caption, and `variant` to give it standalone toolbar-filter chrome — pick the family it sits beside: `pill` matches the filter-bar inputs (`Search`, `MultiSelect`), `button` renders the whole control as an outline `Button` with the switch as its trailing icon. `bordered` is shorthand for `variant="pill"`. `tone="pending"` paints the checked track amber, for a switch whose change has not been written yet — pair it with a `SelectionBar` reporting the same diff. The unchecked track never changes: nothing is pending about a switch that is off. Forwards a ref to its root element, so it can anchor an `InlineConfirm`. _Key props:_ `checked`, `onChange`, `disabled`, `ToggleSize` (`small` | `medium`), `ToggleTone` (`default` | `pending`), `label`, `ToggleVariant` (`pill` | `button`), `bordered`, `aria-label`, `aria-labelledby`, `aria-describedby`, `id`.
89
89
  - **`Dropdown`** — Single-select dropdown. `DropdownSize` is `small` (28px) | `medium` (32px) | `large` (42px), matching Button; `fullWidth` fills the container instead of sizing to content, which is what a table cell or a toolbar slot needs — without it the field column sizes to its content, so a consumer has to compute a width; `width` fixes that width outright, the way `MultiSelect` does, for a toolbar lining the two up rather than letting each hug its own label — a narrower container still wins, since the width clamps to it; Until this release a `search` trigger could not go below the intrinsic width of its bare `input`, so a `width` under roughly 200px was ignored; it now shrinks with the rest. The open menu fills the trigger and left-aligns under it, as Figma draws every tier of `Dropdown Component - Standard`, so `width` sizes both — the menu is the width of the control that opened it and does not widen for a long option, which ellipsises instead, so a row of controls and their open menus stay on one set of edges; below 160px the menu keeps that floor rather than shrinking to a chip nobody can read, and `minWidth` raises that floor without widening the trigger — for a picker a column pins narrow whose options carry a subtitle the trigger never shows; `menuAlign="end"` lines a menu wider than its trigger up with the trigger's right edge instead, so a control at the right of a card opens its menu inwards rather than past the card; near a viewport edge it anchors to the trigger's other side before falling back to the viewport, so the two share an edge wherever one fits; `small` is the chip that sits beside a page title and rounds tighter (10px) than the taller sizes. These names each moved up a tier in 0.71 — the old `xsmall`/`small`/`medium` are now `small`/`medium`/`large`. Pass `value` for a controlled selection whose source of truth lives elsewhere (URL or app state): the trigger follows it without the consumer remounting via `key`, which used to discard focus mid-interaction. `defaultValue` keeps the uncontrolled behaviour. `clearable` adds a clear control to the trigger once something is selected and reports it as `onItemSelected(undefined)` — without it a selection cannot be undone, so filter bars had to invent a sentinel "any" item to carry that meaning. `requiredIndicator` switches the label between the library default of naming the optional fields, an `asterisk` on the required ones for designs drawn that way, and `none` for a field whose value is supplied rather than asked for — a locked control is still required, but a marker only sends the reader looking for something to fill in. Visual only: `aria-required` is set from `required` whichever is chosen. `showSubtitleInTrigger` promotes the selected item's `subtitle` into the closed trigger, on one line under the value, so the consequence of a choice stays on screen once the menu shuts — the trigger trades its fixed height for a floor and grows by that line, and either line too long for it ellipsises, with a single tooltip carrying whichever lines are cut off, value first. It does not combine with `search`, whose trigger is a single-line text input with nowhere to put a second line, so the type rejects the pair rather than letting a consumer ask for a subtitle and get none. `subtitleLayout` decides how a subtitle sits in the open menu: the default `inline` keeps it on the label's row after a divider, which suits a short qualifier, while `stacked` gives it its own line beneath the label — one line either way, ellipsised rather than wrapped, so inline a sentence-length subtitle is cut back to a few words. Every option row carries a tooltip for whatever it had to cut, across all three of label, subtitle and `rightSideText`, so nothing the row hides is unreadable. Passing `formControl` is enough to show validation on the same terms as `Input`, though only validation: the trigger still paints `value` or `defaultValue`, never the value the form holds. The resolver's message renders on its own line beneath the field in the destructive colour, politely announced, with `aria-invalid` and `aria-describedby` wired, while an explicit `helperText` keeps its own line above it and stays dimmed, and `error={false}` suppresses the resolver outright. An item's `subtitle` renders inline markdown in the option, trigger and tooltip, with links drawn but inert, since the option is itself the click target; a subtitle with no markdown syntax stays a plain text node. An item's `badge` (e.g. a `Tag`) sits at the right end of its option in the open menu, after any `rightSideText`, and never shows in the closed trigger or the search input; it does not shrink, and the option is still named by its label alone. _Key props:_ `DropdownItem[]` (items take `badge`), `DropdownSize`, `fullWidth`, `width`, `minWidth`, `menuAlign` (`start` | `end`), `value`, `defaultValue`, `label`, `search`, `showSubtitleInTrigger`, `subtitleLayout`, `clearable`, `required`, `requiredIndicator`, `error`, `helperText`, `loading`, `formControl`, `aria-label`, `aria-labelledby`, `id`.
90
90
  - **`MultiSelect`** — Multi-select dropdown. `width` fixes the trigger, as `Dropdown` and `Search` take it, so a toolbar can put its controls on one width — and, as with those two, a container narrower than the value still wins; the menu fills the trigger and left-aligns under it, so that one prop sizes both; `minWidth` raises the 160px floor a narrow trigger falls back to. An option label too long for the menu ellipsises, and hovering it shows the full label — above the description in `hover` mode — as `Dropdown` does for its options. `requiredIndicator` follows the same three-way convention the single-select controls do, so a form choosing asterisks does not get "(Optional)" on one of its fields. _Key props:_ `MultiSelectOption[]`, `MultiSelectSize`, `width`, `minWidth`, `label`, `searchable`, `inlineSearch`, `chips`, `chipsRemovable`, `required`, `requiredIndicator`, `aria-label`, `aria-labelledby`, `id`.
91
+ - **`FilterSearch`** — One search field that filters across several categories. Clicking lists the filters, each with its description and an example; picking one lists its values behind a `Filters > Name` breadcrumb, and typing before picking searches the values of every filter at once, tagging each match with the filter it belongs to. Select All / Deselect All act on whatever the query matches. Each filter becomes a chip (`Connector: Notion`, or `Connector: (Notion, Attio)`) that reopens its values when clicked; Backspace in an empty field removes the last one. Controlled: `onChange` gets the next selection and the `changes` that produced it, an empty `items` meaning the filter was removed, so a URL-backed selection writes back only what moved. Chips keep the order filters were added in even when `value` comes back reordered, and keep their labels while `filters` reloads. _Key props:_ `filters`, `value`, `onChange`, `onClear`, `onActiveFilterChange`, `placeholder`, `size`, `width`, `loading`, `disabled`, `clearable`, `maxVisibleChips`, `aria-label`, `aria-labelledby`, `id`.
91
92
  - **`QueryInput`** — A textbox with inline syntax colouring and a suggestion menu anchored to the caret. It carries no grammar of its own: the consumer parses the value, hands back `tokens` to colour and `suggestions` to offer, so the vocabulary lives in the consuming app rather than here. _Key props:_ `value`, `onChange`, `tokens`, `suggestions`, `activeRange`, `onAccept`, `onQueryContextChange`, `onSubmit`, `placeholder`, `size`, `disabled`, `loading`, `emptyMessage`, `className`, `style`, `aria-label`, `aria-labelledby`.
92
93
  - **`SettingRow`** — One setting: a labelled row with its control on the trailing edge, and the sentence that explains it underneath. `control` may be a render prop receiving the label’s id, so a control that no `<label htmlFor>` can reach — `Toggle` renders a `<button role="switch">` — still names itself via `aria-labelledby`. _Key props:_ `label`, `badge`, `description`, `control`, `className`.
93
94
 
@@ -0,0 +1,572 @@
1
+ import { jsxs as f, Fragment as B, jsx as l } from "react/jsx-runtime";
2
+ import { useId as je, useState as F, useRef as E, useMemo as U, useEffect as X, useCallback as q, useLayoutEffect as we, useInsertionEffect as Ue } from "react";
3
+ import { createPortal as Xe } from "react-dom";
4
+ import qe from "./CustomIcons/CaretDownIcon.js";
5
+ import Be from "./CustomIcons/CheckmarkIcon.js";
6
+ import ke from "./CustomIcons/DismissIcon.js";
7
+ import Ve from "./CustomIcons/SearchIcon.js";
8
+ import Ze from "./Spinner.js";
9
+ import { CONTROL_TEXT_SIZE_CLASS as J, CONTROL_TEXT_SIZE as Ke } from "./controlTextSize.js";
10
+ import { controlWidthStyle as Qe } from "./controlWidth.js";
11
+ import { getTriggerVisual as Ye, DROPDOWN_TRIGGER_ATTR as Je } from "./dropdownTriggerStyle.js";
12
+ import { pushEscapeLayer as et } from "./escapeLayers.js";
13
+ import { orderSelections as tt, resolveMode as rt, filterRows as Se, searchRows as nt, chipText as it, removeFilter as ot, clearAll as lt, selectAll as at, deselectAll as st, toggleValue as ct } from "./filterSearchModel.js";
14
+ import { ensureFilterSearchStyles as dt, FILTER_SEARCH_CLASS as I } from "./filterSearchStyles.js";
15
+ import { measureMenuGeometry as pt, MENU_MAX_HEIGHT_PX as ut, MENU_SPECS as O } from "./floatingMenuGeometry.js";
16
+ import { FOCUS_RING as ee } from "./focusRing.js";
17
+ import { usePopupLayer as mt } from "./overlayLayers.js";
18
+ import { useTopLayer as ft } from "./topLayer.js";
19
+ import { useLatestRef as ht } from "./useLatestRef.js";
20
+ const gt = {
21
+ small: `min-h-[var(--malachite-control-height-small,28px)] gap-1 px-2.5 py-[2px] ${J.small} rounded-[var(--malachite-radius-10,10px)]`,
22
+ medium: `min-h-[var(--malachite-control-height-medium,32px)] gap-1 px-3 py-[3px] ${J.medium} rounded-xl`,
23
+ large: `min-h-[var(--malachite-control-height-large,42px)] gap-1.5 px-4 py-[6px] ${J.large} rounded-[var(--malachite-radius-xxl,14px)]`
24
+ }, xt = {
25
+ small: "gap-1",
26
+ medium: "gap-1",
27
+ large: "gap-1.5"
28
+ }, te = {
29
+ resting: "bg-[var(--malachite-tag-default-background,rgba(123,123,123,0.1))] text-[var(--malachite-text-primary,rgb(10,10,10))]",
30
+ active: "bg-[var(--malachite-accent-background,rgb(0,175,102))] text-[var(--malachite-card-background,rgb(255,255,255))]"
31
+ }, bt = 5, re = (i) => !!i && !(i.kind === "item" && i.row.item.disabled), vt = (i, s) => {
32
+ for (let g = Math.max(s, 0); g < i.length; g++)
33
+ if (re(i[g])) return g;
34
+ return -1;
35
+ }, yt = {
36
+ display: "flex",
37
+ alignItems: "center",
38
+ gap: 8,
39
+ width: "100%",
40
+ flexShrink: 0,
41
+ border: "none",
42
+ textAlign: "left",
43
+ borderRadius: 8,
44
+ padding: "6px 10px",
45
+ font: "inherit",
46
+ cursor: "pointer",
47
+ minWidth: 0
48
+ }, Ee = {
49
+ borderRadius: 6,
50
+ padding: "2px 6px",
51
+ flexShrink: 0,
52
+ whiteSpace: "nowrap"
53
+ }, V = {
54
+ overflow: "hidden",
55
+ textOverflow: "ellipsis",
56
+ whiteSpace: "nowrap",
57
+ minWidth: 0
58
+ }, Re = { ...V, marginLeft: "auto", flexShrink: 1 }, Ce = (i) => i.preventDefault(), wt = (i) => {
59
+ i.preventDefault(), i.stopPropagation();
60
+ }, kt = ({ row: i, showFilter: s }) => {
61
+ if (i.kind === "filter")
62
+ return /* @__PURE__ */ f(B, { children: [
63
+ /* @__PURE__ */ l("span", { className: I.pill, style: Ee, children: i.filter.label }),
64
+ i.filter.info && /* @__PURE__ */ l("span", { style: V, children: i.filter.info }),
65
+ i.filter.example && /* @__PURE__ */ l("span", { className: I.hint, style: Re, children: i.filter.example })
66
+ ] });
67
+ if (i.kind === "toggle")
68
+ return /* @__PURE__ */ l("span", { style: { color: "var(--malachite-text-primary, rgb(10, 10, 10))" }, children: i.toggle === "select" ? "Select All" : "Deselect All" });
69
+ const { item: g, filter: R, selected: Z } = i.row;
70
+ return /* @__PURE__ */ f(B, { children: [
71
+ s && /* @__PURE__ */ l("span", { className: I.pill, style: Ee, children: R.label }),
72
+ /* @__PURE__ */ l("span", { style: V, children: g.label }),
73
+ Z ? /* @__PURE__ */ l(
74
+ Be,
75
+ {
76
+ size: 16,
77
+ style: {
78
+ marginLeft: "auto",
79
+ flexShrink: 0,
80
+ color: "var(--malachite-accent-background, rgb(0, 175, 102))"
81
+ }
82
+ }
83
+ ) : s && R.info && /* @__PURE__ */ l("span", { className: I.hint, style: Re, children: R.info })
84
+ ] });
85
+ }, Gt = ({
86
+ filters: i,
87
+ value: s,
88
+ onChange: g,
89
+ onClear: R,
90
+ onActiveFilterChange: Z,
91
+ placeholder: K = "Search",
92
+ size: Q = "medium",
93
+ width: Ne,
94
+ loading: p = !1,
95
+ disabled: u = !1,
96
+ clearable: Ie = !1,
97
+ maxVisibleChips: De = 4,
98
+ className: Ae = "",
99
+ style: Le,
100
+ id: Te,
101
+ "aria-label": ne,
102
+ "aria-labelledby": ie
103
+ }) => {
104
+ var ye;
105
+ const oe = je(), le = `${oe}-listbox`, ae = (e) => `${oe}-option-${e}`, [a, Y] = F(!1), [x, y] = F(""), [c, $e] = F(null), [se, m] = F(0), [d, ce] = F(null), D = E(null), T = E(null), w = E(null), H = E(null), de = E([]);
106
+ ft(w, a);
107
+ const Me = mt("multiselect", a, w), pe = E(/* @__PURE__ */ new Map()), $ = U(() => {
108
+ var t;
109
+ const e = new Map(pe.current);
110
+ for (const r of i) {
111
+ const n = ((t = e.get(r.value)) == null ? void 0 : t.items) ?? /* @__PURE__ */ new Map();
112
+ e.set(r.value, {
113
+ label: r.label,
114
+ items: new Map([
115
+ ...n,
116
+ ...r.items.map((o) => [o.value, o.label])
117
+ ])
118
+ });
119
+ }
120
+ return e;
121
+ }, [i]);
122
+ X(() => {
123
+ pe.current = $;
124
+ }, [$]);
125
+ const ue = E([]), M = U(
126
+ () => tt(s, ue.current, c),
127
+ [s, c]
128
+ );
129
+ X(() => {
130
+ ue.current = M;
131
+ }, [M]);
132
+ const h = rt(c, x), C = i.find((e) => e.value === c), me = E(c), P = ht(Z), k = q(
133
+ (e) => {
134
+ var t;
135
+ me.current !== e && (me.current = e, $e(e), (t = P.current) == null || t.call(P, e));
136
+ },
137
+ [P]
138
+ ), b = U(() => {
139
+ if (p) return [];
140
+ if (h === "filters") return i.map((r) => ({ kind: "filter", filter: r }));
141
+ const { toggle: e, items: t } = h === "items" ? Se(C, s, x) : nt(i, s, x);
142
+ return [
143
+ ...e ? [{ kind: "toggle", toggle: e }] : [],
144
+ ...t.map((r) => ({ kind: "item", row: r }))
145
+ ];
146
+ }, [p, h, i, C, s, x]), z = U(() => {
147
+ const e = new Map(s.map((n) => [n.filter, n.items])), t = (n) => {
148
+ const o = $.get(n), v = e.get(n);
149
+ if (!v)
150
+ return n === c ? [{ filter: n, text: (o == null ? void 0 : o.label) ?? n }] : [];
151
+ if (!o && p) return [];
152
+ const N = v.map((S) => (o == null ? void 0 : o.items.get(S)) ?? S);
153
+ return [{ filter: n, text: it((o == null ? void 0 : o.label) ?? n, N) }];
154
+ }, r = c !== null && !M.includes(c);
155
+ return [...M, ...r ? [c] : []].flatMap(t);
156
+ }, [M, s, $, p, c]), fe = z.slice(0, De), he = z.length - fe.length, W = q(
157
+ ({ next: e, changes: t }) => {
158
+ t.length > 0 && g(e, t);
159
+ },
160
+ [g]
161
+ ), A = q(() => {
162
+ Y(!1), y(""), k(null), ce(null);
163
+ }, [k]), G = () => {
164
+ u || a || (m(0), Y(!0));
165
+ }, _ = q(() => {
166
+ const e = D.current;
167
+ e && ce(
168
+ pt(w.current, {
169
+ triggerRect: e.getBoundingClientRect(),
170
+ preset: "standard",
171
+ viewportWidth: window.innerWidth,
172
+ viewportHeight: window.innerHeight
173
+ })
174
+ );
175
+ }, []);
176
+ X(() => {
177
+ u && a && A();
178
+ }, [u, a, A]), X(() => {
179
+ if (!a) return;
180
+ const e = (o) => {
181
+ const v = o.composedPath(), N = D.current, S = w.current;
182
+ (!N || !v.includes(N)) && (!S || !v.includes(S)) && A();
183
+ };
184
+ let t = null;
185
+ const r = () => {
186
+ t === null && (t = requestAnimationFrame(() => {
187
+ t = null, _();
188
+ }));
189
+ }, n = et(A);
190
+ return document.addEventListener("mousedown", e), window.addEventListener("scroll", r, !0), window.addEventListener("resize", r), () => {
191
+ document.removeEventListener("mousedown", e), window.removeEventListener("scroll", r, !0), window.removeEventListener("resize", r), t !== null && cancelAnimationFrame(t), n();
192
+ };
193
+ }, [a, A, _]), we(() => {
194
+ if (!a || (_(), typeof ResizeObserver > "u")) return;
195
+ const e = new ResizeObserver(() => _());
196
+ return w.current && e.observe(w.current), D.current && e.observe(D.current), () => e.disconnect();
197
+ }, [a, _]), we(() => {
198
+ H.current && (H.current.scrollTop = 0);
199
+ }, [h, c, x]), Ue(() => {
200
+ a && dt();
201
+ }, [a]);
202
+ const ge = (e) => {
203
+ requestAnimationFrame(() => {
204
+ const t = de.current[e], r = H.current;
205
+ if (!t || !r) return;
206
+ const n = t.offsetTop, o = n + t.offsetHeight;
207
+ o > r.scrollTop + r.clientHeight ? r.scrollTop = o - r.clientHeight : n < r.scrollTop && (r.scrollTop = n);
208
+ });
209
+ }, xe = (e) => {
210
+ if (e.kind === "filter") {
211
+ k(e.filter.value), y(""), m(0);
212
+ return;
213
+ }
214
+ if (e.kind === "toggle") {
215
+ const j = h === "items" && C ? [C] : i;
216
+ W(
217
+ e.toggle === "select" ? at(j, s, x) : st(j, s, x)
218
+ ), y(""), m(0);
219
+ return;
220
+ }
221
+ const { item: t, filter: r } = e.row;
222
+ if (t.disabled) return;
223
+ const n = ct(s, r.value, t.value);
224
+ if (W(n), y(""), h !== "items") {
225
+ m(0);
226
+ return;
227
+ }
228
+ const { toggle: o, items: v } = Se(r, n.next, ""), N = v.findIndex((j) => j.item.value === t.value), S = N < 0 ? 0 : N + (o ? 1 : 0);
229
+ m(S), ge(S);
230
+ }, _e = (e) => {
231
+ if (b.length === 0) return;
232
+ let t = L < 0 ? se : L;
233
+ for (let r = 0; r < b.length; r++)
234
+ if (t = (t + e + b.length) % b.length, re(b[t])) {
235
+ m(t), ge(t);
236
+ return;
237
+ }
238
+ }, be = (e) => {
239
+ e === c && k(null), W(ot(s, e));
240
+ }, Fe = (e) => {
241
+ var t;
242
+ u || p || (k(e), y(""), m(0), Y(!0), (t = T.current) == null || t.focus());
243
+ }, Oe = (e) => {
244
+ if (!(e.nativeEvent.isComposing || e.nativeEvent.keyCode === 229))
245
+ switch (e.key) {
246
+ case "ArrowDown":
247
+ case "ArrowUp": {
248
+ if (e.preventDefault(), !a) {
249
+ G();
250
+ return;
251
+ }
252
+ _e(e.key === "ArrowDown" ? 1 : -1);
253
+ return;
254
+ }
255
+ case "Enter": {
256
+ if (e.preventDefault(), !a) {
257
+ G();
258
+ return;
259
+ }
260
+ const t = b[L];
261
+ re(t) && xe(t);
262
+ return;
263
+ }
264
+ case "Backspace": {
265
+ if (x !== "") return;
266
+ if (c !== null && !s.some((o) => o.filter === c)) {
267
+ e.preventDefault(), k(null), m(0);
268
+ return;
269
+ }
270
+ const r = z.filter((o) => s.some((v) => v.filter === o.filter)), n = r[r.length - 1];
271
+ if (!n) return;
272
+ e.preventDefault(), be(n.filter);
273
+ return;
274
+ }
275
+ }
276
+ }, He = (e) => {
277
+ e.stopPropagation(), W(lt(s)), k(null), y(""), R == null || R();
278
+ }, Pe = () => {
279
+ k(null), y(""), m(0);
280
+ }, ve = Ie && s.length > 0 && !u && !p, L = a ? vt(b, se) : -1, ze = Ye({ disabled: u, open: a }), We = h === "filters" ? "No filters available" : "No matching values", Ge = {
281
+ position: "fixed",
282
+ top: (d == null ? void 0 : d.top) ?? 0,
283
+ left: (d == null ? void 0 : d.left) ?? 0,
284
+ width: d == null ? void 0 : d.width,
285
+ background: O.standard.background,
286
+ border: O.standard.border,
287
+ borderRadius: O.standard.radius,
288
+ boxShadow: O.standard.shadow,
289
+ padding: O.standard.padding,
290
+ display: "flex",
291
+ flexDirection: "column",
292
+ gap: 4,
293
+ maxHeight: (d == null ? void 0 : d.maxHeight) ?? ut,
294
+ fontSize: Ke[Q],
295
+ visibility: d ? "visible" : "hidden",
296
+ opacity: d ? 1 : 0
297
+ };
298
+ return /* @__PURE__ */ f(B, { children: [
299
+ /* @__PURE__ */ f(
300
+ "div",
301
+ {
302
+ className: `relative min-w-0 ${Ae}`,
303
+ style: { ...Qe(Ne), ...Le },
304
+ children: [
305
+ /* @__PURE__ */ f(
306
+ "div",
307
+ {
308
+ ref: D,
309
+ [Je]: "",
310
+ "data-open": a || void 0,
311
+ onBlur: (e) => {
312
+ var r, n;
313
+ const t = e.relatedTarget;
314
+ t && ((r = D.current) != null && r.contains(t) || (n = w.current) != null && n.contains(t)) || A();
315
+ },
316
+ onMouseDown: (e) => {
317
+ var t;
318
+ u || (e.target !== T.current && e.preventDefault(), (t = T.current) == null || t.focus(), G());
319
+ },
320
+ style: {
321
+ transition: "border-color 0.15s ease, box-shadow 0.15s ease",
322
+ ...ze
323
+ },
324
+ className: `w-full min-w-0 flex items-center border border-solid font-normal ${gt[Q]} ${u ? "cursor-not-allowed" : "cursor-text"}`,
325
+ children: [
326
+ /* @__PURE__ */ l(
327
+ Ve,
328
+ {
329
+ size: 13,
330
+ className: "shrink-0 mr-1",
331
+ style: { color: "var(--malachite-text-placeholder, rgb(187, 187, 187))" }
332
+ }
333
+ ),
334
+ /* @__PURE__ */ f(
335
+ "div",
336
+ {
337
+ className: `flex flex-1 min-w-0 flex-wrap items-center ${xt[Q]} ${ve ? "pr-[18px]" : ""}`,
338
+ children: [
339
+ fe.map((e) => {
340
+ const t = a && e.filter === c;
341
+ return /* @__PURE__ */ f(
342
+ "span",
343
+ {
344
+ onMouseDown: wt,
345
+ className: `inline-flex items-center gap-1 max-w-[max(60px,calc(100%-64px))] min-w-0 h-[22px] pl-1.5 pr-1 rounded-md text-xs ${t ? te.active : te.resting}`,
346
+ children: [
347
+ /* @__PURE__ */ l(
348
+ "button",
349
+ {
350
+ type: "button",
351
+ disabled: u,
352
+ title: e.text,
353
+ "aria-label": `Edit filter ${e.text}`,
354
+ onClick: () => Fe(e.filter),
355
+ className: `truncate min-w-0 bg-transparent border-none p-0 text-[length:inherit] text-current ${u || p ? "cursor-default" : "cursor-pointer"} ${ee}`,
356
+ children: e.text
357
+ }
358
+ ),
359
+ !u && /* @__PURE__ */ l(
360
+ "button",
361
+ {
362
+ type: "button",
363
+ "aria-label": `Remove filter ${e.text}`,
364
+ onClick: (r) => {
365
+ var n;
366
+ r.stopPropagation(), be(e.filter), (n = T.current) == null || n.focus();
367
+ },
368
+ className: `flex items-center justify-center shrink-0 bg-transparent border-none p-0 text-current opacity-70 hover:opacity-100 cursor-pointer ${ee}`,
369
+ children: /* @__PURE__ */ l(ke, { size: 10 })
370
+ }
371
+ )
372
+ ]
373
+ },
374
+ e.filter
375
+ );
376
+ }),
377
+ he > 0 && /* @__PURE__ */ f(
378
+ "span",
379
+ {
380
+ className: `inline-flex items-center h-[22px] px-1.5 rounded-md text-xs ${te.resting}`,
381
+ children: [
382
+ "+",
383
+ he
384
+ ]
385
+ }
386
+ ),
387
+ /* @__PURE__ */ l(
388
+ "input",
389
+ {
390
+ ref: T,
391
+ id: Te,
392
+ type: "text",
393
+ role: "combobox",
394
+ value: x,
395
+ disabled: u,
396
+ placeholder: z.length === 0 ? K : "",
397
+ autoComplete: "off",
398
+ spellCheck: !1,
399
+ "aria-label": ie ? void 0 : ne ?? K,
400
+ "aria-labelledby": ie,
401
+ "aria-haspopup": "listbox",
402
+ "aria-autocomplete": "list",
403
+ "aria-expanded": a,
404
+ "aria-controls": a ? le : void 0,
405
+ "aria-activedescendant": L >= 0 ? ae(L) : void 0,
406
+ "aria-busy": p || void 0,
407
+ onChange: (e) => {
408
+ y(e.target.value), m(0), G();
409
+ },
410
+ onKeyDown: Oe,
411
+ className: "flex-1 min-w-[60px] border-none outline-none bg-transparent p-0 text-[length:inherit] text-[var(--malachite-text-primary,rgb(10,10,10))] placeholder:text-[var(--malachite-text-placeholder,rgb(187,187,187))] disabled:cursor-not-allowed"
412
+ }
413
+ )
414
+ ]
415
+ }
416
+ ),
417
+ /* @__PURE__ */ l("span", { className: "flex items-center justify-center shrink-0 size-[14px] ml-1", children: p ? /* @__PURE__ */ l(Ze, { size: "xxxsmall" }) : /* @__PURE__ */ l(
418
+ qe,
419
+ {
420
+ size: 13,
421
+ style: {
422
+ color: a ? "var(--malachite-text-primary, rgb(10, 10, 10))" : "var(--malachite-text-placeholder, rgb(187, 187, 187))",
423
+ transition: "transform 0.2s ease, color 0.2s ease",
424
+ transform: a ? "rotate(180deg)" : "rotate(0deg)"
425
+ }
426
+ }
427
+ ) })
428
+ ]
429
+ }
430
+ ),
431
+ ve && /* @__PURE__ */ l(
432
+ "button",
433
+ {
434
+ type: "button",
435
+ "aria-label": "Clear filters",
436
+ onMouseDown: Ce,
437
+ onClick: He,
438
+ className: `absolute top-0 bottom-0 right-[30px] flex items-center justify-center px-[2px] bg-transparent border-none text-[var(--malachite-text-placeholder)] hover:text-[var(--malachite-text-primary)] cursor-pointer ${ee}`,
439
+ children: /* @__PURE__ */ l(ke, { size: 10 })
440
+ }
441
+ )
442
+ ]
443
+ }
444
+ ),
445
+ a && Xe(
446
+ /* @__PURE__ */ f(
447
+ "div",
448
+ {
449
+ ref: w,
450
+ ...Me,
451
+ style: Ge,
452
+ onMouseDown: Ce,
453
+ children: [
454
+ /* @__PURE__ */ l(
455
+ "div",
456
+ {
457
+ style: {
458
+ padding: "4px 10px 2px",
459
+ color: "var(--malachite-text-placeholder, rgb(187, 187, 187))",
460
+ display: "flex",
461
+ gap: 4,
462
+ minWidth: 0
463
+ },
464
+ children: h === "items" ? /* @__PURE__ */ f(B, { children: [
465
+ /* @__PURE__ */ l(
466
+ "button",
467
+ {
468
+ type: "button",
469
+ tabIndex: -1,
470
+ onClick: Pe,
471
+ className: I.crumb,
472
+ style: {
473
+ background: "none",
474
+ border: "none",
475
+ padding: 0,
476
+ font: "inherit",
477
+ cursor: "pointer"
478
+ },
479
+ children: "Filters"
480
+ }
481
+ ),
482
+ /* @__PURE__ */ l("span", { style: V, children: `> ${(C == null ? void 0 : C.label) ?? ((ye = $.get(c ?? "")) == null ? void 0 : ye.label) ?? ""}` })
483
+ ] }) : "Filters"
484
+ }
485
+ ),
486
+ /* @__PURE__ */ f(
487
+ "div",
488
+ {
489
+ ref: H,
490
+ id: le,
491
+ role: "listbox",
492
+ "aria-multiselectable": h !== "filters",
493
+ "aria-label": ne ?? K,
494
+ "aria-busy": p || void 0,
495
+ style: {
496
+ display: "flex",
497
+ flexDirection: "column",
498
+ gap: 2,
499
+ overflowY: "auto",
500
+ overscrollBehavior: "contain",
501
+ minHeight: 0,
502
+ flex: 1,
503
+ scrollbarWidth: "thin"
504
+ },
505
+ children: [
506
+ p && Array.from({ length: bt }, (e, t) => /* @__PURE__ */ l(
507
+ "div",
508
+ {
509
+ role: "presentation",
510
+ className: I.loading,
511
+ style: {
512
+ height: 24,
513
+ margin: "4px 10px",
514
+ borderRadius: 6,
515
+ background: "var(--malachite-tag-default-background, rgba(123, 123, 123, 0.1))"
516
+ }
517
+ },
518
+ t
519
+ )),
520
+ !p && b.length === 0 && /* @__PURE__ */ l(
521
+ "div",
522
+ {
523
+ role: "presentation",
524
+ style: {
525
+ padding: "6px 10px",
526
+ color: "var(--malachite-text-secondary, rgb(123, 123, 123))"
527
+ },
528
+ children: We
529
+ }
530
+ ),
531
+ b.map((e, t) => {
532
+ const r = e.kind === "filter" ? `filter:${e.filter.value}` : e.kind === "toggle" ? "toggle" : `item:${e.row.filter.value}:${e.row.item.value}`, n = e.kind === "item" && !!e.row.item.disabled;
533
+ return /* @__PURE__ */ l(
534
+ "button",
535
+ {
536
+ id: ae(t),
537
+ ref: (o) => {
538
+ de.current[t] = o;
539
+ },
540
+ type: "button",
541
+ tabIndex: -1,
542
+ role: "option",
543
+ "aria-selected": e.kind === "item" ? e.row.selected : !1,
544
+ "aria-disabled": n || void 0,
545
+ "data-highlighted": t === L,
546
+ "data-disabled": n,
547
+ className: I.row,
548
+ onMouseEnter: () => {
549
+ n || m(t);
550
+ },
551
+ onClick: () => xe(e),
552
+ style: yt,
553
+ children: /* @__PURE__ */ l(kt, { row: e, showFilter: h === "search" })
554
+ },
555
+ r
556
+ );
557
+ })
558
+ ]
559
+ }
560
+ )
561
+ ]
562
+ }
563
+ ),
564
+ document.body
565
+ )
566
+ ] });
567
+ };
568
+ export {
569
+ Gt as FilterSearch,
570
+ Gt as default
571
+ };
572
+ //# sourceMappingURL=FilterSearch.js.map