@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
@@ -0,0 +1,60 @@
1
+ import { MAX_VIEWPORT_WIDTH } from "./drawer-panel-class.js";
2
+ export const MIN_WIDTH_PX = 320;
3
+ export const MAX_WIDTH_PX = 1000;
4
+ // Must match the static `max(600px,37.5vw)` in floatingSideClass — Tailwind's
5
+ // JIT can't read these at runtime, so the two are kept in sync by hand.
6
+ // 600px (vs. the old 520px floor) gives a two-column field row (e.g.
7
+ // street/number, zip/city) room to breathe, while staying well under half
8
+ // of a 1280px viewport so a narrow window isn't dominated by the drawer.
9
+ // 37.5vw (vs. the old 25vw) keeps that same 600px on a 1280–1600px window
10
+ // (25vw was still under the floor there, making the ratio dead weight up
11
+ // to 2400px) but actually grows past the floor by ~1600px, so a 1920px
12
+ // window — where the two-column row is cramped just as often — lands
13
+ // around 720px instead of being stuck at 600px alongside a 1280px window.
14
+ const DEFAULT_WIDTH_MIN_PX = 600;
15
+ const DEFAULT_WIDTH_VIEWPORT_RATIO = 0.375;
16
+ const DEFAULT_BLUR_PX = 0;
17
+ const DEFAULT_DIM_PERCENT = 20;
18
+ export function defaultWidthFromViewport() {
19
+ return typeof window === "undefined"
20
+ ? DEFAULT_WIDTH_MIN_PX
21
+ : Math.max(DEFAULT_WIDTH_MIN_PX, Math.round(window.innerWidth * DEFAULT_WIDTH_VIEWPORT_RATIO));
22
+ }
23
+ function normalizeWidth(width) {
24
+ return typeof width === "number" ? `${width}px` : width;
25
+ }
26
+ export function buildOverlayStyle(backdrop) {
27
+ const blurPx = backdrop?.blurPx ?? DEFAULT_BLUR_PX;
28
+ const dimPercent = backdrop?.dimPercent ?? DEFAULT_DIM_PERCENT;
29
+ return {
30
+ backgroundColor: `rgb(0 0 0 / ${dimPercent}%)`,
31
+ ...(blurPx > 0 ? { backdropFilter: `blur(${blurPx}px)` } : {}),
32
+ };
33
+ }
34
+ function isHorizontalSide(side) {
35
+ return side === "left" || side === "right";
36
+ }
37
+ function customWidthStyleOf(input) {
38
+ const { side, narrow, canResize, width } = input;
39
+ return !canResize && !narrow && isHorizontalSide(side) && width !== undefined
40
+ ? { width: normalizeWidth(width), maxWidth: MAX_VIEWPORT_WIDTH }
41
+ : undefined;
42
+ }
43
+ // Inline style because tailwind-merge doesn't dedupe `top-*`/`bottom-*` against the base `inset-y-0`;
44
+ // style wins over class regardless of generated CSS order.
45
+ function verticalOffsetStyleOf(input) {
46
+ const { side, narrow, variant, belowHeader } = input;
47
+ return belowHeader && variant === "flush" && !narrow && isHorizontalSide(side)
48
+ ? { top: "var(--shell-header-height)", bottom: 0 }
49
+ : undefined;
50
+ }
51
+ export function buildSheetStyle(input) {
52
+ const { narrow, canResize, effectiveWidthPx } = input;
53
+ const verticalOffsetStyle = verticalOffsetStyleOf(input);
54
+ const widthStyle = canResize && !narrow
55
+ ? { width: effectiveWidthPx, maxWidth: "none" }
56
+ : customWidthStyleOf(input);
57
+ return widthStyle === undefined && verticalOffsetStyle === undefined
58
+ ? undefined
59
+ : { ...widthStyle, ...verticalOffsetStyle };
60
+ }
@@ -1,83 +1,20 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
- import { Maximize2Icon, Minimize2Icon } from "lucide-react";
3
2
  import { useEffect, useState } from "react";
4
3
  import { useTranslation } from "../../index.js";
5
- import { clamp } from "../lib/clamp.js";
6
4
  import { cn } from "../lib/cn.js";
7
5
  import { useIsNarrowViewport } from "../primitives/use-narrow-viewport.js";
8
6
  import { Sheet, SheetDescription, SheetFooter, SheetHeader, SheetTitle } from "../ui/sheet.js";
7
+ import { sidePanelClass } from "./drawer-panel-class.js";
8
+ import { renderMaximizeButton, renderResizeHandle } from "./drawer-resize-controls.js";
9
+ import { buildOverlayStyle, buildSheetStyle } from "./drawer-sizing.js";
9
10
  import { DrawerSheetContent } from "./sheet-parts.js";
10
- import { arrowKeyDelta, usePointerDrag } from "./use-pointer-drag.js";
11
+ import { useDrawerResizeHandle, useDrawerResizeState } from "./use-drawer-resize.js";
11
12
  // Mirrors primitives/index.tsx's cardFooter + cardFooterBorder (row layout,
12
13
  // end-justified actions, top border, panel surface instead of the card
13
14
  // footer's bg-muted/30) — those two consts aren't exported, so the
14
15
  // equivalent classes are inlined here rather than growing the vendored
15
16
  // SheetFooter's own default className with panel-specific styling.
16
17
  const DRAWER_FOOTER_CLASS = "flex-row flex-wrap items-center justify-end border-t bg-background px-[var(--card-padding)] py-4";
17
- const MIN_WIDTH_PX = 320;
18
- const MAX_WIDTH_PX = 1000;
19
- // Must match the static `max(600px,37.5vw)` in floatingSideClass — Tailwind's
20
- // JIT can't read these at runtime, so the two are kept in sync by hand.
21
- // 600px (vs. the old 520px floor) gives a two-column field row (e.g.
22
- // street/number, zip/city) room to breathe, while staying well under half
23
- // of a 1280px viewport so a narrow window isn't dominated by the drawer.
24
- // 37.5vw (vs. the old 25vw) keeps that same 600px on a 1280–1600px window
25
- // (25vw was still under the floor there, making the ratio dead weight up
26
- // to 2400px) but actually grows past the floor by ~1600px, so a 1920px
27
- // window — where the two-column row is cramped just as often — lands
28
- // around 720px instead of being stuck at 600px alongside a 1280px window.
29
- const DEFAULT_WIDTH_MIN_PX = 600;
30
- const DEFAULT_WIDTH_VIEWPORT_RATIO = 0.375;
31
- const DEFAULT_BLUR_PX = 0;
32
- const DEFAULT_DIM_PERCENT = 20;
33
- function defaultWidthFromViewport() {
34
- return typeof window === "undefined"
35
- ? DEFAULT_WIDTH_MIN_PX
36
- : Math.max(DEFAULT_WIDTH_MIN_PX, Math.round(window.innerWidth * DEFAULT_WIDTH_VIEWPORT_RATIO));
37
- }
38
- function normalizeWidth(width) {
39
- return typeof width === "number" ? `${width}px` : width;
40
- }
41
- // Must match the static `max(600px,37.5vw)` in sidePanelClass — Tailwind's
42
- // JIT can't read DEFAULT_WIDTH_MIN_PX/DEFAULT_WIDTH_VIEWPORT_RATIO at
43
- // runtime, so the two are kept in sync by hand.
44
- // Same cap as WIDTH_CLASS: an explicit `width` must not outgrow the viewport.
45
- const MAX_VIEWPORT_WIDTH = "85vw";
46
- const WIDTH_CLASS = "w-[max(600px,37.5vw)] max-w-[85vw] sm:max-w-[max(600px,37.5vw)]";
47
- // "floating": 32px margin + 32px radius so the panel reads as detached from
48
- // the viewport edge, unlike the sheet primitive's flush-edge default.
49
- // "flush" docks it back to that flush-edge default instead, borrowing the
50
- // vendored Sheet's per-side border (only the edge facing the app content).
51
- // Below the narrow-viewport breakpoint neither treatment fits — the JS hook
52
- // decides, not a `sm:` Tailwind prefix, since the resize width below already
53
- // comes from JS and two decision sources is the bug this avoids.
54
- function sidePanelClass(side, narrow, variant, belowHeader) {
55
- if (narrow)
56
- return "inset-0 h-full w-full max-w-none rounded-none border-0 overflow-hidden";
57
- if (variant === "flush") {
58
- switch (side) {
59
- // h-full would pin height to 100vh despite the shifted top; h-auto lets inset-y-0 size it.
60
- case "left":
61
- return `inset-y-0 left-0 ${belowHeader ? "h-auto" : "h-full"} ${WIDTH_CLASS} border-r shadow-2xl overflow-hidden`;
62
- case "top":
63
- return `inset-x-0 ${belowHeader ? "top-(--shell-header-height)" : "top-0"} h-auto max-h-[80vh] border-b shadow-2xl overflow-hidden`;
64
- case "bottom":
65
- return "inset-x-0 bottom-0 h-auto max-h-[80vh] border-t shadow-2xl overflow-hidden";
66
- default:
67
- return `inset-y-0 right-0 ${belowHeader ? "h-auto" : "h-full"} ${WIDTH_CLASS} border-l shadow-2xl overflow-hidden`;
68
- }
69
- }
70
- switch (side) {
71
- case "left":
72
- return `inset-y-8 left-8 h-auto ${WIDTH_CLASS} rounded-[2rem] border shadow-2xl overflow-hidden`;
73
- case "top":
74
- return "inset-x-8 top-8 h-auto max-h-[80vh] rounded-[2rem] border shadow-2xl overflow-hidden";
75
- case "bottom":
76
- return "inset-x-8 bottom-8 h-auto max-h-[80vh] rounded-[2rem] border shadow-2xl overflow-hidden";
77
- default:
78
- return `inset-y-8 right-8 h-auto ${WIDTH_CLASS} rounded-[2rem] border shadow-2xl overflow-hidden`;
79
- }
80
- }
81
18
  const DRAWER_INSET_PROPERTY = {
82
19
  left: "--kumiko-drawer-inset-left",
83
20
  right: "--kumiko-drawer-inset-right",
@@ -118,60 +55,24 @@ export function Drawer({ open, onOpenChange, side = "right", title, description,
118
55
  const t = useTranslation();
119
56
  const narrow = useIsNarrowViewport();
120
57
  const canResize = resize !== undefined && (side === "left" || side === "right");
121
- const customWidthStyle = !canResize && !narrow && (side === "left" || side === "right") && width !== undefined
122
- ? { width: normalizeWidth(width), maxWidth: MAX_VIEWPORT_WIDTH }
123
- : undefined;
124
- const minWidthPx = resize?.minWidthPx ?? MIN_WIDTH_PX;
125
- const maxWidthPx = resize?.maxWidthPx ?? MAX_WIDTH_PX;
126
- const effectiveMaxWidthPx = () => typeof window === "undefined"
127
- ? maxWidthPx
128
- : Math.min(maxWidthPx, Math.round(window.innerWidth * 0.9));
129
- const [resizedWidthPx, setResizedWidthPx] = useState(() => clamp(resize?.defaultWidthPx ?? defaultWidthFromViewport(), minWidthPx, effectiveMaxWidthPx()));
130
- const [maximized, setMaximized] = useState(false);
58
+ const resizeState = useDrawerResizeState(resize);
131
59
  const [sheet, setSheet] = useState(null);
132
60
  usePublishDrawerInset(sheet, side, open && !modal && !narrow);
133
- const blurPx = backdrop?.blurPx ?? DEFAULT_BLUR_PX;
134
- const dimPercent = backdrop?.dimPercent ?? DEFAULT_DIM_PERCENT;
135
- const overlayStyle = {
136
- backgroundColor: `rgb(0 0 0 / ${dimPercent}%)`,
137
- ...(blurPx > 0 ? { backdropFilter: `blur(${blurPx}px)` } : {}),
138
- };
139
- const effectiveWidthPx = maximized ? effectiveMaxWidthPx() : resizedWidthPx;
140
- // Inline style because tailwind-merge doesn't dedupe `top-*`/`bottom-*` against the base `inset-y-0`;
141
- // style wins over class regardless of generated CSS order.
142
- const verticalOffsetStyle = belowHeader && variant === "flush" && !narrow && (side === "left" || side === "right")
143
- ? { top: "var(--shell-header-height)", bottom: 0 }
144
- : undefined;
145
- const widthStyle = canResize && !narrow ? { width: effectiveWidthPx, maxWidth: "none" } : customWidthStyle;
146
- const sheetStyle = widthStyle === undefined && verticalOffsetStyle === undefined
147
- ? undefined
148
- : { ...widthStyle, ...verticalOffsetStyle };
149
- const handleDrag = usePointerDrag({
150
- onStart: () => {
151
- setMaximized(false);
152
- return { startWidth: effectiveWidthPx };
153
- },
154
- onMove: ({ startWidth }, { dx }) => {
155
- const signedDelta = side === "right" ? -dx : dx;
156
- setResizedWidthPx(clamp(startWidth + signedDelta, minWidthPx, effectiveMaxWidthPx()));
157
- },
61
+ const overlayStyle = buildOverlayStyle(backdrop);
62
+ const sheetStyle = buildSheetStyle({
63
+ side,
64
+ narrow,
65
+ canResize,
66
+ width,
67
+ effectiveWidthPx: resizeState.effectiveWidthPx,
68
+ variant,
69
+ belowHeader,
158
70
  });
159
- const onHandleKeyDown = (event) => {
160
- const keyDelta = arrowKeyDelta(event);
161
- if (keyDelta === undefined || keyDelta.dx === 0)
162
- return;
163
- event.preventDefault();
164
- setMaximized(false);
165
- const delta = side === "right" ? -keyDelta.dx : keyDelta.dx;
166
- // Seed from the currently visible width, not the stale `resizedWidthPx`
167
- // state — while maximized, it still holds the pre-maximize value, so a
168
- // key press would otherwise jump the drawer back to that old size
169
- // instead of resizing relative to what's on screen.
170
- setResizedWidthPx((current) => clamp((maximized ? effectiveWidthPx : current) + delta, minWidthPx, effectiveMaxWidthPx()));
171
- };
71
+ const handle = useDrawerResizeHandle(side, resizeState);
72
+ const resizeControls = { t, side, resize: resizeState, handle };
172
73
  return (_jsx(Sheet, { open: open, onOpenChange: onOpenChange, modal: modal, children: _jsxs(DrawerSheetContent, { ref: setSheet, side: side, "data-testid": testId, overlayStyle: overlayStyle,
173
74
  // Radix already drops the overlay and the focus trap for a
174
75
  // non-modal Root; only the dismiss-on-outside-interaction is left
175
76
  // to suppress, so a click into the page behind keeps the panel open.
176
- onInteractOutside: modal ? undefined : (event) => event.preventDefault(), showCloseButton: showCloseButton, closeLabel: t("kumiko.dialog.close"), className: cn(sidePanelClass(side, narrow, variant, belowHeader), fillBody && "gap-0", !narrow && panelClassName), style: sheetStyle, children: [canResize && !narrow && (_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" })) })), (title !== undefined || description !== undefined) && (_jsxs(SheetHeader, { children: [title !== undefined && _jsx(SheetTitle, { children: title }), description !== undefined && _jsx(SheetDescription, { children: description })] })), _jsx("div", { className: fillBody ? "flex min-h-0 flex-1 flex-col" : "flex-1 overflow-y-auto px-4", children: children }), footer !== undefined && (_jsx(SheetFooter, { className: DRAWER_FOOTER_CLASS, children: footer })), canResize && !narrow && (_jsx("div", { role: "separator", "aria-orientation": "vertical", "aria-label": t("kumiko.widget.drawer.resize"), "aria-valuenow": effectiveWidthPx, "aria-valuemin": minWidthPx, "aria-valuemax": effectiveMaxWidthPx(), tabIndex: 0, ...handleDrag, onKeyDown: 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") }))] }) }));
77
+ onInteractOutside: modal ? undefined : (event) => event.preventDefault(), showCloseButton: showCloseButton, closeLabel: t("kumiko.dialog.close"), className: cn(sidePanelClass(side, narrow, variant, belowHeader), fillBody && "gap-0", !narrow && panelClassName), style: sheetStyle, children: [canResize && !narrow && renderMaximizeButton(resizeControls), (title !== undefined || description !== undefined) && (_jsxs(SheetHeader, { children: [title !== undefined && _jsx(SheetTitle, { children: title }), description !== undefined && _jsx(SheetDescription, { children: description })] })), _jsx("div", { className: fillBody ? "flex min-h-0 flex-1 flex-col" : "flex-1 overflow-y-auto px-4", children: children }), footer !== undefined && (_jsx(SheetFooter, { className: DRAWER_FOOTER_CLASS, children: footer })), canResize && !narrow && renderResizeHandle(resizeControls)] }) }));
177
78
  }
@@ -0,0 +1,13 @@
1
+ import { type ReactNode } from "react";
2
+ import { type ChartWindow } from "./chart-window.ts";
3
+ export declare function StackedAreaBrush({ times, totals, window, labels, onChange, testId, }: {
4
+ readonly times: readonly number[];
5
+ readonly totals: readonly number[];
6
+ readonly window: ChartWindow;
7
+ readonly labels: {
8
+ readonly start: string;
9
+ readonly end: string;
10
+ };
11
+ readonly onChange: (window: ChartWindow) => void;
12
+ readonly testId: string;
13
+ }): ReactNode;
@@ -0,0 +1,91 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { useRef } from "react";
3
+ import { percent } from "./chart-data.js";
4
+ import { nearestIndex, snapBrushWindow, snapMovedBrushWindow, } from "./chart-window.js";
5
+ const BRUSH_OVERVIEW_WIDTH = 100;
6
+ const BRUSH_OVERVIEW_HEIGHT = 24;
7
+ function brushKeyTarget(key, index, min, max) {
8
+ if (key === "ArrowLeft")
9
+ return index - 1;
10
+ if (key === "ArrowRight")
11
+ return index + 1;
12
+ if (key === "Home")
13
+ return min;
14
+ return key === "End" ? max : undefined;
15
+ }
16
+ export function StackedAreaBrush({ times, totals, window, labels, onChange, testId, }) {
17
+ const trackRef = useRef(null);
18
+ const dragRef = useRef(null);
19
+ const firstMs = times[0] ?? 0;
20
+ const lastMs = times[times.length - 1] ?? 1;
21
+ const span = Math.max(1, lastMs - firstMs);
22
+ const fractionOf = (atMs) => Math.max(0, Math.min(1, (atMs - firstMs) / span));
23
+ const startIndex = nearestIndex(times, window.startMs);
24
+ const endIndex = nearestIndex(times, window.endMs);
25
+ const lastIndex = times.length - 1;
26
+ const max = Math.max(...totals, 1);
27
+ 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)}`);
28
+ const overviewPath = `M 0 ${BRUSH_OVERVIEW_HEIGHT} L ${overviewPoints.join(" L ")} L ${BRUSH_OVERVIEW_WIDTH} ${BRUSH_OVERVIEW_HEIGHT} Z`;
29
+ const commit = (startMs, endMs, anchor) => onChange(snapBrushWindow(times, startMs, endMs, anchor));
30
+ const beginDrag = (mode) => (e) => {
31
+ e.currentTarget.setPointerCapture(e.pointerId);
32
+ dragRef.current = {
33
+ mode,
34
+ originX: e.clientX,
35
+ originStartMs: window.startMs,
36
+ originEndMs: window.endMs,
37
+ };
38
+ };
39
+ const moveDrag = (e) => {
40
+ const drag = dragRef.current;
41
+ const trackWidth = trackRef.current?.getBoundingClientRect().width ?? 0;
42
+ if (drag === null || trackWidth <= 0)
43
+ return;
44
+ const deltaMs = ((e.clientX - drag.originX) / trackWidth) * span;
45
+ if (drag.mode === "move") {
46
+ const shift = Math.max(firstMs - drag.originStartMs, Math.min(lastMs - drag.originEndMs, deltaMs));
47
+ onChange(snapMovedBrushWindow(times, drag.originStartMs, drag.originEndMs, shift));
48
+ }
49
+ else if (drag.mode === "start") {
50
+ commit(drag.originStartMs + deltaMs, drag.originEndMs, "start");
51
+ }
52
+ else {
53
+ commit(drag.originStartMs, drag.originEndMs + deltaMs, "end");
54
+ }
55
+ };
56
+ const endDrag = () => {
57
+ dragRef.current = null;
58
+ };
59
+ const dragHandlers = (mode) => ({
60
+ onPointerDown: beginDrag(mode),
61
+ onPointerMove: moveDrag,
62
+ onPointerUp: endDrag,
63
+ onPointerCancel: endDrag,
64
+ });
65
+ const handleKeyDown = (handle) => (e) => {
66
+ const isStart = handle === "start";
67
+ const index = isStart ? startIndex : endIndex;
68
+ const min = isStart ? 0 : startIndex + 1;
69
+ const maxIndex = isStart ? endIndex - 1 : lastIndex;
70
+ const target = brushKeyTarget(e.key, index, min, maxIndex);
71
+ if (target === undefined)
72
+ return;
73
+ e.preventDefault();
74
+ const next = Math.max(min, Math.min(maxIndex, target));
75
+ if (next === index)
76
+ return;
77
+ const nextStart = times[isStart ? next : startIndex];
78
+ const nextEnd = times[isStart ? endIndex : next];
79
+ if (nextStart !== undefined && nextEnd !== undefined) {
80
+ onChange({ startMs: nextStart, endMs: nextEnd });
81
+ }
82
+ };
83
+ const startFraction = fractionOf(window.startMs);
84
+ const endFraction = fractionOf(window.endMs);
85
+ 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";
86
+ 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" }) }));
87
+ 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: {
88
+ left: percent(startFraction),
89
+ width: percent(Math.max(0, endFraction - startFraction)),
90
+ }, ...dragHandlers("move") }), renderHandle("start"), renderHandle("end")] }));
91
+ }
@@ -0,0 +1,6 @@
1
+ import { type ReactNode } from "react";
2
+ import { type StackedAreaChartProps } from "./stacked-area-props.ts";
3
+ export type { StackedAreaChartProps } from "./stacked-area-props.ts";
4
+ /** Stacked bands over time. Right of `todayMs` is the forecast: lighter
5
+ * fill plus a vertical "today" line. */
6
+ export declare function StackedAreaChart(rawProps: StackedAreaChartProps): ReactNode;
@@ -0,0 +1,45 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { useEffect, useId, useRef } from "react";
3
+ import { renderBrushBar, renderLegend, renderMarkerLegend, renderPlotRow, renderRangeSwitch, } from "./stacked-area-chrome.js";
4
+ import { bucketTotals, buildStackedAreaGeometry } from "./stacked-area-geometry.js";
5
+ import { resolveStackedAreaProps } from "./stacked-area-props.js";
6
+ import { useStackedAreaTooltip } from "./use-stacked-area-tooltip.js";
7
+ import { useStackedAreaWindow } from "./use-stacked-area-window.js";
8
+ /** Stacked bands over time. Right of `todayMs` is the forecast: lighter
9
+ * fill plus a vertical "today" line. */
10
+ export function StackedAreaChart(rawProps) {
11
+ const props = resolveStackedAreaProps(rawProps);
12
+ const { scrollable, emptyContent, testId, tones, colors } = props;
13
+ const clipId = useId();
14
+ const scrollRef = useRef(null);
15
+ const win = useStackedAreaWindow(props);
16
+ const tooltip = useStackedAreaTooltip(win.times);
17
+ const { times, todayFraction } = win;
18
+ const bucketCount = times.length;
19
+ useEffect(() => {
20
+ const el = scrollRef.current;
21
+ if (!scrollable || el === null || bucketCount === 0)
22
+ return;
23
+ el.scrollLeft =
24
+ todayFraction === undefined
25
+ ? el.scrollWidth
26
+ : Math.max(0, todayFraction * el.scrollWidth - el.clientWidth / 2);
27
+ }, [scrollable, todayFraction, bucketCount]);
28
+ if (times.length === 0)
29
+ return _jsx("div", { "data-testid": testId, children: emptyContent });
30
+ const totals = bucketTotals(times, win.series);
31
+ if (times.length === 1 && totals[0] === 0) {
32
+ return _jsx("div", { "data-testid": testId, children: emptyContent });
33
+ }
34
+ const geometry = buildStackedAreaGeometry({
35
+ times,
36
+ totals,
37
+ series: win.series,
38
+ lines: win.lines,
39
+ markers: win.markers,
40
+ fractionOf: win.fractionOf,
41
+ palette: { tones, colors },
42
+ });
43
+ const model = { props, clipId, win, tooltip, geometry };
44
+ return (_jsxs("div", { "data-testid": testId, className: "@container flex flex-col gap-3", children: [renderRangeSwitch(model), renderPlotRow(model, scrollRef), renderBrushBar(model), renderLegend(model), renderMarkerLegend(model)] }));
45
+ }
@@ -0,0 +1,7 @@
1
+ import type { ReactNode, RefObject } from "react";
2
+ import type { StackedAreaModel } from "./stacked-area-model.ts";
3
+ export declare function renderRangeSwitch(m: StackedAreaModel): ReactNode;
4
+ export declare function renderBrushBar(m: StackedAreaModel): ReactNode;
5
+ export declare function renderPlotRow(m: StackedAreaModel, scrollRef: RefObject<HTMLDivElement | null>): ReactNode;
6
+ export declare function renderLegend(m: StackedAreaModel): ReactNode;
7
+ export declare function renderMarkerLegend(m: StackedAreaModel): ReactNode;
@@ -0,0 +1,77 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { cn } from "../lib/cn.js";
3
+ import { chartColor, seriesTotal, valueAt } from "./chart-data.js";
4
+ import { MarkerLegend, markerLegendItems } from "./chart-markers.js";
5
+ import { ChartLegend, YTicks } from "./chart-parts.js";
6
+ import { ModeSwitch } from "./mode-switch.js";
7
+ import { StackedAreaBrush } from "./stacked-area-brush.js";
8
+ import { STACKED_AREA_PLOT_HEIGHT_CLASS } from "./stacked-area-geometry.js";
9
+ import { renderStackedAreaPlot } from "./stacked-area-plot.js";
10
+ const SCROLLABLE_BUCKET_WIDTH_PX = 48;
11
+ export function renderRangeSwitch(m) {
12
+ const { ranges, ariaLabel, testId } = m.props;
13
+ const { isControlled, selection, changeSelection } = m.win;
14
+ if (isControlled || ranges === undefined)
15
+ return undefined;
16
+ return (_jsx("div", { className: "flex justify-end", children: _jsx(ModeSwitch, { value: selection?.kind === "range" ? selection.value : null, options: ranges.options.map((option) => ({
17
+ value: option.value,
18
+ label: option.label,
19
+ })), onChange: (value) => changeSelection({ kind: "range", value }), ariaLabel: ariaLabel, variant: "pill", testId: testId !== undefined ? `${testId}-range` : undefined }) }));
20
+ }
21
+ export function renderBrushBar(m) {
22
+ const { brush, brushLabels, series: allSeries, testId } = m.props;
23
+ const { allTimes, visibleWindow, changeSelection } = m.win;
24
+ if (!brush || allTimes.length < 2)
25
+ return undefined;
26
+ return (_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` }));
27
+ }
28
+ function renderPlotArea(m, scrollRef) {
29
+ const plot = renderStackedAreaPlot(m);
30
+ if (!m.props.scrollable)
31
+ return _jsx("div", { className: "relative grow", children: plot });
32
+ return (_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: {
33
+ minWidth: "100%",
34
+ width: `${m.win.times.length * SCROLLABLE_BUCKET_WIDTH_PX}px`,
35
+ }, children: plot }) }));
36
+ }
37
+ export function renderPlotRow(m, scrollRef) {
38
+ const { scrollable, formatAxisValue } = m.props;
39
+ return (_jsxs("div", { className: "flex gap-2", children: [_jsx(YTicks, { values: m.geometry.yTickValues, formatValue: formatAxisValue, className: cn("shrink-0 pb-0", STACKED_AREA_PLOT_HEIGHT_CLASS, scrollable && "mt-4") }), renderPlotArea(m, scrollRef)] }));
40
+ }
41
+ function buildLegendItems(m) {
42
+ const { tones, colors, showLegendTotals, markerLegend } = m.props;
43
+ const { series, markers } = m.win;
44
+ return [
45
+ ...series.map((s, i) => ({
46
+ key: s.key,
47
+ label: s.label,
48
+ color: chartColor(s.key, i, tones, colors),
49
+ ...(showLegendTotals && { total: seriesTotal(s) }),
50
+ })),
51
+ ...m.geometry.linePaths.map((line) => ({
52
+ key: `line:${line.key}`,
53
+ testIdKey: `line-${line.key}`,
54
+ label: line.label,
55
+ color: line.color,
56
+ swatch: line.dashed ? "dashed-line" : "line",
57
+ })),
58
+ ...(markerLegend === "legend"
59
+ ? markerLegendItems(markers ?? []).map((item) => ({
60
+ ...item,
61
+ swatch: "dashed-line",
62
+ }))
63
+ : []),
64
+ ];
65
+ }
66
+ export function renderLegend(m) {
67
+ return _jsx(ChartLegend, { formatValue: m.props.formatValue, items: buildLegendItems(m) });
68
+ }
69
+ export function renderMarkerLegend(m) {
70
+ const { markerLegend, markerLabels } = m.props;
71
+ const { markers } = m.win;
72
+ if (markers === undefined || markers.length === 0)
73
+ return undefined;
74
+ if (markerLegend !== "list" && !markerLabels)
75
+ return undefined;
76
+ return (_jsx(MarkerLegend, { markers: markers, formatMarkerTime: m.win.formatMarkerTimeInWindow, ...(markerLabels && { className: "@2xl:hidden" }) }));
77
+ }
@@ -0,0 +1,54 @@
1
+ import { type ChartLine, type ChartMarker, type ChartSeries, type ColorMap, type ToneMap } from "./chart-data.ts";
2
+ export declare const STACKED_AREA_PLOT_HEIGHT_CLASS = "h-56 @2xl:h-64";
3
+ export declare const PLOT_WIDTH = 300;
4
+ export declare const PLOT_HEIGHT = 120;
5
+ export declare const AXIS_TIERS: readonly [{
6
+ readonly maxLabels: 4;
7
+ readonly visibilityClass: "@xl:hidden";
8
+ }, {
9
+ readonly maxLabels: 7;
10
+ readonly visibilityClass: "hidden @xl:block @4xl:hidden";
11
+ }, {
12
+ readonly maxLabels: 12;
13
+ readonly visibilityClass: "hidden @4xl:block";
14
+ }];
15
+ export declare function bucketTotals(times: readonly number[], series: readonly ChartSeries[]): readonly number[];
16
+ type Colors = {
17
+ readonly tones: ToneMap | undefined;
18
+ readonly colors: ColorMap | undefined;
19
+ };
20
+ export type StackedBand = {
21
+ readonly key: string;
22
+ readonly color: string;
23
+ readonly edge: string;
24
+ readonly d: string;
25
+ readonly dot: number | undefined;
26
+ };
27
+ export type StackedLinePath = {
28
+ readonly key: string;
29
+ readonly label: string;
30
+ readonly dashed: boolean;
31
+ readonly color: string;
32
+ readonly d: string;
33
+ };
34
+ export declare function buildStackedAreaGeometry(input: {
35
+ readonly times: readonly number[];
36
+ readonly totals: readonly number[];
37
+ readonly series: readonly ChartSeries[];
38
+ readonly lines: readonly ChartLine[];
39
+ readonly markers: readonly ChartMarker[] | undefined;
40
+ readonly fractionOf: (atMs: number) => number;
41
+ readonly palette: Colors;
42
+ }): {
43
+ yTickValues: readonly number[];
44
+ yOf: (value: number) => number;
45
+ bands: readonly StackedBand[];
46
+ linePaths: readonly StackedLinePath[];
47
+ coloredMarkers: (ChartMarker & {
48
+ readonly key: string;
49
+ } & {
50
+ readonly color: string;
51
+ })[];
52
+ };
53
+ export type StackedAreaGeometry = ReturnType<typeof buildStackedAreaGeometry>;
54
+ export {};
@@ -0,0 +1,83 @@
1
+ import { chartColor, clampDotY, valueAt, } from "./chart-data.js";
2
+ import { sortMarkers } from "./chart-markers.js";
3
+ import { evenYTickValues } from "./chart-parts.js";
4
+ export const STACKED_AREA_PLOT_HEIGHT_CLASS = "h-56 @2xl:h-64";
5
+ export const PLOT_WIDTH = 300;
6
+ export const PLOT_HEIGHT = 120;
7
+ // Container-query density tiers for the x axis: label budget per tier.
8
+ export const AXIS_TIERS = [
9
+ { maxLabels: 4, visibilityClass: "@xl:hidden" },
10
+ { maxLabels: 7, visibilityClass: "hidden @xl:block @4xl:hidden" },
11
+ { maxLabels: 12, visibilityClass: "hidden @4xl:block" },
12
+ ];
13
+ const FINE_NICE_STEPS = [1, 1.2, 1.5, 2, 2.5, 3, 4, 5, 6, 8, 10];
14
+ // Finer than niceCeil so a 1.1 M peak tops out at 1.2 M instead of 2 M.
15
+ function fineNiceCeil(max) {
16
+ if (max <= 0)
17
+ return 1;
18
+ const magnitude = 10 ** Math.floor(Math.log10(max));
19
+ const normalized = max / magnitude;
20
+ const step = FINE_NICE_STEPS.find((candidate) => normalized <= candidate) ?? 10;
21
+ return step * magnitude;
22
+ }
23
+ function linePathSegments(line, xOf, yOf) {
24
+ const commands = [];
25
+ let penDown = false;
26
+ for (const point of [...line.points].sort((a, b) => a.atMs - b.atMs)) {
27
+ if (point.value === null) {
28
+ penDown = false;
29
+ continue;
30
+ }
31
+ commands.push(`${penDown ? "L" : "M"} ${xOf(point.atMs).toFixed(1)} ${yOf(point.value).toFixed(1)}`);
32
+ penDown = true;
33
+ }
34
+ return commands.join(" ");
35
+ }
36
+ export function bucketTotals(times, series) {
37
+ return times.map((atMs) => series.reduce((sum, s) => sum + valueAt(s, atMs), 0));
38
+ }
39
+ function buildBands(input) {
40
+ const { times, series, yOf, fractionOf, palette } = input;
41
+ const isSingleBucket = times.length === 1;
42
+ const lower = times.map(() => 0);
43
+ return series.map((s, si) => {
44
+ const upper = times.map((atMs, ti) => (lower[ti] ?? 0) + valueAt(s, atMs));
45
+ const top = times.map((atMs, ti) => `${(fractionOf(atMs) * PLOT_WIDTH).toFixed(1)} ${yOf(upper[ti] ?? 0).toFixed(1)}`);
46
+ const bottom = times
47
+ .map((atMs, ti) => `${(fractionOf(atMs) * PLOT_WIDTH).toFixed(1)} ${yOf(lower[ti] ?? 0).toFixed(1)}`)
48
+ .reverse();
49
+ const bucketTop = upper[0] ?? 0;
50
+ const bucketHeight = bucketTop - (lower[0] ?? 0);
51
+ for (let ti = 0; ti < times.length; ti++)
52
+ lower[ti] = upper[ti] ?? 0;
53
+ return {
54
+ key: s.key,
55
+ color: chartColor(s.key, si, palette.tones, palette.colors),
56
+ edge: `M ${top.join(" L ")}`,
57
+ d: `M ${top.join(" L ")} L ${bottom.join(" L ")} Z`,
58
+ dot: isSingleBucket && bucketHeight > 0 ? clampDotY(yOf(bucketTop), PLOT_HEIGHT) : undefined,
59
+ };
60
+ });
61
+ }
62
+ export function buildStackedAreaGeometry(input) {
63
+ const { times, totals, series, lines, markers, fractionOf, palette } = input;
64
+ const lineValues = lines.flatMap((line) => line.points.flatMap((p) => (p.value === null ? [] : [p.value])));
65
+ // Doubling keeps a lone bucket away from the top edge of the y-scale.
66
+ const max = fineNiceCeil(Math.max(...totals, ...lineValues) * (times.length === 1 ? 2 : 1));
67
+ const yOf = (value) => PLOT_HEIGHT - (value / max) * PLOT_HEIGHT;
68
+ const lineColorOffset = series.length;
69
+ const linePaths = lines.map((line, li) => ({
70
+ key: line.key,
71
+ label: line.label,
72
+ dashed: line.dashed === true,
73
+ color: chartColor(line.key, lineColorOffset + li, palette.tones, palette.colors),
74
+ d: linePathSegments(line, (atMs) => fractionOf(atMs) * PLOT_WIDTH, yOf),
75
+ }));
76
+ return {
77
+ yTickValues: evenYTickValues(max),
78
+ yOf,
79
+ bands: buildBands({ times, series, yOf, fractionOf, palette }),
80
+ linePaths,
81
+ coloredMarkers: sortMarkers(markers ?? []).filter((marker) => marker.color !== undefined),
82
+ };
83
+ }
@@ -0,0 +1,11 @@
1
+ import type { StackedAreaGeometry } from "./stacked-area-geometry.ts";
2
+ import type { ResolvedStackedAreaProps } from "./stacked-area-props.ts";
3
+ import type { useStackedAreaTooltip } from "./use-stacked-area-tooltip.ts";
4
+ import type { useStackedAreaWindow } from "./use-stacked-area-window.ts";
5
+ export type StackedAreaModel = {
6
+ readonly props: ResolvedStackedAreaProps;
7
+ readonly clipId: string;
8
+ readonly win: ReturnType<typeof useStackedAreaWindow>;
9
+ readonly tooltip: ReturnType<typeof useStackedAreaTooltip>;
10
+ readonly geometry: StackedAreaGeometry;
11
+ };
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,3 @@
1
+ import type { ReactNode } from "react";
2
+ import type { StackedAreaModel } from "./stacked-area-model.ts";
3
+ export declare function renderStackedAreaPlot(m: StackedAreaModel): ReactNode;