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:
Digital Production Factory
2025-12-09 18:45:48 -03:00
commit 276ed71f31
884 changed files with 373737 additions and 0 deletions

View File

@@ -0,0 +1,190 @@
/**
* ds-component-list.js
* Component listing and management interface
* Shows all components with links to Storybook and adoption stats
*/
import URLBuilder from '../../utils/url-builder.js';
export default class ComponentList extends HTMLElement {
constructor() {
super();
this.components = [
{ id: 'button', name: 'Button', category: 'Inputs', adoption: 95, variants: 12 },
{ id: 'input', name: 'Input Field', category: 'Inputs', adoption: 88, variants: 8 },
{ id: 'card', name: 'Card', category: 'Containers', adoption: 92, variants: 5 },
{ id: 'modal', name: 'Modal', category: 'Containers', adoption: 78, variants: 3 },
{ id: 'badge', name: 'Badge', category: 'Status', adoption: 85, variants: 6 },
{ id: 'tooltip', name: 'Tooltip', category: 'Helpers', adoption: 72, variants: 4 },
{ id: 'dropdown', name: 'Dropdown', category: 'Inputs', adoption: 81, variants: 4 },
{ id: 'pagination', name: 'Pagination', category: 'Navigation', adoption: 65, variants: 2 },
];
this.selectedCategory = 'All';
}
connectedCallback() {
this.render();
this.setupEventListeners();
}
render() {
this.innerHTML = `
<div style="padding: 24px; height: 100%; overflow-y: auto;">
<div style="margin-bottom: 24px;">
<h1 style="margin: 0 0 8px 0; font-size: 24px;">Design System Components</h1>
<p style="margin: 0; color: var(--vscode-text-dim);">
Browse, preview, and track component adoption
</p>
</div>
<!-- Filter Bar -->
<div style="margin-bottom: 24px; display: flex; gap: 8px; flex-wrap: wrap;">
<button class="filter-btn" data-category="All" style="
padding: 6px 12px;
background: var(--vscode-selection);
color: var(--vscode-foreground);
border: 1px solid var(--vscode-focusBorder);
border-radius: 4px;
cursor: pointer;
font-size: 12px;
font-weight: 500;
">All</button>
${['Inputs', 'Containers', 'Status', 'Helpers', 'Navigation'].map(cat => `
<button class="filter-btn" data-category="${cat}" style="
padding: 6px 12px;
background: var(--vscode-sidebar);
color: var(--vscode-foreground);
border: 1px solid var(--vscode-border);
border-radius: 4px;
cursor: pointer;
font-size: 12px;
font-weight: 500;
">${cat}</button>
`).join('')}
</div>
<!-- Components Grid -->
<div style="display: grid; grid-template-columns: repeat(auto-fill, minmax(300px, 1fr)); gap: 16px;">
${this.renderComponents()}
</div>
</div>
`;
}
renderComponents() {
const filtered = this.selectedCategory === 'All'
? this.components
: this.components.filter(c => c.category === this.selectedCategory);
return filtered.map(component => `
<div style="
background: var(--vscode-sidebar);
border: 1px solid var(--vscode-border);
border-radius: 4px;
overflow: hidden;
">
<!-- Header -->
<div style="
background: var(--vscode-bg);
padding: 12px;
border-bottom: 1px solid var(--vscode-border);
">
<div style="display: flex; justify-content: space-between; align-items: start; margin-bottom: 8px;">
<div>
<div style="font-weight: 600; font-size: 14px; margin-bottom: 2px;">
${component.name}
</div>
<div style="font-size: 11px; color: var(--vscode-text-dim);">
${component.category}
</div>
</div>
<span style="
background: #4caf50;
color: white;
padding: 2px 8px;
border-radius: 3px;
font-size: 10px;
font-weight: 600;
">${component.adoption}%</span>
</div>
</div>
<!-- Content -->
<div style="padding: 12px;">
<div style="display: flex; justify-content: space-between; margin-bottom: 12px; font-size: 12px;">
<span>Variants: <strong>${component.variants}</strong></span>
<span>Adoption: <strong>${component.adoption}%</strong></span>
</div>
<!-- Progress Bar -->
<div style="width: 100%; height: 4px; background: var(--vscode-bg); border-radius: 2px; overflow: hidden; margin-bottom: 12px;">
<div style="width: ${component.adoption}%; height: 100%; background: #4caf50;"></div>
</div>
<!-- Actions -->
<div style="display: flex; gap: 8px;">
<button class="storybook-btn" data-component-id="${component.id}" style="
flex: 1;
padding: 6px;
background: var(--vscode-button-background);
color: var(--vscode-button-foreground);
border: none;
border-radius: 3px;
cursor: pointer;
font-size: 11px;
font-weight: 500;
">📖 Storybook</button>
<button class="edit-btn" data-component-id="${component.id}" style="
flex: 1;
padding: 6px;
background: var(--vscode-button-secondaryBackground);
color: var(--vscode-button-secondaryForeground);
border: none;
border-radius: 3px;
cursor: pointer;
font-size: 11px;
font-weight: 500;
">✏️ Edit</button>
</div>
</div>
</div>
`).join('');
}
setupEventListeners() {
// Filter buttons
this.querySelectorAll('.filter-btn').forEach(btn => {
btn.addEventListener('click', () => {
this.selectedCategory = btn.dataset.category;
this.render();
this.setupEventListeners();
});
});
// Storybook buttons
this.querySelectorAll('.storybook-btn').forEach(btn => {
btn.addEventListener('click', () => {
const componentId = btn.dataset.componentId;
const component = this.components.find(c => c.id === componentId);
if (component) {
const url = URLBuilder.getComponentUrl(component);
window.open(url, '_blank');
}
});
});
// Edit buttons
this.querySelectorAll('.edit-btn').forEach(btn => {
btn.addEventListener('click', () => {
const componentId = btn.dataset.componentId;
this.dispatchEvent(new CustomEvent('edit-component', {
detail: { componentId },
bubbles: true,
composed: true
}));
});
});
}
}
customElements.define('ds-component-list', ComponentList);

View File

@@ -0,0 +1,249 @@
/**
* ds-icon-list.js
* Icon gallery and management
* Browse and export icons from the design system
*/
export default class IconList extends HTMLElement {
constructor() {
super();
this.icons = [
{ id: 'check', name: 'Check', category: 'Status', svg: '✓', tags: ['status', 'success', 'validation'] },
{ id: 'x', name: 'Close', category: 'Status', svg: '✕', tags: ['status', 'error', 'dismiss'] },
{ id: 'info', name: 'Info', category: 'Status', svg: 'ⓘ', tags: ['status', 'information', 'help'] },
{ id: 'warning', name: 'Warning', category: 'Status', svg: '⚠', tags: ['status', 'warning', 'alert'] },
{ id: 'arrow-right', name: 'Arrow Right', category: 'Navigation', svg: '→', tags: ['navigation', 'direction', 'next'] },
{ id: 'arrow-left', name: 'Arrow Left', category: 'Navigation', svg: '←', tags: ['navigation', 'direction', 'back'] },
{ id: 'arrow-up', name: 'Arrow Up', category: 'Navigation', svg: '↑', tags: ['navigation', 'direction', 'up'] },
{ id: 'arrow-down', name: 'Arrow Down', category: 'Navigation', svg: '↓', tags: ['navigation', 'direction', 'down'] },
{ id: 'search', name: 'Search', category: 'Actions', svg: '🔍', tags: ['action', 'search', 'find'] },
{ id: 'settings', name: 'Settings', category: 'Actions', svg: '⚙', tags: ['action', 'settings', 'config'] },
{ id: 'download', name: 'Download', category: 'Actions', svg: '⬇', tags: ['action', 'download', 'save'] },
{ id: 'upload', name: 'Upload', category: 'Actions', svg: '⬆', tags: ['action', 'upload', 'import'] },
];
this.selectedCategory = 'All';
this.searchTerm = '';
}
connectedCallback() {
this.render();
this.setupEventListeners();
}
render() {
this.innerHTML = `
<div style="padding: 24px; height: 100%; overflow-y: auto;">
<div style="margin-bottom: 24px;">
<h1 style="margin: 0 0 8px 0; font-size: 24px;">Icon Library</h1>
<p style="margin: 0; color: var(--vscode-text-dim);">
Browse and manage icon assets
</p>
</div>
<!-- Search and Filter -->
<div style="margin-bottom: 24px; display: flex; gap: 12px;">
<input
id="icon-search"
type="text"
placeholder="Search icons..."
style="
flex: 1;
padding: 8px 12px;
border: 1px solid var(--vscode-input-border);
background: var(--vscode-input-background);
color: var(--vscode-foreground);
border-radius: 4px;
font-size: 12px;
"
/>
<select id="icon-filter" style="
padding: 8px 12px;
border: 1px solid var(--vscode-input-border);
background: var(--vscode-input-background);
color: var(--vscode-foreground);
border-radius: 4px;
font-size: 12px;
">
<option value="All">All Categories</option>
<option value="Status">Status</option>
<option value="Navigation">Navigation</option>
<option value="Actions">Actions</option>
</select>
</div>
<!-- Icon Grid -->
<div style="display: grid; grid-template-columns: repeat(auto-fill, minmax(100px, 1fr)); gap: 12px; margin-bottom: 24px;">
${this.renderIconCards()}
</div>
<!-- Export Section -->
<div style="background: var(--vscode-sidebar); border: 1px solid var(--vscode-border); border-radius: 4px; padding: 16px;">
<h3 style="margin: 0 0 12px 0; font-size: 14px;">Export Options</h3>
<div style="display: flex; gap: 8px;">
<button id="export-svg-btn" style="
padding: 8px 16px;
background: var(--vscode-button-secondaryBackground);
color: var(--vscode-button-secondaryForeground);
border: none;
border-radius: 4px;
cursor: pointer;
font-weight: 500;
font-size: 12px;
">📦 Export as SVG</button>
<button id="export-font-btn" style="
padding: 8px 16px;
background: var(--vscode-button-secondaryBackground);
color: var(--vscode-button-secondaryForeground);
border: none;
border-radius: 4px;
cursor: pointer;
font-weight: 500;
font-size: 12px;
">🔤 Export as Font</button>
<button id="export-json-btn" style="
padding: 8px 16px;
background: var(--vscode-button-secondaryBackground);
color: var(--vscode-button-secondaryForeground);
border: none;
border-radius: 4px;
cursor: pointer;
font-weight: 500;
font-size: 12px;
">📄 Export as JSON</button>
</div>
</div>
</div>
`;
}
renderIconCards() {
let filtered = this.icons;
if (this.selectedCategory !== 'All') {
filtered = filtered.filter(i => i.category === this.selectedCategory);
}
if (this.searchTerm) {
const term = this.searchTerm.toLowerCase();
filtered = filtered.filter(i =>
i.name.toLowerCase().includes(term) ||
i.id.toLowerCase().includes(term) ||
i.tags.some(t => t.includes(term))
);
}
return filtered.map(icon => `
<div class="icon-card" data-icon-id="${icon.id}" style="
background: var(--vscode-sidebar);
border: 1px solid var(--vscode-border);
border-radius: 4px;
padding: 12px;
display: flex;
flex-direction: column;
align-items: center;
cursor: pointer;
transition: all 0.2s;
">
<div style="
font-size: 32px;
margin-bottom: 8px;
width: 48px;
height: 48px;
display: flex;
align-items: center;
justify-content: center;
background: var(--vscode-bg);
border-radius: 3px;
">${icon.svg}</div>
<div style="text-align: center; width: 100%;">
<div style="font-size: 11px; font-weight: 500; margin-bottom: 2px;">
${icon.name}
</div>
<div style="font-size: 10px; color: var(--vscode-text-dim); font-family: monospace;">
${icon.id}
</div>
<div style="font-size: 9px; color: var(--vscode-text-dim); margin-top: 4px;">
${icon.category}
</div>
</div>
</div>
`).join('');
}
setupEventListeners() {
// Search input
const searchInput = this.querySelector('#icon-search');
if (searchInput) {
searchInput.addEventListener('input', (e) => {
this.searchTerm = e.target.value;
this.render();
this.setupEventListeners();
});
}
// Category filter
const filterSelect = this.querySelector('#icon-filter');
if (filterSelect) {
filterSelect.addEventListener('change', (e) => {
this.selectedCategory = e.target.value;
this.render();
this.setupEventListeners();
});
}
// Icon cards (copy on click)
this.querySelectorAll('.icon-card').forEach(card => {
card.addEventListener('click', () => {
const iconId = card.dataset.iconId;
navigator.clipboard.writeText(iconId).then(() => {
const originalBg = card.style.background;
card.style.background = 'var(--vscode-selection)';
setTimeout(() => {
card.style.background = originalBg;
}, 300);
});
});
});
// Export buttons
const exportSvgBtn = this.querySelector('#export-svg-btn');
if (exportSvgBtn) {
exportSvgBtn.addEventListener('click', () => {
this.downloadIcons('svg');
});
}
const exportJsonBtn = this.querySelector('#export-json-btn');
if (exportJsonBtn) {
exportJsonBtn.addEventListener('click', () => {
this.downloadIcons('json');
});
}
}
downloadIcons(format) {
const data = format === 'json'
? JSON.stringify(this.icons, null, 2)
: this.generateSVGSheet();
const blob = new Blob([data], { type: format === 'json' ? 'application/json' : 'image/svg+xml' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = `icons.${format === 'json' ? 'json' : 'svg'}`;
a.click();
URL.revokeObjectURL(url);
}
generateSVGSheet() {
return `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1200 800">
${this.icons.map((icon, i) => `
<text x="${(i % 12) * 100 + 50}" y="${Math.floor(i / 12) * 100 + 50}" font-size="40" text-anchor="middle">
${icon.svg}
</text>
`).join('')}
</svg>`;
}
}
customElements.define('ds-icon-list', IconList);

View File

@@ -0,0 +1,293 @@
/**
* ds-jira-issues.js
* Jira issue tracker integration
* View project-specific Jira issues for design system work
*/
import contextStore from '../../stores/context-store.js';
export default class JiraIssues extends HTMLElement {
constructor() {
super();
this.state = {
projectId: null,
issues: [],
filterStatus: 'All',
isLoading: false
};
// Mock data for demo
this.mockIssues = [
{ key: 'DSS-234', summary: 'Add Button component variants', status: 'In Progress', type: 'Task', priority: 'High', assignee: 'John Doe' },
{ key: 'DSS-235', summary: 'Update color token naming convention', status: 'To Do', type: 'Story', priority: 'Medium', assignee: 'Unassigned' },
{ key: 'DSS-236', summary: 'Fix Card component accessibility', status: 'In Review', type: 'Bug', priority: 'High', assignee: 'Jane Smith' },
{ key: 'DSS-237', summary: 'Document Typography system', status: 'Done', type: 'Task', priority: 'Low', assignee: 'Mike Johnson' },
{ key: 'DSS-238', summary: 'Create Icon font export', status: 'To Do', type: 'Task', priority: 'Medium', assignee: 'Sarah Wilson' },
{ key: 'DSS-239', summary: 'Implement Figma sync automation', status: 'In Progress', type: 'Epic', priority: 'High', assignee: 'John Doe' },
];
}
connectedCallback() {
this.render();
this.setupEventListeners();
// Subscribe to project context changes
this.unsubscribe = contextStore.subscribe(({ state }) => {
this.state.projectId = state.projectId;
this.loadIssues();
});
// Load initial issues
this.state.projectId = contextStore.get('projectId');
this.loadIssues();
}
disconnectedCallback() {
if (this.unsubscribe) this.unsubscribe();
}
async loadIssues() {
this.state.isLoading = true;
this.renderLoading();
// Simulate API call delay
await new Promise(resolve => setTimeout(resolve, 800));
// In real implementation, fetch from Jira API via backend
this.state.issues = this.mockIssues;
this.state.isLoading = false;
this.render();
this.setupEventListeners();
}
renderLoading() {
this.innerHTML = `
<div style="padding: 24px; display: flex; align-items: center; justify-content: center; height: 100%;">
<div style="text-align: center;">
<div style="font-size: 24px; margin-bottom: 12px;">⏳</div>
<div style="font-size: 12px; color: var(--vscode-text-dim);">Loading Jira issues...</div>
</div>
</div>
`;
}
render() {
this.innerHTML = `
<div style="padding: 24px; height: 100%; overflow-y: auto;">
<div style="margin-bottom: 24px; display: flex; justify-content: space-between; align-items: start;">
<div>
<h1 style="margin: 0 0 8px 0; font-size: 24px;">Jira Issues</h1>
<p style="margin: 0; color: var(--vscode-text-dim);">
${this.state.projectId ? `Project: ${this.state.projectId}` : 'Select a project to view issues'}
</p>
</div>
<button id="create-issue-btn" style="
padding: 8px 16px;
background: var(--vscode-button-background);
color: var(--vscode-button-foreground);
border: none;
border-radius: 4px;
cursor: pointer;
font-weight: 500;
font-size: 12px;
">+ New Issue</button>
</div>
<!-- Status Filter -->
<div style="margin-bottom: 24px; display: flex; gap: 8px;">
${['All', 'To Do', 'In Progress', 'In Review', 'Done'].map(status => `
<button class="status-filter" data-status="${status}" style="
padding: 6px 12px;
background: ${this.state.filterStatus === status ? 'var(--vscode-selection)' : 'var(--vscode-sidebar)'};
color: var(--vscode-foreground);
border: 1px solid ${this.state.filterStatus === status ? 'var(--vscode-focusBorder)' : 'var(--vscode-border)'};
border-radius: 4px;
cursor: pointer;
font-size: 12px;
font-weight: 500;
">${status}</button>
`).join('')}
</div>
<!-- Issues List -->
<div style="display: flex; flex-direction: column; gap: 12px;">
${this.renderIssuesList()}
</div>
</div>
`;
}
renderIssuesList() {
const filtered = this.state.filterStatus === 'All'
? this.state.issues
: this.state.issues.filter(i => i.status === this.state.filterStatus);
if (filtered.length === 0) {
return `
<div style="
background: var(--vscode-sidebar);
border: 1px solid var(--vscode-border);
border-radius: 4px;
padding: 24px;
text-align: center;
color: var(--vscode-text-dim);
">
No issues found in this status
</div>
`;
}
return filtered.map(issue => `
<div class="jira-issue" data-issue-key="${issue.key}" style="
background: var(--vscode-sidebar);
border: 1px solid var(--vscode-border);
border-radius: 4px;
padding: 16px;
cursor: pointer;
transition: all 0.2s;
">
<div style="display: flex; justify-content: space-between; align-items: start; margin-bottom: 12px;">
<div style="display: flex; gap: 12px; align-items: start; flex: 1;">
<!-- Issue Type Badge -->
<div style="
padding: 4px 8px;
background: ${this.getTypeColor(issue.type)};
color: white;
border-radius: 3px;
font-size: 10px;
font-weight: 600;
min-width: 50px;
text-align: center;
">${issue.type}</div>
<!-- Issue Content -->
<div style="flex: 1;">
<div style="display: flex; align-items: center; gap: 12px; margin-bottom: 6px;">
<span style="font-family: monospace; font-weight: 600; color: #0066CC;">
${issue.key}
</span>
<span style="font-weight: 500; font-size: 13px;">
${issue.summary}
</span>
</div>
<div style="display: flex; gap: 12px; font-size: 11px; color: var(--vscode-text-dim);">
<span>Assignee: ${issue.assignee}</span>
<span>Priority: ${issue.priority}</span>
</div>
</div>
</div>
<!-- Status Badge -->
<div style="
padding: 4px 12px;
background: var(--vscode-bg);
border: 1px solid var(--vscode-border);
border-radius: 3px;
font-size: 11px;
font-weight: 500;
">${issue.status}</div>
</div>
<!-- Actions -->
<div style="display: flex; gap: 8px; padding-top: 12px; border-top: 1px solid var(--vscode-border);">
<button class="open-issue-btn" style="
padding: 4px 12px;
background: var(--vscode-button-secondaryBackground);
color: var(--vscode-button-secondaryForeground);
border: none;
border-radius: 3px;
cursor: pointer;
font-size: 11px;
">Open in Jira</button>
<button class="link-pr-btn" style="
padding: 4px 12px;
background: var(--vscode-button-secondaryBackground);
color: var(--vscode-button-secondaryForeground);
border: none;
border-radius: 3px;
cursor: pointer;
font-size: 11px;
">Link PR</button>
<button class="assign-btn" style="
padding: 4px 12px;
background: var(--vscode-button-secondaryBackground);
color: var(--vscode-button-secondaryForeground);
border: none;
border-radius: 3px;
cursor: pointer;
font-size: 11px;
">Assign</button>
</div>
</div>
`).join('');
}
getTypeColor(type) {
const colors = {
'Bug': '#f44336',
'Task': '#2196f3',
'Story': '#4caf50',
'Epic': '#9c27b0',
'Subtask': '#ff9800'
};
return colors[type] || '#999';
}
setupEventListeners() {
// Status filters
this.querySelectorAll('.status-filter').forEach(btn => {
btn.addEventListener('click', () => {
this.state.filterStatus = btn.dataset.status;
this.render();
this.setupEventListeners();
});
});
// Create issue button
const createBtn = this.querySelector('#create-issue-btn');
if (createBtn) {
createBtn.addEventListener('click', () => {
this.dispatchEvent(new CustomEvent('create-issue', {
detail: { projectId: this.state.projectId },
bubbles: true,
composed: true
}));
});
}
// Issue actions
this.querySelectorAll('.open-issue-btn').forEach(btn => {
btn.addEventListener('click', (e) => {
e.stopPropagation();
const issueKey = e.target.closest('.jira-issue').dataset.issueKey;
window.open(`https://jira.atlassian.net/browse/${issueKey}`, '_blank');
});
});
this.querySelectorAll('.link-pr-btn').forEach(btn => {
btn.addEventListener('click', (e) => {
e.stopPropagation();
const issueKey = e.target.closest('.jira-issue').dataset.issueKey;
this.dispatchEvent(new CustomEvent('link-pr', {
detail: { issueKey },
bubbles: true,
composed: true
}));
});
});
this.querySelectorAll('.assign-btn').forEach(btn => {
btn.addEventListener('click', (e) => {
e.stopPropagation();
const issueKey = e.target.closest('.jira-issue').dataset.issueKey;
this.dispatchEvent(new CustomEvent('assign-issue', {
detail: { issueKey },
bubbles: true,
composed: true
}));
});
});
}
}
customElements.define('ds-jira-issues', JiraIssues);

View File

@@ -0,0 +1,197 @@
/**
* ds-token-list.js
* Design token listing and management
* View, edit, and validate design tokens
*/
import contextStore from '../../stores/context-store.js';
export default class TokenList extends HTMLElement {
constructor() {
super();
this.tokens = [
{ id: 'color-primary', name: 'Primary Color', category: 'Colors', value: '#0066CC', usage: 156 },
{ id: 'color-success', name: 'Success Color', category: 'Colors', value: '#4caf50', usage: 89 },
{ id: 'color-error', name: 'Error Color', category: 'Colors', value: '#f44336', usage: 76 },
{ id: 'color-warning', name: 'Warning Color', category: 'Colors', value: '#ff9800', usage: 54 },
{ id: 'spacing-xs', name: 'Extra Small Spacing', category: 'Spacing', value: '4px', usage: 234 },
{ id: 'spacing-sm', name: 'Small Spacing', category: 'Spacing', value: '8px', usage: 312 },
{ id: 'spacing-md', name: 'Medium Spacing', category: 'Spacing', value: '16px', usage: 445 },
{ id: 'spacing-lg', name: 'Large Spacing', category: 'Spacing', value: '24px', usage: 198 },
{ id: 'font-body', name: 'Body Font', category: 'Typography', value: 'Inter, sans-serif', usage: 678 },
{ id: 'font-heading', name: 'Heading Font', category: 'Typography', value: 'Poppins, sans-serif', usage: 234 },
];
this.selectedCategory = 'All';
this.editingTokenId = null;
}
connectedCallback() {
this.render();
this.setupEventListeners();
}
render() {
this.innerHTML = `
<div style="padding: 24px; height: 100%; overflow-y: auto;">
<div style="margin-bottom: 24px; display: flex; justify-content: space-between; align-items: start;">
<div>
<h1 style="margin: 0 0 8px 0; font-size: 24px;">Design Tokens</h1>
<p style="margin: 0; color: var(--vscode-text-dim);">
Manage and track design token usage across the system
</p>
</div>
<button id="export-btn" style="
padding: 8px 16px;
background: var(--vscode-button-background);
color: var(--vscode-button-foreground);
border: none;
border-radius: 4px;
cursor: pointer;
font-weight: 500;
font-size: 12px;
">⬇️ Export Tokens</button>
</div>
<!-- Category Filter -->
<div style="margin-bottom: 24px; display: flex; gap: 8px; flex-wrap: wrap;">
${['All', 'Colors', 'Spacing', 'Typography', 'Shadows', 'Borders'].map(cat => `
<button class="filter-btn" data-category="${cat}" style="
padding: 6px 12px;
background: ${this.selectedCategory === cat ? 'var(--vscode-selection)' : 'var(--vscode-sidebar)'};
color: var(--vscode-foreground);
border: 1px solid ${this.selectedCategory === cat ? 'var(--vscode-focusBorder)' : 'var(--vscode-border)'};
border-radius: 4px;
cursor: pointer;
font-size: 12px;
font-weight: 500;
">${cat}</button>
`).join('')}
</div>
<!-- Token Table -->
<div style="background: var(--vscode-sidebar); border: 1px solid var(--vscode-border); border-radius: 4px; overflow: hidden;">
<div style="display: grid; grid-template-columns: 2fr 1fr 1fr 0.8fr 0.8fr; gap: 0; font-size: 11px; font-weight: 600; background: var(--vscode-bg); border-bottom: 1px solid var(--vscode-border); padding: 12px; color: var(--vscode-text-dim); text-transform: uppercase; letter-spacing: 0.5px;">
<div>Token Name</div>
<div>Category</div>
<div>Value</div>
<div>Usage</div>
<div>Actions</div>
</div>
<div id="tokens-container" style="max-height: 500px; overflow-y: auto;">
${this.renderTokenRows()}
</div>
</div>
</div>
`;
}
renderTokenRows() {
const filtered = this.selectedCategory === 'All'
? this.tokens
: this.tokens.filter(t => t.category === this.selectedCategory);
return filtered.map(token => `
<div style="
display: grid;
grid-template-columns: 2fr 1fr 1fr 0.8fr 0.8fr;
gap: 0;
align-items: center;
padding: 12px;
border-bottom: 1px solid var(--vscode-border);
font-size: 12px;
">
<div>
<div style="font-weight: 500; margin-bottom: 2px;">${token.name}</div>
<div style="font-size: 10px; color: var(--vscode-text-dim); font-family: monospace;">
${token.id}
</div>
</div>
<div style="color: var(--vscode-text-dim); font-size: 11px;">
${token.category}
</div>
<div style="font-family: monospace; background: var(--vscode-bg); padding: 4px 6px; border-radius: 2px;">
${token.value}
</div>
<div style="text-align: center; color: var(--vscode-text-dim);">
${token.usage}
</div>
<div style="display: flex; gap: 4px;">
<button class="edit-token-btn" data-token-id="${token.id}" style="
padding: 3px 8px;
background: var(--vscode-button-secondaryBackground);
color: var(--vscode-button-secondaryForeground);
border: none;
border-radius: 2px;
cursor: pointer;
font-size: 10px;
">Edit</button>
<button class="copy-token-btn" data-token-value="${token.value}" style="
padding: 3px 8px;
background: var(--vscode-button-secondaryBackground);
color: var(--vscode-button-secondaryForeground);
border: none;
border-radius: 2px;
cursor: pointer;
font-size: 10px;
">Copy</button>
</div>
</div>
`).join('');
}
setupEventListeners() {
// Filter buttons
this.querySelectorAll('.filter-btn').forEach(btn => {
btn.addEventListener('click', () => {
this.selectedCategory = btn.dataset.category;
this.render();
this.setupEventListeners();
});
});
// Export button
const exportBtn = this.querySelector('#export-btn');
if (exportBtn) {
exportBtn.addEventListener('click', () => {
const tokenData = JSON.stringify(this.tokens, null, 2);
const blob = new Blob([tokenData], { type: 'application/json' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = 'design-tokens.json';
a.click();
URL.revokeObjectURL(url);
});
}
// Edit token buttons
this.querySelectorAll('.edit-token-btn').forEach(btn => {
btn.addEventListener('click', () => {
const tokenId = btn.dataset.tokenId;
const token = this.tokens.find(t => t.id === tokenId);
this.dispatchEvent(new CustomEvent('edit-token', {
detail: { token },
bubbles: true,
composed: true
}));
});
});
// Copy token buttons
this.querySelectorAll('.copy-token-btn').forEach(btn => {
btn.addEventListener('click', () => {
const value = btn.dataset.tokenValue;
navigator.clipboard.writeText(value).then(() => {
const originalText = btn.textContent;
btn.textContent = '✓ Copied';
setTimeout(() => {
btn.textContent = originalText;
}, 1500);
});
});
});
}
}
customElements.define('ds-token-list', TokenList);