Migrated from design-system-swarm with fresh git history.
Old project history preserved in /home/overbits/apps/design-system-swarm
Core components:
- MCP Server (Python FastAPI with mcp 1.23.1)
- Claude Plugin (agents, commands, skills, strategies, hooks, core)
- DSS Backend (dss-mvp1 - token translation, Figma sync)
- Admin UI (Node.js/React)
- Server (Node.js/Express)
- Storybook integration (dss-mvp1/.storybook)
Self-contained configuration:
- All paths relative or use DSS_BASE_PATH=/home/overbits/dss
- PYTHONPATH configured for dss-mvp1 and dss-claude-plugin
- .env file with all configuration
- Claude plugin uses ${CLAUDE_PLUGIN_ROOT} for portability
Migration completed: $(date)
🤖 Clean migration with full functionality preserved
21 KiB
21 KiB
DSS Coding Standards Migration Guide
This guide shows how to migrate existing code to DSS coding standards defined in .knowledge/dss-coding-standards.json.
Table of Contents
- Shadow DOM Migration
- Inline Event Handler Removal
- Inline Style Extraction
- Semantic HTML
- Accessibility Improvements
- Logger Migration
- State Management
Shadow DOM Migration
❌ Before (No Shadow DOM)
export default class MyComponent extends HTMLElement {
connectedCallback() {
this.innerHTML = `
<div class="container">
<h2>Title</h2>
<p>Content</p>
</div>
`;
}
}
✅ After (With Shadow DOM)
export default class MyComponent extends HTMLElement {
constructor() {
super();
this.attachShadow({ mode: 'open' }); // ✓ Enable Shadow DOM
}
connectedCallback() {
this.render();
}
render() {
this.shadowRoot.innerHTML = `
<style>
:host {
display: block;
}
.container {
padding: 16px;
background: var(--vscode-sidebar-background);
}
h2 {
color: var(--vscode-foreground);
font-size: 16px;
}
</style>
<div class="container">
<h2>Title</h2>
<p>Content</p>
</div>
`;
}
}
Key Changes:
- ✓ Add
attachShadow()in constructor - ✓ Change
this.innerHTMLtothis.shadowRoot.innerHTML - ✓ Extract styles to
<style>block in Shadow DOM - ✓ Use VSCode theme CSS variables
Inline Event Handler Removal
❌ Before (Inline Events - FORBIDDEN)
render() {
this.innerHTML = `
<div
class="card"
onclick="this.getRootNode().host.handleClick()"
onmouseover="this.style.transform='scale(1.02)'"
onmouseout="this.style.transform='scale(1)'">
<button onclick="alert('clicked')">Click me</button>
</div>
`;
}
✅ After (Event Delegation + CSS)
constructor() {
super();
this.attachShadow({ mode: 'open' });
}
connectedCallback() {
this.render();
this.setupEventListeners(); // ✓ Setup after render
}
disconnectedCallback() {
// ✓ Cleanup happens automatically with AbortController
if (this.abortController) {
this.abortController.abort();
}
}
render() {
this.shadowRoot.innerHTML = `
<style>
.card {
transition: transform 0.2s;
}
.card:hover {
transform: scale(1.02); /* ✓ CSS hover instead of JS */
}
button {
padding: 8px 16px;
}
</style>
<div class="card" data-action="cardClick">
<button data-action="buttonClick" type="button">Click me</button>
</div>
`;
}
setupEventListeners() {
// ✓ Event delegation with AbortController for cleanup
this.abortController = new AbortController();
this.shadowRoot.addEventListener('click', (e) => {
const action = e.target.closest('[data-action]')?.dataset.action;
if (action === 'cardClick') {
this.handleCardClick(e);
} else if (action === 'buttonClick') {
this.handleButtonClick(e);
}
}, { signal: this.abortController.signal });
}
handleCardClick(e) {
console.log('Card clicked');
}
handleButtonClick(e) {
e.stopPropagation();
this.dispatchEvent(new CustomEvent('button-clicked', {
bubbles: true,
composed: true
}));
}
Key Changes:
- ✓ Remove ALL
onclick,onmouseover,onmouseoutattributes - ✓ Use CSS
:hoverfor hover effects - ✓ Event delegation with
data-actionattributes - ✓ Single event listener using
closest('[data-action]') - ✓ AbortController for automatic cleanup
- ✓ Custom events for component communication
Inline Style Extraction
❌ Before (Inline Styles Everywhere)
render() {
this.innerHTML = `
<div style="background: #1e1e1e; padding: 24px; border-radius: 4px;">
<h2 style="color: #ffffff; font-size: 18px; margin-bottom: 12px;">
${this.title}
</h2>
<button style="padding: 8px 16px; background: #0e639c; color: white; border: none; border-radius: 2px; cursor: pointer;">
Action
</button>
</div>
`;
}
✅ After (Styles in Shadow DOM)
render() {
this.shadowRoot.innerHTML = `
<style>
:host {
display: block;
}
.container {
background: var(--vscode-sidebar-background);
padding: 24px;
border-radius: 4px;
}
h2 {
color: var(--vscode-foreground);
font-size: 18px;
margin: 0 0 12px 0;
}
button {
padding: 8px 16px;
background: var(--vscode-button-background);
color: var(--vscode-button-foreground);
border: none;
border-radius: 2px;
cursor: pointer;
transition: background-color 0.1s;
}
button:hover {
background: var(--vscode-button-hoverBackground);
}
button:focus-visible {
outline: 2px solid var(--vscode-focusBorder);
outline-offset: 2px;
}
</style>
<div class="container">
<h2>${this.title}</h2>
<button type="button">Action</button>
</div>
`;
}
Key Changes:
- ✓ ALL styles moved to
<style>block - ✓ Use VSCode theme CSS variables
- ✓ Add hover and focus states in CSS
- ✓ Exception: Dynamic values like
transform: translateX(${x}px)allowed
Semantic HTML
❌ Before (Divs as Buttons)
render() {
this.innerHTML = `
<div class="tool-item" onclick="this.selectTool()">
<div class="tool-name">Settings</div>
<div class="tool-desc">Configure options</div>
</div>
<div class="close" onclick="this.close()">×</div>
`;
}
✅ After (Semantic Elements)
render() {
this.shadowRoot.innerHTML = `
<style>
button {
appearance: none;
background: transparent;
border: 1px solid transparent;
padding: 8px;
width: 100%;
text-align: left;
cursor: pointer;
border-radius: 4px;
}
button:hover {
background: var(--vscode-list-hoverBackground);
}
button:focus-visible {
outline: 2px solid var(--vscode-focusBorder);
}
.tool-name {
font-size: 13px;
font-weight: 500;
}
.tool-desc {
font-size: 11px;
color: var(--vscode-descriptionForeground);
}
.close-btn {
padding: 4px 8px;
font-size: 20px;
}
</style>
<button type="button" data-action="selectTool">
<div class="tool-name">Settings</div>
<div class="tool-desc">Configure options</div>
</button>
<button
type="button"
class="close-btn"
data-action="close"
aria-label="Close dialog">
×
</button>
`;
}
Key Changes:
- ✓ Use
<button type="button">for interactive elements - ✓ Add
aria-labelfor icon-only buttons - ✓ Keyboard accessible by default
- ✓ Proper focus management
Accessibility Improvements
❌ Before (Poor A11y)
render() {
this.innerHTML = `
<div class="modal">
<div class="close" onclick="this.close()">×</div>
<div class="content">${this.content}</div>
</div>
`;
}
✅ After (WCAG 2.1 AA Compliant)
render() {
this.shadowRoot.innerHTML = `
<style>
.modal {
position: fixed;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
background: var(--vscode-sidebar-background);
border: 1px solid var(--vscode-widget-border);
border-radius: 4px;
padding: 24px;
max-width: 600px;
}
.close-btn {
position: absolute;
top: 8px;
right: 8px;
background: transparent;
border: none;
font-size: 20px;
cursor: pointer;
padding: 4px 8px;
}
.close-btn:focus-visible {
outline: 2px solid var(--vscode-focusBorder);
outline-offset: 2px;
}
</style>
<div
class="modal"
role="dialog"
aria-modal="true"
aria-labelledby="modal-title">
<button
class="close-btn"
type="button"
data-action="close"
aria-label="Close dialog">
×
</button>
<h2 id="modal-title">${this.title}</h2>
<div class="content">${this.content}</div>
</div>
`;
}
connectedCallback() {
this.render();
this.setupEventListeners();
this.trapFocus(); // ✓ Keep focus inside modal
this.previousFocus = document.activeElement; // ✓ Store for restoration
}
disconnectedCallback() {
if (this.previousFocus) {
this.previousFocus.focus(); // ✓ Restore focus on close
}
if (this.abortController) {
this.abortController.abort();
}
}
trapFocus() {
const focusable = this.shadowRoot.querySelectorAll(
'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])'
);
const firstFocusable = focusable[0];
const lastFocusable = focusable[focusable.length - 1];
this.shadowRoot.addEventListener('keydown', (e) => {
if (e.key === 'Tab') {
if (e.shiftKey && document.activeElement === firstFocusable) {
e.preventDefault();
lastFocusable.focus();
} else if (!e.shiftKey && document.activeElement === lastFocusable) {
e.preventDefault();
firstFocusable.focus();
}
} else if (e.key === 'Escape') {
this.close();
}
});
firstFocusable.focus(); // ✓ Focus first element
}
Key Changes:
- ✓ Add ARIA attributes (
role,aria-modal,aria-labelledby) - ✓ Semantic
<button>witharia-label - ✓ Focus trapping for modals
- ✓ Keyboard support (Tab, Shift+Tab, Escape)
- ✓ Focus restoration on close
- ✓
:focus-visiblestyling
Logger Migration
❌ Before (console.log Everywhere)
async loadData() {
console.log('Loading data...');
try {
const response = await fetch('/api/data');
const data = await response.json();
console.log('Data loaded:', data);
this.data = data;
} catch (error) {
console.error('Failed to load data:', error);
}
}
processData() {
console.log('Processing...');
console.warn('This might take a while');
// processing logic
console.log('Done processing');
}
✅ After (Centralized Logger)
import { logger } from '../utils/logger.js';
async loadData() {
const endTimer = logger.time('[MyComponent] Data load'); // ✓ Performance timing
logger.debug('[MyComponent] Starting data load'); // ✓ debug() only in dev
try {
const response = await fetch('/api/data');
const data = await response.json();
logger.info('[MyComponent] Data loaded successfully', {
itemCount: data.length
}); // ✓ Structured data
this.data = data;
endTimer(); // Logs elapsed time
} catch (error) {
logger.error('[MyComponent] Failed to load data', error); // ✓ Proper error logging
throw error;
}
}
processData() {
logger.info('[MyComponent] Starting data processing');
logger.warn('[MyComponent] Heavy processing operation'); // ✓ Use warn for concerns
// processing logic
logger.info('[MyComponent] Processing completed');
}
Key Changes:
- ✓ Import logger utility
- ✓ Use
logger.debug()for development-only logs - ✓ Use
logger.info()for informational messages - ✓ Use
logger.warn()for warnings - ✓ Use
logger.error()for errors - ✓ Add component name prefix
[ComponentName] - ✓ Use
logger.time()for performance measurements
Logger API:
// Enable debug logs: localStorage.setItem('dss_debug', 'true')
// Or in console: window.dssLogger.enableDebug()
logger.debug('Debug message'); // Only in dev or when debug enabled
logger.info('Info message'); // Always shown
logger.warn('Warning'); // Warning level
logger.error('Error', err); // Error level
const endTimer = logger.time('Operation label');
// ... do work ...
endTimer(); // Logs: [TIME] Operation label: 234ms
State Management
❌ Before (Direct DOM Manipulation)
export default class Counter extends HTMLElement {
connectedCallback() {
this.count = 0;
this.innerHTML = `
<div>Count: <span id="count">0</span></div>
<button onclick="this.getRootNode().host.increment()">+</button>
`;
}
increment() {
this.count++;
document.getElementById('count').textContent = this.count; // ✗ Direct DOM
}
}
✅ After (Reactive State Updates)
import contextStore from '../stores/context-store.js';
export default class Counter extends HTMLElement {
constructor() {
super();
this.attachShadow({ mode: 'open' });
this.state = {
count: 0
};
}
connectedCallback() {
this.render();
this.setupEventListeners();
// ✓ Subscribe to global state changes
this.unsubscribe = contextStore.subscribeToKey('someValue', (newValue) => {
this.setState({ externalValue: newValue });
});
}
disconnectedCallback() {
if (this.unsubscribe) {
this.unsubscribe(); // ✓ Cleanup subscription
}
if (this.abortController) {
this.abortController.abort();
}
}
setState(updates) {
// ✓ Immutable state update
this.state = { ...this.state, ...updates };
this.render(); // ✓ Re-render on state change
}
render() {
this.shadowRoot.innerHTML = `
<style>
.container {
display: flex;
align-items: center;
gap: 16px;
padding: 16px;
}
button {
padding: 8px 16px;
background: var(--vscode-button-background);
color: var(--vscode-button-foreground);
border: none;
border-radius: 2px;
cursor: pointer;
}
</style>
<div class="container">
<div>Count: <span>${this.state.count}</span></div>
<button type="button" data-action="increment">+</button>
<button type="button" data-action="decrement">-</button>
</div>
`;
}
setupEventListeners() {
this.abortController = new AbortController();
this.shadowRoot.addEventListener('click', (e) => {
const action = e.target.closest('[data-action]')?.dataset.action;
if (action === 'increment') {
this.setState({ count: this.state.count + 1 }); // ✓ State update triggers render
} else if (action === 'decrement') {
this.setState({ count: this.state.count - 1 });
}
}, { signal: this.abortController.signal });
}
}
customElements.define('ds-counter', Counter);
Key Changes:
- ✓ State in
this.stateobject - ✓
setState()method for immutable updates - ✓ State changes trigger
render() - ✓ No direct DOM manipulation
- ✓ Subscribe to global state via contextStore
- ✓ Cleanup subscriptions in
disconnectedCallback
Complete Example: Full Migration
❌ Before (All Anti-Patterns)
export default class OldComponent extends HTMLElement {
connectedCallback() {
this.data = [];
this.render();
}
async loadData() {
console.log('Loading...');
const response = await fetch('/api/data');
this.data = await response.json();
this.render();
}
render() {
this.innerHTML = `
<div style="padding: 24px; background: #1e1e1e;">
<h2 style="color: white; font-size: 18px;">Title</h2>
<div class="item" onclick="alert('clicked')" onmouseover="this.style.background='#333'" onmouseout="this.style.background=''">
Click me
</div>
<div onclick="this.getRootNode().host.loadData()" style="cursor: pointer; padding: 8px;">Load Data</div>
</div>
`;
}
}
customElements.define('old-component', OldComponent);
✅ After (DSS Standards Compliant)
import { logger } from '../utils/logger.js';
import contextStore from '../stores/context-store.js';
export default class NewComponent extends HTMLElement {
constructor() {
super();
this.attachShadow({ mode: 'open' }); // ✓ Shadow DOM
this.state = {
data: [],
isLoading: false,
error: null
};
}
connectedCallback() {
this.render();
this.setupEventListeners();
this.loadData(); // Initial load
// ✓ Global state subscription
this.unsubscribe = contextStore.subscribeToKey('theme', (newTheme) => {
logger.debug('[NewComponent] Theme changed', { theme: newTheme });
});
}
disconnectedCallback() {
// ✓ Cleanup
if (this.unsubscribe) this.unsubscribe();
if (this.abortController) this.abortController.abort();
}
setState(updates) {
this.state = { ...this.state, ...updates };
this.render();
}
async loadData() {
const endTimer = logger.time('[NewComponent] Data load');
this.setState({ isLoading: true, error: null });
try {
const response = await fetch('/api/data');
if (!response.ok) throw new Error(`HTTP ${response.status}`);
const data = await response.json();
logger.info('[NewComponent] Data loaded', { count: data.length });
this.setState({ data, isLoading: false });
endTimer();
} catch (error) {
logger.error('[NewComponent] Failed to load data', error);
this.setState({
error: error.message,
isLoading: false
});
}
}
render() {
this.shadowRoot.innerHTML = `
<style>
:host {
display: block;
}
.container {
padding: 24px;
background: var(--vscode-sidebar-background);
}
h2 {
color: var(--vscode-foreground);
font-size: 18px;
margin: 0 0 16px 0;
}
.item {
padding: 12px;
background: var(--vscode-list-inactiveSelectionBackground);
border-radius: 4px;
margin-bottom: 8px;
cursor: pointer;
transition: background-color 0.1s;
}
.item:hover {
background: var(--vscode-list-hoverBackground);
}
.item:focus-visible {
outline: 2px solid var(--vscode-focusBorder);
outline-offset: 2px;
}
button {
padding: 8px 16px;
background: var(--vscode-button-background);
color: var(--vscode-button-foreground);
border: none;
border-radius: 2px;
cursor: pointer;
}
button:disabled {
opacity: 0.5;
cursor: not-allowed;
}
</style>
<div class="container">
<h2>Title</h2>
${this.state.data.map((item) => `
<div class="item" tabindex="0" data-action="itemClick" data-item-id="${item.id}">
${item.name}
</div>
`).join('')}
<button
type="button"
data-action="loadData"
?disabled="${this.state.isLoading}">
${this.state.isLoading ? 'Loading...' : 'Load Data'}
</button>
${this.state.error ? `
<div role="alert" style="color: var(--vscode-errorForeground); margin-top: 8px;">
Error: ${this.state.error}
</div>
` : ''}
</div>
`;
}
setupEventListeners() {
this.abortController = new AbortController();
// ✓ Event delegation
this.shadowRoot.addEventListener('click', (e) => {
const action = e.target.closest('[data-action]')?.dataset.action;
if (action === 'itemClick') {
const itemId = e.target.dataset.itemId;
this.handleItemClick(itemId);
} else if (action === 'loadData') {
this.loadData();
}
}, { signal: this.abortController.signal });
// ✓ Keyboard support
this.shadowRoot.addEventListener('keydown', (e) => {
if (e.key === 'Enter' && e.target.hasAttribute('data-action')) {
e.target.click();
}
}, { signal: this.abortController.signal });
}
handleItemClick(itemId) {
logger.debug('[NewComponent] Item clicked', { itemId });
this.dispatchEvent(new CustomEvent('item-selected', {
detail: { itemId },
bubbles: true,
composed: true // ✓ Bubble out of Shadow DOM
}));
}
}
customElements.define('ds-new-component', NewComponent);
All Improvements Applied:
- ✓ Shadow DOM with encapsulated styles
- ✓ No inline event handlers
- ✓ No inline styles (all in
<style>block) - ✓ Semantic
<button>elements withtype="button" - ✓ Event delegation pattern
- ✓ Proper state management with
setState() - ✓ Logger utility instead of console.log
- ✓ Accessibility (keyboard support, focus management, ARIA)
- ✓ Error handling and loading states
- ✓ AbortController for cleanup
- ✓ Custom events for component communication
Testing Your Migration
After migrating, verify compliance:
# Run quality checks
./scripts/verify-quality.sh
# Expected results:
# ✓ No inline event handlers (0)
# ✓ Inline styles ≤10
# ✓ Console statements ≤10
# ✓ All syntax valid
Reference Implementations
Study these files for best practices:
admin-ui/js/workdesks/base-workdesk.jsadmin-ui/js/components/metrics/ds-frontpage.jsadmin-ui/js/components/metrics/ds-metric-card.js
Standards Documentation
Full standards: .knowledge/dss-coding-standards.json
Need help? Check the coding standards JSON for detailed rules, patterns, and enforcement mechanisms.