@tangle-network/ui 11.34.0 → 11.36.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +12 -0
- package/README.md +1 -1
- package/dist/index.d.ts +4 -3
- package/dist/index.js +2 -1
- package/dist/lib/control-presentation.d.ts +34 -1
- package/dist/lib/control-presentation.js +1 -1
- package/dist/lib/focus.d.ts +2 -2
- package/dist/lib/focus.js +3 -2
- package/dist/lib/motion.d.ts +4 -0
- package/dist/lib/motion.js +7 -0
- package/dist/lib/surface-motion.js +84 -0
- package/dist/presentation.d.ts +88 -0
- package/dist/presentation.js +183 -0
- package/dist/primitives/alert-dialog.js +1 -1
- package/dist/primitives/alert.d.ts +1 -2
- package/dist/primitives/button.d.ts +2 -5
- package/dist/primitives/button.js +3 -30
- package/dist/primitives/card.js +3 -7
- package/dist/primitives/date-picker.d.ts +1 -1
- package/dist/primitives/dialog.js +29 -16
- package/dist/primitives/empty-state.js +6 -5
- package/dist/primitives/heading.d.ts +1 -0
- package/dist/primitives/heading.js +3 -2
- package/dist/primitives/help-text.js +2 -1
- package/dist/primitives/input.d.ts +1 -4
- package/dist/primitives/input.js +3 -24
- package/dist/primitives/label.js +1 -2
- package/dist/primitives/metric-strip.js +7 -12
- package/dist/primitives/page-header.d.ts +1 -0
- package/dist/primitives/page-header.js +7 -6
- package/dist/primitives/page-shell.d.ts +8 -2
- package/dist/primitives/page-shell.js +21 -2
- package/dist/primitives/pagination.js +1 -1
- package/dist/primitives/segmented-control.d.ts +6 -3
- package/dist/primitives/segmented-control.js +105 -56
- package/dist/primitives/sheet.js +29 -19
- package/dist/primitives/skeleton.js +1 -1
- package/dist/primitives/status-pill.d.ts +3 -1
- package/dist/primitives/status-pill.js +12 -3
- package/dist/primitives/table.js +10 -9
- package/dist/primitives/tabs.js +3 -12
- package/dist/primitives/tooltip.js +11 -3
- package/dist/primitives.d.ts +3 -2
- package/dist/primitives.js +2 -1
- package/dist/utils.d.ts +1 -1
- package/package.json +15 -5
- package/src/lib/control-presentation.ts +2 -4
- package/src/lib/focus.ts +3 -2
- package/src/lib/motion.ts +7 -0
- package/src/lib/surface-motion.tsx +78 -0
- package/src/presentation.ts +205 -0
- package/src/primitives/alert.tsx +1 -2
- package/src/primitives/button.tsx +4 -40
- package/src/primitives/card.tsx +5 -10
- package/src/primitives/checkbox-separator-sheet-tooltip.test.tsx +2 -1
- package/src/primitives/control-presentation.test.tsx +2 -7
- package/src/primitives/dialog.tsx +19 -18
- package/src/primitives/empty-state.tsx +6 -5
- package/src/primitives/heading.tsx +4 -2
- package/src/primitives/help-text.tsx +3 -2
- package/src/primitives/input.tsx +3 -31
- package/src/primitives/label.tsx +2 -4
- package/src/primitives/metric-strip.tsx +8 -17
- package/src/primitives/page-header.tsx +8 -5
- package/src/primitives/page-shell.tsx +13 -2
- package/src/primitives/segmented-control.stories.tsx +89 -0
- package/src/primitives/segmented-control.test.tsx +20 -7
- package/src/primitives/segmented-control.tsx +90 -29
- package/src/primitives/sheet.tsx +16 -11
- package/src/primitives/skeleton.tsx +1 -1
- package/src/primitives/status-pill.test.tsx +1 -0
- package/src/primitives/status-pill.tsx +10 -4
- package/src/primitives/table.tsx +10 -9
- package/src/primitives/tabs.tsx +5 -17
- package/src/primitives/tooltip.tsx +6 -5
- package/src/styles/compat.css +12 -0
- package/src/styles/presentation.css +30 -0
- package/src/primitives/page-shell.test.tsx +0 -24
package/src/primitives/sheet.tsx
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
|
47
|
-
left: "inset-y-0 left-0 h-full w-3/4 border-r
|
|
48
|
-
top: "inset-x-0 top-0 h-auto border-b
|
|
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
|
|
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
|
);
|
|
@@ -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>
|
package/src/primitives/table.tsx
CHANGED
|
@@ -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(
|
|
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(
|
|
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(
|
|
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(
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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(
|
|
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(
|
|
123
|
+
className={cn(tablePresentation.caption, className)}
|
|
123
124
|
{...props}
|
|
124
125
|
/>
|
|
125
126
|
));
|
package/src/primitives/tabs.tsx
CHANGED
|
@@ -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 {
|
|
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
|
-
|
|
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(
|
|
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
|
-
|
|
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(
|
|
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
|
-
|
|
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
|
-
});
|