Draw the progress indicator without Tailwind

Plan step 36 (actions). <x-progress> renders data-md-progress with
data-md-color and data-md-circular/-wavy/-thick, and
data-md-value/-max in place of data-value/data-max; no class list.
progress.css draws LinearProgressIndicatorTokens and
CircularProgressIndicatorTokens' stroke, sizes and colours, and the
linear indicator's unconditional right-to-left mirror. Its sizing is
a default only, same as loading.css: a caller's own class, unlayered
or in Tailwind's utilities layer, still outranks it, so the width/
size detection regex the view carried is gone with it.

resources/js/progress.js's WATCHED array and its reads follow the
renamed hooks. ProgressTest (Feature and Browser) is rewritten on the
hooks and ComponentStylesheet.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Qwx5USif3wFFmxtHg5U1g9
This commit is contained in:
Andreas Reinhold / reini
2026-09-14 16:46:52 +02:00
co-authored by Claude Opus 5
parent ce8ac4d316
commit 15cd306aba
7 changed files with 211 additions and 99 deletions
+35 -60
View File
@@ -6,13 +6,15 @@
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.
Linear by default, as wide as its container, unless the caller's own class narrows it — an
application's unlayered CSS always outranks the package's default, `w-*` Tailwind utilities
included. `circular` is 40px, 48px wavy, the same way. `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.
@@ -25,7 +27,11 @@
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. --}}
The SVG is `wire:ignore`; only the root's attributes change on a morph.
Drawn by resources/css/components/progress.css from `data-md-progress`, `data-md-color` and
the `data-md-circular`/`data-md-wavy`/`data-md-thick`/`data-md-value`/`data-md-max` progress.js
also reads and writes; no class list. --}}
@props([
'value' => null,
@@ -50,23 +56,6 @@
$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.
@@ -147,44 +136,30 @@
$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));
$attributes = $attributes->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-md-progress' => true,
'data-md-color' => $color,
'data-md-value' => $amount === null ? null : $format($amount),
'x-bind:data-md-value' => $bound,
'data-md-max' => $format($maximum),
'data-md-circular' => $circular ? true : null,
'data-md-wavy' => $wavy ? true : null,
'data-md-thick' => $thick ? true : null,
], fn ($attribute): bool => $attribute !== null));
@endphp
<span {{ $attributes }}>
<svg class="block size-full"@if ($box) viewBox="0 0 {{ $box }} {{ $box }}"@endif fill="none" stroke-width="{{ $stroke }}" stroke-linecap="round" aria-hidden="true" focusable="false" wire:ignore>
<svg @if ($box) viewBox="0 0 {{ $box }} {{ $box }}" @endif fill="none" stroke-width="{{ $stroke }}" stroke-linecap="round" aria-hidden="true" focusable="false" wire:ignore>
@foreach ($segments as $line)
<svg x="{{ $format($line['from'] * 100) }}%" overflow="visible"><line x1="{{ $format($line['fromOffset'] - $line['toOffset']) }}" y1="{{ $format($middle) }}" x2="{{ $format(($line['to'] - $line['from']) * 100) }}%" y2="{{ $format($middle) }}" transform="translate({{ $format($line['toOffset']) }} 0)"@if ($line['active']) stroke="currentColor"@endif /></svg>
@endforeach