@stackone/malachite 2.45.0 → 2.47.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.
@@ -1,4 +1,4 @@
1
- const S = 8, y = 160, F = 160, $ = 180, L = {
1
+ const S = 8, y = 160, F = 160, $ = 180, g = {
2
2
  standard: {
3
3
  width: "trigger",
4
4
  minWidth: 160,
@@ -22,85 +22,86 @@ const S = 8, y = 160, F = 160, $ = 180, L = {
22
22
  background: "var(--malachite-card-background, rgb(255, 255, 255))",
23
23
  border: "1px solid var(--malachite-card-border, rgb(232, 232, 232))"
24
24
  }
25
- }, V = 400, X = ({
25
+ }, L = 400, O = ({
26
26
  triggerRect: t,
27
- preset: s,
27
+ preset: n,
28
28
  minWidth: o,
29
- contentWidth: i,
30
- menuHeight: a,
31
- viewportWidth: e,
29
+ align: e,
30
+ contentWidth: s,
31
+ menuHeight: c,
32
+ viewportWidth: m,
32
33
  viewportHeight: h
33
34
  }) => {
34
- const n = L[s], r = e - 16, m = Math.max(n.minWidth, o ?? 0), P = Math.min(r, Math.max(n.maxWidth ?? r, m)), u = n.width === "trigger" ? t.width : i ?? P, c = Math.max(0, Math.min(Math.max(u, m), P)), I = n.align === "right" ? t.right - c : t.left, d = n.align === "right" ? t.left : t.right - c, N = 8, b = e - c - 8, E = (f) => f >= N && f <= b;
35
- let _;
36
- E(I) ? _ = I : E(d) ? _ = d : _ = Math.max(N, Math.min(I, b));
37
- const l = h - t.bottom - n.gap - 8, T = t.top - n.gap - 8, M = a > 0 && a > l && T > l, x = Math.max(0, Math.min(V, M ? T : l)), p = a > 0 ? Math.min(a, x) : 0;
35
+ const i = g[n], a = m - 16, f = Math.max(i.minWidth, o ?? 0), P = Math.min(a, Math.max(i.maxWidth ?? a, f)), T = i.width === "trigger" ? t.width : s ?? P, _ = Math.max(0, Math.min(Math.max(T, f), P)), r = e ?? i.align, d = r === "right" ? t.right - _ : t.left, I = r === "right" ? t.left : t.right - _, N = 8, b = m - _ - 8, E = (D) => D >= N && D <= b;
36
+ let x;
37
+ E(d) ? x = d : E(I) ? x = I : x = Math.max(N, Math.min(d, b));
38
+ const l = h - t.bottom - i.gap - 8, u = t.top - i.gap - 8, M = c > 0 && c > l && u > l, X = Math.max(0, Math.min(L, M ? u : l)), p = c > 0 ? Math.min(c, X) : 0;
38
39
  return { top: Math.max(
39
40
  8,
40
- M ? t.top - n.gap - p : t.bottom + n.gap
41
- ), left: _, width: c, maxHeight: x, openUp: M };
41
+ M ? t.top - i.gap - p : t.bottom + i.gap
42
+ ), left: x, width: _, maxHeight: X, openUp: M };
42
43
  }, k = (t) => {
43
44
  if (!t || t.style.width !== "")
44
45
  return;
45
- const s = t.querySelectorAll('button, [role="menuitem"]');
46
+ const n = t.querySelectorAll('button, [role="menuitem"]');
46
47
  let o = 0;
47
- if (s.forEach((h) => {
48
- o = Math.max(o, h.scrollWidth);
48
+ if (n.forEach((m) => {
49
+ o = Math.max(o, m.scrollWidth);
49
50
  }), o === 0)
50
51
  return;
51
- const i = getComputedStyle(t), a = (h) => Number.parseFloat(h) || 0, e = a(i.paddingLeft) + a(i.paddingRight) + a(i.borderLeftWidth) + a(i.borderRightWidth);
52
- return Math.ceil(o + e);
53
- }, B = (t, s) => {
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);
54
+ }, B = (t, n) => {
54
55
  if (!t)
55
- return X({ ...s, menuHeight: 0 });
56
- const o = `${X({ ...s, menuHeight: 0 }).width}px`;
57
- return t.style.width !== o && (t.style.width = o), X({ ...s, menuHeight: t.offsetHeight });
58
- }, C = (t, s, o) => t.bottom < 0 || t.top > o || t.right < 0 || t.left > s, v = (t) => `${t.top},${t.left},${t.width},${t.height}`, g = (t, s, o) => t === "end" ? s.right - o : t === "center" ? (s.left + s.right - o) / 2 : s.left, q = (t) => t.scrollHeight + t.offsetHeight - t.clientHeight, j = { small: 8, medium: 12, large: 14 }, D = (t, s, o) => Math.max(8, Math.min(t, o - s - 8)), z = ({
56
+ return O({ ...n, menuHeight: 0 });
57
+ const o = `${O({ ...n, menuHeight: 0 }).width}px`;
58
+ return t.style.width !== o && (t.style.width = o), O({ ...n, menuHeight: t.offsetHeight });
59
+ }, C = (t, n, o) => t.bottom < 0 || t.top > o || t.right < 0 || t.left > n, v = (t) => `${t.top},${t.left},${t.width},${t.height}`, V = (t, n, o) => t === "end" ? n.right - o : t === "center" ? (n.left + n.right - o) / 2 : n.left, q = (t) => t.scrollHeight + t.offsetHeight - t.clientHeight, j = { small: 8, medium: 12, large: 14 }, A = (t, n, o) => Math.max(8, Math.min(t, o - n - 8)), z = ({
59
60
  triggerRect: t,
60
- placement: s,
61
+ placement: n,
61
62
  panelWidth: o,
62
- panelHeight: i,
63
- gap: a,
64
- viewportWidth: e,
65
- viewportHeight: h
63
+ panelHeight: e,
64
+ gap: s,
65
+ viewportWidth: c,
66
+ viewportHeight: m
66
67
  }) => {
67
- const [n, r] = s.split("-"), m = 8;
68
- if (n === "top" || n === "bottom") {
69
- const M = h - t.bottom - a - m, x = t.top - a - m, p = n === "bottom" ? M : x, A = n === "bottom" ? x : M, f = i > p && A > p ? n === "bottom" ? "top" : "bottom" : n, G = Math.max(m, t.bottom + a), O = Math.min(h - m, t.top - a), U = Math.max(
68
+ const [h, i] = n.split("-"), a = 8;
69
+ if (h === "top" || h === "bottom") {
70
+ const l = m - t.bottom - s - a, u = t.top - s - a, M = h === "bottom" ? l : u, X = h === "bottom" ? u : l, p = e > M && X > M ? h === "bottom" ? "top" : "bottom" : h, G = Math.max(a, t.bottom + s), D = Math.min(m - a, t.top - s), U = Math.max(
70
71
  0,
71
- f === "bottom" ? h - G - m : O - m
72
- ), W = Math.min(i, U), w = f === "bottom" ? G : Math.max(m, O - W), H = D(
73
- g(r, t, o),
72
+ p === "bottom" ? m - G - a : D - a
73
+ ), W = Math.min(e, U), w = p === "bottom" ? G : Math.max(a, D - W), H = A(
74
+ V(i, t, o),
74
75
  o,
75
- e
76
+ c
76
77
  );
77
- return { top: w, left: H, maxHeight: U, side: f };
78
+ return { top: w, left: H, maxHeight: U, side: p };
78
79
  }
79
- const P = e - t.right - a - m, u = t.left - a - m, c = n === "right" ? P : u, I = n === "right" ? u : P, d = o > c && I > c ? n === "right" ? "left" : "right" : n, N = d === "right" ? t.right + a : t.left - a - o, b = Math.max(m, t.top), E = h - b - m;
80
- if (r === "start" && E >= Math.min(i, 160))
80
+ const f = c - t.right - s - a, P = t.left - s - a, T = h === "right" ? f : P, _ = h === "right" ? P : f, r = o > T && _ > T ? h === "right" ? "left" : "right" : h, d = r === "right" ? t.right + s : t.left - s - o, I = Math.max(a, t.top), N = m - I - a;
81
+ if (i === "start" && N >= Math.min(e, 160))
81
82
  return {
82
- top: b,
83
- left: D(N, o, e),
84
- maxHeight: E,
85
- side: d
83
+ top: I,
84
+ left: A(d, o, c),
85
+ maxHeight: N,
86
+ side: r
86
87
  };
87
- const _ = Math.max(0, h - m * 2), l = Math.min(i, _);
88
- return { top: D(
89
- r === "end" ? t.bottom - l : t.top,
90
- l,
91
- h
92
- ), left: D(N, o, e), maxHeight: _, side: d };
88
+ const b = Math.max(0, m - a * 2), E = Math.min(e, b);
89
+ return { top: A(
90
+ i === "end" ? t.bottom - E : t.top,
91
+ E,
92
+ m
93
+ ), left: A(d, o, c), maxHeight: b, side: r };
93
94
  };
94
95
  export {
95
96
  j as FLOATING_OFFSET_PX,
96
- V as MENU_MAX_HEIGHT_PX,
97
+ L as MENU_MAX_HEIGHT_PX,
97
98
  F as MENU_MIN_WIDTH_PX,
98
99
  $ as MENU_OPTIONS_MIN_WIDTH_PX,
99
- L as MENU_SPECS,
100
+ g as MENU_SPECS,
100
101
  S as MENU_VIEWPORT_PADDING_PX,
101
102
  y as SIDE_PANEL_MIN_HEIGHT_PX,
102
103
  z as computeFloatingGeometry,
103
- X as computeMenuGeometry,
104
+ O as computeMenuGeometry,
104
105
  q as fullPanelHeight,
105
106
  C as isTriggerOffscreen,
106
107
  k as measureContentWidth,
@@ -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 /** 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 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 preferredLeft = spec.align === 'right' ? triggerRect.right - width : triggerRect.left;\n const oppositeLeft = spec.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","contentWidth","menuHeight","viewportWidth","viewportHeight","spec","available","floor","ceiling","natural","width","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","align","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.\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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"}
@@ -0,0 +1,5 @@
1
+ const o = (l) => l != null && l !== !1;
2
+ export {
3
+ o as hasBadge
4
+ };
5
+ //# sourceMappingURL=hasBadge.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"hasBadge.js","sources":["../../src/components/hasBadge.ts"],"sourcesContent":["import type React from 'react';\n\n// React renders 0 and '' as text, so only the values it skips mean \"no badge\".\nexport const hasBadge = (badge: React.ReactNode): boolean =>\n badge !== undefined && badge !== null && badge !== false;\n"],"names":["hasBadge","badge"],"mappings":"AAGO,MAAMA,IAAW,CAACC,MACEA,KAAU,QAAQA,MAAU;"}
package/dist/index.d.ts CHANGED
@@ -1355,7 +1355,7 @@ export declare const DoubleCheckIcon: React.FC<IconProps_39>;
1355
1355
 
1356
1356
  export declare const DownloadIcon: React.FC<IconProps_40>;
1357
1357
 
1358
- export declare const Dropdown: ({ items, onItemSelected, defaultValue, value, disabled, error, tone, label, hint, rightSideContent, tooltip, name, required, requiredIndicator, loading, menuLoading, formControl, description, helperText, placeholder, search, showSubtitleInTrigger, subtitleLayout, clearable, size, fullWidth, width, minWidth, id, "aria-label": ariaLabel, "aria-labelledby": ariaLabelledBy, }: DropdownProps) => JSX_2.Element;
1358
+ export declare const Dropdown: ({ items, onItemSelected, defaultValue, value, disabled, error, tone, label, hint, rightSideContent, tooltip, name, required, requiredIndicator, loading, menuLoading, formControl, description, helperText, placeholder, search, showSubtitleInTrigger, subtitleLayout, clearable, size, fullWidth, width, minWidth, menuAlign, id, "aria-label": ariaLabel, "aria-labelledby": ariaLabelledBy, }: DropdownProps) => JSX_2.Element;
1359
1359
 
1360
1360
  declare interface DropdownBaseProps {
1361
1361
  label?: string;
@@ -1395,6 +1395,11 @@ declare interface DropdownBaseProps {
1395
1395
  * picker in a 200px slot — whose options carry a subtitle the trigger never has to show.
1396
1396
  */
1397
1397
  minWidth?: number;
1398
+ /**
1399
+ * Which trigger edge a menu wider than its trigger lines up with. `end` grows it leftwards,
1400
+ * for a control at the right of its container. Either flips when the viewport has no room.
1401
+ */
1402
+ menuAlign?: 'start' | 'end';
1398
1403
  /** Names the trigger from the light DOM, where `htmlFor` cannot reach into the shadow root. */
1399
1404
  id?: string;
1400
1405
  'aria-label'?: string;
@@ -1407,12 +1412,16 @@ export declare interface DropdownItem {
1407
1412
  icon?: default_2.ReactNode;
1408
1413
  /** Alternate label shown in the closed trigger when selected (falls back to `label`). */
1409
1414
  triggerLabel?: string;
1415
+ /** Short tag shown at the right of the closed trigger while this item is selected. */
1416
+ triggerTag?: string;
1410
1417
  /** Items sharing a `group` render under a section header, in first-seen order. */
1411
1418
  group?: string;
1412
1419
  subtitle?: string;
1413
1420
  rightSideText?: string;
1414
1421
  rightSideTextVariant?: 'muted' | 'primary';
1415
1422
  rightSideTextAlignment?: 'left' | 'right';
1423
+ /** Right-aligned badge shown in the open menu only (not in the trigger or the search input). */
1424
+ badge?: default_2.ReactNode;
1416
1425
  }
1417
1426
 
1418
1427
  export declare type DropdownProps = DropdownBaseProps & DropdownTriggerModeProps;