The slot was cast to a string and escaped, so an icon beside a badge's word showed as literal markup. It renders as HTML now; `value` stays escaped, and a slot holding only whitespace or comments is still a dot (hasActualContent rather than isNotEmpty). `color="neutral"` draws neutral ink on every variant: a dot or count in on-surface-variant with surface text (the ink the outline badge already writes in), a tonal label in surface-container-high with on-surface-variant text, and an outline in outline-variant. `color="plain"` emits no background, text or border colour in any variant, keeping shape, size and type, so an application's own colour classes paint it without racing the badge's. Every existing colour renders the same classes as before, and an unknown colour still falls back to error. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RHoXZSHc8gGpZjFmA5fPc2
87 lines
4.6 KiB
PHP
87 lines
4.6 KiB
PHP
{{-- A badge: M3's count or dot on an icon, or a short status label.
|
|
|
|
M3's two badges (BadgeTokens, androidx Compose Material 3, Apache-2.0):
|
|
- `<x-badge />` — the small badge, a 6px dot: something new, no number.
|
|
- `<x-badge value="3" />` — the large badge, 16px tall, label-small: a count. `max` caps what is
|
|
shown ("99+").
|
|
Both are `error` by default, as M3 draws them. `floating` pins one to the top-end corner of a
|
|
`relative` parent — an icon or an icon button:
|
|
|
|
<span class="relative inline-flex"><x-icon name="notifications" /><x-badge value="4" floating /></span>
|
|
|
|
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`,
|
|
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: `<x-badge value="Run" tonal color="plain" class="bg-tertiary-container text-on-tertiary-container" />`.
|
|
|
|
The value is `value`, or the slot, which renders as HTML — an icon beside the word:
|
|
`<x-badge tonal><x-icon name="bolt" class="size-3" /> Pro</x-badge>`. 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. --}}
|
|
|
|
@props([
|
|
'value' => null,
|
|
'max' => null,
|
|
'color' => null,
|
|
'tone' => null,
|
|
'tonal' => false,
|
|
'outline' => false,
|
|
'floating' => false,
|
|
'label' => null,
|
|
])
|
|
|
|
@php
|
|
$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([
|
|
'inline-flex shrink-0 items-center justify-center whitespace-nowrap',
|
|
'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,
|
|
'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,
|
|
])
|
|
->merge(array_filter([
|
|
'aria-label' => $label,
|
|
'aria-hidden' => $label === null && ! $status ? 'true' : null,
|
|
]));
|
|
@endphp
|
|
|
|
<span {{ $attributes }}>@unless ($dot)@if ($markup){{ $slot }}@else{{ $text }}@endif@endunless</span>
|