diff --git a/resources/boost/skills/livewire-material-development/SKILL.md b/resources/boost/skills/livewire-material-development/SKILL.md index e16e4965..69a51eb6 100644 --- a/resources/boost/skills/livewire-material-development/SKILL.md +++ b/resources/boost/skills/livewire-material-development/SKILL.md @@ -353,7 +353,10 @@ A value the server changes animates after a morph (the SVG is `wire:ignore`; onl ### `` - `` — M3's small badge, a dot. `` — M3's large badge, a count. Both `error` by default. `floating` pins it to the top-end corner of a `relative` parent: ``. A dot or count is `aria-hidden` unless it has a `label`; name the control instead ("Messages, 4 unread"). -- ``, ``, `` — a status label (not an M3 badge) in the colour's container or a neutral edge. `color` (alias `tone`): `error` default, `primary`, `secondary`, `tertiary`, `success`, `warning`, `info`. +- ``, ``, `` — a status label (not an M3 badge) in the colour's container or a neutral edge. `color` (alias `tone`): `error` default, `primary`, `secondary`, `tertiary`, `success`, `warning`, `info`, `neutral`, `plain`; an unknown colour is `error`. +- `color="neutral"` — neutral ink on every variant: a dot or count in on-surface-variant with surface text, `tonal` in surface-container-high with on-surface-variant text, `outline` in the outline-variant edge with on-surface-variant text. +- `color="plain"` — no background, text or border colour in any variant (shape, size and type stay), so the classes you pass paint it: ``. Pass both a background and a text class; an `outline` badge's edge takes the text colour unless you pass a `border-*` colour. +- The value is `value` or the slot; the slot renders as HTML: ` Pro`. `value` is escaped. A slot that holds only whitespace or comments is still a dot. ### `` diff --git a/resources/views/components/badge.blade.php b/resources/views/components/badge.blade.php index f7c4b04f..00dec897 100644 --- a/resources/views/components/badge.blade.php +++ b/resources/views/components/badge.blade.php @@ -11,7 +11,16 @@ The status label is not an M3 badge but every app needs one: `tonal` draws the value in the colour's container ("Expired" in error-container), `outline` in a neutral edge. `color` (alias - `tone`): `error` (the default), `primary`, `secondary`, `tertiary`, `success`, `warning`, `info`. + `tone`): `error` (the default), `primary`, `secondary`, `tertiary`, `success`, `warning`, `info`, + and two without a hue of their own: + - `neutral` — neutral ink on every variant: on-surface-variant with surface text as a dot or + count (the ink an outline badge already writes in), surface-container-high with + on-surface-variant text when `tonal`, the outline-variant edge when `outline`. + - `plain` — no background, text or border colour at all, only shape, size and type, so the + caller's classes paint it: ``. + + The value is `value`, or the slot, which renders as HTML — an icon beside the word: + ` Pro`. With neither it is a dot. A count or dot says nothing to a screen reader on its own: give the icon's control a label that includes it ("Notifications, 4 new"), or pass `label` here. --}} @@ -28,23 +37,34 @@ ]) @php - $color = in_array($color ?? $tone, ['primary', 'secondary', 'tertiary', 'error', 'success', 'warning', 'info'], true) ? ($color ?? $tone) : 'error'; - $text = $value ?? ($slot->isNotEmpty() ? trim((string) $slot) : null); + $color = in_array($color ?? $tone, ['primary', 'secondary', 'tertiary', 'error', 'success', 'warning', 'info', 'neutral', 'plain'], true) ? ($color ?? $tone) : 'error'; + // hasActualContent(): a slot holding only a comment, or an empty @foreach, is still a dot. + $markup = $value === null && $slot->hasActualContent(); + $text = $value ?? ($markup ? trim((string) $slot) : null); $dot = blank($text); $status = ($tonal || $outline) && ! $dot; if (! $dot && $max !== null && is_numeric($text) && (int) $text > (int) $max) { $text = $max.'+'; + $markup = false; } $filled = [ 'primary' => 'bg-primary text-on-primary', 'secondary' => 'bg-secondary text-on-secondary', 'tertiary' => 'bg-tertiary text-on-tertiary', 'error' => 'bg-error text-on-error', 'success' => 'bg-success text-on-success', 'warning' => 'bg-warning text-on-warning', 'info' => 'bg-info text-on-info', + 'neutral' => 'bg-on-surface-variant text-surface', ]; $container = [ 'primary' => 'bg-primary-container text-on-primary-container', 'secondary' => 'bg-secondary-container text-on-secondary-container', 'tertiary' => 'bg-tertiary-container text-on-tertiary-container', 'error' => 'bg-error-container text-on-error-container', 'success' => 'bg-success-container text-on-success-container', 'warning' => 'bg-warning-container text-on-warning-container', 'info' => 'bg-info-container text-on-info-container', + 'neutral' => 'bg-surface-container-high text-on-surface-variant', ]; + $paint = match (true) { + $color === 'plain' => '', + ! $status => $filled[$color], + $tonal => $container[$color], + default => 'border-outline-variant text-on-surface-variant', + }; $attributes = $attributes ->class([ @@ -52,9 +72,8 @@ 'size-1.5 rounded-corner-full' => $dot, 'h-4 min-w-4 rounded-corner-full px-1 type-label-sm tabular-nums' => ! $dot && ! $status, 'h-6 gap-1 rounded-corner-sm px-2 type-label-md' => $status, - $filled[$color] => ! $status, - $container[$color] => $tonal && ! $dot, - 'border border-outline-variant text-on-surface-variant' => $outline && ! $tonal && ! $dot, + 'border' => $outline && ! $tonal && ! $dot, + $paint => $paint !== '', 'absolute top-0.5 end-0.5' => $floating && $dot, 'absolute -top-1 start-[calc(100%-0.75rem)]' => $floating && ! $dot, ]) @@ -64,4 +83,4 @@ ])); @endphp -@unless ($dot){{ $text }}@endunless +@unless ($dot)@if ($markup){{ $slot }}@else{{ $text }}@endif@endunless diff --git a/resources/views/showcase/sections/communication.blade.php b/resources/views/showcase/sections/communication.blade.php index 47c46dde..8a13f002 100644 --- a/resources/views/showcase/sections/communication.blade.php +++ b/resources/views/showcase/sections/communication.blade.php @@ -8,6 +8,11 @@ + + + + Pro + BLADE, 'Snackbars' => <<<'BLADE'
diff --git a/tests/Feature/Components/BadgeTest.php b/tests/Feature/Components/BadgeTest.php index 9442f42b..4c8cd824 100644 --- a/tests/Feature/Components/BadgeTest.php +++ b/tests/Feature/Components/BadgeTest.php @@ -30,3 +30,49 @@ it('draws a status label in a container or an outline', function () { ->toContain('border border-outline-variant text-on-surface-variant') ->not->toContain('bg-error'); }); + +it('renders its slot as HTML, and is a dot while the slot holds nothing but comments', function () { + $html = (string) $this->blade(' Pro'); + + expect($html) + ->toContain('h-6 gap-1 rounded-corner-sm') + ->toContain('toContain(' Pro') + ->not->toContain('<svg') + ->and((string) $this->blade(''))->toContain('<b>4</b>') + ->and((string) $this->blade("\n \n"))->toContain('size-1.5 rounded-corner-full') + ->and((string) $this->blade('120'))->toContain('>99+'); +}); + +it('draws neutral ink on every variant', function () { + expect((string) $this->blade(''))->toContain('size-1.5 rounded-corner-full bg-on-surface-variant text-surface') + ->and((string) $this->blade(''))->toContain('tabular-nums bg-on-surface-variant text-surface') + ->and((string) $this->blade(''))->toContain('type-label-md bg-surface-container-high text-on-surface-variant') + ->and((string) $this->blade(''))->toContain('type-label-md border border-outline-variant text-on-surface-variant'); +}); + +it('leaves a plain badge to the caller\'s colour classes', function (string $badge, string $shape) { + $html = (string) $this->blade($badge); + + preg_match('/class="([^"]*)"/', $html, $class); + + expect($class[1]) + ->toContain($shape) + ->toEndWith('bg-tertiary-container text-on-tertiary-container') + ->and(preg_replace('/bg-tertiary-container text-on-tertiary-container$/', '', $class[1]))->not->toMatch('/(^|\s)(bg|text|border)-/'); +})->with([ + 'dot' => ['', 'size-1.5 rounded-corner-full'], + 'count' => ['', 'h-4 min-w-4 rounded-corner-full px-1 type-label-sm tabular-nums'], + 'tonal' => ['', 'h-6 gap-1 rounded-corner-sm px-2 type-label-md'], + 'outline' => ['', 'h-6 gap-1 rounded-corner-sm px-2 type-label-md border'], +]); + +it('keeps its default colours, and falls back to error on a colour it does not know', function () { + expect((string) $this->blade('')) + ->toContain('class="inline-flex shrink-0 items-center justify-center whitespace-nowrap h-4 min-w-4 rounded-corner-full px-1 type-label-sm tabular-nums bg-error text-on-error"') + ->and((string) $this->blade('')) + ->toContain('class="inline-flex shrink-0 items-center justify-center whitespace-nowrap h-6 gap-1 rounded-corner-sm px-2 type-label-md bg-error-container text-on-error-container"') + ->and((string) $this->blade('')) + ->toContain('class="inline-flex shrink-0 items-center justify-center whitespace-nowrap h-6 gap-1 rounded-corner-sm px-2 type-label-md border border-outline-variant text-on-surface-variant"') + ->and((string) $this->blade(''))->toContain('bg-error text-on-error'); +});