@seekalfred/primitives 0.7.4 → 0.8.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 +255 -3
- package/dist/alert/alert.d.ts +2 -2
- package/dist/alert/alert.js +1 -1
- package/dist/alert/alert.js.map +1 -1
- package/dist/alert-dialog/alert-dialog.js +1 -1
- package/dist/alert-dialog/alert-dialog.js.map +1 -1
- package/dist/avatar/avatar.d.ts +1 -1
- package/dist/badge/badge.d.ts +16 -1
- package/dist/badge/badge.js +12 -5
- package/dist/badge/badge.js.map +1 -1
- package/dist/button/button.d.ts +24 -2
- package/dist/button/button.js +62 -13
- package/dist/button/button.js.map +1 -1
- package/dist/card/card.js +1 -1
- package/dist/card/card.js.map +1 -1
- package/dist/checkbox/checkbox.d.ts +25 -1
- package/dist/checkbox/checkbox.js +103 -28
- package/dist/checkbox/checkbox.js.map +1 -1
- package/dist/checkbox/index.d.ts +2 -2
- package/dist/checkbox/index.js +2 -2
- package/dist/code-block/code-block.d.ts +45 -0
- package/dist/code-block/code-block.js +74 -0
- package/dist/code-block/code-block.js.map +1 -0
- package/dist/code-block/index.d.ts +2 -0
- package/dist/code-block/index.js +2 -0
- package/dist/command-palette/command-palette.d.ts +128 -0
- package/dist/command-palette/command-palette.js +371 -0
- package/dist/command-palette/command-palette.js.map +1 -0
- package/dist/command-palette/index.d.ts +2 -0
- package/dist/command-palette/index.js +2 -0
- package/dist/dialog/dialog.js +1 -1
- package/dist/dialog/dialog.js.map +1 -1
- package/dist/empty-state/empty-state.d.ts +17 -0
- package/dist/empty-state/empty-state.js +9 -7
- package/dist/empty-state/empty-state.js.map +1 -1
- package/dist/index.d.ts +8 -4
- package/dist/index.js +12 -8
- package/dist/info-tooltip/index.d.ts +2 -0
- package/dist/info-tooltip/index.js +2 -0
- package/dist/info-tooltip/info-tooltip.d.ts +35 -0
- package/dist/info-tooltip/info-tooltip.js +49 -0
- package/dist/info-tooltip/info-tooltip.js.map +1 -0
- package/dist/input/input.js +1 -1
- package/dist/lib/selection-control.js +32 -1
- package/dist/lib/selection-control.js.map +1 -1
- package/dist/line-chart/line-chart.d.ts +2 -1
- package/dist/line-chart/line-chart.js +2 -2
- package/dist/line-chart/line-chart.js.map +1 -1
- package/dist/mention-input/mention-input.js +1 -1
- package/dist/money-input/money-input.js +1 -1
- package/dist/multi-select/multi-select.js +1 -1
- package/dist/number-field/number-field.js +1 -1
- package/dist/password-field/password-field.js +1 -1
- package/dist/password-strength/password-strength.js +1 -1
- package/dist/phone-input/phone-input.js +1 -1
- package/dist/popover/popover.js +1 -1
- package/dist/popover/popover.js.map +1 -1
- package/dist/progress/progress.d.ts +3 -3
- package/dist/radio-group/radio-group.d.ts +12 -3
- package/dist/radio-group/radio-group.js +62 -52
- package/dist/radio-group/radio-group.js.map +1 -1
- package/dist/rich-text-editor/rich-text-editor.js +1 -1
- package/dist/scroll-area/scroll-area.d.ts +1 -1
- package/dist/search-field/search-field.js +1 -1
- package/dist/select/select.d.ts +1 -1
- package/dist/select/select.js +1 -1
- package/dist/separator/separator.d.ts +1 -1
- package/dist/shortcut-recorder/shortcut-recorder.js +1 -1
- package/dist/stat-card/stat-card.d.ts +2 -2
- package/dist/styles.css +1 -1
- package/dist/table/index.d.ts +2 -2
- package/dist/table/index.js +2 -2
- package/dist/table/table.d.ts +99 -1
- package/dist/table/table.js +123 -23
- package/dist/table/table.js.map +1 -1
- package/dist/tabs/tabs.d.ts +4 -2
- package/dist/tabs/tabs.js +7 -5
- package/dist/tabs/tabs.js.map +1 -1
- package/dist/tag-input/tag-input.js +1 -1
- package/dist/textarea/textarea.js +1 -1
- package/dist/toast/toast.d.ts +2 -2
- package/dist/toggle-button/index.d.ts +2 -0
- package/dist/toggle-button/index.js +2 -0
- package/dist/toggle-button/toggle-button.d.ts +93 -0
- package/dist/toggle-button/toggle-button.js +199 -0
- package/dist/toggle-button/toggle-button.js.map +1 -0
- package/package.json +19 -3
package/dist/badge/badge.js
CHANGED
|
@@ -73,6 +73,10 @@ const badgeVariants = cva([
|
|
|
73
73
|
shape: {
|
|
74
74
|
rounded: "rounded-md",
|
|
75
75
|
pill: "rounded-full"
|
|
76
|
+
},
|
|
77
|
+
wrap: {
|
|
78
|
+
true: "min-w-0 max-w-full shrink whitespace-normal break-words text-start",
|
|
79
|
+
false: ""
|
|
76
80
|
}
|
|
77
81
|
},
|
|
78
82
|
compoundVariants: [
|
|
@@ -171,7 +175,8 @@ const badgeVariants = cva([
|
|
|
171
175
|
tone: "neutral",
|
|
172
176
|
appearance: "soft",
|
|
173
177
|
size: "md",
|
|
174
|
-
shape: "rounded"
|
|
178
|
+
shape: "rounded",
|
|
179
|
+
wrap: false
|
|
175
180
|
}
|
|
176
181
|
});
|
|
177
182
|
/** The name an icon swaps under: a different component swaps; the same one re-rendering does not. */
|
|
@@ -180,27 +185,29 @@ function iconKey(icon) {
|
|
|
180
185
|
const type = icon.type;
|
|
181
186
|
return typeof type === "string" ? type : type.displayName ?? type.name ?? "icon";
|
|
182
187
|
}
|
|
183
|
-
const Badge = forwardRef(function Badge({ className, tone, appearance, size, shape, icon, children, ...props }, ref) {
|
|
188
|
+
const Badge = forwardRef(function Badge({ className, tone, appearance, size, shape, wrap = false, icon, children, ...props }, ref) {
|
|
184
189
|
const content = useRef(null);
|
|
185
190
|
const text = typeof children === "string" || typeof children === "number" ? String(children) : null;
|
|
186
191
|
const glyph = icon === void 0 || icon === null ? "" : iconKey(icon);
|
|
187
|
-
useMorphWidth(content, `${text ?? ""}|${glyph}
|
|
192
|
+
useMorphWidth(content, `${text ?? ""}|${glyph}`, !wrap);
|
|
188
193
|
return /* @__PURE__ */ jsx("span", {
|
|
189
194
|
ref,
|
|
190
195
|
...props,
|
|
191
196
|
"data-slot": "badge",
|
|
192
197
|
"data-tone": tone ?? "neutral",
|
|
198
|
+
"data-wrap": wrap || void 0,
|
|
193
199
|
className: cn(badgeVariants({
|
|
194
200
|
tone,
|
|
195
201
|
appearance,
|
|
196
202
|
size,
|
|
197
|
-
shape
|
|
203
|
+
shape,
|
|
204
|
+
wrap
|
|
198
205
|
}), className),
|
|
199
206
|
children: /* @__PURE__ */ jsx("span", {
|
|
200
207
|
className: "button-slot",
|
|
201
208
|
children: /* @__PURE__ */ jsxs("span", {
|
|
202
209
|
ref: content,
|
|
203
|
-
className: "inline-flex flex-none items-center gap-1",
|
|
210
|
+
className: wrap ? "inline-flex min-w-0 items-center gap-1" : "inline-flex flex-none items-center gap-1",
|
|
204
211
|
children: [glyph ? /* @__PURE__ */ jsx(SwapText, {
|
|
205
212
|
swapKey: glyph,
|
|
206
213
|
motion: "glyph",
|
package/dist/badge/badge.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"badge.js","names":[],"sources":["../../src/badge/badge.tsx"],"sourcesContent":["\"use client\";\n\nimport { cva, type VariantProps } from \"class-variance-authority\";\nimport {\n forwardRef,\n isValidElement,\n useRef,\n type ComponentPropsWithoutRef,\n type ReactNode,\n} from \"react\";\n\nimport { cn } from \"../lib/cn.js\";\nimport { useMorphWidth } from \"../lib/motion.js\";\nimport { SwapText } from \"../lib/swap-text.js\";\n\n/**\n * Badge -- the status pill, in every shape the product asks for.\n *\n * Presentational, so there is no React Aria base. RAC's `Tag` looks similar\n * and is not equivalent: it is focusable, selectable and removable, and it is\n * illegal outside a `TagGroup` collection. A status pill is none of those.\n *\n * THIS ABSORBED THE PRODUCT'S `Pill`. The app shipped two components for one\n * job -- `badge.tsx` with a `variant` axis and `pill.tsx` with a `tone` axis --\n * across 22 and 22 consumer files. They disagreed about everything: shape\n * (`rounded-md` against `rounded-full`), size (one fixed, one `sm`/`md`/`lg`)\n * and naming, so `pill` accepted `blue` AND `info`, `green` AND `success`, for\n * the same colour. Converging them is why `variant` is gone.\n *\n * The axes are the two questions a call site actually answers -- what does\n * this MEAN (`tone`) and how loud should it be (`appearance`) -- rather than\n * one axis whose values are the cross product. Six tones by three appearances\n * is eighteen `variant` values otherwise, and shipping eighteen is how a\n * design system ends up with `softPrimary` next to `primaryOutline`.\n *\n * Every string below is a STATIC literal, including the compound ones.\n * Tailwind scans source text, so a class assembled at runtime is invisible to\n * it and would never be emitted.\n *\n * Three things in the legacy are deliberately not ported:\n *\n * - `asChild` and its `[a&]:hover:` rules. Zero call sites; the hover styles\n * existed only to colour the anchor asChild produced.\n * - `focus-visible:ring-*`. A span is not focusable, so the ring could never\n * appear. A consumer who adds `tabIndex` owns the indicator, and a ring is\n * the wrong one anyway -- box-shadows vanish under forced-colors.\n * - `aria-invalid:*`. A badge is never invalid; that was boilerplate copied\n * across every shadcn control.\n */\nexport const badgeVariants = cva(\n [\n \"inline-flex w-fit shrink-0 items-center justify-center gap-1\",\n \"border-(length:--border-width) border-transparent\",\n // NO leading-* here. It lives on each `size` instead, AFTER that size's\n // text-* class: tailwind-merge treats a later text-* as owning\n // line-height, so a leading-* in this base string is silently stripped the\n // moment a size variant adds one. The NoUaLeak story caught exactly that.\n \"font-sans font-medium\",\n \"whitespace-nowrap\",\n // box-border and font-sans are not boilerplate. The reset in\n // @seekalfred/styles is OPT-IN, so a consumer may load none: an unstyled\n // span inherits the host's serif, and content-box would add the padding\n // to any width a consumer sets.\n \"box-border\",\n \"[&_svg]:pointer-events-none [&_svg]:size-3\",\n // Colour, border and fill ease over 240ms as the tone changes.\n \"motion-safe:transition-colors motion-safe:duration-(--duration-standard) motion-safe:ease-settle\",\n ],\n {\n variants: {\n /** What the badge MEANS. Colour follows from it; it is never picked directly. */\n tone: {\n neutral: \"\",\n primary: \"\",\n success: \"\",\n warning: \"\",\n danger: \"\",\n info: \"\",\n },\n /** How loud it is. */\n appearance: {\n solid: \"\",\n soft: \"\",\n outline: \"bg-transparent\",\n },\n size: {\n sm: \"px-2 py-0.5 text-xs leading-(--leading-snug)\",\n md: \"px-2.5 py-0.5 text-xs leading-(--leading-snug)\",\n lg: \"px-3 py-1 text-sm leading-(--leading-snug)\",\n },\n /*\n * The product's two components disagreed on this and nothing else:\n * `badge.tsx` was rounded-md, `pill.tsx` was rounded-full. Both survive\n * as one axis rather than one winning, because a count-bearing badge and\n * a verdict pill really do read differently.\n */\n shape: { rounded: \"rounded-md\", pill: \"rounded-full\" },\n },\n compoundVariants: [\n // SOLID -- the fill carries the meaning, so the text must clear 4.5:1 on it.\n /*\n * muted-foreground, NOT `secondary`. --color-secondary resolves to\n * periwinkle.400, so a \"neutral\" badge rendered lavender -- caught in a\n * browser, not by any assertion, because the story compared it against\n * the same wrong token it was painted with. A tone called neutral has to\n * be neutral; the periwinkle fill the product uses for its `secondary`\n * badge is the brand's SECOND colour, not an absence of one.\n */\n { tone: \"neutral\", appearance: \"solid\", class: \"bg-muted-foreground text-background\" },\n { tone: \"primary\", appearance: \"solid\", class: \"bg-primary text-primary-foreground\" },\n { tone: \"success\", appearance: \"solid\", class: \"bg-success-strong text-success-strong-foreground\" },\n { tone: \"warning\", appearance: \"solid\", class: \"bg-warning-strong text-warning-strong-foreground\" },\n { tone: \"danger\", appearance: \"solid\", class: \"bg-destructive text-destructive-foreground\" },\n { tone: \"info\", appearance: \"solid\", class: \"bg-info-strong text-info-strong-foreground\" },\n\n /*\n * SOFT -- a tint, and the default, because this is what a status pill is.\n * The ink is one step darker than the obvious pairing for success,\n * warning and info: the product measured 4.13, 3.48 and 3.34 on its own\n * tints and had to darken all three. Our `-foreground` tokens already\n * encode that correction, which is why they are used rather than\n * `-strong`.\n */\n { tone: \"neutral\", appearance: \"soft\", class: \"bg-muted text-muted-foreground\" },\n // No primary-subtle-foreground token: plain `foreground` on this tint\n // measures 18.70 light / 9.78 dark, already graded for the Table's\n // highlighted row.\n { tone: \"primary\", appearance: \"soft\", class: \"bg-primary-subtle text-foreground\" },\n { tone: \"success\", appearance: \"soft\", class: \"bg-success-subtle text-success-foreground\" },\n { tone: \"warning\", appearance: \"soft\", class: \"bg-warning-subtle text-warning-foreground\" },\n {\n tone: \"danger\",\n appearance: \"soft\",\n class: \"bg-destructive-subtle text-destructive-subtle-foreground\",\n },\n { tone: \"info\", appearance: \"soft\", class: \"bg-info-subtle text-info-foreground\" },\n\n /*\n * OUTLINE -- no fill, so the border is the only thing separating the\n * badge from the page and is held to the 3:1 non-text floor of WCAG\n * 1.4.11. Text AND border are `-strong`, not `-foreground`: with no tint\n * behind it the label sits on `background`, and `-strong on background`\n * is the pair already graded at 4.5. `-foreground` is graded only\n * against its own `-subtle`, so using it here would have been a ratio\n * nobody had measured.\n */\n { tone: \"neutral\", appearance: \"outline\", class: \"border-border text-foreground\" },\n { tone: \"primary\", appearance: \"outline\", class: \"border-primary-strong text-foreground\" },\n {\n tone: \"success\",\n appearance: \"outline\",\n class: \"border-success-strong text-success-strong\",\n },\n {\n tone: \"warning\",\n appearance: \"outline\",\n class: \"border-warning-strong text-warning-strong\",\n },\n {\n tone: \"danger\",\n appearance: \"outline\",\n class: \"border-destructive-strong text-destructive-strong\",\n },\n { tone: \"info\", appearance: \"outline\", class: \"border-info-strong text-info-strong\" },\n ],\n defaultVariants: { tone: \"neutral\", appearance: \"soft\", size: \"md\", shape: \"rounded\" },\n },\n);\n\nexport interface BadgeProps\n extends ComponentPropsWithoutRef<\"span\">, VariantProps<typeof badgeVariants> {\n /**\n * A leading icon. A different icon component swaps in place (the new one grows in\n * from small and blurred); the same one re-rendering stays still. Mark it\n * `aria-hidden` when the label already says what it means.\n */\n icon?: ReactNode;\n}\n\n/** The name an icon swaps under: a different component swaps; the same one re-rendering does not. */\nfunction iconKey(icon: ReactNode) {\n if (!isValidElement(icon)) return \"icon\";\n const type = icon.type as string | { displayName?: string; name?: string };\n return typeof type === \"string\" ? type : (type.displayName ?? type.name ?? \"icon\");\n}\n\nexport const Badge = forwardRef<HTMLSpanElement, BadgeProps>(function Badge(\n { className, tone, appearance, size, shape, icon, children, ...props },\n ref,\n) {\n const content = useRef<HTMLSpanElement>(null);\n const text = typeof children === \"string\" || typeof children === \"number\" ? String(children) : null;\n const glyph = icon === undefined || icon === null ? \"\" : iconKey(icon);\n // The pill follows its content: a new label or icon swaps in place while the width springs to the new size.\n useMorphWidth(content, `${text ?? \"\"}|${glyph}`);\n return (\n <span\n ref={ref}\n {...props}\n data-slot=\"badge\"\n data-tone={tone ?? \"neutral\"}\n className={cn(badgeVariants({ tone, appearance, size, shape }), className)}\n >\n <span className=\"button-slot\">\n <span ref={content} className=\"inline-flex flex-none items-center gap-1\">\n {glyph ? (\n <SwapText swapKey={glyph} motion=\"glyph\" className=\"inline-flex items-center\">\n {icon}\n </SwapText>\n ) : null}\n {text === null ? (\n children\n ) : (\n <SwapText swapKey={text} className=\"inline-block\">\n {text}\n </SwapText>\n )}\n </span>\n </span>\n </span>\n );\n});\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAiDA,MAAa,gBAAgB,IAC3B;CACE;CACA;CAKA;CACA;CAKA;CACA;CAEA;AACF,GACA;CACE,UAAU;;EAER,MAAM;GACJ,SAAS;GACT,SAAS;GACT,SAAS;GACT,SAAS;GACT,QAAQ;GACR,MAAM;EACR;;EAEA,YAAY;GACV,OAAO;GACP,MAAM;GACN,SAAS;EACX;EACA,MAAM;GACJ,IAAI;GACJ,IAAI;GACJ,IAAI;EACN;EAOA,OAAO;GAAE,SAAS;GAAc,MAAM;EAAe;CACvD;CACA,kBAAkB;EAUhB;GAAE,MAAM;GAAW,YAAY;GAAS,OAAO;EAAsC;EACrF;GAAE,MAAM;GAAW,YAAY;GAAS,OAAO;EAAqC;EACpF;GAAE,MAAM;GAAW,YAAY;GAAS,OAAO;EAAmD;EAClG;GAAE,MAAM;GAAW,YAAY;GAAS,OAAO;EAAmD;EAClG;GAAE,MAAM;GAAU,YAAY;GAAS,OAAO;EAA6C;EAC3F;GAAE,MAAM;GAAQ,YAAY;GAAS,OAAO;EAA6C;EAUzF;GAAE,MAAM;GAAW,YAAY;GAAQ,OAAO;EAAiC;EAI/E;GAAE,MAAM;GAAW,YAAY;GAAQ,OAAO;EAAoC;EAClF;GAAE,MAAM;GAAW,YAAY;GAAQ,OAAO;EAA4C;EAC1F;GAAE,MAAM;GAAW,YAAY;GAAQ,OAAO;EAA4C;EAC1F;GACE,MAAM;GACN,YAAY;GACZ,OAAO;EACT;EACA;GAAE,MAAM;GAAQ,YAAY;GAAQ,OAAO;EAAsC;EAWjF;GAAE,MAAM;GAAW,YAAY;GAAW,OAAO;EAAgC;EACjF;GAAE,MAAM;GAAW,YAAY;GAAW,OAAO;EAAwC;EACzF;GACE,MAAM;GACN,YAAY;GACZ,OAAO;EACT;EACA;GACE,MAAM;GACN,YAAY;GACZ,OAAO;EACT;EACA;GACE,MAAM;GACN,YAAY;GACZ,OAAO;EACT;EACA;GAAE,MAAM;GAAQ,YAAY;GAAW,OAAO;EAAsC;CACtF;CACA,iBAAiB;EAAE,MAAM;EAAW,YAAY;EAAQ,MAAM;EAAM,OAAO;CAAU;AACvF,CACF;;AAaA,SAAS,QAAQ,MAAiB;CAChC,IAAI,CAAC,eAAe,IAAI,GAAG,OAAO;CAClC,MAAM,OAAO,KAAK;CAClB,OAAO,OAAO,SAAS,WAAW,OAAQ,KAAK,eAAe,KAAK,QAAQ;AAC7E;AAEA,MAAa,QAAQ,WAAwC,SAAS,MACpE,EAAE,WAAW,MAAM,YAAY,MAAM,OAAO,MAAM,UAAU,GAAG,SAC/D,KACA;CACA,MAAM,UAAU,OAAwB,IAAI;CAC5C,MAAM,OAAO,OAAO,aAAa,YAAY,OAAO,aAAa,WAAW,OAAO,QAAQ,IAAI;CAC/F,MAAM,QAAQ,SAAS,KAAA,KAAa,SAAS,OAAO,KAAK,QAAQ,IAAI;CAErE,cAAc,SAAS,GAAG,QAAQ,GAAG,GAAG,OAAO;CAC/C,OACE,oBAAC,QAAD;EACO;EACL,GAAI;EACJ,aAAU;EACV,aAAW,QAAQ;EACnB,WAAW,GAAG,cAAc;GAAE;GAAM;GAAY;GAAM;EAAM,CAAC,GAAG,SAAS;EAEzE,UAAA,oBAAC,QAAD;GAAM,WAAU;GACd,UAAA,qBAAC,QAAD;IAAM,KAAK;IAAS,WAAU;IAA9B,UAAA,CACG,QACC,oBAAC,UAAD;KAAU,SAAS;KAAO,QAAO;KAAQ,WAAU;KAChD,UAAA;IACO,CAAA,IACR,MACH,SAAS,OACR,WAEA,oBAAC,UAAD;KAAU,SAAS;KAAM,WAAU;KAChC,UAAA;IACO,CAAA,CAER;;EACF,CAAA;CACF,CAAA;AAEV,CAAC"}
|
|
1
|
+
{"version":3,"file":"badge.js","names":[],"sources":["../../src/badge/badge.tsx"],"sourcesContent":["\"use client\";\n\nimport { cva, type VariantProps } from \"class-variance-authority\";\nimport {\n forwardRef,\n isValidElement,\n useRef,\n type ComponentPropsWithoutRef,\n type ReactNode,\n} from \"react\";\n\nimport { cn } from \"../lib/cn.js\";\nimport { useMorphWidth } from \"../lib/motion.js\";\nimport { SwapText } from \"../lib/swap-text.js\";\n\n/**\n * Badge -- the status pill, in every shape the product asks for.\n *\n * Presentational, so there is no React Aria base. RAC's `Tag` looks similar\n * and is not equivalent: it is focusable, selectable and removable, and it is\n * illegal outside a `TagGroup` collection. A status pill is none of those.\n *\n * THIS ABSORBED THE PRODUCT'S `Pill`. The app shipped two components for one\n * job -- `badge.tsx` with a `variant` axis and `pill.tsx` with a `tone` axis --\n * across 22 and 22 consumer files. They disagreed about everything: shape\n * (`rounded-md` against `rounded-full`), size (one fixed, one `sm`/`md`/`lg`)\n * and naming, so `pill` accepted `blue` AND `info`, `green` AND `success`, for\n * the same colour. Converging them is why `variant` is gone.\n *\n * The axes are the two questions a call site actually answers -- what does\n * this MEAN (`tone`) and how loud should it be (`appearance`) -- rather than\n * one axis whose values are the cross product. Six tones by three appearances\n * is eighteen `variant` values otherwise, and shipping eighteen is how a\n * design system ends up with `softPrimary` next to `primaryOutline`.\n *\n * Every string below is a STATIC literal, including the compound ones.\n * Tailwind scans source text, so a class assembled at runtime is invisible to\n * it and would never be emitted.\n *\n * Three things in the legacy are deliberately not ported:\n *\n * - `asChild` and its `[a&]:hover:` rules. Zero call sites; the hover styles\n * existed only to colour the anchor asChild produced.\n * - `focus-visible:ring-*`. A span is not focusable, so the ring could never\n * appear. A consumer who adds `tabIndex` owns the indicator, and a ring is\n * the wrong one anyway -- box-shadows vanish under forced-colors.\n * - `aria-invalid:*`. A badge is never invalid; that was boilerplate copied\n * across every shadcn control.\n */\nexport const badgeVariants = cva(\n [\n \"inline-flex w-fit shrink-0 items-center justify-center gap-1\",\n \"border-(length:--border-width) border-transparent\",\n // NO leading-* here. It lives on each `size` instead, AFTER that size's\n // text-* class: tailwind-merge treats a later text-* as owning\n // line-height, so a leading-* in this base string is silently stripped the\n // moment a size variant adds one. The NoUaLeak story caught exactly that.\n \"font-sans font-medium\",\n \"whitespace-nowrap\",\n // box-border and font-sans are not boilerplate. The reset in\n // @seekalfred/styles is OPT-IN, so a consumer may load none: an unstyled\n // span inherits the host's serif, and content-box would add the padding\n // to any width a consumer sets.\n \"box-border\",\n \"[&_svg]:pointer-events-none [&_svg]:size-3\",\n // Colour, border and fill ease over 240ms as the tone changes.\n \"motion-safe:transition-colors motion-safe:duration-(--duration-standard) motion-safe:ease-settle\",\n ],\n {\n variants: {\n /** What the badge MEANS. Colour follows from it; it is never picked directly. */\n tone: {\n neutral: \"\",\n primary: \"\",\n success: \"\",\n warning: \"\",\n danger: \"\",\n info: \"\",\n },\n /** How loud it is. */\n appearance: {\n solid: \"\",\n soft: \"\",\n outline: \"bg-transparent\",\n },\n size: {\n sm: \"px-2 py-0.5 text-xs leading-(--leading-snug)\",\n md: \"px-2.5 py-0.5 text-xs leading-(--leading-snug)\",\n lg: \"px-3 py-1 text-sm leading-(--leading-snug)\",\n },\n /*\n * The product's two components disagreed on this and nothing else:\n * `badge.tsx` was rounded-md, `pill.tsx` was rounded-full. Both survive\n * as one axis rather than one winning, because a count-bearing badge and\n * a verdict pill really do read differently.\n */\n shape: { rounded: \"rounded-md\", pill: \"rounded-full\" },\n /*\n * Opt-in wrapping, for a tag in a table cell or a narrow column. Off, the\n * badge is one line that never shrinks. On, it may shrink to its container\n * and break the label across lines (long unbroken words too), the text\n * starts at the leading edge, and the box grows in height. The base\n * `whitespace-nowrap`, `w-fit` and `shrink-0` lose to these by merge order\n * (cn runs tailwind-merge over our own classes).\n */\n wrap: {\n true: \"min-w-0 max-w-full shrink whitespace-normal break-words text-start\",\n false: \"\",\n },\n },\n compoundVariants: [\n // SOLID -- the fill carries the meaning, so the text must clear 4.5:1 on it.\n /*\n * muted-foreground, NOT `secondary`. --color-secondary resolves to\n * periwinkle.400, so a \"neutral\" badge rendered lavender -- caught in a\n * browser, not by any assertion, because the story compared it against\n * the same wrong token it was painted with. A tone called neutral has to\n * be neutral; the periwinkle fill the product uses for its `secondary`\n * badge is the brand's SECOND colour, not an absence of one.\n */\n { tone: \"neutral\", appearance: \"solid\", class: \"bg-muted-foreground text-background\" },\n { tone: \"primary\", appearance: \"solid\", class: \"bg-primary text-primary-foreground\" },\n { tone: \"success\", appearance: \"solid\", class: \"bg-success-strong text-success-strong-foreground\" },\n { tone: \"warning\", appearance: \"solid\", class: \"bg-warning-strong text-warning-strong-foreground\" },\n { tone: \"danger\", appearance: \"solid\", class: \"bg-destructive text-destructive-foreground\" },\n { tone: \"info\", appearance: \"solid\", class: \"bg-info-strong text-info-strong-foreground\" },\n\n /*\n * SOFT -- a tint, and the default, because this is what a status pill is.\n * The ink is one step darker than the obvious pairing for success,\n * warning and info: the product measured 4.13, 3.48 and 3.34 on its own\n * tints and had to darken all three. Our `-foreground` tokens already\n * encode that correction, which is why they are used rather than\n * `-strong`.\n */\n { tone: \"neutral\", appearance: \"soft\", class: \"bg-muted text-muted-foreground\" },\n // No primary-subtle-foreground token: plain `foreground` on this tint\n // measures 18.70 light / 9.78 dark, already graded for the Table's\n // highlighted row.\n { tone: \"primary\", appearance: \"soft\", class: \"bg-primary-subtle text-foreground\" },\n { tone: \"success\", appearance: \"soft\", class: \"bg-success-subtle text-success-foreground\" },\n { tone: \"warning\", appearance: \"soft\", class: \"bg-warning-subtle text-warning-foreground\" },\n {\n tone: \"danger\",\n appearance: \"soft\",\n class: \"bg-destructive-subtle text-destructive-subtle-foreground\",\n },\n { tone: \"info\", appearance: \"soft\", class: \"bg-info-subtle text-info-foreground\" },\n\n /*\n * OUTLINE -- no fill, so the border is the only thing separating the\n * badge from the page and is held to the 3:1 non-text floor of WCAG\n * 1.4.11. Text AND border are `-strong`, not `-foreground`: with no tint\n * behind it the label sits on `background`, and `-strong on background`\n * is the pair already graded at 4.5. `-foreground` is graded only\n * against its own `-subtle`, so using it here would have been a ratio\n * nobody had measured.\n */\n { tone: \"neutral\", appearance: \"outline\", class: \"border-border text-foreground\" },\n { tone: \"primary\", appearance: \"outline\", class: \"border-primary-strong text-foreground\" },\n {\n tone: \"success\",\n appearance: \"outline\",\n class: \"border-success-strong text-success-strong\",\n },\n {\n tone: \"warning\",\n appearance: \"outline\",\n class: \"border-warning-strong text-warning-strong\",\n },\n {\n tone: \"danger\",\n appearance: \"outline\",\n class: \"border-destructive-strong text-destructive-strong\",\n },\n { tone: \"info\", appearance: \"outline\", class: \"border-info-strong text-info-strong\" },\n ],\n defaultVariants: { tone: \"neutral\", appearance: \"soft\", size: \"md\", shape: \"rounded\", wrap: false },\n },\n);\n\nexport interface BadgeProps\n extends ComponentPropsWithoutRef<\"span\">, VariantProps<typeof badgeVariants> {\n /**\n * A leading icon. A different icon component swaps in place (the new one grows in\n * from small and blurred); the same one re-rendering stays still. Mark it\n * `aria-hidden` when the label already says what it means.\n */\n icon?: ReactNode;\n /**\n * Let the label wrap onto more lines instead of staying on one. For a tag in\n * a table cell or a narrow column: the badge shrinks to its container, the\n * label breaks (even a long word with no spaces), the text starts at the\n * leading edge and the height grows with the lines. Off by default, and then\n * a badge is one line exactly as before. With a long label prefer\n * `shape=\"rounded\"`: a multi-line `pill` has very round ends.\n *\n * The width does not spring to a new label while this is on (that morph\n * measures the unwrapped width).\n *\n * @default false\n */\n wrap?: boolean;\n}\n\n/** The name an icon swaps under: a different component swaps; the same one re-rendering does not. */\nfunction iconKey(icon: ReactNode) {\n if (!isValidElement(icon)) return \"icon\";\n const type = icon.type as string | { displayName?: string; name?: string };\n return typeof type === \"string\" ? type : (type.displayName ?? type.name ?? \"icon\");\n}\n\nexport const Badge = forwardRef<HTMLSpanElement, BadgeProps>(function Badge(\n { className, tone, appearance, size, shape, wrap = false, icon, children, ...props },\n ref,\n) {\n const content = useRef<HTMLSpanElement>(null);\n const text = typeof children === \"string\" || typeof children === \"number\" ? String(children) : null;\n const glyph = icon === undefined || icon === null ? \"\" : iconKey(icon);\n // The pill follows its content: a new label or icon swaps in place while the width springs to the new size.\n useMorphWidth(content, `${text ?? \"\"}|${glyph}`, !wrap);\n return (\n <span\n ref={ref}\n {...props}\n data-slot=\"badge\"\n data-tone={tone ?? \"neutral\"}\n data-wrap={wrap || undefined}\n className={cn(badgeVariants({ tone, appearance, size, shape, wrap }), className)}\n >\n <span className=\"button-slot\">\n <span\n ref={content}\n className={wrap ? \"inline-flex min-w-0 items-center gap-1\" : \"inline-flex flex-none items-center gap-1\"}\n >\n {glyph ? (\n <SwapText swapKey={glyph} motion=\"glyph\" className=\"inline-flex items-center\">\n {icon}\n </SwapText>\n ) : null}\n {text === null ? (\n children\n ) : (\n <SwapText swapKey={text} className=\"inline-block\">\n {text}\n </SwapText>\n )}\n </span>\n </span>\n </span>\n );\n});\n"],"mappings":"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"}
|
package/dist/button/button.d.ts
CHANGED
|
@@ -3,8 +3,9 @@ import { ReactNode } from "react";
|
|
|
3
3
|
import { ButtonProps, LinkProps } from "react-aria-components";
|
|
4
4
|
//#region src/button/button.d.ts
|
|
5
5
|
export declare const buttonVariants: (props?: ({
|
|
6
|
-
variant?: "
|
|
7
|
-
size?: "icon" | "sm" | "md" | "lg" | null | undefined;
|
|
6
|
+
variant?: "link" | "outline" | "primary" | "ai" | "secondary" | "outlineNeutral" | "ghost" | "destructive" | null | undefined;
|
|
7
|
+
size?: "icon" | "sm" | "md" | "lg" | "icon-sm" | null | undefined;
|
|
8
|
+
wrap?: boolean | null | undefined;
|
|
8
9
|
} & import("class-variance-authority/types").ClassProp) | undefined) => string;
|
|
9
10
|
/**
|
|
10
11
|
* Props.
|
|
@@ -37,10 +38,29 @@ interface ButtonProps$1 extends Omit<ButtonProps, "className" | "style">, Varian
|
|
|
37
38
|
*
|
|
38
39
|
* NOT the route for `size="icon"`. An icon-only button passes its glyph as
|
|
39
40
|
* `children`, which is what gives it its single centred child.
|
|
41
|
+
*
|
|
42
|
+
* `variant="ai"` supplies a wand here when this is `undefined`. Pass your own
|
|
43
|
+
* node to replace it, or `false` (or `null`) for no icon at all. The wand is
|
|
44
|
+
* not added to `size="icon"`, whose glyph is `children`.
|
|
40
45
|
*/
|
|
41
46
|
startIcon?: ReactNode;
|
|
42
47
|
/** Icon rendered after the label. Same rules as {@link ButtonProps.startIcon}. */
|
|
43
48
|
endIcon?: ReactNode;
|
|
49
|
+
/**
|
|
50
|
+
* Let the label wrap onto more lines. For a button whose text is long and
|
|
51
|
+
* not ours (a prompt, a name): it shrinks to its container, breaks the label
|
|
52
|
+
* (even a long word with no spaces), starts the text at the leading edge, and
|
|
53
|
+
* grows taller than `size`'s height, which becomes its minimum. Off by
|
|
54
|
+
* default, and then a button is one line at a fixed height exactly as before.
|
|
55
|
+
* Pass `className="justify-start"` to start a link-style button at the
|
|
56
|
+
* leading edge of its row. A wrapping button's width does not spring when its
|
|
57
|
+
* label changes.
|
|
58
|
+
*
|
|
59
|
+
* Not for `size="icon"` or `"icon-sm"`, which have no label.
|
|
60
|
+
*
|
|
61
|
+
* @default false
|
|
62
|
+
*/
|
|
63
|
+
wrap?: boolean;
|
|
44
64
|
/**
|
|
45
65
|
* Makes the button a link. It renders React Aria's `Link` -- a real `<a>`,
|
|
46
66
|
* announced as a link -- with exactly the classes a button of the same
|
|
@@ -84,6 +104,8 @@ interface ButtonProps$1 extends Omit<ButtonProps, "className" | "style">, Varian
|
|
|
84
104
|
* <Button startIcon={<SaveIcon aria-hidden />}>Save changes</Button>
|
|
85
105
|
* <Button endIcon={<ChevronIcon aria-hidden />}>Continue</Button>
|
|
86
106
|
* <Button variant="outline" size="icon" aria-label="Close"><XIcon /></Button>
|
|
107
|
+
* <Button variant="ghost" size="icon-sm" aria-label="More information"><InfoIcon /></Button>
|
|
108
|
+
* <Button variant="link" wrap className="justify-start">A long prompt that may run over several lines</Button>
|
|
87
109
|
* <Button href="/reports" variant="outlineNeutral">View reports</Button>
|
|
88
110
|
*/
|
|
89
111
|
declare const Button$1: import("react").ForwardRefExoticComponent<ButtonProps$1 & import("react").RefAttributes<HTMLButtonElement>>;
|
package/dist/button/button.js
CHANGED
|
@@ -55,13 +55,15 @@ const buttonVariants = cva([
|
|
|
55
55
|
"data-[disabled]:opacity-(--opacity-button-disabled)"
|
|
56
56
|
],
|
|
57
57
|
ai: [
|
|
58
|
-
"bg-primary text-primary-foreground shadow-surface",
|
|
59
|
-
"ai-beam",
|
|
58
|
+
"bg-primary bg-(image:--gradient-ai) text-primary-foreground shadow-surface",
|
|
60
59
|
"motion-safe:button-motion",
|
|
61
60
|
pressScale,
|
|
62
|
-
"data-[hovered]:bg-
|
|
61
|
+
"data-[hovered]:bg-(image:--gradient-ai-hover) data-[hovered]:shadow-resting",
|
|
62
|
+
"data-[pressed]:bg-(image:--gradient-ai-pressed)",
|
|
63
63
|
"motion-safe:data-[hovered]:[&_svg]:-rotate-12",
|
|
64
|
-
"data-[
|
|
64
|
+
"data-[focus-visible]:outline-0 data-[focus-visible]:border-primary-foreground",
|
|
65
|
+
"forced-colors:data-[focus-visible]:outline-(length:--focus-ring-width)",
|
|
66
|
+
"data-[disabled]:opacity-100 data-[disabled]:bg-none data-[disabled]:bg-muted data-[disabled]:text-muted-foreground data-[disabled]:border-border data-[disabled]:shadow-none"
|
|
65
67
|
],
|
|
66
68
|
secondary: [
|
|
67
69
|
"bg-surface text-foreground border-(length:--border-width) border-border shadow-surface",
|
|
@@ -116,12 +118,35 @@ const buttonVariants = cva([
|
|
|
116
118
|
sm: "h-(--density-control-h-sm) px-4 text-sm",
|
|
117
119
|
md: "h-(--density-control-h-md) px-4 text-sm",
|
|
118
120
|
lg: "h-(--density-control-h-lg) px-(--density-control-pad-lg) text-lg",
|
|
119
|
-
icon: "size-(--density-control-h-md) px-0 text-base"
|
|
121
|
+
icon: "size-(--density-control-h-md) px-0 text-base",
|
|
122
|
+
"icon-sm": "size-6 px-0 text-base [&_svg:not([class*='size-'])]:size-3.5"
|
|
123
|
+
},
|
|
124
|
+
wrap: {
|
|
125
|
+
true: "h-auto max-w-full min-w-0 shrink whitespace-normal break-words text-start",
|
|
126
|
+
false: ""
|
|
120
127
|
}
|
|
121
128
|
},
|
|
129
|
+
compoundVariants: [
|
|
130
|
+
{
|
|
131
|
+
size: "sm",
|
|
132
|
+
wrap: true,
|
|
133
|
+
class: "min-h-(--density-control-h-sm) py-1.5"
|
|
134
|
+
},
|
|
135
|
+
{
|
|
136
|
+
size: "md",
|
|
137
|
+
wrap: true,
|
|
138
|
+
class: "min-h-(--density-control-h-md) py-2"
|
|
139
|
+
},
|
|
140
|
+
{
|
|
141
|
+
size: "lg",
|
|
142
|
+
wrap: true,
|
|
143
|
+
class: "min-h-(--density-control-h-lg) py-2.5"
|
|
144
|
+
}
|
|
145
|
+
],
|
|
122
146
|
defaultVariants: {
|
|
123
147
|
variant: "primary",
|
|
124
|
-
size: "md"
|
|
148
|
+
size: "md",
|
|
149
|
+
wrap: false
|
|
125
150
|
}
|
|
126
151
|
});
|
|
127
152
|
/** A key for the label content: text plus element names, so a new label or icon crossfades while prop-only updates stay in place. */
|
|
@@ -177,12 +202,12 @@ function Loader({ show }) {
|
|
|
177
202
|
* Nothing animates on first mount, and a prop-only update (same key) stays in
|
|
178
203
|
* place.
|
|
179
204
|
*/
|
|
180
|
-
function ButtonContent({ children, pending }) {
|
|
205
|
+
function ButtonContent({ children, pending, wrap }) {
|
|
181
206
|
const key = labelKey(children);
|
|
182
207
|
const icon = isIconKey(key);
|
|
183
208
|
const contentRef = useRef(null);
|
|
184
209
|
const phaseRef = useRef(null);
|
|
185
|
-
useMorphWidth(contentRef, key);
|
|
210
|
+
useMorphWidth(contentRef, key, !wrap);
|
|
186
211
|
const { leaving, changed, remove } = useLeavingItems(key, {
|
|
187
212
|
node: children,
|
|
188
213
|
icon
|
|
@@ -191,7 +216,7 @@ function ButtonContent({ children, pending }) {
|
|
|
191
216
|
className: cn("button-slot", pending && "opacity-0"),
|
|
192
217
|
children: /* @__PURE__ */ jsxs("span", {
|
|
193
218
|
ref: contentRef,
|
|
194
|
-
className: "inline-flex flex-none items-center",
|
|
219
|
+
className: wrap ? "inline-flex min-w-0 items-center" : "inline-flex flex-none items-center",
|
|
195
220
|
children: [leaving.map((item) => /* @__PURE__ */ jsx(LeavingPhase, {
|
|
196
221
|
phase: {
|
|
197
222
|
id: item.id,
|
|
@@ -203,13 +228,32 @@ function ButtonContent({ children, pending }) {
|
|
|
203
228
|
}, item.id)), /* @__PURE__ */ jsx("span", {
|
|
204
229
|
ref: phaseRef,
|
|
205
230
|
"data-slot": "button-label",
|
|
206
|
-
className: cn("inline-flex items-center justify-center gap-(--density-control-gap)
|
|
231
|
+
className: cn(["inline-flex items-center justify-center gap-(--density-control-gap) [&_svg]:shrink-0", wrap ? "min-w-0 whitespace-normal" : "whitespace-nowrap"], changed && (icon ? "motion-safe:animate-glyph-in motion-reduce:animate-fade-in" : "motion-safe:animate-label-in motion-reduce:animate-fade-in")),
|
|
207
232
|
children
|
|
208
233
|
}, key)]
|
|
209
234
|
})
|
|
210
235
|
})] });
|
|
211
236
|
}
|
|
212
237
|
/**
|
|
238
|
+
* The AI button's wand: a 24px outline glyph in the same stroke style as the
|
|
239
|
+
* library's other inline icons (the package ships no icon dependency). Drawn
|
|
240
|
+
* after lucide's "wand-sparkles", which is what the app's `Wand2` is.
|
|
241
|
+
*/
|
|
242
|
+
function WandIcon() {
|
|
243
|
+
return /* @__PURE__ */ jsxs("svg", {
|
|
244
|
+
viewBox: "0 0 24 24",
|
|
245
|
+
fill: "none",
|
|
246
|
+
stroke: "currentColor",
|
|
247
|
+
strokeWidth: "1.75",
|
|
248
|
+
strokeLinecap: "round",
|
|
249
|
+
strokeLinejoin: "round",
|
|
250
|
+
"aria-hidden": "true",
|
|
251
|
+
"data-slot": "button-wand",
|
|
252
|
+
children: [/* @__PURE__ */ jsx("path", { d: "m21.64 3.64-1.28-1.28a1.21 1.21 0 0 0-1.72 0L2.36 18.64a1.21 1.21 0 0 0 0 1.72l1.28 1.28a1.2 1.2 0 0 0 1.72 0L21.64 5.36a1.2 1.2 0 0 0 0-1.72" }), /* @__PURE__ */ jsx("path", { d: "m14 7 3 3M5 6v4M19 14v4M10 2v2M7 8H3M21 16h-4M11 3H9" })]
|
|
253
|
+
});
|
|
254
|
+
}
|
|
255
|
+
WandIcon.displayName = "WandIcon";
|
|
256
|
+
/**
|
|
213
257
|
* A button.
|
|
214
258
|
*
|
|
215
259
|
* A label that changes (a different text, or a different icon) crossfades, and
|
|
@@ -228,18 +272,23 @@ function ButtonContent({ children, pending }) {
|
|
|
228
272
|
* <Button startIcon={<SaveIcon aria-hidden />}>Save changes</Button>
|
|
229
273
|
* <Button endIcon={<ChevronIcon aria-hidden />}>Continue</Button>
|
|
230
274
|
* <Button variant="outline" size="icon" aria-label="Close"><XIcon /></Button>
|
|
275
|
+
* <Button variant="ghost" size="icon-sm" aria-label="More information"><InfoIcon /></Button>
|
|
276
|
+
* <Button variant="link" wrap className="justify-start">A long prompt that may run over several lines</Button>
|
|
231
277
|
* <Button href="/reports" variant="outlineNeutral">View reports</Button>
|
|
232
278
|
*/
|
|
233
|
-
const Button$1 = forwardRef(function Button$2({ className, variant, size, startIcon, endIcon, children, href, target, rel, download, ...props }, ref) {
|
|
279
|
+
const Button$1 = forwardRef(function Button$2({ className, variant, size, wrap, startIcon, endIcon, children, href, target, rel, download, ...props }, ref) {
|
|
234
280
|
const classes = cn(buttonVariants({
|
|
235
281
|
variant,
|
|
236
|
-
size
|
|
282
|
+
size,
|
|
283
|
+
wrap
|
|
237
284
|
}), className);
|
|
285
|
+
const leadingIcon = startIcon === void 0 && variant === "ai" && size !== "icon" ? /* @__PURE__ */ jsx(WandIcon, {}) : startIcon;
|
|
238
286
|
const button = useAriaBusy(props.isPending === true);
|
|
239
287
|
const content = (renderProps) => /* @__PURE__ */ jsxs(ButtonContent, {
|
|
240
288
|
pending: renderProps.isPending,
|
|
289
|
+
wrap: wrap === true,
|
|
241
290
|
children: [
|
|
242
|
-
|
|
291
|
+
leadingIcon,
|
|
243
292
|
typeof children === "function" ? children(renderProps) : children,
|
|
244
293
|
endIcon
|
|
245
294
|
]
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"button.js","names":["Button","AriaLink","AriaButton"],"sources":["../../src/button/button.tsx"],"sourcesContent":["\"use client\";\n\nimport { cva, type VariantProps } from \"class-variance-authority\";\nimport {\n forwardRef,\n isValidElement,\n useCallback,\n useRef,\n useState,\n type ForwardedRef,\n type ReactNode,\n type RefObject,\n} from \"react\";\nimport {\n Button as AriaButton,\n Link as AriaLink,\n type ButtonProps as AriaButtonProps,\n type ButtonRenderProps,\n type LinkProps as AriaLinkProps,\n type PressEvent,\n} from \"react-aria-components\";\n\nimport { cn } from \"../lib/cn.js\";\nimport { mergeRefs } from \"../lib/merge-refs.js\";\nimport {\n anchorsLayer,\n pressScaleFor,\n useAriaBusy,\n useExitBackstop,\n useLeavingItems,\n useMorphWidth,\n type Rect,\n} from \"../lib/motion.js\";\n\n/**\n * Variant classes, exported so a consumer can reuse the exact classes we ship\n * rather than guessing at them.\n *\n * NOT the answer for a link-shaped button any more: pass `href` to Button. On a\n * consumer's own <a> these classes get none of their states -- hover, press and\n * the focus ring all key on React Aria data attributes that a plain anchor never\n * receives, and the base sets outline-none, so keyboard focus was invisible.\n *\n * Ported from the legacy shadcn Button. The variant and size vocabulary is\n * deliberately identical so existing call sites migrate by rename, with six\n * corrections that the Alfred contracts require -- each marked below.\n *\n * Every string here is a STATIC literal. Tailwind scans source text, so a class\n * assembled at runtime is invisible to it and would simply never be emitted.\n *\n * State comes from React Aria data attributes rather than pseudo-classes:\n * data-[focus-visible] is keyboard focus only, and data-[pressed] covers mouse,\n * touch and keyboard uniformly. The original had no pressed state at all.\n */\n/*\n * The press: the button scales down while pressed (see onPressStart for the\n * scale, which depends on width) and springs back on release. Anchors of\n * floating layers never scale: a menu that opens on pointerdown would measure a\n * shrunken trigger.\n */\nconst pressScale =\n \"motion-safe:data-[pressed]:scale-(--press-scale) [&:is([aria-haspopup]:not([aria-haspopup=false]),[data-state],[role=combobox])]:scale-none!\";\n\nexport const buttonVariants = cva(\n [\n \"inline-flex items-center justify-center gap-(--density-control-gap) shrink-0 whitespace-nowrap\",\n // No leading-* here on purpose. tailwind-merge treats a later text-* class\n // as owning line-height (a Tailwind v3 assumption, where text-base carried\n // one), so a leading-* in the base string is silently STRIPPED by cn() once\n // a size variant adds text-base. It is not needed either: the control has a\n // fixed height and centres with flex, so line-height cannot overflow it.\n // A component with auto height must put leading-* AFTER its text-* class.\n \"rounded-control font-sans font-medium\",\n \"cursor-pointer select-none\",\n\n // font-sans above is part of the same problem: a <button> does not inherit\n // the page font, so without a reset it renders in the user-agent face\n // (Arial in Chromium) rather than the design system's. The same applies to\n // bg-transparent: an unstyled <button> paints the user-agent ButtonFace\n // grey -- which is what surfaces the moment a fill class fails to resolve,\n // and is why the paint stories compare against tokens rather than checking\n // for transparency.\n //\n // The reset in @seekalfred/styles is OPT-IN, so a consumer may never load\n // one. A bare <button> then keeps its user-agent border (2px, black, in\n // Chromium) and inline padding, which is a black box around every variant\n // that does not set a border of its own. The original inherited a zeroed\n // border from Tailwind Preflight in the app; a library component cannot\n // assume the consumer supplied one.\n \"border-(length:--border-width) border-transparent bg-transparent\",\n\n // Icon handling, ported verbatim: an svg child is sized unless the caller\n // already said otherwise, and never swallows the press.\n \"[&_svg]:pointer-events-none [&_svg]:shrink-0\",\n \"[&_svg:not([class*='size-'])]:size-(--icon-sm)\",\n\n // CORRECTION 1 -- the original focused with `ring-[3px]`, which is a\n // box-shadow. Box-shadows are discarded under forced-colors, taking the\n // only focus indication with them, and axe has no rule that would catch it\n // (ARCHITECTURE section 5, rule 6). A real outline survives.\n //\n // outline-solid on the focus variant is load-bearing, not decoration.\n // `outline-none` sets BOTH outline-style:none and --tw-outline-style:none,\n // and Tailwind's outline-width utility resolves its style from that same\n // variable -- so the focus variant applied a width and a colour while\n // faithfully re-applying style:none, and painted nothing. The button was\n // keyboard-focusable with no visible ring. axe has no focus-indicator\n // rule, so every automated gate stayed green; the KeyboardFocus story now\n // asserts outlineStyle directly.\n \"outline-none\",\n \"data-[focus-visible]:outline-solid\",\n \"data-[focus-visible]:outline-(length:--focus-ring-width)\",\n \"data-[focus-visible]:outline-offset-(--focus-ring-offset)\",\n \"data-[focus-visible]:outline-(--color-ring)\",\n\n // CORRECTION 2 -- `disabled:pointer-events-none` also kills the cursor, so\n // the control reads as inert rather than as unavailable. React Aria already\n // blocks activation; this only has to say so.\n // The opacity half of the disabled look moved into each variant: the\n // outlined and ghost buttons stay fully opaque and mute their colours\n // instead, which a single base opacity cannot express.\n \"data-[disabled]:cursor-not-allowed\",\n \"[-webkit-tap-highlight-color:transparent]\",\n\n // React Aria's own pending state. The original had no equivalent.\n \"data-[pending]:cursor-progress\",\n ],\n {\n variants: {\n variant: {\n // CORRECTION 3 -- the legacy button filled this variant with the brand\n // gradient. It is flat here, because the brand system caps\n // --gradient-brand at one intentional element per view and keeps that\n // budget for the logo and progress bars: \"a gradient on every button\n // spends that budget everywhere. Ours is the flat primary.\"\n //\n // CORRECTION 4 -- the label is ink, not white. Brand orange #FF8431\n // carries white at 2.44:1; ink clears it at 8.35:1, which is the brand\n // system's own --text-on-orange rule. Its Button ships white only as a\n // dated, individually signed-off exception, and this library's contrast\n // contract is build-failing with no exemption mechanism to inherit it.\n //\n // Hover and pressed are their own TOKENS again (primary 400 and 600),\n // as the design spec (Oct 2026) sets them; an opacity step would make\n // the label pair depend on whatever sits behind the button. Both pairs\n // are in the contrast contract (contrast-pairs.json).\n primary: [\n \"bg-primary text-primary-foreground shadow-surface\",\n \"motion-safe:button-motion\",\n pressScale,\n // Design spec (Oct 2026): hover primary 400, pressed primary 600.\n \"data-[hovered]:bg-primary-hover data-[hovered]:shadow-resting\",\n \"data-[pressed]:bg-primary-pressed\",\n \"data-[disabled]:opacity-(--opacity-button-disabled)\",\n ],\n /*\n * The AI action (Generate, Seek Alfred). The primary fill with a beam\n * that runs around its edge in orange, lavender and teal; on hover the\n * beam speeds up, the button glows and an icon tilts. Disabled stops the\n * beam. See ai-beam in styles.css.\n */\n ai: [\n \"bg-primary text-primary-foreground shadow-surface\",\n \"ai-beam\",\n \"motion-safe:button-motion\",\n pressScale,\n \"data-[hovered]:bg-primary-hover\",\n \"motion-safe:data-[hovered]:[&_svg]:-rotate-12\",\n \"data-[disabled]:opacity-(--opacity-button-disabled)\",\n ],\n /*\n * Design spec (Oct 2026): Secondary is a white surface with a light\n * border and dark text -- the same paint as `outlineNeutral` below, kept\n * under this name so existing call sites need no rename. It used to be\n * the lavender fill; the lavender `secondary` role still exists for\n * badges and highlights, just not on a button.\n */\n secondary: [\n \"bg-surface text-foreground border-(length:--border-width) border-border shadow-surface\",\n \"motion-safe:button-motion\",\n pressScale,\n \"data-[hovered]:bg-muted data-[hovered]:shadow-resting\",\n \"data-[pressed]:bg-muted\",\n // A disabled secondary button keeps its shape and mutes its fill and\n // label, so it reads as a button that is off, not as plain text.\n \"data-[disabled]:opacity-100 data-[disabled]:bg-muted data-[disabled]:text-muted-foreground data-[disabled]:border-border\",\n ],\n // primary-strong, never primary: the brand orange is 2.44:1 on the page\n // and cannot legally be a label OR a border. That is the whole reason\n // the token exists.\n outline: [\n \"bg-surface text-primary-strong border-(length:--border-width) border-primary-strong shadow-surface\",\n \"motion-safe:button-motion\",\n pressScale,\n \"data-[hovered]:bg-muted data-[hovered]:shadow-resting\",\n \"data-[pressed]:bg-muted data-[pressed]:border-foreground\",\n \"data-[disabled]:opacity-100 data-[disabled]:bg-muted data-[disabled]:text-muted-foreground data-[disabled]:border-border\",\n ],\n /*\n * The NEUTRAL outline, and the product's single most-used button:\n * 99 call sites against 32 for the brand-coloured one.\n *\n * It exists because `outline` above is brand-toned -- orange border,\n * orange label -- which is what the product calls `secondaryOutline`.\n * Its plain `outline` is this: a grey border and ordinary body text,\n * for a button that must not compete with the primary action next to\n * it. One name could not cover both, and renaming `outline` would have\n * silently repainted every existing call site of ours instead.\n *\n * This is the design spec's Secondary button: surface, the light\n * `border` hairline and body text. The hairline is below 3:1 by UX\n * decision; the label, hover fill and focus ring carry the boundary.\n */\n outlineNeutral: [\n \"bg-surface text-foreground border-(length:--border-width) border-border shadow-surface\",\n \"motion-safe:button-motion\",\n pressScale,\n \"data-[hovered]:bg-muted data-[hovered]:shadow-resting\",\n \"data-[pressed]:bg-muted\",\n \"data-[disabled]:opacity-100 data-[disabled]:bg-muted data-[disabled]:text-text-disabled\",\n ],\n ghost: [\n \"bg-transparent text-text-secondary\",\n \"motion-safe:button-motion\",\n pressScale,\n \"data-[hovered]:bg-muted data-[hovered]:text-foreground\",\n \"data-[disabled]:opacity-100 data-[disabled]:text-text-disabled\",\n // CORRECTION 5 -- pressed must not be background-only. A ghost button\n // has no border at rest, so the pressed border is the non-colour cue\n // that survives forced-colors (rule 6 again).\n \"data-[pressed]:bg-muted data-[pressed]:border-primary-strong\",\n ],\n // Design spec (Oct 2026): an outlined Danger button, not a red fill.\n destructive: [\n \"bg-surface text-destructive-strong border-(length:--border-width) border-border shadow-surface\",\n \"motion-safe:button-motion\",\n pressScale,\n \"data-[hovered]:bg-muted data-[hovered]:border-destructive-strong\",\n \"data-[pressed]:bg-muted data-[pressed]:border-destructive-strong\",\n // Muted fill, label tinted toward destructive.\n \"data-[disabled]:opacity-100 data-[disabled]:bg-muted data-[disabled]:border-border data-[disabled]:text-[color-mix(in_oklab,var(--color-destructive)_55%,var(--color-muted))]\",\n ],\n link: [\n \"bg-transparent text-text-link underline-offset-(--underline-offset)\",\n \"motion-safe:button-motion\",\n \"data-[disabled]:text-text-disabled\",\n \"data-[hovered]:underline\",\n \"data-[pressed]:underline data-[pressed]:opacity-80\",\n // A trailing arrow moves 3px on hover.\n \"[&_svg:last-child]:transition-transform motion-safe:data-[hovered]:[&_svg:last-child]:translate-x-0.75 rtl:motion-safe:data-[hovered]:[&_svg:last-child]:-translate-x-0.75\",\n ],\n },\n size: {\n // Height, padding and label size all come from the control scale, so\n // data-density moves them together. The icon-only variant is square at\n // the md height. The legacy shadcn rule that tightened padding when a\n // child was an icon is gone: it needed three more magic numbers, the\n // brand system has no equivalent, and the gap token now owns icon\n // spacing.\n // Design spec (Oct 2026): 16px padding and 14px label at 36px and 44px.\n sm: \"h-(--density-control-h-sm) px-4 text-sm\",\n md: \"h-(--density-control-h-md) px-4 text-sm\",\n lg: \"h-(--density-control-h-lg) px-(--density-control-pad-lg) text-lg\",\n // text-base even though there is no visible label: without it the\n // user-agent default (13.33px in Chromium) leaks in and sizes any\n // em-based child against the wrong root.\n icon: \"size-(--density-control-h-md) px-0 text-base\",\n },\n },\n defaultVariants: {\n variant: \"primary\",\n size: \"md\",\n },\n },\n);\n\n/*\n * The label motion, without an animation library.\n *\n * A presence library would keep the outgoing label mounted for the crossfade\n * and drive a motion value for the width morph. CSS cannot keep an outgoing\n * label mounted or observe its own content width, so those two jobs are the\n * plain React below; every animation itself is a CSS keyframe or transition\n * (@seekalfred/styles motion.css, the button-* utilities in styles.css, the\n * --animate-* tokens).\n */\n\n/** A key for the label content: text plus element names, so a new label or icon crossfades while prop-only updates stay in place. */\nfunction labelKey(node: ReactNode): string {\n if (node == null || typeof node === \"boolean\") return \"\";\n if (typeof node === \"string\" || typeof node === \"number\" || typeof node === \"bigint\") return String(node);\n if (Array.isArray(node)) return node.map(labelKey).join(\"\");\n if (!isValidElement(node)) return \"\";\n const type = node.type as string | { displayName?: string; name?: string };\n return \"<\" + (typeof type === \"string\" ? type : (type?.displayName ?? type?.name ?? \"\")) + \">\" + labelKey((node.props as { children?: ReactNode }).children);\n}\n\n/** A label with no text once element names are stripped is an icon, which scales instead of rising. */\nconst isIconKey = (key: string) => !/\\S/.test(key.replace(/<[^>]*>/g, \"\"));\n\ninterface LeavingLabel {\n id: number;\n node: ReactNode;\n icon: boolean;\n rect: Rect;\n}\n\nfunction LeavingPhase({ phase, onDone }: { phase: LeavingLabel; onDone: (id: number) => void }) {\n const { id } = phase;\n const remove = useCallback(() => onDone(id), [onDone, id]);\n useExitBackstop(true, remove);\n return (\n <span\n aria-hidden=\"true\"\n onAnimationEnd={(event) => {\n if (event.target === event.currentTarget) remove();\n }}\n style={phase.rect}\n className={cn(\n \"absolute inline-flex items-center justify-center gap-(--density-control-gap) whitespace-nowrap [&_svg]:shrink-0\",\n phase.icon\n ? \"motion-safe:animate-glyph-out motion-reduce:animate-fade-out\"\n : \"motion-safe:animate-label-out motion-reduce:animate-fade-out\",\n )}\n >\n {phase.node}\n </span>\n );\n}\n\n/** The loading spinner: it scales in on the snappy spring, and out smaller with a little blur. Nothing animates if it is there on first mount. */\nfunction Loader({ show }: { show: boolean }) {\n const [initial] = useState(show);\n const [rendered, setRendered] = useState(show);\n const [animated, setAnimated] = useState(false);\n const leaving = rendered && !show;\n if (show && !rendered) setRendered(true);\n if (!animated && show !== initial) setAnimated(true);\n const unmount = useCallback(() => setRendered(false), []);\n useExitBackstop(leaving, unmount);\n if (!rendered) return null;\n return (\n <span\n aria-hidden=\"true\"\n onAnimationEnd={(event) => {\n if (leaving && event.target === event.currentTarget) unmount();\n }}\n className={cn(\n \"absolute top-1/2 left-1/2 -m-2 grid size-4\",\n leaving\n ? \"motion-safe:animate-loader-out motion-reduce:animate-fade-out\"\n : animated && \"motion-safe:animate-loader-in motion-reduce:animate-fade-in\",\n )}\n >\n <span className=\"rounded-circle border-(length:--border-width-spinner) border-current border-r-transparent motion-safe:animate-spin-fast\" />\n </span>\n );\n}\n\n/**\n * The button's inside: a loader, and a slot that holds the current label.\n *\n * A new label (a different text, or a different icon) mounts under a new key and\n * rises in while the old one is popped out of flow where it was and lifts away.\n * Nothing animates on first mount, and a prop-only update (same key) stays in\n * place.\n */\nfunction ButtonContent({ children, pending }: { children: ReactNode; pending: boolean }) {\n const key = labelKey(children);\n const icon = isIconKey(key);\n const contentRef = useRef<HTMLSpanElement>(null);\n const phaseRef = useRef<HTMLSpanElement>(null);\n\n useMorphWidth(contentRef, key);\n const { leaving, changed, remove } = useLeavingItems(key, { node: children, icon }, () => phaseRef.current);\n\n return (\n <>\n <Loader show={pending} />\n <span className={cn(\"button-slot\", pending && \"opacity-0\")}>\n <span ref={contentRef} className=\"inline-flex flex-none items-center\">\n {leaving.map((item) => (\n <LeavingPhase key={item.id} phase={{ id: item.id, node: item.value.node, icon: item.value.icon, rect: item.rect }} onDone={remove} />\n ))}\n <span\n key={key}\n ref={phaseRef}\n data-slot=\"button-label\"\n className={cn(\n \"inline-flex items-center justify-center gap-(--density-control-gap) whitespace-nowrap [&_svg]:shrink-0\",\n changed &&\n (icon\n ? \"motion-safe:animate-glyph-in motion-reduce:animate-fade-in\"\n : \"motion-safe:animate-label-in motion-reduce:animate-fade-in\"),\n )}\n >\n {children}\n </span>\n </span>\n </span>\n </>\n );\n}\n\n/**\n * Props.\n *\n * CORRECTION 6 -- the legacy Button was a plain `<button>`, so it took\n * `onClick`, `disabled` and Radix's `asChild`. React Aria's Button has none of\n * those: `ButtonProps` explicitly omits `onClick` and `href`. The replacements\n * are `onPress`, `isDisabled`, and `href` -- which renders React Aria's Link in\n * place of its Button. See the migration table in the package README.\n *\n * Deliberately NOT intersected with ComponentPropsWithoutRef<\"button\">: that\n * would advertise disabled, tabIndex and role, which React Aria does not\n * implement and silently ignores. React Aria already forwards the native event\n * surface, so nothing is lost.\n */\nexport interface ButtonProps\n extends Omit<AriaButtonProps, \"className\" | \"style\">, VariantProps<typeof buttonVariants> {\n /** Extra classes. Merged last, so they win conflicts with ours. */\n className?: string;\n /**\n * Icon rendered before the label.\n *\n * Mark the glyph `aria-hidden` -- the label is the accessible name, and a\n * second name source only makes the button announce twice. It is sized by the\n * base `[&_svg]` rule at `--icon-sm` unless it carries a `size-*` class of\n * its own, and spaced by `--density-control-gap`.\n *\n * Two named slots rather than one `icon` plus a position prop: a button can\n * legitimately carry a leading glyph AND a trailing affordance, which one\n * icon and a position cannot express.\n *\n * NOT the route for `size=\"icon\"`. An icon-only button passes its glyph as\n * `children`, which is what gives it its single centred child.\n */\n startIcon?: ReactNode;\n /** Icon rendered after the label. Same rules as {@link ButtonProps.startIcon}. */\n endIcon?: ReactNode;\n /**\n * Makes the button a link. It renders React Aria's `Link` -- a real `<a>`,\n * announced as a link -- with exactly the classes a button of the same\n * `variant` and `size` gets, and the same hover, press and focus-ring states.\n *\n * Use it whenever activating the control navigates. A button that navigates\n * cannot be opened in a new tab, is announced as the wrong role, and is\n * skipped by a screen reader's list of links.\n *\n * Client-side routing needs React Aria's `RouterProvider` mounted once above\n * it; without one, an `href` is an ordinary page load, which is also what an\n * external or download link wants.\n *\n * Button-only props have nothing to act on here: `type`, `form*` and\n * `isPending` are ignored, and the `ref` receives the `<a>`.\n */\n href?: AriaLinkProps[\"href\"];\n /** With `href` only: where to open it, e.g. `\"_blank\"`. */\n target?: AriaLinkProps[\"target\"];\n /** With `href` only. Give `\"noreferrer\"` alongside `target=\"_blank\"` to an external site. */\n rel?: AriaLinkProps[\"rel\"];\n /** With `href` only: download the target instead of opening it, optionally under this file name. */\n download?: AriaLinkProps[\"download\"];\n}\n\n/**\n * A button.\n *\n * A label that changes (a different text, or a different icon) crossfades, and\n * the button's width springs to fit it. While that runs, the OUTGOING label is\n * still in the DOM for about 200ms, `aria-hidden` and out of flow, so read a\n * button's text after the change settles, and do not put `id`s, inputs or\n * anything stateful inside a label that changes. Text that changes constantly,\n * such as a live counter, crossfades every time it does.\n *\n * `size=\"icon\"` renders a square button with no visible text, so it **must**\n * carry an `aria-label` -- otherwise it has no accessible name and fails axe's\n * `button-name` rule. The IconOnly story is the guard on that.\n *\n * @example\n * <Button onPress={save}>Save changes</Button>\n * <Button startIcon={<SaveIcon aria-hidden />}>Save changes</Button>\n * <Button endIcon={<ChevronIcon aria-hidden />}>Continue</Button>\n * <Button variant=\"outline\" size=\"icon\" aria-label=\"Close\"><XIcon /></Button>\n * <Button href=\"/reports\" variant=\"outlineNeutral\">View reports</Button>\n */\nexport const Button = forwardRef<HTMLButtonElement, ButtonProps>(function Button(\n { className, variant, size, startIcon, endIcon, children, href, target, rel, download, ...props },\n ref,\n) {\n const classes = cn(buttonVariants({ variant, size }), className);\n\n const button = useAriaBusy(props.isPending === true);\n\n /*\n * children passes through UNTOUCHED when neither slot is used, so a button\n * with no icon renders byte-identical DOM to before this prop existed. That is\n * what keeps this an additive change rather than a structural one -- and the\n * childOrder assertions in the stories are what hold it true, since they read\n * the button's direct children.\n *\n * The slot branch has to be a render function because React Aria's children\n * are ChildrenOrFunction<ButtonRenderProps>: a consumer may pass a function to\n * read isPressed or isPending, and wrapping the node directly would silently\n * render that function as nothing. The fragment adds no element of its own.\n */\n const content = (renderProps: ButtonRenderProps & { defaultChildren: ReactNode | undefined }) => (\n <ButtonContent pending={renderProps.isPending}>\n {startIcon}\n {typeof children === \"function\" ? children(renderProps) : children}\n {endIcon}\n </ButtonContent>\n );\n\n const { onPressStart } = props;\n const pressStart = (event: PressEvent) => {\n if (!anchorsLayer(props)) {\n const target = event.target as HTMLElement;\n target.style.setProperty(\"--press-scale\", pressScaleFor(target.offsetWidth));\n }\n onPressStart?.(event);\n };\n\n if (href !== undefined) {\n /*\n * The same classes on React Aria's Link. It sets data-hovered,\n * data-pressed and data-focus-visible on the <a>, which is what every\n * state class above keys on -- nothing here is re-implemented.\n *\n * Button-only props need no stripping: Link runs its props through\n * filterDOMProps, so `type` never reaches the <a>, and `isPending` is not a\n * DOM prop. A link has no pending state, so a render-function child is told\n * it is not pending rather than handed an object missing the field. The ref\n * is typed for the button this usually is; with href it receives the <a>,\n * which the TSDoc on `href` says.\n */\n return (\n <AriaLink\n ref={ref as unknown as ForwardedRef<HTMLAnchorElement>}\n {...(props as Omit<AriaLinkProps, \"className\" | \"children\">)}\n onPressStart={pressStart}\n href={href}\n target={target}\n rel={rel}\n download={download}\n data-slot=\"button\"\n className={classes}\n >\n {(renderProps) => content({ ...renderProps, isPending: false })}\n </AriaLink>\n );\n }\n\n return (\n <AriaButton ref={mergeRefs(ref, button as RefObject<HTMLButtonElement | null>)} {...props} onPressStart={pressStart} data-slot=\"button\" className={classes}>\n {content}\n </AriaButton>\n );\n});\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA4DA,MAAM,aACJ;AAEF,MAAa,iBAAiB,IAC5B;CACE;CAOA;CACA;CAgBA;CAIA;CACA;CAeA;CACA;CACA;CACA;CACA;CAQA;CACA;CAGA;AACF,GACA;CACE,UAAU;EACR,SAAS;GAiBP,SAAS;IACP;IACA;IACA;IAEA;IACA;IACA;GACF;GAOA,IAAI;IACF;IACA;IACA;IACA;IACA;IACA;IACA;GACF;GAQA,WAAW;IACT;IACA;IACA;IACA;IACA;IAGA;GACF;GAIA,SAAS;IACP;IACA;IACA;IACA;IACA;IACA;GACF;GAgBA,gBAAgB;IACd;IACA;IACA;IACA;IACA;IACA;GACF;GACA,OAAO;IACL;IACA;IACA;IACA;IACA;IAIA;GACF;GAEA,aAAa;IACX;IACA;IACA;IACA;IACA;IAEA;GACF;GACA,MAAM;IACJ;IACA;IACA;IACA;IACA;IAEA;GACF;EACF;EACA,MAAM;GAQJ,IAAI;GACJ,IAAI;GACJ,IAAI;GAIJ,MAAM;EACR;CACF;CACA,iBAAiB;EACf,SAAS;EACT,MAAM;CACR;AACF,CACF;;AAcA,SAAS,SAAS,MAAyB;CACzC,IAAI,QAAQ,QAAQ,OAAO,SAAS,WAAW,OAAO;CACtD,IAAI,OAAO,SAAS,YAAY,OAAO,SAAS,YAAY,OAAO,SAAS,UAAU,OAAO,OAAO,IAAI;CACxG,IAAI,MAAM,QAAQ,IAAI,GAAG,OAAO,KAAK,IAAI,QAAQ,CAAC,CAAC,KAAK,EAAE;CAC1D,IAAI,CAAC,eAAe,IAAI,GAAG,OAAO;CAClC,MAAM,OAAO,KAAK;CAClB,OAAO,OAAO,OAAO,SAAS,WAAW,OAAQ,MAAM,eAAe,MAAM,QAAQ,MAAO,MAAM,SAAU,KAAK,MAAmC,QAAQ;AAC7J;;AAGA,MAAM,aAAa,QAAgB,CAAC,KAAK,KAAK,IAAI,QAAQ,YAAY,EAAE,CAAC;AASzE,SAAS,aAAa,EAAE,OAAO,UAAiE;CAC9F,MAAM,EAAE,OAAO;CACf,MAAM,SAAS,kBAAkB,OAAO,EAAE,GAAG,CAAC,QAAQ,EAAE,CAAC;CACzD,gBAAgB,MAAM,MAAM;CAC5B,OACE,oBAAC,QAAD;EACE,eAAY;EACZ,iBAAiB,UAAU;GACzB,IAAI,MAAM,WAAW,MAAM,eAAe,OAAO;EACnD;EACA,OAAO,MAAM;EACb,WAAW,GACT,mHACA,MAAM,OACF,iEACA,8DACN;EAEC,UAAA,MAAM;CACH,CAAA;AAEV;;AAGA,SAAS,OAAO,EAAE,QAA2B;CAC3C,MAAM,CAAC,WAAW,SAAS,IAAI;CAC/B,MAAM,CAAC,UAAU,eAAe,SAAS,IAAI;CAC7C,MAAM,CAAC,UAAU,eAAe,SAAS,KAAK;CAC9C,MAAM,UAAU,YAAY,CAAC;CAC7B,IAAI,QAAQ,CAAC,UAAU,YAAY,IAAI;CACvC,IAAI,CAAC,YAAY,SAAS,SAAS,YAAY,IAAI;CACnD,MAAM,UAAU,kBAAkB,YAAY,KAAK,GAAG,CAAC,CAAC;CACxD,gBAAgB,SAAS,OAAO;CAChC,IAAI,CAAC,UAAU,OAAO;CACtB,OACE,oBAAC,QAAD;EACE,eAAY;EACZ,iBAAiB,UAAU;GACzB,IAAI,WAAW,MAAM,WAAW,MAAM,eAAe,QAAQ;EAC/D;EACA,WAAW,GACT,8CACA,UACI,kEACA,YAAY,6DAClB;EAEA,UAAA,oBAAC,QAAD,EAAM,WAAU,0HAA2H,CAAA;CACvI,CAAA;AAEV;;;;;;;;;AAUA,SAAS,cAAc,EAAE,UAAU,WAAsD;CACvF,MAAM,MAAM,SAAS,QAAQ;CAC7B,MAAM,OAAO,UAAU,GAAG;CAC1B,MAAM,aAAa,OAAwB,IAAI;CAC/C,MAAM,WAAW,OAAwB,IAAI;CAE7C,cAAc,YAAY,GAAG;CAC7B,MAAM,EAAE,SAAS,SAAS,WAAW,gBAAgB,KAAK;EAAE,MAAM;EAAU;CAAK,SAAS,SAAS,OAAO;CAE1G,OACE,qBAAA,YAAA,EAAA,UAAA,CACE,oBAAC,QAAD,EAAQ,MAAM,QAAU,CAAA,GACxB,oBAAC,QAAD;EAAM,WAAW,GAAG,eAAe,WAAW,WAAW;EACvD,UAAA,qBAAC,QAAD;GAAM,KAAK;GAAY,WAAU;GAAjC,UAAA,CACG,QAAQ,KAAK,SACZ,oBAAC,cAAD;IAA4B,OAAO;KAAE,IAAI,KAAK;KAAI,MAAM,KAAK,MAAM;KAAM,MAAM,KAAK,MAAM;KAAM,MAAM,KAAK;IAAK;IAAG,QAAQ;GAAS,GAAjH,KAAK,EAA4G,CACrI,GACD,oBAAC,QAAD;IAEE,KAAK;IACL,aAAU;IACV,WAAW,GACT,0GACA,YACG,OACG,+DACA,6DACR;IAEC;GACG,GAZC,GAYD,CACF;;CACF,CAAA,CACN,EAAA,CAAA;AAEN;;;;;;;;;;;;;;;;;;;;;;AAoFA,MAAaA,WAAS,WAA2C,SAASA,SACxE,EAAE,WAAW,SAAS,MAAM,WAAW,SAAS,UAAU,MAAM,QAAQ,KAAK,UAAU,GAAG,SAC1F,KACA;CACA,MAAM,UAAU,GAAG,eAAe;EAAE;EAAS;CAAK,CAAC,GAAG,SAAS;CAE/D,MAAM,SAAS,YAAY,MAAM,cAAc,IAAI;CAcnD,MAAM,WAAW,gBACf,qBAAC,eAAD;EAAe,SAAS,YAAY;EAApC,UAAA;GACG;GACA,OAAO,aAAa,aAAa,SAAS,WAAW,IAAI;GACzD;EACY;;CAGjB,MAAM,EAAE,iBAAiB;CACzB,MAAM,cAAc,UAAsB;EACxC,IAAI,CAAC,aAAa,KAAK,GAAG;GACxB,MAAM,SAAS,MAAM;GACrB,OAAO,MAAM,YAAY,iBAAiB,cAAc,OAAO,WAAW,CAAC;EAC7E;EACA,eAAe,KAAK;CACtB;CAEA,IAAI,SAAS,KAAA,GAaX,OACE,oBAACC,MAAD;EACO;EACL,GAAK;EACL,cAAc;EACR;EACE;EACH;EACK;EACV,aAAU;EACV,WAAW;EAET,WAAA,gBAAgB,QAAQ;GAAE,GAAG;GAAa,WAAW;EAAM,CAAC;CACtD,CAAA;CAId,OACE,oBAACC,QAAD;EAAY,KAAK,UAAU,KAAK,MAA6C;EAAG,GAAI;EAAO,cAAc;EAAY,aAAU;EAAS,WAAW;EAChJ,UAAA;CACS,CAAA;AAEhB,CAAC"}
|
|
1
|
+
{"version":3,"file":"button.js","names":["Button","AriaLink","AriaButton"],"sources":["../../src/button/button.tsx"],"sourcesContent":["\"use client\";\n\nimport { cva, type VariantProps } from \"class-variance-authority\";\nimport {\n forwardRef,\n isValidElement,\n useCallback,\n useRef,\n useState,\n type ForwardedRef,\n type ReactNode,\n type RefObject,\n} from \"react\";\nimport {\n Button as AriaButton,\n Link as AriaLink,\n type ButtonProps as AriaButtonProps,\n type ButtonRenderProps,\n type LinkProps as AriaLinkProps,\n type PressEvent,\n} from \"react-aria-components\";\n\nimport { cn } from \"../lib/cn.js\";\nimport { mergeRefs } from \"../lib/merge-refs.js\";\nimport {\n anchorsLayer,\n pressScaleFor,\n useAriaBusy,\n useExitBackstop,\n useLeavingItems,\n useMorphWidth,\n type Rect,\n} from \"../lib/motion.js\";\n\n/**\n * Variant classes, exported so a consumer can reuse the exact classes we ship\n * rather than guessing at them.\n *\n * NOT the answer for a link-shaped button any more: pass `href` to Button. On a\n * consumer's own <a> these classes get none of their states -- hover, press and\n * the focus ring all key on React Aria data attributes that a plain anchor never\n * receives, and the base sets outline-none, so keyboard focus was invisible.\n *\n * Ported from the legacy shadcn Button. The variant and size vocabulary is\n * deliberately identical so existing call sites migrate by rename, with six\n * corrections that the Alfred contracts require -- each marked below.\n *\n * Every string here is a STATIC literal. Tailwind scans source text, so a class\n * assembled at runtime is invisible to it and would simply never be emitted.\n *\n * State comes from React Aria data attributes rather than pseudo-classes:\n * data-[focus-visible] is keyboard focus only, and data-[pressed] covers mouse,\n * touch and keyboard uniformly. The original had no pressed state at all.\n */\n/*\n * The press: the button scales down while pressed (see onPressStart for the\n * scale, which depends on width) and springs back on release. Anchors of\n * floating layers never scale: a menu that opens on pointerdown would measure a\n * shrunken trigger.\n */\nconst pressScale =\n \"motion-safe:data-[pressed]:scale-(--press-scale) [&:is([aria-haspopup]:not([aria-haspopup=false]),[data-state],[role=combobox])]:scale-none!\";\n\nexport const buttonVariants = cva(\n [\n \"inline-flex items-center justify-center gap-(--density-control-gap) shrink-0 whitespace-nowrap\",\n // No leading-* here on purpose. tailwind-merge treats a later text-* class\n // as owning line-height (a Tailwind v3 assumption, where text-base carried\n // one), so a leading-* in the base string is silently STRIPPED by cn() once\n // a size variant adds text-base. It is not needed either: the control has a\n // fixed height and centres with flex, so line-height cannot overflow it.\n // A component with auto height must put leading-* AFTER its text-* class.\n \"rounded-control font-sans font-medium\",\n \"cursor-pointer select-none\",\n\n // font-sans above is part of the same problem: a <button> does not inherit\n // the page font, so without a reset it renders in the user-agent face\n // (Arial in Chromium) rather than the design system's. The same applies to\n // bg-transparent: an unstyled <button> paints the user-agent ButtonFace\n // grey -- which is what surfaces the moment a fill class fails to resolve,\n // and is why the paint stories compare against tokens rather than checking\n // for transparency.\n //\n // The reset in @seekalfred/styles is OPT-IN, so a consumer may never load\n // one. A bare <button> then keeps its user-agent border (2px, black, in\n // Chromium) and inline padding, which is a black box around every variant\n // that does not set a border of its own. The original inherited a zeroed\n // border from Tailwind Preflight in the app; a library component cannot\n // assume the consumer supplied one.\n \"border-(length:--border-width) border-transparent bg-transparent\",\n\n // Icon handling, ported verbatim: an svg child is sized unless the caller\n // already said otherwise, and never swallows the press.\n \"[&_svg]:pointer-events-none [&_svg]:shrink-0\",\n \"[&_svg:not([class*='size-'])]:size-(--icon-sm)\",\n\n // CORRECTION 1 -- the original focused with `ring-[3px]`, which is a\n // box-shadow. Box-shadows are discarded under forced-colors, taking the\n // only focus indication with them, and axe has no rule that would catch it\n // (ARCHITECTURE section 5, rule 6). A real outline survives.\n //\n // outline-solid on the focus variant is load-bearing, not decoration.\n // `outline-none` sets BOTH outline-style:none and --tw-outline-style:none,\n // and Tailwind's outline-width utility resolves its style from that same\n // variable -- so the focus variant applied a width and a colour while\n // faithfully re-applying style:none, and painted nothing. The button was\n // keyboard-focusable with no visible ring. axe has no focus-indicator\n // rule, so every automated gate stayed green; the KeyboardFocus story now\n // asserts outlineStyle directly.\n \"outline-none\",\n \"data-[focus-visible]:outline-solid\",\n \"data-[focus-visible]:outline-(length:--focus-ring-width)\",\n \"data-[focus-visible]:outline-offset-(--focus-ring-offset)\",\n \"data-[focus-visible]:outline-(--color-ring)\",\n\n // CORRECTION 2 -- `disabled:pointer-events-none` also kills the cursor, so\n // the control reads as inert rather than as unavailable. React Aria already\n // blocks activation; this only has to say so.\n // The opacity half of the disabled look moved into each variant: the\n // outlined and ghost buttons stay fully opaque and mute their colours\n // instead, which a single base opacity cannot express.\n \"data-[disabled]:cursor-not-allowed\",\n \"[-webkit-tap-highlight-color:transparent]\",\n\n // React Aria's own pending state. The original had no equivalent.\n \"data-[pending]:cursor-progress\",\n ],\n {\n variants: {\n variant: {\n // CORRECTION 3 -- the legacy button filled this variant with the brand\n // gradient. It is flat here, because the brand system caps\n // --gradient-brand at one intentional element per view and keeps that\n // budget for the logo and progress bars: \"a gradient on every button\n // spends that budget everywhere. Ours is the flat primary.\"\n //\n // CORRECTION 4 -- the label is ink, not white. Brand orange #FF8431\n // carries white at 2.44:1; ink clears it at 8.35:1, which is the brand\n // system's own --text-on-orange rule. Its Button ships white only as a\n // dated, individually signed-off exception, and this library's contrast\n // contract is build-failing with no exemption mechanism to inherit it.\n //\n // Hover and pressed are their own TOKENS again (primary 400 and 600),\n // as the design spec (Oct 2026) sets them; an opacity step would make\n // the label pair depend on whatever sits behind the button. Both pairs\n // are in the contrast contract (contrast-pairs.json).\n primary: [\n \"bg-primary text-primary-foreground shadow-surface\",\n \"motion-safe:button-motion\",\n pressScale,\n // Design spec (Oct 2026): hover primary 400, pressed primary 600.\n \"data-[hovered]:bg-primary-hover data-[hovered]:shadow-resting\",\n \"data-[pressed]:bg-primary-pressed\",\n \"data-[disabled]:opacity-(--opacity-button-disabled)\",\n ],\n /*\n * The AI action: every generative action, and nothing else (Madhuri,\n * 2026-11-01). An orange GRADIENT fill (--gradient-ai, primary 400 to\n * 600), an ink label and a wand icon, which Button supplies itself.\n *\n * This is the ONE sanctioned exception to CORRECTION 3 above: primary\n * stays flat, and the gradient belongs to `ai` alone. Do not flatten it,\n * and do not copy it to another variant.\n *\n * The fill is a background-image over bg-primary, so a browser that\n * drops the gradient still paints brand orange under the ink label.\n * Hover and pressed swap the image for their own tokens (an opacity step\n * would make the label pair depend on what is behind the button), and\n * every stop of all three is graded for the ink label in\n * gradient-ai.test.ts.\n *\n * Disabled drops the gradient entirely (bg-none) for the muted fill, so\n * it reads as off, not as a dimmed AI button.\n *\n * Focus: the control's OWN 1px border turns ink and no ring is drawn\n * (approved, 2026-11-01). The outline is removed only here. Under\n * forced-colors a border colour is replaced by a system colour, so the\n * change would vanish: there the outline comes back, for the same\n * reason as CORRECTION 1.\n *\n * The old turning colour beam (ai-beam) is gone: it does not belong to\n * the confirmed look, and its ring would have sat on top of the focus\n * border.\n */\n ai: [\n \"bg-primary bg-(image:--gradient-ai) text-primary-foreground shadow-surface\",\n \"motion-safe:button-motion\",\n pressScale,\n \"data-[hovered]:bg-(image:--gradient-ai-hover) data-[hovered]:shadow-resting\",\n \"data-[pressed]:bg-(image:--gradient-ai-pressed)\",\n \"motion-safe:data-[hovered]:[&_svg]:-rotate-12\",\n \"data-[focus-visible]:outline-0 data-[focus-visible]:border-primary-foreground\",\n \"forced-colors:data-[focus-visible]:outline-(length:--focus-ring-width)\",\n \"data-[disabled]:opacity-100 data-[disabled]:bg-none data-[disabled]:bg-muted data-[disabled]:text-muted-foreground data-[disabled]:border-border data-[disabled]:shadow-none\",\n ],\n /*\n * Design spec (Oct 2026): Secondary is a white surface with a light\n * border and dark text -- the same paint as `outlineNeutral` below, kept\n * under this name so existing call sites need no rename. It used to be\n * the lavender fill; the lavender `secondary` role still exists for\n * badges and highlights, just not on a button.\n */\n secondary: [\n \"bg-surface text-foreground border-(length:--border-width) border-border shadow-surface\",\n \"motion-safe:button-motion\",\n pressScale,\n \"data-[hovered]:bg-muted data-[hovered]:shadow-resting\",\n \"data-[pressed]:bg-muted\",\n // A disabled secondary button keeps its shape and mutes its fill and\n // label, so it reads as a button that is off, not as plain text.\n \"data-[disabled]:opacity-100 data-[disabled]:bg-muted data-[disabled]:text-muted-foreground data-[disabled]:border-border\",\n ],\n // primary-strong, never primary: the brand orange is 2.44:1 on the page\n // and cannot legally be a label OR a border. That is the whole reason\n // the token exists.\n outline: [\n \"bg-surface text-primary-strong border-(length:--border-width) border-primary-strong shadow-surface\",\n \"motion-safe:button-motion\",\n pressScale,\n \"data-[hovered]:bg-muted data-[hovered]:shadow-resting\",\n \"data-[pressed]:bg-muted data-[pressed]:border-foreground\",\n \"data-[disabled]:opacity-100 data-[disabled]:bg-muted data-[disabled]:text-muted-foreground data-[disabled]:border-border\",\n ],\n /*\n * The NEUTRAL outline, and the product's single most-used button:\n * 99 call sites against 32 for the brand-coloured one.\n *\n * It exists because `outline` above is brand-toned -- orange border,\n * orange label -- which is what the product calls `secondaryOutline`.\n * Its plain `outline` is this: a grey border and ordinary body text,\n * for a button that must not compete with the primary action next to\n * it. One name could not cover both, and renaming `outline` would have\n * silently repainted every existing call site of ours instead.\n *\n * This is the design spec's Secondary button: surface, the light\n * `border` hairline and body text. The hairline is below 3:1 by UX\n * decision; the label, hover fill and focus ring carry the boundary.\n */\n outlineNeutral: [\n \"bg-surface text-foreground border-(length:--border-width) border-border shadow-surface\",\n \"motion-safe:button-motion\",\n pressScale,\n \"data-[hovered]:bg-muted data-[hovered]:shadow-resting\",\n \"data-[pressed]:bg-muted\",\n \"data-[disabled]:opacity-100 data-[disabled]:bg-muted data-[disabled]:text-text-disabled\",\n ],\n ghost: [\n \"bg-transparent text-text-secondary\",\n \"motion-safe:button-motion\",\n pressScale,\n \"data-[hovered]:bg-muted data-[hovered]:text-foreground\",\n \"data-[disabled]:opacity-100 data-[disabled]:text-text-disabled\",\n // CORRECTION 5 -- pressed must not be background-only. A ghost button\n // has no border at rest, so the pressed border is the non-colour cue\n // that survives forced-colors (rule 6 again).\n \"data-[pressed]:bg-muted data-[pressed]:border-primary-strong\",\n ],\n // Design spec (Oct 2026): an outlined Danger button, not a red fill.\n destructive: [\n \"bg-surface text-destructive-strong border-(length:--border-width) border-border shadow-surface\",\n \"motion-safe:button-motion\",\n pressScale,\n \"data-[hovered]:bg-muted data-[hovered]:border-destructive-strong\",\n \"data-[pressed]:bg-muted data-[pressed]:border-destructive-strong\",\n // Muted fill, label tinted toward destructive.\n \"data-[disabled]:opacity-100 data-[disabled]:bg-muted data-[disabled]:border-border data-[disabled]:text-[color-mix(in_oklab,var(--color-destructive)_55%,var(--color-muted))]\",\n ],\n link: [\n \"bg-transparent text-text-link underline-offset-(--underline-offset)\",\n \"motion-safe:button-motion\",\n \"data-[disabled]:text-text-disabled\",\n \"data-[hovered]:underline\",\n \"data-[pressed]:underline data-[pressed]:opacity-80\",\n // A trailing arrow moves 3px on hover.\n \"[&_svg:last-child]:transition-transform motion-safe:data-[hovered]:[&_svg:last-child]:translate-x-0.75 rtl:motion-safe:data-[hovered]:[&_svg:last-child]:-translate-x-0.75\",\n ],\n },\n size: {\n // Height, padding and label size all come from the control scale, so\n // data-density moves them together. The icon-only variant is square at\n // the md height. The legacy shadcn rule that tightened padding when a\n // child was an icon is gone: it needed three more magic numbers, the\n // brand system has no equivalent, and the gap token now owns icon\n // spacing.\n // Design spec (Oct 2026): 16px padding and 14px label at 36px and 44px.\n sm: \"h-(--density-control-h-sm) px-4 text-sm\",\n md: \"h-(--density-control-h-md) px-4 text-sm\",\n lg: \"h-(--density-control-h-lg) px-(--density-control-pad-lg) text-lg\",\n // text-base even though there is no visible label: without it the\n // user-agent default (13.33px in Chromium) leaks in and sizes any\n // em-based child against the wrong root.\n icon: \"size-(--density-control-h-md) px-0 text-base\",\n // The compact icon-only button: a 24px square (the WCAG 2.5.8 target\n // minimum) for a glyph that sits beside text, such as an info trigger\n // next to a heading. It is NOT on the density scale: it is a fixed\n // minimum target, so it does not grow with data-density. Same rules as\n // `icon`: the glyph is `children` and it must carry an `aria-label`. The\n // glyph is 14px unless it carries a size-* class of its own.\n \"icon-sm\": \"size-6 px-0 text-base [&_svg:not([class*='size-'])]:size-3.5\",\n },\n // Opt-in wrapping, for a button whose label is long, user-authored text (a\n // prompt in a list, a name in a cell). The base is one unbreakable line at a\n // fixed height; wrap lets the label break across lines, the box grow from\n // the size's height as a minimum, the text start at the leading edge, and\n // the button shrink to its container. Our later classes beat the base's\n // whitespace-nowrap, h-* and shrink-0 by merge order (cn runs tailwind-merge\n // over our own classes), and the inner label spans follow the `wrap` prop.\n wrap: {\n true: \"h-auto max-w-full min-w-0 shrink whitespace-normal break-words text-start\",\n false: \"\",\n },\n },\n compoundVariants: [\n { size: \"sm\", wrap: true, class: \"min-h-(--density-control-h-sm) py-1.5\" },\n { size: \"md\", wrap: true, class: \"min-h-(--density-control-h-md) py-2\" },\n { size: \"lg\", wrap: true, class: \"min-h-(--density-control-h-lg) py-2.5\" },\n ],\n defaultVariants: {\n variant: \"primary\",\n size: \"md\",\n wrap: false,\n },\n },\n);\n\n/*\n * The label motion, without an animation library.\n *\n * A presence library would keep the outgoing label mounted for the crossfade\n * and drive a motion value for the width morph. CSS cannot keep an outgoing\n * label mounted or observe its own content width, so those two jobs are the\n * plain React below; every animation itself is a CSS keyframe or transition\n * (@seekalfred/styles motion.css, the button-* utilities in styles.css, the\n * --animate-* tokens).\n */\n\n/** A key for the label content: text plus element names, so a new label or icon crossfades while prop-only updates stay in place. */\nfunction labelKey(node: ReactNode): string {\n if (node == null || typeof node === \"boolean\") return \"\";\n if (typeof node === \"string\" || typeof node === \"number\" || typeof node === \"bigint\") return String(node);\n if (Array.isArray(node)) return node.map(labelKey).join(\"\");\n if (!isValidElement(node)) return \"\";\n const type = node.type as string | { displayName?: string; name?: string };\n return \"<\" + (typeof type === \"string\" ? type : (type?.displayName ?? type?.name ?? \"\")) + \">\" + labelKey((node.props as { children?: ReactNode }).children);\n}\n\n/** A label with no text once element names are stripped is an icon, which scales instead of rising. */\nconst isIconKey = (key: string) => !/\\S/.test(key.replace(/<[^>]*>/g, \"\"));\n\ninterface LeavingLabel {\n id: number;\n node: ReactNode;\n icon: boolean;\n rect: Rect;\n}\n\nfunction LeavingPhase({ phase, onDone }: { phase: LeavingLabel; onDone: (id: number) => void }) {\n const { id } = phase;\n const remove = useCallback(() => onDone(id), [onDone, id]);\n useExitBackstop(true, remove);\n return (\n <span\n aria-hidden=\"true\"\n onAnimationEnd={(event) => {\n if (event.target === event.currentTarget) remove();\n }}\n style={phase.rect}\n className={cn(\n \"absolute inline-flex items-center justify-center gap-(--density-control-gap) whitespace-nowrap [&_svg]:shrink-0\",\n phase.icon\n ? \"motion-safe:animate-glyph-out motion-reduce:animate-fade-out\"\n : \"motion-safe:animate-label-out motion-reduce:animate-fade-out\",\n )}\n >\n {phase.node}\n </span>\n );\n}\n\n/** The loading spinner: it scales in on the snappy spring, and out smaller with a little blur. Nothing animates if it is there on first mount. */\nfunction Loader({ show }: { show: boolean }) {\n const [initial] = useState(show);\n const [rendered, setRendered] = useState(show);\n const [animated, setAnimated] = useState(false);\n const leaving = rendered && !show;\n if (show && !rendered) setRendered(true);\n if (!animated && show !== initial) setAnimated(true);\n const unmount = useCallback(() => setRendered(false), []);\n useExitBackstop(leaving, unmount);\n if (!rendered) return null;\n return (\n <span\n aria-hidden=\"true\"\n onAnimationEnd={(event) => {\n if (leaving && event.target === event.currentTarget) unmount();\n }}\n className={cn(\n \"absolute top-1/2 left-1/2 -m-2 grid size-4\",\n leaving\n ? \"motion-safe:animate-loader-out motion-reduce:animate-fade-out\"\n : animated && \"motion-safe:animate-loader-in motion-reduce:animate-fade-in\",\n )}\n >\n <span className=\"rounded-circle border-(length:--border-width-spinner) border-current border-r-transparent motion-safe:animate-spin-fast\" />\n </span>\n );\n}\n\n/**\n * The button's inside: a loader, and a slot that holds the current label.\n *\n * A new label (a different text, or a different icon) mounts under a new key and\n * rises in while the old one is popped out of flow where it was and lifts away.\n * Nothing animates on first mount, and a prop-only update (same key) stays in\n * place.\n */\nfunction ButtonContent({ children, pending, wrap }: { children: ReactNode; pending: boolean; wrap: boolean }) {\n const key = labelKey(children);\n const icon = isIconKey(key);\n const contentRef = useRef<HTMLSpanElement>(null);\n const phaseRef = useRef<HTMLSpanElement>(null);\n\n // A wrapping label has no single width to spring to: the morph measures the unwrapped line.\n useMorphWidth(contentRef, key, !wrap);\n const { leaving, changed, remove } = useLeavingItems(key, { node: children, icon }, () => phaseRef.current);\n\n return (\n <>\n <Loader show={pending} />\n <span className={cn(\"button-slot\", pending && \"opacity-0\")}>\n <span ref={contentRef} className={wrap ? \"inline-flex min-w-0 items-center\" : \"inline-flex flex-none items-center\"}>\n {leaving.map((item) => (\n <LeavingPhase key={item.id} phase={{ id: item.id, node: item.value.node, icon: item.value.icon, rect: item.rect }} onDone={remove} />\n ))}\n <span\n key={key}\n ref={phaseRef}\n data-slot=\"button-label\"\n className={cn(\n [\n \"inline-flex items-center justify-center gap-(--density-control-gap) [&_svg]:shrink-0\",\n wrap ? \"min-w-0 whitespace-normal\" : \"whitespace-nowrap\",\n ],\n changed &&\n (icon\n ? \"motion-safe:animate-glyph-in motion-reduce:animate-fade-in\"\n : \"motion-safe:animate-label-in motion-reduce:animate-fade-in\"),\n )}\n >\n {children}\n </span>\n </span>\n </span>\n </>\n );\n}\n\n/**\n * The AI button's wand: a 24px outline glyph in the same stroke style as the\n * library's other inline icons (the package ships no icon dependency). Drawn\n * after lucide's \"wand-sparkles\", which is what the app's `Wand2` is.\n */\nfunction WandIcon() {\n return (\n <svg\n viewBox=\"0 0 24 24\"\n fill=\"none\"\n stroke=\"currentColor\"\n strokeWidth=\"1.75\"\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n aria-hidden=\"true\"\n data-slot=\"button-wand\"\n >\n <path d=\"m21.64 3.64-1.28-1.28a1.21 1.21 0 0 0-1.72 0L2.36 18.64a1.21 1.21 0 0 0 0 1.72l1.28 1.28a1.2 1.2 0 0 0 1.72 0L21.64 5.36a1.2 1.2 0 0 0 0-1.72\" />\n <path d=\"m14 7 3 3M5 6v4M19 14v4M10 2v2M7 8H3M21 16h-4M11 3H9\" />\n </svg>\n );\n}\nWandIcon.displayName = \"WandIcon\";\n\n/**\n * Props.\n *\n * CORRECTION 6 -- the legacy Button was a plain `<button>`, so it took\n * `onClick`, `disabled` and Radix's `asChild`. React Aria's Button has none of\n * those: `ButtonProps` explicitly omits `onClick` and `href`. The replacements\n * are `onPress`, `isDisabled`, and `href` -- which renders React Aria's Link in\n * place of its Button. See the migration table in the package README.\n *\n * Deliberately NOT intersected with ComponentPropsWithoutRef<\"button\">: that\n * would advertise disabled, tabIndex and role, which React Aria does not\n * implement and silently ignores. React Aria already forwards the native event\n * surface, so nothing is lost.\n */\nexport interface ButtonProps\n extends Omit<AriaButtonProps, \"className\" | \"style\">, VariantProps<typeof buttonVariants> {\n /** Extra classes. Merged last, so they win conflicts with ours. */\n className?: string;\n /**\n * Icon rendered before the label.\n *\n * Mark the glyph `aria-hidden` -- the label is the accessible name, and a\n * second name source only makes the button announce twice. It is sized by the\n * base `[&_svg]` rule at `--icon-sm` unless it carries a `size-*` class of\n * its own, and spaced by `--density-control-gap`.\n *\n * Two named slots rather than one `icon` plus a position prop: a button can\n * legitimately carry a leading glyph AND a trailing affordance, which one\n * icon and a position cannot express.\n *\n * NOT the route for `size=\"icon\"`. An icon-only button passes its glyph as\n * `children`, which is what gives it its single centred child.\n *\n * `variant=\"ai\"` supplies a wand here when this is `undefined`. Pass your own\n * node to replace it, or `false` (or `null`) for no icon at all. The wand is\n * not added to `size=\"icon\"`, whose glyph is `children`.\n */\n startIcon?: ReactNode;\n /** Icon rendered after the label. Same rules as {@link ButtonProps.startIcon}. */\n endIcon?: ReactNode;\n /**\n * Let the label wrap onto more lines. For a button whose text is long and\n * not ours (a prompt, a name): it shrinks to its container, breaks the label\n * (even a long word with no spaces), starts the text at the leading edge, and\n * grows taller than `size`'s height, which becomes its minimum. Off by\n * default, and then a button is one line at a fixed height exactly as before.\n * Pass `className=\"justify-start\"` to start a link-style button at the\n * leading edge of its row. A wrapping button's width does not spring when its\n * label changes.\n *\n * Not for `size=\"icon\"` or `\"icon-sm\"`, which have no label.\n *\n * @default false\n */\n wrap?: boolean;\n /**\n * Makes the button a link. It renders React Aria's `Link` -- a real `<a>`,\n * announced as a link -- with exactly the classes a button of the same\n * `variant` and `size` gets, and the same hover, press and focus-ring states.\n *\n * Use it whenever activating the control navigates. A button that navigates\n * cannot be opened in a new tab, is announced as the wrong role, and is\n * skipped by a screen reader's list of links.\n *\n * Client-side routing needs React Aria's `RouterProvider` mounted once above\n * it; without one, an `href` is an ordinary page load, which is also what an\n * external or download link wants.\n *\n * Button-only props have nothing to act on here: `type`, `form*` and\n * `isPending` are ignored, and the `ref` receives the `<a>`.\n */\n href?: AriaLinkProps[\"href\"];\n /** With `href` only: where to open it, e.g. `\"_blank\"`. */\n target?: AriaLinkProps[\"target\"];\n /** With `href` only. Give `\"noreferrer\"` alongside `target=\"_blank\"` to an external site. */\n rel?: AriaLinkProps[\"rel\"];\n /** With `href` only: download the target instead of opening it, optionally under this file name. */\n download?: AriaLinkProps[\"download\"];\n}\n\n/**\n * A button.\n *\n * A label that changes (a different text, or a different icon) crossfades, and\n * the button's width springs to fit it. While that runs, the OUTGOING label is\n * still in the DOM for about 200ms, `aria-hidden` and out of flow, so read a\n * button's text after the change settles, and do not put `id`s, inputs or\n * anything stateful inside a label that changes. Text that changes constantly,\n * such as a live counter, crossfades every time it does.\n *\n * `size=\"icon\"` renders a square button with no visible text, so it **must**\n * carry an `aria-label` -- otherwise it has no accessible name and fails axe's\n * `button-name` rule. The IconOnly story is the guard on that.\n *\n * @example\n * <Button onPress={save}>Save changes</Button>\n * <Button startIcon={<SaveIcon aria-hidden />}>Save changes</Button>\n * <Button endIcon={<ChevronIcon aria-hidden />}>Continue</Button>\n * <Button variant=\"outline\" size=\"icon\" aria-label=\"Close\"><XIcon /></Button>\n * <Button variant=\"ghost\" size=\"icon-sm\" aria-label=\"More information\"><InfoIcon /></Button>\n * <Button variant=\"link\" wrap className=\"justify-start\">A long prompt that may run over several lines</Button>\n * <Button href=\"/reports\" variant=\"outlineNeutral\">View reports</Button>\n */\nexport const Button = forwardRef<HTMLButtonElement, ButtonProps>(function Button(\n { className, variant, size, wrap, startIcon, endIcon, children, href, target, rel, download, ...props },\n ref,\n) {\n const classes = cn(buttonVariants({ variant, size, wrap }), className);\n // The AI button brings its own wand; startIcon={false|null} opts out and a node replaces it.\n const leadingIcon = startIcon === undefined && variant === \"ai\" && size !== \"icon\" ? <WandIcon /> : startIcon;\n\n const button = useAriaBusy(props.isPending === true);\n\n /*\n * children passes through UNTOUCHED when neither slot is used, so a button\n * with no icon renders byte-identical DOM to before this prop existed. That is\n * what keeps this an additive change rather than a structural one -- and the\n * childOrder assertions in the stories are what hold it true, since they read\n * the button's direct children.\n *\n * The slot branch has to be a render function because React Aria's children\n * are ChildrenOrFunction<ButtonRenderProps>: a consumer may pass a function to\n * read isPressed or isPending, and wrapping the node directly would silently\n * render that function as nothing. The fragment adds no element of its own.\n */\n const content = (renderProps: ButtonRenderProps & { defaultChildren: ReactNode | undefined }) => (\n <ButtonContent pending={renderProps.isPending} wrap={wrap === true}>\n {leadingIcon}\n {typeof children === \"function\" ? children(renderProps) : children}\n {endIcon}\n </ButtonContent>\n );\n\n const { onPressStart } = props;\n const pressStart = (event: PressEvent) => {\n if (!anchorsLayer(props)) {\n const target = event.target as HTMLElement;\n target.style.setProperty(\"--press-scale\", pressScaleFor(target.offsetWidth));\n }\n onPressStart?.(event);\n };\n\n if (href !== undefined) {\n /*\n * The same classes on React Aria's Link. It sets data-hovered,\n * data-pressed and data-focus-visible on the <a>, which is what every\n * state class above keys on -- nothing here is re-implemented.\n *\n * Button-only props need no stripping: Link runs its props through\n * filterDOMProps, so `type` never reaches the <a>, and `isPending` is not a\n * DOM prop. A link has no pending state, so a render-function child is told\n * it is not pending rather than handed an object missing the field. The ref\n * is typed for the button this usually is; with href it receives the <a>,\n * which the TSDoc on `href` says.\n */\n return (\n <AriaLink\n ref={ref as unknown as ForwardedRef<HTMLAnchorElement>}\n {...(props as Omit<AriaLinkProps, \"className\" | \"children\">)}\n onPressStart={pressStart}\n href={href}\n target={target}\n rel={rel}\n download={download}\n data-slot=\"button\"\n className={classes}\n >\n {(renderProps) => content({ ...renderProps, isPending: false })}\n </AriaLink>\n );\n }\n\n return (\n <AriaButton ref={mergeRefs(ref, button as RefObject<HTMLButtonElement | null>)} {...props} onPressStart={pressStart} data-slot=\"button\" className={classes}>\n {content}\n </AriaButton>\n );\n});\n"],"mappings":"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"}
|
package/dist/card/card.js
CHANGED
|
@@ -94,7 +94,7 @@ const cardVariants = cva([
|
|
|
94
94
|
* `p-6` on the shell would make impossible.
|
|
95
95
|
*/
|
|
96
96
|
const cardHeaderVariants = cva("flex flex-col gap-1.5 px-6 py-(--card-section-py)");
|
|
97
|
-
const cardTitleVariants = cva(["text-base leading-(--leading-none) font-semibold", "m-0"]);
|
|
97
|
+
const cardTitleVariants = cva(["font-display text-base leading-(--leading-none) font-semibold", "m-0"]);
|
|
98
98
|
const cardDescriptionVariants = cva(["text-sm leading-(--leading-normal) text-muted-foreground"]);
|
|
99
99
|
const cardContentVariants = cva("px-6");
|
|
100
100
|
const cardFooterVariants = cva("flex items-center px-6 py-(--card-section-py)");
|
package/dist/card/card.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"card.js","names":[],"sources":["../../src/card/card.tsx"],"sourcesContent":["\"use client\";\n\nimport { cva } from \"class-variance-authority\";\nimport { forwardRef, useState, type ComponentPropsWithoutRef } from \"react\";\n\nimport { cn, cnAll } from \"../lib/cn.js\";\n\n/**\n * Card, ported from the legacy shadcn component.\n *\n * Presentational, so there is no React Aria base -- React Aria ships no card,\n * and a container that is not focusable, selectable or interactive needs no\n * behaviour to inherit.\n *\n * Five parts, from six. The census (24 consumer files, the most-used\n * container in the app) reads: Card 55, CardContent 40, CardHeader 21,\n * CardTitle 19, CardDescription 8, CardFooter 2, **CardAction 0**.\n *\n * Dropping `CardAction` takes three things with it, which is most of the\n * legacy header's complexity: the `grid`, the `grid-rows-[auto_auto]`, and\n * the `has-data-[slot=card-action]:grid-cols-[1fr_auto]` rule that existed\n * only to reserve a column for it. With no action to place, a header is a\n * stack.\n *\n * Two more legacy rules are gone. `[.border-b]:pb-6` and `[.border-t]:pt-6`\n * added padding when the CONSUMER happened to add a border class -- clever,\n * but action at a distance through a class name we do not own, and the census\n * found zero application call sites relying on it (one call site passes\n * `border-b-0`, which removes a border it never had). `@container/card-header`\n * named a container nothing queried.\n *\n * Every string in the cva blocks below is a STATIC literal. Tailwind scans\n * source text, so a class assembled at runtime is invisible to it and would\n * simply never be emitted.\n */\nexport const cardVariants = cva(\n [\n \"flex flex-col\",\n \"rounded-panel border-(length:--border-width) border-border\",\n \"bg-card text-card-foreground shadow-surface\",\n\n // box-border and font-sans are not boilerplate. The reset in\n // @seekalfred/styles is OPT-IN, so a consumer may load none: content-box\n // would add the padding to any width they set (the Alert shipped\n // overflowing its container by 46px), and an unstyled document renders\n // text in a serif (the Accordion shipped its panel that way). Declaring\n // the family HERE rather than on each part means one declaration covers\n // the whole subtree by inheritance.\n \"box-border font-sans\",\n ],\n {\n variants: {\n /**\n * The block padding and the gap live here, not in the base, so `flush`\n * REPLACES them rather than relying on a merge to drop them: the\n * exported `cardVariants({ flush: true })` is correct on its own.\n *\n * Flush keeps the outline, radius, surface and shadow, and clips its\n * content to the inside of the rounded border: without the clip a filled\n * child (a table header, a chart) shows square corners over the curve and\n * covers the left and right hairline. `overflow: clip`, not `hidden`, so\n * the card does not become a scroll container and a sticky table header\n * inside it still sticks to the page. The sections keep their inline\n * padding, so text and a section's fill stay clear of the corner curve\n * (the fill still meets the side edges: background paints the padding).\n *\n * The header and footer hold text, so under flush they keep the 24px\n * block inset the shell gave up. Without it the title and the footer line\n * sat against the top and bottom borders, inside the corner curve, and\n * the clip cut their first glyphs. The inset is a custom property both\n * sections read, set per variant (never left undefined), so a nested\n * default card resets it, and a section's own `py-*` still wins by merge.\n * CardContent stays block-flush: a table or chart meets the edges.\n */\n flush: {\n false: \"gap-6 py-6 [--card-section-py:--spacing(0)]\",\n true: \"gap-0 py-0 overflow-clip [--card-section-py:--spacing(6)]\",\n },\n /**\n * A featured card: the hairline is replaced by a brand gradient edge,\n * painted by the `card-highlighted` utility in styles.css. The border\n * stays (transparent), so a highlighted card has exactly the box of a\n * plain one and a list of mixed cards does not shift.\n */\n highlighted: {\n false: \"\",\n true: \"card-highlighted border-transparent\",\n },\n },\n compoundVariants: [\n // The flush clip would cut away the gradient edge, which is painted in\n // the border area outside the padding box. See card-highlighted-flush.\n { flush: true, highlighted: true, class: \"card-highlighted-flush\" },\n ],\n defaultVariants: { flush: false, highlighted: false },\n },\n);\n\n/**\n * Padding is inline-only on the sections and block-only on the shell.\n *\n * That split is load-bearing rather than stylistic: it is what lets a\n * consumer run a divider or a full-bleed image edge to edge, which a single\n * `p-6` on the shell would make impossible.\n */\nexport const cardHeaderVariants = cva(\"flex flex-col gap-1.5 px-6 py-(--card-section-py)\");\n\nexport const cardTitleVariants = cva([\n // leading-* AFTER text-*: tailwind-merge treats a later text-* as owning\n // line-height and silently strips a leading-* that precedes it.\n \"text-base leading-(--leading-none) font-semibold\",\n // The user-agent <h3> ships a 1em block margin and font-size 1.17em. The\n // legacy component was a <div> and never met either.\n \"m-0\",\n]);\n\nexport const cardDescriptionVariants = cva([\n \"text-sm leading-(--leading-normal) text-muted-foreground\",\n]);\n\nexport const cardContentVariants = cva(\"px-6\");\n\nexport const cardFooterVariants = cva(\"flex items-center px-6 py-(--card-section-py)\");\n\n/**\n * A content container.\n *\n * @example\n * <Card>\n * <CardHeader>\n * <CardTitle>Learning Progress</CardTitle>\n * <CardDescription>Across all active courses.</CardDescription>\n * </CardHeader>\n * <CardContent>Twelve of eighteen modules complete.</CardContent>\n * </Card>\n */\nexport interface CardProps extends ComponentPropsWithoutRef<\"div\"> {\n /**\n * Make the card respond to a pointer: it floats 2px on a spring while it is pointed\n * at, its border darkens and its shadow deepens. Mouse and pen only; touch never lifts.\n * Leave it off for a card that is not a target: a static card should not move.\n */\n interactive?: boolean;\n /**\n * A featured card: a brand gradient edge in place of the hairline. Use it for the\n * one card a view wants the eye to land on (a recommended action, the suggested\n * plan), never for several at once. The edge is decoration, so say WHY the card\n * is featured in its content too: colour alone does not reach a screen reader.\n * @default false\n */\n highlighted?: boolean;\n /**\n * Edge-to-edge content: no shell padding and no gap between the parts, and the\n * content is clipped to the rounded border. The outline, radius, surface and\n * shadow stay. For a table, list or full-bleed chart that must meet the card's\n * top and bottom. The header and footer keep a 24px block inset so their text\n * clears the corner curve, and every section keeps its inline padding: a\n * section's fill already reaches the side edges.\n * @default false\n */\n flush?: boolean;\n}\n\nexport const Card = forwardRef<HTMLDivElement, CardProps>(function Card(\n {\n className,\n interactive = false,\n highlighted = false,\n flush = false,\n onPointerEnter,\n onPointerLeave,\n ...props\n },\n ref,\n) {\n // Pointed at by a mouse or pen. Touch never lifts, so it is read from the pointer, not :hover (which a tap leaves stuck).\n const [pointed, setPointed] = useState(false);\n return (\n <div\n ref={ref}\n {...props}\n data-slot=\"card\"\n data-interactive={interactive || undefined}\n data-highlighted={highlighted || undefined}\n data-flush={flush || undefined}\n data-pointed={(interactive && pointed) || undefined}\n onPointerEnter={(event) => {\n onPointerEnter?.(event);\n if (interactive && event.pointerType !== \"touch\") setPointed(true);\n }}\n onPointerLeave={(event) => {\n onPointerLeave?.(event);\n setPointed(false);\n }}\n className={cnAll(cardVariants({ flush, highlighted }), interactive && \"card-lift\", className)}\n />\n );\n});\n\n/** Title and description, stacked. */\nexport type CardHeaderProps = ComponentPropsWithoutRef<\"div\">;\n\nexport const CardHeader = forwardRef<HTMLDivElement, CardHeaderProps>(function CardHeader(\n { className, ...props },\n ref,\n) {\n return (\n <div\n ref={ref}\n {...props}\n data-slot=\"card-header\"\n className={cn(cardHeaderVariants(), className)}\n />\n );\n});\n\n/**\n * The card's heading.\n *\n * A REAL heading, unlike the legacy `<div class=\"font-semibold\">`. None of\n * the 19 legacy call sites wrapped one, so 19 card titles were invisible to\n * screen-reader heading navigation -- the same shape of finding as the Avatar\n * port's missing `alt`, and the migration is when it gets answered.\n */\nexport interface CardTitleProps extends ComponentPropsWithoutRef<\"h3\"> {\n /**\n * Heading level.\n *\n * `3` by default, because a card almost always sits under a section\n * heading. It is a prop rather than a constant because the correct level\n * is a fact about the PAGE, and a fixed one would be wrong somewhere; the\n * level changes semantics only, never the rendered size.\n *\n * @default 3\n */\n level?: 1 | 2 | 3 | 4 | 5 | 6;\n}\n\nexport const CardTitle = forwardRef<HTMLHeadingElement, CardTitleProps>(function CardTitle(\n { className, level = 3, ...props },\n ref,\n) {\n // Static union, not a template literal: `h${level}` is a string React\n // accepts but TypeScript cannot narrow to an intrinsic element, and the\n // cast that silences it would also accept `h7`.\n const Heading = ({ 1: \"h1\", 2: \"h2\", 3: \"h3\", 4: \"h4\", 5: \"h5\", 6: \"h6\" } as const)[level];\n\n return (\n <Heading\n ref={ref}\n {...props}\n data-slot=\"card-title\"\n className={cn(cardTitleVariants(), className)}\n />\n );\n});\n\n/** Secondary text under the title. */\nexport type CardDescriptionProps = ComponentPropsWithoutRef<\"div\">;\n\nexport const CardDescription = forwardRef<HTMLDivElement, CardDescriptionProps>(\n function CardDescription({ className, ...props }, ref) {\n return (\n <div\n ref={ref}\n {...props}\n data-slot=\"card-description\"\n className={cn(cardDescriptionVariants(), className)}\n />\n );\n },\n);\n\n/** The card's body. */\nexport type CardContentProps = ComponentPropsWithoutRef<\"div\">;\n\nexport const CardContent = forwardRef<HTMLDivElement, CardContentProps>(function CardContent(\n { className, ...props },\n ref,\n) {\n return (\n <div\n ref={ref}\n {...props}\n data-slot=\"card-content\"\n className={cn(cardContentVariants(), className)}\n />\n );\n});\n\n/** Actions or metadata at the foot of the card. */\nexport type CardFooterProps = ComponentPropsWithoutRef<\"div\">;\n\nexport const CardFooter = forwardRef<HTMLDivElement, CardFooterProps>(function CardFooter(\n { className, ...props },\n ref,\n) {\n return (\n <div\n ref={ref}\n {...props}\n data-slot=\"card-footer\"\n className={cn(cardFooterVariants(), className)}\n />\n );\n});\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAmCA,MAAa,eAAe,IAC1B;CACE;CACA;CACA;CASA;AACF,GACA;CACE,UAAU;;;;;;;;;;;;;;;;;;;;;;;EAuBR,OAAO;GACL,OAAO;GACP,MAAM;EACR;;;;;;;EAOA,aAAa;GACX,OAAO;GACP,MAAM;EACR;CACF;CACA,kBAAkB,CAGhB;EAAE,OAAO;EAAM,aAAa;EAAM,OAAO;CAAyB,CACpE;CACA,iBAAiB;EAAE,OAAO;EAAO,aAAa;CAAM;AACtD,CACF;;;;;;;;AASA,MAAa,qBAAqB,IAAI,mDAAmD;AAEzF,MAAa,oBAAoB,IAAI,CAGnC,oDAGA,KACF,CAAC;AAED,MAAa,0BAA0B,IAAI,CACzC,0DACF,CAAC;AAED,MAAa,sBAAsB,IAAI,MAAM;AAE7C,MAAa,qBAAqB,IAAI,+CAA+C;AAyCrF,MAAa,OAAO,WAAsC,SAAS,KACjE,EACE,WACA,cAAc,OACd,cAAc,OACd,QAAQ,OACR,gBACA,gBACA,GAAG,SAEL,KACA;CAEA,MAAM,CAAC,SAAS,cAAc,SAAS,KAAK;CAC5C,OACE,oBAAC,OAAD;EACO;EACL,GAAI;EACJ,aAAU;EACV,oBAAkB,eAAe,KAAA;EACjC,oBAAkB,eAAe,KAAA;EACjC,cAAY,SAAS,KAAA;EACrB,gBAAe,eAAe,WAAY,KAAA;EAC1C,iBAAiB,UAAU;GACzB,iBAAiB,KAAK;GACtB,IAAI,eAAe,MAAM,gBAAgB,SAAS,WAAW,IAAI;EACnE;EACA,iBAAiB,UAAU;GACzB,iBAAiB,KAAK;GACtB,WAAW,KAAK;EAClB;EACA,WAAW,MAAM,aAAa;GAAE;GAAO;EAAY,CAAC,GAAG,eAAe,aAAa,SAAS;CAC7F,CAAA;AAEL,CAAC;AAKD,MAAa,aAAa,WAA4C,SAAS,WAC7E,EAAE,WAAW,GAAG,SAChB,KACA;CACA,OACE,oBAAC,OAAD;EACO;EACL,GAAI;EACJ,aAAU;EACV,WAAW,GAAG,mBAAmB,GAAG,SAAS;CAC9C,CAAA;AAEL,CAAC;AAwBD,MAAa,YAAY,WAA+C,SAAS,UAC/E,EAAE,WAAW,QAAQ,GAAG,GAAG,SAC3B,KACA;CAIA,MAAM,UAAW;EAAE,GAAG;EAAM,GAAG;EAAM,GAAG;EAAM,GAAG;EAAM,GAAG;EAAM,GAAG;CAAK,EAAY;CAEpF,OACE,oBAAC,SAAD;EACO;EACL,GAAI;EACJ,aAAU;EACV,WAAW,GAAG,kBAAkB,GAAG,SAAS;CAC7C,CAAA;AAEL,CAAC;AAKD,MAAa,kBAAkB,WAC7B,SAAS,gBAAgB,EAAE,WAAW,GAAG,SAAS,KAAK;CACrD,OACE,oBAAC,OAAD;EACO;EACL,GAAI;EACJ,aAAU;EACV,WAAW,GAAG,wBAAwB,GAAG,SAAS;CACnD,CAAA;AAEL,CACF;AAKA,MAAa,cAAc,WAA6C,SAAS,YAC/E,EAAE,WAAW,GAAG,SAChB,KACA;CACA,OACE,oBAAC,OAAD;EACO;EACL,GAAI;EACJ,aAAU;EACV,WAAW,GAAG,oBAAoB,GAAG,SAAS;CAC/C,CAAA;AAEL,CAAC;AAKD,MAAa,aAAa,WAA4C,SAAS,WAC7E,EAAE,WAAW,GAAG,SAChB,KACA;CACA,OACE,oBAAC,OAAD;EACO;EACL,GAAI;EACJ,aAAU;EACV,WAAW,GAAG,mBAAmB,GAAG,SAAS;CAC9C,CAAA;AAEL,CAAC"}
|
|
1
|
+
{"version":3,"file":"card.js","names":[],"sources":["../../src/card/card.tsx"],"sourcesContent":["\"use client\";\n\nimport { cva } from \"class-variance-authority\";\nimport { forwardRef, useState, type ComponentPropsWithoutRef } from \"react\";\n\nimport { cn, cnAll } from \"../lib/cn.js\";\n\n/**\n * Card, ported from the legacy shadcn component.\n *\n * Presentational, so there is no React Aria base -- React Aria ships no card,\n * and a container that is not focusable, selectable or interactive needs no\n * behaviour to inherit.\n *\n * Five parts, from six. The census (24 consumer files, the most-used\n * container in the app) reads: Card 55, CardContent 40, CardHeader 21,\n * CardTitle 19, CardDescription 8, CardFooter 2, **CardAction 0**.\n *\n * Dropping `CardAction` takes three things with it, which is most of the\n * legacy header's complexity: the `grid`, the `grid-rows-[auto_auto]`, and\n * the `has-data-[slot=card-action]:grid-cols-[1fr_auto]` rule that existed\n * only to reserve a column for it. With no action to place, a header is a\n * stack.\n *\n * Two more legacy rules are gone. `[.border-b]:pb-6` and `[.border-t]:pt-6`\n * added padding when the CONSUMER happened to add a border class -- clever,\n * but action at a distance through a class name we do not own, and the census\n * found zero application call sites relying on it (one call site passes\n * `border-b-0`, which removes a border it never had). `@container/card-header`\n * named a container nothing queried.\n *\n * Every string in the cva blocks below is a STATIC literal. Tailwind scans\n * source text, so a class assembled at runtime is invisible to it and would\n * simply never be emitted.\n */\nexport const cardVariants = cva(\n [\n \"flex flex-col\",\n \"rounded-panel border-(length:--border-width) border-border\",\n \"bg-card text-card-foreground shadow-surface\",\n\n // box-border and font-sans are not boilerplate. The reset in\n // @seekalfred/styles is OPT-IN, so a consumer may load none: content-box\n // would add the padding to any width they set (the Alert shipped\n // overflowing its container by 46px), and an unstyled document renders\n // text in a serif (the Accordion shipped its panel that way). Declaring\n // the family HERE rather than on each part means one declaration covers\n // the whole subtree by inheritance.\n \"box-border font-sans\",\n ],\n {\n variants: {\n /**\n * The block padding and the gap live here, not in the base, so `flush`\n * REPLACES them rather than relying on a merge to drop them: the\n * exported `cardVariants({ flush: true })` is correct on its own.\n *\n * Flush keeps the outline, radius, surface and shadow, and clips its\n * content to the inside of the rounded border: without the clip a filled\n * child (a table header, a chart) shows square corners over the curve and\n * covers the left and right hairline. `overflow: clip`, not `hidden`, so\n * the card does not become a scroll container and a sticky table header\n * inside it still sticks to the page. The sections keep their inline\n * padding, so text and a section's fill stay clear of the corner curve\n * (the fill still meets the side edges: background paints the padding).\n *\n * The header and footer hold text, so under flush they keep the 24px\n * block inset the shell gave up. Without it the title and the footer line\n * sat against the top and bottom borders, inside the corner curve, and\n * the clip cut their first glyphs. The inset is a custom property both\n * sections read, set per variant (never left undefined), so a nested\n * default card resets it, and a section's own `py-*` still wins by merge.\n * CardContent stays block-flush: a table or chart meets the edges.\n */\n flush: {\n false: \"gap-6 py-6 [--card-section-py:--spacing(0)]\",\n true: \"gap-0 py-0 overflow-clip [--card-section-py:--spacing(6)]\",\n },\n /**\n * A featured card: the hairline is replaced by a brand gradient edge,\n * painted by the `card-highlighted` utility in styles.css. The border\n * stays (transparent), so a highlighted card has exactly the box of a\n * plain one and a list of mixed cards does not shift.\n */\n highlighted: {\n false: \"\",\n true: \"card-highlighted border-transparent\",\n },\n },\n compoundVariants: [\n // The flush clip would cut away the gradient edge, which is painted in\n // the border area outside the padding box. See card-highlighted-flush.\n { flush: true, highlighted: true, class: \"card-highlighted-flush\" },\n ],\n defaultVariants: { flush: false, highlighted: false },\n },\n);\n\n/**\n * Padding is inline-only on the sections and block-only on the shell.\n *\n * That split is load-bearing rather than stylistic: it is what lets a\n * consumer run a divider or a full-bleed image edge to edge, which a single\n * `p-6` on the shell would make impossible.\n */\nexport const cardHeaderVariants = cva(\"flex flex-col gap-1.5 px-6 py-(--card-section-py)\");\n\nexport const cardTitleVariants = cva([\n // leading-* AFTER text-*: tailwind-merge treats a later text-* as owning\n // line-height and silently strips a leading-* that precedes it.\n \"font-display text-base leading-(--leading-none) font-semibold\",\n // The user-agent <h3> ships a 1em block margin and font-size 1.17em. The\n // legacy component was a <div> and never met either.\n \"m-0\",\n]);\n\nexport const cardDescriptionVariants = cva([\n \"text-sm leading-(--leading-normal) text-muted-foreground\",\n]);\n\nexport const cardContentVariants = cva(\"px-6\");\n\nexport const cardFooterVariants = cva(\"flex items-center px-6 py-(--card-section-py)\");\n\n/**\n * A content container.\n *\n * @example\n * <Card>\n * <CardHeader>\n * <CardTitle>Learning Progress</CardTitle>\n * <CardDescription>Across all active courses.</CardDescription>\n * </CardHeader>\n * <CardContent>Twelve of eighteen modules complete.</CardContent>\n * </Card>\n */\nexport interface CardProps extends ComponentPropsWithoutRef<\"div\"> {\n /**\n * Make the card respond to a pointer: it floats 2px on a spring while it is pointed\n * at, its border darkens and its shadow deepens. Mouse and pen only; touch never lifts.\n * Leave it off for a card that is not a target: a static card should not move.\n */\n interactive?: boolean;\n /**\n * A featured card: a brand gradient edge in place of the hairline. Use it for the\n * one card a view wants the eye to land on (a recommended action, the suggested\n * plan), never for several at once. The edge is decoration, so say WHY the card\n * is featured in its content too: colour alone does not reach a screen reader.\n * @default false\n */\n highlighted?: boolean;\n /**\n * Edge-to-edge content: no shell padding and no gap between the parts, and the\n * content is clipped to the rounded border. The outline, radius, surface and\n * shadow stay. For a table, list or full-bleed chart that must meet the card's\n * top and bottom. The header and footer keep a 24px block inset so their text\n * clears the corner curve, and every section keeps its inline padding: a\n * section's fill already reaches the side edges.\n * @default false\n */\n flush?: boolean;\n}\n\nexport const Card = forwardRef<HTMLDivElement, CardProps>(function Card(\n {\n className,\n interactive = false,\n highlighted = false,\n flush = false,\n onPointerEnter,\n onPointerLeave,\n ...props\n },\n ref,\n) {\n // Pointed at by a mouse or pen. Touch never lifts, so it is read from the pointer, not :hover (which a tap leaves stuck).\n const [pointed, setPointed] = useState(false);\n return (\n <div\n ref={ref}\n {...props}\n data-slot=\"card\"\n data-interactive={interactive || undefined}\n data-highlighted={highlighted || undefined}\n data-flush={flush || undefined}\n data-pointed={(interactive && pointed) || undefined}\n onPointerEnter={(event) => {\n onPointerEnter?.(event);\n if (interactive && event.pointerType !== \"touch\") setPointed(true);\n }}\n onPointerLeave={(event) => {\n onPointerLeave?.(event);\n setPointed(false);\n }}\n className={cnAll(cardVariants({ flush, highlighted }), interactive && \"card-lift\", className)}\n />\n );\n});\n\n/** Title and description, stacked. */\nexport type CardHeaderProps = ComponentPropsWithoutRef<\"div\">;\n\nexport const CardHeader = forwardRef<HTMLDivElement, CardHeaderProps>(function CardHeader(\n { className, ...props },\n ref,\n) {\n return (\n <div\n ref={ref}\n {...props}\n data-slot=\"card-header\"\n className={cn(cardHeaderVariants(), className)}\n />\n );\n});\n\n/**\n * The card's heading.\n *\n * A REAL heading, unlike the legacy `<div class=\"font-semibold\">`. None of\n * the 19 legacy call sites wrapped one, so 19 card titles were invisible to\n * screen-reader heading navigation -- the same shape of finding as the Avatar\n * port's missing `alt`, and the migration is when it gets answered.\n */\nexport interface CardTitleProps extends ComponentPropsWithoutRef<\"h3\"> {\n /**\n * Heading level.\n *\n * `3` by default, because a card almost always sits under a section\n * heading. It is a prop rather than a constant because the correct level\n * is a fact about the PAGE, and a fixed one would be wrong somewhere; the\n * level changes semantics only, never the rendered size.\n *\n * @default 3\n */\n level?: 1 | 2 | 3 | 4 | 5 | 6;\n}\n\nexport const CardTitle = forwardRef<HTMLHeadingElement, CardTitleProps>(function CardTitle(\n { className, level = 3, ...props },\n ref,\n) {\n // Static union, not a template literal: `h${level}` is a string React\n // accepts but TypeScript cannot narrow to an intrinsic element, and the\n // cast that silences it would also accept `h7`.\n const Heading = ({ 1: \"h1\", 2: \"h2\", 3: \"h3\", 4: \"h4\", 5: \"h5\", 6: \"h6\" } as const)[level];\n\n return (\n <Heading\n ref={ref}\n {...props}\n data-slot=\"card-title\"\n className={cn(cardTitleVariants(), className)}\n />\n );\n});\n\n/** Secondary text under the title. */\nexport type CardDescriptionProps = ComponentPropsWithoutRef<\"div\">;\n\nexport const CardDescription = forwardRef<HTMLDivElement, CardDescriptionProps>(\n function CardDescription({ className, ...props }, ref) {\n return (\n <div\n ref={ref}\n {...props}\n data-slot=\"card-description\"\n className={cn(cardDescriptionVariants(), className)}\n />\n );\n },\n);\n\n/** The card's body. */\nexport type CardContentProps = ComponentPropsWithoutRef<\"div\">;\n\nexport const CardContent = forwardRef<HTMLDivElement, CardContentProps>(function CardContent(\n { className, ...props },\n ref,\n) {\n return (\n <div\n ref={ref}\n {...props}\n data-slot=\"card-content\"\n className={cn(cardContentVariants(), className)}\n />\n );\n});\n\n/** Actions or metadata at the foot of the card. */\nexport type CardFooterProps = ComponentPropsWithoutRef<\"div\">;\n\nexport const CardFooter = forwardRef<HTMLDivElement, CardFooterProps>(function CardFooter(\n { className, ...props },\n ref,\n) {\n return (\n <div\n ref={ref}\n {...props}\n data-slot=\"card-footer\"\n className={cn(cardFooterVariants(), className)}\n />\n );\n});\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAmCA,MAAa,eAAe,IAC1B;CACE;CACA;CACA;CASA;AACF,GACA;CACE,UAAU;;;;;;;;;;;;;;;;;;;;;;;EAuBR,OAAO;GACL,OAAO;GACP,MAAM;EACR;;;;;;;EAOA,aAAa;GACX,OAAO;GACP,MAAM;EACR;CACF;CACA,kBAAkB,CAGhB;EAAE,OAAO;EAAM,aAAa;EAAM,OAAO;CAAyB,CACpE;CACA,iBAAiB;EAAE,OAAO;EAAO,aAAa;CAAM;AACtD,CACF;;;;;;;;AASA,MAAa,qBAAqB,IAAI,mDAAmD;AAEzF,MAAa,oBAAoB,IAAI,CAGnC,iEAGA,KACF,CAAC;AAED,MAAa,0BAA0B,IAAI,CACzC,0DACF,CAAC;AAED,MAAa,sBAAsB,IAAI,MAAM;AAE7C,MAAa,qBAAqB,IAAI,+CAA+C;AAyCrF,MAAa,OAAO,WAAsC,SAAS,KACjE,EACE,WACA,cAAc,OACd,cAAc,OACd,QAAQ,OACR,gBACA,gBACA,GAAG,SAEL,KACA;CAEA,MAAM,CAAC,SAAS,cAAc,SAAS,KAAK;CAC5C,OACE,oBAAC,OAAD;EACO;EACL,GAAI;EACJ,aAAU;EACV,oBAAkB,eAAe,KAAA;EACjC,oBAAkB,eAAe,KAAA;EACjC,cAAY,SAAS,KAAA;EACrB,gBAAe,eAAe,WAAY,KAAA;EAC1C,iBAAiB,UAAU;GACzB,iBAAiB,KAAK;GACtB,IAAI,eAAe,MAAM,gBAAgB,SAAS,WAAW,IAAI;EACnE;EACA,iBAAiB,UAAU;GACzB,iBAAiB,KAAK;GACtB,WAAW,KAAK;EAClB;EACA,WAAW,MAAM,aAAa;GAAE;GAAO;EAAY,CAAC,GAAG,eAAe,aAAa,SAAS;CAC7F,CAAA;AAEL,CAAC;AAKD,MAAa,aAAa,WAA4C,SAAS,WAC7E,EAAE,WAAW,GAAG,SAChB,KACA;CACA,OACE,oBAAC,OAAD;EACO;EACL,GAAI;EACJ,aAAU;EACV,WAAW,GAAG,mBAAmB,GAAG,SAAS;CAC9C,CAAA;AAEL,CAAC;AAwBD,MAAa,YAAY,WAA+C,SAAS,UAC/E,EAAE,WAAW,QAAQ,GAAG,GAAG,SAC3B,KACA;CAIA,MAAM,UAAW;EAAE,GAAG;EAAM,GAAG;EAAM,GAAG;EAAM,GAAG;EAAM,GAAG;EAAM,GAAG;CAAK,EAAY;CAEpF,OACE,oBAAC,SAAD;EACO;EACL,GAAI;EACJ,aAAU;EACV,WAAW,GAAG,kBAAkB,GAAG,SAAS;CAC7C,CAAA;AAEL,CAAC;AAKD,MAAa,kBAAkB,WAC7B,SAAS,gBAAgB,EAAE,WAAW,GAAG,SAAS,KAAK;CACrD,OACE,oBAAC,OAAD;EACO;EACL,GAAI;EACJ,aAAU;EACV,WAAW,GAAG,wBAAwB,GAAG,SAAS;CACnD,CAAA;AAEL,CACF;AAKA,MAAa,cAAc,WAA6C,SAAS,YAC/E,EAAE,WAAW,GAAG,SAChB,KACA;CACA,OACE,oBAAC,OAAD;EACO;EACL,GAAI;EACJ,aAAU;EACV,WAAW,GAAG,oBAAoB,GAAG,SAAS;CAC/C,CAAA;AAEL,CAAC;AAKD,MAAa,aAAa,WAA4C,SAAS,WAC7E,EAAE,WAAW,GAAG,SAChB,KACA;CACA,OACE,oBAAC,OAAD;EACO;EACL,GAAI;EACJ,aAAU;EACV,WAAW,GAAG,mBAAmB,GAAG,SAAS;CAC9C,CAAA;AAEL,CAAC"}
|
|
@@ -38,6 +38,7 @@ import { CheckboxProps } from "react-aria-components";
|
|
|
38
38
|
*/
|
|
39
39
|
export declare const checkboxVariants: (props?: import("class-variance-authority/types").ClassProp | undefined) => string;
|
|
40
40
|
export declare const checkboxBoxVariants: (props?: import("class-variance-authority/types").ClassProp | undefined) => string;
|
|
41
|
+
export declare const checkboxCardVariants: (props?: import("class-variance-authority/types").ClassProp | undefined) => string;
|
|
41
42
|
interface CheckboxProps$1 extends Omit<CheckboxProps, "className" | "style" | "children"> {
|
|
42
43
|
/**
|
|
43
44
|
* The label, and the control's accessible name.
|
|
@@ -47,7 +48,30 @@ interface CheckboxProps$1 extends Omit<CheckboxProps, "className" | "style" | "c
|
|
|
47
48
|
* accessible name, which is what every legacy call site would have been.
|
|
48
49
|
*/
|
|
49
50
|
children?: ReactNode;
|
|
50
|
-
/**
|
|
51
|
+
/**
|
|
52
|
+
* `card` draws the option as a selectable card: a bordered surface that is
|
|
53
|
+
* all hit target, with a `description`, an `icon` and `media`, and the box at
|
|
54
|
+
* its end. Selected shows a `primary-strong` edge AND the tick, so it never
|
|
55
|
+
* rests on colour alone. Focus is the card's own border turning dark, with no
|
|
56
|
+
* ring. The default variant renders exactly as before.
|
|
57
|
+
*
|
|
58
|
+
* @default "default"
|
|
59
|
+
*/
|
|
60
|
+
variant?: "default" | "card";
|
|
61
|
+
/**
|
|
62
|
+
* A line under the title, read as the option's description
|
|
63
|
+
* (`aria-describedby`), not part of its name. Card variant only.
|
|
64
|
+
*/
|
|
65
|
+
description?: ReactNode;
|
|
66
|
+
/** A decorative icon before the title. Card variant only; hidden from assistive tech. */
|
|
67
|
+
icon?: ReactNode;
|
|
68
|
+
/**
|
|
69
|
+
* Content above the title, edge to edge in the card: a thumbnail, a preview.
|
|
70
|
+
* Card variant only. Give an `<img>` its own `alt`, or `alt=""` when the
|
|
71
|
+
* title says it all.
|
|
72
|
+
*/
|
|
73
|
+
media?: ReactNode;
|
|
74
|
+
/** Extra classes for the label (the whole card in the card variant). Merged last, so they win conflicts. */
|
|
51
75
|
className?: string;
|
|
52
76
|
}
|
|
53
77
|
declare const Checkbox$1: import("react").ForwardRefExoticComponent<CheckboxProps$1 & import("react").RefAttributes<HTMLInputElement>>;
|