text-strikethrough.stories.tsx 8.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279
  1. // @ts-nocheck
  2. import { createSignal, onMount } from "solid-js"
  3. import { createResizeObserver } from "@solid-primitives/resize-observer"
  4. import { createStore } from "solid-js/store"
  5. import { useSpring } from "./motion-spring"
  6. import { TextStrikethrough } from "./text-strikethrough"
  7. const TEXT_SHORT = "Remove inline measure nodes"
  8. const TEXT_MED = "Remove inline measure nodes and keep width morph behavior intact"
  9. const TEXT_LONG =
  10. "Refactor ToolStatusTitle DOM measurement to offscreen global measurer (unconstrained by timeline layout)"
  11. const btn = (active?: boolean) =>
  12. ({
  13. padding: "8px 18px",
  14. "border-radius": "6px",
  15. border: "1px solid var(--color-divider, #444)",
  16. background: active ? "var(--color-accent, #58f)" : "var(--color-fill-element, #222)",
  17. color: "var(--color-text, #eee)",
  18. cursor: "pointer",
  19. "font-size": "14px",
  20. "font-weight": "500",
  21. }) as const
  22. const heading = {
  23. "font-size": "11px",
  24. "font-weight": "600",
  25. "text-transform": "uppercase" as const,
  26. "letter-spacing": "0.05em",
  27. color: "var(--text-weak, #888)",
  28. "margin-bottom": "4px",
  29. }
  30. const card = {
  31. padding: "16px 20px",
  32. "border-radius": "10px",
  33. border: "1px solid var(--border-weak-base, #333)",
  34. background: "var(--surface-base, #1a1a1a)",
  35. }
  36. /* ─── Variant A: scaleX pseudo-line at 50% ─── */
  37. function VariantA(props: { active: boolean; text: string }) {
  38. const progress = useSpring(
  39. () => (props.active ? 1 : 0),
  40. () => ({ visualDuration: 0.35, bounce: 0 }),
  41. )
  42. return (
  43. <span
  44. style={{
  45. position: "relative",
  46. display: "block",
  47. color: props.active ? "var(--text-weak, #888)" : "var(--text-strong, #eee)",
  48. transition: "color 220ms ease",
  49. }}
  50. >
  51. {props.text}
  52. <span
  53. style={{
  54. position: "absolute",
  55. left: "0",
  56. right: "0",
  57. top: "50%",
  58. height: "1.5px",
  59. background: "currentColor",
  60. "transform-origin": "left center",
  61. transform: `scaleX(${progress()})`,
  62. "pointer-events": "none",
  63. }}
  64. />
  65. </span>
  66. )
  67. }
  68. /* ─── Variant D: background-image line ─── */
  69. function VariantD(props: { active: boolean; text: string }) {
  70. const progress = useSpring(
  71. () => (props.active ? 1 : 0),
  72. () => ({ visualDuration: 0.35, bounce: 0 }),
  73. )
  74. return (
  75. <span
  76. style={{
  77. display: "block",
  78. color: props.active ? "var(--text-weak, #888)" : "var(--text-strong, #eee)",
  79. transition: "color 220ms ease",
  80. "background-image": "linear-gradient(currentColor, currentColor)",
  81. "background-repeat": "no-repeat",
  82. "background-size": `${progress() * 100}% 1.5px`,
  83. "background-position": "left center",
  84. }}
  85. >
  86. {props.text}
  87. </span>
  88. )
  89. }
  90. /* ─── Variant E: grid stacking + clip-path (container %) ─── */
  91. function VariantE(props: { active: boolean; text: string }) {
  92. const progress = useSpring(
  93. () => (props.active ? 1 : 0),
  94. () => ({ visualDuration: 0.35, bounce: 0 }),
  95. )
  96. return (
  97. <span
  98. style={{
  99. display: "grid",
  100. color: props.active ? "var(--text-weak, #888)" : "var(--text-strong, #eee)",
  101. transition: "color 220ms ease",
  102. }}
  103. >
  104. <span style={{ "grid-area": "1 / 1" }}>{props.text}</span>
  105. <span
  106. aria-hidden="true"
  107. style={{
  108. "grid-area": "1 / 1",
  109. "text-decoration": "line-through",
  110. "pointer-events": "none",
  111. "clip-path": `inset(0 ${(1 - progress()) * 100}% 0 0)`,
  112. }}
  113. >
  114. {props.text}
  115. </span>
  116. </span>
  117. )
  118. }
  119. /* ─── Variant F: grid stacking + clip-path mapped to text width ─── */
  120. function VariantF(props: { active: boolean; text: string }) {
  121. const progress = useSpring(
  122. () => (props.active ? 1 : 0),
  123. () => ({ visualDuration: 0.35, bounce: 0 }),
  124. )
  125. let baseRef: HTMLSpanElement | undefined
  126. let containerRef: HTMLSpanElement | undefined
  127. const [state, setState] = createStore({
  128. textWidth: 0,
  129. containerWidth: 0,
  130. })
  131. const textWidth = () => state.textWidth
  132. const containerWidth = () => state.containerWidth
  133. const measure = () => {
  134. if (baseRef) setState("textWidth", baseRef.scrollWidth)
  135. if (containerRef) setState("containerWidth", containerRef.offsetWidth)
  136. }
  137. onMount(measure)
  138. createResizeObserver(() => containerRef, measure)
  139. const clipRight = () => {
  140. const cw = containerWidth()
  141. const tw = textWidth()
  142. if (cw <= 0 || tw <= 0) return `${(1 - progress()) * 100}%`
  143. const revealed = progress() * tw
  144. const remaining = Math.max(0, cw - revealed)
  145. return `${remaining}px`
  146. }
  147. return (
  148. <span
  149. ref={containerRef}
  150. style={{
  151. display: "grid",
  152. color: props.active ? "var(--text-weak, #888)" : "var(--text-strong, #eee)",
  153. transition: "color 220ms ease",
  154. }}
  155. >
  156. <span ref={baseRef} style={{ "grid-area": "1 / 1" }}>
  157. {props.text}
  158. </span>
  159. <span
  160. aria-hidden="true"
  161. style={{
  162. "grid-area": "1 / 1",
  163. "text-decoration": "line-through",
  164. "pointer-events": "none",
  165. "clip-path": `inset(0 ${clipRight()} 0 0)`,
  166. }}
  167. >
  168. {props.text}
  169. </span>
  170. </span>
  171. )
  172. }
  173. export default {
  174. title: "UI/Text Strikethrough",
  175. id: "components-text-strikethrough",
  176. tags: ["autodocs"],
  177. parameters: {
  178. docs: {
  179. description: {
  180. component: `### Animated Strikethrough Variants
  181. - **A** — scaleX line at 50% (single line only)
  182. - **D** — background-image line (single line only)
  183. - **E** — grid stacking + clip-path (container %)
  184. - **F** — grid stacking + clip-path mapped to text width (the real component)`,
  185. },
  186. },
  187. },
  188. }
  189. export const Playground = {
  190. render: () => {
  191. const [active, setActive] = createSignal(false)
  192. const toggle = () => setActive((v) => !v)
  193. return (
  194. <div style={{ display: "grid", gap: "24px", padding: "24px", "max-width": "700px" }}>
  195. <button onClick={toggle} style={btn(active())}>
  196. {active() ? "Undo strikethrough" : "Strike through all"}
  197. </button>
  198. <div style={card}>
  199. <div style={heading}>F — grid stacking + clip mapped to text width (THE COMPONENT)</div>
  200. <TextStrikethrough
  201. active={active()}
  202. text={TEXT_SHORT}
  203. style={{
  204. color: active() ? "var(--text-weak, #888)" : "var(--text-strong, #eee)",
  205. transition: "color 220ms ease",
  206. }}
  207. />
  208. <div style={{ "margin-top": "12px" }} />
  209. <TextStrikethrough
  210. active={active()}
  211. text={TEXT_MED}
  212. style={{
  213. color: active() ? "var(--text-weak, #888)" : "var(--text-strong, #eee)",
  214. transition: "color 220ms ease",
  215. }}
  216. />
  217. <div style={{ "margin-top": "12px" }} />
  218. <TextStrikethrough
  219. active={active()}
  220. text={TEXT_LONG}
  221. style={{
  222. color: active() ? "var(--text-weak, #888)" : "var(--text-strong, #eee)",
  223. transition: "color 220ms ease",
  224. }}
  225. />
  226. </div>
  227. <div style={card}>
  228. <div style={heading}>F (inline) — same but just inline variants</div>
  229. <VariantF active={active()} text={TEXT_SHORT} />
  230. <div style={{ "margin-top": "12px" }} />
  231. <VariantF active={active()} text={TEXT_MED} />
  232. <div style={{ "margin-top": "12px" }} />
  233. <VariantF active={active()} text={TEXT_LONG} />
  234. </div>
  235. <div style={card}>
  236. <div style={heading}>E — grid stacking + clip-path (container %)</div>
  237. <VariantE active={active()} text={TEXT_SHORT} />
  238. <div style={{ "margin-top": "12px" }} />
  239. <VariantE active={active()} text={TEXT_MED} />
  240. <div style={{ "margin-top": "12px" }} />
  241. <VariantE active={active()} text={TEXT_LONG} />
  242. </div>
  243. <div style={card}>
  244. <div style={heading}>A — scaleX line at 50%</div>
  245. <VariantA active={active()} text={TEXT_SHORT} />
  246. <div style={{ "margin-top": "12px" }} />
  247. <VariantA active={active()} text={TEXT_LONG} />
  248. </div>
  249. <div style={card}>
  250. <div style={heading}>D — background-image line</div>
  251. <VariantD active={active()} text={TEXT_SHORT} />
  252. <div style={{ "margin-top": "12px" }} />
  253. <VariantD active={active()} text={TEXT_LONG} />
  254. </div>
  255. </div>
  256. )
  257. },
  258. }