style: Migliorato design, top app bar e drawer

This commit is contained in:
Maicol Battistini 2023-12-08 17:02:25 +01:00
parent b5deeda68b
commit e059c4fec5
No known key found for this signature in database
14 changed files with 747 additions and 148 deletions

View File

@ -12,8 +12,8 @@
"scss-types": "typed-scss-modules resources/scss -i node_modules"
},
"dependencies": {
"@formkit/auto-animate": "0.8.0",
"@formkit/auto-animate": "0.8.1",
"@lit-labs/motion": "^1.0.6",
"@maicol07/inertia-mithril": "^1.0.3",
"@maicol07/material-web-additions": "^1.4.4",
"@material/mwc-snackbar": "^0.27.0",

View File

@ -8,24 +8,14 @@ md-icon svg {
line-height: 0;
}
md-list-item md-icon {
margin-left: 16px;
color: var(--_leading-icon-color);
svg {
width: var(--_leading-icon-size, var(--_icon-size, var(--_size)));
height: var(--_leading-icon-size, var(--_icon-size, var(--_size)));
}
}
md-data-table md-icon[slot^="pagination"] {
--_color: inherit;
}
label:has(> :is(md-checkbox, md-switch)) {
display: flex;
align-items: center;
gap: 8px;
align-items: center;
}
md-filled-text-field, md-outlined-text-field {
@ -36,45 +26,52 @@ md-filled-select::part(menu), md-outlined-select::part(menu) {
max-height: 300px;
}
md-navigation-drawer, md-navigation-drawer-modal {
> md-list {
display: flex;
justify-content: center;
min-width: 300px;
nav-drawer md-list.nav {
--md-list-container-color: transparent;
display: block;
margin-inline: var(--nav-drawer-list-spacing, 12px);
min-width: unset;
md-list-item {
--md-list-item-container-shape: 100px;
--md-ripple-shape: 100px;
md-list-item[href] {
margin-block: var(--nav-drawer-list-spacing, 12px);
display: block;
--md-focus-ring-shape: var(--nav-drawer-items-border-radius, 28px);
border-radius: var(--nav-drawer-items-border-radius, 28px);
display: block;
width: 85%;
@media (forced-colors: active) {
border-radius: var(--nav-drawer-items-border-radius, 28px);
border: 1px solid CanvasText;
}
&[active] {
--md-list-item-container-color: var(--md-sys-color-secondary-container, #3f51b5);
}
&[selected] {
background-color: var(--md-sys-color-surface-container-highest);
md-icon[slot="start"] svg {
// TODO: Waiting for token implementation in Material Web
// (see https://m3.material.io/components/navigation-drawer/specs#ce8bfbcf-3dec-45d2-9d8b-5e10af1cf87d)
width: 24px;
height: 24px;
@media (forced-colors: active) {
border: 4px double CanvasText;
}
}
}
}
top-app-bar {
--mdc-theme-primary: var(--md-sys-color-surface, #fff);
--mdc-theme-on-primary: var(--md-sys-color-on-surface, #5f6368);
width: 100%;
[slot="navigationIcon"], [slot="title"] {
--md-icon-color: var(--md-sys-color-on-surface, #5f6368);
md-list-item::part(focus-ring) {
--md-focus-ring-shape: var(--nav-drawer-items-border-radius, 28px);
}
[slot="actionItems"] {
--md-icon-color: var(--md-sys-color-on-surface-variant, #5f6368);
md-item {
font-size: var(--nav-drawer-item-font-size, 24px);
padding-block-end: 0;
&[slot="headline"] {
/* shadow root slot has overflow:hidden, it's cutting some text off */
padding: block 2px;
}
&:first-of-type {
padding-block: 0;
}
& + md-list-item[href] {
margin-block-start: 0;
}
}
}

View File

@ -5,22 +5,13 @@
@import "theme/theme";
main {
flex: 1 0;
}
top-app-bar main {
margin-right: 16px;
}
body {
margin: 0 !important;
}
#app {
[slot="app-content"] {
display: grid;
grid-template-rows: 1fr auto;
min-height: 100vh;
}
.center-logo {

View File

@ -7,8 +7,8 @@
body {
@extend .body-large;
color: var(--md-sys-color-on-background);
background-color: var(--md-sys-color-background, #fff);
color: var(--md-sys-color-on-surface);
background-color: var(--md-sys-color-surface-container, #fff);
}
small {

View File

@ -13,7 +13,7 @@ import {
} from 'mithril-utilities';
import logoUrl from '../../images/logo_completo.png';
import TopAppBar from './layout/TopAppBar';
import Scaffold from '@osm/Components/layout/Scaffold';
export interface PageAttributes<A extends Record<string, any> & {external?: boolean} = Record<string, any>> extends Attributes, Required<ComponentAttributes<A>> {
}
@ -65,11 +65,10 @@ export default abstract class Page<A extends PageAttributes = PageAttributes> ex
{contents}
</md-elevated-card>
</div>
) : <>
<TopAppBar>
) : (
<Scaffold>
{contents}
</TopAppBar>
<Footer/>
</>;
</Scaffold>
);
}
}

View File

@ -1,13 +1,10 @@
import '@material/web/list/list.js';
import '../m3/NavigationDrawer';
import '../m3/NavigationDrawerModal';
import '../../WebComponents/NavDrawer';
import {
mdiAccountGroupOutline,
mdiMenuOpen,
mdiViewDashboardOutline
} from '@mdi/js';
import MdIcon from '@osm/Components/MdIcon';
import {VnodeCollectionItem} from '@osm/typings/jsx';
import {isMobile} from '@osm/utils/misc';
import {collect} from 'collect.js';
@ -21,7 +18,7 @@ import {
Component
} from 'mithril-utilities';
import {DrawerEntry} from './DrawerEntry';
import {DrawerItem} from './DrawerItem';
export interface DrawerAttributes extends Attributes {
open: Stream<boolean>;
@ -36,24 +33,27 @@ export default class Drawer<A extends DrawerAttributes = DrawerAttributes> exten
}
view(vnode: Vnode<A>): Children {
// noinspection LocalVariableNamingConventionJS
const DrawerTag = isMobile() ? 'md-navigation-drawer-modal' : 'md-navigation-drawer';
return (
<DrawerTag opened={this.open()}>
{DrawerTag === 'md-navigation-drawer-modal' && <md-icon-button onclick={this.onMobileMenuButtonClick.bind(this)}><MdIcon icon={mdiMenuOpen}/></md-icon-button>}
<md-list>{this.entries().values<VnodeCollectionItem>().all()}</md-list>
</DrawerTag>
<nav-drawer {...vnode.attrs} open={this.open()} onclose={this.onDrawerClose.bind(this)}>
{vnode.children}
<md-list
aria-label="List of pages"
role="menubar"
class="nav">
{this.entries().values<VnodeCollectionItem>().all()}
</md-list>
</nav-drawer>
);
}
entries() {
return collect<VnodeCollectionItem>({
dashboard: <DrawerEntry href={route('dashboard')} icon={mdiViewDashboardOutline}>{__('Dashboard')}</DrawerEntry>,
users: <DrawerEntry href={route('users.index')} icon={mdiAccountGroupOutline}>{__('Utenti')}</DrawerEntry>
dashboard: <DrawerItem href={route('dashboard')} icon={mdiViewDashboardOutline}>{__('Dashboard')}</DrawerItem>,
users: <DrawerItem href={route('users.index')} icon={mdiAccountGroupOutline}>{__('Utenti')}</DrawerItem>
});
}
onMobileMenuButtonClick() {
this.open(!this.open());
onDrawerClose() {
this.open(false);
}
}

View File

@ -1,39 +0,0 @@
import '@material/web/icon/icon.js';
import '@material/web/list/list-item.js';
import {router} from '@maicol07/inertia-mithril';
import {ListItemEl} from '@material/web/list/internal/listitem/list-item';
import type * as MaterialIcons from '@mdi/js';
import MdIcon from '@osm/Components/MdIcon';
import {Vnode} from 'mithril';
import {
Attributes,
Component
} from 'mithril-utilities';
import {ValueOf} from 'type-fest';
type Icons = ValueOf<typeof MaterialIcons>;
export interface DrawerEntryAttributes extends Attributes, Partial<ListItemEl> {
href: ListItemEl['href'];
icon: Icons;
}
export class DrawerEntry<A extends DrawerEntryAttributes = DrawerEntryAttributes> extends Component<A> {
view(vnode: Vnode<A>) {
return (
<md-list-item headline={vnode.children as string} active={this.isRouteActive(vnode.attrs.href)} href={vnode.attrs.href} onclick={this.navigateToRoute.bind(this)} {...this.attrs.all()}>
<MdIcon icon={vnode.attrs.icon} slot="start"/>
</md-list-item>
);
}
isRouteActive(href: string) {
return route(route().current()!, route().params) === href;
}
navigateToRoute(event: Event) {
event.preventDefault();
router.visit((event.target as ListItemEl).href);
}
}

View File

@ -0,0 +1,53 @@
import '@material/web/icon/icon.js';
import '@material/web/list/list-item.js';
import {router} from '@maicol07/inertia-mithril';
import {ListItemEl} from '@material/web/list/internal/listitem/list-item';
import type * as MaterialIcons from '@mdi/js';
import MdIcon from '@osm/Components/MdIcon';
import {Vnode, VnodeDOM} from 'mithril';
import {
Attributes,
Component
} from 'mithril-utilities';
import {ValueOf} from 'type-fest';
type Icons = ValueOf<typeof MaterialIcons>;
export interface DrawerItemAttributes extends Attributes, Partial<ListItemEl> {
href: ListItemEl['href'];
icon: Icons;
}
export class DrawerItem<A extends DrawerItemAttributes = DrawerItemAttributes> extends Component<A> {
oncreate(vnode: VnodeDOM<A, this>) {
super.oncreate(vnode);
this.element.setAttribute('href', vnode.attrs.href); // Fix for Mithril not setting the href attribute
}
view(vnode: Vnode<A>) {
return (
<md-list-item
type="link"
role="presentation"
selected={this.isRouteActive(vnode.attrs.href)}
href={vnode.attrs.href}
onclick={this.navigateToRoute.bind(this, vnode.attrs.href)}
{...this.attrs.all()}>
<div slot="headline">
{vnode.children}
</div>
<MdIcon icon={vnode.attrs.icon} slot="start"/>
</md-list-item>
);
}
isRouteActive(href: string) {
return route(route().current()!, route().params) === href;
}
navigateToRoute(url: string, event: Event) {
event.preventDefault();
router.visit(url);
}
}

View File

@ -0,0 +1,30 @@
import Drawer from '@osm/Components/layout/Drawer';
import Footer from '@osm/Components/layout/Footer';
import TopAppBar from '@osm/Components/layout/TopAppBar';
import {isMobile} from '@osm/utils/misc';
import {Children, Vnode} from 'mithril';
import Stream from 'mithril/stream';
import {Attributes, Component} from 'mithril-utilities';
export interface ScaffoldAttributes extends Attributes {
}
export default class Scaffold<A extends ScaffoldAttributes = ScaffoldAttributes> extends Component<A> {
drawerOpen = Stream(!isMobile());
view(vnode: Vnode<A, this>): Children {
return (
<Drawer open={this.drawerOpen}>
<TopAppBar slot="top-app-bar" drawer-open-state={this.drawerOpen}/>
<div slot="app-content">
<main id="appContent">
{vnode.children}
</main>
<Footer/>
</div>
</Drawer>
);
}
}

View File

@ -6,7 +6,6 @@ import {
mdiMenuOpen
} from '@mdi/js';
import logo from '@osm/../images/logo.png';
import Drawer from '@osm/Components/layout/Drawer';
import NotificationsAction from '@osm/Components/layout/topappbar_actions/NotificationsAction';
import PeriodSwitcherAction from '@osm/Components/layout/topappbar_actions/PeriodSwitcherAction';
import PrintAction from '@osm/Components/layout/topappbar_actions/PrintAction';
@ -28,56 +27,63 @@ import {
Component
} from 'mithril-utilities';
export default class TopAppBar extends Component {
drawerOpenState = Stream(!isMobile());
export interface TopAppBarAttributes extends Attributes {
'drawer-open-state'?: Stream<boolean>;
}
view(vnode: Vnode) {
export default class TopAppBar<A extends TopAppBarAttributes = TopAppBarAttributes> extends Component<A> {
drawerOpenState!: Stream<boolean>;
oninit(vnode: Vnode<A, this>) {
super.oninit(vnode);
this.drawerOpenState = vnode.attrs['drawer-open-state'] ?? Stream(!isMobile());
}
view(vnode: Vnode<A, this>) {
return (
<>
<top-app-bar>
<top-app-bar {...m.censor(vnode.attrs, ['drawer-open-state'])} drawer-open={this.drawerOpenState()} onmenu-button-toggle={this.onMenuButtonClick.bind(this)}>
{this.navigationIcon(vnode)}
<div style={{display: 'flex'}}>
<Drawer open={this.drawerOpenState}/>
<main id="appContent" style={{marginLeft: (!isMobile() && !this.drawerOpenState()) ? '16px' : undefined}}>
{vnode.children}
</main>
<div slot="start">
{this.start(vnode)}
</div>
{this.branding(vnode)}
{this.actions().toArray()}
<div slot="end">
{this.actions().toArray()}
</div>
</top-app-bar>
</>
);
}
navigationIcon(vnode: Vnode) {
navigationIcon(vnode: Vnode<A, this>) {
return (
<md-icon-button slot="navigationIcon" onclick={this.onNavigationIconClick.bind(this)}>
<MdIcon icon={this.drawerOpenState() ? mdiMenuOpen : mdiMenu}/>
</md-icon-button>
<>
<MdIcon icon={mdiMenuOpen} slot="menu-button-icon-selected"/>
<MdIcon icon={mdiMenu} slot="menu-button-icon"/>
</>
);
}
branding(vnode: Vnode) {
start(vnode: Vnode<A, this>) {
return (
<div slot="title" style={{display: 'flex', alignItems: 'center'}}>
<div style={{display: 'flex', alignItems: 'center'}}>
{this.logo(vnode)}
{this.title(vnode)}
</div>
);
}
logo(vnode: Vnode) {
logo(vnode: Vnode<A, this>) {
return <img src={logo} alt={__('OpenSTAManager')} style={{height: '50px', marginRight: '8px'}}/>;
}
title(vnode: Vnode) {
title(vnode: Vnode<A, this>) {
return <span>{__('OpenSTAManager')}</span>;
}
oncreate(vnode: VnodeDOM<Attributes, this>) {
oncreate(vnode: VnodeDOM<A, this>) {
super.oncreate(vnode);
mobileMediaQuery().addEventListener('change', (event) => {
@ -86,6 +92,14 @@ export default class TopAppBar extends Component {
});
}
end(vnode: Vnode<A, this>) {
return (
<>
{this.actions().toArray()}
</>
);
}
actions() {
return collect<VnodeCollectionItem>({
notifications: <NotificationsAction/>,
@ -95,7 +109,7 @@ export default class TopAppBar extends Component {
});
}
onNavigationIconClick() {
onMenuButtonClick() {
this.drawerOpenState(!this.drawerOpenState());
}
}

View File

@ -10,6 +10,9 @@ declare global {
}
}
/**
* @deprecated Use `nav-drawer` instead until official Material Web NavigationDrawer is released.
*/
@customElement('md-navigation-drawer')
export default class MdNavigationDrawer extends MDNavigationDrawer {
static override readonly styles = [sharedStyles, styles, css`

View File

@ -10,6 +10,9 @@ declare global {
}
}
/**
* @deprecated Use `nav-drawer` instead until official Material Web NavigationDrawer is released.
*/
@customElement('md-navigation-drawer-modal')
export default class MdNavigationDrawer extends MDNavigationDrawerModal {
static override readonly styles = [sharedStyles, styles, css`

View File

@ -0,0 +1,420 @@
/*
/**
* @license
* Copyright 2023 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
import {animate, fadeIn, fadeOut} from '@lit-labs/motion';
import {EASING} from '@material/web/internal/motion/animation.js';
import {LitElement, PropertyValues, css, html, nothing} from 'lit';
import {customElement, property, state} from 'lit/decorators.js';
declare global {
interface HTMLElementTagNameMap {
'nav-drawer': NavDrawer;
}
}
/**
* A layout element that positions the top-app-bar, the main page content, and
* the side navigation drawer.
*
* The drawer will automatically set itself as collapsible at narrower page
* widths, and position itself inline with the page at wider page widths. Most
* importantly, this sidebar is SSR compatible.
*/
@customElement('nav-drawer')
export default class NavDrawer extends LitElement {
/**
* Whether or not the side drawer is collapsible or inline.
*/
@state() private isCollapsible = false;
@property({type: Boolean, attribute: 'open', reflect: true}) open = false;
@property({attribute: 'page-title'}) pageTitle = '';
private lastDrawerOpen = this.open;
render() {
const showModal = this.isCollapsible && this.open;
// Values taken from internal material motion spec
const drawerSlideAnimationDuration = showModal ? 500 : 150;
const drawerContentOpacityDuration = showModal ? 300 : 150;
const scrimOpacityDuration = 150;
const drawerSlideAnimationEasing = showModal
? EASING.EMPHASIZED
: EASING.EMPHASIZED_ACCELERATE;
return html`
<div class="root">
<slot name="top-app-bar"></slot>
<div class="body ${this.open ? 'open' : ''}">
<div class="spacer">
${showModal
? html`<div
class="scrim"
@click=${this.onScrimClick}
${animate({
properties: ['opacity'],
keyframeOptions: {
duration: scrimOpacityDuration,
easing: 'linear',
},
in: fadeIn,
out: fadeOut,
})}></div>`
: nothing}
<aside
?inert=${this.isCollapsible && !this.open}
${animate({
properties: ['transform', 'width'],
keyframeOptions: {
duration: drawerSlideAnimationDuration,
easing: drawerSlideAnimationEasing,
},
})}>
<div class="scroll-wrapper">
<slot
${animate({
properties: ['opacity'],
keyframeOptions: {
duration: drawerContentOpacityDuration,
easing: 'linear',
},
})}></slot>
</div>
</aside>
</div>
<div class="panes">
<slot name="panes"></slot>${this.renderContent(showModal)}
</div>
</div>
</div>
`;
}
private renderContent(showModal: boolean) {
return html` <div
class="pane content-pane"
?inert=${showModal}>
<div class="scroll-wrapper">
<div class="content">
<slot name="app-content"></slot>
</div>
</div>
</div>`;
}
/**
* Closes the drawer on scrim click.
*/
private onScrimClick() {
this.open = false;
this.dispatchEvent(new CustomEvent('close', {bubbles: true}));
}
firstUpdated() {
const queryResult = window.matchMedia('(max-width: 768px)');
this.isCollapsible = queryResult.matches;
// Listen for page resizes to mark the drawer as collapsible.
queryResult.addEventListener('change', (e) => {
this.isCollapsible = e.matches;
});
}
updated(changed: PropertyValues<this>) {
super.updated(changed);
if (
this.lastDrawerOpen !== this.open &&
this.open &&
this.isCollapsible
) {
(
this.querySelector(
'md-list.nav md-list-item[tabindex="0"]',
) as HTMLElement
)?.focus();
}
}
static styles = css`
:host {
--_drawer-width: var(--nav-drawer-width, 300px);
/* When in wide mode inline start margin is handled by the sidebar */
--_pane-margin-inline-start: 0px;
--_pane-margin-inline-end: var(--nav-drawer-spacing-end-horizontal, 16px);
--_pane-margin-block-end: var(--nav-drawer-spacing-end-vertical, 16px);
min-height: 100dvh;
display: flex;
flex-direction: column;
}
::slotted(nav) {
list-style: none;
}
.body {
display: flex;
flex-grow: 1;
}
.spacer {
position: relative;
transition: min-width 0.5s cubic-bezier(0.3, 0, 0, 1);
}
.spacer,
aside {
min-width: var(--_drawer-width);
max-width: var(--_drawer-width);
}
@media (min-width: 768px) {
.body:not(.open) :is(aside, .spacer) {
min-width: 0;
width: 0;
}
:host {
--_pane-margin-inline-start: var(--nav-drawer-pane-spacing-start, 28px);
}
}
.body:not(.open) .scroll-wrapper {
overflow-x: hidden;
}
.pane {
box-sizing: border-box;
overflow: auto;
width: 100%;
/* Explicit height to make overflow work */
height: calc(
100dvh - var(--top-app-bar-height, calc(48px + 2 * var(--top-app-bar-spacing, 12px))) -
var(--_pane-margin-block-end)
);
background-color: var(--md-sys-color-surface);
border-radius: var(--nav-drawer-pane-border-radius, 28px);
}
::slotted([slot="app-content"]) {
//max-height: calc(
// 100dvh - var(--top-app-bar-height, calc(48px + 2 * var(--top-app-bar-spacing, 12px))) -
// var(--_pane-margin-block-end) - var(--nav-drawer-pane-spacing, 28px) * 2
//);
height: calc(
100dvh - var(--top-app-bar-height, calc(48px + 2 * var(--top-app-bar-spacing, 12px))) -
var(--_pane-margin-block-end) - var(--nav-drawer-pane-spacing, 28px) * 2
);
}
.pane,
.panes {
/* emphasized duration matching render fn for sidebar */
transition: 0.5s cubic-bezier(0.3, 0, 0, 1);
transition-property: margin, height, border-radius, max-width, width;
}
.panes {
display: flex;
justify-content: start;
flex-direction: row-reverse;
gap: var(--_pane-margin-inline-end);
margin-inline: var(--_pane-margin-inline-start)
var(--_pane-margin-inline-end);
width: 100%;
max-width: calc(
100% - var(--_drawer-width) - var(--_pane-margin-inline-start) -
var(--_pane-margin-inline-end)
);
background-color: var(--md-sys-color-surface-container);
}
.body:not(.open) .panes {
max-width: calc(
100% - var(--_pane-margin-inline-start) -
var(--_pane-margin-inline-end)
);
}
.pane.content-pane {
flex-grow: 1;
}
.content {
flex-grow: 1;
display: flex;
justify-content: center;
box-sizing: border-box;
padding-inline: var(--nav-drawer-content-spacing, 28px);
width: 100%;
}
.content slot {
display: block;
width: 100%;
max-width: min(100%, var(--_max-width));
}
aside {
transition: transform 0.5s cubic-bezier(0.3, 0, 0, 1);
position: fixed;
isolation: isolate;
inset: var(--top-app-bar-height, calc(48px + 2 * var(--top-app-bar-spacing, 12px))) 0 0 0;
background-color: var(--md-sys-color-surface-container);
overflow: hidden;
}
.scroll-wrapper {
overflow-y: auto;
max-height: 100%;
border-radius: inherit;
box-sizing: border-box;
}
.pane .scroll-wrapper {
padding-block: var(--nav-drawer-pane-spacing, 28px);
}
aside slot {
display: block;
}
.scrim {
background-color: rgba(0, 0, 0, 0.32);
}
@media (max-width: 768px) {
.spacer {
min-width: 0px;
}
aside {
z-index: 12;
}
.panes {
max-width: calc(
100% - var(--_pane-margin-inline-start) -
var(--_pane-margin-inline-end)
);
}
.content {
max-width: 100vw;
padding-inline: var(--nav-drawer-pane-spacing, 28px);
}
.scrim {
position: fixed;
inset: 0;
}
aside {
transition: unset;
transform: translateX(-100%);
border-radius: 0 var(--nav-drawer-border-radius, 28px) var(--nav-drawer-border-radius, 28px) 0;
}
:host {
--_pane-margin-inline-start: var(--nav-drawer-pane-spacing, 28px);
}
.open aside {
transform: translateX(0);
}
aside slot {
opacity: 0;
}
.open aside slot {
opacity: 1;
}
.open .scrim {
inset: 0;
z-index: 11;
}
}
@media (max-width: 600px) {
.pane {
border-end-start-radius: 0;
border-end-end-radius: 0;
}
:host {
--_pane-margin-block-end: 0px;
--_pane-margin-inline-start: 0px;
--_pane-margin-inline-end: 0px;
}
}
/* On desktop, make the scrollbars less blocky so you can see the border
* radius of the pane. On most mobile platforms, these scrollbars are hidden
* by default. It'll still unfortunately render on top of the border radius.
*/
@media (pointer: fine) {
:host {
--_scrollbar-width: 8px;
}
.scroll-wrapper {
/* firefox */
scrollbar-color: var(--md-sys-color-primary) transparent;
scrollbar-width: thin;
}
.content {
/* adjust for the scrollbar width */
padding-inline-end: calc(
var(--nav-drawer-pane-spacing, 28px) - var(--_scrollbar-width)
);
}
/* Chromium + Safari */
.scroll-wrapper::-webkit-scrollbar {
background-color: transparent;
width: var(--_scrollbar-width);
}
.scroll-wrapper::-webkit-scrollbar-thumb {
background-color: var(--md-sys-color-primary);
border-radius: calc(var(--_scrollbar-width) / 2);
}
}
@media (forced-colors: active) {
.pane {
border: 1px solid CanvasText;
}
@media (max-width: 1500px) {
aside {
box-sizing: border-box;
border: 1px solid CanvasText;
}
.scrim {
background-color: rgba(0, 0, 0, 0.75);
}
}
@media (pointer: fine) {
.scroll-wrapper {
/* firefox */
scrollbar-color: CanvasText transparent;
}
.scroll-wrapper::-webkit-scrollbar-thumb {
/* Chromium + Safari */
background-color: CanvasText;
}
}
}
`;
}

View File

@ -1,18 +1,146 @@
import {TopAppBar as MWCTopAppBar} from '@material/mwc-top-app-bar';
import {css} from 'lit';
import {customElement} from 'lit/decorators.js';
import '@material/web/focus/md-focus-ring.js';
import '@material/web/icon/icon.js';
import '@material/web/iconbutton/icon-button.js';
import type {MdIconButton} from '@material/web/iconbutton/icon-button.js';
import {css, html, LitElement} from 'lit';
import {customElement, property, state} from 'lit/decorators.js';
import {live} from 'lit/directives/live.js';
/**
* Top app bar of the catalog.
*/
@customElement('top-app-bar')
export default class TopAppBar extends LitElement {
/**
* Whether the drawer is open.
*/
@property({type: Boolean, attribute: 'drawer-open'}) private drawerOpen = false;
render() {
return html`
<header>
<div class="default-content">
<section class="start">
<md-icon-button
toggle
class="menu-button"
aria-label-selected="open navigation menu"
aria-label="close navigation menu"
aria-expanded=${this.drawerOpen ? 'false' : 'true'}
title="${this.drawerOpen ? 'Close' : 'Open'} navigation menu"
.selected=${live(this.drawerOpen)}
@input=${this.onMenuIconToggle}>
<slot name="menu-button-icon-selected" slot="selected">
<md-icon>menu</md-icon>
</slot>
<slot name="menu-button-icon">
<md-icon>menu_open</md-icon>
</slot>
</md-icon-button>
<slot name="start" class="start-content"></slot>
</section>
<section class="end">
<slot name="end"></slot>
</section>
</div>
<slot></slot>
</header>
`;
}
/**
* Toggles the sidebar's open state.
*/
private onMenuIconToggle(e: InputEvent) {
this.drawerOpen = !(e.target as MdIconButton).selected;
this.dispatchEvent(new CustomEvent('menu-button-toggle', {detail: this.drawerOpen, bubbles: true}));
}
static styles = css`
:host,
header {
display: block;
height: var(--top-app-bar-height, calc(48px + 2 * var(--top-app-bar-spacing, 12px)));
}
header {
position: fixed;
inset: 0 0 auto 0;
display: flex;
align-items: center;
box-sizing: border-box;
padding: var(--top-app-bar-elements-spacing-left, 12px) var(--top-app-bar-elements-spacing-right, 16px);
background-color: var(--md-sys-color-surface-container);
color: var(--md-sys-color-on-surface);
z-index: 12;
}
.default-content {
width: 100%;
display: flex;
align-items: center;
}
md-icon-button:not(:defined) {
width: 40px;
height: 40px;
display: flex;
visibility: hidden;
}
md-icon-button * {
display: block;
}
a {
color: var(--md-sys-color-primary);
font-size: max(var(--top-app-bar-title-font-size), 22px);
text-decoration: none;
padding-inline: 12px;
position: relative;
outline: none;
vertical-align: middle;
}
.menu-button {
margin-right: 16px;
--_selected-icon-color: inherit;
--_selected-hover-icon-color: inherit;
--_selected-focus-icon-color: inherit;
--_selected-hover-state-layer-color: var(--_hover-state-layer-color);
--_selected-pressed-icon-color: inherit;
--_selected-pressed-hover-icon-color: inherit;
--_selected-pressed-focus-icon-color: inherit;
--_selected-pressed-state-layer-color: var(--_hover-state-layer-color);
}
.start {
display: flex;
align-items: center;
}
.start .start-content * {
color: var(--md-sys-color-primary);
}
.end {
flex-grow: 1;
display: flex;
justify-content: flex-end;
}
@media (max-width: 768px) {
.start .start-content {
display: none;
}
}
`;
}
declare global {
interface HTMLElementTagNameMap {
'top-app-bar': TopAppBar;
}
}
@customElement('top-app-bar')
export default class TopAppBar extends MWCTopAppBar {
static styles = [...MWCTopAppBar.styles, css`
header.mdc-top-app-bar {
z-index: 0;
}
`];
}