diff --git a/packages/diagram-client/src/diagram-client.css b/packages/diagram-client/src/diagram-client.css index b5ac416..578e2c5 100644 --- a/packages/diagram-client/src/diagram-client.css +++ b/packages/diagram-client/src/diagram-client.css @@ -774,6 +774,28 @@ i.cal-toggle-feedback-edges.cal-feedback-hidden-icon { opacity: 0.10; } +/* ── Node center images (a family's own mark, in its own colours) ── + * Light and dark are separate images, as on the palette; VS Code stamps the + * theme on the body. The dark one is the base, so a mark with only one image + * shows it in every theme. */ +.node-center-image { + pointer-events: none; +} + +.node-center-image-light { + display: none; +} + +body.vscode-light .node-center-image.has-light .node-center-image-dark, +body.vscode-high-contrast-light .node-center-image.has-light .node-center-image-dark { + display: none; +} + +body.vscode-light .node-center-image-light, +body.vscode-high-contrast-light .node-center-image-light { + display: inline; +} + /* External actor node styles - header fill handled via header-compartment */ .external-actor-node .node-header.external-actor-header { fill: var(--workflow-color-external-default); diff --git a/packages/diagram-client/src/views.ts b/packages/diagram-client/src/views.ts index 1ab7b47..bb8fbeb 100644 --- a/packages/diagram-client/src/views.ts +++ b/packages/diagram-client/src/views.ts @@ -180,8 +180,47 @@ function renderRunRing(width: number, height: number): VNode { ); } -/** Internals reached by the family and ring tests; not part of the module's surface. */ -export const __testables = { resolveNodeFamily, renderRunRing, RUN_RING_BLEED_PX }; +/** Internals reached by the family, mark and ring tests; not part of the module's surface. */ +export const __testables = { resolveNodeFamily, renderRunRing, renderNodeCenterImage, RUN_RING_BLEED_PX }; + +/** + * Draw a family's own full-colour mark centred in the node body, where an icon + * family draws its watermark — the image its palette entry shows. + * + * Light and dark are separate images, as on the palette, so both are laid down + * and CSS shows the one for the theme (`has-light` says there is a light one + * to switch to). + */ +function renderNodeCenterImage( + family: NodeFamilySpec, + nodeWidth: number, + nodeHeight: number +): VNode[] { + const image = family.image; + if (!image?.dark) { + return []; + } + const size = NODE_ICON_SIZE; + const bodyTop = HEADER_HEIGHT; + const x = nodeWidth / 2 - size / 2; + const y = bodyTop + (nodeHeight - bodyTop) / 2 - size / 2; + const hasLight = Boolean(image.light); + const place = (href: string, theme: 'dark' | 'light'): VNode => svg('image', { + class: { [`node-center-image-${theme}`]: true }, + attrs: { href, x, y, width: size, height: size, preserveAspectRatio: 'xMidYMid meet' } + }); + + return [svg('g', { + class: { + 'node-center-image': true, + [`node-image-${family.id ?? family.annotation}`]: true, + 'has-light': hasLight + } + }, + place(image.dark, 'dark'), + ...(hasLight ? [place(image.light!, 'light')] : []) + )]; +} /** * Render a semi-transparent SVG icon centered in the node body (below the header). @@ -749,6 +788,7 @@ export class ActorNodeView extends ShapeView { // Above the body so the travelling head is not painted over by it. ...(isExecuting ? [renderRunRing(width, bodyHeight)] : []), ...(family?.icon ? [renderNodeCenterIcon(family, width, bodyHeight)] : []), + ...(family?.image ? renderNodeCenterImage(family, width, bodyHeight) : []), ...context.renderChildren(node), ...(footerLabelNode ? [footerLabelNode] : []) ); @@ -817,6 +857,7 @@ export class ExternalActorNodeView extends ShapeView { // Above the body so the travelling head is not painted over by it. ...(isExecuting ? [renderRunRing(width, bodyHeight)] : []), ...(family?.icon ? [renderNodeCenterIcon(family, width, bodyHeight)] : []), + ...(family?.image ? renderNodeCenterImage(family, width, bodyHeight) : []), ...context.renderChildren(node), ...(footerLabelNode ? [footerLabelNode] : []) ); diff --git a/packages/diagram-client/test/node-families.test.ts b/packages/diagram-client/test/node-families.test.ts index 1d0d18e..1830de6 100644 --- a/packages/diagram-client/test/node-families.test.ts +++ b/packages/diagram-client/test/node-families.test.ts @@ -120,3 +120,37 @@ describe('the CSS a declaration generates', () => { expect(nodeFamilyCss(undefined)).toBe(''); }); }); + +/** + * A family can wear its own full-colour mark — the image its palette entry + * shows — where an icon family draws a watermark. + */ +describe('a family drawn with its own mark', () => { + const { renderNodeCenterImage } = __testables; + const DARK = 'data:image/svg+xml,dark'; + const LIGHT = 'data:image/svg+xml,light'; + const family = { annotation: 'streamblocks', color: '#818a94', image: { dark: DARK, light: LIGHT } }; + + // The same reading as the ring tests: the factory keeps its data on `data` + // or `props` depending on where it runs. + const dataOf = (vnode: any): any => vnode.data ?? vnode.props; + const classesOf = (vnode: any) => + Object.entries(dataOf(vnode)?.class ?? {}).filter(([, on]) => on).map(([name]) => name); + const hrefOf = (vnode: any) => dataOf(vnode).attrs.href; + + it('lays down both images for the theme to choose between', () => { + const [group] = renderNodeCenterImage(family, 200, 100) as any[]; + const images = group.children as any[]; + + expect(classesOf(group)).toContain('has-light'); + expect(images.map(hrefOf)).toEqual([DARK, LIGHT]); + expect(images.map(classesOf)).toEqual([['node-center-image-dark'], ['node-center-image-light']]); + }); + + it('shows the dark one everywhere when there is no light one', () => { + const [group] = renderNodeCenterImage({ ...family, image: { dark: DARK } }, 200, 100) as any[]; + + expect(classesOf(group)).not.toContain('has-light'); + expect((group.children as any[]).map(hrefOf)).toEqual([DARK]); + }); +}); diff --git a/packages/shared/src/diagram-seams.ts b/packages/shared/src/diagram-seams.ts index 6ec9013..ba5f9b0 100644 --- a/packages/shared/src/diagram-seams.ts +++ b/packages/shared/src/diagram-seams.ts @@ -194,6 +194,15 @@ export interface NodeFamilySpec { viewBox: number; paths: string[]; }; + /** + * The family's own full-colour mark, drawn in the node body where an icon + * would be — the image a palette entry shows, one per theme (`dark` is the + * base, `light` is optional). + */ + image?: { + dark: string; + light?: string; + }; } export interface EntityPaletteItemSpec {