Skip to content

Commit b1f39e7

Browse files
authored
Merge pull request #3590 from superdoc-dev/caio/sd-3322-content-controls-chrome-none-css-vars
feat(painter-dom): custom SDT styling variables under chrome:'none' (SD-3322)
2 parents 7348c74 + 928475b commit b1f39e7

5 files changed

Lines changed: 155 additions & 20 deletions

File tree

apps/docs/editor/custom-ui/content-controls.mdx

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,33 @@ new SuperDoc({
3030

3131
The event tells you *what* is active; `getRect` tells you *where* to draw. `active` is an `SdtRef` with `id`, `tag`, `alias`, `controlType`, and `scope`.
3232

33+
## Style the controls in place
34+
35+
Turning off chrome erases the built-in look, including hover and selection. To paint your own field and clause look, set `--sd-content-controls-custom-*` variables on the painted wrapper. Target it by your own `data-sdt-*` attributes. No `!important`, and no need to touch SuperDoc's internal state classes: the painter applies your variables across rest, hover, and selected, so the box stays stable and you never write `.ProseMirror-selectednode` or hover rules yourself.
36+
37+
```css
38+
/* A field your app tagged { kind: 'smartField', ... } */
39+
.superdoc-cc-chrome-none .superdoc-structured-content-inline[data-sdt-tag*='smartField'] {
40+
--sd-content-controls-custom-inline-border: 1px solid #1355ff;
41+
--sd-content-controls-custom-inline-bg: color-mix(in srgb, #1355ff 12%, transparent);
42+
--sd-content-controls-custom-inline-hover-bg: color-mix(in srgb, #1355ff 20%, transparent);
43+
--sd-content-controls-custom-inline-radius: 4px;
44+
--sd-content-controls-custom-inline-padding: 1px 6px;
45+
}
46+
47+
/* A clause your app tagged { kind: 'reusableSection', ... } */
48+
.superdoc-cc-chrome-none .superdoc-structured-content-block[data-sdt-tag*='reusableSection'] {
49+
--sd-content-controls-custom-block-border: 1px solid #d6e0ff;
50+
--sd-content-controls-custom-block-border-left: 4px solid #1355ff; /* accent rail */
51+
--sd-content-controls-custom-block-bg: color-mix(in srgb, #1355ff 4%, transparent);
52+
--sd-content-controls-custom-block-radius: 6px;
53+
}
54+
```
55+
56+
`border` is a full CSS shorthand; `border-left` is an optional accent rail for block clauses. The background variables cascade, so set only what changes: `-hover-bg` defaults to `-bg`, and `-selected-bg` defaults to `-hover-bg`.
57+
58+
This is the path for `chrome: 'none'`. To theme the **built-in** chrome instead (`chrome: 'default'`), use the `--sd-content-controls-*` variables (without `custom`).
59+
3360
## Pick the right surface
3461

3562
| Goal | API |

apps/docs/editor/theming/custom-themes.mdx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -318,6 +318,8 @@ If you also want tracked-change text inside comment threads to match, set `--sd-
318318

319319
DOCX content controls (SDTs): form fields, dropdowns, date pickers.
320320

321+
These theme the **built-in** chrome (`modules.contentControls.chrome: 'default'`). If you turn the chrome off (`chrome: 'none'`) to draw your own field/clause look, style the controls with the `--sd-content-controls-custom-*` variables instead. See [Custom UI > Content controls](/editor/custom-ui/content-controls).
322+
321323
| Variable | Default | Controls |
322324
|----------|---------|----------|
323325
| `--sd-content-controls-block-border` | `#629be7` | Block control border |

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

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -322,6 +322,60 @@ describe('ensureSdtContainerStyles', () => {
322322
expect(lastChromeShowing).toBeGreaterThan(-1);
323323
expect(chromeNoneSuppression).toBeGreaterThan(lastChromeShowing);
324324
});
325+
326+
it('exposes a --sd-content-controls-custom-* styling surface under chrome-none (SD-3322)', () => {
327+
ensureSdtContainerStyles(document);
328+
const styleEl = document.querySelector('[data-superdoc-sdt-container-styles="true"]');
329+
const cssText = styleEl?.textContent ?? '';
330+
331+
// Inline rest reads the custom vars; the default-preserving fallbacks
332+
// (0-width transparent border, no background/radius/padding) keep
333+
// chrome-none visually empty when no variable is set.
334+
expect(cssText).toContain('background: var(--sd-content-controls-custom-inline-bg, none);');
335+
expect(cssText).toContain('border: var(--sd-content-controls-custom-inline-border, 0 solid transparent);');
336+
expect(cssText).toContain('padding: var(--sd-content-controls-custom-inline-padding, 0);');
337+
expect(cssText).toContain('border-radius: var(--sd-content-controls-custom-inline-radius, 0);');
338+
339+
// Hover and selected re-assert the SAME border var (constant box, no jitter)
340+
// and read the background vars, which cascade from the rest background.
341+
expect(cssText).toContain(
342+
'background: var(--sd-content-controls-custom-inline-hover-bg, var(--sd-content-controls-custom-inline-bg, none));',
343+
);
344+
expect(cssText).toContain(
345+
'background: var(--sd-content-controls-custom-inline-selected-bg, var(--sd-content-controls-custom-inline-hover-bg, var(--sd-content-controls-custom-inline-bg, none)));',
346+
);
347+
348+
// Block exposes the same set plus an accent rail (-border-left) that falls
349+
// back to the regular border.
350+
expect(cssText).toContain('background: var(--sd-content-controls-custom-block-bg, none);');
351+
expect(cssText).toContain(
352+
'border-left: var(--sd-content-controls-custom-block-border-left, var(--sd-content-controls-custom-block-border, 0 solid transparent));',
353+
);
354+
});
355+
356+
it('locked-hover under chrome-none follows the custom hover background, not the built-in lock-hover (SD-3322)', () => {
357+
ensureSdtContainerStyles(document);
358+
const styleEl = document.querySelector('[data-superdoc-sdt-container-styles="true"]');
359+
const cssText = styleEl?.textContent ?? '';
360+
361+
// The base lock-hover rules (built-in tint on inline, transparent on block)
362+
// come first and have equal specificity to the plain custom hover rules, so
363+
// they would otherwise win for locked controls.
364+
const baseInlineLockHover = cssText.indexOf('background-color: var(--sd-content-controls-lock-hover-bg');
365+
const baseBlockLockHover = cssText.indexOf(
366+
'.superdoc-structured-content-block[data-lock-mode].sdt-group-hover:not(.ProseMirror-selectednode) {',
367+
);
368+
expect(baseInlineLockHover).toBeGreaterThan(-1);
369+
expect(baseBlockLockHover).toBeGreaterThan(-1);
370+
371+
// The chrome-none lock-hover reset re-asserts the custom hover background
372+
// AFTER them (extra .superdoc-cc-chrome-none class + later source order wins),
373+
// so a locked control under chrome:'none' uses the custom variable.
374+
const customInlineHoverReassert = cssText.lastIndexOf('--sd-content-controls-custom-inline-hover-bg');
375+
const customBlockHoverReassert = cssText.lastIndexOf('--sd-content-controls-custom-block-hover-bg');
376+
expect(customInlineHoverReassert).toBeGreaterThan(baseInlineLockHover);
377+
expect(customBlockHoverReassert).toBeGreaterThan(baseBlockLockHover);
378+
});
325379
});
326380

327381
describe('ensureTrackChangeStyles', () => {

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

Lines changed: 54 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -787,30 +787,56 @@ const SDT_CONTAINER_STYLES = `
787787
/* Global content-control chrome opt-out: preserve SDT wrappers/datasets while
788788
* suppressing built-in visual chrome on structured-content controls. Their
789789
* label elements are not emitted by renderer/helpers when this class is
790-
* present (DOM non-emission), and these rules neutralize
791-
* border/padding/hover/selection visuals. documentSection chrome (e.g. the
792-
* locked-section tooltip) is intentionally preserved and not in scope. */
793-
.superdoc-cc-chrome-none .superdoc-structured-content-inline,
790+
* present (DOM non-emission). documentSection chrome (e.g. the locked-section
791+
* tooltip) is intentionally preserved and not in scope.
792+
*
793+
* Custom styling surface (SD-3322): instead of fully erasing the look, these
794+
* rules read --sd-content-controls-custom-* variables whose defaults reproduce
795+
* the empty look (0-width transparent border, no background, no radius/padding).
796+
* So chrome:'none' stays visually empty by default, but a consumer can paint
797+
* their own field/clause look by setting those variables on the painted wrapper
798+
* (target it via data-sdt-* attributes) - no !important, and no need to fight
799+
* the .ProseMirror-selectednode / .sdt-group-hover state classes, because the
800+
* painter reads the variables across rest, hover, and selected. The border is a
801+
* full shorthand (e.g. "1px solid #1355ff"); its default "0 solid transparent"
802+
* is identical in layout to no border. It's re-asserted in every state so the
803+
* box never shifts (no jitter); only the background changes on hover/selected.
804+
* Block controls add a -border-left override for an accent rail. */
805+
.superdoc-cc-chrome-none .superdoc-structured-content-inline {
806+
padding: var(--sd-content-controls-custom-inline-padding, 0);
807+
border: var(--sd-content-controls-custom-inline-border, 0 solid transparent);
808+
border-radius: var(--sd-content-controls-custom-inline-radius, 0);
809+
background: var(--sd-content-controls-custom-inline-bg, none);
810+
}
794811
.superdoc-cc-chrome-none .superdoc-structured-content-block {
795-
border: none;
796-
padding: 0;
797-
border-radius: 0;
798-
background: none;
812+
padding: var(--sd-content-controls-custom-block-padding, 0);
813+
border: var(--sd-content-controls-custom-block-border, 0 solid transparent);
814+
border-left: var(--sd-content-controls-custom-block-border-left, var(--sd-content-controls-custom-block-border, 0 solid transparent));
815+
border-radius: var(--sd-content-controls-custom-block-radius, 0);
816+
background: var(--sd-content-controls-custom-block-bg, none);
799817
}
800818
801819
.superdoc-cc-chrome-none .superdoc-structured-content-inline:hover,
820+
.superdoc-cc-chrome-none .superdoc-structured-content-inline[data-lock-mode]:hover {
821+
border: var(--sd-content-controls-custom-inline-border, 0 solid transparent);
822+
background: var(--sd-content-controls-custom-inline-hover-bg, var(--sd-content-controls-custom-inline-bg, none));
823+
}
802824
.superdoc-cc-chrome-none .superdoc-structured-content-block:hover,
803825
.superdoc-cc-chrome-none .superdoc-structured-content-block.sdt-group-hover,
804-
.superdoc-cc-chrome-none .superdoc-structured-content-block[data-lock-mode].sdt-group-hover,
805-
.superdoc-cc-chrome-none .superdoc-structured-content-inline[data-lock-mode]:hover {
806-
border: none;
807-
background: none;
826+
.superdoc-cc-chrome-none .superdoc-structured-content-block[data-lock-mode].sdt-group-hover {
827+
border: var(--sd-content-controls-custom-block-border, 0 solid transparent);
828+
border-left: var(--sd-content-controls-custom-block-border-left, var(--sd-content-controls-custom-block-border, 0 solid transparent));
829+
background: var(--sd-content-controls-custom-block-hover-bg, var(--sd-content-controls-custom-block-bg, none));
808830
}
809831
810-
.superdoc-cc-chrome-none .superdoc-structured-content-inline.ProseMirror-selectednode,
832+
.superdoc-cc-chrome-none .superdoc-structured-content-inline.ProseMirror-selectednode {
833+
border: var(--sd-content-controls-custom-inline-border, 0 solid transparent);
834+
background: var(--sd-content-controls-custom-inline-selected-bg, var(--sd-content-controls-custom-inline-hover-bg, var(--sd-content-controls-custom-inline-bg, none)));
835+
}
811836
.superdoc-cc-chrome-none .superdoc-structured-content-block.ProseMirror-selectednode {
812-
border-color: transparent;
813-
background: none;
837+
border: var(--sd-content-controls-custom-block-border, 0 solid transparent);
838+
border-left: var(--sd-content-controls-custom-block-border-left, var(--sd-content-controls-custom-block-border, 0 solid transparent));
839+
background: var(--sd-content-controls-custom-block-selected-bg, var(--sd-content-controls-custom-block-hover-bg, var(--sd-content-controls-custom-block-bg, none)));
814840
}
815841
816842
/* Hover highlight for SDT containers.
@@ -859,11 +885,20 @@ const SDT_CONTAINER_STYLES = `
859885
border: none;
860886
}
861887
862-
/* Reset the lock-hover z-index boost so a suppressed SDT does not stack
863-
* above host-attached custom UI. Mirrors the base lock-hover selectors with
864-
* the chrome-none prefix so specificity stays above the boost rule. */
865-
.superdoc-cc-chrome-none .superdoc-structured-content-block[data-lock-mode].sdt-group-hover:not(.ProseMirror-selectednode),
888+
/* Chrome opt-out for the lock-hover affordance. The base lock-hover rules above
889+
* paint a built-in tint and boost z-index on hovered locked controls; under
890+
* chrome:'none' that would override the custom hover background and stack above
891+
* host-attached UI. Re-assert the custom hover background (so a locked control
892+
* follows --sd-content-controls-custom-*-hover-bg, defaulting to empty - no tint
893+
* leaks) and reset the z-index. Mirrors the base lock-hover selectors with the
894+
* chrome-none prefix, so the extra class wins over the base rules. Split inline
895+
* vs block because each reads its own hover variable. */
866896
.superdoc-cc-chrome-none .superdoc-structured-content-inline[data-lock-mode]:hover:not(.ProseMirror-selectednode, [data-appearance='hidden']) {
897+
background: var(--sd-content-controls-custom-inline-hover-bg, var(--sd-content-controls-custom-inline-bg, none));
898+
z-index: auto;
899+
}
900+
.superdoc-cc-chrome-none .superdoc-structured-content-block[data-lock-mode].sdt-group-hover:not(.ProseMirror-selectednode) {
901+
background: var(--sd-content-controls-custom-block-hover-bg, var(--sd-content-controls-custom-block-bg, none));
867902
z-index: auto;
868903
}
869904

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

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -199,7 +199,8 @@
199199
--sd-tracked-changes-delete-background-focused: #cb0e4744;
200200
--sd-tracked-changes-format-background-focused: #ffd70033;
201201

202-
/* Styles: content controls (SDT) — blue accent, intentionally standalone */
202+
/* Styles: content controls (SDT) — blue accent, intentionally standalone.
203+
These theme the BUILT-IN chrome (modules.contentControls.chrome: 'default'). */
203204
--sd-content-controls-block-border: #629be7;
204205
--sd-content-controls-block-hover-border: transparent;
205206
--sd-content-controls-block-hover-bg: var(--sd-ui-hover-bg);
@@ -211,6 +212,22 @@
211212
--sd-content-controls-label-text: #ffffff;
212213
--sd-content-controls-lock-hover-bg: rgba(98, 155, 231, 0.08);
213214

215+
/* Custom SDT styling under chrome:'none' (SD-3322). The built-in chrome is
216+
off, so set these on the painted wrapper (target via data-sdt-* attributes)
217+
to paint your own field/clause look. The painter applies them across rest,
218+
hover, and selected, so no !important and no state-class selectors are
219+
needed. Unset by default (chrome:'none' stays visually empty). `border` is a
220+
full shorthand, e.g. `1px solid #1355ff`; block adds a `-border-left` for an
221+
accent rail. Inline:
222+
--sd-content-controls-custom-inline-bg
223+
--sd-content-controls-custom-inline-border
224+
--sd-content-controls-custom-inline-radius
225+
--sd-content-controls-custom-inline-padding
226+
--sd-content-controls-custom-inline-hover-bg
227+
--sd-content-controls-custom-inline-selected-bg
228+
Block (same set, plus):
229+
--sd-content-controls-custom-block-border-left */
230+
214231
/* UI: surface system — dialog and floating overlays */
215232
--sd-ui-surface-bg: var(--sd-popover-bg, var(--sd-ui-bg));
216233
--sd-ui-surface-border: var(--sd-ui-border);

0 commit comments

Comments
 (0)