Draw the chips without Tailwind
<x-chip> renders data-md-chip with its type and its states and parts as data-md-* attributes (elevated, disabled, selected, actionable, the icon, check slot, avatar, action and remove button), and every colour, size, target and transition moves into components/chip.css on tokens (plan step 36). Icons take size 18; the filter checkbox is md-visually-hidden. chips.js follows the renamed chip hooks. A disabled filter chip drawn as a label no longer shows a hover state layer, which M3 never gives a disabled control. Browser tests cover the 32px chip and the remove button's 48px target. 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
b42069503d
commit
d8072d878a
@@ -24,6 +24,7 @@
|
||||
@import './components/checkbox.css';
|
||||
@import './components/radio.css';
|
||||
@import './components/toggle.css';
|
||||
@import './components/chip.css';
|
||||
|
||||
/* Containment */
|
||||
|
||||
|
||||
@@ -0,0 +1,444 @@
|
||||
/*
|
||||
* <x-chip>: M3's assist, filter, input and suggestion chips (resources/views/components/chip.blade.php).
|
||||
*
|
||||
* From androidx Compose Material 3 (Chip.kt, AssistChipTokens, FilterChipTokens, InputChipTokens,
|
||||
* SuggestionChipTokens at androidx/androidx 27cf9a7, Apache-2.0): 32px tall, the small corner,
|
||||
* label-large, 18px icons and a 24px avatar; 16px padding beside a label and 8px beside an icon,
|
||||
* 8px between (an input chip: 12px, 8px, and 4px beside an avatar). Compose draws the 1px border
|
||||
* inside the chip; a CSS border takes room, so every padding beside it is 1px short of its token
|
||||
* (7px for 8, 11px for 12, 15px for 16).
|
||||
*
|
||||
* Colours by type and state, each one role (never an opacity but M3's disabled 12% and 38%,
|
||||
* tokens/state.css):
|
||||
*
|
||||
* flat outline-variant border; on-surface ink (assist) or on-surface-variant (the rest);
|
||||
* the border turns on-surface (assist) or on-surface-variant while keyboard-focused
|
||||
* elevated surface-container-low at elevation 1, 2 on hover, no border (not for input chips)
|
||||
* selected a filter chip that is checked or pressed, an input chip marked `data-md-selected`:
|
||||
* secondary-container with on-secondary-container ink, no border; a flat selected
|
||||
* filter chip rises to elevation 1 on hover
|
||||
* disabled on-surface ink at 38%; the border (flat) or the container (elevated, selected) at 12%
|
||||
*
|
||||
* Leading icons are primary on assist, suggestion and filter chips; on an input chip they are
|
||||
* on-surface-variant, primary on a selected chip and while a chip with its own action is hovered,
|
||||
* focused or pressed (`data-md-actionable`). A trailing icon is primary on assist and suggestion
|
||||
* chips. A disabled chip's icons take its ink.
|
||||
*
|
||||
* The state layer is the chip's own ::before in the chip's ink (an input chip draws it on its
|
||||
* action, since the remove button is a second control), at M3's hover, focus and press opacities;
|
||||
* the focus indicator is the package's secondary ring outside the chip. Both the chip (or an input
|
||||
* chip's action) and the remove button catch presses over 48×48 — M3 lets the target extend beyond
|
||||
* the visible container. The filter chip's check grows in on the fast spatial spring and fades in
|
||||
* on the slow effects one; it shrinks on default effects and fades on fast effects, as
|
||||
* AnimatingChipContent does.
|
||||
*
|
||||
* [data-md-chip="assist|filter|input|suggestion"] the chip; data-md-elevated, data-md-disabled,
|
||||
* data-md-selected, data-md-actionable,
|
||||
* data-md-icon-start, data-md-icon-end,
|
||||
* data-md-avatar, data-md-removable
|
||||
* [data-md-chip-check-slot] a filter chip's check, over its icon when it has one
|
||||
* [data-md-chip-icon], [data-md-chip-check]
|
||||
* [data-md-chip-action] an input chip's own button or link (a <span> without one)
|
||||
* [data-md-chip-avatar], [data-md-chip-icon], [data-md-chip-label]
|
||||
* [data-md-chip-remove] an input chip's remove button
|
||||
* [data-md-chip-label], [data-md-chip-icon], [data-md-chip-trailing]
|
||||
*/
|
||||
|
||||
@layer material.reset, material.tokens, material.base, material.layout, material.components, material.text, material.visibility;
|
||||
|
||||
@import './icon.css';
|
||||
/* TODO(step 36): @import './tooltip.css' once <x-tooltip> leaves Tailwind (actions stream). */
|
||||
|
||||
@layer material.components {
|
||||
[data-md-chip] {
|
||||
--chip-disabled-container: color-mix(in srgb, var(--md-sys-color-on-surface) calc(var(--md-sys-state-disabled-container-opacity) * 100%), transparent);
|
||||
--chip-disabled-ink: color-mix(in srgb, var(--md-sys-color-on-surface) calc(var(--md-sys-state-disabled-content-opacity) * 100%), transparent);
|
||||
|
||||
position: relative;
|
||||
display: inline-flex;
|
||||
flex-shrink: 0;
|
||||
align-items: center;
|
||||
max-width: 100%;
|
||||
height: 32px;
|
||||
border: 1px solid var(--md-sys-color-outline-variant);
|
||||
border-radius: var(--md-sys-shape-corner-sm);
|
||||
color: var(--md-sys-color-on-surface-variant);
|
||||
font: var(--md-sys-typescale-label-lg);
|
||||
letter-spacing: var(--md-sys-typescale-label-lg-tracking);
|
||||
font-variation-settings: normal;
|
||||
white-space: nowrap;
|
||||
-webkit-user-select: none;
|
||||
user-select: none;
|
||||
transition-property: background-color, border-color, color, box-shadow;
|
||||
transition-duration: var(--md-sys-motion-effects-fast-duration);
|
||||
transition-timing-function: var(--md-sys-motion-effects-fast);
|
||||
}
|
||||
|
||||
/* ---- Assist, filter and suggestion: the chip is the control ------------------------------ */
|
||||
|
||||
[data-md-chip]:not([data-md-chip='input']) {
|
||||
isolation: isolate;
|
||||
padding-inline: 15px;
|
||||
outline: none;
|
||||
cursor: pointer;
|
||||
|
||||
/* The 48px target, past the chip's 32px. */
|
||||
&::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
inset-inline: 0;
|
||||
top: 50%;
|
||||
height: var(--md-sys-measurement-space600);
|
||||
translate: 0 -50%;
|
||||
}
|
||||
|
||||
&::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
z-index: -1;
|
||||
border-radius: inherit;
|
||||
background-color: currentColor;
|
||||
opacity: 0;
|
||||
pointer-events: none;
|
||||
transition: opacity var(--md-sys-motion-effects-fast-duration) var(--md-sys-motion-effects-fast);
|
||||
}
|
||||
|
||||
@media (hover: hover) {
|
||||
&:hover::before {
|
||||
opacity: var(--md-sys-state-hover-state-layer-opacity);
|
||||
}
|
||||
}
|
||||
|
||||
&:focus-visible::before,
|
||||
&:has(:focus-visible)::before {
|
||||
opacity: var(--md-sys-state-focus-state-layer-opacity);
|
||||
}
|
||||
|
||||
&:active::before {
|
||||
opacity: var(--md-sys-state-pressed-state-layer-opacity);
|
||||
}
|
||||
|
||||
&:is(:focus-visible, :has(:focus-visible)) {
|
||||
outline: 3px solid var(--md-sys-color-secondary);
|
||||
outline-offset: 2px;
|
||||
}
|
||||
|
||||
&:is(:disabled, [aria-disabled='true'], [data-md-disabled])::before {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
[data-md-chip]:is([data-md-chip='filter'], [data-md-icon-start]):not([data-md-chip='input']) {
|
||||
padding-inline-start: 7px;
|
||||
}
|
||||
|
||||
[data-md-chip][data-md-icon-end] {
|
||||
padding-inline-end: 7px;
|
||||
}
|
||||
|
||||
[data-md-chip='assist'] {
|
||||
color: var(--md-sys-color-on-surface);
|
||||
}
|
||||
|
||||
[data-md-chip='assist']:not([data-md-elevated], [data-md-disabled]):focus-visible {
|
||||
border-color: var(--md-sys-color-on-surface);
|
||||
}
|
||||
|
||||
:is([data-md-chip='suggestion'], [data-md-chip='filter']):not([data-md-elevated], [data-md-disabled], :has(:checked), [aria-pressed='true']):is(:focus-visible, :has(:focus-visible)) {
|
||||
border-color: var(--md-sys-color-on-surface-variant);
|
||||
}
|
||||
|
||||
[data-md-chip][data-md-elevated] {
|
||||
border-color: transparent;
|
||||
background-color: var(--md-sys-color-surface-container-low);
|
||||
box-shadow: var(--md-sys-elevation-1);
|
||||
}
|
||||
|
||||
@media (hover: hover) {
|
||||
[data-md-chip][data-md-elevated]:not([data-md-disabled]):hover {
|
||||
box-shadow: var(--md-sys-elevation-2);
|
||||
}
|
||||
}
|
||||
|
||||
/* Selected: a checked filter chip, or a pressed one. */
|
||||
[data-md-chip='filter']:is(:has(:checked), [aria-pressed='true']),
|
||||
[data-md-chip='input'][data-md-selected] {
|
||||
border-color: transparent;
|
||||
background-color: var(--md-sys-color-secondary-container);
|
||||
color: var(--md-sys-color-on-secondary-container);
|
||||
}
|
||||
|
||||
@media (hover: hover) {
|
||||
[data-md-chip='filter']:not([data-md-elevated], [data-md-disabled]):is(:has(:checked), [aria-pressed='true']):hover {
|
||||
box-shadow: var(--md-sys-elevation-1);
|
||||
}
|
||||
}
|
||||
|
||||
[data-md-chip][data-md-disabled] {
|
||||
border-color: var(--chip-disabled-container);
|
||||
color: var(--chip-disabled-ink);
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
[data-md-chip][data-md-disabled]:is([data-md-elevated], [data-md-selected], :has(:checked), [aria-pressed='true']) {
|
||||
border-color: transparent;
|
||||
background-color: var(--chip-disabled-container);
|
||||
}
|
||||
|
||||
[data-md-chip][data-md-disabled]:not(a) {
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
/* A disabled link leaves the pointer too. */
|
||||
a[data-md-chip][aria-disabled='true'] {
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
/* ---- Contents ------------------------------------------------------------------------------ */
|
||||
|
||||
[data-md-chip-label] {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
:is([data-md-chip], [data-md-chip-action]) > [data-md-chip-icon] {
|
||||
margin-inline-end: var(--md-sys-measurement-space100);
|
||||
}
|
||||
|
||||
[data-md-chip-trailing] {
|
||||
margin-inline-start: var(--md-sys-measurement-space100);
|
||||
}
|
||||
|
||||
[data-md-chip]:not([data-md-chip='input'], [data-md-disabled]) [data-md-chip-icon],
|
||||
[data-md-chip='input'][data-md-selected]:not([data-md-disabled]) [data-md-chip-icon],
|
||||
:is([data-md-chip='assist'], [data-md-chip='suggestion']):not([data-md-disabled]) [data-md-chip-trailing] {
|
||||
color: var(--md-sys-color-primary);
|
||||
}
|
||||
|
||||
[data-md-chip][data-md-actionable] [data-md-chip-icon] {
|
||||
transition: color var(--md-sys-motion-effects-fast-duration) var(--md-sys-motion-effects-fast);
|
||||
}
|
||||
|
||||
[data-md-chip][data-md-actionable]:is(:active, :has(:focus-visible)) [data-md-chip-icon] {
|
||||
color: var(--md-sys-color-primary);
|
||||
}
|
||||
|
||||
@media (hover: hover) {
|
||||
[data-md-chip][data-md-actionable]:hover [data-md-chip-icon] {
|
||||
color: var(--md-sys-color-primary);
|
||||
}
|
||||
}
|
||||
|
||||
/* The filter chip's check: from nothing to 18px as it is selected, or over the icon it replaces. */
|
||||
[data-md-chip-check-slot] {
|
||||
display: grid;
|
||||
flex-shrink: 0;
|
||||
width: 0;
|
||||
height: 18px;
|
||||
margin-inline-end: var(--md-sys-measurement-space100);
|
||||
overflow: hidden;
|
||||
transition: width var(--md-sys-motion-effects-default-duration) var(--md-sys-motion-effects-default);
|
||||
}
|
||||
|
||||
[data-md-chip-check-slot] > * {
|
||||
grid-area: 1 / 1;
|
||||
}
|
||||
|
||||
[data-md-chip-check-slot]:has([data-md-chip-icon]) {
|
||||
width: 18px;
|
||||
transition: none;
|
||||
}
|
||||
|
||||
[data-md-chip]:is(:has(:checked), [aria-pressed='true']) [data-md-chip-check-slot]:not(:has([data-md-chip-icon])) {
|
||||
width: 18px;
|
||||
transition-duration: var(--md-sys-motion-spatial-fast-duration);
|
||||
transition-timing-function: var(--md-sys-motion-spatial-fast);
|
||||
}
|
||||
|
||||
[data-md-chip-check-slot] [data-md-chip-icon] {
|
||||
transition: opacity var(--md-sys-motion-effects-fast-duration) var(--md-sys-motion-effects-fast);
|
||||
}
|
||||
|
||||
[data-md-chip-check] {
|
||||
opacity: 0;
|
||||
transition: opacity var(--md-sys-motion-effects-fast-duration) var(--md-sys-motion-effects-fast);
|
||||
}
|
||||
|
||||
[data-md-chip]:is(:has(:checked), [aria-pressed='true']) [data-md-chip-check-slot] [data-md-chip-icon] {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
[data-md-chip]:is(:has(:checked), [aria-pressed='true']) [data-md-chip-check] {
|
||||
opacity: 1;
|
||||
transition-duration: var(--md-sys-motion-effects-slow-duration);
|
||||
transition-timing-function: var(--md-sys-motion-effects-slow);
|
||||
}
|
||||
|
||||
/* ---- Input chips: an action of their own, and a remove button ---------------------------- */
|
||||
|
||||
[data-md-chip='input'] {
|
||||
isolation: isolate;
|
||||
}
|
||||
|
||||
[data-md-chip='input']:not([data-md-selected], [data-md-disabled]):has([data-md-chip-action]:focus-visible) {
|
||||
border-color: var(--md-sys-color-on-surface-variant);
|
||||
}
|
||||
|
||||
[data-md-chip='input']:has([data-md-chip-action]:focus-visible) {
|
||||
outline: 3px solid var(--md-sys-color-secondary);
|
||||
outline-offset: 2px;
|
||||
}
|
||||
|
||||
[data-md-chip-action] {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
min-width: 0;
|
||||
height: 100%;
|
||||
padding-inline: 11px;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
[data-md-chip][data-md-icon-start] [data-md-chip-action] {
|
||||
padding-inline-start: 7px;
|
||||
}
|
||||
|
||||
[data-md-chip][data-md-avatar] [data-md-chip-action] {
|
||||
padding-inline-start: 3px;
|
||||
}
|
||||
|
||||
[data-md-chip][data-md-removable] [data-md-chip-action] {
|
||||
padding-inline-end: var(--md-sys-measurement-space100);
|
||||
}
|
||||
|
||||
:is(a, button)[data-md-chip-action] {
|
||||
cursor: pointer;
|
||||
|
||||
/* The 48px target and the state layer, across the whole chip. */
|
||||
&::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
inset-inline: 0;
|
||||
top: 50%;
|
||||
height: var(--md-sys-measurement-space600);
|
||||
translate: 0 -50%;
|
||||
}
|
||||
}
|
||||
|
||||
[data-md-chip]:not([data-md-disabled]) :is(a, button)[data-md-chip-action] {
|
||||
&::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
inset: -1px;
|
||||
z-index: -1;
|
||||
border-radius: var(--md-sys-shape-corner-sm);
|
||||
background-color: currentColor;
|
||||
opacity: 0;
|
||||
transition: opacity var(--md-sys-motion-effects-fast-duration) var(--md-sys-motion-effects-fast);
|
||||
}
|
||||
|
||||
@media (hover: hover) {
|
||||
&:hover::before {
|
||||
opacity: var(--md-sys-state-hover-state-layer-opacity);
|
||||
}
|
||||
}
|
||||
|
||||
&:focus-visible::before {
|
||||
opacity: var(--md-sys-state-focus-state-layer-opacity);
|
||||
}
|
||||
|
||||
&:active::before {
|
||||
opacity: var(--md-sys-state-pressed-state-layer-opacity);
|
||||
}
|
||||
}
|
||||
|
||||
[data-md-chip][data-md-disabled] :is(a, button)[data-md-chip-action] {
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
[data-md-chip][data-md-disabled] a[data-md-chip-action] {
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
[data-md-chip-avatar] {
|
||||
flex-shrink: 0;
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
margin-inline-end: var(--md-sys-measurement-space100);
|
||||
border-radius: var(--md-sys-shape-corner-full);
|
||||
}
|
||||
|
||||
span[data-md-chip-avatar] {
|
||||
display: grid;
|
||||
place-items: center;
|
||||
background-color: var(--md-sys-color-primary-container);
|
||||
color: var(--md-sys-color-on-primary-container);
|
||||
font: var(--md-sys-typescale-label-sm);
|
||||
letter-spacing: var(--md-sys-typescale-label-sm-tracking);
|
||||
font-variation-settings: normal;
|
||||
}
|
||||
|
||||
img[data-md-chip-avatar] {
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
[data-md-chip][data-md-disabled] [data-md-chip-avatar] {
|
||||
opacity: var(--md-sys-state-disabled-content-opacity);
|
||||
}
|
||||
|
||||
/* An 18px icon in a 24px state layer, catching presses 15px out on every side: M3's 48px. */
|
||||
[data-md-chip-remove] {
|
||||
position: relative;
|
||||
display: grid;
|
||||
flex-shrink: 0;
|
||||
place-items: center;
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
margin-inline-end: 7px;
|
||||
border-radius: var(--md-sys-shape-corner-full);
|
||||
outline: none;
|
||||
cursor: pointer;
|
||||
|
||||
&::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
inset: -15px;
|
||||
}
|
||||
|
||||
&:disabled {
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
&:not(:disabled) {
|
||||
&::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
inset: -3px;
|
||||
border-radius: var(--md-sys-shape-corner-full);
|
||||
background-color: currentColor;
|
||||
opacity: 0;
|
||||
transition: opacity var(--md-sys-motion-effects-fast-duration) var(--md-sys-motion-effects-fast);
|
||||
}
|
||||
|
||||
@media (hover: hover) {
|
||||
&:hover::before {
|
||||
opacity: var(--md-sys-state-hover-state-layer-opacity);
|
||||
}
|
||||
}
|
||||
|
||||
&:focus-visible {
|
||||
outline: 3px solid var(--md-sys-color-secondary);
|
||||
outline-offset: 2px;
|
||||
}
|
||||
|
||||
&:focus-visible::before {
|
||||
opacity: var(--md-sys-state-focus-state-layer-opacity);
|
||||
}
|
||||
|
||||
&:active::before {
|
||||
opacity: var(--md-sys-state-pressed-state-layer-opacity);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user