@stll/ui 0.67.0 → 0.69.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 (53) hide show
  1. package/dist/calendar/day-calendar.d.ts +22 -6
  2. package/dist/calendar/day-calendar.js +97 -26
  3. package/dist/calendar/resource-calendar.js +7 -9
  4. package/dist/calendar/topic-colors.d.ts +3 -0
  5. package/dist/calendar/topic-colors.js +19 -0
  6. package/dist/components/button.js +4 -4
  7. package/dist/components/composer-tokens.d.ts +6 -2
  8. package/dist/components/composer-tokens.js +6 -2
  9. package/dist/components/composer.d.ts +13 -0
  10. package/dist/components/composer.js +19 -3
  11. package/dist/components/court-filter.js +4 -4
  12. package/dist/components/date-picker-popover.js +3 -2
  13. package/dist/components/document-identity-badge.js +9 -4
  14. package/dist/components/file-input.js +3 -2
  15. package/dist/components/identity-text.js +4 -2
  16. package/dist/components/input-group.js +3 -2
  17. package/dist/components/landing.js +6 -5
  18. package/dist/components/list.d.ts +11 -5
  19. package/dist/components/list.js +28 -9
  20. package/dist/components/loader.js +4 -2
  21. package/dist/components/outline-rail.js +6 -4
  22. package/dist/components/run-state-mark.d.ts +8 -0
  23. package/dist/components/run-state-mark.js +30 -0
  24. package/dist/components/run-state-mark.native.d.ts +9 -0
  25. package/dist/components/run-state-mark.native.js +98 -0
  26. package/dist/components/run-state-mark.shared.d.ts +53 -0
  27. package/dist/components/run-state-mark.shared.js +45 -0
  28. package/dist/components/status-mark.js +6 -6
  29. package/dist/components/status-mark.shared.d.ts +12 -0
  30. package/dist/components/status-mark.shared.js +14 -0
  31. package/dist/components/table.d.ts +1 -1
  32. package/dist/components/table.js +14 -8
  33. package/dist/components/text.native.d.ts +3 -1
  34. package/dist/components/text.native.js +4 -2
  35. package/dist/components/truncated-text.d.ts +14 -3
  36. package/dist/components/truncated-text.js +21 -5
  37. package/dist/components/view-toolbar.d.ts +5 -1
  38. package/dist/components/view-toolbar.js +6 -2
  39. package/dist/index.d.ts +2 -2
  40. package/dist/index.js +2 -2
  41. package/dist/inspector/chrome.d.ts +3 -3
  42. package/dist/inspector/chrome.js +28 -17
  43. package/dist/kanban/column-band-header.js +3 -2
  44. package/dist/kanban/column-header.js +3 -2
  45. package/dist/kanban/sticky-lane.js +3 -2
  46. package/dist/lib/matter-colors.js +2 -5
  47. package/dist/lib/stable-hash.d.ts +2 -0
  48. package/dist/lib/stable-hash.js +8 -0
  49. package/dist/review/review-comment-card.js +7 -6
  50. package/dist/review/review-decision-actions.d.ts +8 -1
  51. package/dist/review/review-decision-actions.js +13 -4
  52. package/dist/styles/theme.css +24 -0
  53. package/package.json +11 -3
@@ -1,9 +1,10 @@
1
1
  import { DayCalendarGranularity, DayCalendarRange } from "./day-calendar.logic.js";
2
2
  import { ReactElement, ReactNode } from "react";
3
3
  /**
4
- * A block paints in its matter's colour; in a topic's colour, a lighter tint
5
- * from the same palette keyed by a stable topic key and without the matter's
6
- * edge; or in the neutral surface tone.
4
+ * A block paints in its matter's colour, with the matter's edge; in a topic's
5
+ * colour, from the separate low-chroma topic set keyed by a stable topic key
6
+ * and without an edge, so it never reads as a matter; or in the neutral
7
+ * surface tone.
7
8
  */
8
9
  export type DayCalendarTone = {
9
10
  color: string | null;
@@ -21,22 +22,37 @@ export type DayCalendarTone = {
21
22
  * button the caller acts on.
22
23
  */
23
24
  export type DayCalendarEntryState = "draft" | "final" | "suggestion";
25
+ /**
26
+ * What a block shows on hover, for example the apps a `mixed` block merged.
27
+ * `actions` holds controls: it is a popover that also opens on focus or
28
+ * press and stays open while the pointer is on it, so its block is a button.
29
+ */
30
+ export type DayCalendarBlockTooltip = {
31
+ content: ReactNode;
32
+ type: "actions";
33
+ } | {
34
+ content: ReactNode;
35
+ type: "text";
36
+ };
24
37
  type DayCalendarBlockFields = DayCalendarRange & {
25
38
  accessibleLabel: string;
26
39
  caption?: ReactNode;
27
40
  id: string;
28
41
  title: ReactNode;
29
42
  tone: DayCalendarTone;
30
- /** Shown on hover, for example the apps a `mixed` block merged. */
31
- tooltip?: ReactNode;
43
+ tooltip?: DayCalendarBlockTooltip;
32
44
  };
33
45
  export type DayCalendarBlock = (DayCalendarBlockFields & {
34
46
  kind: "entry";
35
47
  state: Exclude<DayCalendarEntryState, "suggestion">;
36
- }) | (DayCalendarBlockFields & {
48
+ }) | (Omit<DayCalendarBlockFields, "tooltip"> & {
37
49
  kind: "entry";
38
50
  onActivate: () => void;
39
51
  state: "suggestion";
52
+ /** A suggestion is already a button; its hover only describes it. */
53
+ tooltip?: Extract<DayCalendarBlockTooltip, {
54
+ type: "text";
55
+ }>;
40
56
  }) | (DayCalendarBlockFields & {
41
57
  kind: "mixed";
42
58
  }) | (DayCalendarBlockFields & {
@@ -1,7 +1,10 @@
1
+ import { Popover, PopoverContent as PopoverPopup, PopoverTrigger } from "../components/popover.js";
1
2
  import { Tooltip, TooltipContent as TooltipPopup, TooltipTrigger } from "../components/tooltip.js";
2
- import { getMatterColor, resolveMatterColor } from "../lib/matter-colors.js";
3
+ import { TruncatedText } from "../components/truncated-text.js";
4
+ import { resolveMatterColor } from "../lib/matter-colors.js";
3
5
  import { cn } from "../lib/utils.js";
4
6
  import { DAY_CALENDAR_SLOT_HEIGHT_PX, boundsHeight, extendRange, getDayBounds, getHourTicks, isRepeatedWallTime, layoutOverlaps, moveRange, offsetToTime, rangeFromDrag, slotAt, timeToOffset } from "./day-calendar.logic.js";
7
+ import { getTopicColor } from "./topic-colors.js";
5
8
  import { Fragment, jsx, jsxs } from "react/jsx-runtime";
6
9
  import { useState } from "react";
7
10
  import { panic } from "better-result";
@@ -81,7 +84,7 @@ const DayCalendar = ({ ariaLabel, columns, date, granularity, hours, locale, now
81
84
  const nowVisible = nowMs >= bounds.startMs && nowMs < bounds.endMs;
82
85
  const timeAt = (event) => offsetToTime(event.clientY - event.currentTarget.getBoundingClientRect().top, grid);
83
86
  const handlePointerDown = (columnId, event) => {
84
- if (event.button !== 0) return;
87
+ if (event.button !== 0 || !isWithinColumn(event)) return;
85
88
  event.currentTarget.setPointerCapture(event.pointerId);
86
89
  const anchorMs = timeAt(event);
87
90
  setDraft({
@@ -127,6 +130,7 @@ const DayCalendar = ({ ariaLabel, columns, date, granularity, hours, locale, now
127
130
  return slotAt(nowVisible ? nowMs : bounds.startMs, grid);
128
131
  };
129
132
  const handleKeyDown = (columnId, event) => {
133
+ if (event.target !== event.currentTarget) return;
130
134
  const current = draft?.columnId === columnId ? draft : null;
131
135
  switch (event.key) {
132
136
  case "ArrowDown":
@@ -186,11 +190,11 @@ const DayCalendar = ({ ariaLabel, columns, date, granularity, hours, locale, now
186
190
  columns.map((column, index) => /* @__PURE__ */ jsxs("header", {
187
191
  className: cn("bg-card border-border text-muted-foreground sticky top-0 z-30 flex min-w-0 items-center gap-2 border-s border-b px-3 py-2.5 text-xs font-medium", index === columns.length - 1 && "rounded-se-xl"),
188
192
  "data-slot": "day-calendar-column-header",
189
- children: [/* @__PURE__ */ jsx("span", {
190
- className: "min-w-0 shrink-0 truncate",
193
+ children: [/* @__PURE__ */ jsx(TruncatedText, {
194
+ className: "shrink-0",
191
195
  children: column.label
192
- }), column.badge === void 0 ? null : /* @__PURE__ */ jsx("span", {
193
- className: "bg-muted text-muted-foreground text-2xs min-w-0 truncate rounded-full px-2 py-0.5",
196
+ }), column.badge === void 0 ? null : /* @__PURE__ */ jsx(TruncatedText, {
197
+ className: "bg-muted text-muted-foreground text-2xs rounded-full px-2 py-0.5",
194
198
  children: column.badge
195
199
  })]
196
200
  }, column.id)),
@@ -306,6 +310,11 @@ const ColumnBlocks = ({ blocks, grid }) => {
306
310
  }, block.id);
307
311
  });
308
312
  };
313
+ /** A block's popover is portalled out of the column, yet its events still
314
+ * bubble to the column through React. */
315
+ function isWithinColumn({ currentTarget, target }) {
316
+ return target instanceof Node && currentTarget.contains(target);
317
+ }
309
318
  /** Below this height a block puts its title and caption on one line. */
310
319
  const TWO_LINE_BLOCK_MIN_PX = 40;
311
320
  /** A brief block paints at least this tall, unless the grid ends sooner. */
@@ -329,12 +338,15 @@ const DayCalendarBlockView = ({ block, grid, lane, range }) => {
329
338
  insetBlockStart: top,
330
339
  insetInlineStart: `${String(lane.lane * 100 / lane.laneCount)}%`
331
340
  };
332
- const className = cn("text-foreground flex h-full w-full min-w-0 overflow-hidden rounded-md px-2.5 text-start text-xs", blockSize < TWO_LINE_BLOCK_MIN_PX ? "flex-row items-center gap-2" : "flex-col gap-0.5 py-1", BLOCK_KIND_CLASSES[block.kind], block.kind === "entry" && ENTRY_STATE_CLASSES[block.state], block.tone.type === "matter" && "border-s-[3px]", block.tone.type === "neutral" && "bg-muted");
333
- const content = blockSize < 16 ? null : /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx("span", {
334
- className: "min-w-0 shrink-0 truncate font-medium",
341
+ const oneLine = blockSize < TWO_LINE_BLOCK_MIN_PX;
342
+ const className = cn("text-foreground @container flex h-full w-full min-w-0 overflow-hidden rounded-md px-2.5 text-start text-xs", oneLine ? "flex-row items-center gap-2" : "flex-col gap-0.5 py-1", BLOCK_KIND_CLASSES[block.kind], block.kind === "entry" && ENTRY_STATE_CLASSES[block.state], block.tone.type === "matter" && "border-s-[3px]", block.tone.type === "neutral" && "bg-muted");
343
+ const content = blockSize < 16 ? null : /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx(TruncatedText, {
344
+ className: cn("font-medium", oneLine ? "shrink" : "shrink-0"),
345
+ hint: false,
335
346
  children: block.title
336
- }), block.caption === void 0 ? null : /* @__PURE__ */ jsx("span", {
337
- className: "text-muted-foreground min-w-0 truncate tabular-nums",
347
+ }), block.caption === void 0 ? null : /* @__PURE__ */ jsx(TruncatedText, {
348
+ className: cn("text-muted-foreground tabular-nums", oneLine && "shrink-[999]"),
349
+ hint: false,
338
350
  children: block.caption
339
351
  })] });
340
352
  const shared = {
@@ -345,9 +357,68 @@ const DayCalendarBlockView = ({ block, grid, lane, range }) => {
345
357
  "data-slot": "day-calendar-block",
346
358
  style: blockSurfaceStyle(block)
347
359
  };
360
+ return /* @__PURE__ */ jsx("div", {
361
+ className: cn("absolute px-1.5 py-px", block.tooltip === void 0 && !(block.kind === "entry" && block.state === "suggestion") && "pointer-events-none"),
362
+ style: placement,
363
+ children: /* @__PURE__ */ jsx(BlockSurface, {
364
+ block,
365
+ content,
366
+ shared
367
+ })
368
+ });
369
+ };
370
+ /** A block whose hover holds actions answers hover, focus and an open
371
+ * popover like a suggestion does. */
372
+ const ACTIONS_BLOCK_CLASSES = "focus-visible:ring-ring data-popup-open:ring-ring/50 hover:ring-ring/50 outline-none hover:ring-1 focus-visible:ring-2 data-popup-open:ring-1";
373
+ /** A block's actions, opened by hover, focus or press. Once something in
374
+ * them is pressed they stay open until dismissed: a press can move the
375
+ * layout under a pointer that never moved (a notice above the calendar
376
+ * goes away), and a hover popover would close as if the pointer had left,
377
+ * taking the action's confirmation with it. */
378
+ const ActionsPopover = ({ children, trigger }) => {
379
+ const [open, setOpen] = useState(false);
380
+ const [held, setHeld] = useState(false);
381
+ return /* @__PURE__ */ jsxs(Popover, {
382
+ onOpenChange: (next, { reason }) => {
383
+ if (!next && held && reason === "trigger-hover") return;
384
+ setOpen(next);
385
+ if (!next) setHeld(false);
386
+ },
387
+ open,
388
+ children: [/* @__PURE__ */ jsx(PopoverTrigger, {
389
+ delay: 300,
390
+ openOnHover: true,
391
+ render: trigger
392
+ }), /* @__PURE__ */ jsx(PopoverPopup, {
393
+ className: "max-w-72",
394
+ onPointerDown: () => setHeld(true),
395
+ side: "top",
396
+ tooltipStyle: true,
397
+ children
398
+ })]
399
+ });
400
+ };
401
+ /** A block whose hover holds actions is a button that opens them; a
402
+ * suggestion is a button the caller acts on; any other block is read. */
403
+ const BlockSurface = ({ block, content, shared }) => {
404
+ const state = block.kind === "entry" ? block.state : void 0;
405
+ if (block.tooltip?.type === "actions") {
406
+ const trigger = /* @__PURE__ */ jsx("button", {
407
+ ...shared,
408
+ className: cn(shared.className, ACTIONS_BLOCK_CLASSES),
409
+ "data-state": state,
410
+ onPointerDown: (event) => event.stopPropagation(),
411
+ type: "button",
412
+ children: content
413
+ });
414
+ return /* @__PURE__ */ jsx(ActionsPopover, {
415
+ trigger,
416
+ children: block.tooltip.content
417
+ });
418
+ }
348
419
  const surface = block.kind === "entry" && block.state === "suggestion" ? /* @__PURE__ */ jsx("button", {
349
420
  ...shared,
350
- "data-state": block.state,
421
+ "data-state": state,
351
422
  onClick: block.onActivate,
352
423
  onKeyDown: (event) => event.stopPropagation(),
353
424
  onPointerDown: (event) => event.stopPropagation(),
@@ -355,37 +426,37 @@ const DayCalendarBlockView = ({ block, grid, lane, range }) => {
355
426
  children: content
356
427
  }) : /* @__PURE__ */ jsx("article", {
357
428
  ...shared,
358
- "data-state": block.kind === "entry" ? block.state : void 0,
429
+ "data-state": state,
359
430
  children: content
360
431
  });
361
- return /* @__PURE__ */ jsx("div", {
362
- className: cn("absolute px-1.5 py-px", block.tooltip === void 0 && !(block.kind === "entry" && block.state === "suggestion") && "pointer-events-none"),
363
- style: placement,
364
- children: block.tooltip === void 0 ? surface : /* @__PURE__ */ jsxs(Tooltip, { children: [/* @__PURE__ */ jsx(TooltipTrigger, { render: surface }), /* @__PURE__ */ jsx(TooltipPopup, {
365
- className: "max-w-72",
366
- children: block.tooltip
367
- })] })
368
- });
432
+ return block.tooltip === void 0 ? surface : /* @__PURE__ */ jsxs(Tooltip, { children: [/* @__PURE__ */ jsx(TooltipTrigger, { render: surface }), /* @__PURE__ */ jsx(TooltipPopup, {
433
+ className: "max-w-72",
434
+ children: block.tooltip.content
435
+ })] });
369
436
  };
370
437
  /** A suggestion's tint is half an entry's, so proposed time reads lighter. */
371
438
  const TONE_TINT_PERCENT = {
372
439
  entry: 16,
373
440
  suggestion: 8
374
441
  };
375
- /** A topic is not a matter yet: its tint sits between a suggestion's and an
376
- * entry's. */
377
- const TOPIC_TINT_PERCENT = 12;
442
+ /** A topic swatch is already muted, so it mixes in stronger than a matter's
443
+ * to stay visible; the result is still far less saturated. */
444
+ const TOPIC_TINT_PERCENT = 30;
378
445
  function blockSurfaceStyle(block) {
379
446
  const tone = block.tone;
380
447
  const suggestion = block.kind === "entry" && block.state === "suggestion";
381
448
  switch (tone.type) {
382
449
  case "neutral": return { backgroundColor: suggestion ? "transparent" : void 0 };
383
- case "topic": return { backgroundColor: `color-mix(in srgb, ${getMatterColor(tone.key)} ${String(TOPIC_TINT_PERCENT)}%, var(--card))` };
450
+ case "topic": return {
451
+ "--day-calendar-tone": getTopicColor(tone.key),
452
+ backgroundColor: `color-mix(in srgb, var(--day-calendar-tone) ${String(TOPIC_TINT_PERCENT)}%, var(--card))`
453
+ };
384
454
  case "matter": {
385
455
  const color = resolveMatterColor(tone.matterId, tone.color);
386
456
  const tint = suggestion ? TONE_TINT_PERCENT.suggestion : TONE_TINT_PERCENT.entry;
387
457
  return {
388
- backgroundColor: `color-mix(in srgb, ${color} ${String(tint)}%, var(--card))`,
458
+ "--day-calendar-tone": color,
459
+ backgroundColor: `color-mix(in srgb, var(--day-calendar-tone) ${String(tint)}%, var(--card))`,
389
460
  borderColor: suggestion ? color : void 0,
390
461
  borderInlineStartColor: color
391
462
  };
@@ -1,3 +1,4 @@
1
+ import { TruncatedText } from "../components/truncated-text.js";
1
2
  import { cn } from "../lib/utils.js";
2
3
  import { CalendarCell, CalendarEntryButton, CalendarEntrySurface, CalendarHeaderCell, CalendarHeaderRow } from "./primitives.js";
3
4
  import { assertConsecutiveCalendarDates, layoutResourceCalendarEntries, nextCalendarDate } from "./resource-calendar.logic.js";
@@ -99,11 +100,11 @@ const ResourceCalendar = ({ ariaLabel, columns, empty, entries, onSelectEntry, r
99
100
  className: "bg-card sticky start-0 z-20 border-b-0 px-4 py-3",
100
101
  id: resourceHeaderId,
101
102
  role: "rowheader",
102
- children: [/* @__PURE__ */ jsx("span", {
103
- className: "block truncate text-sm font-semibold",
103
+ children: [/* @__PURE__ */ jsx(TruncatedText, {
104
+ className: "text-sm font-semibold",
104
105
  children: resource.label
105
- }), resource.meta === void 0 ? null : /* @__PURE__ */ jsx("span", {
106
- className: "text-muted-foreground mt-1 block truncate text-xs",
106
+ }), resource.meta === void 0 ? null : /* @__PURE__ */ jsx(TruncatedText, {
107
+ className: "text-muted-foreground mt-1 text-xs",
107
108
  children: resource.meta
108
109
  })]
109
110
  }), columns.map((column, columnIndex) => {
@@ -146,11 +147,8 @@ const ResourceCalendarEntryView = ({ entry, labelledBy, onSelectEntry, placement
146
147
  insetInlineStart: 0,
147
148
  inlineSize: `calc(${String(placement.span)} * 100%)`
148
149
  };
149
- const content = /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx("span", {
150
- className: "block truncate",
151
- children: entry.label
152
- }), entry.meta === void 0 ? null : /* @__PURE__ */ jsx("span", {
153
- className: "block truncate opacity-75",
150
+ const content = /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx(TruncatedText, { children: entry.label }), entry.meta === void 0 ? null : /* @__PURE__ */ jsx(TruncatedText, {
151
+ className: "opacity-75",
154
152
  children: entry.meta
155
153
  })] });
156
154
  return /* @__PURE__ */ jsx("div", {
@@ -0,0 +1,3 @@
1
+ export declare const TOPIC_SWATCHES: readonly ["--topic-sand", "--topic-sage", "--topic-mist", "--topic-heather", "--topic-clay", "--topic-slate"];
2
+ export declare const getTopicSwatch: (key: string) => "--topic-clay" | "--topic-heather" | "--topic-mist" | "--topic-sage" | "--topic-sand" | "--topic-slate";
3
+ export declare const getTopicColor: (key: string) => string;
@@ -0,0 +1,19 @@
1
+ import { stableHash } from "../lib/stable-hash.js";
2
+ // src/calendar/topic-colors.ts
3
+ /**
4
+ * Swatches for time no matter claimed yet, such as one document's or window's
5
+ * stretch of a day. A separate low-chroma set (`--topic-*` in theme.css), so
6
+ * a topic is never painted in a colour a matter can have.
7
+ */
8
+ const DEFAULT_TOPIC_SWATCH = "--topic-sand";
9
+ const TOPIC_SWATCHES = [
10
+ DEFAULT_TOPIC_SWATCH,
11
+ "--topic-sage",
12
+ "--topic-mist",
13
+ "--topic-heather",
14
+ "--topic-clay",
15
+ "--topic-slate"
16
+ ];
17
+ const getTopicSwatch = (key) => TOPIC_SWATCHES[stableHash(key) % TOPIC_SWATCHES.length] ?? DEFAULT_TOPIC_SWATCH;
18
+ const getTopicColor = (key) => `var(${getTopicSwatch(key)})`;
19
+ export { TOPIC_SWATCHES, getTopicColor, getTopicSwatch };
@@ -6,6 +6,7 @@ import { cn } from "../lib/utils.js";
6
6
  import { BUTTON_PLAIN_VARIANT, BUTTON_VARIANT_TOKENS } from "./button.shared.js";
7
7
  import { Loader } from "./loader.js";
8
8
  import { renderTooltipTrigger } from "./tooltip-trigger-helper.js";
9
+ import { TruncatedText } from "./truncated-text.js";
9
10
  import { Fragment, jsx, jsxs } from "react/jsx-runtime";
10
11
  import { Fragment as Fragment$1, isValidElement } from "react";
11
12
  import { mergeProps } from "@base-ui/react/merge-props";
@@ -156,13 +157,12 @@ const BUTTON_SIZE_CONTENT = {
156
157
  * A button never grows past its container (`max-w-full` on every variant).
157
158
  * A one-line text label is cut to fit with an ellipsis; the button's
158
159
  * accessible name keeps the whole text, and `tooltip` can show it. The label
159
- * is a plain span, not a `TruncatedText`: a tooltip root in every button's
160
+ * is a `TruncatedText` without its own hint: a tooltip root in every button's
160
161
  * label would cost every screen with many buttons. `layout="wrap"` lets a
161
162
  * label take several lines instead. An inline button is a word in a sentence
162
163
  * and a plain button lays out its own content, so neither has a one-line
163
164
  * label to cut.
164
165
  */
165
- const BUTTON_LABEL_CLASS = "block min-w-0 truncate";
166
166
  const cutsLabel = ({ layout, size, variant }) => layout === void 0 && variant !== "plain" && variant !== "link" && size !== "inline" && BUTTON_SIZE_CONTENT[size ?? BUTTON_DEFAULT_SIZE] === BUTTON_CONTENT.text;
167
167
  /**
168
168
  * Each run of adjacent text among the children, cut to the button's width.
@@ -178,9 +178,9 @@ const containedLabel = (children) => {
178
178
  let run = [];
179
179
  const closeRun = () => {
180
180
  if (run.length > 0) {
181
- parts.push(/* @__PURE__ */ jsx("span", {
182
- className: BUTTON_LABEL_CLASS,
181
+ parts.push(/* @__PURE__ */ jsx(TruncatedText, {
183
182
  "data-slot": "button-label",
183
+ hint: false,
184
184
  children: run
185
185
  }, `label:${String(parts.length)}`));
186
186
  run = [];
@@ -49,8 +49,12 @@ export declare const COMPOSER_TEXT_CLASS = "text-sm leading-5";
49
49
  export declare const COMPOSER_LARGE_TEXT_WELL_CLASS = "ps-3 pe-3 pt-2 pb-1";
50
50
  /** The `large` editable element's stature: three lines before it grows. */
51
51
  export declare const COMPOSER_LARGE_EDITOR_CLASS = "min-h-15";
52
- /** The placeholder painted over an empty editor, in either stature. */
53
- export declare const COMPOSER_PLACEHOLDER_CLASS = "text-foreground-placeholder pointer-events-none absolute truncate text-sm";
52
+ /**
53
+ * The placeholder painted over an empty editor, in either stature.
54
+ * `ComposerPlaceholder` cuts its text through TruncatedText; a row that wears
55
+ * this class around other content cuts its own text the same way.
56
+ */
57
+ export declare const COMPOSER_PLACEHOLDER_CLASS = "text-foreground-placeholder pointer-events-none absolute text-sm";
54
58
  /** The `large` action row under the text well: leading controls, trailing send. */
55
59
  export declare const COMPOSER_LARGE_ACTION_ROW_CLASS = "flex items-center justify-end gap-0.5 px-1.5 pb-1.5";
56
60
  /** The leading group of the action row (the (+) menu, or a scope control). */
@@ -50,8 +50,12 @@ const COMPOSER_TEXT_CLASS = "text-sm leading-5";
50
50
  const COMPOSER_LARGE_TEXT_WELL_CLASS = "ps-3 pe-3 pt-2 pb-1";
51
51
  /** The `large` editable element's stature: three lines before it grows. */
52
52
  const COMPOSER_LARGE_EDITOR_CLASS = "min-h-15";
53
- /** The placeholder painted over an empty editor, in either stature. */
54
- const COMPOSER_PLACEHOLDER_CLASS = "text-foreground-placeholder pointer-events-none absolute truncate text-sm";
53
+ /**
54
+ * The placeholder painted over an empty editor, in either stature.
55
+ * `ComposerPlaceholder` cuts its text through TruncatedText; a row that wears
56
+ * this class around other content cuts its own text the same way.
57
+ */
58
+ const COMPOSER_PLACEHOLDER_CLASS = "text-foreground-placeholder pointer-events-none absolute text-sm";
55
59
  /** The `large` action row under the text well: leading controls, trailing send. */
56
60
  const COMPOSER_LARGE_ACTION_ROW_CLASS = "flex items-center justify-end gap-0.5 px-1.5 pb-1.5";
57
61
  /** The leading group of the action row (the (+) menu, or a scope control). */
@@ -1,5 +1,18 @@
1
1
  import { COMPOSER_BOX_ANONYMIZED_CLASS, COMPOSER_BOX_CLASS, COMPOSER_BOX_FOCUS_CLASS, COMPOSER_COMPACT_ROW_CLASS, COMPOSER_COMPACT_TEXT_CELL_CLASS, COMPOSER_CONTROL_BUTTON_SIZE, COMPOSER_LARGE_ACTION_ROW_CLASS, COMPOSER_LARGE_EDITOR_CLASS, COMPOSER_LARGE_TEXT_WELL_CLASS, COMPOSER_LEADING_GROUP_CLASS, COMPOSER_PICKER_TRIGGER_CLASS, COMPOSER_PLACEHOLDER_CLASS, COMPOSER_SEND_BUTTON_CLASS, COMPOSER_TEXT_CLASS } from "./composer-tokens.js";
2
2
  import { ReactNode } from "react";
3
+ /** Where the placeholder sits over the editor, in each stature. */
4
+ declare const COMPOSER_PLACEHOLDER_POSITION_CLASS: {
5
+ readonly compact: "start-0 end-0 top-1/2 -translate-y-1/2";
6
+ readonly large: "start-3 end-3 top-2";
7
+ };
8
+ /**
9
+ * The placeholder painted over an empty editor, cut to one line. It takes no
10
+ * pointer and is hidden from assistive tech, so it shows no hint.
11
+ */
12
+ export declare const ComposerPlaceholder: ({ children, stature }: {
13
+ children: ReactNode;
14
+ stature: keyof typeof COMPOSER_PLACEHOLDER_POSITION_CLASS;
15
+ }) => import("react").JSX.Element;
3
16
  type ComposerStatusRowProps = {
4
17
  /** Left cluster: matter picker, web-search / anonymize toggles, ... */
5
18
  start?: ReactNode | undefined;
@@ -3,8 +3,24 @@ import { ChevronDownIcon } from "../icons.js";
3
3
  import { cn } from "../lib/utils.js";
4
4
  import { COMPOSER_BOX_ANONYMIZED_CLASS, COMPOSER_BOX_CLASS, COMPOSER_BOX_FOCUS_CLASS, COMPOSER_COMPACT_ROW_CLASS, COMPOSER_COMPACT_TEXT_CELL_CLASS, COMPOSER_CONTROL_BUTTON_SIZE, COMPOSER_LARGE_ACTION_ROW_CLASS, COMPOSER_LARGE_EDITOR_CLASS, COMPOSER_LARGE_TEXT_WELL_CLASS, COMPOSER_LEADING_GROUP_CLASS, COMPOSER_PICKER_TRIGGER_CLASS, COMPOSER_PLACEHOLDER_CLASS, COMPOSER_SEND_BUTTON_CLASS, COMPOSER_TEXT_CLASS } from "./composer-tokens.js";
5
5
  import { DropdownMenu as Menu, DropdownMenuContent as MenuPopup, DropdownMenuRadioGroup as MenuRadioGroup, DropdownMenuRadioItem as MenuRadioItem, DropdownMenuTrigger as MenuTrigger } from "./menu.js";
6
+ import { TruncatedText } from "./truncated-text.js";
6
7
  import { jsx, jsxs } from "react/jsx-runtime";
7
8
  // src/components/composer.tsx
9
+ /** Where the placeholder sits over the editor, in each stature. */
10
+ const COMPOSER_PLACEHOLDER_POSITION_CLASS = {
11
+ compact: "start-0 end-0 top-1/2 -translate-y-1/2",
12
+ large: "start-3 end-3 top-2"
13
+ };
14
+ /**
15
+ * The placeholder painted over an empty editor, cut to one line. It takes no
16
+ * pointer and is hidden from assistive tech, so it shows no hint.
17
+ */
18
+ const ComposerPlaceholder = ({ children, stature }) => /* @__PURE__ */ jsx(TruncatedText, {
19
+ "aria-hidden": "true",
20
+ className: cn(COMPOSER_PLACEHOLDER_CLASS, COMPOSER_PLACEHOLDER_POSITION_CLASS[stature]),
21
+ hint: false,
22
+ children
23
+ });
8
24
  /**
9
25
  * The slim status row rendered beneath a composer box: a `text-xs` row with
10
26
  * a start cluster and an end slot pinned to the far edge. Shared by every
@@ -54,8 +70,8 @@ const ComposerPicker = ({ ariaLabel, icon, onChange, options, value }) => {
54
70
  className: "flex size-3 shrink-0 items-center justify-center [&>svg]:size-3",
55
71
  children: icon
56
72
  }),
57
- /* @__PURE__ */ jsx("span", {
58
- className: "truncate",
73
+ /* @__PURE__ */ jsx(TruncatedText, {
74
+ hint: false,
59
75
  children: current?.label ?? value
60
76
  }),
61
77
  /* @__PURE__ */ jsx(ChevronDownIcon, {
@@ -76,4 +92,4 @@ const ComposerPicker = ({ ariaLabel, icon, onChange, options, value }) => {
76
92
  })
77
93
  })] });
78
94
  };
79
- export { COMPOSER_BOX_ANONYMIZED_CLASS, COMPOSER_BOX_CLASS, COMPOSER_BOX_FOCUS_CLASS, COMPOSER_COMPACT_ROW_CLASS, COMPOSER_COMPACT_TEXT_CELL_CLASS, COMPOSER_CONTROL_BUTTON_SIZE, COMPOSER_LARGE_ACTION_ROW_CLASS, COMPOSER_LARGE_EDITOR_CLASS, COMPOSER_LARGE_TEXT_WELL_CLASS, COMPOSER_LEADING_GROUP_CLASS, COMPOSER_PICKER_TRIGGER_CLASS, COMPOSER_PLACEHOLDER_CLASS, COMPOSER_SEND_BUTTON_CLASS, COMPOSER_TEXT_CLASS, ComposerPicker, ComposerStatusRow };
95
+ export { COMPOSER_BOX_ANONYMIZED_CLASS, COMPOSER_BOX_CLASS, COMPOSER_BOX_FOCUS_CLASS, COMPOSER_COMPACT_ROW_CLASS, COMPOSER_COMPACT_TEXT_CELL_CLASS, COMPOSER_CONTROL_BUTTON_SIZE, COMPOSER_LARGE_ACTION_ROW_CLASS, COMPOSER_LARGE_EDITOR_CLASS, COMPOSER_LARGE_TEXT_WELL_CLASS, COMPOSER_LEADING_GROUP_CLASS, COMPOSER_PICKER_TRIGGER_CLASS, COMPOSER_PLACEHOLDER_CLASS, COMPOSER_SEND_BUTTON_CLASS, COMPOSER_TEXT_CLASS, ComposerPicker, ComposerPlaceholder, ComposerStatusRow };
@@ -7,6 +7,7 @@ import { DirectionalIcon } from "./directional-icon.js";
7
7
  import { Field, FieldLabel } from "./field.js";
8
8
  import { ScrollArea } from "./scroll-area.js";
9
9
  import { SearchField } from "./search-field.js";
10
+ import { TruncatedText } from "./truncated-text.js";
10
11
  import { jsx, jsxs } from "react/jsx-runtime";
11
12
  import { useId, useState } from "react";
12
13
  // src/components/court-filter.tsx
@@ -56,10 +57,9 @@ const SingleCourtGroup = ({ group, options }) => {
56
57
  if (checked) options.onChange(void 0);
57
58
  }
58
59
  }),
59
- /* @__PURE__ */ jsx(BidiText, {
60
- as: "span",
61
- className: cn("text-foreground min-w-0 flex-1 truncate", checked && "font-medium"),
62
- title: label ?? value,
60
+ /* @__PURE__ */ jsx(TruncatedText, {
61
+ className: cn("text-foreground flex-1", checked && "font-medium"),
62
+ render: /* @__PURE__ */ jsx(BidiText, { as: "span" }),
63
63
  children: label ?? value
64
64
  }),
65
65
  badgeText !== void 0 && /* @__PURE__ */ jsx("bdi", {
@@ -9,6 +9,7 @@ import { DirectionalIcon } from "./directional-icon.js";
9
9
  import { Input } from "./input.js";
10
10
  import { Popover, PopoverContent as PopoverPopup, PopoverTrigger } from "./popover.js";
11
11
  import { TimeField } from "./time-field.js";
12
+ import { TruncatedText } from "./truncated-text.js";
12
13
  import { Fragment, jsx, jsxs } from "react/jsx-runtime";
13
14
  import { useCallback, useId, useMemo, useRef, useState, useSyncExternalStore } from "react";
14
15
  import { Result, panic } from "better-result";
@@ -465,8 +466,8 @@ const DatePickerPopoverContent = (props) => {
465
466
  }),
466
467
  children: [
467
468
  showIcon && /* @__PURE__ */ jsx(CalendarIcon, { className: "size-3.5 shrink-0" }),
468
- /* @__PURE__ */ jsx("span", {
469
- className: "min-w-0 flex-1 truncate text-start",
469
+ /* @__PURE__ */ jsx(TruncatedText, {
470
+ className: "flex-1 text-start",
470
471
  id: labelledBy ? displayValueId : void 0,
471
472
  children: displayLabel
472
473
  }),
@@ -3,6 +3,7 @@ import { cn } from "../lib/utils.js";
3
3
  import { CourtBadge } from "./court-badge.js";
4
4
  import { COURT_TIER_WEIGHT, caseNumberIdentityLabels, statuteIdentityLabels } from "./document-identity-badge.logic.js";
5
5
  import { Tooltip, TooltipContent as TooltipPopup, TooltipTrigger } from "./tooltip.js";
6
+ import { TruncatedText } from "./truncated-text.js";
6
7
  import { jsx, jsxs } from "react/jsx-runtime";
7
8
  import { panic } from "better-result";
8
9
  // src/components/document-identity-badge.tsx
@@ -58,11 +59,15 @@ function referenceLabelMark(labels, kind) {
58
59
  "data-slot": "document-identity-badge",
59
60
  "data-kind": kind,
60
61
  dir: "ltr",
61
- children: [/* @__PURE__ */ jsx("bdi", {
62
- className: cn("min-w-0 truncate", classes.short),
62
+ children: [/* @__PURE__ */ jsx(TruncatedText, {
63
+ className: classes.short,
64
+ hint: false,
65
+ render: /* @__PURE__ */ jsx("bdi", {}),
63
66
  children: labels.short
64
- }), /* @__PURE__ */ jsx("bdi", {
65
- className: cn("min-w-0 truncate", classes.long),
67
+ }), /* @__PURE__ */ jsx(TruncatedText, {
68
+ className: classes.long,
69
+ hint: false,
70
+ render: /* @__PURE__ */ jsx("bdi", {}),
66
71
  children: labels.long
67
72
  })]
68
73
  });
@@ -3,6 +3,7 @@ import { UploadIcon } from "../icons.js";
3
3
  import { openFilePicker } from "../lib/file-picker.js";
4
4
  import { cn } from "../lib/utils.js";
5
5
  import { Button } from "./button.js";
6
+ import { TruncatedText } from "./truncated-text.js";
6
7
  import { jsx, jsxs } from "react/jsx-runtime";
7
8
  import { useId } from "react";
8
9
  // src/components/file-input.tsx
@@ -23,8 +24,8 @@ const FileInput = ({ accept, "aria-labelledby": labelledBy, className, disabled,
23
24
  type: "button",
24
25
  variant: "outline",
25
26
  children: [/* @__PURE__ */ jsx(UploadIcon, { "aria-hidden": "true" }), chooseLabel]
26
- }), /* @__PURE__ */ jsx("span", {
27
- className: "text-muted-foreground min-w-0 truncate text-sm",
27
+ }), /* @__PURE__ */ jsx(TruncatedText, {
28
+ className: "text-muted-foreground text-sm",
28
29
  "data-slot": "file-input-name",
29
30
  children: file ? /* @__PURE__ */ jsx("bdi", { children: file.name }) : emptyLabel
30
31
  })]
@@ -2,6 +2,7 @@
2
2
  import { cn } from "../lib/utils.js";
3
3
  import { withoutDirectionalControls } from "./bidi-text.js";
4
4
  import { Tooltip, TooltipContent as TooltipPopup, TooltipTrigger } from "./tooltip.js";
5
+ import { TruncatedText } from "./truncated-text.js";
5
6
  import { jsx, jsxs } from "react/jsx-runtime";
6
7
  import { useCallback, useId, useState } from "react";
7
8
  // src/components/identity-text.tsx
@@ -51,9 +52,10 @@ const IdentityText = ({ children, className }) => {
51
52
  setOpen(true);
52
53
  }
53
54
  },
54
- render: /* @__PURE__ */ jsx("span", {
55
- className: cn("focus-visible:ring-ring block min-w-0 truncate outline-none [unicode-bidi:isolate] focus-visible:ring-2", className),
55
+ render: /* @__PURE__ */ jsx(TruncatedText, {
56
+ className: cn("focus-visible:ring-ring outline-none [unicode-bidi:isolate] focus-visible:ring-2", className),
56
57
  dir: "auto",
58
+ hint: false,
57
59
  ref: textRef,
58
60
  role: overflow && !hasFocusOwner ? "button" : void 0,
59
61
  tabIndex: overflow && !hasFocusOwner ? 0 : void 0
@@ -2,6 +2,7 @@
2
2
  import { cn } from "../lib/utils.js";
3
3
  import { Input } from "./input.js";
4
4
  import { Textarea } from "./textarea.js";
5
+ import { TruncatedText } from "./truncated-text.js";
5
6
  import { jsx } from "react/jsx-runtime";
6
7
  import { cva } from "class-variance-authority";
7
8
  // src/components/input-group.tsx
@@ -35,8 +36,8 @@ const InputGroupAddon = ({ className, align = "inline-start", ...props }) => /*
35
36
  },
36
37
  ...props
37
38
  });
38
- const InputGroupText = ({ className, ...props }) => /* @__PURE__ */ jsx("span", {
39
- className: cn("text-muted-foreground flex items-center gap-2 truncate leading-none [&_svg]:pointer-events-none [&_svg]:-mx-0.5 in-[[data-slot=input-group]:has([data-slot=input-control],[data-slot=textarea-control])]:[&_svg:not([class*='size-'])]:size-4.5 sm:in-[[data-slot=input-group]:has([data-slot=input-control],[data-slot=textarea-control])]:[&_svg:not([class*='size-'])]:size-4", className),
39
+ const InputGroupText = ({ className, ...props }) => /* @__PURE__ */ jsx(TruncatedText, {
40
+ className: cn("text-muted-foreground flex items-center gap-2 leading-none [&_svg]:pointer-events-none [&_svg]:-mx-0.5 in-[[data-slot=input-group]:has([data-slot=input-control],[data-slot=textarea-control])]:[&_svg:not([class*='size-'])]:size-4.5 sm:in-[[data-slot=input-group]:has([data-slot=input-control],[data-slot=textarea-control])]:[&_svg:not([class*='size-'])]:size-4", className),
40
41
  ...props
41
42
  });
42
43
  const InputGroupInput = ({ className, ...props }) => /* @__PURE__ */ jsx(Input, {
@@ -2,6 +2,7 @@
2
2
  import { cn } from "../lib/utils.js";
3
3
  import { BidiText } from "./bidi-text.js";
4
4
  import { ScrollArea } from "./scroll-area.js";
5
+ import { TruncatedText } from "./truncated-text.js";
5
6
  import { jsx, jsxs } from "react/jsx-runtime";
6
7
  // src/components/landing.tsx
7
8
  const LandingLayout = ({ actions, hero, children, footer }) => /* @__PURE__ */ jsxs("div", {
@@ -88,12 +89,12 @@ const LandingItemText = ({ icon, iconTone = "muted", meta, title }) => /* @__PUR
88
89
  children: icon
89
90
  }), /* @__PURE__ */ jsxs("span", {
90
91
  className: "min-w-0 flex-1",
91
- children: [/* @__PURE__ */ jsx(BidiText, {
92
- as: "span",
93
- className: "text-foreground block truncate text-sm font-medium",
92
+ children: [/* @__PURE__ */ jsx(TruncatedText, {
93
+ className: "text-foreground text-sm font-medium",
94
+ render: /* @__PURE__ */ jsx(BidiText, { as: "span" }),
94
95
  children: title
95
- }), meta !== void 0 && meta !== null ? /* @__PURE__ */ jsx("span", {
96
- className: "text-muted-foreground block truncate text-xs",
96
+ }), meta !== void 0 && meta !== null ? /* @__PURE__ */ jsx(TruncatedText, {
97
+ className: "text-muted-foreground text-xs",
97
98
  children: meta
98
99
  }) : null]
99
100
  })]