claudeup 6.2.0 → 6.3.0
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.
- package/README.md +22 -0
- package/bin/claudeup.js +6 -1
- package/package.json +6 -6
- package/src/__tests__/alias-adopt.test.ts +331 -329
- package/src/__tests__/alias-parser.test.ts +384 -376
- package/src/__tests__/alias-shell-writer.test.ts +638 -625
- package/src/__tests__/alias-store.test.ts +143 -143
- package/src/__tests__/catalog-cache-store.test.ts +265 -265
- package/src/__tests__/catalog-notice.test.ts +123 -123
- package/src/__tests__/cli-apply-seams.test.ts +12 -8
- package/src/__tests__/cli-router.test.ts +132 -3
- package/src/__tests__/cli-update-view.test.ts +27 -8
- package/src/__tests__/community-staleness.test.ts +0 -2
- package/src/__tests__/component-badge.test.ts +4 -4
- package/src/__tests__/content-drift.test.ts +5 -1
- package/src/__tests__/conventions-integration.test.ts +774 -689
- package/src/__tests__/conventions-manager.test.ts +1216 -1152
- package/src/__tests__/doctor-bins.test.ts +14 -6
- package/src/__tests__/doctor.test.ts +438 -425
- package/src/__tests__/dual-write-prevention.test.ts +277 -282
- package/src/__tests__/enabled-not-installed.test.ts +71 -63
- package/src/__tests__/file-locking.test.ts +196 -196
- package/src/__tests__/gap-fill-versions.test.ts +318 -311
- package/src/__tests__/github-budget.test.ts +170 -164
- package/src/__tests__/gitignore-detector.test.ts +160 -152
- package/src/__tests__/gitignore-fixer.test.ts +285 -247
- package/src/__tests__/gitignore-prerun.test.ts +57 -57
- package/src/__tests__/gitignore-resolver.test.ts +173 -146
- package/src/__tests__/gitignore-service.test.ts +121 -119
- package/src/__tests__/go-module.test.ts +43 -0
- package/src/__tests__/install-command.test.ts +3 -3
- package/src/__tests__/install-plan.test.ts +13 -7
- package/src/__tests__/manifest.test.ts +16 -5
- package/src/__tests__/markdown-renderer.test.ts +0 -1
- package/src/__tests__/marketplace-badge.test.ts +111 -90
- package/src/__tests__/marketplace-version.test.ts +64 -61
- package/src/__tests__/plugin-manager-fallback.test.ts +249 -239
- package/src/__tests__/plugin-requires.test.ts +55 -1
- package/src/__tests__/plugin-setup.test.ts +111 -9
- package/src/__tests__/profile-command.test.ts +1 -7
- package/src/__tests__/profile-materializer.test.ts +31 -8
- package/src/__tests__/profile-sync.test.ts +23 -5
- package/src/__tests__/registry-version-resolution.test.ts +61 -47
- package/src/__tests__/resolve-executable.test.ts +1 -1
- package/src/__tests__/resolver.test.ts +38 -13
- package/src/__tests__/scope-action.test.ts +52 -50
- package/src/__tests__/scope-squares.test.tsx +4 -5
- package/src/__tests__/skill-install-files.test.ts +111 -103
- package/src/__tests__/symlink-manager.test.ts +27 -10
- package/src/__tests__/theme-env.test.ts +281 -0
- package/src/__tests__/theme-resolve.test.ts +380 -0
- package/src/__tests__/update-apply.test.ts +1 -3
- package/src/__tests__/useGitignoreModal.test.ts +69 -61
- package/src/__tests__/version-snapshot.test.ts +183 -165
- package/src/cli/ansi.ts +23 -15
- package/src/cli/doctor.ts +10 -9
- package/src/cli/install.ts +3 -3
- package/src/cli/profile.ts +5 -4
- package/src/cli/prompt.ts +0 -1
- package/src/cli/router.ts +69 -2
- package/src/cli/update-view.ts +11 -6
- package/src/cli/update.ts +9 -4
- package/src/cli/upgrade.ts +6 -4
- package/src/data/alias-flags.ts +260 -260
- package/src/data/gitignore-defaults.ts +20 -20
- package/src/data/gitignore-reasons.ts +86 -92
- package/src/data/gitignore-templates.ts +14 -14
- package/src/data/predefined-profiles.ts +1 -2
- package/src/data/skill-repos.ts +43 -35
- package/src/main.tsx +29 -6
- package/src/opentui.d.ts +1 -3
- package/src/prerunner/index.ts +7 -8
- package/src/services/alias-settings.ts +20 -23
- package/src/services/alias-shell-writer.ts +800 -803
- package/src/services/alias-store.ts +77 -77
- package/src/services/claude-settings.ts +27 -18
- package/src/services/content-drift.ts +14 -4
- package/src/services/conventions-manager.ts +692 -617
- package/src/services/doctor.ts +464 -403
- package/src/services/gitignore-detector.ts +113 -103
- package/src/services/gitignore-fixer.ts +204 -189
- package/src/services/gitignore-prerun.ts +26 -26
- package/src/services/gitignore-resolver.ts +162 -144
- package/src/services/gitignore-service.ts +120 -117
- package/src/services/install-plan.ts +4 -4
- package/src/services/local-marketplace.ts +7 -5
- package/src/services/manifest.ts +1 -1
- package/src/services/marketplace-catalog-git.ts +5 -2
- package/src/services/marketplace-fetcher.ts +241 -235
- package/src/services/plugin-manager.ts +17 -20
- package/src/services/plugin-mcp-config.ts +2 -2
- package/src/services/plugin-requires.ts +16 -8
- package/src/services/plugin-setup.ts +32 -15
- package/src/services/plugin-version-check.ts +4 -4
- package/src/services/profile-materializer.ts +9 -5
- package/src/services/profile-sync.ts +12 -4
- package/src/services/profiles.ts +3 -3
- package/src/services/resolver.ts +10 -6
- package/src/services/settings-manager.ts +82 -46
- package/src/services/skills-manager.ts +11 -9
- package/src/services/symlink-manager.ts +2 -5
- package/src/services/toolchain.ts +3 -3
- package/src/services/update-cache.ts +1 -1
- package/src/services/update-engine.ts +4 -4
- package/src/services/update-plan.ts +9 -11
- package/src/services/version-snapshot.ts +88 -88
- package/src/types/gitignore.ts +28 -28
- package/src/ui/App.tsx +36 -36
- package/src/ui/adapters/settingsAdapter.ts +2 -2
- package/src/ui/components/CategoryHeader.tsx +1 -1
- package/src/ui/components/FlagDetailEditor.tsx +0 -0
- package/src/ui/components/ScopeIndicator.tsx +3 -1
- package/src/ui/components/ScrollableList.tsx +2 -1
- package/src/ui/components/StyledText.tsx +1 -1
- package/src/ui/components/layout/FooterHints.tsx +5 -3
- package/src/ui/components/layout/Panel.tsx +2 -2
- package/src/ui/components/layout/ScreenLayout.tsx +5 -4
- package/src/ui/components/modals/InputModal.tsx +7 -1
- package/src/ui/components/modals/ModalContainer.tsx +3 -3
- package/src/ui/components/modals/SelectModal.tsx +1 -1
- package/src/ui/components/primitives/DetailSection.tsx +1 -1
- package/src/ui/components/primitives/KeyValueLine.tsx +1 -1
- package/src/ui/components/primitives/ScopeSquares.tsx +2 -2
- package/src/ui/hooks/index.ts +0 -1
- package/src/ui/hooks/useGitignoreModal.ts +1 -5
- package/src/ui/hooks/useMismatchModal.ts +4 -10
- package/src/ui/renderers/cliToolRenderers.tsx +2 -2
- package/src/ui/renderers/gitignoreRenderers.tsx +2 -2
- package/src/ui/renderers/mcpRenderers.tsx +1 -1
- package/src/ui/renderers/pluginRenderers.tsx +3 -9
- package/src/ui/renderers/profileRenderers.tsx +9 -8
- package/src/ui/renderers/settingsRenderers.tsx +4 -4
- package/src/ui/renderers/skillRenderers.tsx +14 -14
- package/src/ui/screens/AliasScreen.tsx +31 -34
- package/src/ui/screens/CliToolsScreen.tsx +14 -13
- package/src/ui/screens/EnvVarsScreen.tsx +8 -8
- package/src/ui/screens/GitignoreScreen.tsx +15 -14
- package/src/ui/screens/McpRegistryScreen.tsx +10 -7
- package/src/ui/screens/McpScreen.tsx +10 -10
- package/src/ui/screens/PluginsScreen.tsx +17 -21
- package/src/ui/screens/ProfilesScreen.tsx +25 -24
- package/src/ui/screens/SkillsScreen.tsx +16 -14
- package/src/ui/state/AppContext.tsx +4 -8
- package/src/ui/state/DimensionsContext.tsx +3 -2
- package/src/ui/theme-mode.ts +20 -38
- package/src/ui/theme-resolve.ts +148 -0
- package/src/utils/clipboard.ts +5 -5
- package/src/utils/command-utils.ts +2 -2
- package/src/utils/config-dir.ts +1 -3
- package/src/utils/file-locking.ts +88 -86
- package/src/utils/go-module.ts +36 -0
- package/src/ui/hooks/useAsyncData.ts +0 -127
- package/src/ui/state/AnimationContext.tsx +0 -76
|
@@ -5,10 +5,10 @@
|
|
|
5
5
|
* Uses lock files for cross-platform compatibility
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
|
-
import {
|
|
9
|
-
import {
|
|
10
|
-
import {
|
|
11
|
-
import
|
|
8
|
+
import { existsSync } from "node:fs";
|
|
9
|
+
import { mkdir, open } from "node:fs/promises";
|
|
10
|
+
import type { FileHandle } from "node:fs/promises";
|
|
11
|
+
import { dirname } from "node:path";
|
|
12
12
|
|
|
13
13
|
/**
|
|
14
14
|
* Maximum time to wait for a lock (milliseconds)
|
|
@@ -30,62 +30,64 @@ const LOCK_RETRY_INTERVAL_MS = 100;
|
|
|
30
30
|
* @throws Error if lock cannot be acquired within timeout
|
|
31
31
|
*/
|
|
32
32
|
export async function withFileLock<T>(
|
|
33
|
-
|
|
34
|
-
|
|
33
|
+
filePath: string,
|
|
34
|
+
operation: () => Promise<T>,
|
|
35
35
|
): Promise<T> {
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
36
|
+
const lockPath = `${filePath}.lock`;
|
|
37
|
+
const lockDir = dirname(lockPath);
|
|
38
|
+
|
|
39
|
+
// Ensure lock directory exists
|
|
40
|
+
if (!existsSync(lockDir)) {
|
|
41
|
+
await mkdir(lockDir, { recursive: true });
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const startTime = Date.now();
|
|
45
|
+
let fileHandle: FileHandle | null = null;
|
|
46
|
+
|
|
47
|
+
// Try to acquire lock with retries
|
|
48
|
+
while (Date.now() - startTime < LOCK_TIMEOUT_MS) {
|
|
49
|
+
try {
|
|
50
|
+
// Try to create lock file exclusively (fails if exists)
|
|
51
|
+
fileHandle = await open(lockPath, "wx");
|
|
52
|
+
|
|
53
|
+
// Write PID to lock file for debugging
|
|
54
|
+
await fileHandle.write(`${process.pid}\n${new Date().toISOString()}\n`);
|
|
55
|
+
|
|
56
|
+
break;
|
|
57
|
+
} catch (error: unknown) {
|
|
58
|
+
// Lock file exists, retry after interval
|
|
59
|
+
if ((error as NodeJS.ErrnoException).code === "EEXIST") {
|
|
60
|
+
await new Promise((resolve) =>
|
|
61
|
+
setTimeout(resolve, LOCK_RETRY_INTERVAL_MS),
|
|
62
|
+
);
|
|
63
|
+
continue;
|
|
64
|
+
}
|
|
65
|
+
throw error;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
if (fileHandle === null) {
|
|
70
|
+
throw new Error(
|
|
71
|
+
`Failed to acquire lock on ${filePath} after ${LOCK_TIMEOUT_MS}ms. ` +
|
|
72
|
+
`Another process may be holding the lock. Lock file: ${lockPath}`,
|
|
73
|
+
);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
try {
|
|
77
|
+
// Execute operation with lock held
|
|
78
|
+
return await operation();
|
|
79
|
+
} finally {
|
|
80
|
+
// Release lock
|
|
81
|
+
try {
|
|
82
|
+
await fileHandle.close();
|
|
83
|
+
// Delete lock file
|
|
84
|
+
const fs = await import("node:fs/promises");
|
|
85
|
+
await fs.unlink(lockPath);
|
|
86
|
+
} catch (error) {
|
|
87
|
+
// Log error but don't throw - operation succeeded
|
|
88
|
+
console.error(`Warning: Failed to release lock ${lockPath}:`, error);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
89
91
|
}
|
|
90
92
|
|
|
91
93
|
/**
|
|
@@ -95,8 +97,8 @@ export async function withFileLock<T>(
|
|
|
95
97
|
* @returns True if lock file exists
|
|
96
98
|
*/
|
|
97
99
|
export function isFileLocked(filePath: string): boolean {
|
|
98
|
-
|
|
99
|
-
|
|
100
|
+
const lockPath = `${filePath}.lock`;
|
|
101
|
+
return existsSync(lockPath);
|
|
100
102
|
}
|
|
101
103
|
|
|
102
104
|
/**
|
|
@@ -106,16 +108,16 @@ export function isFileLocked(filePath: string): boolean {
|
|
|
106
108
|
* @param filePath - Path to file whose lock to remove
|
|
107
109
|
*/
|
|
108
110
|
export async function forceRemoveLock(filePath: string): Promise<void> {
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
111
|
+
const lockPath = `${filePath}.lock`;
|
|
112
|
+
try {
|
|
113
|
+
const fs = await import("node:fs/promises");
|
|
114
|
+
await fs.unlink(lockPath);
|
|
115
|
+
} catch (error: unknown) {
|
|
116
|
+
// Ignore ENOENT (lock doesn't exist)
|
|
117
|
+
if ((error as NodeJS.ErrnoException).code !== "ENOENT") {
|
|
118
|
+
throw error;
|
|
119
|
+
}
|
|
120
|
+
}
|
|
119
121
|
}
|
|
120
122
|
|
|
121
123
|
/**
|
|
@@ -125,20 +127,20 @@ export async function forceRemoveLock(filePath: string): Promise<void> {
|
|
|
125
127
|
* @returns Lock info or null if not locked
|
|
126
128
|
*/
|
|
127
129
|
export async function getLockInfo(
|
|
128
|
-
|
|
130
|
+
filePath: string,
|
|
129
131
|
): Promise<{ pid: string; timestamp: string } | null> {
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
132
|
+
const lockPath = `${filePath}.lock`;
|
|
133
|
+
|
|
134
|
+
if (!existsSync(lockPath)) {
|
|
135
|
+
return null;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
try {
|
|
139
|
+
const fs = await import("node:fs/promises");
|
|
140
|
+
const content = await fs.readFile(lockPath, "utf-8");
|
|
141
|
+
const [pid, timestamp] = content.trim().split("\n");
|
|
142
|
+
return { pid, timestamp };
|
|
143
|
+
} catch (error) {
|
|
144
|
+
return null;
|
|
145
|
+
}
|
|
144
146
|
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Go module path → installed binary name.
|
|
3
|
+
*
|
|
4
|
+
* `go install` names the binary after the last path element of the module
|
|
5
|
+
* path, EXCEPT when that element is a major-version suffix: the module
|
|
6
|
+
* `github.com/MadAppGang/tmux-mcp/v2` installs `tmux-mcp`, not `v2`. Taking a
|
|
7
|
+
* plain basename therefore points every later check at a binary that never
|
|
8
|
+
* exists — presence probes fail, so the module is reinstalled on every run,
|
|
9
|
+
* doctor reports it missing, and the pin comparison never runs at all.
|
|
10
|
+
*
|
|
11
|
+
* Only the Go rule lives here. npm and cargo share the `name@version` syntax
|
|
12
|
+
* but not the suffix convention — an npm scoped package can legitimately be
|
|
13
|
+
* called `@scope/v2` — so this must not be folded into the generic spec parser.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Trailing major-version suffix per the Go modules spec: `/vN` with N >= 2.
|
|
18
|
+
* Anchored to the END of the path with nothing after `vN`, so a segment that
|
|
19
|
+
* merely starts with "v2" (`github.com/x/v2tool`) is a real name, not a suffix.
|
|
20
|
+
* v0 and v1 are never written as a path suffix. The alternation, not a plain
|
|
21
|
+
* `[2-9][0-9]*`, because that shorthand rejects v10–v19 — a first digit of 1
|
|
22
|
+
* is only excluded when it is the ONLY digit.
|
|
23
|
+
*/
|
|
24
|
+
const MAJOR_VERSION_SUFFIX = /\/v(?:[2-9]|[1-9][0-9]+)$/;
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* The binary `go install <modulePath>` produces.
|
|
28
|
+
*
|
|
29
|
+
* Takes the MODULE PATH — the part before any `@version` — so callers that
|
|
30
|
+
* hold a full `path@version` spec split it first. An empty or single-segment
|
|
31
|
+
* input is returned unchanged.
|
|
32
|
+
*/
|
|
33
|
+
export function goBinaryName(modulePath: string): string {
|
|
34
|
+
const withoutSuffix = modulePath.replace(MAJOR_VERSION_SUFFIX, "");
|
|
35
|
+
return withoutSuffix.split("/").pop() || withoutSuffix;
|
|
36
|
+
}
|
|
@@ -1,127 +0,0 @@
|
|
|
1
|
-
import { useState, useEffect, useCallback } from "react";
|
|
2
|
-
import type { AsyncData } from "../state/types.js";
|
|
3
|
-
|
|
4
|
-
interface UseAsyncDataOptions<T> {
|
|
5
|
-
/** Function that fetches data */
|
|
6
|
-
fetcher: () => Promise<T>;
|
|
7
|
-
/** Dependencies that trigger refetch when changed */
|
|
8
|
-
deps?: unknown[];
|
|
9
|
-
/** Whether to fetch immediately on mount */
|
|
10
|
-
immediate?: boolean;
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
interface UseAsyncDataResult<T> {
|
|
14
|
-
/** Current async data state */
|
|
15
|
-
data: AsyncData<T>;
|
|
16
|
-
/** Whether data is currently loading */
|
|
17
|
-
isLoading: boolean;
|
|
18
|
-
/** Whether data has been successfully loaded */
|
|
19
|
-
isSuccess: boolean;
|
|
20
|
-
/** Whether there was an error loading data */
|
|
21
|
-
isError: boolean;
|
|
22
|
-
/** Error if any */
|
|
23
|
-
error: Error | null;
|
|
24
|
-
/** Trigger a manual refetch */
|
|
25
|
-
refetch: () => Promise<void>;
|
|
26
|
-
/** Raw data if available */
|
|
27
|
-
value: T | null;
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
/**
|
|
31
|
-
* Hook for fetching async data with loading/error states
|
|
32
|
-
*/
|
|
33
|
-
export function useAsyncData<T>({
|
|
34
|
-
fetcher,
|
|
35
|
-
deps = [],
|
|
36
|
-
immediate = true,
|
|
37
|
-
}: UseAsyncDataOptions<T>): UseAsyncDataResult<T> {
|
|
38
|
-
const [data, setData] = useState<AsyncData<T>>({ status: "idle" });
|
|
39
|
-
|
|
40
|
-
const refetch = useCallback(async () => {
|
|
41
|
-
setData({ status: "loading" });
|
|
42
|
-
try {
|
|
43
|
-
const result = await fetcher();
|
|
44
|
-
setData({ status: "success", data: result });
|
|
45
|
-
} catch (err) {
|
|
46
|
-
setData({
|
|
47
|
-
status: "error",
|
|
48
|
-
error: err instanceof Error ? err : new Error(String(err)),
|
|
49
|
-
});
|
|
50
|
-
}
|
|
51
|
-
}, [fetcher]);
|
|
52
|
-
|
|
53
|
-
useEffect(() => {
|
|
54
|
-
if (immediate) {
|
|
55
|
-
refetch();
|
|
56
|
-
}
|
|
57
|
-
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
58
|
-
}, deps);
|
|
59
|
-
|
|
60
|
-
return {
|
|
61
|
-
data,
|
|
62
|
-
isLoading: data.status === "loading",
|
|
63
|
-
isSuccess: data.status === "success",
|
|
64
|
-
isError: data.status === "error",
|
|
65
|
-
error: data.status === "error" ? data.error : null,
|
|
66
|
-
refetch,
|
|
67
|
-
value: data.status === "success" ? data.data : null,
|
|
68
|
-
};
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
/**
|
|
72
|
-
* Hook for fetching async data with debounced query
|
|
73
|
-
*/
|
|
74
|
-
export function useDebouncedAsyncData<T>({
|
|
75
|
-
fetcher,
|
|
76
|
-
query,
|
|
77
|
-
debounceMs = 300,
|
|
78
|
-
minQueryLength = 1,
|
|
79
|
-
}: {
|
|
80
|
-
fetcher: (query: string) => Promise<T>;
|
|
81
|
-
query: string;
|
|
82
|
-
debounceMs?: number;
|
|
83
|
-
minQueryLength?: number;
|
|
84
|
-
}): UseAsyncDataResult<T> {
|
|
85
|
-
const [data, setData] = useState<AsyncData<T>>({ status: "idle" });
|
|
86
|
-
|
|
87
|
-
const refetch = useCallback(async () => {
|
|
88
|
-
if (query.length < minQueryLength) {
|
|
89
|
-
setData({ status: "idle" });
|
|
90
|
-
return;
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
setData({ status: "loading" });
|
|
94
|
-
try {
|
|
95
|
-
const result = await fetcher(query);
|
|
96
|
-
setData({ status: "success", data: result });
|
|
97
|
-
} catch (err) {
|
|
98
|
-
setData({
|
|
99
|
-
status: "error",
|
|
100
|
-
error: err instanceof Error ? err : new Error(String(err)),
|
|
101
|
-
});
|
|
102
|
-
}
|
|
103
|
-
}, [fetcher, query, minQueryLength]);
|
|
104
|
-
|
|
105
|
-
useEffect(() => {
|
|
106
|
-
if (query.length < minQueryLength) {
|
|
107
|
-
setData({ status: "idle" });
|
|
108
|
-
return;
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
const timeoutId = setTimeout(() => {
|
|
112
|
-
refetch();
|
|
113
|
-
}, debounceMs);
|
|
114
|
-
|
|
115
|
-
return () => clearTimeout(timeoutId);
|
|
116
|
-
}, [query, debounceMs, minQueryLength, refetch]);
|
|
117
|
-
|
|
118
|
-
return {
|
|
119
|
-
data,
|
|
120
|
-
isLoading: data.status === "loading",
|
|
121
|
-
isSuccess: data.status === "success",
|
|
122
|
-
isError: data.status === "error",
|
|
123
|
-
error: data.status === "error" ? data.error : null,
|
|
124
|
-
refetch,
|
|
125
|
-
value: data.status === "success" ? data.data : null,
|
|
126
|
-
};
|
|
127
|
-
}
|
|
@@ -1,76 +0,0 @@
|
|
|
1
|
-
import React, {
|
|
2
|
-
createContext,
|
|
3
|
-
useContext,
|
|
4
|
-
useState,
|
|
5
|
-
type ReactNode,
|
|
6
|
-
} from "react";
|
|
7
|
-
|
|
8
|
-
// ============================================================================
|
|
9
|
-
// Context Type
|
|
10
|
-
// ============================================================================
|
|
11
|
-
|
|
12
|
-
interface Timeline {
|
|
13
|
-
id: string;
|
|
14
|
-
// Timeline instance from useTimeline hook
|
|
15
|
-
instance: any;
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
interface AnimationContextValue {
|
|
19
|
-
timelines: Map<string, any>;
|
|
20
|
-
registerTimeline: (id: string, timeline: any) => void;
|
|
21
|
-
unregisterTimeline: (id: string) => void;
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
// ============================================================================
|
|
25
|
-
// Context
|
|
26
|
-
// ============================================================================
|
|
27
|
-
|
|
28
|
-
const AnimationContext = createContext<AnimationContextValue | null>(null);
|
|
29
|
-
|
|
30
|
-
// ============================================================================
|
|
31
|
-
// Provider
|
|
32
|
-
// ============================================================================
|
|
33
|
-
|
|
34
|
-
interface AnimationProviderProps {
|
|
35
|
-
children: ReactNode;
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
export function AnimationProvider({ children }: AnimationProviderProps) {
|
|
39
|
-
const [timelines, setTimelines] = useState<Map<string, any>>(new Map());
|
|
40
|
-
|
|
41
|
-
const registerTimeline = (id: string, timeline: any) => {
|
|
42
|
-
setTimelines((prev) => {
|
|
43
|
-
const next = new Map(prev);
|
|
44
|
-
next.set(id, timeline);
|
|
45
|
-
return next;
|
|
46
|
-
});
|
|
47
|
-
};
|
|
48
|
-
|
|
49
|
-
const unregisterTimeline = (id: string) => {
|
|
50
|
-
setTimelines((prev) => {
|
|
51
|
-
const next = new Map(prev);
|
|
52
|
-
next.delete(id);
|
|
53
|
-
return next;
|
|
54
|
-
});
|
|
55
|
-
};
|
|
56
|
-
|
|
57
|
-
return (
|
|
58
|
-
<AnimationContext.Provider
|
|
59
|
-
value={{ timelines, registerTimeline, unregisterTimeline }}
|
|
60
|
-
>
|
|
61
|
-
{children}
|
|
62
|
-
</AnimationContext.Provider>
|
|
63
|
-
);
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
// ============================================================================
|
|
67
|
-
// Hook: useAnimation
|
|
68
|
-
// ============================================================================
|
|
69
|
-
|
|
70
|
-
export function useAnimation(): AnimationContextValue {
|
|
71
|
-
const context = useContext(AnimationContext);
|
|
72
|
-
if (!context) {
|
|
73
|
-
throw new Error("useAnimation must be used within an AnimationProvider");
|
|
74
|
-
}
|
|
75
|
-
return context;
|
|
76
|
-
}
|