style: Migliorato design, top app bar e drawer
This commit is contained in:
parent
b5deeda68b
commit
e059c4fec5
|
@ -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",
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
}
|
|
@ -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());
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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`
|
||||
|
|
|
@ -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`
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
`];
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue