@seekalfred/primitives 0.7.1 → 0.7.3

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.
Files changed (101) hide show
  1. package/README.md +195 -4
  2. package/dist/alert/alert.d.ts +49 -4
  3. package/dist/alert/alert.js +66 -38
  4. package/dist/alert/alert.js.map +1 -1
  5. package/dist/alert/index.d.ts +2 -2
  6. package/dist/alert/index.js +2 -2
  7. package/dist/alert-dialog/alert-dialog.d.ts +17 -4
  8. package/dist/alert-dialog/alert-dialog.js +26 -2
  9. package/dist/alert-dialog/alert-dialog.js.map +1 -1
  10. package/dist/badge/badge.d.ts +1 -1
  11. package/dist/button/button.d.ts +2 -2
  12. package/dist/calendar/calendar.d.ts +1 -1
  13. package/dist/card/card.d.ts +22 -1
  14. package/dist/card/card.js +60 -7
  15. package/dist/card/card.js.map +1 -1
  16. package/dist/carousel/carousel.d.ts +21 -0
  17. package/dist/carousel/carousel.js +34 -8
  18. package/dist/carousel/carousel.js.map +1 -1
  19. package/dist/copy-button/copy-button.d.ts +18 -2
  20. package/dist/copy-button/copy-button.js +29 -8
  21. package/dist/copy-button/copy-button.js.map +1 -1
  22. package/dist/donut-chart/donut-chart.d.ts +22 -0
  23. package/dist/donut-chart/donut-chart.js +35 -18
  24. package/dist/donut-chart/donut-chart.js.map +1 -1
  25. package/dist/donut-chart/index.d.ts +2 -2
  26. package/dist/empty-state/empty-state.d.ts +21 -0
  27. package/dist/empty-state/empty-state.js +16 -4
  28. package/dist/empty-state/empty-state.js.map +1 -1
  29. package/dist/empty-state/index.d.ts +2 -2
  30. package/dist/empty-state/index.js +2 -2
  31. package/dist/filter-bar/filter-bar.d.ts +4 -1
  32. package/dist/filter-bar/filter-bar.js +53 -9
  33. package/dist/filter-bar/filter-bar.js.map +1 -1
  34. package/dist/index.d.ts +10 -10
  35. package/dist/index.js +7 -7
  36. package/dist/input/input.js +4 -4
  37. package/dist/input/input.js.map +1 -1
  38. package/dist/lib/field-shell.js +10 -3
  39. package/dist/lib/field-shell.js.map +1 -1
  40. package/dist/lib/field-surface.js +22 -1
  41. package/dist/lib/field-surface.js.map +1 -1
  42. package/dist/lib/use-copy-feedback.js +17 -3
  43. package/dist/lib/use-copy-feedback.js.map +1 -1
  44. package/dist/line-chart/index.d.ts +2 -2
  45. package/dist/line-chart/line-chart.d.ts +42 -2
  46. package/dist/line-chart/line-chart.js +143 -14
  47. package/dist/line-chart/line-chart.js.map +1 -1
  48. package/dist/money-input/money-input.d.ts +22 -1
  49. package/dist/money-input/money-input.js +13 -7
  50. package/dist/money-input/money-input.js.map +1 -1
  51. package/dist/morph-select/morph-select.d.ts +19 -1
  52. package/dist/morph-select/morph-select.js +21 -2
  53. package/dist/morph-select/morph-select.js.map +1 -1
  54. package/dist/multi-select/index.d.ts +2 -2
  55. package/dist/multi-select/index.js +2 -2
  56. package/dist/multi-select/multi-select.d.ts +43 -2
  57. package/dist/multi-select/multi-select.js +93 -36
  58. package/dist/multi-select/multi-select.js.map +1 -1
  59. package/dist/password-field/password-field.d.ts +16 -0
  60. package/dist/password-field/password-field.js +37 -8
  61. package/dist/password-field/password-field.js.map +1 -1
  62. package/dist/popover/index.d.ts +2 -2
  63. package/dist/popover/index.js +2 -2
  64. package/dist/popover/popover.d.ts +39 -2
  65. package/dist/popover/popover.js +29 -2
  66. package/dist/popover/popover.js.map +1 -1
  67. package/dist/progress/progress.d.ts +16 -2
  68. package/dist/progress/progress.js +15 -8
  69. package/dist/progress/progress.js.map +1 -1
  70. package/dist/radio-group/index.d.ts +2 -2
  71. package/dist/radio-group/index.js +2 -2
  72. package/dist/radio-group/radio-group.d.ts +33 -3
  73. package/dist/radio-group/radio-group.js +109 -9
  74. package/dist/radio-group/radio-group.js.map +1 -1
  75. package/dist/scroll-area/scroll-area.js +4 -3
  76. package/dist/scroll-area/scroll-area.js.map +1 -1
  77. package/dist/search-field/search-field.d.ts +22 -1
  78. package/dist/search-field/search-field.js +22 -7
  79. package/dist/search-field/search-field.js.map +1 -1
  80. package/dist/select/select.d.ts +1 -1
  81. package/dist/select/select.js +8 -6
  82. package/dist/select/select.js.map +1 -1
  83. package/dist/separator/separator.d.ts +21 -9
  84. package/dist/separator/separator.js +33 -11
  85. package/dist/separator/separator.js.map +1 -1
  86. package/dist/stat-card/index.d.ts +2 -2
  87. package/dist/stat-card/index.js +2 -2
  88. package/dist/stat-card/stat-card.d.ts +34 -1
  89. package/dist/stat-card/stat-card.js +51 -21
  90. package/dist/stat-card/stat-card.js.map +1 -1
  91. package/dist/styles.css +1 -1
  92. package/dist/table/table.d.ts +1 -1
  93. package/dist/time-picker/time-picker.d.ts +36 -1
  94. package/dist/time-picker/time-picker.js +33 -5
  95. package/dist/time-picker/time-picker.js.map +1 -1
  96. package/dist/toast/toast.d.ts +2 -2
  97. package/dist/usage-meter/index.d.ts +2 -2
  98. package/dist/usage-meter/usage-meter.d.ts +30 -0
  99. package/dist/usage-meter/usage-meter.js +56 -38
  100. package/dist/usage-meter/usage-meter.js.map +1 -1
  101. package/package.json +3 -3
@@ -7,7 +7,8 @@ import { ReactNode } from "react";
7
7
  *
8
8
  * Built from `Dialog`'s own overlay and panel styles and the same React Aria
9
9
  * parts, but with its own layout: a header (title, a grey subtitle and a
10
- * circular close button), a thin divider, the body text, then the actions.
10
+ * circular close button), a thin divider, the description sentence, an
11
+ * optional `body` of rich content, then the actions.
11
12
  * The legacy `alert-dialog.tsx` was a 157-line clone of `dialog.tsx`; this
12
13
  * shares the tokens and the motion classes instead, so the two cannot drift
13
14
  * on radius, scrim or entrance.
@@ -30,7 +31,7 @@ import { ReactNode } from "react";
30
31
  * header's copy of the second one.
31
32
  * - `description` is REQUIRED. A confirmation with no explanation is the
32
33
  * thing this component exists to prevent, and the role only associates it
33
- * when it is there. It is the body text above the actions.
34
+ * when it is there. It is the sentence under the divider.
34
35
  *
35
36
  * THE TWO ACTIONS MUST NEVER LOOK ALIKE. `AlertDialogCancel` is the secondary
36
37
  * button (white, light border) and cancels through `slot="close"`;
@@ -43,8 +44,8 @@ import { ReactNode } from "react";
43
44
  */
44
45
  export interface AlertDialogProps extends Omit<DialogProps, "role" | "closeButton" | "isDismissable"> {
45
46
  /**
46
- * What is about to happen, and whether it can be undone. Rendered as the body text
47
- * between the divider and the actions.
47
+ * What is about to happen, and whether it can be undone. Rendered as the
48
+ * sentence under the divider, above the `body` slot (if any) and the actions.
48
49
  *
49
50
  * **Required**, unlike on `Dialog`. `role="alertdialog"` is what makes
50
51
  * React Aria wire this to `aria-describedby`, so it is both the sentence
@@ -53,6 +54,18 @@ export interface AlertDialogProps extends Omit<DialogProps, "role" | "closeButto
53
54
  description: ReactNode;
54
55
  /** A short grey line under the title, in the header. */
55
56
  subtitle?: ReactNode;
57
+ /**
58
+ * Rich content between the description and the actions: a plan comparison,
59
+ * a before/after diff, a quoted reply, a form field or a choice to make.
60
+ *
61
+ * It is NOT part of the accessible description: the screen reader announces
62
+ * the one `description` sentence with the name, and the body is read in
63
+ * place or as focus reaches its controls (each with its own label). So the
64
+ * `description` must say everything needed to answer, including that a
65
+ * choice or field below is required ("Pick what happens to your data.").
66
+ * `false`, `true` and `""` (as from `cond && <X />`) render no body.
67
+ */
68
+ body?: ReactNode;
56
69
  /** The actions. Give the cancel button `slot="close"`. */
57
70
  children?: ReactNode;
58
71
  }
@@ -10,6 +10,11 @@ import { Fragment as Fragment$1, jsx, jsxs } from "react/jsx-runtime";
10
10
  //#region src/alert-dialog/alert-dialog.tsx
11
11
  const alertDialogActionsVariants = cva("flex flex-wrap items-center justify-end gap-2 px-8 pb-8");
12
12
  /**
13
+ * The body slot: the description's side padding, so the two share an edge,
14
+ * and the description's own bottom space repeated below it before the actions.
15
+ */
16
+ const alertDialogBodyVariants = cva("min-w-0 px-8 pb-6 text-sm leading-(--leading-normal) text-foreground");
17
+ /**
13
18
  * The cancel action: the secondary button, wired to dismiss the dialog.
14
19
  *
15
20
  * @example
@@ -45,6 +50,16 @@ const AlertDialogAction = forwardRef(function AlertDialogAction({ className, ...
45
50
  });
46
51
  });
47
52
  const alertDialogHeaderVariants = cva("flex items-start gap-4 border-b-(length:--border-width) border-border px-8 pt-8 pb-6");
53
+ /**
54
+ * The title column. With no subtitle it is at least the close button's height
55
+ * and centres the title in it, so a one-line title sits level with the close;
56
+ * a longer title grows the column from the top as before. With a subtitle the
57
+ * two lines start at the top of the button, as they always have.
58
+ */
59
+ const alertDialogTitleBlockVariants = cva("min-w-0 flex-1", { variants: { subtitle: {
60
+ true: "",
61
+ false: "flex min-h-9 flex-col justify-center"
62
+ } } });
48
63
  const alertDialogTitleVariants = cva("m-0 text-lg leading-(--leading-tight) font-semibold text-foreground");
49
64
  const alertDialogCloseVariants = cva([
50
65
  "inline-flex size-9 shrink-0 items-center justify-center",
@@ -68,7 +83,11 @@ function Swappable({ value }) {
68
83
  children: value
69
84
  });
70
85
  }
71
- const AlertDialog = forwardRef(function AlertDialog({ title, subtitle, beforeTitle, description, children, className, closeLabel = "Close", overlayClassName, entrance = "rise", ...props }, ref) {
86
+ /** `cond && <X />` hands over `false`; React renders nothing for it, so neither does the slot. */
87
+ function isEmpty(node) {
88
+ return node == null || typeof node === "boolean" || node === "";
89
+ }
90
+ const AlertDialog = forwardRef(function AlertDialog({ title, subtitle, beforeTitle, description, body, children, className, closeLabel = "Close", overlayClassName, entrance = "rise", ...props }, ref) {
72
91
  return /* @__PURE__ */ jsx(ModalOverlay, {
73
92
  ...props,
74
93
  isDismissable: false,
@@ -87,7 +106,7 @@ const AlertDialog = forwardRef(function AlertDialog({ title, subtitle, beforeTit
87
106
  "data-slot": "alert-dialog-header",
88
107
  className: alertDialogHeaderVariants(),
89
108
  children: [/* @__PURE__ */ jsxs("div", {
90
- className: "min-w-0 flex-1",
109
+ className: alertDialogTitleBlockVariants({ subtitle: subtitle != null }),
91
110
  children: [/* @__PURE__ */ jsx(Heading, {
92
111
  slot: "title",
93
112
  "data-slot": "dialog-title",
@@ -121,6 +140,11 @@ const AlertDialog = forwardRef(function AlertDialog({ title, subtitle, beforeTit
121
140
  className: "m-0 block px-8 py-6 text-sm leading-(--leading-normal) text-foreground",
122
141
  children: /* @__PURE__ */ jsx(Swappable, { value: description })
123
142
  }),
143
+ isEmpty(body) ? null : /* @__PURE__ */ jsx("div", {
144
+ "data-slot": "alert-dialog-body",
145
+ className: alertDialogBodyVariants(),
146
+ children: body
147
+ }),
124
148
  children == null ? null : /* @__PURE__ */ jsx("div", {
125
149
  "data-slot": "alert-dialog-actions",
126
150
  className: alertDialogActionsVariants(),
@@ -1 +1 @@
1
- {"version":3,"file":"alert-dialog.js","names":["Button","AriaModalOverlay","AriaModal","AriaDialog","AriaHeading","AriaButton","AriaText"],"sources":["../../src/alert-dialog/alert-dialog.tsx"],"sourcesContent":["\"use client\";\n\nimport { cva } from \"class-variance-authority\";\nimport { forwardRef, type ReactNode } from \"react\";\nimport {\n Button as AriaButton,\n Dialog as AriaDialog,\n Heading as AriaHeading,\n Modal as AriaModal,\n ModalOverlay as AriaModalOverlay,\n Text as AriaText,\n} from \"react-aria-components\";\n\nimport { Button, type ButtonProps } from \"../button/button.js\";\nimport { cnAll } from \"../lib/cn.js\";\nimport { SwapText } from \"../lib/swap-text.js\";\nimport { dialogOverlayVariants, dialogPanelVariants, type DialogProps } from \"../dialog/dialog.js\";\n\n/**\n * AlertDialog: a confirmation that must be answered.\n *\n * Built from `Dialog`'s own overlay and panel styles and the same React Aria\n * parts, but with its own layout: a header (title, a grey subtitle and a\n * circular close button), a thin divider, the body text, then the actions.\n * The legacy `alert-dialog.tsx` was a 157-line clone of `dialog.tsx`; this\n * shares the tokens and the motion classes instead, so the two cannot drift\n * on radius, scrim or entrance.\n *\n * Census: 2 files, all eight parts used twice each, `AlertDialogPortal`,\n * `AlertDialogOverlay` and `AlertDialogTrigger` never rendered externally.\n *\n * What the preset forces, and why each one is a defect if forgotten:\n *\n * - `role=\"alertdialog\"`. React Aria associates the description with\n * `aria-describedby` ONLY for this role (useDialog.mjs:29), so a\n * confirmation built from a plain Dialog loses the sentence explaining\n * what is about to happen.\n * - `isDismissable={false}`. Clicking the page must not resolve a\n * destructive confirmation. Escape still cancels, which is the APG\n * behaviour and what Radix does.\n * - THE CLOSE (X) IS CANCEL. It is `slot=\"close\"`, the same dismiss wiring as\n * a Cancel button, so it goes through the same `onOpenChange(false)` and\n * focus returns to the trigger. It is not a third answer: it is the\n * header's copy of the second one.\n * - `description` is REQUIRED. A confirmation with no explanation is the\n * thing this component exists to prevent, and the role only associates it\n * when it is there. It is the body text above the actions.\n *\n * THE TWO ACTIONS MUST NEVER LOOK ALIKE. `AlertDialogCancel` is the secondary\n * button (white, light border) and cancels through `slot=\"close\"`;\n * `AlertDialogAction` is the primary button painted destructive red, because a\n * confirmation here is a destructive one. The legacy `AlertDialogAction`\n * defaulted to the gradient primary and BOTH call sites overrode it back to\n * destructive by hand (`data-table.tsx:146`, `calendar-view.tsx:638`); the red\n * is the default now, so nobody fights it. Plain `Button`s still work in the\n * actions slot for anything else.\n */\nexport interface AlertDialogProps extends Omit<\n DialogProps,\n \"role\" | \"closeButton\" | \"isDismissable\"\n> {\n /**\n * What is about to happen, and whether it can be undone. Rendered as the body text\n * between the divider and the actions.\n *\n * **Required**, unlike on `Dialog`. `role=\"alertdialog\"` is what makes\n * React Aria wire this to `aria-describedby`, so it is both the sentence\n * the user needs and the one the screen reader announces with the name.\n */\n description: ReactNode;\n /** A short grey line under the title, in the header. */\n subtitle?: ReactNode;\n /** The actions. Give the cancel button `slot=\"close\"`. */\n children?: ReactNode;\n}\n\nconst alertDialogActionsVariants = cva(\"flex flex-wrap items-center justify-end gap-2 px-8 pb-8\");\n\n/**\n * The cancel action: the secondary button, wired to dismiss the dialog.\n *\n * @example\n * <AlertDialogCancel>Cancel</AlertDialogCancel>\n */\nexport const AlertDialogCancel = forwardRef<HTMLButtonElement, ButtonProps>(\n function AlertDialogCancel(props, ref) {\n return <Button ref={ref} slot=\"close\" variant=\"secondary\" size=\"sm\" {...props} />;\n },\n);\n\n/**\n * The confirm action: the primary button in the destructive red.\n *\n * It is the primary button with its fill, hover, pressed and label roles\n * pointed at the destructive tokens, so it keeps the primary's press motion\n * and shadow. Variables on the element, rather than a competing background\n * class, so no stylesheet order decides which colour wins.\n *\n * @example\n * <AlertDialogAction onPress={remove}>Delete</AlertDialogAction>\n */\nexport const AlertDialogAction = forwardRef<HTMLButtonElement, ButtonProps>(\n function AlertDialogAction({ className, ...props }, ref) {\n return (\n <Button\n ref={ref}\n variant=\"primary\"\n size=\"sm\"\n className={cnAll(\n \"[--color-primary:var(--color-destructive)] [--color-primary-hover:var(--color-destructive-hover)] [--color-primary-pressed:var(--color-destructive-pressed)] [--color-primary-foreground:var(--color-destructive-foreground)]\",\n className,\n )}\n {...props}\n />\n );\n },\n);\n\nconst alertDialogHeaderVariants = cva(\n \"flex items-start gap-4 border-b-(length:--border-width) border-border px-8 pt-8 pb-6\",\n);\n\nconst alertDialogTitleVariants = cva(\n \"m-0 text-lg leading-(--leading-tight) font-semibold text-foreground\",\n);\n\nconst alertDialogCloseVariants = cva([\n \"inline-flex size-9 shrink-0 items-center justify-center\",\n \"rounded-circle box-border cursor-pointer\",\n \"border-(length:--border-width) border-border bg-surface p-0 text-text-secondary\",\n // Fill on hover, a quick press to .96 and a spring release (see calendar-nav-motion).\n \"calendar-nav-motion\",\n \"data-[hovered]:bg-muted data-[hovered]:text-foreground\",\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 \"[&_svg]:pointer-events-none [&_svg]:size-4\",\n]);\n\n/** A string swaps as a whole when its text changes while the dialog is open. */\nfunction Swappable({ value }: { value: ReactNode }) {\n if (typeof value !== \"string\") return <>{value}</>;\n return (\n <SwapText swapKey={value} className=\"block\">\n {value}\n </SwapText>\n );\n}\n\nexport const AlertDialog = forwardRef<HTMLElement, AlertDialogProps>(function AlertDialog(\n {\n title,\n subtitle,\n beforeTitle,\n description,\n children,\n className,\n closeLabel = \"Close\",\n overlayClassName,\n entrance = \"rise\",\n ...props\n },\n ref,\n) {\n return (\n <AriaModalOverlay\n {...props}\n isDismissable={false}\n data-slot=\"dialog-overlay\"\n className={cnAll(\n dialogOverlayVariants(),\n entrance === \"rise\" && \"dialog-overlay-motion\",\n overlayClassName,\n )}\n >\n <AriaModal className=\"contents\">\n <AriaDialog\n ref={ref}\n role=\"alertdialog\"\n data-slot=\"dialog\"\n className={cnAll(\n dialogPanelVariants(),\n // The panel has no padding of its own: the header, body and actions pad themselves,\n // so the divider can run edge to edge.\n \"gap-0 p-0\",\n entrance === \"rise\" && \"dialog-panel-motion\",\n className,\n )}\n >\n {beforeTitle}\n <div data-slot=\"alert-dialog-header\" className={alertDialogHeaderVariants()}>\n <div className=\"min-w-0 flex-1\">\n {/* slot=\"title\" is what React Aria wires aria-labelledby to. */}\n <AriaHeading\n slot=\"title\"\n data-slot=\"dialog-title\"\n className={alertDialogTitleVariants()}\n >\n <Swappable value={title} />\n </AriaHeading>\n {subtitle == null ? null : (\n <p\n data-slot=\"alert-dialog-subtitle\"\n className=\"m-0 mt-1 text-sm leading-(--leading-normal) text-text-secondary\"\n >\n <Swappable value={subtitle} />\n </p>\n )}\n </div>\n {/* slot=\"close\" is the same dismiss wiring as a Cancel button: one handler, focus back to the trigger. */}\n <AriaButton\n slot=\"close\"\n aria-label={closeLabel}\n data-slot=\"alert-dialog-close\"\n className={alertDialogCloseVariants()}\n >\n <svg\n aria-hidden=\"true\"\n viewBox=\"0 0 16 16\"\n fill=\"none\"\n stroke=\"currentColor\"\n strokeWidth=\"1.75\"\n strokeLinecap=\"round\"\n >\n <path d=\"m4 4 8 8M12 4l-8 8\" />\n </svg>\n </AriaButton>\n </div>\n <AriaText\n slot=\"description\"\n elementType=\"p\"\n data-slot=\"dialog-description\"\n className=\"m-0 block px-8 py-6 text-sm leading-(--leading-normal) text-foreground\"\n >\n <Swappable value={description} />\n </AriaText>\n {children == null ? null : (\n <div data-slot=\"alert-dialog-actions\" className={alertDialogActionsVariants()}>\n {children}\n </div>\n )}\n </AriaDialog>\n </AriaModal>\n </AriaModalOverlay>\n );\n});\n"],"mappings":";;;;;;;;;;AA4EA,MAAM,6BAA6B,IAAI,yDAAyD;;;;;;;AAQhG,MAAa,oBAAoB,WAC/B,SAAS,kBAAkB,OAAO,KAAK;CACrC,OAAO,oBAACA,UAAD;EAAa;EAAK,MAAK;EAAQ,SAAQ;EAAY,MAAK;EAAK,GAAI;CAAQ,CAAA;AAClF,CACF;;;;;;;;;;;;AAaA,MAAa,oBAAoB,WAC/B,SAAS,kBAAkB,EAAE,WAAW,GAAG,SAAS,KAAK;CACvD,OACE,oBAACA,UAAD;EACO;EACL,SAAQ;EACR,MAAK;EACL,WAAW,MACT,iOACA,SACF;EACA,GAAI;CACL,CAAA;AAEL,CACF;AAEA,MAAM,4BAA4B,IAChC,sFACF;AAEA,MAAM,2BAA2B,IAC/B,qEACF;AAEA,MAAM,2BAA2B,IAAI;CACnC;CACA;CACA;CAEA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;AACF,CAAC;;AAGD,SAAS,UAAU,EAAE,SAA+B;CAClD,IAAI,OAAO,UAAU,UAAU,OAAO,oBAAA,YAAA,EAAA,UAAG,MAAQ,CAAA;CACjD,OACE,oBAAC,UAAD;EAAU,SAAS;EAAO,WAAU;EACjC,UAAA;CACO,CAAA;AAEd;AAEA,MAAa,cAAc,WAA0C,SAAS,YAC5E,EACE,OACA,UACA,aACA,aACA,UACA,WACA,aAAa,SACb,kBACA,WAAW,QACX,GAAG,SAEL,KACA;CACA,OACE,oBAACC,cAAD;EACE,GAAI;EACJ,eAAe;EACf,aAAU;EACV,WAAW,MACT,sBAAsB,GACtB,aAAa,UAAU,yBACvB,gBACF;EAEA,UAAA,oBAACC,OAAD;GAAW,WAAU;GACnB,UAAA,qBAACC,QAAD;IACO;IACL,MAAK;IACL,aAAU;IACV,WAAW,MACT,oBAAoB,GAGpB,aACA,aAAa,UAAU,uBACvB,SACF;IAXF,UAAA;KAaG;KACD,qBAAC,OAAD;MAAK,aAAU;MAAsB,WAAW,0BAA0B;MAA1E,UAAA,CACE,qBAAC,OAAD;OAAK,WAAU;OAAf,UAAA,CAEE,oBAACC,SAAD;QACE,MAAK;QACL,aAAU;QACV,WAAW,yBAAyB;QAEpC,UAAA,oBAAC,WAAD,EAAW,OAAO,MAAQ,CAAA;OACf,CAAA,GACZ,YAAY,OAAO,OAClB,oBAAC,KAAD;QACE,aAAU;QACV,WAAU;QAEV,UAAA,oBAAC,WAAD,EAAW,OAAO,SAAW,CAAA;OAC5B,CAAA,CAEF;MAEL,CAAA,GAAA,oBAACC,QAAD;OACE,MAAK;OACL,cAAY;OACZ,aAAU;OACV,WAAW,yBAAyB;OAEpC,UAAA,oBAAC,OAAD;QACE,eAAY;QACZ,SAAQ;QACR,MAAK;QACL,QAAO;QACP,aAAY;QACZ,eAAc;QAEd,UAAA,oBAAC,QAAD,EAAM,GAAE,qBAAsB,CAAA;OAC3B,CAAA;MACK,CAAA,CACT;;KACL,oBAACC,MAAD;MACE,MAAK;MACL,aAAY;MACZ,aAAU;MACV,WAAU;MAEV,UAAA,oBAAC,WAAD,EAAW,OAAO,YAAc,CAAA;KACxB,CAAA;KACT,YAAY,OAAO,OAClB,oBAAC,OAAD;MAAK,aAAU;MAAuB,WAAW,2BAA2B;MACzE;KACE,CAAA;IAEG;;EACH,CAAA;CACK,CAAA;AAEtB,CAAC"}
1
+ {"version":3,"file":"alert-dialog.js","names":["Button","AriaModalOverlay","AriaModal","AriaDialog","AriaHeading","AriaButton","AriaText"],"sources":["../../src/alert-dialog/alert-dialog.tsx"],"sourcesContent":["\"use client\";\n\nimport { cva } from \"class-variance-authority\";\nimport { forwardRef, type ReactNode } from \"react\";\nimport {\n Button as AriaButton,\n Dialog as AriaDialog,\n Heading as AriaHeading,\n Modal as AriaModal,\n ModalOverlay as AriaModalOverlay,\n Text as AriaText,\n} from \"react-aria-components\";\n\nimport { Button, type ButtonProps } from \"../button/button.js\";\nimport { cnAll } from \"../lib/cn.js\";\nimport { SwapText } from \"../lib/swap-text.js\";\nimport { dialogOverlayVariants, dialogPanelVariants, type DialogProps } from \"../dialog/dialog.js\";\n\n/**\n * AlertDialog: a confirmation that must be answered.\n *\n * Built from `Dialog`'s own overlay and panel styles and the same React Aria\n * parts, but with its own layout: a header (title, a grey subtitle and a\n * circular close button), a thin divider, the description sentence, an\n * optional `body` of rich content, then the actions.\n * The legacy `alert-dialog.tsx` was a 157-line clone of `dialog.tsx`; this\n * shares the tokens and the motion classes instead, so the two cannot drift\n * on radius, scrim or entrance.\n *\n * Census: 2 files, all eight parts used twice each, `AlertDialogPortal`,\n * `AlertDialogOverlay` and `AlertDialogTrigger` never rendered externally.\n *\n * What the preset forces, and why each one is a defect if forgotten:\n *\n * - `role=\"alertdialog\"`. React Aria associates the description with\n * `aria-describedby` ONLY for this role (useDialog.mjs:29), so a\n * confirmation built from a plain Dialog loses the sentence explaining\n * what is about to happen.\n * - `isDismissable={false}`. Clicking the page must not resolve a\n * destructive confirmation. Escape still cancels, which is the APG\n * behaviour and what Radix does.\n * - THE CLOSE (X) IS CANCEL. It is `slot=\"close\"`, the same dismiss wiring as\n * a Cancel button, so it goes through the same `onOpenChange(false)` and\n * focus returns to the trigger. It is not a third answer: it is the\n * header's copy of the second one.\n * - `description` is REQUIRED. A confirmation with no explanation is the\n * thing this component exists to prevent, and the role only associates it\n * when it is there. It is the sentence under the divider.\n *\n * THE TWO ACTIONS MUST NEVER LOOK ALIKE. `AlertDialogCancel` is the secondary\n * button (white, light border) and cancels through `slot=\"close\"`;\n * `AlertDialogAction` is the primary button painted destructive red, because a\n * confirmation here is a destructive one. The legacy `AlertDialogAction`\n * defaulted to the gradient primary and BOTH call sites overrode it back to\n * destructive by hand (`data-table.tsx:146`, `calendar-view.tsx:638`); the red\n * is the default now, so nobody fights it. Plain `Button`s still work in the\n * actions slot for anything else.\n */\nexport interface AlertDialogProps extends Omit<\n DialogProps,\n \"role\" | \"closeButton\" | \"isDismissable\"\n> {\n /**\n * What is about to happen, and whether it can be undone. Rendered as the\n * sentence under the divider, above the `body` slot (if any) and the actions.\n *\n * **Required**, unlike on `Dialog`. `role=\"alertdialog\"` is what makes\n * React Aria wire this to `aria-describedby`, so it is both the sentence\n * the user needs and the one the screen reader announces with the name.\n */\n description: ReactNode;\n /** A short grey line under the title, in the header. */\n subtitle?: ReactNode;\n /**\n * Rich content between the description and the actions: a plan comparison,\n * a before/after diff, a quoted reply, a form field or a choice to make.\n *\n * It is NOT part of the accessible description: the screen reader announces\n * the one `description` sentence with the name, and the body is read in\n * place or as focus reaches its controls (each with its own label). So the\n * `description` must say everything needed to answer, including that a\n * choice or field below is required (\"Pick what happens to your data.\").\n * `false`, `true` and `\"\"` (as from `cond && <X />`) render no body.\n */\n body?: ReactNode;\n /** The actions. Give the cancel button `slot=\"close\"`. */\n children?: ReactNode;\n}\n\nconst alertDialogActionsVariants = cva(\"flex flex-wrap items-center justify-end gap-2 px-8 pb-8\");\n\n/**\n * The body slot: the description's side padding, so the two share an edge,\n * and the description's own bottom space repeated below it before the actions.\n */\nconst alertDialogBodyVariants = cva(\n \"min-w-0 px-8 pb-6 text-sm leading-(--leading-normal) text-foreground\",\n);\n\n/**\n * The cancel action: the secondary button, wired to dismiss the dialog.\n *\n * @example\n * <AlertDialogCancel>Cancel</AlertDialogCancel>\n */\nexport const AlertDialogCancel = forwardRef<HTMLButtonElement, ButtonProps>(\n function AlertDialogCancel(props, ref) {\n return <Button ref={ref} slot=\"close\" variant=\"secondary\" size=\"sm\" {...props} />;\n },\n);\n\n/**\n * The confirm action: the primary button in the destructive red.\n *\n * It is the primary button with its fill, hover, pressed and label roles\n * pointed at the destructive tokens, so it keeps the primary's press motion\n * and shadow. Variables on the element, rather than a competing background\n * class, so no stylesheet order decides which colour wins.\n *\n * @example\n * <AlertDialogAction onPress={remove}>Delete</AlertDialogAction>\n */\nexport const AlertDialogAction = forwardRef<HTMLButtonElement, ButtonProps>(\n function AlertDialogAction({ className, ...props }, ref) {\n return (\n <Button\n ref={ref}\n variant=\"primary\"\n size=\"sm\"\n className={cnAll(\n \"[--color-primary:var(--color-destructive)] [--color-primary-hover:var(--color-destructive-hover)] [--color-primary-pressed:var(--color-destructive-pressed)] [--color-primary-foreground:var(--color-destructive-foreground)]\",\n className,\n )}\n {...props}\n />\n );\n },\n);\n\nconst alertDialogHeaderVariants = cva(\n \"flex items-start gap-4 border-b-(length:--border-width) border-border px-8 pt-8 pb-6\",\n);\n\n/**\n * The title column. With no subtitle it is at least the close button's height\n * and centres the title in it, so a one-line title sits level with the close;\n * a longer title grows the column from the top as before. With a subtitle the\n * two lines start at the top of the button, as they always have.\n */\nconst alertDialogTitleBlockVariants = cva(\"min-w-0 flex-1\", {\n variants: {\n subtitle: {\n true: \"\",\n false: \"flex min-h-9 flex-col justify-center\",\n },\n },\n});\n\nconst alertDialogTitleVariants = cva(\n \"m-0 text-lg leading-(--leading-tight) font-semibold text-foreground\",\n);\n\nconst alertDialogCloseVariants = cva([\n \"inline-flex size-9 shrink-0 items-center justify-center\",\n \"rounded-circle box-border cursor-pointer\",\n \"border-(length:--border-width) border-border bg-surface p-0 text-text-secondary\",\n // Fill on hover, a quick press to .96 and a spring release (see calendar-nav-motion).\n \"calendar-nav-motion\",\n \"data-[hovered]:bg-muted data-[hovered]:text-foreground\",\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 \"[&_svg]:pointer-events-none [&_svg]:size-4\",\n]);\n\n/** A string swaps as a whole when its text changes while the dialog is open. */\nfunction Swappable({ value }: { value: ReactNode }) {\n if (typeof value !== \"string\") return <>{value}</>;\n return (\n <SwapText swapKey={value} className=\"block\">\n {value}\n </SwapText>\n );\n}\n\n/** `cond && <X />` hands over `false`; React renders nothing for it, so neither does the slot. */\nfunction isEmpty(node: ReactNode) {\n return node == null || typeof node === \"boolean\" || node === \"\";\n}\n\nexport const AlertDialog = forwardRef<HTMLElement, AlertDialogProps>(function AlertDialog(\n {\n title,\n subtitle,\n beforeTitle,\n description,\n body,\n children,\n className,\n closeLabel = \"Close\",\n overlayClassName,\n entrance = \"rise\",\n ...props\n },\n ref,\n) {\n return (\n <AriaModalOverlay\n {...props}\n isDismissable={false}\n data-slot=\"dialog-overlay\"\n className={cnAll(\n dialogOverlayVariants(),\n entrance === \"rise\" && \"dialog-overlay-motion\",\n overlayClassName,\n )}\n >\n <AriaModal className=\"contents\">\n <AriaDialog\n ref={ref}\n role=\"alertdialog\"\n data-slot=\"dialog\"\n className={cnAll(\n dialogPanelVariants(),\n // The panel has no padding of its own: the header, body and actions pad themselves,\n // so the divider can run edge to edge.\n \"gap-0 p-0\",\n entrance === \"rise\" && \"dialog-panel-motion\",\n className,\n )}\n >\n {beforeTitle}\n <div data-slot=\"alert-dialog-header\" className={alertDialogHeaderVariants()}>\n <div className={alertDialogTitleBlockVariants({ subtitle: subtitle != null })}>\n {/* slot=\"title\" is what React Aria wires aria-labelledby to. */}\n <AriaHeading\n slot=\"title\"\n data-slot=\"dialog-title\"\n className={alertDialogTitleVariants()}\n >\n <Swappable value={title} />\n </AriaHeading>\n {subtitle == null ? null : (\n <p\n data-slot=\"alert-dialog-subtitle\"\n className=\"m-0 mt-1 text-sm leading-(--leading-normal) text-text-secondary\"\n >\n <Swappable value={subtitle} />\n </p>\n )}\n </div>\n {/* slot=\"close\" is the same dismiss wiring as a Cancel button: one handler, focus back to the trigger. */}\n <AriaButton\n slot=\"close\"\n aria-label={closeLabel}\n data-slot=\"alert-dialog-close\"\n className={alertDialogCloseVariants()}\n >\n <svg\n aria-hidden=\"true\"\n viewBox=\"0 0 16 16\"\n fill=\"none\"\n stroke=\"currentColor\"\n strokeWidth=\"1.75\"\n strokeLinecap=\"round\"\n >\n <path d=\"m4 4 8 8M12 4l-8 8\" />\n </svg>\n </AriaButton>\n </div>\n <AriaText\n slot=\"description\"\n elementType=\"p\"\n data-slot=\"dialog-description\"\n className=\"m-0 block px-8 py-6 text-sm leading-(--leading-normal) text-foreground\"\n >\n <Swappable value={description} />\n </AriaText>\n {isEmpty(body) ? null : (\n <div data-slot=\"alert-dialog-body\" className={alertDialogBodyVariants()}>\n {body}\n </div>\n )}\n {children == null ? null : (\n <div data-slot=\"alert-dialog-actions\" className={alertDialogActionsVariants()}>\n {children}\n </div>\n )}\n </AriaDialog>\n </AriaModal>\n </AriaModalOverlay>\n );\n});\n"],"mappings":";;;;;;;;;;AAyFA,MAAM,6BAA6B,IAAI,yDAAyD;;;;;AAMhG,MAAM,0BAA0B,IAC9B,sEACF;;;;;;;AAQA,MAAa,oBAAoB,WAC/B,SAAS,kBAAkB,OAAO,KAAK;CACrC,OAAO,oBAACA,UAAD;EAAa;EAAK,MAAK;EAAQ,SAAQ;EAAY,MAAK;EAAK,GAAI;CAAQ,CAAA;AAClF,CACF;;;;;;;;;;;;AAaA,MAAa,oBAAoB,WAC/B,SAAS,kBAAkB,EAAE,WAAW,GAAG,SAAS,KAAK;CACvD,OACE,oBAACA,UAAD;EACO;EACL,SAAQ;EACR,MAAK;EACL,WAAW,MACT,iOACA,SACF;EACA,GAAI;CACL,CAAA;AAEL,CACF;AAEA,MAAM,4BAA4B,IAChC,sFACF;;;;;;;AAQA,MAAM,gCAAgC,IAAI,kBAAkB,EAC1D,UAAU,EACR,UAAU;CACR,MAAM;CACN,OAAO;AACT,EACF,EACF,CAAC;AAED,MAAM,2BAA2B,IAC/B,qEACF;AAEA,MAAM,2BAA2B,IAAI;CACnC;CACA;CACA;CAEA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;AACF,CAAC;;AAGD,SAAS,UAAU,EAAE,SAA+B;CAClD,IAAI,OAAO,UAAU,UAAU,OAAO,oBAAA,YAAA,EAAA,UAAG,MAAQ,CAAA;CACjD,OACE,oBAAC,UAAD;EAAU,SAAS;EAAO,WAAU;EACjC,UAAA;CACO,CAAA;AAEd;;AAGA,SAAS,QAAQ,MAAiB;CAChC,OAAO,QAAQ,QAAQ,OAAO,SAAS,aAAa,SAAS;AAC/D;AAEA,MAAa,cAAc,WAA0C,SAAS,YAC5E,EACE,OACA,UACA,aACA,aACA,MACA,UACA,WACA,aAAa,SACb,kBACA,WAAW,QACX,GAAG,SAEL,KACA;CACA,OACE,oBAACC,cAAD;EACE,GAAI;EACJ,eAAe;EACf,aAAU;EACV,WAAW,MACT,sBAAsB,GACtB,aAAa,UAAU,yBACvB,gBACF;EAEA,UAAA,oBAACC,OAAD;GAAW,WAAU;GACnB,UAAA,qBAACC,QAAD;IACO;IACL,MAAK;IACL,aAAU;IACV,WAAW,MACT,oBAAoB,GAGpB,aACA,aAAa,UAAU,uBACvB,SACF;IAXF,UAAA;KAaG;KACD,qBAAC,OAAD;MAAK,aAAU;MAAsB,WAAW,0BAA0B;MAA1E,UAAA,CACE,qBAAC,OAAD;OAAK,WAAW,8BAA8B,EAAE,UAAU,YAAY,KAAK,CAAC;OAA5E,UAAA,CAEE,oBAACC,SAAD;QACE,MAAK;QACL,aAAU;QACV,WAAW,yBAAyB;QAEpC,UAAA,oBAAC,WAAD,EAAW,OAAO,MAAQ,CAAA;OACf,CAAA,GACZ,YAAY,OAAO,OAClB,oBAAC,KAAD;QACE,aAAU;QACV,WAAU;QAEV,UAAA,oBAAC,WAAD,EAAW,OAAO,SAAW,CAAA;OAC5B,CAAA,CAEF;MAEL,CAAA,GAAA,oBAACC,QAAD;OACE,MAAK;OACL,cAAY;OACZ,aAAU;OACV,WAAW,yBAAyB;OAEpC,UAAA,oBAAC,OAAD;QACE,eAAY;QACZ,SAAQ;QACR,MAAK;QACL,QAAO;QACP,aAAY;QACZ,eAAc;QAEd,UAAA,oBAAC,QAAD,EAAM,GAAE,qBAAsB,CAAA;OAC3B,CAAA;MACK,CAAA,CACT;;KACL,oBAACC,MAAD;MACE,MAAK;MACL,aAAY;MACZ,aAAU;MACV,WAAU;MAEV,UAAA,oBAAC,WAAD,EAAW,OAAO,YAAc,CAAA;KACxB,CAAA;KACT,QAAQ,IAAI,IAAI,OACf,oBAAC,OAAD;MAAK,aAAU;MAAoB,WAAW,wBAAwB;MACnE,UAAA;KACE,CAAA;KAEN,YAAY,OAAO,OAClB,oBAAC,OAAD;MAAK,aAAU;MAAuB,WAAW,2BAA2B;MACzE;KACE,CAAA;IAEG;;EACH,CAAA;CACK,CAAA;AAEtB,CAAC"}
@@ -36,7 +36,7 @@ import { ComponentPropsWithoutRef, ReactNode } from "react";
36
36
  * across every shadcn control.
37
37
  */
38
38
  export declare const badgeVariants: (props?: ({
39
- tone?: "neutral" | "success" | "warning" | "danger" | "info" | "primary" | null | undefined;
39
+ tone?: "primary" | "success" | "warning" | "danger" | "neutral" | "info" | null | undefined;
40
40
  appearance?: "outline" | "solid" | "soft" | null | undefined;
41
41
  size?: "sm" | "md" | "lg" | null | undefined;
42
42
  shape?: "rounded" | "pill" | null | undefined;
@@ -3,8 +3,8 @@ 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?: "link" | "outline" | "primary" | "ai" | "secondary" | "outlineNeutral" | "ghost" | "destructive" | null | undefined;
7
- size?: "icon" | "sm" | "md" | "lg" | null | undefined;
6
+ variant?: "link" | "primary" | "ai" | "secondary" | "outline" | "outlineNeutral" | "ghost" | "destructive" | null | undefined;
7
+ size?: "sm" | "md" | "lg" | "icon" | null | undefined;
8
8
  } & import("class-variance-authority/types").ClassProp) | undefined) => string;
9
9
  /**
10
10
  * Props.
@@ -50,7 +50,7 @@ export declare const calendarClearVariants: (props?: import("class-variance-auth
50
50
  * is a strikethrough.
51
51
  */
52
52
  export declare const calendarCellVariants: (props?: ({
53
- selection?: "single" | "range" | "band" | null | undefined;
53
+ selection?: "range" | "single" | "band" | null | undefined;
54
54
  } & import("class-variance-authority/types").ClassProp) | undefined) => string;
55
55
  interface CalendarSharedProps {
56
56
  /**
@@ -28,7 +28,10 @@ import { ComponentPropsWithoutRef } from "react";
28
28
  * source text, so a class assembled at runtime is invisible to it and would
29
29
  * simply never be emitted.
30
30
  */
31
- export declare const cardVariants: (props?: import("class-variance-authority/types").ClassProp | undefined) => string;
31
+ export declare const cardVariants: (props?: ({
32
+ flush?: boolean | null | undefined;
33
+ highlighted?: boolean | null | undefined;
34
+ } & import("class-variance-authority/types").ClassProp) | undefined) => string;
32
35
  /**
33
36
  * Padding is inline-only on the sections and block-only on the shell.
34
37
  *
@@ -60,6 +63,24 @@ export interface CardProps extends ComponentPropsWithoutRef<"div"> {
60
63
  * Leave it off for a card that is not a target: a static card should not move.
61
64
  */
62
65
  interactive?: boolean;
66
+ /**
67
+ * A featured card: a brand gradient edge in place of the hairline. Use it for the
68
+ * one card a view wants the eye to land on (a recommended action, the suggested
69
+ * plan), never for several at once. The edge is decoration, so say WHY the card
70
+ * is featured in its content too: colour alone does not reach a screen reader.
71
+ * @default false
72
+ */
73
+ highlighted?: boolean;
74
+ /**
75
+ * Edge-to-edge content: no shell padding and no gap between the parts, and the
76
+ * content is clipped to the rounded border. The outline, radius, surface and
77
+ * shadow stay. For a table, list or full-bleed chart that must meet the card's
78
+ * top and bottom. The header and footer keep a 24px block inset so their text
79
+ * clears the corner curve, and every section keeps its inline padding: a
80
+ * section's fill already reaches the side edges.
81
+ * @default false
82
+ */
83
+ flush?: boolean;
63
84
  }
64
85
  export declare const Card: import("react").ForwardRefExoticComponent<CardProps & import("react").RefAttributes<HTMLDivElement>>;
65
86
  /** Title and description, stacked. */
package/dist/card/card.js CHANGED
@@ -33,11 +33,59 @@ import { jsx } from "react/jsx-runtime";
33
33
  * simply never be emitted.
34
34
  */
35
35
  const cardVariants = cva([
36
- "flex flex-col gap-6",
37
- "rounded-panel border-(length:--border-width) border-border py-6",
36
+ "flex flex-col",
37
+ "rounded-panel border-(length:--border-width) border-border",
38
38
  "bg-card text-card-foreground shadow-surface",
39
39
  "box-border font-sans"
40
- ]);
40
+ ], {
41
+ variants: {
42
+ /**
43
+ * The block padding and the gap live here, not in the base, so `flush`
44
+ * REPLACES them rather than relying on a merge to drop them: the
45
+ * exported `cardVariants({ flush: true })` is correct on its own.
46
+ *
47
+ * Flush keeps the outline, radius, surface and shadow, and clips its
48
+ * content to the inside of the rounded border: without the clip a filled
49
+ * child (a table header, a chart) shows square corners over the curve and
50
+ * covers the left and right hairline. `overflow: clip`, not `hidden`, so
51
+ * the card does not become a scroll container and a sticky table header
52
+ * inside it still sticks to the page. The sections keep their inline
53
+ * padding, so text and a section's fill stay clear of the corner curve
54
+ * (the fill still meets the side edges: background paints the padding).
55
+ *
56
+ * The header and footer hold text, so under flush they keep the 24px
57
+ * block inset the shell gave up. Without it the title and the footer line
58
+ * sat against the top and bottom borders, inside the corner curve, and
59
+ * the clip cut their first glyphs. The inset is a custom property both
60
+ * sections read, set per variant (never left undefined), so a nested
61
+ * default card resets it, and a section's own `py-*` still wins by merge.
62
+ * CardContent stays block-flush: a table or chart meets the edges.
63
+ */
64
+ flush: {
65
+ false: "gap-6 py-6 [--card-section-py:--spacing(0)]",
66
+ true: "gap-0 py-0 overflow-clip [--card-section-py:--spacing(6)]"
67
+ },
68
+ /**
69
+ * A featured card: the hairline is replaced by a brand gradient edge,
70
+ * painted by the `card-highlighted` utility in styles.css. The border
71
+ * stays (transparent), so a highlighted card has exactly the box of a
72
+ * plain one and a list of mixed cards does not shift.
73
+ */
74
+ highlighted: {
75
+ false: "",
76
+ true: "card-highlighted border-transparent"
77
+ }
78
+ },
79
+ compoundVariants: [{
80
+ flush: true,
81
+ highlighted: true,
82
+ class: "card-highlighted-flush"
83
+ }],
84
+ defaultVariants: {
85
+ flush: false,
86
+ highlighted: false
87
+ }
88
+ });
41
89
  /**
42
90
  * Padding is inline-only on the sections and block-only on the shell.
43
91
  *
@@ -45,18 +93,20 @@ const cardVariants = cva([
45
93
  * consumer run a divider or a full-bleed image edge to edge, which a single
46
94
  * `p-6` on the shell would make impossible.
47
95
  */
48
- const cardHeaderVariants = cva("flex flex-col gap-1.5 px-6");
96
+ const cardHeaderVariants = cva("flex flex-col gap-1.5 px-6 py-(--card-section-py)");
49
97
  const cardTitleVariants = cva(["text-base leading-(--leading-none) font-semibold", "m-0"]);
50
98
  const cardDescriptionVariants = cva(["text-sm leading-(--leading-normal) text-muted-foreground"]);
51
99
  const cardContentVariants = cva("px-6");
52
- const cardFooterVariants = cva("flex items-center px-6");
53
- const Card = forwardRef(function Card({ className, interactive = false, onPointerEnter, onPointerLeave, ...props }, ref) {
100
+ const cardFooterVariants = cva("flex items-center px-6 py-(--card-section-py)");
101
+ const Card = forwardRef(function Card({ className, interactive = false, highlighted = false, flush = false, onPointerEnter, onPointerLeave, ...props }, ref) {
54
102
  const [pointed, setPointed] = useState(false);
55
103
  return /* @__PURE__ */ jsx("div", {
56
104
  ref,
57
105
  ...props,
58
106
  "data-slot": "card",
59
107
  "data-interactive": interactive || void 0,
108
+ "data-highlighted": highlighted || void 0,
109
+ "data-flush": flush || void 0,
60
110
  "data-pointed": interactive && pointed || void 0,
61
111
  onPointerEnter: (event) => {
62
112
  onPointerEnter?.(event);
@@ -66,7 +116,10 @@ const Card = forwardRef(function Card({ className, interactive = false, onPointe
66
116
  onPointerLeave?.(event);
67
117
  setPointed(false);
68
118
  },
69
- className: cnAll(cardVariants(), interactive && "card-lift", className)
119
+ className: cnAll(cardVariants({
120
+ flush,
121
+ highlighted
122
+ }), interactive && "card-lift", className)
70
123
  });
71
124
  });
72
125
  const CardHeader = forwardRef(function CardHeader({ className, ...props }, ref) {
@@ -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 \"flex flex-col gap-6\",\n \"rounded-panel border-(length:--border-width) border-border py-6\",\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/**\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\");\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\");\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\nexport const Card = forwardRef<HTMLDivElement, CardProps>(function Card(\n { className, interactive = false, onPointerEnter, onPointerLeave, ...props },\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-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(), 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,IAAI;CAC9B;CACA;CACA;CASA;AACF,CAAC;;;;;;;;AASD,MAAa,qBAAqB,IAAI,4BAA4B;AAElE,MAAa,oBAAoB,IAAI,CAGnC,oDAGA,KACF,CAAC;AAED,MAAa,0BAA0B,IAAI,CACzC,0DACF,CAAC;AAED,MAAa,sBAAsB,IAAI,MAAM;AAE7C,MAAa,qBAAqB,IAAI,wBAAwB;AAuB9D,MAAa,OAAO,WAAsC,SAAS,KACjE,EAAE,WAAW,cAAc,OAAO,gBAAgB,gBAAgB,GAAG,SACrE,KACA;CAEA,MAAM,CAAC,SAAS,cAAc,SAAS,KAAK;CAC5C,OACE,oBAAC,OAAD;EACO;EACL,GAAI;EACJ,aAAU;EACV,oBAAkB,eAAe,KAAA;EACjC,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,GAAG,eAAe,aAAa,SAAS;CACvE,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 \"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"}
@@ -16,6 +16,12 @@ import { ReactNode } from "react";
16
16
  * rotation pauses on hover, keyboard focus and touch, while the carousel is off
17
17
  * screen or the tab is hidden, and is unavailable under reduced motion (where
18
18
  * scrolling also jumps instead of gliding).
19
+ *
20
+ * `loop` wraps the ends: next on the last slide goes to the first, previous on
21
+ * the first to the last, from the controls and the arrow keys alike (a swipe
22
+ * or drag still stops at the end of the track). `peek={false}` shows one slide
23
+ * at a time across the full width, with no neighbours in view, for a viewer
24
+ * such as a single creative asset.
19
25
  */
20
26
  export interface CarouselProps {
21
27
  /** Names the carousel for assistive technology, such as "From the Harbour suites". */
@@ -35,6 +41,21 @@ export interface CarouselProps {
35
41
  interval?: number;
36
42
  /** Starts rotating on mount. Off by default, so motion begins with the viewer. @default false */
37
43
  autoplay?: boolean;
44
+ /**
45
+ * Wraps around at the ends: next on the last slide goes to the first and
46
+ * previous on the first goes to the last, from the controls and the arrow
47
+ * keys. The controls are then never disabled. Home and End still go to the
48
+ * first and last slide; a swipe or drag stops at the end of the track.
49
+ * @default false
50
+ */
51
+ loop?: boolean;
52
+ /**
53
+ * Neighbouring slides peek in at the sides. `false` shows one slide at a
54
+ * time across the full width (`slideSize` is ignored), with the controls
55
+ * spanning it too.
56
+ * @default true
57
+ */
58
+ peek?: boolean;
38
59
  /** Names of the controls, for translation. */
39
60
  labels?: {
40
61
  /** @default "Previous slide" */
@@ -9,6 +9,15 @@ import { jsx, jsxs } from "react/jsx-runtime";
9
9
  const SLOP = 6;
10
10
  /** Release speed in px/s that always moves at least one slide. */
11
11
  const FLICK = 360;
12
+ /**
13
+ * How long a glide may take before the carousel stops waiting for it: one slide,
14
+ * plus more per extra slide (a `loop` wrap crosses them all), capped.
15
+ */
16
+ const BACKSTOP = {
17
+ base: 1e3,
18
+ perSlide: 250,
19
+ max: 3e3
20
+ };
12
21
  /** Where a flick would come to rest: velocity (px/s) projected with a paging deceleration rate. */
13
22
  const project = (velocity, rate = .995) => velocity / 1e3 * rate / (1 - rate);
14
23
  const clamp = (value, min, max) => Math.min(max, Math.max(min, value));
@@ -53,7 +62,7 @@ const Chevron = ({ back }) => /* @__PURE__ */ jsx("svg", {
53
62
  "aria-hidden": "true",
54
63
  children: /* @__PURE__ */ jsx("path", { d: back ? "M10 3.5 5.5 8l4.5 4.5" : "M6 3.5 10.5 8 6 12.5" })
55
64
  });
56
- const Carousel = forwardRef(function Carousel({ label, children, index: controlledIndex, defaultIndex = 0, onIndexChange, slideSize, slideLabel = defaultSlideLabel, interval, autoplay = false, labels, className }, ref) {
65
+ const Carousel = forwardRef(function Carousel({ label, children, index: controlledIndex, defaultIndex = 0, onIndexChange, slideSize, slideLabel = defaultSlideLabel, interval, autoplay = false, loop = false, peek = true, labels, className }, ref) {
57
66
  const slides = Children.toArray(children);
58
67
  const count = slides.length;
59
68
  const last = Math.max(0, count - 1);
@@ -121,7 +130,9 @@ const Carousel = forwardRef(function Carousel({ label, children, index: controll
121
130
  }
122
131
  travelling.current = i;
123
132
  clearTimeout(backstop.current);
124
- backstop.current = setTimeout(() => travelling.current = null, 1e3);
133
+ const distance = Math.abs(box.scrollLeft - left) / (step() || 1);
134
+ const wait = Math.min(BACKSTOP.max, BACKSTOP.base + BACKSTOP.perSlide * Math.max(0, distance - 1));
135
+ backstop.current = setTimeout(() => travelling.current = null, wait);
125
136
  box.scrollTo({
126
137
  left,
127
138
  behavior: "smooth"
@@ -158,6 +169,17 @@ const Carousel = forwardRef(function Carousel({ label, children, index: controll
158
169
  }
159
170
  }, [index]);
160
171
  useEffect(() => () => clearTimeout(backstop.current), []);
172
+ useEffect(() => {
173
+ const box = viewport.current;
174
+ if (!box) return;
175
+ const onScrollEnd = () => {
176
+ if (travelling.current === null) return;
177
+ travelling.current = null;
178
+ clearTimeout(backstop.current);
179
+ };
180
+ box.addEventListener("scrollend", onScrollEnd);
181
+ return () => box.removeEventListener("scrollend", onScrollEnd);
182
+ }, []);
161
183
  function onScroll() {
162
184
  const position = paint();
163
185
  const box = viewport.current;
@@ -170,6 +192,8 @@ const Carousel = forwardRef(function Carousel({ label, children, index: controll
170
192
  if (next !== target.current) commit(next, true);
171
193
  }
172
194
  const advance = () => select(index >= last ? 0 : index + 1, false);
195
+ /** The slide `by` steps from `from`: clamped at the ends, or around them with `loop`. */
196
+ const stepFrom = (from, by) => loop ? (from + by + count) % count : clamp(from + by, 0, last);
173
197
  useEffect(() => {
174
198
  const node = root.current;
175
199
  if (!interval || !node) return;
@@ -251,7 +275,7 @@ const Carousel = forwardRef(function Carousel({ label, children, index: controll
251
275
  const node = event.target;
252
276
  if (node.closest("input, textarea, select, [contenteditable]")) return;
253
277
  const from = target.current;
254
- const to = event.key === "ArrowLeft" ? from - 1 : event.key === "ArrowRight" ? from + 1 : event.key === "Home" ? 0 : event.key === "End" ? last : null;
278
+ const to = event.key === "ArrowLeft" ? stepFrom(from, -1) : event.key === "ArrowRight" ? stepFrom(from, 1) : event.key === "Home" ? 0 : event.key === "End" ? last : null;
255
279
  if (to === null) return;
256
280
  event.preventDefault();
257
281
  const next = clamp(to, 0, last);
@@ -272,8 +296,10 @@ const Carousel = forwardRef(function Carousel({ label, children, index: controll
272
296
  "data-slot": "carousel",
273
297
  "aria-roledescription": "carousel",
274
298
  "aria-label": label,
299
+ "data-loop": loop || void 0,
300
+ "data-peek": peek ? void 0 : "false",
275
301
  className: cn("carousel", className),
276
- style: slideSize ? { "--carousel-slide-size": slideSize } : void 0,
302
+ style: slideSize && peek ? { "--carousel-slide-size": slideSize } : void 0,
277
303
  onKeyDown,
278
304
  onFocus: (event) => setFocusPaused(event.target !== play.current && event.target.matches(":focus-visible")),
279
305
  onBlur: (event) => {
@@ -359,16 +385,16 @@ const Carousel = forwardRef(function Carousel({ label, children, index: controll
359
385
  children: [/* @__PURE__ */ jsx("button", {
360
386
  type: "button",
361
387
  "aria-label": text.previous,
362
- "aria-disabled": index === 0,
388
+ "aria-disabled": !loop && index === 0,
363
389
  className: "carousel-control",
364
- onClick: () => target.current > 0 && select(target.current - 1),
390
+ onClick: () => (loop || target.current > 0) && select(stepFrom(target.current, -1)),
365
391
  children: /* @__PURE__ */ jsx(Chevron, { back: true })
366
392
  }), /* @__PURE__ */ jsx("button", {
367
393
  type: "button",
368
394
  "aria-label": text.next,
369
- "aria-disabled": index === last,
395
+ "aria-disabled": !loop && index === last,
370
396
  className: "carousel-control",
371
- onClick: () => target.current < last && select(target.current + 1),
397
+ onClick: () => (loop || target.current < last) && select(stepFrom(target.current, 1)),
372
398
  children: /* @__PURE__ */ jsx(Chevron, {})
373
399
  })]
374
400
  })