Skip to content

Commit 8c18a2f

Browse files
authored
feat(hub-ui): add @devframes/hub-ui — the reference UI filling the hub's ui slot (#170)
A port of Vite DevTools' web components onto @devframes/hub, prebuilt into two entries the hub serves through DevframeHubUi: - embedded.js — the floating DockEmbedded bootstrap (single self- contained ES module; always visible by design — visibility policy belongs to whoever authors an embedded entry) - a standalone viewer SPA (vanilla shell mounting DockStandalone, relative asset paths, served at the hub base) createUi() returns the slot object pointing at the built artifacts. The components (dock shell, panels, command palette, messages/toasts, views, json-render catalog) are Vue custom elements with shadow-root styles compiled ahead of time from the shared devframe design system (@antfu/design sage-green preset); imports are rewired from @vitejs/devtools-kit onto @devframes/hub's client runtime, the view-mode machinery is removed, and branding/element names/storage keys move to the devframes namespace. Storybook follows the repo's shared vue3-vite setup. Co-developed with Vite DevTools — the components descend from vitejs/devtools (MIT).
1 parent abe4a89 commit 8c18a2f

142 files changed

Lines changed: 12709 additions & 376 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

alias.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@ export const alias = {
4949
'@devframes/hub/node': r('hub/src/node/index.ts'),
5050
'@devframes/hub/types': r('hub/src/types/index.ts'),
5151
'@devframes/hub': r('hub/src/index.ts'),
52+
'@devframes/hub-ui': r('hub-ui/src/index.ts'),
5253
'@devframes/nuxt/runtime/plugin.client': r('nuxt/src/runtime/plugin.client.ts'),
5354
'@devframes/nuxt': r('nuxt/src/index.ts'),
5455
'@devframes/next/client': r('next/src/client.tsx'),

packages/hub-ui/.storybook/main.ts

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
import type { StorybookConfig } from '@storybook/vue3-vite'
2+
import vue from '@vitejs/plugin-vue'
3+
import UnoCSS from 'unocss/vite'
4+
import { mergeConfig } from 'vite'
5+
import { alias } from '../../../alias'
6+
7+
const config: StorybookConfig = {
8+
stories: ['../src/**/*.stories.@(js|jsx|mjs|ts|tsx)'],
9+
addons: ['@storybook/addon-docs'],
10+
framework: {
11+
name: '@storybook/vue3-vite',
12+
options: {},
13+
},
14+
docs: {},
15+
async viteFinal(config) {
16+
return mergeConfig(config, {
17+
resolve: { alias },
18+
plugins: [vue(), UnoCSS()],
19+
// Dev tool reached from arbitrary hostnames (LAN IPs, tunnels,
20+
// tailnets), e.g. when iframed by the storybook-hub example.
21+
server: { allowedHosts: true },
22+
})
23+
},
24+
}
25+
export default config
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
import type { Decorator, Preview } from '@storybook/vue3-vite'
2+
import 'virtual:uno.css'
3+
import '@antfu/design/styles.css'
4+
import '../src/client/style.css'
5+
6+
// Drive the shared `@antfu/design` tokens off the toolbar theme toggle: dark mode
7+
// is the `.dark` class on `<html>`, and the canvas takes the semantic
8+
// `bg-base`/`color-base` surface — matching every other devframe surface.
9+
function applyTheme(theme: string): void {
10+
document.documentElement.classList.toggle('dark', theme !== 'light')
11+
document.body.classList.add('bg-base', 'color-base', 'font-sans')
12+
}
13+
14+
const withTheme: Decorator = (story, context) => {
15+
applyTheme(context.globals.theme ?? 'dark')
16+
return { components: { story }, template: '<story />' }
17+
}
18+
19+
const preview: Preview = {
20+
parameters: {
21+
layout: 'fullscreen',
22+
controls: {
23+
expanded: true,
24+
matchers: {
25+
color: /(background|color)$/i,
26+
date: /Date$/i,
27+
},
28+
},
29+
},
30+
globalTypes: {
31+
theme: {
32+
description: 'Color theme',
33+
defaultValue: 'dark',
34+
toolbar: {
35+
title: 'Theme',
36+
icon: 'contrast',
37+
items: [
38+
{ value: 'light', title: 'Light', icon: 'sun' },
39+
{ value: 'dark', title: 'Dark', icon: 'moon' },
40+
],
41+
dynamicTitle: true,
42+
},
43+
},
44+
},
45+
decorators: [withTheme],
46+
}
47+
48+
export default preview

packages/hub-ui/package.json

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
{
2+
"name": "@devframes/hub-ui",
3+
"type": "module",
4+
"version": "0.8.1",
5+
"description": "Reference UI for @devframes/hub \u2014 the floating dock and standalone viewer filling the hub's ui slot.",
6+
"author": "Anthony Fu <anthonyfu117@hotmail.com>",
7+
"license": "MIT",
8+
"homepage": "https://github.com/devframes/devframe#readme",
9+
"repository": {
10+
"directory": "packages/hub-ui",
11+
"type": "git",
12+
"url": "git+https://github.com/devframes/devframe.git"
13+
},
14+
"bugs": "https://github.com/devframes/devframe/issues",
15+
"keywords": [
16+
"devtools",
17+
"devframe",
18+
"hub",
19+
"dock",
20+
"ui"
21+
],
22+
"sideEffects": false,
23+
"exports": {
24+
".": "./dist/index.mjs",
25+
"./package.json": "./package.json"
26+
},
27+
"types": "./dist/index.d.mts",
28+
"files": [
29+
"dist"
30+
],
31+
"scripts": {
32+
"build": "pnpm run build:css && pnpm run build:node && pnpm run build:embedded && pnpm run build:standalone",
33+
"build:css": "tsx scripts/build-css.ts",
34+
"build:node": "tsdown",
35+
"build:embedded": "vite build --config vite.embedded.config.ts",
36+
"build:standalone": "vite build --config src/client/standalone/vite.config.ts",
37+
"typecheck": "tsc --noEmit",
38+
"prepack": "pnpm build",
39+
"storybook": "storybook dev -p 6014",
40+
"build-storybook": "storybook build"
41+
},
42+
"peerDependencies": {
43+
"@devframes/hub": "workspace:*",
44+
"devframe": "workspace:*"
45+
},
46+
"devDependencies": {
47+
"@devframes/hub": "workspace:*",
48+
"@iconify-json/ph": "catalog:frontend",
49+
"@json-render/core": "catalog:deps",
50+
"@json-render/vue": "catalog:frontend",
51+
"@storybook/addon-docs": "catalog:storybook",
52+
"@storybook/vue3-vite": "catalog:storybook",
53+
"@types/node": "catalog:types",
54+
"@unocss/reset": "catalog:frontend",
55+
"@vitejs/plugin-vue": "catalog:build",
56+
"@vueuse/core": "catalog:frontend",
57+
"devframe": "workspace:*",
58+
"dompurify": "catalog:frontend",
59+
"fuse.js": "catalog:frontend",
60+
"iframe-pane": "catalog:frontend",
61+
"magic-string": "catalog:build",
62+
"storybook": "catalog:storybook",
63+
"tinyglobby": "catalog:deps",
64+
"tsdown": "catalog:build",
65+
"tsx": "catalog:build",
66+
"unocss": "catalog:frontend",
67+
"vite": "catalog:build",
68+
"vue": "catalog:frontend"
69+
}
70+
}
Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
import fs from 'node:fs/promises'
2+
import { createRequire } from 'node:module'
3+
import { join } from 'node:path'
4+
import { fileURLToPath } from 'node:url'
5+
import { colors as c } from 'devframe/utils/colors'
6+
import MagicString from 'magic-string'
7+
import { glob } from 'tinyglobby'
8+
import { createGenerator } from 'unocss'
9+
import config from '../uno.config'
10+
11+
// Compile the components' UnoCSS output ahead of time into a plain string
12+
// module (`src/client/.generated/css.ts`) that `defineCustomElement` adopts
13+
// into each shadow root — the dock stays fully styled inside any host page
14+
// without a global stylesheet, and the host page's own styles can't leak in.
15+
const SRC_DIR = fileURLToPath(new URL('../src/client', import.meta.url))
16+
const GLOBS = ['components/**/*.{ts,vue}', 'state/**/*.ts', 'embedded/**/*.ts', 'standalone/**/*.{ts,html}']
17+
// Story-only utility classes must not leak into the shipped stylesheet.
18+
const IGNORE = ['**/*.stories.*', '**/__tests__/**']
19+
const USER_STYLE = join(SRC_DIR, 'style.css')
20+
const GENERATED_CSS = join(SRC_DIR, '.generated/css.ts')
21+
22+
export async function buildCSS(): Promise<void> {
23+
const require = createRequire(import.meta.url)
24+
const reset = await fs.readFile(require.resolve('@unocss/reset/tailwind.css'), 'utf-8')
25+
const files = await glob(GLOBS, {
26+
cwd: SRC_DIR,
27+
absolute: true,
28+
ignore: IGNORE,
29+
})
30+
31+
const generator = await createGenerator(config)
32+
33+
const tokens = new Set<string>()
34+
for (const file of files) {
35+
const content = await fs.readFile(file, 'utf-8')
36+
await generator.applyExtractors(content, file, tokens)
37+
}
38+
39+
// The hand-written stylesheet may use `--at-apply` — run it through the
40+
// configured transformers (directives, variant groups) before merging.
41+
const userStyle = new MagicString(await fs.readFile(USER_STYLE, 'utf-8').catch(() => ''))
42+
for (const transformer of generator.config.transformers ?? []) {
43+
await transformer.transform(userStyle, USER_STYLE, { uno: generator } as any)
44+
}
45+
46+
const unoResult = await generator.generate(tokens)
47+
const css = [
48+
reset,
49+
userStyle.toString(),
50+
unoResult.css,
51+
].join('\n')
52+
53+
await fs.mkdir(join(SRC_DIR, '.generated'), { recursive: true })
54+
await fs.writeFile(GENERATED_CSS, [
55+
`/* eslint-disable eslint-comments/no-unlimited-disable */`,
56+
`/* eslint-disable */`,
57+
`export default ${JSON.stringify(String(css))}`,
58+
'',
59+
].join('\n'))
60+
console.log(`${c.green('✓')} CSS built (${files.length} sources, ${(css.length / 1024).toFixed(1)} kB)`)
61+
}
62+
63+
await buildCSS()

packages/hub-ui/src/client/.generated/css.ts

Lines changed: 3 additions & 0 deletions
Large diffs are not rendered by default.
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
import type { DocksContext } from '@devframes/hub/client'
2+
import type { VueElementConstructor } from 'vue'
3+
import { defineCustomElement } from 'vue'
4+
import css from '../.generated/css'
5+
import Component from './dock/DockEmbedded.vue'
6+
7+
export const DockEmbedded = defineCustomElement(
8+
Component,
9+
{
10+
shadowRoot: true,
11+
styles: [css],
12+
},
13+
) as VueElementConstructor<{
14+
context: DocksContext
15+
}>
16+
17+
customElements.define('devframes-dock-embedded', DockEmbedded)
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
import type { DocksContext } from '@devframes/hub/client'
2+
import type { VueElementConstructor } from 'vue'
3+
import { defineCustomElement } from 'vue'
4+
import css from '../.generated/css'
5+
import Component from './dock/DockStandalone.vue'
6+
7+
export const DockStandalone = defineCustomElement(
8+
Component,
9+
{
10+
shadowRoot: true,
11+
styles: [css],
12+
},
13+
) as VueElementConstructor<{
14+
context: DocksContext
15+
}>
16+
17+
if (!customElements.get('devframes-dock-standalone'))
18+
customElements.define('devframes-dock-standalone', DockStandalone)
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
import type { Meta, StoryObj } from '@storybook/vue3-vite'
2+
import { defineComponent, h, onMounted } from 'vue'
3+
import { groupedEntries } from '../../stories/fixtures'
4+
import { mountWithContext } from '../../stories/story-helpers'
5+
import CommandPalette from './CommandPalette.vue'
6+
7+
const meta = {
8+
title: 'Commands/Palette',
9+
component: CommandPalette,
10+
tags: ['autodocs'],
11+
parameters: {
12+
layout: 'fullscreen',
13+
// Fixed, full-screen overlay — render in an iframe for the docs canvas.
14+
docs: {
15+
story: { inline: false, height: '480px' },
16+
description: {
17+
component: 'The ⌘K command palette. Lists client + server commands (and dock navigation), supports fuzzy search and drill-down into grouped commands.',
18+
},
19+
},
20+
},
21+
} satisfies Meta
22+
23+
export default meta
24+
type Story = StoryObj
25+
26+
/**
27+
* The palette opened over the built-in and dock-navigation commands.
28+
*
29+
* `paletteOpen` is flipped in `onMounted` (not before mount): the palette's
30+
* open transition keys off the `show` change, so a value that's already `true`
31+
* at mount would leave it stuck at `opacity-0`.
32+
*/
33+
export const Open: Story = {
34+
render: () => ({
35+
setup: () => mountWithContext(
36+
{ entries: groupedEntries },
37+
ctx => h(defineComponent({
38+
setup() {
39+
onMounted(() => {
40+
ctx.commands.paletteOpen = true
41+
})
42+
return () => h(CommandPalette, { context: ctx })
43+
},
44+
})),
45+
),
46+
}),
47+
}

0 commit comments

Comments
 (0)