@jorgerdz/timeview 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (90) hide show
  1. package/CHANGELOG.md +15 -0
  2. package/LICENSE +21 -0
  3. package/README.md +263 -0
  4. package/dist/cli/timeview.js +6710 -0
  5. package/dist/timeview.cjs +1 -0
  6. package/dist/timeview.js +5667 -0
  7. package/dist/tokens.css +67 -0
  8. package/dist/types/timeview/BandedTimeline.d.ts +11 -0
  9. package/dist/types/timeview/BandedTimeline.d.ts.map +1 -0
  10. package/dist/types/timeview/DensityHeatmap.d.ts +11 -0
  11. package/dist/types/timeview/DensityHeatmap.d.ts.map +1 -0
  12. package/dist/types/timeview/LaneCalendar.d.ts +11 -0
  13. package/dist/types/timeview/LaneCalendar.d.ts.map +1 -0
  14. package/dist/types/timeview/MetricTimeline.d.ts +8 -0
  15. package/dist/types/timeview/MetricTimeline.d.ts.map +1 -0
  16. package/dist/types/timeview/SpanMatrix.d.ts +8 -0
  17. package/dist/types/timeview/SpanMatrix.d.ts.map +1 -0
  18. package/dist/types/timeview/config.d.ts +22 -0
  19. package/dist/types/timeview/config.d.ts.map +1 -0
  20. package/dist/types/timeview/core/aggregate.d.ts +113 -0
  21. package/dist/types/timeview/core/aggregate.d.ts.map +1 -0
  22. package/dist/types/timeview/core/calendar.d.ts +27 -0
  23. package/dist/types/timeview/core/calendar.d.ts.map +1 -0
  24. package/dist/types/timeview/core/intervals.d.ts +8 -0
  25. package/dist/types/timeview/core/intervals.d.ts.map +1 -0
  26. package/dist/types/timeview/core/labels.d.ts +5 -0
  27. package/dist/types/timeview/core/labels.d.ts.map +1 -0
  28. package/dist/types/timeview/core/metric.d.ts +58 -0
  29. package/dist/types/timeview/core/metric.d.ts.map +1 -0
  30. package/dist/types/timeview/core/time.d.ts +22 -0
  31. package/dist/types/timeview/core/time.d.ts.map +1 -0
  32. package/dist/types/timeview/dashboard.d.ts +17 -0
  33. package/dist/types/timeview/dashboard.d.ts.map +1 -0
  34. package/dist/types/timeview/data.d.ts +21 -0
  35. package/dist/types/timeview/data.d.ts.map +1 -0
  36. package/dist/types/timeview/export.d.ts +14 -0
  37. package/dist/types/timeview/export.d.ts.map +1 -0
  38. package/dist/types/timeview/index.d.ts +28 -0
  39. package/dist/types/timeview/index.d.ts.map +1 -0
  40. package/dist/types/timeview/registry.d.ts +285 -0
  41. package/dist/types/timeview/registry.d.ts.map +1 -0
  42. package/dist/types/timeview/shared/Caption.d.ts +9 -0
  43. package/dist/types/timeview/shared/Caption.d.ts.map +1 -0
  44. package/dist/types/timeview/shared/EmptyState.d.ts +16 -0
  45. package/dist/types/timeview/shared/EmptyState.d.ts.map +1 -0
  46. package/dist/types/timeview/shared/Legend.d.ts +10 -0
  47. package/dist/types/timeview/shared/Legend.d.ts.map +1 -0
  48. package/dist/types/timeview/shared/Tooltip.d.ts +15 -0
  49. package/dist/types/timeview/shared/Tooltip.d.ts.map +1 -0
  50. package/dist/types/timeview/shared/useMeasuredWidth.d.ts +2 -0
  51. package/dist/types/timeview/shared/useMeasuredWidth.d.ts.map +1 -0
  52. package/dist/types/timeview/types.d.ts +158 -0
  53. package/dist/types/timeview/types.d.ts.map +1 -0
  54. package/docs/AGENT-USAGE.md +93 -0
  55. package/docs/COMPATIBILITY.md +134 -0
  56. package/docs/STUDIO.md +41 -0
  57. package/examples/README.md +21 -0
  58. package/examples/configs/bandedTimeline.json +31 -0
  59. package/examples/configs/densityHeatmap.json +33 -0
  60. package/examples/configs/laneCalendar.json +31 -0
  61. package/examples/configs/metricTimeline.json +51 -0
  62. package/examples/configs/spanMatrix.json +31 -0
  63. package/package.json +94 -0
  64. package/render.html +12 -0
  65. package/src/render.tsx +67 -0
  66. package/src/styles/tokens.css +67 -0
  67. package/src/timeview/BandedTimeline.tsx +620 -0
  68. package/src/timeview/DensityHeatmap.tsx +513 -0
  69. package/src/timeview/LaneCalendar.tsx +496 -0
  70. package/src/timeview/MetricTimeline.tsx +993 -0
  71. package/src/timeview/SpanMatrix.tsx +721 -0
  72. package/src/timeview/config.ts +399 -0
  73. package/src/timeview/core/aggregate.ts +317 -0
  74. package/src/timeview/core/calendar.ts +81 -0
  75. package/src/timeview/core/intervals.ts +52 -0
  76. package/src/timeview/core/labels.ts +19 -0
  77. package/src/timeview/core/metric.ts +263 -0
  78. package/src/timeview/core/time.ts +103 -0
  79. package/src/timeview/dashboard.ts +80 -0
  80. package/src/timeview/data.ts +242 -0
  81. package/src/timeview/export.ts +48 -0
  82. package/src/timeview/index.ts +106 -0
  83. package/src/timeview/registry.ts +207 -0
  84. package/src/timeview/shared/Caption.tsx +40 -0
  85. package/src/timeview/shared/EmptyState.tsx +90 -0
  86. package/src/timeview/shared/Legend.tsx +67 -0
  87. package/src/timeview/shared/Tooltip.tsx +59 -0
  88. package/src/timeview/shared/useMeasuredWidth.ts +21 -0
  89. package/src/timeview/types.ts +159 -0
  90. package/vite.config.ts +11 -0
@@ -0,0 +1,48 @@
1
+ import type { TimeviewConfigV1 } from "./config";
2
+ import type { ViewSpec } from "./types";
3
+
4
+ export type ExportFormat = "html" | "png";
5
+ export type ExportPresetId = "1200x720" | "1600x900";
6
+
7
+ export const EXPORT_PRESETS: Record<ExportPresetId, { width: number; height: number; label: string }> = {
8
+ "1200x720": { width: 1200, height: 720, label: "1200x720" },
9
+ "1600x900": { width: 1600, height: 900, label: "1600x900" },
10
+ };
11
+
12
+ export function isExportPresetId(value: string): value is ExportPresetId {
13
+ return value in EXPORT_PRESETS;
14
+ }
15
+
16
+ export function slug(value: string): string {
17
+ return value
18
+ .trim()
19
+ .toLowerCase()
20
+ .replace(/[^a-z0-9]+/g, "-")
21
+ .replace(/^-+|-+$/g, "")
22
+ .slice(0, 80);
23
+ }
24
+
25
+ export function exportFileName(config: TimeviewConfigV1, extension: ExportFormat): string {
26
+ const title = slug(config.dataset.meta?.title || "timeview");
27
+ const visualizer = slug(config.visualizer);
28
+ return `timeview-${visualizer}-${title || "view"}.${extension}`;
29
+ }
30
+
31
+ export function localIsoDate(now = new Date()): string {
32
+ const year = now.getFullYear();
33
+ const month = String(now.getMonth() + 1).padStart(2, "0");
34
+ const day = String(now.getDate()).padStart(2, "0");
35
+ return `${year}-${month}-${day}`;
36
+ }
37
+
38
+ export function freezeConfigForExport(config: TimeviewConfigV1, today = localIsoDate()): TimeviewConfigV1 {
39
+ if (!("today" in config.spec) || config.spec.today !== "auto") return config;
40
+
41
+ return {
42
+ ...config,
43
+ spec: {
44
+ ...config.spec,
45
+ today,
46
+ } as ViewSpec,
47
+ };
48
+ }
@@ -0,0 +1,106 @@
1
+ // Timeview — public surface.
2
+ //
3
+ // A library of reusable, time-based visualization components. Every visualizer
4
+ // renders a shared `TimeDataset` projected by a view-specific `ViewSpec`.
5
+
6
+ export { BandedTimeline } from "./BandedTimeline";
7
+ export type { BandedTimelineProps } from "./BandedTimeline";
8
+ export { LaneCalendar } from "./LaneCalendar";
9
+ export type { LaneCalendarProps } from "./LaneCalendar";
10
+ export { DensityHeatmap } from "./DensityHeatmap";
11
+ export type { DensityHeatmapProps } from "./DensityHeatmap";
12
+ export { SpanMatrix } from "./SpanMatrix";
13
+ export type { SpanMatrixProps } from "./SpanMatrix";
14
+ export { MetricTimeline } from "./MetricTimeline";
15
+ export type { MetricTimelineProps } from "./MetricTimeline";
16
+ export { TV_VISUALIZERS, TV_VISUALIZER_BY_ID } from "./registry";
17
+ export type { TimeviewVisualizer, VisualizerId } from "./registry";
18
+ export {
19
+ cloneTimeviewConfig,
20
+ decodeTimeviewConfig,
21
+ encodeTimeviewConfig,
22
+ normalizePalette,
23
+ normalizeTimeviewConfig,
24
+ normalizeViewSpec,
25
+ validateTimeDataset,
26
+ validateViewSpec,
27
+ } from "./config";
28
+ export type { TimeviewConfigV1, ValidationResult } from "./config";
29
+ export { normalizeTimeviewDashboard } from "./dashboard";
30
+ export type { TimeviewDashboardPanelV1, TimeviewDashboardV1 } from "./dashboard";
31
+ export { EXPORT_PRESETS, exportFileName, freezeConfigForExport, isExportPresetId, localIsoDate, slug } from "./export";
32
+ export type { ExportFormat, ExportPresetId } from "./export";
33
+ export { tvBuckets, tvAggregate, tvFmtDuration, tvSpanMatrix } from "./core/aggregate";
34
+ export type {
35
+ TimeBucket,
36
+ AggregateGroup,
37
+ AggregateItem,
38
+ AggregateCell,
39
+ AggregateOptions,
40
+ AggregateMatrix,
41
+ SpanMatrixCell,
42
+ SpanMatrixEventItem,
43
+ SpanMatrixIntervalItem,
44
+ SpanMatrixModel,
45
+ SpanMatrixOptions,
46
+ SpanMatrixRow,
47
+ SpanMatrixRun,
48
+ } from "./core/aggregate";
49
+ export {
50
+ METRIC_MIN_SPAN,
51
+ tvClampViewport,
52
+ tvDefaultViewport,
53
+ tvLineSegments,
54
+ tvMetricModel,
55
+ tvMetricTimeTicks,
56
+ tvNearest,
57
+ tvNiceTicks,
58
+ tvStateAt,
59
+ } from "./core/metric";
60
+ export type { MetricEvent, MetricLineSegment, MetricModel, MetricPoint, MetricState, MetricTarget, MetricTimeTick, NiceTicks } from "./core/metric";
61
+ export { tvGridWeeks, tvDayCell, tvSplitInterval, tvSameUTCDay, startOfDayUTC } from "./core/calendar";
62
+ export type { CalendarWeek, CalendarCell, CalendarIntervalSegment } from "./core/calendar";
63
+
64
+ export {
65
+ TV_DATA,
66
+ TV_PALETTES,
67
+ DAY_MS,
68
+ domainOf,
69
+ tvScale,
70
+ tvDays,
71
+ tvWeeks,
72
+ fmt,
73
+ } from "./data";
74
+ export type { FixtureKey } from "./data";
75
+
76
+ export type {
77
+ TimeInput,
78
+ TimeLabel,
79
+ TimeEvent,
80
+ TimeInterval,
81
+ MetricSample,
82
+ MetricSeries,
83
+ TimeDataset,
84
+ ViewSpecBase,
85
+ BandedTimelineSpec,
86
+ LaneCalendarSpec,
87
+ DensityHeatmapSpec,
88
+ SpanMatrixSpec,
89
+ MetricTimelineSpec,
90
+ ViewSpec,
91
+ TvScale,
92
+ OverlapMode,
93
+ LaneMode,
94
+ TodayMode,
95
+ TodayValue,
96
+ TimeBucketMode,
97
+ HeatmapMeasure,
98
+ HeatmapGroupBy,
99
+ HeatmapScaleMode,
100
+ MetricStateMode,
101
+ MetricYAxisMode,
102
+ MetricDefaultDays,
103
+ Density,
104
+ LegendPosition,
105
+ CaptionPosition,
106
+ } from "./types";
@@ -0,0 +1,207 @@
1
+ // registry.ts — demo/library registry for Timeview visualizers.
2
+
3
+ import type { ComponentType } from "react";
4
+ import { BandedTimeline } from "./BandedTimeline";
5
+ import type { BandedTimelineProps } from "./BandedTimeline";
6
+ import { LaneCalendar } from "./LaneCalendar";
7
+ import type { LaneCalendarProps } from "./LaneCalendar";
8
+ import { DensityHeatmap } from "./DensityHeatmap";
9
+ import type { DensityHeatmapProps } from "./DensityHeatmap";
10
+ import { SpanMatrix } from "./SpanMatrix";
11
+ import type { SpanMatrixProps } from "./SpanMatrix";
12
+ import { MetricTimeline } from "./MetricTimeline";
13
+ import type { MetricTimelineProps } from "./MetricTimeline";
14
+ import { TV_DATA, TV_PALETTES } from "./data";
15
+ import type { BandedTimelineSpec, DensityHeatmapSpec, LaneCalendarSpec, MetricTimelineSpec, SpanMatrixSpec, TimeDataset } from "./types";
16
+
17
+ export type VisualizerId = "bandedTimeline" | "laneCalendar" | "densityHeatmap" | "spanMatrix" | "metricTimeline";
18
+ type VisualizerSpec = BandedTimelineSpec | LaneCalendarSpec | DensityHeatmapSpec | SpanMatrixSpec | MetricTimelineSpec;
19
+ export type VisualizerProps = BandedTimelineProps | LaneCalendarProps | DensityHeatmapProps | SpanMatrixProps | MetricTimelineProps;
20
+
21
+ export interface TimeviewVisualizer {
22
+ id: VisualizerId;
23
+ kind: NonNullable<VisualizerSpec["kind"]>;
24
+ label: string;
25
+ description: string;
26
+ Component: ComponentType<any>;
27
+ defaultDataset: TimeDataset;
28
+ defaultSpec: VisualizerSpec;
29
+ defaultPalette: string[];
30
+ controls: {
31
+ legend: boolean;
32
+ events: boolean;
33
+ caption: boolean;
34
+ today: boolean;
35
+ options: string[];
36
+ };
37
+ agentGuide: string;
38
+ }
39
+
40
+ export const TV_VISUALIZERS = [
41
+ {
42
+ id: "bandedTimeline",
43
+ kind: "bandedTimeline",
44
+ label: "BandedTimeline",
45
+ description: "Horizontal day axis with interval bands and milestone diamonds.",
46
+ Component: BandedTimeline,
47
+ defaultDataset: TV_DATA.default,
48
+ defaultSpec: {
49
+ kind: "bandedTimeline",
50
+ title: TV_DATA.default.meta?.title,
51
+ overlapMode: "lanes",
52
+ density: "comfortable",
53
+ legend: { enabled: true, position: "bottom" },
54
+ caption: { enabled: true, position: "bottom", text: "Intervals may overlap. Diamond markers show milestone dates." },
55
+ events: { showPoints: true, showLabels: true },
56
+ },
57
+ defaultPalette: TV_PALETTES.Studio,
58
+ controls: {
59
+ legend: true,
60
+ events: true,
61
+ caption: true,
62
+ today: false,
63
+ options: ["overlapMode", "density", "legend", "caption", "eventLabels", "palette"],
64
+ },
65
+ agentGuide: "Use for plans, launches, trips, and schedules where interval overlap and milestone order matter on a horizontal timeline.",
66
+ },
67
+ {
68
+ id: "laneCalendar",
69
+ kind: "laneCalendar",
70
+ label: "LaneCalendar",
71
+ description: "Week rows and day columns with interval chips and milestone diamonds.",
72
+ Component: LaneCalendar,
73
+ defaultDataset: TV_DATA.default,
74
+ defaultSpec: {
75
+ kind: "laneCalendar",
76
+ title: TV_DATA.default.meta?.title,
77
+ laneMode: "packed",
78
+ density: "comfortable",
79
+ legend: { enabled: true, position: "bottom" },
80
+ caption: {
81
+ enabled: true,
82
+ position: "bottom",
83
+ text: "Chips span the days an interval covers and split at week boundaries; diamonds mark milestone dates.",
84
+ },
85
+ events: { showLabels: true },
86
+ today: null,
87
+ },
88
+ defaultPalette: TV_PALETTES.Studio,
89
+ controls: {
90
+ legend: true,
91
+ events: true,
92
+ caption: true,
93
+ today: true,
94
+ options: ["laneMode", "density", "legend", "caption", "milestoneLabels", "today", "palette"],
95
+ },
96
+ agentGuide: "Use for calendar-first views where week rows, day cells, and current-day context are more useful than a continuous axis.",
97
+ },
98
+ {
99
+ id: "densityHeatmap",
100
+ kind: "densityHeatmap",
101
+ label: "DensityHeatmap",
102
+ description: "Aggregated category-by-time matrix for counts and active duration.",
103
+ Component: DensityHeatmap,
104
+ defaultDataset: TV_DATA.default,
105
+ defaultSpec: {
106
+ kind: "densityHeatmap",
107
+ title: TV_DATA.default.meta?.title,
108
+ bucket: "day",
109
+ measure: "count",
110
+ groupBy: "category",
111
+ scaleMode: "category",
112
+ density: "comfortable",
113
+ showValues: true,
114
+ caption: {
115
+ enabled: true,
116
+ position: "bottom",
117
+ text: "Cell intensity encodes density. Intervals add active duration to every bucket they touch; milestones add a count to their bucket.",
118
+ },
119
+ },
120
+ defaultPalette: TV_PALETTES.Studio,
121
+ controls: {
122
+ legend: false,
123
+ events: false,
124
+ caption: true,
125
+ today: false,
126
+ options: ["bucket", "measure", "groupBy", "scaleMode", "density", "showValues", "caption", "palette"],
127
+ },
128
+ agentGuide: "Use for high-volume data when the goal is spotting density patterns by day/week and category rather than reading every item.",
129
+ },
130
+ {
131
+ id: "metricTimeline",
132
+ kind: "metricTimeline",
133
+ label: "MetricTimeline",
134
+ description: "Numeric trend line with diet/state bands and a navigable viewport.",
135
+ Component: MetricTimeline,
136
+ defaultDataset: TV_DATA.metric,
137
+ defaultSpec: {
138
+ kind: "metricTimeline",
139
+ title: TV_DATA.metric.meta?.title,
140
+ stateMode: "band",
141
+ yAxis: "auto",
142
+ defaultDays: 90,
143
+ today: "2026-06-09",
144
+ density: "comfortable",
145
+ showPoints: true,
146
+ showValues: false,
147
+ showTarget: true,
148
+ minimap: true,
149
+ legend: { enabled: true, position: "bottom" },
150
+ caption: {
151
+ enabled: true,
152
+ position: "bottom",
153
+ text: "The line shows logged values. State bands mark diet, maintenance, and off-plan periods; the minimap freezes the active viewport for export.",
154
+ },
155
+ events: { showMarkers: true },
156
+ },
157
+ defaultPalette: TV_PALETTES.Studio,
158
+ controls: {
159
+ legend: true,
160
+ events: true,
161
+ caption: true,
162
+ today: true,
163
+ options: ["stateMode", "yAxis", "defaultDays", "points", "values", "target", "minimap", "milestoneMarkers", "legend", "caption", "palette"],
164
+ },
165
+ agentGuide:
166
+ "Use for logged numeric measurements over time, such as weight, spend, latency, or mood, especially when contextual periods like diet versus non-diet should explain the trend.",
167
+ },
168
+ {
169
+ id: "spanMatrix",
170
+ kind: "spanMatrix",
171
+ label: "SpanMatrix",
172
+ description: "Presence and continuity matrix by category and day/week bucket.",
173
+ Component: SpanMatrix,
174
+ defaultDataset: TV_DATA.stacked,
175
+ defaultSpec: {
176
+ kind: "spanMatrix",
177
+ title: TV_DATA.stacked.meta?.title,
178
+ bucket: "day",
179
+ groupBy: "category",
180
+ zoom: 1,
181
+ density: "comfortable",
182
+ showCounts: true,
183
+ legend: { enabled: true, position: "bottom" },
184
+ caption: {
185
+ enabled: true,
186
+ position: "bottom",
187
+ text: "Each cell marks whether a category had an interval active that day; touching cells join into one run, and hatched cells carry two or more overlapping spans.",
188
+ },
189
+ events: { showMarkers: true },
190
+ today: "2026-01-14",
191
+ },
192
+ defaultPalette: TV_PALETTES.Studio,
193
+ controls: {
194
+ legend: true,
195
+ events: true,
196
+ caption: true,
197
+ today: true,
198
+ options: ["bucket", "groupBy", "density", "showCounts", "milestoneMarkers", "today", "legend", "caption", "palette"],
199
+ },
200
+ agentGuide: "Use for coverage views where the goal is seeing category presence, continuity, gaps, overlaps, and milestones across day/week buckets.",
201
+ },
202
+ ] satisfies TimeviewVisualizer[];
203
+
204
+ export const TV_VISUALIZER_BY_ID = Object.fromEntries(TV_VISUALIZERS.map((entry) => [entry.id, entry])) as Record<
205
+ VisualizerId,
206
+ TimeviewVisualizer
207
+ >;
@@ -0,0 +1,40 @@
1
+ // Caption.tsx — shared note-style caption for Timeview components.
2
+
3
+ import type { CaptionPosition } from "../types";
4
+
5
+ interface CaptionProps {
6
+ position: CaptionPosition;
7
+ text: string;
8
+ narrow: boolean;
9
+ }
10
+
11
+ export function Caption({ position, text, narrow }: CaptionProps) {
12
+ return (
13
+ <div
14
+ style={{
15
+ fontFamily: "var(--tv-font)",
16
+ fontSize: narrow ? 12 : 13,
17
+ lineHeight: 1.5,
18
+ color: "var(--tv-ink-3)",
19
+ padding: position === "top" ? "0 0 14px" : "14px 0 0",
20
+ borderTop: position === "bottom" ? "1px solid var(--tv-line)" : "none",
21
+ marginTop: position === "bottom" ? 4 : 0,
22
+ maxWidth: 760,
23
+ }}
24
+ >
25
+ <span
26
+ style={{
27
+ fontFamily: "var(--tv-mono)",
28
+ fontSize: 10.5,
29
+ letterSpacing: ".06em",
30
+ textTransform: "uppercase",
31
+ color: "var(--tv-ink-4)",
32
+ marginRight: 8,
33
+ }}
34
+ >
35
+ Note
36
+ </span>
37
+ {text}
38
+ </div>
39
+ );
40
+ }
@@ -0,0 +1,90 @@
1
+ // EmptyState.tsx - shared static-safe empty-state block for Timeview visualizers.
2
+
3
+ import type { CSSProperties } from "react";
4
+
5
+ type EmptyStateIcon = "calendar" | "grid" | "matrix" | "trend";
6
+ type EmptyStateVariant = "overlay" | "panel";
7
+
8
+ interface EmptyStateProps {
9
+ title: string;
10
+ description?: string;
11
+ icon?: EmptyStateIcon;
12
+ variant?: EmptyStateVariant;
13
+ background?: boolean;
14
+ bordered?: boolean;
15
+ height?: CSSProperties["height"];
16
+ narrow?: boolean;
17
+ }
18
+
19
+ export function EmptyState({
20
+ title,
21
+ description,
22
+ icon = "calendar",
23
+ variant = "overlay",
24
+ background = false,
25
+ bordered = false,
26
+ height = 200,
27
+ narrow = false,
28
+ }: EmptyStateProps) {
29
+ const isPanel = variant === "panel";
30
+ return (
31
+ <div
32
+ style={{
33
+ position: isPanel ? "relative" : "absolute",
34
+ inset: isPanel ? undefined : 0,
35
+ height: isPanel ? height : undefined,
36
+ border: bordered ? "1px solid var(--tv-grid)" : "none",
37
+ borderRadius: bordered ? "var(--tv-r-md)" : undefined,
38
+ display: "flex",
39
+ flexDirection: "column",
40
+ alignItems: "center",
41
+ justifyContent: "center",
42
+ gap: description ? 7 : 6,
43
+ padding: narrow ? 16 : 20,
44
+ pointerEvents: "none",
45
+ textAlign: "center",
46
+ background: background ? "rgba(255,255,255,0.55)" : undefined,
47
+ }}
48
+ >
49
+ <EmptyIcon icon={icon} size={icon === "grid" || icon === "matrix" ? 28 : 30} />
50
+ <div style={{ fontSize: narrow ? 13.5 : 14, fontWeight: 600, color: "var(--tv-ink-3)", lineHeight: 1.25 }}>
51
+ {title}
52
+ </div>
53
+ {description && (
54
+ <div style={{ fontSize: 12.5, color: "var(--tv-ink-4)", lineHeight: 1.35, maxWidth: 320 }}>
55
+ {description}
56
+ </div>
57
+ )}
58
+ </div>
59
+ );
60
+ }
61
+
62
+ function EmptyIcon({ icon, size }: { icon: EmptyStateIcon; size: number }) {
63
+ return (
64
+ <svg width={size} height={size} viewBox="0 0 24 24" fill="none" stroke="var(--tv-ink-5)" strokeWidth="1.5" aria-hidden="true">
65
+ {icon === "trend" ? (
66
+ <>
67
+ <path d="M3 17l5-6 4 3 5-7" />
68
+ <path d="M3 21h18" />
69
+ </>
70
+ ) : icon === "grid" ? (
71
+ <>
72
+ <rect x="3" y="3" width="7" height="7" rx="1.5" />
73
+ <rect x="14" y="3" width="7" height="7" rx="1.5" />
74
+ <rect x="3" y="14" width="7" height="7" rx="1.5" />
75
+ <rect x="14" y="14" width="7" height="7" rx="1.5" />
76
+ </>
77
+ ) : icon === "matrix" ? (
78
+ <>
79
+ <rect x="3" y="5" width="18" height="14" rx="2" />
80
+ <path d="M3 10h18M8 14h5" />
81
+ </>
82
+ ) : (
83
+ <>
84
+ <rect x="3" y="5" width="18" height="14" rx="2" />
85
+ <path d="M3 10h18M8 3v4M16 3v4" />
86
+ </>
87
+ )}
88
+ </svg>
89
+ );
90
+ }
@@ -0,0 +1,67 @@
1
+ // Legend.tsx — shared categorical legend for Timeview components.
2
+
3
+ import type { LegendPosition, TimeLabel } from "../types";
4
+
5
+ interface LegendProps {
6
+ labels: TimeLabel[];
7
+ colors: Record<string, string>;
8
+ position: LegendPosition;
9
+ vertical?: boolean;
10
+ }
11
+
12
+ export function Legend({ labels, colors, position, vertical = false }: LegendProps) {
13
+ return (
14
+ <div
15
+ style={{
16
+ display: "flex",
17
+ flexDirection: vertical ? "column" : "row",
18
+ flexWrap: "wrap",
19
+ gap: vertical ? 8 : "8px 16px",
20
+ alignItems: vertical ? "stretch" : "center",
21
+ padding: vertical ? 0 : position === "top" ? "0 0 14px" : "14px 0 2px",
22
+ }}
23
+ >
24
+ {labels.map((l) => {
25
+ const c = colors[l.id];
26
+ return (
27
+ <div
28
+ key={l.id}
29
+ style={{
30
+ display: "inline-flex",
31
+ alignItems: "center",
32
+ gap: 8,
33
+ minWidth: 0,
34
+ flex: "none",
35
+ maxWidth: vertical ? "100%" : 240,
36
+ }}
37
+ >
38
+ <span
39
+ style={{
40
+ width: 11,
41
+ height: 11,
42
+ borderRadius: 3,
43
+ background: c,
44
+ boxShadow: "inset 0 0 0 1px rgba(0,0,0,0.14)",
45
+ flex: "none",
46
+ }}
47
+ />
48
+ <span
49
+ style={{
50
+ fontFamily: "var(--tv-font)",
51
+ fontSize: 12.5,
52
+ color: "var(--tv-ink-2)",
53
+ fontWeight: 500,
54
+ lineHeight: 1.3,
55
+ whiteSpace: vertical ? "normal" : "nowrap",
56
+ overflow: vertical ? "visible" : "hidden",
57
+ textOverflow: vertical ? "clip" : "ellipsis",
58
+ }}
59
+ >
60
+ {l.name}
61
+ </span>
62
+ </div>
63
+ );
64
+ })}
65
+ </div>
66
+ );
67
+ }
@@ -0,0 +1,59 @@
1
+ // Tooltip.tsx — shared tooltip shell for Timeview components.
2
+
3
+ import type { CSSProperties } from "react";
4
+
5
+ interface TooltipProps {
6
+ left: number;
7
+ top: number;
8
+ width: number;
9
+ color: string;
10
+ label: string;
11
+ title: string;
12
+ line: string;
13
+ sub: string;
14
+ kindLabel: string;
15
+ isEvent: boolean;
16
+ }
17
+
18
+ export function Tooltip({ left, top, width, color, label, title, line, sub, kindLabel, isEvent }: TooltipProps) {
19
+ const dotStyle: CSSProperties = {
20
+ width: 9,
21
+ height: 9,
22
+ borderRadius: isEvent ? 1 : "50%",
23
+ background: isEvent ? "#fff" : color,
24
+ border: isEvent ? `2px solid ${color}` : "none",
25
+ transform: isEvent ? "rotate(45deg)" : "none",
26
+ flex: "none",
27
+ };
28
+ return (
29
+ <div data-tv-transient="tooltip" style={{ position: "absolute", left, top, width, pointerEvents: "none", zIndex: 50 }}>
30
+ <div
31
+ style={{
32
+ background: "#fff",
33
+ border: "1px solid var(--tv-line)",
34
+ borderRadius: "var(--tv-r-md)",
35
+ boxShadow: "var(--tv-shadow-pop)",
36
+ padding: "10px 13px",
37
+ }}
38
+ >
39
+ <div style={{ display: "flex", alignItems: "center", gap: 7, marginBottom: 5 }}>
40
+ <span style={dotStyle} />
41
+ <span
42
+ style={{
43
+ fontFamily: "var(--tv-mono)",
44
+ fontSize: 10,
45
+ letterSpacing: ".05em",
46
+ textTransform: "uppercase",
47
+ color: "var(--tv-ink-4)",
48
+ }}
49
+ >
50
+ {kindLabel} · {label}
51
+ </span>
52
+ </div>
53
+ <div style={{ fontSize: 14, fontWeight: 600, color: "var(--tv-ink)", marginBottom: 4, lineHeight: 1.3 }}>{title}</div>
54
+ <div style={{ fontFamily: "var(--tv-mono)", fontSize: 11, color: "var(--tv-ink-3)", lineHeight: 1.45 }}>{line}</div>
55
+ <div style={{ fontFamily: "var(--tv-mono)", fontSize: 11, color: "var(--tv-ink-4)", marginTop: 2 }}>{sub}</div>
56
+ </div>
57
+ </div>
58
+ );
59
+ }
@@ -0,0 +1,21 @@
1
+ // useMeasuredWidth.ts — ResizeObserver width measurement for responsive visualizers.
2
+
3
+ import { useLayoutEffect, useRef, useState } from "react";
4
+
5
+ export function useMeasuredWidth<T extends HTMLElement>(defaultWidth = 1200) {
6
+ const ref = useRef<T>(null);
7
+ const [width, setWidth] = useState(defaultWidth);
8
+
9
+ useLayoutEffect(() => {
10
+ const el = ref.current;
11
+ if (!el) return;
12
+ const ro = new ResizeObserver((ents) => {
13
+ for (const e of ents) setWidth(e.contentRect.width);
14
+ });
15
+ ro.observe(el);
16
+ setWidth(el.getBoundingClientRect().width);
17
+ return () => ro.disconnect();
18
+ }, []);
19
+
20
+ return [ref, width] as const;
21
+ }