@tangle-network/ui 11.40.0 → 11.40.1

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 CHANGED
@@ -1,5 +1,13 @@
1
1
  # @tangle-network/ui
2
2
 
3
+ ## 11.40.1
4
+
5
+ ### Patch Changes
6
+
7
+ - af0c788: Give Button, Input and SelectTrigger a shared 44px minimum in both dimensions on coarse primary pointers, including compact density. Icon buttons keep a square 44px touch target. Fine-pointer sizes and the existing field text floor are unchanged.
8
+
9
+ Name UploadProgress retry and remove actions after their file, and render them with the shared ghost icon Button for consistent focus and hit areas. Actions remain non-submitting buttons; retry is available only for failed files.
10
+
3
11
  ## 11.40.0
4
12
 
5
13
  ### Minor Changes
@@ -35,7 +35,7 @@ export declare const fieldSizes: {
35
35
  readonly lg: string;
36
36
  };
37
37
  export declare const controlSizes: {
38
- readonly compact: "h-[var(--control-height)] px-3 py-1 text-sm";
38
+ readonly compact: "h-[var(--control-height)] px-3 py-1 text-sm pointer-coarse:min-h-11 pointer-coarse:min-w-11";
39
39
  readonly touch: "h-11 min-h-11 min-w-11 px-4 py-2 text-base";
40
40
  };
41
41
  export declare const fieldPresentation: string;
@@ -30,18 +30,20 @@ const controlSquare = {
30
30
  */
31
31
  const fieldTouchText = "pointer-coarse:text-base";
32
32
  /**
33
- * A control that is shorter than 44px on a fine pointer still gets a 44px hit
34
- * height on a coarse one (WCAG 2.5.5, Apple HIG). Same floor as `touch`.
33
+ * A control smaller than 44px on a fine pointer still gets a 44px target
34
+ * in both dimensions on a coarse one (WCAG 2.5.5, Apple HIG). Same floor as `touch`.
35
+ * Follow the primary pointer, matching SegmentedControl; a secondary touch
36
+ * input alone does not inflate a fine-pointer desktop layout.
35
37
  */
36
- const touchTarget = "pointer-coarse:min-h-11";
38
+ const touchTarget = "pointer-coarse:min-h-11 pointer-coarse:min-w-11";
37
39
  /** Field sizes: the control scale with field padding. */
38
40
  const fieldSizes = {
39
- sm: cn(controlHeight.sm, controlText.sm, controlRadius.sm, "px-2.5"),
40
- md: cn(controlHeight.md, controlText.md, controlRadius.md, "px-3"),
41
- lg: cn(controlHeight.lg, controlText.lg, controlRadius.lg, "px-4")
41
+ sm: cn(controlHeight.sm, controlText.sm, controlRadius.sm, touchTarget, "px-2.5"),
42
+ md: cn(controlHeight.md, controlText.md, controlRadius.md, touchTarget, "px-3"),
43
+ lg: cn(controlHeight.lg, controlText.lg, controlRadius.lg, touchTarget, "px-4")
42
44
  };
43
45
  const controlSizes = {
44
- compact: "h-[var(--control-height)] px-3 py-1 text-sm",
46
+ compact: `h-[var(--control-height)] px-3 py-1 text-sm ${touchTarget}`,
45
47
  touch: "h-11 min-h-11 min-w-11 px-4 py-2 text-base"
46
48
  };
47
49
  const fieldPresentation = cn(focusField, ixField, controlMotion, "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)]");
@@ -2,7 +2,7 @@ import { controlMotion, surfaceMotion } from "./lib/motion.js";
2
2
  import { ixControl, ixControlSoft, ixSelected, ixTabUnderline } from "./lib/interaction.js";
3
3
  import { focusFieldInvalid, focusRing } from "./lib/focus.js";
4
4
  import { cn } from "./lib/utils.js";
5
- import { controlHeight, controlRadius, controlSizes, controlSquare, controlText, fieldPresentation, fieldSizes, fieldTouchText } from "./lib/control-presentation.js";
5
+ import { controlHeight, controlRadius, controlSizes, controlSquare, controlText, fieldPresentation, fieldSizes, fieldTouchText, touchTarget } from "./lib/control-presentation.js";
6
6
  import { cva } from "class-variance-authority";
7
7
  //#region src/presentation.ts
8
8
  /** React-free presentation. Consumers scan this published source with Tailwind. */
@@ -19,15 +19,15 @@ const buttonVariants = cva(`inline-flex items-center justify-center gap-2 whites
19
19
  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}`
20
20
  },
21
21
  size: {
22
- default: `${controlHeight.md} ${controlText.md} ${controlRadius.md} px-4 py-2`,
23
- md: `${controlHeight.md} ${controlText.md} ${controlRadius.md} px-4 py-2`,
24
- sm: `${controlHeight.sm} ${controlText.sm} ${controlRadius.sm} px-3`,
25
- lg: `${controlHeight.lg} ${controlText.lg} ${controlRadius.lg} px-7`,
22
+ default: `${controlHeight.md} ${controlText.md} ${controlRadius.md} ${touchTarget} px-4 py-2`,
23
+ md: `${controlHeight.md} ${controlText.md} ${controlRadius.md} ${touchTarget} px-4 py-2`,
24
+ sm: `${controlHeight.sm} ${controlText.sm} ${controlRadius.sm} ${touchTarget} px-3`,
25
+ lg: `${controlHeight.lg} ${controlText.lg} ${controlRadius.lg} ${touchTarget} px-7`,
26
26
  display: "tangle-control-display",
27
27
  xl: "h-13 rounded-xl px-9 text-base",
28
- icon: `${controlSquare.md} ${controlText.md} ${controlRadius.md}`,
29
- "icon-sm": `${controlSquare.sm} ${controlText.sm} ${controlRadius.sm}`,
30
- "icon-lg": `${controlSquare.lg} ${controlText.lg} ${controlRadius.lg}`,
28
+ icon: `${controlSquare.md} ${controlText.md} ${controlRadius.md} ${touchTarget}`,
29
+ "icon-sm": `${controlSquare.sm} ${controlText.sm} ${controlRadius.sm} ${touchTarget}`,
30
+ "icon-lg": `${controlSquare.lg} ${controlText.lg} ${controlRadius.lg} ${touchTarget}`,
31
31
  ...controlSizes
32
32
  }
33
33
  },
@@ -152,7 +152,7 @@ function SegmentedControl({ value, onValueChange, options, variant = "row", clas
152
152
  "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)]"
153
153
  ], variant === "tabs" && [
154
154
  "shrink-0 rounded-none border-b-2 border-transparent -mb-px px-4 py-2",
155
- "pointer-coarse:min-h-11",
155
+ "pointer-coarse:min-h-11 pointer-coarse:min-w-11",
156
156
  focusRingInset,
157
157
  "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)]"
158
158
  ], active ? "text-[var(--accent-text)]" : "text-muted-foreground hover:text-foreground"),
@@ -1,4 +1,5 @@
1
1
  import { cn } from "../lib/utils.js";
2
+ import { Button } from "./button.js";
2
3
  import { jsx, jsxs } from "react/jsx-runtime";
3
4
  import { AlertCircle, CheckCircle2, FileText, Loader2, RefreshCw, X } from "lucide-react";
4
5
  //#region src/primitives/upload-progress.tsx
@@ -52,16 +53,20 @@ function UploadProgress({ files, onRemove, onRetry, className }) {
52
53
  }),
53
54
  /* @__PURE__ */ jsxs("div", {
54
55
  className: "flex shrink-0 items-center gap-1",
55
- children: [file.status === "error" && onRetry && /* @__PURE__ */ jsx("button", {
56
+ children: [file.status === "error" && onRetry && /* @__PURE__ */ jsx(Button, {
56
57
  type: "button",
58
+ variant: "ghost",
59
+ size: "icon-sm",
57
60
  onClick: () => onRetry(file.id),
58
- className: "rounded p-1 text-muted-foreground transition-colors hover:bg-accent hover:text-foreground",
59
- children: /* @__PURE__ */ jsx(RefreshCw, { className: "h-3.5 w-3.5" })
60
- }), onRemove && /* @__PURE__ */ jsx("button", {
61
+ "aria-label": `Retry ${file.name}`,
62
+ children: /* @__PURE__ */ jsx(RefreshCw, { "aria-hidden": "true" })
63
+ }), onRemove && /* @__PURE__ */ jsx(Button, {
61
64
  type: "button",
65
+ variant: "ghost",
66
+ size: "icon-sm",
62
67
  onClick: () => onRemove(file.id),
63
- className: "rounded p-1 text-muted-foreground transition-colors hover:bg-accent hover:text-foreground",
64
- children: /* @__PURE__ */ jsx(X, { className: "h-3.5 w-3.5" })
68
+ "aria-label": `Remove ${file.name}`,
69
+ children: /* @__PURE__ */ jsx(X, { "aria-hidden": "true" })
65
70
  })]
66
71
  })
67
72
  ]
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tangle-network/ui",
3
- "version": "11.40.0",
3
+ "version": "11.40.1",
4
4
  "description": "Generic React UI components for Tangle products — primitives, chat, run, files, editor, markdown.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -46,22 +46,24 @@ export const controlSquare = {
46
46
  export const fieldTouchText = "pointer-coarse:text-base";
47
47
 
48
48
  /**
49
- * A control that is shorter than 44px on a fine pointer still gets a 44px hit
50
- * height on a coarse one (WCAG 2.5.5, Apple HIG). Same floor as `touch`.
49
+ * A control smaller than 44px on a fine pointer still gets a 44px target
50
+ * in both dimensions on a coarse one (WCAG 2.5.5, Apple HIG). Same floor as `touch`.
51
+ * Follow the primary pointer, matching SegmentedControl; a secondary touch
52
+ * input alone does not inflate a fine-pointer desktop layout.
51
53
  */
52
- export const touchTarget = "pointer-coarse:min-h-11";
54
+ export const touchTarget = "pointer-coarse:min-h-11 pointer-coarse:min-w-11";
53
55
 
54
56
  /** Field sizes: the control scale with field padding. */
55
57
  export const fieldSizes = {
56
- sm: cn(controlHeight.sm, controlText.sm, controlRadius.sm, "px-2.5"),
57
- md: cn(controlHeight.md, controlText.md, controlRadius.md, "px-3"),
58
- lg: cn(controlHeight.lg, controlText.lg, controlRadius.lg, "px-4"),
58
+ sm: cn(controlHeight.sm, controlText.sm, controlRadius.sm, touchTarget, "px-2.5"),
59
+ md: cn(controlHeight.md, controlText.md, controlRadius.md, touchTarget, "px-3"),
60
+ lg: cn(controlHeight.lg, controlText.lg, controlRadius.lg, touchTarget, "px-4"),
59
61
  } as const satisfies Record<ControlSize, string>;
60
62
 
61
63
  // Explicit opt-in sizes kept for existing callers. Compact follows the
62
64
  // consumer's density token (md); touch is lg with a guaranteed 44px minimum.
63
65
  export const controlSizes = {
64
- compact: "h-[var(--control-height)] px-3 py-1 text-sm",
66
+ compact: `h-[var(--control-height)] px-3 py-1 text-sm ${touchTarget}`,
65
67
  touch: "h-11 min-h-11 min-w-11 px-4 py-2 text-base",
66
68
  } as const;
67
69
 
@@ -1,7 +1,7 @@
1
1
  /** React-free presentation. Consumers scan this published source with Tailwind. */
2
2
  import { surfaceMotion } from "./lib/motion";
3
3
  import { cva } from "class-variance-authority";
4
- import { controlHeight, controlMotion, controlRadius, controlSizes, controlSquare, controlText, fieldPresentation, fieldSizes, fieldTouchText } from "./lib/control-presentation";
4
+ import { controlHeight, controlMotion, controlRadius, controlSizes, controlSquare, controlText, fieldPresentation, fieldSizes, fieldTouchText, touchTarget } from "./lib/control-presentation";
5
5
  import { focusRing, focusFieldInvalid } from "./lib/focus";
6
6
  import { ixControl, ixControlSoft, ixSelected, ixTabUnderline } from "./lib/interaction";
7
7
  import { cn } from "./lib/utils";
@@ -29,15 +29,15 @@ export const buttonVariants = cva(
29
29
  `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}`,
30
30
  },
31
31
  size: {
32
- default: `${controlHeight.md} ${controlText.md} ${controlRadius.md} px-4 py-2`,
33
- md: `${controlHeight.md} ${controlText.md} ${controlRadius.md} px-4 py-2`,
34
- sm: `${controlHeight.sm} ${controlText.sm} ${controlRadius.sm} px-3`,
35
- lg: `${controlHeight.lg} ${controlText.lg} ${controlRadius.lg} px-7`,
32
+ default: `${controlHeight.md} ${controlText.md} ${controlRadius.md} ${touchTarget} px-4 py-2`,
33
+ md: `${controlHeight.md} ${controlText.md} ${controlRadius.md} ${touchTarget} px-4 py-2`,
34
+ sm: `${controlHeight.sm} ${controlText.sm} ${controlRadius.sm} ${touchTarget} px-3`,
35
+ lg: `${controlHeight.lg} ${controlText.lg} ${controlRadius.lg} ${touchTarget} px-7`,
36
36
  display: "tangle-control-display",
37
37
  xl: "h-13 rounded-xl px-9 text-base",
38
- icon: `${controlSquare.md} ${controlText.md} ${controlRadius.md}`,
39
- "icon-sm": `${controlSquare.sm} ${controlText.sm} ${controlRadius.sm}`,
40
- "icon-lg": `${controlSquare.lg} ${controlText.lg} ${controlRadius.lg}`,
38
+ icon: `${controlSquare.md} ${controlText.md} ${controlRadius.md} ${touchTarget}`,
39
+ "icon-sm": `${controlSquare.sm} ${controlText.sm} ${controlRadius.sm} ${touchTarget}`,
40
+ "icon-lg": `${controlSquare.lg} ${controlText.lg} ${controlRadius.lg} ${touchTarget}`,
41
41
  ...controlSizes,
42
42
  },
43
43
  },
@@ -1,6 +1,7 @@
1
1
  import * as React from "react";
2
2
  import type { Meta, StoryObj } from "@storybook/react";
3
3
  import { Button } from "./button";
4
+ import { X } from "lucide-react";
4
5
  import { HelpText } from "./help-text";
5
6
  import { Input, Textarea } from "./input";
6
7
  import { Label } from "./label";
@@ -13,7 +14,7 @@ function Contract() {
13
14
  return (
14
15
  <main className="mx-auto max-w-5xl space-y-6 bg-background p-4 text-foreground" data-presentation-contract>
15
16
  <h1 className="text-xl font-semibold">Field and metric presentation</h1>
16
- {(["sm", "md", "lg", "compact", "touch"] as const).map((size) => (
17
+ {(["default", "sm", "md", "lg", "compact", "touch"] as const).map((size) => (
17
18
  <section key={size} data-size-row={size} className="space-y-2">
18
19
  <h2 className="font-semibold">{size}</h2>
19
20
  <div className="flex flex-wrap items-end gap-3">
@@ -27,10 +28,16 @@ function Contract() {
27
28
  </Select>
28
29
  <Button size={size} onClick={() => setActions((value) => value + 1)}>Save</Button>
29
30
  <Button size={size} variant="outline">Cancel</Button>
31
+ <Button size={size} aria-label={`${size} short action`}>+</Button>
30
32
  </div>
31
33
  <Textarea size={size} aria-label={`${size} notes`} placeholder="Details" />
32
34
  </section>
33
35
  ))}
36
+ <section className="flex flex-wrap gap-3" aria-label="Icon controls">
37
+ {(["icon-sm", "icon", "icon-lg"] as const).map((size) => (
38
+ <Button key={size} size={size} aria-label={`${size} action`} onClick={() => setActions((value) => value + 1)}><X aria-hidden="true" /></Button>
39
+ ))}
40
+ </section>
34
41
  <section className="max-w-sm space-y-1.5" data-form-text>
35
42
  <Label htmlFor="workspace-name">Workspace name</Label>
36
43
  <Input id="workspace-name" aria-describedby="workspace-name-hint" placeholder="Acme" />
@@ -1,4 +1,6 @@
1
1
  import type { Meta, StoryObj } from '@storybook/react'
2
+ import { useState } from 'react'
3
+ import { Button } from './button'
2
4
  import { UploadProgress, type UploadFile } from './upload-progress'
3
5
 
4
6
  const meta: Meta<typeof UploadProgress> = {
@@ -7,7 +9,7 @@ const meta: Meta<typeof UploadProgress> = {
7
9
  parameters: { layout: 'centered', backgrounds: { default: 'dark' } },
8
10
  decorators: [
9
11
  (Story) => (
10
- <div className="w-[420px]">
12
+ <div className="w-[420px] max-w-[calc(100vw-2rem)]">
11
13
  <Story />
12
14
  </div>
13
15
  ),
@@ -118,3 +120,36 @@ export const NoActions: Story = {
118
120
  name: 'No remove / retry handlers',
119
121
  args: { files: [complete, uploading45, error] },
120
122
  }
123
+
124
+ // A failed upload inside a form, as used by the Agent Builder cover field.
125
+ // Each retry becomes pending; a new observed failure makes retry available again.
126
+ const cover: UploadFile = {
127
+ id: 'cover',
128
+ name: 'cover-image-with-a-long-filename-for-project-review.png',
129
+ size: 2048,
130
+ status: 'error',
131
+ error: 'The upload failed. Try again or remove this file.',
132
+ }
133
+
134
+ function RecoveryExample() {
135
+ const [files, setFiles] = useState<UploadFile[]>([cover, complete])
136
+ const [retries, setRetries] = useState(0)
137
+ const [submits, setSubmits] = useState(0)
138
+ return (
139
+ <form className="space-y-4" onSubmit={(event) => { event.preventDefault(); setSubmits((count) => count + 1) }}>
140
+ <UploadProgress
141
+ files={files}
142
+ onRetry={(id) => {
143
+ setRetries((count) => count + 1)
144
+ setFiles((current) => current.map((file) => file.id === id ? { ...file, status: 'pending', error: undefined } : file))
145
+ }}
146
+ onRemove={(id) => setFiles((current) => current.filter((file) => file.id !== id))}
147
+ />
148
+ <Button type="button" variant="outline" onClick={() => setFiles((current) => current.map((file) => file.id === cover.id ? cover : file))}>Simulate another failure</Button>
149
+ <output className="block text-foreground" aria-label="Retry count">{retries}</output>
150
+ <output className="block text-foreground" aria-label="Form submissions">{submits}</output>
151
+ </form>
152
+ )
153
+ }
154
+
155
+ export const Recovery: Story = { render: () => <RecoveryExample /> }
@@ -7,6 +7,7 @@
7
7
 
8
8
  import { AlertCircle, CheckCircle2, FileText, Loader2, RefreshCw, X } from "lucide-react";
9
9
  import { cn } from "../lib/utils";
10
+ import { Button } from "./button";
10
11
 
11
12
  export interface UploadFile {
12
13
  id: string;
@@ -85,22 +86,26 @@ export function UploadProgress({ files, onRemove, onRetry, className }: UploadPr
85
86
  {/* Actions */}
86
87
  <div className="flex shrink-0 items-center gap-1">
87
88
  {file.status === "error" && onRetry && (
88
- <button
89
+ <Button
89
90
  type="button"
91
+ variant="ghost"
92
+ size="icon-sm"
90
93
  onClick={() => onRetry(file.id)}
91
- className="rounded p-1 text-muted-foreground transition-colors hover:bg-accent hover:text-foreground"
94
+ aria-label={`Retry ${file.name}`}
92
95
  >
93
- <RefreshCw className="h-3.5 w-3.5" />
94
- </button>
96
+ <RefreshCw aria-hidden="true" />
97
+ </Button>
95
98
  )}
96
99
  {onRemove && (
97
- <button
100
+ <Button
98
101
  type="button"
102
+ variant="ghost"
103
+ size="icon-sm"
99
104
  onClick={() => onRemove(file.id)}
100
- className="rounded p-1 text-muted-foreground transition-colors hover:bg-accent hover:text-foreground"
105
+ aria-label={`Remove ${file.name}`}
101
106
  >
102
- <X className="h-3.5 w-3.5" />
103
- </button>
107
+ <X aria-hidden="true" />
108
+ </Button>
104
109
  )}
105
110
  </div>
106
111
  </div>
@@ -1,10 +1,10 @@
1
1
  /* Generated by scripts/generate-presentation-css.mjs from shared primitive recipes. Requires Tailwind + Brand theme. */
2
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; }
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 pointer-coarse:min-h-11 pointer-coarse:min-w-11 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 pointer-coarse:min-h-11 pointer-coarse:min-w-11 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 pointer-coarse:min-h-11 pointer-coarse:min-w-11 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 pointer-coarse:min-h-11 pointer-coarse:min-w-11 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 pointer-coarse:min-h-11 pointer-coarse:min-w-11 px-3; }
8
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
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
10
  .table-header { @apply h-12 px-4 text-left align-middle font-medium text-muted-foreground [&:has([role=checkbox])]:pr-0; }