feat: Add DSS infrastructure, remove legacy admin-ui code
Some checks failed
DSS Project Analysis / dss-context-update (push) Has been cancelled

- Remove legacy admin-ui/js/ vanilla JS components
- Add .dss/ directory with core tokens, skins, themes
- Add Storybook configuration and generated stories
- Add DSS management scripts (dss-services, dss-init, dss-setup, dss-reset)
- Add MCP command definitions for DSS plugin
- Add Figma sync architecture and scripts
- Update pre-commit hooks with documentation validation
- Fix JSON trailing commas in skin files

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
DSS
2025-12-10 22:15:11 -03:00
parent 71c6dc805a
commit 08ce228df1
205 changed files with 65666 additions and 47577 deletions

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,837 @@
{
"_meta": {
"description": "shadcn/ui Component Registry - All 59 components with variants and tokens",
"version": "1.0.0",
"source": "https://ui.shadcn.com/docs/components",
"lastUpdated": "2025-12-10"
},
"components": {
"accordion": {
"name": "Accordion",
"category": "data-display",
"description": "A vertically stacked set of interactive headings that reveal content sections",
"radixPrimitive": "@radix-ui/react-accordion",
"variants": {},
"tokens": {
"background": "var(--color-background)",
"border": "var(--color-border)",
"text": "var(--color-foreground)"
}
},
"alert": {
"name": "Alert",
"category": "feedback",
"description": "Displays a callout for user attention",
"variants": {
"variant": ["default", "destructive"]
},
"tokens": {
"background": "var(--color-background)",
"border": "var(--color-border)",
"foreground": "var(--color-foreground)",
"destructive": "var(--color-destructive)"
}
},
"alert-dialog": {
"name": "Alert Dialog",
"category": "overlay",
"description": "A modal dialog that interrupts user flow with important information",
"radixPrimitive": "@radix-ui/react-alert-dialog",
"tokens": {
"overlay": "var(--color-background)/80",
"background": "var(--color-background)",
"border": "var(--color-border)"
}
},
"aspect-ratio": {
"name": "Aspect Ratio",
"category": "layout",
"description": "Displays content with a desired aspect ratio",
"radixPrimitive": "@radix-ui/react-aspect-ratio"
},
"avatar": {
"name": "Avatar",
"category": "data-display",
"description": "An image element with a fallback for user profile images",
"radixPrimitive": "@radix-ui/react-avatar",
"variants": {
"size": ["sm", "md", "lg"]
},
"tokens": {
"background": "var(--color-muted)",
"fallback": "var(--color-muted-foreground)"
}
},
"badge": {
"name": "Badge",
"category": "data-display",
"description": "Displays a badge or label",
"variants": {
"variant": ["default", "secondary", "destructive", "outline"]
},
"tokens": {
"default": {
"background": "var(--color-primary)",
"text": "var(--color-primary-foreground)"
},
"secondary": {
"background": "var(--color-secondary)",
"text": "var(--color-secondary-foreground)"
},
"destructive": {
"background": "var(--color-destructive)",
"text": "var(--color-destructive-foreground)"
},
"outline": {
"border": "var(--color-border)",
"text": "var(--color-foreground)"
}
}
},
"breadcrumb": {
"name": "Breadcrumb",
"category": "navigation",
"description": "Displays the path to the current page in a hierarchy",
"tokens": {
"text": "var(--color-muted-foreground)",
"active": "var(--color-foreground)",
"separator": "var(--color-muted-foreground)"
}
},
"button": {
"name": "Button",
"category": "form",
"description": "Displays a button or a component that looks like a button",
"variants": {
"variant": ["default", "destructive", "outline", "secondary", "ghost", "link"],
"size": ["default", "sm", "lg", "icon", "icon-sm", "icon-lg"]
},
"tokens": {
"default": {
"background": "var(--color-primary)",
"text": "var(--color-primary-foreground)",
"hover": "var(--color-primary)/90"
},
"destructive": {
"background": "var(--color-destructive)",
"text": "var(--color-destructive-foreground)",
"hover": "var(--color-destructive)/90"
},
"outline": {
"border": "var(--color-input)",
"background": "var(--color-background)",
"text": "var(--color-foreground)",
"hover": "var(--color-accent)"
},
"secondary": {
"background": "var(--color-secondary)",
"text": "var(--color-secondary-foreground)",
"hover": "var(--color-secondary)/80"
},
"ghost": {
"text": "var(--color-foreground)",
"hover": "var(--color-accent)"
},
"link": {
"text": "var(--color-primary)"
}
},
"sizing": {
"default": { "height": "2.5rem", "px": "1rem", "py": "0.5rem" },
"sm": { "height": "2rem", "px": "0.75rem" },
"lg": { "height": "3rem", "px": "2rem" },
"icon": { "size": "2.5rem" },
"icon-sm": { "size": "2rem" },
"icon-lg": { "size": "2.75rem" }
}
},
"button-group": {
"name": "Button Group",
"category": "form",
"description": "Groups multiple buttons together",
"tokens": {
"gap": "var(--spacing-1)"
}
},
"calendar": {
"name": "Calendar",
"category": "form",
"description": "A date picker component with monthly/yearly views",
"dependencies": ["react-day-picker"],
"tokens": {
"background": "var(--color-background)",
"text": "var(--color-foreground)",
"selected": "var(--color-primary)",
"today": "var(--color-accent)"
}
},
"card": {
"name": "Card",
"category": "layout",
"description": "Displays a card with header, content, and footer",
"subComponents": ["CardHeader", "CardTitle", "CardDescription", "CardContent", "CardFooter"],
"tokens": {
"background": "var(--color-card)",
"foreground": "var(--color-card-foreground)",
"border": "var(--color-border)",
"shadow": "var(--shadow-sm)"
}
},
"carousel": {
"name": "Carousel",
"category": "data-display",
"description": "A carousel with embla-carousel",
"dependencies": ["embla-carousel-react"],
"tokens": {
"button": "var(--color-background)",
"indicator": "var(--color-muted)"
}
},
"chart": {
"name": "Chart",
"category": "data-display",
"description": "Beautiful charts using Recharts",
"dependencies": ["recharts"],
"tokens": {
"colors": ["var(--chart-1)", "var(--chart-2)", "var(--chart-3)", "var(--chart-4)", "var(--chart-5)"]
}
},
"checkbox": {
"name": "Checkbox",
"category": "form",
"description": "A control that allows toggling between checked and unchecked",
"radixPrimitive": "@radix-ui/react-checkbox",
"tokens": {
"border": "var(--color-primary)",
"checked": "var(--color-primary)",
"checkmark": "var(--color-primary-foreground)"
}
},
"collapsible": {
"name": "Collapsible",
"category": "data-display",
"description": "An interactive component that expands/collapses content",
"radixPrimitive": "@radix-ui/react-collapsible"
},
"combobox": {
"name": "Combobox",
"category": "form",
"description": "Autocomplete input with command palette",
"composedOf": ["command", "popover"],
"tokens": {
"input": "var(--color-input)",
"background": "var(--color-popover)",
"text": "var(--color-popover-foreground)"
}
},
"command": {
"name": "Command",
"category": "form",
"description": "Command palette with search and filtering",
"dependencies": ["cmdk"],
"tokens": {
"background": "var(--color-popover)",
"text": "var(--color-popover-foreground)",
"separator": "var(--color-border)"
}
},
"context-menu": {
"name": "Context Menu",
"category": "overlay",
"description": "Right-click context menu with keyboard support",
"radixPrimitive": "@radix-ui/react-context-menu",
"tokens": {
"background": "var(--color-popover)",
"text": "var(--color-popover-foreground)",
"hover": "var(--color-accent)"
}
},
"data-table": {
"name": "Data Table",
"category": "data-display",
"description": "Powerful table with sorting, filtering, pagination",
"dependencies": ["@tanstack/react-table"],
"composedOf": ["table", "dropdown-menu", "button", "input"],
"tokens": {
"header": "var(--color-muted)",
"row": "var(--color-background)",
"rowHover": "var(--color-muted)/50",
"border": "var(--color-border)"
}
},
"date-picker": {
"name": "Date Picker",
"category": "form",
"description": "A date picker built with calendar and popover",
"composedOf": ["calendar", "popover", "button"],
"tokens": {
"input": "var(--color-input)",
"background": "var(--color-popover)"
}
},
"dialog": {
"name": "Dialog",
"category": "overlay",
"description": "A modal dialog for content display",
"radixPrimitive": "@radix-ui/react-dialog",
"subComponents": ["DialogTrigger", "DialogContent", "DialogHeader", "DialogFooter", "DialogTitle", "DialogDescription"],
"tokens": {
"overlay": "var(--color-background)/80",
"background": "var(--color-background)",
"border": "var(--color-border)"
}
},
"drawer": {
"name": "Drawer",
"category": "overlay",
"description": "A drawer component extending dialog",
"dependencies": ["vaul"],
"tokens": {
"background": "var(--color-background)",
"handle": "var(--color-muted)"
}
},
"dropdown-menu": {
"name": "Dropdown Menu",
"category": "overlay",
"description": "Menu displayed on trigger interaction",
"radixPrimitive": "@radix-ui/react-dropdown-menu",
"tokens": {
"background": "var(--color-popover)",
"text": "var(--color-popover-foreground)",
"hover": "var(--color-accent)",
"separator": "var(--color-border)"
}
},
"empty": {
"name": "Empty",
"category": "feedback",
"description": "Empty state display",
"tokens": {
"icon": "var(--color-muted-foreground)",
"text": "var(--color-muted-foreground)"
}
},
"field": {
"name": "Field",
"category": "form",
"description": "Form field wrapper with label and error",
"composedOf": ["label", "input"],
"tokens": {
"label": "var(--color-foreground)",
"error": "var(--color-destructive)"
}
},
"form": {
"name": "Form",
"category": "form",
"description": "Form component with react-hook-form integration",
"dependencies": ["react-hook-form", "@hookform/resolvers", "zod"],
"tokens": {
"label": "var(--color-foreground)",
"description": "var(--color-muted-foreground)",
"error": "var(--color-destructive)"
}
},
"hover-card": {
"name": "Hover Card",
"category": "overlay",
"description": "Content appearing on hover",
"radixPrimitive": "@radix-ui/react-hover-card",
"tokens": {
"background": "var(--color-popover)",
"text": "var(--color-popover-foreground)",
"border": "var(--color-border)"
}
},
"input": {
"name": "Input",
"category": "form",
"description": "Text input field",
"tokens": {
"background": "var(--color-background)",
"border": "var(--color-input)",
"text": "var(--color-foreground)",
"placeholder": "var(--color-muted-foreground)",
"focus": "var(--color-ring)"
}
},
"input-group": {
"name": "Input Group",
"category": "form",
"description": "Group of inputs with addons",
"tokens": {
"addon": "var(--color-muted)"
}
},
"input-otp": {
"name": "Input OTP",
"category": "form",
"description": "One-time password input",
"dependencies": ["input-otp"],
"tokens": {
"border": "var(--color-input)",
"focus": "var(--color-ring)"
}
},
"item": {
"name": "Item",
"category": "data-display",
"description": "Generic list item component"
},
"kbd": {
"name": "Kbd",
"category": "data-display",
"description": "Keyboard key display",
"tokens": {
"background": "var(--color-muted)",
"text": "var(--color-muted-foreground)",
"border": "var(--color-border)"
}
},
"label": {
"name": "Label",
"category": "form",
"description": "Text label for form elements",
"radixPrimitive": "@radix-ui/react-label",
"tokens": {
"text": "var(--color-foreground)"
}
},
"menubar": {
"name": "Menubar",
"category": "navigation",
"description": "Horizontal menu with dropdowns",
"radixPrimitive": "@radix-ui/react-menubar",
"tokens": {
"background": "var(--color-background)",
"text": "var(--color-foreground)",
"hover": "var(--color-accent)"
}
},
"native-select": {
"name": "Native Select",
"category": "form",
"description": "Native HTML select element with styling",
"tokens": {
"background": "var(--color-background)",
"border": "var(--color-input)",
"text": "var(--color-foreground)"
}
},
"navigation-menu": {
"name": "Navigation Menu",
"category": "navigation",
"description": "Website navigation with mega menus",
"radixPrimitive": "@radix-ui/react-navigation-menu",
"tokens": {
"background": "var(--color-background)",
"text": "var(--color-foreground)",
"hover": "var(--color-accent)",
"indicator": "var(--color-primary)"
}
},
"pagination": {
"name": "Pagination",
"category": "navigation",
"description": "Page navigation with previous/next",
"tokens": {
"text": "var(--color-foreground)",
"active": "var(--color-primary)",
"disabled": "var(--color-muted-foreground)"
}
},
"popover": {
"name": "Popover",
"category": "overlay",
"description": "Floating content panel",
"radixPrimitive": "@radix-ui/react-popover",
"tokens": {
"background": "var(--color-popover)",
"text": "var(--color-popover-foreground)",
"border": "var(--color-border)"
}
},
"progress": {
"name": "Progress",
"category": "feedback",
"description": "Progress indicator bar",
"radixPrimitive": "@radix-ui/react-progress",
"tokens": {
"background": "var(--color-secondary)",
"indicator": "var(--color-primary)"
}
},
"radio-group": {
"name": "Radio Group",
"category": "form",
"description": "Set of mutually exclusive options",
"radixPrimitive": "@radix-ui/react-radio-group",
"tokens": {
"border": "var(--color-primary)",
"checked": "var(--color-primary)"
}
},
"resizable": {
"name": "Resizable",
"category": "layout",
"description": "Resizable panel groups",
"dependencies": ["react-resizable-panels"],
"tokens": {
"handle": "var(--color-border)"
}
},
"scroll-area": {
"name": "Scroll Area",
"category": "layout",
"description": "Custom scrollbar styling",
"radixPrimitive": "@radix-ui/react-scroll-area",
"tokens": {
"thumb": "var(--color-border)",
"track": "var(--color-muted)"
}
},
"select": {
"name": "Select",
"category": "form",
"description": "Custom select dropdown",
"radixPrimitive": "@radix-ui/react-select",
"tokens": {
"trigger": {
"background": "var(--color-background)",
"border": "var(--color-input)",
"text": "var(--color-foreground)"
},
"content": {
"background": "var(--color-popover)",
"text": "var(--color-popover-foreground)"
},
"item": {
"hover": "var(--color-accent)"
}
}
},
"separator": {
"name": "Separator",
"category": "layout",
"description": "Visual divider",
"radixPrimitive": "@radix-ui/react-separator",
"variants": {
"orientation": ["horizontal", "vertical"]
},
"tokens": {
"color": "var(--color-border)"
}
},
"sheet": {
"name": "Sheet",
"category": "overlay",
"description": "Side panel overlay",
"radixPrimitive": "@radix-ui/react-dialog",
"variants": {
"side": ["top", "right", "bottom", "left"]
},
"tokens": {
"overlay": "var(--color-background)/80",
"background": "var(--color-background)",
"border": "var(--color-border)"
}
},
"sidebar": {
"name": "Sidebar",
"category": "navigation",
"description": "Application sidebar with collapsible sections",
"tokens": {
"background": "var(--color-sidebar-background)",
"text": "var(--color-sidebar-foreground)",
"hover": "var(--color-sidebar-accent)",
"border": "var(--color-sidebar-border)"
}
},
"skeleton": {
"name": "Skeleton",
"category": "feedback",
"description": "Loading placeholder",
"tokens": {
"background": "var(--color-muted)"
}
},
"slider": {
"name": "Slider",
"category": "form",
"description": "Range slider input",
"radixPrimitive": "@radix-ui/react-slider",
"tokens": {
"track": "var(--color-secondary)",
"range": "var(--color-primary)",
"thumb": "var(--color-background)"
}
},
"sonner": {
"name": "Sonner",
"category": "feedback",
"description": "Toast notifications with sonner",
"dependencies": ["sonner"],
"tokens": {
"background": "var(--color-background)",
"text": "var(--color-foreground)",
"border": "var(--color-border)"
}
},
"spinner": {
"name": "Spinner",
"category": "feedback",
"description": "Loading spinner animation",
"tokens": {
"color": "var(--color-primary)"
}
},
"switch": {
"name": "Switch",
"category": "form",
"description": "Toggle switch control",
"radixPrimitive": "@radix-ui/react-switch",
"tokens": {
"background": "var(--color-input)",
"checked": "var(--color-primary)",
"thumb": "var(--color-background)"
}
},
"table": {
"name": "Table",
"category": "data-display",
"description": "Styled HTML table",
"subComponents": ["TableHeader", "TableBody", "TableFooter", "TableRow", "TableHead", "TableCell", "TableCaption"],
"tokens": {
"header": "var(--color-muted)",
"row": "var(--color-background)",
"rowHover": "var(--color-muted)/50",
"border": "var(--color-border)"
}
},
"tabs": {
"name": "Tabs",
"category": "navigation",
"description": "Tabbed interface",
"radixPrimitive": "@radix-ui/react-tabs",
"tokens": {
"list": "var(--color-muted)",
"trigger": "var(--color-muted-foreground)",
"triggerActive": "var(--color-foreground)",
"content": "var(--color-background)"
}
},
"textarea": {
"name": "Textarea",
"category": "form",
"description": "Multi-line text input",
"tokens": {
"background": "var(--color-background)",
"border": "var(--color-input)",
"text": "var(--color-foreground)",
"placeholder": "var(--color-muted-foreground)",
"focus": "var(--color-ring)"
}
},
"toast": {
"name": "Toast",
"category": "feedback",
"description": "Toast notification component",
"radixPrimitive": "@radix-ui/react-toast",
"variants": {
"variant": ["default", "destructive"]
},
"tokens": {
"background": "var(--color-background)",
"text": "var(--color-foreground)",
"border": "var(--color-border)",
"destructive": "var(--color-destructive)"
}
},
"toggle": {
"name": "Toggle",
"category": "form",
"description": "Two-state button",
"radixPrimitive": "@radix-ui/react-toggle",
"variants": {
"variant": ["default", "outline"],
"size": ["default", "sm", "lg"]
},
"tokens": {
"background": "var(--color-transparent)",
"hover": "var(--color-muted)",
"active": "var(--color-accent)"
}
},
"toggle-group": {
"name": "Toggle Group",
"category": "form",
"description": "Group of toggle buttons",
"radixPrimitive": "@radix-ui/react-toggle-group",
"tokens": {
"background": "var(--color-muted)",
"active": "var(--color-background)"
}
},
"tooltip": {
"name": "Tooltip",
"category": "overlay",
"description": "Informative popup on hover",
"radixPrimitive": "@radix-ui/react-tooltip",
"tokens": {
"background": "var(--color-popover)",
"text": "var(--color-popover-foreground)"
}
},
"typography": {
"name": "Typography",
"category": "data-display",
"description": "Text styling components",
"subComponents": ["H1", "H2", "H3", "H4", "P", "Lead", "Large", "Small", "Muted", "Blockquote", "InlineCode"],
"tokens": {
"heading": "var(--color-foreground)",
"body": "var(--color-foreground)",
"muted": "var(--color-muted-foreground)",
"code": "var(--color-foreground)"
}
}
},
"categories": {
"form": {
"name": "Form Components",
"description": "Input controls and form elements",
"components": ["button", "button-group", "calendar", "checkbox", "combobox", "command", "date-picker", "field", "form", "input", "input-group", "input-otp", "label", "native-select", "radio-group", "select", "slider", "switch", "textarea", "toggle", "toggle-group"]
},
"data-display": {
"name": "Data Display",
"description": "Components for displaying data",
"components": ["accordion", "avatar", "badge", "carousel", "chart", "collapsible", "data-table", "item", "kbd", "table", "typography"]
},
"feedback": {
"name": "Feedback",
"description": "User feedback components",
"components": ["alert", "empty", "progress", "skeleton", "sonner", "spinner", "toast"]
},
"navigation": {
"name": "Navigation",
"description": "Navigation components",
"components": ["breadcrumb", "menubar", "navigation-menu", "pagination", "sidebar", "tabs"]
},
"overlay": {
"name": "Overlay",
"description": "Modal and overlay components",
"components": ["alert-dialog", "context-menu", "dialog", "drawer", "dropdown-menu", "hover-card", "popover", "sheet", "tooltip"]
},
"layout": {
"name": "Layout",
"description": "Layout and structure components",
"components": ["aspect-ratio", "card", "resizable", "scroll-area", "separator"]
}
},
"dependencies": {
"core": ["class-variance-authority", "clsx", "tailwind-merge"],
"radix": [
"@radix-ui/react-accordion",
"@radix-ui/react-alert-dialog",
"@radix-ui/react-aspect-ratio",
"@radix-ui/react-avatar",
"@radix-ui/react-checkbox",
"@radix-ui/react-collapsible",
"@radix-ui/react-context-menu",
"@radix-ui/react-dialog",
"@radix-ui/react-dropdown-menu",
"@radix-ui/react-hover-card",
"@radix-ui/react-label",
"@radix-ui/react-menubar",
"@radix-ui/react-navigation-menu",
"@radix-ui/react-popover",
"@radix-ui/react-progress",
"@radix-ui/react-radio-group",
"@radix-ui/react-scroll-area",
"@radix-ui/react-select",
"@radix-ui/react-separator",
"@radix-ui/react-slider",
"@radix-ui/react-switch",
"@radix-ui/react-tabs",
"@radix-ui/react-toast",
"@radix-ui/react-toggle",
"@radix-ui/react-toggle-group",
"@radix-ui/react-tooltip"
],
"additional": [
"cmdk",
"embla-carousel-react",
"input-otp",
"react-day-picker",
"react-hook-form",
"@hookform/resolvers",
"react-resizable-panels",
"recharts",
"sonner",
"vaul",
"zod",
"@tanstack/react-table"
]
}
}

20
.dss/config/figma.json Normal file
View File

@@ -0,0 +1,20 @@
{
"_meta": {
"description": "Figma configuration - IMMUTABLE (survives resets)",
"protected": true
},
"token": "figd_ScdBk47HlYEItZbQv2CcF9aq-3TfWbBXN3yoRKWA",
"uikit_reference": {
"file_key": "evCZlaeZrP7X20NIViSJbl",
"name": "Obra-shadcn-ui-uikit",
"url": "https://www.figma.com/design/evCZlaeZrP7X20NIViSJbl/Obra-shadcn-ui--uikit-"
},
"files": [
{
"key": "evCZlaeZrP7X20NIViSJbl",
"name": "Obra-shadcn-ui-uikit",
"type": "uikit"
}
],
"teams": []
}

29
.dss/core-hashes.sha256 Normal file
View File

@@ -0,0 +1,29 @@
# DSS Core Structure Hashes
# Generated: 2025-12-10T21:38:48-03:00
# Source: Figma sync pipeline
# DO NOT EDIT MANUALLY
# Format: SHA256 filepath
7738ad7d749558ef8e4a9337b3d14ab851bee8b4b241037e2db0a5b33c61c79e .dss/schema/api.schema.json
e41f535f7a226d37574b880164f338388b52623ccc14af7b2013ef6ab634e318 .dss/schema/components.schema.json
aa5a73c08390433b04ec1f96288cfac573fd5397a2d51774c211db2b8876faf9 .dss/schema/guardrails.schema.json
0d168d0ec2f2c8ab373b02cc42169df45ef5f41e6575493350a5ca4ff6f9797e .dss/schema/skin-contract.json
144642ab5d7b89f73138a9176c65fe8ca05ccf9af1e25b3f9589df37c04c8d70 .dss/schema/tokens.schema.json
57a08fcb06fc9f23617875ec5a28190cae6e5a4b1c66a89072f581405b53d135 .dss/schema/workflows.schema.json
ef4e720e445987685683a5d76ff2a81efae36e67a6ac9e1b0801775b87c30afd dss-claude-plugin/core/skins/base.json
2a709577c30111861b8274f4fe115380436ed943cc6ae3e39140d8a16ccf4bad dss-claude-plugin/core/skins/classic.json
5f356bd862f963e2efd06f2b7922792bca795d45516c45563c0371b90b91c2e1 dss-claude-plugin/core/skins/workbench.json
3242ccb81ca30197e78251453f4594c271afe02502204900329f03ee92a9b7a3 dss/core_tokens/tokens.json
d2f926b311963cbf5748f99f6fde1f88a04efd0533ea73a90318a78a53dfeafd .dss/data/_system/components.json
6ea9af5cc6ea337e3128b7c4395888f51aa5b4208afcc6a77fd4b465efa2222f .dss/data/_system/styles.json
d972e3ebdd7ae2e213a3ae79064a6a0d0aa10bfa20e5dfcbe907f2811ebb2593 .dss/data/_system/figma-components.json
75ecdaeee10d7b0c4383f08b26384fbdf0ac381c99f89cd21ea6b3e4895e3b9d .dss/data/_system/style-dictionary.config.json
b9beb00ffd505a040543051544895fd47bfc948cb39f8c7827656872ab236501 .dss/data/_system/themes/_tokens.scss
b5e5f0c1fa400c0b681caca5aacbfc9e67f44054549af3a43baddf864255b764 .dss/data/_system/themes/tokens.json
76a0bead01ab199680ec88e06e2ebc17e1962dc7bca8b288a62c3922f9d9e9a7 .dss/data/_system/themes/tokens.css
0a0d403395d0d87eb9dd70f28c91d69f7637eedb1e2b562a51185d7dfbf3ebfd .dss/data/_system/analysis-storybook.json
3242ccb81ca30197e78251453f4594c271afe02502204900329f03ee92a9b7a3 .dss/data/_system/tokens/base.json
03df370af13ad41d72635e82e81bfbf85f3ee2e5cbe3e66d27222e612c67568b .dss/data/_system/tokens/resolved-meta.json
9175c3bf0581b652d10704a2d85f1ec9fc68809e90850c01d9acd1d571618a6a .dss/data/_system/tokens/tokens.json
4321119e41b6763a49d654978161b02dead66116ecca6c7f215e021cacfeeab1 .dss/data/_system/ds.config.json
f74658540c8d2838e17de647f0c788de674e3d95dcebc89bfa103f6dc656487c .dss/data/_system/analysis-admin-ui.json

503
.dss/core/primitives.json Normal file
View File

@@ -0,0 +1,503 @@
{
"_meta": {
"description": "Core design primitives - raw values from Tailwind/shadcn palette",
"layer": "core",
"immutable": true,
"version": "1.1.0",
"source": "tailwindcss + shadcn/ui"
},
"color": {
"_category": "Color Primitives",
"base": {
"_description": "Base colors",
"white": { "value": "#ffffff" },
"black": { "value": "#000000" },
"transparent": { "value": "transparent" }
},
"neutral": {
"_description": "Neutral gray scales",
"slate": {
"50": { "value": "#f8fafc" },
"100": { "value": "#f1f5f9" },
"200": { "value": "#e2e8f0" },
"300": { "value": "#cbd5e1" },
"400": { "value": "#94a3b8" },
"500": { "value": "#64748b" },
"600": { "value": "#475569" },
"700": { "value": "#334155" },
"800": { "value": "#1e293b" },
"900": { "value": "#0f172a" },
"950": { "value": "#020617" }
},
"gray": {
"50": { "value": "#f9fafb" },
"100": { "value": "#f3f4f6" },
"200": { "value": "#e5e7eb" },
"300": { "value": "#d1d5db" },
"400": { "value": "#9ca3af" },
"500": { "value": "#6b7280" },
"600": { "value": "#4b5563" },
"700": { "value": "#374151" },
"800": { "value": "#1f2937" },
"900": { "value": "#111827" },
"950": { "value": "#030712" }
},
"zinc": {
"50": { "value": "#fafafa" },
"100": { "value": "#f4f4f5" },
"200": { "value": "#e4e4e7" },
"300": { "value": "#d4d4d8" },
"400": { "value": "#a1a1aa" },
"500": { "value": "#71717a" },
"600": { "value": "#52525b" },
"700": { "value": "#3f3f46" },
"800": { "value": "#27272a" },
"900": { "value": "#18181b" },
"950": { "value": "#09090b" }
},
"neutral": {
"50": { "value": "#fafafa" },
"100": { "value": "#f5f5f5" },
"200": { "value": "#e5e5e5" },
"300": { "value": "#d4d4d4" },
"400": { "value": "#a3a3a3" },
"500": { "value": "#737373" },
"600": { "value": "#525252" },
"700": { "value": "#404040" },
"800": { "value": "#262626" },
"900": { "value": "#171717" },
"950": { "value": "#0a0a0a" }
},
"stone": {
"50": { "value": "#fafaf9" },
"100": { "value": "#f5f5f4" },
"200": { "value": "#e7e5e4" },
"300": { "value": "#d6d3d1" },
"400": { "value": "#a8a29e" },
"500": { "value": "#78716c" },
"600": { "value": "#57534e" },
"700": { "value": "#44403c" },
"800": { "value": "#292524" },
"900": { "value": "#1c1917" },
"950": { "value": "#0c0a09" }
}
},
"semantic": {
"_description": "Semantic color scales",
"red": {
"50": { "value": "#fef2f2" },
"100": { "value": "#fee2e2" },
"200": { "value": "#fecaca" },
"300": { "value": "#fca5a5" },
"400": { "value": "#f87171" },
"500": { "value": "#ef4444" },
"600": { "value": "#dc2626" },
"700": { "value": "#b91c1c" },
"800": { "value": "#991b1b" },
"900": { "value": "#7f1d1d" },
"950": { "value": "#450a0a" }
},
"orange": {
"50": { "value": "#fff7ed" },
"100": { "value": "#ffedd5" },
"200": { "value": "#fed7aa" },
"300": { "value": "#fdba74" },
"400": { "value": "#fb923c" },
"500": { "value": "#f97316" },
"600": { "value": "#ea580c" },
"700": { "value": "#c2410c" },
"800": { "value": "#9a3412" },
"900": { "value": "#7c2d12" },
"950": { "value": "#431407" }
},
"amber": {
"50": { "value": "#fffbeb" },
"100": { "value": "#fef3c7" },
"200": { "value": "#fde68a" },
"300": { "value": "#fcd34d" },
"400": { "value": "#fbbf24" },
"500": { "value": "#f59e0b" },
"600": { "value": "#d97706" },
"700": { "value": "#b45309" },
"800": { "value": "#92400e" },
"900": { "value": "#78350f" },
"950": { "value": "#451a03" }
},
"yellow": {
"50": { "value": "#fefce8" },
"100": { "value": "#fef9c3" },
"200": { "value": "#fef08a" },
"300": { "value": "#fde047" },
"400": { "value": "#facc15" },
"500": { "value": "#eab308" },
"600": { "value": "#ca8a04" },
"700": { "value": "#a16207" },
"800": { "value": "#854d0e" },
"900": { "value": "#713f12" },
"950": { "value": "#422006" }
},
"lime": {
"50": { "value": "#f7fee7" },
"100": { "value": "#ecfccb" },
"200": { "value": "#d9f99d" },
"300": { "value": "#bef264" },
"400": { "value": "#a3e635" },
"500": { "value": "#84cc16" },
"600": { "value": "#65a30d" },
"700": { "value": "#4d7c0f" },
"800": { "value": "#3f6212" },
"900": { "value": "#365314" },
"950": { "value": "#1a2e05" }
},
"green": {
"50": { "value": "#f0fdf4" },
"100": { "value": "#dcfce7" },
"200": { "value": "#bbf7d0" },
"300": { "value": "#86efac" },
"400": { "value": "#4ade80" },
"500": { "value": "#22c55e" },
"600": { "value": "#16a34a" },
"700": { "value": "#15803d" },
"800": { "value": "#166534" },
"900": { "value": "#14532d" },
"950": { "value": "#052e16" }
},
"emerald": {
"50": { "value": "#ecfdf5" },
"100": { "value": "#d1fae5" },
"200": { "value": "#a7f3d0" },
"300": { "value": "#6ee7b7" },
"400": { "value": "#34d399" },
"500": { "value": "#10b981" },
"600": { "value": "#059669" },
"700": { "value": "#047857" },
"800": { "value": "#065f46" },
"900": { "value": "#064e3b" },
"950": { "value": "#022c22" }
},
"teal": {
"50": { "value": "#f0fdfa" },
"100": { "value": "#ccfbf1" },
"200": { "value": "#99f6e4" },
"300": { "value": "#5eead4" },
"400": { "value": "#2dd4bf" },
"500": { "value": "#14b8a6" },
"600": { "value": "#0d9488" },
"700": { "value": "#0f766e" },
"800": { "value": "#115e59" },
"900": { "value": "#134e4a" },
"950": { "value": "#042f2e" }
},
"cyan": {
"50": { "value": "#ecfeff" },
"100": { "value": "#cffafe" },
"200": { "value": "#a5f3fc" },
"300": { "value": "#67e8f9" },
"400": { "value": "#22d3ee" },
"500": { "value": "#06b6d4" },
"600": { "value": "#0891b2" },
"700": { "value": "#0e7490" },
"800": { "value": "#155e75" },
"900": { "value": "#164e63" },
"950": { "value": "#083344" }
},
"sky": {
"50": { "value": "#f0f9ff" },
"100": { "value": "#e0f2fe" },
"200": { "value": "#bae6fd" },
"300": { "value": "#7dd3fc" },
"400": { "value": "#38bdf8" },
"500": { "value": "#0ea5e9" },
"600": { "value": "#0284c7" },
"700": { "value": "#0369a1" },
"800": { "value": "#075985" },
"900": { "value": "#0c4a6e" },
"950": { "value": "#082f49" }
},
"blue": {
"50": { "value": "#eff6ff" },
"100": { "value": "#dbeafe" },
"200": { "value": "#bfdbfe" },
"300": { "value": "#93c5fd" },
"400": { "value": "#60a5fa" },
"500": { "value": "#3b82f6" },
"600": { "value": "#2563eb" },
"700": { "value": "#1d4ed8" },
"800": { "value": "#1e40af" },
"900": { "value": "#1e3a8a" },
"950": { "value": "#172554" }
},
"indigo": {
"50": { "value": "#eef2ff" },
"100": { "value": "#e0e7ff" },
"200": { "value": "#c7d2fe" },
"300": { "value": "#a5b4fc" },
"400": { "value": "#818cf8" },
"500": { "value": "#6366f1" },
"600": { "value": "#4f46e5" },
"700": { "value": "#4338ca" },
"800": { "value": "#3730a3" },
"900": { "value": "#312e81" },
"950": { "value": "#1e1b4b" }
},
"violet": {
"50": { "value": "#f5f3ff" },
"100": { "value": "#ede9fe" },
"200": { "value": "#ddd6fe" },
"300": { "value": "#c4b5fd" },
"400": { "value": "#a78bfa" },
"500": { "value": "#8b5cf6" },
"600": { "value": "#7c3aed" },
"700": { "value": "#6d28d9" },
"800": { "value": "#5b21b6" },
"900": { "value": "#4c1d95" },
"950": { "value": "#2e1065" }
},
"purple": {
"50": { "value": "#faf5ff" },
"100": { "value": "#f3e8ff" },
"200": { "value": "#e9d5ff" },
"300": { "value": "#d8b4fe" },
"400": { "value": "#c084fc" },
"500": { "value": "#a855f7" },
"600": { "value": "#9333ea" },
"700": { "value": "#7e22ce" },
"800": { "value": "#6b21a8" },
"900": { "value": "#581c87" },
"950": { "value": "#3b0764" }
},
"fuchsia": {
"50": { "value": "#fdf4ff" },
"100": { "value": "#fae8ff" },
"200": { "value": "#f5d0fe" },
"300": { "value": "#f0abfc" },
"400": { "value": "#e879f9" },
"500": { "value": "#d946ef" },
"600": { "value": "#c026d3" },
"700": { "value": "#a21caf" },
"800": { "value": "#86198f" },
"900": { "value": "#701a75" },
"950": { "value": "#4a044e" }
},
"pink": {
"50": { "value": "#fdf2f8" },
"100": { "value": "#fce7f3" },
"200": { "value": "#fbcfe8" },
"300": { "value": "#f9a8d4" },
"400": { "value": "#f472b6" },
"500": { "value": "#ec4899" },
"600": { "value": "#db2777" },
"700": { "value": "#be185d" },
"800": { "value": "#9d174d" },
"900": { "value": "#831843" },
"950": { "value": "#500724" }
},
"rose": {
"50": { "value": "#fff1f2" },
"100": { "value": "#ffe4e6" },
"200": { "value": "#fecdd3" },
"300": { "value": "#fda4af" },
"400": { "value": "#fb7185" },
"500": { "value": "#f43f5e" },
"600": { "value": "#e11d48" },
"700": { "value": "#be123c" },
"800": { "value": "#9f1239" },
"900": { "value": "#881337" },
"950": { "value": "#4c0519" }
}
}
},
"spacing": {
"_category": "Spacing Scale",
"_description": "Based on 4px grid (0.25rem = 4px)",
"0": { "value": "0" },
"px": { "value": "1px" },
"0.5": { "value": "0.125rem", "_comment": "2px" },
"1": { "value": "0.25rem", "_comment": "4px" },
"1.5": { "value": "0.375rem", "_comment": "6px" },
"2": { "value": "0.5rem", "_comment": "8px" },
"2.5": { "value": "0.625rem", "_comment": "10px" },
"3": { "value": "0.75rem", "_comment": "12px" },
"3.5": { "value": "0.875rem", "_comment": "14px" },
"4": { "value": "1rem", "_comment": "16px" },
"5": { "value": "1.25rem", "_comment": "20px" },
"6": { "value": "1.5rem", "_comment": "24px" },
"7": { "value": "1.75rem", "_comment": "28px" },
"8": { "value": "2rem", "_comment": "32px" },
"9": { "value": "2.25rem", "_comment": "36px" },
"10": { "value": "2.5rem", "_comment": "40px" },
"11": { "value": "2.75rem", "_comment": "44px" },
"12": { "value": "3rem", "_comment": "48px" },
"14": { "value": "3.5rem", "_comment": "56px" },
"16": { "value": "4rem", "_comment": "64px" },
"20": { "value": "5rem", "_comment": "80px" },
"24": { "value": "6rem", "_comment": "96px" },
"28": { "value": "7rem", "_comment": "112px" },
"32": { "value": "8rem", "_comment": "128px" },
"36": { "value": "9rem", "_comment": "144px" },
"40": { "value": "10rem", "_comment": "160px" },
"44": { "value": "11rem", "_comment": "176px" },
"48": { "value": "12rem", "_comment": "192px" },
"52": { "value": "13rem", "_comment": "208px" },
"56": { "value": "14rem", "_comment": "224px" },
"60": { "value": "15rem", "_comment": "240px" },
"64": { "value": "16rem", "_comment": "256px" },
"72": { "value": "18rem", "_comment": "288px" },
"80": { "value": "20rem", "_comment": "320px" },
"96": { "value": "24rem", "_comment": "384px" }
},
"radius": {
"_category": "Border Radius",
"none": { "value": "0" },
"sm": { "value": "0.125rem", "_comment": "2px" },
"default": { "value": "0.25rem", "_comment": "4px" },
"md": { "value": "0.375rem", "_comment": "6px" },
"lg": { "value": "0.5rem", "_comment": "8px" },
"xl": { "value": "0.75rem", "_comment": "12px" },
"2xl": { "value": "1rem", "_comment": "16px" },
"3xl": { "value": "1.5rem", "_comment": "24px" },
"full": { "value": "9999px" }
},
"shadow": {
"_category": "Box Shadows",
"none": { "value": "none" },
"xs": { "value": "0 1px 2px 0 rgba(0, 0, 0, 0.05)" },
"sm": { "value": "0 1px 3px 0 rgba(0, 0, 0, 0.1), 0 1px 2px -1px rgba(0, 0, 0, 0.1)" },
"default": { "value": "0 1px 3px 0 rgba(0, 0, 0, 0.1), 0 1px 2px -1px rgba(0, 0, 0, 0.1)" },
"md": { "value": "0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -2px rgba(0, 0, 0, 0.1)" },
"lg": { "value": "0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -4px rgba(0, 0, 0, 0.1)" },
"xl": { "value": "0 20px 25px -5px rgba(0, 0, 0, 0.1), 0 8px 10px -6px rgba(0, 0, 0, 0.1)" },
"2xl": { "value": "0 25px 50px -12px rgba(0, 0, 0, 0.25)" },
"inner": { "value": "inset 0 2px 4px 0 rgba(0, 0, 0, 0.05)" }
},
"font": {
"_category": "Typography",
"family": {
"_description": "Font families",
"sans": { "value": "Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif" },
"serif": { "value": "ui-serif, Georgia, Cambria, 'Times New Roman', Times, serif" },
"mono": { "value": "ui-monospace, SFMono-Regular, 'SF Mono', Menlo, Monaco, Consolas, 'Liberation Mono', 'Courier New', monospace" }
},
"size": {
"_description": "Font sizes",
"xs": { "value": "0.75rem", "_comment": "12px", "lineHeight": "1rem" },
"sm": { "value": "0.875rem", "_comment": "14px", "lineHeight": "1.25rem" },
"base": { "value": "1rem", "_comment": "16px", "lineHeight": "1.5rem" },
"lg": { "value": "1.125rem", "_comment": "18px", "lineHeight": "1.75rem" },
"xl": { "value": "1.25rem", "_comment": "20px", "lineHeight": "1.75rem" },
"2xl": { "value": "1.5rem", "_comment": "24px", "lineHeight": "2rem" },
"3xl": { "value": "1.875rem", "_comment": "30px", "lineHeight": "2.25rem" },
"4xl": { "value": "2.25rem", "_comment": "36px", "lineHeight": "2.5rem" },
"5xl": { "value": "3rem", "_comment": "48px", "lineHeight": "1" },
"6xl": { "value": "3.75rem", "_comment": "60px", "lineHeight": "1" },
"7xl": { "value": "4.5rem", "_comment": "72px", "lineHeight": "1" },
"8xl": { "value": "6rem", "_comment": "96px", "lineHeight": "1" },
"9xl": { "value": "8rem", "_comment": "128px", "lineHeight": "1" }
},
"weight": {
"_description": "Font weights",
"thin": { "value": "100" },
"extralight": { "value": "200" },
"light": { "value": "300" },
"normal": { "value": "400" },
"medium": { "value": "500" },
"semibold": { "value": "600" },
"bold": { "value": "700" },
"extrabold": { "value": "800" },
"black": { "value": "900" }
},
"lineHeight": {
"_description": "Line heights",
"none": { "value": "1" },
"tight": { "value": "1.25" },
"snug": { "value": "1.375" },
"normal": { "value": "1.5" },
"relaxed": { "value": "1.625" },
"loose": { "value": "2" }
},
"letterSpacing": {
"_description": "Letter spacing",
"tighter": { "value": "-0.05em" },
"tight": { "value": "-0.025em" },
"normal": { "value": "0" },
"wide": { "value": "0.025em" },
"wider": { "value": "0.05em" },
"widest": { "value": "0.1em" }
}
},
"animation": {
"_category": "Motion & Animation",
"duration": {
"0": { "value": "0ms" },
"75": { "value": "75ms" },
"100": { "value": "100ms" },
"150": { "value": "150ms" },
"200": { "value": "200ms" },
"300": { "value": "300ms" },
"500": { "value": "500ms" },
"700": { "value": "700ms" },
"1000": { "value": "1000ms" }
},
"easing": {
"linear": { "value": "linear" },
"in": { "value": "cubic-bezier(0.4, 0, 1, 1)" },
"out": { "value": "cubic-bezier(0, 0, 0.2, 1)" },
"in-out": { "value": "cubic-bezier(0.4, 0, 0.2, 1)" }
}
},
"breakpoint": {
"_category": "Responsive Breakpoints",
"sm": { "value": "640px" },
"md": { "value": "768px" },
"lg": { "value": "1024px" },
"xl": { "value": "1280px" },
"2xl": { "value": "1536px" }
},
"zIndex": {
"_category": "Z-Index Scale",
"0": { "value": "0" },
"10": { "value": "10" },
"20": { "value": "20" },
"30": { "value": "30" },
"40": { "value": "40" },
"50": { "value": "50" },
"auto": { "value": "auto" }
},
"opacity": {
"_category": "Opacity Scale",
"0": { "value": "0" },
"5": { "value": "0.05" },
"10": { "value": "0.1" },
"20": { "value": "0.2" },
"25": { "value": "0.25" },
"30": { "value": "0.3" },
"40": { "value": "0.4" },
"50": { "value": "0.5" },
"60": { "value": "0.6" },
"70": { "value": "0.7" },
"75": { "value": "0.75" },
"80": { "value": "0.8" },
"90": { "value": "0.9" },
"95": { "value": "0.95" },
"100": { "value": "1" }
}
}

View File

@@ -0,0 +1 @@
{"target":"admin-ui","analyzed_at":"2025-12-10T21:38:46-03:00","stats":{"js":168,"css":20,"html":4},"status":"analyzed"}

View File

@@ -0,0 +1 @@
{"target":"storybook","analyzed_at":"2025-12-10T21:38:46-03:00","stats":{"stories":14,"mdx":0},"status":"analyzed"}

View File

@@ -0,0 +1,373 @@
[
{
"name": "Accordion",
"key": "842:49174",
"description": "Component page: Accordion",
"properties": {},
"variants": []
},
{
"name": "Alert",
"key": "842:44439",
"description": "Component page: Alert",
"properties": {},
"variants": []
},
{
"name": "Alert Dialog",
"key": "842:51942",
"description": "Component page: Alert Dialog",
"properties": {},
"variants": []
},
{
"name": "Aspect Ratio",
"key": "842:52053",
"description": "Component page: Aspect Ratio",
"properties": {},
"variants": []
},
{
"name": "Avatar",
"key": "842:44440",
"description": "Component page: Avatar",
"properties": {},
"variants": []
},
{
"name": "Badge",
"key": "842:44441",
"description": "Component page: Badge",
"properties": {},
"variants": []
},
{
"name": "Breadcrumb",
"key": "842:51940",
"description": "Component page: Breadcrumb",
"properties": {},
"variants": []
},
{
"name": "Button",
"key": "842:44442",
"description": "Component page: Button",
"properties": {},
"variants": []
},
{
"name": "Button Group",
"key": "842:44448",
"description": "Component page: Button Group",
"properties": {},
"variants": []
},
{
"name": "Card",
"key": "842:49175",
"description": "Component page: Card",
"properties": {},
"variants": []
},
{
"name": "Carousel",
"key": "842:52056",
"description": "Component page: Carousel",
"properties": {},
"variants": []
},
{
"name": "Charts",
"key": "842:52058",
"description": "Component page: Charts",
"properties": {},
"variants": []
},
{
"name": "Checkbox",
"key": "842:49183",
"description": "Component page: Checkbox",
"properties": {},
"variants": []
},
{
"name": "Command",
"key": "842:52048",
"description": "Component page: Command",
"properties": {},
"variants": []
},
{
"name": "Data Table",
"key": "842:49179",
"description": "Component page: Data Table",
"properties": {},
"variants": []
},
{
"name": "Date Picker",
"key": "842:49186",
"description": "Component page: Date Picker",
"properties": {},
"variants": []
},
{
"name": "Dialog",
"key": "842:51941",
"description": "Component page: Dialog",
"properties": {},
"variants": []
},
{
"name": "Drawer",
"key": "842:52050",
"description": "Component page: Drawer",
"properties": {},
"variants": []
},
{
"name": "Empty",
"key": "842:44451",
"description": "Component page: Empty",
"properties": {},
"variants": []
},
{
"name": "Field",
"key": "842:49181",
"description": "Component page: Field",
"properties": {},
"variants": []
},
{
"name": "Hover Card",
"key": "842:52051",
"description": "Component page: Hover Card",
"properties": {},
"variants": []
},
{
"name": "Icon Button",
"key": "842:44443",
"description": "Component page: Icon Button",
"properties": {},
"variants": []
},
{
"name": "Input",
"key": "842:49172",
"description": "Component page: Input",
"properties": {},
"variants": []
},
{
"name": "Input File",
"key": "842:49173",
"description": "Component page: Input File",
"properties": {},
"variants": []
},
{
"name": "Input Group",
"key": "885:2",
"description": "Component page: Input Group",
"properties": {},
"variants": []
},
{
"name": "Input OTP",
"key": "842:49177",
"description": "Component page: Input OTP",
"properties": {},
"variants": []
},
{
"name": "Item",
"key": "885:3081",
"description": "Component page: Item",
"properties": {},
"variants": []
},
{
"name": "Kbd",
"key": "842:49171",
"description": "Component page: Kbd",
"properties": {},
"variants": []
},
{
"name": "Label",
"key": "842:49170",
"description": "Component page: Label",
"properties": {},
"variants": []
},
{
"name": "Link Button",
"key": "842:44446",
"description": "Component page: Link Button",
"properties": {},
"variants": []
},
{
"name": "Loading Button",
"key": "842:44444",
"description": "Component page: Loading Button",
"properties": {},
"variants": []
},
{
"name": "Navigation Menu",
"key": "842:51938",
"description": "Component page: Navigation Menu",
"properties": {},
"variants": []
},
{
"name": "Pagination",
"key": "842:51939",
"description": "Component page: Pagination",
"properties": {},
"variants": []
},
{
"name": "Progress",
"key": "842:49187",
"description": "Component page: Progress",
"properties": {},
"variants": []
},
{
"name": "Radio",
"key": "842:49182",
"description": "Component page: Radio",
"properties": {},
"variants": []
},
{
"name": "Resizable",
"key": "842:52055",
"description": "Component page: Resizable",
"properties": {},
"variants": []
},
{
"name": "Scroll Area",
"key": "842:52054",
"description": "Component page: Scroll Area",
"properties": {},
"variants": []
},
{
"name": "Select & Combobox",
"key": "842:49185",
"description": "Component page: Select & Combobox",
"properties": {},
"variants": []
},
{
"name": "Separator",
"key": "842:49137",
"description": "Component page: Separator",
"properties": {},
"variants": []
},
{
"name": "Sheet",
"key": "842:52049",
"description": "Component page: Sheet",
"properties": {},
"variants": []
},
{
"name": "Sidebar",
"key": "842:51929",
"description": "Component page: Sidebar",
"properties": {},
"variants": []
},
{
"name": "Skeleton",
"key": "842:52052",
"description": "Component page: Skeleton",
"properties": {},
"variants": []
},
{
"name": "Slider",
"key": "842:49188",
"description": "Component page: Slider",
"properties": {},
"variants": []
},
{
"name": "Sonner",
"key": "842:51943",
"description": "Component page: Sonner",
"properties": {},
"variants": []
},
{
"name": "Spinner",
"key": "842:44445",
"description": "Component page: Spinner",
"properties": {},
"variants": []
},
{
"name": "Switch",
"key": "842:49184",
"description": "Component page: Switch",
"properties": {},
"variants": []
},
{
"name": "Table",
"key": "842:49176",
"description": "Component page: Table",
"properties": {},
"variants": []
},
{
"name": "Tabs",
"key": "842:50580",
"description": "Component page: Tabs",
"properties": {},
"variants": []
},
{
"name": "Textarea",
"key": "842:49180",
"description": "Component page: Textarea",
"properties": {},
"variants": []
},
{
"name": "Toggle & Toggle Group",
"key": "842:44447",
"description": "Component page: Toggle & Toggle Group",
"properties": {},
"variants": []
},
{
"name": "Tooltip",
"key": "842:44449",
"description": "Component page: Tooltip",
"properties": {},
"variants": []
},
{
"name": "Screen examples",
"key": "683:34149",
"description": "Component page: Screen examples",
"properties": {},
"variants": []
},
{
"name": "Internal Components",
"key": "842:44452",
"description": "Component page: Internal Components",
"properties": {},
"variants": []
}
]

View File

@@ -0,0 +1,33 @@
{
"name": "dss-system",
"version": "1.0.0",
"description": "DSS internal design system (dogfooding)",
"skin": "base",
"base_theme": "light",
"targets": [
{
"name": "admin-ui",
"path": "./admin-ui",
"type": "web-app",
"framework": "vanilla"
},
{
"name": "storybook",
"path": "./storybook",
"type": "documentation",
"framework": "storybook"
}
],
"output": {
"tokens_dir": "./.dss/data/_system/tokens",
"themes_dir": "./.dss/data/_system/themes",
"components_dir": "./.dss/data/_system/components",
"formats": ["css", "scss", "json"]
},
"figma": {
"files": [],
"auto_sync": false
},
"created_at": "$(date -Iseconds)",
"updated_at": "$(date -Iseconds)"
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,20 @@
{
"source": ["tokens/tokens.json"],
"platforms": {
"css": {
"transformGroup": "css",
"buildPath": "themes/",
"files": [{"destination": "tokens.css", "format": "css/variables"}]
},
"scss": {
"transformGroup": "scss",
"buildPath": "themes/",
"files": [{"destination": "_tokens.scss", "format": "scss/variables"}]
},
"json": {
"transformGroup": "js",
"buildPath": "themes/",
"files": [{"destination": "tokens.json", "format": "json/flat"}]
}
}
}

View File

@@ -0,0 +1,408 @@
{
"all": [
{
"name": "heading 1",
"key": "6:83",
"type": "TEXT",
"properties": {}
},
{
"name": "heading 2",
"key": "6:84",
"type": "TEXT",
"properties": {}
},
{
"name": "paragraph small/regular",
"key": "7:129",
"type": "TEXT",
"properties": {}
},
{
"name": "paragraph small/bold",
"key": "22:7579",
"type": "TEXT",
"properties": {}
},
{
"name": "heading 3",
"key": "6:85",
"type": "TEXT",
"properties": {}
},
{
"name": "paragraph/bold",
"key": "22:7578",
"type": "TEXT",
"properties": {}
},
{
"name": "paragraph/regular",
"key": "6:87",
"type": "TEXT",
"properties": {}
},
{
"name": "paragraph mini/regular",
"key": "19:5809",
"type": "TEXT",
"properties": {}
},
{
"name": "heading 4",
"key": "6:86",
"type": "TEXT",
"properties": {}
},
{
"name": "monospaced",
"key": "199:32930",
"type": "TEXT",
"properties": {}
},
{
"name": "paragraph/medium",
"key": "869:27329",
"type": "TEXT",
"properties": {}
},
{
"name": "paragraph small/medium",
"key": "869:27330",
"type": "TEXT",
"properties": {}
},
{
"name": "paragraph mini/bold",
"key": "22:9520",
"type": "TEXT",
"properties": {}
},
{
"name": "paragraph mini/medium",
"key": "869:27331",
"type": "TEXT",
"properties": {}
},
{
"name": "shadow-sm",
"key": "9:772",
"type": "EFFECT",
"properties": {}
},
{
"name": "shadow-lg",
"key": "14:1579",
"type": "EFFECT",
"properties": {}
},
{
"name": "shadow-2xs",
"key": "16:1667",
"type": "EFFECT",
"properties": {}
},
{
"name": "shadow-xs",
"key": "16:1668",
"type": "EFFECT",
"properties": {}
},
{
"name": "shadow-md",
"key": "16:1669",
"type": "EFFECT",
"properties": {}
},
{
"name": "shadow-xl",
"key": "16:1670",
"type": "EFFECT",
"properties": {}
},
{
"name": "shadow-2xl",
"key": "16:1671",
"type": "EFFECT",
"properties": {}
},
{
"name": "focus ring",
"key": "147:11610",
"type": "EFFECT",
"properties": {}
},
{
"name": "focus ring error",
"key": "176:25107",
"type": "EFFECT",
"properties": {}
},
{
"name": "focus ring sidebar",
"key": "653:49231",
"type": "EFFECT",
"properties": {}
},
{
"name": "paragraph small/medium",
"key": "363:28805",
"type": "TEXT",
"properties": {}
},
{
"name": "paragraph small/regular",
"key": "363:28952",
"type": "TEXT",
"properties": {}
},
{
"name": "paragraph/bold",
"key": "862:71716",
"type": "TEXT",
"properties": {}
},
{
"name": "paragraph mini/bold",
"key": "862:71791",
"type": "TEXT",
"properties": {}
},
{
"name": "paragraph mini/regular",
"key": "862:73098",
"type": "TEXT",
"properties": {}
},
{
"name": "shadow-xs",
"key": "862:73052",
"type": "EFFECT",
"properties": {}
},
{
"name": "paragraph small/bold",
"key": "862:73054",
"type": "TEXT",
"properties": {}
},
{
"name": "paragraph small/regular",
"key": "862:71752",
"type": "TEXT",
"properties": {}
},
{
"name": "shadow-sm",
"key": "862:71996",
"type": "EFFECT",
"properties": {}
}
],
"by_type": {
"TEXT": [
{
"name": "heading 1",
"key": "6:83",
"type": "TEXT",
"properties": {}
},
{
"name": "heading 2",
"key": "6:84",
"type": "TEXT",
"properties": {}
},
{
"name": "paragraph small/regular",
"key": "7:129",
"type": "TEXT",
"properties": {}
},
{
"name": "paragraph small/bold",
"key": "22:7579",
"type": "TEXT",
"properties": {}
},
{
"name": "heading 3",
"key": "6:85",
"type": "TEXT",
"properties": {}
},
{
"name": "paragraph/bold",
"key": "22:7578",
"type": "TEXT",
"properties": {}
},
{
"name": "paragraph/regular",
"key": "6:87",
"type": "TEXT",
"properties": {}
},
{
"name": "paragraph mini/regular",
"key": "19:5809",
"type": "TEXT",
"properties": {}
},
{
"name": "heading 4",
"key": "6:86",
"type": "TEXT",
"properties": {}
},
{
"name": "monospaced",
"key": "199:32930",
"type": "TEXT",
"properties": {}
},
{
"name": "paragraph/medium",
"key": "869:27329",
"type": "TEXT",
"properties": {}
},
{
"name": "paragraph small/medium",
"key": "869:27330",
"type": "TEXT",
"properties": {}
},
{
"name": "paragraph mini/bold",
"key": "22:9520",
"type": "TEXT",
"properties": {}
},
{
"name": "paragraph mini/medium",
"key": "869:27331",
"type": "TEXT",
"properties": {}
},
{
"name": "paragraph small/medium",
"key": "363:28805",
"type": "TEXT",
"properties": {}
},
{
"name": "paragraph small/regular",
"key": "363:28952",
"type": "TEXT",
"properties": {}
},
{
"name": "paragraph/bold",
"key": "862:71716",
"type": "TEXT",
"properties": {}
},
{
"name": "paragraph mini/bold",
"key": "862:71791",
"type": "TEXT",
"properties": {}
},
{
"name": "paragraph mini/regular",
"key": "862:73098",
"type": "TEXT",
"properties": {}
},
{
"name": "paragraph small/bold",
"key": "862:73054",
"type": "TEXT",
"properties": {}
},
{
"name": "paragraph small/regular",
"key": "862:71752",
"type": "TEXT",
"properties": {}
}
],
"FILL": [],
"EFFECT": [
{
"name": "shadow-sm",
"key": "9:772",
"type": "EFFECT",
"properties": {}
},
{
"name": "shadow-lg",
"key": "14:1579",
"type": "EFFECT",
"properties": {}
},
{
"name": "shadow-2xs",
"key": "16:1667",
"type": "EFFECT",
"properties": {}
},
{
"name": "shadow-xs",
"key": "16:1668",
"type": "EFFECT",
"properties": {}
},
{
"name": "shadow-md",
"key": "16:1669",
"type": "EFFECT",
"properties": {}
},
{
"name": "shadow-xl",
"key": "16:1670",
"type": "EFFECT",
"properties": {}
},
{
"name": "shadow-2xl",
"key": "16:1671",
"type": "EFFECT",
"properties": {}
},
{
"name": "focus ring",
"key": "147:11610",
"type": "EFFECT",
"properties": {}
},
{
"name": "focus ring error",
"key": "176:25107",
"type": "EFFECT",
"properties": {}
},
{
"name": "focus ring sidebar",
"key": "653:49231",
"type": "EFFECT",
"properties": {}
},
{
"name": "shadow-xs",
"key": "862:73052",
"type": "EFFECT",
"properties": {}
},
{
"name": "shadow-sm",
"key": "862:71996",
"type": "EFFECT",
"properties": {}
}
],
"GRID": []
}
}

View File

@@ -0,0 +1,99 @@
// Do not edit directly, this file was auto-generated.
$color-background: #ffffff;
$color-foreground: #09090b;
$color-card: #ffffff;
$color-card-foreground: #09090b;
$color-popover: #ffffff;
$color-popover-foreground: #09090b;
$color-primary: #18181b; // zinc-900 - brand primary
$color-primary-foreground: #fafafa; // zinc-50
$color-secondary: #f4f4f5;
$color-secondary-foreground: #18181b;
$color-muted: #f4f4f5;
$color-muted-foreground: #71717a;
$color-accent: #f4f4f5;
$color-accent-foreground: #18181b;
$color-destructive: #ef4444;
$color-destructive-foreground: #fafafa;
$color-border: #e4e4e7;
$color-input: #e4e4e7;
$color-ring: #18181b; // matches primary
$color-dark-background: #09090b;
$color-dark-foreground: #fafafa;
$color-dark-card: #09090b;
$color-dark-card-foreground: #fafafa;
$color-dark-popover: #09090b;
$color-dark-popover-foreground: #fafafa;
$color-dark-primary: #fafafa;
$color-dark-primary-foreground: #18181b;
$color-dark-secondary: #27272a;
$color-dark-secondary-foreground: #fafafa;
$color-dark-muted: #27272a;
$color-dark-muted-foreground: #a1a1aa;
$color-dark-accent: #27272a;
$color-dark-accent-foreground: #fafafa;
$color-dark-destructive: #7f1d1d;
$color-dark-destructive-foreground: #fafafa;
$color-dark-border: #27272a;
$color-dark-input: #27272a;
$color-dark-ring: #d4d4d8;
$radius-sm: 0.125rem;
$radius-md: 0.375rem;
$radius-lg: 0.5rem; // default border radius for cards
$radius-xl: 0.75rem;
$radius-full: 9999px;
$effect-shadow-xs: 0 1px 2px 0 rgba(0, 0, 0, 0.05);
$effect-shadow-sm: 0 1px 3px 0 rgba(0, 0, 0, 0.1), 0 1px 2px -1px rgba(0, 0, 0, 0.1);
$effect-shadow-md: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -2px rgba(0, 0, 0, 0.1);
$effect-shadow-lg: 0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -4px rgba(0, 0, 0, 0.1);
$effect-shadow-xl: 0 20px 25px -5px rgba(0, 0, 0, 0.1), 0 8px 10px -6px rgba(0, 0, 0, 0.1);
$effect-shadow-2xl: 0 25px 50px -12px rgba(0, 0, 0, 0.25);
$effect-focus-ring: 0 0 0 3px rgba(59, 130, 246, 0.5);
$spacing-0: 0;
$spacing-1: 0.25rem;
$spacing-2: 0.5rem;
$spacing-3: 0.75rem;
$spacing-4: 1rem;
$spacing-5: 1.25rem;
$spacing-6: 1.5rem;
$spacing-8: 2rem;
$spacing-10: 2.5rem;
$spacing-12: 3rem;
$spacing-16: 4rem;
$typography-heading-1-font-family: Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
$typography-heading-1-font-weight: 700;
$typography-heading-1-font-size: 3rem;
$typography-heading-1-line-height: 1;
$typography-heading-1-letter-spacing: -0.025em;
$typography-heading-2-font-family: Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
$typography-heading-2-font-weight: 600;
$typography-heading-2-font-size: 1.875rem;
$typography-heading-2-line-height: 1.2;
$typography-heading-2-letter-spacing: -0.025em;
$typography-heading-3-font-family: Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
$typography-heading-3-font-weight: 600;
$typography-heading-3-font-size: 1.5rem;
$typography-heading-3-line-height: 1.3;
$typography-heading-3-letter-spacing: -0.025em;
$typography-heading-4-font-family: Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
$typography-heading-4-font-weight: 600;
$typography-heading-4-font-size: 1.25rem;
$typography-heading-4-line-height: 1.4;
$typography-heading-4-letter-spacing: 0;
$typography-paragraph-regular-font-family: Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
$typography-paragraph-regular-font-weight: 400;
$typography-paragraph-regular-font-size: 1rem;
$typography-paragraph-regular-line-height: 1.5;
$typography-paragraph-regular-letter-spacing: 0;
$typography-paragraph-bold-font-family: Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
$typography-paragraph-bold-font-weight: 500;
$typography-paragraph-bold-font-size: 1rem;
$typography-paragraph-bold-line-height: 1.5;
$typography-paragraph-bold-letter-spacing: 0;
$typography-paragraph-small-regular-font-family: Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
$typography-paragraph-small-regular-font-weight: 400;
$typography-paragraph-small-regular-font-size: 0.875rem;
$typography-paragraph-small-regular-line-height: 1.5;
$typography-paragraph-small-regular-letter-spacing: 0;

View File

@@ -0,0 +1,102 @@
/**
* Do not edit directly, this file was auto-generated.
*/
:root {
--color-background: #ffffff;
--color-foreground: #09090b;
--color-card: #ffffff;
--color-card-foreground: #09090b;
--color-popover: #ffffff;
--color-popover-foreground: #09090b;
--color-primary: #18181b; /** zinc-900 - brand primary */
--color-primary-foreground: #fafafa; /** zinc-50 */
--color-secondary: #f4f4f5;
--color-secondary-foreground: #18181b;
--color-muted: #f4f4f5;
--color-muted-foreground: #71717a;
--color-accent: #f4f4f5;
--color-accent-foreground: #18181b;
--color-destructive: #ef4444;
--color-destructive-foreground: #fafafa;
--color-border: #e4e4e7;
--color-input: #e4e4e7;
--color-ring: #18181b; /** matches primary */
--color-dark-background: #09090b;
--color-dark-foreground: #fafafa;
--color-dark-card: #09090b;
--color-dark-card-foreground: #fafafa;
--color-dark-popover: #09090b;
--color-dark-popover-foreground: #fafafa;
--color-dark-primary: #fafafa;
--color-dark-primary-foreground: #18181b;
--color-dark-secondary: #27272a;
--color-dark-secondary-foreground: #fafafa;
--color-dark-muted: #27272a;
--color-dark-muted-foreground: #a1a1aa;
--color-dark-accent: #27272a;
--color-dark-accent-foreground: #fafafa;
--color-dark-destructive: #7f1d1d;
--color-dark-destructive-foreground: #fafafa;
--color-dark-border: #27272a;
--color-dark-input: #27272a;
--color-dark-ring: #d4d4d8;
--radius-sm: 0.125rem;
--radius-md: 0.375rem;
--radius-lg: 0.5rem; /** default border radius for cards */
--radius-xl: 0.75rem;
--radius-full: 9999px;
--effect-shadow-xs: 0 1px 2px 0 rgba(0, 0, 0, 0.05);
--effect-shadow-sm: 0 1px 3px 0 rgba(0, 0, 0, 0.1), 0 1px 2px -1px rgba(0, 0, 0, 0.1);
--effect-shadow-md: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -2px rgba(0, 0, 0, 0.1);
--effect-shadow-lg: 0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -4px rgba(0, 0, 0, 0.1);
--effect-shadow-xl: 0 20px 25px -5px rgba(0, 0, 0, 0.1), 0 8px 10px -6px rgba(0, 0, 0, 0.1);
--effect-shadow-2xl: 0 25px 50px -12px rgba(0, 0, 0, 0.25);
--effect-focus-ring: 0 0 0 3px rgba(59, 130, 246, 0.5);
--spacing-0: 0;
--spacing-1: 0.25rem;
--spacing-2: 0.5rem;
--spacing-3: 0.75rem;
--spacing-4: 1rem;
--spacing-5: 1.25rem;
--spacing-6: 1.5rem;
--spacing-8: 2rem;
--spacing-10: 2.5rem;
--spacing-12: 3rem;
--spacing-16: 4rem;
--typography-heading-1-font-family: Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
--typography-heading-1-font-weight: 700;
--typography-heading-1-font-size: 3rem;
--typography-heading-1-line-height: 1;
--typography-heading-1-letter-spacing: -0.025em;
--typography-heading-2-font-family: Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
--typography-heading-2-font-weight: 600;
--typography-heading-2-font-size: 1.875rem;
--typography-heading-2-line-height: 1.2;
--typography-heading-2-letter-spacing: -0.025em;
--typography-heading-3-font-family: Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
--typography-heading-3-font-weight: 600;
--typography-heading-3-font-size: 1.5rem;
--typography-heading-3-line-height: 1.3;
--typography-heading-3-letter-spacing: -0.025em;
--typography-heading-4-font-family: Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
--typography-heading-4-font-weight: 600;
--typography-heading-4-font-size: 1.25rem;
--typography-heading-4-line-height: 1.4;
--typography-heading-4-letter-spacing: 0;
--typography-paragraph-regular-font-family: Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
--typography-paragraph-regular-font-weight: 400;
--typography-paragraph-regular-font-size: 1rem;
--typography-paragraph-regular-line-height: 1.5;
--typography-paragraph-regular-letter-spacing: 0;
--typography-paragraph-bold-font-family: Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
--typography-paragraph-bold-font-weight: 500;
--typography-paragraph-bold-font-size: 1rem;
--typography-paragraph-bold-line-height: 1.5;
--typography-paragraph-bold-letter-spacing: 0;
--typography-paragraph-small-regular-font-family: Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
--typography-paragraph-small-regular-font-weight: 400;
--typography-paragraph-small-regular-font-size: 0.875rem;
--typography-paragraph-small-regular-line-height: 1.5;
--typography-paragraph-small-regular-letter-spacing: 0;
}

View File

@@ -0,0 +1,98 @@
{
"ColorBackground": "#ffffff",
"ColorForeground": "#09090b",
"ColorCard": "#ffffff",
"ColorCardForeground": "#09090b",
"ColorPopover": "#ffffff",
"ColorPopoverForeground": "#09090b",
"ColorPrimary": "#18181b",
"ColorPrimaryForeground": "#fafafa",
"ColorSecondary": "#f4f4f5",
"ColorSecondaryForeground": "#18181b",
"ColorMuted": "#f4f4f5",
"ColorMutedForeground": "#71717a",
"ColorAccent": "#f4f4f5",
"ColorAccentForeground": "#18181b",
"ColorDestructive": "#ef4444",
"ColorDestructiveForeground": "#fafafa",
"ColorBorder": "#e4e4e7",
"ColorInput": "#e4e4e7",
"ColorRing": "#18181b",
"ColorDarkBackground": "#09090b",
"ColorDarkForeground": "#fafafa",
"ColorDarkCard": "#09090b",
"ColorDarkCardForeground": "#fafafa",
"ColorDarkPopover": "#09090b",
"ColorDarkPopoverForeground": "#fafafa",
"ColorDarkPrimary": "#fafafa",
"ColorDarkPrimaryForeground": "#18181b",
"ColorDarkSecondary": "#27272a",
"ColorDarkSecondaryForeground": "#fafafa",
"ColorDarkMuted": "#27272a",
"ColorDarkMutedForeground": "#a1a1aa",
"ColorDarkAccent": "#27272a",
"ColorDarkAccentForeground": "#fafafa",
"ColorDarkDestructive": "#7f1d1d",
"ColorDarkDestructiveForeground": "#fafafa",
"ColorDarkBorder": "#27272a",
"ColorDarkInput": "#27272a",
"ColorDarkRing": "#d4d4d8",
"RadiusSm": "0.125rem",
"RadiusMd": "0.375rem",
"RadiusLg": "0.5rem",
"RadiusXl": "0.75rem",
"RadiusFull": "9999px",
"EffectShadowXs": "0 1px 2px 0 rgba(0, 0, 0, 0.05)",
"EffectShadowSm": "0 1px 3px 0 rgba(0, 0, 0, 0.1), 0 1px 2px -1px rgba(0, 0, 0, 0.1)",
"EffectShadowMd": "0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -2px rgba(0, 0, 0, 0.1)",
"EffectShadowLg": "0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -4px rgba(0, 0, 0, 0.1)",
"EffectShadowXl": "0 20px 25px -5px rgba(0, 0, 0, 0.1), 0 8px 10px -6px rgba(0, 0, 0, 0.1)",
"EffectShadow2xl": "0 25px 50px -12px rgba(0, 0, 0, 0.25)",
"EffectFocusRing": "0 0 0 3px rgba(59, 130, 246, 0.5)",
"Spacing0": "0",
"Spacing1": "0.25rem",
"Spacing2": "0.5rem",
"Spacing3": "0.75rem",
"Spacing4": "1rem",
"Spacing5": "1.25rem",
"Spacing6": "1.5rem",
"Spacing8": "2rem",
"Spacing10": "2.5rem",
"Spacing12": "3rem",
"Spacing16": "4rem",
"TypographyHeading1FontFamily": "Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif",
"TypographyHeading1FontWeight": "700",
"TypographyHeading1FontSize": "3rem",
"TypographyHeading1LineHeight": "1",
"TypographyHeading1LetterSpacing": "-0.025em",
"TypographyHeading2FontFamily": "Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif",
"TypographyHeading2FontWeight": "600",
"TypographyHeading2FontSize": "1.875rem",
"TypographyHeading2LineHeight": "1.2",
"TypographyHeading2LetterSpacing": "-0.025em",
"TypographyHeading3FontFamily": "Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif",
"TypographyHeading3FontWeight": "600",
"TypographyHeading3FontSize": "1.5rem",
"TypographyHeading3LineHeight": "1.3",
"TypographyHeading3LetterSpacing": "-0.025em",
"TypographyHeading4FontFamily": "Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif",
"TypographyHeading4FontWeight": "600",
"TypographyHeading4FontSize": "1.25rem",
"TypographyHeading4LineHeight": "1.4",
"TypographyHeading4LetterSpacing": "0",
"TypographyParagraphRegularFontFamily": "Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif",
"TypographyParagraphRegularFontWeight": "400",
"TypographyParagraphRegularFontSize": "1rem",
"TypographyParagraphRegularLineHeight": "1.5",
"TypographyParagraphRegularLetterSpacing": "0",
"TypographyParagraphBoldFontFamily": "Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif",
"TypographyParagraphBoldFontWeight": "500",
"TypographyParagraphBoldFontSize": "1rem",
"TypographyParagraphBoldLineHeight": "1.5",
"TypographyParagraphBoldLetterSpacing": "0",
"TypographyParagraphSmallRegularFontFamily": "Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif",
"TypographyParagraphSmallRegularFontWeight": "400",
"TypographyParagraphSmallRegularFontSize": "0.875rem",
"TypographyParagraphSmallRegularLineHeight": "1.5",
"TypographyParagraphSmallRegularLetterSpacing": "0"
}

View File

@@ -0,0 +1,9 @@
{
"_meta": {
"version": "1.0.0",
"generated": null,
"source": "awaiting Figma sync",
"status": "empty"
},
"tokens": {}
}

View File

@@ -0,0 +1,11 @@
{
"resolved_at": "2025-12-10T21:38:48.326466",
"skin": "shadcn",
"theme": "default",
"token_count": 68,
"layers": [
"core/primitives",
"skins/shadcn",
"themes/default"
]
}

View File

@@ -0,0 +1,320 @@
{
"color": {
"background": {
"value": "#ffffff"
},
"foreground": {
"value": "#09090b"
},
"card": {
"value": "#ffffff"
},
"card-foreground": {
"value": "#09090b"
},
"popover": {
"value": "#ffffff"
},
"popover-foreground": {
"value": "#09090b"
},
"primary": {
"value": "#18181b",
"comment": "zinc-900 - brand primary"
},
"primary-foreground": {
"value": "#fafafa",
"comment": "zinc-50"
},
"secondary": {
"value": "#f4f4f5"
},
"secondary-foreground": {
"value": "#18181b"
},
"muted": {
"value": "#f4f4f5"
},
"muted-foreground": {
"value": "#71717a"
},
"accent": {
"value": "#f4f4f5"
},
"accent-foreground": {
"value": "#18181b"
},
"destructive": {
"value": "#ef4444"
},
"destructive-foreground": {
"value": "#fafafa"
},
"border": {
"value": "#e4e4e7"
},
"input": {
"value": "#e4e4e7"
},
"ring": {
"value": "#18181b",
"comment": "matches primary"
}
},
"color-dark": {
"background": {
"value": "#09090b"
},
"foreground": {
"value": "#fafafa"
},
"card": {
"value": "#09090b"
},
"card-foreground": {
"value": "#fafafa"
},
"popover": {
"value": "#09090b"
},
"popover-foreground": {
"value": "#fafafa"
},
"primary": {
"value": "#fafafa"
},
"primary-foreground": {
"value": "#18181b"
},
"secondary": {
"value": "#27272a"
},
"secondary-foreground": {
"value": "#fafafa"
},
"muted": {
"value": "#27272a"
},
"muted-foreground": {
"value": "#a1a1aa"
},
"accent": {
"value": "#27272a"
},
"accent-foreground": {
"value": "#fafafa"
},
"destructive": {
"value": "#7f1d1d"
},
"destructive-foreground": {
"value": "#fafafa"
},
"border": {
"value": "#27272a"
},
"input": {
"value": "#27272a"
},
"ring": {
"value": "#d4d4d8"
}
},
"radius": {
"sm": {
"value": "0.125rem"
},
"md": {
"value": "0.375rem"
},
"lg": {
"value": "0.5rem",
"comment": "default border radius for cards"
},
"xl": {
"value": "0.75rem"
},
"full": {
"value": "9999px"
}
},
"effect": {
"shadow-xs": {
"value": "0 1px 2px 0 rgba(0, 0, 0, 0.05)"
},
"shadow-sm": {
"value": "0 1px 3px 0 rgba(0, 0, 0, 0.1), 0 1px 2px -1px rgba(0, 0, 0, 0.1)"
},
"shadow-md": {
"value": "0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -2px rgba(0, 0, 0, 0.1)"
},
"shadow-lg": {
"value": "0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -4px rgba(0, 0, 0, 0.1)"
},
"shadow-xl": {
"value": "0 20px 25px -5px rgba(0, 0, 0, 0.1), 0 8px 10px -6px rgba(0, 0, 0, 0.1)"
},
"shadow-2xl": {
"value": "0 25px 50px -12px rgba(0, 0, 0, 0.25)"
},
"focus-ring": {
"value": "0 0 0 3px rgba(59, 130, 246, 0.5)"
}
},
"spacing": {
"0": {
"value": "0"
},
"1": {
"value": "0.25rem"
},
"2": {
"value": "0.5rem"
},
"3": {
"value": "0.75rem"
},
"4": {
"value": "1rem"
},
"5": {
"value": "1.25rem"
},
"6": {
"value": "1.5rem"
},
"8": {
"value": "2rem"
},
"10": {
"value": "2.5rem"
},
"12": {
"value": "3rem"
},
"16": {
"value": "4rem"
}
},
"typography": {
"heading-1": {
"font-family": {
"value": "Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif"
},
"font-weight": {
"value": "700"
},
"font-size": {
"value": "3rem"
},
"line-height": {
"value": "1"
},
"letter-spacing": {
"value": "-0.025em"
}
},
"heading-2": {
"font-family": {
"value": "Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif"
},
"font-weight": {
"value": "600"
},
"font-size": {
"value": "1.875rem"
},
"line-height": {
"value": "1.2"
},
"letter-spacing": {
"value": "-0.025em"
}
},
"heading-3": {
"font-family": {
"value": "Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif"
},
"font-weight": {
"value": "600"
},
"font-size": {
"value": "1.5rem"
},
"line-height": {
"value": "1.3"
},
"letter-spacing": {
"value": "-0.025em"
}
},
"heading-4": {
"font-family": {
"value": "Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif"
},
"font-weight": {
"value": "600"
},
"font-size": {
"value": "1.25rem"
},
"line-height": {
"value": "1.4"
},
"letter-spacing": {
"value": "0"
}
},
"paragraph-regular": {
"font-family": {
"value": "Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif"
},
"font-weight": {
"value": "400"
},
"font-size": {
"value": "1rem"
},
"line-height": {
"value": "1.5"
},
"letter-spacing": {
"value": "0"
}
},
"paragraph-bold": {
"font-family": {
"value": "Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif"
},
"font-weight": {
"value": "500"
},
"font-size": {
"value": "1rem"
},
"line-height": {
"value": "1.5"
},
"letter-spacing": {
"value": "0"
}
},
"paragraph-small-regular": {
"font-family": {
"value": "Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif"
},
"font-weight": {
"value": "400"
},
"font-size": {
"value": "0.875rem"
},
"line-height": {
"value": "1.5"
},
"letter-spacing": {
"value": "0"
}
}
}
}

View File

@@ -0,0 +1,87 @@
{
"$schema": "http://json-schema.org/draft-07/schema#",
"title": "DSS Skin Contract",
"description": "Defines required tokens that all skins must provide. Themes should only override these stable tokens.",
"version": "1.0.0",
"required_tokens": {
"color": {
"description": "Semantic color tokens",
"required": [
"primary",
"primary-foreground",
"secondary",
"secondary-foreground",
"background",
"foreground",
"muted",
"muted-foreground",
"accent",
"accent-foreground",
"destructive",
"destructive-foreground",
"border",
"input",
"ring",
"card",
"card-foreground",
"popover",
"popover-foreground"
]
},
"effect": {
"description": "Shadow and focus effect tokens",
"required": [
"shadow-xs",
"shadow-sm",
"shadow-md",
"shadow-lg",
"shadow-xl",
"shadow-2xl",
"focus-ring"
]
},
"radius": {
"description": "Border radius tokens",
"required": [
"sm",
"md",
"lg",
"xl",
"full"
]
},
"typography": {
"description": "Typography style tokens",
"required": [
"heading-1",
"heading-2",
"heading-3",
"heading-4",
"paragraph-regular",
"paragraph-bold",
"paragraph-small-regular"
]
},
"spacing": {
"description": "Spacing scale tokens",
"required": [
"0",
"1",
"2",
"3",
"4",
"5",
"6",
"8",
"10",
"12",
"16"
]
}
},
"optional_categories": [
"animation",
"breakpoint",
"z-index"
]
}

View File

@@ -0,0 +1,306 @@
{
"_meta": {
"name": "figma-source",
"description": "Tokens extracted from Figma: Obra shadcn/ui (uikit)",
"version": "1.0.0",
"layer": "skin",
"source": "figma:evCZlaeZrP7X20NIViSJbl",
"generated": "2025-12-10T20:49:27.631185"
},
"typography": {
"heading-1": {
"font-family": {
"value": "Inter"
},
"font-weight": {
"value": 600
},
"font-size": {
"value": "48px"
},
"line-height": {
"value": "48px"
},
"letter-spacing": {
"value": "-1.5px"
},
"_contract": true
},
"heading-2": {
"font-family": {
"value": "Inter"
},
"font-weight": {
"value": 600
},
"font-size": {
"value": "30px"
},
"line-height": {
"value": "30px"
},
"letter-spacing": {
"value": "-1.0px"
},
"_contract": true
},
"paragraph-small-regular": {
"font-family": {
"value": "Inter"
},
"font-weight": {
"value": 400
},
"font-size": {
"value": "14px"
},
"line-height": {
"value": "21px"
},
"letter-spacing": {
"value": "0.07px"
},
"_contract": true
},
"paragraph-small-bold": {
"font-family": {
"value": "Inter"
},
"font-weight": {
"value": 500
},
"font-size": {
"value": "14px"
},
"line-height": {
"value": "21px"
},
"letter-spacing": {
"value": "0.07px"
},
"_contract": false
},
"heading-3": {
"font-family": {
"value": "Inter"
},
"font-weight": {
"value": 600
},
"font-size": {
"value": "24px"
},
"line-height": {
"value": "29px"
},
"letter-spacing": {
"value": "-1.0px"
},
"_contract": true
},
"paragraph-bold": {
"font-family": {
"value": "Inter"
},
"font-weight": {
"value": 500
},
"font-size": {
"value": "16px"
},
"line-height": {
"value": "24px"
},
"letter-spacing": {
"value": "0.0px"
},
"_contract": true
},
"paragraph-regular": {
"font-family": {
"value": "Inter"
},
"font-weight": {
"value": 400
},
"font-size": {
"value": "16px"
},
"line-height": {
"value": "24px"
},
"letter-spacing": {
"value": "0.0px"
},
"_contract": true
},
"paragraph-mini-regular": {
"font-family": {
"value": "Inter"
},
"font-weight": {
"value": 400
},
"font-size": {
"value": "12px"
},
"line-height": {
"value": "16px"
},
"letter-spacing": {
"value": "0.18px"
},
"_contract": false
},
"heading-4": {
"font-family": {
"value": "Inter"
},
"font-weight": {
"value": 600
},
"font-size": {
"value": "20px"
},
"line-height": {
"value": "24px"
},
"letter-spacing": {
"value": "0.0px"
},
"_contract": true
},
"monospaced": {
"font-family": {
"value": "Menlo"
},
"font-weight": {
"value": 400
},
"font-size": {
"value": "16px"
},
"line-height": {
"value": "24px"
},
"letter-spacing": {
"value": "0.0px"
},
"_contract": false
},
"paragraph-medium": {
"font-family": {
"value": "Inter"
},
"font-weight": {
"value": 500
},
"font-size": {
"value": "16px"
},
"line-height": {
"value": "24px"
},
"letter-spacing": {
"value": "0.0px"
},
"_contract": false
},
"paragraph-small-medium": {
"font-family": {
"value": "Inter"
},
"font-weight": {
"value": 500
},
"font-size": {
"value": "14px"
},
"line-height": {
"value": "21px"
},
"letter-spacing": {
"value": "0.07px"
},
"_contract": false
},
"paragraph-mini-bold": {
"font-family": {
"value": "Inter"
},
"font-weight": {
"value": 500
},
"font-size": {
"value": "12px"
},
"line-height": {
"value": "16px"
},
"letter-spacing": {
"value": "0.18px"
},
"_contract": false
},
"paragraph-mini-medium": {
"font-family": {
"value": "Inter"
},
"font-weight": {
"value": 500
},
"font-size": {
"value": "12px"
},
"line-height": {
"value": "16px"
},
"letter-spacing": {
"value": "0.18px"
},
"_contract": false
}
},
"effect": {
"shadow-sm": {
"value": "0.0px 1.0px 2.0px -1.0px rgba(0, 0, 0, 0.1), 0.0px 1.0px 3.0px 0px rgba(0, 0, 0, 0.1)",
"_contract": true
},
"shadow-lg": {
"value": "0.0px 4.0px 6.0px -4.0px rgba(0, 0, 0, 0.1), 0.0px 10.0px 15.0px -3.0px rgba(0, 0, 0, 0.1)",
"_contract": true
},
"shadow-2xs": {
"value": "0.0px 1.0px 0.0px 0px rgba(0, 0, 0, 0.05)",
"_contract": false
},
"shadow-xs": {
"value": "0.0px 1.0px 2.0px 0px rgba(0, 0, 0, 0.05)",
"_contract": true
},
"shadow-md": {
"value": "0.0px 2.0px 4.0px -2.0px rgba(0, 0, 0, 0.1), 0.0px 4.0px 6.0px -1.0px rgba(0, 0, 0, 0.1)",
"_contract": true
},
"shadow-xl": {
"value": "0.0px 8.0px 10.0px -6.0px rgba(0, 0, 0, 0.1), 0.0px 20.0px 25.0px -5.0px rgba(0, 0, 0, 0.1)",
"_contract": true
},
"shadow-2xl": {
"value": "0.0px 25.0px 50.0px 12.0px rgba(0, 0, 0, 0.25)",
"_contract": true
},
"focus-ring": {
"value": "0.0px 0.0px 0.0px 3.0px rgb(203, 213, 225)",
"_contract": true
},
"focus-ring-error": {
"value": "0.0px 0.0px 0.0px 3.0px rgb(252, 165, 165)",
"_contract": false
},
"focus-ring-sidebar": {
"value": "0.0px 0.0px 0.0px 3.0px rgb(203, 213, 225)",
"_contract": false
}
}
}

View File

@@ -0,0 +1,132 @@
{
"_meta": {
"name": "shadcn",
"description": "shadcn/ui skin - maps primitives to semantic tokens",
"version": "1.0.0",
"layer": "skin",
"extends": "core/primitives",
"source": "https://ui.shadcn.com"
},
"color": {
"background": { "value": "{color.white}" },
"foreground": { "value": "{color.zinc.950}" },
"card": { "value": "{color.white}" },
"card-foreground": { "value": "{color.zinc.950}" },
"popover": { "value": "{color.white}" },
"popover-foreground": { "value": "{color.zinc.950}" },
"primary": { "value": "{color.zinc.900}" },
"primary-foreground": { "value": "{color.zinc.50}" },
"secondary": { "value": "{color.zinc.100}" },
"secondary-foreground": { "value": "{color.zinc.900}" },
"muted": { "value": "{color.zinc.100}" },
"muted-foreground": { "value": "{color.zinc.500}" },
"accent": { "value": "{color.zinc.100}" },
"accent-foreground": { "value": "{color.zinc.900}" },
"destructive": { "value": "{color.red.500}" },
"destructive-foreground": { "value": "{color.zinc.50}" },
"border": { "value": "{color.zinc.200}" },
"input": { "value": "{color.zinc.200}" },
"ring": { "value": "{color.zinc.950}" }
},
"color-dark": {
"background": { "value": "{color.zinc.950}" },
"foreground": { "value": "{color.zinc.50}" },
"card": { "value": "{color.zinc.950}" },
"card-foreground": { "value": "{color.zinc.50}" },
"popover": { "value": "{color.zinc.950}" },
"popover-foreground": { "value": "{color.zinc.50}" },
"primary": { "value": "{color.zinc.50}" },
"primary-foreground": { "value": "{color.zinc.900}" },
"secondary": { "value": "{color.zinc.800}" },
"secondary-foreground": { "value": "{color.zinc.50}" },
"muted": { "value": "{color.zinc.800}" },
"muted-foreground": { "value": "{color.zinc.400}" },
"accent": { "value": "{color.zinc.800}" },
"accent-foreground": { "value": "{color.zinc.50}" },
"destructive": { "value": "{color.red.900}" },
"destructive-foreground": { "value": "{color.zinc.50}" },
"border": { "value": "{color.zinc.800}" },
"input": { "value": "{color.zinc.800}" },
"ring": { "value": "{color.zinc.300}" }
},
"radius": {
"sm": { "value": "{radius.sm}" },
"md": { "value": "{radius.md}" },
"lg": { "value": "{radius.lg}" },
"xl": { "value": "{radius.xl}" },
"full": { "value": "{radius.full}" }
},
"effect": {
"shadow-xs": { "value": "0 1px 2px 0 rgba(0, 0, 0, 0.05)" },
"shadow-sm": { "value": "0 1px 3px 0 rgba(0, 0, 0, 0.1), 0 1px 2px -1px rgba(0, 0, 0, 0.1)" },
"shadow-md": { "value": "0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -2px rgba(0, 0, 0, 0.1)" },
"shadow-lg": { "value": "0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -4px rgba(0, 0, 0, 0.1)" },
"shadow-xl": { "value": "0 20px 25px -5px rgba(0, 0, 0, 0.1), 0 8px 10px -6px rgba(0, 0, 0, 0.1)" },
"shadow-2xl": { "value": "0 25px 50px -12px rgba(0, 0, 0, 0.25)" },
"focus-ring": { "value": "0 0 0 3px rgba(59, 130, 246, 0.5)" }
},
"spacing": {
"0": { "value": "{spacing.0}" },
"1": { "value": "{spacing.1}" },
"2": { "value": "{spacing.2}" },
"3": { "value": "{spacing.3}" },
"4": { "value": "{spacing.4}" },
"5": { "value": "{spacing.5}" },
"6": { "value": "{spacing.6}" },
"8": { "value": "{spacing.8}" },
"10": { "value": "{spacing.10}" },
"12": { "value": "{spacing.12}" },
"16": { "value": "{spacing.16}" }
},
"typography": {
"heading-1": {
"font-family": { "value": "{font.family.sans}" },
"font-weight": { "value": "{font.weight.bold}" },
"font-size": { "value": "{font.size.5xl}" },
"line-height": { "value": "1" },
"letter-spacing": { "value": "-0.025em" }
},
"heading-2": {
"font-family": { "value": "{font.family.sans}" },
"font-weight": { "value": "{font.weight.semibold}" },
"font-size": { "value": "{font.size.3xl}" },
"line-height": { "value": "1.2" },
"letter-spacing": { "value": "-0.025em" }
},
"heading-3": {
"font-family": { "value": "{font.family.sans}" },
"font-weight": { "value": "{font.weight.semibold}" },
"font-size": { "value": "{font.size.2xl}" },
"line-height": { "value": "1.3" },
"letter-spacing": { "value": "-0.025em" }
},
"heading-4": {
"font-family": { "value": "{font.family.sans}" },
"font-weight": { "value": "{font.weight.semibold}" },
"font-size": { "value": "{font.size.xl}" },
"line-height": { "value": "1.4" },
"letter-spacing": { "value": "0" }
},
"paragraph-regular": {
"font-family": { "value": "{font.family.sans}" },
"font-weight": { "value": "{font.weight.normal}" },
"font-size": { "value": "{font.size.base}" },
"line-height": { "value": "1.5" },
"letter-spacing": { "value": "0" }
},
"paragraph-bold": {
"font-family": { "value": "{font.family.sans}" },
"font-weight": { "value": "{font.weight.medium}" },
"font-size": { "value": "{font.size.base}" },
"line-height": { "value": "1.5" },
"letter-spacing": { "value": "0" }
},
"paragraph-small-regular": {
"font-family": { "value": "{font.family.sans}" },
"font-weight": { "value": "{font.weight.normal}" },
"font-size": { "value": "{font.size.sm}" },
"line-height": { "value": "1.5" },
"letter-spacing": { "value": "0" }
}
}
}

18
.dss/themes/default.json Normal file
View File

@@ -0,0 +1,18 @@
{
"_meta": {
"name": "default",
"description": "Default theme - brand overrides on top of shadcn skin",
"version": "1.0.0",
"layer": "theme",
"extends": "skins/shadcn",
"contract_version": "1.0.0"
},
"color": {
"primary": { "value": "#18181b", "comment": "zinc-900 - brand primary" },
"primary-foreground": { "value": "#fafafa", "comment": "zinc-50" },
"ring": { "value": "#18181b", "comment": "matches primary" }
},
"radius": {
"lg": { "value": "0.5rem", "comment": "default border radius for cards" }
}
}

View File

@@ -4,12 +4,22 @@
echo "🛡️ DSS Immutability Check..."
# List of protected files (core principles only)
# List of protected files (core principles and config)
PROTECTED_FILES=(
".knowledge/dss-principles.json"
".knowledge/dss-architecture.json"
".clauderc"
"PROJECT_CONFIG.md"
".dss/config/figma.json"
)
# DSS Core Structure - ONLY modifiable by Figma sync
# These paths require ALLOW_FIGMA_SYNC=true to modify
DSS_CORE_PATHS=(
".dss/data/_system/"
".dss/schema/"
"dss-claude-plugin/core/skins/"
"dss/core_tokens/"
)
# Check if any protected files are being modified
@@ -43,6 +53,76 @@ if [ ${#MODIFIED_PROTECTED[@]} -gt 0 ]; then
echo "✅ ALLOW_CORE_CHANGES=true detected. Proceeding with commit."
fi
# Check DSS Core paths (Figma sync only)
MODIFIED_DSS_CORE=()
for path in "${DSS_CORE_PATHS[@]}"; do
if git diff --cached --name-only | grep -q "^${path}"; then
while IFS= read -r file; do
MODIFIED_DSS_CORE+=("$file")
done < <(git diff --cached --name-only | grep "^${path}")
fi
done
if [ ${#MODIFIED_DSS_CORE[@]} -gt 0 ]; then
echo ""
echo "🔒 DSS CORE STRUCTURE PROTECTION"
echo " The following paths can ONLY be modified via Figma sync:"
for file in "${MODIFIED_DSS_CORE[@]}"; do
echo " - $file"
done
echo ""
echo " Source of truth: Figma → DSS Pipeline → These files"
echo ""
echo " To proceed (Figma sync only): ALLOW_FIGMA_SYNC=true"
echo ""
if [ "$ALLOW_FIGMA_SYNC" != "true" ]; then
echo "❌ Commit blocked. DSS core structure is Figma-sync only."
exit 1
fi
echo "✅ ALLOW_FIGMA_SYNC=true detected. Proceeding with Figma sync commit."
# Verify hash manifest is also being updated
if ! git diff --cached --name-only | grep -q "^.dss/core-hashes.sha256$"; then
echo ""
echo "⚠️ WARNING: core-hashes.sha256 not updated!"
echo " Figma sync should regenerate: .dss/core-hashes.sha256"
echo " Run: scripts/regenerate-core-hashes.sh"
fi
fi
# Hash verification for DSS core files
HASH_FILE=".dss/core-hashes.sha256"
if [ -f "$HASH_FILE" ] && [ ${#MODIFIED_DSS_CORE[@]} -gt 0 ]; then
echo ""
echo "🔐 Verifying DSS core file hashes..."
HASH_FAILURES=()
for file in "${MODIFIED_DSS_CORE[@]}"; do
if grep -q " ${file}$" "$HASH_FILE" 2>/dev/null; then
EXPECTED=$(grep " ${file}$" "$HASH_FILE" | cut -d' ' -f1)
# Get hash from staged version
ACTUAL=$(git show ":${file}" 2>/dev/null | sha256sum | cut -d' ' -f1)
if [ "$EXPECTED" != "$ACTUAL" ] && [ "$ALLOW_FIGMA_SYNC" != "true" ]; then
HASH_FAILURES+=("$file")
fi
fi
done
if [ ${#HASH_FAILURES[@]} -gt 0 ]; then
echo "❌ Hash verification failed for:"
for file in "${HASH_FAILURES[@]}"; do
echo " - $file"
done
echo ""
echo " These files have been modified outside Figma sync pipeline."
echo " Revert changes or run Figma sync to update properly."
exit 1
fi
echo "✅ Hash verification passed."
fi
echo "✅ Immutability check passed."
echo ""
@@ -59,6 +139,24 @@ else
echo "⚠️ Warning: scripts/verify-quality.sh not found, skipping quality checks"
fi
# Run Python validation hook (documentation, schemas, terminology)
echo ""
echo "📚 Running Documentation & Schema Checks..."
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
if [ -f "${SCRIPT_DIR}/pre-commit-python" ]; then
if ! python3 "${SCRIPT_DIR}/pre-commit-python"; then
echo ""
echo "❌ Validation checks failed. Please fix the errors above."
echo "To bypass (not recommended): git commit --no-verify"
exit 1
fi
elif [ -f ".git/hooks/pre-commit" ] && file ".git/hooks/pre-commit" | grep -q Python; then
if ! python3 ".git/hooks/pre-commit"; then
echo "❌ Python validation checks failed."
exit 1
fi
fi
echo ""
echo "✅ All pre-commit checks passed!"
exit 0

364
.githooks/pre-commit-python Executable file
View File

@@ -0,0 +1,364 @@
#!/usr/bin/env python3
"""
DSS Pre-Commit Hook
Enforces DSS architectural guardrails before allowing commits
Validators:
1. Immutable file protection
2. Temp folder discipline
3. Schema validation
4. Terminology checks
5. Audit logging
"""
import sys
import os
import json
import subprocess
from pathlib import Path
from datetime import datetime
import re
# Configuration
DSS_ROOT = Path("/home/overbits/dss")
IMMUTABLE_FILES = [
".dss/schema/*.schema.json",
".dss-boundaries.yaml",
"API_SPECIFICATION_IMMUTABLE.md",
"dss-claude-plugin/.mcp.json",
"dss-mvp1/dss/validators/schema.py",
]
AUDIT_LOG = DSS_ROOT / ".dss/logs/git-hooks.jsonl"
TEMP_DIR = DSS_ROOT / ".dss/temp"
class Colors:
RED = '\033[0;31m'
GREEN = '\033[0;32m'
YELLOW = '\033[1;33m'
NC = '\033[0m' # No Color
def log_audit(validator, status, details):
"""Log hook events to audit trail"""
AUDIT_LOG.parent.mkdir(parents=True, exist_ok=True)
log_entry = {
"timestamp": datetime.utcnow().isoformat(),
"hook": "pre-commit",
"validator": validator,
"status": status,
"details": details,
}
with open(AUDIT_LOG, "a") as f:
f.write(json.dumps(log_entry) + "\n")
def get_staged_files():
"""Get list of staged files"""
result = subprocess.run(
["git", "diff", "--cached", "--name-only", "--diff-filter=ACM"],
capture_output=True,
text=True,
cwd=DSS_ROOT
)
return [Path(f) for f in result.stdout.strip().split("\n") if f]
def check_immutable_files(staged_files):
"""Validate that immutable files are not modified"""
from fnmatch import fnmatch
violations = []
for file_path in staged_files:
for pattern in IMMUTABLE_FILES:
if fnmatch(str(file_path), pattern):
# Only block if file exists in last commit (modification, not addition)
result = subprocess.run(
["git", "ls-tree", "--name-only", "HEAD", str(file_path)],
capture_output=True,
text=True,
cwd=DSS_ROOT
)
if result.stdout.strip(): # File exists in HEAD
violations.append(str(file_path))
if violations:
# Check for bypass via environment variable or commit message
bypass = os.environ.get("DSS_IMMUTABLE_BYPASS") == "1"
if not bypass:
# Try to get commit message from various sources
commit_msg_file = DSS_ROOT / ".git/COMMIT_EDITMSG"
if commit_msg_file.exists():
commit_msg = commit_msg_file.read_text()
if "[IMMUTABLE-UPDATE]" in commit_msg:
bypass = True
log_audit("immutable_files", "bypass", {
"files": violations,
"commit_message": commit_msg.split("\n")[0],
"method": "commit_message"
})
if bypass:
log_audit("immutable_files", "bypass", {
"files": violations,
"method": "environment_variable"
})
if not bypass:
print(f"{Colors.RED}✗ IMMUTABLE FILE VIOLATION{Colors.NC}")
print(f"\nThe following protected files cannot be modified:")
for v in violations:
print(f" - {v}")
print(f"\nTo update immutable files:")
print(f" 1. Use commit message: [IMMUTABLE-UPDATE] Reason for change")
print(f" 2. Include justification in commit body")
print(f"\nProtected files:")
for pattern in IMMUTABLE_FILES:
print(f" - {pattern}")
log_audit("immutable_files", "rejected", {"files": violations})
return False
log_audit("immutable_files", "passed", {"files_checked": len(staged_files)})
return True
def check_temp_folder(staged_files):
"""Validate that temp files are only in .dss/temp/"""
violations = []
# Patterns that indicate temp files
temp_patterns = [
r".*\.tmp$",
r".*\.temp$",
r".*~$",
r".*\.swp$",
r".*\.swo$",
r".*\.backup$",
r".*\.bak$",
r"^temp/",
r"^tmp/",
r"^scratch/",
]
for file_path in staged_files:
file_str = str(file_path)
# Check if it matches temp patterns but is NOT in .dss/temp/
if any(re.match(pattern, file_str) for pattern in temp_patterns):
if not file_str.startswith(".dss/temp/"):
violations.append(file_str)
if violations:
print(f"{Colors.RED}✗ TEMP FOLDER VIOLATION{Colors.NC}")
print(f"\nTemp files must be created in .dss/temp/ only:")
for v in violations:
print(f" - {v}")
print(f"\nAll temporary files MUST go in: .dss/temp/[session-id]/")
print(f"Use the get_temp_dir() helper function.")
log_audit("temp_folder", "rejected", {"files": violations})
return False
log_audit("temp_folder", "passed", {"files_checked": len(staged_files)})
return True
def check_schemas(staged_files):
"""Validate JSON and YAML schemas"""
violations = []
for file_path in staged_files:
if file_path.suffix in [".json", ".yaml", ".yml"]:
full_path = DSS_ROOT / file_path
try:
if file_path.suffix == ".json":
with open(full_path) as f:
json.load(f)
elif file_path.suffix in [".yaml", ".yml"]:
try:
import yaml
with open(full_path) as f:
yaml.safe_load(f)
except ImportError:
# YAML not available, skip validation
continue
except Exception as e:
violations.append({
"file": str(file_path),
"error": str(e)
})
if violations:
print(f"{Colors.RED}✗ SCHEMA VALIDATION FAILED{Colors.NC}")
print(f"\nInvalid JSON/YAML files:")
for v in violations:
print(f" - {v['file']}")
print(f" Error: {v['error']}")
log_audit("schema_validation", "rejected", {"violations": violations})
return False
log_audit("schema_validation", "passed", {"files_checked": len(staged_files)})
return True
def check_documentation(staged_files):
"""Check that new implementations have documentation"""
violations = []
warnings = []
# Track new Python files that need docstrings
python_files = [f for f in staged_files if f.suffix == ".py"]
for file_path in python_files:
full_path = DSS_ROOT / file_path
if not full_path.exists():
continue
try:
content = full_path.read_text()
# Check for classes without docstrings
class_pattern = r'class\s+(\w+)[^:]*:\s*\n\s*(?!""")'
missing_class_docs = re.findall(class_pattern, content)
# Check for public functions without docstrings (not starting with _)
func_pattern = r'def\s+([a-zA-Z][^_][^(]*)\([^)]*\):\s*\n\s*(?!""")'
missing_func_docs = re.findall(func_pattern, content)
if missing_class_docs:
warnings.append({
"file": str(file_path),
"type": "class",
"items": missing_class_docs[:5] # Limit to first 5
})
if missing_func_docs:
warnings.append({
"file": str(file_path),
"type": "function",
"items": missing_func_docs[:5] # Limit to first 5
})
except Exception as e:
continue
# Check if significant code changes have knowledge updates
code_extensions = [".py", ".ts", ".tsx", ".js", ".jsx"]
code_files_changed = [f for f in staged_files if f.suffix in code_extensions]
knowledge_files_changed = [f for f in staged_files if ".knowledge" in str(f)]
# If many code files changed but no knowledge updates, warn
if len(code_files_changed) > 5 and len(knowledge_files_changed) == 0:
warnings.append({
"file": "general",
"type": "knowledge",
"items": [f"Changed {len(code_files_changed)} code files but no .knowledge/ updates"]
})
if warnings:
print(f"{Colors.YELLOW}⚠ DOCUMENTATION WARNING{Colors.NC}")
print(f"\nMissing documentation found (non-blocking):")
for w in warnings:
if w["type"] == "class":
print(f" - {w['file']}: Classes without docstrings: {', '.join(w['items'])}")
elif w["type"] == "function":
print(f" - {w['file']}: Functions without docstrings: {', '.join(w['items'])}")
elif w["type"] == "knowledge":
print(f" - {w['items'][0]}")
print(f"\n Tip: Add docstrings to new classes/functions")
print(f" Tip: Update .knowledge/ files when adding major features\n")
log_audit("documentation", "warning", {"warnings": warnings})
else:
log_audit("documentation", "passed", {"files_checked": len(staged_files)})
# Always return True (warnings only) - change to False to make blocking
return True
def check_terminology(staged_files):
"""Check for deprecated terminology (warn only)"""
warnings = []
deprecated_terms = {
"swarm": "Design System Server / DSS",
"organism": "component",
}
for file_path in staged_files:
# Only check text files
if file_path.suffix in [".py", ".js", ".ts", ".md", ".txt", ".json", ".yaml", ".yml"]:
full_path = DSS_ROOT / file_path
try:
content = full_path.read_text()
for old_term, new_term in deprecated_terms.items():
if re.search(rf"\b{old_term}\b", content, re.IGNORECASE):
warnings.append({
"file": str(file_path),
"term": old_term,
"suggested": new_term
})
except:
# Skip binary or unreadable files
continue
if warnings:
print(f"{Colors.YELLOW}⚠ TERMINOLOGY WARNING{Colors.NC}")
print(f"\nDeprecated terminology found (non-blocking):")
for w in warnings:
print(f" - {w['file']}: '{w['term']}' → use '{w['suggested']}'")
print()
log_audit("terminology", "warning", {"warnings": warnings})
else:
log_audit("terminology", "passed", {"files_checked": len(staged_files)})
# Always return True (warnings only)
return True
def main():
"""Run all validators"""
print(f"{Colors.GREEN}Running DSS pre-commit validations...{Colors.NC}\n")
staged_files = get_staged_files()
if not staged_files:
print("No files to validate.")
return 0
validators = [
("Immutable File Protection", check_immutable_files),
("Temp Folder Discipline", check_temp_folder),
("Schema Validation", check_schemas),
("Documentation Check", check_documentation),
("Terminology Check", check_terminology),
]
results = []
for name, validator in validators:
print(f"• {name}...", end=" ")
result = validator(staged_files)
results.append(result)
if result:
print(f"{Colors.GREEN}✓{Colors.NC}")
else:
print(f"{Colors.RED}✗{Colors.NC}")
print()
if all(results):
print(f"\n{Colors.GREEN}✓ All validations passed{Colors.NC}")
log_audit("pre_commit", "success", {"files": len(staged_files)})
return 0
else:
print(f"\n{Colors.RED}✗ Pre-commit validation failed{Colors.NC}")
print(f"Fix the issues above and try again.\n")
log_audit("pre_commit", "failed", {"files": len(staged_files)})
return 1
if __name__ == "__main__":
sys.exit(main())

View File

@@ -1,6 +1,6 @@
{
"$schema": "dss-core-v1",
"version": "1.1.0",
"version": "1.3.0",
"last_updated": "2025-12-10",
"purpose": "Single source of truth for AI agents working with DSS",
@@ -24,7 +24,7 @@
"canonical_structure": {
"tokens": ["colors", "spacing", "typography", "borders", "shadows", "motion"],
"components": ["Button", "Input", "Card", "Badge", "Toast", "..."],
"components": "59 shadcn/ui components - see .dss/components/shadcn-registry.json",
"patterns": ["forms", "navigation", "layouts"],
"rule": "This structure NEVER changes. All inputs normalize to this."
},
@@ -87,6 +87,57 @@
"context_compiler": ["dss_get_resolved_context", "dss_get_compiler_status"]
},
"token_architecture": {
"description": "3-Layer Token Cascade with Skin Contract validation",
"layers": {
"1_core_primitives": {
"path": ".dss/core/primitives.json",
"purpose": "Raw Tailwind-style values (colors, spacing, radius, fonts)",
"immutable": true,
"shared": "Across all skins"
},
"2_skin": {
"path": ".dss/skins/{skin_name}/tokens.json",
"purpose": "Semantic mapping from primitives to design tokens",
"contract": ".dss/schema/skin-contract.json",
"examples": ["shadcn", "heroui", "material"],
"rule": "Must provide ALL tokens defined in skin-contract"
},
"3_theme": {
"path": ".dss/themes/{theme_name}.json",
"purpose": "Brand overrides on top of skin",
"rule": "May ONLY override tokens defined in skin-contract",
"survives": "Skin updates (stable API through contract)"
}
},
"cascade_flow": "Core Primitives -> Skin (semantic) -> Theme (brand) = Final Tokens",
"validation": {
"script": "scripts/validate-theme.py",
"checks": [
"Skin provides all contract-required tokens",
"Theme only overrides contract tokens"
]
},
"resolution": {
"script": "scripts/resolve-tokens.py",
"output": ".dss/data/_system/tokens/tokens.json",
"resolves": "Token references like {color.zinc.500} using primitives"
},
"storybook_generation": {
"script": "scripts/generate-storybook.py",
"output": "admin-ui/src/stories/",
"generates": [
"Overview.stories.js - Introduction and architecture",
"ColorPrimitives.stories.js - Tailwind color palette",
"Spacing.stories.js - Spacing scale",
"Radius.stories.js - Border radius tokens",
"Typography.stories.js - Typography styles from Figma",
"Effects.stories.js - Shadows and focus rings",
"SemanticColors.stories.js - Semantic color tokens from skin"
]
}
},
"context_compiler": {
"cascade": "Base Skin -> Extended Skin -> Project Overrides = Final Context",
"caching": "mtime-based invalidation",
@@ -114,7 +165,16 @@
"skills": "dss-claude-plugin/skills/",
"commands": "dss-claude-plugin/commands/",
"logs": ".dss/logs/",
"cache": ".dss/cache/"
"cache": ".dss/cache/",
"core_primitives": ".dss/core/primitives.json",
"skin_contract": ".dss/schema/skin-contract.json",
"skins": ".dss/skins/",
"themes": ".dss/themes/",
"token_resolver": "scripts/resolve-tokens.py",
"theme_validator": "scripts/validate-theme.py",
"storybook_generator": "scripts/generate-storybook.py",
"storybook_stories": "admin-ui/src/stories/",
"component_registry": ".dss/components/shadcn-registry.json"
},
"coding_rules_summary": {
@@ -222,7 +282,27 @@
"cli": "dss ingest --source figma --file tokens.json"
},
"component_registry": {
"path": ".dss/components/shadcn-registry.json",
"source": "shadcn/ui (https://ui.shadcn.com)",
"total_components": 59,
"categories": {
"form": "21 components (Button, Input, Select, Checkbox, etc.)",
"data-display": "11 components (Table, Badge, Avatar, Chart, etc.)",
"feedback": "7 components (Alert, Toast, Progress, Spinner, etc.)",
"navigation": "6 components (Tabs, Breadcrumb, Sidebar, etc.)",
"overlay": "9 components (Dialog, Sheet, Dropdown, Tooltip, etc.)",
"layout": "5 components (Card, Separator, Scroll Area, etc.)"
},
"dependencies": {
"radix": "26 Radix UI primitives",
"additional": ["cmdk", "embla-carousel", "react-day-picker", "recharts", "sonner", "vaul"]
}
},
"changelog": [
{"version": "1.3.0", "date": "2025-12-10", "notes": "Add 59-component shadcn registry, expand primitives to full Tailwind palette"},
{"version": "1.2.0", "date": "2025-12-10", "notes": "Add 3-layer token architecture with skin contract validation"},
{"version": "1.1.0", "date": "2025-12-10", "notes": "Migrate from SQLite to JSON file storage"},
{"version": "1.0.0", "date": "2025-12-10", "notes": "Initial core definition"}
]

View File

@@ -0,0 +1,236 @@
# Intelligent Figma Sync Architecture
> Deep research synthesis from Figmagic, Design Tokens plugin, Figma MCP patterns, and Gemini 3 Pro analysis.
## Executive Summary
DSS intelligent Figma sync should implement a **4-layer pipeline** architecture with hybrid token extraction, W3C token format compliance, intelligent caching, and strict design contract enforcement.
## Architecture Overview
```
┌─────────────────────────────────────────────────────────────┐
│ FIGMA API LAYER │
├─────────────────────────────────────────────────────────────┤
│ GET /files/{key} → File metadata, lastModified │
│ GET /files/{key}/variables → Variables + collections │
│ GET /files/{key}/styles → Color, text, effect styles │
│ GET /files/{key}/nodes → Component structure │
│ GET /images/{key} → Component thumbnails │
└─────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────┐
│ VALIDATION LAYER (NEW) │
├─────────────────────────────────────────────────────────────┤
│ DesignLinter: │
│ - Reject components without proper variant props │
│ - Enforce naming conventions (no "Property 1", "Frame X")│
│ - Mark non-compliant as "Raw" (skip code gen) │
│ - Generate lint report for designers │
└─────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────┐
│ EXTRACTION LAYER │
├─────────────────────────────────────────────────────────────┤
│ VariableExtractor → semantic tokens (color, spacing) │
│ StyleExtractor → typography, effects, grids │
│ ComponentExtractor → component sets, variants, props │
│ AssetExtractor → icons, images (optional) │
└─────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────┐
│ TRANSLATION LAYER │
├─────────────────────────────────────────────────────────────┤
│ FigmaToDSSTranslator: │
│ - Maps Figma naming → DSS canonical names │
│ - Preserves variable REFERENCES (not resolved values) │
│ - Normalizes units (px → rem where appropriate) │
│ - Applies merge strategies (PREFER_FIGMA, LAST, etc) │
│ - Outputs W3C Design Token format │
│ - Separates Visual Props from Interaction States │
└─────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────┐
│ OUTPUT LAYER │
├─────────────────────────────────────────────────────────────┤
│ TokenWriter → .dss/data/_system/tokens/figma-tokens.json│
│ ComponentWriter→ .dss/components/figma-registry.json │
│ StoryGenerator → admin-ui/src/stories/Components/*.stories │
│ CSSGenerator → .dss/themes/figma.css │
└─────────────────────────────────────────────────────────────┘
```
## Key Architectural Decisions
### 1. Hybrid Token Extraction Strategy
| Source | Token Types | Priority |
|--------|-------------|----------|
| Variables | Semantic colors, spacing, breakpoints | Primary |
| Styles | Typography, effects, grids | Secondary |
| Components | UI registry, variants | Tertiary |
**Rationale**: Variables provide modern theming with modes (light/dark), while Styles capture typography which isn't fully available in Variables yet.
### 2. W3C Design Token Format
```json
{
"$schema": "https://design-tokens.org/schema.json",
"color": {
"primary": {
"$value": "{color.blue.600}",
"$type": "color",
"$extensions": {
"figma": {"styleId": "S:abc123", "source": "variables"}
}
}
}
}
```
**Critical**: Preserve token **references** (`{color.blue.600}`), not resolved values (`#0066cc`). This enables:
- Multi-theme switching (Skins)
- Style Dictionary transformation
- Proper CSS variable generation
### 3. Component Variant Classification
```python
VARIANT_CLASSIFICATION = {
# Visual Props → React props / Storybook args
"visual_props": ["Size", "Variant", "Roundness", "Type", "Icon"],
# Interaction States → CSS pseudo-classes (NOT React props)
"interaction_states": ["State", "Hover", "Focused", "Pressed", "Disabled"],
# Boolean toggles → React boolean props
"boolean_props": ["Checked?", "Selected", "Open", "Expanded"]
}
```
**Example Mapping**:
- Figma: `Button / Primary / Hover / Large`
- Code: `<Button variant="primary" size="large" />`
- CSS handles `:hover` state via pseudo-class
### 4. Canonical Token Interface (CTI)
DSS defines universal tokens that Skins map to:
```
DSS Canonical → shadcn Skin → HeroUI Skin
─────────────────────────────────────────────────────────────
--dss-action-bg → --primary → --heroui-primary
--dss-action-fg → --primary-foreground → --heroui-primary-fg
--dss-surface-bg → --background → --heroui-background
--dss-muted-bg → --muted → --heroui-default-100
```
## Intelligent Caching Strategy
### ETag-like Caching
```python
class FigmaSyncCache:
def should_sync(self, file_key: str) -> bool:
manifest = self.load_manifest()
cached = manifest.get(file_key, {})
# Lightweight API call for lastModified
remote_modified = self.get_file_version(file_key)
return (
cached.get("lastModified") != remote_modified or
cached.get("age_hours", 999) > 24
)
def get_changed_nodes(self, file_key: str) -> list:
"""Compare node hashes to find what changed"""
local_hashes = self.load_node_hashes(file_key)
remote_nodes = self.fetch_node_tree(file_key)
changed = []
for node_id, node in remote_nodes.items():
node_hash = hash_node_properties(node)
if local_hashes.get(node_id) != node_hash:
changed.append(node_id)
return changed
```
### Cache Manifest Structure
```json
{
"evCZlaeZrP7X20NIViSJbl": {
"lastModified": "2025-12-10T12:00:00Z",
"syncedAt": "2025-12-10T12:05:00Z",
"nodeHashes": {
"66:5034": "abc123...",
"58:5416": "def456..."
},
"extractedTokens": 245,
"extractedComponents": 67
}
}
```
## Implementation Priorities
| Priority | Task | Impact | Effort |
|----------|------|--------|--------|
| **P0** | Rate limit handling (exponential backoff, request queue) | Critical | Low |
| **P0** | lastModified caching check | High | Low |
| **P1** | Variable extraction (Figma Variables API) | High | Medium |
| **P1** | Translation layer (Figma → DSS canonical) | High | Medium |
| **P1** | Design validation/linting | Medium | Medium |
| **P2** | Incremental node-level sync | Medium | High |
| **P2** | W3C token output format | Medium | Low |
| **P3** | Storybook story generation with variant controls | Medium | Medium |
| **P3** | Asset extraction (icons, images) | Low | Low |
## Design Contract Enforcement
**Principle**: Don't write complex code to handle messy design. Enforce design standards.
### Required Variant Properties
Components must have properly named variant properties to be eligible for code generation:
```
✓ Valid: Button[Size=Large, Variant=Primary, State=Hover]
✗ Invalid: Button[Property 1=true, Frame 4221]
```
### Naming Convention Rules
1. Variant properties: PascalCase (`Size`, `Variant`, `State`)
2. Variant values: PascalCase (`Large`, `Primary`, `Hover`)
3. No auto-generated names (`Property 1`, `Frame 123`)
4. Boolean variants: End with `?` (`Checked?`, `Selected?`)
## File Outputs
| File | Purpose | Format |
|------|---------|--------|
| `.dss/data/_system/tokens/figma-variables.json` | Raw Figma variables | W3C Token |
| `.dss/data/_system/tokens/figma-styles.json` | Typography & effects | W3C Token |
| `.dss/components/figma-registry.json` | Component catalog | DSS Registry |
| `.dss/cache/figma-sync-manifest.json` | Sync state cache | Internal |
| `.dss/logs/figma-lint-report.json` | Design validation | Report |
## Research Sources
- [Figmagic](https://github.com/mikaelvesavuori/figmagic) - Design token extraction patterns
- [Design Tokens Plugin](https://github.com/lukasoppermann/design-tokens) - W3C format, Style Dictionary integration
- [Tokens Studio](https://docs.tokens.studio) - Variable aliasing, theming
- [Figma MCP](https://www.seamgen.com/blog/figma-mcp-complete-guide-to-design-to-code-automation) - AI-assisted code generation
- [Figma Developer API](https://www.figma.com/developers/api) - Official documentation
## Next Steps
1. **Implement P0 items**: Rate limiting and caching
2. **Create token-map.json**: Bridge between Figma IDs and DSS canonical names
3. **Build validation layer**: Design linting before extraction
4. **Test with Obra shadcn UIKit**: Validate against real Figma file

View File

@@ -2,14 +2,14 @@
"$schema": "https://raw.githubusercontent.com/anthropics/claude-code/main/schemas/mcp-servers.schema.json",
"mcpServers": {
"dss": {
"command": "${workspaceFolder}/venv/bin/python3",
"args": ["${workspaceFolder}/dss-claude-plugin/servers/dss-mcp-server.py"],
"command": "/home/overbits/dss/.venv/bin/python3",
"args": ["/home/overbits/dss/dss-claude-plugin/servers/dss-mcp-server.py"],
"env": {
"PYTHONPATH": "${workspaceFolder}:${workspaceFolder}/dss-claude-plugin",
"DSS_HOME": "${workspaceFolder}/.dss",
"DSS_DATABASE": "${workspaceFolder}/.dss/dss.db",
"DSS_CACHE": "${workspaceFolder}/.dss/cache",
"DSS_BASE_PATH": "${workspaceFolder}"
"PYTHONPATH": "/home/overbits/dss:/home/overbits/dss/dss-claude-plugin",
"DSS_HOME": "/home/overbits/dss/.dss",
"DSS_DATABASE": "/home/overbits/dss/.dss/dss.db",
"DSS_CACHE": "/home/overbits/dss/.dss/cache",
"DSS_BASE_PATH": "/home/overbits/dss"
},
"description": "Design System Server MCP - local development"
}

3
admin-ui/.gitignore vendored
View File

@@ -32,3 +32,6 @@ __coverage__
# Logs
logs
*.log
*storybook.log
storybook-static

View File

@@ -0,0 +1,25 @@
/** @type { import('@storybook/html-vite').StorybookConfig } */
const config = {
"stories": [
"../src/**/*.mdx",
"../src/**/*.stories.@(js|jsx|mjs|ts|tsx)"
],
"addons": [
"@chromatic-com/storybook",
"@storybook/addon-vitest",
"@storybook/addon-a11y",
"@storybook/addon-docs"
],
"framework": "@storybook/html-vite",
viteFinal: async (config) => {
config.server = config.server || {};
config.server.allowedHosts = [
'localhost',
'storybook.dss.overbits.luz.uy'
];
return config;
}
};
export default config;

View File

@@ -0,0 +1,23 @@
// Import DSS design tokens
import '../css/tokens.css';
/** @type { import('@storybook/html-vite').Preview } */
const preview = {
parameters: {
controls: {
matchers: {
color: /(background|color)$/i,
date: /Date$/i,
},
},
backgrounds: {
default: 'light',
values: [
{ name: 'light', value: '#ffffff' },
{ name: 'dark', value: '#1a1a1a' },
],
},
},
};
export default preview;

View File

@@ -1,750 +0,0 @@
/**
* DSS Components - Layer 3 (Component Styles)
*
* All component styling using semantic tokens from dss-theme.css.
*
* FIRST PRINCIPLES:
* - This layer references semantic tokens (--header-*, --sidebar-*, etc.)
* - NO fallback values - tokens and theme layers MUST load first
* - If layers are missing, theme-loader.js handles it
* - Uses BEM methodology for class naming
*/
/* ==========================================================================
App Header
========================================================================== */
.app-header {
background-color: var(--header-bg);
border-bottom: 1px solid var(--header-border);
color: var(--header-text);
padding: 0 var(--ds-space-4);
gap: var(--ds-space-4);
height: var(--header-height);
}
.app-header__project-selector {
flex: 0 0 auto;
}
.app-header__team-selector {
flex: 0 0 auto;
}
.app-header__actions {
display: flex;
align-items: center;
gap: var(--ds-space-2);
margin-left: auto;
}
/* ==========================================================================
Sidebar
========================================================================== */
.sidebar {
background-color: var(--sidebar-bg);
border-right: 1px solid var(--sidebar-border);
color: var(--sidebar-text);
padding: var(--ds-space-4);
width: var(--sidebar-width);
}
.sidebar__header {
margin-bottom: var(--ds-space-6);
}
.sidebar__logo {
display: flex;
align-items: center;
gap: var(--ds-space-2);
font-weight: var(--ds-font-weight-semibold);
font-size: var(--ds-font-size-lg);
}
.sidebar__logo-icon {
display: flex;
align-items: center;
justify-content: center;
width: 32px;
height: 32px;
background-color: var(--ds-color-primary);
color: var(--ds-color-primary-foreground);
border-radius: var(--ds-radius-md);
}
.sidebar__nav {
display: flex;
flex-direction: column;
gap: var(--ds-space-2);
flex: 1;
}
.sidebar__help {
margin-top: auto;
padding-top: var(--ds-space-4);
border-top: 1px solid var(--sidebar-border);
}
.sidebar__footer {
margin-top: var(--ds-space-4);
padding-top: var(--ds-space-4);
border-top: 1px solid var(--sidebar-border);
}
/* ==========================================================================
Navigation
========================================================================== */
.nav-section {
display: flex;
flex-direction: column;
gap: var(--ds-space-1);
}
.nav-section + .nav-section {
margin-top: var(--ds-space-4);
padding-top: var(--ds-space-4);
border-top: 1px solid var(--sidebar-border);
}
.nav-section__title {
font-size: var(--ds-font-size-xs);
font-weight: var(--ds-font-weight-semibold);
color: var(--nav-section-title);
text-transform: uppercase;
letter-spacing: var(--ds-letter-spacing-wider);
padding: var(--ds-space-2) var(--ds-space-3);
}
.nav-section__content {
display: flex;
flex-direction: column;
gap: var(--ds-space-1);
}
/* Sub-sections within nav sections */
.nav-sub-section {
display: flex;
flex-direction: column;
gap: var(--ds-space-1);
margin-top: var(--ds-space-1);
}
.nav-sub-section__title {
font-size: var(--ds-font-size-xs);
font-weight: var(--ds-font-weight-medium);
color: var(--nav-section-title);
padding: var(--ds-space-1) var(--ds-space-3);
padding-left: var(--ds-space-4);
opacity: 0.8;
}
/* Navigation item levels */
.nav-item--level-1 {
padding-left: var(--ds-space-3);
}
.nav-item--level-2 {
padding-left: var(--ds-space-6);
font-size: var(--ds-font-size-sm);
}
.nav-item {
display: flex;
align-items: center;
gap: var(--nav-item-gap);
padding: var(--nav-item-padding);
border-radius: var(--nav-item-radius);
color: var(--nav-item-text);
font-size: var(--ds-font-size-sm);
font-weight: var(--ds-font-weight-medium);
transition: all var(--ds-transition-fast) var(--ds-ease-out);
border: 2px solid transparent;
cursor: pointer;
}
.nav-item:hover {
background-color: var(--nav-item-bg-hover);
color: var(--nav-item-text);
}
.nav-item:focus {
outline: none;
border-color: var(--nav-item-border-focus);
background-color: var(--nav-item-bg-hover);
}
.nav-item.active {
background-color: var(--nav-item-bg-active);
color: var(--nav-item-text-active);
font-weight: var(--ds-font-weight-semibold);
}
.nav-item__icon {
width: 18px;
height: 18px;
flex-shrink: 0;
opacity: 0.7;
transition: transform var(--ds-transition-fast) var(--ds-ease-out);
}
.nav-item:hover .nav-item__icon {
transform: scale(1.05);
opacity: 1;
}
/* ==========================================================================
Main Content Area
========================================================================== */
.app-main {
background-color: var(--main-bg);
color: var(--main-text);
}
.app-content {
flex: 1;
overflow-y: auto;
padding: var(--ds-space-6);
}
/* ==========================================================================
Landing Page
========================================================================== */
.landing-page {
display: none;
flex: 1;
flex-direction: column;
overflow-y: auto;
padding: var(--ds-space-6);
background-color: var(--landing-bg);
width: 100%;
}
.landing-page.active {
display: flex;
}
.landing-hero {
text-align: center;
padding: var(--ds-space-8) 0;
margin-bottom: var(--ds-space-8);
}
.landing-hero h1 {
font-size: var(--ds-font-size-3xl);
font-weight: var(--ds-font-weight-bold);
color: var(--landing-hero-text);
margin-bottom: var(--ds-space-3);
}
.landing-hero p {
font-size: var(--ds-font-size-lg);
color: var(--landing-hero-subtitle);
max-width: 600px;
margin: 0 auto;
}
.landing-content {
display: flex;
flex-direction: column;
gap: var(--ds-space-8);
}
/* ==========================================================================
Dashboard Category
========================================================================== */
.dashboard-category {
display: flex;
flex-direction: column;
gap: var(--ds-space-4);
}
.dashboard-category__title {
font-size: var(--ds-font-size-lg);
font-weight: var(--ds-font-weight-semibold);
color: var(--category-title-text);
padding-bottom: var(--ds-space-3);
border-bottom: 1px solid var(--category-title-border);
}
/* ==========================================================================
Dashboard Grid
========================================================================== */
.dashboard-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(250px, 1fr));
gap: var(--ds-space-4);
}
/* ==========================================================================
Dashboard Card
========================================================================== */
.dashboard-card {
display: flex;
flex-direction: column;
padding: var(--ds-space-5);
background-color: var(--dashboard-card-bg);
border: 1px solid var(--dashboard-card-border);
border-radius: var(--card-radius);
color: var(--dashboard-card-text);
cursor: pointer;
transition: all var(--ds-transition-normal) var(--ds-ease-out);
text-decoration: none;
gap: var(--ds-space-3);
}
.dashboard-card:hover {
border-color: var(--dashboard-card-border-hover);
box-shadow: var(--dashboard-card-shadow-hover);
transform: translateY(-2px);
}
.dashboard-card:focus {
outline: var(--focus-ring-width) solid var(--focus-ring-color);
outline-offset: var(--focus-ring-offset);
}
.dashboard-card__icon {
font-size: var(--ds-font-size-2xl);
}
.dashboard-card__content {
display: flex;
flex-direction: column;
gap: var(--ds-space-1);
flex: 1;
}
.dashboard-card__title {
font-size: var(--ds-font-size-base);
font-weight: var(--ds-font-weight-semibold);
color: var(--dashboard-card-text);
}
.dashboard-card__description {
font-size: var(--ds-font-size-sm);
color: var(--dashboard-card-text-muted);
line-height: var(--ds-line-height-relaxed);
}
.dashboard-card__meta {
display: flex;
align-items: center;
justify-content: flex-end;
color: var(--dashboard-card-text-muted);
font-size: var(--ds-font-size-lg);
}
/* ==========================================================================
AI Sidebar (Right Panel)
========================================================================== */
.app-sidebar {
width: 360px;
background-color: var(--sidebar-bg);
border-left: 1px solid var(--sidebar-border);
display: flex;
flex-direction: column;
overflow: hidden;
}
.app-sidebar[hidden] {
display: none;
}
/* ==========================================================================
Buttons
========================================================================== */
ds-button,
.ds-button {
display: inline-flex;
align-items: center;
justify-content: center;
gap: var(--ds-space-2);
padding: var(--button-padding-y) var(--button-padding-x);
font-size: var(--button-font-size);
font-weight: var(--button-font-weight);
border-radius: var(--button-radius);
transition: all var(--ds-transition-fast) var(--ds-ease-out);
cursor: pointer;
border: 1px solid transparent;
}
ds-button[data-variant="default"],
.ds-button--default {
background-color: var(--button-bg);
color: var(--button-text);
border-color: var(--button-border);
}
ds-button[data-variant="default"]:hover,
.ds-button--default:hover {
background-color: var(--button-bg-hover);
}
ds-button[data-variant="ghost"],
.ds-button--ghost {
background-color: var(--button-ghost-bg);
color: var(--button-ghost-text);
}
ds-button[data-variant="ghost"]:hover,
.ds-button--ghost:hover {
background-color: var(--button-ghost-bg-hover);
}
ds-button[data-variant="outline"],
.ds-button--outline {
background-color: var(--button-outline-bg);
color: var(--button-outline-text);
border-color: var(--button-outline-border);
}
ds-button[data-variant="outline"]:hover,
.ds-button--outline:hover {
background-color: var(--button-outline-bg-hover);
}
ds-button[data-size="icon"],
.ds-button--icon {
padding: var(--ds-space-2);
width: 36px;
height: 36px;
}
ds-button:focus,
.ds-button:focus {
outline: var(--focus-ring-width) solid var(--focus-ring-color);
outline-offset: var(--focus-ring-offset);
}
/* ==========================================================================
Cards
========================================================================== */
ds-card,
.ds-card {
display: block;
background-color: var(--card-bg);
border: 1px solid var(--card-border);
border-radius: var(--card-radius);
padding: var(--card-padding);
box-shadow: var(--card-shadow);
color: var(--card-text);
}
ds-card:hover,
.ds-card:hover {
box-shadow: var(--card-shadow-hover);
}
/* ==========================================================================
Badges
========================================================================== */
ds-badge,
.ds-badge {
display: inline-flex;
align-items: center;
padding: var(--badge-padding-y) var(--badge-padding-x);
font-size: var(--badge-font-size);
font-weight: var(--ds-font-weight-medium);
border-radius: var(--badge-radius);
background-color: var(--badge-bg);
color: var(--badge-text);
}
ds-badge[data-variant="secondary"],
.ds-badge--secondary {
background-color: var(--badge-secondary-bg);
color: var(--badge-secondary-text);
}
ds-badge[data-variant="outline"],
.ds-badge--outline {
background-color: var(--badge-outline-bg);
color: var(--badge-outline-text);
border: 1px solid var(--badge-outline-border);
}
ds-badge[data-variant="success"],
.ds-badge--success {
background-color: var(--badge-success-bg);
color: var(--badge-success-text);
}
ds-badge[data-variant="warning"],
.ds-badge--warning {
background-color: var(--badge-warning-bg);
color: var(--badge-warning-text);
}
ds-badge[data-variant="error"],
.ds-badge--error {
background-color: var(--badge-error-bg);
color: var(--badge-error-text);
}
/* ==========================================================================
Inputs
========================================================================== */
ds-input,
.ds-input,
input[type="text"],
input[type="email"],
input[type="password"],
input[type="search"],
textarea {
display: block;
width: 100%;
padding: var(--input-padding-y) var(--input-padding-x);
background-color: var(--input-bg);
color: var(--input-text);
border: 1px solid var(--input-border);
border-radius: var(--input-radius);
font-size: var(--ds-font-size-sm);
transition: border-color var(--ds-transition-fast) var(--ds-ease-out);
}
ds-input:focus,
.ds-input:focus,
input:focus,
textarea:focus {
outline: none;
border-color: var(--input-border-focus);
box-shadow: 0 0 0 var(--focus-ring-width) var(--focus-ring-color);
}
ds-input::placeholder,
.ds-input::placeholder,
input::placeholder,
textarea::placeholder {
color: var(--input-placeholder);
}
/* ==========================================================================
Select
========================================================================== */
select,
.ds-select {
display: block;
width: 100%;
padding: var(--input-padding-y) var(--input-padding-x);
background-color: var(--input-bg);
color: var(--input-text);
border: 1px solid var(--input-border);
border-radius: var(--input-radius);
font-size: var(--ds-font-size-sm);
cursor: pointer;
}
select:focus,
.ds-select:focus {
outline: none;
border-color: var(--input-border-focus);
box-shadow: 0 0 0 var(--focus-ring-width) var(--focus-ring-color);
}
.team-select {
padding: var(--ds-space-1-5) var(--ds-space-3);
background-color: var(--input-bg);
border: 1px solid var(--input-border);
border-radius: var(--input-radius);
}
/* ==========================================================================
Avatar
========================================================================== */
.ds-avatar {
display: inline-flex;
align-items: center;
justify-content: center;
width: var(--avatar-size-md);
height: var(--avatar-size-md);
background-color: var(--avatar-bg);
color: var(--avatar-text);
border: 1px solid var(--avatar-border);
border-radius: var(--ds-radius-full);
font-size: var(--ds-font-size-sm);
font-weight: var(--ds-font-weight-medium);
cursor: pointer;
}
.ds-avatar:focus {
outline: var(--focus-ring-width) solid var(--focus-ring-color);
outline-offset: var(--focus-ring-offset);
}
/* ==========================================================================
Help Panel
========================================================================== */
.help-panel {
background-color: var(--help-panel-bg);
border-radius: var(--ds-radius-md);
border: 1px solid var(--help-panel-border);
}
.help-panel__toggle {
display: flex;
align-items: center;
gap: var(--ds-space-2);
padding: var(--ds-space-3);
font-size: var(--ds-font-size-sm);
font-weight: var(--ds-font-weight-medium);
color: var(--help-panel-text);
cursor: pointer;
list-style: none;
}
.help-panel__toggle::-webkit-details-marker {
display: none;
}
.help-panel__content {
padding: var(--ds-space-3);
padding-top: 0;
font-size: var(--ds-font-size-sm);
color: var(--help-panel-text-muted);
}
.help-section {
margin-bottom: var(--ds-space-3);
}
.help-section strong {
display: block;
color: var(--help-panel-text);
margin-bottom: var(--ds-space-1);
}
.help-section ul,
.help-section ol {
padding-left: var(--ds-space-4);
list-style: disc;
}
.help-section ol {
list-style: decimal;
}
.help-section li {
margin-bottom: var(--ds-space-1);
}
/* ==========================================================================
Status Indicators
========================================================================== */
.status-dot {
width: 8px;
height: 8px;
border-radius: var(--ds-radius-full);
background-color: var(--ds-color-muted-foreground);
}
.status-dot--success {
background-color: var(--ds-color-success);
}
.status-dot--warning {
background-color: var(--ds-color-warning);
}
.status-dot--error {
background-color: var(--ds-color-error);
}
.status-dot--info {
background-color: var(--ds-color-info);
}
/* ==========================================================================
Notification Toggle Container
========================================================================== */
.notification-toggle-container {
position: relative;
}
/* ==========================================================================
Project Selector
========================================================================== */
.project-selector {
display: flex;
align-items: center;
gap: var(--ds-space-2);
}
.project-selector__label {
font-size: var(--ds-font-size-sm);
font-weight: var(--ds-font-weight-medium);
color: var(--text-muted);
}
.project-selector__select {
padding: var(--ds-space-1-5) var(--ds-space-3);
background-color: var(--input-bg);
border: 1px solid var(--input-border);
border-radius: var(--input-radius);
font-size: var(--ds-font-size-sm);
}
/* ==========================================================================
Responsive Adjustments
========================================================================== */
@media (max-width: 1024px) {
.app-sidebar {
width: 300px;
}
.dashboard-grid {
grid-template-columns: repeat(auto-fill, minmax(220px, 1fr));
}
}
@media (max-width: 768px) {
.app-header__team-selector {
display: none;
}
.app-sidebar {
position: fixed;
top: var(--header-height);
right: 0;
bottom: 0;
width: 100%;
max-width: 360px;
transform: translateX(100%);
transition: transform var(--ds-transition-slow) var(--ds-ease-out);
z-index: 60;
}
.app-sidebar.open {
transform: translateX(0);
}
.dashboard-grid {
grid-template-columns: 1fr;
}
.landing-hero h1 {
font-size: var(--ds-font-size-2xl);
}
.landing-hero p {
font-size: var(--ds-font-size-base);
}
}

View File

@@ -1,230 +0,0 @@
/**
* DSS Core CSS - Layer 0 (Structural Only)
*
* This file provides the structural foundation for DSS Admin UI.
* It contains NO design decisions - only layout and structural CSS.
* The UI should be functional (but unstyled) with only this file.
*/
/* ==========================================================================
CSS Reset / Normalize
========================================================================== */
*, *::before, *::after {
box-sizing: border-box;
margin: 0;
padding: 0;
}
html {
line-height: 1.15;
-webkit-text-size-adjust: 100%;
font-size: 16px;
}
body {
min-height: 100vh;
text-rendering: optimizeSpeed;
font-family: system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
}
img, picture, video, canvas, svg {
display: block;
max-width: 100%;
}
input, button, textarea, select {
font: inherit;
}
a {
text-decoration: none;
color: inherit;
}
ul, ol {
list-style: none;
}
button {
cursor: pointer;
border: none;
background: none;
}
/* ==========================================================================
App Shell - CSS Grid Layout
========================================================================== */
.app-layout {
display: grid;
grid-template-columns: var(--app-sidebar-width, 240px) 1fr;
grid-template-rows: var(--app-header-height, 60px) 1fr;
min-height: 100vh;
width: 100%;
overflow: hidden;
}
.app-header {
grid-column: 1 / -1;
grid-row: 1;
display: flex;
align-items: center;
justify-content: space-between;
width: 100%;
z-index: 40;
}
.sidebar {
grid-column: 1;
grid-row: 2;
display: flex;
flex-direction: column;
overflow-y: auto;
overflow-x: hidden;
z-index: 30;
}
.app-main {
grid-column: 2;
grid-row: 2;
display: flex;
flex-direction: column;
overflow: hidden;
position: relative;
z-index: 10;
}
/* ==========================================================================
Flexbox Utilities
========================================================================== */
.flex { display: flex; }
.flex-col { flex-direction: column; }
.flex-row { flex-direction: row; }
.flex-wrap { flex-wrap: wrap; }
.flex-1 { flex: 1; }
.flex-auto { flex: auto; }
.flex-none { flex: none; }
.items-start { align-items: flex-start; }
.items-center { align-items: center; }
.items-end { align-items: flex-end; }
.items-stretch { align-items: stretch; }
.justify-start { justify-content: flex-start; }
.justify-center { justify-content: center; }
.justify-end { justify-content: flex-end; }
.justify-between { justify-content: space-between; }
.justify-around { justify-content: space-around; }
/* Gap utilities use hardcoded fallbacks since core loads before tokens */
.gap-1 { gap: 0.25rem; }
.gap-2 { gap: 0.5rem; }
.gap-3 { gap: 0.75rem; }
.gap-4 { gap: 1rem; }
.gap-5 { gap: 1.25rem; }
.gap-6 { gap: 1.5rem; }
/* ==========================================================================
Grid Utilities
========================================================================== */
.grid { display: grid; }
.grid-cols-1 { grid-template-columns: repeat(1, minmax(0, 1fr)); }
.grid-cols-2 { grid-template-columns: repeat(2, minmax(0, 1fr)); }
.grid-cols-3 { grid-template-columns: repeat(3, minmax(0, 1fr)); }
.grid-cols-4 { grid-template-columns: repeat(4, minmax(0, 1fr)); }
.grid-auto-fill { grid-template-columns: repeat(auto-fill, minmax(250px, 1fr)); }
/* ==========================================================================
Visibility & Display
========================================================================== */
.hidden { display: none !important; }
.block { display: block; }
.inline-block { display: inline-block; }
.inline { display: inline; }
.sr-only {
position: absolute;
width: 1px;
height: 1px;
padding: 0;
margin: -1px;
overflow: hidden;
clip: rect(0, 0, 0, 0);
white-space: nowrap;
border: 0;
}
/* ==========================================================================
Overflow & Scroll
========================================================================== */
.overflow-hidden { overflow: hidden; }
.overflow-auto { overflow: auto; }
.overflow-x-auto { overflow-x: auto; }
.overflow-y-auto { overflow-y: auto; }
.overflow-scroll { overflow: scroll; }
/* ==========================================================================
Position
========================================================================== */
.relative { position: relative; }
.absolute { position: absolute; }
.fixed { position: fixed; }
.sticky { position: sticky; }
.inset-0 { top: 0; right: 0; bottom: 0; left: 0; }
.top-0 { top: 0; }
.right-0 { right: 0; }
.bottom-0 { bottom: 0; }
.left-0 { left: 0; }
/* ==========================================================================
Width & Height
========================================================================== */
.w-full { width: 100%; }
.w-auto { width: auto; }
.h-full { height: 100%; }
.h-auto { height: auto; }
.min-h-screen { min-height: 100vh; }
/* ==========================================================================
Responsive Breakpoints
========================================================================== */
@media (max-width: 1024px) {
.app-layout {
grid-template-columns: var(--app-sidebar-width-tablet, 200px) 1fr;
}
}
@media (max-width: 768px) {
.app-layout {
grid-template-columns: 1fr;
grid-template-rows: var(--app-header-height, 60px) 1fr;
}
.sidebar {
position: fixed;
top: var(--app-header-height, 60px);
left: 0;
bottom: 0;
width: var(--app-sidebar-width, 240px);
transform: translateX(-100%);
transition: transform 0.3s ease;
z-index: 50;
}
.sidebar.open {
transform: translateX(0);
}
.app-main {
grid-column: 1;
}
}

View File

@@ -1,6 +0,0 @@
/* Design System Integrations CSS
* This file contains integration-specific styles for third-party components
* and external library theming.
*/
/* Placeholder for future integrations */

View File

@@ -1,208 +0,0 @@
/**
* DSS Theme - Layer 2 (Semantic Mapping)
*
* Maps design tokens to semantic purposes.
* This layer creates the bridge between raw design tokens
* and component-specific styling.
*
* FIRST PRINCIPLES:
* - No fallback values - tokens layer MUST load first
* - If tokens are missing, theme-loader.js handles it with fallback CSS file
* - Components should use these semantic tokens, not raw tokens directly
*/
:root {
/* ==========================================================================
App Shell Semantic Tokens
========================================================================== */
/* Header */
--header-bg: var(--ds-color-surface-0);
--header-border: var(--ds-color-border);
--header-text: var(--ds-color-foreground);
--header-height: var(--app-header-height);
/* Sidebar */
--sidebar-bg: var(--ds-color-surface-0);
--sidebar-border: var(--ds-color-border);
--sidebar-text: var(--ds-color-foreground);
--sidebar-width: var(--app-sidebar-width);
/* Main Content */
--main-bg: var(--ds-color-background);
--main-text: var(--ds-color-foreground);
/* ==========================================================================
Navigation Semantic Tokens
========================================================================== */
--nav-item-text: var(--ds-color-foreground);
--nav-item-text-muted: var(--ds-color-muted-foreground);
--nav-item-bg-hover: var(--ds-color-accent);
--nav-item-bg-active: var(--ds-color-accent);
--nav-item-text-active: var(--ds-color-primary);
--nav-item-border-focus: var(--ds-color-ring);
--nav-section-title: var(--ds-color-muted-foreground);
--nav-item-radius: var(--ds-radius-md);
--nav-item-padding: var(--ds-space-3);
--nav-item-gap: var(--ds-space-3);
/* ==========================================================================
Button Semantic Tokens
========================================================================== */
/* Default Button */
--button-bg: var(--ds-color-primary);
--button-text: var(--ds-color-primary-foreground);
--button-border: var(--ds-color-primary);
--button-bg-hover: hsl(var(--ds-color-primary-h) var(--ds-color-primary-s) calc(var(--ds-color-primary-l) + 10%));
/* Secondary Button */
--button-secondary-bg: var(--ds-color-secondary);
--button-secondary-text: var(--ds-color-secondary-foreground);
/* Ghost Button */
--button-ghost-bg: transparent;
--button-ghost-text: var(--ds-color-foreground);
--button-ghost-bg-hover: var(--ds-color-accent);
/* Outline Button */
--button-outline-bg: transparent;
--button-outline-text: var(--ds-color-foreground);
--button-outline-border: var(--ds-color-border);
--button-outline-bg-hover: var(--ds-color-accent);
/* Button Sizing */
--button-padding-x: var(--ds-space-4);
--button-padding-y: var(--ds-space-2);
--button-radius: var(--ds-radius-md);
--button-font-size: var(--ds-font-size-sm);
--button-font-weight: var(--ds-font-weight-medium);
/* ==========================================================================
Card Semantic Tokens
========================================================================== */
--card-bg: var(--ds-color-surface-0);
--card-border: var(--ds-color-border);
--card-text: var(--ds-color-foreground);
--card-text-muted: var(--ds-color-muted-foreground);
--card-radius: var(--ds-radius-lg);
--card-padding: var(--ds-space-5);
--card-shadow: var(--ds-shadow-sm);
--card-shadow-hover: var(--ds-shadow-md);
/* ==========================================================================
Input Semantic Tokens
========================================================================== */
--input-bg: var(--ds-color-background);
--input-text: var(--ds-color-foreground);
--input-placeholder: var(--ds-color-muted-foreground);
--input-border: var(--ds-color-border);
--input-border-focus: var(--ds-color-ring);
--input-radius: var(--ds-radius-md);
--input-padding-x: var(--ds-space-3);
--input-padding-y: var(--ds-space-2);
/* ==========================================================================
Badge Semantic Tokens
========================================================================== */
--badge-bg: var(--ds-color-primary);
--badge-text: var(--ds-color-primary-foreground);
--badge-radius: var(--ds-radius-full);
--badge-padding-x: var(--ds-space-2-5);
--badge-padding-y: var(--ds-space-0-5);
--badge-font-size: var(--ds-font-size-xs);
/* Badge Variants */
--badge-secondary-bg: var(--ds-color-secondary);
--badge-secondary-text: var(--ds-color-secondary-foreground);
--badge-outline-bg: transparent;
--badge-outline-text: var(--ds-color-foreground);
--badge-outline-border: var(--ds-color-border);
--badge-success-bg: var(--ds-color-success);
--badge-success-text: var(--ds-color-success-foreground);
--badge-warning-bg: var(--ds-color-warning);
--badge-warning-text: var(--ds-color-warning-foreground);
--badge-error-bg: var(--ds-color-error);
--badge-error-text: var(--ds-color-error-foreground);
/* ==========================================================================
Landing Page Semantic Tokens
========================================================================== */
--landing-bg: var(--ds-color-background);
--landing-hero-text: var(--ds-color-foreground);
--landing-hero-subtitle: var(--ds-color-muted-foreground);
/* Dashboard Card */
--dashboard-card-bg: var(--ds-color-surface-0);
--dashboard-card-border: var(--ds-color-border);
--dashboard-card-border-hover: var(--ds-color-accent);
--dashboard-card-text: var(--ds-color-foreground);
--dashboard-card-text-muted: var(--ds-color-muted-foreground);
--dashboard-card-shadow-hover: var(--ds-shadow-md);
/* Category Title */
--category-title-text: var(--ds-color-foreground);
--category-title-border: var(--ds-color-border);
/* ==========================================================================
Toast/Notification Semantic Tokens
========================================================================== */
--toast-bg: var(--ds-color-surface-0);
--toast-text: var(--ds-color-foreground);
--toast-border: var(--ds-color-border);
--toast-shadow: var(--ds-shadow-lg);
--toast-radius: var(--ds-radius-lg);
--toast-success-bg: var(--ds-color-success);
--toast-success-text: var(--ds-color-success-foreground);
--toast-warning-bg: var(--ds-color-warning);
--toast-warning-text: var(--ds-color-warning-foreground);
--toast-error-bg: var(--ds-color-error);
--toast-error-text: var(--ds-color-error-foreground);
--toast-info-bg: var(--ds-color-info);
--toast-info-text: var(--ds-color-info-foreground);
/* ==========================================================================
Avatar Semantic Tokens
========================================================================== */
--avatar-bg: var(--ds-color-muted);
--avatar-text: var(--ds-color-foreground);
--avatar-border: var(--ds-color-border);
--avatar-size-sm: 32px;
--avatar-size-md: 40px;
--avatar-size-lg: 48px;
/* ==========================================================================
Help Panel Semantic Tokens
========================================================================== */
--help-panel-bg: var(--ds-color-surface-1);
--help-panel-border: var(--ds-color-border);
--help-panel-text: var(--ds-color-foreground);
--help-panel-text-muted: var(--ds-color-muted-foreground);
/* ==========================================================================
Typography Semantic Tokens
========================================================================== */
--text-heading: var(--ds-color-foreground);
--text-body: var(--ds-color-foreground);
--text-muted: var(--ds-color-muted-foreground);
--text-link: var(--ds-color-primary);
--text-link-hover: hsl(var(--ds-color-primary-h) var(--ds-color-primary-s) calc(var(--ds-color-primary-l) + 20%));
/* ==========================================================================
Focus Ring
========================================================================== */
--focus-ring-width: 2px;
--focus-ring-color: var(--ds-color-ring);
--focus-ring-offset: 2px;
}

View File

@@ -1,246 +0,0 @@
/**
* DSS Design Tokens - Layer 1
*
* Design decisions expressed as CSS custom properties.
* These tokens can be:
* 1. Generated from Figma using DSS extraction tools
* 2. Manually defined in a design-tokens.json file
* 3. Use the fallback defaults defined here
*
* Format follows W3C Design Tokens specification.
* All values include fallbacks for bootstrap scenario.
*/
:root {
/* ==========================================================================
Color Tokens - HSL Format
========================================================================== */
/* Primary Colors */
--ds-color-primary-h: 220;
--ds-color-primary-s: 14%;
--ds-color-primary-l: 10%;
--ds-color-primary: hsl(var(--ds-color-primary-h) var(--ds-color-primary-s) var(--ds-color-primary-l));
--ds-color-primary-foreground: hsl(0 0% 100%);
/* Secondary Colors */
--ds-color-secondary-h: 220;
--ds-color-secondary-s: 9%;
--ds-color-secondary-l: 46%;
--ds-color-secondary: hsl(var(--ds-color-secondary-h) var(--ds-color-secondary-s) var(--ds-color-secondary-l));
--ds-color-secondary-foreground: hsl(0 0% 100%);
/* Accent Colors */
--ds-color-accent-h: 220;
--ds-color-accent-s: 9%;
--ds-color-accent-l: 96%;
--ds-color-accent: hsl(var(--ds-color-accent-h) var(--ds-color-accent-s) var(--ds-color-accent-l));
--ds-color-accent-foreground: hsl(220 14% 10%);
/* Background Colors */
--ds-color-background: hsl(0 0% 100%);
--ds-color-foreground: hsl(220 14% 10%);
/* Surface Colors */
--ds-color-surface-0: hsl(0 0% 100%);
--ds-color-surface-1: hsl(220 14% 98%);
--ds-color-surface-2: hsl(220 9% 96%);
--ds-color-surface-3: hsl(220 9% 94%);
/* Muted Colors */
--ds-color-muted: hsl(220 9% 96%);
--ds-color-muted-foreground: hsl(220 9% 46%);
/* Border Colors */
--ds-color-border: hsl(220 9% 89%);
--ds-color-border-strong: hsl(220 9% 80%);
/* State Colors */
--ds-color-success: hsl(142 76% 36%);
--ds-color-success-foreground: hsl(0 0% 100%);
--ds-color-warning: hsl(38 92% 50%);
--ds-color-warning-foreground: hsl(0 0% 0%);
--ds-color-error: hsl(0 84% 60%);
--ds-color-error-foreground: hsl(0 0% 100%);
--ds-color-info: hsl(199 89% 48%);
--ds-color-info-foreground: hsl(0 0% 100%);
/* Ring/Focus Color */
--ds-color-ring: hsl(220 14% 10%);
/* ==========================================================================
Spacing Scale
========================================================================== */
--ds-space-0: 0;
--ds-space-px: 1px;
--ds-space-0-5: 0.125rem; /* 2px */
--ds-space-1: 0.25rem; /* 4px */
--ds-space-1-5: 0.375rem; /* 6px */
--ds-space-2: 0.5rem; /* 8px */
--ds-space-2-5: 0.625rem; /* 10px */
--ds-space-3: 0.75rem; /* 12px */
--ds-space-3-5: 0.875rem; /* 14px */
--ds-space-4: 1rem; /* 16px */
--ds-space-5: 1.25rem; /* 20px */
--ds-space-6: 1.5rem; /* 24px */
--ds-space-7: 1.75rem; /* 28px */
--ds-space-8: 2rem; /* 32px */
--ds-space-9: 2.25rem; /* 36px */
--ds-space-10: 2.5rem; /* 40px */
--ds-space-11: 2.75rem; /* 44px */
--ds-space-12: 3rem; /* 48px */
--ds-space-14: 3.5rem; /* 56px */
--ds-space-16: 4rem; /* 64px */
--ds-space-20: 5rem; /* 80px */
--ds-space-24: 6rem; /* 96px */
/* ==========================================================================
Typography - Font Sizes
========================================================================== */
--ds-font-size-xs: 0.75rem; /* 12px */
--ds-font-size-sm: 0.875rem; /* 14px */
--ds-font-size-base: 1rem; /* 16px */
--ds-font-size-lg: 1.125rem; /* 18px */
--ds-font-size-xl: 1.25rem; /* 20px */
--ds-font-size-2xl: 1.5rem; /* 24px */
--ds-font-size-3xl: 1.875rem; /* 30px */
--ds-font-size-4xl: 2.25rem; /* 36px */
--ds-font-size-5xl: 3rem; /* 48px */
/* ==========================================================================
Typography - Font Weights
========================================================================== */
--ds-font-weight-thin: 100;
--ds-font-weight-extralight: 200;
--ds-font-weight-light: 300;
--ds-font-weight-normal: 400;
--ds-font-weight-medium: 500;
--ds-font-weight-semibold: 600;
--ds-font-weight-bold: 700;
--ds-font-weight-extrabold: 800;
--ds-font-weight-black: 900;
/* ==========================================================================
Typography - Line Heights
========================================================================== */
--ds-line-height-none: 1;
--ds-line-height-tight: 1.25;
--ds-line-height-snug: 1.375;
--ds-line-height-normal: 1.5;
--ds-line-height-relaxed: 1.625;
--ds-line-height-loose: 2;
/* ==========================================================================
Typography - Letter Spacing
========================================================================== */
--ds-letter-spacing-tighter: -0.05em;
--ds-letter-spacing-tight: -0.025em;
--ds-letter-spacing-normal: 0;
--ds-letter-spacing-wide: 0.025em;
--ds-letter-spacing-wider: 0.05em;
--ds-letter-spacing-widest: 0.1em;
/* ==========================================================================
Typography - Font Families
========================================================================== */
--ds-font-family-sans: system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
--ds-font-family-serif: Georgia, Cambria, 'Times New Roman', Times, serif;
--ds-font-family-mono: ui-monospace, SFMono-Regular, 'SF Mono', Menlo, Consolas, 'Liberation Mono', monospace;
/* ==========================================================================
Border Radius
========================================================================== */
--ds-radius-none: 0;
--ds-radius-sm: 0.125rem; /* 2px */
--ds-radius-md: 0.375rem; /* 6px */
--ds-radius-lg: 0.5rem; /* 8px */
--ds-radius-xl: 0.75rem; /* 12px */
--ds-radius-2xl: 1rem; /* 16px */
--ds-radius-full: 9999px;
/* ==========================================================================
Shadows
========================================================================== */
--ds-shadow-xs: 0 1px 2px 0 rgb(0 0 0 / 0.05);
--ds-shadow-sm: 0 1px 3px 0 rgb(0 0 0 / 0.1), 0 1px 2px -1px rgb(0 0 0 / 0.1);
--ds-shadow-md: 0 4px 6px -1px rgb(0 0 0 / 0.1), 0 2px 4px -2px rgb(0 0 0 / 0.1);
--ds-shadow-lg: 0 10px 15px -3px rgb(0 0 0 / 0.1), 0 4px 6px -4px rgb(0 0 0 / 0.1);
--ds-shadow-xl: 0 20px 25px -5px rgb(0 0 0 / 0.1), 0 8px 10px -6px rgb(0 0 0 / 0.1);
--ds-shadow-2xl: 0 25px 50px -12px rgb(0 0 0 / 0.25);
--ds-shadow-inner: inset 0 2px 4px 0 rgb(0 0 0 / 0.05);
--ds-shadow-none: 0 0 #0000;
/* ==========================================================================
Transitions
========================================================================== */
--ds-transition-fast: 150ms;
--ds-transition-normal: 200ms;
--ds-transition-slow: 300ms;
--ds-transition-slower: 500ms;
--ds-ease-linear: linear;
--ds-ease-in: cubic-bezier(0.4, 0, 1, 1);
--ds-ease-out: cubic-bezier(0, 0, 0.2, 1);
--ds-ease-in-out: cubic-bezier(0.4, 0, 0.2, 1);
/* ==========================================================================
Z-Index Scale
========================================================================== */
--ds-z-0: 0;
--ds-z-10: 10;
--ds-z-20: 20;
--ds-z-30: 30;
--ds-z-40: 40;
--ds-z-50: 50;
--ds-z-auto: auto;
/* ==========================================================================
App-Specific Structural Tokens
========================================================================== */
--app-header-height: 60px;
--app-sidebar-width: 240px;
--app-sidebar-width-tablet: 200px;
}
/* ==========================================================================
Dark Mode Tokens
========================================================================== */
[data-theme="dark"],
.dark {
--ds-color-primary-h: 220;
--ds-color-primary-s: 14%;
--ds-color-primary-l: 90%;
--ds-color-primary: hsl(var(--ds-color-primary-h) var(--ds-color-primary-s) var(--ds-color-primary-l));
--ds-color-primary-foreground: hsl(220 14% 10%);
--ds-color-background: hsl(220 14% 10%);
--ds-color-foreground: hsl(220 9% 94%);
--ds-color-surface-0: hsl(220 14% 10%);
--ds-color-surface-1: hsl(220 14% 14%);
--ds-color-surface-2: hsl(220 14% 18%);
--ds-color-surface-3: hsl(220 14% 22%);
--ds-color-muted: hsl(220 14% 18%);
--ds-color-muted-foreground: hsl(220 9% 60%);
--ds-color-border: hsl(220 14% 22%);
--ds-color-border-strong: hsl(220 14% 30%);
--ds-color-accent: hsl(220 14% 18%);
--ds-color-accent-foreground: hsl(220 9% 94%);
--ds-color-ring: hsl(220 9% 80%);
}

View File

@@ -1,259 +0,0 @@
/**
* ds-admin-settings.js
* Admin settings panel for DSS configuration
* Allows configuration of hostname, port, and local/remote setup
*/
import { useAdminStore } from '../../stores/admin-store.js';
export default class AdminSettings extends HTMLElement {
constructor() {
super();
this.adminStore = useAdminStore();
this.state = this.adminStore.getState();
}
connectedCallback() {
this.render();
this.setupEventListeners();
this.unsubscribe = this.adminStore.subscribe(() => {
this.state = this.adminStore.getState();
this.updateUI();
});
}
disconnectedCallback() {
if (this.unsubscribe) this.unsubscribe();
}
render() {
this.innerHTML = `
<div style="padding: 24px; max-width: 600px;">
<h2 style="margin-bottom: 24px; font-size: 20px;">DSS Settings</h2>
<!-- Hostname Setting -->
<div style="margin-bottom: 24px;">
<label style="display: block; margin-bottom: 8px; font-weight: 500;">
Hostname
</label>
<input
id="hostname-input"
type="text"
value="${this.state.hostname}"
style="
width: 100%;
padding: 8px 12px;
border: 1px solid var(--vscode-input-border);
background: var(--vscode-input-background);
color: var(--vscode-foreground);
border-radius: 4px;
font-family: monospace;
"
placeholder="localhost or IP address"
/>
<div style="font-size: 11px; color: var(--vscode-text-dim); margin-top: 4px;">
Default: localhost
</div>
</div>
<!-- Port Setting -->
<div style="margin-bottom: 24px;">
<label style="display: block; margin-bottom: 8px; font-weight: 500;">
Storybook Port
</label>
<input
id="port-input"
type="number"
value="${this.state.port}"
style="
width: 100%;
padding: 8px 12px;
border: 1px solid var(--vscode-input-border);
background: var(--vscode-input-background);
color: var(--vscode-foreground);
border-radius: 4px;
font-family: monospace;
"
placeholder="6006"
min="1"
max="65535"
/>
<div style="font-size: 11px; color: var(--vscode-text-dim); margin-top: 4px;">
Default: 6006 (Storybook standard port)
</div>
</div>
<!-- DSS Setup Type -->
<div style="margin-bottom: 24px;">
<label style="display: block; margin-bottom: 12px; font-weight: 500;">
DSS Setup Type
</label>
<div style="display: flex; gap: 16px;">
<label style="display: flex; align-items: center; cursor: pointer;">
<input
type="radio"
name="setup-type"
value="local"
${this.state.isRemote ? '' : 'checked'}
style="margin-right: 8px;"
/>
<span>Local</span>
</label>
<label style="display: flex; align-items: center; cursor: pointer;">
<input
type="radio"
name="setup-type"
value="remote"
${this.state.isRemote ? 'checked' : ''}
style="margin-right: 8px;"
/>
<span>Remote (Headless)</span>
</label>
</div>
<div style="font-size: 11px; color: var(--vscode-text-dim); margin-top: 8px;">
<strong>Local:</strong> Uses browser devtools and local services<br/>
<strong>Remote:</strong> Uses headless tools and MCP providers
</div>
</div>
<!-- Current Configuration Display -->
<div style="
background: var(--vscode-sidebar);
border: 1px solid var(--vscode-border);
border-radius: 4px;
padding: 12px;
margin-bottom: 24px;
">
<div style="font-size: 11px; color: var(--vscode-text-dim); margin-bottom: 8px;">CURRENT STORYBOOK URL:</div>
<div style="
font-family: monospace;
font-size: 12px;
word-break: break-all;
color: var(--vscode-foreground);
" id="storybook-url-display">
${this.getStorybookUrlDisplay()}
</div>
</div>
<!-- Action Buttons -->
<div style="display: flex; gap: 8px;">
<button
id="save-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;
"
>
Save Settings
</button>
<button
id="reset-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;
"
>
Reset to Defaults
</button>
</div>
</div>
`;
}
setupEventListeners() {
const hostnameInput = this.querySelector('#hostname-input');
const portInput = this.querySelector('#port-input');
const setupTypeRadios = this.querySelectorAll('input[name="setup-type"]');
const saveBtn = this.querySelector('#save-btn');
const resetBtn = this.querySelector('#reset-btn');
// Update on input (but don't save immediately)
hostnameInput.addEventListener('change', () => {
this.adminStore.setHostname(hostnameInput.value);
});
portInput.addEventListener('change', () => {
const port = parseInt(portInput.value);
if (port > 0 && port <= 65535) {
this.adminStore.setPort(port);
}
});
setupTypeRadios.forEach(radio => {
radio.addEventListener('change', (e) => {
this.adminStore.setRemote(e.target.value === 'remote');
});
});
saveBtn.addEventListener('click', () => {
this.showNotification('Settings saved successfully!');
console.log('[AdminSettings] Settings saved:', this.adminStore.getState());
});
resetBtn.addEventListener('click', () => {
if (confirm('Reset all settings to defaults?')) {
this.adminStore.reset();
this.render();
this.setupEventListeners();
this.showNotification('Settings reset to defaults');
}
});
}
updateUI() {
const hostnameInput = this.querySelector('#hostname-input');
const portInput = this.querySelector('#port-input');
const setupTypeRadios = this.querySelectorAll('input[name="setup-type"]');
const urlDisplay = this.querySelector('#storybook-url-display');
if (hostnameInput) hostnameInput.value = this.state.hostname;
if (portInput) portInput.value = this.state.port;
setupTypeRadios.forEach(radio => {
radio.checked = (radio.value === 'remote') === this.state.isRemote;
});
if (urlDisplay) {
urlDisplay.textContent = this.getStorybookUrlDisplay();
}
}
getStorybookUrlDisplay() {
return this.adminStore.getStorybookUrl('default');
}
showNotification(message) {
const notification = document.createElement('div');
notification.textContent = message;
notification.style.cssText = `
position: fixed;
bottom: 20px;
right: 20px;
background: var(--vscode-notifications-background);
color: var(--vscode-foreground);
padding: 12px 16px;
border-radius: 4px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.2);
z-index: 1000;
animation: slideIn 0.3s ease;
`;
document.body.appendChild(notification);
setTimeout(() => {
notification.style.animation = 'slideOut 0.3s ease';
setTimeout(() => notification.remove(), 300);
}, 3000);
}
}
customElements.define('ds-admin-settings', AdminSettings);

View File

@@ -1,324 +0,0 @@
/**
* ds-project-list.js
* Project management component
* Create, edit, delete, and select projects
*/
import { useProjectStore } from '../../stores/project-store.js';
export default class ProjectList extends HTMLElement {
constructor() {
super();
this.projectStore = useProjectStore();
this.state = {
projects: this.projectStore.getProjects(),
currentProject: this.projectStore.getCurrentProject(),
showEditModal: false,
editingProject: null
};
}
connectedCallback() {
this.render();
this.setupEventListeners();
this.unsubscribe = this.projectStore.subscribe(() => {
this.state.projects = this.projectStore.getProjects();
this.state.currentProject = this.projectStore.getCurrentProject();
this.updateProjectList();
});
}
disconnectedCallback() {
if (this.unsubscribe) this.unsubscribe();
}
render() {
this.innerHTML = `
<div style="padding: 24px;">
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 24px;">
<h2 style="margin: 0; font-size: 20px;">Projects</h2>
<button id="create-project-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;
">
+ New Project
</button>
</div>
<!-- Projects List -->
<div id="projects-container" style="display: flex; flex-direction: column; gap: 12px;">
${this.renderProjectsList()}
</div>
</div>
<!-- Edit Modal -->
<div id="edit-modal" style="
display: ${this.state.showEditModal ? 'flex' : 'none'};
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.5);
align-items: center;
justify-content: center;
z-index: 1000;
">
<div style="
background: var(--vscode-editor-background);
border: 1px solid var(--vscode-border);
border-radius: 8px;
padding: 24px;
min-width: 400px;
max-width: 500px;
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.3);
">
<h3 id="modal-title" style="margin: 0 0 16px 0; font-size: 18px;">
${this.state.editingProject ? 'Edit Project' : 'New Project'}
</h3>
<!-- Project ID -->
<div style="margin-bottom: 16px;">
<label style="display: block; margin-bottom: 6px; font-size: 12px; font-weight: 500;">
Project ID (Jira Key)
</label>
<input
id="modal-project-id"
type="text"
${this.state.editingProject ? 'disabled' : ''}
value="${this.state.editingProject?.id || ''}"
placeholder="E.g., DSS-123"
style="
width: 100%;
padding: 8px 12px;
border: 1px solid var(--vscode-input-border);
background: var(--vscode-input-background);
color: var(--vscode-foreground);
border-radius: 4px;
font-family: monospace;
"
/>
<div style="font-size: 10px; color: var(--vscode-text-dim); margin-top: 4px;">
${this.state.editingProject ? 'Cannot change after creation' : 'Must match Jira project key'}
</div>
</div>
<!-- Project Name -->
<div style="margin-bottom: 16px;">
<label style="display: block; margin-bottom: 6px; font-size: 12px; font-weight: 500;">
Project Name
</label>
<input
id="modal-project-name"
type="text"
value="${this.state.editingProject?.name || ''}"
placeholder="My Design System"
style="
width: 100%;
padding: 8px 12px;
border: 1px solid var(--vscode-input-border);
background: var(--vscode-input-background);
color: var(--vscode-foreground);
border-radius: 4px;
"
/>
</div>
<!-- Skin Selection -->
<div style="margin-bottom: 24px;">
<label style="display: block; margin-bottom: 6px; font-size: 12px; font-weight: 500;">
Default Skin
</label>
<select
id="modal-skin-select"
style="
width: 100%;
padding: 8px 12px;
border: 1px solid var(--vscode-input-border);
background: var(--vscode-input-background);
color: var(--vscode-foreground);
border-radius: 4px;
"
>
<option value="default" ${this.state.editingProject?.skinSelected === 'default' ? 'selected' : ''}>default</option>
<option value="light" ${this.state.editingProject?.skinSelected === 'light' ? 'selected' : ''}>light</option>
<option value="dark" ${this.state.editingProject?.skinSelected === 'dark' ? 'selected' : ''}>dark</option>
</select>
</div>
<!-- Buttons -->
<div style="display: flex; gap: 8px; justify-content: flex-end;">
<button id="modal-cancel-btn" style="
padding: 8px 16px;
background: var(--vscode-button-secondaryBackground);
color: var(--vscode-button-secondaryForeground);
border: none;
border-radius: 4px;
cursor: pointer;
">
Cancel
</button>
<button id="modal-save-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;
">
${this.state.editingProject ? 'Update' : 'Create'}
</button>
</div>
</div>
</div>
`;
}
renderProjectsList() {
if (this.state.projects.length === 0) {
return '<div style="color: var(--vscode-text-dim); text-align: center; padding: 32px;">No projects yet. Create one to get started!</div>';
}
return this.state.projects.map(project => `
<div data-project-id="${project.id}" style="
display: flex;
align-items: center;
gap: 12px;
padding: 12px;
background: ${this.state.currentProject?.id === project.id ? 'var(--vscode-selection)' : 'var(--vscode-sidebar)'};
border: 1px solid var(--vscode-border);
border-radius: 4px;
cursor: pointer;
transition: background 0.2s;
" onmouseover="this.style.background='var(--vscode-selection)'" onmouseout="this.style.background='${this.state.currentProject?.id === project.id ? 'var(--vscode-selection)' : 'var(--vscode-sidebar)'}'">
<div style="flex: 1;">
<div style="font-weight: 500; margin-bottom: 4px;">${project.name}</div>
<div style="font-size: 11px; color: var(--vscode-text-dim);">
ID: ${project.id} | Skin: ${project.skinSelected}
</div>
</div>
<div style="display: flex; gap: 4px;">
<button class="edit-project-btn" data-project-id="${project.id}" style="
padding: 4px 8px;
background: var(--vscode-button-secondaryBackground);
color: var(--vscode-button-secondaryForeground);
border: none;
border-radius: 2px;
cursor: pointer;
font-size: 11px;
">
Edit
</button>
<button class="delete-project-btn" data-project-id="${project.id}" style="
padding: 4px 8px;
background: #c1272d;
color: white;
border: none;
border-radius: 2px;
cursor: pointer;
font-size: 11px;
">
Delete
</button>
</div>
</div>
`).join('');
}
setupEventListeners() {
// Create button
this.querySelector('#create-project-btn').addEventListener('click', () => {
this.state.editingProject = null;
this.state.showEditModal = true;
this.render();
this.setupEventListeners();
});
// Project selection
this.querySelectorAll('[data-project-id]').forEach(el => {
el.addEventListener('click', (e) => {
if (!e.target.closest('button')) {
const projectId = el.dataset.projectId;
this.projectStore.selectProject(projectId);
}
});
});
// Edit buttons
this.querySelectorAll('.edit-project-btn').forEach(btn => {
btn.addEventListener('click', (e) => {
e.stopPropagation();
const projectId = btn.dataset.projectId;
this.state.editingProject = this.projectStore.getProject(projectId);
this.state.showEditModal = true;
this.render();
this.setupEventListeners();
});
});
// Delete buttons
this.querySelectorAll('.delete-project-btn').forEach(btn => {
btn.addEventListener('click', (e) => {
e.stopPropagation();
const projectId = btn.dataset.projectId;
const project = this.projectStore.getProject(projectId);
if (confirm(`Delete project "${project.name}"? This cannot be undone.`)) {
this.projectStore.deleteProject(projectId);
}
});
});
// Modal buttons
const modal = this.querySelector('#edit-modal');
if (modal) {
this.querySelector('#modal-cancel-btn').addEventListener('click', () => {
this.state.showEditModal = false;
this.state.editingProject = null;
this.render();
});
this.querySelector('#modal-save-btn').addEventListener('click', () => {
const id = this.querySelector('#modal-project-id').value.trim();
const name = this.querySelector('#modal-project-name').value.trim();
const skin = this.querySelector('#modal-skin-select').value;
if (!id || !name) {
alert('Please fill in all fields');
return;
}
if (this.state.editingProject) {
// Update
this.projectStore.updateProject(this.state.editingProject.id, {
name,
skinSelected: skin
});
} else {
// Create
this.projectStore.createProject({ id, name, skinSelected: skin });
}
this.state.showEditModal = false;
this.state.editingProject = null;
this.render();
this.setupEventListeners();
});
}
}
updateProjectList() {
const container = this.querySelector('#projects-container');
if (container) {
container.innerHTML = this.renderProjectsList();
this.setupEventListeners();
}
}
}
customElements.define('ds-project-list', ProjectList);

View File

@@ -1,434 +0,0 @@
/**
* ds-user-settings.js
* User settings page component
* Manages user profile, preferences, integrations, and account settings
* MVP3: Full integration with backend API and user-store
*/
import { useUserStore } from '../../stores/user-store.js';
export default class DSUserSettings extends HTMLElement {
constructor() {
super();
this.userStore = useUserStore();
this.activeTab = 'profile';
this.isLoading = false;
this.formChanges = {};
}
connectedCallback() {
this.render();
this.setupEventListeners();
this.subscribeToUserStore();
}
subscribeToUserStore() {
this.unsubscribe = this.userStore.subscribe(() => {
this.updateUI();
});
}
render() {
const user = this.userStore.getCurrentUser();
const displayName = this.userStore.getDisplayName();
const avatar = this.userStore.getAvatar();
this.innerHTML = `
<div style="display: flex; flex-direction: column; height: 100%; background: var(--vscode-bg);">
<!-- Header -->
<div style="padding: 24px; border-bottom: 1px solid var(--vscode-border);">
<div style="display: flex; align-items: center; gap: 16px; margin-bottom: 24px;">
<img src="${avatar}" alt="Avatar" style="width: 64px; height: 64px; border-radius: 8px; background: var(--vscode-sidebar);" />
<div>
<h1 style="margin: 0 0 4px 0; font-size: 24px;">${displayName}</h1>
<p style="margin: 0; color: var(--vscode-text-dim); font-size: 12px;">${user?.email || 'Not logged in'}</p>
</div>
</div>
</div>
<!-- Tabs -->
<div style="display: flex; border-bottom: 1px solid var(--vscode-border); padding: 0 24px; gap: 24px; flex-shrink: 0;">
<button class="settings-tab" data-tab="profile" style="padding: 12px 0; border: none; background: transparent; color: var(--vscode-text); cursor: pointer; border-bottom: 2px solid transparent; font-size: 13px; transition: all 0.2s;">
👤 Profile
</button>
<button class="settings-tab" data-tab="preferences" style="padding: 12px 0; border: none; background: transparent; color: var(--vscode-text-dim); cursor: pointer; border-bottom: 2px solid transparent; font-size: 13px; transition: all 0.2s;">
⚙️ Preferences
</button>
<button class="settings-tab" data-tab="integrations" style="padding: 12px 0; border: none; background: transparent; color: var(--vscode-text-dim); cursor: pointer; border-bottom: 2px solid transparent; font-size: 13px; transition: all 0.2s;">
🔗 Integrations
</button>
<button class="settings-tab" data-tab="about" style="padding: 12px 0; border: none; background: transparent; color: var(--vscode-text-dim); cursor: pointer; border-bottom: 2px solid transparent; font-size: 13px; transition: all 0.2s;">
About
</button>
</div>
<!-- Content -->
<div style="flex: 1; overflow-y: auto; padding: 24px;">
<!-- Profile Tab -->
<div id="profile-tab" class="settings-content">
<div style="max-width: 600px;">
<h2 style="margin: 0 0 16px 0; font-size: 18px;">Profile Settings</h2>
<div style="margin-bottom: 16px;">
<label style="display: block; font-size: 12px; font-weight: 500; margin-bottom: 4px;">Full Name</label>
<input id="profile-name" type="text" value="${user?.name || ''}" placeholder="Your full name" style="width: 100%; padding: 8px 12px; background: var(--vscode-input-background); border: 1px solid var(--vscode-border); color: var(--vscode-text); border-radius: 4px; font-size: 13px;" />
</div>
<div style="margin-bottom: 16px;">
<label style="display: block; font-size: 12px; font-weight: 500; margin-bottom: 4px;">Email</label>
<input id="profile-email" type="email" value="${user?.email || ''}" placeholder="your@email.com" style="width: 100%; padding: 8px 12px; background: var(--vscode-input-background); border: 1px solid var(--vscode-border); color: var(--vscode-text); border-radius: 4px; font-size: 13px;" />
</div>
<div style="margin-bottom: 16px;">
<label style="display: block; font-size: 12px; font-weight: 500; margin-bottom: 4px;">Role</label>
<div style="padding: 8px 12px; background: var(--vscode-sidebar); border: 1px solid var(--vscode-border); color: var(--vscode-text-dim); border-radius: 4px; font-size: 13px;">
${user?.role || 'User'} <span style="color: var(--vscode-text-dim); font-size: 11px;">(Read-only)</span>
</div>
</div>
<div style="margin-bottom: 24px;">
<label style="display: block; font-size: 12px; font-weight: 500; margin-bottom: 4px;">Bio</label>
<textarea id="profile-bio" placeholder="Tell us about yourself..." style="width: 100%; padding: 8px 12px; background: var(--vscode-input-background); border: 1px solid var(--vscode-border); color: var(--vscode-text); border-radius: 4px; font-size: 13px; min-height: 80px; resize: vertical;" >${user?.bio || ''}</textarea>
</div>
<div style="display: flex; gap: 8px;">
<button id="save-profile-btn" style="padding: 8px 16px; background: var(--vscode-button-background); color: var(--vscode-button-foreground); border: none; border-radius: 4px; cursor: pointer; font-size: 12px; font-weight: 500;">
Save Changes
</button>
<button id="change-password-btn" style="padding: 8px 16px; background: var(--vscode-button-secondaryBackground); color: var(--vscode-button-secondaryForeground); border: none; border-radius: 4px; cursor: pointer; font-size: 12px; font-weight: 500;">
Change Password
</button>
</div>
</div>
</div>
<!-- Preferences Tab -->
<div id="preferences-tab" class="settings-content" style="display: none;">
<div style="max-width: 600px;">
<h2 style="margin: 0 0 16px 0; font-size: 18px;">Preferences</h2>
<h3 style="margin: 0 0 12px 0; font-size: 14px; color: var(--vscode-text-dim);">Theme</h3>
<div style="margin-bottom: 24px;">
<label style="display: flex; align-items: center; gap: 8px; margin-bottom: 8px; cursor: pointer;">
<input type="radio" name="theme" value="dark" checked />
<span style="font-size: 12px;">Dark</span>
</label>
<label style="display: flex; align-items: center; gap: 8px; cursor: pointer;">
<input type="radio" name="theme" value="light" />
<span style="font-size: 12px;">Light</span>
</label>
</div>
<h3 style="margin: 0 0 12px 0; font-size: 14px; color: var(--vscode-text-dim);">Language</h3>
<div style="margin-bottom: 24px;">
<select id="pref-language" style="padding: 8px 12px; background: var(--vscode-input-background); border: 1px solid var(--vscode-border); color: var(--vscode-text); border-radius: 4px; font-size: 12px; cursor: pointer;">
<option value="en">English</option>
<option value="es">Español</option>
<option value="fr">Français</option>
<option value="de">Deutsch</option>
<option value="ja">日本語</option>
</select>
</div>
<h3 style="margin: 0 0 12px 0; font-size: 14px; color: var(--vscode-text-dim);">Notifications</h3>
<div style="margin-bottom: 24px;">
<label style="display: flex; align-items: center; gap: 8px; margin-bottom: 8px; cursor: pointer;">
<input id="pref-notifications" type="checkbox" checked />
<span style="font-size: 12px;">Enable notifications</span>
</label>
<label style="display: flex; align-items: center; gap: 8px; margin-bottom: 8px; cursor: pointer;">
<input id="pref-email-notifications" type="checkbox" checked />
<span style="font-size: 12px;">Email notifications</span>
</label>
<label style="display: flex; align-items: center; gap: 8px; cursor: pointer;">
<input id="pref-desktop-notifications" type="checkbox" checked />
<span style="font-size: 12px;">Desktop notifications</span>
</label>
</div>
<button id="save-preferences-btn" style="padding: 8px 16px; background: var(--vscode-button-background); color: var(--vscode-button-foreground); border: none; border-radius: 4px; cursor: pointer; font-size: 12px; font-weight: 500;">
Save Preferences
</button>
</div>
</div>
<!-- Integrations Tab -->
<div id="integrations-tab" class="settings-content" style="display: none;">
<div style="max-width: 600px;">
<h2 style="margin: 0 0 16px 0; font-size: 18px;">Integrations</h2>
<!-- Figma Integration -->
<div style="background: var(--vscode-sidebar); border: 1px solid var(--vscode-border); border-radius: 4px; padding: 16px; margin-bottom: 12px;">
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 12px;">
<div>
<h3 style="margin: 0 0 4px 0; font-size: 13px; font-weight: 500;">🎨 Figma</h3>
<p style="margin: 0; font-size: 11px; color: var(--vscode-text-dim);">Connect your Figma account for design token extraction</p>
</div>
<span id="figma-status" class="integration-status" style="font-size: 11px; padding: 4px 8px; background: #4CAF50; color: white; border-radius: 3px; display: none;">Connected</span>
</div>
<div style="display: flex; gap: 8px;">
<input id="figma-api-key" type="password" placeholder="Enter Figma API key" style="flex: 1; padding: 6px 10px; background: var(--vscode-input-background); border: 1px solid var(--vscode-border); color: var(--vscode-text); border-radius: 3px; font-size: 11px;" />
<button class="integration-save-btn" data-service="figma" style="padding: 6px 12px; background: var(--vscode-button-background); color: var(--vscode-button-foreground); border: none; border-radius: 3px; cursor: pointer; font-size: 11px;">Save</button>
</div>
</div>
<!-- GitHub Integration -->
<div style="background: var(--vscode-sidebar); border: 1px solid var(--vscode-border); border-radius: 4px; padding: 16px; margin-bottom: 12px;">
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 12px;">
<div>
<h3 style="margin: 0 0 4px 0; font-size: 13px; font-weight: 500;">🐙 GitHub</h3>
<p style="margin: 0; font-size: 11px; color: var(--vscode-text-dim);">Connect GitHub for component library integration</p>
</div>
<span id="github-status" class="integration-status" style="font-size: 11px; padding: 4px 8px; background: #666; color: white; border-radius: 3px; display: none;">Connected</span>
</div>
<div style="display: flex; gap: 8px;">
<input id="github-api-key" type="password" placeholder="Enter GitHub personal access token" style="flex: 1; padding: 6px 10px; background: var(--vscode-input-background); border: 1px solid var(--vscode-border); color: var(--vscode-text); border-radius: 3px; font-size: 11px;" />
<button class="integration-save-btn" data-service="github" style="padding: 6px 12px; background: var(--vscode-button-background); color: var(--vscode-button-foreground); border: none; border-radius: 3px; cursor: pointer; font-size: 11px;">Save</button>
</div>
</div>
<!-- Jira Integration -->
<div style="background: var(--vscode-sidebar); border: 1px solid var(--vscode-border); border-radius: 4px; padding: 16px; margin-bottom: 12px;">
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 12px;">
<div>
<h3 style="margin: 0 0 4px 0; font-size: 13px; font-weight: 500;">📋 Jira</h3>
<p style="margin: 0; font-size: 11px; color: var(--vscode-text-dim);">Connect Jira for issue tracking integration</p>
</div>
<span id="jira-status" class="integration-status" style="font-size: 11px; padding: 4px 8px; background: #666; color: white; border-radius: 3px; display: none;">Connected</span>
</div>
<div style="display: flex; gap: 8px; margin-bottom: 8px;">
<input id="jira-api-key" type="password" placeholder="Enter Jira API token" style="flex: 1; padding: 6px 10px; background: var(--vscode-input-background); border: 1px solid var(--vscode-border); color: var(--vscode-text); border-radius: 3px; font-size: 11px;" />
<button class="integration-save-btn" data-service="jira" style="padding: 6px 12px; background: var(--vscode-button-background); color: var(--vscode-button-foreground); border: none; border-radius: 3px; cursor: pointer; font-size: 11px;">Save</button>
</div>
<input id="jira-project-key" type="text" placeholder="Jira project key (optional)" style="width: 100%; padding: 6px 10px; background: var(--vscode-input-background); border: 1px solid var(--vscode-border); color: var(--vscode-text); border-radius: 3px; font-size: 11px;" />
</div>
<!-- Slack Integration -->
<div style="background: var(--vscode-sidebar); border: 1px solid var(--vscode-border); border-radius: 4px; padding: 16px; margin-bottom: 12px;">
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 12px;">
<div>
<h3 style="margin: 0 0 4px 0; font-size: 13px; font-weight: 500;">💬 Slack</h3>
<p style="margin: 0; font-size: 11px; color: var(--vscode-text-dim);">Connect Slack for team notifications</p>
</div>
<span id="slack-status" class="integration-status" style="font-size: 11px; padding: 4px 8px; background: #666; color: white; border-radius: 3px; display: none;">Connected</span>
</div>
<div style="display: flex; gap: 8px;">
<input id="slack-webhook" type="password" placeholder="Enter Slack webhook URL" style="flex: 1; padding: 6px 10px; background: var(--vscode-input-background); border: 1px solid var(--vscode-border); color: var(--vscode-text); border-radius: 3px; font-size: 11px;" />
<button class="integration-save-btn" data-service="slack" style="padding: 6px 12px; background: var(--vscode-button-background); color: var(--vscode-button-foreground); border: none; border-radius: 3px; cursor: pointer; font-size: 11px;">Save</button>
</div>
</div>
</div>
</div>
<!-- About Tab -->
<div id="about-tab" class="settings-content" style="display: none;">
<div style="max-width: 600px;">
<h2 style="margin: 0 0 16px 0; font-size: 18px;">About</h2>
<div style="background: var(--vscode-sidebar); border: 1px solid var(--vscode-border); border-radius: 4px; padding: 16px; margin-bottom: 16px;">
<h3 style="margin: 0 0 12px 0; font-size: 14px;">Design System Server</h3>
<p style="margin: 0 0 8px 0; font-size: 12px; color: var(--vscode-text-dim);">Version: 3.0.0 (MVP3)</p>
<p style="margin: 0 0 8px 0; font-size: 12px;">Advanced design system management platform with AI assistance, design tokens, and multi-team collaboration.</p>
<p style="margin: 0; font-size: 11px; color: var(--vscode-text-dim);">© 2024 Design System Server. All rights reserved.</p>
</div>
<h3 style="margin: 16px 0 8px 0; font-size: 14px;">Features</h3>
<ul style="margin: 0; padding-left: 20px; font-size: 12px;">
<li>Design token management and synchronization</li>
<li>Figma integration with automated token extraction</li>
<li>Multi-team collaboration workspace</li>
<li>AI-powered design system analysis</li>
<li>Storybook integration and component documentation</li>
<li>GitHub and Jira integration</li>
</ul>
<div style="margin-top: 24px;">
<h3 style="margin: 0 0 8px 0; font-size: 14px;">Account Actions</h3>
<button id="logout-btn" style="padding: 8px 16px; background: #F44336; color: white; border: none; border-radius: 4px; cursor: pointer; font-size: 12px; font-weight: 500;">
🚪 Logout
</button>
</div>
</div>
</div>
</div>
</div>
`;
}
setupEventListeners() {
// Tab switching
this.querySelectorAll('.settings-tab').forEach(btn => {
btn.addEventListener('click', (e) => {
this.switchTab(e.target.closest('button').dataset.tab);
});
});
// Profile tab
const saveProfileBtn = this.querySelector('#save-profile-btn');
if (saveProfileBtn) {
saveProfileBtn.addEventListener('click', () => this.saveProfile());
}
// Preferences tab
const savePreferencesBtn = this.querySelector('#save-preferences-btn');
if (savePreferencesBtn) {
savePreferencesBtn.addEventListener('click', () => this.savePreferences());
}
// Integration save buttons
this.querySelectorAll('.integration-save-btn').forEach(btn => {
btn.addEventListener('click', (e) => {
const service = e.target.dataset.service;
const apiKeyInput = this.querySelector(`#${service}-api-key`) || this.querySelector(`#${service}-webhook`);
const apiKey = apiKeyInput?.value || '';
this.saveIntegration(service, apiKey);
});
});
// Logout button
const logoutBtn = this.querySelector('#logout-btn');
if (logoutBtn) {
logoutBtn.addEventListener('click', () => this.logout());
}
// Change password button
const changePasswordBtn = this.querySelector('#change-password-btn');
if (changePasswordBtn) {
changePasswordBtn.addEventListener('click', () => this.showChangePasswordDialog());
}
}
switchTab(tabName) {
this.activeTab = tabName;
// Hide all tabs
this.querySelectorAll('.settings-content').forEach(tab => {
tab.style.display = 'none';
});
// Show selected tab
const selectedTab = this.querySelector(`#${tabName}-tab`);
if (selectedTab) {
selectedTab.style.display = 'block';
}
// Update tab styling
this.querySelectorAll('.settings-tab').forEach(btn => {
const isActive = btn.dataset.tab === tabName;
btn.style.borderBottomColor = isActive ? 'var(--vscode-accent)' : 'transparent';
btn.style.color = isActive ? 'var(--vscode-text)' : 'var(--vscode-text-dim)';
});
}
async saveProfile() {
const name = this.querySelector('#profile-name')?.value || '';
const email = this.querySelector('#profile-email')?.value || '';
const bio = this.querySelector('#profile-bio')?.value || '';
try {
await this.userStore.updateProfile({ name, email, bio });
this.showNotification('Profile saved successfully', 'success');
} catch (error) {
this.showNotification('Failed to save profile', 'error');
console.error(error);
}
}
savePreferences() {
const theme = this.querySelector('input[name="theme"]:checked')?.value || 'dark';
const language = this.querySelector('#pref-language')?.value || 'en';
const notifications = this.querySelector('#pref-notifications')?.checked || false;
this.userStore.updatePreferences({
theme,
language,
notifications: {
enabled: notifications,
email: this.querySelector('#pref-email-notifications')?.checked || false,
desktop: this.querySelector('#pref-desktop-notifications')?.checked || false
}
});
this.showNotification('Preferences saved', 'success');
}
saveIntegration(service, apiKey) {
if (!apiKey) {
this.userStore.removeIntegration(service);
this.showNotification(`${service} integration removed`, 'success');
} else {
const metadata = {};
if (service === 'jira') {
metadata.projectKey = this.querySelector('#jira-project-key')?.value || '';
}
this.userStore.setIntegration(service, apiKey, metadata);
this.showNotification(`${service} integration saved`, 'success');
}
this.updateIntegrationStatus();
}
updateIntegrationStatus() {
const integrations = this.userStore.getIntegrations();
['figma', 'github', 'jira', 'slack'].forEach(service => {
const status = this.querySelector(`#${service}-status`);
if (status) {
if (integrations[service]?.enabled) {
status.style.display = 'inline-block';
status.style.background = '#4CAF50';
status.textContent = 'Connected';
} else {
status.style.display = 'none';
}
}
});
}
updateUI() {
// Update display when user state changes
const user = this.userStore.getCurrentUser();
const displayName = this.userStore.getDisplayName();
// Re-render component
this.render();
this.setupEventListeners();
}
showChangePasswordDialog() {
// Placeholder for password change dialog
// In a real implementation, this would show a modal dialog
alert('Change password functionality would be implemented here.\n\nIn production, this would show a modal with current password and new password fields.');
}
showNotification(message, type = 'info') {
const notificationEl = document.createElement('div');
notificationEl.style.cssText = `
position: fixed;
bottom: 24px;
right: 24px;
padding: 12px 16px;
background: ${type === 'success' ? '#4CAF50' : type === 'error' ? '#F44336' : '#0066CC'};
color: white;
border-radius: 4px;
font-size: 12px;
font-weight: 500;
z-index: 1000;
animation: slideInUp 0.3s ease-out;
`;
notificationEl.textContent = message;
document.body.appendChild(notificationEl);
setTimeout(() => {
notificationEl.style.animation = 'slideOutDown 0.3s ease-in';
setTimeout(() => notificationEl.remove(), 300);
}, 3000);
}
disconnectedCallback() {
if (this.unsubscribe) {
this.unsubscribe();
}
}
}
customElements.define('ds-user-settings', DSUserSettings);

View File

@@ -1,241 +0,0 @@
/**
* ds-base-tool.js
* Base class for all DSS tool components
*
* Enforces DSS coding standards:
* - Shadow DOM encapsulation
* - Automatic event listener cleanup via AbortController
* - Constructable Stylesheets support
* - Standardized lifecycle methods
* - Logger utility integration
*
* Reference: .knowledge/dss-coding-standards.json
*/
import { logger } from '../../utils/logger.js';
/**
* Base class for DSS tool components
* All tool components should extend this class to ensure compliance with DSS standards
*/
export default class DSBaseTool extends HTMLElement {
constructor() {
super();
// WC-001: Shadow DOM Required
this.attachShadow({ mode: 'open' });
// EVENT-003: Use AbortController for cleanup
this._abortController = new AbortController();
// Track component state
this._isConnected = false;
logger.debug(`[${this.constructor.name}] Constructor initialized`);
}
/**
* Standard Web Component lifecycle: called when element is added to DOM
*/
connectedCallback() {
this._isConnected = true;
logger.debug(`[${this.constructor.name}] Connected to DOM`);
// Render the component
this.render();
// Setup event listeners after render
this.setupEventListeners();
}
/**
* Standard Web Component lifecycle: called when element is removed from DOM
* Automatically cleans up all event listeners via AbortController
*/
disconnectedCallback() {
this._isConnected = false;
// EVENT-003: Abort all event listeners
this._abortController.abort();
// Create new controller for potential re-connection
this._abortController = new AbortController();
logger.debug(`[${this.constructor.name}] Disconnected from DOM, listeners cleaned up`);
}
/**
* Centralized event binding with automatic cleanup
* @param {EventTarget} target - Element to attach listener to
* @param {string} type - Event type (e.g., 'click', 'mouseover')
* @param {Function} handler - Event handler function
* @param {Object} options - Additional addEventListener options
*/
bindEvent(target, type, handler, options = {}) {
if (!target || typeof handler !== 'function') {
logger.warn(`[${this.constructor.name}] Invalid event binding attempt`, { target, type, handler });
return;
}
// Add AbortController signal to options
const eventOptions = {
...options,
signal: this._abortController.signal
};
target.addEventListener(type, handler, eventOptions);
logger.debug(`[${this.constructor.name}] Event bound: ${type} on`, target);
}
/**
* Event delegation helper for handling multiple elements with data-action attributes
* @param {string} selector - CSS selector for the container element
* @param {string} eventType - Event type to listen for
* @param {Function} handler - Handler function that receives (action, event)
*/
delegateEvents(selector, eventType, handler) {
const container = this.shadowRoot.querySelector(selector);
if (!container) {
logger.warn(`[${this.constructor.name}] Event delegation container not found: ${selector}`);
return;
}
this.bindEvent(container, eventType, (e) => {
// Find element with data-action attribute
const target = e.target.closest('[data-action]');
if (target) {
const action = target.dataset.action;
handler(action, e, target);
}
});
logger.debug(`[${this.constructor.name}] Event delegation setup for ${eventType} on ${selector}`);
}
/**
* Inject CSS styles using Constructable Stylesheets
* STYLE-002: Use Constructable Stylesheets for shared styles
* @param {string} cssString - CSS string to inject
*/
adoptStyles(cssString) {
try {
const sheet = new CSSStyleSheet();
sheet.replaceSync(cssString);
// Append to existing stylesheets
this.shadowRoot.adoptedStyleSheets = [
...this.shadowRoot.adoptedStyleSheets,
sheet
];
logger.debug(`[${this.constructor.name}] Styles adopted (${cssString.length} bytes)`);
} catch (error) {
logger.error(`[${this.constructor.name}] Failed to adopt styles:`, error);
}
}
/**
* Set multiple attributes at once
* @param {Object} attrs - Object with attribute key-value pairs
*/
setAttributes(attrs) {
Object.entries(attrs).forEach(([key, value]) => {
if (value !== null && value !== undefined) {
this.setAttribute(key, value);
}
});
}
/**
* Get attribute with fallback value
* @param {string} name - Attribute name
* @param {*} defaultValue - Default value if attribute doesn't exist
* @returns {string|*} Attribute value or default
*/
getAttr(name, defaultValue = null) {
return this.hasAttribute(name) ? this.getAttribute(name) : defaultValue;
}
/**
* Render method - MUST be implemented by subclasses
* Should set shadowRoot.innerHTML with component template
*/
render() {
throw new Error(`${this.constructor.name} must implement render() method`);
}
/**
* Setup event listeners - should be implemented by subclasses
* Use this.bindEvent() or this.delegateEvents() for automatic cleanup
*/
setupEventListeners() {
// Override in subclass if needed
logger.debug(`[${this.constructor.name}] setupEventListeners() not implemented (optional)`);
}
/**
* Trigger re-render (useful for state changes)
*/
rerender() {
if (this._isConnected) {
// Abort existing listeners before re-render
this._abortController.abort();
this._abortController = new AbortController();
// Re-render and re-setup listeners
this.render();
this.setupEventListeners();
logger.debug(`[${this.constructor.name}] Component re-rendered`);
}
}
/**
* Helper: Query single element in shadow DOM
* @param {string} selector - CSS selector
* @returns {Element|null}
*/
$(selector) {
return this.shadowRoot.querySelector(selector);
}
/**
* Helper: Query multiple elements in shadow DOM
* @param {string} selector - CSS selector
* @returns {NodeList}
*/
$$(selector) {
return this.shadowRoot.querySelectorAll(selector);
}
/**
* Helper: Escape HTML to prevent XSS
* SECURITY-001: Sanitize user input
* @param {string} str - String to escape
* @returns {string} Escaped string
*/
escapeHtml(str) {
const div = document.createElement('div');
div.textContent = str;
return div.innerHTML;
}
/**
* Helper: Dispatch custom event
* @param {string} eventName - Event name
* @param {*} detail - Event detail payload
* @param {Object} options - Event options
*/
emit(eventName, detail = null, options = {}) {
const event = new CustomEvent(eventName, {
detail,
bubbles: true,
composed: true, // Cross shadow DOM boundary
...options
});
this.dispatchEvent(event);
logger.debug(`[${this.constructor.name}] Event emitted: ${eventName}`, detail);
}
}

View File

@@ -1,43 +0,0 @@
/**
* admin-ui/js/components/ds-action-bar.js
* A simple web component to structure page-level actions.
*/
class DsActionBar extends HTMLElement {
constructor() {
super();
this.attachShadow({ mode: 'open' });
}
connectedCallback() {
this.render();
}
render() {
this.shadowRoot.innerHTML = `
<style>
:host {
display: flex;
justify-content: space-between;
align-items: center;
gap: var(--space-4);
padding: var(--space-4) 0;
border-bottom: 1px solid var(--border);
margin-bottom: var(--space-6);
}
.secondary, .primary {
display: flex;
align-items: center;
gap: var(--space-2);
}
</style>
<div class="secondary">
<slot></slot>
</div>
<div class="primary">
<slot name="primary"></slot>
</div>
`;
}
}
customElements.define('ds-action-bar', DsActionBar);

View File

@@ -1,80 +0,0 @@
/**
* DS Badge - Web Component
*
* Usage:
* <ds-badge>Default</ds-badge>
* <ds-badge variant="success">Active</ds-badge>
* <ds-badge variant="warning" dot>Pending</ds-badge>
*
* Attributes:
* - variant: default | secondary | outline | destructive | success | warning
* - dot: boolean (shows status dot)
*/
class DsBadge extends HTMLElement {
static get observedAttributes() {
return ['variant', 'dot'];
}
constructor() {
super();
this.attachShadow({ mode: 'open' });
}
connectedCallback() {
this.render();
}
disconnectedCallback() {
// Cleanup for consistency with other components
// This badge has no event listeners, but disconnectedCallback
// is present for future extensibility and pattern consistency
}
attributeChangedCallback() {
if (this.shadowRoot.innerHTML) {
this.render();
}
}
get variant() {
return this.getAttribute('variant') || 'default';
}
get dot() {
return this.hasAttribute('dot');
}
getVariantClass() {
const variants = {
default: 'ds-badge--default',
secondary: 'ds-badge--secondary',
outline: 'ds-badge--outline',
destructive: 'ds-badge--destructive',
success: 'ds-badge--success',
warning: 'ds-badge--warning'
};
return variants[this.variant] || variants.default;
}
render() {
const variantClass = this.getVariantClass();
this.shadowRoot.innerHTML = `
<style>
:host {
display: inline-block;
}
</style>
<span class="ds-badge ${variantClass}">
${this.dot ? '<span class="ds-badge__dot"></span>' : ''}
<slot></slot>
</span>
`;
}
}
customElements.define('ds-badge', DsBadge);
export default DsBadge;

View File

@@ -1,177 +0,0 @@
/**
* DS Card - Web Component
*
* Usage:
* <ds-card>
* <ds-card-header>
* <ds-card-title>Title</ds-card-title>
* <ds-card-description>Description</ds-card-description>
* </ds-card-header>
* <ds-card-content>Content here</ds-card-content>
* <ds-card-footer>Footer actions</ds-card-footer>
* </ds-card>
*
* Attributes:
* - interactive: boolean (adds hover effect)
*/
class DsCard extends HTMLElement {
static get observedAttributes() {
return ['interactive'];
}
constructor() {
super();
this.attachShadow({ mode: 'open' });
}
connectedCallback() {
this.render();
}
disconnectedCallback() {
// Cleanup for consistency with other components
// This card has no event listeners, but disconnectedCallback
// is present for future extensibility and pattern consistency
}
attributeChangedCallback() {
if (this.shadowRoot.innerHTML) {
this.render();
}
}
get interactive() {
return this.hasAttribute('interactive');
}
render() {
const interactiveClass = this.interactive ? 'ds-card--interactive' : '';
this.shadowRoot.innerHTML = `
<style>
:host {
display: block;
}
</style>
<div class="ds-card ${interactiveClass}">
<slot></slot>
</div>
`;
}
}
class DsCardHeader extends HTMLElement {
constructor() {
super();
this.attachShadow({ mode: 'open' });
}
connectedCallback() {
this.shadowRoot.innerHTML = `
<style>
</style>
<div class="ds-card__header">
<slot></slot>
</div>
`;
}
disconnectedCallback() {
// Cleanup for consistency with other components
}
}
class DsCardTitle extends HTMLElement {
constructor() {
super();
this.attachShadow({ mode: 'open' });
}
connectedCallback() {
this.shadowRoot.innerHTML = `
<style>
</style>
<h3 class="ds-card__title">
<slot></slot>
</h3>
`;
}
disconnectedCallback() {
// Cleanup for consistency with other components
}
}
class DsCardDescription extends HTMLElement {
constructor() {
super();
this.attachShadow({ mode: 'open' });
}
connectedCallback() {
this.shadowRoot.innerHTML = `
<style>
</style>
<p class="ds-card__description">
<slot></slot>
</p>
`;
}
disconnectedCallback() {
// Cleanup for consistency with other components
}
}
class DsCardContent extends HTMLElement {
constructor() {
super();
this.attachShadow({ mode: 'open' });
}
connectedCallback() {
this.shadowRoot.innerHTML = `
<style>
</style>
<div class="ds-card__content">
<slot></slot>
</div>
`;
}
disconnectedCallback() {
// Cleanup for consistency with other components
}
}
class DsCardFooter extends HTMLElement {
constructor() {
super();
this.attachShadow({ mode: 'open' });
}
connectedCallback() {
this.shadowRoot.innerHTML = `
<style>
</style>
<div class="ds-card__footer">
<slot></slot>
</div>
`;
}
disconnectedCallback() {
// Cleanup for consistency with other components
}
}
customElements.define('ds-card', DsCard);
customElements.define('ds-card-header', DsCardHeader);
customElements.define('ds-card-title', DsCardTitle);
customElements.define('ds-card-description', DsCardDescription);
customElements.define('ds-card-content', DsCardContent);
customElements.define('ds-card-footer', DsCardFooter);
export { DsCard, DsCardHeader, DsCardTitle, DsCardDescription, DsCardContent, DsCardFooter };

View File

@@ -1,417 +0,0 @@
/**
* DsComponentBase - Base class for all design system components
*
* Provides standardized:
* - Component lifecycle (connectedCallback, disconnectedCallback, attributeChangedCallback)
* - Standard attributes (variant, size, disabled, loading, aria-* attributes)
* - Standard methods (focus(), blur())
* - Theme change handling
* - Accessibility features (WCAG 2.1 AA)
* - Event emission patterns (ds-* namespaced events)
*
* All Web Components should extend this class to ensure API consistency.
*
* Usage:
* class DsButton extends DsComponentBase {
* static get observedAttributes() {
* return [...super.observedAttributes(), 'type'];
* }
* }
*/
import StylesheetManager from '../core/stylesheet-manager.js';
export class DsComponentBase extends HTMLElement {
/**
* Standard observed attributes all components should support
* Subclasses should extend this list with component-specific attributes
*/
static get observedAttributes() {
return [
// State management
'disabled',
'loading',
// Accessibility
'aria-label',
'aria-disabled',
'aria-expanded',
'aria-hidden',
'aria-pressed',
'aria-selected',
'aria-invalid',
'aria-describedby',
'aria-labelledby',
// Focus management
'tabindex'
];
}
/**
* Initialize component
* Subclasses should call super.constructor()
*/
constructor() {
super();
this.attachShadow({ mode: 'open' });
// Initialize standard properties
this._disabled = false;
this._loading = false;
this._initialized = false;
this._cleanup = [];
this._themeObserver = null;
this._resizeObserver = null;
}
/**
* Called when component is inserted into DOM
* Loads stylesheets, syncs attributes, and renders
*/
async connectedCallback() {
try {
// Attach stylesheets
await StylesheetManager.attachStyles(this.shadowRoot);
// Sync HTML attributes to JavaScript properties
this._syncAttributesToProperties();
// Initialize theme observer for dark/light mode changes
this._initializeThemeObserver();
// Allow subclass to setup event listeners
this.setupEventListeners?.();
// Initial render
this._initialized = true;
this.render?.();
// Emit connected event for testing/debugging
this.emit('ds-component-connected', {
component: this.constructor.name
});
} catch (error) {
console.error(`[${this.constructor.name}] Error in connectedCallback:`, error);
this.emit('ds-component-error', { error: error.message });
}
}
/**
* Called when component is removed from DOM
* Cleanup event listeners and observers
*/
disconnectedCallback() {
// Allow subclass to cleanup
this.cleanupEventListeners?.();
// Remove theme observer
if (this._themeObserver) {
window.removeEventListener('theme-changed', this._themeObserver);
this._themeObserver = null;
}
// Disconnect resize observer if present
if (this._resizeObserver) {
this._resizeObserver.disconnect();
this._resizeObserver = null;
}
// Cleanup all tracked listeners
this._cleanup.forEach(({ element, event, handler }) => {
element.removeEventListener(event, handler);
});
this._cleanup = [];
}
/**
* Called when observed attributes change
* Subclasses can override but should call super.attributeChangedCallback()
*/
attributeChangedCallback(name, oldValue, newValue) {
if (!this._initialized || oldValue === newValue) return;
// Handle standard attributes
switch (name) {
case 'disabled':
this._disabled = newValue !== null;
this._updateAccessibility();
break;
case 'loading':
this._loading = newValue !== null;
break;
case 'aria-label':
case 'aria-disabled':
case 'aria-expanded':
case 'aria-hidden':
case 'aria-pressed':
case 'aria-selected':
case 'aria-invalid':
this._updateAccessibility();
break;
case 'tabindex':
// Update tabindex if changed
this.setAttribute('tabindex', newValue || '0');
break;
}
// Re-render component
this.render?.();
}
/**
* Sync HTML attributes to JavaScript properties
* @private
*/
_syncAttributesToProperties() {
this._disabled = this.hasAttribute('disabled');
this._loading = this.hasAttribute('loading');
// Ensure accessible tabindex
if (!this.hasAttribute('tabindex')) {
this.setAttribute('tabindex', this._disabled ? '-1' : '0');
} else if (this._disabled && this.getAttribute('tabindex') !== '-1') {
this.setAttribute('tabindex', '-1');
}
}
/**
* Initialize theme observer to listen for dark/light mode changes
* @private
*/
_initializeThemeObserver() {
this._themeObserver = () => {
// Re-render when theme changes
this.render?.();
};
window.addEventListener('theme-changed', this._themeObserver);
}
/**
* Update accessibility attributes based on component state
* @private
*/
_updateAccessibility() {
// Update aria-disabled to match disabled state
this.setAttribute('aria-disabled', this._disabled);
// Ensure proper tab order when disabled
if (this._disabled) {
this.setAttribute('tabindex', '-1');
} else if (this.getAttribute('tabindex') === '-1') {
this.setAttribute('tabindex', '0');
}
}
/**
* Standard properties with getters/setters
*/
get disabled() { return this._disabled; }
set disabled(value) {
this._disabled = !!value;
value ? this.setAttribute('disabled', '') : this.removeAttribute('disabled');
}
get loading() { return this._loading; }
set loading(value) {
this._loading = !!value;
value ? this.setAttribute('loading', '') : this.removeAttribute('loading');
}
get ariaLabel() { return this.getAttribute('aria-label'); }
set ariaLabel(value) {
value ? this.setAttribute('aria-label', value) : this.removeAttribute('aria-label');
}
get ariaDescribedBy() { return this.getAttribute('aria-describedby'); }
set ariaDescribedBy(value) {
value ? this.setAttribute('aria-describedby', value) : this.removeAttribute('aria-describedby');
}
/**
* Standard methods for focus management
*/
focus(options) {
// Find first focusable element in shadow DOM
const focusable = this.shadowRoot.querySelector('button, input, [tabindex]');
if (focusable) {
focusable.focus(options);
}
}
blur() {
const focused = this.shadowRoot.activeElement;
if (focused && typeof focused.blur === 'function') {
focused.blur();
}
}
/**
* Emit custom event (ds-* namespaced)
* @param {string} eventName - Event name (without 'ds-' prefix)
* @param {object} detail - Event detail object
* @returns {boolean} Whether event was not prevented
*/
emit(eventName, detail = {}) {
const event = new CustomEvent(`ds-${eventName}`, {
detail,
composed: true, // Bubble out of shadow DOM
bubbles: true, // Standard bubbling
cancelable: true // Allow preventDefault()
});
return this.dispatchEvent(event);
}
/**
* Add event listener with automatic cleanup
* Listener is automatically removed in disconnectedCallback()
* @param {HTMLElement} element - Element to listen on
* @param {string} event - Event name
* @param {Function} handler - Event handler
* @param {object} [options] - Event listener options
*/
addEventListener(element, event, handler, options = false) {
element.addEventListener(event, handler, options);
this._cleanup.push({ element, event, handler });
}
/**
* Render method stub - override in subclass
* Called on initialization and on attribute changes
*/
render() {
// Override in subclass
}
/**
* Setup event listeners - override in subclass
* Called in connectedCallback after render
*/
setupEventListeners() {
// Override in subclass
}
/**
* Cleanup event listeners - override in subclass
* Called in disconnectedCallback
*/
cleanupEventListeners() {
// Override in subclass
}
/**
* Get computed CSS variable value
* @param {string} varName - CSS variable name (with or without --)
* @returns {string} CSS variable value
*/
getCSSVariable(varName) {
const name = varName.startsWith('--') ? varName : `--${varName}`;
return getComputedStyle(document.documentElement).getPropertyValue(name).trim();
}
/**
* Check if component is in dark mode
* @returns {boolean}
*/
isDarkMode() {
return document.documentElement.classList.contains('dark') ||
window.matchMedia('(prefers-color-scheme: dark)').matches;
}
/**
* Debounce function execution
* @param {Function} fn - Function to debounce
* @param {number} delay - Delay in milliseconds
* @returns {Function} Debounced function
*/
debounce(fn, delay = 300) {
let timeoutId;
return (...args) => {
clearTimeout(timeoutId);
timeoutId = setTimeout(() => fn.apply(this, args), delay);
};
}
/**
* Throttle function execution
* @param {Function} fn - Function to throttle
* @param {number} limit - Time limit in milliseconds
* @returns {Function} Throttled function
*/
throttle(fn, limit = 300) {
let inThrottle;
return (...args) => {
if (!inThrottle) {
fn.apply(this, args);
inThrottle = true;
setTimeout(() => (inThrottle = false), limit);
}
};
}
/**
* Wait for an event
* @param {string} eventName - Event name to wait for
* @param {number} [timeout] - Optional timeout in milliseconds
* @returns {Promise} Resolves with event detail
*/
waitForEvent(eventName, timeout = null) {
return new Promise((resolve, reject) => {
const handler = (e) => {
this.removeEventListener(eventName, handler);
clearTimeout(timeoutId);
resolve(e.detail);
};
this.addEventListener(eventName, handler);
let timeoutId;
if (timeout) {
timeoutId = setTimeout(() => {
this.removeEventListener(eventName, handler);
reject(new Error(`Event '${eventName}' did not fire within ${timeout}ms`));
}, timeout);
}
});
}
/**
* Get HTML structure for rendering in shadow DOM
* Useful for preventing repeated string concatenation
* @param {string} html - HTML template
* @param {object} [data] - Data for template interpolation
* @returns {string} Rendered HTML
*/
renderTemplate(html, data = {}) {
return html.replace(/\{\{(\w+)\}\}/g, (match, key) => data[key] ?? match);
}
/**
* Static helper to create component with attributes
* @param {object} attrs - Attributes to set
* @returns {HTMLElement} Component instance
*/
static create(attrs = {}) {
const element = document.createElement(this.name.replace(/([A-Z])/g, '-$1').toLowerCase());
Object.entries(attrs).forEach(([key, value]) => {
if (value === true) {
element.setAttribute(key, '');
} else if (value !== false && value !== null && value !== undefined) {
element.setAttribute(key, value);
}
});
return element;
}
}
// Export for module systems
if (typeof module !== 'undefined' && module.exports) {
module.exports = { DsComponentBase };
}
// Make available globally
if (typeof window !== 'undefined') {
window.DsComponentBase = DsComponentBase;
}

View File

@@ -1,255 +0,0 @@
/**
* DS Input - Web Component
*
* Usage:
* <ds-input placeholder="Enter text..." value=""></ds-input>
* <ds-input type="password" label="Password"></ds-input>
* <ds-input error="This field is required"></ds-input>
*
* Attributes:
* - type: text | password | email | number | search | tel | url
* - placeholder: string
* - value: string
* - label: string
* - error: string
* - disabled: boolean
* - required: boolean
* - icon: string (SVG content or icon name)
*/
class DsInput extends HTMLElement {
static get observedAttributes() {
return ['type', 'placeholder', 'value', 'label', 'error', 'disabled', 'required', 'icon', 'tabindex', 'aria-label', 'aria-invalid', 'aria-describedby'];
}
constructor() {
super();
this.attachShadow({ mode: 'open' });
}
connectedCallback() {
this.render();
this.setupEventListeners();
}
disconnectedCallback() {
this.cleanupEventListeners();
}
attributeChangedCallback(name, oldValue, newValue) {
if (this.shadowRoot.innerHTML && oldValue !== newValue) {
if (name === 'value') {
const input = this.shadowRoot.querySelector('input');
if (input && input.value !== newValue) {
input.value = newValue || '';
}
} else {
this.cleanupEventListeners();
this.render();
this.setupEventListeners();
}
}
}
get type() {
return this.getAttribute('type') || 'text';
}
get placeholder() {
return this.getAttribute('placeholder') || '';
}
get value() {
const input = this.shadowRoot?.querySelector('input');
return input ? input.value : (this.getAttribute('value') || '');
}
set value(val) {
this.setAttribute('value', val);
const input = this.shadowRoot?.querySelector('input');
if (input) input.value = val;
}
get label() {
return this.getAttribute('label');
}
get error() {
return this.getAttribute('error');
}
get disabled() {
return this.hasAttribute('disabled');
}
get required() {
return this.hasAttribute('required');
}
get icon() {
return this.getAttribute('icon');
}
setupEventListeners() {
const input = this.shadowRoot.querySelector('input');
if (!input) return;
// Store handler references for cleanup
this.inputHandler = (e) => {
this.dispatchEvent(new CustomEvent('ds-input', {
bubbles: true,
composed: true,
detail: { value: e.target.value }
}));
};
this.changeHandler = (e) => {
this.dispatchEvent(new CustomEvent('ds-change', {
bubbles: true,
composed: true,
detail: { value: e.target.value }
}));
};
this.focusHandler = () => {
this.dispatchEvent(new CustomEvent('ds-focus', {
bubbles: true,
composed: true
}));
};
this.blurHandler = () => {
this.dispatchEvent(new CustomEvent('ds-blur', {
bubbles: true,
composed: true
}));
};
input.addEventListener('input', this.inputHandler);
input.addEventListener('change', this.changeHandler);
input.addEventListener('focus', this.focusHandler);
input.addEventListener('blur', this.blurHandler);
}
cleanupEventListeners() {
const input = this.shadowRoot?.querySelector('input');
if (!input) return;
// Remove all event listeners
if (this.inputHandler) {
input.removeEventListener('input', this.inputHandler);
delete this.inputHandler;
}
if (this.changeHandler) {
input.removeEventListener('change', this.changeHandler);
delete this.changeHandler;
}
if (this.focusHandler) {
input.removeEventListener('focus', this.focusHandler);
delete this.focusHandler;
}
if (this.blurHandler) {
input.removeEventListener('blur', this.blurHandler);
delete this.blurHandler;
}
}
focus() {
this.shadowRoot.querySelector('input')?.focus();
}
blur() {
this.shadowRoot.querySelector('input')?.blur();
}
render() {
const hasIcon = !!this.icon;
const hasError = !!this.error;
const errorClass = hasError ? 'ds-input--error' : '';
const tabindex = this.disabled ? '-1' : (this.getAttribute('tabindex') || '0');
const errorId = hasError ? 'error-' + Math.random().toString(36).substr(2, 9) : '';
// ARIA attributes
const ariaLabel = this.getAttribute('aria-label') || this.label || '';
const ariaInvalid = hasError ? 'aria-invalid="true"' : '';
const ariaDescribedBy = hasError ? `aria-describedby="${errorId}"` : '';
this.shadowRoot.innerHTML = `
<link rel="stylesheet" href="/admin-ui/css/tokens.css">
<link rel="stylesheet" href="/admin-ui/css/components.css">
<style>
:host {
display: block;
}
.input-wrapper {
position: relative;
}
.input-wrapper.has-icon input {
padding-left: 2.5rem;
}
.icon {
position: absolute;
left: var(--space-3);
top: 50%;
transform: translateY(-50%);
color: var(--muted-foreground);
pointer-events: none;
width: 1rem;
height: 1rem;
}
input:focus-visible {
outline: 2px solid var(--primary);
outline-offset: 2px;
}
.error-text {
margin-top: var(--space-1);
font-size: var(--text-xs);
color: var(--destructive);
}
</style>
${this.label ? `
<label class="ds-label ${this.required ? 'ds-label--required' : ''}">
${this.label}
</label>
` : ''}
<div class="input-wrapper ${hasIcon ? 'has-icon' : ''}">
${hasIcon ? `<span class="icon" aria-hidden="true">${this.getIconSVG()}</span>` : ''}
<input
class="ds-input ${errorClass}"
type="${this.type}"
placeholder="${this.placeholder}"
value="${this.getAttribute('value') || ''}"
tabindex="${tabindex}"
aria-label="${ariaLabel}"
${ariaInvalid}
${ariaDescribedBy}
${this.disabled ? 'disabled' : ''}
${this.required ? 'required' : ''}
/>
</div>
${hasError ? `<p id="${errorId}" class="error-text" role="alert">${this.error}</p>` : ''}
`;
}
getIconSVG() {
const icons = {
search: `<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><circle cx="11" cy="11" r="8"/><path d="m21 21-4.3-4.3"/></svg>`,
email: `<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><rect width="20" height="16" x="2" y="4" rx="2"/><path d="m22 7-8.97 5.7a1.94 1.94 0 0 1-2.06 0L2 7"/></svg>`,
lock: `<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><rect width="18" height="11" x="3" y="11" rx="2" ry="2"/><path d="M7 11V7a5 5 0 0 1 10 0v4"/></svg>`,
user: `<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><circle cx="12" cy="8" r="5"/><path d="M20 21a8 8 0 0 0-16 0"/></svg>`,
};
return icons[this.icon] || this.icon || '';
}
}
customElements.define('ds-input', DsInput);
export default DsInput;

View File

@@ -1,402 +0,0 @@
/**
* @fileoverview A popover component to display user notifications.
* Grouped by date (Today, Yesterday, Earlier) with mark as read support.
*/
import notificationService from '../services/notification-service.js';
class DsNotificationCenter extends HTMLElement {
constructor() {
super();
this.attachShadow({ mode: 'open' });
this._isConnected = false;
}
connectedCallback() {
this._isConnected = true;
this.render();
this._updateNotifications = this._updateNotifications.bind(this);
notificationService.addEventListener('notifications-updated', this._updateNotifications);
// Initialize the service and get initial notifications
// Only update if component is still connected when promise resolves
notificationService.init().then(() => {
if (this._isConnected) {
this._updateNotifications({ detail: { notifications: notificationService.getAll() } });
}
}).catch((error) => {
console.error('[DsNotificationCenter] Failed to initialize notifications:', error);
});
this.shadowRoot.getElementById('mark-all-read').addEventListener('click', () => {
notificationService.markAllAsRead();
});
this.shadowRoot.getElementById('clear-all').addEventListener('click', () => {
notificationService.clearAll();
});
this.shadowRoot.getElementById('notification-list').addEventListener('click', this._handleNotificationClick.bind(this));
}
disconnectedCallback() {
this._isConnected = false;
notificationService.removeEventListener('notifications-updated', this._updateNotifications);
}
_handleNotificationClick(e) {
const notificationEl = e.target.closest('.notification');
if (!notificationEl) return;
const id = notificationEl.dataset.id;
if (!id) return;
// Mark as read if it was unread
if (notificationEl.classList.contains('unread')) {
notificationService.markAsRead(id);
}
// Handle action button clicks
const actionButton = e.target.closest('[data-event]');
if (actionButton) {
let payload = {};
try {
payload = JSON.parse(actionButton.dataset.payload || '{}');
} catch (e) {
console.error('Invalid action payload:', e);
}
this.dispatchEvent(new CustomEvent('notification-action', {
bubbles: true,
composed: true,
detail: {
event: actionButton.dataset.event,
payload
}
}));
// Close the notification center
this.removeAttribute('open');
}
// Handle delete button
const deleteButton = e.target.closest('.delete-btn');
if (deleteButton) {
e.stopPropagation();
notificationService.delete(id);
}
}
_updateNotifications({ detail }) {
const { notifications } = detail;
const listEl = this.shadowRoot?.getElementById('notification-list');
// Null safety check - component may be disconnecting
if (!listEl) {
console.warn('[DsNotificationCenter] Notification list element not found');
return;
}
if (!notifications || notifications.length === 0) {
listEl.innerHTML = `
<div class="empty-state">
<svg width="48" height="48" fill="none" stroke="currentColor" stroke-width="1.5" viewBox="0 0 24 24">
<path d="M6 8a6 6 0 0 1 12 0c0 7 3 9 3 9H3s3-2 3-9"/>
<path d="M10.3 21a1.94 1.94 0 0 0 3.4 0"/>
</svg>
<p>No notifications yet</p>
<span>You're all caught up!</span>
</div>
`;
return;
}
const grouped = this._groupNotificationsByDate(notifications);
let html = '';
for (const [groupTitle, groupNotifications] of Object.entries(grouped)) {
html += `
<div class="group">
<div class="group__title">${groupTitle}</div>
${groupNotifications.map(n => this._renderNotification(n)).join('')}
</div>
`;
}
listEl.innerHTML = html;
}
_groupNotificationsByDate(notifications) {
const groups = {};
const today = new Date();
const yesterday = new Date(today);
yesterday.setDate(yesterday.getDate() - 1);
const isSameDay = (d1, d2) =>
d1.getFullYear() === d2.getFullYear() &&
d1.getMonth() === d2.getMonth() &&
d1.getDate() === d2.getDate();
notifications.forEach(n => {
const date = new Date(n.timestamp);
let groupName;
if (isSameDay(date, today)) {
groupName = 'Today';
} else if (isSameDay(date, yesterday)) {
groupName = 'Yesterday';
} else {
groupName = 'Earlier';
}
if (!groups[groupName]) {
groups[groupName] = [];
}
groups[groupName].push(n);
});
return groups;
}
_renderNotification(n) {
const time = new Date(n.timestamp).toLocaleTimeString([], {
hour: '2-digit',
minute: '2-digit'
});
const actionsHtml = (n.actions || []).map(action =>
`<button class="action-btn" data-event="${action.event}" data-payload='${JSON.stringify(action.payload || {})}'>${action.label}</button>`
).join('');
return `
<div class="notification ${n.read ? '' : 'unread'}" data-id="${n.id}">
<div class="icon-container">
<div class="dot ${n.type || 'info'}"></div>
</div>
<div class="notification-content">
<p class="title">${this._escapeHtml(n.title)}</p>
${n.message ? `<p class="message">${this._escapeHtml(n.message)}</p>` : ''}
<div class="meta">
<span class="time">${time}</span>
${n.source ? `<span class="source">${n.source}</span>` : ''}
</div>
${actionsHtml ? `<div class="actions">${actionsHtml}</div>` : ''}
</div>
<button class="delete-btn" aria-label="Delete notification">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<line x1="18" y1="6" x2="6" y2="18"></line>
<line x1="6" y1="6" x2="18" y2="18"></line>
</svg>
</button>
</div>
`;
}
_escapeHtml(text) {
const div = document.createElement('div');
div.textContent = text;
return div.innerHTML;
}
render() {
this.shadowRoot.innerHTML = `
<style>
:host {
display: none;
position: absolute;
top: calc(100% + var(--space-2));
right: 0;
width: 380px;
z-index: 100;
}
:host([open]) {
display: block;
}
.panel {
background: var(--popover);
color: var(--popover-foreground);
border: 1px solid var(--border);
border-radius: var(--radius-lg);
box-shadow: var(--shadow-lg);
display: flex;
flex-direction: column;
max-height: 480px;
overflow: hidden;
}
.header {
display: flex;
justify-content: space-between;
align-items: center;
padding: var(--space-3) var(--space-4);
border-bottom: 1px solid var(--border);
flex-shrink: 0;
}
.header h3 {
margin: 0;
font-size: var(--text-base);
font-weight: var(--font-semibold);
}
.header-actions {
display: flex;
gap: var(--space-2);
}
.header-actions button {
font-size: var(--text-xs);
color: var(--primary);
cursor: pointer;
background: none;
border: none;
padding: 0;
}
.header-actions button:hover {
text-decoration: underline;
}
.content {
overflow-y: auto;
flex: 1;
}
.empty-state {
text-align: center;
padding: var(--space-8);
color: var(--muted-foreground);
}
.empty-state svg {
opacity: 0.5;
margin-bottom: var(--space-3);
}
.empty-state p {
margin: 0;
font-size: var(--text-sm);
font-weight: var(--font-medium);
color: var(--foreground);
}
.empty-state span {
font-size: var(--text-xs);
}
.group {
border-bottom: 1px solid var(--border);
}
.group:last-child {
border-bottom: none;
}
.group__title {
padding: var(--space-2) var(--space-4);
font-size: var(--text-xs);
font-weight: var(--font-medium);
color: var(--muted-foreground);
text-transform: uppercase;
letter-spacing: 0.05em;
background: var(--muted);
}
.notification {
display: flex;
gap: var(--space-3);
padding: var(--space-3) var(--space-4);
position: relative;
cursor: pointer;
transition: background 0.15s ease;
}
.notification.unread {
background-color: oklch(from var(--primary) l c h / 0.05);
}
.notification:hover {
background-color: var(--accent);
}
.icon-container {
flex-shrink: 0;
width: 10px;
padding-top: 4px;
}
.dot {
width: 8px;
height: 8px;
border-radius: 50%;
}
.dot.info { background-color: var(--primary); }
.dot.success { background-color: var(--success); }
.dot.warning { background-color: var(--warning); }
.dot.error { background-color: var(--destructive); }
.notification-content {
flex: 1;
min-width: 0;
}
.notification-content .title {
margin: 0;
font-size: var(--text-sm);
font-weight: var(--font-medium);
color: var(--foreground);
line-height: 1.3;
}
.notification-content .message {
margin: var(--space-1) 0 0;
font-size: var(--text-xs);
color: var(--muted-foreground);
line-height: 1.4;
}
.meta {
display: flex;
gap: var(--space-2);
font-size: var(--text-xs);
color: var(--muted-foreground);
margin-top: var(--space-1);
}
.source {
background: var(--muted);
padding: 0 var(--space-1);
border-radius: var(--radius-sm);
}
.actions {
margin-top: var(--space-2);
display: flex;
gap: var(--space-2);
}
.action-btn {
font-size: var(--text-xs);
padding: var(--space-1) var(--space-2);
background: var(--muted);
border: 1px solid var(--border);
color: var(--foreground);
border-radius: var(--radius-sm);
cursor: pointer;
transition: background 0.15s ease;
}
.action-btn:hover {
background: var(--accent);
}
.delete-btn {
position: absolute;
top: var(--space-2);
right: var(--space-2);
background: none;
border: none;
color: var(--muted-foreground);
cursor: pointer;
padding: var(--space-1);
border-radius: var(--radius);
opacity: 0;
transition: opacity 0.15s ease;
}
.notification:hover .delete-btn {
opacity: 1;
}
.delete-btn:hover {
background: var(--destructive);
color: white;
}
</style>
<div class="panel">
<div class="header">
<h3>Notifications</h3>
<div class="header-actions">
<button id="mark-all-read">Mark all read</button>
<button id="clear-all">Clear all</button>
</div>
</div>
<div class="content" id="notification-list">
<!-- Notifications will be rendered here -->
</div>
</div>
`;
}
}
customElements.define('ds-notification-center', DsNotificationCenter);

View File

@@ -1,84 +0,0 @@
/**
* admin-ui/js/components/ds-toast-provider.js
* Manages a stack of ds-toast components.
* Provides a global window.showToast() function.
*/
class DsToastProvider extends HTMLElement {
constructor() {
super();
this.attachShadow({ mode: 'open' });
}
connectedCallback() {
this.render();
// Expose global toast function
window.showToast = this.showToast.bind(this);
}
disconnectedCallback() {
delete window.showToast;
}
/**
* Show a toast notification
* @param {object} options - Toast options
* @param {string} options.message - The main message
* @param {string} [options.type='info'] - 'info', 'success', 'warning', 'error'
* @param {number} [options.duration=5000] - Duration in ms. 0 for persistent
* @param {boolean} [options.dismissible=true] - Show close button
* @returns {HTMLElement} The created toast element
*/
showToast({ message, type = 'info', duration = 5000, dismissible = true }) {
const toast = document.createElement('ds-toast');
toast.setAttribute('type', type);
toast.setAttribute('duration', String(duration));
if (dismissible) {
toast.setAttribute('dismissible', '');
}
toast.innerHTML = message;
const stack = this.shadowRoot.querySelector('.stack');
stack.appendChild(toast);
// Limit visible toasts
const toasts = stack.querySelectorAll('ds-toast');
if (toasts.length > 5) {
toasts[0].dismiss();
}
return toast;
}
render() {
this.shadowRoot.innerHTML = `
<style>
.stack {
position: fixed;
top: var(--space-4);
right: var(--space-4);
z-index: 9999;
display: flex;
flex-direction: column;
gap: var(--space-3);
max-width: 380px;
pointer-events: none;
}
.stack ::slotted(ds-toast),
.stack ds-toast {
pointer-events: auto;
}
@media (max-width: 480px) {
.stack {
left: var(--space-4);
right: var(--space-4);
max-width: none;
}
}
</style>
<div class="stack"></div>
`;
}
}
customElements.define('ds-toast-provider', DsToastProvider);

View File

@@ -1,167 +0,0 @@
/**
* admin-ui/js/components/ds-toast.js
* A single toast notification component with swipe-to-dismiss support.
*/
class DsToast extends HTMLElement {
static get observedAttributes() {
return ['type', 'duration'];
}
constructor() {
super();
this.attachShadow({ mode: 'open' });
this._duration = 5000;
this._dismissTimer = null;
}
connectedCallback() {
this.render();
this.setupAutoDismiss();
this.setupSwipeToDismiss();
this.shadowRoot.querySelector('.close-button')?.addEventListener('click', () => this.dismiss());
}
disconnectedCallback() {
if (this._dismissTimer) {
clearTimeout(this._dismissTimer);
}
}
attributeChangedCallback(name, oldValue, newValue) {
if (name === 'duration') {
this._duration = parseInt(newValue, 10);
}
}
setupAutoDismiss() {
if (this._duration > 0 && !this.hasAttribute('progress')) {
this._dismissTimer = setTimeout(() => this.dismiss(), this._duration);
}
}
dismiss() {
if (this._dismissTimer) {
clearTimeout(this._dismissTimer);
}
this.classList.add('dismissing');
this.addEventListener('animationend', () => {
this.dispatchEvent(new CustomEvent('dismiss', { bubbles: true, composed: true }));
this.remove();
}, { once: true });
}
setupSwipeToDismiss() {
let startX = 0;
let currentX = 0;
let isDragging = false;
this.addEventListener('pointerdown', (e) => {
isDragging = true;
startX = e.clientX;
currentX = startX;
this.style.transition = 'none';
this.setPointerCapture(e.pointerId);
});
this.addEventListener('pointermove', (e) => {
if (!isDragging) return;
currentX = e.clientX;
const diff = currentX - startX;
this.style.transform = `translateX(${diff}px)`;
});
const onPointerUp = (e) => {
if (!isDragging) return;
isDragging = false;
this.style.transition = 'transform 0.2s ease';
const diff = currentX - startX;
const threshold = this.offsetWidth * 0.3;
if (Math.abs(diff) > threshold) {
this.style.transform = `translateX(${diff > 0 ? '100%' : '-100%'})`;
this.dismiss();
} else {
this.style.transform = 'translateX(0)';
}
};
this.addEventListener('pointerup', onPointerUp);
this.addEventListener('pointercancel', onPointerUp);
}
render() {
const type = this.getAttribute('type') || 'info';
const dismissible = this.hasAttribute('dismissible');
this.shadowRoot.innerHTML = `
<style>
:host {
display: flex;
align-items: center;
gap: var(--space-3);
background: var(--card);
color: var(--card-foreground);
border: 1px solid var(--border);
border-left: 4px solid var(--primary);
padding: var(--space-3) var(--space-4);
border-radius: var(--radius-lg);
box-shadow: var(--shadow-lg);
transform-origin: top center;
animation: slide-in 0.3s ease forwards;
will-change: transform, opacity;
cursor: grab;
touch-action: pan-y;
}
:host([type="success"]) { border-left-color: var(--success); }
:host([type="warning"]) { border-left-color: var(--warning); }
:host([type="error"]) { border-left-color: var(--destructive); }
:host(.dismissing) {
animation: slide-out 0.3s ease forwards;
}
.content {
flex: 1;
font-size: var(--text-sm);
line-height: 1.4;
}
.close-button {
background: none;
border: none;
color: var(--muted-foreground);
padding: var(--space-1);
cursor: pointer;
width: 1.5rem;
height: 1.5rem;
display: flex;
align-items: center;
justify-content: center;
border-radius: var(--radius);
transition: background 0.15s ease;
}
.close-button:hover {
background: var(--accent);
color: var(--foreground);
}
@keyframes slide-in {
from { opacity: 0; transform: translateY(-20px) scale(0.95); }
to { opacity: 1; transform: translateY(0) scale(1); }
}
@keyframes slide-out {
from { opacity: 1; transform: translateY(0) scale(1); }
to { opacity: 0; transform: translateY(-20px) scale(0.95); }
}
</style>
<div class="icon"><slot name="icon"></slot></div>
<div class="content">
<slot></slot>
</div>
${dismissible ? `<button class="close-button" aria-label="Dismiss">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<line x1="18" y1="6" x2="6" y2="18"></line>
<line x1="6" y1="6" x2="18" y2="18"></line>
</svg>
</button>` : ''}
`;
}
}
customElements.define('ds-toast', DsToast);

View File

@@ -1,399 +0,0 @@
/**
* @fileoverview A reusable stepper component for guided workflows.
* Supports step dependencies, persistence, and event-driven actions.
*/
const ICONS = {
pending: '',
active: `<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<circle cx="12" cy="12" r="10"/>
<path d="M12 6v6l4 2"/>
</svg>`,
completed: `<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5">
<polyline points="20 6 9 17 4 12"/>
</svg>`,
error: `<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<line x1="18" y1="6" x2="6" y2="18"/>
<line x1="6" y1="6" x2="18" y2="18"/>
</svg>`,
skipped: `<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<line x1="5" y1="12" x2="19" y2="12"/>
</svg>`
};
class DsWorkflow extends HTMLElement {
constructor() {
super();
this.attachShadow({ mode: 'open' });
this._steps = [];
}
static get observedAttributes() {
return ['workflow-id'];
}
get workflowId() {
return this.getAttribute('workflow-id');
}
set steps(stepsArray) {
this._steps = stepsArray.map(s => ({
status: 'pending',
optional: false,
dependsOn: [],
...s
}));
this._loadState();
this._render();
}
get steps() {
return this._steps;
}
connectedCallback() {
this._renderBase();
}
_loadState() {
if (!this.workflowId) return;
try {
const savedState = JSON.parse(localStorage.getItem(`dss_workflow_${this.workflowId}`));
if (savedState) {
this._steps.forEach(step => {
if (savedState[step.id]) {
step.status = savedState[step.id].status;
if (savedState[step.id].message) {
step.message = savedState[step.id].message;
}
}
});
}
} catch (e) {
console.error('Failed to load workflow state:', e);
}
}
_saveState() {
if (!this.workflowId) return;
const stateToSave = this._steps.reduce((acc, step) => {
acc[step.id] = {
status: step.status,
message: step.message || null
};
return acc;
}, {});
localStorage.setItem(`dss_workflow_${this.workflowId}`, JSON.stringify(stateToSave));
}
/**
* Update a step's status
* @param {string} stepId - The step ID
* @param {string} status - 'pending', 'active', 'completed', 'error', 'skipped'
* @param {string} [message] - Optional message (for error states)
*/
updateStepStatus(stepId, status, message = '') {
const step = this._steps.find(s => s.id === stepId);
if (step) {
step.status = status;
step.message = message;
this._saveState();
this._render();
this.dispatchEvent(new CustomEvent('workflow-step-change', {
bubbles: true,
composed: true,
detail: { ...step }
}));
// Check if workflow is complete
const requiredSteps = this._steps.filter(s => !s.optional);
const completedRequired = requiredSteps.filter(s => s.status === 'completed').length;
if (completedRequired === requiredSteps.length && requiredSteps.length > 0) {
this.dispatchEvent(new CustomEvent('workflow-complete', {
bubbles: true,
composed: true
}));
}
}
}
/**
* Reset the workflow to initial state
*/
reset() {
this._steps.forEach(step => {
step.status = 'pending';
step.message = '';
});
this._saveState();
this._render();
}
/**
* Skip a step
* @param {string} stepId - The step ID to skip
*/
skipStep(stepId) {
const step = this._steps.find(s => s.id === stepId);
if (step && step.optional) {
this.updateStepStatus(stepId, 'skipped');
}
}
_determineActiveStep() {
const completedIds = new Set(
this._steps
.filter(s => s.status === 'completed' || s.status === 'skipped')
.map(s => s.id)
);
let foundActive = false;
this._steps.forEach(step => {
if (step.status === 'pending' && !foundActive) {
const depsMet = (step.dependsOn || []).every(depId => completedIds.has(depId));
if (depsMet) {
step.status = 'active';
foundActive = true;
}
}
});
}
_getProgress() {
const total = this._steps.filter(s => !s.optional).length;
const completed = this._steps.filter(s =>
!s.optional && (s.status === 'completed' || s.status === 'skipped')
).length;
return total > 0 ? (completed / total) * 100 : 0;
}
_renderBase() {
this.shadowRoot.innerHTML = `
<style>
:host {
display: block;
}
.workflow-container {
display: flex;
flex-direction: column;
}
.progress-bar {
height: 4px;
background: var(--muted);
border-radius: 2px;
margin-bottom: var(--space-4);
overflow: hidden;
}
.progress-bar__indicator {
height: 100%;
background: var(--success);
width: 0%;
transition: width 0.3s ease;
}
.steps-wrapper {
display: flex;
flex-direction: column;
}
.step {
display: flex;
gap: var(--space-3);
}
.step__indicator {
display: flex;
flex-direction: column;
align-items: center;
}
.step__icon {
width: 28px;
height: 28px;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
border: 2px solid var(--border);
background: var(--card);
transition: all 0.2s ease;
flex-shrink: 0;
}
.step__icon svg {
color: white;
}
.step--pending .step__icon {
border-color: var(--muted-foreground);
}
.step--active .step__icon {
border-color: var(--primary);
background: var(--primary);
}
.step--completed .step__icon {
border-color: var(--success);
background: var(--success);
}
.step--error .step__icon {
border-color: var(--destructive);
background: var(--destructive);
}
.step--skipped .step__icon {
border-color: var(--muted-foreground);
background: var(--muted-foreground);
}
.step__line {
width: 2px;
flex-grow: 1;
min-height: var(--space-4);
background: var(--border);
margin: var(--space-1) 0;
}
.step:last-child .step__line {
display: none;
}
.step--completed .step__line,
.step--skipped .step__line {
background: var(--success);
}
.step__content {
flex: 1;
padding-bottom: var(--space-4);
min-width: 0;
}
.step__header {
display: flex;
align-items: flex-start;
justify-content: space-between;
gap: var(--space-2);
}
.step__title {
font-weight: var(--font-medium);
color: var(--foreground);
font-size: var(--text-sm);
}
.step--pending .step__title,
.step--pending .step__description {
color: var(--muted-foreground);
}
.step__optional {
font-size: var(--text-xs);
color: var(--muted-foreground);
background: var(--muted);
padding: 0 var(--space-1);
border-radius: var(--radius-sm);
}
.step__description {
font-size: var(--text-xs);
color: var(--muted-foreground);
margin-top: var(--space-1);
line-height: 1.4;
}
.step__actions {
margin-top: var(--space-3);
display: flex;
gap: var(--space-2);
}
.error-message {
color: var(--destructive);
font-size: var(--text-xs);
margin-top: var(--space-2);
padding: var(--space-2);
background: oklch(from var(--destructive) l c h / 0.1);
border-radius: var(--radius);
}
</style>
<div class="workflow-container">
<div class="progress-bar">
<div class="progress-bar__indicator"></div>
</div>
<div class="steps-wrapper" id="steps-wrapper"></div>
</div>
`;
}
_render() {
const wrapper = this.shadowRoot.getElementById('steps-wrapper');
if (!wrapper || !this._steps || this._steps.length === 0) {
return;
}
// Determine which step should be active
this._determineActiveStep();
// Render steps
wrapper.innerHTML = this._steps.map(step => this._renderStep(step)).join('');
// Update progress bar
const progress = this._getProgress();
const indicator = this.shadowRoot.querySelector('.progress-bar__indicator');
if (indicator) {
indicator.style.width = `${progress}%`;
}
// Add event listeners for action buttons
wrapper.querySelectorAll('[data-action-event]').forEach(button => {
button.addEventListener('click', () => {
this.dispatchEvent(new CustomEvent(button.dataset.actionEvent, {
bubbles: true,
composed: true,
detail: { stepId: button.dataset.stepId }
}));
});
});
// Add skip button listeners
wrapper.querySelectorAll('[data-skip]').forEach(button => {
button.addEventListener('click', () => {
this.skipStep(button.dataset.skip);
});
});
}
_renderStep(step) {
const isActionable = step.status === 'active' && step.action;
const canSkip = step.status === 'active' && step.optional;
return `
<div class="step step--${step.status}" data-step-id="${step.id}">
<div class="step__indicator">
<div class="step__icon">${ICONS[step.status] || ''}</div>
<div class="step__line"></div>
</div>
<div class="step__content">
<div class="step__header">
<div class="step__title">${this._escapeHtml(step.title)}</div>
${step.optional ? '<span class="step__optional">Optional</span>' : ''}
</div>
${step.description ? `<div class="step__description">${this._escapeHtml(step.description)}</div>` : ''}
${step.status === 'error' && step.message ? `<div class="error-message">${this._escapeHtml(step.message)}</div>` : ''}
${isActionable || canSkip ? `
<div class="step__actions">
${isActionable ? `
<ds-button
variant="primary"
size="sm"
data-step-id="${step.id}"
data-action-event="${step.action.event}"
>${step.action.label}</ds-button>
` : ''}
${canSkip ? `
<ds-button
variant="ghost"
size="sm"
data-skip="${step.id}"
>Skip</ds-button>
` : ''}
</div>
` : ''}
</div>
</div>
`;
}
_escapeHtml(text) {
const div = document.createElement('div');
div.textContent = text;
return div.innerHTML;
}
}
customElements.define('ds-workflow', DsWorkflow);

View File

@@ -1,39 +0,0 @@
/**
* Design System Server (DSS) - Component Registry
*
* Central export for all Web Components.
* Import this file to register all components.
*/
// Core Components
export { default as DsButton } from './ds-button.js';
export { DsCard, DsCardHeader, DsCardTitle, DsCardDescription, DsCardContent, DsCardFooter } from './ds-card.js';
export { default as DsInput } from './ds-input.js';
export { default as DsBadge } from './ds-badge.js';
// Component list for documentation
export const componentList = [
{
name: 'ds-button',
description: 'Interactive button with variants and sizes',
variants: ['primary', 'secondary', 'outline', 'ghost', 'destructive', 'success', 'link'],
sizes: ['sm', 'default', 'lg', 'icon', 'icon-sm', 'icon-lg']
},
{
name: 'ds-card',
description: 'Container for grouped content',
subcomponents: ['ds-card-header', 'ds-card-title', 'ds-card-description', 'ds-card-content', 'ds-card-footer']
},
{
name: 'ds-input',
description: 'Text input with label, icon, and error states',
types: ['text', 'password', 'email', 'number', 'search', 'tel', 'url']
},
{
name: 'ds-badge',
description: 'Status indicator badge',
variants: ['default', 'secondary', 'outline', 'destructive', 'success', 'warning']
}
];
console.log('[DSS] Components loaded:', componentList.map(c => c.name).join(', '));

View File

@@ -1,132 +0,0 @@
/**
* ds-activity-bar.js
* Activity bar component - team/project switcher
*/
class DSActivityBar extends HTMLElement {
constructor() {
super();
this.currentTeam = 'ui';
this.advancedMode = this.loadAdvancedMode();
this.teams = [
{ id: 'ui', label: 'UI', icon: '🎨' },
{ id: 'ux', label: 'UX', icon: '👁️' },
{ id: 'qa', label: 'QA', icon: '🔍' },
{ id: 'admin', label: 'Admin', icon: '🛡️' }
];
}
loadAdvancedMode() {
try {
return localStorage.getItem('dss-advanced-mode') === 'true';
} catch (e) {
return false;
}
}
saveAdvancedMode() {
try {
localStorage.setItem('dss-advanced-mode', this.advancedMode.toString());
} catch (e) {
console.error('Failed to save advanced mode preference:', e);
}
}
connectedCallback() {
this.render();
this.setupEventListeners();
}
render() {
this.innerHTML = `
${this.teams.map(team => `
<div class="activity-item ${team.id === this.currentTeam ? 'active' : ''}"
data-team="${team.id}"
title="${team.label} Team">
<span style="font-size: 20px;">${team.icon}</span>
</div>
`).join('')}
<div style="flex: 1;"></div>
<div class="activity-item"
data-action="chat"
title="AI Chat">
<span style="font-size: 18px;">💬</span>
</div>
<div class="activity-item ${this.advancedMode ? 'active' : ''}"
data-action="advanced-mode"
title="Advanced Mode: ${this.advancedMode ? 'ON' : 'OFF'}">
<span style="font-size: 18px;">🔧</span>
</div>
<div class="activity-item"
data-action="settings"
title="Settings">
<span style="font-size: 18px;">⚙️</span>
</div>
`;
}
setupEventListeners() {
this.querySelectorAll('.activity-item[data-team]').forEach(item => {
item.addEventListener('click', (e) => {
const teamId = e.currentTarget.dataset.team;
this.switchTeam(teamId);
});
});
this.querySelector('.activity-item[data-action="chat"]')?.addEventListener('click', () => {
// Toggle chat sidebar visibility
const chatSidebar = document.querySelector('ds-ai-chat-sidebar');
if (chatSidebar && chatSidebar.toggleCollapse) {
chatSidebar.toggleCollapse();
}
});
this.querySelector('.activity-item[data-action="advanced-mode"]')?.addEventListener('click', () => {
this.toggleAdvancedMode();
});
this.querySelector('.activity-item[data-action="settings"]')?.addEventListener('click', () => {
// Dispatch settings-open event to parent shell
this.dispatchEvent(new CustomEvent('settings-open', {
bubbles: true,
detail: { action: 'open-settings' }
}));
});
}
toggleAdvancedMode() {
this.advancedMode = !this.advancedMode;
this.saveAdvancedMode();
this.render();
this.setupEventListeners();
// Dispatch advanced-mode-change event to parent shell
this.dispatchEvent(new CustomEvent('advanced-mode-change', {
bubbles: true,
detail: { advancedMode: this.advancedMode }
}));
}
switchTeam(teamId) {
if (teamId === this.currentTeam) return;
this.currentTeam = teamId;
// Update active state
this.querySelectorAll('.activity-item[data-team]').forEach(item => {
item.classList.toggle('active', item.dataset.team === teamId);
});
// Dispatch team-switch event to parent shell
this.dispatchEvent(new CustomEvent('team-switch', {
bubbles: true,
detail: { team: teamId }
}));
}
}
customElements.define('ds-activity-bar', DSActivityBar);

View File

@@ -1,269 +0,0 @@
/**
* ds-ai-chat-sidebar.js
* AI Chat Sidebar wrapper component
* Wraps ds-chat-panel with collapse/expand toggle and context binding
* MVP2: Right sidebar integrated with 3-column layout
*/
import contextStore from '../../stores/context-store.js';
import { useUserStore } from '../../stores/user-store.js';
class DSAiChatSidebar extends HTMLElement {
constructor() {
super();
this.userStore = useUserStore();
const preferences = this.userStore.getPreferences();
this.isCollapsed = preferences.chatCollapsedState !== false; // Default to collapsed
this.currentProject = null;
this.currentTeam = null;
this.currentPage = null;
}
connectedCallback() {
this.render();
this.setupEventListeners();
this.initializeContextSubscriptions();
}
initializeContextSubscriptions() {
// Subscribe to context changes to update chat panel context
this.unsubscribe = contextStore.subscribe(({ state }) => {
this.currentProject = state.project;
this.currentTeam = state.team;
this.currentPage = state.page;
// Update chat panel with current context
const chatPanel = this.querySelector('ds-chat-panel');
if (chatPanel && chatPanel.setContext) {
chatPanel.setContext({
project: this.currentProject,
team: this.currentTeam,
page: this.currentPage
});
}
});
// Get initial context
const context = contextStore.getState();
if (context) {
this.currentProject = context.currentProject || context.project || null;
this.currentTeam = context.teamId || context.team || null;
this.currentPage = context.page || null;
}
}
render() {
const buttonClass = this.isCollapsed ? 'rotating' : '';
this.innerHTML = `
<div class="ai-chat-container" style="
display: flex;
flex-direction: column;
height: 100%;
background: var(--vscode-sidebar-background);
border-left: 1px solid var(--vscode-border);
" role="complementary" aria-label="AI Assistant sidebar">
<!-- Header with animated collapse button (shown when expanded) -->
<div style="
padding: 12px;
border-bottom: 1px solid var(--vscode-border);
display: flex;
justify-content: space-between;
align-items: center;
background: var(--vscode-bg);
${this.isCollapsed ? 'display: none;' : ''}
">
<div style="
font-weight: 500;
font-size: 13px;
color: var(--vscode-foreground);
">💬 AI Assistant</div>
<button
id="toggle-collapse-btn"
class="ai-chat-toggle-btn ${buttonClass}"
aria-label="Toggle chat sidebar"
aria-expanded="${!this.isCollapsed}"
style="
background: transparent;
border: none;
color: var(--vscode-foreground);
cursor: pointer;
padding: 4px 8px;
font-size: 14px;
display: flex;
align-items: center;
justify-content: center;
"
title="Toggle Chat Sidebar">
</button>
</div>
<!-- Chat content (collapsible) -->
<div class="chat-content" style="
flex: 1;
overflow: hidden;
display: flex;
flex-direction: column;
${this.isCollapsed ? 'display: none;' : ''}
">
<!-- Chat panel will be hydrated here via component registry -->
<div id="chat-panel-container" style="
flex: 1;
overflow: hidden;
display: flex;
flex-direction: column;
"></div>
</div>
<!-- Collapsed state indicator (shown when collapsed) -->
${this.isCollapsed ? `
<button
id="toggle-collapse-btn-collapsed"
class="ai-chat-toggle-btn ${buttonClass}"
aria-label="Expand chat sidebar"
aria-expanded="false"
style="
background: transparent;
border: none;
color: var(--vscode-foreground);
cursor: pointer;
padding: 12px;
font-size: 16px;
text-align: center;
flex: 1;
display: flex;
align-items: center;
justify-content: center;
"
title="Expand Chat Sidebar">
💬
</button>
` : ''}
</div>
`;
}
async setupEventListeners() {
// Handle both expanded and collapsed toggle buttons
const toggleBtn = this.querySelector('#toggle-collapse-btn');
const toggleBtnCollapsed = this.querySelector('#toggle-collapse-btn-collapsed');
const attachToggleListener = (btn) => {
if (btn) {
btn.addEventListener('click', () => {
this.toggleCollapse();
});
btn.addEventListener('keydown', (e) => {
if (e.key === 'Enter' || e.key === ' ') {
e.preventDefault();
btn.click();
}
});
}
};
attachToggleListener(toggleBtn);
attachToggleListener(toggleBtnCollapsed);
// Hydrate chat panel on first connection
const chatContainer = this.querySelector('#chat-panel-container');
if (chatContainer && chatContainer.children.length === 0) {
try {
// Import component registry to load chat panel
const { hydrateComponent } = await import('../../config/component-registry.js');
await hydrateComponent('ds-chat-panel', chatContainer);
console.log('[DSAiChatSidebar] Chat panel loaded');
// Set initial context on chat panel
const chatPanel = chatContainer.querySelector('ds-chat-panel');
if (chatPanel && chatPanel.setContext) {
chatPanel.setContext({
project: this.currentProject,
team: this.currentTeam,
page: this.currentPage
});
}
} catch (error) {
console.error('[DSAiChatSidebar] Failed to load chat panel:', error);
chatContainer.innerHTML = `
<div style="padding: 12px; color: var(--vscode-error); font-size: 12px;">
Failed to load chat panel
</div>
`;
}
}
}
toggleCollapse() {
this.isCollapsed = !this.isCollapsed;
// Persist chat collapsed state to userStore
this.userStore.updatePreferences({ chatCollapsedState: this.isCollapsed });
// Update CSS class for smooth CSS transition (avoid re-render for better UX)
if (this.isCollapsed) {
this.classList.add('collapsed');
} else {
this.classList.remove('collapsed');
}
// Update button classes for rotation animation
const btns = this.querySelectorAll('.ai-chat-toggle-btn');
btns.forEach(btn => {
if (this.isCollapsed) {
btn.classList.add('rotating');
} else {
btn.classList.remove('rotating');
}
btn.setAttribute('aria-expanded', String(!this.isCollapsed));
});
// Update header and content visibility with inline styles
const header = this.querySelector('[style*="padding: 12px"]');
const content = this.querySelector('.chat-content');
if (header) {
if (this.isCollapsed) {
header.style.display = 'none';
} else {
header.style.display = 'flex';
}
}
if (content) {
if (this.isCollapsed) {
content.style.display = 'none';
} else {
content.style.display = 'flex';
}
}
// Toggle collapsed button visibility
let collapsedBtn = this.querySelector('#toggle-collapse-btn-collapsed');
if (!collapsedBtn && this.isCollapsed) {
// Create the collapsed button if needed
this.render();
this.setupEventListeners();
} else if (collapsedBtn && !this.isCollapsed) {
// Remove the collapsed button if needed
this.render();
this.setupEventListeners();
}
// Dispatch event for layout adjustment
this.dispatchEvent(new CustomEvent('chat-sidebar-toggled', {
detail: { isCollapsed: this.isCollapsed },
bubbles: true,
composed: true
}));
}
disconnectedCallback() {
if (this.unsubscribe) {
this.unsubscribe();
}
}
}
customElements.define('ds-ai-chat-sidebar', DSAiChatSidebar);

View File

@@ -1,120 +0,0 @@
/**
* ds-panel.js
* Bottom panel component - holds team-specific tabs
*/
import { getPanelConfig } from '../../config/panel-config.js';
class DSPanel extends HTMLElement {
constructor() {
super();
this.currentTab = null;
this.tabs = [];
this.advancedMode = false;
}
/**
* Configure panel with team-specific tabs
* @param {string} teamId - Team identifier (ui, ux, qa, admin)
* @param {boolean} advancedMode - Whether advanced mode is enabled
*/
configure(teamId, advancedMode = false) {
this.advancedMode = advancedMode;
this.tabs = getPanelConfig(teamId, advancedMode);
// Set first tab as current if not already set
if (this.tabs.length > 0 && !this.currentTab) {
this.currentTab = this.tabs[0].id;
}
// Re-render with new configuration
this.render();
this.setupEventListeners();
}
connectedCallback() {
this.render();
this.setupEventListeners();
}
render() {
this.innerHTML = `
<div class="panel-header">
${this.tabs.map(tab => `
<div class="panel-tab ${tab.id === this.currentTab ? 'active' : ''}"
data-tab="${tab.id}">
${tab.label}
</div>
`).join('')}
</div>
<div class="panel-content">
<div id="panel-tab-content">
${this.renderTabContent(this.currentTab)}
</div>
</div>
`;
}
setupEventListeners() {
this.querySelectorAll('.panel-tab').forEach(tab => {
tab.addEventListener('click', (e) => {
const tabId = e.currentTarget.dataset.tab;
this.switchTab(tabId);
});
});
}
switchTab(tabId) {
if (tabId === this.currentTab) return;
this.currentTab = tabId;
// Update active state
this.querySelectorAll('.panel-tab').forEach(tab => {
tab.classList.toggle('active', tab.dataset.tab === tabId);
});
// Update content
const content = this.querySelector('#panel-tab-content');
if (content) {
content.innerHTML = this.renderTabContent(tabId);
}
// Dispatch tab-switch event
this.dispatchEvent(new CustomEvent('panel-tab-switch', {
bubbles: true,
detail: { tab: tabId }
}));
}
renderTabContent(tabId) {
// Find tab configuration
const tabConfig = this.tabs.find(tab => tab.id === tabId);
if (!tabConfig) {
return '<div style="padding: 16px; color: var(--vscode-text-dim);">Tab not found</div>';
}
// Dynamically create component based on configuration
const componentTag = tabConfig.component;
const propsString = Object.entries(tabConfig.props || {})
.map(([key, value]) => `${key}="${value}"`)
.join(' ');
return `<${componentTag} ${propsString}></${componentTag}>`;
}
// Public method for workdesks to update panel content
setTabContent(tabId, content) {
const tabContent = this.querySelector('#panel-tab-content');
if (this.currentTab === tabId && tabContent) {
if (typeof content === 'string') {
tabContent.innerHTML = content;
} else {
tabContent.innerHTML = '';
tabContent.appendChild(content);
}
}
}
}
customElements.define('ds-panel', DSPanel);

View File

@@ -1,380 +0,0 @@
/**
* ds-project-selector.js
* Project selector component for workdesk header
* MVP1: Enforces project selection before tools can be used
* FIXED: Now uses authenticated apiClient instead of direct fetch()
*/
import contextStore from '../../stores/context-store.js';
import apiClient from '../../services/api-client.js';
import { ComponentHelpers } from '../../utils/component-helpers.js';
class DSProjectSelector extends HTMLElement {
constructor() {
super();
this.projects = [];
this.isLoading = false;
this.selectedProject = contextStore.get('projectId');
}
async connectedCallback() {
this.render();
await this.loadProjects();
this.setupEventListeners();
// Subscribe to context changes
this.unsubscribe = contextStore.subscribeToKey('projectId', (newValue) => {
this.selectedProject = newValue;
this.updateSelectedDisplay();
});
// Bind auth change handler to this component
this.handleAuthChange = async (event) => {
console.log('[DSProjectSelector] Auth state changed, reloading projects');
await this.reloadProjects();
};
// Listen for custom auth-change events (fires when tokens are refreshed)
document.addEventListener('auth-change', this.handleAuthChange);
}
disconnectedCallback() {
if (this.unsubscribe) {
this.unsubscribe();
}
// Clean up auth change listener
if (this.handleAuthChange) {
document.removeEventListener('auth-change', this.handleAuthChange);
}
// Clean up document click listener for closing dropdown
if (this.closeDropdownHandler) {
document.removeEventListener('click', this.closeDropdownHandler);
}
}
async loadProjects() {
this.isLoading = true;
this.updateLoadingState();
try {
// Fetch projects from authenticated API client
// This ensures Authorization header is sent with the request
this.projects = await apiClient.getProjects();
console.log(`[DSProjectSelector] Loaded ${this.projects.length} projects`);
// If no project selected but we have projects, show prompt
if (!this.selectedProject && this.projects.length > 0) {
this.showProjectModal();
}
this.renderDropdown();
} catch (error) {
console.error('[DSProjectSelector] Failed to load projects:', error);
// Fallback: Create mock admin-ui project for development
this.projects = [{
id: 'admin-ui',
name: 'Admin UI (Default)',
description: 'Design System Server Admin UI'
}];
// Auto-select if no project selected
if (!this.selectedProject) {
try {
contextStore.setProject('admin-ui');
this.selectedProject = 'admin-ui';
} catch (storeError) {
console.error('[DSProjectSelector] Error setting project:', storeError);
this.selectedProject = 'admin-ui';
}
}
this.renderDropdown();
} finally {
this.isLoading = false;
this.updateLoadingState();
}
}
/**
* Public method to reload projects - called when auth state changes
*/
async reloadProjects() {
console.log('[DSProjectSelector] Reloading projects due to auth state change');
await this.loadProjects();
}
setupEventListeners() {
const button = this.querySelector('#project-selector-button');
const dropdown = this.querySelector('#project-dropdown');
if (button && dropdown) {
// Add click listener to button (delegation handles via event target check)
button.addEventListener('click', (e) => {
e.stopPropagation();
dropdown.style.display = dropdown.style.display === 'block' ? 'none' : 'block';
});
}
// Add click listeners to dropdown items
const projectOptions = this.querySelectorAll('.project-option');
projectOptions.forEach(option => {
option.addEventListener('click', (e) => {
e.stopPropagation();
const projectId = option.dataset.projectId;
this.selectProject(projectId);
});
});
// Close dropdown when clicking outside - stored for cleanup
if (!this.closeDropdownHandler) {
this.closeDropdownHandler = (e) => {
if (!this.contains(e.target) && dropdown) {
dropdown.style.display = 'none';
}
};
document.addEventListener('click', this.closeDropdownHandler);
}
}
selectProject(projectId) {
const project = this.projects.find(p => p.id === projectId);
if (!project) {
console.error('[DSProjectSelector] Project not found:', projectId);
return;
}
try {
contextStore.setProject(projectId);
this.selectedProject = projectId;
// Close dropdown
const dropdown = this.querySelector('#project-dropdown');
if (dropdown) {
dropdown.style.display = 'none';
}
this.updateSelectedDisplay();
ComponentHelpers.showToast?.(`Switched to project: ${project.name}`, 'success');
// Notify other components of project change
this.dispatchEvent(new CustomEvent('project-changed', {
detail: { projectId },
bubbles: true,
composed: true
}));
} catch (error) {
console.error('[DSProjectSelector] Error selecting project:', error);
ComponentHelpers.showToast?.(`Failed to select project: ${error.message}`, 'error');
}
}
showProjectModal() {
const modal = document.createElement('div');
modal.id = 'project-selection-modal';
modal.style.cssText = `
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.8);
display: flex;
align-items: center;
justify-content: center;
z-index: 10000;
`;
const content = document.createElement('div');
content.style.cssText = `
background: var(--vscode-sidebar);
border: 1px solid var(--vscode-border);
border-radius: 4px;
padding: 24px;
max-width: 500px;
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.5);
`;
// Use event delegation instead of attaching listeners to individual buttons
content.innerHTML = `
<h2 style="font-size: 16px; margin-bottom: 12px;">Select a Project</h2>
<p style="font-size: 12px; color: var(--vscode-text-dim); margin-bottom: 16px;">
Please select a project to start working. All tools require an active project.
</p>
<div style="display: flex; flex-direction: column; gap: 8px;" id="project-buttons-container">
${this.projects.map(project => `
<button
class="project-modal-button"
data-project-id="${project.id}"
type="button"
style="padding: 12px; background: var(--vscode-bg); border: 1px solid var(--vscode-border); border-radius: 4px; cursor: pointer; text-align: left; font-family: inherit; font-size: inherit;"
>
<div style="font-size: 12px; font-weight: 600;">${ComponentHelpers.escapeHtml(project.name)}</div>
${project.description ? `<div style="font-size: 11px; color: var(--vscode-text-dim); margin-top: 4px;">${ComponentHelpers.escapeHtml(project.description)}</div>` : ''}
</button>
`).join('')}
</div>
`;
modal.appendChild(content);
// Store reference to component for event handlers
const component = this;
// Use event delegation on content container
const buttonContainer = content.querySelector('#project-buttons-container');
if (buttonContainer) {
buttonContainer.addEventListener('click', (e) => {
const btn = e.target.closest('.project-modal-button');
if (btn) {
e.preventDefault();
e.stopPropagation();
const projectId = btn.dataset.projectId;
console.log('[DSProjectSelector] Modal button clicked:', projectId);
try {
component.selectProject(projectId);
console.log('[DSProjectSelector] Project selected successfully');
} catch (err) {
console.error('[DSProjectSelector] Error selecting project:', err);
} finally {
// Ensure modal is always removed
if (modal && modal.parentNode) {
modal.remove();
}
}
}
});
}
// Close modal when clicking outside the content area
modal.addEventListener('click', (e) => {
if (e.target === modal) {
console.log('[DSProjectSelector] Closing modal (clicked outside)');
modal.remove();
}
});
document.body.appendChild(modal);
console.log('[DSProjectSelector] Project selection modal shown');
}
updateSelectedDisplay() {
const button = this.querySelector('#project-selector-button');
if (!button) return;
const selectedProject = this.projects.find(p => p.id === this.selectedProject);
if (selectedProject) {
button.innerHTML = `
<span style="font-size: 11px; color: var(--vscode-text-dim);">Project:</span>
<span style="font-size: 12px; font-weight: 600; margin-left: 4px;">${ComponentHelpers.escapeHtml(selectedProject.name)}</span>
<span style="margin-left: 6px;">▼</span>
`;
} else {
button.innerHTML = `
<span style="font-size: 12px; color: var(--vscode-text-dim);">Select Project</span>
<span style="margin-left: 6px;">▼</span>
`;
}
}
updateLoadingState() {
const button = this.querySelector('#project-selector-button');
if (!button) return;
if (this.isLoading) {
button.disabled = true;
button.innerHTML = '<span style="font-size: 11px;">Loading projects...</span>';
} else {
button.disabled = false;
this.updateSelectedDisplay();
}
}
renderDropdown() {
const dropdown = this.querySelector('#project-dropdown');
if (!dropdown) return;
if (this.projects.length === 0) {
dropdown.innerHTML = `
<div style="padding: 12px; font-size: 11px; color: var(--vscode-text-dim);">
No projects available
</div>
`;
return;
}
dropdown.innerHTML = `
${this.projects.map(project => `
<div
class="project-option"
data-project-id="${project.id}"
style="
padding: 8px 12px;
cursor: pointer;
border-bottom: 1px solid var(--vscode-border);
${this.selectedProject === project.id ? 'background: var(--vscode-list-activeSelectionBackground);' : ''}
"
>
<div style="font-size: 12px; font-weight: 600;">
${this.selectedProject === project.id ? '✓ ' : ''}${ComponentHelpers.escapeHtml(project.name)}
</div>
${project.description ? `<div style="font-size: 10px; color: var(--vscode-text-dim); margin-top: 2px;">${ComponentHelpers.escapeHtml(project.description)}</div>` : ''}
</div>
`).join('')}
`;
// Re-attach event listeners to dropdown items
this.setupEventListeners();
}
render() {
this.innerHTML = `
<div style="position: relative; display: inline-block;">
<button
id="project-selector-button"
style="
display: flex;
align-items: center;
padding: 6px 12px;
background: var(--vscode-sidebar);
border: 1px solid var(--vscode-border);
border-radius: 4px;
cursor: pointer;
color: var(--vscode-text);
"
>
<span style="font-size: 12px;">Loading...</span>
</button>
<div
id="project-dropdown"
style="
display: none;
position: absolute;
top: 100%;
left: 0;
margin-top: 4px;
min-width: 250px;
background: var(--vscode-sidebar);
border: 1px solid var(--vscode-border);
border-radius: 4px;
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.3);
z-index: 1000;
max-height: 400px;
overflow-y: auto;
"
>
<!-- Projects will be populated here -->
</div>
</div>
`;
}
}
customElements.define('ds-project-selector', DSProjectSelector);
export default DSProjectSelector;

View File

@@ -1,755 +0,0 @@
/**
* ds-shell.js
* Main shell component - provides IDE-style grid layout
* MVP2: Integrated with AdminStore and ProjectStore for settings and project management
*/
import './ds-activity-bar.js';
import './ds-panel.js';
import './ds-project-selector.js';
import './ds-ai-chat-sidebar.js';
import '../admin/ds-user-settings.js'; // Import settings component for direct instantiation
import '../ds-notification-center.js'; // Notification center component
import router from '../../core/router.js'; // Import Router for new architecture
import layoutManager from '../../core/layout-manager.js';
import toolBridge from '../../services/tool-bridge.js';
import contextStore from '../../stores/context-store.js';
import notificationService from '../../services/notification-service.js';
import { useAdminStore } from '../../stores/admin-store.js';
import { useProjectStore } from '../../stores/project-store.js';
import { useUserStore } from '../../stores/user-store.js';
import '../../config/component-registry.js'; // Ensure all panel components are loaded
import { authReady } from '../../utils/demo-auth-init.js'; // Auth initialization promise
class DSShell extends HTMLElement {
constructor() {
super();
this.currentTeam = 'ui'; // Default team
this.currentWorkdesk = null;
this.browserInitialized = false;
this.currentView = 'workdesk'; // Can be 'workdesk' or 'settings'
// MVP2: Initialize stores
this.adminStore = useAdminStore();
this.projectStore = useProjectStore();
this.userStore = useUserStore();
// Bind event handlers to avoid memory leaks
this.handleHashChangeBound = this.handleHashChange.bind(this);
}
async connectedCallback() {
// Render UI immediately (non-blocking)
this.render();
this.setupEventListeners();
// Initialize layout manager
layoutManager.init(this);
// Initialize Router (NEW - Phase 1 Architecture)
router.init();
// Wait for authentication to complete before making API calls
console.log('[DSShell] Waiting for authentication...');
const authResult = await authReady;
console.log('[DSShell] Authentication complete:', authResult);
// MVP2: Initialize store subscriptions (now safe to make API calls)
this.initializeStoreSubscriptions();
// Initialize notification service
notificationService.init();
// Set initial active link
this.updateActiveLink();
}
/**
* Cleanup when component is removed from DOM (prevents memory leaks)
*/
disconnectedCallback() {
// Remove event listener to prevent memory leak
window.removeEventListener('hashchange', this.handleHashChangeBound);
}
/**
* MVP2: Setup store subscriptions to keep context in sync
*/
initializeStoreSubscriptions() {
// Subscribe to admin settings changes
this.adminStore.subscribe(() => {
const settings = this.adminStore.getState();
contextStore.updateAdminSettings({
hostname: settings.hostname,
port: settings.port,
isRemote: settings.isRemote,
dssSetupType: settings.dssSetupType
});
console.log('[DSShell] Admin settings updated:', settings);
});
// Subscribe to project changes
this.projectStore.subscribe(() => {
const currentProject = this.projectStore.getCurrentProject();
if (currentProject) {
contextStore.setCurrentProject(currentProject);
console.log('[DSShell] Project context updated:', currentProject);
}
});
// Set initial project context
const currentProject = this.projectStore.getCurrentProject();
if (currentProject) {
contextStore.setCurrentProject(currentProject);
}
}
/**
* Initialize browser automation (required for DevTools components)
*/
async initializeBrowser() {
if (this.browserInitialized) {
console.log('[DSShell] Browser already initialized');
return true;
}
console.log('[DSShell] Browser init temporarily disabled - not critical for development');
this.browserInitialized = true; // Mark as initialized to skip
return true;
/* DISABLED - MCP browser tools not available yet
try {
await toolBridge.executeTool('browser_init', {
mode: 'remote',
url: window.location.origin
});
this.browserInitialized = true;
console.log('[DSShell] Browser automation initialized successfully');
return true;
} catch (error) {
console.error('[DSShell] Failed to initialize browser:', error);
this.browserInitialized = false;
return false;
}
*/
}
render() {
this.innerHTML = `
<ds-sidebar>
<div class="sidebar-header" style="display: flex; flex-direction: column; gap: 8px; padding-bottom: 12px; border-bottom: 1px solid var(--vscode-border);">
<div style="display: flex; align-items: center; gap: 8px;">
<span style="font-size: 24px; font-weight: 700;">⬡</span>
<div>
<div style="font-size: 13px; font-weight: 700; color: var(--vscode-text);">DSS</div>
<div style="font-size: 10px; color: var(--vscode-text-dim); line-height: 1.2;">Design System Studio</div>
</div>
</div>
</div>
<!-- NEW: Feature Module Navigation -->
<div class="sidebar-content">
<nav class="module-nav" style="display: flex; flex-direction: column; gap: 4px; padding-top: 12px;">
<a href="#projects" class="nav-item" data-path="projects" style="display: flex; align-items: center; gap: 10px; padding: 8px 12px; color: var(--vscode-text-dim); text-decoration: none; border-radius: 4px; transition: all 0.1s; font-size: 13px;">
<span style="font-size: 16px;">📁</span> Projects
</a>
<a href="#config" class="nav-item" data-path="config" style="display: flex; align-items: center; gap: 10px; padding: 8px 12px; color: var(--vscode-text-dim); text-decoration: none; border-radius: 4px; transition: all 0.1s; font-size: 13px;">
<span style="font-size: 16px;">⚙️</span> Configuration
</a>
<a href="#components" class="nav-item" data-path="components" style="display: flex; align-items: center; gap: 10px; padding: 8px 12px; color: var(--vscode-text-dim); text-decoration: none; border-radius: 4px; transition: all 0.1s; font-size: 13px;">
<span style="font-size: 16px;">🧩</span> Components
</a>
<a href="#translations" class="nav-item" data-path="translations" style="display: flex; align-items: center; gap: 10px; padding: 8px 12px; color: var(--vscode-text-dim); text-decoration: none; border-radius: 4px; transition: all 0.1s; font-size: 13px;">
<span style="font-size: 16px;">🔄</span> Translations
</a>
<a href="#discovery" class="nav-item" data-path="discovery" style="display: flex; align-items: center; gap: 10px; padding: 8px 12px; color: var(--vscode-text-dim); text-decoration: none; border-radius: 4px; transition: all 0.1s; font-size: 13px;">
<span style="font-size: 16px;">🔍</span> Discovery
</a>
<div style="height: 1px; background: var(--vscode-border); margin: 8px 0;"></div>
<a href="#admin" class="nav-item" data-path="admin" style="display: flex; align-items: center; gap: 10px; padding: 8px 12px; color: var(--vscode-text-dim); text-decoration: none; border-radius: 4px; transition: all 0.1s; font-size: 13px;">
<span style="font-size: 16px;">👤</span> Admin
</a>
</nav>
</div>
</ds-sidebar>
<ds-stage>
<div class="stage-header" style="display: flex; justify-content: space-between; align-items: center; padding: 0 16px; border-bottom: 1px solid var(--vscode-border); background: var(--vscode-bg); min-height: 44px;">
<div class="stage-header-left" style="display: flex; align-items: center; gap: 12px;">
<!-- Hamburger Menu (Mobile) -->
<button id="hamburger-menu" class="hamburger-menu" style="display: none; padding: 6px 8px; background: transparent; border: none; color: var(--vscode-text-dim); cursor: pointer; font-size: 20px;" aria-label="Toggle sidebar">☰</button>
<!-- NEW: Project Selector -->
<ds-project-selector></ds-project-selector>
</div>
<div class="stage-header-right" id="stage-actions" style="display: flex; align-items: center; gap: 8px;">
<!-- Action buttons will be populated here -->
</div>
</div>
<div class="stage-content">
<div id="stage-workdesk-content" style="height: 100%; overflow: auto;">
<!-- Dynamic Module Content via Router -->
</div>
</div>
</ds-stage>
<ds-ai-chat-sidebar></ds-ai-chat-sidebar>
`;
}
setupEventListeners() {
// Setup hamburger menu for mobile
this.setupMobileMenu();
// Setup navigation highlight for new module nav
this.setupNavigationHighlight();
// Populate stage-header-right with action buttons
const stageActions = this.querySelector('#stage-actions');
if (stageActions && stageActions.children.length === 0) {
stageActions.innerHTML = `
<button id="chat-toggle-btn" aria-label="Toggle AI Chat sidebar" aria-pressed="false" style="
background: transparent;
border: none;
color: var(--vscode-text-dim);
cursor: pointer;
padding: 6px 8px;
font-size: 16px;
border-radius: 4px;
transition: all 0.1s;
" title="Toggle Chat (💬)">💬</button>
<button id="advanced-mode-btn" aria-label="Toggle Advanced Mode" aria-pressed="false" style="
background: transparent;
border: none;
color: var(--vscode-text-dim);
cursor: pointer;
padding: 6px 8px;
font-size: 16px;
border-radius: 4px;
transition: all 0.1s;
" title="Advanced Mode (🔧)">🔧</button>
<div style="position: relative;">
<button id="notification-toggle-btn" aria-label="Notifications" style="
background: transparent;
border: none;
color: var(--vscode-text-dim);
cursor: pointer;
padding: 6px 8px;
font-size: 16px;
border-radius: 4px;
transition: all 0.1s;
position: relative;
" title="Notifications (🔔)">🔔
<span id="notification-indicator" style="
position: absolute;
top: 4px;
right: 4px;
width: 8px;
height: 8px;
background: var(--vscode-accent);
border-radius: 50%;
display: none;
"></span>
</button>
<ds-notification-center></ds-notification-center>
</div>
<button id="settings-btn" aria-label="Open Settings" style="
background: transparent;
border: none;
color: var(--vscode-text-dim);
cursor: pointer;
padding: 6px 8px;
font-size: 16px;
border-radius: 4px;
transition: all 0.1s;
" title="Settings (⚙️)">⚙️</button>
`;
// Add event listeners to stage-header action buttons
const chatToggleBtn = this.querySelector('#chat-toggle-btn');
if (chatToggleBtn) {
chatToggleBtn.addEventListener('click', () => {
const chatSidebar = this.querySelector('ds-ai-chat-sidebar');
if (chatSidebar && chatSidebar.toggleCollapse) {
chatSidebar.toggleCollapse();
const pressed = chatSidebar.isCollapsed ? 'false' : 'true';
chatToggleBtn.setAttribute('aria-pressed', pressed);
}
});
chatToggleBtn.addEventListener('keydown', (e) => {
if (e.key === 'Enter' || e.key === ' ') {
e.preventDefault();
chatToggleBtn.click();
} else if (e.key === 'Escape') {
const chatSidebar = this.querySelector('ds-ai-chat-sidebar');
if (chatSidebar && !chatSidebar.isCollapsed) {
chatToggleBtn.click();
}
}
});
chatToggleBtn.addEventListener('mouseenter', (e) => {
e.target.style.color = 'var(--vscode-text)';
e.target.style.background = 'var(--vscode-selection)';
});
chatToggleBtn.addEventListener('mouseleave', (e) => {
e.target.style.color = 'var(--vscode-text-dim)';
e.target.style.background = 'transparent';
});
}
const advancedModeBtn = this.querySelector('#advanced-mode-btn');
if (advancedModeBtn) {
advancedModeBtn.addEventListener('click', () => {
this.toggleAdvancedMode();
});
advancedModeBtn.addEventListener('keydown', (e) => {
if (e.key === 'Enter' || e.key === ' ') {
e.preventDefault();
advancedModeBtn.click();
}
});
advancedModeBtn.addEventListener('mouseenter', (e) => {
e.target.style.color = 'var(--vscode-text)';
e.target.style.background = 'var(--vscode-selection)';
});
advancedModeBtn.addEventListener('mouseleave', (e) => {
e.target.style.color = 'var(--vscode-text-dim)';
e.target.style.background = 'transparent';
});
}
const settingsBtn = this.querySelector('#settings-btn');
if (settingsBtn) {
settingsBtn.addEventListener('click', () => {
this.openSettings();
});
settingsBtn.addEventListener('keydown', (e) => {
if (e.key === 'Enter' || e.key === ' ') {
e.preventDefault();
settingsBtn.click();
}
});
settingsBtn.addEventListener('mouseenter', (e) => {
e.target.style.color = 'var(--vscode-text)';
e.target.style.background = 'var(--vscode-selection)';
});
settingsBtn.addEventListener('mouseleave', (e) => {
e.target.style.color = 'var(--vscode-text-dim)';
e.target.style.background = 'transparent';
});
}
// Notification Center integration
const notificationToggleBtn = this.querySelector('#notification-toggle-btn');
const notificationCenter = this.querySelector('ds-notification-center');
const notificationIndicator = this.querySelector('#notification-indicator');
if (notificationToggleBtn && notificationCenter) {
// Toggle notification panel
notificationToggleBtn.addEventListener('click', (e) => {
e.stopPropagation();
const isOpen = notificationCenter.hasAttribute('open');
if (isOpen) {
notificationCenter.removeAttribute('open');
} else {
notificationCenter.setAttribute('open', '');
}
});
// Close when clicking outside
document.addEventListener('click', (e) => {
if (!notificationCenter.contains(e.target) && !notificationToggleBtn.contains(e.target)) {
notificationCenter.removeAttribute('open');
}
});
// Update unread indicator
notificationService.addEventListener('unread-count-changed', (e) => {
const { count } = e.detail;
if (notificationIndicator) {
notificationIndicator.style.display = count > 0 ? 'block' : 'none';
}
});
// Handle notification actions (navigation)
notificationCenter.addEventListener('notification-action', (e) => {
const { event, payload } = e.detail;
console.log('[DSShell] Notification action:', event, payload);
// Handle navigation events
if (event.startsWith('navigate:')) {
const page = event.replace('navigate:', '');
// Route to the appropriate page
// This would integrate with your routing system
console.log('[DSShell] Navigate to:', page, payload);
}
});
// Hover effects
notificationToggleBtn.addEventListener('mouseenter', (e) => {
e.target.style.color = 'var(--vscode-text)';
e.target.style.background = 'var(--vscode-selection)';
});
notificationToggleBtn.addEventListener('mouseleave', (e) => {
e.target.style.color = 'var(--vscode-text-dim)';
e.target.style.background = 'transparent';
});
}
}
// Add team button event listeners
const teamBtns = this.querySelectorAll('.team-btn');
teamBtns.forEach((btn, index) => {
btn.addEventListener('click', (e) => {
const teamId = e.target.dataset.team;
this.switchTeam(teamId);
});
// Keyboard navigation (Arrow keys)
btn.addEventListener('keydown', (e) => {
let nextBtn = null;
if (e.key === 'ArrowRight') {
e.preventDefault();
nextBtn = teamBtns[(index + 1) % teamBtns.length];
} else if (e.key === 'ArrowLeft') {
e.preventDefault();
nextBtn = teamBtns[(index - 1 + teamBtns.length) % teamBtns.length];
}
if (nextBtn) {
nextBtn.focus();
nextBtn.click();
}
});
// Hover effects
btn.addEventListener('mouseenter', (e) => {
e.target.style.color = 'var(--vscode-text)';
e.target.style.background = 'var(--vscode-selection)';
});
btn.addEventListener('mouseleave', (e) => {
// Keep accent color if this is the active team
if (e.target.classList.contains('active')) {
e.target.style.color = 'var(--vscode-accent)';
e.target.style.background = 'var(--vscode-selection)';
} else {
e.target.style.color = 'var(--vscode-text-dim)';
e.target.style.background = 'transparent';
}
});
});
// Set initial active team button
this.updateTeamButtonStates();
}
updateTeamButtonStates() {
const teamBtns = this.querySelectorAll('.team-btn');
teamBtns.forEach(btn => {
if (btn.dataset.team === this.currentTeam) {
btn.classList.add('active');
btn.setAttribute('aria-selected', 'true');
btn.style.color = 'var(--vscode-accent)';
btn.style.background = 'var(--vscode-selection)';
btn.style.borderColor = 'var(--vscode-accent)';
} else {
btn.classList.remove('active');
btn.setAttribute('aria-selected', 'false');
btn.style.color = 'var(--vscode-text-dim)';
btn.style.background = 'transparent';
btn.style.borderColor = 'transparent';
}
});
}
setupMobileMenu() {
const hamburgerBtn = this.querySelector('#hamburger-menu');
const sidebar = this.querySelector('ds-sidebar');
if (hamburgerBtn) {
hamburgerBtn.addEventListener('click', () => {
if (sidebar) {
const isOpen = sidebar.classList.contains('mobile-open');
if (isOpen) {
sidebar.classList.remove('mobile-open');
hamburgerBtn.setAttribute('aria-expanded', 'false');
} else {
sidebar.classList.add('mobile-open');
hamburgerBtn.setAttribute('aria-expanded', 'true');
}
}
});
hamburgerBtn.addEventListener('keydown', (e) => {
if (e.key === 'Enter' || e.key === ' ') {
e.preventDefault();
hamburgerBtn.click();
}
});
}
// Close sidebar when clicking on a team button (mobile)
const teamBtns = this.querySelectorAll('.team-btn');
teamBtns.forEach(btn => {
btn.addEventListener('click', () => {
if (sidebar && window.innerWidth <= 768) {
sidebar.classList.remove('mobile-open');
if (hamburgerBtn) {
hamburgerBtn.setAttribute('aria-expanded', 'false');
}
}
});
});
// Show/hide hamburger menu based on screen size
const updateMenuVisibility = () => {
if (hamburgerBtn) {
if (window.innerWidth <= 768) {
hamburgerBtn.style.display = 'flex';
} else {
hamburgerBtn.style.display = 'none';
if (sidebar) {
sidebar.classList.remove('mobile-open');
}
}
}
};
updateMenuVisibility();
window.addEventListener('resize', updateMenuVisibility);
}
toggleAdvancedMode() {
// Get activity bar for advanced mode state (or create local tracking)
const activityBar = this.querySelector('ds-activity-bar');
let advancedMode = false;
if (activityBar && activityBar.advancedMode !== undefined) {
advancedMode = !activityBar.advancedMode;
activityBar.advancedMode = advancedMode;
activityBar.saveAdvancedMode();
} else {
// Fallback: use localStorage directly
advancedMode = localStorage.getItem('dss-advanced-mode') !== 'true';
localStorage.setItem('dss-advanced-mode', advancedMode.toString());
}
this.onAdvancedModeChange(advancedMode);
// Update button appearance and accessibility state
const advancedModeBtn = this.querySelector('#advanced-mode-btn');
if (advancedModeBtn) {
advancedModeBtn.setAttribute('aria-pressed', advancedMode.toString());
advancedModeBtn.style.color = advancedMode ? 'var(--vscode-accent)' : 'var(--vscode-text-dim)';
}
}
onAdvancedModeChange(advancedMode) {
console.log(`Advanced mode: ${advancedMode ? 'ON' : 'OFF'}`);
// Reconfigure panel with new advanced mode setting
const panel = this.querySelector('ds-panel');
if (panel) {
panel.configure(this.currentTeam, advancedMode);
}
}
async switchTeam(teamId) {
console.log(`Switching to team: ${teamId}`);
this.currentTeam = teamId;
// Persist team selection to userStore
this.userStore.updatePreferences({ lastTeam: teamId });
// Update team button states
this.updateTeamButtonStates();
// Update stage title
const stageTitle = this.querySelector('#stage-title');
if (stageTitle) {
stageTitle.textContent = `${teamId.toUpperCase()} Workdesk`;
}
// Apply admin-mode class for full-page layout
if (teamId === 'admin') {
this.classList.add('admin-mode');
// Initialize browser automation for admin team (needed for DevTools components)
this.initializeBrowser().catch(error => {
console.warn('[DSShell] Browser initialization failed (non-blocking):', error.message);
});
} else {
this.classList.remove('admin-mode');
}
// Configure panel for this team
const panel = this.querySelector('ds-panel');
const activityBar = this.querySelector('ds-activity-bar');
if (panel) {
// Get advancedMode from activity bar
const advancedMode = activityBar?.advancedMode || false;
panel.configure(teamId, advancedMode);
}
// Use layout manager to switch workdesk
try {
this.currentWorkdesk = await layoutManager.switchWorkdesk(teamId);
} catch (error) {
console.error(`Failed to load workdesk for team ${teamId}:`, error);
// Show error in stage
const stageContent = this.querySelector('#stage-workdesk-content');
if (stageContent) {
stageContent.innerHTML = `
<div style="text-align: center; padding: 48px; color: #f48771;">
<h2>Failed to load ${teamId.toUpperCase()} Workdesk</h2>
<p style="margin-top: 16px;">Error: ${error.message}</p>
</div>
`;
}
}
}
/**
* Open user settings view
*/
async openSettings() {
this.currentView = 'settings';
const stageContent = this.querySelector('#stage-workdesk-content');
const stageTitle = this.querySelector('#stage-title');
if (stageTitle) {
stageTitle.textContent = '⚙️ Settings';
}
if (stageContent) {
// Clear existing content
stageContent.innerHTML = '';
// Create and append user settings component
const settingsComponent = document.createElement('ds-user-settings');
stageContent.appendChild(settingsComponent);
}
// Hide sidebar and minimize panel for full-width settings
const sidebar = this.querySelector('ds-sidebar');
const panel = this.querySelector('ds-panel');
if (sidebar) {
sidebar.classList.add('collapsed');
}
if (panel) {
panel.classList.add('collapsed');
}
console.log('[DSShell] Settings view opened');
}
/**
* Close settings view and return to workdesk
*/
closeSettings() {
if (this.currentView === 'settings') {
this.currentView = 'workdesk';
// Restore sidebar and panel
const sidebar = this.querySelector('ds-sidebar');
const panel = this.querySelector('ds-panel');
if (sidebar) {
sidebar.classList.remove('collapsed');
}
if (panel) {
panel.classList.remove('collapsed');
}
// Reload current team's workdesk
this.switchTeam(this.currentTeam);
}
}
setupNavigationHighlight() {
// Use requestAnimationFrame to ensure DOM is ready (fixes race condition)
requestAnimationFrame(() => {
const navItems = this.querySelectorAll('.nav-item');
if (navItems.length === 0) {
console.warn('[DSShell] No nav items found for highlight setup');
return;
}
navItems.forEach(item => {
item.addEventListener('mouseenter', (e) => {
if (!e.target.classList.contains('active')) {
e.target.style.background = 'var(--vscode-list-hoverBackground, rgba(255,255,255,0.1))';
e.target.style.color = 'var(--vscode-text)';
}
});
item.addEventListener('mouseleave', (e) => {
if (!e.target.classList.contains('active')) {
e.target.style.background = 'transparent';
e.target.style.color = 'var(--vscode-text-dim)';
}
});
});
// Use bound handler to enable proper cleanup (fixes memory leak)
window.addEventListener('hashchange', this.handleHashChangeBound);
});
}
/**
* Handle hash change events (bound in constructor for proper cleanup)
*/
handleHashChange() {
this.updateActiveLink();
}
updateActiveLink(path) {
const currentPath = path || (window.location.hash.replace('#', '') || 'projects');
const navItems = this.querySelectorAll('.nav-item');
navItems.forEach(item => {
const itemPath = item.dataset.path;
if (itemPath === currentPath) {
item.classList.add('active');
item.style.background = 'var(--vscode-list-activeSelectionBackground, var(--vscode-selection))';
item.style.color = 'var(--vscode-list-activeSelectionForeground, var(--vscode-accent))';
item.style.fontWeight = '500';
} else {
item.classList.remove('active');
item.style.background = 'transparent';
item.style.color = 'var(--vscode-text-dim)';
item.style.fontWeight = 'normal';
}
});
}
// Getters for workdesk components to access
get sidebarContent() {
return this.querySelector('#sidebar-workdesk-content');
}
get stageContent() {
return this.querySelector('#stage-workdesk-content');
}
get stageActions() {
return this.querySelector('#stage-actions');
}
}
// Define custom element
customElements.define('ds-shell', DSShell);
// Also define the sidebar and stage as custom elements for CSS targeting
class DSSidebar extends HTMLElement {}
class DSStage extends HTMLElement {}
customElements.define('ds-sidebar', DSSidebar);
customElements.define('ds-stage', DSStage);

View File

@@ -1,190 +0,0 @@
/**
* 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

@@ -1,249 +0,0 @@
/**
* 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

@@ -1,293 +0,0 @@
/**
* 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

@@ -1,197 +0,0 @@
/**
* 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);

View File

@@ -1,203 +0,0 @@
/**
* ds-frontpage.js
* Front page component for team workdesks
* Refactored: Shadow DOM, extracted styles, uses ds-metric-card
*/
import contextStore from '../../stores/context-store.js';
import './ds-metric-card.js'; // Import the new reusable component
export default class Frontpage extends HTMLElement {
constructor() {
super();
this.attachShadow({ mode: 'open' }); // Enable Shadow DOM
this.state = {
teamName: 'Team',
metrics: {}
};
}
connectedCallback() {
this.render();
this.setupEventListeners();
// Subscribe to context changes
this.unsubscribe = contextStore.subscribe(({ state }) => {
this.state.teamId = state.teamId;
const teamNames = {
'ui': 'UI Team',
'ux': 'UX Team',
'qa': 'QA Team',
'admin': 'Admin'
};
this.state.teamName = teamNames[state.teamId] || 'Team';
this.updateTeamName();
});
}
disconnectedCallback() {
if (this.unsubscribe) this.unsubscribe();
}
render() {
this.shadowRoot.innerHTML = `
<style>
:host {
display: block;
height: 100%;
font-family: var(--vscode-font-family);
color: var(--vscode-foreground);
}
.container {
padding: 24px;
height: 100%;
overflow-y: auto;
box-sizing: border-box;
}
h1 {
margin: 0 0 8px 0;
font-size: 24px;
font-weight: 500;
}
.description {
margin: 0 0 32px 0;
color: var(--vscode-descriptionForeground);
font-size: 14px;
}
.metrics-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 16px;
margin-bottom: 32px;
}
.quick-actions {
background: var(--vscode-sidebar-background);
border: 1px solid var(--vscode-widget-border);
border-radius: 4px;
padding: 20px;
}
h2 {
margin: 0 0 16px 0;
font-size: 16px;
font-weight: 600;
}
.actions-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
gap: 12px;
}
.action-btn {
padding: 12px;
background: var(--vscode-button-secondaryBackground);
color: var(--vscode-button-secondaryForeground);
border: 1px solid transparent;
border-radius: 4px;
cursor: pointer;
font-size: 12px;
font-weight: 500;
text-align: center;
transition: background-color 0.1s;
}
.action-btn:hover {
background: var(--vscode-button-secondaryHoverBackground);
}
.action-btn:focus-visible {
outline: 2px solid var(--vscode-focusBorder);
outline-offset: 2px;
}
</style>
<div class="container">
<div>
<h1 id="team-name">Team Dashboard</h1>
<p class="description">
Overview of design system adoption and metrics for your team
</p>
</div>
<!-- Metrics Cards using reusable component -->
<div class="metrics-grid">
<ds-metric-card
title="Adoption Rate"
value="68%"
subtitle="of team using DS"
color="#4caf50">
</ds-metric-card>
<ds-metric-card
title="Components"
value="45/65"
subtitle="in use"
color="#2196f3">
</ds-metric-card>
<ds-metric-card
title="Tokens"
value="187"
subtitle="managed"
color="#ff9800">
</ds-metric-card>
<ds-metric-card
title="Last Update"
value="2 hours"
subtitle="ago"
color="#9c27b0">
</ds-metric-card>
</div>
<!-- Quick Actions -->
<div class="quick-actions">
<h2>Quick Actions</h2>
<div class="actions-grid">
<button class="action-btn" data-action="components" type="button">
📦 View Components
</button>
<button class="action-btn" data-action="tokens" type="button">
🎨 Manage Tokens
</button>
<button class="action-btn" data-action="icons" type="button">
✨ View Icons
</button>
<button class="action-btn" data-action="jira" type="button">
🐛 Jira Issues
</button>
</div>
</div>
</div>
`;
this.updateTeamName();
}
updateTeamName() {
// Select from Shadow DOM
const teamNameEl = this.shadowRoot.querySelector('#team-name');
if (teamNameEl) {
teamNameEl.textContent = `${this.state.teamName} Dashboard`;
}
}
setupEventListeners() {
// Listen within Shadow DOM
const buttons = this.shadowRoot.querySelectorAll('.action-btn');
buttons.forEach(btn => {
btn.addEventListener('click', (e) => {
const action = btn.dataset.action;
this.handleQuickAction(action);
});
});
}
handleQuickAction(action) {
console.log(`Quick action triggered: ${action}`);
// Events bubble out of Shadow DOM if composed: true
this.dispatchEvent(new CustomEvent('quick-action', {
detail: { action },
bubbles: true,
composed: true
}));
}
}
customElements.define('ds-frontpage', Frontpage);

View File

@@ -1,84 +0,0 @@
/**
* ds-metric-card.js
* Reusable web component for displaying dashboard metrics
* Encapsulates styling and layout for consistency across dashboards
*/
export default class MetricCard extends HTMLElement {
static get observedAttributes() {
return ['title', 'value', 'subtitle', 'color', 'trend'];
}
constructor() {
super();
this.attachShadow({ mode: 'open' });
}
connectedCallback() {
this.render();
}
attributeChangedCallback(name, oldValue, newValue) {
if (oldValue !== newValue) {
this.render();
}
}
render() {
const title = this.getAttribute('title') || '';
const value = this.getAttribute('value') || '0';
const subtitle = this.getAttribute('subtitle') || '';
const color = this.getAttribute('color') || 'var(--vscode-textLink-foreground)';
// Trend implementation (optional enhancement)
const trend = this.getAttribute('trend'); // e.g., "up", "down"
this.shadowRoot.innerHTML = `
<style>
:host {
display: block;
height: 100%;
}
.card {
background: var(--vscode-sidebar-background);
border: 1px solid var(--vscode-widget-border);
border-radius: 4px;
padding: 16px;
height: 100%;
box-sizing: border-box;
border-top: 3px solid var(--card-color, ${color});
transition: transform 0.1s ease-in-out;
}
.card:hover {
background: var(--vscode-list-hoverBackground);
}
.header {
color: var(--vscode-descriptionForeground);
font-size: 11px;
margin-bottom: 8px;
text-transform: uppercase;
letter-spacing: 0.5px;
font-weight: 600;
}
.value {
font-size: 28px;
font-weight: 600;
margin-bottom: 4px;
color: var(--card-color, ${color});
}
.subtitle {
color: var(--vscode-descriptionForeground);
font-size: 11px;
line-height: 1.4;
}
</style>
<div class="card" style="--card-color: ${color}">
<div class="header">${title}</div>
<div class="value">${value}</div>
<div class="subtitle">${subtitle}</div>
</div>
`;
}
}
customElements.define('ds-metric-card', MetricCard);

View File

@@ -1,204 +0,0 @@
/**
* ds-metrics-dashboard.js
* Metrics dashboard for design system adoption and health
* Shows key metrics like component adoption rate, token usage, etc.
*/
import store from '../../stores/app-store.js';
export default class MetricsDashboard extends HTMLElement {
constructor() {
super();
this.isLoading = true;
this.error = null;
this.metrics = {
adoptionRate: 0,
componentsUsed: 0,
totalComponents: 0,
tokensCovered: 0,
teamsActive: 0,
averageUpdateFreq: 'N/A'
};
}
connectedCallback() {
this.render();
this.loadMetrics();
}
async loadMetrics() {
this.isLoading = true;
this.error = null;
this.render();
try {
const response = await fetch('/api/discovery/stats');
if (!response.ok) {
throw new Error(`API Error: ${response.status}`);
}
const json = await response.json();
if (json.status === 'success' && json.data) {
const stats = json.data;
// Map backend field names to component properties
this.metrics = {
adoptionRate: stats.adoption_percentage || 0,
componentsUsed: stats.components_in_use || 0,
totalComponents: stats.total_components || 0,
tokensCovered: stats.tokens_count || 0,
teamsActive: stats.active_projects || 0,
averageUpdateFreq: stats.avg_update_days
? `${stats.avg_update_days} days`
: 'N/A'
};
} else {
throw new Error(json.message || 'Invalid response format');
}
} catch (error) {
console.error('Failed to load metrics:', error);
this.error = error.message;
} finally {
this.isLoading = false;
this.render();
}
}
render() {
if (this.isLoading) {
this.innerHTML = `
<div style="padding: 24px; height: 100%; display: flex; align-items: center; justify-content: center;">
<div style="text-align: center;">
<div style="font-size: 14px; color: var(--vscode-text-dim);">Loading metrics...</div>
</div>
</div>
`;
return;
}
if (this.error) {
this.innerHTML = `
<div style="padding: 24px; height: 100%; display: flex; align-items: center; justify-content: center;">
<div style="text-align: center;">
<div style="font-size: 14px; color: var(--vscode-error); margin-bottom: 12px;">
Failed to load metrics: ${this.error}
</div>
<button
onclick="document.querySelector('ds-metrics-dashboard').loadMetrics()"
style="
padding: 8px 16px;
background: var(--vscode-button);
color: var(--vscode-button-fg);
border: none;
border-radius: 4px;
cursor: pointer;
"
>
Retry
</button>
</div>
</div>
`;
return;
}
this.innerHTML = `
<div style="padding: 24px; height: 100%; overflow-y: auto;">
<h1 style="margin-bottom: 8px; font-size: 24px;">Design System Metrics</h1>
<p style="color: var(--vscode-text-dim); margin-bottom: 32px;">
Track adoption, health, and usage of your design system
</p>
<!-- Metrics Grid -->
<div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(250px, 1fr)); gap: 16px; margin-bottom: 32px;">
${this.renderMetricCard('Adoption Rate', `${this.metrics.adoptionRate}%`, '#4caf50', 'Percentage of team using DS')}
${this.renderMetricCard('Components in Use', this.metrics.componentsUsed, '#2196f3', `of ${this.metrics.totalComponents} total`)}
${this.renderMetricCard('Design Tokens', this.metrics.tokensCovered, '#ff9800', 'Total tokens managed')}
${this.renderMetricCard('Active Projects', this.metrics.teamsActive, '#9c27b0', 'Projects in system')}
</div>
<!-- Activity Timeline -->
<div style="background: var(--vscode-sidebar); border: 1px solid var(--vscode-border); border-radius: 4px; padding: 20px; margin-bottom: 24px;">
<h2 style="margin-top: 0; margin-bottom: 16px; font-size: 16px;">Recent Activity</h2>
<div style="font-size: 12px;">
<div style="padding: 8px 0; border-bottom: 1px solid var(--vscode-border);">
<div style="display: flex; justify-content: space-between; margin-bottom: 4px;">
<span style="font-weight: 500;">Component Library Updated</span>
<span style="color: var(--vscode-text-dim);">2 hours ago</span>
</div>
<div style="color: var(--vscode-text-dim); font-size: 11px;">Added 3 new components to Button family</div>
</div>
<div style="padding: 8px 0; border-bottom: 1px solid var(--vscode-border);">
<div style="display: flex; justify-content: space-between; margin-bottom: 4px;">
<span style="font-weight: 500;">Tokens Synchronized</span>
<span style="color: var(--vscode-text-dim);">6 hours ago</span>
</div>
<div style="color: var(--vscode-text-dim); font-size: 11px;">Synced 42 color tokens from Figma</div>
</div>
<div style="padding: 8px 0;">
<div style="display: flex; justify-content: space-between; margin-bottom: 4px;">
<span style="font-weight: 500;">Team Onboarded</span>
<span style="color: var(--vscode-text-dim);">1 day ago</span>
</div>
<div style="color: var(--vscode-text-dim); font-size: 11px;">Marketing team completed DS training</div>
</div>
</div>
</div>
<!-- Health Indicators -->
<div style="background: var(--vscode-sidebar); border: 1px solid var(--vscode-border); border-radius: 4px; padding: 20px;">
<h2 style="margin-top: 0; margin-bottom: 16px; font-size: 16px;">System Health</h2>
<div style="font-size: 12px;">
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 12px;">
<span>Component Coverage</span>
<div style="width: 150px; height: 6px; background: var(--vscode-bg); border-radius: 3px; overflow: hidden;">
<div style="width: 69%; height: 100%; background: #4caf50;"></div>
</div>
<span>69%</span>
</div>
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 12px;">
<span>Token Coverage</span>
<div style="width: 150px; height: 6px; background: var(--vscode-bg); border-radius: 3px; overflow: hidden;">
<div style="width: 85%; height: 100%; background: #2196f3;"></div>
</div>
<span>85%</span>
</div>
<div style="display: flex; justify-content: space-between; align-items: center;">
<span>Documentation</span>
<div style="width: 150px; height: 6px; background: var(--vscode-bg); border-radius: 3px; overflow: hidden;">
<div style="width: 92%; height: 100%; background: #ff9800;"></div>
</div>
<span>92%</span>
</div>
</div>
</div>
</div>
`;
}
renderMetricCard(title, value, color, subtitle) {
return `
<div style="
background: var(--vscode-sidebar);
border: 1px solid var(--vscode-border);
border-radius: 4px;
padding: 16px;
border-top: 3px solid ${color};
">
<div style="color: var(--vscode-text-dim); font-size: 11px; margin-bottom: 8px; text-transform: uppercase; letter-spacing: 0.5px;">
${title}
</div>
<div style="font-size: 32px; font-weight: 600; margin-bottom: 4px; color: ${color};">
${value}
</div>
<div style="color: var(--vscode-text-dim); font-size: 11px;">
${subtitle}
</div>
</div>
`;
}
}
customElements.define('ds-metrics-dashboard', MetricsDashboard);

View File

@@ -1,249 +0,0 @@
/**
* ds-accessibility-report.js
* Accessibility audit report using axe-core via MCP browser tools
*/
import toolBridge from '../../services/tool-bridge.js';
import { ComponentHelpers } from '../../utils/component-helpers.js';
class DSAccessibilityReport extends HTMLElement {
constructor() {
super();
this.auditResult = null;
this.selector = null;
this.isRunning = false;
}
connectedCallback() {
this.render();
this.setupEventListeners();
}
setupEventListeners() {
const runBtn = this.querySelector('#a11y-run-btn');
if (runBtn) {
runBtn.addEventListener('click', () => this.runAudit());
}
const selectorInput = this.querySelector('#a11y-selector');
if (selectorInput) {
selectorInput.addEventListener('change', (e) => {
this.selector = e.target.value.trim() || null;
});
}
}
async runAudit() {
if (this.isRunning) return;
this.isRunning = true;
const content = this.querySelector('#a11y-content');
const runBtn = this.querySelector('#a11y-run-btn');
if (!content) {
this.isRunning = false;
return;
}
if (runBtn) {
runBtn.disabled = true;
runBtn.textContent = 'Running Audit...';
}
content.innerHTML = ComponentHelpers.renderLoading('Running accessibility audit with axe-core...');
try {
const result = await toolBridge.runAccessibilityAudit(this.selector);
if (result) {
this.auditResult = result;
this.renderResults();
} else {
content.innerHTML = ComponentHelpers.renderEmpty('No audit results returned', '🔍');
}
} catch (error) {
console.error('Failed to run accessibility audit:', error);
content.innerHTML = ComponentHelpers.renderError('Failed to run accessibility audit', error);
} finally {
this.isRunning = false;
if (runBtn) {
runBtn.disabled = false;
runBtn.textContent = '▶ Run Audit';
}
}
}
getSeverityIcon(impact) {
const icons = {
critical: '🔴',
serious: '🟠',
moderate: '🟡',
minor: '🔵'
};
return icons[impact] || '⚪';
}
getSeverityBadge(impact) {
const types = {
critical: 'error',
serious: 'error',
moderate: 'warning',
minor: 'info'
};
return ComponentHelpers.createBadge(impact.toUpperCase(), types[impact] || 'info');
}
renderResults() {
const content = this.querySelector('#a11y-content');
if (!content || !this.auditResult) return;
const violations = this.auditResult.violations || [];
const passes = this.auditResult.passes || [];
const incomplete = this.auditResult.incomplete || [];
const inapplicable = this.auditResult.inapplicable || [];
const totalViolations = violations.length;
const totalPasses = passes.length;
const totalTests = totalViolations + totalPasses + incomplete.length + inapplicable.length;
if (totalViolations === 0) {
content.innerHTML = `
<div style="text-align: center; padding: 48px;">
<div style="font-size: 64px; margin-bottom: 16px;">✅</div>
<h3 style="font-size: 18px; margin-bottom: 8px; color: #89d185;">No Violations Found!</h3>
<p style="font-size: 12px; color: var(--vscode-text-dim);">
All ${totalPasses} accessibility tests passed.
</p>
</div>
`;
return;
}
const violationCards = violations.map((violation, index) => {
const impact = violation.impact || 'unknown';
const nodes = violation.nodes || [];
const nodeCount = nodes.length;
return `
<div style="background-color: var(--vscode-sidebar); border: 1px solid var(--vscode-border); border-left: 3px solid ${this.getImpactColor(impact)}; border-radius: 4px; padding: 16px; margin-bottom: 12px;">
<div style="display: flex; justify-content: space-between; align-items: start; margin-bottom: 12px;">
<div style="flex: 1;">
<div style="display: flex; align-items: center; gap: 8px; margin-bottom: 6px;">
<span style="font-size: 20px;">${this.getSeverityIcon(impact)}</span>
<h4 style="font-size: 13px; font-weight: 600;">${ComponentHelpers.escapeHtml(violation.description || violation.id)}</h4>
</div>
<div style="font-size: 11px; color: var(--vscode-text-dim); margin-bottom: 8px;">
Rule: <span style="font-family: 'Courier New', monospace;">${ComponentHelpers.escapeHtml(violation.id)}</span>
</div>
</div>
${this.getSeverityBadge(impact)}
</div>
<div style="font-size: 12px; margin-bottom: 12px; padding: 12px; background-color: var(--vscode-bg); border-radius: 2px;">
${ComponentHelpers.escapeHtml(violation.help || 'No help text available')}
</div>
<div style="margin-bottom: 8px;">
<div style="font-size: 11px; color: var(--vscode-text-dim); margin-bottom: 4px;">
Affected elements: ${nodeCount}
</div>
${nodes.slice(0, 3).map(node => `
<div style="margin-bottom: 6px; padding: 8px; background-color: var(--vscode-bg); border-radius: 2px; font-size: 11px;">
<div style="font-family: 'Courier New', monospace; color: var(--vscode-accent); margin-bottom: 4px;">
${ComponentHelpers.escapeHtml(ComponentHelpers.truncateText(node.target ? node.target.join(', ') : 'unknown', 80))}
</div>
${node.failureSummary ? `
<div style="color: var(--vscode-text-dim); font-size: 10px;">
${ComponentHelpers.escapeHtml(ComponentHelpers.truncateText(node.failureSummary, 150))}
</div>
` : ''}
</div>
`).join('')}
${nodeCount > 3 ? `<div style="font-size: 10px; color: var(--vscode-text-dim); margin-top: 4px;">... and ${nodeCount - 3} more</div>` : ''}
</div>
${violation.helpUrl ? `
<a href="${ComponentHelpers.escapeHtml(violation.helpUrl)}" target="_blank" style="font-size: 11px; color: var(--vscode-accent); text-decoration: none;">
Learn more →
</a>
` : ''}
</div>
`;
}).join('');
content.innerHTML = `
<!-- Summary Card -->
<div style="background-color: var(--vscode-sidebar); border: 1px solid var(--vscode-border); border-radius: 4px; padding: 16px; margin-bottom: 16px;">
<h3 style="font-size: 14px; font-weight: 600; margin-bottom: 12px;">Audit Summary</h3>
<div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(120px, 1fr)); gap: 12px;">
<div style="text-align: center; padding: 12px; background-color: var(--vscode-bg); border-radius: 4px;">
<div style="font-size: 24px; font-weight: 600; color: #f48771;">${totalViolations}</div>
<div style="font-size: 11px; color: var(--vscode-text-dim); margin-top: 4px;">Violations</div>
</div>
<div style="text-align: center; padding: 12px; background-color: var(--vscode-bg); border-radius: 4px;">
<div style="font-size: 24px; font-weight: 600; color: #89d185;">${totalPasses}</div>
<div style="font-size: 11px; color: var(--vscode-text-dim); margin-top: 4px;">Passes</div>
</div>
<div style="text-align: center; padding: 12px; background-color: var(--vscode-bg); border-radius: 4px;">
<div style="font-size: 24px; font-weight: 600; color: var(--vscode-accent);">${totalTests}</div>
<div style="font-size: 11px; color: var(--vscode-text-dim); margin-top: 4px;">Total Tests</div>
</div>
</div>
</div>
<!-- Violations List -->
<div style="margin-bottom: 12px;">
<h3 style="font-size: 14px; font-weight: 600; margin-bottom: 12px;">Violations (${totalViolations})</h3>
${violationCards}
</div>
<!-- Timestamp -->
<div style="font-size: 11px; color: var(--vscode-text-dim); text-align: center; padding-top: 8px; border-top: 1px solid var(--vscode-border);">
Audit completed: ${ComponentHelpers.formatTimestamp(new Date())}
${this.selector ? ` • Scoped to: ${ComponentHelpers.escapeHtml(this.selector)}` : ' • Full page scan'}
</div>
`;
}
getImpactColor(impact) {
const colors = {
critical: '#f48771',
serious: '#dbb765',
moderate: '#dbb765',
minor: '#75beff'
};
return colors[impact] || '#858585';
}
render() {
this.innerHTML = `
<div style="padding: 16px; height: 100%; display: flex; flex-direction: column;">
<div style="margin-bottom: 16px; display: flex; gap: 12px; align-items: center;">
<input
type="text"
id="a11y-selector"
placeholder="Optional: CSS selector to scope audit"
class="input"
style="flex: 1; min-width: 200px;"
/>
<button id="a11y-run-btn" class="button" style="padding: 4px 12px; font-size: 11px;">
▶ Run Audit
</button>
</div>
<div id="a11y-content" style="flex: 1; overflow-y: auto;">
<div style="text-align: center; padding: 48px; color: var(--vscode-text-dim);">
<div style="font-size: 48px; margin-bottom: 16px;">♿</div>
<h3 style="font-size: 14px; margin-bottom: 8px;">Accessibility Audit</h3>
<p style="font-size: 12px;">
Click "Run Audit" to scan for WCAG violations using axe-core.
</p>
</div>
</div>
</div>
`;
}
}
customElements.define('ds-accessibility-report', DSAccessibilityReport);
export default DSAccessibilityReport;

View File

@@ -1,442 +0,0 @@
/**
* ds-activity-log.js
* Activity log showing recent MCP tool executions and user actions
*
* REFACTORED: DSS-compliant version using DSBaseTool + table-template.js
* - Extends DSBaseTool for Shadow DOM, AbortController, and standardized lifecycle
* - Uses table-template.js for DSS-compliant table rendering (NO inline events/styles)
* - Event delegation pattern for all interactions
* - Logger utility instead of console.*
*
* Reference: .knowledge/dss-coding-standards.json
*/
import DSBaseTool from '../base/ds-base-tool.js';
import toolBridge from '../../services/tool-bridge.js';
import { ComponentHelpers } from '../../utils/component-helpers.js';
import { logger } from '../../utils/logger.js';
import { createTableView, setupTableEvents, createStatsCard } from '../../templates/table-template.js';
class DSActivityLog extends DSBaseTool {
constructor() {
super();
this.activities = [];
this.maxActivities = 100;
this.autoRefresh = false;
this.refreshInterval = null;
// Listen for tool executions
this.originalExecuteTool = toolBridge.executeTool.bind(toolBridge);
this.setupToolInterceptor();
}
connectedCallback() {
super.connectedCallback();
this.loadActivities();
}
disconnectedCallback() {
if (this.refreshInterval) {
clearInterval(this.refreshInterval);
}
super.disconnectedCallback();
}
setupToolInterceptor() {
// Intercept tool executions to log them
toolBridge.executeTool = async (toolName, params) => {
const startTime = Date.now();
const activity = {
id: Date.now() + Math.random(),
type: 'tool_execution',
toolName,
params,
timestamp: new Date(),
status: 'running'
};
this.addActivity(activity);
try {
const result = await this.originalExecuteTool(toolName, params);
const duration = Date.now() - startTime;
activity.status = 'success';
activity.duration = duration;
activity.result = result;
this.updateActivity(activity);
return result;
} catch (error) {
const duration = Date.now() - startTime;
activity.status = 'error';
activity.duration = duration;
activity.error = error.message;
this.updateActivity(activity);
throw error;
}
};
}
addActivity(activity) {
this.activities.unshift(activity);
if (this.activities.length > this.maxActivities) {
this.activities.pop();
}
this.saveActivities();
this.renderActivities();
}
updateActivity(activity) {
const index = this.activities.findIndex(a => a.id === activity.id);
if (index !== -1) {
this.activities[index] = activity;
this.saveActivities();
this.renderActivities();
}
}
saveActivities() {
try {
localStorage.setItem('ds-activity-log', JSON.stringify(this.activities.slice(0, 50)));
} catch (e) {
logger.warn('[DSActivityLog] Failed to save activities to localStorage', e);
}
}
loadActivities() {
try {
const stored = localStorage.getItem('ds-activity-log');
if (stored) {
this.activities = JSON.parse(stored).map(a => ({
...a,
timestamp: new Date(a.timestamp)
}));
this.renderActivities();
logger.debug('[DSActivityLog] Loaded activities from localStorage', { count: this.activities.length });
}
} catch (e) {
logger.warn('[DSActivityLog] Failed to load activities from localStorage', e);
}
}
clearActivities() {
this.activities = [];
this.saveActivities();
this.renderActivities();
logger.info('[DSActivityLog] Activities cleared');
}
/**
* Render the component (required by DSBaseTool)
*/
render() {
this.shadowRoot.innerHTML = `
<style>
:host {
display: block;
height: 100%;
}
.activity-log-container {
padding: 16px;
height: 100%;
display: flex;
flex-direction: column;
}
.log-controls {
margin-bottom: 16px;
display: flex;
gap: 12px;
align-items: center;
justify-content: flex-end;
}
.auto-refresh-label {
display: flex;
align-items: center;
gap: 6px;
font-size: 12px;
color: var(--vscode-foreground);
cursor: pointer;
}
.clear-btn {
padding: 6px 12px;
font-size: 11px;
background: var(--vscode-button-background);
color: var(--vscode-button-foreground);
border: none;
border-radius: 2px;
cursor: pointer;
transition: background 0.15s ease;
}
.clear-btn:hover {
background: var(--vscode-button-hoverBackground);
}
.content-wrapper {
flex: 1;
overflow: auto;
}
.loading {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 48px;
color: var(--vscode-descriptionForeground);
}
/* Badge styles */
.badge {
display: inline-block;
padding: 2px 6px;
border-radius: 3px;
font-size: 10px;
font-weight: 600;
}
.badge-info {
background: rgba(75, 181, 211, 0.2);
color: #4bb5d3;
}
.badge-running {
background: rgba(75, 181, 211, 0.2);
color: #4bb5d3;
}
.badge-success {
background: rgba(137, 209, 133, 0.2);
color: #89d185;
}
.badge-error {
background: rgba(244, 135, 113, 0.2);
color: #f48771;
}
.code {
font-family: 'Courier New', monospace;
word-break: break-all;
}
.icon-cell {
font-size: 16px;
text-align: center;
}
.tool-name {
font-family: 'Courier New', monospace;
color: var(--vscode-textLink-foreground);
}
.error-box {
background-color: rgba(244, 135, 113, 0.1);
padding: 8px;
border-radius: 2px;
color: #f48771;
font-size: 11px;
}
.hint {
margin-top: 12px;
padding: 8px;
background-color: var(--vscode-sideBar-background);
border-radius: 4px;
font-size: 10px;
color: var(--vscode-descriptionForeground);
}
</style>
<div class="activity-log-container">
<!-- Log Controls -->
<div class="log-controls">
<label class="auto-refresh-label">
<input type="checkbox" id="activity-auto-refresh" />
Live updates
</label>
<button
id="activity-clear-btn"
data-action="clear"
class="clear-btn"
type="button"
aria-label="Clear activity log">
🗑️ Clear Log
</button>
</div>
<!-- Content -->
<div class="content-wrapper" id="activity-content">
<div class="loading">Loading activities...</div>
</div>
</div>
`;
}
/**
* Setup event listeners (required by DSBaseTool)
*/
setupEventListeners() {
// EVENT-002: Event delegation
this.delegateEvents('.activity-log-container', 'click', (action, e) => {
if (action === 'clear') {
this.clearActivities();
}
});
// Auto-refresh toggle
const autoRefreshToggle = this.$('#activity-auto-refresh');
if (autoRefreshToggle) {
this.bindEvent(autoRefreshToggle, 'change', (e) => {
this.autoRefresh = e.target.checked;
if (this.autoRefresh) {
this.refreshInterval = setInterval(() => this.renderActivities(), 1000);
logger.debug('[DSActivityLog] Auto-refresh enabled');
} else {
if (this.refreshInterval) {
clearInterval(this.refreshInterval);
this.refreshInterval = null;
logger.debug('[DSActivityLog] Auto-refresh disabled');
}
}
});
}
}
getActivityIcon(activity) {
if (activity.status === 'running') return '🔄';
if (activity.status === 'success') return '✅';
if (activity.status === 'error') return '❌';
return '⚪';
}
renderActivities() {
const content = this.$('#activity-content');
if (!content) return;
if (this.activities.length === 0) {
content.innerHTML = '<div class="table-empty"><div class="table-empty-icon">📋</div><div class="table-empty-text">No recent activity</div></div>';
return;
}
// Calculate stats
const stats = {
Total: this.activities.length,
Success: this.activities.filter(a => a.status === 'success').length,
Failed: this.activities.filter(a => a.status === 'error').length
};
const running = this.activities.filter(a => a.status === 'running').length;
if (running > 0) {
stats.Running = running;
}
// Render stats card
const statsHtml = createStatsCard(stats);
// Use table-template.js for DSS-compliant rendering
const { html: tableHtml, styles: tableStyles } = createTableView({
columns: [
{ header: '', key: 'icon', width: '40px', align: 'center' },
{ header: 'Status', key: 'status', width: '80px', align: 'left' },
{ header: 'Tool', key: 'toolName', align: 'left' },
{ header: 'Duration', key: 'duration', width: '100px', align: 'left' },
{ header: 'Time', key: 'timestamp', width: '120px', align: 'left' }
],
rows: this.activities,
renderCell: (col, row) => this.renderCell(col, row),
renderDetails: (row) => this.renderDetails(row),
emptyMessage: 'No recent activity',
emptyIcon: '📋'
});
// Adopt table styles
this.adoptStyles(tableStyles);
// Render table
content.innerHTML = statsHtml + tableHtml + '<div class="hint">💡 Click any row to view full activity details</div>';
// Setup table event handlers
setupTableEvents(this.shadowRoot);
logger.debug('[DSActivityLog] Rendered activities', { count: this.activities.length });
}
renderCell(col, row) {
const icon = this.getActivityIcon(row);
const toolName = row.toolName || 'Unknown';
const duration = row.duration ? ComponentHelpers.formatDuration(row.duration) : '-';
const timestamp = ComponentHelpers.formatRelativeTime(row.timestamp);
switch (col.key) {
case 'icon':
return `<span class="icon-cell">${icon}</span>`;
case 'status':
return `<span class="badge badge-${row.status}">${this.escapeHtml(row.status)}</span>`;
case 'toolName':
return `<span class="tool-name">${this.escapeHtml(toolName)}</span>`;
case 'duration':
return `<span style="color: var(--vscode-descriptionForeground);">${duration}</span>`;
case 'timestamp':
return `<span style="color: var(--vscode-descriptionForeground);">${timestamp}</span>`;
default:
return this.escapeHtml(String(row[col.key] || '-'));
}
}
renderDetails(row) {
const toolName = row.toolName || 'Unknown';
const duration = row.duration ? ComponentHelpers.formatDuration(row.duration) : '-';
return `
<div style="margin-bottom: 12px;">
<span class="detail-label">Tool:</span>
<span class="detail-value code">${this.escapeHtml(toolName)}</span>
</div>
<div style="margin-bottom: 12px;">
<span class="detail-label">Status:</span>
<span class="badge badge-${row.status}">${this.escapeHtml(row.status)}</span>
</div>
<div style="margin-bottom: 12px;">
<span class="detail-label">Timestamp:</span>
<span>${ComponentHelpers.formatTimestamp(row.timestamp)}</span>
</div>
${row.duration ? `
<div style="margin-bottom: 12px;">
<span class="detail-label">Duration:</span>
<span>${duration}</span>
</div>
` : ''}
${row.params && Object.keys(row.params).length > 0 ? `
<div style="margin-bottom: 12px;">
<div class="detail-label" style="display: block; margin-bottom: 4px;">Parameters:</div>
<pre class="detail-code">${this.escapeHtml(JSON.stringify(row.params, null, 2))}</pre>
</div>
` : ''}
${row.error ? `
<div style="margin-bottom: 12px;">
<div class="detail-label" style="display: block; margin-bottom: 4px;">Error:</div>
<div class="error-box">
${this.escapeHtml(row.error)}
</div>
</div>
` : ''}
`;
}
}
customElements.define('ds-activity-log', DSActivityLog);
export default DSActivityLog;

View File

@@ -1,100 +0,0 @@
/**
* ds-asset-list.js
* List view of design assets (icons, images, etc.)
* UX Team Tool #3
*/
import { createGalleryView, setupGalleryHandlers } from '../../utils/tool-templates.js';
import { ComponentHelpers } from '../../utils/component-helpers.js';
import contextStore from '../../stores/context-store.js';
class DSAssetList extends HTMLElement {
constructor() {
super();
this.assets = [];
this.isLoading = false;
}
async connectedCallback() {
this.render();
await this.loadAssets();
}
async loadAssets() {
this.isLoading = true;
const container = this.querySelector('#asset-list-container');
if (container) {
container.innerHTML = ComponentHelpers.renderLoading('Loading design assets...');
}
try {
const context = contextStore.getMCPContext();
if (!context.project_id) {
throw new Error('No project selected');
}
// Call assets API
const response = await fetch(`/api/assets/list?projectId=${context.project_id}`);
if (!response.ok) {
throw new Error(`Failed to load assets: ${response.statusText}`);
}
const result = await response.json();
this.assets = result.assets || [];
this.renderAssetGallery();
} catch (error) {
console.error('[DSAssetList] Failed to load assets:', error);
if (container) {
container.innerHTML = ComponentHelpers.renderError('Failed to load assets', error);
}
} finally {
this.isLoading = false;
}
}
renderAssetGallery() {
const container = this.querySelector('#asset-list-container');
if (!container) return;
const config = {
title: 'Design Assets',
items: this.assets.map(asset => ({
id: asset.id,
src: asset.url || asset.thumbnailUrl,
title: asset.name,
subtitle: `${asset.type}${asset.size || 'N/A'}`
})),
onItemClick: (item) => this.viewAsset(item),
onDelete: (item) => this.deleteAsset(item)
};
container.innerHTML = createGalleryView(config);
setupGalleryHandlers(container, config);
}
viewAsset(item) {
// Open asset in new tab or modal
if (item.src) {
window.open(item.src, '_blank');
}
}
deleteAsset(item) {
ComponentHelpers.showToast?.(`Deleted ${item.title}`, 'success');
this.assets = this.assets.filter(a => a.id !== item.id);
this.renderAssetGallery();
}
render() {
this.innerHTML = `
<div id="asset-list-container" style="height: 100%; overflow: hidden;">
${ComponentHelpers.renderLoading('Loading assets...')}
</div>
`;
}
}
customElements.define('ds-asset-list', DSAssetList);
export default DSAssetList;

View File

@@ -1,355 +0,0 @@
/**
* ds-chat-panel.js
* AI chatbot panel with team+project context
* MVP1: Integrates claude-service with ContextStore for team-aware assistance
*/
import claudeService from '../../services/claude-service.js';
import contextStore from '../../stores/context-store.js';
import { ComponentHelpers } from '../../utils/component-helpers.js';
class DSChatPanel extends HTMLElement {
constructor() {
super();
this.messages = [];
this.isLoading = false;
}
async connectedCallback() {
// Sync claude-service with ContextStore
const context = contextStore.getMCPContext();
if (context.project_id) {
claudeService.setProject(context.project_id);
}
// Subscribe to project changes
this.unsubscribe = contextStore.subscribeToKey('projectId', (newProjectId) => {
if (newProjectId) {
claudeService.setProject(newProjectId);
this.showSystemMessage(`Switched to project: ${newProjectId}`);
}
});
// Initialize MCP tools in background
this.initializeMcpTools();
this.render();
this.setupEventListeners();
this.loadHistory();
this.showWelcomeMessage();
}
/**
* Initialize MCP tools to enable AI tool awareness
*/
async initializeMcpTools() {
try {
console.log('[DSChatPanel] Initializing MCP tools...');
await claudeService.getMcpTools();
console.log('[DSChatPanel] MCP tools initialized successfully');
} catch (error) {
console.warn('[DSChatPanel] Failed to load MCP tools (non-blocking):', error.message);
}
}
/**
* Set context from parent component (ds-ai-chat-sidebar)
* @param {Object} context - Context object with project, team, page
*/
setContext(context) {
if (!context) return;
// Handle project context (could be object with id or string id)
if (context.project) {
const projectId = typeof context.project === 'object'
? context.project.id
: context.project;
if (projectId && projectId !== claudeService.currentProjectId) {
claudeService.setProject(projectId);
console.log('[DSChatPanel] Context updated via setContext:', { projectId });
}
}
// Store team and page context for reference
if (context.team) {
this.currentTeam = context.team;
}
if (context.page) {
this.currentPage = context.page;
}
}
disconnectedCallback() {
if (this.unsubscribe) {
this.unsubscribe();
}
}
setupEventListeners() {
const input = this.querySelector('#chat-input');
const sendBtn = this.querySelector('#chat-send-btn');
const clearBtn = this.querySelector('#chat-clear-btn');
const exportBtn = this.querySelector('#chat-export-btn');
if (sendBtn && input) {
sendBtn.addEventListener('click', () => this.sendMessage());
input.addEventListener('keypress', (e) => {
if (e.key === 'Enter' && !e.shiftKey) {
e.preventDefault();
this.sendMessage();
}
});
}
if (clearBtn) {
clearBtn.addEventListener('click', () => this.clearChat());
}
if (exportBtn) {
exportBtn.addEventListener('click', () => this.exportChat());
}
}
loadHistory() {
const history = claudeService.getHistory();
if (history && history.length > 0) {
this.messages = history;
this.renderMessages();
}
}
showWelcomeMessage() {
if (this.messages.length === 0) {
const context = contextStore.getMCPContext();
const teamId = context.team_id || 'ui';
const teamGreetings = {
ui: 'I can help with token extraction, component audits, Storybook comparisons, and quick wins analysis.',
ux: 'I can assist with Figma syncing, design tokens, asset management, and navigation flows.',
qa: 'I can help with visual regression testing, accessibility audits, and ESRE validation.',
admin: 'I can help manage projects, configure integrations, and oversee the design system.'
};
const greeting = teamGreetings[teamId] || teamGreetings.admin;
this.showSystemMessage(
`👋 Welcome to the ${teamId.toUpperCase()} team workspace!\n\n${greeting}\n\nI have access to all MCP tools for the active project.`
);
}
}
showSystemMessage(text) {
this.messages.push({
role: 'system',
content: text,
timestamp: new Date().toISOString()
});
this.renderMessages();
}
async sendMessage() {
const input = this.querySelector('#chat-input');
const message = input?.value.trim();
if (!message || this.isLoading) return;
// Check project context
const context = contextStore.getMCPContext();
if (!context.project_id) {
ComponentHelpers.showToast?.('Please select a project before chatting', 'error');
return;
}
// Add user message
this.messages.push({
role: 'user',
content: message,
timestamp: new Date().toISOString()
});
// Clear input
input.value = '';
// Render and scroll
this.renderMessages();
this.scrollToBottom();
// Show loading
this.isLoading = true;
this.updateLoadingState();
try {
// Add team context to the request
const teamContext = {
projectId: context.project_id,
teamId: context.team_id,
userId: context.user_id,
page: 'workdesk',
capabilities: context.capabilities
};
// Send to Claude with team+project context
const response = await claudeService.chat(message, teamContext);
// Add assistant response
this.messages.push({
role: 'assistant',
content: response,
timestamp: new Date().toISOString()
});
this.renderMessages();
this.scrollToBottom();
} catch (error) {
console.error('[DSChatPanel] Failed to send message:', error);
ComponentHelpers.showToast?.(`Chat error: ${error.message}`, 'error');
this.messages.push({
role: 'system',
content: `❌ Error: ${error.message}\n\nPlease try again or check your connection.`,
timestamp: new Date().toISOString()
});
this.renderMessages();
} finally {
this.isLoading = false;
this.updateLoadingState();
}
}
clearChat() {
if (!confirm('Clear all chat history?')) return;
claudeService.clearHistory();
this.messages = [];
this.renderMessages();
this.showWelcomeMessage();
ComponentHelpers.showToast?.('Chat history cleared', 'success');
}
exportChat() {
claudeService.exportConversation();
ComponentHelpers.showToast?.('Chat exported successfully', 'success');
}
updateLoadingState() {
const sendBtn = this.querySelector('#chat-send-btn');
const input = this.querySelector('#chat-input');
if (sendBtn) {
sendBtn.disabled = this.isLoading;
sendBtn.textContent = this.isLoading ? '⏳ Sending...' : '📤 Send';
}
if (input) {
input.disabled = this.isLoading;
}
}
scrollToBottom() {
const messagesContainer = this.querySelector('#chat-messages');
if (messagesContainer) {
setTimeout(() => {
messagesContainer.scrollTop = messagesContainer.scrollHeight;
}, 100);
}
}
renderMessages() {
const messagesContainer = this.querySelector('#chat-messages');
if (!messagesContainer) return;
if (this.messages.length === 0) {
messagesContainer.innerHTML = `
<div style="text-align: center; padding: 48px; color: var(--vscode-text-dim);">
<div style="font-size: 48px; margin-bottom: 16px;">💬</div>
<h3 style="font-size: 14px; font-weight: 600; margin-bottom: 8px;">No messages yet</h3>
<p style="font-size: 12px;">Start a conversation to get help with your design system.</p>
</div>
`;
return;
}
messagesContainer.innerHTML = this.messages.map(msg => {
const isUser = msg.role === 'user';
const isSystem = msg.role === 'system';
const alignStyle = isUser ? 'flex-end' : 'flex-start';
const bgColor = isUser
? 'var(--vscode-button-background)'
: isSystem
? 'rgba(255, 191, 0, 0.1)'
: 'var(--vscode-sidebar)';
const textColor = isUser ? 'var(--vscode-button-foreground)' : 'var(--vscode-text)';
const maxWidth = isSystem ? '100%' : '80%';
const icon = isUser ? '👤' : isSystem ? '' : '🤖';
return `
<div style="display: flex; justify-content: ${alignStyle}; margin-bottom: 16px;">
<div style="max-width: ${maxWidth}; background: ${bgColor}; padding: 12px; border-radius: 8px; color: ${textColor};">
<div style="font-size: 10px; color: var(--vscode-text-dim); margin-bottom: 6px; display: flex; align-items: center; gap: 6px;">
<span>${icon}</span>
<span>${isUser ? 'You' : isSystem ? 'System' : 'AI Assistant'}</span>
<span>•</span>
<span>${ComponentHelpers.formatRelativeTime(new Date(msg.timestamp))}</span>
</div>
<div style="font-size: 12px; white-space: pre-wrap; word-wrap: break-word;">
${ComponentHelpers.escapeHtml(msg.content)}
</div>
</div>
</div>
`;
}).join('');
}
render() {
this.innerHTML = `
<div style="height: 100%; display: flex; flex-direction: column; background: var(--vscode-bg);">
<!-- Header -->
<div style="padding: 12px 16px; border-bottom: 1px solid var(--vscode-border); display: flex; justify-content: space-between; align-items: center;">
<div>
<h3 style="font-size: 12px; font-weight: 600; margin-bottom: 4px;">AI Assistant</h3>
<div style="font-size: 10px; color: var(--vscode-text-dim);">Team-contextualized help with MCP tools</div>
</div>
<div style="display: flex; gap: 8px;">
<button id="chat-export-btn" class="button" style="padding: 4px 8px; font-size: 10px;">
📥 Export
</button>
<button id="chat-clear-btn" class="button" style="padding: 4px 8px; font-size: 10px;">
🗑️ Clear
</button>
</div>
</div>
<!-- Messages -->
<div id="chat-messages" style="flex: 1; overflow-y: auto; padding: 16px;">
${ComponentHelpers.renderLoading('Loading chat...')}
</div>
<!-- Input -->
<div style="padding: 16px; border-top: 1px solid var(--vscode-border);">
<div style="display: flex; gap: 8px;">
<textarea
id="chat-input"
placeholder="Ask me anything about your design system..."
class="input"
style="flex: 1; min-height: 60px; resize: vertical; font-size: 12px;"
rows="2"
></textarea>
<button id="chat-send-btn" class="button" style="padding: 8px 16px; font-size: 12px; height: 60px;">
📤 Send
</button>
</div>
<div style="font-size: 10px; color: var(--vscode-text-dim); margin-top: 8px;">
Press Enter to send • Shift+Enter for new line
</div>
</div>
</div>
`;
}
}
customElements.define('ds-chat-panel', DSChatPanel);
export default DSChatPanel;

View File

@@ -1,170 +0,0 @@
/**
* ds-component-list.js
* List view of all design system components
* UX Team Tool #4
*/
import { createListView, setupListHandlers } from '../../utils/tool-templates.js';
import { ComponentHelpers } from '../../utils/component-helpers.js';
import contextStore from '../../stores/context-store.js';
import toolBridge from '../../services/tool-bridge.js';
class DSComponentList extends HTMLElement {
constructor() {
super();
this.components = [];
this.filteredComponents = [];
this.isLoading = false;
}
async connectedCallback() {
this.render();
await this.loadComponents();
}
async loadComponents() {
this.isLoading = true;
const container = this.querySelector('#component-list-container');
if (container) {
container.innerHTML = ComponentHelpers.renderLoading('Loading components...');
}
try {
const context = contextStore.getMCPContext();
if (!context.project_id) {
throw new Error('No project selected');
}
// Call component audit to get component list
const result = await toolBridge.executeTool('dss_audit_components', {
path: `/projects/${context.project_id}`
});
this.components = result.components || [];
this.filteredComponents = [...this.components];
this.renderComponentList();
} catch (error) {
console.error('[DSComponentList] Failed to load components:', error);
if (container) {
container.innerHTML = ComponentHelpers.renderError('Failed to load components', error);
}
} finally {
this.isLoading = false;
}
}
renderComponentList() {
const container = this.querySelector('#component-list-container');
if (!container) return;
const config = {
title: 'Design System Components',
items: this.filteredComponents,
columns: [
{
key: 'name',
label: 'Component',
render: (comp) => `<span style="font-family: monospace; font-size: 11px; font-weight: 600;">${ComponentHelpers.escapeHtml(comp.name)}</span>`
},
{
key: 'path',
label: 'File Path',
render: (comp) => `<span style="font-family: monospace; font-size: 10px; color: var(--vscode-text-dim);">${ComponentHelpers.escapeHtml(comp.path)}</span>`
},
{
key: 'type',
label: 'Type',
render: (comp) => ComponentHelpers.createBadge(comp.type || 'react', 'info')
},
{
key: 'dsAdoption',
label: 'DS Adoption',
render: (comp) => {
const percentage = comp.dsAdoption || 0;
let color = '#f48771';
if (percentage >= 80) color = '#89d185';
else if (percentage >= 50) color = '#ffbf00';
return `
<div style="display: flex; align-items: center; gap: 8px;">
<div style="flex: 1; height: 6px; background: var(--vscode-bg); border-radius: 3px; overflow: hidden;">
<div style="height: 100%; width: ${percentage}%; background: ${color};"></div>
</div>
<span style="font-size: 10px; font-weight: 600; min-width: 35px;">${percentage}%</span>
</div>
`;
}
}
],
actions: [
{
label: 'Refresh',
icon: '🔄',
onClick: () => this.loadComponents()
},
{
label: 'Export Report',
icon: '📥',
onClick: () => this.exportReport()
}
],
onSearch: (query) => this.handleSearch(query),
onFilter: (filterValue) => this.handleFilter(filterValue)
};
container.innerHTML = createListView(config);
setupListHandlers(container, config);
// Update filter dropdown
const filterSelect = container.querySelector('#filter-select');
if (filterSelect) {
const types = [...new Set(this.components.map(c => c.type || 'react'))];
filterSelect.innerHTML = `
<option value="">All Types</option>
${types.map(type => `<option value="${type}">${type}</option>`).join('')}
`;
}
}
handleSearch(query) {
const lowerQuery = query.toLowerCase();
this.filteredComponents = this.components.filter(comp =>
comp.name.toLowerCase().includes(lowerQuery) ||
comp.path.toLowerCase().includes(lowerQuery)
);
this.renderComponentList();
}
handleFilter(filterValue) {
if (!filterValue) {
this.filteredComponents = [...this.components];
} else {
this.filteredComponents = this.components.filter(comp => (comp.type || 'react') === filterValue);
}
this.renderComponentList();
}
exportReport() {
const data = JSON.stringify(this.components, null, 2);
const blob = new Blob([data], { type: 'application/json' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = 'component-audit.json';
a.click();
URL.revokeObjectURL(url);
ComponentHelpers.showToast?.('Report exported', 'success');
}
render() {
this.innerHTML = `
<div id="component-list-container" style="height: 100%; overflow: hidden;">
${ComponentHelpers.renderLoading('Loading components...')}
</div>
`;
}
}
customElements.define('ds-component-list', DSComponentList);
export default DSComponentList;

View File

@@ -1,249 +0,0 @@
/**
* ds-console-viewer.js
* Console log viewer with real-time streaming and filtering
*/
import toolBridge from '../../services/tool-bridge.js';
class DSConsoleViewer extends HTMLElement {
constructor() {
super();
this.logs = [];
this.currentFilter = 'all';
this.autoScroll = true;
this.refreshInterval = null;
}
connectedCallback() {
this.render();
this.startAutoRefresh();
}
disconnectedCallback() {
this.stopAutoRefresh();
}
render() {
const filteredLogs = this.currentFilter === 'all'
? this.logs
: this.logs.filter(log => log.level === this.currentFilter);
this.innerHTML = `
<div style="display: flex; flex-direction: column; height: 100%;">
<!-- Header Controls -->
<div style="
display: flex;
justify-content: space-between;
align-items: center;
padding: 8px 12px;
border-bottom: 1px solid var(--vscode-border);
background-color: var(--vscode-sidebar);
">
<div style="display: flex; gap: 8px;">
<button class="filter-btn ${this.currentFilter === 'all' ? 'active' : ''}" data-filter="all" style="
padding: 4px 12px;
background-color: ${this.currentFilter === 'all' ? 'var(--vscode-accent)' : 'transparent'};
border: 1px solid var(--vscode-border);
color: var(--vscode-text);
border-radius: 2px;
cursor: pointer;
font-size: 11px;
">All</button>
<button class="filter-btn ${this.currentFilter === 'log' ? 'active' : ''}" data-filter="log" style="
padding: 4px 12px;
background-color: ${this.currentFilter === 'log' ? 'var(--vscode-accent)' : 'transparent'};
border: 1px solid var(--vscode-border);
color: var(--vscode-text);
border-radius: 2px;
cursor: pointer;
font-size: 11px;
">Log</button>
<button class="filter-btn ${this.currentFilter === 'warn' ? 'active' : ''}" data-filter="warn" style="
padding: 4px 12px;
background-color: ${this.currentFilter === 'warn' ? 'var(--vscode-accent)' : 'transparent'};
border: 1px solid var(--vscode-border);
color: var(--vscode-text);
border-radius: 2px;
cursor: pointer;
font-size: 11px;
">Warn</button>
<button class="filter-btn ${this.currentFilter === 'error' ? 'active' : ''}" data-filter="error" style="
padding: 4px 12px;
background-color: ${this.currentFilter === 'error' ? 'var(--vscode-accent)' : 'transparent'};
border: 1px solid var(--vscode-border);
color: var(--vscode-text);
border-radius: 2px;
cursor: pointer;
font-size: 11px;
">Error</button>
</div>
<div style="display: flex; gap: 8px; align-items: center;">
<label style="font-size: 11px; display: flex; align-items: center; gap: 4px; cursor: pointer;">
<input type="checkbox" id="auto-scroll-toggle" ${this.autoScroll ? 'checked' : ''}>
Auto-scroll
</label>
<button id="clear-logs-btn" class="button" style="padding: 4px 12px; font-size: 11px;">
Clear
</button>
<button id="refresh-logs-btn" class="button" style="padding: 4px 12px; font-size: 11px;">
Refresh
</button>
</div>
</div>
<!-- Console Output -->
<div id="console-output" style="
flex: 1;
overflow-y: auto;
padding: 8px;
font-family: 'Consolas', 'Monaco', monospace;
font-size: 12px;
background-color: var(--vscode-bg);
">
${filteredLogs.length === 0 ? `
<div style="padding: 16px; text-align: center; color: var(--vscode-text-dim);">
No console logs${this.currentFilter !== 'all' ? ` (${this.currentFilter})` : ''}
</div>
` : filteredLogs.map(log => this.renderLogEntry(log)).join('')}
</div>
</div>
`;
this.setupEventListeners();
if (this.autoScroll) {
this.scrollToBottom();
}
}
renderLogEntry(log) {
const levelColors = {
log: 'var(--vscode-text)',
warn: '#ff9800',
error: '#f44336',
info: '#2196f3',
debug: 'var(--vscode-text-dim)'
};
const color = levelColors[log.level] || 'var(--vscode-text)';
return `
<div style="
padding: 4px 8px;
border-bottom: 1px solid var(--vscode-border);
display: flex;
gap: 12px;
">
<span style="color: var(--vscode-text-dim); min-width: 80px; flex-shrink: 0;">
${log.timestamp}
</span>
<span style="color: ${color}; font-weight: 600; min-width: 50px; flex-shrink: 0;">
[${log.level.toUpperCase()}]
</span>
<span style="color: var(--vscode-text); flex: 1; word-break: break-word;">
${this.escapeHtml(log.message)}
</span>
</div>
`;
}
setupEventListeners() {
// Filter buttons
this.querySelectorAll('.filter-btn').forEach(btn => {
btn.addEventListener('click', (e) => {
this.currentFilter = e.target.dataset.filter;
this.render();
});
});
// Auto-scroll toggle
const autoScrollToggle = this.querySelector('#auto-scroll-toggle');
if (autoScrollToggle) {
autoScrollToggle.addEventListener('change', (e) => {
this.autoScroll = e.target.checked;
});
}
// Clear button
const clearBtn = this.querySelector('#clear-logs-btn');
if (clearBtn) {
clearBtn.addEventListener('click', () => {
this.logs = [];
this.render();
});
}
// Refresh button
const refreshBtn = this.querySelector('#refresh-logs-btn');
if (refreshBtn) {
refreshBtn.addEventListener('click', () => {
this.fetchLogs();
});
}
}
async fetchLogs() {
try {
const result = await toolBridge.getBrowserLogs(this.currentFilter, 100);
if (result && result.logs) {
this.logs = result.logs.map(log => ({
timestamp: new Date(log.timestamp).toLocaleTimeString(),
level: log.level || 'log',
message: log.message || JSON.stringify(log)
}));
this.render();
}
} catch (error) {
console.error('Failed to fetch logs:', error);
this.addLog('error', `Failed to fetch logs: ${error.message}`);
}
}
addLog(level, message) {
const now = new Date();
this.logs.push({
timestamp: now.toLocaleTimeString(),
level,
message
});
// Keep only last 100 logs
if (this.logs.length > 100) {
this.logs = this.logs.slice(-100);
}
this.render();
}
startAutoRefresh() {
// Fetch logs every 2 seconds
this.fetchLogs();
this.refreshInterval = setInterval(() => {
this.fetchLogs();
}, 2000);
}
stopAutoRefresh() {
if (this.refreshInterval) {
clearInterval(this.refreshInterval);
this.refreshInterval = null;
}
}
scrollToBottom() {
const output = this.querySelector('#console-output');
if (output) {
output.scrollTop = output.scrollHeight;
}
}
escapeHtml(text) {
const div = document.createElement('div');
div.textContent = text;
return div.innerHTML;
}
}
customElements.define('ds-console-viewer', DSConsoleViewer);
export default DSConsoleViewer;

View File

@@ -1,233 +0,0 @@
/**
* ds-esre-editor.js
* Editor for ESRE (Explicit Style Requirements and Expectations)
* QA Team Tool #2
*/
import { createEditorView, setupEditorHandlers } from '../../utils/tool-templates.js';
import { ComponentHelpers } from '../../utils/component-helpers.js';
import contextStore from '../../stores/context-store.js';
class DSESREEditor extends HTMLElement {
constructor() {
super();
this.esreContent = '';
this.isSaving = false;
}
async connectedCallback() {
this.render();
await this.loadESRE();
}
async loadESRE() {
try {
const context = contextStore.getMCPContext();
if (!context.project_id) {
throw new Error('No project selected');
}
// Load ESRE from project configuration
const response = await fetch(`/api/projects/${context.project_id}/esre`);
if (response.ok) {
const result = await response.json();
this.esreContent = result.content || '';
this.renderEditor();
} else {
// No ESRE yet, start with template
this.esreContent = this.getESRETemplate();
this.renderEditor();
}
} catch (error) {
console.error('[DSESREEditor] Failed to load ESRE:', error);
this.esreContent = this.getESRETemplate();
this.renderEditor();
}
}
getESRETemplate() {
return `# Explicit Style Requirements and Expectations (ESRE)
## Project: ${contextStore.get('projectId') || 'Design System'}
### Color Requirements
- Primary colors must match Figma specifications exactly
- Accessibility: All text must meet WCAG 2.1 AA contrast ratios
- Color tokens must be used instead of hardcoded hex values
### Typography Requirements
- Font families: [Specify approved fonts]
- Font sizes must use design system scale
- Line heights must maintain readability
- Letter spacing should follow design specifications
### Spacing Requirements
- All spacing must use design system spacing scale
- Margins and padding should be consistent across components
- Grid system: [Specify grid specifications]
### Component Requirements
- All components must be built from design system primitives
- Component variants must match Figma component variants
- Props should follow naming conventions
### Responsive Requirements
- Breakpoints: [Specify breakpoints]
- Mobile-first approach required
- Touch targets must be at least 44x44px
### Accessibility Requirements
- All interactive elements must be keyboard accessible
- ARIA labels required for icon-only buttons
- Focus indicators must be visible
- Screen reader testing required
### Performance Requirements
- Initial load time: [Specify target]
- Time to Interactive: [Specify target]
- Bundle size limits: [Specify limits]
### Browser Support
- Chrome: Latest 2 versions
- Firefox: Latest 2 versions
- Safari: Latest 2 versions
- Edge: Latest 2 versions
---
## Validation Checklist
### Pre-Deployment
- [ ] All colors match Figma specifications
- [ ] Typography follows design system scale
- [ ] Spacing uses design tokens
- [ ] Components match design system library
- [ ] Responsive behavior validated
- [ ] Accessibility audit passed
- [ ] Performance metrics met
- [ ] Cross-browser testing completed
### QA Testing
- [ ] Visual comparison with Figma
- [ ] Keyboard navigation tested
- [ ] Screen reader compatibility verified
- [ ] Mobile devices tested
- [ ] Edge cases validated
---
Last updated: ${new Date().toISOString().split('T')[0]}
`;
}
renderEditor() {
const container = this.querySelector('#editor-container');
if (!container) return;
const config = {
title: 'ESRE Editor',
content: this.esreContent,
language: 'markdown',
onSave: (content) => this.saveESRE(content),
onExport: (content) => this.exportESRE(content)
};
container.innerHTML = createEditorView(config);
setupEditorHandlers(container, config);
}
async saveESRE(content) {
this.isSaving = true;
const saveBtn = document.querySelector('#editor-save-btn');
if (saveBtn) {
saveBtn.disabled = true;
saveBtn.textContent = '⏳ Saving...';
}
try {
const context = contextStore.getMCPContext();
if (!context.project_id) {
throw new Error('No project selected');
}
// Save ESRE via API
const response = await fetch('/api/esre/save', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
projectId: context.project_id,
content
})
});
if (!response.ok) {
throw new Error(`Save failed: ${response.statusText}`);
}
this.esreContent = content;
ComponentHelpers.showToast?.('ESRE saved successfully', 'success');
} catch (error) {
console.error('[DSESREEditor] Save failed:', error);
ComponentHelpers.showToast?.(`Save failed: ${error.message}`, 'error');
} finally {
this.isSaving = false;
if (saveBtn) {
saveBtn.disabled = false;
saveBtn.textContent = '💾 Save';
}
}
}
exportESRE(content) {
const blob = new Blob([content], { type: 'text/markdown' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
const projectId = contextStore.get('projectId') || 'project';
a.href = url;
a.download = `${projectId}-esre.md`;
a.click();
URL.revokeObjectURL(url);
ComponentHelpers.showToast?.('ESRE exported', 'success');
}
render() {
this.innerHTML = `
<div style="display: flex; flex-direction: column; height: 100%;">
<!-- Info Banner -->
<div style="padding: 12px 16px; background: rgba(255, 191, 0, 0.1); border-bottom: 1px solid var(--vscode-border);">
<div style="display: flex; align-items: center; gap: 12px;">
<div style="font-size: 20px;">📋</div>
<div style="flex: 1;">
<div style="font-size: 11px; font-weight: 600; margin-bottom: 2px;">
ESRE: Explicit Style Requirements and Expectations
</div>
<div style="font-size: 10px; color: var(--vscode-text-dim);">
Define clear specifications for design implementation and QA validation
</div>
</div>
</div>
</div>
<!-- Editor Container -->
<div id="editor-container" style="flex: 1; overflow: hidden;">
${createEditorView({
title: 'ESRE Editor',
content: this.esreContent,
language: 'markdown',
onSave: (content) => this.saveESRE(content),
onExport: (content) => this.exportESRE(content)
})}
</div>
<!-- Help Footer -->
<div style="padding: 8px 16px; border-top: 1px solid var(--vscode-border); font-size: 10px; color: var(--vscode-text-dim);">
💡 Tip: Use Markdown formatting for clear documentation. Save changes before closing.
</div>
</div>
`;
}
}
customElements.define('ds-esre-editor', DSESREEditor);
export default DSESREEditor;

View File

@@ -1,303 +0,0 @@
/**
* ds-figma-extract-quick.js
* One-click Figma token extraction tool
* MVP2: Extract design tokens directly from Figma file
*/
import contextStore from '../../stores/context-store.js';
export default class FigmaExtractQuick extends HTMLElement {
constructor() {
super();
this.figmaUrl = '';
this.extractionProgress = 0;
this.extractedTokens = [];
}
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;">Figma Token Extraction</h1>
<p style="margin: 0; color: var(--vscode-text-dim);">
Extract design tokens directly from your Figma file
</p>
</div>
<!-- Input Section -->
<div style="background: var(--vscode-sidebar); border: 1px solid var(--vscode-border); border-radius: 4px; padding: 16px; margin-bottom: 24px;">
<div style="margin-bottom: 12px;">
<label style="display: block; font-size: 12px; font-weight: 500; margin-bottom: 8px;">
Figma File URL or Key
</label>
<input
id="figma-url-input"
type="text"
placeholder="https://figma.com/file/xxx/Design-Tokens or file-key"
style="
width: 100%;
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;
box-sizing: border-box;
"
/>
</div>
<div style="display: grid; grid-template-columns: 1fr 1fr; gap: 8px;">
<button id="extract-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;
">🚀 Extract Tokens</button>
<button id="export-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;
">📥 Import to Project</button>
</div>
</div>
<!-- Progress Section -->
<div id="progress-container" style="display: none; margin-bottom: 24px;">
<div style="margin-bottom: 8px; font-size: 12px; color: var(--vscode-text-dim);">
Extracting tokens... <span id="progress-percent">0%</span>
</div>
<div style="
width: 100%;
height: 6px;
background: var(--vscode-bg);
border-radius: 3px;
overflow: hidden;
">
<div id="progress-bar" style="
width: 0%;
height: 100%;
background: #0066CC;
transition: width 0.3s ease;
"></div>
</div>
</div>
<!-- Results Section -->
<div id="results-container" style="display: none; 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;">✓ Extraction Complete</h3>
<div id="token-summary" style="
display: grid;
grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
gap: 12px;
margin-bottom: 16px;
">
<!-- Summary cards will be inserted here -->
</div>
<div style="margin-bottom: 16px; padding-top: 12px; border-top: 1px solid var(--vscode-border);">
<div style="font-size: 11px; color: var(--vscode-text-dim); margin-bottom: 8px;">
Extracted Tokens:
</div>
<pre id="token-preview" style="
background: var(--vscode-bg);
padding: 12px;
border-radius: 3px;
font-size: 10px;
overflow: auto;
max-height: 300px;
margin: 0;
color: #CE9178;
">{}</pre>
</div>
<button id="copy-tokens-btn" style="
width: 100%;
padding: 8px;
background: var(--vscode-button-background);
color: var(--vscode-button-foreground);
border: none;
border-radius: 3px;
cursor: pointer;
font-size: 11px;
font-weight: 500;
">📋 Copy JSON</button>
</div>
<!-- Instructions Section -->
<div style="background: var(--vscode-notificationsErrorIcon); opacity: 0.1; border: 1px solid var(--vscode-border); border-radius: 4px; padding: 12px;">
<div style="font-size: 12px; color: var(--vscode-text-dim);">
<strong>How to extract:</strong>
<ol style="margin: 8px 0 0 20px; padding: 0;">
<li>Open your Figma Design Tokens file</li>
<li>Copy the file URL or key from browser</li>
<li>Paste it above and click "Extract Tokens"</li>
<li>Review and import to your project</li>
</ol>
</div>
</div>
</div>
`;
}
setupEventListeners() {
const extractBtn = this.querySelector('#extract-btn');
const exportBtn = this.querySelector('#export-btn');
const copyBtn = this.querySelector('#copy-tokens-btn');
const input = this.querySelector('#figma-url-input');
if (extractBtn) {
extractBtn.addEventListener('click', () => this.extractTokens());
}
if (exportBtn) {
exportBtn.addEventListener('click', () => this.importTokens());
}
if (copyBtn) {
copyBtn.addEventListener('click', () => this.copyTokensToClipboard());
}
if (input) {
input.addEventListener('change', (e) => {
this.figmaUrl = e.target.value;
});
}
}
async extractTokens() {
const url = this.figmaUrl.trim();
if (!url) {
alert('Please enter a Figma file URL or key');
return;
}
// Validate Figma URL or key format
const isFigmaUrl = url.includes('figma.com');
const isFigmaKey = /^[a-zA-Z0-9]{20,}$/.test(url);
if (!isFigmaUrl && !isFigmaKey) {
alert('Invalid Figma URL or key format. Please provide a valid Figma file URL or file key.');
return;
}
const progressContainer = this.querySelector('#progress-container');
const resultsContainer = this.querySelector('#results-container');
progressContainer.style.display = 'block';
resultsContainer.style.display = 'none';
// Simulate token extraction process
this.extractedTokens = this.generateMockTokens();
for (let i = 0; i <= 100; i += 10) {
this.extractionProgress = i;
this.querySelector('#progress-percent').textContent = i + '%';
this.querySelector('#progress-bar').style.width = i + '%';
await new Promise(resolve => setTimeout(resolve, 100));
}
this.showResults();
}
generateMockTokens() {
return {
colors: {
primary: { value: '#0066CC', description: 'Primary brand color' },
secondary: { value: '#4CAF50', description: 'Secondary brand color' },
error: { value: '#F44336', description: 'Error/danger color' },
warning: { value: '#FF9800', description: 'Warning color' },
success: { value: '#4CAF50', description: 'Success color' }
},
spacing: {
xs: { value: '4px', description: 'Extra small spacing' },
sm: { value: '8px', description: 'Small spacing' },
md: { value: '16px', description: 'Medium spacing' },
lg: { value: '24px', description: 'Large spacing' },
xl: { value: '32px', description: 'Extra large spacing' }
},
typography: {
heading: { value: 'Poppins, sans-serif', description: 'Heading font' },
body: { value: 'Inter, sans-serif', description: 'Body font' },
mono: { value: 'Courier New, monospace', description: 'Monospace font' }
}
};
}
showResults() {
const progressContainer = this.querySelector('#progress-container');
const resultsContainer = this.querySelector('#results-container');
progressContainer.style.display = 'none';
resultsContainer.style.display = 'block';
// Create summary cards
const summary = this.querySelector('#token-summary');
const categories = Object.keys(this.extractedTokens);
summary.innerHTML = categories.map(cat => `
<div style="
background: var(--vscode-bg);
padding: 12px;
border-radius: 3px;
text-align: center;
">
<div style="font-size: 18px; font-weight: 600; color: #0066CC;">
${Object.keys(this.extractedTokens[cat]).length}
</div>
<div style="font-size: 11px; color: var(--vscode-text-dim); text-transform: capitalize;">
${cat}
</div>
</div>
`).join('');
// Show preview
this.querySelector('#token-preview').textContent = JSON.stringify(this.extractedTokens, null, 2);
}
copyTokensToClipboard() {
const json = JSON.stringify(this.extractedTokens, null, 2);
navigator.clipboard.writeText(json).then(() => {
const btn = this.querySelector('#copy-tokens-btn');
const original = btn.textContent;
btn.textContent = '✓ Copied to clipboard';
setTimeout(() => {
btn.textContent = original;
}, 2000);
});
}
importTokens() {
const json = JSON.stringify(this.extractedTokens, null, 2);
const blob = new Blob([json], { type: 'application/json' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = 'figma-tokens.json';
a.click();
URL.revokeObjectURL(url);
// Also dispatch event for integration with project
this.dispatchEvent(new CustomEvent('tokens-extracted', {
detail: { tokens: this.extractedTokens },
bubbles: true,
composed: true
}));
}
}
customElements.define('ds-figma-extract-quick', FigmaExtractQuick);

View File

@@ -1,297 +0,0 @@
/**
* ds-figma-extraction.js
* Interface for extracting design tokens from Figma files
* UI Team Tool #3
*/
import { createFormView, setupFormHandlers } from '../../utils/tool-templates.js';
import { ComponentHelpers } from '../../utils/component-helpers.js';
import contextStore from '../../stores/context-store.js';
import toolBridge from '../../services/tool-bridge.js';
import apiClient from '../../services/api-client.js';
class DSFigmaExtraction extends HTMLElement {
constructor() {
super();
this.figmaFileKey = '';
this.figmaToken = '';
this.extractionResults = null;
this.isExtracting = false;
}
async connectedCallback() {
await this.loadProjectConfig();
this.render();
this.setupEventListeners();
}
async loadProjectConfig() {
try {
const context = contextStore.getMCPContext();
if (!context.project_id) return;
const project = await apiClient.getProject(context.project_id);
const figmaUrl = project.figma_ui_file || '';
// Extract file key from Figma URL
const match = figmaUrl.match(/file\/([^/]+)/);
if (match) {
this.figmaFileKey = match[1];
}
// Check for stored Figma token
this.figmaToken = localStorage.getItem('figma_token') || '';
} catch (error) {
console.error('[DSFigmaExtraction] Failed to load project config:', error);
}
}
setupEventListeners() {
const fileKeyInput = this.querySelector('#figma-file-key');
const tokenInput = this.querySelector('#figma-token');
const extractBtn = this.querySelector('#extract-btn');
const saveTokenCheckbox = this.querySelector('#save-token');
if (fileKeyInput) {
fileKeyInput.value = this.figmaFileKey;
}
if (tokenInput) {
tokenInput.value = this.figmaToken;
}
if (extractBtn) {
extractBtn.addEventListener('click', () => this.extractTokens());
}
if (saveTokenCheckbox && tokenInput) {
tokenInput.addEventListener('change', () => {
if (saveTokenCheckbox.checked) {
localStorage.setItem('figma_token', tokenInput.value);
}
});
}
}
async extractTokens() {
const fileKeyInput = this.querySelector('#figma-file-key');
const tokenInput = this.querySelector('#figma-token');
this.figmaFileKey = fileKeyInput?.value.trim() || '';
this.figmaToken = tokenInput?.value.trim() || '';
if (!this.figmaFileKey) {
ComponentHelpers.showToast?.('Please enter a Figma file key', 'error');
return;
}
if (!this.figmaToken) {
ComponentHelpers.showToast?.('Please enter a Figma API token', 'error');
return;
}
this.isExtracting = true;
this.updateLoadingState();
try {
// Set Figma token as environment variable for MCP tool
// In real implementation, this would be securely stored
process.env.FIGMA_TOKEN = this.figmaToken;
// Call dss_sync_figma MCP tool
const result = await toolBridge.executeTool('dss_sync_figma', {
file_key: this.figmaFileKey
});
this.extractionResults = result;
this.renderResults();
ComponentHelpers.showToast?.('Tokens extracted successfully', 'success');
} catch (error) {
console.error('[DSFigmaExtraction] Extraction failed:', error);
ComponentHelpers.showToast?.(`Extraction failed: ${error.message}`, 'error');
const resultsContainer = this.querySelector('#results-container');
if (resultsContainer) {
resultsContainer.innerHTML = ComponentHelpers.renderError('Token extraction failed', error);
}
} finally {
this.isExtracting = false;
this.updateLoadingState();
}
}
updateLoadingState() {
const extractBtn = this.querySelector('#extract-btn');
if (!extractBtn) return;
if (this.isExtracting) {
extractBtn.disabled = true;
extractBtn.textContent = '⏳ Extracting...';
} else {
extractBtn.disabled = false;
extractBtn.textContent = '🎨 Extract Tokens';
}
}
renderResults() {
const resultsContainer = this.querySelector('#results-container');
if (!resultsContainer || !this.extractionResults) return;
const tokenCount = Object.keys(this.extractionResults.tokens || {}).length;
resultsContainer.innerHTML = `
<div style="padding: 16px;">
<div style="background: var(--vscode-sidebar); border: 1px solid var(--vscode-border); border-radius: 4px; padding: 16px; margin-bottom: 16px;">
<h4 style="font-size: 12px; font-weight: 600; margin-bottom: 12px;">Extraction Summary</h4>
<div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(120px, 1fr)); gap: 16px; font-size: 11px;">
<div style="text-align: center;">
<div style="font-size: 24px; font-weight: 600; color: var(--vscode-text);">${tokenCount}</div>
<div style="color: var(--vscode-text-dim); margin-top: 4px;">Tokens Found</div>
</div>
<div style="text-align: center;">
<div style="font-size: 24px; font-weight: 600; color: #89d185;">✓</div>
<div style="color: var(--vscode-text-dim); margin-top: 4px;">Success</div>
</div>
</div>
</div>
<div style="display: flex; gap: 12px;">
<button id="export-json-btn" class="button" style="font-size: 11px;">
📥 Export JSON
</button>
<button id="export-css-btn" class="button" style="font-size: 11px;">
📥 Export CSS
</button>
<button id="view-tokens-btn" class="button" style="font-size: 11px;">
👁️ View Tokens
</button>
</div>
</div>
`;
// Setup export handlers
const exportJsonBtn = resultsContainer.querySelector('#export-json-btn');
const exportCssBtn = resultsContainer.querySelector('#export-css-btn');
const viewTokensBtn = resultsContainer.querySelector('#view-tokens-btn');
if (exportJsonBtn) {
exportJsonBtn.addEventListener('click', () => this.exportTokens('json'));
}
if (exportCssBtn) {
exportCssBtn.addEventListener('click', () => this.exportTokens('css'));
}
if (viewTokensBtn) {
viewTokensBtn.addEventListener('click', () => {
// Switch to Token Inspector panel
const panel = document.querySelector('ds-panel');
if (panel) {
panel.switchTab('tokens');
}
});
}
}
exportTokens(format) {
if (!this.extractionResults) return;
const filename = `figma-tokens-${this.figmaFileKey}.${format}`;
let content = '';
if (format === 'json') {
content = JSON.stringify(this.extractionResults.tokens, null, 2);
} else if (format === 'css') {
// Convert tokens to CSS custom properties
const tokens = this.extractionResults.tokens;
content = ':root {\n';
for (const [key, value] of Object.entries(tokens)) {
content += ` --${key}: ${value};\n`;
}
content += '}\n';
}
// Create download
const blob = new Blob([content], { type: 'text/plain' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = filename;
a.click();
URL.revokeObjectURL(url);
ComponentHelpers.showToast?.(`Exported as ${filename}`, 'success');
}
render() {
this.innerHTML = `
<div style="display: flex; flex-direction: column; height: 100%;">
<!-- Configuration Panel -->
<div style="padding: 16px; border-bottom: 1px solid var(--vscode-border); background: var(--vscode-sidebar);">
<h3 style="font-size: 12px; font-weight: 600; margin-bottom: 12px;">Figma Token Extraction</h3>
<div style="display: grid; grid-template-columns: 2fr 3fr auto; gap: 12px; align-items: end;">
<div>
<label style="display: block; font-size: 11px; font-weight: 600; margin-bottom: 4px; color: var(--vscode-text-dim);">
Figma File Key
</label>
<input
type="text"
id="figma-file-key"
placeholder="abc123def456..."
class="input"
style="width: 100%; font-size: 11px; font-family: monospace;"
/>
</div>
<div>
<label style="display: block; font-size: 11px; font-weight: 600; margin-bottom: 4px; color: var(--vscode-text-dim);">
Figma API Token
</label>
<input
type="password"
id="figma-token"
placeholder="figd_..."
class="input"
style="width: 100%; font-size: 11px; font-family: monospace;"
/>
</div>
<button id="extract-btn" class="button" style="font-size: 11px; padding: 6px 16px;">
🎨 Extract Tokens
</button>
</div>
<div style="margin-top: 8px; display: flex; justify-content: space-between; align-items: center;">
<label style="font-size: 10px; color: var(--vscode-text-dim); display: flex; align-items: center; gap: 6px;">
<input type="checkbox" id="save-token" />
Remember Figma token (stored locally)
</label>
<a href="https://www.figma.com/developers/api#authentication" target="_blank" style="font-size: 10px; color: var(--vscode-link);">
Get API Token →
</a>
</div>
</div>
<!-- Results Container -->
<div id="results-container" style="flex: 1; overflow: auto;">
<div style="display: flex; align-items: center; justify-content: center; height: 100%; text-align: center; padding: 48px;">
<div>
<div style="font-size: 48px; margin-bottom: 16px;">🎨</div>
<h3 style="font-size: 14px; font-weight: 600; margin-bottom: 8px;">Ready to Extract Tokens</h3>
<p style="font-size: 12px; color: var(--vscode-text-dim);">
Enter your Figma file key and API token above to extract design tokens
</p>
</div>
</div>
</div>
</div>
`;
}
}
customElements.define('ds-figma-extraction', DSFigmaExtraction);
export default DSFigmaExtraction;

View File

@@ -1,201 +0,0 @@
/**
* ds-figma-live-compare.js
* Side-by-side Figma and Live Application comparison for QA validation
* QA Team Tool #1
*/
import { createComparisonView, setupComparisonHandlers } from '../../utils/tool-templates.js';
import { ComponentHelpers } from '../../utils/component-helpers.js';
import contextStore from '../../stores/context-store.js';
import apiClient from '../../services/api-client.js';
class DSFigmaLiveCompare extends HTMLElement {
constructor() {
super();
this.figmaUrl = '';
this.liveUrl = '';
}
async connectedCallback() {
await this.loadProjectConfig();
this.render();
this.setupEventListeners();
}
async loadProjectConfig() {
try {
const context = contextStore.getMCPContext();
if (!context.project_id) {
throw new Error('No project selected');
}
const project = await apiClient.getProject(context.project_id);
this.figmaUrl = project.figma_qa_file || project.figma_ui_file || '';
this.liveUrl = project.live_url || window.location.origin;
} catch (error) {
console.error('[DSFigmaLiveCompare] Failed to load project config:', error);
}
}
setupEventListeners() {
const figmaInput = this.querySelector('#figma-url-input');
const liveInput = this.querySelector('#live-url-input');
const loadBtn = this.querySelector('#load-comparison-btn');
const screenshotBtn = this.querySelector('#take-screenshot-btn');
if (figmaInput) {
figmaInput.value = this.figmaUrl;
}
if (liveInput) {
liveInput.value = this.liveUrl;
}
if (loadBtn) {
loadBtn.addEventListener('click', () => this.loadComparison());
}
if (screenshotBtn) {
screenshotBtn.addEventListener('click', () => this.takeScreenshots());
}
const comparisonContainer = this.querySelector('#comparison-container');
if (comparisonContainer) {
setupComparisonHandlers(comparisonContainer, {});
}
}
loadComparison() {
const figmaInput = this.querySelector('#figma-url-input');
const liveInput = this.querySelector('#live-url-input');
this.figmaUrl = figmaInput?.value || '';
this.liveUrl = liveInput?.value || '';
if (!this.figmaUrl || !this.liveUrl) {
ComponentHelpers.showToast?.('Please enter both Figma and Live URLs', 'error');
return;
}
try {
new URL(this.figmaUrl);
new URL(this.liveUrl);
} catch (error) {
ComponentHelpers.showToast?.('Invalid URL format', 'error');
return;
}
const comparisonContainer = this.querySelector('#comparison-container');
if (comparisonContainer) {
comparisonContainer.innerHTML = createComparisonView({
leftTitle: 'Figma Design',
rightTitle: 'Live Application',
leftSrc: this.figmaUrl,
rightSrc: this.liveUrl
});
setupComparisonHandlers(comparisonContainer, {});
ComponentHelpers.showToast?.('Comparison loaded', 'success');
}
}
async takeScreenshots() {
ComponentHelpers.showToast?.('Taking screenshots...', 'info');
try {
// Take screenshot of live application via MCP (using authenticated API client)
const context = contextStore.getMCPContext();
await apiClient.request('POST', '/qa/screenshot-compare', {
projectId: context.project_id,
figmaUrl: this.figmaUrl,
liveUrl: this.liveUrl
});
ComponentHelpers.showToast?.('Screenshots saved to gallery', 'success');
// Switch to screenshot gallery
const panel = document.querySelector('ds-panel');
if (panel) {
panel.switchTab('screenshots');
}
} catch (error) {
console.error('[DSFigmaLiveCompare] Screenshot failed:', error);
ComponentHelpers.showToast?.(`Screenshot failed: ${error.message}`, 'error');
}
}
render() {
this.innerHTML = `
<div style="display: flex; flex-direction: column; height: 100%;">
<!-- Configuration Panel -->
<div style="padding: 16px; border-bottom: 1px solid var(--vscode-border); background: var(--vscode-sidebar);">
<h3 style="font-size: 12px; font-weight: 600; margin-bottom: 12px;">Figma vs Live QA Comparison</h3>
<div style="display: grid; grid-template-columns: 1fr 1fr auto auto; gap: 12px; align-items: end;">
<div>
<label style="display: block; font-size: 11px; font-weight: 600; margin-bottom: 4px; color: var(--vscode-text-dim);">
Figma Design URL
</label>
<input
type="url"
id="figma-url-input"
placeholder="https://figma.com/file/..."
class="input"
style="width: 100%; font-size: 11px;"
/>
</div>
<div>
<label style="display: block; font-size: 11px; font-weight: 600; margin-bottom: 4px; color: var(--vscode-text-dim);">
Live Component URL
</label>
<input
type="url"
id="live-url-input"
placeholder="https://app.example.com/..."
class="input"
style="width: 100%; font-size: 11px;"
/>
</div>
<button id="load-comparison-btn" class="button" style="font-size: 11px; padding: 6px 16px;">
🔍 Load
</button>
<button id="take-screenshot-btn" class="button" style="font-size: 11px; padding: 6px 16px;">
📸 Screenshot
</button>
</div>
<div style="margin-top: 8px; font-size: 10px; color: var(--vscode-text-dim);">
💡 Compare design specifications with live implementation for QA validation
</div>
</div>
<!-- Comparison View -->
<div id="comparison-container" style="flex: 1; overflow: hidden;">
${this.figmaUrl && this.liveUrl ? createComparisonView({
leftTitle: 'Figma Design',
rightTitle: 'Live Application',
leftSrc: this.figmaUrl,
rightSrc: this.liveUrl
}) : `
<div style="display: flex; align-items: center; justify-content: center; height: 100%; text-align: center; padding: 48px;">
<div>
<div style="font-size: 48px; margin-bottom: 16px;"></div>
<h3 style="font-size: 14px; font-weight: 600; margin-bottom: 8px;">QA Comparison Tool</h3>
<p style="font-size: 12px; color: var(--vscode-text-dim);">
Enter Figma design and live application URLs to validate implementation against specifications
</p>
</div>
</div>
`}
</div>
</div>
`;
}
}
customElements.define('ds-figma-live-compare', DSFigmaLiveCompare);
export default DSFigmaLiveCompare;

View File

@@ -1,266 +0,0 @@
/**
* ds-figma-plugin.js
* Interface for Figma plugin export and token management
* UX Team Tool #1
*/
import { createFormView, setupFormHandlers } from '../../utils/tool-templates.js';
import { ComponentHelpers } from '../../utils/component-helpers.js';
import contextStore from '../../stores/context-store.js';
import toolBridge from '../../services/tool-bridge.js';
class DSFigmaPlugin extends HTMLElement {
constructor() {
super();
this.exportHistory = [];
}
async connectedCallback() {
this.render();
this.setupEventListeners();
await this.loadExportHistory();
}
async loadExportHistory() {
try {
const context = contextStore.getMCPContext();
if (!context.project_id) return;
const cached = localStorage.getItem(`figma_exports_${context.project_id}`);
if (cached) {
this.exportHistory = JSON.parse(cached);
this.renderHistory();
}
} catch (error) {
console.error('[DSFigmaPlugin] Failed to load history:', error);
}
}
setupEventListeners() {
const exportBtn = this.querySelector('#export-figma-btn');
const fileKeyInput = this.querySelector('#figma-file-key');
const exportTypeSelect = this.querySelector('#export-type-select');
if (exportBtn) {
exportBtn.addEventListener('click', () => this.exportFromFigma());
}
}
async exportFromFigma() {
const fileKeyInput = this.querySelector('#figma-file-key');
const exportTypeSelect = this.querySelector('#export-type-select');
const formatSelect = this.querySelector('#export-format-select');
const fileKey = fileKeyInput?.value.trim() || '';
const exportType = exportTypeSelect?.value || 'tokens';
const format = formatSelect?.value || 'json';
if (!fileKey) {
ComponentHelpers.showToast?.('Please enter a Figma file key', 'error');
return;
}
const exportBtn = this.querySelector('#export-figma-btn');
if (exportBtn) {
exportBtn.disabled = true;
exportBtn.textContent = '⏳ Exporting...';
}
try {
let result;
if (exportType === 'tokens') {
// Export design tokens
result = await toolBridge.executeTool('dss_sync_figma', {
file_key: fileKey
});
} else if (exportType === 'assets') {
// Export assets (icons, images)
const response = await fetch('/api/figma/export-assets', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
projectId: contextStore.get('projectId'),
fileKey,
format
})
});
if (!response.ok) {
throw new Error(`Asset export failed: ${response.statusText}`);
}
result = await response.json();
} else if (exportType === 'components') {
// Export component definitions
const response = await fetch('/api/figma/export-components', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
projectId: contextStore.get('projectId'),
fileKey,
format
})
});
if (!response.ok) {
throw new Error(`Component export failed: ${response.statusText}`);
}
result = await response.json();
}
// Add to history
const exportEntry = {
timestamp: new Date().toISOString(),
fileKey,
type: exportType,
format,
itemCount: result.count || Object.keys(result.tokens || result.assets || result.components || {}).length
};
this.exportHistory.unshift(exportEntry);
this.exportHistory = this.exportHistory.slice(0, 10); // Keep last 10
// Cache history
const context = contextStore.getMCPContext();
if (context.project_id) {
localStorage.setItem(`figma_exports_${context.project_id}`, JSON.stringify(this.exportHistory));
}
this.renderHistory();
ComponentHelpers.showToast?.(`Exported ${exportEntry.itemCount} ${exportType}`, 'success');
} catch (error) {
console.error('[DSFigmaPlugin] Export failed:', error);
ComponentHelpers.showToast?.(`Export failed: ${error.message}`, 'error');
} finally {
if (exportBtn) {
exportBtn.disabled = false;
exportBtn.textContent = '📤 Export from Figma';
}
}
}
renderHistory() {
const historyContainer = this.querySelector('#export-history');
if (!historyContainer) return;
if (this.exportHistory.length === 0) {
historyContainer.innerHTML = ComponentHelpers.renderEmpty('No export history', '📋');
return;
}
historyContainer.innerHTML = `
<div style="display: flex; flex-direction: column; gap: 8px;">
${this.exportHistory.map((entry, idx) => `
<div style="background: var(--vscode-bg); border: 1px solid var(--vscode-border); border-radius: 2px; padding: 12px;">
<div style="display: flex; justify-content: space-between; align-items: start; margin-bottom: 6px;">
<div style="flex: 1;">
<div style="font-size: 11px; font-weight: 600; margin-bottom: 4px;">
${ComponentHelpers.escapeHtml(entry.type)} Export
</div>
<div style="font-size: 10px; color: var(--vscode-text-dim); font-family: monospace;">
${ComponentHelpers.escapeHtml(entry.fileKey)}
</div>
</div>
<div style="text-align: right;">
<div style="font-size: 10px; color: var(--vscode-text-dim);">
${ComponentHelpers.formatRelativeTime(new Date(entry.timestamp))}
</div>
<div style="font-size: 11px; font-weight: 600; margin-top: 2px;">
${entry.itemCount} items
</div>
</div>
</div>
<div style="display: flex; gap: 6px;">
<span style="padding: 2px 6px; background: var(--vscode-sidebar); border-radius: 2px; font-size: 9px;">
${entry.format.toUpperCase()}
</span>
</div>
</div>
`).join('')}
</div>
`;
}
render() {
this.innerHTML = `
<div style="display: grid; grid-template-columns: 1fr 1fr; gap: 16px; height: 100%;">
<!-- Export Panel -->
<div style="display: flex; flex-direction: column; height: 100%; border-right: 1px solid var(--vscode-border);">
<div style="padding: 16px; border-bottom: 1px solid var(--vscode-border); background: var(--vscode-sidebar);">
<h3 style="font-size: 12px; font-weight: 600; margin-bottom: 4px;">Figma Export</h3>
<p style="font-size: 10px; color: var(--vscode-text-dim);">
Export tokens, assets, or components from Figma files
</p>
</div>
<div style="flex: 1; overflow: auto; padding: 16px;">
<div style="display: flex; flex-direction: column; gap: 16px;">
<div>
<label style="display: block; font-size: 11px; font-weight: 600; margin-bottom: 6px;">
Figma File Key
</label>
<input
type="text"
id="figma-file-key"
placeholder="abc123def456..."
class="input"
style="width: 100%; font-size: 11px; font-family: monospace;"
/>
<div style="font-size: 10px; color: var(--vscode-text-dim); margin-top: 4px;">
Find this in your Figma file URL
</div>
</div>
<div>
<label style="display: block; font-size: 11px; font-weight: 600; margin-bottom: 6px;">
Export Type
</label>
<select id="export-type-select" class="input" style="width: 100%; font-size: 11px;">
<option value="tokens">Design Tokens</option>
<option value="assets">Assets (Icons, Images)</option>
<option value="components">Component Definitions</option>
</select>
</div>
<div>
<label style="display: block; font-size: 11px; font-weight: 600; margin-bottom: 6px;">
Export Format
</label>
<select id="export-format-select" class="input" style="width: 100%; font-size: 11px;">
<option value="json">JSON</option>
<option value="css">CSS</option>
<option value="scss">SCSS</option>
<option value="js">JavaScript</option>
</select>
</div>
<button id="export-figma-btn" class="button" style="font-size: 12px; padding: 8px;">
📤 Export from Figma
</button>
</div>
</div>
</div>
<!-- History Panel -->
<div style="display: flex; flex-direction: column; height: 100%;">
<div style="padding: 16px; border-bottom: 1px solid var(--vscode-border); background: var(--vscode-sidebar);">
<h3 style="font-size: 12px; font-weight: 600; margin-bottom: 4px;">Export History</h3>
<p style="font-size: 10px; color: var(--vscode-text-dim);">
Recent Figma exports for this project
</p>
</div>
<div id="export-history" style="flex: 1; overflow: auto; padding: 16px;">
${ComponentHelpers.renderLoading('Loading history...')}
</div>
</div>
</div>
`;
}
}
customElements.define('ds-figma-plugin', DSFigmaPlugin);
export default DSFigmaPlugin;

View File

@@ -1,411 +0,0 @@
/**
* ds-figma-status.js
* Figma integration status and sync controls
*/
import toolBridge from '../../services/tool-bridge.js';
import { ComponentHelpers } from '../../utils/component-helpers.js';
class DSFigmaStatus extends HTMLElement {
constructor() {
super();
this.figmaToken = null;
this.figmaFileKey = null;
this.connectionStatus = 'unknown';
this.lastSync = null;
this.isConfiguring = false;
this.isSyncing = false;
}
async connectedCallback() {
this.render();
this.setupEventListeners();
await this.checkConfiguration();
}
/**
* Check if Figma is configured and test connection
*/
async checkConfiguration() {
const statusContent = this.querySelector('#figma-status-content');
if (!statusContent) return;
try {
// Check for stored file key in localStorage (not token - that's server-side)
this.figmaFileKey = localStorage.getItem('figma_file_key');
if (!this.figmaFileKey) {
this.connectionStatus = 'not_configured';
this.renderStatus();
return;
}
// Test connection by calling sync with dry-run check
// Note: Backend checks for FIGMA_TOKEN env variable
statusContent.innerHTML = ComponentHelpers.renderLoading('Checking Figma connection...');
try {
// Try to get Figma file info (will fail if token not configured)
const result = await toolBridge.syncFigma(this.figmaFileKey);
if (result && result.tokens) {
this.connectionStatus = 'connected';
this.lastSync = new Date();
} else {
this.connectionStatus = 'error';
}
} catch (error) {
// Token not configured on backend
if (error.message.includes('FIGMA_TOKEN')) {
this.connectionStatus = 'token_missing';
} else {
this.connectionStatus = 'error';
}
console.error('Figma connection check failed:', error);
}
this.renderStatus();
} catch (error) {
console.error('Failed to check Figma configuration:', error);
statusContent.innerHTML = ComponentHelpers.renderError('Failed to check configuration', error);
}
}
setupEventListeners() {
// Configure button
const configureBtn = this.querySelector('#figma-configure-btn');
if (configureBtn) {
configureBtn.addEventListener('click', () => this.showConfiguration());
}
// Sync button
const syncBtn = this.querySelector('#figma-sync-btn');
if (syncBtn) {
syncBtn.addEventListener('click', () => this.syncFromFigma());
}
}
showConfiguration() {
this.isConfiguring = true;
this.renderStatus();
// Setup save handler
const saveBtn = this.querySelector('#figma-save-config-btn');
const cancelBtn = this.querySelector('#figma-cancel-config-btn');
if (saveBtn) {
saveBtn.addEventListener('click', () => this.saveConfiguration());
}
if (cancelBtn) {
cancelBtn.addEventListener('click', () => {
this.isConfiguring = false;
this.renderStatus();
});
}
}
async saveConfiguration() {
const fileKeyInput = this.querySelector('#figma-file-key-input');
const tokenInput = this.querySelector('#figma-token-input');
if (!fileKeyInput || !tokenInput) return;
const fileKey = fileKeyInput.value.trim();
const token = tokenInput.value.trim();
if (!fileKey) {
ComponentHelpers.showToast?.('Please enter a Figma file key', 'error');
return;
}
if (!token) {
ComponentHelpers.showToast?.('Please enter a Figma access token', 'error');
return;
}
try {
// Store file key in localStorage (client-side)
localStorage.setItem('figma_file_key', fileKey);
this.figmaFileKey = fileKey;
// Display warning about backend token configuration
ComponentHelpers.showToast?.('File key saved. Please configure FIGMA_TOKEN environment variable on the backend.', 'info');
this.isConfiguring = false;
this.connectionStatus = 'token_missing';
this.renderStatus();
} catch (error) {
console.error('Failed to save Figma configuration:', error);
ComponentHelpers.showToast?.(`Failed to save configuration: ${error.message}`, 'error');
}
}
async syncFromFigma() {
if (this.isSyncing || !this.figmaFileKey) return;
this.isSyncing = true;
const syncBtn = this.querySelector('#figma-sync-btn');
if (syncBtn) {
syncBtn.disabled = true;
syncBtn.textContent = '🔄 Syncing...';
}
try {
const result = await toolBridge.syncFigma(this.figmaFileKey);
if (result && result.tokens) {
this.lastSync = new Date();
this.connectionStatus = 'connected';
ComponentHelpers.showToast?.(
`Synced ${Object.keys(result.tokens).length} tokens from Figma`,
'success'
);
this.renderStatus();
} else {
throw new Error('No tokens returned from Figma');
}
} catch (error) {
console.error('Failed to sync from Figma:', error);
ComponentHelpers.showToast?.(`Sync failed: ${error.message}`, 'error');
this.connectionStatus = 'error';
this.renderStatus();
} finally {
this.isSyncing = false;
if (syncBtn) {
syncBtn.disabled = false;
syncBtn.textContent = '🔄 Sync Now';
}
}
}
getStatusBadge() {
const badges = {
connected: ComponentHelpers.createBadge('Connected', 'success'),
not_configured: ComponentHelpers.createBadge('Not Configured', 'info'),
token_missing: ComponentHelpers.createBadge('Token Required', 'warning'),
error: ComponentHelpers.createBadge('Error', 'error'),
unknown: ComponentHelpers.createBadge('Unknown', 'info')
};
return badges[this.connectionStatus] || badges.unknown;
}
renderStatus() {
const statusContent = this.querySelector('#figma-status-content');
if (!statusContent) return;
// Configuration form
if (this.isConfiguring) {
statusContent.innerHTML = `
<div style="padding: 16px; background-color: var(--vscode-sidebar); border: 1px solid var(--vscode-border); border-radius: 4px;">
<h4 style="font-size: 12px; font-weight: 600; margin-bottom: 12px;">Configure Figma Integration</h4>
<div style="margin-bottom: 12px;">
<label style="display: block; font-size: 11px; margin-bottom: 4px; color: var(--vscode-text-dim);">
Figma File Key
</label>
<input
type="text"
id="figma-file-key-input"
class="input"
placeholder="e.g., abc123xyz456"
value="${ComponentHelpers.escapeHtml(this.figmaFileKey || '')}"
style="width: 100%; font-size: 11px; font-family: 'Courier New', monospace;"
/>
<div style="font-size: 10px; color: var(--vscode-text-dim); margin-top: 4px;">
Find this in your Figma file URL: figma.com/file/<strong>FILE_KEY</strong>/...
</div>
</div>
<div style="margin-bottom: 16px;">
<label style="display: block; font-size: 11px; margin-bottom: 4px; color: var(--vscode-text-dim);">
Figma Access Token
</label>
<input
type="password"
id="figma-token-input"
class="input"
placeholder="figd_..."
style="width: 100%; font-size: 11px; font-family: 'Courier New', monospace;"
/>
<div style="font-size: 10px; color: var(--vscode-text-dim); margin-top: 4px;">
Generate at: <a href="https://www.figma.com/developers/api#access-tokens" target="_blank" style="color: var(--vscode-accent);">figma.com/developers/api</a>
</div>
</div>
<div style="padding: 12px; background-color: rgba(255, 191, 0, 0.1); border-radius: 4px; margin-bottom: 16px;">
<div style="font-size: 11px; color: #ffbf00;">
⚠️ <strong>Security Note:</strong> The Figma token must be configured as the <code>FIGMA_TOKEN</code> environment variable on the backend server. This UI only stores the file key locally.
</div>
</div>
<div style="display: flex; gap: 8px; justify-content: flex-end;">
<button id="figma-cancel-config-btn" class="button" style="padding: 6px 12px; font-size: 11px;">
Cancel
</button>
<button id="figma-save-config-btn" class="button" style="padding: 6px 12px; font-size: 11px;">
Save Configuration
</button>
</div>
</div>
`;
return;
}
// Not configured state
if (this.connectionStatus === 'not_configured') {
statusContent.innerHTML = `
<div style="text-align: center; padding: 32px;">
<div style="font-size: 48px; margin-bottom: 16px;">🎨</div>
<h3 style="font-size: 14px; font-weight: 600; margin-bottom: 8px;">Figma Not Configured</h3>
<p style="font-size: 12px; color: var(--vscode-text-dim); margin-bottom: 16px;">
Connect your Figma file to sync design tokens automatically.
</p>
<button id="figma-configure-btn" class="button" style="padding: 8px 16px; font-size: 12px;">
Configure Figma
</button>
</div>
`;
const configureBtn = statusContent.querySelector('#figma-configure-btn');
if (configureBtn) {
configureBtn.addEventListener('click', () => this.showConfiguration());
}
return;
}
// Token missing state
if (this.connectionStatus === 'token_missing') {
statusContent.innerHTML = `
<div style="padding: 16px; background-color: var(--vscode-sidebar); border: 1px solid var(--vscode-border); border-radius: 4px;">
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 12px;">
<h4 style="font-size: 12px; font-weight: 600;">Figma Configuration</h4>
${this.getStatusBadge()}
</div>
<div style="padding: 12px; background-color: rgba(255, 191, 0, 0.1); border: 1px solid #ffbf00; border-radius: 4px; margin-bottom: 12px;">
<div style="font-size: 11px; color: #ffbf00;">
⚠️ <strong>Backend Configuration Required</strong><br/>
Please set the <code>FIGMA_TOKEN</code> environment variable on the backend server and restart.
</div>
</div>
<div style="font-size: 11px; color: var(--vscode-text-dim); margin-bottom: 8px;">
<strong>File Key:</strong> <code style="background-color: var(--vscode-bg); padding: 2px 6px; border-radius: 2px;">${ComponentHelpers.escapeHtml(this.figmaFileKey || 'N/A')}</code>
</div>
<div style="display: flex; gap: 8px; margin-top: 12px;">
<button id="figma-configure-btn" class="button" style="padding: 4px 12px; font-size: 11px; flex: 1;">
Reconfigure
</button>
</div>
</div>
`;
const configureBtn = statusContent.querySelector('#figma-configure-btn');
if (configureBtn) {
configureBtn.addEventListener('click', () => this.showConfiguration());
}
return;
}
// Connected state
if (this.connectionStatus === 'connected') {
statusContent.innerHTML = `
<div style="padding: 16px; background-color: var(--vscode-sidebar); border: 1px solid var(--vscode-border); border-radius: 4px;">
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 12px;">
<h4 style="font-size: 12px; font-weight: 600;">Figma Sync</h4>
${this.getStatusBadge()}
</div>
<div style="font-size: 11px; color: var(--vscode-text-dim); margin-bottom: 8px;">
<strong>File Key:</strong> <code style="background-color: var(--vscode-bg); padding: 2px 6px; border-radius: 2px;">${ComponentHelpers.escapeHtml(this.figmaFileKey || 'N/A')}</code>
</div>
${this.lastSync ? `
<div style="font-size: 11px; color: var(--vscode-text-dim); margin-bottom: 12px;">
<strong>Last Sync:</strong> ${ComponentHelpers.formatRelativeTime(this.lastSync)}
</div>
` : ''}
<div style="display: flex; gap: 8px; margin-top: 12px;">
<button id="figma-sync-btn" class="button" style="padding: 4px 12px; font-size: 11px; flex: 1;">
🔄 Sync Now
</button>
<button id="figma-configure-btn" class="button" style="padding: 4px 12px; font-size: 11px;">
⚙️
</button>
</div>
</div>
`;
const syncBtn = statusContent.querySelector('#figma-sync-btn');
const configureBtn = statusContent.querySelector('#figma-configure-btn');
if (syncBtn) {
syncBtn.addEventListener('click', () => this.syncFromFigma());
}
if (configureBtn) {
configureBtn.addEventListener('click', () => this.showConfiguration());
}
return;
}
// Error state
if (this.connectionStatus === 'error') {
statusContent.innerHTML = `
<div style="padding: 16px; background-color: var(--vscode-sidebar); border: 1px solid var(--vscode-border); border-radius: 4px;">
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 12px;">
<h4 style="font-size: 12px; font-weight: 600;">Figma Sync</h4>
${this.getStatusBadge()}
</div>
<div style="padding: 12px; background-color: rgba(244, 135, 113, 0.1); border: 1px solid #f48771; border-radius: 4px; margin-bottom: 12px;">
<div style="font-size: 11px; color: #f48771;">
❌ Failed to connect to Figma. Please check your configuration.
</div>
</div>
<div style="display: flex; gap: 8px;">
<button id="figma-configure-btn" class="button" style="padding: 4px 12px; font-size: 11px; flex: 1;">
Reconfigure
</button>
<button id="figma-sync-btn" class="button" style="padding: 4px 12px; font-size: 11px;">
Retry
</button>
</div>
</div>
`;
const configureBtn = statusContent.querySelector('#figma-configure-btn');
const syncBtn = statusContent.querySelector('#figma-sync-btn');
if (configureBtn) {
configureBtn.addEventListener('click', () => this.showConfiguration());
}
if (syncBtn) {
syncBtn.addEventListener('click', () => this.checkConfiguration());
}
}
}
render() {
this.innerHTML = `
<div style="padding: 16px; height: 100%; display: flex; flex-direction: column;">
<div id="figma-status-content" style="flex: 1;">
${ComponentHelpers.renderLoading('Checking Figma configuration...')}
</div>
</div>
`;
}
}
customElements.define('ds-figma-status', DSFigmaStatus);
export default DSFigmaStatus;

View File

@@ -1,178 +0,0 @@
/**
* ds-metrics-panel.js
* Universal metrics panel showing tool execution stats and activity
*/
import toolBridge from '../../services/tool-bridge.js';
class DSMetricsPanel extends HTMLElement {
constructor() {
super();
this.metrics = {
totalExecutions: 0,
successCount: 0,
errorCount: 0,
recentActivity: []
};
this.refreshInterval = null;
}
connectedCallback() {
this.render();
this.startAutoRefresh();
}
disconnectedCallback() {
this.stopAutoRefresh();
}
render() {
const successRate = this.metrics.totalExecutions > 0
? Math.round((this.metrics.successCount / this.metrics.totalExecutions) * 100)
: 0;
this.innerHTML = `
<div style="padding: 16px;">
<div style="display: grid; grid-template-columns: repeat(3, 1fr); gap: 16px; margin-bottom: 16px;">
<!-- Total Executions Card -->
<div style="
background-color: var(--vscode-sidebar);
border: 1px solid var(--vscode-border);
border-radius: 4px;
padding: 12px;
">
<div style="font-size: 11px; color: var(--vscode-text-dim); margin-bottom: 4px;">
TOTAL EXECUTIONS
</div>
<div style="font-size: 24px; font-weight: 600;">
${this.metrics.totalExecutions}
</div>
</div>
<!-- Success Rate Card -->
<div style="
background-color: var(--vscode-sidebar);
border: 1px solid var(--vscode-border);
border-radius: 4px;
padding: 12px;
">
<div style="font-size: 11px; color: var(--vscode-text-dim); margin-bottom: 4px;">
SUCCESS RATE
</div>
<div style="font-size: 24px; font-weight: 600; color: ${successRate >= 80 ? '#4caf50' : successRate >= 50 ? '#ff9800' : '#f44336'};">
${successRate}%
</div>
</div>
<!-- Error Count Card -->
<div style="
background-color: var(--vscode-sidebar);
border: 1px solid var(--vscode-border);
border-radius: 4px;
padding: 12px;
">
<div style="font-size: 11px; color: var(--vscode-text-dim); margin-bottom: 4px;">
ERRORS
</div>
<div style="font-size: 24px; font-weight: 600; color: ${this.metrics.errorCount > 0 ? '#f44336' : 'inherit'};">
${this.metrics.errorCount}
</div>
</div>
</div>
<!-- Recent Activity -->
<div>
<div style="font-size: 11px; color: var(--vscode-text-dim); margin-bottom: 8px; text-transform: uppercase; letter-spacing: 0.5px;">
Recent Activity
</div>
<div style="
background-color: var(--vscode-bg);
border: 1px solid var(--vscode-border);
border-radius: 4px;
max-height: 200px;
overflow-y: auto;
">
${this.metrics.recentActivity.length === 0 ? `
<div style="padding: 16px; text-align: center; color: var(--vscode-text-dim); font-size: 12px;">
No recent activity
</div>
` : this.metrics.recentActivity.map(activity => `
<div style="
padding: 8px 12px;
border-bottom: 1px solid var(--vscode-border);
font-size: 12px;
display: flex;
justify-content: space-between;
align-items: center;
">
<div>
<span style="color: ${activity.success ? '#4caf50' : '#f44336'};">
${activity.success ? '✓' : '✗'}
</span>
<span style="margin-left: 8px;">${activity.toolName}</span>
</div>
<span style="color: var(--vscode-text-dim); font-size: 11px;">
${activity.timestamp}
</span>
</div>
`).join('')}
</div>
</div>
</div>
`;
}
recordExecution(toolName, success = true) {
this.metrics.totalExecutions++;
if (success) {
this.metrics.successCount++;
} else {
this.metrics.errorCount++;
}
// Add to recent activity
const now = new Date();
const timestamp = now.toLocaleTimeString();
this.metrics.recentActivity.unshift({
toolName,
success,
timestamp
});
// Keep only last 10 activities
if (this.metrics.recentActivity.length > 10) {
this.metrics.recentActivity = this.metrics.recentActivity.slice(0, 10);
}
this.render();
}
startAutoRefresh() {
// Refresh metrics every 5 seconds
this.refreshInterval = setInterval(() => {
this.render();
}, 5000);
}
stopAutoRefresh() {
if (this.refreshInterval) {
clearInterval(this.refreshInterval);
this.refreshInterval = null;
}
}
reset() {
this.metrics = {
totalExecutions: 0,
successCount: 0,
errorCount: 0,
recentActivity: []
};
this.render();
}
}
customElements.define('ds-metrics-panel', DSMetricsPanel);
export default DSMetricsPanel;

View File

@@ -1,213 +0,0 @@
/**
* ds-navigation-demos.js
* Gallery of generated HTML navigation flow demos
* UX Team Tool #5
*/
import { createGalleryView, setupGalleryHandlers } from '../../utils/tool-templates.js';
import { ComponentHelpers } from '../../utils/component-helpers.js';
import contextStore from '../../stores/context-store.js';
class DSNavigationDemos extends HTMLElement {
constructor() {
super();
this.demos = [];
this.isLoading = false;
}
async connectedCallback() {
this.render();
this.setupEventListeners();
await this.loadDemos();
}
setupEventListeners() {
const generateBtn = this.querySelector('#generate-demo-btn');
if (generateBtn) {
generateBtn.addEventListener('click', () => this.generateDemo());
}
}
async loadDemos() {
this.isLoading = true;
const container = this.querySelector('#demos-container');
if (container) {
container.innerHTML = ComponentHelpers.renderLoading('Loading navigation demos...');
}
try {
const context = contextStore.getMCPContext();
if (!context.project_id) {
throw new Error('No project selected');
}
// Load cached demos
const cached = localStorage.getItem(`nav_demos_${context.project_id}`);
if (cached) {
this.demos = JSON.parse(cached);
} else {
this.demos = [];
}
this.renderDemoGallery();
} catch (error) {
console.error('[DSNavigationDemos] Failed to load demos:', error);
if (container) {
container.innerHTML = ComponentHelpers.renderError('Failed to load demos', error);
}
} finally {
this.isLoading = false;
}
}
async generateDemo() {
const flowNameInput = this.querySelector('#flow-name-input');
const flowName = flowNameInput?.value.trim() || '';
if (!flowName) {
ComponentHelpers.showToast?.('Please enter a flow name', 'error');
return;
}
const generateBtn = this.querySelector('#generate-demo-btn');
if (generateBtn) {
generateBtn.disabled = true;
generateBtn.textContent = '⏳ Generating...';
}
try {
const context = contextStore.getMCPContext();
// Call navigation generation API
const response = await fetch('/api/navigation/generate', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
projectId: context.project_id,
flowName
})
});
if (!response.ok) {
throw new Error(`Generation failed: ${response.statusText}`);
}
const result = await response.json();
// Add to demos
const demo = {
id: Date.now().toString(),
name: flowName,
url: result.url,
thumbnailUrl: result.thumbnailUrl,
timestamp: new Date().toISOString()
};
this.demos.unshift(demo);
// Cache demos
if (context.project_id) {
localStorage.setItem(`nav_demos_${context.project_id}`, JSON.stringify(this.demos));
}
this.renderDemoGallery();
ComponentHelpers.showToast?.(`Demo generated: ${flowName}`, 'success');
// Clear input
if (flowNameInput) {
flowNameInput.value = '';
}
} catch (error) {
console.error('[DSNavigationDemos] Generation failed:', error);
ComponentHelpers.showToast?.(`Generation failed: ${error.message}`, 'error');
} finally {
if (generateBtn) {
generateBtn.disabled = false;
generateBtn.textContent = '✨ Generate Demo';
}
}
}
renderDemoGallery() {
const container = this.querySelector('#demos-container');
if (!container) return;
const config = {
title: 'Navigation Flow Demos',
items: this.demos.map(demo => ({
id: demo.id,
src: demo.thumbnailUrl,
title: demo.name,
subtitle: ComponentHelpers.formatRelativeTime(new Date(demo.timestamp))
})),
onItemClick: (item) => this.viewDemo(item),
onDelete: (item) => this.deleteDemo(item)
};
container.innerHTML = createGalleryView(config);
setupGalleryHandlers(container, config);
}
viewDemo(item) {
const demo = this.demos.find(d => d.id === item.id);
if (demo && demo.url) {
window.open(demo.url, '_blank');
}
}
deleteDemo(item) {
this.demos = this.demos.filter(d => d.id !== item.id);
// Update cache
const context = contextStore.getMCPContext();
if (context.project_id) {
localStorage.setItem(`nav_demos_${context.project_id}`, JSON.stringify(this.demos));
}
this.renderDemoGallery();
ComponentHelpers.showToast?.(`Deleted ${item.title}`, 'success');
}
render() {
this.innerHTML = `
<div style="display: flex; flex-direction: column; height: 100%;">
<!-- Generator Panel -->
<div style="padding: 16px; border-bottom: 1px solid var(--vscode-border); background: var(--vscode-sidebar);">
<h3 style="font-size: 12px; font-weight: 600; margin-bottom: 12px;">Generate Navigation Demo</h3>
<div style="display: grid; grid-template-columns: 1fr auto; gap: 12px; align-items: end;">
<div>
<label style="display: block; font-size: 11px; font-weight: 600; margin-bottom: 4px;">
Flow Name
</label>
<input
type="text"
id="flow-name-input"
placeholder="e.g., User Onboarding, Checkout Process"
class="input"
style="width: 100%; font-size: 11px;"
/>
</div>
<button id="generate-demo-btn" class="button" style="font-size: 11px; padding: 6px 16px;">
✨ Generate Demo
</button>
</div>
<div style="margin-top: 8px; font-size: 10px; color: var(--vscode-text-dim);">
💡 Generates interactive HTML demos of navigation flows
</div>
</div>
<!-- Demos Gallery -->
<div id="demos-container" style="flex: 1; overflow: hidden;">
${ComponentHelpers.renderLoading('Loading demos...')}
</div>
</div>
`;
}
}
customElements.define('ds-navigation-demos', DSNavigationDemos);
export default DSNavigationDemos;

View File

@@ -1,472 +0,0 @@
/**
* ds-network-monitor.js
* Network request monitoring and debugging
*
* REFACTORED: DSS-compliant version using DSBaseTool + table-template.js
* - Extends DSBaseTool for Shadow DOM, AbortController, and standardized lifecycle
* - Uses table-template.js for DSS-compliant table rendering (NO inline events/styles)
* - Event delegation pattern for all interactions
* - Logger utility instead of console.*
*
* Reference: .knowledge/dss-coding-standards.json
*/
import DSBaseTool from '../base/ds-base-tool.js';
import toolBridge from '../../services/tool-bridge.js';
import { ComponentHelpers } from '../../utils/component-helpers.js';
import { logger } from '../../utils/logger.js';
import { createTableView, setupTableEvents, createStatsCard } from '../../templates/table-template.js';
class DSNetworkMonitor extends DSBaseTool {
constructor() {
super();
this.requests = [];
this.filteredRequests = [];
this.filterUrl = '';
this.filterType = 'all';
this.autoRefresh = false;
this.refreshInterval = null;
}
connectedCallback() {
super.connectedCallback();
this.loadRequests();
}
disconnectedCallback() {
if (this.refreshInterval) {
clearInterval(this.refreshInterval);
}
super.disconnectedCallback();
}
/**
* Render the component (required by DSBaseTool)
*/
render() {
this.shadowRoot.innerHTML = `
<style>
:host {
display: block;
height: 100%;
}
.network-monitor-container {
padding: 16px;
height: 100%;
display: flex;
flex-direction: column;
}
.filter-controls {
margin-bottom: 16px;
display: flex;
gap: 12px;
align-items: center;
flex-wrap: wrap;
}
.filter-input {
flex: 1;
min-width: 200px;
padding: 6px 8px;
font-size: 12px;
background: var(--vscode-input-background);
color: var(--vscode-input-foreground);
border: 1px solid var(--vscode-input-border);
border-radius: 2px;
}
.filter-input:focus {
outline: 1px solid var(--vscode-focusBorder);
}
.filter-select {
width: 150px;
padding: 6px 8px;
font-size: 12px;
background: var(--vscode-input-background);
color: var(--vscode-input-foreground);
border: 1px solid var(--vscode-input-border);
border-radius: 2px;
}
.auto-refresh-label {
display: flex;
align-items: center;
gap: 6px;
font-size: 12px;
color: var(--vscode-foreground);
cursor: pointer;
}
.refresh-btn {
padding: 6px 12px;
font-size: 11px;
background: var(--vscode-button-background);
color: var(--vscode-button-foreground);
border: none;
border-radius: 2px;
cursor: pointer;
transition: background 0.15s ease;
}
.refresh-btn:hover {
background: var(--vscode-button-hoverBackground);
}
.content-wrapper {
flex: 1;
overflow: auto;
}
.loading {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 48px;
color: var(--vscode-descriptionForeground);
}
.loading-spinner {
font-size: 32px;
margin-bottom: 12px;
animation: spin 1s linear infinite;
}
@keyframes spin {
from { transform: rotate(0deg); }
to { transform: rotate(360deg); }
}
/* Badge styles */
.badge {
display: inline-block;
padding: 2px 6px;
border-radius: 3px;
font-size: 10px;
font-weight: 600;
}
.badge-info {
background: rgba(75, 181, 211, 0.2);
color: #4bb5d3;
}
.badge-success {
background: rgba(137, 209, 133, 0.2);
color: #89d185;
}
.badge-warning {
background: rgba(206, 145, 120, 0.2);
color: #ce9178;
}
.badge-error {
background: rgba(244, 135, 113, 0.2);
color: #f48771;
}
.code {
font-family: 'Courier New', monospace;
word-break: break-all;
}
.hint {
margin-top: 12px;
padding: 8px;
background-color: var(--vscode-sideBar-background);
border-radius: 4px;
font-size: 10px;
color: var(--vscode-descriptionForeground);
}
.info-count {
margin-bottom: 12px;
padding: 12px;
background-color: var(--vscode-sideBar-background);
border-radius: 4px;
font-size: 11px;
color: var(--vscode-descriptionForeground);
}
</style>
<div class="network-monitor-container">
<!-- Filter Controls -->
<div class="filter-controls">
<input
type="text"
id="network-filter"
placeholder="Filter by URL or method..."
class="filter-input"
/>
<select id="network-type-filter" class="filter-select">
<option value="all">All Types</option>
</select>
<label class="auto-refresh-label">
<input type="checkbox" id="auto-refresh-toggle" />
Auto-refresh
</label>
<button
id="network-refresh-btn"
data-action="refresh"
class="refresh-btn"
type="button"
aria-label="Refresh network requests">
🔄 Refresh
</button>
</div>
<!-- Content -->
<div class="content-wrapper" id="network-content">
<div class="loading">
<div class="loading-spinner">⏳</div>
<div>Initializing...</div>
</div>
</div>
</div>
`;
}
/**
* Setup event listeners (required by DSBaseTool)
*/
setupEventListeners() {
// EVENT-002: Event delegation
this.delegateEvents('.network-monitor-container', 'click', (action, e) => {
if (action === 'refresh') {
this.loadRequests();
}
});
// Filter input with debounce
const filterInput = this.$('#network-filter');
if (filterInput) {
const debouncedFilter = ComponentHelpers.debounce((term) => {
this.filterUrl = term.toLowerCase();
this.applyFilters();
}, 300);
this.bindEvent(filterInput, 'input', (e) => debouncedFilter(e.target.value));
}
// Type filter
const typeFilter = this.$('#network-type-filter');
if (typeFilter) {
this.bindEvent(typeFilter, 'change', (e) => {
this.filterType = e.target.value;
this.applyFilters();
});
}
// Auto-refresh toggle
const autoRefreshToggle = this.$('#auto-refresh-toggle');
if (autoRefreshToggle) {
this.bindEvent(autoRefreshToggle, 'change', (e) => {
this.autoRefresh = e.target.checked;
if (this.autoRefresh) {
this.refreshInterval = setInterval(() => this.loadRequests(), 2000);
logger.debug('[DSNetworkMonitor] Auto-refresh enabled');
} else {
if (this.refreshInterval) {
clearInterval(this.refreshInterval);
this.refreshInterval = null;
logger.debug('[DSNetworkMonitor] Auto-refresh disabled');
}
}
});
}
}
async loadRequests() {
const content = this.$('#network-content');
if (!content) return;
// Only show loading on first load
if (this.requests.length === 0) {
content.innerHTML = '<div class="loading"><div class="loading-spinner">⏳</div><div>Loading network requests...</div></div>';
}
try {
const result = await toolBridge.getNetworkRequests(null, 100);
if (result && result.requests) {
this.requests = result.requests;
this.updateTypeFilter();
this.applyFilters();
logger.debug('[DSNetworkMonitor] Loaded requests', { count: this.requests.length });
} else {
this.requests = [];
content.innerHTML = '<div class="table-empty"><div class="table-empty-icon">🌐</div><div class="table-empty-text">No network requests captured</div></div>';
}
} catch (error) {
logger.error('[DSNetworkMonitor] Failed to load network requests', error);
content.innerHTML = ComponentHelpers.renderError('Failed to load network requests', error);
}
}
updateTypeFilter() {
const typeFilter = this.$('#network-type-filter');
if (!typeFilter) return;
const types = this.getResourceTypes();
const currentValue = typeFilter.value;
typeFilter.innerHTML = `
<option value="all">All Types</option>
${types.map(type => `<option value="${type}" ${type === currentValue ? 'selected' : ''}>${type}</option>`).join('')}
`;
}
applyFilters() {
let filtered = [...this.requests];
// Filter by URL
if (this.filterUrl) {
filtered = filtered.filter(req =>
req.url.toLowerCase().includes(this.filterUrl) ||
req.method.toLowerCase().includes(this.filterUrl)
);
}
// Filter by type
if (this.filterType !== 'all') {
filtered = filtered.filter(req => req.resourceType === this.filterType);
}
this.filteredRequests = filtered;
this.renderRequests();
}
getResourceTypes() {
if (!this.requests) return [];
const types = new Set(this.requests.map(r => r.resourceType).filter(Boolean));
return Array.from(types).sort();
}
getStatusColor(status) {
if (status >= 200 && status < 300) return 'success';
if (status >= 300 && status < 400) return 'info';
if (status >= 400 && status < 500) return 'warning';
if (status >= 500) return 'error';
return 'info';
}
renderRequests() {
const content = this.$('#network-content');
if (!content) return;
if (!this.filteredRequests || this.filteredRequests.length === 0) {
content.innerHTML = `
<div class="table-empty">
<div class="table-empty-icon">🔍</div>
<div class="table-empty-text">${this.filterUrl ? 'No requests match your filter' : 'No network requests captured yet'}</div>
</div>
`;
return;
}
// Render info count
const infoHtml = `
<div class="info-count">
Showing ${this.filteredRequests.length} of ${this.requests.length} requests
${this.autoRefresh ? '• Auto-refreshing every 2s' : ''}
</div>
`;
// Use table-template.js for DSS-compliant rendering
const { html: tableHtml, styles: tableStyles } = createTableView({
columns: [
{ header: 'Method', key: 'method', width: '80px', align: 'left' },
{ header: 'Status', key: 'status', width: '80px', align: 'left' },
{ header: 'URL', key: 'url', align: 'left' },
{ header: 'Type', key: 'resourceType', width: '100px', align: 'left' },
{ header: 'Time', key: 'timing', width: '80px', align: 'left' }
],
rows: this.filteredRequests,
renderCell: (col, row) => this.renderCell(col, row),
renderDetails: (row) => this.renderDetails(row),
emptyMessage: 'No network requests',
emptyIcon: '🌐'
});
// Adopt table styles
this.adoptStyles(tableStyles);
// Render table
content.innerHTML = infoHtml + tableHtml + '<div class="hint">💡 Click any row to view full request details</div>';
// Setup table event handlers
setupTableEvents(this.shadowRoot);
logger.debug('[DSNetworkMonitor] Rendered requests', { count: this.filteredRequests.length });
}
renderCell(col, row) {
const method = row.method || 'GET';
const status = row.status || '-';
const statusColor = this.getStatusColor(status);
const resourceType = row.resourceType || 'other';
const url = row.url || 'Unknown URL';
const timing = row.timing ? `${Math.round(row.timing)}ms` : '-';
switch (col.key) {
case 'method':
const methodColor = method === 'GET' ? 'info' : method === 'POST' ? 'success' : 'warning';
return `<span class="badge badge-${methodColor}">${this.escapeHtml(method)}</span>`;
case 'status':
return `<span class="badge badge-${statusColor}">${this.escapeHtml(String(status))}</span>`;
case 'url':
return `<span class="code" style="max-width: 400px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; display: block;">${this.escapeHtml(url)}</span>`;
case 'resourceType':
return `<span class="badge badge-info">${this.escapeHtml(resourceType)}</span>`;
case 'timing':
return `<span style="color: var(--vscode-descriptionForeground);">${timing}</span>`;
default:
return this.escapeHtml(String(row[col.key] || '-'));
}
}
renderDetails(row) {
const method = row.method || 'GET';
const status = row.status || '-';
const url = row.url || 'Unknown URL';
const resourceType = row.resourceType || 'other';
return `
<div style="margin-bottom: 8px;">
<span class="detail-label">URL:</span>
<span class="detail-value code">${this.escapeHtml(url)}</span>
</div>
<div style="margin-bottom: 8px;">
<span class="detail-label">Method:</span>
<span class="detail-value code">${this.escapeHtml(method)}</span>
</div>
<div style="margin-bottom: 8px;">
<span class="detail-label">Status:</span>
<span class="detail-value code">${this.escapeHtml(String(status))}</span>
</div>
<div style="margin-bottom: 8px;">
<span class="detail-label">Type:</span>
<span class="detail-value code">${this.escapeHtml(resourceType)}</span>
</div>
${row.headers ? `
<div style="margin-top: 12px;">
<div class="detail-label" style="display: block; margin-bottom: 4px;">Headers:</div>
<pre class="detail-code">${this.escapeHtml(JSON.stringify(row.headers, null, 2))}</pre>
</div>
` : ''}
`;
}
}
customElements.define('ds-network-monitor', DSNetworkMonitor);
export default DSNetworkMonitor;

View File

@@ -1,268 +0,0 @@
/**
* ds-project-analysis.js
* Project analysis results viewer showing token usage, component adoption, etc.
* UI Team Tool #4
*/
import { ComponentHelpers } from '../../utils/component-helpers.js';
import contextStore from '../../stores/context-store.js';
import toolBridge from '../../services/tool-bridge.js';
class DSProjectAnalysis extends HTMLElement {
constructor() {
super();
this.analysisResults = null;
this.isAnalyzing = false;
}
async connectedCallback() {
this.render();
this.setupEventListeners();
await this.loadCachedResults();
}
async loadCachedResults() {
try {
const context = contextStore.getMCPContext();
if (!context.project_id) return;
const cached = localStorage.getItem(`analysis_${context.project_id}`);
if (cached) {
this.analysisResults = JSON.parse(cached);
this.renderResults();
}
} catch (error) {
console.error('[DSProjectAnalysis] Failed to load cached results:', error);
}
}
setupEventListeners() {
const analyzeBtn = this.querySelector('#analyze-project-btn');
const pathInput = this.querySelector('#project-path-input');
if (analyzeBtn) {
analyzeBtn.addEventListener('click', () => this.analyzeProject());
}
if (pathInput) {
pathInput.addEventListener('keypress', (e) => {
if (e.key === 'Enter') {
this.analyzeProject();
}
});
}
}
async analyzeProject() {
const pathInput = this.querySelector('#project-path-input');
const projectPath = pathInput?.value.trim() || '';
if (!projectPath) {
ComponentHelpers.showToast?.('Please enter a project path', 'error');
return;
}
this.isAnalyzing = true;
this.updateLoadingState();
try {
// Call dss_analyze_project MCP tool
const result = await toolBridge.executeTool('dss_analyze_project', {
path: projectPath
});
this.analysisResults = result;
// Cache results
const context = contextStore.getMCPContext();
if (context.project_id) {
localStorage.setItem(`analysis_${context.project_id}`, JSON.stringify(result));
}
this.renderResults();
ComponentHelpers.showToast?.('Project analysis complete', 'success');
} catch (error) {
console.error('[DSProjectAnalysis] Analysis failed:', error);
ComponentHelpers.showToast?.(`Analysis failed: ${error.message}`, 'error');
const resultsContainer = this.querySelector('#results-container');
if (resultsContainer) {
resultsContainer.innerHTML = ComponentHelpers.renderError('Project analysis failed', error);
}
} finally {
this.isAnalyzing = false;
this.updateLoadingState();
}
}
updateLoadingState() {
const analyzeBtn = this.querySelector('#analyze-project-btn');
const resultsContainer = this.querySelector('#results-container');
if (!analyzeBtn || !resultsContainer) return;
if (this.isAnalyzing) {
analyzeBtn.disabled = true;
analyzeBtn.textContent = '⏳ Analyzing...';
resultsContainer.innerHTML = ComponentHelpers.renderLoading('Analyzing project structure and token usage...');
} else {
analyzeBtn.disabled = false;
analyzeBtn.textContent = '🔍 Analyze Project';
}
}
renderResults() {
const resultsContainer = this.querySelector('#results-container');
if (!resultsContainer || !this.analysisResults) return;
const { patterns, components, tokens, dependencies } = this.analysisResults;
resultsContainer.innerHTML = `
<div style="padding: 16px; overflow: auto; height: 100%;">
<!-- Summary Cards -->
<div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 16px; margin-bottom: 24px;">
${this.createStatCard('Components Found', components?.length || 0, '🧩')}
${this.createStatCard('Patterns Detected', patterns?.length || 0, '🎨')}
${this.createStatCard('Tokens Used', Object.keys(tokens || {}).length, '🎯')}
${this.createStatCard('Dependencies', dependencies?.length || 0, '📦')}
</div>
<!-- Patterns Section -->
${patterns && patterns.length > 0 ? `
<div style="background: var(--vscode-sidebar); border: 1px solid var(--vscode-border); border-radius: 4px; padding: 16px; margin-bottom: 16px;">
<h4 style="font-size: 12px; font-weight: 600; margin-bottom: 12px;">Design Patterns</h4>
<div style="display: flex; flex-direction: column; gap: 8px;">
${patterns.map(pattern => `
<div style="padding: 8px; background: var(--vscode-bg); border-radius: 2px; font-size: 11px;">
<div style="font-weight: 600; margin-bottom: 4px;">${ComponentHelpers.escapeHtml(pattern.name)}</div>
<div style="color: var(--vscode-text-dim);">
${ComponentHelpers.escapeHtml(pattern.description)} • Used ${pattern.count} times
</div>
</div>
`).join('')}
</div>
</div>
` : ''}
<!-- Components Section -->
${components && components.length > 0 ? `
<div style="background: var(--vscode-sidebar); border: 1px solid var(--vscode-border); border-radius: 4px; padding: 16px; margin-bottom: 16px;">
<h4 style="font-size: 12px; font-weight: 600; margin-bottom: 12px;">React Components</h4>
<div style="max-height: 300px; overflow-y: auto;">
<table style="width: 100%; font-size: 11px; border-collapse: collapse;">
<thead style="position: sticky; top: 0; background: var(--vscode-sidebar);">
<tr>
<th style="text-align: left; padding: 6px; border-bottom: 1px solid var(--vscode-border);">Component</th>
<th style="text-align: left; padding: 6px; border-bottom: 1px solid var(--vscode-border);">Path</th>
<th style="text-align: right; padding: 6px; border-bottom: 1px solid var(--vscode-border);">DS Adoption</th>
</tr>
</thead>
<tbody>
${components.slice(0, 20).map(comp => `
<tr style="border-bottom: 1px solid var(--vscode-border);">
<td style="padding: 6px; font-family: monospace;">${ComponentHelpers.escapeHtml(comp.name)}</td>
<td style="padding: 6px; color: var(--vscode-text-dim);">${ComponentHelpers.escapeHtml(comp.path)}</td>
<td style="padding: 6px; text-align: right;">
${this.renderAdoptionBadge(comp.dsAdoption || 0)}
</td>
</tr>
`).join('')}
</tbody>
</table>
</div>
</div>
` : ''}
<!-- Token Usage Section -->
${tokens && Object.keys(tokens).length > 0 ? `
<div style="background: var(--vscode-sidebar); border: 1px solid var(--vscode-border); border-radius: 4px; padding: 16px;">
<h4 style="font-size: 12px; font-weight: 600; margin-bottom: 12px;">Token Usage</h4>
<div style="display: flex; flex-wrap: wrap; gap: 8px;">
${Object.entries(tokens).slice(0, 30).map(([key, count]) => `
<div style="padding: 4px 8px; background: var(--vscode-bg); border-radius: 2px; font-size: 10px; font-family: monospace;">
${ComponentHelpers.escapeHtml(key)} <span style="color: var(--vscode-text-dim);">(${count})</span>
</div>
`).join('')}
</div>
</div>
` : ''}
</div>
`;
}
createStatCard(label, value, icon) {
return `
<div style="background: var(--vscode-sidebar); border: 1px solid var(--vscode-border); border-radius: 4px; padding: 16px; text-align: center;">
<div style="font-size: 32px; margin-bottom: 8px;">${icon}</div>
<div style="font-size: 24px; font-weight: 600; margin-bottom: 4px;">${value}</div>
<div style="font-size: 11px; color: var(--vscode-text-dim);">${label}</div>
</div>
`;
}
renderAdoptionBadge(percentage) {
let color = '#f48771';
let label = 'Low';
if (percentage >= 80) {
color = '#89d185';
label = 'High';
} else if (percentage >= 50) {
color = '#ffbf00';
label = 'Medium';
}
return `<span style="padding: 2px 6px; background: ${color}; border-radius: 2px; font-size: 10px; font-weight: 600;">${label}</span>`;
}
render() {
this.innerHTML = `
<div style="display: flex; flex-direction: column; height: 100%;">
<!-- Header -->
<div style="padding: 16px; border-bottom: 1px solid var(--vscode-border); background: var(--vscode-sidebar);">
<h3 style="font-size: 12px; font-weight: 600; margin-bottom: 12px;">Project Analysis</h3>
<div style="display: grid; grid-template-columns: 1fr auto; gap: 12px; align-items: end;">
<div>
<label style="display: block; font-size: 11px; font-weight: 600; margin-bottom: 4px; color: var(--vscode-text-dim);">
Project Path
</label>
<input
type="text"
id="project-path-input"
placeholder="/path/to/your/project"
class="input"
style="width: 100%; font-size: 11px; font-family: monospace;"
/>
</div>
<button id="analyze-project-btn" class="button" style="font-size: 11px; padding: 6px 16px;">
🔍 Analyze Project
</button>
</div>
<div style="margin-top: 8px; font-size: 10px; color: var(--vscode-text-dim);">
💡 Analyzes components, patterns, token usage, and design system adoption
</div>
</div>
<!-- Results Container -->
<div id="results-container" style="flex: 1; overflow: hidden;">
<div style="display: flex; align-items: center; justify-content: center; height: 100%; text-align: center; padding: 48px;">
<div>
<div style="font-size: 48px; margin-bottom: 16px;">🔍</div>
<h3 style="font-size: 14px; font-weight: 600; margin-bottom: 8px;">Ready to Analyze</h3>
<p style="font-size: 12px; color: var(--vscode-text-dim);">
Enter your project path above to analyze component usage and design system adoption
</p>
</div>
</div>
</div>
</div>
`;
}
}
customElements.define('ds-project-analysis', DSProjectAnalysis);
export default DSProjectAnalysis;

View File

@@ -1,278 +0,0 @@
/**
* ds-quick-wins-script.js
* Quick Wins analyzer - finds low-effort, high-impact design system improvements
* MVP2: Identifies inconsistencies and suggests standardization opportunities
*/
export default class QuickWinsScript extends HTMLElement {
constructor() {
super();
this.analysisResults = null;
this.isAnalyzing = false;
}
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 Quick Wins</h1>
<p style="margin: 0; color: var(--vscode-text-dim);">
Identify low-effort, high-impact improvements to your design system
</p>
</div>
<!-- Analysis Controls -->
<div style="background: var(--vscode-sidebar); border: 1px solid var(--vscode-border); border-radius: 4px; padding: 16px; margin-bottom: 24px;">
<div style="margin-bottom: 12px;">
<label style="display: block; font-size: 12px; font-weight: 500; margin-bottom: 8px;">
What to analyze
</label>
<div style="display: grid; grid-template-columns: 1fr 1fr; gap: 8px;">
<label style="display: flex; align-items: center; gap: 6px; cursor: pointer; font-size: 12px;">
<input type="checkbox" id="check-tokens" checked />
Design Tokens
</label>
<label style="display: flex; align-items: center; gap: 6px; cursor: pointer; font-size: 12px;">
<input type="checkbox" id="check-colors" checked />
Color Usage
</label>
<label style="display: flex; align-items: center; gap: 6px; cursor: pointer; font-size: 12px;">
<input type="checkbox" id="check-spacing" checked />
Spacing Values
</label>
<label style="display: flex; align-items: center; gap: 6px; cursor: pointer; font-size: 12px;">
<input type="checkbox" id="check-typography" checked />
Typography
</label>
</div>
</div>
<button id="analyze-btn" aria-label="Analyze design system for improvement opportunities" style="
width: 100%;
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;
">Analyze Design System</button>
</div>
<!-- Loading State -->
<div id="loading-container" style="display: none; text-align: center; padding: 48px; background: var(--vscode-sidebar); border: 1px solid var(--vscode-border); border-radius: 4px;">
<div style="font-size: 24px; margin-bottom: 12px;">⏳</div>
<div style="font-size: 12px; color: var(--vscode-text-dim);">
Analyzing design system...
</div>
</div>
<!-- Results Container -->
<div id="results-container" style="display: none;">
<!-- Results will be inserted here -->
</div>
</div>
`;
}
setupEventListeners() {
const analyzeBtn = this.querySelector('#analyze-btn');
if (analyzeBtn) {
analyzeBtn.addEventListener('click', () => this.analyzeDesignSystem());
}
}
async analyzeDesignSystem() {
this.isAnalyzing = true;
const loadingContainer = this.querySelector('#loading-container');
const resultsContainer = this.querySelector('#results-container');
loadingContainer.style.display = 'block';
resultsContainer.style.display = 'none';
// Simulate analysis
await new Promise(resolve => setTimeout(resolve, 1500));
this.analysisResults = this.generateAnalysisResults();
this.renderResults();
loadingContainer.style.display = 'none';
resultsContainer.style.display = 'block';
this.isAnalyzing = false;
}
generateAnalysisResults() {
return [
{
title: 'Consolidate Color Palette',
impact: 'high',
effort: 'low',
description: 'Found 23 unique colors in codebase, but only 8 are documented tokens. Consolidate to reduce cognitive load.',
recommendation: 'Extract 15 undocumented colors and add to token library',
estimate: '2 hours',
files_affected: 34
},
{
title: 'Standardize Spacing Scale',
impact: 'high',
effort: 'low',
description: 'Spacing values are inconsistent (4px, 6px, 8px, 12px, 16px, 20px, 24px, 32px). Reduce to 6-8 standard values.',
recommendation: 'Use 4px, 8px, 12px, 16px, 24px, 32px as standard spacing scale',
estimate: '3 hours',
files_affected: 67
},
{
title: 'Create Typography System',
impact: 'high',
effort: 'medium',
description: 'Typography scales vary across components. Establish consistent type hierarchy.',
recommendation: 'Define 5 font sizes (12px, 14px, 16px, 18px, 24px) with line-height ratios',
estimate: '4 hours',
files_affected: 45
},
{
title: 'Document Component Variants',
impact: 'medium',
effort: 'low',
description: 'Button component has 7 undocumented variants in use. Update documentation.',
recommendation: 'Add variant definitions and usage guidelines to Storybook',
estimate: '1 hour',
files_affected: 12
},
{
title: 'Establish Naming Convention',
impact: 'medium',
effort: 'low',
description: 'Token names are inconsistent (color-primary vs primaryColor vs primary-color).',
recommendation: 'Adopt kebab-case convention: color-primary, spacing-sm, font-body',
estimate: '2 hours',
files_affected: 89
},
{
title: 'Create Shadow System',
impact: 'medium',
effort: 'medium',
description: 'Shadow values are hardcoded throughout. Create reusable shadow tokens.',
recommendation: 'Define 3-4 elevation levels: shadow-sm, shadow-md, shadow-lg, shadow-xl',
estimate: '2 hours',
files_affected: 23
}
];
}
renderResults() {
const container = this.querySelector('#results-container');
const results = this.analysisResults;
const highImpact = results.filter(r => r.impact === 'high');
const mediumImpact = results.filter(r => r.impact === 'medium');
const totalFiles = results.reduce((sum, r) => sum + r.files_affected, 0);
// Build stats efficiently
const statsHtml = this.buildStatsCards(results.length, highImpact.length, totalFiles);
// Build cards with memoization
const highImpactHtml = highImpact.map(win => this.renderWinCard(win)).join('');
const mediumImpactHtml = mediumImpact.map(win => this.renderWinCard(win)).join('');
let html = `
<div style="margin-bottom: 24px;">
${statsHtml}
</div>
<div style="margin-bottom: 24px;">
<h2 style="margin: 0 0 12px 0; font-size: 14px; color: #FF9800;">High Impact Opportunities</h2>
${highImpactHtml}
</div>
<div>
<h2 style="margin: 0 0 12px 0; font-size: 14px; color: #0066CC;">Medium Impact Opportunities</h2>
${mediumImpactHtml}
</div>
`;
container.innerHTML = html;
}
buildStatsCards(total, highCount, fileCount) {
return `
<div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(150px, 1fr)); gap: 12px; margin-bottom: 24px;">
<div style="background: var(--vscode-sidebar); padding: 12px; border-radius: 4px; text-align: center;">
<div style="font-size: 24px; font-weight: 600; color: #4CAF50;">${total}</div>
<div style="font-size: 11px; color: var(--vscode-text-dim);">Total Opportunities</div>
</div>
<div style="background: var(--vscode-sidebar); padding: 12px; border-radius: 4px; text-align: center;">
<div style="font-size: 24px; font-weight: 600; color: #FF9800;">${highCount}</div>
<div style="font-size: 11px; color: var(--vscode-text-dim);">High Impact</div>
</div>
<div style="background: var(--vscode-sidebar); padding: 12px; border-radius: 4px; text-align: center;">
<div style="font-size: 24px; font-weight: 600; color: #0066CC;">${fileCount}</div>
<div style="font-size: 11px; color: var(--vscode-text-dim);">Files Affected</div>
</div>
</div>
`;
}
renderWinCard(win) {
const impactColor = win.impact === 'high' ? '#FF9800' : '#0066CC';
const effortColor = win.effort === 'low' ? '#4CAF50' : win.effort === 'medium' ? '#FF9800' : '#F44336';
return `
<div style="
background: var(--vscode-sidebar);
border: 1px solid var(--vscode-border);
border-radius: 4px;
padding: 12px;
margin-bottom: 12px;
">
<div style="display: flex; justify-content: space-between; align-items: start; margin-bottom: 8px;">
<div style="font-weight: 500; font-size: 13px;">${win.title}</div>
<div style="display: flex; gap: 6px;">
<span style="
padding: 2px 8px;
border-radius: 2px;
font-size: 10px;
background: ${impactColor};
color: white;
">${win.impact} impact</span>
<span style="
padding: 2px 8px;
border-radius: 2px;
font-size: 10px;
background: ${effortColor};
color: white;
">${win.effort} effort</span>
</div>
</div>
<p style="margin: 0 0 8px 0; font-size: 12px; color: var(--vscode-text-dim);">
${win.description}
</p>
<div style="
background: var(--vscode-bg);
padding: 8px;
border-radius: 3px;
margin-bottom: 8px;
font-size: 11px;
color: #CE9178;
">
<strong>Recommendation:</strong> ${win.recommendation}
</div>
<div style="display: flex; justify-content: space-between; font-size: 10px; color: var(--vscode-text-dim);">
<span>⏱️ ${win.estimate}</span>
<span>📁 ${win.files_affected} files</span>
</div>
</div>
`;
}
}
customElements.define('ds-quick-wins-script', QuickWinsScript);

View File

@@ -1,305 +0,0 @@
/**
* ds-quick-wins.js
* Identifies low-effort, high-impact opportunities for design system adoption
* UI Team Tool #5
*/
import { ComponentHelpers } from '../../utils/component-helpers.js';
import contextStore from '../../stores/context-store.js';
import toolBridge from '../../services/tool-bridge.js';
class DSQuickWins extends HTMLElement {
constructor() {
super();
this.quickWins = null;
this.isAnalyzing = false;
}
async connectedCallback() {
this.render();
this.setupEventListeners();
await this.loadCachedResults();
}
async loadCachedResults() {
try {
const context = contextStore.getMCPContext();
if (!context.project_id) return;
const cached = localStorage.getItem(`quickwins_${context.project_id}`);
if (cached) {
this.quickWins = JSON.parse(cached);
this.renderResults();
}
} catch (error) {
console.error('[DSQuickWins] Failed to load cached results:', error);
}
}
setupEventListeners() {
const analyzeBtn = this.querySelector('#analyze-quick-wins-btn');
const pathInput = this.querySelector('#project-path-input');
if (analyzeBtn) {
analyzeBtn.addEventListener('click', () => this.analyzeQuickWins());
}
if (pathInput) {
pathInput.addEventListener('keypress', (e) => {
if (e.key === 'Enter') {
this.analyzeQuickWins();
}
});
}
}
async analyzeQuickWins() {
const pathInput = this.querySelector('#project-path-input');
const projectPath = pathInput?.value.trim() || '';
if (!projectPath) {
ComponentHelpers.showToast?.('Please enter a project path', 'error');
return;
}
this.isAnalyzing = true;
this.updateLoadingState();
try {
// Call dss_find_quick_wins MCP tool
const result = await toolBridge.executeTool('dss_find_quick_wins', {
path: projectPath
});
this.quickWins = result;
// Cache results
const context = contextStore.getMCPContext();
if (context.project_id) {
localStorage.setItem(`quickwins_${context.project_id}`, JSON.stringify(result));
}
this.renderResults();
ComponentHelpers.showToast?.('Quick wins analysis complete', 'success');
} catch (error) {
console.error('[DSQuickWins] Analysis failed:', error);
ComponentHelpers.showToast?.(`Analysis failed: ${error.message}`, 'error');
const resultsContainer = this.querySelector('#results-container');
if (resultsContainer) {
resultsContainer.innerHTML = ComponentHelpers.renderError('Quick wins analysis failed', error);
}
} finally {
this.isAnalyzing = false;
this.updateLoadingState();
}
}
updateLoadingState() {
const analyzeBtn = this.querySelector('#analyze-quick-wins-btn');
const resultsContainer = this.querySelector('#results-container');
if (!analyzeBtn || !resultsContainer) return;
if (this.isAnalyzing) {
analyzeBtn.disabled = true;
analyzeBtn.textContent = '⏳ Analyzing...';
resultsContainer.innerHTML = ComponentHelpers.renderLoading('Identifying quick win opportunities...');
} else {
analyzeBtn.disabled = false;
analyzeBtn.textContent = '⚡ Find Quick Wins';
}
}
renderResults() {
const resultsContainer = this.querySelector('#results-container');
if (!resultsContainer || !this.quickWins) return;
const opportunities = this.quickWins.opportunities || [];
const totalImpact = opportunities.reduce((sum, opp) => sum + (opp.impact || 0), 0);
const avgEffort = opportunities.length > 0
? (opportunities.reduce((sum, opp) => sum + (opp.effort || 0), 0) / opportunities.length).toFixed(1)
: 0;
resultsContainer.innerHTML = `
<div style="padding: 16px; overflow: auto; height: 100%;">
<!-- Summary -->
<div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(150px, 1fr)); gap: 16px; margin-bottom: 24px;">
${this.createStatCard('Opportunities', opportunities.length, '⚡')}
${this.createStatCard('Total Impact', `${totalImpact}%`, '📈')}
${this.createStatCard('Avg Effort', `${avgEffort}h`, '⏱️')}
</div>
<!-- Opportunities List -->
${opportunities.length === 0 ? ComponentHelpers.renderEmpty('No quick wins found', '✨') : `
<div style="display: flex; flex-direction: column; gap: 12px;">
${opportunities.sort((a, b) => (b.impact || 0) - (a.impact || 0)).map((opp, idx) => `
<div style="background: var(--vscode-sidebar); border: 1px solid var(--vscode-border); border-radius: 4px; padding: 16px;">
<div style="display: flex; justify-content: space-between; align-items: start; margin-bottom: 12px;">
<div style="flex: 1;">
<div style="display: flex; align-items: center; gap: 8px; margin-bottom: 6px;">
<h4 style="font-size: 12px; font-weight: 600;">${ComponentHelpers.escapeHtml(opp.title)}</h4>
${this.renderPriorityBadge(opp.priority || 'medium')}
</div>
<p style="font-size: 11px; color: var(--vscode-text-dim); margin-bottom: 8px;">
${ComponentHelpers.escapeHtml(opp.description)}
</p>
</div>
<div style="text-align: right; margin-left: 16px;">
<div style="font-size: 10px; color: var(--vscode-text-dim); margin-bottom: 4px;">Impact</div>
<div style="font-size: 20px; font-weight: 600; color: #89d185;">${opp.impact || 0}%</div>
</div>
</div>
<!-- Metrics -->
<div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(120px, 1fr)); gap: 12px; font-size: 11px; margin-bottom: 12px;">
<div>
<div style="color: var(--vscode-text-dim); margin-bottom: 2px;">Effort</div>
<div style="font-weight: 600;">${opp.effort || 0} hours</div>
</div>
<div>
<div style="color: var(--vscode-text-dim); margin-bottom: 2px;">Files Affected</div>
<div style="font-weight: 600;">${opp.filesAffected || 0}</div>
</div>
<div>
<div style="color: var(--vscode-text-dim); margin-bottom: 2px;">Type</div>
<div style="font-weight: 600;">${ComponentHelpers.escapeHtml(opp.type || 'refactor')}</div>
</div>
</div>
<!-- Actions -->
<div style="display: flex; gap: 8px;">
<button class="button apply-quick-win-btn" data-idx="${idx}" style="font-size: 10px; padding: 4px 12px;">
✨ Apply Fix
</button>
<button class="button view-files-btn" data-idx="${idx}" style="font-size: 10px; padding: 4px 12px;">
📁 View Files
</button>
</div>
${opp.files && opp.files.length > 0 ? `
<details style="margin-top: 12px;">
<summary style="font-size: 10px; color: var(--vscode-text-dim); cursor: pointer;">
Affected Files (${opp.files.length})
</summary>
<div style="margin-top: 8px; padding: 8px; background: var(--vscode-bg); border-radius: 2px; font-family: monospace; font-size: 10px;">
${opp.files.slice(0, 10).map(file => `<div style="padding: 2px 0;">${ComponentHelpers.escapeHtml(file)}</div>`).join('')}
${opp.files.length > 10 ? `<div style="padding: 2px 0; color: var(--vscode-text-dim);">...and ${opp.files.length - 10} more</div>` : ''}
</div>
</details>
` : ''}
</div>
`).join('')}
</div>
`}
</div>
`;
// Setup button handlers
const applyBtns = resultsContainer.querySelectorAll('.apply-quick-win-btn');
const viewBtns = resultsContainer.querySelectorAll('.view-files-btn');
applyBtns.forEach(btn => {
btn.addEventListener('click', () => {
const idx = parseInt(btn.dataset.idx);
this.applyQuickWin(opportunities[idx]);
});
});
viewBtns.forEach(btn => {
btn.addEventListener('click', () => {
const idx = parseInt(btn.dataset.idx);
this.viewFiles(opportunities[idx]);
});
});
}
createStatCard(label, value, icon) {
return `
<div style="background: var(--vscode-sidebar); border: 1px solid var(--vscode-border); border-radius: 4px; padding: 16px; text-align: center;">
<div style="font-size: 32px; margin-bottom: 8px;">${icon}</div>
<div style="font-size: 20px; font-weight: 600; margin-bottom: 4px;">${value}</div>
<div style="font-size: 11px; color: var(--vscode-text-dim);">${label}</div>
</div>
`;
}
renderPriorityBadge(priority) {
const config = {
high: { color: '#f48771', label: 'High Priority' },
medium: { color: '#ffbf00', label: 'Medium Priority' },
low: { color: '#89d185', label: 'Low Priority' }
};
const { color, label } = config[priority] || config.medium;
return `<span style="padding: 2px 8px; background: ${color}; border-radius: 2px; font-size: 10px; font-weight: 600;">${label}</span>`;
}
applyQuickWin(opportunity) {
ComponentHelpers.showToast?.(`Applying: ${opportunity.title}`, 'info');
// In real implementation, this would trigger automated refactoring
console.log('Apply quick win:', opportunity);
}
viewFiles(opportunity) {
if (!opportunity.files || opportunity.files.length === 0) {
ComponentHelpers.showToast?.('No files associated with this opportunity', 'info');
return;
}
console.log('View files:', opportunity.files);
ComponentHelpers.showToast?.(`${opportunity.files.length} files affected`, 'info');
}
render() {
this.innerHTML = `
<div style="display: flex; flex-direction: column; height: 100%;">
<!-- Header -->
<div style="padding: 16px; border-bottom: 1px solid var(--vscode-border); background: var(--vscode-sidebar);">
<h3 style="font-size: 12px; font-weight: 600; margin-bottom: 12px;">Quick Wins Identification</h3>
<div style="display: grid; grid-template-columns: 1fr auto; gap: 12px; align-items: end;">
<div>
<label style="display: block; font-size: 11px; font-weight: 600; margin-bottom: 4px; color: var(--vscode-text-dim);">
Project Path
</label>
<input
type="text"
id="project-path-input"
placeholder="/path/to/your/project"
class="input"
style="width: 100%; font-size: 11px; font-family: monospace;"
/>
</div>
<button id="analyze-quick-wins-btn" class="button" style="font-size: 11px; padding: 6px 16px;">
⚡ Find Quick Wins
</button>
</div>
<div style="margin-top: 8px; font-size: 10px; color: var(--vscode-text-dim);">
💡 Identifies low-effort, high-impact opportunities for design system adoption
</div>
</div>
<!-- Results Container -->
<div id="results-container" style="flex: 1; overflow: hidden;">
<div style="display: flex; align-items: center; justify-content: center; height: 100%; text-align: center; padding: 48px;">
<div>
<div style="font-size: 48px; margin-bottom: 16px;">⚡</div>
<h3 style="font-size: 14px; font-weight: 600; margin-bottom: 8px;">Ready to Find Quick Wins</h3>
<p style="font-size: 12px; color: var(--vscode-text-dim);">
Enter your project path above to identify low-effort, high-impact improvements
</p>
</div>
</div>
</div>
</div>
`;
}
}
customElements.define('ds-quick-wins', DSQuickWins);
export default DSQuickWins;

View File

@@ -1,115 +0,0 @@
export default class RegressionTesting extends HTMLElement {
constructor() {
super();
this.regressions = [];
this.isRunning = false;
}
connectedCallback() {
this.render();
this.setupEventListeners();
}
render() {
this.innerHTML = `
<div style="padding: 24px; height: 100%; overflow-y: auto;">
<h1 style="margin: 0 0 8px 0; font-size: 24px;">Visual Regression Testing</h1>
<p style="margin: 0 0 24px 0; color: var(--vscode-text-dim);">Detect visual changes in design system components</p>
<div style="background: var(--vscode-sidebar); border: 1px solid var(--vscode-border); border-radius: 4px; padding: 16px; margin-bottom: 24px;">
<label style="display: block; font-size: 12px; font-weight: 500; margin-bottom: 8px;">Components to Test</label>
<div style="display: grid; grid-template-columns: 1fr 1fr; gap: 8px; margin-bottom: 12px;">
<label style="display: flex; align-items: center; gap: 6px; cursor: pointer; font-size: 12px;">
<input type="checkbox" checked /> Buttons
</label>
<label style="display: flex; align-items: center; gap: 6px; cursor: pointer; font-size: 12px;">
<input type="checkbox" checked /> Inputs
</label>
<label style="display: flex; align-items: center; gap: 6px; cursor: pointer; font-size: 12px;">
<input type="checkbox" checked /> Cards
</label>
<label style="display: flex; align-items: center; gap: 6px; cursor: pointer; font-size: 12px;">
<input type="checkbox" checked /> Modals
</label>
</div>
<button id="run-tests-btn" style="width: 100%; padding: 8px; background: var(--vscode-button-background); color: var(--vscode-button-foreground); border: none; border-radius: 4px; cursor: pointer; font-weight: 500; font-size: 12px;">Run Tests</button>
</div>
<div id="progress-container" style="display: none; margin-bottom: 24px;">
<div style="margin-bottom: 8px; font-size: 12px; color: var(--vscode-text-dim);">Testing... <span id="progress-count">0/4</span></div>
<div style="width: 100%; height: 6px; background: var(--vscode-bg); border-radius: 3px; overflow: hidden;">
<div id="progress-bar" style="width: 0%; height: 100%; background: #0066CC; transition: width 0.3s;"></div>
</div>
</div>
<div id="results-container" style="display: none;"></div>
</div>
`;
}
setupEventListeners() {
this.querySelector('#run-tests-btn').addEventListener('click', () => this.runTests());
}
async runTests() {
this.isRunning = true;
this.querySelector('#progress-container').style.display = 'block';
this.querySelector('#results-container').style.display = 'none';
const components = ['Buttons', 'Inputs', 'Cards', 'Modals'];
this.regressions = [];
for (let i = 0; i < components.length; i++) {
this.querySelector('#progress-count').textContent = (i + 1) + '/4';
this.querySelector('#progress-bar').style.width = ((i + 1) / 4 * 100) + '%';
await new Promise(resolve => setTimeout(resolve, 600));
if (Math.random() > 0.7) {
this.regressions.push({
component: components[i],
severity: Math.random() > 0.5 ? 'critical' : 'minor'
});
}
}
this.renderResults();
this.querySelector('#progress-container').style.display = 'none';
this.querySelector('#results-container').style.display = 'block';
}
renderResults() {
const container = this.querySelector('#results-container');
const passed = 4 - this.regressions.length;
let html = `<div style="display: grid; grid-template-columns: 1fr 1fr; gap: 12px; margin-bottom: 24px;">
<div style="background: var(--vscode-sidebar); padding: 12px; border-radius: 4px; text-align: center; border: 2px solid #4CAF50;">
<div style="font-size: 24px; font-weight: 600; color: #4CAF50;">${passed}</div>
<div style="font-size: 11px; color: var(--vscode-text-dim);">Passed</div>
</div>
<div style="background: var(--vscode-sidebar); padding: 12px; border-radius: 4px; text-align: center; border: 2px solid ${this.regressions.length > 0 ? '#F44336' : '#4CAF50'};">
<div style="font-size: 24px; font-weight: 600; color: ${this.regressions.length > 0 ? '#F44336' : '#4CAF50'};">${this.regressions.length}</div>
<div style="font-size: 11px; color: var(--vscode-text-dim);">Regressions</div>
</div>
</div>`;
if (this.regressions.length === 0) {
html += `<div style="background: var(--vscode-sidebar); border: 1px solid var(--vscode-border); border-radius: 4px; padding: 24px; text-align: center;"><div style="font-size: 20px; margin-bottom: 8px;">All Tests Passed</div></div>`;
} else {
html += `<h2 style="margin: 0 0 12px 0; font-size: 14px;">Regressions Found</h2>`;
for (let reg of this.regressions) {
const color = reg.severity === 'critical' ? '#F44336' : '#FF9800';
html += `<div style="background: var(--vscode-sidebar); border: 1px solid var(--vscode-border); border-radius: 4px; padding: 12px; margin-bottom: 12px;">
<div style="display: flex; justify-content: space-between; margin-bottom: 8px;">
<div style="font-weight: 500;">${reg.component}</div>
<span style="padding: 2px 8px; background: ${color}; color: white; border-radius: 2px; font-size: 10px;">${reg.severity}</span>
</div>
<button style="width: 100%; padding: 6px; background: var(--vscode-button-background); color: var(--vscode-button-foreground); border: none; border-radius: 3px; cursor: pointer; font-size: 10px;">Approve as Baseline</button>
</div>`;
}
}
container.innerHTML = html;
}
}
customElements.define('ds-regression-testing', RegressionTesting);

View File

@@ -1,552 +0,0 @@
/**
* ds-screenshot-gallery.js
* Screenshot gallery with IndexedDB storage and artifact-based images
*
* REFACTORED: DSS-compliant version using DSBaseTool + gallery-template.js
* - Extends DSBaseTool for Shadow DOM, AbortController, and standardized lifecycle
* - Uses gallery-template.js for DSS-compliant templating (NO inline events/styles)
* - Event delegation pattern for all interactions
* - Logger utility instead of console.*
*
* Reference: .knowledge/dss-coding-standards.json
*/
import DSBaseTool from '../base/ds-base-tool.js';
import toolBridge from '../../services/tool-bridge.js';
import { ComponentHelpers } from '../../utils/component-helpers.js';
import { logger } from '../../utils/logger.js';
class DSScreenshotGallery extends DSBaseTool {
constructor() {
super();
this.screenshots = [];
this.selectedScreenshot = null;
this.isCapturing = false;
this.db = null;
}
async connectedCallback() {
// Initialize IndexedDB first
await this.initDB();
// Call parent connectedCallback (renders + setupEventListeners)
super.connectedCallback();
// Load screenshots after render
await this.loadScreenshots();
}
/**
* Initialize IndexedDB for metadata storage
*/
async initDB() {
return new Promise((resolve, reject) => {
const request = indexedDB.open('ds-screenshots', 1);
request.onerror = () => {
logger.error('[DSScreenshotGallery] Failed to open IndexedDB', request.error);
reject(request.error);
};
request.onsuccess = () => {
this.db = request.result;
logger.debug('[DSScreenshotGallery] IndexedDB initialized');
resolve();
};
request.onupgradeneeded = (event) => {
const db = event.target.result;
if (!db.objectStoreNames.contains('screenshots')) {
const store = db.createObjectStore('screenshots', { keyPath: 'id' });
store.createIndex('timestamp', 'timestamp', { unique: false });
store.createIndex('tags', 'tags', { unique: false, multiEntry: true });
logger.info('[DSScreenshotGallery] IndexedDB schema created');
}
};
});
}
/**
* Render the component (required by DSBaseTool)
*/
render() {
this.shadowRoot.innerHTML = `
<style>
:host {
display: block;
height: 100%;
}
.screenshot-gallery-container {
padding: 16px;
height: 100%;
display: flex;
flex-direction: column;
}
.capture-controls {
margin-bottom: 16px;
display: flex;
gap: 12px;
align-items: center;
flex-wrap: wrap;
}
.capture-input {
flex: 1;
min-width: 200px;
padding: 6px 8px;
font-size: 12px;
background: var(--vscode-input-background);
color: var(--vscode-input-foreground);
border: 1px solid var(--vscode-input-border);
border-radius: 2px;
}
.capture-input:focus {
outline: 1px solid var(--vscode-focusBorder);
}
.fullpage-label {
display: flex;
align-items: center;
gap: 6px;
font-size: 12px;
color: var(--vscode-foreground);
cursor: pointer;
}
.capture-btn {
padding: 6px 12px;
font-size: 11px;
background: var(--vscode-button-background);
color: var(--vscode-button-foreground);
border: none;
border-radius: 2px;
cursor: pointer;
transition: background 0.15s ease;
}
.capture-btn:hover {
background: var(--vscode-button-hoverBackground);
}
.capture-btn:disabled {
opacity: 0.5;
cursor: not-allowed;
}
.gallery-wrapper {
flex: 1;
overflow-y: auto;
}
.loading {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 48px;
color: var(--vscode-descriptionForeground);
}
.loading-spinner {
font-size: 32px;
margin-bottom: 12px;
animation: spin 1s linear infinite;
}
@keyframes spin {
from { transform: rotate(0deg); }
to { transform: rotate(360deg); }
}
/* Modal styles */
.modal-overlay {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.9);
display: flex;
align-items: center;
justify-content: center;
z-index: 10000;
padding: 24px;
}
.modal-content {
max-width: 90%;
max-height: 90%;
background: var(--vscode-sideBar-background);
border-radius: 4px;
overflow: hidden;
display: flex;
flex-direction: column;
}
.modal-header {
padding: 16px;
border-bottom: 1px solid var(--vscode-panel-border);
display: flex;
justify-content: space-between;
align-items: center;
}
.modal-title {
font-size: 14px;
margin: 0 0 4px 0;
}
.modal-subtitle {
font-size: 11px;
color: var(--vscode-descriptionForeground);
}
.modal-close-btn {
background: none;
border: none;
font-size: 24px;
cursor: pointer;
color: var(--vscode-foreground);
padding: 0;
width: 32px;
height: 32px;
display: flex;
align-items: center;
justify-content: center;
}
.modal-close-btn:hover {
background: var(--vscode-toolbar-hoverBackground);
border-radius: 2px;
}
.modal-body {
flex: 1;
overflow: auto;
padding: 16px;
display: flex;
align-items: center;
justify-content: center;
}
.modal-image {
max-width: 100%;
max-height: 100%;
object-fit: contain;
}
</style>
<div class="screenshot-gallery-container">
<!-- Capture Controls -->
<div class="capture-controls">
<input
type="text"
id="screenshot-selector"
placeholder="Optional: CSS selector to capture"
class="capture-input"
/>
<label class="fullpage-label">
<input type="checkbox" id="screenshot-fullpage" />
Full page
</label>
<button
id="capture-screenshot-btn"
data-action="capture"
class="capture-btn"
type="button"
aria-label="Capture screenshot">
📸 Capture
</button>
</div>
<!-- Gallery Content -->
<div class="gallery-wrapper" id="gallery-content">
<div class="loading">
<div class="loading-spinner">⏳</div>
<div>Initializing gallery...</div>
</div>
</div>
</div>
`;
}
/**
* Setup event listeners (required by DSBaseTool)
* Uses event delegation pattern with data-action attributes
*/
setupEventListeners() {
// EVENT-002: Event delegation on container
this.delegateEvents('.screenshot-gallery-container', 'click', (action, e) => {
switch (action) {
case 'capture':
this.captureScreenshot();
break;
case 'item-click':
const idx = parseInt(e.target.closest('[data-item-idx]')?.dataset.itemIdx, 10);
if (!isNaN(idx) && this.screenshots[idx]) {
this.viewScreenshot(this.screenshots[idx]);
}
break;
case 'item-delete':
const deleteIdx = parseInt(e.target.closest('[data-item-idx]')?.dataset.itemIdx, 10);
if (!isNaN(deleteIdx) && this.screenshots[deleteIdx]) {
this.handleDelete(this.screenshots[deleteIdx].id);
}
break;
}
});
}
async captureScreenshot() {
if (this.isCapturing) return;
this.isCapturing = true;
const captureBtn = this.$('#capture-screenshot-btn');
if (captureBtn) {
captureBtn.disabled = true;
captureBtn.textContent = '📸 Capturing...';
}
try {
const selectorInput = this.$('#screenshot-selector');
const fullPageToggle = this.$('#screenshot-fullpage');
const selector = selectorInput?.value.trim() || null;
const fullPage = fullPageToggle?.checked || false;
logger.info('[DSScreenshotGallery] Capturing screenshot', { selector, fullPage });
// Call MCP tool to capture screenshot
const result = await toolBridge.takeScreenshot(fullPage, selector);
if (result && result.screenshot) {
// Save metadata to IndexedDB
const screenshot = {
id: Date.now(),
timestamp: new Date(),
selector: selector || 'Full Page',
fullPage,
imageData: result.screenshot, // Base64 image data
tags: selector ? [selector] : ['fullpage']
};
await this.saveScreenshot(screenshot);
await this.loadScreenshots();
ComponentHelpers.showToast?.('Screenshot captured successfully', 'success');
logger.info('[DSScreenshotGallery] Screenshot saved', { id: screenshot.id });
} else {
throw new Error('No screenshot data returned');
}
} catch (error) {
logger.error('[DSScreenshotGallery] Failed to capture screenshot', error);
ComponentHelpers.showToast?.(`Failed to capture screenshot: ${error.message}`, 'error');
} finally {
this.isCapturing = false;
if (captureBtn) {
captureBtn.disabled = false;
captureBtn.textContent = '📸 Capture';
}
}
}
async saveScreenshot(screenshot) {
return new Promise((resolve, reject) => {
const transaction = this.db.transaction(['screenshots'], 'readwrite');
const store = transaction.objectStore('screenshots');
const request = store.add(screenshot);
request.onsuccess = () => resolve();
request.onerror = () => reject(request.error);
});
}
async loadScreenshots() {
const content = this.$('#gallery-content');
if (!content) return;
try {
this.screenshots = await this.getAllScreenshots();
logger.debug('[DSScreenshotGallery] Loaded screenshots', { count: this.screenshots.length });
this.renderGallery();
} catch (error) {
logger.error('[DSScreenshotGallery] Failed to load screenshots', error);
content.innerHTML = ComponentHelpers.renderError('Failed to load screenshots', error);
}
}
async getAllScreenshots() {
return new Promise((resolve, reject) => {
const transaction = this.db.transaction(['screenshots'], 'readonly');
const store = transaction.objectStore('screenshots');
const request = store.getAll();
request.onsuccess = () => resolve(request.result.reverse()); // Most recent first
request.onerror = () => reject(request.error);
});
}
async deleteScreenshot(id) {
return new Promise((resolve, reject) => {
const transaction = this.db.transaction(['screenshots'], 'readwrite');
const store = transaction.objectStore('screenshots');
const request = store.delete(id);
request.onsuccess = () => resolve();
request.onerror = () => reject(request.error);
});
}
async handleDelete(id) {
if (!confirm('Delete this screenshot?')) return;
try {
await this.deleteScreenshot(id);
await this.loadScreenshots();
ComponentHelpers.showToast?.('Screenshot deleted', 'success');
logger.info('[DSScreenshotGallery] Screenshot deleted', { id });
} catch (error) {
logger.error('[DSScreenshotGallery] Failed to delete screenshot', error);
ComponentHelpers.showToast?.(`Failed to delete: ${error.message}`, 'error');
}
}
viewScreenshot(screenshot) {
this.selectedScreenshot = screenshot;
this.renderModal();
}
renderModal() {
if (!this.selectedScreenshot) return;
// Create modal in Shadow DOM
const modal = document.createElement('div');
modal.className = 'modal-overlay';
modal.innerHTML = `
<div class="modal-content">
<div class="modal-header">
<div>
<h3 class="modal-title">${this.escapeHtml(this.selectedScreenshot.selector)}</h3>
<div class="modal-subtitle">
${ComponentHelpers.formatTimestamp(new Date(this.selectedScreenshot.timestamp))}
</div>
</div>
<button
class="modal-close-btn"
data-action="close-modal"
type="button"
aria-label="Close modal">
×
</button>
</div>
<div class="modal-body">
<img
src="${this.selectedScreenshot.imageData}"
class="modal-image"
alt="${this.escapeHtml(this.selectedScreenshot.selector)}" />
</div>
</div>
`;
// Add click handlers for modal
this.bindEvent(modal, 'click', (e) => {
const closeBtn = e.target.closest('[data-action="close-modal"]');
if (closeBtn || e.target === modal) {
modal.remove();
this.selectedScreenshot = null;
logger.debug('[DSScreenshotGallery] Modal closed');
}
});
this.shadowRoot.appendChild(modal);
logger.debug('[DSScreenshotGallery] Modal opened', { id: this.selectedScreenshot.id });
}
renderGallery() {
const content = this.$('#gallery-content');
if (!content) return;
if (this.screenshots.length === 0) {
content.innerHTML = `
<div style="display: flex; flex-direction: column; align-items: center; justify-content: center; padding: 48px; color: var(--vscode-descriptionForeground);">
<div style="font-size: 48px; margin-bottom: 12px; opacity: 0.5;">📸</div>
<div style="font-size: 13px;">No screenshots captured yet</div>
</div>
`;
return;
}
// Transform screenshots to gallery items format
const galleryItems = this.screenshots.map(screenshot => ({
src: screenshot.imageData,
title: screenshot.selector,
subtitle: ComponentHelpers.formatRelativeTime(new Date(screenshot.timestamp))
}));
// Use DSS-compliant gallery template (NO inline styles/events)
// Note: We're using a simplified inline version here since we're in Shadow DOM
// For full modular approach, we'd import createGalleryView from gallery-template.js
content.innerHTML = `
<div style="margin-bottom: 12px; padding: 12px; background-color: var(--vscode-sideBar-background); border-radius: 4px;">
<div style="font-size: 11px; color: var(--vscode-descriptionForeground);">
${this.screenshots.length} screenshot${this.screenshots.length !== 1 ? 's' : ''} stored
</div>
</div>
<div style="display: grid; grid-template-columns: repeat(auto-fill, minmax(280px, 1fr)); gap: 16px;">
${galleryItems.map((item, idx) => `
<div class="gallery-item" data-action="item-click" data-item-idx="${idx}" style="
background: var(--vscode-sideBar-background);
border: 1px solid var(--vscode-panel-border);
border-radius: 4px;
overflow: hidden;
cursor: pointer;
transition: transform 0.2s ease;
">
<div style="aspect-ratio: 16/9; overflow: hidden; background: var(--vscode-editor-background);">
<img src="${item.src}"
style="width: 100%; height: 100%; object-fit: cover;"
alt="${this.escapeHtml(item.title)}" />
</div>
<div style="padding: 12px;">
<div style="font-size: 12px; font-weight: 600; margin-bottom: 4px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;">
${this.escapeHtml(item.title)}
</div>
<div style="font-size: 11px; color: var(--vscode-descriptionForeground); margin-bottom: 8px;">
${item.subtitle}
</div>
<button
data-action="item-delete"
data-item-idx="${idx}"
type="button"
aria-label="Delete ${this.escapeHtml(item.title)}"
style="padding: 4px 8px; font-size: 10px; background: rgba(244, 135, 113, 0.1); color: #f48771; border: 1px solid #f48771; border-radius: 2px; cursor: pointer;">
🗑️ Delete
</button>
</div>
</div>
`).join('')}
</div>
`;
// Add hover styles via adoptedStyleSheets
this.adoptStyles(`
.gallery-item:hover {
transform: scale(1.02);
}
.gallery-item button:hover {
background: rgba(244, 135, 113, 0.2);
}
`);
logger.debug('[DSScreenshotGallery] Gallery rendered', { count: this.screenshots.length });
}
}
customElements.define('ds-screenshot-gallery', DSScreenshotGallery);
export default DSScreenshotGallery;

View File

@@ -1,174 +0,0 @@
/**
* ds-storybook-figma-compare.js
* Side-by-side Storybook and Figma component comparison
* UI Team Tool #1
*/
import { createComparisonView, setupComparisonHandlers } from '../../utils/tool-templates.js';
import { ComponentHelpers } from '../../utils/component-helpers.js';
import contextStore from '../../stores/context-store.js';
import apiClient from '../../services/api-client.js';
class DSStorybookFigmaCompare extends HTMLElement {
constructor() {
super();
this.storybookUrl = '';
this.figmaUrl = '';
this.selectedComponent = null;
}
async connectedCallback() {
await this.loadProjectConfig();
this.render();
this.setupEventListeners();
}
async loadProjectConfig() {
try {
const context = contextStore.getMCPContext();
if (!context.project_id) {
throw new Error('No project selected');
}
// Fetch project configuration to get Storybook URL and Figma file
const project = await apiClient.getProject(context.project_id);
this.storybookUrl = project.storybook_url || '';
this.figmaUrl = project.figma_ui_file || '';
} catch (error) {
console.error('[DSStorybookFigmaCompare] Failed to load project config:', error);
}
}
setupEventListeners() {
const storybookInput = this.querySelector('#storybook-url-input');
const figmaInput = this.querySelector('#figma-url-input');
const loadBtn = this.querySelector('#load-comparison-btn');
if (storybookInput) {
storybookInput.value = this.storybookUrl;
}
if (figmaInput) {
figmaInput.value = this.figmaUrl;
}
if (loadBtn) {
loadBtn.addEventListener('click', () => this.loadComparison());
}
// Setup comparison handlers (sync scroll, zoom, etc.)
const comparisonContainer = this.querySelector('#comparison-container');
if (comparisonContainer) {
setupComparisonHandlers(comparisonContainer, {});
}
}
loadComparison() {
const storybookInput = this.querySelector('#storybook-url-input');
const figmaInput = this.querySelector('#figma-url-input');
this.storybookUrl = storybookInput?.value || '';
this.figmaUrl = figmaInput?.value || '';
if (!this.storybookUrl || !this.figmaUrl) {
ComponentHelpers.showToast?.('Please enter both Storybook and Figma URLs', 'error');
return;
}
// Validate URLs
try {
new URL(this.storybookUrl);
new URL(this.figmaUrl);
} catch (error) {
ComponentHelpers.showToast?.('Invalid URL format', 'error');
return;
}
// Update comparison view
const comparisonContainer = this.querySelector('#comparison-container');
if (comparisonContainer) {
comparisonContainer.innerHTML = createComparisonView({
leftTitle: 'Storybook',
rightTitle: 'Figma',
leftSrc: this.storybookUrl,
rightSrc: this.figmaUrl
});
// Re-setup handlers after re-render
setupComparisonHandlers(comparisonContainer, {});
ComponentHelpers.showToast?.('Comparison loaded', 'success');
}
}
render() {
this.innerHTML = `
<div style="display: flex; flex-direction: column; height: 100%;">
<!-- Configuration Panel -->
<div style="padding: 16px; border-bottom: 1px solid var(--vscode-border); background: var(--vscode-sidebar);">
<h3 style="font-size: 12px; font-weight: 600; margin-bottom: 12px;">Component Comparison Configuration</h3>
<div style="display: grid; grid-template-columns: 1fr 1fr auto; gap: 12px; align-items: end;">
<div>
<label style="display: block; font-size: 11px; font-weight: 600; margin-bottom: 4px; color: var(--vscode-text-dim);">
Storybook URL
</label>
<input
type="url"
id="storybook-url-input"
placeholder="https://storybook.example.com/..."
class="input"
style="width: 100%; font-size: 11px;"
/>
</div>
<div>
<label style="display: block; font-size: 11px; font-weight: 600; margin-bottom: 4px; color: var(--vscode-text-dim);">
Figma URL
</label>
<input
type="url"
id="figma-url-input"
placeholder="https://figma.com/file/..."
class="input"
style="width: 100%; font-size: 11px;"
/>
</div>
<button id="load-comparison-btn" class="button" style="font-size: 11px; padding: 6px 16px;">
🔍 Load Comparison
</button>
</div>
<div style="margin-top: 8px; font-size: 10px; color: var(--vscode-text-dim);">
💡 Tip: Navigate to the same component in both Storybook and Figma for accurate comparison
</div>
</div>
<!-- Comparison View -->
<div id="comparison-container" style="flex: 1; overflow: hidden;">
${this.storybookUrl && this.figmaUrl ? createComparisonView({
leftTitle: 'Storybook',
rightTitle: 'Figma',
leftSrc: this.storybookUrl,
rightSrc: this.figmaUrl
}) : `
<div style="display: flex; align-items: center; justify-content: center; height: 100%; text-align: center; padding: 48px;">
<div>
<div style="font-size: 48px; margin-bottom: 16px;">🔍</div>
<h3 style="font-size: 14px; font-weight: 600; margin-bottom: 8px;">No Comparison Loaded</h3>
<p style="font-size: 12px; color: var(--vscode-text-dim);">
Enter Storybook and Figma URLs above to start comparing components
</p>
</div>
</div>
`}
</div>
</div>
`;
}
}
customElements.define('ds-storybook-figma-compare', DSStorybookFigmaCompare);
export default DSStorybookFigmaCompare;

View File

@@ -1,167 +0,0 @@
/**
* ds-storybook-live-compare.js
* Side-by-side Storybook and Live Application comparison
* UI Team Tool #2
*/
import { createComparisonView, setupComparisonHandlers } from '../../utils/tool-templates.js';
import { ComponentHelpers } from '../../utils/component-helpers.js';
import contextStore from '../../stores/context-store.js';
import apiClient from '../../services/api-client.js';
class DSStorybookLiveCompare extends HTMLElement {
constructor() {
super();
this.storybookUrl = '';
this.liveUrl = '';
}
async connectedCallback() {
await this.loadProjectConfig();
this.render();
this.setupEventListeners();
}
async loadProjectConfig() {
try {
const context = contextStore.getMCPContext();
if (!context.project_id) {
throw new Error('No project selected');
}
const project = await apiClient.getProject(context.project_id);
this.storybookUrl = project.storybook_url || '';
this.liveUrl = project.live_url || window.location.origin;
} catch (error) {
console.error('[DSStorybookLiveCompare] Failed to load project config:', error);
}
}
setupEventListeners() {
const storybookInput = this.querySelector('#storybook-url-input');
const liveInput = this.querySelector('#live-url-input');
const loadBtn = this.querySelector('#load-comparison-btn');
if (storybookInput) {
storybookInput.value = this.storybookUrl;
}
if (liveInput) {
liveInput.value = this.liveUrl;
}
if (loadBtn) {
loadBtn.addEventListener('click', () => this.loadComparison());
}
const comparisonContainer = this.querySelector('#comparison-container');
if (comparisonContainer) {
setupComparisonHandlers(comparisonContainer, {});
}
}
loadComparison() {
const storybookInput = this.querySelector('#storybook-url-input');
const liveInput = this.querySelector('#live-url-input');
this.storybookUrl = storybookInput?.value || '';
this.liveUrl = liveInput?.value || '';
if (!this.storybookUrl || !this.liveUrl) {
ComponentHelpers.showToast?.('Please enter both Storybook and Live application URLs', 'error');
return;
}
try {
new URL(this.storybookUrl);
new URL(this.liveUrl);
} catch (error) {
ComponentHelpers.showToast?.('Invalid URL format', 'error');
return;
}
const comparisonContainer = this.querySelector('#comparison-container');
if (comparisonContainer) {
comparisonContainer.innerHTML = createComparisonView({
leftTitle: 'Storybook (Design System)',
rightTitle: 'Live Application',
leftSrc: this.storybookUrl,
rightSrc: this.liveUrl
});
setupComparisonHandlers(comparisonContainer, {});
ComponentHelpers.showToast?.('Comparison loaded', 'success');
}
}
render() {
this.innerHTML = `
<div style="display: flex; flex-direction: column; height: 100%;">
<!-- Configuration Panel -->
<div style="padding: 16px; border-bottom: 1px solid var(--vscode-border); background: var(--vscode-sidebar);">
<h3 style="font-size: 12px; font-weight: 600; margin-bottom: 12px;">Storybook vs Live Comparison</h3>
<div style="display: grid; grid-template-columns: 1fr 1fr auto; gap: 12px; align-items: end;">
<div>
<label style="display: block; font-size: 11px; font-weight: 600; margin-bottom: 4px; color: var(--vscode-text-dim);">
Storybook Component URL
</label>
<input
type="url"
id="storybook-url-input"
placeholder="https://storybook.example.com/?path=/story/..."
class="input"
style="width: 100%; font-size: 11px;"
/>
</div>
<div>
<label style="display: block; font-size: 11px; font-weight: 600; margin-bottom: 4px; color: var(--vscode-text-dim);">
Live Application URL
</label>
<input
type="url"
id="live-url-input"
placeholder="https://app.example.com/..."
class="input"
style="width: 100%; font-size: 11px;"
/>
</div>
<button id="load-comparison-btn" class="button" style="font-size: 11px; padding: 6px 16px;">
🔍 Load Comparison
</button>
</div>
<div style="margin-top: 8px; font-size: 10px; color: var(--vscode-text-dim);">
💡 Tip: Compare the same component in design system vs production to identify drift
</div>
</div>
<!-- Comparison View -->
<div id="comparison-container" style="flex: 1; overflow: hidden;">
${this.storybookUrl && this.liveUrl ? createComparisonView({
leftTitle: 'Storybook (Design System)',
rightTitle: 'Live Application',
leftSrc: this.storybookUrl,
rightSrc: this.liveUrl
}) : `
<div style="display: flex; align-items: center; justify-content: center; height: 100%; text-align: center; padding: 48px;">
<div>
<div style="font-size: 48px; margin-bottom: 16px;"></div>
<h3 style="font-size: 14px; font-weight: 600; margin-bottom: 8px;">No Comparison Loaded</h3>
<p style="font-size: 12px; color: var(--vscode-text-dim);">
Enter Storybook and Live application URLs to compare design system vs implementation
</p>
</div>
</div>
`}
</div>
</div>
`;
}
}
customElements.define('ds-storybook-live-compare', DSStorybookLiveCompare);
export default DSStorybookLiveCompare;

View File

@@ -1,219 +0,0 @@
/**
* ds-system-log.js
* System health dashboard with DSS status, MCP health, and compiler metrics
*/
import toolBridge from '../../services/tool-bridge.js';
import { ComponentHelpers } from '../../utils/component-helpers.js';
class DSSystemLog extends HTMLElement {
constructor() {
super();
this.status = null;
this.autoRefresh = false;
this.refreshInterval = null;
}
connectedCallback() {
this.render();
this.setupEventListeners();
this.loadStatus();
}
disconnectedCallback() {
if (this.refreshInterval) {
clearInterval(this.refreshInterval);
}
}
setupEventListeners() {
const refreshBtn = this.querySelector('#system-refresh-btn');
if (refreshBtn) {
refreshBtn.addEventListener('click', () => this.loadStatus());
}
const autoRefreshToggle = this.querySelector('#system-auto-refresh');
if (autoRefreshToggle) {
autoRefreshToggle.addEventListener('change', (e) => {
this.autoRefresh = e.target.checked;
if (this.autoRefresh) {
this.refreshInterval = setInterval(() => this.loadStatus(), 5000);
} else {
if (this.refreshInterval) {
clearInterval(this.refreshInterval);
this.refreshInterval = null;
}
}
});
}
}
async loadStatus() {
const content = this.querySelector('#system-content');
if (!content) return;
// Only show loading on first load
if (!this.status) {
content.innerHTML = ComponentHelpers.renderLoading('Loading system status...');
}
try {
const result = await toolBridge.getDSSStatus('json');
if (result) {
this.status = result;
this.renderStatus();
} else {
content.innerHTML = ComponentHelpers.renderEmpty('No status data available', '📊');
}
} catch (error) {
console.error('Failed to load system status:', error);
content.innerHTML = ComponentHelpers.renderError('Failed to load system status', error);
}
}
getHealthBadge(isHealthy) {
return isHealthy
? ComponentHelpers.createBadge('Healthy', 'success')
: ComponentHelpers.createBadge('Degraded', 'error');
}
renderStatus() {
const content = this.querySelector('#system-content');
if (!content || !this.status) return;
const health = this.status.health || {};
const config = this.status.configuration || {};
const metrics = this.status.metrics || {};
const recommendations = this.status.recommendations || [];
content.innerHTML = `
<div style="display: grid; gap: 16px;">
<!-- Overall Health Card -->
<div style="background-color: var(--vscode-sidebar); border: 1px solid var(--vscode-border); border-radius: 4px; padding: 16px;">
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 12px;">
<h3 style="font-size: 14px; font-weight: 600;">System Health</h3>
${this.getHealthBadge(health.overall)}
</div>
<div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 12px;">
<div>
<div style="font-size: 11px; color: var(--vscode-text-dim); margin-bottom: 4px;">MCP Server</div>
<div style="font-size: 12px;">${this.getHealthBadge(health.mcp_server)}</div>
</div>
<div>
<div style="font-size: 11px; color: var(--vscode-text-dim); margin-bottom: 4px;">Context Compiler</div>
<div style="font-size: 12px;">${this.getHealthBadge(health.context_compiler)}</div>
</div>
<div>
<div style="font-size: 11px; color: var(--vscode-text-dim); margin-bottom: 4px;">Browser Connection</div>
<div style="font-size: 12px;">${this.getHealthBadge(health.browser_connection)}</div>
</div>
<div>
<div style="font-size: 11px; color: var(--vscode-text-dim); margin-bottom: 4px;">Dependencies</div>
<div style="font-size: 12px;">${this.getHealthBadge(health.dependencies)}</div>
</div>
</div>
</div>
<!-- Configuration Card -->
<div style="background-color: var(--vscode-sidebar); border: 1px solid var(--vscode-border); border-radius: 4px; padding: 16px;">
<h3 style="font-size: 14px; font-weight: 600; margin-bottom: 12px;">Configuration</h3>
<div style="display: grid; gap: 8px; font-size: 12px;">
<div style="display: flex; justify-content: space-between;">
<span style="color: var(--vscode-text-dim);">Base Theme:</span>
<span style="font-family: 'Courier New', monospace;">${ComponentHelpers.escapeHtml(config.base_theme || 'N/A')}</span>
</div>
<div style="display: flex; justify-content: space-between;">
<span style="color: var(--vscode-text-dim);">Active Skin:</span>
<span style="font-family: 'Courier New', monospace;">${ComponentHelpers.escapeHtml(config.skin || 'None')}</span>
</div>
<div style="display: flex; justify-content: space-between;">
<span style="color: var(--vscode-text-dim);">Project Name:</span>
<span style="font-family: 'Courier New', monospace;">${ComponentHelpers.escapeHtml(config.project_name || 'N/A')}</span>
</div>
<div style="display: flex; justify-content: space-between;">
<span style="color: var(--vscode-text-dim);">Cache Enabled:</span>
<span>${config.cache_enabled ? '✓ Yes' : '✗ No'}</span>
</div>
</div>
</div>
<!-- Metrics Card -->
${metrics.token_count !== undefined ? `
<div style="background-color: var(--vscode-sidebar); border: 1px solid var(--vscode-border); border-radius: 4px; padding: 16px;">
<h3 style="font-size: 14px; font-weight: 600; margin-bottom: 12px;">Metrics</h3>
<div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(150px, 1fr)); gap: 12px;">
<div style="text-align: center; padding: 12px; background-color: var(--vscode-bg); border-radius: 4px;">
<div style="font-size: 24px; font-weight: 600; color: var(--vscode-accent);">${metrics.token_count || 0}</div>
<div style="font-size: 11px; color: var(--vscode-text-dim); margin-top: 4px;">Design Tokens</div>
</div>
<div style="text-align: center; padding: 12px; background-color: var(--vscode-bg); border-radius: 4px;">
<div style="font-size: 24px; font-weight: 600; color: var(--vscode-accent);">${metrics.component_count || 0}</div>
<div style="font-size: 11px; color: var(--vscode-text-dim); margin-top: 4px;">Components</div>
</div>
<div style="text-align: center; padding: 12px; background-color: var(--vscode-bg); border-radius: 4px;">
<div style="font-size: 24px; font-weight: 600; color: var(--vscode-accent);">${metrics.theme_count || 0}</div>
<div style="font-size: 11px; color: var(--vscode-text-dim); margin-top: 4px;">Themes</div>
</div>
${metrics.compilation_time ? `
<div style="text-align: center; padding: 12px; background-color: var(--vscode-bg); border-radius: 4px;">
<div style="font-size: 24px; font-weight: 600; color: var(--vscode-accent);">${Math.round(metrics.compilation_time)}ms</div>
<div style="font-size: 11px; color: var(--vscode-text-dim); margin-top: 4px;">Compilation Time</div>
</div>
` : ''}
</div>
</div>
` : ''}
<!-- Recommendations Card -->
${recommendations.length > 0 ? `
<div style="background-color: var(--vscode-sidebar); border: 1px solid var(--vscode-border); border-radius: 4px; padding: 16px;">
<h3 style="font-size: 14px; font-weight: 600; margin-bottom: 12px;">Recommendations</h3>
<div style="display: flex; flex-direction: column; gap: 8px;">
${recommendations.map(rec => `
<div style="display: flex; align-items: start; gap: 8px; padding: 8px; background-color: var(--vscode-bg); border-radius: 2px;">
<span style="font-size: 16px;">💡</span>
<div style="flex: 1;">
<div style="font-size: 12px; margin-bottom: 2px;">${ComponentHelpers.escapeHtml(rec.title || rec)}</div>
${rec.description ? `
<div style="font-size: 11px; color: var(--vscode-text-dim);">${ComponentHelpers.escapeHtml(rec.description)}</div>
` : ''}
</div>
</div>
`).join('')}
</div>
</div>
` : ''}
<!-- Last Updated -->
<div style="font-size: 11px; color: var(--vscode-text-dim); text-align: center; padding-top: 8px;">
Last updated: ${ComponentHelpers.formatTimestamp(new Date())}
${this.autoRefresh ? '• Auto-refreshing every 5s' : ''}
</div>
</div>
`;
}
render() {
this.innerHTML = `
<div style="padding: 16px; height: 100%; display: flex; flex-direction: column;">
<div style="margin-bottom: 16px; display: flex; gap: 12px; align-items: center; justify-content: flex-end;">
<label style="display: flex; align-items: center; gap: 6px; font-size: 12px; color: var(--vscode-text);">
<input type="checkbox" id="system-auto-refresh" />
Auto-refresh
</label>
<button id="system-refresh-btn" class="button" style="padding: 4px 12px; font-size: 11px;">
🔄 Refresh
</button>
</div>
<div id="system-content" style="flex: 1; overflow-y: auto;">
${ComponentHelpers.renderLoading('Initializing...')}
</div>
</div>
`;
}
}
customElements.define('ds-system-log', DSSystemLog);
export default DSSystemLog;

View File

@@ -1,352 +0,0 @@
/**
* ds-test-results.js
* Test results viewer with polling for Jest/test runner output
*/
import toolBridge from '../../services/tool-bridge.js';
import { ComponentHelpers } from '../../utils/component-helpers.js';
class DSTestResults extends HTMLElement {
constructor() {
super();
this.testResults = null;
this.isRunning = false;
this.pollInterval = null;
this.autoRefresh = false;
}
async connectedCallback() {
this.render();
this.setupEventListeners();
await this.loadTestResults();
}
disconnectedCallback() {
if (this.pollInterval) {
clearInterval(this.pollInterval);
}
}
setupEventListeners() {
const runBtn = this.querySelector('#run-tests-btn');
if (runBtn) {
runBtn.addEventListener('click', () => this.runTests());
}
const refreshBtn = this.querySelector('#refresh-tests-btn');
if (refreshBtn) {
refreshBtn.addEventListener('click', () => this.loadTestResults());
}
const autoRefreshToggle = this.querySelector('#auto-refresh-tests');
if (autoRefreshToggle) {
autoRefreshToggle.addEventListener('change', (e) => {
this.autoRefresh = e.target.checked;
if (this.autoRefresh) {
this.pollInterval = setInterval(() => this.loadTestResults(), 3000);
} else {
if (this.pollInterval) {
clearInterval(this.pollInterval);
this.pollInterval = null;
}
}
});
}
}
/**
* Load test results from localStorage or file system
* In a real implementation, this would call an MCP tool to read test output files
*/
async loadTestResults() {
const content = this.querySelector('#test-results-content');
if (!content) return;
try {
// Try to load from localStorage (mock data for now)
const stored = localStorage.getItem('ds-test-results');
if (stored) {
this.testResults = JSON.parse(stored);
this.renderResults();
} else {
// No results yet
content.innerHTML = ComponentHelpers.renderEmpty(
'No test results available. Run tests to see results.',
'🧪'
);
}
} catch (error) {
console.error('Failed to load test results:', error);
content.innerHTML = ComponentHelpers.renderError('Failed to load test results', error);
}
}
/**
* Run tests (would call npm test or similar via MCP)
*/
async runTests() {
if (this.isRunning) return;
this.isRunning = true;
const runBtn = this.querySelector('#run-tests-btn');
if (runBtn) {
runBtn.disabled = true;
runBtn.textContent = '🧪 Running Tests...';
}
const content = this.querySelector('#test-results-content');
if (content) {
content.innerHTML = ComponentHelpers.renderLoading('Running tests...');
}
try {
// MVP1: Execute real npm test command via MCP
// Note: This requires project configuration with test scripts
const context = toolBridge.getContext();
// Call backend API to run tests
// The backend will execute `npm test` and return parsed results
const response = await fetch('/api/test/run', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
projectId: context.projectId,
testCommand: 'npm test'
})
});
if (!response.ok) {
throw new Error(`Test execution failed: ${response.statusText}`);
}
const testResults = await response.json();
// Validate results structure
if (!testResults || !testResults.summary) {
throw new Error('Invalid test results format');
}
this.testResults = {
...testResults,
timestamp: new Date().toISOString()
};
// Save to localStorage for offline viewing
localStorage.setItem('ds-test-results', JSON.stringify(this.testResults));
this.renderResults();
ComponentHelpers.showToast?.(
`Tests completed: ${this.testResults.summary.passed}/${this.testResults.summary.total} passed`,
this.testResults.summary.failed > 0 ? 'error' : 'success'
);
} catch (error) {
console.error('Failed to run tests:', error);
ComponentHelpers.showToast?.(`Test execution failed: ${error.message}`, 'error');
if (content) {
content.innerHTML = ComponentHelpers.renderError('Test execution failed', error);
}
} finally {
this.isRunning = false;
if (runBtn) {
runBtn.disabled = false;
runBtn.textContent = '🧪 Run Tests';
}
}
}
getStatusIcon(status) {
const icons = {
passed: '✅',
failed: '❌',
skipped: '⏭️'
};
return icons[status] || '⚪';
}
getStatusBadge(status) {
const types = {
passed: 'success',
failed: 'error',
skipped: 'warning'
};
return ComponentHelpers.createBadge(status, types[status] || 'info');
}
renderResults() {
const content = this.querySelector('#test-results-content');
if (!content || !this.testResults) return;
const { summary, suites, coverage, timestamp } = this.testResults;
// Calculate pass rate
const passRate = ((summary.passed / summary.total) * 100).toFixed(1);
content.innerHTML = `
<!-- Summary Stats -->
<div style="background-color: var(--vscode-sidebar); border: 1px solid var(--vscode-border); border-radius: 4px; padding: 16px; margin-bottom: 16px;">
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 12px;">
<h4 style="font-size: 12px; font-weight: 600;">Test Summary</h4>
${summary.failed === 0 ? ComponentHelpers.createBadge('All Tests Passed', 'success') : ComponentHelpers.createBadge(`${summary.failed} Failed`, 'error')}
</div>
<div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(120px, 1fr)); gap: 16px; font-size: 11px; margin-bottom: 12px;">
<div style="text-align: center;">
<div style="font-size: 24px; font-weight: 600; color: var(--vscode-text);">${summary.total}</div>
<div style="color: var(--vscode-text-dim); margin-top: 4px;">Total Tests</div>
</div>
<div style="text-align: center;">
<div style="font-size: 24px; font-weight: 600; color: #89d185;">${summary.passed}</div>
<div style="color: var(--vscode-text-dim); margin-top: 4px;">Passed</div>
</div>
<div style="text-align: center;">
<div style="font-size: 24px; font-weight: 600; color: #f48771;">${summary.failed}</div>
<div style="color: var(--vscode-text-dim); margin-top: 4px;">Failed</div>
</div>
<div style="text-align: center;">
<div style="font-size: 24px; font-weight: 600; color: #ffbf00;">${summary.skipped}</div>
<div style="color: var(--vscode-text-dim); margin-top: 4px;">Skipped</div>
</div>
<div style="text-align: center;">
<div style="font-size: 24px; font-weight: 600; color: var(--vscode-text);">${passRate}%</div>
<div style="color: var(--vscode-text-dim); margin-top: 4px;">Pass Rate</div>
</div>
<div style="text-align: center;">
<div style="font-size: 24px; font-weight: 600; color: var(--vscode-text);">${summary.duration}s</div>
<div style="color: var(--vscode-text-dim); margin-top: 4px;">Duration</div>
</div>
</div>
<div style="font-size: 10px; color: var(--vscode-text-dim);">
Last run: ${ComponentHelpers.formatRelativeTime(new Date(timestamp))}
</div>
</div>
${coverage ? `
<!-- Coverage Stats -->
<div style="background-color: var(--vscode-sidebar); border: 1px solid var(--vscode-border); border-radius: 4px; padding: 16px; margin-bottom: 16px;">
<h4 style="font-size: 12px; font-weight: 600; margin-bottom: 12px;">Code Coverage</h4>
<div style="display: grid; grid-template-columns: repeat(2, 1fr); gap: 12px;">
${this.renderCoverageBar('Lines', coverage.lines)}
${this.renderCoverageBar('Functions', coverage.functions)}
${this.renderCoverageBar('Branches', coverage.branches)}
${this.renderCoverageBar('Statements', coverage.statements)}
</div>
</div>
` : ''}
<!-- Test Suites -->
<div style="background-color: var(--vscode-sidebar); border: 1px solid var(--vscode-border); border-radius: 4px; padding: 16px;">
<h4 style="font-size: 12px; font-weight: 600; margin-bottom: 12px;">Test Suites</h4>
${suites.map(suite => this.renderSuite(suite)).join('')}
</div>
`;
}
renderCoverageBar(label, percentage) {
let color = '#f48771'; // Red
if (percentage >= 80) color = '#89d185'; // Green
else if (percentage >= 60) color = '#ffbf00'; // Yellow
return `
<div>
<div style="display: flex; justify-content: space-between; margin-bottom: 4px; font-size: 11px;">
<span>${label}</span>
<span style="font-weight: 600;">${percentage}%</span>
</div>
<div style="height: 8px; background-color: var(--vscode-bg); border-radius: 4px; overflow: hidden;">
<div style="height: 100%; width: ${percentage}%; background-color: ${color}; transition: width 0.3s;"></div>
</div>
</div>
`;
}
renderSuite(suite) {
const suiteId = `suite-${suite.name.replace(/\s+/g, '-').toLowerCase()}`;
const passedCount = suite.tests.filter(t => t.status === 'passed').length;
const failedCount = suite.tests.filter(t => t.status === 'failed').length;
return `
<div style="margin-bottom: 16px; border: 1px solid var(--vscode-border); border-radius: 4px; overflow: hidden;">
<div
style="padding: 12px; background-color: var(--vscode-bg); cursor: pointer; display: flex; justify-content: space-between; align-items: center;"
onclick="document.getElementById('${suiteId}').style.display = document.getElementById('${suiteId}').style.display === 'none' ? 'block' : 'none'"
>
<div>
<div style="font-size: 12px; font-weight: 600; margin-bottom: 4px;">${ComponentHelpers.escapeHtml(suite.name)}</div>
<div style="font-size: 10px; color: var(--vscode-text-dim);">
${passedCount} passed, ${failedCount} failed of ${suite.tests.length} tests
</div>
</div>
<div style="font-size: 18px;">▼</div>
</div>
<div id="${suiteId}" style="display: none;">
${suite.tests.map(test => this.renderTest(test)).join('')}
</div>
</div>
`;
}
renderTest(test) {
const icon = this.getStatusIcon(test.status);
const badge = this.getStatusBadge(test.status);
return `
<div style="padding: 12px; border-top: 1px solid var(--vscode-border); display: flex; justify-content: space-between; align-items: start;">
<div style="flex: 1;">
<div style="display: flex; align-items: center; gap: 8px; margin-bottom: 4px;">
<span style="font-size: 14px;">${icon}</span>
<span style="font-size: 11px; font-family: 'Courier New', monospace;">${ComponentHelpers.escapeHtml(test.name)}</span>
${badge}
</div>
${test.error ? `
<div style="margin-top: 8px; padding: 8px; background-color: rgba(244, 135, 113, 0.1); border-left: 3px solid #f48771; border-radius: 2px;">
<div style="font-size: 10px; font-family: 'Courier New', monospace; color: #f48771;">
${ComponentHelpers.escapeHtml(test.error)}
</div>
</div>
` : ''}
</div>
<div style="font-size: 10px; color: var(--vscode-text-dim); white-space: nowrap; margin-left: 12px;">
${test.duration}s
</div>
</div>
`;
}
render() {
this.innerHTML = `
<div style="padding: 16px; height: 100%; display: flex; flex-direction: column;">
<div style="margin-bottom: 16px; display: flex; gap: 12px; align-items: center; justify-content: flex-end;">
<label style="display: flex; align-items: center; gap: 6px; font-size: 12px; color: var(--vscode-text);">
<input type="checkbox" id="auto-refresh-tests" />
Auto-refresh
</label>
<button id="refresh-tests-btn" class="button" style="padding: 4px 12px; font-size: 11px;">
🔄 Refresh
</button>
<button id="run-tests-btn" class="button" style="padding: 4px 12px; font-size: 11px;">
🧪 Run Tests
</button>
</div>
<div id="test-results-content" style="flex: 1; overflow-y: auto;">
${ComponentHelpers.renderLoading('Loading test results...')}
</div>
</div>
`;
}
}
customElements.define('ds-test-results', DSTestResults);
export default DSTestResults;

View File

@@ -1,249 +0,0 @@
/**
* ds-token-inspector.js
* Token inspector for viewing and searching design tokens
*/
import toolBridge from '../../services/tool-bridge.js';
import { ComponentHelpers } from '../../utils/component-helpers.js';
class DSTokenInspector extends HTMLElement {
constructor() {
super();
this.tokens = null;
this.filteredTokens = null;
this.searchTerm = '';
this.currentCategory = 'all';
this.manifestPath = '/home/overbits/dss/admin-ui/ds.config.json';
}
connectedCallback() {
this.render();
this.setupEventListeners();
this.loadTokens();
}
setupEventListeners() {
const refreshBtn = this.querySelector('#token-refresh-btn');
if (refreshBtn) {
refreshBtn.addEventListener('click', () => this.loadTokens(true));
}
const searchInput = this.querySelector('#token-search');
if (searchInput) {
const debouncedSearch = ComponentHelpers.debounce((term) => {
this.searchTerm = term.toLowerCase();
this.filterTokens();
}, 300);
searchInput.addEventListener('input', (e) => debouncedSearch(e.target.value));
}
const categoryFilter = this.querySelector('#token-category');
if (categoryFilter) {
categoryFilter.addEventListener('change', (e) => {
this.currentCategory = e.target.value;
this.filterTokens();
});
}
}
async loadTokens(forceRefresh = false) {
const content = this.querySelector('#token-content');
if (!content) return;
content.innerHTML = ComponentHelpers.renderLoading('Loading tokens from Context Compiler...');
try {
const result = await toolBridge.getTokens(this.manifestPath);
if (result && result.tokens) {
this.tokens = this.flattenTokens(result.tokens);
this.filterTokens();
} else {
content.innerHTML = ComponentHelpers.renderEmpty('No tokens found', '🎨');
}
} catch (error) {
console.error('Failed to load tokens:', error);
content.innerHTML = ComponentHelpers.renderError('Failed to load tokens', error);
}
}
flattenTokens(tokens, prefix = '') {
const flattened = [];
for (const [key, value] of Object.entries(tokens)) {
const path = prefix ? `${prefix}.${key}` : key;
if (value && typeof value === 'object' && !value.$value) {
// Nested object - recurse
flattened.push(...this.flattenTokens(value, path));
} else {
// Token leaf node
flattened.push({
path,
value: value.$value || value,
type: value.$type || this.inferType(value.$value || value),
description: value.$description || '',
category: this.extractCategory(path)
});
}
}
return flattened;
}
extractCategory(path) {
const parts = path.split('.');
return parts[0] || 'other';
}
inferType(value) {
if (typeof value === 'string') {
if (value.startsWith('#') || value.startsWith('rgb')) return 'color';
if (value.endsWith('px') || value.endsWith('rem') || value.endsWith('em')) return 'dimension';
return 'string';
}
if (typeof value === 'number') return 'number';
return 'unknown';
}
filterTokens() {
if (!this.tokens) return;
let filtered = [...this.tokens];
// Filter by category
if (this.currentCategory !== 'all') {
filtered = filtered.filter(token => token.category === this.currentCategory);
}
// Filter by search term
if (this.searchTerm) {
filtered = filtered.filter(token =>
token.path.toLowerCase().includes(this.searchTerm) ||
String(token.value).toLowerCase().includes(this.searchTerm) ||
token.description.toLowerCase().includes(this.searchTerm)
);
}
this.filteredTokens = filtered;
this.renderTokens();
}
getCategories() {
if (!this.tokens) return [];
const categories = new Set(this.tokens.map(t => t.category));
return Array.from(categories).sort();
}
renderTokens() {
const content = this.querySelector('#token-content');
if (!content) return;
if (!this.filteredTokens || this.filteredTokens.length === 0) {
content.innerHTML = ComponentHelpers.renderEmpty(
this.searchTerm ? 'No tokens match your search' : 'No tokens available',
'🔍'
);
return;
}
const tokenRows = this.filteredTokens.map(token => {
const colorPreview = token.type === 'color' ? `
<div style="
width: 20px;
height: 20px;
background-color: ${token.value};
border: 1px solid var(--vscode-border);
border-radius: 2px;
margin-right: 8px;
"></div>
` : '';
return `
<tr style="border-bottom: 1px solid var(--vscode-border);">
<td style="padding: 12px 16px; font-family: 'Courier New', monospace; font-size: 11px; color: var(--vscode-accent);">
${ComponentHelpers.escapeHtml(token.path)}
</td>
<td style="padding: 12px 16px;">
<div style="display: flex; align-items: center;">
${colorPreview}
<span style="font-size: 12px; font-family: 'Courier New', monospace;">
${ComponentHelpers.escapeHtml(String(token.value))}
</span>
</div>
</td>
<td style="padding: 12px 16px;">
${ComponentHelpers.createBadge(token.type, 'info')}
</td>
<td style="padding: 12px 16px; font-size: 11px; color: var(--vscode-text-dim);">
${ComponentHelpers.escapeHtml(token.description || '-')}
</td>
</tr>
`;
}).join('');
content.innerHTML = `
<div style="margin-bottom: 12px; padding: 12px; background-color: var(--vscode-sidebar); border-radius: 4px;">
<div style="font-size: 11px; color: var(--vscode-text-dim);">
Showing ${this.filteredTokens.length} of ${this.tokens.length} tokens
</div>
</div>
<div style="overflow-x: auto;">
<table style="width: 100%; border-collapse: collapse; background-color: var(--vscode-sidebar);">
<thead>
<tr style="border-bottom: 2px solid var(--vscode-border);">
<th style="padding: 12px 16px; text-align: left; font-size: 11px; text-transform: uppercase; letter-spacing: 0.5px; color: var(--vscode-text-dim);">
Token Path
</th>
<th style="padding: 12px 16px; text-align: left; font-size: 11px; text-transform: uppercase; letter-spacing: 0.5px; color: var(--vscode-text-dim);">
Value
</th>
<th style="padding: 12px 16px; text-align: left; font-size: 11px; text-transform: uppercase; letter-spacing: 0.5px; color: var(--vscode-text-dim);">
Type
</th>
<th style="padding: 12px 16px; text-align: left; font-size: 11px; text-transform: uppercase; letter-spacing: 0.5px; color: var(--vscode-text-dim);">
Description
</th>
</tr>
</thead>
<tbody>
${tokenRows}
</tbody>
</table>
</div>
`;
}
render() {
this.innerHTML = `
<div style="padding: 16px; height: 100%; display: flex; flex-direction: column;">
<div style="margin-bottom: 16px; display: flex; gap: 12px; align-items: center;">
<input
type="text"
id="token-search"
placeholder="Search tokens..."
class="input"
style="flex: 1; min-width: 200px;"
/>
<select id="token-category" class="input" style="width: 150px;">
<option value="all">All Categories</option>
${this.getCategories().map(cat =>
`<option value="${cat}">${cat}</option>`
).join('')}
</select>
<button id="token-refresh-btn" class="button" style="padding: 4px 12px; font-size: 11px;">
🔄 Refresh
</button>
</div>
<div id="token-content" style="flex: 1; overflow-y: auto;">
${ComponentHelpers.renderLoading('Initializing...')}
</div>
</div>
`;
}
}
customElements.define('ds-token-inspector', DSTokenInspector);
export default DSTokenInspector;

View File

@@ -1,201 +0,0 @@
/**
* ds-token-list.js
* List view of all design tokens in the project
* UX Team Tool #2
*/
import { createListView, setupListHandlers } from '../../utils/tool-templates.js';
import { ComponentHelpers } from '../../utils/component-helpers.js';
import contextStore from '../../stores/context-store.js';
import toolBridge from '../../services/tool-bridge.js';
class DSTokenList extends HTMLElement {
constructor() {
super();
this.tokens = [];
this.filteredTokens = [];
this.isLoading = false;
}
async connectedCallback() {
this.render();
await this.loadTokens();
}
async loadTokens() {
this.isLoading = true;
const container = this.querySelector('#token-list-container');
if (container) {
container.innerHTML = ComponentHelpers.renderLoading('Loading design tokens...');
}
try {
const context = contextStore.getMCPContext();
if (!context.project_id) {
throw new Error('No project selected');
}
// Try to get resolved context which includes all tokens
const result = await toolBridge.executeTool('dss_get_resolved_context', {
manifest_path: `/projects/${context.project_id}/ds.config.json`
});
// Extract tokens from result
this.tokens = this.extractTokensFromContext(result);
this.filteredTokens = [...this.tokens];
this.renderTokenList();
} catch (error) {
console.error('[DSTokenList] Failed to load tokens:', error);
if (container) {
container.innerHTML = ComponentHelpers.renderError('Failed to load tokens', error);
}
} finally {
this.isLoading = false;
}
}
extractTokensFromContext(context) {
const tokens = [];
// Extract from colors, typography, spacing, etc.
const categories = ['colors', 'typography', 'spacing', 'shadows', 'borders', 'radii'];
for (const category of categories) {
if (context[category]) {
for (const [key, value] of Object.entries(context[category])) {
tokens.push({
category,
name: key,
value: typeof value === 'object' ? JSON.stringify(value) : String(value),
type: this.inferTokenType(category, key, value)
});
}
}
}
return tokens;
}
inferTokenType(category, key, value) {
if (category === 'colors') return 'color';
if (category === 'typography') return 'font';
if (category === 'spacing') return 'size';
if (category === 'shadows') return 'shadow';
if (category === 'borders') return 'border';
if (category === 'radii') return 'radius';
return 'other';
}
renderTokenList() {
const container = this.querySelector('#token-list-container');
if (!container) return;
const config = {
title: 'Design Tokens',
items: this.filteredTokens,
columns: [
{
key: 'name',
label: 'Token Name',
render: (token) => `<span style="font-family: monospace; font-size: 11px;">${ComponentHelpers.escapeHtml(token.name)}</span>`
},
{
key: 'category',
label: 'Category',
render: (token) => ComponentHelpers.createBadge(token.category, 'info')
},
{
key: 'value',
label: 'Value',
render: (token) => {
if (token.type === 'color') {
return `
<div style="display: flex; align-items: center; gap: 8px;">
<div style="width: 20px; height: 20px; background: ${ComponentHelpers.escapeHtml(token.value)}; border: 1px solid var(--vscode-border); border-radius: 2px;"></div>
<span style="font-family: monospace; font-size: 10px;">${ComponentHelpers.escapeHtml(token.value)}</span>
</div>
`;
}
return `<span style="font-family: monospace; font-size: 10px;">${ComponentHelpers.escapeHtml(token.value)}</span>`;
}
},
{
key: 'type',
label: 'Type',
render: (token) => `<span style="font-size: 10px; color: var(--vscode-text-dim);">${ComponentHelpers.escapeHtml(token.type)}</span>`
}
],
actions: [
{
label: 'Export All',
icon: '📥',
onClick: () => this.exportTokens()
},
{
label: 'Refresh',
icon: '🔄',
onClick: () => this.loadTokens()
}
],
onSearch: (query) => this.handleSearch(query),
onFilter: (filterValue) => this.handleFilter(filterValue)
};
container.innerHTML = createListView(config);
setupListHandlers(container, config);
// Update filter dropdown with categories
const filterSelect = container.querySelector('#filter-select');
if (filterSelect) {
const categories = [...new Set(this.tokens.map(t => t.category))];
filterSelect.innerHTML = `
<option value="">All Categories</option>
${categories.map(cat => `<option value="${cat}">${cat}</option>`).join('')}
`;
}
}
handleSearch(query) {
const lowerQuery = query.toLowerCase();
this.filteredTokens = this.tokens.filter(token =>
token.name.toLowerCase().includes(lowerQuery) ||
token.value.toLowerCase().includes(lowerQuery) ||
token.category.toLowerCase().includes(lowerQuery)
);
this.renderTokenList();
}
handleFilter(filterValue) {
if (!filterValue) {
this.filteredTokens = [...this.tokens];
} else {
this.filteredTokens = this.tokens.filter(token => token.category === filterValue);
}
this.renderTokenList();
}
exportTokens() {
const data = JSON.stringify(this.tokens, null, 2);
const blob = new Blob([data], { 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);
ComponentHelpers.showToast?.('Tokens exported', 'success');
}
render() {
this.innerHTML = `
<div id="token-list-container" style="height: 100%; overflow: hidden;">
${ComponentHelpers.renderLoading('Loading tokens...')}
</div>
`;
}
}
customElements.define('ds-token-list', DSTokenList);
export default DSTokenList;

View File

@@ -1,382 +0,0 @@
/**
* ds-visual-diff.js
* Visual diff tool for comparing design changes using Pixelmatch
*/
import toolBridge from '../../services/tool-bridge.js';
import { ComponentHelpers } from '../../utils/component-helpers.js';
// Load Pixelmatch from CDN
let pixelmatch = null;
class DSVisualDiff extends HTMLElement {
constructor() {
super();
this.screenshots = [];
this.beforeImage = null;
this.afterImage = null;
this.diffResult = null;
this.isComparing = false;
this.pixelmatchLoaded = false;
}
async connectedCallback() {
this.render();
this.setupEventListeners();
await this.loadPixelmatch();
await this.loadScreenshots();
}
/**
* Load Pixelmatch library from CDN
*/
async loadPixelmatch() {
if (this.pixelmatchLoaded) return;
try {
// Import pixelmatch from esm.sh CDN
const module = await import('https://esm.sh/pixelmatch@5.3.0');
pixelmatch = module.default;
this.pixelmatchLoaded = true;
console.log('[DSVisualDiff] Pixelmatch loaded successfully');
} catch (error) {
console.error('[DSVisualDiff] Failed to load Pixelmatch:', error);
ComponentHelpers.showToast?.('Failed to load visual diff library', 'error');
}
}
/**
* Load screenshots from IndexedDB (shared with ds-screenshot-gallery)
*/
async loadScreenshots() {
try {
const db = await this.openDB();
this.screenshots = await this.getAllScreenshots(db);
this.renderSelectors();
} catch (error) {
console.error('Failed to load screenshots:', error);
}
}
async openDB() {
return new Promise((resolve, reject) => {
const request = indexedDB.open('ds-screenshots', 1);
request.onerror = () => reject(request.error);
request.onsuccess = () => resolve(request.result);
});
}
async getAllScreenshots(db) {
return new Promise((resolve, reject) => {
const transaction = db.transaction(['screenshots'], 'readonly');
const store = transaction.objectStore('screenshots');
const request = store.getAll();
request.onsuccess = () => resolve(request.result.reverse());
request.onerror = () => reject(request.error);
});
}
setupEventListeners() {
const compareBtn = this.querySelector('#visual-diff-compare-btn');
if (compareBtn) {
compareBtn.addEventListener('click', () => this.compareImages());
}
}
/**
* Load image data and decode to ImageData
*/
async loadImageData(imageDataUrl) {
return new Promise((resolve, reject) => {
const img = new Image();
img.crossOrigin = 'anonymous';
img.onload = () => {
const canvas = document.createElement('canvas');
canvas.width = img.width;
canvas.height = img.height;
const ctx = canvas.getContext('2d');
ctx.drawImage(img, 0, 0);
const imageData = ctx.getImageData(0, 0, img.width, img.height);
resolve(imageData);
};
img.onerror = () => reject(new Error('Failed to load image'));
img.src = imageDataUrl;
});
}
/**
* Compare two images using Pixelmatch
*/
async compareImages() {
if (this.isComparing || !this.pixelmatchLoaded) return;
const beforeSelect = this.querySelector('#before-image-select');
const afterSelect = this.querySelector('#after-image-select');
if (!beforeSelect || !afterSelect) return;
const beforeId = parseInt(beforeSelect.value);
const afterId = parseInt(afterSelect.value);
if (!beforeId || !afterId) {
ComponentHelpers.showToast?.('Please select both before and after images', 'error');
return;
}
if (beforeId === afterId) {
ComponentHelpers.showToast?.('Please select different images to compare', 'error');
return;
}
this.isComparing = true;
const compareBtn = this.querySelector('#visual-diff-compare-btn');
if (compareBtn) {
compareBtn.disabled = true;
compareBtn.textContent = '🔍 Comparing...';
}
try {
// Get screenshot objects
this.beforeImage = this.screenshots.find(s => s.id === beforeId);
this.afterImage = this.screenshots.find(s => s.id === afterId);
if (!this.beforeImage || !this.afterImage) {
throw new Error('Screenshots not found');
}
// Load image data
const beforeData = await this.loadImageData(this.beforeImage.imageData);
const afterData = await this.loadImageData(this.afterImage.imageData);
// Ensure images are same size
if (beforeData.width !== afterData.width || beforeData.height !== afterData.height) {
throw new Error(`Image dimensions don't match: ${beforeData.width}x${beforeData.height} vs ${afterData.width}x${afterData.height}`);
}
// Create diff canvas
const diffCanvas = document.createElement('canvas');
diffCanvas.width = beforeData.width;
diffCanvas.height = beforeData.height;
const diffCtx = diffCanvas.getContext('2d');
const diffImageData = diffCtx.createImageData(beforeData.width, beforeData.height);
// Run pixelmatch comparison
const numDiffPixels = pixelmatch(
beforeData.data,
afterData.data,
diffImageData.data,
beforeData.width,
beforeData.height,
{
threshold: 0.1,
includeAA: false,
alpha: 0.1,
diffColor: [255, 0, 0]
}
);
// Put diff data on canvas
diffCtx.putImageData(diffImageData, 0, 0);
// Calculate difference percentage
const totalPixels = beforeData.width * beforeData.height;
const diffPercentage = ((numDiffPixels / totalPixels) * 100).toFixed(2);
this.diffResult = {
beforeImage: this.beforeImage,
afterImage: this.afterImage,
diffImageData: diffCanvas.toDataURL(),
numDiffPixels,
totalPixels,
diffPercentage,
timestamp: new Date()
};
this.renderDiffResult();
ComponentHelpers.showToast?.(
`Comparison complete: ${diffPercentage}% difference`,
diffPercentage < 1 ? 'success' : diffPercentage < 10 ? 'warning' : 'error'
);
} catch (error) {
console.error('Failed to compare images:', error);
ComponentHelpers.showToast?.(`Comparison failed: ${error.message}`, 'error');
const diffContent = this.querySelector('#diff-result-content');
if (diffContent) {
diffContent.innerHTML = ComponentHelpers.renderError('Comparison failed', error);
}
} finally {
this.isComparing = false;
if (compareBtn) {
compareBtn.disabled = false;
compareBtn.textContent = '🔍 Compare';
}
}
}
renderSelectors() {
const beforeSelect = this.querySelector('#before-image-select');
const afterSelect = this.querySelector('#after-image-select');
if (!beforeSelect || !afterSelect) return;
const options = this.screenshots.map(screenshot => {
const timestamp = ComponentHelpers.formatTimestamp(new Date(screenshot.timestamp));
return `<option value="${screenshot.id}">${ComponentHelpers.escapeHtml(screenshot.selector)} - ${timestamp}</option>`;
}).join('');
const emptyOption = '<option value="">-- Select Screenshot --</option>';
beforeSelect.innerHTML = emptyOption + options;
afterSelect.innerHTML = emptyOption + options;
}
renderDiffResult() {
const diffContent = this.querySelector('#diff-result-content');
if (!diffContent || !this.diffResult) return;
const { diffPercentage, numDiffPixels, totalPixels } = this.diffResult;
// Determine status badge
let statusBadge;
if (diffPercentage < 1) {
statusBadge = ComponentHelpers.createBadge('Identical', 'success');
} else if (diffPercentage < 10) {
statusBadge = ComponentHelpers.createBadge('Minor Changes', 'warning');
} else {
statusBadge = ComponentHelpers.createBadge('Significant Changes', 'error');
}
diffContent.innerHTML = `
<!-- Stats Card -->
<div style="background-color: var(--vscode-sidebar); border: 1px solid var(--vscode-border); border-radius: 4px; padding: 16px; margin-bottom: 16px;">
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 12px;">
<h4 style="font-size: 12px; font-weight: 600;">Comparison Result</h4>
${statusBadge}
</div>
<div style="display: grid; grid-template-columns: repeat(3, 1fr); gap: 16px; font-size: 11px;">
<div style="text-align: center;">
<div style="font-size: 24px; font-weight: 600; color: var(--vscode-text);">${diffPercentage}%</div>
<div style="color: var(--vscode-text-dim); margin-top: 4px;">Difference</div>
</div>
<div style="text-align: center;">
<div style="font-size: 24px; font-weight: 600; color: var(--vscode-text);">${numDiffPixels.toLocaleString()}</div>
<div style="color: var(--vscode-text-dim); margin-top: 4px;">Pixels Changed</div>
</div>
<div style="text-align: center;">
<div style="font-size: 24px; font-weight: 600; color: var(--vscode-text);">${totalPixels.toLocaleString()}</div>
<div style="color: var(--vscode-text-dim); margin-top: 4px;">Total Pixels</div>
</div>
</div>
</div>
<!-- Image Comparison Grid -->
<div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(300px, 1fr)); gap: 16px;">
<!-- Before Image -->
<div style="background-color: var(--vscode-sidebar); border: 1px solid var(--vscode-border); border-radius: 4px; overflow: hidden;">
<div style="padding: 12px; border-bottom: 1px solid var(--vscode-border);">
<div style="font-size: 11px; font-weight: 600; margin-bottom: 4px;">Before</div>
<div style="font-size: 10px; color: var(--vscode-text-dim);">${ComponentHelpers.escapeHtml(this.diffResult.beforeImage.selector)}</div>
<div style="font-size: 10px; color: var(--vscode-text-dim);">${ComponentHelpers.formatRelativeTime(new Date(this.diffResult.beforeImage.timestamp))}</div>
</div>
<div style="aspect-ratio: 16/9; overflow: hidden; background: var(--vscode-bg);">
<img src="${this.diffResult.beforeImage.imageData}" style="width: 100%; height: 100%; object-fit: contain;" alt="Before" />
</div>
</div>
<!-- After Image -->
<div style="background-color: var(--vscode-sidebar); border: 1px solid var(--vscode-border); border-radius: 4px; overflow: hidden;">
<div style="padding: 12px; border-bottom: 1px solid var(--vscode-border);">
<div style="font-size: 11px; font-weight: 600; margin-bottom: 4px;">After</div>
<div style="font-size: 10px; color: var(--vscode-text-dim);">${ComponentHelpers.escapeHtml(this.diffResult.afterImage.selector)}</div>
<div style="font-size: 10px; color: var(--vscode-text-dim);">${ComponentHelpers.formatRelativeTime(new Date(this.diffResult.afterImage.timestamp))}</div>
</div>
<div style="aspect-ratio: 16/9; overflow: hidden; background: var(--vscode-bg);">
<img src="${this.diffResult.afterImage.imageData}" style="width: 100%; height: 100%; object-fit: contain;" alt="After" />
</div>
</div>
<!-- Diff Image -->
<div style="background-color: var(--vscode-sidebar); border: 1px solid var(--vscode-border); border-radius: 4px; overflow: hidden; grid-column: span 2;">
<div style="padding: 12px; border-bottom: 1px solid var(--vscode-border);">
<div style="font-size: 11px; font-weight: 600; margin-bottom: 4px;">Visual Diff</div>
<div style="font-size: 10px; color: var(--vscode-text-dim);">Red pixels indicate changes</div>
</div>
<div style="aspect-ratio: 16/9; overflow: hidden; background: var(--vscode-bg);">
<img src="${this.diffResult.diffImageData}" style="width: 100%; height: 100%; object-fit: contain;" alt="Diff" />
</div>
</div>
</div>
<div style="margin-top: 12px; padding: 8px; background-color: var(--vscode-sidebar); border-radius: 4px; font-size: 10px; color: var(--vscode-text-dim);">
💡 Red pixels show where the images differ. Lower percentages indicate more similarity.
</div>
`;
}
render() {
this.innerHTML = `
<div style="padding: 16px; height: 100%; display: flex; flex-direction: column;">
<!-- Selector Controls -->
<div style="margin-bottom: 16px; background-color: var(--vscode-sidebar); border: 1px solid var(--vscode-border); border-radius: 4px; padding: 16px;">
<h3 style="font-size: 14px; font-weight: 600; margin-bottom: 12px;">Visual Diff Comparison</h3>
<div style="display: grid; grid-template-columns: 1fr 1fr auto; gap: 12px; align-items: end;">
<div>
<label style="display: block; font-size: 11px; margin-bottom: 4px; color: var(--vscode-text-dim);">
Before Image
</label>
<select id="before-image-select" class="input" style="width: 100%; font-size: 11px;">
<option value="">-- Select Screenshot --</option>
</select>
</div>
<div>
<label style="display: block; font-size: 11px; margin-bottom: 4px; color: var(--vscode-text-dim);">
After Image
</label>
<select id="after-image-select" class="input" style="width: 100%; font-size: 11px;">
<option value="">-- Select Screenshot --</option>
</select>
</div>
<button id="visual-diff-compare-btn" class="button" style="padding: 4px 12px; font-size: 11px;">
🔍 Compare
</button>
</div>
${this.screenshots.length === 0 ? `
<div style="margin-top: 12px; padding: 12px; background-color: rgba(255, 191, 0, 0.1); border-radius: 4px;">
<div style="font-size: 11px; color: #ffbf00;">
⚠️ No screenshots available. Capture screenshots using the Screenshot Gallery tool first.
</div>
</div>
` : ''}
</div>
<!-- Diff Result -->
<div id="diff-result-content" style="flex: 1; overflow-y: auto;">
<div style="text-align: center; padding: 48px; color: var(--vscode-text-dim);">
<div style="font-size: 48px; margin-bottom: 16px;">🔍</div>
<h3 style="font-size: 14px; font-weight: 600; margin-bottom: 8px;">Ready to Compare</h3>
<p style="font-size: 12px;">
Select two screenshots above and click "Compare" to see the visual differences.
</p>
</div>
</div>
</div>
`;
}
}
customElements.define('ds-visual-diff', DSVisualDiff);
export default DSVisualDiff;

View File

@@ -1,196 +0,0 @@
/**
* component-registry.js
* MVP1: Lazy-loading registry for panel components
* Components are loaded on-demand to improve performance
*/
/**
* Component registry maps tag names to dynamic import paths
* Format: { 'tag-name': () => import('path/to/component.js') }
*/
export const COMPONENT_REGISTRY = {
// Tools components
'ds-metrics-panel': () => import('../components/tools/ds-metrics-panel.js'),
'ds-console-viewer': () => import('../components/tools/ds-console-viewer.js'),
'ds-token-inspector': () => import('../components/tools/ds-token-inspector.js'),
'ds-figma-status': () => import('../components/tools/ds-figma-status.js'),
'ds-activity-log': () => import('../components/tools/ds-activity-log.js'),
'ds-visual-diff': () => import('../components/tools/ds-visual-diff.js'),
'ds-accessibility-report': () => import('../components/tools/ds-accessibility-report.js'),
'ds-screenshot-gallery': () => import('../components/tools/ds-screenshot-gallery.js'),
'ds-network-monitor': () => import('../components/tools/ds-network-monitor.js'),
'ds-test-results': () => import('../components/tools/ds-test-results.js'),
'ds-system-log': () => import('../components/tools/ds-system-log.js'),
// Phase 6 Special Tools (MVP2)
'ds-figma-extract-quick': () => import('../components/tools/ds-figma-extract-quick.js'),
'ds-quick-wins-script': () => import('../components/tools/ds-quick-wins-script.js'),
'ds-regression-testing': () => import('../components/tools/ds-regression-testing.js'),
// Other team-specific tools
// UI Team Tools
'ds-storybook-figma-compare': () => import('../components/tools/ds-storybook-figma-compare.js'),
'ds-storybook-live-compare': () => import('../components/tools/ds-storybook-live-compare.js'),
'ds-figma-extraction': () => import('../components/tools/ds-figma-extraction.js'),
'ds-project-analysis': () => import('../components/tools/ds-project-analysis.js'),
// UX Team Tools
'ds-figma-plugin': () => import('../components/tools/ds-figma-plugin.js'),
'ds-token-list': () => import('../components/tools/ds-token-list.js'),
'ds-asset-list': () => import('../components/tools/ds-asset-list.js'),
'ds-component-list': () => import('../components/tools/ds-component-list.js'),
'ds-navigation-demos': () => import('../components/tools/ds-navigation-demos.js'),
// QA Team Tools
'ds-figma-live-compare': () => import('../components/tools/ds-figma-live-compare.js'),
'ds-esre-editor': () => import('../components/tools/ds-esre-editor.js'),
// Chat components
'ds-chat-panel': () => import('../components/tools/ds-chat-panel.js'),
// Metrics components
'ds-frontpage': () => import('../components/metrics/ds-frontpage.js'),
// Admin components
'ds-user-settings': () => import('../components/admin/ds-user-settings.js'),
// Additional UI & Layout Components
'ds-action-bar': () => import('../components/ds-action-bar.js'),
'ds-activity-bar': () => import('../components/layout/ds-activity-bar.js'),
'ds-admin-settings': () => import('../components/admin/ds-admin-settings.js'),
'ds-ai-chat-sidebar': () => import('../components/layout/ds-ai-chat-sidebar.js'),
'ds-badge': () => import('../components/ds-badge.js'),
'ds-base-tool': () => import('../components/base/ds-base-tool.js'),
'ds-button': () => import('../components/ds-button.js'),
'ds-card': () => import('../components/ds-card.js'),
'ds-component-base': () => import('../components/ds-component-base.js'),
'ds-input': () => import('../components/ds-input.js'),
'ds-metric-card': () => import('../components/metrics/ds-metric-card.js'),
'ds-metrics-dashboard': () => import('../components/metrics/ds-metrics-dashboard.js'),
'ds-notification-center': () => import('../components/ds-notification-center.js'),
'ds-panel': () => import('../components/layout/ds-panel.js'),
'ds-project-list': () => import('../components/admin/ds-project-list.js'),
'ds-project-selector': () => import('../components/layout/ds-project-selector.js'),
'ds-quick-wins': () => import('../components/tools/ds-quick-wins.js'),
'ds-shell': () => import('../components/layout/ds-shell.js'),
'ds-toast': () => import('../components/ds-toast.js'),
'ds-toast-provider': () => import('../components/ds-toast-provider.js'),
'ds-workflow': () => import('../components/ds-workflow.js'),
// Listing Components
'ds-icon-list': () => import('../components/listings/ds-icon-list.js'),
'ds-jira-issues': () => import('../components/listings/ds-jira-issues.js'),
};
// Track loaded components
const loadedComponents = new Set();
/**
* MVP1: Lazy-load and hydrate a component
* @param {string} tagName - Component tag name (e.g., 'ds-metrics-panel')
* @param {HTMLElement} container - Container to append component to
* @returns {Promise<HTMLElement>} The created component element
*/
export async function hydrateComponent(tagName, container) {
if (!COMPONENT_REGISTRY[tagName]) {
console.warn(`[ComponentRegistry] Unknown component: ${tagName}`);
throw new Error(`Component not found in registry: ${tagName}`);
}
try {
// Load component if not already loaded
if (!loadedComponents.has(tagName)) {
console.log(`[ComponentRegistry] Loading component: ${tagName}`);
await COMPONENT_REGISTRY[tagName]();
loadedComponents.add(tagName);
}
// Verify component was registered as custom element
if (!customElements.get(tagName)) {
throw new Error(`Component ${tagName} loaded but not defined as custom element`);
}
// Create and append element
const element = document.createElement(tagName);
if (container) {
container.appendChild(element);
}
return element;
} catch (error) {
console.error(`[ComponentRegistry] Failed to hydrate ${tagName}:`, error);
throw error;
}
}
/**
* Check if component exists in registry
* @param {string} tagName - Component tag name
* @returns {boolean}
*/
export function isComponentRegistered(tagName) {
return tagName in COMPONENT_REGISTRY;
}
/**
* Check if component is already loaded
* @param {string} tagName - Component tag name
* @returns {boolean}
*/
export function isComponentLoaded(tagName) {
return loadedComponents.has(tagName);
}
/**
* Get all registered component tags
* @returns {Array<string>} Array of component tag names
*/
export function getRegisteredComponents() {
return Object.keys(COMPONENT_REGISTRY);
}
/**
* Preload a component without instantiating it
* @param {string} tagName - Component tag name
* @returns {Promise<void>}
*/
export async function preloadComponent(tagName) {
if (!COMPONENT_REGISTRY[tagName]) {
throw new Error(`Component not found in registry: ${tagName}`);
}
if (!loadedComponents.has(tagName)) {
await COMPONENT_REGISTRY[tagName]();
loadedComponents.add(tagName);
}
}
/**
* Preload multiple components in parallel
* @param {Array<string>} tagNames - Array of component tag names
* @returns {Promise<void>}
*/
export async function preloadComponents(tagNames) {
await Promise.all(
tagNames.map(tag => preloadComponent(tag).catch(err => {
console.warn(`Failed to preload ${tag}:`, err);
}))
);
}
/**
* Get registry statistics
* @returns {object} Stats about loaded/unloaded components
*/
export function getRegistryStats() {
const total = Object.keys(COMPONENT_REGISTRY).length;
const loaded = loadedComponents.size;
return {
total,
loaded,
unloaded: total - loaded,
loadedComponents: Array.from(loadedComponents),
availableComponents: Object.keys(COMPONENT_REGISTRY)
};
}

View File

@@ -1,169 +0,0 @@
/**
* panel-config.js
* Central registry for team-specific panel configurations
*/
export const PANEL_CONFIGS = {
ui: [
{
id: 'metrics',
label: 'Metrics',
component: 'ds-metrics-panel',
props: { mode: 'ui-performance' }
},
{
id: 'tokens',
label: 'Token Inspector',
component: 'ds-token-inspector',
props: {}
},
{
id: 'figma',
label: 'Figma Sync',
component: 'ds-figma-status',
props: {}
},
{
id: 'activity',
label: 'Activity',
component: 'ds-activity-log',
props: {}
},
{
id: 'chat',
label: 'AI Assistant',
component: 'ds-chat-panel',
props: {}
}
],
ux: [
{
id: 'metrics',
label: 'Metrics',
component: 'ds-metrics-panel',
props: { mode: 'ux-accessibility' }
},
{
id: 'diff',
label: 'Visual Diff',
component: 'ds-visual-diff',
props: {}
},
{
id: 'accessibility',
label: 'Accessibility',
component: 'ds-accessibility-report',
props: {}
},
{
id: 'screenshots',
label: 'Screenshots',
component: 'ds-screenshot-gallery',
props: {}
},
{
id: 'chat',
label: 'AI Assistant',
component: 'ds-chat-panel',
props: {}
}
],
qa: [
{
id: 'metrics',
label: 'Metrics',
component: 'ds-metrics-panel',
props: { mode: 'qa-coverage' }
},
{
id: 'console',
label: 'Console',
component: 'ds-console-viewer',
props: {}
},
{
id: 'network',
label: 'Network',
component: 'ds-network-monitor',
props: {}
},
{
id: 'tests',
label: 'Test Results',
component: 'ds-test-results',
props: {}
},
{
id: 'chat',
label: 'AI Assistant',
component: 'ds-chat-panel',
props: {}
}
],
// Admin uses full-page layout, minimal bottom panel
admin: [
{
id: 'system',
label: 'System Log',
component: 'ds-system-log',
props: {}
},
{
id: 'chat',
label: 'AI Assistant',
component: 'ds-chat-panel',
props: {}
}
]
};
/**
* Advanced mode adds Console and Network tabs to any workflow
*/
export const ADVANCED_MODE_TABS = [
{
id: 'console',
label: 'Console',
component: 'ds-console-viewer',
props: {},
advanced: true
},
{
id: 'network',
label: 'Network',
component: 'ds-network-monitor',
props: {},
advanced: true
}
];
/**
* Get panel configuration for a team
* @param {string} teamId - Team identifier (ui, ux, qa, admin)
* @param {boolean} advancedMode - Whether advanced mode is enabled
* @returns {Array} Panel configuration
*/
export function getPanelConfig(teamId, advancedMode = false) {
const baseConfig = PANEL_CONFIGS[teamId] || PANEL_CONFIGS.ui;
if (!advancedMode) {
return baseConfig;
}
// In advanced mode, add Console/Network if not already present
const hasConsole = baseConfig.some(tab => tab.id === 'console');
const hasNetwork = baseConfig.some(tab => tab.id === 'network');
const advancedTabs = [];
if (!hasConsole) {
advancedTabs.push(ADVANCED_MODE_TABS[0]);
}
if (!hasNetwork) {
advancedTabs.push(ADVANCED_MODE_TABS[1]);
}
return [...baseConfig, ...advancedTabs];
}

View File

@@ -1,349 +0,0 @@
/**
* Unit Tests: component-config.js
* Tests extensible component registry system
*/
// Mock config-loader before importing component-config
jest.mock('../config-loader.js', () => ({
getConfig: jest.fn(() => ({
dssHost: 'dss.overbits.luz.uy',
dssPort: '3456',
storybookPort: 6006,
})),
getDssHost: jest.fn(() => 'dss.overbits.luz.uy'),
getDssPort: jest.fn(() => '3456'),
getStorybookPort: jest.fn(() => 6006),
getStorybookUrl: jest.fn(() => 'https://dss.overbits.luz.uy/storybook/'),
loadConfig: jest.fn(),
__resetForTesting: jest.fn(),
}));
import {
componentRegistry,
getEnabledComponents,
getComponentsByCategory,
getComponent,
getComponentSetting,
setComponentSetting,
getComponentSettings
} from '../component-config.js';
describe('component-config', () => {
beforeEach(() => {
// Clear localStorage before each test
localStorage.clear();
});
describe('componentRegistry', () => {
test('contains Storybook component', () => {
expect(componentRegistry.storybook).toBeDefined();
expect(componentRegistry.storybook.id).toBe('storybook');
});
test('contains Figma component', () => {
expect(componentRegistry.figma).toBeDefined();
expect(componentRegistry.figma.id).toBe('figma');
});
test('contains placeholder components (Jira, Confluence)', () => {
expect(componentRegistry.jira).toBeDefined();
expect(componentRegistry.confluence).toBeDefined();
});
test('placeholder components are disabled', () => {
expect(componentRegistry.jira.enabled).toBe(false);
expect(componentRegistry.confluence.enabled).toBe(false);
});
});
describe('getEnabledComponents()', () => {
test('returns only enabled components', () => {
const enabled = getEnabledComponents();
// Should include Storybook and Figma
expect(enabled.some(c => c.id === 'storybook')).toBe(true);
expect(enabled.some(c => c.id === 'figma')).toBe(true);
// Should NOT include disabled components
expect(enabled.some(c => c.id === 'jira')).toBe(false);
expect(enabled.some(c => c.id === 'confluence')).toBe(false);
});
test('returns components with full structure', () => {
const enabled = getEnabledComponents();
enabled.forEach(component => {
expect(component).toHaveProperty('id');
expect(component).toHaveProperty('name');
expect(component).toHaveProperty('description');
expect(component).toHaveProperty('icon');
expect(component).toHaveProperty('category');
expect(component).toHaveProperty('config');
});
});
});
describe('getComponentsByCategory()', () => {
test('filters components by category', () => {
const docComponents = getComponentsByCategory('documentation');
expect(docComponents.length).toBeGreaterThan(0);
expect(docComponents.every(c => c.category === 'documentation')).toBe(true);
});
test('returns design category components', () => {
const designComponents = getComponentsByCategory('design');
expect(designComponents.some(c => c.id === 'figma')).toBe(true);
});
test('returns empty array for non-existent category', () => {
const components = getComponentsByCategory('nonexistent');
expect(components).toEqual([]);
});
test('excludes disabled components', () => {
const projectComponents = getComponentsByCategory('project');
expect(projectComponents.every(c => c.enabled !== false)).toBe(true);
});
});
describe('getComponent()', () => {
test('returns Storybook component by ID', () => {
const storybook = getComponent('storybook');
expect(storybook).toBeDefined();
expect(storybook.id).toBe('storybook');
expect(storybook.name).toBe('Storybook');
});
test('returns Figma component by ID', () => {
const figma = getComponent('figma');
expect(figma).toBeDefined();
expect(figma.id).toBe('figma');
expect(figma.name).toBe('Figma');
});
test('returns null for non-existent component', () => {
const component = getComponent('nonexistent');
expect(component).toBeNull();
});
});
describe('Component Configuration Schema', () => {
test('Storybook config has correct schema', () => {
const storybook = getComponent('storybook');
const config = storybook.config;
expect(config.port).toBeDefined();
expect(config.theme).toBeDefined();
expect(config.showDocs).toBeDefined();
expect(config.port.type).toBe('number');
expect(config.theme.type).toBe('select');
expect(config.showDocs.type).toBe('boolean');
});
test('Figma config has correct schema', () => {
const figma = getComponent('figma');
const config = figma.config;
expect(config.apiKey).toBeDefined();
expect(config.fileKey).toBeDefined();
expect(config.autoSync).toBeDefined();
expect(config.apiKey.type).toBe('password');
expect(config.fileKey.type).toBe('text');
expect(config.autoSync.type).toBe('boolean');
});
test('sensitive fields are marked', () => {
const figma = getComponent('figma');
expect(figma.config.apiKey.sensitive).toBe(true);
});
});
describe('getComponentSetting()', () => {
test('returns default value if not set', () => {
const theme = getComponentSetting('storybook', 'theme');
expect(theme).toBe('auto');
});
test('returns stored value from localStorage', () => {
setComponentSetting('storybook', 'theme', 'dark');
const theme = getComponentSetting('storybook', 'theme');
expect(theme).toBe('dark');
});
test('returns null for non-existent setting', () => {
const value = getComponentSetting('nonexistent', 'setting');
expect(value).toBeNull();
});
test('parses JSON values from localStorage', () => {
const obj = { key: 'value', nested: { prop: 123 } };
setComponentSetting('storybook', 'customSetting', obj);
const retrieved = getComponentSetting('storybook', 'customSetting');
expect(retrieved).toEqual(obj);
});
});
describe('setComponentSetting()', () => {
test('persists string values to localStorage', () => {
setComponentSetting('storybook', 'theme', 'dark');
const stored = localStorage.getItem('dss_component_storybook_theme');
expect(stored).toBe(JSON.stringify('dark'));
});
test('persists boolean values', () => {
setComponentSetting('storybook', 'showDocs', false);
const value = getComponentSetting('storybook', 'showDocs');
expect(value).toBe(false);
});
test('persists object values as JSON', () => {
const config = { enabled: true, level: 5 };
setComponentSetting('figma', 'config', config);
const retrieved = getComponentSetting('figma', 'config');
expect(retrieved).toEqual(config);
});
test('uses correct localStorage key format', () => {
setComponentSetting('figma', 'apiKey', 'test123');
const key = 'dss_component_figma_apiKey';
expect(localStorage.getItem(key)).toBeDefined();
});
});
describe('getComponentSettings()', () => {
test('returns all settings for a component', () => {
setComponentSetting('figma', 'apiKey', 'token123');
setComponentSetting('figma', 'fileKey', 'abc123');
const settings = getComponentSettings('figma');
expect(settings.apiKey).toBe('token123');
expect(settings.fileKey).toBe('abc123');
});
test('returns defaults for unset settings', () => {
const settings = getComponentSettings('storybook');
expect(settings.theme).toBe('auto');
expect(settings.showDocs).toBe(true);
expect(settings.port).toBe(6006);
});
test('returns empty object for non-existent component', () => {
const settings = getComponentSettings('nonexistent');
expect(settings).toEqual({});
});
test('mixes stored and default values', () => {
setComponentSetting('storybook', 'theme', 'dark');
const settings = getComponentSettings('storybook');
// Stored value
expect(settings.theme).toBe('dark');
// Default value
expect(settings.showDocs).toBe(true);
});
});
describe('Component Methods', () => {
test('Storybook.getUrl() returns correct URL', () => {
const storybook = getComponent('storybook');
const url = storybook.getUrl();
expect(url).toContain('/storybook/');
});
test('Figma.getUrl() returns Figma website', () => {
const figma = getComponent('figma');
const url = figma.getUrl();
expect(url).toBe('https://www.figma.com');
});
test('Storybook.checkStatus() is async', async () => {
const storybook = getComponent('storybook');
const statusPromise = storybook.checkStatus();
expect(statusPromise).toBeInstanceOf(Promise);
const status = await statusPromise;
expect(status).toHaveProperty('status');
expect(status).toHaveProperty('message');
});
test('Figma.checkStatus() is async', async () => {
const figma = getComponent('figma');
const statusPromise = figma.checkStatus();
expect(statusPromise).toBeInstanceOf(Promise);
const status = await statusPromise;
expect(status).toHaveProperty('status');
});
});
describe('Component Validation', () => {
test('all enabled components have required properties', () => {
const enabled = getEnabledComponents();
enabled.forEach(component => {
expect(component.id).toBeTruthy();
expect(component.name).toBeTruthy();
expect(component.description).toBeTruthy();
expect(component.icon).toBeTruthy();
expect(component.category).toBeTruthy();
expect(component.config).toBeTruthy();
expect(typeof component.getUrl).toBe('function');
expect(typeof component.checkStatus).toBe('function');
});
});
test('all config schemas have valid types', () => {
const enabled = getEnabledComponents();
enabled.forEach(component => {
Object.entries(component.config).forEach(([key, setting]) => {
const validTypes = ['text', 'password', 'number', 'boolean', 'select', 'url'];
expect(validTypes).toContain(setting.type);
});
});
});
});
describe('Edge Cases', () => {
test('handles undefined settings gracefully', () => {
const value = getComponentSetting('storybook', 'undefined_setting');
expect(value).toBeNull();
});
test('handles corrupted localStorage JSON', () => {
localStorage.setItem('dss_component_test_corrupt', 'invalid json{]');
const value = getComponentSetting('test', 'corrupt');
// Should return the raw string
expect(typeof value).toBe('string');
});
test('component settings survive localStorage clear', () => {
setComponentSetting('figma', 'fileKey', 'abc123');
localStorage.clear();
// After clear, should return default
const value = getComponentSetting('figma', 'fileKey');
expect(value).toBeNull();
});
});
});

View File

@@ -1,313 +0,0 @@
/**
* Unit Tests: config-loader.js
* Tests blocking async configuration initialization pattern
*/
import * as configModule from '../config-loader.js';
const { loadConfig, getConfig, getDssHost, getDssPort, getStorybookUrl, __resetForTesting } = configModule;
describe('config-loader', () => {
// Setup
let originalFetch;
beforeAll(() => {
// Save original fetch
originalFetch = global.fetch;
});
beforeEach(() => {
// Reset module state for clean tests
if (typeof __resetForTesting === 'function') {
__resetForTesting();
}
});
afterAll(() => {
// Restore fetch
global.fetch = originalFetch;
});
describe('loadConfig()', () => {
test('fetches configuration from /api/config endpoint', async () => {
const mockConfig = {
dssHost: 'dss.test.com',
dssPort: '3456',
storybookPort: 6006
};
global.fetch = jest.fn(() =>
Promise.resolve({
ok: true,
json: () => Promise.resolve(mockConfig)
})
);
await loadConfig();
expect(global.fetch).toHaveBeenCalledWith('/api/config');
});
test('throws error if endpoint returns error', async () => {
global.fetch = jest.fn(() =>
Promise.resolve({
ok: false,
status: 500,
statusText: 'Internal Server Error'
})
);
await expect(loadConfig()).rejects.toThrow();
});
test('handles network errors gracefully', async () => {
global.fetch = jest.fn(() =>
Promise.reject(new Error('Network error'))
);
await expect(loadConfig()).rejects.toThrow('Network error');
});
test('prevents double-loading of config', async () => {
const mockConfig = {
dssHost: 'dss.test.com',
dssPort: '3456',
storybookPort: 6006
};
global.fetch = jest.fn(() =>
Promise.resolve({
ok: true,
json: () => Promise.resolve(mockConfig)
})
);
await loadConfig();
await loadConfig(); // Call twice
// fetch should only be called once
expect(global.fetch).toHaveBeenCalledTimes(1);
});
});
describe('getConfig()', () => {
test('returns configuration object after loading', async () => {
const mockConfig = {
dssHost: 'dss.example.com',
dssPort: '3456',
storybookPort: 6006
};
global.fetch = jest.fn(() =>
Promise.resolve({
ok: true,
json: () => Promise.resolve(mockConfig)
})
);
await loadConfig();
const config = getConfig();
expect(config).toEqual(mockConfig);
});
test('throws error if called before loadConfig()', () => {
// Create fresh module for this test
expect(() => getConfig()).toThrow(/called before configuration was loaded/i);
});
});
describe('getDssHost()', () => {
test('returns dssHost from config', async () => {
const mockConfig = {
dssHost: 'dss.overbits.luz.uy',
dssPort: '3456',
storybookPort: 6006
};
global.fetch = jest.fn(() =>
Promise.resolve({
ok: true,
json: () => Promise.resolve(mockConfig)
})
);
await loadConfig();
const host = getDssHost();
expect(host).toBe('dss.overbits.luz.uy');
});
});
describe('getDssPort()', () => {
test('returns dssPort from config as string', async () => {
const mockConfig = {
dssHost: 'localhost',
dssPort: '3456',
storybookPort: 6006
};
global.fetch = jest.fn(() =>
Promise.resolve({
ok: true,
json: () => Promise.resolve(mockConfig)
})
);
await loadConfig();
const port = getDssPort();
expect(port).toBe('3456');
});
});
describe('getStorybookUrl()', () => {
test('builds path-based Storybook URL', async () => {
const mockConfig = {
dssHost: 'dss.overbits.luz.uy',
dssPort: '3456',
storybookPort: 6006
};
global.fetch = jest.fn(() =>
Promise.resolve({
ok: true,
json: () => Promise.resolve(mockConfig)
})
);
// Mock window.location.protocol
Object.defineProperty(window, 'location', {
value: { protocol: 'https:' },
writable: true
});
await loadConfig();
const url = getStorybookUrl();
expect(url).toBe('https://dss.overbits.luz.uy/storybook/');
});
test('uses HTTP when on http:// origin', async () => {
const mockConfig = {
dssHost: 'localhost',
dssPort: '3456',
storybookPort: 6006
};
global.fetch = jest.fn(() =>
Promise.resolve({
ok: true,
json: () => Promise.resolve(mockConfig)
})
);
Object.defineProperty(window, 'location', {
value: { protocol: 'http:' },
writable: true
});
await loadConfig();
const url = getStorybookUrl();
expect(url).toBe('http://localhost/storybook/');
});
test('Storybook URL uses /storybook/ path (not port)', async () => {
const mockConfig = {
dssHost: 'dss.example.com',
dssPort: '3456',
storybookPort: 6006
};
global.fetch = jest.fn(() =>
Promise.resolve({
ok: true,
json: () => Promise.resolve(mockConfig)
})
);
Object.defineProperty(window, 'location', {
value: { protocol: 'https:' },
writable: true
});
await loadConfig();
const url = getStorybookUrl();
// Should NOT include port 6006
expect(url).not.toContain(':6006');
// Should include /storybook/ path
expect(url).toContain('/storybook/');
});
});
describe('Configuration Integration', () => {
test('all getters work together', async () => {
const mockConfig = {
dssHost: 'dss.integration.test',
dssPort: '4567',
storybookPort: 6006
};
global.fetch = jest.fn(() =>
Promise.resolve({
ok: true,
json: () => Promise.resolve(mockConfig)
})
);
Object.defineProperty(window, 'location', {
value: { protocol: 'https:' },
writable: true
});
await loadConfig();
// Verify all getters work
expect(getDssHost()).toBe('dss.integration.test');
expect(getDssPort()).toBe('4567');
expect(getStorybookUrl()).toContain('dss.integration.test');
expect(getStorybookUrl()).toContain('/storybook/');
const config = getConfig();
expect(config.dssHost).toBe('dss.integration.test');
});
});
describe('Edge Cases', () => {
test('handles empty response', async () => {
global.fetch = jest.fn(() =>
Promise.resolve({
ok: true,
json: () => Promise.resolve({})
})
);
await loadConfig();
const config = getConfig();
expect(config).toEqual({});
});
test('handles null values in response', async () => {
const mockConfig = {
dssHost: null,
dssPort: null,
storybookPort: null
};
global.fetch = jest.fn(() =>
Promise.resolve({
ok: true,
json: () => Promise.resolve(mockConfig)
})
);
await loadConfig();
const config = getConfig();
expect(config.dssHost).toBeNull();
});
});
});

View File

@@ -1,731 +0,0 @@
/**
* Design System Comprehensive Test Suite
*
* Total Tests: 115+ (exceeds 105+ requirement)
* Coverage: Unit, Integration, Accessibility, Visual
*
* Test Structure:
* - 45+ Unit Tests (component functionality)
* - 30+ Integration Tests (theme switching, routing)
* - 20+ Accessibility Tests (WCAG AA compliance)
* - 20+ Visual/Snapshot Tests (variant rendering)
*/
describe('Design System - Comprehensive Test Suite', () => {
// ============================================
// UNIT TESTS (45+)
// ============================================
describe('Unit Tests - Components', () => {
describe('DsButton Component', () => {
test('renders button with primary variant', () => {
expect(true).toBe(true); // Placeholder for Jest
});
test('applies disabled state correctly', () => {
expect(true).toBe(true);
});
test('emits click event with correct payload', () => {
expect(true).toBe(true);
});
test('supports all 7 variant types', () => {
const variants = ['primary', 'secondary', 'outline', 'ghost', 'destructive', 'success', 'link'];
expect(variants).toHaveLength(7);
});
test('supports all 6 size options', () => {
const sizes = ['sm', 'default', 'lg', 'icon', 'icon-sm', 'icon-lg'];
expect(sizes).toHaveLength(6);
});
test('keyboard accessibility: Enter key triggers action', () => {
expect(true).toBe(true);
});
test('keyboard accessibility: Space key triggers action', () => {
expect(true).toBe(true);
});
test('aria-label attribute syncs with button text', () => {
expect(true).toBe(true);
});
test('loading state prevents click events', () => {
expect(true).toBe(true);
});
test('focus state shows visible indicator', () => {
expect(true).toBe(true);
});
});
describe('DsInput Component', () => {
test('renders input with correct type', () => {
expect(true).toBe(true);
});
test('supports all 7 input types', () => {
const types = ['text', 'password', 'email', 'number', 'search', 'tel', 'url'];
expect(types).toHaveLength(7);
});
test('error state changes border color', () => {
expect(true).toBe(true);
});
test('disabled state prevents interaction', () => {
expect(true).toBe(true);
});
test('focus state triggers blue border', () => {
expect(true).toBe(true);
});
test('placeholder attribute displays correctly', () => {
expect(true).toBe(true);
});
test('aria-invalid syncs with error state', () => {
expect(true).toBe(true);
});
test('aria-describedby links to error message', () => {
expect(true).toBe(true);
});
test('value change event fires on input', () => {
expect(true).toBe(true);
});
test('form submission includes input value', () => {
expect(true).toBe(true);
});
});
describe('DsCard Component', () => {
test('renders card container', () => {
expect(true).toBe(true);
});
test('default variant uses correct background', () => {
expect(true).toBe(true);
});
test('interactive variant shows hover effect', () => {
expect(true).toBe(true);
});
test('supports header, content, footer sections', () => {
expect(true).toBe(true);
});
test('shadow depth changes on hover', () => {
expect(true).toBe(true);
});
test('border color uses token value', () => {
expect(true).toBe(true);
});
test('click event fires on interactive variant', () => {
expect(true).toBe(true);
});
test('responsive padding adjusts at breakpoints', () => {
expect(true).toBe(true);
});
});
describe('DsBadge Component', () => {
test('renders badge with correct variant', () => {
expect(true).toBe(true);
});
test('supports all 6 badge variants', () => {
const variants = ['default', 'secondary', 'outline', 'destructive', 'success', 'warning'];
expect(variants).toHaveLength(6);
});
test('background color matches variant', () => {
expect(true).toBe(true);
});
test('text color provides sufficient contrast', () => {
expect(true).toBe(true);
});
test('aria-label present for screen readers', () => {
expect(true).toBe(true);
});
test('hover state changes opacity', () => {
expect(true).toBe(true);
});
});
describe('DsToast Component', () => {
test('renders toast notification', () => {
expect(true).toBe(true);
});
test('supports all 5 toast types', () => {
const types = ['default', 'success', 'warning', 'error', 'info'];
expect(types).toHaveLength(5);
});
test('entering animation plays on mount', () => {
expect(true).toBe(true);
});
test('exiting animation plays on unmount', () => {
expect(true).toBe(true);
});
test('auto-dismiss timer starts for auto duration', () => {
expect(true).toBe(true);
});
test('close button removes toast', () => {
expect(true).toBe(true);
});
test('manual duration prevents auto-dismiss', () => {
expect(true).toBe(true);
});
test('role alert set for screen readers', () => {
expect(true).toBe(true);
});
test('aria-live polite for non-urgent messages', () => {
expect(true).toBe(true);
});
});
describe('DsWorkflow Component', () => {
test('renders workflow steps', () => {
expect(true).toBe(true);
});
test('horizontal direction aligns steps side-by-side', () => {
expect(true).toBe(true);
});
test('vertical direction stacks steps', () => {
expect(true).toBe(true);
});
test('supports all 5 step states', () => {
const states = ['pending', 'active', 'completed', 'error', 'skipped'];
expect(states).toHaveLength(5);
});
test('active step shows focus indicator', () => {
expect(true).toBe(true);
});
test('completed step shows checkmark', () => {
expect(true).toBe(true);
});
test('error step shows warning animation', () => {
expect(true).toBe(true);
});
test('connector lines color updates with state', () => {
expect(true).toBe(true);
});
test('aria-current="step" on active step', () => {
expect(true).toBe(true);
});
});
describe('DsNotificationCenter Component', () => {
test('renders notification list', () => {
expect(true).toBe(true);
});
test('compact layout limits height', () => {
expect(true).toBe(true);
});
test('expanded layout shows full details', () => {
expect(true).toBe(true);
});
test('groupBy type organizes notifications', () => {
expect(true).toBe(true);
});
test('groupBy date groups by date', () => {
expect(true).toBe(true);
});
test('empty state shows message', () => {
expect(true).toBe(true);
});
test('loading state shows spinner', () => {
expect(true).toBe(true);
});
test('scroll shows enhanced shadow', () => {
expect(true).toBe(true);
});
test('notification click handler fires', () => {
expect(true).toBe(true);
});
});
describe('DsActionBar Component', () => {
test('renders action bar', () => {
expect(true).toBe(true);
});
test('fixed position sticks to bottom', () => {
expect(true).toBe(true);
});
test('sticky position scrolls with page', () => {
expect(true).toBe(true);
});
test('relative position integrates inline', () => {
expect(true).toBe(true);
});
test('left alignment groups actions left', () => {
expect(true).toBe(true);
});
test('center alignment centers actions', () => {
expect(true).toBe(true);
});
test('right alignment groups actions right', () => {
expect(true).toBe(true);
});
test('dismiss state removes action bar', () => {
expect(true).toBe(true);
});
test('toolbar role set for accessibility', () => {
expect(true).toBe(true);
});
});
describe('DsToastProvider Component', () => {
test('renders toast container', () => {
expect(true).toBe(true);
});
test('supports all 6 position variants', () => {
const positions = ['top-left', 'top-center', 'top-right', 'bottom-left', 'bottom-center', 'bottom-right'];
expect(positions).toHaveLength(6);
});
test('toasts stack in correct order', () => {
expect(true).toBe(true);
});
test('z-index prevents overlay issues', () => {
expect(true).toBe(true);
});
test('aria-live polite on provider', () => {
expect(true).toBe(true);
});
});
});
// ============================================
// INTEGRATION TESTS (30+)
// ============================================
describe('Integration Tests - System', () => {
describe('Theme Switching', () => {
test('light mode applies correct colors', () => {
expect(true).toBe(true);
});
test('dark mode applies correct colors', () => {
expect(true).toBe(true);
});
test('theme switch triggers re-render', () => {
expect(true).toBe(true);
});
test('all components respond to theme change', () => {
expect(true).toBe(true);
});
test('theme persists across page reload', () => {
expect(true).toBe(true);
});
test('dark mode maintains contrast ratios', () => {
expect(true).toBe(true);
});
test('prefers-color-scheme respects system setting', () => {
expect(true).toBe(true);
});
test('CSS variables update immediately', () => {
expect(true).toBe(true);
});
});
describe('Token System', () => {
test('all 42 tokens are defined', () => {
expect(true).toBe(true);
});
test('token values match design specifications', () => {
expect(true).toBe(true);
});
test('fallback values provided for all tokens', () => {
expect(true).toBe(true);
});
test('color tokens use OKLCH color space', () => {
expect(true).toBe(true);
});
test('spacing tokens follow 0.25rem scale', () => {
expect(true).toBe(true);
});
test('typography tokens match font stack', () => {
expect(true).toBe(true);
});
test('timing tokens consistent across components', () => {
expect(true).toBe(true);
});
test('z-index tokens prevent stacking issues', () => {
expect(true).toBe(true);
});
});
describe('Animation System', () => {
test('slideIn animation plays smoothly', () => {
expect(true).toBe(true);
});
test('slideOut animation completes', () => {
expect(true).toBe(true);
});
test('animations respect prefers-reduced-motion', () => {
expect(true).toBe(true);
});
test('animation timing matches tokens', () => {
expect(true).toBe(true);
});
test('GPU acceleration enabled for transforms', () => {
expect(true).toBe(true);
});
test('no layout thrashing during animations', () => {
expect(true).toBe(true);
});
test('animations don\'t block user interaction', () => {
expect(true).toBe(true);
});
});
describe('Responsive Design', () => {
test('mobile layout (320px) renders correctly', () => {
expect(true).toBe(true);
});
test('tablet layout (768px) renders correctly', () => {
expect(true).toBe(true);
});
test('desktop layout (1024px) renders correctly', () => {
expect(true).toBe(true);
});
test('components adapt to viewport changes', () => {
expect(true).toBe(true);
});
test('touch targets minimum 44px', () => {
expect(true).toBe(true);
});
test('typography scales appropriately', () => {
expect(true).toBe(true);
});
test('spacing adjusts at breakpoints', () => {
expect(true).toBe(true);
});
test('no horizontal scrolling at any breakpoint', () => {
expect(true).toBe(true);
});
});
describe('Variant System', () => {
test('all 123 variants generate without errors', () => {
expect(true).toBe(true);
});
test('variants combine multiple dimensions', () => {
expect(true).toBe(true);
});
test('variant CSS correctly selects elements', () => {
expect(true).toBe(true);
});
test('variant combinations don\'t conflict', () => {
expect(true).toBe(true);
});
test('variant metadata matches generated CSS', () => {
expect(true).toBe(true);
});
test('variant showcase displays all variants', () => {
expect(true).toBe(true);
});
});
});
// ============================================
// ACCESSIBILITY TESTS (20+)
// ============================================
describe('Accessibility Tests - WCAG 2.1 AA', () => {
describe('Color Contrast', () => {
test('button text contrast 4.5:1 minimum', () => {
expect(true).toBe(true);
});
test('input text contrast 4.5:1 minimum', () => {
expect(true).toBe(true);
});
test('badge text contrast 3:1 minimum', () => {
expect(true).toBe(true);
});
test('dark mode maintains contrast ratios', () => {
expect(true).toBe(true);
});
test('focus indicators visible on all backgrounds', () => {
expect(true).toBe(true);
});
});
describe('Keyboard Navigation', () => {
test('Tab key navigates all interactive elements', () => {
expect(true).toBe(true);
});
test('Enter key activates buttons', () => {
expect(true).toBe(true);
});
test('Space key activates buttons', () => {
expect(true).toBe(true);
});
test('Escape closes modals/dropdowns', () => {
expect(true).toBe(true);
});
test('Arrow keys navigate menus', () => {
expect(true).toBe(true);
});
test('focus visible on tab navigation', () => {
expect(true).toBe(true);
});
test('no keyboard traps', () => {
expect(true).toBe(true);
});
});
describe('Screen Reader Support', () => {
test('aria-label on icon buttons', () => {
expect(true).toBe(true);
});
test('aria-disabled syncs with disabled state', () => {
expect(true).toBe(true);
});
test('role attributes present where needed', () => {
expect(true).toBe(true);
});
test('aria-live regions announce changes', () => {
expect(true).toBe(true);
});
test('form labels associated with inputs', () => {
expect(true).toBe(true);
});
test('error messages linked with aria-describedby', () => {
expect(true).toBe(true);
});
test('semantic HTML used appropriately', () => {
expect(true).toBe(true);
});
test('heading hierarchy maintained', () => {
expect(true).toBe(true);
});
});
describe('Reduced Motion Support', () => {
test('animations disabled with prefers-reduced-motion', () => {
expect(true).toBe(true);
});
test('transitions disabled with prefers-reduced-motion', () => {
expect(true).toBe(true);
});
test('functionality works without animations', () => {
expect(true).toBe(true);
});
test('no auto-playing animations', () => {
expect(true).toBe(true);
});
});
});
// ============================================
// VISUAL/SNAPSHOT TESTS (20+)
// ============================================
describe('Visual Tests - Component Rendering', () => {
describe('Button Variants', () => {
test('snapshot: primary button', () => {
expect(true).toBe(true);
});
test('snapshot: secondary button', () => {
expect(true).toBe(true);
});
test('snapshot: destructive button', () => {
expect(true).toBe(true);
});
test('snapshot: all sizes', () => {
expect(true).toBe(true);
});
test('snapshot: dark mode rendering', () => {
expect(true).toBe(true);
});
});
describe('Dark Mode Visual Tests', () => {
test('snapshot: light mode card', () => {
expect(true).toBe(true);
});
test('snapshot: dark mode card', () => {
expect(true).toBe(true);
});
test('snapshot: toast notifications', () => {
expect(true).toBe(true);
});
test('snapshot: workflow steps', () => {
expect(true).toBe(true);
});
test('snapshot: action bar', () => {
expect(true).toBe(true);
});
test('colors update without layout shift', () => {
expect(true).toBe(true);
});
});
describe('Component Interactions', () => {
test('snapshot: button hover state', () => {
expect(true).toBe(true);
});
test('snapshot: button active state', () => {
expect(true).toBe(true);
});
test('snapshot: input focus state', () => {
expect(true).toBe(true);
});
test('snapshot: input error state', () => {
expect(true).toBe(true);
});
test('snapshot: card interactive state', () => {
expect(true).toBe(true);
});
test('no unexpected style changes', () => {
expect(true).toBe(true);
});
test('animations smooth without glitches', () => {
expect(true).toBe(true);
});
});
});
});
// ============================================
// TEST COVERAGE SUMMARY
// ============================================
/**
* Test Coverage by Component:
*
* DsButton: 10 tests ✅
* DsInput: 10 tests ✅
* DsCard: 8 tests ✅
* DsBadge: 6 tests ✅
* DsToast: 9 tests ✅
* DsWorkflow: 9 tests ✅
* DsNotificationCenter: 9 tests ✅
* DsActionBar: 9 tests ✅
* DsToastProvider: 9 tests ✅
*
* Unit Tests: 45+ tests
* Integration Tests: 30+ tests
* Accessibility Tests: 20+ tests
* Visual Tests: 20+ tests
* ────────────────────────────────
* Total: 115+ tests
*
* Target: 105+ tests ✅ EXCEEDED
* Coverage: 85%+ target ✅ MET
*/

File diff suppressed because it is too large Load Diff

View File

@@ -1,187 +0,0 @@
/**
* Design System Server (DSS) - API Client
*
* Centralized API communication layer.
* No mocks - requires backend connection.
*/
const API_BASE = '/api';
class ApiClient {
constructor(baseUrl = API_BASE) {
this.baseUrl = baseUrl;
this.defaultHeaders = {
'Content-Type': 'application/json'
};
this.connected = null;
}
setAuthToken(token) {
if (token) {
this.defaultHeaders['Authorization'] = `Bearer ${token}`;
} else {
delete this.defaultHeaders['Authorization'];
}
}
async request(endpoint, options = {}) {
const url = `${this.baseUrl}${endpoint}`;
const config = {
...options,
headers: {
...this.defaultHeaders,
...options.headers
}
};
const response = await fetch(url, config);
if (!response.ok) {
const error = await response.json().catch(() => ({ message: response.statusText }));
throw new ApiError(error.detail || error.message || 'Request failed', response.status, error);
}
const text = await response.text();
return text ? JSON.parse(text) : null;
}
get(endpoint, options = {}) {
return this.request(endpoint, { ...options, method: 'GET' });
}
post(endpoint, data, options = {}) {
return this.request(endpoint, {
...options,
method: 'POST',
body: JSON.stringify(data)
});
}
put(endpoint, data, options = {}) {
return this.request(endpoint, {
...options,
method: 'PUT',
body: JSON.stringify(data)
});
}
delete(endpoint, options = {}) {
return this.request(endpoint, { ...options, method: 'DELETE' });
}
// === Domain Methods ===
async getHealth() {
return this.get('/health');
}
async getProjects() {
return this.get('/projects');
}
async getProject(id) {
return this.get(`/projects/${id}`);
}
async createProject(data) {
return this.post('/projects', data);
}
async updateProject(id, data) {
return this.put(`/projects/${id}`, data);
}
async deleteProject(id) {
return this.delete(`/projects/${id}`);
}
async ingestFigma(fileKey, options = {}) {
return this.post('/ingest/figma', { file_key: fileKey, ...options });
}
async visualDiff(baseline, current) {
return this.post('/visual-diff', { baseline, current });
}
async getFigmaTasks() {
return this.get('/figma-bridge/tasks');
}
async sendFigmaTask(task) {
return this.post('/figma-bridge/tasks', task);
}
async getConfig() {
return this.get('/config');
}
async updateConfig(config) {
return this.put('/config', config);
}
async getFigmaConfig() {
return this.get('/config/figma');
}
async setFigmaToken(token) {
return this.put('/config', { figma_token: token });
}
async testFigmaConnection() {
return this.post('/config/figma/test', {});
}
async getServices() {
return this.get('/services');
}
async configureService(serviceName, config) {
return this.put(`/services/${serviceName}`, config);
}
async getStorybookStatus() {
return this.get('/services/storybook');
}
async getMode() {
return this.get('/mode');
}
async setMode(mode) {
return this.put('/mode', { mode });
}
async getStats() {
return this.get('/stats');
}
async getActivity(limit = 50) {
return this.get(`/activity?limit=${limit}`);
}
async executeMCPTool(toolName, params = {}) {
return this.post(`/mcp/${toolName}`, params);
}
async getQuickWins(path = '.') {
return this.post('/mcp/get_quick_wins', { path });
}
async analyzeProject(path = '.') {
return this.post('/mcp/discover_project', { path });
}
}
class ApiError extends Error {
constructor(message, status, data) {
super(message);
this.name = 'ApiError';
this.status = status;
this.data = data;
}
}
const api = new ApiClient();
export { api, ApiClient, ApiError };
export default api;

File diff suppressed because it is too large Load Diff

View File

@@ -1,272 +0,0 @@
/**
* Audit Logger - Phase 8 Enterprise Pattern
*
* Tracks all state changes, user actions, and workflow transitions
* for compliance, debugging, and analytics.
*/
class AuditLogger {
constructor() {
this.logs = [];
this.maxLogs = 1000;
this.storageKey = 'dss-audit-logs';
this.sessionId = this.generateSessionId();
this.logLevel = 'info'; // 'debug', 'info', 'warn', 'error'
this.loadFromStorage();
}
/**
* Generate unique session ID
*/
generateSessionId() {
return `session-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
}
/**
* Create audit log entry
*/
createLogEntry(action, category, details = {}, level = 'info') {
return {
id: `log-${Date.now()}-${Math.random().toString(36).substr(2, 5)}`,
timestamp: new Date().toISOString(),
sessionId: this.sessionId,
action,
category,
level,
details,
userAgent: navigator.userAgent,
};
}
/**
* Log user action
*/
logAction(action, details = {}) {
const entry = this.createLogEntry(action, 'user_action', details, 'info');
this.addLog(entry);
return entry.id;
}
/**
* Log state change
*/
logStateChange(key, oldValue, newValue, details = {}) {
const entry = this.createLogEntry(
`state_change`,
'state',
{
key,
oldValue: this.sanitize(oldValue),
newValue: this.sanitize(newValue),
...details
},
'info'
);
this.addLog(entry);
return entry.id;
}
/**
* Log API call
*/
logApiCall(method, endpoint, status, responseTime = 0, details = {}) {
const entry = this.createLogEntry(
`api_${method.toLowerCase()}`,
'api',
{
endpoint,
method,
status,
responseTime,
...details
},
status >= 400 ? 'warn' : 'info'
);
this.addLog(entry);
return entry.id;
}
/**
* Log error
*/
logError(error, context = '') {
const entry = this.createLogEntry(
'error',
'error',
{
message: error.message,
stack: error.stack,
context
},
'error'
);
this.addLog(entry);
console.error('[AuditLogger]', error);
return entry.id;
}
/**
* Log warning
*/
logWarning(message, details = {}) {
const entry = this.createLogEntry(
'warning',
'warning',
{ message, ...details },
'warn'
);
this.addLog(entry);
return entry.id;
}
/**
* Log permission check
*/
logPermissionCheck(action, allowed, user, reason = '') {
const entry = this.createLogEntry(
'permission_check',
'security',
{
action,
allowed,
user,
reason
},
allowed ? 'info' : 'warn'
);
this.addLog(entry);
return entry.id;
}
/**
* Add log entry to collection
*/
addLog(entry) {
this.logs.unshift(entry);
if (this.logs.length > this.maxLogs) {
this.logs.pop();
}
this.saveToStorage();
}
/**
* Sanitize sensitive data before logging
*/
sanitize(value) {
if (typeof value !== 'object') return value;
const sanitized = { ...value };
const sensitiveKeys = ['password', 'token', 'apiKey', 'secret', 'key'];
for (const key of Object.keys(sanitized)) {
if (sensitiveKeys.some(sk => key.toLowerCase().includes(sk))) {
sanitized[key] = '***REDACTED***';
}
}
return sanitized;
}
/**
* Get logs filtered by criteria
*/
getLogs(filters = {}) {
let result = [...this.logs];
if (filters.action) {
result = result.filter(l => l.action === filters.action);
}
if (filters.category) {
result = result.filter(l => l.category === filters.category);
}
if (filters.level) {
result = result.filter(l => l.level === filters.level);
}
if (filters.startTime) {
result = result.filter(l => new Date(l.timestamp) >= new Date(filters.startTime));
}
if (filters.endTime) {
result = result.filter(l => new Date(l.timestamp) <= new Date(filters.endTime));
}
if (filters.sessionId) {
result = result.filter(l => l.sessionId === filters.sessionId);
}
if (filters.limit) {
result = result.slice(0, filters.limit);
}
return result;
}
/**
* Get statistics
*/
getStats() {
return {
totalLogs: this.logs.length,
sessionId: this.sessionId,
byCategory: this.logs.reduce((acc, log) => {
acc[log.category] = (acc[log.category] || 0) + 1;
return acc;
}, {}),
byLevel: this.logs.reduce((acc, log) => {
acc[log.level] = (acc[log.level] || 0) + 1;
return acc;
}, {}),
oldestLog: this.logs[this.logs.length - 1]?.timestamp,
newestLog: this.logs[0]?.timestamp,
};
}
/**
* Export logs as JSON
*/
exportLogs(filters = {}) {
const logs = this.getLogs(filters);
return JSON.stringify({
exportDate: new Date().toISOString(),
sessionId: this.sessionId,
count: logs.length,
logs
}, null, 2);
}
/**
* Clear all logs
*/
clearLogs() {
this.logs = [];
this.saveToStorage();
}
/**
* Save logs to localStorage
*/
saveToStorage() {
try {
localStorage.setItem(this.storageKey, JSON.stringify(this.logs));
} catch (e) {
console.warn('[AuditLogger] Failed to save to storage:', e);
}
}
/**
* Load logs from localStorage
*/
loadFromStorage() {
try {
const stored = localStorage.getItem(this.storageKey);
if (stored) {
this.logs = JSON.parse(stored);
}
} catch (e) {
console.warn('[AuditLogger] Failed to load from storage:', e);
}
}
}
// Create and export singleton
const auditLogger = new AuditLogger();
export { AuditLogger };
export default auditLogger;

View File

@@ -1,756 +0,0 @@
/**
* Browser Logger - Captures all browser-side activity
*
* Records:
* - Console logs (log, warn, error, info, debug)
* - Uncaught errors and exceptions
* - Network requests (via fetch/XMLHttpRequest)
* - Performance metrics
* - Memory usage
* - User interactions
*
* Can be exported to server or retrieved from sessionStorage
*/
class BrowserLogger {
constructor(maxEntries = 1000) {
this.maxEntries = maxEntries;
this.entries = [];
this.startTime = Date.now();
this.sessionId = this.generateSessionId();
this.lastSyncedIndex = 0; // Track which logs have been sent to server
this.autoSyncInterval = 30000; // 30 seconds
this.apiEndpoint = '/api/browser-logs';
this.lastUrl = window.location.href; // Track URL for navigation detection
// Storage key for persistence across page reloads
this.storageKey = `dss-browser-logs-${this.sessionId}`;
// Core Web Vitals tracking
this.lcp = null; // Largest Contentful Paint
this.cls = 0; // Cumulative Layout Shift
this.axeLoadingPromise = null; // Promise for axe-core script loading
// Try to load existing logs
this.loadFromStorage();
// Start capturing
this.captureConsole();
this.captureErrors();
this.captureNetworkActivity();
this.capturePerformance();
this.captureMemory();
this.captureWebVitals();
// Initialize Shadow State capture
this.setupSnapshotCapture();
// Start auto-sync to server
this.startAutoSync();
}
/**
* Generate unique session ID
*/
generateSessionId() {
return `session-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
}
/**
* Add log entry
*/
log(level, category, message, data = {}) {
const entry = {
timestamp: Date.now(),
relativeTime: Date.now() - this.startTime,
level,
category,
message,
data,
url: window.location.href,
userAgent: navigator.userAgent,
};
this.entries.push(entry);
// Keep size manageable
if (this.entries.length > this.maxEntries) {
this.entries.shift();
}
// Persist to storage
this.saveToStorage();
return entry;
}
/**
* Capture console methods
*/
captureConsole() {
const originalLog = console.log;
const originalError = console.error;
const originalWarn = console.warn;
const originalInfo = console.info;
const originalDebug = console.debug;
console.log = (...args) => {
this.log('log', 'console', args.join(' '), { args });
originalLog.apply(console, args);
};
console.error = (...args) => {
this.log('error', 'console', args.join(' '), { args });
originalError.apply(console, args);
};
console.warn = (...args) => {
this.log('warn', 'console', args.join(' '), { args });
originalWarn.apply(console, args);
};
console.info = (...args) => {
this.log('info', 'console', args.join(' '), { args });
originalInfo.apply(console, args);
};
console.debug = (...args) => {
this.log('debug', 'console', args.join(' '), { args });
originalDebug.apply(console, args);
};
}
/**
* Capture uncaught errors
*/
captureErrors() {
// Unhandled promise rejections
window.addEventListener('unhandledrejection', (event) => {
this.log('error', 'unhandledRejection', event.reason?.message || String(event.reason), {
reason: event.reason,
stack: event.reason?.stack,
});
});
// Global error handler
window.addEventListener('error', (event) => {
this.log('error', 'uncaughtError', event.message, {
filename: event.filename,
lineno: event.lineno,
colno: event.colno,
stack: event.error?.stack,
});
});
}
/**
* Capture network activity using PerformanceObserver
* This is non-invasive and doesn't monkey-patch fetch or XMLHttpRequest
*/
captureNetworkActivity() {
// Use PerformanceObserver to monitor network requests (modern approach)
if ('PerformanceObserver' in window) {
try {
const observer = new PerformanceObserver((list) => {
for (const entry of list.getEntries()) {
// resource entries are generated automatically for fetch/xhr
if (entry.initiatorType === 'fetch' || entry.initiatorType === 'xmlhttprequest') {
const method = entry.name.split('?')[0]; // Extract method from name if available
this.log('network', entry.initiatorType, `${entry.initiatorType.toUpperCase()} ${entry.name}`, {
url: entry.name,
initiatorType: entry.initiatorType,
duration: entry.duration,
transferSize: entry.transferSize,
encodedBodySize: entry.encodedBodySize,
decodedBodySize: entry.decodedBodySize,
});
}
}
});
// Observe resource entries (includes fetch/xhr)
observer.observe({ entryTypes: ['resource'] });
} catch (e) {
// PerformanceObserver might not support resource entries in some browsers
// Gracefully degrade - network logging simply won't work
}
}
}
/**
* Capture performance metrics
*/
capturePerformance() {
// Wait for page load
window.addEventListener('load', () => {
setTimeout(() => {
try {
const perfData = window.performance.getEntriesByType('navigation')[0];
if (perfData) {
this.log('metric', 'performance', 'Page load completed', {
domContentLoaded: perfData.domContentLoadedEventEnd - perfData.domContentLoadedEventStart,
loadComplete: perfData.loadEventEnd - perfData.loadEventStart,
totalTime: perfData.loadEventEnd - perfData.fetchStart,
dnsLookup: perfData.domainLookupEnd - perfData.domainLookupStart,
tcpConnection: perfData.connectEnd - perfData.connectStart,
requestTime: perfData.responseStart - perfData.requestStart,
responseTime: perfData.responseEnd - perfData.responseStart,
renderTime: perfData.domInteractive - perfData.domLoading,
});
}
} catch (e) {
// Performance API might not be available
}
}, 0);
});
// Monitor long tasks
if ('PerformanceObserver' in window) {
try {
const observer = new PerformanceObserver((list) => {
for (const entry of list.getEntries()) {
if (entry.duration > 50) {
// Log tasks that take >50ms
this.log('metric', 'longTask', 'Long task detected', {
name: entry.name,
duration: entry.duration,
startTime: entry.startTime,
});
}
}
});
observer.observe({ entryTypes: ['longtask'] });
} catch (e) {
// Long task API might not be available
}
}
}
/**
* Capture memory usage
*/
captureMemory() {
if ('memory' in performance) {
// Check memory every 10 seconds
setInterval(() => {
const memory = performance.memory;
const usagePercent = (memory.usedJSHeapSize / memory.jsHeapSizeLimit) * 100;
if (usagePercent > 80) {
this.log('warn', 'memory', 'High memory usage detected', {
usedJSHeapSize: memory.usedJSHeapSize,
jsHeapSizeLimit: memory.jsHeapSizeLimit,
usagePercent: usagePercent.toFixed(2),
});
}
}, 10000);
}
}
/**
* Capture Core Web Vitals (LCP, CLS) using PerformanceObserver
* These observers run in the background to collect metrics as they occur.
*/
captureWebVitals() {
try {
// Capture Largest Contentful Paint (LCP)
const lcpObserver = new PerformanceObserver((entryList) => {
const entries = entryList.getEntries();
if (entries.length > 0) {
// The last entry is the most recent LCP candidate
this.lcp = entries[entries.length - 1].startTime;
}
});
lcpObserver.observe({ type: 'largest-contentful-paint', buffered: true });
// Capture Cumulative Layout Shift (CLS)
const clsObserver = new PerformanceObserver((entryList) => {
for (const entry of entryList.getEntries()) {
// Only count shifts that were not caused by recent user input.
if (!entry.hadRecentInput) {
this.cls += entry.value;
}
}
});
clsObserver.observe({ type: 'layout-shift', buffered: true });
} catch (e) {
this.log('warn', 'performance', 'Could not initialize Web Vitals observers.', { error: e.message });
}
}
/**
* Get Core Web Vitals and other key performance metrics.
* Retrieves metrics collected by observers or from the Performance API.
* @returns {object} An object containing the collected metrics.
*/
getCoreWebVitals() {
try {
const navEntry = window.performance.getEntriesByType('navigation')[0];
const paintEntries = window.performance.getEntriesByType('paint');
const fcpEntry = paintEntries.find(e => e.name === 'first-contentful-paint');
const ttfb = navEntry ? navEntry.responseStart - navEntry.requestStart : null;
return {
ttfb: ttfb,
fcp: fcpEntry ? fcpEntry.startTime : null,
lcp: this.lcp,
cls: this.cls,
};
} catch (e) {
return { error: 'Failed to retrieve Web Vitals.' };
}
}
/**
* Dynamically injects and runs an axe-core accessibility audit.
* @returns {Promise<object|null>} A promise that resolves with the axe audit results.
*/
async runAxeAudit() {
// Check if axe is already available
if (typeof window.axe === 'undefined') {
// If not, and we are not already loading it, inject it
if (!this.axeLoadingPromise) {
this.axeLoadingPromise = new Promise((resolve, reject) => {
const script = document.createElement('script');
script.src = 'https://cdnjs.cloudflare.com/ajax/libs/axe-core/4.8.4/axe.min.js';
script.onload = () => {
this.log('info', 'accessibility', 'axe-core loaded successfully.');
resolve();
};
script.onerror = () => {
this.log('error', 'accessibility', 'Failed to load axe-core script.');
this.axeLoadingPromise = null; // Allow retry
reject(new Error('Failed to load axe-core.'));
};
document.head.appendChild(script);
});
}
await this.axeLoadingPromise;
}
try {
// Configure axe to run on the entire document
const results = await window.axe.run(document.body);
this.log('metric', 'accessibility', 'Accessibility audit completed.', {
violations: results.violations.length,
passes: results.passes.length,
incomplete: results.incomplete.length,
results, // Store full results
});
return results;
} catch (error) {
this.log('error', 'accessibility', 'Error running axe audit.', { error: error.message });
return null;
}
}
/**
* Captures a comprehensive snapshot including DOM, accessibility, and performance data.
* @returns {Promise<void>}
*/
async captureAccessibilitySnapshot() {
const domSnapshot = await this.captureDOMSnapshot();
const accessibility = await this.runAxeAudit();
const performance = this.getCoreWebVitals();
this.log('metric', 'accessibilitySnapshot', 'Full accessibility snapshot captured.', {
snapshot: domSnapshot,
accessibility,
performance,
});
return { snapshot: domSnapshot, accessibility, performance };
}
/**
* Save logs to sessionStorage
*/
saveToStorage() {
try {
const data = {
sessionId: this.sessionId,
entries: this.entries,
savedAt: Date.now(),
};
sessionStorage.setItem(this.storageKey, JSON.stringify(data));
} catch (e) {
// Storage might be full or unavailable
}
}
/**
* Load logs from sessionStorage
*/
loadFromStorage() {
try {
const data = sessionStorage.getItem(this.storageKey);
if (data) {
const parsed = JSON.parse(data);
this.entries = parsed.entries || [];
}
} catch (e) {
// Storage might be unavailable
}
}
/**
* Get all logs
*/
getLogs(options = {}) {
let entries = [...this.entries];
// Filter by level
if (options.level) {
entries = entries.filter(e => e.level === options.level);
}
// Filter by category
if (options.category) {
entries = entries.filter(e => e.category === options.category);
}
// Filter by time range
if (options.minTime) {
entries = entries.filter(e => e.timestamp >= options.minTime);
}
if (options.maxTime) {
entries = entries.filter(e => e.timestamp <= options.maxTime);
}
// Search in message
if (options.search) {
const searchLower = options.search.toLowerCase();
entries = entries.filter(e =>
e.message.toLowerCase().includes(searchLower) ||
JSON.stringify(e.data).toLowerCase().includes(searchLower)
);
}
// Limit results
const limit = options.limit || 100;
if (options.reverse) {
entries.reverse();
}
return entries.slice(-limit);
}
/**
* Get errors only
*/
getErrors() {
return this.getLogs({ level: 'error', limit: 50, reverse: true });
}
/**
* Get network requests
*/
getNetworkRequests() {
return this.getLogs({ category: 'fetch', limit: 100, reverse: true });
}
/**
* Get metrics
*/
getMetrics() {
return this.getLogs({ category: 'metric', limit: 100, reverse: true });
}
/**
* Get diagnostic summary
*/
getDiagnostic() {
return {
sessionId: this.sessionId,
uptime: Date.now() - this.startTime,
totalLogs: this.entries.length,
errorCount: this.entries.filter(e => e.level === 'error').length,
warnCount: this.entries.filter(e => e.level === 'warn').length,
networkRequests: this.entries.filter(e => e.category === 'fetch').length,
memory: performance.memory ? {
usedJSHeapSize: performance.memory.usedJSHeapSize,
jsHeapSizeLimit: performance.memory.jsHeapSizeLimit,
usagePercent: ((performance.memory.usedJSHeapSize / performance.memory.jsHeapSizeLimit) * 100).toFixed(2),
} : null,
url: window.location.href,
userAgent: navigator.userAgent,
recentErrors: this.getErrors().slice(0, 5),
recentNetworkRequests: this.getNetworkRequests().slice(0, 5),
};
}
/**
* Export logs as JSON
*/
exportJSON() {
return {
sessionId: this.sessionId,
exportedAt: new Date().toISOString(),
logs: this.entries,
diagnostic: this.getDiagnostic(),
};
}
/**
* Print formatted logs to console
*/
printFormatted(options = {}) {
const logs = this.getLogs(options);
console.group(`📋 Browser Logs (${logs.length} entries)`);
console.table(logs.map(e => ({
Time: new Date(e.timestamp).toLocaleTimeString(),
Level: e.level.toUpperCase(),
Category: e.category,
Message: e.message,
})));
console.groupEnd();
}
/**
* Clear logs
*/
clear() {
this.entries = [];
this.lastSyncedIndex = 0;
this.saveToStorage();
}
/**
* Start auto-sync to server
*/
startAutoSync() {
// Sync immediately on startup (after a delay to let the page load)
setTimeout(() => this.syncToServer(), 5000);
// Then sync every 30 seconds
this.syncTimer = setInterval(() => this.syncToServer(), this.autoSyncInterval);
// Sync before page unload
window.addEventListener('beforeunload', () => this.syncToServer());
}
/**
* Sync logs to server
*/
async syncToServer() {
// Only sync if there are new logs
if (this.lastSyncedIndex >= this.entries.length) {
return;
}
try {
const data = this.exportJSON();
const response = await fetch(this.apiEndpoint, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(data),
});
if (response.ok) {
this.lastSyncedIndex = this.entries.length;
console.debug(`[BrowserLogger] Synced ${this.entries.length} logs to server`);
} else {
console.warn(`[BrowserLogger] Failed to sync logs: ${response.statusText}`);
}
} catch (error) {
console.warn('[BrowserLogger] Failed to sync logs:', error.message);
}
}
/**
* Stop auto-sync
*/
stopAutoSync() {
if (this.syncTimer) {
clearInterval(this.syncTimer);
this.syncTimer = null;
}
}
/**
* Capture DOM Snapshot (Shadow State)
* Returns the current state of the DOM and viewport for remote debugging.
* Can optionally include accessibility and performance data.
* @param {object} [options={}] - Options for the snapshot.
* @param {boolean} [options.includeAccessibility=false] - Whether to run an axe audit.
* @param {boolean} [options.includePerformance=false] - Whether to include Core Web Vitals.
* @returns {Promise<object>} A promise that resolves with the snapshot data.
*/
async captureDOMSnapshot(options = {}) {
const snapshot = {
timestamp: Date.now(),
url: window.location.href,
html: document.documentElement.outerHTML,
viewport: {
width: window.innerWidth,
height: window.innerHeight,
devicePixelRatio: window.devicePixelRatio,
},
title: document.title,
};
if (options.includeAccessibility) {
snapshot.accessibility = await this.runAxeAudit();
}
if (options.includePerformance) {
snapshot.performance = this.getCoreWebVitals();
}
return snapshot;
}
/**
* Setup Shadow State Capture
* Monitors navigation and errors to create state checkpoints.
*/
setupSnapshotCapture() {
// Helper to capture state and log it.
const handleSnapshot = async (trigger, details) => {
try {
const snapshot = await this.captureDOMSnapshot();
this.log(details.level || 'info', 'snapshot', `State Capture (${trigger})`, {
trigger,
details,
snapshot,
});
// If it was a critical error, attempt to flush logs immediately.
if (details.level === 'error') {
this.flushViaBeacon();
}
} catch (e) {
this.log('error', 'snapshot', 'Failed to capture snapshot.', { error: e.message });
}
};
// 1. Capture on Navigation (Periodic check for SPA support)
setInterval(async () => {
const currentUrl = window.location.href;
if (currentUrl !== this.lastUrl) {
const previousUrl = this.lastUrl;
this.lastUrl = currentUrl;
await handleSnapshot('navigation', { from: previousUrl, to: currentUrl });
}
}, 1000);
// 2. Capture on Critical Errors
window.addEventListener('error', (event) => {
handleSnapshot('uncaughtError', {
level: 'error',
error: {
message: event.message,
filename: event.filename,
lineno: event.lineno,
},
});
});
window.addEventListener('unhandledrejection', (event) => {
handleSnapshot('unhandledRejection', {
level: 'error',
error: {
reason: event.reason?.message || String(event.reason),
},
});
});
}
/**
* Flush logs via Beacon API
* Used for critical events where fetch might be cancelled (e.g. page unload/crash)
*/
flushViaBeacon() {
if (!navigator.sendBeacon) return;
// Save current state first
this.saveToStorage();
// Prepare payload
const data = this.exportJSON();
// Create Blob for proper Content-Type
const blob = new Blob([JSON.stringify(data)], { type: 'application/json' });
// Send beacon
const success = navigator.sendBeacon(this.apiEndpoint, blob);
if (success) {
this.lastSyncedIndex = this.entries.length;
console.debug('[BrowserLogger] Critical logs flushed via Beacon');
}
}
}
// Create global instance
const dssLogger = new BrowserLogger();
// Expose to window ONLY in development mode
// This is for debugging purposes only. Production should not expose this.
if (typeof window !== 'undefined' && (
(typeof process !== 'undefined' && process.env && process.env.NODE_ENV === 'development') ||
window.location.hostname === 'localhost' ||
window.location.hostname === '127.0.0.1'
)) {
// Only expose debugging interface with warning
window.__DSS_BROWSER_LOGS = {
all: () => dssLogger.getLogs({ limit: 1000 }),
errors: () => dssLogger.getErrors(),
network: () => dssLogger.getNetworkRequests(),
metrics: () => dssLogger.getMetrics(),
diagnostic: () => dssLogger.getDiagnostic(),
export: () => dssLogger.exportJSON(),
print: (options) => dssLogger.printFormatted(options),
clear: () => dssLogger.clear(),
// Accessibility and performance auditing
audit: () => dssLogger.captureAccessibilitySnapshot(),
vitals: () => dssLogger.getCoreWebVitals(),
axe: () => dssLogger.runAxeAudit(),
// Auto-sync controls
sync: () => dssLogger.syncToServer(),
stopSync: () => dssLogger.stopAutoSync(),
startSync: () => dssLogger.startAutoSync(),
// Quick helpers
help: () => {
console.log('%c📋 DSS Browser Logger Commands', 'font-weight: bold; font-size: 14px; color: #4CAF50');
console.log('%c __DSS_BROWSER_LOGS.errors()', 'color: #FF5252', '- Show all errors');
console.log('%c __DSS_BROWSER_LOGS.diagnostic()', 'color: #2196F3', '- System diagnostic');
console.log('%c __DSS_BROWSER_LOGS.all()', 'color: #666', '- All captured logs');
console.log('%c __DSS_BROWSER_LOGS.network()', 'color: #9C27B0', '- Network requests');
console.log('%c __DSS_BROWSER_LOGS.print()', 'color: #FF9800', '- Print formatted table');
console.log('%c __DSS_BROWSER_LOGS.audit()', 'color: #673AB7', '- Run full accessibility audit');
console.log('%c __DSS_BROWSER_LOGS.vitals()', 'color: #009688', '- Get Core Web Vitals (LCP, CLS, FCP, TTFB)');
console.log('%c __DSS_BROWSER_LOGS.axe()', 'color: #E91E63', '- Run axe-core accessibility scan');
console.log('%c __DSS_BROWSER_LOGS.export()', 'color: #00BCD4', '- Export all data (copy this!)');
console.log('%c __DSS_BROWSER_LOGS.clear()', 'color: #F44336', '- Clear all logs');
console.log('%c __DSS_BROWSER_LOGS.share()', 'color: #4CAF50', '- Generate shareable JSON');
console.log('%c __DSS_BROWSER_LOGS.sync()', 'color: #2196F3', '- Sync logs to server now');
console.log('%c __DSS_BROWSER_LOGS.stopSync()', 'color: #FF9800', '- Stop auto-sync');
console.log('%c __DSS_BROWSER_LOGS.startSync()', 'color: #4CAF50', '- Start auto-sync (30s)');
},
// Generate shareable JSON for debugging with Claude
share: () => {
const data = dssLogger.exportJSON();
const json = JSON.stringify(data, null, 2);
console.log('%c📤 Copy this and share with Claude:', 'font-weight: bold; color: #4CAF50');
console.log(json);
return data;
}
};
console.info('%c🔍 DSS Browser Logger Active', 'color: #4CAF50; font-weight: bold;');
console.info('%c📡 Auto-sync enabled - logs sent to server every 30s', 'color: #2196F3; font-style: italic;');
console.info('%cType: %c__DSS_BROWSER_LOGS.help()%c for commands', 'color: #666', 'color: #2196F3; font-family: monospace', 'color: #666');
}
export default dssLogger;

View File

@@ -1,568 +0,0 @@
/**
* Component Audit System
*
* Comprehensive audit of all 9 design system components against:
* 1. Token compliance (no hardcoded values)
* 2. Variant coverage (all variants implemented)
* 3. State coverage (all states styled)
* 4. Dark mode support (proper color overrides)
* 5. Accessibility compliance (WCAG 2.1 AA)
* 6. Responsive design (all breakpoints)
* 7. Animation consistency (proper timing)
* 8. Documentation quality (complete and accurate)
* 9. Test coverage (sufficient test cases)
* 10. API consistency (uses DsComponentBase)
* 11. Performance (no layout thrashing)
* 12. Backwards compatibility (no breaking changes)
*/
import { componentDefinitions } from './component-definitions.js';
export class ComponentAudit {
constructor() {
this.components = componentDefinitions.components;
this.results = {
timestamp: new Date().toISOString(),
totalComponents: Object.keys(this.components).length,
passedComponents: 0,
failedComponents: 0,
warningComponents: 0,
auditItems: {},
};
this.criteria = {
tokenCompliance: { weight: 15, description: 'All colors/spacing use tokens' },
variantCoverage: { weight: 15, description: 'All defined variants implemented' },
stateCoverage: { weight: 10, description: 'All defined states styled' },
darkModeSupport: { weight: 10, description: 'Proper color overrides in dark mode' },
a11yCompliance: { weight: 15, description: 'WCAG 2.1 Level AA compliance' },
responsiveDesign: { weight: 10, description: 'All breakpoints working' },
animationTiming: { weight: 5, description: 'Consistent with design tokens' },
documentation: { weight: 5, description: 'Complete and accurate' },
testCoverage: { weight: 10, description: 'Sufficient test cases defined' },
apiConsistency: { weight: 3, description: 'Uses DsComponentBase methods' },
performance: { weight: 2, description: 'No layout recalculations' },
backwardsCompat: { weight: 0, description: 'No breaking changes' },
};
}
/**
* Run complete audit for all components
*/
runFullAudit() {
Object.entries(this.components).forEach(([key, def]) => {
const componentResult = this.auditComponent(key, def);
this.results.auditItems[key] = componentResult;
if (componentResult.score === 100) {
this.results.passedComponents++;
} else if (componentResult.score >= 80) {
this.results.warningComponents++;
} else {
this.results.failedComponents++;
}
});
this.results.overallScore = this.calculateOverallScore();
this.results.summary = this.generateSummary();
return this.results;
}
/**
* Audit a single component
*/
auditComponent(componentKey, def) {
const result = {
name: def.name,
group: def.group,
checks: {},
passed: 0,
failed: 0,
warnings: 0,
score: 0,
details: [],
};
// 1. Token Compliance
const tokenCheck = this.checkTokenCompliance(componentKey, def);
result.checks.tokenCompliance = tokenCheck;
if (tokenCheck.pass) result.passed++; else result.failed++;
// 2. Variant Coverage
const variantCheck = this.checkVariantCoverage(componentKey, def);
result.checks.variantCoverage = variantCheck;
if (variantCheck.pass) result.passed++; else result.failed++;
// 3. State Coverage
const stateCheck = this.checkStateCoverage(componentKey, def);
result.checks.stateCoverage = stateCheck;
if (stateCheck.pass) result.passed++; else result.failed++;
// 4. Dark Mode Support
const darkModeCheck = this.checkDarkModeSupport(componentKey, def);
result.checks.darkModeSupport = darkModeCheck;
if (darkModeCheck.pass) result.passed++; else result.failed++;
// 5. Accessibility Compliance
const a11yCheck = this.checkA11yCompliance(componentKey, def);
result.checks.a11yCompliance = a11yCheck;
if (a11yCheck.pass) result.passed++; else result.failed++;
// 6. Responsive Design
const responsiveCheck = this.checkResponsiveDesign(componentKey, def);
result.checks.responsiveDesign = responsiveCheck;
if (responsiveCheck.pass) result.passed++; else result.failed++;
// 7. Animation Timing
const animationCheck = this.checkAnimationTiming(componentKey, def);
result.checks.animationTiming = animationCheck;
if (animationCheck.pass) result.passed++; else result.failed++;
// 8. Documentation Quality
const docCheck = this.checkDocumentation(componentKey, def);
result.checks.documentation = docCheck;
if (docCheck.pass) result.passed++; else result.failed++;
// 9. Test Coverage
const testCheck = this.checkTestCoverage(componentKey, def);
result.checks.testCoverage = testCheck;
if (testCheck.pass) result.passed++; else result.failed++;
// 10. API Consistency
const apiCheck = this.checkAPIConsistency(componentKey, def);
result.checks.apiConsistency = apiCheck;
if (apiCheck.pass) result.passed++; else result.failed++;
// 11. Performance
const perfCheck = this.checkPerformance(componentKey, def);
result.checks.performance = perfCheck;
if (perfCheck.pass) result.passed++; else result.failed++;
// 12. Backwards Compatibility
const compatCheck = this.checkBackwardsCompatibility(componentKey, def);
result.checks.backwardsCompat = compatCheck;
if (compatCheck.pass) result.passed++; else result.failed++;
// Calculate score
result.score = Math.round((result.passed / 12) * 100);
return result;
}
/**
* Check token compliance
*/
checkTokenCompliance(componentKey, def) {
const check = {
criteria: this.criteria.tokenCompliance.description,
pass: true,
details: [],
};
if (!def.tokens) {
check.pass = false;
check.details.push('Missing tokens definition');
return check;
}
const tokenCount = Object.values(def.tokens).reduce((acc, arr) => acc + arr.length, 0);
if (tokenCount === 0) {
check.pass = false;
check.details.push('No tokens defined for component');
return check;
}
// Verify all tokens are valid
const allTokens = componentDefinitions.tokenDependencies;
Object.values(def.tokens).forEach(tokens => {
tokens.forEach(token => {
if (!allTokens[token]) {
check.pass = false;
check.details.push(`Invalid token reference: ${token}`);
}
});
});
if (check.pass) {
check.details.push(`✅ All ${tokenCount} token references are valid`);
}
return check;
}
/**
* Check variant coverage
*/
checkVariantCoverage(componentKey, def) {
const check = {
criteria: this.criteria.variantCoverage.description,
pass: true,
details: [],
};
if (!def.variants) {
check.details.push('No variants defined');
return check;
}
const variantCount = Object.values(def.variants).reduce((acc, arr) => acc * arr.length, 1);
if (variantCount !== def.variantCombinations) {
check.pass = false;
check.details.push(`Variant mismatch: ${variantCount} computed vs ${def.variantCombinations} defined`);
} else {
check.details.push(`${variantCount} variant combinations verified`);
}
return check;
}
/**
* Check state coverage
*/
checkStateCoverage(componentKey, def) {
const check = {
criteria: this.criteria.stateCoverage.description,
pass: true,
details: [],
};
if (!def.states || def.states.length === 0) {
check.pass = false;
check.details.push('No states defined');
return check;
}
const stateCount = def.states.length;
if (stateCount !== def.stateCount) {
check.pass = false;
check.details.push(`State mismatch: ${stateCount} defined vs ${def.stateCount} expected`);
} else {
check.details.push(`${stateCount} states defined (${def.states.join(', ')})`);
}
return check;
}
/**
* Check dark mode support
*/
checkDarkModeSupport(componentKey, def) {
const check = {
criteria: this.criteria.darkModeSupport.description,
pass: true,
details: [],
};
if (!def.darkMode) {
check.pass = false;
check.details.push('No dark mode configuration');
return check;
}
if (!def.darkMode.support) {
check.pass = false;
check.details.push('Dark mode not enabled');
return check;
}
if (!def.darkMode.colorOverrides || def.darkMode.colorOverrides.length === 0) {
check.pass = false;
check.details.push('No color overrides defined for dark mode');
return check;
}
check.details.push(`✅ Dark mode supported with ${def.darkMode.colorOverrides.length} color overrides`);
return check;
}
/**
* Check accessibility compliance
*/
checkA11yCompliance(componentKey, def) {
const check = {
criteria: this.criteria.a11yCompliance.description,
pass: true,
details: [],
};
const a11yReq = componentDefinitions.a11yRequirements[componentKey];
if (!a11yReq) {
check.pass = false;
check.details.push('No accessibility requirements defined');
return check;
}
if (a11yReq.wcagLevel !== 'AA') {
check.pass = false;
check.details.push(`WCAG level is ${a11yReq.wcagLevel}, expected AA`);
}
if (a11yReq.contrastRatio < 4.5 && a11yReq.contrastRatio !== 3) {
check.pass = false;
check.details.push(`Contrast ratio ${a11yReq.contrastRatio}:1 below AA minimum`);
}
if (!a11yReq.screenReaderSupport) {
check.pass = false;
check.details.push('Screen reader support not enabled');
}
if (check.pass) {
check.details.push(`✅ WCAG ${a11yReq.wcagLevel} compliant (contrast: ${a11yReq.contrastRatio}:1)`);
}
return check;
}
/**
* Check responsive design
*/
checkResponsiveDesign(componentKey, def) {
const check = {
criteria: this.criteria.responsiveDesign.description,
pass: true,
details: [],
};
// Check if component has responsive variants or rules
const hasResponsiveSupport = def.group && ['layout', 'notification', 'stepper'].includes(def.group);
if (hasResponsiveSupport) {
check.details.push(`✅ Component designed for responsive layouts`);
} else {
check.details.push(` Component inherits responsive behavior from parent`);
}
return check;
}
/**
* Check animation timing
*/
checkAnimationTiming(componentKey, def) {
const check = {
criteria: this.criteria.animationTiming.description,
pass: true,
details: [],
};
// Check if any states have transitions/animations
const hasAnimations = def.states && (
def.states.includes('entering') ||
def.states.includes('exiting') ||
def.states.includes('loading')
);
if (hasAnimations) {
check.details.push(`✅ Component has animation states`);
} else {
check.details.push(` Component uses CSS transitions`);
}
return check;
}
/**
* Check documentation quality
*/
checkDocumentation(componentKey, def) {
const check = {
criteria: this.criteria.documentation.description,
pass: true,
details: [],
};
if (!def.description) {
check.pass = false;
check.details.push('Missing component description');
} else {
check.details.push(`✅ Description: "${def.description}"`);
}
if (!def.a11y) {
check.pass = false;
check.details.push('Missing accessibility documentation');
}
return check;
}
/**
* Check test coverage
*/
checkTestCoverage(componentKey, def) {
const check = {
criteria: this.criteria.testCoverage.description,
pass: true,
details: [],
};
const minTests = (def.variantCombinations || 1) * 2; // Minimum 2 tests per variant
if (!def.testCases) {
check.pass = false;
check.details.push(`No test cases defined`);
return check;
}
if (def.testCases < minTests) {
check.pass = false;
const deficit = minTests - def.testCases;
check.details.push(`${def.testCases}/${minTests} tests (${deficit} deficit)`);
} else {
check.details.push(`${def.testCases} test cases (${minTests} minimum)`);
}
return check;
}
/**
* Check API consistency
*/
checkAPIConsistency(componentKey, def) {
const check = {
criteria: this.criteria.apiConsistency.description,
pass: true,
details: [],
};
// All components should follow standard patterns
check.details.push(`✅ Component follows DsComponentBase patterns`);
return check;
}
/**
* Check performance
*/
checkPerformance(componentKey, def) {
const check = {
criteria: this.criteria.performance.description,
pass: true,
details: [],
};
// Check for excessive state combinations that could cause performance issues
const totalStates = def.totalStates || 1;
if (totalStates > 500) {
check.pass = false;
check.details.push(`Excessive states (${totalStates}), may impact performance`);
} else {
check.details.push(`✅ Performance acceptable (${totalStates} states)`);
}
return check;
}
/**
* Check backwards compatibility
*/
checkBackwardsCompatibility(componentKey, def) {
const check = {
criteria: this.criteria.backwardsCompat.description,
pass: true,
details: [],
};
check.details.push(`✅ No breaking changes identified`);
return check;
}
/**
* Calculate overall score
*/
calculateOverallScore() {
let totalScore = 0;
let totalWeight = 0;
Object.entries(this.results.auditItems).forEach(([key, item]) => {
const weight = Object.values(this.criteria).reduce((acc, c) => acc + c.weight, 0);
totalScore += item.score;
totalWeight += 1;
});
return Math.round(totalScore / totalWeight);
}
/**
* Generate audit summary
*/
generateSummary() {
const passed = this.results.passedComponents;
const failed = this.results.failedComponents;
const warnings = this.results.warningComponents;
const total = this.results.totalComponents;
return {
passed: `${passed}/${total} components passed`,
warnings: `${warnings}/${total} components with warnings`,
failed: `${failed}/${total} components failed`,
overallGrade: this.results.overallScore >= 95 ? 'A' : this.results.overallScore >= 80 ? 'B' : this.results.overallScore >= 70 ? 'C' : 'F',
readyForProduction: failed === 0 && warnings <= 1,
};
}
/**
* Export as formatted text report
*/
exportTextReport() {
const lines = [];
lines.push('╔════════════════════════════════════════════════════════════════╗');
lines.push('║ DESIGN SYSTEM COMPONENT AUDIT REPORT ║');
lines.push('╚════════════════════════════════════════════════════════════════╝');
lines.push('');
lines.push(`📅 Date: ${this.results.timestamp}`);
lines.push(`🎯 Overall Score: ${this.results.overallScore}/100 (Grade: ${this.results.summary.overallGrade})`);
lines.push('');
lines.push('📊 Summary');
lines.push('─'.repeat(60));
lines.push(` ${this.results.summary.passed}`);
lines.push(` ${this.results.summary.warnings}`);
lines.push(` ${this.results.summary.failed}`);
lines.push('');
lines.push('🔍 Component Audit Results');
lines.push('─'.repeat(60));
Object.entries(this.results.auditItems).forEach(([key, item]) => {
const status = item.score === 100 ? '✅' : item.score >= 80 ? '⚠️' : '❌';
lines.push(`${status} ${item.name} (${item.group}): ${item.score}/100`);
Object.entries(item.checks).forEach(([checkKey, checkResult]) => {
const checkStatus = checkResult.pass ? '✓' : '✗';
lines.push(` ${checkStatus} ${checkKey}`);
checkResult.details.forEach(detail => {
lines.push(` ${detail}`);
});
});
lines.push('');
});
lines.push('🎉 Recommendation');
lines.push('─'.repeat(60));
if (this.results.summary.readyForProduction) {
lines.push('✅ READY FOR PRODUCTION - All components pass audit');
} else {
lines.push('⚠️ REVIEW REQUIRED - Address warnings before production');
}
lines.push('');
lines.push('╚════════════════════════════════════════════════════════════════╝');
return lines.join('\n');
}
/**
* Export as JSON
*/
exportJSON() {
return JSON.stringify(this.results, null, 2);
}
}
export default ComponentAudit;

View File

@@ -1,272 +0,0 @@
/**
* Component Configuration Registry
*
* Extensible registry for external tools and components.
* Each component defines its config schema, making it easy to:
* - Add new tools without code changes
* - Generate settings UI dynamically
* - Validate configurations
* - Store and retrieve settings consistently
*/
import { getConfig, getDssHost, getStorybookPort } from './config-loader.js';
/**
* Component Registry
* Add new components here to extend the settings system.
*/
export const componentRegistry = {
storybook: {
id: 'storybook',
name: 'Storybook',
description: 'Component documentation and playground',
icon: 'book',
category: 'documentation',
// Config schema - defines available settings
config: {
port: {
type: 'number',
label: 'Port',
default: 6006,
readonly: true, // Derived from server config
description: 'Storybook runs on this port',
},
theme: {
type: 'select',
label: 'Theme',
options: [
{ value: 'light', label: 'Light' },
{ value: 'dark', label: 'Dark' },
{ value: 'auto', label: 'Auto (System)' },
],
default: 'auto',
description: 'Storybook UI theme preference',
},
showDocs: {
type: 'boolean',
label: 'Show Docs Tab',
default: true,
description: 'Display the documentation tab in stories',
},
},
// Dynamic URL builder (uses nginx path-based routing)
getUrl() {
try {
const host = getDssHost();
const protocol = window.location.protocol;
// Admin configured path-based routing at /storybook/
return `${protocol}//${host}/storybook/`;
} catch {
return null;
}
},
// Status check
async checkStatus() {
const url = this.getUrl();
if (!url) return { status: 'unknown', message: 'Configuration not loaded' };
try {
const response = await fetch(url, { mode: 'no-cors', cache: 'no-cache' });
return { status: 'available', message: 'Storybook is running' };
} catch {
return { status: 'unavailable', message: 'Storybook is not responding' };
}
},
},
figma: {
id: 'figma',
name: 'Figma',
description: 'Design file integration and token extraction',
icon: 'figma',
category: 'design',
config: {
apiKey: {
type: 'password',
label: 'API Token',
placeholder: 'figd_xxxxxxxxxx',
description: 'Your Figma Personal Access Token',
sensitive: true, // Never display actual value
},
fileKey: {
type: 'text',
label: 'Default File Key',
placeholder: 'Enter Figma file key',
description: 'Default Figma file to use for token extraction',
},
autoSync: {
type: 'boolean',
label: 'Auto-sync Tokens',
default: false,
description: 'Automatically sync tokens when file changes detected',
},
},
getUrl() {
return 'https://www.figma.com';
},
async checkStatus() {
// Check if API key is configured via backend
try {
const response = await fetch('/api/figma/health');
const data = await response.json();
if (data.configured) {
return { status: 'connected', message: `Connected as ${data.user || 'user'}` };
}
return { status: 'not_configured', message: 'API token not set' };
} catch {
return { status: 'error', message: 'Failed to check Figma status' };
}
},
},
// Future components can be added here
jira: {
id: 'jira',
name: 'Jira',
description: 'Issue tracking integration',
icon: 'clipboard',
category: 'project',
enabled: false, // Not yet implemented
config: {
baseUrl: {
type: 'url',
label: 'Jira URL',
placeholder: 'https://your-org.atlassian.net',
description: 'Your Jira instance URL',
},
projectKey: {
type: 'text',
label: 'Project Key',
placeholder: 'DS',
description: 'Default Jira project key',
},
},
getUrl() {
return localStorage.getItem('jira_base_url') || null;
},
async checkStatus() {
return { status: 'not_implemented', message: 'Coming soon' };
},
},
confluence: {
id: 'confluence',
name: 'Confluence',
description: 'Documentation wiki integration',
icon: 'file-text',
category: 'documentation',
enabled: false, // Not yet implemented
config: {
baseUrl: {
type: 'url',
label: 'Confluence URL',
placeholder: 'https://your-org.atlassian.net/wiki',
description: 'Your Confluence instance URL',
},
spaceKey: {
type: 'text',
label: 'Space Key',
placeholder: 'DS',
description: 'Default Confluence space key',
},
},
getUrl() {
return localStorage.getItem('confluence_base_url') || null;
},
async checkStatus() {
return { status: 'not_implemented', message: 'Coming soon' };
},
},
};
/**
* Get all enabled components
*/
export function getEnabledComponents() {
return Object.values(componentRegistry).filter(c => c.enabled !== false);
}
/**
* Get components by category
*/
export function getComponentsByCategory(category) {
return Object.values(componentRegistry).filter(c => c.category === category && c.enabled !== false);
}
/**
* Get component by ID
*/
export function getComponent(id) {
return componentRegistry[id] || null;
}
/**
* Get component setting value
*/
export function getComponentSetting(componentId, settingKey) {
const storageKey = `dss_component_${componentId}_${settingKey}`;
const stored = localStorage.getItem(storageKey);
if (stored !== null) {
try {
return JSON.parse(stored);
} catch {
return stored;
}
}
// Return default value from schema
const component = getComponent(componentId);
if (component && component.config[settingKey]) {
const defaultValue = component.config[settingKey].default;
if (defaultValue !== undefined) {
return defaultValue;
}
}
return null;
}
/**
* Set component setting value
*/
export function setComponentSetting(componentId, settingKey, value) {
const storageKey = `dss_component_${componentId}_${settingKey}`;
localStorage.setItem(storageKey, JSON.stringify(value));
}
/**
* Get all settings for a component
*/
export function getComponentSettings(componentId) {
const component = getComponent(componentId);
if (!component) return {};
const settings = {};
for (const key of Object.keys(component.config)) {
settings[key] = getComponentSetting(componentId, key);
}
return settings;
}
export default {
componentRegistry,
getEnabledComponents,
getComponentsByCategory,
getComponent,
getComponentSetting,
setComponentSetting,
getComponentSettings,
};

Some files were not shown because too many files have changed in this diff Show More