@misoto22/design 0.8.0 → 0.9.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 (157) hide show
  1. package/dist/agent/Accordion.md +1 -1
  2. package/dist/agent/Alert.md +1 -1
  3. package/dist/agent/AppShell.md +1 -1
  4. package/dist/agent/ArchitectureFigure.md +1 -1
  5. package/dist/agent/AreaChart.md +1 -1
  6. package/dist/agent/Article.md +1 -1
  7. package/dist/agent/AspectRatio.md +1 -1
  8. package/dist/agent/Avatar.md +1 -1
  9. package/dist/agent/Badge.md +1 -1
  10. package/dist/agent/BarChart.md +1 -1
  11. package/dist/agent/BarList.md +1 -1
  12. package/dist/agent/BigNumber.md +1 -1
  13. package/dist/agent/BoxPlot.md +1 -1
  14. package/dist/agent/Breadcrumb.md +1 -1
  15. package/dist/agent/BulletChart.md +1 -1
  16. package/dist/agent/Button.md +1 -1
  17. package/dist/agent/Calendar.md +1 -1
  18. package/dist/agent/Card.md +1 -1
  19. package/dist/agent/Checkbox.md +1 -1
  20. package/dist/agent/Code.md +1 -1
  21. package/dist/agent/CodeBlock.md +1 -1
  22. package/dist/agent/Collapsible.md +1 -1
  23. package/dist/agent/ColorPicker.md +75 -0
  24. package/dist/agent/Combobox.md +1 -1
  25. package/dist/agent/Command.md +1 -1
  26. package/dist/agent/ComposedChart.md +1 -1
  27. package/dist/agent/ContextMenu.md +1 -1
  28. package/dist/agent/DataflowFigure.md +1 -1
  29. package/dist/agent/DatePicker.md +1 -1
  30. package/dist/agent/DescriptionList.md +1 -1
  31. package/dist/agent/Diagram.md +1 -1
  32. package/dist/agent/DiagramCanvas.md +1 -1
  33. package/dist/agent/DiagramExportMenu.md +1 -1
  34. package/dist/agent/DiagramInspector.md +1 -1
  35. package/dist/agent/DiagramLegend.md +1 -1
  36. package/dist/agent/DiagramMinimap.md +1 -1
  37. package/dist/agent/DiagramToolbar.md +1 -1
  38. package/dist/agent/Dialog.md +1 -1
  39. package/dist/agent/DropdownMenu.md +1 -1
  40. package/dist/agent/EmptyState.md +1 -1
  41. package/dist/agent/ErrorState.md +1 -1
  42. package/dist/agent/Facet.md +1 -1
  43. package/dist/agent/Field.md +1 -1
  44. package/dist/agent/FigureBand.md +1 -1
  45. package/dist/agent/FloatingIconButton.md +1 -1
  46. package/dist/agent/FunnelChart.md +1 -1
  47. package/dist/agent/Heading.md +1 -1
  48. package/dist/agent/Heatmap.md +1 -1
  49. package/dist/agent/Histogram.md +1 -1
  50. package/dist/agent/Input.md +1 -1
  51. package/dist/agent/Kbd.md +1 -1
  52. package/dist/agent/LifecycleFigure.md +1 -1
  53. package/dist/agent/LineChart.md +1 -1
  54. package/dist/agent/LinkArrow.md +1 -1
  55. package/dist/agent/Markdown.md +1 -1
  56. package/dist/agent/NativeSelect.md +1 -1
  57. package/dist/agent/NavItem.md +1 -1
  58. package/dist/agent/NumberField.md +72 -0
  59. package/dist/agent/Pagination.md +1 -1
  60. package/dist/agent/PieChart.md +1 -1
  61. package/dist/agent/Popover.md +1 -1
  62. package/dist/agent/Progress.md +1 -1
  63. package/dist/agent/RadarChart.md +1 -1
  64. package/dist/agent/RadialChart.md +1 -1
  65. package/dist/agent/RadioGroup.md +1 -1
  66. package/dist/agent/SankeyChart.md +1 -1
  67. package/dist/agent/ScatterChart.md +1 -1
  68. package/dist/agent/ScrollArea.md +1 -1
  69. package/dist/agent/SearchableMenu.md +1 -1
  70. package/dist/agent/Select.md +1 -1
  71. package/dist/agent/Separator.md +1 -1
  72. package/dist/agent/SequenceFigure.md +1 -1
  73. package/dist/agent/Sheet.md +1 -1
  74. package/dist/agent/Sidebar.md +193 -0
  75. package/dist/agent/Skeleton.md +1 -1
  76. package/dist/agent/Slider.md +7 -3
  77. package/dist/agent/Sparkline.md +1 -1
  78. package/dist/agent/Spinner.md +1 -1
  79. package/dist/agent/StatusDot.md +1 -1
  80. package/dist/agent/StatusPill.md +1 -1
  81. package/dist/agent/Steps.md +1 -1
  82. package/dist/agent/Switch.md +1 -1
  83. package/dist/agent/Table.md +3 -1
  84. package/dist/agent/Tabs.md +1 -1
  85. package/dist/agent/Tag.md +1 -1
  86. package/dist/agent/Text.md +1 -1
  87. package/dist/agent/Textarea.md +1 -1
  88. package/dist/agent/Timestamp.md +1 -1
  89. package/dist/agent/Toast.md +1 -1
  90. package/dist/agent/ToggleGroup.md +1 -1
  91. package/dist/agent/Toolbar.md +1 -1
  92. package/dist/agent/Tooltip.md +1 -1
  93. package/dist/agent/TreemapChart.md +1 -1
  94. package/dist/agent/WaterfallChart.md +1 -1
  95. package/dist/agent/WorkflowFigure.md +1 -1
  96. package/dist/agent/catalog.json +361 -2
  97. package/dist/agent/index.md +8 -5
  98. package/dist/charts/BulletChart/BulletChart.js +16 -2
  99. package/dist/charts/BulletChart/BulletChart.js.map +1 -1
  100. package/dist/charts/RadarChart/RadarChart.js +4 -2
  101. package/dist/charts/RadarChart/RadarChart.js.map +1 -1
  102. package/dist/components/ColorPicker/ColorPicker.d.ts +67 -0
  103. package/dist/components/ColorPicker/ColorPicker.js +334 -0
  104. package/dist/components/ColorPicker/ColorPicker.js.map +1 -0
  105. package/dist/components/Command/Command.js +1 -1
  106. package/dist/components/Command/Command.js.map +1 -1
  107. package/dist/components/ContextMenu/ContextMenu.js +1 -1
  108. package/dist/components/ContextMenu/ContextMenu.js.map +1 -1
  109. package/dist/components/Dialog/Dialog.js +1 -1
  110. package/dist/components/Dialog/Dialog.js.map +1 -1
  111. package/dist/components/DropdownMenu/DropdownMenu.js +1 -1
  112. package/dist/components/DropdownMenu/DropdownMenu.js.map +1 -1
  113. package/dist/components/ErrorState/ErrorState.js +1 -1
  114. package/dist/components/ErrorState/ErrorState.js.map +1 -1
  115. package/dist/components/NavItem/NavItem.js +1 -1
  116. package/dist/components/NavItem/NavItem.js.map +1 -1
  117. package/dist/components/NumberField/NumberField.d.ts +69 -0
  118. package/dist/components/NumberField/NumberField.js +162 -0
  119. package/dist/components/NumberField/NumberField.js.map +1 -0
  120. package/dist/components/Popover/Popover.js +1 -1
  121. package/dist/components/Popover/Popover.js.map +1 -1
  122. package/dist/components/Select/Select.js +1 -1
  123. package/dist/components/Select/Select.js.map +1 -1
  124. package/dist/components/Sheet/Sheet.js +1 -1
  125. package/dist/components/Sheet/Sheet.js.map +1 -1
  126. package/dist/components/Sidebar/Sidebar.d.ts +349 -0
  127. package/dist/components/Sidebar/Sidebar.js +455 -0
  128. package/dist/components/Sidebar/Sidebar.js.map +1 -0
  129. package/dist/components/Slider/Slider.d.ts +22 -1
  130. package/dist/components/Slider/Slider.js +72 -8
  131. package/dist/components/Slider/Slider.js.map +1 -1
  132. package/dist/components/Table/Table.d.ts +14 -0
  133. package/dist/components/Table/Table.js.map +1 -1
  134. package/dist/components/Tabs/Tabs.js +7 -1
  135. package/dist/components/Tabs/Tabs.js.map +1 -1
  136. package/dist/diagrams/lib/route.d.ts +22 -1
  137. package/dist/diagrams/lib/route.js +24 -1
  138. package/dist/diagrams/lib/route.js.map +1 -1
  139. package/dist/diagrams/lib/wires.js +9 -5
  140. package/dist/diagrams/lib/wires.js.map +1 -1
  141. package/dist/index.d.ts +3 -0
  142. package/dist/index.js +3 -0
  143. package/dist/index.js.map +1 -1
  144. package/dist/lib/color.d.ts +80 -0
  145. package/dist/lib/color.js +189 -0
  146. package/dist/lib/color.js.map +1 -0
  147. package/dist/lib/numeric.d.ts +35 -0
  148. package/dist/lib/numeric.js +29 -0
  149. package/dist/lib/numeric.js.map +1 -0
  150. package/dist/styles.css +1 -1
  151. package/dist/themes.css +8 -0
  152. package/dist/tokens.css +66 -3
  153. package/dist/tokens.d.ts +3 -0
  154. package/dist/tokens.js +17 -0
  155. package/dist/tokens.json +17 -0
  156. package/package.json +1 -1
  157. package/skills/misoto22-design/rules/forms.md +3 -1
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../src/charts/BulletChart/BulletChart.tsx"],"sourcesContent":["import type { ReactNode } from 'react'\nimport { ChartFigure } from '../lib/figure'\nimport { type ChartEmptyProps } from '../lib/empty'\nimport { defaultTick } from '../lib/format'\nimport { clampedFraction } from '../lib/scale'\n\n/**\n * The heaviest and lightest a qualitative band may be, as multiples of\n * `--chart-fill`.\n *\n * Few's rule is no more than five bands in one hue, arranged so the low end of\n * the scale is the heaviest. The measure bar is solid ink on top of them, so\n * the ceiling has to stay well under it: bands that compete with the bar stop\n * being the ground it is read against.\n */\nconst BAND_WEIGHT = { heaviest: 1, lightest: 0.3 } as const\n\n/** How tall the measure bar is, as a share of the track. Few's is a third. */\nconst BAR_SHARE = 0.34\n\n/** One measure, its target, and the bands it is read against. */\nexport interface BulletMeasure {\n /** What is being measured. Doubles as the row's key, so it must be unique. */\n name: string\n /** The number the row is about. */\n value: number\n /**\n * The number the measure is being judged against — last year, the budget,\n * the SLO. Drawn as a rule across the bar, not as a second bar.\n */\n target?: number\n /**\n * The qualitative bands, as ascending UPPER bounds. `[60, 80]` on a 0–100\n * scale draws three bands: 0–60, 60–80, 80–100.\n *\n * Few's limit is five, and it is a real one: past that the shades stop being\n * separable and the bands become a gradient nobody can read a boundary off.\n */\n ranges?: number[]\n /** The scale, as `[min, max]`. Falls back to the chart's, then to the data. */\n domain?: [number, number]\n /** A second line under the name — the unit, the window, the owner. */\n detail?: ReactNode\n}\n\n/** A measure with its scale and bands resolved. */\ninterface ResolvedMeasure extends BulletMeasure {\n domain: [number, number]\n bands: { from: number; to: number; weight: number }[]\n /** Written out for the hidden table, which cannot print an array. */\n rangeLabel: string\n}\n\n/** Where a value sits on the scale, 0 to 1, clamped to it. */\nfunction position(value: number, [low, high]: [number, number]): number {\n return clampedFraction(value, low, high)\n}\n\n/** Whether a number is off the end of the scale it is being drawn against. */\nfunction overflows(value: number | undefined, [low, high]: [number, number]): boolean {\n return value !== undefined && Number.isFinite(value) && (value > high || value < low)\n}\n\n/**\n * A share of the track, as a CSS percentage.\n *\n * Rounded, because binary floating point turns `0.8 - 0.5` into\n * `30.000000000000004%` — a number no layout engine cares about and every\n * reader of the DOM does. Four places is finer than a subpixel on any track\n * this chart will be given.\n */\nfunction pct(share: number): string {\n return `${Number((share * 100).toFixed(4))}%`\n}\n\n/** The bands, from the ascending upper bounds, each with its own weight. */\nfunction bands(\n ranges: number[],\n [low, high]: [number, number],\n formatValue: (value: number) => string,\n): { list: ResolvedMeasure['bands']; label: string } {\n const set = [...new Set(ranges.filter(Number.isFinite))].sort((a, b) => a - b)\n // A bound off the end of the scale has no boundary to draw: it would be a\n // band with no width, or one that starts past the track. It is dropped from\n // the picture and kept in the LABEL, because a caller who pinned `[0, 50]`\n // and passed `[60, 80]` has made a mistake, and one flat band that says\n // nothing is how it stays invisible.\n const bounds = set.filter((bound) => bound > low && bound < high)\n\n const edges = [low, ...bounds, high]\n const count = edges.length - 1\n const { heaviest, lightest } = BAND_WEIGHT\n\n const list = Array.from({ length: count }, (_, index) => ({\n from: edges[index]!,\n to: edges[index + 1]!,\n // Heaviest at the low end, lightening as the scale rises — so the measure\n // bar, which is solid, stands out most where performance is best.\n weight: count === 1 ? lightest : heaviest - (index / (count - 1)) * (heaviest - lightest),\n }))\n\n // The label names the bounds the caller SET, not the band tops: the last\n // band's top is the end of the scale, which is not a threshold anybody chose\n // — and not the drawable subset either, or a bound the scale cannot hold\n // would go missing from the reading as well as from the picture.\n return { list, label: set.map(formatValue).join(', ') }\n}\n\n/** Every measure, with its scale and bands worked out. */\nfunction resolve(\n data: BulletMeasure[],\n domain: [number, number] | undefined,\n ranges: number[] | undefined,\n formatValue: (value: number) => string,\n): ResolvedMeasure[] {\n return data.map((measure) => {\n const own = measure.ranges ?? ranges ?? []\n const high = Math.max(measure.value, measure.target ?? Number.NEGATIVE_INFINITY, ...own)\n // Anchored at zero unless told otherwise: the measure is drawn as a BAR,\n // and a bar read off a scale that does not start at zero overstates every\n // difference on it.\n const scale: [number, number] = measure.domain ?? domain ?? [Math.min(0, measure.value), high || 1]\n const built = bands(own, scale, formatValue)\n\n return { ...measure, domain: scale, bands: built.list, rangeLabel: built.label }\n })\n}\n\nexport interface BulletChartProps {\n /**\n * What the chart shows, in a sentence a reader could act on. Required, and\n * announced to a screen reader even when it is not printed.\n */\n title: string\n /** Prints the title above the rows instead of hiding it from sight. */\n showTitle?: boolean\n /** A line under the title — the unit, the window, the caveat. */\n description?: ReactNode\n /** One entry per measure. Order is the reading order; it is not sorted. */\n data: BulletMeasure[]\n /**\n * The bands every measure falls back to, as ascending upper bounds.\n *\n * Shared bands only mean something when the measures share a scale. Where\n * they do not — a latency beside a conversion rate — put `ranges` and\n * `domain` on each measure instead.\n */\n ranges?: number[]\n /** The scale every measure falls back to, as `[min, max]`. */\n domain?: [number, number]\n /** Formats every number the chart prints. */\n formatValue?: (value: number) => string\n /** Prints the scale's two ends under each track. */\n showScale?: boolean\n /** Merged onto the figure, last, so a call site can size or space it. */\n className?: string\n /** Drops the hidden table view. Only correct when the page prints the data itself. */\n hideDataTable?: boolean\n /** What the chart shows when it has nothing to draw. */\n empty?: ChartEmptyProps\n}\n\n/**\n * A measure, its target, and the bands that say whether the number is any good\n * — one dense row per thing being tracked.\n *\n * Stephen Few designed this to replace the dashboard gauge, and the argument\n * has held: a speedometer spends a whole card saying one number badly, where a\n * bullet graph says the same number, its target, and the qualitative context\n * around it in the height of a line of text. Ten of them stack into a status\n * page a reader can scan in one pass.\n *\n * What it will not do is show change over time. A bullet graph is one instant,\n * and `target` is the only comparison it carries; when the question is\n * \"how did we get here\" it wants a `<LineChart>`, and when several measures\n * have to be compared against each OTHER rather than each against its own\n * target, a `<BarList>` ranks them and this does not.\n *\n * **The bands are the part to be careful with.** They are a judgement — someone\n * decided that 80 is \"good\" — drawn in the same ink as the measurement, and a\n * reader has no way to tell an agreed threshold from a number somebody typed.\n * They also compress: a value near the top of the scale sits in the same band\n * whether it cleared the boundary by a point or by thirty. The band answers\n * \"is this acceptable\", never \"by how much\".\n *\n * No rendering engine, deliberately — this is the one chart in the set with\n * nothing to compute. Each row is a single linear scale with no axis, no ticks\n * and no shared plot area, so it is laid out as HTML: the bands and the bar are\n * inline-axis offsets, which means the whole chart mirrors correctly in a\n * right-to-left document, where an SVG drawn in user space would not.\n *\n * @example\n * <BulletChart\n * title=\"Service levels against target\"\n * data={[{ name: 'Uptime', value: 99.4, target: 99.9, ranges: [98, 99.5] }]}\n * domain={[95, 100]}\n * />\n */\nexport function BulletChart({\n title,\n showTitle,\n description,\n data,\n ranges,\n domain,\n formatValue = defaultTick,\n showScale = false,\n className,\n hideDataTable = false,\n empty,\n}: BulletChartProps) {\n const measures = resolve(data, domain, ranges, formatValue)\n\n return (\n <ChartFigure\n title={title}\n showTitle={showTitle}\n description={description}\n className={className}\n table={\n hideDataTable\n ? false\n : {\n rows: measures as unknown as Record<string, unknown>[],\n rowKey: 'name',\n columns: [\n { key: 'value', label: 'Value' },\n { key: 'target', label: 'Target' },\n { key: 'rangeLabel', label: 'Range bounds' },\n ],\n }\n }\n isEmpty={measures.length === 0}\n empty={empty}\n >\n <div className=\"flex w-full flex-col gap-4\">\n {measures.map((measure) => (\n <BulletRow\n key={measure.name}\n measure={measure}\n formatValue={formatValue}\n showScale={showScale}\n />\n ))}\n </div>\n </ChartFigure>\n )\n}\n\nfunction BulletRow({\n measure,\n formatValue,\n showScale,\n}: {\n measure: ResolvedMeasure\n formatValue: (value: number) => string\n showScale: boolean\n}) {\n const scale = measure.domain\n const barShare = position(measure.value, scale)\n const targetShare = measure.target === undefined ? null : position(measure.target, scale)\n const runsOver = overflows(measure.value, scale) || overflows(measure.target, scale)\n\n return (\n <div className=\"flex w-full flex-col gap-1.5\">\n <div className=\"flex items-baseline justify-between gap-3\">\n <span className=\"truncate text-sm text-(--ink)\">\n {measure.name}\n {measure.detail && (\n <span className=\"ms-2 mono-meta text-(--ink-3-aa)\">{measure.detail}</span>\n )}\n </span>\n <span className=\"shrink-0 font-mono text-xs tabular-nums text-(--ink)\">\n {formatValue(measure.value)}\n {measure.target !== undefined && (\n <span className=\"text-(--ink-3-aa)\"> / {formatValue(measure.target)}</span>\n )}\n </span>\n </div>\n\n {/* The graphic repeats what the line above and the hidden table already\n say, so it is out of the accessibility tree entirely rather than\n announced a third time as a stack of empty boxes. */}\n <div\n aria-hidden\n data-slot=\"bullet-track\"\n className=\"relative h-6 w-full overflow-hidden rounded-(--radius-sm)\"\n >\n {measure.bands.map((band) => (\n <span\n key={`${band.from}-${band.to}`}\n data-slot=\"bullet-band\"\n className=\"absolute inset-y-0 block\"\n style={{\n insetInlineStart: pct(position(band.from, scale)),\n inlineSize: pct(position(band.to, scale) - position(band.from, scale)),\n // The one translucent-mark pair in the token layer, because it is\n // the only one that holds a different number on each ground.\n backgroundColor: `color-mix(in srgb, var(--series-1) calc(var(--chart-fill) * ${band.weight} * 100%), transparent)`,\n }}\n />\n ))}\n\n {/* Solid, and a third of the track's height. The measure is the one\n thing here that is a measurement rather than a judgement, so it is\n the only mark drawn at full weight. */}\n <span\n data-slot=\"bullet-bar\"\n className=\"absolute block bg-(--series-1)\"\n style={{\n insetInlineStart: 0,\n insetBlockStart: pct((1 - BAR_SHARE) / 2),\n blockSize: pct(BAR_SHARE),\n inlineSize: pct(barShare),\n }}\n />\n\n {targetShare !== null && (\n // A rule ACROSS the bar rather than a second bar beside it: the two\n // are different kinds of number, and the reader has to be able to\n // tell at a glance which one was achieved and which was asked for.\n <span\n data-slot=\"bullet-target\"\n className=\"absolute inset-y-1 block w-0.5 bg-(--ink)\"\n style={{ insetInlineStart: pct(targetShare), marginInlineStart: '-1px' }}\n />\n )}\n\n {runsOver && (\n // Clamped and silent, a number past the end of the scale fills the\n // track exactly as a number AT the end of it does — so the one thing\n // the row cannot say is the one thing the reader most needs to know.\n // The notch is what breaks that tie; the printed figure above still\n // carries the real number.\n <span\n data-slot=\"bullet-overflow\"\n className=\"absolute inset-y-0 end-0 block w-1.5 bg-(--chart-surface)\"\n style={{\n clipPath: 'polygon(100% 0, 0 50%, 100% 100%)',\n }}\n />\n )}\n </div>\n\n {showScale && (\n <div className=\"flex justify-between mono-meta text-(--chart-axis)\">\n <span>{formatValue(scale[0])}</span>\n <span>{formatValue(scale[1])}</span>\n </div>\n )}\n </div>\n )\n}\n\nexport default BulletChart\n"],"mappings":"AA6OU,cA6BF,YA7BE;AA5OV,SAAS,mBAAmB;AAC5B,eAAqC;AACrC,SAAS,mBAAmB;AAC5B,SAAS,uBAAuB;AAWhC,MAAM,cAAc,EAAE,UAAU,GAAG,UAAU,IAAI;AAGjD,MAAM,YAAY;AAoClB,SAAS,SAAS,OAAe,CAAC,KAAK,IAAI,GAA6B;AACtE,SAAO,gBAAgB,OAAO,KAAK,IAAI;AACzC;AAGA,SAAS,UAAU,OAA2B,CAAC,KAAK,IAAI,GAA8B;AACpF,SAAO,UAAU,UAAa,OAAO,SAAS,KAAK,MAAM,QAAQ,QAAQ,QAAQ;AACnF;AAUA,SAAS,IAAI,OAAuB;AAClC,SAAO,GAAG,QAAQ,QAAQ,KAAK,QAAQ,CAAC,CAAC,CAAC;AAC5C;AAGA,SAAS,MACP,QACA,CAAC,KAAK,IAAI,GACV,aACmD;AACnD,QAAM,MAAM,CAAC,GAAG,IAAI,IAAI,OAAO,OAAO,OAAO,QAAQ,CAAC,CAAC,EAAE,KAAK,CAAC,GAAG,MAAM,IAAI,CAAC;AAM7E,QAAM,SAAS,IAAI,OAAO,CAAC,UAAU,QAAQ,OAAO,QAAQ,IAAI;AAEhE,QAAM,QAAQ,CAAC,KAAK,GAAG,QAAQ,IAAI;AACnC,QAAM,QAAQ,MAAM,SAAS;AAC7B,QAAM,EAAE,UAAU,SAAS,IAAI;AAE/B,QAAM,OAAO,MAAM,KAAK,EAAE,QAAQ,MAAM,GAAG,CAAC,GAAG,WAAW;AAAA,IACxD,MAAM,MAAM,KAAK;AAAA,IACjB,IAAI,MAAM,QAAQ,CAAC;AAAA;AAAA;AAAA,IAGnB,QAAQ,UAAU,IAAI,WAAW,WAAY,SAAS,QAAQ,MAAO,WAAW;AAAA,EAClF,EAAE;AAMF,SAAO,EAAE,MAAM,OAAO,IAAI,IAAI,WAAW,EAAE,KAAK,IAAI,EAAE;AACxD;AAGA,SAAS,QACP,MACA,QACA,QACA,aACmB;AACnB,SAAO,KAAK,IAAI,CAAC,YAAY;AAC3B,UAAM,MAAM,QAAQ,UAAU,UAAU,CAAC;AACzC,UAAM,OAAO,KAAK,IAAI,QAAQ,OAAO,QAAQ,UAAU,OAAO,mBAAmB,GAAG,GAAG;AAIvF,UAAM,QAA0B,QAAQ,UAAU,UAAU,CAAC,KAAK,IAAI,GAAG,QAAQ,KAAK,GAAG,QAAQ,CAAC;AAClG,UAAM,QAAQ,MAAM,KAAK,OAAO,WAAW;AAE3C,WAAO,EAAE,GAAG,SAAS,QAAQ,OAAO,OAAO,MAAM,MAAM,YAAY,MAAM,MAAM;AAAA,EACjF,CAAC;AACH;AAwEO,SAAS,YAAY;AAAA,EAC1B;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA,cAAc;AAAA,EACd,YAAY;AAAA,EACZ;AAAA,EACA,gBAAgB;AAAA,EAChB;AACF,GAAqB;AACnB,QAAM,WAAW,QAAQ,MAAM,QAAQ,QAAQ,WAAW;AAE1D,SACE;AAAA,IAAC;AAAA;AAAA,MACC;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA,OACE,gBACI,QACA;AAAA,QACE,MAAM;AAAA,QACN,QAAQ;AAAA,QACR,SAAS;AAAA,UACP,EAAE,KAAK,SAAS,OAAO,QAAQ;AAAA,UAC/B,EAAE,KAAK,UAAU,OAAO,SAAS;AAAA,UACjC,EAAE,KAAK,cAAc,OAAO,eAAe;AAAA,QAC7C;AAAA,MACF;AAAA,MAEN,SAAS,SAAS,WAAW;AAAA,MAC7B;AAAA,MAEA,8BAAC,SAAI,WAAU,8BACZ,mBAAS,IAAI,CAAC,YACb;AAAA,QAAC;AAAA;AAAA,UAEC;AAAA,UACA;AAAA,UACA;AAAA;AAAA,QAHK,QAAQ;AAAA,MAIf,CACD,GACH;AAAA;AAAA,EACF;AAEJ;AAEA,SAAS,UAAU;AAAA,EACjB;AAAA,EACA;AAAA,EACA;AACF,GAIG;AACD,QAAM,QAAQ,QAAQ;AACtB,QAAM,WAAW,SAAS,QAAQ,OAAO,KAAK;AAC9C,QAAM,cAAc,QAAQ,WAAW,SAAY,OAAO,SAAS,QAAQ,QAAQ,KAAK;AACxF,QAAM,WAAW,UAAU,QAAQ,OAAO,KAAK,KAAK,UAAU,QAAQ,QAAQ,KAAK;AAEnF,SACE,qBAAC,SAAI,WAAU,gCACb;AAAA,yBAAC,SAAI,WAAU,6CACb;AAAA,2BAAC,UAAK,WAAU,iCACb;AAAA,gBAAQ;AAAA,QACR,QAAQ,UACP,oBAAC,UAAK,WAAU,oCAAoC,kBAAQ,QAAO;AAAA,SAEvE;AAAA,MACA,qBAAC,UAAK,WAAU,wDACb;AAAA,oBAAY,QAAQ,KAAK;AAAA,QACzB,QAAQ,WAAW,UAClB,qBAAC,UAAK,WAAU,qBAAoB;AAAA;AAAA,UAAI,YAAY,QAAQ,MAAM;AAAA,WAAE;AAAA,SAExE;AAAA,OACF;AAAA,IAKA;AAAA,MAAC;AAAA;AAAA,QACC,eAAW;AAAA,QACX,aAAU;AAAA,QACV,WAAU;AAAA,QAET;AAAA,kBAAQ,MAAM,IAAI,CAAC,SAClB;AAAA,YAAC;AAAA;AAAA,cAEC,aAAU;AAAA,cACV,WAAU;AAAA,cACV,OAAO;AAAA,gBACL,kBAAkB,IAAI,SAAS,KAAK,MAAM,KAAK,CAAC;AAAA,gBAChD,YAAY,IAAI,SAAS,KAAK,IAAI,KAAK,IAAI,SAAS,KAAK,MAAM,KAAK,CAAC;AAAA;AAAA;AAAA,gBAGrE,iBAAiB,+DAA+D,KAAK,MAAM;AAAA,cAC7F;AAAA;AAAA,YATK,GAAG,KAAK,IAAI,IAAI,KAAK,EAAE;AAAA,UAU9B,CACD;AAAA,UAKD;AAAA,YAAC;AAAA;AAAA,cACC,aAAU;AAAA,cACV,WAAU;AAAA,cACV,OAAO;AAAA,gBACL,kBAAkB;AAAA,gBAClB,iBAAiB,KAAK,IAAI,aAAa,CAAC;AAAA,gBACxC,WAAW,IAAI,SAAS;AAAA,gBACxB,YAAY,IAAI,QAAQ;AAAA,cAC1B;AAAA;AAAA,UACF;AAAA,UAEC,gBAAgB;AAAA;AAAA;AAAA,UAIf;AAAA,YAAC;AAAA;AAAA,cACC,aAAU;AAAA,cACV,WAAU;AAAA,cACV,OAAO,EAAE,kBAAkB,IAAI,WAAW,GAAG,mBAAmB,OAAO;AAAA;AAAA,UACzE;AAAA,UAGD;AAAA;AAAA;AAAA;AAAA;AAAA,UAMC;AAAA,YAAC;AAAA;AAAA,cACC,aAAU;AAAA,cACV,WAAU;AAAA,cACV,OAAO;AAAA,gBACL,UAAU;AAAA,cACZ;AAAA;AAAA,UACF;AAAA;AAAA;AAAA,IAEJ;AAAA,IAEC,aACC,qBAAC,SAAI,WAAU,sDACb;AAAA,0BAAC,UAAM,sBAAY,MAAM,CAAC,CAAC,GAAE;AAAA,MAC7B,oBAAC,UAAM,sBAAY,MAAM,CAAC,CAAC,GAAE;AAAA,OAC/B;AAAA,KAEJ;AAEJ;AAEA,IAAO,sBAAQ;","names":[]}
1
+ {"version":3,"sources":["../../../src/charts/BulletChart/BulletChart.tsx"],"sourcesContent":["import type { ReactNode } from 'react'\nimport { ChartFigure } from '../lib/figure'\nimport { type ChartEmptyProps } from '../lib/empty'\nimport { defaultTick } from '../lib/format'\nimport { clampedFraction } from '../lib/scale'\n\n/**\n * The heaviest and lightest a qualitative band may be, as multiples of\n * `--chart-fill`.\n *\n * Few's rule is no more than five bands in one hue, arranged so the low end of\n * the scale is the heaviest. The measure bar is solid ink on top of them, so\n * the ceiling has to stay well under it: bands that compete with the bar stop\n * being the ground it is read against.\n */\nconst BAND_WEIGHT = { heaviest: 1, lightest: 0.3 } as const\n\n/** How tall the measure bar is, as a share of the track. Few's is a third. */\nconst BAR_SHARE = 0.34\n\n/** One measure, its target, and the bands it is read against. */\nexport interface BulletMeasure {\n /** What is being measured. Doubles as the row's key, so it must be unique. */\n name: string\n /** The number the row is about. */\n value: number\n /**\n * The number the measure is being judged against — last year, the budget,\n * the SLO. Drawn as a rule across the bar, not as a second bar.\n */\n target?: number\n /**\n * The qualitative bands, as ascending UPPER bounds. `[60, 80]` on a 0–100\n * scale draws three bands: 0–60, 60–80, 80–100.\n *\n * Few's limit is five, and it is a real one: past that the shades stop being\n * separable and the bands become a gradient nobody can read a boundary off.\n */\n ranges?: number[]\n /** The scale, as `[min, max]`. Falls back to the chart's, then to the data. */\n domain?: [number, number]\n /** A second line under the name — the unit, the window, the owner. */\n detail?: ReactNode\n}\n\n/** A measure with its scale and bands resolved. */\ninterface ResolvedMeasure extends BulletMeasure {\n domain: [number, number]\n bands: { from: number; to: number; weight: number }[]\n /** Written out for the hidden table, which cannot print an array. */\n rangeLabel: string\n}\n\n/** Where a value sits on the scale, 0 to 1, clamped to it. */\nfunction position(value: number, [low, high]: [number, number]): number {\n return clampedFraction(value, low, high)\n}\n\n/** Whether a number is off the end of the scale it is being drawn against. */\nfunction overflows(value: number | undefined, [low, high]: [number, number]): boolean {\n return value !== undefined && Number.isFinite(value) && (value > high || value < low)\n}\n\n/**\n * A share of the track, as a CSS percentage.\n *\n * Rounded, because binary floating point turns `0.8 - 0.5` into\n * `30.000000000000004%` — a number no layout engine cares about and every\n * reader of the DOM does. Four places is finer than a subpixel on any track\n * this chart will be given.\n */\nfunction pct(share: number): string {\n return `${Number((share * 100).toFixed(4))}%`\n}\n\n/** The bands, from the ascending upper bounds, each with its own weight. */\nfunction bands(\n ranges: number[],\n [low, high]: [number, number],\n formatValue: (value: number) => string,\n): { list: ResolvedMeasure['bands']; label: string } {\n const set = [...new Set(ranges.filter(Number.isFinite))].sort((a, b) => a - b)\n // A bound off the end of the scale has no boundary to draw: it would be a\n // band with no width, or one that starts past the track. It is dropped from\n // the picture and kept in the LABEL, because a caller who pinned `[0, 50]`\n // and passed `[60, 80]` has made a mistake, and one flat band that says\n // nothing is how it stays invisible.\n const bounds = set.filter((bound) => bound > low && bound < high)\n\n const edges = [low, ...bounds, high]\n const count = edges.length - 1\n const { heaviest, lightest } = BAND_WEIGHT\n\n const list = Array.from({ length: count }, (_, index) => ({\n from: edges[index]!,\n to: edges[index + 1]!,\n // Heaviest at the low end, lightening as the scale rises — so the measure\n // bar, which is solid, stands out most where performance is best.\n weight: count === 1 ? lightest : heaviest - (index / (count - 1)) * (heaviest - lightest),\n }))\n\n // The label names the bounds the caller SET, not the band tops: the last\n // band's top is the end of the scale, which is not a threshold anybody chose\n // — and not the drawable subset either, or a bound the scale cannot hold\n // would go missing from the reading as well as from the picture.\n return { list, label: set.map(formatValue).join(', ') }\n}\n\n/** Every measure, with its scale and bands worked out. */\nfunction resolve(\n data: BulletMeasure[],\n domain: [number, number] | undefined,\n ranges: number[] | undefined,\n formatValue: (value: number) => string,\n): ResolvedMeasure[] {\n return data.map((measure) => {\n const own = measure.ranges ?? ranges ?? []\n const high = Math.max(measure.value, measure.target ?? Number.NEGATIVE_INFINITY, ...own)\n // Anchored at zero unless told otherwise: the measure is drawn as a BAR,\n // and a bar read off a scale that does not start at zero overstates every\n // difference on it.\n const scale: [number, number] = measure.domain ?? domain ?? [Math.min(0, measure.value), high || 1]\n const built = bands(own, scale, formatValue)\n\n return { ...measure, domain: scale, bands: built.list, rangeLabel: built.label }\n })\n}\n\nexport interface BulletChartProps {\n /**\n * What the chart shows, in a sentence a reader could act on. Required, and\n * announced to a screen reader even when it is not printed.\n */\n title: string\n /** Prints the title above the rows instead of hiding it from sight. */\n showTitle?: boolean\n /** A line under the title — the unit, the window, the caveat. */\n description?: ReactNode\n /** One entry per measure. Order is the reading order; it is not sorted. */\n data: BulletMeasure[]\n /**\n * The bands every measure falls back to, as ascending upper bounds.\n *\n * Shared bands only mean something when the measures share a scale. Where\n * they do not — a latency beside a conversion rate — put `ranges` and\n * `domain` on each measure instead.\n */\n ranges?: number[]\n /** The scale every measure falls back to, as `[min, max]`. */\n domain?: [number, number]\n /** Formats every number the chart prints. */\n formatValue?: (value: number) => string\n /** Prints the scale's two ends under each track. */\n showScale?: boolean\n /** Merged onto the figure, last, so a call site can size or space it. */\n className?: string\n /** Drops the hidden table view. Only correct when the page prints the data itself. */\n hideDataTable?: boolean\n /** What the chart shows when it has nothing to draw. */\n empty?: ChartEmptyProps\n}\n\n/**\n * A measure, its target, and the bands that say whether the number is any good\n * — one dense row per thing being tracked.\n *\n * Stephen Few designed this to replace the dashboard gauge, and the argument\n * has held: a speedometer spends a whole card saying one number badly, where a\n * bullet graph says the same number, its target, and the qualitative context\n * around it in the height of a line of text. Ten of them stack into a status\n * page a reader can scan in one pass.\n *\n * What it will not do is show change over time. A bullet graph is one instant,\n * and `target` is the only comparison it carries; when the question is\n * \"how did we get here\" it wants a `<LineChart>`, and when several measures\n * have to be compared against each OTHER rather than each against its own\n * target, a `<BarList>` ranks them and this does not.\n *\n * **The bands are the part to be careful with.** They are a judgement — someone\n * decided that 80 is \"good\" — drawn in the same ink as the measurement, and a\n * reader has no way to tell an agreed threshold from a number somebody typed.\n * They also compress: a value near the top of the scale sits in the same band\n * whether it cleared the boundary by a point or by thirty. The band answers\n * \"is this acceptable\", never \"by how much\".\n *\n * No rendering engine, deliberately — this is the one chart in the set with\n * nothing to compute. Each row is a single linear scale with no axis, no ticks\n * and no shared plot area, so it is laid out as HTML: the bands and the bar are\n * inline-axis offsets, which means the whole chart mirrors correctly in a\n * right-to-left document, where an SVG drawn in user space would not.\n *\n * @example\n * <BulletChart\n * title=\"Service levels against target\"\n * data={[{ name: 'Uptime', value: 99.4, target: 99.9, ranges: [98, 99.5] }]}\n * domain={[95, 100]}\n * />\n */\nexport function BulletChart({\n title,\n showTitle,\n description,\n data,\n ranges,\n domain,\n formatValue = defaultTick,\n showScale = false,\n className,\n hideDataTable = false,\n empty,\n}: BulletChartProps) {\n const measures = resolve(data, domain, ranges, formatValue)\n\n return (\n <ChartFigure\n title={title}\n showTitle={showTitle}\n description={description}\n className={className}\n table={\n hideDataTable\n ? false\n : {\n rows: measures as unknown as Record<string, unknown>[],\n rowKey: 'name',\n columns: [\n { key: 'value', label: 'Value' },\n { key: 'target', label: 'Target' },\n { key: 'rangeLabel', label: 'Range bounds' },\n ],\n }\n }\n isEmpty={measures.length === 0}\n empty={empty}\n >\n <div className=\"flex w-full flex-col gap-4\">\n {measures.map((measure) => (\n <BulletRow\n key={measure.name}\n measure={measure}\n formatValue={formatValue}\n showScale={showScale}\n />\n ))}\n </div>\n </ChartFigure>\n )\n}\n\nfunction BulletRow({\n measure,\n formatValue,\n showScale,\n}: {\n measure: ResolvedMeasure\n formatValue: (value: number) => string\n showScale: boolean\n}) {\n const scale = measure.domain\n const barShare = position(measure.value, scale)\n const targetShare = measure.target === undefined ? null : position(measure.target, scale)\n const runsOver = overflows(measure.value, scale) || overflows(measure.target, scale)\n\n return (\n <div className=\"flex w-full flex-col gap-1.5\">\n <div className=\"flex items-baseline justify-between gap-3\">\n <span className=\"truncate text-sm text-(--ink)\">\n {measure.name}\n {measure.detail && (\n <span className=\"ms-2 mono-meta text-(--ink-3-aa)\">{measure.detail}</span>\n )}\n </span>\n <span className=\"shrink-0 font-mono text-xs tabular-nums text-(--ink)\">\n {formatValue(measure.value)}\n {measure.target !== undefined && (\n <span className=\"text-(--ink-3-aa)\"> / {formatValue(measure.target)}</span>\n )}\n </span>\n </div>\n\n {/* The graphic repeats what the line above and the hidden table already\n say, so it is out of the accessibility tree entirely rather than\n announced a third time as a stack of empty boxes. */}\n <div\n aria-hidden\n data-slot=\"bullet-track\"\n className=\"relative h-6 w-full overflow-hidden rounded-(--radius-sm)\"\n >\n {measure.bands.map((band) => (\n <span\n key={`${band.from}-${band.to}`}\n data-slot=\"bullet-band\"\n className=\"absolute inset-y-0 block\"\n style={{\n insetInlineStart: pct(position(band.from, scale)),\n inlineSize: pct(position(band.to, scale) - position(band.from, scale)),\n // The one translucent-mark pair in the token layer, because it is\n // the only one that holds a different number on each ground.\n backgroundColor: `color-mix(in srgb, var(--series-1) calc(var(--chart-fill) * ${band.weight} * 100%), transparent)`,\n }}\n />\n ))}\n\n {/* Solid, and a third of the track's height. The measure is the one\n thing here that is a measurement rather than a judgement, so it is\n the only mark drawn at full weight. */}\n <span\n data-slot=\"bullet-bar\"\n className=\"absolute block bg-(--series-1)\"\n style={{\n insetInlineStart: 0,\n insetBlockStart: pct((1 - BAR_SHARE) / 2),\n blockSize: pct(BAR_SHARE),\n inlineSize: pct(barShare),\n }}\n />\n\n {targetShare !== null && (\n // A rule ACROSS the bar rather than a second bar beside it: the two\n // are different kinds of number, and the reader has to be able to\n // tell at a glance which one was achieved and which was asked for.\n //\n // The paper halo is what makes that true where the rule lands ON the\n // bar. Both are drawn in --ink, so a target inside the achieved range\n // — which is every measure that is doing well — was an ink rule on an\n // ink bar and simply not there.\n <span\n data-slot=\"bullet-target\"\n className=\"absolute inset-y-1 block w-0.5 bg-(--ink) shadow-[0_0_0_1px_var(--paper)]\"\n style={{\n insetInlineStart: pct(targetShare),\n // Pulled back by its own width in PROPORTION to where it sits:\n // flush at the start at 0, centred in the middle, flush at the end\n // at 1. A fixed half-width pull put a target of zero half outside\n // the track, where `overflow-hidden` took it — so \"Open incidents,\n // target 0\" drew no target at all, on the one measure whose whole\n // point is the distance from it.\n marginInlineStart: `${-2 * targetShare}px`,\n }}\n />\n )}\n\n {runsOver && (\n // Clamped and silent, a number past the end of the scale fills the\n // track exactly as a number AT the end of it does — so the one thing\n // the row cannot say is the one thing the reader most needs to know.\n // The notch is what breaks that tie; the printed figure above still\n // carries the real number.\n <span\n data-slot=\"bullet-overflow\"\n className=\"absolute inset-y-0 end-0 block w-1.5 bg-(--chart-surface)\"\n style={{\n clipPath: 'polygon(100% 0, 0 50%, 100% 100%)',\n }}\n />\n )}\n </div>\n\n {showScale && (\n <div className=\"flex justify-between mono-meta text-(--chart-axis)\">\n <span>{formatValue(scale[0])}</span>\n <span>{formatValue(scale[1])}</span>\n </div>\n )}\n </div>\n )\n}\n\nexport default BulletChart\n"],"mappings":"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","names":[]}
@@ -111,7 +111,9 @@ function Radar({
111
111
  const opacity = { stroke: dimmed ? 0.2 : 1, fill: dimmed ? 0.1 : 1, dot: dimmed ? 0.2 : 1 };
112
112
  const isFilled = variant === "filled";
113
113
  const { dot, activeDot } = resolveDots(children, id, dataKey, opacity.dot);
114
- const filledStrength = `calc(var(--chart-fill) * ${FILL_STRENGTH})`;
114
+ const dimFill = dimmed ? ` * ${opacity.fill}` : "";
115
+ const filledStrength = `calc(var(--chart-fill) * ${FILL_STRENGTH}${dimFill})`;
116
+ const filledOpacity = fillOpacity === void 0 ? filledStrength : fillOpacity * opacity.fill;
115
117
  const stops = colorStops(config[dataKey]);
116
118
  return /* @__PURE__ */ jsxs(Fragment, { children: [
117
119
  /* @__PURE__ */ jsx(
@@ -122,7 +124,7 @@ function Radar({
122
124
  strokeOpacity: opacity.stroke,
123
125
  strokeWidth: STROKE_WIDTH,
124
126
  fill: isFilled ? `url(#${id}-fill-${dataKey})` : "none",
125
- fillOpacity: isFilled ? (fillOpacity ?? filledStrength) * opacity.fill : 0,
127
+ fillOpacity: isFilled ? filledOpacity : 0,
126
128
  dot,
127
129
  activeDot,
128
130
  filter: glowing ? `url(#${id}-glow-${dataKey})` : void 0,
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../src/charts/RadarChart/RadarChart.tsx"],"sourcesContent":["'use client'\n\nimport {\n Children,\n createContext,\n isValidElement,\n useContext,\n useEffect,\n useId,\n useMemo,\n useState,\n type ComponentProps,\n type FC,\n type ReactElement,\n type ReactNode,\n} from 'react'\nimport {\n PolarAngleAxis as RechartsPolarAngleAxis,\n PolarGrid as RechartsPolarGrid,\n PolarRadiusAxis as RechartsPolarRadiusAxis,\n Radar as RechartsRadar,\n RadarChart as RechartsRadarChart,\n} from 'recharts'\nimport { useReducedMotion } from 'motion/react'\nimport { ChartContainer, colorStops, type ChartConfig } from '../lib/chart'\nimport { ChartFigure } from '../lib/figure'\nimport { type ChartEmptyProps } from '../lib/empty'\nimport { useChartSelection } from '../lib/selection'\nimport { ChartBackground, type ChartBackgroundVariant } from '../lib/background'\nimport { ChartDot, type ChartDotVariant } from '../lib/dot'\nimport { ChartLegend, ChartLegendContent } from '../lib/legend'\nimport { ChartTooltip, ChartTooltipContent } from '../lib/tooltip'\nimport { LoadingIndicator } from '../lib/loading'\nimport { ColorStops, GlowFilter, SeriesGradient } from '../lib/paint'\nimport type { ChartLegendSlotProps, ChartTooltipSlotProps } from '../AreaChart/AreaChart'\n\nconst STROKE_WIDTH = 1\n/**\n * How solid a filled radar is, as a multiplier on `--chart-fill`.\n *\n * Above 1 because a radar's fill is the mark itself rather than a wash under a\n * line — it has to hold its shape where two of them overlap.\n */\nconst FILL_STRENGTH = 2.2\nconst LOADING_POINTS = 6\n\n/** One skeleton morph, in milliseconds. */\nconst LOADING_CYCLE = 1500\n\n/**\n * Whether a radar is filled or left as an outline.\n *\n * `lines` is the honest default above two series: filled polygons overlap, and\n * the reader is then judging areas through two layers of translucency, which is\n * the one thing a radar is worst at.\n */\nexport type RadarVariant = 'filled' | 'lines'\n\ninterface RadarChartContextValue {\n config: ChartConfig\n isLoading: boolean\n selectedDataKey: string | null\n selectDataKey: (dataKey: string | null) => void\n}\n\nconst RadarChartContext = createContext<RadarChartContextValue | null>(null)\n\nfunction useRadarChart(): RadarChartContextValue {\n const context = useContext(RadarChartContext)\n if (!context) throw new Error('A radar chart part must be rendered inside <RadarChart>')\n return context\n}\n\ntype ValidateKeys<TData, TConfig> = {\n [K in keyof TConfig]: K extends keyof TData ? ChartConfig[string] : never\n}\n\nexport interface RadarChartProps<\n TData extends Record<string, unknown>,\n TConfig extends Record<string, ChartConfig[string]>,\n> {\n config: TConfig & ValidateKeys<TData, TConfig>\n /** The rows the chart draws. One entry per point, bar or category. */\n data: TData[]\n /**\n * What the chart shows, in a sentence a reader could act on. Required, and\n * announced to a screen reader even when it is not printed.\n */\n title: string\n /** Prints the title above the plot instead of hiding it from sight. */\n showTitle?: boolean\n /** A line under the title — the unit, the window, the caveat. */\n description?: ReactNode\n /**\n * The composed parts — axes, grid, tooltip, legend, and the marks\n * themselves.\n */\n children: ReactNode\n /** Merged onto the figure, last, so a call site can size or space it. */\n className?: string\n /** Escape hatch onto the raw Recharts chart element. */\n chartProps?: ComponentProps<typeof RechartsRadarChart>\n /** The series lit on first render, when the chart keeps its own selection. */\n defaultSelectedDataKey?: string | null\n /**\n * The selected series, driven from outside.\n *\n * Give this and the chart follows it; leave it undefined and the chart keeps\n * its own, starting from `defaultSelectedDataKey`.\n */\n selectedDataKey?: string | null\n /** Fires when the selection changes, and with null when it is cleared. */\n onSelectionChange?: (selectedDataKey: string | null) => void\n /**\n * Swaps the marks for an animated skeleton, keeping the measured height so\n * the page does not jump when the data lands.\n */\n isLoading?: boolean\n /** How many points the skeleton draws. */\n loadingPoints?: number\n /** The row field naming each spoke. Used by the table view. */\n angleDataKey?: keyof TData & string\n /**\n * Drops the hidden table view. Only correct when the page prints the data\n * itself.\n */\n hideDataTable?: boolean\n /**\n * What the chart shows when it has nothing to draw. `false` keeps the empty\n * plot, for a chart whose emptiness is itself the reading.\n */\n empty?: ChartEmptyProps | false\n}\n\n/**\n * A profile across several named dimensions — the shape for \"what is this thing\n * strong and weak at\".\n *\n * It reads a SHAPE, not a set of values: the area a radar encloses depends on\n * the order the spokes happen to be in, so it is the wrong chart for comparing\n * magnitudes and the right one for recognising a silhouette. Two or three\n * series at most.\n *\n * @example\n * <RadarChart title=\"Team skills\" config={config} data={data} angleDataKey=\"skill\">\n * <RadarChart.PolarGrid />\n * <RadarChart.PolarAngleAxis dataKey=\"skill\" />\n * <RadarChart.Tooltip />\n * <RadarChart.Radar dataKey=\"current\" />\n * </RadarChart>\n */\nexport function RadarChart<\n TData extends Record<string, unknown>,\n TConfig extends Record<string, ChartConfig[string]>,\n>({\n config,\n data,\n title,\n showTitle,\n description,\n children,\n className,\n chartProps,\n defaultSelectedDataKey = null,\n selectedDataKey: controlledDataKey,\n onSelectionChange,\n isLoading = false,\n loadingPoints,\n angleDataKey,\n hideDataTable = false,\n empty,\n}: RadarChartProps<TData, TConfig>) {\n const chartId = useId().replace(/:/g, '')\n const loadingData = useLoadingShape(isLoading, loadingPoints)\n const [selectedDataKey, selectDataKey] = useChartSelection(\n controlledDataKey,\n defaultSelectedDataKey,\n onSelectionChange,\n )\n\n const context = useMemo<RadarChartContextValue>(\n () => ({ config, isLoading, selectedDataKey, selectDataKey }),\n [config, isLoading, selectDataKey, selectedDataKey],\n )\n\n return (\n <RadarChartContext.Provider value={context}>\n <ChartFigure\n title={title}\n showTitle={showTitle}\n description={description}\n className={className}\n table={\n hideDataTable\n ? false\n : {\n rows: data,\n rowKey: angleDataKey,\n columns: Object.entries(config).map(([key, series]) => ({\n key,\n label: series.label,\n })),\n }\n }\n isEmpty={!isLoading && data.length === 0}\n empty={empty}\n >\n <ChartContainer config={config}>\n <RechartsRadarChart id={chartId} data={isLoading ? loadingData : data} {...chartProps}>\n {children}\n {isLoading && <LoadingRadar />}\n </RechartsRadarChart>\n </ChartContainer>\n <LoadingIndicator isLoading={isLoading} />\n </ChartFigure>\n </RadarChartContext.Provider>\n )\n}\n\nexport interface RadarProps {\n dataKey: string\n variant?: RadarVariant\n /**\n * How solid the filled area is, 0–1. Left unset it follows `--chart-fill`,\n * which is what keeps it readable on both grounds.\n */\n fillOpacity?: number\n /** A halo around the outline — for the one series that is the point. */\n glowing?: boolean\n isClickable?: boolean\n children?: ReactNode\n radarProps?: Omit<ComponentProps<typeof RechartsRadar>, 'dataKey'>\n}\n\n/** One radar series, with its stroke, fill and glow scoped to its own id. */\nfunction Radar({\n dataKey,\n variant = 'filled',\n fillOpacity,\n glowing = false,\n isClickable = false,\n children,\n radarProps,\n}: RadarProps) {\n const { config, isLoading, selectedDataKey, selectDataKey } = useRadarChart()\n const id = useId().replace(/:/g, '')\n\n if (isLoading) return null\n\n const isSelected = selectedDataKey === null || selectedDataKey === dataKey\n const dimmed = isClickable && !isSelected\n // The fill recedes twice as far as the stroke. A dimmed outline still traces\n // its shape, which is what a radar is read by; a dimmed fill just muddies the\n // one that was picked.\n const opacity = { stroke: dimmed ? 0.2 : 1, fill: dimmed ? 0.1 : 1, dot: dimmed ? 0.2 : 1 }\n const isFilled = variant === 'filled'\n\n const { dot, activeDot } = resolveDots(children, id, dataKey, opacity.dot)\n // `--chart-fill` is a number, not a length, so it can be read into a\n // calc() the SVG attribute accepts — which is how the default follows the\n // ground without the component knowing which ground it is on.\n const filledStrength = `calc(var(--chart-fill) * ${FILL_STRENGTH})` as unknown as number\n const stops = colorStops(config[dataKey])\n\n return (\n <>\n <RechartsRadar\n dataKey={dataKey}\n stroke={`url(#${id}-stroke-${dataKey})`}\n strokeOpacity={opacity.stroke}\n strokeWidth={STROKE_WIDTH}\n fill={isFilled ? `url(#${id}-fill-${dataKey})` : 'none'}\n fillOpacity={isFilled ? (fillOpacity ?? filledStrength) * opacity.fill : 0}\n dot={dot}\n activeDot={activeDot}\n filter={glowing ? `url(#${id}-glow-${dataKey})` : undefined}\n className=\"transition-opacity duration-(--duration-base)\"\n style={isClickable ? { cursor: 'pointer' } : undefined}\n onClick={() => {\n if (!isClickable) return\n selectDataKey(selectedDataKey === dataKey ? null : dataKey)\n }}\n {...radarProps}\n />\n <defs>\n {/* The dots read this one; the stroke and fill get their own below. */}\n <SeriesGradient id={id} dataKey={dataKey} config={config} />\n <linearGradient id={`${id}-stroke-${dataKey}`} x1=\"0\" y1=\"0\" x2=\"1\" y2=\"1\">\n <ColorStops dataKey={dataKey} stops={stops} />\n </linearGradient>\n {isFilled && (\n // Radial, and fading outward: a flat fill makes the outer ring — the\n // part carrying the shape — the least readable part of the mark.\n <radialGradient id={`${id}-fill-${dataKey}`} cx=\"50%\" cy=\"50%\" r=\"50%\">\n <ColorStops\n dataKey={dataKey}\n stops={stops}\n opacities={\n stops === 1 ? [0.8, 0.3] : Array.from({ length: stops }, (_, i) => (i === 0 ? 0.8 : 0.3))\n }\n />\n </radialGradient>\n )}\n {glowing && <GlowFilter id={id} dataKey={dataKey} spread={4} intensity={0.6} />}\n </defs>\n </>\n )\n}\n\nexport interface DotProps {\n variant?: ChartDotVariant\n}\n\n/** The resting point marker for the radar it is composed inside. A slot. */\nconst Dot: FC<DotProps> = () => null\n\n/** The marker under the pointer. */\nconst ActiveDot: FC<DotProps> = () => null\n\nfunction resolveDots(children: ReactNode, id: string, dataKey: string, dotOpacity: number) {\n let dot: ComponentProps<typeof RechartsRadar>['dot'] = false\n let activeDot: ComponentProps<typeof RechartsRadar>['activeDot'] = false\n\n Children.forEach(children, (child) => {\n if (!isValidElement(child)) return\n const { variant } = (child as ReactElement<DotProps>).props\n\n if (child.type === Dot) {\n dot = <ChartDot variant={variant} dataKey={dataKey} chartId={id} fillOpacity={dotOpacity} />\n }\n if (child.type === ActiveDot) {\n activeDot = (\n <ChartDot variant={variant} dataKey={dataKey} chartId={id} fillOpacity={dotOpacity} />\n )\n }\n })\n\n return { dot, activeDot }\n}\n\n/**\n * The web behind the marks. `gridType=\"circle\"` swaps the polygon for rings,\n * which reads as a scale rather than as a shape and suits a single series.\n */\nfunction PolarGrid({\n gridType = 'polygon',\n stroke = 'var(--chart-grid)',\n strokeDasharray = '3 4',\n ...rest\n}: ComponentProps<typeof RechartsPolarGrid>) {\n return (\n <RechartsPolarGrid\n gridType={gridType}\n stroke={stroke}\n strokeDasharray={strokeDasharray}\n {...rest}\n />\n )\n}\n\n/** The spoke labels around the perimeter. */\nfunction PolarAngleAxis({\n tick = { fill: 'var(--chart-axis)', fontSize: 12 },\n tickLine = false,\n ...rest\n}: ComponentProps<typeof RechartsPolarAngleAxis>) {\n const { isLoading } = useRadarChart()\n if (isLoading) return null\n return <RechartsPolarAngleAxis tick={tick} tickLine={tickLine} {...rest} />\n}\n\n/** The scale running out from the centre. */\nfunction PolarRadiusAxis({\n tick = { fill: 'var(--chart-axis)', fontSize: 10 },\n tickLine = false,\n axisLine = false,\n ...rest\n}: ComponentProps<typeof RechartsPolarRadiusAxis>) {\n const { isLoading } = useRadarChart()\n if (isLoading) return null\n return (\n <RechartsPolarRadiusAxis tick={tick} tickLine={tickLine} axisLine={axisLine} {...rest} />\n )\n}\n\n/** The hover panel. No crosshair: there is no straight axis to run one along. */\nfunction Tooltip({ variant, roundness, defaultIndex }: ChartTooltipSlotProps) {\n const { isLoading, selectedDataKey } = useRadarChart()\n if (isLoading) return null\n\n return (\n <ChartTooltip\n defaultIndex={defaultIndex}\n cursor={false}\n content={\n <ChartTooltipContent selected={selectedDataKey} roundness={roundness} variant={variant} />\n }\n />\n )\n}\n\n/** The key. Under the web and centred, since the spokes name dimensions, not series. */\nfunction Legend({\n variant,\n align = 'center',\n verticalAlign = 'bottom',\n isClickable = false,\n}: ChartLegendSlotProps) {\n const { isLoading, selectedDataKey, selectDataKey } = useRadarChart()\n if (isLoading) return null\n\n return (\n <ChartLegend\n verticalAlign={verticalAlign}\n align={align}\n content={\n <ChartLegendContent\n selected={selectedDataKey}\n onSelectChange={selectDataKey}\n isClickable={isClickable}\n variant={variant}\n />\n }\n />\n )\n}\n\n/** The decorative plate behind the web. */\nfunction Background({ variant = 'dots' }: { variant?: ChartBackgroundVariant }) {\n const { isLoading } = useRadarChart()\n if (isLoading) return null\n return <ChartBackground variant={variant} />\n}\n\n/** A deterministic spoke value, so the server and the client agree. */\nfunction shape(index: number, cycle: number): number {\n const x = Math.sin(index * 91.7 + cycle * 47.3) * 21_942.7\n return 30 + (x - Math.floor(x)) * 70\n}\n\n/**\n * The skeleton's spokes, re-rolled once per cycle.\n *\n * An interval rather than the shimmer's exit callback: a radar's skeleton\n * animates by MORPHING between shapes, which is Recharts' own transition, so\n * there is no travelling highlight to synchronise against.\n */\nfunction useLoadingShape(isLoading: boolean, points = LOADING_POINTS) {\n const [cycle, setCycle] = useState(0)\n\n useEffect(() => {\n if (!isLoading) return\n const timer = setInterval(() => setCycle((previous) => previous + 1), LOADING_CYCLE)\n return () => clearInterval(timer)\n }, [isLoading])\n\n return useMemo(\n () =>\n Array.from({ length: points }, (_, index) => ({\n spoke: String.fromCharCode(65 + index),\n value: shape(index, cycle),\n })),\n [cycle, points],\n )\n}\n\n/** The skeleton radar, morphing between shapes while the data is in flight. */\nfunction LoadingRadar() {\n const reduceMotion = useReducedMotion()\n\n return (\n <RechartsRadar\n dataKey=\"value\"\n stroke=\"currentColor\"\n strokeOpacity={0.3}\n strokeWidth={2}\n fill=\"currentColor\"\n fillOpacity={0.1}\n dot={false}\n className=\"text-(--ink)\"\n isAnimationActive={!reduceMotion}\n animationDuration={LOADING_CYCLE}\n animationEasing=\"ease-in-out\"\n />\n )\n}\n\nRadarChart.Radar = Radar\nRadarChart.Dot = Dot\nRadarChart.ActiveDot = ActiveDot\nRadarChart.PolarGrid = PolarGrid\nRadarChart.PolarAngleAxis = PolarAngleAxis\nRadarChart.PolarRadiusAxis = PolarRadiusAxis\nRadarChart.Tooltip = Tooltip\nRadarChart.Legend = Legend\nRadarChart.Background = Background\n\nexport default RadarChart\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../../../src/charts/RadarChart/RadarChart.tsx"],"sourcesContent":["'use client'\n\nimport {\n Children,\n createContext,\n isValidElement,\n useContext,\n useEffect,\n useId,\n useMemo,\n useState,\n type ComponentProps,\n type FC,\n type ReactElement,\n type ReactNode,\n} from 'react'\nimport {\n PolarAngleAxis as RechartsPolarAngleAxis,\n PolarGrid as RechartsPolarGrid,\n PolarRadiusAxis as RechartsPolarRadiusAxis,\n Radar as RechartsRadar,\n RadarChart as RechartsRadarChart,\n} from 'recharts'\nimport { useReducedMotion } from 'motion/react'\nimport { ChartContainer, colorStops, type ChartConfig } from '../lib/chart'\nimport { ChartFigure } from '../lib/figure'\nimport { type ChartEmptyProps } from '../lib/empty'\nimport { useChartSelection } from '../lib/selection'\nimport { ChartBackground, type ChartBackgroundVariant } from '../lib/background'\nimport { ChartDot, type ChartDotVariant } from '../lib/dot'\nimport { ChartLegend, ChartLegendContent } from '../lib/legend'\nimport { ChartTooltip, ChartTooltipContent } from '../lib/tooltip'\nimport { LoadingIndicator } from '../lib/loading'\nimport { ColorStops, GlowFilter, SeriesGradient } from '../lib/paint'\nimport type { ChartLegendSlotProps, ChartTooltipSlotProps } from '../AreaChart/AreaChart'\n\nconst STROKE_WIDTH = 1\n/**\n * How solid a filled radar is, as a multiplier on `--chart-fill`.\n *\n * Above 1 because a radar's fill is the mark itself rather than a wash under a\n * line — it has to hold its shape where two of them overlap.\n */\nconst FILL_STRENGTH = 2.2\nconst LOADING_POINTS = 6\n\n/** One skeleton morph, in milliseconds. */\nconst LOADING_CYCLE = 1500\n\n/**\n * Whether a radar is filled or left as an outline.\n *\n * `lines` is the honest default above two series: filled polygons overlap, and\n * the reader is then judging areas through two layers of translucency, which is\n * the one thing a radar is worst at.\n */\nexport type RadarVariant = 'filled' | 'lines'\n\ninterface RadarChartContextValue {\n config: ChartConfig\n isLoading: boolean\n selectedDataKey: string | null\n selectDataKey: (dataKey: string | null) => void\n}\n\nconst RadarChartContext = createContext<RadarChartContextValue | null>(null)\n\nfunction useRadarChart(): RadarChartContextValue {\n const context = useContext(RadarChartContext)\n if (!context) throw new Error('A radar chart part must be rendered inside <RadarChart>')\n return context\n}\n\ntype ValidateKeys<TData, TConfig> = {\n [K in keyof TConfig]: K extends keyof TData ? ChartConfig[string] : never\n}\n\nexport interface RadarChartProps<\n TData extends Record<string, unknown>,\n TConfig extends Record<string, ChartConfig[string]>,\n> {\n config: TConfig & ValidateKeys<TData, TConfig>\n /** The rows the chart draws. One entry per point, bar or category. */\n data: TData[]\n /**\n * What the chart shows, in a sentence a reader could act on. Required, and\n * announced to a screen reader even when it is not printed.\n */\n title: string\n /** Prints the title above the plot instead of hiding it from sight. */\n showTitle?: boolean\n /** A line under the title — the unit, the window, the caveat. */\n description?: ReactNode\n /**\n * The composed parts — axes, grid, tooltip, legend, and the marks\n * themselves.\n */\n children: ReactNode\n /** Merged onto the figure, last, so a call site can size or space it. */\n className?: string\n /** Escape hatch onto the raw Recharts chart element. */\n chartProps?: ComponentProps<typeof RechartsRadarChart>\n /** The series lit on first render, when the chart keeps its own selection. */\n defaultSelectedDataKey?: string | null\n /**\n * The selected series, driven from outside.\n *\n * Give this and the chart follows it; leave it undefined and the chart keeps\n * its own, starting from `defaultSelectedDataKey`.\n */\n selectedDataKey?: string | null\n /** Fires when the selection changes, and with null when it is cleared. */\n onSelectionChange?: (selectedDataKey: string | null) => void\n /**\n * Swaps the marks for an animated skeleton, keeping the measured height so\n * the page does not jump when the data lands.\n */\n isLoading?: boolean\n /** How many points the skeleton draws. */\n loadingPoints?: number\n /** The row field naming each spoke. Used by the table view. */\n angleDataKey?: keyof TData & string\n /**\n * Drops the hidden table view. Only correct when the page prints the data\n * itself.\n */\n hideDataTable?: boolean\n /**\n * What the chart shows when it has nothing to draw. `false` keeps the empty\n * plot, for a chart whose emptiness is itself the reading.\n */\n empty?: ChartEmptyProps | false\n}\n\n/**\n * A profile across several named dimensions — the shape for \"what is this thing\n * strong and weak at\".\n *\n * It reads a SHAPE, not a set of values: the area a radar encloses depends on\n * the order the spokes happen to be in, so it is the wrong chart for comparing\n * magnitudes and the right one for recognising a silhouette. Two or three\n * series at most.\n *\n * @example\n * <RadarChart title=\"Team skills\" config={config} data={data} angleDataKey=\"skill\">\n * <RadarChart.PolarGrid />\n * <RadarChart.PolarAngleAxis dataKey=\"skill\" />\n * <RadarChart.Tooltip />\n * <RadarChart.Radar dataKey=\"current\" />\n * </RadarChart>\n */\nexport function RadarChart<\n TData extends Record<string, unknown>,\n TConfig extends Record<string, ChartConfig[string]>,\n>({\n config,\n data,\n title,\n showTitle,\n description,\n children,\n className,\n chartProps,\n defaultSelectedDataKey = null,\n selectedDataKey: controlledDataKey,\n onSelectionChange,\n isLoading = false,\n loadingPoints,\n angleDataKey,\n hideDataTable = false,\n empty,\n}: RadarChartProps<TData, TConfig>) {\n const chartId = useId().replace(/:/g, '')\n const loadingData = useLoadingShape(isLoading, loadingPoints)\n const [selectedDataKey, selectDataKey] = useChartSelection(\n controlledDataKey,\n defaultSelectedDataKey,\n onSelectionChange,\n )\n\n const context = useMemo<RadarChartContextValue>(\n () => ({ config, isLoading, selectedDataKey, selectDataKey }),\n [config, isLoading, selectDataKey, selectedDataKey],\n )\n\n return (\n <RadarChartContext.Provider value={context}>\n <ChartFigure\n title={title}\n showTitle={showTitle}\n description={description}\n className={className}\n table={\n hideDataTable\n ? false\n : {\n rows: data,\n rowKey: angleDataKey,\n columns: Object.entries(config).map(([key, series]) => ({\n key,\n label: series.label,\n })),\n }\n }\n isEmpty={!isLoading && data.length === 0}\n empty={empty}\n >\n <ChartContainer config={config}>\n <RechartsRadarChart id={chartId} data={isLoading ? loadingData : data} {...chartProps}>\n {children}\n {isLoading && <LoadingRadar />}\n </RechartsRadarChart>\n </ChartContainer>\n <LoadingIndicator isLoading={isLoading} />\n </ChartFigure>\n </RadarChartContext.Provider>\n )\n}\n\nexport interface RadarProps {\n dataKey: string\n variant?: RadarVariant\n /**\n * How solid the filled area is, 0–1. Left unset it follows `--chart-fill`,\n * which is what keeps it readable on both grounds.\n */\n fillOpacity?: number\n /** A halo around the outline — for the one series that is the point. */\n glowing?: boolean\n isClickable?: boolean\n children?: ReactNode\n radarProps?: Omit<ComponentProps<typeof RechartsRadar>, 'dataKey'>\n}\n\n/** One radar series, with its stroke, fill and glow scoped to its own id. */\nfunction Radar({\n dataKey,\n variant = 'filled',\n fillOpacity,\n glowing = false,\n isClickable = false,\n children,\n radarProps,\n}: RadarProps) {\n const { config, isLoading, selectedDataKey, selectDataKey } = useRadarChart()\n const id = useId().replace(/:/g, '')\n\n if (isLoading) return null\n\n const isSelected = selectedDataKey === null || selectedDataKey === dataKey\n const dimmed = isClickable && !isSelected\n // The fill recedes twice as far as the stroke. A dimmed outline still traces\n // its shape, which is what a radar is read by; a dimmed fill just muddies the\n // one that was picked.\n const opacity = { stroke: dimmed ? 0.2 : 1, fill: dimmed ? 0.1 : 1, dot: dimmed ? 0.2 : 1 }\n const isFilled = variant === 'filled'\n\n const { dot, activeDot } = resolveDots(children, id, dataKey, opacity.dot)\n // `--chart-fill` is a number, not a length, so it can be read into a\n // calc() the SVG attribute accepts — which is how the default follows the\n // ground without the component knowing which ground it is on. Dimming has\n // to multiply inside the calc() rather than scale the result: the value is\n // a string, and multiplying it in JS hands the attribute NaN.\n const dimFill = dimmed ? ` * ${opacity.fill}` : ''\n const filledStrength = `calc(var(--chart-fill) * ${FILL_STRENGTH}${dimFill})` as unknown as number\n const filledOpacity = fillOpacity === undefined ? filledStrength : fillOpacity * opacity.fill\n const stops = colorStops(config[dataKey])\n\n return (\n <>\n <RechartsRadar\n dataKey={dataKey}\n stroke={`url(#${id}-stroke-${dataKey})`}\n strokeOpacity={opacity.stroke}\n strokeWidth={STROKE_WIDTH}\n fill={isFilled ? `url(#${id}-fill-${dataKey})` : 'none'}\n fillOpacity={isFilled ? filledOpacity : 0}\n dot={dot}\n activeDot={activeDot}\n filter={glowing ? `url(#${id}-glow-${dataKey})` : undefined}\n className=\"transition-opacity duration-(--duration-base)\"\n style={isClickable ? { cursor: 'pointer' } : undefined}\n onClick={() => {\n if (!isClickable) return\n selectDataKey(selectedDataKey === dataKey ? null : dataKey)\n }}\n {...radarProps}\n />\n <defs>\n {/* The dots read this one; the stroke and fill get their own below. */}\n <SeriesGradient id={id} dataKey={dataKey} config={config} />\n <linearGradient id={`${id}-stroke-${dataKey}`} x1=\"0\" y1=\"0\" x2=\"1\" y2=\"1\">\n <ColorStops dataKey={dataKey} stops={stops} />\n </linearGradient>\n {isFilled && (\n // Radial, and fading outward: a flat fill makes the outer ring — the\n // part carrying the shape — the least readable part of the mark.\n <radialGradient id={`${id}-fill-${dataKey}`} cx=\"50%\" cy=\"50%\" r=\"50%\">\n <ColorStops\n dataKey={dataKey}\n stops={stops}\n opacities={\n stops === 1 ? [0.8, 0.3] : Array.from({ length: stops }, (_, i) => (i === 0 ? 0.8 : 0.3))\n }\n />\n </radialGradient>\n )}\n {glowing && <GlowFilter id={id} dataKey={dataKey} spread={4} intensity={0.6} />}\n </defs>\n </>\n )\n}\n\nexport interface DotProps {\n variant?: ChartDotVariant\n}\n\n/** The resting point marker for the radar it is composed inside. A slot. */\nconst Dot: FC<DotProps> = () => null\n\n/** The marker under the pointer. */\nconst ActiveDot: FC<DotProps> = () => null\n\nfunction resolveDots(children: ReactNode, id: string, dataKey: string, dotOpacity: number) {\n let dot: ComponentProps<typeof RechartsRadar>['dot'] = false\n let activeDot: ComponentProps<typeof RechartsRadar>['activeDot'] = false\n\n Children.forEach(children, (child) => {\n if (!isValidElement(child)) return\n const { variant } = (child as ReactElement<DotProps>).props\n\n if (child.type === Dot) {\n dot = <ChartDot variant={variant} dataKey={dataKey} chartId={id} fillOpacity={dotOpacity} />\n }\n if (child.type === ActiveDot) {\n activeDot = (\n <ChartDot variant={variant} dataKey={dataKey} chartId={id} fillOpacity={dotOpacity} />\n )\n }\n })\n\n return { dot, activeDot }\n}\n\n/**\n * The web behind the marks. `gridType=\"circle\"` swaps the polygon for rings,\n * which reads as a scale rather than as a shape and suits a single series.\n */\nfunction PolarGrid({\n gridType = 'polygon',\n stroke = 'var(--chart-grid)',\n strokeDasharray = '3 4',\n ...rest\n}: ComponentProps<typeof RechartsPolarGrid>) {\n return (\n <RechartsPolarGrid\n gridType={gridType}\n stroke={stroke}\n strokeDasharray={strokeDasharray}\n {...rest}\n />\n )\n}\n\n/** The spoke labels around the perimeter. */\nfunction PolarAngleAxis({\n tick = { fill: 'var(--chart-axis)', fontSize: 12 },\n tickLine = false,\n ...rest\n}: ComponentProps<typeof RechartsPolarAngleAxis>) {\n const { isLoading } = useRadarChart()\n if (isLoading) return null\n return <RechartsPolarAngleAxis tick={tick} tickLine={tickLine} {...rest} />\n}\n\n/** The scale running out from the centre. */\nfunction PolarRadiusAxis({\n tick = { fill: 'var(--chart-axis)', fontSize: 10 },\n tickLine = false,\n axisLine = false,\n ...rest\n}: ComponentProps<typeof RechartsPolarRadiusAxis>) {\n const { isLoading } = useRadarChart()\n if (isLoading) return null\n return (\n <RechartsPolarRadiusAxis tick={tick} tickLine={tickLine} axisLine={axisLine} {...rest} />\n )\n}\n\n/** The hover panel. No crosshair: there is no straight axis to run one along. */\nfunction Tooltip({ variant, roundness, defaultIndex }: ChartTooltipSlotProps) {\n const { isLoading, selectedDataKey } = useRadarChart()\n if (isLoading) return null\n\n return (\n <ChartTooltip\n defaultIndex={defaultIndex}\n cursor={false}\n content={\n <ChartTooltipContent selected={selectedDataKey} roundness={roundness} variant={variant} />\n }\n />\n )\n}\n\n/** The key. Under the web and centred, since the spokes name dimensions, not series. */\nfunction Legend({\n variant,\n align = 'center',\n verticalAlign = 'bottom',\n isClickable = false,\n}: ChartLegendSlotProps) {\n const { isLoading, selectedDataKey, selectDataKey } = useRadarChart()\n if (isLoading) return null\n\n return (\n <ChartLegend\n verticalAlign={verticalAlign}\n align={align}\n content={\n <ChartLegendContent\n selected={selectedDataKey}\n onSelectChange={selectDataKey}\n isClickable={isClickable}\n variant={variant}\n />\n }\n />\n )\n}\n\n/** The decorative plate behind the web. */\nfunction Background({ variant = 'dots' }: { variant?: ChartBackgroundVariant }) {\n const { isLoading } = useRadarChart()\n if (isLoading) return null\n return <ChartBackground variant={variant} />\n}\n\n/** A deterministic spoke value, so the server and the client agree. */\nfunction shape(index: number, cycle: number): number {\n const x = Math.sin(index * 91.7 + cycle * 47.3) * 21_942.7\n return 30 + (x - Math.floor(x)) * 70\n}\n\n/**\n * The skeleton's spokes, re-rolled once per cycle.\n *\n * An interval rather than the shimmer's exit callback: a radar's skeleton\n * animates by MORPHING between shapes, which is Recharts' own transition, so\n * there is no travelling highlight to synchronise against.\n */\nfunction useLoadingShape(isLoading: boolean, points = LOADING_POINTS) {\n const [cycle, setCycle] = useState(0)\n\n useEffect(() => {\n if (!isLoading) return\n const timer = setInterval(() => setCycle((previous) => previous + 1), LOADING_CYCLE)\n return () => clearInterval(timer)\n }, [isLoading])\n\n return useMemo(\n () =>\n Array.from({ length: points }, (_, index) => ({\n spoke: String.fromCharCode(65 + index),\n value: shape(index, cycle),\n })),\n [cycle, points],\n )\n}\n\n/** The skeleton radar, morphing between shapes while the data is in flight. */\nfunction LoadingRadar() {\n const reduceMotion = useReducedMotion()\n\n return (\n <RechartsRadar\n dataKey=\"value\"\n stroke=\"currentColor\"\n strokeOpacity={0.3}\n strokeWidth={2}\n fill=\"currentColor\"\n fillOpacity={0.1}\n dot={false}\n className=\"text-(--ink)\"\n isAnimationActive={!reduceMotion}\n animationDuration={LOADING_CYCLE}\n animationEasing=\"ease-in-out\"\n />\n )\n}\n\nRadarChart.Radar = Radar\nRadarChart.Dot = Dot\nRadarChart.ActiveDot = ActiveDot\nRadarChart.PolarGrid = PolarGrid\nRadarChart.PolarAngleAxis = PolarAngleAxis\nRadarChart.PolarRadiusAxis = PolarRadiusAxis\nRadarChart.Tooltip = Tooltip\nRadarChart.Legend = Legend\nRadarChart.Background = Background\n\nexport default RadarChart\n"],"mappings":"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","names":[]}
@@ -0,0 +1,67 @@
1
+ import * as react from 'react';
2
+
3
+ interface ColorPickerProps {
4
+ /**
5
+ * Names the control. Required — the trigger shows a colour, and a colour is
6
+ * not a name.
7
+ *
8
+ * Announced together with the value, the way `Select`'s is: a reader hears
9
+ * "Brand colour, #a78bfa". Inside a `Field` with a label, that label is used
10
+ * and this one is not repeated.
11
+ */
12
+ label: string;
13
+ /** The colour, as any absolute CSS colour string. */
14
+ value?: string;
15
+ defaultValue?: string;
16
+ /**
17
+ * Fires with a CSS colour string in whichever notation the panel is set to —
18
+ * so the notation a caller passes in is the notation they get back until
19
+ * somebody changes it in the panel.
20
+ */
21
+ onValueChange?: (value: string) => void;
22
+ disabled?: boolean;
23
+ /** Paints the resting border with `--danger` and reflects `aria-invalid`. */
24
+ invalid?: boolean;
25
+ className?: string;
26
+ /** The TRIGGER's id — the element a label points at. A `Field` sets it. */
27
+ id?: string;
28
+ /** Ids of the copy describing the control. A `Field` sets it from hint, error and description. */
29
+ 'aria-describedby'?: string;
30
+ /** The spelling a form library sets; read together with `invalid`. */
31
+ 'aria-invalid'?: boolean | 'true' | 'false';
32
+ /** Announced on the trigger. A `Field` sets it from `required`. */
33
+ 'aria-required'?: boolean;
34
+ }
35
+ /**
36
+ * A colour, chosen or typed.
37
+ *
38
+ * The panel works in OKLCH, and that is the reason to reach for this rather
39
+ * than `<input type="color">`. In HSV — which is what the native picker and
40
+ * most libraries use — dragging along a row of constant "lightness" walks
41
+ * through colours the eye reads as getting darker, so a reader tuning a palette
42
+ * is fighting the instrument. OKLCH's lightness is the one a person sees, so
43
+ * two colours at the same height on the plane genuinely match, and the hue
44
+ * strip stays at the lightness already chosen instead of showing a rainbow that
45
+ * belongs to some other colour.
46
+ *
47
+ * The plane is normalised to the gamut ROW BY ROW: its right edge is the most
48
+ * chroma that exists at that lightness and hue, so the whole surface is
49
+ * reachable rather than a lens of colour inside a field of clipped duplicates.
50
+ *
51
+ * **What it accepts.** Hex, `rgb()`, `hsl()`, `oklch()` and
52
+ * `color(display-p3 …)`, in both syntaxes. Named colours are not accepted:
53
+ * resolving them needs a table of every CSS name or a live DOM, and a box that
54
+ * takes "rebeccapurple" but not "papayawhip" is worse than one that takes
55
+ * neither.
56
+ *
57
+ * **Keyboard.** The plane is a group of two real sliders — chroma across,
58
+ * lightness up — so the arrows move it and a screen reader announces where it
59
+ * is. That is the part a 2D canvas usually leaves out, and leaving it out makes
60
+ * the control unusable rather than merely awkward.
61
+ *
62
+ * @example
63
+ * <Field label="Brand colour"><ColorPicker label="Brand colour" defaultValue="#a78bfa" /></Field>
64
+ */
65
+ declare function ColorPicker({ label, value: controlled, defaultValue, onValueChange, disabled, invalid, className, id, 'aria-describedby': describedBy, 'aria-invalid': ariaInvalid, 'aria-required': ariaRequired, }: ColorPickerProps): react.JSX.Element;
66
+
67
+ export { ColorPicker, type ColorPickerProps, ColorPicker as default };
@@ -0,0 +1,334 @@
1
+ "use client";
2
+ import { jsx, jsxs } from "react/jsx-runtime";
3
+ import { useEffect, useId, useRef, useState } from "react";
4
+ import { cn } from "../../lib/cn.js";
5
+ import {
6
+ clamp,
7
+ colorFormat,
8
+ colorToRgb,
9
+ fitGamut,
10
+ formatColor,
11
+ maxChroma,
12
+ parseColor
13
+ } from "../../lib/color.js";
14
+ import { CONTROL_BASE, CONTROL_BORDER, isInvalid } from "../../lib/control.js";
15
+ import { warnBlankName } from "../../lib/warn.js";
16
+ import { useFieldControl } from "../Field/field-control.js";
17
+ import { Input } from "../Input/Input.js";
18
+ import { Popover, PopoverContent, PopoverTrigger } from "../Popover/Popover.js";
19
+ import { ToggleGroup, ToggleGroupItem } from "../ToggleGroup/ToggleGroup.js";
20
+ const PLANE_W = 256;
21
+ const PLANE_H = 160;
22
+ const HUE_STOPS = 73;
23
+ const BLACK = { l: 0, c: 0, h: 0, a: 1 };
24
+ const FORMAT_LABELS = {
25
+ hex: "Hex",
26
+ oklch: "OKLCH",
27
+ // Not "Display P3": the three segments share the strip evenly, and the long
28
+ // name wraps to two lines inside its third and makes the whole row twice as
29
+ // tall as the two beside it.
30
+ p3: "P3"
31
+ };
32
+ const checkerboard = (square) => `repeating-conic-gradient(var(--stone) 0% 25%, var(--paper) 0% 50%) 0 0 / ${square} ${square}`;
33
+ function ColorPicker({
34
+ label,
35
+ value: controlled,
36
+ defaultValue = "#000000",
37
+ onValueChange,
38
+ disabled,
39
+ invalid,
40
+ className,
41
+ id,
42
+ "aria-describedby": describedBy,
43
+ "aria-invalid": ariaInvalid,
44
+ "aria-required": ariaRequired
45
+ }) {
46
+ warnBlankName("ColorPicker", "label", label, "the trigger is announced with no name");
47
+ const bad = isInvalid(invalid, ariaInvalid);
48
+ const field = useFieldControl();
49
+ const generated = useId();
50
+ const triggerId = id ?? generated;
51
+ const valueId = `${triggerId}-value`;
52
+ const nameId = field?.labelId ?? `${triggerId}-name`;
53
+ const [uncontrolled, setUncontrolled] = useState(defaultValue);
54
+ const value = controlled ?? uncontrolled;
55
+ const [color, setColor] = useState(() => parseColor(defaultValue) ?? BLACK);
56
+ const [format, setFormat] = useState(() => colorFormat(defaultValue));
57
+ const emitted = useRef(null);
58
+ const saturationRef = useRef(0);
59
+ useEffect(() => {
60
+ if (value === emitted.current) return;
61
+ const parsed = parseColor(value);
62
+ if (!parsed) return;
63
+ setColor((prev) => ({ ...parsed, h: parsed.c < 1e-7 ? prev.h : parsed.h }));
64
+ setFormat(colorFormat(value));
65
+ }, [value]);
66
+ const space = format === "hex" ? "srgb" : "p3";
67
+ const ceiling = maxChroma(color.l, color.h, space);
68
+ const saturation = ceiling > 0 ? clamp(color.c / ceiling) : saturationRef.current;
69
+ useEffect(() => {
70
+ if (ceiling > 0) saturationRef.current = saturation;
71
+ }, [ceiling, saturation]);
72
+ const commit = (next, nextFormat = format) => {
73
+ const fitted = nextFormat === "oklch" ? next : fitGamut(next, nextFormat === "p3" ? "p3" : "srgb");
74
+ setColor(fitted);
75
+ setFormat(nextFormat);
76
+ const text = formatColor(fitted, nextFormat);
77
+ emitted.current = text;
78
+ if (controlled === void 0) setUncontrolled(text);
79
+ onValueChange?.(text);
80
+ };
81
+ const setPlane = (nextSaturation, lightness) => {
82
+ saturationRef.current = nextSaturation;
83
+ commit({ ...color, l: lightness, c: nextSaturation * maxChroma(lightness, color.h, space) });
84
+ };
85
+ const planeRef = useRef(null);
86
+ const canvasRef = useRef(null);
87
+ const painted = useRef("");
88
+ const trackPointer = (event) => {
89
+ const rect = planeRef.current?.getBoundingClientRect();
90
+ if (!rect || rect.width === 0 || rect.height === 0) return;
91
+ setPlane(
92
+ clamp((event.clientX - rect.left) / rect.width),
93
+ clamp(1 - (event.clientY - rect.top) / rect.height)
94
+ );
95
+ };
96
+ const paint = (canvas) => {
97
+ if (!canvas) return;
98
+ const key = `${color.h.toFixed(3)}:${space}`;
99
+ if (painted.current === key) return;
100
+ const context = canvas.getContext("2d");
101
+ if (!context || typeof context.createImageData !== "function") return;
102
+ painted.current = key;
103
+ const pixels = context.createImageData(PLANE_W, PLANE_H);
104
+ for (let y = 0; y < PLANE_H; y++) {
105
+ const l = 1 - y / (PLANE_H - 1);
106
+ const rowMax = maxChroma(l, color.h, space);
107
+ for (let x = 0; x < PLANE_W; x++) {
108
+ const rgb = colorToRgb({ l, c: x / (PLANE_W - 1) * rowMax, h: color.h, a: 1 }, "srgb");
109
+ const at = (y * PLANE_W + x) * 4;
110
+ for (let channel = 0; channel < 3; channel++) {
111
+ pixels.data[at + channel] = Math.round(clamp(rgb[channel] ?? 0) * 255);
112
+ }
113
+ pixels.data[at + 3] = 255;
114
+ }
115
+ }
116
+ context.putImageData(pixels, 0, 0);
117
+ };
118
+ useEffect(() => {
119
+ paint(canvasRef.current);
120
+ });
121
+ const opaque = formatColor({ ...color, a: 1 }, "oklch");
122
+ const hueTrack = Array.from({ length: HUE_STOPS }, (_, i) => {
123
+ const h = i * (360 / (HUE_STOPS - 1));
124
+ return formatColor({ l: color.l, c: saturation * maxChroma(color.l, h, space), h, a: 1 }, "oklch");
125
+ }).join(", ");
126
+ const typed = parseColor(value);
127
+ return /* @__PURE__ */ jsxs(Popover, { children: [
128
+ /* @__PURE__ */ jsx(PopoverTrigger, { asChild: true, children: /* @__PURE__ */ jsxs(
129
+ "button",
130
+ {
131
+ type: "button",
132
+ id: triggerId,
133
+ disabled,
134
+ "aria-labelledby": `${nameId} ${valueId}`,
135
+ "aria-describedby": describedBy,
136
+ "aria-required": ariaRequired,
137
+ "aria-invalid": bad || void 0,
138
+ className: cn(
139
+ CONTROL_BASE,
140
+ bad ? CONTROL_BORDER.invalid : CONTROL_BORDER.resting,
141
+ "flex cursor-pointer items-center gap-2.5 text-start",
142
+ className
143
+ ),
144
+ children: [
145
+ field?.labelId == null && /* @__PURE__ */ jsx("span", { id: nameId, className: "sr-only", children: label }),
146
+ /* @__PURE__ */ jsx(
147
+ "span",
148
+ {
149
+ "aria-hidden": "true",
150
+ className: "size-4 shrink-0 rounded-(--radius-xs) border border-(--rule-2)",
151
+ style: { background: `linear-gradient(${value}, ${value}), ${checkerboard("4px")}` }
152
+ }
153
+ ),
154
+ /* @__PURE__ */ jsx("span", { id: valueId, className: "mono-meta truncate", children: value })
155
+ ]
156
+ }
157
+ ) }),
158
+ /* @__PURE__ */ jsxs(PopoverContent, { label: `${label} picker`, className: "flex w-72 flex-col gap-3", children: [
159
+ /* @__PURE__ */ jsx(
160
+ ToggleGroup,
161
+ {
162
+ type: "single",
163
+ value: format,
164
+ onValueChange: (next) => next && commit(color, next),
165
+ "aria-label": "Colour notation",
166
+ className: "w-full",
167
+ children: Object.keys(FORMAT_LABELS).map((option) => /* @__PURE__ */ jsx(ToggleGroupItem, { value: option, className: "flex-1", children: FORMAT_LABELS[option] }, option))
168
+ }
169
+ ),
170
+ /* @__PURE__ */ jsxs(
171
+ "div",
172
+ {
173
+ ref: planeRef,
174
+ role: "group",
175
+ "aria-label": `${label} field`,
176
+ onPointerDown: (event) => {
177
+ if (event.button !== 0) return;
178
+ event.preventDefault();
179
+ event.currentTarget.setPointerCapture(event.pointerId);
180
+ trackPointer(event);
181
+ },
182
+ onPointerMove: (event) => {
183
+ if (event.currentTarget.hasPointerCapture(event.pointerId)) trackPointer(event);
184
+ },
185
+ onPointerUp: (event) => {
186
+ if (event.currentTarget.hasPointerCapture(event.pointerId)) {
187
+ event.currentTarget.releasePointerCapture(event.pointerId);
188
+ }
189
+ },
190
+ className: "relative h-40 w-full cursor-crosshair touch-none select-none rounded-(--radius) focus-within:[outline:var(--focus-w)_solid_var(--focus)] focus-within:[outline-offset:var(--focus-off)]",
191
+ children: [
192
+ /* @__PURE__ */ jsx(
193
+ "canvas",
194
+ {
195
+ ref: (node) => {
196
+ canvasRef.current = node;
197
+ if (!node) painted.current = "";
198
+ paint(node);
199
+ },
200
+ width: PLANE_W,
201
+ height: PLANE_H,
202
+ "aria-hidden": "true",
203
+ className: "block size-full rounded-(--radius)"
204
+ }
205
+ ),
206
+ /* @__PURE__ */ jsx(
207
+ "span",
208
+ {
209
+ "aria-hidden": "true",
210
+ className: "pointer-events-none absolute inset-0 rounded-(--radius) shadow-[inset_0_0_0_1px_var(--rule)]"
211
+ }
212
+ ),
213
+ /* @__PURE__ */ jsx(
214
+ "span",
215
+ {
216
+ "aria-hidden": "true",
217
+ className: "pointer-events-none absolute -m-1.5 size-3 rounded-(--radius-pill) border-2 border-(--paper) shadow-[0_0_0_1px_var(--rule-hard)]",
218
+ style: { left: `${saturation * 100}%`, top: `${(1 - color.l) * 100}%`, background: opaque }
219
+ }
220
+ ),
221
+ /* @__PURE__ */ jsx(
222
+ "input",
223
+ {
224
+ type: "range",
225
+ min: 0,
226
+ max: 100,
227
+ step: 1,
228
+ value: Math.round(saturation * 100),
229
+ "aria-label": "Chroma",
230
+ "aria-valuetext": `${Math.round(saturation * 100)} percent chroma`,
231
+ onChange: (event) => setPlane(Number(event.target.value) / 100, color.l),
232
+ className: "sr-only"
233
+ }
234
+ ),
235
+ /* @__PURE__ */ jsx(
236
+ "input",
237
+ {
238
+ type: "range",
239
+ min: 0,
240
+ max: 100,
241
+ step: 1,
242
+ value: Math.round(color.l * 100),
243
+ "aria-label": "Lightness",
244
+ "aria-valuetext": `${Math.round(color.l * 100)} percent lightness`,
245
+ onChange: (event) => setPlane(saturation, Number(event.target.value) / 100),
246
+ className: "sr-only"
247
+ }
248
+ )
249
+ ]
250
+ }
251
+ ),
252
+ /* @__PURE__ */ jsx(
253
+ ColorTrack,
254
+ {
255
+ name: "Hue",
256
+ max: 360,
257
+ step: 1,
258
+ value: color.h,
259
+ announce: `${Math.round(color.h)} degrees`,
260
+ thumb: opaque,
261
+ track: `linear-gradient(to right in oklab, ${hueTrack})`,
262
+ onChange: (h) => commit({ ...color, h, c: saturation * maxChroma(color.l, h, space) })
263
+ }
264
+ ),
265
+ /* @__PURE__ */ jsx(
266
+ ColorTrack,
267
+ {
268
+ name: "Opacity",
269
+ max: 100,
270
+ step: 1,
271
+ value: color.a * 100,
272
+ announce: `${Math.round(color.a * 100)} percent`,
273
+ thumb: formatColor(color, "oklch"),
274
+ track: `linear-gradient(to right, transparent, ${opaque}), ${checkerboard("8px")}`,
275
+ onChange: (percent) => commit({ ...color, a: percent / 100 })
276
+ }
277
+ ),
278
+ /* @__PURE__ */ jsx(
279
+ Input,
280
+ {
281
+ value,
282
+ spellCheck: false,
283
+ autoComplete: "off",
284
+ "aria-label": `${label} as CSS`,
285
+ invalid: typed === null,
286
+ onChange: (event) => {
287
+ const next = event.target.value;
288
+ emitted.current = next;
289
+ if (controlled === void 0) setUncontrolled(next);
290
+ const parsed = parseColor(next);
291
+ if (parsed) {
292
+ setColor(parsed);
293
+ setFormat(colorFormat(next));
294
+ }
295
+ onValueChange?.(next);
296
+ },
297
+ className: "mono-meta"
298
+ }
299
+ )
300
+ ] })
301
+ ] });
302
+ }
303
+ function ColorTrack({ name, max, step, value, announce, track, thumb, onChange }) {
304
+ return /* @__PURE__ */ jsxs("label", { className: "flex items-center gap-3 text-sm text-(--ink-2)", children: [
305
+ /* @__PURE__ */ jsx("span", { className: "w-14 shrink-0", children: name }),
306
+ /* @__PURE__ */ jsx(
307
+ "input",
308
+ {
309
+ type: "range",
310
+ min: 0,
311
+ max,
312
+ step,
313
+ value,
314
+ "aria-valuetext": announce,
315
+ onChange: (event) => onChange(Number(event.target.value)),
316
+ style: { background: track },
317
+ className: cn(
318
+ "h-4 w-full cursor-pointer appearance-none rounded-(--radius-xs) shadow-[inset_0_0_0_1px_var(--rule)]",
319
+ "[&::-webkit-slider-thumb]:h-6 [&::-webkit-slider-thumb]:w-4 [&::-webkit-slider-thumb]:appearance-none [&::-webkit-slider-thumb]:rounded-(--radius-xs) [&::-webkit-slider-thumb]:border-2 [&::-webkit-slider-thumb]:border-(--paper) [&::-webkit-slider-thumb]:bg-(--thumb) [&::-webkit-slider-thumb]:bg-clip-padding [&::-webkit-slider-thumb]:shadow-[0_0_0_1px_var(--rule-hard)]",
320
+ "[&::-moz-range-thumb]:h-6 [&::-moz-range-thumb]:w-4 [&::-moz-range-thumb]:rounded-(--radius-xs) [&::-moz-range-thumb]:border-2 [&::-moz-range-thumb]:border-(--paper) [&::-moz-range-thumb]:bg-(--thumb) [&::-moz-range-thumb]:bg-clip-padding [&::-moz-range-thumb]:shadow-[0_0_0_1px_var(--rule-hard)]"
321
+ ),
322
+ ref: (node) => {
323
+ node?.style.setProperty("--thumb", thumb);
324
+ }
325
+ }
326
+ )
327
+ ] });
328
+ }
329
+ var ColorPicker_default = ColorPicker;
330
+ export {
331
+ ColorPicker,
332
+ ColorPicker_default as default
333
+ };
334
+ //# sourceMappingURL=ColorPicker.js.map