Migrated from design-system-swarm with fresh git history.
Old project history preserved in /home/overbits/apps/design-system-swarm
Core components:
- MCP Server (Python FastAPI with mcp 1.23.1)
- Claude Plugin (agents, commands, skills, strategies, hooks, core)
- DSS Backend (dss-mvp1 - token translation, Figma sync)
- Admin UI (Node.js/React)
- Server (Node.js/Express)
- Storybook integration (dss-mvp1/.storybook)
Self-contained configuration:
- All paths relative or use DSS_BASE_PATH=/home/overbits/dss
- PYTHONPATH configured for dss-mvp1 and dss-claude-plugin
- .env file with all configuration
- Claude plugin uses ${CLAUDE_PLUGIN_ROOT} for portability
Migration completed: $(date)
🤖 Clean migration with full functionality preserved
256 lines
7.2 KiB
JavaScript
256 lines
7.2 KiB
JavaScript
/**
|
|
* 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;
|