Skip to content

Commit f4967a3

Browse files
committed
feat: update interactive zone styling and enhance theme support
1 parent c5d96ad commit f4967a3

8 files changed

Lines changed: 224 additions & 136 deletions

File tree

RELEASE_NOTES.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
- **Color scheme**: `color-scheme` attribute to override light/dark mode per element (inherits from parent by default).
99
- **Mixed content highlighting**: When `language="html"`, `<style>` blocks use SCSS highlighting and `<script>` blocks use TypeScript highlighting automatically.
1010
- **CSS custom properties**: All 18 token colors and 10 UI colors exposed as `--token-*` and `--code-*` variables for external customization.
11-
- **Editable zone styling**: Interactive controls expose `part="editable"` and 8 CSS custom properties (`--code-editable-text-decoration`, `--code-editable-border-radius`, `--code-editable-border`, `--code-editable-outline`, `--code-editable-background`, `--code-editable-padding`, etc.) for full decoration customization. Built-in styles: wavy (default), dotted, dashed, highlight, outline, pill, hand-drawn, none.
11+
- **Interactive zone styling**: Interactive controls expose `part="interactive"` for external CSS styling (supports `:hover`). Themes provide color hooks: `--code-interactive-highlight` (accent), `--code-interactive-color` (text), `--code-interactive-bg-color` (background), `--code-interactive-border-color` (border). Shadow DOM decoration customizable via `--code-interactive-text-decoration`, `--code-interactive-border`, etc. Built-in styles: wavy (default), dotted, dashed, highlight, outline, pill, hand-drawn, none.
1212
- **Condition value matching**: Conditional textareas now support `condition="key=value"` syntax to show content when a binding equals a specific value (in addition to existing truthy/falsy checks).
1313
- **Select carousel mode**: New `carousel` boolean attribute on `<code-binding type="select">` cycles through options on click instead of opening a dropdown. Supports keyboard navigation (ArrowUp/ArrowDown).
1414

@@ -35,7 +35,7 @@
3535

3636
### Tests
3737

38-
- Added 52 new tests: cleanup (3), XSS (3), conditional inline (1), copy button (8), line numbers (5), accessibility (6), condition value matching (5), part="editable" (5), editable zone CSS (4), carousel (6 rendering + 6 code-binding)
38+
- Added 52 new tests: cleanup (3), XSS (3), conditional inline (1), copy button (8), line numbers (5), accessibility (6), condition value matching (5), part="interactive" (5), interactive zone CSS (4), carousel (6 rendering + 6 code-binding)
3939
- Updated tests: theme system (7), mixed content highlighting (7) — 177 tests total
4040

4141
---

demo/index.html

Lines changed: 116 additions & 83 deletions
Original file line numberDiff line numberDiff line change
@@ -324,7 +324,10 @@
324324
--code-copy-border: rgba(255,255,255,0.2);
325325
--code-copy-accent: #769aa5;
326326
--code-color-preview-border: rgba(255,255,255,0.3);
327-
--code-editable-underline: #769aa5;
327+
--code-interactive-highlight: #769aa5;
328+
--code-interactive-color: inherit;
329+
--code-interactive-bg-color: transparent;
330+
--code-interactive-border-color: var(--code-interactive-highlight);
328331
--code-comment-color: #808080;
329332
border: 1px solid var(--border-color);
330333
border-top: none;
@@ -511,38 +514,74 @@ <h2>Theme & Color Scheme</h2>
511514
</interactive-code>
512515
</section>
513516

514-
<!-- Editable Zone Style -->
517+
<!-- Interactive Zone Style -->
515518
<section>
516-
<h2>Editable Zone Style</h2>
517-
<p>Customize how interactive zones look. Select a style to see the CSS and apply it to "Seen by user" blocks below.</p>
519+
<h2>Interactive Zone Style</h2>
520+
<p>Customize how interactive zones look. Select a style to see the CSS and apply it globally.</p>
518521

519522
<interactive-code class="inline-code" language="scss" show-copy>
520-
<textarea>/* Interactive zone style: ${editableStyle} */
521-
interactive-code {</textarea>
522-
<textarea condition="editableStyle=wavy"> /* default — no custom property needed */</textarea>
523-
<textarea condition="editableStyle=dotted"> --code-editable-text-decoration: underline dotted 1.5px;
524-
--code-editable-text-underline-offset: 3px;</textarea>
525-
<textarea condition="editableStyle=dashed"> --code-editable-text-decoration: underline dashed 1.5px;
526-
--code-editable-text-underline-offset: 3px;</textarea>
527-
<textarea condition="editableStyle=highlight"> --code-editable-text-decoration: none;
528-
--code-editable-background: rgba(128,128,128,0.15);
529-
--code-editable-border-radius: 3px;
530-
--code-editable-padding: 1px 4px;</textarea>
531-
<textarea condition="editableStyle=outline"> --code-editable-text-decoration: none;
532-
--code-editable-outline: 1px dashed;
533-
--code-editable-outline-offset: 2px;</textarea>
534-
<textarea condition="editableStyle=pill"> --code-editable-text-decoration: none;
535-
--code-editable-background: rgba(128,128,128,0.12);
536-
--code-editable-border-radius: 12px;
537-
--code-editable-padding: 1px 8px;</textarea>
538-
<textarea condition="editableStyle=hand-drawn"> --code-editable-text-decoration: none;
539-
--code-editable-border: 1.5px solid;
540-
--code-editable-border-radius: 255px 15px 225px 15px / 15px 225px 15px 255px;
541-
--code-editable-padding: 2px 6px;</textarea>
542-
<textarea condition="editableStyle=none"> --code-editable-text-decoration: none;</textarea>
543-
<textarea>}</textarea>
544-
<code-binding key="editableStyle" type="select" carousel options="wavy,dotted,dashed,highlight,outline,pill,hand-drawn,none" value="wavy"
545-
onchange="applyEditableStyle(e.detail)"></code-binding>
523+
<textarea>/* Interactive zone style: ${interactiveStyle} */</textarea>
524+
<textarea condition="interactiveStyle=wavy">/* built-in default — no custom CSS needed */</textarea>
525+
<textarea condition="interactiveStyle=dotted">interactive-code::part(interactive) {
526+
text-decoration: underline dotted 1.5px var(--code-interactive-highlight);
527+
text-underline-offset: 3px;
528+
}
529+
530+
interactive-code::part(interactive):hover {
531+
text-decoration-thickness: 2.5px;
532+
}</textarea>
533+
<textarea condition="interactiveStyle=dashed">interactive-code::part(interactive) {
534+
text-decoration: underline dashed 1.5px var(--code-interactive-highlight);
535+
text-underline-offset: 3px;
536+
}
537+
538+
interactive-code::part(interactive):hover {
539+
text-decoration-thickness: 2.5px;
540+
}</textarea>
541+
<textarea condition="interactiveStyle=highlight">interactive-code::part(interactive) {
542+
text-decoration: none;
543+
background: rgba(128,128,128,0.15);
544+
border-radius: 3px;
545+
padding: 1px 4px;
546+
}
547+
548+
interactive-code::part(interactive):hover {
549+
background: rgba(128,128,128,0.25);
550+
}</textarea>
551+
<textarea condition="interactiveStyle=outline">interactive-code::part(interactive) {
552+
text-decoration: none;
553+
outline: 1px dashed var(--code-interactive-border-color);
554+
outline-offset: 2px;
555+
}
556+
557+
interactive-code::part(interactive):hover {
558+
outline-style: solid;
559+
}</textarea>
560+
<textarea condition="interactiveStyle=pill">interactive-code::part(interactive) {
561+
text-decoration: none;
562+
background: rgba(128,128,128,0.12);
563+
border-radius: 12px;
564+
padding: 1px 8px;
565+
}
566+
567+
interactive-code::part(interactive):hover {
568+
background: rgba(128,128,128,0.25);
569+
}</textarea>
570+
<textarea condition="interactiveStyle=hand-drawn">interactive-code::part(interactive) {
571+
text-decoration: none;
572+
border: 1.5px solid var(--code-interactive-border-color);
573+
border-radius: 55% 45% 50% 40% / 40% 50% 45% 55%;
574+
padding: 2px 8px;
575+
}
576+
577+
interactive-code::part(interactive):hover {
578+
box-shadow: 0 0 0 1px var(--code-interactive-border-color);
579+
}</textarea>
580+
<textarea condition="interactiveStyle=none">interactive-code::part(interactive) {
581+
text-decoration: none;
582+
}</textarea>
583+
<code-binding key="interactiveStyle" type="select" carousel options="hand-drawn,wavy,dotted,dashed,highlight,outline,pill,none" value="hand-drawn"
584+
onchange="applyInteractiveStyle(e.detail)"></code-binding>
546585
</interactive-code>
547586
</section>
548587

@@ -1346,59 +1385,53 @@ <h3>Features</h3>
13461385
// Load initial theme (catppuccin)
13471386
loadThemeCSS('catppuccin');
13481387

1349-
function applyEditableStyle(style) {
1350-
var els = document.querySelectorAll('.user-view interactive-code');
1351-
var props = [
1352-
'--code-editable-text-decoration',
1353-
'--code-editable-text-underline-offset',
1354-
'--code-editable-border-radius',
1355-
'--code-editable-border',
1356-
'--code-editable-outline',
1357-
'--code-editable-outline-offset',
1358-
'--code-editable-background',
1359-
'--code-editable-padding'
1360-
];
1361-
els.forEach(function(el) {
1362-
props.forEach(function(p) { el.style.removeProperty(p); });
1363-
switch (style) {
1364-
case 'wavy':
1365-
break;
1366-
case 'dotted':
1367-
el.style.setProperty('--code-editable-text-decoration', 'underline dotted 1.5px');
1368-
el.style.setProperty('--code-editable-text-underline-offset', '3px');
1369-
break;
1370-
case 'dashed':
1371-
el.style.setProperty('--code-editable-text-decoration', 'underline dashed 1.5px');
1372-
el.style.setProperty('--code-editable-text-underline-offset', '3px');
1373-
break;
1374-
case 'highlight':
1375-
el.style.setProperty('--code-editable-text-decoration', 'none');
1376-
el.style.setProperty('--code-editable-background', 'rgba(128,128,128,0.15)');
1377-
el.style.setProperty('--code-editable-border-radius', '3px');
1378-
el.style.setProperty('--code-editable-padding', '1px 4px');
1379-
break;
1380-
case 'outline':
1381-
el.style.setProperty('--code-editable-text-decoration', 'none');
1382-
el.style.setProperty('--code-editable-outline', '1px dashed');
1383-
el.style.setProperty('--code-editable-outline-offset', '2px');
1384-
break;
1385-
case 'pill':
1386-
el.style.setProperty('--code-editable-text-decoration', 'none');
1387-
el.style.setProperty('--code-editable-background', 'rgba(128,128,128,0.12)');
1388-
el.style.setProperty('--code-editable-border-radius', '12px');
1389-
el.style.setProperty('--code-editable-padding', '1px 8px');
1390-
break;
1391-
case 'hand-drawn':
1392-
el.style.setProperty('--code-editable-text-decoration', 'none');
1393-
el.style.setProperty('--code-editable-border', '1.5px solid');
1394-
el.style.setProperty('--code-editable-border-radius', '255px 15px 225px 15px / 15px 225px 15px 255px');
1395-
el.style.setProperty('--code-editable-padding', '2px 6px');
1396-
break;
1397-
case 'none':
1398-
el.style.setProperty('--code-editable-text-decoration', 'none');
1399-
break;
1400-
}
1401-
});
1388+
// Apply initial interactive style (hand-drawn)
1389+
applyInteractiveStyle('hand-drawn');
1390+
1391+
function applyInteractiveStyle(style) {
1392+
var styleEl = document.getElementById('interactive-style-sheet');
1393+
if (!styleEl) {
1394+
styleEl = document.createElement('style');
1395+
styleEl.id = 'interactive-style-sheet';
1396+
document.head.appendChild(styleEl);
1397+
}
1398+
var s = 'interactive-code::part(interactive)';
1399+
var h = s + ':hover';
1400+
var hl = 'var(--code-interactive-highlight)';
1401+
var bc = 'var(--code-interactive-border-color)';
1402+
var css = '';
1403+
switch (style) {
1404+
case 'wavy':
1405+
break;
1406+
case 'dotted':
1407+
css = s + ' { text-decoration: underline dotted 1.5px ' + hl + '; text-underline-offset: 3px; } '
1408+
+ h + ' { text-decoration-thickness: 2.5px; }';
1409+
break;
1410+
case 'dashed':
1411+
css = s + ' { text-decoration: underline dashed 1.5px ' + hl + '; text-underline-offset: 3px; } '
1412+
+ h + ' { text-decoration-thickness: 2.5px; }';
1413+
break;
1414+
case 'highlight':
1415+
css = s + ' { text-decoration: none; background: rgba(128,128,128,0.15); border-radius: 3px; padding: 1px 4px; } '
1416+
+ h + ' { background: rgba(128,128,128,0.25); }';
1417+
break;
1418+
case 'outline':
1419+
css = s + ' { text-decoration: none; outline: 1px dashed ' + bc + '; outline-offset: 2px; } '
1420+
+ h + ' { outline-style: solid; }';
1421+
break;
1422+
case 'pill':
1423+
css = s + ' { text-decoration: none; background: rgba(128,128,128,0.12); border-radius: 12px; padding: 1px 8px; } '
1424+
+ h + ' { background: rgba(128,128,128,0.25); }';
1425+
break;
1426+
case 'hand-drawn':
1427+
css = s + ' { text-decoration: none; border: 1.5px solid ' + bc + '; border-radius: 55% 45% 50% 40% / 40% 50% 45% 55%; padding: 2px 8px; } '
1428+
+ h + ' { box-shadow: 0 0 0 1px ' + bc + '; }';
1429+
break;
1430+
case 'none':
1431+
css = s + ' { text-decoration: none; }';
1432+
break;
1433+
}
1434+
styleEl.textContent = css;
14021435
}
14031436
</script>
14041437
</body>

src/interactive-code.element.spec.ts

Lines changed: 41 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1443,8 +1443,8 @@ describe('InteractiveCodeElement', () => {
14431443
});
14441444
});
14451445

1446-
describe('part="editable" attribute', () => {
1447-
it('should add part="editable" to boolean controls', async () => {
1446+
describe('part="interactive" attribute', () => {
1447+
it('should expose part="interactive" on boolean controls', async () => {
14481448
element.innerHTML = `
14491449
<textarea>\${enabled}</textarea>
14501450
<code-binding key="enabled" type="boolean" value="true"></code-binding>
@@ -1454,10 +1454,10 @@ describe('InteractiveCodeElement', () => {
14541454
await new Promise(resolve => setTimeout(resolve, 150));
14551455

14561456
const control = element.shadowRoot?.querySelector('.inline-boolean');
1457-
expect(control?.getAttribute('part')).toBe('editable');
1457+
expect(control?.getAttribute('part')).toBe('interactive');
14581458
});
14591459

1460-
it('should add part="editable" to number controls', async () => {
1460+
it('should add part="interactive" to number controls', async () => {
14611461
element.innerHTML = `
14621462
<textarea>\${count}</textarea>
14631463
<code-binding key="count" type="number" value="5"></code-binding>
@@ -1467,10 +1467,10 @@ describe('InteractiveCodeElement', () => {
14671467
await new Promise(resolve => setTimeout(resolve, 150));
14681468

14691469
const control = element.shadowRoot?.querySelector('.inline-number');
1470-
expect(control?.getAttribute('part')).toBe('editable');
1470+
expect(control?.getAttribute('part')).toBe('interactive');
14711471
});
14721472

1473-
it('should add part="editable" to string controls', async () => {
1473+
it('should add part="interactive" to string controls', async () => {
14741474
element.innerHTML = `
14751475
<textarea>\${name}</textarea>
14761476
<code-binding key="name" type="string" value="test"></code-binding>
@@ -1480,10 +1480,10 @@ describe('InteractiveCodeElement', () => {
14801480
await new Promise(resolve => setTimeout(resolve, 150));
14811481

14821482
const control = element.shadowRoot?.querySelector('.inline-string');
1483-
expect(control?.getAttribute('part')).toBe('editable');
1483+
expect(control?.getAttribute('part')).toBe('interactive');
14841484
});
14851485

1486-
it('should add part="editable" to line toggle controls', async () => {
1486+
it('should add part="interactive" to line toggle controls', async () => {
14871487
element.setAttribute('language', 'scss');
14881488
element.innerHTML = `
14891489
<textarea>\${toggle}color: red;</textarea>
@@ -1494,10 +1494,10 @@ describe('InteractiveCodeElement', () => {
14941494
await new Promise(resolve => setTimeout(resolve, 150));
14951495

14961496
const toggle = element.shadowRoot?.querySelector('.line-toggle');
1497-
expect(toggle?.getAttribute('part')).toBe('editable');
1497+
expect(toggle?.getAttribute('part')).toBe('interactive');
14981498
});
14991499

1500-
it('should add part="editable" to block toggle controls', async () => {
1500+
it('should add part="interactive" to block toggle controls', async () => {
15011501
element.setAttribute('language', 'typescript');
15021502
element.innerHTML = `
15031503
<textarea>\${block}const x = 1;\${/block}</textarea>
@@ -1508,7 +1508,7 @@ describe('InteractiveCodeElement', () => {
15081508
await new Promise(resolve => setTimeout(resolve, 150));
15091509

15101510
const toggle = element.shadowRoot?.querySelector('.block-toggle');
1511-
expect(toggle?.getAttribute('part')).toBe('editable');
1511+
expect(toggle?.getAttribute('part')).toBe('interactive');
15121512
});
15131513
});
15141514

@@ -1543,7 +1543,7 @@ describe('InteractiveCodeElement', () => {
15431543
expect(valueSpan?.textContent).toBe('green');
15441544
});
15451545

1546-
it('should have role="button" and part="editable" on carousel', async () => {
1546+
it('should have role="button" and part="interactive" on carousel', async () => {
15471547
element.innerHTML = `
15481548
<textarea>\${color}</textarea>
15491549
<code-binding key="color" type="select" carousel options="red, green, blue" value="red"></code-binding>
@@ -1554,7 +1554,7 @@ describe('InteractiveCodeElement', () => {
15541554

15551555
const carousel = element.shadowRoot?.querySelector('.inline-select-carousel');
15561556
expect(carousel?.getAttribute('role')).toBe('button');
1557-
expect(carousel?.getAttribute('part')).toBe('editable');
1557+
expect(carousel?.getAttribute('part')).toBe('interactive');
15581558
});
15591559

15601560
it('should cycle value on click via toggle action', async () => {
@@ -1576,6 +1576,25 @@ describe('InteractiveCodeElement', () => {
15761576
expect(valueSpan?.textContent).toBe('green');
15771577
});
15781578

1579+
it('should cycle backward on shift+click via previous()', async () => {
1580+
element.innerHTML = `
1581+
<textarea>\${color}</textarea>
1582+
<code-binding key="color" type="select" carousel options="red, green, blue" value="green"></code-binding>
1583+
`;
1584+
document.body.appendChild(element);
1585+
1586+
await new Promise(resolve => setTimeout(resolve, 150));
1587+
1588+
const carousel = element.shadowRoot?.querySelector('.inline-select-carousel') as HTMLElement;
1589+
const shiftClick = new MouseEvent('click', { bubbles: true, shiftKey: true });
1590+
carousel.dispatchEvent(shiftClick);
1591+
1592+
await new Promise(resolve => setTimeout(resolve, 50));
1593+
1594+
const binding = element.querySelector('code-binding') as CodeBindingElement;
1595+
expect(binding.value).toBe('red');
1596+
});
1597+
15791598
it('should also work with 2 options when carousel is set', async () => {
15801599
element.innerHTML = `
15811600
<textarea>\${mode}</textarea>
@@ -1608,29 +1627,29 @@ describe('InteractiveCodeElement', () => {
16081627
});
16091628
});
16101629

1611-
describe('editable zone CSS custom properties', () => {
1612-
it('should use --code-editable-text-decoration in CSS', () => {
1630+
describe('interactive zone CSS custom properties', () => {
1631+
it('should use --code-interactive-text-decoration in CSS', () => {
16131632
document.body.appendChild(element);
16141633
const style = element.shadowRoot?.querySelector('style');
1615-
expect(style?.textContent).toContain('var(--code-editable-text-decoration,');
1634+
expect(style?.textContent).toContain('var(--code-interactive-text-decoration,');
16161635
});
16171636

1618-
it('should use --code-editable-border-radius in CSS', () => {
1637+
it('should use --code-interactive-border-radius in CSS', () => {
16191638
document.body.appendChild(element);
16201639
const style = element.shadowRoot?.querySelector('style');
1621-
expect(style?.textContent).toContain('var(--code-editable-border-radius,');
1640+
expect(style?.textContent).toContain('var(--code-interactive-border-radius,');
16221641
});
16231642

1624-
it('should use --code-editable-border in CSS', () => {
1643+
it('should use --code-interactive-border in CSS', () => {
16251644
document.body.appendChild(element);
16261645
const style = element.shadowRoot?.querySelector('style');
1627-
expect(style?.textContent).toContain('var(--code-editable-border,');
1646+
expect(style?.textContent).toContain('var(--code-interactive-border,');
16281647
});
16291648

1630-
it('should use --code-editable-padding in CSS', () => {
1649+
it('should use --code-interactive-padding in CSS', () => {
16311650
document.body.appendChild(element);
16321651
const style = element.shadowRoot?.querySelector('style');
1633-
expect(style?.textContent).toContain('var(--code-editable-padding,');
1652+
expect(style?.textContent).toContain('var(--code-interactive-padding,');
16341653
});
16351654
});
16361655
});

0 commit comments

Comments
 (0)