diff --git a/plugins/code-server/package.json b/plugins/code-server/package.json
index 3fabbf6..fd82749 100644
--- a/plugins/code-server/package.json
+++ b/plugins/code-server/package.json
@@ -42,6 +42,7 @@
"scripts": {
"build": "tsdown && vite build --config src/spa/vite.config.ts",
"watch": "tsdown --watch",
+ "typecheck": "tsc --noEmit",
"dev": "vite --config src/spa/vite.config.ts --host 0.0.0.0",
"storybook": "storybook dev -p 6006 --host 0.0.0.0",
"build-storybook": "storybook build",
diff --git a/plugins/code-server/tsconfig.json b/plugins/code-server/tsconfig.json
index 8a6d5a7..1b1d87c 100644
--- a/plugins/code-server/tsconfig.json
+++ b/plugins/code-server/tsconfig.json
@@ -1,7 +1,8 @@
{
"extends": "../../tsconfig.base.json",
"compilerOptions": {
- "composite": true,
"lib": ["esnext", "dom"]
- }
+ },
+ "include": ["src", "test", "tsdown.config.ts"],
+ "exclude": ["dist", "src/spa/dist", ".next", "out", "node_modules"]
}
diff --git a/plugins/git/.gitignore b/plugins/git/.gitignore
new file mode 100644
index 0000000..0d65ee7
--- /dev/null
+++ b/plugins/git/.gitignore
@@ -0,0 +1,7 @@
+.next
+dist
+next-env.d.ts
+node_modules
+out
+.turbo
+storybook-static
diff --git a/plugins/git/.storybook/main.ts b/plugins/git/.storybook/main.ts
new file mode 100644
index 0000000..9610717
--- /dev/null
+++ b/plugins/git/.storybook/main.ts
@@ -0,0 +1,18 @@
+import type { StorybookConfig } from '@storybook/react-vite'
+import tailwindcss from '@tailwindcss/vite'
+
+const config: StorybookConfig = {
+ stories: ['../src/client/**/*.stories.@(ts|tsx)'],
+ addons: ['@storybook/addon-docs', '@storybook/addon-a11y'],
+ framework: {
+ name: '@storybook/react-vite',
+ options: {},
+ },
+ viteFinal(viteConfig) {
+ viteConfig.plugins ??= []
+ viteConfig.plugins.push(tailwindcss())
+ return viteConfig
+ },
+}
+
+export default config
diff --git a/plugins/git/.storybook/preview.tsx b/plugins/git/.storybook/preview.tsx
new file mode 100644
index 0000000..d65afa6
--- /dev/null
+++ b/plugins/git/.storybook/preview.tsx
@@ -0,0 +1,42 @@
+import type { Decorator, Preview } from '@storybook/react-vite'
+import { useEffect } from 'react'
+import '../src/client/app/globals.css'
+
+const withTheme: Decorator = (Story, context) => {
+ const theme = context.globals.theme ?? 'dark'
+ useEffect(() => {
+ document.documentElement.classList.toggle('dark', theme === 'dark')
+ }, [theme])
+ return (
+
+ )
+}
+
+const preview: Preview = {
+ parameters: {
+ layout: 'fullscreen',
+ controls: { expanded: true },
+ },
+ globalTypes: {
+ theme: {
+ description: 'Color theme',
+ defaultValue: 'dark',
+ toolbar: {
+ title: 'Theme',
+ icon: 'contrast',
+ items: [
+ { value: 'light', title: 'Light', icon: 'sun' },
+ { value: 'dark', title: 'Dark', icon: 'moon' },
+ ],
+ dynamicTitle: true,
+ },
+ },
+ },
+ decorators: [withTheme],
+}
+
+export default preview
diff --git a/plugins/git/LICENSE.md b/plugins/git/LICENSE.md
new file mode 100644
index 0000000..09e688c
--- /dev/null
+++ b/plugins/git/LICENSE.md
@@ -0,0 +1,21 @@
+MIT License
+
+Copyright (c) 2026-PRESENT Anthony Fu
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
diff --git a/plugins/git/README.md b/plugins/git/README.md
new file mode 100644
index 0000000..57be136
--- /dev/null
+++ b/plugins/git/README.md
@@ -0,0 +1,90 @@
+# @devframes/plugin-git
+
+Git integration for [devframe](https://github.com/devframes/devframe) — a
+repository dashboard with a **Next.js App Router + shadcn/ui** SPA over
+type-safe RPC. The host process shells out to `git` and exposes the repository;
+the same bundle runs as a live dev server or a fully static deployment.
+
+Status, a SourceTree-style **commit graph**, branches, and diffs are read-only;
+staging, unstaging, and committing are available when write mode is enabled. The
+UI follows the system **light/dark** preference with a manual toggle.
+
+## Install
+
+```sh
+npm i -D @devframes/plugin-git
+```
+
+## Standalone CLI
+
+Run the dashboard against the current repository:
+
+```sh
+npx devframe-git # dev server (live RPC over WebSocket)
+npx devframe-git --write # also enable staging / committing from the UI
+npx devframe-git build # static deploy → dist-static/
+npx devframe-git --port 4000
+```
+
+## Programmatic
+
+`createGitDevframe(options)` returns a devframe definition you can mount into
+any host with devframe's adapters, or drive yourself.
+
+```ts
+import { createGitDevframe } from '@devframes/plugin-git'
+import { createCli } from 'devframe/adapters/cli'
+
+await createCli(createGitDevframe({ repoRoot: process.cwd() })).parse()
+```
+
+| Option | Default | Description |
+|--------|---------|-------------|
+| `repoRoot` | the devframe `cwd` | Repository directory to inspect. |
+| `basePath` | adapter-resolved | Mount path (`/` standalone, `/__git/` hosted). |
+| `distDir` | bundled SPA | Override the served SPA directory. |
+| `port` | `9710` | Preferred dev-server port. |
+| `write` | `false` | Enable staging, unstaging, and committing from the UI. |
+
+## RPC surface
+
+The read functions are each a `query` with `snapshot: true`: resolved live over
+WebSocket in dev, and served from a snapshot baked at build time for static
+deploys. Each degrades to an empty, `isRepo: false` result outside a git
+repository.
+
+- `git:status` — branch, upstream tracking (ahead/behind), staged / unstaged /
+ untracked files, parsed from `git status --porcelain=v2`. Reports `canWrite`.
+- `git:log` — paginated commit history (`limit` / `skip`) including parent
+ hashes, which drive the commit graph.
+- `git:branches` — local branches with SHA, upstream, ahead/behind, tip subject.
+- `git:diff` — per-file added/deleted counts for the working tree or index, plus
+ a unified patch for a selected file.
+
+Write actions are `action` functions, registered only when write mode is enabled
+(`createGitDevframe({ write: true })` or the `--write` flag) and gated behind
+`status.canWrite` in the UI. Each returns fresh status (commit returns a result):
+
+- `git:stage` — `git add` the given paths.
+- `git:unstage` — `git restore --staged` the given paths.
+- `git:commit` — commit the staged changes with a message.
+
+## Develop
+
+```sh
+pnpm -C plugins/git dev # client (Next.js HMR) + RPC backend together
+pnpm -C plugins/git build # tsdown (node) + next build (SPA) → dist/
+```
+
+`pnpm dev` starts the Next.js dev server (with hot-reload) and the devframe
+RPC/WebSocket backend at the same time, then prints both URLs — open the UI one.
+The SPA connects to the backend over the WebSocket port carried in
+`NEXT_PUBLIC_DEVFRAME_WS`. Override ports with `PORT` (UI) and
+`DEVFRAME_GIT_PORT` (backend). Run a single side with `dev:client` or
+`dev:server`.
+
+The SPA is a standard shadcn/ui setup (Tailwind v4, `components/ui/*`). Three
+Next.js settings in `src/client/next.config.mjs` keep it portable: `output:
+'export'` (devframe owns the server), `assetPrefix: '.'` (relative assets so the
+same bundle works at any base), and `trailingSlash: true` (composes with
+devframe's static directory-with-index resolution).
diff --git a/plugins/git/bin.mjs b/plugins/git/bin.mjs
new file mode 100755
index 0000000..8287d1a
--- /dev/null
+++ b/plugins/git/bin.mjs
@@ -0,0 +1,3 @@
+#!/usr/bin/env node
+// Thin launcher for the published package — runs the compiled CLI entry.
+import './dist/cli.mjs'
diff --git a/plugins/git/package.json b/plugins/git/package.json
new file mode 100644
index 0000000..c6f1432
--- /dev/null
+++ b/plugins/git/package.json
@@ -0,0 +1,81 @@
+{
+ "name": "@devframes/plugin-git",
+ "type": "module",
+ "version": "0.5.2",
+ "description": "Git integration for devframe — a read-only repository dashboard (status, log, branches, diff) with a Next.js + shadcn/ui SPA over type-safe RPC.",
+ "author": "Anthony Fu ",
+ "license": "MIT",
+ "homepage": "https://github.com/devframes/devframe#readme",
+ "repository": {
+ "directory": "plugins/git",
+ "type": "git",
+ "url": "git+https://github.com/devframes/devframe.git"
+ },
+ "bugs": "https://github.com/devframes/devframe/issues",
+ "keywords": [
+ "devtools",
+ "devframe",
+ "git",
+ "dashboard",
+ "plugin"
+ ],
+ "sideEffects": false,
+ "exports": {
+ ".": "./dist/index.mjs",
+ "./package.json": "./package.json"
+ },
+ "types": "./dist/index.d.mts",
+ "bin": {
+ "devframe-git": "./bin.mjs"
+ },
+ "files": [
+ "bin.mjs",
+ "dist"
+ ],
+ "scripts": {
+ "build": "tsdown && pnpm run build:spa",
+ "build:spa": "next build src/client && rm -rf dist/client && cp -r src/client/out dist/client",
+ "cli:build": "node bin.mjs build --out-dir dist/static",
+ "dev": "node scripts/dev.mjs",
+ "dev:server": "node src/cli.ts",
+ "dev:client": "next dev src/client",
+ "storybook": "storybook dev -p 6006",
+ "storybook:build": "storybook build -o storybook-static",
+ "watch": "tsdown --watch",
+ "typecheck": "tsc --noEmit",
+ "prepack": "pnpm run build",
+ "test": "vitest run"
+ },
+ "dependencies": {
+ "devframe": "workspace:*",
+ "pathe": "catalog:deps"
+ },
+ "devDependencies": {
+ "@radix-ui/react-scroll-area": "catalog:frontend",
+ "@radix-ui/react-separator": "catalog:frontend",
+ "@radix-ui/react-slot": "catalog:frontend",
+ "@radix-ui/react-tabs": "catalog:frontend",
+ "@storybook/addon-a11y": "catalog:frontend",
+ "@storybook/addon-docs": "catalog:frontend",
+ "@storybook/react-vite": "catalog:frontend",
+ "@tailwindcss/postcss": "catalog:frontend",
+ "@tailwindcss/vite": "catalog:frontend",
+ "@types/react": "catalog:types",
+ "@types/react-dom": "catalog:types",
+ "class-variance-authority": "catalog:frontend",
+ "clsx": "catalog:frontend",
+ "get-port-please": "catalog:deps",
+ "h3": "catalog:deps",
+ "lucide-react": "catalog:frontend",
+ "next": "catalog:frontend",
+ "react": "catalog:frontend",
+ "react-dom": "catalog:frontend",
+ "storybook": "catalog:frontend",
+ "tailwind-merge": "catalog:frontend",
+ "tailwindcss": "catalog:frontend",
+ "tsdown": "catalog:build",
+ "tw-animate-css": "catalog:frontend",
+ "vitest": "catalog:testing",
+ "ws": "catalog:deps"
+ }
+}
diff --git a/plugins/git/scripts/dev.mjs b/plugins/git/scripts/dev.mjs
new file mode 100644
index 0000000..880a136
--- /dev/null
+++ b/plugins/git/scripts/dev.mjs
@@ -0,0 +1,55 @@
+// Runs the devframe RPC/WebSocket backend and the Next.js dev server (with
+// HMR) together. Open the UI URL printed below; the client connects to the
+// backend over WebSocket via NEXT_PUBLIC_DEVFRAME_WS.
+import { spawn } from 'node:child_process'
+import { createRequire } from 'node:module'
+import process from 'node:process'
+import { fileURLToPath } from 'node:url'
+
+const require = createRequire(import.meta.url)
+const root = fileURLToPath(new URL('..', import.meta.url))
+const host = process.env.HOST ?? '0.0.0.0'
+const serverPort = process.env.DEVFRAME_GIT_PORT ?? '9710'
+const clientPort = process.env.PORT ?? '3000'
+// Resolve the Next.js bin explicitly so this works regardless of PATH.
+const nextBin = require.resolve('next/dist/bin/next')
+
+const children = [
+ // RPC + WebSocket backend (devframe). Serves the prebuilt SPA too, but in
+ // dev you open the Next server below for hot-reloading.
+ spawn(process.execPath, ['src/cli.ts', '--port', serverPort, '--host', host], {
+ cwd: root,
+ stdio: 'inherit',
+ env: process.env,
+ }),
+ // Next.js dev server (HMR). Points the client at the backend WebSocket.
+ spawn(process.execPath, [nextBin, 'dev', 'src/client', '--port', clientPort, '--hostname', host], {
+ cwd: root,
+ stdio: 'inherit',
+ env: { ...process.env, NEXT_PUBLIC_DEVFRAME_WS: serverPort },
+ }),
+]
+
+console.error(`\n @devframes/plugin-git dev`)
+console.error(` UI (HMR): http://localhost:${clientPort}`)
+console.error(` RPC backend: http://localhost:${serverPort}\n`)
+
+let shuttingDown = false
+function shutdown(code = 0) {
+ if (shuttingDown)
+ return
+ shuttingDown = true
+ for (const child of children)
+ child.kill('SIGTERM')
+ process.exit(code)
+}
+
+process.on('SIGINT', () => shutdown(0))
+process.on('SIGTERM', () => shutdown(0))
+for (const child of children) {
+ child.on('exit', code => shutdown(code ?? 0))
+ child.on('error', (error) => {
+ console.error(error)
+ shutdown(1)
+ })
+}
diff --git a/plugins/git/src/cli.ts b/plugins/git/src/cli.ts
new file mode 100644
index 0000000..57bd383
--- /dev/null
+++ b/plugins/git/src/cli.ts
@@ -0,0 +1,16 @@
+import process from 'node:process'
+import { createCli } from 'devframe/adapters/cli'
+import { createGitDevframe } from './index.ts'
+
+const cli = createCli(createGitDevframe(), {
+ onReady({ origin }) {
+ // devframe is headless by default — print our own ready banner so the
+ // dev server doesn't look like it silently did nothing.
+ console.error(`\n @devframes/plugin-git ready at ${origin}\n`)
+ },
+})
+
+cli.parse().catch((error) => {
+ console.error(error)
+ process.exit(1)
+})
diff --git a/plugins/git/src/client/app/globals.css b/plugins/git/src/client/app/globals.css
new file mode 100644
index 0000000..cfb0152
--- /dev/null
+++ b/plugins/git/src/client/app/globals.css
@@ -0,0 +1,96 @@
+@import "tailwindcss";
+@import "tw-animate-css";
+
+/* Explicit source globs so class detection covers app/ and components/. */
+@source "../**/*.{ts,tsx}";
+
+@custom-variant dark (&:is(.dark *));
+
+:root {
+ --radius: 0.625rem;
+ --background: oklch(1 0 0);
+ --foreground: oklch(0.145 0 0);
+ --card: oklch(1 0 0);
+ --card-foreground: oklch(0.145 0 0);
+ --popover: oklch(1 0 0);
+ --popover-foreground: oklch(0.145 0 0);
+ --primary: oklch(0.205 0 0);
+ --primary-foreground: oklch(0.985 0 0);
+ --secondary: oklch(0.97 0 0);
+ --secondary-foreground: oklch(0.205 0 0);
+ --muted: oklch(0.97 0 0);
+ --muted-foreground: oklch(0.556 0 0);
+ --accent: oklch(0.97 0 0);
+ --accent-foreground: oklch(0.205 0 0);
+ --destructive: oklch(0.577 0.245 27.325);
+ --border: oklch(0.922 0 0);
+ --input: oklch(0.922 0 0);
+ --ring: oklch(0.708 0 0);
+ --success: oklch(0.6 0.13 160);
+ --warning: oklch(0.75 0.15 80);
+}
+
+.dark {
+ --background: oklch(0.145 0 0);
+ --foreground: oklch(0.985 0 0);
+ --card: oklch(0.205 0 0);
+ --card-foreground: oklch(0.985 0 0);
+ --popover: oklch(0.205 0 0);
+ --popover-foreground: oklch(0.985 0 0);
+ --primary: oklch(0.922 0 0);
+ --primary-foreground: oklch(0.205 0 0);
+ --secondary: oklch(0.269 0 0);
+ --secondary-foreground: oklch(0.985 0 0);
+ --muted: oklch(0.269 0 0);
+ --muted-foreground: oklch(0.708 0 0);
+ --accent: oklch(0.269 0 0);
+ --accent-foreground: oklch(0.985 0 0);
+ --destructive: oklch(0.704 0.191 22.216);
+ --border: oklch(1 0 0 / 10%);
+ --input: oklch(1 0 0 / 15%);
+ --ring: oklch(0.556 0 0);
+ --success: oklch(0.7 0.14 160);
+ --warning: oklch(0.8 0.15 80);
+}
+
+@theme inline {
+ --radius-sm: calc(var(--radius) - 4px);
+ --radius-md: calc(var(--radius) - 2px);
+ --radius-lg: var(--radius);
+ --radius-xl: calc(var(--radius) + 4px);
+ --color-background: var(--background);
+ --color-foreground: var(--foreground);
+ --color-card: var(--card);
+ --color-card-foreground: var(--card-foreground);
+ --color-popover: var(--popover);
+ --color-popover-foreground: var(--popover-foreground);
+ --color-primary: var(--primary);
+ --color-primary-foreground: var(--primary-foreground);
+ --color-secondary: var(--secondary);
+ --color-secondary-foreground: var(--secondary-foreground);
+ --color-muted: var(--muted);
+ --color-muted-foreground: var(--muted-foreground);
+ --color-accent: var(--accent);
+ --color-accent-foreground: var(--accent-foreground);
+ --color-destructive: var(--destructive);
+ --color-border: var(--border);
+ --color-input: var(--input);
+ --color-ring: var(--ring);
+ --color-success: var(--success);
+ --color-warning: var(--warning);
+ --font-mono: ui-monospace, SFMono-Regular, "Cascadia Code", "Menlo", monospace;
+}
+
+@layer base {
+ * {
+ border-color: var(--color-border);
+ outline-color: color-mix(in oklab, var(--color-ring) 50%, transparent);
+ }
+
+ body {
+ background-color: var(--color-background);
+ color: var(--color-foreground);
+ font-family: system-ui, -apple-system, "Segoe UI", sans-serif;
+ -webkit-font-smoothing: antialiased;
+ }
+}
diff --git a/plugins/git/src/client/app/layout.tsx b/plugins/git/src/client/app/layout.tsx
new file mode 100644
index 0000000..012843d
--- /dev/null
+++ b/plugins/git/src/client/app/layout.tsx
@@ -0,0 +1,22 @@
+import type { Metadata } from 'next'
+import type { ReactNode } from 'react'
+import './globals.css'
+
+export const metadata: Metadata = {
+ title: 'Git Dashboard',
+ description: 'A devframe Git integration with a Next.js App Router + shadcn/ui SPA.',
+}
+
+// Set the theme class before paint to avoid a flash of the wrong theme.
+const themeScript = `(function(){try{var k='devframe-git-theme';var t=localStorage.getItem(k);if(t!=='light'&&t!=='dark'){t=window.matchMedia('(prefers-color-scheme: dark)').matches?'dark':'light';}if(t==='dark')document.documentElement.classList.add('dark');}catch(e){document.documentElement.classList.add('dark');}})();`
+
+export default function RootLayout({ children }: { children: ReactNode }) {
+ return (
+
+
+
+
+ {children}
+
+ )
+}
diff --git a/plugins/git/src/client/app/page.tsx b/plugins/git/src/client/app/page.tsx
new file mode 100644
index 0000000..e37d578
--- /dev/null
+++ b/plugins/git/src/client/app/page.tsx
@@ -0,0 +1,5 @@
+import { Dashboard } from '../components/dashboard'
+
+export default function Page() {
+ return
+}
diff --git a/plugins/git/src/client/components/branches-panel.tsx b/plugins/git/src/client/components/branches-panel.tsx
new file mode 100644
index 0000000..d6bb8a6
--- /dev/null
+++ b/plugins/git/src/client/components/branches-panel.tsx
@@ -0,0 +1,13 @@
+'use client'
+
+import type { DevframeRpcClient } from 'devframe/client'
+import { useCallback } from 'react'
+import { useRpcResource } from './use-rpc-resource'
+import { BranchesPanelView } from './views/branches-panel-view'
+
+export function BranchesPanel() {
+ const loader = useCallback((rpc: DevframeRpcClient) => rpc.call('git:branches'), [])
+ const { data, loading, refresh } = useRpcResource(loader)
+
+ return
+}
diff --git a/plugins/git/src/client/components/dashboard.tsx b/plugins/git/src/client/components/dashboard.tsx
new file mode 100644
index 0000000..8f16fbb
--- /dev/null
+++ b/plugins/git/src/client/components/dashboard.tsx
@@ -0,0 +1,90 @@
+'use client'
+
+import { FileDiff, GitBranch, GitCommitHorizontal, GitGraph, ListTree, Moon, Sun } from 'lucide-react'
+import { BranchesPanel } from './branches-panel'
+import { DiffPanel } from './diff-panel'
+import { LogPanel } from './log-panel'
+import { RpcProvider, useRpc } from './rpc-provider'
+import { StatusPanel } from './status-panel'
+import { useTheme } from './theme'
+import { Badge } from './ui/badge'
+import { Button } from './ui/button'
+import { Card, CardContent } from './ui/card'
+import { Tabs, TabsContent, TabsList, TabsTrigger } from './ui/tabs'
+
+function ConnectionBadge() {
+ const { rpc, error } = useRpc()
+ if (error)
+ return disconnected
+ if (!rpc)
+ return connecting…
+ const backend = rpc.connectionMeta.backend
+ return (
+
+ {backend === 'websocket' ? 'live' : 'static'}
+
+ )
+}
+
+function ThemeToggle() {
+ const { theme, toggle } = useTheme()
+ return (
+
+ )
+}
+
+export function Dashboard() {
+ return (
+
+
+
+
+
+
+
Git Dashboard
+
+ devframe + Next.js · type-safe RPC into the host repository
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Status
+
+
+
+ Commits
+
+
+
+ Branches
+
+
+
+ Diff
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ )
+}
diff --git a/plugins/git/src/client/components/diff-panel.tsx b/plugins/git/src/client/components/diff-panel.tsx
new file mode 100644
index 0000000..c3d9a90
--- /dev/null
+++ b/plugins/git/src/client/components/diff-panel.tsx
@@ -0,0 +1,50 @@
+'use client'
+
+import type { DevframeRpcClient } from 'devframe/client'
+import { useCallback, useState } from 'react'
+import { useRpcResource } from './use-rpc-resource'
+import { DiffPanelView, DiffPatchView } from './views/diff-panel-view'
+
+function PatchViewer({ staged, path }: { staged: boolean, path: string }) {
+ const loader = useCallback(
+ (rpc: DevframeRpcClient) => rpc.call('git:diff', { staged, path }),
+ [staged, path],
+ )
+ const { data, loading } = useRpcResource(loader)
+ return (
+
+ )
+}
+
+export function DiffPanel() {
+ const [staged, setStaged] = useState(false)
+ const [selected, setSelected] = useState(null)
+
+ const loader = useCallback(
+ (rpc: DevframeRpcClient) => rpc.call('git:diff', { staged }),
+ [staged],
+ )
+ const { data, loading, refresh } = useRpcResource(loader)
+
+ const selectScope = useCallback((value: boolean) => {
+ setStaged(value)
+ setSelected(null)
+ }, [])
+
+ return (
+ : null}
+ />
+ )
+}
diff --git a/plugins/git/src/client/components/log-panel.tsx b/plugins/git/src/client/components/log-panel.tsx
new file mode 100644
index 0000000..8564ac8
--- /dev/null
+++ b/plugins/git/src/client/components/log-panel.tsx
@@ -0,0 +1,98 @@
+'use client'
+
+import type { DevframeRpcClient } from 'devframe/client'
+import type { Commit } from '../../index'
+import { useCallback, useEffect, useRef, useState } from 'react'
+import { useRpc } from './rpc-provider'
+import { LogPanelView } from './views/log-panel-view'
+
+const PAGE = 30
+
+export function LogPanel() {
+ const { rpc } = useRpc()
+ const [isRepo, setIsRepo] = useState(null)
+ const [commits, setCommits] = useState([])
+ const [skip, setSkip] = useState(0)
+ const [hasMore, setHasMore] = useState(false)
+ const [loading, setLoading] = useState(false)
+ const [error, setError] = useState(null)
+ const commitsRef = useRef([])
+
+ useEffect(() => {
+ commitsRef.current = commits
+ }, [commits])
+
+ const loadPage = useCallback(async (
+ client: DevframeRpcClient,
+ nextSkip: number,
+ mode: 'replace' | 'append',
+ ) => {
+ setLoading(true)
+ setError(null)
+ try {
+ const page = await client.call('git:log', { limit: PAGE, skip: nextSkip })
+ setIsRepo(page.isRepo)
+ if (mode === 'replace') {
+ setCommits(page.commits)
+ setSkip(page.commits.length)
+ }
+ else {
+ const seen = new Set(commitsRef.current.map(c => c.hash))
+ const unique = page.commits.filter((c) => {
+ if (seen.has(c.hash))
+ return false
+ seen.add(c.hash)
+ return true
+ })
+ if (unique.length === 0) {
+ // Static fallback snapshots can return the same page for any args.
+ setHasMore(false)
+ return
+ }
+ setCommits(prev => [...prev, ...unique])
+ setSkip(prev => prev + unique.length)
+ }
+ setHasMore(page.hasMore)
+ }
+ catch (e) {
+ setError(e instanceof Error ? e.message : String(e))
+ }
+ finally {
+ setLoading(false)
+ }
+ }, [])
+
+ useEffect(() => {
+ if (!rpc)
+ return
+ void loadPage(rpc, 0, 'replace')
+ }, [rpc, loadPage])
+
+ const refresh = useCallback(async () => {
+ if (!rpc)
+ return
+ await loadPage(rpc, 0, 'replace')
+ }, [rpc, loadPage])
+
+ const loadMore = useCallback(async () => {
+ if (!rpc)
+ return
+ await loadPage(rpc, skip, 'append')
+ }, [rpc, skip, loadPage])
+
+ const liveBackend = rpc?.connectionMeta.backend === 'websocket'
+
+ return (
+
+ )
+}
diff --git a/plugins/git/src/client/components/rpc-provider.tsx b/plugins/git/src/client/components/rpc-provider.tsx
new file mode 100644
index 0000000..22b7db8
--- /dev/null
+++ b/plugins/git/src/client/components/rpc-provider.tsx
@@ -0,0 +1,52 @@
+'use client'
+
+import type { DevframeRpcClient } from 'devframe/client'
+import type { ReactNode } from 'react'
+import { connectDevframe } from 'devframe/client'
+import { createContext, use, useEffect, useState } from 'react'
+
+interface ConnectionState {
+ rpc: DevframeRpcClient | null
+ error: string | null
+}
+
+const RpcContext = createContext({ rpc: null, error: null })
+
+export function useRpc(): ConnectionState {
+ return use(RpcContext)
+}
+
+export function RpcProvider({ children }: { children: ReactNode }) {
+ const [state, setState] = useState({ rpc: null, error: null })
+
+ useEffect(() => {
+ let cancelled = false
+ // In combined dev (`pnpm dev`) the SPA is served by Next for HMR while
+ // the RPC backend runs as a separate devframe server. This env var points
+ // the client straight at that WebSocket; unset in production, where the
+ // client discovers `./__connection.json` next to index.html.
+ // Next.js statically inlines `process.env.NEXT_PUBLIC_*` at build time.
+ // eslint-disable-next-line node/prefer-global/process
+ const devWs = process.env.NEXT_PUBLIC_DEVFRAME_WS
+ const options = devWs
+ ? { connectionMeta: { backend: 'websocket' as const, websocket: devWs } }
+ : undefined
+ connectDevframe(options).then(
+ (rpc) => {
+ if (!cancelled)
+ setState({ rpc, error: null })
+ },
+ (err: unknown) => {
+ if (cancelled)
+ return
+ const message = err instanceof Error ? err.message : String(err)
+ setState({ rpc: null, error: message })
+ },
+ )
+ return () => {
+ cancelled = true
+ }
+ }, [])
+
+ return {children}
+}
diff --git a/plugins/git/src/client/components/status-panel.tsx b/plugins/git/src/client/components/status-panel.tsx
new file mode 100644
index 0000000..40723da
--- /dev/null
+++ b/plugins/git/src/client/components/status-panel.tsx
@@ -0,0 +1,78 @@
+'use client'
+
+import type { DevframeRpcClient } from 'devframe/client'
+import { useCallback, useState } from 'react'
+import { useRpc } from './rpc-provider'
+import { useRpcResource } from './use-rpc-resource'
+import { StatusPanelView } from './views/status-panel-view'
+
+export function StatusPanel() {
+ const { rpc } = useRpc()
+ const loader = useCallback((r: DevframeRpcClient) => r.call('git:status'), [])
+ const { data, loading, refresh, setData } = useRpcResource(loader)
+ const [busy, setBusy] = useState(false)
+ const [message, setMessage] = useState('')
+ const [note, setNote] = useState(null)
+
+ const canWrite = !!data?.canWrite && rpc?.connectionMeta.backend === 'websocket'
+
+ const stage = useCallback(async (paths: string[]) => {
+ if (!rpc || paths.length === 0)
+ return
+ setBusy(true)
+ setNote(null)
+ try {
+ setData(await rpc.call('git:stage', { paths }))
+ }
+ finally {
+ setBusy(false)
+ }
+ }, [rpc, setData])
+
+ const unstage = useCallback(async (paths: string[]) => {
+ if (!rpc || paths.length === 0)
+ return
+ setBusy(true)
+ setNote(null)
+ try {
+ setData(await rpc.call('git:unstage', { paths }))
+ }
+ finally {
+ setBusy(false)
+ }
+ }, [rpc, setData])
+
+ const commit = useCallback(async () => {
+ if (!rpc)
+ return
+ setBusy(true)
+ setNote(null)
+ try {
+ const result = await rpc.call('git:commit', { message })
+ setData(result.status)
+ if (result.ok)
+ setMessage('')
+ else
+ setNote(result.message)
+ }
+ finally {
+ setBusy(false)
+ }
+ }, [rpc, message, setData])
+
+ return (
+
+ )
+}
diff --git a/plugins/git/src/client/components/theme.ts b/plugins/git/src/client/components/theme.ts
new file mode 100644
index 0000000..39f6fa8
--- /dev/null
+++ b/plugins/git/src/client/components/theme.ts
@@ -0,0 +1,51 @@
+'use client'
+
+import { useEffect, useState } from 'react'
+
+export type Theme = 'light' | 'dark'
+
+const STORAGE_KEY = 'devframe-git-theme'
+
+function readStored(): Theme | null {
+ try {
+ const value = localStorage.getItem(STORAGE_KEY)
+ return value === 'light' || value === 'dark' ? value : null
+ }
+ catch {
+ return null
+ }
+}
+
+function systemTheme(): Theme {
+ return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'
+}
+
+/**
+ * Theme state synced to the `.dark` class on `` and persisted to
+ * localStorage. The initial class is set by an inline script in the document
+ * head (see `layout.tsx`) to avoid a flash; this hook keeps React in step.
+ */
+export function useTheme() {
+ const [theme, setTheme] = useState('dark')
+
+ useEffect(() => {
+ setTheme(readStored() ?? systemTheme())
+ }, [])
+
+ useEffect(() => {
+ document.documentElement.classList.toggle('dark', theme === 'dark')
+ }, [theme])
+
+ function toggle() {
+ setTheme((current) => {
+ const next: Theme = current === 'dark' ? 'light' : 'dark'
+ try {
+ localStorage.setItem(STORAGE_KEY, next)
+ }
+ catch {}
+ return next
+ })
+ }
+
+ return { theme, toggle }
+}
diff --git a/plugins/git/src/client/components/ui/badge.tsx b/plugins/git/src/client/components/ui/badge.tsx
new file mode 100644
index 0000000..c641ba0
--- /dev/null
+++ b/plugins/git/src/client/components/ui/badge.tsx
@@ -0,0 +1,42 @@
+import type { VariantProps } from 'class-variance-authority'
+import { Slot } from '@radix-ui/react-slot'
+import { cva } from 'class-variance-authority'
+import * as React from 'react'
+import { cn } from '../../lib/utils'
+
+const badgeVariants = cva(
+ 'inline-flex items-center justify-center rounded-md border px-2 py-0.5 text-xs font-medium w-fit whitespace-nowrap shrink-0 gap-1 [&>svg]:size-3 [&>svg]:pointer-events-none transition-[color,box-shadow] overflow-hidden',
+ {
+ variants: {
+ variant: {
+ default: 'border-transparent bg-primary text-primary-foreground',
+ secondary: 'border-transparent bg-secondary text-secondary-foreground',
+ destructive: 'border-transparent bg-destructive text-white',
+ outline: 'text-foreground',
+ success: 'border-transparent bg-success/15 text-success',
+ warning: 'border-transparent bg-warning/15 text-warning',
+ },
+ },
+ defaultVariants: {
+ variant: 'default',
+ },
+ },
+)
+
+function Badge({
+ className,
+ variant,
+ asChild = false,
+ ...props
+}: React.ComponentProps<'span'> & VariantProps & { asChild?: boolean }) {
+ const Comp = asChild ? Slot : 'span'
+ return (
+
+ )
+}
+
+export { Badge, badgeVariants }
diff --git a/plugins/git/src/client/components/ui/button.tsx b/plugins/git/src/client/components/ui/button.tsx
new file mode 100644
index 0000000..128ce76
--- /dev/null
+++ b/plugins/git/src/client/components/ui/button.tsx
@@ -0,0 +1,50 @@
+import type { VariantProps } from 'class-variance-authority'
+import { Slot } from '@radix-ui/react-slot'
+import { cva } from 'class-variance-authority'
+import * as React from 'react'
+import { cn } from '../../lib/utils'
+
+const buttonVariants = cva(
+ 'inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-all disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*=\'size-\'])]:size-4 [&_svg]:shrink-0 shrink-0 outline-none focus-visible:ring-[3px] focus-visible:ring-ring/50 cursor-pointer',
+ {
+ variants: {
+ variant: {
+ default: 'bg-primary text-primary-foreground shadow-xs hover:bg-primary/90',
+ destructive: 'bg-destructive text-white shadow-xs hover:bg-destructive/90',
+ outline: 'border bg-background shadow-xs hover:bg-accent hover:text-accent-foreground',
+ secondary: 'bg-secondary text-secondary-foreground shadow-xs hover:bg-secondary/80',
+ ghost: 'hover:bg-accent hover:text-accent-foreground',
+ link: 'text-primary underline-offset-4 hover:underline',
+ },
+ size: {
+ default: 'h-9 px-4 py-2 has-[>svg]:px-3',
+ sm: 'h-8 gap-1.5 rounded-md px-3 has-[>svg]:px-2.5',
+ lg: 'h-10 rounded-md px-6 has-[>svg]:px-4',
+ icon: 'size-9',
+ },
+ },
+ defaultVariants: {
+ variant: 'default',
+ size: 'default',
+ },
+ },
+)
+
+function Button({
+ className,
+ variant,
+ size,
+ asChild = false,
+ ...props
+}: React.ComponentProps<'button'> & VariantProps & { asChild?: boolean }) {
+ const Comp = asChild ? Slot : 'button'
+ return (
+
+ )
+}
+
+export { Button, buttonVariants }
diff --git a/plugins/git/src/client/components/ui/card.tsx b/plugins/git/src/client/components/ui/card.tsx
new file mode 100644
index 0000000..1c2db0b
--- /dev/null
+++ b/plugins/git/src/client/components/ui/card.tsx
@@ -0,0 +1,74 @@
+import * as React from 'react'
+import { cn } from '../../lib/utils'
+
+function Card({ className, ...props }: React.ComponentProps<'div'>) {
+ return (
+
+ )
+}
+
+function CardHeader({ className, ...props }: React.ComponentProps<'div'>) {
+ return (
+
+ )
+}
+
+function CardTitle({ className, ...props }: React.ComponentProps<'div'>) {
+ return (
+
+ )
+}
+
+function CardDescription({ className, ...props }: React.ComponentProps<'div'>) {
+ return (
+
+ )
+}
+
+function CardAction({ className, ...props }: React.ComponentProps<'div'>) {
+ return (
+
+ )
+}
+
+function CardContent({ className, ...props }: React.ComponentProps<'div'>) {
+ return (
+
+ )
+}
+
+function CardFooter({ className, ...props }: React.ComponentProps<'div'>) {
+ return (
+
+ )
+}
+
+export { Card, CardAction, CardContent, CardDescription, CardFooter, CardHeader, CardTitle }
diff --git a/plugins/git/src/client/components/ui/scroll-area.tsx b/plugins/git/src/client/components/ui/scroll-area.tsx
new file mode 100644
index 0000000..b90ee69
--- /dev/null
+++ b/plugins/git/src/client/components/ui/scroll-area.tsx
@@ -0,0 +1,55 @@
+'use client'
+
+import * as ScrollAreaPrimitive from '@radix-ui/react-scroll-area'
+import * as React from 'react'
+import { cn } from '../../lib/utils'
+
+function ScrollArea({
+ className,
+ children,
+ ...props
+}: React.ComponentProps) {
+ return (
+
+
+ {children}
+
+
+
+
+ )
+}
+
+function ScrollBar({
+ className,
+ orientation = 'vertical',
+ ...props
+}: React.ComponentProps) {
+ return (
+
+
+
+ )
+}
+
+export { ScrollArea, ScrollBar }
diff --git a/plugins/git/src/client/components/ui/separator.tsx b/plugins/git/src/client/components/ui/separator.tsx
new file mode 100644
index 0000000..c71c6f5
--- /dev/null
+++ b/plugins/git/src/client/components/ui/separator.tsx
@@ -0,0 +1,27 @@
+'use client'
+
+import * as SeparatorPrimitive from '@radix-ui/react-separator'
+import * as React from 'react'
+import { cn } from '../../lib/utils'
+
+function Separator({
+ className,
+ orientation = 'horizontal',
+ decorative = true,
+ ...props
+}: React.ComponentProps) {
+ return (
+
+ )
+}
+
+export { Separator }
diff --git a/plugins/git/src/client/components/ui/skeleton.tsx b/plugins/git/src/client/components/ui/skeleton.tsx
new file mode 100644
index 0000000..b26efd2
--- /dev/null
+++ b/plugins/git/src/client/components/ui/skeleton.tsx
@@ -0,0 +1,14 @@
+import type * as React from 'react'
+import { cn } from '../../lib/utils'
+
+function Skeleton({ className, ...props }: React.ComponentProps<'div'>) {
+ return (
+
+ )
+}
+
+export { Skeleton }
diff --git a/plugins/git/src/client/components/ui/tabs.tsx b/plugins/git/src/client/components/ui/tabs.tsx
new file mode 100644
index 0000000..ed5db4c
--- /dev/null
+++ b/plugins/git/src/client/components/ui/tabs.tsx
@@ -0,0 +1,47 @@
+'use client'
+
+import * as TabsPrimitive from '@radix-ui/react-tabs'
+import * as React from 'react'
+import { cn } from '../../lib/utils'
+
+function Tabs({ className, ...props }: React.ComponentProps) {
+ return (
+
+ )
+}
+
+function TabsList({ className, ...props }: React.ComponentProps) {
+ return (
+
+ )
+}
+
+function TabsTrigger({ className, ...props }: React.ComponentProps) {
+ return (
+
+ )
+}
+
+function TabsContent({ className, ...props }: React.ComponentProps) {
+ return (
+
+ )
+}
+
+export { Tabs, TabsContent, TabsList, TabsTrigger }
diff --git a/plugins/git/src/client/components/ui/textarea.tsx b/plugins/git/src/client/components/ui/textarea.tsx
new file mode 100644
index 0000000..844e96a
--- /dev/null
+++ b/plugins/git/src/client/components/ui/textarea.tsx
@@ -0,0 +1,17 @@
+import type * as React from 'react'
+import { cn } from '../../lib/utils'
+
+function Textarea({ className, ...props }: React.ComponentProps<'textarea'>) {
+ return (
+
+ )
+}
+
+export { Textarea }
diff --git a/plugins/git/src/client/components/use-rpc-resource.ts b/plugins/git/src/client/components/use-rpc-resource.ts
new file mode 100644
index 0000000..f2f19b6
--- /dev/null
+++ b/plugins/git/src/client/components/use-rpc-resource.ts
@@ -0,0 +1,49 @@
+'use client'
+
+import type { DevframeRpcClient } from 'devframe/client'
+import type { Dispatch, SetStateAction } from 'react'
+import { useCallback, useEffect, useState } from 'react'
+import { useRpc } from './rpc-provider'
+
+export interface RpcResource {
+ data: T | null
+ loading: boolean
+ error: string | null
+ refresh: () => Promise
+ /** Replace the data directly — e.g. with the result of a mutation. */
+ setData: Dispatch>
+}
+
+/**
+ * Load a value from an RPC call, exposing `{ data, loading, error, refresh }`.
+ * `loader` must be stable — wrap it in `useCallback` at the call site so the
+ * effect re-runs only when its real inputs change.
+ */
+export function useRpcResource(loader: (rpc: DevframeRpcClient) => Promise): RpcResource {
+ const { rpc } = useRpc()
+ const [data, setData] = useState(null)
+ const [loading, setLoading] = useState(false)
+ const [error, setError] = useState(null)
+
+ const refresh = useCallback(async () => {
+ if (!rpc)
+ return
+ setLoading(true)
+ setError(null)
+ try {
+ setData(await loader(rpc))
+ }
+ catch (e) {
+ setError(e instanceof Error ? e.message : String(e))
+ }
+ finally {
+ setLoading(false)
+ }
+ }, [rpc, loader])
+
+ useEffect(() => {
+ void refresh()
+ }, [refresh])
+
+ return { data, loading, error, refresh, setData }
+}
diff --git a/plugins/git/src/client/components/views/branches-panel-view.tsx b/plugins/git/src/client/components/views/branches-panel-view.tsx
new file mode 100644
index 0000000..f9a568b
--- /dev/null
+++ b/plugins/git/src/client/components/views/branches-panel-view.tsx
@@ -0,0 +1,85 @@
+'use client'
+
+import type { Branch, GitBranches } from '../../../index'
+import { ArrowDown, ArrowUp, Check, GitBranch, RefreshCw } from 'lucide-react'
+import { Badge } from '../ui/badge'
+import { Button } from '../ui/button'
+import { ScrollArea } from '../ui/scroll-area'
+import { Skeleton } from '../ui/skeleton'
+
+export interface BranchesPanelViewProps {
+ data: GitBranches | null
+ loading: boolean
+ onRefresh: () => void | Promise
+}
+
+function BranchRow({ branch }: { branch: Branch }) {
+ return (
+
+
+
+
+
+ {branch.name}
+
+ {branch.current && (
+
+
+ current
+
+ )}
+ {branch.gone && upstream gone}
+
+ {branch.subject &&
{branch.subject}
}
+
+
+ {branch.ahead > 0 && (
+
+
+ {branch.ahead}
+
+ )}
+ {branch.behind > 0 && (
+
+
+ {branch.behind}
+
+ )}
+
{branch.sha}
+
+
+ )
+}
+
+export function BranchesPanelView({ data, loading, onRefresh }: BranchesPanelViewProps) {
+ return (
+
+
+
+ {data?.isRepo ? `${data.branches.length} branches` : ' '}
+
+
+
+
+ {!data && (
+
+ {Array.from({ length: 4 }).map((_, i) => )}
+
+ )}
+
+ {data && !data.isRepo && (
+
The working directory is not a git repository.
+ )}
+
+ {data?.isRepo && data.branches.length > 0 && (
+
+
+ {data.branches.map(branch => )}
+
+
+ )}
+
+ )
+}
diff --git a/plugins/git/src/client/components/views/diff-panel-view.tsx b/plugins/git/src/client/components/views/diff-panel-view.tsx
new file mode 100644
index 0000000..998df5f
--- /dev/null
+++ b/plugins/git/src/client/components/views/diff-panel-view.tsx
@@ -0,0 +1,159 @@
+'use client'
+
+import type { ReactNode } from 'react'
+import type { GitDiff } from '../../../index'
+import { RefreshCw } from 'lucide-react'
+import { cn } from '../../lib/utils'
+import { Badge } from '../ui/badge'
+import { Button } from '../ui/button'
+import { ScrollArea } from '../ui/scroll-area'
+import { Skeleton } from '../ui/skeleton'
+
+export interface DiffPanelViewProps {
+ data: GitDiff | null
+ loading: boolean
+ staged: boolean
+ selected: string | null
+ onSelectScope: (staged: boolean) => void
+ onSelectFile: (path: string) => void
+ onRefresh: () => void | Promise
+ /** Rendered below the file list when a file is selected (the patch viewer). */
+ patchSlot?: ReactNode
+}
+
+function patchLineClass(line: string): string {
+ if (line.startsWith('@@'))
+ return 'text-primary'
+ if (line.startsWith('+') && !line.startsWith('+++'))
+ return 'text-success'
+ if (line.startsWith('-') && !line.startsWith('---'))
+ return 'text-destructive'
+ if (line.startsWith('diff ') || line.startsWith('index ') || line.startsWith('+++') || line.startsWith('---'))
+ return 'text-muted-foreground font-semibold'
+ return 'text-foreground'
+}
+
+/** Pure renderer for a unified patch. */
+export function DiffPatchView({ patch, loading, truncated }: { patch: string | null, loading: boolean, truncated: boolean }) {
+ if (loading)
+ return
+ if (!patch)
+ return No textual diff available (binary or unchanged).
+ return (
+
+
+ {patch.split('\n').map((line, i) => (
+ {line || ' '}
+ ))}
+
+ {truncated && Patch truncated.
}
+
+ )
+}
+
+export function DiffPanelView(props: DiffPanelViewProps) {
+ const { data, loading, staged, selected, onSelectScope, onSelectFile, onRefresh, patchSlot } = props
+ return (
+
+
+
+ {([['Working tree', false], ['Staged', true]] as const).map(([label, value]) => (
+
+ ))}
+
+
+ {data?.isRepo && (
+
+
+ +
+ {data.totalAdditions}
+
+ {' '}
+
+ −
+ {data.totalDeletions}
+
+
+ )}
+
+
+
+
+ {!data && (
+
+ {Array.from({ length: 3 }).map((_, i) => )}
+
+ )}
+
+ {data && !data.isRepo && (
+
The working directory is not a git repository.
+ )}
+
+ {data?.isRepo && data.files.length === 0 && (
+
+ No
+ {staged ? ' staged' : ' unstaged'}
+ {' '}
+ changes.
+
+ )}
+
+ {data?.isRepo && data.files.length > 0 && (
+ <>
+
+
+ {data.files.map(file => (
+ -
+
+
+ ))}
+
+
+
+ {selected && (
+
+
{selected}
+ {patchSlot}
+
+ )}
+ >
+ )}
+
+ )
+}
diff --git a/plugins/git/src/client/components/views/log-panel-view.stories.tsx b/plugins/git/src/client/components/views/log-panel-view.stories.tsx
new file mode 100644
index 0000000..558d230
--- /dev/null
+++ b/plugins/git/src/client/components/views/log-panel-view.stories.tsx
@@ -0,0 +1,39 @@
+import type { Meta, StoryObj } from '@storybook/react-vite'
+import type { Commit } from '../../../index'
+import { LogPanelView } from './log-panel-view'
+
+const now = Date.now()
+
+const commits: Commit[] = [
+ { hash: 'a1b2c3d4e5f6', shortHash: 'a1b2c3d', parents: ['b2c3d4e', 'c3d4e5f'], author: 'Ada Lovelace', email: 'ada@example.dev', date: now - 36e5, subject: 'Merge branch feature/graph into main', body: '', refs: ['HEAD -> main'] },
+ { hash: 'b2c3d4e', shortHash: 'b2c3d4e', parents: ['d4e5f60'], author: 'Ada Lovelace', email: 'ada@example.dev', date: now - 72e5, subject: 'Tidy up dashboard layout', body: '', refs: [] },
+ { hash: 'c3d4e5f', shortHash: 'c3d4e5f', parents: ['d4e5f60'], author: 'Lin Chen', email: 'lin@example.dev', date: now - 9e6, subject: 'Add commit graph lanes', body: '', refs: ['origin/feature/graph'] },
+ { hash: 'd4e5f60', shortHash: 'd4e5f60', parents: [], author: 'Ada Lovelace', email: 'ada@example.dev', date: now - 9e8, subject: 'Initial commit', body: '', refs: ['v0.1.0'] },
+]
+
+const meta = {
+ title: 'Panels/Log',
+ component: LogPanelView,
+ args: {
+ rpcConnected: true,
+ isRepo: true,
+ commits,
+ hasMore: false,
+ loading: false,
+ error: null,
+ liveBackend: true,
+ onRefresh: () => undefined,
+ onLoadMore: () => undefined,
+ },
+} satisfies Meta
+
+export default meta
+type Story = StoryObj
+
+export const Default: Story = {}
+export const Connecting: Story = { args: { rpcConnected: false } }
+export const Empty: Story = { args: { commits: [] } }
+export const NotARepo: Story = { args: { isRepo: false, commits: [] } }
+export const HasMore: Story = { args: { hasMore: true } }
+export const StaticMode: Story = { args: { hasMore: true, liveBackend: false } }
+export const Error: Story = { args: { error: 'fatal: bad revision' } }
diff --git a/plugins/git/src/client/components/views/log-panel-view.tsx b/plugins/git/src/client/components/views/log-panel-view.tsx
new file mode 100644
index 0000000..f3144fc
--- /dev/null
+++ b/plugins/git/src/client/components/views/log-panel-view.tsx
@@ -0,0 +1,178 @@
+'use client'
+
+import type { Commit } from '../../../index'
+import type { GraphRow } from '../../lib/commit-graph'
+import { RefreshCw } from 'lucide-react'
+import { useMemo } from 'react'
+import { computeGraph } from '../../lib/commit-graph'
+import { Badge } from '../ui/badge'
+import { Button } from '../ui/button'
+import { ScrollArea } from '../ui/scroll-area'
+import { Skeleton } from '../ui/skeleton'
+
+const ROW_H = 42
+const COL_W = 12
+const NODE_R = 4
+
+export interface LogPanelViewProps {
+ rpcConnected: boolean
+ isRepo: boolean | null
+ commits: Commit[]
+ hasMore: boolean
+ loading: boolean
+ error: string | null
+ liveBackend: boolean
+ onRefresh: () => void | Promise
+ onLoadMore: () => void | Promise
+}
+
+function relativeTime(epoch: number): string {
+ const diff = Date.now() - epoch
+ const mins = Math.round(diff / 60000)
+ if (mins < 1)
+ return 'just now'
+ if (mins < 60)
+ return `${mins}m ago`
+ const hours = Math.round(mins / 60)
+ if (hours < 24)
+ return `${hours}h ago`
+ const days = Math.round(hours / 24)
+ if (days < 30)
+ return `${days}d ago`
+ return new Date(epoch).toLocaleDateString()
+}
+
+function cx(col: number): number {
+ return col * COL_W + COL_W / 2
+}
+
+function linkPath(fromCol: number, fromY: number, toCol: number, toY: number): string {
+ const x1 = cx(fromCol)
+ const x2 = cx(toCol)
+ if (fromCol === toCol)
+ return `M ${x1} ${fromY} L ${x2} ${toY}`
+ const midY = (fromY + toY) / 2
+ return `M ${x1} ${fromY} C ${x1} ${midY} ${x2} ${midY} ${x2} ${toY}`
+}
+
+function GraphCell({ row, width }: { row: GraphRow, width: number }) {
+ const mid = ROW_H / 2
+ return (
+
+ )
+}
+
+function CommitRow({ commit, row, gutter }: { commit: Commit, row: GraphRow, gutter: number }) {
+ return (
+
+
+
+
+ {commit.subject}
+ {commit.refs.map(ref => (
+ {ref}
+ ))}
+
+
+ {commit.shortHash}
+ {commit.author}
+ ·
+ {relativeTime(commit.date)}
+
+
+
+ )
+}
+
+export function LogPanelView(props: LogPanelViewProps) {
+ const {
+ rpcConnected,
+ isRepo,
+ commits,
+ hasMore,
+ loading,
+ error,
+ liveBackend,
+ onRefresh,
+ onLoadMore,
+ } = props
+
+ const graph = useMemo(
+ () => computeGraph(commits),
+ [commits],
+ )
+ const gutter = Math.max(graph.columns, 1) * COL_W + COL_W / 2
+
+ return (
+
+
+
+ {isRepo ? `${commits.length} commits` : ' '}
+
+
+
+
+ {!rpcConnected && (
+
+ {Array.from({ length: 5 }).map((_, i) => )}
+
+ )}
+
+ {error && (
+
{error}
+ )}
+
+ {isRepo === false && (
+
The working directory is not a git repository.
+ )}
+
+ {isRepo === true && commits.length === 0 && (
+
No commits yet.
+ )}
+
+ {isRepo === true && commits.length > 0 && (
+
+
+ {commits.map((commit, i) => (
+
+ ))}
+
+
+ )}
+
+ {isRepo === true && hasMore && (
+
+ )}
+
+ {isRepo === true && hasMore && !liveBackend && (
+
Load more is available in live mode.
+ )}
+
+ )
+}
diff --git a/plugins/git/src/client/components/views/status-panel-view.tsx b/plugins/git/src/client/components/views/status-panel-view.tsx
new file mode 100644
index 0000000..e5bccf0
--- /dev/null
+++ b/plugins/git/src/client/components/views/status-panel-view.tsx
@@ -0,0 +1,229 @@
+'use client'
+
+import type { ReactNode } from 'react'
+import type { FileStatusCode, GitStatus, StatusFileEntry } from '../../../index'
+import { ArrowDown, ArrowUp, Check, GitBranch, Minus, Plus, RefreshCw } from 'lucide-react'
+import { Badge } from '../ui/badge'
+import { Button } from '../ui/button'
+import { ScrollArea } from '../ui/scroll-area'
+import { Skeleton } from '../ui/skeleton'
+import { Textarea } from '../ui/textarea'
+
+export interface StatusPanelViewProps {
+ data: GitStatus | null
+ loading: boolean
+ busy: boolean
+ canWrite: boolean
+ message: string
+ note: string | null
+ onRefresh: () => void | Promise
+ onStage: (paths: string[]) => void | Promise
+ onUnstage: (paths: string[]) => void | Promise
+ onCommit: () => void | Promise
+ onMessageChange: (value: string) => void
+}
+
+const STATUS_LABEL: Record = {
+ 'modified': 'M',
+ 'added': 'A',
+ 'deleted': 'D',
+ 'renamed': 'R',
+ 'copied': 'C',
+ 'type-changed': 'T',
+ 'unmerged': 'U',
+ 'unknown': '?',
+}
+
+function statusColor(code: FileStatusCode): string {
+ switch (code) {
+ case 'added': return 'text-success'
+ case 'deleted': return 'text-destructive'
+ case 'modified': return 'text-warning'
+ case 'unmerged': return 'text-destructive'
+ default: return 'text-muted-foreground'
+ }
+}
+
+function FileRow({ entry, action }: { entry: StatusFileEntry, action?: ReactNode }) {
+ return (
+
+
+ {STATUS_LABEL[entry.status]}
+
+
+ {entry.from ? `${entry.from} → ${entry.path}` : entry.path}
+
+ {action}
+
+ )
+}
+
+function Section({
+ title,
+ count,
+ headerAction,
+ children,
+}: {
+ title: string
+ count: number
+ headerAction?: ReactNode
+ children: ReactNode
+}) {
+ if (count === 0)
+ return null
+ return (
+
+
+
+ {title}
+ {count}
+
+ {headerAction}
+
+
+
+ )
+}
+
+export function StatusPanelView(props: StatusPanelViewProps) {
+ const { data, loading, busy, canWrite, message, note, onRefresh, onStage, onUnstage, onCommit, onMessageChange } = props
+
+ const stageBtn = (paths: string[], label: string) => (
+
+ )
+ const unstageBtn = (paths: string[], label: string) => (
+
+ )
+
+ return (
+
+
+
+ {data?.isRepo
+ ? (
+ <>
+
+
+ {data.detached ? `detached @ ${data.head}` : data.branch}
+
+ {data.upstream && (
+
+ {data.upstream}
+ {data.ahead > 0 && (
+
+
+ {data.ahead}
+
+ )}
+ {data.behind > 0 && (
+
+
+ {data.behind}
+
+ )}
+
+ )}
+ {data.clean
+ ? (
+
+
+ clean
+
+ )
+ :
working tree dirty}
+ >
+ )
+ :
}
+
+
+
+
+ {data && !data.isRepo && (
+
The working directory is not a git repository.
+ )}
+
+ {data?.isRepo && data.clean && (
+
Nothing to commit — the working tree is clean.
+ )}
+
+ {data?.isRepo && !data.clean && (
+ <>
+
+
+ 0
+ ?
+ : undefined}
+ >
+ {data.staged.map(entry => (
+
+ ))}
+
+
+ 0
+ ?
+ : undefined}
+ >
+ {data.unstaged.map(entry => (
+
+ ))}
+
+
+ 0
+ ?
+ : undefined}
+ >
+ {data.untracked.map(path => (
+
+ ))}
+
+
+
+
+ {canWrite && data.staged.length > 0 && (
+
+ )}
+ >
+ )}
+
+ )
+}
diff --git a/plugins/git/src/client/lib/commit-graph.ts b/plugins/git/src/client/lib/commit-graph.ts
new file mode 100644
index 0000000..64b0aee
--- /dev/null
+++ b/plugins/git/src/client/lib/commit-graph.ts
@@ -0,0 +1,142 @@
+// Lane-assignment for a commit graph, à la SourceTree / GitHub Desktop.
+// Commits are processed top-to-bottom (newest first, --topo-order); lanes
+// track the next commit each column is heading toward. Columns are stable
+// (no compaction), so vertical lines stay put and branches/merges read clearly.
+
+export interface GraphInput {
+ hash: string
+ parents: string[]
+}
+
+export interface GraphLink {
+ /** Column at the top of the half. */
+ from: number
+ /** Column at the bottom of the half. */
+ to: number
+ color: string
+}
+
+export interface GraphRow {
+ /** Column of this commit's node. */
+ col: number
+ /** Node color. */
+ color: string
+ /** Connectors in the top half (row top → node center). */
+ topLinks: GraphLink[]
+ /** Connectors in the bottom half (node center → row bottom). */
+ bottomLinks: GraphLink[]
+}
+
+export interface CommitGraph {
+ rows: GraphRow[]
+ /** Total columns spanned, for sizing the gutter. */
+ columns: number
+}
+
+export const GRAPH_COLORS = [
+ '#3b82f6', // blue
+ '#22c55e', // green
+ '#ef4444', // red
+ '#a855f7', // purple
+ '#f59e0b', // amber
+ '#06b6d4', // cyan
+ '#ec4899', // pink
+ '#84cc16', // lime
+]
+
+interface Lane {
+ hash: string
+ color: string
+}
+
+export function computeGraph(commits: GraphInput[]): CommitGraph {
+ const lanes: (Lane | null)[] = []
+ let colorIndex = 0
+ const nextColor = () => GRAPH_COLORS[colorIndex++ % GRAPH_COLORS.length]
+ const firstFree = () => {
+ const i = lanes.indexOf(null)
+ return i === -1 ? lanes.length : i
+ }
+
+ const rows: GraphRow[] = []
+
+ for (const commit of commits) {
+ const { hash } = commit
+ const parents = commit.parents
+
+ // Snapshot lanes entering this row from above.
+ const incoming: { col: number, hash: string, color: string }[] = []
+ for (let k = 0; k < lanes.length; k++) {
+ const lane = lanes[k]
+ if (lane)
+ incoming.push({ col: k, hash: lane.hash, color: lane.color })
+ }
+
+ // The node's column: an existing lane targeting it, else a fresh lane.
+ let col = lanes.findIndex(l => l?.hash === hash)
+ let color: string
+ if (col === -1) {
+ col = firstFree()
+ color = nextColor()
+ }
+ else {
+ color = lanes[col]!.color
+ }
+
+ // Every lane targeting this commit converges into the node.
+ for (let k = 0; k < lanes.length; k++) {
+ if (lanes[k]?.hash === hash)
+ lanes[k] = null
+ }
+ lanes[col] = null
+
+ // Route the node down to its parents.
+ const parentCols: { col: number, color: string }[] = []
+ parents.forEach((parent, index) => {
+ if (index === 0) {
+ lanes[col] = { hash: parent, color }
+ parentCols.push({ col, color })
+ return
+ }
+ let pc = lanes.findIndex(l => l?.hash === parent)
+ if (pc === -1) {
+ pc = firstFree()
+ lanes[pc] = { hash: parent, color: nextColor() }
+ }
+ parentCols.push({ col: pc, color: lanes[pc]!.color })
+ })
+ if (parents.length === 0)
+ lanes[col] = null
+
+ // Drop trailing empty lanes to keep the gutter tight.
+ while (lanes.length > 0 && lanes[lanes.length - 1] == null)
+ lanes.pop()
+
+ const topLinks: GraphLink[] = incoming.map(lane => ({
+ from: lane.col,
+ to: lane.hash === hash ? col : lane.col,
+ color: lane.color,
+ }))
+
+ const bottomLinks: GraphLink[] = []
+ for (const lane of incoming) {
+ if (lane.hash !== hash)
+ bottomLinks.push({ from: lane.col, to: lane.col, color: lane.color })
+ }
+ for (const parent of parentCols)
+ bottomLinks.push({ from: col, to: parent.col, color: parent.color })
+
+ rows.push({ col, color, topLinks, bottomLinks })
+ }
+
+ let columns = 0
+ for (const row of rows) {
+ columns = Math.max(columns, row.col + 1)
+ for (const link of row.topLinks)
+ columns = Math.max(columns, link.from + 1, link.to + 1)
+ for (const link of row.bottomLinks)
+ columns = Math.max(columns, link.from + 1, link.to + 1)
+ }
+
+ return { rows, columns }
+}
diff --git a/plugins/git/src/client/lib/utils.ts b/plugins/git/src/client/lib/utils.ts
new file mode 100644
index 0000000..abba253
--- /dev/null
+++ b/plugins/git/src/client/lib/utils.ts
@@ -0,0 +1,7 @@
+import type { ClassValue } from 'clsx'
+import { clsx } from 'clsx'
+import { twMerge } from 'tailwind-merge'
+
+export function cn(...inputs: ClassValue[]) {
+ return twMerge(clsx(inputs))
+}
diff --git a/plugins/git/src/client/next.config.mjs b/plugins/git/src/client/next.config.mjs
new file mode 100644
index 0000000..55bd2e3
--- /dev/null
+++ b/plugins/git/src/client/next.config.mjs
@@ -0,0 +1,15 @@
+/** @type {import('next').NextConfig} */
+const nextConfig = {
+ output: 'export',
+ assetPrefix: '.',
+ trailingSlash: true,
+ images: { unoptimized: true },
+ // The workspace tsconfig uses path aliases that point at devframe's
+ // source so source-level edits HMR cleanly. Next.js's incremental TS
+ // check can't follow workspace project references through those aliases
+ // and ends up type-checking unrelated source. Defer typechecking to the
+ // workspace's own `tsc -b` (`pnpm typecheck`), which honors references.
+ typescript: { ignoreBuildErrors: true },
+}
+
+export default nextConfig
diff --git a/plugins/git/src/client/postcss.config.mjs b/plugins/git/src/client/postcss.config.mjs
new file mode 100644
index 0000000..6e8ba06
--- /dev/null
+++ b/plugins/git/src/client/postcss.config.mjs
@@ -0,0 +1,6 @@
+/** @type {import('postcss-load-config').Config} */
+export default {
+ plugins: {
+ '@tailwindcss/postcss': {},
+ },
+}
diff --git a/plugins/git/src/client/tsconfig.json b/plugins/git/src/client/tsconfig.json
new file mode 100644
index 0000000..3a67913
--- /dev/null
+++ b/plugins/git/src/client/tsconfig.json
@@ -0,0 +1,25 @@
+{
+ "extends": "../../../../tsconfig.base.json",
+ "compilerOptions": {
+ "incremental": true,
+ "jsx": "preserve",
+ "lib": ["ESNext", "DOM"],
+ "module": "ESNext",
+ "moduleResolution": "Bundler",
+ "allowJs": true,
+ "noEmit": true,
+ "esModuleInterop": true,
+ "isolatedDeclarations": false,
+ "plugins": [{ "name": "next" }]
+ },
+ "include": [
+ "next-env.d.ts",
+ "**/*.ts",
+ "**/*.tsx"
+ ],
+ "exclude": [
+ "node_modules",
+ ".next",
+ "out"
+ ]
+}
diff --git a/plugins/git/src/index.ts b/plugins/git/src/index.ts
new file mode 100644
index 0000000..878a72d
--- /dev/null
+++ b/plugins/git/src/index.ts
@@ -0,0 +1,81 @@
+import type { DevframeDefinition } from 'devframe/types'
+import { fileURLToPath } from 'node:url'
+import { defineDevframe } from 'devframe/types'
+import { dirname, resolve } from 'pathe'
+import { configureGit } from './rpc/context.ts'
+import { readFunctions, writeFunctions } from './rpc/index.ts'
+
+export type { Branch, GitBranches } from './rpc/functions/branches.ts'
+export type { CommitArgs, CommitResult } from './rpc/functions/commit.ts'
+export type { DiffArgs, DiffFile, GitDiff } from './rpc/functions/diff.ts'
+export type { Commit, GitLog, LogArgs } from './rpc/functions/log.ts'
+export type { StageArgs } from './rpc/functions/stage.ts'
+export type { FileStatusCode, GitStatus, StatusFileEntry } from './rpc/functions/status.ts'
+export type { UnstageArgs } from './rpc/functions/unstage.ts'
+
+// Package root, resolved one level up from this module — which sits at
+// `/src/index.ts` in dev and `/dist/index.mjs` once built, so
+// the bundled SPA is always `/dist/client`.
+const PKG_ROOT = dirname(dirname(fileURLToPath(import.meta.url)))
+
+export interface GitDevframeOptions {
+ /** Repository directory to inspect. Defaults to the devframe `cwd`. */
+ repoRoot?: string
+ /**
+ * Mount path override. Left to the adapter by default: `/` for standalone
+ * (cli / build / spa), `/__git/` for hosted (vite / embedded).
+ */
+ basePath?: string
+ /** SPA dist directory. Defaults to the package's bundled SPA. */
+ distDir?: string
+ /** Preferred dev-server port (default 9710). */
+ port?: number
+ /**
+ * Enable staging, unstaging, and committing from the UI. Read-only by
+ * default; the standalone CLI also accepts a `--write` flag.
+ */
+ write?: boolean
+}
+
+/**
+ * Create the Git dashboard devframe. Mount it into any host via devframe's
+ * adapters, or run it standalone with the bundled CLI (`devframe-git`).
+ */
+export function createGitDevframe(options: GitDevframeOptions = {}): DevframeDefinition {
+ const distDir = options.distDir ?? resolve(PKG_ROOT, 'dist/client')
+ return defineDevframe({
+ id: 'git',
+ name: 'Git',
+ version: '0.5.2',
+ packageName: '@devframes/plugin-git',
+ homepage: 'https://github.com/devframes/devframe/tree/main/plugins/git#readme',
+ description: 'Git dashboard for devframe',
+ icon: 'ph:git-branch-duotone',
+ basePath: options.basePath,
+ cli: {
+ command: 'devframe-git',
+ port: options.port ?? 9710,
+ distDir,
+ auth: false,
+ configure(cli) {
+ cli.option('--write', 'Enable staging, unstaging, and committing from the UI')
+ },
+ },
+ spa: { loader: 'none' },
+ setup(ctx, info) {
+ const write = options.write ?? info?.flags?.write === true
+ configureGit(ctx, {
+ cwd: options.repoRoot ? resolve(options.repoRoot) : ctx.cwd,
+ write,
+ })
+ for (const fn of readFunctions)
+ ctx.rpc.register(fn)
+ if (write) {
+ for (const fn of writeFunctions)
+ ctx.rpc.register(fn)
+ }
+ },
+ })
+}
+
+export default createGitDevframe()
diff --git a/plugins/git/src/node/git.ts b/plugins/git/src/node/git.ts
new file mode 100644
index 0000000..61c38c3
--- /dev/null
+++ b/plugins/git/src/node/git.ts
@@ -0,0 +1,62 @@
+import { execFile } from 'node:child_process'
+import process from 'node:process'
+import { promisify } from 'node:util'
+
+const execFileAsync = promisify(execFile)
+
+/** Field/record separators that never appear in git output we care about. */
+export const UNIT = '\x1F'
+export const RECORD = '\x1E'
+
+const MAX_BUFFER = 1024 * 1024 * 64
+
+export interface GitRunResult {
+ stdout: string
+ stderr: string
+}
+
+/**
+ * Run a git command in `cwd`. Rejects when git exits non-zero — callers that
+ * tolerate failure (e.g. "no upstream configured") should use {@link tryGit}.
+ */
+export async function runGit(cwd: string, args: string[]): Promise {
+ const { stdout, stderr } = await execFileAsync('git', args, {
+ cwd,
+ maxBuffer: MAX_BUFFER,
+ windowsHide: true,
+ // Force plain, locale-independent output so parsers stay stable.
+ env: { ...process.env, GIT_PAGER: 'cat', GIT_OPTIONAL_LOCKS: '0', LC_ALL: 'C' },
+ })
+ return { stdout, stderr }
+}
+
+/** Run a git command, returning trimmed stdout or `null` when it fails. */
+export async function tryGit(cwd: string, args: string[]): Promise {
+ try {
+ const { stdout } = await runGit(cwd, args)
+ return stdout.replace(/\n$/, '')
+ }
+ catch {
+ return null
+ }
+}
+
+/** Resolve the repository root for `cwd`, or `null` when `cwd` is outside a repo. */
+export async function resolveRepoRoot(cwd: string): Promise {
+ return tryGit(cwd, ['rev-parse', '--show-toplevel'])
+}
+
+/** Split git output on a separator, dropping the trailing empty segment. */
+export function splitClean(input: string, separator: string): string[] {
+ return input.split(separator).filter(part => part.length > 0)
+}
+
+/**
+ * Extract a concise, single-line message from a failed `execFile` error.
+ * git writes useful text (e.g. "nothing to commit") to stdout/stderr.
+ */
+export function gitErrorMessage(error: unknown): string {
+ const e = error as { stderr?: string, stdout?: string, message?: string }
+ const text = (e?.stderr || e?.stdout || e?.message || 'git command failed').trim()
+ return text.split('\n')[0]
+}
diff --git a/plugins/git/src/rpc/context.ts b/plugins/git/src/rpc/context.ts
new file mode 100644
index 0000000..583e2d0
--- /dev/null
+++ b/plugins/git/src/rpc/context.ts
@@ -0,0 +1,60 @@
+import type { DevframeNodeContext } from 'devframe/types'
+import { resolveRepoRoot } from '../node/git.ts'
+
+export interface GitConfig {
+ /** Directory the dashboard inspects. */
+ cwd: string
+ /** Whether staging / unstaging / committing actions are enabled. */
+ write?: boolean
+}
+
+export interface GitContext {
+ /** Directory the dashboard inspects. */
+ readonly cwd: string
+ /** Whether write actions (stage / unstage / commit) are enabled. */
+ readonly write: boolean
+ /**
+ * Resolve the repository root, memoized for the lifetime of the context.
+ * Resolves to `null` when `cwd` is not inside a git repository.
+ */
+ resolveRoot: () => Promise
+}
+
+const configs = new WeakMap()
+const contexts = new WeakMap()
+
+/**
+ * Record the working directory for a context. Called from the devframe
+ * `setup` before any RPC handler runs, so {@link getGitContext} can honor a
+ * `repoRoot` override instead of the raw `ctx.cwd`.
+ */
+export function configureGit(ctx: DevframeNodeContext, config: GitConfig): void {
+ configs.set(ctx, config)
+}
+
+/**
+ * Per-`DevframeNodeContext` git state. Each RPC function file pulls its
+ * working directory and (memoized) repo-root lookup from here instead of
+ * re-resolving on every call.
+ */
+export function getGitContext(ctx: DevframeNodeContext): GitContext {
+ let existing = contexts.get(ctx)
+ if (existing)
+ return existing
+
+ const config = configs.get(ctx)
+ const cwd = config?.cwd ?? ctx.cwd
+ const write = config?.write ?? false
+ let rootPromise: Promise | undefined
+
+ existing = {
+ cwd,
+ write,
+ resolveRoot: () => {
+ rootPromise ??= resolveRepoRoot(cwd)
+ return rootPromise
+ },
+ }
+ contexts.set(ctx, existing)
+ return existing
+}
diff --git a/plugins/git/src/rpc/functions/branches.ts b/plugins/git/src/rpc/functions/branches.ts
new file mode 100644
index 0000000..0f4fed3
--- /dev/null
+++ b/plugins/git/src/rpc/functions/branches.ts
@@ -0,0 +1,87 @@
+import { defineRpcFunction } from 'devframe'
+import { splitClean, tryGit, UNIT } from '../../node/git.ts'
+import { getGitContext } from '../context.ts'
+
+export interface Branch {
+ name: string
+ current: boolean
+ sha: string
+ upstream: string | null
+ subject: string
+ ahead: number
+ behind: number
+ /** `true` when the upstream branch no longer exists. */
+ gone: boolean
+}
+
+export interface GitBranches {
+ isRepo: boolean
+ current: string | null
+ branches: Branch[]
+}
+
+const FORMAT = [
+ '%(refname:short)',
+ '%(objectname:short)',
+ '%(HEAD)', // '*' on the checked-out branch, ' ' otherwise
+ '%(upstream:short)',
+ '%(upstream:track)', // e.g. "[ahead 2, behind 1]", "[gone]", or ""
+ '%(contents:subject)',
+].join(UNIT)
+
+function parseTrack(track: string): { ahead: number, behind: number, gone: boolean } {
+ if (track.includes('gone'))
+ return { ahead: 0, behind: 0, gone: true }
+ const ahead = track.match(/ahead (\d+)/)
+ const behind = track.match(/behind (\d+)/)
+ return {
+ ahead: ahead ? Number(ahead[1]) : 0,
+ behind: behind ? Number(behind[1]) : 0,
+ gone: false,
+ }
+}
+
+export const branches = defineRpcFunction({
+ name: 'git:branches',
+ type: 'query',
+ snapshot: true,
+ jsonSerializable: true,
+ setup: (ctx) => {
+ const git = getGitContext(ctx)
+ return {
+ handler: async (): Promise => {
+ const root = await git.resolveRoot()
+ if (!root)
+ return { isRepo: false, current: null, branches: [] }
+
+ const raw = await tryGit(git.cwd, [
+ 'for-each-ref',
+ `--format=${FORMAT}`,
+ 'refs/heads',
+ ])
+ if (!raw)
+ return { isRepo: true, current: null, branches: [] }
+
+ let current: string | null = null
+ const branches: Branch[] = splitClean(raw, '\n').map((line) => {
+ const [name, sha, head, upstream, track, subject] = line.split(UNIT)
+ const isCurrent = head === '*'
+ if (isCurrent)
+ current = name
+ return {
+ name,
+ current: isCurrent,
+ sha,
+ upstream: upstream || null,
+ subject: subject ?? '',
+ ...parseTrack(track ?? ''),
+ }
+ })
+
+ // Surface the current branch first, then the rest in ref order.
+ branches.sort((a, b) => Number(b.current) - Number(a.current))
+ return { isRepo: true, current, branches }
+ },
+ }
+ },
+})
diff --git a/plugins/git/src/rpc/functions/commit.ts b/plugins/git/src/rpc/functions/commit.ts
new file mode 100644
index 0000000..8eca967
--- /dev/null
+++ b/plugins/git/src/rpc/functions/commit.ts
@@ -0,0 +1,49 @@
+import type { GitStatus } from './status.ts'
+import { defineRpcFunction } from 'devframe'
+import { gitErrorMessage, runGit, tryGit } from '../../node/git.ts'
+import { getGitContext } from '../context.ts'
+import { readStatus } from './status.ts'
+
+export interface CommitArgs {
+ /** Commit message. */
+ message: string
+}
+
+export interface CommitResult {
+ /** `true` when the commit succeeded. */
+ ok: boolean
+ /** Short hash of the new commit, or `null` on failure. */
+ hash: string | null
+ /** Human-readable outcome (e.g. "nothing to commit"). */
+ message: string
+ /** Working-tree status after the attempt. */
+ status: GitStatus
+}
+
+export const commit = defineRpcFunction({
+ name: 'git:commit',
+ type: 'action',
+ jsonSerializable: true,
+ setup: (ctx) => {
+ const git = getGitContext(ctx)
+ return {
+ handler: async (args: CommitArgs): Promise => {
+ const message = (args?.message ?? '').trim()
+ const root = await git.resolveRoot()
+ if (!root)
+ return { ok: false, hash: null, message: 'Not a git repository.', status: await readStatus(git) }
+ if (!message)
+ return { ok: false, hash: null, message: 'Commit message is required.', status: await readStatus(git) }
+
+ try {
+ await runGit(git.cwd, ['commit', '-m', message])
+ const hash = await tryGit(git.cwd, ['rev-parse', '--short', 'HEAD'])
+ return { ok: true, hash, message: 'Committed.', status: await readStatus(git) }
+ }
+ catch (error) {
+ return { ok: false, hash: null, message: gitErrorMessage(error), status: await readStatus(git) }
+ }
+ },
+ }
+ },
+})
diff --git a/plugins/git/src/rpc/functions/diff.ts b/plugins/git/src/rpc/functions/diff.ts
new file mode 100644
index 0000000..cf9582a
--- /dev/null
+++ b/plugins/git/src/rpc/functions/diff.ts
@@ -0,0 +1,106 @@
+import { defineRpcFunction } from 'devframe'
+import { runGit, splitClean, tryGit } from '../../node/git.ts'
+import { getGitContext } from '../context.ts'
+
+/** Hard cap on the returned patch text to keep payloads bounded. */
+const PATCH_CHAR_LIMIT = 200_000
+
+export interface DiffFile {
+ path: string
+ additions: number
+ deletions: number
+ binary: boolean
+}
+
+export interface GitDiff {
+ isRepo: boolean
+ staged: boolean
+ path: string | null
+ files: DiffFile[]
+ totalAdditions: number
+ totalDeletions: number
+ /** Unified patch text — populated when `path` targets a single file. */
+ patch: string | null
+ /** `true` when `patch` was clipped to {@link PATCH_CHAR_LIMIT}. */
+ truncated: boolean
+}
+
+export interface DiffArgs {
+ /** Limit the diff to a single path; omit for the whole tree. */
+ path?: string
+ /** Diff the index against HEAD instead of the working tree. */
+ staged?: boolean
+}
+
+function parseNumstat(raw: string): DiffFile[] {
+ return splitClean(raw, '\n').map((line) => {
+ const [add, del, ...rest] = line.split('\t')
+ const binary = add === '-' || del === '-'
+ return {
+ path: rest.join('\t'),
+ additions: binary ? 0 : Number(add),
+ deletions: binary ? 0 : Number(del),
+ binary,
+ }
+ })
+}
+
+export const diff = defineRpcFunction({
+ name: 'git:diff',
+ type: 'query',
+ snapshot: true,
+ jsonSerializable: true,
+ setup: (ctx) => {
+ const git = getGitContext(ctx)
+ return {
+ handler: async (args: DiffArgs = {}): Promise => {
+ const { path, staged = false } = args
+ const root = await git.resolveRoot()
+ if (!root) {
+ return {
+ isRepo: false,
+ staged,
+ path: path ?? null,
+ files: [],
+ totalAdditions: 0,
+ totalDeletions: 0,
+ patch: null,
+ truncated: false,
+ }
+ }
+
+ const base = staged ? ['diff', '--cached'] : ['diff']
+ const scope = path ? ['--', path] : []
+
+ const numstatRaw = await tryGit(git.cwd, [...base, '--numstat', ...scope])
+ const files = numstatRaw ? parseNumstat(numstatRaw) : []
+ const totalAdditions = files.reduce((sum, f) => sum + f.additions, 0)
+ const totalDeletions = files.reduce((sum, f) => sum + f.deletions, 0)
+
+ let patch: string | null = null
+ let truncated = false
+ if (path) {
+ const { stdout } = await runGit(git.cwd, [...base, ...scope])
+ if (stdout.length > PATCH_CHAR_LIMIT) {
+ patch = stdout.slice(0, PATCH_CHAR_LIMIT)
+ truncated = true
+ }
+ else {
+ patch = stdout
+ }
+ }
+
+ return {
+ isRepo: true,
+ staged,
+ path: path ?? null,
+ files,
+ totalAdditions,
+ totalDeletions,
+ patch,
+ truncated,
+ }
+ },
+ }
+ },
+})
diff --git a/plugins/git/src/rpc/functions/log.ts b/plugins/git/src/rpc/functions/log.ts
new file mode 100644
index 0000000..5cdcd51
--- /dev/null
+++ b/plugins/git/src/rpc/functions/log.ts
@@ -0,0 +1,100 @@
+import { defineRpcFunction } from 'devframe'
+import { RECORD, splitClean, tryGit, UNIT } from '../../node/git.ts'
+import { getGitContext } from '../context.ts'
+
+export interface Commit {
+ hash: string
+ shortHash: string
+ author: string
+ email: string
+ /** Author date as epoch milliseconds. */
+ date: number
+ subject: string
+ body: string
+ /** Ref names pointing at this commit (branches, tags, HEAD). */
+ refs: string[]
+ /** Full parent hashes — drives the commit graph. */
+ parents: string[]
+}
+
+export interface GitLog {
+ isRepo: boolean
+ commits: Commit[]
+ limit: number
+ skip: number
+ /** `true` when the page filled to `limit`, hinting at further history. */
+ hasMore: boolean
+}
+
+export interface LogArgs {
+ /** Number of commits to return (clamped to 1–200, default 30). */
+ limit?: number
+ /** Commits to skip from the tip, for pagination (default 0). */
+ skip?: number
+}
+
+// Stable, parseable format: unit-separated fields, record-separated commits.
+const FORMAT = [
+ '%H', // full hash
+ '%h', // short hash
+ '%P', // parent hashes (space-separated)
+ '%an', // author name
+ '%ae', // author email
+ '%aI', // author date, strict ISO 8601
+ '%D', // ref names
+ '%s', // subject
+ '%b', // body
+].join(UNIT) + RECORD
+
+function clamp(value: number, min: number, max: number): number {
+ return Math.min(Math.max(value, min), max)
+}
+
+function parseLog(raw: string): Commit[] {
+ return splitClean(raw, RECORD).map((record) => {
+ const [hash, shortHash, parents, author, email, isoDate, refs, subject, body] = record
+ .replace(/^\n/, '')
+ .split(UNIT)
+ return {
+ hash,
+ shortHash,
+ author,
+ email,
+ date: Date.parse(isoDate),
+ subject,
+ body: (body ?? '').trim(),
+ refs: refs ? refs.split(', ').map(r => r.trim()).filter(Boolean) : [],
+ parents: parents ? parents.split(' ').filter(Boolean) : [],
+ }
+ })
+}
+
+export const log = defineRpcFunction({
+ name: 'git:log',
+ type: 'query',
+ snapshot: true,
+ jsonSerializable: true,
+ setup: (ctx) => {
+ const git = getGitContext(ctx)
+ return {
+ handler: async (args: LogArgs = {}): Promise => {
+ const limit = clamp(Math.trunc(args.limit ?? 30), 1, 200)
+ const skip = Math.max(0, Math.trunc(args.skip ?? 0))
+ const root = await git.resolveRoot()
+ if (!root)
+ return { isRepo: false, commits: [], limit, skip, hasMore: false }
+
+ const raw = await tryGit(git.cwd, [
+ 'log',
+ '--topo-order',
+ `--max-count=${limit}`,
+ `--skip=${skip}`,
+ `--pretty=format:${FORMAT}`,
+ ])
+ // `null` happens on a repo with no commits yet — treat as empty.
+ const commits = raw ? parseLog(raw) : []
+ return { isRepo: true, commits, limit, skip, hasMore: commits.length === limit }
+ },
+ }
+ },
+})
diff --git a/plugins/git/src/rpc/functions/stage.ts b/plugins/git/src/rpc/functions/stage.ts
new file mode 100644
index 0000000..43812bb
--- /dev/null
+++ b/plugins/git/src/rpc/functions/stage.ts
@@ -0,0 +1,28 @@
+import type { GitStatus } from './status.ts'
+import { defineRpcFunction } from 'devframe'
+import { runGit } from '../../node/git.ts'
+import { getGitContext } from '../context.ts'
+import { readStatus } from './status.ts'
+
+export interface StageArgs {
+ /** Paths to stage (`git add`). */
+ paths: string[]
+}
+
+export const stage = defineRpcFunction({
+ name: 'git:stage',
+ type: 'action',
+ jsonSerializable: true,
+ setup: (ctx) => {
+ const git = getGitContext(ctx)
+ return {
+ handler: async (args: StageArgs): Promise => {
+ const paths = args?.paths ?? []
+ const root = await git.resolveRoot()
+ if (root && paths.length > 0)
+ await runGit(git.cwd, ['add', '--', ...paths])
+ return readStatus(git)
+ },
+ }
+ },
+})
diff --git a/plugins/git/src/rpc/functions/status.ts b/plugins/git/src/rpc/functions/status.ts
new file mode 100644
index 0000000..714e98d
--- /dev/null
+++ b/plugins/git/src/rpc/functions/status.ts
@@ -0,0 +1,177 @@
+import type { GitContext } from '../context.ts'
+import { defineRpcFunction } from 'devframe'
+import { runGit } from '../../node/git.ts'
+import { getGitContext } from '../context.ts'
+
+export type FileStatusCode
+ = | 'modified'
+ | 'added'
+ | 'deleted'
+ | 'renamed'
+ | 'copied'
+ | 'type-changed'
+ | 'unmerged'
+ | 'unknown'
+
+export interface StatusFileEntry {
+ path: string
+ /** Previous path, present for renames and copies. */
+ from?: string
+ status: FileStatusCode
+}
+
+export interface GitStatus {
+ /** `false` when the working directory is not inside a git repository. */
+ isRepo: boolean
+ root: string | null
+ /** Current branch name, or `null` when HEAD is detached. */
+ branch: string | null
+ detached: boolean
+ /** Short HEAD object name. */
+ head: string | null
+ upstream: string | null
+ ahead: number
+ behind: number
+ staged: StatusFileEntry[]
+ unstaged: StatusFileEntry[]
+ untracked: string[]
+ /** `true` when there are no staged, unstaged, or untracked changes. */
+ clean: boolean
+ /** `true` when stage / unstage / commit actions are available. */
+ canWrite: boolean
+}
+
+export const EMPTY_STATUS: GitStatus = {
+ isRepo: false,
+ root: null,
+ branch: null,
+ detached: false,
+ head: null,
+ upstream: null,
+ ahead: 0,
+ behind: 0,
+ staged: [],
+ unstaged: [],
+ untracked: [],
+ clean: true,
+ canWrite: false,
+}
+
+function mapCode(code: string): FileStatusCode {
+ switch (code) {
+ case 'M': return 'modified'
+ case 'A': return 'added'
+ case 'D': return 'deleted'
+ case 'R': return 'renamed'
+ case 'C': return 'copied'
+ case 'T': return 'type-changed'
+ case 'U': return 'unmerged'
+ default: return 'unknown'
+ }
+}
+
+/**
+ * Parse `git status --porcelain=v2 --branch -z` into a structured snapshot.
+ * Records are NUL-separated; rename/copy (type `2`) entries consume an extra
+ * token for the original path.
+ */
+function parseStatus(root: string, raw: string): GitStatus {
+ const tokens = raw.split('\0')
+ const status: GitStatus = { ...EMPTY_STATUS, isRepo: true, root, staged: [], unstaged: [], untracked: [] }
+
+ for (let i = 0; i < tokens.length; i++) {
+ const token = tokens[i]
+ if (!token)
+ continue
+
+ if (token.startsWith('# ')) {
+ const [, key, ...rest] = token.split(' ')
+ const value = rest.join(' ')
+ if (key === 'branch.head') {
+ if (value === '(detached)') {
+ status.detached = true
+ status.branch = null
+ }
+ else {
+ status.branch = value
+ }
+ }
+ else if (key === 'branch.oid' && value !== '(initial)') {
+ status.head = value.slice(0, 9)
+ }
+ else if (key === 'branch.upstream') {
+ status.upstream = value
+ }
+ else if (key === 'branch.ab') {
+ const match = value.match(/\+(\d+)\s+-(\d+)/)
+ if (match) {
+ status.ahead = Number(match[1])
+ status.behind = Number(match[2])
+ }
+ }
+ continue
+ }
+
+ if (token.startsWith('1 ') || token.startsWith('2 ')) {
+ const renamed = token.startsWith('2 ')
+ const fields = token.split(' ')
+ const xy = fields[1]
+ const x = xy[0]
+ const y = xy[1]
+ // Type 1 path begins at field 8; type 2 inserts the rename score at
+ // field 8, pushing the path to field 9 and the original to a NUL token.
+ const path = fields.slice(renamed ? 9 : 8).join(' ')
+ const from = renamed ? tokens[++i] : undefined
+
+ if (x !== '.') {
+ status.staged.push(from ? { path, from, status: mapCode(x) } : { path, status: mapCode(x) })
+ }
+ if (y !== '.') {
+ status.unstaged.push({ path, status: mapCode(y) })
+ }
+ continue
+ }
+
+ if (token.startsWith('u ')) {
+ const path = token.split(' ').slice(10).join(' ')
+ status.unstaged.push({ path, status: 'unmerged' })
+ continue
+ }
+
+ if (token.startsWith('? ')) {
+ status.untracked.push(token.slice(2))
+ }
+ }
+
+ status.clean = status.staged.length === 0
+ && status.unstaged.length === 0
+ && status.untracked.length === 0
+ return status
+}
+
+/**
+ * Read the working-tree status for a git context. Shared by the `git:status`
+ * query and the write actions (which return fresh status after mutating).
+ */
+export async function readStatus(git: GitContext): Promise {
+ const root = await git.resolveRoot()
+ if (!root)
+ return { ...EMPTY_STATUS, canWrite: false }
+ const { stdout } = await runGit(git.cwd, ['status', '--porcelain=v2', '--branch', '-z'])
+ const status = parseStatus(root, stdout)
+ status.canWrite = git.write
+ return status
+}
+
+export const status = defineRpcFunction({
+ name: 'git:status',
+ type: 'query',
+ snapshot: true,
+ jsonSerializable: true,
+ setup: (ctx) => {
+ const git = getGitContext(ctx)
+ return {
+ handler: (): Promise => readStatus(git),
+ }
+ },
+})
diff --git a/plugins/git/src/rpc/functions/unstage.ts b/plugins/git/src/rpc/functions/unstage.ts
new file mode 100644
index 0000000..a21ddcd
--- /dev/null
+++ b/plugins/git/src/rpc/functions/unstage.ts
@@ -0,0 +1,28 @@
+import type { GitStatus } from './status.ts'
+import { defineRpcFunction } from 'devframe'
+import { runGit } from '../../node/git.ts'
+import { getGitContext } from '../context.ts'
+import { readStatus } from './status.ts'
+
+export interface UnstageArgs {
+ /** Paths to unstage (`git restore --staged`). */
+ paths: string[]
+}
+
+export const unstage = defineRpcFunction({
+ name: 'git:unstage',
+ type: 'action',
+ jsonSerializable: true,
+ setup: (ctx) => {
+ const git = getGitContext(ctx)
+ return {
+ handler: async (args: UnstageArgs): Promise => {
+ const paths = args?.paths ?? []
+ const root = await git.resolveRoot()
+ if (root && paths.length > 0)
+ await runGit(git.cwd, ['restore', '--staged', '--', ...paths])
+ return readStatus(git)
+ },
+ }
+ },
+})
diff --git a/plugins/git/src/rpc/index.ts b/plugins/git/src/rpc/index.ts
new file mode 100644
index 0000000..79d5a54
--- /dev/null
+++ b/plugins/git/src/rpc/index.ts
@@ -0,0 +1,20 @@
+import type { RpcDefinitionsToFunctions } from 'devframe/rpc'
+import { branches } from './functions/branches.ts'
+import { commit } from './functions/commit.ts'
+import { diff } from './functions/diff.ts'
+import { log } from './functions/log.ts'
+import { stage } from './functions/stage.ts'
+import { status } from './functions/status.ts'
+import { unstage } from './functions/unstage.ts'
+
+/** Read-only RPC — always registered. */
+export const readFunctions = [status, log, branches, diff] as const
+
+/** Mutating RPC — registered only when write actions are enabled. */
+export const writeFunctions = [stage, unstage, commit] as const
+
+export const serverFunctions = [status, log, branches, diff, stage, unstage, commit] as const
+
+declare module 'devframe' {
+ interface DevframeRpcServerFunctions extends RpcDefinitionsToFunctions {}
+}
diff --git a/plugins/git/test/_repo.ts b/plugins/git/test/_repo.ts
new file mode 100644
index 0000000..944f727
--- /dev/null
+++ b/plugins/git/test/_repo.ts
@@ -0,0 +1,75 @@
+import { execFileSync } from 'node:child_process'
+import { mkdtempSync, rmSync, writeFileSync } from 'node:fs'
+import { tmpdir } from 'node:os'
+import { join } from 'node:path'
+import process from 'node:process'
+
+export interface TempRepo {
+ dir: string
+ cleanup: () => void
+}
+
+const GIT_ENV = {
+ ...process.env,
+ GIT_AUTHOR_NAME: 'Test User',
+ GIT_AUTHOR_EMAIL: 'test@example.com',
+ GIT_COMMITTER_NAME: 'Test User',
+ GIT_COMMITTER_EMAIL: 'test@example.com',
+ GIT_AUTHOR_DATE: '2020-01-01T00:00:00Z',
+ GIT_COMMITTER_DATE: '2020-01-01T00:00:00Z',
+ // Ignore the developer's global/system config so commits are deterministic.
+ GIT_CONFIG_GLOBAL: '/dev/null',
+ GIT_CONFIG_SYSTEM: '/dev/null',
+}
+
+function git(dir: string, args: string[]): void {
+ execFileSync('git', args, { cwd: dir, stdio: 'pipe', env: GIT_ENV })
+}
+
+function write(dir: string, file: string, content: string): void {
+ writeFileSync(join(dir, file), content)
+}
+
+/**
+ * Create a throwaway git repository with a known shape:
+ * - branch `main` with two commits, plus a `feature/x` branch.
+ * - one staged add (`staged.txt`), one unstaged modification (`README.md`),
+ * and one untracked file (`untracked.txt`).
+ */
+export function createTempRepo(): TempRepo {
+ const dir = mkdtempSync(join(tmpdir(), 'devframe-git-'))
+ git(dir, ['init', '-b', 'main'])
+ git(dir, ['config', 'user.name', 'Test User'])
+ git(dir, ['config', 'user.email', 'test@example.com'])
+ git(dir, ['config', 'commit.gpgsign', 'false'])
+
+ write(dir, 'README.md', '# Demo\n')
+ git(dir, ['add', 'README.md'])
+ git(dir, ['commit', '-m', 'init: add readme'])
+
+ write(dir, 'a.txt', 'hello\n')
+ git(dir, ['add', 'a.txt'])
+ git(dir, ['commit', '-m', 'feat: add a.txt'])
+
+ git(dir, ['branch', 'feature/x'])
+
+ // Working-tree state for status/diff assertions.
+ write(dir, 'README.md', '# Demo\nmore\n') // unstaged modification
+ write(dir, 'staged.txt', 'staged content\n')
+ git(dir, ['add', 'staged.txt']) // staged add
+ write(dir, 'untracked.txt', 'untracked\n') // untracked
+
+ return {
+ dir,
+ cleanup: () => rmSync(dir, { recursive: true, force: true }),
+ }
+}
+
+/** Create an empty (non-git) temp directory. */
+export function createTempDir(): TempRepo {
+ const dir = mkdtempSync(join(tmpdir(), 'devframe-git-bare-'))
+ return {
+ dir,
+ cleanup: () => rmSync(dir, { recursive: true, force: true }),
+ }
+}
diff --git a/plugins/git/test/_utils.ts b/plugins/git/test/_utils.ts
new file mode 100644
index 0000000..c3cc794
--- /dev/null
+++ b/plugins/git/test/_utils.ts
@@ -0,0 +1,72 @@
+import type { StartedServer } from 'devframe/node'
+import type { DevframeNodeContext } from 'devframe/types'
+import type { GitDevframeOptions } from '../src/index'
+import { DEVFRAME_CONNECTION_META_FILENAME } from 'devframe/constants'
+import {
+ createH3DevframeHost,
+ createHostContext,
+ startHttpAndWs,
+} from 'devframe/node'
+import { mountStaticHandler } from 'devframe/utils/serve-static'
+import { getPort } from 'get-port-please'
+import { H3 } from 'h3'
+import { resolve } from 'pathe'
+import { createGitDevframe } from '../src/index'
+
+export interface DashboardServer extends StartedServer {
+ basePath: string
+}
+
+/**
+ * Build a node context rooted at `cwd`. Used directly by the build-dump test
+ * and indirectly by {@link startDashboardServer}.
+ */
+export async function createDashboardContext(
+ cwd: string,
+ mode: 'dev' | 'build' = 'dev',
+ options: GitDevframeOptions = {},
+ app: H3 = new H3(),
+): Promise {
+ const devframe = createGitDevframe(options)
+ const h3Host = createH3DevframeHost({
+ origin: 'http://127.0.0.1',
+ appName: devframe.id,
+ mount: (base, dir) => mountStaticHandler(app, base, dir),
+ })
+ const ctx = await createHostContext({ cwd, mode, host: h3Host })
+ await devframe.setup(ctx)
+ return ctx
+}
+
+/**
+ * Boot the dashboard server in-process against `cwd`, mirroring the CLI
+ * adapter's WS+HTTP wiring on a random free port. Bound to 127.0.0.1 to
+ * avoid the IPv4/IPv6 race documented in devframe's ws transport tests.
+ */
+export async function startDashboardServer(
+ cwd: string,
+ options: GitDevframeOptions = {},
+): Promise {
+ const devframe = createGitDevframe(options)
+ const distDir = devframe.cli!.distDir!
+ // The factory leaves basePath adapter-resolved; standalone defaults to '/'.
+ const basePath = devframe.basePath ?? '/'
+ const host = '127.0.0.1'
+ const port = await getPort({ host, random: true })
+
+ const app = new H3()
+ const h3Host = createH3DevframeHost({
+ origin: `http://${host}:${port}`,
+ appName: devframe.id,
+ mount: (base, dir) => mountStaticHandler(app, base, dir),
+ })
+ const ctx = await createHostContext({ cwd, mode: 'dev', host: h3Host })
+ await devframe.setup(ctx)
+
+ const metaPath = `${basePath}${DEVFRAME_CONNECTION_META_FILENAME}`
+ app.use(metaPath, () => ({ backend: 'websocket', websocket: port }))
+ mountStaticHandler(app, basePath, resolve(distDir))
+
+ const server = await startHttpAndWs({ context: ctx, host, port, app, auth: false })
+ return Object.assign(server, { basePath })
+}
diff --git a/plugins/git/test/git.test.ts b/plugins/git/test/git.test.ts
new file mode 100644
index 0000000..727ddbd
--- /dev/null
+++ b/plugins/git/test/git.test.ts
@@ -0,0 +1,248 @@
+import type { CommitResult, GitBranches, GitDiff, GitLog, GitStatus } from '../src/index'
+import { createRpcClient } from 'devframe/rpc/client'
+import { collectStaticRpcDump } from 'devframe/rpc/dump'
+import { createWsRpcChannel } from 'devframe/rpc/transports/ws-client'
+import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
+import { WebSocket } from 'ws'
+import { createTempDir, createTempRepo } from './_repo'
+import { createDashboardContext, startDashboardServer } from './_utils'
+
+vi.stubGlobal('WebSocket', WebSocket)
+
+function bootRpc(port: number) {
+ const channel = createWsRpcChannel({ url: `ws://127.0.0.1:${port}` })
+ return createRpcClient({}, { channel })
+}
+
+describe('@devframes/plugin-git', () => {
+ let repo: ReturnType
+ let server: Awaited>
+
+ beforeEach(async () => {
+ repo = createTempRepo()
+ server = await startDashboardServer(repo.dir)
+ })
+
+ afterEach(async () => {
+ await server?.close()
+ repo?.cleanup()
+ })
+
+ it('serves connection meta pointing at the WS backend', async () => {
+ const res = await fetch(`${server.origin}${server.basePath}__connection.json`)
+ expect(res.status).toBe(200)
+ const meta = await res.json() as { backend: string, websocket: number }
+ expect(meta.backend).toBe('websocket')
+ expect(meta.websocket).toBe(server.port)
+ })
+
+ it('reports branch, staged, unstaged, and untracked status', async () => {
+ const rpc = bootRpc(server.port)
+ const status = await rpc.$call('git:status') as GitStatus
+ expect(status.isRepo).toBe(true)
+ expect(status.branch).toBe('main')
+ expect(status.detached).toBe(false)
+ expect(status.head).toMatch(/^[0-9a-f]+$/)
+ expect(status.clean).toBe(false)
+
+ expect(status.staged).toContainEqual({ path: 'staged.txt', status: 'added' })
+ expect(status.unstaged).toContainEqual({ path: 'README.md', status: 'modified' })
+ expect(status.untracked).toContain('untracked.txt')
+ })
+
+ it('returns the commit log newest-first', async () => {
+ const rpc = bootRpc(server.port)
+ const log = await rpc.$call('git:log', { limit: 30 }) as GitLog
+ expect(log.isRepo).toBe(true)
+ expect(log.commits).toHaveLength(2)
+ expect(log.commits[0].subject).toBe('feat: add a.txt')
+ expect(log.commits[1].subject).toBe('init: add readme')
+ expect(log.commits[0].author).toBe('Test User')
+ expect(log.commits[0].email).toBe('test@example.com')
+ expect(typeof log.commits[0].date).toBe('number')
+ expect(log.hasMore).toBe(false)
+ // Parents drive the commit graph: the tip points at the root, which has none.
+ expect(log.commits[1].parents).toEqual([])
+ expect(log.commits[0].parents).toEqual([log.commits[1].hash])
+ })
+
+ it('paginates the log and flags more history', async () => {
+ const rpc = bootRpc(server.port)
+ const page = await rpc.$call('git:log', { limit: 1 }) as GitLog
+ expect(page.commits).toHaveLength(1)
+ expect(page.commits[0].subject).toBe('feat: add a.txt')
+ expect(page.hasMore).toBe(true)
+
+ const next = await rpc.$call('git:log', { limit: 1, skip: 1 }) as GitLog
+ expect(next.commits).toHaveLength(1)
+ expect(next.commits[0].subject).toBe('init: add readme')
+ expect(next.hasMore).toBe(true)
+
+ const tail = await rpc.$call('git:log', { limit: 1, skip: 2 }) as GitLog
+ expect(tail.commits).toHaveLength(0)
+ expect(tail.hasMore).toBe(false)
+ })
+
+ it('lists local branches with the current one first', async () => {
+ const rpc = bootRpc(server.port)
+ const result = await rpc.$call('git:branches', {}) as GitBranches
+ expect(result.isRepo).toBe(true)
+ expect(result.current).toBe('main')
+ expect(result.branches).toHaveLength(2)
+ expect(result.branches[0].current).toBe(true)
+ expect(result.branches[0].name).toBe('main')
+ expect(result.branches.map(b => b.name).sort()).toEqual(['feature/x', 'main'])
+ })
+
+ it('summarizes the working-tree diff', async () => {
+ const rpc = bootRpc(server.port)
+ const diff = await rpc.$call('git:diff', {}) as GitDiff
+ expect(diff.isRepo).toBe(true)
+ expect(diff.staged).toBe(false)
+ expect(diff.files.map(f => f.path)).toContain('README.md')
+ // Staged and untracked files don't appear in the working-tree diff.
+ expect(diff.files.map(f => f.path)).not.toContain('staged.txt')
+ expect(diff.totalAdditions).toBeGreaterThan(0)
+ expect(diff.patch).toBeNull()
+ })
+
+ it('summarizes the staged diff', async () => {
+ const rpc = bootRpc(server.port)
+ const diff = await rpc.$call('git:diff', { staged: true }) as GitDiff
+ expect(diff.staged).toBe(true)
+ expect(diff.files.map(f => f.path)).toContain('staged.txt')
+ })
+
+ it('returns a unified patch for a single path', async () => {
+ const rpc = bootRpc(server.port)
+ const diff = await rpc.$call('git:diff', { path: 'README.md' }) as GitDiff
+ expect(diff.path).toBe('README.md')
+ expect(diff.files.map(f => f.path)).toEqual(['README.md'])
+ expect(diff.patch).toContain('+more')
+ expect(diff.truncated).toBe(false)
+ })
+})
+
+describe('@devframes/plugin-git (non-repo directory)', () => {
+ let dir: ReturnType
+ let server: Awaited>
+
+ beforeEach(async () => {
+ dir = createTempDir()
+ server = await startDashboardServer(dir.dir)
+ })
+
+ afterEach(async () => {
+ await server?.close()
+ dir?.cleanup()
+ })
+
+ it('degrades gracefully outside a git repository', async () => {
+ const rpc = bootRpc(server.port)
+ const status = await rpc.$call('git:status') as GitStatus
+ expect(status.isRepo).toBe(false)
+ expect(status.branch).toBeNull()
+ expect(status.clean).toBe(true)
+
+ const log = await rpc.$call('git:log', {}) as GitLog
+ expect(log.isRepo).toBe(false)
+ expect(log.commits).toEqual([])
+
+ const branches = await rpc.$call('git:branches', {}) as GitBranches
+ expect(branches.isRepo).toBe(false)
+
+ const diff = await rpc.$call('git:diff', {}) as GitDiff
+ expect(diff.isRepo).toBe(false)
+ expect(diff.files).toEqual([])
+ })
+})
+
+describe('@devframes/plugin-git (build snapshot)', () => {
+ it('bakes a status snapshot for static deployment', async () => {
+ const repo = createTempRepo()
+ try {
+ const ctx = await createDashboardContext(repo.dir, 'build')
+ const dump = await collectStaticRpcDump(ctx.rpc.definitions.values(), ctx)
+
+ const entry = dump.manifest['git:status']
+ expect(entry).toBeDefined()
+ expect(entry.type).toBe('query')
+ expect(entry.fallback).toBeTruthy()
+
+ // The baked fallback is what a static client returns for any call.
+ const file = dump.files[entry.fallback]
+ const status = (file.data as { output: GitStatus }).output
+ expect(status.isRepo).toBe(true)
+ expect(status.branch).toBe('main')
+ }
+ finally {
+ repo.cleanup()
+ }
+ })
+})
+
+describe('@devframes/plugin-git (write actions)', () => {
+ it('stages, unstages, and commits when write is enabled', async () => {
+ const repo = createTempRepo()
+ const server = await startDashboardServer(repo.dir, { write: true })
+ try {
+ const rpc = bootRpc(server.port)
+
+ const initial = await rpc.$call('git:status') as GitStatus
+ expect(initial.canWrite).toBe(true)
+
+ // Stage the unstaged + untracked files.
+ let status = await rpc.$call('git:stage', { paths: ['README.md', 'untracked.txt'] }) as GitStatus
+ expect(status.staged.map(f => f.path)).toEqual(
+ expect.arrayContaining(['staged.txt', 'README.md', 'untracked.txt']),
+ )
+ expect(status.untracked).not.toContain('untracked.txt')
+
+ // Unstage one of them again.
+ status = await rpc.$call('git:unstage', { paths: ['staged.txt'] }) as GitStatus
+ expect(status.staged.map(f => f.path)).not.toContain('staged.txt')
+
+ // Commit what's left staged.
+ const result = await rpc.$call('git:commit', { message: 'test: commit from ui' }) as CommitResult
+ expect(result.ok).toBe(true)
+ expect(result.hash).toMatch(/^[0-9a-f]+$/)
+
+ const log = await rpc.$call('git:log', {}) as GitLog
+ expect(log.commits[0].subject).toBe('test: commit from ui')
+ }
+ finally {
+ await server.close()
+ repo.cleanup()
+ }
+ })
+
+ it('rejects an empty commit message', async () => {
+ const repo = createTempRepo()
+ const server = await startDashboardServer(repo.dir, { write: true })
+ try {
+ const rpc = bootRpc(server.port)
+ const result = await rpc.$call('git:commit', { message: ' ' }) as CommitResult
+ expect(result.ok).toBe(false)
+ expect(result.hash).toBeNull()
+ }
+ finally {
+ await server.close()
+ repo.cleanup()
+ }
+ })
+
+ it('omits write actions when write is disabled', async () => {
+ const repo = createTempRepo()
+ const server = await startDashboardServer(repo.dir)
+ try {
+ const rpc = bootRpc(server.port)
+ const status = await rpc.$call('git:status') as GitStatus
+ expect(status.canWrite).toBe(false)
+ await expect(rpc.$call('git:stage', { paths: ['README.md'] })).rejects.toBeDefined()
+ }
+ finally {
+ await server.close()
+ repo.cleanup()
+ }
+ })
+})
diff --git a/plugins/git/tsconfig.json b/plugins/git/tsconfig.json
new file mode 100644
index 0000000..bd57e78
--- /dev/null
+++ b/plugins/git/tsconfig.json
@@ -0,0 +1,8 @@
+{
+ "extends": "../../tsconfig.base.json",
+ "compilerOptions": {
+ "lib": ["esnext", "dom"]
+ },
+ "include": ["src", "test", "tsdown.config.ts"],
+ "exclude": ["dist", "src/client", ".next", "out", "node_modules"]
+}
diff --git a/plugins/git/tsdown.config.ts b/plugins/git/tsdown.config.ts
new file mode 100644
index 0000000..17ab5f0
--- /dev/null
+++ b/plugins/git/tsdown.config.ts
@@ -0,0 +1,15 @@
+import { defineConfig } from 'tsdown'
+
+export default defineConfig({
+ entry: {
+ index: 'src/index.ts',
+ cli: 'src/cli.ts',
+ },
+ outExtensions: () => ({ js: '.mjs', dts: '.d.mts' }),
+ clean: true,
+ tsconfig: '../../tsconfig.base.json',
+ dts: true,
+ platform: 'node',
+ // The Next.js SPA under src/client is built separately (`build:spa`);
+ // tsdown only compiles the node-side entries above.
+})
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 65e4d1e..b126d7f 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -111,6 +111,42 @@ catalogs:
specifier: ^2.0.17
version: 2.0.17
frontend:
+ '@radix-ui/react-scroll-area':
+ specifier: ^1.2.12
+ version: 1.2.12
+ '@radix-ui/react-separator':
+ specifier: ^1.1.10
+ version: 1.1.10
+ '@radix-ui/react-slot':
+ specifier: ^1.3.0
+ version: 1.3.0
+ '@radix-ui/react-tabs':
+ specifier: ^1.1.15
+ version: 1.1.15
+ '@storybook/addon-a11y':
+ specifier: ^10.4.6
+ version: 10.4.6
+ '@storybook/addon-docs':
+ specifier: ^10.4.6
+ version: 10.4.6
+ '@storybook/react-vite':
+ specifier: ^10.4.6
+ version: 10.4.6
+ '@tailwindcss/postcss':
+ specifier: ^4.3.1
+ version: 4.3.1
+ '@tailwindcss/vite':
+ specifier: ^4.3.1
+ version: 4.3.1
+ class-variance-authority:
+ specifier: ^0.7.1
+ version: 0.7.1
+ clsx:
+ specifier: ^2.1.1
+ version: 2.1.1
+ lucide-react:
+ specifier: ^1.20.0
+ version: 1.20.0
next:
specifier: ^16.2.6
version: 16.2.6
@@ -123,6 +159,18 @@ catalogs:
react-dom:
specifier: ^19.2.6
version: 19.2.6
+ storybook:
+ specifier: ^10.4.6
+ version: 10.4.6
+ tailwind-merge:
+ specifier: ^3.6.0
+ version: 3.6.0
+ tailwindcss:
+ specifier: ^4.3.1
+ version: 4.3.1
+ tw-animate-css:
+ specifier: ^1.4.0
+ version: 1.4.0
inlined:
'@antfu/utils':
specifier: ^9.3.0
@@ -305,7 +353,7 @@ importers:
version: link:../../packages/devframe
next:
specifier: catalog:frontend
- version: 16.2.6(@playwright/test@1.60.0)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)
+ version: 16.2.6(@babel/core@7.29.0)(@playwright/test@1.60.0)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)
react:
specifier: catalog:frontend
version: 19.2.6
@@ -358,7 +406,7 @@ importers:
version: link:../../packages/devframe
next:
specifier: catalog:frontend
- version: 16.2.6(@playwright/test@1.60.0)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)
+ version: 16.2.6(@babel/core@7.29.0)(@playwright/test@1.60.0)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)
react:
specifier: catalog:frontend
version: 19.2.6
@@ -572,11 +620,103 @@ importers:
specifier: catalog:deps
version: 8.21.0
+ plugins/git:
+ dependencies:
+ devframe:
+ specifier: workspace:*
+ version: link:../../packages/devframe
+ pathe:
+ specifier: catalog:deps
+ version: 2.0.3
+ devDependencies:
+ '@radix-ui/react-scroll-area':
+ specifier: catalog:frontend
+ version: 1.2.12(@types/react-dom@19.2.3(@types/react@19.2.15))(@types/react@19.2.15)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)
+ '@radix-ui/react-separator':
+ specifier: catalog:frontend
+ version: 1.1.10(@types/react-dom@19.2.3(@types/react@19.2.15))(@types/react@19.2.15)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)
+ '@radix-ui/react-slot':
+ specifier: catalog:frontend
+ version: 1.3.0(@types/react@19.2.15)(react@19.2.6)
+ '@radix-ui/react-tabs':
+ specifier: catalog:frontend
+ version: 1.1.15(@types/react-dom@19.2.3(@types/react@19.2.15))(@types/react@19.2.15)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)
+ '@storybook/addon-a11y':
+ specifier: catalog:frontend
+ version: 10.4.6(storybook@10.4.6(@testing-library/dom@10.4.1)(@types/react@19.2.15)(react-dom@19.2.6(react@19.2.6))(react@19.2.6))
+ '@storybook/addon-docs':
+ specifier: catalog:frontend
+ version: 10.4.6(@types/react-dom@19.2.3(@types/react@19.2.15))(@types/react@19.2.15)(esbuild@0.28.0)(rollup@4.60.3)(storybook@10.4.6(@testing-library/dom@10.4.1)(@types/react@19.2.15)(react-dom@19.2.6(react@19.2.6))(react@19.2.6))(vite@8.0.14(@types/node@25.9.1)(esbuild@0.28.0)(jiti@2.7.0)(terser@5.47.1)(tsx@4.22.3)(yaml@2.8.4))
+ '@storybook/react-vite':
+ specifier: catalog:frontend
+ version: 10.4.6(@types/react-dom@19.2.3(@types/react@19.2.15))(@types/react@19.2.15)(esbuild@0.28.0)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)(rollup@4.60.3)(storybook@10.4.6(@testing-library/dom@10.4.1)(@types/react@19.2.15)(react-dom@19.2.6(react@19.2.6))(react@19.2.6))(typescript@6.0.3)(vite@8.0.14(@types/node@25.9.1)(esbuild@0.28.0)(jiti@2.7.0)(terser@5.47.1)(tsx@4.22.3)(yaml@2.8.4))
+ '@tailwindcss/postcss':
+ specifier: catalog:frontend
+ version: 4.3.1
+ '@tailwindcss/vite':
+ specifier: catalog:frontend
+ version: 4.3.1(vite@8.0.14(@types/node@25.9.1)(esbuild@0.28.0)(jiti@2.7.0)(terser@5.47.1)(tsx@4.22.3)(yaml@2.8.4))
+ '@types/react':
+ specifier: catalog:types
+ version: 19.2.15
+ '@types/react-dom':
+ specifier: catalog:types
+ version: 19.2.3(@types/react@19.2.15)
+ class-variance-authority:
+ specifier: catalog:frontend
+ version: 0.7.1
+ clsx:
+ specifier: catalog:frontend
+ version: 2.1.1
+ get-port-please:
+ specifier: catalog:deps
+ version: 3.2.0
+ h3:
+ specifier: catalog:deps
+ version: 2.0.1-rc.22(crossws@0.4.5(srvx@0.11.15))
+ lucide-react:
+ specifier: catalog:frontend
+ version: 1.20.0(react@19.2.6)
+ next:
+ specifier: catalog:frontend
+ version: 16.2.6(@babel/core@7.29.0)(@playwright/test@1.60.0)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)
+ react:
+ specifier: catalog:frontend
+ version: 19.2.6
+ react-dom:
+ specifier: catalog:frontend
+ version: 19.2.6(react@19.2.6)
+ storybook:
+ specifier: catalog:frontend
+ version: 10.4.6(@testing-library/dom@10.4.1)(@types/react@19.2.15)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)
+ tailwind-merge:
+ specifier: catalog:frontend
+ version: 3.6.0
+ tailwindcss:
+ specifier: catalog:frontend
+ version: 4.3.1
+ tsdown:
+ specifier: catalog:build
+ version: 0.22.0(oxc-resolver@11.21.3)(tsx@4.22.3)(typescript@6.0.3)
+ tw-animate-css:
+ specifier: catalog:frontend
+ version: 1.4.0
+ vitest:
+ specifier: catalog:testing
+ version: 4.1.7(@types/node@25.9.1)(vite@8.0.14(@types/node@25.9.1)(esbuild@0.28.0)(jiti@2.7.0)(terser@5.47.1)(tsx@4.22.3)(yaml@2.8.4))
+ ws:
+ specifier: catalog:deps
+ version: 8.21.0
+
packages:
'@adobe/css-tools@4.5.0':
resolution: {integrity: sha512-6OzddxPio9UiWTCemp4N8cYLV2ZN1ncRnV1cVGtve7dhPOtRkleRyx32GQCYSwDYgaHU3USMm84tNsvKzRCa1Q==}
+ '@alloc/quick-lru@5.2.0':
+ resolution: {integrity: sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==}
+ engines: {node: '>=10'}
+
'@antfu/eslint-config@9.0.0':
resolution: {integrity: sha512-8aQW0UWHoNMdVxTfzs1+w10t26plsc9oFs8YhCyCtST5nnANJe/VAjqvR3hYI1l3PHBeo4tjVMg8wuu6g3OLlA==}
hasBin: true
@@ -1477,6 +1617,15 @@ packages:
resolution: {integrity: sha512-wgm9Ehl2jpeqP3zw/7mo3kRHFp5MEDhqAdwy1fTGkHAwnkGOVsgpvQhL8B5n1qlb01jV3n/bI0ZfZp5lWA1k4w==}
engines: {node: '>=18.0.0'}
+ '@joshwooding/vite-plugin-react-docgen-typescript@0.7.0':
+ resolution: {integrity: sha512-qvsTEwEFefhdirGOPnu9Wp6ChfIwy2dBCRuETU3uE+4cC+PFoxMSiiEhxk4lOluA34eARHA0OxqsEUYDqRMgeQ==}
+ peerDependencies:
+ typescript: '>= 4.3.x'
+ vite: ^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0
+ peerDependenciesMeta:
+ typescript:
+ optional: true
+
'@jridgewell/gen-mapping@0.3.13':
resolution: {integrity: sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==}
@@ -1507,6 +1656,12 @@ packages:
engines: {node: '>=18'}
hasBin: true
+ '@mdx-js/react@3.1.1':
+ resolution: {integrity: sha512-f++rKLQgUVYDAtECQ6fn/is15GkEH9+nZPM3MS0RcxVqoTfawHvDlSCH7JbMhAM6uJ32v3eXLvLmLvjGu7PTQw==}
+ peerDependencies:
+ '@types/react': '>=16'
+ react: '>=16'
+
'@mermaid-js/mermaid-mindmap@9.3.0':
resolution: {integrity: sha512-IhtYSVBBRYviH1Ehu8gk69pMDF8DSRqXBRDMWrEfHoaMruHeaP2DXA3PBnuwsMaCdPQhlUUcy/7DBLAEIXvCAw==}
@@ -1523,12 +1678,6 @@ packages:
'@cfworker/json-schema':
optional: true
- '@napi-rs/wasm-runtime@1.1.4':
- resolution: {integrity: sha512-3NQNNgA1YSlJb/kMH1ildASP9HW7/7kYnRI2szWJaofaS1hWmbGI4H+d3+22aGzXXN9IJ+n+GiFVcGipJP18ow==}
- peerDependencies:
- '@emnapi/core': ^1.7.1
- '@emnapi/runtime': ^1.7.1
-
'@napi-rs/wasm-runtime@1.1.5':
resolution: {integrity: sha512-AWPoBRJ9tsnVhor4sjO7rkni+7p+2IAEFj6cx06UgP10jkQHqay/36uRV/bFkgrh18D9vb4cr8Q0Pthskgzy+Q==}
peerDependencies:
@@ -1634,10 +1783,6 @@ packages:
'@vitejs/devtools':
optional: true
- '@nuxt/kit@4.4.5':
- resolution: {integrity: sha512-J0BpoOomzd3iVZozYlZJ7AwAVliXRgeChZnAkQLfg8d0h/Q+aMK9kkHuhwFULASaRn5idiD4BIhOUz7/uoLbSw==}
- engines: {node: '>=18.12.0'}
-
'@nuxt/kit@4.4.6':
resolution: {integrity: sha512-AzsqBJeG7b3whIciyzkz4nBossEotM314KzKAptc8kH07ORBIR8Qh3QYKepo2YZwtxiDP2Y9aqzAztwpSEDHtw==}
engines: {node: '>=18.12.0'}
@@ -2588,11 +2733,183 @@ packages:
'@quansync/fs@1.0.0':
resolution: {integrity: sha512-4TJ3DFtlf1L5LDMaM6CanJ/0lckGNtJcMjQ1NAV6zDmA0tEHKZtxNKin8EgPaVX1YzljbxckyT2tJrpQKAtngQ==}
- '@rolldown/binding-android-arm64@1.0.0':
- resolution: {integrity: sha512-TWMZnRLMe63C2Lhyicviu7ZHaU4kxa6PS3rofvc9GmcvptzNN11BcfQ4Sl7MwTOsisQoa2keB/EBdNCAnUo8vA==}
- engines: {node: ^20.19.0 || >=22.12.0}
- cpu: [arm64]
- os: [android]
+ '@radix-ui/number@1.1.2':
+ resolution: {integrity: sha512-ceTwaxc4I5IOi97DgCotl3pqiyRGvffcc0oOsE2dQYaJOFIDsDt4VWG6xEbg1QePv9QWausCEIppud/tJ1wNig==}
+
+ '@radix-ui/primitive@1.1.4':
+ resolution: {integrity: sha512-7AdCK9PQyiljKoBDbN8OuctCbd/esdwZPQ8RtOE3SsyQtUpiPb+ND75q0jEhC1m1ecBI0MFNeLJvwIh9iKHRcQ==}
+
+ '@radix-ui/react-collection@1.1.10':
+ resolution: {integrity: sha512-IVVz4EvBcKjrzKgof714qDnz/SzQAkLA2Emh5edlHbgcE6fNd3Un6CJLlaYcnm8N4JmAtzQgse4dOKxcD2yc9g==}
+ peerDependencies:
+ '@types/react': '*'
+ '@types/react-dom': '*'
+ react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ peerDependenciesMeta:
+ '@types/react':
+ optional: true
+ '@types/react-dom':
+ optional: true
+
+ '@radix-ui/react-compose-refs@1.1.3':
+ resolution: {integrity: sha512-rYOP8OMnuuPMQF1uhPVlGNcCDlkokKqGFE3JcxFViIkAXP7EvFWUliJAstrapypaBLJNHbZL6jGhbVDGTwmVhA==}
+ peerDependencies:
+ '@types/react': '*'
+ react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ peerDependenciesMeta:
+ '@types/react':
+ optional: true
+
+ '@radix-ui/react-context@1.1.4':
+ resolution: {integrity: sha512-QwH4PO5urrbO+FaGd5Aglg+YJgWTyyuZ3g/6mKvsqraLkglDdckw9JafgL5McL5VEJ6EPNduPaT3ZE9BttDAqg==}
+ peerDependencies:
+ '@types/react': '*'
+ react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ peerDependenciesMeta:
+ '@types/react':
+ optional: true
+
+ '@radix-ui/react-direction@1.1.2':
+ resolution: {integrity: sha512-C3vFhbyi4SW3PmbAi6Awpu4OzJtd0MxGurvSsYtr7p7nM8RNB3VAF3CUmnp2j50knpkrRcB7+ycVXzgLgF6yNA==}
+ peerDependencies:
+ '@types/react': '*'
+ react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ peerDependenciesMeta:
+ '@types/react':
+ optional: true
+
+ '@radix-ui/react-id@1.1.2':
+ resolution: {integrity: sha512-orBC88futVpqCmhX1p4cvquNHsELQ+w+vBJnuj3ftETI5bJb0bZn3Tqu3SWN2IOcPycTnMGnhwoermvISt72sA==}
+ peerDependencies:
+ '@types/react': '*'
+ react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ peerDependenciesMeta:
+ '@types/react':
+ optional: true
+
+ '@radix-ui/react-presence@1.1.6':
+ resolution: {integrity: sha512-zdTk4PlUO0E18HnZ3wYbW0KkJJxWCdiNYp6g6X1PtONFhxVkg01vliTJAmwIszU6mHiyBOoW9P0rAugl5/hULQ==}
+ peerDependencies:
+ '@types/react': '*'
+ '@types/react-dom': '*'
+ react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ peerDependenciesMeta:
+ '@types/react':
+ optional: true
+ '@types/react-dom':
+ optional: true
+
+ '@radix-ui/react-primitive@2.1.6':
+ resolution: {integrity: sha512-wetd0QI77DbvrPpTAvH1SqOxsYF2wZe5TNxqwOd5Ty4XDpV3dpV0s8K/1MGMJBeY5o7lg8ub5VIt1Ub+yVen6g==}
+ peerDependencies:
+ '@types/react': '*'
+ '@types/react-dom': '*'
+ react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ peerDependenciesMeta:
+ '@types/react':
+ optional: true
+ '@types/react-dom':
+ optional: true
+
+ '@radix-ui/react-roving-focus@1.1.13':
+ resolution: {integrity: sha512-9gkwneI0guf8JDmrFxPjJF6Ozzgioyw+/lonYNCwefS9ZHA05er0BVHiXr+LbWGHxUfczvMY6G1oiZZi1VzjRw==}
+ peerDependencies:
+ '@types/react': '*'
+ '@types/react-dom': '*'
+ react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ peerDependenciesMeta:
+ '@types/react':
+ optional: true
+ '@types/react-dom':
+ optional: true
+
+ '@radix-ui/react-scroll-area@1.2.12':
+ resolution: {integrity: sha512-xuafVzQiTCLsyEjakowTdG3OgTXsmO7IdCiO77otIa+z44xoLNs9Do5eg7POFumIOCjtG6djfm6RKUKpUa/csA==}
+ peerDependencies:
+ '@types/react': '*'
+ '@types/react-dom': '*'
+ react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ peerDependenciesMeta:
+ '@types/react':
+ optional: true
+ '@types/react-dom':
+ optional: true
+
+ '@radix-ui/react-separator@1.1.10':
+ resolution: {integrity: sha512-Y6K6jLQCVfCnTL2MEtGxDLffkhNfEfHsEg3Wa8JU+IWdn3EWbLXd3OuOfQRN7p/W/cUce1WyTk3QeuAoDBzN9g==}
+ peerDependencies:
+ '@types/react': '*'
+ '@types/react-dom': '*'
+ react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ peerDependenciesMeta:
+ '@types/react':
+ optional: true
+ '@types/react-dom':
+ optional: true
+
+ '@radix-ui/react-slot@1.3.0':
+ resolution: {integrity: sha512-MojKku4U/miO8Av4Dkb+ctMAQx7JmY96LmtDQlAarCRtd7rN52QCSzBF+XAvr5S6coSVj9HEPBgHAHKEJVk/WA==}
+ peerDependencies:
+ '@types/react': '*'
+ react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ peerDependenciesMeta:
+ '@types/react':
+ optional: true
+
+ '@radix-ui/react-tabs@1.1.15':
+ resolution: {integrity: sha512-kxc9gI6/HfcU4nfMMVS3AmQK414kbU1IE6UCJmMmxjhO3cRPXOyYnmvyKD+ODt7q56nRq9l7Wovi6uaGwKgMlg==}
+ peerDependencies:
+ '@types/react': '*'
+ '@types/react-dom': '*'
+ react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ peerDependenciesMeta:
+ '@types/react':
+ optional: true
+ '@types/react-dom':
+ optional: true
+
+ '@radix-ui/react-use-callback-ref@1.1.2':
+ resolution: {integrity: sha512-xCso9j1/u8sEgP1RNHjFrXJLApL8LiqOkI1R4ywuN00rxWdYg4oQXuwKLS3i0j5NWLromUD27/4nlxj2UFVvIw==}
+ peerDependencies:
+ '@types/react': '*'
+ react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ peerDependenciesMeta:
+ '@types/react':
+ optional: true
+
+ '@radix-ui/react-use-controllable-state@1.2.3':
+ resolution: {integrity: sha512-PLzC90MS+ReootmjC597dvopoelpZ8Q61HJkDXZSExitIq7PL55vHNnesAHwguHK0aPfBnpdNzQtv1uliaqQrA==}
+ peerDependencies:
+ '@types/react': '*'
+ react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ peerDependenciesMeta:
+ '@types/react':
+ optional: true
+
+ '@radix-ui/react-use-effect-event@0.0.3':
+ resolution: {integrity: sha512-6c8ZqvPTWILEKnyVkP53EGRCcpnJiKTC21sS/6R1GF5xKyHJJWQEPfkqlcgUkdRQivd6tb23abUwe4ngWmY0JA==}
+ peerDependencies:
+ '@types/react': '*'
+ react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ peerDependenciesMeta:
+ '@types/react':
+ optional: true
+
+ '@radix-ui/react-use-layout-effect@1.1.2':
+ resolution: {integrity: sha512-jrBWOxZITuGcnjRCM2t2U5ZPkCLxD+Ym6DjfssS5haTj2iiak/DOb64JeN6OdLfLgptb6/e2kKR+ZuTrGoZTPA==}
+ peerDependencies:
+ '@types/react': '*'
+ react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ peerDependenciesMeta:
+ '@types/react':
+ optional: true
'@rolldown/binding-android-arm64@1.0.2':
resolution: {integrity: sha512-ZS4D1JPGn/MYQN/SYDWftIE/nVsM8j/AFOYEzAoOE2O3NktQOZru+/vYXGbR/qtdLdIfGCP0lcoJiYVzsEz+iQ==}
@@ -2600,61 +2917,30 @@ packages:
cpu: [arm64]
os: [android]
- '@rolldown/binding-darwin-arm64@1.0.0':
- resolution: {integrity: sha512-6XcD+8k0gPVItNagEw78/qqcBDwKcwDYS8V2hRmVsfUSIrd8cWe/CBvRDI5toqFyPfj+FJr6t8U6Xj2P2prEew==}
- engines: {node: ^20.19.0 || >=22.12.0}
- cpu: [arm64]
- os: [darwin]
-
'@rolldown/binding-darwin-arm64@1.0.2':
resolution: {integrity: sha512-vdFA9+C/rekyGce7WqHs/xoT0ioZEWaOFyZLIV1mEeNFaFDUQrPIo8Vs2GvJ6eetb3rzDUtUBgzto3ExpXJB3w==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [arm64]
os: [darwin]
- '@rolldown/binding-darwin-x64@1.0.0':
- resolution: {integrity: sha512-iN/tWVXRQDWvmZlKdceP1Dwug9GDpEymhb9p4xnEe6zvCg5lFmzVljl+1qR1NVx3yfGpr2Na+CuLmv5IU8uzfQ==}
- engines: {node: ^20.19.0 || >=22.12.0}
- cpu: [x64]
- os: [darwin]
-
'@rolldown/binding-darwin-x64@1.0.2':
resolution: {integrity: sha512-BewSOwTHazv77DTYiAZXSqqKZ4KP/KonFisDMVU7PImxoWfB2aepnPhd2E4SWz3zDzYgDNbs6jBmTdgNnF02GA==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [x64]
os: [darwin]
- '@rolldown/binding-freebsd-x64@1.0.0':
- resolution: {integrity: sha512-jjQMDvvwSOuhOwMszD/klSOjyWMM3zI64hWTj9KT5x4MxRbZAf+7vLQ6qouRhtsLVFHr3f0ILaJAfgENPiQdAQ==}
- engines: {node: ^20.19.0 || >=22.12.0}
- cpu: [x64]
- os: [freebsd]
-
'@rolldown/binding-freebsd-x64@1.0.2':
resolution: {integrity: sha512-m41o7M0YWtUdqk61Tb+jnKb2rN++iRdIASlExkUoKfIAH30DOHCB8fVLzSUpbWHHU8esmEioY62PxzexE8MBuA==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [x64]
os: [freebsd]
- '@rolldown/binding-linux-arm-gnueabihf@1.0.0':
- resolution: {integrity: sha512-d//Dtg2x6/m3mbV64yUGNnDGNZaDGRpDLLNGerHQUVObuNaIQaaDp25yUiqGXtHEXX+NP2d0wAlmKgpYgIAJ2A==}
- engines: {node: ^20.19.0 || >=22.12.0}
- cpu: [arm]
- os: [linux]
-
'@rolldown/binding-linux-arm-gnueabihf@1.0.2':
resolution: {integrity: sha512-jcojB9H7W/jS29pMKWAK1N+fU99vXodHDTatS3b3y/XSOCiHo0kkA74pL3jJmkoQtYpOCxDvaKs1fo2Ij/1X5w==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [arm]
os: [linux]
- '@rolldown/binding-linux-arm64-gnu@1.0.0':
- resolution: {integrity: sha512-n7Ofp0mx+aB2cC+Sdy5YtMnXtY9lchnHbY+3Yt0uq9JsWQExf4f5Whu0tK0R8Jdc9S6RchTHjIFY7uc92puOVQ==}
- engines: {node: ^20.19.0 || >=22.12.0}
- cpu: [arm64]
- os: [linux]
- libc: [glibc]
-
'@rolldown/binding-linux-arm64-gnu@1.0.2':
resolution: {integrity: sha512-1jn6qDU5iiOgFgygDzKUuKP0maTi0/f1+sBLgvij/76C77Nm3ts6ufz9Bjg5q5dduxiUIxtq86JIoBvo1xQ4Ig==}
engines: {node: ^20.19.0 || >=22.12.0}
@@ -2662,13 +2948,6 @@ packages:
os: [linux]
libc: [glibc]
- '@rolldown/binding-linux-arm64-musl@1.0.0':
- resolution: {integrity: sha512-EIVjy2cgd7uuMMo94FVkBp7F6DhcZAUwNURkSG3RwUmvAXR6s0ISxM81U+IydcZByPG0pZIHsf1b6kTxoFDgJA==}
- engines: {node: ^20.19.0 || >=22.12.0}
- cpu: [arm64]
- os: [linux]
- libc: [musl]
-
'@rolldown/binding-linux-arm64-musl@1.0.2':
resolution: {integrity: sha512-QVLO/czFMdoMFSqlX3bcswcJNm/23r+qoa/jgtmFc/qEp6/jXmIkDjF/XIo8dPfGaiwy1xfQn8o77L79GeXFgw==}
engines: {node: ^20.19.0 || >=22.12.0}
@@ -2676,13 +2955,6 @@ packages:
os: [linux]
libc: [musl]
- '@rolldown/binding-linux-ppc64-gnu@1.0.0':
- resolution: {integrity: sha512-JEwwOPcwTLAcpDQlqSmjEmfs63xJnSiUNIGvLcDLUHCWK4XowpS/7c7tUsUH6uT/ct6bMUTdXKfI8967FYj6mg==}
- engines: {node: ^20.19.0 || >=22.12.0}
- cpu: [ppc64]
- os: [linux]
- libc: [glibc]
-
'@rolldown/binding-linux-ppc64-gnu@1.0.2':
resolution: {integrity: sha512-hgO5Abm0w5UL6FEa2iFnZqo2KlK7TQ5QhV5x09hujBf7t5KzHQ1VmfPuTpqRy/rNlSxua3eWH374xxiVrP+lcA==}
engines: {node: ^20.19.0 || >=22.12.0}
@@ -2690,13 +2962,6 @@ packages:
os: [linux]
libc: [glibc]
- '@rolldown/binding-linux-s390x-gnu@1.0.0':
- resolution: {integrity: sha512-0wjCFhLrihtAubnT9iA0N++0pSV0z5Hg7tNGdNJ4RFaINceHadoF+kiFGyY1qSSNVIAZtLotG8Ju1bgDPkjnFA==}
- engines: {node: ^20.19.0 || >=22.12.0}
- cpu: [s390x]
- os: [linux]
- libc: [glibc]
-
'@rolldown/binding-linux-s390x-gnu@1.0.2':
resolution: {integrity: sha512-fy8rXxuYEu602abC8MUNaPjYLIFzReOaEIEMKMUa0rFEUxNpVXhs15KSSQ4qlqSaM7B6rcj9rDZgADh/IGDzLQ==}
engines: {node: ^20.19.0 || >=22.12.0}
@@ -2704,13 +2969,6 @@ packages:
os: [linux]
libc: [glibc]
- '@rolldown/binding-linux-x64-gnu@1.0.0':
- resolution: {integrity: sha512-Dfn7iak9BcMMePxcoJfpSbWqnEyrp/dRF63/8qW/eHBdOZov6x5aShLLEYGYdIeSJ6vMLK/XCVB+lGIxm41bQA==}
- engines: {node: ^20.19.0 || >=22.12.0}
- cpu: [x64]
- os: [linux]
- libc: [glibc]
-
'@rolldown/binding-linux-x64-gnu@1.0.2':
resolution: {integrity: sha512-0+bOkiQ779+r1WpoHOWHqncvyySci0vKph+myNDYb+im6meJAzHQXay6oEgnkHuUGouM1LKTZwqKpBow6Kj7CQ==}
engines: {node: ^20.19.0 || >=22.12.0}
@@ -2718,13 +2976,6 @@ packages:
os: [linux]
libc: [glibc]
- '@rolldown/binding-linux-x64-musl@1.0.0':
- resolution: {integrity: sha512-5/utzzDmD/pD/bmuaUcbTf/sZYy0aztwIVlfpoW1fTjCZ0BaPOMVWGZL1zvgxyi7ZIVYWlxKONHmSbHuiOh8Jw==}
- engines: {node: ^20.19.0 || >=22.12.0}
- cpu: [x64]
- os: [linux]
- libc: [musl]
-
'@rolldown/binding-linux-x64-musl@1.0.2':
resolution: {integrity: sha512-mjSkrzZK5Qsl0a9d1JgILOiuZOSDTVdKENcSXBoqbzSrspLR/4/IRVDo5wd2GgZjNss/viBFJdeq+j7qH2nypw==}
engines: {node: ^20.19.0 || >=22.12.0}
@@ -2732,58 +2983,29 @@ packages:
os: [linux]
libc: [musl]
- '@rolldown/binding-openharmony-arm64@1.0.0':
- resolution: {integrity: sha512-ouJs8VcUomfLfpbUECqFMRqdV4x6aeAK3MA4m6vTrJJjKyWTV5KnxZx7Jd9G+GlDaQQxubcba00x16OyJ1meig==}
- engines: {node: ^20.19.0 || >=22.12.0}
- cpu: [arm64]
- os: [openharmony]
-
'@rolldown/binding-openharmony-arm64@1.0.2':
resolution: {integrity: sha512-1v5vHasdfQAZoEHakBV72LIFAC9JjnymsiKxp+GEr/ma3+NJCPSaYK+qavInOovJkgwFrs7GccX2d6IgDA3Z5w==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [arm64]
os: [openharmony]
- '@rolldown/binding-wasm32-wasi@1.0.0':
- resolution: {integrity: sha512-E+oHKGiDA+lsKMmFtffDDw91EryDT7uJocrIuCHqhm6bCTM6xFK+3gaCkYOHfPwQr0cCNarSM2xaELoQDz9jJg==}
- engines: {node: ^20.19.0 || >=22.12.0}
- cpu: [wasm32]
-
'@rolldown/binding-wasm32-wasi@1.0.2':
resolution: {integrity: sha512-mb1VobWn6NheziTk5/WEaR6AKVbrwT5sOi6C7zk3gy/pD1qtJfU1j4PgTo2NJnOtbL9Dl3Aeei8w9jJ7qC2jZQ==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [wasm32]
- '@rolldown/binding-win32-arm64-msvc@1.0.0':
- resolution: {integrity: sha512-yYK02n8Rngo+gbm1y6G0+7jk1sJ/2Wt7K0me0Y7k/ErBpyf+LJ2gFpqWVTcRV1rUepBlQRmpgWkTQCiiwrK0Ow==}
- engines: {node: ^20.19.0 || >=22.12.0}
- cpu: [arm64]
- os: [win32]
-
'@rolldown/binding-win32-arm64-msvc@1.0.2':
resolution: {integrity: sha512-SqKonF56vA/L2yHwHYcEp2P34URpOZ7d1fS635cTkpDnUtEGdUbhI6NzsPdqeSWvAAeGDrxjWjNmibDIdFf9/A==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [arm64]
os: [win32]
- '@rolldown/binding-win32-x64-msvc@1.0.0':
- resolution: {integrity: sha512-14bpChMahXRRXiTwahSl+zzHPW6qQTXtkMuJBFlbo+pqSAews2d4BdCSHfrJ/MBsCZtpmTafsY+1QhBzitcmdg==}
- engines: {node: ^20.19.0 || >=22.12.0}
- cpu: [x64]
- os: [win32]
-
'@rolldown/binding-win32-x64-msvc@1.0.2':
resolution: {integrity: sha512-v7qRI7gXLRINcOGXt+7YmAZ6iFuyZVMIoXAxhd8oP+DR9dLfL9GfNIx7PLMxmhZdvq8waUJBQiWN9EKNy+TRBQ==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [x64]
os: [win32]
- '@rolldown/pluginutils@1.0.0':
- resolution: {integrity: sha512-aKs/3GSWyV0mrhNmt/96/Z3yczC3yvrzYATCiCXQebBsGyYzjNdUphRVLeJQ67ySKVXRfMxt2lm12pmXvbPFQQ==}
-
- '@rolldown/pluginutils@1.0.0-rc.13':
- resolution: {integrity: sha512-3ngTAv6F/Py35BsYbeeLeecvhMKdsKm4AoOETVhAA+Qc8nrA2I0kF7oa93mE9qnIurngOSpMnQ0x2nQY2FPviA==}
-
'@rolldown/pluginutils@1.0.1':
resolution: {integrity: sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw==}
@@ -3049,6 +3271,20 @@ packages:
'@standard-schema/spec@1.1.0':
resolution: {integrity: sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==}
+ '@storybook/addon-a11y@10.4.6':
+ resolution: {integrity: sha512-XCJy+f0DFOiCgUU9knRDlLDxVFI+AAQ3/wE/NF85zB9iDPPS2DwkSN+mas3zDgHt66zhN8Cq3/UiyCDUweV9Zw==}
+ peerDependencies:
+ storybook: ^10.4.6
+
+ '@storybook/addon-docs@10.4.6':
+ resolution: {integrity: sha512-aWAfP5JMiT5a3zBJizwroCRzOCqZwDTJmvsYvwMD3ilIEa/kT1vhf6Xrbk4XIPhDwbh8Hpb/Gfnka1xBYEISWg==}
+ peerDependencies:
+ '@types/react': ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0
+ storybook: ^10.4.6
+ peerDependenciesMeta:
+ '@types/react':
+ optional: true
+
'@storybook/builder-vite@10.4.6':
resolution: {integrity: sha512-BHBtD81HiXUiDQz/CaFynLtWmm7AFUQn8VnXuHipZ8KlnUANopa4yqdVuy/Gwz8ub254uFI5NMZsW/KlgWNgNg==}
peerDependencies:
@@ -3093,6 +3329,45 @@ packages:
react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0
react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0
+ '@storybook/react-dom-shim@10.4.6':
+ resolution: {integrity: sha512-iGNmKzrq9vgl2PDrYAnZKI+yvac3Ym+lJXXuQaqlFRS23zA5MNm4EBX+rAG7WulqchoK6NaZ0KQOs2mAgEpTMg==}
+ peerDependencies:
+ '@types/react': ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0
+ '@types/react-dom': ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0
+ react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0
+ react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0
+ storybook: ^10.4.6
+ peerDependenciesMeta:
+ '@types/react':
+ optional: true
+ '@types/react-dom':
+ optional: true
+
+ '@storybook/react-vite@10.4.6':
+ resolution: {integrity: sha512-0arEQtybqGYXHbXpTot+Wv9YtG+V5Vp43QayXavPKQ20M8mpEzhyCPKd0EhqMGSC1Z1UEt0hm365WUBhI9LfKA==}
+ peerDependencies:
+ react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0
+ react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0
+ storybook: ^10.4.6
+ vite: ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0
+
+ '@storybook/react@10.4.6':
+ resolution: {integrity: sha512-9Y7YecrVFe1/01KYjfOLxVqTg2Aq+IO6TEv6sC2U0PfD0AWCSCmQ91QqgBpN/XW4aFFWoiZNinyXMUlU8zxy2w==}
+ peerDependencies:
+ '@types/react': ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0
+ '@types/react-dom': ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0
+ react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0
+ react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0
+ storybook: ^10.4.6
+ typescript: '>= 4.9.x'
+ peerDependenciesMeta:
+ '@types/react':
+ optional: true
+ '@types/react-dom':
+ optional: true
+ typescript:
+ optional: true
+
'@stylistic/eslint-plugin@5.10.0':
resolution: {integrity: sha512-nPK52ZHvot8Ju/0A4ucSX1dcPV2/1clx0kLcH5wDmrE4naKso7TUC/voUyU1O9OTKTrR6MYip6LP0ogEMQ9jPQ==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
@@ -3102,6 +3377,103 @@ packages:
'@swc/helpers@0.5.15':
resolution: {integrity: sha512-JQ5TuMi45Owi4/BIMAJBoSQoOJu12oOk/gADqlcUL9JEdHB8vyjUSsxqeNXnmXHjYKMi2WcYtezGEEhqUI/E2g==}
+ '@tailwindcss/node@4.3.1':
+ resolution: {integrity: sha512-6NDaqRoAMSXD1mr/RXu0HBvNE9a2n5tHPsxu9XHLws8o4Twes5rBM2205SUUiJ9goAtadrN6xTGX0UDEwp/N4A==}
+
+ '@tailwindcss/oxide-android-arm64@4.3.1':
+ resolution: {integrity: sha512-SVlyf61g374l5cHyg8x9kf5xmLcOaxvOTsbsqDnSsDJaKOEFZ7GCvi84VAVGpxojYOs1+3K6M0UjXfqPU8vmOQ==}
+ engines: {node: '>= 20'}
+ cpu: [arm64]
+ os: [android]
+
+ '@tailwindcss/oxide-darwin-arm64@4.3.1':
+ resolution: {integrity: sha512-hVnWLwv+e/l7c4WKyVtHVrIPvYdqWHjRB3MDIqARynzFtnQg85kmQEFCbV9Ja0VVx4xXTIiDWY60Y7iz/iNoDA==}
+ engines: {node: '>= 20'}
+ cpu: [arm64]
+ os: [darwin]
+
+ '@tailwindcss/oxide-darwin-x64@4.3.1':
+ resolution: {integrity: sha512-Cf7abu0WVgbhU7ANgPUnSAvm7nCvMweusHb8FnaHlLfv/Caq4GYaEZg7ZImzzmjx4lIAfuS8q+eLIS7A7IzxIg==}
+ engines: {node: '>= 20'}
+ cpu: [x64]
+ os: [darwin]
+
+ '@tailwindcss/oxide-freebsd-x64@4.3.1':
+ resolution: {integrity: sha512-ZZqzX2Y+GXtXXfqSfpJhDm60OoZfvLHLCgm+J7NVqgHHJjG/m9ugZI77RwTsVd4fnBJuCFP6Ae6kTJb71UdS8g==}
+ engines: {node: '>= 20'}
+ cpu: [x64]
+ os: [freebsd]
+
+ '@tailwindcss/oxide-linux-arm-gnueabihf@4.3.1':
+ resolution: {integrity: sha512-/Ah/xik0LaMYfv9DZ0S/t4pBlBNYOcqtRwusjgovHkvT8ixueWCLyJjsaF5kQIckjb4IT8Q6K6p/iPmZMixYgg==}
+ engines: {node: '>= 20'}
+ cpu: [arm]
+ os: [linux]
+
+ '@tailwindcss/oxide-linux-arm64-gnu@4.3.1':
+ resolution: {integrity: sha512-gqdFoVJlw444GvpnheZLHmvTzSxI/cOUUh2KSNejQjTcYkW062SVD+En0rUgD+QV91bz1XGIGtt1HJd48xUGbQ==}
+ engines: {node: '>= 20'}
+ cpu: [arm64]
+ os: [linux]
+ libc: [glibc]
+
+ '@tailwindcss/oxide-linux-arm64-musl@4.3.1':
+ resolution: {integrity: sha512-Bwv9KwOvE0VKa86xPFif9b9c3Y1NxOV1P0gLti/IYaWEsQYZXDlxfGEtA8mdDZ7SG3wyNXAWYT5SIn3giL57oA==}
+ engines: {node: '>= 20'}
+ cpu: [arm64]
+ os: [linux]
+ libc: [musl]
+
+ '@tailwindcss/oxide-linux-x64-gnu@4.3.1':
+ resolution: {integrity: sha512-Ymi8O8T15HYQdOUWUtTI6ldN0neHP85FC+Qz32xTcZ7iJXtem/x8ITev0o1e9e5rkqj4lONZfTRLvkmin1+tKg==}
+ engines: {node: '>= 20'}
+ cpu: [x64]
+ os: [linux]
+ libc: [glibc]
+
+ '@tailwindcss/oxide-linux-x64-musl@4.3.1':
+ resolution: {integrity: sha512-M+P/91qJ6uILLw4k2G93GMDRAXj61SMvFQYt39AqvUqYgExXpLL5aepfns7sj4HiAQeolirQF9E0lzRvdf4zPQ==}
+ engines: {node: '>= 20'}
+ cpu: [x64]
+ os: [linux]
+ libc: [musl]
+
+ '@tailwindcss/oxide-wasm32-wasi@4.3.1':
+ resolution: {integrity: sha512-zsM8uOeqvVGHsAXsJxsT28ttosFahLJKCLOTUBqRAtKnVgGSRitds9T432QiT8b77Yga7JIBkulIRRlJPtYhRA==}
+ engines: {node: '>=14.0.0'}
+ cpu: [wasm32]
+ bundledDependencies:
+ - '@napi-rs/wasm-runtime'
+ - '@emnapi/core'
+ - '@emnapi/runtime'
+ - '@tybys/wasm-util'
+ - '@emnapi/wasi-threads'
+ - tslib
+
+ '@tailwindcss/oxide-win32-arm64-msvc@4.3.1':
+ resolution: {integrity: sha512-aiNvSq9BsVk8V513lDKlrCFAgf8qBMPZTpgEhInL+NwQqs97mYmupVMrPrgBBSL8Pv/0zXu9MrMF9rMun1ZeNg==}
+ engines: {node: '>= 20'}
+ cpu: [arm64]
+ os: [win32]
+
+ '@tailwindcss/oxide-win32-x64-msvc@4.3.1':
+ resolution: {integrity: sha512-xDEyu1rg290472FEGaKHnzyDyh5QH+AlWvsU5hMoMtPpzmKlRI0jaYKCgSHDYtaQWZOYbMaduSyCwFwY4n1HmA==}
+ engines: {node: '>= 20'}
+ cpu: [x64]
+ os: [win32]
+
+ '@tailwindcss/oxide@4.3.1':
+ resolution: {integrity: sha512-yVPyo8RNkabVr3O2EhHEE0Rewu7YKzc1DhIqfL46LKveFrmu9XbDazNOJY7/GRuvw1h6u3utWnR29H/p5JPlgA==}
+ engines: {node: '>= 20'}
+
+ '@tailwindcss/postcss@4.3.1':
+ resolution: {integrity: sha512-dNJuNbdEJT/SWRuXTYP1WSamelsz3ztkUsdtWQPjrexysrTpaEPM40P/71knXiXLYEojqPOEGitVLLpPMS5T6A==}
+
+ '@tailwindcss/vite@4.3.1':
+ resolution: {integrity: sha512-hItDHuIIlEV61R+faXu66s1K36aTurO/Qw0e45Vskz57gXl9pWOT6eg3zmcEui6CZXddbN7zd41bwmvag4JGwQ==}
+ peerDependencies:
+ vite: ^5.2.0 || ^6 || ^7 || ^8
+
'@testing-library/dom@10.4.1':
resolution: {integrity: sha512-o4PXJQidqJl82ckFaXUeoAW+XysPLauYI43Abki5hABd853iMhitooc6znOnczgbTYmEP6U6/y1ZyKAIsvMKGg==}
engines: {node: '>=18'}
@@ -3152,6 +3524,18 @@ packages:
'@types/aria-query@5.0.4':
resolution: {integrity: sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw==}
+ '@types/babel__core@7.20.5':
+ resolution: {integrity: sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==}
+
+ '@types/babel__generator@7.27.0':
+ resolution: {integrity: sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==}
+
+ '@types/babel__template@7.4.4':
+ resolution: {integrity: sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==}
+
+ '@types/babel__traverse@7.28.0':
+ resolution: {integrity: sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==}
+
'@types/chai@5.2.3':
resolution: {integrity: sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==}
@@ -3254,6 +3638,9 @@ packages:
'@types/deep-eql@4.0.2':
resolution: {integrity: sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==}
+ '@types/doctrine@0.0.9':
+ resolution: {integrity: sha512-eOIHzCUSH7SMfonMG1LsC2f8vxBFtho6NGBznK41R84YzPuvSBzrhEps33IsQiOW9+VL6NQ9DbjQJznk/S4uRA==}
+
'@types/esrecurse@4.3.1':
resolution: {integrity: sha512-xJBAbDifo5hpffDBuHl0Y8ywswbiAp/Wi7Y/GtAgSlZyIABppyurxVueOPE8LUQOxdlgi6Zqce7uoEpqNTeiUw==}
@@ -3290,12 +3677,12 @@ packages:
'@types/mdurl@2.0.0':
resolution: {integrity: sha512-RGdgjQUZba5p6QEFAVx2OGb8rQDL/cPRG7GiedRzMcJ1tYnUANBncjbSB1NRGwbvjcPeikRABz2nshyPk1bhWg==}
+ '@types/mdx@2.0.14':
+ resolution: {integrity: sha512-T48PeuJtvLosNTPVhfnIp3i/n3a4g4Bad7YCq5k64D4u7NwDrAotikQ+5+sjtUvBmxCMlbo3dVL+C2dP0rWHzg==}
+
'@types/ms@2.1.0':
resolution: {integrity: sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==}
- '@types/node@25.7.0':
- resolution: {integrity: sha512-z+pdZyxE+RTQE9AcboAZCb4otwcrvgHD+GlBpPgn0emDVt0ohrTMhAwlr2Wd9nZ+nihhYFxO2pThz3C5qSu2Eg==}
-
'@types/node@25.9.1':
resolution: {integrity: sha512-xfrlY7UD5rMJk3ZVJP8BNzS28J36YJg+xp+LPXV1TdWxr8uMH5A860QNxYDGQe/ylDSgjxE52Q9VnO7p75tJxg==}
@@ -3416,13 +3803,6 @@ packages:
vite: ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0
vue: ^3.0.0
- '@vitejs/plugin-vue@6.0.6':
- resolution: {integrity: sha512-u9HHgfrq3AjXlysn0eINFnWQOJQLO9WN6VprZ8FXl7A2bYisv3Hui9Ij+7QZ41F/WYWarHjwBbXtD7dKg3uxbg==}
- engines: {node: ^20.19.0 || >=22.12.0}
- peerDependencies:
- vite: ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0
- vue: ^3.2.25
-
'@vitejs/plugin-vue@6.0.7':
resolution: {integrity: sha512-km+p+XdSz9Sxm5rqUbqcSfZYaAniKxWBj1KURl+Jr7UaPvvX7BmaWMdP69I5rrFDeQGyxAG7NXdc57vz+snhWg==}
engines: {node: ^20.19.0 || >=22.12.0}
@@ -3466,9 +3846,6 @@ packages:
'@vitest/pretty-format@3.2.4':
resolution: {integrity: sha512-IVNZik8IVRJRTr9fxlitMKeJeXFFFN0JaB9PHPGQ8NKQbGpfjlTx9zO4RefN8gp7eqjNy8nyK3NZmBzOPeIxtA==}
- '@vitest/pretty-format@4.1.5':
- resolution: {integrity: sha512-7I3q6l5qr03dVfMX2wCo9FxwSJbPdwKjy2uu/YPpU3wfHvIL4QHwVRp57OfGrDFeUJ8/8QdfBKIV12FTtLn00g==}
-
'@vitest/pretty-format@4.1.7':
resolution: {integrity: sha512-umgCarTOYQWIaDMvGDRZij+6b9oVeLIyJzfN+AS88e0ZOU3QTgNNSTtjQOpcvWr3np1N0j4WgZj+sb3oYBDscw==}
@@ -3487,9 +3864,6 @@ packages:
'@vitest/utils@3.2.4':
resolution: {integrity: sha512-fB2V0JFrQSMsCo9HiSq3Ezpdv4iYaXRG1Sx8edX3MwxfyNn83mKiGzOcH+Fkxt4MHxr3y42fQi1oeAInqgX2QA==}
- '@vitest/utils@4.1.5':
- resolution: {integrity: sha512-76wdkrmfXfqGjueGgnb45ITPyUi1ycZ4IHgC2bhPDUfWHklY/q3MdLOAB+TF1e6xfl8NxNY0ZYaPCFNWSsw3Ug==}
-
'@vitest/utils@4.1.7':
resolution: {integrity: sha512-T532WBu791cBxJlCl6SO+J14l81DQx6uQHm1bQbmCDY7nqlEIgkza/UFnSBNaUtSf41unldDFjdOBYEQC4b5Hw==}
@@ -3684,10 +4058,6 @@ packages:
resolution: {integrity: sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==}
engines: {node: '>=12'}
- ansis@4.2.0:
- resolution: {integrity: sha512-HqZ5rWlFjGiV0tDm3UxxgNRqsOTniqoKZu0pIAfh7TZQMGuZK+hH0drySty0si0QXj1ieop4+SkSfPZBPPkHig==}
- engines: {node: '>=14'}
-
ansis@4.3.0:
resolution: {integrity: sha512-44mvgtPvohuU/70DdY5Oz2AIrLJ9k6/5x4KmoSvPwO+5Moijo0+N9D0fKbbYZQWP1hNm5CpOf+E01jhxG/r8xg==}
engines: {node: '>=14'}
@@ -3754,6 +4124,10 @@ packages:
peerDependencies:
postcss: ^8.1.0
+ axe-core@4.12.1:
+ resolution: {integrity: sha512-s7iGf5GaVMxEG0ENN9x+xTr7GFZCb1ZP/1uATUpCEK2X78nDB3RwbtFCo9pGAf9ru+VwoQ464DkaLEeRM08wJA==}
+ engines: {node: '>=4'}
+
b4a@1.8.1:
resolution: {integrity: sha512-aiqre1Nr0B/6DgE2N5vwTc+2/oQZ4Wh1t4NznYY4E00y8LCt6NqdRv81so00oo27D8MVKTpUa/MwUUtBLXCoDw==}
peerDependencies:
@@ -3957,6 +4331,9 @@ packages:
citty@0.2.2:
resolution: {integrity: sha512-+6vJA3L98yv+IdfKGZHBNiGW5KHn22e/JwID0Strsz8h4S/csAu/OuICwxrg44k5MRiZHWIo8XXuJgQTriRP4w==}
+ class-variance-authority@0.7.1:
+ resolution: {integrity: sha512-Ka+9Trutv7G8M6WT6SeiRWz792K5qEqIGEGzXKhAE6xOWAY6pPH8U+9IY3oCMv6kqTmLsv7Xh/2w2RigkePMsg==}
+
clean-regexp@1.0.0:
resolution: {integrity: sha512-GfisEZEJvzKrmGWkvfhgzcz/BllN1USeqD2V6tg14OAOgaCD2Z/PUEuxnAZ/nPvmaHRG7a8y77p1T/IRQ4D1Hw==}
engines: {node: '>=4'}
@@ -3968,6 +4345,10 @@ packages:
resolution: {integrity: sha512-k7ndgKhwoQveBL+/1tqGJYNz097I7WOvwbmmU2AR5+magtbjPWQTS1C5vzGkBC8Ym8UWRzfKUzUUqFLypY4Q+w==}
engines: {node: '>=20'}
+ clsx@2.1.1:
+ resolution: {integrity: sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==}
+ engines: {node: '>=6'}
+
cluster-key-slot@1.1.2:
resolution: {integrity: sha512-RMr0FhtfXemyinomL4hrWcYJxmX6deFdCxpJzhDttxgO1+bcCnkk+9drydLVDmAMG7NE6aN/fl4F7ucU/90gAA==}
engines: {node: '>=0.10.0'}
@@ -4401,6 +4782,10 @@ packages:
resolution: {integrity: sha512-DPi0FmjiSU5EvQV0++GFDOJ9ASQUVFh5kD+OzOnYdi7n3Wpm9hWWGfB/O2blfHcMVTL5WkQXSnRiK9makhrcnw==}
engines: {node: '>=0.3.1'}
+ doctrine@3.0.0:
+ resolution: {integrity: sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==}
+ engines: {node: '>=6.0.0'}
+
dom-accessibility-api@0.5.16:
resolution: {integrity: sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg==}
@@ -4473,8 +4858,8 @@ packages:
resolution: {integrity: sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==}
engines: {node: '>= 0.8'}
- enhanced-resolve@5.21.2:
- resolution: {integrity: sha512-xe9vQb5kReirPUxgQrXA3ihgbCqssmTiM7cOZ+Gzu+VeGWgpV98lLZvp0dl4yriyAePcewxGUs9UpKD8PET9KQ==}
+ enhanced-resolve@5.21.6:
+ resolution: {integrity: sha512-aNnGCvbJ/RIyWo1IuhNdVjnNF+EjH9wpzpNHt+ci/m9He9LJvUN8wrCcXjp9cWsGNAuvSpVFTx/vraAFQ8qGjQ==}
engines: {node: '>=10.13.0'}
entities@4.5.0:
@@ -5464,6 +5849,11 @@ packages:
lru-cache@5.1.1:
resolution: {integrity: sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==}
+ lucide-react@1.20.0:
+ resolution: {integrity: sha512-jhXLeC/7m0/tjL1nzMdKk6x256zWA6AtbhTVreHOiKPoeX2d6MK4FbyIQPpVq0E6iPWBisyy1TW+pEge/uMEuQ==}
+ peerDependencies:
+ react: ^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0
+
lz-string@1.5.0:
resolution: {integrity: sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==}
hasBin: true
@@ -5689,6 +6079,9 @@ packages:
resolution: {integrity: sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==}
engines: {node: '>=16 || 14 >=14.17'}
+ minimist@1.2.8:
+ resolution: {integrity: sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==}
+
minipass@7.1.3:
resolution: {integrity: sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==}
engines: {node: '>=16 || 14 >=14.17'}
@@ -6231,10 +6624,6 @@ packages:
resolution: {integrity: sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ==}
engines: {node: ^10 || ^12 || >=14}
- postcss@8.5.14:
- resolution: {integrity: sha512-SoSL4+OSEtR99LHFZQiJLkT59C5B1amGO1NzTwj7TT1qCUgUO6hxOvzkOYxD+vMrXBM3XJIKzokoERdqQq/Zmg==}
- engines: {node: ^10 || ^12 || >=14}
-
postcss@8.5.15:
resolution: {integrity: sha512-FfR8sjd4em2T6fb3I2MwAJU7HWVMr9zba+enmQeeWFfCbm+UOC/0X4DS8XtpUTMwWMGbjKYP7xjfNekzyGmB3A==}
engines: {node: ^10 || ^12 || >=14}
@@ -6306,6 +6695,15 @@ packages:
rc9@3.0.1:
resolution: {integrity: sha512-gMDyleLWVE+i6Sgtc0QbbY6pEKqYs97NGi6isHQPqYlLemPoO8dxQ3uGi0f4NiP98c+jMW6cG1Kx9dDwfvqARQ==}
+ react-docgen-typescript@2.4.0:
+ resolution: {integrity: sha512-ZtAp5XTO5HRzQctjPU0ybY0RRCQO19X/8fxn3w7y2VVTUbGHDKULPTL4ky3vB05euSgG5NpALhEhDPvQ56wvXg==}
+ peerDependencies:
+ typescript: '>= 4.3.x'
+
+ react-docgen@8.0.3:
+ resolution: {integrity: sha512-aEZ9qP+/M+58x2qgfSFEWH1BxLyHe5+qkLNJOZQb5iGS017jpbRnoKhNRrXPeA6RfBrZO5wZrT9DMC1UqE1f1w==}
+ engines: {node: ^20.9.0 || >=22}
+
react-dom@19.2.6:
resolution: {integrity: sha512-0prMI+hvBbPjsWnxDLxlCGyM8PN6UuWjEUCYmZhO67xIV9Xasa/r/vDnq+Xyq4Lo27g8QSbO5YzARu0D1Sps3g==}
peerDependencies:
@@ -6423,11 +6821,6 @@ packages:
vue-tsc:
optional: true
- rolldown@1.0.0:
- resolution: {integrity: sha512-yD986aXDESFGS95spT1LAv0jssywP4npMEjmMHyN2/5+eE8qQJUype2AaKkRiLgBgyD0LFlubwAht7VmY8rGoA==}
- engines: {node: ^20.19.0 || >=22.12.0}
- hasBin: true
-
rolldown@1.0.2:
resolution: {integrity: sha512-oZx5zVDtVB44AW3eaifgDml1gWRDZGvjcfdxonE4swNPG98PrrXjaO/KrnUjzlMnztCCRVlUueA1kCXhARGk6g==}
engines: {node: ^20.19.0 || >=22.12.0}
@@ -6702,6 +7095,10 @@ packages:
resolution: {integrity: sha512-uCC2VHvQRYu+lMh4My/sFNmF2klFymLX1wHJeXnbEJERpV/ZsVuonzerjfrGpIGF7LBVa1O7i9kjiWvJiFck8g==}
engines: {node: '>=0.10.0'}
+ strip-bom@3.0.0:
+ resolution: {integrity: sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==}
+ engines: {node: '>=4'}
+
strip-final-newline@3.0.0:
resolution: {integrity: sha512-dOESqjYr96iWYylGObzd39EuNTa5VJxyvVAEm5Jnh7KGo75V43Hk1odPQkNDyXNmUR6k+gEiDVXnjB8HJ3crXw==}
engines: {node: '>=12'}
@@ -6766,6 +7163,12 @@ packages:
resolution: {integrity: sha512-yEFYrVhod+hdNyx7g5Bnkkb0G6si8HJurOoOEgC8B/O0uXLHlaey/65KRv6cuWBNhBgHKAROVpc7QyYqE5gFng==}
engines: {node: '>=20'}
+ tailwind-merge@3.6.0:
+ resolution: {integrity: sha512-uxL7qAVQriqRQPAyK3pj66VqskWqoZ37PW94jwOTwNfq/z9oyu1V+eqrZqtR2+fCiXdYOZe/Modt8GtvqNzu+w==}
+
+ tailwindcss@4.3.1:
+ resolution: {integrity: sha512-hk+TB1m+K8CYNrP6rjQaq/Y+4Zylwpa87mLYBKCunwnnQ9p+fHb7kmSfGqyEJoxF/O6CDyABWVFEafNSYKll+Q==}
+
tapable@2.3.3:
resolution: {integrity: sha512-uxc/zpqFg6x7C8vOE7lh6Lbda8eEL9zmVm/PLeTPBRhh1xCgdWaQ+J1CUieGpIfm2HdtsUpRv+HshiasBMcc6A==}
engines: {node: '>=6'}
@@ -6798,10 +7201,6 @@ packages:
resolution: {integrity: sha512-Ae3OVUqifDw0wBriIBS7yVaW44Dp6eSHQcyq4Igc7eN2TJH/2YsicswaW+J/OuMvhpDPOKEgpAZCjkb4hpoyeA==}
engines: {node: ^16.14.0 || >= 17.3.0}
- tinyexec@1.1.2:
- resolution: {integrity: sha512-dAqSqE/RabpBKI8+h26GfLq6Vb3JVXs30XYQjdMjaj/c2tS8IYYMbIzP599KtRj7c57/wYApb3QjgRgXmrCukA==}
- engines: {node: '>=18'}
-
tinyexec@1.2.2:
resolution: {integrity: sha512-M/Q0B2cp4K7kynaT/vnED1j8TlLY+Pp7C6Wl2bl/7u/F0mUVwdyOpwomQb8JpYLitHUssAJRmLZdMCGsrx7i+g==}
engines: {node: '>=18'}
@@ -6862,6 +7261,10 @@ packages:
resolution: {integrity: sha512-q5W7tVM71e2xjHZTlgfTDoPF/SmqKG5hddq9SzR49CH2hayqRKJtQ4mtRlSxKaJlR/+9rEM+mnBHf7I2/BQcpQ==}
engines: {node: '>=6.10'}
+ tsconfig-paths@4.2.0:
+ resolution: {integrity: sha512-NoZ4roiN7LnbKn9QqE1amc9DJfzvZXxF4xDavcOWt1BPkdx+m+0gJuPM+S0vCe7zTJMYUP0R8pO2XMr+Y8oLIg==}
+ engines: {node: '>=6'}
+
tsdown@0.22.0:
resolution: {integrity: sha512-FgW0hHb27nGQA/+F3d5+U9wKXkfilk9DVkc5+7x/ZqF03g+Hoz/eeApT32jqxATt9eRoR+1jxk7MUMON+O4CXw==}
engines: {node: ^22.18.0 || >=24.0.0}
@@ -6917,6 +7320,9 @@ packages:
resolution: {integrity: sha512-VpKvD9Z0Hu/xrGUAYX1wnhfpqv835wIwGqeKfulvBPTOcDap0E3nFwyzCAVV85fB1sBcBDEfTP+7FSW7GzwWSQ==}
hasBin: true
+ tw-animate-css@1.4.0:
+ resolution: {integrity: sha512-7bziOlRqH0hJx80h/3mbicLW7o8qLsH5+RaLR2t+OHM3D0JlWGODQKQ4cxbK7WlvmUxpcj6Kgu6EKqjrGFe3QQ==}
+
type-check@0.4.0:
resolution: {integrity: sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==}
engines: {node: '>= 0.8.0'}
@@ -6959,9 +7365,6 @@ packages:
unctx@2.5.0:
resolution: {integrity: sha512-p+Rz9x0R7X+CYDkT+Xg8/GhpcShTlU8n+cf9OtOEf7zEQsNcCZO1dPKNRDqvUTaq+P32PMMkxWHwfrxkqfqAYg==}
- undici-types@7.21.0:
- resolution: {integrity: sha512-w9IMgQrz4O0YN1LtB7K5P63vhlIOvC7opSmouCJ+ZywlPAlO9gIkJ+otk6LvGpAs2wg4econaCz3TvQ9xPoyuQ==}
-
undici-types@7.24.6:
resolution: {integrity: sha512-WRNW+sJgj5OBN4/0JpHFqtqzhpbnV0GuB+OozA9gCL7a993SmU+1JBZCzLNxYsbMfIeDL+lTsphD5jN5N+n0zg==}
@@ -6979,15 +7382,6 @@ packages:
resolution: {integrity: sha512-wH590V9VNgYH9g3lH9wWjTrUoKsjLF6sGLjhR4sH1LWpLmCOH0Zf7PukhDA8BiS7KHe4oPNkcTHqYkj7SOGUOw==}
engines: {node: '>=20'}
- unimport@6.2.0:
- resolution: {integrity: sha512-4NcqaphAHQff4eBWQ3pjVOCYNLlmVGGMoLDmboobh8+OQe9yP7UyeoMP043M1bG0YNc3CqtukD2VuINxOqm4rQ==}
- engines: {node: '>=18.12.0'}
- peerDependencies:
- oxc-parser: '*'
- peerDependenciesMeta:
- oxc-parser:
- optional: true
-
unimport@6.3.0:
resolution: {integrity: sha512-M+Dxk5W9WRd+8j56W9tp8lGW/dmMc7g5zj7BWQnEjKQhryBstqsi1V0izb0zHwSkEN8cSYV7K75/bykairV2tA==}
engines: {node: '>=18.12.0'}
@@ -7546,6 +7940,8 @@ snapshots:
'@adobe/css-tools@4.5.0': {}
+ '@alloc/quick-lru@5.2.0': {}
+
'@antfu/eslint-config@9.0.0(@typescript-eslint/rule-tester@8.59.2(eslint@10.4.0(jiti@2.7.0))(typescript@6.0.3))(@typescript-eslint/typescript-estree@8.59.2(typescript@6.0.3))(@typescript-eslint/utils@8.59.2(eslint@10.4.0(jiti@2.7.0))(typescript@6.0.3))(@vue/compiler-sfc@3.5.34)(eslint@10.4.0(jiti@2.7.0))(typescript@6.0.3)(vitest@4.1.7(@types/node@25.9.1)(vite@8.0.14(@types/node@25.9.1)(esbuild@0.28.0)(jiti@2.7.0)(terser@5.47.1)(tsx@4.22.3)(yaml@2.8.4)))':
dependencies:
'@antfu/install-pkg': 1.1.0
@@ -7600,13 +7996,13 @@ snapshots:
'@antfu/install-pkg@1.1.0':
dependencies:
package-manager-detector: 1.6.0
- tinyexec: 1.1.2
+ tinyexec: 1.2.2
'@antfu/ni@30.1.0':
dependencies:
fzf: 0.5.2
package-manager-detector: 1.6.0
- tinyexec: 1.1.2
+ tinyexec: 1.2.2
tinyglobby: 0.2.16
'@antfu/utils@9.3.0': {}
@@ -7857,7 +8253,7 @@ snapshots:
'@dxup/nuxt@0.4.1(magicast@0.5.2)(typescript@6.0.3)':
dependencies:
'@dxup/unimport': 0.1.2
- '@nuxt/kit': 4.4.5(magicast@0.5.2)
+ '@nuxt/kit': 4.4.6(magicast@0.5.2)
chokidar: 5.0.0
pathe: 2.0.3
tinyglobby: 0.2.16
@@ -8277,7 +8673,7 @@ snapshots:
'@img/sharp-wasm32@0.34.5':
dependencies:
- '@emnapi/runtime': 1.10.0
+ '@emnapi/runtime': 1.11.0
optional: true
'@img/sharp-win32-arm64@0.34.5':
@@ -8304,6 +8700,14 @@ snapshots:
dependencies:
minipass: 7.1.3
+ '@joshwooding/vite-plugin-react-docgen-typescript@0.7.0(typescript@6.0.3)(vite@8.0.14(@types/node@25.9.1)(esbuild@0.28.0)(jiti@2.7.0)(terser@5.47.1)(tsx@4.22.3)(yaml@2.8.4))':
+ dependencies:
+ glob: 13.0.6
+ react-docgen-typescript: 2.4.0(typescript@6.0.3)
+ vite: 8.0.14(@types/node@25.9.1)(esbuild@0.28.0)(jiti@2.7.0)(terser@5.47.1)(tsx@4.22.3)(yaml@2.8.4)
+ optionalDependencies:
+ typescript: 6.0.3
+
'@jridgewell/gen-mapping@0.3.13':
dependencies:
'@jridgewell/sourcemap-codec': 1.5.5
@@ -8349,6 +8753,12 @@ snapshots:
- encoding
- supports-color
+ '@mdx-js/react@3.1.1(@types/react@19.2.15)(react@19.2.6)':
+ dependencies:
+ '@types/mdx': 2.0.14
+ '@types/react': 19.2.15
+ react: 19.2.6
+
'@mermaid-js/mermaid-mindmap@9.3.0':
dependencies:
'@braintree/sanitize-url': 6.0.4
@@ -8386,24 +8796,24 @@ snapshots:
transitivePeerDependencies:
- supports-color
- '@napi-rs/wasm-runtime@1.1.4(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)':
+ '@napi-rs/wasm-runtime@1.1.5(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)':
dependencies:
'@emnapi/core': 1.10.0
'@emnapi/runtime': 1.10.0
'@tybys/wasm-util': 0.10.2
optional: true
- '@napi-rs/wasm-runtime@1.1.4(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2)':
+ '@napi-rs/wasm-runtime@1.1.5(@emnapi/core@1.11.0)(@emnapi/runtime@1.11.0)':
dependencies:
- '@emnapi/core': 1.9.2
- '@emnapi/runtime': 1.9.2
+ '@emnapi/core': 1.11.0
+ '@emnapi/runtime': 1.11.0
'@tybys/wasm-util': 0.10.2
optional: true
- '@napi-rs/wasm-runtime@1.1.5(@emnapi/core@1.11.0)(@emnapi/runtime@1.11.0)':
+ '@napi-rs/wasm-runtime@1.1.5(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2)':
dependencies:
- '@emnapi/core': 1.11.0
- '@emnapi/runtime': 1.11.0
+ '@emnapi/core': 1.9.2
+ '@emnapi/runtime': 1.9.2
'@tybys/wasm-util': 0.10.2
optional: true
@@ -8472,7 +8882,7 @@ snapshots:
srvx: 0.11.15
std-env: 4.1.0
tinyclip: 0.1.12
- tinyexec: 1.1.2
+ tinyexec: 1.2.2
ufo: 1.6.4
youch: 4.1.1
optionalDependencies:
@@ -8487,7 +8897,7 @@ snapshots:
'@nuxt/devtools-kit@3.2.4(magicast@0.5.2)(vite@8.0.14(@types/node@25.9.1)(esbuild@0.28.0)(jiti@2.7.0)(terser@5.47.1)(tsx@4.22.3)(yaml@2.8.4))':
dependencies:
- '@nuxt/kit': 4.4.5(magicast@0.5.2)
+ '@nuxt/kit': 4.4.6(magicast@0.5.2)
execa: 8.0.1
vite: 8.0.14(@types/node@25.9.1)(esbuild@0.28.0)(jiti@2.7.0)(terser@5.47.1)(tsx@4.22.3)(yaml@2.8.4)
transitivePeerDependencies:
@@ -8508,7 +8918,7 @@ snapshots:
dependencies:
'@nuxt/devtools-kit': 3.2.4(magicast@0.5.2)(vite@8.0.14(@types/node@25.9.1)(esbuild@0.28.0)(jiti@2.7.0)(terser@5.47.1)(tsx@4.22.3)(yaml@2.8.4))
'@nuxt/devtools-wizard': 3.2.4
- '@nuxt/kit': 4.4.5(magicast@0.5.2)
+ '@nuxt/kit': 4.4.6(magicast@0.5.2)
'@vue/devtools-core': 8.1.2(vue@3.5.34(typescript@6.0.3))
'@vue/devtools-kit': 8.1.2
birpc: 4.0.0
@@ -8535,7 +8945,7 @@ snapshots:
structured-clone-es: 2.0.0
tinyglobby: 0.2.16
vite: 8.0.14(@types/node@25.9.1)(esbuild@0.28.0)(jiti@2.7.0)(terser@5.47.1)(tsx@4.22.3)(yaml@2.8.4)
- vite-plugin-inspect: 11.3.3(@nuxt/kit@4.4.5(magicast@0.5.2))(vite@8.0.14(@types/node@25.9.1)(esbuild@0.28.0)(jiti@2.7.0)(terser@5.47.1)(tsx@4.22.3)(yaml@2.8.4))
+ vite-plugin-inspect: 11.3.3(@nuxt/kit@4.4.6(magicast@0.5.2))(vite@8.0.14(@types/node@25.9.1)(esbuild@0.28.0)(jiti@2.7.0)(terser@5.47.1)(tsx@4.22.3)(yaml@2.8.4))
vite-plugin-vue-tracer: 1.3.0(vite@8.0.14(@types/node@25.9.1)(esbuild@0.28.0)(jiti@2.7.0)(terser@5.47.1)(tsx@4.22.3)(yaml@2.8.4))(vue@3.5.34(typescript@6.0.3))
which: 6.0.1
ws: 8.21.0
@@ -8545,31 +8955,6 @@ snapshots:
- utf-8-validate
- vue
- '@nuxt/kit@4.4.5(magicast@0.5.2)':
- dependencies:
- c12: 3.3.4(magicast@0.5.2)
- consola: 3.4.2
- defu: 6.1.7
- destr: 2.0.5
- errx: 0.1.0
- exsolve: 1.0.8
- ignore: 7.0.5
- jiti: 2.7.0
- klona: 2.0.6
- mlly: 1.8.2
- ohash: 2.0.11
- pathe: 2.0.3
- pkg-types: 2.3.1
- rc9: 3.0.1
- scule: 1.3.0
- semver: 7.8.1
- tinyglobby: 0.2.16
- ufo: 1.6.4
- unctx: 2.5.0
- untyped: 2.0.0
- transitivePeerDependencies:
- - magicast
-
'@nuxt/kit@4.4.6(magicast@0.5.2)':
dependencies:
c12: 3.3.4(magicast@0.5.2)
@@ -8688,9 +9073,9 @@ snapshots:
'@rollup/plugin-replace': 6.0.3(rollup@4.60.3)
'@vitejs/plugin-vue': 6.0.7(vite@7.3.3(@types/node@25.9.1)(jiti@2.7.0)(lightningcss@1.32.0)(terser@5.47.1)(tsx@4.22.3)(yaml@2.8.4))(vue@3.5.34(typescript@6.0.3))
'@vitejs/plugin-vue-jsx': 5.1.5(vite@7.3.3(@types/node@25.9.1)(jiti@2.7.0)(lightningcss@1.32.0)(terser@5.47.1)(tsx@4.22.3)(yaml@2.8.4))(vue@3.5.34(typescript@6.0.3))
- autoprefixer: 10.5.0(postcss@8.5.14)
+ autoprefixer: 10.5.0(postcss@8.5.15)
consola: 3.4.2
- cssnano: 8.0.1(postcss@8.5.14)
+ cssnano: 8.0.1(postcss@8.5.15)
defu: 6.1.7
escape-string-regexp: 5.0.0
exsolve: 1.0.8
@@ -8704,7 +9089,7 @@ snapshots:
nypm: 0.6.6
pathe: 2.0.3
pkg-types: 2.3.1
- postcss: 8.5.14
+ postcss: 8.5.15
seroval: 1.5.4
std-env: 4.1.0
ufo: 1.6.4
@@ -8797,7 +9182,7 @@ snapshots:
dependencies:
'@emnapi/core': 1.10.0
'@emnapi/runtime': 1.10.0
- '@napi-rs/wasm-runtime': 1.1.4(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)
+ '@napi-rs/wasm-runtime': 1.1.5(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)
optional: true
'@oxc-minify/binding-win32-arm64-msvc@0.131.0':
@@ -8957,21 +9342,21 @@ snapshots:
dependencies:
'@emnapi/core': 1.9.2
'@emnapi/runtime': 1.9.2
- '@napi-rs/wasm-runtime': 1.1.4(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2)
+ '@napi-rs/wasm-runtime': 1.1.5(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2)
optional: true
'@oxc-parser/binding-wasm32-wasi@0.129.0':
dependencies:
'@emnapi/core': 1.10.0
'@emnapi/runtime': 1.10.0
- '@napi-rs/wasm-runtime': 1.1.4(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)
+ '@napi-rs/wasm-runtime': 1.1.5(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)
optional: true
'@oxc-parser/binding-wasm32-wasi@0.131.0':
dependencies:
'@emnapi/core': 1.10.0
'@emnapi/runtime': 1.10.0
- '@napi-rs/wasm-runtime': 1.1.4(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)
+ '@napi-rs/wasm-runtime': 1.1.5(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)
optional: true
'@oxc-parser/binding-win32-arm64-msvc@0.127.0':
@@ -9122,7 +9507,7 @@ snapshots:
dependencies:
'@emnapi/core': 1.10.0
'@emnapi/runtime': 1.10.0
- '@napi-rs/wasm-runtime': 1.1.4(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)
+ '@napi-rs/wasm-runtime': 1.1.5(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)
optional: true
'@oxc-transform/binding-win32-arm64-msvc@0.131.0':
@@ -9265,108 +9650,207 @@ snapshots:
dependencies:
quansync: 1.0.0
- '@rolldown/binding-android-arm64@1.0.0':
- optional: true
+ '@radix-ui/number@1.1.2': {}
- '@rolldown/binding-android-arm64@1.0.2':
- optional: true
+ '@radix-ui/primitive@1.1.4': {}
- '@rolldown/binding-darwin-arm64@1.0.0':
- optional: true
+ '@radix-ui/react-collection@1.1.10(@types/react-dom@19.2.3(@types/react@19.2.15))(@types/react@19.2.15)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)':
+ dependencies:
+ '@radix-ui/react-compose-refs': 1.1.3(@types/react@19.2.15)(react@19.2.6)
+ '@radix-ui/react-context': 1.1.4(@types/react@19.2.15)(react@19.2.6)
+ '@radix-ui/react-primitive': 2.1.6(@types/react-dom@19.2.3(@types/react@19.2.15))(@types/react@19.2.15)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)
+ '@radix-ui/react-slot': 1.3.0(@types/react@19.2.15)(react@19.2.6)
+ react: 19.2.6
+ react-dom: 19.2.6(react@19.2.6)
+ optionalDependencies:
+ '@types/react': 19.2.15
+ '@types/react-dom': 19.2.3(@types/react@19.2.15)
- '@rolldown/binding-darwin-arm64@1.0.2':
- optional: true
+ '@radix-ui/react-compose-refs@1.1.3(@types/react@19.2.15)(react@19.2.6)':
+ dependencies:
+ react: 19.2.6
+ optionalDependencies:
+ '@types/react': 19.2.15
- '@rolldown/binding-darwin-x64@1.0.0':
- optional: true
+ '@radix-ui/react-context@1.1.4(@types/react@19.2.15)(react@19.2.6)':
+ dependencies:
+ react: 19.2.6
+ optionalDependencies:
+ '@types/react': 19.2.15
- '@rolldown/binding-darwin-x64@1.0.2':
+ '@radix-ui/react-direction@1.1.2(@types/react@19.2.15)(react@19.2.6)':
+ dependencies:
+ react: 19.2.6
+ optionalDependencies:
+ '@types/react': 19.2.15
+
+ '@radix-ui/react-id@1.1.2(@types/react@19.2.15)(react@19.2.6)':
+ dependencies:
+ '@radix-ui/react-use-layout-effect': 1.1.2(@types/react@19.2.15)(react@19.2.6)
+ react: 19.2.6
+ optionalDependencies:
+ '@types/react': 19.2.15
+
+ '@radix-ui/react-presence@1.1.6(@types/react-dom@19.2.3(@types/react@19.2.15))(@types/react@19.2.15)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)':
+ dependencies:
+ '@radix-ui/react-use-layout-effect': 1.1.2(@types/react@19.2.15)(react@19.2.6)
+ react: 19.2.6
+ react-dom: 19.2.6(react@19.2.6)
+ optionalDependencies:
+ '@types/react': 19.2.15
+ '@types/react-dom': 19.2.3(@types/react@19.2.15)
+
+ '@radix-ui/react-primitive@2.1.6(@types/react-dom@19.2.3(@types/react@19.2.15))(@types/react@19.2.15)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)':
+ dependencies:
+ '@radix-ui/react-slot': 1.3.0(@types/react@19.2.15)(react@19.2.6)
+ react: 19.2.6
+ react-dom: 19.2.6(react@19.2.6)
+ optionalDependencies:
+ '@types/react': 19.2.15
+ '@types/react-dom': 19.2.3(@types/react@19.2.15)
+
+ '@radix-ui/react-roving-focus@1.1.13(@types/react-dom@19.2.3(@types/react@19.2.15))(@types/react@19.2.15)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)':
+ dependencies:
+ '@radix-ui/primitive': 1.1.4
+ '@radix-ui/react-collection': 1.1.10(@types/react-dom@19.2.3(@types/react@19.2.15))(@types/react@19.2.15)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)
+ '@radix-ui/react-compose-refs': 1.1.3(@types/react@19.2.15)(react@19.2.6)
+ '@radix-ui/react-context': 1.1.4(@types/react@19.2.15)(react@19.2.6)
+ '@radix-ui/react-direction': 1.1.2(@types/react@19.2.15)(react@19.2.6)
+ '@radix-ui/react-id': 1.1.2(@types/react@19.2.15)(react@19.2.6)
+ '@radix-ui/react-primitive': 2.1.6(@types/react-dom@19.2.3(@types/react@19.2.15))(@types/react@19.2.15)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)
+ '@radix-ui/react-use-callback-ref': 1.1.2(@types/react@19.2.15)(react@19.2.6)
+ '@radix-ui/react-use-controllable-state': 1.2.3(@types/react@19.2.15)(react@19.2.6)
+ react: 19.2.6
+ react-dom: 19.2.6(react@19.2.6)
+ optionalDependencies:
+ '@types/react': 19.2.15
+ '@types/react-dom': 19.2.3(@types/react@19.2.15)
+
+ '@radix-ui/react-scroll-area@1.2.12(@types/react-dom@19.2.3(@types/react@19.2.15))(@types/react@19.2.15)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)':
+ dependencies:
+ '@radix-ui/number': 1.1.2
+ '@radix-ui/primitive': 1.1.4
+ '@radix-ui/react-compose-refs': 1.1.3(@types/react@19.2.15)(react@19.2.6)
+ '@radix-ui/react-context': 1.1.4(@types/react@19.2.15)(react@19.2.6)
+ '@radix-ui/react-direction': 1.1.2(@types/react@19.2.15)(react@19.2.6)
+ '@radix-ui/react-presence': 1.1.6(@types/react-dom@19.2.3(@types/react@19.2.15))(@types/react@19.2.15)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)
+ '@radix-ui/react-primitive': 2.1.6(@types/react-dom@19.2.3(@types/react@19.2.15))(@types/react@19.2.15)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)
+ '@radix-ui/react-use-callback-ref': 1.1.2(@types/react@19.2.15)(react@19.2.6)
+ '@radix-ui/react-use-layout-effect': 1.1.2(@types/react@19.2.15)(react@19.2.6)
+ react: 19.2.6
+ react-dom: 19.2.6(react@19.2.6)
+ optionalDependencies:
+ '@types/react': 19.2.15
+ '@types/react-dom': 19.2.3(@types/react@19.2.15)
+
+ '@radix-ui/react-separator@1.1.10(@types/react-dom@19.2.3(@types/react@19.2.15))(@types/react@19.2.15)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)':
+ dependencies:
+ '@radix-ui/react-primitive': 2.1.6(@types/react-dom@19.2.3(@types/react@19.2.15))(@types/react@19.2.15)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)
+ react: 19.2.6
+ react-dom: 19.2.6(react@19.2.6)
+ optionalDependencies:
+ '@types/react': 19.2.15
+ '@types/react-dom': 19.2.3(@types/react@19.2.15)
+
+ '@radix-ui/react-slot@1.3.0(@types/react@19.2.15)(react@19.2.6)':
+ dependencies:
+ '@radix-ui/react-compose-refs': 1.1.3(@types/react@19.2.15)(react@19.2.6)
+ react: 19.2.6
+ optionalDependencies:
+ '@types/react': 19.2.15
+
+ '@radix-ui/react-tabs@1.1.15(@types/react-dom@19.2.3(@types/react@19.2.15))(@types/react@19.2.15)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)':
+ dependencies:
+ '@radix-ui/primitive': 1.1.4
+ '@radix-ui/react-context': 1.1.4(@types/react@19.2.15)(react@19.2.6)
+ '@radix-ui/react-direction': 1.1.2(@types/react@19.2.15)(react@19.2.6)
+ '@radix-ui/react-id': 1.1.2(@types/react@19.2.15)(react@19.2.6)
+ '@radix-ui/react-presence': 1.1.6(@types/react-dom@19.2.3(@types/react@19.2.15))(@types/react@19.2.15)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)
+ '@radix-ui/react-primitive': 2.1.6(@types/react-dom@19.2.3(@types/react@19.2.15))(@types/react@19.2.15)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)
+ '@radix-ui/react-roving-focus': 1.1.13(@types/react-dom@19.2.3(@types/react@19.2.15))(@types/react@19.2.15)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)
+ '@radix-ui/react-use-controllable-state': 1.2.3(@types/react@19.2.15)(react@19.2.6)
+ react: 19.2.6
+ react-dom: 19.2.6(react@19.2.6)
+ optionalDependencies:
+ '@types/react': 19.2.15
+ '@types/react-dom': 19.2.3(@types/react@19.2.15)
+
+ '@radix-ui/react-use-callback-ref@1.1.2(@types/react@19.2.15)(react@19.2.6)':
+ dependencies:
+ react: 19.2.6
+ optionalDependencies:
+ '@types/react': 19.2.15
+
+ '@radix-ui/react-use-controllable-state@1.2.3(@types/react@19.2.15)(react@19.2.6)':
+ dependencies:
+ '@radix-ui/react-use-effect-event': 0.0.3(@types/react@19.2.15)(react@19.2.6)
+ '@radix-ui/react-use-layout-effect': 1.1.2(@types/react@19.2.15)(react@19.2.6)
+ react: 19.2.6
+ optionalDependencies:
+ '@types/react': 19.2.15
+
+ '@radix-ui/react-use-effect-event@0.0.3(@types/react@19.2.15)(react@19.2.6)':
+ dependencies:
+ '@radix-ui/react-use-layout-effect': 1.1.2(@types/react@19.2.15)(react@19.2.6)
+ react: 19.2.6
+ optionalDependencies:
+ '@types/react': 19.2.15
+
+ '@radix-ui/react-use-layout-effect@1.1.2(@types/react@19.2.15)(react@19.2.6)':
+ dependencies:
+ react: 19.2.6
+ optionalDependencies:
+ '@types/react': 19.2.15
+
+ '@rolldown/binding-android-arm64@1.0.2':
optional: true
- '@rolldown/binding-freebsd-x64@1.0.0':
+ '@rolldown/binding-darwin-arm64@1.0.2':
optional: true
- '@rolldown/binding-freebsd-x64@1.0.2':
+ '@rolldown/binding-darwin-x64@1.0.2':
optional: true
- '@rolldown/binding-linux-arm-gnueabihf@1.0.0':
+ '@rolldown/binding-freebsd-x64@1.0.2':
optional: true
'@rolldown/binding-linux-arm-gnueabihf@1.0.2':
optional: true
- '@rolldown/binding-linux-arm64-gnu@1.0.0':
- optional: true
-
'@rolldown/binding-linux-arm64-gnu@1.0.2':
optional: true
- '@rolldown/binding-linux-arm64-musl@1.0.0':
- optional: true
-
'@rolldown/binding-linux-arm64-musl@1.0.2':
optional: true
- '@rolldown/binding-linux-ppc64-gnu@1.0.0':
- optional: true
-
'@rolldown/binding-linux-ppc64-gnu@1.0.2':
optional: true
- '@rolldown/binding-linux-s390x-gnu@1.0.0':
- optional: true
-
'@rolldown/binding-linux-s390x-gnu@1.0.2':
optional: true
- '@rolldown/binding-linux-x64-gnu@1.0.0':
- optional: true
-
'@rolldown/binding-linux-x64-gnu@1.0.2':
optional: true
- '@rolldown/binding-linux-x64-musl@1.0.0':
- optional: true
-
'@rolldown/binding-linux-x64-musl@1.0.2':
optional: true
- '@rolldown/binding-openharmony-arm64@1.0.0':
- optional: true
-
'@rolldown/binding-openharmony-arm64@1.0.2':
optional: true
- '@rolldown/binding-wasm32-wasi@1.0.0':
- dependencies:
- '@emnapi/core': 1.10.0
- '@emnapi/runtime': 1.10.0
- '@napi-rs/wasm-runtime': 1.1.4(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)
- optional: true
-
'@rolldown/binding-wasm32-wasi@1.0.2':
dependencies:
'@emnapi/core': 1.10.0
'@emnapi/runtime': 1.10.0
- '@napi-rs/wasm-runtime': 1.1.4(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)
- optional: true
-
- '@rolldown/binding-win32-arm64-msvc@1.0.0':
+ '@napi-rs/wasm-runtime': 1.1.5(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)
optional: true
'@rolldown/binding-win32-arm64-msvc@1.0.2':
optional: true
- '@rolldown/binding-win32-x64-msvc@1.0.0':
- optional: true
-
'@rolldown/binding-win32-x64-msvc@1.0.2':
optional: true
- '@rolldown/pluginutils@1.0.0': {}
-
- '@rolldown/pluginutils@1.0.0-rc.13': {}
-
'@rolldown/pluginutils@1.0.1': {}
'@rollup/plugin-alias@6.0.0(rollup@4.60.3)':
@@ -9566,6 +10050,31 @@ snapshots:
'@standard-schema/spec@1.1.0': {}
+ '@storybook/addon-a11y@10.4.6(storybook@10.4.6(@testing-library/dom@10.4.1)(@types/react@19.2.15)(react-dom@19.2.6(react@19.2.6))(react@19.2.6))':
+ dependencies:
+ '@storybook/global': 5.0.0
+ axe-core: 4.12.1
+ storybook: 10.4.6(@testing-library/dom@10.4.1)(@types/react@19.2.15)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)
+
+ '@storybook/addon-docs@10.4.6(@types/react-dom@19.2.3(@types/react@19.2.15))(@types/react@19.2.15)(esbuild@0.28.0)(rollup@4.60.3)(storybook@10.4.6(@testing-library/dom@10.4.1)(@types/react@19.2.15)(react-dom@19.2.6(react@19.2.6))(react@19.2.6))(vite@8.0.14(@types/node@25.9.1)(esbuild@0.28.0)(jiti@2.7.0)(terser@5.47.1)(tsx@4.22.3)(yaml@2.8.4))':
+ dependencies:
+ '@mdx-js/react': 3.1.1(@types/react@19.2.15)(react@19.2.6)
+ '@storybook/csf-plugin': 10.4.6(esbuild@0.28.0)(rollup@4.60.3)(storybook@10.4.6(@testing-library/dom@10.4.1)(@types/react@19.2.15)(react-dom@19.2.6(react@19.2.6))(react@19.2.6))(vite@8.0.14(@types/node@25.9.1)(esbuild@0.28.0)(jiti@2.7.0)(terser@5.47.1)(tsx@4.22.3)(yaml@2.8.4))
+ '@storybook/icons': 2.0.2(react-dom@19.2.6(react@19.2.6))(react@19.2.6)
+ '@storybook/react-dom-shim': 10.4.6(@types/react-dom@19.2.3(@types/react@19.2.15))(@types/react@19.2.15)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)(storybook@10.4.6(@testing-library/dom@10.4.1)(@types/react@19.2.15)(react-dom@19.2.6(react@19.2.6))(react@19.2.6))
+ react: 19.2.6
+ react-dom: 19.2.6(react@19.2.6)
+ storybook: 10.4.6(@testing-library/dom@10.4.1)(@types/react@19.2.15)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)
+ ts-dedent: 2.2.0
+ optionalDependencies:
+ '@types/react': 19.2.15
+ transitivePeerDependencies:
+ - '@types/react-dom'
+ - esbuild
+ - rollup
+ - vite
+ - webpack
+
'@storybook/builder-vite@10.4.6(esbuild@0.28.0)(rollup@4.60.3)(storybook@10.4.6(@testing-library/dom@10.4.1)(@types/react@19.2.15)(react-dom@19.2.6(react@19.2.6))(react@19.2.6))(vite@8.0.14(@types/node@25.9.1)(esbuild@0.28.0)(jiti@2.7.0)(terser@5.47.1)(tsx@4.22.3)(yaml@2.8.4))':
dependencies:
'@storybook/csf-plugin': 10.4.6(esbuild@0.28.0)(rollup@4.60.3)(storybook@10.4.6(@testing-library/dom@10.4.1)(@types/react@19.2.15)(react-dom@19.2.6(react@19.2.6))(react@19.2.6))(vite@8.0.14(@types/node@25.9.1)(esbuild@0.28.0)(jiti@2.7.0)(terser@5.47.1)(tsx@4.22.3)(yaml@2.8.4))
@@ -9610,6 +10119,55 @@ snapshots:
react: 19.2.6
react-dom: 19.2.6(react@19.2.6)
+ '@storybook/react-dom-shim@10.4.6(@types/react-dom@19.2.3(@types/react@19.2.15))(@types/react@19.2.15)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)(storybook@10.4.6(@testing-library/dom@10.4.1)(@types/react@19.2.15)(react-dom@19.2.6(react@19.2.6))(react@19.2.6))':
+ dependencies:
+ react: 19.2.6
+ react-dom: 19.2.6(react@19.2.6)
+ storybook: 10.4.6(@testing-library/dom@10.4.1)(@types/react@19.2.15)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)
+ optionalDependencies:
+ '@types/react': 19.2.15
+ '@types/react-dom': 19.2.3(@types/react@19.2.15)
+
+ '@storybook/react-vite@10.4.6(@types/react-dom@19.2.3(@types/react@19.2.15))(@types/react@19.2.15)(esbuild@0.28.0)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)(rollup@4.60.3)(storybook@10.4.6(@testing-library/dom@10.4.1)(@types/react@19.2.15)(react-dom@19.2.6(react@19.2.6))(react@19.2.6))(typescript@6.0.3)(vite@8.0.14(@types/node@25.9.1)(esbuild@0.28.0)(jiti@2.7.0)(terser@5.47.1)(tsx@4.22.3)(yaml@2.8.4))':
+ dependencies:
+ '@joshwooding/vite-plugin-react-docgen-typescript': 0.7.0(typescript@6.0.3)(vite@8.0.14(@types/node@25.9.1)(esbuild@0.28.0)(jiti@2.7.0)(terser@5.47.1)(tsx@4.22.3)(yaml@2.8.4))
+ '@rollup/pluginutils': 5.3.0(rollup@4.60.3)
+ '@storybook/builder-vite': 10.4.6(esbuild@0.28.0)(rollup@4.60.3)(storybook@10.4.6(@testing-library/dom@10.4.1)(@types/react@19.2.15)(react-dom@19.2.6(react@19.2.6))(react@19.2.6))(vite@8.0.14(@types/node@25.9.1)(esbuild@0.28.0)(jiti@2.7.0)(terser@5.47.1)(tsx@4.22.3)(yaml@2.8.4))
+ '@storybook/react': 10.4.6(@types/react-dom@19.2.3(@types/react@19.2.15))(@types/react@19.2.15)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)(storybook@10.4.6(@testing-library/dom@10.4.1)(@types/react@19.2.15)(react-dom@19.2.6(react@19.2.6))(react@19.2.6))(typescript@6.0.3)
+ empathic: 2.0.1
+ magic-string: 0.30.21
+ react: 19.2.6
+ react-docgen: 8.0.3
+ react-dom: 19.2.6(react@19.2.6)
+ resolve: 1.22.12
+ storybook: 10.4.6(@testing-library/dom@10.4.1)(@types/react@19.2.15)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)
+ tsconfig-paths: 4.2.0
+ vite: 8.0.14(@types/node@25.9.1)(esbuild@0.28.0)(jiti@2.7.0)(terser@5.47.1)(tsx@4.22.3)(yaml@2.8.4)
+ transitivePeerDependencies:
+ - '@types/react'
+ - '@types/react-dom'
+ - esbuild
+ - rollup
+ - supports-color
+ - typescript
+ - webpack
+
+ '@storybook/react@10.4.6(@types/react-dom@19.2.3(@types/react@19.2.15))(@types/react@19.2.15)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)(storybook@10.4.6(@testing-library/dom@10.4.1)(@types/react@19.2.15)(react-dom@19.2.6(react@19.2.6))(react@19.2.6))(typescript@6.0.3)':
+ dependencies:
+ '@storybook/global': 5.0.0
+ '@storybook/react-dom-shim': 10.4.6(@types/react-dom@19.2.3(@types/react@19.2.15))(@types/react@19.2.15)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)(storybook@10.4.6(@testing-library/dom@10.4.1)(@types/react@19.2.15)(react-dom@19.2.6(react@19.2.6))(react@19.2.6))
+ react: 19.2.6
+ react-docgen: 8.0.3
+ react-docgen-typescript: 2.4.0(typescript@6.0.3)
+ react-dom: 19.2.6(react@19.2.6)
+ storybook: 10.4.6(@testing-library/dom@10.4.1)(@types/react@19.2.15)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)
+ optionalDependencies:
+ '@types/react': 19.2.15
+ '@types/react-dom': 19.2.3(@types/react@19.2.15)
+ typescript: 6.0.3
+ transitivePeerDependencies:
+ - supports-color
+
'@stylistic/eslint-plugin@5.10.0(eslint@10.4.0(jiti@2.7.0))':
dependencies:
'@eslint-community/eslint-utils': 4.9.1(eslint@10.4.0(jiti@2.7.0))
@@ -9624,6 +10182,82 @@ snapshots:
dependencies:
tslib: 2.8.1
+ '@tailwindcss/node@4.3.1':
+ dependencies:
+ '@jridgewell/remapping': 2.3.5
+ enhanced-resolve: 5.21.6
+ jiti: 2.7.0
+ lightningcss: 1.32.0
+ magic-string: 0.30.21
+ source-map-js: 1.2.1
+ tailwindcss: 4.3.1
+
+ '@tailwindcss/oxide-android-arm64@4.3.1':
+ optional: true
+
+ '@tailwindcss/oxide-darwin-arm64@4.3.1':
+ optional: true
+
+ '@tailwindcss/oxide-darwin-x64@4.3.1':
+ optional: true
+
+ '@tailwindcss/oxide-freebsd-x64@4.3.1':
+ optional: true
+
+ '@tailwindcss/oxide-linux-arm-gnueabihf@4.3.1':
+ optional: true
+
+ '@tailwindcss/oxide-linux-arm64-gnu@4.3.1':
+ optional: true
+
+ '@tailwindcss/oxide-linux-arm64-musl@4.3.1':
+ optional: true
+
+ '@tailwindcss/oxide-linux-x64-gnu@4.3.1':
+ optional: true
+
+ '@tailwindcss/oxide-linux-x64-musl@4.3.1':
+ optional: true
+
+ '@tailwindcss/oxide-wasm32-wasi@4.3.1':
+ optional: true
+
+ '@tailwindcss/oxide-win32-arm64-msvc@4.3.1':
+ optional: true
+
+ '@tailwindcss/oxide-win32-x64-msvc@4.3.1':
+ optional: true
+
+ '@tailwindcss/oxide@4.3.1':
+ optionalDependencies:
+ '@tailwindcss/oxide-android-arm64': 4.3.1
+ '@tailwindcss/oxide-darwin-arm64': 4.3.1
+ '@tailwindcss/oxide-darwin-x64': 4.3.1
+ '@tailwindcss/oxide-freebsd-x64': 4.3.1
+ '@tailwindcss/oxide-linux-arm-gnueabihf': 4.3.1
+ '@tailwindcss/oxide-linux-arm64-gnu': 4.3.1
+ '@tailwindcss/oxide-linux-arm64-musl': 4.3.1
+ '@tailwindcss/oxide-linux-x64-gnu': 4.3.1
+ '@tailwindcss/oxide-linux-x64-musl': 4.3.1
+ '@tailwindcss/oxide-wasm32-wasi': 4.3.1
+ '@tailwindcss/oxide-win32-arm64-msvc': 4.3.1
+ '@tailwindcss/oxide-win32-x64-msvc': 4.3.1
+
+ '@tailwindcss/postcss@4.3.1':
+ dependencies:
+ '@alloc/quick-lru': 5.2.0
+ '@tailwindcss/node': 4.3.1
+ '@tailwindcss/oxide': 4.3.1
+ postcss: 8.5.15
+ tailwindcss: 4.3.1
+
+ '@tailwindcss/vite@4.3.1(vite@8.0.14(@types/node@25.9.1)(esbuild@0.28.0)(jiti@2.7.0)(terser@5.47.1)(tsx@4.22.3)(yaml@2.8.4))':
+ dependencies:
+ '@tailwindcss/node': 4.3.1
+ '@tailwindcss/oxide': 4.3.1
+ tailwindcss: 4.3.1
+ vite: 8.0.14(@types/node@25.9.1)(esbuild@0.28.0)(jiti@2.7.0)(terser@5.47.1)(tsx@4.22.3)(yaml@2.8.4)
+
'@testing-library/dom@10.4.1':
dependencies:
'@babel/code-frame': 7.29.0
@@ -9673,6 +10307,27 @@ snapshots:
'@types/aria-query@5.0.4': {}
+ '@types/babel__core@7.20.5':
+ dependencies:
+ '@babel/parser': 7.29.3
+ '@babel/types': 7.29.0
+ '@types/babel__generator': 7.27.0
+ '@types/babel__template': 7.4.4
+ '@types/babel__traverse': 7.28.0
+
+ '@types/babel__generator@7.27.0':
+ dependencies:
+ '@babel/types': 7.29.0
+
+ '@types/babel__template@7.4.4':
+ dependencies:
+ '@babel/parser': 7.29.3
+ '@babel/types': 7.29.0
+
+ '@types/babel__traverse@7.28.0':
+ dependencies:
+ '@babel/types': 7.29.0
+
'@types/chai@5.2.3':
dependencies:
'@types/deep-eql': 4.0.2
@@ -9801,6 +10456,8 @@ snapshots:
'@types/deep-eql@4.0.2': {}
+ '@types/doctrine@0.0.9': {}
+
'@types/esrecurse@4.3.1': {}
'@types/estree@1.0.8': {}
@@ -9832,11 +10489,9 @@ snapshots:
'@types/mdurl@2.0.0': {}
- '@types/ms@2.1.0': {}
+ '@types/mdx@2.0.14': {}
- '@types/node@25.7.0':
- dependencies:
- undici-types: 7.21.0
+ '@types/ms@2.1.0': {}
'@types/node@25.9.1':
dependencies:
@@ -9861,7 +10516,7 @@ snapshots:
'@types/ws@8.18.1':
dependencies:
- '@types/node': 25.7.0
+ '@types/node': 25.9.1
'@typescript-eslint/eslint-plugin@8.59.2(@typescript-eslint/parser@8.59.2(eslint@10.4.0(jiti@2.7.0))(typescript@6.0.3))(eslint@10.4.0(jiti@2.7.0))(typescript@6.0.3)':
dependencies:
@@ -10009,19 +10664,13 @@ snapshots:
'@babel/core': 7.29.0
'@babel/plugin-syntax-typescript': 7.28.6(@babel/core@7.29.0)
'@babel/plugin-transform-typescript': 7.28.6(@babel/core@7.29.0)
- '@rolldown/pluginutils': 1.0.0
+ '@rolldown/pluginutils': 1.0.1
'@vue/babel-plugin-jsx': 2.0.1(@babel/core@7.29.0)
vite: 7.3.3(@types/node@25.9.1)(jiti@2.7.0)(lightningcss@1.32.0)(terser@5.47.1)(tsx@4.22.3)(yaml@2.8.4)
vue: 3.5.34(typescript@6.0.3)
transitivePeerDependencies:
- supports-color
- '@vitejs/plugin-vue@6.0.6(vite@7.3.3(@types/node@25.9.1)(jiti@2.7.0)(lightningcss@1.32.0)(terser@5.47.1)(tsx@4.22.3)(yaml@2.8.4))(vue@3.5.34(typescript@6.0.3))':
- dependencies:
- '@rolldown/pluginutils': 1.0.0-rc.13
- vite: 7.3.3(@types/node@25.9.1)(jiti@2.7.0)(lightningcss@1.32.0)(terser@5.47.1)(tsx@4.22.3)(yaml@2.8.4)
- vue: 3.5.34(typescript@6.0.3)
-
'@vitejs/plugin-vue@6.0.7(vite@7.3.3(@types/node@25.9.1)(jiti@2.7.0)(lightningcss@1.32.0)(terser@5.47.1)(tsx@4.22.3)(yaml@2.8.4))(vue@3.5.34(typescript@6.0.3))':
dependencies:
'@rolldown/pluginutils': 1.0.1
@@ -10069,10 +10718,6 @@ snapshots:
dependencies:
tinyrainbow: 2.0.0
- '@vitest/pretty-format@4.1.5':
- dependencies:
- tinyrainbow: 3.1.0
-
'@vitest/pretty-format@4.1.7':
dependencies:
tinyrainbow: 3.1.0
@@ -10101,12 +10746,6 @@ snapshots:
loupe: 3.2.1
tinyrainbow: 2.0.0
- '@vitest/utils@4.1.5':
- dependencies:
- '@vitest/pretty-format': 4.1.5
- convert-source-map: 2.0.0
- tinyrainbow: 3.1.0
-
'@vitest/utils@4.1.7':
dependencies:
'@vitest/pretty-format': 4.1.7
@@ -10174,7 +10813,7 @@ snapshots:
'@vue/shared': 3.5.34
estree-walker: 2.0.2
magic-string: 0.30.21
- postcss: 8.5.14
+ postcss: 8.5.15
source-map-js: 1.2.1
'@vue/compiler-ssr@3.5.34':
@@ -10303,8 +10942,6 @@ snapshots:
ansi-styles@6.2.3: {}
- ansis@4.2.0: {}
-
ansis@4.3.0: {}
anymatch@3.1.3:
@@ -10376,15 +11013,17 @@ snapshots:
async@3.2.6: {}
- autoprefixer@10.5.0(postcss@8.5.14):
+ autoprefixer@10.5.0(postcss@8.5.15):
dependencies:
browserslist: 4.28.2
caniuse-lite: 1.0.30001792
fraction.js: 5.3.4
picocolors: 1.1.1
- postcss: 8.5.14
+ postcss: 8.5.15
postcss-value-parser: 4.2.0
+ axe-core@4.12.1: {}
+
b4a@1.8.1: {}
babel-plugin-transform-hook-names@1.0.2(@babel/core@7.29.0):
@@ -10493,7 +11132,7 @@ snapshots:
jsonc-parser: 3.3.1
package-manager-detector: 1.6.0
semver: 7.8.1
- tinyexec: 1.1.2
+ tinyexec: 1.2.2
tinyglobby: 0.2.16
unconfig: 7.5.0
yaml: 2.8.4
@@ -10580,6 +11219,10 @@ snapshots:
citty@0.2.2: {}
+ class-variance-authority@0.7.1:
+ dependencies:
+ clsx: 2.1.1
+
clean-regexp@1.0.0:
dependencies:
escape-string-regexp: 1.0.5
@@ -10592,6 +11235,8 @@ snapshots:
strip-ansi: 7.2.0
wrap-ansi: 9.0.2
+ clsx@2.1.1: {}
+
cluster-key-slot@1.1.2: {}
color-convert@2.0.1:
@@ -10714,48 +11359,48 @@ snapshots:
cssesc@3.0.0: {}
- cssnano-preset-default@8.0.1(postcss@8.5.14):
+ cssnano-preset-default@8.0.1(postcss@8.5.15):
dependencies:
browserslist: 4.28.2
- cssnano-utils: 6.0.0(postcss@8.5.14)
- postcss: 8.5.14
- postcss-calc: 10.1.1(postcss@8.5.14)
- postcss-colormin: 8.0.0(postcss@8.5.14)
- postcss-convert-values: 8.0.0(postcss@8.5.14)
- postcss-discard-comments: 8.0.0(postcss@8.5.14)
- postcss-discard-duplicates: 8.0.0(postcss@8.5.14)
- postcss-discard-empty: 8.0.0(postcss@8.5.14)
- postcss-discard-overridden: 8.0.0(postcss@8.5.14)
- postcss-merge-longhand: 8.0.0(postcss@8.5.14)
- postcss-merge-rules: 8.0.0(postcss@8.5.14)
- postcss-minify-font-values: 8.0.0(postcss@8.5.14)
- postcss-minify-gradients: 8.0.0(postcss@8.5.14)
- postcss-minify-params: 8.0.0(postcss@8.5.14)
- postcss-minify-selectors: 8.0.1(postcss@8.5.14)
- postcss-normalize-charset: 8.0.0(postcss@8.5.14)
- postcss-normalize-display-values: 8.0.0(postcss@8.5.14)
- postcss-normalize-positions: 8.0.0(postcss@8.5.14)
- postcss-normalize-repeat-style: 8.0.0(postcss@8.5.14)
- postcss-normalize-string: 8.0.0(postcss@8.5.14)
- postcss-normalize-timing-functions: 8.0.0(postcss@8.5.14)
- postcss-normalize-unicode: 8.0.0(postcss@8.5.14)
- postcss-normalize-url: 8.0.0(postcss@8.5.14)
- postcss-normalize-whitespace: 8.0.0(postcss@8.5.14)
- postcss-ordered-values: 8.0.0(postcss@8.5.14)
- postcss-reduce-initial: 8.0.0(postcss@8.5.14)
- postcss-reduce-transforms: 8.0.0(postcss@8.5.14)
- postcss-svgo: 8.0.0(postcss@8.5.14)
- postcss-unique-selectors: 8.0.0(postcss@8.5.14)
-
- cssnano-utils@6.0.0(postcss@8.5.14):
- dependencies:
- postcss: 8.5.14
-
- cssnano@8.0.1(postcss@8.5.14):
- dependencies:
- cssnano-preset-default: 8.0.1(postcss@8.5.14)
+ cssnano-utils: 6.0.0(postcss@8.5.15)
+ postcss: 8.5.15
+ postcss-calc: 10.1.1(postcss@8.5.15)
+ postcss-colormin: 8.0.0(postcss@8.5.15)
+ postcss-convert-values: 8.0.0(postcss@8.5.15)
+ postcss-discard-comments: 8.0.0(postcss@8.5.15)
+ postcss-discard-duplicates: 8.0.0(postcss@8.5.15)
+ postcss-discard-empty: 8.0.0(postcss@8.5.15)
+ postcss-discard-overridden: 8.0.0(postcss@8.5.15)
+ postcss-merge-longhand: 8.0.0(postcss@8.5.15)
+ postcss-merge-rules: 8.0.0(postcss@8.5.15)
+ postcss-minify-font-values: 8.0.0(postcss@8.5.15)
+ postcss-minify-gradients: 8.0.0(postcss@8.5.15)
+ postcss-minify-params: 8.0.0(postcss@8.5.15)
+ postcss-minify-selectors: 8.0.1(postcss@8.5.15)
+ postcss-normalize-charset: 8.0.0(postcss@8.5.15)
+ postcss-normalize-display-values: 8.0.0(postcss@8.5.15)
+ postcss-normalize-positions: 8.0.0(postcss@8.5.15)
+ postcss-normalize-repeat-style: 8.0.0(postcss@8.5.15)
+ postcss-normalize-string: 8.0.0(postcss@8.5.15)
+ postcss-normalize-timing-functions: 8.0.0(postcss@8.5.15)
+ postcss-normalize-unicode: 8.0.0(postcss@8.5.15)
+ postcss-normalize-url: 8.0.0(postcss@8.5.15)
+ postcss-normalize-whitespace: 8.0.0(postcss@8.5.15)
+ postcss-ordered-values: 8.0.0(postcss@8.5.15)
+ postcss-reduce-initial: 8.0.0(postcss@8.5.15)
+ postcss-reduce-transforms: 8.0.0(postcss@8.5.15)
+ postcss-svgo: 8.0.0(postcss@8.5.15)
+ postcss-unique-selectors: 8.0.0(postcss@8.5.15)
+
+ cssnano-utils@6.0.0(postcss@8.5.15):
+ dependencies:
+ postcss: 8.5.15
+
+ cssnano@8.0.1(postcss@8.5.15):
+ dependencies:
+ cssnano-preset-default: 8.0.1(postcss@8.5.15)
lilconfig: 3.1.3
- postcss: 8.5.14
+ postcss: 8.5.15
csso@5.0.5:
dependencies:
@@ -11000,6 +11645,10 @@ snapshots:
diff@8.0.4: {}
+ doctrine@3.0.0:
+ dependencies:
+ esutils: 2.0.3
+
dom-accessibility-api@0.5.16: {}
dom-accessibility-api@0.6.3: {}
@@ -11060,7 +11709,7 @@ snapshots:
encodeurl@2.0.0: {}
- enhanced-resolve@5.21.2:
+ enhanced-resolve@5.21.6:
dependencies:
graceful-fs: 4.2.11
tapable: 2.3.3
@@ -11239,7 +11888,7 @@ snapshots:
eslint-plugin-n@18.0.1(eslint@10.4.0(jiti@2.7.0))(typescript@6.0.3):
dependencies:
'@eslint-community/eslint-utils': 4.9.1(eslint@10.4.0(jiti@2.7.0))
- enhanced-resolve: 5.21.2
+ enhanced-resolve: 5.21.6
eslint: 10.4.0(jiti@2.7.0)
eslint-plugin-es-x: 7.8.0(eslint@10.4.0(jiti@2.7.0))
get-tsconfig: 4.14.0
@@ -12103,6 +12752,10 @@ snapshots:
dependencies:
yallist: 3.1.1
+ lucide-react@1.20.0(react@19.2.6):
+ dependencies:
+ react: 19.2.6
+
lz-string@1.5.0: {}
magic-regexp@0.11.0:
@@ -12544,6 +13197,8 @@ snapshots:
dependencies:
brace-expansion: 2.1.0
+ minimist@1.2.8: {}
+
minipass@7.1.3: {}
minisearch@7.2.0: {}
@@ -12581,7 +13236,7 @@ snapshots:
negotiator@1.0.0: {}
- next@16.2.6(@playwright/test@1.60.0)(react-dom@19.2.6(react@19.2.6))(react@19.2.6):
+ next@16.2.6(@babel/core@7.29.0)(@playwright/test@1.60.0)(react-dom@19.2.6(react@19.2.6))(react@19.2.6):
dependencies:
'@next/env': 16.2.6
'@swc/helpers': 0.5.15
@@ -12590,7 +13245,7 @@ snapshots:
postcss: 8.4.31
react: 19.2.6
react-dom: 19.2.6(react@19.2.6)
- styled-jsx: 5.1.6(react@19.2.6)
+ styled-jsx: 5.1.6(@babel/core@7.29.0)(react@19.2.6)
optionalDependencies:
'@next/swc-darwin-arm64': 16.2.6
'@next/swc-darwin-x64': 16.2.6
@@ -12671,7 +13326,7 @@ snapshots:
uncrypto: 0.1.3
unctx: 2.5.0
unenv: 2.0.0-rc.24
- unimport: 6.2.0(oxc-parser@0.131.0)
+ unimport: 6.3.0(oxc-parser@0.131.0)(rolldown@1.0.2)
unplugin-utils: 0.3.1
unstorage: 1.17.5(db0@0.3.4)(ioredis@5.10.1)
untyped: 2.0.0
@@ -12890,7 +13545,7 @@ snapshots:
dependencies:
citty: 0.2.2
pathe: 2.0.3
- tinyexec: 1.1.2
+ tinyexec: 1.2.2
object-assign@4.1.1: {}
@@ -13204,144 +13859,144 @@ snapshots:
path-data-parser: 0.1.0
points-on-curve: 0.2.0
- postcss-calc@10.1.1(postcss@8.5.14):
+ postcss-calc@10.1.1(postcss@8.5.15):
dependencies:
- postcss: 8.5.14
+ postcss: 8.5.15
postcss-selector-parser: 7.1.1
postcss-value-parser: 4.2.0
- postcss-colormin@8.0.0(postcss@8.5.14):
+ postcss-colormin@8.0.0(postcss@8.5.15):
dependencies:
'@colordx/core': 5.4.3
browserslist: 4.28.2
caniuse-api: 3.0.0
- postcss: 8.5.14
+ postcss: 8.5.15
postcss-value-parser: 4.2.0
- postcss-convert-values@8.0.0(postcss@8.5.14):
+ postcss-convert-values@8.0.0(postcss@8.5.15):
dependencies:
browserslist: 4.28.2
- postcss: 8.5.14
+ postcss: 8.5.15
postcss-value-parser: 4.2.0
- postcss-discard-comments@8.0.0(postcss@8.5.14):
+ postcss-discard-comments@8.0.0(postcss@8.5.15):
dependencies:
- postcss: 8.5.14
+ postcss: 8.5.15
postcss-selector-parser: 7.1.1
- postcss-discard-duplicates@8.0.0(postcss@8.5.14):
+ postcss-discard-duplicates@8.0.0(postcss@8.5.15):
dependencies:
- postcss: 8.5.14
+ postcss: 8.5.15
- postcss-discard-empty@8.0.0(postcss@8.5.14):
+ postcss-discard-empty@8.0.0(postcss@8.5.15):
dependencies:
- postcss: 8.5.14
+ postcss: 8.5.15
- postcss-discard-overridden@8.0.0(postcss@8.5.14):
+ postcss-discard-overridden@8.0.0(postcss@8.5.15):
dependencies:
- postcss: 8.5.14
+ postcss: 8.5.15
- postcss-merge-longhand@8.0.0(postcss@8.5.14):
+ postcss-merge-longhand@8.0.0(postcss@8.5.15):
dependencies:
- postcss: 8.5.14
+ postcss: 8.5.15
postcss-value-parser: 4.2.0
- stylehacks: 8.0.0(postcss@8.5.14)
+ stylehacks: 8.0.0(postcss@8.5.15)
- postcss-merge-rules@8.0.0(postcss@8.5.14):
+ postcss-merge-rules@8.0.0(postcss@8.5.15):
dependencies:
browserslist: 4.28.2
caniuse-api: 3.0.0
- cssnano-utils: 6.0.0(postcss@8.5.14)
- postcss: 8.5.14
+ cssnano-utils: 6.0.0(postcss@8.5.15)
+ postcss: 8.5.15
postcss-selector-parser: 7.1.1
- postcss-minify-font-values@8.0.0(postcss@8.5.14):
+ postcss-minify-font-values@8.0.0(postcss@8.5.15):
dependencies:
- postcss: 8.5.14
+ postcss: 8.5.15
postcss-value-parser: 4.2.0
- postcss-minify-gradients@8.0.0(postcss@8.5.14):
+ postcss-minify-gradients@8.0.0(postcss@8.5.15):
dependencies:
'@colordx/core': 5.4.3
- cssnano-utils: 6.0.0(postcss@8.5.14)
- postcss: 8.5.14
+ cssnano-utils: 6.0.0(postcss@8.5.15)
+ postcss: 8.5.15
postcss-value-parser: 4.2.0
- postcss-minify-params@8.0.0(postcss@8.5.14):
+ postcss-minify-params@8.0.0(postcss@8.5.15):
dependencies:
browserslist: 4.28.2
- cssnano-utils: 6.0.0(postcss@8.5.14)
- postcss: 8.5.14
+ cssnano-utils: 6.0.0(postcss@8.5.15)
+ postcss: 8.5.15
postcss-value-parser: 4.2.0
- postcss-minify-selectors@8.0.1(postcss@8.5.14):
+ postcss-minify-selectors@8.0.1(postcss@8.5.15):
dependencies:
browserslist: 4.28.2
caniuse-api: 3.0.0
cssesc: 3.0.0
- postcss: 8.5.14
+ postcss: 8.5.15
postcss-selector-parser: 7.1.1
- postcss-normalize-charset@8.0.0(postcss@8.5.14):
+ postcss-normalize-charset@8.0.0(postcss@8.5.15):
dependencies:
- postcss: 8.5.14
+ postcss: 8.5.15
- postcss-normalize-display-values@8.0.0(postcss@8.5.14):
+ postcss-normalize-display-values@8.0.0(postcss@8.5.15):
dependencies:
- postcss: 8.5.14
+ postcss: 8.5.15
postcss-value-parser: 4.2.0
- postcss-normalize-positions@8.0.0(postcss@8.5.14):
+ postcss-normalize-positions@8.0.0(postcss@8.5.15):
dependencies:
- postcss: 8.5.14
+ postcss: 8.5.15
postcss-value-parser: 4.2.0
- postcss-normalize-repeat-style@8.0.0(postcss@8.5.14):
+ postcss-normalize-repeat-style@8.0.0(postcss@8.5.15):
dependencies:
- postcss: 8.5.14
+ postcss: 8.5.15
postcss-value-parser: 4.2.0
- postcss-normalize-string@8.0.0(postcss@8.5.14):
+ postcss-normalize-string@8.0.0(postcss@8.5.15):
dependencies:
- postcss: 8.5.14
+ postcss: 8.5.15
postcss-value-parser: 4.2.0
- postcss-normalize-timing-functions@8.0.0(postcss@8.5.14):
+ postcss-normalize-timing-functions@8.0.0(postcss@8.5.15):
dependencies:
- postcss: 8.5.14
+ postcss: 8.5.15
postcss-value-parser: 4.2.0
- postcss-normalize-unicode@8.0.0(postcss@8.5.14):
+ postcss-normalize-unicode@8.0.0(postcss@8.5.15):
dependencies:
browserslist: 4.28.2
- postcss: 8.5.14
+ postcss: 8.5.15
postcss-value-parser: 4.2.0
- postcss-normalize-url@8.0.0(postcss@8.5.14):
+ postcss-normalize-url@8.0.0(postcss@8.5.15):
dependencies:
- postcss: 8.5.14
+ postcss: 8.5.15
postcss-value-parser: 4.2.0
- postcss-normalize-whitespace@8.0.0(postcss@8.5.14):
+ postcss-normalize-whitespace@8.0.0(postcss@8.5.15):
dependencies:
- postcss: 8.5.14
+ postcss: 8.5.15
postcss-value-parser: 4.2.0
- postcss-ordered-values@8.0.0(postcss@8.5.14):
+ postcss-ordered-values@8.0.0(postcss@8.5.15):
dependencies:
- cssnano-utils: 6.0.0(postcss@8.5.14)
- postcss: 8.5.14
+ cssnano-utils: 6.0.0(postcss@8.5.15)
+ postcss: 8.5.15
postcss-value-parser: 4.2.0
- postcss-reduce-initial@8.0.0(postcss@8.5.14):
+ postcss-reduce-initial@8.0.0(postcss@8.5.15):
dependencies:
browserslist: 4.28.2
caniuse-api: 3.0.0
- postcss: 8.5.14
+ postcss: 8.5.15
- postcss-reduce-transforms@8.0.0(postcss@8.5.14):
+ postcss-reduce-transforms@8.0.0(postcss@8.5.15):
dependencies:
- postcss: 8.5.14
+ postcss: 8.5.15
postcss-value-parser: 4.2.0
postcss-selector-parser@7.1.1:
@@ -13349,15 +14004,15 @@ snapshots:
cssesc: 3.0.0
util-deprecate: 1.0.2
- postcss-svgo@8.0.0(postcss@8.5.14):
+ postcss-svgo@8.0.0(postcss@8.5.15):
dependencies:
- postcss: 8.5.14
+ postcss: 8.5.15
postcss-value-parser: 4.2.0
svgo: 4.0.1
- postcss-unique-selectors@8.0.0(postcss@8.5.14):
+ postcss-unique-selectors@8.0.0(postcss@8.5.15):
dependencies:
- postcss: 8.5.14
+ postcss: 8.5.15
postcss-selector-parser: 7.1.1
postcss-value-parser@4.2.0: {}
@@ -13368,12 +14023,6 @@ snapshots:
picocolors: 1.1.1
source-map-js: 1.2.1
- postcss@8.5.14:
- dependencies:
- nanoid: 3.3.12
- picocolors: 1.1.1
- source-map-js: 1.2.1
-
postcss@8.5.15:
dependencies:
nanoid: 3.3.12
@@ -13439,6 +14088,25 @@ snapshots:
defu: 6.1.7
destr: 2.0.5
+ react-docgen-typescript@2.4.0(typescript@6.0.3):
+ dependencies:
+ typescript: 6.0.3
+
+ react-docgen@8.0.3:
+ dependencies:
+ '@babel/core': 7.29.0
+ '@babel/traverse': 7.29.0
+ '@babel/types': 7.29.0
+ '@types/babel__core': 7.20.5
+ '@types/babel__traverse': 7.28.0
+ '@types/doctrine': 0.0.9
+ '@types/resolve': 1.20.2
+ doctrine: 3.0.0
+ resolve: 1.22.12
+ strip-indent: 4.1.1
+ transitivePeerDependencies:
+ - supports-color
+
react-dom@19.2.6(react@19.2.6):
dependencies:
react: 19.2.6
@@ -13537,7 +14205,7 @@ snapshots:
robust-predicates@3.0.3: {}
- rolldown-plugin-dts@0.25.0(oxc-resolver@11.21.3)(rolldown@1.0.0)(typescript@6.0.3):
+ rolldown-plugin-dts@0.25.0(oxc-resolver@11.21.3)(rolldown@1.0.2)(typescript@6.0.3):
dependencies:
'@babel/generator': 8.0.0-rc.4
'@babel/helper-validator-identifier': 8.0.0-rc.4
@@ -13547,37 +14215,16 @@ snapshots:
dts-resolver: 3.0.0(oxc-resolver@11.21.3)
get-tsconfig: 5.0.0-beta.5
obug: 2.1.1
- rolldown: 1.0.0
+ rolldown: 1.0.2
optionalDependencies:
typescript: 6.0.3
transitivePeerDependencies:
- oxc-resolver
- rolldown@1.0.0:
- dependencies:
- '@oxc-project/types': 0.129.0
- '@rolldown/pluginutils': 1.0.0
- optionalDependencies:
- '@rolldown/binding-android-arm64': 1.0.0
- '@rolldown/binding-darwin-arm64': 1.0.0
- '@rolldown/binding-darwin-x64': 1.0.0
- '@rolldown/binding-freebsd-x64': 1.0.0
- '@rolldown/binding-linux-arm-gnueabihf': 1.0.0
- '@rolldown/binding-linux-arm64-gnu': 1.0.0
- '@rolldown/binding-linux-arm64-musl': 1.0.0
- '@rolldown/binding-linux-ppc64-gnu': 1.0.0
- '@rolldown/binding-linux-s390x-gnu': 1.0.0
- '@rolldown/binding-linux-x64-gnu': 1.0.0
- '@rolldown/binding-linux-x64-musl': 1.0.0
- '@rolldown/binding-openharmony-arm64': 1.0.0
- '@rolldown/binding-wasm32-wasi': 1.0.0
- '@rolldown/binding-win32-arm64-msvc': 1.0.0
- '@rolldown/binding-win32-x64-msvc': 1.0.0
-
rolldown@1.0.2:
dependencies:
'@oxc-project/types': 0.132.0
- '@rolldown/pluginutils': 1.0.0
+ '@rolldown/pluginutils': 1.0.1
optionalDependencies:
'@rolldown/binding-android-arm64': 1.0.2
'@rolldown/binding-darwin-arm64': 1.0.2
@@ -13959,6 +14606,8 @@ snapshots:
strip-bom-string@1.0.0: {}
+ strip-bom@3.0.0: {}
+
strip-final-newline@3.0.0: {}
strip-indent@3.0.0:
@@ -13973,15 +14622,17 @@ snapshots:
structured-clone-es@2.0.0: {}
- styled-jsx@5.1.6(react@19.2.6):
+ styled-jsx@5.1.6(@babel/core@7.29.0)(react@19.2.6):
dependencies:
client-only: 0.0.1
react: 19.2.6
+ optionalDependencies:
+ '@babel/core': 7.29.0
- stylehacks@8.0.0(postcss@8.5.14):
+ stylehacks@8.0.0(postcss@8.5.15):
dependencies:
browserslist: 4.28.2
- postcss: 8.5.14
+ postcss: 8.5.15
postcss-selector-parser: 7.1.1
stylis@4.4.0: {}
@@ -14008,6 +14659,10 @@ snapshots:
tagged-tag@1.0.0: {}
+ tailwind-merge@3.6.0: {}
+
+ tailwindcss@4.3.1: {}
+
tapable@2.3.3: {}
tar-stream@3.2.0:
@@ -14055,8 +14710,6 @@ snapshots:
tinyclip@0.1.12: {}
- tinyexec@1.1.2: {}
-
tinyexec@1.2.2: {}
tinyglobby@0.2.16:
@@ -14099,9 +14752,15 @@ snapshots:
ts-dedent@2.2.0: {}
+ tsconfig-paths@4.2.0:
+ dependencies:
+ json5: 2.2.3
+ minimist: 1.2.8
+ strip-bom: 3.0.0
+
tsdown@0.22.0(oxc-resolver@11.21.3)(tsx@4.22.3)(typescript@6.0.3):
dependencies:
- ansis: 4.2.0
+ ansis: 4.3.0
cac: 7.0.0
defu: 6.1.7
empathic: 2.0.1
@@ -14109,10 +14768,10 @@ snapshots:
import-without-cache: 0.4.0
obug: 2.1.1
picomatch: 4.0.4
- rolldown: 1.0.0
- rolldown-plugin-dts: 0.25.0(oxc-resolver@11.21.3)(rolldown@1.0.0)(typescript@6.0.3)
+ rolldown: 1.0.2
+ rolldown-plugin-dts: 0.25.0(oxc-resolver@11.21.3)(rolldown@1.0.2)(typescript@6.0.3)
semver: 7.8.1
- tinyexec: 1.1.2
+ tinyexec: 1.2.2
tinyglobby: 0.2.16
tree-kill: 1.2.2
unconfig-core: 7.5.0
@@ -14129,7 +14788,7 @@ snapshots:
tsnapi@0.3.3(vitest@4.1.7(@types/node@25.9.1)(vite@8.0.14(@types/node@25.9.1)(esbuild@0.28.0)(jiti@2.7.0)(terser@5.47.1)(tsx@4.22.3)(yaml@2.8.4))):
dependencies:
- '@vitest/utils': 4.1.5
+ '@vitest/utils': 4.1.7
cac: 7.0.0
magic-string: 0.30.21
oxc-parser: 0.129.0
@@ -14152,6 +14811,8 @@ snapshots:
'@turbo/windows-64': 2.9.15
'@turbo/windows-arm64': 2.9.15
+ tw-animate-css@1.4.0: {}
+
type-check@0.4.0:
dependencies:
prelude-ls: 1.2.1
@@ -14198,8 +14859,6 @@ snapshots:
magic-string: 0.30.21
unplugin: 2.3.11
- undici-types@7.21.0: {}
-
undici-types@7.24.6: {}
unenv@2.0.0-rc.24:
@@ -14214,25 +14873,6 @@ snapshots:
unicorn-magic@0.4.0: {}
- unimport@6.2.0(oxc-parser@0.131.0):
- dependencies:
- acorn: 8.16.0
- escape-string-regexp: 5.0.0
- estree-walker: 3.0.3
- local-pkg: 1.1.2
- magic-string: 0.30.21
- mlly: 1.8.2
- pathe: 2.0.3
- picomatch: 4.0.4
- pkg-types: 2.3.1
- scule: 1.3.0
- strip-literal: 3.1.0
- tinyglobby: 0.2.16
- unplugin: 3.0.0
- unplugin-utils: 0.3.1
- optionalDependencies:
- oxc-parser: 0.131.0
-
unimport@6.3.0(oxc-parser@0.131.0)(rolldown@1.0.2):
dependencies:
acorn: 8.16.0
@@ -14426,7 +15066,7 @@ snapshots:
optionator: 0.9.4
typescript: 6.0.3
- vite-plugin-inspect@11.3.3(@nuxt/kit@4.4.5(magicast@0.5.2))(vite@8.0.14(@types/node@25.9.1)(esbuild@0.28.0)(jiti@2.7.0)(terser@5.47.1)(tsx@4.22.3)(yaml@2.8.4)):
+ vite-plugin-inspect@11.3.3(@nuxt/kit@4.4.6(magicast@0.5.2))(vite@8.0.14(@types/node@25.9.1)(esbuild@0.28.0)(jiti@2.7.0)(terser@5.47.1)(tsx@4.22.3)(yaml@2.8.4)):
dependencies:
ansis: 4.3.0
debug: 4.4.3
@@ -14439,7 +15079,7 @@ snapshots:
vite: 8.0.14(@types/node@25.9.1)(esbuild@0.28.0)(jiti@2.7.0)(terser@5.47.1)(tsx@4.22.3)(yaml@2.8.4)
vite-dev-rpc: 1.1.0(vite@8.0.14(@types/node@25.9.1)(esbuild@0.28.0)(jiti@2.7.0)(terser@5.47.1)(tsx@4.22.3)(yaml@2.8.4))
optionalDependencies:
- '@nuxt/kit': 4.4.5(magicast@0.5.2)
+ '@nuxt/kit': 4.4.6(magicast@0.5.2)
transitivePeerDependencies:
- supports-color
@@ -14468,7 +15108,7 @@ snapshots:
esbuild: 0.27.7
fdir: 6.5.0(picomatch@4.0.4)
picomatch: 4.0.4
- postcss: 8.5.14
+ postcss: 8.5.15
rollup: 4.60.3
tinyglobby: 0.2.16
optionalDependencies:
@@ -14513,7 +15153,7 @@ snapshots:
'@shikijs/transformers': 3.23.0
'@shikijs/types': 3.23.0
'@types/markdown-it': 14.1.2
- '@vitejs/plugin-vue': 6.0.6(vite@7.3.3(@types/node@25.9.1)(jiti@2.7.0)(lightningcss@1.32.0)(terser@5.47.1)(tsx@4.22.3)(yaml@2.8.4))(vue@3.5.34(typescript@6.0.3))
+ '@vitejs/plugin-vue': 6.0.7(vite@7.3.3(@types/node@25.9.1)(jiti@2.7.0)(lightningcss@1.32.0)(terser@5.47.1)(tsx@4.22.3)(yaml@2.8.4))(vue@3.5.34(typescript@6.0.3))
'@vue/devtools-api': 8.1.2
'@vue/shared': 3.5.34
'@vueuse/core': 14.3.0(vue@3.5.34(typescript@6.0.3))
@@ -14569,7 +15209,7 @@ snapshots:
picomatch: 4.0.4
std-env: 4.1.0
tinybench: 2.9.0
- tinyexec: 1.1.2
+ tinyexec: 1.2.2
tinyglobby: 0.2.16
tinyrainbow: 3.1.0
vite: 8.0.14(@types/node@25.9.1)(esbuild@0.28.0)(jiti@2.7.0)(terser@5.47.1)(tsx@4.22.3)(yaml@2.8.4)
diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml
index ceb1423..e3c52ed 100644
--- a/pnpm-workspace.yaml
+++ b/pnpm-workspace.yaml
@@ -62,10 +62,26 @@ catalogs:
vitepress: ^2.0.0-alpha.17
vitepress-plugin-mermaid: ^2.0.17
frontend:
+ '@radix-ui/react-scroll-area': ^1.2.12
+ '@radix-ui/react-separator': ^1.1.10
+ '@radix-ui/react-slot': ^1.3.0
+ '@radix-ui/react-tabs': ^1.1.15
+ '@storybook/addon-a11y': ^10.4.6
+ '@storybook/addon-docs': ^10.4.6
+ '@storybook/react-vite': ^10.4.6
+ '@tailwindcss/postcss': ^4.3.1
+ '@tailwindcss/vite': ^4.3.1
+ class-variance-authority: ^0.7.1
+ clsx: ^2.1.1
+ lucide-react: ^1.20.0
next: ^16.2.6
preact: ^10.29.2
react: ^19.2.6
react-dom: ^19.2.6
+ storybook: ^10.4.6
+ tailwind-merge: ^3.6.0
+ tailwindcss: ^4.3.1
+ tw-animate-css: ^1.4.0
inlined:
'@antfu/utils': ^9.3.0
ua-parser-modern: ^0.1.1
diff --git a/tests/__snapshots__/tsnapi/@devframes/plugin-git/index.snapshot.d.ts b/tests/__snapshots__/tsnapi/@devframes/plugin-git/index.snapshot.d.ts
new file mode 100644
index 0000000..42974cf
--- /dev/null
+++ b/tests/__snapshots__/tsnapi/@devframes/plugin-git/index.snapshot.d.ts
@@ -0,0 +1,117 @@
+/**
+ * Generated by tsnapi — public API snapshot of `@devframes/plugin-git`
+ */
+// #region Interfaces
+export interface Branch {
+ name: string;
+ current: boolean;
+ sha: string;
+ upstream: string | null;
+ subject: string;
+ ahead: number;
+ behind: number;
+ gone: boolean;
+}
+export interface Commit {
+ hash: string;
+ shortHash: string;
+ author: string;
+ email: string;
+ date: number;
+ subject: string;
+ body: string;
+ refs: string[];
+ parents: string[];
+}
+export interface CommitArgs {
+ message: string;
+}
+export interface CommitResult {
+ ok: boolean;
+ hash: string | null;
+ message: string;
+ status: GitStatus;
+}
+export interface DiffArgs {
+ path?: string;
+ staged?: boolean;
+}
+export interface DiffFile {
+ path: string;
+ additions: number;
+ deletions: number;
+ binary: boolean;
+}
+export interface GitBranches {
+ isRepo: boolean;
+ current: string | null;
+ branches: Branch[];
+}
+export interface GitDevframeOptions {
+ repoRoot?: string;
+ basePath?: string;
+ distDir?: string;
+ port?: number;
+ write?: boolean;
+}
+export interface GitDiff {
+ isRepo: boolean;
+ staged: boolean;
+ path: string | null;
+ files: DiffFile[];
+ totalAdditions: number;
+ totalDeletions: number;
+ patch: string | null;
+ truncated: boolean;
+}
+export interface GitLog {
+ isRepo: boolean;
+ commits: Commit[];
+ limit: number;
+ skip: number;
+ hasMore: boolean;
+}
+export interface GitStatus {
+ isRepo: boolean;
+ root: string | null;
+ branch: string | null;
+ detached: boolean;
+ head: string | null;
+ upstream: string | null;
+ ahead: number;
+ behind: number;
+ staged: StatusFileEntry[];
+ unstaged: StatusFileEntry[];
+ untracked: string[];
+ clean: boolean;
+ canWrite: boolean;
+}
+export interface LogArgs {
+ limit?: number;
+ skip?: number;
+}
+export interface StageArgs {
+ paths: string[];
+}
+export interface StatusFileEntry {
+ path: string;
+ from?: string;
+ status: FileStatusCode;
+}
+export interface UnstageArgs {
+ paths: string[];
+}
+// #endregion
+
+// #region Types
+export type FileStatusCode = 'modified' | 'added' | 'deleted' | 'renamed' | 'copied' | 'type-changed' | 'unmerged' | 'unknown';
+// #endregion
+
+// #region Functions
+export declare function createGitDevframe(_?: GitDevframeOptions): DevframeDefinition;
+// #endregion
+
+// #region Default Export
+declare const _default: DevframeDefinition;
+export default _default
+// #endregion
\ No newline at end of file
diff --git a/tests/__snapshots__/tsnapi/@devframes/plugin-git/index.snapshot.js b/tests/__snapshots__/tsnapi/@devframes/plugin-git/index.snapshot.js
new file mode 100644
index 0000000..011a2eb
--- /dev/null
+++ b/tests/__snapshots__/tsnapi/@devframes/plugin-git/index.snapshot.js
@@ -0,0 +1,10 @@
+/**
+ * Generated by tsnapi — public API snapshot of `@devframes/plugin-git`
+ */
+// #region Default Export
+export default src_default
+// #endregion
+
+// #region Other
+export { createGitDevframe }
+// #endregion
\ No newline at end of file
diff --git a/turbo.json b/turbo.json
index c456d30..9c1d976 100644
--- a/turbo.json
+++ b/turbo.json
@@ -48,6 +48,11 @@
"dependsOn": ["devframe#build"],
"outputs": ["dist/**"]
},
+ "@devframes/plugin-git#build": {
+ "outputLogs": "new-only",
+ "dependsOn": ["devframe#build"],
+ "outputs": ["dist/**"]
+ },
"files-inspector-example#cli:build": {
"outputLogs": "new-only",
"dependsOn": ["files-inspector-example#build"],
@@ -63,6 +68,11 @@
"outputLogs": "new-only",
"dependsOn": ["next-runtime-snapshot-example#build"],
"outputs": ["dist/static/**"]
+ },
+ "@devframes/plugin-git#cli:build": {
+ "outputLogs": "new-only",
+ "dependsOn": ["@devframes/plugin-git#build"],
+ "outputs": ["dist/static/**"]
}
}
}
diff --git a/vitest.config.ts b/vitest.config.ts
index 97c753b..b1a1376 100644
--- a/vitest.config.ts
+++ b/vitest.config.ts
@@ -13,6 +13,7 @@ export default defineConfig({
'examples/files-inspector',
'examples/streaming-chat',
'examples/next-runtime-snapshot',
+ 'plugins/git',
'examples/minimal-next-devframe-hub',
{
test: {