diff --git a/.github/workflows/integration-check.yml b/.github/workflows/integration-check.yml new file mode 100644 index 0000000..e9c29b9 --- /dev/null +++ b/.github/workflows/integration-check.yml @@ -0,0 +1,91 @@ +name: Platform Integration Check + +on: + push: + branches: [main] + pull_request: + types: [opened, synchronize, reopened] + +permissions: + contents: read + +jobs: + integration-rules: + name: Platform Integration Rules + runs-on: ubuntu-latest + + steps: + - name: Checkout code + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 + + - name: Setup Bun + uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2 + with: + bun-version: "1.3.9" + + - name: Install dependencies + run: bun install --frozen-lockfile + + - name: Build library dist + run: bun run build:lib + + - name: Check — No custom @theme text tokens + run: | + echo "Checking globals.css for custom text tokens..." + if grep -E '^\s*--text-[a-z]+:' src/app/globals.css | grep -v 'font-'; then + echo "::error::Custom text tokens found in @theme. tailwind-merge will silently strip these." + echo "Use standard Tailwind classes (text-xs, text-sm) or arbitrary values (text-[0.625rem]) instead." + exit 1 + fi + echo "✓ No custom text tokens" + + - name: Check — No custom text-* classes in components + run: | + echo "Checking for custom text token usage in components..." + CUSTOM=$(grep -rn '\btext-body\b\|\btext-data\b\|\btext-label\b\|\btext-title\b\|\btext-micro\b\|\btext-caption\b' \ + src/components/ src/workspace/ --include="*.tsx" || true) + if [ -n "$CUSTOM" ]; then + echo "::error::Custom text tokens used in components. twMerge strips these silently." + echo "$CUSTOM" + exit 1 + fi + echo "✓ No custom text token usage" + + - name: Check — Lucide icons have strokeWidth + run: | + echo "Checking Lucide icons for strokeWidth prop..." + MISSING=$(grep -rn '<[A-Z][a-zA-Z]* className="w-' src/components/ src/workspace/ --include="*.tsx" \ + | grep -v 'admin/' | grep -v 'ui/' \ + | grep -v 'strokeWidth' \ + | grep -v ' to avoid platform CSS conflicts." + echo "$BUTTONS" + exit 1 + fi + echo "✓ No Button size=icon in compact areas" + + - name: Check — Dist chunks contain expected classes + run: | + echo "Checking dist for responsive class coverage..." + ALL_MD=$(grep -roh 'md:[a-z-]*' dist/*.mjs | sort -u) + WORKSPACE_MD=$(grep -roh 'md:[a-z-]*' dist/workspace.mjs | sort -u) + ONLY_IN_CHUNKS=$(comm -23 <(echo "$ALL_MD") <(echo "$WORKSPACE_MD")) + if [ -n "$ONLY_IN_CHUNKS" ]; then + echo "::notice::Responsive classes found only in chunks (not workspace.mjs):" + echo "$ONLY_IN_CHUNKS" + echo "Platform must scan chunk-*.mjs via @source directive." + fi + echo "✓ Dist chunk analysis complete" diff --git a/.github/workflows/npm-publish.yml b/.github/workflows/npm-publish.yml new file mode 100644 index 0000000..a6b158a --- /dev/null +++ b/.github/workflows/npm-publish.yml @@ -0,0 +1,111 @@ +name: NPM Publish + +on: + push: + tags: + - 'v*' + workflow_dispatch: + inputs: + version: + description: 'Version to publish (e.g., 0.9.8). Leave empty to use package.json version.' + required: false + type: string + dry_run: + description: 'Dry run (do not actually publish)' + required: false + type: boolean + default: false + +permissions: + contents: read + +jobs: + validate: + name: Validate + runs-on: ubuntu-latest + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Setup Bun + uses: oven-sh/setup-bun@v2 + with: + bun-version: "1.3.9" + + - name: Install dependencies + run: bun install --frozen-lockfile + + - name: Lint + run: bun run lint + + - name: Typecheck + run: bun run typecheck + + - name: Test + run: bun run test + + - name: Build + run: bun run build + env: + NEXT_TELEMETRY_DISABLED: 1 + JWT_SECRET: test-secret-for-ci-build-only-32ch + ADMIN_EMAIL: admin@libredb.org + ADMIN_PASSWORD: test-admin + USER_EMAIL: user@libredb.org + USER_PASSWORD: test-user + + publish: + name: Publish to NPM + runs-on: ubuntu-latest + needs: validate + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Setup Bun + uses: oven-sh/setup-bun@v2 + with: + bun-version: "1.3.9" + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: '22' + registry-url: 'https://registry.npmjs.org' + + - name: Install dependencies + run: bun install --frozen-lockfile + + - name: Build library (tsup) + run: bun run build:lib + + - name: Set version (if provided) + if: inputs.version != '' + env: + INPUT_VERSION: ${{ inputs.version }} + run: npm version "$INPUT_VERSION" --no-git-tag-version + + - name: Verify package + run: | + echo "Package name: $(node -p "require('./package.json').name")" + echo "Package version: $(node -p "require('./package.json').version")" + + - name: Publish (dry run) + if: inputs.dry_run == true + run: npm publish --access public --dry-run + env: + NODE_AUTH_TOKEN: ${{ secrets.NPMJS_TOKEN }} + + - name: Publish + if: inputs.dry_run != true + run: npm publish --access public + env: + NODE_AUTH_TOKEN: ${{ secrets.NPMJS_TOKEN }} + + - name: Summary + run: | + VERSION=$(node -p "require('./package.json').version") + echo "## Published @libredb/studio@${VERSION}" >> "$GITHUB_STEP_SUMMARY" + echo "" >> "$GITHUB_STEP_SUMMARY" + echo "- **Registry:** https://www.npmjs.com/package/@libredb/studio" >> "$GITHUB_STEP_SUMMARY" + echo "- **Install:** \`npm install @libredb/studio@${VERSION}\`" >> "$GITHUB_STEP_SUMMARY" diff --git a/.gitignore b/.gitignore index eb337a2..15c6cb2 100644 --- a/.gitignore +++ b/.gitignore @@ -98,3 +98,6 @@ seed-connections.yaml .playwright-mcp/*.png +npmjs-token + +.npmrc diff --git a/.npmignore b/.npmignore new file mode 100644 index 0000000..b870b3c --- /dev/null +++ b/.npmignore @@ -0,0 +1,27 @@ +# Publish only source code needed for npm consumers +# Everything is ignored by default, then we allowlist + +# Ignore everything +* + +# Allow source exports +!src/exports/ +!src/exports/** + +# Allow library code (providers, types, utils) +!src/lib/ +!src/lib/** + +# Allow components +!src/components/ +!src/components/** + +# Allow hooks (used by components) +!src/hooks/ +!src/hooks/** + +# Allow package files +!package.json +!README.md +!LICENSE +!tsconfig.json diff --git a/CLAUDE.md b/CLAUDE.md index 7eb90ff..5326c4e 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -2,10 +2,16 @@ This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. +> **CRITICAL: This project is published as an npm package (`@libredb/studio`) and consumed by `libredb-platform`.** +> Every CSS class, Tailwind utility, icon prop, and component choice MUST follow the Platform Integration Rules below. +> Violations cause silent style/layout breakage that only appears when embedded in platform — not in standalone studio. + ## Project Overview LibreDB Studio is a web-based SQL IDE for cloud-native teams. It supports PostgreSQL, MySQL, SQLite, Oracle, SQL Server, MongoDB, Redis with AI-powered query assistance. +**Dual usage:** Studio runs both as a standalone Next.js app AND as an embedded npm package inside libredb-platform. The `build:lib` command (tsup) produces the package dist. Any UI change must be verified in both modes. + ## Github * Repository: https://github.com/libredb/libredb-studio * Container Registry: https://github.com/libredb/libredb-studio/pkgs/container/libredb-studio @@ -48,6 +54,11 @@ bun run test:e2e # Coverage report bun run test:coverage +# Library build (tsup) — exports @libredb/studio package for platform consumption +# IMPORTANT: After changing any component used by platform (workspace, providers, etc.), +# you MUST run this command. `bun run build` (Next.js) does NOT update the package dist. +bun run build:lib + # Docker development docker-compose up -d @@ -61,6 +72,51 @@ The project uses ESLint 9 for linting and `bun:test` for testing with `@testing- > **Important**: Always use `bun run test` instead of bare `bun test`. Component tests require isolated execution groups (handled by `tests/run-components.sh`) to prevent `mock.module()` cross-contamination between test files. +## Platform Integration Rules (npm package @libredb/studio) + +Studio is consumed by libredb-platform as an npm package via `build:lib` (tsup). These rules prevent silent style/layout breakage that only appears when embedded in platform. + +### Tailwind CSS Rules + +| Do | Don't | Why | +|----|-------|-----| +| `text-xs`, `text-sm` (standard) | `text-body`, `text-data` (custom @theme) | `tailwind-merge` strips custom tokens silently | +| `text-[0.625rem]` (arbitrary value) | `text-label` (custom @theme) | Arbitrary values are twMerge-safe | +| `font-medium`, `font-normal` | `font-bold` everywhere | Studio is compact IDE, lighter weights | +| `w-3 h-3`, `w-3.5 h-3.5` (icons) | `w-4 h-4` or larger | Studio icons smaller than platform | + +**Never define custom text tokens in `@theme` block.** `tailwind-merge` (used in `cn()`) interprets `text-body` as a color utility, not font-size. When combined with `text-muted-foreground`, twMerge silently removes `text-body` → no font-size applied → browser default 16px. This bug is invisible in standalone studio (Tailwind generates the CSS) but breaks embedded mode. + +### Lucide Icon Rules + +Always pass `strokeWidth={1.5}` to every Lucide icon: +```tsx + +``` +Lucide defaults to `strokeWidth=2` and emits `width="24" height="24"` HTML attributes. Custom DB icons use `strokeWidth=1.5` without HTML size attributes. Without this prop, Lucide icons appear thicker and potentially larger than custom icons. + +### Component Rules + +- **Small icon buttons:** Use plain ` {onExecuteQuery && ( )} @@ -159,9 +159,9 @@ export function AIAutopilotPanel({ connection, schemaContext, onExecuteQuery }: // Headers if (line.startsWith('## ')) { - elements.push(

{line.slice(3)}

); + elements.push(

{line.slice(3)}

); } else if (line.startsWith('### ')) { - elements.push(

{line.slice(4)}

); + elements.push(

{line.slice(4)}

); } else if (line.startsWith('- ')) { const content = line.slice(2).replace(/\*\*(.*?)\*\*/g, '$1'); elements.push(
  • ); @@ -185,9 +185,9 @@ export function AIAutopilotPanel({ connection, schemaContext, onExecuteQuery }:
    - +
    - + AI Performance Autopilot
    @@ -195,16 +195,16 @@ export function AIAutopilotPanel({ connection, schemaContext, onExecuteQuery }: onClick={runAutopilot} disabled={isLoading || !connection} className={cn( - "flex items-center gap-1.5 px-3 py-1.5 rounded-lg text-[10px] font-bold transition-colors", + "flex items-center gap-1.5 px-3 py-1.5 rounded-lg text-xs font-medium transition-colors", isLoading ? "bg-cyan-600/20 text-cyan-400 cursor-wait" : "bg-cyan-600 hover:bg-cyan-500 text-white" )} > {isLoading ? ( - <> Analyzing... + <> Analyzing... ) : ( - <> {report ? 'Re-analyze' : 'Run Analysis'} + <> {report ? 'Re-analyze' : 'Run Analysis'} )}
    @@ -213,9 +213,9 @@ export function AIAutopilotPanel({ connection, schemaContext, onExecuteQuery }:
    {!report && !isLoading && !error && (
    - -

    AI Performance Autopilot

    -

    + +

    AI Performance Autopilot

    +

    Click "Run Analysis" to get AI-powered optimization recommendations

    diff --git a/src/components/CodeGenerator.tsx b/src/components/CodeGenerator.tsx index 5d5f4a7..afc652e 100644 --- a/src/components/CodeGenerator.tsx +++ b/src/components/CodeGenerator.tsx @@ -204,15 +204,15 @@ export function CodeGenerator({ {/* Header */}
    - - Code Generator + + Code Generator {tableName} {databaseType && ( - {databaseType} + {databaseType} )}
    @@ -224,7 +224,7 @@ export function CodeGenerator({ className="flex items-center gap-2 px-3 py-1.5 rounded-lg bg-white/5 border border-white/10 text-xs text-zinc-300 hover:bg-white/10 transition-colors" > {currentLang.label} - + {showLangDropdown && (
    @@ -247,21 +247,21 @@ export function CodeGenerator({ {/* Code Preview */}
    -
    +          
                 {code}
               
    {/* Footer */}
    -

    +

    Generated from {tableName} • {tableSchema?.columns?.length || 0} columns • {currentLang.ext} format

    diff --git a/src/components/CommandPalette.tsx b/src/components/CommandPalette.tsx index b1cece9..785d370 100644 --- a/src/components/CommandPalette.tsx +++ b/src/components/CommandPalette.tsx @@ -108,42 +108,42 @@ export function CommandPalette({ {/* Quick Actions */} runAction(onExecuteQuery)}> - + Run Query Ctrl+Enter runAction(onFormatQuery)}> - + Format Query runAction(onSaveQuery)}> - + Save Current Query runAction(onToggleAI)}> - + AI Assistant runAction(onAddConnection)}> - + New Connection runAction(onNavigateHealth)}> - + Health Dashboard runAction(onNavigateMonitoring)}> - + Monitoring {activeConnection && ( runAction(onShowDiagram)}> - + Schema Diagram (ERD) )} runAction(onLogout)}> - + Logout @@ -158,10 +158,10 @@ export function CommandPalette({ key={conn.id} onSelect={() => runAction(() => onSelectConnection(conn))} > - + {conn.name} {activeConnection?.id === conn.id && ( - Active + Active )} ); @@ -177,9 +177,9 @@ export function CommandPalette({ key={table.name} onSelect={() => runAction(() => onTableClick(table.name))} > - + {table.name} - + {table.columns.length} cols {table.rowCount !== undefined && ` / ${table.rowCount} rows`} @@ -196,9 +196,9 @@ export function CommandPalette({ key={sq.id} onSelect={() => runAction(() => onLoadSavedQuery(sq.query))} > - + {sq.name} - + {sq.query.substring(0, 40)}... @@ -214,9 +214,9 @@ export function CommandPalette({ key={item.id} onSelect={() => runAction(() => onLoadHistoryQuery(item.query))} > - + {item.query.substring(0, 60)} - {item.executionTime}ms + {item.executionTime}ms ))} diff --git a/src/components/ConnectionModal.tsx b/src/components/ConnectionModal.tsx index a89ac61..d70bfc4 100644 --- a/src/components/ConnectionModal.tsx +++ b/src/components/ConnectionModal.tsx @@ -1,6 +1,5 @@ "use client"; -import React from 'react'; import { Dialog, DialogContent, DialogTitle, DialogDescription } from '@/components/ui/dialog'; import { Drawer, DrawerContent, DrawerHeader, DrawerTitle, DrawerDescription } from '@/components/ui/drawer'; import { Button } from '@/components/ui/button'; @@ -19,9 +18,11 @@ interface ConnectionModalProps { onClose: () => void; onConnect: (conn: DatabaseConnection) => void; editConnection?: DatabaseConnection | null; + /** Optional API adapter: when provided, bypasses the built-in /api/db/test-connection fetch. */ + onTestConnection?: (connection: DatabaseConnection) => Promise<{ success: boolean; latency?: number; error?: string }>; } -export function ConnectionModal({ isOpen, onClose, onConnect, editConnection }: ConnectionModalProps) { +export function ConnectionModal({ isOpen, onClose, onConnect, editConnection, onTestConnection }: ConnectionModalProps) { const isMobile = useIsMobile(); const { // Connection fields @@ -73,7 +74,7 @@ export function ConnectionModal({ isOpen, onClose, onConnect, editConnection }: // Derived data dbTypes, - } = useConnectionForm({ isOpen, onClose, onConnect, editConnection }); + } = useConnectionForm({ isOpen, onClose, onConnect, editConnection, onTestConnection }); const formContent = ( <> @@ -91,22 +92,22 @@ export function ConnectionModal({ isOpen, onClose, onConnect, editConnection }:
    - +
    -

    +

    {isEditMode ? 'Edit Connection' : 'New Connection'}

    -

    +

    {isEditMode ? 'Update your database connection parameters.' : 'Configure your database connection parameters securely.'}

    {!isEditMode && ( )} @@ -123,7 +124,7 @@ export function ConnectionModal({ isOpen, onClose, onConnect, editConnection }: className="mb-6 overflow-hidden" >
    -
    @@ -154,28 +155,28 @@ export function ConnectionModal({ isOpen, onClose, onConnect, editConnection }: {/* Connection Name - always visible */}
    - - + +
    setName(e.target.value)} placeholder="My Database" - className="h-10 bg-zinc-900/50 border-white/5 focus:border-blue-500/50 transition-all text-sm" + className="h-10 bg-zinc-900/50 border-white/5 focus:border-blue-500/50 transition-all text-xs" />
    {/* Environment Selector */}
    - +
    {(Object.keys(ENVIRONMENT_COLORS) as ConnectionEnvironment[]).map((env) => ( @@ -227,25 +228,25 @@ export function ConnectionModal({ isOpen, onClose, onConnect, editConnection }:
    @@ -255,28 +256,28 @@ export function ConnectionModal({ isOpen, onClose, onConnect, editConnection }: <>
    - - + +
    setConnectionString(e.target.value)} placeholder="mongodb://localhost:27017/mydb or mongodb+srv://..." - className="h-10 bg-zinc-900/50 border-white/5 focus:border-blue-500/50 transition-all text-sm font-mono" + className="h-10 bg-zinc-900/50 border-white/5 focus:border-blue-500/50 transition-all text-xs font-mono" />
    - - + +
    setDatabase(e.target.value)} placeholder="Extracted from URI if not provided" - className="h-10 bg-zinc-900/50 border-white/5 focus:border-blue-500/50 transition-all text-sm font-mono" + className="h-10 bg-zinc-900/50 border-white/5 focus:border-blue-500/50 transition-all text-xs font-mono" />
    @@ -284,8 +285,8 @@ export function ConnectionModal({ isOpen, onClose, onConnect, editConnection }: <>
    - - + +
    setHost(e.target.value)} placeholder="localhost" - className="md:col-span-3 h-10 bg-zinc-900/50 border-white/5 focus:border-blue-500/50 transition-all text-sm" + className="md:col-span-3 h-10 bg-zinc-900/50 border-white/5 focus:border-blue-500/50 transition-all text-xs" /> setPort(e.target.value)} - className="h-10 bg-zinc-900/50 border-white/5 focus:border-blue-500/50 transition-all text-sm font-mono" + className="h-10 bg-zinc-900/50 border-white/5 focus:border-blue-500/50 transition-all text-xs font-mono" />
    @@ -307,21 +308,21 @@ export function ConnectionModal({ isOpen, onClose, onConnect, editConnection }:
    - - + +
    setUser(e.target.value)} placeholder="postgres" - className="h-10 bg-zinc-900/50 border-white/5 focus:border-blue-500/50 transition-all text-sm" + className="h-10 bg-zinc-900/50 border-white/5 focus:border-blue-500/50 transition-all text-xs" />
    - - + +
    setPassword(e.target.value)} placeholder="••••••••" - className="h-10 bg-zinc-900/50 border-white/5 focus:border-blue-500/50 transition-all text-sm" + className="h-10 bg-zinc-900/50 border-white/5 focus:border-blue-500/50 transition-all text-xs" />
    - - + +
    setDatabase(e.target.value)} placeholder="production_db" - className="h-10 bg-zinc-900/50 border-white/5 focus:border-blue-500/50 transition-all text-sm font-mono" + className="h-10 bg-zinc-900/50 border-white/5 focus:border-blue-500/50 transition-all text-xs font-mono" />
    @@ -358,12 +359,12 @@ export function ConnectionModal({ isOpen, onClose, onConnect, editConnection }: