diff --git a/icon.svg b/icon.svg new file mode 100644 index 0000000..2e5ffbc --- /dev/null +++ b/icon.svg @@ -0,0 +1,12 @@ + + + + + + + + + + + + diff --git a/package.json b/package.json index ba60db7..69b330e 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/src/client/globals.d.ts b/src/client/globals.d.ts index 83993c6..8fd1f50 100644 --- a/src/client/globals.d.ts +++ b/src/client/globals.d.ts @@ -17,3 +17,10 @@ declare module '*.module.css' { const classes: Record 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 +} diff --git a/src/client/icon.tsx b/src/client/icon.tsx index 3898e35..79cc123 100644 --- a/src/client/icon.tsx +++ b/src/client/icon.tsx @@ -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 = [ - ['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 `` tags: the sheet's strokes in paint order, whitespace-folded. */ +const SHEET_MARKUP = sheetMarkup + .slice(sheetMarkup.indexOf('>') + 1, sheetMarkup.lastIndexOf('')) + .trim() + .replace(/>\s+<') /** 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]) => )} - + dangerouslySetInnerHTML={{ __html: SHEET_MARKUP }} + /> ) } diff --git a/tests/client/icon.spec.ts b/tests/client/icon.spec.ts index 7f179aa..6622185 100644 --- a/tests/client/icon.spec.ts +++ b/tests/client/icon.spec.ts @@ -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(//g)].map(([, d, fill]) => ({ d, fill })) + const m = await mount(h(ContextIcon, {})) + const componentPaths = queryAll(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, /^/) + }) }) describe('makeContextTabTitle — the chip-title seat', () => { diff --git a/tsdown.config.ts b/tsdown.config.ts index 08b7779..3d3410b 100644 --- a/tsdown.config.ts +++ b/tsdown.config.ts @@ -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