diff --git a/design/panel-theme.ts b/design/panel-theme.ts new file mode 100644 index 000000000..79cd81a48 --- /dev/null +++ b/design/panel-theme.ts @@ -0,0 +1,35 @@ +import type { DevframeBranding } from '../packages/hub-ui/src/types' +import { setupDevframeConnection } from 'devframe/client' + +/** Apply startup branding to a built-in devframe's iframe SPA. */ +export function applyPanelBranding(): () => void { + if (window.parent === window) + return () => {} + + const root = document.documentElement + const previous = root.style.getPropertyValue('--devframe-primary') + const priority = root.style.getPropertyPriority('--devframe-primary') + let disposed = false + let applied = false + + void setupDevframeConnection().then(({ connectionMeta }) => { + const configs = connectionMeta.configs as { ui?: { branding?: DevframeBranding } } | undefined + const color = configs?.ui?.branding?.primaryColor + if (disposed || !color || !CSS.supports('color', color)) + return + root.style.setProperty('--devframe-primary', color) + applied = true + }).catch(() => { + // Connection failures leave the SPA's default palette intact. + }) + + return () => { + disposed = true + if (!applied) + return + if (previous) + root.style.setProperty('--devframe-primary', previous, priority) + else + root.style.removeProperty('--devframe-primary') + } +} diff --git a/design/uno.config.ts b/design/uno.config.ts index 297cad0a4..2c2f6e547 100644 --- a/design/uno.config.ts +++ b/design/uno.config.ts @@ -1,6 +1,6 @@ import type { Preset } from 'unocss' import { fileURLToPath } from 'node:url' -import { presetAnthonyDesign } from '@antfu/design/unocss' +import { presetAnthonyDesign, resolvePrimary } from '@antfu/design/unocss' import { defineConfig, presetIcons, @@ -60,7 +60,16 @@ export function createDesignConfig(options: CreateDesignConfigOptions = {}) { * shared border color (matching `border-base`) for unqualified borders. */ preflights: [{ getCSS: () => '*,::before,::after{border-color:#8882}' }], + theme: { + // Stable palette for status marks and preview content that intentionally + // keeps the default accent when the surrounding panel adopts a theme. + colors: { devframe: resolvePrimary('#3a6a45') }, + }, shortcuts: { + /** Fixed semantic colors stay independent of the panel's primary accent. */ + 'color-status-positive': 'color-devframe-600 dark:color-devframe-300', + 'color-preview-accent': 'color-devframe-600 dark:color-devframe-300', + 'bg-preview-accent': 'bg-devframe', /** Fixed navbar height, shared by every surface's top nav. */ 'h-nav': 'h-10', /** Named z-index layers, shared across every surface. */ diff --git a/docs/content/1.guide/18.hub-initiate.md b/docs/content/1.guide/18.hub-initiate.md index bfbd648cf..9fc32d826 100644 --- a/docs/content/1.guide/18.hub-initiate.md +++ b/docs/content/1.guide/18.hub-initiate.md @@ -53,7 +53,7 @@ interface DevframeHubUi { `@devframes/hub-ui`'s `createUi()` is the reference (standalone `viewer` SPA + floating dock); its `setup(ctx)` publishes config to `ctx.staticConfig.ui` (`ConnectionMeta.configs.ui`): - **`viewer`**: set to `false` to disable the standalone viewer. -- **`branding`**: rebrand the UI (logo, name, primary color). `background` accepts any CSS `background` value (color, gradient, image, or `transparent`) or `{ light, dark }` variants. These flat forms apply everywhere. Use `{ standalone, iframe? }` to specialize the framed viewer; an omitted `iframe` value falls back to `standalone`. +- **`branding`**: rebrand the UI (logo, name, primary color). `background` accepts any CSS `background` value (color, gradient, image, or `transparent`) or `{ light, dark }` variants. These flat forms apply everywhere. Use `{ standalone, iframe? }` to specialize the framed viewer; an omitted `iframe` value falls back to `standalone`. Built-in devframe panels read `branding.primaryColor` from the existing connection metadata at startup and map it to CSS primary tokens. Standalone SPAs keep their default accent; status and preview palettes remain independent. Reload panels after changing startup branding. - **`dockPreferences`** tunes the dock rail: `categoryOrder`, floating-dock `maxVisibleItems`, first-run `defaultMode` (`'float'`/`'edge'`) and `defaultPosition`. - **`embeddedVisibility`** sets the floating dock's reveal policy: - `'normal'` (default): shows immediately. diff --git a/plugins/a11y/app/main.tsx b/plugins/a11y/app/main.tsx index 4463c0610..125c2efc7 100644 --- a/plugins/a11y/app/main.tsx +++ b/plugins/a11y/app/main.tsx @@ -1,9 +1,14 @@ +/// + /* @refresh reload */ + import { render } from 'solid-js/web' +import { applyPanelBranding } from '../../../design/panel-theme' import { App } from './app.tsx' import 'virtual:uno.css' import '@antfu/design/styles.css' import './styles.css' +import '../../../packages/hub-ui/src/client/primary-ramp.css' // Shared design tokens flip on the `.dark` class; mirror the OS preference onto // (the other devframe plugins follow the same approach). @@ -20,3 +25,6 @@ if (!root) throw new Error('#app mount node missing from index.html') render(() => , root) + +const stopPanelTheme = applyPanelBranding() +import.meta.hot?.dispose(stopPanelTheme) diff --git a/plugins/assets/app/main.ts b/plugins/assets/app/main.ts index 63d704e3f..a09b21739 100644 --- a/plugins/assets/app/main.ts +++ b/plugins/assets/app/main.ts @@ -1,8 +1,10 @@ import { createApp } from 'vue' +import { applyPanelBranding } from '../../../design/panel-theme' import App from './app/App.vue' import 'virtual:uno.css' import 'floating-vue/dist/style.css' import '@antfu/design/styles.css' +import '../../../packages/hub-ui/src/client/primary-ramp.css' // Shared design tokens flip on the `.dark` class; mirror the OS preference // onto (the built-in devframe plugins all follow this approach). @@ -15,3 +17,6 @@ applyScheme(mq.matches) mq.addEventListener('change', e => applyScheme(e.matches)) createApp(App).mount('#app') + +const stopPanelTheme = applyPanelBranding() +import.meta.hot?.dispose(stopPanelTheme) diff --git a/plugins/code-server/app/main.ts b/plugins/code-server/app/main.ts index e6b23a3f3..aa0fdbda8 100644 --- a/plugins/code-server/app/main.ts +++ b/plugins/code-server/app/main.ts @@ -1,8 +1,12 @@ +/// + import { createApp } from 'vue' +import { applyPanelBranding } from '../../../design/panel-theme' import App from './App.vue' import 'virtual:uno.css' import '@antfu/design/styles.css' import './style.css' +import '../../../packages/hub-ui/src/client/primary-ramp.css' // The shared design tokens flip on the `.dark` class; mirror the OS preference // onto (the other devframe plugins follow the same approach). @@ -15,3 +19,6 @@ applyScheme(mq.matches) mq.addEventListener('change', e => applyScheme(e.matches)) createApp(App).mount('#app') + +const stopPanelTheme = applyPanelBranding() +import.meta.hot?.dispose(stopPanelTheme) diff --git a/plugins/data-inspector/app/components/QueryEditor.vue b/plugins/data-inspector/app/components/QueryEditor.vue index e084aa8c5..e7d0c87a0 100644 --- a/plugins/data-inspector/app/components/QueryEditor.vue +++ b/plugins/data-inspector/app/components/QueryEditor.vue @@ -366,7 +366,7 @@ ul.discovery-view-editor-hints-popup { --at-apply: 'bg-#8882 color-active'; } .discovery-view-editor-hint .match { - --at-apply: 'color-primary-700 dark:color-primary-300'; + --at-apply: 'color-active'; font-weight: 600; } .discovery-view-editor-hint::before { diff --git a/plugins/data-inspector/app/main.ts b/plugins/data-inspector/app/main.ts index 6b3cb9934..009ed3fb4 100644 --- a/plugins/data-inspector/app/main.ts +++ b/plugins/data-inspector/app/main.ts @@ -1,4 +1,7 @@ +/// + import { createApp } from 'vue' +import { applyPanelBranding } from '../../../design/panel-theme' import App from './App.vue' import 'virtual:uno.css' // floating-vue's base popper/transition structure, then @antfu/design's themed @@ -8,5 +11,9 @@ import 'floating-vue/dist/style.css' import '@antfu/design/styles/floating-vue.css' import '@antfu/design/styles.css' import './style.css' +import '../../../packages/hub-ui/src/client/primary-ramp.css' createApp(App).mount('#app') + +const stopPanelTheme = applyPanelBranding() +import.meta.hot?.dispose(stopPanelTheme) diff --git a/plugins/git/app/app/layout.tsx b/plugins/git/app/app/layout.tsx index 39b7b2e1e..9e5d763bd 100644 --- a/plugins/git/app/app/layout.tsx +++ b/plugins/git/app/app/layout.tsx @@ -2,6 +2,7 @@ import type { Metadata } from 'next' import type { ReactNode } from 'react' import './globals.css' import '@antfu/design/styles.css' +import '../../../../packages/hub-ui/src/client/primary-ramp.css' export const metadata: Metadata = { title: 'Git Dashboard', diff --git a/plugins/git/app/components/theme.ts b/plugins/git/app/components/theme.ts index 39f6fa854..9e822cb47 100644 --- a/plugins/git/app/components/theme.ts +++ b/plugins/git/app/components/theme.ts @@ -1,6 +1,7 @@ 'use client' import { useEffect, useState } from 'react' +import { applyPanelBranding } from '../../../../design/panel-theme' export type Theme = 'light' | 'dark' @@ -28,6 +29,8 @@ function systemTheme(): Theme { export function useTheme() { const [theme, setTheme] = useState('dark') + useEffect(() => applyPanelBranding(), []) + useEffect(() => { setTheme(readStored() ?? systemTheme()) }, []) diff --git a/plugins/git/app/components/ui/status-mark.tsx b/plugins/git/app/components/ui/status-mark.tsx index 0bdee2ac0..08db8e726 100644 --- a/plugins/git/app/components/ui/status-mark.tsx +++ b/plugins/git/app/components/ui/status-mark.tsx @@ -20,8 +20,8 @@ const STATUS_COLOR: Record = { 'added': 'text-success', 'deleted': 'text-error', 'modified': 'text-warning', - 'renamed': 'color-active', - 'copied': 'color-active', + 'renamed': 'color-status-positive', + 'copied': 'color-status-positive', 'type-changed': 'text-warning', 'unmerged': 'text-error', 'unknown': 'color-muted', diff --git a/plugins/inspect/app/main.ts b/plugins/inspect/app/main.ts index 1894611c0..8bd0b7167 100644 --- a/plugins/inspect/app/main.ts +++ b/plugins/inspect/app/main.ts @@ -1,9 +1,13 @@ +/// + import { createApp } from 'vue' +import { applyPanelBranding } from '../../../design/panel-theme' import App from './App.vue' import 'virtual:uno.css' import 'floating-vue/dist/style.css' import '@antfu/design/styles.css' import './style.css' +import '../../../packages/hub-ui/src/client/primary-ramp.css' // The shared design tokens flip on the `.dark` class; mirror the OS preference // onto (the other devframe plugins follow the same approach). @@ -16,3 +20,6 @@ applyScheme(mq.matches) mq.addEventListener('change', e => applyScheme(e.matches)) createApp(App).mount('#app') + +const stopPanelTheme = applyPanelBranding() +import.meta.hot?.dispose(stopPanelTheme) diff --git a/plugins/messages/app/main.ts b/plugins/messages/app/main.ts index 1a314e3d0..6bfca9ec8 100644 --- a/plugins/messages/app/main.ts +++ b/plugins/messages/app/main.ts @@ -1,4 +1,8 @@ +/// + +import { applyPanelBranding } from '../../../design/panel-theme' import { mountMessages } from './client/index' +import '../../../packages/hub-ui/src/client/primary-ramp.css' // The shared design tokens flip on the `.dark` class; mirror the OS preference // onto (the other devframe plugins follow the same approach). The @@ -19,3 +23,6 @@ if (!app) mountMessages(app).catch((error) => { app.textContent = `Failed to connect: ${error instanceof Error ? error.message : String(error)}` }) + +const stopPanelTheme = applyPanelBranding() +import.meta.hot?.dispose(stopPanelTheme) diff --git a/plugins/og/app/app/app.vue b/plugins/og/app/app/app.vue index 0f10f9965..3f2ca6dcf 100644 --- a/plugins/og/app/app/app.vue +++ b/plugins/og/app/app/app.vue @@ -49,7 +49,7 @@ onMounted(() => viewer.inspect())
- + {{ viewer.snapshot.value.url }} diff --git a/plugins/og/app/app/components/previews/TelegramPreview.vue b/plugins/og/app/app/components/previews/TelegramPreview.vue index 107b4ac84..ee6ad7999 100644 --- a/plugins/og/app/app/components/previews/TelegramPreview.vue +++ b/plugins/og/app/app/components/previews/TelegramPreview.vue @@ -11,12 +11,12 @@ const time = new Intl.DateTimeFormat(undefined, { hour: '2-digit', minute: '2-di