@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
@@ -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", () => {
@@ -26,7 +26,8 @@ export type StatusTone =
26
26
  | "danger"
27
27
  | "info"
28
28
  | "neutral"
29
- | "running";
29
+ | "running"
30
+ | "violet";
30
31
 
31
32
  export interface StatusPillProps
32
33
  extends Omit<React.HTMLAttributes<HTMLSpanElement>, "children"> {
@@ -46,10 +47,13 @@ export interface StatusPillProps
46
47
  * For a control that supplies its own surface (a chip, a selected row).
47
48
  */
48
49
  bare?: boolean;
50
+ /** Decorative caller glyph; the visible text remains the accessible status. */
51
+ icon?: React.ReactNode;
49
52
  size?: "sm" | "md";
50
53
  }
51
54
 
52
55
  const TONE_SURFACE: Record<StatusTone, string> = {
56
+ violet: "bg-[var(--surface-violet-bg)] text-[var(--surface-violet-text)] border-[var(--surface-violet-border)]",
53
57
  success:
54
58
  "bg-[var(--surface-success-bg)] text-[var(--surface-success-text)] border-[var(--surface-success-border)]",
55
59
  warning:
@@ -77,6 +81,7 @@ const TONE_SURFACE: Record<StatusTone, string> = {
77
81
  * has left — the glyph — with nothing to signal that it happened.
78
82
  */
79
83
  const TONE_TEXT: Record<StatusTone, string> = {
84
+ violet: "text-[var(--surface-violet-text)]",
80
85
  success: "text-[var(--surface-success-text)]",
81
86
  warning: "text-[var(--surface-warning-text)]",
82
87
  danger: "text-[var(--surface-danger-text)]",
@@ -100,6 +105,7 @@ function ToneGlyph({ tone }: { tone: StatusTone }) {
100
105
  className: "size-2 shrink-0",
101
106
  };
102
107
  switch (tone) {
108
+ case "violet": return <svg {...common} aria-hidden="true" fill="currentColor"><path d="M4 .5 7.5 4 4 7.5 .5 4Z"/></svg>;
103
109
  // Settled and good: a solid disc.
104
110
  case "success":
105
111
  return (
@@ -165,7 +171,7 @@ function ToneGlyph({ tone }: { tone: StatusTone }) {
165
171
  }
166
172
 
167
173
  const StatusPill = React.forwardRef<HTMLSpanElement, StatusPillProps>(
168
- ({ className, tone, children, bare = false, size = "sm", ...props }, ref) => (
174
+ ({ className, tone, children, icon, bare = false, size = "sm", ...props }, ref) => (
169
175
  <span
170
176
  ref={ref}
171
177
  className={cn(
@@ -180,8 +186,8 @@ const StatusPill = React.forwardRef<HTMLSpanElement, StatusPillProps>(
180
186
  >
181
187
  {/* In `bare` mode the tone rides on the glyph alone, so it is scoped to
182
188
  the wrapper the glyph sits in rather than applied to the whole pill. */}
183
- <span className={cn("inline-flex", bare && TONE_TEXT[tone])}>
184
- <ToneGlyph tone={tone} />
189
+ <span aria-hidden="true" className={cn("inline-flex [&>svg]:size-3 [&>svg]:shrink-0", bare && TONE_TEXT[tone])}>
190
+ {icon ?? <ToneGlyph tone={tone} />}
185
191
  </span>
186
192
  {children}
187
193
  </span>
@@ -1,3 +1,4 @@
1
+ import { tablePresentation } from "../presentation";
1
2
  import * as React from "react";
2
3
  import { cn } from "../lib/utils";
3
4
 
@@ -17,7 +18,7 @@ const Table = React.forwardRef<HTMLTableElement, TableProps>(
17
18
  const table = (
18
19
  <table
19
20
  ref={ref}
20
- className={cn("w-full caption-bottom text-sm", className)}
21
+ className={cn(tablePresentation.root, className)}
21
22
  {...props}
22
23
  />
23
24
  );
@@ -27,7 +28,7 @@ const Table = React.forwardRef<HTMLTableElement, TableProps>(
27
28
  return (
28
29
  <div
29
30
  {...attributes}
30
- className={cn("relative w-full overflow-auto", wrapperClassName)}
31
+ className={cn(tablePresentation.wrapper, wrapperClassName)}
31
32
  >
32
33
  {table}
33
34
  </div>
@@ -40,7 +41,7 @@ const TableHeader = React.forwardRef<
40
41
  HTMLTableSectionElement,
41
42
  React.HTMLAttributes<HTMLTableSectionElement>
42
43
  >(({ className, ...props }, ref) => (
43
- <thead ref={ref} className={cn("[&_tr]:border-b", className)} {...props} />
44
+ <thead ref={ref} className={cn(tablePresentation.head, className)} {...props} />
44
45
  ));
45
46
  TableHeader.displayName = "TableHeader";
46
47
 
@@ -50,7 +51,7 @@ const TableBody = React.forwardRef<
50
51
  >(({ className, ...props }, ref) => (
51
52
  <tbody
52
53
  ref={ref}
53
- className={cn("[&_tr:last-child]:border-0", className)}
54
+ className={cn(tablePresentation.body, className)}
54
55
  {...props}
55
56
  />
56
57
  ));
@@ -63,7 +64,7 @@ const TableFooter = React.forwardRef<
63
64
  <tfoot
64
65
  ref={ref}
65
66
  className={cn(
66
- "border-t bg-card font-medium [&>tr]:last:border-b-0",
67
+ tablePresentation.footer,
67
68
  className,
68
69
  )}
69
70
  {...props}
@@ -78,7 +79,7 @@ const TableRow = React.forwardRef<
78
79
  <tr
79
80
  ref={ref}
80
81
  className={cn(
81
- "border-border border-b transition-colors hover:bg-muted/50 data-[state=selected]:bg-muted/50",
82
+ tablePresentation.row,
82
83
  className,
83
84
  )}
84
85
  {...props}
@@ -93,7 +94,7 @@ const TableHead = React.forwardRef<
93
94
  <th
94
95
  ref={ref}
95
96
  className={cn(
96
- "h-12 px-4 text-left align-middle font-medium text-muted-foreground [&:has([role=checkbox])]:pr-0",
97
+ tablePresentation.columnHeader,
97
98
  className,
98
99
  )}
99
100
  {...props}
@@ -107,7 +108,7 @@ const TableCell = React.forwardRef<
107
108
  >(({ className, ...props }, ref) => (
108
109
  <td
109
110
  ref={ref}
110
- className={cn("p-4 align-middle [&:has([role=checkbox])]:pr-0", className)}
111
+ className={cn(tablePresentation.cell, className)}
111
112
  {...props}
112
113
  />
113
114
  ));
@@ -119,7 +120,7 @@ const TableCaption = React.forwardRef<
119
120
  >(({ className, ...props }, ref) => (
120
121
  <caption
121
122
  ref={ref}
122
- className={cn("mt-4 text-muted-foreground text-sm", className)}
123
+ className={cn(tablePresentation.caption, className)}
123
124
  {...props}
124
125
  />
125
126
  ));
@@ -1,7 +1,7 @@
1
1
  import * as TabsPrimitive from "@radix-ui/react-tabs";
2
2
  import * as React from "react";
3
3
  import { focusRing } from "../lib/focus";
4
- import { ixControlSoft, ixSelected, ixTabUnderline } from "../lib/interaction";
4
+ import { tabsPresentation } from "../presentation";
5
5
  import { cn } from "../lib/utils";
6
6
 
7
7
  const Tabs = TabsPrimitive.Root;
@@ -12,17 +12,12 @@ const TabsList = React.forwardRef<
12
12
  variant?: "default" | "pills" | "underline";
13
13
  }
14
14
  >(({ className, variant = "default", ...props }, ref) => {
15
- const variants = {
16
- default:
17
- "inline-flex h-10 items-center justify-center rounded-lg bg-muted p-1 text-muted-foreground",
18
- pills: "inline-flex items-center gap-2",
19
- underline: "inline-flex items-center gap-4 border-b border-border",
20
- };
15
+
21
16
 
22
17
  return (
23
18
  <TabsPrimitive.List
24
19
  ref={ref}
25
- className={cn(variants[variant], className)}
20
+ className={cn(tabsPresentation.list[variant], className)}
26
21
  {...props}
27
22
  />
28
23
  );
@@ -38,19 +33,12 @@ const TabsTrigger = React.forwardRef<
38
33
  // Selection is the shared selected state (lib/interaction.ts): a tint and an
39
34
  // inset ring for default and pills, a 2px underline for underline, whose
40
35
  // border-b-2 slot stays transparent.
41
- const variants = {
42
- default:
43
- `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}`,
44
- pills:
45
- `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}`,
46
- underline:
47
- `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}`,
48
- };
36
+
49
37
 
50
38
  return (
51
39
  <TabsPrimitive.Trigger
52
40
  ref={ref}
53
- className={cn(variants[variant], className)}
41
+ className={cn(tabsPresentation.trigger[variant], className)}
54
42
  {...props}
55
43
  />
56
44
  );
@@ -2,6 +2,8 @@
2
2
 
3
3
  import * as TooltipPrimitive from "@radix-ui/react-tooltip";
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
  /**
@@ -33,20 +35,19 @@ const TooltipTrigger = TooltipPrimitive.Trigger;
33
35
  const TooltipContent = React.forwardRef<
34
36
  React.ElementRef<typeof TooltipPrimitive.Content>,
35
37
  React.ComponentPropsWithoutRef<typeof TooltipPrimitive.Content>
36
- >(({ className, sideOffset = 6, ...props }, ref) => (
38
+ >(({ className, sideOffset = 6, asChild, children, ...props }, ref) => (
37
39
  <TooltipPrimitive.Portal>
38
40
  <TooltipPrimitive.Content
39
41
  ref={ref}
40
42
  sideOffset={sideOffset}
41
43
  className={cn(
42
44
  "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",
43
- "data-[state=delayed-open]:animate-in data-[state=delayed-open]:fade-in-0 data-[state=delayed-open]:zoom-in-95",
44
- "data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95",
45
- "duration-150 ease-out motion-reduce:animate-none",
45
+ surfaceMotion,
46
46
  className,
47
47
  )}
48
48
  {...props}
49
- />
49
+ asChild
50
+ ><SurfaceMotion kind="tooltip" asChild={asChild}>{children}</SurfaceMotion></TooltipPrimitive.Content>
50
51
  </TooltipPrimitive.Portal>
51
52
  ));
52
53
  TooltipContent.displayName = TooltipPrimitive.Content.displayName;
@@ -0,0 +1,12 @@
1
+ /* Generated by scripts/generate-presentation-css.mjs from shared primitive recipes. Requires Tailwind + Brand theme. */
2
+ @layer components {
3
+ .btn { @apply inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-lg font-medium [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 focus-visible:outline-1 focus-visible:outline-[var(--focus-border)] focus-visible:ring-4 focus-visible:ring-[var(--focus-halo)] focus-visible:outline-offset-0 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 h-[var(--control-height,2.25rem)] text-[length:var(--control-text-md,0.875rem)] rounded-lg px-4 py-2; }
4
+ .btn-primary { @apply inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-lg font-medium [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 focus-visible:outline-1 focus-visible:outline-[var(--focus-border)] focus-visible:ring-4 focus-visible:ring-[var(--focus-halo)] focus-visible:outline-offset-0 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 bg-primary text-primary-foreground hover:bg-primary/90 active:scale-[0.97] hover:border-[var(--ix-hover-border)] hover:shadow-[var(--ix-hover)] focus:not-focus-visible:border-[var(--ix-hover-border)] focus:not-focus-visible:shadow-[var(--ix-after)] active:border-[hsl(var(--hsl-ring))] active:shadow-[var(--ix-press)] focus-visible:shadow-none h-[var(--control-height,2.25rem)] text-[length:var(--control-text-md,0.875rem)] rounded-lg px-4 py-2; }
5
+ .btn-secondary { @apply inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-lg font-medium [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 focus-visible:outline-1 focus-visible:outline-[var(--focus-border)] focus-visible:ring-4 focus-visible:ring-[var(--focus-halo)] focus-visible:outline-offset-0 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 bg-muted border border-border text-foreground hover:bg-muted/80 active:scale-[0.97] hover:border-[var(--ix-hover-border)] hover:shadow-[var(--ix-hover)] focus:not-focus-visible:border-[var(--ix-hover-border)] focus:not-focus-visible:shadow-[var(--ix-after)] active:border-[hsl(var(--hsl-ring))] active:shadow-[var(--ix-press)] focus-visible:shadow-none h-[var(--control-height,2.25rem)] text-[length:var(--control-text-md,0.875rem)] rounded-lg px-4 py-2; }
6
+ .btn-danger { @apply inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-lg font-medium [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 focus-visible:outline-1 focus-visible:outline-[var(--focus-border)] focus-visible:ring-4 focus-visible:ring-[var(--focus-halo)] focus-visible:outline-offset-0 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 bg-destructive text-destructive-foreground hover:bg-destructive/90 active:scale-[0.97] hover:border-[var(--ix-hover-border)] hover:shadow-[var(--ix-hover)] focus:not-focus-visible:border-[var(--ix-hover-border)] focus:not-focus-visible:shadow-[var(--ix-after)] active:border-[hsl(var(--hsl-ring))] active:shadow-[var(--ix-press)] focus-visible:shadow-none h-[var(--control-height,2.25rem)] text-[length:var(--control-text-md,0.875rem)] rounded-lg px-4 py-2; }
7
+ .input { @apply 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 focus:outline-hidden focus:border-[var(--focus-border)] focus:ring-3 focus:ring-[var(--focus-halo)] bg-[var(--field-surface,var(--well))] border-[var(--field-border)] shadow-[var(--shadow-well)] hover:border-[var(--ix-hover-border)] hover:shadow-[var(--shadow-well),var(--ix-hover)] [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 min-w-0 text-foreground aria-invalid:border-[var(--surface-danger-border)] aria-invalid:hover:border-[var(--surface-danger-border)] aria-invalid:focus:border-[var(--focus-border-danger)] aria-invalid:focus:ring-[var(--focus-halo-danger)] autofill:shadow-[inset_0_0_0_1000px_var(--field-surface,var(--well))] autofill:[-webkit-text-fill-color:var(--text-primary)] autofill:caret-[var(--text-primary)] pointer-coarse:text-base h-[var(--control-height,2.25rem)] text-[length:var(--control-text-md,0.875rem)] rounded-lg px-3; }
8
+ .card { @apply rounded-[var(--radius-lg)] border text-card-foreground [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 bg-card border-border shadow-[var(--edge-highlight),var(--shadow-card)] p-4; }
9
+ .stat-card { @apply rounded-[var(--radius-lg)] border text-card-foreground [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 bg-card border-border shadow-[var(--edge-highlight),var(--shadow-card)] min-w-0 border-border p-4 sm:p-5; }
10
+ .table-header { @apply h-12 px-4 text-left align-middle font-medium text-muted-foreground [&:has([role=checkbox])]:pr-0; }
11
+ .table-cell { @apply p-4 align-middle [&:has([role=checkbox])]:pr-0; }
12
+ }
@@ -0,0 +1,30 @@
1
+ /* Requires Brand styles; the same contracts serve React and Hono. */
2
+ .tangle-heading { color: hsl(var(--foreground)); font-family: var(--font-display); font-weight: 600; line-height: var(--line-height-heading); letter-spacing: var(--tracking-tight); }
3
+ .tangle-heading[data-variant="display"] { font-size: var(--font-size-display); line-height: var(--line-height-display); }
4
+ .tangle-heading[data-variant="hero"] { font-size: var(--font-size-hero); line-height: var(--line-height-display); }
5
+ .tangle-heading[data-variant="page"] { font-size: var(--font-size-3xl); }
6
+ .tangle-heading[data-variant="section"] { font-size: var(--font-size-xl); }
7
+ .tangle-heading[data-variant="subsection"] { font-size: var(--font-size-lg); }
8
+ .tangle-heading[data-variant="eyebrow"] { font-size: var(--font-size-sm); text-transform: uppercase; letter-spacing: var(--tracking-wide); color: hsl(var(--muted-foreground)); }
9
+ .tangle-page-header { min-width: 0; margin-bottom: 1.5rem; }
10
+ .tangle-page-header-main { display: flex; flex-wrap: wrap; justify-content: space-between; gap: 1.5rem; }
11
+ .tangle-intro .tangle-page-header-main { display: block; }
12
+ .tangle-reading-shell { display: grid; grid-template-columns: minmax(0,1fr); gap: 2rem; padding: 2rem var(--hero-gutter); max-width: var(--content-max-width); margin: auto; min-width: 0; }
13
+ .tangle-reading-shell > * { min-width: 0; }
14
+ .tangle-reading-article { width: 100%; max-width: var(--reading-max-width); overflow-wrap: anywhere; }
15
+ .tangle-reading-nav a, .tangle-reading-outline a { display: block; padding: 0.5rem 0; color: hsl(var(--muted-foreground)); text-decoration: none; }
16
+ .tangle-reading-nav a[aria-current]:not([aria-current="false"]), .tangle-reading-outline a[aria-current]:not([aria-current="false"]) { color: var(--accent-text); font-weight: 600; }
17
+ @media(min-width: 64rem) {
18
+ .tangle-reading-shell { grid-template-columns: 13rem minmax(0,1fr); }
19
+ .tangle-reading-outline { display: none; }
20
+ .tangle-reading-nav { position: sticky; top: var(--shell-header-height); align-self: start; max-height: calc(100dvh - var(--shell-header-height)); overflow: auto; }
21
+ }
22
+ @media(min-width: 80rem) {
23
+ .tangle-reading-shell:has(.tangle-reading-outline) { grid-template-columns: 13rem minmax(0,1fr) 11rem; }
24
+ .tangle-reading-outline { display: block; position: sticky; top: var(--shell-header-height); align-self: start; }
25
+ }
26
+ /* Native dialog has the same solid reading surface as React's Dialog/Sheet. */
27
+ .tangle-native-dialog { max-width: min(32rem,calc(100% - 2rem)); max-height: calc(100dvh - 2rem); overflow: auto; margin: auto; padding: 1.5rem; border: 1px solid hsl(var(--border)); border-radius: var(--radius-lg); background: hsl(var(--card)); color: hsl(var(--card-foreground)); }
28
+ .tangle-native-dialog::backdrop { background: var(--scrim); }
29
+
30
+ .tangle-page-description { margin-top: 0.25rem; max-width: 65ch; color: hsl(var(--muted-foreground)); font-size: var(--font-size-label); overflow-wrap: anywhere; }
@@ -1,24 +0,0 @@
1
- import { render, screen } from "@testing-library/react";
2
- import { describe, expect, it } from "vitest";
3
- import { PageShell } from "./page-shell";
4
-
5
- describe("PageShell", () => {
6
- it("ports Sandbox's width, gutters and rhythm without imposing a landmark or grid", () => {
7
- const { container } = render(<PageShell><p>Content</p></PageShell>);
8
- const shell = container.firstElementChild;
9
- expect(shell?.tagName).toBe("DIV");
10
- expect(shell).toHaveClass("mx-auto", "max-w-6xl", "space-y-8", "px-6", "py-8", "lg:px-8");
11
- expect(shell).not.toHaveAttribute("role");
12
- expect(shell).not.toHaveClass("grid");
13
- expect(screen.queryByRole("main")).toBeNull();
14
- expect(screen.getByText("Content")).toBeInTheDocument();
15
- });
16
-
17
- it("allows a caller-owned fluid layout without introducing shell behavior", () => {
18
- const { container } = render(<main><PageShell className="mx-0 max-w-none px-0 lg:px-0"><p>Workbench</p></PageShell></main>);
19
- const shell = container.querySelector("main > div");
20
- expect(shell).toHaveClass("mx-0", "max-w-none", "px-0", "lg:px-0");
21
- expect(shell).not.toHaveClass("mx-auto", "max-w-6xl", "px-6", "lg:px-8");
22
- expect(screen.getAllByRole("main")).toHaveLength(1);
23
- });
24
- });