feat(client): brand the Plugins page cards with the package icon.svg

The Plugins page's package cards draw each bundle's icon from the Host's
package metadata (manifest-relative SVG/PNG/JPEG/WebP, inlined as a data
URI), which we never declared - the cards fell back to the default
artwork. The Context emblem's single graphic source is now the
package-root icon.svg: the package.json "icon" field points the Host's
reader at it, and the client build inlines the same file through a
"*.svg?raw" channel (the bundled ContextIcon re-renders it at every
requested size), so the code carries no second copy of the paths. A spec
pins the file and the component in lockstep; the files list ships the
asset.
This commit is contained in:
bowenliang123 committed 2026-09-23 16:44:08 +08:00
1 parent 0016d9c62f
commit bdb63a1aed
6 files changed
+72 -23

No files matched your search

+12
View File
@@ -0,0 +1,12 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1024 1024">
<path d="M899.984 19.873h-3.452c-26.123 0-47.296 21.172-47.296 47.296v888.508c0 26.127 21.173 47.298 47.296 47.298h3.452c26.119 0 47.297-21.171 47.297-47.298V67.169c0-26.124-21.177-47.296-47.297-47.296z" fill="#4A5699"/>
<path d="M132.643 19.873h-3.449c-26.12 0-47.296 21.172-47.296 47.296v888.508c0 26.127 21.177 47.298 47.296 47.298h3.449c26.123 0 47.299-21.171 47.299-47.298V67.169c0-26.124-21.176-47.296-47.299-47.296z" fill="#C45FA0"/>
<path d="M899.463 19.873H129.194c-26.12 0-47.296 21.172-47.296 47.296v3.377c0 26.12 21.177 47.299 47.296 47.299h770.269c26.123 0 47.296-21.179 47.296-47.299v-3.377c0-26.124-21.173-47.296-47.296-47.296z" fill="#6277BA"/>
<path d="M899.463 905.006H129.194c-26.12 0-47.296 21.17-47.296 47.29v3.381c0 26.127 21.177 47.298 47.296 47.298h770.269c26.123 0 47.296-21.171 47.296-47.298v-3.381c0-26.12-21.173-47.29-47.296-47.29z" fill="#C45FA0"/>
<path d="M717.962 543.153H542.047c-26.121 0-47.298 21.175-47.298 47.297v3.724c0 26.123 21.177 47.293 47.298 47.293h175.915c26.121 0 47.297-21.17 47.297-47.293v-3.724c0-26.122-21.176-47.297-47.297-47.297z" fill="#E5594F"/>
<path d="M689.268 198.849H513.355c-26.122 0-47.298 21.175-47.298 47.297v3.722c0 26.12 21.176 47.297 47.298 47.297h175.912c26.122 0 47.298-21.177 47.298-47.297v-3.722c0-26.122-21.175-47.297-47.297-47.297z" fill="#F0D043"/>
<path d="M757.789 353.081H261.17c-26.121 0-47.297 21.172-47.297 47.296v3.377c0 26.121 21.177 47.299 47.297 47.299h496.619c26.121 0 47.296-21.178 47.296-47.299v-3.377c0-26.125-21.175-47.296-47.296-47.296z" fill="#E5594F"/>
<path d="M762.638 726.225h-496.62c-26.12 0-47.294 21.18-47.294 47.301v3.377c0 26.12 21.174 47.3 47.294 47.3h496.62c26.122 0 47.296-21.18 47.296-47.3v-3.377c0-26.122-21.174-47.301-47.296-47.301z" fill="#6277BA"/>
<path d="M355.734 543.328H281.41c-26.122 0-47.297 21.17-47.297 47.293v3.378c0 26.118 21.175 47.297 47.297 47.297h74.324c26.123 0 47.296-21.179 47.296-47.297v-3.378c0-26.123-21.174-47.293-47.296-47.293z" fill="#F39A2B"/>
<path d="M334.85 248.006m-48.986 0a48.986 48.986 0 1 0 97.972 0 48.986 48.986 0 1 0-97.972 0Z" fill="#F39A2B"/>
</svg>

After

Width:  |  Height:  |  Size: 2.1 KiB

+2
View File
@@ -2,6 +2,7 @@
"name": "dsh-context",
"version": "0.54.4",
"description": "A DeepSeek Harness plugin for context insight and management, with context dashboard and context command, for understanding how the context is made of, and how it evolves.",
"icon": "icon.svg",
"author": "bowenliang123",
"repository": {
"type": "git",
@@ -26,6 +27,7 @@
"./package.json": "./package.json"
},
"files": [
"icon.svg",
"lib/client.js",
"lib/index.js",
"lib/index.d.ts",
+7
View File
@@ -17,3 +17,10 @@ declare module '*.module.css' {
const classes: Record<string, string>
export default classes
}
// The package emblem's static file (../icon.svg) rides the same build-time
// channel idea: `?raw` inlines the file's markup as the default export.
declare module '*.svg?raw' {
const markup: string
export default markup
}
+14 -21
View File
@@ -2,29 +2,23 @@
* The Context emblem: the colourful document sheet, bundled rather than read
* off the harness primitives so the plugin's identity is self-contained.
*
* It fills the right-Sidebar tab type's two glyph seats, exactly as the shipped
* files type fills its own: the guide capsule before the tab is open
* (`SidebarGuideEntry.icon`) and the chip title beside the label once it is
* (`sidebar.right.pane.tab.title`). Fixed fills, never `currentColor` — the
* sheet is deliberately polychrome on both light and dark chrome.
* The single graphic source is the package-root `icon.svg` — the same file
* package.json `icon` hands to the Host's package-meta reader, which serves
* it to the Plugins page's package cards. The client bundle inlines the
* file's markup at build time (the `?raw` channel in tsdown.config.ts) and
* re-renders it at every requested size. Fixed fills, never `currentColor` —
* the sheet is deliberately polychrome on both light and dark chrome.
*/
import sheetMarkup from '../../icon.svg?raw'
import type { ReactElement } from 'react'
import type { Translate } from './i18n'
/** The sheet's strokes, in paint order, as `[path, fill]` pairs. */
const SHEET_PATHS: ReadonlyArray<readonly [string, string]> = [
['M899.984 19.873h-3.452c-26.123 0-47.296 21.172-47.296 47.296v888.508c0 26.127 21.173 47.298 47.296 47.298h3.452c26.119 0 47.297-21.171 47.297-47.298V67.169c0-26.124-21.177-47.296-47.297-47.296z', '#4A5699'],
['M132.643 19.873h-3.449c-26.12 0-47.296 21.172-47.296 47.296v888.508c0 26.127 21.177 47.298 47.296 47.298h3.449c26.123 0 47.299-21.171 47.299-47.298V67.169c0-26.124-21.176-47.296-47.299-47.296z', '#C45FA0'],
['M899.463 19.873H129.194c-26.12 0-47.296 21.172-47.296 47.296v3.377c0 26.12 21.177 47.299 47.296 47.299h770.269c26.123 0 47.296-21.179 47.296-47.299v-3.377c0-26.124-21.173-47.296-47.296-47.296z', '#6277BA'],
['M899.463 905.006H129.194c-26.12 0-47.296 21.17-47.296 47.29v3.381c0 26.127 21.177 47.298 47.296 47.298h770.269c26.123 0 47.296-21.171 47.296-47.298v-3.381c0-26.12-21.173-47.29-47.296-47.29z', '#C45FA0'],
['M717.962 543.153H542.047c-26.121 0-47.298 21.175-47.298 47.297v3.724c0 26.123 21.177 47.293 47.298 47.293h175.915c26.121 0 47.297-21.17 47.297-47.293v-3.724c0-26.122-21.176-47.297-47.297-47.297z', '#E5594F'],
['M689.268 198.849H513.355c-26.122 0-47.298 21.175-47.298 47.297v3.722c0 26.12 21.176 47.297 47.298 47.297h175.912c26.122 0 47.298-21.177 47.298-47.297v-3.722c0-26.122-21.175-47.297-47.297-47.297z', '#F0D043'],
['M757.789 353.081H261.17c-26.121 0-47.297 21.172-47.297 47.296v3.377c0 26.121 21.177 47.299 47.297 47.299h496.619c26.121 0 47.296-21.178 47.296-47.299v-3.377c0-26.125-21.175-47.296-47.296-47.296z', '#E5594F'],
['M762.638 726.225h-496.62c-26.12 0-47.294 21.18-47.294 47.301v3.377c0 26.12 21.174 47.3 47.294 47.3h496.62c26.122 0 47.296-21.18 47.296-47.3v-3.377c0-26.122-21.174-47.301-47.296-47.301z', '#6277BA'],
['M355.734 543.328H281.41c-26.122 0-47.297 21.17-47.297 47.293v3.378c0 26.118 21.175 47.297 47.297 47.297h74.324c26.123 0 47.296-21.179 47.296-47.297v-3.378c0-26.123-21.174-47.293-47.296-47.293z', '#F39A2B'],
['M334.85 248.006m-48.986 0a48.986 48.986 0 1 0 97.972 0 48.986 48.986 0 1 0-97.972 0Z', '#F39A2B'],
]
/** Everything between the file's `<svg>` tags: the sheet's strokes in paint order, whitespace-folded. */
const SHEET_MARKUP = sheetMarkup
.slice(sheetMarkup.indexOf('>') + 1, sheetMarkup.lastIndexOf('</svg>'))
.trim()
.replace(/>\s+</g, '><')
/** The emblem's props, matching the harness `IconProps` the guide capsule hands it. */
export interface ContextIconProps {
@@ -44,9 +38,8 @@ export function ContextIcon({ size = 20, className }: ContextIconProps): ReactEl
className={className}
aria-hidden="true"
xmlns="http://www.w3.org/2000/svg"
>
{SHEET_PATHS.map(([d, fill]) => <path key={d} d={d} fill={fill} />)}
</svg>
dangerouslySetInnerHTML={{ __html: SHEET_MARKUP }}
/>
)
}
+21 -1
View File
@@ -1,8 +1,11 @@
// The Context emblem (src/client/icon.tsx): the bundled polychrome document
// sheet that fills the right-Sidebar tab type's two glyph seats — the guide
// capsule and the chip title. Bundled rather than read off the harness
// primitives, so these specs render the real component.
// primitives, so these specs render the real component. The same sheet is
// exported statically as the package-root icon.svg the Host's package-meta
// reader serves to the Plugins page; a spec pins the two in lockstep.
import { readFile } from 'node:fs/promises'
import { createElement as h } from 'react'
import assert from 'node:assert/strict'
import { describe, test } from 'vitest'
@@ -35,6 +38,23 @@ describe('ContextIcon', () => {
assert.equal(svg.getAttribute('class'), null)
await m.unmount()
})
test('the package-root icon.svg stays in lockstep with the component', async () => {
// The static file is what the Host's package-meta reader serves to the
// Plugins page (package.json `icon`), so a component edit that skips the
// file — or a manual file edit that skips the component — fails here.
const raw = await readFile('icon.svg', 'utf8')
const svgPaths = [...raw.matchAll(/<path d="([^"]+)" fill="([^"]+)"\/>/g)].map(([, d, fill]) => ({ d, fill }))
const m = await mount(h(ContextIcon, {}))
const componentPaths = queryAll<SVGPathElement>(m.container, 'path').map(p => ({
d: p.getAttribute('d'),
fill: p.getAttribute('fill'),
}))
await m.unmount()
assert.ok(svgPaths.length >= 10)
assert.deepEqual(svgPaths, componentPaths)
assert.match(raw, /^<svg xmlns="http:\/\/www\.w3\.org\/2000\/svg" viewBox="0 0 1024 1024">/)
})
})
describe('makeContextTabTitle — the chip-title seat', () => {
+16 -1
View File
@@ -226,7 +226,22 @@ export default defineConfig([
),
},
plugins: [{
name: 'dsh-client-bundle-purity', resolveId(source: string) {
name: 'dsh-svg-raw',
// `*.svg?raw` inlines a file's markup as its default export — the
// emblem (icon.svg) is the package's single graphic source, shared
// with the Host's package-meta reader through package.json `icon`.
resolveId(source: string, importer: string | undefined) {
if (!source.endsWith('.svg?raw')) return null
const target = source.slice(0, -'?raw'.length)
return importer === undefined ? target : resolvePath(dirname(importer), target) + '?raw'
},
load(id: string) {
if (!id.endsWith('.svg?raw')) return null
return `export default ${JSON.stringify(readFileSync(id.slice(0, -'?raw'.length), 'utf8'))}`
},
}, {
name: 'dsh-client-bundle-purity',
resolveId(source: string) {
if (!source.startsWith('@deepseek-ai/')) return null
if (isRequested(source)) return null
if (VENDORED_LIBRARY.test(source)) return null