@tablestore/tablestore-agent-cli 0.1.7 → 0.1.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (153) hide show
  1. package/dist/dashboard-runtime/dashboard/.next/BUILD_ID +1 -1
  2. package/dist/dashboard-runtime/dashboard/.next/build-manifest.json +3 -3
  3. package/dist/dashboard-runtime/dashboard/.next/prerender-manifest.json +3 -3
  4. package/dist/dashboard-runtime/dashboard/.next/server/app/(dashboard)/chat/page.js.nft.json +1 -1
  5. package/dist/dashboard-runtime/dashboard/.next/server/app/(dashboard)/chat/page_client-reference-manifest.js +1 -1
  6. package/dist/dashboard-runtime/dashboard/.next/server/app/(dashboard)/knowledge-base/[name]/documents/page.js.nft.json +1 -1
  7. package/dist/dashboard-runtime/dashboard/.next/server/app/(dashboard)/knowledge-base/[name]/documents/page_client-reference-manifest.js +1 -1
  8. package/dist/dashboard-runtime/dashboard/.next/server/app/(dashboard)/knowledge-base/[name]/page.js.nft.json +1 -1
  9. package/dist/dashboard-runtime/dashboard/.next/server/app/(dashboard)/knowledge-base/[name]/page_client-reference-manifest.js +1 -1
  10. package/dist/dashboard-runtime/dashboard/.next/server/app/(dashboard)/knowledge-base/[name]/qa/page.js.nft.json +1 -1
  11. package/dist/dashboard-runtime/dashboard/.next/server/app/(dashboard)/knowledge-base/[name]/qa/page_client-reference-manifest.js +1 -1
  12. package/dist/dashboard-runtime/dashboard/.next/server/app/(dashboard)/knowledge-base/[name]/retrieval/page.js.nft.json +1 -1
  13. package/dist/dashboard-runtime/dashboard/.next/server/app/(dashboard)/knowledge-base/[name]/retrieval/page_client-reference-manifest.js +1 -1
  14. package/dist/dashboard-runtime/dashboard/.next/server/app/(dashboard)/knowledge-base/[name]/settings/page.js.nft.json +1 -1
  15. package/dist/dashboard-runtime/dashboard/.next/server/app/(dashboard)/knowledge-base/[name]/settings/page_client-reference-manifest.js +1 -1
  16. package/dist/dashboard-runtime/dashboard/.next/server/app/(dashboard)/knowledge-base/create/page_client-reference-manifest.js +1 -1
  17. package/dist/dashboard-runtime/dashboard/.next/server/app/(dashboard)/knowledge-base/page.js.nft.json +1 -1
  18. package/dist/dashboard-runtime/dashboard/.next/server/app/(dashboard)/knowledge-base/page_client-reference-manifest.js +1 -1
  19. package/dist/dashboard-runtime/dashboard/.next/server/app/(dashboard)/llm-providers/[id]/page_client-reference-manifest.js +1 -1
  20. package/dist/dashboard-runtime/dashboard/.next/server/app/(dashboard)/llm-providers/new/page_client-reference-manifest.js +1 -1
  21. package/dist/dashboard-runtime/dashboard/.next/server/app/(dashboard)/llm-providers/page.js.nft.json +1 -1
  22. package/dist/dashboard-runtime/dashboard/.next/server/app/(dashboard)/llm-providers/page_client-reference-manifest.js +1 -1
  23. package/dist/dashboard-runtime/dashboard/.next/server/app/(dashboard)/memory/[store]/messages/page.js.nft.json +1 -1
  24. package/dist/dashboard-runtime/dashboard/.next/server/app/(dashboard)/memory/[store]/messages/page_client-reference-manifest.js +1 -1
  25. package/dist/dashboard-runtime/dashboard/.next/server/app/(dashboard)/memory/[store]/page.js.nft.json +1 -1
  26. package/dist/dashboard-runtime/dashboard/.next/server/app/(dashboard)/memory/[store]/page_client-reference-manifest.js +1 -1
  27. package/dist/dashboard-runtime/dashboard/.next/server/app/(dashboard)/memory/[store]/requests/page.js.nft.json +1 -1
  28. package/dist/dashboard-runtime/dashboard/.next/server/app/(dashboard)/memory/[store]/requests/page_client-reference-manifest.js +1 -1
  29. package/dist/dashboard-runtime/dashboard/.next/server/app/(dashboard)/memory/[store]/search/page.js.nft.json +1 -1
  30. package/dist/dashboard-runtime/dashboard/.next/server/app/(dashboard)/memory/[store]/search/page_client-reference-manifest.js +1 -1
  31. package/dist/dashboard-runtime/dashboard/.next/server/app/(dashboard)/memory/[store]/settings/page.js.nft.json +1 -1
  32. package/dist/dashboard-runtime/dashboard/.next/server/app/(dashboard)/memory/[store]/settings/page_client-reference-manifest.js +1 -1
  33. package/dist/dashboard-runtime/dashboard/.next/server/app/(dashboard)/memory/[store]/units/page.js.nft.json +1 -1
  34. package/dist/dashboard-runtime/dashboard/.next/server/app/(dashboard)/memory/[store]/units/page_client-reference-manifest.js +1 -1
  35. package/dist/dashboard-runtime/dashboard/.next/server/app/(dashboard)/memory/create/page_client-reference-manifest.js +1 -1
  36. package/dist/dashboard-runtime/dashboard/.next/server/app/(dashboard)/memory/page.js.nft.json +1 -1
  37. package/dist/dashboard-runtime/dashboard/.next/server/app/(dashboard)/memory/page_client-reference-manifest.js +1 -1
  38. package/dist/dashboard-runtime/dashboard/.next/server/app/(dashboard)/projects/page.js.nft.json +1 -1
  39. package/dist/dashboard-runtime/dashboard/.next/server/app/(dashboard)/projects/page_client-reference-manifest.js +1 -1
  40. package/dist/dashboard-runtime/dashboard/.next/server/app/_global-error.html +1 -1
  41. package/dist/dashboard-runtime/dashboard/.next/server/app/_global-error.rsc +1 -1
  42. package/dist/dashboard-runtime/dashboard/.next/server/app/_global-error.segments/__PAGE__.segment.rsc +1 -1
  43. package/dist/dashboard-runtime/dashboard/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
  44. package/dist/dashboard-runtime/dashboard/.next/server/app/_global-error.segments/_head.segment.rsc +1 -1
  45. package/dist/dashboard-runtime/dashboard/.next/server/app/_global-error.segments/_index.segment.rsc +1 -1
  46. package/dist/dashboard-runtime/dashboard/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
  47. package/dist/dashboard-runtime/dashboard/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
  48. package/dist/dashboard-runtime/dashboard/.next/server/app/_not-found.html +1 -1
  49. package/dist/dashboard-runtime/dashboard/.next/server/app/_not-found.rsc +2 -2
  50. package/dist/dashboard-runtime/dashboard/.next/server/app/_not-found.segments/_full.segment.rsc +2 -2
  51. package/dist/dashboard-runtime/dashboard/.next/server/app/_not-found.segments/_head.segment.rsc +1 -1
  52. package/dist/dashboard-runtime/dashboard/.next/server/app/_not-found.segments/_index.segment.rsc +2 -2
  53. package/dist/dashboard-runtime/dashboard/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +1 -1
  54. package/dist/dashboard-runtime/dashboard/.next/server/app/_not-found.segments/_not-found.segment.rsc +1 -1
  55. package/dist/dashboard-runtime/dashboard/.next/server/app/_not-found.segments/_tree.segment.rsc +2 -2
  56. package/dist/dashboard-runtime/dashboard/.next/server/app/api/v1/knowledge-bases/[name]/documents/upload/route.js.nft.json +1 -1
  57. package/dist/dashboard-runtime/dashboard/.next/server/app/index.html +1 -1
  58. package/dist/dashboard-runtime/dashboard/.next/server/app/index.rsc +2 -2
  59. package/dist/dashboard-runtime/dashboard/.next/server/app/index.segments/__PAGE__.segment.rsc +1 -1
  60. package/dist/dashboard-runtime/dashboard/.next/server/app/index.segments/_full.segment.rsc +2 -2
  61. package/dist/dashboard-runtime/dashboard/.next/server/app/index.segments/_head.segment.rsc +1 -1
  62. package/dist/dashboard-runtime/dashboard/.next/server/app/index.segments/_index.segment.rsc +2 -2
  63. package/dist/dashboard-runtime/dashboard/.next/server/app/index.segments/_tree.segment.rsc +2 -2
  64. package/dist/dashboard-runtime/dashboard/.next/server/app/onboarding/page_client-reference-manifest.js +1 -1
  65. package/dist/dashboard-runtime/dashboard/.next/server/app/onboarding.html +1 -1
  66. package/dist/dashboard-runtime/dashboard/.next/server/app/onboarding.rsc +3 -3
  67. package/dist/dashboard-runtime/dashboard/.next/server/app/onboarding.segments/_full.segment.rsc +3 -3
  68. package/dist/dashboard-runtime/dashboard/.next/server/app/onboarding.segments/_head.segment.rsc +1 -1
  69. package/dist/dashboard-runtime/dashboard/.next/server/app/onboarding.segments/_index.segment.rsc +2 -2
  70. package/dist/dashboard-runtime/dashboard/.next/server/app/onboarding.segments/_tree.segment.rsc +2 -2
  71. package/dist/dashboard-runtime/dashboard/.next/server/app/onboarding.segments/onboarding/__PAGE__.segment.rsc +2 -2
  72. package/dist/dashboard-runtime/dashboard/.next/server/app/onboarding.segments/onboarding.segment.rsc +1 -1
  73. package/dist/dashboard-runtime/dashboard/.next/server/app/page_client-reference-manifest.js +1 -1
  74. package/dist/dashboard-runtime/dashboard/.next/server/chunks/ssr/0b1r_app_(dashboard)_knowledge-base_[name]_documents_documents-client_tsx_0g2uxmt._.js +1 -1
  75. package/dist/dashboard-runtime/dashboard/.next/server/chunks/ssr/[root-of-the-server]__09_30hi._.js +3 -0
  76. package/dist/dashboard-runtime/dashboard/.next/server/chunks/ssr/[root-of-the-server]__0kaiok7._.js +3 -0
  77. package/dist/dashboard-runtime/dashboard/.next/server/chunks/ssr/[root-of-the-server]__0l937k4._.js +3 -0
  78. package/dist/dashboard-runtime/dashboard/.next/server/chunks/ssr/_0-dlo4s._.js +1 -1
  79. package/dist/dashboard-runtime/dashboard/.next/server/chunks/ssr/_01pp~xx._.js +3 -0
  80. package/dist/dashboard-runtime/dashboard/.next/server/chunks/ssr/{_0476v7r._.js → _08j6p-z._.js} +2 -2
  81. package/dist/dashboard-runtime/dashboard/.next/server/chunks/ssr/{_0ct0n55._.js → _09eom0_._.js} +2 -2
  82. package/dist/dashboard-runtime/dashboard/.next/server/chunks/ssr/{_0mzxmfh._.js → _0f-may2._.js} +2 -2
  83. package/dist/dashboard-runtime/dashboard/.next/server/chunks/ssr/_0fp_nnb._.js +3 -0
  84. package/dist/dashboard-runtime/dashboard/.next/server/chunks/ssr/_0l6jz-5._.js +3 -0
  85. package/dist/dashboard-runtime/dashboard/.next/server/chunks/ssr/{_0fg6iap._.js → _0ntv66s._.js} +2 -2
  86. package/dist/dashboard-runtime/dashboard/.next/server/chunks/ssr/_0sc0tpz._.js +3 -0
  87. package/dist/dashboard-runtime/dashboard/.next/server/chunks/ssr/_0tyxwr3._.js +3 -0
  88. package/dist/dashboard-runtime/dashboard/.next/server/chunks/ssr/_0u9md36._.js +1 -1
  89. package/dist/dashboard-runtime/dashboard/.next/server/chunks/ssr/dashboard_src_065m1.j._.js +1 -1
  90. package/dist/dashboard-runtime/dashboard/.next/server/chunks/ssr/dashboard_src_app_(dashboard)_knowledge-base_[name]_layout-client_tsx_05momc5._.js +1 -1
  91. package/dist/dashboard-runtime/dashboard/.next/server/middleware-build-manifest.js +3 -3
  92. package/dist/dashboard-runtime/dashboard/.next/server/pages/404.html +1 -1
  93. package/dist/dashboard-runtime/dashboard/.next/server/pages/500.html +1 -1
  94. package/dist/dashboard-runtime/dashboard/.next/server/server-reference-manifest.js +1 -1
  95. package/dist/dashboard-runtime/dashboard/.next/server/server-reference-manifest.json +1 -1
  96. package/dist/dashboard-runtime/dashboard/.next/static/chunks/0-xqk6wfme5fu.js +1 -0
  97. package/dist/dashboard-runtime/dashboard/.next/static/chunks/{0jeb9kxtd_jq..js → 00.fyg_t3xjc~.js} +1 -1
  98. package/dist/dashboard-runtime/dashboard/.next/static/chunks/01k-.wkndubwy.js +138 -0
  99. package/dist/dashboard-runtime/dashboard/.next/static/chunks/07r22x5uhjxjb.js +1 -0
  100. package/dist/dashboard-runtime/dashboard/.next/static/chunks/0cbu3udngtx-v.js +4 -0
  101. package/dist/dashboard-runtime/dashboard/.next/static/chunks/0h7d69g2fdgs6.js +138 -0
  102. package/dist/dashboard-runtime/dashboard/.next/static/chunks/0iv0g360z0ja5.js +1 -0
  103. package/dist/dashboard-runtime/dashboard/.next/static/chunks/0jf4ytf.izmzh.js +1 -0
  104. package/dist/dashboard-runtime/dashboard/.next/static/chunks/{17nx_vdk_qemm.js → 0m42mmyxmgn-i.js} +1 -1
  105. package/dist/dashboard-runtime/dashboard/.next/static/chunks/0odz1whf-ozqn.css +1 -0
  106. package/dist/dashboard-runtime/dashboard/.next/static/chunks/0ouxv_2qvf6.9.js +1 -0
  107. package/dist/dashboard-runtime/dashboard/.next/static/chunks/0rbkgj._oqsb3.js +1 -0
  108. package/dist/dashboard-runtime/dashboard/.next/static/chunks/0u6u.649ou67b.js +1 -0
  109. package/dist/dashboard-runtime/dashboard/.next/static/chunks/0vf7~eysvh9bq.js +1 -0
  110. package/dist/dashboard-runtime/dashboard/.next/static/chunks/{09qsmnfvk8dm3.js → 0wmaq.ay~r2sn.js} +1 -1
  111. package/dist/dashboard-runtime/dashboard/.next/static/chunks/{0i5aqiffpm9jr.js → 0xb0_9t-vmo6e.js} +1 -1
  112. package/dist/dashboard-runtime/dashboard/.next/static/chunks/0yp_2ea75d19v.js +1 -0
  113. package/dist/dashboard-runtime/dashboard/.next/static/chunks/{0s3mvx9_dr13c.js → 11cizim1aug~b.js} +1 -1
  114. package/dist/dashboard-runtime/dashboard/.next/static/chunks/13-2ei295hl0-.js +1 -0
  115. package/dist/dashboard-runtime/dashboard/src/app/(dashboard)/knowledge-base/[name]/qa/page.tsx +23 -1
  116. package/dist/dashboard-runtime/dashboard/src/components/documents/doc-upload-dialog.tsx +370 -91
  117. package/dist/dashboard-runtime/dashboard/src/components/knowledge-base/kb-card.tsx +8 -5
  118. package/dist/dashboard-runtime/dashboard/src/components/knowledge-base/kb-header.tsx +18 -10
  119. package/dist/dashboard-runtime/dashboard/src/components/memory/memory-header.tsx +8 -12
  120. package/dist/dashboard-runtime/dashboard/src/components/qa/qa-chat-panel.tsx +45 -4
  121. package/dist/dashboard-runtime/dashboard/src/components/qa/qa-message.tsx +636 -36
  122. package/dist/dashboard-runtime/dashboard/src/components/shared/confirm-dialog.tsx +8 -1
  123. package/dist/dashboard-runtime/dashboard/src/components/shared/refresh-button.tsx +102 -86
  124. package/dist/dashboard-runtime/dashboard/src/hooks/use-qa-chat.ts +261 -58
  125. package/dist/dashboard-runtime/dashboard/src/lib/chat-message-ops.ts +76 -0
  126. package/dist/dashboard-runtime/dashboard/src/lib/dev-flags.ts +24 -0
  127. package/dist/dashboard-runtime/dashboard/src/types/llm-chat.ts +19 -0
  128. package/package.json +1 -1
  129. package/dist/dashboard-runtime/dashboard/.next/server/chunks/ssr/[root-of-the-server]__0i0y9.r._.js +0 -3
  130. package/dist/dashboard-runtime/dashboard/.next/server/chunks/ssr/[root-of-the-server]__0r_6wpx._.js +0 -3
  131. package/dist/dashboard-runtime/dashboard/.next/server/chunks/ssr/[root-of-the-server]__0y8rrcu._.js +0 -3
  132. package/dist/dashboard-runtime/dashboard/.next/server/chunks/ssr/_02e0c00._.js +0 -3
  133. package/dist/dashboard-runtime/dashboard/.next/server/chunks/ssr/_0doczxg._.js +0 -3
  134. package/dist/dashboard-runtime/dashboard/.next/server/chunks/ssr/_0e9y.xc._.js +0 -3
  135. package/dist/dashboard-runtime/dashboard/.next/server/chunks/ssr/_0ti~om_._.js +0 -3
  136. package/dist/dashboard-runtime/dashboard/.next/server/chunks/ssr/_0~s~e-0._.js +0 -3
  137. package/dist/dashboard-runtime/dashboard/.next/static/chunks/055_bo6_3nt-s.js +0 -1
  138. package/dist/dashboard-runtime/dashboard/.next/static/chunks/07l4dodn5t6l4.js +0 -4
  139. package/dist/dashboard-runtime/dashboard/.next/static/chunks/0a5ass8u78k90.js +0 -1
  140. package/dist/dashboard-runtime/dashboard/.next/static/chunks/0j~63ld9lbb25.js +0 -1
  141. package/dist/dashboard-runtime/dashboard/.next/static/chunks/0l6znnjqw.4yw.js +0 -1
  142. package/dist/dashboard-runtime/dashboard/.next/static/chunks/0o-c2g8y5pw-c.js +0 -1
  143. package/dist/dashboard-runtime/dashboard/.next/static/chunks/0o0i2ke2jbdvq.js +0 -138
  144. package/dist/dashboard-runtime/dashboard/.next/static/chunks/0u2lo2lzizxy_.js +0 -1
  145. package/dist/dashboard-runtime/dashboard/.next/static/chunks/0v.hno.6q74nn.css +0 -1
  146. package/dist/dashboard-runtime/dashboard/.next/static/chunks/100.ax5hitxko.js +0 -1
  147. package/dist/dashboard-runtime/dashboard/.next/static/chunks/14ti2gx~1fiub.js +0 -1
  148. package/dist/dashboard-runtime/dashboard/.next/static/chunks/14vqmdm7sosj2.js +0 -1
  149. package/dist/dashboard-runtime/dashboard/.next/static/chunks/17h~onnkksf44.js +0 -138
  150. package/dist/dashboard-runtime/dashboard/.next/static/chunks/186jubvt5xyuj.js +0 -1
  151. /package/dist/dashboard-runtime/dashboard/.next/static/{wWmSSYUu4w4mIF1R_7UoM → BGnMIk4OPjFx_qvLFMTBc}/_buildManifest.js +0 -0
  152. /package/dist/dashboard-runtime/dashboard/.next/static/{wWmSSYUu4w4mIF1R_7UoM → BGnMIk4OPjFx_qvLFMTBc}/_clientMiddlewareManifest.js +0 -0
  153. /package/dist/dashboard-runtime/dashboard/.next/static/{wWmSSYUu4w4mIF1R_7UoM → BGnMIk4OPjFx_qvLFMTBc}/_ssgManifest.js +0 -0
@@ -1,7 +1,7 @@
1
1
 
2
2
  "use client";
3
3
 
4
- import { useState, useCallback } from "react";
4
+ import { useState, useCallback, type ReactNode } from "react";
5
5
  import {
6
6
  Dialog,
7
7
  DialogContent,
@@ -23,6 +23,10 @@ interface ConfirmDialogProps {
23
23
  variant?: "default" | "danger";
24
24
  onConfirm: () => void;
25
25
  confirmInput?: string;
26
+ /** Optional body slot rendered between the description and the footer.
27
+ * Used for inline form elements (e.g. the chat-message delete dialog's
28
+ * 「同时删除下方的 AI 回复」 checkbox). */
29
+ children?: ReactNode;
26
30
  }
27
31
 
28
32
  export function ConfirmDialog({
@@ -35,6 +39,7 @@ export function ConfirmDialog({
35
39
  variant = "default",
36
40
  onConfirm,
37
41
  confirmInput,
42
+ children,
38
43
  }: ConfirmDialogProps) {
39
44
  const [inputValue, setInputValue] = useState("");
40
45
 
@@ -81,6 +86,8 @@ export function ConfirmDialog({
81
86
  </div>
82
87
  )}
83
88
 
89
+ {children && <div className="py-1">{children}</div>}
90
+
84
91
  <DialogFooter>
85
92
  <Button variant="outline" onClick={() => handleOpenChange(false)}>
86
93
  {cancelText}
@@ -1,6 +1,6 @@
1
1
  "use client";
2
2
 
3
- import { useEffect, useRef, useState } from "react";
3
+ import { useEffect, useRef } from "react";
4
4
  import { RefreshCw } from "lucide-react";
5
5
  import { Button } from "@/components/ui/button";
6
6
  import { cn } from "@/lib/utils";
@@ -22,32 +22,29 @@ interface RefreshButtonProps {
22
22
  variant?: "outline" | "ghost";
23
23
  }
24
24
 
25
- /** One full turn takes this long. Matches the transition duration so the
26
- * tick cadence and the animation finish together, producing continuous
27
- * rotation without visible seams. */
28
- const TURN_MS = 600;
29
- /** Minimum on-screen spin time to avoid a flash on sub-100ms refetches. */
30
- const MIN_SPIN_MS = 300;
31
- /** Material `ease-standard`: smooth ease-in-out that looks natural on both
32
- * the first turn and the final settling turn. */
33
- const EASING = "cubic-bezier(0.4, 0, 0.2, 1)";
25
+ /** Duration of one full revolution. 800ms is the GitHub / Vercel / Slack
26
+ * band fast enough to feel responsive, slow enough to read as motion
27
+ * rather than a blur. */
28
+ const SPIN_PERIOD_MS = 800;
34
29
 
35
30
  /**
36
- * Refresh button with a strictly monotonic rotation model.
31
+ * Refresh button with **atomic chained rotations**.
37
32
  *
38
- * Every spin increments an integer `turns` counter; the outer wrapper
39
- * animates to `rotate(turns * 360deg)` via a single CSS transition. There is
40
- * no keyframe-to-static handoff, no angle sampling, no settle phase so the
41
- * icon physically cannot rewind or over-rotate.
33
+ * Industry pattern (GitHub, Linear, Raycast, ChatGPT regenerate): a refresh
34
+ * spin is not a single "infinite" CSS animation that gets stopped mid-flight
35
+ * it's a chain of complete 360° cycles. Each cycle is one Animation with
36
+ * `iterations: 1, easing: 'linear'`. On `finish`, if loading is still in
37
+ * flight we start another cycle; otherwise we stop. This way:
42
38
  *
43
- * - While `loading`, a timer advances `turns` once per {@link TURN_MS}, each
44
- * tick landing exactly on a full revolution and continuing seamlessly into
45
- * the next one thanks to the matching transition duration.
46
- * - When `loading` flips to false (after {@link MIN_SPIN_MS} has elapsed) the
47
- * timer stops; the last in-flight transition finishes naturally with an
48
- * ease-out tail.
49
- * - Hover tilt lives on a separate inner element, so its transform composes
50
- * with the outer rotation rather than fighting it.
39
+ * - The icon always lands cleanly at (== 360°), no partial-angle settle.
40
+ * - Speed is constant the entire time no "slow down to a stop" that looks
41
+ * like the spinner hit a wall, which is the most common cause of refresh
42
+ * animations feeling unnatural.
43
+ * - The transition between "loading" and "idle" never produces a snap-back,
44
+ * even if the request finishes inside a single millisecond.
45
+ *
46
+ * A minimum of one full revolution plays after a click, so sub-100ms refetches
47
+ * still register as "I did something" instead of flickering.
51
48
  *
52
49
  * Drive `loading` from TanStack Query's `isRefetching` (not `isFetching`) so
53
50
  * background auto-refetches on mount do not animate without user intent.
@@ -59,66 +56,80 @@ export function RefreshButton({
59
56
  className,
60
57
  size = "icon",
61
58
  label,
62
- variant = "outline",
59
+ variant = "ghost",
63
60
  }: RefreshButtonProps) {
64
- // Accumulated full revolutions. Monotonic: `turns` only ever grows, so the
65
- // wrapper never animates backwards.
66
- const [turns, setTurns] = useState(0);
67
- // Whether the timer is actively ticking. Tracked to dedupe starts and to
68
- // decide whether MIN_SPIN_MS still needs to be honoured on stop.
69
- const activeRef = useRef(false);
70
- const intervalRef = useRef<ReturnType<typeof setInterval> | null>(null);
71
- const stopTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
72
- const startedAtRef = useRef(0);
61
+ const iconRef = useRef<HTMLSpanElement | null>(null);
62
+ /** Currently-running cycle; null when idle. */
63
+ const animRef = useRef<Animation | null>(null);
64
+ /** Mirror of the `loading` prop accessible from inside the `finish`
65
+ * listener, which closes over a stale value if we read the prop directly. */
66
+ const loadingRef = useRef(loading);
73
67
 
74
- const clearAll = () => {
75
- if (intervalRef.current) {
76
- clearInterval(intervalRef.current);
77
- intervalRef.current = null;
78
- }
79
- if (stopTimerRef.current) {
80
- clearTimeout(stopTimerRef.current);
81
- stopTimerRef.current = null;
82
- }
83
- };
68
+ const startCycle = () => {
69
+ const el = iconRef.current;
70
+ if (!el) return;
71
+ if (animRef.current) return;
84
72
 
85
- const startSpinning = () => {
86
- if (activeRef.current) return;
87
- activeRef.current = true;
88
- startedAtRef.current = performance.now();
89
- // Advance immediately so the user sees motion on the first frame.
90
- setTurns((t) => t + 1);
91
- intervalRef.current = setInterval(() => {
92
- setTurns((t) => t + 1);
93
- }, TURN_MS);
94
- };
73
+ const anim = el.animate(
74
+ [{ transform: "rotate(0deg)" }, { transform: "rotate(360deg)" }],
75
+ {
76
+ duration: SPIN_PERIOD_MS,
77
+ easing: "linear",
78
+ iterations: 1,
79
+ fill: "none",
80
+ },
81
+ );
82
+ animRef.current = anim;
83
+
84
+ anim.addEventListener("finish", () => {
85
+ animRef.current = null;
86
+ // Chain the next cycle if the request is still in flight. The chain
87
+ // is implicit, not explicit: each cycle only knows about itself.
88
+ if (loadingRef.current) {
89
+ startCycle();
90
+ }
91
+ });
95
92
 
96
- const stopSpinning = () => {
97
- clearAll();
98
- activeRef.current = false;
99
- // No angle reset: the current turn finishes its in-flight transition and
100
- // ends cleanly aligned with 360deg * turns.
93
+ // `cancel` shouldn't normally fire — we never call .cancel() ourselves
94
+ // but defensively clear the ref so a fresh click can start a new cycle.
95
+ anim.addEventListener("cancel", () => {
96
+ animRef.current = null;
97
+ });
101
98
  };
102
99
 
100
+ // Keep the ref in sync so the `finish` listener sees the latest value.
101
+ useEffect(() => {
102
+ loadingRef.current = loading;
103
+ }, [loading]);
104
+
105
+ // Start a cycle when loading begins. We don't have to do anything when
106
+ // loading ends — the in-flight cycle finishes on its own and the chain
107
+ // doesn't continue because `loadingRef.current` will be false.
103
108
  useEffect(() => {
104
109
  if (loading) {
105
- startSpinning();
106
- return;
110
+ startCycle();
107
111
  }
108
- if (!activeRef.current) return;
109
- const elapsed = performance.now() - startedAtRef.current;
110
- const remaining = MIN_SPIN_MS - elapsed;
111
- if (remaining <= 0) {
112
- stopSpinning();
113
- return;
114
- }
115
- stopTimerRef.current = setTimeout(stopSpinning, remaining);
112
+ // startCycle is a stable closure over refs; including it would force a
113
+ // re-run on every render with no behaviour change.
114
+ // eslint-disable-next-line react-hooks/exhaustive-deps
116
115
  }, [loading]);
117
116
 
118
- useEffect(() => () => clearAll(), []);
117
+ useEffect(
118
+ () => () => {
119
+ animRef.current?.cancel();
120
+ animRef.current = null;
121
+ },
122
+ [],
123
+ );
119
124
 
120
125
  const handleClick = () => {
121
126
  if (loading) return;
127
+ // Start a cycle eagerly so the user gets visual confirmation even when
128
+ // the request resolves in <100ms (cache hit). Cycles are atomic: once
129
+ // started, they always play to completion regardless of `loading` —
130
+ // that's what makes the "minimum one full revolution" guarantee
131
+ // structural rather than a separate timer.
132
+ startCycle();
122
133
  onClick();
123
134
  };
124
135
 
@@ -127,33 +138,38 @@ export function RefreshButton({
127
138
 
128
139
  return (
129
140
  <Button
141
+ type="button"
130
142
  variant={variant}
131
143
  size={size}
132
144
  onClick={handleClick}
133
- disabled={loading}
145
+ // We don't disable while loading: the icon is its own affordance, and
146
+ // disabling produces a "dead" hit state that feels worse than letting
147
+ // a redundant click no-op silently.
134
148
  title={title}
135
149
  aria-label={label ?? title}
136
- className={cn("group transition-colors", className)}
150
+ aria-busy={loading || undefined}
151
+ className={cn(
152
+ "group text-muted-foreground transition-colors duration-150",
153
+ "hover:text-foreground active:scale-[0.96]",
154
+ // Soft hover wash for the default ghost variant; outlined stays as-is.
155
+ variant === "ghost" && "hover:bg-[hsl(var(--accent))]",
156
+ className,
157
+ )}
137
158
  >
138
- {/* Outer wrapper owns the monotonic rotation. Using inline style keeps
139
- the transform in sync with `turns` regardless of Tailwind arbitrary
140
- value caching. */}
159
+ {/*
160
+ The icon lives inside its own span which is what we animate. We
161
+ intentionally do NOT add a hover-tilt to this span — a hover transform
162
+ and the rotate animation share the same `transform` property, and
163
+ switching from "tilted" to "rotating-from-0" produces a visible snap
164
+ back to 0deg the moment the user clicks. Hover state is conveyed via
165
+ text + background colour shifts on the parent button instead.
166
+ */}
141
167
  <span
168
+ ref={iconRef}
142
169
  aria-hidden
143
170
  className={cn("inline-flex transform-gpu", showLabel && "mr-1.5")}
144
- style={{
145
- transform: `rotate(${turns * 360}deg)`,
146
- transition: `transform ${TURN_MS}ms ${EASING}`,
147
- }}
148
171
  >
149
- {/* Inner icon owns the idle hover tilt, completely decoupled from
150
- the outer rotation so the two transforms compose without fighting. */}
151
- <RefreshCw
152
- className={cn(
153
- iconSize,
154
- "transition-transform duration-200 group-hover:-rotate-[35deg]",
155
- )}
156
- />
172
+ <RefreshCw className={iconSize} />
157
173
  </span>
158
174
  {showLabel && label}
159
175
  </Button>