2.0.0 took Tailwind out of the stack, but the texts Boost copies into every application still taught its utilities: the core guideline said "built on Tailwind CSS", the always-on material-3 guideline wrote every rule as `bg-primary`, `type-*`, `rounded-corner-*`, `state-layer` and `medium:`, and both skills' tables and examples did the same. None of those classes exists in 2.0's stylesheets, so an agent following the guideline wrote markup that compiled to nothing. Found moving ReStride onto 2.0: its CLAUDE.md, and SealShare's, carry these texts. The guidelines and skills now teach what 2.0 has: component and layout component props (`gap="space200"`, `hide-from="medium"`), the `md-type-*`, `md-ink-*` and interaction classes, and `--md-sys-*` tokens in the application's own CSS, with breakpoints as range media queries. UPGRADE.md §1, §2 and §4 describe the finished move instead of the in-between state, and header comments that pointed at the removed tokens/utilities.css and tailwind.css, or called a component "still Tailwind", say what is true now. BoostVocabularyTest runs DesignGuard over the code in every shipped guideline and skill (fenced Blade and CSS, and each inline class list), so a text that teaches a class the stylesheets do not define fails the suite; it finds 268 violations in the texts as 2.0.0 shipped them. The guard's own table of what it reports is exempt. BoostResourcesTest now asks the design skill for each breakpoint's prop value and media query instead of the removed `medium:` variants. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
432 lines
17 KiB
CSS
432 lines
17 KiB
CSS
/*
|
|
* The menus: `<x-menu>` and what a form's own dropdowns wear too, so a menu and a form read as
|
|
* one family.
|
|
*
|
|
* Three lists wear it:
|
|
*
|
|
* - `<x-menu>`'s own popover (`[data-md-menu-popover]`) and, inside it, a submenu
|
|
* (`[data-md-submenu]`, menu-item.css).
|
|
* - `<x-select>`'s: the native <select>, opted into the browser's customizable select
|
|
* (`appearance: base-select`). `::picker(select)` is the menu and each <option> a row, while
|
|
* the keyboard, type-to-find, the screen reader and the form value stay the browser's own.
|
|
* - `<x-choices searchable>`'s listbox, which Alpine draws, as `[data-md-field-menu]` and
|
|
* `[data-md-field-option]`.
|
|
*
|
|
* M3's menu as its tokens have it (StandardMenuTokens/VibrantMenuTokens/SegmentedMenuTokens,
|
|
* androidx Compose Material 3, Apache-2.0): surface-container-low at elevation 2, large corner,
|
|
* 48px rows in body-large, the chosen row in secondary-container with a tick at its end (so it is
|
|
* not told by colour alone), and the state layer on hover (only where a pointer can hover) and
|
|
* focus.
|
|
*
|
|
* `[data-md-menu]` is `<x-menu>`'s root, holding the trigger and the popover; the popover itself
|
|
* is `[data-md-menu-popover]`, capped at 288px so a long menu scrolls instead of running off the
|
|
* top layer's edge (ACT-04), and transitions in by growing from its trigger's corner while it
|
|
* fades, on the spatial and effects springs respectively (ACT-26). `data-md-vibrant` swaps its
|
|
* container for tertiary-container; a nested submenu inherits the colour through
|
|
* `--material-menu-surface`/`--material-menu-ink`, which the sheet's own copy of the list falls
|
|
* back to instead (it is not a descendant of the popover).
|
|
*
|
|
* `sheet-at-compact`'s bottom sheet is `<x-bottom-sheet>` (bottom-sheet.css): `[data-md-menu-sheet]`
|
|
* cancels its 24px padding and restyles a submenu to open in place under its item instead of
|
|
* beside it (M3 calls submenus "best suited to large screens").
|
|
*
|
|
* Where the browser has no customizable select, the select keeps its native list, which
|
|
* `color-scheme` themes. Every rule for the select's own rows is inside `@supports`: a browser
|
|
* that paints option colours into its native list would otherwise show a half-painted one.
|
|
*/
|
|
|
|
@layer material.reset, material.tokens, material.base, material.layout, material.components, material.text, material.visibility;
|
|
|
|
@import './icon.css';
|
|
@import './bottom-sheet.css';
|
|
|
|
@layer material.components {
|
|
[data-md-menu] {
|
|
position: relative;
|
|
display: inline-flex;
|
|
}
|
|
|
|
[data-md-menu-trigger] {
|
|
display: inline-flex;
|
|
}
|
|
|
|
[data-md-menu-popover] {
|
|
--material-menu-surface: var(--md-sys-color-surface-container-low);
|
|
--material-menu-ink: var(--md-sys-color-on-surface);
|
|
|
|
inset: auto;
|
|
margin: 0;
|
|
margin-block: var(--md-sys-measurement-space50);
|
|
min-inline-size: 112px;
|
|
max-inline-size: 280px;
|
|
max-block-size: min(288px, calc(100dvh - 32px));
|
|
overflow-y: auto;
|
|
border-width: 0;
|
|
border-radius: var(--md-sys-shape-corner-lg);
|
|
padding: var(--md-sys-measurement-space50);
|
|
background-color: var(--material-menu-surface);
|
|
color: var(--material-menu-ink);
|
|
box-shadow: var(--md-sys-elevation-2);
|
|
position-try-fallbacks: flip-block, flip-inline, flip-block flip-inline;
|
|
opacity: 0;
|
|
scale: 0.95;
|
|
transition-property: opacity, scale, display, overlay;
|
|
transition-duration:
|
|
var(--md-sys-motion-effects-fast-duration), var(--md-sys-motion-spatial-fast-duration),
|
|
var(--md-sys-motion-spatial-fast-duration), var(--md-sys-motion-spatial-fast-duration);
|
|
transition-timing-function:
|
|
var(--md-sys-motion-effects-fast), var(--md-sys-motion-spatial-fast), linear, linear;
|
|
transition-behavior: allow-discrete;
|
|
|
|
&:popover-open {
|
|
opacity: 1;
|
|
scale: 1;
|
|
}
|
|
|
|
@starting-style {
|
|
&:popover-open {
|
|
opacity: 0;
|
|
scale: 0.95;
|
|
}
|
|
}
|
|
}
|
|
|
|
[data-md-menu-popover]:has(> [data-md-menu-filter]) {
|
|
padding: 0;
|
|
}
|
|
|
|
[data-md-menu-popover][data-md-vibrant] {
|
|
--material-menu-surface: var(--md-sys-color-tertiary-container);
|
|
--material-menu-ink: var(--md-sys-color-on-tertiary-container);
|
|
}
|
|
|
|
[data-md-menu-popover][data-md-position='bottom-start'] {
|
|
transform-origin: top;
|
|
position-area: bottom span-right;
|
|
}
|
|
|
|
[data-md-menu-popover][data-md-position='bottom-end'] {
|
|
transform-origin: top;
|
|
position-area: bottom span-left;
|
|
}
|
|
|
|
[data-md-menu-popover][data-md-position='top-start'] {
|
|
transform-origin: bottom;
|
|
position-area: top span-right;
|
|
}
|
|
|
|
[data-md-menu-popover][data-md-position='top-end'] {
|
|
transform-origin: bottom;
|
|
position-area: top span-left;
|
|
}
|
|
|
|
/*
|
|
* `<x-menu filter>`: M3's menu as a filtering surface. The field stays put while the list
|
|
* scrolls under it and takes the menu's own container, so a vibrant menu's field is vibrant
|
|
* too. It is a plain <input> with no field chrome — M3's menus embed a text field, not a text
|
|
* field component.
|
|
*/
|
|
[data-md-menu-filter] {
|
|
position: sticky;
|
|
inset-block-start: 0;
|
|
z-index: 1;
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 12px;
|
|
min-block-size: var(--md-sys-measurement-space600);
|
|
padding-inline: var(--md-sys-measurement-space200);
|
|
border-block-end: 1px solid var(--md-sys-color-outline-variant);
|
|
background-color: var(--material-menu-surface, var(--md-sys-color-surface-container-low));
|
|
}
|
|
|
|
[data-md-menu-filter] input {
|
|
flex: 1;
|
|
min-inline-size: 0;
|
|
border-width: 0;
|
|
background-color: transparent;
|
|
padding: 0;
|
|
color: inherit;
|
|
font: var(--md-sys-typescale-body-lg);
|
|
letter-spacing: var(--md-sys-typescale-body-lg-tracking);
|
|
font-variation-settings: normal;
|
|
outline: none;
|
|
}
|
|
|
|
[data-md-menu-filter] input::placeholder {
|
|
color: color-mix(in srgb, var(--material-menu-ink, var(--md-sys-color-on-surface)) 70%, transparent);
|
|
}
|
|
|
|
[data-md-menu-list] {
|
|
padding: var(--md-sys-measurement-space50);
|
|
}
|
|
|
|
[data-md-menu-empty] {
|
|
padding-inline: var(--md-sys-measurement-space200);
|
|
padding-block: 12px;
|
|
color: var(--md-sys-color-on-surface-variant);
|
|
font: var(--md-sys-typescale-body-md);
|
|
letter-spacing: var(--md-sys-typescale-body-md-tracking);
|
|
font-variation-settings: normal;
|
|
}
|
|
|
|
/* The filter hides what the query leaves out with the `hidden` attribute; a row's own
|
|
`display: flex` would otherwise beat the user agent's `[hidden] { display: none }`. */
|
|
:is([data-md-menu-popover], [data-md-menu-sheet]) [hidden] {
|
|
display: none;
|
|
}
|
|
|
|
/* `data-md-active` is the row the arrow keys are on while the focus stays in the field — the
|
|
state layer the roving focus would have drawn, without taking the focus off the field. A
|
|
chosen or current row keeps its own colour. */
|
|
:is([data-md-menu-popover], [data-md-menu-sheet]) [role^="menuitem"][data-md-active]:not([aria-checked="true"], [aria-current="page"]) {
|
|
background-color: color-mix(in srgb, var(--material-menu-ink, var(--md-sys-color-on-surface)) 8%, transparent);
|
|
}
|
|
|
|
/*
|
|
* `<x-menu sheet-at-compact>`: the copy of the list in the bottom sheet a compact window opens
|
|
* instead of the popover. The sheet is surface-container-low, as the standard menu is, so the
|
|
* fallbacks above already paint its field; the field reaches both edges of the sheet and holds
|
|
* its icon where the rows hold theirs, 24px in (the list is 8px in, a row's own padding 16px).
|
|
* `-24px` cancels the bottom sheet's own padding.
|
|
*/
|
|
[data-md-menu-sheet] {
|
|
margin-inline: calc(-1 * var(--md-sys-measurement-space300));
|
|
padding-inline: var(--md-sys-measurement-space100);
|
|
}
|
|
|
|
[data-md-menu-sheet]:has(> [data-md-menu-filter]) {
|
|
padding-inline: 0;
|
|
}
|
|
|
|
[data-md-menu-sheet] [data-md-menu-filter] {
|
|
padding-inline: var(--md-sys-measurement-space300);
|
|
}
|
|
|
|
[data-md-menu-sheet] [data-md-menu-list] {
|
|
padding-inline: var(--md-sys-measurement-space100);
|
|
padding-block: var(--md-sys-measurement-space50) 0;
|
|
}
|
|
|
|
/*
|
|
* A submenu there opens in place: its list stands under the item, inset by 16px, instead of in
|
|
* a popover beside it, which a sheet has no room for (M3 calls submenus "best suited to large
|
|
* screens", docs/reference/m3/components-actions-communication-containment.md § Menus). The
|
|
* markup is the popover's, never shown as one: the user agent's `[popover]` box is undone, the
|
|
* list is shown while its item says `aria-expanded="true"`, and it fades in on the effects
|
|
* track.
|
|
*/
|
|
[data-md-menu-sheet] [data-md-submenu] {
|
|
position: static;
|
|
inset: auto;
|
|
inline-size: auto;
|
|
max-inline-size: none;
|
|
block-size: auto;
|
|
max-block-size: none;
|
|
margin: 0;
|
|
padding-block: var(--md-sys-measurement-space25);
|
|
padding-inline: var(--md-sys-measurement-space200) 0;
|
|
overflow: visible;
|
|
border-radius: 0;
|
|
background-color: transparent;
|
|
color: inherit;
|
|
box-shadow: none;
|
|
opacity: 1;
|
|
scale: none;
|
|
transition: opacity var(--md-sys-motion-effects-fast-duration) var(--md-sys-motion-effects-fast);
|
|
}
|
|
|
|
[data-md-menu-sheet] [aria-expanded="true"] + [data-md-submenu] {
|
|
display: block;
|
|
|
|
@starting-style {
|
|
opacity: 0;
|
|
}
|
|
}
|
|
|
|
/* The chevron turns from the way a popover would open to the way this list did: down, which
|
|
is a quarter turn the other way on the chevron `mirror-rtl` mirrors too. */
|
|
[data-md-menu-sheet] [data-md-submenu-chevron] {
|
|
transition: rotate var(--md-sys-motion-spatial-fast-duration) var(--md-sys-motion-spatial-fast);
|
|
}
|
|
|
|
[data-md-menu-sheet] [aria-expanded="true"] > [data-md-submenu-chevron] {
|
|
rotate: 90deg;
|
|
|
|
&:is([dir='rtl'], [dir='rtl'] *) {
|
|
rotate: -90deg;
|
|
}
|
|
}
|
|
|
|
[data-md-field-menu] {
|
|
max-block-size: 18rem;
|
|
overflow-y: auto;
|
|
padding-block: var(--md-sys-measurement-space100);
|
|
border-radius: var(--md-sys-shape-corner-xs);
|
|
background-color: var(--md-sys-color-surface-container);
|
|
box-shadow: var(--md-sys-elevation-2);
|
|
}
|
|
|
|
/* As a popover (`<x-choices searchable>`), it hangs under its field, as wide, and goes above
|
|
only when there is no room below. */
|
|
[data-md-field-menu][popover] {
|
|
inset: auto;
|
|
top: anchor(bottom);
|
|
left: anchor(left);
|
|
width: anchor-size(width);
|
|
margin: var(--md-sys-measurement-space50) 0;
|
|
border-width: 0;
|
|
padding-inline: 0;
|
|
color: var(--md-sys-color-on-surface);
|
|
position-try-fallbacks: flip-block;
|
|
}
|
|
|
|
[data-md-field-option][aria-disabled="true"] {
|
|
color: color-mix(in srgb, var(--md-sys-color-on-surface) 38%, transparent);
|
|
cursor: default;
|
|
}
|
|
|
|
[data-md-field-option] {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 12px;
|
|
min-block-size: var(--md-sys-measurement-space600);
|
|
padding-inline: var(--md-sys-measurement-space200);
|
|
color: var(--md-sys-color-on-surface);
|
|
font: var(--md-sys-typescale-body-lg);
|
|
letter-spacing: var(--md-sys-typescale-body-lg-tracking);
|
|
font-variation-settings: normal;
|
|
cursor: pointer;
|
|
}
|
|
|
|
/* `data-md-active` is the row the arrow keys or the pointer are on. */
|
|
[data-md-field-option][data-md-active] {
|
|
background-color: color-mix(in srgb, var(--md-sys-color-on-surface) 8%, transparent);
|
|
}
|
|
|
|
[data-md-field-option][aria-selected="true"] {
|
|
background-color: var(--md-sys-color-secondary-container);
|
|
color: var(--md-sys-color-on-secondary-container);
|
|
}
|
|
|
|
[data-md-field-option][aria-selected="true"][data-md-active] {
|
|
background-color: color-mix(in srgb, var(--md-sys-color-on-secondary-container) 8%, var(--md-sys-color-secondary-container));
|
|
}
|
|
|
|
[data-md-field-check] {
|
|
margin-inline-start: auto;
|
|
}
|
|
|
|
[data-md-field-option]:not([aria-selected="true"]) [data-md-field-check] {
|
|
visibility: hidden;
|
|
}
|
|
|
|
@supports (appearance: base-select) {
|
|
/* The select itself opts in from select.css, after its own `appearance: none`: both sit in
|
|
`material.components` with the same specificity, and select.css's block always comes
|
|
after this file, which it imports. */
|
|
select[data-md-field-control]::picker(select) {
|
|
appearance: base-select;
|
|
}
|
|
|
|
/* The menu. The browser anchors it to the select; the select covers its whole
|
|
field (field.css), so the menu is at least the field's width. It hangs under
|
|
the field and goes above only when it does not fit there — the browser's own
|
|
order tries the roomier side first, which opened a menu upwards with room to
|
|
spare under it. It opens with a fade and a grow on the spatial spring and
|
|
closes with a fade, as `<x-menu>` does. */
|
|
select[data-md-field-control]::picker(select) {
|
|
position-try-order: normal;
|
|
max-block-size: 18rem;
|
|
max-inline-size: calc(100vw - 32px);
|
|
margin-block: var(--md-sys-measurement-space50);
|
|
padding-block: var(--md-sys-measurement-space100);
|
|
border-width: 0;
|
|
border-radius: var(--md-sys-shape-corner-xs);
|
|
background-color: var(--md-sys-color-surface-container);
|
|
color: var(--md-sys-color-on-surface);
|
|
box-shadow: var(--md-sys-elevation-2);
|
|
opacity: 0;
|
|
transform-origin: top;
|
|
transition-property: opacity, display, overlay;
|
|
transition-duration: var(--md-sys-motion-effects-fast-duration);
|
|
transition-timing-function: var(--md-sys-motion-effects-fast);
|
|
transition-behavior: allow-discrete;
|
|
}
|
|
|
|
select[data-md-field-control]:open::picker(select) {
|
|
opacity: 1;
|
|
transition-property: opacity, scale, display, overlay;
|
|
transition-duration: var(--md-sys-motion-spatial-fast-duration);
|
|
transition-timing-function: var(--md-sys-motion-spatial-fast);
|
|
}
|
|
|
|
@starting-style {
|
|
select[data-md-field-control]:open::picker(select) {
|
|
opacity: 0;
|
|
scale: 0.95;
|
|
}
|
|
}
|
|
|
|
select[data-md-field-control] option {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 12px;
|
|
min-block-size: var(--md-sys-measurement-space600);
|
|
padding-block: 0;
|
|
padding-inline: var(--md-sys-measurement-space200);
|
|
background-color: transparent;
|
|
color: var(--md-sys-color-on-surface);
|
|
font: var(--md-sys-typescale-body-lg);
|
|
letter-spacing: var(--md-sys-typescale-body-lg-tracking);
|
|
font-variation-settings: normal;
|
|
outline: none;
|
|
cursor: pointer;
|
|
}
|
|
|
|
@media (hover: hover) {
|
|
select[data-md-field-control] option:hover {
|
|
background-color: color-mix(in srgb, var(--md-sys-color-on-surface) 8%, transparent);
|
|
}
|
|
}
|
|
|
|
select[data-md-field-control] option:focus-visible {
|
|
background-color: color-mix(in srgb, var(--md-sys-color-on-surface) 10%, transparent);
|
|
}
|
|
|
|
select[data-md-field-control] option:checked {
|
|
background-color: var(--md-sys-color-secondary-container);
|
|
color: var(--md-sys-color-on-secondary-container);
|
|
}
|
|
|
|
@media (hover: hover) {
|
|
select[data-md-field-control] option:checked:hover {
|
|
background-color: color-mix(in srgb, var(--md-sys-color-on-secondary-container) 8%, var(--md-sys-color-secondary-container));
|
|
}
|
|
}
|
|
|
|
select[data-md-field-control] option:checked:focus-visible {
|
|
background-color: color-mix(in srgb, var(--md-sys-color-on-secondary-container) 10%, var(--md-sys-color-secondary-container));
|
|
}
|
|
|
|
select[data-md-field-control] option:disabled {
|
|
background-color: transparent;
|
|
color: color-mix(in srgb, var(--md-sys-color-on-surface) 38%, transparent);
|
|
cursor: default;
|
|
}
|
|
|
|
/* The tick, at the row's end: the browser draws it before the label, and hides
|
|
it on every row but the chosen one. The Material Symbol, as a mask, so it
|
|
takes the row's ink. */
|
|
select[data-md-field-control] option::checkmark {
|
|
content: "";
|
|
order: 1;
|
|
flex: none;
|
|
inline-size: var(--md-sys-measurement-space300);
|
|
block-size: var(--md-sys-measurement-space300);
|
|
margin-inline-start: auto;
|
|
background-color: currentColor;
|
|
mask: url("../../svg/symbols/outlined/check.svg") center / contain no-repeat;
|
|
}
|
|
}
|
|
}
|