Skip to content

Commit 04005a5

Browse files
authored
feat: enhance block variable styling and add tests for structured content fields (#3186)
1 parent 8786b70 commit 04005a5

9 files changed

Lines changed: 249 additions & 12 deletions

File tree

packages/layout-engine/painters/dom/src/index.test.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12595,7 +12595,7 @@ describe('applyRunDataAttributes', () => {
1259512595
type: 'structuredContent',
1259612596
scope: 'block',
1259712597
id: 'scb-block-1',
12598-
tag: 'dropdown',
12598+
tag: '{"fieldType":"signer"}',
1259912599
alias: 'Block Content Control',
1260012600
},
1260112601
},
@@ -12652,10 +12652,12 @@ describe('applyRunDataAttributes', () => {
1265212652
expect(fragment.dataset.sdtType).toBe('structuredContent');
1265312653
expect(fragment.dataset.sdtScope).toBe('block');
1265412654
expect(fragment.dataset.sdtId).toBe('scb-block-1');
12655+
expect(fragment.dataset.sdtTag).toBe('{"fieldType":"signer"}');
1265512656

1265612657
// Should have the label element
1265712658
const label = fragment.querySelector('.superdoc-structured-content__label') as HTMLElement;
1265812659
expect(label).toBeTruthy();
12660+
expect(label.classList.contains('superdoc-structured-content-block__label')).toBe(true);
1265912661
expect(label.textContent).toBe('Block Content Control');
1266012662

1266112663
// Should have container boundary markers

packages/layout-engine/painters/dom/src/styles.test.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,16 @@ describe('lineStyles', () => {
1515
});
1616

1717
describe('ensureSdtContainerStyles', () => {
18+
it('exposes hover border tokens for structured content overrides', () => {
19+
ensureSdtContainerStyles(document);
20+
21+
const styleEl = document.querySelector('[data-superdoc-sdt-container-styles="true"]');
22+
const cssText = styleEl?.textContent ?? '';
23+
24+
expect(cssText).toContain('border-color: var(--sd-content-controls-block-hover-border, transparent);');
25+
expect(cssText).toContain('border-color: var(--sd-content-controls-inline-hover-border, transparent);');
26+
});
27+
1828
it('suppresses structured-content hover backgrounds in viewing mode, including grouped hover', () => {
1929
ensureSdtContainerStyles(document);
2030

packages/layout-engine/painters/dom/src/styles.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -506,13 +506,13 @@ const SDT_CONTAINER_STYLES = `
506506
507507
.superdoc-structured-content-block:not(.ProseMirror-selectednode):hover {
508508
background-color: var(--sd-content-controls-block-hover-bg, #f2f2f2);
509-
border-color: transparent;
509+
border-color: var(--sd-content-controls-block-hover-border, transparent);
510510
}
511511
512512
/* Group hover (JavaScript-coordinated via PresentationEditor) */
513513
.superdoc-structured-content-block.sdt-group-hover:not(.ProseMirror-selectednode) {
514514
background-color: var(--sd-content-controls-block-hover-bg, #f2f2f2);
515-
border-color: transparent;
515+
border-color: var(--sd-content-controls-block-hover-border, transparent);
516516
}
517517
518518
.superdoc-structured-content-block.ProseMirror-selectednode {
@@ -606,7 +606,7 @@ const SDT_CONTAINER_STYLES = `
606606
/* Hover effect for inline structured content */
607607
.superdoc-structured-content-inline:not(.ProseMirror-selectednode):hover {
608608
background-color: var(--sd-content-controls-inline-hover-bg, #f2f2f2);
609-
border-color: transparent;
609+
border-color: var(--sd-content-controls-inline-hover-border, transparent);
610610
}
611611
612612
.superdoc-structured-content-inline.ProseMirror-selectednode {

packages/layout-engine/painters/dom/src/utils/sdt-helpers.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -99,7 +99,7 @@ export function getSdtContainerConfig(sdt: SdtMetadata | null | undefined): SdtC
9999
return {
100100
className: 'superdoc-structured-content-block',
101101
labelText: sdt.alias ?? 'Structured content',
102-
labelClassName: 'superdoc-structured-content__label',
102+
labelClassName: 'superdoc-structured-content__label superdoc-structured-content-block__label',
103103
isStart: true,
104104
isEnd: true,
105105
};

packages/superdoc/src/assets/styles/helpers/variables.css

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -197,8 +197,10 @@
197197

198198
/* Styles: content controls (SDT) — blue accent, intentionally standalone */
199199
--sd-content-controls-block-border: #629be7;
200+
--sd-content-controls-block-hover-border: transparent;
200201
--sd-content-controls-block-hover-bg: var(--sd-ui-hover-bg);
201202
--sd-content-controls-inline-border: #629be7;
203+
--sd-content-controls-inline-hover-border: transparent;
202204
--sd-content-controls-inline-hover-bg: var(--sd-ui-hover-bg);
203205
--sd-content-controls-label-border: #629be7;
204206
--sd-content-controls-label-bg: #629be7ee;
Lines changed: 28 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,26 +1,52 @@
11
.superdoc-structured-content-inline.superdoc-structured-content-inline,
22
.superdoc-structured-content-block.superdoc-structured-content-block {
33
border-color: var(--superdoc-field-owner-color, #629be7);
4+
--sd-content-controls-inline-border: var(--superdoc-field-owner-color, #629be7);
5+
--sd-content-controls-inline-hover-border: var(--superdoc-field-owner-color, #629be7);
6+
--sd-content-controls-inline-hover-bg: color-mix(in srgb, var(--superdoc-field-owner-color, #629be7) 8%, transparent);
7+
--sd-content-controls-block-border: var(--superdoc-field-owner-color, #629be7);
8+
--sd-content-controls-block-hover-border: var(--superdoc-field-owner-color, #629be7);
9+
--sd-content-controls-block-hover-bg: color-mix(in srgb, var(--superdoc-field-owner-color, #629be7) 8%, transparent);
10+
--sd-content-controls-lock-hover-bg: color-mix(in srgb, var(--superdoc-field-owner-color, #629be7) 8%, transparent);
11+
--sd-content-controls-label-border: var(--superdoc-field-owner-color, #629be7);
12+
--sd-content-controls-label-bg: color-mix(in srgb, var(--superdoc-field-owner-color, #629be7) 87%, transparent);
413
}
514
.superdoc-structured-content-inline.superdoc-structured-content-inline:hover,
615
.superdoc-structured-content-block.superdoc-structured-content-block:hover {
716
border-color: var(--superdoc-field-owner-color, #629be7);
817
}
918
.superdoc-structured-content-inline.superdoc-structured-content-inline .superdoc-structured-content-inline__label,
10-
.superdoc-structured-content-block.superdoc-structured-content-block .superdoc-structured-content-block__label {
19+
.superdoc-structured-content-block.superdoc-structured-content-block .superdoc-structured-content-block__label,
20+
.superdoc-structured-content-block.superdoc-structured-content-block .superdoc-structured-content__label {
1121
border-color: var(--superdoc-field-owner-color, #629be7);
1222
background-color: color-mix(in srgb, var(--superdoc-field-owner-color, #629be7) 87%, transparent);
23+
color: var(--sd-content-controls-label-text, #ffffff);
1324
}
1425
.superdoc-structured-content-inline[data-sdt-tag*='"fieldType":"signer"'],
1526
.superdoc-structured-content-block[data-sdt-tag*='"fieldType":"signer"'] {
1627
border-color: var(--superdoc-field-signer-color, #d97706);
28+
--sd-content-controls-inline-border: var(--superdoc-field-signer-color, #d97706);
29+
--sd-content-controls-inline-hover-border: var(--superdoc-field-signer-color, #d97706);
30+
--sd-content-controls-inline-hover-bg: color-mix(
31+
in srgb,
32+
var(--superdoc-field-signer-color, #d97706) 8%,
33+
transparent
34+
);
35+
--sd-content-controls-block-border: var(--superdoc-field-signer-color, #d97706);
36+
--sd-content-controls-block-hover-border: var(--superdoc-field-signer-color, #d97706);
37+
--sd-content-controls-block-hover-bg: color-mix(in srgb, var(--superdoc-field-signer-color, #d97706) 8%, transparent);
38+
--sd-content-controls-lock-hover-bg: color-mix(in srgb, var(--superdoc-field-signer-color, #d97706) 8%, transparent);
39+
--sd-content-controls-label-border: var(--superdoc-field-signer-color, #d97706);
40+
--sd-content-controls-label-bg: color-mix(in srgb, var(--superdoc-field-signer-color, #d97706) 87%, transparent);
1741
}
1842
.superdoc-structured-content-inline[data-sdt-tag*='"fieldType":"signer"']:hover,
1943
.superdoc-structured-content-block[data-sdt-tag*='"fieldType":"signer"']:hover {
2044
border-color: var(--superdoc-field-signer-color, #d97706);
2145
}
2246
.superdoc-structured-content-inline[data-sdt-tag*='"fieldType":"signer"'] .superdoc-structured-content-inline__label,
23-
.superdoc-structured-content-block[data-sdt-tag*='"fieldType":"signer"'] .superdoc-structured-content-block__label {
47+
.superdoc-structured-content-block[data-sdt-tag*='"fieldType":"signer"'] .superdoc-structured-content-block__label,
48+
.superdoc-structured-content-block[data-sdt-tag*='"fieldType":"signer"'] .superdoc-structured-content__label {
2449
border-color: var(--superdoc-field-signer-color, #d97706);
2550
background-color: color-mix(in srgb, var(--superdoc-field-signer-color, #d97706) 87%, transparent);
51+
color: var(--sd-content-controls-label-text, #ffffff);
2652
}

packages/template-builder/src/tests/utils.test.ts

Lines changed: 24 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -225,13 +225,35 @@ describe('generateFieldColorCSS', () => {
225225
it('uses correct label selectors for inline and block', () => {
226226
const css = generateFieldColorCSS({ owner: '#629be7' }, '.scope');
227227
expect(css).toContain('.superdoc-structured-content-inline__label');
228+
expect(css).toContain('.superdoc-structured-content-block__label');
228229
expect(css).toContain('.superdoc-structured-content__label');
229-
// Should NOT contain the wrong block label class
230-
expect(css).not.toContain('.superdoc-structured-content-block__label');
231230
});
232231

233232
it('uses color-mix for label backgrounds', () => {
234233
const css = generateFieldColorCSS({ owner: '#629be7' }, '.scope');
235234
expect(css).toContain('color-mix(in srgb, #629be7 87%, transparent)');
236235
});
236+
237+
it('sets block styling variables from field colors', () => {
238+
const css = generateFieldColorCSS({ signer: '#d97706' }, '.scope');
239+
240+
expect(css).toContain('--sd-content-controls-block-border: #d97706;');
241+
expect(css).toContain('--sd-content-controls-block-hover-border: #d97706;');
242+
expect(css).toContain('--sd-content-controls-block-hover-bg: color-mix(in srgb, #d97706 8%, transparent);');
243+
expect(css).toContain('--sd-content-controls-lock-hover-bg: color-mix(in srgb, #d97706 8%, transparent);');
244+
});
245+
246+
it('keeps label text color configurable through the content control token', () => {
247+
const css = generateFieldColorCSS({ owner: '#629be7' }, '.scope');
248+
249+
expect(css).toContain('color: var(--sd-content-controls-label-text, #ffffff);');
250+
});
251+
252+
it('includes selected block border rules for field colors', () => {
253+
const css = generateFieldColorCSS({ signer: '#d97706' }, '.scope');
254+
255+
expect(css).toContain(
256+
'.scope .superdoc-structured-content-block[data-sdt-tag*=\'"fieldType":"signer"\'].ProseMirror-selectednode',
257+
);
258+
});
237259
});

packages/template-builder/src/utils.ts

Lines changed: 23 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -82,22 +82,42 @@ export const getFieldTypeStyle = (fieldType: string, fieldColors?: Record<string
8282
const SDT_INLINE = '.superdoc-structured-content-inline';
8383
const SDT_BLOCK = '.superdoc-structured-content-block';
8484
const INLINE_LABEL = '.superdoc-structured-content-inline__label';
85-
const BLOCK_LABEL = '.superdoc-structured-content__label';
85+
const BLOCK_LABEL = '.superdoc-structured-content-block__label';
86+
// Keep legacy selector support while we align the structured-content label API.
87+
const LEGACY_BLOCK_LABEL = '.superdoc-structured-content__label';
8688

8789
function buildColorRules(scope: string, selector: string, color: string): string {
90+
const hoverFill = `color-mix(in srgb, ${color} 8%, transparent)`;
91+
const labelFill = `color-mix(in srgb, ${color} 87%, transparent)`;
92+
8893
return `
8994
${scope} ${SDT_INLINE}${selector},
9095
${scope} ${SDT_BLOCK}${selector} {
9196
border-color: ${color};
97+
--sd-content-controls-inline-border: ${color};
98+
--sd-content-controls-inline-hover-border: ${color};
99+
--sd-content-controls-inline-hover-bg: ${hoverFill};
100+
--sd-content-controls-block-border: ${color};
101+
--sd-content-controls-block-hover-border: ${color};
102+
--sd-content-controls-block-hover-bg: ${hoverFill};
103+
--sd-content-controls-lock-hover-bg: ${hoverFill};
104+
--sd-content-controls-label-border: ${color};
105+
--sd-content-controls-label-bg: ${labelFill};
92106
}
93107
${scope} ${SDT_INLINE}${selector}:hover,
94108
${scope} ${SDT_BLOCK}${selector}:hover {
95109
border-color: ${color};
96110
}
111+
${scope} ${SDT_INLINE}${selector}.ProseMirror-selectednode,
112+
${scope} ${SDT_BLOCK}${selector}.ProseMirror-selectednode {
113+
border-color: ${color};
114+
}
97115
${scope} ${SDT_INLINE}${selector} ${INLINE_LABEL},
98-
${scope} ${SDT_BLOCK}${selector} ${BLOCK_LABEL} {
116+
${scope} ${SDT_BLOCK}${selector} ${BLOCK_LABEL},
117+
${scope} ${SDT_BLOCK}${selector} ${LEGACY_BLOCK_LABEL} {
99118
border-color: ${color};
100-
background-color: color-mix(in srgb, ${color} 87%, transparent);
119+
background-color: ${labelFill};
120+
color: var(--sd-content-controls-label-text, #ffffff);
101121
}`;
102122
}
103123

Lines changed: 155 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,155 @@
1+
import fs from 'node:fs';
2+
import path from 'node:path';
3+
import { fileURLToPath } from 'node:url';
4+
import { expect, test } from '../fixtures/superdoc.js';
5+
6+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
7+
const repoRoot = path.resolve(__dirname, '../../../..');
8+
const fieldTypesCss = fs.readFileSync(
9+
path.join(repoRoot, 'packages/template-builder/src/styles/field-types.css'),
10+
'utf8',
11+
);
12+
13+
test('inline and block structured content field chrome use Template Builder field styles', async ({ superdoc }) => {
14+
await superdoc.page.addStyleTag({
15+
content: `
16+
:root {
17+
--superdoc-field-owner-color: rgb(37, 99, 235);
18+
--superdoc-field-signer-color: rgb(220, 38, 38);
19+
--sd-content-controls-label-text: rgb(255, 255, 0);
20+
}
21+
${fieldTypesCss}
22+
`,
23+
});
24+
25+
await superdoc.page.evaluate(() => {
26+
const fieldAttrs = (id: string, alias: string, fieldType: 'owner' | 'signer') => ({
27+
id,
28+
alias,
29+
tag: `{"fieldType":"${fieldType}"}`,
30+
});
31+
32+
const inlineField = (id: string, alias: string, fieldType: 'owner' | 'signer', text: string) => ({
33+
type: 'paragraph',
34+
content: [
35+
{
36+
type: 'structuredContent',
37+
attrs: fieldAttrs(id, alias, fieldType),
38+
content: [{ type: 'text', text }],
39+
},
40+
],
41+
});
42+
43+
const blockField = (id: string, alias: string, fieldType: 'owner' | 'signer', text: string) => ({
44+
type: 'structuredContentBlock',
45+
attrs: fieldAttrs(id, alias, fieldType),
46+
content: [
47+
{
48+
type: 'paragraph',
49+
content: [{ type: 'text', text }],
50+
},
51+
],
52+
});
53+
54+
const spacer = () => ({ type: 'paragraph', content: [{ type: 'text', text: ' ' }] });
55+
56+
const editor = (window as any).editor;
57+
const doc = editor.schema.nodeFromJSON({
58+
type: 'doc',
59+
content: [
60+
inlineField('1', 'Signer Inline', 'signer', 'Signer inline value'),
61+
spacer(),
62+
spacer(),
63+
blockField('2', 'Signer Block', 'signer', 'Signature area'),
64+
spacer(),
65+
spacer(),
66+
inlineField('3', 'Owner Inline', 'owner', 'Owner inline value'),
67+
spacer(),
68+
spacer(),
69+
blockField('4', 'Owner Block', 'owner', 'Owner approval area'),
70+
],
71+
});
72+
editor.view.dispatch(editor.state.tr.replaceWith(0, editor.state.doc.content.size, doc.content));
73+
});
74+
await superdoc.waitForStable();
75+
76+
const renderedPage = superdoc.page.locator('.superdoc-page').first();
77+
const getInline = (fieldType: 'owner' | 'signer') =>
78+
renderedPage.locator(`.superdoc-structured-content-inline[data-sdt-tag*='"fieldType":"${fieldType}"']`).first();
79+
const getBlock = (fieldType: 'owner' | 'signer') =>
80+
renderedPage.locator(`.superdoc-structured-content-block[data-sdt-tag*='"fieldType":"${fieldType}"']`).first();
81+
82+
const showInlineLabel = async (fieldType: 'owner' | 'signer') => {
83+
const inline = getInline(fieldType);
84+
await inline.evaluate((el) => el.classList.add('ProseMirror-selectednode'));
85+
return inline;
86+
};
87+
88+
const showBlockLabel = async (fieldType: 'owner' | 'signer') => {
89+
const block = getBlock(fieldType);
90+
await block.evaluate((el) => {
91+
el.classList.remove('sdt-group-hover');
92+
el.classList.add('ProseMirror-selectednode');
93+
});
94+
return block;
95+
};
96+
97+
const expectHoverBackgroundParity = async (fieldType: 'owner' | 'signer') => {
98+
const inline = getInline(fieldType);
99+
const block = getBlock(fieldType);
100+
101+
await inline.evaluate((el) => el.classList.remove('ProseMirror-selectednode'));
102+
await inline.hover();
103+
const inlineHoverBackground = await inline.evaluate((el) => getComputedStyle(el).backgroundColor);
104+
105+
await block.evaluate((el) => {
106+
el.classList.remove('ProseMirror-selectednode');
107+
el.classList.add('sdt-group-hover');
108+
});
109+
await expect
110+
.poll(async () => block.evaluate((el) => getComputedStyle(el).backgroundColor))
111+
.toBe(inlineHoverBackground);
112+
113+
await inline.evaluate((el) => el.classList.add('ProseMirror-selectednode'));
114+
await block.evaluate((el) => {
115+
el.classList.remove('sdt-group-hover');
116+
el.classList.add('ProseMirror-selectednode');
117+
});
118+
};
119+
120+
const signerInline = await showInlineLabel('signer');
121+
await expect(signerInline).toBeVisible();
122+
await expect(signerInline).toHaveCSS('border-color', 'rgb(220, 38, 38)');
123+
124+
const signerInlineLabel = signerInline.locator('.superdoc-structured-content-inline__label');
125+
await expect(signerInlineLabel).toHaveCSS('color', 'rgb(255, 255, 0)');
126+
await expect(signerInlineLabel).toHaveCSS('border-color', 'rgb(220, 38, 38)');
127+
128+
const signerBlock = await showBlockLabel('signer');
129+
await expect(signerBlock).toBeVisible();
130+
await expect(signerBlock).toHaveCSS('border-top-color', 'rgb(220, 38, 38)');
131+
132+
const signerBlockLabel = signerBlock.locator('.superdoc-structured-content-block__label');
133+
await expect(signerBlockLabel).toHaveCSS('color', 'rgb(255, 255, 0)');
134+
await expect(signerBlockLabel).toHaveCSS('border-color', 'rgb(220, 38, 38)');
135+
await expectHoverBackgroundParity('signer');
136+
137+
const ownerInline = await showInlineLabel('owner');
138+
await expect(ownerInline).toBeVisible();
139+
await expect(ownerInline).toHaveCSS('border-color', 'rgb(37, 99, 235)');
140+
141+
const ownerInlineLabel = ownerInline.locator('.superdoc-structured-content-inline__label');
142+
await expect(ownerInlineLabel).toHaveCSS('color', 'rgb(255, 255, 0)');
143+
await expect(ownerInlineLabel).toHaveCSS('border-color', 'rgb(37, 99, 235)');
144+
145+
const ownerBlock = await showBlockLabel('owner');
146+
await expect(ownerBlock).toBeVisible();
147+
await expect(ownerBlock).toHaveCSS('border-top-color', 'rgb(37, 99, 235)');
148+
149+
const ownerBlockLabel = ownerBlock.locator('.superdoc-structured-content-block__label');
150+
await expect(ownerBlockLabel).toHaveCSS('color', 'rgb(255, 255, 0)');
151+
await expect(ownerBlockLabel).toHaveCSS('border-color', 'rgb(37, 99, 235)');
152+
await expectHoverBackgroundParity('owner');
153+
154+
await superdoc.screenshot('template-builder-owner-and-signer-inline-and-block-field-styling');
155+
});

0 commit comments

Comments
 (0)