@tangle-network/ui 11.34.0 → 11.36.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 (78) hide show
  1. package/CHANGELOG.md +12 -0
  2. package/README.md +1 -1
  3. package/dist/index.d.ts +4 -3
  4. package/dist/index.js +2 -1
  5. package/dist/lib/control-presentation.d.ts +34 -1
  6. package/dist/lib/control-presentation.js +1 -1
  7. package/dist/lib/focus.d.ts +2 -2
  8. package/dist/lib/focus.js +3 -2
  9. package/dist/lib/motion.d.ts +4 -0
  10. package/dist/lib/motion.js +7 -0
  11. package/dist/lib/surface-motion.js +84 -0
  12. package/dist/presentation.d.ts +88 -0
  13. package/dist/presentation.js +183 -0
  14. package/dist/primitives/alert-dialog.js +1 -1
  15. package/dist/primitives/alert.d.ts +1 -2
  16. package/dist/primitives/button.d.ts +2 -5
  17. package/dist/primitives/button.js +3 -30
  18. package/dist/primitives/card.js +3 -7
  19. package/dist/primitives/date-picker.d.ts +1 -1
  20. package/dist/primitives/dialog.js +29 -16
  21. package/dist/primitives/empty-state.js +6 -5
  22. package/dist/primitives/heading.d.ts +1 -0
  23. package/dist/primitives/heading.js +3 -2
  24. package/dist/primitives/help-text.js +2 -1
  25. package/dist/primitives/input.d.ts +1 -4
  26. package/dist/primitives/input.js +3 -24
  27. package/dist/primitives/label.js +1 -2
  28. package/dist/primitives/metric-strip.js +7 -12
  29. package/dist/primitives/page-header.d.ts +1 -0
  30. package/dist/primitives/page-header.js +7 -6
  31. package/dist/primitives/page-shell.d.ts +8 -2
  32. package/dist/primitives/page-shell.js +21 -2
  33. package/dist/primitives/pagination.js +1 -1
  34. package/dist/primitives/segmented-control.d.ts +6 -3
  35. package/dist/primitives/segmented-control.js +105 -56
  36. package/dist/primitives/sheet.js +29 -19
  37. package/dist/primitives/skeleton.js +1 -1
  38. package/dist/primitives/status-pill.d.ts +3 -1
  39. package/dist/primitives/status-pill.js +12 -3
  40. package/dist/primitives/table.js +10 -9
  41. package/dist/primitives/tabs.js +3 -12
  42. package/dist/primitives/tooltip.js +11 -3
  43. package/dist/primitives.d.ts +3 -2
  44. package/dist/primitives.js +2 -1
  45. package/dist/utils.d.ts +1 -1
  46. package/package.json +15 -5
  47. package/src/lib/control-presentation.ts +2 -4
  48. package/src/lib/focus.ts +3 -2
  49. package/src/lib/motion.ts +7 -0
  50. package/src/lib/surface-motion.tsx +78 -0
  51. package/src/presentation.ts +205 -0
  52. package/src/primitives/alert.tsx +1 -2
  53. package/src/primitives/button.tsx +4 -40
  54. package/src/primitives/card.tsx +5 -10
  55. package/src/primitives/checkbox-separator-sheet-tooltip.test.tsx +2 -1
  56. package/src/primitives/control-presentation.test.tsx +2 -7
  57. package/src/primitives/dialog.tsx +19 -18
  58. package/src/primitives/empty-state.tsx +6 -5
  59. package/src/primitives/heading.tsx +4 -2
  60. package/src/primitives/help-text.tsx +3 -2
  61. package/src/primitives/input.tsx +3 -31
  62. package/src/primitives/label.tsx +2 -4
  63. package/src/primitives/metric-strip.tsx +8 -17
  64. package/src/primitives/page-header.tsx +8 -5
  65. package/src/primitives/page-shell.tsx +13 -2
  66. package/src/primitives/segmented-control.stories.tsx +89 -0
  67. package/src/primitives/segmented-control.test.tsx +20 -7
  68. package/src/primitives/segmented-control.tsx +90 -29
  69. package/src/primitives/sheet.tsx +16 -11
  70. package/src/primitives/skeleton.tsx +1 -1
  71. package/src/primitives/status-pill.test.tsx +1 -0
  72. package/src/primitives/status-pill.tsx +10 -4
  73. package/src/primitives/table.tsx +10 -9
  74. package/src/primitives/tabs.tsx +5 -17
  75. package/src/primitives/tooltip.tsx +6 -5
  76. package/src/styles/compat.css +12 -0
  77. package/src/styles/presentation.css +30 -0
  78. package/src/primitives/page-shell.test.tsx +0 -24
@@ -5,14 +5,39 @@ import { cn } from "../lib/utils.js";
5
5
  import "../lib/control-presentation.js";
6
6
  import * as React from "react";
7
7
  import { jsx, jsxs } from "react/jsx-runtime";
8
+ import { ChevronLeft, ChevronRight } from "lucide-react";
8
9
  //#region src/primitives/segmented-control.tsx
9
- /** Width of the faded edge that marks more tabs past the strip, in px. */
10
- const TAB_EDGE_FADE = 24;
11
- const tabEdgeFade = [
12
- "data-[overflow-start]:[mask-image:linear-gradient(to_right,transparent,#000_24px)]",
13
- "data-[overflow-end]:[mask-image:linear-gradient(to_right,#000_calc(100%-24px),transparent)]",
14
- "data-[overflow-start]:data-[overflow-end]:[mask-image:linear-gradient(to_right,transparent,#000_24px,#000_calc(100%-24px),transparent)]"
15
- ];
10
+ /** Size of the edge chevron, in px. The selected option is kept this far
11
+ * (plus the strip's own padding) from an edge so the chevron never covers it. */
12
+ const SCROLL_CUE_SIZE = 32;
13
+ /**
14
+ * A chevron pinned to one edge of the scrolling strip. It is a sticky flex
15
+ * item with a negative margin equal to its width plus the gap, so it takes no room in the
16
+ * row (showing it never reflows the options) and stays at the visible edge
17
+ * while the strip scrolls. A solid surface keeps it legible on any plane,
18
+ * which an edge fade is not: a fade over the gap between two options, or over
19
+ * a host's inline padding, shows nothing.
20
+ */
21
+ function ScrollCue({ side, onPage }) {
22
+ const Icon = side === "start" ? ChevronLeft : ChevronRight;
23
+ return /* @__PURE__ */ jsx("button", {
24
+ type: "button",
25
+ tabIndex: -1,
26
+ "aria-hidden": "true",
27
+ "data-scroll-cue": side,
28
+ onClick: onPage,
29
+ className: cn("sticky z-[1] flex shrink-0 items-center justify-center self-center rounded-full border border-border bg-card text-muted-foreground shadow-sm transition-colors hover:text-foreground", side === "start" ? "left-0" : "right-0"),
30
+ style: {
31
+ width: SCROLL_CUE_SIZE,
32
+ height: SCROLL_CUE_SIZE,
33
+ [side === "start" ? "marginRight" : "marginLeft"]: -36
34
+ },
35
+ children: /* @__PURE__ */ jsx(Icon, {
36
+ className: "h-4 w-4",
37
+ "aria-hidden": "true"
38
+ })
39
+ });
40
+ }
16
41
  function SegmentedControl({ value, onValueChange, options, variant = "row", className, ...rest }) {
17
42
  if (typeof process !== "undefined" && process?.env?.NODE_ENV !== "production" && !rest["aria-label"] && !rest["aria-labelledby"]) console.warn("[SegmentedControl] role=\"radiogroup\" requires either aria-label or aria-labelledby for accessibility.");
18
43
  const optionRefs = React.useRef(/* @__PURE__ */ new Map());
@@ -21,7 +46,7 @@ function SegmentedControl({ value, onValueChange, options, variant = "row", clas
21
46
  start: false,
22
47
  end: false
23
48
  });
24
- const tabs = variant === "tabs";
49
+ const optionKey = options.map((o) => o.value).join("\0");
25
50
  const measureOverflow = React.useCallback(() => {
26
51
  const strip = stripRef.current;
27
52
  if (!strip) return;
@@ -32,31 +57,45 @@ function SegmentedControl({ value, onValueChange, options, variant = "row", clas
32
57
  end
33
58
  });
34
59
  }, []);
35
- React.useLayoutEffect(() => {
36
- if (!tabs) return;
60
+ const valueRef = React.useRef(value);
61
+ valueRef.current = value;
62
+ const revealSelected = React.useCallback(() => {
37
63
  const strip = stripRef.current;
38
- const option = optionRefs.current.get(value);
39
- if (strip && option) {
64
+ const option = optionRefs.current.get(valueRef.current);
65
+ if (strip && option && strip.scrollWidth > strip.clientWidth) {
66
+ const style = getComputedStyle(strip);
67
+ const reserveStart = 36 + (Number.parseFloat(style.paddingLeft) || 0);
68
+ const reserveEnd = 36 + (Number.parseFloat(style.paddingRight) || 0);
40
69
  const start = option.offsetLeft;
41
70
  const end = start + option.offsetWidth;
42
- if (start < strip.scrollLeft + TAB_EDGE_FADE) strip.scrollLeft = Math.max(0, start - TAB_EDGE_FADE);
43
- else if (end > strip.scrollLeft + strip.clientWidth - TAB_EDGE_FADE) strip.scrollLeft = end - strip.clientWidth + TAB_EDGE_FADE;
71
+ if (start - reserveStart < strip.scrollLeft) strip.scrollLeft = Math.max(0, start - reserveStart);
72
+ else if (end + reserveEnd > strip.scrollLeft + strip.clientWidth) strip.scrollLeft = end + reserveEnd - strip.clientWidth;
44
73
  }
45
74
  measureOverflow();
46
- }, [
47
- tabs,
75
+ }, [measureOverflow]);
76
+ React.useLayoutEffect(revealSelected, [
48
77
  value,
49
78
  options.length,
50
- measureOverflow
79
+ revealSelected
51
80
  ]);
52
81
  React.useEffect(() => {
53
- if (!tabs || typeof ResizeObserver === "undefined") return;
82
+ if (typeof ResizeObserver === "undefined") return;
54
83
  const strip = stripRef.current;
55
84
  if (!strip) return;
56
- const observer = new ResizeObserver(measureOverflow);
85
+ const observer = new ResizeObserver(revealSelected);
57
86
  observer.observe(strip);
87
+ for (const option of optionRefs.current.values()) observer.observe(option);
58
88
  return () => observer.disconnect();
59
- }, [tabs, measureOverflow]);
89
+ }, [optionKey, revealSelected]);
90
+ const page = (direction) => {
91
+ const strip = stripRef.current;
92
+ if (!strip) return;
93
+ const reduce = typeof window !== "undefined" && window.matchMedia?.("(prefers-reduced-motion: reduce)").matches;
94
+ strip.scrollBy({
95
+ left: direction * strip.clientWidth * .75,
96
+ behavior: reduce ? "auto" : "smooth"
97
+ });
98
+ };
60
99
  const hasMatch = options.some((o) => o.value === value);
61
100
  const handleKeyDown = (e) => {
62
101
  if (options.length === 0) return;
@@ -73,7 +112,7 @@ function SegmentedControl({ value, onValueChange, options, variant = "row", clas
73
112
  optionRefs.current.get(options[next].value)?.focus();
74
113
  }
75
114
  };
76
- return /* @__PURE__ */ jsx("div", {
115
+ return /* @__PURE__ */ jsxs("div", {
77
116
  role: "radiogroup",
78
117
  id: rest.id,
79
118
  "aria-label": rest["aria-label"],
@@ -83,41 +122,51 @@ function SegmentedControl({ value, onValueChange, options, variant = "row", clas
83
122
  "data-overflow-end": overflow.end || void 0,
84
123
  ref: stripRef,
85
124
  onKeyDown: handleKeyDown,
86
- onScroll: tabs ? measureOverflow : void 0,
87
- className: cn("flex gap-1", variant === "row" && "flex-wrap items-center rounded-lg border border-[var(--field-border)] bg-[var(--well)] p-1", variant === "tabs" && ["relative flex-nowrap items-end border-b border-border pb-0 overflow-x-auto", tabEdgeFade], className),
88
- children: options.map((option, i) => {
89
- const active = option.value === value;
90
- const focusable = active || !hasMatch && i === 0;
91
- return /* @__PURE__ */ jsxs("button", {
92
- ref: (el) => {
93
- if (el) optionRefs.current.set(option.value, el);
94
- else optionRefs.current.delete(option.value);
95
- },
96
- type: "button",
97
- role: "radio",
98
- "aria-checked": active,
99
- "data-testid": option.testId,
100
- tabIndex: focusable ? 0 : -1,
101
- onClick: () => {
102
- if (!active) onValueChange(option.value);
103
- },
104
- className: cn("relative inline-flex items-center gap-2 whitespace-nowrap text-sm font-medium transition-[color,background-color,border-color,box-shadow]", variant === "row" && [
105
- "rounded-md border border-transparent px-3 py-1.5",
106
- focusRing,
107
- "hover:bg-[var(--ix-hover-bg)] active:bg-[var(--ix-sel-bg)]",
108
- "aria-pressed:bg-[var(--ix-sel-bg)] aria-pressed:text-[var(--accent-text)] aria-pressed:shadow-[inset_0_0_0_1px_var(--ix-sel-ring)] aria-checked:bg-[var(--ix-sel-bg)] aria-checked:text-[var(--accent-text)] aria-checked:shadow-[inset_0_0_0_1px_var(--ix-sel-ring)] aria-[current=page]:bg-[var(--ix-sel-bg)] aria-[current=page]:text-[var(--accent-text)] aria-[current=page]:shadow-[inset_0_0_0_1px_var(--ix-sel-ring)] data-[state=on]:bg-[var(--ix-sel-bg)] data-[state=on]:shadow-[inset_0_0_0_1px_var(--ix-sel-ring)] data-[state=active]:bg-[var(--ix-sel-bg)] data-[state=active]:text-[var(--accent-text)] data-[state=active]:shadow-[inset_0_0_0_1px_var(--ix-sel-ring)]"
109
- ], variant === "tabs" && [
110
- "shrink-0 rounded-none border-b-2 border-transparent -mb-px px-4 py-2",
111
- "pointer-coarse:min-h-11",
112
- focusRingInset,
113
- "hover:shadow-[inset_0_-2px_0_hsl(var(--hsl-ring)/0.4)] data-[state=active]:bg-[hsl(var(--hsl-ring)/0.07)] data-[state=active]:text-[var(--accent-text)] data-[state=active]:shadow-[inset_0_-2px_0_var(--ix-sel-ring)] aria-checked:bg-[hsl(var(--hsl-ring)/0.07)] aria-checked:text-[var(--accent-text)] aria-checked:shadow-[inset_0_-2px_0_var(--ix-sel-ring)]"
114
- ], active ? "text-[var(--accent-text)]" : "text-muted-foreground hover:text-foreground"),
115
- children: [/* @__PURE__ */ jsx("span", { children: option.label }), option.adornment && /* @__PURE__ */ jsx("span", {
116
- className: cn("text-xs", active ? "text-[var(--accent-text)]/80" : "text-muted-foreground"),
117
- children: option.adornment
118
- })]
119
- }, option.value);
120
- })
125
+ onScroll: measureOverflow,
126
+ className: cn("relative flex gap-1", variant === "row" && "flex-wrap items-center rounded-lg border border-[var(--field-border)] bg-[var(--well)] p-1", variant === "tabs" && "flex-nowrap items-end border-b border-border pb-0 overflow-x-auto", className),
127
+ children: [
128
+ overflow.start && /* @__PURE__ */ jsx(ScrollCue, {
129
+ side: "start",
130
+ onPage: () => page(-1)
131
+ }),
132
+ options.map((option, i) => {
133
+ const active = option.value === value;
134
+ const focusable = active || !hasMatch && i === 0;
135
+ return /* @__PURE__ */ jsxs("button", {
136
+ ref: (el) => {
137
+ if (el) optionRefs.current.set(option.value, el);
138
+ else optionRefs.current.delete(option.value);
139
+ },
140
+ type: "button",
141
+ role: "radio",
142
+ "aria-checked": active,
143
+ "data-testid": option.testId,
144
+ tabIndex: focusable ? 0 : -1,
145
+ onClick: () => {
146
+ if (!active) onValueChange(option.value);
147
+ },
148
+ className: cn("relative inline-flex items-center gap-2 whitespace-nowrap text-sm font-medium transition-[color,background-color,border-color,box-shadow]", variant === "row" && [
149
+ "rounded-md border border-transparent px-3 py-1.5",
150
+ focusRing,
151
+ "hover:bg-[var(--ix-hover-bg)] active:bg-[var(--ix-sel-bg)]",
152
+ "aria-pressed:bg-[var(--ix-sel-bg)] aria-pressed:text-[var(--accent-text)] aria-pressed:shadow-[inset_0_0_0_1px_var(--ix-sel-ring)] aria-checked:bg-[var(--ix-sel-bg)] aria-checked:text-[var(--accent-text)] aria-checked:shadow-[inset_0_0_0_1px_var(--ix-sel-ring)] aria-[current=page]:bg-[var(--ix-sel-bg)] aria-[current=page]:text-[var(--accent-text)] aria-[current=page]:shadow-[inset_0_0_0_1px_var(--ix-sel-ring)] data-[state=on]:bg-[var(--ix-sel-bg)] data-[state=on]:shadow-[inset_0_0_0_1px_var(--ix-sel-ring)] data-[state=active]:bg-[var(--ix-sel-bg)] data-[state=active]:text-[var(--accent-text)] data-[state=active]:shadow-[inset_0_0_0_1px_var(--ix-sel-ring)]"
153
+ ], variant === "tabs" && [
154
+ "shrink-0 rounded-none border-b-2 border-transparent -mb-px px-4 py-2",
155
+ "pointer-coarse:min-h-11",
156
+ focusRingInset,
157
+ "hover:shadow-[inset_0_-2px_0_hsl(var(--hsl-ring)/0.4)] data-[state=active]:bg-[hsl(var(--hsl-ring)/0.07)] data-[state=active]:text-[var(--accent-text)] data-[state=active]:shadow-[inset_0_-2px_0_var(--ix-sel-ring)] aria-checked:bg-[hsl(var(--hsl-ring)/0.07)] aria-checked:text-[var(--accent-text)] aria-checked:shadow-[inset_0_-2px_0_var(--ix-sel-ring)]"
158
+ ], active ? "text-[var(--accent-text)]" : "text-muted-foreground hover:text-foreground"),
159
+ children: [/* @__PURE__ */ jsx("span", { children: option.label }), option.adornment && /* @__PURE__ */ jsx("span", {
160
+ className: cn("text-xs", active ? "text-[var(--accent-text)]/80" : "text-muted-foreground"),
161
+ children: option.adornment
162
+ })]
163
+ }, option.value);
164
+ }),
165
+ overflow.end && /* @__PURE__ */ jsx(ScrollCue, {
166
+ side: "end",
167
+ onPage: () => page(1)
168
+ })
169
+ ]
121
170
  });
122
171
  }
123
172
  //#endregion
@@ -1,6 +1,8 @@
1
1
  "use client";
2
+ import { surfaceMotion } from "../lib/motion.js";
2
3
  import { focusRing } from "../lib/focus.js";
3
4
  import { cn } from "../lib/utils.js";
5
+ import { SurfaceMotion } from "../lib/surface-motion.js";
4
6
  import { cva } from "class-variance-authority";
5
7
  import * as React from "react";
6
8
  import { jsx, jsxs } from "react/jsx-runtime";
@@ -12,36 +14,44 @@ const Sheet = SheetPrimitive.Root;
12
14
  const SheetTrigger = SheetPrimitive.Trigger;
13
15
  const SheetClose = SheetPrimitive.Close;
14
16
  const SheetPortal = SheetPrimitive.Portal;
15
- const SheetOverlay = React.forwardRef(({ className, ...props }, ref) => /* @__PURE__ */ jsx(SheetPrimitive.Overlay, {
17
+ const SheetOverlay = React.forwardRef(({ className, asChild, children, ...props }, ref) => /* @__PURE__ */ jsx(SheetPrimitive.Overlay, {
16
18
  ref,
17
- className: cn("fixed inset-0 z-50 bg-black/40", "data-[state=closed]:animate-out data-[state=open]:animate-in", "data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0", className),
18
- ...props
19
+ className: cn("fixed inset-0 z-50 bg-black/40", surfaceMotion, className),
20
+ ...props,
21
+ asChild: true,
22
+ children: /* @__PURE__ */ jsx(SurfaceMotion, {
23
+ kind: "overlay",
24
+ asChild,
25
+ children
26
+ })
19
27
  }));
20
28
  SheetOverlay.displayName = SheetPrimitive.Overlay.displayName;
21
- const sheetVariants = cva([
22
- "fixed z-50 flex flex-col gap-4 border-border bg-card shadow-lg",
23
- "data-[state=closed]:animate-out data-[state=open]:animate-in",
24
- "data-[state=closed]:duration-200 data-[state=open]:duration-300 ease-out motion-reduce:animate-none"
25
- ], {
29
+ const sheetVariants = cva(["fixed z-50 flex flex-col gap-4 border-border bg-card shadow-lg", surfaceMotion], {
26
30
  variants: { side: {
27
- right: "inset-y-0 right-0 h-full w-3/4 border-l data-[state=closed]:slide-out-to-right data-[state=open]:slide-in-from-right sm:max-w-sm",
28
- left: "inset-y-0 left-0 h-full w-3/4 border-r data-[state=closed]:slide-out-to-left data-[state=open]:slide-in-from-left sm:max-w-sm",
29
- top: "inset-x-0 top-0 h-auto border-b data-[state=closed]:slide-out-to-top data-[state=open]:slide-in-from-top",
30
- bottom: "inset-x-0 bottom-0 h-auto border-t data-[state=closed]:slide-out-to-bottom data-[state=open]:slide-in-from-bottom"
31
+ right: "inset-y-0 right-0 h-full w-3/4 border-l sm:max-w-sm",
32
+ left: "inset-y-0 left-0 h-full w-3/4 border-r sm:max-w-sm",
33
+ top: "inset-x-0 top-0 h-auto border-b",
34
+ bottom: "inset-x-0 bottom-0 h-auto border-t"
31
35
  } },
32
36
  defaultVariants: { side: "right" }
33
37
  });
34
- const SheetContent = React.forwardRef(({ className, children, side = "right", hideCloseButton = false, ...props }, ref) => /* @__PURE__ */ jsxs(SheetPortal, { children: [/* @__PURE__ */ jsx(SheetOverlay, {}), /* @__PURE__ */ jsxs(SheetPrimitive.Content, {
38
+ const SheetContent = React.forwardRef(({ className, children, side = "right", hideCloseButton = false, asChild, ...props }, ref) => /* @__PURE__ */ jsxs(SheetPortal, { children: [/* @__PURE__ */ jsx(SheetOverlay, {}), /* @__PURE__ */ jsx(SheetPrimitive.Content, {
35
39
  ref,
36
40
  className: cn(sheetVariants({ side }), className),
37
41
  ...props,
38
- children: [children, !hideCloseButton && /* @__PURE__ */ jsxs(SheetPrimitive.Close, {
39
- className: cn("absolute top-4 right-4 rounded-sm text-muted-foreground opacity-70 transition-[opacity,box-shadow] hover:opacity-100 focus-visible:opacity-100 disabled:pointer-events-none", focusRing),
40
- children: [/* @__PURE__ */ jsx(X, { className: "size-4" }), /* @__PURE__ */ jsx("span", {
41
- className: "sr-only",
42
- children: "Close"
42
+ asChild: true,
43
+ children: /* @__PURE__ */ jsxs(SurfaceMotion, {
44
+ kind: "sheet",
45
+ side: side ?? "right",
46
+ asChild,
47
+ children: [children, !hideCloseButton && /* @__PURE__ */ jsxs(SheetPrimitive.Close, {
48
+ className: cn("absolute top-4 right-4 rounded-sm text-muted-foreground opacity-70 transition-[opacity,box-shadow] hover:opacity-100 focus-visible:opacity-100 disabled:pointer-events-none", "[transition:color_var(--motion-control),background-color_var(--motion-control),border-color_var(--motion-control),box-shadow_var(--motion-control),transform_var(--motion-control),scale_var(--motion-control),opacity_var(--motion-control)] motion-reduce:transition-none", focusRing),
49
+ children: [/* @__PURE__ */ jsx(X, { className: "size-4" }), /* @__PURE__ */ jsx("span", {
50
+ className: "sr-only",
51
+ children: "Close"
52
+ })]
43
53
  })]
44
- })]
54
+ })
45
55
  })] }));
46
56
  SheetContent.displayName = SheetPrimitive.Content.displayName;
47
57
  const SheetHeader = ({ className, ...props }) => /* @__PURE__ */ jsx("div", {
@@ -3,7 +3,7 @@ import { jsx, jsxs } from "react/jsx-runtime";
3
3
  //#region src/primitives/skeleton.tsx
4
4
  function Skeleton({ className, ...props }) {
5
5
  return /* @__PURE__ */ jsx("div", {
6
- className: cn("animate-pulse rounded-lg bg-muted/50", className),
6
+ className: cn("animate-pulse motion-reduce:animate-none rounded-lg bg-muted/50", className),
7
7
  ...props
8
8
  });
9
9
  }
@@ -19,7 +19,7 @@ import * as React from "react";
19
19
  * that always brings its own background cannot be placed onto a plane that
20
20
  * breaks it.
21
21
  */
22
- export type StatusTone = "success" | "warning" | "danger" | "info" | "neutral" | "running";
22
+ export type StatusTone = "success" | "warning" | "danger" | "info" | "neutral" | "running" | "violet";
23
23
  export interface StatusPillProps extends Omit<React.HTMLAttributes<HTMLSpanElement>, "children"> {
24
24
  tone: StatusTone;
25
25
  children: React.ReactNode;
@@ -37,6 +37,8 @@ export interface StatusPillProps extends Omit<React.HTMLAttributes<HTMLSpanEleme
37
37
  * For a control that supplies its own surface (a chip, a selected row).
38
38
  */
39
39
  bare?: boolean;
40
+ /** Decorative caller glyph; the visible text remains the accessible status. */
41
+ icon?: React.ReactNode;
40
42
  size?: "sm" | "md";
41
43
  }
42
44
  declare const StatusPill: React.ForwardRefExoticComponent<StatusPillProps & React.RefAttributes<HTMLSpanElement>>;
@@ -3,6 +3,7 @@ import * as React from "react";
3
3
  import { jsx, jsxs } from "react/jsx-runtime";
4
4
  //#region src/primitives/status-pill.tsx
5
5
  const TONE_SURFACE = {
6
+ violet: "bg-[var(--surface-violet-bg)] text-[var(--surface-violet-text)] border-[var(--surface-violet-border)]",
6
7
  success: "bg-[var(--surface-success-bg)] text-[var(--surface-success-text)] border-[var(--surface-success-border)]",
7
8
  warning: "bg-[var(--surface-warning-bg)] text-[var(--surface-warning-text)] border-[var(--surface-warning-border)]",
8
9
  danger: "bg-[var(--surface-danger-bg)] text-[var(--surface-danger-text)] border-[var(--surface-danger-border)]",
@@ -18,6 +19,7 @@ const TONE_SURFACE = {
18
19
  * has left — the glyph — with nothing to signal that it happened.
19
20
  */
20
21
  const TONE_TEXT = {
22
+ violet: "text-[var(--surface-violet-text)]",
21
23
  success: "text-[var(--surface-success-text)]",
22
24
  warning: "text-[var(--surface-warning-text)]",
23
25
  danger: "text-[var(--surface-danger-text)]",
@@ -35,6 +37,12 @@ function ToneGlyph({ tone }) {
35
37
  className: "size-2 shrink-0"
36
38
  };
37
39
  switch (tone) {
40
+ case "violet": return /* @__PURE__ */ jsx("svg", {
41
+ ...common,
42
+ "aria-hidden": "true",
43
+ fill: "currentColor",
44
+ children: /* @__PURE__ */ jsx("path", { d: "M4 .5 7.5 4 4 7.5 .5 4Z" })
45
+ });
38
46
  case "success": return /* @__PURE__ */ jsx("svg", {
39
47
  ...common,
40
48
  "aria-hidden": "true",
@@ -99,13 +107,14 @@ function ToneGlyph({ tone }) {
99
107
  });
100
108
  }
101
109
  }
102
- const StatusPill = React.forwardRef(({ className, tone, children, bare = false, size = "sm", ...props }, ref) => /* @__PURE__ */ jsxs("span", {
110
+ const StatusPill = React.forwardRef(({ className, tone, children, icon, bare = false, size = "sm", ...props }, ref) => /* @__PURE__ */ jsxs("span", {
103
111
  ref,
104
112
  className: cn("inline-flex items-center gap-1.5 whitespace-nowrap rounded-full font-medium", size === "sm" ? "px-2 py-0.5 text-xs" : "px-2.5 py-1 text-sm", bare ? "border border-transparent bg-transparent px-0" : cn("border", TONE_SURFACE[tone]), className),
105
113
  ...props,
106
114
  children: [/* @__PURE__ */ jsx("span", {
107
- className: cn("inline-flex", bare && TONE_TEXT[tone]),
108
- children: /* @__PURE__ */ jsx(ToneGlyph, { tone })
115
+ "aria-hidden": "true",
116
+ className: cn("inline-flex [&>svg]:size-3 [&>svg]:shrink-0", bare && TONE_TEXT[tone]),
117
+ children: icon ?? /* @__PURE__ */ jsx(ToneGlyph, { tone })
109
118
  }), children]
110
119
  }));
111
120
  StatusPill.displayName = "StatusPill";
@@ -1,61 +1,62 @@
1
1
  import { cn } from "../lib/utils.js";
2
+ import { tablePresentation } from "../presentation.js";
2
3
  import * as React from "react";
3
4
  import { jsx } from "react/jsx-runtime";
4
5
  //#region src/primitives/table.tsx
5
6
  const Table = React.forwardRef(({ className, wrapper = true, wrapperProps, ...props }, ref) => {
6
7
  const table = /* @__PURE__ */ jsx("table", {
7
8
  ref,
8
- className: cn("w-full caption-bottom text-sm", className),
9
+ className: cn(tablePresentation.root, className),
9
10
  ...props
10
11
  });
11
12
  if (!wrapper) return table;
12
13
  const { className: wrapperClassName, ...attributes } = wrapperProps ?? {};
13
14
  return /* @__PURE__ */ jsx("div", {
14
15
  ...attributes,
15
- className: cn("relative w-full overflow-auto", wrapperClassName),
16
+ className: cn(tablePresentation.wrapper, wrapperClassName),
16
17
  children: table
17
18
  });
18
19
  });
19
20
  Table.displayName = "Table";
20
21
  const TableHeader = React.forwardRef(({ className, ...props }, ref) => /* @__PURE__ */ jsx("thead", {
21
22
  ref,
22
- className: cn("[&_tr]:border-b", className),
23
+ className: cn(tablePresentation.head, className),
23
24
  ...props
24
25
  }));
25
26
  TableHeader.displayName = "TableHeader";
26
27
  const TableBody = React.forwardRef(({ className, ...props }, ref) => /* @__PURE__ */ jsx("tbody", {
27
28
  ref,
28
- className: cn("[&_tr:last-child]:border-0", className),
29
+ className: cn(tablePresentation.body, className),
29
30
  ...props
30
31
  }));
31
32
  TableBody.displayName = "TableBody";
32
33
  const TableFooter = React.forwardRef(({ className, ...props }, ref) => /* @__PURE__ */ jsx("tfoot", {
33
34
  ref,
34
- className: cn("border-t bg-card font-medium [&>tr]:last:border-b-0", className),
35
+ className: cn(tablePresentation.footer, className),
35
36
  ...props
36
37
  }));
37
38
  TableFooter.displayName = "TableFooter";
38
39
  const TableRow = React.forwardRef(({ className, ...props }, ref) => /* @__PURE__ */ jsx("tr", {
39
40
  ref,
40
- className: cn("border-border border-b transition-colors hover:bg-muted/50 data-[state=selected]:bg-muted/50", className),
41
+ className: cn(tablePresentation.row, className),
41
42
  ...props
42
43
  }));
43
44
  TableRow.displayName = "TableRow";
44
45
  const TableHead = React.forwardRef(({ className, ...props }, ref) => /* @__PURE__ */ jsx("th", {
45
46
  ref,
46
- className: cn("h-12 px-4 text-left align-middle font-medium text-muted-foreground [&:has([role=checkbox])]:pr-0", className),
47
+ className: cn(tablePresentation.columnHeader, className),
47
48
  ...props
48
49
  }));
49
50
  TableHead.displayName = "TableHead";
50
51
  const TableCell = React.forwardRef(({ className, ...props }, ref) => /* @__PURE__ */ jsx("td", {
51
52
  ref,
52
- className: cn("p-4 align-middle [&:has([role=checkbox])]:pr-0", className),
53
+ className: cn(tablePresentation.cell, className),
53
54
  ...props
54
55
  }));
55
56
  TableCell.displayName = "TableCell";
56
57
  const TableCaption = React.forwardRef(({ className, ...props }, ref) => /* @__PURE__ */ jsx("caption", {
57
58
  ref,
58
- className: cn("mt-4 text-muted-foreground text-sm", className),
59
+ className: cn(tablePresentation.caption, className),
59
60
  ...props
60
61
  }));
61
62
  TableCaption.displayName = "TableCaption";
@@ -1,6 +1,6 @@
1
- import { ixControlSoft, ixSelected, ixTabUnderline } from "../lib/interaction.js";
2
1
  import { focusRing } from "../lib/focus.js";
3
2
  import { cn } from "../lib/utils.js";
3
+ import { tabsPresentation } from "../presentation.js";
4
4
  import * as React from "react";
5
5
  import { jsx } from "react/jsx-runtime";
6
6
  import * as TabsPrimitive from "@radix-ui/react-tabs";
@@ -9,24 +9,15 @@ const Tabs = TabsPrimitive.Root;
9
9
  const TabsList = React.forwardRef(({ className, variant = "default", ...props }, ref) => {
10
10
  return /* @__PURE__ */ jsx(TabsPrimitive.List, {
11
11
  ref,
12
- className: cn({
13
- default: "inline-flex h-10 items-center justify-center rounded-lg bg-muted p-1 text-muted-foreground",
14
- pills: "inline-flex items-center gap-2",
15
- underline: "inline-flex items-center gap-4 border-b border-border"
16
- }[variant], className),
12
+ className: cn(tabsPresentation.list[variant], className),
17
13
  ...props
18
14
  });
19
15
  });
20
16
  TabsList.displayName = TabsPrimitive.List.displayName;
21
17
  const TabsTrigger = React.forwardRef(({ className, variant = "default", ...props }, ref) => {
22
- const variants = {
23
- default: `inline-flex items-center justify-center whitespace-nowrap rounded-md px-3 py-1.5 text-sm font-medium transition-all ${focusRing} disabled:pointer-events-none disabled:opacity-50 ${ixControlSoft} ${ixSelected}`,
24
- pills: `inline-flex items-center justify-center whitespace-nowrap rounded-full px-4 py-2 text-sm font-medium transition-all ${focusRing} disabled:pointer-events-none disabled:opacity-50 data-[state=inactive]:text-muted-foreground ${ixControlSoft} ${ixSelected}`,
25
- underline: `inline-flex items-center justify-center whitespace-nowrap rounded-t-md pb-3 text-sm font-medium transition-all ${focusRing} disabled:pointer-events-none disabled:opacity-50 border-b-2 border-transparent data-[state=inactive]:text-muted-foreground ${ixTabUnderline}`
26
- };
27
18
  return /* @__PURE__ */ jsx(TabsPrimitive.Trigger, {
28
19
  ref,
29
- className: cn(variants[variant], className),
20
+ className: cn(tabsPresentation.trigger[variant], className),
30
21
  ...props
31
22
  });
32
23
  });
@@ -1,5 +1,7 @@
1
1
  "use client";
2
+ import { surfaceMotion } from "../lib/motion.js";
2
3
  import { cn } from "../lib/utils.js";
4
+ import { SurfaceMotion } from "../lib/surface-motion.js";
3
5
  import * as React from "react";
4
6
  import { jsx } from "react/jsx-runtime";
5
7
  import * as TooltipPrimitive from "@radix-ui/react-tooltip";
@@ -21,11 +23,17 @@ const TooltipTrigger = TooltipPrimitive.Trigger;
21
23
  * Inverse-surface label for a control whose name is otherwise only an icon.
22
24
  * It is supplementary: the trigger still needs its own accessible name.
23
25
  */
24
- const TooltipContent = React.forwardRef(({ className, sideOffset = 6, ...props }, ref) => /* @__PURE__ */ jsx(TooltipPrimitive.Portal, { children: /* @__PURE__ */ jsx(TooltipPrimitive.Content, {
26
+ const TooltipContent = React.forwardRef(({ className, sideOffset = 6, asChild, children, ...props }, ref) => /* @__PURE__ */ jsx(TooltipPrimitive.Portal, { children: /* @__PURE__ */ jsx(TooltipPrimitive.Content, {
25
27
  ref,
26
28
  sideOffset,
27
- className: cn("z-50 max-w-xs origin-(--radix-tooltip-content-transform-origin) rounded-md bg-foreground px-2.5 py-1.5 text-background text-xs leading-snug shadow-md", "data-[state=delayed-open]:animate-in data-[state=delayed-open]:fade-in-0 data-[state=delayed-open]:zoom-in-95", "data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95", "duration-150 ease-out motion-reduce:animate-none", className),
28
- ...props
29
+ className: cn("z-50 max-w-xs origin-(--radix-tooltip-content-transform-origin) rounded-md bg-foreground px-2.5 py-1.5 text-background text-xs leading-snug shadow-md", surfaceMotion, className),
30
+ ...props,
31
+ asChild: true,
32
+ children: /* @__PURE__ */ jsx(SurfaceMotion, {
33
+ kind: "tooltip",
34
+ asChild,
35
+ children
36
+ })
29
37
  }) }));
30
38
  TooltipContent.displayName = TooltipPrimitive.Content.displayName;
31
39
  //#endregion
@@ -1,4 +1,5 @@
1
- import { Button, ButtonProps, buttonVariants } from "./primitives/button.js";
1
+ import { buttonVariants } from "./presentation.js";
2
+ import { Button, ButtonProps } from "./primitives/button.js";
2
3
  import { ArtifactPane, ArtifactPaneProps } from "./primitives/artifact-pane.js";
3
4
  import { CATEGORY_TONES, CategoryTone, TONE_CLASSES, Tone, ToneClasses, toneFor } from "./primitives/tone.js";
4
5
  import { IconTile, IconTileProps, initialsOf } from "./primitives/icon-tile.js";
@@ -44,9 +45,9 @@ import { ThemeToggle, useTheme } from "./primitives/theme-toggle.js";
44
45
  import { Heading, HeadingProps, HeadingVariant } from "./primitives/heading.js";
45
46
  import { PageShell, PageShellProps } from "./primitives/page-shell.js";
46
47
  import { PageHeader, PageHeaderProps } from "./primitives/page-header.js";
47
- import { StatusPill, StatusPillProps, StatusTone } from "./primitives/status-pill.js";
48
48
  import { Alert, AlertDescription, AlertProps, AlertTitle, AlertTone } from "./primitives/alert.js";
49
49
  import { Pagination, PaginationProps } from "./primitives/pagination.js";
50
+ import { StatusPill, StatusPillProps, StatusTone } from "./primitives/status-pill.js";
50
51
  import { Tag, TagProps } from "./primitives/tag.js";
51
52
  import { Chip, ChipProps } from "./primitives/chip.js";
52
53
  import { Metric, MetricProps, MetricStrip, MetricStripProps } from "./primitives/metric-strip.js";
@@ -1,4 +1,5 @@
1
- import { Button, buttonVariants } from "./primitives/button.js";
1
+ import { buttonVariants } from "./presentation.js";
2
+ import { Button } from "./primitives/button.js";
2
3
  import { Heading } from "./primitives/heading.js";
3
4
  import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from "./primitives/card.js";
4
5
  import { Dialog, DialogClose, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogOverlay, DialogPortal, DialogTitle, DialogTrigger } from "./primitives/dialog.js";
package/dist/utils.d.ts CHANGED
@@ -1,8 +1,8 @@
1
+ import { ixControl, ixControlAfter, ixControlSoft, ixField, ixRow, ixSelected, ixTabUnderline } from "./lib/interaction.js";
1
2
  import { copyText } from "./utils/copy-text.js";
2
3
  import { formatBytes, formatDuration, formatUptime, plainTextFromMarkdown, truncateText } from "./utils/format.js";
3
4
  import { timeAgo } from "./utils/time-ago.js";
4
5
  import { TOOL_CATEGORY_ICONS, getToolCategory, getToolDisplayMetadata, getToolErrorText } from "./utils/tool-display.js";
5
6
  import { cn } from "./lib/utils.js";
6
7
  import { focusField, focusFieldInvalid, focusFieldWithin, focusRing, focusRingInset } from "./lib/focus.js";
7
- import { ixControl, ixControlAfter, ixControlSoft, ixField, ixRow, ixSelected, ixTabUnderline } from "./lib/interaction.js";
8
8
  export { TOOL_CATEGORY_ICONS, cn, copyText, focusField, focusFieldInvalid, focusFieldWithin, focusRing, focusRingInset, formatBytes, formatDuration, formatUptime, getToolCategory, getToolDisplayMetadata, getToolErrorText, ixControl, ixControlAfter, ixControlSoft, ixField, ixRow, ixSelected, ixTabUnderline, plainTextFromMarkdown, timeAgo, truncateText };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tangle-network/ui",
3
- "version": "11.34.0",
3
+ "version": "11.36.0",
4
4
  "description": "Generic React UI components for Tangle products — primitives, chat, run, files, editor, markdown.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -13,7 +13,9 @@
13
13
  "url": "https://github.com/tangle-network/brand/issues"
14
14
  },
15
15
  "homepage": "https://github.com/tangle-network/brand",
16
- "sideEffects": false,
16
+ "sideEffects": [
17
+ "./src/styles/*.css"
18
+ ],
17
19
  "files": [
18
20
  "dist",
19
21
  "src",
@@ -111,7 +113,14 @@
111
113
  "types": "./dist/redaction.d.ts",
112
114
  "import": "./dist/redaction.js",
113
115
  "default": "./dist/redaction.js"
114
- }
116
+ },
117
+ "./presentation": {
118
+ "types": "./dist/presentation.d.ts",
119
+ "import": "./dist/presentation.js",
120
+ "default": "./dist/presentation.js"
121
+ },
122
+ "./styles/presentation.css": "./src/styles/presentation.css",
123
+ "./styles/compat.css": "./src/styles/compat.css"
115
124
  },
116
125
  "dependencies": {
117
126
  "@pierre/theme": "1.1.0",
@@ -139,6 +148,7 @@
139
148
  "github-slugger": "^2.0.0",
140
149
  "lucide-react": "^1.48.0",
141
150
  "marked": "^18.0.14",
151
+ "motion": "14.2.0",
142
152
  "react-markdown": "^10.1.0",
143
153
  "rehype-sanitize": "^6.0.0",
144
154
  "rehype-slug": "^6.0.0",
@@ -150,7 +160,7 @@
150
160
  "peerDependencies": {
151
161
  "@hocuspocus/provider": ">=2.15.0 <5.0.0",
152
162
  "@nanostores/react": ">=0.8.4 <3.0.0",
153
- "@tangle-network/brand": "^1.24.1",
163
+ "@tangle-network/brand": "^1.25.1",
154
164
  "@tiptap/core": ">=3.20.4 <4.0.0",
155
165
  "@tiptap/extension-collaboration": ">=3.20.4 <4.0.0",
156
166
  "@tiptap/extension-collaboration-caret": ">=3.7.0 <4.0.0",
@@ -228,7 +238,7 @@
228
238
  "design-system"
229
239
  ],
230
240
  "scripts": {
231
- "build": "node scripts/generate-openui-validators.mjs --check && tsdown && node ../../scripts/validate-dist.mjs && node scripts/probe-openui-worker.mjs",
241
+ "build": "node scripts/generate-openui-validators.mjs --check && tsdown && node scripts/generate-presentation-css.mjs --check && node ../../scripts/validate-dist.mjs && node scripts/probe-openui-worker.mjs",
232
242
  "gen:openui-schema": "node scripts/generate-openui-validators.mjs",
233
243
  "dev": "tsdown --watch"
234
244
  }
@@ -2,10 +2,8 @@ import { focusField } from "./focus";
2
2
  import { ixField } from "./interaction";
3
3
  import { cn } from "./utils";
4
4
 
5
- // Time-valued tokens only: --transition-fast is a legacy duration + easing
6
- // shorthand, not a valid transition-duration. Reuse Brand's scoped vocabulary.
7
- export const controlMotion =
8
- "duration-[var(--duration-fast)] ease-[var(--ease-standard)] motion-reduce:transition-none motion-reduce:duration-0";
5
+ import { controlMotion } from "./motion";
6
+ export { controlMotion } from "./motion";
9
7
 
10
8
  /**
11
9
  * The control scale. Button, Input, Textarea and SelectTrigger take the same