feat(storybook): Add Web Component support to story generator
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
The Storybook generator now supports Web Components in addition to React: - Added ComponentType enum (REACT, WEB_COMPONENT) - Enhanced _parse_component to detect Web Components via HTMLElement/customElements - Added _parse_web_component to extract attributes from JSDoc and observedAttributes - Added _generate_csf3_web_component for Web Component story generation - Updated file patterns to include .js files - Moved ds-button.js to src/components/ for proper discovery - Fixed missing requests import in figma.py The generator now successfully generates Storybook stories for admin-ui's ds-button Web Component with all variants, sizes, and states. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -1,198 +0,0 @@
|
||||
/**
|
||||
* DS Button - Web Component
|
||||
*
|
||||
* Usage:
|
||||
* <ds-button variant="primary" size="default">Click me</ds-button>
|
||||
* <ds-button variant="outline" disabled>Disabled</ds-button>
|
||||
* <ds-button variant="ghost" size="icon"><svg>...</svg></ds-button>
|
||||
*
|
||||
* Attributes:
|
||||
* - variant: primary | secondary | outline | ghost | destructive | success | link
|
||||
* - size: sm | default | lg | icon | icon-sm | icon-lg
|
||||
* - disabled: boolean
|
||||
* - loading: boolean
|
||||
* - type: button | submit | reset
|
||||
*/
|
||||
|
||||
class DsButton extends HTMLElement {
|
||||
static get observedAttributes() {
|
||||
return ['variant', 'size', 'disabled', 'loading', 'type', 'tabindex', 'aria-label', 'aria-expanded', 'aria-pressed'];
|
||||
}
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
this.attachShadow({ mode: 'open' });
|
||||
}
|
||||
|
||||
connectedCallback() {
|
||||
this.render();
|
||||
this.setupEventListeners();
|
||||
}
|
||||
|
||||
disconnectedCallback() {
|
||||
this.cleanupEventListeners();
|
||||
}
|
||||
|
||||
attributeChangedCallback() {
|
||||
if (this.shadowRoot.innerHTML) {
|
||||
this.render();
|
||||
}
|
||||
}
|
||||
|
||||
get variant() {
|
||||
return this.getAttribute('variant') || 'primary';
|
||||
}
|
||||
|
||||
get size() {
|
||||
return this.getAttribute('size') || 'default';
|
||||
}
|
||||
|
||||
get disabled() {
|
||||
return this.hasAttribute('disabled');
|
||||
}
|
||||
|
||||
get loading() {
|
||||
return this.hasAttribute('loading');
|
||||
}
|
||||
|
||||
get type() {
|
||||
return this.getAttribute('type') || 'button';
|
||||
}
|
||||
|
||||
setupEventListeners() {
|
||||
const button = this.shadowRoot.querySelector('button');
|
||||
|
||||
// Store handler references for cleanup
|
||||
this.clickHandler = (e) => {
|
||||
if (this.disabled || this.loading) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
return;
|
||||
}
|
||||
this.dispatchEvent(new CustomEvent('ds-click', {
|
||||
bubbles: true,
|
||||
composed: true,
|
||||
detail: { originalEvent: e }
|
||||
}));
|
||||
};
|
||||
|
||||
this.keydownHandler = (e) => {
|
||||
// Enter or Space to activate button
|
||||
if ((e.key === 'Enter' || e.key === ' ') && !this.disabled && !this.loading) {
|
||||
e.preventDefault();
|
||||
button.click();
|
||||
}
|
||||
};
|
||||
|
||||
this.focusHandler = (e) => {
|
||||
// Delegate focus to internal button
|
||||
if (e.target === this && !this.disabled) {
|
||||
button.focus();
|
||||
}
|
||||
};
|
||||
|
||||
button.addEventListener('click', this.clickHandler);
|
||||
this.addEventListener('keydown', this.keydownHandler);
|
||||
this.addEventListener('focus', this.focusHandler);
|
||||
}
|
||||
|
||||
cleanupEventListeners() {
|
||||
const button = this.shadowRoot?.querySelector('button');
|
||||
if (button && this.clickHandler) {
|
||||
button.removeEventListener('click', this.clickHandler);
|
||||
delete this.clickHandler;
|
||||
}
|
||||
if (this.keydownHandler) {
|
||||
this.removeEventListener('keydown', this.keydownHandler);
|
||||
delete this.keydownHandler;
|
||||
}
|
||||
if (this.focusHandler) {
|
||||
this.removeEventListener('focus', this.focusHandler);
|
||||
delete this.focusHandler;
|
||||
}
|
||||
}
|
||||
|
||||
getVariantClass() {
|
||||
const variants = {
|
||||
primary: 'ds-btn--primary',
|
||||
secondary: 'ds-btn--secondary',
|
||||
outline: 'ds-btn--outline',
|
||||
ghost: 'ds-btn--ghost',
|
||||
destructive: 'ds-btn--destructive',
|
||||
success: 'ds-btn--success',
|
||||
link: 'ds-btn--link'
|
||||
};
|
||||
return variants[this.variant] || variants.primary;
|
||||
}
|
||||
|
||||
getSizeClass() {
|
||||
const sizes = {
|
||||
sm: 'ds-btn--sm',
|
||||
default: '',
|
||||
lg: 'ds-btn--lg',
|
||||
icon: 'ds-btn--icon',
|
||||
'icon-sm': 'ds-btn--icon-sm',
|
||||
'icon-lg': 'ds-btn--icon-lg'
|
||||
};
|
||||
return sizes[this.size] || '';
|
||||
}
|
||||
|
||||
render() {
|
||||
const variantClass = this.getVariantClass();
|
||||
const sizeClass = this.getSizeClass();
|
||||
const disabledAttr = this.disabled || this.loading ? 'disabled' : '';
|
||||
const tabindex = this.disabled ? '-1' : (this.getAttribute('tabindex') || '0');
|
||||
|
||||
// ARIA attributes delegation
|
||||
const ariaLabel = this.getAttribute('aria-label') ? `aria-label="${this.getAttribute('aria-label')}"` : '';
|
||||
const ariaExpanded = this.getAttribute('aria-expanded') ? `aria-expanded="${this.getAttribute('aria-expanded')}"` : '';
|
||||
const ariaPressed = this.getAttribute('aria-pressed') ? `aria-pressed="${this.getAttribute('aria-pressed')}"` : '';
|
||||
const ariaAttrs = `${ariaLabel} ${ariaExpanded} ${ariaPressed}`.trim();
|
||||
|
||||
this.shadowRoot.innerHTML = `
|
||||
<style>
|
||||
|
||||
:host {
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
button {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
button:focus-visible {
|
||||
outline: 2px solid var(--primary);
|
||||
outline-offset: 2px;
|
||||
}
|
||||
|
||||
.loading-spinner {
|
||||
display: inline-block;
|
||||
width: 1rem;
|
||||
height: 1rem;
|
||||
border: 2px solid currentColor;
|
||||
border-top-color: transparent;
|
||||
border-radius: 50%;
|
||||
animation: spin 0.75s linear infinite;
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
to { transform: rotate(360deg); }
|
||||
}
|
||||
</style>
|
||||
<button
|
||||
class="ds-btn ${variantClass} ${sizeClass}"
|
||||
type="${this.type}"
|
||||
tabindex="${tabindex}"
|
||||
${disabledAttr}
|
||||
${ariaAttrs}
|
||||
>
|
||||
${this.loading ? '<span class="loading-spinner"></span>' : ''}
|
||||
<slot></slot>
|
||||
</button>
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
customElements.define('ds-button', DsButton);
|
||||
|
||||
export default DsButton;
|
||||
Reference in New Issue
Block a user