@stll/ui 0.17.0 → 0.17.1

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.
@@ -10,8 +10,8 @@
10
10
  */
11
11
  declare const buttonAccessibleDisabledClass = "cursor-not-allowed opacity-64";
12
12
  declare const buttonVariants: (props?: ({
13
- size?: "xs" | "sm" | "icon" | "default" | "icon-lg" | "icon-sm" | "icon-xl" | "icon-xs" | "lg" | "xl" | null | undefined;
14
- variant?: "destructive" | "outline" | "link" | "default" | "destructive-outline" | "ghost" | "secondary" | null | undefined;
13
+ size?: "sm" | "default" | "lg" | "xs" | "icon" | "icon-lg" | "icon-sm" | "icon-xl" | "icon-xs" | "xl" | null | undefined;
14
+ variant?: "link" | "default" | "destructive" | "outline" | "destructive-outline" | "ghost" | "secondary" | null | undefined;
15
15
  } & import("class-variance-authority/types").ClassProp) | undefined) => string;
16
16
  //#endregion
17
17
  export { buttonAccessibleDisabledClass, buttonVariants };
@@ -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;
@@ -7,8 +7,14 @@ type KanbanColumnBandHeaderProps = {
7
7
  swatch?: ReactNode;
8
8
  /** Short text after the name, such as the band's card count. */
9
9
  meta?: ReactNode;
10
- /** Whether the band's columns are shown; drives the toggle and its icon. */
10
+ /** The band's persisted state; drives the toggle, its icon, and its label. */
11
11
  collapsed: boolean;
12
+ /**
13
+ * Render only the toggle, for the narrow slot of a folded band. Defaults to
14
+ * `collapsed`; a board that peeks a collapsed band open passes `false` so
15
+ * the caption shows the name while the toggle still offers to pin it open.
16
+ */
17
+ compact?: boolean | undefined;
12
18
  /** Accessible name for the toggle, such as "Collapse To do". */
13
19
  toggleLabel: string;
14
20
  onCollapsedChange: (collapsed: boolean) => void;
@@ -16,12 +22,11 @@ type KanbanColumnBandHeaderProps = {
16
22
  actions?: ReactNode;
17
23
  };
18
24
  /**
19
- * The band header row above a run of columns: the same rhythm as
20
- * `KanbanColumnHeader` (swatch, name, count, controls), plus the collapse
21
- * toggle that folds the run into one narrow lane. Collapsed, the header is
22
- * the only thing left of the band, so it also carries the band's identity on
23
- * `aria-expanded` for the columns it hides.
25
+ * The band line above a run of columns: one 28px row of toggle, swatch, name,
26
+ * and count, so a band costs the board a caption's height and nothing more.
27
+ * Folded, only the toggle remains in this line; the band's name moves down
28
+ * into the narrow column body, where the height is already there.
24
29
  */
25
- declare const KanbanColumnBandHeader: ({ title, swatch, meta, collapsed, toggleLabel, onCollapsedChange, actions }: KanbanColumnBandHeaderProps) => import("react").JSX.Element;
30
+ declare const KanbanColumnBandHeader: ({ title, swatch, meta, collapsed, compact, toggleLabel, onCollapsedChange, actions }: KanbanColumnBandHeaderProps) => import("react").JSX.Element;
26
31
  //#endregion
27
32
  export { KanbanColumnBandHeader, KanbanColumnBandHeaderProps };
@@ -1,42 +1,42 @@
1
1
  import { cn } from "../lib/utils.js";
2
2
  import { DirectionalIcon } from "../components/directional-icon.js";
3
3
  import { ChevronDownIcon } from "lucide-react";
4
- import { jsx, jsxs } from "react/jsx-runtime";
4
+ import { Fragment, jsx, jsxs } from "react/jsx-runtime";
5
5
  //#region src/kanban/column-band-header.tsx
6
6
  /**
7
- * The band header row above a run of columns: the same rhythm as
8
- * `KanbanColumnHeader` (swatch, name, count, controls), plus the collapse
9
- * toggle that folds the run into one narrow lane. Collapsed, the header is
10
- * the only thing left of the band, so it also carries the band's identity on
11
- * `aria-expanded` for the columns it hides.
7
+ * The band line above a run of columns: one 28px row of toggle, swatch, name,
8
+ * and count, so a band costs the board a caption's height and nothing more.
9
+ * Folded, only the toggle remains in this line; the band's name moves down
10
+ * into the narrow column body, where the height is already there.
12
11
  */
13
- const KanbanColumnBandHeader = ({ title, swatch, meta, collapsed, toggleLabel, onCollapsedChange, actions }) => /* @__PURE__ */ jsxs("div", {
14
- className: cn("flex items-center gap-2 py-2", collapsed ? "flex-col px-1" : "px-3"),
12
+ const KanbanColumnBandHeader = ({ title, swatch, meta, collapsed, compact = collapsed, toggleLabel, onCollapsedChange, actions }) => /* @__PURE__ */ jsxs("div", {
13
+ className: cn("flex h-7 items-center gap-1", compact ? "justify-center" : "pe-1"),
15
14
  "data-collapsed": collapsed ? "" : void 0,
15
+ "data-compact": compact ? "" : void 0,
16
16
  "data-slot": "kanban-column-band-header",
17
- children: [
18
- /* @__PURE__ */ jsx("button", {
19
- "aria-expanded": !collapsed,
20
- "aria-label": toggleLabel,
21
- className: "hover:bg-muted/60 text-muted-foreground hover:text-foreground flex size-8 shrink-0 items-center justify-center rounded-md transition-[background-color]",
22
- onClick: () => onCollapsedChange(!collapsed),
23
- type: "button",
24
- children: /* @__PURE__ */ jsx(DirectionalIcon, {
25
- className: cn("size-4 transition-transform", collapsed && "-rotate-90"),
26
- flip: collapsed,
27
- icon: ChevronDownIcon
28
- })
29
- }),
17
+ children: [/* @__PURE__ */ jsx("button", {
18
+ "aria-expanded": !collapsed,
19
+ "aria-label": toggleLabel,
20
+ className: "hover:bg-muted/60 text-muted-foreground hover:text-foreground flex size-6 shrink-0 items-center justify-center rounded-md transition-[background-color]",
21
+ onClick: () => onCollapsedChange(!collapsed),
22
+ title: toggleLabel,
23
+ type: "button",
24
+ children: /* @__PURE__ */ jsx(DirectionalIcon, {
25
+ className: cn("size-3.5 transition-transform", collapsed && "-rotate-90"),
26
+ flip: collapsed,
27
+ icon: ChevronDownIcon
28
+ })
29
+ }), compact ? null : /* @__PURE__ */ jsxs(Fragment, { children: [
30
30
  swatch,
31
31
  /* @__PURE__ */ jsxs("span", {
32
- className: cn("flex min-w-0 items-center gap-1.5 text-sm font-medium", collapsed ? "rotate-180 [writing-mode:vertical-rl]" : "flex-1 truncate"),
32
+ className: "flex min-w-0 flex-1 items-baseline gap-1.5 truncate text-xs font-medium",
33
33
  children: [title, meta !== void 0 && /* @__PURE__ */ jsx("span", {
34
- className: "text-muted-foreground text-xs tabular-nums",
34
+ className: "text-muted-foreground font-normal tabular-nums",
35
35
  children: meta
36
36
  })]
37
37
  }),
38
- collapsed ? null : actions
39
- ]
38
+ actions
39
+ ] })]
40
40
  });
41
41
  //#endregion
42
42
  export { KanbanColumnBandHeader };
@@ -23,7 +23,14 @@ type KanbanSubgroupBandHeaderContext = {
23
23
  columns: KanbanBoardColumn[];
24
24
  /** Rows across the band's columns, every lane included. */
25
25
  count: number;
26
+ /** The persisted state the toggle reports and flips. */
26
27
  collapsed: boolean;
28
+ /**
29
+ * Whether the band renders as one narrow slot right now. False while a
30
+ * collapsed band peeks open under the pointer, so a caption can show its
31
+ * name and offer to pin the band open.
32
+ */
33
+ folded: boolean;
27
34
  onCollapsedChange: (collapsed: boolean) => void;
28
35
  };
29
36
  /**
@@ -50,13 +57,14 @@ type KanbanSubgroupBoardProps<TRow> = {
50
57
  renderLaneIdentity: (context: KanbanSubgroupLaneIdentityContext) => ReactNode;
51
58
  renderCell: (context: KanbanSubgroupCellContext<TRow>) => ReactNode;
52
59
  /**
53
- * The header above a band's columns. Defaults to `KanbanColumnBandHeader`
60
+ * The line above a band's columns. Defaults to `KanbanColumnBandHeader`
54
61
  * with the band's label and count.
55
62
  */
56
63
  renderBandHeader?: ((context: KanbanSubgroupBandHeaderContext) => ReactNode) | undefined;
57
64
  /**
58
- * A lane's slot while its band is collapsed. Defaults to the count alone;
59
- * a host that accepts drops into a collapsed band renders its target here.
65
+ * A lane's slot while its band is collapsed. Defaults to the band's name
66
+ * set vertically over the count; a host that accepts drops into a collapsed
67
+ * band renders its target here.
60
68
  */
61
69
  renderCollapsedBandCell?: ((context: KanbanSubgroupCollapsedBandCellContext<TRow>) => ReactNode) | undefined;
62
70
  /** Accessible name for a band's collapse toggle. */
@@ -74,12 +82,14 @@ type KanbanSubgroupBoardProps<TRow> = {
74
82
  /**
75
83
  * Reusable swimlane layout over the canonical two-axis Kanban matrix.
76
84
  *
77
- * Columns that carry band metadata render under a band header and can be
78
- * collapsed as a run: the band folds into one narrow slot in every row, whose
79
- * cells stay reachable (a host renders its drop target in them), and peeks
80
- * open while a pointer rests on it, so a drag can still land on a specific
81
- * column inside. Every row is laid out span by span with the same widths,
82
- * which is what keeps headers, counts, and cells aligned in every state.
85
+ * Columns that carry band metadata render under a one-line band caption and
86
+ * can be collapsed as a run: the band folds into one narrow slot in every
87
+ * row, whose cells stay reachable (a host renders its drop target in them),
88
+ * and peeks open while a pointer rests on it, so a drag can still land on a
89
+ * specific column inside. Every row is laid out span by span with the same
90
+ * widths, which is what keeps captions, headers, counts, and cells aligned
91
+ * in every state; the caption line never grows past its own height, so a
92
+ * folded band costs no vertical space.
83
93
  */
84
94
  declare const KanbanSubgroupBoard: <TRow>({ matrix, renderColumnHeader, renderLaneIdentity, renderCell, renderBandHeader, renderCollapsedBandCell, formatBandToggleLabel, formatCount, isLaneCollapsed, onLaneCollapsedChange, isBandCollapsed, onBandCollapsedChange, footer, className }: KanbanSubgroupBoardProps<TRow>) => import("react").JSX.Element;
85
95
  //#endregion
@@ -15,12 +15,14 @@ const KANBAN_BAND_PEEK_DELAY_MS = 400;
15
15
  /**
16
16
  * Reusable swimlane layout over the canonical two-axis Kanban matrix.
17
17
  *
18
- * Columns that carry band metadata render under a band header and can be
19
- * collapsed as a run: the band folds into one narrow slot in every row, whose
20
- * cells stay reachable (a host renders its drop target in them), and peeks
21
- * open while a pointer rests on it, so a drag can still land on a specific
22
- * column inside. Every row is laid out span by span with the same widths,
23
- * which is what keeps headers, counts, and cells aligned in every state.
18
+ * Columns that carry band metadata render under a one-line band caption and
19
+ * can be collapsed as a run: the band folds into one narrow slot in every
20
+ * row, whose cells stay reachable (a host renders its drop target in them),
21
+ * and peeks open while a pointer rests on it, so a drag can still land on a
22
+ * specific column inside. Every row is laid out span by span with the same
23
+ * widths, which is what keeps captions, headers, counts, and cells aligned
24
+ * in every state; the caption line never grows past its own height, so a
25
+ * folded band costs no vertical space.
24
26
  */
25
27
  const KanbanSubgroupBoard = ({ matrix, renderColumnHeader, renderLaneIdentity, renderCell, renderBandHeader, renderCollapsedBandCell, formatBandToggleLabel, formatCount = String, isLaneCollapsed, onLaneCollapsedChange, isBandCollapsed, onBandCollapsedChange, footer, className }) => {
26
28
  const [collapsedLaneValues, setCollapsedLaneValues] = useState(() => /* @__PURE__ */ new Set());
@@ -73,10 +75,14 @@ const KanbanSubgroupBoard = ({ matrix, renderColumnHeader, renderLaneIdentity, r
73
75
  return next;
74
76
  });
75
77
  };
76
- const isBandFolded = (band) => {
77
- if (peekingBandId === band.id) return false;
78
- return isBandCollapsed ? isBandCollapsed(band) : collapsedBandIds.has(band.id);
79
- };
78
+ /** The band's persisted state: what the toggle reports and flips. */
79
+ const isBandCollapsedNow = (band) => isBandCollapsed ? isBandCollapsed(band) : collapsedBandIds.has(band.id);
80
+ /**
81
+ * Whether the band renders folded right now. A peek opens the layout
82
+ * without changing the persisted state, so a peeked band still reports
83
+ * itself collapsed and its toggle pins it open rather than closing it.
84
+ */
85
+ const isBandFolded = (band) => peekingBandId !== band.id && isBandCollapsedNow(band);
80
86
  const setBandCollapsed = (band, collapsed) => {
81
87
  setPeekingBandId(null);
82
88
  if (onBandCollapsedChange) {
@@ -122,17 +128,20 @@ const KanbanSubgroupBoard = ({ matrix, renderColumnHeader, renderLaneIdentity, r
122
128
  })
123
129
  });
124
130
  const bandHeader = (band, span) => {
125
- const collapsed = isBandFolded(band);
131
+ const collapsed = isBandCollapsedNow(band);
132
+ const folded = isBandFolded(band);
126
133
  const context = {
127
134
  band,
128
135
  collapsed,
129
136
  columns: span.columns,
130
137
  count: span.columns.reduce((sum, column) => sum + columnCount(column), 0),
138
+ folded,
131
139
  onCollapsedChange: (next) => setBandCollapsed(band, next)
132
140
  };
133
141
  if (renderBandHeader) return /* @__PURE__ */ jsx(Fragment, { children: renderBandHeader(context) });
134
142
  return /* @__PURE__ */ jsx(KanbanColumnBandHeader, {
135
143
  collapsed,
144
+ compact: folded,
136
145
  meta: formatCount(context.count),
137
146
  title: band.label,
138
147
  toggleLabel: formatBandToggleLabel ? formatBandToggleLabel(band, collapsed) : band.label,
@@ -148,10 +157,16 @@ const KanbanSubgroupBoard = ({ matrix, renderColumnHeader, renderLaneIdentity, r
148
157
  count,
149
158
  laneValue
150
159
  }) });
151
- return /* @__PURE__ */ jsx("div", {
152
- className: "text-muted-foreground flex justify-center px-1 py-2 text-xs tabular-nums",
160
+ return /* @__PURE__ */ jsxs("div", {
161
+ className: "text-muted-foreground flex flex-col items-center gap-2 py-2 text-xs",
153
162
  "data-kanban-collapsed-band-count": count,
154
- children: formatCount(count)
163
+ children: [/* @__PURE__ */ jsx("span", {
164
+ className: "max-h-40 truncate font-medium [writing-mode:vertical-rl]",
165
+ children: band.label
166
+ }), /* @__PURE__ */ jsx("span", {
167
+ className: "tabular-nums",
168
+ children: formatCount(count)
169
+ })]
155
170
  });
156
171
  };
157
172
  const foldedCount = (span, cells) => {
@@ -173,7 +188,7 @@ const KanbanSubgroupBoard = ({ matrix, renderColumnHeader, renderLaneIdentity, r
173
188
  /* @__PURE__ */ jsxs("div", {
174
189
  className: "bg-background sticky top-0 z-20 pt-4 pb-3",
175
190
  children: [hasBands ? /* @__PURE__ */ jsx("div", {
176
- className: "flex gap-3 pb-2",
191
+ className: "flex items-end gap-3 pb-1.5",
177
192
  "data-kanban-band-row": "",
178
193
  children: spans.map((span) => {
179
194
  const band = span.band;
@@ -183,13 +198,13 @@ const KanbanSubgroupBoard = ({ matrix, renderColumnHeader, renderLaneIdentity, r
183
198
  }, spanKey(span));
184
199
  if (isBandFolded(band)) return /* @__PURE__ */ jsx(FoldedBandSlot, {
185
200
  band,
186
- className: cn("border-border/60 bg-muted/40 rounded-lg border", KANBAN_COLLAPSED_BAND_WIDTH_CLASS),
201
+ className: cn("border-border/60 border-b", KANBAN_COLLAPSED_BAND_WIDTH_CLASS),
187
202
  onPeek: setPeekingBandId,
188
203
  onPeekEnd: endPeek,
189
204
  children: bandHeader(band, span)
190
205
  }, spanKey(span));
191
206
  return /* @__PURE__ */ jsx("div", {
192
- className: "border-border/60 bg-muted/40 shrink-0 rounded-lg border",
207
+ className: "border-border/60 shrink-0 border-b",
193
208
  "data-kanban-band": band.id,
194
209
  style: { width: `${String(spanWidth(span))}px` },
195
210
  onPointerLeave: peekingBandId === band.id ? endPeek : void 0,
@@ -290,6 +305,8 @@ const KanbanSubgroupBoard = ({ matrix, renderColumnHeader, renderLaneIdentity, r
290
305
  * The narrow slot a folded band occupies in a row. A pointer resting on it
291
306
  * for `KANBAN_BAND_PEEK_DELAY_MS` peeks the band open; leaving ends the peek
292
307
  * and cancels a pending one, so a drag passing over it does not unfold it.
308
+ * The pointer must enter the slot after it appeared: a band folded under a
309
+ * resting pointer does not peek straight back open.
293
310
  */
294
311
  const FoldedBandSlot = ({ band, children, className, onPeek, onPeekEnd }) => {
295
312
  const timer = useRef(null);
@@ -300,8 +317,8 @@ const FoldedBandSlot = ({ band, children, className, onPeek, onPeekEnd }) => {
300
317
  className,
301
318
  "data-kanban-band": band.id,
302
319
  "data-kanban-band-collapsed": "",
303
- onPointerEnter: () => {
304
- if (timer.current !== null) clearTimeout(timer.current);
320
+ onPointerMove: () => {
321
+ if (timer.current !== null) return;
305
322
  timer.current = setTimeout(() => {
306
323
  timer.current = null;
307
324
  onPeek(band.id);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@stll/ui",
3
- "version": "0.17.0",
3
+ "version": "0.17.1",
4
4
  "description": "Stella's design system: bidi-aware React primitives built on Base UI, the dockable inspector pane, and the Tailwind v4 theme they are styled with.",
5
5
  "keywords": [
6
6
  "base-ui",