<script lang="ts">
/**
 * element_fit_text.svelte
 *
 * Wrapper component for the fit_text Svelte action. Renders a div that
 * auto-scales its font size to fill the available space (binary search).
 *
 * CRITICAL — height must be constrained for auto-scaling to work:
 *   The action checks scrollHeight <= offsetHeight to detect overflow.
 *   If the wrapper div has no explicit height it expands to fit content,
 *   making scrollHeight == offsetHeight always — so the binary search
 *   returns max font immediately (appears broken / no scaling).
 *
 *   Set height via the `height` prop (CSS string, e.g. "1.5in", "3rem", "80px"),
 *   OR apply a Tailwind height class via the `class` prop (e.g. "h-[1.5in]"),
 *   OR let a parent flex container with a defined size control the height
 *   and pass class="h-full" to fill it.
 *
 * Props:
 *   min          — minimum font size in px (default: 16)
 *   max          — maximum font size in px (default: 80)
 *   manual_size  — when set, caps the binary search at this size (overflow-safe preferred max)
 *   disabled     — when true, neither auto-scaling nor manual_size is applied
 *   height       — explicit CSS height for the wrapper div (e.g. "1.5in", "3rem")
 *   width        — explicit CSS width for the wrapper div (rarely needed; usually inherits)
 *   class        — extra Tailwind/CSS classes for the wrapper div
 *   style        — extra inline styles for the wrapper div
 *
 * Example — auto-scale to fill an explicitly sized region:
 *   <Element_fit_text min={20} max={80} height="1.5in" class="leading-none">
 *     {attendee_name}
 *   </Element_fit_text>
 *
 * Example — auto-scale filling a flex parent (parent must have a defined height):
 *   <!-- parent: flex-1 min-h-0 overflow-hidden -->
 *   <Element_fit_text min={20} max={80} class="h-full leading-none">
 *     {attendee_name}
 *   </Element_fit_text>
 *
 * Example — manual cap (prefers this size, scales down if it doesn't fit):
 *   <Element_fit_text min={20} max={80} manual_size={font_size_name}>
 *     {attendee_name}
 *   </Element_fit_text>
 */

import { fit_text } from './action_fit_text';
import type { Snippet } from 'svelte';

interface Props {
    min?: number;
    max?: number;
    /**
     * When set, caps the binary search at this size (overflow-safe preferred max).
     * If the text fits at manual_size → renders at exactly manual_size.
     * If manual_size is too large for the container → scales down to what fits.
     * Reset to null to restore full auto-scaling.
     */
    manual_size?: number | null;
    /** When true, neither auto-scaling nor manual_size is applied. */
    disabled?: boolean;
    /**
     * Explicit CSS height for the wrapper div (e.g. "1.5in", "80px", "3rem").
     * REQUIRED for auto-scaling unless height is controlled via class or a flex parent.
     * Without a constrained height, offsetHeight == scrollHeight always → max font returned.
     */
    height?: string;
    /** Explicit CSS width for the wrapper div. Usually not needed — inherits from parent. */
    width?: string;
    class?: string;
    style?: string;
    children?: Snippet;
}

let {
    min = 16,
    max = 80,
    manual_size = null,
    disabled = false,
    height,
    width,
    class: extra_class = '',
    style: extra_style = '',
    children
}: Props = $props();

// Always run the action unless explicitly disabled.
// When manual_size is set, use it as the max for the binary search — the action
// still runs so the text can never overflow the container. This means manual_size
// is "no larger than X, but still container-safe":
//   manual_size fits  → renders at exactly manual_size (same as before)
//   manual_size too big → action scales down to what fits (prevents clipping)
//   manual_size=null  → auto-scales up to max as normal
// Using min=1 in manual mode: the user may intentionally want sizes below the
// auto-scale minimum (e.g. a very long name that only reads well at 22px).
// The controls' own FONT_SIZE_MIN prevents comically small values.
//
// WHY NOT inline style: if we set font-size via style attr AND the action sets it
// via node.style.fontSize, the last writer wins — causing unpredictable results.
// Letting the action own font-size entirely avoids this race.
let action_params = $derived(
    disabled
        ? null
        : {
              min: manual_size != null ? 1 : min,
              max: manual_size != null ? manual_size : max
          }
);

// Compose the final inline style — font-size is NOT here; the action owns it.
let computed_style = $derived(() => {
    const parts: string[] = [];
    if (height) parts.push(`height: ${height}`);
    if (width) parts.push(`width: ${width}`);
    if (extra_style) parts.push(extra_style);
    return parts.join('; ');
});
</script>

<div class={extra_class} style={computed_style()} use:fit_text={action_params}>
    {@render children?.()}
</div>
