Skip to content

Commit 5706837

Browse files
committed
feat(icons): support imported inline SVG data
1 parent 7405628 commit 5706837

62 files changed

Lines changed: 1837 additions & 237 deletions

File tree

Some content is hidden

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

alias.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,7 @@ export const alias = {
5555
'@devframes/hub/node': r('hub/src/node/index.ts'),
5656
'@devframes/hub/types': r('hub/src/types/index.ts'),
5757
'@devframes/hub': r('hub/src/index.ts'),
58+
'@devframes/hub-ui/icons': r('hub-ui/src/client/utils/icons.ts'),
5859
'@devframes/hub-ui': r('hub-ui/src/index.ts'),
5960
'@devframes/nuxt/runtime/plugin.client': r('nuxt/src/runtime/plugin.client.ts'),
6061
'@devframes/nuxt/single': r('nuxt/src/single.ts'),

design/dock-icon.ts

Lines changed: 6 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -1,45 +1,14 @@
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, since 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'
1+
import type { DevframeIcon } from 'devframe/types'
2+
import { getIconSource, getIconSvg } from '../packages/hub-ui/src/client/utils/icons'
153

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 whose `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)
4+
/** Resolve the selected icon variant; failed remote requests leave the monogram fallback. */
5+
export async function dockIconSvg(icon: DevframeIcon | undefined, dark = false): Promise<string | undefined> {
6+
if (!icon)
367
return undefined
378
try {
38-
return await getIconifySvg(match[1]!, match[2]!)
9+
return await getIconSvg(getIconSource(icon, dark))
3910
}
4011
catch {
41-
// A failed fetch (offline / flaky CDN) degrades to the text-initial
42-
// fallback, not a thrown error out of a render path.
4312
return undefined
4413
}
4514
}

docs/content/1.guide/16.hub.md

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -254,3 +254,25 @@ Two minimal hubs mount every built-in devframe behind an icon dock, plus a "Tabb
254254
## Diagnostics
255255

256256
Hub-side diagnostic codes live in the `DF8xxx` range; see the [error reference](/errors).
257+
258+
## Inline icons
259+
260+
Dock entries, commands, launcher entries, and terminal sessions accept named Iconify icons, image URLs, imported Iconify data, or raw SVG markup:
261+
262+
```ts
263+
import terminal from '@iconify-icons/ph/terminal-window-duotone'
264+
265+
const importedIcon = { icon: terminal }
266+
const localIcon = {
267+
icon: { svg: '<svg viewBox="0 0 24 24"><path fill="currentColor" d="M4 4h16v16H4z"/></svg>' },
268+
}
269+
const themedIcon = { icon: { light: terminal, dark: localIcon.icon } }
270+
```
271+
272+
Import only the icons you use from your icon package, or import a committed SVG as text through your build tool. These objects are JSON-serializable; no registration or runtime package lookup is required. The shared `DevframeIconSource` type describes one source, and `DevframeDockEntryIcon` also accepts a light/dark pair.
273+
274+
The reference hub UI provider sanitizes imported markup and renders it inline. `currentColor` inherits the surrounding theme, hover, selected, and disabled styles. Image URLs and data URLs retain the `<img>` path and do not inherit text color. The JSON renderer's component catalog remains separate.
275+
276+
The stock hub UI provider bundles a finite set of Phosphor icons used by its dock defaults, including Terminals, Messages, and Inspector. Other named icons continue to use the remote Iconify service. No complete icon collection is shipped to the browser.
277+
278+
External custom hub UI providers must support the new object forms before using them. The browser-only `@devframes/hub-ui/icons` entry exports `getIconSource(icon, dark)`, `isIconImage(source)`, and `getIconSvg(source)` for custom renderers: select a variant, render image sources with `<img>`, and render resolved sanitized markup inline. Handle rejected remote requests and ignore results after the source changes or the view is removed.

examples/custom-hub-next/src/client/app/page.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -223,7 +223,7 @@ function pollDrawer(
223223
/** Fetches (and caches, for the component's lifetime) a dock icon's sanitized SVG. */
224224
function useDockIconSvg(icon: DevframeDockEntry['icon']): string | undefined {
225225
const [svg, setSvg] = useState<string | undefined>(undefined)
226-
const key = typeof icon === 'string' ? icon : icon?.light
226+
const key = JSON.stringify(icon)
227227

228228
useEffect(() => {
229229
let cancelled = false

examples/custom-hub-vite/src/client/main.ts

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -77,10 +77,6 @@ function renderList<T>(host: HTMLElement, items: readonly T[], row: (item: T) =>
7777
: '<li class="rounded-lg border border-base bg-base border-dashed px2.5 py1.5 text-xs font-mono op-mute">empty</li>'
7878
}
7979

80-
function iconName(icon: DevframeDockEntry['icon']): string | undefined {
81-
return typeof icon === 'string' ? icon : icon?.light
82-
}
83-
8480
// One dock-rail button: a monogram placeholder for the icon (patched with the
8581
// real SVG once `paintDockIcons` resolves it), the title, and an optional badge.
8682
function dockButton(entry: DevframeDockEntry, selectedId: string | null): string {
@@ -103,7 +99,7 @@ function dockButton(entry: DevframeDockEntry, selectedId: string | null): string
10399
// keeps the monogram.
104100
function paintDockIcons(list: readonly DevframeDockEntry[]): void {
105101
for (const entry of list) {
106-
if (!iconName(entry.icon))
102+
if (!entry.icon)
107103
continue
108104
void dockIconSvg(entry.icon).then((svg) => {
109105
const slot = el.docks.querySelector<HTMLElement>(`[data-dock-icon="${entry.id}"]`)

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@
3737
"release": "bumpp -r",
3838
"typecheck": "pnpm run verify:typecheck-coverage && turbo run typecheck",
3939
"verify:typecheck-coverage": "tsx scripts/verify-typecheck-coverage.ts",
40-
"postinstall": "npx simple-git-hooks && skills-npm && pnpm run build:css"
40+
"postinstall": "npx simple-git-hooks && skills-npm && pnpm run build:css && pnpm --filter @devframes/hub-ui run build:icons"
4141
},
4242
"devDependencies": {
4343
"@antfu/design": "catalog:frontend",

packages/devframe/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,7 @@
9494
}
9595
},
9696
"dependencies": {
97+
"@iconify/types": "catalog:types",
9798
"@modelcontextprotocol/server": "catalog:deps",
9899
"@standard-schema/spec": "catalog:deps",
99100
"birpc": "catalog:deps",

packages/devframe/src/types/devframe.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import type { CAC } from 'cac'
22
import type { CliFlagsSchema } from '../adapters/flags'
33
import type { DevframeAuthHandler } from '../node/auth/handler'
44
import type { DevframeNodeContext } from './context'
5+
import type { DevframeIcon } from './icons'
56
import type { StaticAssetsSource } from './remote-assets'
67
import type { DevframeServiceInput } from './services'
78

@@ -295,7 +296,7 @@ export interface DevframeDockDefaults {
295296
/** Dock entry title. Defaults to the definition's `name`. */
296297
title?: string
297298
/** Dock entry icon. Defaults to the definition's `icon`. */
298-
icon?: string | { light: string, dark: string }
299+
icon?: DevframeIcon
299300
/**
300301
* Sort weight within the dock; higher sorts earlier.
301302
* @default 0
@@ -383,7 +384,7 @@ export interface DevframeDefinition {
383384
homepage: string
384385
/** One-line summary of what the tool does. */
385386
description: string
386-
icon?: string | { light: string, dark: string }
387+
icon?: DevframeIcon
387388
/**
388389
* Default dock attributes applied when a hub mounts this devframe as an
389390
* iframe dock entry. Consulted only by the hub install path (`ctx.install`),
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
import type { IconifyIcon } from '@iconify/types'
2+
3+
/** Iconify name, image URL, or inline icon data supplied by a devframe. */
4+
export type DevframeIconSource = string | IconifyIcon | { svg: string }
5+
6+
/** An icon shared across themes, or separate light and dark sources. */
7+
export type DevframeIcon = DevframeIconSource | { light: DevframeIconSource, dark: DevframeIconSource }

packages/devframe/src/types/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ export * from './devframe'
44
export * from './diagnostics'
55
export * from './events'
66
export * from './host'
7+
export * from './icons'
78
export * from './remote-assets'
89
export * from './rpc'
910
export * from './rpc-augments'

0 commit comments

Comments
 (0)