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
This commit is contained in:
854
docs/03_reference/MIGRATION_GUIDE.md
Normal file
854
docs/03_reference/MIGRATION_GUIDE.md
Normal file
@@ -0,0 +1,854 @@
|
||||
# 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.
|
||||
Reference in New Issue
Block a user