tool-count-summary.stories.tsx 7.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238
  1. // @ts-nocheck
  2. import { onCleanup } from "solid-js"
  3. import { createStore } from "solid-js/store"
  4. import { AnimatedCountList, type CountItem } from "./tool-count-summary"
  5. import { ToolStatusTitle } from "./tool-status-title"
  6. export default {
  7. title: "UI/AnimatedCountList",
  8. id: "components-animated-count-list",
  9. tags: ["autodocs"],
  10. parameters: {
  11. docs: {
  12. description: {
  13. component: `### Overview
  14. Animated count list that smoothly transitions items in/out as counts change.
  15. Uses \`grid-template-columns: 0fr → 1fr\` for width animations and the odometer
  16. digit roller for count transitions. Shown here with \`ToolStatusTitle\` exactly
  17. as it appears in the context tool group on the session page.`,
  18. },
  19. },
  20. },
  21. }
  22. const TEXT = {
  23. active: "Exploring",
  24. done: "Explored",
  25. read: { one: "{{count}} read", other: "{{count}} reads" },
  26. search: { one: "{{count}} search", other: "{{count}} searches" },
  27. list: { one: "{{count}} list", other: "{{count}} lists" },
  28. } as const
  29. function rand(min: number, max: number) {
  30. return Math.floor(Math.random() * (max - min + 1)) + min
  31. }
  32. const btn = (accent?: boolean) =>
  33. ({
  34. padding: "6px 14px",
  35. "border-radius": "6px",
  36. border: "1px solid var(--color-divider, #333)",
  37. background: accent ? "var(--color-danger-fill, #c33)" : "var(--color-fill-element, #222)",
  38. color: "var(--color-text, #eee)",
  39. cursor: "pointer",
  40. "font-size": "13px",
  41. }) as const
  42. const smallBtn = (active?: boolean) =>
  43. ({
  44. padding: "4px 12px",
  45. "border-radius": "6px",
  46. border: active ? "1px solid var(--color-accent, #58f)" : "1px solid var(--color-divider, #333)",
  47. background: active ? "var(--color-accent, #58f)" : "var(--color-fill-element, #222)",
  48. color: "var(--color-text, #eee)",
  49. cursor: "pointer",
  50. "font-size": "12px",
  51. }) as const
  52. export const Playground = {
  53. render: () => {
  54. const [state, setState] = createStore({
  55. reads: 0,
  56. searches: 0,
  57. lists: 0,
  58. active: false,
  59. reducedMotion: false,
  60. })
  61. const reads = () => state.reads
  62. const searches = () => state.searches
  63. const lists = () => state.lists
  64. const active = () => state.active
  65. const reducedMotion = () => state.reducedMotion
  66. let timeouts: ReturnType<typeof setTimeout>[] = []
  67. const clearAll = () => {
  68. for (const t of timeouts) clearTimeout(t)
  69. timeouts = []
  70. }
  71. onCleanup(clearAll)
  72. const startSim = () => {
  73. clearAll()
  74. setState("reads", 0)
  75. setState("searches", 0)
  76. setState("lists", 0)
  77. setState("active", true)
  78. const steps = rand(3, 10)
  79. let elapsed = 0
  80. for (let i = 0; i < steps; i++) {
  81. const delay = rand(300, 800)
  82. elapsed += delay
  83. const t = setTimeout(() => {
  84. const pick = rand(0, 2)
  85. if (pick === 0) setState("reads", (value) => value + 1)
  86. else if (pick === 1) setState("searches", (value) => value + 1)
  87. else setState("lists", (value) => value + 1)
  88. }, elapsed)
  89. timeouts.push(t)
  90. }
  91. const end = setTimeout(() => setState("active", false), elapsed + 100)
  92. timeouts.push(end)
  93. }
  94. const stopSim = () => {
  95. clearAll()
  96. setState("active", false)
  97. }
  98. const reset = () => {
  99. stopSim()
  100. setState("reads", 0)
  101. setState("searches", 0)
  102. setState("lists", 0)
  103. }
  104. const items = (): CountItem[] => [
  105. { key: "read", count: reads(), one: TEXT.read.one, other: TEXT.read.other },
  106. { key: "search", count: searches(), one: TEXT.search.one, other: TEXT.search.other },
  107. { key: "list", count: lists(), one: TEXT.list.one, other: TEXT.list.other },
  108. ]
  109. return (
  110. <div style={{ display: "grid", gap: "24px", padding: "20px", "max-width": "520px" }}>
  111. {reducedMotion() && (
  112. <style>
  113. {`[data-reduced-motion="true"] *,
  114. [data-reduced-motion="true"] *::before,
  115. [data-reduced-motion="true"] *::after {
  116. transition-duration: 0ms !important;
  117. }`}
  118. </style>
  119. )}
  120. {/* Matches context-tool-group-trigger layout from message-part.tsx */}
  121. <span
  122. data-reduced-motion={reducedMotion()}
  123. style={{
  124. display: "flex",
  125. "align-items": "center",
  126. gap: "8px",
  127. "font-size": "14px",
  128. "font-weight": "500",
  129. color: "var(--text-strong, #eee)",
  130. "min-width": "0",
  131. }}
  132. >
  133. <span style={{ "flex-shrink": "0" }}>
  134. <ToolStatusTitle active={active()} activeText={TEXT.active} doneText={TEXT.done} split={false} />
  135. </span>
  136. <span
  137. style={{
  138. "min-width": "0",
  139. overflow: "hidden",
  140. "text-overflow": "ellipsis",
  141. "white-space": "nowrap",
  142. "font-weight": "400",
  143. color: "var(--text-base, #ccc)",
  144. }}
  145. >
  146. <AnimatedCountList items={items()} fallback="" />
  147. </span>
  148. </span>
  149. <div style={{ display: "flex", gap: "8px", "flex-wrap": "wrap" }}>
  150. <button onClick={() => (active() ? stopSim() : startSim())} style={btn(active())}>
  151. {active() ? "Stop" : "Simulate"}
  152. </button>
  153. <button onClick={reset} style={btn()}>
  154. Reset
  155. </button>
  156. <button onClick={() => setState("reducedMotion", (value) => !value)} style={smallBtn(reducedMotion())}>
  157. {reducedMotion() ? "Motion: reduced" : "Motion: normal"}
  158. </button>
  159. </div>
  160. <div style={{ display: "flex", gap: "8px", "flex-wrap": "wrap" }}>
  161. <button onClick={() => setState("reads", (value) => value + 1)} style={smallBtn()}>
  162. + read
  163. </button>
  164. <button onClick={() => setState("searches", (value) => value + 1)} style={smallBtn()}>
  165. + search
  166. </button>
  167. <button onClick={() => setState("lists", (value) => value + 1)} style={smallBtn()}>
  168. + list
  169. </button>
  170. </div>
  171. <div
  172. style={{
  173. "font-size": "11px",
  174. color: "var(--color-text-weak, #888)",
  175. "font-family": "monospace",
  176. }}
  177. >
  178. motion: {reducedMotion() ? "reduced" : "normal"} · active: {active() ? "true" : "false"} · reads: {reads()} ·
  179. searches: {searches()} · lists: {lists()}
  180. </div>
  181. </div>
  182. )
  183. },
  184. }
  185. export const Empty = {
  186. render: () => (
  187. <span style={{ display: "flex", "align-items": "center", gap: "8px", "font-size": "14px", "font-weight": "500" }}>
  188. <ToolStatusTitle active activeText="Exploring" doneText="Explored" split={false} />
  189. <AnimatedCountList
  190. items={[
  191. { key: "read", count: 0, one: "{{count}} read", other: "{{count}} reads" },
  192. { key: "search", count: 0, one: "{{count}} search", other: "{{count}} searches" },
  193. ]}
  194. fallback=""
  195. />
  196. </span>
  197. ),
  198. }
  199. export const Done = {
  200. render: () => (
  201. <span style={{ display: "flex", "align-items": "center", gap: "8px", "font-size": "14px", "font-weight": "500" }}>
  202. <ToolStatusTitle active={false} activeText="Exploring" doneText="Explored" split={false} />
  203. <span style={{ "font-weight": "400", color: "var(--text-base, #ccc)" }}>
  204. <AnimatedCountList
  205. items={[
  206. { key: "read", count: 5, one: "{{count}} read", other: "{{count}} reads" },
  207. { key: "search", count: 3, one: "{{count}} search", other: "{{count}} searches" },
  208. { key: "list", count: 1, one: "{{count}} list", other: "{{count}} lists" },
  209. ]}
  210. fallback=""
  211. />
  212. </span>
  213. </span>
  214. ),
  215. }