import type { MaybeRefOrGetter } from 'vue' import { computed, toValue } from 'vue' /** The 8 named directions, mirroring the `.to-*` utilities in gradients.css. */ export type GradientDir = 'to-t' | 'to-b' | 'to-l' | 'to-r' | 'to-tl' | 'to-tr' | 'to-bl' | 'to-br' /** `gradient` prop: boolean (auto from `color`) or a space-separated tone list, * e.g. "purple" (2-stop with `color`) or "blue purple pink" (full control). */ export type GradientProp = boolean | string /** `gradient-dir` prop: a named direction, or an angle in degrees (number or * numeric string). Undefined → CSS default (135deg). */ export type GradientDirProp = GradientDir | number | string const DIR_MAP: Record = { 'to-t': 'to top', 'to-b': 'to bottom', 'to-l': 'to left', 'to-r': 'to right', 'to-tl': 'to top left', 'to-tr': 'to top right', 'to-bl': 'to bottom left', 'to-br': 'to bottom right', } /** Resolve a gradient-dir value to a CSS angle/keyword, or undefined for default. */ function resolveAngle(dir: GradientDirProp | undefined): string | undefined { if (dir == null || dir === '') { return undefined } if (typeof dir === 'string' && dir in DIR_MAP) { return DIR_MAP[dir as GradientDir] } const n = Number(dir) return Number.isFinite(n) ? `${n}deg` : undefined } /** Split a tone string ("blue purple pink") into individual tone tokens. */ function parseTones(value: GradientProp | undefined): string[] { if (typeof value !== 'string') { return [] } return value.trim().split(/\s+/).filter(Boolean) } /** * Resolve a single stop token to a CSS color value. Accepts, in order: * - a `var(...)` expression → used as-is * - a raw custom property `--bgl-x` → wrapped in `var(--bgl-x)` * - an explicit color (#hex, rgb(), hsl(), color-mix(), named like "white") * → used as-is * - a theme tone like "blue" / "primary-30" → wrapped in `var(--bgl-blue)` */ function resolveStop(token: string): string { const t = token.trim() if (t.startsWith('var(') || t.startsWith('#') || /^(rgb|hsl|color-mix|linear-gradient)\(/.test(t)) { return t } if (t.startsWith('--')) { return `var(${t})` } return `var(--bgl-${t})` } interface UseGradientVariantArgs { /** The `gradient` prop value. */ gradient: MaybeRefOrGetter /** The `gradient-dir` prop value. */ gradientDir?: MaybeRefOrGetter /** The component's `color` prop — used as the first stop when present. */ color?: MaybeRefOrGetter } /** * Shared logic for the `gradient` variant across Btn / Badge (and anything else). * * Stop resolution: * - `color` (if set) is the first stop, followed by tones from `gradient`. * - With no `color`, all stops come from `gradient`. * - A single resolved stop (boolean `gradient`, or one tone) lets the CSS * `.gradient` modifier auto-derive a darker second stop — so we inject * nothing and rely on its `--bgl-grad-default-*` fallback. * * Returns the inline CSS custom properties to bind via `:style`. Named direction * / via / extra stops authored as gradients.css classes still compose on top. */ export function useGradientVariant({ gradient, gradientDir, color }: UseGradientVariantArgs) { /** Whether the gradient variant is active at all. */ const isGradient = computed(() => { const g = toValue(gradient) return g === true || (typeof g === 'string' && g.trim() !== '') }) const stops = computed(() => { const c = toValue(color) const tones = parseTones(toValue(gradient)) return [c, ...tones].filter(Boolean) as string[] }) const gradientStyle = computed>(() => { if (!isGradient.value) { return {} } const style: Record = {} const angle = resolveAngle(toValue(gradientDir)) if (angle) { style['--bgl-grad-angle'] = angle } const s = stops.value // 0–1 stops → let the CSS auto-derive a darker second stop. Inject nothing. if (s.length < 2) { return style } style['--bgl-grad-from'] = resolveStop(s[0]) style['--bgl-grad-to'] = resolveStop(s[s.length - 1]) // Middle stops become the `via` slot (comma-terminated, like gradients.css). const middle = s.slice(1, -1) if (middle.length) { style['--bgl-grad-via'] = `${middle.map(resolveStop).join(', ')}, ` } return style }) return { isGradient, gradientStyle, stops } }