@reopt-ai/opt-ui 1.13.0 → 1.15.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.
- package/CHANGELOG.md +45 -0
- package/COMPONENT_CATALOG.md +644 -123
- package/README.md +35 -7
- package/dist/core/index.cjs +4 -4
- package/dist/core/index.d.cts +2 -2
- package/dist/core/index.d.ts +2 -2
- package/dist/core/index.js +2 -2
- package/dist/docs/02-components/01-core.md +261 -58
- package/dist/docs/02-components/02-visuals.md +1 -1
- package/dist/docs/02-components/03-shells.md +391 -65
- package/dist/docs/02-components/04-surfaces.md +1 -1
- package/dist/docs/02-components/index.md +4 -4
- package/dist/id-registry.cjs +32 -0
- package/dist/id-registry.js +32 -0
- package/dist/id-registry.json +68 -0
- package/dist/index.cjs +1196 -427
- package/dist/index.d.cts +208 -19
- package/dist/index.d.ts +208 -19
- package/dist/index.js +1050 -295
- package/dist/{key-pad-menu-DMmbQ1jF.js → key-pad-menu-BCOeYvsH.js} +59 -67
- package/dist/{key-pad-menu-BOVu97bj.d.cts → key-pad-menu-CaIsTHB2.d.cts} +22 -9
- package/dist/{key-pad-menu-0KWOnELe.cjs → key-pad-menu-Jaqxhx9P.cjs} +57 -77
- package/dist/{key-pad-menu-NlZ9zNF8.d.ts → key-pad-menu-odAEycIX.d.ts} +22 -9
- package/dist/meta.cjs +877 -26
- package/dist/meta.js +877 -26
- package/dist/{field-sidebar-CRdToHRO.js → replay-player-layout-BVUNsUS1.js} +904 -203
- package/dist/{field-sidebar-CxxSK4JO.cjs → replay-player-layout-Cmzptq89.cjs} +944 -201
- package/dist/{field-sidebar-D-rwuN_w.d.cts → replay-player-layout-DpqdfdQ6.d.ts} +246 -3
- package/dist/{field-sidebar-Dz7J4ett.d.ts → replay-player-layout-DuZW-gFF.d.cts} +246 -3
- package/dist/shells/index.cjs +58 -52
- package/dist/shells/index.d.cts +2 -2
- package/dist/shells/index.d.ts +2 -2
- package/dist/shells/index.js +2 -2
- package/dist/{text-truncate-Bd-0WkSP.js → text-truncate-B-8nJgnu.js} +200 -101
- package/dist/{text-truncate-eMzvU_7k.d.cts → text-truncate-DOxUijpB.d.cts} +3 -30
- package/dist/{text-truncate-eMzvU_7k.d.ts → text-truncate-DOxUijpB.d.ts} +3 -30
- package/dist/{text-truncate-BeYW6XZu.cjs → text-truncate-gQhpMy-Z.cjs} +214 -97
- package/dist/theme/server.d.cts +2 -2
- package/dist/theme/server.d.ts +2 -2
- package/package.json +2 -2
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
"use client";
|
|
2
|
-
const require_text_truncate = require("./text-truncate-
|
|
2
|
+
const require_text_truncate = require("./text-truncate-gQhpMy-Z.cjs");
|
|
3
3
|
let react_jsx_runtime = require("react/jsx-runtime");
|
|
4
4
|
let react = require("react");
|
|
5
5
|
react = require_text_truncate.__toESM(react, 1);
|
|
@@ -278,7 +278,7 @@ function CommandGroup(_ref5) {
|
|
|
278
278
|
className: "text-text-tertiary px-2 py-1.5 text-xs font-medium",
|
|
279
279
|
children: heading
|
|
280
280
|
}) : void 0,
|
|
281
|
-
className: require_text_truncate.cn("py-1", className)
|
|
281
|
+
className: require_text_truncate.cn("py-1 [&:not(:has([data-slot=command-item]))]:hidden", className)
|
|
282
282
|
}, props), {}, {
|
|
283
283
|
"data-opt-id": "JPK1Y",
|
|
284
284
|
"data-opt-slug": "command.CommandGroup"
|
|
@@ -359,16 +359,20 @@ function CommandPalette({ commands, open, onOpenChange, onSelect, inline = false
|
|
|
359
359
|
"aria-label": resolvedLabels.list,
|
|
360
360
|
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(CommandEmpty, { children: empty !== null && empty !== void 0 ? empty : resolvedLabels.empty }), hasCommands ? Array.from(grouped.entries()).map(([group, items]) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(CommandGroup, {
|
|
361
361
|
heading: group,
|
|
362
|
-
children: items.map((cmd) =>
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
cmd.
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
362
|
+
children: items.map((cmd) => {
|
|
363
|
+
var _cmd$keywords;
|
|
364
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(CommandItem, {
|
|
365
|
+
value: cmd.id,
|
|
366
|
+
keywords: [
|
|
367
|
+
cmd.label,
|
|
368
|
+
cmd.group,
|
|
369
|
+
...(_cmd$keywords = cmd.keywords) !== null && _cmd$keywords !== void 0 ? _cmd$keywords : [],
|
|
370
|
+
...cmd.shortcut ? [cmd.shortcut] : []
|
|
371
|
+
],
|
|
372
|
+
onSelect: handleSelect,
|
|
373
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: cmd.label }), cmd.shortcut && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(CommandShortcut, { children: cmd.shortcut })]
|
|
374
|
+
}, cmd.id);
|
|
375
|
+
})
|
|
372
376
|
}, group)) : null]
|
|
373
377
|
}),
|
|
374
378
|
footer
|
|
@@ -458,9 +462,65 @@ function DashboardGrid({ stats, columns = 3, labels: customLabels }) {
|
|
|
458
462
|
});
|
|
459
463
|
}
|
|
460
464
|
//#endregion
|
|
465
|
+
//#region src/lib/use-async-search.ts
|
|
466
|
+
/** Debounced search with explicit failure and latest-request-only results. */
|
|
467
|
+
function useAsyncSearch({ query, search, enabled, debounceMs }) {
|
|
468
|
+
const [results, setResults] = (0, react.useState)([]);
|
|
469
|
+
const [loading, setLoading] = (0, react.useState)(false);
|
|
470
|
+
const [failed, setFailed] = (0, react.useState)(false);
|
|
471
|
+
const [attempt, setAttempt] = (0, react.useState)(0);
|
|
472
|
+
const generationRef = (0, react.useRef)(0);
|
|
473
|
+
const timerRef = (0, react.useRef)(void 0);
|
|
474
|
+
const invalidate = (0, react.useCallback)(() => {
|
|
475
|
+
generationRef.current += 1;
|
|
476
|
+
clearTimeout(timerRef.current);
|
|
477
|
+
setResults([]);
|
|
478
|
+
setLoading(false);
|
|
479
|
+
setFailed(false);
|
|
480
|
+
}, []);
|
|
481
|
+
(0, react.useEffect)(() => {
|
|
482
|
+
const generation = ++generationRef.current;
|
|
483
|
+
setResults([]);
|
|
484
|
+
setFailed(false);
|
|
485
|
+
setLoading(enabled && Boolean(search));
|
|
486
|
+
if (enabled && search) timerRef.current = setTimeout(async () => {
|
|
487
|
+
try {
|
|
488
|
+
const items = await search(query);
|
|
489
|
+
if (generationRef.current === generation) setResults(items);
|
|
490
|
+
} catch (_unused) {
|
|
491
|
+
if (generationRef.current === generation) setFailed(true);
|
|
492
|
+
} finally {
|
|
493
|
+
if (generationRef.current === generation) setLoading(false);
|
|
494
|
+
}
|
|
495
|
+
}, debounceMs);
|
|
496
|
+
return () => {
|
|
497
|
+
generationRef.current += 1;
|
|
498
|
+
clearTimeout(timerRef.current);
|
|
499
|
+
};
|
|
500
|
+
}, [
|
|
501
|
+
query,
|
|
502
|
+
search,
|
|
503
|
+
enabled,
|
|
504
|
+
debounceMs,
|
|
505
|
+
attempt
|
|
506
|
+
]);
|
|
507
|
+
return {
|
|
508
|
+
results,
|
|
509
|
+
loading,
|
|
510
|
+
failed,
|
|
511
|
+
retry: (0, react.useCallback)(() => {
|
|
512
|
+
invalidate();
|
|
513
|
+
setAttempt((current) => current + 1);
|
|
514
|
+
}, [invalidate]),
|
|
515
|
+
invalidate
|
|
516
|
+
};
|
|
517
|
+
}
|
|
518
|
+
//#endregion
|
|
461
519
|
//#region src/shells/search-combobox.tsx
|
|
462
520
|
const defaultLabels$33 = {
|
|
463
521
|
noResults: "결과가 없습니다.",
|
|
522
|
+
error: "검색에 실패했습니다. 다시 시도해주세요.",
|
|
523
|
+
retry: "다시 시도",
|
|
464
524
|
loading: "Loading…"
|
|
465
525
|
};
|
|
466
526
|
/** Renders the search combobox component. */
|
|
@@ -468,10 +528,13 @@ function SearchCombobox({ items, placeholder = "검색...", label = "검색", on
|
|
|
468
528
|
const labels = require_text_truncate._objectSpread2(require_text_truncate._objectSpread2({}, defaultLabels$33), customLabels);
|
|
469
529
|
const inputId = (0, react.useId)();
|
|
470
530
|
const [query, setQuery] = (0, react.useState)("");
|
|
471
|
-
const
|
|
472
|
-
const
|
|
473
|
-
|
|
474
|
-
|
|
531
|
+
const inputRef = (0, react.useRef)(null);
|
|
532
|
+
const { results: asyncResults, loading, failed, retry, invalidate } = useAsyncSearch({
|
|
533
|
+
query,
|
|
534
|
+
search: onSearch,
|
|
535
|
+
enabled: isAsync && Boolean(query),
|
|
536
|
+
debounceMs
|
|
537
|
+
});
|
|
475
538
|
const syncMatches = (0, react.useMemo)(() => {
|
|
476
539
|
if (isAsync) return [];
|
|
477
540
|
if (!query) return items;
|
|
@@ -482,45 +545,15 @@ function SearchCombobox({ items, placeholder = "검색...", label = "검색", on
|
|
|
482
545
|
isAsync
|
|
483
546
|
]);
|
|
484
547
|
const handleInput = (event) => {
|
|
485
|
-
|
|
548
|
+
inputRef.current = event.currentTarget;
|
|
549
|
+
invalidate();
|
|
486
550
|
setQuery(event.currentTarget.value);
|
|
487
551
|
};
|
|
488
552
|
const handleValueChange = (item) => {
|
|
489
|
-
|
|
553
|
+
invalidate();
|
|
490
554
|
setQuery(item);
|
|
491
555
|
onSelect === null || onSelect === void 0 || onSelect(item);
|
|
492
556
|
};
|
|
493
|
-
(0, react.useEffect)(() => {
|
|
494
|
-
const generation = ++searchGenerationRef.current;
|
|
495
|
-
if (timerRef.current) clearTimeout(timerRef.current);
|
|
496
|
-
if (!isAsync || !onSearch) {
|
|
497
|
-
setLoading(false);
|
|
498
|
-
return;
|
|
499
|
-
}
|
|
500
|
-
if (!query) {
|
|
501
|
-
setAsyncResults([]);
|
|
502
|
-
setLoading(false);
|
|
503
|
-
return;
|
|
504
|
-
}
|
|
505
|
-
setLoading(true);
|
|
506
|
-
timerRef.current = setTimeout(async () => {
|
|
507
|
-
try {
|
|
508
|
-
const results = await onSearch(query);
|
|
509
|
-
if (searchGenerationRef.current !== generation) return;
|
|
510
|
-
setAsyncResults(results);
|
|
511
|
-
} finally {
|
|
512
|
-
if (searchGenerationRef.current === generation) setLoading(false);
|
|
513
|
-
}
|
|
514
|
-
}, debounceMs);
|
|
515
|
-
return () => {
|
|
516
|
-
if (timerRef.current) clearTimeout(timerRef.current);
|
|
517
|
-
};
|
|
518
|
-
}, [
|
|
519
|
-
query,
|
|
520
|
-
isAsync,
|
|
521
|
-
onSearch,
|
|
522
|
-
debounceMs
|
|
523
|
-
]);
|
|
524
557
|
const matches = isAsync ? asyncResults : syncMatches;
|
|
525
558
|
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
526
559
|
className: "gap-element flex flex-col",
|
|
@@ -534,29 +567,42 @@ function SearchCombobox({ items, placeholder = "검색...", label = "검색", on
|
|
|
534
567
|
className: require_text_truncate.OPT_TEXT_LABEL,
|
|
535
568
|
children: label
|
|
536
569
|
}),
|
|
537
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(require_text_truncate.ComboboxInput, {
|
|
570
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(require_text_truncate.ComboboxInput, require_text_truncate._objectSpread2({
|
|
538
571
|
id: inputId,
|
|
539
572
|
placeholder,
|
|
540
573
|
className: "max-w-xs",
|
|
541
574
|
"aria-busy": loading || void 0,
|
|
542
575
|
onInput: handleInput
|
|
543
|
-
}
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
576
|
+
}, failed ? {
|
|
577
|
+
"aria-expanded": false,
|
|
578
|
+
"aria-controls": void 0
|
|
579
|
+
} : {})),
|
|
580
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(require_text_truncate.ComboboxPopover, require_text_truncate._objectSpread2(require_text_truncate._objectSpread2({ className: "max-w-xs" }, failed ? { role: "presentation" } : {}), {}, { children: isAsync && failed ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
581
|
+
role: "alert",
|
|
582
|
+
className: "gap-element flex flex-col px-3 py-2 text-sm",
|
|
583
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: labels.error }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(require_text_truncate.Button, {
|
|
584
|
+
variant: "secondary",
|
|
585
|
+
size: "sm",
|
|
586
|
+
onClick: () => {
|
|
587
|
+
var _inputRef$current;
|
|
588
|
+
retry();
|
|
589
|
+
(_inputRef$current = inputRef.current) === null || _inputRef$current === void 0 || _inputRef$current.focus();
|
|
590
|
+
},
|
|
591
|
+
children: labels.retry
|
|
592
|
+
})]
|
|
593
|
+
}) : isAsync && loading ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
594
|
+
className: "text-text-secondary gap-element flex items-center px-3 py-2 text-sm",
|
|
595
|
+
role: "status",
|
|
596
|
+
"aria-live": "polite",
|
|
597
|
+
"aria-busy": "true",
|
|
598
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(require_text_truncate.Spinner, {
|
|
599
|
+
size: "sm",
|
|
600
|
+
"aria-hidden": "true"
|
|
601
|
+
}), labels.loading]
|
|
602
|
+
}) : matches.length === 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(require_text_truncate.ComboboxEmpty, { children: labels.noResults }) : matches.map((item) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(require_text_truncate.ComboboxItem, {
|
|
603
|
+
value: item,
|
|
604
|
+
children: item
|
|
605
|
+
}, item)) }))
|
|
560
606
|
]
|
|
561
607
|
})
|
|
562
608
|
});
|
|
@@ -901,13 +947,16 @@ function FaqAccordion({ items, labels: customLabels }) {
|
|
|
901
947
|
//#region src/shells/settings-form.tsx
|
|
902
948
|
const defaultLabels$26 = {
|
|
903
949
|
formAriaLabel: "설정 폼",
|
|
950
|
+
submitError: "저장에 실패했습니다. 다시 시도해주세요.",
|
|
904
951
|
submitButton: "저장",
|
|
905
952
|
successMessage: "설정이 저장되었습니다."
|
|
906
953
|
};
|
|
907
954
|
/** Renders the settings form component. */
|
|
908
955
|
function SettingsForm({ fields, onSubmit, labels: customLabels }) {
|
|
956
|
+
var _labels$submitError;
|
|
909
957
|
const labels = require_text_truncate._objectSpread2(require_text_truncate._objectSpread2({}, defaultLabels$26), customLabels);
|
|
910
958
|
const [submitted, setSubmitted] = (0, react.useState)(false);
|
|
959
|
+
const formId = (0, react.useId)();
|
|
911
960
|
const defaultValues = {};
|
|
912
961
|
for (const field of fields) {
|
|
913
962
|
var _field$defaultValue;
|
|
@@ -933,9 +982,10 @@ function SettingsForm({ fields, onSubmit, labels: customLabels }) {
|
|
|
933
982
|
}
|
|
934
983
|
return errors;
|
|
935
984
|
},
|
|
936
|
-
onSubmit: (values) => {
|
|
985
|
+
onSubmit: async (values) => {
|
|
986
|
+
setSubmitted(false);
|
|
987
|
+
await (onSubmit === null || onSubmit === void 0 ? void 0 : onSubmit(values));
|
|
937
988
|
setSubmitted(true);
|
|
938
|
-
onSubmit === null || onSubmit === void 0 || onSubmit(values);
|
|
939
989
|
}
|
|
940
990
|
});
|
|
941
991
|
if (fields.length === 0) return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(require_text_truncate.EmptyState, {
|
|
@@ -948,138 +998,142 @@ function SettingsForm({ fields, onSubmit, labels: customLabels }) {
|
|
|
948
998
|
className: "gap-group flex flex-col",
|
|
949
999
|
"data-opt-id": "9TEEJ",
|
|
950
1000
|
"data-opt-slug": "settings-form.SettingsForm",
|
|
951
|
-
children: [
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(require_text_truncate.
|
|
967
|
-
|
|
968
|
-
children: field.label
|
|
969
|
-
}),
|
|
970
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(require_text_truncate.TagInput, {
|
|
1001
|
+
children: [
|
|
1002
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)(require_text_truncate.FormRoot, {
|
|
1003
|
+
store: form,
|
|
1004
|
+
"aria-label": labels.formAriaLabel,
|
|
1005
|
+
children: [fields.map((field) => {
|
|
1006
|
+
var _field$type;
|
|
1007
|
+
if (field.visible !== void 0) {
|
|
1008
|
+
const currentValues = form.getValues();
|
|
1009
|
+
if (!(typeof field.visible === "function" ? field.visible(currentValues) : field.visible)) return null;
|
|
1010
|
+
}
|
|
1011
|
+
if (field.type === "tags") {
|
|
1012
|
+
const rawValue = form.getValue(field.name);
|
|
1013
|
+
const tags = Array.isArray(rawValue) ? rawValue : [];
|
|
1014
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(require_text_truncate.FormField, {
|
|
1015
|
+
name: field.name,
|
|
1016
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(require_text_truncate.TagInput, {
|
|
1017
|
+
label: field.label,
|
|
971
1018
|
value: tags,
|
|
972
1019
|
onChange: (next) => form.setValue(field.name, next),
|
|
973
|
-
placeholder: field.placeholder
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
1020
|
+
placeholder: field.placeholder,
|
|
1021
|
+
error: form.getFieldTouched(field.name) ? form.getError(field.name) : void 0
|
|
1022
|
+
})
|
|
1023
|
+
}, field.name);
|
|
1024
|
+
}
|
|
1025
|
+
if (field.type === "radio") {
|
|
1026
|
+
var _field$options;
|
|
1027
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("fieldset", {
|
|
1028
|
+
className: "gap-element flex flex-col",
|
|
1029
|
+
children: [
|
|
1030
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("legend", {
|
|
1031
|
+
className: "text-text-primary text-sm font-medium",
|
|
1032
|
+
children: field.label
|
|
1033
|
+
}),
|
|
1034
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(require_text_truncate.FormRadioGroup, { children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
1035
|
+
className: "gap-group flex",
|
|
1036
|
+
children: (_field$options = field.options) === null || _field$options === void 0 ? void 0 : _field$options.map((option) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
|
|
1037
|
+
className: "text-text-primary gap-element flex items-center text-sm",
|
|
1038
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(require_text_truncate.FormRadio, {
|
|
1039
|
+
name: field.name,
|
|
1040
|
+
value: option.value
|
|
1041
|
+
}), option.label]
|
|
1042
|
+
}, option.value))
|
|
1043
|
+
}) }),
|
|
1044
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(require_text_truncate.FormError, { name: field.name })
|
|
1045
|
+
]
|
|
1046
|
+
}, field.name);
|
|
1047
|
+
}
|
|
1048
|
+
if (field.type === "checkbox") return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(require_text_truncate.FormField, {
|
|
1049
|
+
name: field.name,
|
|
1050
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
|
|
1051
|
+
className: "text-text-primary gap-element flex items-center text-sm",
|
|
1052
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(require_text_truncate.FormCheckbox, { name: field.name }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: field.label })]
|
|
1053
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(require_text_truncate.FormError, { name: field.name })]
|
|
977
1054
|
}, field.name);
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
1055
|
+
if (field.type === "textarea") {
|
|
1056
|
+
var _field$rows, _field$resize;
|
|
1057
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(require_text_truncate.FormField, {
|
|
1058
|
+
name: field.name,
|
|
1059
|
+
children: [
|
|
1060
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(require_text_truncate.FormLabel, { children: field.label }),
|
|
1061
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(require_text_truncate.FormTextarea, {
|
|
1062
|
+
name: field.name,
|
|
1063
|
+
placeholder: field.placeholder,
|
|
1064
|
+
rows: (_field$rows = field.rows) !== null && _field$rows !== void 0 ? _field$rows : 4,
|
|
1065
|
+
resize: (_field$resize = field.resize) !== null && _field$resize !== void 0 ? _field$resize : "vertical",
|
|
1066
|
+
className: field.font === "mono" ? "font-mono text-sm" : void 0
|
|
1067
|
+
}),
|
|
1068
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(require_text_truncate.FormError, { name: field.name })
|
|
1069
|
+
]
|
|
1070
|
+
}, field.name);
|
|
1071
|
+
}
|
|
1072
|
+
if (field.type === "select") {
|
|
1073
|
+
var _field$options2;
|
|
1074
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(require_text_truncate.FormField, {
|
|
1075
|
+
name: field.name,
|
|
1076
|
+
children: [
|
|
1077
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(require_text_truncate.FormLabel, {
|
|
1078
|
+
htmlFor: `${formId}-${field.name}`,
|
|
1079
|
+
children: field.label
|
|
1080
|
+
}),
|
|
1081
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(require_text_truncate.FormSelect, {
|
|
1082
|
+
name: field.name,
|
|
1083
|
+
id: `${formId}-${field.name}`,
|
|
1084
|
+
"aria-label": field.label,
|
|
1085
|
+
"aria-describedby": form.getError(field.name) && form.getFieldTouched(field.name) ? `${formId}-${field.name}-error` : void 0,
|
|
1086
|
+
placeholder: field.placeholder,
|
|
1087
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(require_text_truncate.FormSelectPopover, { children: (_field$options2 = field.options) === null || _field$options2 === void 0 ? void 0 : _field$options2.map((option) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(require_text_truncate.FormSelectItem, {
|
|
1088
|
+
value: option.value,
|
|
1089
|
+
children: option.label
|
|
1090
|
+
}, option.value)) })
|
|
1091
|
+
}),
|
|
1092
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(require_text_truncate.FormError, {
|
|
1093
|
+
name: field.name,
|
|
1094
|
+
id: `${formId}-${field.name}-error`
|
|
1095
|
+
})
|
|
1096
|
+
]
|
|
1097
|
+
}, field.name);
|
|
1098
|
+
}
|
|
1099
|
+
if (field.type === "switch") return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
|
|
1100
|
+
className: "gap-group flex items-center",
|
|
1101
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(require_text_truncate.FormSwitch, { name: field.name }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1102
|
+
className: "flex flex-col",
|
|
1103
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
985
1104
|
className: "text-text-primary text-sm font-medium",
|
|
986
1105
|
children: field.label
|
|
987
|
-
}),
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(require_text_truncate.FormRadio, {
|
|
993
|
-
name: field.name,
|
|
994
|
-
value: option.value
|
|
995
|
-
}), option.label]
|
|
996
|
-
}, option.value))
|
|
997
|
-
}) }),
|
|
998
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(require_text_truncate.FormError, { name: field.name })
|
|
999
|
-
]
|
|
1000
|
-
}, field.name);
|
|
1001
|
-
}
|
|
1002
|
-
if (field.type === "checkbox") return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1003
|
-
className: "gap-element flex flex-col",
|
|
1004
|
-
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
|
|
1005
|
-
className: "text-text-primary gap-element flex items-center text-sm",
|
|
1006
|
-
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(require_text_truncate.FormCheckbox, { name: field.name }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: field.label })]
|
|
1007
|
-
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(require_text_truncate.FormError, { name: field.name })]
|
|
1008
|
-
}, field.name);
|
|
1009
|
-
if (field.type === "textarea") {
|
|
1010
|
-
var _field$rows, _field$resize;
|
|
1011
|
-
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1012
|
-
className: "gap-element flex flex-col",
|
|
1013
|
-
children: [
|
|
1014
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(require_text_truncate.FormLabel, {
|
|
1015
|
-
name: field.name,
|
|
1016
|
-
children: field.label
|
|
1017
|
-
}),
|
|
1018
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(require_text_truncate.FormTextarea, {
|
|
1019
|
-
name: field.name,
|
|
1020
|
-
placeholder: field.placeholder,
|
|
1021
|
-
rows: (_field$rows = field.rows) !== null && _field$rows !== void 0 ? _field$rows : 4,
|
|
1022
|
-
resize: (_field$resize = field.resize) !== null && _field$resize !== void 0 ? _field$resize : "vertical",
|
|
1023
|
-
className: field.font === "mono" ? "font-mono text-sm" : void 0
|
|
1024
|
-
}),
|
|
1025
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(require_text_truncate.FormError, { name: field.name })
|
|
1026
|
-
]
|
|
1106
|
+
}), field.description && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1107
|
+
className: "text-text-secondary text-sm",
|
|
1108
|
+
children: field.description
|
|
1109
|
+
})]
|
|
1110
|
+
})]
|
|
1027
1111
|
}, field.name);
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
var _field$options2;
|
|
1031
|
-
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1032
|
-
className: "gap-element flex flex-col",
|
|
1112
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(require_text_truncate.FormField, {
|
|
1113
|
+
name: field.name,
|
|
1033
1114
|
children: [
|
|
1034
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(require_text_truncate.FormLabel, {
|
|
1035
|
-
|
|
1036
|
-
children: field.label
|
|
1037
|
-
}),
|
|
1038
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(require_text_truncate.FormSelect, {
|
|
1115
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(require_text_truncate.FormLabel, { children: field.label }),
|
|
1116
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(require_text_truncate.FormInput, {
|
|
1039
1117
|
name: field.name,
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
value: option.value,
|
|
1043
|
-
children: option.label
|
|
1044
|
-
}, option.value)) })
|
|
1118
|
+
type: (_field$type = field.type) !== null && _field$type !== void 0 ? _field$type : "text",
|
|
1119
|
+
placeholder: field.placeholder
|
|
1045
1120
|
}),
|
|
1046
1121
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(require_text_truncate.FormError, { name: field.name })
|
|
1047
1122
|
]
|
|
1048
1123
|
}, field.name);
|
|
1049
|
-
}
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
}, field.name);
|
|
1063
|
-
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1064
|
-
className: "gap-element flex flex-col",
|
|
1065
|
-
children: [
|
|
1066
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(require_text_truncate.FormLabel, {
|
|
1067
|
-
name: field.name,
|
|
1068
|
-
children: field.label
|
|
1069
|
-
}),
|
|
1070
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(require_text_truncate.FormInput, {
|
|
1071
|
-
name: field.name,
|
|
1072
|
-
type: (_field$type = field.type) !== null && _field$type !== void 0 ? _field$type : "text",
|
|
1073
|
-
placeholder: field.placeholder
|
|
1074
|
-
}),
|
|
1075
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(require_text_truncate.FormError, { name: field.name })
|
|
1076
|
-
]
|
|
1077
|
-
}, field.name);
|
|
1078
|
-
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(require_text_truncate.FormSubmit, { children: labels.submitButton })]
|
|
1079
|
-
}), submitted && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
1080
|
-
className: "border-success bg-success-subtle text-success-fg rounded-lg border p-3 text-sm",
|
|
1081
|
-
children: labels.successMessage
|
|
1082
|
-
})]
|
|
1124
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(require_text_truncate.FormSubmit, { children: labels.submitButton })]
|
|
1125
|
+
}),
|
|
1126
|
+
form.getSubmitError() !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
1127
|
+
role: "alert",
|
|
1128
|
+
className: "border-danger bg-danger-subtle text-danger-fg rounded-lg border p-3 text-sm",
|
|
1129
|
+
children: (_labels$submitError = labels.submitError) !== null && _labels$submitError !== void 0 ? _labels$submitError : "처리에 실패했습니다. 다시 시도해주세요."
|
|
1130
|
+
}),
|
|
1131
|
+
submitted && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
1132
|
+
role: "status",
|
|
1133
|
+
className: "border-success bg-success-subtle text-success-fg rounded-lg border p-3 text-sm",
|
|
1134
|
+
children: labels.successMessage
|
|
1135
|
+
})
|
|
1136
|
+
]
|
|
1083
1137
|
});
|
|
1084
1138
|
}
|
|
1085
1139
|
//#endregion
|
|
@@ -4621,18 +4675,6 @@ function QueryHistoryList({ items, loading = false, onLoad, onRerun, onClear, he
|
|
|
4621
4675
|
"data-opt-slug": "query-history-list.QueryHistoryList",
|
|
4622
4676
|
children: [
|
|
4623
4677
|
header,
|
|
4624
|
-
onClear && items.length > 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
4625
|
-
className: "flex flex-none justify-end px-2 pt-1",
|
|
4626
|
-
children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(require_text_truncate.Button, {
|
|
4627
|
-
variant: "ghost",
|
|
4628
|
-
size: "sm",
|
|
4629
|
-
onClick: onClear,
|
|
4630
|
-
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(lucide_react.Trash2, {
|
|
4631
|
-
className: "size-3.5",
|
|
4632
|
-
"aria-hidden": true
|
|
4633
|
-
}), labels.clear]
|
|
4634
|
-
})
|
|
4635
|
-
}),
|
|
4636
4678
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
4637
4679
|
className: "min-h-0 flex-1 overflow-y-auto",
|
|
4638
4680
|
children: loading ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
@@ -4695,6 +4737,19 @@ function QueryHistoryList({ items, loading = false, onLoad, onRerun, onClear, he
|
|
|
4695
4737
|
}, item.id);
|
|
4696
4738
|
})
|
|
4697
4739
|
})
|
|
4740
|
+
}),
|
|
4741
|
+
onClear && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
4742
|
+
className: "border-border-subtle flex flex-none justify-end border-t px-2 py-1",
|
|
4743
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(require_text_truncate.Button, {
|
|
4744
|
+
variant: "ghost",
|
|
4745
|
+
size: "sm",
|
|
4746
|
+
onClick: onClear,
|
|
4747
|
+
disabled: items.length === 0,
|
|
4748
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(lucide_react.Trash2, {
|
|
4749
|
+
className: "size-3.5",
|
|
4750
|
+
"aria-hidden": true
|
|
4751
|
+
}), labels.clear]
|
|
4752
|
+
})
|
|
4698
4753
|
})
|
|
4699
4754
|
]
|
|
4700
4755
|
});
|
|
@@ -8576,6 +8631,652 @@ function FieldValues({ values, loading, total, sampledRows, onFilter, labels })
|
|
|
8576
8631
|
});
|
|
8577
8632
|
}
|
|
8578
8633
|
//#endregion
|
|
8634
|
+
//#region src/shells/replay-scrubber.tsx
|
|
8635
|
+
const MARKER_MERGE_PX = 14;
|
|
8636
|
+
function defaultFormatTime(offset) {
|
|
8637
|
+
const total = Math.max(0, Math.floor(offset / 1e3));
|
|
8638
|
+
const minutes = Math.floor(total / 60);
|
|
8639
|
+
const seconds = total % 60;
|
|
8640
|
+
return `${minutes}:${String(seconds).padStart(2, "0")}`;
|
|
8641
|
+
}
|
|
8642
|
+
const DEFAULT_SEGMENT_NAMES = {
|
|
8643
|
+
active: "Active",
|
|
8644
|
+
inactive: "Inactive",
|
|
8645
|
+
gap: "No data",
|
|
8646
|
+
buffer: "Loading"
|
|
8647
|
+
};
|
|
8648
|
+
const SEGMENT_CLASS = {
|
|
8649
|
+
active: "bg-accent/35",
|
|
8650
|
+
inactive: "bg-bg-muted",
|
|
8651
|
+
gap: "bg-transparent",
|
|
8652
|
+
buffer: "bg-[repeating-linear-gradient(45deg,transparent_0,transparent_3px,var(--color-border)_3px,var(--color-border)_6px)]"
|
|
8653
|
+
};
|
|
8654
|
+
const MARKER_CLASS = {
|
|
8655
|
+
default: "bg-text-tertiary",
|
|
8656
|
+
accent: "bg-accent",
|
|
8657
|
+
danger: "bg-danger",
|
|
8658
|
+
warning: "bg-warning"
|
|
8659
|
+
};
|
|
8660
|
+
/**
|
|
8661
|
+
* Merge markers that would render within `mergePx` of each other. Each group
|
|
8662
|
+
* anchors on its first marker; the tooltip lists every member.
|
|
8663
|
+
*/
|
|
8664
|
+
function groupReplayMarkers(markers, start, end, widthPx, mergePx = MARKER_MERGE_PX) {
|
|
8665
|
+
const span = Math.max(1, end - start);
|
|
8666
|
+
const threshold = widthPx > 0 ? mergePx / widthPx * span : 0;
|
|
8667
|
+
const sorted = [...markers].filter((marker) => marker.time >= start && marker.time <= end).sort((a, b) => a.time - b.time);
|
|
8668
|
+
const groups = [];
|
|
8669
|
+
for (const marker of sorted) {
|
|
8670
|
+
var _marker$tone;
|
|
8671
|
+
const last = groups[groups.length - 1];
|
|
8672
|
+
if (last && marker.time - last.time <= threshold) {
|
|
8673
|
+
last.items.push(marker);
|
|
8674
|
+
if (marker.tone === "danger") last.tone = "danger";
|
|
8675
|
+
else if (marker.tone === "warning" && last.tone !== "danger") last.tone = "warning";
|
|
8676
|
+
continue;
|
|
8677
|
+
}
|
|
8678
|
+
groups.push({
|
|
8679
|
+
key: marker.id,
|
|
8680
|
+
time: marker.time,
|
|
8681
|
+
items: [marker],
|
|
8682
|
+
tone: (_marker$tone = marker.tone) !== null && _marker$tone !== void 0 ? _marker$tone : "default"
|
|
8683
|
+
});
|
|
8684
|
+
}
|
|
8685
|
+
return groups;
|
|
8686
|
+
}
|
|
8687
|
+
/** Build the SVG area path for the activity band (log scale, like a seismograph). */
|
|
8688
|
+
function activityPath(activity) {
|
|
8689
|
+
if (!activity.length) return "";
|
|
8690
|
+
const n = activity.length;
|
|
8691
|
+
return `M 0,100 ${activity.map((value, index) => {
|
|
8692
|
+
const x = n === 1 ? 50 : index / (n - 1) * 100;
|
|
8693
|
+
const y = 100 - Math.log1p(Math.min(1, Math.max(0, value)) * 9) / Math.log(10) * 100;
|
|
8694
|
+
return `L ${x.toFixed(2)},${y.toFixed(2)}`;
|
|
8695
|
+
}).join(" ")} L 100,100 Z`;
|
|
8696
|
+
}
|
|
8697
|
+
/**
|
|
8698
|
+
* ReplayScrubber — the timeline of a session recording.
|
|
8699
|
+
*
|
|
8700
|
+
* A plain `Slider` knows only a position. A recording also has stretches
|
|
8701
|
+
* where nothing happened, stretches that have not loaded, events worth
|
|
8702
|
+
* jumping to, and a rhythm of activity. This track layers all of those
|
|
8703
|
+
* under one playhead and commits a seek only when the interaction ends,
|
|
8704
|
+
* so dragging never floods the player.
|
|
8705
|
+
*/
|
|
8706
|
+
function ReplayScrubber({ start, end, position, segments = [], buffered = [], activity, markers = [], onSeek, onPreview, onMarkerSelect, formatTime, stepMs = 5e3, largeStepMs = 3e4, disabled = false, labels, className }) {
|
|
8707
|
+
var _labels$track, _labels$markerGroup;
|
|
8708
|
+
const id = (0, react.useId)();
|
|
8709
|
+
const trackRef = (0, react.useRef)(null);
|
|
8710
|
+
const [width, setWidth] = (0, react.useState)(0);
|
|
8711
|
+
const [hover, setHover] = (0, react.useState)(null);
|
|
8712
|
+
const [dragging, setDragging] = (0, react.useState)(null);
|
|
8713
|
+
const span = Math.max(1, end - start);
|
|
8714
|
+
const clamp = (0, react.useCallback)((time) => Math.min(end, Math.max(start, time)), [start, end]);
|
|
8715
|
+
const format = (0, react.useCallback)((time) => formatTime ? formatTime(time) : defaultFormatTime(time - start), [formatTime, start]);
|
|
8716
|
+
const shown = clamp(dragging !== null && dragging !== void 0 ? dragging : position);
|
|
8717
|
+
const percent = (shown - start) / span * 100;
|
|
8718
|
+
(0, react.useEffect)(() => {
|
|
8719
|
+
const node = trackRef.current;
|
|
8720
|
+
if (!node || typeof ResizeObserver === "undefined") {
|
|
8721
|
+
var _node$clientWidth;
|
|
8722
|
+
setWidth((_node$clientWidth = node === null || node === void 0 ? void 0 : node.clientWidth) !== null && _node$clientWidth !== void 0 ? _node$clientWidth : 0);
|
|
8723
|
+
return;
|
|
8724
|
+
}
|
|
8725
|
+
const observer = new ResizeObserver((entries) => {
|
|
8726
|
+
const entry = entries[0];
|
|
8727
|
+
if (entry) setWidth(entry.contentRect.width);
|
|
8728
|
+
});
|
|
8729
|
+
observer.observe(node);
|
|
8730
|
+
setWidth(node.clientWidth);
|
|
8731
|
+
return () => observer.disconnect();
|
|
8732
|
+
}, []);
|
|
8733
|
+
const timeAt = (0, react.useCallback)((clientX) => {
|
|
8734
|
+
var _trackRef$current;
|
|
8735
|
+
const rect = (_trackRef$current = trackRef.current) === null || _trackRef$current === void 0 ? void 0 : _trackRef$current.getBoundingClientRect();
|
|
8736
|
+
if (!rect || rect.width === 0) return start;
|
|
8737
|
+
return start + Math.min(1, Math.max(0, (clientX - rect.left) / rect.width)) * span;
|
|
8738
|
+
}, [start, span]);
|
|
8739
|
+
const handlePointerDown = (event) => {
|
|
8740
|
+
var _event$currentTarget$, _event$currentTarget;
|
|
8741
|
+
if (disabled || event.button !== 0) return;
|
|
8742
|
+
event.preventDefault();
|
|
8743
|
+
(_event$currentTarget$ = (_event$currentTarget = event.currentTarget).setPointerCapture) === null || _event$currentTarget$ === void 0 || _event$currentTarget$.call(_event$currentTarget, event.pointerId);
|
|
8744
|
+
event.currentTarget.focus();
|
|
8745
|
+
const time = timeAt(event.clientX);
|
|
8746
|
+
setDragging(time);
|
|
8747
|
+
onPreview === null || onPreview === void 0 || onPreview(time);
|
|
8748
|
+
};
|
|
8749
|
+
const handlePointerMove = (event) => {
|
|
8750
|
+
if (disabled) return;
|
|
8751
|
+
const time = timeAt(event.clientX);
|
|
8752
|
+
if (dragging !== null) {
|
|
8753
|
+
setDragging(time);
|
|
8754
|
+
onPreview === null || onPreview === void 0 || onPreview(time);
|
|
8755
|
+
} else {
|
|
8756
|
+
setHover(time);
|
|
8757
|
+
onPreview === null || onPreview === void 0 || onPreview(time);
|
|
8758
|
+
}
|
|
8759
|
+
};
|
|
8760
|
+
const handlePointerUp = (event) => {
|
|
8761
|
+
var _event$currentTarget$2, _event$currentTarget2;
|
|
8762
|
+
if (dragging === null) return;
|
|
8763
|
+
(_event$currentTarget$2 = (_event$currentTarget2 = event.currentTarget).releasePointerCapture) === null || _event$currentTarget$2 === void 0 || _event$currentTarget$2.call(_event$currentTarget2, event.pointerId);
|
|
8764
|
+
const time = clamp(timeAt(event.clientX));
|
|
8765
|
+
setDragging(null);
|
|
8766
|
+
onSeek(time);
|
|
8767
|
+
};
|
|
8768
|
+
const handlePointerCancel = () => {
|
|
8769
|
+
setDragging(null);
|
|
8770
|
+
onPreview === null || onPreview === void 0 || onPreview(null);
|
|
8771
|
+
};
|
|
8772
|
+
const handlePointerLeave = () => {
|
|
8773
|
+
setHover(null);
|
|
8774
|
+
if (dragging === null) onPreview === null || onPreview === void 0 || onPreview(null);
|
|
8775
|
+
};
|
|
8776
|
+
const handleKeyDown = (event) => {
|
|
8777
|
+
if (disabled) return;
|
|
8778
|
+
let next = null;
|
|
8779
|
+
switch (event.key) {
|
|
8780
|
+
case "ArrowLeft":
|
|
8781
|
+
case "ArrowDown":
|
|
8782
|
+
next = shown - (event.shiftKey ? largeStepMs : stepMs);
|
|
8783
|
+
break;
|
|
8784
|
+
case "ArrowRight":
|
|
8785
|
+
case "ArrowUp":
|
|
8786
|
+
next = shown + (event.shiftKey ? largeStepMs : stepMs);
|
|
8787
|
+
break;
|
|
8788
|
+
case "PageDown":
|
|
8789
|
+
next = shown - largeStepMs;
|
|
8790
|
+
break;
|
|
8791
|
+
case "PageUp":
|
|
8792
|
+
next = shown + largeStepMs;
|
|
8793
|
+
break;
|
|
8794
|
+
case "Home":
|
|
8795
|
+
next = start;
|
|
8796
|
+
break;
|
|
8797
|
+
case "End":
|
|
8798
|
+
next = end;
|
|
8799
|
+
break;
|
|
8800
|
+
default: return;
|
|
8801
|
+
}
|
|
8802
|
+
event.preventDefault();
|
|
8803
|
+
onSeek(clamp(next));
|
|
8804
|
+
};
|
|
8805
|
+
const groups = (0, react.useMemo)(() => groupReplayMarkers(markers, start, end, width), [
|
|
8806
|
+
markers,
|
|
8807
|
+
start,
|
|
8808
|
+
end,
|
|
8809
|
+
width
|
|
8810
|
+
]);
|
|
8811
|
+
const path = (0, react.useMemo)(() => activity ? activityPath(activity) : "", [activity]);
|
|
8812
|
+
const trackLabel = (_labels$track = labels === null || labels === void 0 ? void 0 : labels.track) !== null && _labels$track !== void 0 ? _labels$track : "Replay position";
|
|
8813
|
+
const segmentNames = require_text_truncate._objectSpread2(require_text_truncate._objectSpread2({}, DEFAULT_SEGMENT_NAMES), labels === null || labels === void 0 ? void 0 : labels.segment);
|
|
8814
|
+
const groupLabel = (_labels$markerGroup = labels === null || labels === void 0 ? void 0 : labels.markerGroup) !== null && _labels$markerGroup !== void 0 ? _labels$markerGroup : ((count) => `${count} events`);
|
|
8815
|
+
const preview = dragging !== null && dragging !== void 0 ? dragging : hover;
|
|
8816
|
+
const previewPercent = preview === null ? null : (clamp(preview) - start) / span * 100;
|
|
8817
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
8818
|
+
className: require_text_truncate.cn("relative flex w-full flex-col", className),
|
|
8819
|
+
"data-opt-id": "B98JP",
|
|
8820
|
+
"data-opt-slug": "replay-scrubber.ReplayScrubber",
|
|
8821
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
8822
|
+
className: "relative h-3",
|
|
8823
|
+
"aria-hidden": groups.length === 0,
|
|
8824
|
+
children: groups.map((group) => {
|
|
8825
|
+
const left = (group.time - start) / span * 100;
|
|
8826
|
+
const title = group.items.length === 1 ? group.items[0].label : `${groupLabel(group.items.length)}\n${group.items.slice(0, 8).map((item) => `${format(item.time)} ${item.label}`).join("\n")}${group.items.length > 8 ? "\n…" : ""}`;
|
|
8827
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
8828
|
+
type: "button",
|
|
8829
|
+
title,
|
|
8830
|
+
"aria-label": group.items.length === 1 ? `${format(group.time)} · ${group.items[0].label}` : `${format(group.time)} · ${groupLabel(group.items.length)}`,
|
|
8831
|
+
disabled,
|
|
8832
|
+
onClick: (event) => {
|
|
8833
|
+
event.stopPropagation();
|
|
8834
|
+
const marker = group.items[0];
|
|
8835
|
+
if (onMarkerSelect) onMarkerSelect(marker);
|
|
8836
|
+
else onSeek(clamp(marker.time));
|
|
8837
|
+
},
|
|
8838
|
+
className: require_text_truncate.cn("absolute top-0 flex h-3 w-3 -translate-x-1/2 items-center justify-center rounded-sm", require_text_truncate.OPT_FOCUS_VISIBLE),
|
|
8839
|
+
style: { left: `${left}%` },
|
|
8840
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { className: require_text_truncate.cn("block h-2.5 rounded-sm", group.items.length > 1 ? "w-2" : "w-[3px]", MARKER_CLASS[group.tone]) })
|
|
8841
|
+
}, group.key);
|
|
8842
|
+
})
|
|
8843
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
8844
|
+
ref: trackRef,
|
|
8845
|
+
id,
|
|
8846
|
+
role: "slider",
|
|
8847
|
+
tabIndex: disabled ? -1 : 0,
|
|
8848
|
+
"aria-label": trackLabel,
|
|
8849
|
+
"aria-valuemin": start,
|
|
8850
|
+
"aria-valuemax": end,
|
|
8851
|
+
"aria-valuenow": shown,
|
|
8852
|
+
"aria-valuetext": format(shown),
|
|
8853
|
+
"aria-disabled": disabled || void 0,
|
|
8854
|
+
onPointerDown: handlePointerDown,
|
|
8855
|
+
onPointerMove: handlePointerMove,
|
|
8856
|
+
onPointerUp: handlePointerUp,
|
|
8857
|
+
onPointerCancel: handlePointerCancel,
|
|
8858
|
+
onPointerLeave: handlePointerLeave,
|
|
8859
|
+
onKeyDown: handleKeyDown,
|
|
8860
|
+
className: require_text_truncate.cn("group relative h-6 w-full touch-none select-none rounded", disabled ? "cursor-not-allowed opacity-50" : "cursor-pointer", require_text_truncate.OPT_FOCUS_VISIBLE),
|
|
8861
|
+
children: [
|
|
8862
|
+
path ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("svg", {
|
|
8863
|
+
"aria-hidden": "true",
|
|
8864
|
+
viewBox: "0 0 100 100",
|
|
8865
|
+
preserveAspectRatio: "none",
|
|
8866
|
+
className: "pointer-events-none absolute inset-x-0 bottom-1 h-5 w-full opacity-40 transition-opacity group-hover:opacity-80",
|
|
8867
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", {
|
|
8868
|
+
d: path,
|
|
8869
|
+
className: "fill-accent/40"
|
|
8870
|
+
})
|
|
8871
|
+
}) : null,
|
|
8872
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
8873
|
+
className: "bg-bg-subtle absolute inset-x-0 bottom-0 h-2 overflow-hidden rounded-full",
|
|
8874
|
+
children: [
|
|
8875
|
+
segments.map((segment, index) => {
|
|
8876
|
+
const left = (segment.start - start) / span * 100;
|
|
8877
|
+
const w = (segment.end - segment.start) / span * 100;
|
|
8878
|
+
if (w <= 0) return null;
|
|
8879
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
8880
|
+
role: "img",
|
|
8881
|
+
"aria-label": `${segmentNames[segment.kind]} ${format(segment.start)}–${format(segment.end)}`,
|
|
8882
|
+
className: require_text_truncate.cn("absolute inset-y-0", SEGMENT_CLASS[segment.kind]),
|
|
8883
|
+
style: {
|
|
8884
|
+
left: `${left}%`,
|
|
8885
|
+
width: `${w}%`
|
|
8886
|
+
}
|
|
8887
|
+
}, `${segment.kind}-${index}`);
|
|
8888
|
+
}),
|
|
8889
|
+
buffered.map((range, index) => {
|
|
8890
|
+
const left = (range.start - start) / span * 100;
|
|
8891
|
+
const w = (range.end - range.start) / span * 100;
|
|
8892
|
+
if (w <= 0) return null;
|
|
8893
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
8894
|
+
"aria-hidden": "true",
|
|
8895
|
+
className: "bg-text-tertiary/25 absolute inset-y-0",
|
|
8896
|
+
style: {
|
|
8897
|
+
left: `${left}%`,
|
|
8898
|
+
width: `${w}%`
|
|
8899
|
+
}
|
|
8900
|
+
}, `buffered-${index}`);
|
|
8901
|
+
}),
|
|
8902
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
8903
|
+
"aria-hidden": "true",
|
|
8904
|
+
className: "bg-accent/80 absolute inset-y-0 left-0",
|
|
8905
|
+
style: { width: `${percent}%` }
|
|
8906
|
+
})
|
|
8907
|
+
]
|
|
8908
|
+
}),
|
|
8909
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
8910
|
+
"aria-hidden": "true",
|
|
8911
|
+
className: "border-surface bg-accent pointer-events-none absolute bottom-0 h-4 w-4 -translate-x-1/2 translate-y-1/4 rounded-full border-2 shadow-sm",
|
|
8912
|
+
style: { left: `${percent}%` }
|
|
8913
|
+
}),
|
|
8914
|
+
previewPercent !== null && !disabled ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("output", {
|
|
8915
|
+
"aria-hidden": "true",
|
|
8916
|
+
className: require_text_truncate.cn("pointer-events-none absolute -top-6 -translate-x-1/2 rounded bg-surface-overlay px-1.5 py-0.5 text-[11px] font-medium tabular-nums shadow", require_text_truncate.OPT_TEXT_SECONDARY),
|
|
8917
|
+
style: { left: `${Math.min(97, Math.max(3, previewPercent))}%` },
|
|
8918
|
+
children: format(clamp(preview))
|
|
8919
|
+
}) : null
|
|
8920
|
+
]
|
|
8921
|
+
})]
|
|
8922
|
+
});
|
|
8923
|
+
}
|
|
8924
|
+
ReplayScrubber.displayName = "ReplayScrubber";
|
|
8925
|
+
//#endregion
|
|
8926
|
+
//#region src/shells/replay-transport.tsx
|
|
8927
|
+
const DEFAULT_SPEEDS = [
|
|
8928
|
+
.5,
|
|
8929
|
+
1,
|
|
8930
|
+
1.5,
|
|
8931
|
+
2,
|
|
8932
|
+
3,
|
|
8933
|
+
4,
|
|
8934
|
+
8,
|
|
8935
|
+
16
|
|
8936
|
+
];
|
|
8937
|
+
/** `m:ss` for anything under an hour, `h:mm:ss` beyond it. */
|
|
8938
|
+
function formatReplayElapsed(ms) {
|
|
8939
|
+
const total = Math.max(0, Math.floor(ms / 1e3));
|
|
8940
|
+
const hours = Math.floor(total / 3600);
|
|
8941
|
+
const minutes = Math.floor(total % 3600 / 60);
|
|
8942
|
+
const seconds = total % 60;
|
|
8943
|
+
const mmss = `${hours ? String(minutes).padStart(2, "0") : minutes}:${String(seconds).padStart(2, "0")}`;
|
|
8944
|
+
return hours ? `${hours}:${mmss}` : mmss;
|
|
8945
|
+
}
|
|
8946
|
+
function defaultClock(time) {
|
|
8947
|
+
return new Date(time).toLocaleTimeString(void 0, {
|
|
8948
|
+
hour: "2-digit",
|
|
8949
|
+
minute: "2-digit",
|
|
8950
|
+
second: "2-digit"
|
|
8951
|
+
});
|
|
8952
|
+
}
|
|
8953
|
+
/**
|
|
8954
|
+
* ReplayTransport — play/pause, jumps, speed and skip-inactivity for a
|
|
8955
|
+
* session recording.
|
|
8956
|
+
*
|
|
8957
|
+
* It renders controls only; the host owns playback. Timecode shows elapsed
|
|
8958
|
+
* time by default and can flip to the clock time of the visitor's session,
|
|
8959
|
+
* which is what an engineer needs when matching a recording against logs.
|
|
8960
|
+
*/
|
|
8961
|
+
function ReplayTransport({ playing, ended = false, position, start, end, onTogglePlay, onSkip, onSkipToStart, jumpMs = 1e4, speed, speeds = DEFAULT_SPEEDS, onSpeedChange, skipInactive, onSkipInactiveChange, fullscreen, onFullscreenChange, timecode = "elapsed", onTimecodeChange, formatClock = defaultClock, disabled = false, children, labels, shortcuts, className }) {
|
|
8962
|
+
var _labels$play, _labels$pause, _labels$restart, _labels$skipToStart, _labels$back, _labels$forward, _labels$speed, _labels$skipInactive, _labels$enterFullscre, _labels$exitFullscree, _labels$elapsed, _labels$clock;
|
|
8963
|
+
const jumpSeconds = Math.round(jumpMs / 1e3);
|
|
8964
|
+
const text = {
|
|
8965
|
+
play: (_labels$play = labels === null || labels === void 0 ? void 0 : labels.play) !== null && _labels$play !== void 0 ? _labels$play : "Play",
|
|
8966
|
+
pause: (_labels$pause = labels === null || labels === void 0 ? void 0 : labels.pause) !== null && _labels$pause !== void 0 ? _labels$pause : "Pause",
|
|
8967
|
+
restart: (_labels$restart = labels === null || labels === void 0 ? void 0 : labels.restart) !== null && _labels$restart !== void 0 ? _labels$restart : "Restart",
|
|
8968
|
+
skipToStart: (_labels$skipToStart = labels === null || labels === void 0 ? void 0 : labels.skipToStart) !== null && _labels$skipToStart !== void 0 ? _labels$skipToStart : "Go to start",
|
|
8969
|
+
back: (_labels$back = labels === null || labels === void 0 ? void 0 : labels.back) !== null && _labels$back !== void 0 ? _labels$back : ((s) => `Back ${s}s`),
|
|
8970
|
+
forward: (_labels$forward = labels === null || labels === void 0 ? void 0 : labels.forward) !== null && _labels$forward !== void 0 ? _labels$forward : ((s) => `Forward ${s}s`),
|
|
8971
|
+
speed: (_labels$speed = labels === null || labels === void 0 ? void 0 : labels.speed) !== null && _labels$speed !== void 0 ? _labels$speed : "Playback speed",
|
|
8972
|
+
skipInactive: (_labels$skipInactive = labels === null || labels === void 0 ? void 0 : labels.skipInactive) !== null && _labels$skipInactive !== void 0 ? _labels$skipInactive : "Skip inactivity",
|
|
8973
|
+
enterFullscreen: (_labels$enterFullscre = labels === null || labels === void 0 ? void 0 : labels.enterFullscreen) !== null && _labels$enterFullscre !== void 0 ? _labels$enterFullscre : "Enter fullscreen",
|
|
8974
|
+
exitFullscreen: (_labels$exitFullscree = labels === null || labels === void 0 ? void 0 : labels.exitFullscreen) !== null && _labels$exitFullscree !== void 0 ? _labels$exitFullscree : "Exit fullscreen",
|
|
8975
|
+
elapsed: (_labels$elapsed = labels === null || labels === void 0 ? void 0 : labels.elapsed) !== null && _labels$elapsed !== void 0 ? _labels$elapsed : "Show elapsed time",
|
|
8976
|
+
clock: (_labels$clock = labels === null || labels === void 0 ? void 0 : labels.clock) !== null && _labels$clock !== void 0 ? _labels$clock : "Show clock time"
|
|
8977
|
+
};
|
|
8978
|
+
const clamped = Math.min(end, Math.max(start, position));
|
|
8979
|
+
const timeLabel = timecode === "clock" ? formatClock(clamped) : `${formatReplayElapsed(clamped - start)} / ${formatReplayElapsed(end - start)}`;
|
|
8980
|
+
const withHint = (label, key) => key ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
8981
|
+
className: "inline-flex items-center gap-1",
|
|
8982
|
+
children: [label, /* @__PURE__ */ (0, react_jsx_runtime.jsx)(require_text_truncate.Kbd, {
|
|
8983
|
+
hint: true,
|
|
8984
|
+
children: key
|
|
8985
|
+
})]
|
|
8986
|
+
}) : label;
|
|
8987
|
+
const playLabel = playing ? text.pause : ended ? text.restart : text.play;
|
|
8988
|
+
const PlayIcon = playing ? lucide_react.Pause : ended ? lucide_react.RotateCcw : lucide_react.Play;
|
|
8989
|
+
const selectSpeed = (0, react.useCallback)((value) => () => onSpeedChange(value), [onSpeedChange]);
|
|
8990
|
+
(0, react.useEffect)(() => {
|
|
8991
|
+
if (!speeds.includes(speed)) onSpeedChange(speeds.includes(1) ? 1 : speeds[0]);
|
|
8992
|
+
}, [
|
|
8993
|
+
speed,
|
|
8994
|
+
speeds,
|
|
8995
|
+
onSpeedChange
|
|
8996
|
+
]);
|
|
8997
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
8998
|
+
className: require_text_truncate.cn("flex w-full flex-wrap items-center gap-element", className),
|
|
8999
|
+
"data-opt-id": "RW9FW",
|
|
9000
|
+
"data-opt-slug": "replay-transport.ReplayTransport",
|
|
9001
|
+
children: [
|
|
9002
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
9003
|
+
className: "flex items-center gap-0.5",
|
|
9004
|
+
children: [
|
|
9005
|
+
onSkipToStart ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(require_text_truncate.Button, {
|
|
9006
|
+
type: "button",
|
|
9007
|
+
size: "sm",
|
|
9008
|
+
variant: "ghost",
|
|
9009
|
+
"aria-label": text.skipToStart,
|
|
9010
|
+
title: text.skipToStart,
|
|
9011
|
+
disabled,
|
|
9012
|
+
onClick: onSkipToStart,
|
|
9013
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(lucide_react.SkipBack, {
|
|
9014
|
+
className: "size-4",
|
|
9015
|
+
"aria-hidden": "true"
|
|
9016
|
+
})
|
|
9017
|
+
}) : null,
|
|
9018
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(require_text_truncate.Button, {
|
|
9019
|
+
type: "button",
|
|
9020
|
+
size: "sm",
|
|
9021
|
+
variant: "ghost",
|
|
9022
|
+
"aria-label": text.back(jumpSeconds),
|
|
9023
|
+
title: (shortcuts === null || shortcuts === void 0 ? void 0 : shortcuts.back) ? `${text.back(jumpSeconds)} (${shortcuts.back})` : text.back(jumpSeconds),
|
|
9024
|
+
disabled,
|
|
9025
|
+
onClick: () => onSkip(-jumpMs),
|
|
9026
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(lucide_react.Rewind, {
|
|
9027
|
+
className: "size-4",
|
|
9028
|
+
"aria-hidden": "true"
|
|
9029
|
+
})
|
|
9030
|
+
}),
|
|
9031
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(require_text_truncate.Button, {
|
|
9032
|
+
type: "button",
|
|
9033
|
+
size: "md",
|
|
9034
|
+
variant: "primary",
|
|
9035
|
+
"aria-label": playLabel,
|
|
9036
|
+
"aria-pressed": playing,
|
|
9037
|
+
title: (shortcuts === null || shortcuts === void 0 ? void 0 : shortcuts.playPause) ? `${playLabel} (${shortcuts.playPause})` : playLabel,
|
|
9038
|
+
disabled,
|
|
9039
|
+
onClick: onTogglePlay,
|
|
9040
|
+
className: "rounded-full px-0 [&]:h-9 [&]:w-9",
|
|
9041
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(PlayIcon, {
|
|
9042
|
+
className: "size-4",
|
|
9043
|
+
"aria-hidden": "true"
|
|
9044
|
+
})
|
|
9045
|
+
}),
|
|
9046
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(require_text_truncate.Button, {
|
|
9047
|
+
type: "button",
|
|
9048
|
+
size: "sm",
|
|
9049
|
+
variant: "ghost",
|
|
9050
|
+
"aria-label": text.forward(jumpSeconds),
|
|
9051
|
+
title: (shortcuts === null || shortcuts === void 0 ? void 0 : shortcuts.forward) ? `${text.forward(jumpSeconds)} (${shortcuts.forward})` : text.forward(jumpSeconds),
|
|
9052
|
+
disabled,
|
|
9053
|
+
onClick: () => onSkip(jumpMs),
|
|
9054
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(lucide_react.FastForward, {
|
|
9055
|
+
className: "size-4",
|
|
9056
|
+
"aria-hidden": "true"
|
|
9057
|
+
})
|
|
9058
|
+
})
|
|
9059
|
+
]
|
|
9060
|
+
}),
|
|
9061
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
9062
|
+
type: "button",
|
|
9063
|
+
className: require_text_truncate.cn("min-w-[7.5rem] rounded px-1 text-left text-sm tabular-nums", require_text_truncate.OPT_TEXT_SECONDARY, onTimecodeChange ? "cursor-pointer hover:bg-bg-subtle" : "cursor-default"),
|
|
9064
|
+
"aria-label": timecode === "clock" ? text.elapsed : text.clock,
|
|
9065
|
+
title: timecode === "clock" ? text.elapsed : text.clock,
|
|
9066
|
+
disabled: !onTimecodeChange,
|
|
9067
|
+
onClick: () => onTimecodeChange === null || onTimecodeChange === void 0 ? void 0 : onTimecodeChange(timecode === "clock" ? "elapsed" : "clock"),
|
|
9068
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("output", {
|
|
9069
|
+
"aria-live": "off",
|
|
9070
|
+
children: timeLabel
|
|
9071
|
+
})
|
|
9072
|
+
}),
|
|
9073
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
9074
|
+
className: "gap-element ml-auto flex items-center",
|
|
9075
|
+
children: [
|
|
9076
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)(require_text_truncate.MenuRoot, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)(require_text_truncate.MenuTrigger, {
|
|
9077
|
+
"aria-label": text.speed,
|
|
9078
|
+
title: text.speed,
|
|
9079
|
+
disabled,
|
|
9080
|
+
className: require_text_truncate.cn("inline-flex h-[var(--opt-control-sm)] cursor-pointer items-center gap-1 rounded-lg px-2 text-xs font-medium tabular-nums hover:bg-bg-subtle disabled:cursor-not-allowed disabled:opacity-40", require_text_truncate.OPT_TEXT_SECONDARY),
|
|
9081
|
+
children: [
|
|
9082
|
+
speed,
|
|
9083
|
+
"×",
|
|
9084
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(lucide_react.ChevronDown, {
|
|
9085
|
+
className: "size-3",
|
|
9086
|
+
"aria-hidden": "true"
|
|
9087
|
+
})
|
|
9088
|
+
]
|
|
9089
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(require_text_truncate.MenuPopover, {
|
|
9090
|
+
"aria-label": text.speed,
|
|
9091
|
+
className: "min-w-[6rem]",
|
|
9092
|
+
children: speeds.map((value) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(require_text_truncate.MenuItemRadio, {
|
|
9093
|
+
name: "replay-speed",
|
|
9094
|
+
value: String(value),
|
|
9095
|
+
checked: value === speed,
|
|
9096
|
+
onChange: selectSpeed(value),
|
|
9097
|
+
className: "tabular-nums",
|
|
9098
|
+
children: [value, "×"]
|
|
9099
|
+
}, value))
|
|
9100
|
+
})] }),
|
|
9101
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(require_text_truncate.Toggle, {
|
|
9102
|
+
size: "sm",
|
|
9103
|
+
pressed: skipInactive,
|
|
9104
|
+
onPressedChange: onSkipInactiveChange,
|
|
9105
|
+
disabled,
|
|
9106
|
+
"aria-label": text.skipInactive,
|
|
9107
|
+
title: (shortcuts === null || shortcuts === void 0 ? void 0 : shortcuts.skipInactive) ? `${text.skipInactive} (${shortcuts.skipInactive})` : text.skipInactive,
|
|
9108
|
+
children: withHint(text.skipInactive, void 0)
|
|
9109
|
+
}),
|
|
9110
|
+
children,
|
|
9111
|
+
onFullscreenChange ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(require_text_truncate.Button, {
|
|
9112
|
+
type: "button",
|
|
9113
|
+
size: "sm",
|
|
9114
|
+
variant: "ghost",
|
|
9115
|
+
"aria-label": fullscreen ? text.exitFullscreen : text.enterFullscreen,
|
|
9116
|
+
"aria-pressed": !!fullscreen,
|
|
9117
|
+
title: (shortcuts === null || shortcuts === void 0 ? void 0 : shortcuts.fullscreen) ? `${fullscreen ? text.exitFullscreen : text.enterFullscreen} (${shortcuts.fullscreen})` : fullscreen ? text.exitFullscreen : text.enterFullscreen,
|
|
9118
|
+
onClick: () => onFullscreenChange(!fullscreen),
|
|
9119
|
+
children: fullscreen ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(lucide_react.Minimize2, {
|
|
9120
|
+
className: "size-4",
|
|
9121
|
+
"aria-hidden": "true"
|
|
9122
|
+
}) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)(lucide_react.Maximize2, {
|
|
9123
|
+
className: "size-4",
|
|
9124
|
+
"aria-hidden": "true"
|
|
9125
|
+
})
|
|
9126
|
+
}) : null
|
|
9127
|
+
]
|
|
9128
|
+
}),
|
|
9129
|
+
(shortcuts === null || shortcuts === void 0 ? void 0 : shortcuts.playPause) ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
9130
|
+
className: require_text_truncate.cn("sr-only", require_text_truncate.OPT_TEXT_TERTIARY),
|
|
9131
|
+
children: [
|
|
9132
|
+
playLabel,
|
|
9133
|
+
" ",
|
|
9134
|
+
shortcuts.playPause
|
|
9135
|
+
]
|
|
9136
|
+
}) : null
|
|
9137
|
+
]
|
|
9138
|
+
});
|
|
9139
|
+
}
|
|
9140
|
+
ReplayTransport.displayName = "ReplayTransport";
|
|
9141
|
+
//#endregion
|
|
9142
|
+
//#region src/shells/replay-player-layout.tsx
|
|
9143
|
+
function isTextTarget(target) {
|
|
9144
|
+
if (!(target instanceof HTMLElement)) return false;
|
|
9145
|
+
const tag = target.tagName;
|
|
9146
|
+
return tag === "INPUT" || tag === "TEXTAREA" || tag === "SELECT" || target.isContentEditable;
|
|
9147
|
+
}
|
|
9148
|
+
/** Normalize a keyboard event into a shortcut key such as `space`, `arrowleft`, `shift+arrowright`. */
|
|
9149
|
+
function replayShortcutKey(event) {
|
|
9150
|
+
const key = event.key === " " ? "space" : event.key.toLowerCase();
|
|
9151
|
+
return [...[
|
|
9152
|
+
event.ctrlKey || event.metaKey ? "mod" : "",
|
|
9153
|
+
event.altKey ? "alt" : "",
|
|
9154
|
+
event.shiftKey ? "shift" : ""
|
|
9155
|
+
].filter(Boolean), key].join("+");
|
|
9156
|
+
}
|
|
9157
|
+
/**
|
|
9158
|
+
* ReplayPlayerLayout — stage, chrome and inspector for a session replay.
|
|
9159
|
+
*
|
|
9160
|
+
* Owns three things every replay UI otherwise reinvents: a resizable
|
|
9161
|
+
* inspector that keeps its width, fullscreen on the whole player rather
|
|
9162
|
+
* than on the recording iframe alone, and a keyboard scope that reacts to
|
|
9163
|
+
* shortcuts only while the player is focused and never while someone is
|
|
9164
|
+
* typing in a field.
|
|
9165
|
+
*/
|
|
9166
|
+
function ReplayPlayerLayout({ stage, meta, controls, inspector, inspectorOpen = true, onInspectorOpenChange, inspectorSize = 34, layoutId, fullscreen = false, onFullscreenChange, shortcuts, overlay, labels, className }) {
|
|
9167
|
+
var _labels$region, _labels$openInspector, _labels$closeInspecto;
|
|
9168
|
+
const rootRef = (0, react.useRef)(null);
|
|
9169
|
+
const [hovering, setHovering] = (0, react.useState)(false);
|
|
9170
|
+
const shortcutsRef = (0, react.useRef)(shortcuts);
|
|
9171
|
+
shortcutsRef.current = shortcuts;
|
|
9172
|
+
const text = {
|
|
9173
|
+
region: (_labels$region = labels === null || labels === void 0 ? void 0 : labels.region) !== null && _labels$region !== void 0 ? _labels$region : "Session replay",
|
|
9174
|
+
openInspector: (_labels$openInspector = labels === null || labels === void 0 ? void 0 : labels.openInspector) !== null && _labels$openInspector !== void 0 ? _labels$openInspector : "Show details",
|
|
9175
|
+
closeInspector: (_labels$closeInspecto = labels === null || labels === void 0 ? void 0 : labels.closeInspector) !== null && _labels$closeInspecto !== void 0 ? _labels$closeInspecto : "Hide details"
|
|
9176
|
+
};
|
|
9177
|
+
(0, react.useEffect)(() => {
|
|
9178
|
+
const node = rootRef.current;
|
|
9179
|
+
if (!node || typeof document === "undefined") return;
|
|
9180
|
+
const active = document.fullscreenElement === node;
|
|
9181
|
+
if (fullscreen && !active && node.requestFullscreen) node.requestFullscreen().catch(() => onFullscreenChange === null || onFullscreenChange === void 0 ? void 0 : onFullscreenChange(false));
|
|
9182
|
+
else if (!fullscreen && active && document.exitFullscreen) document.exitFullscreen().catch(() => {});
|
|
9183
|
+
}, [fullscreen, onFullscreenChange]);
|
|
9184
|
+
(0, react.useEffect)(() => {
|
|
9185
|
+
if (typeof document === "undefined") return;
|
|
9186
|
+
const handleChange = () => {
|
|
9187
|
+
const active = document.fullscreenElement === rootRef.current;
|
|
9188
|
+
if (active !== fullscreen) onFullscreenChange === null || onFullscreenChange === void 0 || onFullscreenChange(active);
|
|
9189
|
+
};
|
|
9190
|
+
document.addEventListener("fullscreenchange", handleChange);
|
|
9191
|
+
return () => document.removeEventListener("fullscreenchange", handleChange);
|
|
9192
|
+
}, [fullscreen, onFullscreenChange]);
|
|
9193
|
+
const handleKeyDown = (0, react.useCallback)((event) => {
|
|
9194
|
+
const map = shortcutsRef.current;
|
|
9195
|
+
if (!map || isTextTarget(event.target)) return;
|
|
9196
|
+
const handler = map[replayShortcutKey(event.nativeEvent)];
|
|
9197
|
+
if (!handler) return;
|
|
9198
|
+
if (handler(event.nativeEvent) !== false) {
|
|
9199
|
+
event.preventDefault();
|
|
9200
|
+
event.stopPropagation();
|
|
9201
|
+
}
|
|
9202
|
+
}, []);
|
|
9203
|
+
const showInspector = Boolean(inspector) && inspectorOpen;
|
|
9204
|
+
const toggle = onInspectorOpenChange ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(require_text_truncate.Button, {
|
|
9205
|
+
type: "button",
|
|
9206
|
+
size: "sm",
|
|
9207
|
+
variant: "ghost",
|
|
9208
|
+
"aria-label": inspectorOpen ? text.closeInspector : text.openInspector,
|
|
9209
|
+
"aria-pressed": inspectorOpen,
|
|
9210
|
+
title: inspectorOpen ? text.closeInspector : text.openInspector,
|
|
9211
|
+
onClick: () => onInspectorOpenChange(!inspectorOpen),
|
|
9212
|
+
children: inspectorOpen ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(lucide_react.PanelRightClose, {
|
|
9213
|
+
className: "size-4",
|
|
9214
|
+
"aria-hidden": "true"
|
|
9215
|
+
}) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)(lucide_react.PanelRightOpen, {
|
|
9216
|
+
className: "size-4",
|
|
9217
|
+
"aria-hidden": "true"
|
|
9218
|
+
})
|
|
9219
|
+
}) : null;
|
|
9220
|
+
const main = /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
9221
|
+
className: "flex h-full min-h-0 min-w-0 flex-1 flex-col",
|
|
9222
|
+
children: [
|
|
9223
|
+
meta || toggle ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
9224
|
+
className: "gap-element border-border flex min-h-9 items-center border-b px-3 py-1 text-xs",
|
|
9225
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
9226
|
+
className: "min-w-0 flex-1",
|
|
9227
|
+
children: meta
|
|
9228
|
+
}), toggle]
|
|
9229
|
+
}) : null,
|
|
9230
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
9231
|
+
className: "bg-bg-subtle relative min-h-0 flex-1",
|
|
9232
|
+
onPointerEnter: () => setHovering(true),
|
|
9233
|
+
onPointerLeave: () => setHovering(false),
|
|
9234
|
+
"data-hovering": hovering || void 0,
|
|
9235
|
+
children: [stage, overlay ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
9236
|
+
className: "pointer-events-none absolute inset-0 flex items-center justify-center [&>*]:pointer-events-auto",
|
|
9237
|
+
children: overlay
|
|
9238
|
+
}) : null]
|
|
9239
|
+
}),
|
|
9240
|
+
controls ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
9241
|
+
className: "border-border border-t px-3 py-2",
|
|
9242
|
+
children: controls
|
|
9243
|
+
}) : null
|
|
9244
|
+
]
|
|
9245
|
+
});
|
|
9246
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
9247
|
+
ref: rootRef,
|
|
9248
|
+
role: "region",
|
|
9249
|
+
"aria-label": text.region,
|
|
9250
|
+
tabIndex: -1,
|
|
9251
|
+
onKeyDown: handleKeyDown,
|
|
9252
|
+
className: require_text_truncate.cn("flex min-h-0 w-full flex-col overflow-hidden rounded-lg bg-surface", require_text_truncate.OPT_BORDER, require_text_truncate.OPT_FOCUS_VISIBLE, fullscreen ? "h-screen" : "h-full", className),
|
|
9253
|
+
"data-opt-id": "A3VYS",
|
|
9254
|
+
"data-opt-slug": "replay-player-layout.ReplayPlayerLayout",
|
|
9255
|
+
children: showInspector ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(require_text_truncate.ResizablePanelGroup, {
|
|
9256
|
+
direction: "horizontal",
|
|
9257
|
+
id: layoutId,
|
|
9258
|
+
children: [
|
|
9259
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(require_text_truncate.ResizablePanel, {
|
|
9260
|
+
defaultSize: 100 - inspectorSize,
|
|
9261
|
+
minSize: 40,
|
|
9262
|
+
children: main
|
|
9263
|
+
}),
|
|
9264
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(require_text_truncate.ResizableHandle, { withHandle: true }),
|
|
9265
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(require_text_truncate.ResizablePanel, {
|
|
9266
|
+
defaultSize: inspectorSize,
|
|
9267
|
+
minSize: 20,
|
|
9268
|
+
maxSize: 60,
|
|
9269
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
9270
|
+
className: "flex h-full min-h-0 flex-col overflow-hidden",
|
|
9271
|
+
children: inspector
|
|
9272
|
+
})
|
|
9273
|
+
})
|
|
9274
|
+
]
|
|
9275
|
+
}) : main
|
|
9276
|
+
});
|
|
9277
|
+
}
|
|
9278
|
+
ReplayPlayerLayout.displayName = "ReplayPlayerLayout";
|
|
9279
|
+
//#endregion
|
|
8579
9280
|
Object.defineProperty(exports, "ALL_COMPOUND_THEMES", {
|
|
8580
9281
|
enumerable: true,
|
|
8581
9282
|
get: function() {
|
|
@@ -8870,6 +9571,24 @@ Object.defineProperty(exports, "QueryWorkspace", {
|
|
|
8870
9571
|
return QueryWorkspace;
|
|
8871
9572
|
}
|
|
8872
9573
|
});
|
|
9574
|
+
Object.defineProperty(exports, "ReplayPlayerLayout", {
|
|
9575
|
+
enumerable: true,
|
|
9576
|
+
get: function() {
|
|
9577
|
+
return ReplayPlayerLayout;
|
|
9578
|
+
}
|
|
9579
|
+
});
|
|
9580
|
+
Object.defineProperty(exports, "ReplayScrubber", {
|
|
9581
|
+
enumerable: true,
|
|
9582
|
+
get: function() {
|
|
9583
|
+
return ReplayScrubber;
|
|
9584
|
+
}
|
|
9585
|
+
});
|
|
9586
|
+
Object.defineProperty(exports, "ReplayTransport", {
|
|
9587
|
+
enumerable: true,
|
|
9588
|
+
get: function() {
|
|
9589
|
+
return ReplayTransport;
|
|
9590
|
+
}
|
|
9591
|
+
});
|
|
8873
9592
|
Object.defineProperty(exports, "SaveQueryDialog", {
|
|
8874
9593
|
enumerable: true,
|
|
8875
9594
|
get: function() {
|
|
@@ -9062,6 +9781,12 @@ Object.defineProperty(exports, "formatQueryCell", {
|
|
|
9062
9781
|
return formatQueryCell;
|
|
9063
9782
|
}
|
|
9064
9783
|
});
|
|
9784
|
+
Object.defineProperty(exports, "formatReplayElapsed", {
|
|
9785
|
+
enumerable: true,
|
|
9786
|
+
get: function() {
|
|
9787
|
+
return formatReplayElapsed;
|
|
9788
|
+
}
|
|
9789
|
+
});
|
|
9065
9790
|
Object.defineProperty(exports, "generatedTheme", {
|
|
9066
9791
|
enumerable: true,
|
|
9067
9792
|
get: function() {
|
|
@@ -9086,6 +9811,12 @@ Object.defineProperty(exports, "getThemeDescriptor", {
|
|
|
9086
9811
|
return getThemeDescriptor;
|
|
9087
9812
|
}
|
|
9088
9813
|
});
|
|
9814
|
+
Object.defineProperty(exports, "groupReplayMarkers", {
|
|
9815
|
+
enumerable: true,
|
|
9816
|
+
get: function() {
|
|
9817
|
+
return groupReplayMarkers;
|
|
9818
|
+
}
|
|
9819
|
+
});
|
|
9089
9820
|
Object.defineProperty(exports, "hasActiveQueryFilters", {
|
|
9090
9821
|
enumerable: true,
|
|
9091
9822
|
get: function() {
|
|
@@ -9182,6 +9913,12 @@ Object.defineProperty(exports, "removeClause", {
|
|
|
9182
9913
|
return removeClause;
|
|
9183
9914
|
}
|
|
9184
9915
|
});
|
|
9916
|
+
Object.defineProperty(exports, "replayShortcutKey", {
|
|
9917
|
+
enumerable: true,
|
|
9918
|
+
get: function() {
|
|
9919
|
+
return replayShortcutKey;
|
|
9920
|
+
}
|
|
9921
|
+
});
|
|
9185
9922
|
Object.defineProperty(exports, "resolveDescriptor", {
|
|
9186
9923
|
enumerable: true,
|
|
9187
9924
|
get: function() {
|
|
@@ -9230,6 +9967,12 @@ Object.defineProperty(exports, "toast", {
|
|
|
9230
9967
|
return toast;
|
|
9231
9968
|
}
|
|
9232
9969
|
});
|
|
9970
|
+
Object.defineProperty(exports, "useAsyncSearch", {
|
|
9971
|
+
enumerable: true,
|
|
9972
|
+
get: function() {
|
|
9973
|
+
return useAsyncSearch;
|
|
9974
|
+
}
|
|
9975
|
+
});
|
|
9233
9976
|
Object.defineProperty(exports, "useAutoRefresh", {
|
|
9234
9977
|
enumerable: true,
|
|
9235
9978
|
get: function() {
|