feat: restructure as pnpm monorepo with Tauri desktop shell
- Migrate to packages/web + packages/desktop workspace layout via git mv - Add Tauri v2 desktop shell with @tauri-apps/plugin-http for CORS bypass - Configure Turborepo with package-level dependsOn build graph - Add semantic-release with exec plugin for GHA output and disabled PR comments - Fix http:default capability scope to allow all HTTP/HTTPS origins - Add Vite Tauri integration (clearScreen, TAURI_DEV_HOST, target, envPrefix) - Add semantic-release.yml and release.yml GitHub Actions workflows - Fix all Biome lint errors (noArrayIndexKey, noNonNullAssertion, button types)
This commit is contained in:
64
packages/web/src/lib/config.ts
Normal file
64
packages/web/src/lib/config.ts
Normal file
@@ -0,0 +1,64 @@
|
||||
import { httpFetch } from "@/lib/http";
|
||||
import { z } from "zod";
|
||||
|
||||
const CONFIG_KEY = "openconcho:config";
|
||||
|
||||
export const configSchema = z.object({
|
||||
baseUrl: z.string().url("Must be a valid URL"),
|
||||
token: z.string().optional().default(""),
|
||||
});
|
||||
|
||||
export type Config = z.infer<typeof configSchema>;
|
||||
|
||||
export function loadConfig(): Config | null {
|
||||
try {
|
||||
const raw = localStorage.getItem(CONFIG_KEY);
|
||||
if (!raw) return null;
|
||||
const parsed = JSON.parse(raw);
|
||||
return configSchema.parse(parsed);
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
export function saveConfig(config: Config): void {
|
||||
localStorage.setItem(CONFIG_KEY, JSON.stringify(config));
|
||||
}
|
||||
|
||||
export function clearConfig(): void {
|
||||
localStorage.removeItem(CONFIG_KEY);
|
||||
}
|
||||
|
||||
export type HealthStatus = "ok" | "auth-required" | "unreachable" | "checking";
|
||||
|
||||
export async function checkConnection(
|
||||
baseUrl: string,
|
||||
token?: string,
|
||||
): Promise<{
|
||||
status: HealthStatus;
|
||||
message: string;
|
||||
}> {
|
||||
try {
|
||||
const headers: Record<string, string> = { "Content-Type": "application/json" };
|
||||
if (token) headers.Authorization = `Bearer ${token}`;
|
||||
|
||||
const res = await httpFetch(`${baseUrl}/v3/workspaces/list`, {
|
||||
method: "POST",
|
||||
headers,
|
||||
body: JSON.stringify({}),
|
||||
signal: AbortSignal.timeout(5000),
|
||||
});
|
||||
|
||||
if (res.ok) return { status: "ok", message: "Connected successfully" };
|
||||
if (res.status === 401 || res.status === 403) {
|
||||
return { status: "auth-required", message: "Authentication required — provide an API token" };
|
||||
}
|
||||
return { status: "unreachable", message: `Server returned ${res.status}` };
|
||||
} catch (err) {
|
||||
const msg = err instanceof Error ? err.message : "Unknown error";
|
||||
if (msg.includes("AbortError") || msg.includes("timeout")) {
|
||||
return { status: "unreachable", message: "Connection timed out" };
|
||||
}
|
||||
return { status: "unreachable", message: `Cannot reach server: ${msg}` };
|
||||
}
|
||||
}
|
||||
43
packages/web/src/lib/constants.ts
Normal file
43
packages/web/src/lib/constants.ts
Normal file
@@ -0,0 +1,43 @@
|
||||
// Semantic color tokens for inline styles.
|
||||
// CSS variables (var(--text-1) etc.) handle theme-aware colors.
|
||||
// These constants are for fixed semantic states that don't invert with theme.
|
||||
|
||||
export const COLOR = {
|
||||
// Status
|
||||
success: "#34d399",
|
||||
successDim: "rgba(52,211,153,0.08)",
|
||||
successBorder: "rgba(52,211,153,0.2)",
|
||||
|
||||
warning: "#f59e0b",
|
||||
warningDim: "rgba(245,158,11,0.08)",
|
||||
warningBorder: "rgba(245,158,11,0.2)",
|
||||
|
||||
destructive: "#f87171",
|
||||
destructiveDim: "rgba(239,68,68,0.08)",
|
||||
destructiveBorder: "rgba(239,68,68,0.2)",
|
||||
|
||||
// Accent (indigo — matches --accent CSS var)
|
||||
accent: "#6366f1",
|
||||
accentText: "#818cf8",
|
||||
accentSoft: "#c7d2fe",
|
||||
accentDim: "rgba(99,102,241,0.08)",
|
||||
accentDimHover: "rgba(99,102,241,0.06)",
|
||||
accentSubtle: "rgba(99,102,241,0.1)",
|
||||
accentMuted: "rgba(99,102,241,0.6)",
|
||||
accentGlow: "rgba(99,102,241,0.4)",
|
||||
accentBorder: "rgba(99,102,241,0.2)",
|
||||
accentBorderStrong: "rgba(99,102,241,0.15)",
|
||||
accentSpinnerTrack: "rgba(99,102,241,0.15)",
|
||||
|
||||
// Neutral dim (slate-300 at opacity)
|
||||
dimText: "rgba(148,163,184,0.5)",
|
||||
dimIcon: "rgba(148,163,184,0.3)",
|
||||
|
||||
// Error detail text
|
||||
destructiveMuted: "rgba(248,113,113,0.6)",
|
||||
destructiveBorderStrong: "rgba(239,68,68,0.25)",
|
||||
|
||||
// Framer-motion hover card base state (inline only — CSS vars can't be animated)
|
||||
cardBaseBg: "rgba(255,255,255,0.02)",
|
||||
cardBaseBorder: "rgba(255,255,255,0.06)",
|
||||
} as const;
|
||||
12
packages/web/src/lib/http.ts
Normal file
12
packages/web/src/lib/http.ts
Normal file
@@ -0,0 +1,12 @@
|
||||
import { fetch as tauriFetch } from "@tauri-apps/plugin-http";
|
||||
|
||||
// Route fetch through Rust (reqwest) when running in Tauri — bypasses WebView CORS enforcement.
|
||||
// Falls back to native browser fetch during plain web dev (`pnpm dev:web`).
|
||||
const isTauri = Boolean(
|
||||
typeof window !== "undefined" &&
|
||||
(window as unknown as Record<string, unknown>).__TAURI_INTERNALS__,
|
||||
);
|
||||
|
||||
export const httpFetch: typeof globalThis.fetch = isTauri
|
||||
? (tauriFetch as typeof globalThis.fetch)
|
||||
: globalThis.fetch;
|
||||
14
packages/web/src/lib/theme.ts
Normal file
14
packages/web/src/lib/theme.ts
Normal file
@@ -0,0 +1,14 @@
|
||||
const THEME_KEY = "openconcho:theme";
|
||||
|
||||
export type Theme = "dark" | "light";
|
||||
|
||||
export function getStoredTheme(): Theme {
|
||||
const stored = localStorage.getItem(THEME_KEY) as Theme | null;
|
||||
if (stored === "dark" || stored === "light") return stored;
|
||||
return window.matchMedia("(prefers-color-scheme: dark)").matches ? "dark" : "light";
|
||||
}
|
||||
|
||||
export function applyTheme(theme: Theme): void {
|
||||
document.documentElement.setAttribute("data-theme", theme);
|
||||
localStorage.setItem(THEME_KEY, theme);
|
||||
}
|
||||
16
packages/web/src/lib/utils.ts
Normal file
16
packages/web/src/lib/utils.ts
Normal file
@@ -0,0 +1,16 @@
|
||||
import { type ClassValue, clsx } from "clsx";
|
||||
import { twMerge } from "tailwind-merge";
|
||||
|
||||
export function cn(...inputs: ClassValue[]) {
|
||||
return twMerge(clsx(inputs));
|
||||
}
|
||||
|
||||
const _compact = new Intl.NumberFormat(undefined, {
|
||||
notation: "compact",
|
||||
maximumFractionDigits: 1,
|
||||
});
|
||||
|
||||
export function formatCount(n: number): string {
|
||||
if (n < 1_000) return String(n);
|
||||
return _compact.format(n);
|
||||
}
|
||||
Reference in New Issue
Block a user