@cosmicdrift/kumiko-renderer 0.389.0 → 0.390.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 (230) hide show
  1. package/dist/app/projection-detail-body.js +59 -394
  2. package/dist/app/projection-detail-header-model.d.ts +11 -0
  3. package/dist/app/projection-detail-header-model.js +33 -0
  4. package/dist/app/projection-detail-header-region.d.ts +28 -0
  5. package/dist/app/projection-detail-header-region.js +81 -0
  6. package/dist/app/projection-detail-header.d.ts +29 -0
  7. package/dist/app/projection-detail-header.js +84 -0
  8. package/dist/app/projection-detail-navigation.d.ts +13 -0
  9. package/dist/app/projection-detail-navigation.js +91 -0
  10. package/dist/app/projection-detail-states.d.ts +22 -0
  11. package/dist/app/projection-detail-states.js +26 -0
  12. package/dist/app/projection-drawer-host.d.ts +12 -0
  13. package/dist/app/projection-drawer-host.js +10 -0
  14. package/dist/app/projection-list-body.d.ts +1 -1
  15. package/dist/app/projection-list-body.js +69 -189
  16. package/dist/app/projection-list-render-props.d.ts +56 -0
  17. package/dist/app/projection-list-render-props.js +47 -0
  18. package/dist/app/projection-list-render.d.ts +26 -0
  19. package/dist/app/projection-list-render.js +20 -0
  20. package/dist/app/projection-list-row-click.d.ts +12 -0
  21. package/dist/app/projection-list-row-click.js +14 -0
  22. package/dist/app/use-projection-detail-actions.d.ts +35 -0
  23. package/dist/app/use-projection-detail-actions.js +156 -0
  24. package/dist/app/use-projection-detail-data.d.ts +19 -0
  25. package/dist/app/use-projection-detail-data.js +47 -0
  26. package/dist/app/use-projection-detail-layout.d.ts +17 -0
  27. package/dist/app/use-projection-detail-layout.js +23 -0
  28. package/dist/app/use-projection-list-actions.d.ts +31 -0
  29. package/dist/app/use-projection-list-actions.js +73 -0
  30. package/dist/app/use-projection-list-data.d.ts +20 -0
  31. package/dist/app/use-projection-list-data.js +26 -0
  32. package/dist/app/use-projection-list-definitions.d.ts +11 -0
  33. package/dist/app/use-projection-list-definitions.js +20 -0
  34. package/dist/app/use-projection-list-query.d.ts +28 -0
  35. package/dist/app/use-projection-list-query.js +83 -0
  36. package/dist/components/render-edit-banners.d.ts +12 -0
  37. package/dist/components/render-edit-banners.js +90 -0
  38. package/dist/components/render-edit-draft-save-helpers.d.ts +19 -0
  39. package/dist/components/render-edit-draft-save-helpers.js +42 -0
  40. package/dist/components/render-edit-field-grid.d.ts +4 -0
  41. package/dist/components/render-edit-field-grid.js +40 -0
  42. package/dist/components/render-edit-fields-section.d.ts +4 -0
  43. package/dist/components/render-edit-fields-section.js +76 -0
  44. package/dist/components/render-edit-form-actions.d.ts +4 -0
  45. package/dist/components/render-edit-form-actions.js +66 -0
  46. package/dist/components/render-edit-header-actions.d.ts +11 -0
  47. package/dist/components/render-edit-header-actions.js +120 -0
  48. package/dist/components/render-edit-layout-flags.d.ts +29 -0
  49. package/dist/components/render-edit-layout-flags.js +106 -0
  50. package/dist/components/render-edit-layout.d.ts +4 -0
  51. package/dist/components/render-edit-layout.js +79 -0
  52. package/dist/components/render-edit-render-model.d.ts +63 -0
  53. package/dist/components/render-edit-render-model.js +14 -0
  54. package/dist/components/render-edit-section-kinds.d.ts +4 -0
  55. package/dist/components/render-edit-section-kinds.js +115 -0
  56. package/dist/components/render-edit-submit-result.d.ts +8 -0
  57. package/dist/components/render-edit-submit-result.js +77 -0
  58. package/dist/components/render-edit-submit.d.ts +7 -0
  59. package/dist/components/render-edit-submit.js +113 -0
  60. package/dist/components/render-edit-titles.d.ts +17 -0
  61. package/dist/components/render-edit-titles.js +99 -0
  62. package/dist/components/render-edit-wizard-navigation.d.ts +11 -0
  63. package/dist/components/render-edit-wizard-navigation.js +115 -0
  64. package/dist/components/render-edit.d.ts +2 -2
  65. package/dist/components/render-edit.js +63 -1128
  66. package/dist/components/render-field-choice-inputs.d.ts +7 -0
  67. package/dist/components/render-field-choice-inputs.js +52 -0
  68. package/dist/components/render-field-control.d.ts +12 -0
  69. package/dist/components/render-field-control.js +93 -0
  70. package/dist/components/render-field-display-text.d.ts +4 -0
  71. package/dist/components/render-field-display-text.js +48 -0
  72. package/dist/components/render-field-fill-action.d.ts +13 -0
  73. package/dist/components/render-field-fill-action.js +63 -0
  74. package/dist/components/render-field-input-shared.d.ts +39 -0
  75. package/dist/components/render-field-input-shared.js +20 -0
  76. package/dist/components/render-field-input.d.ts +3 -0
  77. package/dist/components/render-field-input.js +34 -0
  78. package/dist/components/render-field-meta.d.ts +6 -0
  79. package/dist/components/render-field-meta.js +51 -0
  80. package/dist/components/render-field-read-only.d.ts +11 -0
  81. package/dist/components/render-field-read-only.js +75 -0
  82. package/dist/components/render-field-reference.d.ts +15 -0
  83. package/dist/components/render-field-reference.js +103 -0
  84. package/dist/components/render-field-scalar-inputs.d.ts +10 -0
  85. package/dist/components/render-field-scalar-inputs.js +35 -0
  86. package/dist/components/render-field-text-inputs.d.ts +3 -0
  87. package/dist/components/render-field-text-inputs.js +34 -0
  88. package/dist/components/render-field-values.d.ts +18 -0
  89. package/dist/components/render-field-values.js +111 -0
  90. package/dist/components/render-field-write-only.d.ts +9 -0
  91. package/dist/components/render-field-write-only.js +29 -0
  92. package/dist/components/render-field.d.ts +2 -2
  93. package/dist/components/render-field.js +39 -670
  94. package/dist/components/render-list-header-slot.d.ts +5 -0
  95. package/dist/components/render-list-header-slot.js +30 -0
  96. package/dist/components/render-list-reference-bridge.d.ts +3 -0
  97. package/dist/components/render-list-reference-bridge.js +25 -0
  98. package/dist/components/render-list-reference-columns.d.ts +25 -0
  99. package/dist/components/render-list-reference-columns.js +137 -0
  100. package/dist/components/render-list-table-props.d.ts +52 -0
  101. package/dist/components/render-list-table-props.js +53 -0
  102. package/dist/components/render-list-toolbar-model.d.ts +42 -0
  103. package/dist/components/render-list-toolbar-model.js +82 -0
  104. package/dist/components/render-list-toolbar.d.ts +38 -0
  105. package/dist/components/render-list-toolbar.js +40 -0
  106. package/dist/components/render-list-types.d.ts +113 -0
  107. package/dist/components/render-list-types.js +1 -0
  108. package/dist/components/render-list.d.ts +4 -113
  109. package/dist/components/render-list.js +91 -307
  110. package/dist/components/use-reference-input-state.d.ts +20 -0
  111. package/dist/components/use-reference-input-state.js +85 -0
  112. package/dist/components/use-render-edit-controlled-sync.d.ts +3 -0
  113. package/dist/components/use-render-edit-controlled-sync.js +26 -0
  114. package/dist/components/use-render-edit-draft-save.d.ts +29 -0
  115. package/dist/components/use-render-edit-draft-save.js +141 -0
  116. package/dist/components/use-render-edit-form.d.ts +10 -0
  117. package/dist/components/use-render-edit-form.js +101 -0
  118. package/dist/components/use-render-edit-sections.d.ts +15 -0
  119. package/dist/components/use-render-edit-sections.js +100 -0
  120. package/dist/components/use-render-edit-state.d.ts +65 -0
  121. package/dist/components/use-render-edit-state.js +158 -0
  122. package/dist/components/use-render-edit-steps.d.ts +13 -0
  123. package/dist/components/use-render-edit-steps.js +52 -0
  124. package/dist/components/use-render-list-columns.d.ts +18 -0
  125. package/dist/components/use-render-list-columns.js +41 -0
  126. package/dist/components/use-render-list-search.d.ts +4 -0
  127. package/dist/components/use-render-list-search.js +50 -0
  128. package/dist/primitives.d.ts +1 -1
  129. package/dist/web/app/dashboard-body.d.ts +1 -1
  130. package/dist/web/app/dashboard-body.js +42 -246
  131. package/dist/web/app/dashboard-header.d.ts +34 -0
  132. package/dist/web/app/dashboard-header.js +58 -0
  133. package/dist/web/app/dashboard-screen-controls.d.ts +11 -0
  134. package/dist/web/app/dashboard-screen-controls.js +42 -0
  135. package/dist/web/app/dashboard-screen-tiles.d.ts +8 -0
  136. package/dist/web/app/dashboard-screen-tiles.js +42 -0
  137. package/dist/web/app/dashboard-visibility.d.ts +42 -0
  138. package/dist/web/app/dashboard-visibility.js +102 -0
  139. package/dist/web/app/use-dashboard-screen-state.d.ts +35 -0
  140. package/dist/web/app/use-dashboard-screen-state.js +69 -0
  141. package/dist/web/primitives/data-table/actions-column.js +6 -5
  142. package/dist/web/primitives/data-table/card-body.d.ts +6 -0
  143. package/dist/web/primitives/data-table/card-body.js +32 -0
  144. package/dist/web/primitives/data-table/card-row.d.ts +5 -0
  145. package/dist/web/primitives/data-table/card-row.js +31 -0
  146. package/dist/web/primitives/data-table/card-view.d.ts +3 -0
  147. package/dist/web/primitives/data-table/card-view.js +13 -0
  148. package/dist/web/primitives/data-table/column-resize-handle.js +3 -1
  149. package/dist/web/primitives/data-table/data-cell-props.d.ts +7 -0
  150. package/dist/web/primitives/data-table/data-cell-props.js +8 -0
  151. package/dist/web/primitives/data-table/row-render-parts.d.ts +8 -0
  152. package/dist/web/primitives/data-table/row-render-parts.js +35 -0
  153. package/dist/web/primitives/data-table/table-body.d.ts +3 -0
  154. package/dist/web/primitives/data-table/table-body.js +32 -0
  155. package/dist/web/primitives/data-table/table-content.d.ts +3 -0
  156. package/dist/web/primitives/data-table/table-content.js +41 -0
  157. package/dist/web/primitives/data-table/table-data-cell.d.ts +4 -0
  158. package/dist/web/primitives/data-table/table-data-cell.js +25 -0
  159. package/dist/web/primitives/data-table/table-header-row.d.ts +3 -0
  160. package/dist/web/primitives/data-table/table-header-row.js +21 -0
  161. package/dist/web/primitives/data-table/table-view.d.ts +3 -0
  162. package/dist/web/primitives/data-table/table-view.js +25 -0
  163. package/dist/web/primitives/data-table/toolbar.d.ts +6 -0
  164. package/dist/web/primitives/data-table/toolbar.js +53 -0
  165. package/dist/web/primitives/data-table/use-data-table-state.d.ts +44 -0
  166. package/dist/web/primitives/data-table/use-data-table-state.js +123 -0
  167. package/dist/web/primitives/data-table.d.ts +3 -4
  168. package/dist/web/primitives/data-table.js +32 -252
  169. package/dist/web/primitives/file-value-list.js +1 -1
  170. package/dist/web/primitives/form-footer-parts.d.ts +25 -0
  171. package/dist/web/primitives/form-footer-parts.js +55 -0
  172. package/dist/web/primitives/form.js +33 -43
  173. package/dist/web/primitives/input-choice-kinds.d.ts +4 -0
  174. package/dist/web/primitives/input-choice-kinds.js +67 -0
  175. package/dist/web/primitives/input-file-kinds.d.ts +3 -0
  176. package/dist/web/primitives/input-file-kinds.js +6 -0
  177. package/dist/web/primitives/input-number-kinds.d.ts +4 -0
  178. package/dist/web/primitives/input-number-kinds.js +16 -0
  179. package/dist/web/primitives/input-optional-props.d.ts +20 -0
  180. package/dist/web/primitives/input-optional-props.js +24 -0
  181. package/dist/web/primitives/input-select-presentation.d.ts +13 -0
  182. package/dist/web/primitives/input-select-presentation.js +19 -0
  183. package/dist/web/primitives/input-shared.d.ts +22 -0
  184. package/dist/web/primitives/input-shared.js +10 -0
  185. package/dist/web/primitives/input-temporal-kinds.d.ts +5 -0
  186. package/dist/web/primitives/input-temporal-kinds.js +10 -0
  187. package/dist/web/primitives/input-text-kinds.d.ts +7 -0
  188. package/dist/web/primitives/input-text-kinds.js +33 -0
  189. package/dist/web/primitives/input.js +35 -161
  190. package/dist/web/widgets/chart-data.d.ts +35 -0
  191. package/dist/web/widgets/chart-data.js +37 -0
  192. package/dist/web/widgets/chart-markers.d.ts +26 -0
  193. package/dist/web/widgets/chart-markers.js +64 -0
  194. package/dist/web/widgets/chart-parts.d.ts +20 -0
  195. package/dist/web/widgets/chart-parts.js +15 -0
  196. package/dist/web/widgets/charts.d.ts +5 -78
  197. package/dist/web/widgets/charts.js +7 -485
  198. package/dist/web/widgets/drawer-panel-class.d.ts +3 -0
  199. package/dist/web/widgets/drawer-panel-class.js +40 -0
  200. package/dist/web/widgets/drawer-resize-controls.d.ts +11 -0
  201. package/dist/web/widgets/drawer-resize-controls.js +12 -0
  202. package/dist/web/widgets/drawer-sizing.d.ts +19 -0
  203. package/dist/web/widgets/drawer-sizing.js +60 -0
  204. package/dist/web/widgets/drawer.js +17 -116
  205. package/dist/web/widgets/stacked-area-brush.d.ts +13 -0
  206. package/dist/web/widgets/stacked-area-brush.js +91 -0
  207. package/dist/web/widgets/stacked-area-chart.d.ts +6 -0
  208. package/dist/web/widgets/stacked-area-chart.js +45 -0
  209. package/dist/web/widgets/stacked-area-chrome.d.ts +7 -0
  210. package/dist/web/widgets/stacked-area-chrome.js +77 -0
  211. package/dist/web/widgets/stacked-area-geometry.d.ts +54 -0
  212. package/dist/web/widgets/stacked-area-geometry.js +83 -0
  213. package/dist/web/widgets/stacked-area-model.d.ts +11 -0
  214. package/dist/web/widgets/stacked-area-model.js +1 -0
  215. package/dist/web/widgets/stacked-area-plot.d.ts +3 -0
  216. package/dist/web/widgets/stacked-area-plot.js +51 -0
  217. package/dist/web/widgets/stacked-area-props.d.ts +83 -0
  218. package/dist/web/widgets/stacked-area-props.js +16 -0
  219. package/dist/web/widgets/stacked-area-svg.d.ts +3 -0
  220. package/dist/web/widgets/stacked-area-svg.js +38 -0
  221. package/dist/web/widgets/stacked-area-tooltip.d.ts +3 -0
  222. package/dist/web/widgets/stacked-area-tooltip.js +86 -0
  223. package/dist/web/widgets/use-drawer-resize.d.ts +22 -0
  224. package/dist/web/widgets/use-drawer-resize.js +52 -0
  225. package/dist/web/widgets/use-stacked-area-tooltip.d.ts +9 -0
  226. package/dist/web/widgets/use-stacked-area-tooltip.js +34 -0
  227. package/dist/web/widgets/use-stacked-area-window.d.ts +20 -0
  228. package/dist/web/widgets/use-stacked-area-window.js +76 -0
  229. package/package.json +10 -10
  230. package/src/changes.json +54 -0
@@ -1,9 +1,10 @@
1
1
  import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
- import { useEffect, useId, useMemo, useRef, useState, } from "react";
2
+ import { useId } from "react";
3
3
  import { cn } from "../lib/cn.js";
4
4
  import { NARROW_VIEWPORT_BREAKPOINT_PX, useIsViewportBelow, } from "../primitives/use-narrow-viewport.js";
5
- import { bucketIndexAtFraction, initialWindowSelection, nearestIndex, pointsWithin, resolveStackedAreaWindow, snapBrushWindow, snapMovedBrushWindow, stackedAreaAxisTicks, stackedAreaDateFormatFor, } from "./chart-window.js";
6
- import { ModeSwitch } from "./mode-switch.js";
5
+ import { bucketTimes, chartColor, clampDotY, percent, SINGLE_POINT_RADIUS, seriesTotal, valueAt, } from "./chart-data.js";
6
+ import { MarkerLegend, MarkerPins } from "./chart-markers.js";
7
+ import { ChartLegend, evenYTickValues, YTicks } from "./chart-parts.js";
7
8
  import { STATUS_TONE_TEXT } from "./status-badge.js";
8
9
  // Inline SVG charts without a chart dependency. Colors come exclusively from the
9
10
  // --color-status-* / --color-foreground theme tokens; axis labels are supplied
@@ -43,10 +44,6 @@ export function StatusBarChart({ entries, ariaLabel, startLabel, endLabel, highl
43
44
  return (_jsxs("g", { children: [!dense && (_jsx("defs", { children: _jsxs("linearGradient", { id: gradId, x1: "0", x2: "0", y1: "0", y2: "1", children: [_jsx("stop", { offset: "0%", stopColor: color, stopOpacity: isLast ? 0.85 : 0.5 }), _jsx("stop", { offset: "100%", stopColor: color, stopOpacity: 0.05 })] }) })), isLast && (_jsx("rect", { x: x - barGap / 2, y: 0, width: barWidth + barGap, height: chartHeight, fill: "var(--color-foreground)", fillOpacity: 0.06 })), _jsx("rect", { x: x, y: barY, width: barWidth, height: barHeight, fill: dense ? color : `url(#${gradId})`, fillOpacity: dense ? (isLast ? 1 : 0.75) : undefined, children: entry.label !== undefined && _jsx("title", { children: entry.label }) }), !dense && (_jsx("rect", { x: x, y: barY - tickHeight, width: barWidth, height: tickHeight, fill: "var(--color-foreground)", fillOpacity: isLast ? 1.0 : 0.7 }))] }, entry.key));
44
45
  })] }), (startLabel !== undefined || endLabel !== undefined) && (_jsxs("div", { className: "mt-0.5 flex justify-between text-[11px] text-muted-foreground", children: [_jsx("span", { children: startLabel }), _jsx("span", { children: endLabel })] }))] }));
45
46
  }
46
- const SINGLE_POINT_RADIUS = 3;
47
- function clampDotY(y, height) {
48
- return Math.max(SINGLE_POINT_RADIUS, Math.min(height - SINGLE_POINT_RADIUS, y));
49
- }
50
47
  /** Quadratic midpoint technique: smooths the zigzag line without overshoot
51
48
  * (~5 lines instead of a Catmull-Rom/Bezier fit). */
52
49
  export function smoothPath(pts) {
@@ -131,24 +128,6 @@ export function TimeseriesChart({ points, windowStartMs, windowEndMs, tone = "ok
131
128
  yOf(line.value) >= chartHeight / 4 && "-translate-y-full", STATUS_TONE_TEXT[line.tone ?? "muted"]), style: { top: `${(yOf(line.value) / chartHeight) * 100}%` }, children: line.label }, `${line.value}:${line.label}`)))] })) : (svg), xAxis !== undefined ? (_jsx("div", { className: "mt-1 flex justify-between text-[11px] text-muted-foreground", children: evenlySpaced(xAxis.ticks).map((fraction) => (_jsx("span", { "data-x-label": "", children: xAxis.format(windowStartMs + fraction * (windowEndMs - windowStartMs)) }, fraction))) })) : (axisLabels !== undefined && (_jsxs("div", { className: "mt-1 flex justify-between text-[11px] text-muted-foreground", children: [_jsx("span", { children: axisLabels.start }), axisLabels.mid !== undefined && _jsx("span", { children: axisLabels.mid }), _jsx("span", { children: axisLabels.end })] })))] }));
132
129
  return (_jsxs("div", { "data-testid": testId, className: STATUS_TONE_TEXT[tone], children: [yAxis === undefined ? (plot) : (_jsxs("div", { className: cn("flex gap-1.5", yAxis.unit !== undefined && "pt-4"), children: [_jsxs("div", { "aria-hidden": "true", className: cn("relative w-12 shrink-0 text-right text-[11px] text-muted-foreground", heightClass), style: heightStyle, children: [yAxis.unit !== undefined && (_jsx("span", { "data-testid": testId === undefined ? undefined : `${testId}-y-unit`, "data-y-unit": "", className: "absolute right-0 bottom-full mb-1 whitespace-nowrap leading-none", children: yAxis.unit })), yTicks.map((tick, index) => (_jsx("span", { "data-y-label": "", className: cn("absolute right-0 whitespace-nowrap leading-none", yTickPosition(index, yTicks.length)), style: { top: `${(yOf(tick) / chartHeight) * 100}%` }, children: (yAxis.format ?? String)(tick) }, tick)))] }), _jsx("div", { className: "min-w-0 flex-1", children: plot })] })), markers !== undefined && markers.length > 0 && (_jsxs("div", { className: "text-foreground", children: [_jsx(MarkerPins, { markers: markers, windowStartMs: windowStartMs, windowEndMs: windowEndMs }), _jsx("div", { className: "mt-2", children: _jsx(MarkerLegend, { markers: markers, formatMarkerTime: formatMarkerTime }) })] }))] }));
133
130
  }
134
- const CHART_TONE_VAR = {
135
- positive: "var(--color-status-ok)",
136
- negative: "var(--color-status-bad)",
137
- active: "var(--color-status-active)",
138
- neutral: "var(--color-status-neutral)",
139
- };
140
- export function chartToneColor(tone) {
141
- return CHART_TONE_VAR[tone];
142
- }
143
- const CHART_FALLBACK_TONES = ["active", "positive", "negative", "neutral"];
144
- function chartColor(key, index, tones, colors) {
145
- const color = colors?.[key];
146
- if (color !== undefined)
147
- return color;
148
- const tone = tones?.[key] ?? CHART_FALLBACK_TONES[index % CHART_FALLBACK_TONES.length] ?? "active";
149
- return CHART_TONE_VAR[tone];
150
- }
151
- const Y_TICK_COUNT = 3;
152
131
  function niceCeil(max) {
153
132
  if (max <= 0)
154
133
  return 1;
@@ -157,107 +136,13 @@ function niceCeil(max) {
157
136
  const step = normalized <= 1 ? 1 : normalized <= 2 ? 2 : normalized <= 5 ? 5 : 10;
158
137
  return step * magnitude;
159
138
  }
160
- const FINE_NICE_STEPS = [1, 1.2, 1.5, 2, 2.5, 3, 4, 5, 6, 8, 10];
161
- // Finer than niceCeil so a 1.1 M peak tops out at 1.2 M instead of 2 M.
162
- function fineNiceCeil(max) {
163
- if (max <= 0)
164
- return 1;
165
- const magnitude = 10 ** Math.floor(Math.log10(max));
166
- const normalized = max / magnitude;
167
- const step = FINE_NICE_STEPS.find((candidate) => normalized <= candidate) ?? 10;
168
- return step * magnitude;
169
- }
170
139
  function niceYTicks(max, tickCount) {
171
140
  const count = clampTickCount(tickCount);
172
141
  const step = niceCeil(max / (count - 1));
173
142
  // toPrecision strips float noise such as 3 * 0.2 = 0.6000000000000001 from labels and keys.
174
143
  return Array.from({ length: count }, (_, i) => Number((i * step).toPrecision(12)));
175
144
  }
176
- function bucketTimes(series) {
177
- const times = new Set();
178
- for (const s of series)
179
- for (const p of s.points)
180
- times.add(p.atMs);
181
- return [...times].sort((a, b) => a - b);
182
- }
183
- function valueAt(series, atMs) {
184
- return series.points.find((p) => p.atMs === atMs)?.value ?? 0;
185
- }
186
- function linePathSegments(line, xOf, yOf) {
187
- const commands = [];
188
- let penDown = false;
189
- for (const point of [...line.points].sort((a, b) => a.atMs - b.atMs)) {
190
- if (point.value === null) {
191
- penDown = false;
192
- continue;
193
- }
194
- commands.push(`${penDown ? "L" : "M"} ${xOf(point.atMs).toFixed(1)} ${yOf(point.value).toFixed(1)}`);
195
- penDown = true;
196
- }
197
- return commands.join(" ");
198
- }
199
- function seriesTotal(series) {
200
- return series.points.reduce((sum, p) => sum + (p.value ?? 0), 0);
201
- }
202
- function percent(fraction) {
203
- return `${(fraction * 100).toFixed(2)}%`;
204
- }
205
- function ChartLegend({ items, formatValue, }) {
206
- return (_jsx("ul", { className: "flex flex-wrap gap-x-4 gap-y-1 text-xs text-muted-foreground", children: items.map((item) => (_jsxs("li", { "data-testid": `chart-legend-${item.testIdKey ?? item.key}`, className: "flex items-center gap-1.5", children: [item.swatch === undefined || item.swatch === "area" ? (_jsx("span", { "aria-hidden": "true", className: "size-2 rounded-[2px]", style: { backgroundColor: item.color } })) : (_jsx("span", { "aria-hidden": "true", className: "w-3 border-t-2", style: {
207
- borderColor: item.color,
208
- borderTopStyle: item.swatch === "dashed-line" ? "dashed" : "solid",
209
- } })), _jsx("span", { children: item.label }), item.total !== undefined && (_jsx("span", { className: "tabular-nums text-foreground", children: formatValue(item.total) }))] }, item.key))) }));
210
- }
211
- function evenYTickValues(max) {
212
- return Array.from({ length: Y_TICK_COUNT }, (_, i) => (max / (Y_TICK_COUNT - 1)) * i);
213
- }
214
- function YTicks({ values, formatValue, className, }) {
215
- return (_jsx("div", { "aria-hidden": "true", className: cn("flex flex-col-reverse justify-between text-right text-[11px] tabular-nums text-muted-foreground", className), children: values.map((value, i) => (_jsx("span", { children: formatValue(value) }, i))) }));
216
- }
217
145
  const BAR_LABEL_TARGET = 8;
218
- const STACKED_AREA_PLOT_HEIGHT_CLASS = "h-56 @2xl:h-64";
219
- // A 16px pin on a ~230px-wide plot (390px viewport) collides when markers sit
220
- // within ~7% of each other, so close pins are staggered into extra rows.
221
- const MARKER_PIN_MIN_GAP_FRACTION = 0.07;
222
- const MARKER_PIN_MAX_ROWS = 3;
223
- const MARKER_PIN_ROW_HEIGHT_PX = 18;
224
- const TOOLTIP_WIDTH_REM = 14;
225
- const TOOLTIP_OFFSET_PX = 8;
226
- // A swipe over the plot (scrolling the page or the chart) must not open the card.
227
- const TOOLTIP_TAP_MAX_MOVEMENT_PX = 8;
228
- export function assignPinRows(fractions) {
229
- const lastFractionPerRow = [];
230
- return fractions.map((fraction) => {
231
- let row = lastFractionPerRow.findIndex((last) => fraction - last >= MARKER_PIN_MIN_GAP_FRACTION);
232
- if (row === -1) {
233
- row =
234
- lastFractionPerRow.length < MARKER_PIN_MAX_ROWS
235
- ? lastFractionPerRow.length
236
- : MARKER_PIN_MAX_ROWS - 1;
237
- }
238
- lastFractionPerRow[row] = fraction;
239
- return row;
240
- });
241
- }
242
- // Container-query density tiers for the x axis: label budget per tier.
243
- const AXIS_TIERS = [
244
- { maxLabels: 4, visibilityClass: "@xl:hidden" },
245
- { maxLabels: 7, visibilityClass: "hidden @xl:block @4xl:hidden" },
246
- { maxLabels: 12, visibilityClass: "hidden @4xl:block" },
247
- ];
248
- const MARKER_LABEL_FLIP_FRACTION = 0.97;
249
- // Markers sharing a timestamp become one label ("A, B") in the first one's color.
250
- function groupMarkerLabels(markers) {
251
- const groups = new Map();
252
- for (const marker of [...markers].sort((a, b) => a.atMs - b.atMs)) {
253
- groups.set(marker.atMs, [...(groups.get(marker.atMs) ?? []), marker]);
254
- }
255
- return [...groups.entries()].map(([atMs, members]) => ({
256
- atMs,
257
- text: members.map((m) => m.label).join(", "),
258
- color: members[0]?.color ?? "var(--color-foreground)",
259
- }));
260
- }
261
146
  /** Vertically stacked bars per bucket (x axis = bucket, not time).
262
147
  * The last bucket carries `todayLabel` when it contains `windowEndMs` and
263
148
  * has day granularity. */
@@ -317,369 +202,6 @@ export function SegmentBarChart({ rows, tones, colors, ariaLabel, formatValue =
317
202
  total: segmentTotals.get(key)?.total ?? 0,
318
203
  })) })] }));
319
204
  }
320
- // Two markers may share atMs and label, so the occurrence count keeps React keys unique.
321
- function sortMarkers(markers) {
322
- const seen = new Map();
323
- return [...markers]
324
- .sort((a, b) => a.atMs - b.atMs)
325
- .map((marker) => {
326
- const base = `${marker.atMs}-${marker.label}`;
327
- const occurrence = seen.get(base) ?? 0;
328
- seen.set(base, occurrence + 1);
329
- return { ...marker, key: `${base}-${occurrence}` };
330
- });
331
- }
332
- function MarkerPins({ markers, windowStartMs, windowEndMs, numbered = true, formatMarkerTime, className, }) {
333
- const span = Math.max(1, windowEndMs - windowStartMs);
334
- const sorted = sortMarkers(markers);
335
- const fractions = sorted.map((marker) => Math.max(0, Math.min(1, (marker.atMs - windowStartMs) / span)));
336
- const rows = assignPinRows(fractions);
337
- const rowCount = Math.max(1, ...rows.map((row) => row + 1));
338
- return (_jsx("div", { "aria-hidden": "true", className: cn("relative mt-1", className), style: { height: rowCount * MARKER_PIN_ROW_HEIGHT_PX }, children: sorted.map((marker, i) => (_jsx("span", { "data-testid": "chart-marker-pin", title: numbered
339
- ? undefined
340
- : formatMarkerTime === undefined
341
- ? marker.label
342
- : `${marker.label} · ${formatMarkerTime(marker.atMs)}`, className: cn("absolute flex -translate-x-1/2 items-center justify-center rounded-full text-[10px] font-medium text-background", numbered ? "size-4" : "size-2", marker.color === undefined && "bg-foreground"), style: {
343
- left: percent(fractions[i] ?? 0),
344
- top: (rows[i] ?? 0) * MARKER_PIN_ROW_HEIGHT_PX + (numbered ? 0 : 4),
345
- ...(marker.color !== undefined && { backgroundColor: marker.color }),
346
- }, children: numbered ? i + 1 : null }, marker.key))) }));
347
- }
348
- function markerLegendItems(markers) {
349
- const items = new Map();
350
- for (const marker of markers) {
351
- const label = marker.legendLabel ?? marker.label;
352
- const color = marker.color ?? "var(--color-foreground)";
353
- const key = `marker-${label}-${color}`;
354
- if (!items.has(key))
355
- items.set(key, { key, label, color });
356
- }
357
- return [...items.values()];
358
- }
359
- function MarkerLegend({ markers, formatMarkerTime, className, }) {
360
- return (_jsx("ol", { className: cn("flex flex-col gap-1 text-xs text-muted-foreground", className), children: sortMarkers(markers).map((marker, i) => (_jsxs("li", { "data-testid": "chart-marker-item", className: "flex gap-2", children: [_jsx("span", { className: "w-4 shrink-0 text-center tabular-nums text-foreground", style: marker.color !== undefined ? { color: marker.color } : undefined, children: i + 1 }), _jsx("span", { children: marker.label }), formatMarkerTime !== undefined && (_jsx("span", { className: "ml-auto tabular-nums", children: formatMarkerTime(marker.atMs) }))] }, marker.key))) }));
361
- }
362
- const SCROLLABLE_BUCKET_WIDTH_PX = 48;
363
- function bucketLabelTransform(index, count) {
364
- if (index === 0)
365
- return "";
366
- return index === count - 1 ? "-translate-x-full" : "-translate-x-1/2";
367
- }
368
- const DEFAULT_BRUSH_LABELS = { start: "Window start", end: "Window end" };
369
- const BRUSH_OVERVIEW_WIDTH = 100;
370
- const BRUSH_OVERVIEW_HEIGHT = 24;
371
- function brushKeyTarget(key, index, min, max) {
372
- if (key === "ArrowLeft")
373
- return index - 1;
374
- if (key === "ArrowRight")
375
- return index + 1;
376
- if (key === "Home")
377
- return min;
378
- return key === "End" ? max : undefined;
379
- }
380
- function StackedAreaBrush({ times, totals, window, labels, onChange, testId, }) {
381
- const trackRef = useRef(null);
382
- const dragRef = useRef(null);
383
- const firstMs = times[0] ?? 0;
384
- const lastMs = times[times.length - 1] ?? 1;
385
- const span = Math.max(1, lastMs - firstMs);
386
- const fractionOf = (atMs) => Math.max(0, Math.min(1, (atMs - firstMs) / span));
387
- const startIndex = nearestIndex(times, window.startMs);
388
- const endIndex = nearestIndex(times, window.endMs);
389
- const lastIndex = times.length - 1;
390
- const max = Math.max(...totals, 1);
391
- const overviewPoints = times.map((atMs, i) => `${(fractionOf(atMs) * BRUSH_OVERVIEW_WIDTH).toFixed(1)} ${(BRUSH_OVERVIEW_HEIGHT - ((totals[i] ?? 0) / max) * BRUSH_OVERVIEW_HEIGHT).toFixed(1)}`);
392
- const overviewPath = `M 0 ${BRUSH_OVERVIEW_HEIGHT} L ${overviewPoints.join(" L ")} L ${BRUSH_OVERVIEW_WIDTH} ${BRUSH_OVERVIEW_HEIGHT} Z`;
393
- const commit = (startMs, endMs, anchor) => onChange(snapBrushWindow(times, startMs, endMs, anchor));
394
- const beginDrag = (mode) => (e) => {
395
- e.currentTarget.setPointerCapture(e.pointerId);
396
- dragRef.current = {
397
- mode,
398
- originX: e.clientX,
399
- originStartMs: window.startMs,
400
- originEndMs: window.endMs,
401
- };
402
- };
403
- const moveDrag = (e) => {
404
- const drag = dragRef.current;
405
- const trackWidth = trackRef.current?.getBoundingClientRect().width ?? 0;
406
- if (drag === null || trackWidth <= 0)
407
- return;
408
- const deltaMs = ((e.clientX - drag.originX) / trackWidth) * span;
409
- if (drag.mode === "move") {
410
- const shift = Math.max(firstMs - drag.originStartMs, Math.min(lastMs - drag.originEndMs, deltaMs));
411
- onChange(snapMovedBrushWindow(times, drag.originStartMs, drag.originEndMs, shift));
412
- }
413
- else if (drag.mode === "start") {
414
- commit(drag.originStartMs + deltaMs, drag.originEndMs, "start");
415
- }
416
- else {
417
- commit(drag.originStartMs, drag.originEndMs + deltaMs, "end");
418
- }
419
- };
420
- const endDrag = () => {
421
- dragRef.current = null;
422
- };
423
- const dragHandlers = (mode) => ({
424
- onPointerDown: beginDrag(mode),
425
- onPointerMove: moveDrag,
426
- onPointerUp: endDrag,
427
- onPointerCancel: endDrag,
428
- });
429
- const handleKeyDown = (handle) => (e) => {
430
- const isStart = handle === "start";
431
- const index = isStart ? startIndex : endIndex;
432
- const min = isStart ? 0 : startIndex + 1;
433
- const maxIndex = isStart ? endIndex - 1 : lastIndex;
434
- const target = brushKeyTarget(e.key, index, min, maxIndex);
435
- if (target === undefined)
436
- return;
437
- e.preventDefault();
438
- const next = Math.max(min, Math.min(maxIndex, target));
439
- if (next === index)
440
- return;
441
- const nextStart = times[isStart ? next : startIndex];
442
- const nextEnd = times[isStart ? endIndex : next];
443
- if (nextStart !== undefined && nextEnd !== undefined) {
444
- onChange({ startMs: nextStart, endMs: nextEnd });
445
- }
446
- };
447
- const startFraction = fractionOf(window.startMs);
448
- const endFraction = fractionOf(window.endMs);
449
- const handleClass = "absolute top-0 z-10 h-full w-6 -translate-x-1/2 cursor-ew-resize touch-none focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring";
450
- const renderHandle = (handle) => (_jsx("div", { "data-testid": `${testId}-${handle}`, role: "slider", tabIndex: 0, "aria-label": labels[handle], "aria-valuemin": handle === "start" ? 0 : startIndex + 1, "aria-valuemax": handle === "start" ? endIndex - 1 : lastIndex, "aria-valuenow": handle === "start" ? startIndex : endIndex, className: handleClass, style: { left: percent(handle === "start" ? startFraction : endFraction) }, onKeyDown: handleKeyDown(handle), ...dragHandlers(handle), children: _jsx("span", { "aria-hidden": "true", className: "pointer-events-none absolute inset-y-0 left-1/2 w-2 -translate-x-1/2 rounded-sm bg-foreground/60" }) }));
451
- return (_jsxs("div", { ref: trackRef, "data-testid": testId, className: "relative h-8 w-full select-none rounded-sm border border-border", children: [_jsx("svg", { viewBox: `0 0 ${BRUSH_OVERVIEW_WIDTH} ${BRUSH_OVERVIEW_HEIGHT}`, preserveAspectRatio: "none", className: "absolute inset-0 size-full", role: "presentation", children: _jsx("path", { d: overviewPath, fill: "var(--color-muted-foreground)", fillOpacity: 0.25 }) }), _jsx("div", { "data-testid": `${testId}-window`, className: "absolute top-0 h-full cursor-grab touch-none border-x border-primary bg-primary/10", style: {
452
- left: percent(startFraction),
453
- width: percent(Math.max(0, endFraction - startFraction)),
454
- }, ...dragHandlers("move") }), renderHandle("start"), renderHandle("end")] }));
455
- }
456
- /** Stacked bands over time. Right of `todayMs` is the forecast: lighter
457
- * fill plus a vertical "today" line. */
458
- export function StackedAreaChart({ series: allSeries, windowStartMs: fullWindowStartMs, windowEndMs: fullWindowEndMs, todayMs, tones, colors, markers: allMarkers, lines: allLines = [], ranges, initialWindow, brush = false, windowSelection, onWindowSelectionChange, brushLabels = DEFAULT_BRUSH_LABELS, ariaLabel, todayLabel, formatBucketLabel, formatValue = String, formatAxisValue = formatValue, formatMarkerTime, dateFormat, timeZone, markerLegend = "list", markerLabels = false, scrollable = false, showLegendTotals = true, emptyContent, testId, }) {
459
- const clipId = useId();
460
- const scrollRef = useRef(null);
461
- const [internalSelection, setInternalSelection] = useState(initialWindowSelection(ranges, initialWindow));
462
- const isControlled = onWindowSelectionChange !== undefined;
463
- const selection = isControlled ? windowSelection : internalSelection;
464
- const changeSelection = (next) => {
465
- if (isControlled)
466
- onWindowSelectionChange(next);
467
- else
468
- setInternalSelection(next);
469
- };
470
- const allTimes = bucketTimes([...allSeries, ...allLines]);
471
- const isWindowing = ranges !== undefined || brush || selection !== undefined;
472
- const fullWindow = { startMs: fullWindowStartMs, endMs: fullWindowEndMs };
473
- const visibleWindow = isWindowing
474
- ? resolveStackedAreaWindow({
475
- selection,
476
- ranges,
477
- brush,
478
- todayMs,
479
- bucketTimes: allTimes,
480
- fullWindow,
481
- })
482
- : fullWindow;
483
- const windowStartMs = visibleWindow.startMs;
484
- const windowEndMs = visibleWindow.endMs;
485
- const effectiveDateFormat = dateFormat ?? stackedAreaDateFormatFor(visibleWindow);
486
- const formatBucket = (atMs) => formatBucketLabel(atMs, effectiveDateFormat);
487
- const tierTicks = useMemo(() => AXIS_TIERS.map((tier) => stackedAreaAxisTicks({ startMs: windowStartMs, endMs: windowEndMs }, tier.maxLabels, effectiveDateFormat, timeZone)), [windowStartMs, windowEndMs, effectiveDateFormat, timeZone]);
488
- const formatMarkerTimeInWindow = formatMarkerTime === undefined
489
- ? undefined
490
- : (atMs) => formatMarkerTime(atMs, effectiveDateFormat);
491
- const isNarrowed = windowStartMs !== fullWindowStartMs || windowEndMs !== fullWindowEndMs;
492
- const series = isNarrowed
493
- ? allSeries.map((s) => ({ ...s, points: pointsWithin(s.points, visibleWindow) }))
494
- : allSeries;
495
- const lines = isNarrowed
496
- ? allLines.map((line) => ({ ...line, points: pointsWithin(line.points, visibleWindow) }))
497
- : allLines;
498
- const markers = isNarrowed && allMarkers !== undefined ? pointsWithin(allMarkers, visibleWindow) : allMarkers;
499
- const times = bucketTimes([...series, ...lines]);
500
- const bucketCount = times.length;
501
- const span = Math.max(1, windowEndMs - windowStartMs);
502
- const fractionOf = (atMs) => Math.max(0, Math.min(1, (atMs - windowStartMs) / span));
503
- const todayFraction = todayMs !== undefined ? fractionOf(todayMs) : undefined;
504
- const [selectedTooltipAtMs, setSelectedTooltipAtMs] = useState(null);
505
- // Stored as a time, not an index: a shifted window must not retarget the card to another bucket.
506
- const tooltipAtMs = selectedTooltipAtMs !== null && times.includes(selectedTooltipAtMs)
507
- ? selectedTooltipAtMs
508
- : undefined;
509
- const tooltipOverlayRef = useRef(null);
510
- const touchStartRef = useRef(null);
511
- const isTooltipOpen = tooltipAtMs !== undefined;
512
- useEffect(() => {
513
- if (!isTooltipOpen)
514
- return;
515
- const closeOnEscape = (event) => {
516
- if (event.key === "Escape")
517
- setSelectedTooltipAtMs(null);
518
- };
519
- const closeOnOutsidePointerDown = (event) => {
520
- const overlay = tooltipOverlayRef.current;
521
- if (overlay !== null && event.target instanceof Node && overlay.contains(event.target))
522
- return;
523
- setSelectedTooltipAtMs(null);
524
- };
525
- document.addEventListener("keydown", closeOnEscape);
526
- document.addEventListener("pointerdown", closeOnOutsidePointerDown);
527
- return () => {
528
- document.removeEventListener("keydown", closeOnEscape);
529
- document.removeEventListener("pointerdown", closeOnOutsidePointerDown);
530
- };
531
- }, [isTooltipOpen]);
532
- useEffect(() => {
533
- const el = scrollRef.current;
534
- if (!scrollable || el === null || bucketCount === 0)
535
- return;
536
- el.scrollLeft =
537
- todayFraction === undefined
538
- ? el.scrollWidth
539
- : Math.max(0, todayFraction * el.scrollWidth - el.clientWidth / 2);
540
- }, [scrollable, todayFraction, bucketCount]);
541
- if (times.length === 0)
542
- return _jsx("div", { "data-testid": testId, children: emptyContent });
543
- const width = 300;
544
- const height = 120;
545
- const totals = times.map((atMs) => series.reduce((sum, s) => sum + valueAt(s, atMs), 0));
546
- const lineValues = lines.flatMap((line) => line.points.flatMap((p) => (p.value === null ? [] : [p.value])));
547
- const isSingleBucket = times.length === 1;
548
- if (isSingleBucket && totals[0] === 0) {
549
- return _jsx("div", { "data-testid": testId, children: emptyContent });
550
- }
551
- // Doubling keeps a lone bucket away from the top edge of the y-scale.
552
- const max = fineNiceCeil(Math.max(...totals, ...lineValues) * (isSingleBucket ? 2 : 1));
553
- const yTickValues = evenYTickValues(max);
554
- const yOf = (value) => height - (value / max) * height;
555
- const lower = times.map(() => 0);
556
- const bands = series.map((s, si) => {
557
- const upper = times.map((atMs, ti) => (lower[ti] ?? 0) + valueAt(s, atMs));
558
- const top = times.map((atMs, ti) => `${(fractionOf(atMs) * width).toFixed(1)} ${yOf(upper[ti] ?? 0).toFixed(1)}`);
559
- const bottom = times
560
- .map((atMs, ti) => `${(fractionOf(atMs) * width).toFixed(1)} ${yOf(lower[ti] ?? 0).toFixed(1)}`)
561
- .reverse();
562
- const bucketTop = upper[0] ?? 0;
563
- const bucketHeight = bucketTop - (lower[0] ?? 0);
564
- for (let ti = 0; ti < times.length; ti++)
565
- lower[ti] = upper[ti] ?? 0;
566
- return {
567
- key: s.key,
568
- color: chartColor(s.key, si, tones, colors),
569
- edge: `M ${top.join(" L ")}`,
570
- d: `M ${top.join(" L ")} L ${bottom.join(" L ")} Z`,
571
- dot: isSingleBucket && bucketHeight > 0 ? clampDotY(yOf(bucketTop), height) : undefined,
572
- };
573
- });
574
- const lineColorOffset = series.length;
575
- const linePaths = lines.map((line, li) => ({
576
- key: line.key,
577
- label: line.label,
578
- dashed: line.dashed === true,
579
- color: chartColor(line.key, lineColorOffset + li, tones, colors),
580
- d: linePathSegments(line, (atMs) => fractionOf(atMs) * width, yOf),
581
- }));
582
- const coloredMarkers = sortMarkers(markers ?? []).filter((marker) => marker.color !== undefined);
583
- const todayX = (todayFraction ?? 1) * width;
584
- const bandGradientId = (index) => `${clipId}-band-${index}`;
585
- const renderBands = (fillOpacity, edgeOpacity) => bands.map((band, bi) => band.dot === undefined ? (_jsxs("g", { children: [_jsx("path", { d: band.d, fill: `url(#${bandGradientId(bi)})`, fillOpacity: fillOpacity, stroke: "none" }), _jsx("path", { "data-testid": `chart-band-edge-${band.key}`, d: band.edge, fill: "none", stroke: band.color, strokeOpacity: edgeOpacity, strokeWidth: 1.5, vectorEffect: "non-scaling-stroke" })] }, band.key)) : (_jsx("circle", { "data-testid": "stacked-area-single-point", cx: width / 2, cy: band.dot, r: SINGLE_POINT_RADIUS, fill: band.color, fillOpacity: fillOpacity }, band.key)));
586
- const tooltipFraction = tooltipAtMs === undefined ? 0 : fractionOf(tooltipAtMs);
587
- const tooltipRows = tooltipAtMs === undefined
588
- ? []
589
- : [
590
- ...series.map((s, i) => ({
591
- key: s.key,
592
- label: s.label,
593
- color: chartColor(s.key, i, tones, colors),
594
- swatch: "area",
595
- value: valueAt(s, tooltipAtMs),
596
- })),
597
- ...lines.flatMap((line, li) => {
598
- const value = line.points.find((p) => p.atMs === tooltipAtMs)?.value;
599
- return value === null || value === undefined
600
- ? []
601
- : [
602
- {
603
- key: line.key,
604
- label: line.label,
605
- color: linePaths[li]?.color ?? "var(--color-foreground)",
606
- swatch: line.dashed === true ? "dashed-line" : "line",
607
- value,
608
- },
609
- ];
610
- }),
611
- ];
612
- const tooltipOnLeft = tooltipFraction > 0.5;
613
- const tooltipEdge = `${percent(tooltipFraction)} ${tooltipOnLeft ? "-" : "+"} ${TOOLTIP_OFFSET_PX}px`;
614
- const tooltipLeft = tooltipOnLeft
615
- ? `max(0px, calc(${tooltipEdge} - ${TOOLTIP_WIDTH_REM}rem))`
616
- : `max(0px, min(calc(${tooltipEdge}), calc(100% - ${TOOLTIP_WIDTH_REM}rem)))`;
617
- const bucketAtMsUnderPointer = (event) => {
618
- const rect = event.currentTarget.getBoundingClientRect();
619
- return (times[bucketIndexAtFraction(times, visibleWindow, rect.width === 0 ? 0 : (event.clientX - rect.left) / rect.width)] ?? null);
620
- };
621
- const tooltipOverlay = (_jsx("div", { ref: tooltipOverlayRef, "data-testid": "chart-tooltip-overlay", className: cn("absolute inset-x-0 top-0", STACKED_AREA_PLOT_HEIGHT_CLASS), onPointerMove: (event) => {
622
- if (event.pointerType === "mouse")
623
- setSelectedTooltipAtMs(bucketAtMsUnderPointer(event));
624
- }, onPointerLeave: (event) => {
625
- if (event.pointerType === "mouse")
626
- setSelectedTooltipAtMs(null);
627
- }, onPointerDown: (event) => {
628
- if (event.pointerType === "mouse")
629
- return;
630
- touchStartRef.current = { x: event.clientX, y: event.clientY };
631
- }, onPointerCancel: () => {
632
- touchStartRef.current = null;
633
- }, onPointerUp: (event) => {
634
- const start = touchStartRef.current;
635
- touchStartRef.current = null;
636
- if (event.pointerType === "mouse" || start === null)
637
- return;
638
- const moved = Math.hypot(event.clientX - start.x, event.clientY - start.y);
639
- if (moved >= TOOLTIP_TAP_MAX_MOVEMENT_PX)
640
- return;
641
- const next = bucketAtMsUnderPointer(event);
642
- setSelectedTooltipAtMs((current) => (current === next ? null : next));
643
- }, children: tooltipAtMs !== undefined && (_jsxs(_Fragment, { children: [_jsx("div", { "aria-hidden": "true", className: "pointer-events-none absolute inset-y-0 w-px bg-foreground/40", style: { left: percent(tooltipFraction) } }), _jsxs("div", { "data-testid": "chart-tooltip", "data-side": tooltipOnLeft ? "left" : "right", className: "pointer-events-none absolute top-2 rounded-md border bg-popover p-2 text-xs text-popover-foreground shadow-sm", style: { left: tooltipLeft, width: `min(${TOOLTIP_WIDTH_REM}rem, 100%)` }, children: [_jsx("div", { className: "mb-1 font-medium", children: formatBucket(tooltipAtMs) }), tooltipRows.map((row) => (_jsxs("div", { className: "flex items-center gap-1.5", children: [row.swatch === "area" ? (_jsx("span", { "aria-hidden": "true", className: "size-2 shrink-0 rounded-[2px]", style: { backgroundColor: row.color } })) : (_jsx("span", { "aria-hidden": "true", className: "w-3 shrink-0 border-t-2", style: {
644
- borderColor: row.color,
645
- borderTopStyle: row.swatch === "dashed-line" ? "dashed" : "solid",
646
- } })), _jsx("span", { className: "min-w-0 truncate text-muted-foreground", children: row.label }), _jsx("span", { className: "ml-auto tabular-nums", children: formatAxisValue(row.value) })] }, row.key)))] })] })) }));
647
- const plot = (_jsxs(_Fragment, { children: [todayFraction !== undefined && (_jsx("span", { "data-testid": "chart-today-label", className: "absolute -top-4 -translate-x-1/2 text-[11px] text-foreground", style: { left: percent(todayFraction) }, children: todayLabel })), _jsxs("svg", { viewBox: `0 0 ${width} ${height}`, preserveAspectRatio: "none", className: cn("block w-full", STACKED_AREA_PLOT_HEIGHT_CLASS), role: "img", "aria-label": ariaLabel, children: [_jsx("title", { children: ariaLabel }), _jsxs("defs", { children: [bands.map((band, bi) => (_jsxs("linearGradient", { id: bandGradientId(bi), x1: "0", x2: "0", y1: "0", y2: "1", children: [_jsx("stop", { offset: "0%", stopColor: band.color, stopOpacity: 1 }), _jsx("stop", { offset: "100%", stopColor: band.color, stopOpacity: 0.35 })] }, band.key))), _jsx("clipPath", { id: `${clipId}-past`, children: _jsx("rect", { x: 0, y: 0, width: todayX, height: height }) }), _jsx("clipPath", { id: `${clipId}-future`, children: _jsx("rect", { x: todayX, y: 0, width: width - todayX, height: height }) })] }), yTickValues.map((tickValue) => (_jsx("line", { "data-testid": "chart-grid-line", x1: 0, x2: width, y1: yOf(tickValue), y2: yOf(tickValue), stroke: "var(--color-border)", strokeWidth: 1, strokeDasharray: "3 3", vectorEffect: "non-scaling-stroke" }, tickValue))), _jsx("g", { clipPath: `url(#${clipId}-past)`, children: renderBands(0.55, 0.9) }), todayFraction !== undefined && (_jsx("g", { "data-testid": "chart-forecast-region", clipPath: `url(#${clipId}-future)`, children: renderBands(0.25, 0.5) })), coloredMarkers.map((marker) => {
648
- const x = fractionOf(marker.atMs) * width;
649
- return (_jsx("line", { "data-testid": "chart-marker-guide", x1: x, x2: x, y1: 0, y2: height, stroke: marker.color, strokeWidth: 1, strokeDasharray: "2 3", vectorEffect: "non-scaling-stroke" }, marker.key));
650
- }), linePaths.map((line) => (_jsx("path", { "data-testid": `chart-line-${line.key}`, d: line.d, fill: "none", stroke: line.color, strokeWidth: 2, strokeDasharray: line.dashed ? "6 4" : undefined, vectorEffect: "non-scaling-stroke" }, line.key))), todayFraction !== undefined && (_jsx("line", { "data-testid": "chart-today-line", x1: todayX, x2: todayX, y1: 0, y2: height, stroke: "var(--color-foreground)", strokeWidth: 1, strokeDasharray: "3 3", vectorEffect: "non-scaling-stroke" }))] }), scrollable ? (_jsx("div", { "data-testid": "chart-bucket-labels", className: "relative mt-1 h-4 text-[11px] text-muted-foreground", children: times.map((atMs, i) => (_jsx("span", { className: cn("absolute whitespace-nowrap", bucketLabelTransform(i, times.length)), style: { left: percent(fractionOf(atMs)) }, children: formatBucket(atMs) }, atMs))) })) : (_jsxs("div", { className: "relative mt-1 h-4 text-[11px] text-muted-foreground", children: [_jsx("span", { className: "absolute left-0 whitespace-nowrap", children: formatBucket(windowStartMs) }), _jsx("span", { className: "absolute right-0 whitespace-nowrap", children: formatBucket(windowEndMs) }), AXIS_TIERS.map((tier, tierIndex) => (_jsx("div", { className: cn("absolute inset-0", tier.visibilityClass), children: (tierTicks[tierIndex] ?? []).map((tickMs) => (_jsx("span", { "data-testid": "chart-axis-tick", className: "absolute -translate-x-1/2 whitespace-nowrap", style: { left: percent(fractionOf(tickMs)) }, children: formatBucket(tickMs) }, tickMs))) }, tier.maxLabels)))] })), tooltipOverlay, markerLabels &&
651
- groupMarkerLabels(markers ?? []).map((group) => {
652
- const fraction = fractionOf(group.atMs);
653
- const flipped = fraction > MARKER_LABEL_FLIP_FRACTION;
654
- return (_jsx("div", { className: cn("pointer-events-none absolute top-0 hidden @2xl:block", flipped ? "-translate-x-full pr-1" : "pl-1"), style: { left: percent(fraction) }, children: _jsx("span", { "data-testid": "chart-marker-label", className: "block max-h-44 rotate-180 overflow-hidden text-ellipsis whitespace-nowrap text-[11px] leading-none [writing-mode:vertical-rl] @2xl:max-h-60", style: { color: group.color }, children: group.text }) }, group.atMs));
655
- }), markers !== undefined && markers.length > 0 && (_jsx(MarkerPins, { markers: markers, windowStartMs: windowStartMs, windowEndMs: windowEndMs, numbered: markerLegend === "list" || markerLabels, formatMarkerTime: formatMarkerTimeInWindow, ...(markerLabels && { className: "@2xl:hidden" }) }))] }));
656
- const ownRangeSwitch = !isControlled && ranges !== undefined ? (_jsx("div", { className: "flex justify-end", children: _jsx(ModeSwitch, { value: selection?.kind === "range" ? selection.value : null, options: ranges.options.map((option) => ({
657
- value: option.value,
658
- label: option.label,
659
- })), onChange: (value) => changeSelection({ kind: "range", value }), ariaLabel: ariaLabel, variant: "pill", testId: testId !== undefined ? `${testId}-range` : undefined }) })) : undefined;
660
- const brushBar = brush && allTimes.length >= 2 ? (_jsx(StackedAreaBrush, { times: allTimes, totals: allTimes.map((atMs) => allSeries.reduce((sum, s) => sum + valueAt(s, atMs), 0)), window: visibleWindow, labels: brushLabels, onChange: (next) => changeSelection({ kind: "brush", startMs: next.startMs, endMs: next.endMs }), testId: `${testId ?? "stacked-area"}-brush` })) : undefined;
661
- return (_jsxs("div", { "data-testid": testId, className: "@container flex flex-col gap-3", children: [ownRangeSwitch, _jsxs("div", { className: "flex gap-2", children: [_jsx(YTicks, { values: yTickValues, formatValue: formatAxisValue, className: cn("shrink-0 pb-0", STACKED_AREA_PLOT_HEIGHT_CLASS, scrollable && "mt-4") }), scrollable ? (_jsx("div", { ref: scrollRef, "data-testid": "chart-scroll-container", className: "grow overflow-x-auto pt-4", children: _jsx("div", { "data-testid": "chart-scroll-content", className: "relative", style: {
662
- minWidth: "100%",
663
- width: `${times.length * SCROLLABLE_BUCKET_WIDTH_PX}px`,
664
- }, children: plot }) })) : (_jsx("div", { className: "relative grow", children: plot }))] }), brushBar, _jsx(ChartLegend, { formatValue: formatValue, items: [
665
- ...series.map((s, i) => ({
666
- key: s.key,
667
- label: s.label,
668
- color: chartColor(s.key, i, tones, colors),
669
- ...(showLegendTotals && { total: seriesTotal(s) }),
670
- })),
671
- ...linePaths.map((line) => ({
672
- key: `line:${line.key}`,
673
- testIdKey: `line-${line.key}`,
674
- label: line.label,
675
- color: line.color,
676
- swatch: line.dashed ? "dashed-line" : "line",
677
- })),
678
- ...(markerLegend === "legend"
679
- ? markerLegendItems(markers ?? []).map((item) => ({
680
- ...item,
681
- swatch: "dashed-line",
682
- }))
683
- : []),
684
- ] }), markers !== undefined && markers.length > 0 && (markerLegend === "list" || markerLabels) && (_jsx(MarkerLegend, { markers: markers, formatMarkerTime: formatMarkerTimeInWindow, ...(markerLabels && { className: "@2xl:hidden" }) }))] }));
685
- }
205
+ export { chartToneColor } from "./chart-data.js";
206
+ export { assignPinRows } from "./chart-markers.js";
207
+ export { StackedAreaChart } from "./stacked-area-chart.js";
@@ -0,0 +1,3 @@
1
+ export type DrawerSide = "left" | "right" | "top" | "bottom";
2
+ export declare const MAX_VIEWPORT_WIDTH = "85vw";
3
+ export declare function sidePanelClass(side: DrawerSide, narrow: boolean, variant: "floating" | "flush", belowHeader: boolean): string;
@@ -0,0 +1,40 @@
1
+ // Must match the static `max(600px,37.5vw)` in sidePanelClass — Tailwind's
2
+ // JIT can't read DEFAULT_WIDTH_MIN_PX/DEFAULT_WIDTH_VIEWPORT_RATIO at
3
+ // runtime, so the two are kept in sync by hand.
4
+ // Same cap as WIDTH_CLASS: an explicit `width` must not outgrow the viewport.
5
+ export const MAX_VIEWPORT_WIDTH = "85vw";
6
+ const WIDTH_CLASS = "w-[max(600px,37.5vw)] max-w-[85vw] sm:max-w-[max(600px,37.5vw)]";
7
+ // "floating": 32px margin + 32px radius so the panel reads as detached from
8
+ // the viewport edge, unlike the sheet primitive's flush-edge default.
9
+ // "flush" docks it back to that flush-edge default instead, borrowing the
10
+ // vendored Sheet's per-side border (only the edge facing the app content).
11
+ // Below the narrow-viewport breakpoint neither treatment fits — the JS hook
12
+ // decides, not a `sm:` Tailwind prefix, since the resize width below already
13
+ // comes from JS and two decision sources is the bug this avoids.
14
+ export function sidePanelClass(side, narrow, variant, belowHeader) {
15
+ if (narrow)
16
+ return "inset-0 h-full w-full max-w-none rounded-none border-0 overflow-hidden";
17
+ if (variant === "flush") {
18
+ switch (side) {
19
+ // h-full would pin height to 100vh despite the shifted top; h-auto lets inset-y-0 size it.
20
+ case "left":
21
+ return `inset-y-0 left-0 ${belowHeader ? "h-auto" : "h-full"} ${WIDTH_CLASS} border-r shadow-2xl overflow-hidden`;
22
+ case "top":
23
+ return `inset-x-0 ${belowHeader ? "top-(--shell-header-height)" : "top-0"} h-auto max-h-[80vh] border-b shadow-2xl overflow-hidden`;
24
+ case "bottom":
25
+ return "inset-x-0 bottom-0 h-auto max-h-[80vh] border-t shadow-2xl overflow-hidden";
26
+ default:
27
+ return `inset-y-0 right-0 ${belowHeader ? "h-auto" : "h-full"} ${WIDTH_CLASS} border-l shadow-2xl overflow-hidden`;
28
+ }
29
+ }
30
+ switch (side) {
31
+ case "left":
32
+ return `inset-y-8 left-8 h-auto ${WIDTH_CLASS} rounded-[2rem] border shadow-2xl overflow-hidden`;
33
+ case "top":
34
+ return "inset-x-8 top-8 h-auto max-h-[80vh] rounded-[2rem] border shadow-2xl overflow-hidden";
35
+ case "bottom":
36
+ return "inset-x-8 bottom-8 h-auto max-h-[80vh] rounded-[2rem] border shadow-2xl overflow-hidden";
37
+ default:
38
+ return `inset-y-8 right-8 h-auto ${WIDTH_CLASS} rounded-[2rem] border shadow-2xl overflow-hidden`;
39
+ }
40
+ }
@@ -0,0 +1,11 @@
1
+ import type { ReactNode } from "react";
2
+ import type { useTranslation } from "../../index.ts";
3
+ import type { DrawerResizeHandle, DrawerResizeState } from "./use-drawer-resize.ts";
4
+ export type DrawerResizeControlsModel = {
5
+ readonly t: ReturnType<typeof useTranslation>;
6
+ readonly side: "left" | "right" | "top" | "bottom";
7
+ readonly resize: DrawerResizeState;
8
+ readonly handle: DrawerResizeHandle;
9
+ };
10
+ export declare function renderMaximizeButton(model: DrawerResizeControlsModel): ReactNode;
11
+ export declare function renderResizeHandle(model: DrawerResizeControlsModel): ReactNode;
@@ -0,0 +1,12 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import { Maximize2Icon, Minimize2Icon } from "lucide-react";
3
+ import { cn } from "../lib/cn.js";
4
+ export function renderMaximizeButton(model) {
5
+ const { t, resize } = model;
6
+ const { maximized, setMaximized } = resize;
7
+ return (_jsx("button", { type: "button", onClick: () => setMaximized((m) => !m), "aria-pressed": maximized, "aria-label": maximized ? t("kumiko.widget.drawer.restore") : t("kumiko.widget.drawer.maximize"), className: "absolute top-4 right-14 z-10 rounded-xs p-1 text-muted-foreground opacity-70 transition-opacity hover:opacity-100 hover:bg-secondary focus-visible:ring-2 focus-visible:ring-ring focus-visible:outline-hidden", children: maximized ? _jsx(Minimize2Icon, { className: "size-4" }) : _jsx(Maximize2Icon, { className: "size-4" }) }));
8
+ }
9
+ export function renderResizeHandle(model) {
10
+ const { t, side, resize, handle } = model;
11
+ return (_jsx("div", { role: "separator", "aria-orientation": "vertical", "aria-label": t("kumiko.widget.drawer.resize"), "aria-valuenow": resize.effectiveWidthPx, "aria-valuemin": resize.minWidthPx, "aria-valuemax": resize.effectiveMaxWidthPx(), tabIndex: 0, ...handle.handleDrag, onKeyDown: handle.onHandleKeyDown, className: cn("absolute inset-y-0 z-10 w-1 cursor-col-resize touch-none after:absolute after:inset-y-0 after:left-1/2 after:w-3 after:-translate-x-1/2 hover:bg-border", side === "right" ? "left-0 -translate-x-1/2" : "right-0 translate-x-1/2") }));
12
+ }
@@ -0,0 +1,19 @@
1
+ import type { CSSProperties } from "react";
2
+ export declare const MIN_WIDTH_PX = 320;
3
+ export declare const MAX_WIDTH_PX = 1000;
4
+ export declare function defaultWidthFromViewport(): number;
5
+ export declare function buildOverlayStyle(backdrop: {
6
+ readonly blurPx?: number;
7
+ readonly dimPercent?: number;
8
+ } | undefined): CSSProperties;
9
+ type SheetStyleInput = {
10
+ readonly side: "left" | "right" | "top" | "bottom";
11
+ readonly narrow: boolean;
12
+ readonly canResize: boolean;
13
+ readonly width: number | string | undefined;
14
+ readonly effectiveWidthPx: number;
15
+ readonly variant: "floating" | "flush";
16
+ readonly belowHeader: boolean;
17
+ };
18
+ export declare function buildSheetStyle(input: SheetStyleInput): CSSProperties | undefined;
19
+ export {};