@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/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,17 @@
|
|
|
1
1
|
# @tangle-network/ui
|
|
2
2
|
|
|
3
|
+
## 11.36.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- 19a10d9: Extend PageShell reading slots, Heading and PageHeader opt-in presentation, and Button display size. Publish React-free primitive presentation classes and native-dialog binding for Hono without changing existing control defaults.
|
|
8
|
+
|
|
9
|
+
## 11.35.0
|
|
10
|
+
|
|
11
|
+
### Minor Changes
|
|
12
|
+
|
|
13
|
+
- 19e3cd6: SegmentedControl: a strip whose options outgrow it now shows a chevron at each edge with options past it, instead of an edge fade that a host's inline padding or a cut between two options made invisible. The chevrons page the strip on click or tap and take no room in the row. The selected option is kept fully in view clear of the chevrons, and this now also works for a row control a host made scroll with `flex-nowrap overflow-x-auto`.
|
|
14
|
+
|
|
3
15
|
## 11.34.0
|
|
4
16
|
|
|
5
17
|
### Minor Changes
|
package/README.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
Generic React UI components for Tangle products. Imports tokens from `@tangle-network/brand` (peer dep). Tailwind v4-native.
|
|
4
4
|
|
|
5
|
-
Component visual precedent is governed by the repo-level [
|
|
5
|
+
Component visual precedent is governed by the repo-level [design bible](../../docs/design-bible.md) and [component audit](../../docs/component-audit.md). Storybook examples are test surfaces; they are not automatically approved brand patterns.
|
|
6
6
|
|
|
7
7
|
## Install
|
|
8
8
|
|
package/dist/index.d.ts
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { ixControl, ixControlAfter, ixControlSoft, ixField, ixRow, ixSelected, ixTabUnderline } from "./lib/interaction.js";
|
|
2
|
+
import { buttonVariants } from "./presentation.js";
|
|
3
|
+
import { Button, ButtonProps } from "./primitives/button.js";
|
|
2
4
|
import { AuthHeader, AuthHeaderProps, GitHubLoginButton, GitHubLoginButtonProps, SessionUser, UserMenu, UserMenuProps } from "./auth/auth.js";
|
|
3
5
|
import { LoginLayout, LoginLayoutProps } from "./auth/login-layout.js";
|
|
4
6
|
import "./auth.js";
|
|
@@ -88,9 +90,9 @@ import { ThemeToggle, useTheme } from "./primitives/theme-toggle.js";
|
|
|
88
90
|
import { Heading, HeadingProps, HeadingVariant } from "./primitives/heading.js";
|
|
89
91
|
import { PageShell, PageShellProps } from "./primitives/page-shell.js";
|
|
90
92
|
import { PageHeader, PageHeaderProps } from "./primitives/page-header.js";
|
|
91
|
-
import { StatusPill, StatusPillProps, StatusTone } from "./primitives/status-pill.js";
|
|
92
93
|
import { Alert, AlertDescription, AlertProps, AlertTitle, AlertTone } from "./primitives/alert.js";
|
|
93
94
|
import { Pagination, PaginationProps } from "./primitives/pagination.js";
|
|
95
|
+
import { StatusPill, StatusPillProps, StatusTone } from "./primitives/status-pill.js";
|
|
94
96
|
import { Tag, TagProps } from "./primitives/tag.js";
|
|
95
97
|
import { Chip, ChipProps } from "./primitives/chip.js";
|
|
96
98
|
import { Metric, MetricProps, MetricStrip, MetricStripProps } from "./primitives/metric-strip.js";
|
|
@@ -120,7 +122,6 @@ import { timeAgo } from "./utils/time-ago.js";
|
|
|
120
122
|
import { TOOL_CATEGORY_ICONS, getToolCategory, getToolDisplayMetadata, getToolErrorText } from "./utils/tool-display.js";
|
|
121
123
|
import { cn } from "./lib/utils.js";
|
|
122
124
|
import { focusField, focusFieldInvalid, focusFieldWithin, focusRing, focusRingInset } from "./lib/focus.js";
|
|
123
|
-
import { ixControl, ixControlAfter, ixControlSoft, ixField, ixRow, ixSelected, ixTabUnderline } from "./lib/interaction.js";
|
|
124
125
|
import "./utils.js";
|
|
125
126
|
import { CommandPreview, CommandPreviewProps } from "./tool-previews/command-preview.js";
|
|
126
127
|
import { WriteFilePreview, WriteFilePreviewProps } from "./tool-previews/write-file-preview.js";
|
package/dist/index.js
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { ixControl, ixControlAfter, ixControlSoft, ixField, ixRow, ixSelected, ixTabUnderline } from "./lib/interaction.js";
|
|
2
2
|
import { focusField, focusFieldInvalid, focusFieldWithin, focusRing, focusRingInset } from "./lib/focus.js";
|
|
3
3
|
import { cn } from "./lib/utils.js";
|
|
4
|
-
import {
|
|
4
|
+
import { buttonVariants } from "./presentation.js";
|
|
5
|
+
import { Button } from "./primitives/button.js";
|
|
5
6
|
import { Heading } from "./primitives/heading.js";
|
|
6
7
|
import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from "./primitives/card.js";
|
|
7
8
|
import { Dialog, DialogClose, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogOverlay, DialogPortal, DialogTitle, DialogTrigger } from "./primitives/dialog.js";
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { controlMotion } from "./motion.js";
|
|
1
2
|
//#region src/lib/control-presentation.d.ts
|
|
2
3
|
/**
|
|
3
4
|
* The control scale. Button, Input, Textarea and SelectTrigger take the same
|
|
@@ -7,4 +8,36 @@
|
|
|
7
8
|
* Brand stylesheet at the same pixels.
|
|
8
9
|
*/
|
|
9
10
|
export type ControlSize = "sm" | "md" | "lg";
|
|
10
|
-
|
|
11
|
+
export declare const controlHeight: {
|
|
12
|
+
readonly sm: "h-[var(--control-height-sm,2rem)]";
|
|
13
|
+
readonly md: "h-[var(--control-height,2.25rem)]";
|
|
14
|
+
readonly lg: "h-[var(--control-height-lg,2.75rem)]";
|
|
15
|
+
};
|
|
16
|
+
export declare const controlText: {
|
|
17
|
+
readonly sm: "text-[length:var(--control-text-sm,0.75rem)]";
|
|
18
|
+
readonly md: "text-[length:var(--control-text-md,0.875rem)]";
|
|
19
|
+
readonly lg: "text-[length:var(--control-text-lg,1rem)]";
|
|
20
|
+
};
|
|
21
|
+
export declare const controlRadius: {
|
|
22
|
+
readonly sm: "rounded-md";
|
|
23
|
+
readonly md: "rounded-lg";
|
|
24
|
+
readonly lg: "rounded-lg";
|
|
25
|
+
};
|
|
26
|
+
/**
|
|
27
|
+
* Field text never drops below 16px on a coarse pointer: iOS Safari zooms the
|
|
28
|
+
* page on focus for anything smaller.
|
|
29
|
+
*/
|
|
30
|
+
export declare const fieldTouchText = "pointer-coarse:text-base";
|
|
31
|
+
/** Field sizes: the control scale with field padding. */
|
|
32
|
+
export declare const fieldSizes: {
|
|
33
|
+
readonly sm: string;
|
|
34
|
+
readonly md: string;
|
|
35
|
+
readonly lg: string;
|
|
36
|
+
};
|
|
37
|
+
export declare const controlSizes: {
|
|
38
|
+
readonly compact: "h-[var(--control-height)] px-3 py-1 text-sm";
|
|
39
|
+
readonly touch: "h-11 min-h-11 min-w-11 px-4 py-2 text-base";
|
|
40
|
+
};
|
|
41
|
+
export declare const fieldPresentation: string;
|
|
42
|
+
//#endregion
|
|
43
|
+
export { controlMotion };
|
|
@@ -1,8 +1,8 @@
|
|
|
1
|
+
import { controlMotion } from "./motion.js";
|
|
1
2
|
import { ixField } from "./interaction.js";
|
|
2
3
|
import { focusField } from "./focus.js";
|
|
3
4
|
import { cn } from "./utils.js";
|
|
4
5
|
//#region src/lib/control-presentation.ts
|
|
5
|
-
const controlMotion = "duration-[var(--duration-fast)] ease-[var(--ease-standard)] motion-reduce:transition-none motion-reduce:duration-0";
|
|
6
6
|
const controlHeight = {
|
|
7
7
|
sm: "h-[var(--control-height-sm,2rem)]",
|
|
8
8
|
md: "h-[var(--control-height,2.25rem)]",
|
package/dist/lib/focus.d.ts
CHANGED
|
@@ -27,7 +27,7 @@
|
|
|
27
27
|
* replaces it through tailwind-merge; the focus border outranks it because
|
|
28
28
|
* Tailwind emits `focus:` after `hover:`.
|
|
29
29
|
*/
|
|
30
|
-
export declare const focusField = "border-border transition-
|
|
30
|
+
export declare const focusField = "border-border [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 hover:border-[var(--border-strong)] focus:outline-hidden focus:border-[var(--focus-border)] focus:ring-3 focus:ring-[var(--focus-halo)]";
|
|
31
31
|
/**
|
|
32
32
|
* The field treatment for a container that wraps a borderless field, such as a
|
|
33
33
|
* composer around a textarea and its buttons. It lights while a FIELD inside
|
|
@@ -45,7 +45,7 @@ export declare const focusField = "border-border transition-[border-color,box-sh
|
|
|
45
45
|
* own well fill, the `--field-border` hairline and the green hover edge. The
|
|
46
46
|
* borderless field inside stays transparent.
|
|
47
47
|
*/
|
|
48
|
-
export declare const focusFieldWithin = "focus-field-within transition-
|
|
48
|
+
export declare const focusFieldWithin = "focus-field-within [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-[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)] 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)]";
|
|
49
49
|
/** Error state for a field: a danger hairline and the same focus shape in danger. Compose after `focusField`. */
|
|
50
50
|
export declare const focusFieldInvalid = "border-[var(--surface-danger-border)] hover:border-[var(--surface-danger-border)] focus:border-[var(--focus-border-danger)] focus:ring-[var(--focus-halo-danger)]";
|
|
51
51
|
/** Keyboard-only focus for a control: a 1px focus line with the halo beyond it. */
|
package/dist/lib/focus.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { controlMotion } from "./motion.js";
|
|
1
2
|
import { ixField } from "./interaction.js";
|
|
2
3
|
//#region src/lib/focus.ts
|
|
3
4
|
/**
|
|
@@ -28,7 +29,7 @@ import { ixField } from "./interaction.js";
|
|
|
28
29
|
* replaces it through tailwind-merge; the focus border outranks it because
|
|
29
30
|
* Tailwind emits `focus:` after `hover:`.
|
|
30
31
|
*/
|
|
31
|
-
const focusField =
|
|
32
|
+
const focusField = `border-border ${controlMotion} hover:border-[var(--border-strong)] focus:outline-hidden focus:border-[var(--focus-border)] focus:ring-3 focus:ring-[var(--focus-halo)]`;
|
|
32
33
|
/**
|
|
33
34
|
* The field treatment for a container that wraps a borderless field, such as a
|
|
34
35
|
* composer around a textarea and its buttons. It lights while a FIELD inside
|
|
@@ -46,7 +47,7 @@ const focusField = "border-border transition-[border-color,box-shadow] duration-
|
|
|
46
47
|
* own well fill, the `--field-border` hairline and the green hover edge. The
|
|
47
48
|
* borderless field inside stays transparent.
|
|
48
49
|
*/
|
|
49
|
-
const focusFieldWithin = `focus-field-within
|
|
50
|
+
const focusFieldWithin = `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)]`;
|
|
50
51
|
/** Error state for a field: a danger hairline and the same focus shape in danger. Compose after `focusField`. */
|
|
51
52
|
const focusFieldInvalid = "border-[var(--surface-danger-border)] hover:border-[var(--surface-danger-border)] focus:border-[var(--focus-border-danger)] focus:ring-[var(--focus-halo-danger)]";
|
|
52
53
|
const focusRingBase = "focus-visible:outline-1 focus-visible:outline-[var(--focus-border)] focus-visible:ring-4 focus-visible:ring-[var(--focus-halo)]";
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
//#region src/lib/motion.d.ts
|
|
2
|
+
/** Brand motion roles contain duration AND easing; consume complete shorthands. */
|
|
3
|
+
export declare const controlMotion = "[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
|
+
//#endregion
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
//#region src/lib/motion.ts
|
|
2
|
+
/** Brand motion roles contain duration AND easing; consume complete shorthands. */
|
|
3
|
+
const controlMotion = "[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
|
+
/** Radix presence markers carry the scoped role; SurfaceMotion owns interruptible geometry. */
|
|
5
|
+
const surfaceMotion = "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!";
|
|
6
|
+
//#endregion
|
|
7
|
+
export { controlMotion, surfaceMotion };
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { Slot } from "@radix-ui/react-slot";
|
|
3
|
+
import * as React from "react";
|
|
4
|
+
import { jsx } from "react/jsx-runtime";
|
|
5
|
+
import { animate } from "motion";
|
|
6
|
+
//#region src/lib/surface-motion.tsx
|
|
7
|
+
/** Radix owns state, focus and presence; Motion only retargets the visible object. */
|
|
8
|
+
const SurfaceMotion = React.forwardRef(({ kind, side = "right", asChild, "data-state": state, ...props }, forwardedRef) => {
|
|
9
|
+
const element = React.useRef(null);
|
|
10
|
+
const initialized = React.useRef(false);
|
|
11
|
+
const controls = React.useRef(void 0);
|
|
12
|
+
const ref = React.useCallback((node) => {
|
|
13
|
+
element.current = node;
|
|
14
|
+
if (typeof forwardedRef === "function") {
|
|
15
|
+
const cleanup = forwardedRef(node);
|
|
16
|
+
if (typeof cleanup === "function") return () => {
|
|
17
|
+
element.current = null;
|
|
18
|
+
cleanup();
|
|
19
|
+
};
|
|
20
|
+
} else if (forwardedRef) forwardedRef.current = node;
|
|
21
|
+
}, [forwardedRef]);
|
|
22
|
+
React.useLayoutEffect(() => {
|
|
23
|
+
const node = element.current;
|
|
24
|
+
if (!node) return;
|
|
25
|
+
const preference = typeof matchMedia === "function" ? matchMedia("(prefers-reduced-motion: reduce)") : void 0;
|
|
26
|
+
const run = () => {
|
|
27
|
+
const closed = state === "closed";
|
|
28
|
+
const bounds = node.getBoundingClientRect();
|
|
29
|
+
const hidden = {
|
|
30
|
+
opacity: kind === "sheet" ? 1 : 0,
|
|
31
|
+
scale: kind === "dialog" || kind === "tooltip" ? .98 : 1,
|
|
32
|
+
x: kind === "sheet" ? side === "left" ? -bounds.width : side === "right" ? bounds.width : 0 : 0,
|
|
33
|
+
y: kind === "sheet" ? side === "top" ? -bounds.height : side === "bottom" ? bounds.height : 0 : 0
|
|
34
|
+
};
|
|
35
|
+
const target = closed ? hidden : {
|
|
36
|
+
opacity: 1,
|
|
37
|
+
scale: 1,
|
|
38
|
+
x: 0,
|
|
39
|
+
y: 0
|
|
40
|
+
};
|
|
41
|
+
const style = getComputedStyle(node);
|
|
42
|
+
const time = /^(\d+(?:\.\d+)?)(ms|s)$/.exec(style.animationDuration.split(",")[0].trim());
|
|
43
|
+
const seconds = time ? Number(time[1]) / (time[2] === "ms" ? 1e3 : 1) : 0;
|
|
44
|
+
const place = (value) => {
|
|
45
|
+
node.style.opacity = String(value.opacity);
|
|
46
|
+
node.style.transform = `translateX(${value.x}px) translateY(${value.y}px) scale(${value.scale})`;
|
|
47
|
+
};
|
|
48
|
+
if (preference?.matches || seconds <= 0 || style.animationName === "none") {
|
|
49
|
+
controls.current?.stop();
|
|
50
|
+
place(target);
|
|
51
|
+
if (initialized.current) controls.current = animate(node, target, { duration: 0 });
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
if (!initialized.current) {
|
|
55
|
+
place(hidden);
|
|
56
|
+
initialized.current = true;
|
|
57
|
+
}
|
|
58
|
+
const frequency = 12 / seconds;
|
|
59
|
+
controls.current = animate(node, target, {
|
|
60
|
+
type: "spring",
|
|
61
|
+
stiffness: frequency * frequency,
|
|
62
|
+
damping: 2 * frequency,
|
|
63
|
+
restDelta: .001,
|
|
64
|
+
restSpeed: .01
|
|
65
|
+
});
|
|
66
|
+
};
|
|
67
|
+
run();
|
|
68
|
+
preference?.addEventListener("change", run);
|
|
69
|
+
return () => preference?.removeEventListener("change", run);
|
|
70
|
+
}, [
|
|
71
|
+
state,
|
|
72
|
+
kind,
|
|
73
|
+
side
|
|
74
|
+
]);
|
|
75
|
+
React.useEffect(() => () => controls.current?.stop(), []);
|
|
76
|
+
return /* @__PURE__ */ jsx(asChild ? Slot : "div", {
|
|
77
|
+
...props,
|
|
78
|
+
"data-state": state,
|
|
79
|
+
ref
|
|
80
|
+
});
|
|
81
|
+
});
|
|
82
|
+
SurfaceMotion.displayName = "SurfaceMotion";
|
|
83
|
+
//#endregion
|
|
84
|
+
export { SurfaceMotion };
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import { controlMotion } from "./lib/motion.js";
|
|
2
|
+
import { controlHeight, controlRadius, controlSizes, controlText, fieldPresentation, fieldSizes, fieldTouchText } from "./lib/control-presentation.js";
|
|
3
|
+
import { ixControlSoft, ixSelected, ixTabUnderline } from "./lib/interaction.js";
|
|
4
|
+
//#region src/presentation.d.ts
|
|
5
|
+
export declare const buttonVariants: (props?: ({
|
|
6
|
+
variant?: "default" | "destructive" | "ghost" | "link" | "outline" | "sandbox" | "secondary" | null | undefined;
|
|
7
|
+
size?: "compact" | "default" | "display" | "icon" | "icon-lg" | "icon-sm" | "lg" | "md" | "sm" | "touch" | "xl" | null | undefined;
|
|
8
|
+
} & import("class-variance-authority/types").ClassProp) | undefined) => string;
|
|
9
|
+
export declare const inputVariants: (props?: ({
|
|
10
|
+
variant?: "default" | "error" | "sandbox" | null | undefined;
|
|
11
|
+
size?: "compact" | "default" | "lg" | "md" | "sm" | "touch" | null | undefined;
|
|
12
|
+
} & import("class-variance-authority/types").ClassProp) | undefined) => string;
|
|
13
|
+
export declare const tablePresentation: {
|
|
14
|
+
readonly root: "w-full caption-bottom text-sm";
|
|
15
|
+
readonly wrapper: "relative w-full overflow-auto";
|
|
16
|
+
readonly head: "[&_tr]:border-b";
|
|
17
|
+
readonly body: "[&_tr:last-child]:border-0";
|
|
18
|
+
readonly footer: "border-t bg-card font-medium [&>tr]:last:border-b-0";
|
|
19
|
+
readonly row: "border-border border-b [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 hover:bg-muted/50 data-[state=selected]:bg-muted/50";
|
|
20
|
+
readonly columnHeader: "h-12 px-4 text-left align-middle font-medium text-muted-foreground [&:has([role=checkbox])]:pr-0";
|
|
21
|
+
readonly cell: "p-4 align-middle [&:has([role=checkbox])]:pr-0";
|
|
22
|
+
readonly caption: "mt-4 text-muted-foreground text-sm";
|
|
23
|
+
};
|
|
24
|
+
export declare const emptyStatePresentation: {
|
|
25
|
+
readonly root: "flex flex-col items-center justify-center px-4 py-16 text-center";
|
|
26
|
+
readonly icon: "mb-4 rounded-full bg-muted p-4 text-muted-foreground";
|
|
27
|
+
readonly title: "font-semibold text-lg";
|
|
28
|
+
readonly description: "mt-2 max-w-sm text-muted-foreground text-sm";
|
|
29
|
+
readonly action: "mt-6";
|
|
30
|
+
};
|
|
31
|
+
export declare const metricPresentation: {
|
|
32
|
+
readonly strip: "grid min-w-0 rounded-[var(--radius-lg)] border border-border bg-card shadow-[var(--shadow-card)]";
|
|
33
|
+
readonly item: "min-w-0 border-border p-4 sm:p-5";
|
|
34
|
+
readonly value: "mt-1 whitespace-normal [overflow-wrap:anywhere] font-semibold text-xl tabular-nums tracking-tight lg:text-2xl";
|
|
35
|
+
readonly label: "flex min-w-0 flex-wrap items-center gap-2 text-muted-foreground text-sm";
|
|
36
|
+
readonly labelText: "min-w-0 whitespace-normal [overflow-wrap:anywhere]";
|
|
37
|
+
readonly hint: "mt-0.5 whitespace-normal [overflow-wrap:anywhere] text-[var(--text-dim)] text-xs";
|
|
38
|
+
};
|
|
39
|
+
export declare const metricLayouts: {
|
|
40
|
+
readonly 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";
|
|
41
|
+
readonly 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";
|
|
42
|
+
readonly 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";
|
|
43
|
+
readonly 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";
|
|
44
|
+
};
|
|
45
|
+
export declare const dialogPresentation: {
|
|
46
|
+
readonly overlay: "fixed inset-0 z-50 bg-black/40";
|
|
47
|
+
readonly transition: "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!";
|
|
48
|
+
readonly position: "fixed top-[50%] left-[50%] z-50 grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%]";
|
|
49
|
+
readonly surface: "gap-4 rounded-lg border bg-card p-6 shadow-lg";
|
|
50
|
+
readonly close: "absolute top-4 right-4 rounded-sm opacity-70 [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 hover:opacity-100 focus-visible:opacity-100 disabled:pointer-events-none data-[state=open]:bg-accent data-[state=open]:text-muted-foreground";
|
|
51
|
+
readonly header: "flex flex-col space-y-1.5 text-center sm:text-left";
|
|
52
|
+
readonly footer: "flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2";
|
|
53
|
+
readonly title: "font-semibold text-lg leading-none tracking-tight";
|
|
54
|
+
readonly description: "text-muted-foreground text-sm";
|
|
55
|
+
};
|
|
56
|
+
/** Native-dialog adapter for Hono/static markup. Radix remains the React Dialog/Sheet owner. */
|
|
57
|
+
export declare function bindDialog(dialog: HTMLDialogElement, trigger: HTMLElement): () => void;
|
|
58
|
+
export declare const labelVariants: (props?: import("class-variance-authority/types").ClassProp | undefined) => string;
|
|
59
|
+
export declare const helpPresentation: {
|
|
60
|
+
root: string;
|
|
61
|
+
error: string;
|
|
62
|
+
hint: string;
|
|
63
|
+
};
|
|
64
|
+
export declare const cardPresentation: {
|
|
65
|
+
readonly root: "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";
|
|
66
|
+
readonly padding: "p-4";
|
|
67
|
+
readonly variants: {
|
|
68
|
+
readonly default: "bg-card border-border shadow-[var(--edge-highlight),var(--shadow-card)]";
|
|
69
|
+
readonly elevated: "bg-muted/50 border-border shadow-[var(--shadow-card)]";
|
|
70
|
+
readonly glass: "bg-card/80 backdrop-blur-xl border-border shadow-[var(--shadow-card)]";
|
|
71
|
+
readonly sandbox: "bg-muted/50 border-primary/20 shadow-[var(--shadow-accent)]";
|
|
72
|
+
};
|
|
73
|
+
};
|
|
74
|
+
/** Canonical segmented, pill and underline navigation presentation. */
|
|
75
|
+
export declare const tabsPresentation: {
|
|
76
|
+
readonly list: {
|
|
77
|
+
readonly default: "inline-flex h-10 items-center justify-center rounded-lg bg-muted p-1 text-muted-foreground";
|
|
78
|
+
readonly pills: "inline-flex items-center gap-2";
|
|
79
|
+
readonly underline: "inline-flex items-center gap-4 border-b border-border";
|
|
80
|
+
};
|
|
81
|
+
readonly trigger: {
|
|
82
|
+
readonly default: "inline-flex items-center justify-center whitespace-nowrap rounded-md px-3 py-1.5 text-sm 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 hover:bg-[var(--ix-hover-bg)] active:bg-[var(--ix-sel-bg)] aria-pressed:bg-[var(--ix-sel-bg)] aria-pressed:text-[var(--accent-text)] aria-pressed:shadow-[inset_0_0_0_1px_var(--ix-sel-ring)] aria-checked:bg-[var(--ix-sel-bg)] aria-checked:text-[var(--accent-text)] aria-checked:shadow-[inset_0_0_0_1px_var(--ix-sel-ring)] aria-[current=page]:bg-[var(--ix-sel-bg)] aria-[current=page]:text-[var(--accent-text)] aria-[current=page]:shadow-[inset_0_0_0_1px_var(--ix-sel-ring)] data-[state=on]:bg-[var(--ix-sel-bg)] data-[state=on]:shadow-[inset_0_0_0_1px_var(--ix-sel-ring)] data-[state=active]:bg-[var(--ix-sel-bg)] data-[state=active]:text-[var(--accent-text)] data-[state=active]:shadow-[inset_0_0_0_1px_var(--ix-sel-ring)]";
|
|
83
|
+
readonly pills: "inline-flex items-center justify-center whitespace-nowrap rounded-full px-4 py-2 text-sm 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 data-[state=inactive]:text-muted-foreground hover:bg-[var(--ix-hover-bg)] active:bg-[var(--ix-sel-bg)] aria-pressed:bg-[var(--ix-sel-bg)] aria-pressed:text-[var(--accent-text)] aria-pressed:shadow-[inset_0_0_0_1px_var(--ix-sel-ring)] aria-checked:bg-[var(--ix-sel-bg)] aria-checked:text-[var(--accent-text)] aria-checked:shadow-[inset_0_0_0_1px_var(--ix-sel-ring)] aria-[current=page]:bg-[var(--ix-sel-bg)] aria-[current=page]:text-[var(--accent-text)] aria-[current=page]:shadow-[inset_0_0_0_1px_var(--ix-sel-ring)] data-[state=on]:bg-[var(--ix-sel-bg)] data-[state=on]:shadow-[inset_0_0_0_1px_var(--ix-sel-ring)] data-[state=active]:bg-[var(--ix-sel-bg)] data-[state=active]:text-[var(--accent-text)] data-[state=active]:shadow-[inset_0_0_0_1px_var(--ix-sel-ring)]";
|
|
84
|
+
readonly underline: "inline-flex items-center justify-center whitespace-nowrap rounded-t-md pb-3 text-sm 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 border-b-2 border-transparent data-[state=inactive]:text-muted-foreground hover:shadow-[inset_0_-2px_0_hsl(var(--hsl-ring)/0.4)] data-[state=active]:bg-[hsl(var(--hsl-ring)/0.07)] data-[state=active]:text-[var(--accent-text)] data-[state=active]:shadow-[inset_0_-2px_0_var(--ix-sel-ring)] aria-checked:bg-[hsl(var(--hsl-ring)/0.07)] aria-checked:text-[var(--accent-text)] aria-checked:shadow-[inset_0_-2px_0_var(--ix-sel-ring)]";
|
|
85
|
+
};
|
|
86
|
+
};
|
|
87
|
+
//#endregion
|
|
88
|
+
export { controlHeight, controlMotion, controlRadius, controlSizes, controlText, fieldPresentation, fieldSizes, fieldTouchText, ixControlSoft, ixSelected, ixTabUnderline };
|
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
import { controlMotion, surfaceMotion } from "./lib/motion.js";
|
|
2
|
+
import { ixControl, ixControlSoft, ixSelected, ixTabUnderline } from "./lib/interaction.js";
|
|
3
|
+
import { focusFieldInvalid, focusRing } from "./lib/focus.js";
|
|
4
|
+
import { cn } from "./lib/utils.js";
|
|
5
|
+
import { controlHeight, controlRadius, controlSizes, controlSquare, controlText, fieldPresentation, fieldSizes, fieldTouchText } from "./lib/control-presentation.js";
|
|
6
|
+
import { cva } from "class-variance-authority";
|
|
7
|
+
//#region src/presentation.ts
|
|
8
|
+
/** React-free presentation. Consumers scan this published source with Tailwind. */
|
|
9
|
+
const buttonVariants = cva(`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`, {
|
|
10
|
+
variants: {
|
|
11
|
+
variant: {
|
|
12
|
+
default: `bg-primary text-primary-foreground hover:bg-primary/90 active:scale-[0.97] ${ixControl}`,
|
|
13
|
+
destructive: `bg-destructive text-destructive-foreground hover:bg-destructive/90 active:scale-[0.97] ${ixControl}`,
|
|
14
|
+
outline: `border border-border bg-card hover:bg-muted active:scale-[0.97] text-foreground ${ixControl}`,
|
|
15
|
+
secondary: `bg-muted border border-border text-foreground hover:bg-muted/80 active:scale-[0.97] ${ixControl}`,
|
|
16
|
+
ghost: `hover:text-foreground text-muted-foreground border border-transparent ${ixControlSoft}`,
|
|
17
|
+
link: `text-[var(--accent-text)] underline-offset-4 hover:underline ${ixControlSoft}`,
|
|
18
|
+
sandbox: `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}`
|
|
19
|
+
},
|
|
20
|
+
size: {
|
|
21
|
+
default: `${controlHeight.md} ${controlText.md} ${controlRadius.md} px-4 py-2`,
|
|
22
|
+
md: `${controlHeight.md} ${controlText.md} ${controlRadius.md} px-4 py-2`,
|
|
23
|
+
sm: `${controlHeight.sm} ${controlText.sm} ${controlRadius.sm} px-3`,
|
|
24
|
+
lg: `${controlHeight.lg} ${controlText.lg} ${controlRadius.lg} px-7`,
|
|
25
|
+
display: "tangle-control-display",
|
|
26
|
+
xl: "h-13 rounded-xl px-9 text-base",
|
|
27
|
+
icon: `${controlSquare.md} ${controlText.md} ${controlRadius.md}`,
|
|
28
|
+
"icon-sm": `${controlSquare.sm} ${controlText.sm} ${controlRadius.sm}`,
|
|
29
|
+
"icon-lg": `${controlSquare.lg} ${controlText.lg} ${controlRadius.lg}`,
|
|
30
|
+
...controlSizes
|
|
31
|
+
}
|
|
32
|
+
},
|
|
33
|
+
defaultVariants: {
|
|
34
|
+
variant: "default",
|
|
35
|
+
size: "default"
|
|
36
|
+
}
|
|
37
|
+
});
|
|
38
|
+
const inputVariants = cva(cn("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", fieldPresentation, fieldTouchText), {
|
|
39
|
+
variants: {
|
|
40
|
+
variant: {
|
|
41
|
+
default: "",
|
|
42
|
+
sandbox: "",
|
|
43
|
+
error: focusFieldInvalid
|
|
44
|
+
},
|
|
45
|
+
size: {
|
|
46
|
+
default: fieldSizes.md,
|
|
47
|
+
md: fieldSizes.md,
|
|
48
|
+
sm: fieldSizes.sm,
|
|
49
|
+
lg: fieldSizes.lg,
|
|
50
|
+
compact: cn(fieldSizes.md, controlSizes.compact),
|
|
51
|
+
touch: cn(fieldSizes.md, controlSizes.touch)
|
|
52
|
+
}
|
|
53
|
+
},
|
|
54
|
+
defaultVariants: {
|
|
55
|
+
variant: "default",
|
|
56
|
+
size: "default"
|
|
57
|
+
}
|
|
58
|
+
});
|
|
59
|
+
const tablePresentation = {
|
|
60
|
+
root: "w-full caption-bottom text-sm",
|
|
61
|
+
wrapper: "relative w-full overflow-auto",
|
|
62
|
+
head: "[&_tr]:border-b",
|
|
63
|
+
body: "[&_tr:last-child]:border-0",
|
|
64
|
+
footer: "border-t bg-card font-medium [&>tr]:last:border-b-0",
|
|
65
|
+
row: `border-border border-b ${controlMotion} hover:bg-muted/50 data-[state=selected]:bg-muted/50`,
|
|
66
|
+
columnHeader: "h-12 px-4 text-left align-middle font-medium text-muted-foreground [&:has([role=checkbox])]:pr-0",
|
|
67
|
+
cell: "p-4 align-middle [&:has([role=checkbox])]:pr-0",
|
|
68
|
+
caption: "mt-4 text-muted-foreground text-sm"
|
|
69
|
+
};
|
|
70
|
+
const emptyStatePresentation = {
|
|
71
|
+
root: "flex flex-col items-center justify-center px-4 py-16 text-center",
|
|
72
|
+
icon: "mb-4 rounded-full bg-muted p-4 text-muted-foreground",
|
|
73
|
+
title: "font-semibold text-lg",
|
|
74
|
+
description: "mt-2 max-w-sm text-muted-foreground text-sm",
|
|
75
|
+
action: "mt-6"
|
|
76
|
+
};
|
|
77
|
+
const metricPresentation = {
|
|
78
|
+
strip: "grid min-w-0 rounded-[var(--radius-lg)] border border-border bg-card shadow-[var(--shadow-card)]",
|
|
79
|
+
item: "min-w-0 border-border p-4 sm:p-5",
|
|
80
|
+
value: "mt-1 whitespace-normal [overflow-wrap:anywhere] font-semibold text-xl tabular-nums tracking-tight lg:text-2xl",
|
|
81
|
+
label: "flex min-w-0 flex-wrap items-center gap-2 text-muted-foreground text-sm",
|
|
82
|
+
labelText: "min-w-0 whitespace-normal [overflow-wrap:anywhere]",
|
|
83
|
+
hint: "mt-0.5 whitespace-normal [overflow-wrap:anywhere] text-[var(--text-dim)] text-xs"
|
|
84
|
+
};
|
|
85
|
+
const metricLayouts = {
|
|
86
|
+
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",
|
|
87
|
+
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",
|
|
88
|
+
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",
|
|
89
|
+
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"
|
|
90
|
+
};
|
|
91
|
+
const dialogPresentation = {
|
|
92
|
+
overlay: "fixed inset-0 z-50 bg-black/40",
|
|
93
|
+
transition: surfaceMotion,
|
|
94
|
+
position: "fixed top-[50%] left-[50%] z-50 grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%]",
|
|
95
|
+
surface: "gap-4 rounded-lg border bg-card p-6 shadow-lg",
|
|
96
|
+
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`,
|
|
97
|
+
header: "flex flex-col space-y-1.5 text-center sm:text-left",
|
|
98
|
+
footer: "flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2",
|
|
99
|
+
title: "font-semibold text-lg leading-none tracking-tight",
|
|
100
|
+
description: "text-muted-foreground text-sm"
|
|
101
|
+
};
|
|
102
|
+
/** Native-dialog adapter for Hono/static markup. Radix remains the React Dialog/Sheet owner. */
|
|
103
|
+
function bindDialog(dialog, trigger) {
|
|
104
|
+
const sync = () => trigger.setAttribute("aria-expanded", String(dialog.open));
|
|
105
|
+
const show = () => {
|
|
106
|
+
if (!dialog.open) dialog.showModal();
|
|
107
|
+
sync();
|
|
108
|
+
};
|
|
109
|
+
const close = () => {
|
|
110
|
+
sync();
|
|
111
|
+
trigger.focus();
|
|
112
|
+
};
|
|
113
|
+
const click = (event) => {
|
|
114
|
+
const target = event.target;
|
|
115
|
+
if (target instanceof Element && target.closest("[data-dialog-close]")) dialog.close();
|
|
116
|
+
};
|
|
117
|
+
const key = (event) => {
|
|
118
|
+
if (event.key !== "Tab" || !dialog.open) return;
|
|
119
|
+
const candidates = [...dialog.querySelectorAll("*")].filter((element) => (element.tabIndex >= 0 || element.isContentEditable && !element.hasAttribute("tabindex")) && !element.matches(":disabled") && !element.closest("[inert]") && element.getClientRects().length > 0 && getComputedStyle(element).visibility === "visible");
|
|
120
|
+
const radio = (element) => element instanceof HTMLInputElement && element.type === "radio" && !!element.name;
|
|
121
|
+
const sameRadioGroup = (a, b) => radio(a) && radio(b) && a.name === b.name && a.form === b.form && a.getRootNode() === b.getRootNode();
|
|
122
|
+
const stops = candidates.filter((element) => {
|
|
123
|
+
if (!radio(element)) return true;
|
|
124
|
+
const group = candidates.filter((candidate) => sameRadioGroup(element, candidate));
|
|
125
|
+
return element === (group.find((candidate) => candidate.checked) ?? (event.shiftKey ? group.at(-1) : group[0]));
|
|
126
|
+
}).sort((a, b) => (a.tabIndex > 0 ? a.tabIndex : Infinity) - (b.tabIndex > 0 ? b.tabIndex : Infinity));
|
|
127
|
+
const first = stops[0], last = stops.at(-1), active = document.activeElement;
|
|
128
|
+
if (!first) {
|
|
129
|
+
event.preventDefault();
|
|
130
|
+
dialog.focus();
|
|
131
|
+
return;
|
|
132
|
+
}
|
|
133
|
+
const atBoundary = (boundary) => active === boundary || active instanceof Element && sameRadioGroup(active, boundary);
|
|
134
|
+
if (active === dialog || (event.shiftKey ? atBoundary(first) : atBoundary(last))) {
|
|
135
|
+
event.preventDefault();
|
|
136
|
+
(event.shiftKey ? last : first).focus();
|
|
137
|
+
}
|
|
138
|
+
};
|
|
139
|
+
trigger.setAttribute("aria-haspopup", "dialog");
|
|
140
|
+
if (dialog.id) trigger.setAttribute("aria-controls", dialog.id);
|
|
141
|
+
sync();
|
|
142
|
+
trigger.addEventListener("click", show);
|
|
143
|
+
dialog.addEventListener("close", close);
|
|
144
|
+
dialog.addEventListener("click", click);
|
|
145
|
+
dialog.addEventListener("keydown", key);
|
|
146
|
+
return () => {
|
|
147
|
+
trigger.removeEventListener("click", show);
|
|
148
|
+
dialog.removeEventListener("close", close);
|
|
149
|
+
dialog.removeEventListener("click", click);
|
|
150
|
+
dialog.removeEventListener("keydown", key);
|
|
151
|
+
};
|
|
152
|
+
}
|
|
153
|
+
const labelVariants = cva("font-medium text-[length:var(--font-size-label,0.875rem)] leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70");
|
|
154
|
+
const helpPresentation = {
|
|
155
|
+
root: "text-[length:var(--font-size-help,0.75rem)] leading-4",
|
|
156
|
+
error: "font-medium text-[var(--surface-danger-text)]",
|
|
157
|
+
hint: "text-[var(--text-dim)]"
|
|
158
|
+
};
|
|
159
|
+
const cardPresentation = {
|
|
160
|
+
root: `rounded-[var(--radius-lg)] border text-card-foreground ${controlMotion}`,
|
|
161
|
+
padding: "p-4",
|
|
162
|
+
variants: {
|
|
163
|
+
default: "bg-card border-border shadow-[var(--edge-highlight),var(--shadow-card)]",
|
|
164
|
+
elevated: "bg-muted/50 border-border shadow-[var(--shadow-card)]",
|
|
165
|
+
glass: "bg-card/80 backdrop-blur-xl border-border shadow-[var(--shadow-card)]",
|
|
166
|
+
sandbox: "bg-muted/50 border-primary/20 shadow-[var(--shadow-accent)]"
|
|
167
|
+
}
|
|
168
|
+
};
|
|
169
|
+
/** Canonical segmented, pill and underline navigation presentation. */
|
|
170
|
+
const tabsPresentation = {
|
|
171
|
+
list: {
|
|
172
|
+
default: "inline-flex h-10 items-center justify-center rounded-lg bg-muted p-1 text-muted-foreground",
|
|
173
|
+
pills: "inline-flex items-center gap-2",
|
|
174
|
+
underline: "inline-flex items-center gap-4 border-b border-border"
|
|
175
|
+
},
|
|
176
|
+
trigger: {
|
|
177
|
+
default: `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}`,
|
|
178
|
+
pills: `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}`,
|
|
179
|
+
underline: `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}`
|
|
180
|
+
}
|
|
181
|
+
};
|
|
182
|
+
//#endregion
|
|
183
|
+
export { bindDialog, buttonVariants, cardPresentation, controlHeight, controlMotion, controlRadius, controlSizes, controlText, dialogPresentation, emptyStatePresentation, fieldPresentation, fieldSizes, fieldTouchText, helpPresentation, inputVariants, ixControlSoft, ixSelected, ixTabUnderline, labelVariants, metricLayouts, metricPresentation, tablePresentation, tabsPresentation };
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
import { cn } from "../lib/utils.js";
|
|
3
|
-
import { buttonVariants } from "
|
|
3
|
+
import { buttonVariants } from "../presentation.js";
|
|
4
4
|
import * as React from "react";
|
|
5
5
|
import { jsx, jsxs } from "react/jsx-runtime";
|
|
6
6
|
import * as AlertDialogPrimitive from "@radix-ui/react-alert-dialog";
|
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import { StatusTone } from "./status-pill.js";
|
|
2
1
|
import * as React from "react";
|
|
3
2
|
//#region src/primitives/alert.d.ts
|
|
4
3
|
/**
|
|
@@ -13,7 +12,7 @@ import * as React from "react";
|
|
|
13
12
|
* `danger` and `warning` announce themselves (`role="alert"`); the quieter
|
|
14
13
|
* tones are `role="status"` so a screen reader is not interrupted by a note.
|
|
15
14
|
*/
|
|
16
|
-
export type AlertTone =
|
|
15
|
+
export type AlertTone = "neutral" | "info" | "success" | "warning" | "danger";
|
|
17
16
|
export interface AlertProps extends React.HTMLAttributes<HTMLDivElement> {
|
|
18
17
|
tone?: AlertTone;
|
|
19
18
|
/** A leading icon. It takes the tone's colour and sits beside the title. */
|
|
@@ -1,13 +1,10 @@
|
|
|
1
|
+
import { buttonVariants } from "../presentation.js";
|
|
1
2
|
import * as React from "react";
|
|
2
3
|
//#region src/primitives/button.d.ts
|
|
3
|
-
declare const buttonVariants: (props?: ({
|
|
4
|
-
variant?: "default" | "destructive" | "ghost" | "link" | "outline" | "sandbox" | "secondary" | null | undefined;
|
|
5
|
-
size?: "compact" | "default" | "icon" | "icon-lg" | "icon-sm" | "lg" | "md" | "sm" | "touch" | "xl" | null | undefined;
|
|
6
|
-
} & import("class-variance-authority/types").ClassProp) | undefined) => string;
|
|
7
4
|
export interface ButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
|
|
8
5
|
variant?: "default" | "destructive" | "outline" | "secondary" | "ghost" | "link" | "sandbox" | "bare" | null;
|
|
9
6
|
/** sm 32px, default/md 36px (`--control-height`), lg 44px; icon sizes are squares on the same heights. */
|
|
10
|
-
size?: "default" | "md" | "sm" | "lg" | "xl" | "icon" | "icon-sm" | "icon-lg" | "compact" | "touch" | null;
|
|
7
|
+
size?: "default" | "md" | "sm" | "lg" | "xl" | "display" | "icon" | "icon-sm" | "icon-lg" | "compact" | "touch" | null;
|
|
11
8
|
asChild?: boolean;
|
|
12
9
|
loading?: boolean;
|
|
13
10
|
children?: React.ReactNode;
|