-
+
{{ 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 34577c447..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
- {{ card.url }}
+ {{ card.url }}
-
+
-
+
{{ card.hostname }}
From e813c3d1c6ef67424822bbd3ce9ea5ac79697ae1 Mon Sep 17 00:00:00 2001
From: Sakana <15715093608@163.com>
Date: Sun, 13 Sep 2026 13:12:17 +0800
Subject: [PATCH 3/5] docs: index the panel theme API reference
---
docs/content/8.references/index.md | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/docs/content/8.references/index.md b/docs/content/8.references/index.md
index bc01b8fa6..5252db962 100644
--- a/docs/content/8.references/index.md
+++ b/docs/content/8.references/index.md
@@ -11,7 +11,7 @@ Lookup pages the guides link into:
- [When Clauses](/references/when-clauses): the contexts and operators that gate docks, commands, and custom UI.
- [Events Reference](/references/events): every event, broadcast, shared-state key, and channel name, by direction and reach.
- [Node-Side API](/references/node-api): `DevframeDefinition` fields, CLI options, storage scopes, RPC function types, broadcast options, streaming lifecycle, remote assets, the `ctx.services` host and wire-service fields, diagnostics prefixes, and the auth surface.
-- [Browser-Side API](/references/browser-api): `connectDevframe` options, RPC client events, connection statuses, and in-page channel error codes.
+- [Browser-Side API](/references/browser-api): `connectDevframe` options, RPC client events, connection statuses, panel theme APIs, and in-page channel error codes.
- [Hub API](/references/hub-api): hub subsystems, launcher fields, duplication strategies, dock categories, the hub UI protocol, the namespace routes, the client runtime, the client context, and dock entry types.
- [Utilities](/references/utilities): the small, stable helpers under `devframe/utils/*`, bundled into `devframe`.
- [Interactive Auth](/references/interactive-auth): the OTP auth recipe: handshake, resolver gate, connect-time trust, banner.
From 67c7c94f3b83358f7b77c62093ffe1e18cebd407 Mon Sep 17 00:00:00 2001
From: Sakana <15715093608@163.com>
Date: Thu, 17 Sep 2026 14:19:10 +0800
Subject: [PATCH 4/5] fix(panels): inherit startup branding through shared CSS
Replace the proposed public theme APIs and message channel with existing
connection metadata and the shared hub UI primary ramp. Apply one CSS
input at startup and preserve standalone defaults.
Use the existing active-color token for query matches and track shared
CSS and adapter inputs in all nine panel build tasks. Replace protocol
tests with startup branding and cleanup coverage.
---
design/panel-theme.ts | 71 +++-----
docs/content/1.guide/11.client.md | 32 ----
docs/content/1.guide/18.hub-initiate.md | 2 +-
docs/content/8.references/3.events.md | 1 -
docs/content/8.references/5.browser-api.md | 14 --
docs/content/8.references/index.md | 2 +-
packages/devframe/src/client/index.ts | 1 -
packages/devframe/src/client/theme.test.ts | 161 ------------------
packages/devframe/src/client/theme.ts | 104 -----------
packages/devframe/src/events.ts | 1 -
.../client/components/views/ViewIframe.vue | 8 -
plugins/a11y/app/main.tsx | 5 +-
plugins/assets/app/main.ts | 5 +-
plugins/code-server/app/main.ts | 5 +-
.../app/components/QueryEditor.vue | 2 +-
plugins/data-inspector/app/main.ts | 5 +-
plugins/git/app/app/layout.tsx | 1 +
plugins/git/app/components/theme.ts | 4 +-
plugins/inspect/app/main.ts | 5 +-
plugins/messages/app/main.ts | 5 +-
plugins/og/app/main.ts | 5 +-
plugins/terminals/app/main.ts | 5 +-
.../tsnapi/devframe/client.snapshot.d.ts | 9 -
.../tsnapi/devframe/client.snapshot.js | 2 -
.../tsnapi/devframe/constants.snapshot.d.ts | 1 -
tests/panel-branding.test.ts | 74 ++++++++
turbo.json | 9 +
27 files changed, 141 insertions(+), 398 deletions(-)
delete mode 100644 packages/devframe/src/client/theme.test.ts
delete mode 100644 packages/devframe/src/client/theme.ts
create mode 100644 tests/panel-branding.test.ts
diff --git a/design/panel-theme.ts b/design/panel-theme.ts
index 9bf4100b0..79cd81a48 100644
--- a/design/panel-theme.ts
+++ b/design/panel-theme.ts
@@ -1,50 +1,35 @@
-import { watchDevframeTheme } from 'devframe/client'
+import type { DevframeBranding } from '../packages/hub-ui/src/types'
+import { setupDevframeConnection } from 'devframe/client'
-// Match the reference dock's primary ramp. Only accent tokens change; semantic
-// success/error/warning colors and each SPA's chart palettes stay independent.
-const primaryStops = {
- DEFAULT: 0,
- 50: 95,
- 100: 90,
- 200: 75,
- 600: 9,
- 500: 18,
- 400: 34,
- 300: 54,
- 700: -8,
- 800: -25,
- 900: -45,
- 950: -70,
-} as const
+/** Apply startup branding to a built-in devframe's iframe SPA. */
+export function applyPanelBranding(): () => void {
+ if (window.parent === window)
+ return () => {}
-/** Opt a built-in devframe SPA into the hub UI provider's primary accent. */
-export function syncPanelTheme(): () => void {
- const style = document.documentElement.style
- const previous = Object.keys(primaryStops).map((stop) => {
- const name = `--colors-primary-${stop}`
- return { name, value: style.getPropertyValue(name), priority: style.getPropertyPriority(name) }
- })
- function restore() {
- for (const { name, value, priority } of previous) {
- if (value)
- style.setProperty(name, value, priority)
- else
- style.removeProperty(name)
- }
- }
- const stop = watchDevframeTheme(({ primaryColor }) => {
- if (!primaryColor || !CSS.supports('color', primaryColor)) {
- restore()
+ 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
- }
- for (const [stop, white] of Object.entries(primaryStops)) {
- style.setProperty(`--colors-primary-${stop}`, white
- ? `color-mix(in oklab, ${primaryColor}, ${white > 0 ? 'white' : 'black'} ${Math.abs(white)}%)`
- : primaryColor)
- }
+ root.style.setProperty('--devframe-primary', color)
+ applied = true
+ }).catch(() => {
+ // Connection failures leave the SPA's default palette intact.
})
+
return () => {
- stop()
- restore()
+ disposed = true
+ if (!applied)
+ return
+ if (previous)
+ root.style.setProperty('--devframe-primary', previous, priority)
+ else
+ root.style.removeProperty('--devframe-primary')
}
}
diff --git a/docs/content/1.guide/11.client.md b/docs/content/1.guide/11.client.md
index 65a4eb8a4..c26e6a53e 100644
--- a/docs/content/1.guide/11.client.md
+++ b/docs/content/1.guide/11.client.md
@@ -333,35 +333,3 @@ async function reconnect() {
```
In a hub, a hub UI provider reads this status from [`context.connection`](/guide/client-context#the-client-context).
-
-## Panel accent
-
-A SPA can adopt its hub UI provider's primary accent with `watchDevframeTheme()`. Map the hint to your own UI token and retain separate tokens for brand graphics, status colors, and charts:
-
-```ts
-import { watchDevframeTheme } from 'devframe/client'
-
-const stopTheme = watchDevframeTheme(({ primaryColor }) => {
- const style = document.documentElement.style
- if (primaryColor && CSS.supports('color', primaryColor))
- style.setProperty('--panel-accent', primaryColor)
- else
- style.removeProperty('--panel-accent')
-})
-
-// Call stopTheme() when the SPA unmounts.
-```
-
-Use `var(--panel-accent, #3a6a45)` in the SPA's accent styles. A standalone SPA keeps its fallback. Subscription is independent of the RPC connection and works with static builds. The callback receives presentation hints from the direct embedding window; each SPA chooses the UI tokens it changes.
-
-`@devframes/hub-ui` offers its `branding.primaryColor` to panels automatically. A custom hub UI provider can use the same protocol:
-
-```ts
-import { provideDevframeTheme } from 'devframe/client'
-
-const theme = provideDevframeTheme(iframe, { primaryColor: '#6b84fd' })
-theme.update({ primaryColor: '#8250df' })
-// Call theme.dispose() when the iframe view unmounts.
-```
-
-The provider supports both startup orders, reloads, and remounting a preserved iframe. The [Panel theme reference](/references/browser-api#panel-theme) lists the API and origin contract.
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/docs/content/8.references/3.events.md b/docs/content/8.references/3.events.md
index d9c211672..96fb817f8 100644
--- a/docs/content/8.references/3.events.md
+++ b/docs/content/8.references/3.events.md
@@ -127,4 +127,3 @@ Pushed over each panel's [in-page channel](/guide/in-page-channel) port; the pai
|---|---|---|
| `devframe:remote-assets-error` | the remote-assets fallback page, to `window.parent` | The failed package/version/reason, so an embedding hub UI provider can replace the 502 page. |
| `devframe:in-page-channel` | both [in-page channel](/guide/in-page-channel) endpoints, across window boundaries | The versioned handshake envelope (panel hello, page-script port grant). |
-| `devframe:theme` | the embedding window and its panel iframe | Optional accent handshake: `available`, `request`, and `update` with a `DevframeTheme`. |
diff --git a/docs/content/8.references/5.browser-api.md b/docs/content/8.references/5.browser-api.md
index 2bc36e756..38dc018e9 100644
--- a/docs/content/8.references/5.browser-api.md
+++ b/docs/content/8.references/5.browser-api.md
@@ -46,20 +46,6 @@ The values of `rpc.status`: [Handling connection and auth errors](/guide/client#
| `disconnected` | Socket closed (dropped mid-session or never opened). |
| `error` | Fatal: the socket errored or connection meta couldn't load. |
-## Panel theme
-
-Optional presentation hints from the embedding window: [Panel accent](/guide/client#panel-accent). All exports come from `devframe/client`.
-
-| API | Contract |
-|-----|----------|
-| `DevframeTheme.primaryColor` | Optional CSS color. Omitted means use the SPA's default accent. |
-| `watchDevframeTheme(onChange)` | Opt the panel into updates from its direct parent window. Returns a listener cleanup function. Standalone SPAs and SSR return a no-op cleanup. |
-| `provideDevframeTheme(iframe, theme)` | Offer hints to one iframe; only an opted-in panel receives theme data. Returns a `DevframeThemeProvider`. |
-| `provider.update(theme)` | Replace the hints; `update({})` clears the accent override. |
-| `provider.dispose()` | Remove the provider's message and load listeners. |
-
-The protocol supports same-origin and cross-origin iframe documents with a non-opaque origin. Each side checks the sending window; updates target the requesting document's exact origin. A newly loaded iframe document opts in again. The subscription trusts its direct embedding window for presentation hints; the SPA controls how it applies them.
-
## In-page channel endpoints
The browser-only endpoint methods of the [in-page channel](/guide/in-page-channel). `emit()` sends to the opposite endpoint; `on()` handles events arriving from that endpoint.
diff --git a/docs/content/8.references/index.md b/docs/content/8.references/index.md
index 5252db962..bc01b8fa6 100644
--- a/docs/content/8.references/index.md
+++ b/docs/content/8.references/index.md
@@ -11,7 +11,7 @@ Lookup pages the guides link into:
- [When Clauses](/references/when-clauses): the contexts and operators that gate docks, commands, and custom UI.
- [Events Reference](/references/events): every event, broadcast, shared-state key, and channel name, by direction and reach.
- [Node-Side API](/references/node-api): `DevframeDefinition` fields, CLI options, storage scopes, RPC function types, broadcast options, streaming lifecycle, remote assets, the `ctx.services` host and wire-service fields, diagnostics prefixes, and the auth surface.
-- [Browser-Side API](/references/browser-api): `connectDevframe` options, RPC client events, connection statuses, panel theme APIs, and in-page channel error codes.
+- [Browser-Side API](/references/browser-api): `connectDevframe` options, RPC client events, connection statuses, and in-page channel error codes.
- [Hub API](/references/hub-api): hub subsystems, launcher fields, duplication strategies, dock categories, the hub UI protocol, the namespace routes, the client runtime, the client context, and dock entry types.
- [Utilities](/references/utilities): the small, stable helpers under `devframe/utils/*`, bundled into `devframe`.
- [Interactive Auth](/references/interactive-auth): the OTP auth recipe: handshake, resolver gate, connect-time trust, banner.
diff --git a/packages/devframe/src/client/index.ts b/packages/devframe/src/client/index.ts
index 99f5a68f6..1add93bc5 100644
--- a/packages/devframe/src/client/index.ts
+++ b/packages/devframe/src/client/index.ts
@@ -9,7 +9,6 @@ export * from './rpc-streaming'
export { resolveWsUrl, type WsUrlLocation } from './rpc-ws'
export * from './scope'
export * from './settings'
-export * from './theme'
export * from './webmcp'
export const connectDevframe = getDevframeRpcClient
diff --git a/packages/devframe/src/client/theme.test.ts b/packages/devframe/src/client/theme.test.ts
deleted file mode 100644
index 1058c4c39..000000000
--- a/packages/devframe/src/client/theme.test.ts
+++ /dev/null
@@ -1,161 +0,0 @@
-import { afterEach, describe, expect, it, vi } from 'vitest'
-import { DEVFRAME_EVENTS } from '../events'
-import { provideDevframeTheme, watchDevframeTheme } from './theme'
-
-const channel = DEVFRAME_EVENTS.postMessage.theme
-
-function fixture() {
- const parent = new EventTarget() as Window
- const panel = new EventTarget() as Window
- const origin = 'https://panel.example'
- const parentOrigin = 'https://hub.example'
- const queue: (() => void)[] = []
- const send = (target: EventTarget, data: unknown, source: EventTarget, origin: string) => {
- target.dispatchEvent(Object.assign(new Event('message'), { data, source, origin }))
- }
- Object.assign(parent, {
- postMessage: (data: unknown) => queue.push(() => send(parent, data, panel, origin)),
- })
- Object.assign(panel, {
- parent,
- postMessage: (data: unknown, targetOrigin: string) => queue.push(() => {
- if (targetOrigin === '*' || targetOrigin === origin)
- send(panel, data, parent, parentOrigin)
- }),
- })
- // Model both browsing contexts and queued postMessage delivery without a DOM.
- const iframe = Object.assign(new EventTarget(), {
- contentWindow: panel,
- ownerDocument: { defaultView: parent } as Document,
- }) as HTMLIFrameElement
- vi.stubGlobal('window', panel)
- return {
- iframe,
- panel,
- parent,
- send,
- flush() {
- while (queue.length)
- queue.shift()!()
- },
- }
-}
-
-afterEach(() => vi.unstubAllGlobals())
-
-describe('panel theme protocol', () => {
- it.each(['panel', 'provider'])('connects cross-origin when %s boots first', (first) => {
- const f = fixture()
- const changed = vi.fn()
- const startPanel = () => watchDevframeTheme(changed)
- const startProvider = () => provideDevframeTheme(f.iframe, { primaryColor: '#6b84fd' })
- if (first === 'panel') {
- startPanel()
- f.flush()
- startProvider()
- }
- else {
- startProvider()
- f.flush()
- startPanel()
- }
- f.flush()
- expect(changed).toHaveBeenLastCalledWith({ primaryColor: '#6b84fd' })
- })
-
- it('updates and clears the accent without changing other theme state', () => {
- const f = fixture()
- const provider = provideDevframeTheme(f.iframe, { primaryColor: '#6b84fd' })
- const changed = vi.fn()
- watchDevframeTheme(changed)
- f.flush()
- provider.update({ primaryColor: 'rebeccapurple' })
- f.flush()
- expect(changed).toHaveBeenLastCalledWith({ primaryColor: 'rebeccapurple' })
- provider.update({})
- f.flush()
- expect(changed).toHaveBeenLastCalledWith({ primaryColor: undefined })
- })
-
- it('offers no theme data until the iframe opts in', () => {
- const f = fixture()
- const received: unknown[] = []
- f.panel.addEventListener('message', event => received.push((event as MessageEvent).data))
- const provider = provideDevframeTheme(f.iframe, { primaryColor: 'blue' })
- provider.update({ primaryColor: 'red' })
- f.flush()
- expect(received).toEqual([{ channel, type: 'available' }])
- })
-
- it('requires a new document to opt in after navigation', () => {
- const f = fixture()
- const provider = provideDevframeTheme(f.iframe, { primaryColor: 'blue' })
- const changed = vi.fn()
- const stop = watchDevframeTheme(changed)
- f.flush()
- stop()
- f.iframe.dispatchEvent(new Event('load'))
- provider.update({ primaryColor: 'red' })
- f.flush()
- changed.mockClear()
- watchDevframeTheme(changed)
- f.flush()
- expect(changed).toHaveBeenLastCalledWith({ primaryColor: 'red' })
- })
-
- it('rejects theme updates from sibling windows and malformed messages', () => {
- const f = fixture()
- const changed = vi.fn()
- watchDevframeTheme(changed)
- f.send(f.panel, { channel, type: 'update', theme: { primaryColor: 'red' } }, new EventTarget(), 'https://hub.example')
- for (const data of [null, 'blue', { channel, type: 'update', theme: null }, { channel, type: 'update', theme: { primaryColor: 42 } }])
- f.send(f.panel, data, f.parent, 'https://hub.example')
- expect(changed).not.toHaveBeenCalled()
- })
-
- it('rejects requests from unrelated windows and opaque origins', () => {
- const f = fixture()
- const received: unknown[] = []
- f.panel.addEventListener('message', event => received.push((event as MessageEvent).data))
- provideDevframeTheme(f.iframe, { primaryColor: 'blue' })
- f.flush()
- received.length = 0
- f.send(f.parent, { channel, type: 'request' }, new EventTarget(), 'https://panel.example')
- f.send(f.parent, { channel, type: 'request' }, f.panel, 'null')
- f.flush()
- expect(received).toEqual([])
- })
-
- it('cleans up both ends and reconnects a preserved iframe to a new provider', () => {
- const f = fixture()
- const changed = vi.fn()
- const stop = watchDevframeTheme(changed)
- const provider = provideDevframeTheme(f.iframe, { primaryColor: 'blue' })
- f.flush()
- provider.dispose()
- changed.mockClear()
- provider.update({ primaryColor: 'red' })
- f.iframe.dispatchEvent(new Event('load'))
- f.flush()
- expect(changed).not.toHaveBeenCalled()
- const next = provideDevframeTheme(f.iframe, {})
- f.flush()
- expect(changed).toHaveBeenLastCalledWith({ primaryColor: undefined })
- stop()
- changed.mockClear()
- next.update({ primaryColor: 'red' })
- f.flush()
- expect(changed).not.toHaveBeenCalled()
- })
-
- it('leaves standalone SPAs and SSR untouched', () => {
- const changed = vi.fn()
- vi.stubGlobal('window', undefined)
- watchDevframeTheme(changed)()
- const standalone = { parent: undefined as unknown }
- standalone.parent = standalone
- vi.stubGlobal('window', standalone)
- watchDevframeTheme(changed)()
- expect(changed).not.toHaveBeenCalled()
- })
-})
diff --git a/packages/devframe/src/client/theme.ts b/packages/devframe/src/client/theme.ts
deleted file mode 100644
index b0f79443f..000000000
--- a/packages/devframe/src/client/theme.ts
+++ /dev/null
@@ -1,104 +0,0 @@
-import { DEVFRAME_EVENTS } from '../events'
-
-/** Optional presentation hints supplied by the hub UI provider to a panel. */
-export interface DevframeTheme {
- /** CSS color for the panel's primary UI accent. Omitted to use the SPA's default. */
- primaryColor?: string
-}
-
-export interface DevframeThemeProvider {
- /** Replace the presentation hints, including clearing a previous accent. */
- update: (theme: DevframeTheme) => void
- /** Remove the message and iframe-load listeners. */
- dispose: () => void
-}
-
-const channel = DEVFRAME_EVENTS.postMessage.theme
-
-function isThemeMessage(data: unknown): data is { channel: typeof channel, type: string, theme?: unknown } {
- return typeof data === 'object' && data !== null
- && 'channel' in data && data.channel === channel
- && 'type' in data && typeof data.type === 'string'
-}
-
-function isTheme(theme: unknown): theme is DevframeTheme {
- return typeof theme === 'object' && theme !== null
- && (!('primaryColor' in theme) || theme.primaryColor === undefined || typeof theme.primaryColor === 'string')
-}
-
-/**
- * Opt a panel into its embedding window's accent hints. Maps no styles itself:
- * the SPA chooses which UI tokens to update. Returns a listener cleanup.
- * Standalone SPAs and server-side rendering keep their own theme.
- */
-export function watchDevframeTheme(onChange: (theme: DevframeTheme) => void): () => void {
- if (typeof window === 'undefined' || window.parent === window)
- return () => {}
-
- const parent = window.parent
- function request() {
- // The request contains no application data. The embedding window may be
- // cross-origin; only that exact window can answer with presentation hints.
- parent.postMessage({ channel, type: 'request' }, '*')
- }
- function onMessage(event: MessageEvent) {
- if (event.source !== parent || !isThemeMessage(event.data))
- return
- if (event.data.type === 'available')
- request()
- else if (event.data.type === 'update' && isTheme(event.data.theme))
- onChange({ primaryColor: event.data.theme.primaryColor })
- }
-
- window.addEventListener('message', onMessage)
- request()
- return () => window.removeEventListener('message', onMessage)
-}
-
-/**
- * Offer accent hints to one iframe. Only a panel calling watchDevframeTheme()
- * receives updates; the hub UI provider never writes into the iframe's DOM.
- * The ready handshake supports either boot order, reloads and cross-origin SPAs.
- */
-export function provideDevframeTheme(iframe: HTMLIFrameElement, initial: DevframeTheme): DevframeThemeProvider {
- const ownerWindow = iframe.ownerDocument.defaultView!
- let theme = { primaryColor: initial.primaryColor }
- let subscribedOrigin: string | undefined
-
- function announce() {
- iframe.contentWindow?.postMessage({ channel, type: 'available' }, '*')
- }
- function publish() {
- if (subscribedOrigin !== undefined)
- iframe.contentWindow?.postMessage({ channel, type: 'update', theme }, subscribedOrigin)
- }
- function onMessage(event: MessageEvent) {
- if (!iframe.contentWindow || event.source !== iframe.contentWindow || !isThemeMessage(event.data))
- return
- if (event.data.type !== 'request' || event.origin === 'null')
- return
- subscribedOrigin = event.origin
- publish()
- }
- function onLoad() {
- // A new document must opt in again, including after an address-bar navigation.
- subscribedOrigin = undefined
- announce()
- }
-
- ownerWindow.addEventListener('message', onMessage)
- iframe.addEventListener('load', onLoad)
- announce()
-
- return {
- update(next) {
- theme = { primaryColor: next.primaryColor }
- publish()
- },
- dispose() {
- ownerWindow.removeEventListener('message', onMessage)
- iframe.removeEventListener('load', onLoad)
- subscribedOrigin = undefined
- },
- }
-}
diff --git a/packages/devframe/src/events.ts b/packages/devframe/src/events.ts
index 531d2ab2e..ebc958d67 100644
--- a/packages/devframe/src/events.ts
+++ b/packages/devframe/src/events.ts
@@ -70,7 +70,6 @@ export const DEVFRAME_EVENTS = {
},
/** `postMessage` channels the runtime posts across window boundaries. */
postMessage: {
- theme: 'devframe:theme',
remoteAssetsError: 'devframe:remote-assets-error',
inPageChannel: 'devframe:in-page-channel',
},
diff --git a/packages/hub-ui/src/client/components/views/ViewIframe.vue b/packages/hub-ui/src/client/components/views/ViewIframe.vue
index 592b925b3..cd63ddaed 100644
--- a/packages/hub-ui/src/client/components/views/ViewIframe.vue
+++ b/packages/hub-ui/src/client/components/views/ViewIframe.vue
@@ -6,9 +6,7 @@ import type { IframePanes } from 'iframe-pane'
import type { CSSProperties } from 'vue'
import { stripRemoteConnectionFromUrl, watchFrameLocation } from '@devframes/hub/client'
import { DEVFRAME_REMOTE_ASSETS_ERROR_MESSAGE_TYPE } from '@devframes/hub/constants'
-import { provideDevframeTheme } from 'devframe/client'
import { computed, nextTick, onMounted, onUnmounted, ref, useTemplateRef, watchEffect } from 'vue'
-import { useBranding } from '../../state/branding'
import { useSettings } from '../../state/settings-defaults'
import ViewAssetsError from './ViewAssetsError.vue'
import ViewIframeLoading from './ViewIframeLoading.vue'
@@ -21,7 +19,6 @@ const props = defineProps<{
}>()
const settings = useSettings(props.context)
-const branding = useBranding()
const isEdgeMode = computed(() => props.context.panel.store.mode === 'edge')
const addressBarControls = computed(() => typeof props.entry.addressBar === 'object' ? props.entry.addressBar : undefined)
const showAddressBar = computed(() => settings.value.showIframeAddressBar || Boolean(props.entry.addressBar))
@@ -214,7 +211,6 @@ function openExternally() {
let mountedTarget: HTMLDivElement | null = null
let onIframeLoad: (() => void) | undefined
let stopLocationWatch: (() => void) | undefined
-let disposeTheme: (() => void) | undefined
onMounted(() => {
const existed = props.panes.has(paneKey.value)
@@ -236,9 +232,6 @@ onMounted(() => {
style: { boxShadow: 'none', outline: 'none' },
})
const iframe = pane.iframe
- const themeProvider = provideDevframeTheme(iframe, { primaryColor: branding.value.primaryColor })
- disposeTheme = themeProvider.dispose
- watchEffect(() => themeProvider.update({ primaryColor: branding.value.primaryColor }))
// Follow the frame wherever it goes: a document load, but also an SPA
// router's `pushState`/`replaceState` and back/forward, none of which fire
@@ -322,7 +315,6 @@ onMounted(() => {
})
onUnmounted(() => {
- disposeTheme?.()
window.removeEventListener('message', onWindowMessage)
const pane = props.panes.get(paneKey.value)
if (pane && onIframeLoad)
diff --git a/plugins/a11y/app/main.tsx b/plugins/a11y/app/main.tsx
index 7c5738c00..125c2efc7 100644
--- a/plugins/a11y/app/main.tsx
+++ b/plugins/a11y/app/main.tsx
@@ -3,11 +3,12 @@
/* @refresh reload */
import { render } from 'solid-js/web'
-import { syncPanelTheme } from '../../../design/panel-theme'
+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).
@@ -25,5 +26,5 @@ if (!root)
render(() => , root)
-const stopPanelTheme = syncPanelTheme()
+const stopPanelTheme = applyPanelBranding()
import.meta.hot?.dispose(stopPanelTheme)
diff --git a/plugins/assets/app/main.ts b/plugins/assets/app/main.ts
index bc9e3ddde..a09b21739 100644
--- a/plugins/assets/app/main.ts
+++ b/plugins/assets/app/main.ts
@@ -1,9 +1,10 @@
import { createApp } from 'vue'
-import { syncPanelTheme } from '../../../design/panel-theme'
+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).
@@ -17,5 +18,5 @@ mq.addEventListener('change', e => applyScheme(e.matches))
createApp(App).mount('#app')
-const stopPanelTheme = syncPanelTheme()
+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 7d7cceb91..aa0fdbda8 100644
--- a/plugins/code-server/app/main.ts
+++ b/plugins/code-server/app/main.ts
@@ -1,11 +1,12 @@
///
import { createApp } from 'vue'
-import { syncPanelTheme } from '../../../design/panel-theme'
+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).
@@ -19,5 +20,5 @@ mq.addEventListener('change', e => applyScheme(e.matches))
createApp(App).mount('#app')
-const stopPanelTheme = syncPanelTheme()
+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 3bc5fb32a..009ed3fb4 100644
--- a/plugins/data-inspector/app/main.ts
+++ b/plugins/data-inspector/app/main.ts
@@ -1,7 +1,7 @@
///
import { createApp } from 'vue'
-import { syncPanelTheme } from '../../../design/panel-theme'
+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
@@ -11,8 +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 = syncPanelTheme()
+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 daeeaf2b1..9e822cb47 100644
--- a/plugins/git/app/components/theme.ts
+++ b/plugins/git/app/components/theme.ts
@@ -1,7 +1,7 @@
'use client'
import { useEffect, useState } from 'react'
-import { syncPanelTheme } from '../../../../design/panel-theme'
+import { applyPanelBranding } from '../../../../design/panel-theme'
export type Theme = 'light' | 'dark'
@@ -29,7 +29,7 @@ function systemTheme(): Theme {
export function useTheme() {
const [theme, setTheme] = useState('dark')
- useEffect(() => syncPanelTheme(), [])
+ useEffect(() => applyPanelBranding(), [])
useEffect(() => {
setTheme(readStored() ?? systemTheme())
diff --git a/plugins/inspect/app/main.ts b/plugins/inspect/app/main.ts
index 78608d82e..8bd0b7167 100644
--- a/plugins/inspect/app/main.ts
+++ b/plugins/inspect/app/main.ts
@@ -1,12 +1,13 @@
///
import { createApp } from 'vue'
-import { syncPanelTheme } from '../../../design/panel-theme'
+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).
@@ -20,5 +21,5 @@ mq.addEventListener('change', e => applyScheme(e.matches))
createApp(App).mount('#app')
-const stopPanelTheme = syncPanelTheme()
+const stopPanelTheme = applyPanelBranding()
import.meta.hot?.dispose(stopPanelTheme)
diff --git a/plugins/messages/app/main.ts b/plugins/messages/app/main.ts
index 8c0218c97..6bfca9ec8 100644
--- a/plugins/messages/app/main.ts
+++ b/plugins/messages/app/main.ts
@@ -1,7 +1,8 @@
///
-import { syncPanelTheme } from '../../../design/panel-theme'
+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
@@ -23,5 +24,5 @@ mountMessages(app).catch((error) => {
app.textContent = `Failed to connect: ${error instanceof Error ? error.message : String(error)}`
})
-const stopPanelTheme = syncPanelTheme()
+const stopPanelTheme = applyPanelBranding()
import.meta.hot?.dispose(stopPanelTheme)
diff --git a/plugins/og/app/main.ts b/plugins/og/app/main.ts
index bb7a8a788..9306b3092 100644
--- a/plugins/og/app/main.ts
+++ b/plugins/og/app/main.ts
@@ -1,6 +1,7 @@
import { createApp } from 'vue'
-import { syncPanelTheme } from '../../../design/panel-theme'
+import { applyPanelBranding } from '../../../design/panel-theme'
import App from './app/app.vue'
+import '../../../packages/hub-ui/src/client/primary-ramp.css'
const media = window.matchMedia('(prefers-color-scheme: dark)')
function applyScheme(dark: boolean): void {
@@ -12,5 +13,5 @@ media.addEventListener('change', event => applyScheme(event.matches))
createApp(App).mount('#app')
-const stopPanelTheme = syncPanelTheme()
+const stopPanelTheme = applyPanelBranding()
import.meta.hot?.dispose(stopPanelTheme)
diff --git a/plugins/terminals/app/main.ts b/plugins/terminals/app/main.ts
index 4987fc077..40cfdfd28 100644
--- a/plugins/terminals/app/main.ts
+++ b/plugins/terminals/app/main.ts
@@ -1,7 +1,8 @@
///
-import { syncPanelTheme } from '../../../design/panel-theme'
+import { applyPanelBranding } from '../../../design/panel-theme'
import { mountTerminals } from './client/index'
+import '../../../packages/hub-ui/src/client/primary-ramp.css'
const app = document.getElementById('app')
if (!app)
@@ -11,5 +12,5 @@ mountTerminals(app).catch((error) => {
app.textContent = `Failed to connect: ${error instanceof Error ? error.message : String(error)}`
})
-const stopPanelTheme = syncPanelTheme()
+const stopPanelTheme = applyPanelBranding()
import.meta.hot?.dispose(stopPanelTheme)
diff --git a/tests/__snapshots__/tsnapi/devframe/client.snapshot.d.ts b/tests/__snapshots__/tsnapi/devframe/client.snapshot.d.ts
index 5aab5f959..0eebf21b1 100644
--- a/tests/__snapshots__/tsnapi/devframe/client.snapshot.d.ts
+++ b/tests/__snapshots__/tsnapi/devframe/client.snapshot.d.ts
@@ -111,13 +111,6 @@ export interface DevframeServicesClient {
keys: () => string[];
state: () => Promise>;
}
-export interface DevframeTheme {
- primaryColor?: string;
-}
-export interface DevframeThemeProvider {
- update: (_: DevframeTheme) => void;
- dispose: () => void;
-}
export interface RegisterWebMcpToolsOptions {
modelContext?: WebMcpModelContext;
}
@@ -215,7 +208,6 @@ export declare function createScopedClientContext(_:
export declare function getDevframeConnection(): DevframeConnection | undefined;
export declare function getDevframeRpcClient(_?: DevframeRpcClientOptions): Promise;
export declare function isCallableStatus(_: DevframeConnectionStatus): boolean;
-export declare function provideDevframeTheme(_: HTMLIFrameElement, _: DevframeTheme): DevframeThemeProvider;
export declare function readOtpFromUrl(_?: string): string | undefined;
export declare function registerDevframeViewerOrigin(_: DevframeConnection, _?: any): Promise;
export declare function registerWebMcpTools(_: RpcFunctionsCollector, _?: RegisterWebMcpToolsOptions): () => void;
@@ -224,7 +216,6 @@ export declare function resolveSseUrl(_: ConnectionMeta['sse'], _: string, _: Ws
export declare function resolveWebMcpModelContext(): WebMcpModelContext | undefined;
export declare function resolveWsUrl(_: ConnectionMeta['websocket'], _: string, _: WsUrlLocation): string;
export declare function setupDevframeConnection(_?: SetupDevframeConnectionOptions): Promise;
-export declare function watchDevframeTheme(_: (_: DevframeTheme) => void): () => void;
// #endregion
// #region Variables
diff --git a/tests/__snapshots__/tsnapi/devframe/client.snapshot.js b/tests/__snapshots__/tsnapi/devframe/client.snapshot.js
index b77abd471..5624d64ba 100644
--- a/tests/__snapshots__/tsnapi/devframe/client.snapshot.js
+++ b/tests/__snapshots__/tsnapi/devframe/client.snapshot.js
@@ -18,7 +18,6 @@ export function createScopedClientContext(_, _) {}
export function getDevframeConnection() {}
export async function getDevframeRpcClient(_) {}
export function isCallableStatus(_) {}
-export function provideDevframeTheme(_, _) {}
export function readOtpFromUrl(_) {}
export async function registerDevframeViewerOrigin(_, _) {}
export function registerWebMcpTools(_, _) {}
@@ -27,7 +26,6 @@ export function resolveSseUrl(_, _, _) {}
export function resolveWebMcpModelContext() {}
export function resolveWsUrl(_, _, _) {}
export async function setupDevframeConnection(_) {}
-export function watchDevframeTheme(_) {}
// #endregion
// #region Variables
diff --git a/tests/__snapshots__/tsnapi/devframe/constants.snapshot.d.ts b/tests/__snapshots__/tsnapi/devframe/constants.snapshot.d.ts
index 8129c5831..d3f0cf78c 100644
--- a/tests/__snapshots__/tsnapi/devframe/constants.snapshot.d.ts
+++ b/tests/__snapshots__/tsnapi/devframe/constants.snapshot.d.ts
@@ -38,7 +38,6 @@ export declare const DEVFRAME_EVENTS: {
readonly panelStatePatch: "devframe:in-page:panel-state:patch";
};
readonly postMessage: {
- readonly theme: "devframe:theme";
readonly remoteAssetsError: "devframe:remote-assets-error";
readonly inPageChannel: "devframe:in-page-channel";
};
diff --git a/tests/panel-branding.test.ts b/tests/panel-branding.test.ts
new file mode 100644
index 000000000..2a90c6723
--- /dev/null
+++ b/tests/panel-branding.test.ts
@@ -0,0 +1,74 @@
+import { afterEach, beforeEach, expect, it, vi } from 'vitest'
+import { applyPanelBranding } from '../design/panel-theme'
+
+const setup = vi.hoisted(() => vi.fn())
+vi.mock('devframe/client', () => ({ setupDevframeConnection: setup }))
+
+let values: Map
+let cleanup: (() => void) | undefined
+
+beforeEach(() => {
+ values = new Map()
+ setup.mockReset()
+ vi.stubGlobal('window', { parent: {} })
+ vi.stubGlobal('CSS', { supports: (_: string, color: string) => color === '#6b84fd' })
+ vi.stubGlobal('document', {
+ documentElement: {
+ style: {
+ getPropertyValue: (name: string) => values.get(name) ?? '',
+ getPropertyPriority: () => '',
+ setProperty: (name: string, value: string) => values.set(name, value),
+ removeProperty: (name: string) => values.delete(name),
+ },
+ },
+ })
+})
+
+afterEach(() => {
+ cleanup?.()
+ cleanup = undefined
+ vi.unstubAllGlobals()
+})
+
+it('keeps standalone SPAs independent of hub branding', () => {
+ window.parent = window
+ cleanup = applyPanelBranding()
+ expect(setup).not.toHaveBeenCalled()
+ expect(values.size).toBe(0)
+})
+
+it('maps startup metadata to one CSS input and restores the previous palette on cleanup', async () => {
+ values.set('--devframe-primary', 'red')
+ setup.mockResolvedValue({ connectionMeta: { configs: { ui: { branding: { primaryColor: '#6b84fd' } } } } })
+ cleanup = applyPanelBranding()
+ await vi.waitFor(() => expect(values.get('--devframe-primary')).toBe('#6b84fd'))
+ expect([...values.keys()]).toEqual(['--devframe-primary'])
+ cleanup()
+ expect(values.get('--devframe-primary')).toBe('red')
+})
+
+it.each([undefined, '', 'invalid-color'])('keeps the default palette for missing or invalid branding (%s)', async (primaryColor) => {
+ setup.mockResolvedValue({ connectionMeta: { configs: { ui: { branding: { primaryColor } } } } })
+ cleanup = applyPanelBranding()
+ await setup.mock.results[0].value
+ expect(values.size).toBe(0)
+})
+
+it('keeps the default palette when connection metadata is unavailable', async () => {
+ setup.mockRejectedValue(new Error('Unavailable'))
+ cleanup = applyPanelBranding()
+ await new Promise(resolve => setTimeout(resolve, 0))
+ expect(values.size).toBe(0)
+})
+
+it('ignores metadata that arrives after the SPA has been disposed', async () => {
+ let resolve!: (value: unknown) => void
+ setup.mockReturnValue(new Promise((done) => {
+ resolve = done
+ }))
+ cleanup = applyPanelBranding()
+ cleanup()
+ resolve({ connectionMeta: { configs: { ui: { branding: { primaryColor: '#6b84fd' } } } } })
+ await setup.mock.results[0].value
+ expect(values.size).toBe(0)
+})
diff --git a/turbo.json b/turbo.json
index e3c5c8990..7cbcb286c 100644
--- a/turbo.json
+++ b/turbo.json
@@ -69,36 +69,43 @@
},
"@devframes/plugin-code-server#build": {
"outputLogs": "new-only",
+ "inputs": ["$TURBO_DEFAULT$", "$TURBO_ROOT$/design/panel-theme.ts", "$TURBO_ROOT$/packages/hub-ui/src/client/primary-ramp.css"],
"dependsOn": ["devframe#build", "@devframes/vite#build", "@devframes/json-render#build"],
"outputs": ["dist/**", "assets-pkg/dist/**"]
},
"@devframes/plugin-terminals#build": {
"outputLogs": "new-only",
+ "inputs": ["$TURBO_DEFAULT$", "$TURBO_ROOT$/design/panel-theme.ts", "$TURBO_ROOT$/packages/hub-ui/src/client/primary-ramp.css"],
"dependsOn": ["devframe#build", "@devframes/vite#build", "@devframes/json-render#build"],
"outputs": ["dist/**", "assets-pkg/dist/**"]
},
"@devframes/plugin-inspect#build": {
"outputLogs": "new-only",
+ "inputs": ["$TURBO_DEFAULT$", "$TURBO_ROOT$/design/panel-theme.ts", "$TURBO_ROOT$/packages/hub-ui/src/client/primary-ramp.css"],
"dependsOn": ["devframe#build", "@devframes/vite#build", "@devframes/json-render#build"],
"outputs": ["dist/**", "assets-pkg/dist/**"]
},
"@devframes/plugin-og#build": {
"outputLogs": "new-only",
+ "inputs": ["$TURBO_DEFAULT$", "$TURBO_ROOT$/design/panel-theme.ts", "$TURBO_ROOT$/packages/hub-ui/src/client/primary-ramp.css"],
"dependsOn": ["devframe#build", "@devframes/vite#build", "@devframes/json-render#build"],
"outputs": ["dist/**", "assets-pkg/dist/**"]
},
"@devframes/plugin-data-inspector#build": {
"outputLogs": "new-only",
+ "inputs": ["$TURBO_DEFAULT$", "$TURBO_ROOT$/design/panel-theme.ts", "$TURBO_ROOT$/packages/hub-ui/src/client/primary-ramp.css"],
"dependsOn": ["devframe#build", "@devframes/vite#build", "@devframes/json-render#build"],
"outputs": ["dist/**", "assets-pkg/dist/**"]
},
"@devframes/plugin-messages#build": {
"outputLogs": "new-only",
+ "inputs": ["$TURBO_DEFAULT$", "$TURBO_ROOT$/design/panel-theme.ts", "$TURBO_ROOT$/packages/hub-ui/src/client/primary-ramp.css"],
"dependsOn": ["devframe#build", "@devframes/hub#build", "@devframes/vite#build", "@devframes/json-render#build"],
"outputs": ["dist/**", "assets-pkg/dist/**"]
},
"@devframes/plugin-assets#build": {
"outputLogs": "new-only",
+ "inputs": ["$TURBO_DEFAULT$", "$TURBO_ROOT$/design/panel-theme.ts", "$TURBO_ROOT$/packages/hub-ui/src/client/primary-ramp.css"],
"dependsOn": ["devframe#build", "@devframes/vite#build", "@devframes/json-render#build"],
"outputs": ["dist/**", "assets-pkg/dist/**"]
},
@@ -164,11 +171,13 @@
},
"@devframes/plugin-git#build": {
"outputLogs": "new-only",
+ "inputs": ["$TURBO_DEFAULT$", "$TURBO_ROOT$/design/panel-theme.ts", "$TURBO_ROOT$/packages/hub-ui/src/client/primary-ramp.css"],
"dependsOn": ["devframe#build"],
"outputs": ["dist/**", "assets-pkg/dist/**"]
},
"@devframes/plugin-a11y#build": {
"outputLogs": "new-only",
+ "inputs": ["$TURBO_DEFAULT$", "$TURBO_ROOT$/design/panel-theme.ts", "$TURBO_ROOT$/packages/hub-ui/src/client/primary-ramp.css"],
"dependsOn": ["devframe#build", "@devframes/vite#build", "@devframes/json-render#build"],
"outputs": ["dist/**", "assets-pkg/dist/**"]
},
From 1ec28225087ccc0f11eccf068c3af2b237784289 Mon Sep 17 00:00:00 2001
From: "Anthony Fu (via agent)"
Date: Fri, 25 Sep 2026 02:26:23 +0000
Subject: [PATCH 5/5] refactor(design): move shared primary-ramp stylesheet
into design/
Consolidate the two near-identical primary-ramp.css files (hub-ui's and
json-render-ui's) into a single design/primary-ramp.css, next to the ramp
contract it must match (OVERRIDABLE_PRIMARY_STOPS in design/uno.config.ts).
Panels, both shadow-root build-css scripts, and the Storybook preview now
read the one shared file, and the nine panel build tasks track design/**
instead of listing two shared files each.
---
design/build-shadow-css.ts | 2 +-
.../src/client => design}/primary-ramp.css | 43 +++++++++++--------
design/uno.config.ts | 4 +-
examples/hub-deno/src/hub.ts | 2 +-
examples/hub-fastify/src/hub.ts | 2 +-
examples/hub-hono/src/app.ts | 2 +-
examples/hub-next/src/client/hub.ts | 2 +-
examples/hub-nitro/hub.ts | 2 +-
examples/hub-rsbuild/rsbuild.config.ts | 2 +-
examples/hub-sveltekit/src/hub.ts | 2 +-
examples/hub-vite/vite.config.ts | 2 +-
packages/hub-ui/.storybook/preview.ts | 2 +-
packages/hub-ui/scripts/build-css.ts | 2 +-
packages/json-render-ui/scripts/build-css.ts | 2 +-
.../src/renderer-module/primary-ramp.css | 37 ----------------
plugins/a11y/app/main.tsx | 2 +-
plugins/assets/app/main.ts | 2 +-
plugins/code-server/app/main.ts | 2 +-
plugins/data-inspector/app/main.ts | 2 +-
plugins/git/app/app/layout.tsx | 2 +-
plugins/inspect/app/main.ts | 2 +-
plugins/messages/app/main.ts | 2 +-
plugins/og/app/main.ts | 2 +-
plugins/terminals/app/main.ts | 2 +-
turbo.json | 18 ++++----
25 files changed, 56 insertions(+), 88 deletions(-)
rename {packages/hub-ui/src/client => design}/primary-ramp.css (52%)
delete mode 100644 packages/json-render-ui/src/renderer-module/primary-ramp.css
diff --git a/design/build-shadow-css.ts b/design/build-shadow-css.ts
index 68f410d84..c68f86082 100644
--- a/design/build-shadow-css.ts
+++ b/design/build-shadow-css.ts
@@ -26,7 +26,7 @@ export interface BuildShadowCssOptions {
/**
* Absolute path to the primary-ramp override stylesheet, appended AFTER
* the UnoCSS output so its `:host`/`:root, :host` block wins over Wind's
- * own primary declarations (see each package's `primary-ramp.css`).
+ * own primary declarations (the shared `design/primary-ramp.css`).
*/
primaryRampPath: string
/**
diff --git a/packages/hub-ui/src/client/primary-ramp.css b/design/primary-ramp.css
similarity index 52%
rename from packages/hub-ui/src/client/primary-ramp.css
rename to design/primary-ramp.css
index d8c5cf57c..bf05c5eee 100644
--- a/packages/hub-ui/src/client/primary-ramp.css
+++ b/design/primary-ramp.css
@@ -1,9 +1,11 @@
/*
- * Primary color as a single overridable variable: `--devframe-primary`.
- *
- * Appended AFTER the UnoCSS output (see `scripts/build-css.ts`) so this block
- * wins over Wind3's own `:root, :host` primary declarations, and imported after
- * `virtual:uno.css` in the Storybook preview for the same reason.
+ * Primary color as a single overridable variable: `--devframe-primary`. Shared
+ * by every devframe surface: appended after each shadow-root build's UnoCSS
+ * output (the hub-ui dock, the json-render renderer module; see
+ * `build-shadow-css.ts`), imported last by every built-in devframe panel SPA so
+ * it inherits a rebranded hub's accent, and imported after `virtual:uno.css` in
+ * the hub-ui Storybook preview. It always wins over Wind's own `:root, :host`
+ * primary declarations because it comes last.
*
* Each stop (`--colors-primary-*`) is derived from `--devframe-primary` via
* `color-mix`. When `--devframe-primary` is unset, the intermediate
@@ -11,28 +13,31 @@
* references an unset var with no fallback), so each `--colors-primary-`
* falls back to the exact devframe default hex; the default look is preserved,
* and only an explicit override derives a new ramp. The vars inherit through
- * the tree, so setting `--devframe-primary` on the dock host retints
- * everything, and setting it on a group's chrome container retints just that
- * group (the group-accent mechanism).
+ * the tree, so setting `--devframe-primary` on a host (the dock host, a viewer
+ * mount container, or a panel's ``) retints everything below it, and
+ * setting it on a group's chrome container retints just that group (the
+ * group-accent mechanism).
*
- * The dock's shadow root is built on Wind3, which bakes `primary`-based
- * utilities (`text-primary`, `bg-primary`, `btn-primary`, `ring-primary-500`,
- * …) to literal `rgb()` triplets rather than referencing these variables, so
- * `scripts/build-css.ts` rewires those baked colors into CSS relative-color
- * syntax reading `--colors-primary-` (see `rewireBakedPrimaryColors` in
- * `design/uno.config.ts`) so this block actually retints them, not just the
- * handful of rules (the glow gradient below) that reference the variables
- * directly. The stops declared here (`DEFAULT`/`600`/`500`/`400`/`300`) must
- * match `OVERRIDABLE_PRIMARY_STOPS` there.
+ * A shadow root built on Wind3 bakes `primary`-based utilities (`text-primary`,
+ * `bg-primary`, `btn-primary`, `ring-primary-500`, …) to literal `rgb()`
+ * triplets rather than referencing these variables, so `build-shadow-css.ts`
+ * rewires those baked colors into CSS relative-color syntax reading
+ * `--colors-primary-` (see `rewireBakedPrimaryColors` in `uno.config.ts`)
+ * so this block actually retints them, not just the handful of rules (the dock's
+ * glow gradient) that reference the variables directly. The stops declared here
+ * (`DEFAULT`/`600`/`500`/`400`/`300`) must match `OVERRIDABLE_PRIMARY_STOPS`
+ * there.
*
- * `:host` is the effective selector inside the dock's shadow root; `:root` is
- * for the light-DOM Storybook preview (it matches nothing in the shadow root).
+ * `:host` is the effective selector inside a shadow root; `:root` is for the
+ * light-DOM panel SPAs and the Storybook preview (it matches nothing in a
+ * shadow root).
*
* `.devframes-accent-scope` re-declares the whole block on a group's chrome
* container: because custom properties inherit as computed values, a descendant
* that only overrides `--devframe-primary` would keep the `:host`-computed
* stops; re-running the derivation here recomputes them from the container's
* own `--devframe-primary` (a group's `accentColor`), retinting just that group.
+ * Inert on surfaces that never apply the class.
*/
:root,
:host,
diff --git a/design/uno.config.ts b/design/uno.config.ts
index 2c2f6e547..5529c656a 100644
--- a/design/uno.config.ts
+++ b/design/uno.config.ts
@@ -140,8 +140,8 @@ export const shadowSurfaceSafelist: string[] = [
]
/**
- * The primary-ramp stops a shadow-root surface's `primary-ramp.css` exposes
- * as overridable `--colors-primary-` custom properties (derived from
+ * The primary-ramp stops the shared `design/primary-ramp.css` exposes as
+ * overridable `--colors-primary-` custom properties (derived from
* `--devframe-primary`). Must match that file's declarations exactly.
*/
const OVERRIDABLE_PRIMARY_STOPS = ['DEFAULT', '600', '500', '400', '300'] as const
diff --git a/examples/hub-deno/src/hub.ts b/examples/hub-deno/src/hub.ts
index 0bd51cc01..cb1a3b958 100644
--- a/examples/hub-deno/src/hub.ts
+++ b/examples/hub-deno/src/hub.ts
@@ -40,7 +40,7 @@ export const hub: HubInstance = globalRef.__hubDenoMinimal ??= initHub({
* Rebrand the reference UI to Deno's own navy in one field, no CSS:
* `createUi`'s `branding` option publishes `ConnectionMeta.configs.ui.branding`,
* which the dock reads at connect time and feeds into `--devframe-primary`
- * (see `@devframes/hub-ui`'s `primary-ramp.css`).
+ * (see the shared `design/primary-ramp.css`).
*/
ui: createUi({ branding: { primaryColor: '#70ffaf', productName: 'Devframes on Deno' } }),
/**
diff --git a/examples/hub-fastify/src/hub.ts b/examples/hub-fastify/src/hub.ts
index eb01e5166..4b1ffe6eb 100644
--- a/examples/hub-fastify/src/hub.ts
+++ b/examples/hub-fastify/src/hub.ts
@@ -40,7 +40,7 @@ export const hub: HubInstance = globalRef.__hubFastifyMinimal ??= initHub({
* Rebrand the reference UI to Fastify's own black in one field, no CSS:
* `createUi`'s `branding` option publishes `ConnectionMeta.configs.ui.branding`,
* which the dock reads at connect time and feeds into `--devframe-primary`
- * (see `@devframes/hub-ui`'s `primary-ramp.css`).
+ * (see the shared `design/primary-ramp.css`).
*/
ui: createUi({ branding: { primaryColor: '#2f2f2f', productName: 'Devframes on Fastify' } }),
/**
diff --git a/examples/hub-hono/src/app.ts b/examples/hub-hono/src/app.ts
index 125ac97c5..bf692caa7 100644
--- a/examples/hub-hono/src/app.ts
+++ b/examples/hub-hono/src/app.ts
@@ -43,7 +43,7 @@ export const hub: HubInstance = globalRef.__hubHonoMinimal ??= initHub({
* Rebrand the reference UI to Hono's own orange in one field, no CSS:
* `createUi`'s `branding` option publishes `ConnectionMeta.configs.ui.branding`,
* which the dock reads at connect time and feeds into `--devframe-primary`
- * (see `@devframes/hub-ui`'s `primary-ramp.css`).
+ * (see the shared `design/primary-ramp.css`).
*/
ui: createUi({ branding: { primaryColor: '#e36002', productName: 'Devframes on Hono' } }),
/**
diff --git a/examples/hub-next/src/client/hub.ts b/examples/hub-next/src/client/hub.ts
index e3e8012a0..72b6d789c 100644
--- a/examples/hub-next/src/client/hub.ts
+++ b/examples/hub-next/src/client/hub.ts
@@ -65,7 +65,7 @@ async function loadHub(): Promise {
// `@devframes/next/hub` runs the socket on a side-car (Next routes can't
// accept WS upgrades). `createUi`'s `branding` option rebrands the dock via
// `ConnectionMeta.configs.ui.branding`, read at connect time and fed into
- // `--devframe-primary` (see `@devframes/hub-ui`'s `primary-ramp.css`).
+ // `--devframe-primary` (see the shared `design/primary-ramp.css`).
return createNextDevframeHub({
devframes,
ui: (hubUi.createUi as typeof CreateUi)({ branding: { primaryColor: '#3f8ba9', productName: 'Devframes on Next.js' } }),
diff --git a/examples/hub-nitro/hub.ts b/examples/hub-nitro/hub.ts
index 8e6b04642..bdaa1b7c8 100644
--- a/examples/hub-nitro/hub.ts
+++ b/examples/hub-nitro/hub.ts
@@ -41,7 +41,7 @@ export const hub: HubInstance = globalRef.__hubNitroMinimal ??= initHub({
* Rebrand the reference UI to Nitro's own pink/red in one field, no CSS:
* `createUi`'s `branding` option publishes `ConnectionMeta.configs.ui.branding`,
* which the dock reads at connect time and feeds into `--devframe-primary`
- * (see `@devframes/hub-ui`'s `primary-ramp.css`).
+ * (see the shared `design/primary-ramp.css`).
*/
ui: createUi({ branding: { primaryColor: '#ff2056', productName: 'Devframes on Nitro' } }),
/**
diff --git a/examples/hub-rsbuild/rsbuild.config.ts b/examples/hub-rsbuild/rsbuild.config.ts
index 58281f0a2..b8e2b0eb8 100644
--- a/examples/hub-rsbuild/rsbuild.config.ts
+++ b/examples/hub-rsbuild/rsbuild.config.ts
@@ -79,7 +79,7 @@ export default defineConfig({
* CSS: `createUi`'s `branding` option publishes
* `ConnectionMeta.configs.ui.branding`, which the dock reads at
* connect time and feeds into `--devframe-primary` (see
- * `@devframes/hub-ui`'s `primary-ramp.css`).
+ * the shared `design/primary-ramp.css`).
*/
ui: createUi({ branding: { primaryColor: '#ff5e00', productName: 'Devframes on Rsbuild' } }),
/**
diff --git a/examples/hub-sveltekit/src/hub.ts b/examples/hub-sveltekit/src/hub.ts
index 25ad63475..c698339b6 100644
--- a/examples/hub-sveltekit/src/hub.ts
+++ b/examples/hub-sveltekit/src/hub.ts
@@ -41,7 +41,7 @@ export const hub: HubInstance = globalRef.__hubSvelteKitMinimal ??= initHub({
* Rebrand the reference UI to Svelte's own orange in one field, no CSS:
* `createUi`'s `branding` option publishes `ConnectionMeta.configs.ui.branding`,
* which the dock reads at connect time and feeds into `--devframe-primary`
- * (see `@devframes/hub-ui`'s `primary-ramp.css`).
+ * (see the shared `design/primary-ramp.css`).
*/
ui: createUi({ branding: { primaryColor: '#ff3e00', productName: 'Devframes on SvelteKit' } }),
/**
diff --git a/examples/hub-vite/vite.config.ts b/examples/hub-vite/vite.config.ts
index 0e524826c..c1e29392d 100644
--- a/examples/hub-vite/vite.config.ts
+++ b/examples/hub-vite/vite.config.ts
@@ -83,7 +83,7 @@ export default defineConfig({
* Rebrand the reference UI to Vite's own purple in one field, no CSS:
* `createUi`'s `branding` option publishes `ConnectionMeta.configs.ui.branding`,
* which the dock reads at connect time and feeds into `--devframe-primary`
- * (see `@devframes/hub-ui`'s `primary-ramp.css`). Passing `ui` overrides
+ * (see the shared `design/primary-ramp.css`). Passing `ui` overrides
* the default `createUi()` the plugin would otherwise use.
*/
ui: createUi({ branding: { primaryColor: '#646cff', productName: 'Devframes on Vite' } }),
diff --git a/packages/hub-ui/.storybook/preview.ts b/packages/hub-ui/.storybook/preview.ts
index 20baf8de2..45cacd0f2 100644
--- a/packages/hub-ui/.storybook/preview.ts
+++ b/packages/hub-ui/.storybook/preview.ts
@@ -6,7 +6,7 @@ import 'virtual:uno.css'
import '@antfu/design/styles.css'
import '../src/client/style.css'
// After uno so the primary-ramp override wins, matching the shadow-root build.
-import '../src/client/primary-ramp.css'
+import '../../../design/primary-ramp.css'
// Drive the shared `@antfu/design` tokens off the toolbar theme toggle: dark mode
// is the `.dark` class on ``, and the canvas takes the semantic
diff --git a/packages/hub-ui/scripts/build-css.ts b/packages/hub-ui/scripts/build-css.ts
index 213ab1d56..6c19d6147 100644
--- a/packages/hub-ui/scripts/build-css.ts
+++ b/packages/hub-ui/scripts/build-css.ts
@@ -13,7 +13,7 @@ const { sourceCount, css } = await buildShadowCss({
srcDir: SRC_DIR,
globs: ['components/**/*.{ts,vue}', 'state/**/*.ts', 'embedded/**/*.ts', 'standalone/**/*.{ts,html}'],
config,
- primaryRampPath: join(SRC_DIR, 'primary-ramp.css'),
+ primaryRampPath: fileURLToPath(new URL('../../../design/primary-ramp.css', import.meta.url)),
userStylePath: join(SRC_DIR, 'style.css'),
varPrefix: '--un-hub-',
})
diff --git a/packages/json-render-ui/scripts/build-css.ts b/packages/json-render-ui/scripts/build-css.ts
index e940952b7..2f0987cc0 100644
--- a/packages/json-render-ui/scripts/build-css.ts
+++ b/packages/json-render-ui/scripts/build-css.ts
@@ -16,7 +16,7 @@ const { sourceCount, css } = await buildShadowCss({
srcDir: SRC_DIR,
globs: ['components/**/*.ts', 'renderer.ts', 'dock-renderer.ts', 'renderer-module/**/*.ts'],
config,
- primaryRampPath: join(SRC_DIR, 'renderer-module/primary-ramp.css'),
+ primaryRampPath: fileURLToPath(new URL('../../../design/primary-ramp.css', import.meta.url)),
userStylePath: [
moduleRequire.resolve('@antfu/design/styles/scrollbar.css'),
join(SRC_DIR, 'renderer-module/style.css'),
diff --git a/packages/json-render-ui/src/renderer-module/primary-ramp.css b/packages/json-render-ui/src/renderer-module/primary-ramp.css
deleted file mode 100644
index 0de6de26c..000000000
--- a/packages/json-render-ui/src/renderer-module/primary-ramp.css
+++ /dev/null
@@ -1,37 +0,0 @@
-/*
- * Primary color as a single overridable variable: `--devframe-primary`.
- *
- * Mirrors `@devframes/hub-ui`'s ramp (see its `src/client/primary-ramp.css`
- * for the full derivation notes): each stop (`--colors-primary-*`) is
- * derived from `--devframe-primary` via `color-mix`, falling back to the
- * devframe default sage green when it is unset.
- *
- * Appended AFTER the UnoCSS output (see `scripts/build-css.ts`) so this block
- * wins over Wind3's own `:root, :host` primary declarations. `:host` is the
- * effective selector inside the renderer module's shadow root, whose host is
- * the viewer-owned mount container, and `--devframe-primary` (set by a
- * viewer's branding on any ancestor) inherits across the shadow boundary onto
- * it, so a rebranded hub retints the rendered views too.
- *
- * The renderer module's shadow root is built on Wind3, which bakes
- * `primary`-based utilities to literal `rgb()` triplets rather than
- * referencing these variables, so `scripts/build-css.ts` rewires those baked
- * colors into CSS relative-color syntax reading `--colors-primary-`
- * (see `rewireBakedPrimaryColors` in `design/uno.config.ts`) so this block
- * actually retints them. The stops declared here (`DEFAULT`/`600`/`500`/
- * `400`/`300`) must match `OVERRIDABLE_PRIMARY_STOPS` there.
- */
-:root,
-:host {
- --devframe-primary-DEFAULT: var(--devframe-primary);
- --devframe-primary-600: color-mix(in oklab, var(--devframe-primary), white 9%);
- --devframe-primary-500: color-mix(in oklab, var(--devframe-primary), white 18%);
- --devframe-primary-400: color-mix(in oklab, var(--devframe-primary), white 34%);
- --devframe-primary-300: color-mix(in oklab, var(--devframe-primary), white 54%);
-
- --colors-primary-DEFAULT: var(--devframe-primary-DEFAULT, #3a6a45);
- --colors-primary-600: var(--devframe-primary-600, #40704b);
- --colors-primary-500: var(--devframe-primary-500, #578861);
- --colors-primary-400: var(--devframe-primary-400, #75a67e);
- --colors-primary-300: var(--devframe-primary-300, #93c69d);
-}
diff --git a/plugins/a11y/app/main.tsx b/plugins/a11y/app/main.tsx
index 125c2efc7..3b7069ee5 100644
--- a/plugins/a11y/app/main.tsx
+++ b/plugins/a11y/app/main.tsx
@@ -8,7 +8,7 @@ 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'
+import '../../../design/primary-ramp.css'
// Shared design tokens flip on the `.dark` class; mirror the OS preference onto
// (the other devframe plugins follow the same approach).
diff --git a/plugins/assets/app/main.ts b/plugins/assets/app/main.ts
index a09b21739..b89b69a7d 100644
--- a/plugins/assets/app/main.ts
+++ b/plugins/assets/app/main.ts
@@ -4,7 +4,7 @@ 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'
+import '../../../design/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).
diff --git a/plugins/code-server/app/main.ts b/plugins/code-server/app/main.ts
index aa0fdbda8..11ef18057 100644
--- a/plugins/code-server/app/main.ts
+++ b/plugins/code-server/app/main.ts
@@ -6,7 +6,7 @@ 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'
+import '../../../design/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).
diff --git a/plugins/data-inspector/app/main.ts b/plugins/data-inspector/app/main.ts
index 009ed3fb4..7ab6eb803 100644
--- a/plugins/data-inspector/app/main.ts
+++ b/plugins/data-inspector/app/main.ts
@@ -11,7 +11,7 @@ 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'
+import '../../../design/primary-ramp.css'
createApp(App).mount('#app')
diff --git a/plugins/git/app/app/layout.tsx b/plugins/git/app/app/layout.tsx
index 9e5d763bd..56f516fb7 100644
--- a/plugins/git/app/app/layout.tsx
+++ b/plugins/git/app/app/layout.tsx
@@ -2,7 +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'
+import '../../../../design/primary-ramp.css'
export const metadata: Metadata = {
title: 'Git Dashboard',
diff --git a/plugins/inspect/app/main.ts b/plugins/inspect/app/main.ts
index 8bd0b7167..9e153dd40 100644
--- a/plugins/inspect/app/main.ts
+++ b/plugins/inspect/app/main.ts
@@ -7,7 +7,7 @@ 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'
+import '../../../design/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).
diff --git a/plugins/messages/app/main.ts b/plugins/messages/app/main.ts
index 6bfca9ec8..302cefd62 100644
--- a/plugins/messages/app/main.ts
+++ b/plugins/messages/app/main.ts
@@ -2,7 +2,7 @@
import { applyPanelBranding } from '../../../design/panel-theme'
import { mountMessages } from './client/index'
-import '../../../packages/hub-ui/src/client/primary-ramp.css'
+import '../../../design/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
diff --git a/plugins/og/app/main.ts b/plugins/og/app/main.ts
index 9306b3092..2dc142e85 100644
--- a/plugins/og/app/main.ts
+++ b/plugins/og/app/main.ts
@@ -1,7 +1,7 @@
import { createApp } from 'vue'
import { applyPanelBranding } from '../../../design/panel-theme'
import App from './app/app.vue'
-import '../../../packages/hub-ui/src/client/primary-ramp.css'
+import '../../../design/primary-ramp.css'
const media = window.matchMedia('(prefers-color-scheme: dark)')
function applyScheme(dark: boolean): void {
diff --git a/plugins/terminals/app/main.ts b/plugins/terminals/app/main.ts
index 40cfdfd28..a6148503c 100644
--- a/plugins/terminals/app/main.ts
+++ b/plugins/terminals/app/main.ts
@@ -2,7 +2,7 @@
import { applyPanelBranding } from '../../../design/panel-theme'
import { mountTerminals } from './client/index'
-import '../../../packages/hub-ui/src/client/primary-ramp.css'
+import '../../../design/primary-ramp.css'
const app = document.getElementById('app')
if (!app)
diff --git a/turbo.json b/turbo.json
index 7cbcb286c..def0fced9 100644
--- a/turbo.json
+++ b/turbo.json
@@ -69,43 +69,43 @@
},
"@devframes/plugin-code-server#build": {
"outputLogs": "new-only",
- "inputs": ["$TURBO_DEFAULT$", "$TURBO_ROOT$/design/panel-theme.ts", "$TURBO_ROOT$/packages/hub-ui/src/client/primary-ramp.css"],
+ "inputs": ["$TURBO_DEFAULT$", "$TURBO_ROOT$/design/**"],
"dependsOn": ["devframe#build", "@devframes/vite#build", "@devframes/json-render#build"],
"outputs": ["dist/**", "assets-pkg/dist/**"]
},
"@devframes/plugin-terminals#build": {
"outputLogs": "new-only",
- "inputs": ["$TURBO_DEFAULT$", "$TURBO_ROOT$/design/panel-theme.ts", "$TURBO_ROOT$/packages/hub-ui/src/client/primary-ramp.css"],
+ "inputs": ["$TURBO_DEFAULT$", "$TURBO_ROOT$/design/**"],
"dependsOn": ["devframe#build", "@devframes/vite#build", "@devframes/json-render#build"],
"outputs": ["dist/**", "assets-pkg/dist/**"]
},
"@devframes/plugin-inspect#build": {
"outputLogs": "new-only",
- "inputs": ["$TURBO_DEFAULT$", "$TURBO_ROOT$/design/panel-theme.ts", "$TURBO_ROOT$/packages/hub-ui/src/client/primary-ramp.css"],
+ "inputs": ["$TURBO_DEFAULT$", "$TURBO_ROOT$/design/**"],
"dependsOn": ["devframe#build", "@devframes/vite#build", "@devframes/json-render#build"],
"outputs": ["dist/**", "assets-pkg/dist/**"]
},
"@devframes/plugin-og#build": {
"outputLogs": "new-only",
- "inputs": ["$TURBO_DEFAULT$", "$TURBO_ROOT$/design/panel-theme.ts", "$TURBO_ROOT$/packages/hub-ui/src/client/primary-ramp.css"],
+ "inputs": ["$TURBO_DEFAULT$", "$TURBO_ROOT$/design/**"],
"dependsOn": ["devframe#build", "@devframes/vite#build", "@devframes/json-render#build"],
"outputs": ["dist/**", "assets-pkg/dist/**"]
},
"@devframes/plugin-data-inspector#build": {
"outputLogs": "new-only",
- "inputs": ["$TURBO_DEFAULT$", "$TURBO_ROOT$/design/panel-theme.ts", "$TURBO_ROOT$/packages/hub-ui/src/client/primary-ramp.css"],
+ "inputs": ["$TURBO_DEFAULT$", "$TURBO_ROOT$/design/**"],
"dependsOn": ["devframe#build", "@devframes/vite#build", "@devframes/json-render#build"],
"outputs": ["dist/**", "assets-pkg/dist/**"]
},
"@devframes/plugin-messages#build": {
"outputLogs": "new-only",
- "inputs": ["$TURBO_DEFAULT$", "$TURBO_ROOT$/design/panel-theme.ts", "$TURBO_ROOT$/packages/hub-ui/src/client/primary-ramp.css"],
+ "inputs": ["$TURBO_DEFAULT$", "$TURBO_ROOT$/design/**"],
"dependsOn": ["devframe#build", "@devframes/hub#build", "@devframes/vite#build", "@devframes/json-render#build"],
"outputs": ["dist/**", "assets-pkg/dist/**"]
},
"@devframes/plugin-assets#build": {
"outputLogs": "new-only",
- "inputs": ["$TURBO_DEFAULT$", "$TURBO_ROOT$/design/panel-theme.ts", "$TURBO_ROOT$/packages/hub-ui/src/client/primary-ramp.css"],
+ "inputs": ["$TURBO_DEFAULT$", "$TURBO_ROOT$/design/**"],
"dependsOn": ["devframe#build", "@devframes/vite#build", "@devframes/json-render#build"],
"outputs": ["dist/**", "assets-pkg/dist/**"]
},
@@ -171,13 +171,13 @@
},
"@devframes/plugin-git#build": {
"outputLogs": "new-only",
- "inputs": ["$TURBO_DEFAULT$", "$TURBO_ROOT$/design/panel-theme.ts", "$TURBO_ROOT$/packages/hub-ui/src/client/primary-ramp.css"],
+ "inputs": ["$TURBO_DEFAULT$", "$TURBO_ROOT$/design/**"],
"dependsOn": ["devframe#build"],
"outputs": ["dist/**", "assets-pkg/dist/**"]
},
"@devframes/plugin-a11y#build": {
"outputLogs": "new-only",
- "inputs": ["$TURBO_DEFAULT$", "$TURBO_ROOT$/design/panel-theme.ts", "$TURBO_ROOT$/packages/hub-ui/src/client/primary-ramp.css"],
+ "inputs": ["$TURBO_DEFAULT$", "$TURBO_ROOT$/design/**"],
"dependsOn": ["devframe#build", "@devframes/vite#build", "@devframes/json-render#build"],
"outputs": ["dist/**", "assets-pkg/dist/**"]
},