Skip to content

Commit f1bd9b8

Browse files
authored
feat(hub-ui): support transparent standalone viewers (#300)
1 parent e018de5 commit f1bd9b8

8 files changed

Lines changed: 143 additions & 17 deletions

File tree

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

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,8 @@ interface DevframeHubUi {
5252

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

55-
- **`branding`** — rebrand the UI (logo, name, primary color).
55+
- **`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.
5657
- **`dockPreferences`** — dock-rail: `categoryOrder`, floating-dock `maxVisibleItems`, first-run `defaultMode` (`'float'`/`'edge'`) and `defaultPosition`.
5758
- **`embeddedVisibility`** — the floating dock's reveal policy:
5859
- `'normal'` (default) — shows immediately.

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

Lines changed: 12 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -6,15 +6,22 @@
66
<title>Devframes</title>
77
<meta name="description" content="Devframes hub" />
88
<style>
9+
:root {
10+
color-scheme: light;
11+
--devframes-viewer-background: #fff;
12+
}
13+
html.dark {
14+
color-scheme: dark;
15+
--devframes-viewer-background: #111;
16+
}
17+
html.viewer-background-custom {
18+
color-scheme: normal;
19+
}
920
html,
1021
body {
1122
margin: 0;
1223
height: 100%;
13-
background: #fff;
14-
}
15-
html.dark,
16-
html.dark body {
17-
background: #111;
24+
background: var(--devframes-viewer-background);
1825
}
1926
#app {
2027
height: 100%;

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

Lines changed: 19 additions & 5 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

@@ -12,14 +12,28 @@ import { DEFAULT_DOCK_SESSION_STORE } from '../state/docks'
1212
// shell stays frameworkless on purpose; everything visual lives inside the
1313
// custom element's shadow root.
1414

15-
// The standalone page runs in the light DOM, so mirror the color mode onto the
16-
// document element — its background and native controls follow the
17-
// Auto/Light/Dark choice.
15+
// 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+
}
31+
1832
watchEffect(() => {
1933
const el = document.documentElement
2034
el.classList.toggle('dark', isDark.value)
2135
el.classList.toggle('light', !isDark.value)
22-
el.style.colorScheme = isDark.value ? 'dark' : 'light'
36+
applyViewerBackground(el, brandingBackground.value)
2337
})
2438

2539
async function main(): Promise<void> {
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
import { afterEach, describe, expect, it } from 'vitest'
2+
import { setBranding, useBrandingBackground } from './branding'
3+
import { setColorSchemePreference } from './color-mode'
4+
5+
afterEach(() => {
6+
setBranding({})
7+
setColorSchemePreference('auto')
8+
})
9+
10+
describe('useBrandingBackground', () => {
11+
it('preserves an empty dark value for CSS validation', () => {
12+
expect.assertions(1)
13+
14+
setColorSchemePreference('dark')
15+
setBranding({ background: { light: 'white', dark: '' } })
16+
17+
expect(useBrandingBackground().value).toBe('')
18+
})
19+
})

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

Lines changed: 15 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,15 @@ import type { BrandingLogo, DevframeBranding } from '../../types'
33
import { computed, ref } from 'vue'
44
import { isDark } from './color-mode'
55

6+
type ColorSchemeValue = string | { light: string, dark: string }
7+
68
/** Branding with defaults resolved — what the UI actually renders. */
79
export interface ResolvedBranding {
810
productName: string
911
logo?: BrandingLogo
1012
wordmark?: BrandingLogo
1113
primaryColor?: string
14+
background?: DevframeBranding['background']
1215
tagline?: string
1316
favicon?: string
1417
windowTitle: string
@@ -23,6 +26,7 @@ function resolveDefaults(branding: DevframeBranding): ResolvedBranding {
2326
logo: branding.logo,
2427
wordmark: branding.wordmark,
2528
primaryColor: branding.primaryColor,
29+
background: branding.background,
2630
tagline: branding.tagline,
2731
favicon: branding.favicon,
2832
windowTitle: branding.windowTitle?.trim() || productName,
@@ -44,15 +48,20 @@ export function setBranding(branding: DevframeBranding): ResolvedBranding {
4448

4549
/** The logo/wordmark URL for the current color scheme, reactive to it. */
4650
export function useBrandingLogo(pick: (b: ResolvedBranding) => BrandingLogo | undefined = b => b.logo): Ref<string | undefined> {
47-
return computed(() => resolveLogo(pick(currentBranding.value), isDark.value))
51+
return computed(() => resolveColorSchemeValue(pick(currentBranding.value), isDark.value))
52+
}
53+
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))
4857
}
4958

50-
function resolveLogo(logo: BrandingLogo | undefined, dark: boolean): string | undefined {
51-
if (!logo)
59+
function resolveColorSchemeValue(value: ColorSchemeValue | undefined, dark: boolean): string | undefined {
60+
if (!value)
5261
return undefined
53-
if (typeof logo === 'string')
54-
return logo
55-
return dark ? (logo.dark || logo.light) : logo.light
62+
if (typeof value === 'string')
63+
return value
64+
return dark ? (value.dark ?? value.light) : value.light
5665
}
5766

5867
// --- Applying to the DOM --------------------------------------------------

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

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
import type { DevframeHubContext } from '@devframes/hub/node'
2+
import { readFileSync } from 'node:fs'
3+
import { fileURLToPath } from 'node:url'
4+
import { describe, expect, it } from 'vitest'
5+
import { createUi } from './index'
6+
7+
function createContext(): DevframeHubContext {
8+
return { staticConfig: {} } as unknown as DevframeHubContext
9+
}
10+
11+
describe('createUi branding background', () => {
12+
it('defines the viewer background through a static token', () => {
13+
expect.assertions(6)
14+
15+
const html = readFileSync(fileURLToPath(new URL('../dist/client/standalone/index.html', import.meta.url)), 'utf8')
16+
17+
expect(html).not.toContain('__hub-ui.css')
18+
expect(html).toContain('html.viewer-background-custom')
19+
expect(html).toContain('--devframes-viewer-background: #fff')
20+
expect(html).toContain('--devframes-viewer-background: #111')
21+
expect(html).toContain('background: var(--devframes-viewer-background)')
22+
expect(html).toMatch(/html\.viewer-background-custom\s*\{[^}]*color-scheme:\s*normal/)
23+
})
24+
25+
it('preserves the default viewer background', () => {
26+
expect.assertions(2)
27+
28+
const context = createContext()
29+
const ui = createUi()
30+
ui.setup?.(context)
31+
32+
expect(context.staticConfig.ui).toEqual({ branding: {} })
33+
expect(ui.assets).toBeUndefined()
34+
})
35+
36+
it('publishes a CSS viewer background with the branding', () => {
37+
expect.assertions(2)
38+
39+
const context = createContext()
40+
const ui = createUi({ branding: { background: 'transparent' } })
41+
ui.setup?.(context)
42+
43+
expect(context.staticConfig.ui).toEqual({
44+
branding: { background: 'transparent' },
45+
})
46+
expect(ui.assets).toBeUndefined()
47+
})
48+
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+
63+
it('disables the standalone viewer', () => {
64+
expect.assertions(1)
65+
66+
const ui = createUi({ viewer: false })
67+
68+
expect(ui.viewer).toBeUndefined()
69+
})
70+
})

‎packages/hub-ui/src/types.ts‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +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 CSS `background`; a string applies to both color schemes. */
35+
background?: string | { light: string, dark: string }
3436
/** Short line for the auth screen and the standalone meta description. */
3537
tagline?: string
3638
/** 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
@@ -14,6 +14,10 @@ export interface DevframeBranding {
1414
logo?: BrandingLogo;
1515
wordmark?: BrandingLogo;
1616
primaryColor?: string;
17+
background?: string | {
18+
light: string;
19+
dark: string;
20+
};
1721
tagline?: string;
1822
favicon?: string;
1923
windowTitle?: string;

0 commit comments

Comments
 (0)