Draw the badge without Tailwind
Plan step 36. <x-badge> renders data-md-badge with data-md-dot, data-md-variant (solid, tonal or outline, resolved in that order), data-md-color and data-md-floating; badge.css draws BadgeTokens' dot and count, the status label, each colour's pair of roles through custom properties, and M3's anchor geometry when floating. A plain badge sets no colour, so the caller's CSS paints it. NavigationBarTest (navigation group) asserts the dot by data-md-dot. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Qwx5USif3wFFmxtHg5U1g9
This commit is contained in:
co-authored by
Claude Opus 5
parent
d4b71c519c
commit
7bdd3ef61c
@@ -14,6 +14,7 @@
|
||||
/* Actions and communication */
|
||||
@import './components/loading.css';
|
||||
@import './components/tooltip.css';
|
||||
@import './components/badge.css';
|
||||
|
||||
/* Inputs, selection and data */
|
||||
|
||||
|
||||
@@ -0,0 +1,143 @@
|
||||
/*
|
||||
* <x-badge>: M3's small and large badges, and the library's status label.
|
||||
*
|
||||
* BadgeTokens (androidx Compose Material 3, Apache-2.0): the small badge is a 6px dot
|
||||
* (`data-md-dot`), the large badge 16px tall and at least 16px wide with 4px either side of a
|
||||
* `label-small` count in tabular figures, both fully round and `error` by default. A floating
|
||||
* badge sits on M3's anchor geometry at the top-end corner of its `position: relative` parent: the
|
||||
* dot flush in the corner, the count 2px above it with its start 12px in from the parent's end.
|
||||
*
|
||||
* The status label (`data-md-variant`) is not an M3 badge: 24px tall, small corner, 8px either
|
||||
* side, 4px between an icon and its word, `label-medium`. `tonal` draws it in the colour's
|
||||
* container, `solid` in the colour itself, `outline` as a 1px `outline` edge around
|
||||
* `on-surface-variant` text — `outline` rather than `outline-variant`, because M3 asks a badge for
|
||||
* 3:1 against what is behind it and `outline-variant` is the decorative role dividers use.
|
||||
*
|
||||
* `data-md-color` picks the pair of roles; `neutral` has no hue of its own (`on-surface-variant`
|
||||
* and `surface`, or `surface-container-high` behind `on-surface-variant` text), and `plain` sets
|
||||
* no colour at all, so the caller's CSS paints it.
|
||||
*/
|
||||
|
||||
@layer material.reset, material.tokens, material.base, material.layout, material.components, material.text, material.visibility;
|
||||
|
||||
@layer material.components {
|
||||
[data-md-badge] {
|
||||
--md-badge-color: var(--md-sys-color-error);
|
||||
--md-badge-on-color: var(--md-sys-color-on-error);
|
||||
--md-badge-container: var(--md-sys-color-error-container);
|
||||
--md-badge-on-container: var(--md-sys-color-on-error-container);
|
||||
|
||||
display: inline-flex;
|
||||
flex-shrink: 0;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
[data-md-badge][data-md-dot] {
|
||||
inline-size: 6px;
|
||||
block-size: 6px;
|
||||
border-radius: var(--md-sys-shape-corner-full);
|
||||
}
|
||||
|
||||
[data-md-badge]:not([data-md-dot], [data-md-variant]) {
|
||||
min-inline-size: 16px;
|
||||
block-size: 16px;
|
||||
border-radius: var(--md-sys-shape-corner-full);
|
||||
padding-inline: var(--md-sys-measurement-space50);
|
||||
font: var(--md-sys-typescale-label-sm);
|
||||
letter-spacing: var(--md-sys-typescale-label-sm-tracking);
|
||||
font-variation-settings: normal;
|
||||
font-variant-numeric: tabular-nums;
|
||||
}
|
||||
|
||||
[data-md-badge][data-md-variant] {
|
||||
gap: var(--md-sys-measurement-space50);
|
||||
block-size: 24px;
|
||||
border-radius: var(--md-sys-shape-corner-sm);
|
||||
padding-inline: var(--md-sys-measurement-space100);
|
||||
font: var(--md-sys-typescale-label-md);
|
||||
letter-spacing: var(--md-sys-typescale-label-md-tracking);
|
||||
font-variation-settings: normal;
|
||||
}
|
||||
|
||||
[data-md-badge][data-md-variant='outline'] {
|
||||
border: 1px solid;
|
||||
}
|
||||
|
||||
[data-md-badge][data-md-color='primary'] {
|
||||
--md-badge-color: var(--md-sys-color-primary);
|
||||
--md-badge-on-color: var(--md-sys-color-on-primary);
|
||||
--md-badge-container: var(--md-sys-color-primary-container);
|
||||
--md-badge-on-container: var(--md-sys-color-on-primary-container);
|
||||
}
|
||||
|
||||
[data-md-badge][data-md-color='secondary'] {
|
||||
--md-badge-color: var(--md-sys-color-secondary);
|
||||
--md-badge-on-color: var(--md-sys-color-on-secondary);
|
||||
--md-badge-container: var(--md-sys-color-secondary-container);
|
||||
--md-badge-on-container: var(--md-sys-color-on-secondary-container);
|
||||
}
|
||||
|
||||
[data-md-badge][data-md-color='tertiary'] {
|
||||
--md-badge-color: var(--md-sys-color-tertiary);
|
||||
--md-badge-on-color: var(--md-sys-color-on-tertiary);
|
||||
--md-badge-container: var(--md-sys-color-tertiary-container);
|
||||
--md-badge-on-container: var(--md-sys-color-on-tertiary-container);
|
||||
}
|
||||
|
||||
[data-md-badge][data-md-color='success'] {
|
||||
--md-badge-color: var(--md-sys-color-success);
|
||||
--md-badge-on-color: var(--md-sys-color-on-success);
|
||||
--md-badge-container: var(--md-sys-color-success-container);
|
||||
--md-badge-on-container: var(--md-sys-color-on-success-container);
|
||||
}
|
||||
|
||||
[data-md-badge][data-md-color='warning'] {
|
||||
--md-badge-color: var(--md-sys-color-warning);
|
||||
--md-badge-on-color: var(--md-sys-color-on-warning);
|
||||
--md-badge-container: var(--md-sys-color-warning-container);
|
||||
--md-badge-on-container: var(--md-sys-color-on-warning-container);
|
||||
}
|
||||
|
||||
[data-md-badge][data-md-color='info'] {
|
||||
--md-badge-color: var(--md-sys-color-info);
|
||||
--md-badge-on-color: var(--md-sys-color-on-info);
|
||||
--md-badge-container: var(--md-sys-color-info-container);
|
||||
--md-badge-on-container: var(--md-sys-color-on-info-container);
|
||||
}
|
||||
|
||||
[data-md-badge][data-md-color='neutral'] {
|
||||
--md-badge-color: var(--md-sys-color-on-surface-variant);
|
||||
--md-badge-on-color: var(--md-sys-color-surface);
|
||||
--md-badge-container: var(--md-sys-color-surface-container-high);
|
||||
--md-badge-on-container: var(--md-sys-color-on-surface-variant);
|
||||
}
|
||||
|
||||
[data-md-badge]:not([data-md-color='plain']) {
|
||||
background-color: var(--md-badge-color);
|
||||
color: var(--md-badge-on-color);
|
||||
|
||||
&[data-md-variant='tonal'] {
|
||||
background-color: var(--md-badge-container);
|
||||
color: var(--md-badge-on-container);
|
||||
}
|
||||
|
||||
&[data-md-variant='outline'] {
|
||||
border-color: var(--md-sys-color-outline);
|
||||
background-color: transparent;
|
||||
color: var(--md-sys-color-on-surface-variant);
|
||||
}
|
||||
}
|
||||
|
||||
[data-md-badge][data-md-floating] {
|
||||
position: absolute;
|
||||
inset-block-start: calc(-1 * var(--md-sys-measurement-space25));
|
||||
inset-inline-start: calc(100% - 12px);
|
||||
}
|
||||
|
||||
[data-md-badge][data-md-floating][data-md-dot] {
|
||||
inset-block-start: 0;
|
||||
inset-inline: auto 0;
|
||||
}
|
||||
}
|
||||
@@ -7,10 +7,10 @@
|
||||
Both are `error` by default, as M3 draws them, and an `outline` status label draws its edge in
|
||||
`outline`, not `outline-variant`: M3 asks a badge for 3:1 against what is behind it, and
|
||||
`outline-variant` is the decorative role dividers use. `floating` pins one to the top-end
|
||||
corner of a `relative` parent, on M3's anchor geometry — the dot flush in the corner, the
|
||||
count 2px above it — an icon or an icon button:
|
||||
corner of a `position: relative` parent, on M3's anchor geometry — the dot flush in the corner,
|
||||
the count 2px above it — an icon or an icon button:
|
||||
|
||||
<span class="relative inline-flex"><x-icon name="notifications" /><x-badge value="4" floating /></span>
|
||||
<span style="position: relative; display: 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), `solid` in the colour itself (a label that
|
||||
@@ -21,13 +21,17 @@
|
||||
count (the ink an outline badge already writes in), surface-container-high with
|
||||
on-surface-variant text when `tonal`, the outline 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" />`.
|
||||
caller's CSS paints it.
|
||||
|
||||
The value is `value`, or the slot, which renders as HTML — an icon beside the word:
|
||||
`<x-badge tonal><x-icon name="bolt" optical="20" class="size-3" /> Pro</x-badge>`. With neither it is a dot.
|
||||
`<x-badge tonal><x-icon name="bolt" size="12" /> 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. --}}
|
||||
that includes it ("Notifications, 4 new"), or pass `label` here.
|
||||
|
||||
Drawn by resources/css/components/badge.css from `data-md-dot` (the small badge),
|
||||
`data-md-variant` (a status label: `solid`, `tonal` or `outline`, the first of them that is
|
||||
set, in that order), `data-md-color` and `data-md-floating`. --}}
|
||||
|
||||
@props([
|
||||
'value' => null,
|
||||
@@ -54,38 +58,23 @@
|
||||
$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, $solid => $filled[$color],
|
||||
$tonal => $container[$color],
|
||||
default => 'border-outline text-on-surface-variant',
|
||||
// `solid` wins over `tonal`, and either over `outline`, as the three have always resolved.
|
||||
$variant = match (true) {
|
||||
! $status => null,
|
||||
(bool) $solid => 'solid',
|
||||
(bool) $tonal => 'tonal',
|
||||
default => 'outline',
|
||||
};
|
||||
|
||||
$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 && ! $solid && ! $dot,
|
||||
$paint => $paint !== '',
|
||||
'absolute top-0 end-0' => $floating && $dot,
|
||||
'absolute -top-0.5 start-[calc(100%-0.75rem)]' => $floating && ! $dot,
|
||||
])
|
||||
->merge(array_filter([
|
||||
'aria-label' => $label,
|
||||
'aria-hidden' => $label === null && ! $status ? 'true' : null,
|
||||
]));
|
||||
$attributes = $attributes->merge(array_filter([
|
||||
'data-md-badge' => true,
|
||||
'data-md-dot' => $dot ? true : null,
|
||||
'data-md-variant' => $variant,
|
||||
'data-md-color' => $color,
|
||||
'data-md-floating' => $floating ? true : null,
|
||||
'aria-label' => $label,
|
||||
'aria-hidden' => $label === null && ! $status ? 'true' : null,
|
||||
], fn ($value): bool => $value !== null));
|
||||
@endphp
|
||||
|
||||
<span {{ $attributes }}>@unless ($dot)@if ($markup){{ $slot }}@else{{ $text }}@endif@endunless</span>
|
||||
|
||||
Reference in New Issue
Block a user