Skip to content
Merged
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
3 changes: 2 additions & 1 deletion docs/content/1.guide/18.hub-initiate.md
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,8 @@ 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`):

- **`branding`** — rebrand the UI (logo, name, primary color).
- **`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.
- **`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
17 changes: 12 additions & 5 deletions packages/hub-ui/src/client/standalone/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -6,15 +6,22 @@
<title>Devframes</title>
<meta name="description" content="Devframes hub" />
<style>
:root {
color-scheme: light;
--devframes-viewer-background: #fff;
}
html.dark {
color-scheme: dark;
--devframes-viewer-background: #111;
}
html.viewer-background-custom {
color-scheme: normal;
}
html,
body {
margin: 0;
height: 100%;
background: #fff;
}
html.dark,
html.dark body {
background: #111;
background: var(--devframes-viewer-background);
}
#app {
height: 100%;
Expand Down
24 changes: 19 additions & 5 deletions packages/hub-ui/src/client/standalone/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import type { DockSessionStorage } from '@devframes/hub/client'
import { getDevframeRpcClient, setDevframeClientContext } from '@devframes/hub/client'
import { useSessionStorage } from '@vueuse/core'
import { watchEffect } from 'vue'
import { applyDocumentHead, applyPrimaryColor, setBranding } from '../state/branding'
import { applyDocumentHead, applyPrimaryColor, setBranding, useBrandingBackground } from '../state/branding'
import { isDark } from '../state/color-mode'
import { DEFAULT_DOCK_SESSION_STORE } from '../state/docks'

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

// The standalone page runs in the light DOM, so mirror the color mode onto the
// document element — its background and native controls follow the
// Auto/Light/Dark choice.
// 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)
}

watchEffect(() => {
const el = document.documentElement
el.classList.toggle('dark', isDark.value)
el.classList.toggle('light', !isDark.value)
el.style.colorScheme = isDark.value ? 'dark' : 'light'
applyViewerBackground(el, brandingBackground.value)
})

async function main(): Promise<void> {
Expand Down
19 changes: 19 additions & 0 deletions packages/hub-ui/src/client/state/branding.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import { afterEach, describe, expect, it } from 'vitest'
import { setBranding, useBrandingBackground } from './branding'
import { setColorSchemePreference } from './color-mode'

afterEach(() => {
setBranding({})
setColorSchemePreference('auto')
})

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

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

expect(useBrandingBackground().value).toBe('')
})
})
21 changes: 15 additions & 6 deletions packages/hub-ui/src/client/state/branding.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,15 @@ import type { BrandingLogo, DevframeBranding } from '../../types'
import { computed, ref } from 'vue'
import { isDark } from './color-mode'

type ColorSchemeValue = string | { light: string, dark: string }

/** Branding with defaults resolved — what the UI actually renders. */
export interface ResolvedBranding {
productName: string
logo?: BrandingLogo
wordmark?: BrandingLogo
primaryColor?: string
background?: DevframeBranding['background']
tagline?: string
favicon?: string
windowTitle: string
Expand All @@ -23,6 +26,7 @@ function resolveDefaults(branding: DevframeBranding): ResolvedBranding {
logo: branding.logo,
wordmark: branding.wordmark,
primaryColor: branding.primaryColor,
background: branding.background,
tagline: branding.tagline,
favicon: branding.favicon,
windowTitle: branding.windowTitle?.trim() || productName,
Expand All @@ -44,15 +48,20 @@ export function setBranding(branding: DevframeBranding): ResolvedBranding {

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

function resolveLogo(logo: BrandingLogo | undefined, dark: boolean): string | undefined {
if (!logo)
function resolveColorSchemeValue(value: ColorSchemeValue | undefined, dark: boolean): string | undefined {
if (!value)
return undefined
if (typeof logo === 'string')
return logo
return dark ? (logo.dark || logo.light) : logo.light
if (typeof value === 'string')
return value
return dark ? (value.dark ?? value.light) : value.light
}

// --- Applying to the DOM --------------------------------------------------
Expand Down
70 changes: 70 additions & 0 deletions packages/hub-ui/src/index.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
import type { DevframeHubContext } from '@devframes/hub/node'
import { readFileSync } from 'node:fs'
import { fileURLToPath } from 'node:url'
import { describe, expect, it } from 'vitest'
import { createUi } from './index'

function createContext(): DevframeHubContext {
return { staticConfig: {} } as unknown as DevframeHubContext
}

describe('createUi branding background', () => {
it('defines the viewer background through a static token', () => {
expect.assertions(6)

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('--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/)
})

it('preserves the default viewer background', () => {
expect.assertions(2)

const context = createContext()
const ui = createUi()
ui.setup?.(context)

expect(context.staticConfig.ui).toEqual({ branding: {} })
expect(ui.assets).toBeUndefined()
})

it('publishes a CSS viewer background with the branding', () => {
expect.assertions(2)

const context = createContext()
const ui = createUi({ branding: { background: 'transparent' } })
ui.setup?.(context)

expect(context.staticConfig.ui).toEqual({
branding: { background: 'transparent' },
})
expect(ui.assets).toBeUndefined()
})

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

const context = createContext()
const background = {
light: 'linear-gradient(white, transparent)',
dark: 'linear-gradient(#111, transparent)',
}
const ui = createUi({ branding: { background } })
ui.setup?.(context)

expect(context.staticConfig.ui).toEqual({ branding: { background } })
})

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

const ui = createUi({ viewer: false })

expect(ui.viewer).toBeUndefined()
})
})
2 changes: 2 additions & 0 deletions packages/hub-ui/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,8 @@ export interface DevframeBranding {
wordmark?: BrandingLogo
/** Brand color; feeds `--devframe-primary` and the whole primary ramp. */
primaryColor?: string
/** Standalone viewer CSS `background`; a string applies to both color schemes. */
background?: 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 @@ -14,6 +14,10 @@ export interface DevframeBranding {
logo?: BrandingLogo;
wordmark?: BrandingLogo;
primaryColor?: string;
background?: string | {
light: string;
dark: string;
};
tagline?: string;
favicon?: string;
windowTitle?: string;
Expand Down
Loading