diff --git a/resources/css/components.css b/resources/css/components.css index 381e7708..71a24719 100644 --- a/resources/css/components.css +++ b/resources/css/components.css @@ -28,6 +28,7 @@ @import './components/fab-menu.css'; @import './components/rich-tooltip.css'; @import './components/toast.css'; +@import './components/progress.css'; /* Inputs, selection and data */ @import './components/form.css'; diff --git a/resources/css/components/progress.css b/resources/css/components/progress.css new file mode 100644 index 00000000..483f0feb --- /dev/null +++ b/resources/css/components/progress.css @@ -0,0 +1,113 @@ +/* + * : M3 Expressive's progress indicator, drawn on the server for its first frame and + * frame by frame in the browser after. + * + * ProgressIndicatorTokens, LinearProgressIndicatorTokens, CircularProgressIndicatorTokens (androidx + * Compose Material 3, Apache-2.0): 4px stroke (`data-md-thick` 8px), linear 4px tall (8px thick, + * 10px `data-md-wavy`, 14px wavy thick — the container grows by the extra stroke, keeping the + * wave's own height put), circular 40px (44px thick, 48px wavy, 52px wavy thick). `data-md-color` + * is the active indicator's `color` and the track's `stroke`; the track is secondary-container for + * primary and secondary, and the colour's own container otherwise. + * + * Linear mirrors in a right-to-left document unconditionally — M3's own rule for this indicator, + * not the `mirror-rtl` a caller opts an icon into. The sizing here is a default only: an + * application's own class, unlayered or in Tailwind's `utilities` layer, always outranks + * `material.components`, so `w-*`/`size-*` still narrows or grows the indicator exactly as it did + * before this file existed. + * + * resources/js/progress.js reads and writes `data-md-value`, `data-md-max`, `data-md-circular`, + * `data-md-wavy` and `data-md-thick` — ProgressIndicator.kt, WavyProgressIndicator.kt and its + * Linear/CircularWavyProgressModifiers, ported at commit 27cf9a7d5788aa0f5f2d8b6699ce279560daf326 + * — and draws every frame straight onto the SVG's own attributes; nothing here reads a class or + * runs a transition. + */ + +@layer material.reset, material.tokens, material.base, material.layout, material.components, material.text, material.visibility; + +@layer material.components { + [data-md-progress] { + color: var(--md-sys-color-primary); + stroke: var(--md-sys-color-secondary-container); + + & > svg { + display: block; + width: 100%; + height: 100%; + } + } + + [data-md-progress]:not([data-md-circular]) { + display: block; + width: 100%; + height: 4px; + } + + [data-md-progress]:not([data-md-circular]):dir(rtl) { + rotate: 180deg; + } + + [data-md-progress]:not([data-md-circular])[data-md-thick] { + height: 8px; + } + + [data-md-progress]:not([data-md-circular])[data-md-wavy] { + height: 10px; + } + + [data-md-progress]:not([data-md-circular])[data-md-wavy][data-md-thick] { + height: 14px; + } + + [data-md-progress][data-md-circular] { + display: inline-block; + flex-shrink: 0; + vertical-align: middle; + width: 40px; + height: 40px; + } + + [data-md-progress][data-md-circular][data-md-thick] { + width: 44px; + height: 44px; + } + + [data-md-progress][data-md-circular][data-md-wavy] { + width: 48px; + height: 48px; + } + + [data-md-progress][data-md-circular][data-md-wavy][data-md-thick] { + width: 52px; + height: 52px; + } + + [data-md-progress][data-md-color='secondary'] { + color: var(--md-sys-color-secondary); + stroke: var(--md-sys-color-secondary-container); + } + + [data-md-progress][data-md-color='tertiary'] { + color: var(--md-sys-color-tertiary); + stroke: var(--md-sys-color-tertiary-container); + } + + [data-md-progress][data-md-color='error'] { + color: var(--md-sys-color-error); + stroke: var(--md-sys-color-error-container); + } + + [data-md-progress][data-md-color='success'] { + color: var(--md-sys-color-success); + stroke: var(--md-sys-color-success-container); + } + + [data-md-progress][data-md-color='warning'] { + color: var(--md-sys-color-warning); + stroke: var(--md-sys-color-warning-container); + } + + [data-md-progress][data-md-color='info'] { + color: var(--md-sys-color-info); + stroke: var(--md-sys-color-info-container); + } +} diff --git a/resources/js/progress.js b/resources/js/progress.js index a709e7a0..4a1b550c 100644 --- a/resources/js/progress.js +++ b/resources/js/progress.js @@ -5,7 +5,7 @@ * * The server renders a first frame (the value, or a still of the indeterminate animation), so * the indicator is right before this runs. This then owns the SVG, which is `wire:ignore`: a - * Livewire morph only changes the root's `data-value` (so does `x-bind`, for `bind`), and a + * Livewire morph only changes the root's `data-md-value` (so does `x-bind`, for `bind`), and a * MutationObserver turns that into motion. * * - A new value moves on M3 Expressive's effects-slow spring (damping ratio 1, stiffness 800, @@ -54,7 +54,7 @@ */ const SVG = 'http://www.w3.org/2000/svg' -const WATCHED = ['data-value', 'data-max', 'data-circular', 'data-wavy', 'data-thick'] +const WATCHED = ['data-md-value', 'data-md-max', 'data-md-circular', 'data-md-wavy', 'data-md-thick'] // Tokens: *ProgressIndicatorTokens, WavyProgressIndicatorDefaults, ProgressIndicator.kt ---- @@ -1061,16 +1061,16 @@ class Indicator { read(initial) { const element = this.element const shape = WATCHED.slice(2).map((name) => element.hasAttribute(name)).join() - const maximum = parseFloat(element.getAttribute('data-max')) > 0 ? parseFloat(element.getAttribute('data-max')) : 100 - const raw = element.getAttribute('data-value') + const maximum = parseFloat(element.getAttribute('data-md-max')) > 0 ? parseFloat(element.getAttribute('data-md-max')) : 100 + const raw = element.getAttribute('data-md-value') const number = raw === null || raw.trim() === '' ? NaN : Number(raw) const target = Number.isFinite(number) ? clamp(number / maximum, 0, 1) : null if (initial || shape !== this.shape) { this.shape = shape - this.circular = element.hasAttribute('data-circular') - this.wavy = element.hasAttribute('data-wavy') - this.stroke = element.hasAttribute('data-thick') ? 8 : 4 + this.circular = element.hasAttribute('data-md-circular') + this.wavy = element.hasAttribute('data-md-wavy') + this.stroke = element.hasAttribute('data-md-thick') ? 8 : 4 this.build() this.reset(target) } else if ((target === null) !== (this.target === null)) { diff --git a/resources/views/components/progress.blade.php b/resources/views/components/progress.blade.php index 28ea7ece..640e02ad 100644 --- a/resources/views/components/progress.blade.php +++ b/resources/views/components/progress.blade.php @@ -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 -