refactor: convert to bun-workspaces monorepo (desktop/ + www/)

- Move the Tauri app (React frontend + Rust backend) into desktop/ via
  git mv — configs unchanged (relative paths: tauri.conf.json, vite.config.ts)
- Root package.json becomes a private bun workspace container with
  orchestration scripts; desktop package renamed gridline-desktop
- Scaffold www/ with Astro 7 + Tailwind v4 (hand-wired vite plugin,
  static output, ready for Dokploy)
- Update release.yml for the new layout: projectPath: desktop,
  desktop/src-tauri resource paths, rust-cache workspace path
- Update README + AGENTS.md structure trees and dev commands
- Verified: vitest (1074), cargo test (354), desktop build, tauri dev
  (window launches), tauri build (dmg + app bundles), Astro build
This commit is contained in:
2026-08-16 19:31:53 +08:00
parent d752642b4d
commit 6854d889c5
400 changed files with 857 additions and 149 deletions
@@ -0,0 +1,92 @@
import { Input } from "../ui/Input";
import { PasswordInput } from "./PasswordInput";
import { SqlitePathInput } from "./SqlitePathInput";
import { INPUT_ROUNDING } from "../../lib/uiConstants";
import type { ConnectionFormData } from "./connectionFormData";
export interface GeneralTabProps {
form: ConnectionFormData;
onChange: (updates: Partial<ConnectionFormData>) => void;
managedPreset?: "supabase" | "neon" | null;
}
const AUTH_OPTIONS = ["User & Password"];
export function GeneralTab({ form, onChange, managedPreset }: GeneralTabProps) {
const isSqlite = form.db_type === "sqlite";
return (
<div className="space-y-4">
<div>
<label className="block text-sm text-text mb-1.5">{isSqlite ? "File Path" : "Connection URI"}</label>
{isSqlite ? (
<SqlitePathInput value={form.host} onChange={(value) => onChange({ host: value })} />
) : (
<textarea
value={form.connection_string}
onChange={(e) => onChange({ connection_string: e.target.value })}
placeholder="postgresql://user:password@host:5432/database"
aria-label="Connection URI"
rows={1}
className={`w-full ${INPUT_ROUNDING} bg-surface border border-border px-4 py-2 text-sm text-text font-mono placeholder-text-muted/60 focus:outline-none focus:border-accent focus:ring-1 focus:ring-accent/50 transition-colors resize-none overflow-x-auto whitespace-nowrap`}
/>
)}
{managedPreset && (
<p className="text-xs text-accent-muted mt-1.5">
{managedPreset === "supabase" ? "Supabase" : "NeonDB"} requires SSL — enable it under SSH / SSL.
</p>
)}
</div>
{!isSqlite && (
<>
<div className="flex items-center gap-3 my-1" data-testid="or-divider">
<div className="flex-1 h-px bg-border" />
<span className="text-xs text-text-muted">OR</span>
<div className="flex-1 h-px bg-border" />
</div>
<div className="flex gap-3">
<div className="flex-1">
<label className="block text-sm text-text mb-1.5">Host</label>
<Input value={form.host} onChange={(value) => onChange({ host: value })} placeholder="localhost" aria-label="Host" />
</div>
<div className="w-28">
<label className="block text-sm text-text mb-1.5">Port</label>
<Input type="number" value={form.port?.toString() ?? ""} onChange={(value) => onChange({ port: value === "" ? null : Number(value) })} placeholder="5432" aria-label="Port" className="[appearance:textfield] [&::-webkit-outer-spin-button]:appearance-none [&::-webkit-inner-spin-button]:appearance-none [-moz-appearance:textfield]" />
</div>
</div>
<div>
<label className="block text-sm text-text mb-1.5">Authentication</label>
<select
value={AUTH_OPTIONS[0]}
disabled
aria-label="Authentication"
className={`w-full ${INPUT_ROUNDING} bg-surface border border-border px-4 py-2 text-sm text-text opacity-70 cursor-not-allowed`}
>
{AUTH_OPTIONS.map((opt) => <option key={opt}>{opt}</option>)}
</select>
</div>
<div>
<label className="block text-sm text-text mb-1.5">User</label>
<Input value={form.username ?? ""} onChange={(value) => onChange({ username: value || null })} placeholder="postgres" aria-label="User" />
</div>
<div>
<label className="block text-sm text-text mb-1.5">Password</label>
<PasswordInput value={form.password ?? ""} onChange={(value) => onChange({ password: value || null })} placeholder="••••••••" aria-label="Password" />
</div>
<div>
<label className="block text-sm text-text mb-1.5">Database (optional)</label>
<Input value={form.database ?? ""} onChange={(value) => onChange({ database: value || null })} placeholder="database" aria-label="Database" />
</div>
</>
)}
<label className="flex items-center gap-2 text-sm text-text cursor-pointer">
<input type="checkbox" checked={form.use_keychain} onChange={(e) => onChange({ use_keychain: e.target.checked })} aria-label="Enable keychain" className="rounded border-border bg-surface text-accent focus:ring-accent" />
Enable keychain
</label>
<p className="text-xs text-text-muted -mt-1 mb-2">
Saves the DB password to the OS keychain (DB password only; SSH secrets always use the keychain). Uncheck to never persist the password — you'll re-enter it each session.
</p>
</div>
);
}