@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.
- package/dist/app/projection-detail-body.js +59 -394
- package/dist/app/projection-detail-header-model.d.ts +11 -0
- package/dist/app/projection-detail-header-model.js +33 -0
- package/dist/app/projection-detail-header-region.d.ts +28 -0
- package/dist/app/projection-detail-header-region.js +81 -0
- package/dist/app/projection-detail-header.d.ts +29 -0
- package/dist/app/projection-detail-header.js +84 -0
- package/dist/app/projection-detail-navigation.d.ts +13 -0
- package/dist/app/projection-detail-navigation.js +91 -0
- package/dist/app/projection-detail-states.d.ts +22 -0
- package/dist/app/projection-detail-states.js +26 -0
- package/dist/app/projection-drawer-host.d.ts +12 -0
- package/dist/app/projection-drawer-host.js +10 -0
- package/dist/app/projection-list-body.d.ts +1 -1
- package/dist/app/projection-list-body.js +69 -189
- package/dist/app/projection-list-render-props.d.ts +56 -0
- package/dist/app/projection-list-render-props.js +47 -0
- package/dist/app/projection-list-render.d.ts +26 -0
- package/dist/app/projection-list-render.js +20 -0
- package/dist/app/projection-list-row-click.d.ts +12 -0
- package/dist/app/projection-list-row-click.js +14 -0
- package/dist/app/use-projection-detail-actions.d.ts +35 -0
- package/dist/app/use-projection-detail-actions.js +156 -0
- package/dist/app/use-projection-detail-data.d.ts +19 -0
- package/dist/app/use-projection-detail-data.js +47 -0
- package/dist/app/use-projection-detail-layout.d.ts +17 -0
- package/dist/app/use-projection-detail-layout.js +23 -0
- package/dist/app/use-projection-list-actions.d.ts +31 -0
- package/dist/app/use-projection-list-actions.js +73 -0
- package/dist/app/use-projection-list-data.d.ts +20 -0
- package/dist/app/use-projection-list-data.js +26 -0
- package/dist/app/use-projection-list-definitions.d.ts +11 -0
- package/dist/app/use-projection-list-definitions.js +20 -0
- package/dist/app/use-projection-list-query.d.ts +28 -0
- package/dist/app/use-projection-list-query.js +83 -0
- package/dist/components/render-edit-banners.d.ts +12 -0
- package/dist/components/render-edit-banners.js +90 -0
- package/dist/components/render-edit-draft-save-helpers.d.ts +19 -0
- package/dist/components/render-edit-draft-save-helpers.js +42 -0
- package/dist/components/render-edit-field-grid.d.ts +4 -0
- package/dist/components/render-edit-field-grid.js +40 -0
- package/dist/components/render-edit-fields-section.d.ts +4 -0
- package/dist/components/render-edit-fields-section.js +76 -0
- package/dist/components/render-edit-form-actions.d.ts +4 -0
- package/dist/components/render-edit-form-actions.js +66 -0
- package/dist/components/render-edit-header-actions.d.ts +11 -0
- package/dist/components/render-edit-header-actions.js +120 -0
- package/dist/components/render-edit-layout-flags.d.ts +29 -0
- package/dist/components/render-edit-layout-flags.js +106 -0
- package/dist/components/render-edit-layout.d.ts +4 -0
- package/dist/components/render-edit-layout.js +79 -0
- package/dist/components/render-edit-render-model.d.ts +63 -0
- package/dist/components/render-edit-render-model.js +14 -0
- package/dist/components/render-edit-section-kinds.d.ts +4 -0
- package/dist/components/render-edit-section-kinds.js +115 -0
- package/dist/components/render-edit-submit-result.d.ts +8 -0
- package/dist/components/render-edit-submit-result.js +77 -0
- package/dist/components/render-edit-submit.d.ts +7 -0
- package/dist/components/render-edit-submit.js +113 -0
- package/dist/components/render-edit-titles.d.ts +17 -0
- package/dist/components/render-edit-titles.js +99 -0
- package/dist/components/render-edit-wizard-navigation.d.ts +11 -0
- package/dist/components/render-edit-wizard-navigation.js +115 -0
- package/dist/components/render-edit.d.ts +2 -2
- package/dist/components/render-edit.js +63 -1128
- package/dist/components/render-field-choice-inputs.d.ts +7 -0
- package/dist/components/render-field-choice-inputs.js +52 -0
- package/dist/components/render-field-control.d.ts +12 -0
- package/dist/components/render-field-control.js +93 -0
- package/dist/components/render-field-display-text.d.ts +4 -0
- package/dist/components/render-field-display-text.js +48 -0
- package/dist/components/render-field-fill-action.d.ts +13 -0
- package/dist/components/render-field-fill-action.js +63 -0
- package/dist/components/render-field-input-shared.d.ts +39 -0
- package/dist/components/render-field-input-shared.js +20 -0
- package/dist/components/render-field-input.d.ts +3 -0
- package/dist/components/render-field-input.js +34 -0
- package/dist/components/render-field-meta.d.ts +6 -0
- package/dist/components/render-field-meta.js +51 -0
- package/dist/components/render-field-read-only.d.ts +11 -0
- package/dist/components/render-field-read-only.js +75 -0
- package/dist/components/render-field-reference.d.ts +15 -0
- package/dist/components/render-field-reference.js +103 -0
- package/dist/components/render-field-scalar-inputs.d.ts +10 -0
- package/dist/components/render-field-scalar-inputs.js +35 -0
- package/dist/components/render-field-text-inputs.d.ts +3 -0
- package/dist/components/render-field-text-inputs.js +34 -0
- package/dist/components/render-field-values.d.ts +18 -0
- package/dist/components/render-field-values.js +111 -0
- package/dist/components/render-field-write-only.d.ts +9 -0
- package/dist/components/render-field-write-only.js +29 -0
- package/dist/components/render-field.d.ts +2 -2
- package/dist/components/render-field.js +39 -670
- package/dist/components/render-list-header-slot.d.ts +5 -0
- package/dist/components/render-list-header-slot.js +30 -0
- package/dist/components/render-list-reference-bridge.d.ts +3 -0
- package/dist/components/render-list-reference-bridge.js +25 -0
- package/dist/components/render-list-reference-columns.d.ts +25 -0
- package/dist/components/render-list-reference-columns.js +137 -0
- package/dist/components/render-list-table-props.d.ts +52 -0
- package/dist/components/render-list-table-props.js +53 -0
- package/dist/components/render-list-toolbar-model.d.ts +42 -0
- package/dist/components/render-list-toolbar-model.js +82 -0
- package/dist/components/render-list-toolbar.d.ts +38 -0
- package/dist/components/render-list-toolbar.js +40 -0
- package/dist/components/render-list-types.d.ts +113 -0
- package/dist/components/render-list-types.js +1 -0
- package/dist/components/render-list.d.ts +4 -113
- package/dist/components/render-list.js +91 -307
- package/dist/components/use-reference-input-state.d.ts +20 -0
- package/dist/components/use-reference-input-state.js +85 -0
- package/dist/components/use-render-edit-controlled-sync.d.ts +3 -0
- package/dist/components/use-render-edit-controlled-sync.js +26 -0
- package/dist/components/use-render-edit-draft-save.d.ts +29 -0
- package/dist/components/use-render-edit-draft-save.js +141 -0
- package/dist/components/use-render-edit-form.d.ts +10 -0
- package/dist/components/use-render-edit-form.js +101 -0
- package/dist/components/use-render-edit-sections.d.ts +15 -0
- package/dist/components/use-render-edit-sections.js +100 -0
- package/dist/components/use-render-edit-state.d.ts +65 -0
- package/dist/components/use-render-edit-state.js +158 -0
- package/dist/components/use-render-edit-steps.d.ts +13 -0
- package/dist/components/use-render-edit-steps.js +52 -0
- package/dist/components/use-render-list-columns.d.ts +18 -0
- package/dist/components/use-render-list-columns.js +41 -0
- package/dist/components/use-render-list-search.d.ts +4 -0
- package/dist/components/use-render-list-search.js +50 -0
- package/dist/primitives.d.ts +1 -1
- package/dist/web/app/dashboard-body.d.ts +1 -1
- package/dist/web/app/dashboard-body.js +42 -246
- package/dist/web/app/dashboard-header.d.ts +34 -0
- package/dist/web/app/dashboard-header.js +58 -0
- package/dist/web/app/dashboard-screen-controls.d.ts +11 -0
- package/dist/web/app/dashboard-screen-controls.js +42 -0
- package/dist/web/app/dashboard-screen-tiles.d.ts +8 -0
- package/dist/web/app/dashboard-screen-tiles.js +42 -0
- package/dist/web/app/dashboard-visibility.d.ts +42 -0
- package/dist/web/app/dashboard-visibility.js +102 -0
- package/dist/web/app/use-dashboard-screen-state.d.ts +35 -0
- package/dist/web/app/use-dashboard-screen-state.js +69 -0
- package/dist/web/primitives/data-table/actions-column.js +6 -5
- package/dist/web/primitives/data-table/card-body.d.ts +6 -0
- package/dist/web/primitives/data-table/card-body.js +32 -0
- package/dist/web/primitives/data-table/card-row.d.ts +5 -0
- package/dist/web/primitives/data-table/card-row.js +31 -0
- package/dist/web/primitives/data-table/card-view.d.ts +3 -0
- package/dist/web/primitives/data-table/card-view.js +13 -0
- package/dist/web/primitives/data-table/column-resize-handle.js +3 -1
- package/dist/web/primitives/data-table/data-cell-props.d.ts +7 -0
- package/dist/web/primitives/data-table/data-cell-props.js +8 -0
- package/dist/web/primitives/data-table/row-render-parts.d.ts +8 -0
- package/dist/web/primitives/data-table/row-render-parts.js +35 -0
- package/dist/web/primitives/data-table/table-body.d.ts +3 -0
- package/dist/web/primitives/data-table/table-body.js +32 -0
- package/dist/web/primitives/data-table/table-content.d.ts +3 -0
- package/dist/web/primitives/data-table/table-content.js +41 -0
- package/dist/web/primitives/data-table/table-data-cell.d.ts +4 -0
- package/dist/web/primitives/data-table/table-data-cell.js +25 -0
- package/dist/web/primitives/data-table/table-header-row.d.ts +3 -0
- package/dist/web/primitives/data-table/table-header-row.js +21 -0
- package/dist/web/primitives/data-table/table-view.d.ts +3 -0
- package/dist/web/primitives/data-table/table-view.js +25 -0
- package/dist/web/primitives/data-table/toolbar.d.ts +6 -0
- package/dist/web/primitives/data-table/toolbar.js +53 -0
- package/dist/web/primitives/data-table/use-data-table-state.d.ts +44 -0
- package/dist/web/primitives/data-table/use-data-table-state.js +123 -0
- package/dist/web/primitives/data-table.d.ts +3 -4
- package/dist/web/primitives/data-table.js +32 -252
- package/dist/web/primitives/file-value-list.js +1 -1
- package/dist/web/primitives/form-footer-parts.d.ts +25 -0
- package/dist/web/primitives/form-footer-parts.js +55 -0
- package/dist/web/primitives/form.js +33 -43
- package/dist/web/primitives/input-choice-kinds.d.ts +4 -0
- package/dist/web/primitives/input-choice-kinds.js +67 -0
- package/dist/web/primitives/input-file-kinds.d.ts +3 -0
- package/dist/web/primitives/input-file-kinds.js +6 -0
- package/dist/web/primitives/input-number-kinds.d.ts +4 -0
- package/dist/web/primitives/input-number-kinds.js +16 -0
- package/dist/web/primitives/input-optional-props.d.ts +20 -0
- package/dist/web/primitives/input-optional-props.js +24 -0
- package/dist/web/primitives/input-select-presentation.d.ts +13 -0
- package/dist/web/primitives/input-select-presentation.js +19 -0
- package/dist/web/primitives/input-shared.d.ts +22 -0
- package/dist/web/primitives/input-shared.js +10 -0
- package/dist/web/primitives/input-temporal-kinds.d.ts +5 -0
- package/dist/web/primitives/input-temporal-kinds.js +10 -0
- package/dist/web/primitives/input-text-kinds.d.ts +7 -0
- package/dist/web/primitives/input-text-kinds.js +33 -0
- package/dist/web/primitives/input.js +35 -161
- package/dist/web/widgets/chart-data.d.ts +35 -0
- package/dist/web/widgets/chart-data.js +37 -0
- package/dist/web/widgets/chart-markers.d.ts +26 -0
- package/dist/web/widgets/chart-markers.js +64 -0
- package/dist/web/widgets/chart-parts.d.ts +20 -0
- package/dist/web/widgets/chart-parts.js +15 -0
- package/dist/web/widgets/charts.d.ts +5 -78
- package/dist/web/widgets/charts.js +7 -485
- package/dist/web/widgets/drawer-panel-class.d.ts +3 -0
- package/dist/web/widgets/drawer-panel-class.js +40 -0
- package/dist/web/widgets/drawer-resize-controls.d.ts +11 -0
- package/dist/web/widgets/drawer-resize-controls.js +12 -0
- package/dist/web/widgets/drawer-sizing.d.ts +19 -0
- package/dist/web/widgets/drawer-sizing.js +60 -0
- package/dist/web/widgets/drawer.js +17 -116
- package/dist/web/widgets/stacked-area-brush.d.ts +13 -0
- package/dist/web/widgets/stacked-area-brush.js +91 -0
- package/dist/web/widgets/stacked-area-chart.d.ts +6 -0
- package/dist/web/widgets/stacked-area-chart.js +45 -0
- package/dist/web/widgets/stacked-area-chrome.d.ts +7 -0
- package/dist/web/widgets/stacked-area-chrome.js +77 -0
- package/dist/web/widgets/stacked-area-geometry.d.ts +54 -0
- package/dist/web/widgets/stacked-area-geometry.js +83 -0
- package/dist/web/widgets/stacked-area-model.d.ts +11 -0
- package/dist/web/widgets/stacked-area-model.js +1 -0
- package/dist/web/widgets/stacked-area-plot.d.ts +3 -0
- package/dist/web/widgets/stacked-area-plot.js +51 -0
- package/dist/web/widgets/stacked-area-props.d.ts +83 -0
- package/dist/web/widgets/stacked-area-props.js +16 -0
- package/dist/web/widgets/stacked-area-svg.d.ts +3 -0
- package/dist/web/widgets/stacked-area-svg.js +38 -0
- package/dist/web/widgets/stacked-area-tooltip.d.ts +3 -0
- package/dist/web/widgets/stacked-area-tooltip.js +86 -0
- package/dist/web/widgets/use-drawer-resize.d.ts +22 -0
- package/dist/web/widgets/use-drawer-resize.js +52 -0
- package/dist/web/widgets/use-stacked-area-tooltip.d.ts +9 -0
- package/dist/web/widgets/use-stacked-area-tooltip.js +34 -0
- package/dist/web/widgets/use-stacked-area-window.d.ts +20 -0
- package/dist/web/widgets/use-stacked-area-window.js +76 -0
- package/package.json +10 -10
- package/src/changes.json +54 -0
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { cn } from "../lib/cn.js";
|
|
3
|
+
import { percent } from "./chart-data.js";
|
|
4
|
+
import { MarkerPins } from "./chart-markers.js";
|
|
5
|
+
import { AXIS_TIERS } from "./stacked-area-geometry.js";
|
|
6
|
+
import { renderPlotSvg } from "./stacked-area-svg.js";
|
|
7
|
+
import { renderTooltipOverlay } from "./stacked-area-tooltip.js";
|
|
8
|
+
const MARKER_LABEL_FLIP_FRACTION = 0.97;
|
|
9
|
+
function bucketLabelTransform(index, count) {
|
|
10
|
+
if (index === 0)
|
|
11
|
+
return "";
|
|
12
|
+
return index === count - 1 ? "-translate-x-full" : "-translate-x-1/2";
|
|
13
|
+
}
|
|
14
|
+
// Markers sharing a timestamp become one label ("A, B") in the first one's color.
|
|
15
|
+
function groupMarkerLabels(markers) {
|
|
16
|
+
const groups = new Map();
|
|
17
|
+
for (const marker of [...markers].sort((a, b) => a.atMs - b.atMs)) {
|
|
18
|
+
groups.set(marker.atMs, [...(groups.get(marker.atMs) ?? []), marker]);
|
|
19
|
+
}
|
|
20
|
+
return [...groups.entries()].map(([atMs, members]) => ({
|
|
21
|
+
atMs,
|
|
22
|
+
text: members.map((m) => m.label).join(", "),
|
|
23
|
+
color: members[0]?.color ?? "var(--color-foreground)",
|
|
24
|
+
}));
|
|
25
|
+
}
|
|
26
|
+
function renderTodayLabel(m, todayFraction) {
|
|
27
|
+
return (_jsx("span", { "data-testid": "chart-today-label", className: "absolute -top-4 -translate-x-1/2 text-[11px] text-foreground", style: { left: percent(todayFraction) }, children: m.props.todayLabel }));
|
|
28
|
+
}
|
|
29
|
+
function renderScrollableAxis(m) {
|
|
30
|
+
const { times, fractionOf, formatBucket } = m.win;
|
|
31
|
+
return (_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))) }));
|
|
32
|
+
}
|
|
33
|
+
function renderTickedAxis(m) {
|
|
34
|
+
const { fractionOf, formatBucket, windowStartMs, windowEndMs, tierTicks } = m.win;
|
|
35
|
+
return (_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)))] }));
|
|
36
|
+
}
|
|
37
|
+
function renderMarkerLabels(m) {
|
|
38
|
+
return groupMarkerLabels(m.win.markers ?? []).map((group) => {
|
|
39
|
+
const fraction = m.win.fractionOf(group.atMs);
|
|
40
|
+
const flipped = fraction > MARKER_LABEL_FLIP_FRACTION;
|
|
41
|
+
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));
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
function renderMarkerPins(m, markers) {
|
|
45
|
+
const { markerLegend, markerLabels } = m.props;
|
|
46
|
+
return (_jsx(MarkerPins, { markers: markers, windowStartMs: m.win.windowStartMs, windowEndMs: m.win.windowEndMs, numbered: markerLegend === "list" || markerLabels, formatMarkerTime: m.win.formatMarkerTimeInWindow, ...(markerLabels && { className: "@2xl:hidden" }) }));
|
|
47
|
+
}
|
|
48
|
+
export function renderStackedAreaPlot(m) {
|
|
49
|
+
const { todayFraction, markers } = m.win;
|
|
50
|
+
return (_jsxs(_Fragment, { children: [todayFraction !== undefined && renderTodayLabel(m, todayFraction), renderPlotSvg(m), m.props.scrollable ? renderScrollableAxis(m) : renderTickedAxis(m), renderTooltipOverlay(m), m.props.markerLabels && renderMarkerLabels(m), markers !== undefined && markers.length > 0 && renderMarkerPins(m, markers)] }));
|
|
51
|
+
}
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import type { ReactNode } from "react";
|
|
2
|
+
import type { ChartLine, ChartMarker, ChartSeries, ColorMap, ToneMap } from "./chart-data.ts";
|
|
3
|
+
import type { StackedAreaDateFormat, StackedAreaInitialWindow, StackedAreaRanges, StackedAreaWindowSelection } from "./chart-window.ts";
|
|
4
|
+
export type StackedAreaChartProps = {
|
|
5
|
+
readonly series: readonly ChartSeries[];
|
|
6
|
+
readonly windowStartMs: number;
|
|
7
|
+
readonly windowEndMs: number;
|
|
8
|
+
readonly todayMs?: number;
|
|
9
|
+
readonly tones?: ToneMap;
|
|
10
|
+
readonly colors?: ColorMap;
|
|
11
|
+
readonly markers?: readonly ChartMarker[];
|
|
12
|
+
/** Drawn over the bands, not stacked. A `null` value breaks the line. */
|
|
13
|
+
readonly lines?: readonly ChartLine[];
|
|
14
|
+
/** Range presets anchored at today; uncontrolled, the widget renders the switch itself. */
|
|
15
|
+
readonly ranges?: StackedAreaRanges;
|
|
16
|
+
/** Uncontrolled start window; "from-today" ignores the default range and starts at today. */
|
|
17
|
+
readonly initialWindow?: StackedAreaInitialWindow;
|
|
18
|
+
/** Scrubber under the plot to drag the visible window; starts at today without `ranges`. */
|
|
19
|
+
readonly brush?: boolean;
|
|
20
|
+
readonly windowSelection?: StackedAreaWindowSelection;
|
|
21
|
+
/** Makes the window controlled: no own range switch, `windowSelection` is the only source. */
|
|
22
|
+
readonly onWindowSelectionChange?: (selection: StackedAreaWindowSelection) => void;
|
|
23
|
+
/** aria-labels of the two brush handles. */
|
|
24
|
+
readonly brushLabels?: {
|
|
25
|
+
readonly start: string;
|
|
26
|
+
readonly end: string;
|
|
27
|
+
};
|
|
28
|
+
readonly ariaLabel: string;
|
|
29
|
+
readonly todayLabel: string;
|
|
30
|
+
readonly formatBucketLabel: (atMs: number, format: StackedAreaDateFormat) => string;
|
|
31
|
+
readonly formatValue?: (value: number) => string;
|
|
32
|
+
/** y-axis tick labels only (legend and totals keep `formatValue`); defaults to `formatValue`. */
|
|
33
|
+
readonly formatAxisValue?: (value: number) => string;
|
|
34
|
+
readonly formatMarkerTime?: (atMs: number, format: StackedAreaDateFormat) => string;
|
|
35
|
+
/** Overrides the automatic format (month and year once the visible window spans 18 months). */
|
|
36
|
+
readonly dateFormat?: StackedAreaDateFormat;
|
|
37
|
+
/** Zone the x-axis ticks align to; must match the zone `formatBucketLabel` formats in. Default UTC. */
|
|
38
|
+
readonly timeZone?: string;
|
|
39
|
+
/** "legend": unnumbered pins with tooltip, one dashed legend entry per marker label and color. */
|
|
40
|
+
readonly markerLegend?: "list" | "legend";
|
|
41
|
+
/** Writes each marker label vertically next to its guide line on wide containers; narrow ones keep the pins. */
|
|
42
|
+
readonly markerLabels?: boolean;
|
|
43
|
+
/** Fixed width per bucket; the plot scrolls horizontally, y ticks stay put. */
|
|
44
|
+
readonly scrollable?: boolean;
|
|
45
|
+
readonly showLegendTotals?: boolean;
|
|
46
|
+
readonly emptyContent?: ReactNode;
|
|
47
|
+
readonly testId?: string;
|
|
48
|
+
};
|
|
49
|
+
/** formatAxisValue defaults to the resolved formatValue, so the destructuring order matters. */
|
|
50
|
+
export declare function resolveStackedAreaProps({ lines, brush, brushLabels, formatValue, formatAxisValue, markerLegend, markerLabels, scrollable, showLegendTotals, ...rest }: StackedAreaChartProps): {
|
|
51
|
+
series: readonly ChartSeries[];
|
|
52
|
+
windowStartMs: number;
|
|
53
|
+
windowEndMs: number;
|
|
54
|
+
todayMs?: number;
|
|
55
|
+
tones?: ToneMap;
|
|
56
|
+
colors?: ColorMap;
|
|
57
|
+
markers?: readonly ChartMarker[];
|
|
58
|
+
ranges?: StackedAreaRanges;
|
|
59
|
+
initialWindow?: StackedAreaInitialWindow;
|
|
60
|
+
windowSelection?: StackedAreaWindowSelection;
|
|
61
|
+
onWindowSelectionChange?: (selection: StackedAreaWindowSelection) => void;
|
|
62
|
+
ariaLabel: string;
|
|
63
|
+
todayLabel: string;
|
|
64
|
+
formatBucketLabel: (atMs: number, format: StackedAreaDateFormat) => string;
|
|
65
|
+
formatMarkerTime?: (atMs: number, format: StackedAreaDateFormat) => string;
|
|
66
|
+
dateFormat?: StackedAreaDateFormat;
|
|
67
|
+
timeZone?: string;
|
|
68
|
+
emptyContent?: ReactNode;
|
|
69
|
+
testId?: string;
|
|
70
|
+
lines: readonly ChartLine[];
|
|
71
|
+
brush: boolean;
|
|
72
|
+
brushLabels: {
|
|
73
|
+
readonly start: string;
|
|
74
|
+
readonly end: string;
|
|
75
|
+
};
|
|
76
|
+
formatValue: (value: number) => string;
|
|
77
|
+
formatAxisValue: (value: number) => string;
|
|
78
|
+
markerLegend: "legend" | "list";
|
|
79
|
+
markerLabels: boolean;
|
|
80
|
+
scrollable: boolean;
|
|
81
|
+
showLegendTotals: boolean;
|
|
82
|
+
};
|
|
83
|
+
export type ResolvedStackedAreaProps = ReturnType<typeof resolveStackedAreaProps>;
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
const DEFAULT_BRUSH_LABELS = { start: "Window start", end: "Window end" };
|
|
2
|
+
/** formatAxisValue defaults to the resolved formatValue, so the destructuring order matters. */
|
|
3
|
+
export function resolveStackedAreaProps({ lines = [], brush = false, brushLabels = DEFAULT_BRUSH_LABELS, formatValue = String, formatAxisValue = formatValue, markerLegend = "list", markerLabels = false, scrollable = false, showLegendTotals = true, ...rest }) {
|
|
4
|
+
return {
|
|
5
|
+
...rest,
|
|
6
|
+
lines,
|
|
7
|
+
brush,
|
|
8
|
+
brushLabels,
|
|
9
|
+
formatValue,
|
|
10
|
+
formatAxisValue,
|
|
11
|
+
markerLegend,
|
|
12
|
+
markerLabels,
|
|
13
|
+
scrollable,
|
|
14
|
+
showLegendTotals,
|
|
15
|
+
};
|
|
16
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { cn } from "../lib/cn.js";
|
|
3
|
+
import { SINGLE_POINT_RADIUS } from "./chart-data.js";
|
|
4
|
+
import { PLOT_HEIGHT, PLOT_WIDTH, STACKED_AREA_PLOT_HEIGHT_CLASS, } from "./stacked-area-geometry.js";
|
|
5
|
+
const bandGradientId = (m, index) => `${m.clipId}-band-${index}`;
|
|
6
|
+
function todayXOf(m) {
|
|
7
|
+
return (m.win.todayFraction ?? 1) * PLOT_WIDTH;
|
|
8
|
+
}
|
|
9
|
+
function renderBands(m, fillOpacity, edgeOpacity) {
|
|
10
|
+
return m.geometry.bands.map((band, bi) => band.dot === undefined ? (_jsxs("g", { children: [_jsx("path", { d: band.d, fill: `url(#${bandGradientId(m, 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: PLOT_WIDTH / 2, cy: band.dot, r: SINGLE_POINT_RADIUS, fill: band.color, fillOpacity: fillOpacity }, band.key)));
|
|
11
|
+
}
|
|
12
|
+
function renderDefs(m) {
|
|
13
|
+
const todayX = todayXOf(m);
|
|
14
|
+
return (_jsxs("defs", { children: [m.geometry.bands.map((band, bi) => (_jsxs("linearGradient", { id: bandGradientId(m, 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: `${m.clipId}-past`, children: _jsx("rect", { x: 0, y: 0, width: todayX, height: PLOT_HEIGHT }) }), _jsx("clipPath", { id: `${m.clipId}-future`, children: _jsx("rect", { x: todayX, y: 0, width: PLOT_WIDTH - todayX, height: PLOT_HEIGHT }) })] }));
|
|
15
|
+
}
|
|
16
|
+
function renderGridLines(m) {
|
|
17
|
+
const { yOf, yTickValues } = m.geometry;
|
|
18
|
+
return yTickValues.map((tickValue) => (_jsx("line", { "data-testid": "chart-grid-line", x1: 0, x2: PLOT_WIDTH, y1: yOf(tickValue), y2: yOf(tickValue), stroke: "var(--color-border)", strokeWidth: 1, strokeDasharray: "3 3", vectorEffect: "non-scaling-stroke" }, tickValue)));
|
|
19
|
+
}
|
|
20
|
+
function renderMarkerGuides(m) {
|
|
21
|
+
return m.geometry.coloredMarkers.map((marker) => {
|
|
22
|
+
const x = m.win.fractionOf(marker.atMs) * PLOT_WIDTH;
|
|
23
|
+
return (_jsx("line", { "data-testid": "chart-marker-guide", x1: x, x2: x, y1: 0, y2: PLOT_HEIGHT, stroke: marker.color, strokeWidth: 1, strokeDasharray: "2 3", vectorEffect: "non-scaling-stroke" }, marker.key));
|
|
24
|
+
});
|
|
25
|
+
}
|
|
26
|
+
function renderLinePaths(m) {
|
|
27
|
+
return m.geometry.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)));
|
|
28
|
+
}
|
|
29
|
+
function renderTodayLine(m) {
|
|
30
|
+
const todayX = todayXOf(m);
|
|
31
|
+
return (_jsx("line", { "data-testid": "chart-today-line", x1: todayX, x2: todayX, y1: 0, y2: PLOT_HEIGHT, stroke: "var(--color-foreground)", strokeWidth: 1, strokeDasharray: "3 3", vectorEffect: "non-scaling-stroke" }));
|
|
32
|
+
}
|
|
33
|
+
export function renderPlotSvg(m) {
|
|
34
|
+
const { clipId } = m;
|
|
35
|
+
const hasToday = m.win.todayFraction !== undefined;
|
|
36
|
+
const ariaLabel = m.props.ariaLabel;
|
|
37
|
+
return (_jsxs("svg", { viewBox: `0 0 ${PLOT_WIDTH} ${PLOT_HEIGHT}`, preserveAspectRatio: "none", className: cn("block w-full", STACKED_AREA_PLOT_HEIGHT_CLASS), role: "img", "aria-label": ariaLabel, children: [_jsx("title", { children: ariaLabel }), renderDefs(m), renderGridLines(m), _jsx("g", { clipPath: `url(#${clipId}-past)`, children: renderBands(m, 0.55, 0.9) }), hasToday && (_jsx("g", { "data-testid": "chart-forecast-region", clipPath: `url(#${clipId}-future)`, children: renderBands(m, 0.25, 0.5) })), renderMarkerGuides(m), renderLinePaths(m), hasToday && renderTodayLine(m)] }));
|
|
38
|
+
}
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { cn } from "../lib/cn.js";
|
|
3
|
+
import { chartColor, percent, valueAt } from "./chart-data.js";
|
|
4
|
+
import { bucketIndexAtFraction } from "./chart-window.js";
|
|
5
|
+
import { STACKED_AREA_PLOT_HEIGHT_CLASS } from "./stacked-area-geometry.js";
|
|
6
|
+
const TOOLTIP_WIDTH_REM = 14;
|
|
7
|
+
const TOOLTIP_OFFSET_PX = 8;
|
|
8
|
+
// A swipe over the plot (scrolling the page or the chart) must not open the card.
|
|
9
|
+
const TOOLTIP_TAP_MAX_MOVEMENT_PX = 8;
|
|
10
|
+
function buildTooltipRows(m, tooltipAtMs) {
|
|
11
|
+
const { tones, colors } = m.props;
|
|
12
|
+
const { series, lines } = m.win;
|
|
13
|
+
const { linePaths } = m.geometry;
|
|
14
|
+
return [
|
|
15
|
+
...series.map((s, i) => ({
|
|
16
|
+
key: s.key,
|
|
17
|
+
label: s.label,
|
|
18
|
+
color: chartColor(s.key, i, tones, colors),
|
|
19
|
+
swatch: "area",
|
|
20
|
+
value: valueAt(s, tooltipAtMs),
|
|
21
|
+
})),
|
|
22
|
+
...lines.flatMap((line, li) => {
|
|
23
|
+
const value = line.points.find((p) => p.atMs === tooltipAtMs)?.value;
|
|
24
|
+
return value === null || value === undefined
|
|
25
|
+
? []
|
|
26
|
+
: [
|
|
27
|
+
{
|
|
28
|
+
key: line.key,
|
|
29
|
+
label: line.label,
|
|
30
|
+
color: linePaths[li]?.color ?? "var(--color-foreground)",
|
|
31
|
+
swatch: line.dashed === true ? "dashed-line" : "line",
|
|
32
|
+
value,
|
|
33
|
+
},
|
|
34
|
+
];
|
|
35
|
+
}),
|
|
36
|
+
];
|
|
37
|
+
}
|
|
38
|
+
function renderTooltipSwatch(row) {
|
|
39
|
+
if (row.swatch === "area") {
|
|
40
|
+
return (_jsx("span", { "aria-hidden": "true", className: "size-2 shrink-0 rounded-[2px]", style: { backgroundColor: row.color } }));
|
|
41
|
+
}
|
|
42
|
+
return (_jsx("span", { "aria-hidden": "true", className: "w-3 shrink-0 border-t-2", style: {
|
|
43
|
+
borderColor: row.color,
|
|
44
|
+
borderTopStyle: row.swatch === "dashed-line" ? "dashed" : "solid",
|
|
45
|
+
} }));
|
|
46
|
+
}
|
|
47
|
+
function renderTooltipCard(m, tooltipAtMs) {
|
|
48
|
+
const tooltipFraction = m.win.fractionOf(tooltipAtMs);
|
|
49
|
+
const tooltipOnLeft = tooltipFraction > 0.5;
|
|
50
|
+
const tooltipEdge = `${percent(tooltipFraction)} ${tooltipOnLeft ? "-" : "+"} ${TOOLTIP_OFFSET_PX}px`;
|
|
51
|
+
const tooltipLeft = tooltipOnLeft
|
|
52
|
+
? `max(0px, calc(${tooltipEdge} - ${TOOLTIP_WIDTH_REM}rem))`
|
|
53
|
+
: `max(0px, min(calc(${tooltipEdge}), calc(100% - ${TOOLTIP_WIDTH_REM}rem)))`;
|
|
54
|
+
return (_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: m.win.formatBucket(tooltipAtMs) }), buildTooltipRows(m, tooltipAtMs).map((row) => (_jsxs("div", { className: "flex items-center gap-1.5", children: [renderTooltipSwatch(row), _jsx("span", { className: "min-w-0 truncate text-muted-foreground", children: row.label }), _jsx("span", { className: "ml-auto tabular-nums", children: m.props.formatAxisValue(row.value) })] }, row.key)))] })] }));
|
|
55
|
+
}
|
|
56
|
+
function bucketAtMsUnderPointer(m, event) {
|
|
57
|
+
const { times, visibleWindow } = m.win;
|
|
58
|
+
const rect = event.currentTarget.getBoundingClientRect();
|
|
59
|
+
return (times[bucketIndexAtFraction(times, visibleWindow, rect.width === 0 ? 0 : (event.clientX - rect.left) / rect.width)] ?? null);
|
|
60
|
+
}
|
|
61
|
+
export function renderTooltipOverlay(m) {
|
|
62
|
+
const { tooltipAtMs, setSelectedTooltipAtMs, tooltipOverlayRef, touchStartRef } = m.tooltip;
|
|
63
|
+
return (_jsx("div", { ref: tooltipOverlayRef, "data-testid": "chart-tooltip-overlay", className: cn("absolute inset-x-0 top-0", STACKED_AREA_PLOT_HEIGHT_CLASS), onPointerMove: (event) => {
|
|
64
|
+
if (event.pointerType === "mouse")
|
|
65
|
+
setSelectedTooltipAtMs(bucketAtMsUnderPointer(m, event));
|
|
66
|
+
}, onPointerLeave: (event) => {
|
|
67
|
+
if (event.pointerType === "mouse")
|
|
68
|
+
setSelectedTooltipAtMs(null);
|
|
69
|
+
}, onPointerDown: (event) => {
|
|
70
|
+
if (event.pointerType === "mouse")
|
|
71
|
+
return;
|
|
72
|
+
touchStartRef.current = { x: event.clientX, y: event.clientY };
|
|
73
|
+
}, onPointerCancel: () => {
|
|
74
|
+
touchStartRef.current = null;
|
|
75
|
+
}, onPointerUp: (event) => {
|
|
76
|
+
const start = touchStartRef.current;
|
|
77
|
+
touchStartRef.current = null;
|
|
78
|
+
if (event.pointerType === "mouse" || start === null)
|
|
79
|
+
return;
|
|
80
|
+
const moved = Math.hypot(event.clientX - start.x, event.clientY - start.y);
|
|
81
|
+
if (moved >= TOOLTIP_TAP_MAX_MOVEMENT_PX)
|
|
82
|
+
return;
|
|
83
|
+
const next = bucketAtMsUnderPointer(m, event);
|
|
84
|
+
setSelectedTooltipAtMs((current) => (current === next ? null : next));
|
|
85
|
+
}, children: tooltipAtMs !== undefined && renderTooltipCard(m, tooltipAtMs) }));
|
|
86
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { type KeyboardEvent } from "react";
|
|
2
|
+
import { usePointerDrag } from "./use-pointer-drag.ts";
|
|
3
|
+
export type DrawerResizeConfig = {
|
|
4
|
+
readonly defaultWidthPx?: number;
|
|
5
|
+
readonly minWidthPx?: number;
|
|
6
|
+
readonly maxWidthPx?: number;
|
|
7
|
+
};
|
|
8
|
+
export type DrawerResizeState = {
|
|
9
|
+
readonly minWidthPx: number;
|
|
10
|
+
readonly resizedWidthPx: number;
|
|
11
|
+
readonly setResizedWidthPx: (update: number | ((current: number) => number)) => void;
|
|
12
|
+
readonly maximized: boolean;
|
|
13
|
+
readonly setMaximized: (update: boolean | ((current: boolean) => boolean)) => void;
|
|
14
|
+
readonly effectiveWidthPx: number;
|
|
15
|
+
readonly effectiveMaxWidthPx: () => number;
|
|
16
|
+
};
|
|
17
|
+
export declare function useDrawerResizeState(resize: DrawerResizeConfig | undefined): DrawerResizeState;
|
|
18
|
+
export type DrawerResizeHandle = {
|
|
19
|
+
readonly handleDrag: ReturnType<typeof usePointerDrag>;
|
|
20
|
+
readonly onHandleKeyDown: (event: KeyboardEvent<HTMLDivElement>) => void;
|
|
21
|
+
};
|
|
22
|
+
export declare function useDrawerResizeHandle(side: "left" | "right" | "top" | "bottom", resize: DrawerResizeState): DrawerResizeHandle;
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { useState } from "react";
|
|
2
|
+
import { clamp } from "../lib/clamp.js";
|
|
3
|
+
import { defaultWidthFromViewport, MAX_WIDTH_PX, MIN_WIDTH_PX } from "./drawer-sizing.js";
|
|
4
|
+
import { arrowKeyDelta, usePointerDrag } from "./use-pointer-drag.js";
|
|
5
|
+
export function useDrawerResizeState(resize) {
|
|
6
|
+
const minWidthPx = resize?.minWidthPx ?? MIN_WIDTH_PX;
|
|
7
|
+
const maxWidthPx = resize?.maxWidthPx ?? MAX_WIDTH_PX;
|
|
8
|
+
const effectiveMaxWidthPx = () => typeof window === "undefined"
|
|
9
|
+
? maxWidthPx
|
|
10
|
+
: Math.min(maxWidthPx, Math.round(window.innerWidth * 0.9));
|
|
11
|
+
const [resizedWidthPx, setResizedWidthPx] = useState(() => clamp(resize?.defaultWidthPx ?? defaultWidthFromViewport(), minWidthPx, effectiveMaxWidthPx()));
|
|
12
|
+
const [maximized, setMaximized] = useState(false);
|
|
13
|
+
const effectiveWidthPx = maximized ? effectiveMaxWidthPx() : resizedWidthPx;
|
|
14
|
+
return {
|
|
15
|
+
minWidthPx,
|
|
16
|
+
resizedWidthPx,
|
|
17
|
+
setResizedWidthPx,
|
|
18
|
+
maximized,
|
|
19
|
+
setMaximized,
|
|
20
|
+
effectiveWidthPx,
|
|
21
|
+
effectiveMaxWidthPx,
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
export function useDrawerResizeHandle(side, resize) {
|
|
25
|
+
const { minWidthPx, setResizedWidthPx, maximized, setMaximized, effectiveWidthPx } = resize;
|
|
26
|
+
const { effectiveMaxWidthPx } = resize;
|
|
27
|
+
const handleDrag = usePointerDrag({
|
|
28
|
+
onStart: () => {
|
|
29
|
+
setMaximized(false);
|
|
30
|
+
return { startWidth: effectiveWidthPx };
|
|
31
|
+
},
|
|
32
|
+
onMove: ({ startWidth }, { dx }) => {
|
|
33
|
+
const signedDelta = side === "right" ? -dx : dx;
|
|
34
|
+
setResizedWidthPx(clamp(startWidth + signedDelta, minWidthPx, effectiveMaxWidthPx()));
|
|
35
|
+
},
|
|
36
|
+
});
|
|
37
|
+
const onHandleKeyDown = (event) => {
|
|
38
|
+
const keyDelta = arrowKeyDelta(event);
|
|
39
|
+
// skip: only horizontal arrow keys resize the drawer
|
|
40
|
+
if (keyDelta === undefined || keyDelta.dx === 0)
|
|
41
|
+
return;
|
|
42
|
+
event.preventDefault();
|
|
43
|
+
setMaximized(false);
|
|
44
|
+
const delta = side === "right" ? -keyDelta.dx : keyDelta.dx;
|
|
45
|
+
// Seed from the currently visible width, not the stale `resizedWidthPx`
|
|
46
|
+
// state — while maximized, it still holds the pre-maximize value, so a
|
|
47
|
+
// key press would otherwise jump the drawer back to that old size
|
|
48
|
+
// instead of resizing relative to what's on screen.
|
|
49
|
+
setResizedWidthPx((current) => clamp((maximized ? effectiveWidthPx : current) + delta, minWidthPx, effectiveMaxWidthPx()));
|
|
50
|
+
};
|
|
51
|
+
return { handleDrag, onHandleKeyDown };
|
|
52
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
export declare function useStackedAreaTooltip(times: readonly number[]): {
|
|
2
|
+
tooltipAtMs: number | undefined;
|
|
3
|
+
setSelectedTooltipAtMs: import("react").Dispatch<import("react").SetStateAction<number | null>>;
|
|
4
|
+
tooltipOverlayRef: import("react").RefObject<HTMLDivElement | null>;
|
|
5
|
+
touchStartRef: import("react").RefObject<{
|
|
6
|
+
readonly x: number;
|
|
7
|
+
readonly y: number;
|
|
8
|
+
} | null>;
|
|
9
|
+
};
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { useEffect, useRef, useState } from "react";
|
|
2
|
+
export function useStackedAreaTooltip(times) {
|
|
3
|
+
const [selectedTooltipAtMs, setSelectedTooltipAtMs] = useState(null);
|
|
4
|
+
// Stored as a time, not an index: a shifted window must not retarget the card to another bucket.
|
|
5
|
+
const tooltipAtMs = selectedTooltipAtMs !== null && times.includes(selectedTooltipAtMs)
|
|
6
|
+
? selectedTooltipAtMs
|
|
7
|
+
: undefined;
|
|
8
|
+
const tooltipOverlayRef = useRef(null);
|
|
9
|
+
const touchStartRef = useRef(null);
|
|
10
|
+
const isTooltipOpen = tooltipAtMs !== undefined;
|
|
11
|
+
useEffect(() => {
|
|
12
|
+
// skip: no tooltip card open, nothing to dismiss
|
|
13
|
+
if (!isTooltipOpen)
|
|
14
|
+
return;
|
|
15
|
+
const closeOnEscape = (event) => {
|
|
16
|
+
if (event.key === "Escape")
|
|
17
|
+
setSelectedTooltipAtMs(null);
|
|
18
|
+
};
|
|
19
|
+
const closeOnOutsidePointerDown = (event) => {
|
|
20
|
+
const overlay = tooltipOverlayRef.current;
|
|
21
|
+
// skip: pointer landed inside the overlay, taps there select a bucket instead
|
|
22
|
+
if (overlay !== null && event.target instanceof Node && overlay.contains(event.target))
|
|
23
|
+
return;
|
|
24
|
+
setSelectedTooltipAtMs(null);
|
|
25
|
+
};
|
|
26
|
+
document.addEventListener("keydown", closeOnEscape);
|
|
27
|
+
document.addEventListener("pointerdown", closeOnOutsidePointerDown);
|
|
28
|
+
return () => {
|
|
29
|
+
document.removeEventListener("keydown", closeOnEscape);
|
|
30
|
+
document.removeEventListener("pointerdown", closeOnOutsidePointerDown);
|
|
31
|
+
};
|
|
32
|
+
}, [isTooltipOpen]);
|
|
33
|
+
return { tooltipAtMs, setSelectedTooltipAtMs, tooltipOverlayRef, touchStartRef };
|
|
34
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { type ChartWindow, type StackedAreaWindowSelection } from "./chart-window.ts";
|
|
2
|
+
import type { ResolvedStackedAreaProps } from "./stacked-area-props.ts";
|
|
3
|
+
export declare function useStackedAreaWindow(props: ResolvedStackedAreaProps): {
|
|
4
|
+
isControlled: boolean;
|
|
5
|
+
selection: StackedAreaWindowSelection | undefined;
|
|
6
|
+
changeSelection: (next: StackedAreaWindowSelection) => void;
|
|
7
|
+
allTimes: readonly number[];
|
|
8
|
+
visibleWindow: ChartWindow;
|
|
9
|
+
windowStartMs: number;
|
|
10
|
+
windowEndMs: number;
|
|
11
|
+
formatBucket: (atMs: number) => string;
|
|
12
|
+
tierTicks: (readonly number[])[];
|
|
13
|
+
formatMarkerTimeInWindow: ((atMs: number) => string) | undefined;
|
|
14
|
+
series: readonly import("./chart-data.ts").ChartSeries[];
|
|
15
|
+
lines: readonly import("./chart-data.ts").ChartLine[];
|
|
16
|
+
markers: readonly import("./chart-data.ts").ChartMarker[] | undefined;
|
|
17
|
+
times: readonly number[];
|
|
18
|
+
fractionOf: (atMs: number) => number;
|
|
19
|
+
todayFraction: number | undefined;
|
|
20
|
+
};
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import { useMemo, useState } from "react";
|
|
2
|
+
import { bucketTimes } from "./chart-data.js";
|
|
3
|
+
import { initialWindowSelection, pointsWithin, resolveStackedAreaWindow, stackedAreaAxisTicks, stackedAreaDateFormatFor, } from "./chart-window.js";
|
|
4
|
+
import { AXIS_TIERS } from "./stacked-area-geometry.js";
|
|
5
|
+
function resolveVisibleWindow(props, selection, allTimes) {
|
|
6
|
+
const { ranges, brush, todayMs } = props;
|
|
7
|
+
const fullWindow = { startMs: props.windowStartMs, endMs: props.windowEndMs };
|
|
8
|
+
const isWindowing = ranges !== undefined || brush || selection !== undefined;
|
|
9
|
+
if (!isWindowing)
|
|
10
|
+
return fullWindow;
|
|
11
|
+
return resolveStackedAreaWindow({
|
|
12
|
+
selection,
|
|
13
|
+
ranges,
|
|
14
|
+
brush,
|
|
15
|
+
todayMs,
|
|
16
|
+
bucketTimes: allTimes,
|
|
17
|
+
fullWindow,
|
|
18
|
+
});
|
|
19
|
+
}
|
|
20
|
+
function restrictToWindow(props, visibleWindow) {
|
|
21
|
+
const { series: allSeries, lines: allLines, markers: allMarkers } = props;
|
|
22
|
+
const isNarrowed = visibleWindow.startMs !== props.windowStartMs || visibleWindow.endMs !== props.windowEndMs;
|
|
23
|
+
if (!isNarrowed)
|
|
24
|
+
return { series: allSeries, lines: allLines, markers: allMarkers };
|
|
25
|
+
return {
|
|
26
|
+
series: allSeries.map((s) => ({ ...s, points: pointsWithin(s.points, visibleWindow) })),
|
|
27
|
+
lines: allLines.map((line) => ({ ...line, points: pointsWithin(line.points, visibleWindow) })),
|
|
28
|
+
markers: allMarkers !== undefined ? pointsWithin(allMarkers, visibleWindow) : allMarkers,
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
export function useStackedAreaWindow(props) {
|
|
32
|
+
const { ranges, initialWindow, windowSelection, onWindowSelectionChange } = props;
|
|
33
|
+
const { dateFormat, timeZone, formatBucketLabel, formatMarkerTime, todayMs } = props;
|
|
34
|
+
const [internalSelection, setInternalSelection] = useState(initialWindowSelection(ranges, initialWindow));
|
|
35
|
+
const isControlled = onWindowSelectionChange !== undefined;
|
|
36
|
+
const selection = isControlled ? windowSelection : internalSelection;
|
|
37
|
+
const changeSelection = (next) => {
|
|
38
|
+
if (isControlled)
|
|
39
|
+
onWindowSelectionChange(next);
|
|
40
|
+
else
|
|
41
|
+
setInternalSelection(next);
|
|
42
|
+
};
|
|
43
|
+
const allTimes = bucketTimes([...props.series, ...props.lines]);
|
|
44
|
+
const visibleWindow = resolveVisibleWindow(props, selection, allTimes);
|
|
45
|
+
const windowStartMs = visibleWindow.startMs;
|
|
46
|
+
const windowEndMs = visibleWindow.endMs;
|
|
47
|
+
const effectiveDateFormat = dateFormat ?? stackedAreaDateFormatFor(visibleWindow);
|
|
48
|
+
const formatBucket = (atMs) => formatBucketLabel(atMs, effectiveDateFormat);
|
|
49
|
+
const tierTicks = useMemo(() => AXIS_TIERS.map((tier) => stackedAreaAxisTicks({ startMs: windowStartMs, endMs: windowEndMs }, tier.maxLabels, effectiveDateFormat, timeZone)), [windowStartMs, windowEndMs, effectiveDateFormat, timeZone]);
|
|
50
|
+
const formatMarkerTimeInWindow = formatMarkerTime === undefined
|
|
51
|
+
? undefined
|
|
52
|
+
: (atMs) => formatMarkerTime(atMs, effectiveDateFormat);
|
|
53
|
+
const { series, lines, markers } = restrictToWindow(props, visibleWindow);
|
|
54
|
+
const times = bucketTimes([...series, ...lines]);
|
|
55
|
+
const span = Math.max(1, windowEndMs - windowStartMs);
|
|
56
|
+
const fractionOf = (atMs) => Math.max(0, Math.min(1, (atMs - windowStartMs) / span));
|
|
57
|
+
const todayFraction = todayMs !== undefined ? fractionOf(todayMs) : undefined;
|
|
58
|
+
return {
|
|
59
|
+
isControlled,
|
|
60
|
+
selection,
|
|
61
|
+
changeSelection,
|
|
62
|
+
allTimes,
|
|
63
|
+
visibleWindow,
|
|
64
|
+
windowStartMs,
|
|
65
|
+
windowEndMs,
|
|
66
|
+
formatBucket,
|
|
67
|
+
tierTicks,
|
|
68
|
+
formatMarkerTimeInWindow,
|
|
69
|
+
series,
|
|
70
|
+
lines,
|
|
71
|
+
markers,
|
|
72
|
+
times,
|
|
73
|
+
fractionOf,
|
|
74
|
+
todayFraction,
|
|
75
|
+
};
|
|
76
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cosmicdrift/kumiko-renderer",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.390.1",
|
|
4
4
|
"description": "React renderer for Kumiko screens. The root export holds the platform-agnostic logic (primitives contract, hooks, KumikoScreen, navigation and SSE abstractions); ./headless the DOM-free UI logic, ./live the HTTP dispatcher, ./web the browser bindings (HTML primitives, history navigation, EventSource live events, createKumikoApp).",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "Marc Frost <marc@cosmicdriftgamestudio.com>",
|
|
@@ -48,8 +48,8 @@
|
|
|
48
48
|
}
|
|
49
49
|
},
|
|
50
50
|
"dependencies": {
|
|
51
|
-
"@cosmicdrift/kumiko-framework": "0.
|
|
52
|
-
"@cosmicdrift/kumiko-types": "0.
|
|
51
|
+
"@cosmicdrift/kumiko-framework": "0.390.1",
|
|
52
|
+
"@cosmicdrift/kumiko-types": "0.390.1",
|
|
53
53
|
"@radix-ui/react-dialog": "^1.1.15",
|
|
54
54
|
"@radix-ui/react-dropdown-menu": "^2.1.16",
|
|
55
55
|
"@radix-ui/react-label": "^2.1.8",
|
|
@@ -59,10 +59,10 @@
|
|
|
59
59
|
"@radix-ui/react-slot": "^1.2.4",
|
|
60
60
|
"@radix-ui/react-toast": "^1.2.15",
|
|
61
61
|
"@radix-ui/react-tooltip": "^1.2.8",
|
|
62
|
-
"@tiptap/core": "^3.
|
|
63
|
-
"@tiptap/pm": "^3.
|
|
64
|
-
"@tiptap/react": "^3.
|
|
65
|
-
"@tiptap/starter-kit": "^3.
|
|
62
|
+
"@tiptap/core": "^3.31.3",
|
|
63
|
+
"@tiptap/pm": "^3.31.3",
|
|
64
|
+
"@tiptap/react": "^3.31.3",
|
|
65
|
+
"@tiptap/starter-kit": "^3.31.3",
|
|
66
66
|
"@types/qrcode": "^1.5.5",
|
|
67
67
|
"class-variance-authority": "^0.7.1",
|
|
68
68
|
"clsx": "^2.1.1",
|
|
@@ -77,7 +77,7 @@
|
|
|
77
77
|
"react-resizable-panels": "^4.12.2",
|
|
78
78
|
"tailwind-merge": "^3.6.0",
|
|
79
79
|
"temporal-polyfill": "^0.3.2",
|
|
80
|
-
"zod": "^4.
|
|
80
|
+
"zod": "^4.6.7"
|
|
81
81
|
},
|
|
82
82
|
"peerDependencies": {
|
|
83
83
|
"@tailwindcss/cli": "^4.3.0",
|
|
@@ -92,8 +92,8 @@
|
|
|
92
92
|
}
|
|
93
93
|
},
|
|
94
94
|
"devDependencies": {
|
|
95
|
-
"@cosmicdrift/kumiko-locale-de": "0.
|
|
96
|
-
"@cosmicdrift/kumiko-testing": "0.
|
|
95
|
+
"@cosmicdrift/kumiko-locale-de": "0.390.1",
|
|
96
|
+
"@cosmicdrift/kumiko-testing": "0.390.1",
|
|
97
97
|
"@tailwindcss/cli": "^4.3.0",
|
|
98
98
|
"@testing-library/react": "^16.3.2",
|
|
99
99
|
"@testing-library/user-event": "^14.6.1",
|
package/src/changes.json
CHANGED
|
@@ -1,4 +1,58 @@
|
|
|
1
1
|
[
|
|
2
|
+
{
|
|
3
|
+
"version": "0.390.0",
|
|
4
|
+
"type": "fix",
|
|
5
|
+
"title": "previewOpenByDefault opens the newest file",
|
|
6
|
+
"detail": "FileValueList with previewOpenByDefault (renderer format \"file\") now opens only the PDF preview of the newest file instead of the first one. Uploads append their FileRef id, so the newest file is the last id in fileIds."
|
|
7
|
+
},
|
|
8
|
+
{
|
|
9
|
+
"version": "0.390.0",
|
|
10
|
+
"type": "fix",
|
|
11
|
+
"title": "DataTable card title is truncated instead of wrapping",
|
|
12
|
+
"detail": "In the mobile card layout a long title now truncates to one line with its full text on hover, and the row-actions menu no longer shrinks or leaves the viewport next to it. Before, the title wrapped over several lines and could squeeze the menu."
|
|
13
|
+
},
|
|
14
|
+
{
|
|
15
|
+
"version": "0.390.0",
|
|
16
|
+
"type": "fix",
|
|
17
|
+
"title": "DataTable resizing: row click in the leftover width, grip named by title",
|
|
18
|
+
"detail": "With column sizing the actions column absorbs the leftover width, and its cell swallowed every click, so a click on the free area to the right of the last column did not open the row. Only clicks from inside the cell (buttons, menus) stop now; a click on the empty cell reaches the row. The resize grips carry their text as title instead of aria-label, so getByLabel on a column name no longer matches the grip of every column. The accessible name stays, getByRole(\"separator\", { name }) keeps working."
|
|
19
|
+
},
|
|
20
|
+
{
|
|
21
|
+
"version": "0.390.0",
|
|
22
|
+
"type": "fix",
|
|
23
|
+
"title": "One tiptap range across the framework packages",
|
|
24
|
+
"detail": "bundled-features pinned @tiptap/* exactly to 3.31.3 while the renderer asked for ^3.30.5. Apps resolved the renderer's range to a newer patch and kept the exact pin next to it, so their lockfiles held two @tiptap/core and @tiptap/react instances (core 3.31.3 and 3.31.4 in offlot-app, money-horse, publicstatus, phronexsis, show-pony and kumiko-enterprise). Both packages now declare ^3.31.3 for every @tiptap/* dependency, so one resolved version satisfies both; the framework lockfile still resolves 3.31.3."
|
|
25
|
+
},
|
|
26
|
+
{
|
|
27
|
+
"version": "0.390.0",
|
|
28
|
+
"type": "improvement",
|
|
29
|
+
"title": "Table-driven field and input dispatch",
|
|
30
|
+
"detail": "DefaultInput, renderInput, RenderField and the read-only field text dispatch over tables, and ReferenceInput keeps its query and create state in one hook. Rendered markup and behavior are unchanged."
|
|
31
|
+
},
|
|
32
|
+
{
|
|
33
|
+
"version": "0.390.0",
|
|
34
|
+
"type": "improvement",
|
|
35
|
+
"title": "Split list and detail screens into hooks and render functions",
|
|
36
|
+
"detail": "RenderList, ProjectionListBody and ProjectionDetailBody compose focused hooks and render functions for search, columns, toolbar, queries, actions, header and states. Rendered markup and behavior are unchanged."
|
|
37
|
+
},
|
|
38
|
+
{
|
|
39
|
+
"version": "0.390.0",
|
|
40
|
+
"type": "improvement",
|
|
41
|
+
"title": "Split RenderEdit into hooks and per-kind section renderers",
|
|
42
|
+
"detail": "RenderEdit composes focused hooks and builders for state, form, draft, wizard navigation and submit, and renders sections through per-kind functions. Rendered markup and behavior are unchanged."
|
|
43
|
+
},
|
|
44
|
+
{
|
|
45
|
+
"version": "0.390.0",
|
|
46
|
+
"type": "improvement",
|
|
47
|
+
"title": "Split web StackedAreaChart into hooks and render functions",
|
|
48
|
+
"detail": "StackedAreaChart composes window and tooltip hooks with geometry builders and render functions for the plot, tooltip, brush and chrome. Rendered markup and behavior are unchanged."
|
|
49
|
+
},
|
|
50
|
+
{
|
|
51
|
+
"version": "0.390.0",
|
|
52
|
+
"type": "improvement",
|
|
53
|
+
"title": "Split web dashboard, form footer, drawer and data table into hooks and render functions",
|
|
54
|
+
"detail": "WebDashboardBody, FormFooter, Drawer and DefaultDataTable compose focused hooks and render functions for screen state, header, visibility gates, panel tiles, sticky footer rows, drawer resizing, table state, toolbar, table rows and card views. Rendered markup and behavior are unchanged."
|
|
55
|
+
},
|
|
2
56
|
{
|
|
3
57
|
"version": "0.389.0",
|
|
4
58
|
"type": "improvement",
|