@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
@@ -1 +1 @@
1
- {"version":3,"file":"select.js","names":["AriaSelectValue","Select","AriaSelect","AriaLabel","AriaButton","AriaFieldError","AriaPopover","AriaListBox","AriaListBoxItem"],"sources":["../../src/select/select.tsx"],"sourcesContent":["\"use client\";\n\nimport { cva } from \"class-variance-authority\";\nimport { forwardRef, useContext, useState, type ReactNode } from \"react\";\nimport {\n Button as AriaButton,\n FieldError as AriaFieldError,\n Label as AriaLabel,\n ListBox as AriaListBox,\n ListBoxItem as AriaListBoxItem,\n Popover as AriaPopover,\n Select as AriaSelect,\n SelectStateContext,\n SelectValue as AriaSelectValue,\n type ListBoxItemProps as AriaListBoxItemProps,\n type SelectProps as AriaSelectProps,\n} from \"react-aria-components\";\n\nimport { labelVariants } from \"../label/label.js\";\nimport { cn, cnAll } from \"../lib/cn.js\";\nimport { fieldSurfaceVariants } from \"../lib/field-surface.js\";\nimport { useIsomorphicLayoutEffect } from \"../lib/motion.js\";\nimport { RollingValue } from \"../lib/rolling-value.js\";\n\n/**\n * Select, ported from the legacy shadcn/Radix component.\n *\n * FOUR PARTS COLLAPSE INTO ONE. The census reads `SelectItem` 35 and\n * `Select`, `SelectTrigger`, `SelectValue`, `SelectContent` 15 each --\n * those four always appear together in the same shape, exactly like\n * Dialog's header trio, so they are one component and a prop.\n * `SelectGroup`, `SelectLabel`, `SelectSeparator`, `SelectScrollUpButton`\n * and `SelectScrollDownButton` have ZERO call sites and are not ported.\n *\n * `label` IS REQUIRED. The legacy exports a `SelectLabel` that nobody\n * renders and passes no `aria-label` anywhere, so all 15 selects are\n * unnamed comboboxes -- the same finding as Progress, Slider and\n * ScrollArea. Pass `labelHidden` when the surrounding UI already says it.\n *\n * The trigger reuses `fieldSurfaceVariants`, shared with Input and\n * Textarea. A select and a text field sit in the same form row and must\n * be the same box; the legacy styled them separately and drifted --\n * `h-10` here against Input's `h-9`, for no stated reason.\n *\n * The popover is `card`, not a `popover` role: `--popover` is\n * byte-identical to `--card` in the legacy theme. Item hover is `muted`,\n * not `accent`, for the same reason.\n */\n/*\n * `group` is load-bearing, not decoration. React Aria puts `data-invalid` on\n * THIS element, never on the trigger -- so the trigger's own\n * `data-[invalid]:` rule, inherited from the shared field surface, can never\n * match and `isInvalid` painted nothing at all. The trigger reads the state\n * from here instead.\n */\nexport const selectVariants = cva([\"group flex w-full flex-col gap-1.5 box-border font-sans\"]);\n\nexport const selectTriggerVariants = cva(\n [\n // See selectVariants: the invalid state lives on the ROOT, so this reads it\n // from the group rather than from itself.\n \"group-data-[invalid]:border-destructive-strong\",\n fieldSurfaceVariants(),\n // While the menu is open the trigger is pressed and focus has moved into the\n // list, so the border keeps the darkness it had instead of falling back.\n \"[&:where([data-pressed])]:border-border-strong\",\n // The fill follows the border: hovered, pressed and open all read as one\n // state. Ordinary variants, not :where(): the base background is a plain\n // class, so a zero-specificity rule would lose to it. Nothing else paints\n // the background, and a disabled trigger is never hovered or pressed.\n \"data-[hovered]:not-data-[disabled]:bg-muted\",\n \"data-[pressed]:bg-muted\",\n \"group/trigger\",\n \"flex items-center justify-between gap-2 text-start\",\n \"leading-(--leading-normal) cursor-pointer\",\n \"[&_svg]:pointer-events-none [&_svg]:shrink-0\",\n \"[&_svg:not([class*='size-'])]:size-(--icon-sm)\",\n ],\n {\n variants: {\n /**\n * The same rungs and the same tokens as Input and Button. 14 of the\n * product's 32 `SelectTrigger` call sites override the height by hand,\n * and a Select sits in the same form row as a field -- giving Input the\n * axis without giving it here would be exactly the drift\n * `fieldSurfaceVariants` exists to prevent.\n *\n * Type matches Input's at every rung: 14px on `sm` (16px on a touch\n * screen), 16px on `md` and `lg`. A row whose two controls disagree\n * about type size reads as a mistake. The trigger is a button, so iOS\n * would not zoom on it; the coarse-pointer rule is there to keep it\n * equal to the field beside it.\n */\n size: {\n sm: \"h-(--density-control-h-sm) px-(--density-control-pad-sm) text-sm pointer-coarse:text-base\",\n md: \"h-(--density-control-h-md) px-(--density-control-pad-md) text-base\",\n lg: \"h-(--density-control-h-lg) px-(--density-control-pad-lg) text-base\",\n },\n },\n defaultVariants: { size: \"md\" },\n },\n);\n\nexport const selectPopoverVariants = cva(\n [\n // --trigger-width is set inline by React Aria, so the list matches the\n // control it came from rather than guessing a width.\n \"w-(--trigger-width) min-w-32 overflow-y-auto\",\n // Grows from the trigger edge: offset toward it, a little small and clipped to\n // the trigger's height, then opening to full height; 120ms out. React Aria\n // marks the first frame data-entering and the leaving one data-exiting and\n // keeps the list mounted until the animation ends.\n \"[--menu-x:0px] [--menu-y:-6px] origin-top\",\n // The panel also GROWS from the trigger's height: --menu-from is where the clip\n // starts, --menu-flip 1 grows it from the bottom edge when it opens upward.\n \"[--menu-from:var(--density-control-h-md)] [--menu-flip:0]\",\n \"data-[placement=top]:[--menu-y:6px] data-[placement=top]:origin-bottom data-[placement=top]:[--menu-flip:1]\",\n \"data-[placement=left]:[--menu-x:6px] data-[placement=left]:[--menu-y:0px] data-[placement=left]:origin-right\",\n \"data-[placement=right]:[--menu-x:-6px] data-[placement=right]:[--menu-y:0px] data-[placement=right]:origin-left\",\n \"motion-reduce:data-[entering]:animate-select-fade-in\",\n \"motion-safe:data-[exiting]:animate-select-popover-out motion-reduce:data-[exiting]:animate-select-fade-out\",\n \"data-[exiting]:pointer-events-none\",\n \"rounded-panel border-(length:--border-width) border-border p-1\",\n \"bg-card text-card-foreground shadow-floating\",\n \"box-border font-sans text-sm leading-(--leading-normal)\",\n ],\n {\n variants: {\n /**\n * `grow` opens the panel from the trigger's height on the smooth spring (about 700ms). `fade` is the quick 240ms\n * settle for a list whose first row is a search field: a clipped panel would hide it while it grows.\n */\n reveal: {\n grow: \"motion-safe:data-[entering]:animate-select-popover-reveal\",\n fade: \"motion-safe:data-[entering]:animate-select-popover-in\",\n },\n },\n defaultVariants: { reveal: \"grow\" },\n },\n);\n\nexport const selectItemVariants = cva([\n \"relative flex cursor-pointer select-none items-center gap-2\",\n // The same 18px as a Menu row: the hover fill is a soft pill inside the panel.\n \"rounded-control py-1.5 pe-8 ps-2\",\n \"box-border text-sm leading-(--leading-normal) text-card-foreground\",\n // data-focused, not :hover. React Aria drives one highlight from both\n // pointer and keyboard, so the row under the mouse and the row the\n // arrows reached are the same row.\n \"outline-none data-[focused]:bg-muted\",\n \"motion-safe:transition-colors motion-safe:duration-(--duration-instant) motion-safe:ease-settle\",\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 \"data-[disabled]:cursor-not-allowed data-[disabled]:opacity-(--opacity-disabled)\",\n]);\n\n/**\n * A single-choice picker.\n *\n * @example\n * <Select label=\"Country\" placeholder=\"Pick one\"\n * selectedKey={code} onSelectionChange={setCode}>\n * <SelectItem id=\"gb\">United Kingdom</SelectItem>\n * <SelectItem id=\"us\">United States</SelectItem>\n * </Select>\n */\nexport interface SelectProps extends Omit<\n AriaSelectProps<object>,\n \"className\" | \"style\" | \"children\" | \"items\"\n> {\n /** `SelectItem` children. */\n children?: ReactNode;\n /**\n * What is being chosen, and the control's accessible name.\n *\n * **Required.** All 15 legacy selects are unnamed: the legacy exports a\n * `SelectLabel` that nobody renders and passes no `aria-label`.\n */\n label: ReactNode;\n /** Render the label for screen readers only. */\n labelHidden?: boolean;\n /** Shown while nothing is selected. */\n placeholder?: string;\n /**\n * Why the selection is wrong. Rendered under the control, and it is what\n * makes `isInvalid` reach a screen reader.\n *\n * React Aria marks the ROOT `data-invalid` and leaves the button untouched,\n * and an `aria-invalid` handed to that button is dropped by\n * `filterDOMProps`, whose allowlist stops at the labelling attributes -- the\n * same silent loss as `aria-current` on a pagination button. So without a\n * message, an invalid select is invalid only in colour, which is WCAG 1.4.1\n * on its own. React Aria wires this into `aria-describedby` for us.\n */\n errorMessage?: ReactNode;\n /**\n * Control scale of the trigger. Height and inline padding come from the\n * same `--density-control-*` tokens as Input and Button, so a Select, a\n * field and a button on one rung are the same height.\n *\n * Type does not scale with it, matching Input.\n *\n * @default \"md\"\n */\n size?: \"sm\" | \"md\" | \"lg\";\n /** Extra classes for the wrapper. Merged last. */\n className?: string;\n}\n\n/**\n * The shown value. React Aria's own `SelectValue` stays, visually hidden, as the\n * text a screen reader reads; the copy the eye sees rolls when the selection\n * changes: a later option rises from below, an earlier one drops from above. The\n * direction is read from the option's place in the list. A pre-selected value\n * does not animate on first mount.\n */\nfunction ValueRoll({ placeholder }: { placeholder?: string }) {\n const state = useContext(SelectStateContext);\n const key = state?.selectedKey ?? null;\n let index = -1;\n if (state && key != null) {\n let n = 0;\n for (const item of state.collection) {\n if (item.type !== \"item\") continue;\n if (item.key === key) index = n;\n n += 1;\n }\n }\n const [previous, setPrevious] = useState(index);\n const [dir, setDir] = useState(1);\n if (previous !== index) {\n setPrevious(index);\n setDir(index > previous ? 1 : -1);\n }\n const text = state?.selectedItem?.textValue ?? \"\";\n // React Aria resolves a default selection after the first render, so the key\n // starts as null and then settles. Until the collection is built nothing is\n // shown, and the swap remounts once it is, so that settling is not a change.\n const built = (state?.collection.size ?? 0) > 0 || state?.selectedKey != null;\n const [mounted, setMounted] = useState(false);\n useIsomorphicLayoutEffect(() => setMounted(true), []);\n const settled = built || mounted;\n const swapKey = key == null ? \"placeholder\" : `value-${String(key)}`;\n return (\n <RollingValue\n srValue={\n <AriaSelectValue data-slot=\"select-value\" className=\"sr-only\">\n {({ isPlaceholder, selectedText }) => (isPlaceholder ? placeholder : selectedText)}\n </AriaSelectValue>\n }\n swapKey={swapKey}\n dir={dir}\n instanceKey={settled ? \"live\" : \"boot\"}\n >\n {key == null ? (\n <span data-slot=\"select-placeholder\" className=\"text-muted-foreground\">\n {placeholder}\n </span>\n ) : (\n text\n )}\n </RollingValue>\n );\n}\n\nexport const Select = forwardRef<HTMLButtonElement, SelectProps>(function Select(\n { children, label, labelHidden, placeholder, errorMessage, size = \"md\", className, ...props },\n ref,\n) {\n return (\n <AriaSelect {...props} data-slot=\"select\" className={cn(selectVariants(), className)}>\n {/*\n The Label's own classes, so a Select's label and the Label over an\n Input in the same form are one style (same size, weight, colour and\n line height). The select-label slot stays for consumers that target it.\n */}\n <AriaLabel\n data-slot=\"select-label\"\n className={cnAll(labelVariants(), labelHidden && \"sr-only\")}\n >\n {label}\n </AriaLabel>\n\n {/*\n The ref goes on the TRIGGER, not the AriaSelect root above. The root\n is a field-level wrapper div; the trigger is the always-visible\n interactive surface a caller actually wants to focus or click,\n the same reasoning that puts Input's ref on the real control.\n */}\n <AriaButton ref={ref} data-slot=\"select-trigger\" className={selectTriggerVariants({ size })}>\n <ValueRoll placeholder={placeholder} />\n {/*\n aria-hidden: the chevron is decoration. The button already\n announces itself as a combobox, and a second name source only\n makes it announce twice.\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 strokeLinejoin=\"round\"\n className=\"text-muted-foreground motion-safe:transition-[rotate] motion-safe:duration-(--duration-gentle) motion-safe:ease-gentle group-aria-expanded/trigger:rotate-180\"\n >\n <path d=\"m4 6 4 4 4-4\" />\n </svg>\n </AriaButton>\n\n {errorMessage == null ? null : (\n // Not conditional on isInvalid: React Aria renders FieldError only\n // while the field IS invalid, so gating it here as well would just be\n // two sources of truth for one state.\n <AriaFieldError\n data-slot=\"select-error\"\n className=\"text-sm leading-(--leading-normal) text-destructive-strong\"\n >\n {errorMessage}\n </AriaFieldError>\n )}\n\n {/* No border colour on the list: it is lifted by its shadow alone. */}\n <AriaPopover\n data-slot=\"select-popover\"\n className={cn(selectPopoverVariants(), \"border-0\")}\n >\n <AriaListBox data-slot=\"select-list\" className=\"outline-none\">\n {children}\n </AriaListBox>\n </AriaPopover>\n </AriaSelect>\n );\n});\n\n/**\n * One option. Its `id` is the value.\n *\n * @example\n * <SelectItem id=\"gb\">United Kingdom</SelectItem>\n */\nexport interface SelectItemProps extends Omit<\n AriaListBoxItemProps,\n \"className\" | \"style\" | \"children\"\n> {\n children?: ReactNode;\n /**\n * The option's text, for the trigger and for typeahead.\n *\n * Derived from `children` when they are a string. Pass it explicitly\n * when the option renders anything richer than text.\n */\n textValue?: string;\n /** Extra classes. Merged last, so they win conflicts with ours. */\n className?: string;\n}\n\nexport const SelectItem = forwardRef<HTMLDivElement, SelectItemProps>(function SelectItem(\n { className, children, textValue, ...props },\n ref,\n) {\n return (\n <AriaListBoxItem\n ref={ref}\n {...props}\n /*\n textValue, derived from string children.\n React Aria infers an item's text from its children -- but ONLY\n when they are a string. This item uses FUNCTION children so the\n tick can be rendered in JS, which defeats that inference: the\n trigger showed an empty value after selecting, and typeahead had\n nothing to match on. Neither is visible in the type system.\n */\n textValue={textValue ?? (typeof children === \"string\" ? children : undefined)}\n data-slot=\"select-item\"\n className={cn(selectItemVariants(), className)}\n >\n {({ isSelected }) => (\n <>\n {/*\n The tick is rendered in JS rather than toggled with CSS, so\n the precedence is explicit and a test can read it from the\n DOM. ps-8 above reserves the gutter, so selecting an option\n does not shift its label.\n */}\n {isSelected ? (\n <svg\n data-slot=\"select-item-check\"\n aria-hidden=\"true\"\n viewBox=\"0 0 16 16\"\n fill=\"none\"\n stroke=\"currentColor\"\n strokeWidth=\"2\"\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n className=\"absolute end-2 size-3.5 motion-safe:animate-select-check-in\"\n >\n <path d=\"m3.5 8.5 3 3 6-6\" />\n </svg>\n ) : null}\n {children}\n </>\n )}\n </AriaListBoxItem>\n );\n});\n"],"mappings":"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"}
1
+ {"version":3,"file":"select.js","names":["AriaSelectValue","Select","AriaSelect","AriaLabel","AriaButton","AriaFieldError","AriaPopover","AriaListBox","AriaListBoxItem"],"sources":["../../src/select/select.tsx"],"sourcesContent":["\"use client\";\n\nimport { cva } from \"class-variance-authority\";\nimport { forwardRef, useContext, useState, type ReactNode } from \"react\";\nimport {\n Button as AriaButton,\n FieldError as AriaFieldError,\n Label as AriaLabel,\n ListBox as AriaListBox,\n ListBoxItem as AriaListBoxItem,\n Popover as AriaPopover,\n Select as AriaSelect,\n SelectStateContext,\n SelectValue as AriaSelectValue,\n type ListBoxItemProps as AriaListBoxItemProps,\n type SelectProps as AriaSelectProps,\n} from \"react-aria-components\";\n\nimport { labelVariants } from \"../label/label.js\";\nimport { cn, cnAll } from \"../lib/cn.js\";\nimport { fieldBoxSize, fieldSurfaceVariants, fieldTextSize } from \"../lib/field-surface.js\";\nimport { useIsomorphicLayoutEffect } from \"../lib/motion.js\";\nimport { RollingValue } from \"../lib/rolling-value.js\";\n\n/**\n * Select, ported from the legacy shadcn/Radix component.\n *\n * FOUR PARTS COLLAPSE INTO ONE. The census reads `SelectItem` 35 and\n * `Select`, `SelectTrigger`, `SelectValue`, `SelectContent` 15 each --\n * those four always appear together in the same shape, exactly like\n * Dialog's header trio, so they are one component and a prop.\n * `SelectGroup`, `SelectLabel`, `SelectSeparator`, `SelectScrollUpButton`\n * and `SelectScrollDownButton` have ZERO call sites and are not ported.\n *\n * `label` IS REQUIRED. The legacy exports a `SelectLabel` that nobody\n * renders and passes no `aria-label` anywhere, so all 15 selects are\n * unnamed comboboxes -- the same finding as Progress, Slider and\n * ScrollArea. Pass `labelHidden` when the surrounding UI already says it.\n *\n * The trigger reuses `fieldSurfaceVariants`, shared with Input and\n * Textarea. A select and a text field sit in the same form row and must\n * be the same box; the legacy styled them separately and drifted --\n * `h-10` here against Input's `h-9`, for no stated reason.\n *\n * The popover is `card`, not a `popover` role: `--popover` is\n * byte-identical to `--card` in the legacy theme. Item hover is `muted`,\n * not `accent`, for the same reason.\n */\n/*\n * `group` is load-bearing, not decoration. React Aria puts `data-invalid` on\n * THIS element, never on the trigger -- so the trigger's own\n * `data-[invalid]:` rule, inherited from the shared field surface, can never\n * match and `isInvalid` painted nothing at all. The trigger reads the state\n * from here instead.\n */\nexport const selectVariants = cva([\"group flex w-full flex-col gap-1.5 box-border font-sans\"]);\n\n/** The error line under a Select-based control (Select, TimePicker): one class, so they cannot drift. */\nexport const selectErrorClass = \"text-sm leading-(--leading-normal) text-destructive-strong\";\n\nexport const selectTriggerVariants = cva(\n [\n // See selectVariants: the invalid state lives on the ROOT, so this reads it\n // from the group rather than from itself.\n \"group-data-[invalid]:border-destructive-strong\",\n fieldSurfaceVariants(),\n // While the menu is open the trigger is pressed and focus has moved into the\n // list, so the border keeps the darkness it had instead of falling back.\n \"[&:where([data-pressed])]:border-border-strong\",\n // The fill follows the border: hovered, pressed and open all read as one\n // state. Ordinary variants, not :where(): the base background is a plain\n // class, so a zero-specificity rule would lose to it. Nothing else paints\n // the background, and a disabled trigger is never hovered or pressed.\n \"data-[hovered]:not-data-[disabled]:bg-muted\",\n \"data-[pressed]:bg-muted\",\n \"group/trigger\",\n \"flex items-center justify-between gap-2 text-start\",\n \"leading-(--leading-normal) cursor-pointer\",\n \"[&_svg]:pointer-events-none [&_svg]:shrink-0\",\n \"[&_svg:not([class*='size-'])]:size-(--icon-sm)\",\n ],\n {\n variants: {\n /**\n * The same rungs and the same tokens as Input and Button. 14 of the\n * product's 32 `SelectTrigger` call sites override the height by hand,\n * and a Select sits in the same form row as a field -- giving Input the\n * axis without giving it here would be exactly the drift\n * `fieldSurfaceVariants` exists to prevent.\n *\n * Type matches Input's at every rung: 14px on `sm` (16px on a touch\n * screen), 16px on `md` and `lg`. A row whose two controls disagree\n * about type size reads as a mistake. The trigger is a button, so iOS\n * would not zoom on it; the coarse-pointer rule is there to keep it\n * equal to the field beside it.\n */\n size: {\n sm: [fieldBoxSize.sm, fieldTextSize.sm],\n md: [fieldBoxSize.md, fieldTextSize.md],\n lg: [fieldBoxSize.lg, fieldTextSize.lg],\n },\n },\n defaultVariants: { size: \"md\" },\n },\n);\n\nexport const selectPopoverVariants = cva(\n [\n // --trigger-width is set inline by React Aria, so the list matches the\n // control it came from rather than guessing a width.\n \"w-(--trigger-width) min-w-32 overflow-y-auto\",\n // Grows from the trigger edge: offset toward it, a little small and clipped to\n // the trigger's height, then opening to full height; 120ms out. React Aria\n // marks the first frame data-entering and the leaving one data-exiting and\n // keeps the list mounted until the animation ends.\n \"[--menu-x:0px] [--menu-y:-6px] origin-top\",\n // The panel also GROWS from the trigger's height: --menu-from is where the clip\n // starts, --menu-flip 1 grows it from the bottom edge when it opens upward.\n \"[--menu-from:var(--density-control-h-md)] [--menu-flip:0]\",\n \"data-[placement=top]:[--menu-y:6px] data-[placement=top]:origin-bottom data-[placement=top]:[--menu-flip:1]\",\n \"data-[placement=left]:[--menu-x:6px] data-[placement=left]:[--menu-y:0px] data-[placement=left]:origin-right\",\n \"data-[placement=right]:[--menu-x:-6px] data-[placement=right]:[--menu-y:0px] data-[placement=right]:origin-left\",\n \"motion-reduce:data-[entering]:animate-select-fade-in\",\n \"motion-safe:data-[exiting]:animate-select-popover-out motion-reduce:data-[exiting]:animate-select-fade-out\",\n \"data-[exiting]:pointer-events-none\",\n \"rounded-panel border-(length:--border-width) border-border p-1\",\n \"bg-card text-card-foreground shadow-floating\",\n \"box-border font-sans text-sm leading-(--leading-normal)\",\n ],\n {\n variants: {\n /**\n * `grow` opens the panel from the trigger's height on the smooth spring (about 700ms). `fade` is the quick 240ms\n * settle for a list whose first row is a search field: a clipped panel would hide it while it grows.\n */\n reveal: {\n grow: \"motion-safe:data-[entering]:animate-select-popover-reveal\",\n fade: \"motion-safe:data-[entering]:animate-select-popover-in\",\n },\n },\n defaultVariants: { reveal: \"grow\" },\n },\n);\n\nexport const selectItemVariants = cva([\n \"relative flex cursor-pointer select-none items-center gap-2\",\n // The same 18px as a Menu row: the hover fill is a soft pill inside the panel.\n \"rounded-control py-1.5 pe-8 ps-2\",\n \"box-border text-sm leading-(--leading-normal) text-card-foreground\",\n // data-focused, not :hover. React Aria drives one highlight from both\n // pointer and keyboard, so the row under the mouse and the row the\n // arrows reached are the same row.\n \"outline-none data-[focused]:bg-muted\",\n \"motion-safe:transition-colors motion-safe:duration-(--duration-instant) motion-safe:ease-settle\",\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 \"data-[disabled]:cursor-not-allowed data-[disabled]:opacity-(--opacity-disabled)\",\n]);\n\n/**\n * A single-choice picker.\n *\n * @example\n * <Select label=\"Country\" placeholder=\"Pick one\"\n * selectedKey={code} onSelectionChange={setCode}>\n * <SelectItem id=\"gb\">United Kingdom</SelectItem>\n * <SelectItem id=\"us\">United States</SelectItem>\n * </Select>\n */\nexport interface SelectProps extends Omit<\n AriaSelectProps<object>,\n \"className\" | \"style\" | \"children\" | \"items\"\n> {\n /** `SelectItem` children. */\n children?: ReactNode;\n /**\n * What is being chosen, and the control's accessible name.\n *\n * **Required.** All 15 legacy selects are unnamed: the legacy exports a\n * `SelectLabel` that nobody renders and passes no `aria-label`.\n */\n label: ReactNode;\n /** Render the label for screen readers only. */\n labelHidden?: boolean;\n /** Shown while nothing is selected. */\n placeholder?: string;\n /**\n * Why the selection is wrong. Rendered under the control, and it is what\n * makes `isInvalid` reach a screen reader.\n *\n * React Aria marks the ROOT `data-invalid` and leaves the button untouched,\n * and an `aria-invalid` handed to that button is dropped by\n * `filterDOMProps`, whose allowlist stops at the labelling attributes -- the\n * same silent loss as `aria-current` on a pagination button. So without a\n * message, an invalid select is invalid only in colour, which is WCAG 1.4.1\n * on its own. React Aria wires this into `aria-describedby` for us.\n */\n errorMessage?: ReactNode;\n /**\n * Control scale of the trigger. Height and inline padding come from the\n * same `--density-control-*` tokens as Input and Button, so a Select, a\n * field and a button on one rung are the same height.\n *\n * Type does not scale with it, matching Input.\n *\n * @default \"md\"\n */\n size?: \"sm\" | \"md\" | \"lg\";\n /** Extra classes for the wrapper. Merged last. */\n className?: string;\n}\n\n/**\n * The shown value. React Aria's own `SelectValue` stays, visually hidden, as the\n * text a screen reader reads; the copy the eye sees rolls when the selection\n * changes: a later option rises from below, an earlier one drops from above. The\n * direction is read from the option's place in the list. A pre-selected value\n * does not animate on first mount.\n */\nfunction ValueRoll({ placeholder }: { placeholder?: string }) {\n const state = useContext(SelectStateContext);\n const key = state?.selectedKey ?? null;\n let index = -1;\n if (state && key != null) {\n let n = 0;\n for (const item of state.collection) {\n if (item.type !== \"item\") continue;\n if (item.key === key) index = n;\n n += 1;\n }\n }\n const [previous, setPrevious] = useState(index);\n const [dir, setDir] = useState(1);\n if (previous !== index) {\n setPrevious(index);\n setDir(index > previous ? 1 : -1);\n }\n const text = state?.selectedItem?.textValue ?? \"\";\n // React Aria resolves a default selection after the first render, so the key\n // starts as null and then settles. Until the collection is built nothing is\n // shown, and the swap remounts once it is, so that settling is not a change.\n const built = (state?.collection.size ?? 0) > 0 || state?.selectedKey != null;\n const [mounted, setMounted] = useState(false);\n useIsomorphicLayoutEffect(() => setMounted(true), []);\n const settled = built || mounted;\n const swapKey = key == null ? \"placeholder\" : `value-${String(key)}`;\n return (\n <RollingValue\n srValue={\n <AriaSelectValue data-slot=\"select-value\" className=\"sr-only\">\n {({ isPlaceholder, selectedText }) => (isPlaceholder ? placeholder : selectedText)}\n </AriaSelectValue>\n }\n swapKey={swapKey}\n dir={dir}\n instanceKey={settled ? \"live\" : \"boot\"}\n >\n {key == null ? (\n <span data-slot=\"select-placeholder\" className=\"text-muted-foreground\">\n {placeholder}\n </span>\n ) : (\n text\n )}\n </RollingValue>\n );\n}\n\nexport const Select = forwardRef<HTMLButtonElement, SelectProps>(function Select(\n { children, label, labelHidden, placeholder, errorMessage, size = \"md\", className, ...props },\n ref,\n) {\n return (\n <AriaSelect {...props} data-slot=\"select\" className={cn(selectVariants(), className)}>\n {/*\n The Label's own classes, so a Select's label and the Label over an\n Input in the same form are one style (same size, weight, colour and\n line height). The select-label slot stays for consumers that target it.\n */}\n <AriaLabel\n data-slot=\"select-label\"\n className={cnAll(labelVariants(), labelHidden && \"sr-only\")}\n >\n {label}\n </AriaLabel>\n\n {/*\n The ref goes on the TRIGGER, not the AriaSelect root above. The root\n is a field-level wrapper div; the trigger is the always-visible\n interactive surface a caller actually wants to focus or click,\n the same reasoning that puts Input's ref on the real control.\n */}\n <AriaButton ref={ref} data-slot=\"select-trigger\" className={selectTriggerVariants({ size })}>\n <ValueRoll placeholder={placeholder} />\n {/*\n aria-hidden: the chevron is decoration. The button already\n announces itself as a combobox, and a second name source only\n makes it announce twice.\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 strokeLinejoin=\"round\"\n className=\"text-muted-foreground motion-safe:transition-[rotate] motion-safe:duration-(--duration-gentle) motion-safe:ease-gentle group-aria-expanded/trigger:rotate-180\"\n >\n <path d=\"m4 6 4 4 4-4\" />\n </svg>\n </AriaButton>\n\n {errorMessage == null ? null : (\n // Not conditional on isInvalid: React Aria renders FieldError only\n // while the field IS invalid, so gating it here as well would just be\n // two sources of truth for one state.\n <AriaFieldError data-slot=\"select-error\" className={selectErrorClass}>\n {errorMessage}\n </AriaFieldError>\n )}\n\n {/* No border colour on the list: it is lifted by its shadow alone. */}\n <AriaPopover\n data-slot=\"select-popover\"\n className={cn(selectPopoverVariants(), \"border-0\")}\n >\n <AriaListBox data-slot=\"select-list\" className=\"outline-none\">\n {children}\n </AriaListBox>\n </AriaPopover>\n </AriaSelect>\n );\n});\n\n/**\n * One option. Its `id` is the value.\n *\n * @example\n * <SelectItem id=\"gb\">United Kingdom</SelectItem>\n */\nexport interface SelectItemProps extends Omit<\n AriaListBoxItemProps,\n \"className\" | \"style\" | \"children\"\n> {\n children?: ReactNode;\n /**\n * The option's text, for the trigger and for typeahead.\n *\n * Derived from `children` when they are a string. Pass it explicitly\n * when the option renders anything richer than text.\n */\n textValue?: string;\n /** Extra classes. Merged last, so they win conflicts with ours. */\n className?: string;\n}\n\nexport const SelectItem = forwardRef<HTMLDivElement, SelectItemProps>(function SelectItem(\n { className, children, textValue, ...props },\n ref,\n) {\n return (\n <AriaListBoxItem\n ref={ref}\n {...props}\n /*\n textValue, derived from string children.\n React Aria infers an item's text from its children -- but ONLY\n when they are a string. This item uses FUNCTION children so the\n tick can be rendered in JS, which defeats that inference: the\n trigger showed an empty value after selecting, and typeahead had\n nothing to match on. Neither is visible in the type system.\n */\n textValue={textValue ?? (typeof children === \"string\" ? children : undefined)}\n data-slot=\"select-item\"\n className={cn(selectItemVariants(), className)}\n >\n {({ isSelected }) => (\n <>\n {/*\n The tick is rendered in JS rather than toggled with CSS, so\n the precedence is explicit and a test can read it from the\n DOM. ps-8 above reserves the gutter, so selecting an option\n does not shift its label.\n */}\n {isSelected ? (\n <svg\n data-slot=\"select-item-check\"\n aria-hidden=\"true\"\n viewBox=\"0 0 16 16\"\n fill=\"none\"\n stroke=\"currentColor\"\n strokeWidth=\"2\"\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n className=\"absolute end-2 size-3.5 motion-safe:animate-select-check-in\"\n >\n <path d=\"m3.5 8.5 3 3 6-6\" />\n </svg>\n ) : null}\n {children}\n </>\n )}\n </AriaListBoxItem>\n );\n});\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAuDA,MAAa,iBAAiB,IAAI,CAAC,yDAAyD,CAAC;;AAG7F,MAAa,mBAAmB;AAEhC,MAAa,wBAAwB,IACnC;CAGE;CACA,qBAAqB;CAGrB;CAKA;CACA;CACA;CACA;CACA;CACA;CACA;AACF,GACA;CACE,UAAU;;;;;;;;;;;;;;AAcR,MAAM;EACJ,IAAI,CAAC,aAAa,IAAI,cAAc,EAAE;EACtC,IAAI,CAAC,aAAa,IAAI,cAAc,EAAE;EACtC,IAAI,CAAC,aAAa,IAAI,cAAc,EAAE;CACxC,EACF;CACA,iBAAiB,EAAE,MAAM,KAAK;AAChC,CACF;AAEA,MAAa,wBAAwB,IACnC;CAGE;CAKA;CAGA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;AACF,GACA;CACE,UAAU;;;;;AAKR,QAAQ;EACN,MAAM;EACN,MAAM;CACR,EACF;CACA,iBAAiB,EAAE,QAAQ,OAAO;AACpC,CACF;AAEA,MAAa,qBAAqB,IAAI;CACpC;CAEA;CACA;CAIA;CACA;CACA;CACA;CACA;CACA;CACA;AACF,CAAC;;;;;;;;AA8DD,SAAS,UAAU,EAAE,eAAyC;CAC5D,MAAM,QAAQ,WAAW,kBAAkB;CAC3C,MAAM,MAAM,OAAO,eAAe;CAClC,IAAI,QAAQ;CACZ,IAAI,SAAS,OAAO,MAAM;EACxB,IAAI,IAAI;EACR,KAAK,MAAM,QAAQ,MAAM,YAAY;GACnC,IAAI,KAAK,SAAS,QAAQ;GAC1B,IAAI,KAAK,QAAQ,KAAK,QAAQ;GAC9B,KAAK;EACP;CACF;CACA,MAAM,CAAC,UAAU,eAAe,SAAS,KAAK;CAC9C,MAAM,CAAC,KAAK,UAAU,SAAS,CAAC;CAChC,IAAI,aAAa,OAAO;EACtB,YAAY,KAAK;EACjB,OAAO,QAAQ,WAAW,IAAI,EAAE;CAClC;CACA,MAAM,OAAO,OAAO,cAAc,aAAa;CAI/C,MAAM,SAAS,OAAO,WAAW,QAAQ,KAAK,KAAK,OAAO,eAAe;CACzE,MAAM,CAAC,SAAS,cAAc,SAAS,KAAK;CAC5C,gCAAgC,WAAW,IAAI,GAAG,CAAC,CAAC;CACpD,MAAM,UAAU,SAAS;CACzB,MAAM,UAAU,OAAO,OAAO,gBAAgB,SAAS,OAAO,GAAG;CACjE,OACE,oBAAC,cAAD;EACE,SACE,oBAACA,aAAD;GAAiB,aAAU;GAAe,WAAU;GAChD,WAAA,EAAE,eAAe,mBAAoB,gBAAgB,cAAc;EACtD,CAAA;EAEV;EACJ;EACL,aAAa,UAAU,SAAS;EAE/B,UAAA,OAAO,OACN,oBAAC,QAAD;GAAM,aAAU;GAAqB,WAAU;GAC5C,UAAA;EACG,CAAA,IAEN;CAEU,CAAA;AAElB;AAEA,MAAaC,WAAS,WAA2C,SAASA,SACxE,EAAE,UAAU,OAAO,aAAa,aAAa,cAAc,OAAO,MAAM,WAAW,GAAG,SACtF,KACA;CACA,OACE,qBAACC,QAAD;EAAY,GAAI;EAAO,aAAU;EAAS,WAAW,GAAG,eAAe,GAAG,SAAS;EAAnF,UAAA;GAME,oBAACC,OAAD;IACE,aAAU;IACV,WAAW,MAAM,cAAc,GAAG,eAAe,SAAS;IAEzD,UAAA;GACQ,CAAA;GAQX,qBAACC,QAAD;IAAiB;IAAK,aAAU;IAAiB,WAAW,sBAAsB,EAAE,KAAK,CAAC;IAA1F,UAAA,CACE,oBAAC,WAAD,EAAwB,YAAc,CAAA,GAMtC,oBAAC,OAAD;KACE,eAAY;KACZ,SAAQ;KACR,MAAK;KACL,QAAO;KACP,aAAY;KACZ,eAAc;KACd,gBAAe;KACf,WAAU;KAEV,UAAA,oBAAC,QAAD,EAAM,GAAE,eAAgB,CAAA;IACrB,CAAA,CACK;;GAEX,gBAAgB,OAAO,OAItB,oBAACC,YAAD;IAAgB,aAAU;IAAe,WAAW;IACjD,UAAA;GACa,CAAA;GAIlB,oBAACC,SAAD;IACE,aAAU;IACV,WAAW,GAAG,sBAAsB,GAAG,UAAU;IAEjD,UAAA,oBAACC,SAAD;KAAa,aAAU;KAAc,WAAU;KAC5C;IACU,CAAA;GACF,CAAA;EACH;;AAEhB,CAAC;AAwBD,MAAa,aAAa,WAA4C,SAAS,WAC7E,EAAE,WAAW,UAAU,WAAW,GAAG,SACrC,KACA;CACA,OACE,oBAACC,aAAD;EACO;EACL,GAAI;EASJ,WAAW,cAAc,OAAO,aAAa,WAAW,WAAW,KAAA;EACnE,aAAU;EACV,WAAW,GAAG,mBAAmB,GAAG,SAAS;EAE3C,WAAA,EAAE,iBACF,qBAAA,YAAA,EAAA,UAAA,CAOG,aACC,oBAAC,OAAD;GACE,aAAU;GACV,eAAY;GACZ,SAAQ;GACR,MAAK;GACL,QAAO;GACP,aAAY;GACZ,eAAc;GACd,gBAAe;GACf,WAAU;GAEV,UAAA,oBAAC,QAAD,EAAM,GAAE,mBAAoB,CAAA;EACzB,CAAA,IACH,MACH,QACD,EAAA,CAAA;CAEW,CAAA;AAErB,CAAC"}
@@ -6,15 +6,18 @@ import { SeparatorProps } from "react-aria-components";
6
6
  * Census: 7 files, 6 uses, one of them `orientation="vertical"`. No other
7
7
  * prop is ever passed.
8
8
  *
9
- * `decorative` is NOT ported -- zero call sites pass it, and React Aria has
10
- * no equivalent. That does change the semantics of all six existing uses,
11
- * which is worth saying out loud rather than burying: the legacy defaulted
12
- * to `decorative: true` and therefore rendered `role="none"`, so every
13
- * divider in the app is currently invisible to assistive technology. React
14
- * Aria renders an `<hr>` for a horizontal separator, whose implicit role IS
15
- * separator. A visible line between two groups of content is a separator;
16
- * announcing it is the correct behaviour, and it is what the element the
17
- * legacy chose (a `div`) was pretending not to be.
9
+ * The legacy defaulted to `decorative: true` and therefore rendered
10
+ * `role="none"`, so every divider in the app was invisible to assistive
11
+ * technology. Here the default is the opposite, which is worth saying out
12
+ * loud rather than burying: React Aria renders an `<hr>` for a horizontal
13
+ * separator, whose implicit role IS separator. A visible line between two
14
+ * groups of content is a separator; announcing it is the correct behaviour.
15
+ *
16
+ * `decorative` is the opt-in for the other case: a rule between inline
17
+ * metadata ("12 posts | 3 drafts") that only spaces items, where announcing
18
+ * "separator" between every pair is noise. React Aria has no equivalent and
19
+ * its Separator always carries a separator role, so a decorative one is a
20
+ * plain `aria-hidden` div drawn with the same classes.
18
21
  *
19
22
  * Every string in the cva blocks below is a STATIC literal. Tailwind scans
20
23
  * source text, so a class assembled at runtime is invisible to it and would
@@ -33,6 +36,15 @@ export declare const separatorVariants: (props?: ({
33
36
  interface SeparatorProps$1 extends Omit<SeparatorProps, "className" | "style"> {
34
37
  /** @default "horizontal" */
35
38
  orientation?: "horizontal" | "vertical";
39
+ /**
40
+ * Draw the line without announcing it: a plain `div` with
41
+ * `aria-hidden="true"` instead of a separator. For a rule that only spaces
42
+ * inline items (metadata, breadcrumbs-like rows), where a screen reader
43
+ * saying "separator" between each pair is noise. Leave it off for a line
44
+ * between two groups of content.
45
+ * @default false
46
+ */
47
+ decorative?: boolean;
36
48
  /** Extra classes. Merged last, so they win conflicts with ours. */
37
49
  className?: string;
38
50
  }
@@ -11,22 +11,25 @@ import { jsx } from "react/jsx-runtime";
11
11
  * Census: 7 files, 6 uses, one of them `orientation="vertical"`. No other
12
12
  * prop is ever passed.
13
13
  *
14
- * `decorative` is NOT ported -- zero call sites pass it, and React Aria has
15
- * no equivalent. That does change the semantics of all six existing uses,
16
- * which is worth saying out loud rather than burying: the legacy defaulted
17
- * to `decorative: true` and therefore rendered `role="none"`, so every
18
- * divider in the app is currently invisible to assistive technology. React
19
- * Aria renders an `<hr>` for a horizontal separator, whose implicit role IS
20
- * separator. A visible line between two groups of content is a separator;
21
- * announcing it is the correct behaviour, and it is what the element the
22
- * legacy chose (a `div`) was pretending not to be.
14
+ * The legacy defaulted to `decorative: true` and therefore rendered
15
+ * `role="none"`, so every divider in the app was invisible to assistive
16
+ * technology. Here the default is the opposite, which is worth saying out
17
+ * loud rather than burying: React Aria renders an `<hr>` for a horizontal
18
+ * separator, whose implicit role IS separator. A visible line between two
19
+ * groups of content is a separator; announcing it is the correct behaviour.
20
+ *
21
+ * `decorative` is the opt-in for the other case: a rule between inline
22
+ * metadata ("12 posts | 3 drafts") that only spaces items, where announcing
23
+ * "separator" between every pair is noise. React Aria has no equivalent and
24
+ * its Separator always carries a separator role, so a decorative one is a
25
+ * plain `aria-hidden` div drawn with the same classes.
23
26
  *
24
27
  * Every string in the cva blocks below is a STATIC literal. Tailwind scans
25
28
  * source text, so a class assembled at runtime is invisible to it and would
26
29
  * simply never be emitted.
27
30
  */
28
31
  const separatorVariants = cva([
29
- "shrink-0 bg-border border-none",
32
+ "shrink-0 bg-separator border-none",
30
33
  "m-0",
31
34
  "box-border"
32
35
  ], {
@@ -36,7 +39,26 @@ const separatorVariants = cva([
36
39
  } },
37
40
  defaultVariants: { orientation: "horizontal" }
38
41
  });
39
- const Separator$1 = forwardRef(function Separator$2({ className, orientation = "horizontal", ...props }, ref) {
42
+ /** Props a decorative separator does not forward to its div (see below). */
43
+ const DECORATIVE_DROPPED = [
44
+ "elementType",
45
+ "slot",
46
+ "render",
47
+ "aria-label",
48
+ "aria-labelledby"
49
+ ];
50
+ const Separator$1 = forwardRef(function Separator$2({ className, orientation = "horizontal", decorative = false, ...props }, ref) {
51
+ if (decorative) {
52
+ const rest = { ...props };
53
+ for (const key of DECORATIVE_DROPPED) delete rest[key];
54
+ return /* @__PURE__ */ jsx("div", {
55
+ ref,
56
+ ...rest,
57
+ "aria-hidden": "true",
58
+ "data-slot": "separator",
59
+ className: cn(separatorVariants({ orientation }), className)
60
+ });
61
+ }
40
62
  return /* @__PURE__ */ jsx(Separator, {
41
63
  ref,
42
64
  ...props,
@@ -1 +1 @@
1
- {"version":3,"file":"separator.js","names":["Separator","AriaSeparator"],"sources":["../../src/separator/separator.tsx"],"sourcesContent":["\"use client\";\n\nimport { cva } from \"class-variance-authority\";\nimport { forwardRef } from \"react\";\nimport {\n Separator as AriaSeparator,\n type SeparatorProps as AriaSeparatorProps,\n} from \"react-aria-components\";\n\nimport { cn } from \"../lib/cn.js\";\n\n/**\n * Separator, ported from the legacy component onto React Aria's Separator.\n *\n * Census: 7 files, 6 uses, one of them `orientation=\"vertical\"`. No other\n * prop is ever passed.\n *\n * `decorative` is NOT ported -- zero call sites pass it, and React Aria has\n * no equivalent. That does change the semantics of all six existing uses,\n * which is worth saying out loud rather than burying: the legacy defaulted\n * to `decorative: true` and therefore rendered `role=\"none\"`, so every\n * divider in the app is currently invisible to assistive technology. React\n * Aria renders an `<hr>` for a horizontal separator, whose implicit role IS\n * separator. A visible line between two groups of content is a separator;\n * announcing it is the correct behaviour, and it is what the element the\n * legacy chose (a `div`) was pretending not to be.\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 separatorVariants = cva(\n [\n // border-none, not border-0. `border-0` sets the WIDTH to zero but leaves\n // border-style at solid, so the computed style still claims a border that\n // simply measures nothing -- the same bookkeeping the Checkbox parity run\n // turned up on the legacy input. `border-none` says what is true.\n \"shrink-0 bg-border border-none\",\n // The user-agent <hr> ships a block margin and a 1px inset border, and\n // React Aria renders an <hr> for the horizontal case. The reset in\n // @seekalfred/styles is OPT-IN, so a consumer may load none -- unreset,\n // every divider is a grooved double line with space around it.\n \"m-0\",\n \"box-border\",\n ],\n {\n variants: {\n orientation: {\n // h-px / w-px, not the legacy h-[1px] / w-[1px]. Those are bracket\n // values that check:hardcoding rejects, and `px` is a first-class\n // Tailwind step rather than an arbitrary one.\n horizontal: \"h-px w-full\",\n vertical: \"h-full w-px\",\n },\n },\n defaultVariants: { orientation: \"horizontal\" },\n },\n);\n\n/**\n * A visual divider between two groups of content.\n *\n * @example\n * <Separator />\n * <Separator orientation=\"vertical\" />\n */\nexport interface SeparatorProps extends Omit<AriaSeparatorProps, \"className\" | \"style\"> {\n /** @default \"horizontal\" */\n orientation?: \"horizontal\" | \"vertical\";\n /** Extra classes. Merged last, so they win conflicts with ours. */\n className?: string;\n}\n\nexport const Separator = forwardRef<HTMLElement, SeparatorProps>(function Separator(\n { className, orientation = \"horizontal\", ...props },\n ref,\n) {\n return (\n <AriaSeparator\n ref={ref}\n {...props}\n orientation={orientation}\n data-slot=\"separator\"\n className={cn(separatorVariants({ orientation }), className)}\n />\n );\n});\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;AA+BA,MAAa,oBAAoB,IAC/B;CAKE;CAKA;CACA;AACF,GACA;CACE,UAAU,EACR,aAAa;EAIX,YAAY;EACZ,UAAU;CACZ,EACF;CACA,iBAAiB,EAAE,aAAa,aAAa;AAC/C,CACF;AAgBA,MAAaA,cAAY,WAAwC,SAASA,YACxE,EAAE,WAAW,cAAc,cAAc,GAAG,SAC5C,KACA;CACA,OACE,oBAACC,WAAD;EACO;EACL,GAAI;EACS;EACb,aAAU;EACV,WAAW,GAAG,kBAAkB,EAAE,YAAY,CAAC,GAAG,SAAS;CAC5D,CAAA;AAEL,CAAC"}
1
+ {"version":3,"file":"separator.js","names":["Separator","AriaSeparator"],"sources":["../../src/separator/separator.tsx"],"sourcesContent":["\"use client\";\n\nimport { cva } from \"class-variance-authority\";\nimport { forwardRef, type Ref } from \"react\";\nimport {\n Separator as AriaSeparator,\n type SeparatorProps as AriaSeparatorProps,\n} from \"react-aria-components\";\n\nimport { cn } from \"../lib/cn.js\";\n\n/**\n * Separator, ported from the legacy component onto React Aria's Separator.\n *\n * Census: 7 files, 6 uses, one of them `orientation=\"vertical\"`. No other\n * prop is ever passed.\n *\n * The legacy defaulted to `decorative: true` and therefore rendered\n * `role=\"none\"`, so every divider in the app was invisible to assistive\n * technology. Here the default is the opposite, which is worth saying out\n * loud rather than burying: React Aria renders an `<hr>` for a horizontal\n * separator, whose implicit role IS separator. A visible line between two\n * groups of content is a separator; announcing it is the correct behaviour.\n *\n * `decorative` is the opt-in for the other case: a rule between inline\n * metadata (\"12 posts | 3 drafts\") that only spaces items, where announcing\n * \"separator\" between every pair is noise. React Aria has no equivalent and\n * its Separator always carries a separator role, so a decorative one is a\n * plain `aria-hidden` div drawn with the same classes.\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 separatorVariants = cva(\n [\n // border-none, not border-0. `border-0` sets the WIDTH to zero but leaves\n // border-style at solid, so the computed style still claims a border that\n // simply measures nothing -- the same bookkeeping the Checkbox parity run\n // turned up on the legacy input. `border-none` says what is true.\n // The separator role, not `border`: `border` is the outline of a card or\n // field, `separator` the hairline between rows and groups inside one.\n \"shrink-0 bg-separator border-none\",\n // The user-agent <hr> ships a block margin and a 1px inset border, and\n // React Aria renders an <hr> for the horizontal case. The reset in\n // @seekalfred/styles is OPT-IN, so a consumer may load none -- unreset,\n // every divider is a grooved double line with space around it.\n \"m-0\",\n \"box-border\",\n ],\n {\n variants: {\n orientation: {\n // h-px / w-px, not the legacy h-[1px] / w-[1px]. Those are bracket\n // values that check:hardcoding rejects, and `px` is a first-class\n // Tailwind step rather than an arbitrary one.\n horizontal: \"h-px w-full\",\n vertical: \"h-full w-px\",\n },\n },\n defaultVariants: { orientation: \"horizontal\" },\n },\n);\n\n/**\n * A visual divider between two groups of content.\n *\n * @example\n * <Separator />\n * <Separator orientation=\"vertical\" />\n */\nexport interface SeparatorProps extends Omit<AriaSeparatorProps, \"className\" | \"style\"> {\n /** @default \"horizontal\" */\n orientation?: \"horizontal\" | \"vertical\";\n /**\n * Draw the line without announcing it: a plain `div` with\n * `aria-hidden=\"true\"` instead of a separator. For a rule that only spaces\n * inline items (metadata, breadcrumbs-like rows), where a screen reader\n * saying \"separator\" between each pair is noise. Leave it off for a line\n * between two groups of content.\n * @default false\n */\n decorative?: boolean;\n /** Extra classes. Merged last, so they win conflicts with ours. */\n className?: string;\n}\n\n/** Props a decorative separator does not forward to its div (see below). */\nconst DECORATIVE_DROPPED = [\n \"elementType\",\n \"slot\",\n \"render\",\n \"aria-label\",\n \"aria-labelledby\",\n] as const;\n\nexport const Separator = forwardRef<HTMLElement, SeparatorProps>(function Separator(\n { className, orientation = \"horizontal\", decorative = false, ...props },\n ref,\n) {\n if (decorative) {\n // Everything else the caller passed (id, data-*, handlers, dir...) reaches\n // the div, as it reaches React Aria's Separator below. Dropped: the props\n // that only mean something to React Aria (`elementType`, `slot`, `render`)\n // and a name, which an aria-hidden line has no use for.\n const rest = { ...props };\n for (const key of DECORATIVE_DROPPED) delete rest[key];\n return (\n <div\n ref={ref as Ref<HTMLDivElement>}\n {...(rest as Omit<typeof rest, (typeof DECORATIVE_DROPPED)[number]>)}\n aria-hidden=\"true\"\n data-slot=\"separator\"\n className={cn(separatorVariants({ orientation }), className)}\n />\n );\n }\n return (\n <AriaSeparator\n ref={ref}\n {...props}\n orientation={orientation}\n data-slot=\"separator\"\n className={cn(separatorVariants({ orientation }), className)}\n />\n );\n});\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAkCA,MAAa,oBAAoB,IAC/B;CAOE;CAKA;CACA;AACF,GACA;CACE,UAAU,EACR,aAAa;EAIX,YAAY;EACZ,UAAU;CACZ,EACF;CACA,iBAAiB,EAAE,aAAa,aAAa;AAC/C,CACF;;AA0BA,MAAM,qBAAqB;CACzB;CACA;CACA;CACA;CACA;AACF;AAEA,MAAaA,cAAY,WAAwC,SAASA,YACxE,EAAE,WAAW,cAAc,cAAc,aAAa,OAAO,GAAG,SAChE,KACA;CACA,IAAI,YAAY;EAKd,MAAM,OAAO,EAAE,GAAG,MAAM;EACxB,KAAK,MAAM,OAAO,oBAAoB,OAAO,KAAK;EAClD,OACE,oBAAC,OAAD;GACO;GACL,GAAK;GACL,eAAY;GACZ,aAAU;GACV,WAAW,GAAG,kBAAkB,EAAE,YAAY,CAAC,GAAG,SAAS;EAC5D,CAAA;CAEL;CACA,OACE,oBAACC,WAAD;EACO;EACL,GAAI;EACS;EACb,aAAU;EACV,WAAW,GAAG,kBAAkB,EAAE,YAAY,CAAC,GAAG,SAAS;CAC5D,CAAA;AAEL,CAAC"}
@@ -1,2 +1,2 @@
1
- import { StatCard, StatCardProps, StatCardTone, statCardHeaderVariants, statCardLabelVariants, statCardSubVariants, statCardValueVariants, statCardVariants } from "./stat-card.js";
2
- export { StatCard, type StatCardProps, type StatCardTone, statCardHeaderVariants, statCardLabelVariants, statCardSubVariants, statCardValueVariants, statCardVariants };
1
+ import { StatCard, StatCardChangeTone, StatCardProps, StatCardTone, StatCardTrend, statCardHeaderVariants, statCardIconVariants, statCardLabelVariants, statCardSubVariants, statCardValueVariants, statCardVariants } from "./stat-card.js";
2
+ export { StatCard, type StatCardChangeTone, type StatCardProps, type StatCardTone, type StatCardTrend, statCardHeaderVariants, statCardIconVariants, statCardLabelVariants, statCardSubVariants, statCardValueVariants, statCardVariants };
@@ -1,2 +1,2 @@
1
- import { StatCard, statCardHeaderVariants, statCardLabelVariants, statCardSubVariants, statCardValueVariants, statCardVariants } from "./stat-card.js";
2
- export { StatCard, statCardHeaderVariants, statCardLabelVariants, statCardSubVariants, statCardValueVariants, statCardVariants };
1
+ import { StatCard, statCardHeaderVariants, statCardIconVariants, statCardLabelVariants, statCardSubVariants, statCardValueVariants, statCardVariants } from "./stat-card.js";
2
+ export { StatCard, statCardHeaderVariants, statCardIconVariants, statCardLabelVariants, statCardSubVariants, statCardValueVariants, statCardVariants };
@@ -24,6 +24,10 @@ import { ReactNode } from "react";
24
24
  * colour. `neutral` is the default and is right for any figure without a
25
25
  * verdict in it, including every zero.
26
26
  * - Cards in a row are the same height (`h-full` in a grid or flex row).
27
+ * - The change pill's colour is what the change MEANS. By default a rise is
28
+ * good; `trend="inverse"` reads a rise as bad (bounce rate, cost, keywords
29
+ * that dropped out), `trend="neutral"` gives no verdict, and `changeTone`
30
+ * names the meaning outright when the text carries no sign.
27
31
  *
28
32
  * A card can be a link (`href`, React Aria Link, so a router provider routes
29
33
  * it), a button (`onPress`), or a filter toggle (`isSelected` + `onPress`, a
@@ -41,10 +45,16 @@ export declare const statCardVariants: (props?: ({
41
45
  export declare const statCardHeaderVariants: (props?: import("class-variance-authority/types").ClassProp | undefined) => string;
42
46
  export declare const statCardLabelVariants: (props?: import("class-variance-authority/types").ClassProp | undefined) => string;
43
47
  export declare const statCardValueVariants: (props?: ({
44
- tone?: "neutral" | "warning" | "positive" | "negative" | "attention" | null | undefined;
48
+ tone?: "warning" | "neutral" | "positive" | "negative" | "attention" | null | undefined;
45
49
  } & import("class-variance-authority/types").ClassProp) | undefined) => string;
50
+ /** An optional icon before the label. Decorative: the label names the figure. */
51
+ export declare const statCardIconVariants: (props?: import("class-variance-authority/types").ClassProp | undefined) => string;
46
52
  export declare const statCardSubVariants: (props?: import("class-variance-authority/types").ClassProp | undefined) => string;
47
53
  export type StatCardTone = "neutral" | "positive" | "negative" | "warning" | "attention";
54
+ /** Which way is good for this figure: up (`normal`), down (`inverse`), or neither (`neutral`). */
55
+ export type StatCardTrend = "normal" | "inverse" | "neutral";
56
+ /** What a change means, whatever its sign. */
57
+ export type StatCardChangeTone = "positive" | "negative" | "neutral";
48
58
  export interface StatCardProps {
49
59
  /** What the figure is. One line; cut with an ellipsis, full text on hover. */
50
60
  label: string;
@@ -63,6 +73,27 @@ export interface StatCardProps {
63
73
  * place and the pill follows its width.
64
74
  */
65
75
  change?: string;
76
+ /**
77
+ * Which direction of `change` is good. `normal` (default): a leading `+`
78
+ * reads green and a leading `-` red. `inverse`: a rise reads red and a fall
79
+ * green, for a figure where more is worse (bounce rate, cost per click,
80
+ * keywords that fell out of the top 100). `neutral`: no colour either way.
81
+ * @default "normal"
82
+ */
83
+ trend?: StatCardTrend;
84
+ /**
85
+ * The meaning of `change`, named outright. Wins over `trend` and the sign;
86
+ * use it when the text has no `+` or `-` (`"▲4"`) or the verdict comes from
87
+ * elsewhere. It colours only the change pill, never the value (that is
88
+ * `tone`).
89
+ */
90
+ changeTone?: StatCardChangeTone;
91
+ /**
92
+ * An icon before the label. Decorative (hidden from assistive technology).
93
+ * An SVG is drawn at 16px, whatever size it is given. A falsy value
94
+ * (`false`, `null`, `""`) renders nothing, so `icon={cond && <Icon />}` works.
95
+ */
96
+ icon?: ReactNode;
66
97
  /**
67
98
  * What the figure means: `neutral` (default, and right for every zero),
68
99
  * `positive`, `negative`, `warning` (part-way or waiting) or `attention`
@@ -85,6 +116,8 @@ export interface StatCardProps {
85
116
  /** Layout classes for the card (margins, grid placement). */
86
117
  className?: string;
87
118
  "aria-label"?: string;
119
+ /** `data-*` attributes land on the card's root (test ids, analytics hooks). `data-slot` stays the card's own. */
120
+ [dataAttribute: `data-${string}`]: string | number | boolean | undefined;
88
121
  }
89
122
  /**
90
123
  * One headline figure.
@@ -32,6 +32,10 @@ import { jsx, jsxs } from "react/jsx-runtime";
32
32
  * colour. `neutral` is the default and is right for any figure without a
33
33
  * verdict in it, including every zero.
34
34
  * - Cards in a row are the same height (`h-full` in a grid or flex row).
35
+ * - The change pill's colour is what the change MEANS. By default a rise is
36
+ * good; `trend="inverse"` reads a rise as bad (bounce rate, cost, keywords
37
+ * that dropped out), `trend="neutral"` gives no verdict, and `changeTone`
38
+ * names the meaning outright when the text carries no sign.
35
39
  *
36
40
  * A card can be a link (`href`, React Aria Link, so a router provider routes
37
41
  * it), a button (`onPress`), or a filter toggle (`isSelected` + `onPress`, a
@@ -85,6 +89,8 @@ tone: {
85
89
  } },
86
90
  defaultVariants: { tone: "neutral" }
87
91
  });
92
+ /** An optional icon before the label. Decorative: the label names the figure. */
93
+ const statCardIconVariants = cva("flex shrink-0 items-center text-muted-foreground [&_svg]:size-4 [&_svg]:shrink-0");
88
94
  const statCardSubVariants = cva("m-0 mt-4 text-sm leading-(--leading-normal) text-muted-foreground");
89
95
  /** The number in a piece of text, for the direction a swap moves: a larger one rises from below. */
90
96
  const amountIn = (text) => Number(text.replace(/,/g, "").match(/-?\d+(?:\.\d+)?/)?.[0] ?? NaN);
@@ -144,7 +150,20 @@ function figureOf(value) {
144
150
  suffix
145
151
  };
146
152
  }
147
- const changeTone = (change) => /^\+/.test(change) ? "success" : /^[-−]/.test(change) ? "danger" : "neutral";
153
+ const signOf = (change) => /^\+/.test(change) ? 1 : /^[-−]/.test(change) ? -1 : 0;
154
+ /** What a change means: the explicit tone, else its sign read the way `trend` says. */
155
+ function changeMeaning(change, trend, explicit) {
156
+ if (explicit) return explicit;
157
+ const sign = trend === "neutral" ? 0 : signOf(change) * (trend === "inverse" ? -1 : 1);
158
+ return sign > 0 ? "positive" : sign < 0 ? "negative" : "neutral";
159
+ }
160
+ const BADGE_TONE = {
161
+ positive: "success",
162
+ negative: "danger",
163
+ neutral: "neutral"
164
+ };
165
+ /** Only the `data-*` attributes, so nothing else in the rest object reaches the DOM. */
166
+ const dataAttributes = (rest) => Object.fromEntries(Object.entries(rest).filter(([key]) => key.startsWith("data-")));
148
167
  /**
149
168
  * One headline figure.
150
169
  *
@@ -155,33 +174,43 @@ const changeTone = (change) => /^\+/.test(change) ? "success" : /^[-−]/.test(c
155
174
  * <StatCard label="Collected" value="$9,800" tone="positive" href="/payments" />
156
175
  * </div>
157
176
  */
158
- const StatCard = forwardRef(function StatCard({ label, value, sub, change, tone, isSelected, href, onPress, description, children, className, ...rest }, ref) {
177
+ const StatCard = forwardRef(function StatCard({ label, value, sub, change, trend = "normal", changeTone, icon, tone, isSelected, href, onPress, description, children, className, ...rest }, ref) {
159
178
  const figure = figureOf(value);
179
+ const meaning = change ? changeMeaning(change, trend, changeTone) : "neutral";
160
180
  const body = /* @__PURE__ */ jsxs("span", {
161
181
  className: "flex min-w-0 flex-1 flex-col",
162
182
  title: description,
163
183
  children: [
164
184
  /* @__PURE__ */ jsxs("span", {
165
185
  className: statCardHeaderVariants(),
166
- children: [/* @__PURE__ */ jsx("span", {
167
- "data-slot": "stat-card-label",
168
- className: cn(statCardLabelVariants(), "block"),
169
- children: /* @__PURE__ */ jsx(Swap, {
170
- text: label,
171
- className: "*:max-w-full",
172
- children: /* @__PURE__ */ jsx("span", {
173
- className: "block truncate",
174
- title: label,
175
- children: label
186
+ children: [
187
+ icon ? /* @__PURE__ */ jsx("span", {
188
+ "data-slot": "stat-card-icon",
189
+ "aria-hidden": "true",
190
+ className: statCardIconVariants(),
191
+ children: icon
192
+ }) : null,
193
+ /* @__PURE__ */ jsx("span", {
194
+ "data-slot": "stat-card-label",
195
+ className: cn(statCardLabelVariants(), "block"),
196
+ children: /* @__PURE__ */ jsx(Swap, {
197
+ text: label,
198
+ className: "*:max-w-full",
199
+ children: /* @__PURE__ */ jsx("span", {
200
+ className: "block truncate",
201
+ title: label,
202
+ children: label
203
+ })
176
204
  })
177
- })
178
- }), change ? /* @__PURE__ */ jsx(Badge, {
179
- shape: "pill",
180
- tone: changeTone(change),
181
- appearance: changeTone(change) === "neutral" ? "outline" : "soft",
182
- className: "tabular-nums",
183
- children: change
184
- }) : null]
205
+ }),
206
+ change ? /* @__PURE__ */ jsx(Badge, {
207
+ shape: "pill",
208
+ tone: BADGE_TONE[meaning],
209
+ appearance: meaning === "neutral" ? "outline" : "soft",
210
+ className: "tabular-nums",
211
+ children: change
212
+ }) : null
213
+ ]
185
214
  }),
186
215
  /* @__PURE__ */ jsx("span", {
187
216
  "data-slot": "stat-card-value",
@@ -211,6 +240,7 @@ const StatCard = forwardRef(function StatCard({ label, value, sub, change, tone,
211
240
  interactive: isSelected !== void 0 || onPress !== void 0 || href !== void 0
212
241
  }), className);
213
242
  const common = {
243
+ ...dataAttributes(rest),
214
244
  "data-slot": "stat-card",
215
245
  "aria-label": rest["aria-label"]
216
246
  };
@@ -244,6 +274,6 @@ const StatCard = forwardRef(function StatCard({ label, value, sub, change, tone,
244
274
  });
245
275
  });
246
276
  //#endregion
247
- export { StatCard, statCardHeaderVariants, statCardLabelVariants, statCardSubVariants, statCardValueVariants, statCardVariants };
277
+ export { StatCard, statCardHeaderVariants, statCardIconVariants, statCardLabelVariants, statCardSubVariants, statCardValueVariants, statCardVariants };
248
278
 
249
279
  //# sourceMappingURL=stat-card.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"stat-card.js","names":["AriaToggleButton","AriaButton","AriaLink"],"sources":["../../src/stat-card/stat-card.tsx"],"sourcesContent":["\"use client\";\n\nimport { cva } from \"class-variance-authority\";\nimport { forwardRef, useState, type ReactNode, type Ref } from \"react\";\nimport {\n Button as AriaButton,\n Link as AriaLink,\n ToggleButton as AriaToggleButton,\n} from \"react-aria-components\";\n\nimport { AnimatedCounter } from \"../animated-counter/animated-counter.js\";\nimport { Badge } from \"../badge/badge.js\";\nimport { cn } from \"../lib/cn.js\";\nimport { SwapText } from \"../lib/swap-text.js\";\n\n/**\n * StatCard: one headline figure with its label and a line of context. A row\n * of them is a dashboard's summary strip.\n *\n * It exists because a summary strip is on almost every overview screen, and\n * without a shared card each screen picks its own label case, number size,\n * colour and height, so the same figure looks different on two pages.\n *\n * The figure is a counter: a number rolls up to its value the first time the\n * card scrolls into view, and when `value` changes the digits that changed roll\n * to their new faces, the way the number moved. A label, a line of context or\n * a change that is replaced rises in while the old copy lifts away. Reduced\n * motion jumps and fades instead. A value that is not a plain number\n * (a date, \"N/A\", a node) is shown as it is.\n *\n * Rules it holds:\n *\n * - The label is ONE line. A long label is cut with an ellipsis and the full\n * text is on hover, so a row of cards keeps its numbers on one baseline\n * instead of one card's label wrapping and pushing its number down.\n * - The number's colour comes from `tone`, a fixed set of meanings, never a\n * colour. `neutral` is the default and is right for any figure without a\n * verdict in it, including every zero.\n * - Cards in a row are the same height (`h-full` in a grid or flex row).\n *\n * A card can be a link (`href`, React Aria Link, so a router provider routes\n * it), a button (`onPress`), or a filter toggle (`isSelected` + `onPress`, a\n * React Aria ToggleButton with `aria-pressed` and one shared selected look).\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 statCardVariants = cva(\n [\n \"flex h-full min-w-0 flex-col p-6 text-start\",\n \"rounded-panel border-(length:--border-width) bg-surface text-foreground shadow-resting\",\n // box-border and font-sans are not boilerplate: the reset in\n // @seekalfred/styles is opt-in, so a consumer may load none.\n \"box-border font-sans\",\n ],\n {\n variants: {\n /** A filter card that is the one applied. */\n selected: {\n true: \"border-primary-strong\",\n false: \"border-border\",\n },\n /** Whether the card is a link or a button (hover and focus states). */\n interactive: {\n true: [\n \"cursor-pointer outline-none\",\n // A border, not a fill: a muted fill under the muted label fails contrast.\n \"data-[hovered]:not-data-[selected]:border-muted-foreground\",\n // outline-solid on the focus variant is load-bearing: outline-none\n // sets the outline style the width utility resolves from.\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 false: \"\",\n },\n },\n defaultVariants: { selected: false, interactive: false },\n },\n);\n\n/** The top row: the label on the left, the change pill on the right. */\nexport const statCardHeaderVariants = cva(\"mb-8 flex min-h-6 items-center justify-between gap-3\");\n\nexport const statCardLabelVariants = cva([\n \"m-0 min-w-0 flex-1 text-sm leading-(--leading-normal) font-normal\",\n \"text-text-secondary\",\n]);\n\nexport const statCardValueVariants = cva(\n // The counter sets its own colour; this makes it follow the tone instead.\n // leading-* AFTER text-*: tailwind-merge treats a later text-* as owning\n // line-height and silently strips a leading-* that precedes it.\n \"m-0 font-display text-h2 leading-none font-semibold tabular-nums break-words **:data-[slot=animated-counter]:text-inherit\",\n {\n variants: {\n /** What the number MEANS. Colour follows from it; it is never picked directly. */\n tone: {\n neutral: \"text-foreground\",\n positive: \"text-success-strong\",\n negative: \"text-destructive-strong\",\n warning: \"text-warning-strong\",\n attention: \"text-primary-strong\",\n },\n },\n defaultVariants: { tone: \"neutral\" },\n },\n);\n\nexport const statCardSubVariants = cva(\n \"m-0 mt-4 text-sm leading-(--leading-normal) text-muted-foreground\",\n);\n\nexport type StatCardTone = \"neutral\" | \"positive\" | \"negative\" | \"warning\" | \"attention\";\n\nexport interface StatCardProps {\n /** What the figure is. One line; cut with an ellipsis, full text on hover. */\n label: string;\n /**\n * The figure itself, already formatted. A plain number (`12400`), or a string\n * made of a number with text around it (`\"$12,400\"`, `\"42 days\"`, `\"98.5%\"`),\n * counts up on first view and rolls to each new value. Anything else is\n * shown as it is.\n */\n value: ReactNode;\n /** One line of context under the figure. A string that changes swaps in place. */\n sub?: ReactNode;\n /**\n * A change shown as a pill beside the label, such as `\"+12.4%\"`. A leading `+`\n * reads green and a leading `-` red; no sign is neutral. A new value swaps in\n * place and the pill follows its width.\n */\n change?: string;\n /**\n * What the figure means: `neutral` (default, and right for every zero),\n * `positive`, `negative`, `warning` (part-way or waiting) or `attention`\n * (a person has to look).\n */\n tone?: StatCardTone;\n /**\n * The card is a filter, and this says whether it is the one applied.\n * Renders a toggle button with `aria-pressed`. Pair with `onPress`.\n */\n isSelected?: boolean;\n /** Renders the card as a link to this URL. */\n href?: string;\n /** Renders the card as a button (or, with `isSelected`, a toggle). */\n onPress?: () => void;\n /** Hover text for the whole card. */\n description?: string;\n /** Anything under the context line. */\n children?: ReactNode;\n /** Layout classes for the card (margins, grid placement). */\n className?: string;\n \"aria-label\"?: string;\n}\n\n/** The number in a piece of text, for the direction a swap moves: a larger one rises from below. */\nconst amountIn = (text: string) => Number(text.replace(/,/g, \"\").match(/-?\\d+(?:\\.\\d+)?/)?.[0] ?? NaN);\n\n/** 1 when the new text holds a number that did not shrink, -1 when it did. Updated during render. */\nfunction useSwapDirection(text: string | undefined) {\n const [shown, setShown] = useState({ text, dir: 1 });\n if (shown.text !== text) {\n setShown({ text, dir: amountIn(text ?? \"\") < amountIn(shown.text ?? \"\") ? -1 : 1 });\n }\n return shown.dir;\n}\n\n/** Copy that is replaced rises in from the side it moved toward and the old copy lifts away. */\nfunction Swap({\n text,\n className,\n children,\n}: {\n text: string;\n className?: string;\n children: ReactNode;\n}) {\n const dir = useSwapDirection(text);\n return (\n <SwapText swapKey={text} dir={dir} className={cn(\"block min-w-0\", className)}>\n {children}\n </SwapText>\n );\n}\n\ninterface Figure {\n prefix: string;\n value: number;\n decimals: number;\n suffix: string;\n}\n\nconst FORMAT: Record<number, Intl.NumberFormat> = {};\nconst format = (decimals: number) =>\n (FORMAT[decimals] ??= new Intl.NumberFormat(\"en-US\", {\n minimumFractionDigits: decimals,\n maximumFractionDigits: decimals,\n }));\n\n/**\n * A number with text around it, or null. Only a figure that the counter would\n * draw back as the very same text qualifies (\"12400\" would come back as\n * \"12,400\", so it is shown as written rather than quietly reformatted).\n */\nfunction figureOf(value: ReactNode): Figure | null {\n if (typeof value === \"number\") {\n if (!Number.isFinite(value)) return null;\n const decimals = Math.min(4, (String(value).split(\".\")[1] ?? \"\").length);\n return { prefix: \"\", value, decimals, suffix: \"\" };\n }\n if (typeof value !== \"string\") return null;\n const match = /^(\\D*?)(-?\\d[\\d,]*(?:\\.\\d+)?)(\\D*)$/.exec(value);\n if (!match) return null;\n const [, prefix = \"\", digits = \"\", suffix = \"\"] = match;\n const decimals = (digits.split(\".\")[1] ?? \"\").length;\n const number = Number(digits.replace(/,/g, \"\"));\n if (!Number.isFinite(number) || `${prefix}${format(decimals).format(number)}${suffix}` !== value) {\n return null;\n }\n return { prefix, value: number, decimals, suffix };\n}\n\nconst changeTone = (change: string) =>\n /^\\+/.test(change) ? \"success\" : /^[-−]/.test(change) ? \"danger\" : \"neutral\";\n\n/**\n * One headline figure.\n *\n * @example\n * <div className=\"grid gap-4 sm:grid-cols-3\">\n * <StatCard label=\"Open\" value=\"$12,400\" sub=\"18 invoices\" change=\"+4.2%\" />\n * <StatCard label=\"Overdue\" value=\"$3,100\" sub=\"4 invoices\" tone=\"negative\" />\n * <StatCard label=\"Collected\" value=\"$9,800\" tone=\"positive\" href=\"/payments\" />\n * </div>\n */\nexport const StatCard = forwardRef<HTMLElement, StatCardProps>(function StatCard(\n { label, value, sub, change, tone, isSelected, href, onPress, description, children, className, ...rest },\n ref,\n) {\n const figure = figureOf(value);\n const body = (\n // The hover text sits on a box that fills the card: React Aria's Button\n // does not pass a `title` through to the element it renders.\n <span className=\"flex min-w-0 flex-1 flex-col\" title={description}>\n <span className={statCardHeaderVariants()}>\n <span data-slot=\"stat-card-label\" className={cn(statCardLabelVariants(), \"block\")}>\n {/* *:max-w-full: the arriving copy is an inline-block, whose shrink-to-fit width is the whole unwrapped label; capped, the label truncates. */}\n <Swap text={label} className=\"*:max-w-full\">\n <span className=\"block truncate\" title={label}>\n {label}\n </span>\n </Swap>\n </span>\n {change ? (\n <Badge\n shape=\"pill\"\n tone={changeTone(change)}\n appearance={changeTone(change) === \"neutral\" ? \"outline\" : \"soft\"}\n className=\"tabular-nums\"\n >\n {change}\n </Badge>\n ) : null}\n </span>\n <span\n data-slot=\"stat-card-value\"\n data-tone={tone ?? \"neutral\"}\n className={cn(statCardValueVariants({ tone }), \"block\")}\n >\n {figure ? (\n <AnimatedCounter\n value={figure.value}\n decimals={figure.decimals}\n prefix={figure.prefix}\n suffix={figure.suffix}\n animateOnView\n />\n ) : (\n value\n )}\n </span>\n {sub === undefined ? null : (\n <span data-slot=\"stat-card-sub\" className={cn(statCardSubVariants(), \"block\")}>\n {typeof sub === \"string\" ? <Swap text={sub}>{sub}</Swap> : sub}\n </span>\n )}\n {children}\n </span>\n );\n const interactive = isSelected !== undefined || onPress !== undefined || href !== undefined;\n const classes = cn(statCardVariants({ selected: isSelected === true, interactive }), className);\n const common = { \"data-slot\": \"stat-card\", \"aria-label\": rest[\"aria-label\"] };\n\n if (isSelected !== undefined) {\n return (\n <AriaToggleButton\n ref={ref as Ref<HTMLButtonElement>}\n {...common}\n isSelected={isSelected}\n onChange={() => onPress?.()}\n className={classes}\n >\n {body}\n </AriaToggleButton>\n );\n }\n if (onPress) {\n return (\n <AriaButton\n ref={ref as Ref<HTMLButtonElement>}\n {...common}\n onPress={onPress}\n className={classes}\n >\n {body}\n </AriaButton>\n );\n }\n if (href !== undefined) {\n return (\n <AriaLink ref={ref as Ref<HTMLAnchorElement>} {...common} href={href} className={classes}>\n {body}\n </AriaLink>\n );\n }\n return (\n <div ref={ref as Ref<HTMLDivElement>} {...common} className={classes}>\n {body}\n </div>\n );\n});\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAgDA,MAAa,mBAAmB,IAC9B;CACE;CACA;CAGA;AACF,GACA;CACE,UAAU;;EAER,UAAU;GACR,MAAM;GACN,OAAO;EACT;;EAEA,aAAa;GACX,MAAM;IACJ;IAEA;IAGA;IACA;IACA;IACA;GACF;GACA,OAAO;EACT;CACF;CACA,iBAAiB;EAAE,UAAU;EAAO,aAAa;CAAM;AACzD,CACF;;AAGA,MAAa,yBAAyB,IAAI,sDAAsD;AAEhG,MAAa,wBAAwB,IAAI,CACvC,qEACA,qBACF,CAAC;AAED,MAAa,wBAAwB,IAInC,6HACA;CACE,UAAU;;AAER,MAAM;EACJ,SAAS;EACT,UAAU;EACV,UAAU;EACV,SAAS;EACT,WAAW;CACb,EACF;CACA,iBAAiB,EAAE,MAAM,UAAU;AACrC,CACF;AAEA,MAAa,sBAAsB,IACjC,mEACF;;AA+CA,MAAM,YAAY,SAAiB,OAAO,KAAK,QAAQ,MAAM,EAAE,CAAC,CAAC,MAAM,iBAAiB,CAAC,GAAG,MAAM,GAAG;;AAGrG,SAAS,iBAAiB,MAA0B;CAClD,MAAM,CAAC,OAAO,YAAY,SAAS;EAAE;EAAM,KAAK;CAAE,CAAC;CACnD,IAAI,MAAM,SAAS,MACjB,SAAS;EAAE;EAAM,KAAK,SAAS,QAAQ,EAAE,IAAI,SAAS,MAAM,QAAQ,EAAE,IAAI,KAAK;CAAE,CAAC;CAEpF,OAAO,MAAM;AACf;;AAGA,SAAS,KAAK,EACZ,MACA,WACA,YAKC;CACD,MAAM,MAAM,iBAAiB,IAAI;CACjC,OACE,oBAAC,UAAD;EAAU,SAAS;EAAW;EAAK,WAAW,GAAG,iBAAiB,SAAS;EACxE;CACO,CAAA;AAEd;AASA,MAAM,SAA4C,CAAC;AACnD,MAAM,UAAU,aACb,OAAO,cAAc,IAAI,KAAK,aAAa,SAAS;CACnD,uBAAuB;CACvB,uBAAuB;AACzB,CAAC;;;;;;AAOH,SAAS,SAAS,OAAiC;CACjD,IAAI,OAAO,UAAU,UAAU;EAC7B,IAAI,CAAC,OAAO,SAAS,KAAK,GAAG,OAAO;EAEpC,OAAO;GAAE,QAAQ;GAAI;GAAO,UADX,KAAK,IAAI,IAAI,OAAO,KAAK,CAAC,CAAC,MAAM,GAAG,CAAC,CAAC,MAAM,GAAA,CAAI,MAC9B;GAAG,QAAQ;EAAG;CACnD;CACA,IAAI,OAAO,UAAU,UAAU,OAAO;CACtC,MAAM,QAAQ,sCAAsC,KAAK,KAAK;CAC9D,IAAI,CAAC,OAAO,OAAO;CACnB,MAAM,GAAG,SAAS,IAAI,SAAS,IAAI,SAAS,MAAM;CAClD,MAAM,YAAY,OAAO,MAAM,GAAG,CAAC,CAAC,MAAM,GAAA,CAAI;CAC9C,MAAM,SAAS,OAAO,OAAO,QAAQ,MAAM,EAAE,CAAC;CAC9C,IAAI,CAAC,OAAO,SAAS,MAAM,KAAK,GAAG,SAAS,OAAO,QAAQ,CAAC,CAAC,OAAO,MAAM,IAAI,aAAa,OACzF,OAAO;CAET,OAAO;EAAE;EAAQ,OAAO;EAAQ;EAAU;CAAO;AACnD;AAEA,MAAM,cAAc,WAClB,MAAM,KAAK,MAAM,IAAI,YAAY,QAAQ,KAAK,MAAM,IAAI,WAAW;;;;;;;;;;;AAYrE,MAAa,WAAW,WAAuC,SAAS,SACtE,EAAE,OAAO,OAAO,KAAK,QAAQ,MAAM,YAAY,MAAM,SAAS,aAAa,UAAU,WAAW,GAAG,QACnG,KACA;CACA,MAAM,SAAS,SAAS,KAAK;CAC7B,MAAM,OAGJ,qBAAC,QAAD;EAAM,WAAU;EAA+B,OAAO;EAAtD,UAAA;GACE,qBAAC,QAAD;IAAM,WAAW,uBAAuB;IAAxC,UAAA,CACE,oBAAC,QAAD;KAAM,aAAU;KAAkB,WAAW,GAAG,sBAAsB,GAAG,OAAO;KAE9E,UAAA,oBAAC,MAAD;MAAM,MAAM;MAAO,WAAU;MAC3B,UAAA,oBAAC,QAAD;OAAM,WAAU;OAAiB,OAAO;OACrC,UAAA;MACG,CAAA;KACF,CAAA;IACF,CAAA,GACL,SACC,oBAAC,OAAD;KACE,OAAM;KACN,MAAM,WAAW,MAAM;KACvB,YAAY,WAAW,MAAM,MAAM,YAAY,YAAY;KAC3D,WAAU;KAET,UAAA;IACI,CAAA,IACL,IACA;;GACN,oBAAC,QAAD;IACE,aAAU;IACV,aAAW,QAAQ;IACnB,WAAW,GAAG,sBAAsB,EAAE,KAAK,CAAC,GAAG,OAAO;IAErD,UAAA,SACC,oBAAC,iBAAD;KACE,OAAO,OAAO;KACd,UAAU,OAAO;KACjB,QAAQ,OAAO;KACf,QAAQ,OAAO;KACf,eAAA;IACD,CAAA,IAED;GAEE,CAAA;GACL,QAAQ,KAAA,IAAY,OACnB,oBAAC,QAAD;IAAM,aAAU;IAAgB,WAAW,GAAG,oBAAoB,GAAG,OAAO;IACzE,UAAA,OAAO,QAAQ,WAAW,oBAAC,MAAD;KAAM,MAAM;KAAM,UAAA;IAAU,CAAA,IAAI;GACvD,CAAA;GAEP;EACG;;CAGR,MAAM,UAAU,GAAG,iBAAiB;EAAE,UAAU,eAAe;EAAM,aADjD,eAAe,KAAA,KAAa,YAAY,KAAA,KAAa,SAAS,KAAA;CACD,CAAC,GAAG,SAAS;CAC9F,MAAM,SAAS;EAAE,aAAa;EAAa,cAAc,KAAK;CAAc;CAE5E,IAAI,eAAe,KAAA,GACjB,OACE,oBAACA,cAAD;EACO;EACL,GAAI;EACQ;EACZ,gBAAgB,UAAU;EAC1B,WAAW;EAEV,UAAA;CACe,CAAA;CAGtB,IAAI,SACF,OACE,oBAACC,QAAD;EACO;EACL,GAAI;EACK;EACT,WAAW;EAEV,UAAA;CACS,CAAA;CAGhB,IAAI,SAAS,KAAA,GACX,OACE,oBAACC,MAAD;EAAe;EAA+B,GAAI;EAAc;EAAM,WAAW;EAC9E,UAAA;CACO,CAAA;CAGd,OACE,oBAAC,OAAD;EAAU;EAA4B,GAAI;EAAQ,WAAW;EAC1D,UAAA;CACE,CAAA;AAET,CAAC"}
1
+ {"version":3,"file":"stat-card.js","names":["AriaToggleButton","AriaButton","AriaLink"],"sources":["../../src/stat-card/stat-card.tsx"],"sourcesContent":["\"use client\";\n\nimport { cva } from \"class-variance-authority\";\nimport { forwardRef, useState, type ReactNode, type Ref } from \"react\";\nimport {\n Button as AriaButton,\n Link as AriaLink,\n ToggleButton as AriaToggleButton,\n} from \"react-aria-components\";\n\nimport { AnimatedCounter } from \"../animated-counter/animated-counter.js\";\nimport { Badge } from \"../badge/badge.js\";\nimport { cn } from \"../lib/cn.js\";\nimport { SwapText } from \"../lib/swap-text.js\";\n\n/**\n * StatCard: one headline figure with its label and a line of context. A row\n * of them is a dashboard's summary strip.\n *\n * It exists because a summary strip is on almost every overview screen, and\n * without a shared card each screen picks its own label case, number size,\n * colour and height, so the same figure looks different on two pages.\n *\n * The figure is a counter: a number rolls up to its value the first time the\n * card scrolls into view, and when `value` changes the digits that changed roll\n * to their new faces, the way the number moved. A label, a line of context or\n * a change that is replaced rises in while the old copy lifts away. Reduced\n * motion jumps and fades instead. A value that is not a plain number\n * (a date, \"N/A\", a node) is shown as it is.\n *\n * Rules it holds:\n *\n * - The label is ONE line. A long label is cut with an ellipsis and the full\n * text is on hover, so a row of cards keeps its numbers on one baseline\n * instead of one card's label wrapping and pushing its number down.\n * - The number's colour comes from `tone`, a fixed set of meanings, never a\n * colour. `neutral` is the default and is right for any figure without a\n * verdict in it, including every zero.\n * - Cards in a row are the same height (`h-full` in a grid or flex row).\n * - The change pill's colour is what the change MEANS. By default a rise is\n * good; `trend=\"inverse\"` reads a rise as bad (bounce rate, cost, keywords\n * that dropped out), `trend=\"neutral\"` gives no verdict, and `changeTone`\n * names the meaning outright when the text carries no sign.\n *\n * A card can be a link (`href`, React Aria Link, so a router provider routes\n * it), a button (`onPress`), or a filter toggle (`isSelected` + `onPress`, a\n * React Aria ToggleButton with `aria-pressed` and one shared selected look).\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 statCardVariants = cva(\n [\n \"flex h-full min-w-0 flex-col p-6 text-start\",\n \"rounded-panel border-(length:--border-width) bg-surface text-foreground shadow-resting\",\n // box-border and font-sans are not boilerplate: the reset in\n // @seekalfred/styles is opt-in, so a consumer may load none.\n \"box-border font-sans\",\n ],\n {\n variants: {\n /** A filter card that is the one applied. */\n selected: {\n true: \"border-primary-strong\",\n false: \"border-border\",\n },\n /** Whether the card is a link or a button (hover and focus states). */\n interactive: {\n true: [\n \"cursor-pointer outline-none\",\n // A border, not a fill: a muted fill under the muted label fails contrast.\n \"data-[hovered]:not-data-[selected]:border-muted-foreground\",\n // outline-solid on the focus variant is load-bearing: outline-none\n // sets the outline style the width utility resolves from.\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 false: \"\",\n },\n },\n defaultVariants: { selected: false, interactive: false },\n },\n);\n\n/** The top row: the label on the left, the change pill on the right. */\nexport const statCardHeaderVariants = cva(\"mb-8 flex min-h-6 items-center justify-between gap-3\");\n\nexport const statCardLabelVariants = cva([\n \"m-0 min-w-0 flex-1 text-sm leading-(--leading-normal) font-normal\",\n \"text-text-secondary\",\n]);\n\nexport const statCardValueVariants = cva(\n // The counter sets its own colour; this makes it follow the tone instead.\n // leading-* AFTER text-*: tailwind-merge treats a later text-* as owning\n // line-height and silently strips a leading-* that precedes it.\n \"m-0 font-display text-h2 leading-none font-semibold tabular-nums break-words **:data-[slot=animated-counter]:text-inherit\",\n {\n variants: {\n /** What the number MEANS. Colour follows from it; it is never picked directly. */\n tone: {\n neutral: \"text-foreground\",\n positive: \"text-success-strong\",\n negative: \"text-destructive-strong\",\n warning: \"text-warning-strong\",\n attention: \"text-primary-strong\",\n },\n },\n defaultVariants: { tone: \"neutral\" },\n },\n);\n\n/** An optional icon before the label. Decorative: the label names the figure. */\nexport const statCardIconVariants = cva(\n \"flex shrink-0 items-center text-muted-foreground [&_svg]:size-4 [&_svg]:shrink-0\",\n);\n\nexport const statCardSubVariants = cva(\n \"m-0 mt-4 text-sm leading-(--leading-normal) text-muted-foreground\",\n);\n\nexport type StatCardTone = \"neutral\" | \"positive\" | \"negative\" | \"warning\" | \"attention\";\n\n/** Which way is good for this figure: up (`normal`), down (`inverse`), or neither (`neutral`). */\nexport type StatCardTrend = \"normal\" | \"inverse\" | \"neutral\";\n\n/** What a change means, whatever its sign. */\nexport type StatCardChangeTone = \"positive\" | \"negative\" | \"neutral\";\n\nexport interface StatCardProps {\n /** What the figure is. One line; cut with an ellipsis, full text on hover. */\n label: string;\n /**\n * The figure itself, already formatted. A plain number (`12400`), or a string\n * made of a number with text around it (`\"$12,400\"`, `\"42 days\"`, `\"98.5%\"`),\n * counts up on first view and rolls to each new value. Anything else is\n * shown as it is.\n */\n value: ReactNode;\n /** One line of context under the figure. A string that changes swaps in place. */\n sub?: ReactNode;\n /**\n * A change shown as a pill beside the label, such as `\"+12.4%\"`. A leading `+`\n * reads green and a leading `-` red; no sign is neutral. A new value swaps in\n * place and the pill follows its width.\n */\n change?: string;\n /**\n * Which direction of `change` is good. `normal` (default): a leading `+`\n * reads green and a leading `-` red. `inverse`: a rise reads red and a fall\n * green, for a figure where more is worse (bounce rate, cost per click,\n * keywords that fell out of the top 100). `neutral`: no colour either way.\n * @default \"normal\"\n */\n trend?: StatCardTrend;\n /**\n * The meaning of `change`, named outright. Wins over `trend` and the sign;\n * use it when the text has no `+` or `-` (`\"▲4\"`) or the verdict comes from\n * elsewhere. It colours only the change pill, never the value (that is\n * `tone`).\n */\n changeTone?: StatCardChangeTone;\n /**\n * An icon before the label. Decorative (hidden from assistive technology).\n * An SVG is drawn at 16px, whatever size it is given. A falsy value\n * (`false`, `null`, `\"\"`) renders nothing, so `icon={cond && <Icon />}` works.\n */\n icon?: ReactNode;\n /**\n * What the figure means: `neutral` (default, and right for every zero),\n * `positive`, `negative`, `warning` (part-way or waiting) or `attention`\n * (a person has to look).\n */\n tone?: StatCardTone;\n /**\n * The card is a filter, and this says whether it is the one applied.\n * Renders a toggle button with `aria-pressed`. Pair with `onPress`.\n */\n isSelected?: boolean;\n /** Renders the card as a link to this URL. */\n href?: string;\n /** Renders the card as a button (or, with `isSelected`, a toggle). */\n onPress?: () => void;\n /** Hover text for the whole card. */\n description?: string;\n /** Anything under the context line. */\n children?: ReactNode;\n /** Layout classes for the card (margins, grid placement). */\n className?: string;\n \"aria-label\"?: string;\n /** `data-*` attributes land on the card's root (test ids, analytics hooks). `data-slot` stays the card's own. */\n [dataAttribute: `data-${string}`]: string | number | boolean | undefined;\n}\n\n/** The number in a piece of text, for the direction a swap moves: a larger one rises from below. */\nconst amountIn = (text: string) => Number(text.replace(/,/g, \"\").match(/-?\\d+(?:\\.\\d+)?/)?.[0] ?? NaN);\n\n/** 1 when the new text holds a number that did not shrink, -1 when it did. Updated during render. */\nfunction useSwapDirection(text: string | undefined) {\n const [shown, setShown] = useState({ text, dir: 1 });\n if (shown.text !== text) {\n setShown({ text, dir: amountIn(text ?? \"\") < amountIn(shown.text ?? \"\") ? -1 : 1 });\n }\n return shown.dir;\n}\n\n/** Copy that is replaced rises in from the side it moved toward and the old copy lifts away. */\nfunction Swap({\n text,\n className,\n children,\n}: {\n text: string;\n className?: string;\n children: ReactNode;\n}) {\n const dir = useSwapDirection(text);\n return (\n <SwapText swapKey={text} dir={dir} className={cn(\"block min-w-0\", className)}>\n {children}\n </SwapText>\n );\n}\n\ninterface Figure {\n prefix: string;\n value: number;\n decimals: number;\n suffix: string;\n}\n\nconst FORMAT: Record<number, Intl.NumberFormat> = {};\nconst format = (decimals: number) =>\n (FORMAT[decimals] ??= new Intl.NumberFormat(\"en-US\", {\n minimumFractionDigits: decimals,\n maximumFractionDigits: decimals,\n }));\n\n/**\n * A number with text around it, or null. Only a figure that the counter would\n * draw back as the very same text qualifies (\"12400\" would come back as\n * \"12,400\", so it is shown as written rather than quietly reformatted).\n */\nfunction figureOf(value: ReactNode): Figure | null {\n if (typeof value === \"number\") {\n if (!Number.isFinite(value)) return null;\n const decimals = Math.min(4, (String(value).split(\".\")[1] ?? \"\").length);\n return { prefix: \"\", value, decimals, suffix: \"\" };\n }\n if (typeof value !== \"string\") return null;\n const match = /^(\\D*?)(-?\\d[\\d,]*(?:\\.\\d+)?)(\\D*)$/.exec(value);\n if (!match) return null;\n const [, prefix = \"\", digits = \"\", suffix = \"\"] = match;\n const decimals = (digits.split(\".\")[1] ?? \"\").length;\n const number = Number(digits.replace(/,/g, \"\"));\n if (!Number.isFinite(number) || `${prefix}${format(decimals).format(number)}${suffix}` !== value) {\n return null;\n }\n return { prefix, value: number, decimals, suffix };\n}\n\nconst signOf = (change: string) => (/^\\+/.test(change) ? 1 : /^[-−]/.test(change) ? -1 : 0);\n\n/** What a change means: the explicit tone, else its sign read the way `trend` says. */\nfunction changeMeaning(\n change: string,\n trend: StatCardTrend,\n explicit: StatCardChangeTone | undefined,\n): StatCardChangeTone {\n if (explicit) return explicit;\n const sign = trend === \"neutral\" ? 0 : signOf(change) * (trend === \"inverse\" ? -1 : 1);\n return sign > 0 ? \"positive\" : sign < 0 ? \"negative\" : \"neutral\";\n}\n\nconst BADGE_TONE = { positive: \"success\", negative: \"danger\", neutral: \"neutral\" } as const;\n\n/** Only the `data-*` attributes, so nothing else in the rest object reaches the DOM. */\nconst dataAttributes = (rest: Record<string, unknown>) =>\n Object.fromEntries(Object.entries(rest).filter(([key]) => key.startsWith(\"data-\")));\n\n/**\n * One headline figure.\n *\n * @example\n * <div className=\"grid gap-4 sm:grid-cols-3\">\n * <StatCard label=\"Open\" value=\"$12,400\" sub=\"18 invoices\" change=\"+4.2%\" />\n * <StatCard label=\"Overdue\" value=\"$3,100\" sub=\"4 invoices\" tone=\"negative\" />\n * <StatCard label=\"Collected\" value=\"$9,800\" tone=\"positive\" href=\"/payments\" />\n * </div>\n */\nexport const StatCard = forwardRef<HTMLElement, StatCardProps>(function StatCard(\n {\n label,\n value,\n sub,\n change,\n trend = \"normal\",\n changeTone,\n icon,\n tone,\n isSelected,\n href,\n onPress,\n description,\n children,\n className,\n ...rest\n },\n ref,\n) {\n const figure = figureOf(value);\n const meaning = change ? changeMeaning(change, trend, changeTone) : \"neutral\";\n const body = (\n // The hover text sits on a box that fills the card: React Aria's Button\n // does not pass a `title` through to the element it renders.\n <span className=\"flex min-w-0 flex-1 flex-col\" title={description}>\n <span className={statCardHeaderVariants()}>\n {icon ? (\n <span data-slot=\"stat-card-icon\" aria-hidden=\"true\" className={statCardIconVariants()}>\n {icon}\n </span>\n ) : null}\n <span data-slot=\"stat-card-label\" className={cn(statCardLabelVariants(), \"block\")}>\n {/* *:max-w-full: the arriving copy is an inline-block, whose shrink-to-fit width is the whole unwrapped label; capped, the label truncates. */}\n <Swap text={label} className=\"*:max-w-full\">\n <span className=\"block truncate\" title={label}>\n {label}\n </span>\n </Swap>\n </span>\n {change ? (\n <Badge\n shape=\"pill\"\n tone={BADGE_TONE[meaning]}\n appearance={meaning === \"neutral\" ? \"outline\" : \"soft\"}\n className=\"tabular-nums\"\n >\n {change}\n </Badge>\n ) : null}\n </span>\n <span\n data-slot=\"stat-card-value\"\n data-tone={tone ?? \"neutral\"}\n className={cn(statCardValueVariants({ tone }), \"block\")}\n >\n {figure ? (\n <AnimatedCounter\n value={figure.value}\n decimals={figure.decimals}\n prefix={figure.prefix}\n suffix={figure.suffix}\n animateOnView\n />\n ) : (\n value\n )}\n </span>\n {sub === undefined ? null : (\n <span data-slot=\"stat-card-sub\" className={cn(statCardSubVariants(), \"block\")}>\n {typeof sub === \"string\" ? <Swap text={sub}>{sub}</Swap> : sub}\n </span>\n )}\n {children}\n </span>\n );\n const interactive = isSelected !== undefined || onPress !== undefined || href !== undefined;\n const classes = cn(statCardVariants({ selected: isSelected === true, interactive }), className);\n const common = {\n ...dataAttributes(rest),\n \"data-slot\": \"stat-card\",\n \"aria-label\": rest[\"aria-label\"],\n };\n\n if (isSelected !== undefined) {\n return (\n <AriaToggleButton\n ref={ref as Ref<HTMLButtonElement>}\n {...common}\n isSelected={isSelected}\n onChange={() => onPress?.()}\n className={classes}\n >\n {body}\n </AriaToggleButton>\n );\n }\n if (onPress) {\n return (\n <AriaButton\n ref={ref as Ref<HTMLButtonElement>}\n {...common}\n onPress={onPress}\n className={classes}\n >\n {body}\n </AriaButton>\n );\n }\n if (href !== undefined) {\n return (\n <AriaLink ref={ref as Ref<HTMLAnchorElement>} {...common} href={href} className={classes}>\n {body}\n </AriaLink>\n );\n }\n return (\n <div ref={ref as Ref<HTMLDivElement>} {...common} className={classes}>\n {body}\n </div>\n );\n});\n"],"mappings":"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"}