@dennisrongo/skills 0.1.2 → 0.1.3

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.
@@ -0,0 +1,194 @@
1
+ # `src/hooks/useTauriCommand.ts`
2
+
3
+ Generic hook that wraps `invoke()` with `data` / `isLoading` / `error` / `execute`. Every per-domain hook (`useSettings`, `useRecording`, …) composes this. **Components never call `invoke()` directly.**
4
+
5
+ ```ts
6
+ import { invoke } from "@tauri-apps/api/core";
7
+ import { useCallback, useEffect, useState } from "react";
8
+ import { isTauriReady } from "../tauriReady";
9
+
10
+ export interface UseCommandOptions<T> {
11
+ command: string;
12
+ args?: Record<string, unknown>;
13
+ onSuccess?: (data: T) => void;
14
+ onError?: (error: string) => void;
15
+ }
16
+
17
+ export interface CommandState<T> {
18
+ data: T | null;
19
+ isLoading: boolean;
20
+ error: string | null;
21
+ execute: (args?: Record<string, unknown>) => Promise<T>;
22
+ }
23
+
24
+ export function useTauriCommand<T = unknown>(
25
+ options: UseCommandOptions<T>,
26
+ ): CommandState<T> {
27
+ const { command, args: defaultArgs, onSuccess, onError } = options;
28
+ const [data, setData] = useState<T | null>(null);
29
+ const [isLoading, setIsLoading] = useState(false);
30
+ const [error, setError] = useState<string | null>(null);
31
+
32
+ const execute = useCallback(
33
+ async (args?: Record<string, unknown>) => {
34
+ setIsLoading(true);
35
+ setError(null);
36
+ try {
37
+ const result = await invoke<T>(command, args || defaultArgs);
38
+ setData(result);
39
+ onSuccess?.(result);
40
+ return result;
41
+ } catch (err) {
42
+ const errorMsg = String(err);
43
+ setError(errorMsg);
44
+ onError?.(errorMsg);
45
+ throw err;
46
+ } finally {
47
+ setIsLoading(false);
48
+ }
49
+ },
50
+ [command, defaultArgs, onSuccess, onError],
51
+ );
52
+
53
+ return { data, isLoading, error, execute };
54
+ }
55
+
56
+ export interface UseLoadOptions<T> extends UseCommandOptions<T> {
57
+ loadOnMount?: boolean;
58
+ defaultValue?: T;
59
+ }
60
+
61
+ export interface LoadState<T> {
62
+ data: T;
63
+ isLoading: boolean;
64
+ error: string | null;
65
+ execute: (args?: Record<string, unknown>) => Promise<T>;
66
+ reload: () => Promise<T>;
67
+ }
68
+
69
+ export function useTauriLoad<T = unknown>(
70
+ options: UseLoadOptions<T>,
71
+ ): LoadState<T> {
72
+ const { loadOnMount = true, defaultValue, ...commandOptions } = options;
73
+ const { data, isLoading, error, execute } = useTauriCommand<T>(commandOptions);
74
+
75
+ const reload = useCallback(() => execute(), [execute]);
76
+
77
+ useEffect(() => {
78
+ if (!loadOnMount) return;
79
+ if (!isTauriReady()) return;
80
+ execute().catch(() => {
81
+ /* error already captured */
82
+ });
83
+ }, [loadOnMount, execute]);
84
+
85
+ return {
86
+ data: (data ?? defaultValue) as T,
87
+ isLoading,
88
+ error,
89
+ execute,
90
+ reload,
91
+ };
92
+ }
93
+
94
+ export interface UseMutationOptions<T> extends UseCommandOptions<T> {
95
+ onMutate?: () => void | Promise<void>;
96
+ }
97
+
98
+ export interface MutationState<T> {
99
+ isMutating: boolean;
100
+ error: string | null;
101
+ execute: (args?: Record<string, unknown>) => Promise<T>;
102
+ resetError: () => void;
103
+ }
104
+
105
+ export function useTauriMutation<T = unknown>(
106
+ options: UseMutationOptions<T>,
107
+ ): MutationState<T> {
108
+ const { command, args: defaultArgs, onSuccess, onError, onMutate } = options;
109
+ const [isMutating, setIsMutating] = useState(false);
110
+ const [error, setError] = useState<string | null>(null);
111
+
112
+ const execute = useCallback(
113
+ async (args?: Record<string, unknown>) => {
114
+ if (!isTauriReady()) {
115
+ return Promise.reject(new Error("Tauri not ready"));
116
+ }
117
+ setIsMutating(true);
118
+ setError(null);
119
+ try {
120
+ await onMutate?.();
121
+ const result = await invoke<T>(command, args || defaultArgs);
122
+ onSuccess?.(result);
123
+ return result;
124
+ } catch (err) {
125
+ const errorMsg = String(err);
126
+ setError(errorMsg);
127
+ onError?.(errorMsg);
128
+ throw err;
129
+ } finally {
130
+ setIsMutating(false);
131
+ }
132
+ },
133
+ [command, defaultArgs, onMutate, onSuccess, onError],
134
+ );
135
+
136
+ const resetError = useCallback(() => setError(null), []);
137
+
138
+ return { isMutating, error, execute, resetError };
139
+ }
140
+ ```
141
+
142
+ ## Per-domain hook example
143
+
144
+ ```ts
145
+ // src/hooks/useSettings.ts
146
+ import type { Settings } from "../types";
147
+ import { useTauriLoad, useTauriMutation } from "./useTauriCommand";
148
+
149
+ const DEFAULT_SETTINGS: Settings = { /* sensible defaults */ };
150
+
151
+ export function useSettings() {
152
+ return useTauriLoad<Settings>({
153
+ command: "get_settings",
154
+ defaultValue: DEFAULT_SETTINGS,
155
+ });
156
+ }
157
+
158
+ export function useSaveSettings() {
159
+ return useTauriMutation<void>({ command: "save_settings_command" });
160
+ }
161
+ ```
162
+
163
+ ## Component usage
164
+
165
+ ```tsx
166
+ function SettingsPage() {
167
+ const { data: settings, isLoading, error, reload } = useSettings();
168
+ const { execute: saveSettings, isMutating } = useSaveSettings();
169
+
170
+ if (isLoading) return <Spinner />;
171
+ if (error) return <ErrorBanner error={error} onRetry={reload} />;
172
+
173
+ return (
174
+ <Form
175
+ initialValues={settings}
176
+ onSubmit={async (values) => {
177
+ await saveSettings({ settings: values });
178
+ await reload();
179
+ }}
180
+ />
181
+ );
182
+ }
183
+ ```
184
+
185
+ ## Why this shape
186
+
187
+ - **One place to add cross-cutting concerns** — retry, exponential backoff, telemetry, error toasts, request-id headers — all change one file.
188
+ - **Type safety** — `T` flows through `invoke<T>`. The hook user gets `data: T | null`, not `data: any`.
189
+ - **Three flavors** —
190
+ - `useTauriCommand`: imperative; component triggers it.
191
+ - `useTauriLoad`: auto-fires on mount; for "get me this data".
192
+ - `useTauriMutation`: imperative; explicitly skips auto-fire; for write paths.
193
+ - **`isTauriReady()` guard** — In Tauri 2 the IPC bridge is ready before React mounts, but the guard makes the code safe to preview via plain `vite dev` for static UI iteration.
194
+ - **No `invoke()` in components** — Refactoring command names, adding logging, fixing race conditions all happen in one file. Without this discipline, every component reinvents loading/error state slightly differently.
@@ -0,0 +1,189 @@
1
+ # Frontend configs: `vite.config.ts`, `tsconfig.json`, `package.json`, `index.html`
2
+
3
+ ## `vite.config.ts`
4
+
5
+ ```ts
6
+ import { defineConfig } from "vite";
7
+ import react from "@vitejs/plugin-react";
8
+ import { readFileSync } from "fs";
9
+
10
+ // Read tauri.conf.json so the frontend has a single source of truth for version.
11
+ const tauriConfig = JSON.parse(
12
+ readFileSync("./src-tauri/tauri.conf.json", "utf-8"),
13
+ );
14
+
15
+ // @ts-expect-error process is a Node global
16
+ const host = process.env.TAURI_DEV_HOST;
17
+
18
+ export default defineConfig({
19
+ // Relative base so bundled HTML loads from tauri://localhost
20
+ base: "./",
21
+
22
+ plugins: [react()],
23
+
24
+ // Don't hide Rust compiler errors
25
+ clearScreen: false,
26
+
27
+ server: {
28
+ port: 5173,
29
+ strictPort: true, // Tauri's devUrl pins 5173 — fail loudly if taken
30
+ host: host || false,
31
+ hmr: host
32
+ ? { protocol: "ws", host, port: 1421 }
33
+ : undefined,
34
+ watch: {
35
+ // Cargo handles src-tauri/ rebuilds; Vite ignoring it prevents HMR loops
36
+ ignored: ["**/src-tauri/**"],
37
+ },
38
+ },
39
+
40
+ esbuild: { jsx: "automatic" },
41
+
42
+ define: {
43
+ __APP_VERSION__: JSON.stringify(tauriConfig.version),
44
+ },
45
+ });
46
+ ```
47
+
48
+ Vanilla TypeScript (no React)? Drop the `react()` plugin and the `esbuild.jsx` line.
49
+
50
+ ## `tsconfig.json`
51
+
52
+ ```json
53
+ {
54
+ "compilerOptions": {
55
+ "target": "ES2020",
56
+ "useDefineForClassFields": true,
57
+ "module": "ESNext",
58
+ "lib": ["ES2020", "DOM", "DOM.Iterable"],
59
+ "skipLibCheck": true,
60
+ "jsx": "react-jsx",
61
+
62
+ "moduleResolution": "bundler",
63
+ "allowImportingTsExtensions": true,
64
+ "resolveJsonModule": true,
65
+ "isolatedModules": true,
66
+ "noEmit": true,
67
+
68
+ "strict": true,
69
+ "noUnusedLocals": true,
70
+ "noUnusedParameters": true,
71
+ "noFallthroughCasesInSwitch": true
72
+ },
73
+ "include": ["src"],
74
+ "references": [{ "path": "./tsconfig.node.json" }]
75
+ }
76
+ ```
77
+
78
+ `tsconfig.node.json` is a tiny companion for the `vite.config.ts` file itself:
79
+
80
+ ```json
81
+ {
82
+ "compilerOptions": {
83
+ "composite": true,
84
+ "skipLibCheck": true,
85
+ "module": "ESNext",
86
+ "moduleResolution": "bundler",
87
+ "allowSyntheticDefaultImports": true
88
+ },
89
+ "include": ["vite.config.ts"]
90
+ }
91
+ ```
92
+
93
+ ## `package.json`
94
+
95
+ ```json
96
+ {
97
+ "name": "{{app-name}}",
98
+ "private": true,
99
+ "version": "0.1.0",
100
+ "type": "module",
101
+ "scripts": {
102
+ "dev": "vite",
103
+ "build": "tsc && vite build",
104
+ "preview": "vite preview",
105
+ "tauri": "tauri",
106
+ "tauri:dev": "tauri dev",
107
+ "tauri:build": "tauri build",
108
+ "clean": "rimraf node_modules/.vite dist",
109
+ "test": "cd src-tauri && cargo test"
110
+ },
111
+ "dependencies": {
112
+ "@tauri-apps/api": "<latest 2.x>",
113
+ "@tauri-apps/plugin-dialog": "<latest 2.x>",
114
+ "@tauri-apps/plugin-fs": "<latest 2.x>",
115
+ "@tauri-apps/plugin-notification": "<latest 2.x>",
116
+ "@tauri-apps/plugin-opener": "<latest 2.x>",
117
+ "react": "<latest>",
118
+ "react-dom": "<latest>"
119
+ },
120
+ "devDependencies": {
121
+ "@tauri-apps/cli": "<latest 2.x>",
122
+ "@types/react": "<latest>",
123
+ "@types/react-dom": "<latest>",
124
+ "@vitejs/plugin-react": "<latest>",
125
+ "rimraf": "<latest>",
126
+ "typescript": "<latest>",
127
+ "vite": "<latest>"
128
+ }
129
+ }
130
+ ```
131
+
132
+ Resolve `<latest>` markers at scaffold time — never paste a literal version into a generated file.
133
+
134
+ ## `index.html`
135
+
136
+ ```html
137
+ <!DOCTYPE html>
138
+ <html lang="en">
139
+ <head>
140
+ <meta charset="UTF-8" />
141
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
142
+ <title>{{ProductName}}</title>
143
+
144
+ <!-- Optional: anti-flash theme script. Runs BEFORE React mounts so the
145
+ initial paint matches the user's preference. -->
146
+ <script>
147
+ (function() {
148
+ const stored = localStorage.getItem("{{app-name}}-theme");
149
+ const prefersDark = window.matchMedia("(prefers-color-scheme: dark)").matches;
150
+ const isDark = stored === "dark" || ((!stored || stored === "system") && prefersDark);
151
+ if (isDark) document.documentElement.classList.add("dark");
152
+ })();
153
+ </script>
154
+
155
+ <style>
156
+ #app { opacity: 0; transition: opacity 0.15s ease-in; }
157
+ #app.loaded { opacity: 1; }
158
+ </style>
159
+ </head>
160
+ <body>
161
+ <div id="app"></div>
162
+ <script type="module" src="/src/main.tsx"></script>
163
+ </body>
164
+ </html>
165
+ ```
166
+
167
+ ```tsx
168
+ // src/main.tsx
169
+ import { createRoot } from "react-dom/client";
170
+ import App from "./App";
171
+ import "./styles.css";
172
+
173
+ const rootElement = document.getElementById("app");
174
+ if (rootElement) {
175
+ createRoot(rootElement).render(<App />);
176
+ requestAnimationFrame(() => rootElement.classList.add("loaded"));
177
+ }
178
+ ```
179
+
180
+ ## Why these choices
181
+
182
+ - **`base: './'`** — Tauri serves the bundled frontend from `tauri://localhost/`. Absolute paths (`base: '/'`) break asset URLs like `/assets/index.css` because they resolve against the protocol root.
183
+ - **`strictPort: true`** — Tauri's `devUrl` is `http://localhost:5173`. If Vite silently falls back to 5174, Tauri opens a blank window with no error.
184
+ - **`server.watch.ignored: ['**/src-tauri/**']`** — Cargo writes to `src-tauri/target/` constantly during dev. Without this, Vite's HMR triggers on every Cargo file write and reloads the frontend in a tight loop.
185
+ - **`clearScreen: false`** — Vite's default behavior clears the terminal on startup, hiding Rust compile errors. Tauri devs need both visible.
186
+ - **`isolatedModules: true`** — Required for Vite (which transpiles file-by-file, not whole-program).
187
+ - **`strict: true` + `noUnusedLocals` + `noUnusedParameters`** — Catches the kind of "I forgot to wire this up" mistakes that slip past code review.
188
+ - **Anti-flash script** — Without it, a dark-mode user sees a white flash before React's theme provider mounts. The script reads `localStorage` synchronously and applies the `dark` class before first paint.
189
+ - **Single `createRoot` call** — React 18+ doesn't tolerate multiple roots on the same node. Use one entry point.