280 lines
8.4 KiB
TypeScript
280 lines
8.4 KiB
TypeScript
// @ts-nocheck
|
|
import { createSignal, onMount } from "solid-js"
|
|
import { createResizeObserver } from "@solid-primitives/resize-observer"
|
|
import { createStore } from "solid-js/store"
|
|
import { useSpring } from "./motion-spring"
|
|
import { TextStrikethrough } from "./text-strikethrough"
|
|
|
|
const TEXT_SHORT = "Remove inline measure nodes"
|
|
const TEXT_MED = "Remove inline measure nodes and keep width morph behavior intact"
|
|
const TEXT_LONG =
|
|
"Refactor ToolStatusTitle DOM measurement to offscreen global measurer (unconstrained by timeline layout)"
|
|
|
|
const btn = (active?: boolean) =>
|
|
({
|
|
padding: "8px 18px",
|
|
"border-radius": "6px",
|
|
border: "1px solid var(--color-divider, #444)",
|
|
background: active ? "var(--color-accent, #58f)" : "var(--color-fill-element, #222)",
|
|
color: "var(--color-text, #eee)",
|
|
cursor: "pointer",
|
|
"font-size": "14px",
|
|
"font-weight": "500",
|
|
}) as const
|
|
|
|
const heading = {
|
|
"font-size": "11px",
|
|
"font-weight": "600",
|
|
"text-transform": "uppercase" as const,
|
|
"letter-spacing": "0.05em",
|
|
color: "var(--text-weak, #888)",
|
|
"margin-bottom": "4px",
|
|
}
|
|
|
|
const card = {
|
|
padding: "16px 20px",
|
|
"border-radius": "10px",
|
|
border: "1px solid var(--border-weak-base, #333)",
|
|
background: "var(--surface-base, #1a1a1a)",
|
|
}
|
|
|
|
/* ─── Variant A: scaleX pseudo-line at 50% ─── */
|
|
function VariantA(props: { active: boolean; text: string }) {
|
|
const progress = useSpring(
|
|
() => (props.active ? 1 : 0),
|
|
() => ({ visualDuration: 0.35, bounce: 0 }),
|
|
)
|
|
return (
|
|
<span
|
|
style={{
|
|
position: "relative",
|
|
display: "block",
|
|
color: props.active ? "var(--text-weak, #888)" : "var(--text-strong, #eee)",
|
|
transition: "color 220ms ease",
|
|
}}
|
|
>
|
|
{props.text}
|
|
<span
|
|
style={{
|
|
position: "absolute",
|
|
left: "0",
|
|
right: "0",
|
|
top: "50%",
|
|
height: "1.5px",
|
|
background: "currentColor",
|
|
"transform-origin": "left center",
|
|
transform: `scaleX(${progress()})`,
|
|
"pointer-events": "none",
|
|
}}
|
|
/>
|
|
</span>
|
|
)
|
|
}
|
|
|
|
/* ─── Variant D: background-image line ─── */
|
|
function VariantD(props: { active: boolean; text: string }) {
|
|
const progress = useSpring(
|
|
() => (props.active ? 1 : 0),
|
|
() => ({ visualDuration: 0.35, bounce: 0 }),
|
|
)
|
|
return (
|
|
<span
|
|
style={{
|
|
display: "block",
|
|
color: props.active ? "var(--text-weak, #888)" : "var(--text-strong, #eee)",
|
|
transition: "color 220ms ease",
|
|
"background-image": "linear-gradient(currentColor, currentColor)",
|
|
"background-repeat": "no-repeat",
|
|
"background-size": `${progress() * 100}% 1.5px`,
|
|
"background-position": "left center",
|
|
}}
|
|
>
|
|
{props.text}
|
|
</span>
|
|
)
|
|
}
|
|
|
|
/* ─── Variant E: grid stacking + clip-path (container %) ─── */
|
|
function VariantE(props: { active: boolean; text: string }) {
|
|
const progress = useSpring(
|
|
() => (props.active ? 1 : 0),
|
|
() => ({ visualDuration: 0.35, bounce: 0 }),
|
|
)
|
|
return (
|
|
<span
|
|
style={{
|
|
display: "grid",
|
|
color: props.active ? "var(--text-weak, #888)" : "var(--text-strong, #eee)",
|
|
transition: "color 220ms ease",
|
|
}}
|
|
>
|
|
<span style={{ "grid-area": "1 / 1" }}>{props.text}</span>
|
|
<span
|
|
aria-hidden="true"
|
|
style={{
|
|
"grid-area": "1 / 1",
|
|
"text-decoration": "line-through",
|
|
"pointer-events": "none",
|
|
"clip-path": `inset(0 ${(1 - progress()) * 100}% 0 0)`,
|
|
}}
|
|
>
|
|
{props.text}
|
|
</span>
|
|
</span>
|
|
)
|
|
}
|
|
|
|
/* ─── Variant F: grid stacking + clip-path mapped to text width ─── */
|
|
function VariantF(props: { active: boolean; text: string }) {
|
|
const progress = useSpring(
|
|
() => (props.active ? 1 : 0),
|
|
() => ({ visualDuration: 0.35, bounce: 0 }),
|
|
)
|
|
let baseRef: HTMLSpanElement | undefined
|
|
let containerRef: HTMLSpanElement | undefined
|
|
const [state, setState] = createStore({
|
|
textWidth: 0,
|
|
containerWidth: 0,
|
|
})
|
|
const textWidth = () => state.textWidth
|
|
const containerWidth = () => state.containerWidth
|
|
|
|
const measure = () => {
|
|
if (baseRef) setState("textWidth", baseRef.scrollWidth)
|
|
if (containerRef) setState("containerWidth", containerRef.offsetWidth)
|
|
}
|
|
|
|
onMount(measure)
|
|
createResizeObserver(() => containerRef, measure)
|
|
|
|
const clipRight = () => {
|
|
const cw = containerWidth()
|
|
const tw = textWidth()
|
|
if (cw <= 0 || tw <= 0) return `${(1 - progress()) * 100}%`
|
|
const revealed = progress() * tw
|
|
const remaining = Math.max(0, cw - revealed)
|
|
return `${remaining}px`
|
|
}
|
|
|
|
return (
|
|
<span
|
|
ref={containerRef}
|
|
style={{
|
|
display: "grid",
|
|
color: props.active ? "var(--text-weak, #888)" : "var(--text-strong, #eee)",
|
|
transition: "color 220ms ease",
|
|
}}
|
|
>
|
|
<span ref={baseRef} style={{ "grid-area": "1 / 1" }}>
|
|
{props.text}
|
|
</span>
|
|
<span
|
|
aria-hidden="true"
|
|
style={{
|
|
"grid-area": "1 / 1",
|
|
"text-decoration": "line-through",
|
|
"pointer-events": "none",
|
|
"clip-path": `inset(0 ${clipRight()} 0 0)`,
|
|
}}
|
|
>
|
|
{props.text}
|
|
</span>
|
|
</span>
|
|
)
|
|
}
|
|
|
|
export default {
|
|
title: "UI/Text Strikethrough",
|
|
id: "components-text-strikethrough",
|
|
tags: ["autodocs"],
|
|
parameters: {
|
|
docs: {
|
|
description: {
|
|
component: `### Animated Strikethrough Variants
|
|
|
|
- **A** — scaleX line at 50% (single line only)
|
|
- **D** — background-image line (single line only)
|
|
- **E** — grid stacking + clip-path (container %)
|
|
- **F** — grid stacking + clip-path mapped to text width (the real component)`,
|
|
},
|
|
},
|
|
},
|
|
}
|
|
|
|
export const Playground = {
|
|
render: () => {
|
|
const [active, setActive] = createSignal(false)
|
|
const toggle = () => setActive((v) => !v)
|
|
|
|
return (
|
|
<div style={{ display: "grid", gap: "24px", padding: "24px", "max-width": "700px" }}>
|
|
<button onClick={toggle} style={btn(active())}>
|
|
{active() ? "Undo strikethrough" : "Strike through all"}
|
|
</button>
|
|
|
|
<div style={card}>
|
|
<div style={heading}>F — grid stacking + clip mapped to text width (THE COMPONENT)</div>
|
|
<TextStrikethrough
|
|
active={active()}
|
|
text={TEXT_SHORT}
|
|
style={{
|
|
color: active() ? "var(--text-weak, #888)" : "var(--text-strong, #eee)",
|
|
transition: "color 220ms ease",
|
|
}}
|
|
/>
|
|
<div style={{ "margin-top": "12px" }} />
|
|
<TextStrikethrough
|
|
active={active()}
|
|
text={TEXT_MED}
|
|
style={{
|
|
color: active() ? "var(--text-weak, #888)" : "var(--text-strong, #eee)",
|
|
transition: "color 220ms ease",
|
|
}}
|
|
/>
|
|
<div style={{ "margin-top": "12px" }} />
|
|
<TextStrikethrough
|
|
active={active()}
|
|
text={TEXT_LONG}
|
|
style={{
|
|
color: active() ? "var(--text-weak, #888)" : "var(--text-strong, #eee)",
|
|
transition: "color 220ms ease",
|
|
}}
|
|
/>
|
|
</div>
|
|
|
|
<div style={card}>
|
|
<div style={heading}>F (inline) — same but just inline variants</div>
|
|
<VariantF active={active()} text={TEXT_SHORT} />
|
|
<div style={{ "margin-top": "12px" }} />
|
|
<VariantF active={active()} text={TEXT_MED} />
|
|
<div style={{ "margin-top": "12px" }} />
|
|
<VariantF active={active()} text={TEXT_LONG} />
|
|
</div>
|
|
|
|
<div style={card}>
|
|
<div style={heading}>E — grid stacking + clip-path (container %)</div>
|
|
<VariantE active={active()} text={TEXT_SHORT} />
|
|
<div style={{ "margin-top": "12px" }} />
|
|
<VariantE active={active()} text={TEXT_MED} />
|
|
<div style={{ "margin-top": "12px" }} />
|
|
<VariantE active={active()} text={TEXT_LONG} />
|
|
</div>
|
|
|
|
<div style={card}>
|
|
<div style={heading}>A — scaleX line at 50%</div>
|
|
<VariantA active={active()} text={TEXT_SHORT} />
|
|
<div style={{ "margin-top": "12px" }} />
|
|
<VariantA active={active()} text={TEXT_LONG} />
|
|
</div>
|
|
|
|
<div style={card}>
|
|
<div style={heading}>D — background-image line</div>
|
|
<VariantD active={active()} text={TEXT_SHORT} />
|
|
<div style={{ "margin-top": "12px" }} />
|
|
<VariantD active={active()} text={TEXT_LONG} />
|
|
</div>
|
|
</div>
|
|
)
|
|
},
|
|
}
|