Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 22 additions & 0 deletions packages/diagram-client/src/diagram-client.css
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
45 changes: 43 additions & 2 deletions packages/diagram-client/src/views.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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).
Expand Down Expand Up @@ -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] : [])
);
Expand Down Expand Up @@ -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] : [])
);
Expand Down
34 changes: 34 additions & 0 deletions packages/diagram-client/test/node-families.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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]);
});
});
9 changes: 9 additions & 0 deletions packages/shared/src/diagram-seams.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
Loading