Skip to content
Open
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
22 changes: 22 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -101,3 +101,25 @@ jobs:

- name: E2E
run: nr test:e2e

storybook:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd

- name: Install pnpm
uses: pnpm/action-setup@0e279bb959325dab635dd2c09392533439d90093

- name: Set node
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e
with:
node-version: lts/*

- name: Setup
run: npm i -g @antfu/ni

- name: Install
run: nci

- name: Build Storybook
run: pnpm -C packages/core run build:storybook
39 changes: 39 additions & 0 deletions packages/core/.storybook/main.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import type { StorybookConfig } from '@storybook/vue3-vite'
import { fileURLToPath } from 'node:url'
import Vue from '@vitejs/plugin-vue'
import UnoCSS from 'unocss/vite'
import { mergeConfig } from 'vite'
import { alias } from '../../../alias'

const config: StorybookConfig = {
stories: [
'../src/client/**/*.stories.@(ts|tsx)',
],
addons: [],
framework: {
name: '@storybook/vue3-vite',
options: {},
},
core: {
disableTelemetry: true,
},
async viteFinal(base) {
return mergeConfig(base, {
define: {
// The webcomponents entry branches on this flag to import the local
// source instead of the published dist. Stories always run against
// source, so pin it on.
'import.meta.env.VITE_DEVTOOLS_LOCAL_DEV': JSON.stringify('true'),
},
resolve: {
alias,
},
plugins: [
Vue(),
UnoCSS(fileURLToPath(new URL('./uno.config.ts', import.meta.url))),
],
})
},
}

export default config
19 changes: 19 additions & 0 deletions packages/core/.storybook/preview.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
/* Storybook-only chrome. Production styling lives in the shadow-DOM CSS blob. */
html,
body,
#storybook-root {
height: 100%;
margin: 0;
padding: 0;
}

.vite-devtools-storybook-root {
min-height: 100vh;
font-family:
ui-sans-serif,
system-ui,
-apple-system,
'Segoe UI',
Roboto,
sans-serif;
}
57 changes: 57 additions & 0 deletions packages/core/.storybook/preview.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
import type { Preview } from '@storybook/vue3-vite'

import '@unocss/reset/tailwind.css'
import '@xterm/xterm/css/xterm.css'
import 'uno.css'
import './preview.css'

type Theme = 'dark' | 'light'

function applyTheme(theme: Theme) {
const root = document.documentElement
root.classList.toggle('dark', theme === 'dark')
root.style.colorScheme = theme
}

const preview: Preview = {
parameters: {
layout: 'fullscreen',
controls: {
matchers: {
color: /(background|color)$/i,
date: /Date$/i,
},
},
},
globalTypes: {
theme: {
description: 'DevTools color theme',
toolbar: {
title: 'Theme',
icon: 'circlehollow',
items: [
{ value: 'dark', title: 'Dark', icon: 'moon' },
{ value: 'light', title: 'Light', icon: 'sun' },
],
dynamicTitle: true,
},
},
},
initialGlobals: {
theme: 'dark',
},
decorators: [
(story, context) => {
applyTheme((context.globals.theme as Theme) ?? 'dark')
// Wrap via a template so Storybook composes the StoryFn into a real
// component. Rendering the raw `story` fn through `h()` coerces its
// return to a text node ("[object Object]").
return {
components: { story },
template: `<div class="vite-devtools-storybook-root color-base bg-base"><story /></div>`,
}
},
],
}

export default preview
54 changes: 54 additions & 0 deletions packages/core/.storybook/uno.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
import { fileURLToPath } from 'node:url'
import { sharedShortcuts } from '@vitejs/devtools-ui/unocss/shared-shortcuts'
import { theme } from '@vitejs/devtools-ui/unocss/theme'
import {
defineConfig,
presetIcons,
presetWind3,
transformerDirectives,
} from 'unocss'

// Mirrors the webcomponents UnoCSS config (`src/client/webcomponents/uno.config.ts`)
// with two Storybook-specific tweaks:
// 1. `dark: 'class'` so a toolbar toggle can flip the theme via a `.dark`
// class (production uses `media`, baked into the shadow-DOM CSS blob).
// 2. filesystem scanning of the component sources + stories, since the
// components render in the light DOM here and UnoCSS runs live.
export default defineConfig({
shortcuts: [
...sharedShortcuts,
{
'z-viewframe': 'z-20',
'z-viewframe-resizer': 'z-30',
'z-floating-dock': 'z-50',
'z-floating-anchor': 'z-[2147483644]',
'z-floating-tooltip': 'z-[2147483645]',
'z-command-palette': 'z-[2147483646]',
},
],
transformers: [
transformerDirectives(),
],
theme: {
colors: theme.colors,
},
presets: [
presetWind3({
dark: 'class',
variablePrefix: 'vdt-',
}),
presetIcons({
warn: true,
collections: {
logos: () => import('@iconify-json/logos').then(i => i.icons),
ph: () => import('@iconify-json/ph').then(i => i.icons),
},
}),
],
content: {
filesystem: [
fileURLToPath(new URL('../src/client/webcomponents/components/**/*.{vue,ts}', import.meta.url)),
fileURLToPath(new URL('../src/client/**/*.stories.ts', import.meta.url)),
],
},
})
8 changes: 7 additions & 1 deletion packages/core/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,9 @@
"play:debug": "pnpm -C playground run dev:debug",
"play:standalone": "DEBUG='vite:devtools:*' pnpm -C playground run dev:standalone",
"cli": "DEBUG='vite:devtools:*' tsx src/node/cli.ts",
"play:build": "pnpm -C playground run build"
"play:build": "pnpm -C playground run build",
"storybook": "storybook dev -p 6006 --host 0.0.0.0",
"build:storybook": "storybook build -o dist/storybook"
},
"peerDependencies": {
"vite": "*"
Expand All @@ -72,6 +74,8 @@
},
"devDependencies": {
"@clack/prompts": "catalog:inlined",
"@storybook/vue3-vite": "catalog:devtools",
"@unocss/reset": "catalog:build",
"@vitejs/devtools-rolldown": "workspace:*",
"@vitejs/plugin-vue": "catalog:build",
"@xterm/addon-fit": "catalog:frontend",
Expand All @@ -80,8 +84,10 @@
"fuse.js": "catalog:frontend",
"human-id": "catalog:inlined",
"iframe-pane": "catalog:frontend",
"storybook": "catalog:devtools",
"tsdown": "catalog:build",
"typescript": "catalog:devtools",
"unocss": "catalog:build",
"unplugin-vue": "catalog:build",
"unrun": "catalog:build",
"vite": "catalog:build",
Expand Down
3 changes: 2 additions & 1 deletion packages/core/src/client/inject/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,10 +38,11 @@ export async function init(): Promise<void> {
)
;(globalThis as any)[CLIENT_CONTEXT_KEY] = context

const { DockEmbedded } = import.meta.env.VITE_DEVTOOLS_LOCAL_DEV
const { registerDockEmbedded } = import.meta.env.VITE_DEVTOOLS_LOCAL_DEV
? await import('../webcomponents')
: await import('@vitejs/devtools/client/webcomponents')

const DockEmbedded = registerDockEmbedded()
const dockEl = new DockEmbedded({ context })
document.body.appendChild(dockEl)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,16 @@ import { defineCustomElement } from 'vue'
import css from '../.generated/css'
import Component from './dock/DockEmbedded.vue'

export const DOCK_EMBEDDED_TAG = 'vite-devtools-dock-embedded'

/**
* The embedded dock custom-element constructor.
*
* `defineCustomElement` is pure — it only builds the class. Registering the
* element with the global `customElements` registry is deferred to
* {@link registerDockEmbedded} so importing this module stays side-effect-free
* (honoring the package's `sideEffects: false`).
*/
export const DockEmbedded = defineCustomElement(
Component,
{
Expand All @@ -14,4 +24,16 @@ export const DockEmbedded = defineCustomElement(
context: DocksContext
}>

customElements.define('vite-devtools-dock-embedded', DockEmbedded)
/**
* Register the embedded dock as a custom element and return its constructor.
*
* Idempotent and safe to call in environments without a `customElements`
* registry (returns the constructor unchanged). Instantiating the constructor
* with `new` requires the element to be registered first, so call this before
* constructing one.
*/
export function registerDockEmbedded(): VueElementConstructor<{ context: DocksContext }> {
if (typeof customElements !== 'undefined' && !customElements.get(DOCK_EMBEDDED_TAG))
customElements.define(DOCK_EMBEDDED_TAG, DockEmbedded)
return DockEmbedded
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,16 @@ import { defineCustomElement } from 'vue'
import css from '../.generated/css'
import Component from './dock/DockStandalone.vue'

export const DOCK_STANDALONE_TAG = 'vite-devtools-dock-standalone'

/**
* The standalone dock custom-element constructor.
*
* `defineCustomElement` is pure — it only builds the class. Registering the
* element with the global `customElements` registry is deferred to
* {@link registerDockStandalone} so importing this module stays
* side-effect-free (honoring the package's `sideEffects: false`).
*/
export const DockStandalone = defineCustomElement(
Component,
{
Expand All @@ -14,5 +24,16 @@ export const DockStandalone = defineCustomElement(
context: DocksContext
}>

if (!customElements.get('vite-devtools-dock-standalone'))
customElements.define('vite-devtools-dock-standalone', DockStandalone)
/**
* Register the standalone dock as a custom element and return its constructor.
*
* Idempotent and safe to call in environments without a `customElements`
* registry (returns the constructor unchanged). Instantiating the constructor
* with `new` requires the element to be registered first, so call this before
* constructing one.
*/
export function registerDockStandalone(): VueElementConstructor<{ context: DocksContext }> {
if (typeof customElements !== 'undefined' && !customElements.get(DOCK_STANDALONE_TAG))
customElements.define(DOCK_STANDALONE_TAG, DockStandalone)
return DockStandalone
}
9 changes: 9 additions & 0 deletions packages/core/src/client/webcomponents/state/messages.ts
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,15 @@ export function useMessages(context: DocksContext): Reactive<MessagesState> {
return state
}

/**
* Reset the module-level messages singleton. Intended for tests and stories,
* which share one module graph — call before re-initializing `useMessages`
* with a fresh context so state doesn't leak between scenarios.
*/
export function resetMessagesState(): void {
_messagesState = undefined
}

export function markMessagesAsRead(): void {
if (_messagesState)
_messagesState.unreadCount = 0
Expand Down
4 changes: 2 additions & 2 deletions packages/core/src/client/webcomponents/state/popup.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ let detachColorModeSync: (() => void) | undefined
let popupDockElement: (HTMLElement & { remove: () => void }) | undefined
let popupContext: DocksContext | undefined
let loadDockStandalone: () => Promise<new (props: { context: DocksContext }) => HTMLElement> = async () => {
return await import('../components/DockStandalone').then(m => m.DockStandalone)
return await import('../components/DockStandalone').then(m => m.registerDockStandalone())
}

popupEvents.on('popup:open-requested', (context) => {
Expand Down Expand Up @@ -250,7 +250,7 @@ export function closeDockPopup() {

export function setDockStandaloneLoaderForTest(loader?: () => Promise<new (props: { context: DocksContext }) => HTMLElement>) {
loadDockStandalone = loader || (async () => {
return await import('../components/DockStandalone').then(m => m.DockStandalone)
return await import('../components/DockStandalone').then(m => m.registerDockStandalone())
})
}

Expand Down
10 changes: 10 additions & 0 deletions packages/core/src/client/webcomponents/state/terminals.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,16 @@ export interface TerminalState {
}

let _terminalsMap: Reactive<Map<string, TerminalState>> | undefined

/**
* Reset the module-level terminals singleton. Intended for tests and stories,
* which share one module graph — call before re-initializing `useTerminals`
* with a fresh context so state doesn't leak between scenarios.
*/
export function resetTerminalsState(): void {
_terminalsMap = undefined
}

export function useTerminals(context: DocksContext): Reactive<Map<string, TerminalState>> {
if (_terminalsMap) {
return _terminalsMap
Expand Down
Loading
Loading