Draw the account menu without Tailwind
Plan step 36 (navigation group, third batch): <x-account-menu>'s class lists move into resources/css/components/account-menu.css, keyed on data-md-account-menu (the avatar trigger), data-md-account-menu-avatar and data-md-account-menu-header. The trigger already carried the target-extension and state-layer fixes from an earlier pass (N-01, N-15); this rewrite only moves them onto the shared md-state-layer/ md-touch-target/md-focus-ring classes and their z-index-ordering comment into the stylesheet. The header's name and email keep their type and ink as the fixed text classes on the view (md-type-title-sm/md-ink, md-type-body-sm/md-ink-variant), since they are the caller's own prose. Hooks renamed: data-account-menu to data-md-account-menu, data-account-theme to data-md-account-menu-theme, updated in AppBarTest.php, which carries account-menu's render tests. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Qwx5USif3wFFmxtHg5U1g9
This commit is contained in:
co-authored by
Claude Sonnet 5
parent
f1cd6e4eb4
commit
f5c90c3874
@@ -79,3 +79,4 @@
|
|||||||
@import './components/navigation-rail-item.css';
|
@import './components/navigation-rail-item.css';
|
||||||
@import './components/navigation-rail-section.css';
|
@import './components/navigation-rail-section.css';
|
||||||
@import './components/section-nav.css';
|
@import './components/section-nav.css';
|
||||||
|
@import './components/account-menu.css';
|
||||||
|
|||||||
@@ -0,0 +1,53 @@
|
|||||||
|
/*
|
||||||
|
* <x-account-menu>: the avatar trigger of an `<x-menu>`, its name-and-email header and its theme
|
||||||
|
* item — an M3 "extra" the audit found already inside the foundations (docs/audits/m3-alignment/
|
||||||
|
* navigation.md): a 40px trigger reaching M3's 48px target through `md-touch-target` (N-01), with
|
||||||
|
* `md-state-layer` for the hover and pressed feedback every other trigger in the library already
|
||||||
|
* has (N-15) and `md-focus-ring` for its keyboard ring. The avatar image sits behind the state
|
||||||
|
* layer's `::before` (z-index -1, foundation/interaction.css) at z-index -2, so a hover or a press
|
||||||
|
* still washes over the picture instead of under it; nothing clips the pseudo-target the touch
|
||||||
|
* class draws past the 40px circle.
|
||||||
|
*
|
||||||
|
* `[data-md-account-menu-header]` is the name-and-email block above the separator; its own type and
|
||||||
|
* ink are the fixed text classes on the view (`md-type-title-sm md-ink`, `md-type-body-sm
|
||||||
|
* md-ink-variant`), since this is prose the caller supplies, not the component's own part.
|
||||||
|
*/
|
||||||
|
|
||||||
|
@layer material.reset, material.tokens, material.base, material.layout, material.components, material.text, material.visibility;
|
||||||
|
|
||||||
|
@import './menu.css';
|
||||||
|
@import './menu-separator.css';
|
||||||
|
@import './menu-item.css';
|
||||||
|
@import './icon.css';
|
||||||
|
|
||||||
|
@layer material.components {
|
||||||
|
[data-md-account-menu] {
|
||||||
|
display: inline-flex;
|
||||||
|
flex-shrink: 0;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
inline-size: var(--md-sys-measurement-space500);
|
||||||
|
block-size: var(--md-sys-measurement-space500);
|
||||||
|
border-radius: var(--md-sys-shape-corner-full);
|
||||||
|
cursor: pointer;
|
||||||
|
background-color: var(--md-sys-color-primary-container);
|
||||||
|
color: var(--md-sys-color-on-primary-container);
|
||||||
|
font: var(--md-sys-typescale-label-lg);
|
||||||
|
letter-spacing: var(--md-sys-typescale-label-lg-tracking);
|
||||||
|
font-variation-settings: normal;
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-md-account-menu-avatar] {
|
||||||
|
position: relative;
|
||||||
|
z-index: -2;
|
||||||
|
inline-size: 100%;
|
||||||
|
block-size: 100%;
|
||||||
|
border-radius: var(--md-sys-shape-corner-full);
|
||||||
|
object-fit: cover;
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-md-account-menu-header] {
|
||||||
|
padding-inline: 12px;
|
||||||
|
padding-block: var(--md-sys-measurement-space100) 12px;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -37,13 +37,13 @@
|
|||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
aria-label="{{ $label }}"
|
aria-label="{{ $label }}"
|
||||||
data-account-menu
|
data-md-account-menu
|
||||||
class="state-layer touch-target focus-ring inline-flex size-10 shrink-0 cursor-pointer items-center justify-center rounded-corner-full bg-primary-container type-label-lg text-on-primary-container"
|
class="md-state-layer md-touch-target md-focus-ring"
|
||||||
>
|
>
|
||||||
@if ($image)
|
@if ($image)
|
||||||
{{-- Behind the state layer (`state-layer` paints its ::before at z-index -1), so an
|
{{-- Behind the state layer (md-state-layer paints its ::before at z-index -1), so
|
||||||
avatar image still shows the hover and pressed states over it. --}}
|
an avatar image still shows the hover and pressed states over it. --}}
|
||||||
<img src="{{ $avatar }}" alt="" class="relative -z-20 size-full rounded-corner-full object-cover" />
|
<img src="{{ $avatar }}" alt="" data-md-account-menu-avatar />
|
||||||
@elseif (filled($initials))
|
@elseif (filled($initials))
|
||||||
{{ $initials }}
|
{{ $initials }}
|
||||||
@else
|
@else
|
||||||
@@ -53,12 +53,12 @@
|
|||||||
</x-slot:trigger>
|
</x-slot:trigger>
|
||||||
|
|
||||||
@if (filled($name) || filled($email))
|
@if (filled($name) || filled($email))
|
||||||
<div class="px-3 pt-2 pb-3">
|
<div data-md-account-menu-header>
|
||||||
@if (filled($name))
|
@if (filled($name))
|
||||||
<p class="truncate type-title-sm text-on-surface">{{ $name }}</p>
|
<p class="md-truncate md-type-title-sm md-ink">{{ $name }}</p>
|
||||||
@endif
|
@endif
|
||||||
@if (filled($email))
|
@if (filled($email))
|
||||||
<p class="truncate type-body-sm text-on-surface-variant">{{ $email }}</p>
|
<p class="md-truncate md-type-body-sm md-ink-variant">{{ $email }}</p>
|
||||||
@endif
|
@endif
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -68,7 +68,7 @@
|
|||||||
{{ $slot }}
|
{{ $slot }}
|
||||||
|
|
||||||
@if ($theme)
|
@if ($theme)
|
||||||
<x-livewire-material::menu-item icon="contrast" x-on:click="$store.theme.toggle()" data-account-theme>
|
<x-livewire-material::menu-item icon="contrast" x-on:click="$store.theme.toggle()" data-md-account-menu-theme>
|
||||||
<span x-text="$store.theme.resolved === 'dark' ? @js(__('Light theme')) : @js(__('Dark theme'))">{{ __('Theme') }}</span>
|
<span x-text="$store.theme.resolved === 'dark' ? @js(__('Light theme')) : @js(__('Dark theme'))">{{ __('Theme') }}</span>
|
||||||
</x-livewire-material::menu-item>
|
</x-livewire-material::menu-item>
|
||||||
@endif
|
@endif
|
||||||
|
|||||||
@@ -268,17 +268,27 @@ it('opens an account menu from the initials of a name', function () {
|
|||||||
|
|
||||||
expect($html)
|
expect($html)
|
||||||
->toContain('aria-label="Account"')
|
->toContain('aria-label="Account"')
|
||||||
->toContain('data-account-menu')
|
->toContain('data-md-account-menu')
|
||||||
// The 40px avatar reaches 48px, and nothing clips the pseudo-target (N-01); the trigger
|
// The 40px avatar reaches 48px, and nothing clips the pseudo-target (N-01); the trigger
|
||||||
// has a hover and a pressed state like every other trigger in the library (N-15).
|
// has a hover and a pressed state like every other trigger in the library (N-15).
|
||||||
->toContain('state-layer touch-target focus-ring')
|
->toContain('md-state-layer md-touch-target md-focus-ring')
|
||||||
->not->toContain('overflow-hidden')
|
->not->toContain('overflow-hidden')
|
||||||
->toMatch('/>\s*AM\s*<\/button>/')
|
->toMatch('/>\s*AM\s*<\/button>/')
|
||||||
->toContain('anna@example.com')
|
->toContain('anna@example.com')
|
||||||
->toContain('Settings')
|
->toContain('Settings')
|
||||||
->toContain('data-account-theme')
|
->toContain('data-md-account-menu-theme')
|
||||||
->toContain('Sign out')
|
->toContain('Sign out')
|
||||||
->and((string) $this->blade('<x-account-menu avatar="/anna.jpg" :theme="false" />'))
|
->and((string) $this->blade('<x-account-menu avatar="/anna.jpg" :theme="false" />'))
|
||||||
->toContain('<img src="/anna.jpg" alt="" class="relative -z-20 size-full rounded-corner-full object-cover" />')
|
->toContain('<img src="/anna.jpg" alt="" data-md-account-menu-avatar />')
|
||||||
->not->toContain('data-account-theme');
|
->not->toContain('data-md-account-menu-theme');
|
||||||
|
|
||||||
|
$css = ComponentStylesheet::read('account-menu');
|
||||||
|
|
||||||
|
expect($css->declarations('[data-md-account-menu]'))
|
||||||
|
->toHaveKey('inline-size', 'var(--md-sys-measurement-space500)')
|
||||||
|
->toHaveKey('border-radius', 'var(--md-sys-shape-corner-full)')
|
||||||
|
->and($css->declarations('[data-md-account-menu-avatar]'))
|
||||||
|
// Behind the state layer's ::before (z-index -1), so a hover or a press still washes over
|
||||||
|
// the picture instead of under it (N-15).
|
||||||
|
->toHaveKey('z-index', '-2');
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -33,6 +33,7 @@ dataset('navigation components', [
|
|||||||
'navigation-rail-item',
|
'navigation-rail-item',
|
||||||
'navigation-rail-section',
|
'navigation-rail-section',
|
||||||
'section-nav',
|
'section-nav',
|
||||||
|
'account-menu',
|
||||||
]);
|
]);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
Reference in New Issue
Block a user