feat: Add DSS infrastructure, remove legacy admin-ui code
Some checks failed
DSS Project Analysis / dss-context-update (push) Has been cancelled
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:
24828
.dss/components/figma-registry.json
Normal file
24828
.dss/components/figma-registry.json
Normal file
File diff suppressed because it is too large
Load Diff
837
.dss/components/shadcn-registry.json
Normal file
837
.dss/components/shadcn-registry.json
Normal 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
20
.dss/config/figma.json
Normal 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
29
.dss/core-hashes.sha256
Normal 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
503
.dss/core/primitives.json
Normal 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" }
|
||||||
|
}
|
||||||
|
}
|
||||||
1
.dss/data/_system/analysis-admin-ui.json
Normal file
1
.dss/data/_system/analysis-admin-ui.json
Normal 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"}
|
||||||
1
.dss/data/_system/analysis-storybook.json
Normal file
1
.dss/data/_system/analysis-storybook.json
Normal file
@@ -0,0 +1 @@
|
|||||||
|
{"target":"storybook","analyzed_at":"2025-12-10T21:38:46-03:00","stats":{"stories":14,"mdx":0},"status":"analyzed"}
|
||||||
373
.dss/data/_system/components.json
Normal file
373
.dss/data/_system/components.json
Normal 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": []
|
||||||
|
}
|
||||||
|
]
|
||||||
33
.dss/data/_system/ds.config.json
Normal file
33
.dss/data/_system/ds.config.json
Normal 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)"
|
||||||
|
}
|
||||||
29301
.dss/data/_system/figma-components.json
Normal file
29301
.dss/data/_system/figma-components.json
Normal file
File diff suppressed because it is too large
Load Diff
20
.dss/data/_system/style-dictionary.config.json
Normal file
20
.dss/data/_system/style-dictionary.config.json
Normal 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"}]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
408
.dss/data/_system/styles.json
Normal file
408
.dss/data/_system/styles.json
Normal 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": []
|
||||||
|
}
|
||||||
|
}
|
||||||
99
.dss/data/_system/themes/_tokens.scss
Normal file
99
.dss/data/_system/themes/_tokens.scss
Normal 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;
|
||||||
102
.dss/data/_system/themes/tokens.css
Normal file
102
.dss/data/_system/themes/tokens.css
Normal 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;
|
||||||
|
}
|
||||||
98
.dss/data/_system/themes/tokens.json
Normal file
98
.dss/data/_system/themes/tokens.json
Normal 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"
|
||||||
|
}
|
||||||
9
.dss/data/_system/tokens/base.json
Normal file
9
.dss/data/_system/tokens/base.json
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
{
|
||||||
|
"_meta": {
|
||||||
|
"version": "1.0.0",
|
||||||
|
"generated": null,
|
||||||
|
"source": "awaiting Figma sync",
|
||||||
|
"status": "empty"
|
||||||
|
},
|
||||||
|
"tokens": {}
|
||||||
|
}
|
||||||
11
.dss/data/_system/tokens/resolved-meta.json
Normal file
11
.dss/data/_system/tokens/resolved-meta.json
Normal 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"
|
||||||
|
]
|
||||||
|
}
|
||||||
320
.dss/data/_system/tokens/tokens.json
Normal file
320
.dss/data/_system/tokens/tokens.json
Normal 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"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
87
.dss/schema/skin-contract.json
Normal file
87
.dss/schema/skin-contract.json
Normal 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"
|
||||||
|
]
|
||||||
|
}
|
||||||
306
.dss/skins/figma-source/tokens.json
Normal file
306
.dss/skins/figma-source/tokens.json
Normal 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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
132
.dss/skins/shadcn/tokens.json
Normal file
132
.dss/skins/shadcn/tokens.json
Normal 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
18
.dss/themes/default.json
Normal 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" }
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -4,12 +4,22 @@
|
|||||||
|
|
||||||
echo "🛡️ DSS Immutability Check..."
|
echo "🛡️ DSS Immutability Check..."
|
||||||
|
|
||||||
# List of protected files (core principles only)
|
# List of protected files (core principles and config)
|
||||||
PROTECTED_FILES=(
|
PROTECTED_FILES=(
|
||||||
".knowledge/dss-principles.json"
|
".knowledge/dss-principles.json"
|
||||||
".knowledge/dss-architecture.json"
|
".knowledge/dss-architecture.json"
|
||||||
".clauderc"
|
".clauderc"
|
||||||
"PROJECT_CONFIG.md"
|
"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
|
# 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."
|
echo "✅ ALLOW_CORE_CHANGES=true detected. Proceeding with commit."
|
||||||
fi
|
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 "✅ Immutability check passed."
|
||||||
echo ""
|
echo ""
|
||||||
|
|
||||||
@@ -59,6 +139,24 @@ else
|
|||||||
echo "⚠️ Warning: scripts/verify-quality.sh not found, skipping quality checks"
|
echo "⚠️ Warning: scripts/verify-quality.sh not found, skipping quality checks"
|
||||||
fi
|
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 ""
|
||||||
echo "✅ All pre-commit checks passed!"
|
echo "✅ All pre-commit checks passed!"
|
||||||
exit 0
|
exit 0
|
||||||
|
|||||||
364
.githooks/pre-commit-python
Executable file
364
.githooks/pre-commit-python
Executable 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())
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"$schema": "dss-core-v1",
|
"$schema": "dss-core-v1",
|
||||||
"version": "1.1.0",
|
"version": "1.3.0",
|
||||||
"last_updated": "2025-12-10",
|
"last_updated": "2025-12-10",
|
||||||
"purpose": "Single source of truth for AI agents working with DSS",
|
"purpose": "Single source of truth for AI agents working with DSS",
|
||||||
|
|
||||||
@@ -24,7 +24,7 @@
|
|||||||
|
|
||||||
"canonical_structure": {
|
"canonical_structure": {
|
||||||
"tokens": ["colors", "spacing", "typography", "borders", "shadows", "motion"],
|
"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"],
|
"patterns": ["forms", "navigation", "layouts"],
|
||||||
"rule": "This structure NEVER changes. All inputs normalize to this."
|
"rule": "This structure NEVER changes. All inputs normalize to this."
|
||||||
},
|
},
|
||||||
@@ -87,6 +87,57 @@
|
|||||||
"context_compiler": ["dss_get_resolved_context", "dss_get_compiler_status"]
|
"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": {
|
"context_compiler": {
|
||||||
"cascade": "Base Skin -> Extended Skin -> Project Overrides = Final Context",
|
"cascade": "Base Skin -> Extended Skin -> Project Overrides = Final Context",
|
||||||
"caching": "mtime-based invalidation",
|
"caching": "mtime-based invalidation",
|
||||||
@@ -114,7 +165,16 @@
|
|||||||
"skills": "dss-claude-plugin/skills/",
|
"skills": "dss-claude-plugin/skills/",
|
||||||
"commands": "dss-claude-plugin/commands/",
|
"commands": "dss-claude-plugin/commands/",
|
||||||
"logs": ".dss/logs/",
|
"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": {
|
"coding_rules_summary": {
|
||||||
@@ -222,7 +282,27 @@
|
|||||||
"cli": "dss ingest --source figma --file tokens.json"
|
"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": [
|
"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.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"}
|
{"version": "1.0.0", "date": "2025-12-10", "notes": "Initial core definition"}
|
||||||
]
|
]
|
||||||
|
|||||||
236
.knowledge/FIGMA_SYNC_ARCHITECTURE.md
Normal file
236
.knowledge/FIGMA_SYNC_ARCHITECTURE.md
Normal 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
|
||||||
14
.mcp.json
14
.mcp.json
@@ -2,14 +2,14 @@
|
|||||||
"$schema": "https://raw.githubusercontent.com/anthropics/claude-code/main/schemas/mcp-servers.schema.json",
|
"$schema": "https://raw.githubusercontent.com/anthropics/claude-code/main/schemas/mcp-servers.schema.json",
|
||||||
"mcpServers": {
|
"mcpServers": {
|
||||||
"dss": {
|
"dss": {
|
||||||
"command": "${workspaceFolder}/venv/bin/python3",
|
"command": "/home/overbits/dss/.venv/bin/python3",
|
||||||
"args": ["${workspaceFolder}/dss-claude-plugin/servers/dss-mcp-server.py"],
|
"args": ["/home/overbits/dss/dss-claude-plugin/servers/dss-mcp-server.py"],
|
||||||
"env": {
|
"env": {
|
||||||
"PYTHONPATH": "${workspaceFolder}:${workspaceFolder}/dss-claude-plugin",
|
"PYTHONPATH": "/home/overbits/dss:/home/overbits/dss/dss-claude-plugin",
|
||||||
"DSS_HOME": "${workspaceFolder}/.dss",
|
"DSS_HOME": "/home/overbits/dss/.dss",
|
||||||
"DSS_DATABASE": "${workspaceFolder}/.dss/dss.db",
|
"DSS_DATABASE": "/home/overbits/dss/.dss/dss.db",
|
||||||
"DSS_CACHE": "${workspaceFolder}/.dss/cache",
|
"DSS_CACHE": "/home/overbits/dss/.dss/cache",
|
||||||
"DSS_BASE_PATH": "${workspaceFolder}"
|
"DSS_BASE_PATH": "/home/overbits/dss"
|
||||||
},
|
},
|
||||||
"description": "Design System Server MCP - local development"
|
"description": "Design System Server MCP - local development"
|
||||||
}
|
}
|
||||||
|
|||||||
3
admin-ui/.gitignore
vendored
3
admin-ui/.gitignore
vendored
@@ -32,3 +32,6 @@ __coverage__
|
|||||||
# Logs
|
# Logs
|
||||||
logs
|
logs
|
||||||
*.log
|
*.log
|
||||||
|
|
||||||
|
*storybook.log
|
||||||
|
storybook-static
|
||||||
|
|||||||
25
admin-ui/.storybook/main.js
Normal file
25
admin-ui/.storybook/main.js
Normal 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;
|
||||||
23
admin-ui/.storybook/preview.js
Normal file
23
admin-ui/.storybook/preview.js
Normal 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;
|
||||||
@@ -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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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 */
|
|
||||||
@@ -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;
|
|
||||||
}
|
|
||||||
@@ -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%);
|
|
||||||
}
|
|
||||||
@@ -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);
|
|
||||||
@@ -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);
|
|
||||||
@@ -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);
|
|
||||||
@@ -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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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);
|
|
||||||
@@ -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;
|
|
||||||
@@ -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 };
|
|
||||||
@@ -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;
|
|
||||||
}
|
|
||||||
@@ -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;
|
|
||||||
@@ -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);
|
|
||||||
@@ -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);
|
|
||||||
@@ -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);
|
|
||||||
@@ -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);
|
|
||||||
@@ -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(', '));
|
|
||||||
@@ -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);
|
|
||||||
@@ -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);
|
|
||||||
@@ -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);
|
|
||||||
@@ -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;
|
|
||||||
@@ -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);
|
|
||||||
@@ -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);
|
|
||||||
@@ -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);
|
|
||||||
@@ -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);
|
|
||||||
@@ -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);
|
|
||||||
@@ -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);
|
|
||||||
@@ -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);
|
|
||||||
@@ -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);
|
|
||||||
@@ -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;
|
|
||||||
@@ -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;
|
|
||||||
@@ -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;
|
|
||||||
@@ -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;
|
|
||||||
@@ -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;
|
|
||||||
@@ -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;
|
|
||||||
@@ -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;
|
|
||||||
@@ -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);
|
|
||||||
@@ -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;
|
|
||||||
@@ -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;
|
|
||||||
@@ -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;
|
|
||||||
@@ -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;
|
|
||||||
@@ -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;
|
|
||||||
@@ -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;
|
|
||||||
@@ -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;
|
|
||||||
@@ -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;
|
|
||||||
@@ -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);
|
|
||||||
@@ -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;
|
|
||||||
@@ -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);
|
|
||||||
@@ -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;
|
|
||||||
@@ -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;
|
|
||||||
@@ -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;
|
|
||||||
@@ -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;
|
|
||||||
@@ -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;
|
|
||||||
@@ -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;
|
|
||||||
@@ -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;
|
|
||||||
@@ -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;
|
|
||||||
@@ -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)
|
|
||||||
};
|
|
||||||
}
|
|
||||||
@@ -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];
|
|
||||||
}
|
|
||||||
@@ -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();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -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();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -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
@@ -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
@@ -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;
|
|
||||||
@@ -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;
|
|
||||||
@@ -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;
|
|
||||||
@@ -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
Reference in New Issue
Block a user