Skip to content

Commit 78a5f68

Browse files
committed
Merge remote-tracking branch 'origin/main' into feat/assets-plugin
# Conflicts: # pnpm-lock.yaml # pnpm-workspace.yaml
2 parents 2ea73be + a88350d commit 78a5f68

48 files changed

Lines changed: 1218 additions & 694 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

design/dock-icon.ts

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
// Framework-neutral port of @antfu/design's `DisplayIconifyRemoteIcon`:
2+
// https://github.com/antfu/design/blob/main/packages/design/components/Display/DisplayIconifyRemoteIcon.vue
3+
//
4+
// Resolves a devframe dock `icon` (an Iconify `collection:icon` id, e.g.
5+
// `ph:git-branch-duotone`) to its live, sanitized SVG markup, fetched from the
6+
// public `api.iconify.design` CDN. Unlike a UnoCSS `preset-icons` class, this
7+
// needs no `@iconify-json/*` collection installed and no hand-maintained
8+
// id -> class table — any Iconify id just works, at the cost of a network
9+
// round-trip on first render. We reuse @antfu/design's own fetcher, cache and
10+
// sanitizer (`utils/iconify.ts`) rather than reimplementing them; only the id
11+
// parsing and light/dark selection below are devframe-specific, mirroring the
12+
// upstream Vue component's own `icon` prop parsing. Vue surfaces should render
13+
// `DisplayIconifyRemoteIcon` directly instead of using this port.
14+
import { getIconifySvg } from '@antfu/design/utils/iconify'
15+
16+
// Mirrors DisplayIconifyRemoteIcon.vue's own `collection:icon` parse (with an
17+
// optional `i-` prefix tolerated so a UnoCSS-style id also works).
18+
const ICONIFY_ID = /^(?:i-)?([\w-]+):([\w-]+)$/
19+
20+
/**
21+
* Resolve a dock icon (a `collection:icon` string, or a `{ light, dark }`
22+
* pair — the `light` variant is fetched) to its sanitized SVG markup.
23+
*
24+
* Returns `undefined` when the id doesn't parse or the fetch fails, so the
25+
* caller can fall back to a text initial.
26+
*
27+
* @example
28+
* await dockIconSvg('ph:git-branch-duotone') // → '<svg ...>...</svg>'
29+
*/
30+
export async function dockIconSvg(name: string | { light: string, dark: string } | undefined): Promise<string | undefined> {
31+
const id = typeof name === 'string' ? name : name?.light
32+
if (!id)
33+
return undefined
34+
const match = id.match(ICONIFY_ID)
35+
if (!match)
36+
return undefined
37+
try {
38+
return await getIconifySvg(match[1]!, match[2]!)
39+
}
40+
catch {
41+
// A failed fetch (offline / flaky CDN) degrades to the text-initial
42+
// fallback, not a thrown error out of a render path.
43+
return undefined
44+
}
45+
}

examples/minimal-next-devframe-hub/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
"@json-render/react": "catalog:frontend",
2424
"colorjs.io": "catalog:frontend",
2525
"devframe": "workspace:*",
26+
"dompurify": "catalog:frontend",
2627
"minimal-json-render": "workspace:*",
2728
"next": "catalog:frontend",
2829
"react": "catalog:frontend",
Lines changed: 5 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -1,35 +1,5 @@
1-
// @unocss-include
2-
// Map a devframe dock `icon` (e.g. `ph:git-branch-duotone`) to a UnoCSS
3-
// `preset-icons` class. Keeping the class strings literal lets UnoCSS
4-
// statically extract them and inline only these glyphs from `@iconify-json/ph`
5-
// at build time — the same icon strategy vite-devtools uses. Add a row here to
6-
// support another dock icon.
7-
const ICON_CLASS: Record<string, string> = {
8-
'ph:git-branch-duotone': 'i-ph-git-branch-duotone',
9-
'ph:terminal-window-duotone': 'i-ph-terminal-window-duotone',
10-
'ph:code-duotone': 'i-ph-code-duotone',
11-
'ph:stethoscope-duotone': 'i-ph-stethoscope-duotone',
12-
'ph:wheelchair-duotone': 'i-ph-wheelchair-duotone',
13-
'ph:rocket-duotone': 'i-ph-rocket-duotone',
14-
'ph:wrench-duotone': 'i-ph-wrench-duotone',
15-
'ph:plug-duotone': 'i-ph-plug-duotone',
16-
'ph:layout-duotone': 'i-ph-layout-duotone',
17-
'ph:note-pencil-duotone': 'i-ph-note-pencil-duotone',
18-
'ph:squares-four-duotone': 'i-ph-squares-four-duotone',
19-
'ph:house-duotone': 'i-ph-house-duotone',
20-
'ph:puzzle-piece-duotone': 'i-ph-puzzle-piece-duotone',
21-
'ph:clock-duotone': 'i-ph-clock-duotone',
22-
'ph:gear-duotone': 'i-ph-gear-duotone',
23-
'ph:sliders-horizontal-duotone': 'i-ph-sliders-horizontal-duotone',
24-
}
25-
26-
/**
27-
* Resolve a dock icon to its UnoCSS class, or an empty string when the icon
28-
* isn't mapped (the caller falls back to a text initial).
29-
*/
30-
export function iconClass(name: string | { light: string, dark: string } | undefined): string {
31-
if (!name)
32-
return ''
33-
const id = typeof name === 'string' ? name : name.light
34-
return ICON_CLASS[id] ?? ''
35-
}
1+
// Re-exports the shared devframe dock-icon resolver (see
2+
// `design/dock-icon.ts`, a port of @antfu/design's `DisplayIconifyRemoteIcon`)
3+
// so this surface stays in lockstep with every other hub shell instead of
4+
// hand-maintaining its own icon table.
5+
export { dockIconSvg } from '../../../../../design/dock-icon'

examples/minimal-next-devframe-hub/src/client/app/page.tsx

Lines changed: 26 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ import type { DevframeJsonRenderDockEntry } from '@devframes/json-render/hub'
1313
import { connectDevframe, createDevframeClientHost, FRAME_NAV_CHANNEL } from '@devframes/hub/client'
1414
import { useEffect, useMemo, useRef, useState } from 'react'
1515
import { createReactJsonRenderDockRenderer } from '../json-render/dock-renderer'
16-
import { iconClass } from './icons'
16+
import { dockIconSvg } from './icons'
1717

1818
const HUB_BASE = '/__hub/'
1919

@@ -138,11 +138,32 @@ function createClientPlaygroundSpec(clientType: string): DevframeJsonRenderSpec
138138
}
139139
}
140140

141-
/** Render a dock icon, falling back to the title's initial when unmapped. */
141+
/** Fetches (and caches, for the component's lifetime) a dock icon's sanitized SVG. */
142+
function useDockIconSvg(icon: DevframeDockEntry['icon']): string | undefined {
143+
const [svg, setSvg] = useState<string | undefined>(undefined)
144+
const key = typeof icon === 'string' ? icon : icon?.light
145+
146+
useEffect(() => {
147+
let cancelled = false
148+
setSvg(undefined)
149+
void dockIconSvg(icon).then((resolved) => {
150+
if (!cancelled)
151+
setSvg(resolved)
152+
})
153+
return () => {
154+
cancelled = true
155+
}
156+
// Re-fetch only when the icon id itself changes, not on every `icon` object identity.
157+
}, [key])
158+
159+
return svg
160+
}
161+
162+
/** Render a dock icon, falling back to the title's initial while it loads or when unmapped. */
142163
function DockIcon({ entry }: { entry: DevframeDockEntry }) {
143-
const cls = iconClass(entry.icon)
144-
if (cls)
145-
return <span className={`${cls} shrink-0 text-lg`} />
164+
const svg = useDockIconSvg(entry.icon)
165+
if (svg)
166+
return <span className="h-5 w-5 shrink-0 text-lg" dangerouslySetInnerHTML={{ __html: svg }} />
146167
const initial = (entry.title?.[0] ?? '?').toUpperCase()
147168
return <span className="grid h-5 w-5 shrink-0 place-items-center rounded bg-active text-[0.7rem] font-bold">{initial}</span>
148169
}

examples/minimal-next-devframe-hub/src/client/next.config.mjs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,12 @@
11
/** @type {import('next').NextConfig} */
22
const nextConfig = {
33
images: { unoptimized: true },
4+
// @antfu/design ships raw, uncompiled `.ts`/`.vue` source (see its README —
5+
// "no bundling, your build compiles it"). `dockIconSvg` (design/dock-icon.ts)
6+
// imports its `utils/iconify.ts` directly, so Next/Turbopack — which
7+
// otherwise treats node_modules as pre-built and has no loader for a bare
8+
// `.ts` file there — needs to run this package through its own transform.
9+
transpilePackages: ['@antfu/design'],
410
// The workspace typecheck owns source-level project references.
511
typescript: { ignoreBuildErrors: true },
612
// Mounted devframe SPAs are served at `/__<id>/` and reference their assets

examples/minimal-vite-devframe-hub/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@
2626
"@devframes/plugin-terminals": "workspace:*",
2727
"colorjs.io": "catalog:frontend",
2828
"devframe": "workspace:*",
29+
"dompurify": "catalog:frontend",
2930
"minimal-json-render": "workspace:*",
3031
"vue": "catalog:frontend"
3132
},
Lines changed: 5 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -1,35 +1,5 @@
1-
// @unocss-include
2-
// Map a devframe dock `icon` (e.g. `ph:git-branch-duotone`) to a UnoCSS
3-
// `preset-icons` class. Keeping the class strings literal lets UnoCSS
4-
// statically extract them and inline only these glyphs from `@iconify-json/ph`
5-
// at build time — the same icon strategy vite-devtools uses. Add a row here to
6-
// support another dock icon.
7-
const ICON_CLASS: Record<string, string> = {
8-
'ph:git-branch-duotone': 'i-ph-git-branch-duotone',
9-
'ph:terminal-window-duotone': 'i-ph-terminal-window-duotone',
10-
'ph:code-duotone': 'i-ph-code-duotone',
11-
'ph:stethoscope-duotone': 'i-ph-stethoscope-duotone',
12-
'ph:wheelchair-duotone': 'i-ph-wheelchair-duotone',
13-
'ph:rocket-duotone': 'i-ph-rocket-duotone',
14-
'ph:wrench-duotone': 'i-ph-wrench-duotone',
15-
'ph:plug-duotone': 'i-ph-plug-duotone',
16-
'ph:layout-duotone': 'i-ph-layout-duotone',
17-
'ph:note-pencil-duotone': 'i-ph-note-pencil-duotone',
18-
'ph:squares-four-duotone': 'i-ph-squares-four-duotone',
19-
'ph:house-duotone': 'i-ph-house-duotone',
20-
'ph:puzzle-piece-duotone': 'i-ph-puzzle-piece-duotone',
21-
'ph:clock-duotone': 'i-ph-clock-duotone',
22-
'ph:gear-duotone': 'i-ph-gear-duotone',
23-
'ph:sliders-horizontal-duotone': 'i-ph-sliders-horizontal-duotone',
24-
}
25-
26-
/**
27-
* Resolve a dock icon to its UnoCSS class, or an empty string when the icon
28-
* isn't mapped (the caller falls back to a text initial).
29-
*/
30-
export function iconClass(name: string | { light: string, dark: string } | undefined): string {
31-
if (!name)
32-
return ''
33-
const id = typeof name === 'string' ? name : name.light
34-
return ICON_CLASS[id] ?? ''
35-
}
1+
// Re-exports the shared devframe dock-icon resolver (see
2+
// `design/dock-icon.ts`, a port of @antfu/design's `DisplayIconifyRemoteIcon`)
3+
// so this surface stays in lockstep with every other hub shell instead of
4+
// hand-maintaining its own icon table.
5+
export { dockIconSvg } from '../../../../design/dock-icon'

examples/minimal-vite-devframe-hub/src/client/main.ts

Lines changed: 42 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ import type { DevframeJsonRenderSpec } from '@devframes/json-render'
99
import type { DevframeJsonRenderDockEntry } from '@devframes/json-render/hub'
1010
import { connectDevframe, createDevframeClientHost, FRAME_NAV_CHANNEL } from '@devframes/hub/client'
1111
import { createJsonRenderDockRenderer } from '@devframes/json-render-ui'
12-
import { iconClass } from './icons'
12+
import { dockIconSvg } from './icons'
1313
import 'virtual:uno.css'
1414
import '@antfu/design/styles.css'
1515

@@ -40,13 +40,48 @@ function renderList<T>(host: HTMLElement, items: readonly T[], render: (item: T)
4040
host.innerHTML = items.map(render).join('')
4141
}
4242

43-
/** Render a dock icon, falling back to the title's initial when unknown. */
43+
// Session-lifetime cache of resolved dock-icon SVGs, keyed by the icon id
44+
// (e.g. `ph:git-branch-duotone`) — `dockIconSvg` itself caches per fetch, this
45+
// just lets a re-render (e.g. a badge update) paint an already-resolved icon
46+
// synchronously instead of flashing the fallback initial again.
47+
const dockIconCache = new Map<string, string | null>()
48+
49+
function dockIconKey(icon: DevframeDockEntry['icon']): string | undefined {
50+
return typeof icon === 'string' ? icon : icon?.light
51+
}
52+
53+
/** Render a dock icon's placeholder markup: a resolved SVG if cached, else the title's initial. */
4454
function dockIcon(entry: DevframeDockEntry): string {
45-
const cls = iconClass(entry.icon)
46-
if (cls)
47-
return `<span class="${cls} shrink-0 text-lg"></span>`
55+
const key = entry.icon ? dockIconKey(entry.icon) : undefined
56+
const cached = key ? dockIconCache.get(key) : undefined
57+
if (cached)
58+
return `<span class="h-5 w-5 shrink-0 text-lg" data-dock-icon="${entry.id}">${cached}</span>`
4859
const initial = (entry.title?.[0] ?? '?').toUpperCase()
49-
return `<span class="grid h-5 w-5 shrink-0 place-items-center rounded bg-active text-[0.7rem] font-bold">${initial}</span>`
60+
return `<span class="grid h-5 w-5 shrink-0 place-items-center rounded bg-active text-[0.7rem] font-bold" data-dock-icon="${entry.id}">${initial}</span>`
61+
}
62+
63+
/**
64+
* Resolve (and cache) each unresolved dock's icon SVG, then patch just that
65+
* dock's `[data-dock-icon]` element in place — no full re-render, since the
66+
* fetch is async and the dock list may already be showing by the time it
67+
* settles. A dock with no icon or an unparsable/failed fetch keeps its
68+
* fallback initial.
69+
*/
70+
async function hydrateDockIcons(list: readonly DevframeDockEntry[]): Promise<void> {
71+
await Promise.all(list.map(async (entry) => {
72+
const key = entry.icon ? dockIconKey(entry.icon) : undefined
73+
if (!key || dockIconCache.has(key))
74+
return
75+
const svg = await dockIconSvg(entry.icon) ?? null
76+
dockIconCache.set(key, svg)
77+
if (!svg)
78+
return
79+
const el = docksEl.querySelector<HTMLElement>(`[data-dock-icon="${entry.id}"]`)
80+
if (el) {
81+
el.className = 'h-5 w-5 shrink-0 text-lg'
82+
el.innerHTML = svg
83+
}
84+
}))
5085
}
5186

5287
function isIframeDock(d: DevframeDockEntry): d is DevframeDockEntry & { type: 'iframe', url: string } {
@@ -315,6 +350,7 @@ async function main() {
315350

316351
renderList(docksEl, list, d =>
317352
`<li><button type="button" data-dock-id="${d.id}" class="relative inline-flex items-center gap-1.5 max-w-52 px-2 py-1 rounded-md border border-transparent text-sm op-fade select-none cursor-pointer transition hover:op100 hover:bg-active w-full! max-w-none! gap-2.5!${d.id === docksCtx.selectedId ? ' op100! bg-active border-base! color-base' : ''}" title="${d.title}">${dockIcon(d)}<span class="truncate">${d.title}</span>${d.badge ? `<span class="ml-auto shrink-0 rounded bg-active px1 py0.5 text-[0.6rem] font-mono color-base">${d.badge}</span>` : ''}</button></li>`)
353+
void hydrateDockIcons(list)
318354

319355
void showSelection(list)
320356
}

examples/storybook-hub/README.md

Lines changed: 0 additions & 56 deletions
This file was deleted.

examples/storybook-hub/package.json

Lines changed: 0 additions & 30 deletions
This file was deleted.

0 commit comments

Comments
 (0)