624 lines
22 KiB
TypeScript
624 lines
22 KiB
TypeScript
// @ts-nocheck
|
|
import { createEffect, createMemo, onCleanup } from "solid-js"
|
|
import { createStore } from "solid-js/store"
|
|
import type { Todo } from "@opencode-ai/sdk/v2"
|
|
import { useServerSync } from "@/context/global-sync"
|
|
import { PromptInput } from "@/components/prompt-input"
|
|
import { usePrompt } from "@/context/prompt"
|
|
import {
|
|
SessionComposerRegion,
|
|
createSessionComposerController,
|
|
createSessionComposerRegionController,
|
|
} from "@/pages/session/composer"
|
|
|
|
export default {
|
|
title: "UI/Todo Panel Motion",
|
|
id: "components-todo-panel-motion",
|
|
tags: ["autodocs"],
|
|
parameters: {
|
|
docs: {
|
|
description: {
|
|
component: `### Overview
|
|
This playground renders the real session composer region from app code.
|
|
|
|
### Source path
|
|
- \`packages/app/src/pages/session/composer/session-composer-region.tsx\`
|
|
|
|
### Includes
|
|
- \`SessionTodoDock\` (real)
|
|
- \`PromptInput\` (real)
|
|
|
|
No visual reimplementation layer is used for the dock/input stack.`,
|
|
},
|
|
},
|
|
},
|
|
}
|
|
|
|
const pool = [
|
|
"Refactor ToolStatusTitle DOM measurement to offscreen global measurer (unconstrained by timeline layout)",
|
|
"Remove inline measure nodes/CSS hooks and keep width morph behavior intact",
|
|
"Run typechecks/tests and report what changed",
|
|
"Verify reduced-motion behavior in timeline",
|
|
"Review diff for animation edge cases",
|
|
"Document rollout notes in PR description",
|
|
"Check keyboard and screen reader semantics",
|
|
"Add storybook controls for iteration speed",
|
|
]
|
|
|
|
const btn = (accent?: boolean) =>
|
|
({
|
|
padding: "6px 14px",
|
|
"border-radius": "6px",
|
|
border: "1px solid var(--color-divider, #333)",
|
|
background: accent ? "var(--color-accent, #58f)" : "var(--color-fill-element, #222)",
|
|
color: "var(--color-text, #eee)",
|
|
cursor: "pointer",
|
|
"font-size": "13px",
|
|
}) as const
|
|
|
|
const controls = {
|
|
agents: { available: [], options: ["build"], current: "build", loading: false, visible: true, select: () => {} },
|
|
model: {
|
|
selection: {
|
|
current: () => ({ id: "claude-3-7-sonnet", name: "Claude 3.7 Sonnet", provider: { id: "anthropic" } }),
|
|
variant: { list: () => [], current: () => undefined, set: () => {} },
|
|
},
|
|
paid: true,
|
|
loading: false,
|
|
},
|
|
session: {
|
|
id: "story-session",
|
|
tabs: { active: () => undefined, all: () => [], open: () => {}, setActive: () => {} },
|
|
reviewPanel: { opened: () => false, open: () => {} },
|
|
},
|
|
newLayoutDesigns: true,
|
|
}
|
|
|
|
const css = `
|
|
[data-component="todo-stage"] {
|
|
display: grid;
|
|
gap: 20px;
|
|
padding: 20px;
|
|
}
|
|
|
|
[data-component="todo-preview"] {
|
|
height: 560px;
|
|
min-height: 0;
|
|
}
|
|
|
|
[data-component="todo-session-root"] {
|
|
position: relative;
|
|
width: 100%;
|
|
height: 100%;
|
|
overflow: hidden;
|
|
display: flex;
|
|
flex-direction: column;
|
|
background: var(--background-base);
|
|
border: 1px solid var(--border-weak-base);
|
|
border-radius: 12px;
|
|
}
|
|
|
|
[data-component="todo-session-frame"] {
|
|
flex: 1 1 auto;
|
|
min-height: 0;
|
|
display: flex;
|
|
flex-direction: column;
|
|
}
|
|
|
|
[data-component="todo-session-panel"] {
|
|
position: relative;
|
|
flex: 1 1 auto;
|
|
min-height: 0;
|
|
height: 100%;
|
|
display: flex;
|
|
flex-direction: column;
|
|
background: var(--background-stronger);
|
|
}
|
|
|
|
[data-slot="todo-preview-content"] {
|
|
flex: 1 1 auto;
|
|
min-height: 0;
|
|
overflow: hidden;
|
|
}
|
|
|
|
[data-slot="todo-preview-scroll"] {
|
|
height: 100%;
|
|
overflow: auto;
|
|
min-height: 0;
|
|
padding: 14px 16px;
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 10px;
|
|
}
|
|
|
|
[data-slot="todo-preview-spacer"] {
|
|
flex: 1 1 auto;
|
|
min-height: 0;
|
|
}
|
|
|
|
[data-slot="todo-preview-msg"] {
|
|
border-radius: 8px;
|
|
border: 1px solid var(--border-weak-base);
|
|
background: var(--surface-base);
|
|
color: var(--text-weak);
|
|
padding: 8px 10px;
|
|
font-size: 13px;
|
|
line-height: 1.35;
|
|
}
|
|
|
|
[data-slot="todo-preview-msg"][data-strong="true"] {
|
|
color: var(--text-strong);
|
|
}
|
|
`
|
|
|
|
export const Playground = {
|
|
render: () => {
|
|
const global = useServerSync()
|
|
const prompt = usePrompt()
|
|
const [cfg, setCfg] = createStore({
|
|
open: true,
|
|
collapsed: false,
|
|
step: 1,
|
|
dockOpenDuration: 0.3,
|
|
dockOpenBounce: 0,
|
|
dockCloseDuration: 0.3,
|
|
dockCloseBounce: 0,
|
|
drawerExpandDuration: 0.3,
|
|
drawerExpandBounce: 0,
|
|
drawerCollapseDuration: 0.3,
|
|
drawerCollapseBounce: 0,
|
|
subtitleDuration: 600,
|
|
subtitleAuto: true,
|
|
subtitleTravel: 25,
|
|
subtitleEdge: 17,
|
|
countDuration: 600,
|
|
countMask: 18,
|
|
countMaskHeight: 0,
|
|
countWidthDuration: 560,
|
|
})
|
|
const open = () => cfg.open
|
|
const step = () => cfg.step
|
|
const dockOpenDuration = () => cfg.dockOpenDuration
|
|
const dockOpenBounce = () => cfg.dockOpenBounce
|
|
const dockCloseDuration = () => cfg.dockCloseDuration
|
|
const dockCloseBounce = () => cfg.dockCloseBounce
|
|
const drawerExpandDuration = () => cfg.drawerExpandDuration
|
|
const drawerExpandBounce = () => cfg.drawerExpandBounce
|
|
const drawerCollapseDuration = () => cfg.drawerCollapseDuration
|
|
const drawerCollapseBounce = () => cfg.drawerCollapseBounce
|
|
const subtitleDuration = () => cfg.subtitleDuration
|
|
const subtitleAuto = () => cfg.subtitleAuto
|
|
const subtitleTravel = () => cfg.subtitleTravel
|
|
const subtitleEdge = () => cfg.subtitleEdge
|
|
const countDuration = () => cfg.countDuration
|
|
const countMask = () => cfg.countMask
|
|
const countMaskHeight = () => cfg.countMaskHeight
|
|
const countWidthDuration = () => cfg.countWidthDuration
|
|
const state = createSessionComposerController({ closeMs: () => Math.round(dockCloseDuration() * 1000) })
|
|
let frame
|
|
let scrollRef
|
|
|
|
const todos = createMemo<Todo[]>(() => {
|
|
const done = Math.max(0, Math.min(3, step()))
|
|
return pool.slice(0, 3).map((content, i) => ({
|
|
id: `todo-${i + 1}`,
|
|
content,
|
|
status: i < done ? "completed" : i === done && done < 3 ? "in_progress" : "pending",
|
|
}))
|
|
})
|
|
|
|
createEffect(() => {
|
|
global.todo.set("story-session", todos())
|
|
})
|
|
|
|
const clear = () => {
|
|
if (frame) cancelAnimationFrame(frame)
|
|
frame = undefined
|
|
}
|
|
|
|
const pin = () => {
|
|
if (!scrollRef) return
|
|
scrollRef.scrollTop = scrollRef.scrollHeight
|
|
}
|
|
|
|
const collapsed = () => cfg.collapsed
|
|
const setCollapsed = (value: boolean) => setCfg("collapsed", value)
|
|
const openDock = () => {
|
|
clear()
|
|
setCfg("open", true)
|
|
frame = requestAnimationFrame(() => {
|
|
pin()
|
|
frame = undefined
|
|
})
|
|
}
|
|
|
|
const closeDock = () => {
|
|
clear()
|
|
setCfg("open", false)
|
|
}
|
|
|
|
const dockOpen = () => open()
|
|
|
|
const toggleDock = () => {
|
|
if (dockOpen()) {
|
|
closeDock()
|
|
return
|
|
}
|
|
openDock()
|
|
}
|
|
|
|
const toggleDrawer = () => {
|
|
if (!dockOpen()) {
|
|
openDock()
|
|
frame = requestAnimationFrame(() => {
|
|
pin()
|
|
setCollapsed(true)
|
|
frame = undefined
|
|
})
|
|
return
|
|
}
|
|
setCollapsed(!collapsed())
|
|
}
|
|
|
|
const cycle = () => {
|
|
setCfg("step", (value) => (value + 1) % 4)
|
|
}
|
|
|
|
onCleanup(clear)
|
|
|
|
return (
|
|
<div data-component="todo-stage">
|
|
<style>{css}</style>
|
|
|
|
<div data-component="todo-preview">
|
|
<div data-component="todo-session-root">
|
|
<div data-component="todo-session-frame">
|
|
<div data-component="todo-session-panel">
|
|
<div data-slot="todo-preview-content">
|
|
<div data-slot="todo-preview-scroll" class="scroll-view__viewport" ref={scrollRef}>
|
|
<div data-slot="todo-preview-spacer" />
|
|
<div data-slot="todo-preview-msg" data-strong="true">
|
|
Thinking Checking type safety
|
|
</div>
|
|
<div data-slot="todo-preview-msg">Shell Prints five topic blocks between timed commands</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div>
|
|
<SessionComposerRegion
|
|
controller={createSessionComposerRegionController({
|
|
state,
|
|
sessionKey: () => "story-session",
|
|
sessionID: () => "story-session",
|
|
prompt,
|
|
ready: () => true,
|
|
centered: () => false,
|
|
todo: { collapsed, onToggle: () => setCollapsed(!collapsed()) },
|
|
followup: () => undefined,
|
|
revert: () => undefined,
|
|
onResponseSubmit: pin,
|
|
openParent: () => {},
|
|
setPromptRef: () => {},
|
|
setDockRef: () => {},
|
|
})}
|
|
promptInput={
|
|
<PromptInput
|
|
controls={controls}
|
|
submission={{ abort: () => {}, handleSubmit: (event) => event.preventDefault() }}
|
|
ref={() => {}}
|
|
newSessionWorktree=""
|
|
onNewSessionWorktreeReset={() => {}}
|
|
/>
|
|
}
|
|
/>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div style={{ display: "flex", gap: "8px", "flex-wrap": "wrap" }}>
|
|
<button onClick={toggleDock} style={btn(dockOpen())}>
|
|
{dockOpen() ? "Animate close" : "Animate open"}
|
|
</button>
|
|
<button onClick={toggleDrawer} style={btn(dockOpen() && collapsed())}>
|
|
{dockOpen() && collapsed() ? "Expand todo dock" : "Collapse todo dock"}
|
|
</button>
|
|
<button onClick={cycle} style={btn(step() > 0)}>
|
|
Cycle progress ({step()}/3 done)
|
|
</button>
|
|
{[0, 1, 2, 3].map((value) => (
|
|
<button onClick={() => setCfg("step", value)} style={btn(step() === value)}>
|
|
{value} done
|
|
</button>
|
|
))}
|
|
</div>
|
|
|
|
<div style={{ display: "grid", gap: "10px", "max-width": "560px" }}>
|
|
<div style={{ "font-size": "12px", color: "var(--color-text-secondary, #a3a3a3)" }}>Dock open</div>
|
|
<label style={{ display: "flex", "align-items": "center", gap: "12px" }}>
|
|
<span style={{ width: "110px", "font-size": "13px", color: "var(--color-text-secondary, #a3a3a3)" }}>
|
|
duration
|
|
</span>
|
|
<input
|
|
type="range"
|
|
min="0.1"
|
|
max="1"
|
|
step="0.01"
|
|
value={dockOpenDuration()}
|
|
onInput={(event) => setCfg("dockOpenDuration", event.currentTarget.valueAsNumber)}
|
|
style={{ flex: 1 }}
|
|
/>
|
|
<span style={{ width: "64px", "text-align": "right", "font-size": "13px" }}>
|
|
{Math.round(dockOpenDuration() * 1000)}ms
|
|
</span>
|
|
</label>
|
|
<label style={{ display: "flex", "align-items": "center", gap: "12px" }}>
|
|
<span style={{ width: "110px", "font-size": "13px", color: "var(--color-text-secondary, #a3a3a3)" }}>
|
|
bounce
|
|
</span>
|
|
<input
|
|
type="range"
|
|
min="0"
|
|
max="1"
|
|
step="0.01"
|
|
value={dockOpenBounce()}
|
|
onInput={(event) => setCfg("dockOpenBounce", event.currentTarget.valueAsNumber)}
|
|
style={{ flex: 1 }}
|
|
/>
|
|
<span style={{ width: "64px", "text-align": "right", "font-size": "13px" }}>
|
|
{dockOpenBounce().toFixed(2)}
|
|
</span>
|
|
</label>
|
|
|
|
<div style={{ "font-size": "12px", color: "var(--color-text-secondary, #a3a3a3)", "margin-top": "4px" }}>
|
|
Dock close
|
|
</div>
|
|
<label style={{ display: "flex", "align-items": "center", gap: "12px" }}>
|
|
<span style={{ width: "110px", "font-size": "13px", color: "var(--color-text-secondary, #a3a3a3)" }}>
|
|
duration
|
|
</span>
|
|
<input
|
|
type="range"
|
|
min="0.1"
|
|
max="1"
|
|
step="0.01"
|
|
value={dockCloseDuration()}
|
|
onInput={(event) => setCfg("dockCloseDuration", event.currentTarget.valueAsNumber)}
|
|
style={{ flex: 1 }}
|
|
/>
|
|
<span style={{ width: "64px", "text-align": "right", "font-size": "13px" }}>
|
|
{Math.round(dockCloseDuration() * 1000)}ms
|
|
</span>
|
|
</label>
|
|
<label style={{ display: "flex", "align-items": "center", gap: "12px" }}>
|
|
<span style={{ width: "110px", "font-size": "13px", color: "var(--color-text-secondary, #a3a3a3)" }}>
|
|
bounce
|
|
</span>
|
|
<input
|
|
type="range"
|
|
min="0"
|
|
max="1"
|
|
step="0.01"
|
|
value={dockCloseBounce()}
|
|
onInput={(event) => setCfg("dockCloseBounce", event.currentTarget.valueAsNumber)}
|
|
style={{ flex: 1 }}
|
|
/>
|
|
<span style={{ width: "64px", "text-align": "right", "font-size": "13px" }}>
|
|
{dockCloseBounce().toFixed(2)}
|
|
</span>
|
|
</label>
|
|
|
|
<div style={{ "font-size": "12px", color: "var(--color-text-secondary, #a3a3a3)", "margin-top": "4px" }}>
|
|
Drawer expand
|
|
</div>
|
|
<label style={{ display: "flex", "align-items": "center", gap: "12px" }}>
|
|
<span style={{ width: "110px", "font-size": "13px", color: "var(--color-text-secondary, #a3a3a3)" }}>
|
|
duration
|
|
</span>
|
|
<input
|
|
type="range"
|
|
min="0.1"
|
|
max="1"
|
|
step="0.01"
|
|
value={drawerExpandDuration()}
|
|
onInput={(event) => setCfg("drawerExpandDuration", event.currentTarget.valueAsNumber)}
|
|
style={{ flex: 1 }}
|
|
/>
|
|
<span style={{ width: "64px", "text-align": "right", "font-size": "13px" }}>
|
|
{Math.round(drawerExpandDuration() * 1000)}ms
|
|
</span>
|
|
</label>
|
|
<label style={{ display: "flex", "align-items": "center", gap: "12px" }}>
|
|
<span style={{ width: "110px", "font-size": "13px", color: "var(--color-text-secondary, #a3a3a3)" }}>
|
|
bounce
|
|
</span>
|
|
<input
|
|
type="range"
|
|
min="0"
|
|
max="1"
|
|
step="0.01"
|
|
value={drawerExpandBounce()}
|
|
onInput={(event) => setCfg("drawerExpandBounce", event.currentTarget.valueAsNumber)}
|
|
style={{ flex: 1 }}
|
|
/>
|
|
<span style={{ width: "64px", "text-align": "right", "font-size": "13px" }}>
|
|
{drawerExpandBounce().toFixed(2)}
|
|
</span>
|
|
</label>
|
|
|
|
<div style={{ "font-size": "12px", color: "var(--color-text-secondary, #a3a3a3)", "margin-top": "4px" }}>
|
|
Drawer collapse
|
|
</div>
|
|
<label style={{ display: "flex", "align-items": "center", gap: "12px" }}>
|
|
<span style={{ width: "110px", "font-size": "13px", color: "var(--color-text-secondary, #a3a3a3)" }}>
|
|
duration
|
|
</span>
|
|
<input
|
|
type="range"
|
|
min="0.1"
|
|
max="1"
|
|
step="0.01"
|
|
value={drawerCollapseDuration()}
|
|
onInput={(event) => setCfg("drawerCollapseDuration", event.currentTarget.valueAsNumber)}
|
|
style={{ flex: 1 }}
|
|
/>
|
|
<span style={{ width: "64px", "text-align": "right", "font-size": "13px" }}>
|
|
{Math.round(drawerCollapseDuration() * 1000)}ms
|
|
</span>
|
|
</label>
|
|
<label style={{ display: "flex", "align-items": "center", gap: "12px" }}>
|
|
<span style={{ width: "110px", "font-size": "13px", color: "var(--color-text-secondary, #a3a3a3)" }}>
|
|
bounce
|
|
</span>
|
|
<input
|
|
type="range"
|
|
min="0"
|
|
max="1"
|
|
step="0.01"
|
|
value={drawerCollapseBounce()}
|
|
onInput={(event) => setCfg("drawerCollapseBounce", event.currentTarget.valueAsNumber)}
|
|
style={{ flex: 1 }}
|
|
/>
|
|
<span style={{ width: "64px", "text-align": "right", "font-size": "13px" }}>
|
|
{drawerCollapseBounce().toFixed(2)}
|
|
</span>
|
|
</label>
|
|
|
|
<div style={{ "font-size": "12px", color: "var(--color-text-secondary, #a3a3a3)", "margin-top": "4px" }}>
|
|
Subtitle odometer
|
|
</div>
|
|
<label style={{ display: "flex", "align-items": "center", gap: "12px" }}>
|
|
<span style={{ width: "110px", "font-size": "13px", color: "var(--color-text-secondary, #a3a3a3)" }}>
|
|
duration
|
|
</span>
|
|
<input
|
|
type="range"
|
|
min="120"
|
|
max="1400"
|
|
step="10"
|
|
value={subtitleDuration()}
|
|
onInput={(event) => setCfg("subtitleDuration", event.currentTarget.valueAsNumber)}
|
|
style={{ flex: 1 }}
|
|
/>
|
|
<span style={{ width: "64px", "text-align": "right", "font-size": "13px" }}>
|
|
{Math.round(subtitleDuration())}ms
|
|
</span>
|
|
</label>
|
|
<label style={{ display: "flex", "align-items": "center", gap: "12px" }}>
|
|
<span style={{ width: "110px", "font-size": "13px", color: "var(--color-text-secondary, #a3a3a3)" }}>
|
|
auto fit
|
|
</span>
|
|
<input
|
|
type="checkbox"
|
|
checked={subtitleAuto()}
|
|
onInput={(event) => setCfg("subtitleAuto", event.currentTarget.checked)}
|
|
/>
|
|
<span style={{ width: "64px", "text-align": "right", "font-size": "13px" }}>
|
|
{subtitleAuto() ? "on" : "off"}
|
|
</span>
|
|
</label>
|
|
<label style={{ display: "flex", "align-items": "center", gap: "12px" }}>
|
|
<span style={{ width: "110px", "font-size": "13px", color: "var(--color-text-secondary, #a3a3a3)" }}>
|
|
travel
|
|
</span>
|
|
<input
|
|
type="range"
|
|
min="0"
|
|
max="40"
|
|
step="1"
|
|
value={subtitleTravel()}
|
|
onInput={(event) => setCfg("subtitleTravel", event.currentTarget.valueAsNumber)}
|
|
style={{ flex: 1 }}
|
|
/>
|
|
<span style={{ width: "64px", "text-align": "right", "font-size": "13px" }}>{subtitleTravel()}px</span>
|
|
</label>
|
|
<label style={{ display: "flex", "align-items": "center", gap: "12px" }}>
|
|
<span style={{ width: "110px", "font-size": "13px", color: "var(--color-text-secondary, #a3a3a3)" }}>
|
|
edge
|
|
</span>
|
|
<input
|
|
type="range"
|
|
min="1"
|
|
max="40"
|
|
step="1"
|
|
value={subtitleEdge()}
|
|
onInput={(event) => setCfg("subtitleEdge", event.currentTarget.valueAsNumber)}
|
|
style={{ flex: 1 }}
|
|
/>
|
|
<span style={{ width: "64px", "text-align": "right", "font-size": "13px" }}>{subtitleEdge()}%</span>
|
|
</label>
|
|
|
|
<div style={{ "font-size": "12px", color: "var(--color-text-secondary, #a3a3a3)", "margin-top": "4px" }}>
|
|
Count odometer
|
|
</div>
|
|
<label style={{ display: "flex", "align-items": "center", gap: "12px" }}>
|
|
<span style={{ width: "110px", "font-size": "13px", color: "var(--color-text-secondary, #a3a3a3)" }}>
|
|
duration
|
|
</span>
|
|
<input
|
|
type="range"
|
|
min="120"
|
|
max="1400"
|
|
step="10"
|
|
value={countDuration()}
|
|
onInput={(event) => setCfg("countDuration", event.currentTarget.valueAsNumber)}
|
|
style={{ flex: 1 }}
|
|
/>
|
|
<span style={{ width: "64px", "text-align": "right", "font-size": "13px" }}>
|
|
{Math.round(countDuration())}ms
|
|
</span>
|
|
</label>
|
|
<label style={{ display: "flex", "align-items": "center", gap: "12px" }}>
|
|
<span style={{ width: "110px", "font-size": "13px", color: "var(--color-text-secondary, #a3a3a3)" }}>
|
|
mask
|
|
</span>
|
|
<input
|
|
type="range"
|
|
min="4"
|
|
max="40"
|
|
step="1"
|
|
value={countMask()}
|
|
onInput={(event) => setCfg("countMask", event.currentTarget.valueAsNumber)}
|
|
style={{ flex: 1 }}
|
|
/>
|
|
<span style={{ width: "64px", "text-align": "right", "font-size": "13px" }}>{countMask()}%</span>
|
|
</label>
|
|
<label style={{ display: "flex", "align-items": "center", gap: "12px" }}>
|
|
<span style={{ width: "110px", "font-size": "13px", color: "var(--color-text-secondary, #a3a3a3)" }}>
|
|
mask height
|
|
</span>
|
|
<input
|
|
type="range"
|
|
min="0"
|
|
max="14"
|
|
step="1"
|
|
value={countMaskHeight()}
|
|
onInput={(event) => setCfg("countMaskHeight", event.currentTarget.valueAsNumber)}
|
|
style={{ flex: 1 }}
|
|
/>
|
|
<span style={{ width: "64px", "text-align": "right", "font-size": "13px" }}>{countMaskHeight()}px</span>
|
|
</label>
|
|
<label style={{ display: "flex", "align-items": "center", gap: "12px" }}>
|
|
<span style={{ width: "110px", "font-size": "13px", color: "var(--color-text-secondary, #a3a3a3)" }}>
|
|
width spring
|
|
</span>
|
|
<input
|
|
type="range"
|
|
min="0"
|
|
max="1200"
|
|
step="10"
|
|
value={countWidthDuration()}
|
|
onInput={(event) => setCfg("countWidthDuration", event.currentTarget.valueAsNumber)}
|
|
style={{ flex: 1 }}
|
|
/>
|
|
<span style={{ width: "64px", "text-align": "right", "font-size": "13px" }}>
|
|
{Math.round(countWidthDuration())}ms
|
|
</span>
|
|
</label>
|
|
</div>
|
|
</div>
|
|
)
|
|
},
|
|
}
|