@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,4 +1,5 @@
1
1
  import * as React$1 from "react";
2
+ import { TruncatedTextLines } from "./truncated-text.js";
2
3
  import { VariantProps } from "class-variance-authority";
3
4
  import { useRender } from "@base-ui/react/use-render";
4
5
  /**
@@ -39,15 +40,20 @@ declare const listItemMediaVariants: (props?: ({
39
40
  /** Fixed 32px tile for an icon, logo or initial. */
40
41
  declare const ListItemMedia: ({ className, variant, ...props }: React$1.ComponentProps<"div"> & VariantProps<typeof listItemMediaVariants>) => React$1.JSX.Element;
41
42
  declare const ListItemContent: ({ className, ...props }: React$1.ComponentProps<"div">) => React$1.JSX.Element;
42
- declare const ListItemTitle: ({ className, ...props }: React$1.ComponentProps<"span">) => React$1.JSX.Element;
43
+ declare const listItemTitleVariants: (props?: ({
44
+ variant?: "name" | "statement" | null | undefined;
45
+ } & import("class-variance-authority/types").ClassProp) | undefined) => string;
46
+ declare const ListItemTitle: ({ className, variant, ...props }: React$1.ComponentProps<"span"> & VariantProps<typeof listItemTitleVariants>) => React$1.JSX.Element;
43
47
  declare const listItemDescriptionVariants: (props?: ({
44
- variant?: "code" | "text" | null | undefined;
48
+ variant?: "code" | "paragraph" | "text" | null | undefined;
45
49
  } & import("class-variance-authority/types").ClassProp) | undefined) => string;
46
50
  /**
47
- * Secondary line. Pass `line-clamp-2` when two lines are genuinely needed;
48
- * never `whitespace-nowrap`.
51
+ * Secondary line, one line by default. Use the `paragraph` variant (two
52
+ * lines) or `lines` when more is genuinely needed; never `whitespace-nowrap`.
49
53
  */
50
- declare const ListItemDescription: ({ className, variant, ...props }: React$1.ComponentProps<"span"> & VariantProps<typeof listItemDescriptionVariants>) => React$1.JSX.Element;
54
+ declare const ListItemDescription: ({ className, variant, lines, ...props }: React$1.ComponentProps<"span"> & VariantProps<typeof listItemDescriptionVariants> & {
55
+ lines?: TruncatedTextLines;
56
+ }) => React$1.JSX.Element;
51
57
  declare const ListItemActions: ({ className, ...props }: React$1.ComponentProps<"div">) => React$1.JSX.Element;
52
58
  /** Single-line empty row inside a `List`. Phrase it as the next action. */
53
59
  declare const ListEmpty: ({ className, ...props }: React$1.ComponentProps<"li">) => React$1.JSX.Element;
@@ -1,6 +1,7 @@
1
1
  "use client";
2
2
  import { cn } from "../lib/utils.js";
3
3
  import { AnimatedCount } from "./animated-count.js";
4
+ import { TruncatedText } from "./truncated-text.js";
4
5
  import { jsx, jsxs } from "react/jsx-runtime";
5
6
  import { mergeProps } from "@base-ui/react/merge-props";
6
7
  import { useRender } from "@base-ui/react/use-render";
@@ -106,29 +107,47 @@ const ListItemContent = ({ className, ...props }) => /* @__PURE__ */ jsx("div",
106
107
  "data-slot": "list-item-content",
107
108
  ...props
108
109
  });
109
- const ListItemTitle = ({ className, ...props }) => /* @__PURE__ */ jsx("span", {
110
- className: cn("text-foreground truncate text-sm font-medium", className),
110
+ const listItemTitleVariants = cva("text-foreground text-sm font-medium", {
111
+ defaultVariants: { variant: "name" },
112
+ variants: { variant: {
113
+ /** A name: one line, cut at the end. */
114
+ name: "",
115
+ /** A statement a reader must see whole to judge (a proposed fact): up
116
+ * to two lines, then cut. */
117
+ statement: "text-pretty wrap-break-word"
118
+ } }
119
+ });
120
+ const LIST_ITEM_TITLE_LINES = {
121
+ name: 1,
122
+ statement: 2
123
+ };
124
+ const ListItemTitle = ({ className, variant, ...props }) => /* @__PURE__ */ jsx(TruncatedText, {
125
+ className: cn(listItemTitleVariants({ variant }), className),
111
126
  "data-slot": "list-item-title",
112
127
  dir: "auto",
128
+ lines: LIST_ITEM_TITLE_LINES[variant ?? "name"],
113
129
  ...props
114
130
  });
115
131
  const listItemDescriptionVariants = cva("text-muted-foreground text-xs", {
116
132
  defaultVariants: { variant: "text" },
117
133
  variants: { variant: {
118
- /** Prose, clamped to one line so long provider copy cannot widen the row. */
119
- text: "line-clamp-1 wrap-break-word",
120
- /** A URL or command: monospace, left-to-right, truncated at the end. */
121
- code: "truncate font-mono"
134
+ /** Prose, cut to one line so long provider copy cannot widen the row. */
135
+ text: "wrap-break-word",
136
+ /** Prose that genuinely needs a second line (a reason, an error). */
137
+ paragraph: "wrap-break-word",
138
+ /** A URL or command: monospace, left-to-right, cut at the end. */
139
+ code: "font-mono"
122
140
  } }
123
141
  });
124
142
  /**
125
- * Secondary line. Pass `line-clamp-2` when two lines are genuinely needed;
126
- * never `whitespace-nowrap`.
143
+ * Secondary line, one line by default. Use the `paragraph` variant (two
144
+ * lines) or `lines` when more is genuinely needed; never `whitespace-nowrap`.
127
145
  */
128
- const ListItemDescription = ({ className, variant, ...props }) => /* @__PURE__ */ jsx("span", {
146
+ const ListItemDescription = ({ className, variant, lines, ...props }) => /* @__PURE__ */ jsx(TruncatedText, {
129
147
  className: cn(listItemDescriptionVariants({ variant }), className),
130
148
  "data-slot": "list-item-description",
131
149
  dir: variant === "code" ? "ltr" : "auto",
150
+ lines: lines ?? (variant === "paragraph" ? 2 : 1),
132
151
  ...props
133
152
  });
134
153
  const ListItemActions = ({ className, ...props }) => /* @__PURE__ */ jsx("div", {
@@ -1,6 +1,7 @@
1
1
  import { LoaderCircleIcon } from "../icons.js";
2
2
  import { cn } from "../lib/utils.js";
3
3
  import { StellaMark } from "./stella-mark.js";
4
+ import { TruncatedText } from "./truncated-text.js";
4
5
  import { jsx, jsxs } from "react/jsx-runtime";
5
6
  // src/components/loader.tsx
6
7
  /**
@@ -41,8 +42,9 @@ const LoaderState = ({ label, detail, hint, className }) => /* @__PURE__ */ jsxs
41
42
  className: "text-foreground text-sm font-medium",
42
43
  children: label
43
44
  }),
44
- detail !== void 0 && /* @__PURE__ */ jsx("p", {
45
- className: "text-muted-foreground truncate text-xs tabular-nums",
45
+ detail !== void 0 && /* @__PURE__ */ jsx(TruncatedText, {
46
+ className: "text-muted-foreground text-xs tabular-nums",
47
+ render: /* @__PURE__ */ jsx("p", {}),
46
48
  children: detail
47
49
  }),
48
50
  hint !== void 0 && /* @__PURE__ */ jsx("p", {
@@ -7,6 +7,7 @@ import { DirectionalIcon } from "./directional-icon.js";
7
7
  import { reconcileOutlineItems, retainOutlineIdSet } from "./outline-rail.logic.js";
8
8
  import { ScrollArea } from "./scroll-area.js";
9
9
  import { Tooltip, TooltipContent as TooltipPopup, TooltipTrigger } from "./tooltip.js";
10
+ import { TruncatedText } from "./truncated-text.js";
10
11
  import { Fragment, jsx, jsxs } from "react/jsx-runtime";
11
12
  import { memo, useCallback, useEffect, useId, useLayoutEffect, useMemo, useRef, useState } from "react";
12
13
  import { Temporal } from "temporal-polyfill/full";
@@ -168,14 +169,15 @@ const OutlineRowView = ({ item, hasChildren, isCollapsed, isActive, isHovered, i
168
169
  onClick: () => onJump(item.id),
169
170
  type: "button"
170
171
  }),
171
- children: item.title === void 0 ? /* @__PURE__ */ jsx("span", {
172
- className: "min-w-0 truncate",
172
+ children: item.title === void 0 ? /* @__PURE__ */ jsx(TruncatedText, {
173
+ hint: false,
173
174
  children: item.label
174
175
  }) : /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx("span", {
175
176
  className: "shrink-0 font-medium",
176
177
  children: item.label
177
- }), /* @__PURE__ */ jsx("span", {
178
- className: "min-w-0 truncate font-normal",
178
+ }), /* @__PURE__ */ jsx(TruncatedText, {
179
+ className: "font-normal",
180
+ hint: false,
179
181
  children: item.title
180
182
  })] })
181
183
  }), /* @__PURE__ */ jsx(TooltipPopup, { children: outlineEntryText(item) })] }),
@@ -0,0 +1,8 @@
1
+ import { RunMarkSize, RunMarkState, RunStateMarkContract } from "./run-state-mark.shared.js";
2
+ /**
3
+ * Where one run of work stands, as a glyph: a bold "!" when it waits on the
4
+ * reader, a turning ring while it works (./run-state-mark.shared.ts). Its
5
+ * meaning is named by the element around it.
6
+ */
7
+ export declare const RunStateMark: ({ size, state }: RunStateMarkContract) => import("react").JSX.Element;
8
+ export type { RunMarkSize, RunMarkState };
@@ -0,0 +1,30 @@
1
+ import { cn } from "../lib/utils.js";
2
+ import { RUN_MARK_SIZES, RUN_MARK_TONES, RUN_STATE_MARKS } from "./run-state-mark.shared.js";
3
+ import { StatusMark } from "./status-mark.js";
4
+ import { jsx } from "react/jsx-runtime";
5
+ import { panic } from "better-result";
6
+ // src/components/run-state-mark.tsx
7
+ /**
8
+ * Where one run of work stands, as a glyph: a bold "!" when it waits on the
9
+ * reader, a turning ring while it works (./run-state-mark.shared.ts). Its
10
+ * meaning is named by the element around it.
11
+ */
12
+ const RunStateMark = ({ size = "row", state }) => {
13
+ const { glyph, tone } = RUN_STATE_MARKS[state];
14
+ const { glyphClass, markClass } = RUN_MARK_SIZES[size];
15
+ const { textClass } = RUN_MARK_TONES[tone];
16
+ switch (glyph) {
17
+ case "attention": return /* @__PURE__ */ jsx("span", {
18
+ "aria-hidden": true,
19
+ className: cn(textClass, glyphClass, "leading-none font-bold"),
20
+ "data-slot": "run-state-mark",
21
+ children: "!"
22
+ });
23
+ case "running": return /* @__PURE__ */ jsx(StatusMark, {
24
+ className: cn(textClass, markClass),
25
+ status: "running"
26
+ });
27
+ default: return panic("Unhandled run mark glyph");
28
+ }
29
+ };
30
+ export { RunStateMark };
@@ -0,0 +1,9 @@
1
+ import { RunMarkSize, RunMarkState, RunStateMarkContract } from "./run-state-mark.shared.js";
2
+ /**
3
+ * The web's `RunStateMark` (./run-state-mark.tsx) on a phone: the bold "!"
4
+ * in the embedded bold face, or the web `StatusMark`'s running ring drawn
5
+ * with react-native-svg. Hidden from assistive technology, as on the web:
6
+ * the row or heading around it names the state.
7
+ */
8
+ export declare const RunStateMark: ({ size, state }: RunStateMarkContract) => import("react").JSX.Element;
9
+ export type { RunMarkSize, RunMarkState };
@@ -0,0 +1,98 @@
1
+ import { RUN_MARK_SIZES, RUN_MARK_TONES, RUN_STATE_MARKS } from "./run-state-mark.shared.js";
2
+ import { STATUS_MARK_RING_RADIUS, STATUS_MARK_RUNNING_ARC, STATUS_MARK_SPIN_MS, STATUS_MARK_STROKE_WIDTH, STATUS_MARK_TRACK_OPACITY, STATUS_MARK_VIEWBOX } from "./status-mark.shared.js";
3
+ import { sansFontFamily } from "./text.native.js";
4
+ import { jsx, jsxs } from "react/jsx-runtime";
5
+ import { useEffect, useState } from "react";
6
+ import { panic } from "better-result";
7
+ import { Animated, Easing, Text } from "react-native";
8
+ import Svg, { Circle } from "react-native-svg";
9
+ import { useCSSVariable } from "uniwind";
10
+ // src/components/run-state-mark.native.tsx
11
+ /**
12
+ * The web's `RunStateMark` (./run-state-mark.tsx) on a phone: the bold "!"
13
+ * in the embedded bold face, or the web `StatusMark`'s running ring drawn
14
+ * with react-native-svg. Hidden from assistive technology, as on the web:
15
+ * the row or heading around it names the state.
16
+ */
17
+ const RunStateMark = ({ size = "row", state }) => {
18
+ const { glyph, tone } = RUN_STATE_MARKS[state];
19
+ const { glyphClass, px } = RUN_MARK_SIZES[size];
20
+ const { textClass, variable } = RUN_MARK_TONES[tone];
21
+ switch (glyph) {
22
+ case "attention": return /* @__PURE__ */ jsx(Text, {
23
+ accessibilityElementsHidden: true,
24
+ className: `${textClass} ${glyphClass} leading-none`,
25
+ importantForAccessibility: "no-hide-descendants",
26
+ style: { fontFamily: sansFontFamily("bold") },
27
+ children: "!"
28
+ });
29
+ case "running": return /* @__PURE__ */ jsx(RunningRing, {
30
+ colorVariable: variable,
31
+ px
32
+ });
33
+ default: return panic("Unhandled run mark glyph");
34
+ }
35
+ };
36
+ const CENTRE = 8;
37
+ const CIRCUMFERENCE = 2 * Math.PI * STATUS_MARK_RING_RADIUS;
38
+ const ARC_DASHES = [CIRCUMFERENCE * STATUS_MARK_RUNNING_ARC, CIRCUMFERENCE * (1 - STATUS_MARK_RUNNING_ARC)];
39
+ /**
40
+ * The arc turns on its faint track. It keeps turning under Reduce Motion, as
41
+ * the platform's own activity indicator does; the ring is too small to move
42
+ * anything else on screen.
43
+ */
44
+ const RunningRing = ({ colorVariable, px }) => {
45
+ const resolved = useCSSVariable(colorVariable);
46
+ const color = typeof resolved === "string" ? resolved : "currentColor";
47
+ const [turn] = useState(() => new Animated.Value(0));
48
+ useEffect(() => {
49
+ const spin = Animated.loop(Animated.timing(turn, {
50
+ duration: STATUS_MARK_SPIN_MS,
51
+ easing: (progress) => Easing.linear(progress),
52
+ isInteraction: false,
53
+ toValue: 1,
54
+ useNativeDriver: true
55
+ }));
56
+ spin.start();
57
+ return () => {
58
+ spin.stop();
59
+ };
60
+ }, [turn]);
61
+ const rotate = turn.interpolate({
62
+ inputRange: [0, 1],
63
+ outputRange: ["0deg", "360deg"]
64
+ });
65
+ return /* @__PURE__ */ jsx(Animated.View, {
66
+ accessibilityElementsHidden: true,
67
+ importantForAccessibility: "no-hide-descendants",
68
+ style: {
69
+ height: px,
70
+ transform: [{ rotate }],
71
+ width: px
72
+ },
73
+ children: /* @__PURE__ */ jsxs(Svg, {
74
+ fill: "none",
75
+ height: px,
76
+ viewBox: STATUS_MARK_VIEWBOX,
77
+ width: px,
78
+ children: [/* @__PURE__ */ jsx(Circle, {
79
+ cx: CENTRE,
80
+ cy: CENTRE,
81
+ opacity: STATUS_MARK_TRACK_OPACITY,
82
+ r: STATUS_MARK_RING_RADIUS,
83
+ stroke: color,
84
+ strokeWidth: STATUS_MARK_STROKE_WIDTH
85
+ }), /* @__PURE__ */ jsx(Circle, {
86
+ cx: CENTRE,
87
+ cy: CENTRE,
88
+ r: STATUS_MARK_RING_RADIUS,
89
+ stroke: color,
90
+ strokeDasharray: ARC_DASHES,
91
+ strokeLinecap: "round",
92
+ strokeWidth: STATUS_MARK_STROKE_WIDTH,
93
+ transform: `rotate(-90 ${CENTRE} ${CENTRE})`
94
+ })]
95
+ })
96
+ });
97
+ };
98
+ export { RunStateMark };
@@ -0,0 +1,53 @@
1
+ /** The theme colours a run mark takes, as the classes and variables naming them. */
2
+ export declare const RUN_MARK_TONES: {
3
+ readonly muted: {
4
+ readonly textClass: "text-muted-foreground";
5
+ readonly variable: "--color-muted-foreground";
6
+ };
7
+ readonly warning: {
8
+ readonly textClass: "text-warning-foreground";
9
+ readonly variable: "--color-warning-foreground";
10
+ };
11
+ };
12
+ /**
13
+ * Each run state's glyph and tone. Its keys are the states the API reports
14
+ * (`CHAT_ACTIVE_RUN_STATE` in `@stll/api-contract/chat`, which this published
15
+ * package cannot import). A surface passes the state it read, so a state the
16
+ * API adds stops compiling at every surface until it has a mark here.
17
+ */
18
+ export declare const RUN_STATE_MARKS: {
19
+ readonly "needs-you": {
20
+ readonly glyph: "attention";
21
+ readonly tone: "warning";
22
+ };
23
+ readonly working: {
24
+ readonly glyph: "running";
25
+ readonly tone: "muted";
26
+ };
27
+ };
28
+ export type RunMarkState = keyof typeof RUN_STATE_MARKS;
29
+ /**
30
+ * `row` sits in a line of text, `badge` on a small icon. The pixel size is a
31
+ * phone's; the web sets the same size with the classes.
32
+ */
33
+ export declare const RUN_MARK_SIZES: {
34
+ readonly row: {
35
+ readonly glyphClass: "text-xs";
36
+ readonly markClass: "size-3";
37
+ readonly px: 12;
38
+ };
39
+ readonly badge: {
40
+ readonly glyphClass: "text-3xs";
41
+ readonly markClass: "size-2.5";
42
+ readonly px: 10;
43
+ };
44
+ };
45
+ export type RunMarkSize = keyof typeof RUN_MARK_SIZES;
46
+ /**
47
+ * The props every run mark renderer accepts. The mark is decorative: the
48
+ * element around it names the state for assistive technology.
49
+ */
50
+ export type RunStateMarkContract = {
51
+ state: RunMarkState;
52
+ size?: RunMarkSize;
53
+ };
@@ -0,0 +1,45 @@
1
+ // src/components/run-state-mark.shared.ts
2
+ /** The theme colours a run mark takes, as the classes and variables naming them. */
3
+ const RUN_MARK_TONES = {
4
+ muted: {
5
+ textClass: "text-muted-foreground",
6
+ variable: "--color-muted-foreground"
7
+ },
8
+ warning: {
9
+ textClass: "text-warning-foreground",
10
+ variable: "--color-warning-foreground"
11
+ }
12
+ };
13
+ /**
14
+ * Each run state's glyph and tone. Its keys are the states the API reports
15
+ * (`CHAT_ACTIVE_RUN_STATE` in `@stll/api-contract/chat`, which this published
16
+ * package cannot import). A surface passes the state it read, so a state the
17
+ * API adds stops compiling at every surface until it has a mark here.
18
+ */
19
+ const RUN_STATE_MARKS = {
20
+ "needs-you": {
21
+ glyph: "attention",
22
+ tone: "warning"
23
+ },
24
+ working: {
25
+ glyph: "running",
26
+ tone: "muted"
27
+ }
28
+ };
29
+ /**
30
+ * `row` sits in a line of text, `badge` on a small icon. The pixel size is a
31
+ * phone's; the web sets the same size with the classes.
32
+ */
33
+ const RUN_MARK_SIZES = {
34
+ row: {
35
+ glyphClass: "text-xs",
36
+ markClass: "size-3",
37
+ px: 12
38
+ },
39
+ badge: {
40
+ glyphClass: "text-3xs",
41
+ markClass: "size-2.5",
42
+ px: 10
43
+ }
44
+ };
45
+ export { RUN_MARK_SIZES, RUN_MARK_TONES, RUN_STATE_MARKS };
@@ -1,8 +1,8 @@
1
1
  "use client";
2
2
  import { cn } from "../lib/utils.js";
3
+ import { STATUS_MARK_RING_RADIUS, STATUS_MARK_STROKE_WIDTH, STATUS_MARK_VIEWBOX } from "./status-mark.shared.js";
3
4
  import { jsx, jsxs } from "react/jsx-runtime";
4
5
  // src/components/status-mark.tsx
5
- const RING_RADIUS = 6.25;
6
6
  const CHECK_PATH = "M5.4 8.3 7.2 10.1 10.6 6.3";
7
7
  const CROSS_PATH = "M6.1 6.1 9.9 9.9M9.9 6.1 6.1 9.9";
8
8
  const on = {
@@ -26,8 +26,8 @@ const StatusMark = ({ className, label, status }) => /* @__PURE__ */ jsxs("svg",
26
26
  stroke: "currentColor",
27
27
  strokeLinecap: "round",
28
28
  strokeLinejoin: "round",
29
- strokeWidth: 1.5,
30
- viewBox: "0 0 16 16",
29
+ strokeWidth: STATUS_MARK_STROKE_WIDTH,
30
+ viewBox: STATUS_MARK_VIEWBOX,
31
31
  ...label === void 0 ? { "aria-hidden": true } : {
32
32
  "aria-label": label,
33
33
  role: "img"
@@ -38,14 +38,14 @@ const StatusMark = ({ className, label, status }) => /* @__PURE__ */ jsxs("svg",
38
38
  cx: "8",
39
39
  cy: "8",
40
40
  fill: "currentColor",
41
- r: RING_RADIUS,
41
+ r: STATUS_MARK_RING_RADIUS,
42
42
  stroke: "none"
43
43
  }),
44
44
  /* @__PURE__ */ jsx("circle", {
45
45
  className: "opacity-0 transition-opacity duration-200 group-data-[status=running]/status:opacity-20",
46
46
  cx: "8",
47
47
  cy: "8",
48
- r: RING_RADIUS
48
+ r: STATUS_MARK_RING_RADIUS
49
49
  }),
50
50
  /* @__PURE__ */ jsx("g", {
51
51
  className: SPIN_CLASS,
@@ -54,7 +54,7 @@ const StatusMark = ({ className, label, status }) => /* @__PURE__ */ jsxs("svg",
54
54
  cx: "8",
55
55
  cy: "8",
56
56
  pathLength: 100,
57
- r: RING_RADIUS,
57
+ r: STATUS_MARK_RING_RADIUS,
58
58
  transform: "rotate(-90 8 8)"
59
59
  })
60
60
  }),
@@ -0,0 +1,12 @@
1
+ export declare const STATUS_MARK_VIEWBOX = "0 0 16 16";
2
+ export declare const STATUS_MARK_STROKE_WIDTH = 1.5;
3
+ export declare const STATUS_MARK_RING_RADIUS = 6.25;
4
+ /**
5
+ * The running arc's share of the ring and the faint track it runs on. The
6
+ * web spells them as utilities (`[stroke-dasharray:30_70]`, `opacity-20`),
7
+ * which must stay literal for Tailwind to find them.
8
+ */
9
+ export declare const STATUS_MARK_RUNNING_ARC = 0.3;
10
+ export declare const STATUS_MARK_TRACK_OPACITY = 0.2;
11
+ /** One turn of the running arc: `--animate-status-spin` in the theme. */
12
+ export declare const STATUS_MARK_SPIN_MS = 1100;
@@ -0,0 +1,14 @@
1
+ // src/components/status-mark.shared.ts
2
+ const STATUS_MARK_VIEWBOX = "0 0 16 16";
3
+ const STATUS_MARK_STROKE_WIDTH = 1.5;
4
+ const STATUS_MARK_RING_RADIUS = 6.25;
5
+ /**
6
+ * The running arc's share of the ring and the faint track it runs on. The
7
+ * web spells them as utilities (`[stroke-dasharray:30_70]`, `opacity-20`),
8
+ * which must stay literal for Tailwind to find them.
9
+ */
10
+ const STATUS_MARK_RUNNING_ARC = .3;
11
+ const STATUS_MARK_TRACK_OPACITY = .2;
12
+ /** One turn of the running arc: `--animate-status-spin` in the theme. */
13
+ const STATUS_MARK_SPIN_MS = 1100;
14
+ export { STATUS_MARK_RING_RADIUS, STATUS_MARK_RUNNING_ARC, STATUS_MARK_SPIN_MS, STATUS_MARK_STROKE_WIDTH, STATUS_MARK_TRACK_OPACITY, STATUS_MARK_VIEWBOX };
@@ -90,7 +90,7 @@ type TableCellProps = Omit<React$1.ComponentProps<"td">, "align"> & {
90
90
  */
91
91
  flush?: boolean;
92
92
  };
93
- declare const TableCell: ({ className, align, tone, numeric, truncate, flush, ...props }: TableCellProps) => React$1.JSX.Element;
93
+ declare const TableCell: ({ className, align, tone, numeric, truncate, flush, children, ...props }: TableCellProps) => React$1.JSX.Element;
94
94
  type SortDirection = "asc" | "desc" | null;
95
95
  type SortableHeadProps = Omit<TableHeadProps, "onClick"> & {
96
96
  sortDirection?: SortDirection;
@@ -1,6 +1,7 @@
1
1
  import { ArrowDownIcon, ArrowUpIcon } from "../icons.js";
2
2
  import { cn } from "../lib/utils.js";
3
3
  import { Button } from "./button.js";
4
+ import { TruncatedText } from "./truncated-text.js";
4
5
  import { jsx, jsxs } from "react/jsx-runtime";
5
6
  // src/components/table.tsx
6
7
  const Table = ({ className, density = "default", inset = "default", scroll = "self", ...props }) => {
@@ -49,12 +50,20 @@ const TableHead = ({ className, align, numeric = false, ...props }) => /* @__PUR
49
50
  "data-slot": "table-head",
50
51
  ...props
51
52
  });
52
- const TableCell = ({ className, align, tone = "default", numeric = false, truncate = false, flush = false, ...props }) => {
53
+ const TableCell = ({ className, align, tone = "default", numeric = false, truncate = false, flush = false, children, ...props }) => {
53
54
  const resolvedAlign = resolveTableAlign(align, numeric);
55
+ const cellClassName = cn("align-middle leading-none whitespace-nowrap has-[[role=checkbox]]:pe-0", flush ? "p-0" : "p-2.5 in-data-[density=compact]:px-0 in-data-[density=compact]:py-1.5 in-data-[inset=card]:px-4 in-data-[slot=frame]:first:p-[calc(--spacing(2.5)-1px)] in-data-[slot=frame]:last:p-[calc(--spacing(2.5)-1px)]", "in-data-[density=compact]:leading-6 in-data-[density=compact]:whitespace-normal", resolvedAlign !== void 0 && TABLE_ALIGN_CLASS[resolvedAlign], tone === "muted" && "text-muted-foreground", numeric && "tabular-nums", className);
56
+ if (truncate) return /* @__PURE__ */ jsx(TruncatedText, {
57
+ className: cn("table-cell", cellClassName),
58
+ "data-slot": "table-cell",
59
+ render: /* @__PURE__ */ jsx("td", { ...props }),
60
+ children
61
+ });
54
62
  return /* @__PURE__ */ jsx("td", {
55
- className: cn("align-middle leading-none whitespace-nowrap has-[[role=checkbox]]:pe-0", flush ? "p-0" : "p-2.5 in-data-[density=compact]:px-0 in-data-[density=compact]:py-1.5 in-data-[inset=card]:px-4 in-data-[slot=frame]:first:p-[calc(--spacing(2.5)-1px)] in-data-[slot=frame]:last:p-[calc(--spacing(2.5)-1px)]", "in-data-[density=compact]:leading-6 in-data-[density=compact]:whitespace-normal", resolvedAlign !== void 0 && TABLE_ALIGN_CLASS[resolvedAlign], tone === "muted" && "text-muted-foreground", numeric && "tabular-nums", truncate && "truncate", className),
63
+ className: cellClassName,
56
64
  "data-slot": "table-cell",
57
- ...props
65
+ ...props,
66
+ children
58
67
  });
59
68
  };
60
69
  /**
@@ -82,14 +91,11 @@ const SortableHead = ({ children, sortDirection = null, onSort, trailing, classN
82
91
  children: /* @__PURE__ */ jsxs("span", {
83
92
  className: "inline-flex items-center gap-1",
84
93
  children: [/* @__PURE__ */ jsxs(Button, {
85
- className: cn("min-w-0 gap-1", endAligned ? "-me-3 flex-row-reverse" : "-ms-3"),
94
+ className: cn("min-w-0 gap-1 px-2.5", endAligned ? "-me-2.5 flex-row-reverse" : "-ms-2.5"),
86
95
  onClick: onSort,
87
96
  type: "button",
88
97
  variant: "muted",
89
- children: [/* @__PURE__ */ jsx("span", {
90
- className: "truncate",
91
- children
92
- }), /* @__PURE__ */ jsx("span", {
98
+ children: [/* @__PURE__ */ jsx(TruncatedText, { children }), /* @__PURE__ */ jsx("span", {
93
99
  "aria-hidden": true,
94
100
  className: "inline-flex w-3 shrink-0 justify-center",
95
101
  children: sortIcon
@@ -1,5 +1,7 @@
1
- import { TextContract, TextVariant } from "./text.shared.js";
1
+ import { TextContract, TextFace, TextVariant } from "./text.shared.js";
2
2
  import { TextProps } from "react-native";
3
+ /** The embedded face of one weight, for a glyph set outside every role. */
4
+ export declare const sansFontFamily: (weight: TextFace["weight"]) => string;
3
5
  /** The font a role is set in, for a native control that draws its own text. */
4
6
  export declare const textFontFamily: (variant: TextContract["variant"]) => string;
5
7
  /**
@@ -22,8 +22,10 @@ const NOT_A_COLOUR = /^text-(?:xs|sm|base|lg|\d*xl|\[|left|center|right|justify|
22
22
  * whatever order they were written in.
23
23
  */
24
24
  const withForeground = (classes) => classes.split(/\s+/u).some((name) => name.startsWith("text-") && !NOT_A_COLOUR.test(name)) ? classes : `text-foreground ${classes}`;
25
+ /** The embedded face of one weight, for a glyph set outside every role. */
26
+ const sansFontFamily = (weight) => SANS_FACES[weight];
25
27
  /** The font a role is set in, for a native control that draws its own text. */
26
- const textFontFamily = (variant) => SANS_FACES[TEXT_VARIANT_FACES[variant ?? "body"].weight];
28
+ const textFontFamily = (variant) => sansFontFamily(TEXT_VARIANT_FACES[variant ?? "body"].weight);
27
29
  /**
28
30
  * Text in one of the design system's roles (./text.shared.ts), as the web
29
31
  * renderer (./text.tsx) sets it: the same size and colour, in the role's
@@ -34,4 +36,4 @@ const Text = ({ className, style, variant = "body", ...props }) => /* @__PURE__
34
36
  style: [{ fontFamily: textFontFamily(variant) }, style],
35
37
  ...props
36
38
  });
37
- export { Text, textFontFamily };
39
+ export { Text, sansFontFamily, textFontFamily };
@@ -14,10 +14,21 @@ import { useRender } from "@base-ui/react/use-render";
14
14
  * The tap popover stays out of the way when the text sits inside a link,
15
15
  * button or other control: the tap belongs to that control.
16
16
  */
17
- declare const TruncatedText: ({ children, className, lines, render, side, tooltip, ...props }: TruncatedTextProps) => React$1.JSX.Element;
18
- /** How many lines the text may take before it is cut. */
19
- type TruncatedTextLines = 1 | 2 | 3;
17
+ declare const TruncatedText: ({ children, className, hint, lines, render, side, tooltip, ...props }: TruncatedTextProps) => React$1.JSX.Element;
18
+ /**
19
+ * How many lines the text may take before it is cut; `"none"` lets it take
20
+ * as many as it needs, for text that is cut only in some states (collapsed,
21
+ * compact) and shown whole in the others.
22
+ */
23
+ type TruncatedTextLines = 1 | 2 | 3 | 4 | 5 | 6 | 8 | "none";
20
24
  type TruncatedTextProps = useRender.ComponentProps<"span"> & {
25
+ /**
26
+ * Whether cut text shows its whole self on hover, focus or tap. Pass false
27
+ * only where something around the text already shows it (a tooltip on the
28
+ * row, a title the text sits in); the text stays inside its container
29
+ * either way.
30
+ */
31
+ hint?: boolean;
21
32
  lines?: TruncatedTextLines;
22
33
  side?: React$1.ComponentProps<typeof TooltipPopup>["side"];
23
34
  /**
@@ -21,7 +21,7 @@ import { useRender } from "@base-ui/react/use-render";
21
21
  * The tap popover stays out of the way when the text sits inside a link,
22
22
  * button or other control: the tap belongs to that control.
23
23
  */
24
- const TruncatedText = ({ children, className, lines = 1, render, side = "top", tooltip, ...props }) => {
24
+ const TruncatedText = ({ children, className, hint = true, lines = 1, render, side = "top", tooltip, ...props }) => {
25
25
  const ref = React$1.useRef(null);
26
26
  const [surface, setSurface] = React$1.useState(null);
27
27
  const content = tooltip ?? children;
@@ -31,7 +31,7 @@ const TruncatedText = ({ children, className, lines = 1, render, side = "top", t
31
31
  return;
32
32
  }
33
33
  const element = ref.current;
34
- if (element === null || LAST_POINTER_TYPE.get(element) === "touch") return;
34
+ if (element === null || LAST_POINTER_TYPE.get(element) === "touch" || isInsideTooltipTrigger(element)) return;
35
35
  if (isTextOverflowing(element)) setSurface("tooltip");
36
36
  };
37
37
  const handleClick = (event) => {
@@ -49,6 +49,7 @@ const TruncatedText = ({ children, className, lines = 1, render, side = "top", t
49
49
  ref,
50
50
  render
51
51
  });
52
+ if (!hint) return text;
52
53
  return /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsxs(Tooltip, {
53
54
  onOpenChange: handleTooltipOpenChange,
54
55
  open: surface === "tooltip",
@@ -92,10 +93,20 @@ function recordPointerType(event) {
92
93
  function forgetPointerType(event) {
93
94
  LAST_POINTER_TYPE.delete(event.currentTarget);
94
95
  }
96
+ /**
97
+ * `max-w-full` caps the text at its container even where the container does
98
+ * not stretch it (a flex column with `items-start`, an inline-block): without
99
+ * it a nowrap line sizes to its whole text and runs past the container.
100
+ */
95
101
  const TRUNCATED_TEXT_LINES_CLASS = {
96
- 1: "block min-w-0 truncate",
97
- 2: "line-clamp-2 min-w-0 wrap-anywhere",
98
- 3: "line-clamp-3 min-w-0 wrap-anywhere"
102
+ 1: "block min-w-0 max-w-full truncate",
103
+ 2: "line-clamp-2 min-w-0 max-w-full wrap-anywhere",
104
+ 3: "line-clamp-3 min-w-0 max-w-full wrap-anywhere",
105
+ 4: "line-clamp-4 min-w-0 max-w-full wrap-anywhere",
106
+ 5: "line-clamp-5 min-w-0 max-w-full wrap-anywhere",
107
+ 6: "line-clamp-6 min-w-0 max-w-full wrap-anywhere",
108
+ 8: "line-clamp-8 min-w-0 max-w-full wrap-anywhere",
109
+ none: "min-w-0 max-w-full"
99
110
  };
100
111
  /** Whether the element's text is cut by its box, in either direction. */
101
112
  function isTextOverflowing(element) {
@@ -103,6 +114,11 @@ function isTextOverflowing(element) {
103
114
  return element.scrollWidth > element.clientWidth || element.scrollHeight > element.clientHeight;
104
115
  }
105
116
  const CONTROL_SELECTOR = "a[href], button, label, summary, [role=button], [role=link], [role=menuitem], [role=option], [role=tab]";
117
+ /** Whether a tooltip around the element already opens on its pointer. */
118
+ function isInsideTooltipTrigger(element) {
119
+ const parent = element.parentElement;
120
+ return parent !== null && parent.closest("[data-slot=\"tooltip-trigger\"]") !== null;
121
+ }
106
122
  /** Whether a tap on the element already belongs to a control around it. */
107
123
  function isInsideControl(element) {
108
124
  return element.closest(CONTROL_SELECTOR) !== null;