Initial commit: Clean DSS implementation
Migrated from design-system-swarm with fresh git history.
Old project history preserved in /home/overbits/apps/design-system-swarm
Core components:
- MCP Server (Python FastAPI with mcp 1.23.1)
- Claude Plugin (agents, commands, skills, strategies, hooks, core)
- DSS Backend (dss-mvp1 - token translation, Figma sync)
- Admin UI (Node.js/React)
- Server (Node.js/Express)
- Storybook integration (dss-mvp1/.storybook)
Self-contained configuration:
- All paths relative or use DSS_BASE_PATH=/home/overbits/dss
- PYTHONPATH configured for dss-mvp1 and dss-claude-plugin
- .env file with all configuration
- Claude plugin uses ${CLAUDE_PLUGIN_ROOT} for portability
Migration completed: $(date)
🤖 Clean migration with full functionality preserved
This commit is contained in:
255
admin-ui/js/components/ds-input.js
Normal file
255
admin-ui/js/components/ds-input.js
Normal file
@@ -0,0 +1,255 @@
|
||||
/**
|
||||
* 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;
|
||||
Reference in New Issue
Block a user