Skip to content

Commit 0fb1634

Browse files
committed
feat: add editable zone styling and condition value matching support
1 parent 1afe92b commit 0fb1634

4 files changed

Lines changed: 338 additions & 26 deletions

File tree

RELEASE_NOTES.md

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@
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.
12+
- **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).
1113

1214
### Bug Fixes
1315

@@ -32,8 +34,8 @@
3234

3335
### Tests
3436

35-
- Added 26 new tests: cleanup (3), XSS (3), conditional inline (1), copy button (8), line numbers (5), accessibility (6)
36-
- Updated tests: theme system (7), mixed content highlighting (7) — 150 tests total
37+
- Added 40 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)
38+
- Updated tests: theme system (7), mixed content highlighting (7) — 164 tests total
3739

3840
---
3941

demo/index.html

Lines changed: 92 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -494,15 +494,15 @@ <h1>@softwarity/interactive-code</h1>
494494
<h2>Theme & Color Scheme</h2>
495495
<p>Click on highlighted values to change theme and color scheme. Changes apply globally to all "Seen by user" code blocks below.</p>
496496

497-
<interactive-code class="inline-code" language="html">
497+
<interactive-code class="inline-code" language="html" show-copy>
498498
<textarea>${useTheme}<link rel="stylesheet" href="https://unpkg.com/@softwarity/interactive-code/themes/${theme}.css"></textarea>
499499
<code-binding key="useTheme" type="comment" value="true"
500500
onchange="loadThemeCSS(e.detail ? this.closest('interactive-code').querySelector('[key=theme]').value : '')"></code-binding>
501501
<code-binding key="theme" type="select" options="vscode,github,solarized,catppuccin" value="catppuccin"
502502
onchange="var c=this.closest('interactive-code').querySelector('[key=useTheme]'); if(String(c.value)!=='false') loadThemeCSS(e.detail)"></code-binding>
503503
</interactive-code>
504504

505-
<interactive-code class="inline-code" language="scss">
505+
<interactive-code class="inline-code" language="scss" show-copy>
506506
<textarea>html {
507507
color-scheme: ${colorScheme};
508508
}</textarea>
@@ -511,6 +511,41 @@ <h2>Theme & Color Scheme</h2>
511511
</interactive-code>
512512
</section>
513513

514+
<!-- Editable Zone Style -->
515+
<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>
518+
519+
<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" options="wavy,dotted,dashed,highlight,outline,pill,hand-drawn,none" value="wavy"
545+
onchange="applyEditableStyle(e.detail)"></code-binding>
546+
</interactive-code>
547+
</section>
548+
514549
<!-- Playground Section -->
515550
<section class="playground">
516551
<h2>Playground</h2>
@@ -1302,6 +1337,61 @@ <h3>Features</h3>
13021337

13031338
// Load initial theme (catppuccin)
13041339
loadThemeCSS('catppuccin');
1340+
1341+
function applyEditableStyle(style) {
1342+
var els = document.querySelectorAll('.user-view interactive-code');
1343+
var props = [
1344+
'--code-editable-text-decoration',
1345+
'--code-editable-text-underline-offset',
1346+
'--code-editable-border-radius',
1347+
'--code-editable-border',
1348+
'--code-editable-outline',
1349+
'--code-editable-outline-offset',
1350+
'--code-editable-background',
1351+
'--code-editable-padding'
1352+
];
1353+
els.forEach(function(el) {
1354+
props.forEach(function(p) { el.style.removeProperty(p); });
1355+
switch (style) {
1356+
case 'wavy':
1357+
break;
1358+
case 'dotted':
1359+
el.style.setProperty('--code-editable-text-decoration', 'underline dotted 1.5px');
1360+
el.style.setProperty('--code-editable-text-underline-offset', '3px');
1361+
break;
1362+
case 'dashed':
1363+
el.style.setProperty('--code-editable-text-decoration', 'underline dashed 1.5px');
1364+
el.style.setProperty('--code-editable-text-underline-offset', '3px');
1365+
break;
1366+
case 'highlight':
1367+
el.style.setProperty('--code-editable-text-decoration', 'none');
1368+
el.style.setProperty('--code-editable-background', 'rgba(128,128,128,0.15)');
1369+
el.style.setProperty('--code-editable-border-radius', '3px');
1370+
el.style.setProperty('--code-editable-padding', '1px 4px');
1371+
break;
1372+
case 'outline':
1373+
el.style.setProperty('--code-editable-text-decoration', 'none');
1374+
el.style.setProperty('--code-editable-outline', '1px dashed');
1375+
el.style.setProperty('--code-editable-outline-offset', '2px');
1376+
break;
1377+
case 'pill':
1378+
el.style.setProperty('--code-editable-text-decoration', 'none');
1379+
el.style.setProperty('--code-editable-background', 'rgba(128,128,128,0.12)');
1380+
el.style.setProperty('--code-editable-border-radius', '12px');
1381+
el.style.setProperty('--code-editable-padding', '1px 8px');
1382+
break;
1383+
case 'hand-drawn':
1384+
el.style.setProperty('--code-editable-text-decoration', 'none');
1385+
el.style.setProperty('--code-editable-border', '1.5px solid');
1386+
el.style.setProperty('--code-editable-border-radius', '255px 15px 225px 15px / 15px 225px 15px 255px');
1387+
el.style.setProperty('--code-editable-padding', '2px 6px');
1388+
break;
1389+
case 'none':
1390+
el.style.setProperty('--code-editable-text-decoration', 'none');
1391+
break;
1392+
}
1393+
});
1394+
}
13051395
</script>
13061396
</body>
13071397
</html>

src/interactive-code.element.spec.ts

Lines changed: 203 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1334,4 +1334,207 @@ describe('InteractiveCodeElement', () => {
13341334
expect(keyword?.textContent).toBe('const');
13351335
});
13361336
});
1337+
1338+
describe('condition value matching', () => {
1339+
it('should show textarea when condition key=value matches', async () => {
1340+
element.innerHTML = `
1341+
<textarea>header</textarea>
1342+
<textarea condition="style=wavy">wavy content</textarea>
1343+
<textarea condition="style=dotted">dotted content</textarea>
1344+
<code-binding key="style" type="select" options="wavy,dotted,dashed" value="wavy"></code-binding>
1345+
`;
1346+
document.body.appendChild(element);
1347+
1348+
await new Promise(resolve => setTimeout(resolve, 150));
1349+
1350+
const code = element.shadowRoot?.querySelector('code')?.textContent;
1351+
expect(code).toContain('wavy content');
1352+
expect(code).not.toContain('dotted content');
1353+
});
1354+
1355+
it('should update when value changes to match different condition', async () => {
1356+
element.innerHTML = `
1357+
<textarea condition="style=wavy">wavy content</textarea>
1358+
<textarea condition="style=dotted">dotted content</textarea>
1359+
<code-binding key="style" type="select" options="wavy,dotted" value="wavy"></code-binding>
1360+
`;
1361+
document.body.appendChild(element);
1362+
1363+
await new Promise(resolve => setTimeout(resolve, 150));
1364+
1365+
const binding = element.querySelector('code-binding') as any;
1366+
binding.value = 'dotted';
1367+
1368+
await new Promise(resolve => setTimeout(resolve, 50));
1369+
1370+
const code = element.shadowRoot?.querySelector('code')?.textContent;
1371+
expect(code).not.toContain('wavy content');
1372+
expect(code).toContain('dotted content');
1373+
});
1374+
1375+
it('should support negated value matching with !key=value', async () => {
1376+
element.innerHTML = `
1377+
<textarea condition="!style=none">has style</textarea>
1378+
<textarea condition="style=none">no style</textarea>
1379+
<code-binding key="style" type="select" options="wavy,none" value="wavy"></code-binding>
1380+
`;
1381+
document.body.appendChild(element);
1382+
1383+
await new Promise(resolve => setTimeout(resolve, 150));
1384+
1385+
const code = element.shadowRoot?.querySelector('code')?.textContent;
1386+
expect(code).toContain('has style');
1387+
expect(code).not.toContain('no style');
1388+
});
1389+
1390+
it('should not match when binding value differs from expected', async () => {
1391+
element.innerHTML = `
1392+
<textarea condition="style=highlight">highlight content</textarea>
1393+
<code-binding key="style" type="select" options="wavy,highlight" value="wavy"></code-binding>
1394+
`;
1395+
document.body.appendChild(element);
1396+
1397+
await new Promise(resolve => setTimeout(resolve, 150));
1398+
1399+
const code = element.shadowRoot?.querySelector('code')?.textContent;
1400+
expect(code).not.toContain('highlight content');
1401+
});
1402+
1403+
it('should re-evaluate key=value conditions when binding changes externally', async () => {
1404+
element.innerHTML = `
1405+
<textarea>header</textarea>
1406+
<textarea condition="style=a">content A</textarea>
1407+
<textarea condition="style=b">content B</textarea>
1408+
<code-binding key="style" type="select" options="a,b,c" value="a"></code-binding>
1409+
`;
1410+
document.body.appendChild(element);
1411+
1412+
await new Promise(resolve => setTimeout(resolve, 150));
1413+
1414+
let code = element.shadowRoot?.querySelector('code')?.textContent;
1415+
expect(code).toContain('content A');
1416+
expect(code).not.toContain('content B');
1417+
1418+
// Change binding value externally (triggers _handleChange → updateCode)
1419+
const binding = element.querySelector('code-binding') as any;
1420+
binding.value = 'b';
1421+
1422+
await new Promise(resolve => setTimeout(resolve, 50));
1423+
1424+
code = element.shadowRoot?.querySelector('code')?.textContent;
1425+
expect(code).not.toContain('content A');
1426+
expect(code).toContain('content B');
1427+
});
1428+
1429+
it('should coexist with truthy/falsy conditions', async () => {
1430+
element.innerHTML = `
1431+
<textarea condition="enabled">enabled content</textarea>
1432+
<textarea condition="style=wavy">wavy content</textarea>
1433+
<code-binding key="enabled" type="boolean" value="true"></code-binding>
1434+
<code-binding key="style" type="select" options="wavy,dotted" value="wavy"></code-binding>
1435+
`;
1436+
document.body.appendChild(element);
1437+
1438+
await new Promise(resolve => setTimeout(resolve, 150));
1439+
1440+
const code = element.shadowRoot?.querySelector('code')?.textContent;
1441+
expect(code).toContain('enabled content');
1442+
expect(code).toContain('wavy content');
1443+
});
1444+
});
1445+
1446+
describe('part="editable" attribute', () => {
1447+
it('should add part="editable" to boolean controls', async () => {
1448+
element.innerHTML = `
1449+
<textarea>\${enabled}</textarea>
1450+
<code-binding key="enabled" type="boolean" value="true"></code-binding>
1451+
`;
1452+
document.body.appendChild(element);
1453+
1454+
await new Promise(resolve => setTimeout(resolve, 150));
1455+
1456+
const control = element.shadowRoot?.querySelector('.inline-boolean');
1457+
expect(control?.getAttribute('part')).toBe('editable');
1458+
});
1459+
1460+
it('should add part="editable" to number controls', async () => {
1461+
element.innerHTML = `
1462+
<textarea>\${count}</textarea>
1463+
<code-binding key="count" type="number" value="5"></code-binding>
1464+
`;
1465+
document.body.appendChild(element);
1466+
1467+
await new Promise(resolve => setTimeout(resolve, 150));
1468+
1469+
const control = element.shadowRoot?.querySelector('.inline-number');
1470+
expect(control?.getAttribute('part')).toBe('editable');
1471+
});
1472+
1473+
it('should add part="editable" to string controls', async () => {
1474+
element.innerHTML = `
1475+
<textarea>\${name}</textarea>
1476+
<code-binding key="name" type="string" value="test"></code-binding>
1477+
`;
1478+
document.body.appendChild(element);
1479+
1480+
await new Promise(resolve => setTimeout(resolve, 150));
1481+
1482+
const control = element.shadowRoot?.querySelector('.inline-string');
1483+
expect(control?.getAttribute('part')).toBe('editable');
1484+
});
1485+
1486+
it('should add part="editable" to line toggle controls', async () => {
1487+
element.setAttribute('language', 'scss');
1488+
element.innerHTML = `
1489+
<textarea>\${toggle}color: red;</textarea>
1490+
<code-binding key="toggle" type="comment" value="true"></code-binding>
1491+
`;
1492+
document.body.appendChild(element);
1493+
1494+
await new Promise(resolve => setTimeout(resolve, 150));
1495+
1496+
const toggle = element.shadowRoot?.querySelector('.line-toggle');
1497+
expect(toggle?.getAttribute('part')).toBe('editable');
1498+
});
1499+
1500+
it('should add part="editable" to block toggle controls', async () => {
1501+
element.setAttribute('language', 'typescript');
1502+
element.innerHTML = `
1503+
<textarea>\${block}const x = 1;\${/block}</textarea>
1504+
<code-binding key="block" type="comment" value="true"></code-binding>
1505+
`;
1506+
document.body.appendChild(element);
1507+
1508+
await new Promise(resolve => setTimeout(resolve, 150));
1509+
1510+
const toggle = element.shadowRoot?.querySelector('.block-toggle');
1511+
expect(toggle?.getAttribute('part')).toBe('editable');
1512+
});
1513+
});
1514+
1515+
describe('editable zone CSS custom properties', () => {
1516+
it('should use --code-editable-text-decoration in CSS', () => {
1517+
document.body.appendChild(element);
1518+
const style = element.shadowRoot?.querySelector('style');
1519+
expect(style?.textContent).toContain('var(--code-editable-text-decoration,');
1520+
});
1521+
1522+
it('should use --code-editable-border-radius in CSS', () => {
1523+
document.body.appendChild(element);
1524+
const style = element.shadowRoot?.querySelector('style');
1525+
expect(style?.textContent).toContain('var(--code-editable-border-radius,');
1526+
});
1527+
1528+
it('should use --code-editable-border in CSS', () => {
1529+
document.body.appendChild(element);
1530+
const style = element.shadowRoot?.querySelector('style');
1531+
expect(style?.textContent).toContain('var(--code-editable-border,');
1532+
});
1533+
1534+
it('should use --code-editable-padding in CSS', () => {
1535+
document.body.appendChild(element);
1536+
const style = element.shadowRoot?.querySelector('style');
1537+
expect(style?.textContent).toContain('var(--code-editable-padding,');
1538+
});
1539+
});
13371540
});

0 commit comments

Comments
 (0)