@tangle-network/ui 11.35.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 (73) hide show
  1. package/CHANGELOG.md +6 -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/sheet.js +29 -19
  35. package/dist/primitives/skeleton.js +1 -1
  36. package/dist/primitives/status-pill.d.ts +3 -1
  37. package/dist/primitives/status-pill.js +12 -3
  38. package/dist/primitives/table.js +10 -9
  39. package/dist/primitives/tabs.js +3 -12
  40. package/dist/primitives/tooltip.js +11 -3
  41. package/dist/primitives.d.ts +3 -2
  42. package/dist/primitives.js +2 -1
  43. package/dist/utils.d.ts +1 -1
  44. package/package.json +15 -5
  45. package/src/lib/control-presentation.ts +2 -4
  46. package/src/lib/focus.ts +3 -2
  47. package/src/lib/motion.ts +7 -0
  48. package/src/lib/surface-motion.tsx +78 -0
  49. package/src/presentation.ts +205 -0
  50. package/src/primitives/alert.tsx +1 -2
  51. package/src/primitives/button.tsx +4 -40
  52. package/src/primitives/card.tsx +5 -10
  53. package/src/primitives/checkbox-separator-sheet-tooltip.test.tsx +2 -1
  54. package/src/primitives/control-presentation.test.tsx +2 -7
  55. package/src/primitives/dialog.tsx +19 -18
  56. package/src/primitives/empty-state.tsx +6 -5
  57. package/src/primitives/heading.tsx +4 -2
  58. package/src/primitives/help-text.tsx +3 -2
  59. package/src/primitives/input.tsx +3 -31
  60. package/src/primitives/label.tsx +2 -4
  61. package/src/primitives/metric-strip.tsx +8 -17
  62. package/src/primitives/page-header.tsx +8 -5
  63. package/src/primitives/page-shell.tsx +13 -2
  64. package/src/primitives/sheet.tsx +16 -11
  65. package/src/primitives/skeleton.tsx +1 -1
  66. package/src/primitives/status-pill.test.tsx +1 -0
  67. package/src/primitives/status-pill.tsx +10 -4
  68. package/src/primitives/table.tsx +10 -9
  69. package/src/primitives/tabs.tsx +5 -17
  70. package/src/primitives/tooltip.tsx +6 -5
  71. package/src/styles/compat.css +12 -0
  72. package/src/styles/presentation.css +30 -0
  73. package/src/primitives/page-shell.test.tsx +0 -24
@@ -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.35.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.2",
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
package/src/lib/focus.ts CHANGED
@@ -1,3 +1,4 @@
1
+ import { controlMotion } from "./motion";
1
2
  import { ixField } from "./interaction";
2
3
 
3
4
  /**
@@ -30,7 +31,7 @@ import { ixField } from "./interaction";
30
31
  * Tailwind emits `focus:` after `hover:`.
31
32
  */
32
33
  export const focusField =
33
- "border-border transition-[border-color,box-shadow] duration-150 ease-out hover:border-[var(--border-strong)] focus:outline-hidden focus:border-[var(--focus-border)] focus:ring-3 focus:ring-[var(--focus-halo)]";
34
+ `border-border ${controlMotion} hover:border-[var(--border-strong)] focus:outline-hidden focus:border-[var(--focus-border)] focus:ring-3 focus:ring-[var(--focus-halo)]`;
34
35
 
35
36
  /**
36
37
  * The field treatment for a container that wraps a borderless field, such as a
@@ -50,7 +51,7 @@ export const focusField =
50
51
  * borderless field inside stays transparent.
51
52
  */
52
53
  export const focusFieldWithin =
53
- `focus-field-within transition-[border-color,box-shadow] duration-150 ease-out ${ixField} has-[:is(textarea,select,[contenteditable]:not([contenteditable=false]),input:not([type=button],[type=submit],[type=reset],[type=image],[type=checkbox],[type=radio],[type=range],[type=color],[type=file])):focus]:border-[var(--focus-border)] has-[:is(textarea,select,[contenteditable]:not([contenteditable=false]),input:not([type=button],[type=submit],[type=reset],[type=image],[type=checkbox],[type=radio],[type=range],[type=color],[type=file])):focus]:ring-3 has-[:is(textarea,select,[contenteditable]:not([contenteditable=false]),input:not([type=button],[type=submit],[type=reset],[type=image],[type=checkbox],[type=radio],[type=range],[type=color],[type=file])):focus]:ring-[var(--focus-halo)]`;
54
+ `focus-field-within ${controlMotion} ${ixField} has-[:is(textarea,select,[contenteditable]:not([contenteditable=false]),input:not([type=button],[type=submit],[type=reset],[type=image],[type=checkbox],[type=radio],[type=range],[type=color],[type=file])):focus]:border-[var(--focus-border)] has-[:is(textarea,select,[contenteditable]:not([contenteditable=false]),input:not([type=button],[type=submit],[type=reset],[type=image],[type=checkbox],[type=radio],[type=range],[type=color],[type=file])):focus]:ring-3 has-[:is(textarea,select,[contenteditable]:not([contenteditable=false]),input:not([type=button],[type=submit],[type=reset],[type=image],[type=checkbox],[type=radio],[type=range],[type=color],[type=file])):focus]:ring-[var(--focus-halo)]`;
54
55
 
55
56
  /** Error state for a field: a danger hairline and the same focus shape in danger. Compose after `focusField`. */
56
57
  export const focusFieldInvalid =
@@ -0,0 +1,7 @@
1
+ /** Brand motion roles contain duration AND easing; consume complete shorthands. */
2
+ export const controlMotion =
3
+ "[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";
4
+
5
+ /** Radix presence markers carry the scoped role; SurfaceMotion owns interruptible geometry. */
6
+ export const surfaceMotion =
7
+ "data-[state=open]:[animation:tangle-surface-present_var(--motion-surface)_both] data-[state=delayed-open]:[animation:tangle-surface-present_var(--motion-surface)_both] data-[state=instant-open]:[animation:tangle-surface-present_var(--motion-surface)_both] data-[state=closed]:[animation:tangle-surface-absent_var(--motion-dismiss)_both] motion-reduce:animate-none!";
@@ -0,0 +1,78 @@
1
+ "use client";
2
+
3
+ import * as React from "react";
4
+ import { Slot } from "@radix-ui/react-slot";
5
+ import { animate } from "motion";
6
+
7
+ type Side = "top" | "right" | "bottom" | "left";
8
+ type SurfaceProps = React.HTMLAttributes<HTMLDivElement> & {
9
+ kind: "dialog" | "sheet" | "overlay" | "tooltip";
10
+ side?: Side;
11
+ asChild?: boolean;
12
+ "data-state"?: string;
13
+ };
14
+
15
+ /** Radix owns state, focus and presence; Motion only retargets the visible object. */
16
+ export const SurfaceMotion = React.forwardRef<HTMLDivElement, SurfaceProps>(
17
+ ({ kind, side = "right", asChild, "data-state": state, ...props }, forwardedRef) => {
18
+ const element = React.useRef<HTMLDivElement | null>(null);
19
+ const initialized = React.useRef(false);
20
+ const controls = React.useRef<ReturnType<typeof animate> | undefined>(undefined);
21
+ const ref = React.useCallback<React.RefCallback<HTMLDivElement>>((node) => {
22
+ element.current = node;
23
+ if (typeof forwardedRef === "function") {
24
+ const cleanup: unknown = forwardedRef(node);
25
+ if (typeof cleanup === "function") return () => { element.current = null; cleanup(); };
26
+ } else if (forwardedRef) forwardedRef.current = node;
27
+ }, [forwardedRef]);
28
+
29
+ React.useLayoutEffect(() => {
30
+ const node = element.current;
31
+ if (!node) return;
32
+ const preference = typeof matchMedia === "function" ? matchMedia("(prefers-reduced-motion: reduce)") : undefined;
33
+ const run = () => {
34
+ const closed = state === "closed";
35
+ const bounds = node.getBoundingClientRect();
36
+ const hidden = {
37
+ opacity: kind === "sheet" ? 1 : 0,
38
+ scale: kind === "dialog" || kind === "tooltip" ? 0.98 : 1,
39
+ x: kind === "sheet" ? side === "left" ? -bounds.width : side === "right" ? bounds.width : 0 : 0,
40
+ y: kind === "sheet" ? side === "top" ? -bounds.height : side === "bottom" ? bounds.height : 0 : 0,
41
+ };
42
+ const target = closed ? hidden : { opacity: 1, scale: 1, x: 0, y: 0 };
43
+ const style = getComputedStyle(node);
44
+ // The nonvisual marker resolves the full role, including scoped calc().
45
+ const time = /^(\d+(?:\.\d+)?)(ms|s)$/.exec(style.animationDuration.split(",")[0]!.trim());
46
+ const seconds = time ? Number(time[1]) / (time[2] === "ms" ? 1000 : 1) : 0;
47
+ const place = (value: typeof target) => {
48
+ node.style.opacity = String(value.opacity);
49
+ node.style.transform = `translateX(${value.x}px) translateY(${value.y}px) scale(${value.scale})`;
50
+ };
51
+ // Missing styles and reduced motion settle synchronously. No semantic
52
+ // transition waits for a JavaScript completion callback.
53
+ if (preference?.matches || seconds <= 0 || style.animationName === "none") {
54
+ controls.current?.stop();
55
+ place(target);
56
+ if (initialized.current) controls.current = animate(node, target, { duration: 0 });
57
+ return;
58
+ }
59
+ if (!initialized.current) { place(hidden); initialized.current = true; }
60
+ // Critically damped response: <0.01% residual at the role's deadline.
61
+ // Reusing this element's Motion values preserves position and velocity
62
+ // on reversal; a duration-based spring would discard velocity.
63
+ const frequency = 12 / seconds;
64
+ controls.current = animate(node, target, {
65
+ type: "spring", stiffness: frequency * frequency, damping: 2 * frequency,
66
+ restDelta: 0.001, restSpeed: 0.01,
67
+ });
68
+ };
69
+ run();
70
+ preference?.addEventListener("change", run);
71
+ return () => preference?.removeEventListener("change", run);
72
+ }, [state, kind, side]);
73
+ React.useEffect(() => () => controls.current?.stop(), []);
74
+ const Component = asChild ? Slot : "div";
75
+ return <Component {...props} data-state={state} ref={ref} />;
76
+ },
77
+ );
78
+ SurfaceMotion.displayName = "SurfaceMotion";
@@ -0,0 +1,205 @@
1
+ /** React-free presentation. Consumers scan this published source with Tailwind. */
2
+ import { surfaceMotion } from "./lib/motion";
3
+ import { cva } from "class-variance-authority";
4
+ import { controlHeight, controlMotion, controlRadius, controlSizes, controlSquare, controlText, fieldPresentation, fieldSizes, fieldTouchText } from "./lib/control-presentation";
5
+ import { focusRing, focusFieldInvalid } from "./lib/focus";
6
+ import { ixControl, ixControlSoft, ixSelected, ixTabUnderline } from "./lib/interaction";
7
+ import { cn } from "./lib/utils";
8
+ export { controlHeight, controlMotion, controlRadius, controlSizes, controlText, fieldPresentation, fieldSizes, fieldTouchText } from "./lib/control-presentation";
9
+
10
+ export const buttonVariants = cva(
11
+ `inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-lg font-medium ${controlMotion} ${focusRing} disabled:pointer-events-none disabled:opacity-50 aria-disabled:cursor-not-allowed aria-disabled:opacity-50 motion-reduce:active:scale-100 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0`,
12
+ {
13
+ variants: {
14
+ variant: {
15
+ default:
16
+ `bg-primary text-primary-foreground hover:bg-primary/90 active:scale-[0.97] ${ixControl}`,
17
+ destructive:
18
+ `bg-destructive text-destructive-foreground hover:bg-destructive/90 active:scale-[0.97] ${ixControl}`,
19
+ outline:
20
+ `border border-border bg-card hover:bg-muted active:scale-[0.97] text-foreground ${ixControl}`,
21
+ secondary:
22
+ `bg-muted border border-border text-foreground hover:bg-muted/80 active:scale-[0.97] ${ixControl}`,
23
+ ghost:
24
+ `hover:text-foreground text-muted-foreground border border-transparent ${ixControlSoft}`,
25
+ link: `text-[var(--accent-text)] underline-offset-4 hover:underline ${ixControlSoft}`,
26
+ sandbox:
27
+ `bg-[var(--btn-primary-bg)] text-[var(--btn-primary-text)] border border-[var(--border-accent)] hover:bg-[var(--btn-primary-hover)] active:scale-[0.97] ${ixControl}`,
28
+ },
29
+ size: {
30
+ default: `${controlHeight.md} ${controlText.md} ${controlRadius.md} px-4 py-2`,
31
+ md: `${controlHeight.md} ${controlText.md} ${controlRadius.md} px-4 py-2`,
32
+ sm: `${controlHeight.sm} ${controlText.sm} ${controlRadius.sm} px-3`,
33
+ lg: `${controlHeight.lg} ${controlText.lg} ${controlRadius.lg} px-7`,
34
+ display: "tangle-control-display",
35
+ xl: "h-13 rounded-xl px-9 text-base",
36
+ icon: `${controlSquare.md} ${controlText.md} ${controlRadius.md}`,
37
+ "icon-sm": `${controlSquare.sm} ${controlText.sm} ${controlRadius.sm}`,
38
+ "icon-lg": `${controlSquare.lg} ${controlText.lg} ${controlRadius.lg}`,
39
+ ...controlSizes,
40
+ },
41
+ },
42
+ defaultVariants: {
43
+ variant: "default",
44
+ size: "default",
45
+ },
46
+ },
47
+ );
48
+
49
+
50
+ export const inputVariants = cva(
51
+ cn(
52
+ "flex w-full border py-1 placeholder:text-[var(--text-dim)] disabled:cursor-not-allowed disabled:opacity-50 file:border-0 file:bg-transparent file:font-medium",
53
+ fieldPresentation,
54
+ fieldTouchText,
55
+ ),
56
+ {
57
+ variants: {
58
+ // `default` and `sandbox` render the same field; both names stay valid.
59
+ variant: {
60
+ default: "",
61
+ sandbox: "",
62
+ error: focusFieldInvalid,
63
+ },
64
+ size: {
65
+ default: fieldSizes.md,
66
+ md: fieldSizes.md,
67
+ sm: fieldSizes.sm,
68
+ lg: fieldSizes.lg,
69
+ compact: cn(fieldSizes.md, controlSizes.compact),
70
+ touch: cn(fieldSizes.md, controlSizes.touch),
71
+ }
72
+ },
73
+ defaultVariants: {
74
+ variant: "default",
75
+ size: "default",
76
+ },
77
+ }
78
+ );
79
+
80
+
81
+ export const tablePresentation = {
82
+ root: "w-full caption-bottom text-sm",
83
+ wrapper: "relative w-full overflow-auto",
84
+ head: "[&_tr]:border-b",
85
+ body: "[&_tr:last-child]:border-0",
86
+ footer: "border-t bg-card font-medium [&>tr]:last:border-b-0",
87
+ row: `border-border border-b ${controlMotion} hover:bg-muted/50 data-[state=selected]:bg-muted/50`,
88
+ columnHeader: "h-12 px-4 text-left align-middle font-medium text-muted-foreground [&:has([role=checkbox])]:pr-0",
89
+ cell: "p-4 align-middle [&:has([role=checkbox])]:pr-0",
90
+ caption: "mt-4 text-muted-foreground text-sm",
91
+ } as const;
92
+
93
+ export const emptyStatePresentation = {
94
+ root: "flex flex-col items-center justify-center px-4 py-16 text-center",
95
+ icon: "mb-4 rounded-full bg-muted p-4 text-muted-foreground",
96
+ title: "font-semibold text-lg",
97
+ description: "mt-2 max-w-sm text-muted-foreground text-sm",
98
+ action: "mt-6",
99
+ } as const;
100
+
101
+ export const metricPresentation = {
102
+ strip: "grid min-w-0 rounded-[var(--radius-lg)] border border-border bg-card shadow-[var(--shadow-card)]",
103
+ item: "min-w-0 border-border p-4 sm:p-5",
104
+ value: "mt-1 whitespace-normal [overflow-wrap:anywhere] font-semibold text-xl tabular-nums tracking-tight lg:text-2xl",
105
+ label: "flex min-w-0 flex-wrap items-center gap-2 text-muted-foreground text-sm",
106
+ labelText: "min-w-0 whitespace-normal [overflow-wrap:anywhere]",
107
+ hint: "mt-0.5 whitespace-normal [overflow-wrap:anywhere] text-[var(--text-dim)] text-xs",
108
+ } as const;
109
+
110
+ export const metricLayouts = {
111
+ 3: "grid-cols-2 sm:grid-cols-3 max-sm:[&>div:not(:nth-child(2n+1))]:border-l sm:[&>div:not(:nth-child(3n+1))]:border-l",
112
+ 4: "grid-cols-2 sm:grid-cols-4 max-sm:[&>div:not(:nth-child(2n+1))]:border-l sm:[&>div:not(:nth-child(4n+1))]:border-l",
113
+ 5: "grid-cols-2 sm:grid-cols-3 lg:grid-cols-5 max-sm:[&>div:not(:nth-child(2n+1))]:border-l sm:max-lg:[&>div:not(:nth-child(3n+1))]:border-l lg:[&>div:not(:nth-child(5n+1))]:border-l",
114
+ 6: "grid-cols-2 sm:grid-cols-3 lg:grid-cols-6 max-sm:[&>div:not(:nth-child(2n+1))]:border-l sm:max-lg:[&>div:not(:nth-child(3n+1))]:border-l lg:[&>div:not(:nth-child(6n+1))]:border-l",
115
+ } as const;
116
+
117
+ export const dialogPresentation = {
118
+ overlay: "fixed inset-0 z-50 bg-black/40",
119
+ transition: surfaceMotion,
120
+ position: "fixed top-[50%] left-[50%] z-50 grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%]",
121
+ surface: "gap-4 rounded-lg border bg-card p-6 shadow-lg",
122
+ close: `absolute top-4 right-4 rounded-sm opacity-70 ${controlMotion} hover:opacity-100 focus-visible:opacity-100 disabled:pointer-events-none data-[state=open]:bg-accent data-[state=open]:text-muted-foreground`,
123
+ header: "flex flex-col space-y-1.5 text-center sm:text-left",
124
+ footer: "flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2",
125
+ title: "font-semibold text-lg leading-none tracking-tight",
126
+ description: "text-muted-foreground text-sm",
127
+ } as const;
128
+
129
+ /** Native-dialog adapter for Hono/static markup. Radix remains the React Dialog/Sheet owner. */
130
+ export function bindDialog(dialog: HTMLDialogElement, trigger: HTMLElement): () => void {
131
+ const sync = () => trigger.setAttribute("aria-expanded", String(dialog.open));
132
+ const show = () => { if (!dialog.open) dialog.showModal(); sync(); };
133
+ const close = () => { sync(); trigger.focus(); };
134
+ const click = (event: MouseEvent) => {
135
+ const target = event.target;
136
+ if (target instanceof Element && target.closest("[data-dialog-close]")) dialog.close();
137
+ };
138
+ // Native modal inertness blocks page controls, but Tab can still visit browser
139
+ // chrome at a boundary. Keep this reading surface's keyboard cycle local.
140
+ const key = (event: KeyboardEvent) => {
141
+ if (event.key !== "Tab" || !dialog.open) return;
142
+ const candidates = [...dialog.querySelectorAll<HTMLElement>("*")]
143
+ .filter(element => (element.tabIndex >= 0 || (element.isContentEditable && !element.hasAttribute("tabindex")))
144
+ && !element.matches(":disabled") && !element.closest("[inert]")
145
+ && element.getClientRects().length > 0 && getComputedStyle(element).visibility === "visible");
146
+ const radio = (element: Element): element is HTMLInputElement => element instanceof HTMLInputElement && element.type === "radio" && !!element.name;
147
+ const sameRadioGroup = (a: Element, b: Element) => radio(a) && radio(b) && a.name === b.name && a.form === b.form && a.getRootNode() === b.getRootNode();
148
+ const stops = candidates.filter(element => {
149
+ if (!radio(element)) return true;
150
+ const group = candidates.filter(candidate => sameRadioGroup(element, candidate));
151
+ const checked = group.find(candidate => (candidate as HTMLInputElement).checked);
152
+ return element === (checked ?? (event.shiftKey ? group.at(-1) : group[0]));
153
+ }).sort((a, b) => (a.tabIndex > 0 ? a.tabIndex : Infinity) - (b.tabIndex > 0 ? b.tabIndex : Infinity));
154
+ const first = stops[0], last = stops.at(-1), active = document.activeElement;
155
+ if (!first) { event.preventDefault(); dialog.focus(); return; }
156
+ const atBoundary = (boundary: HTMLElement) => active === boundary || (active instanceof Element && sameRadioGroup(active, boundary));
157
+ if (active === dialog || (event.shiftKey ? atBoundary(first) : atBoundary(last!))) {
158
+ event.preventDefault(); (event.shiftKey ? last! : first).focus();
159
+ }
160
+ };
161
+ trigger.setAttribute("aria-haspopup", "dialog");
162
+ if (dialog.id) trigger.setAttribute("aria-controls", dialog.id);
163
+ sync();trigger.addEventListener("click",show);dialog.addEventListener("close",close);dialog.addEventListener("click",click);dialog.addEventListener("keydown",key);
164
+ return () => {trigger.removeEventListener("click",show);dialog.removeEventListener("close",close);dialog.removeEventListener("click",click);dialog.removeEventListener("keydown",key);};
165
+ }
166
+
167
+ export const labelVariants = cva(
168
+ "font-medium text-[length:var(--font-size-label,0.875rem)] leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70",
169
+ );
170
+
171
+ export const helpPresentation = {
172
+ root: "text-[length:var(--font-size-help,0.75rem)] leading-4",
173
+ error: "font-medium text-[var(--surface-danger-text)]",
174
+ hint: "text-[var(--text-dim)]",
175
+ };
176
+
177
+ export const cardPresentation = {
178
+ root: `rounded-[var(--radius-lg)] border text-card-foreground ${controlMotion}`,
179
+ padding: "p-4",
180
+ variants: {
181
+ default: "bg-card border-border shadow-[var(--edge-highlight),var(--shadow-card)]",
182
+ elevated: "bg-muted/50 border-border shadow-[var(--shadow-card)]",
183
+ glass: "bg-card/80 backdrop-blur-xl border-border shadow-[var(--shadow-card)]",
184
+ sandbox: "bg-muted/50 border-primary/20 shadow-[var(--shadow-accent)]",
185
+ },
186
+ } as const;
187
+
188
+ /** Canonical segmented, pill and underline navigation presentation. */
189
+ export const tabsPresentation = {
190
+ list: {
191
+ default:
192
+ "inline-flex h-10 items-center justify-center rounded-lg bg-muted p-1 text-muted-foreground",
193
+ pills: "inline-flex items-center gap-2",
194
+ underline: "inline-flex items-center gap-4 border-b border-border",
195
+ },
196
+ trigger: {
197
+ default:
198
+ `inline-flex items-center justify-center whitespace-nowrap rounded-md px-3 py-1.5 text-sm font-medium ${controlMotion} ${focusRing} disabled:pointer-events-none disabled:opacity-50 ${ixControlSoft} ${ixSelected}`,
199
+ pills:
200
+ `inline-flex items-center justify-center whitespace-nowrap rounded-full px-4 py-2 text-sm font-medium ${controlMotion} ${focusRing} disabled:pointer-events-none disabled:opacity-50 data-[state=inactive]:text-muted-foreground ${ixControlSoft} ${ixSelected}`,
201
+ underline:
202
+ `inline-flex items-center justify-center whitespace-nowrap rounded-t-md pb-3 text-sm font-medium ${controlMotion} ${focusRing} disabled:pointer-events-none disabled:opacity-50 border-b-2 border-transparent data-[state=inactive]:text-muted-foreground ${ixTabUnderline}`,
203
+ }
204
+ } as const;
205
+ export { ixControlSoft, ixSelected, ixTabUnderline } from "./lib/interaction";
@@ -1,6 +1,5 @@
1
1
  import * as React from "react";
2
2
  import { cn } from "../lib/utils";
3
- import type { StatusTone } from "./status-pill";
4
3
 
5
4
  /**
6
5
  * An inline message that stays on the page: a policy note above a form, a
@@ -14,7 +13,7 @@ import type { StatusTone } from "./status-pill";
14
13
  * `danger` and `warning` announce themselves (`role="alert"`); the quieter
15
14
  * tones are `role="status"` so a screen reader is not interrupted by a note.
16
15
  */
17
- export type AlertTone = Exclude<StatusTone, "running">;
16
+ export type AlertTone = "neutral" | "info" | "success" | "warning" | "danger";
18
17
 
19
18
  export interface AlertProps extends React.HTMLAttributes<HTMLDivElement> {
20
19
  tone?: AlertTone;