{{-- M3 Expressive's progress indicator: how far along a process is, or that one is running. `value` (0 to `max`, which is 100 unless given) makes it determinate; without one it is indeterminate. Values outside the range are clamped. `bind` is an Alpine expression for a value that changes in the browser — a Livewire upload's `progress` in the parent's x-data — which the indicator follows (null or undefined: indeterminate). A value the server changes moves the same way after a Livewire morph. Linear by default, as wide as its container (a `w-*` class narrows it). `circular` is 40px, 48px wavy, unless a `size-*` class is passed. `wavy` draws Expressive's wave. `thick` makes the track and indicator 8px instead of 4px and keeps the wave as high as it was, which is what Compose's thick samples do: the container grows by the extra stroke (14px linear wavy, 44px circular, 52px circular wavy). `color` is the active indicator and the stop: `primary` (the default), `secondary`, `tertiary`, `error`, `success`, `warning`, `info`; the track is secondary-container for primary and secondary, and the colour's own container otherwise. A `progressbar` named by `label` ("Progress"), with aria-valuenow while determinate; `:label="false"` makes it decorative where something else already says what is happening. The server draws the first frame — the value, or a still of the indeterminate animation, flat even for a wavy one — so the indicator is right before any script runs. From then on resources/js/progress.js draws it frame by frame, ported from androidx Compose Material 3 at commit 27cf9a7d5788aa0f5f2d8b6699ce279560daf326 (Apache-2.0): ProgressIndicator.kt, WavyProgressIndicator.kt and its Linear/CircularWavyProgressModifiers, with LinearProgressIndicatorTokens, CircularProgressIndicatorTokens and ProgressIndicatorTokens — a 4px gap between indicator and track, a 4px stop at the track's end, wavelengths of 40px (20px indeterminate) and 15px, active indicator in primary, track in secondary-container. The SVG is `wire:ignore`; only the root's attributes change on a morph. --}} @props([ 'value' => null, 'max' => 100, 'bind' => null, 'circular' => false, 'wavy' => false, 'thick' => false, 'color' => 'primary', 'label' => null, ]) @php $format = fn (float $number): string => rtrim(rtrim(number_format(round($number, 3) + 0.0, 3, '.', ''), '0'), '.'); $maximum = is_numeric($max) && (float) $max > 0 ? (float) $max : 100.0; $amount = is_numeric($value) ? min(max((float) $value, 0.0), $maximum) : null; $fraction = $amount === null ? null : $amount / $maximum; $bound = filled($bind) ? (string) $bind : null; $color = in_array($color, ['primary', 'secondary', 'tertiary', 'error', 'success', 'warning', 'info'], true) ? $color : 'primary'; $decorative = $label === false; $label = $decorative ? null : ($label ?? __('Progress')); $classList = (string) $attributes->get('class'); $sized = $circular ? preg_match('/(^|\s)(size|w|h)-/', $classList) === 1 : preg_match('/(^|\s)w-/', $classList) === 1; // Every colour class written out whole, so Tailwind compiles it. The active indicator is // drawn in currentColor; the track takes the inherited `stroke`. $ink = [ 'primary' => 'text-primary', 'secondary' => 'text-secondary', 'tertiary' => 'text-tertiary', 'error' => 'text-error', 'success' => 'text-success', 'warning' => 'text-warning', 'info' => 'text-info', ]; $trackInk = [ 'primary' => 'stroke-secondary-container', 'secondary' => 'stroke-secondary-container', 'tertiary' => 'stroke-tertiary-container', 'error' => 'stroke-error-container', 'success' => 'stroke-success-container', 'warning' => 'stroke-warning-container', 'info' => 'stroke-info-container', ]; $stroke = $thick ? 8 : 4; $cap = $stroke / 2; // The space between an active line and the track, caps included: TrackActiveSpace + the stroke. $spacing = 4 + $stroke; // The first frame. Linear lines sit at a fraction of the width plus a length in pixels, // which a nested and a translate express without knowing the width. $segments = []; $arcs = []; $stopOffset = null; $box = null; $middle = ($circular || ! $wavy ? $stroke : ($thick ? 14 : 10)) / 2; $segment = fn (float $from, float $fromOffset, float $to, float $toOffset, bool $active): array => compact('from', 'fromOffset', 'to', 'toOffset', 'active'); if (! $circular && $fraction !== null) { if ($fraction <= 0) { $segments[] = $segment(0, $cap, 1, -$cap, false); } else { if ($fraction < 1) { $segments[] = $segment($fraction, $spacing, 1, -$cap, false); } $segments[] = $segment(0, $cap, $fraction, $fraction >= 1 ? -$cap : 0, true); } // The stop indicator's centre, from the end: half its 4px, plus its offset on a thick track. $stopOffset = 2 + min(($stroke - 4) / 2, 6); } elseif (! $circular) { // progress.js's LINEAR_STILL (875 ms): the first line at 25.74–62.57%, the second at 0–3.98%. $segments[] = $segment(0.6257, $spacing, 1, -$cap, false); $segments[] = $segment(0.2574, 0, 0.6257, 0, true); $segments[] = $segment(0.0398, $spacing, 0.2574, -$spacing, false); $segments[] = $segment(0, $cap, 0.0398, 0, true); } else { $box = ($wavy ? 48 : 40) + ($thick ? 4 : 0); $centre = $box / 2; $radius = ($box - $stroke) / 2; $arc = function (float $start, float $sweep) use ($centre, $radius, $format): string { if ($sweep == 0) { return ''; } if (abs($sweep) >= 360) { return 'M'.$format($centre + $radius).' '.$format($centre).'A'.$format($radius).' '.$format($radius).' 0 1 1 '.$format($centre - $radius).' '.$format($centre) .'A'.$format($radius).' '.$format($radius).' 0 1 1 '.$format($centre + $radius).' '.$format($centre).'Z'; } [$x0, $y0] = [$centre + $radius * cos(deg2rad($start)), $centre + $radius * sin(deg2rad($start))]; [$x1, $y1] = [$centre + $radius * cos(deg2rad($start + $sweep)), $centre + $radius * sin(deg2rad($start + $sweep))]; if (hypot($x1 - $x0, $y1 - $y0) < 0.01) { return 'M'.$format($x0).' '.$format($y0).'L'.$format($x0 + 0.001).' '.$format($y0); } return 'M'.$format($x0).' '.$format($y0).'A'.$format($radius).' '.$format($radius).' 0 '.(abs($sweep) > 180 ? 1 : 0).' '.($sweep > 0 ? 1 : 0).' '.$format($x1).' '.$format($y1); }; // Determinate from 12 o'clock; indeterminate is progress.js's CIRCULAR_STILL (2000 ms): // turned to 9 o'clock, 61.33% of the circle. $start = $fraction === null ? 180 : 270; $sweep = ($fraction ?? 0.6133) * 360; if ($wavy) { $circumference = 2 * M_PI * $radius; $reach = $sweep / 360 * $circumference; $trackGap = (min($reach, $cap) * 2 + min($reach, 4)) / $circumference * 360; if (360 - $sweep - $trackGap * 2 > 0) { $arcs[] = ['d' => $arc($start + $sweep + $trackGap, 360 - $sweep - $trackGap * 2), 'active' => false]; } } elseif ($fraction !== null) { $trackGap = min($sweep, (4 + $stroke) / (M_PI * $box) * 360); $arcs[] = ['d' => $arc($start + $sweep + $trackGap, 360 - $sweep - $trackGap * 2), 'active' => false]; } $arcs[] = ['d' => $arc($start, $sweep), 'active' => true]; } $attributes = $attributes ->class([ 'block rtl:rotate-180' => ! $circular, 'w-full' => ! $circular && ! $sized, 'h-1' => ! $circular && ! $wavy && ! $thick, 'h-2' => ! $circular && ! $wavy && $thick, 'h-2.5' => ! $circular && $wavy && ! $thick, 'h-3.5' => ! $circular && $wavy && $thick, 'inline-block shrink-0 align-middle' => $circular, 'size-10' => $circular && ! $sized && ! $wavy && ! $thick, 'size-11' => $circular && ! $sized && ! $wavy && $thick, 'size-12' => $circular && ! $sized && $wavy && ! $thick, 'size-13' => $circular && ! $sized && $wavy && $thick, $ink[$color], $trackInk[$color], ]) ->merge(array_filter([ 'role' => $decorative ? null : 'progressbar', 'aria-label' => $label, 'aria-hidden' => $decorative ? 'true' : null, 'aria-valuemin' => $decorative ? null : '0', 'aria-valuemax' => $decorative ? null : $format($maximum), 'aria-valuenow' => $decorative || $amount === null ? null : $format($amount), 'x-bind:aria-valuenow' => $decorative || $bound === null ? null : "((v) => v === null || v === undefined || v === false || v === '' || isNaN(v) ? null : Math.min(Math.max(Number(v), 0), ".$format($maximum)."))(".$bound.')', 'x-data' => 'materialProgress', 'data-value' => $amount === null ? null : $format($amount), 'x-bind:data-value' => $bound, 'data-max' => $format($maximum), 'data-circular' => $circular ? true : null, 'data-wavy' => $wavy ? true : null, 'data-thick' => $thick ? true : null, ], fn ($attribute): bool => $attribute !== null)); @endphp