Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion docs/content/1.guide/18.hub-initiate.md
Original file line number Diff line number Diff line change
Expand Up @@ -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; omit it to keep the design default.
- **`branding`** — rebrand the UI (logo, name, primary color). `background` accepts any CSS `background` value (color, gradient, image, or `transparent`) or `{ light, dark }` variants. `embeddedBackground` overrides it when the standalone viewer runs inside an iframe. Omit either value to inherit its fallback.
- **`dockPreferences`** — dock-rail: `categoryOrder`, floating-dock `maxVisibleItems`, first-run `defaultMode` (`'float'`/`'edge'`) and `defaultPosition`.
- **`embeddedVisibility`** — the floating dock's reveal policy:
- `'normal'` (default) — shows immediately.
Expand Down
3 changes: 0 additions & 3 deletions packages/hub-ui/src/client/standalone/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,6 @@
color-scheme: dark;
--devframes-viewer-background: #111;
}
html.viewer-background-custom {
color-scheme: normal;
}
html,
body {
margin: 0;
Expand Down
19 changes: 4 additions & 15 deletions packages/hub-ui/src/client/standalone/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { watchEffect } from 'vue'
import { applyDocumentHead, applyPrimaryColor, setBranding, useBrandingBackground } from '../state/branding'
import { isDark } from '../state/color-mode'
import { DEFAULT_DOCK_SESSION_STORE } from '../state/docks'
import { applyViewerBackground } from './viewer-background'

// The standalone viewer — a vanilla shell served at the hub base itself
// (`DevframeHubUi.viewer`): resolve the shared connection, build the docks
Expand All @@ -13,21 +14,9 @@ import { DEFAULT_DOCK_SESSION_STORE } from '../state/docks'
// custom element's shadow root.

// The standalone viewer runs in the light DOM, so mirror the color mode onto the
// document element — its background follows the Auto/Light/Dark choice. The
// component tree carries `color-scheme` for its native controls; keeping that
// off the document lets custom backgrounds composite with the host page.
const brandingBackground = useBrandingBackground()

function applyViewerBackground(documentElement: HTMLElement, background: string | undefined): void {
if (background === undefined || !CSS.supports('background', background)) {
documentElement.classList.remove('viewer-background-custom')
documentElement.style.removeProperty('--devframes-viewer-background')
return
}

documentElement.classList.add('viewer-background-custom')
documentElement.style.setProperty('--devframes-viewer-background', background)
}
// document element — its background and foreground controls follow the
// Auto/Light/Dark choice, including when branding supplies a custom background.
const brandingBackground = useBrandingBackground(window.self !== window.top)

watchEffect(() => {
const el = document.documentElement
Expand Down
45 changes: 45 additions & 0 deletions packages/hub-ui/src/client/standalone/viewer-background.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import type { ViewerBackgroundElement } from './viewer-background'
import { describe, expect, it, vi } from 'vitest'
import { applyViewerBackground } from './viewer-background'

function createElement(): ViewerBackgroundElement {
return {
classList: {
add: vi.fn(),
remove: vi.fn(),
},
style: {
removeProperty: vi.fn(),
setProperty: vi.fn(),
},
}
}

describe('applyViewerBackground', () => {
it('applies a supported CSS background', () => {
expect.assertions(4)

const element = createElement()
const supports = vi.fn(() => true)

applyViewerBackground(element, 'linear-gradient(white, transparent)', supports)

expect(supports).toHaveBeenCalledWith('background', 'linear-gradient(white, transparent)')
expect(element.classList.add).toHaveBeenCalledWith('viewer-background-custom')
expect(element.style.setProperty).toHaveBeenCalledWith('--devframes-viewer-background', 'linear-gradient(white, transparent)')
expect(element.classList.remove).not.toHaveBeenCalled()
})

it.each([undefined, 'not-a-background'])('restores the default for %s', (background) => {
expect.assertions(3)

const element = createElement()
const supports = vi.fn(() => false)

applyViewerBackground(element, background, supports)

expect(element.classList.remove).toHaveBeenCalledWith('viewer-background-custom')
expect(element.style.removeProperty).toHaveBeenCalledWith('--devframes-viewer-background')
expect(element.style.setProperty).not.toHaveBeenCalled()
})
})
20 changes: 20 additions & 0 deletions packages/hub-ui/src/client/standalone/viewer-background.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
export interface ViewerBackgroundElement {
classList: Pick<DOMTokenList, 'add' | 'remove'>
style: Pick<CSSStyleDeclaration, 'removeProperty' | 'setProperty'>
}

/** Apply a validated branding background to the standalone viewer document. */
export function applyViewerBackground(
documentElement: ViewerBackgroundElement,
background: string | undefined,
supports = (property: string, value: string): boolean => CSS.supports(property, value),
): void {
if (background === undefined || !supports('background', background)) {
documentElement.classList.remove('viewer-background-custom')
documentElement.style.removeProperty('--devframes-viewer-background')
return
}

documentElement.classList.add('viewer-background-custom')
documentElement.style.setProperty('--devframes-viewer-background', background)
Comment on lines +12 to +19
}
27 changes: 26 additions & 1 deletion packages/hub-ui/src/client/state/branding.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,37 @@ afterEach(() => {
})

describe('useBrandingBackground', () => {
it.each([
{ embedded: false, preference: 'light' as const, expected: 'standalone-light' },
{ embedded: false, preference: 'dark' as const, expected: 'standalone-dark' },
{ embedded: true, preference: 'light' as const, expected: 'embedded-light' },
{ embedded: true, preference: 'dark' as const, expected: 'embedded-dark' },
])('resolves the $preference background when embedded is $embedded', ({ embedded, preference, expected }) => {
expect.assertions(1)

setColorSchemePreference(preference)
setBranding({
background: { light: 'standalone-light', dark: 'standalone-dark' },
embeddedBackground: { light: 'embedded-light', dark: 'embedded-dark' },
})

expect(useBrandingBackground(embedded).value).toBe(expected)
})

it('falls back to the standalone background when no embedded value is configured', () => {
expect.assertions(1)

setBranding({ background: 'shared' })

expect(useBrandingBackground(true).value).toBe('shared')
})

it('preserves an empty dark value for CSS validation', () => {
expect.assertions(1)

setColorSchemePreference('dark')
setBranding({ background: { light: 'white', dark: '' } })

expect(useBrandingBackground().value).toBe('')
expect(useBrandingBackground(false).value).toBe('')
})
})
13 changes: 10 additions & 3 deletions packages/hub-ui/src/client/state/branding.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ export interface ResolvedBranding {
wordmark?: BrandingLogo
primaryColor?: string
background?: DevframeBranding['background']
embeddedBackground?: DevframeBranding['embeddedBackground']
tagline?: string
favicon?: string
windowTitle: string
Expand All @@ -27,6 +28,7 @@ function resolveDefaults(branding: DevframeBranding): ResolvedBranding {
wordmark: branding.wordmark,
primaryColor: branding.primaryColor,
background: branding.background,
embeddedBackground: branding.embeddedBackground,
tagline: branding.tagline,
favicon: branding.favicon,
windowTitle: branding.windowTitle?.trim() || productName,
Expand All @@ -51,9 +53,14 @@ export function useBrandingLogo(pick: (b: ResolvedBranding) => BrandingLogo | un
return computed(() => resolveColorSchemeValue(pick(currentBranding.value), isDark.value))
}

/** The standalone viewer background for the current color scheme. */
export function useBrandingBackground(): Ref<string | undefined> {
return computed(() => resolveColorSchemeValue(currentBranding.value.background, isDark.value))
/** The standalone viewer background for its frame context and current color scheme. */
export function useBrandingBackground(embedded: boolean): Ref<string | undefined> {
return computed(() => resolveColorSchemeValue(
embedded
? (currentBranding.value.embeddedBackground ?? currentBranding.value.background)
: currentBranding.value.background,
isDark.value,
))
}

function resolveColorSchemeValue(value: ColorSchemeValue | undefined, dark: boolean): string | undefined {
Expand Down
17 changes: 15 additions & 2 deletions packages/hub-ui/src/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,11 +15,11 @@ describe('createUi branding background', () => {
const html = readFileSync(fileURLToPath(new URL('../dist/client/standalone/index.html', import.meta.url)), 'utf8')

expect(html).not.toContain('__hub-ui.css')
expect(html).toContain('html.viewer-background-custom')
expect(html).toContain('color-scheme: light')
expect(html).toContain('--devframes-viewer-background: #fff')
expect(html).toContain('--devframes-viewer-background: #111')
expect(html).toContain('background: var(--devframes-viewer-background)')
expect(html).toMatch(/html\.viewer-background-custom\s*\{[^}]*color-scheme:\s*normal/)
expect(html).not.toMatch(/html\.viewer-background-custom\s*\{[^}]*color-scheme:\s*normal/)
})

it('preserves the default viewer background', () => {
Expand Down Expand Up @@ -60,6 +60,19 @@ describe('createUi branding background', () => {
expect(context.staticConfig.ui).toEqual({ branding: { background } })
})

it('publishes color-scheme embedded viewer backgrounds with the branding', () => {
expect.assertions(1)

const embeddedBackground = {
light: 'rgba(255, 255, 255, 0.5)',
dark: 'rgba(17, 17, 17, 0.5)',
}
const ui = createUi({ branding: { embeddedBackground } })
const context = createContext()
ui.setup?.(context)
expect(context.staticConfig.ui).toEqual({ branding: { embeddedBackground } })
})

it('disables the standalone viewer', () => {
expect.assertions(1)

Expand Down
2 changes: 2 additions & 0 deletions packages/hub-ui/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,8 @@ export interface DevframeBranding {
primaryColor?: string
/** Standalone viewer CSS `background`; a string applies to both color schemes. */
background?: string | { light: string, dark: string }
/** Standalone viewer CSS `background` when the viewer runs inside an iframe. */
embeddedBackground?: string | { light: string, dark: string }
/** Short line for the auth screen and the standalone meta description. */
tagline?: string
/** Favicon URL — applied on the standalone viewer and the popped-out window only. */
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,10 @@ export interface DevframeBranding {
light: string;
dark: string;
};
embeddedBackground?: string | {
light: string;
dark: string;
};
tagline?: string;
favicon?: string;
windowTitle?: string;
Expand Down
Loading