@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 +3 -2
- package/dist/components/FilterSearch.js +572 -0
- package/dist/components/FilterSearch.js.map +1 -0
- package/dist/components/SelectableCard.js +198 -194
- package/dist/components/SelectableCard.js.map +1 -1
- package/dist/components/Tag.js +77 -63
- package/dist/components/Tag.js.map +1 -1
- package/dist/components/filterSearchModel.js +76 -0
- package/dist/components/filterSearchModel.js.map +1 -0
- package/dist/components/filterSearchStyles.js +28 -0
- package/dist/components/filterSearchStyles.js.map +1 -0
- package/dist/components/floatingMenuGeometry.js +1 -1
- package/dist/components/floatingMenuGeometry.js.map +1 -1
- package/dist/index.d.ts +74 -0
- package/dist/index.js +407 -405
- package/dist/index.js.map +1 -1
- package/dist/tailwind.css.js +1 -1
- package/package.json +1 -1
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"filterSearchModel.js","sources":["../../src/components/filterSearchModel.ts"],"sourcesContent":["export interface FilterSearchItem {\n value: string;\n label: string;\n disabled?: boolean;\n}\n\nexport interface FilterSearchFilter {\n value: string;\n label: string;\n /** Shown beside the filter's name in the menu, and beside its values in a cross-filter search. */\n info?: string;\n /** Right-aligned hint on the filter's row, e.g. `e.g. Attio, Ashby`. */\n example?: string;\n items: FilterSearchItem[];\n}\n\nexport interface FilterSearchSelection {\n filter: string;\n items: string[];\n}\n\n/** One filter's new values. An empty `items` means the filter was removed. */\nexport interface FilterSearchChange {\n filter: string;\n items: string[];\n}\n\nexport interface FilterSearchUpdate {\n next: FilterSearchSelection[];\n changes: FilterSearchChange[];\n}\n\nexport type FilterSearchMode = 'filters' | 'items' | 'search';\n\nexport type FilterSearchToggle = 'select' | 'deselect';\n\nexport interface FilterSearchItemRow {\n item: FilterSearchItem;\n filter: FilterSearchFilter;\n selected: boolean;\n}\n\nexport interface FilterSearchRows {\n /** The Select All / Deselect All row, absent when no enabled value matches. */\n toggle: FilterSearchToggle | null;\n items: FilterSearchItemRow[];\n}\n\nexport const resolveMode = (activeFilter: string | null, query: string): FilterSearchMode => {\n if (activeFilter !== null) return 'items';\n return query.trim().length > 0 ? 'search' : 'filters';\n};\n\nexport const matchesQuery = (item: FilterSearchItem, query: string): boolean => {\n const needle = query.trim().toLowerCase();\n return item.label.toLowerCase().includes(needle) || item.value.toLowerCase().includes(needle);\n};\n\nconst selectedValues = (selections: FilterSearchSelection[], filter: string): Set<string> =>\n new Set(selections.find((selection) => selection.filter === filter)?.items ?? []);\n\nconst toRows = (\n filters: FilterSearchFilter[],\n selections: FilterSearchSelection[],\n): FilterSearchItemRow[] =>\n filters.flatMap((filter) => {\n const selected = selectedValues(selections, filter.value);\n return filter.items.map((item) => ({ item, filter, selected: selected.has(item.value) }));\n });\n\n/**\n * \"Deselect All\" replaces \"Select All\" once every enabled value is chosen, or once every value\n * the query matches is.\n */\nconst toggleFor = (all: FilterSearchItemRow[], query: string): FilterSearchToggle | null => {\n const matched = all.filter((row) => matchesQuery(row.item, query));\n if (!matched.some((row) => !row.item.disabled)) return null;\n\n const everyEnabledSelected = all.every((row) => row.item.disabled || row.selected);\n const everyMatchSelected =\n query.trim().length > 0 &&\n matched.filter((row) => !row.item.disabled).every((row) => row.selected);\n\n return everyEnabledSelected || everyMatchSelected ? 'deselect' : 'select';\n};\n\nconst rowsFor = (all: FilterSearchItemRow[], query: string): FilterSearchRows => ({\n toggle: toggleFor(all, query),\n items: all.filter((row) => matchesQuery(row.item, query)),\n});\n\n/** The values of one filter, as the menu lists them once that filter is picked. */\nexport const filterRows = (\n filter: FilterSearchFilter | undefined,\n selections: FilterSearchSelection[],\n query: string,\n): FilterSearchRows =>\n filter ? rowsFor(toRows([filter], selections), query) : { toggle: null, items: [] };\n\n/** The values of every filter that match a query typed before any filter was picked. */\nexport const searchRows = (\n filters: FilterSearchFilter[],\n selections: FilterSearchSelection[],\n query: string,\n): FilterSearchRows => rowsFor(toRows(filters, selections), query);\n\n// As sets: a Select All that adds nothing returns the values in item order, not the order chosen.\nconst sameItems = (a: string[], b: string[]) => {\n const set = new Set(b);\n return a.length === set.size && a.every((value) => set.has(value));\n};\n\n/** Applies per-filter changes, keeping each filter where it already was and dropping the emptied. */\nconst applyChanges = (\n selections: FilterSearchSelection[],\n proposed: FilterSearchChange[],\n): FilterSearchUpdate => {\n const changes = proposed.filter(\n (change) => !sameItems(change.items, [...selectedValues(selections, change.filter)]),\n );\n const byFilter = new Map(changes.map((change) => [change.filter, change.items]));\n const kept = selections\n .map((selection) => ({\n filter: selection.filter,\n items: byFilter.get(selection.filter) ?? selection.items,\n }))\n .filter((selection) => selection.items.length > 0);\n const added = changes.filter(\n (change) =>\n change.items.length > 0 &&\n !selections.some((selection) => selection.filter === change.filter),\n );\n\n return { next: [...kept, ...added], changes };\n};\n\nexport const toggleValue = (\n selections: FilterSearchSelection[],\n filter: string,\n value: string,\n): FilterSearchUpdate => {\n const current = [...selectedValues(selections, filter)];\n const items = current.includes(value)\n ? current.filter((item) => item !== value)\n : [...current, value];\n\n return applyChanges(selections, [{ filter, items }]);\n};\n\n/** Select All keeps what is already chosen and adds every enabled value the query matches. */\nexport const selectAll = (\n filters: FilterSearchFilter[],\n selections: FilterSearchSelection[],\n query: string,\n): FilterSearchUpdate =>\n applyChanges(\n selections,\n filters.map((filter) => {\n const selected = selectedValues(selections, filter.value);\n const added = filter.items\n .filter(\n (item) =>\n !selected.has(item.value) && !item.disabled && matchesQuery(item, query),\n )\n .map((item) => item.value);\n // Chosen values stay even when `items` no longer lists them (a stale or lazy list).\n return { filter: filter.value, items: [...selected, ...added] };\n }),\n );\n\n/** Deselect All drops every enabled value the query matches and keeps the rest. */\nexport const deselectAll = (\n filters: FilterSearchFilter[],\n selections: FilterSearchSelection[],\n query: string,\n): FilterSearchUpdate =>\n applyChanges(\n selections,\n filters.map((filter) => {\n const selected = selectedValues(selections, filter.value);\n const dropped = new Set(\n filter.items\n .filter((item) => !item.disabled && matchesQuery(item, query))\n .map((item) => item.value),\n );\n return {\n filter: filter.value,\n items: [...selected].filter((value) => !dropped.has(value)),\n };\n }),\n );\n\nexport const removeFilter = (\n selections: FilterSearchSelection[],\n filter: string,\n): FilterSearchUpdate => applyChanges(selections, [{ filter, items: [] }]);\n\nexport const clearAll = (selections: FilterSearchSelection[]): FilterSearchUpdate =>\n applyChanges(\n selections,\n selections.map((selection) => ({ filter: selection.filter, items: [] })),\n );\n\n/** `Connector: Notion`, or `Connector: (Notion, Attio)` once there is more than one value. */\nexport const chipText = (filterLabel: string, itemLabels: string[]): string => {\n if (itemLabels.length === 0) return filterLabel;\n if (itemLabels.length === 1) return `${filterLabel}: ${itemLabels[0]}`;\n return `${filterLabel}: (${itemLabels.join(', ')})`;\n};\n\n/**\n * The order chips show filters in: when each was first added. A consumer reading its selection back\n * from the URL gets it in a fixed key order, which would reshuffle the chips on every change.\n */\nexport const orderSelections = (\n selections: FilterSearchSelection[],\n order: string[],\n /** A filter to hold its place in `order` while it has no values, e.g. the one being edited. */\n keep: string | null = null,\n): string[] => {\n const present = new Set(selections.map((selection) => selection.filter));\n const kept = order.filter((filter) => present.has(filter) || filter === keep);\n return [\n ...kept,\n ...selections.map((selection) => selection.filter).filter((f) => !kept.includes(f)),\n ];\n};\n"],"names":["resolveMode","activeFilter","query","matchesQuery","item","needle","selectedValues","selections","filter","_a","selection","toRows","filters","selected","toggleFor","all","matched","row","everyEnabledSelected","everyMatchSelected","rowsFor","filterRows","searchRows","sameItems","a","b","set","value","applyChanges","proposed","changes","change","byFilter","kept","added","toggleValue","current","items","selectAll","deselectAll","dropped","removeFilter","clearAll","chipText","filterLabel","itemLabels","orderSelections","order","keep","present","f"],"mappings":"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"}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { injectStyleSheet as n } from "./injectStyleSheet.js";
|
|
2
|
+
import { ensureOverlayStyles as s } from "./portalOverlay.js";
|
|
3
|
+
const e = ".malachite-filtersearch-row", t = ".malachite-filtersearch-pill", c = ".malachite-filtersearch-hint", o = ".malachite-filtersearch-loading", a = ".malachite-filtersearch-crumb", r = "var(--malachite-accent-foreground, rgb(2, 122, 66))", h = "var(--malachite-accent-background, rgb(0, 175, 102))", d = "var(--malachite-accent-background-glass, rgba(0, 175, 102, 0.08))", l = "var(--malachite-text-secondary, rgb(123, 123, 123))", i = "var(--malachite-text-placeholder, rgb(187, 187, 187))", m = "var(--malachite-tag-default-background, rgba(123, 123, 123, 0.1))", u = "var(--malachite-card-background, rgb(255, 255, 255))", g = [
|
|
4
|
+
`${e} { background: transparent; color: ${l}; }`,
|
|
5
|
+
`${e}[data-highlighted='true'] { background: ${d}; color: ${r}; }`,
|
|
6
|
+
`${e}[data-disabled='true'] { color: ${i}; cursor: not-allowed; }`,
|
|
7
|
+
`${t} { background: ${m}; color: ${l}; }`,
|
|
8
|
+
`${e}[data-highlighted='true'] ${t} { background: ${h}; color: ${u}; }`,
|
|
9
|
+
`${c} { color: ${i}; }`,
|
|
10
|
+
`${e}[data-highlighted='true'] ${c} { color: ${r}; }`,
|
|
11
|
+
`${a} { color: inherit; }`,
|
|
12
|
+
`${a}:hover { color: ${r}; }`,
|
|
13
|
+
`${a}:focus-visible { outline: 2px solid var(--malachite-text-primary, rgb(10, 10, 10)); outline-offset: 2px; }`,
|
|
14
|
+
"@keyframes malachite-filtersearch-pulse { 50% { opacity: 0.5; } }",
|
|
15
|
+
`${o} { animation: malachite-filtersearch-pulse 1.6s ease-in-out infinite; }`,
|
|
16
|
+
`@media (prefers-reduced-motion: reduce) { ${o} { animation: none; } }`
|
|
17
|
+
], f = {
|
|
18
|
+
row: e.slice(1),
|
|
19
|
+
pill: t.slice(1),
|
|
20
|
+
hint: c.slice(1),
|
|
21
|
+
loading: o.slice(1),
|
|
22
|
+
crumb: a.slice(1)
|
|
23
|
+
}, p = () => (s(), n("malachite-filtersearch-styles", g));
|
|
24
|
+
export {
|
|
25
|
+
f as FILTER_SEARCH_CLASS,
|
|
26
|
+
p as ensureFilterSearchStyles
|
|
27
|
+
};
|
|
28
|
+
//# sourceMappingURL=filterSearchStyles.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"filterSearchStyles.js","sources":["../../src/components/filterSearchStyles.ts"],"sourcesContent":["// The menu portals into document.body, outside the shadow root the Tailwind sheet is adopted into,\n// so its rows are styled here. Never inline a row's background: it would outrank these rules.\n\nimport { injectStyleSheet } from './injectStyleSheet';\nimport { ensureOverlayStyles } from './portalOverlay';\n\nconst ROW = '.malachite-filtersearch-row';\nconst PILL = '.malachite-filtersearch-pill';\nconst HINT = '.malachite-filtersearch-hint';\nconst LOADING = '.malachite-filtersearch-loading';\nconst CRUMB = '.malachite-filtersearch-crumb';\n\nconst ACCENT = 'var(--malachite-accent-foreground, rgb(2, 122, 66))';\nconst ACCENT_FILL = 'var(--malachite-accent-background, rgb(0, 175, 102))';\nconst ACCENT_GLASS = 'var(--malachite-accent-background-glass, rgba(0, 175, 102, 0.08))';\nconst SECONDARY = 'var(--malachite-text-secondary, rgb(123, 123, 123))';\nconst PLACEHOLDER = 'var(--malachite-text-placeholder, rgb(187, 187, 187))';\nconst PILL_BACKGROUND = 'var(--malachite-tag-default-background, rgba(123, 123, 123, 0.1))';\n// No palette entry exists for text on a solid accent fill; the card surface is its contrast.\nconst ON_ACCENT = 'var(--malachite-card-background, rgb(255, 255, 255))';\n\n// Highlight is the keyboard cursor, so it paints the accent tint the old antd menu did.\nconst RULES = [\n `${ROW} { background: transparent; color: ${SECONDARY}; }`,\n `${ROW}[data-highlighted='true'] { background: ${ACCENT_GLASS}; color: ${ACCENT}; }`,\n `${ROW}[data-disabled='true'] { color: ${PLACEHOLDER}; cursor: not-allowed; }`,\n `${PILL} { background: ${PILL_BACKGROUND}; color: ${SECONDARY}; }`,\n `${ROW}[data-highlighted='true'] ${PILL} { background: ${ACCENT_FILL}; color: ${ON_ACCENT}; }`,\n `${HINT} { color: ${PLACEHOLDER}; }`,\n `${ROW}[data-highlighted='true'] ${HINT} { color: ${ACCENT}; }`,\n `${CRUMB} { color: inherit; }`,\n `${CRUMB}:hover { color: ${ACCENT}; }`,\n `${CRUMB}:focus-visible { outline: 2px solid var(--malachite-text-primary, rgb(10, 10, 10)); outline-offset: 2px; }`,\n `@keyframes malachite-filtersearch-pulse { 50% { opacity: 0.5; } }`,\n `${LOADING} { animation: malachite-filtersearch-pulse 1.6s ease-in-out infinite; }`,\n `@media (prefers-reduced-motion: reduce) { ${LOADING} { animation: none; } }`,\n];\n\nexport const FILTER_SEARCH_CLASS = {\n row: ROW.slice(1),\n pill: PILL.slice(1),\n hint: HINT.slice(1),\n loading: LOADING.slice(1),\n crumb: CRUMB.slice(1),\n} as const;\n\nexport const ensureFilterSearchStyles = (): CSSStyleSheet | null => {\n ensureOverlayStyles();\n return injectStyleSheet('malachite-filtersearch-styles', RULES);\n};\n"],"names":["ROW","PILL","HINT","LOADING","CRUMB","ACCENT","ACCENT_FILL","ACCENT_GLASS","SECONDARY","PLACEHOLDER","PILL_BACKGROUND","ON_ACCENT","RULES","FILTER_SEARCH_CLASS","ensureFilterSearchStyles","ensureOverlayStyles","injectStyleSheet"],"mappings":";;AAMA,MAAMA,IAAM,+BACNC,IAAO,gCACPC,IAAO,gCACPC,IAAU,mCACVC,IAAQ,iCAERC,IAAS,uDACTC,IAAc,wDACdC,IAAe,qEACfC,IAAY,uDACZC,IAAc,yDACdC,IAAkB,qEAElBC,IAAY,wDAGZC,IAAQ;AAAA,EACV,GAAGZ,CAAG,sCAAsCQ,CAAS;AAAA,EACrD,GAAGR,CAAG,2CAA2CO,CAAY,YAAYF,CAAM;AAAA,EAC/E,GAAGL,CAAG,mCAAmCS,CAAW;AAAA,EACpD,GAAGR,CAAI,kBAAkBS,CAAe,YAAYF,CAAS;AAAA,EAC7D,GAAGR,CAAG,6BAA6BC,CAAI,kBAAkBK,CAAW,YAAYK,CAAS;AAAA,EACzF,GAAGT,CAAI,aAAaO,CAAW;AAAA,EAC/B,GAAGT,CAAG,6BAA6BE,CAAI,aAAaG,CAAM;AAAA,EAC1D,GAAGD,CAAK;AAAA,EACR,GAAGA,CAAK,mBAAmBC,CAAM;AAAA,EACjC,GAAGD,CAAK;AAAA,EACR;AAAA,EACA,GAAGD,CAAO;AAAA,EACV,6CAA6CA,CAAO;AACxD,GAEaU,IAAsB;AAAA,EAC/B,KAAKb,EAAI,MAAM,CAAC;AAAA,EAChB,MAAMC,EAAK,MAAM,CAAC;AAAA,EAClB,MAAMC,EAAK,MAAM,CAAC;AAAA,EAClB,SAASC,EAAQ,MAAM,CAAC;AAAA,EACxB,OAAOC,EAAM,MAAM,CAAC;AACxB,GAEaU,IAA2B,OACpCC,EAAA,GACOC,EAAiB,iCAAiCJ,CAAK;"}
|
|
@@ -50,7 +50,7 @@ const S = 8, y = 160, F = 160, $ = 180, g = {
|
|
|
50
50
|
}), o === 0)
|
|
51
51
|
return;
|
|
52
52
|
const e = getComputedStyle(t), s = (m) => Number.parseFloat(m) || 0, c = s(e.paddingLeft) + s(e.paddingRight) + s(e.borderLeftWidth) + s(e.borderRightWidth);
|
|
53
|
-
return Math.ceil(o + c);
|
|
53
|
+
return Math.ceil(o + 1 + c);
|
|
54
54
|
}, B = (t, n) => {
|
|
55
55
|
if (!t)
|
|
56
56
|
return O({ ...n, menuHeight: 0 });
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"floatingMenuGeometry.js","sources":["../../src/components/floatingMenuGeometry.ts"],"sourcesContent":["// Figma: `Dropdown Component - Item List` fills its trigger, 10px below — 444 (68:2789),\n// 200 (2343:2201), 414 (1437:2986). `Dropdown Component - Options List` (213:4320) is a\n// fixed 280px, 14px below. Figma declares no trigger width and no floor; 160 and 400 are ours.\n\nexport const MENU_VIEWPORT_PADDING_PX = 8;\n\nexport const SIDE_PANEL_MIN_HEIGHT_PX = 160;\n\nexport const MENU_MIN_WIDTH_PX = 160;\n\n// Figma draws the Options menu at a flat 280. Taken as a ceiling rather than a width: a kebab\n// of four short verbs then carries 130px of air, and every kebab in a consuming app is the same\n// width whatever it holds. 180 is the floor those menus sized themselves to before this preset.\nexport const MENU_OPTIONS_MIN_WIDTH_PX = 180;\n\nexport type MenuPreset = 'standard' | 'options';\n\nexport interface MenuSpec {\n /** `'trigger'` fills the control that opened it; `'content'` sizes to the widest item. */\n width: 'trigger' | 'content';\n /** Where this preset stops shrinking. */\n minWidth: number;\n /** Where a content-sized one stops growing. A caller's `minWidth` still raises past it. */\n maxWidth?: number;\n align: 'left' | 'right';\n gap: number;\n radius: number;\n padding: number;\n shadow: string;\n background: string;\n border: string;\n}\n\nexport const MENU_SPECS: Record<MenuPreset, MenuSpec> = {\n standard: {\n width: 'trigger',\n minWidth: MENU_MIN_WIDTH_PX,\n align: 'left',\n gap: 10,\n radius: 12,\n padding: 4,\n shadow: '0px 2px 5px 0px rgba(0, 0, 0, 0.1)',\n background: 'var(--malachite-card-background, rgb(255, 255, 255))',\n border: '1px solid var(--malachite-card-border, rgb(232, 232, 232))',\n },\n options: {\n width: 'content',\n minWidth: MENU_OPTIONS_MIN_WIDTH_PX,\n maxWidth: 280,\n align: 'right',\n gap: 14,\n radius: 12,\n padding: 8,\n shadow: '0px 4px 10px 0px rgba(0, 0, 0, 0.1)',\n background: 'var(--malachite-card-background, rgb(255, 255, 255))',\n border: '1px solid var(--malachite-card-border, rgb(232, 232, 232))',\n },\n};\n\nexport const MENU_MAX_HEIGHT_PX = 400;\n\nexport interface MenuGeometry {\n top: number;\n left: number;\n width: number;\n maxHeight: number;\n openUp: boolean;\n}\n\nexport interface MenuGeometryInput {\n triggerRect: Pick<DOMRect, 'top' | 'left' | 'right' | 'bottom' | 'width'>;\n preset: MenuPreset;\n minWidth?: number;\n /** Which trigger edge the menu prefers to share; defaults to the preset's. */\n align?: 'left' | 'right';\n /** The widest item, for a content-sized preset. Absent until the menu has been laid out. */\n contentWidth?: number;\n menuHeight: number;\n viewportWidth: number;\n viewportHeight: number;\n}\n\nexport const computeMenuGeometry = ({\n triggerRect,\n preset,\n minWidth,\n align: alignOverride,\n contentWidth,\n menuHeight,\n viewportWidth,\n viewportHeight,\n}: MenuGeometryInput): MenuGeometry => {\n const spec = MENU_SPECS[preset];\n const available = viewportWidth - MENU_VIEWPORT_PADDING_PX * 2;\n\n const floor = Math.max(spec.minWidth, minWidth ?? 0);\n const ceiling = Math.min(available, Math.max(spec.maxWidth ?? available, floor));\n const natural = spec.width === 'trigger' ? triggerRect.width : (contentWidth ?? ceiling);\n const width = Math.max(0, Math.min(Math.max(natural, floor), ceiling));\n\n const align = alignOverride ?? spec.align;\n const preferredLeft = align === 'right' ? triggerRect.right - width : triggerRect.left;\n const oppositeLeft = align === 'right' ? triggerRect.left : triggerRect.right - width;\n const minLeft = MENU_VIEWPORT_PADDING_PX;\n const maxLeft = viewportWidth - width - MENU_VIEWPORT_PADDING_PX;\n const fits = (candidate: number) => candidate >= minLeft && candidate <= maxLeft;\n\n let left: number;\n if (fits(preferredLeft)) {\n left = preferredLeft;\n } else if (fits(oppositeLeft)) {\n left = oppositeLeft;\n } else {\n left = Math.max(minLeft, Math.min(preferredLeft, maxLeft));\n }\n\n // Which side, then how much of it. \"More room above\" is not \"enough room above\", so the\n // height is clamped to whichever side won. Clamping `top` instead keeps the menu on screen\n // by painting it over its own trigger, which also swallows the click that closes it.\n const roomBelow = viewportHeight - triggerRect.bottom - spec.gap - MENU_VIEWPORT_PADDING_PX;\n const roomAbove = triggerRect.top - spec.gap - MENU_VIEWPORT_PADDING_PX;\n const openUp = menuHeight > 0 && menuHeight > roomBelow && roomAbove > roomBelow;\n\n const maxHeight = Math.max(0, Math.min(MENU_MAX_HEIGHT_PX, openUp ? roomAbove : roomBelow));\n const height = menuHeight > 0 ? Math.min(menuHeight, maxHeight) : 0;\n\n const top = Math.max(\n MENU_VIEWPORT_PADDING_PX,\n openUp ? triggerRect.top - spec.gap - height : triggerRect.bottom + spec.gap,\n );\n\n return { top, left, width, maxHeight, openUp };\n};\n\n// The height decides whether the menu opens upwards, and it is only meaningful at the width the\n// menu will actually have — so on the pass that first sizes the panel the width is written\n// before the height is read. The guard is what keeps that out of the steady state: every later\n// call finds the width already set and writes nothing, so a `ResizeObserver` callback does not\n// resize what it is observing (Chrome's \"undelivered notifications\" error) and a per-frame\n// reposition does not force a reflow. After the first pass this is a pure read.\n// A content-sized preset needs the width the panel chose for itself, which is only readable\n// while no width has been written to it — i.e. on the pass that opens the menu. The caller keeps\n// it from there, because after the first write `offsetWidth` reports what we wrote, not what the\n// items want.\nexport const measureContentWidth = (panel: HTMLElement | null): number | undefined => {\n if (!panel || panel.style.width !== '') {\n return undefined;\n }\n\n // The rows, not the panel. Each label carries `min-width: 0` and `overflow: hidden` so it\n // can ellipsise, which also means it contributes nothing to the panel's intrinsic width —\n // measuring the panel itself returns a box the size of its own padding. `scrollWidth` on a\n // clipped row is the width the row wanted.\n const rows = panel.querySelectorAll<HTMLElement>('button, [role=\"menuitem\"]');\n let widest = 0;\n rows.forEach((row) => {\n widest = Math.max(widest, row.scrollWidth);\n });\n\n if (widest === 0) {\n return undefined;\n }\n\n // The panel is border-box, so the width we hand back has to carry its own padding and rule.\n // Each read is defaulted: a computed style can come back empty — jsdom never expands the\n // `border` shorthand — and one NaN here would make the whole width NaN, which CSS drops,\n // leaving the panel unsized with no sign of why.\n const styles = getComputedStyle(panel);\n const edge = (value: string) => Number.parseFloat(value) || 0;\n const chrome =\n edge(styles.paddingLeft) +\n edge(styles.paddingRight) +\n edge(styles.borderLeftWidth) +\n edge(styles.borderRightWidth);\n\n return Math.ceil(widest + chrome);\n};\n\nexport const measureMenuGeometry = (\n panel: HTMLElement | null,\n input: Omit<MenuGeometryInput, 'menuHeight'>,\n): MenuGeometry => {\n if (!panel) {\n return computeMenuGeometry({ ...input, menuHeight: 0 });\n }\n\n const cssWidth = `${computeMenuGeometry({ ...input, menuHeight: 0 }).width}px`;\n if (panel.style.width !== cssWidth) {\n panel.style.width = cssWidth;\n }\n\n return computeMenuGeometry({ ...input, menuHeight: panel.offsetHeight });\n};\n\nexport const isTriggerOffscreen = (\n rect: Pick<DOMRect, 'top' | 'left' | 'right' | 'bottom'>,\n viewportWidth: number,\n viewportHeight: number,\n): boolean =>\n rect.bottom < 0 || rect.top > viewportHeight || rect.right < 0 || rect.left > viewportWidth;\n\nexport const rectSignature = (rect: Pick<DOMRect, 'top' | 'left' | 'width' | 'height'>): string =>\n `${rect.top},${rect.left},${rect.width},${rect.height}`;\n\nexport type FloatingPlacement =\n | 'bottom-start'\n | 'bottom-end'\n | 'top-start'\n | 'top-center'\n | 'right-start'\n | 'right-end'\n | 'left-start';\n\nconst alignedLeft = (\n align: 'start' | 'end' | 'center',\n triggerRect: Pick<DOMRect, 'left' | 'right'>,\n panelWidth: number,\n): number => {\n if (align === 'end') {\n return triggerRect.right - panelWidth;\n }\n if (align === 'center') {\n return (triggerRect.left + triggerRect.right - panelWidth) / 2;\n }\n return triggerRect.left;\n};\n\nexport const fullPanelHeight = (panel: HTMLElement): number =>\n panel.scrollHeight + panel.offsetHeight - panel.clientHeight;\n\nexport const FLOATING_OFFSET_PX = { small: 8, medium: 12, large: 14 } as const;\n\nexport type FloatingOffset = keyof typeof FLOATING_OFFSET_PX;\n\nexport interface FloatingGeometryInput {\n triggerRect: Pick<DOMRect, 'top' | 'left' | 'right' | 'bottom'>;\n placement: FloatingPlacement;\n panelWidth: number;\n panelHeight: number;\n gap: number;\n viewportWidth: number;\n viewportHeight: number;\n}\n\nexport interface FloatingGeometry {\n top: number;\n left: number;\n maxHeight: number;\n side: 'top' | 'bottom' | 'left' | 'right';\n}\n\nconst clampToViewport = (value: number, size: number, viewport: number): number =>\n Math.max(MENU_VIEWPORT_PADDING_PX, Math.min(value, viewport - size - MENU_VIEWPORT_PADDING_PX));\n\nexport const computeFloatingGeometry = ({\n triggerRect,\n placement,\n panelWidth,\n panelHeight,\n gap,\n viewportWidth,\n viewportHeight,\n}: FloatingGeometryInput): FloatingGeometry => {\n const [preferred, align] = placement.split('-') as [\n FloatingGeometry['side'],\n 'start' | 'end' | 'center',\n ];\n const pad = MENU_VIEWPORT_PADDING_PX;\n\n if (preferred === 'top' || preferred === 'bottom') {\n const roomBelow = viewportHeight - triggerRect.bottom - gap - pad;\n const roomAbove = triggerRect.top - gap - pad;\n const wanted = preferred === 'bottom' ? roomBelow : roomAbove;\n const opposite = preferred === 'bottom' ? roomAbove : roomBelow;\n const side: FloatingGeometry['side'] =\n panelHeight > wanted && opposite > wanted\n ? preferred === 'bottom'\n ? 'top'\n : 'bottom'\n : preferred;\n const belowTop = Math.max(pad, triggerRect.bottom + gap);\n const aboveBottom = Math.min(viewportHeight - pad, triggerRect.top - gap);\n const maxHeight = Math.max(\n 0,\n side === 'bottom' ? viewportHeight - belowTop - pad : aboveBottom - pad,\n );\n const height = Math.min(panelHeight, maxHeight);\n const top = side === 'bottom' ? belowTop : Math.max(pad, aboveBottom - height);\n const left = clampToViewport(\n alignedLeft(align, triggerRect, panelWidth),\n panelWidth,\n viewportWidth,\n );\n return { top, left, maxHeight, side };\n }\n\n const roomRight = viewportWidth - triggerRect.right - gap - pad;\n const roomLeft = triggerRect.left - gap - pad;\n const wanted = preferred === 'right' ? roomRight : roomLeft;\n const opposite = preferred === 'right' ? roomLeft : roomRight;\n const side: FloatingGeometry['side'] =\n panelWidth > wanted && opposite > wanted\n ? preferred === 'right'\n ? 'left'\n : 'right'\n : preferred;\n const left = side === 'right' ? triggerRect.right + gap : triggerRect.left - gap - panelWidth;\n const levelTop = Math.max(pad, triggerRect.top);\n const roomBelow = viewportHeight - levelTop - pad;\n if (align === 'start' && roomBelow >= Math.min(panelHeight, SIDE_PANEL_MIN_HEIGHT_PX)) {\n return {\n top: levelTop,\n left: clampToViewport(left, panelWidth, viewportWidth),\n maxHeight: roomBelow,\n side,\n };\n }\n const maxHeight = Math.max(0, viewportHeight - pad * 2);\n const height = Math.min(panelHeight, maxHeight);\n const top = clampToViewport(\n align === 'end' ? triggerRect.bottom - height : triggerRect.top,\n height,\n viewportHeight,\n );\n return { top, left: clampToViewport(left, panelWidth, viewportWidth), maxHeight, side };\n};\n"],"names":["MENU_VIEWPORT_PADDING_PX","SIDE_PANEL_MIN_HEIGHT_PX","MENU_MIN_WIDTH_PX","MENU_OPTIONS_MIN_WIDTH_PX","MENU_SPECS","MENU_MAX_HEIGHT_PX","computeMenuGeometry","triggerRect","preset","minWidth","alignOverride","contentWidth","menuHeight","viewportWidth","viewportHeight","spec","available","floor","ceiling","natural","width","align","preferredLeft","oppositeLeft","minLeft","maxLeft","fits","candidate","left","roomBelow","roomAbove","openUp","maxHeight","height","measureContentWidth","panel","rows","widest","row","styles","edge","value","chrome","measureMenuGeometry","input","cssWidth","isTriggerOffscreen","rect","rectSignature","alignedLeft","panelWidth","fullPanelHeight","FLOATING_OFFSET_PX","clampToViewport","size","viewport","computeFloatingGeometry","placement","panelHeight","gap","preferred","pad","wanted","opposite","side","belowTop","aboveBottom","top","roomRight","roomLeft","levelTop"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"floatingMenuGeometry.js","sources":["../../src/components/floatingMenuGeometry.ts"],"sourcesContent":["// Figma: `Dropdown Component - Item List` fills its trigger, 10px below — 444 (68:2789),\n// 200 (2343:2201), 414 (1437:2986). `Dropdown Component - Options List` (213:4320) is a\n// fixed 280px, 14px below. Figma declares no trigger width and no floor; 160 and 400 are ours.\n\nexport const MENU_VIEWPORT_PADDING_PX = 8;\n\nexport const SIDE_PANEL_MIN_HEIGHT_PX = 160;\n\nexport const MENU_MIN_WIDTH_PX = 160;\n\n// Figma draws the Options menu at a flat 280. Taken as a ceiling rather than a width: a kebab\n// of four short verbs then carries 130px of air, and every kebab in a consuming app is the same\n// width whatever it holds. 180 is the floor those menus sized themselves to before this preset.\nexport const MENU_OPTIONS_MIN_WIDTH_PX = 180;\n\nexport type MenuPreset = 'standard' | 'options';\n\nexport interface MenuSpec {\n /** `'trigger'` fills the control that opened it; `'content'` sizes to the widest item. */\n width: 'trigger' | 'content';\n /** Where this preset stops shrinking. */\n minWidth: number;\n /** Where a content-sized one stops growing. A caller's `minWidth` still raises past it. */\n maxWidth?: number;\n align: 'left' | 'right';\n gap: number;\n radius: number;\n padding: number;\n shadow: string;\n background: string;\n border: string;\n}\n\nexport const MENU_SPECS: Record<MenuPreset, MenuSpec> = {\n standard: {\n width: 'trigger',\n minWidth: MENU_MIN_WIDTH_PX,\n align: 'left',\n gap: 10,\n radius: 12,\n padding: 4,\n shadow: '0px 2px 5px 0px rgba(0, 0, 0, 0.1)',\n background: 'var(--malachite-card-background, rgb(255, 255, 255))',\n border: '1px solid var(--malachite-card-border, rgb(232, 232, 232))',\n },\n options: {\n width: 'content',\n minWidth: MENU_OPTIONS_MIN_WIDTH_PX,\n maxWidth: 280,\n align: 'right',\n gap: 14,\n radius: 12,\n padding: 8,\n shadow: '0px 4px 10px 0px rgba(0, 0, 0, 0.1)',\n background: 'var(--malachite-card-background, rgb(255, 255, 255))',\n border: '1px solid var(--malachite-card-border, rgb(232, 232, 232))',\n },\n};\n\nexport const MENU_MAX_HEIGHT_PX = 400;\n\nexport interface MenuGeometry {\n top: number;\n left: number;\n width: number;\n maxHeight: number;\n openUp: boolean;\n}\n\nexport interface MenuGeometryInput {\n triggerRect: Pick<DOMRect, 'top' | 'left' | 'right' | 'bottom' | 'width'>;\n preset: MenuPreset;\n minWidth?: number;\n /** Which trigger edge the menu prefers to share; defaults to the preset's. */\n align?: 'left' | 'right';\n /** The widest item, for a content-sized preset. Absent until the menu has been laid out. */\n contentWidth?: number;\n menuHeight: number;\n viewportWidth: number;\n viewportHeight: number;\n}\n\nexport const computeMenuGeometry = ({\n triggerRect,\n preset,\n minWidth,\n align: alignOverride,\n contentWidth,\n menuHeight,\n viewportWidth,\n viewportHeight,\n}: MenuGeometryInput): MenuGeometry => {\n const spec = MENU_SPECS[preset];\n const available = viewportWidth - MENU_VIEWPORT_PADDING_PX * 2;\n\n const floor = Math.max(spec.minWidth, minWidth ?? 0);\n const ceiling = Math.min(available, Math.max(spec.maxWidth ?? available, floor));\n const natural = spec.width === 'trigger' ? triggerRect.width : (contentWidth ?? ceiling);\n const width = Math.max(0, Math.min(Math.max(natural, floor), ceiling));\n\n const align = alignOverride ?? spec.align;\n const preferredLeft = align === 'right' ? triggerRect.right - width : triggerRect.left;\n const oppositeLeft = align === 'right' ? triggerRect.left : triggerRect.right - width;\n const minLeft = MENU_VIEWPORT_PADDING_PX;\n const maxLeft = viewportWidth - width - MENU_VIEWPORT_PADDING_PX;\n const fits = (candidate: number) => candidate >= minLeft && candidate <= maxLeft;\n\n let left: number;\n if (fits(preferredLeft)) {\n left = preferredLeft;\n } else if (fits(oppositeLeft)) {\n left = oppositeLeft;\n } else {\n left = Math.max(minLeft, Math.min(preferredLeft, maxLeft));\n }\n\n // Which side, then how much of it. \"More room above\" is not \"enough room above\", so the\n // height is clamped to whichever side won. Clamping `top` instead keeps the menu on screen\n // by painting it over its own trigger, which also swallows the click that closes it.\n const roomBelow = viewportHeight - triggerRect.bottom - spec.gap - MENU_VIEWPORT_PADDING_PX;\n const roomAbove = triggerRect.top - spec.gap - MENU_VIEWPORT_PADDING_PX;\n const openUp = menuHeight > 0 && menuHeight > roomBelow && roomAbove > roomBelow;\n\n const maxHeight = Math.max(0, Math.min(MENU_MAX_HEIGHT_PX, openUp ? roomAbove : roomBelow));\n const height = menuHeight > 0 ? Math.min(menuHeight, maxHeight) : 0;\n\n const top = Math.max(\n MENU_VIEWPORT_PADDING_PX,\n openUp ? triggerRect.top - spec.gap - height : triggerRect.bottom + spec.gap,\n );\n\n return { top, left, width, maxHeight, openUp };\n};\n\n// The height decides whether the menu opens upwards, and it is only meaningful at the width the\n// menu will actually have — so on the pass that first sizes the panel the width is written\n// before the height is read. The guard is what keeps that out of the steady state: every later\n// call finds the width already set and writes nothing, so a `ResizeObserver` callback does not\n// resize what it is observing (Chrome's \"undelivered notifications\" error) and a per-frame\n// reposition does not force a reflow. After the first pass this is a pure read.\n// A content-sized preset needs the width the panel chose for itself, which is only readable\n// while no width has been written to it — i.e. on the pass that opens the menu. The caller keeps\n// it from there, because after the first write `offsetWidth` reports what we wrote, not what the\n// items want.\nexport const measureContentWidth = (panel: HTMLElement | null): number | undefined => {\n if (!panel || panel.style.width !== '') {\n return undefined;\n }\n\n // The rows, not the panel. Each label carries `min-width: 0` and `overflow: hidden` so it\n // can ellipsise, which also means it contributes nothing to the panel's intrinsic width —\n // measuring the panel itself returns a box the size of its own padding. `scrollWidth` on a\n // clipped row is the width the row wanted. It is rounded to a whole pixel, so a label\n // 144.2px wide reports 144 and ellipsises at that width; the extra pixel covers the fraction.\n const rows = panel.querySelectorAll<HTMLElement>('button, [role=\"menuitem\"]');\n let widest = 0;\n rows.forEach((row) => {\n widest = Math.max(widest, row.scrollWidth);\n });\n\n if (widest === 0) {\n return undefined;\n }\n\n // The panel is border-box, so the width we hand back has to carry its own padding and rule.\n // Each read is defaulted: a computed style can come back empty — jsdom never expands the\n // `border` shorthand — and one NaN here would make the whole width NaN, which CSS drops,\n // leaving the panel unsized with no sign of why.\n const styles = getComputedStyle(panel);\n const edge = (value: string) => Number.parseFloat(value) || 0;\n const chrome =\n edge(styles.paddingLeft) +\n edge(styles.paddingRight) +\n edge(styles.borderLeftWidth) +\n edge(styles.borderRightWidth);\n\n return Math.ceil(widest + 1 + chrome);\n};\n\nexport const measureMenuGeometry = (\n panel: HTMLElement | null,\n input: Omit<MenuGeometryInput, 'menuHeight'>,\n): MenuGeometry => {\n if (!panel) {\n return computeMenuGeometry({ ...input, menuHeight: 0 });\n }\n\n const cssWidth = `${computeMenuGeometry({ ...input, menuHeight: 0 }).width}px`;\n if (panel.style.width !== cssWidth) {\n panel.style.width = cssWidth;\n }\n\n return computeMenuGeometry({ ...input, menuHeight: panel.offsetHeight });\n};\n\nexport const isTriggerOffscreen = (\n rect: Pick<DOMRect, 'top' | 'left' | 'right' | 'bottom'>,\n viewportWidth: number,\n viewportHeight: number,\n): boolean =>\n rect.bottom < 0 || rect.top > viewportHeight || rect.right < 0 || rect.left > viewportWidth;\n\nexport const rectSignature = (rect: Pick<DOMRect, 'top' | 'left' | 'width' | 'height'>): string =>\n `${rect.top},${rect.left},${rect.width},${rect.height}`;\n\nexport type FloatingPlacement =\n | 'bottom-start'\n | 'bottom-end'\n | 'top-start'\n | 'top-center'\n | 'right-start'\n | 'right-end'\n | 'left-start';\n\nconst alignedLeft = (\n align: 'start' | 'end' | 'center',\n triggerRect: Pick<DOMRect, 'left' | 'right'>,\n panelWidth: number,\n): number => {\n if (align === 'end') {\n return triggerRect.right - panelWidth;\n }\n if (align === 'center') {\n return (triggerRect.left + triggerRect.right - panelWidth) / 2;\n }\n return triggerRect.left;\n};\n\nexport const fullPanelHeight = (panel: HTMLElement): number =>\n panel.scrollHeight + panel.offsetHeight - panel.clientHeight;\n\nexport const FLOATING_OFFSET_PX = { small: 8, medium: 12, large: 14 } as const;\n\nexport type FloatingOffset = keyof typeof FLOATING_OFFSET_PX;\n\nexport interface FloatingGeometryInput {\n triggerRect: Pick<DOMRect, 'top' | 'left' | 'right' | 'bottom'>;\n placement: FloatingPlacement;\n panelWidth: number;\n panelHeight: number;\n gap: number;\n viewportWidth: number;\n viewportHeight: number;\n}\n\nexport interface FloatingGeometry {\n top: number;\n left: number;\n maxHeight: number;\n side: 'top' | 'bottom' | 'left' | 'right';\n}\n\nconst clampToViewport = (value: number, size: number, viewport: number): number =>\n Math.max(MENU_VIEWPORT_PADDING_PX, Math.min(value, viewport - size - MENU_VIEWPORT_PADDING_PX));\n\nexport const computeFloatingGeometry = ({\n triggerRect,\n placement,\n panelWidth,\n panelHeight,\n gap,\n viewportWidth,\n viewportHeight,\n}: FloatingGeometryInput): FloatingGeometry => {\n const [preferred, align] = placement.split('-') as [\n FloatingGeometry['side'],\n 'start' | 'end' | 'center',\n ];\n const pad = MENU_VIEWPORT_PADDING_PX;\n\n if (preferred === 'top' || preferred === 'bottom') {\n const roomBelow = viewportHeight - triggerRect.bottom - gap - pad;\n const roomAbove = triggerRect.top - gap - pad;\n const wanted = preferred === 'bottom' ? roomBelow : roomAbove;\n const opposite = preferred === 'bottom' ? roomAbove : roomBelow;\n const side: FloatingGeometry['side'] =\n panelHeight > wanted && opposite > wanted\n ? preferred === 'bottom'\n ? 'top'\n : 'bottom'\n : preferred;\n const belowTop = Math.max(pad, triggerRect.bottom + gap);\n const aboveBottom = Math.min(viewportHeight - pad, triggerRect.top - gap);\n const maxHeight = Math.max(\n 0,\n side === 'bottom' ? viewportHeight - belowTop - pad : aboveBottom - pad,\n );\n const height = Math.min(panelHeight, maxHeight);\n const top = side === 'bottom' ? belowTop : Math.max(pad, aboveBottom - height);\n const left = clampToViewport(\n alignedLeft(align, triggerRect, panelWidth),\n panelWidth,\n viewportWidth,\n );\n return { top, left, maxHeight, side };\n }\n\n const roomRight = viewportWidth - triggerRect.right - gap - pad;\n const roomLeft = triggerRect.left - gap - pad;\n const wanted = preferred === 'right' ? roomRight : roomLeft;\n const opposite = preferred === 'right' ? roomLeft : roomRight;\n const side: FloatingGeometry['side'] =\n panelWidth > wanted && opposite > wanted\n ? preferred === 'right'\n ? 'left'\n : 'right'\n : preferred;\n const left = side === 'right' ? triggerRect.right + gap : triggerRect.left - gap - panelWidth;\n const levelTop = Math.max(pad, triggerRect.top);\n const roomBelow = viewportHeight - levelTop - pad;\n if (align === 'start' && roomBelow >= Math.min(panelHeight, SIDE_PANEL_MIN_HEIGHT_PX)) {\n return {\n top: levelTop,\n left: clampToViewport(left, panelWidth, viewportWidth),\n maxHeight: roomBelow,\n side,\n };\n }\n const maxHeight = Math.max(0, viewportHeight - pad * 2);\n const height = Math.min(panelHeight, maxHeight);\n const top = clampToViewport(\n align === 'end' ? triggerRect.bottom - height : triggerRect.top,\n height,\n viewportHeight,\n );\n return { top, left: clampToViewport(left, panelWidth, viewportWidth), maxHeight, side };\n};\n"],"names":["MENU_VIEWPORT_PADDING_PX","SIDE_PANEL_MIN_HEIGHT_PX","MENU_MIN_WIDTH_PX","MENU_OPTIONS_MIN_WIDTH_PX","MENU_SPECS","MENU_MAX_HEIGHT_PX","computeMenuGeometry","triggerRect","preset","minWidth","alignOverride","contentWidth","menuHeight","viewportWidth","viewportHeight","spec","available","floor","ceiling","natural","width","align","preferredLeft","oppositeLeft","minLeft","maxLeft","fits","candidate","left","roomBelow","roomAbove","openUp","maxHeight","height","measureContentWidth","panel","rows","widest","row","styles","edge","value","chrome","measureMenuGeometry","input","cssWidth","isTriggerOffscreen","rect","rectSignature","alignedLeft","panelWidth","fullPanelHeight","FLOATING_OFFSET_PX","clampToViewport","size","viewport","computeFloatingGeometry","placement","panelHeight","gap","preferred","pad","wanted","opposite","side","belowTop","aboveBottom","top","roomRight","roomLeft","levelTop"],"mappings":"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"}
|
package/dist/index.d.ts
CHANGED
|
@@ -1167,6 +1167,15 @@ export declare interface ConnectionHeaderProps {
|
|
|
1167
1167
|
|
|
1168
1168
|
export declare const ConnectorIcon: React.FC<IconProps_29>;
|
|
1169
1169
|
|
|
1170
|
+
/**
|
|
1171
|
+
* The text half of the 28/32/42 control ladder. Keys are control sizes, which deliberately do
|
|
1172
|
+
* NOT match the type scale's names: control `medium` is 13px (`font-size-small`, not `-medium`
|
|
1173
|
+
* at 14px) and control `large` is the off-ladder 15px rung, not `font-size-large` at 16px.
|
|
1174
|
+
*/
|
|
1175
|
+
declare type ControlSize = 'small' | 'medium' | 'large';
|
|
1176
|
+
|
|
1177
|
+
declare type ControlWidth = string | number;
|
|
1178
|
+
|
|
1170
1179
|
export declare const CopyIcon: React.FC<IconProps_30>;
|
|
1171
1180
|
|
|
1172
1181
|
export declare const CsvIcon: React.FC<IconProps_31>;
|
|
@@ -1589,6 +1598,62 @@ export declare const FilledCaretRightIcon: React.FC<IconProps_52>;
|
|
|
1589
1598
|
|
|
1590
1599
|
export declare const FilterIcon: React.FC<IconProps_53>;
|
|
1591
1600
|
|
|
1601
|
+
export declare const FilterSearch: default_2.FC<FilterSearchProps>;
|
|
1602
|
+
|
|
1603
|
+
/** One filter's new values. An empty `items` means the filter was removed. */
|
|
1604
|
+
export declare interface FilterSearchChange {
|
|
1605
|
+
filter: string;
|
|
1606
|
+
items: string[];
|
|
1607
|
+
}
|
|
1608
|
+
|
|
1609
|
+
export declare interface FilterSearchFilter {
|
|
1610
|
+
value: string;
|
|
1611
|
+
label: string;
|
|
1612
|
+
/** Shown beside the filter's name in the menu, and beside its values in a cross-filter search. */
|
|
1613
|
+
info?: string;
|
|
1614
|
+
/** Right-aligned hint on the filter's row, e.g. `e.g. Attio, Ashby`. */
|
|
1615
|
+
example?: string;
|
|
1616
|
+
items: FilterSearchItem[];
|
|
1617
|
+
}
|
|
1618
|
+
|
|
1619
|
+
export declare interface FilterSearchItem {
|
|
1620
|
+
value: string;
|
|
1621
|
+
label: string;
|
|
1622
|
+
disabled?: boolean;
|
|
1623
|
+
}
|
|
1624
|
+
|
|
1625
|
+
export declare interface FilterSearchProps {
|
|
1626
|
+
filters: FilterSearchFilter[];
|
|
1627
|
+
/** Controlled: the bar always shows `value`, so a URL-backed selection needs no syncing. */
|
|
1628
|
+
value: FilterSearchSelection[];
|
|
1629
|
+
/** `changes` lists only the filters that moved; an empty `items` means that filter was removed. */
|
|
1630
|
+
onChange: (next: FilterSearchSelection[], changes: FilterSearchChange[]) => void;
|
|
1631
|
+
/** Called when the clear control empties the bar, after `onChange`. */
|
|
1632
|
+
onClear?: () => void;
|
|
1633
|
+
/** The filter whose values are open, or `null` on the filter list — for loading values lazily. */
|
|
1634
|
+
onActiveFilterChange?: (filter: string | null) => void;
|
|
1635
|
+
placeholder?: string;
|
|
1636
|
+
size?: FilterSearchSize;
|
|
1637
|
+
width?: ControlWidth;
|
|
1638
|
+
loading?: boolean;
|
|
1639
|
+
disabled?: boolean;
|
|
1640
|
+
clearable?: boolean;
|
|
1641
|
+
/** Chips past this count collapse into `+N`. */
|
|
1642
|
+
maxVisibleChips?: number;
|
|
1643
|
+
className?: string;
|
|
1644
|
+
style?: default_2.CSSProperties;
|
|
1645
|
+
id?: string;
|
|
1646
|
+
'aria-label'?: string;
|
|
1647
|
+
'aria-labelledby'?: string;
|
|
1648
|
+
}
|
|
1649
|
+
|
|
1650
|
+
export declare interface FilterSearchSelection {
|
|
1651
|
+
filter: string;
|
|
1652
|
+
items: string[];
|
|
1653
|
+
}
|
|
1654
|
+
|
|
1655
|
+
export declare type FilterSearchSize = ControlSize;
|
|
1656
|
+
|
|
1592
1657
|
export declare const Flex: default_2.FC<FlexProps>;
|
|
1593
1658
|
|
|
1594
1659
|
export declare enum FlexAlign {
|
|
@@ -4066,6 +4131,9 @@ export declare interface SelectableCardStatus {
|
|
|
4066
4131
|
label: string;
|
|
4067
4132
|
/** Maps to Tag's palette — `default` (grey) for "Disabled", `warning` (amber) for "Draft". */
|
|
4068
4133
|
type?: TagType;
|
|
4134
|
+
/** Explains what the status means, shown on hovering the tag and read as part of the card's
|
|
4135
|
+
* description, since the tag sits inside the control and is never focused on its own. */
|
|
4136
|
+
tooltip?: string;
|
|
4069
4137
|
}
|
|
4070
4138
|
|
|
4071
4139
|
/** The card's palette: `profile` is green, `library` neutral, `editing` amber for a
|
|
@@ -4790,6 +4858,12 @@ export declare interface TagProps {
|
|
|
4790
4858
|
pointerType?: 'pointer' | 'default';
|
|
4791
4859
|
onClick?: () => void;
|
|
4792
4860
|
onRemove?: (e: default_2.MouseEvent) => void;
|
|
4861
|
+
/** Explains the tag on hover. Takes the place of the truncation tooltip, which only repeats
|
|
4862
|
+
* the label. */
|
|
4863
|
+
tooltip?: string;
|
|
4864
|
+
/** False when something else already reads `tooltip` to assistive tech, such as a card's
|
|
4865
|
+
* description, so it is not announced twice. */
|
|
4866
|
+
tooltipDescribesTag?: boolean;
|
|
4793
4867
|
className?: string;
|
|
4794
4868
|
}
|
|
4795
4869
|
|