lightview
Version:
A reactive UI library with features of Bau, Juris, and HTMX plus safe LLM UI generation
222 lines (188 loc) • 7.28 kB
JavaScript
/**
* Lightview Components - Toggle
* A toggle switch component using DaisyUI 5 styling
* @see https://daisyui.com/components/toggle/
*
* Uses DaisyUI's form-control pattern:
* <div class="form-control">
* <label class="label cursor-pointer">
* <span class="label-text">Label</span>
* <input type="checkbox" class="toggle" />
* </label>
* </div>
*/
import '../daisyui.js';
/**
* Toggle Component
* @param {Object} props - Toggle properties
* @param {boolean|Signal} props.checked - Controlled checked state
* @param {boolean} props.defaultChecked - Initial checked state (uncontrolled)
* @param {string} props.size - 'xs' | 'sm' | 'md' | 'lg' (default: 'md')
* @param {string} props.color - 'primary' | 'secondary' | 'accent' | 'info' | 'success' | 'warning' | 'error'
* @param {boolean} props.disabled - Disable toggle
* @param {string} props.label - Label text
* @param {string} props.labelPosition - 'left' | 'right' (default: 'left')
* @param {string} props.description - Description text below label
* @param {Function} props.onChange - Change handler
* @param {boolean} props.useShadow - Render in Shadow DOM
*/
const Toggle = (props = {}) => {
const { tags, signal } = globalThis.Lightview || {};
const LVX = globalThis.LightviewX || {};
if (!tags) {
console.error('Lightview not found');
return null;
}
const { div, input, label, span, shadowDOM } = tags;
const {
checked,
defaultChecked = false,
size = 'md',
color,
disabled = false,
label: labelText,
labelPosition = 'left',
description,
onChange,
name,
id,
class: className = '',
useShadow,
theme, // Explicit theme override
...rest
} = props;
const toggleId = id || `toggle-${Math.random().toString(36).slice(2, 9)}`;
// Internal state for uncontrolled mode
const internalChecked = signal ? signal(defaultChecked) : { value: defaultChecked };
const isControlled = checked !== undefined;
const getChecked = () => {
if (isControlled) {
return typeof checked === 'function' ? checked() :
(checked && typeof checked.value !== 'undefined') ? checked.value : checked;
}
return internalChecked.value;
};
const handleChange = (e) => {
const newValue = e.target.checked;
if (!isControlled) {
internalChecked.value = newValue;
}
// If controlled with a signal, update it
if (isControlled && checked && typeof checked.value !== 'undefined') {
checked.value = newValue;
}
if (onChange) onChange(newValue, e);
};
// Build DaisyUI toggle classes
const getToggleClass = () => {
const classes = ['toggle'];
if (size && size !== 'md') {
classes.push(`toggle-${size}`);
}
if (color) {
classes.push(`toggle-${color}`);
}
return classes.join(' ');
};
const toggleInput = input({
type: 'checkbox',
class: getToggleClass(),
checked: isControlled
? (typeof checked === 'function' ? checked : () => getChecked())
: () => internalChecked.value,
disabled: typeof disabled === 'function' ? disabled : disabled,
name,
id: toggleId,
onchange: handleChange,
role: 'switch',
'aria-checked': isControlled
? (typeof checked === 'function' ? checked : () => getChecked())
: () => internalChecked.value,
...rest
});
// If no label, return just the toggle
if (!labelText) {
// Check if we should use shadow DOM
let usesShadow = false;
if (LVX.shouldUseShadow) {
usesShadow = LVX.shouldUseShadow(useShadow);
} else {
usesShadow = useShadow === true;
}
if (usesShadow) {
const adoptedStyleSheets = LVX.getAdoptedStyleSheets ? LVX.getAdoptedStyleSheets() : [];
// Use reactive theme signal if available, otherwise fallback to explicit 'theme' prop or default
const themeValue = theme || (LVX.themeSignal ? () => LVX.themeSignal.value : 'light');
return div({ class: 'content', style: 'display: inline-block' },
shadowDOM({ mode: 'open', adoptedStyleSheets },
div({ 'data-theme': themeValue }, toggleInput)
)
);
}
return toggleInput;
}
// Build label content
const labelContent = div({ style: 'display: flex; flex-direction: column;' },
span({ class: 'label-text' }, labelText),
description ? span({ class: 'label-text-alt', style: 'opacity: 0.7;' }, description) : null
);
// Arrange based on label position
const labelChildren = labelPosition === 'right'
? [toggleInput, labelContent]
: [labelContent, toggleInput];
const formControl = div({
class: `form-control ${className}`.trim()
},
label({ class: 'label cursor-pointer', style: 'justify-content: flex-start; gap: 0.75rem;' },
...labelChildren
)
);
// Check if we should use shadow DOM
let usesShadow = false;
if (LVX.shouldUseShadow) {
usesShadow = LVX.shouldUseShadow(useShadow);
} else {
usesShadow = useShadow === true;
}
if (usesShadow) {
const adoptedStyleSheets = LVX.getAdoptedStyleSheets ? LVX.getAdoptedStyleSheets() : [];
// Use reactive theme signal if available, otherwise fallback to explicit 'theme' prop or default
const themeValue = theme || (LVX.themeSignal ? () => LVX.themeSignal.value : 'light');
return span({ style: 'margin-right: 0.5rem' },
shadowDOM({ mode: 'open', adoptedStyleSheets },
div({ 'data-theme': themeValue, style: 'display: inline-block' }, formControl)
)
);
}
return formControl;
};
// Auto-register
globalThis.Lightview.tags.Toggle = Toggle;
// Register as Custom Element
if (globalThis.LightviewX?.customElementWrapper) {
const ToggleElement = globalThis.LightviewX.customElementWrapper(Toggle, {
attributeMap: {
checked: Boolean,
defaultChecked: Boolean,
size: String,
color: String,
disabled: Boolean,
label: String,
labelPosition: String,
description: String,
name: String,
id: String,
class: String,
theme: String
}
});
if (!customElements.get('lv-toggle')) {
customElements.define('lv-toggle', ToggleElement);
}
} else if (globalThis.LightviewX?.createCustomElement) {
const ToggleElement = globalThis.LightviewX.createCustomElement(Toggle);
if (!customElements.get('lv-toggle')) {
customElements.define('lv-toggle', ToggleElement);
}
}
export default Toggle;