mirror of
https://gitea.toothfairyai.com/ToothFairyAI/tf_code.git
synced 2026-04-24 17:44:49 +00:00
Animation Smorgasbord (#15637)
Co-authored-by: Adam <2363879+adamdotdevin@users.noreply.github.com>
This commit is contained in:
329
packages/ui/src/components/shell-submessage-motion.stories.tsx
Normal file
329
packages/ui/src/components/shell-submessage-motion.stories.tsx
Normal file
@@ -0,0 +1,329 @@
|
||||
// @ts-nocheck
|
||||
import { createEffect, createSignal, onCleanup } from "solid-js"
|
||||
import { BasicTool } from "./basic-tool"
|
||||
import { animate } from "motion"
|
||||
|
||||
export default {
|
||||
title: "UI/Shell Submessage Motion",
|
||||
id: "components-shell-submessage-motion",
|
||||
tags: ["autodocs"],
|
||||
parameters: {
|
||||
docs: {
|
||||
description: {
|
||||
component: `### Overview
|
||||
Interactive playground for animating the Shell tool subtitle ("submessage") in the timeline trigger row.
|
||||
|
||||
### Production component path
|
||||
- Trigger layout: \`packages/ui/src/components/basic-tool.tsx\`
|
||||
- Bash tool subtitle source: \`packages/ui/src/components/message-part.tsx\` (tool: \`bash\`, \`trigger.subtitle\`)
|
||||
|
||||
### What this playground tunes
|
||||
- Width reveal (spring-driven pixel width via \`useSpring\`)
|
||||
- Opacity fade
|
||||
- Blur settle`,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
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 sliderLabel = {
|
||||
"font-size": "11px",
|
||||
"font-family": "monospace",
|
||||
color: "var(--color-text-weak, #666)",
|
||||
"min-width": "84px",
|
||||
"flex-shrink": "0",
|
||||
"text-align": "right",
|
||||
}
|
||||
|
||||
const sliderValue = {
|
||||
"font-family": "monospace",
|
||||
"font-size": "11px",
|
||||
color: "var(--color-text-weak, #aaa)",
|
||||
"min-width": "76px",
|
||||
}
|
||||
|
||||
const shellCss = `
|
||||
[data-component="shell-submessage-scene"] [data-component="tool-trigger"] [data-slot="basic-tool-tool-info-main"] {
|
||||
align-items: baseline;
|
||||
}
|
||||
|
||||
[data-component="shell-submessage"] {
|
||||
min-width: 0;
|
||||
max-width: 100%;
|
||||
display: inline-flex;
|
||||
align-items: baseline;
|
||||
vertical-align: baseline;
|
||||
}
|
||||
|
||||
[data-component="shell-submessage"] [data-slot="shell-submessage-width"] {
|
||||
min-width: 0;
|
||||
max-width: 100%;
|
||||
display: inline-flex;
|
||||
align-items: baseline;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
[data-component="shell-submessage"] [data-slot="shell-submessage-value"] {
|
||||
display: inline-block;
|
||||
vertical-align: baseline;
|
||||
min-width: 0;
|
||||
line-height: inherit;
|
||||
white-space: nowrap;
|
||||
opacity: 0;
|
||||
filter: blur(var(--shell-sub-blur, 2px));
|
||||
transition-property: opacity, filter;
|
||||
transition-duration: var(--shell-sub-fade-ms, 320ms);
|
||||
transition-timing-function: var(--shell-sub-fade-ease, cubic-bezier(0.22, 1, 0.36, 1));
|
||||
}
|
||||
|
||||
[data-component="shell-submessage"][data-visible] [data-slot="shell-submessage-value"] {
|
||||
opacity: 1;
|
||||
filter: blur(0px);
|
||||
}
|
||||
`
|
||||
|
||||
const ease = {
|
||||
smooth: "cubic-bezier(0.16, 1, 0.3, 1)",
|
||||
snappy: "cubic-bezier(0.22, 1, 0.36, 1)",
|
||||
standard: "cubic-bezier(0.2, 0.8, 0.2, 1)",
|
||||
linear: "linear",
|
||||
}
|
||||
|
||||
function SpringSubmessage(props: { text: string; visible: boolean; visualDuration: number; bounce: number }) {
|
||||
let ref: HTMLSpanElement | undefined
|
||||
let widthRef: HTMLSpanElement | undefined
|
||||
|
||||
createEffect(() => {
|
||||
if (!widthRef) return
|
||||
if (props.visible) {
|
||||
requestAnimationFrame(() => {
|
||||
ref?.setAttribute("data-visible", "")
|
||||
animate(
|
||||
widthRef!,
|
||||
{ width: "auto" },
|
||||
{ type: "spring", visualDuration: props.visualDuration, bounce: props.bounce },
|
||||
)
|
||||
})
|
||||
} else {
|
||||
ref?.removeAttribute("data-visible")
|
||||
animate(
|
||||
widthRef,
|
||||
{ width: "0px" },
|
||||
{ type: "spring", visualDuration: props.visualDuration, bounce: props.bounce },
|
||||
)
|
||||
}
|
||||
})
|
||||
|
||||
return (
|
||||
<span ref={ref} data-component="shell-submessage">
|
||||
<span ref={widthRef} data-slot="shell-submessage-width" style={{ width: "0px" }}>
|
||||
<span data-slot="basic-tool-tool-subtitle">
|
||||
<span data-slot="shell-submessage-value">{props.text || "\u00A0"}</span>
|
||||
</span>
|
||||
</span>
|
||||
</span>
|
||||
)
|
||||
}
|
||||
|
||||
export const Playground = {
|
||||
render: () => {
|
||||
const [text, setText] = createSignal("Prints five topic blocks between timed commands")
|
||||
const [show, setShow] = createSignal(true)
|
||||
const [visualDuration, setVisualDuration] = createSignal(0.35)
|
||||
const [bounce, setBounce] = createSignal(0)
|
||||
const [fadeMs, setFadeMs] = createSignal(320)
|
||||
const [blur, setBlur] = createSignal(2)
|
||||
const [fadeEase, setFadeEase] = createSignal<keyof typeof ease>("snappy")
|
||||
const [auto, setAuto] = createSignal(false)
|
||||
let replayTimer
|
||||
let autoTimer
|
||||
|
||||
const replay = () => {
|
||||
setShow(false)
|
||||
if (replayTimer) clearTimeout(replayTimer)
|
||||
replayTimer = setTimeout(() => {
|
||||
setShow(true)
|
||||
}, 50)
|
||||
}
|
||||
|
||||
const stopAuto = () => {
|
||||
if (autoTimer) clearInterval(autoTimer)
|
||||
autoTimer = undefined
|
||||
setAuto(false)
|
||||
}
|
||||
|
||||
const toggleAuto = () => {
|
||||
if (auto()) {
|
||||
stopAuto()
|
||||
return
|
||||
}
|
||||
setAuto(true)
|
||||
autoTimer = setInterval(replay, 2200)
|
||||
}
|
||||
|
||||
onCleanup(() => {
|
||||
if (replayTimer) clearTimeout(replayTimer)
|
||||
if (autoTimer) clearInterval(autoTimer)
|
||||
})
|
||||
|
||||
return (
|
||||
<div
|
||||
data-component="shell-submessage-scene"
|
||||
style={{
|
||||
display: "grid",
|
||||
gap: "20px",
|
||||
padding: "20px",
|
||||
"max-width": "860px",
|
||||
"--shell-sub-fade-ms": `${fadeMs()}ms`,
|
||||
"--shell-sub-blur": `${blur()}px`,
|
||||
"--shell-sub-fade-ease": ease[fadeEase()],
|
||||
}}
|
||||
>
|
||||
<style>{shellCss}</style>
|
||||
|
||||
<BasicTool
|
||||
icon="console"
|
||||
defaultOpen
|
||||
trigger={
|
||||
<div data-slot="basic-tool-tool-info-structured">
|
||||
<div data-slot="basic-tool-tool-info-main">
|
||||
<span data-slot="basic-tool-tool-title">Shell</span>
|
||||
<SpringSubmessage text={text()} visible={show()} visualDuration={visualDuration()} bounce={bounce()} />
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
"border-radius": "8px",
|
||||
border: "1px solid var(--color-divider, #333)",
|
||||
background: "var(--color-fill-secondary, #161616)",
|
||||
padding: "14px 16px",
|
||||
"font-family": "ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace",
|
||||
"font-size": "18px",
|
||||
color: "var(--color-text, #eee)",
|
||||
"white-space": "pre-wrap",
|
||||
}}
|
||||
>
|
||||
{"$ cat <<'TOPIC1'"}
|
||||
</div>
|
||||
</BasicTool>
|
||||
|
||||
<div style={{ display: "flex", gap: "8px", "flex-wrap": "wrap" }}>
|
||||
<button onClick={replay} style={btn()}>
|
||||
Replay entry
|
||||
</button>
|
||||
<button onClick={() => setShow((v) => !v)} style={btn(show())}>
|
||||
{show() ? "Hide subtitle" : "Show subtitle"}
|
||||
</button>
|
||||
<button onClick={toggleAuto} style={btn(auto())}>
|
||||
{auto() ? "Stop auto replay" : "Auto replay"}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div
|
||||
style={{
|
||||
display: "grid",
|
||||
gap: "10px",
|
||||
"border-top": "1px solid var(--color-divider, #333)",
|
||||
"padding-top": "14px",
|
||||
}}
|
||||
>
|
||||
<div style={{ display: "flex", "align-items": "center", gap: "12px" }}>
|
||||
<span style={sliderLabel}>subtitle</span>
|
||||
<input
|
||||
value={text()}
|
||||
onInput={(e) => setText(e.currentTarget.value)}
|
||||
style={{
|
||||
width: "420px",
|
||||
"max-width": "100%",
|
||||
padding: "6px 8px",
|
||||
"border-radius": "6px",
|
||||
border: "1px solid var(--color-divider, #333)",
|
||||
background: "var(--color-fill-element, #222)",
|
||||
color: "var(--color-text, #eee)",
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div style={{ display: "flex", "align-items": "center", gap: "12px" }}>
|
||||
<span style={sliderLabel}>visualDuration</span>
|
||||
<input
|
||||
type="range"
|
||||
min={0.05}
|
||||
max={1.5}
|
||||
step={0.01}
|
||||
value={visualDuration()}
|
||||
onInput={(e) => setVisualDuration(Number(e.currentTarget.value))}
|
||||
/>
|
||||
<span style={sliderValue}>{visualDuration().toFixed(2)}s</span>
|
||||
</div>
|
||||
|
||||
<div style={{ display: "flex", "align-items": "center", gap: "12px" }}>
|
||||
<span style={sliderLabel}>bounce</span>
|
||||
<input
|
||||
type="range"
|
||||
min={0}
|
||||
max={0.5}
|
||||
step={0.01}
|
||||
value={bounce()}
|
||||
onInput={(e) => setBounce(Number(e.currentTarget.value))}
|
||||
/>
|
||||
<span style={sliderValue}>{bounce().toFixed(2)}</span>
|
||||
</div>
|
||||
|
||||
<div style={{ display: "flex", "align-items": "center", gap: "12px" }}>
|
||||
<span style={sliderLabel}>fade ease</span>
|
||||
<button
|
||||
onClick={() =>
|
||||
setFadeEase((v) =>
|
||||
v === "snappy" ? "smooth" : v === "smooth" ? "standard" : v === "standard" ? "linear" : "snappy",
|
||||
)
|
||||
}
|
||||
style={btn()}
|
||||
>
|
||||
{fadeEase()}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div style={{ display: "flex", "align-items": "center", gap: "12px" }}>
|
||||
<span style={sliderLabel}>fade</span>
|
||||
<input
|
||||
type="range"
|
||||
min={0}
|
||||
max={1400}
|
||||
step={10}
|
||||
value={fadeMs()}
|
||||
onInput={(e) => setFadeMs(Number(e.currentTarget.value))}
|
||||
/>
|
||||
<span style={sliderValue}>{fadeMs()}ms</span>
|
||||
</div>
|
||||
|
||||
<div style={{ display: "flex", "align-items": "center", gap: "12px" }}>
|
||||
<span style={sliderLabel}>blur</span>
|
||||
<input
|
||||
type="range"
|
||||
min={0}
|
||||
max={14}
|
||||
step={0.5}
|
||||
value={blur()}
|
||||
onInput={(e) => setBlur(Number(e.currentTarget.value))}
|
||||
/>
|
||||
<span style={sliderValue}>{blur()}px</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
},
|
||||
}
|
||||
Reference in New Issue
Block a user