lightview
Version:
A reactive UI library with features of Bau, Juris, and HTMX plus safe LLM UI generation
220 lines (184 loc) • 6.89 kB
JavaScript
/**
* Lightview Components - Rating
* A rating component using DaisyUI 5 styling
* @see https://daisyui.com/components/rating/
*/
import '../daisyui.js';
/**
* Rating Component
* @param {Object} props - Rating properties
* @param {number|Signal} props.value - Current rating value (controlled)
* @param {number} props.defaultValue - Default rating value (uncontrolled)
* @param {number} props.max - Maximum stars (default: 5)
* @param {string} props.size - 'xs' | 'sm' | 'md' | 'lg'
* @param {boolean} props.half - Allow half stars
* @param {string} props.color - Color for stars (default: 'orange-400')
* @param {string} props.mask - 'star' | 'star-2' | 'heart' | 'circle' | 'square' | 'diamond' (default: 'star-2')
* @param {boolean} props.hidden - Include hidden 0-star option for clearing
* @param {boolean} props.disabled - Disable rating
* @param {boolean} props.readOnly - Make read-only (just display)
* @param {string} props.label - Label text
* @param {string} props.helper - Helper text
* @param {Function} props.onChange - Change handler
* @param {boolean} props.useShadow - Render in Shadow DOM
*/
const Rating = (props = {}) => {
const { tags, signal } = globalThis.Lightview || {};
const LVX = globalThis.LightviewX || {};
if (!tags) {
console.error('Lightview not found');
return null;
}
const { div, input, fieldset, legend, p, span, shadowDOM } = tags;
const {
value,
defaultValue = 0,
max = 5,
size,
half = false,
color = 'orange-400',
mask = 'star-2',
hidden = false,
disabled = false,
readOnly = false,
label: labelText,
helper,
name = `rating-${Math.random().toString(36).slice(2, 9)}`,
onChange,
class: className = '',
useShadow,
...rest
} = props;
// Internal state
const internalValue = signal ? signal(defaultValue) : { value: defaultValue };
const isControlled = value !== undefined;
const getValue = () => {
if (isControlled) {
return typeof value === 'function' ? value() :
(value && typeof value.value !== 'undefined') ? value.value : value;
}
return internalValue.value;
};
const handleChange = (rating) => {
if (readOnly || disabled) return;
if (!isControlled) {
internalValue.value = rating;
}
if (isControlled && value && typeof value.value !== 'undefined') {
value.value = rating;
}
if (onChange) onChange(rating);
};
// Build rating classes
const getRatingClass = () => {
const classes = ['rating'];
if (size) classes.push(`rating-${size}`);
if (half) classes.push('rating-half');
return classes.join(' ');
};
const bgClass = `bg-${color}`;
// Build rating inputs
const buildInputs = () => {
const inputs = [];
const currentValue = getValue();
if (hidden) {
inputs.push(input({
type: 'radio',
name,
class: 'rating-hidden',
checked: () => getValue() === 0,
disabled,
onchange: () => handleChange(0)
}));
}
for (let i = 1; i <= max; i++) {
if (half) {
// Half stars
inputs.push(input({
type: 'radio',
name,
class: `mask mask-${mask} mask-half-1 ${bgClass}`,
'aria-label': `${i - 0.5} stars`,
checked: () => getValue() === i - 0.5,
disabled,
onchange: () => handleChange(i - 0.5)
}));
inputs.push(input({
type: 'radio',
name,
class: `mask mask-${mask} mask-half-2 ${bgClass}`,
'aria-label': `${i} stars`,
checked: () => getValue() === i,
disabled,
onchange: () => handleChange(i)
}));
} else {
inputs.push(input({
type: 'radio',
name,
class: `mask mask-${mask} ${bgClass}`,
'aria-label': `${i} stars`,
checked: () => getValue() === i,
disabled,
onchange: () => handleChange(i)
}));
}
}
return inputs;
};
const ratingEl = div({
class: getRatingClass(),
...rest
}, ...buildInputs());
// If no label and no helper, return just the rating
if (!labelText && !helper) {
let usesShadow = false;
if (LVX.shouldUseShadow) {
usesShadow = LVX.shouldUseShadow(useShadow);
} else {
usesShadow = useShadow === true;
}
if (usesShadow) {
const adoptedStyleSheets = LVX.getAdoptedStyleSheets ? LVX.getAdoptedStyleSheets() : [];
const themeValue = LVX.themeSignal ? () => LVX.themeSignal.value : 'light';
return div({ class: 'content', style: 'display: inline-block' },
shadowDOM({ mode: 'open', adoptedStyleSheets },
div({ 'data-theme': themeValue }, ratingEl)
)
);
}
return ratingEl;
}
// Build with fieldset pattern
const fieldsetContent = [];
if (labelText) {
fieldsetContent.push(legend({ class: 'fieldset-legend' }, labelText));
}
fieldsetContent.push(ratingEl);
if (helper) {
fieldsetContent.push(p({ class: 'label' }, helper));
}
const wrapperEl = fieldset({
class: `fieldset ${className}`.trim()
}, ...fieldsetContent);
// 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() : [];
const themeValue = 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' }, wrapperEl)
)
);
}
return wrapperEl;
};
// Auto-register
globalThis.Lightview.tags.Rating = Rating;
export default Rating;