Files
dss/docs/03_reference/MIGRATION_GUIDE.md
Digital Production Factory 276ed71f31 Initial commit: Clean DSS implementation
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
2025-12-09 18:45:48 -03:00

855 lines
21 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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](#shadow-dom-migration)
- [Inline Event Handler Removal](#inline-event-handler-removal)
- [Inline Style Extraction](#inline-style-extraction)
- [Semantic HTML](#semantic-html)
- [Accessibility Improvements](#accessibility-improvements)
- [Logger Migration](#logger-migration)
- [State Management](#state-management)
---
## Shadow DOM Migration
### ❌ Before (No Shadow DOM)
```javascript
export default class MyComponent extends HTMLElement {
connectedCallback() {
this.innerHTML = `
<div class="container">
<h2>Title</h2>
<p>Content</p>
</div>
`;
}
}
```
### ✅ After (With Shadow DOM)
```javascript
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.innerHTML` to `this.shadowRoot.innerHTML`
- ✓ Extract styles to `<style>` block in Shadow DOM
- ✓ Use VSCode theme CSS variables
---
## Inline Event Handler Removal
### ❌ Before (Inline Events - FORBIDDEN)
```javascript
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)
```javascript
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`, `onmouseout` attributes
- ✓ Use CSS `:hover` for hover effects
- ✓ Event delegation with `data-action` attributes
- ✓ Single event listener using `closest('[data-action]')`
- ✓ AbortController for automatic cleanup
- ✓ Custom events for component communication
---
## Inline Style Extraction
### ❌ Before (Inline Styles Everywhere)
```javascript
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)
```javascript
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)
```javascript
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)
```javascript
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-label` for icon-only buttons
- ✓ Keyboard accessible by default
- ✓ Proper focus management
---
## Accessibility Improvements
### ❌ Before (Poor A11y)
```javascript
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)
```javascript
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>` with `aria-label`
- ✓ Focus trapping for modals
- ✓ Keyboard support (Tab, Shift+Tab, Escape)
- ✓ Focus restoration on close
-`:focus-visible` styling
---
## Logger Migration
### ❌ Before (console.log Everywhere)
```javascript
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)
```javascript
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:**
```javascript
// 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)
```javascript
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)
```javascript
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.state` object
-`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)
```javascript
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)
```javascript
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 with `type="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:
```bash
# 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.js`
- `admin-ui/js/components/metrics/ds-frontpage.js`
- `admin-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.