Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions docs/.vitepress/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ const DevToolsKitNav = [
{ text: 'Dock System', link: '/kit/dock-system' },
{ text: 'RPC', link: '/kit/rpc' },
{ text: 'Shared State', link: '/kit/shared-state' },
{ text: 'Logs & Notifications', link: '/kit/logs' },
]

const SocialLinks = [
Expand Down Expand Up @@ -66,6 +67,7 @@ export default extendConfig(withMermaid(defineConfig({
{ text: 'Dock System', link: '/kit/dock-system' },
{ text: 'RPC', link: '/kit/rpc' },
{ text: 'Shared State', link: '/kit/shared-state' },
{ text: 'Logs', link: '/kit/logs' },
],
},
],
Expand Down
168 changes: 168 additions & 0 deletions docs/kit/logs.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,168 @@
# Logs & Notifications

The Logs system allows plugins to emit structured log entries from both the server (Node.js) and client (browser) contexts. Logs are displayed in the built-in **Logs** panel in the DevTools dock, and can optionally appear as toast notifications.

## Use Cases

- **Accessibility audits** — Run a11y checks or similar tools on the client side, report warnings with element positions
- **Runtime errors** — Capture and display errors with stack traces
- **Linting & testing** — Run ESLint or test runners alongside the dev server and surface results with file positions
- **Notifications** — Short-lived messages like "URL copied" that auto-dismiss

## Log Entry Fields

| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `message` | `string` | Yes | Short title or summary |
| `level` | `'info' \| 'warn' \| 'error' \| 'success' \| 'debug'` | Yes | Severity level, determines color and icon |
| `description` | `string` | No | Detailed description or explanation |
| `stacktrace` | `string` | No | Stack trace string |
| `filePosition` | `{ file, line?, column? }` | No | Source file location (clickable in the panel) |
| `elementPosition` | `{ selector?, boundingBox?, description? }` | No | DOM element position info |
| `notify` | `boolean` | No | Show as a toast notification |
| `category` | `string` | No | Grouping category (e.g., `'a11y'`, `'lint'`) |
| `labels` | `string[]` | No | Tags for filtering |
| `autoDismiss` | `number` | No | Time in ms to auto-dismiss the toast (default: 5000) |
| `autoDelete` | `number` | No | Time in ms to auto-delete the log entry |
| `status` | `'loading' \| 'idle'` | No | Status indicator (shows spinner when `'loading'`) |
| `id` | `string` | No | Explicit id for deduplication — re-adding with the same id updates the existing entry |

Comment on lines +14 to +29
Copy link

Copilot AI Mar 11, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The markdown table uses || at the start of each row, which won’t render as a table in VitePress/Markdown (it should be single | pipes). Please update the table syntax so the docs render correctly.

Copilot uses AI. Check for mistakes.
The `source` field is automatically set to `'server'` or `'client'` depending on where the log was emitted.

## Usage

Both server-side and client-side share the same `context.logs` API. All methods return Promises, but you don't need to `await` them for fire-and-forget usage.

### Fire-and-Forget

```ts
// No await needed — just emit the log
context.logs.add({
message: 'Plugin initialized',
level: 'info',
})
```

### With Handle

`await` the `add()` call to get a `DevToolsLogHandle` for subsequent updates:

```ts
// Await to get a handle for later updates
const handle = await context.logs.add({
id: 'my-build',
message: 'Building...',
level: 'info',
status: 'loading',
})

// Later, update via the handle
await handle.update({
message: 'Build complete',
level: 'success',
status: 'idle',
})

// Or dismiss it
await handle.dismiss()
```

### Server-Side Example

```ts
export function myPlugin() {
return {
name: 'my-plugin',
devtools: {
setup(context) {
// Fire-and-forget
context.logs.add({
message: 'Plugin initialized',
level: 'info',
})
},
},
}
}
```

### Client-Side Example

```ts
import type { DockClientScriptContext } from '@vitejs/devtools-kit/client'

export default async function (context: DockClientScriptContext) {
// Await to get the handle
const log = await context.logs.add({
message: 'Running audit...',
level: 'info',
status: 'loading',
notify: true,
})

// ... do work ...

// Update via handle — can also be fire-and-forget
log.update({
message: 'Audit complete — 3 issues found',
level: 'warn',
status: 'idle',
})
}
```

## Log Handle

`context.logs.add()` returns a `Promise<DevToolsLogHandle>` with:

| Property/Method | Description |
|-----------------|-------------|
| `handle.id` | The log entry id |
| `handle.entry` | The current `DevToolsLogEntry` data |
| `handle.update(patch)` | Partially update the log entry (returns `Promise`) |
| `handle.dismiss()` | Remove the log entry (returns `Promise`) |

Both `handle.update()` and `handle.dismiss()` return Promises but can be used without `await` for fire-and-forget.

## Deduplication

When you call `add()` with an explicit `id` that already exists, the existing entry is **updated** instead of duplicated. This is useful for logs that represent ongoing operations:

```ts
// First call creates the entry
context.logs.add({ id: 'my-scan', message: 'Scanning...', level: 'info', status: 'loading' })

// Second call with same id updates it
context.logs.add({ id: 'my-scan', message: 'Scan complete', level: 'success', status: 'idle' })
```

## Toast Notifications

Set `notify: true` to show the log entry as a toast notification overlay. Toasts appear regardless of whether the Logs panel is open.

```ts
context.logs.add({
message: 'URL copied to clipboard',
level: 'success',
notify: true,
autoDismiss: 2000, // disappear after 2 seconds
})
```

The default auto-dismiss time for toasts is 5 seconds.

## Managing Logs

```ts
// Remove a specific log by id
context.logs.remove(entryId)

// Clear all logs
context.logs.clear()
```

Logs have a maximum capacity of 1000 entries. When the limit is reached, the oldest entries are automatically removed.

## Dock Badge

The Logs dock icon automatically shows a badge with the total log count. The icon is hidden when there are no logs.
34 changes: 34 additions & 0 deletions examples/plugin-a11y-checker/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
{
"name": "example-plugin-a11y-checker",
"type": "module",
"version": "0.0.0-alpha.34",
"private": true,
"exports": {
".": "./dist/index.mjs",
"./package.json": "./package.json"
},
"types": "./dist/index.d.mts",
"files": [
"dist"
],
"scripts": {
"build:node": "tsdown --config-loader=tsx",
"build": "pnpm run build:node",
"play:dev": "pnpm run build && cd playground && DEBUG='vite:devtools:*' vite"
},
"peerDependencies": {
"vite": "*"
},
"dependencies": {
"@vitejs/devtools": "workspace:*",
"@vitejs/devtools-kit": "workspace:*",
"axe-core": "catalog:devtools"
},
"devDependencies": {
"solid-js": "catalog:devtools",
"tsdown": "catalog:build",
"unocss": "catalog:build",
"vite": "catalog:build",
"vite-plugin-solid": "catalog:devtools"
}
}
12 changes: 12 additions & 0 deletions examples/plugin-a11y-checker/playground/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>A11y Checker Playground</title>
</head>
<body>
<div id="app"></div>
<script type="module" src="/src/main.tsx"></script>
</body>
</html>
73 changes: 73 additions & 0 deletions examples/plugin-a11y-checker/playground/src/App.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
// @unocss-include

/**
* This component has intentional accessibility issues
* for testing the A11y Checker plugin.
*/
export default function App() {
return (
<div class="min-h-screen bg-gradient-to-br from-slate-100 via-blue-50 to-cyan-100 text-slate-800 dark:from-slate-950 dark:via-slate-900 dark:to-slate-800 dark:text-slate-100">
<main class="mx-auto max-w-4xl px-6 py-10">
<section class="rounded-2xl border border-slate-200 bg-white/80 p-6 shadow-xl shadow-slate-300/20 backdrop-blur dark:border-slate-700 dark:bg-slate-900/70 dark:shadow-black/25">
<h1 class="m-0 text-3xl font-semibold tracking-tight">A11y Checker Playground</h1>
<p class="mt-3 leading-7 text-slate-700 dark:text-slate-300">
Open Vite DevTools and click the
{' '}
<strong>A11y Checker</strong>
{' '}
icon
(wheelchair) to run an accessibility audit on this page.
The results will appear in the
{' '}
<strong>Logs</strong>
{' '}
panel.
</p>
</section>

{/* Intentional a11y issues below */}

<section class="mt-6 rounded-2xl border border-slate-200 bg-white/80 p-6 shadow-xl dark:border-slate-700 dark:bg-slate-900/70">
<h2 class="text-xl font-semibold mb-4">Test Cases</h2>

{/* Issue: image without alt */}
<div class="mb-4">
<h3 class="text-sm font-medium mb-2 op50">Image without alt text</h3>
<img src="https://placehold.co/200x100" width="200" height="100" />
</div>

{/* Issue: button with no accessible name */}
<div class="mb-4">
<h3 class="text-sm font-medium mb-2 op50">Button without label</h3>
<button class="px-3 py-1 rounded bg-blue-500 text-white" />
</div>

{/* Issue: low contrast text */}
<div class="mb-4">
<h3 class="text-sm font-medium mb-2 op50">Low contrast text</h3>
<p style={{ 'color': '#ccc', 'background-color': '#fff' }}>
This text has very low contrast and is hard to read.
</p>
</div>

{/* Issue: form input without label */}
<div class="mb-4">
<h3 class="text-sm font-medium mb-2 op50">Input without label</h3>
<input type="text" placeholder="Enter something..." class="border rounded px-2 py-1" />
</div>

{/* Issue: clickable div without role */}
<div class="mb-4">
<h3 class="text-sm font-medium mb-2 op50">Clickable div without role</h3>
<div
onClick={() => {}}
class="cursor-pointer bg-purple-100 dark:bg-purple-900 rounded px-3 py-2 inline-block"
>
Click me (I'm a div, not a button)
</div>
</div>
</section>
</main>
</div>
)
}
11 changes: 11 additions & 0 deletions examples/plugin-a11y-checker/playground/src/main.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
/* @refresh reload */
import { render } from 'solid-js/web'
import App from './App'
import '@unocss/reset/tailwind.css'
import 'virtual:uno.css'

const root = document.getElementById('app')
if (!root)
throw new Error('Missing #app root')

render(() => <App />, root)
1 change: 1 addition & 0 deletions examples/plugin-a11y-checker/playground/src/vite-env.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
/// <reference types="vite/client" />
21 changes: 21 additions & 0 deletions examples/plugin-a11y-checker/playground/vite.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import { fileURLToPath } from 'node:url'
import { DevTools } from '@vitejs/devtools'
import UnoCSS from 'unocss/vite'
import { defineConfig } from 'vite'
import solid from 'vite-plugin-solid'
import { A11yCheckerPlugin } from '../src/node'

const unoConfig = fileURLToPath(new URL('../uno.config.ts', import.meta.url))

export default defineConfig({
plugins: [
DevTools({
builtinDevTools: false,
}),
solid(),
A11yCheckerPlugin(),
UnoCSS({
configFile: unoConfig,
}),
],
})
Loading
Loading