Skip to content

Commit dca6735

Browse files
committed
feat(hub-ui): support embedded viewer backgrounds
1 parent a55f3d5 commit dca6735

10 files changed

Lines changed: 127 additions & 25 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
@@ -53,7 +53,7 @@ interface DevframeHubUi {
5353
`@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`):
5454

5555
- **`viewer`** — set to `false` to disable the standalone viewer.
56-
- **`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.
56+
- **`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.
5757
- **`dockPreferences`** — dock-rail: `categoryOrder`, floating-dock `maxVisibleItems`, first-run `defaultMode` (`'float'`/`'edge'`) and `defaultPosition`.
5858
- **`embeddedVisibility`** — the floating dock's reveal policy:
5959
- `'normal'` (default) — shows immediately.

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

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -14,9 +14,6 @@
1414
color-scheme: dark;
1515
--devframes-viewer-background: #111;
1616
}
17-
html.viewer-background-custom {
18-
color-scheme: normal;
19-
}
2017
html,
2118
body {
2219
margin: 0;

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

Lines changed: 4 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import { watchEffect } from 'vue'
55
import { applyDocumentHead, applyPrimaryColor, setBranding, useBrandingBackground } from '../state/branding'
66
import { isDark } from '../state/color-mode'
77
import { DEFAULT_DOCK_SESSION_STORE } from '../state/docks'
8+
import { applyViewerBackground } from './viewer-background'
89

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

1516
// The standalone viewer runs in the light DOM, so mirror the color mode onto the
16-
// document element — its background follows the Auto/Light/Dark choice. The
17-
// component tree carries `color-scheme` for its native controls; keeping that
18-
// off the document lets custom backgrounds composite with the host page.
19-
const brandingBackground = useBrandingBackground()
20-
21-
function applyViewerBackground(documentElement: HTMLElement, background: string | undefined): void {
22-
if (background === undefined || !CSS.supports('background', background)) {
23-
documentElement.classList.remove('viewer-background-custom')
24-
documentElement.style.removeProperty('--devframes-viewer-background')
25-
return
26-
}
27-
28-
documentElement.classList.add('viewer-background-custom')
29-
documentElement.style.setProperty('--devframes-viewer-background', background)
30-
}
17+
// document element — its background and foreground controls follow the
18+
// Auto/Light/Dark choice, including when branding supplies a custom background.
19+
const brandingBackground = useBrandingBackground(window.self !== window.top)
3120

3221
watchEffect(() => {
3322
const el = document.documentElement
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
import type { ViewerBackgroundElement } from './viewer-background'
2+
import { describe, expect, it, vi } from 'vitest'
3+
import { applyViewerBackground } from './viewer-background'
4+
5+
function createElement(): ViewerBackgroundElement {
6+
return {
7+
classList: {
8+
add: vi.fn(),
9+
remove: vi.fn(),
10+
},
11+
style: {
12+
removeProperty: vi.fn(),
13+
setProperty: vi.fn(),
14+
},
15+
}
16+
}
17+
18+
describe('applyViewerBackground', () => {
19+
it('applies a supported CSS background', () => {
20+
expect.assertions(4)
21+
22+
const element = createElement()
23+
const supports = vi.fn(() => true)
24+
25+
applyViewerBackground(element, 'linear-gradient(white, transparent)', supports)
26+
27+
expect(supports).toHaveBeenCalledWith('background', 'linear-gradient(white, transparent)')
28+
expect(element.classList.add).toHaveBeenCalledWith('viewer-background-custom')
29+
expect(element.style.setProperty).toHaveBeenCalledWith('--devframes-viewer-background', 'linear-gradient(white, transparent)')
30+
expect(element.classList.remove).not.toHaveBeenCalled()
31+
})
32+
33+
it.each([undefined, 'not-a-background'])('restores the default for %s', (background) => {
34+
expect.assertions(3)
35+
36+
const element = createElement()
37+
const supports = vi.fn(() => false)
38+
39+
applyViewerBackground(element, background, supports)
40+
41+
expect(element.classList.remove).toHaveBeenCalledWith('viewer-background-custom')
42+
expect(element.style.removeProperty).toHaveBeenCalledWith('--devframes-viewer-background')
43+
expect(element.style.setProperty).not.toHaveBeenCalled()
44+
})
45+
})
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
export interface ViewerBackgroundElement {
2+
classList: Pick<DOMTokenList, 'add' | 'remove'>
3+
style: Pick<CSSStyleDeclaration, 'removeProperty' | 'setProperty'>
4+
}
5+
6+
/** Apply a validated branding background to the standalone viewer document. */
7+
export function applyViewerBackground(
8+
documentElement: ViewerBackgroundElement,
9+
background: string | undefined,
10+
supports = (property: string, value: string): boolean => CSS.supports(property, value),
11+
): void {
12+
if (background === undefined || !supports('background', background)) {
13+
documentElement.classList.remove('viewer-background-custom')
14+
documentElement.style.removeProperty('--devframes-viewer-background')
15+
return
16+
}
17+
18+
documentElement.classList.add('viewer-background-custom')
19+
documentElement.style.setProperty('--devframes-viewer-background', background)
20+
}

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

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,12 +8,37 @@ afterEach(() => {
88
})
99

1010
describe('useBrandingBackground', () => {
11+
it.each([
12+
{ embedded: false, preference: 'light' as const, expected: 'standalone-light' },
13+
{ embedded: false, preference: 'dark' as const, expected: 'standalone-dark' },
14+
{ embedded: true, preference: 'light' as const, expected: 'embedded-light' },
15+
{ embedded: true, preference: 'dark' as const, expected: 'embedded-dark' },
16+
])('resolves the $preference background when embedded is $embedded', ({ embedded, preference, expected }) => {
17+
expect.assertions(1)
18+
19+
setColorSchemePreference(preference)
20+
setBranding({
21+
background: { light: 'standalone-light', dark: 'standalone-dark' },
22+
embeddedBackground: { light: 'embedded-light', dark: 'embedded-dark' },
23+
})
24+
25+
expect(useBrandingBackground(embedded).value).toBe(expected)
26+
})
27+
28+
it('falls back to the standalone background when no embedded value is configured', () => {
29+
expect.assertions(1)
30+
31+
setBranding({ background: 'shared' })
32+
33+
expect(useBrandingBackground(true).value).toBe('shared')
34+
})
35+
1136
it('preserves an empty dark value for CSS validation', () => {
1237
expect.assertions(1)
1338

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

17-
expect(useBrandingBackground().value).toBe('')
42+
expect(useBrandingBackground(false).value).toBe('')
1843
})
1944
})

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

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ export interface ResolvedBranding {
1212
wordmark?: BrandingLogo
1313
primaryColor?: string
1414
background?: DevframeBranding['background']
15+
embeddedBackground?: DevframeBranding['embeddedBackground']
1516
tagline?: string
1617
favicon?: string
1718
windowTitle: string
@@ -27,6 +28,7 @@ function resolveDefaults(branding: DevframeBranding): ResolvedBranding {
2728
wordmark: branding.wordmark,
2829
primaryColor: branding.primaryColor,
2930
background: branding.background,
31+
embeddedBackground: branding.embeddedBackground,
3032
tagline: branding.tagline,
3133
favicon: branding.favicon,
3234
windowTitle: branding.windowTitle?.trim() || productName,
@@ -51,9 +53,14 @@ export function useBrandingLogo(pick: (b: ResolvedBranding) => BrandingLogo | un
5153
return computed(() => resolveColorSchemeValue(pick(currentBranding.value), isDark.value))
5254
}
5355

54-
/** The standalone viewer background for the current color scheme. */
55-
export function useBrandingBackground(): Ref<string | undefined> {
56-
return computed(() => resolveColorSchemeValue(currentBranding.value.background, isDark.value))
56+
/** The standalone viewer background for its frame context and current color scheme. */
57+
export function useBrandingBackground(embedded: boolean): Ref<string | undefined> {
58+
return computed(() => resolveColorSchemeValue(
59+
embedded
60+
? (currentBranding.value.embeddedBackground ?? currentBranding.value.background)
61+
: currentBranding.value.background,
62+
isDark.value,
63+
))
5764
}
5865

5966
function resolveColorSchemeValue(value: ColorSchemeValue | undefined, dark: boolean): string | undefined {

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

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,11 +15,11 @@ describe('createUi branding background', () => {
1515
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-custom')
18+
expect(html).toContain('color-scheme: light')
1919
expect(html).toContain('--devframes-viewer-background: #fff')
2020
expect(html).toContain('--devframes-viewer-background: #111')
2121
expect(html).toContain('background: var(--devframes-viewer-background)')
22-
expect(html).toMatch(/html\.viewer-background-custom\s*\{[^}]*color-scheme:\s*normal/)
22+
expect(html).not.toMatch(/html\.viewer-background-custom\s*\{[^}]*color-scheme:\s*normal/)
2323
})
2424

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

63+
it('publishes color-scheme embedded viewer backgrounds with the branding', () => {
64+
expect.assertions(1)
65+
66+
const embeddedBackground = {
67+
light: 'rgba(255, 255, 255, 0.5)',
68+
dark: 'rgba(17, 17, 17, 0.5)',
69+
}
70+
const ui = createUi({ branding: { embeddedBackground } })
71+
const context = createContext()
72+
ui.setup?.(context)
73+
expect(context.staticConfig.ui).toEqual({ branding: { embeddedBackground } })
74+
})
75+
6376
it('disables the standalone viewer', () => {
6477
expect.assertions(1)
6578

packages/hub-ui/src/types.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,8 @@ export interface DevframeBranding {
3333
primaryColor?: string
3434
/** Standalone viewer CSS `background`; a string applies to both color schemes. */
3535
background?: string | { light: string, dark: string }
36+
/** Standalone viewer CSS `background` when the viewer runs inside an iframe. */
37+
embeddedBackground?: string | { light: string, dark: string }
3638
/** Short line for the auth screen and the standalone meta description. */
3739
tagline?: string
3840
/** 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 & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,10 @@ export interface DevframeBranding {
1818
light: string;
1919
dark: string;
2020
};
21+
embeddedBackground?: string | {
22+
light: string;
23+
dark: string;
24+
};
2125
tagline?: string;
2226
favicon?: string;
2327
windowTitle?: string;

0 commit comments

Comments
 (0)