@tangle-network/ui 11.35.0 → 11.37.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 (93) hide show
  1. package/CHANGELOG.md +20 -0
  2. package/README.md +1 -1
  3. package/dist/code-presentation.d.ts +2 -0
  4. package/dist/code-presentation.js +2 -0
  5. package/dist/index.d.ts +8 -5
  6. package/dist/index.js +6 -3
  7. package/dist/lib/code-presentation.d.ts +92 -0
  8. package/dist/lib/code-presentation.js +437 -0
  9. package/dist/lib/control-presentation.d.ts +34 -1
  10. package/dist/lib/control-presentation.js +1 -1
  11. package/dist/lib/focus.d.ts +2 -2
  12. package/dist/lib/focus.js +3 -2
  13. package/dist/lib/motion.d.ts +4 -0
  14. package/dist/lib/motion.js +7 -0
  15. package/dist/lib/surface-motion.js +105 -0
  16. package/dist/markdown/api-schema.d.ts +9 -0
  17. package/dist/markdown/api-schema.js +20 -0
  18. package/dist/markdown/code-block.d.ts +15 -7
  19. package/dist/markdown/code-block.js +120 -268
  20. package/dist/markdown.d.ts +4 -2
  21. package/dist/markdown.js +4 -2
  22. package/dist/presentation.d.ts +88 -0
  23. package/dist/presentation.js +183 -0
  24. package/dist/primitives/alert-dialog.js +1 -1
  25. package/dist/primitives/alert.d.ts +1 -2
  26. package/dist/primitives/button.d.ts +2 -5
  27. package/dist/primitives/button.js +5 -31
  28. package/dist/primitives/card.js +3 -7
  29. package/dist/primitives/code-block.d.ts +1 -1
  30. package/dist/primitives/date-picker.d.ts +1 -1
  31. package/dist/primitives/dialog.js +29 -16
  32. package/dist/primitives/empty-state.js +6 -5
  33. package/dist/primitives/heading.d.ts +1 -0
  34. package/dist/primitives/heading.js +3 -2
  35. package/dist/primitives/help-text.js +2 -1
  36. package/dist/primitives/input.d.ts +1 -4
  37. package/dist/primitives/input.js +3 -24
  38. package/dist/primitives/label.js +1 -2
  39. package/dist/primitives/metric-strip.js +7 -12
  40. package/dist/primitives/page-header.d.ts +1 -0
  41. package/dist/primitives/page-header.js +7 -6
  42. package/dist/primitives/page-shell.d.ts +8 -2
  43. package/dist/primitives/page-shell.js +21 -2
  44. package/dist/primitives/pagination.js +1 -1
  45. package/dist/primitives/popover.js +11 -3
  46. package/dist/primitives/sheet.js +29 -19
  47. package/dist/primitives/skeleton.js +1 -1
  48. package/dist/primitives/status-pill.d.ts +3 -1
  49. package/dist/primitives/status-pill.js +12 -3
  50. package/dist/primitives/table.js +10 -9
  51. package/dist/primitives/tabs.js +3 -12
  52. package/dist/primitives/tooltip.js +11 -3
  53. package/dist/primitives.d.ts +3 -2
  54. package/dist/primitives.js +2 -1
  55. package/dist/utils.d.ts +1 -1
  56. package/package.json +20 -5
  57. package/src/code-presentation.ts +2 -0
  58. package/src/lib/code-presentation.ts +257 -0
  59. package/src/lib/control-presentation.ts +2 -4
  60. package/src/lib/focus.ts +3 -2
  61. package/src/lib/motion.ts +7 -0
  62. package/src/lib/surface-motion.tsx +85 -0
  63. package/src/markdown/api-presentation.test.tsx +63 -0
  64. package/src/markdown/api-schema.tsx +9 -0
  65. package/src/markdown/code-block.tsx +54 -185
  66. package/src/markdown/index.ts +2 -1
  67. package/src/presentation.ts +205 -0
  68. package/src/primitives/alert.tsx +1 -2
  69. package/src/primitives/button.tsx +6 -41
  70. package/src/primitives/card.tsx +5 -10
  71. package/src/primitives/checkbox-separator-sheet-tooltip.test.tsx +2 -1
  72. package/src/primitives/code-block.tsx +1 -1
  73. package/src/primitives/control-presentation.test.tsx +2 -7
  74. package/src/primitives/dialog.tsx +19 -18
  75. package/src/primitives/empty-state.tsx +6 -5
  76. package/src/primitives/heading.tsx +4 -2
  77. package/src/primitives/help-text.tsx +3 -2
  78. package/src/primitives/input.tsx +3 -31
  79. package/src/primitives/label.tsx +2 -4
  80. package/src/primitives/metric-strip.tsx +8 -17
  81. package/src/primitives/page-header.tsx +8 -5
  82. package/src/primitives/page-shell.tsx +13 -2
  83. package/src/primitives/popover.tsx +6 -8
  84. package/src/primitives/sheet.tsx +16 -11
  85. package/src/primitives/skeleton.tsx +1 -1
  86. package/src/primitives/status-pill.test.tsx +1 -0
  87. package/src/primitives/status-pill.tsx +10 -4
  88. package/src/primitives/table.tsx +10 -9
  89. package/src/primitives/tabs.tsx +5 -17
  90. package/src/primitives/tooltip.tsx +6 -5
  91. package/src/styles/compat.css +12 -0
  92. package/src/styles/presentation.css +38 -0
  93. package/src/primitives/page-shell.test.tsx +0 -24
@@ -1,5 +1,6 @@
1
1
  import * as React from "react";
2
2
  import { ixControl } from "../lib/interaction";
3
+ import { cardPresentation } from "../presentation";
3
4
  import { cn } from "../lib/utils";
4
5
  import { Heading } from "./heading";
5
6
 
@@ -24,20 +25,14 @@ const Card = React.forwardRef<
24
25
  interactive?: boolean;
25
26
  }
26
27
  >(({ className, variant = "default", hover = false, interactive = false, ...props }, ref) => {
27
- const variants = {
28
- default: "bg-card border-border shadow-[var(--edge-highlight),var(--shadow-card)]",
29
- elevated: "bg-muted/50 border-border shadow-[var(--shadow-card)]",
30
- glass: "bg-card/80 backdrop-blur-xl border-border shadow-[var(--shadow-card)]",
31
- sandbox: "bg-muted/50 border-primary/20 shadow-[var(--shadow-accent)]",
32
- };
28
+
33
29
 
34
30
  return (
35
31
  <div
36
32
  ref={ref}
37
33
  className={cn(
38
- "rounded-[var(--radius-lg)] border text-card-foreground transition-[border-color,box-shadow,background-color]",
39
- "duration-[var(--transition-default)]",
40
- variants[variant],
34
+ cardPresentation.root,
35
+ cardPresentation.variants[variant],
41
36
  (hover || interactive) && ixControl,
42
37
  className,
43
38
  )}
@@ -98,7 +93,7 @@ const CardContent = React.forwardRef<
98
93
  >(({ className, ...props }, ref) => (
99
94
  <div
100
95
  ref={ref}
101
- className={cn("p-4 [&:not(:first-child)]:pt-0", className)}
96
+ className={cn(cardPresentation.padding, "[&:not(:first-child)]:pt-0", className)}
102
97
  {...props}
103
98
  />
104
99
  ));
@@ -71,7 +71,8 @@ describe("Sheet", () => {
71
71
  );
72
72
  fireEvent.click(screen.getByText("Open chat"));
73
73
  const dialog = await screen.findByRole("dialog", { name: "Assistant" });
74
- expect(dialog.className).toContain("slide-in-from-left");
74
+ expect(dialog).toHaveAttribute("data-state", "open");
75
+ expect(dialog.style.opacity).toBe("1");
75
76
  fireEvent.click(screen.getByRole("button", { name: "Close" }));
76
77
  await waitFor(() => expect(screen.queryByRole("dialog")).not.toBeInTheDocument());
77
78
  });
@@ -1,7 +1,7 @@
1
1
  // Re-exports the canonical themed CodeBlock from the markdown package.
2
2
  // Kept here for backwards compatibility with openui and other importers.
3
3
  export { CodeBlock, CopyButton } from "../markdown/code-block";
4
- export type { CodeBlockProps } from "../markdown/code-block";
4
+ export type { CodeBlockProps, CopyButtonProps } from "../markdown/code-block";
5
5
 
6
6
  import { cn } from "../lib/utils";
7
7
 
@@ -1,7 +1,7 @@
1
1
  import * as React from "react";
2
2
  import { cleanup, render, screen } from "@testing-library/react";
3
3
  import { afterEach, describe, expect, it } from "vitest";
4
- import { controlHeight, controlText, controlMotion, fieldPresentation } from "../lib/control-presentation";
4
+ import { controlHeight, controlText, fieldPresentation } from "../lib/control-presentation";
5
5
  import { HelpText } from "./help-text";
6
6
  import { Label } from "./label";
7
7
  import { Button, buttonVariants } from "./button";
@@ -11,7 +11,7 @@ import { Select, SelectTrigger, SelectValue } from "./select";
11
11
  afterEach(cleanup);
12
12
 
13
13
  describe("shared control presentation", () => {
14
- it("uses the field well, not the border/track alias, with scoped foreground and time-only motion", () => {
14
+ it("uses the field well, not the border/track alias, with scoped foreground", () => {
15
15
  // The well is the brand's --well context variable; Toolbar can still pick the fill.
16
16
  expect(fieldPresentation).toContain("bg-[var(--field-surface,var(--well))]");
17
17
  expect(fieldPresentation.split(" ")).not.toContain("bg-input");
@@ -20,10 +20,6 @@ describe("shared control presentation", () => {
20
20
  expect(fieldPresentation.split(" ")).toContain("border-[var(--field-border)]");
21
21
  expect(fieldPresentation.split(" ")).not.toContain("border-border");
22
22
  expect(fieldPresentation).toContain("text-foreground");
23
- expect(fieldPresentation).not.toContain("duration-150");
24
- expect(controlMotion).toContain("duration-[var(--duration-fast)]");
25
- expect(controlMotion).not.toContain("--transition-fast");
26
- expect(controlMotion).toContain("motion-reduce:duration-0");
27
23
  });
28
24
 
29
25
  it("puts Button, Input, Textarea and SelectTrigger on one height and text scale", () => {
@@ -116,7 +112,6 @@ describe("shared control presentation", () => {
116
112
  expect(buttonVariants({ variant: "destructive" })).toContain("text-destructive-foreground");
117
113
  expect(buttonVariants({ variant: "sandbox" })).toContain("text-[var(--btn-primary-text)]");
118
114
  expect(buttonVariants()).not.toContain("transition-all");
119
- expect(buttonVariants()).toContain("box-shadow,transform,scale");
120
115
  expect(buttonVariants()).toContain("motion-reduce:active:scale-100");
121
116
  });
122
117
  });
@@ -1,3 +1,5 @@
1
+ import { SurfaceMotion } from "../lib/surface-motion";
2
+ import { dialogPresentation } from "../presentation";
1
3
  import * as DialogPrimitive from "@radix-ui/react-dialog";
2
4
  import { X } from "lucide-react";
3
5
  import * as React from "react";
@@ -15,17 +17,17 @@ const DialogClose = DialogPrimitive.Close;
15
17
  const DialogOverlay = React.forwardRef<
16
18
  React.ElementRef<typeof DialogPrimitive.Overlay>,
17
19
  React.ComponentPropsWithoutRef<typeof DialogPrimitive.Overlay>
18
- >(({ className, ...props }, ref) => (
20
+ >(({ className, asChild, children, ...props }, ref) => (
19
21
  <DialogPrimitive.Overlay
20
22
  ref={ref}
21
23
  className={cn(
22
- "fixed inset-0 z-50 bg-black/40",
23
- "data-[state=closed]:animate-out data-[state=open]:animate-in",
24
- "data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0",
24
+ dialogPresentation.overlay,
25
+ dialogPresentation.transition,
25
26
  className,
26
27
  )}
27
28
  {...props}
28
- />
29
+ asChild
30
+ ><SurfaceMotion kind="overlay" asChild={asChild}>{children}</SurfaceMotion></DialogPrimitive.Overlay>
29
31
  ));
30
32
  DialogOverlay.displayName = DialogPrimitive.Overlay.displayName;
31
33
 
@@ -40,7 +42,7 @@ const DialogContent = React.forwardRef<
40
42
  */
41
43
  hideCloseButton?: boolean;
42
44
  }
43
- >(({ className, children, variant = "default", hideCloseButton = false, ...props }, ref) => {
45
+ >(({ className, children, variant = "default", hideCloseButton = false, asChild, ...props }, ref) => {
44
46
  const variants = {
45
47
  default: "border-border",
46
48
  sandbox: "border-[var(--border-accent)] shadow-[var(--shadow-accent)]",
@@ -52,23 +54,21 @@ const DialogContent = React.forwardRef<
52
54
  <DialogPrimitive.Content
53
55
  ref={ref}
54
56
  className={cn(
55
- "fixed top-[50%] left-[50%] z-50 grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%]",
56
- "gap-4 rounded-lg border bg-card p-6 shadow-lg duration-200",
57
- "data-[state=closed]:animate-out data-[state=open]:animate-in",
58
- "data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0",
59
- "data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95",
60
- "data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%]",
61
- "data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%]",
57
+ dialogPresentation.position,
58
+ dialogPresentation.surface,
59
+ dialogPresentation.transition,
62
60
  variants[variant],
63
61
  className,
64
62
  )}
65
63
  {...props}
64
+ asChild
66
65
  >
66
+ <SurfaceMotion kind="dialog" asChild={asChild}>
67
67
  {children}
68
68
  {!hideCloseButton && (
69
69
  <DialogPrimitive.Close
70
70
  className={cn(
71
- "absolute top-4 right-4 rounded-sm opacity-70 transition-[opacity,box-shadow] hover:opacity-100 focus-visible:opacity-100 disabled:pointer-events-none data-[state=open]:bg-accent data-[state=open]:text-muted-foreground",
71
+ dialogPresentation.close,
72
72
  focusRing,
73
73
  )}
74
74
  >
@@ -76,6 +76,7 @@ const DialogContent = React.forwardRef<
76
76
  <span className="sr-only">Close</span>
77
77
  </DialogPrimitive.Close>
78
78
  )}
79
+ </SurfaceMotion>
79
80
  </DialogPrimitive.Content>
80
81
  </DialogPortal>
81
82
  );
@@ -88,7 +89,7 @@ const DialogHeader = ({
88
89
  }: React.HTMLAttributes<HTMLDivElement>) => (
89
90
  <div
90
91
  className={cn(
91
- "flex flex-col space-y-1.5 text-center sm:text-left",
92
+ dialogPresentation.header,
92
93
  className,
93
94
  )}
94
95
  {...props}
@@ -102,7 +103,7 @@ const DialogFooter = ({
102
103
  }: React.HTMLAttributes<HTMLDivElement>) => (
103
104
  <div
104
105
  className={cn(
105
- "flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2",
106
+ dialogPresentation.footer,
106
107
  className,
107
108
  )}
108
109
  {...props}
@@ -117,7 +118,7 @@ const DialogTitle = React.forwardRef<
117
118
  <DialogPrimitive.Title
118
119
  ref={ref}
119
120
  className={cn(
120
- "font-semibold text-lg leading-none tracking-tight",
121
+ dialogPresentation.title,
121
122
  className,
122
123
  )}
123
124
  {...props}
@@ -131,7 +132,7 @@ const DialogDescription = React.forwardRef<
131
132
  >(({ className, ...props }, ref) => (
132
133
  <DialogPrimitive.Description
133
134
  ref={ref}
134
- className={cn("text-muted-foreground text-sm", className)}
135
+ className={cn(dialogPresentation.description, className)}
135
136
  {...props}
136
137
  />
137
138
  ));
@@ -1,3 +1,4 @@
1
+ import { emptyStatePresentation } from "../presentation";
1
2
  import * as React from "react";
2
3
  import { cn } from "../lib/utils";
3
4
 
@@ -17,23 +18,23 @@ const EmptyState = React.forwardRef<HTMLDivElement, EmptyStateProps>(
17
18
  <div
18
19
  ref={ref}
19
20
  className={cn(
20
- "flex flex-col items-center justify-center px-4 py-16 text-center",
21
+ emptyStatePresentation.root,
21
22
  className,
22
23
  )}
23
24
  {...props}
24
25
  >
25
26
  {icon && (
26
- <div className="mb-4 rounded-full bg-muted p-4 text-muted-foreground">
27
+ <div className={emptyStatePresentation.icon}>
27
28
  {icon}
28
29
  </div>
29
30
  )}
30
- <Title className="font-semibold text-lg">{title}</Title>
31
+ <Title className={emptyStatePresentation.title}>{title}</Title>
31
32
  {description && (
32
- <p className="mt-2 max-w-sm text-muted-foreground text-sm">
33
+ <p className={emptyStatePresentation.description}>
33
34
  {description}
34
35
  </p>
35
36
  )}
36
- {action && <div className="mt-6">{action}</div>}
37
+ {action && <div className={emptyStatePresentation.action}>{action}</div>}
37
38
  </div>
38
39
  );
39
40
  },
@@ -38,6 +38,7 @@ export interface HeadingProps
38
38
  extends Omit<React.HTMLAttributes<HTMLElement>, "role"> {
39
39
  /** Visual treatment, independent of the rendered element. Defaults to section. */
40
40
  variant?: HeadingVariant;
41
+ presentation?: "operational" | "expressive";
41
42
  /** Semantic element; changing it does not change the visual variant. */
42
43
  as?: React.ElementType;
43
44
  /**
@@ -53,7 +54,7 @@ export interface HeadingProps
53
54
  * display/page h1 per page; choose `as` according to the document hierarchy.
54
55
  */
55
56
  const Heading = React.forwardRef<HTMLElement, HeadingProps>(
56
- ({ variant, role, as, className, ...props }, ref) => {
57
+ ({ variant, role, as, className, presentation, ...props }, ref) => {
57
58
  const legacyVariant =
58
59
  role && Object.prototype.hasOwnProperty.call(VARIANT_CLASS, role)
59
60
  ? (role as HeadingVariant)
@@ -64,7 +65,8 @@ const Heading = React.forwardRef<HTMLElement, HeadingProps>(
64
65
  return (
65
66
  <Tag
66
67
  ref={ref}
67
- className={cn("[overflow-wrap:anywhere]", VARIANT_CLASS[visualVariant], className)}
68
+ className={cn("[overflow-wrap:anywhere]", presentation ? (presentation === "expressive" ? "tangle-display" : "tangle-heading") : VARIANT_CLASS[visualVariant], className)}
69
+ data-variant={presentation ? visualVariant : undefined}
68
70
  role={legacyVariant ? undefined : role}
69
71
  {...props}
70
72
  />
@@ -1,3 +1,4 @@
1
+ import { helpPresentation } from "../presentation";
1
2
  import * as React from "react";
2
3
  import { cn } from "../lib/utils";
3
4
 
@@ -17,8 +18,8 @@ const HelpText = React.forwardRef<HTMLParagraphElement, HelpTextProps>(
17
18
  ref={ref}
18
19
  data-tone={tone}
19
20
  className={cn(
20
- "text-[length:var(--font-size-help,0.75rem)] leading-4",
21
- tone === "error" ? "font-medium text-[var(--surface-danger-text)]" : "text-[var(--text-dim)]",
21
+ helpPresentation.root,
22
+ helpPresentation[tone],
22
23
  className,
23
24
  )}
24
25
  {...props}
@@ -1,45 +1,17 @@
1
1
  "use client";
2
2
 
3
3
  import * as React from "react";
4
- import { controlRadius, controlSizes, controlText, fieldPresentation, fieldSizes, fieldTouchText } from "../lib/control-presentation";
4
+ import { controlRadius, controlSizes, controlText, fieldPresentation, fieldTouchText } from "../lib/control-presentation";
5
5
  import { focusFieldInvalid } from "../lib/focus";
6
6
  import { cn } from "../lib/utils";
7
7
  import { HelpText } from "./help-text";
8
8
  import { Label } from "./label";
9
9
 
10
- import { cva, type VariantProps } from "class-variance-authority";
10
+ import { inputVariants } from "../presentation";
11
+ import { type VariantProps } from "class-variance-authority";
11
12
 
12
13
  // Sizes follow the shared control scale (control-presentation.ts), so an Input
13
14
  // and a Button at the same size share a height, text size and corner.
14
- const inputVariants = cva(
15
- cn(
16
- "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",
17
- fieldPresentation,
18
- fieldTouchText,
19
- ),
20
- {
21
- variants: {
22
- // `default` and `sandbox` render the same field; both names stay valid.
23
- variant: {
24
- default: "",
25
- sandbox: "",
26
- error: focusFieldInvalid,
27
- },
28
- size: {
29
- default: fieldSizes.md,
30
- md: fieldSizes.md,
31
- sm: fieldSizes.sm,
32
- lg: fieldSizes.lg,
33
- compact: cn(fieldSizes.md, controlSizes.compact),
34
- touch: cn(fieldSizes.md, controlSizes.touch),
35
- }
36
- },
37
- defaultVariants: {
38
- variant: "default",
39
- size: "default",
40
- },
41
- }
42
- );
43
15
 
44
16
  // Only rendered descriptions are added; caller IDs and their order are retained.
45
17
  function descriptionIds(...ids: Array<string | undefined>) {
@@ -1,14 +1,12 @@
1
1
  "use client";
2
2
 
3
3
  import * as LabelPrimitive from "@radix-ui/react-label";
4
- import { cva, type VariantProps } from "class-variance-authority";
4
+ import { labelVariants } from "../presentation";
5
5
  import * as React from "react";
6
6
  import { cn } from "../lib/utils";
7
7
 
8
8
  // One label size (`--font-size-label`, 14px) for every field size.
9
- const labelVariants = cva(
10
- "font-medium text-[length:var(--font-size-label,0.875rem)] leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70",
11
- );
9
+
12
10
 
13
11
  const Label = React.forwardRef<
14
12
  React.ElementRef<typeof LabelPrimitive.Root>,
@@ -1,17 +1,8 @@
1
+ import { metricPresentation, metricLayouts } from "../presentation";
1
2
  import * as React from "react";
2
3
  import { cn } from "../lib/utils";
3
4
  import { StatusPill, type StatusTone } from "./status-pill";
4
5
 
5
- // Each finite preset owns its grid AND its row-start rules. Keep the strings
6
- // literal so host Tailwind consumers scanning the published source emit them.
7
- // Ranges do not overlap: no drawn border is subsequently suppressed.
8
- const layouts = {
9
- 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",
10
- 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",
11
- 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",
12
- 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",
13
- } as const;
14
-
15
6
  /** Headline readings on one plane, with native definition-list semantics. */
16
7
  export interface MetricStripProps
17
8
  extends React.HTMLAttributes<HTMLDListElement> {
@@ -26,8 +17,8 @@ const MetricStrip = React.forwardRef<HTMLDListElement, MetricStripProps>(
26
17
  <dl
27
18
  ref={ref}
28
19
  className={cn(
29
- "grid min-w-0 rounded-[var(--radius-lg)] border border-border bg-card shadow-[var(--shadow-card)]",
30
- layouts[columns],
20
+ metricPresentation.strip,
21
+ metricLayouts[columns],
31
22
  className,
32
23
  )}
33
24
  {...props}
@@ -55,10 +46,10 @@ const Metric = React.forwardRef<HTMLDivElement, MetricProps>(
55
46
  ({ className, label, value, hint, attention, icon, ...props }, ref) => (
56
47
  <div
57
48
  ref={ref}
58
- className={cn("min-w-0 border-border p-4 sm:p-5", className)}
49
+ className={cn(metricPresentation.item, className)}
59
50
  {...props}
60
51
  >
61
- <dt className="flex min-w-0 flex-wrap items-center gap-2 text-muted-foreground text-sm">
52
+ <dt className={metricPresentation.label}>
62
53
  {icon != null && icon !== false && (
63
54
  <span
64
55
  aria-hidden="true"
@@ -67,7 +58,7 @@ const Metric = React.forwardRef<HTMLDivElement, MetricProps>(
67
58
  {icon}
68
59
  </span>
69
60
  )}
70
- <span className="min-w-0 whitespace-normal [overflow-wrap:anywhere]">{label}</span>
61
+ <span className={metricPresentation.labelText}>{label}</span>
71
62
  {attention && (
72
63
  <StatusPill tone={attention.tone}>{attention.label}</StatusPill>
73
64
  )}
@@ -76,7 +67,7 @@ const Metric = React.forwardRef<HTMLDivElement, MetricProps>(
76
67
  title. Wrap even unbroken values without making the grid wider. */}
77
68
  <dd
78
69
  className={cn(
79
- "mt-1 whitespace-normal [overflow-wrap:anywhere] font-semibold text-xl tabular-nums tracking-tight lg:text-2xl",
70
+ metricPresentation.value,
80
71
  attention?.tone === "danger" && "text-[var(--surface-danger-text)]",
81
72
  )}
82
73
  title={typeof value === "string" ? value : undefined}
@@ -85,7 +76,7 @@ const Metric = React.forwardRef<HTMLDivElement, MetricProps>(
85
76
  </dd>
86
77
  {hint !== undefined && hint !== null && hint !== false && hint !== "" && (
87
78
  <dd
88
- className="mt-0.5 whitespace-normal [overflow-wrap:anywhere] text-[var(--text-dim)] text-xs"
79
+ className={metricPresentation.hint}
89
80
  title={typeof hint === "string" ? hint : undefined}
90
81
  >
91
82
  {hint}
@@ -10,6 +10,7 @@ import { Heading } from "./heading";
10
10
  export interface PageHeaderProps
11
11
  extends Omit<React.HTMLAttributes<HTMLElement>, "title"> {
12
12
  title: React.ReactNode;
13
+ presentation?: "operational" | "expressive";
13
14
  description?: React.ReactNode;
14
15
  /** Canonical action slot. If supplied (even null), takes precedence over action. */
15
16
  actions?: React.ReactNode;
@@ -31,6 +32,7 @@ const PageHeader = React.forwardRef<HTMLElement, PageHeaderProps>(
31
32
  (
32
33
  {
33
34
  className,
35
+ presentation,
34
36
  title,
35
37
  description,
36
38
  actions,
@@ -46,28 +48,29 @@ const PageHeader = React.forwardRef<HTMLElement, PageHeaderProps>(
46
48
  ) => {
47
49
  const resolvedActions = actions === undefined ? action : actions;
48
50
  return (
49
- <header ref={ref} className={cn("mb-6 min-w-0", className)} {...props}>
50
- <div className="flex min-w-0 flex-wrap items-start justify-between gap-x-6 gap-y-3">
51
+ <header ref={ref} className={cn(presentation === "expressive" ? "tangle-intro" : presentation === "operational" ? "tangle-page-header" : "mb-6 min-w-0", className)} {...props}>
52
+ <div className={presentation ? "tangle-page-header-main" : "flex min-w-0 flex-wrap items-start justify-between gap-x-6 gap-y-3"}>
51
53
  <div className="min-w-0 flex-[1_1_16rem]">
52
54
  {hasContent(eyebrow) && (
53
55
  <Heading variant="eyebrow" className="mb-1.5">{eyebrow}</Heading>
54
56
  )}
55
57
  <Heading
56
58
  id={titleId}
57
- variant={level === 1 ? "page" : "section"}
59
+ variant={presentation === "expressive" ? "display" : level === 1 ? "page" : "section"}
60
+ presentation={presentation}
58
61
  as={titleAs ?? (level === 1 ? "h1" : "h2")}
59
62
  className="text-balance"
60
63
  >
61
64
  {title}
62
65
  </Heading>
63
66
  {hasContent(description) && (
64
- <p className="mt-1 max-w-prose text-muted-foreground text-sm [overflow-wrap:anywhere]">
67
+ <p className={presentation === "expressive" ? "tangle-intro-description" : presentation ? "tangle-page-description" : "mt-1 max-w-prose text-muted-foreground text-sm [overflow-wrap:anywhere]"}>
65
68
  {description}
66
69
  </p>
67
70
  )}
68
71
  </div>
69
72
  {hasContent(resolvedActions) && (
70
- <div className="flex min-w-0 max-w-full flex-wrap items-center gap-2 [overflow-wrap:anywhere] [&>*]:min-w-0 [&>*]:max-w-full">
73
+ <div className={presentation === "expressive" ? "tangle-intro-actions" : "flex min-w-0 max-w-full flex-wrap items-center gap-2 [overflow-wrap:anywhere] [&>*]:min-w-0 [&>*]:max-w-full"}>
71
74
  {resolvedActions}
72
75
  </div>
73
76
  )}
@@ -2,7 +2,13 @@ import type { ReactNode } from "react";
2
2
  import { cn } from "../lib/utils";
3
3
 
4
4
  export interface PageShellProps {
5
- children: ReactNode;
5
+ children?: ReactNode;
6
+ /** Optional reading layout. Content, routing and navigation interaction stay caller-owned. */
7
+ navigation?: ReactNode;
8
+ article?: ReactNode;
9
+ outline?: ReactNode;
10
+ navigationLabel?: string;
11
+ outlineLabel?: string;
6
12
  className?: string;
7
13
  }
8
14
 
@@ -12,7 +18,12 @@ export interface PageShellProps {
12
18
  * The caller owns landmarks, routing, auth, navigation and data state. Use
13
19
  * className to override the bound/gutters, or omit it for a workbench layout.
14
20
  */
15
- export function PageShell({ children, className }: PageShellProps) {
21
+ export function PageShell({ children, className, navigation, article, outline, navigationLabel = "Sections", outlineLabel = "On this page" }: PageShellProps) {
22
+ if (navigation || article || outline) return <div className={cn("tangle-reading-shell",className)}>
23
+ {navigation && <nav aria-label={navigationLabel} className="tangle-reading-nav">{navigation}</nav>}
24
+ <article className="tangle-reading-article">{article ?? children}</article>
25
+ {outline && <aside aria-label={outlineLabel} className="tangle-reading-outline">{outline}</aside>}
26
+ </div>;
16
27
  return (
17
28
  <div
18
29
  className={cn(
@@ -2,6 +2,8 @@
2
2
 
3
3
  import * as PopoverPrimitive from "@radix-ui/react-popover";
4
4
  import * as React from "react";
5
+ import { SurfaceMotion } from "../lib/surface-motion";
6
+ import { surfaceMotion } from "../lib/motion";
5
7
  import { cn } from "../lib/utils";
6
8
 
7
9
  const Popover = PopoverPrimitive.Root;
@@ -20,7 +22,7 @@ const PopoverClose = PopoverPrimitive.Close;
20
22
  const PopoverContent = React.forwardRef<
21
23
  React.ElementRef<typeof PopoverPrimitive.Content>,
22
24
  React.ComponentPropsWithoutRef<typeof PopoverPrimitive.Content>
23
- >(({ className, align = "center", sideOffset = 4, ...props }, ref) => (
25
+ >(({ className, align = "center", sideOffset = 4, asChild, children, ...props }, ref) => (
24
26
  <PopoverPrimitive.Portal>
25
27
  <PopoverPrimitive.Content
26
28
  ref={ref}
@@ -28,16 +30,12 @@ const PopoverContent = React.forwardRef<
28
30
  sideOffset={sideOffset}
29
31
  className={cn(
30
32
  "z-50 w-72 max-w-[calc(100vw-2rem)] origin-(--radix-popover-content-transform-origin) rounded-xl border border-border bg-card p-4 text-foreground shadow-[var(--shadow-card)] outline-hidden",
31
- "data-[state=closed]:animate-out data-[state=open]:animate-in",
32
- "data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0",
33
- "data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95",
34
- "data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2",
35
- "data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
36
- "motion-reduce:animate-none",
33
+ surfaceMotion,
37
34
  className,
38
35
  )}
39
36
  {...props}
40
- />
37
+ asChild
38
+ ><SurfaceMotion kind="popover" asChild={asChild}>{children}</SurfaceMotion></PopoverPrimitive.Content>
41
39
  </PopoverPrimitive.Portal>
42
40
  ));
43
41
  PopoverContent.displayName = PopoverPrimitive.Content.displayName;
@@ -4,6 +4,8 @@ import * as SheetPrimitive from "@radix-ui/react-dialog";
4
4
  import { cva, type VariantProps } from "class-variance-authority";
5
5
  import { X } from "lucide-react";
6
6
  import * as React from "react";
7
+ import { SurfaceMotion } from "../lib/surface-motion";
8
+ import { controlMotion, surfaceMotion } from "../lib/motion";
7
9
  import { focusRing } from "../lib/focus";
8
10
  import { cn } from "../lib/utils";
9
11
 
@@ -19,35 +21,34 @@ const SheetPortal = SheetPrimitive.Portal;
19
21
  const SheetOverlay = React.forwardRef<
20
22
  React.ElementRef<typeof SheetPrimitive.Overlay>,
21
23
  React.ComponentPropsWithoutRef<typeof SheetPrimitive.Overlay>
22
- >(({ className, ...props }, ref) => (
24
+ >(({ className, asChild, children, ...props }, ref) => (
23
25
  <SheetPrimitive.Overlay
24
26
  ref={ref}
25
27
  className={cn(
26
28
  "fixed inset-0 z-50 bg-black/40",
27
- "data-[state=closed]:animate-out data-[state=open]:animate-in",
28
- "data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0",
29
+ surfaceMotion,
29
30
  className,
30
31
  )}
31
32
  {...props}
32
- />
33
+ asChild
34
+ ><SurfaceMotion kind="overlay" asChild={asChild}>{children}</SurfaceMotion></SheetPrimitive.Overlay>
33
35
  ));
34
36
  SheetOverlay.displayName = SheetPrimitive.Overlay.displayName;
35
37
 
36
38
  const sheetVariants = cva(
37
39
  [
38
40
  "fixed z-50 flex flex-col gap-4 border-border bg-card shadow-lg",
39
- "data-[state=closed]:animate-out data-[state=open]:animate-in",
40
- "data-[state=closed]:duration-200 data-[state=open]:duration-300 ease-out motion-reduce:animate-none",
41
+ surfaceMotion,
41
42
  ],
42
43
  {
43
44
  variants: {
44
45
  side: {
45
46
  right:
46
- "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",
47
- 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",
48
- top: "inset-x-0 top-0 h-auto border-b data-[state=closed]:slide-out-to-top data-[state=open]:slide-in-from-top",
47
+ "inset-y-0 right-0 h-full w-3/4 border-l sm:max-w-sm",
48
+ left: "inset-y-0 left-0 h-full w-3/4 border-r sm:max-w-sm",
49
+ top: "inset-x-0 top-0 h-auto border-b",
49
50
  bottom:
50
- "inset-x-0 bottom-0 h-auto border-t data-[state=closed]:slide-out-to-bottom data-[state=open]:slide-in-from-bottom",
51
+ "inset-x-0 bottom-0 h-auto border-t",
51
52
  },
52
53
  },
53
54
  defaultVariants: { side: "right" },
@@ -61,19 +62,22 @@ const SheetContent = React.forwardRef<
61
62
  /** Hide only the built-in close button; Escape and outside clicks still dismiss. */
62
63
  hideCloseButton?: boolean;
63
64
  }
64
- >(({ className, children, side = "right", hideCloseButton = false, ...props }, ref) => (
65
+ >(({ className, children, side = "right", hideCloseButton = false, asChild, ...props }, ref) => (
65
66
  <SheetPortal>
66
67
  <SheetOverlay />
67
68
  <SheetPrimitive.Content
68
69
  ref={ref}
69
70
  className={cn(sheetVariants({ side }), className)}
70
71
  {...props}
72
+ asChild
71
73
  >
74
+ <SurfaceMotion kind="sheet" side={side ?? "right"} asChild={asChild}>
72
75
  {children}
73
76
  {!hideCloseButton && (
74
77
  <SheetPrimitive.Close
75
78
  className={cn(
76
79
  "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",
80
+ controlMotion,
77
81
  focusRing,
78
82
  )}
79
83
  >
@@ -81,6 +85,7 @@ const SheetContent = React.forwardRef<
81
85
  <span className="sr-only">Close</span>
82
86
  </SheetPrimitive.Close>
83
87
  )}
88
+ </SurfaceMotion>
84
89
  </SheetPrimitive.Content>
85
90
  </SheetPortal>
86
91
  ));
@@ -6,7 +6,7 @@ function Skeleton({
6
6
  }: React.HTMLAttributes<HTMLDivElement>) {
7
7
  return (
8
8
  <div
9
- className={cn("animate-pulse rounded-lg bg-muted/50", className)}
9
+ className={cn("animate-pulse motion-reduce:animate-none rounded-lg bg-muted/50", className)}
10
10
  {...props}
11
11
  />
12
12
  );
@@ -9,6 +9,7 @@ const TONES: StatusTone[] = [
9
9
  "info",
10
10
  "running",
11
11
  "neutral",
12
+ "violet",
12
13
  ];
13
14
 
14
15
  describe("StatusPill", () => {