Skip to content

Commit e0c898d

Browse files
committed
feat(hub-ui): accept custom viewer backgrounds
1 parent 7892b85 commit e0c898d

7 files changed

Lines changed: 55 additions & 24 deletions

File tree

docs/content/1.guide/18.hub-initiate.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,7 @@ interface DevframeHubUi {
6262
`@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`):
6363

6464
- **`viewer`** — set to `false` to disable the standalone viewer.
65-
- **`branding`** — rebrand the UI (logo, name, primary color), or set `background: 'transparent'` when the standalone viewer should reveal the host page behind its iframe.
65+
- **`branding`** — rebrand the UI (logo, name, primary color). `background` accepts any CSS `background` value (color, gradient, image, or `transparent`) or `{ light, dark }` variants; omit it to keep the design default.
6666
- **`dockPreferences`** — dock-rail: `categoryOrder`, floating-dock `maxVisibleItems`, first-run `defaultMode` (`'float'`/`'edge'`) and `defaultPosition`.
6767
- **`embeddedVisibility`** — the floating dock's reveal policy:
6868
- `'normal'` (default) — shows immediately.

packages/hub-ui/src/client/standalone/index.html

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,9 +14,8 @@
1414
color-scheme: dark;
1515
--devframes-viewer-background: #111;
1616
}
17-
html.viewer-background-transparent {
17+
html.viewer-background-custom {
1818
color-scheme: normal;
19-
--devframes-viewer-background: transparent;
2019
}
2120
html,
2221
body {

packages/hub-ui/src/client/standalone/main.ts

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import type { DockSessionStorage } from '@devframes/hub/client'
22
import { getDevframeRpcClient, setDevframeClientContext } from '@devframes/hub/client'
33
import { useSessionStorage } from '@vueuse/core'
44
import { watchEffect } from 'vue'
5-
import { applyDocumentHead, applyPrimaryColor, setBranding } from '../state/branding'
5+
import { applyDocumentHead, applyPrimaryColor, setBranding, useBrandingBackground } from '../state/branding'
66
import { isDark } from '../state/color-mode'
77
import { DEFAULT_DOCK_SESSION_STORE } from '../state/docks'
88

@@ -15,11 +15,21 @@ import { DEFAULT_DOCK_SESSION_STORE } from '../state/docks'
1515
// The standalone viewer runs in the light DOM, so mirror the color mode onto the
1616
// document element — its background follows the Auto/Light/Dark choice. The
1717
// component tree carries `color-scheme` for its native controls; keeping that
18-
// off the document lets the transparent viewer reveal the host page.
18+
// off the document lets custom backgrounds composite with the host page.
19+
const brandingBackground = useBrandingBackground()
20+
1921
watchEffect(() => {
2022
const el = document.documentElement
2123
el.classList.toggle('dark', isDark.value)
2224
el.classList.toggle('light', !isDark.value)
25+
26+
const background = brandingBackground.value
27+
const hasValidBackground = background !== undefined && CSS.supports('background', background)
28+
el.classList.toggle('viewer-background-custom', hasValidBackground)
29+
if (hasValidBackground)
30+
el.style.setProperty('--devframes-viewer-background', background)
31+
else
32+
el.style.removeProperty('--devframes-viewer-background')
2333
})
2434

2535
async function main(): Promise<void> {
@@ -39,7 +49,6 @@ async function main(): Promise<void> {
3949
// established above.
4050
const branding = setBranding(rpc.connectionMeta.configs?.ui?.branding || {})
4151
applyDocumentHead(document, branding)
42-
document.documentElement.classList.toggle('viewer-background-transparent', branding.background === 'transparent')
4352

4453
// Per-tab session UI state (which dock is open + its route). `sessionStorage`
4554
// so a reload restores the selection after the auth handshake, per-tab.

packages/hub-ui/src/client/state/branding.ts

Lines changed: 13 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ export interface ResolvedBranding {
99
logo?: BrandingLogo
1010
wordmark?: BrandingLogo
1111
primaryColor?: string
12-
background: 'default' | 'transparent'
12+
background?: DevframeBranding['background']
1313
tagline?: string
1414
favicon?: string
1515
windowTitle: string
@@ -24,7 +24,7 @@ function resolveDefaults(branding: DevframeBranding): ResolvedBranding {
2424
logo: branding.logo,
2525
wordmark: branding.wordmark,
2626
primaryColor: branding.primaryColor,
27-
background: branding.background || 'default',
27+
background: branding.background,
2828
tagline: branding.tagline,
2929
favicon: branding.favicon,
3030
windowTitle: branding.windowTitle?.trim() || productName,
@@ -46,15 +46,20 @@ export function setBranding(branding: DevframeBranding): ResolvedBranding {
4646

4747
/** The logo/wordmark URL for the current color scheme, reactive to it. */
4848
export function useBrandingLogo(pick: (b: ResolvedBranding) => BrandingLogo | undefined = b => b.logo): Ref<string | undefined> {
49-
return computed(() => resolveLogo(pick(currentBranding.value), isDark.value))
49+
return computed(() => resolveColorSchemeValue(pick(currentBranding.value), isDark.value))
5050
}
5151

52-
function resolveLogo(logo: BrandingLogo | undefined, dark: boolean): string | undefined {
53-
if (!logo)
52+
/** The standalone viewer background for the current color scheme. */
53+
export function useBrandingBackground(): Ref<string | undefined> {
54+
return computed(() => resolveColorSchemeValue(currentBranding.value.background, isDark.value))
55+
}
56+
57+
function resolveColorSchemeValue(value: BrandingLogo | undefined, dark: boolean): string | undefined {
58+
if (!value)
5459
return undefined
55-
if (typeof logo === 'string')
56-
return logo
57-
return dark ? (logo.dark || logo.light) : logo.light
60+
if (typeof value === 'string')
61+
return value
62+
return dark ? (value.dark || value.light) : value.light
5863
}
5964

6065
// --- Applying to the DOM --------------------------------------------------

packages/hub-ui/src/index.test.ts

Lines changed: 22 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import type { DevframeHubContext } from '@devframes/hub/node'
22
import { readFileSync } from 'node:fs'
3-
import { join } from 'node:path'
3+
import { fileURLToPath } from 'node:url'
44
import { describe, expect, it } from 'vitest'
55
import { createUi } from './index'
66

@@ -10,15 +10,16 @@ function createContext(): DevframeHubContext {
1010

1111
describe('createUi branding background', () => {
1212
it('defines the viewer background through a static token', () => {
13-
expect.assertions(5)
13+
expect.assertions(6)
1414

15-
const html = readFileSync(join(import.meta.dirname, '..', 'dist', 'client', 'standalone', 'index.html'), 'utf8')
15+
const html = readFileSync(fileURLToPath(new URL('../dist/client/standalone/index.html', import.meta.url)), 'utf8')
1616

1717
expect(html).not.toContain('__hub-ui.css')
18-
expect(html).toContain('html.viewer-background-transparent')
19-
expect(html).toContain('--devframes-viewer-background: transparent')
18+
expect(html).toContain('html.viewer-background-custom')
19+
expect(html).toContain('--devframes-viewer-background: #fff')
20+
expect(html).toContain('--devframes-viewer-background: #111')
2021
expect(html).toContain('background: var(--devframes-viewer-background)')
21-
expect(html).toMatch(/html\.viewer-background-transparent\s*\{[^}]*color-scheme:\s*normal/)
22+
expect(html).toMatch(/html\.viewer-background-custom\s*\{[^}]*color-scheme:\s*normal/)
2223
})
2324

2425
it('preserves the default viewer background', () => {
@@ -32,7 +33,7 @@ describe('createUi branding background', () => {
3233
expect(ui.assets).toBeUndefined()
3334
})
3435

35-
it('publishes a transparent viewer background with the branding', () => {
36+
it('publishes a CSS viewer background with the branding', () => {
3637
expect.assertions(2)
3738

3839
const context = createContext()
@@ -45,6 +46,20 @@ describe('createUi branding background', () => {
4546
expect(ui.assets).toBeUndefined()
4647
})
4748

49+
it('publishes color-scheme viewer backgrounds with the branding', () => {
50+
expect.assertions(1)
51+
52+
const context = createContext()
53+
const background = {
54+
light: 'linear-gradient(white, transparent)',
55+
dark: 'linear-gradient(#111, transparent)',
56+
}
57+
const ui = createUi({ branding: { background } })
58+
ui.setup?.(context)
59+
60+
expect(context.staticConfig.ui).toEqual({ branding: { background } })
61+
})
62+
4863
it('disables the standalone viewer', () => {
4964
expect.assertions(1)
5065

packages/hub-ui/src/types.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -31,8 +31,8 @@ export interface DevframeBranding {
3131
wordmark?: BrandingLogo
3232
/** Brand color; feeds `--devframe-primary` and the whole primary ramp. */
3333
primaryColor?: string
34-
/** Standalone viewer background. Default: `'default'`. */
35-
background?: 'default' | 'transparent'
34+
/** Standalone viewer CSS `background`; a string applies to both color schemes. */
35+
background?: string | { light: string, dark: string }
3636
/** Short line for the auth screen and the standalone meta description. */
3737
tagline?: string
3838
/** Favicon URL — applied on the standalone viewer and the popped-out window only. */

tests/__snapshots__/tsnapi/@devframes/hub-ui/index.snapshot.d.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,10 @@ export interface DevframeBranding {
1414
logo?: BrandingLogo;
1515
wordmark?: BrandingLogo;
1616
primaryColor?: string;
17-
background?: 'default' | 'transparent';
17+
background?: string | {
18+
light: string;
19+
dark: string;
20+
};
1821
tagline?: string;
1922
favicon?: string;
2023
windowTitle?: string;

0 commit comments

Comments
 (0)