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: 2 additions & 0 deletions packages/hub-ui/.storybook/preview.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ import type { Decorator, Preview } from '@storybook/vue3-vite'
import 'virtual:uno.css'
import '@antfu/design/styles.css'
import '../src/client/style.css'
// After uno so the primary-ramp override wins, matching the shadow-root build.
import '../src/client/primary-ramp.css'

// Drive the shared `@antfu/design` tokens off the toolbar theme toggle: dark mode
// is the `.dark` class on `<html>`, and the canvas takes the semantic
Expand Down
7 changes: 7 additions & 0 deletions packages/hub-ui/scripts/build-css.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,11 @@ const GLOBS = ['components/**/*.{ts,vue}', 'state/**/*.ts', 'embedded/**/*.ts',
// Story-only utility classes must not leak into the shipped stylesheet.
const IGNORE = ['**/*.stories.*', '**/__tests__/**']
const USER_STYLE = join(SRC_DIR, 'style.css')
// The single-overridable-variable primary ramp. Appended AFTER the UnoCSS
// output so its `:host` block wins over Wind4's own `:root, :host` primary
// declarations (kept in its own file so the Storybook preview can import the
// exact same override after `virtual:uno.css`). See the file's own comment.
const PRIMARY_RAMP = join(SRC_DIR, 'primary-ramp.css')
const GENERATED_CSS = join(SRC_DIR, '.generated/css.ts')

export async function buildCSS(): Promise<void> {
Expand Down Expand Up @@ -55,11 +60,13 @@ export async function buildCSS(): Promise<void> {
await transformer.transform(userStyle, USER_STYLE, { uno: generator } as any)
}

const primaryRamp = await fs.readFile(PRIMARY_RAMP, 'utf-8')
const unoResult = await generator.generate(tokens)
const css = [
reset,
userStyle.toString(),
unoResult.css,
primaryRamp,
].join('\n')

await fs.mkdir(join(SRC_DIR, '.generated'), { recursive: true })
Expand Down
2 changes: 1 addition & 1 deletion packages/hub-ui/src/client/.generated/css.ts

Large diffs are not rendered by default.

11 changes: 2 additions & 9 deletions packages/hub-ui/src/client/components/dock/DockEntry.vue
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,8 @@
import type { DevframeDockEntryBase } from '@devframes/hub'
import type { DocksContext } from '@devframes/hub/client'
import { useEventListener } from '@vueuse/core'
import { computed, useTemplateRef } from 'vue'
import { useTemplateRef } from 'vue'
import { setFloatingTooltip } from '../../state/floating-tooltip'
import { accentTextStyle } from '../../utils/accent-color'
import { openDockContextMenu } from './DockContextMenu'
import DockIcon from './DockIcon.vue'

Expand All @@ -18,17 +17,12 @@ const props = withDefaults(
isVertical?: boolean
badge?: string
tooltip?: boolean
/** A `group` entry's optional `accentColor`, applied while selected/active. */
accentColor?: string
}>(),
{
tooltip: true,
},
)

// Only kicks in while selected — an idle group button keeps its default look.
const accentStyle = computed(() => (props.isSelected ? accentTextStyle(props.accentColor) : undefined))

const button = useTemplateRef<HTMLButtonElement>('button')

function updateTooltip() {
Expand Down Expand Up @@ -84,11 +78,10 @@ useEventListener('pointerdown', () => {
<button
ref="button"
:aria-label="dock.title"
:style="accentStyle"
:class="[
isVertical ? 'rotate-270' : '',
isDimmed ? 'op50 saturate-0' : '',
isSelected ? (accentColor ? 'scale-120' : 'scale-120 text-primary') : '',
isSelected ? 'scale-120 text-primary' : '',
isAction ? 'bg-[#8881] hover:bg-[#8882] rounded-full' : 'rounded-xl',
]"
class="flex items-center justify-center p1.5 hover:bg-[#8881] hover:scale-110 transition-all duration-300 relative outline-none"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { computed, h, ref, useTemplateRef } from 'vue'
import { getGroupMembers, getGroupMembersGrouped, resolveGroupDefaultChild } from '../../state/dock-settings'
import { sharedStateToRef } from '../../state/docks'
import { setDocksGroupPanel, useDocksGroupPanel } from '../../state/floating-tooltip'
import { accentVarStyle } from '../../utils/accent-color'
import DockEntry from './DockEntry.vue'
import DockGroupPopover from './DockGroupPopover.vue'

Expand Down Expand Up @@ -119,15 +120,14 @@ function onClick() {
</script>

<template>
<div ref="groupButton">
<div ref="groupButton" :class="group.accentColor ? 'devframes-accent-scope' : ''" :style="accentVarStyle(group.accentColor)">
<DockEntry
:context="context"
:dock="group"
:is-vertical="isVertical"
:is-selected="isActive || isPanelVisible"
:is-dimmed="dimInactive && selected ? !isActive : false"
:badge="group.badge"
:accent-color="group.accentColor"
@click="onClick"
/>
</div>
Expand Down
26 changes: 15 additions & 11 deletions packages/hub-ui/src/client/components/dock/DockGroupPopover.vue
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
import type { DevframeDockEntriesGrouped, DevframeDockEntry, DevframeViewGroup } from '@devframes/hub'
import type { DocksContext } from '@devframes/hub/client'
import { computed } from 'vue'
import { accentActiveStyle, accentTextStyle } from '../../utils/accent-color'
import { accentVarStyle } from '../../utils/accent-color'
import DockIcon from './DockIcon.vue'

const props = defineProps<{
Expand All @@ -18,17 +18,22 @@ const emit = defineEmits<{
}>()

const isEmpty = computed(() => props.members.every(([, items]) => items.length === 0))

const headerStyle = computed(() => accentTextStyle(props.group.accentColor))

function memberStyle(memberId: string) {
return props.selectedId === memberId ? accentActiveStyle(props.group.accentColor) : undefined
}
</script>

<template>
<div class="flex flex-col gap-0.5 min-w-44 max-w-64" @mousemove.stop>
<div class="flex items-center gap-1.5 px4 mx--2 pt2 pb2.5 font-bold text-2.75 uppercase tracking-wide border-b border-base" :style="headerStyle">
<!-- The group's accentColor overrides --devframe-primary for the whole
popover, so the header icon and the selected row's text-primary/bg-active
render in the accent. -->
<div
class="flex flex-col gap-0.5 min-w-44 max-w-64"
:class="group.accentColor ? 'devframes-accent-scope' : ''"
:style="accentVarStyle(group.accentColor)"
@mousemove.stop
>
<div
class="flex items-center gap-1.5 px4 mx--2 pt2 pb2.5 font-bold text-2.75 uppercase tracking-wide border-b border-base"
:class="group.accentColor ? 'color-active' : ''"
>
<DockIcon :icon="group.icon" class="w-4.5 h-4.5" />
<span class="truncate">{{ group.title }}</span>
</div>
Expand All @@ -39,8 +44,7 @@ function memberStyle(memberId: string) {
v-for="member of items"
:key="member.id"
class="flex items-center gap-2 w-full px2 py1.5 rounded text-sm text-left transition"
:class="selectedId === member.id ? (group.accentColor ? '' : 'text-primary bg-active') : 'op80 hover:op100 hover:bg-active'"
:style="memberStyle(member.id)"
:class="selectedId === member.id ? 'text-primary bg-active' : 'op80 hover:op100 hover:bg-active'"
@click="emit('select', member)"
>
<DockIcon :icon="member.icon" class="w-4.5 h-4.5 flex-none" />
Expand Down
27 changes: 12 additions & 15 deletions packages/hub-ui/src/client/components/dock/DockGroupSidebar.vue
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import { computed, h, ref, useTemplateRef } from 'vue'
import { deriveSidebarCapacity, docksSplitGroupsWithCapacity, getGroupMembersGrouped } from '../../state/dock-settings'
import { sharedStateToRef } from '../../state/docks'
import { setDocksSidebarOverflowPanel, setFloatingTooltip, useDocksSidebarOverflowPanel } from '../../state/floating-tooltip'
import { accentActiveStyle, accentTextStyle } from '../../utils/accent-color'
import { accentVarStyle } from '../../utils/accent-color'
import DockGroupPopover from './DockGroupPopover.vue'
import DockIcon from './DockIcon.vue'

Expand Down Expand Up @@ -126,22 +126,21 @@ watchDebounced(

// Highlight the button when the hidden selection lives here, or while open.
const moreButtonActive = computed(() => selectedInOverflow.value || isOverflowPanelVisible.value)

// Accent styling, mirroring DockGroupPopover: falls back to the default
// Uno classes below when the group sets no `accentColor`.
const anchorStyle = computed(() => accentTextStyle(props.group.accentColor))
function memberStyle(memberId: string) {
return props.selectedId === memberId ? accentActiveStyle(props.group.accentColor) : undefined
}
const moreButtonStyle = computed(() => (moreButtonActive.value ? accentActiveStyle(props.group.accentColor) : undefined))
</script>

<template>
<div ref="sidebar" class="devframes-group-sidebar flex flex-col flex-none w-12 h-full border-r border-base of-y-hidden of-x-hidden select-none py1.5 items-center gap-0.5">
<!-- The group's accentColor overrides --devframe-primary for the whole rail,
so the anchor + selected members' text-primary/bg-active render accent. -->
<div
ref="sidebar"
class="devframes-group-sidebar flex flex-col flex-none w-12 h-full border-r border-base of-y-hidden of-x-hidden select-none py1.5 items-center gap-0.5"
:class="group.accentColor ? 'devframes-accent-scope' : ''"
:style="accentVarStyle(group.accentColor)"
>
<!-- Group anchor -->
<div
class="flex items-center justify-center w-8 h-8 op60"
:style="anchorStyle"
:class="group.accentColor ? 'color-active' : ''"
@pointerenter="showTooltip($event, group.title)"
@pointerleave="hideTooltip"
>
Expand All @@ -158,8 +157,7 @@ const moreButtonStyle = computed(() => (moreButtonActive.value ? accentActiveSty
:key="member.id"
:aria-label="member.title"
class="relative flex items-center justify-center w-8 h-8 rounded-lg transition"
:class="selectedId === member.id ? (group.accentColor ? '' : 'text-primary bg-active') : 'op60 hover:op100 hover:bg-active'"
:style="memberStyle(member.id)"
:class="selectedId === member.id ? 'text-primary bg-active' : 'op60 hover:op100 hover:bg-active'"
@pointerenter="showTooltip($event, member.title)"
@pointerleave="hideTooltip"
@pointerdown="hideTooltip"
Expand All @@ -181,8 +179,7 @@ const moreButtonStyle = computed(() => (moreButtonActive.value ? accentActiveSty
ref="moreButton"
aria-label="Show more"
class="relative flex items-center justify-center w-8 h-8 rounded-lg transition mt-auto flex-none"
:class="moreButtonActive ? (group.accentColor ? '' : 'text-primary bg-active') : 'op60 hover:op100 hover:bg-active'"
:style="moreButtonStyle"
:class="moreButtonActive ? 'text-primary bg-active' : 'op60 hover:op100 hover:bg-active'"
@pointerenter="showTooltip($event, 'Show more')"
@pointerleave="hideTooltip"
@pointerdown="hideTooltip"
Expand Down
34 changes: 33 additions & 1 deletion packages/hub-ui/src/client/components/icons/Brand.stories.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,15 @@
import type { Meta, StoryObj } from '@storybook/vue3-vite'
import { h } from 'vue'
import { h, onScopeDispose } from 'vue'
import { setBranding } from '../../state/branding'
import BrandMark from './BrandMark.vue'
import BrandWordmark from './BrandWordmark.vue'

// A stand-in consumer logo (inline data-URI SVG) so the branded stories need no
// network. A violet hexagon, echoing the sample `primaryColor` below.
const SAMPLE_LOGO = `data:image/svg+xml,${encodeURIComponent(
`<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path fill="#7c3aed" d="M12 2l9 5v10l-9 5-9-5V7z"/></svg>`,
)}`

const meta = {
title: 'Brand/Logos',
tags: ['autodocs'],
Expand All @@ -29,3 +36,28 @@ export const Wordmark: Story = {
name: 'BrandWordmark',
render: () => ({ setup: () => () => h(BrandWordmark) }),
}

/**
* A rebrand via `createUi({ branding })` / the host page — a custom product
* name, logo, and primary color. The wrapper sets `--devframe-primary` (what
* the bootstrap applies from `branding.primaryColor`), so the ramp — and the
* mark/wordmark — retint together.
*/
export const Branded: Story = {
name: 'Branded (custom)',
render: () => ({
setup() {
setBranding({ productName: 'Acme DevTools', logo: SAMPLE_LOGO, primaryColor: '#7c3aed' })
// Restore devframe defaults so sibling stories are unaffected.
onScopeDispose(() => setBranding({}))
return () => h('div', { class: 'devframes-accent-scope flex flex-col gap-4 p-6', style: '--devframe-primary: #7c3aed' }, [
h('div', { class: 'w-16 h-16' }, h(BrandMark)),
h(BrandWordmark),
h('div', { class: 'flex items-center gap-3' }, [
h('button', { class: 'btn-primary' }, 'Primary button'),
h('span', { class: 'color-active font-medium' }, 'color-active text'),
]),
])
},
}),
}
16 changes: 14 additions & 2 deletions packages/hub-ui/src/client/components/icons/BrandMark.vue
Original file line number Diff line number Diff line change
@@ -1,10 +1,22 @@
<script setup lang="ts">
import { useBranding, useBrandingLogo } from '../../state/branding'
import DevframeLogo from './DevframeLogo.vue'

// The brand mark — the consumer's branding logo (per color scheme) when set,
// otherwise devframe's default mark. Sized by the caller's classes.
const branding = useBranding()
const logo = useBrandingLogo(b => b.logo)
</script>

<template>
<!-- The devframe brand mark — sized by the caller's classes. -->
<div class="flex items-center justify-center">
<DevframeLogo class="w-full h-full" />
<img
v-if="logo"
:src="logo"
:alt="`${branding.productName} logo`"
class="w-full h-full object-contain"
draggable="false"
>
<DevframeLogo v-else class="w-full h-full" />
</div>
</template>
29 changes: 25 additions & 4 deletions packages/hub-ui/src/client/components/icons/BrandWordmark.vue
Original file line number Diff line number Diff line change
@@ -1,12 +1,33 @@
<script setup lang="ts">
import { useBranding, useBrandingLogo } from '../../state/branding'
import DevframeLogo from './DevframeLogo.vue'

// The wordmark — a consumer-supplied wordmark image, or the mark composed with
// the product name. A decorative flourish (e.g. peeking above the palette).
const branding = useBranding()
const wordmark = useBrandingLogo(b => b.wordmark)
const logo = useBrandingLogo(b => b.logo)
</script>

<template>
<!-- The devframe wordmark — a decorative brand flourish (e.g. peeking
above the command palette). -->
<div class="flex items-center gap-1.5 select-none" aria-hidden="true">
<DevframeLogo class="w-6 h-6 flex-none" />
<span class="text-xl font-bold tracking-tight color-base">Devframes</span>
<img
v-if="wordmark"
:src="wordmark"
:alt="branding.productName"
class="h-6 flex-none object-contain"
draggable="false"
>
<template v-else>
<img
v-if="logo"
:src="logo"
:alt="`${branding.productName} logo`"
class="w-6 h-6 flex-none object-contain"
draggable="false"
>
<DevframeLogo v-else class="w-6 h-6 flex-none" />
<span class="text-xl font-bold tracking-tight color-base">{{ branding.productName }}</span>
</template>
</div>
</template>
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import type { DocksContext } from '@devframes/hub/client'
import type { SharedState } from 'devframe/utils/shared-state'
import type { HubDocksUserSettings } from '../../state/dock-settings'
import { DEFAULT_STATE_USER_SETTINGS } from '@devframes/hub/constants'
import { useBranding } from '../../state/branding'
import { useConfirm } from '../../state/confirm'
import { sharedStateToRef } from '../../state/docks'

Expand All @@ -12,6 +13,7 @@ const props = defineProps<{
}>()

const settings = sharedStateToRef(props.settingsStore)
const branding = useBranding()
const confirm = useConfirm()

async function resetAllSettings() {
Expand Down Expand Up @@ -54,7 +56,7 @@ async function resetDocks() {
async function deauthorize() {
if (await confirm({
title: 'Revoke Authorization',
message: 'Revoke this browser\'s access to Devframes? You\'ll need to authorize again with a new code to reconnect.',
message: `Revoke this browser's access to ${branding.value.productName}? You'll need to authorize again with a new code to reconnect.`,
})) {
// Revokes this session's bearer token server-side; the server then
// broadcasts `devframe:auth:revoked`, dropping this (and any sibling)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import type { DocksContext } from '@devframes/hub/client'
import type { SharedState } from 'devframe/utils/shared-state'
import type { HubDocksUserSettings } from '../../state/dock-settings'
import { computed } from 'vue'
import { useBranding } from '../../state/branding'
import { colorSchemePreference, setColorSchemePreference } from '../../state/color-mode'
import { sharedStateToRef } from '../../state/docks'
import { isDockPopupSupported, requestDockPopupOpen, useIsDockPopupOpen } from '../../state/popup'
Expand All @@ -13,6 +14,7 @@ const props = defineProps<{
}>()

const settings = sharedStateToRef(props.settingsStore)
const branding = useBranding()
const panelStore = props.context.panel.store
const isEmbedded = props.context.clientType === 'embedded'
const isDockPopupOpen = useIsDockPopupOpen()
Expand Down Expand Up @@ -52,7 +54,7 @@ function setDockMode(mode: string) {
<div class="flex flex-col gap-2">
<div class="flex flex-col">
<span class="text-sm">Color mode</span>
<span class="text-xs op50">Theme for Devframes and its inner panels</span>
<span class="text-xs op50">Theme for {{ branding.productName }} and its inner panels</span>
</div>
<div class="flex items-center gap-1 bg-gray/10 rounded-lg p1 w-fit">
<button
Expand All @@ -74,7 +76,7 @@ function setDockMode(mode: string) {
<div v-if="isEmbedded && !isDockPopupOpen" class="flex flex-col gap-2">
<div class="flex flex-col">
<span class="text-sm">Dock mode</span>
<span class="text-xs op50">How the Devframes panel is displayed</span>
<span class="text-xs op50">How the {{ branding.productName }} panel is displayed</span>
</div>
<div class="flex items-center gap-1 bg-gray/10 rounded-lg p1 w-fit">
<button
Expand Down Expand Up @@ -124,7 +126,7 @@ function setDockMode(mode: string) {
</button>
<div class="flex flex-col">
<span class="text-sm">Close panel on outside click</span>
<span class="text-xs op50">Close the Devframes panel when clicking outside of it (embedded mode only)</span>
<span class="text-xs op50">Close the {{ branding.productName }} panel when clicking outside of it (embedded mode only)</span>
</div>
</label>
</div>
Expand Down
Loading