@stll/ui 0.18.0 → 0.20.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.
Files changed (39) hide show
  1. package/dist/components/button-variants.d.ts +2 -2
  2. package/dist/components/composer.d.ts +108 -0
  3. package/dist/components/composer.js +132 -0
  4. package/dist/components/landing.d.ts +61 -0
  5. package/dist/components/landing.js +97 -0
  6. package/dist/components/sidebar.d.ts +109 -0
  7. package/dist/components/sidebar.js +361 -0
  8. package/dist/components/sidebar.logic.d.ts +34 -0
  9. package/dist/components/sidebar.logic.js +27 -0
  10. package/dist/index.d.ts +6 -3
  11. package/dist/index.js +6 -3
  12. package/dist/inspector/entity-tab.d.ts +54 -0
  13. package/dist/inspector/entity-tab.js +56 -0
  14. package/dist/inspector/entity-tab.logic.d.ts +28 -0
  15. package/dist/inspector/entity-tab.logic.js +32 -0
  16. package/dist/inspector/facet-bar.d.ts +46 -0
  17. package/dist/inspector/facet-bar.js +166 -0
  18. package/dist/inspector/facet-bar.logic.d.ts +51 -0
  19. package/dist/inspector/facet-bar.logic.js +53 -0
  20. package/dist/inspector/index.d.ts +3 -1
  21. package/dist/inspector/index.js +3 -1
  22. package/dist/inspector/tabs.d.ts +1 -1
  23. package/dist/kanban/band-peek.d.ts +26 -33
  24. package/dist/kanban/band-peek.js +24 -30
  25. package/dist/kanban/column-header.d.ts +8 -1
  26. package/dist/kanban/column-header.js +9 -2
  27. package/dist/kanban/drag-interactions.d.ts +9 -1
  28. package/dist/kanban/drag-interactions.js +10 -1
  29. package/dist/kanban/index.d.ts +2 -2
  30. package/dist/kanban/index.js +2 -2
  31. package/dist/kanban/sortable-interactions.d.ts +12 -0
  32. package/dist/kanban/subgroup-board.d.ts +33 -6
  33. package/dist/kanban/subgroup-board.js +75 -22
  34. package/dist/kanban/virtual-cell.d.ts +10 -1
  35. package/dist/kanban/virtual-cell.js +25 -3
  36. package/dist/lib/control-size.d.ts +1 -1
  37. package/dist/lib/slot.d.ts +28 -0
  38. package/dist/lib/slot.js +64 -0
  39. package/package.json +17 -1
@@ -0,0 +1,54 @@
1
+ import { ComponentProps, ReactNode } from "react";
2
+ //#region src/inspector/entity-tab.d.ts
3
+ /**
4
+ * One rail tab for an open entity: a bordered `InspectorRailTab` cell
5
+ * showing the active tab's icon, with a tooltip carrying the full label.
6
+ * Generic over what "entity" means — the host supplies the icon dispatch
7
+ * (file type, task status, chat, …) and, for kinds that want it, the
8
+ * inactive-state glyph; this only owns the cell's shared shape and
9
+ * affordances.
10
+ */
11
+ declare const InspectorEntityTab: ({ active, label, glyph, icon, onClose, onSelect, ...props }: InspectorEntityTabProps) => import("react").JSX.Element;
12
+ type InspectorEntityTabPassthroughProps = Omit<ComponentProps<"button">, "children" | "onAuxClick" | "onClick">;
13
+ type InspectorEntityTabProps = {
14
+ /** Whether this tab is the one currently shown in the pane. Drives the
15
+ * active spine, and — when `glyph` is given — swaps the cell's content
16
+ * between `glyph` and `icon`. */
17
+ active: boolean;
18
+ /** Full entity name. Both the button's accessible name and the tooltip
19
+ * text — the cell itself never has room to show it. */
20
+ label: string;
21
+ /**
22
+ * Short text shown in the cell instead of `icon` while the tab is
23
+ * inactive (e.g. a 3-character stem the host derives with
24
+ * `entityTabGlyph`). Omit it for an entity kind whose icon alone already
25
+ * identifies it (a status glyph, a colored dot, a logo): `icon` then
26
+ * renders in both states, dimmed while inactive instead of swapping to
27
+ * text.
28
+ */
29
+ glyph?: string | undefined;
30
+ /** Shown in the cell while the tab is active — and, with no `glyph`,
31
+ * while inactive too (dimmed). */
32
+ icon: ReactNode;
33
+ /**
34
+ * Middle-click (`onAuxClick` button 1) closes the tab. A keyboard- and
35
+ * pointer-reachable close affordance still has to exist — normally a
36
+ * "Close" item in the host's own context menu, wired to the same
37
+ * callback — this only covers the middle-click gesture.
38
+ */
39
+ onClose?: (() => void) | undefined;
40
+ /** Fires on a (contained) left click: activate this tab. */
41
+ onSelect?: (() => void) | undefined;
42
+ } & InspectorEntityTabPassthroughProps;
43
+ /**
44
+ * Short abbreviation for a rail tab's inactive glyph: the filename stem,
45
+ * the first `length` characters; the cell uppercases it.
46
+ *
47
+ * A leading dot is part of the name, not an extension separator — a
48
+ * dotfile's own extension (a second dot further in) still drops, but the
49
+ * leading dot itself survives into the glyph: `entityTabGlyph(".gitignore")`
50
+ * is `".gi"`, not `""`.
51
+ */
52
+ declare const entityTabGlyph: (name: string, length?: number) => string;
53
+ //#endregion
54
+ export { InspectorEntityTab, InspectorEntityTabProps, entityTabGlyph };
@@ -0,0 +1,56 @@
1
+ import { cn } from "../lib/utils.js";
2
+ import { SIDE_RAIL_TAB_ICON_SIZE } from "./layout-tokens.js";
3
+ import { Tooltip, TooltipContent as TooltipPopup, TooltipTrigger } from "../components/tooltip.js";
4
+ import { InspectorRailTab } from "./chrome.js";
5
+ import { containedEventHandler } from "../hooks/use-contained-handler.js";
6
+ import { resolveEntityTabActivateHandler, resolveEntityTabCloseHandler } from "./entity-tab.logic.js";
7
+ import { jsx, jsxs } from "react/jsx-runtime";
8
+ //#region src/inspector/entity-tab.tsx
9
+ /**
10
+ * One rail tab for an open entity: a bordered `InspectorRailTab` cell
11
+ * showing the active tab's icon, with a tooltip carrying the full label.
12
+ * Generic over what "entity" means — the host supplies the icon dispatch
13
+ * (file type, task status, chat, …) and, for kinds that want it, the
14
+ * inactive-state glyph; this only owns the cell's shared shape and
15
+ * affordances.
16
+ */
17
+ const InspectorEntityTab = ({ active, label, glyph, icon, onClose, onSelect, ...props }) => /* @__PURE__ */ jsxs(Tooltip, { children: [/* @__PURE__ */ jsx(TooltipTrigger, { render: /* @__PURE__ */ jsx(InspectorRailTab, {
18
+ ...props,
19
+ active,
20
+ "aria-label": label,
21
+ onAuxClick: (event) => {
22
+ const close = resolveEntityTabCloseHandler(event.button, onClose);
23
+ if (close) {
24
+ event.preventDefault();
25
+ close();
26
+ }
27
+ },
28
+ onClick: containedEventHandler(resolveEntityTabActivateHandler(onSelect)),
29
+ children: !active && glyph !== void 0 ? /* @__PURE__ */ jsx("span", {
30
+ className: "text-[9px] leading-none font-semibold tracking-tight uppercase",
31
+ children: glyph
32
+ }) : /* @__PURE__ */ jsx("span", {
33
+ className: cn("flex items-center justify-center", SIDE_RAIL_TAB_ICON_SIZE, !active && "opacity-70"),
34
+ children: icon
35
+ })
36
+ }) }), /* @__PURE__ */ jsx(TooltipPopup, {
37
+ side: "left",
38
+ children: label
39
+ })] });
40
+ /**
41
+ * Short abbreviation for a rail tab's inactive glyph: the filename stem,
42
+ * the first `length` characters; the cell uppercases it.
43
+ *
44
+ * A leading dot is part of the name, not an extension separator — a
45
+ * dotfile's own extension (a second dot further in) still drops, but the
46
+ * leading dot itself survives into the glyph: `entityTabGlyph(".gitignore")`
47
+ * is `".gi"`, not `""`.
48
+ */
49
+ const entityTabGlyph = (name, length = 3) => {
50
+ const leadingDot = name.startsWith(".") ? "." : "";
51
+ const rest = leadingDot === "" ? name : name.slice(1);
52
+ const dot = rest.lastIndexOf(".");
53
+ return (leadingDot + (dot === -1 ? rest : rest.slice(0, dot))).slice(0, length);
54
+ };
55
+ //#endregion
56
+ export { InspectorEntityTab, entityTabGlyph };
@@ -0,0 +1,28 @@
1
+ //#region src/inspector/entity-tab.logic.d.ts
2
+ /**
3
+ * Pure activation/close-gesture policy for `InspectorEntityTab`, kept out
4
+ * of the component so it's unit-testable without a DOM: each function
5
+ * takes only the plain values a `MouseEvent` carries (or the host's
6
+ * optional callbacks), never the event object itself.
7
+ */
8
+ /** Whether a mouse button number is the tab's "close" gesture
9
+ * (middle-click). */
10
+ declare const isEntityTabCloseGesture: (button: number) => boolean;
11
+ /**
12
+ * Resolves what an `onAuxClick` with this `button` should do: close the
13
+ * tab (the given `onClose`) when the gesture matches and the host
14
+ * supplied one, or nothing (`undefined`) otherwise. Returning `undefined`
15
+ * for "do nothing" (rather than a boolean) lets the component skip
16
+ * `preventDefault()` too when there's truly nothing to do, leaving the
17
+ * browser's own middle-click behavior alone.
18
+ */
19
+ declare const resolveEntityTabCloseHandler: (button: number, onClose: (() => void) | undefined) => (() => void) | undefined;
20
+ /**
21
+ * Resolves the tab's click/activation handler: `onSelect` when the host
22
+ * supplied one, a stable no-op otherwise, so the component always has a
23
+ * callable handler to hand to `containedEventHandler` regardless of
24
+ * whether the host cares about selection.
25
+ */
26
+ declare const resolveEntityTabActivateHandler: (onSelect: (() => void) | undefined) => (() => void);
27
+ //#endregion
28
+ export { isEntityTabCloseGesture, resolveEntityTabActivateHandler, resolveEntityTabCloseHandler };
@@ -0,0 +1,32 @@
1
+ //#region src/inspector/entity-tab.logic.ts
2
+ /**
3
+ * Pure activation/close-gesture policy for `InspectorEntityTab`, kept out
4
+ * of the component so it's unit-testable without a DOM: each function
5
+ * takes only the plain values a `MouseEvent` carries (or the host's
6
+ * optional callbacks), never the event object itself.
7
+ */
8
+ /** The `MouseEvent.button` value for a middle-click (the wheel / auxiliary
9
+ * button) — the browser convention `onAuxClick` reports it under. */
10
+ const MIDDLE_CLICK_BUTTON = 1;
11
+ /** Whether a mouse button number is the tab's "close" gesture
12
+ * (middle-click). */
13
+ const isEntityTabCloseGesture = (button) => button === MIDDLE_CLICK_BUTTON;
14
+ /**
15
+ * Resolves what an `onAuxClick` with this `button` should do: close the
16
+ * tab (the given `onClose`) when the gesture matches and the host
17
+ * supplied one, or nothing (`undefined`) otherwise. Returning `undefined`
18
+ * for "do nothing" (rather than a boolean) lets the component skip
19
+ * `preventDefault()` too when there's truly nothing to do, leaving the
20
+ * browser's own middle-click behavior alone.
21
+ */
22
+ const resolveEntityTabCloseHandler = (button, onClose) => isEntityTabCloseGesture(button) ? onClose : void 0;
23
+ /**
24
+ * Resolves the tab's click/activation handler: `onSelect` when the host
25
+ * supplied one, a stable no-op otherwise, so the component always has a
26
+ * callable handler to hand to `containedEventHandler` regardless of
27
+ * whether the host cares about selection.
28
+ */
29
+ const resolveEntityTabActivateHandler = (onSelect) => onSelect ?? entityTabNoop;
30
+ const entityTabNoop = () => void 0;
31
+ //#endregion
32
+ export { isEntityTabCloseGesture, resolveEntityTabActivateHandler, resolveEntityTabCloseHandler };
@@ -0,0 +1,46 @@
1
+ //#region src/inspector/facet-bar.d.ts
2
+ type InspectorFacetBarProps<F extends string> = {
3
+ facet: F;
4
+ facets: readonly F[];
5
+ /** Display label per facet. */
6
+ labels: Record<F, string>;
7
+ /**
8
+ * Facets rendered but not interactive — visible so users can find
9
+ * them, but clicking does nothing (e.g. an AI-suggestions chip
10
+ * before any proposals exist).
11
+ */
12
+ disabledFacets?: ReadonlySet<F> | undefined;
13
+ /**
14
+ * Bumping this replays a one-shot attention pulse on the active
15
+ * chip (e.g. after a background action changes it). Undefined or
16
+ * unchanged: no pulse.
17
+ */
18
+ pulseSeq?: number | undefined;
19
+ /**
20
+ * Suffix appended to the active facet's label, e.g. `"v1"` →
21
+ * "Preview · v1". Hidden on inactive chips so the row stays
22
+ * scannable.
23
+ */
24
+ activeBadge?: string | undefined;
25
+ /** Accessible name for the overflow chevron trigger. The host owns
26
+ * copy/translation; this only wires the affordance. */
27
+ overflowMenuLabel: string;
28
+ onChange: (next: F) => void;
29
+ };
30
+ /**
31
+ * Inspector subtab row: a single line of pill chips at toolbar-row
32
+ * height. Presentational and facet-agnostic — every inspector tab
33
+ * type (file-viewer facets, template-studio fields/clauses/history)
34
+ * drives it with its own facet union + labels so the row reads
35
+ * identically across the inspector.
36
+ *
37
+ * Labels are never truncated: when the row is too narrow for every
38
+ * chip, the ones that don't fit collapse into a trailing chevron
39
+ * (`˅`) dropdown. The active facet is pinned visible ahead of the rest
40
+ * so the current tab stays readable rather than hiding inside the menu
41
+ * — except at the narrowest widths, where not even the active chip fits
42
+ * beside the trigger; see `resolveFacetOverflow`'s narrow-width floor.
43
+ */
44
+ declare const InspectorFacetBar: <F extends string>({ facet, facets, labels, disabledFacets, pulseSeq, activeBadge, overflowMenuLabel, onChange }: InspectorFacetBarProps<F>) => import("react").JSX.Element;
45
+ //#endregion
46
+ export { InspectorFacetBar, InspectorFacetBarProps };
@@ -0,0 +1,166 @@
1
+ import { cn } from "../lib/utils.js";
2
+ import { TOOLBAR_ROW_HEIGHT } from "./layout-tokens.js";
3
+ import { Tooltip, TooltipContent as TooltipPopup, TooltipTrigger } from "../components/tooltip.js";
4
+ import { Button } from "../components/button.js";
5
+ import { DropdownMenu as Menu, DropdownMenuContent as MenuPopup, DropdownMenuItem as MenuItem, DropdownMenuTrigger as MenuTrigger } from "../components/menu.js";
6
+ import { resolveFacetOverflow } from "./facet-bar.logic.js";
7
+ import { ChevronDownIcon } from "lucide-react";
8
+ import { jsx, jsxs } from "react/jsx-runtime";
9
+ import { useEffect, useRef, useState } from "react";
10
+ //#region src/inspector/facet-bar.tsx
11
+ /**
12
+ * Inspector subtab row: a single line of pill chips at toolbar-row
13
+ * height. Presentational and facet-agnostic — every inspector tab
14
+ * type (file-viewer facets, template-studio fields/clauses/history)
15
+ * drives it with its own facet union + labels so the row reads
16
+ * identically across the inspector.
17
+ *
18
+ * Labels are never truncated: when the row is too narrow for every
19
+ * chip, the ones that don't fit collapse into a trailing chevron
20
+ * (`˅`) dropdown. The active facet is pinned visible ahead of the rest
21
+ * so the current tab stays readable rather than hiding inside the menu
22
+ * — except at the narrowest widths, where not even the active chip fits
23
+ * beside the trigger; see `resolveFacetOverflow`'s narrow-width floor.
24
+ */
25
+ const InspectorFacetBar = ({ facet, facets, labels, disabledFacets, pulseSeq, activeBadge, overflowMenuLabel, onChange }) => {
26
+ const lastPulseSeq = useRef(pulseSeq);
27
+ const containerRef = useRef(null);
28
+ const measureRef = useRef(null);
29
+ const [visibleCount, setVisibleCount] = useState(facets.length);
30
+ useEffect(() => {
31
+ if (pulseSeq === void 0 || pulseSeq === lastPulseSeq.current) return;
32
+ lastPulseSeq.current = pulseSeq;
33
+ const activeChip = containerRef.current?.querySelector("[data-facet-chip-active]");
34
+ if (activeChip instanceof HTMLElement) flashFacetChip(activeChip);
35
+ }, [pulseSeq]);
36
+ useEffect(() => {
37
+ const container = containerRef.current;
38
+ const measure = measureRef.current;
39
+ if (!container || !measure) return;
40
+ const recompute = () => {
41
+ const style = getComputedStyle(container);
42
+ const padX = (Number.parseFloat(style.paddingInlineStart) || 0) + (Number.parseFloat(style.paddingInlineEnd) || 0);
43
+ const gap = Number.parseFloat(style.columnGap) || 0;
44
+ const availableWidth = container.clientWidth - padX;
45
+ const cells = [...measure.children];
46
+ const triggerWidth = cells.at(-1)?.getBoundingClientRect().width ?? 0;
47
+ const chipWidths = cells.slice(0, facets.length).map((cell) => cell.getBoundingClientRect().width);
48
+ const policy = resolveFacetOverflow({
49
+ activeIndex: facets.indexOf(facet),
50
+ availableWidth,
51
+ chipWidths,
52
+ gap,
53
+ triggerWidth
54
+ });
55
+ setVisibleCount(policy.visibleCount);
56
+ };
57
+ const observer = new ResizeObserver(recompute);
58
+ observer.observe(container);
59
+ observer.observe(measure);
60
+ recompute();
61
+ return () => observer.disconnect();
62
+ }, [facets, facet]);
63
+ const overflowing = visibleCount < facets.length;
64
+ const activeIndex = facets.indexOf(facet);
65
+ let visibleFacets;
66
+ let overflowFacets;
67
+ if (!overflowing) {
68
+ visibleFacets = [...facets];
69
+ overflowFacets = [];
70
+ } else if (visibleCount === 0) {
71
+ visibleFacets = [];
72
+ overflowFacets = [...facets];
73
+ } else if (activeIndex < visibleCount) {
74
+ visibleFacets = facets.slice(0, visibleCount);
75
+ overflowFacets = facets.slice(visibleCount);
76
+ } else {
77
+ visibleFacets = [...facets.slice(0, visibleCount - 1), facet];
78
+ overflowFacets = facets.filter((value) => !visibleFacets.includes(value));
79
+ }
80
+ return /* @__PURE__ */ jsxs("div", {
81
+ className: cn("bg-background/85 supports-[backdrop-filter]:bg-background/65 sticky top-0 z-10 flex shrink-0 items-center gap-0.5 overflow-hidden border-b px-1.5 backdrop-blur", TOOLBAR_ROW_HEIGHT),
82
+ ref: containerRef,
83
+ children: [
84
+ visibleFacets.map((value) => /* @__PURE__ */ jsx(FacetChip, {
85
+ activeBadge,
86
+ disabled: disabledFacets?.has(value) ?? false,
87
+ isActive: value === facet,
88
+ label: labels[value],
89
+ onSelect: () => onChange(value)
90
+ }, value)),
91
+ overflowing && /* @__PURE__ */ jsxs(Menu, { children: [/* @__PURE__ */ jsx(MenuTrigger, {
92
+ "aria-label": overflowMenuLabel,
93
+ className: "ms-auto",
94
+ render: /* @__PURE__ */ jsx(Button, {
95
+ size: "icon-sm",
96
+ type: "button",
97
+ variant: "outline"
98
+ }),
99
+ children: /* @__PURE__ */ jsx(ChevronDownIcon, {})
100
+ }), /* @__PURE__ */ jsx(MenuPopup, {
101
+ align: "end",
102
+ side: "bottom",
103
+ children: overflowFacets.map((value) => /* @__PURE__ */ jsx(MenuItem, {
104
+ disabled: disabledFacets?.has(value) ?? false,
105
+ onClick: () => onChange(value),
106
+ children: labels[value]
107
+ }, value))
108
+ })] }),
109
+ /* @__PURE__ */ jsxs("div", {
110
+ "aria-hidden": "true",
111
+ className: "pointer-events-none invisible absolute flex w-max items-center gap-0.5",
112
+ ref: measureRef,
113
+ children: [facets.map((value) => /* @__PURE__ */ jsx("span", {
114
+ className: cn(CHIP_CLASS, INACTIVE_CHIP_CLASS),
115
+ children: labels[value]
116
+ }, value)), /* @__PURE__ */ jsx("span", { className: "size-8 shrink-0 rounded-lg border sm:size-7" })]
117
+ })
118
+ ]
119
+ });
120
+ };
121
+ /** One-shot attention flash: a ring that appears with a couple of opacity
122
+ * dips over 1.4s, then fades — the same imperative Web Animations API
123
+ * technique the rail's `flashTabElement` uses, so a single call finishes on
124
+ * its own without any pulsing state to track or clean up. */
125
+ const flashFacetChip = (el) => {
126
+ const ring = "0 0 0 2px var(--color-foreground-disabled)";
127
+ el.animate([
128
+ {
129
+ boxShadow: ring,
130
+ opacity: 1
131
+ },
132
+ {
133
+ boxShadow: ring,
134
+ opacity: .5
135
+ },
136
+ {
137
+ boxShadow: ring,
138
+ opacity: 1
139
+ },
140
+ {
141
+ boxShadow: ring,
142
+ opacity: .5
143
+ },
144
+ {
145
+ boxShadow: "0 0 0 2px transparent",
146
+ opacity: 1
147
+ }
148
+ ], {
149
+ duration: 1400,
150
+ easing: "ease-in-out"
151
+ });
152
+ };
153
+ const CHIP_CLASS = "shrink-0 rounded-md px-1.5 py-1 text-xs font-medium whitespace-nowrap transition-colors";
154
+ const INACTIVE_CHIP_CLASS = "text-muted-foreground hover:bg-muted hover:text-foreground";
155
+ const FacetChip = ({ label, isActive, disabled, activeBadge, onSelect }) => /* @__PURE__ */ jsxs(Tooltip, { children: [/* @__PURE__ */ jsx(TooltipTrigger, {
156
+ render: /* @__PURE__ */ jsx("button", {
157
+ className: cn(CHIP_CLASS, isActive ? "bg-foreground text-background" : INACTIVE_CHIP_CLASS, disabled && "cursor-not-allowed opacity-40 hover:bg-transparent"),
158
+ "data-facet-chip-active": isActive ? "" : void 0,
159
+ disabled,
160
+ onClick: onSelect,
161
+ type: "button"
162
+ }),
163
+ children: label
164
+ }), /* @__PURE__ */ jsx(TooltipPopup, { children: isActive && activeBadge !== void 0 ? `${label} · ${activeBadge}` : label })] });
165
+ //#endregion
166
+ export { InspectorFacetBar };
@@ -0,0 +1,51 @@
1
+ //#region src/inspector/facet-bar.logic.d.ts
2
+ /**
3
+ * Pure overflow policy for `InspectorFacetBar`: given the row's available
4
+ * width and each chip's natural (measured) width, decides how many chips
5
+ * stay visible and whether the overflow trigger renders — kept out of the
6
+ * component so it's unit-testable without a DOM or a `ResizeObserver`.
7
+ */
8
+ type FacetOverflowInput = {
9
+ /** Width inside the row available to the chips (the trigger's own
10
+ * footprint is reserved out of this by the policy, not by the caller). */
11
+ availableWidth: number;
12
+ /** Each chip's natural width, in the same order as the facets they
13
+ * represent. */
14
+ chipWidths: readonly number[];
15
+ /** Index into `chipWidths` of the active chip — always kept visible
16
+ * whenever anything is. */
17
+ activeIndex: number;
18
+ /** The overflow trigger's own footprint, reserved alongside the chips
19
+ * whenever not every chip fits. */
20
+ triggerWidth: number;
21
+ /** Horizontal gap between adjacent chips, and between the last chip and
22
+ * the trigger. */
23
+ gap?: number;
24
+ };
25
+ type FacetOverflowPolicy = {
26
+ /** How many chips are visible. Counts a prefix of `chipWidths` in their
27
+ * given order when the active chip already falls inside that prefix;
28
+ * otherwise counts the active chip plus however many others (in order,
29
+ * skipping the active one) fit alongside it. `0` is the narrow-width
30
+ * floor: not even the active chip fits, so nothing but the trigger
31
+ * shows. */
32
+ visibleCount: number;
33
+ /** Whether the overflow trigger renders at all. */
34
+ showOverflowTrigger: boolean;
35
+ };
36
+ /**
37
+ * Decides the facet row's visible/overflow split.
38
+ *
39
+ * Narrow-width floor: if even the active chip cannot fit beside the
40
+ * trigger, showing it anyway would push the trigger past
41
+ * `availableWidth` — the row clips overflow, so the trigger (the only way
42
+ * to reach ANY facet from this row) would become unreachable. Rather than
43
+ * shrink the active chip below its natural width (this policy has no
44
+ * minimum-width input to shrink to), this drops the active chip from the
45
+ * visible row too and shows only the trigger: every facet, including the
46
+ * active one, still reaches through the overflow menu, and the trigger
47
+ * itself always stays on screen.
48
+ */
49
+ declare const resolveFacetOverflow: ({ availableWidth, chipWidths, activeIndex, triggerWidth, gap }: FacetOverflowInput) => FacetOverflowPolicy;
50
+ //#endregion
51
+ export { FacetOverflowInput, FacetOverflowPolicy, resolveFacetOverflow };
@@ -0,0 +1,53 @@
1
+ //#region src/inspector/facet-bar.logic.ts
2
+ const sumWithGaps = (widths, gap) => widths.reduce((sum, width, index) => sum + width + (index > 0 ? gap : 0), 0);
3
+ /**
4
+ * Decides the facet row's visible/overflow split.
5
+ *
6
+ * Narrow-width floor: if even the active chip cannot fit beside the
7
+ * trigger, showing it anyway would push the trigger past
8
+ * `availableWidth` — the row clips overflow, so the trigger (the only way
9
+ * to reach ANY facet from this row) would become unreachable. Rather than
10
+ * shrink the active chip below its natural width (this policy has no
11
+ * minimum-width input to shrink to), this drops the active chip from the
12
+ * visible row too and shows only the trigger: every facet, including the
13
+ * active one, still reaches through the overflow menu, and the trigger
14
+ * itself always stays on screen.
15
+ */
16
+ const resolveFacetOverflow = ({ availableWidth, chipWidths, activeIndex, triggerWidth, gap = 0 }) => {
17
+ if (sumWithGaps(chipWidths, gap) <= availableWidth) return {
18
+ visibleCount: chipWidths.length,
19
+ showOverflowTrigger: false
20
+ };
21
+ let used = 0;
22
+ let count = 0;
23
+ for (const width of chipWidths) {
24
+ const add = width + (count > 0 ? gap : 0);
25
+ if (used + add + gap + triggerWidth > availableWidth) break;
26
+ used += add;
27
+ count += 1;
28
+ }
29
+ if (activeIndex < count) return {
30
+ visibleCount: count,
31
+ showOverflowTrigger: true
32
+ };
33
+ const activeWidth = chipWidths[activeIndex] ?? 0;
34
+ if (activeWidth + gap + triggerWidth > availableWidth) return {
35
+ visibleCount: 0,
36
+ showOverflowTrigger: true
37
+ };
38
+ used = activeWidth;
39
+ count = 0;
40
+ for (const [index, width] of chipWidths.entries()) {
41
+ if (index === activeIndex) continue;
42
+ const add = width + gap;
43
+ if (used + add + gap + triggerWidth > availableWidth) break;
44
+ used += add;
45
+ count += 1;
46
+ }
47
+ return {
48
+ visibleCount: count + 1,
49
+ showOverflowTrigger: true
50
+ };
51
+ };
52
+ //#endregion
53
+ export { resolveFacetOverflow };
@@ -1,7 +1,9 @@
1
1
  import { Inspector, InspectorActions, InspectorContent, InspectorDescription, InspectorEmptyRow, InspectorHeader, InspectorHeaderText, InspectorProperty, InspectorPropertyLabel, InspectorPropertyList, InspectorPropertyValue, InspectorRail, InspectorRailCell, InspectorRailContent, InspectorRailFooter, InspectorRailIconButton, InspectorRailTab, InspectorSection, InspectorSectionTitle, InspectorTitle } from "./chrome.js";
2
2
  import { InspectorDock } from "./dock.js";
3
+ import { InspectorEntityTab, entityTabGlyph } from "./entity-tab.js";
4
+ import { InspectorFacetBar } from "./facet-bar.js";
3
5
  import { InspectorTab, InspectorTabList, InspectorTabPanel, InspectorTabs } from "./tabs.js";
4
6
  import { PROPERTY_ROW_GRID, SIDE_RAIL_CONTAINER_CLASS, SIDE_RAIL_ICON_BUTTON_SIZE, SIDE_RAIL_TAB_ICON_SIZE, SIDE_RAIL_WIDTH, TOOLBAR_ROW_HEIGHT, TOOLBAR_ROW_HEIGHT_PX } from "./layout-tokens.js";
5
7
  import { INSPECTOR_CONTENT_MIN_WIDTH, INSPECTOR_EDITOR_MIN_WIDTH, INSPECTOR_PANE_DEFAULT_WIDTH, INSPECTOR_PANE_MAX_WIDTH, INSPECTOR_PANE_MIN_WIDTH, INSPECTOR_RAIL_WIDTH, resolveInspectorDockWidth, resolveInspectorPaneMaxWidth, resolveInspectorPaneWidth, shouldForceSidebarCollapsed } from "./pane-width.js";
6
8
  import { INSPECTOR_PANE_KEYBOARD_PAGE_STEP, INSPECTOR_PANE_KEYBOARD_STEP, parsePersistedPaneWidth, resolveDragWidth, resolveKeyboardWidth, useInspectorPaneWidth } from "./use-pane-width.js";
7
- export { INSPECTOR_CONTENT_MIN_WIDTH, INSPECTOR_EDITOR_MIN_WIDTH, INSPECTOR_PANE_DEFAULT_WIDTH, INSPECTOR_PANE_KEYBOARD_PAGE_STEP, INSPECTOR_PANE_KEYBOARD_STEP, INSPECTOR_PANE_MAX_WIDTH, INSPECTOR_PANE_MIN_WIDTH, INSPECTOR_RAIL_WIDTH, Inspector, InspectorActions, InspectorContent, InspectorDescription, InspectorDock, InspectorEmptyRow, InspectorHeader, InspectorHeaderText, InspectorProperty, InspectorPropertyLabel, InspectorPropertyList, InspectorPropertyValue, InspectorRail, InspectorRailCell, InspectorRailContent, InspectorRailFooter, InspectorRailIconButton, InspectorRailTab, InspectorSection, InspectorSectionTitle, InspectorTab, InspectorTabList, InspectorTabPanel, InspectorTabs, InspectorTitle, PROPERTY_ROW_GRID, SIDE_RAIL_CONTAINER_CLASS, SIDE_RAIL_ICON_BUTTON_SIZE, SIDE_RAIL_TAB_ICON_SIZE, SIDE_RAIL_WIDTH, TOOLBAR_ROW_HEIGHT, TOOLBAR_ROW_HEIGHT_PX, parsePersistedPaneWidth, resolveDragWidth, resolveInspectorDockWidth, resolveInspectorPaneMaxWidth, resolveInspectorPaneWidth, resolveKeyboardWidth, shouldForceSidebarCollapsed, useInspectorPaneWidth };
9
+ export { INSPECTOR_CONTENT_MIN_WIDTH, INSPECTOR_EDITOR_MIN_WIDTH, INSPECTOR_PANE_DEFAULT_WIDTH, INSPECTOR_PANE_KEYBOARD_PAGE_STEP, INSPECTOR_PANE_KEYBOARD_STEP, INSPECTOR_PANE_MAX_WIDTH, INSPECTOR_PANE_MIN_WIDTH, INSPECTOR_RAIL_WIDTH, Inspector, InspectorActions, InspectorContent, InspectorDescription, InspectorDock, InspectorEmptyRow, InspectorEntityTab, InspectorFacetBar, InspectorHeader, InspectorHeaderText, InspectorProperty, InspectorPropertyLabel, InspectorPropertyList, InspectorPropertyValue, InspectorRail, InspectorRailCell, InspectorRailContent, InspectorRailFooter, InspectorRailIconButton, InspectorRailTab, InspectorSection, InspectorSectionTitle, InspectorTab, InspectorTabList, InspectorTabPanel, InspectorTabs, InspectorTitle, PROPERTY_ROW_GRID, SIDE_RAIL_CONTAINER_CLASS, SIDE_RAIL_ICON_BUTTON_SIZE, SIDE_RAIL_TAB_ICON_SIZE, SIDE_RAIL_WIDTH, TOOLBAR_ROW_HEIGHT, TOOLBAR_ROW_HEIGHT_PX, entityTabGlyph, parsePersistedPaneWidth, resolveDragWidth, resolveInspectorDockWidth, resolveInspectorPaneMaxWidth, resolveInspectorPaneWidth, resolveKeyboardWidth, shouldForceSidebarCollapsed, useInspectorPaneWidth };
@@ -2,6 +2,8 @@ import { PROPERTY_ROW_GRID, SIDE_RAIL_CONTAINER_CLASS, SIDE_RAIL_ICON_BUTTON_SIZ
2
2
  import { Inspector, InspectorActions, InspectorContent, InspectorDescription, InspectorEmptyRow, InspectorHeader, InspectorHeaderText, InspectorProperty, InspectorPropertyLabel, InspectorPropertyList, InspectorPropertyValue, InspectorRail, InspectorRailCell, InspectorRailContent, InspectorRailFooter, InspectorRailIconButton, InspectorRailTab, InspectorSection, InspectorSectionTitle, InspectorTitle } from "./chrome.js";
3
3
  import { INSPECTOR_CONTENT_MIN_WIDTH, INSPECTOR_EDITOR_MIN_WIDTH, INSPECTOR_PANE_DEFAULT_WIDTH, INSPECTOR_PANE_MAX_WIDTH, INSPECTOR_PANE_MIN_WIDTH, INSPECTOR_RAIL_WIDTH, resolveInspectorDockWidth, resolveInspectorPaneMaxWidth, resolveInspectorPaneWidth, shouldForceSidebarCollapsed } from "./pane-width.js";
4
4
  import { InspectorDock } from "./dock.js";
5
+ import { InspectorEntityTab, entityTabGlyph } from "./entity-tab.js";
6
+ import { InspectorFacetBar } from "./facet-bar.js";
5
7
  import { InspectorTab, InspectorTabList, InspectorTabPanel, InspectorTabs } from "./tabs.js";
6
8
  import { INSPECTOR_PANE_KEYBOARD_PAGE_STEP, INSPECTOR_PANE_KEYBOARD_STEP, parsePersistedPaneWidth, resolveDragWidth, resolveKeyboardWidth, useInspectorPaneWidth } from "./use-pane-width.js";
7
- export { INSPECTOR_CONTENT_MIN_WIDTH, INSPECTOR_EDITOR_MIN_WIDTH, INSPECTOR_PANE_DEFAULT_WIDTH, INSPECTOR_PANE_KEYBOARD_PAGE_STEP, INSPECTOR_PANE_KEYBOARD_STEP, INSPECTOR_PANE_MAX_WIDTH, INSPECTOR_PANE_MIN_WIDTH, INSPECTOR_RAIL_WIDTH, Inspector, InspectorActions, InspectorContent, InspectorDescription, InspectorDock, InspectorEmptyRow, InspectorHeader, InspectorHeaderText, InspectorProperty, InspectorPropertyLabel, InspectorPropertyList, InspectorPropertyValue, InspectorRail, InspectorRailCell, InspectorRailContent, InspectorRailFooter, InspectorRailIconButton, InspectorRailTab, InspectorSection, InspectorSectionTitle, InspectorTab, InspectorTabList, InspectorTabPanel, InspectorTabs, InspectorTitle, PROPERTY_ROW_GRID, SIDE_RAIL_CONTAINER_CLASS, SIDE_RAIL_ICON_BUTTON_SIZE, SIDE_RAIL_TAB_ICON_SIZE, SIDE_RAIL_WIDTH, TOOLBAR_ROW_HEIGHT, TOOLBAR_ROW_HEIGHT_PX, parsePersistedPaneWidth, resolveDragWidth, resolveInspectorDockWidth, resolveInspectorPaneMaxWidth, resolveInspectorPaneWidth, resolveKeyboardWidth, shouldForceSidebarCollapsed, useInspectorPaneWidth };
9
+ export { INSPECTOR_CONTENT_MIN_WIDTH, INSPECTOR_EDITOR_MIN_WIDTH, INSPECTOR_PANE_DEFAULT_WIDTH, INSPECTOR_PANE_KEYBOARD_PAGE_STEP, INSPECTOR_PANE_KEYBOARD_STEP, INSPECTOR_PANE_MAX_WIDTH, INSPECTOR_PANE_MIN_WIDTH, INSPECTOR_RAIL_WIDTH, Inspector, InspectorActions, InspectorContent, InspectorDescription, InspectorDock, InspectorEmptyRow, InspectorEntityTab, InspectorFacetBar, InspectorHeader, InspectorHeaderText, InspectorProperty, InspectorPropertyLabel, InspectorPropertyList, InspectorPropertyValue, InspectorRail, InspectorRailCell, InspectorRailContent, InspectorRailFooter, InspectorRailIconButton, InspectorRailTab, InspectorSection, InspectorSectionTitle, InspectorTab, InspectorTabList, InspectorTabPanel, InspectorTabs, InspectorTitle, PROPERTY_ROW_GRID, SIDE_RAIL_CONTAINER_CLASS, SIDE_RAIL_ICON_BUTTON_SIZE, SIDE_RAIL_TAB_ICON_SIZE, SIDE_RAIL_WIDTH, TOOLBAR_ROW_HEIGHT, TOOLBAR_ROW_HEIGHT_PX, entityTabGlyph, parsePersistedPaneWidth, resolveDragWidth, resolveInspectorDockWidth, resolveInspectorPaneMaxWidth, resolveInspectorPaneWidth, resolveKeyboardWidth, shouldForceSidebarCollapsed, useInspectorPaneWidth };
@@ -8,7 +8,7 @@ import { Tabs } from "@base-ui/react/tabs";
8
8
  */
9
9
  declare const InspectorTabs: ({ className, ...props }: Omit<Tabs.Root.Props, "orientation">) => React$1.JSX.Element;
10
10
  declare const INSPECTOR_RAIL_MEDIA_QUERY: "(min-width: 48rem)";
11
- declare const resolveInspectorTabOrientation: (isRailLayout: boolean) => "horizontal" | "vertical";
11
+ declare const resolveInspectorTabOrientation: (isRailLayout: boolean) => "vertical" | "horizontal";
12
12
  declare const InspectorTabList: ({ className, ...props }: Tabs.List.Props) => React$1.JSX.Element;
13
13
  declare const InspectorTab: ({ className, ...props }: Tabs.Tab.Props) => React$1.JSX.Element;
14
14
  declare const InspectorTabPanel: ({ className, ...props }: Tabs.Panel.Props) => React$1.JSX.Element;
@@ -1,25 +1,22 @@
1
1
  //#region src/kanban/band-peek.d.ts
2
2
  /**
3
- * The peek a collapsed band opens while a pointer rests on its folded slot,
4
- * as a state machine with an injectable scheduler so its timing rules can be
5
- * tested without a DOM.
3
+ * The peek a collapsed band opens while a dragged card rests on its folded
4
+ * slot, as a state machine with an injectable scheduler so its timing rules
5
+ * can be tested without a DOM. A plain hover never peeks: the peek exists so
6
+ * a drag can still land on a specific column inside a folded band, and the
7
+ * board only feeds the controller drag events.
6
8
  *
7
- * Two rules keep a peek from fighting the pointer:
8
- *
9
- * - A slot that appeared under the pointer does not peek. Folding a band
10
- * from its caption leaves the new slot right under the cursor; the first
11
- * movement there must not reopen what was just closed. The band is
12
- * suppressed until the pointer leaves the slot once.
13
- * - A peek ends only after the pointer has left every part of the open
14
- * band for a short linger. The band renders as separate elements (its
15
- * caption, then its columns in each lane), so moving from the caption
16
- * down into a column crosses an element boundary; without the linger the
17
- * band would fold under the pointer and the slot would peek it straight
18
- * back open.
9
+ * One rule keeps a peek from fighting the drag: it ends only after the drag
10
+ * has left every part of the open band for a short linger. The band renders
11
+ * as separate elements (its caption, then its columns in each lane), so
12
+ * moving from the caption down into a column crosses an element boundary;
13
+ * without the linger the band would fold under the card and the slot would
14
+ * peek it straight back open. The end of the drag, wherever it lands, ends
15
+ * the peek at once.
19
16
  */
20
- /** How long a pointer rests on a folded slot before the band peeks open. */
17
+ /** How long a dragged card rests on a folded slot before the band peeks open. */
21
18
  declare const KANBAN_BAND_PEEK_DELAY_MS = 400;
22
- /** How long the pointer may be outside an open band before the peek ends. */
19
+ /** How long the drag may be outside an open band before the peek ends. */
23
20
  declare const KANBAN_BAND_PEEK_LINGER_MS = 150;
24
21
  /** Runs `callback` after `ms`; the returned function cancels it. */
25
22
  type BandPeekScheduler = (callback: () => void, ms: number) => () => void;
@@ -32,21 +29,18 @@ type BandPeekControllerOptions = {
32
29
  schedule?: BandPeekScheduler;
33
30
  };
34
31
  type BandPeekController = {
35
- /** The pointer moved inside a band's folded slot. */
36
- slotPointerMove: (bandId: string) => void;
37
- /** The pointer left a band's folded slot. */
38
- slotPointerLeave: (bandId: string) => void;
39
- /** The pointer entered a part of a band that is rendered open. */
40
- openPointerEnter: (bandId: string) => void;
41
- /** The pointer left a part of a band that is rendered open. */
42
- openPointerLeave: (bandId: string) => void;
43
- /**
44
- * The band was folded by a pointer on its caption, so its slot now sits
45
- * under the pointer; it must not peek until the pointer leaves the slot.
46
- */
47
- foldedUnderPointer: (bandId: string) => void;
32
+ /** A dragged card moved over a band's folded slot. */
33
+ slotDragOver: (bandId: string) => void;
34
+ /** The drag left a band's folded slot. */
35
+ slotDragLeave: (bandId: string) => void;
36
+ /** The drag entered a part of a band that is rendered open. */
37
+ openDragEnter: (bandId: string) => void;
38
+ /** The drag left a part of a band that is rendered open. */
39
+ openDragLeave: (bandId: string) => void;
40
+ /** The drag ended (dropped or cancelled), wherever it was. */
41
+ dragEnded: () => void;
48
42
  /**
49
- * The band was folded without a pointer on it (keyboard, or a controlled
43
+ * The band was folded (from its caption, the keyboard, or a controlled
50
44
  * caller): any peek ends, and the slot may peek on the next hover.
51
45
  */
52
46
  bandFolded: (bandId: string) => void;
@@ -54,8 +48,7 @@ type BandPeekController = {
54
48
  bandExpanded: (bandId: string) => void;
55
49
  /**
56
50
  * A band's folded slot left the DOM (the band expanded or disappeared) so
57
- * a peek it was about to open, or a suppression it carried, no longer
58
- * applies.
51
+ * a peek it was about to open no longer applies.
59
52
  */
60
53
  slotUnmounted: (bandId: string) => void;
61
54
  /** Ends any pending timer, for unmount. */