@vllnt/ui 0.2.1-canary.ab2c69a → 0.2.1-canary.b3f1dff
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/components/bottom-activity-strip/bottom-activity-strip.js +91 -0
- package/dist/components/bottom-activity-strip/index.js +6 -0
- package/dist/components/comment-pin/comment-pin.js +104 -0
- package/dist/components/comment-pin/index.js +6 -0
- package/dist/components/heat-overlay/heat-overlay.js +92 -0
- package/dist/components/heat-overlay/index.js +6 -0
- package/dist/components/index.js +40 -0
- package/dist/components/live-cursor/index.js +6 -0
- package/dist/components/live-cursor/live-cursor.js +62 -0
- package/dist/components/metric-cluster/index.js +6 -0
- package/dist/components/metric-cluster/metric-cluster.js +96 -0
- package/dist/components/playback-ghost/index.js +6 -0
- package/dist/components/playback-ghost/playback-ghost.js +83 -0
- package/dist/components/presence-sync-indicator/index.js +6 -0
- package/dist/components/presence-sync-indicator/presence-sync-indicator.js +73 -0
- package/dist/components/run-timeline/index.js +6 -0
- package/dist/components/run-timeline/run-timeline.js +221 -0
- package/dist/components/state-badge-overlay/index.js +6 -0
- package/dist/components/state-badge-overlay/state-badge-overlay.js +90 -0
- package/dist/components/timeline-scrubber/index.js +6 -0
- package/dist/components/timeline-scrubber/timeline-scrubber.js +179 -0
- package/dist/index.d.ts +826 -1
- package/package.json +1 -1
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
3
|
+
import {
|
|
4
|
+
forwardRef
|
|
5
|
+
} from "react";
|
|
6
|
+
import { cn } from "../../lib/utils";
|
|
7
|
+
const KIND_GLYPH = {
|
|
8
|
+
agent: "\u25C7",
|
|
9
|
+
artifact: "\u25CC",
|
|
10
|
+
input: "\u2198",
|
|
11
|
+
output: "\u2197",
|
|
12
|
+
run: "\u25B6",
|
|
13
|
+
task: "\u25A2"
|
|
14
|
+
};
|
|
15
|
+
const KIND_LABEL = {
|
|
16
|
+
agent: "Agent",
|
|
17
|
+
artifact: "Artifact",
|
|
18
|
+
input: "Input",
|
|
19
|
+
output: "Output",
|
|
20
|
+
run: "Run",
|
|
21
|
+
task: "Task"
|
|
22
|
+
};
|
|
23
|
+
const DEFAULT_LABELS = {
|
|
24
|
+
region: "Playback ghost"
|
|
25
|
+
};
|
|
26
|
+
const clamp01 = (v) => {
|
|
27
|
+
if (v < 0) {
|
|
28
|
+
return 0;
|
|
29
|
+
}
|
|
30
|
+
if (v > 1) {
|
|
31
|
+
return 1;
|
|
32
|
+
}
|
|
33
|
+
return v;
|
|
34
|
+
};
|
|
35
|
+
const PlaybackGhost = forwardRef(
|
|
36
|
+
(props, ref) => {
|
|
37
|
+
const {
|
|
38
|
+
className,
|
|
39
|
+
kind,
|
|
40
|
+
label,
|
|
41
|
+
labels,
|
|
42
|
+
opacity = 0.4,
|
|
43
|
+
size = 40,
|
|
44
|
+
x,
|
|
45
|
+
y,
|
|
46
|
+
...rest
|
|
47
|
+
} = props;
|
|
48
|
+
const resolvedLabels = { ...DEFAULT_LABELS, ...labels };
|
|
49
|
+
const safeOpacity = clamp01(opacity);
|
|
50
|
+
const safeSize = size < 16 ? 16 : size;
|
|
51
|
+
const ariaLabel = kind ? `${resolvedLabels.region}: ${KIND_LABEL[kind]}` : resolvedLabels.region;
|
|
52
|
+
return /* @__PURE__ */ jsxs(
|
|
53
|
+
"div",
|
|
54
|
+
{
|
|
55
|
+
"aria-label": ariaLabel,
|
|
56
|
+
className: cn(
|
|
57
|
+
"pointer-events-none absolute z-10 inline-flex -translate-x-1/2 -translate-y-1/2 items-center justify-center gap-1.5 rounded-md border border-dashed border-border/70 bg-background/40 px-2 py-1 text-xs text-foreground backdrop-blur-sm",
|
|
58
|
+
className
|
|
59
|
+
),
|
|
60
|
+
"data-playback-ghost": true,
|
|
61
|
+
"data-playback-kind": kind ?? "unknown",
|
|
62
|
+
ref,
|
|
63
|
+
role: "img",
|
|
64
|
+
style: {
|
|
65
|
+
left: x,
|
|
66
|
+
minHeight: safeSize,
|
|
67
|
+
minWidth: safeSize,
|
|
68
|
+
opacity: safeOpacity,
|
|
69
|
+
top: y
|
|
70
|
+
},
|
|
71
|
+
...rest,
|
|
72
|
+
children: [
|
|
73
|
+
kind ? /* @__PURE__ */ jsx("span", { "aria-hidden": "true", className: "text-muted-foreground", children: KIND_GLYPH[kind] }) : null,
|
|
74
|
+
label ? /* @__PURE__ */ jsx("span", { className: "truncate", "data-playback-ghost-label": true, children: label }) : null
|
|
75
|
+
]
|
|
76
|
+
}
|
|
77
|
+
);
|
|
78
|
+
}
|
|
79
|
+
);
|
|
80
|
+
PlaybackGhost.displayName = "PlaybackGhost";
|
|
81
|
+
export {
|
|
82
|
+
PlaybackGhost
|
|
83
|
+
};
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
3
|
+
import {
|
|
4
|
+
forwardRef
|
|
5
|
+
} from "react";
|
|
6
|
+
import { cn } from "../../lib/utils";
|
|
7
|
+
const STATE_LABEL = {
|
|
8
|
+
error: "Sync error",
|
|
9
|
+
live: "Live",
|
|
10
|
+
offline: "Offline",
|
|
11
|
+
reconnecting: "Reconnecting",
|
|
12
|
+
syncing: "Syncing"
|
|
13
|
+
};
|
|
14
|
+
const STATE_DOT = {
|
|
15
|
+
error: "bg-red-500",
|
|
16
|
+
live: "bg-emerald-500",
|
|
17
|
+
offline: "bg-muted-foreground",
|
|
18
|
+
reconnecting: "bg-amber-500 animate-pulse",
|
|
19
|
+
syncing: "bg-blue-500 animate-pulse"
|
|
20
|
+
};
|
|
21
|
+
const STATE_TEXT = {
|
|
22
|
+
error: "text-red-700 dark:text-red-300",
|
|
23
|
+
live: "text-emerald-700 dark:text-emerald-300",
|
|
24
|
+
offline: "text-muted-foreground",
|
|
25
|
+
reconnecting: "text-amber-700 dark:text-amber-300",
|
|
26
|
+
syncing: "text-blue-700 dark:text-blue-300"
|
|
27
|
+
};
|
|
28
|
+
const DEFAULT_LABELS = {
|
|
29
|
+
region: "Presence sync"
|
|
30
|
+
};
|
|
31
|
+
const PresenceSyncIndicator = forwardRef((props, ref) => {
|
|
32
|
+
const { className, label, labels, state, status, ...rest } = props;
|
|
33
|
+
const resolvedLabels = { ...DEFAULT_LABELS, ...labels };
|
|
34
|
+
const text = label ?? STATE_LABEL[state];
|
|
35
|
+
return /* @__PURE__ */ jsxs(
|
|
36
|
+
"div",
|
|
37
|
+
{
|
|
38
|
+
"aria-label": `${resolvedLabels.region}: ${STATE_LABEL[state]}`,
|
|
39
|
+
className: cn(
|
|
40
|
+
"inline-flex items-center gap-1.5 rounded-full border border-border bg-background px-2 py-1 text-[11px] shadow-sm",
|
|
41
|
+
className
|
|
42
|
+
),
|
|
43
|
+
"data-presence-state": state,
|
|
44
|
+
"data-presence-sync": true,
|
|
45
|
+
ref,
|
|
46
|
+
role: "status",
|
|
47
|
+
...rest,
|
|
48
|
+
children: [
|
|
49
|
+
/* @__PURE__ */ jsx(
|
|
50
|
+
"span",
|
|
51
|
+
{
|
|
52
|
+
"aria-hidden": "true",
|
|
53
|
+
className: cn("h-1.5 w-1.5 rounded-full", STATE_DOT[state]),
|
|
54
|
+
"data-presence-sync-dot": true
|
|
55
|
+
}
|
|
56
|
+
),
|
|
57
|
+
/* @__PURE__ */ jsx("span", { className: cn("font-medium", STATE_TEXT[state]), children: text }),
|
|
58
|
+
status ? /* @__PURE__ */ jsx(
|
|
59
|
+
"span",
|
|
60
|
+
{
|
|
61
|
+
className: "text-[10px] text-muted-foreground",
|
|
62
|
+
"data-presence-sync-status": true,
|
|
63
|
+
children: status
|
|
64
|
+
}
|
|
65
|
+
) : null
|
|
66
|
+
]
|
|
67
|
+
}
|
|
68
|
+
);
|
|
69
|
+
});
|
|
70
|
+
PresenceSyncIndicator.displayName = "PresenceSyncIndicator";
|
|
71
|
+
export {
|
|
72
|
+
PresenceSyncIndicator
|
|
73
|
+
};
|
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
3
|
+
import {
|
|
4
|
+
forwardRef
|
|
5
|
+
} from "react";
|
|
6
|
+
import { cn } from "../../lib/utils";
|
|
7
|
+
const STATE_FILL = {
|
|
8
|
+
complete: "bg-emerald-500/70",
|
|
9
|
+
failed: "bg-red-500/70",
|
|
10
|
+
queued: "bg-amber-500/70",
|
|
11
|
+
running: "bg-blue-500/70",
|
|
12
|
+
stopped: "bg-muted-foreground/40"
|
|
13
|
+
};
|
|
14
|
+
const STATE_LABEL = {
|
|
15
|
+
complete: "Complete",
|
|
16
|
+
failed: "Failed",
|
|
17
|
+
queued: "Queued",
|
|
18
|
+
running: "Running",
|
|
19
|
+
stopped: "Stopped"
|
|
20
|
+
};
|
|
21
|
+
const DEFAULT_LABELS = {
|
|
22
|
+
empty: "No phases",
|
|
23
|
+
region: "Run timeline"
|
|
24
|
+
};
|
|
25
|
+
const clamp = (v, min, max) => {
|
|
26
|
+
if (v < min) {
|
|
27
|
+
return min;
|
|
28
|
+
}
|
|
29
|
+
if (v > max) {
|
|
30
|
+
return max;
|
|
31
|
+
}
|
|
32
|
+
return v;
|
|
33
|
+
};
|
|
34
|
+
const Endpoints = (props) => {
|
|
35
|
+
const fmt = props.format;
|
|
36
|
+
const showCursor = typeof props.cursor === "number";
|
|
37
|
+
return /* @__PURE__ */ jsxs("div", { className: "flex items-baseline justify-between gap-2 text-[11px] text-muted-foreground", children: [
|
|
38
|
+
/* @__PURE__ */ jsx("span", { "data-run-timeline-start": true, children: fmt ? fmt(props.start) : props.start }),
|
|
39
|
+
showCursor ? /* @__PURE__ */ jsx(
|
|
40
|
+
"span",
|
|
41
|
+
{
|
|
42
|
+
className: "font-semibold text-foreground",
|
|
43
|
+
"data-run-timeline-cursor-label": true,
|
|
44
|
+
children: fmt ? fmt(props.cursor ?? 0) : props.cursor
|
|
45
|
+
}
|
|
46
|
+
) : null,
|
|
47
|
+
/* @__PURE__ */ jsx("span", { "data-run-timeline-end": true, children: fmt ? fmt(props.end) : props.end })
|
|
48
|
+
] });
|
|
49
|
+
};
|
|
50
|
+
const PhaseBar = (props) => {
|
|
51
|
+
const { laneIndex, laneTotal, phase, span, start } = props;
|
|
52
|
+
const left = clamp((phase.start - start) / span, 0, 1) * 100;
|
|
53
|
+
const right = clamp((phase.end - start) / span, 0, 1) * 100;
|
|
54
|
+
const width = Math.max(right - left, 0.5);
|
|
55
|
+
const top = laneIndex / laneTotal * 100;
|
|
56
|
+
const height = 100 / laneTotal;
|
|
57
|
+
const state = phase.state ?? "running";
|
|
58
|
+
const sharedStyle = {
|
|
59
|
+
height: `${height}%`,
|
|
60
|
+
left: `${left}%`,
|
|
61
|
+
top: `${top}%`,
|
|
62
|
+
width: `${width}%`
|
|
63
|
+
};
|
|
64
|
+
const ariaLabel = `${STATE_LABEL[state]} ${phase.start} \u2192 ${phase.end}`;
|
|
65
|
+
if (phase.onActivate) {
|
|
66
|
+
const handleClick = () => {
|
|
67
|
+
phase.onActivate?.();
|
|
68
|
+
};
|
|
69
|
+
return /* @__PURE__ */ jsx(
|
|
70
|
+
"button",
|
|
71
|
+
{
|
|
72
|
+
"aria-label": ariaLabel,
|
|
73
|
+
className: cn(
|
|
74
|
+
"absolute flex items-center justify-start overflow-hidden truncate rounded-sm border border-border/50 px-1 text-left text-[10px] text-foreground transition-opacity hover:opacity-90 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring",
|
|
75
|
+
STATE_FILL[state]
|
|
76
|
+
),
|
|
77
|
+
"data-run-phase": phase.id,
|
|
78
|
+
"data-run-phase-state": state,
|
|
79
|
+
onClick: handleClick,
|
|
80
|
+
style: sharedStyle,
|
|
81
|
+
type: "button",
|
|
82
|
+
children: phase.label
|
|
83
|
+
}
|
|
84
|
+
);
|
|
85
|
+
}
|
|
86
|
+
return /* @__PURE__ */ jsx(
|
|
87
|
+
"span",
|
|
88
|
+
{
|
|
89
|
+
"aria-label": ariaLabel,
|
|
90
|
+
className: cn(
|
|
91
|
+
"absolute flex items-center justify-start overflow-hidden truncate rounded-sm border border-border/50 px-1 text-[10px] text-foreground",
|
|
92
|
+
STATE_FILL[state]
|
|
93
|
+
),
|
|
94
|
+
"data-run-phase": phase.id,
|
|
95
|
+
"data-run-phase-state": state,
|
|
96
|
+
role: "img",
|
|
97
|
+
style: sharedStyle,
|
|
98
|
+
children: phase.label
|
|
99
|
+
}
|
|
100
|
+
);
|
|
101
|
+
};
|
|
102
|
+
const TrackBody = (props) => {
|
|
103
|
+
const { cursor, end, lanes, phases, start } = props;
|
|
104
|
+
const span = end - start;
|
|
105
|
+
const cursorRatio = typeof cursor === "number" ? clamp((cursor - start) / span, 0, 1) : null;
|
|
106
|
+
return /* @__PURE__ */ jsxs("div", { className: "flex items-stretch", children: [
|
|
107
|
+
/* @__PURE__ */ jsx("div", { className: "flex w-24 flex-col gap-px text-[10px] uppercase tracking-wide text-muted-foreground", children: lanes.map((lane) => /* @__PURE__ */ jsx(
|
|
108
|
+
"div",
|
|
109
|
+
{
|
|
110
|
+
className: "flex h-7 items-center px-2",
|
|
111
|
+
"data-run-timeline-lane": lane.id,
|
|
112
|
+
children: lane.label
|
|
113
|
+
},
|
|
114
|
+
lane.id
|
|
115
|
+
)) }),
|
|
116
|
+
/* @__PURE__ */ jsxs(
|
|
117
|
+
"div",
|
|
118
|
+
{
|
|
119
|
+
className: "relative flex-1 overflow-hidden rounded-md border border-border bg-muted/20",
|
|
120
|
+
"data-run-timeline-track": true,
|
|
121
|
+
style: { height: `${lanes.length * 28}px` },
|
|
122
|
+
children: [
|
|
123
|
+
phases.map((phase) => {
|
|
124
|
+
const index = lanes.findIndex(
|
|
125
|
+
(lane) => lane.id === (phase.laneId ?? "default")
|
|
126
|
+
);
|
|
127
|
+
if (index === -1) {
|
|
128
|
+
return null;
|
|
129
|
+
}
|
|
130
|
+
return /* @__PURE__ */ jsx(
|
|
131
|
+
PhaseBar,
|
|
132
|
+
{
|
|
133
|
+
laneIndex: index,
|
|
134
|
+
laneTotal: lanes.length,
|
|
135
|
+
phase,
|
|
136
|
+
span,
|
|
137
|
+
start
|
|
138
|
+
},
|
|
139
|
+
phase.id
|
|
140
|
+
);
|
|
141
|
+
}),
|
|
142
|
+
cursorRatio === null ? null : /* @__PURE__ */ jsx(
|
|
143
|
+
"span",
|
|
144
|
+
{
|
|
145
|
+
"aria-hidden": "true",
|
|
146
|
+
className: "absolute top-0 bottom-0 w-px bg-foreground",
|
|
147
|
+
"data-run-timeline-cursor": true,
|
|
148
|
+
style: { left: `${cursorRatio * 100}%` }
|
|
149
|
+
}
|
|
150
|
+
)
|
|
151
|
+
]
|
|
152
|
+
}
|
|
153
|
+
)
|
|
154
|
+
] });
|
|
155
|
+
};
|
|
156
|
+
const resolveLanes = (explicit) => {
|
|
157
|
+
if (explicit && explicit.length > 0) {
|
|
158
|
+
return explicit;
|
|
159
|
+
}
|
|
160
|
+
return [{ id: "default", label: "Run" }];
|
|
161
|
+
};
|
|
162
|
+
const RunTimeline = forwardRef(
|
|
163
|
+
(props, ref) => {
|
|
164
|
+
const {
|
|
165
|
+
className,
|
|
166
|
+
cursor,
|
|
167
|
+
end,
|
|
168
|
+
formatValue,
|
|
169
|
+
labels,
|
|
170
|
+
lanes,
|
|
171
|
+
phases,
|
|
172
|
+
start,
|
|
173
|
+
...rest
|
|
174
|
+
} = props;
|
|
175
|
+
const resolvedLabels = { ...DEFAULT_LABELS, ...labels };
|
|
176
|
+
const safeEnd = end <= start ? start + 1 : end;
|
|
177
|
+
const resolvedLanes = resolveLanes(lanes);
|
|
178
|
+
return /* @__PURE__ */ jsxs(
|
|
179
|
+
"section",
|
|
180
|
+
{
|
|
181
|
+
"aria-label": resolvedLabels.region,
|
|
182
|
+
className: cn("flex w-full flex-col gap-2", className),
|
|
183
|
+
"data-run-timeline": true,
|
|
184
|
+
ref,
|
|
185
|
+
...rest,
|
|
186
|
+
children: [
|
|
187
|
+
/* @__PURE__ */ jsx(
|
|
188
|
+
Endpoints,
|
|
189
|
+
{
|
|
190
|
+
cursor,
|
|
191
|
+
end: safeEnd,
|
|
192
|
+
format: formatValue,
|
|
193
|
+
start
|
|
194
|
+
}
|
|
195
|
+
),
|
|
196
|
+
phases.length === 0 ? /* @__PURE__ */ jsx(
|
|
197
|
+
"p",
|
|
198
|
+
{
|
|
199
|
+
className: "rounded-md border border-border bg-muted/20 px-2 py-3 text-center text-[11px] text-muted-foreground",
|
|
200
|
+
"data-run-timeline-state": "empty",
|
|
201
|
+
children: resolvedLabels.empty
|
|
202
|
+
}
|
|
203
|
+
) : /* @__PURE__ */ jsx(
|
|
204
|
+
TrackBody,
|
|
205
|
+
{
|
|
206
|
+
cursor,
|
|
207
|
+
end: safeEnd,
|
|
208
|
+
lanes: resolvedLanes,
|
|
209
|
+
phases,
|
|
210
|
+
start
|
|
211
|
+
}
|
|
212
|
+
)
|
|
213
|
+
]
|
|
214
|
+
}
|
|
215
|
+
);
|
|
216
|
+
}
|
|
217
|
+
);
|
|
218
|
+
RunTimeline.displayName = "RunTimeline";
|
|
219
|
+
export {
|
|
220
|
+
RunTimeline
|
|
221
|
+
};
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
3
|
+
import {
|
|
4
|
+
forwardRef
|
|
5
|
+
} from "react";
|
|
6
|
+
import { cn } from "../../lib/utils";
|
|
7
|
+
const STATE_LABEL = {
|
|
8
|
+
complete: "Complete",
|
|
9
|
+
failed: "Failed",
|
|
10
|
+
idle: "Idle",
|
|
11
|
+
queued: "Queued",
|
|
12
|
+
running: "Running",
|
|
13
|
+
stopped: "Stopped"
|
|
14
|
+
};
|
|
15
|
+
const STATE_TONE = {
|
|
16
|
+
complete: "border-emerald-500/40 bg-emerald-500/15 text-emerald-700 dark:text-emerald-300",
|
|
17
|
+
failed: "border-red-500/40 bg-red-500/15 text-red-700 dark:text-red-300",
|
|
18
|
+
idle: "border-border bg-muted/40 text-muted-foreground",
|
|
19
|
+
queued: "border-amber-500/40 bg-amber-500/15 text-amber-700 dark:text-amber-300",
|
|
20
|
+
running: "border-blue-500/40 bg-blue-500/15 text-blue-700 dark:text-blue-300",
|
|
21
|
+
stopped: "border-border bg-background text-foreground"
|
|
22
|
+
};
|
|
23
|
+
const STATE_DOT = {
|
|
24
|
+
complete: "bg-emerald-500",
|
|
25
|
+
failed: "bg-red-500",
|
|
26
|
+
idle: "bg-muted-foreground",
|
|
27
|
+
queued: "bg-amber-500",
|
|
28
|
+
running: "bg-blue-500 animate-pulse",
|
|
29
|
+
stopped: "bg-muted-foreground"
|
|
30
|
+
};
|
|
31
|
+
const ANCHOR_TRANSFORM = {
|
|
32
|
+
"bottom-left": "translate(-100%, 100%)",
|
|
33
|
+
"bottom-right": "translate(0%, 100%)",
|
|
34
|
+
"top-left": "translate(-100%, -100%)",
|
|
35
|
+
"top-right": "translate(0%, -100%)"
|
|
36
|
+
};
|
|
37
|
+
const DEFAULT_LABELS = {
|
|
38
|
+
region: "State"
|
|
39
|
+
};
|
|
40
|
+
const StateBadgeOverlay = forwardRef((props, ref) => {
|
|
41
|
+
const {
|
|
42
|
+
anchor = "top-right",
|
|
43
|
+
className,
|
|
44
|
+
label,
|
|
45
|
+
labels,
|
|
46
|
+
state,
|
|
47
|
+
x,
|
|
48
|
+
y,
|
|
49
|
+
...rest
|
|
50
|
+
} = props;
|
|
51
|
+
const resolvedLabels = { ...DEFAULT_LABELS, ...labels };
|
|
52
|
+
const text = label ?? STATE_LABEL[state];
|
|
53
|
+
return /* @__PURE__ */ jsxs(
|
|
54
|
+
"div",
|
|
55
|
+
{
|
|
56
|
+
"aria-label": `${resolvedLabels.region}: ${STATE_LABEL[state]}`,
|
|
57
|
+
className: cn(
|
|
58
|
+
"pointer-events-none absolute z-20 inline-flex items-center gap-1 rounded-full border px-2 py-0.5 text-[10px] font-medium uppercase tracking-wide shadow-sm backdrop-blur",
|
|
59
|
+
STATE_TONE[state],
|
|
60
|
+
className
|
|
61
|
+
),
|
|
62
|
+
"data-state-anchor": anchor,
|
|
63
|
+
"data-state-badge-overlay": true,
|
|
64
|
+
"data-state-name": state,
|
|
65
|
+
ref,
|
|
66
|
+
role: "status",
|
|
67
|
+
style: {
|
|
68
|
+
left: x,
|
|
69
|
+
top: y,
|
|
70
|
+
transform: ANCHOR_TRANSFORM[anchor]
|
|
71
|
+
},
|
|
72
|
+
...rest,
|
|
73
|
+
children: [
|
|
74
|
+
/* @__PURE__ */ jsx(
|
|
75
|
+
"span",
|
|
76
|
+
{
|
|
77
|
+
"aria-hidden": "true",
|
|
78
|
+
className: cn("h-1.5 w-1.5 rounded-full", STATE_DOT[state]),
|
|
79
|
+
"data-state-badge-dot": true
|
|
80
|
+
}
|
|
81
|
+
),
|
|
82
|
+
text
|
|
83
|
+
]
|
|
84
|
+
}
|
|
85
|
+
);
|
|
86
|
+
});
|
|
87
|
+
StateBadgeOverlay.displayName = "StateBadgeOverlay";
|
|
88
|
+
export {
|
|
89
|
+
StateBadgeOverlay
|
|
90
|
+
};
|
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
3
|
+
import {
|
|
4
|
+
forwardRef,
|
|
5
|
+
useId
|
|
6
|
+
} from "react";
|
|
7
|
+
import { cn } from "../../lib/utils";
|
|
8
|
+
const TONE_FILL = {
|
|
9
|
+
danger: "bg-red-500",
|
|
10
|
+
neutral: "bg-foreground",
|
|
11
|
+
primary: "bg-blue-500",
|
|
12
|
+
success: "bg-emerald-500",
|
|
13
|
+
warn: "bg-amber-500"
|
|
14
|
+
};
|
|
15
|
+
const DEFAULT_LABELS = {
|
|
16
|
+
region: "Timeline scrubber"
|
|
17
|
+
};
|
|
18
|
+
const clamp = (v, min, max) => {
|
|
19
|
+
if (v < min) {
|
|
20
|
+
return min;
|
|
21
|
+
}
|
|
22
|
+
if (v > max) {
|
|
23
|
+
return max;
|
|
24
|
+
}
|
|
25
|
+
return v;
|
|
26
|
+
};
|
|
27
|
+
const Labels = (props) => {
|
|
28
|
+
const fmt = props.formatValue;
|
|
29
|
+
return /* @__PURE__ */ jsxs("div", { className: "flex items-baseline justify-between gap-2", children: [
|
|
30
|
+
/* @__PURE__ */ jsx("span", { "data-timeline-scrubber-start": true, children: fmt ? fmt(props.start) : props.start }),
|
|
31
|
+
/* @__PURE__ */ jsx(
|
|
32
|
+
"span",
|
|
33
|
+
{
|
|
34
|
+
className: "font-semibold text-foreground",
|
|
35
|
+
"data-timeline-scrubber-cursor": true,
|
|
36
|
+
children: fmt ? fmt(props.clamped) : props.clamped
|
|
37
|
+
}
|
|
38
|
+
),
|
|
39
|
+
/* @__PURE__ */ jsx("span", { "data-timeline-scrubber-end": true, children: fmt ? fmt(props.end) : props.end })
|
|
40
|
+
] });
|
|
41
|
+
};
|
|
42
|
+
const Track = (props) => /* @__PURE__ */ jsxs("div", { className: "relative h-6", children: [
|
|
43
|
+
/* @__PURE__ */ jsx(
|
|
44
|
+
"span",
|
|
45
|
+
{
|
|
46
|
+
"aria-hidden": "true",
|
|
47
|
+
className: "absolute left-0 right-0 top-1/2 h-1 -translate-y-1/2 rounded-full bg-muted"
|
|
48
|
+
}
|
|
49
|
+
),
|
|
50
|
+
/* @__PURE__ */ jsx(
|
|
51
|
+
"span",
|
|
52
|
+
{
|
|
53
|
+
"aria-hidden": "true",
|
|
54
|
+
className: cn(
|
|
55
|
+
"absolute left-0 top-1/2 h-1 -translate-y-1/2 rounded-full",
|
|
56
|
+
TONE_FILL[props.tone]
|
|
57
|
+
),
|
|
58
|
+
"data-timeline-scrubber-fill": true,
|
|
59
|
+
style: { width: `${props.ratio * 100}%` }
|
|
60
|
+
}
|
|
61
|
+
),
|
|
62
|
+
props.ticks?.map((tick) => /* @__PURE__ */ jsx(
|
|
63
|
+
TickMark,
|
|
64
|
+
{
|
|
65
|
+
scrubberId: props.scrubberId,
|
|
66
|
+
span: props.span,
|
|
67
|
+
start: props.start,
|
|
68
|
+
tick
|
|
69
|
+
},
|
|
70
|
+
tick.id
|
|
71
|
+
)),
|
|
72
|
+
/* @__PURE__ */ jsx(
|
|
73
|
+
"input",
|
|
74
|
+
{
|
|
75
|
+
"aria-label": props.ariaLabel,
|
|
76
|
+
className: "absolute inset-0 h-full w-full cursor-pointer appearance-none bg-transparent focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring",
|
|
77
|
+
"data-timeline-scrubber-input": true,
|
|
78
|
+
id: props.inputId,
|
|
79
|
+
max: props.max,
|
|
80
|
+
min: props.min,
|
|
81
|
+
onChange: props.onChange,
|
|
82
|
+
step: props.step,
|
|
83
|
+
type: "range",
|
|
84
|
+
value: props.value
|
|
85
|
+
}
|
|
86
|
+
)
|
|
87
|
+
] });
|
|
88
|
+
const TickMark = (props) => {
|
|
89
|
+
const { scrubberId, span, start, tick } = props;
|
|
90
|
+
const ratio = clamp((tick.value - start) / span, 0, 1);
|
|
91
|
+
const tone = tick.tone ?? "neutral";
|
|
92
|
+
return /* @__PURE__ */ jsx(
|
|
93
|
+
"span",
|
|
94
|
+
{
|
|
95
|
+
"aria-hidden": "true",
|
|
96
|
+
className: cn(
|
|
97
|
+
"absolute top-1/2 h-2.5 w-px -translate-y-1/2 rounded-full",
|
|
98
|
+
TONE_FILL[tone]
|
|
99
|
+
),
|
|
100
|
+
"data-scrubber-tick": tick.id,
|
|
101
|
+
"data-scrubber-tick-of": scrubberId,
|
|
102
|
+
"data-scrubber-tick-tone": tone,
|
|
103
|
+
style: { left: `${ratio * 100}%` },
|
|
104
|
+
title: typeof tick.label === "string" ? tick.label : void 0
|
|
105
|
+
}
|
|
106
|
+
);
|
|
107
|
+
};
|
|
108
|
+
const TimelineScrubber = forwardRef((props, ref) => {
|
|
109
|
+
const {
|
|
110
|
+
className,
|
|
111
|
+
end,
|
|
112
|
+
formatValue,
|
|
113
|
+
labels,
|
|
114
|
+
onValueChange,
|
|
115
|
+
start,
|
|
116
|
+
step = 1,
|
|
117
|
+
ticks,
|
|
118
|
+
tone = "primary",
|
|
119
|
+
value,
|
|
120
|
+
...rest
|
|
121
|
+
} = props;
|
|
122
|
+
const resolvedLabels = { ...DEFAULT_LABELS, ...labels };
|
|
123
|
+
const inputId = useId();
|
|
124
|
+
const safeEnd = end <= start ? start + 1 : end;
|
|
125
|
+
const span = safeEnd - start;
|
|
126
|
+
const clamped = clamp(value, start, safeEnd);
|
|
127
|
+
const ratio = clamp((clamped - start) / span, 0, 1);
|
|
128
|
+
const handleChange = (event) => {
|
|
129
|
+
onValueChange(clamp(Number(event.target.value), start, safeEnd));
|
|
130
|
+
};
|
|
131
|
+
return /* @__PURE__ */ jsxs(
|
|
132
|
+
"div",
|
|
133
|
+
{
|
|
134
|
+
"aria-label": resolvedLabels.region,
|
|
135
|
+
className: cn(
|
|
136
|
+
"flex w-full flex-col gap-1 text-[11px] text-muted-foreground",
|
|
137
|
+
className
|
|
138
|
+
),
|
|
139
|
+
"data-timeline-scrubber": true,
|
|
140
|
+
"data-timeline-tone": tone,
|
|
141
|
+
ref,
|
|
142
|
+
role: "group",
|
|
143
|
+
...rest,
|
|
144
|
+
children: [
|
|
145
|
+
/* @__PURE__ */ jsx(
|
|
146
|
+
Labels,
|
|
147
|
+
{
|
|
148
|
+
clamped,
|
|
149
|
+
end: safeEnd,
|
|
150
|
+
formatValue,
|
|
151
|
+
start
|
|
152
|
+
}
|
|
153
|
+
),
|
|
154
|
+
/* @__PURE__ */ jsx(
|
|
155
|
+
Track,
|
|
156
|
+
{
|
|
157
|
+
ariaLabel: resolvedLabels.region,
|
|
158
|
+
inputId,
|
|
159
|
+
max: safeEnd,
|
|
160
|
+
min: start,
|
|
161
|
+
onChange: handleChange,
|
|
162
|
+
ratio,
|
|
163
|
+
scrubberId: inputId,
|
|
164
|
+
span,
|
|
165
|
+
start,
|
|
166
|
+
step,
|
|
167
|
+
ticks,
|
|
168
|
+
tone,
|
|
169
|
+
value: clamped
|
|
170
|
+
}
|
|
171
|
+
)
|
|
172
|
+
]
|
|
173
|
+
}
|
|
174
|
+
);
|
|
175
|
+
});
|
|
176
|
+
TimelineScrubber.displayName = "TimelineScrubber";
|
|
177
|
+
export {
|
|
178
|
+
TimelineScrubber
|
|
179
|
+
};
|