@seekalfred/primitives 0.7.4 → 0.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (87) hide show
  1. package/README.md +255 -3
  2. package/dist/alert/alert.d.ts +2 -2
  3. package/dist/alert/alert.js +1 -1
  4. package/dist/alert/alert.js.map +1 -1
  5. package/dist/alert-dialog/alert-dialog.js +1 -1
  6. package/dist/alert-dialog/alert-dialog.js.map +1 -1
  7. package/dist/avatar/avatar.d.ts +1 -1
  8. package/dist/badge/badge.d.ts +16 -1
  9. package/dist/badge/badge.js +12 -5
  10. package/dist/badge/badge.js.map +1 -1
  11. package/dist/button/button.d.ts +24 -2
  12. package/dist/button/button.js +62 -13
  13. package/dist/button/button.js.map +1 -1
  14. package/dist/card/card.js +1 -1
  15. package/dist/card/card.js.map +1 -1
  16. package/dist/checkbox/checkbox.d.ts +25 -1
  17. package/dist/checkbox/checkbox.js +103 -28
  18. package/dist/checkbox/checkbox.js.map +1 -1
  19. package/dist/checkbox/index.d.ts +2 -2
  20. package/dist/checkbox/index.js +2 -2
  21. package/dist/code-block/code-block.d.ts +45 -0
  22. package/dist/code-block/code-block.js +74 -0
  23. package/dist/code-block/code-block.js.map +1 -0
  24. package/dist/code-block/index.d.ts +2 -0
  25. package/dist/code-block/index.js +2 -0
  26. package/dist/command-palette/command-palette.d.ts +128 -0
  27. package/dist/command-palette/command-palette.js +371 -0
  28. package/dist/command-palette/command-palette.js.map +1 -0
  29. package/dist/command-palette/index.d.ts +2 -0
  30. package/dist/command-palette/index.js +2 -0
  31. package/dist/dialog/dialog.js +1 -1
  32. package/dist/dialog/dialog.js.map +1 -1
  33. package/dist/empty-state/empty-state.d.ts +17 -0
  34. package/dist/empty-state/empty-state.js +9 -7
  35. package/dist/empty-state/empty-state.js.map +1 -1
  36. package/dist/index.d.ts +8 -4
  37. package/dist/index.js +12 -8
  38. package/dist/info-tooltip/index.d.ts +2 -0
  39. package/dist/info-tooltip/index.js +2 -0
  40. package/dist/info-tooltip/info-tooltip.d.ts +35 -0
  41. package/dist/info-tooltip/info-tooltip.js +49 -0
  42. package/dist/info-tooltip/info-tooltip.js.map +1 -0
  43. package/dist/input/input.js +1 -1
  44. package/dist/lib/selection-control.js +32 -1
  45. package/dist/lib/selection-control.js.map +1 -1
  46. package/dist/line-chart/line-chart.d.ts +2 -1
  47. package/dist/line-chart/line-chart.js +2 -2
  48. package/dist/line-chart/line-chart.js.map +1 -1
  49. package/dist/mention-input/mention-input.js +1 -1
  50. package/dist/money-input/money-input.js +1 -1
  51. package/dist/multi-select/multi-select.js +1 -1
  52. package/dist/number-field/number-field.js +1 -1
  53. package/dist/password-field/password-field.js +1 -1
  54. package/dist/password-strength/password-strength.js +1 -1
  55. package/dist/phone-input/phone-input.js +1 -1
  56. package/dist/popover/popover.js +1 -1
  57. package/dist/popover/popover.js.map +1 -1
  58. package/dist/progress/progress.d.ts +3 -3
  59. package/dist/radio-group/radio-group.d.ts +12 -3
  60. package/dist/radio-group/radio-group.js +62 -52
  61. package/dist/radio-group/radio-group.js.map +1 -1
  62. package/dist/rich-text-editor/rich-text-editor.js +1 -1
  63. package/dist/scroll-area/scroll-area.d.ts +1 -1
  64. package/dist/search-field/search-field.js +1 -1
  65. package/dist/select/select.d.ts +1 -1
  66. package/dist/select/select.js +1 -1
  67. package/dist/separator/separator.d.ts +1 -1
  68. package/dist/shortcut-recorder/shortcut-recorder.js +1 -1
  69. package/dist/stat-card/stat-card.d.ts +2 -2
  70. package/dist/styles.css +1 -1
  71. package/dist/table/index.d.ts +2 -2
  72. package/dist/table/index.js +2 -2
  73. package/dist/table/table.d.ts +99 -1
  74. package/dist/table/table.js +123 -23
  75. package/dist/table/table.js.map +1 -1
  76. package/dist/tabs/tabs.d.ts +4 -2
  77. package/dist/tabs/tabs.js +7 -5
  78. package/dist/tabs/tabs.js.map +1 -1
  79. package/dist/tag-input/tag-input.js +1 -1
  80. package/dist/textarea/textarea.js +1 -1
  81. package/dist/toast/toast.d.ts +2 -2
  82. package/dist/toggle-button/index.d.ts +2 -0
  83. package/dist/toggle-button/index.js +2 -0
  84. package/dist/toggle-button/toggle-button.d.ts +93 -0
  85. package/dist/toggle-button/toggle-button.js +199 -0
  86. package/dist/toggle-button/toggle-button.js.map +1 -0
  87. package/package.json +19 -3
@@ -1 +1 @@
1
- {"version":3,"file":"tabs.js","names":["Tabs","AriaTabs","TabList","AriaTabList","Tab","AriaTab","TabPanel","AriaTabPanel"],"sources":["../../src/tabs/tabs.tsx"],"sourcesContent":["\"use client\";\n\nimport { cva } from \"class-variance-authority\";\nimport { forwardRef, useEffect, useMemo, useState } from \"react\";\nimport {\n Tab as AriaTab,\n TabList as AriaTabList,\n TabPanel as AriaTabPanel,\n Tabs as AriaTabs,\n type TabListProps as AriaTabListProps,\n type TabPanelProps as AriaTabPanelProps,\n type TabProps as AriaTabProps,\n type TabsProps as AriaTabsProps,\n} from \"react-aria-components\";\n\nimport { cn } from \"../lib/cn.js\";\nimport { mergeRefs } from \"../lib/merge-refs.js\";\n\n/**\n * Tabs, ported from the legacy shadcn/Radix component.\n *\n * The largest legacy surface in this migration: 18 files, `TabsTrigger`\n * 51, `TabsContent` 31, `Tabs` 17, `TabsList` 17. Props: `value` 88,\n * `className` 76, `defaultValue` 12, `onValueChange` 10, `disabled` 6.\n *\n * The rename is mechanical but it touches every one of those:\n * `value` -> `selectedKey`, `defaultValue` -> `defaultSelectedKey`,\n * `onValueChange` -> `onSelectionChange`, and the `value` on each trigger\n * and panel becomes `id`. React Aria pairs a Tab to its TabPanel by that\n * id, exactly as Radix paired them by value.\n *\n * THE SELECTED TAB GAINS A BORDER, and that is a contrast fix rather than\n * a decoration. The legacy marks the selected tab with `bg-background` on\n * a `bg-muted` list -- 1.13:1, which is not a boundary anybody can see --\n * and leans on `shadow-sm` to carry the rest. Under WCAG 1.4.11 the thing\n * telling you which tab is active has to clear 3:1. `border-border` on\n * `muted` is 4.65:1 and is already in the contrast contract.\n *\n * Two bracket values are gone because check:hardcoding rejects them:\n * `p-[3px]` on the list and `h-[calc(100%-1px)]` on the trigger. The list\n * is `p-1` and the trigger sizes to the list rather than to a value one\n * pixel short of it.\n *\n * ONE BEHAVIOUR DIFFERS AND IS WORTH KNOWING. Radix keeps every mounted\n * TabsContent in the DOM and hides the inactive ones; React Aria renders\n * ONLY the selected panel. A panel that holds uncommitted form state, a\n * running video or a scroll position will lose it on tab change. React\n * Aria's `shouldForceMount` on TabPanel restores the old behaviour where\n * a call site needs it.\n */\nexport const tabsVariants = cva([\n \"flex flex-col gap-2 box-border font-sans\",\n // May be narrower than its tabs: in a flex row (a toolbar beside a search\n // field) it shrinks to the space it has instead of pushing the page wider.\n \"min-w-0 max-w-full\",\n]);\n\nexport const tabListVariants = cva([\n \"inline-flex w-fit items-center justify-center gap-1\",\n // p-1, not the legacy p-[3px]: a bracket value check:hardcoding rejects.\n \"rounded-control p-1\",\n \"bg-muted text-muted-foreground\",\n // The selection highlight that glides between tabs (styles.css).\n \"tab-highlight\",\n \"box-border\",\n\n // A ROW WIDER THAN ITS BOX SCROLLS INSIDE IT. Capped at the parent's\n // width; while its tabs are wider than that, TabList sets\n // data-overflowing and the row scrolls sideways on its own. A row that\n // fits never carries the attribute, so nothing about it changes.\n \"max-w-full\",\n // How wide the fade is, and where it starts measured from the far edge.\n \"[--fade-tabs:--spacing(6)] [--fade-tabs-from:calc(100%-var(--fade-tabs))]\",\n \"data-[overflowing]:overflow-x-auto data-[overflowing]:overscroll-x-contain\",\n // justify-center spills centred overflow out of BOTH sides, and a scroll\n // box cannot reach the spill on its start side: the first tab would be\n // cut off for good. Scrolling rows start at the start edge.\n \"data-[overflowing]:justify-start\",\n // The cue that there is more: the side with tabs out of view fades out.\n // Masks are physical, so the start and end swap sides in RTL.\n \"ltr:data-[fade-start]:mask-l-from-(--fade-tabs-from) rtl:data-[fade-start]:mask-r-from-(--fade-tabs-from)\",\n \"ltr:data-[fade-end]:mask-r-from-(--fade-tabs-from) rtl:data-[fade-end]:mask-l-from-(--fade-tabs-from)\",\n]);\n\n/**\n * Sets data-overflowing, data-fade-start and data-fade-end on a tab row\n * while its tabs are wider than its box, and on the first measurement that\n * finds it too wide, scrolls the selected tab into view INSIDE the row\n * (never the page, so a row below the fold does not drag the page to it).\n *\n * The attributes are written straight onto the element, so a scroll or a\n * resize re-renders nothing. Arrow keys need no help: React Aria keeps the\n * focused tab in view in the row that scrolls.\n */\nconst FADE_PX = 24;\n\nfunction useScrollWhenTooWide() {\n const [row, setRow] = useState<HTMLDivElement | null>(null);\n useEffect(() => {\n if (!row || typeof ResizeObserver === \"undefined\") return;\n let revealed = false;\n const measure = () => {\n const overflowing = row.scrollWidth > row.clientWidth + 1;\n // Distance from the start edge; scrollLeft runs negative in RTL.\n const scrolled = Math.abs(row.scrollLeft);\n row.toggleAttribute(\"data-overflowing\", overflowing);\n row.toggleAttribute(\"data-fade-start\", overflowing && scrolled > 1);\n row.toggleAttribute(\n \"data-fade-end\",\n overflowing && scrolled + row.clientWidth < row.scrollWidth - 1,\n );\n if (!overflowing || revealed) return;\n revealed = true;\n const selected = row.querySelector<HTMLElement>(\"[data-selected]\");\n if (!selected) return;\n // Clear of the fade: --spacing(6), the width the mask draws.\n const fade = FADE_PX;\n const box = row.getBoundingClientRect();\n const tab = selected.getBoundingClientRect();\n if (tab.left < box.left + fade) row.scrollLeft -= box.left + fade - tab.left;\n else if (tab.right > box.right - fade) row.scrollLeft += tab.right - (box.right - fade);\n // The scroll event lands a frame later; the fades are right on first paint.\n measure();\n };\n // The row's box changes with its parent; a tab's width changes when its\n // label does (a count that loads).\n const resized = new ResizeObserver(measure);\n const watch = () => {\n resized.disconnect();\n resized.observe(row);\n for (const tab of Array.from(row.children)) resized.observe(tab);\n };\n const added = new MutationObserver(() => {\n watch();\n measure();\n });\n watch();\n added.observe(row, { childList: true });\n row.addEventListener(\"scroll\", measure, { passive: true });\n measure();\n return () => {\n resized.disconnect();\n added.disconnect();\n row.removeEventListener(\"scroll\", measure);\n };\n }, [row]);\n return setRow;\n}\n\n/**\n * Places the selection highlight (see tab-highlight in styles.css) on the selected\n * tab and keeps it there as tabs resize, and tells the tabs root which way the\n * selection moved (`--tab-dir`) so the next panel arrives from that side. All of it\n * is written straight onto the elements, so no render happens. The first placement\n * has no transition; `data-hl-ready` turns it on two frames later.\n */\nfunction useTabHighlight() {\n const [row, setRow] = useState<HTMLDivElement | null>(null);\n useEffect(() => {\n if (!row || typeof ResizeObserver === \"undefined\") return;\n let previous: HTMLElement | null = null;\n let frames = [0, 0];\n const place = () => {\n const selected = row.querySelector<HTMLElement>('[role=\"tab\"][data-selected]');\n if (!selected) {\n row.removeAttribute(\"data-hl\");\n previous = null;\n return;\n }\n row.style.setProperty(\"--hl-x\", `${selected.offsetLeft}px`);\n row.style.setProperty(\"--hl-y\", `${selected.offsetTop}px`);\n row.style.setProperty(\"--hl-w\", `${selected.offsetWidth}px`);\n row.style.setProperty(\"--hl-h\", `${selected.offsetHeight}px`);\n row.setAttribute(\"data-hl\", \"\");\n if (previous && previous !== selected && previous.isConnected) {\n const all = Array.from(row.querySelectorAll('[role=\"tab\"]'));\n const forward = all.indexOf(selected) > all.indexOf(previous);\n // The row reads right to left in RTL, so the same order change is the other way.\n const flip = getComputedStyle(row).direction === \"rtl\";\n const root = row.closest<HTMLElement>('[data-slot=\"tabs\"]');\n root?.style.setProperty(\"--tab-dir\", String(forward !== flip ? 1 : -1));\n root?.setAttribute(\"data-tab-changed\", \"\");\n }\n previous = selected;\n };\n const resized = new ResizeObserver(place);\n const watch = () => {\n resized.disconnect();\n resized.observe(row);\n for (const tab of Array.from(row.children)) resized.observe(tab);\n };\n const changed = new MutationObserver((records) => {\n if (records.some((record) => record.type === \"childList\")) watch();\n place();\n });\n watch();\n changed.observe(row, { attributes: true, attributeFilter: [\"data-selected\"], childList: true, subtree: true });\n place();\n frames[0] = requestAnimationFrame(() => {\n frames[1] = requestAnimationFrame(() => row.setAttribute(\"data-hl-ready\", \"\"));\n });\n return () => {\n resized.disconnect();\n changed.disconnect();\n frames.forEach(cancelAnimationFrame);\n frames = [0, 0];\n };\n }, [row]);\n return setRow;\n}\n\nexport const tabVariants = cva([\n \"inline-flex flex-1 items-center justify-center gap-1.5 whitespace-nowrap\",\n \"rounded-md px-2 py-1\",\n \"text-sm leading-(--leading-normal) font-medium text-foreground\",\n \"cursor-pointer select-none box-border\",\n // A transparent border at rest so selecting one does not move the text.\n \"border-(length:--border-width) border-transparent\",\n \"motion-safe:transition-colors motion-safe:duration-(--duration-fast) motion-safe:ease-standard\",\n\n // The selected tab. bg-background on a muted list is 1.13:1 -- invisible\n // as a boundary -- so the border is what satisfies WCAG 1.4.11 at\n // 4.65:1. The fill and the shadow are the polish on top of it.\n // Until the row has placed its gliding highlight (data-hl), the tab paints\n // its own fill so there is never a frame without a boundary.\n \"not-in-data-[hl]:data-[selected]:bg-surface not-in-data-[hl]:data-[selected]:border-border\",\n \"not-in-data-[hl]:data-[selected]:shadow-surface\",\n\n // outline-solid is load-bearing: outline-none sets --tw-outline-style to\n // none and the width utility resolves its style from that same variable,\n // so a focus rule can apply a width and a colour and paint nothing.\n \"outline-none\",\n \"data-[focus-visible]:outline-solid\",\n \"data-[focus-visible]:outline-(length:--focus-ring-width)\",\n \"data-[focus-visible]:outline-offset-(--focus-ring-offset)\",\n \"data-[focus-visible]:outline-(--color-ring)\",\n\n \"data-[disabled]:cursor-not-allowed data-[disabled]:opacity-(--opacity-disabled)\",\n \"[&_svg]:pointer-events-none [&_svg]:shrink-0\",\n \"[&_svg:not([class*='size-'])]:size-(--icon-sm)\",\n]);\n\nexport const tabPanelVariants = cva([\n \"flex-1 box-border font-sans text-foreground\",\n // Arrives from the side the new tab sits on, once the selection has changed.\n \"tab-panel-motion\",\n // React Aria gives the panel tabindex=0 when it holds no focusable\n // child, so it can take a ring it does not want at rest.\n \"outline-none\",\n \"data-[focus-visible]:outline-solid\",\n \"data-[focus-visible]:outline-(length:--focus-ring-width)\",\n \"data-[focus-visible]:outline-offset-(--focus-ring-offset)\",\n \"data-[focus-visible]:outline-(--color-ring)\",\n]);\n\n/**\n * A set of panels, one visible at a time.\n *\n * @example\n * <Tabs defaultSelectedKey=\"overview\">\n * <TabList aria-label=\"Sections\">\n * <Tab id=\"overview\">Overview</Tab>\n * <Tab id=\"activity\">Activity</Tab>\n * </TabList>\n * <TabPanel id=\"overview\">...</TabPanel>\n * <TabPanel id=\"activity\">...</TabPanel>\n * </Tabs>\n */\nexport interface TabsProps extends Omit<AriaTabsProps, \"className\" | \"style\"> {\n /** Extra classes. Merged last, so they win conflicts with ours. */\n className?: string;\n}\n\nexport const Tabs = forwardRef<HTMLDivElement, TabsProps>(function Tabs(\n { className, ...props },\n ref,\n) {\n return (\n <AriaTabs ref={ref} {...props} data-slot=\"tabs\" className={cn(tabsVariants(), className)} />\n );\n});\n\n/**\n * The row of tabs.\n *\n * `aria-label` is worth passing: a tablist is a landmark-ish grouping and\n * a page with two of them is ambiguous without one.\n */\nexport interface TabListProps extends Omit<AriaTabListProps<object>, \"className\" | \"style\"> {\n className?: string;\n}\n\nexport const TabList = forwardRef<HTMLDivElement, TabListProps>(function TabList(\n { className, ...props },\n ref,\n) {\n const watch = useScrollWhenTooWide();\n const glide = useTabHighlight();\n // Memoised: a new callback every render would detach and reattach the row.\n const merged = useMemo(() => mergeRefs(ref, watch, glide), [ref, watch, glide]);\n return (\n <AriaTabList\n ref={merged}\n {...props}\n data-slot=\"tab-list\"\n className={cn(tabListVariants(), className)}\n />\n );\n});\n\n/** One tab. Its `id` pairs it with the panel of the same `id`. */\nexport interface TabProps extends Omit<AriaTabProps, \"className\" | \"style\"> {\n className?: string;\n}\n\nexport const Tab = forwardRef<HTMLDivElement, TabProps>(function Tab({ className, ...props }, ref) {\n return <AriaTab ref={ref} {...props} data-slot=\"tab\" className={cn(tabVariants(), className)} />;\n});\n\n/** One panel. Its `id` pairs it with the tab of the same `id`. */\nexport interface TabPanelProps extends Omit<AriaTabPanelProps, \"className\" | \"style\"> {\n className?: string;\n}\n\nexport const TabPanel = forwardRef<HTMLDivElement, TabPanelProps>(function TabPanel(\n { className, ...props },\n ref,\n) {\n return (\n <AriaTabPanel\n ref={ref}\n {...props}\n data-slot=\"tab-panel\"\n className={cn(tabPanelVariants(), className)}\n />\n );\n});\n"],"mappings":"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"}
1
+ {"version":3,"file":"tabs.js","names":["Tabs","AriaTabs","TabList","AriaTabList","Tab","AriaTab","TabPanel","AriaTabPanel"],"sources":["../../src/tabs/tabs.tsx"],"sourcesContent":["\"use client\";\n\nimport { cva } from \"class-variance-authority\";\nimport { forwardRef, useEffect, useMemo, useState } from \"react\";\nimport {\n Tab as AriaTab,\n TabList as AriaTabList,\n TabPanel as AriaTabPanel,\n Tabs as AriaTabs,\n type TabListProps as AriaTabListProps,\n type TabPanelProps as AriaTabPanelProps,\n type TabProps as AriaTabProps,\n type TabsProps as AriaTabsProps,\n} from \"react-aria-components\";\n\nimport { cn } from \"../lib/cn.js\";\nimport { mergeRefs } from \"../lib/merge-refs.js\";\n\n/**\n * Tabs, ported from the legacy shadcn/Radix component.\n *\n * The largest legacy surface in this migration: 18 files, `TabsTrigger`\n * 51, `TabsContent` 31, `Tabs` 17, `TabsList` 17. Props: `value` 88,\n * `className` 76, `defaultValue` 12, `onValueChange` 10, `disabled` 6.\n *\n * The rename is mechanical but it touches every one of those:\n * `value` -> `selectedKey`, `defaultValue` -> `defaultSelectedKey`,\n * `onValueChange` -> `onSelectionChange`, and the `value` on each trigger\n * and panel becomes `id`. React Aria pairs a Tab to its TabPanel by that\n * id, exactly as Radix paired them by value.\n *\n * THE SELECTED TAB GAINS A BORDER, and that is a contrast fix rather than\n * a decoration. The legacy marks the selected tab with `bg-background` on\n * a `bg-muted` list -- 1.13:1, which is not a boundary anybody can see --\n * and leans on `shadow-sm` to carry the rest. Under WCAG 1.4.11 the thing\n * telling you which tab is active has to clear 3:1. The border is\n * `primary-strong` on the `primary-subtle` track, and is in the contrast\n * contract. Tab text is `primary-strong` for active and inactive tabs alike\n * (UX owner, 2026-11-01).\n *\n * Two bracket values are gone because check:hardcoding rejects them:\n * `p-[3px]` on the list and `h-[calc(100%-1px)]` on the trigger. The list\n * is `p-1` and the trigger sizes to the list rather than to a value one\n * pixel short of it.\n *\n * ONE BEHAVIOUR DIFFERS AND IS WORTH KNOWING. Radix keeps every mounted\n * TabsContent in the DOM and hides the inactive ones; React Aria renders\n * ONLY the selected panel. A panel that holds uncommitted form state, a\n * running video or a scroll position will lose it on tab change. React\n * Aria's `shouldForceMount` on TabPanel restores the old behaviour where\n * a call site needs it.\n */\nexport const tabsVariants = cva([\n \"flex flex-col gap-2 box-border font-sans\",\n // May be narrower than its tabs: in a flex row (a toolbar beside a search\n // field) it shrinks to the space it has instead of pushing the page wider.\n \"min-w-0 max-w-full\",\n]);\n\nexport const tabListVariants = cva([\n \"inline-flex w-fit items-center justify-center gap-1\",\n // p-1, not the legacy p-[3px]: a bracket value check:hardcoding rejects.\n \"rounded-control p-1\",\n \"bg-primary-subtle text-primary-strong\",\n // The selection highlight that glides between tabs (styles.css).\n \"tab-highlight\",\n \"box-border\",\n\n // A ROW WIDER THAN ITS BOX SCROLLS INSIDE IT. Capped at the parent's\n // width; while its tabs are wider than that, TabList sets\n // data-overflowing and the row scrolls sideways on its own. A row that\n // fits never carries the attribute, so nothing about it changes.\n \"max-w-full\",\n // How wide the fade is, and where it starts measured from the far edge.\n \"[--fade-tabs:--spacing(6)] [--fade-tabs-from:calc(100%-var(--fade-tabs))]\",\n \"data-[overflowing]:overflow-x-auto data-[overflowing]:overscroll-x-contain\",\n // justify-center spills centred overflow out of BOTH sides, and a scroll\n // box cannot reach the spill on its start side: the first tab would be\n // cut off for good. Scrolling rows start at the start edge.\n \"data-[overflowing]:justify-start\",\n // The cue that there is more: the side with tabs out of view fades out.\n // Masks are physical, so the start and end swap sides in RTL.\n \"ltr:data-[fade-start]:mask-l-from-(--fade-tabs-from) rtl:data-[fade-start]:mask-r-from-(--fade-tabs-from)\",\n \"ltr:data-[fade-end]:mask-r-from-(--fade-tabs-from) rtl:data-[fade-end]:mask-l-from-(--fade-tabs-from)\",\n]);\n\n/**\n * Sets data-overflowing, data-fade-start and data-fade-end on a tab row\n * while its tabs are wider than its box, and on the first measurement that\n * finds it too wide, scrolls the selected tab into view INSIDE the row\n * (never the page, so a row below the fold does not drag the page to it).\n *\n * The attributes are written straight onto the element, so a scroll or a\n * resize re-renders nothing. Arrow keys need no help: React Aria keeps the\n * focused tab in view in the row that scrolls.\n */\nconst FADE_PX = 24;\n\nfunction useScrollWhenTooWide() {\n const [row, setRow] = useState<HTMLDivElement | null>(null);\n useEffect(() => {\n if (!row || typeof ResizeObserver === \"undefined\") return;\n let revealed = false;\n const measure = () => {\n const overflowing = row.scrollWidth > row.clientWidth + 1;\n // Distance from the start edge; scrollLeft runs negative in RTL.\n const scrolled = Math.abs(row.scrollLeft);\n row.toggleAttribute(\"data-overflowing\", overflowing);\n row.toggleAttribute(\"data-fade-start\", overflowing && scrolled > 1);\n row.toggleAttribute(\n \"data-fade-end\",\n overflowing && scrolled + row.clientWidth < row.scrollWidth - 1,\n );\n if (!overflowing || revealed) return;\n revealed = true;\n const selected = row.querySelector<HTMLElement>(\"[data-selected]\");\n if (!selected) return;\n // Clear of the fade: --spacing(6), the width the mask draws.\n const fade = FADE_PX;\n const box = row.getBoundingClientRect();\n const tab = selected.getBoundingClientRect();\n if (tab.left < box.left + fade) row.scrollLeft -= box.left + fade - tab.left;\n else if (tab.right > box.right - fade) row.scrollLeft += tab.right - (box.right - fade);\n // The scroll event lands a frame later; the fades are right on first paint.\n measure();\n };\n // The row's box changes with its parent; a tab's width changes when its\n // label does (a count that loads).\n const resized = new ResizeObserver(measure);\n const watch = () => {\n resized.disconnect();\n resized.observe(row);\n for (const tab of Array.from(row.children)) resized.observe(tab);\n };\n const added = new MutationObserver(() => {\n watch();\n measure();\n });\n watch();\n added.observe(row, { childList: true });\n row.addEventListener(\"scroll\", measure, { passive: true });\n measure();\n return () => {\n resized.disconnect();\n added.disconnect();\n row.removeEventListener(\"scroll\", measure);\n };\n }, [row]);\n return setRow;\n}\n\n/**\n * Places the selection highlight (see tab-highlight in styles.css) on the selected\n * tab and keeps it there as tabs resize, and tells the tabs root which way the\n * selection moved (`--tab-dir`) so the next panel arrives from that side. All of it\n * is written straight onto the elements, so no render happens. The first placement\n * has no transition; `data-hl-ready` turns it on two frames later.\n */\nfunction useTabHighlight() {\n const [row, setRow] = useState<HTMLDivElement | null>(null);\n useEffect(() => {\n if (!row || typeof ResizeObserver === \"undefined\") return;\n let previous: HTMLElement | null = null;\n let frames = [0, 0];\n const place = () => {\n const selected = row.querySelector<HTMLElement>('[role=\"tab\"][data-selected]');\n if (!selected) {\n row.removeAttribute(\"data-hl\");\n previous = null;\n return;\n }\n row.style.setProperty(\"--hl-x\", `${selected.offsetLeft}px`);\n row.style.setProperty(\"--hl-y\", `${selected.offsetTop}px`);\n row.style.setProperty(\"--hl-w\", `${selected.offsetWidth}px`);\n row.style.setProperty(\"--hl-h\", `${selected.offsetHeight}px`);\n row.setAttribute(\"data-hl\", \"\");\n if (previous && previous !== selected && previous.isConnected) {\n const all = Array.from(row.querySelectorAll('[role=\"tab\"]'));\n const forward = all.indexOf(selected) > all.indexOf(previous);\n // The row reads right to left in RTL, so the same order change is the other way.\n const flip = getComputedStyle(row).direction === \"rtl\";\n const root = row.closest<HTMLElement>('[data-slot=\"tabs\"]');\n root?.style.setProperty(\"--tab-dir\", String(forward !== flip ? 1 : -1));\n root?.setAttribute(\"data-tab-changed\", \"\");\n }\n previous = selected;\n };\n const resized = new ResizeObserver(place);\n const watch = () => {\n resized.disconnect();\n resized.observe(row);\n for (const tab of Array.from(row.children)) resized.observe(tab);\n };\n const changed = new MutationObserver((records) => {\n if (records.some((record) => record.type === \"childList\")) watch();\n place();\n });\n watch();\n changed.observe(row, { attributes: true, attributeFilter: [\"data-selected\"], childList: true, subtree: true });\n place();\n frames[0] = requestAnimationFrame(() => {\n frames[1] = requestAnimationFrame(() => row.setAttribute(\"data-hl-ready\", \"\"));\n });\n return () => {\n resized.disconnect();\n changed.disconnect();\n frames.forEach(cancelAnimationFrame);\n frames = [0, 0];\n };\n }, [row]);\n return setRow;\n}\n\nexport const tabVariants = cva([\n \"inline-flex flex-1 items-center justify-center gap-1.5 whitespace-nowrap\",\n \"rounded-md px-2 py-1\",\n \"text-sm leading-(--leading-normal) font-medium text-primary-strong\",\n \"cursor-pointer select-none box-border\",\n // A transparent border at rest so selecting one does not move the text.\n \"border-(length:--border-width) border-transparent\",\n \"motion-safe:transition-colors motion-safe:duration-(--duration-fast) motion-safe:ease-standard\",\n\n // The selected tab: a white fill with a 1px primary-strong border on the\n // primary-subtle track (UX owner, 2026-11-01). Text is primary-strong for\n // the active and the inactive tabs alike. A white fill on that track is\n // barely a boundary, so the border is what satisfies WCAG 1.4.11 (it is\n // graded in contrast-pairs.json); the fill and the shadow are polish.\n // Until the row has placed its gliding highlight (data-hl), the tab paints\n // its own fill so there is never a frame without a boundary.\n \"not-in-data-[hl]:data-[selected]:bg-surface not-in-data-[hl]:data-[selected]:border-primary-strong\",\n \"not-in-data-[hl]:data-[selected]:shadow-surface\",\n\n // outline-solid is load-bearing: outline-none sets --tw-outline-style to\n // none and the width utility resolves its style from that same variable,\n // so a focus rule can apply a width and a colour and paint nothing.\n \"outline-none\",\n \"data-[focus-visible]:outline-solid\",\n \"data-[focus-visible]:outline-(length:--focus-ring-width)\",\n \"data-[focus-visible]:outline-offset-(--focus-ring-offset)\",\n \"data-[focus-visible]:outline-(--color-ring)\",\n\n \"data-[disabled]:cursor-not-allowed data-[disabled]:opacity-(--opacity-disabled)\",\n \"[&_svg]:pointer-events-none [&_svg]:shrink-0\",\n \"[&_svg:not([class*='size-'])]:size-(--icon-sm)\",\n]);\n\nexport const tabPanelVariants = cva([\n \"flex-1 box-border font-sans text-foreground\",\n // Arrives from the side the new tab sits on, once the selection has changed.\n \"tab-panel-motion\",\n // React Aria gives the panel tabindex=0 when it holds no focusable\n // child, so it can take a ring it does not want at rest.\n \"outline-none\",\n \"data-[focus-visible]:outline-solid\",\n \"data-[focus-visible]:outline-(length:--focus-ring-width)\",\n \"data-[focus-visible]:outline-offset-(--focus-ring-offset)\",\n \"data-[focus-visible]:outline-(--color-ring)\",\n]);\n\n/**\n * A set of panels, one visible at a time.\n *\n * @example\n * <Tabs defaultSelectedKey=\"overview\">\n * <TabList aria-label=\"Sections\">\n * <Tab id=\"overview\">Overview</Tab>\n * <Tab id=\"activity\">Activity</Tab>\n * </TabList>\n * <TabPanel id=\"overview\">...</TabPanel>\n * <TabPanel id=\"activity\">...</TabPanel>\n * </Tabs>\n */\nexport interface TabsProps extends Omit<AriaTabsProps, \"className\" | \"style\"> {\n /** Extra classes. Merged last, so they win conflicts with ours. */\n className?: string;\n}\n\nexport const Tabs = forwardRef<HTMLDivElement, TabsProps>(function Tabs(\n { className, ...props },\n ref,\n) {\n return (\n <AriaTabs ref={ref} {...props} data-slot=\"tabs\" className={cn(tabsVariants(), className)} />\n );\n});\n\n/**\n * The row of tabs.\n *\n * `aria-label` is worth passing: a tablist is a landmark-ish grouping and\n * a page with two of them is ambiguous without one.\n */\nexport interface TabListProps extends Omit<AriaTabListProps<object>, \"className\" | \"style\"> {\n className?: string;\n}\n\nexport const TabList = forwardRef<HTMLDivElement, TabListProps>(function TabList(\n { className, ...props },\n ref,\n) {\n const watch = useScrollWhenTooWide();\n const glide = useTabHighlight();\n // Memoised: a new callback every render would detach and reattach the row.\n const merged = useMemo(() => mergeRefs(ref, watch, glide), [ref, watch, glide]);\n return (\n <AriaTabList\n ref={merged}\n {...props}\n data-slot=\"tab-list\"\n className={cn(tabListVariants(), className)}\n />\n );\n});\n\n/** One tab. Its `id` pairs it with the panel of the same `id`. */\nexport interface TabProps extends Omit<AriaTabProps, \"className\" | \"style\"> {\n className?: string;\n}\n\nexport const Tab = forwardRef<HTMLDivElement, TabProps>(function Tab({ className, ...props }, ref) {\n return <AriaTab ref={ref} {...props} data-slot=\"tab\" className={cn(tabVariants(), className)} />;\n});\n\n/** One panel. Its `id` pairs it with the tab of the same `id`. */\nexport interface TabPanelProps extends Omit<AriaTabPanelProps, \"className\" | \"style\"> {\n className?: string;\n}\n\nexport const TabPanel = forwardRef<HTMLDivElement, TabPanelProps>(function TabPanel(\n { className, ...props },\n ref,\n) {\n return (\n <AriaTabPanel\n ref={ref}\n {...props}\n data-slot=\"tab-panel\"\n className={cn(tabPanelVariants(), className)}\n />\n );\n});\n"],"mappings":"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"}
@@ -2,9 +2,9 @@
2
2
  import { cn, cnAll } from "../lib/cn.js";
3
3
  import { prefersReducedMotion, useExitBackstop, useIsomorphicLayoutEffect } from "../lib/motion.js";
4
4
  import { useHeightFollow } from "../lib/height-follow.js";
5
+ import { FieldShell, useFieldWiring } from "../lib/field-shell.js";
5
6
  import { fieldSurfaceVariants } from "../lib/field-surface.js";
6
7
  import { useLeavingList } from "../lib/leaving-list.js";
7
- import { FieldShell, useFieldWiring } from "../lib/field-shell.js";
8
8
  import { forwardRef, useCallback, useImperativeHandle, useRef, useState } from "react";
9
9
  import { Button, Group, Input, Tag, TagGroup, TagList } from "react-aria-components";
10
10
  import { Fragment as Fragment$1, jsx, jsxs } from "react/jsx-runtime";
@@ -1,7 +1,7 @@
1
1
  "use client";
2
2
  import { cn } from "../lib/cn.js";
3
- import { fieldSurfaceVariants } from "../lib/field-surface.js";
4
3
  import { FieldShell, hasFieldProps, useFieldWiring } from "../lib/field-shell.js";
4
+ import { fieldSurfaceVariants } from "../lib/field-surface.js";
5
5
  import { cva } from "class-variance-authority";
6
6
  import { forwardRef } from "react";
7
7
  import { TextArea } from "react-aria-components";
@@ -34,11 +34,11 @@ import { UNSTABLE_ToastQueue } from "react-aria-components";
34
34
  /** Where the region sits. Fixed, above everything, out of the way of content. */
35
35
  export declare const toastRegionVariants: (props?: import("class-variance-authority/types").ClassProp | undefined) => string;
36
36
  export declare const toastVariants: (props?: ({
37
- tone?: "neutral" | "warning" | "success" | "info" | "danger" | null | undefined;
37
+ tone?: "neutral" | "success" | "warning" | "danger" | "info" | null | undefined;
38
38
  } & import("class-variance-authority/types").ClassProp) | undefined) => string;
39
39
  /** The status icon's circle: the tone's light fill with its AA text colour. */
40
40
  export declare const toastIconVariants: (props?: ({
41
- tone?: "neutral" | "warning" | "success" | "info" | "danger" | null | undefined;
41
+ tone?: "neutral" | "success" | "warning" | "danger" | "info" | null | undefined;
42
42
  } & import("class-variance-authority/types").ClassProp) | undefined) => string;
43
43
  export declare const toastCloseVariants: (props?: import("class-variance-authority/types").ClassProp | undefined) => string;
44
44
  /** What a toast carries. `title` is required: an unnamed toast announces nothing. */
@@ -0,0 +1,2 @@
1
+ import { ToggleButton, ToggleButtonGroup, ToggleButtonGroupProps, ToggleButtonProps, toggleButtonGroupVariants, toggleButtonVariants } from "./toggle-button.js";
2
+ export { ToggleButton, ToggleButtonGroup, type ToggleButtonGroupProps, type ToggleButtonProps, toggleButtonGroupVariants, toggleButtonVariants };
@@ -0,0 +1,2 @@
1
+ import { ToggleButton, ToggleButtonGroup, toggleButtonGroupVariants, toggleButtonVariants } from "./toggle-button.js";
2
+ export { ToggleButton, ToggleButtonGroup, toggleButtonGroupVariants, toggleButtonVariants };
@@ -0,0 +1,93 @@
1
+ import { VariantProps } from "class-variance-authority";
2
+ import { ReactNode } from "react";
3
+ import { ToggleButtonGroupProps, ToggleButtonProps } from "react-aria-components";
4
+ //#region src/toggle-button/toggle-button.d.ts
5
+ /**
6
+ * ToggleButton: a button that stays pressed. One question, two answers
7
+ * ("Has new reply", "Primary key"), where a Switch would sit in a form row and
8
+ * a Checkbox would need a label beside a box.
9
+ *
10
+ * Built on React Aria's `ToggleButton`, so it is a real `<button>` with
11
+ * `aria-pressed`; in a {@link ToggleButtonGroup} that allows one choice the
12
+ * same buttons are a toolbar of `aria-pressed` buttons. It is NOT Button with `aria-pressed` and a
13
+ * swapped variant, which is what the apps hand-build today: the selected look
14
+ * here does not rest on colour.
15
+ *
16
+ * SELECTED IS SHOWN THREE WAYS. A tint, a `primary-strong` edge and a check
17
+ * mark drawn in `currentColor`, so the state survives forced colours and
18
+ * colour-blindness (WCAG 1.4.1). The mark sits INSIDE the control's own box,
19
+ * in the corner, so it never shifts the layout when it appears and is never
20
+ * clipped by a table row or scroll area outside the border. An icon-only
21
+ * toggle carries it too.
22
+ *
23
+ * FOCUS follows the field surface: on `:focus-visible` the control's own 1px
24
+ * border turns dark and nothing else is drawn, no ring. Forced colours
25
+ * override a border colour, so only there does an outline come back.
26
+ *
27
+ * Every string is a STATIC literal. Tailwind scans source text, so a class
28
+ * assembled at runtime is invisible to it and would never be emitted.
29
+ */
30
+ export declare const toggleButtonVariants: (props?: ({
31
+ size?: "sm" | "md" | null | undefined;
32
+ iconOnly?: boolean | null | undefined;
33
+ } & import("class-variance-authority/types").ClassProp) | undefined) => string;
34
+ /** The group's row. Buttons wrap rather than overflow a narrow container. */
35
+ export declare const toggleButtonGroupVariants: (props?: import("class-variance-authority/types").ClassProp | undefined) => string;
36
+ interface ToggleButtonProps$1 extends Omit<ToggleButtonProps, "className" | "style" | "children">, Omit<VariantProps<typeof toggleButtonVariants>, "iconOnly"> {
37
+ /** The label, or the glyph of an `iconOnly` toggle. */
38
+ children?: ReactNode;
39
+ /**
40
+ * Only a glyph, in a square button. Pass the glyph as `children` and name the
41
+ * button with `aria-label`; without one it has no accessible name.
42
+ *
43
+ * @default false
44
+ */
45
+ iconOnly?: boolean;
46
+ /** Extra classes. Merged last, so they win conflicts with ours. */
47
+ className?: string;
48
+ }
49
+ /**
50
+ * A button that is on or off. Controlled with `isSelected` and
51
+ * `onChange(isSelected)`, or uncontrolled with `defaultSelected`.
52
+ *
53
+ * Give an icon-only toggle an `aria-label`, and wrap it in a `TooltipTrigger`
54
+ * when the glyph alone is not enough. The label should name the control, not
55
+ * its state: assistive technology says "pressed" itself.
56
+ *
57
+ * @example
58
+ * <ToggleButton isSelected={unread} onChange={setUnread}>Has new reply</ToggleButton>
59
+ * <ToggleButton iconOnly aria-label="Primary key"><KeyIcon /></ToggleButton>
60
+ */
61
+ declare const ToggleButton$1: import("react").ForwardRefExoticComponent<ToggleButtonProps$1 & import("react").RefAttributes<HTMLButtonElement>>;
62
+ interface ToggleButtonGroupProps$1 extends Omit<ToggleButtonGroupProps, "className" | "style"> {
63
+ /** Extra classes. Merged last, so they win conflicts with ours. */
64
+ className?: string;
65
+ }
66
+ /**
67
+ * A row of ToggleButtons that share a selection. Always a toolbar of
68
+ * `aria-pressed` buttons: ONE tab stop, arrow keys move focus, Space or Enter
69
+ * toggles the focused button (selection never follows focus).
70
+ *
71
+ * `selectionMode="single"` (the default) lets one button be pressed at a time:
72
+ * pressing another moves the selection to it, and pressing the pressed one
73
+ * clears it unless `disallowEmptySelection`. Tab enters the group on the pressed
74
+ * button (on the first when none is). `"multiple"` lets any number be pressed.
75
+ * Each child needs an `id`, which is its key in `selectedKeys`.
76
+ *
77
+ * Why not `role="radiogroup"`: React Aria's single mode is a radio group in
78
+ * name only. Its arrows move focus and do not check, and every button is a tab
79
+ * stop, which is not the APG radio pattern. A true radio group needs the
80
+ * selection to follow focus, which a button that you press does not do. The
81
+ * toolbar of pressed buttons is the APG pattern that matches how these behave.
82
+ * If you need a radio group, use `RadioGroup` (`variant="card"` for the framed look).
83
+ *
84
+ * @example
85
+ * <ToggleButtonGroup aria-label="Range" selectionMode="single" disallowEmptySelection>
86
+ * <ToggleButton id="7d">7 days</ToggleButton>
87
+ * <ToggleButton id="30d">30 days</ToggleButton>
88
+ * </ToggleButtonGroup>
89
+ */
90
+ declare const ToggleButtonGroup$1: import("react").ForwardRefExoticComponent<ToggleButtonGroupProps$1 & import("react").RefAttributes<HTMLDivElement>>;
91
+ //#endregion
92
+ export { ToggleButton$1 as ToggleButton, ToggleButtonGroup$1 as ToggleButtonGroup, ToggleButtonGroupProps$1 as ToggleButtonGroupProps, ToggleButtonProps$1 as ToggleButtonProps };
93
+ //# sourceMappingURL=toggle-button.d.ts.map
@@ -0,0 +1,199 @@
1
+ "use client";
2
+ import { cn } from "../lib/cn.js";
3
+ import { mergeRefs } from "../lib/merge-refs.js";
4
+ import { useIsomorphicLayoutEffect } from "../lib/motion.js";
5
+ import { cva } from "class-variance-authority";
6
+ import { createContext, forwardRef, useContext, useMemo, useRef, useState } from "react";
7
+ import { ToggleButton, ToggleButtonGroup } from "react-aria-components";
8
+ import { Fragment as Fragment$1, jsx, jsxs } from "react/jsx-runtime";
9
+ //#region src/toggle-button/toggle-button.tsx
10
+ /**
11
+ * ToggleButton: a button that stays pressed. One question, two answers
12
+ * ("Has new reply", "Primary key"), where a Switch would sit in a form row and
13
+ * a Checkbox would need a label beside a box.
14
+ *
15
+ * Built on React Aria's `ToggleButton`, so it is a real `<button>` with
16
+ * `aria-pressed`; in a {@link ToggleButtonGroup} that allows one choice the
17
+ * same buttons are a toolbar of `aria-pressed` buttons. It is NOT Button with `aria-pressed` and a
18
+ * swapped variant, which is what the apps hand-build today: the selected look
19
+ * here does not rest on colour.
20
+ *
21
+ * SELECTED IS SHOWN THREE WAYS. A tint, a `primary-strong` edge and a check
22
+ * mark drawn in `currentColor`, so the state survives forced colours and
23
+ * colour-blindness (WCAG 1.4.1). The mark sits INSIDE the control's own box,
24
+ * in the corner, so it never shifts the layout when it appears and is never
25
+ * clipped by a table row or scroll area outside the border. An icon-only
26
+ * toggle carries it too.
27
+ *
28
+ * FOCUS follows the field surface: on `:focus-visible` the control's own 1px
29
+ * border turns dark and nothing else is drawn, no ring. Forced colours
30
+ * override a border colour, so only there does an outline come back.
31
+ *
32
+ * Every string is a STATIC literal. Tailwind scans source text, so a class
33
+ * assembled at runtime is invisible to it and would never be emitted.
34
+ */
35
+ const toggleButtonVariants = cva([
36
+ "relative inline-flex items-center justify-center gap-(--density-control-gap) shrink-0 whitespace-nowrap",
37
+ "rounded-control font-sans font-medium text-foreground",
38
+ "cursor-pointer select-none box-border",
39
+ "border-(length:--border-width) border-border bg-surface shadow-surface",
40
+ "[&_svg]:pointer-events-none [&_svg]:shrink-0",
41
+ "[&_svg:not([class*='size-'])]:size-(--icon-sm)",
42
+ "[-webkit-tap-highlight-color:transparent]",
43
+ "motion-safe:transition-[border-color,background-color] motion-safe:duration-(--duration-quick) motion-safe:ease-settle",
44
+ "data-[hovered]:bg-muted data-[hovered]:border-border-strong",
45
+ "data-[selected]:bg-primary-subtle data-[selected]:border-primary-strong",
46
+ "data-[selected]:data-[hovered]:bg-primary-subtle",
47
+ "outline-none",
48
+ "data-[focus-visible]:border-foreground",
49
+ "data-[focus-visible]:data-[selected]:border-foreground",
50
+ "forced-colors:data-[focus-visible]:outline-solid",
51
+ "forced-colors:data-[focus-visible]:outline-(length:--focus-ring-width)",
52
+ "forced-colors:data-[focus-visible]:outline-offset-(--focus-ring-offset)",
53
+ "data-[disabled]:cursor-not-allowed data-[disabled]:bg-muted data-[disabled]:text-text-disabled data-[disabled]:border-border",
54
+ "data-[disabled]:data-[selected]:bg-muted data-[disabled]:data-[selected]:border-border"
55
+ ], {
56
+ variants: {
57
+ size: {
58
+ sm: "h-(--density-control-h-sm) text-sm",
59
+ md: "h-(--density-control-h-md) text-sm"
60
+ },
61
+ /** A square button that holds only a glyph. It must carry an `aria-label`. */
62
+ iconOnly: {
63
+ true: "px-0 text-base",
64
+ false: "ps-4 pe-5"
65
+ }
66
+ },
67
+ compoundVariants: [{
68
+ size: "sm",
69
+ iconOnly: true,
70
+ class: "w-(--density-control-h-sm)"
71
+ }, {
72
+ size: "md",
73
+ iconOnly: true,
74
+ class: "w-(--density-control-h-md)"
75
+ }],
76
+ defaultVariants: {
77
+ size: "sm",
78
+ iconOnly: false
79
+ }
80
+ });
81
+ /** The group's row. Buttons wrap rather than overflow a narrow container. */
82
+ const toggleButtonGroupVariants = cva(["box-border inline-flex flex-wrap gap-2 font-sans", "data-[orientation=vertical]:flex-col"]);
83
+ const TabStopContext = createContext(null);
84
+ const CHECK_PLACEMENT = {
85
+ label: "size-3 end-1.5 top-1/2 -translate-y-1/2",
86
+ icon: "size-2.5 end-1.5 top-1.5"
87
+ };
88
+ /**
89
+ * A button that is on or off. Controlled with `isSelected` and
90
+ * `onChange(isSelected)`, or uncontrolled with `defaultSelected`.
91
+ *
92
+ * Give an icon-only toggle an `aria-label`, and wrap it in a `TooltipTrigger`
93
+ * when the glyph alone is not enough. The label should name the control, not
94
+ * its state: assistive technology says "pressed" itself.
95
+ *
96
+ * @example
97
+ * <ToggleButton isSelected={unread} onChange={setUnread}>Has new reply</ToggleButton>
98
+ * <ToggleButton iconOnly aria-label="Primary key"><KeyIcon /></ToggleButton>
99
+ */
100
+ const ToggleButton$1 = forwardRef(function ToggleButton$2({ className, size, iconOnly = false, children, excludeFromTabOrder, ...props }, ref) {
101
+ const stop = useContext(TabStopContext);
102
+ const skipTab = stop !== null && String(props.id) !== stop;
103
+ return /* @__PURE__ */ jsx(ToggleButton, {
104
+ ref,
105
+ ...props,
106
+ excludeFromTabOrder: excludeFromTabOrder ?? skipTab,
107
+ "data-slot": "toggle-button",
108
+ "data-key": props.id,
109
+ className: cn(toggleButtonVariants({
110
+ size,
111
+ iconOnly
112
+ }), className),
113
+ children: ({ isSelected }) => /* @__PURE__ */ jsxs(Fragment$1, { children: [children, isSelected ? /* @__PURE__ */ jsx("svg", {
114
+ "aria-hidden": "true",
115
+ "data-slot": "toggle-button-check",
116
+ viewBox: "0 0 12 12",
117
+ fill: "none",
118
+ stroke: "currentColor",
119
+ strokeWidth: "1.75",
120
+ strokeLinecap: "round",
121
+ strokeLinejoin: "round",
122
+ className: cn("pointer-events-none absolute", iconOnly ? CHECK_PLACEMENT.icon : CHECK_PLACEMENT.label),
123
+ children: /* @__PURE__ */ jsx("path", { d: "M2.5 6.25 5 8.75 9.5 3.5" })
124
+ }) : null] })
125
+ });
126
+ });
127
+ /**
128
+ * A row of ToggleButtons that share a selection. Always a toolbar of
129
+ * `aria-pressed` buttons: ONE tab stop, arrow keys move focus, Space or Enter
130
+ * toggles the focused button (selection never follows focus).
131
+ *
132
+ * `selectionMode="single"` (the default) lets one button be pressed at a time:
133
+ * pressing another moves the selection to it, and pressing the pressed one
134
+ * clears it unless `disallowEmptySelection`. Tab enters the group on the pressed
135
+ * button (on the first when none is). `"multiple"` lets any number be pressed.
136
+ * Each child needs an `id`, which is its key in `selectedKeys`.
137
+ *
138
+ * Why not `role="radiogroup"`: React Aria's single mode is a radio group in
139
+ * name only. Its arrows move focus and do not check, and every button is a tab
140
+ * stop, which is not the APG radio pattern. A true radio group needs the
141
+ * selection to follow focus, which a button that you press does not do. The
142
+ * toolbar of pressed buttons is the APG pattern that matches how these behave.
143
+ * If you need a radio group, use `RadioGroup` (`variant="card"` for the framed look).
144
+ *
145
+ * @example
146
+ * <ToggleButtonGroup aria-label="Range" selectionMode="single" disallowEmptySelection>
147
+ * <ToggleButton id="7d">7 days</ToggleButton>
148
+ * <ToggleButton id="30d">30 days</ToggleButton>
149
+ * </ToggleButtonGroup>
150
+ */
151
+ const ToggleButtonGroup$1 = forwardRef(function ToggleButtonGroup$2({ className, selectionMode = "single", selectedKeys, defaultSelectedKeys, onSelectionChange, ...props }, ref) {
152
+ const single = selectionMode === "single";
153
+ const [inner, setInner] = useState(() => new Set(defaultSelectedKeys ?? []));
154
+ const chosen = selectedKeys === void 0 ? inner : new Set(selectedKeys);
155
+ const own = useRef(null);
156
+ const [stop, setStop] = useState(null);
157
+ useIsomorphicLayoutEffect(() => {
158
+ if (!single || chosen.size === 0) return setStop(null);
159
+ const enabled = [...own.current?.querySelectorAll("[data-slot=\"toggle-button\"]") ?? []].filter((el) => !el.disabled);
160
+ const pressed = enabled.find((el) => el.getAttribute("aria-pressed") === "true");
161
+ setStop((pressed ?? enabled[0])?.getAttribute("data-key") ?? null);
162
+ });
163
+ const shared = {
164
+ ref: useMemo(() => mergeRefs(own, ref), [ref]),
165
+ ...props,
166
+ "data-slot": "toggle-button-group",
167
+ className: cn(toggleButtonGroupVariants(), className)
168
+ };
169
+ if (!single) return /* @__PURE__ */ jsx(ToggleButtonGroup, {
170
+ ...shared,
171
+ selectionMode: "multiple",
172
+ selectedKeys,
173
+ defaultSelectedKeys,
174
+ onSelectionChange
175
+ });
176
+ const change = (next) => {
177
+ if (next === "all") return;
178
+ let keys = next;
179
+ if (next.size > 1) {
180
+ const added = [...next].find((key) => !chosen.has(key));
181
+ keys = /* @__PURE__ */ new Set([added ?? [...next][0]]);
182
+ }
183
+ if (selectedKeys === void 0) setInner(keys);
184
+ onSelectionChange?.(keys);
185
+ };
186
+ return /* @__PURE__ */ jsx(TabStopContext.Provider, {
187
+ value: stop,
188
+ children: /* @__PURE__ */ jsx(ToggleButtonGroup, {
189
+ ...shared,
190
+ selectionMode: "multiple",
191
+ selectedKeys: chosen,
192
+ onSelectionChange: change
193
+ })
194
+ });
195
+ });
196
+ //#endregion
197
+ export { ToggleButton$1 as ToggleButton, ToggleButtonGroup$1 as ToggleButtonGroup, toggleButtonGroupVariants, toggleButtonVariants };
198
+
199
+ //# sourceMappingURL=toggle-button.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"toggle-button.js","names":["ToggleButton","AriaToggleButton","ToggleButtonGroup","AriaToggleButtonGroup"],"sources":["../../src/toggle-button/toggle-button.tsx"],"sourcesContent":["\"use client\";\n\nimport { cva, type VariantProps } from \"class-variance-authority\";\nimport {\n createContext,\n forwardRef,\n useContext,\n useMemo,\n useRef,\n useState,\n type ReactNode,\n} from \"react\";\nimport {\n ToggleButton as AriaToggleButton,\n ToggleButtonGroup as AriaToggleButtonGroup,\n type ToggleButtonGroupProps as AriaToggleButtonGroupProps,\n type Key,\n type Selection,\n type ToggleButtonProps as AriaToggleButtonProps,\n} from \"react-aria-components\";\n\nimport { cn } from \"../lib/cn.js\";\nimport { mergeRefs } from \"../lib/merge-refs.js\";\nimport { useIsomorphicLayoutEffect } from \"../lib/motion.js\";\n\n/**\n * ToggleButton: a button that stays pressed. One question, two answers\n * (\"Has new reply\", \"Primary key\"), where a Switch would sit in a form row and\n * a Checkbox would need a label beside a box.\n *\n * Built on React Aria's `ToggleButton`, so it is a real `<button>` with\n * `aria-pressed`; in a {@link ToggleButtonGroup} that allows one choice the\n * same buttons are a toolbar of `aria-pressed` buttons. It is NOT Button with `aria-pressed` and a\n * swapped variant, which is what the apps hand-build today: the selected look\n * here does not rest on colour.\n *\n * SELECTED IS SHOWN THREE WAYS. A tint, a `primary-strong` edge and a check\n * mark drawn in `currentColor`, so the state survives forced colours and\n * colour-blindness (WCAG 1.4.1). The mark sits INSIDE the control's own box,\n * in the corner, so it never shifts the layout when it appears and is never\n * clipped by a table row or scroll area outside the border. An icon-only\n * toggle carries it too.\n *\n * FOCUS follows the field surface: on `:focus-visible` the control's own 1px\n * border turns dark and nothing else is drawn, no ring. Forced colours\n * override a border colour, so only there does an outline come back.\n *\n * Every string is a STATIC literal. Tailwind scans source text, so a class\n * assembled at runtime is invisible to it and would never be emitted.\n */\nexport const toggleButtonVariants = cva(\n [\n \"relative inline-flex items-center justify-center gap-(--density-control-gap) shrink-0 whitespace-nowrap\",\n \"rounded-control font-sans font-medium text-foreground\",\n \"cursor-pointer select-none box-border\",\n \"border-(length:--border-width) border-border bg-surface shadow-surface\",\n \"[&_svg]:pointer-events-none [&_svg]:shrink-0\",\n \"[&_svg:not([class*='size-'])]:size-(--icon-sm)\",\n \"[-webkit-tap-highlight-color:transparent]\",\n \"motion-safe:transition-[border-color,background-color] motion-safe:duration-(--duration-quick) motion-safe:ease-settle\",\n\n // Hover darkens the edge a step, as on a text field.\n \"data-[hovered]:bg-muted data-[hovered]:border-border-strong\",\n\n // Selected: tint and edge. The check mark below is the cue that is not colour.\n \"data-[selected]:bg-primary-subtle data-[selected]:border-primary-strong\",\n \"data-[selected]:data-[hovered]:bg-primary-subtle\",\n\n // Focus is the control's own border going dark. Declared last and doubled\n // with the selected state so a pressed, focused toggle shows it too.\n // outline-none, then the outline only under forced colours, where a border\n // colour is overridden and would leave no indicator (same recipe as\n // fieldSurfaceVariants).\n \"outline-none\",\n \"data-[focus-visible]:border-foreground\",\n \"data-[focus-visible]:data-[selected]:border-foreground\",\n \"forced-colors:data-[focus-visible]:outline-solid\",\n \"forced-colors:data-[focus-visible]:outline-(length:--focus-ring-width)\",\n \"forced-colors:data-[focus-visible]:outline-offset-(--focus-ring-offset)\",\n\n // Disabled comes after, and doubled with the selected state for the same reason, so a\n // disabled, selected toggle drops the tint and the edge and keeps only the check mark.\n \"data-[disabled]:cursor-not-allowed data-[disabled]:bg-muted data-[disabled]:text-text-disabled data-[disabled]:border-border\",\n \"data-[disabled]:data-[selected]:bg-muted data-[disabled]:data-[selected]:border-border\",\n ],\n {\n variants: {\n size: {\n sm: \"h-(--density-control-h-sm) text-sm\",\n md: \"h-(--density-control-h-md) text-sm\",\n },\n /** A square button that holds only a glyph. It must carry an `aria-label`. */\n iconOnly: {\n true: \"px-0 text-base\",\n // 4px more at the end than the start: room for the selected mark, always, so nothing moves.\n false: \"ps-4 pe-5\",\n },\n },\n compoundVariants: [\n { size: \"sm\", iconOnly: true, class: \"w-(--density-control-h-sm)\" },\n { size: \"md\", iconOnly: true, class: \"w-(--density-control-h-md)\" },\n ],\n defaultVariants: { size: \"sm\", iconOnly: false },\n },\n);\n\n/** The group's row. Buttons wrap rather than overflow a narrow container. */\nexport const toggleButtonGroupVariants = cva([\n \"box-border inline-flex flex-wrap gap-2 font-sans\",\n \"data-[orientation=vertical]:flex-col\",\n]);\n\n// The key (as a string) of a single-choice group's one tab stop, or null when the group has\n// no opinion (outside a group, in multiple mode, nothing pressed, or nothing enabled).\nconst TabStopContext = createContext<string | null>(null);\n\nexport interface ToggleButtonProps\n extends\n Omit<AriaToggleButtonProps, \"className\" | \"style\" | \"children\">,\n Omit<VariantProps<typeof toggleButtonVariants>, \"iconOnly\"> {\n /** The label, or the glyph of an `iconOnly` toggle. */\n children?: ReactNode;\n /**\n * Only a glyph, in a square button. Pass the glyph as `children` and name the\n * button with `aria-label`; without one it has no accessible name.\n *\n * @default false\n */\n iconOnly?: boolean;\n /** Extra classes. Merged last, so they win conflicts with ours. */\n className?: string;\n}\n\n// Where the mark sits inside the box. A label toggle keeps 4px more padding at the end\n// (always, so nothing moves when the mark appears) and the mark is centred in it; an\n// icon-only toggle has no spare room, so the mark takes the top corner, drawn smaller.\nconst CHECK_PLACEMENT = {\n label: \"size-3 end-1.5 top-1/2 -translate-y-1/2\",\n icon: \"size-2.5 end-1.5 top-1.5\",\n} as const;\n\n/**\n * A button that is on or off. Controlled with `isSelected` and\n * `onChange(isSelected)`, or uncontrolled with `defaultSelected`.\n *\n * Give an icon-only toggle an `aria-label`, and wrap it in a `TooltipTrigger`\n * when the glyph alone is not enough. The label should name the control, not\n * its state: assistive technology says \"pressed\" itself.\n *\n * @example\n * <ToggleButton isSelected={unread} onChange={setUnread}>Has new reply</ToggleButton>\n * <ToggleButton iconOnly aria-label=\"Primary key\"><KeyIcon /></ToggleButton>\n */\nexport const ToggleButton = forwardRef<HTMLButtonElement, ToggleButtonProps>(function ToggleButton(\n { className, size, iconOnly = false, children, excludeFromTabOrder, ...props },\n ref,\n) {\n // Single choice: Tab enters the group on the chosen button, the rest are reached by arrows.\n const stop = useContext(TabStopContext);\n const skipTab = stop !== null && String(props.id) !== stop;\n return (\n <AriaToggleButton\n ref={ref}\n {...props}\n excludeFromTabOrder={excludeFromTabOrder ?? skipTab}\n data-slot=\"toggle-button\"\n data-key={props.id}\n className={cn(toggleButtonVariants({ size, iconOnly }), className)}\n >\n {({ isSelected }) => (\n <>\n {children}\n {isSelected ? (\n // Decorative: the button's own pressed state carries it for assistive technology.\n <svg\n aria-hidden=\"true\"\n data-slot=\"toggle-button-check\"\n viewBox=\"0 0 12 12\"\n fill=\"none\"\n stroke=\"currentColor\"\n strokeWidth=\"1.75\"\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n className={cn(\n \"pointer-events-none absolute\",\n iconOnly ? CHECK_PLACEMENT.icon : CHECK_PLACEMENT.label,\n )}\n >\n <path d=\"M2.5 6.25 5 8.75 9.5 3.5\" />\n </svg>\n ) : null}\n </>\n )}\n </AriaToggleButton>\n );\n});\n\nexport interface ToggleButtonGroupProps extends Omit<\n AriaToggleButtonGroupProps,\n \"className\" | \"style\"\n> {\n /** Extra classes. Merged last, so they win conflicts with ours. */\n className?: string;\n}\n\n/**\n * A row of ToggleButtons that share a selection. Always a toolbar of\n * `aria-pressed` buttons: ONE tab stop, arrow keys move focus, Space or Enter\n * toggles the focused button (selection never follows focus).\n *\n * `selectionMode=\"single\"` (the default) lets one button be pressed at a time:\n * pressing another moves the selection to it, and pressing the pressed one\n * clears it unless `disallowEmptySelection`. Tab enters the group on the pressed\n * button (on the first when none is). `\"multiple\"` lets any number be pressed.\n * Each child needs an `id`, which is its key in `selectedKeys`.\n *\n * Why not `role=\"radiogroup\"`: React Aria's single mode is a radio group in\n * name only. Its arrows move focus and do not check, and every button is a tab\n * stop, which is not the APG radio pattern. A true radio group needs the\n * selection to follow focus, which a button that you press does not do. The\n * toolbar of pressed buttons is the APG pattern that matches how these behave.\n * If you need a radio group, use `RadioGroup` (`variant=\"card\"` for the framed look).\n *\n * @example\n * <ToggleButtonGroup aria-label=\"Range\" selectionMode=\"single\" disallowEmptySelection>\n * <ToggleButton id=\"7d\">7 days</ToggleButton>\n * <ToggleButton id=\"30d\">30 days</ToggleButton>\n * </ToggleButtonGroup>\n */\nexport const ToggleButtonGroup = forwardRef<HTMLDivElement, ToggleButtonGroupProps>(\n function ToggleButtonGroup(\n {\n className,\n selectionMode = \"single\",\n selectedKeys,\n defaultSelectedKeys,\n onSelectionChange,\n ...props\n },\n ref,\n ) {\n const single = selectionMode === \"single\";\n const [inner, setInner] = useState<Set<Key>>(() => new Set(defaultSelectedKeys ?? []));\n const chosen = selectedKeys === undefined ? inner : new Set(selectedKeys);\n\n // The one tab stop: the pressed button, or the first enabled one when the pressed button is\n // disabled. Read from the DOM after each render because disabled may come from any wrapper.\n const own = useRef<HTMLDivElement | null>(null);\n const [stop, setStop] = useState<string | null>(null);\n useIsomorphicLayoutEffect(() => {\n if (!single || chosen.size === 0) return setStop(null);\n const enabled = [\n ...(own.current?.querySelectorAll<HTMLElement>('[data-slot=\"toggle-button\"]') ?? []),\n ].filter((el) => !(el as HTMLButtonElement).disabled);\n const pressed = enabled.find((el) => el.getAttribute(\"aria-pressed\") === \"true\");\n setStop((pressed ?? enabled[0])?.getAttribute(\"data-key\") ?? null);\n });\n\n // Memoized on the caller's ref so the merged callback is stable across renders: a new function\n // each render would have React call the caller's callback ref with null and then the node again.\n const mergedRef = useMemo(() => mergeRefs(own, ref), [ref]);\n\n const shared = {\n ref: mergedRef,\n ...props,\n \"data-slot\": \"toggle-button-group\",\n className: cn(toggleButtonGroupVariants(), className),\n };\n\n if (!single) {\n return (\n <AriaToggleButtonGroup\n {...shared}\n selectionMode=\"multiple\"\n selectedKeys={selectedKeys}\n defaultSelectedKeys={defaultSelectedKeys}\n onSelectionChange={onSelectionChange}\n />\n );\n }\n\n // React Aria's own single mode is a radio group; we want the toolbar, so run it as\n // \"multiple\" and keep it to one key: the newly pressed key replaces the old one.\n const change = (next: Selection) => {\n if (next === \"all\") return;\n let keys: Set<Key> = next;\n if (next.size > 1) {\n const added = [...next].find((key) => !chosen.has(key));\n keys = new Set([added ?? [...next][0]!]);\n }\n if (selectedKeys === undefined) setInner(keys);\n onSelectionChange?.(keys);\n };\n\n return (\n <TabStopContext.Provider value={stop}>\n <AriaToggleButtonGroup\n {...shared}\n selectionMode=\"multiple\"\n selectedKeys={chosen}\n onSelectionChange={change}\n />\n </TabStopContext.Provider>\n );\n },\n);\n"],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@seekalfred/primitives",
3
- "version": "0.7.4",
3
+ "version": "0.8.0",
4
4
  "type": "module",
5
5
  "license": "MIT",
6
6
  "description": "Accessible, styled primitives built on React Aria Components.",
@@ -104,6 +104,14 @@
104
104
  "types": "./dist/chip-group/index.d.ts",
105
105
  "default": "./dist/chip-group/index.js"
106
106
  },
107
+ "./code-block": {
108
+ "types": "./dist/code-block/index.d.ts",
109
+ "default": "./dist/code-block/index.js"
110
+ },
111
+ "./command-palette": {
112
+ "types": "./dist/command-palette/index.d.ts",
113
+ "default": "./dist/command-palette/index.js"
114
+ },
107
115
  "./confirm-morph": {
108
116
  "types": "./dist/confirm-morph/index.d.ts",
109
117
  "default": "./dist/confirm-morph/index.js"
@@ -160,6 +168,10 @@
160
168
  "types": "./dist/gauge/index.d.ts",
161
169
  "default": "./dist/gauge/index.js"
162
170
  },
171
+ "./info-tooltip": {
172
+ "types": "./dist/info-tooltip/index.d.ts",
173
+ "default": "./dist/info-tooltip/index.js"
174
+ },
163
175
  "./input": {
164
176
  "types": "./dist/input/index.d.ts",
165
177
  "default": "./dist/input/index.js"
@@ -360,6 +372,10 @@
360
372
  "types": "./dist/toast/index.d.ts",
361
373
  "default": "./dist/toast/index.js"
362
374
  },
375
+ "./toggle-button": {
376
+ "types": "./dist/toggle-button/index.d.ts",
377
+ "default": "./dist/toggle-button/index.js"
378
+ },
363
379
  "./tooltip": {
364
380
  "types": "./dist/tooltip/index.d.ts",
365
381
  "default": "./dist/tooltip/index.js"
@@ -395,7 +411,7 @@
395
411
  "@lexical/rich-text": "0.52.0",
396
412
  "@lexical/selection": "0.52.0",
397
413
  "@lexical/utils": "0.52.0",
398
- "@seekalfred/tokens": "^0.7.4",
414
+ "@seekalfred/tokens": "^0.8.0",
399
415
  "class-variance-authority": "0.7.1",
400
416
  "clsx": "2.1.1",
401
417
  "lexical": "0.52.0",
@@ -404,7 +420,7 @@
404
420
  "devDependencies": {
405
421
  "@arethetypeswrong/cli": "0.18.5",
406
422
  "@internationalized/date": "3.12.4",
407
- "@seekalfred/styles": "^0.7.4",
423
+ "@seekalfred/styles": "^0.8.0",
408
424
  "@seekalfred/tsconfig": "0.0.0",
409
425
  "@size-limit/preset-small-lib": "13.0.3",
410
426
  "@storybook/react-vite": "10.6.0",