{"version":3,"file":"rating.cjs","names":[],"sources":["../src/inputs/rating/rating.ts"],"sourcesContent":["import { bind, createStableId, define, getHost, html, prop, useField } from '@vielzeug/ore';\nimport { computed, signal } from '@vielzeug/ripple';\nimport { createErrorHelperState, createSliderControl } from '../../core';\nimport type { ComponentSize, ThemeColor } from '../../types';\nimport '../../content/icon/icon';\nimport { disablableBundle, sizableBundle, themableBundle } from '../../shared';\nimport { coarsePointerMixin, colorThemeMixin, reducedMotionMixin, sizeVariantMixin } from '../../styles';\nimport { defineFieldValue, dispatchNativeFieldEvent, setFieldValue } from '../shared/native-field-event';\nimport { renderHelperRegion } from '../shared/templates';\nimport componentStyles from './rating.css?inline';\n\nexport type OreRatingEvents = {\n  change: Event;\n  input: Event;\n};\n\n/** Rating props */\nexport type OreRatingProps = {\n  /** Theme color */\n  color?: ThemeColor;\n  /** Disable interaction */\n  disabled?: boolean;\n  /** Error message — marks the field as invalid (fallback when the `error` slot is empty) */\n  error?: string;\n  /** Helper text displayed below the stars (fallback when the `helper` slot is empty) */\n  helper?: string;\n  /** Accessible group label */\n  label?: string;\n  /** Maximum rating (number of stars) */\n  max?: number;\n  /** Form field name */\n  name?: string;\n  /** Make rating read-only */\n  readonly?: boolean;\n  /** Component size */\n  size?: ComponentSize;\n  /** Render selected stars as solid-filled instead of outline-only */\n  solid?: boolean;\n  /** Current rating value */\n  value?: number;\n};\n\n/**\n * A star rating input.\n *\n * @element ore-rating\n *\n * @attr {number} value - Current rating value (default: 0)\n * @attr {number} max - Maximum number of stars (default: 5)\n * @attr {boolean} readonly - Read-only display mode\n * @attr {boolean} disabled - Disabled state\n * @attr {string} label - aria-label for the group (default: 'Rating')\n * @attr {string} color - Theme color for filled stars: 'primary' | 'secondary' | 'info' | 'success' | 'warning' | 'error'\n * @attr {string} size - 'sm' | 'md' | 'lg'\n * @attr {string} name - Form field name\n * @attr {boolean} solid - Fill selected stars (outline remains default when omitted)\n * @attr {string} helper - Helper text below the stars\n * @attr {string} error - Error message below the stars\n *\n * @fires input - Emitted when the rating changes.\n * @fires change - Emitted when the rating changes.\n *\n * @cssprop --rating-star-size - Star diameter\n * @cssprop --rating-color-empty - Empty star color\n * @cssprop --rating-color-filled - Filled star color\n * @cssprop --rating-gap - Gap between stars\n *\n * @part stars - Stars container.\n * @part star - Star item element.\n * @part helper-text - The helper/error text element.\n * @example\n * ```html\n * <ore-rating value=\"3\" max=\"5\" color=\"warning\"></ore-rating>\n * <ore-rating value=\"4\" solid></ore-rating>\n * ```\n */\nexport const RATING_TAG = 'ore-rating' as const;\ndefine<OreRatingProps>(RATING_TAG, {\n  formAssociated: true,\n  props: {\n    ...themableBundle,\n    ...sizableBundle,\n    ...disablableBundle,\n    error: prop.string(),\n    helper: prop.string(),\n    label: prop.string('Rating'),\n    max: prop.number(5),\n    name: prop.string(),\n    readonly: prop.bool(false),\n    solid: prop.bool(false),\n    value: prop.number(0),\n  },\n  setup(props) {\n    const el = getHost();\n    const normalizedValue = computed(() => {\n      const max = Math.max(1, Number(props.max?.value) || 5);\n      const raw = Number(props.value?.value);\n      const safe = Number.isFinite(raw) ? raw : 0;\n\n      return Math.min(max, Math.max(0, safe));\n    });\n\n    const isDisabled = computed(() => Boolean(props.disabled.value));\n\n    defineFieldValue(\n      el,\n      () => String(normalizedValue.value),\n      (value) => {\n        el.setAttribute('value', value);\n      },\n    );\n\n    useField({\n      disabled: isDisabled,\n      value: computed(() => String(normalizedValue.value || 0)),\n    });\n\n    const assistiveId = createStableId('helper');\n    const assistive = createErrorHelperState({ error: props.error, helper: props.helper });\n    const errorText = computed(() => assistive.value.errorText);\n    const helperText = computed(() => assistive.value.helperText);\n    const ariaDescribedBy = computed(() => (errorText.value || helperText.value ? assistiveId : null));\n\n    const isInteractive = computed(() => !props.readonly?.value && !isDisabled.value);\n    const hovered = signal<number | null>(null);\n    const displayValue = computed(() => hovered.value ?? normalizedValue.value);\n    const getStarButtons = () => {\n      return [...(el.shadowRoot?.querySelectorAll<HTMLButtonElement>('[data-star]') ?? [])];\n    };\n    const ratingControl = createSliderControl({\n      max: computed(() => Number(props.max?.value) || 5),\n      min: signal(1),\n      step: signal(1),\n    });\n\n    function spawnSparkles(star: number) {\n      const layer = el.shadowRoot?.querySelector<HTMLElement>('.sparkle-layer');\n      const btn = el.shadowRoot?.querySelector<HTMLElement>(`[data-star=\"${star}\"]`);\n\n      if (!layer || !btn) return;\n\n      const cx = btn.offsetLeft + btn.offsetWidth / 2;\n      const cy = btn.offsetTop + btn.offsetHeight / 2;\n      const count = 10;\n\n      for (let i = 0; i < count; i++) {\n        const p = document.createElement('span');\n        const angle = (360 / count) * i + (Math.random() * 30 - 15);\n        const dist = 18 + Math.random() * 20;\n        const size = 3 + Math.random() * 4;\n        const dur = 380 + Math.random() * 220;\n\n        p.className = 'sparkle';\n        p.style.cssText = [\n          `left:${cx}px`,\n          `top:${cy}px`,\n          `--_angle:${angle}deg`,\n          `--_dist:${dist}px`,\n          `width:${size}px`,\n          `height:${size}px`,\n          `--_dur:${dur}ms`,\n          `animation-delay:${Math.random() * 60}ms`,\n        ].join(';');\n        layer.appendChild(p);\n        p.addEventListener('animationend', () => p.remove(), { once: true });\n      }\n    }\n    function select(star: number, _originalEvent?: Event) {\n      if (!isInteractive.value) return;\n\n      const max = Math.max(1, Number(props.max?.value) || 5);\n      const nextValue = Math.min(max, Math.max(0, star));\n\n      if (nextValue === normalizedValue.value) return;\n\n      setFieldValue(el, String(nextValue));\n      dispatchNativeFieldEvent(el, 'input');\n      dispatchNativeFieldEvent(el, 'change');\n      spawnSparkles(nextValue);\n    }\n    function handleKeydown(e: KeyboardEvent, star: number) {\n      const next = ratingControl.nextFromKey(e.key, star);\n\n      if (next == null) return;\n\n      e.preventDefault();\n      select(next, e);\n\n      const buttons = getStarButtons();\n\n      buttons[next - 1]?.focus();\n    }\n\n    const stars = computed(() => {\n      const max = Number(props.max?.value) || 5;\n\n      return Array.from({ length: max }, (_, i) => i + 1);\n    });\n\n    bind({ attr: { size: props.size } });\n\n    return html`\n      <div\n        class=\"stars\"\n        part=\"stars\"\n        role=\"radiogroup\"\n        aria-label=\"${props.label}\"\n        aria-describedby=\"${ariaDescribedBy}\">\n        ${() =>\n          stars.value.map(\n            (star) => html`\n              <button\n                class=\"star-btn\"\n                part=\"star\"\n                type=\"button\"\n                role=\"radio\"\n                aria-label=\"${() => `${star} ${star === 1 ? 'star' : 'stars'}`}\"\n                aria-checked=\"${() => String(star === normalizedValue.value)}\"\n                data-star=\"${star}\"\n                ?data-filled=\"${() => star <= displayValue.value}\"\n                disabled=\"${() => (!isInteractive.value ? true : null)}\"\n                @click=\"${(e: Event) => select(star, e)}\"\n                @pointerenter=\"${() => {\n                  if (isInteractive.value) hovered.value = star;\n                }}\"\n                @pointerleave=\"${() => {\n                  hovered.value = null;\n                }}\"\n                @keydown=\"${(e: KeyboardEvent) => handleKeydown(e, star)}\">\n                <ore-icon name=\"star\" size=\"var(--_star-size)\" stroke-width=\"1.5\" aria-hidden=\"true\"></ore-icon>\n              </button>\n            `,\n          )}\n        <div class=\"sparkle-layer\"></div>\n      </div>\n      ${renderHelperRegion(assistiveId, errorText, helperText)}\n    `;\n  },\n  styles: [colorThemeMixin, sizeVariantMixin({}), coarsePointerMixin, reducedMotionMixin, componentStyles],\n});\n"],"mappings":"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"}