@misoto22/folio 0.17.0 → 0.17.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (149) hide show
  1. package/dist/agent/Accordion.md +1 -1
  2. package/dist/agent/Actions.md +1 -1
  3. package/dist/agent/Alert.md +1 -1
  4. package/dist/agent/AppShell.md +1 -1
  5. package/dist/agent/ArchitectureFigure.md +1 -1
  6. package/dist/agent/AreaChart.md +1 -1
  7. package/dist/agent/Article.md +1 -1
  8. package/dist/agent/AspectRatio.md +1 -1
  9. package/dist/agent/Avatar.md +1 -1
  10. package/dist/agent/Badge.md +1 -1
  11. package/dist/agent/BarChart.md +1 -1
  12. package/dist/agent/BarList.md +1 -1
  13. package/dist/agent/BigNumber.md +1 -1
  14. package/dist/agent/BoxPlot.md +1 -1
  15. package/dist/agent/Breadcrumb.md +1 -1
  16. package/dist/agent/BulletChart.md +1 -1
  17. package/dist/agent/Button.md +1 -1
  18. package/dist/agent/Calendar.md +1 -1
  19. package/dist/agent/CalendarHeatmap.md +1 -1
  20. package/dist/agent/Card.md +1 -1
  21. package/dist/agent/Checkbox.md +1 -1
  22. package/dist/agent/Code.md +1 -1
  23. package/dist/agent/CodeBlock.md +1 -1
  24. package/dist/agent/Collapsible.md +1 -1
  25. package/dist/agent/Collection.md +1 -1
  26. package/dist/agent/ColorPicker.md +1 -1
  27. package/dist/agent/Combobox.md +1 -1
  28. package/dist/agent/Command.md +1 -1
  29. package/dist/agent/ComposedChart.md +1 -1
  30. package/dist/agent/Contact.md +1 -1
  31. package/dist/agent/Content.md +1 -1
  32. package/dist/agent/ContextMenu.md +1 -1
  33. package/dist/agent/Conversation.md +1 -1
  34. package/dist/agent/DataflowFigure.md +1 -1
  35. package/dist/agent/DatePicker.md +1 -1
  36. package/dist/agent/DescriptionList.md +1 -1
  37. package/dist/agent/Diagram.md +1 -1
  38. package/dist/agent/DiagramCanvas.md +1 -1
  39. package/dist/agent/DiagramExportMenu.md +1 -1
  40. package/dist/agent/DiagramInspector.md +1 -1
  41. package/dist/agent/DiagramLegend.md +1 -1
  42. package/dist/agent/DiagramMinimap.md +1 -1
  43. package/dist/agent/DiagramToolbar.md +1 -1
  44. package/dist/agent/Dialog.md +1 -1
  45. package/dist/agent/DropdownMenu.md +1 -1
  46. package/dist/agent/EmptyState.md +1 -1
  47. package/dist/agent/ErrorState.md +1 -1
  48. package/dist/agent/Evidence.md +1 -1
  49. package/dist/agent/Facet.md +1 -1
  50. package/dist/agent/Field.md +1 -1
  51. package/dist/agent/FigureBand.md +1 -1
  52. package/dist/agent/FloatingIconButton.md +1 -1
  53. package/dist/agent/FunnelChart.md +1 -1
  54. package/dist/agent/Heading.md +1 -1
  55. package/dist/agent/Heatmap.md +1 -1
  56. package/dist/agent/Histogram.md +1 -1
  57. package/dist/agent/Input.md +1 -1
  58. package/dist/agent/Kbd.md +1 -1
  59. package/dist/agent/LifecycleFigure.md +1 -1
  60. package/dist/agent/LineChart.md +1 -1
  61. package/dist/agent/LinkArrow.md +1 -1
  62. package/dist/agent/Markdown.md +1 -1
  63. package/dist/agent/Media.md +1 -1
  64. package/dist/agent/Metric.md +1 -1
  65. package/dist/agent/Metrics.md +1 -1
  66. package/dist/agent/Music.md +1 -1
  67. package/dist/agent/NativeSelect.md +1 -1
  68. package/dist/agent/NavItem.md +1 -1
  69. package/dist/agent/NumberField.md +1 -1
  70. package/dist/agent/PageHeader.md +1 -1
  71. package/dist/agent/Pagination.md +1 -1
  72. package/dist/agent/PieChart.md +1 -1
  73. package/dist/agent/Popover.md +1 -1
  74. package/dist/agent/Portfolio.md +1 -1
  75. package/dist/agent/PortfolioIndex.md +1 -1
  76. package/dist/agent/Progress.md +1 -1
  77. package/dist/agent/RadarChart.md +1 -1
  78. package/dist/agent/RadialChart.md +1 -1
  79. package/dist/agent/RadioGroup.md +1 -1
  80. package/dist/agent/Reading.md +1 -1
  81. package/dist/agent/Recovery.md +1 -1
  82. package/dist/agent/SankeyChart.md +1 -1
  83. package/dist/agent/ScatterChart.md +1 -1
  84. package/dist/agent/ScrollArea.md +1 -1
  85. package/dist/agent/SearchPalette.md +1 -1
  86. package/dist/agent/SearchableMenu.md +1 -1
  87. package/dist/agent/Select.md +1 -1
  88. package/dist/agent/Separator.md +1 -1
  89. package/dist/agent/SequenceFigure.md +1 -1
  90. package/dist/agent/Sheet.md +1 -1
  91. package/dist/agent/Sidebar.md +1 -1
  92. package/dist/agent/SiteNavigation.md +1 -1
  93. package/dist/agent/SiteShell.md +1 -1
  94. package/dist/agent/Skeleton.md +1 -1
  95. package/dist/agent/Slider.md +1 -1
  96. package/dist/agent/Sparkline.md +1 -1
  97. package/dist/agent/Spinner.md +1 -1
  98. package/dist/agent/StatusDot.md +1 -1
  99. package/dist/agent/StatusPill.md +1 -1
  100. package/dist/agent/Steps.md +1 -1
  101. package/dist/agent/Switch.md +1 -1
  102. package/dist/agent/Table.md +1 -1
  103. package/dist/agent/Tabs.md +1 -1
  104. package/dist/agent/Tag.md +1 -1
  105. package/dist/agent/Text.md +1 -1
  106. package/dist/agent/Textarea.md +1 -1
  107. package/dist/agent/Timeline.md +1 -1
  108. package/dist/agent/Timestamp.md +1 -1
  109. package/dist/agent/Toast.md +1 -1
  110. package/dist/agent/ToggleGroup.md +1 -1
  111. package/dist/agent/Toolbar.md +1 -1
  112. package/dist/agent/Tooltip.md +1 -1
  113. package/dist/agent/TreemapChart.md +1 -1
  114. package/dist/agent/WaterfallChart.md +1 -1
  115. package/dist/agent/Workbench.md +1 -1
  116. package/dist/agent/WorkflowFigure.md +1 -1
  117. package/dist/agent/catalog.json +1 -1
  118. package/dist/agent/index.md +1 -1
  119. package/dist/article.css +1 -1
  120. package/dist/components/FigureBand/FigureBand.js +1 -1
  121. package/dist/components/FigureBand/FigureBand.js.map +1 -1
  122. package/dist/components/Pagination/Pagination.js +2 -2
  123. package/dist/components/Pagination/Pagination.js.map +1 -1
  124. package/dist/components/Table/Table.js +1 -1
  125. package/dist/components/Table/Table.js.map +1 -1
  126. package/dist/components/Tag/Tag.js +1 -1
  127. package/dist/components/Tag/Tag.js.map +1 -1
  128. package/dist/components/Text/Text.js +2 -2
  129. package/dist/components/Text/Text.js.map +1 -1
  130. package/dist/components/ToggleGroup/ToggleGroup.js +2 -2
  131. package/dist/components/ToggleGroup/ToggleGroup.js.map +1 -1
  132. package/dist/semantic.css +6 -4
  133. package/dist/styles.css +1 -1
  134. package/dist/themes.css +11 -5
  135. package/dist/tokens.css +13 -0
  136. package/dist/tokens.d.ts +2 -0
  137. package/dist/tokens.js +14 -1
  138. package/dist/tokens.json +14 -1
  139. package/dist/website-collection.css +2 -2
  140. package/dist/website-contact.css +9 -3
  141. package/dist/website-conversation.css +5 -5
  142. package/dist/website-evidence.css +1 -1
  143. package/dist/website-media.css +1 -1
  144. package/dist/website-metrics.css +1 -1
  145. package/dist/website-music.css +1 -1
  146. package/dist/website-reading.css +3 -3
  147. package/dist/website-recovery.css +1 -1
  148. package/dist/website-timeline.css +3 -3
  149. package/package.json +1 -1
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../src/components/Pagination/Pagination.tsx"],"sourcesContent":["'use client'\n\nimport { RiArrowLeftSLine, RiArrowRightSLine } from '@remixicon/react'\nimport type { HTMLAttributes } from 'react'\nimport { cn } from '../../lib/cn'\nimport { useSelectionIndicator } from '../../lib/useSelectionIndicator'\n\nexport interface PaginationProps extends Omit<HTMLAttributes<HTMLElement>, 'onChange'> {\n /** 1-based. */\n page: number\n /** Total pages. A value of 1 or less renders nothing. */\n pageCount: number\n onPageChange: (page: number) => void\n /** How many numbered pages sit either side of the current one. */\n siblings?: number\n label?: string\n /**\n * Names the two step controls.\n *\n * Each is a chevron and nothing else, so these strings are its entire\n * accessible name — the same reason `AppShell` exposes `openLabel` and\n * `closeLabel` rather than baking them in.\n */\n previousLabel?: string\n nextLabel?: string\n /**\n * Names one numbered page.\n *\n * A function rather than a template, because the number does not sit in the\n * same place in every language and neither does the noun around it —\n * `\"Page 3\"`, `\"第 3 页\"`, `\"Seite 3\"`. The visible digit is the same either\n * way; this is what a screen reader hears instead of a bare number.\n */\n pageLabel?: (page: number) => string\n}\n\nconst GAP = '…' as const\n\n/**\n * Builds the visible page list: always the first and last page, a window around\n * the current one, and an ellipsis wherever the sequence skips.\n *\n * Returned as numbers and a literal ellipsis rather than as pre-rendered nodes,\n * so the shape is testable without a DOM — the off-by-one at the window edges\n * is the whole difficulty of this component.\n */\nexport function paginationRange(page: number, pageCount: number, siblings = 1): (number | typeof GAP)[] {\n const first = 1\n const last = pageCount\n\n // Below the width the elided form would occupy — first, last, the window and\n // two ellipses — printing every page is both shorter and more useful. Without\n // this, a five-page list renders \"1 2 … 5\", which hides a page to save\n // nothing.\n const widest = 2 * siblings + 5\n if (pageCount <= widest) {\n return Array.from({ length: pageCount }, (_, index) => index + 1)\n }\n\n const start = Math.max(first, page - siblings)\n const end = Math.min(last, page + siblings)\n\n const pages: (number | typeof GAP)[] = []\n if (start > first) {\n pages.push(first)\n // A single skipped page is printed, not elided: \"1 … 3\" is longer than\n // \"1 2 3\" and tells the reader less.\n if (start > first + 2) pages.push(GAP)\n else if (start === first + 2) pages.push(first + 1)\n }\n for (let n = start; n <= end; n += 1) pages.push(n)\n if (end < last) {\n if (end < last - 2) pages.push(GAP)\n else if (end === last - 2) pages.push(last - 1)\n pages.push(last)\n }\n return pages\n}\n\nconst STEP =\n 'inline-flex size-(--control-h-sm) items-center justify-center rounded-(--radius-pill) border transition-colors duration-(--duration-fast) disabled:opacity-(--disabled-opacity) disabled:pointer-events-none'\n\n/**\n * Numbered pagination.\n *\n * The current page is marked by one filled pill that TRAVELS between the\n * numbers rather than by a background switching off on one and on on another.\n * Two backgrounds cross-fading reads as two things changing; a shape moving\n * reads as the one thing that actually did.\n *\n * A `<nav>` wrapping a list, and the current page is a `<button aria-current>`\n * rather than a styled `<span>` — a reader jumping by landmark needs to find\n * the control, and a reader on the current page needs to be told they are\n * already there.\n *\n * Renders nothing at one page or fewer. A pager for a single page is furniture.\n *\n * Every string a reader hears is a prop. The chevrons carry no text, so\n * `previousLabel` and `nextLabel` are the whole of those two controls' names,\n * and `pageLabel` is a function because \"Page 3\" is a phrase whose parts move\n * around between languages.\n *\n * @example\n * <Pagination page={page} pageCount={12} onPageChange={setPage} />\n */\nexport function Pagination({\n page,\n pageCount,\n onPageChange,\n siblings = 1,\n label = 'Pagination',\n previousLabel = 'Previous page',\n nextLabel = 'Next page',\n pageLabel = (page) => `Page ${page}`,\n className,\n ...rest\n}: PaginationProps) {\n // Called before the early return, because a hook cannot be conditional — and\n // keyed on the page so the pill re-measures when the elided window shifts and\n // the numbers under it change.\n const [listRef, indicator] = useSelectionIndicator<HTMLOListElement>(String(page))\n\n if (pageCount <= 1) return null\n const pages = paginationRange(page, pageCount, siblings)\n\n return (\n <nav aria-label={label} className={cn('flex items-center gap-1.5', className)} {...rest}>\n <button\n type=\"button\"\n aria-label={previousLabel}\n disabled={page <= 1}\n onClick={() => onPageChange(page - 1)}\n className={cn(STEP, 'border-(--rule-2) text-(--ink-2) hover:border-(--ink) hover:text-(--ink)')}\n >\n <RiArrowLeftSLine size={16} aria-hidden />\n </button>\n\n <ol ref={listRef} className=\"relative m-0 flex list-none items-center gap-1 p-0\">\n {indicator.ready && (\n <span\n aria-hidden\n data-folio-animated\n className=\"absolute rounded-(--radius-pill) bg-(--accent) transition-[transform,width] duration-(--duration-base) ease-(--ease-out-expo) motion-reduce:transition-none\"\n style={{\n transform: `translate(${indicator.offset}px, ${indicator.top}px)`,\n width: indicator.width,\n height: indicator.height,\n insetInlineStart: 0,\n top: 0,\n }}\n />\n )}\n {pages.map((entry, index) =>\n entry === GAP ? (\n <li key={`gap-${index}`} aria-hidden=\"true\" className=\"px-1 mono-meta text-(--ink-3-aa)\">\n {GAP}\n </li>\n ) : (\n <li key={entry} className=\"relative z-1\">\n <button\n type=\"button\"\n aria-current={entry === page ? 'page' : undefined}\n aria-label={pageLabel(entry)}\n data-indicator-active={entry === page ? 'true' : undefined}\n onClick={() => onPageChange(entry)}\n className={cn(\n STEP,\n 'mono-meta tabular-nums',\n entry === page\n ? 'border-transparent text-(--accent-foreground)'\n : 'border-transparent text-(--ink-2) hover:border-(--rule-2) hover:text-(--ink)',\n )}\n >\n {entry}\n </button>\n </li>\n ),\n )}\n </ol>\n\n <button\n type=\"button\"\n aria-label={nextLabel}\n disabled={page >= pageCount}\n onClick={() => onPageChange(page + 1)}\n className={cn(STEP, 'border-(--rule-2) text-(--ink-2) hover:border-(--ink) hover:text-(--ink)')}\n >\n <RiArrowRightSLine size={16} aria-hidden />\n </button>\n </nav>\n )\n}\n\nexport default Pagination\n"],"mappings":"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","names":["page"]}
1
+ {"version":3,"sources":["../../../src/components/Pagination/Pagination.tsx"],"sourcesContent":["'use client'\n\nimport { RiArrowLeftSLine, RiArrowRightSLine } from '@remixicon/react'\nimport type { HTMLAttributes } from 'react'\nimport { cn } from '../../lib/cn'\nimport { useSelectionIndicator } from '../../lib/useSelectionIndicator'\n\nexport interface PaginationProps extends Omit<HTMLAttributes<HTMLElement>, 'onChange'> {\n /** 1-based. */\n page: number\n /** Total pages. A value of 1 or less renders nothing. */\n pageCount: number\n onPageChange: (page: number) => void\n /** How many numbered pages sit either side of the current one. */\n siblings?: number\n label?: string\n /**\n * Names the two step controls.\n *\n * Each is a chevron and nothing else, so these strings are its entire\n * accessible name — the same reason `AppShell` exposes `openLabel` and\n * `closeLabel` rather than baking them in.\n */\n previousLabel?: string\n nextLabel?: string\n /**\n * Names one numbered page.\n *\n * A function rather than a template, because the number does not sit in the\n * same place in every language and neither does the noun around it —\n * `\"Page 3\"`, `\"第 3 页\"`, `\"Seite 3\"`. The visible digit is the same either\n * way; this is what a screen reader hears instead of a bare number.\n */\n pageLabel?: (page: number) => string\n}\n\nconst GAP = '…' as const\n\n/**\n * Builds the visible page list: always the first and last page, a window around\n * the current one, and an ellipsis wherever the sequence skips.\n *\n * Returned as numbers and a literal ellipsis rather than as pre-rendered nodes,\n * so the shape is testable without a DOM — the off-by-one at the window edges\n * is the whole difficulty of this component.\n */\nexport function paginationRange(page: number, pageCount: number, siblings = 1): (number | typeof GAP)[] {\n const first = 1\n const last = pageCount\n\n // Below the width the elided form would occupy — first, last, the window and\n // two ellipses — printing every page is both shorter and more useful. Without\n // this, a five-page list renders \"1 2 … 5\", which hides a page to save\n // nothing.\n const widest = 2 * siblings + 5\n if (pageCount <= widest) {\n return Array.from({ length: pageCount }, (_, index) => index + 1)\n }\n\n const start = Math.max(first, page - siblings)\n const end = Math.min(last, page + siblings)\n\n const pages: (number | typeof GAP)[] = []\n if (start > first) {\n pages.push(first)\n // A single skipped page is printed, not elided: \"1 … 3\" is longer than\n // \"1 2 3\" and tells the reader less.\n if (start > first + 2) pages.push(GAP)\n else if (start === first + 2) pages.push(first + 1)\n }\n for (let n = start; n <= end; n += 1) pages.push(n)\n if (end < last) {\n if (end < last - 2) pages.push(GAP)\n else if (end === last - 2) pages.push(last - 1)\n pages.push(last)\n }\n return pages\n}\n\nconst STEP =\n 'inline-flex size-(--control-h-sm) items-center justify-center rounded-(--radius-pill) border transition-colors duration-(--duration-fast) disabled:opacity-(--disabled-opacity) disabled:pointer-events-none'\n\n/**\n * Numbered pagination.\n *\n * The current page is marked by one filled pill that TRAVELS between the\n * numbers rather than by a background switching off on one and on on another.\n * Two backgrounds cross-fading reads as two things changing; a shape moving\n * reads as the one thing that actually did.\n *\n * A `<nav>` wrapping a list, and the current page is a `<button aria-current>`\n * rather than a styled `<span>` — a reader jumping by landmark needs to find\n * the control, and a reader on the current page needs to be told they are\n * already there.\n *\n * Renders nothing at one page or fewer. A pager for a single page is furniture.\n *\n * Every string a reader hears is a prop. The chevrons carry no text, so\n * `previousLabel` and `nextLabel` are the whole of those two controls' names,\n * and `pageLabel` is a function because \"Page 3\" is a phrase whose parts move\n * around between languages.\n *\n * @example\n * <Pagination page={page} pageCount={12} onPageChange={setPage} />\n */\nexport function Pagination({\n page,\n pageCount,\n onPageChange,\n siblings = 1,\n label = 'Pagination',\n previousLabel = 'Previous page',\n nextLabel = 'Next page',\n pageLabel = (page) => `Page ${page}`,\n className,\n ...rest\n}: PaginationProps) {\n // Called before the early return, because a hook cannot be conditional — and\n // keyed on the page so the pill re-measures when the elided window shifts and\n // the numbers under it change.\n const [listRef, indicator] = useSelectionIndicator<HTMLOListElement>(String(page))\n\n if (pageCount <= 1) return null\n const pages = paginationRange(page, pageCount, siblings)\n\n return (\n <nav aria-label={label} className={cn('flex items-center gap-1.5', className)} {...rest}>\n <button\n type=\"button\"\n aria-label={previousLabel}\n disabled={page <= 1}\n onClick={() => onPageChange(page - 1)}\n className={cn(STEP, 'border-(--rule-2) text-(--ink-2) hover:border-(--ink) hover:text-(--ink)')}\n >\n <RiArrowLeftSLine size={16} aria-hidden />\n </button>\n\n <ol ref={listRef} className=\"relative m-0 flex list-none items-center gap-1 p-0\">\n {indicator.ready && (\n <span\n aria-hidden\n data-folio-animated\n className=\"absolute rounded-(--radius-pill) bg-(--action) transition-[transform,width] duration-(--duration-base) ease-(--ease-out-expo) motion-reduce:transition-none\"\n style={{\n transform: `translate(${indicator.offset}px, ${indicator.top}px)`,\n width: indicator.width,\n height: indicator.height,\n insetInlineStart: 0,\n top: 0,\n }}\n />\n )}\n {pages.map((entry, index) =>\n entry === GAP ? (\n <li key={`gap-${index}`} aria-hidden=\"true\" className=\"px-1 mono-meta text-(--ink-3-aa)\">\n {GAP}\n </li>\n ) : (\n <li key={entry} className=\"relative z-1\">\n <button\n type=\"button\"\n aria-current={entry === page ? 'page' : undefined}\n aria-label={pageLabel(entry)}\n data-indicator-active={entry === page ? 'true' : undefined}\n onClick={() => onPageChange(entry)}\n className={cn(\n STEP,\n 'mono-meta tabular-nums',\n entry === page\n ? 'border-transparent text-(--action-foreground)'\n : 'border-transparent text-(--ink-2) hover:border-(--rule-2) hover:text-(--ink)',\n )}\n >\n {entry}\n </button>\n </li>\n ),\n )}\n </ol>\n\n <button\n type=\"button\"\n aria-label={nextLabel}\n disabled={page >= pageCount}\n onClick={() => onPageChange(page + 1)}\n className={cn(STEP, 'border-(--rule-2) text-(--ink-2) hover:border-(--ink) hover:text-(--ink)')}\n >\n <RiArrowRightSLine size={16} aria-hidden />\n </button>\n </nav>\n )\n}\n\nexport default Pagination\n"],"mappings":"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","names":["page"]}
@@ -7,7 +7,7 @@ const ALIGN = {
7
7
  center: "text-center",
8
8
  end: "text-end"
9
9
  };
10
- const ROWS = "[&_thead]:border-b [&_thead]:border-(--rule-hard) [&_tbody_tr]:border-b [&_tbody_tr]:border-(--rule) [&_tbody_tr:last-child]:border-b-0";
10
+ const ROWS = "[&_thead]:border-b [&_thead]:border-(--rule-divider) [&_tbody_tr]:border-b [&_tbody_tr]:border-(--rule) [&_tbody_tr:last-child]:border-b-0";
11
11
  const SOFT_HEAD = "[&_thead]:border-(--rule-2)";
12
12
  const COLUMNS = "[&_th]:border-e [&_td]:border-e [&_th]:border-(--rule) [&_td]:border-(--rule) [&_th:last-child]:border-e-0 [&_td:last-child]:border-e-0";
13
13
  const BOXED = "rounded-(--radius) border border-(--rule-2) [--table-pad-x:0.75rem]";
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../src/components/Table/Table.tsx"],"sourcesContent":["import { RiArrowDownLine, RiArrowUpLine, RiExpandUpDownLine } from '@remixicon/react'\nimport type { HTMLAttributes, TableHTMLAttributes, TdHTMLAttributes, ThHTMLAttributes } from 'react'\nimport { cn } from '../../lib/cn'\nimport { warnBlankName } from '../../lib/warn'\n\nexport type TableAlign = 'start' | 'center' | 'end'\n\n/**\n * Which rules the table draws.\n *\n * `rows` is the default and the right answer for reading down a column: one\n * hairline between records and nothing else, so the eye tracks a line across\n * without a grid competing for it. `grid` adds the vertical rules a dense\n * numeric table needs to keep columns apart. `bordered` puts an edge around the\n * whole thing, for a table that sits loose on a page rather than inside a card\n * that already has one. `none` is for a table inside something that draws its\n * own structure.\n */\nexport type TableBorders = 'rows' | 'grid' | 'bordered' | 'bordered-grid' | 'none'\n\nconst ALIGN: Record<TableAlign, string> = {\n start: 'text-start',\n center: 'text-center',\n end: 'text-end',\n}\n\n/**\n * Every rule the table draws, expressed from ONE element.\n *\n * Not React context, which was the first attempt: `createContext` makes the\n * module client-only, which turned every table on every page into a hydration\n * boundary and took the static build down with it. Descendant selectors from\n * the wrapper reach the same cells, cost nothing at runtime, and keep the whole\n * component server-renderable.\n *\n * `--table-pad-x` rather than a padding class per mode, because the ruled\n * variants need symmetric padding inside their boxes while the plain one wants\n * a trailing gap and no leading one.\n */\nconst ROWS =\n '[&_thead]:border-b [&_thead]:border-(--rule-hard) [&_tbody_tr]:border-b [&_tbody_tr]:border-(--rule) [&_tbody_tr:last-child]:border-b-0'\nconst SOFT_HEAD = '[&_thead]:border-(--rule-2)'\nconst COLUMNS =\n '[&_th]:border-e [&_td]:border-e [&_th]:border-(--rule) [&_td]:border-(--rule) [&_th:last-child]:border-e-0 [&_td:last-child]:border-e-0'\nconst BOXED = 'rounded-(--radius) border border-(--rule-2) [--table-pad-x:0.75rem]'\n\nconst BORDERS: Record<TableBorders, string> = {\n rows: ROWS,\n grid: cn(ROWS, COLUMNS, '[--table-pad-x:0.75rem]'),\n bordered: cn(ROWS, SOFT_HEAD, BOXED),\n 'bordered-grid': cn(ROWS, SOFT_HEAD, COLUMNS, BOXED),\n none: '',\n}\n\nexport interface TableProps extends TableHTMLAttributes<HTMLTableElement> {\n /**\n * Describes the table for a screen reader, which cannot see the heading above\n * it. Rendered as a visually-hidden `<caption>` — required, because an\n * unnamed table in a page with three tables is unnavigable.\n */\n caption: string\n /** Prints the caption instead of hiding it. */\n showCaption?: boolean\n /**\n * Pins the header row while the body scrolls. Needs a bounded height on the\n * container — otherwise the page scrolls, not the table, and nothing sticks.\n */\n stickyHeader?: boolean\n /** Which rules to draw. See {@link TableBorders}. */\n borders?: TableBorders\n /** Tightens the row padding, for a table that is mostly numbers. */\n density?: 'comfortable' | 'compact'\n}\n\n/**\n * A ruled data table.\n *\n * Wrapped in its own horizontally scrolling container, because a table is the\n * one block that legitimately exceeds the measure — and a page that scrolls\n * sideways as a whole is a layout bug, while a table that does is a table.\n *\n * That container is focusable, and it has to be. A scrollable region whose\n * contents are not themselves focusable is unreachable by keyboard: there is no\n * element to Tab to and therefore no way to press an arrow key at it, so the\n * columns past the fold simply do not exist for anyone not using a mouse.\n *\n * It is also POSITIONED, which is not decoration. `sr-only` is\n * `position: absolute`, so a visually-hidden label in a cell resolves against\n * the nearest positioned ancestor — and with none between it and the document,\n * it escapes the scroll container and every `overflow-hidden` around it, and\n * widens the page by however far the table happens to be scrolled.\n *\n * No zebra striping at any border setting. In a monochrome system a striped row\n * is a second surface competing with the page ground, and the hairline between\n * rows is already enough to track a line across.\n *\n * @example\n * <Table caption=\"Deploy history\">\n * <THead><TR><TH>Commit</TH><TH align=\"end\" sortable sortDirection={dir} onSort={sort}>Duration</TH></TR></THead>\n * <TBody><TR><TD>a1b2c3d</TD><TD align=\"end\">2m 14s</TD></TR></TBody>\n * </Table>\n * @example\n * <Table caption=\"Inventory\" borders=\"bordered-grid\" density=\"compact\">…</Table>\n */\nexport function Table({\n caption,\n showCaption = false,\n stickyHeader = false,\n borders = 'rows',\n density = 'comfortable',\n className,\n children,\n ...rest\n}: TableProps) {\n warnBlankName('Table', 'caption', caption, 'the table is announced with no name and a reader arrives in it with no idea what it lists')\n return (\n <div\n tabIndex={0}\n role=\"region\"\n aria-label={caption}\n data-table-density={density}\n className={cn('relative w-full overflow-x-auto scroll-slim', BORDERS[borders])}\n >\n <table\n className={cn(\n 'w-full border-collapse text-start text-sm',\n stickyHeader &&\n '[&_thead]:sticky [&_thead]:top-0 [&_thead]:z-(--z-rule) [&_thead]:bg-(--paper)',\n className,\n )}\n {...rest}\n >\n <caption className={cn(showCaption ? 'pb-3 text-start eyebrow text-(--ink-3-aa)' : 'sr-only')}>\n {caption}\n </caption>\n {children}\n </table>\n </div>\n )\n}\n\n/** Header group. The rule under it is drawn by the table's border setting. */\nexport function THead({ className, ...rest }: HTMLAttributes<HTMLTableSectionElement>) {\n return <thead className={className} {...rest} />\n}\n\nexport function TBody({ className, ...rest }: HTMLAttributes<HTMLTableSectionElement>) {\n return <tbody className={className} {...rest} />\n}\n\nexport function TR({ className, ...rest }: HTMLAttributes<HTMLTableRowElement>) {\n return <tr className={className} {...rest} />\n}\n\nexport type SortDirection = 'ascending' | 'descending' | 'none'\n\nexport interface THProps extends Omit<ThHTMLAttributes<HTMLTableCellElement>, 'align'> {\n /** Which edge the column's contents sit against. Numbers belong at `end`. */\n align?: TableAlign\n /** Makes the label a button and shows the sort marker. */\n sortable?: boolean\n /**\n * Which way this column is sorted. Also set as `aria-sort`, which is the\n * only way a screen reader learns a table is ordered at all — a caret drawn\n * in the header tells it nothing.\n */\n sortDirection?: SortDirection\n onSort?: () => void\n}\n\n/**\n * Cell padding, in one string both cells share.\n *\n * `--table-pad-x` is 0 by default and set by the ruled variants, so the plain\n * table keeps its trailing gap and no leading one while a boxed table gets\n * symmetric padding inside its border.\n */\nconst CELL =\n 'px-[var(--table-pad-x,0)] [&:not(:last-child)]:pe-[calc(var(--table-pad-x,0px)+1.5rem)]'\n\nconst SORT_ICON = {\n ascending: RiArrowUpLine,\n descending: RiArrowDownLine,\n none: RiExpandUpDownLine,\n} as const\n\n/**\n * A column label. Mono and uppercase, so it never reads as data.\n *\n * When sortable, the label becomes a `<button>` INSIDE the `<th>` rather than\n * the `<th>` becoming clickable: a cell with a click handler is not focusable\n * and not announced as a control, so the sort exists only for a mouse.\n *\n * Sorting is per column and opt-in. A table where every header is a button\n * invites the reader to try sorting a column the data cannot be ordered by.\n */\nexport function TH({\n align = 'start',\n sortable = false,\n sortDirection = 'none',\n onSort,\n className,\n children,\n ...rest\n}: THProps) {\n const Icon = SORT_ICON[sortDirection]\n\n return (\n <th\n scope=\"col\"\n aria-sort={sortable ? sortDirection : undefined}\n className={cn(CELL, 'py-3 align-bottom eyebrow text-(--ink-3-aa)', ALIGN[align], className)}\n {...rest}\n >\n {sortable ? (\n <button\n type=\"button\"\n onClick={onSort}\n className={cn(\n 'group inline-flex items-center gap-1.5 eyebrow transition-colors duration-(--duration-fast) hover:text-(--ink)',\n sortDirection === 'none' ? 'text-(--ink-3-aa)' : 'text-(--ink)',\n )}\n >\n {children}\n <Icon\n size={12}\n aria-hidden\n className={cn(\n 'shrink-0 transition-opacity',\n sortDirection === 'none' && 'opacity-40 group-hover:opacity-100',\n )}\n />\n </button>\n ) : (\n children\n )}\n </th>\n )\n}\n\nexport interface TDProps extends Omit<TdHTMLAttributes<HTMLTableCellElement>, 'align'> {\n /** Which edge the cell's contents sit against. Match the column's header. */\n align?: TableAlign\n}\n\n/**\n * A body cell.\n *\n * TOP-aligned, and that is a choice about which kind of table is the common\n * one here: a cell holding a paragraph. Top is what lines the first lines of a\n * row up with each other, and centring a two-word term against a four-line\n * description puts the term in the middle of nothing.\n *\n * It is the wrong default for the other kind — a row of one-line values with a\n * control in it. A 36px button next to 16px of text makes a 52px row, and every\n * other cell then hangs at the top of it with twenty pixels of nothing\n * underneath, which reads as a column that has slipped. Pass\n * `className=\"align-middle\"` on those rows; the data-table template does.\n */\nexport function TD({ align = 'start', className, ...rest }: TDProps) {\n return (\n <td\n className={cn(CELL, 'py-[var(--table-pad-y)] align-top text-(--ink-2)', ALIGN[align], className)}\n {...rest}\n />\n )\n}\n\nexport default Table\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../../../src/components/Table/Table.tsx"],"sourcesContent":["import { RiArrowDownLine, RiArrowUpLine, RiExpandUpDownLine } from '@remixicon/react'\nimport type { HTMLAttributes, TableHTMLAttributes, TdHTMLAttributes, ThHTMLAttributes } from 'react'\nimport { cn } from '../../lib/cn'\nimport { warnBlankName } from '../../lib/warn'\n\nexport type TableAlign = 'start' | 'center' | 'end'\n\n/**\n * Which rules the table draws.\n *\n * `rows` is the default and the right answer for reading down a column: one\n * hairline between records and nothing else, so the eye tracks a line across\n * without a grid competing for it. `grid` adds the vertical rules a dense\n * numeric table needs to keep columns apart. `bordered` puts an edge around the\n * whole thing, for a table that sits loose on a page rather than inside a card\n * that already has one. `none` is for a table inside something that draws its\n * own structure.\n */\nexport type TableBorders = 'rows' | 'grid' | 'bordered' | 'bordered-grid' | 'none'\n\nconst ALIGN: Record<TableAlign, string> = {\n start: 'text-start',\n center: 'text-center',\n end: 'text-end',\n}\n\n/**\n * Every rule the table draws, expressed from ONE element.\n *\n * Not React context, which was the first attempt: `createContext` makes the\n * module client-only, which turned every table on every page into a hydration\n * boundary and took the static build down with it. Descendant selectors from\n * the wrapper reach the same cells, cost nothing at runtime, and keep the whole\n * component server-renderable.\n *\n * `--table-pad-x` rather than a padding class per mode, because the ruled\n * variants need symmetric padding inside their boxes while the plain one wants\n * a trailing gap and no leading one.\n */\nconst ROWS =\n '[&_thead]:border-b [&_thead]:border-(--rule-divider) [&_tbody_tr]:border-b [&_tbody_tr]:border-(--rule) [&_tbody_tr:last-child]:border-b-0'\nconst SOFT_HEAD = '[&_thead]:border-(--rule-2)'\nconst COLUMNS =\n '[&_th]:border-e [&_td]:border-e [&_th]:border-(--rule) [&_td]:border-(--rule) [&_th:last-child]:border-e-0 [&_td:last-child]:border-e-0'\nconst BOXED = 'rounded-(--radius) border border-(--rule-2) [--table-pad-x:0.75rem]'\n\nconst BORDERS: Record<TableBorders, string> = {\n rows: ROWS,\n grid: cn(ROWS, COLUMNS, '[--table-pad-x:0.75rem]'),\n bordered: cn(ROWS, SOFT_HEAD, BOXED),\n 'bordered-grid': cn(ROWS, SOFT_HEAD, COLUMNS, BOXED),\n none: '',\n}\n\nexport interface TableProps extends TableHTMLAttributes<HTMLTableElement> {\n /**\n * Describes the table for a screen reader, which cannot see the heading above\n * it. Rendered as a visually-hidden `<caption>` — required, because an\n * unnamed table in a page with three tables is unnavigable.\n */\n caption: string\n /** Prints the caption instead of hiding it. */\n showCaption?: boolean\n /**\n * Pins the header row while the body scrolls. Needs a bounded height on the\n * container — otherwise the page scrolls, not the table, and nothing sticks.\n */\n stickyHeader?: boolean\n /** Which rules to draw. See {@link TableBorders}. */\n borders?: TableBorders\n /** Tightens the row padding, for a table that is mostly numbers. */\n density?: 'comfortable' | 'compact'\n}\n\n/**\n * A ruled data table.\n *\n * Wrapped in its own horizontally scrolling container, because a table is the\n * one block that legitimately exceeds the measure — and a page that scrolls\n * sideways as a whole is a layout bug, while a table that does is a table.\n *\n * That container is focusable, and it has to be. A scrollable region whose\n * contents are not themselves focusable is unreachable by keyboard: there is no\n * element to Tab to and therefore no way to press an arrow key at it, so the\n * columns past the fold simply do not exist for anyone not using a mouse.\n *\n * It is also POSITIONED, which is not decoration. `sr-only` is\n * `position: absolute`, so a visually-hidden label in a cell resolves against\n * the nearest positioned ancestor — and with none between it and the document,\n * it escapes the scroll container and every `overflow-hidden` around it, and\n * widens the page by however far the table happens to be scrolled.\n *\n * No zebra striping at any border setting. In a monochrome system a striped row\n * is a second surface competing with the page ground, and the hairline between\n * rows is already enough to track a line across.\n *\n * @example\n * <Table caption=\"Deploy history\">\n * <THead><TR><TH>Commit</TH><TH align=\"end\" sortable sortDirection={dir} onSort={sort}>Duration</TH></TR></THead>\n * <TBody><TR><TD>a1b2c3d</TD><TD align=\"end\">2m 14s</TD></TR></TBody>\n * </Table>\n * @example\n * <Table caption=\"Inventory\" borders=\"bordered-grid\" density=\"compact\">…</Table>\n */\nexport function Table({\n caption,\n showCaption = false,\n stickyHeader = false,\n borders = 'rows',\n density = 'comfortable',\n className,\n children,\n ...rest\n}: TableProps) {\n warnBlankName('Table', 'caption', caption, 'the table is announced with no name and a reader arrives in it with no idea what it lists')\n return (\n <div\n tabIndex={0}\n role=\"region\"\n aria-label={caption}\n data-table-density={density}\n className={cn('relative w-full overflow-x-auto scroll-slim', BORDERS[borders])}\n >\n <table\n className={cn(\n 'w-full border-collapse text-start text-sm',\n stickyHeader &&\n '[&_thead]:sticky [&_thead]:top-0 [&_thead]:z-(--z-rule) [&_thead]:bg-(--paper)',\n className,\n )}\n {...rest}\n >\n <caption className={cn(showCaption ? 'pb-3 text-start eyebrow text-(--ink-3-aa)' : 'sr-only')}>\n {caption}\n </caption>\n {children}\n </table>\n </div>\n )\n}\n\n/** Header group. The rule under it is drawn by the table's border setting. */\nexport function THead({ className, ...rest }: HTMLAttributes<HTMLTableSectionElement>) {\n return <thead className={className} {...rest} />\n}\n\nexport function TBody({ className, ...rest }: HTMLAttributes<HTMLTableSectionElement>) {\n return <tbody className={className} {...rest} />\n}\n\nexport function TR({ className, ...rest }: HTMLAttributes<HTMLTableRowElement>) {\n return <tr className={className} {...rest} />\n}\n\nexport type SortDirection = 'ascending' | 'descending' | 'none'\n\nexport interface THProps extends Omit<ThHTMLAttributes<HTMLTableCellElement>, 'align'> {\n /** Which edge the column's contents sit against. Numbers belong at `end`. */\n align?: TableAlign\n /** Makes the label a button and shows the sort marker. */\n sortable?: boolean\n /**\n * Which way this column is sorted. Also set as `aria-sort`, which is the\n * only way a screen reader learns a table is ordered at all — a caret drawn\n * in the header tells it nothing.\n */\n sortDirection?: SortDirection\n onSort?: () => void\n}\n\n/**\n * Cell padding, in one string both cells share.\n *\n * `--table-pad-x` is 0 by default and set by the ruled variants, so the plain\n * table keeps its trailing gap and no leading one while a boxed table gets\n * symmetric padding inside its border.\n */\nconst CELL =\n 'px-[var(--table-pad-x,0)] [&:not(:last-child)]:pe-[calc(var(--table-pad-x,0px)+1.5rem)]'\n\nconst SORT_ICON = {\n ascending: RiArrowUpLine,\n descending: RiArrowDownLine,\n none: RiExpandUpDownLine,\n} as const\n\n/**\n * A column label. Mono and uppercase, so it never reads as data.\n *\n * When sortable, the label becomes a `<button>` INSIDE the `<th>` rather than\n * the `<th>` becoming clickable: a cell with a click handler is not focusable\n * and not announced as a control, so the sort exists only for a mouse.\n *\n * Sorting is per column and opt-in. A table where every header is a button\n * invites the reader to try sorting a column the data cannot be ordered by.\n */\nexport function TH({\n align = 'start',\n sortable = false,\n sortDirection = 'none',\n onSort,\n className,\n children,\n ...rest\n}: THProps) {\n const Icon = SORT_ICON[sortDirection]\n\n return (\n <th\n scope=\"col\"\n aria-sort={sortable ? sortDirection : undefined}\n className={cn(CELL, 'py-3 align-bottom eyebrow text-(--ink-3-aa)', ALIGN[align], className)}\n {...rest}\n >\n {sortable ? (\n <button\n type=\"button\"\n onClick={onSort}\n className={cn(\n 'group inline-flex items-center gap-1.5 eyebrow transition-colors duration-(--duration-fast) hover:text-(--ink)',\n sortDirection === 'none' ? 'text-(--ink-3-aa)' : 'text-(--ink)',\n )}\n >\n {children}\n <Icon\n size={12}\n aria-hidden\n className={cn(\n 'shrink-0 transition-opacity',\n sortDirection === 'none' && 'opacity-40 group-hover:opacity-100',\n )}\n />\n </button>\n ) : (\n children\n )}\n </th>\n )\n}\n\nexport interface TDProps extends Omit<TdHTMLAttributes<HTMLTableCellElement>, 'align'> {\n /** Which edge the cell's contents sit against. Match the column's header. */\n align?: TableAlign\n}\n\n/**\n * A body cell.\n *\n * TOP-aligned, and that is a choice about which kind of table is the common\n * one here: a cell holding a paragraph. Top is what lines the first lines of a\n * row up with each other, and centring a two-word term against a four-line\n * description puts the term in the middle of nothing.\n *\n * It is the wrong default for the other kind — a row of one-line values with a\n * control in it. A 36px button next to 16px of text makes a 52px row, and every\n * other cell then hangs at the top of it with twenty pixels of nothing\n * underneath, which reads as a column that has slipped. Pass\n * `className=\"align-middle\"` on those rows; the data-table template does.\n */\nexport function TD({ align = 'start', className, ...rest }: TDProps) {\n return (\n <td\n className={cn(CELL, 'py-[var(--table-pad-y)] align-top text-(--ink-2)', ALIGN[align], className)}\n {...rest}\n />\n )\n}\n\nexport default Table\n"],"mappings":"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","names":[]}
@@ -5,7 +5,7 @@ function Tag({ children, active, onClick, onRemove, removeLabel, className, ...r
5
5
  const split = Boolean(onClick && onRemove);
6
6
  const chip = cn(
7
7
  "inline-flex items-center rounded-(--radius-sm) font-(family-name:--label-font) text-xs tracking-[calc(0.025em*var(--label-tracking-scale))] transition-colors duration-(--duration-fast)",
8
- active ? "bg-(--accent) text-(--accent-foreground)" : "bg-(--stone) text-(--ink-3-aa)",
8
+ active ? "bg-(--action) text-(--action-foreground)" : "bg-(--stone) text-(--ink-3-aa)",
9
9
  onRemove ? "gap-1 pe-1.5" : "pe-2.5",
10
10
  // Handed to the label button in the split form, so the padding is part of
11
11
  // the target rather than a ring of dead space around it.
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../src/components/Tag/Tag.tsx"],"sourcesContent":["import { RiCloseLine } from '@remixicon/react'\nimport type { HTMLAttributes, ReactNode } from 'react'\nimport { cn } from '../../lib/cn'\n\ninterface TagBase extends Omit<HTMLAttributes<HTMLElement>, 'onClick'> {\n children: ReactNode\n /**\n * Renders the pressed/selected look, and declares the chip a toggle.\n *\n * An interactive chip reads `aria-pressed` off this same value, so the accent\n * fill and the state a screen reader hears cannot drift apart. Leave it off\n * for a chip whose `onClick` navigates or opens something: a control that is\n * neither pressed nor unpressed is better announced as neither than as\n * \"not pressed\".\n */\n active?: boolean\n /**\n * Makes the chip ITSELF the filter control.\n *\n * This prop exists because the alternative did not survive contact with\n * `onRemove`. The advice used to be to wrap the tag in a button at the call\n * site, which is fine on its own and invalid the moment the chip is also\n * removable: the remove control is a real `<button>`, so the wrapper puts a\n * button inside a button — markup the parser splits into siblings, leaving a\n * DOM neither the author nor the accessibility tree expects.\n *\n * With this the wrapper is never written. The chip carries the click, the\n * focus ring and the pressed state on the element that draws them, and a\n * removable filter chip renders its two controls side by side instead of one\n * inside the other.\n */\n onClick?: () => void\n}\n\n/**\n * The removable form: a chip for one thing the reader has already chosen.\n *\n * `removeLabel` is required alongside `onRemove` and not optional with a\n * default, because \"Remove\" repeated down a row of filters is eight controls a\n * screen reader cannot tell apart. Write the subject into it — \"Remove\n * TypeScript filter\".\n */\ninterface TagRemovable {\n /** Called when the reader dismisses the chip. */\n onRemove: () => void\n /** The remove button's accessible name. Name the subject, not the action. */\n removeLabel: string\n}\n\ninterface TagStatic {\n onRemove?: never\n removeLabel?: never\n}\n\nexport type TagProps = TagBase & (TagRemovable | TagStatic)\n\n/**\n * A subject label — a topic, a technology, a filter facet — that can filter\n * with `onClick` and be dismissed with `onRemove`.\n *\n * Distinct from `Badge`, which carries a state or a count. A tag names what\n * something is ABOUT, so several sit together in a row and the reader scans\n * them; a badge is one fact about one record.\n *\n * This is where a `Token` component would have gone. It was not built: a token\n * is a tag with a remove button, and the difference between the two is one\n * prop, not one component. The system already ships three things that look\n * alike — `Badge`, `Tag`, `StatusPill` — and a fourth whose whole distinction\n * is an X on the end would be the one a call site picks by coin toss.\n *\n * Presentational until it is given a handler, and the component owns both\n * interactive cases rather than leaving one to a wrapper at the call site.\n * That is not a convenience: a wrapping `<button>` around a chip that already\n * holds the remove `<button>` is a button inside a button, which no parser\n * keeps and no accessibility tree reports the way it was written. Given both,\n * the chip renders the label and the X as SIBLING buttons — the label takes\n * the leading padding with it, so it is the whole of the chip up to the X\n * rather than the words with dead padding around them.\n *\n * @example\n * <Tag>TypeScript</Tag>\n * @example\n * <Tag active={on} onClick={toggle}>Rust</Tag>\n * @example\n * <Tag onRemove={() => drop('rust')} removeLabel=\"Remove Rust filter\">Rust</Tag>\n * @example\n * // Both: a filter the reader can toggle and also take out of the row.\n * <Tag active={on} onClick={toggle} onRemove={drop} removeLabel=\"Remove Rust filter\">Rust</Tag>\n */\nexport function Tag({ children, active, onClick, onRemove, removeLabel, className, ...rest }: TagProps) {\n // Two controls cannot nest, so a chip that is both a filter and a selection\n // splits its label out into a button of its own beside the remove button.\n const split = Boolean(onClick && onRemove)\n\n const chip = cn(\n 'inline-flex items-center rounded-(--radius-sm) font-(family-name:--label-font) text-xs tracking-[calc(0.025em*var(--label-tracking-scale))] transition-colors duration-(--duration-fast)',\n active ? 'bg-(--accent) text-(--accent-foreground)' : 'bg-(--stone) text-(--ink-3-aa)',\n onRemove ? 'gap-1 pe-1.5' : 'pe-2.5',\n // Handed to the label button in the split form, so the padding is part of\n // the target rather than a ring of dead space around it.\n !split && 'py-1 ps-2.5',\n className,\n )\n\n const remove = onRemove ? (\n <button\n type=\"button\"\n onClick={onRemove}\n aria-label={removeLabel}\n className={cn(\n // The drawn box is 16px, which is under the 24px minimum a pointer\n // target has to clear (WCAG 2.5.8). The inset pseudo-element takes\n // the hit area out to 24 without changing what is drawn or pushing\n // the chips apart — the 44px floor is for a standalone control, and\n // a chip is 24 tall to begin with.\n 'relative inline-flex size-4 shrink-0 items-center justify-center rounded-(--radius-xs)',\n \"before:absolute before:inset-[-4px] before:content-['']\",\n 'opacity-60 transition-opacity duration-(--duration-fast) hover:opacity-100 focus-visible:opacity-100',\n )}\n >\n <RiCloseLine size={12} aria-hidden />\n </button>\n ) : null\n\n if (split) {\n return (\n <span className={chip} {...rest}>\n <button\n type=\"button\"\n onClick={onClick}\n aria-pressed={active}\n className=\"rounded-(--radius-sm) py-1 ps-2.5 font-(family-name:--label-font) text-xs tracking-[calc(0.025em*var(--label-tracking-scale))]\"\n >\n {children}\n </button>\n {remove}\n </span>\n )\n }\n\n if (onClick) {\n return (\n <button type=\"button\" onClick={onClick} aria-pressed={active} className={chip} {...rest}>\n {children}\n </button>\n )\n }\n\n return (\n <span className={chip} {...rest}>\n {children}\n {remove}\n </span>\n )\n}\n\nexport default Tag\n"],"mappings":"AAwHM,cAMA,YANA;AAxHN,SAAS,mBAAmB;AAE5B,SAAS,UAAU;AAuFZ,SAAS,IAAI,EAAE,UAAU,QAAQ,SAAS,UAAU,aAAa,WAAW,GAAG,KAAK,GAAa;AAGtG,QAAM,QAAQ,QAAQ,WAAW,QAAQ;AAEzC,QAAM,OAAO;AAAA,IACX;AAAA,IACA,SAAS,6CAA6C;AAAA,IACtD,WAAW,iBAAiB;AAAA;AAAA;AAAA,IAG5B,CAAC,SAAS;AAAA,IACV;AAAA,EACF;AAEA,QAAM,SAAS,WACb;AAAA,IAAC;AAAA;AAAA,MACC,MAAK;AAAA,MACL,SAAS;AAAA,MACT,cAAY;AAAA,MACZ,WAAW;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,QAMT;AAAA,QACA;AAAA,QACA;AAAA,MACF;AAAA,MAEA,8BAAC,eAAY,MAAM,IAAI,eAAW,MAAC;AAAA;AAAA,EACrC,IACE;AAEJ,MAAI,OAAO;AACT,WACE,qBAAC,UAAK,WAAW,MAAO,GAAG,MACzB;AAAA;AAAA,QAAC;AAAA;AAAA,UACC,MAAK;AAAA,UACL;AAAA,UACA,gBAAc;AAAA,UACd,WAAU;AAAA,UAET;AAAA;AAAA,MACH;AAAA,MACC;AAAA,OACH;AAAA,EAEJ;AAEA,MAAI,SAAS;AACX,WACE,oBAAC,YAAO,MAAK,UAAS,SAAkB,gBAAc,QAAQ,WAAW,MAAO,GAAG,MAChF,UACH;AAAA,EAEJ;AAEA,SACE,qBAAC,UAAK,WAAW,MAAO,GAAG,MACxB;AAAA;AAAA,IACA;AAAA,KACH;AAEJ;AAEA,IAAO,cAAQ;","names":[]}
1
+ {"version":3,"sources":["../../../src/components/Tag/Tag.tsx"],"sourcesContent":["import { RiCloseLine } from '@remixicon/react'\nimport type { HTMLAttributes, ReactNode } from 'react'\nimport { cn } from '../../lib/cn'\n\ninterface TagBase extends Omit<HTMLAttributes<HTMLElement>, 'onClick'> {\n children: ReactNode\n /**\n * Renders the pressed/selected look, and declares the chip a toggle.\n *\n * An interactive chip reads `aria-pressed` off this same value, so the accent\n * fill and the state a screen reader hears cannot drift apart. Leave it off\n * for a chip whose `onClick` navigates or opens something: a control that is\n * neither pressed nor unpressed is better announced as neither than as\n * \"not pressed\".\n */\n active?: boolean\n /**\n * Makes the chip ITSELF the filter control.\n *\n * This prop exists because the alternative did not survive contact with\n * `onRemove`. The advice used to be to wrap the tag in a button at the call\n * site, which is fine on its own and invalid the moment the chip is also\n * removable: the remove control is a real `<button>`, so the wrapper puts a\n * button inside a button — markup the parser splits into siblings, leaving a\n * DOM neither the author nor the accessibility tree expects.\n *\n * With this the wrapper is never written. The chip carries the click, the\n * focus ring and the pressed state on the element that draws them, and a\n * removable filter chip renders its two controls side by side instead of one\n * inside the other.\n */\n onClick?: () => void\n}\n\n/**\n * The removable form: a chip for one thing the reader has already chosen.\n *\n * `removeLabel` is required alongside `onRemove` and not optional with a\n * default, because \"Remove\" repeated down a row of filters is eight controls a\n * screen reader cannot tell apart. Write the subject into it — \"Remove\n * TypeScript filter\".\n */\ninterface TagRemovable {\n /** Called when the reader dismisses the chip. */\n onRemove: () => void\n /** The remove button's accessible name. Name the subject, not the action. */\n removeLabel: string\n}\n\ninterface TagStatic {\n onRemove?: never\n removeLabel?: never\n}\n\nexport type TagProps = TagBase & (TagRemovable | TagStatic)\n\n/**\n * A subject label — a topic, a technology, a filter facet — that can filter\n * with `onClick` and be dismissed with `onRemove`.\n *\n * Distinct from `Badge`, which carries a state or a count. A tag names what\n * something is ABOUT, so several sit together in a row and the reader scans\n * them; a badge is one fact about one record.\n *\n * This is where a `Token` component would have gone. It was not built: a token\n * is a tag with a remove button, and the difference between the two is one\n * prop, not one component. The system already ships three things that look\n * alike — `Badge`, `Tag`, `StatusPill` — and a fourth whose whole distinction\n * is an X on the end would be the one a call site picks by coin toss.\n *\n * Presentational until it is given a handler, and the component owns both\n * interactive cases rather than leaving one to a wrapper at the call site.\n * That is not a convenience: a wrapping `<button>` around a chip that already\n * holds the remove `<button>` is a button inside a button, which no parser\n * keeps and no accessibility tree reports the way it was written. Given both,\n * the chip renders the label and the X as SIBLING buttons — the label takes\n * the leading padding with it, so it is the whole of the chip up to the X\n * rather than the words with dead padding around them.\n *\n * @example\n * <Tag>TypeScript</Tag>\n * @example\n * <Tag active={on} onClick={toggle}>Rust</Tag>\n * @example\n * <Tag onRemove={() => drop('rust')} removeLabel=\"Remove Rust filter\">Rust</Tag>\n * @example\n * // Both: a filter the reader can toggle and also take out of the row.\n * <Tag active={on} onClick={toggle} onRemove={drop} removeLabel=\"Remove Rust filter\">Rust</Tag>\n */\nexport function Tag({ children, active, onClick, onRemove, removeLabel, className, ...rest }: TagProps) {\n // Two controls cannot nest, so a chip that is both a filter and a selection\n // splits its label out into a button of its own beside the remove button.\n const split = Boolean(onClick && onRemove)\n\n const chip = cn(\n 'inline-flex items-center rounded-(--radius-sm) font-(family-name:--label-font) text-xs tracking-[calc(0.025em*var(--label-tracking-scale))] transition-colors duration-(--duration-fast)',\n active ? 'bg-(--action) text-(--action-foreground)' : 'bg-(--stone) text-(--ink-3-aa)',\n onRemove ? 'gap-1 pe-1.5' : 'pe-2.5',\n // Handed to the label button in the split form, so the padding is part of\n // the target rather than a ring of dead space around it.\n !split && 'py-1 ps-2.5',\n className,\n )\n\n const remove = onRemove ? (\n <button\n type=\"button\"\n onClick={onRemove}\n aria-label={removeLabel}\n className={cn(\n // The drawn box is 16px, which is under the 24px minimum a pointer\n // target has to clear (WCAG 2.5.8). The inset pseudo-element takes\n // the hit area out to 24 without changing what is drawn or pushing\n // the chips apart — the 44px floor is for a standalone control, and\n // a chip is 24 tall to begin with.\n 'relative inline-flex size-4 shrink-0 items-center justify-center rounded-(--radius-xs)',\n \"before:absolute before:inset-[-4px] before:content-['']\",\n 'opacity-60 transition-opacity duration-(--duration-fast) hover:opacity-100 focus-visible:opacity-100',\n )}\n >\n <RiCloseLine size={12} aria-hidden />\n </button>\n ) : null\n\n if (split) {\n return (\n <span className={chip} {...rest}>\n <button\n type=\"button\"\n onClick={onClick}\n aria-pressed={active}\n className=\"rounded-(--radius-sm) py-1 ps-2.5 font-(family-name:--label-font) text-xs tracking-[calc(0.025em*var(--label-tracking-scale))]\"\n >\n {children}\n </button>\n {remove}\n </span>\n )\n }\n\n if (onClick) {\n return (\n <button type=\"button\" onClick={onClick} aria-pressed={active} className={chip} {...rest}>\n {children}\n </button>\n )\n }\n\n return (\n <span className={chip} {...rest}>\n {children}\n {remove}\n </span>\n )\n}\n\nexport default Tag\n"],"mappings":"AAwHM,cAMA,YANA;AAxHN,SAAS,mBAAmB;AAE5B,SAAS,UAAU;AAuFZ,SAAS,IAAI,EAAE,UAAU,QAAQ,SAAS,UAAU,aAAa,WAAW,GAAG,KAAK,GAAa;AAGtG,QAAM,QAAQ,QAAQ,WAAW,QAAQ;AAEzC,QAAM,OAAO;AAAA,IACX;AAAA,IACA,SAAS,6CAA6C;AAAA,IACtD,WAAW,iBAAiB;AAAA;AAAA;AAAA,IAG5B,CAAC,SAAS;AAAA,IACV;AAAA,EACF;AAEA,QAAM,SAAS,WACb;AAAA,IAAC;AAAA;AAAA,MACC,MAAK;AAAA,MACL,SAAS;AAAA,MACT,cAAY;AAAA,MACZ,WAAW;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,QAMT;AAAA,QACA;AAAA,QACA;AAAA,MACF;AAAA,MAEA,8BAAC,eAAY,MAAM,IAAI,eAAW,MAAC;AAAA;AAAA,EACrC,IACE;AAEJ,MAAI,OAAO;AACT,WACE,qBAAC,UAAK,WAAW,MAAO,GAAG,MACzB;AAAA;AAAA,QAAC;AAAA;AAAA,UACC,MAAK;AAAA,UACL;AAAA,UACA,gBAAc;AAAA,UACd,WAAU;AAAA,UAET;AAAA;AAAA,MACH;AAAA,MACC;AAAA,OACH;AAAA,EAEJ;AAEA,MAAI,SAAS;AACX,WACE,oBAAC,YAAO,MAAK,UAAS,SAAkB,gBAAc,QAAQ,WAAW,MAAO,GAAG,MAChF,UACH;AAAA,EAEJ;AAEA,SACE,qBAAC,UAAK,WAAW,MAAO,GAAG,MACxB;AAAA;AAAA,IACA;AAAA,KACH;AAEJ;AAEA,IAAO,cAAQ;","names":[]}
@@ -3,8 +3,8 @@ import { cn } from "../../lib/cn.js";
3
3
  const SIZE = {
4
4
  xs: "text-xs leading-[1.6]",
5
5
  sm: "text-sm leading-[1.65]",
6
- base: "text-base font-light leading-[1.75]",
7
- lead: "text-[length:var(--fs-item)] font-light leading-[1.6]"
6
+ base: "text-base [font-weight:var(--body-weight)] leading-[1.75]",
7
+ lead: "text-[length:var(--fs-item)] [font-weight:var(--body-weight)] leading-[1.6]"
8
8
  };
9
9
  const TONE = {
10
10
  body: "text-(--ink-2)",
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../src/components/Text/Text.tsx"],"sourcesContent":["import type { ElementType, HTMLAttributes, ReactNode } from 'react'\nimport { cn } from '../../lib/cn'\n\n/**\n * A rung of the type scale, named for what it is FOR rather than for a pixel\n * value. `base` is body copy; `lead` is the standfirst that carries a piece and\n * is the only step here that touches the heading ladder (`--fs-item`, its\n * bottom rung). Nothing above that: a paragraph larger than a record title is a\n * heading that forgot to be one.\n */\nexport type TextSize = 'xs' | 'sm' | 'base' | 'lead'\n\n/**\n * A rung of the ink ladder. Three, because the ladder has three.\n *\n * `muted` is `--ink-3-aa` and never `--ink-3`. The two look identical on paper\n * and are not the same token: `--ink-3` is a translucent tint that inherits\n * whatever is under it, so it clears AA on the page ground and quietly fails on\n * a card, a plate, or a code block.\n */\nexport type TextTone = 'body' | 'strong' | 'muted'\n\nexport type TextElement = 'p' | 'span' | 'div' | 'li' | 'figcaption'\n\n/**\n * Weight travels with the size, deliberately.\n *\n * The system sets long-form copy at 300 — it is what gives a page of prose its\n * air. At 12 and 14px that same weight is a hairline: the strokes thin out\n * below the point where a light face is a choice and turn into a legibility\n * bug. So the two small steps step back up to 400, and a caller never has to\n * know that the trade exists.\n */\nconst SIZE: Record<TextSize, string> = {\n xs: 'text-xs leading-[1.6]',\n sm: 'text-sm leading-[1.65]',\n base: 'text-base font-light leading-[1.75]',\n lead: 'text-[length:var(--fs-item)] font-light leading-[1.6]',\n}\n\nconst TONE: Record<TextTone, string> = {\n body: 'text-(--ink-2)',\n strong: 'text-(--ink)',\n muted: 'text-(--ink-3-aa)',\n}\n\nexport interface TextProps extends HTMLAttributes<HTMLElement> {\n children?: ReactNode\n /** Which rung of the type scale. See {@link TextSize}. */\n size?: TextSize\n /** Which rung of the ink ladder. See {@link TextTone}. */\n tone?: TextTone\n /**\n * The element, and ONLY the element.\n *\n * Changing it changes what the markup means — a `span` inside a sentence, an\n * `li` inside a list someone else opened — and changes nothing about the\n * look. That separation is the whole point: the alternative is a `<p>` nested\n * inside a `<p>`, which the HTML parser silently splits into two, or a\n * paragraph faked out of a `<div>` because the real element brought a size\n * with it.\n */\n as?: TextElement\n}\n\n/**\n * A paragraph, or a run of text that wants the system's voice.\n *\n * The step between `Article` and raw JSX. `Article` styles a whole reading\n * column from element selectors and is the right answer for a post; this is for\n * the single paragraph that is NOT in a column — a card's description, a\n * dialog's explanation, the line under an empty state.\n *\n * The default tone is `body` (`--ink-2`), not `--ink`. A page whose paragraphs\n * are all full-strength ink has spent the top of the ladder on its body copy\n * and has nothing left for the headings, which is the single most common way a\n * monochrome page loses its hierarchy.\n *\n * @example\n * <Text>Twelve releases this quarter, none rolled back.</Text>\n * @example\n * <Text size=\"lead\" tone=\"strong\">A monochrome system for software and writing.</Text>\n * @example\n * <Text as=\"span\" size=\"sm\" tone=\"muted\">Updated just now</Text>\n */\nexport function Text({\n children,\n size = 'base',\n tone = 'body',\n as = 'p',\n className,\n ...rest\n}: TextProps) {\n const Comp = as as ElementType\n\n return (\n <Comp className={cn('m-0', SIZE[size], TONE[tone], className)} {...rest}>\n {children}\n </Comp>\n )\n}\n\nexport default Text\n"],"mappings":"AAgGI;AA/FJ,SAAS,UAAU;AAgCnB,MAAM,OAAiC;AAAA,EACrC,IAAI;AAAA,EACJ,IAAI;AAAA,EACJ,MAAM;AAAA,EACN,MAAM;AACR;AAEA,MAAM,OAAiC;AAAA,EACrC,MAAM;AAAA,EACN,QAAQ;AAAA,EACR,OAAO;AACT;AAyCO,SAAS,KAAK;AAAA,EACnB;AAAA,EACA,OAAO;AAAA,EACP,OAAO;AAAA,EACP,KAAK;AAAA,EACL;AAAA,EACA,GAAG;AACL,GAAc;AACZ,QAAM,OAAO;AAEb,SACE,oBAAC,QAAK,WAAW,GAAG,OAAO,KAAK,IAAI,GAAG,KAAK,IAAI,GAAG,SAAS,GAAI,GAAG,MAChE,UACH;AAEJ;AAEA,IAAO,eAAQ;","names":[]}
1
+ {"version":3,"sources":["../../../src/components/Text/Text.tsx"],"sourcesContent":["import type { ElementType, HTMLAttributes, ReactNode } from 'react'\nimport { cn } from '../../lib/cn'\n\n/**\n * A rung of the type scale, named for what it is FOR rather than for a pixel\n * value. `base` is body copy; `lead` is the standfirst that carries a piece and\n * is the only step here that touches the heading ladder (`--fs-item`, its\n * bottom rung). Nothing above that: a paragraph larger than a record title is a\n * heading that forgot to be one.\n */\nexport type TextSize = 'xs' | 'sm' | 'base' | 'lead'\n\n/**\n * A rung of the ink ladder. Three, because the ladder has three.\n *\n * `muted` is `--ink-3-aa` and never `--ink-3`. The two look identical on paper\n * and are not the same token: `--ink-3` is a translucent tint that inherits\n * whatever is under it, so it clears AA on the page ground and quietly fails on\n * a card, a plate, or a code block.\n */\nexport type TextTone = 'body' | 'strong' | 'muted'\n\nexport type TextElement = 'p' | 'span' | 'div' | 'li' | 'figcaption'\n\n/**\n * Weight travels with the size, deliberately.\n *\n * The system sets long-form copy at 300 — it is what gives a page of prose its\n * air. At 12 and 14px that same weight is a hairline: the strokes thin out\n * below the point where a light face is a choice and turn into a legibility\n * bug. So the two small steps step back up to 400, and a caller never has to\n * know that the trade exists.\n */\nconst SIZE: Record<TextSize, string> = {\n xs: 'text-xs leading-[1.6]',\n sm: 'text-sm leading-[1.65]',\n base: 'text-base [font-weight:var(--body-weight)] leading-[1.75]',\n lead: 'text-[length:var(--fs-item)] [font-weight:var(--body-weight)] leading-[1.6]',\n}\n\nconst TONE: Record<TextTone, string> = {\n body: 'text-(--ink-2)',\n strong: 'text-(--ink)',\n muted: 'text-(--ink-3-aa)',\n}\n\nexport interface TextProps extends HTMLAttributes<HTMLElement> {\n children?: ReactNode\n /** Which rung of the type scale. See {@link TextSize}. */\n size?: TextSize\n /** Which rung of the ink ladder. See {@link TextTone}. */\n tone?: TextTone\n /**\n * The element, and ONLY the element.\n *\n * Changing it changes what the markup means — a `span` inside a sentence, an\n * `li` inside a list someone else opened — and changes nothing about the\n * look. That separation is the whole point: the alternative is a `<p>` nested\n * inside a `<p>`, which the HTML parser silently splits into two, or a\n * paragraph faked out of a `<div>` because the real element brought a size\n * with it.\n */\n as?: TextElement\n}\n\n/**\n * A paragraph, or a run of text that wants the system's voice.\n *\n * The step between `Article` and raw JSX. `Article` styles a whole reading\n * column from element selectors and is the right answer for a post; this is for\n * the single paragraph that is NOT in a column — a card's description, a\n * dialog's explanation, the line under an empty state.\n *\n * The default tone is `body` (`--ink-2`), not `--ink`. A page whose paragraphs\n * are all full-strength ink has spent the top of the ladder on its body copy\n * and has nothing left for the headings, which is the single most common way a\n * monochrome page loses its hierarchy.\n *\n * @example\n * <Text>Twelve releases this quarter, none rolled back.</Text>\n * @example\n * <Text size=\"lead\" tone=\"strong\">A monochrome system for software and writing.</Text>\n * @example\n * <Text as=\"span\" size=\"sm\" tone=\"muted\">Updated just now</Text>\n */\nexport function Text({\n children,\n size = 'base',\n tone = 'body',\n as = 'p',\n className,\n ...rest\n}: TextProps) {\n const Comp = as as ElementType\n\n return (\n <Comp className={cn('m-0', SIZE[size], TONE[tone], className)} {...rest}>\n {children}\n </Comp>\n )\n}\n\nexport default Text\n"],"mappings":"AAgGI;AA/FJ,SAAS,UAAU;AAgCnB,MAAM,OAAiC;AAAA,EACrC,IAAI;AAAA,EACJ,IAAI;AAAA,EACJ,MAAM;AAAA,EACN,MAAM;AACR;AAEA,MAAM,OAAiC;AAAA,EACrC,MAAM;AAAA,EACN,QAAQ;AAAA,EACR,OAAO;AACT;AAyCO,SAAS,KAAK;AAAA,EACnB;AAAA,EACA,OAAO;AAAA,EACP,OAAO;AAAA,EACP,KAAK;AAAA,EACL;AAAA,EACA,GAAG;AACL,GAAc;AACZ,QAAM,OAAO;AAEb,SACE,oBAAC,QAAK,WAAW,GAAG,OAAO,KAAK,IAAI,GAAG,KAAK,IAAI,GAAG,SAAS,GAAI,GAAG,MAChE,UACH;AAEJ;AAEA,IAAO,eAAQ;","names":[]}
@@ -45,7 +45,7 @@ function ToggleGroup({
45
45
  {
46
46
  "aria-hidden": true,
47
47
  "data-folio-animated": true,
48
- className: "absolute rounded-(--radius-pill) bg-(--accent) transition-[transform,width] duration-(--duration-base) ease-(--ease-out-expo) motion-reduce:transition-none",
48
+ className: "absolute rounded-(--radius-pill) bg-(--action) transition-[transform,width] duration-(--duration-base) ease-(--ease-out-expo) motion-reduce:transition-none",
49
49
  style: {
50
50
  transform: `translate(${indicator.offset}px, ${indicator.top}px)`,
51
51
  width: indicator.width,
@@ -72,7 +72,7 @@ function ToggleGroupItem({ className, value, ...props }) {
72
72
  "data-indicator-active": active ? "true" : void 0,
73
73
  className: cn(
74
74
  "relative z-1 inline-flex min-h-(--control-h-sm) items-center justify-center gap-2 rounded-(--radius-pill) px-3.5 text-sm transition-colors duration-(--duration-fast) disabled:opacity-(--disabled-opacity) disabled:pointer-events-none",
75
- isSingle ? active ? "text-(--accent-foreground)" : "text-(--ink-3-aa) hover:text-(--ink)" : "text-(--ink-3-aa) hover:text-(--ink) data-[state=on]:bg-(--accent) data-[state=on]:text-(--accent-foreground)",
75
+ isSingle ? active ? "text-(--action-foreground)" : "text-(--ink-3-aa) hover:text-(--ink)" : "text-(--ink-3-aa) hover:text-(--ink) data-[state=on]:bg-(--action) data-[state=on]:text-(--action-foreground)",
76
76
  className
77
77
  ),
78
78
  ...props
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../src/components/ToggleGroup/ToggleGroup.tsx"],"sourcesContent":["'use client'\n\nimport * as ToggleGroupPrimitive from '@radix-ui/react-toggle-group'\nimport { createContext, useContext, useState, type ComponentProps } from 'react'\nimport { cn } from '../../lib/cn'\nimport { useFieldControl } from '../Field/field-control'\nimport { useSelectionIndicator } from '../../lib/useSelectionIndicator'\n\nexport type ToggleGroupProps = ComponentProps<typeof ToggleGroupPrimitive.Root>\n\n/** Lets an item know whether the group tracks one value or several. */\nconst SingleValueContext = createContext<string | null>(null)\n\n/**\n * A segmented control: several options, one strip.\n *\n * `type=\"single\"` is a choice — Radix gives it radio semantics, and it is the\n * right shape for a view switcher or a density setting. `type=\"multiple\"` is a\n * set of independent toggles, which is a different thing announced differently;\n * choosing the wrong one is how a \"filter by tag\" control ends up telling a\n * screen reader that picking one tag unpicks the others.\n *\n * The two look different on purpose. A single-value strip moves ONE filled pill\n * between its options, so the eye follows a thing travelling; a multiple-value\n * strip fills each pressed option separately, because there is no single\n * selection to travel. Two people looking at a screenshot should be able to\n * tell which kind they are looking at, and before this they could not.\n *\n * Distinct from `Tabs`, which switches PANELS and owns a tabpanel relationship.\n * A toggle group changes a value.\n *\n * Inside a `Field` the strip takes its name from that label, by pointing back at\n * it — the root is a div, and `<label for>` does not bind to one, so the words\n * above it do not click through. Standing alone it still needs its own\n * `aria-label`.\n *\n * @example\n * <ToggleGroup type=\"single\" defaultValue=\"grid\" aria-label=\"Layout\">\n * <ToggleGroupItem value=\"grid\">Grid</ToggleGroupItem>\n * <ToggleGroupItem value=\"list\">List</ToggleGroupItem>\n * </ToggleGroup>\n */\nexport function ToggleGroup({\n className,\n children,\n 'aria-required': ariaRequired,\n ...props\n}: ToggleGroupProps) {\n const field = useFieldControl()\n const named = props['aria-label'] != null || props['aria-labelledby'] != null\n\n // Tracked here rather than left to Radix alone, because the sliding pill has\n // to know which item to measure — and an uncontrolled group never tells\n // anyone what it picked.\n const single = props.type === 'single'\n const [uncontrolled, setUncontrolled] = useState<string>(\n (single ? (props.defaultValue as string | undefined) : undefined) ?? '',\n )\n const value = single ? ((props.value as string | undefined) ?? uncontrolled) : null\n const [ref, indicator] = useSelectionIndicator<HTMLDivElement>(value)\n\n const root = (\n <ToggleGroupPrimitive.Root\n ref={ref}\n aria-labelledby={named ? undefined : field?.labelId}\n // Only on the single-value strip. Radix gives that one role=\"radiogroup\",\n // which takes aria-required; a multiple-value strip is a role=\"toolbar\",\n // where the attribute is not allowed at all — so a Field's `required`\n // reaches one shape of this control and not the other.\n aria-required={props.type === 'single' ? ariaRequired : undefined}\n className={cn(\n // `w-fit` as well as `inline-flex`: a flex or grid parent stretches its\n // children to the track by default, and `inline-flex` does not opt out\n // of that. Without it the strip grew to whatever the widest sibling was\n // — a label above it — and left dead space after the last segment.\n 'relative inline-flex w-fit items-center gap-1 rounded-(--radius-pill) border border-(--rule-2) p-1',\n className,\n )}\n {...props}\n onValueChange={(next: never) => {\n if (single) setUncontrolled((next as string) ?? '')\n ;(props.onValueChange as ((v: never) => void) | undefined)?.(next)\n }}\n >\n {single && indicator.ready && (\n <span\n aria-hidden\n data-folio-animated\n className=\"absolute rounded-(--radius-pill) bg-(--accent) transition-[transform,width] duration-(--duration-base) ease-(--ease-out-expo) motion-reduce:transition-none\"\n style={{\n transform: `translate(${indicator.offset}px, ${indicator.top}px)`,\n width: indicator.width,\n height: indicator.height,\n insetInlineStart: 0,\n top: 0,\n }}\n />\n )}\n {children}\n </ToggleGroupPrimitive.Root>\n )\n\n return single ? (\n <SingleValueContext.Provider value={value}>{root}</SingleValueContext.Provider>\n ) : (\n root\n )\n}\n\nexport type ToggleGroupItemProps = ComponentProps<typeof ToggleGroupPrimitive.Item>\n\n/**\n * One segment.\n *\n * In a single-value group the item draws no background of its own — the\n * travelling pill behind it does — so it only changes ink. In a multiple-value\n * group it fills, because there is nothing travelling.\n */\nexport function ToggleGroupItem({ className, value, ...props }: ToggleGroupItemProps) {\n const selected = useContext(SingleValueContext)\n const isSingle = selected !== null\n const active = isSingle && selected === value\n\n return (\n <ToggleGroupPrimitive.Item\n value={value}\n data-indicator-active={active ? 'true' : undefined}\n className={cn(\n 'relative z-1 inline-flex min-h-(--control-h-sm) items-center justify-center gap-2 rounded-(--radius-pill) px-3.5 text-sm transition-colors duration-(--duration-fast) disabled:opacity-(--disabled-opacity) disabled:pointer-events-none',\n isSingle\n ? active\n ? 'text-(--accent-foreground)'\n : 'text-(--ink-3-aa) hover:text-(--ink)'\n : 'text-(--ink-3-aa) hover:text-(--ink) data-[state=on]:bg-(--accent) data-[state=on]:text-(--accent-foreground)',\n className,\n )}\n {...props}\n />\n )\n}\n\nexport default ToggleGroup\n"],"mappings":";AA8DI,SAuBI,KAvBJ;AA5DJ,YAAY,0BAA0B;AACtC,SAAS,eAAe,YAAY,gBAAqC;AACzE,SAAS,UAAU;AACnB,SAAS,uBAAuB;AAChC,SAAS,6BAA6B;AAKtC,MAAM,qBAAqB,cAA6B,IAAI;AA+BrD,SAAS,YAAY;AAAA,EAC1B;AAAA,EACA;AAAA,EACA,iBAAiB;AAAA,EACjB,GAAG;AACL,GAAqB;AACnB,QAAM,QAAQ,gBAAgB;AAC9B,QAAM,QAAQ,MAAM,YAAY,KAAK,QAAQ,MAAM,iBAAiB,KAAK;AAKzE,QAAM,SAAS,MAAM,SAAS;AAC9B,QAAM,CAAC,cAAc,eAAe,IAAI;AAAA,KACrC,SAAU,MAAM,eAAsC,WAAc;AAAA,EACvE;AACA,QAAM,QAAQ,SAAW,MAAM,SAAgC,eAAgB;AAC/E,QAAM,CAAC,KAAK,SAAS,IAAI,sBAAsC,KAAK;AAEpE,QAAM,OACJ;AAAA,IAAC,qBAAqB;AAAA,IAArB;AAAA,MACC;AAAA,MACA,mBAAiB,QAAQ,SAAY,OAAO;AAAA,MAK5C,iBAAe,MAAM,SAAS,WAAW,eAAe;AAAA,MACxD,WAAW;AAAA;AAAA;AAAA;AAAA;AAAA,QAKT;AAAA,QACA;AAAA,MACF;AAAA,MACC,GAAG;AAAA,MACJ,eAAe,CAAC,SAAgB;AAC9B,YAAI,OAAQ,iBAAiB,QAAmB,EAAE;AACjD,QAAC,MAAM,gBAAqD,IAAI;AAAA,MACnE;AAAA,MAEC;AAAA,kBAAU,UAAU,SACnB;AAAA,UAAC;AAAA;AAAA,YACC,eAAW;AAAA,YACX,uBAAmB;AAAA,YACnB,WAAU;AAAA,YACV,OAAO;AAAA,cACL,WAAW,aAAa,UAAU,MAAM,OAAO,UAAU,GAAG;AAAA,cAC5D,OAAO,UAAU;AAAA,cACjB,QAAQ,UAAU;AAAA,cAClB,kBAAkB;AAAA,cAClB,KAAK;AAAA,YACP;AAAA;AAAA,QACF;AAAA,QAED;AAAA;AAAA;AAAA,EACH;AAGF,SAAO,SACL,oBAAC,mBAAmB,UAAnB,EAA4B,OAAe,gBAAK,IAEjD;AAEJ;AAWO,SAAS,gBAAgB,EAAE,WAAW,OAAO,GAAG,MAAM,GAAyB;AACpF,QAAM,WAAW,WAAW,kBAAkB;AAC9C,QAAM,WAAW,aAAa;AAC9B,QAAM,SAAS,YAAY,aAAa;AAExC,SACE;AAAA,IAAC,qBAAqB;AAAA,IAArB;AAAA,MACC;AAAA,MACA,yBAAuB,SAAS,SAAS;AAAA,MACzC,WAAW;AAAA,QACT;AAAA,QACA,WACI,SACE,+BACA,yCACF;AAAA,QACJ;AAAA,MACF;AAAA,MACC,GAAG;AAAA;AAAA,EACN;AAEJ;AAEA,IAAO,sBAAQ;","names":[]}
1
+ {"version":3,"sources":["../../../src/components/ToggleGroup/ToggleGroup.tsx"],"sourcesContent":["'use client'\n\nimport * as ToggleGroupPrimitive from '@radix-ui/react-toggle-group'\nimport { createContext, useContext, useState, type ComponentProps } from 'react'\nimport { cn } from '../../lib/cn'\nimport { useFieldControl } from '../Field/field-control'\nimport { useSelectionIndicator } from '../../lib/useSelectionIndicator'\n\nexport type ToggleGroupProps = ComponentProps<typeof ToggleGroupPrimitive.Root>\n\n/** Lets an item know whether the group tracks one value or several. */\nconst SingleValueContext = createContext<string | null>(null)\n\n/**\n * A segmented control: several options, one strip.\n *\n * `type=\"single\"` is a choice — Radix gives it radio semantics, and it is the\n * right shape for a view switcher or a density setting. `type=\"multiple\"` is a\n * set of independent toggles, which is a different thing announced differently;\n * choosing the wrong one is how a \"filter by tag\" control ends up telling a\n * screen reader that picking one tag unpicks the others.\n *\n * The two look different on purpose. A single-value strip moves ONE filled pill\n * between its options, so the eye follows a thing travelling; a multiple-value\n * strip fills each pressed option separately, because there is no single\n * selection to travel. Two people looking at a screenshot should be able to\n * tell which kind they are looking at, and before this they could not.\n *\n * Distinct from `Tabs`, which switches PANELS and owns a tabpanel relationship.\n * A toggle group changes a value.\n *\n * Inside a `Field` the strip takes its name from that label, by pointing back at\n * it — the root is a div, and `<label for>` does not bind to one, so the words\n * above it do not click through. Standing alone it still needs its own\n * `aria-label`.\n *\n * @example\n * <ToggleGroup type=\"single\" defaultValue=\"grid\" aria-label=\"Layout\">\n * <ToggleGroupItem value=\"grid\">Grid</ToggleGroupItem>\n * <ToggleGroupItem value=\"list\">List</ToggleGroupItem>\n * </ToggleGroup>\n */\nexport function ToggleGroup({\n className,\n children,\n 'aria-required': ariaRequired,\n ...props\n}: ToggleGroupProps) {\n const field = useFieldControl()\n const named = props['aria-label'] != null || props['aria-labelledby'] != null\n\n // Tracked here rather than left to Radix alone, because the sliding pill has\n // to know which item to measure — and an uncontrolled group never tells\n // anyone what it picked.\n const single = props.type === 'single'\n const [uncontrolled, setUncontrolled] = useState<string>(\n (single ? (props.defaultValue as string | undefined) : undefined) ?? '',\n )\n const value = single ? ((props.value as string | undefined) ?? uncontrolled) : null\n const [ref, indicator] = useSelectionIndicator<HTMLDivElement>(value)\n\n const root = (\n <ToggleGroupPrimitive.Root\n ref={ref}\n aria-labelledby={named ? undefined : field?.labelId}\n // Only on the single-value strip. Radix gives that one role=\"radiogroup\",\n // which takes aria-required; a multiple-value strip is a role=\"toolbar\",\n // where the attribute is not allowed at all — so a Field's `required`\n // reaches one shape of this control and not the other.\n aria-required={props.type === 'single' ? ariaRequired : undefined}\n className={cn(\n // `w-fit` as well as `inline-flex`: a flex or grid parent stretches its\n // children to the track by default, and `inline-flex` does not opt out\n // of that. Without it the strip grew to whatever the widest sibling was\n // — a label above it — and left dead space after the last segment.\n 'relative inline-flex w-fit items-center gap-1 rounded-(--radius-pill) border border-(--rule-2) p-1',\n className,\n )}\n {...props}\n onValueChange={(next: never) => {\n if (single) setUncontrolled((next as string) ?? '')\n ;(props.onValueChange as ((v: never) => void) | undefined)?.(next)\n }}\n >\n {single && indicator.ready && (\n <span\n aria-hidden\n data-folio-animated\n className=\"absolute rounded-(--radius-pill) bg-(--action) transition-[transform,width] duration-(--duration-base) ease-(--ease-out-expo) motion-reduce:transition-none\"\n style={{\n transform: `translate(${indicator.offset}px, ${indicator.top}px)`,\n width: indicator.width,\n height: indicator.height,\n insetInlineStart: 0,\n top: 0,\n }}\n />\n )}\n {children}\n </ToggleGroupPrimitive.Root>\n )\n\n return single ? (\n <SingleValueContext.Provider value={value}>{root}</SingleValueContext.Provider>\n ) : (\n root\n )\n}\n\nexport type ToggleGroupItemProps = ComponentProps<typeof ToggleGroupPrimitive.Item>\n\n/**\n * One segment.\n *\n * In a single-value group the item draws no background of its own — the\n * travelling pill behind it does — so it only changes ink. In a multiple-value\n * group it fills, because there is nothing travelling.\n */\nexport function ToggleGroupItem({ className, value, ...props }: ToggleGroupItemProps) {\n const selected = useContext(SingleValueContext)\n const isSingle = selected !== null\n const active = isSingle && selected === value\n\n return (\n <ToggleGroupPrimitive.Item\n value={value}\n data-indicator-active={active ? 'true' : undefined}\n className={cn(\n 'relative z-1 inline-flex min-h-(--control-h-sm) items-center justify-center gap-2 rounded-(--radius-pill) px-3.5 text-sm transition-colors duration-(--duration-fast) disabled:opacity-(--disabled-opacity) disabled:pointer-events-none',\n isSingle\n ? active\n ? 'text-(--action-foreground)'\n : 'text-(--ink-3-aa) hover:text-(--ink)'\n : 'text-(--ink-3-aa) hover:text-(--ink) data-[state=on]:bg-(--action) data-[state=on]:text-(--action-foreground)',\n className,\n )}\n {...props}\n />\n )\n}\n\nexport default ToggleGroup\n"],"mappings":";AA8DI,SAuBI,KAvBJ;AA5DJ,YAAY,0BAA0B;AACtC,SAAS,eAAe,YAAY,gBAAqC;AACzE,SAAS,UAAU;AACnB,SAAS,uBAAuB;AAChC,SAAS,6BAA6B;AAKtC,MAAM,qBAAqB,cAA6B,IAAI;AA+BrD,SAAS,YAAY;AAAA,EAC1B;AAAA,EACA;AAAA,EACA,iBAAiB;AAAA,EACjB,GAAG;AACL,GAAqB;AACnB,QAAM,QAAQ,gBAAgB;AAC9B,QAAM,QAAQ,MAAM,YAAY,KAAK,QAAQ,MAAM,iBAAiB,KAAK;AAKzE,QAAM,SAAS,MAAM,SAAS;AAC9B,QAAM,CAAC,cAAc,eAAe,IAAI;AAAA,KACrC,SAAU,MAAM,eAAsC,WAAc;AAAA,EACvE;AACA,QAAM,QAAQ,SAAW,MAAM,SAAgC,eAAgB;AAC/E,QAAM,CAAC,KAAK,SAAS,IAAI,sBAAsC,KAAK;AAEpE,QAAM,OACJ;AAAA,IAAC,qBAAqB;AAAA,IAArB;AAAA,MACC;AAAA,MACA,mBAAiB,QAAQ,SAAY,OAAO;AAAA,MAK5C,iBAAe,MAAM,SAAS,WAAW,eAAe;AAAA,MACxD,WAAW;AAAA;AAAA;AAAA;AAAA;AAAA,QAKT;AAAA,QACA;AAAA,MACF;AAAA,MACC,GAAG;AAAA,MACJ,eAAe,CAAC,SAAgB;AAC9B,YAAI,OAAQ,iBAAiB,QAAmB,EAAE;AACjD,QAAC,MAAM,gBAAqD,IAAI;AAAA,MACnE;AAAA,MAEC;AAAA,kBAAU,UAAU,SACnB;AAAA,UAAC;AAAA;AAAA,YACC,eAAW;AAAA,YACX,uBAAmB;AAAA,YACnB,WAAU;AAAA,YACV,OAAO;AAAA,cACL,WAAW,aAAa,UAAU,MAAM,OAAO,UAAU,GAAG;AAAA,cAC5D,OAAO,UAAU;AAAA,cACjB,QAAQ,UAAU;AAAA,cAClB,kBAAkB;AAAA,cAClB,KAAK;AAAA,YACP;AAAA;AAAA,QACF;AAAA,QAED;AAAA;AAAA;AAAA,EACH;AAGF,SAAO,SACL,oBAAC,mBAAmB,UAAnB,EAA4B,OAAe,gBAAK,IAEjD;AAEJ;AAWO,SAAS,gBAAgB,EAAE,WAAW,OAAO,GAAG,MAAM,GAAyB;AACpF,QAAM,WAAW,WAAW,kBAAkB;AAC9C,QAAM,WAAW,aAAa;AAC9B,QAAM,SAAS,YAAY,aAAa;AAExC,SACE;AAAA,IAAC,qBAAqB;AAAA,IAArB;AAAA,MACC;AAAA,MACA,yBAAuB,SAAS,SAAS;AAAA,MACzC,WAAW;AAAA,QACT;AAAA,QACA,WACI,SACE,+BACA,yCACF;AAAA,QACJ;AAAA,MACF;AAAA,MACC,GAAG;AAAA;AAAA,EACN;AAEJ;AAEA,IAAO,sBAAQ;","names":[]}
package/dist/semantic.css CHANGED
@@ -69,10 +69,12 @@
69
69
  writes the next panel. */
70
70
  --accent-on-muted: var(--red-ink);
71
71
 
72
- /* The fill of the one primary action on a screen. It follows the accent
73
- unless `data-action` says otherwise: a preset that keeps its accent for
74
- links and the current state can hand the primary button back to ink,
75
- without a checked box or the current page losing the accent. */
72
+ /* The SOLID fill of a primary action or a chosen option — the primary
73
+ button, the sliding pill of a segmented control, the active filter chip,
74
+ the current page. It follows the accent unless `data-action` says
75
+ otherwise: a preset that keeps its accent for links can hand these large
76
+ fills back to ink, while small marks (a checked box, a switch, a slider's
77
+ range) keep the accent. */
76
78
  --action: var(--accent);
77
79
  --action-foreground: var(--accent-foreground);
78
80