@kinetixui/ui 0.23.2 → 0.24.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/README.md +134 -0
- package/dist/index.d.ts +212 -31
- package/dist/index.js +1466 -1118
- package/dist/kx-src-hash.json +3 -0
- package/package.json +7 -7
- package/src/components/accordion.tsx +1 -1
- package/src/components/alert-dialog.tsx +7 -5
- package/src/components/avatar.tsx +4 -1
- package/src/components/banner.tsx +9 -3
- package/src/components/breadcrumb.tsx +1 -1
- package/src/components/button.tsx +1 -1
- package/src/components/card.tsx +77 -6
- package/src/components/checkbox.tsx +42 -8
- package/src/components/collapsible.tsx +41 -1
- package/src/components/command.tsx +2 -2
- package/src/components/context-menu.tsx +21 -13
- package/src/components/dialog.tsx +5 -4
- package/src/components/direction-provider.tsx +85 -1
- package/src/components/drawer.tsx +62 -18
- package/src/components/dropdown-menu.tsx +12 -4
- package/src/components/file-upload.tsx +1 -1
- package/src/components/hover-card.tsx +1 -1
- package/src/components/inform.tsx +9 -3
- package/src/components/input-group.tsx +27 -5
- package/src/components/input-otp.tsx +48 -5
- package/src/components/input.tsx +20 -3
- package/src/components/json-viewer.tsx +1 -1
- package/src/components/menubar.tsx +21 -15
- package/src/components/modal.tsx +3 -2
- package/src/components/multi-select.tsx +14 -5
- package/src/components/native-select.tsx +10 -2
- package/src/components/navigation-bar.tsx +1 -1
- package/src/components/navigation-menu.tsx +6 -6
- package/src/components/number-input.tsx +28 -10
- package/src/components/pagination.tsx +2 -2
- package/src/components/popover.tsx +29 -5
- package/src/components/radio-group.tsx +23 -8
- package/src/components/segmented-control.tsx +20 -4
- package/src/components/select.tsx +25 -8
- package/src/components/sheet.tsx +4 -3
- package/src/components/spinner.tsx +1 -1
- package/src/components/switch.tsx +31 -8
- package/src/components/table.tsx +121 -8
- package/src/components/tabs.tsx +29 -4
- package/src/components/tag.tsx +1 -1
- package/src/components/textarea.tsx +20 -3
- package/src/components/tooltip.tsx +14 -4
- package/src/components/tour.tsx +22 -7
- package/src/components/tree-view.tsx +1 -1
- package/src/components-behavior.test.tsx +74 -0
- package/src/components-direction-portal.test.tsx +482 -0
- package/src/components-form.test.tsx +435 -0
- package/src/components-icons.test.tsx +197 -0
- package/src/components-keyboard.test.tsx +542 -2
- package/src/components-overlay.test.tsx +381 -0
- package/src/components-rtl.test.tsx +258 -0
- package/src/components-selection.test.tsx +207 -0
- package/src/components-table-scroll.test.tsx +363 -0
- package/src/index.ts +2 -0
- package/src/navigation-menu-focus.test.tsx +35 -0
- package/src/stories/Card.stories.tsx +81 -1
- package/src/stories/Checkbox.stories.tsx +23 -1
- package/src/stories/ContextMenu.stories.tsx +1 -1
- package/src/stories/Input.stories.tsx +34 -0
- package/src/stories/InputGroup.stories.tsx +96 -2
- package/src/stories/InputOTP.stories.tsx +24 -1
- package/src/stories/MultiSelect.stories.tsx +25 -1
- package/src/stories/NativeSelect.stories.tsx +24 -1
- package/src/stories/NumberInput.stories.tsx +20 -1
- package/src/stories/Progress.stories.tsx +1 -1
- package/src/stories/RadioGroup.stories.tsx +33 -1
- package/src/stories/SegmentedControl.stories.tsx +21 -1
- package/src/stories/Select.stories.tsx +28 -1
- package/src/stories/Slider.stories.tsx +1 -1
- package/src/stories/Switch.stories.tsx +37 -1
- package/src/stories/Table.stories.tsx +35 -1
- package/src/stories/Tabs.stories.tsx +27 -2
- package/src/stories/Textarea.stories.tsx +34 -0
- package/src/usage/icons.tsx +60 -0
- package/tailwind.config.ts +118 -22
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
Source of truth: apps/web/src/registry/demos.tsx */
|
|
3
3
|
import * as React from "react";
|
|
4
4
|
import type { Meta, StoryObj } from "@storybook/react-vite";
|
|
5
|
-
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@kinetixui/ui";
|
|
5
|
+
import { Table, TableBody, TableCaption, TableCell, TableHead, TableHeader, TableRow } from "@kinetixui/ui";
|
|
6
6
|
|
|
7
7
|
const Demo = () => (
|
|
8
8
|
<Table>
|
|
@@ -40,3 +40,37 @@ const meta = {
|
|
|
40
40
|
export default meta;
|
|
41
41
|
|
|
42
42
|
export const Default: StoryObj<typeof meta> = { render: () => <Demo /> };
|
|
43
|
+
|
|
44
|
+
export const Overflowing: StoryObj<typeof meta> = { render: () => (
|
|
45
|
+
<div className="w-[22rem] max-w-full">
|
|
46
|
+
<Table>
|
|
47
|
+
<TableCaption>Settlement ledger, March</TableCaption>
|
|
48
|
+
<TableHeader>
|
|
49
|
+
<TableRow>
|
|
50
|
+
{["Reference", "Counterparty", "Instrument", "Booked", "Value date", "Currency", "Notional", "Status", "Desk"].map(
|
|
51
|
+
(h) => (
|
|
52
|
+
<TableHead key={h} className="whitespace-nowrap">
|
|
53
|
+
{h}
|
|
54
|
+
</TableHead>
|
|
55
|
+
),
|
|
56
|
+
)}
|
|
57
|
+
</TableRow>
|
|
58
|
+
</TableHeader>
|
|
59
|
+
<TableBody>
|
|
60
|
+
{[
|
|
61
|
+
["STL-4471", "Northwind Trading", "FX forward", "02 Mar", "04 Mar", "EUR", "1,250,000", "Settled", "Rates"],
|
|
62
|
+
["STL-4472", "Halvorsen Capital", "Interest swap", "03 Mar", "05 Mar", "USD", "4,000,000", "Pending", "Rates"],
|
|
63
|
+
["STL-4473", "Keystone Mutual", "Equity basket", "03 Mar", "06 Mar", "GBP", "860,500", "Failed", "Cash"],
|
|
64
|
+
].map((row) => (
|
|
65
|
+
<TableRow key={row[0]}>
|
|
66
|
+
{row.map((cell) => (
|
|
67
|
+
<TableCell key={cell} className="whitespace-nowrap">
|
|
68
|
+
{cell}
|
|
69
|
+
</TableCell>
|
|
70
|
+
))}
|
|
71
|
+
</TableRow>
|
|
72
|
+
))}
|
|
73
|
+
</TableBody>
|
|
74
|
+
</Table>
|
|
75
|
+
</div>
|
|
76
|
+
) };
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
Source of truth: apps/web/src/registry/demos.tsx · controls: scripts/gen-stories.mjs CONTROLS */
|
|
3
3
|
import * as React from "react";
|
|
4
4
|
import type { Meta, StoryObj } from "@storybook/react-vite";
|
|
5
|
-
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@kinetixui/ui";
|
|
5
|
+
import { Card, CardContent, Tabs, TabsContent, TabsList, TabsTrigger } from "@kinetixui/ui";
|
|
6
6
|
|
|
7
7
|
const Demo = () => (
|
|
8
8
|
<Tabs defaultValue="account" className="w-[360px]">
|
|
@@ -25,7 +25,7 @@ const meta = {
|
|
|
25
25
|
args: { defaultValue: "account" },
|
|
26
26
|
argTypes: { defaultValue: { control: "inline-radio", options: ["account", "password"] } },
|
|
27
27
|
parameters: {
|
|
28
|
-
layout: "
|
|
28
|
+
layout: "padded",
|
|
29
29
|
docs: { source: { code: "<Tabs defaultValue=\"account\">\\n <TabsList>\\n <TabsTrigger value=\"account\">Account</TabsTrigger>\\n <TabsTrigger value=\"password\">Password</TabsTrigger>\\n </TabsList>\\n <TabsContent value=\"account\">…</TabsContent>\\n</Tabs>", language: "tsx" } },
|
|
30
30
|
},
|
|
31
31
|
} satisfies Meta;
|
|
@@ -48,3 +48,28 @@ export const Playground: StoryObj<typeof meta> = { render: (args) => (
|
|
|
48
48
|
) };
|
|
49
49
|
|
|
50
50
|
export const Default: StoryObj<typeof meta> = { render: () => <Demo /> };
|
|
51
|
+
|
|
52
|
+
export const States: StoryObj<typeof meta> = { render: () => (
|
|
53
|
+
<div className="grid w-[26rem] max-w-full gap-6">
|
|
54
|
+
{(["page", "card"] as const).map((where) => {
|
|
55
|
+
const tabs = (
|
|
56
|
+
<Tabs defaultValue="overview">
|
|
57
|
+
<TabsList data-kx-case={where} aria-label={where === "page" ? "Project" : "Workspace"}>
|
|
58
|
+
<TabsTrigger value="overview" data-kx-case="selected">Overview</TabsTrigger>
|
|
59
|
+
<TabsTrigger value="activity" data-kx-case="unselected">Activity</TabsTrigger>
|
|
60
|
+
<TabsTrigger value="billing" data-kx-case="disabled" disabled>Billing</TabsTrigger>
|
|
61
|
+
</TabsList>
|
|
62
|
+
<TabsContent value="overview" className="text-sm text-muted-foreground">Three deployments this week.</TabsContent>
|
|
63
|
+
<TabsContent value="activity" className="text-sm text-muted-foreground">No new activity.</TabsContent>
|
|
64
|
+
</Tabs>
|
|
65
|
+
);
|
|
66
|
+
return where === "page" ? (
|
|
67
|
+
<div key={where}>{tabs}</div>
|
|
68
|
+
) : (
|
|
69
|
+
<Card key={where}>
|
|
70
|
+
<CardContent className="p-4">{tabs}</CardContent>
|
|
71
|
+
</Card>
|
|
72
|
+
);
|
|
73
|
+
})}
|
|
74
|
+
</div>
|
|
75
|
+
) };
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import type { Meta, StoryObj } from "@storybook/react-vite";
|
|
2
2
|
import { Textarea } from "../components/textarea";
|
|
3
|
+
import { Card, CardContent } from "../components/card";
|
|
4
|
+
import { Label } from "../components/label";
|
|
3
5
|
|
|
4
6
|
const STATES = ["Default", "Focus", "Error", "Disabled"] as const;
|
|
5
7
|
|
|
@@ -47,6 +49,38 @@ export const States: Story = {
|
|
|
47
49
|
),
|
|
48
50
|
};
|
|
49
51
|
|
|
52
|
+
/**
|
|
53
|
+
* The text-entry state contract (TOKENS.md, "Text entry and navigation") as a real form produces it: empty
|
|
54
|
+
* with a placeholder, filled, invalid with its error text, read-only and disabled, side by side in a raised
|
|
55
|
+
* Card. Real states, not the `state` prop's pinned ones — `check:entry-visual` drives hover and focus on
|
|
56
|
+
* these with a real pointer and keyboard, and both axe passes read them.
|
|
57
|
+
*/
|
|
58
|
+
export const EntryStates: Story = {
|
|
59
|
+
render: () => (
|
|
60
|
+
<Card className="w-[22rem] max-w-full">
|
|
61
|
+
<CardContent className="grid gap-5 p-6">
|
|
62
|
+
{([
|
|
63
|
+
["rest", "Bio", { placeholder: "A sentence or two about you" }],
|
|
64
|
+
["filled", "Shipping notes", { defaultValue: "Leave at the side door." }],
|
|
65
|
+
["invalid", "Reason for refund", { defaultValue: "No", "aria-invalid": true, "aria-describedby": "ta-invalid-error" }],
|
|
66
|
+
["readonly", "Signed agreement", { readOnly: true, defaultValue: "Accepted on 2 October." }],
|
|
67
|
+
["disabled", "Admin note", { disabled: true, defaultValue: "Only admins can edit this." }],
|
|
68
|
+
] as const).map(([key, label, props]) => (
|
|
69
|
+
<div key={key} className="grid gap-2">
|
|
70
|
+
<Label htmlFor={`ta-${key}`}>{label}</Label>
|
|
71
|
+
<Textarea id={`ta-${key}`} data-kx-case={key} rows={3} {...props} />
|
|
72
|
+
{key === "invalid" ? (
|
|
73
|
+
<p id="ta-invalid-error" className="text-body-sm text-destructive">
|
|
74
|
+
Tell us a little more, at least 20 characters.
|
|
75
|
+
</p>
|
|
76
|
+
) : null}
|
|
77
|
+
</div>
|
|
78
|
+
))}
|
|
79
|
+
</CardContent>
|
|
80
|
+
</Card>
|
|
81
|
+
),
|
|
82
|
+
};
|
|
83
|
+
|
|
50
84
|
export const WithFieldChrome: Story = {
|
|
51
85
|
render: (args) => (
|
|
52
86
|
<div className="flex flex-col gap-6">
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The React icon examples /docs/icons shows. Each `kx-usage:<key>` region is extracted by `pnpm gen:usage`,
|
|
3
|
+
* so the snippet on the page is code `tsc` type-checks against the real props, and components-icons.test.tsx
|
|
4
|
+
* renders it. Not exported from the package: an example, not API.
|
|
5
|
+
*/
|
|
6
|
+
import * as React from "react";
|
|
7
|
+
import { CircleX, Trash2 } from "lucide-react";
|
|
8
|
+
import { Banner } from "../components/banner";
|
|
9
|
+
import { Button } from "../components/button";
|
|
10
|
+
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "../components/tooltip";
|
|
11
|
+
|
|
12
|
+
/** Stands in for an application's own icon component: any SVG, no lucide, no KinetixUI. */
|
|
13
|
+
function CompanyCloseIcon(props: React.SVGProps<SVGSVGElement>) {
|
|
14
|
+
return (
|
|
15
|
+
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth={2} {...props}>
|
|
16
|
+
<path d="M6 6l12 12M18 6L6 18" />
|
|
17
|
+
</svg>
|
|
18
|
+
);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function IconsDismissIconExample({ onDismiss = () => {} }: { onDismiss?: () => void }) {
|
|
22
|
+
return (
|
|
23
|
+
<div className="flex flex-col gap-3">
|
|
24
|
+
{/* kx-usage:icons-dismiss-icon */}
|
|
25
|
+
{/* Default: lucide's X */}
|
|
26
|
+
<Banner onDismiss={onDismiss}>Changes saved.</Banner>
|
|
27
|
+
|
|
28
|
+
{/* Another lucide icon */}
|
|
29
|
+
<Banner onDismiss={onDismiss} dismissIcon={<CircleX />}>
|
|
30
|
+
Changes saved.
|
|
31
|
+
</Banner>
|
|
32
|
+
|
|
33
|
+
{/* Your own icon: sized to 14px, coloured by the banner, named "Dismiss" */}
|
|
34
|
+
<Banner onDismiss={onDismiss} dismissIcon={<CompanyCloseIcon />}>
|
|
35
|
+
Changes saved.
|
|
36
|
+
</Banner>
|
|
37
|
+
{/* kx-usage:end */}
|
|
38
|
+
</div>
|
|
39
|
+
);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export function IconsIconOnlyButtonExample({ onDelete = () => {} }: { onDelete?: () => void }) {
|
|
43
|
+
return (
|
|
44
|
+
<TooltipProvider>
|
|
45
|
+
{/* kx-usage:icons-icon-only-button */}
|
|
46
|
+
<Tooltip>
|
|
47
|
+
<TooltipTrigger asChild>
|
|
48
|
+
{/* size="icon" keeps the svg visible and turns the text into the button's
|
|
49
|
+
screen-reader-only name: "Delete draft, button" */}
|
|
50
|
+
<Button size="icon" variant="Ghost" onClick={onDelete}>
|
|
51
|
+
<Trash2 />
|
|
52
|
+
<span>Delete draft</span>
|
|
53
|
+
</Button>
|
|
54
|
+
</TooltipTrigger>
|
|
55
|
+
<TooltipContent>Delete draft</TooltipContent>
|
|
56
|
+
</Tooltip>
|
|
57
|
+
{/* kx-usage:end */}
|
|
58
|
+
</TooltipProvider>
|
|
59
|
+
);
|
|
60
|
+
}
|
package/tailwind.config.ts
CHANGED
|
@@ -17,23 +17,35 @@ export default {
|
|
|
17
17
|
fontFamily: { sans: "var(--font-sans)" },
|
|
18
18
|
// Material-3 type scale — `text-body-md`, `text-headline-lg`, … each carries
|
|
19
19
|
// its line-height, tracking and weight. Mirrors tokens/semantic/typography.json.
|
|
20
|
+
//
|
|
21
|
+
// Sizes and line-heights are rem so that text follows the reader's browser font-size setting;
|
|
22
|
+
// the numbers are the canonical scale divided by the 16px root, so every step renders at exactly
|
|
23
|
+
// its old pixel size at the default setting (14px -> 0.875rem) and grows from there. They are not
|
|
24
|
+
// hand-maintained against the tokens any more: `check:type-scale` derives this table from
|
|
25
|
+
// tokens/semantic/typography.json and fails if the two disagree, which is how the px values here
|
|
26
|
+
// went on silently outliving the ones in globals.css.
|
|
27
|
+
//
|
|
28
|
+
// Letter-spacing stays in px deliberately. It is an optical constant of the typeface at a
|
|
29
|
+
// reference size rather than a size a reader asked to change, the primitive tokens are shared
|
|
30
|
+
// across steps so there is no one font size to make it relative to, and at 0.1-0.5px it is far
|
|
31
|
+
// below the threshold where scaling it would be legible.
|
|
20
32
|
fontSize: {
|
|
21
|
-
"display-lg": ["
|
|
22
|
-
"display-md": ["
|
|
23
|
-
"display-sm": ["
|
|
24
|
-
"headline-lg": ["
|
|
25
|
-
"headline-md": ["
|
|
26
|
-
"headline-sm": ["
|
|
27
|
-
"title-lg": ["
|
|
28
|
-
"title-md": ["
|
|
29
|
-
"title-sm": ["
|
|
30
|
-
"title-dialog": ["
|
|
31
|
-
"label-lg": ["
|
|
32
|
-
"label-md": ["
|
|
33
|
-
"label-sm": ["
|
|
34
|
-
"body-lg": ["
|
|
35
|
-
"body-md": ["
|
|
36
|
-
"body-sm": ["
|
|
33
|
+
"display-lg": ["3.5625rem", { lineHeight: "4rem", letterSpacing: "-0.25px", fontWeight: "500" }],
|
|
34
|
+
"display-md": ["2.8125rem", { lineHeight: "3.25rem", letterSpacing: "0", fontWeight: "500" }],
|
|
35
|
+
"display-sm": ["2.25rem", { lineHeight: "2.75rem", letterSpacing: "0", fontWeight: "400" }],
|
|
36
|
+
"headline-lg": ["2rem", { lineHeight: "2.5rem", letterSpacing: "0", fontWeight: "600" }],
|
|
37
|
+
"headline-md": ["1.75rem", { lineHeight: "2.25rem", letterSpacing: "0", fontWeight: "400" }],
|
|
38
|
+
"headline-sm": ["1.5rem", { lineHeight: "2rem", letterSpacing: "0", fontWeight: "400" }],
|
|
39
|
+
"title-lg": ["1.375rem", { lineHeight: "1.75rem", letterSpacing: "0", fontWeight: "400" }],
|
|
40
|
+
"title-md": ["1rem", { lineHeight: "1.5rem", letterSpacing: "0.15px", fontWeight: "500" }],
|
|
41
|
+
"title-sm": ["0.875rem", { lineHeight: "1.25rem", letterSpacing: "0.1px", fontWeight: "500" }],
|
|
42
|
+
"title-dialog": ["1.125rem", { lineHeight: "1.5rem", letterSpacing: "0", fontWeight: "600" }],
|
|
43
|
+
"label-lg": ["0.875rem", { lineHeight: "1.25rem", letterSpacing: "0.1px", fontWeight: "500" }],
|
|
44
|
+
"label-md": ["0.75rem", { lineHeight: "1rem", letterSpacing: "0.5px", fontWeight: "500" }],
|
|
45
|
+
"label-sm": ["0.6875rem", { lineHeight: "1rem", letterSpacing: "0.5px", fontWeight: "500" }],
|
|
46
|
+
"body-lg": ["1rem", { lineHeight: "1.5rem", letterSpacing: "0.5px", fontWeight: "400" }],
|
|
47
|
+
"body-md": ["0.875rem", { lineHeight: "1.25rem", letterSpacing: "0.25px", fontWeight: "400" }],
|
|
48
|
+
"body-sm": ["0.75rem", { lineHeight: "1rem", letterSpacing: "0", fontWeight: "400" }],
|
|
37
49
|
},
|
|
38
50
|
boxShadow: {
|
|
39
51
|
sm: "var(--shadow-sm)",
|
|
@@ -54,7 +66,16 @@ export default {
|
|
|
54
66
|
popover: { DEFAULT: c("--popover"), foreground: c("--popover-foreground") },
|
|
55
67
|
primary: { DEFAULT: c("--primary"), foreground: c("--primary-foreground") },
|
|
56
68
|
secondary: { DEFAULT: c("--secondary"), foreground: c("--secondary-foreground") },
|
|
57
|
-
muted: {
|
|
69
|
+
muted: {
|
|
70
|
+
DEFAULT: c("--muted"),
|
|
71
|
+
foreground: c("--muted-foreground"),
|
|
72
|
+
// Secondary text on a tinted container (bg-primary/10, bg-destructive/10). `--muted-foreground`
|
|
73
|
+
// is tuned against `--background` and `--muted`; a 10% tint drops it to 4.33:1.
|
|
74
|
+
"on-container": c("--semantic-muted-on-container"),
|
|
75
|
+
},
|
|
76
|
+
// The grouped section raised content sits on (TOKENS.md, surface model). Not `muted`: dark `muted` is
|
|
77
|
+
// lighter than `card`, so a card on it read as recessed.
|
|
78
|
+
surface: { grouped: c("--surface-grouped") },
|
|
58
79
|
accent: { DEFAULT: c("--accent"), foreground: c("--accent-foreground") },
|
|
59
80
|
destructive: { DEFAULT: c("--destructive"), foreground: c("--destructive-foreground") },
|
|
60
81
|
success: { DEFAULT: c("--success"), foreground: c("--success-foreground") },
|
|
@@ -62,10 +83,14 @@ export default {
|
|
|
62
83
|
info: {
|
|
63
84
|
DEFAULT: c("--info"),
|
|
64
85
|
foreground: c("--info-foreground"),
|
|
65
|
-
// text on a tinted info surface (bg-info/10) — --info alone is 4.15:1 there
|
|
66
|
-
|
|
86
|
+
// text on a tinted info surface (bg-info/10) — --info alone is 4.15:1 there. The top-level role
|
|
87
|
+
// every platform shares; `--semantic-on-info-container` is its legacy alias.
|
|
88
|
+
"on-container": c("--on-info-container"),
|
|
67
89
|
},
|
|
68
90
|
tertiary: { DEFAULT: c("--tertiary"), foreground: c("--tertiary-foreground") },
|
|
91
|
+
// The modal backdrop. The role carries its own alpha (black at 40% light, 60% dark), so it is mapped
|
|
92
|
+
// without `<alpha-value>`: `bg-scrim`, not `bg-scrim/40`.
|
|
93
|
+
scrim: "hsl(var(--scrim))",
|
|
69
94
|
// Role tokens layered over `primary` (which stays the shadcn-compatible source). `action`, `link`
|
|
70
95
|
// and `focus` default to it, so a theme that only sets `--primary` still works; override them
|
|
71
96
|
// individually to split the interactive colour, link colour and focus colour. `brand` is identity
|
|
@@ -123,6 +148,10 @@ export default {
|
|
|
123
148
|
// Added alongside Tailwind's own numeric scales (z-10, opacity-50, …),
|
|
124
149
|
// not replacing them — these are the token-backed semantic names.
|
|
125
150
|
transitionDuration: {
|
|
151
|
+
// `instant` is the press/toggle tier from tokens/primitives/motion.json. It was generated
|
|
152
|
+
// into globals.css but never wired here, so the one duration meant for immediate feedback
|
|
153
|
+
// was the one nobody could reach — components fell back to Tailwind's untokenised 150ms.
|
|
154
|
+
instant: "var(--duration-instant)",
|
|
126
155
|
fast: "var(--duration-fast)",
|
|
127
156
|
base: "var(--duration-base)",
|
|
128
157
|
slow: "var(--duration-slow)",
|
|
@@ -133,6 +162,13 @@ export default {
|
|
|
133
162
|
// cubic-bezier(0,0,1,1) value, now token-backed instead of hardcoded
|
|
134
163
|
linear: "var(--easing-linear)",
|
|
135
164
|
standard: "var(--easing-standard)",
|
|
165
|
+
// Same gap as `instant`: enter/exit/emphasized are defined in the DTCG source and emitted
|
|
166
|
+
// as CSS variables, but were unreachable from a class. Directional easing is the whole
|
|
167
|
+
// point of the enter/exit pair — a surface that opens should decelerate and one that
|
|
168
|
+
// closes should accelerate, and without these you cannot say that in a utility.
|
|
169
|
+
enter: "var(--easing-enter)",
|
|
170
|
+
exit: "var(--easing-exit)",
|
|
171
|
+
emphasized: "var(--easing-emphasized)",
|
|
136
172
|
},
|
|
137
173
|
zIndex: {
|
|
138
174
|
base: "var(--z-index-base)",
|
|
@@ -151,18 +187,78 @@ export default {
|
|
|
151
187
|
keyframes: {
|
|
152
188
|
"accordion-down": { from: { height: "0" }, to: { height: "var(--radix-accordion-content-height)" } },
|
|
153
189
|
"accordion-up": { from: { height: "var(--radix-accordion-content-height)" }, to: { height: "0" } },
|
|
190
|
+
// Collapsible is the same disclosure gesture as Accordion on a different Radix primitive, so
|
|
191
|
+
// it reads its own measured height variable and is otherwise identical.
|
|
192
|
+
"collapsible-down": { from: { height: "0" }, to: { height: "var(--radix-collapsible-content-height)" } },
|
|
193
|
+
"collapsible-up": { from: { height: "var(--radix-collapsible-content-height)" }, to: { height: "0" } },
|
|
154
194
|
"caret-blink": { "0%,70%,100%": { opacity: "1" }, "20%,50%": { opacity: "0" } },
|
|
155
195
|
marquee: { from: { transform: "translateX(0)" }, to: { transform: "translateX(-50%)" } },
|
|
156
196
|
"typing-dot": { "0%,60%,100%": { transform: "translateY(0)", opacity: "0.4" }, "30%": { transform: "translateY(-3px)", opacity: "1" } },
|
|
157
197
|
},
|
|
198
|
+
/**
|
|
199
|
+
* Durations and easings come from the tokens, not from numbers typed here.
|
|
200
|
+
*
|
|
201
|
+
* `0.2s ease-out` happened to equal `--duration-fast` and to be the same curve as
|
|
202
|
+
* `--easing-enter`, so the values were right and the provenance was not — a change to the token
|
|
203
|
+
* would have moved every other transition in the system and left disclosure behind.
|
|
204
|
+
*
|
|
205
|
+
* Disclosure uses the directional pair the easing tokens exist for: opening decelerates
|
|
206
|
+
* (`enter`), closing accelerates (`exit`). The fallbacks match the token values and exist
|
|
207
|
+
* because `animation` is a shorthand — an undefined `var()` makes the whole declaration invalid,
|
|
208
|
+
* which would mean no animation at all rather than an un-themed one. `marquee` already
|
|
209
|
+
* establishes that pattern below.
|
|
210
|
+
*
|
|
211
|
+
* `caret-blink` and `typing-dot` keep their literal timings. They are looping affordances
|
|
212
|
+
* rather than state transitions, their rhythms were chosen for their own reasons, and
|
|
213
|
+
* `scripts/a11y-animations.mjs` documents caret-blink at 1.25s by name. Retiming them to the
|
|
214
|
+
* nearest token would change how they look to buy consistency nobody asked for, which is a
|
|
215
|
+
* separate decision from giving disclosure its provenance back.
|
|
216
|
+
*/
|
|
158
217
|
animation: {
|
|
159
|
-
"accordion-down": "accordion-down 0.
|
|
160
|
-
"accordion-up": "accordion-up 0.
|
|
218
|
+
"accordion-down": "accordion-down var(--duration-fast, 200ms) var(--easing-enter, cubic-bezier(0, 0, 0.2, 1))",
|
|
219
|
+
"accordion-up": "accordion-up var(--duration-fast, 200ms) var(--easing-exit, cubic-bezier(0.4, 0, 1, 1))",
|
|
220
|
+
"collapsible-down": "collapsible-down var(--duration-fast, 200ms) var(--easing-enter, cubic-bezier(0, 0, 0.2, 1))",
|
|
221
|
+
"collapsible-up": "collapsible-up var(--duration-fast, 200ms) var(--easing-exit, cubic-bezier(0.4, 0, 1, 1))",
|
|
161
222
|
"caret-blink": "caret-blink 1.25s ease-out infinite",
|
|
162
223
|
marquee: "marquee var(--marquee-duration, 32s) linear infinite",
|
|
163
224
|
"typing-dot": "typing-dot 1.2s ease-in-out infinite",
|
|
164
225
|
},
|
|
165
226
|
},
|
|
166
227
|
},
|
|
167
|
-
plugins: [
|
|
228
|
+
plugins: [
|
|
229
|
+
require("tailwindcss-animate"),
|
|
230
|
+
/**
|
|
231
|
+
* Reduced motion, for everything this package ships.
|
|
232
|
+
*
|
|
233
|
+
* `packages/ui` ships no CSS of its own — components are class strings and consumers extend
|
|
234
|
+
* this config — so this base layer is the only place a library-wide guarantee can live.
|
|
235
|
+
* It is needed: `tailwindcss-animate` emits no `prefers-reduced-motion` rule at all, so every
|
|
236
|
+
* `animate-in` / `animate-out` on Dialog, Sheet, Popover, Dropdown, Tooltip and the rest ran at
|
|
237
|
+
* full motion in a consumer's app regardless of the operating-system setting.
|
|
238
|
+
*
|
|
239
|
+
* It shortens durations rather than removing motion, and that distinction is the safety
|
|
240
|
+
* property. `transition-duration: 0.01ms` still arrives at the same end state, so a Switch
|
|
241
|
+
* thumb is still translated, a checked box is still checked, and an IoT control still shows
|
|
242
|
+
* confirmed rather than requested — the interpolation goes, the information does not. That is
|
|
243
|
+
* what makes a blanket rule safe here, where `transition: none` would be a blunter instrument
|
|
244
|
+
* and `animation: none` an actively dangerous one: Radix unmounts an overlay on `animationend`,
|
|
245
|
+
* and an animation that never runs never fires it, so the overlay would stay in the tree.
|
|
246
|
+
*
|
|
247
|
+
* Components that need more than this still say so at their own call site with
|
|
248
|
+
* `motion-reduce:`, and `packages/iot` already does exactly that where a pulse or a bar carries
|
|
249
|
+
* meaning. This is the floor, not a replacement for that.
|
|
250
|
+
*/
|
|
251
|
+
({ addBase }: { addBase: (styles: Record<string, unknown>) => void }) => {
|
|
252
|
+
addBase({
|
|
253
|
+
"@media (prefers-reduced-motion: reduce)": {
|
|
254
|
+
"*, ::before, ::after": {
|
|
255
|
+
"animation-duration": "0.01ms !important",
|
|
256
|
+
"animation-iteration-count": "1 !important",
|
|
257
|
+
"transition-duration": "0.01ms !important",
|
|
258
|
+
"scroll-behavior": "auto !important",
|
|
259
|
+
},
|
|
260
|
+
},
|
|
261
|
+
});
|
|
262
|
+
},
|
|
263
|
+
],
|
|
168
264
|
} satisfies Config;
|