Skip to content

Commit b3bfb9e

Browse files
committed
docs: devframe branding in header and brand assets, grouped guide sidebar
1 parent 3d6e970 commit b3bfb9e

8 files changed

Lines changed: 403 additions & 1 deletion

File tree

‎docs/app/app.config.ts‎

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@ export default defineAppConfig({
77
title: 'Devframe',
88
logo: {
99
alt: 'Devframe',
10+
light: '/logo.svg',
11+
dark: '/logo.svg',
1012
},
1113
nav: [
1214
{
@@ -47,6 +49,67 @@ export default defineAppConfig({
4749
},
4850

4951
docs: {
52+
// Labeled sidebar groups per content section, consumed by the shadowed
53+
// `useFilteredNavigation` composable (mirrors the old VitePress sidebar).
54+
sidebarGroups: {
55+
guide: [
56+
{ title: 'Introduction', items: ['/guide'] },
57+
{
58+
title: 'Define your tool',
59+
items: [
60+
'/guide/devframe-definition',
61+
'/guide/rpc',
62+
'/guide/shared-state',
63+
'/guide/streaming',
64+
'/guide/client-assets',
65+
'/guide/scoped-context',
66+
'/guide/json-render',
67+
'/guide/diagnostics',
68+
'/guide/when-clauses',
69+
],
70+
},
71+
{
72+
title: 'Mount anywhere',
73+
items: [
74+
'/adapters/initiate',
75+
'/adapters',
76+
'/guide/standalone-cli',
77+
'/guide/client',
78+
'/guide/transports',
79+
'/guide/security',
80+
],
81+
},
82+
{ title: 'Agentic', items: ['/guide/agent-native'] },
83+
{
84+
title: 'Compose a hub',
85+
items: [
86+
'/guide/hub',
87+
'/guide/client-context',
88+
'/guide/hub-initiate',
89+
'/guide/services',
90+
'/guide/deep-linking',
91+
'/guide/events',
92+
],
93+
},
94+
{
95+
title: 'Customize the UI',
96+
items: [
97+
'/guide/build-your-own-json-render-frontend',
98+
'/guide/build-your-own-hub-ui',
99+
],
100+
},
101+
{ title: 'Ecosystem', items: ['/guide/built-with'] },
102+
{
103+
title: 'Migrations',
104+
items: [
105+
'/guide/migration-0.9',
106+
'/guide/migration-0.8',
107+
'/guide/migration-0.7',
108+
'/guide/migration-0.6',
109+
],
110+
},
111+
],
112+
},
50113
ogImage: {
51114
tagline: 'Build a devtool once. Mount it anywhere.',
52115
},
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
<script setup lang="ts">
2+
// Shadows the comark-docs layer's AppHeaderBrand: drops the Vercel OSS mark
3+
// and shows the devframe logo with its wordmark instead.
4+
const { header } = useAppConfig()
5+
const cms = useDocsContent()
6+
</script>
7+
8+
<template>
9+
<div class="flex shrink-0 items-center gap-2.5">
10+
<NuxtLink
11+
:to="prefixLink(header?.to || '/', cms.base)"
12+
:aria-label="header?.logo?.alt || header?.title"
13+
class="flex items-center gap-2 text-highlighted! lg:mr-3"
14+
>
15+
<AppHeaderLogo class="shrink-0" />
16+
<span class="text-lg font-semibold text-highlighted">{{ header?.title }}</span>
17+
</NuxtLink>
18+
</div>
19+
</template>
Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,117 @@
1+
<script setup lang="ts">
2+
const props = defineProps<{
3+
/** Variant label shown under the preview. */
4+
name: string
5+
/** Preview / PNG background color. */
6+
bg: string
7+
/** Raw SVG source of the asset. */
8+
svg: string
9+
/** Download base name (without extension). */
10+
file: string
11+
/** Intrinsic size used for the PNG render. */
12+
size: { w: number, h: number }
13+
}>()
14+
15+
const copied = ref(false)
16+
17+
function withXmlns(svg: string): string {
18+
return svg.includes('xmlns=')
19+
? svg
20+
: svg.replace('<svg ', '<svg xmlns="http://www.w3.org/2000/svg" ')
21+
}
22+
23+
async function copySvg(): Promise<void> {
24+
copied.value = true
25+
setTimeout(() => (copied.value = false), 1500)
26+
await navigator.clipboard.writeText(withXmlns(props.svg))
27+
}
28+
29+
function downloadSvg(): void {
30+
triggerDownload(
31+
URL.createObjectURL(new Blob([withXmlns(props.svg)], { type: 'image/svg+xml;charset=utf-8' })),
32+
`${props.file}.svg`,
33+
)
34+
}
35+
36+
async function downloadPng(): Promise<void> {
37+
const PAD = 32
38+
const SCALE = 2
39+
const { w, h } = props.size
40+
41+
const canvas = document.createElement('canvas')
42+
canvas.width = (w + PAD * 2) * SCALE
43+
canvas.height = (h + PAD * 2) * SCALE
44+
const ctx = canvas.getContext('2d')!
45+
ctx.fillStyle = props.bg
46+
ctx.fillRect(0, 0, canvas.width, canvas.height)
47+
48+
const img = new Image()
49+
// Data URLs are more reliable than blob URLs for SVG → canvas in Chromium.
50+
img.src = `data:image/svg+xml;charset=utf-8,${encodeURIComponent(withXmlns(props.svg))}`
51+
await img.decode()
52+
ctx.drawImage(img, PAD * SCALE, PAD * SCALE, w * SCALE, h * SCALE)
53+
54+
const pngBlob = await new Promise<Blob | null>(resolve => canvas.toBlob(resolve, 'image/png'))
55+
if (!pngBlob)
56+
return
57+
triggerDownload(URL.createObjectURL(pngBlob), `${props.file}.png`)
58+
}
59+
60+
function triggerDownload(url: string, filename: string): void {
61+
const a = document.createElement('a')
62+
a.href = url
63+
a.download = filename
64+
document.body.appendChild(a)
65+
a.click()
66+
document.body.removeChild(a)
67+
setTimeout(() => URL.revokeObjectURL(url), 1000)
68+
}
69+
</script>
70+
71+
<template>
72+
<div class="rounded-xl border border-default overflow-hidden">
73+
<div
74+
class="flex items-center justify-center py-14 px-10"
75+
:style="{ backgroundColor: bg }"
76+
>
77+
<img
78+
src="/logo.svg"
79+
alt=""
80+
class="h-16 w-auto"
81+
>
82+
</div>
83+
84+
<div class="flex items-center justify-between px-4 py-3 bg-muted">
85+
<span class="text-sm text-muted font-medium">{{ name }}</span>
86+
<div class="flex items-center gap-1.5">
87+
<UTooltip text="Copy SVG">
88+
<UButton
89+
:icon="copied ? 'i-lucide-check' : 'i-lucide-clipboard'"
90+
color="neutral"
91+
variant="ghost"
92+
size="xs"
93+
square
94+
class="cursor-pointer"
95+
@click="copySvg"
96+
/>
97+
</UTooltip>
98+
<UButton
99+
label="SVG"
100+
color="neutral"
101+
variant="outline"
102+
size="xs"
103+
class="cursor-pointer"
104+
@click="downloadSvg"
105+
/>
106+
<UButton
107+
label="PNG"
108+
color="neutral"
109+
variant="outline"
110+
size="xs"
111+
class="cursor-pointer"
112+
@click="downloadPng"
113+
/>
114+
</div>
115+
</div>
116+
</div>
117+
</template>
Lines changed: 123 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,123 @@
1+
/**
2+
* Shadows the comark-docs layer's `useFilteredNavigation` to support labeled
3+
* sidebar groups, restoring the grouped guide sidebar the VitePress site had.
4+
*
5+
* Groups are declared in `app.config.ts` under `docs.sidebarGroups`, keyed by
6+
* top-level content section; each group lists page paths (which may live in
7+
* other sections, e.g. the adapter pages the guide's "Mount anywhere" group
8+
* links to). Sections without a groups entry keep the layer's behavior: the
9+
* section's pages as a flat list (single-section tab) or the sections as
10+
* collapsible groups (multi-section tab).
11+
*/
12+
13+
/** Structural mirror of comark-content's NavigationItem (avoids depending on the transitive package). */
14+
interface NavItem {
15+
title: string
16+
path: string
17+
stem?: string
18+
children?: NavItem[]
19+
page?: boolean
20+
}
21+
22+
interface SidebarGroup {
23+
title: string
24+
items: string[]
25+
}
26+
27+
interface HeaderNavGroup {
28+
label: string
29+
sections?: string[]
30+
}
31+
32+
/** Logical top-level segment of a path, ignoring the active version `base`. */
33+
function segmentOf(path: string, base: string): string {
34+
const rel = base && path.startsWith(base) ? path.slice(base.length) : path
35+
return rel.split('/').filter(Boolean)[0] ?? ''
36+
}
37+
38+
function relOf(path: string, base: string): string {
39+
return (base && path.startsWith(base) ? path.slice(base.length) : path) || '/'
40+
}
41+
42+
export function useFilteredNavigation(): ComputedRef<NavItem[]> {
43+
const route = useRoute()
44+
const content = useDocsContent()
45+
const appConfig = useAppConfig()
46+
const navigation = inject<Ref<NavItem[]>>('navigation', ref([]))
47+
48+
return computed<NavItem[]>(() => {
49+
const base = content.value.base
50+
const seg = segmentOf(route.path, base)
51+
const nav = navigation.value ?? []
52+
53+
const headerGroups = (appConfig.header?.nav ?? []) as HeaderNavGroup[]
54+
const groups = headerGroups.length
55+
? headerGroups
56+
: nav.map(item => ({ label: item.title, sections: [segmentOf(item.path, base)] }))
57+
58+
const active = groups.find(group => group.sections?.includes(seg)) ?? groups[0]
59+
const sections = active?.sections
60+
// Manual tabs (no sections) have no content sidebar; fall back to the full tree.
61+
if (!sections?.length)
62+
return nav
63+
64+
// Single-section tab with configured groups: labeled subsections.
65+
const sidebarGroups = (appConfig as { docs?: { sidebarGroups?: Record<string, SidebarGroup[]> } })
66+
.docs
67+
?.sidebarGroups
68+
const groupsConfig = sections.length === 1 ? sidebarGroups?.[sections[0]!] : undefined
69+
if (groupsConfig) {
70+
// Index every page by base-relative path, preferring leaf pages over
71+
// section nodes (a section node carries the directory title).
72+
const byPath = new Map<string, NavItem>()
73+
const walk = (items: NavItem[]): void => {
74+
for (const item of items) {
75+
if (item.page !== false) {
76+
const rel = relOf(item.path, base)
77+
const existing = byPath.get(rel)
78+
if (!existing || (existing.children?.length && !item.children?.length))
79+
byPath.set(rel, item)
80+
}
81+
if (item.children)
82+
walk(item.children)
83+
}
84+
}
85+
walk(nav)
86+
87+
const used = new Set<string>()
88+
const grouped: NavItem[] = []
89+
for (const [index, group] of groupsConfig.entries()) {
90+
const children = group.items
91+
.map((path) => {
92+
used.add(path)
93+
return byPath.get(path)
94+
})
95+
.filter(child => child !== undefined)
96+
.map(child => ({ ...child, children: undefined }))
97+
if (!children.length)
98+
continue
99+
grouped.push({
100+
title: group.title,
101+
path: `${base}/${sections[0]}-group-${index}`,
102+
children,
103+
page: false,
104+
})
105+
}
106+
107+
// Append pages the groups don't cover, so a new page never vanishes.
108+
const node = nav.find(item => segmentOf(item.path, base) === sections[0])
109+
const leftovers = (node?.children ?? [])
110+
.filter(child => !used.has(relOf(child.path, base)))
111+
return [...grouped, ...leftovers]
112+
}
113+
114+
// Single-section tab: that section's children as a flat tree.
115+
if (sections.length === 1) {
116+
const node = nav.find(item => segmentOf(item.path, base) === sections[0])
117+
return (node?.children ?? []).filter(child => child.path !== node?.path)
118+
}
119+
120+
// Multi-section tab: the sections as collapsible groups.
121+
return nav.filter(item => sections.includes(segmentOf(item.path, base)))
122+
})
123+
}

0 commit comments

Comments
 (0)