Add two pane view to calendar panel (#18286)

Co-authored-by: Paul Bottein <paul.bottein@gmail.com>
This commit is contained in:
Bram Kragten 2023-10-23 16:13:38 +02:00 committed by GitHub
parent 6cae11f0a6
commit 463a3244cf
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 528 additions and 124 deletions

View File

@ -182,6 +182,8 @@ const createWebpackConfig = ({
"@lit-labs/virtualizer/layouts/grid.js",
"@lit-labs/virtualizer/polyfills/resize-observer-polyfill/ResizeObserver":
"@lit-labs/virtualizer/polyfills/resize-observer-polyfill/ResizeObserver.js",
"@lit-labs/observers/resize-controller":
"@lit-labs/observers/resize-controller.js",
},
},
output: {

View File

@ -52,10 +52,12 @@
"@lezer/highlight": "1.1.6",
"@lit-labs/context": "0.4.1",
"@lit-labs/motion": "1.0.4",
"@lit-labs/observers": "2.0.1",
"@lit-labs/virtualizer": "2.0.7",
"@lrnwebcomponents/simple-tooltip": "7.0.18",
"@material/chips": "=14.0.0-canary.53b3cad2f.0",
"@material/data-table": "=14.0.0-canary.53b3cad2f.0",
"@material/mwc-base": "0.27.0",
"@material/mwc-button": "0.27.0",
"@material/mwc-checkbox": "0.27.0",
"@material/mwc-circular-progress": "0.27.0",

View File

@ -26,6 +26,8 @@ export class HaButtonMenu extends LitElement {
@property({ type: Boolean }) public fixed = false;
@property({ type: Boolean, attribute: "no-anchor" }) public noAnchor = false;
@query("mwc-menu", true) private _menu?: Menu;
public get items() {
@ -82,7 +84,7 @@ export class HaButtonMenu extends LitElement {
if (this.disabled) {
return;
}
this._menu!.anchor = this;
this._menu!.anchor = this.noAnchor ? null : this;
this._menu!.show();
}

View File

@ -17,6 +17,9 @@ export class HaButton extends Button {
.mdc-button {
height: var(--button-height, 36px);
}
.trailing-icon {
display: flex;
}
`,
];
}

View File

@ -0,0 +1,320 @@
import {
addHasRemoveClass,
BaseElement,
} from "@material/mwc-base/base-element";
import { supportsPassiveEventListener } from "@material/mwc-base/utils";
import { MDCTopAppBarAdapter } from "@material/top-app-bar/adapter";
import { strings } from "@material/top-app-bar/constants";
import MDCFixedTopAppBarFoundation from "@material/top-app-bar/fixed/foundation";
import { html, css, nothing } from "lit";
import { property, query, customElement } from "lit/decorators";
import { classMap } from "lit/directives/class-map";
import { styles } from "@material/mwc-top-app-bar/mwc-top-app-bar.css";
import { haStyleScrollbar } from "../resources/styles";
export const passiveEventOptionsIfSupported = supportsPassiveEventListener
? { passive: true }
: undefined;
@customElement("ha-two-pane-top-app-bar-fixed")
export abstract class TopAppBarBaseBase extends BaseElement {
protected override mdcFoundation!: MDCFixedTopAppBarFoundation;
protected override mdcFoundationClass = MDCFixedTopAppBarFoundation;
@query(".mdc-top-app-bar") protected mdcRoot!: HTMLElement;
// _actionItemsSlot should have type HTMLSlotElement, but when TypeScript's
// emitDecoratorMetadata is enabled, the HTMLSlotElement constructor will
// be emitted into the runtime, which will cause an "HTMLSlotElement is
// undefined" error in browsers that don't define it (e.g. IE11).
@query('slot[name="actionItems"]') protected _actionItemsSlot!: HTMLElement;
protected _scrollTarget!: HTMLElement | Window;
@property({ type: Boolean }) centerTitle = false;
@property({ type: Boolean, reflect: true }) prominent = false;
@property({ type: Boolean, reflect: true }) dense = false;
@property({ type: Boolean }) pane = false;
@property({ type: Boolean }) footer = false;
@query(".content") private _contentElement!: HTMLElement;
@query(".pane .ha-scrollbar") private _paneElement?: HTMLElement;
@property({ type: Object })
get scrollTarget() {
return this._scrollTarget || window;
}
set scrollTarget(value) {
this.unregisterListeners();
const old = this.scrollTarget;
this._scrollTarget = value;
this.updateRootPosition();
this.requestUpdate("scrollTarget", old);
this.registerListeners();
}
protected updateRootPosition() {
if (this.mdcRoot) {
const windowScroller = this.scrollTarget === window;
// we add support for top-app-bar's tied to an element scroller.
this.mdcRoot.style.position = windowScroller ? "" : "absolute";
}
}
protected barClasses() {
return {
"mdc-top-app-bar--dense": this.dense,
"mdc-top-app-bar--prominent": this.prominent,
"center-title": this.centerTitle,
"mdc-top-app-bar--fixed": true,
"mdc-top-app-bar--pane": this.pane,
};
}
protected contentClasses() {
return {
"mdc-top-app-bar--fixed-adjust": !this.dense && !this.prominent,
"mdc-top-app-bar--dense-fixed-adjust": this.dense && !this.prominent,
"mdc-top-app-bar--prominent-fixed-adjust": !this.dense && this.prominent,
"mdc-top-app-bar--dense-prominent-fixed-adjust":
this.dense && this.prominent,
"mdc-top-app-bar--pane": this.pane,
};
}
protected override render() {
const title = html`<span class="mdc-top-app-bar__title"
><slot name="title"></slot
></span>`;
return html`
<header class="mdc-top-app-bar ${classMap(this.barClasses())}">
<div class="mdc-top-app-bar__row">
${this.pane
? html`<section
class="mdc-top-app-bar__section mdc-top-app-bar__section--align-start"
id="title"
>
<slot
name="navigationIcon"
@click=${this.handleNavigationClick}
></slot>
${title}
</section>`
: nothing}
<section class="mdc-top-app-bar__section" id="navigation">
${this.pane
? nothing
: html`<slot
name="navigationIcon"
@click=${this.handleNavigationClick}
></slot
>${title}`}
</section>
<section
class="mdc-top-app-bar__section mdc-top-app-bar__section--align-end"
id="actions"
role="toolbar"
>
<slot name="actionItems"></slot>
</section>
</div>
</header>
<div class=${classMap(this.contentClasses())}>
${this.pane
? html`<div class="pane">
<div class="shadow-container"></div>
<div class="ha-scrollbar">
<slot name="pane"></slot>
</div>
${this.footer
? html`<div class="footer">
<slot name="pane-footer"></slot>
</div>`
: nothing}
</div>`
: nothing}
<div class="main">
${this.pane ? html`<div class="shadow-container"></div>` : nothing}
<div class="content">
<slot></slot>
</div>
</div>
</div>
`;
}
protected updated(changedProperties) {
super.updated(changedProperties);
if (
changedProperties.has("pane") &&
changedProperties.get("pane") !== undefined
) {
this.unregisterListeners();
this.registerListeners();
}
}
protected createAdapter(): MDCTopAppBarAdapter {
return {
...addHasRemoveClass(this.mdcRoot),
setStyle: (prprty: string, value: string) =>
this.mdcRoot.style.setProperty(prprty, value),
getTopAppBarHeight: () => this.mdcRoot.clientHeight,
notifyNavigationIconClicked: () => {
this.dispatchEvent(
new Event(strings.NAVIGATION_EVENT, {
bubbles: true,
cancelable: true,
})
);
},
getViewportScrollY: () =>
this.scrollTarget instanceof Window
? this.scrollTarget.pageYOffset
: this.scrollTarget.scrollTop,
getTotalActionItems: () =>
(this._actionItemsSlot as HTMLSlotElement).assignedNodes({
flatten: true,
}).length,
};
}
protected handleTargetScroll = () => {
this.mdcFoundation.handleTargetScroll();
};
protected handlePaneScroll = (ev) => {
if (ev.target.scrollTop > 0) {
ev.target.parentElement.classList.add("scrolled");
} else {
ev.target.parentElement.classList.remove("scrolled");
}
};
protected handleNavigationClick = () => {
this.mdcFoundation.handleNavigationClick();
};
protected registerListeners() {
if (this.pane) {
this._paneElement!.addEventListener(
"scroll",
this.handlePaneScroll,
passiveEventOptionsIfSupported
);
this._contentElement.addEventListener(
"scroll",
this.handlePaneScroll,
passiveEventOptionsIfSupported
);
return;
}
this.scrollTarget.addEventListener(
"scroll",
this.handleTargetScroll,
passiveEventOptionsIfSupported
);
}
protected unregisterListeners() {
this._paneElement?.removeEventListener("scroll", this.handlePaneScroll);
this._contentElement.removeEventListener("scroll", this.handlePaneScroll);
this.scrollTarget.removeEventListener("scroll", this.handleTargetScroll);
}
protected override firstUpdated() {
super.firstUpdated();
this.updateRootPosition();
this.registerListeners();
}
override disconnectedCallback() {
super.disconnectedCallback();
this.unregisterListeners();
}
static override styles = [
styles,
haStyleScrollbar,
css`
.mdc-top-app-bar__row {
height: var(--header-height);
border-bottom: var(--app-header-border-bottom);
}
.mdc-top-app-bar--fixed-adjust {
padding-top: var(--header-height);
}
.shadow-container {
position: absolute;
top: calc(-1 * var(--header-height));
width: 100%;
height: var(--header-height);
z-index: 1;
transition: box-shadow 200ms linear;
}
.scrolled .shadow-container {
box-shadow: var(
--mdc-top-app-bar-fixed-box-shadow,
0px 2px 4px -1px rgba(0, 0, 0, 0.2),
0px 4px 5px 0px rgba(0, 0, 0, 0.14),
0px 1px 10px 0px rgba(0, 0, 0, 0.12)
);
}
.mdc-top-app-bar {
--mdc-typography-headline6-font-weight: 400;
color: var(--app-header-text-color, var(--mdc-theme-on-primary, #fff));
background-color: var(
--app-header-background-color,
var(--mdc-theme-primary)
);
}
.mdc-top-app-bar--pane.mdc-top-app-bar--fixed-scrolled {
box-shadow: none;
}
#title {
border-right: 1px solid rgba(255, 255, 255, 0.12);
box-sizing: border-box;
flex: 0 0 var(--sidepane-width, 250px);
width: var(--sidepane-width, 250px);
}
div.mdc-top-app-bar--pane {
display: flex;
height: calc(100vh - var(--header-height));
}
.pane {
border-right: 1px solid var(--divider-color);
box-sizing: border-box;
display: flex;
flex: 0 0 var(--sidepane-width, 250px);
width: var(--sidepane-width, 250px);
flex-direction: column;
position: relative;
}
.pane .ha-scrollbar {
flex: 1;
}
.pane .footer {
border-top: 1px solid var(--divider-color);
}
.main {
min-height: 100%;
}
.mdc-top-app-bar--pane .main {
position: relative;
flex: 1;
height: 100%;
}
.mdc-top-app-bar--pane .content {
height: 100%;
overflow: auto;
}
`,
];
}

View File

@ -143,7 +143,7 @@ export const getCalendars = (hass: HomeAssistant): Calendar[] =>
)
.sort()
.map((eid, idx) => ({
entity_id: eid,
...hass.states[eid],
name: computeStateName(hass.states[eid]),
backgroundColor: getColorByIndex(idx),
}));

View File

@ -439,6 +439,11 @@ export class HAFullCalendar extends LitElement {
justify-content: initial;
}
.header {
padding-right: var(--calendar-header-padding);
padding-left: var(--calendar-header-padding);
}
.navigation {
display: flex;
align-items: center;
@ -513,7 +518,11 @@ export class HAFullCalendar extends LitElement {
.fc-theme-standard .fc-scrollgrid {
border: 1px solid var(--divider-color);
border-radius: var(--mdc-shape-small, 4px);
border-width: var(--calendar-border-width, 1px);
border-radius: var(
--calendar-border-radius,
var(--mdc-shape-small, 4px)
);
}
.fc-theme-standard td {

View File

@ -1,22 +1,31 @@
import "@material/mwc-checkbox";
import "@material/mwc-formfield";
import { mdiRefresh } from "@mdi/js";
import { ResizeController } from "@lit-labs/observers/resize-controller";
import "@material/mwc-list";
import type { RequestSelectedDetail } from "@material/mwc-list/mwc-list-item";
import { mdiChevronDown, mdiRefresh } from "@mdi/js";
import {
css,
CSSResultGroup,
html,
LitElement,
PropertyValues,
TemplateResult,
css,
html,
nothing,
} from "lit";
import { customElement, property, state } from "lit/decorators";
import { styleMap } from "lit/directives/style-map";
import { storage } from "../../common/decorators/storage";
import { HASSDomEvent } from "../../common/dom/fire_event";
import { computeStateName } from "../../common/entity/compute_state_name";
import "../../components/ha-button";
import "../../components/ha-button-menu";
import "../../components/ha-card";
import "../../components/ha-check-list-item";
import "../../components/ha-icon-button";
import type { HaListItem } from "../../components/ha-list-item";
import "../../components/ha-menu-button";
import "../../components/ha-state-icon";
import "../../components/ha-svg-icon";
import "../../components/ha-two-pane-top-app-bar-fixed";
import {
Calendar,
CalendarEvent,
@ -26,7 +35,6 @@ import {
import { haStyle } from "../../resources/styles";
import type { CalendarViewChanged, HomeAssistant } from "../../types";
import "./ha-full-calendar";
import "../../components/ha-top-app-bar-fixed";
@customElement("ha-panel-calendar")
class PanelCalendar extends LitElement {
@ -35,6 +43,8 @@ class PanelCalendar extends LitElement {
@property({ type: Boolean, reflect: true })
public narrow!: boolean;
@property({ type: Boolean, reflect: true }) public mobile = false;
@state() private _calendars: Calendar[] = [];
@state() private _events: CalendarEvent[] = [];
@ -51,6 +61,38 @@ class PanelCalendar extends LitElement {
private _end?: Date;
private _showPaneController = new ResizeController(this, {
callback: (entries: ResizeObserverEntry[]) =>
entries[0]?.contentRect.width > 750,
});
private _mql?: MediaQueryList;
private _headerHeight = 56;
public connectedCallback() {
super.connectedCallback();
this._mql = window.matchMedia(
"(max-width: 450px), all and (max-height: 500px)"
);
this._mql.addListener(this._setIsMobile);
this.mobile = this._mql.matches;
const computedStyles = getComputedStyle(this);
this._headerHeight = Number(
computedStyles.getPropertyValue("--header-height").replace("px", "")
);
}
public disconnectedCallback() {
super.disconnectedCallback();
this._mql?.removeListener(this._setIsMobile!);
this._mql = undefined;
}
private _setIsMobile = (ev: MediaQueryListEvent) => {
this.mobile = ev.matches;
};
public willUpdate(changedProps: PropertyValues): void {
super.willUpdate(changedProps);
if (!this.hasUpdated) {
@ -59,44 +101,64 @@ class PanelCalendar extends LitElement {
}
protected render(): TemplateResult {
const calendarItems = this._calendars.map(
(selCal) => html`
<ha-check-list-item
@request-selected=${this._requestSelected}
graphic="icon"
style=${styleMap({
"--mdc-theme-secondary": selCal.backgroundColor!,
})}
.value=${selCal.entity_id}
.selected=${!this._deSelectedCalendars.includes(selCal.entity_id)}
>
<ha-state-icon slot="graphic" .state=${selCal}></ha-state-icon>
${selCal.name}
</ha-check-list-item>
`
);
const showPane = this._showPaneController.value ?? !this.narrow;
return html`
<ha-top-app-bar-fixed>
<ha-two-pane-top-app-bar-fixed .pane=${showPane}>
<ha-menu-button
slot="navigationIcon"
.hass=${this.hass}
.narrow=${this.narrow}
></ha-menu-button>
<div slot="title">${this.hass.localize("panel.calendar")}</div>
${!showPane
? html`<ha-button-menu
slot="title"
class="lists"
multi
fixed
.noAnchor=${this.mobile}
.y=${this.mobile
? this._headerHeight / 2
: this._headerHeight / 4}
.x=${this.mobile ? 0 : undefined}
>
<ha-button slot="trigger">
${this.hass.localize("ui.components.calendar.my_calendars")}
<ha-svg-icon
slot="trailingIcon"
.path=${mdiChevronDown}
></ha-svg-icon>
</ha-button>
${calendarItems}
</ha-button-menu>`
: html`<div slot="title">
${this.hass.localize("ui.components.calendar.my_calendars")}
</div>`}
<ha-icon-button
slot="actionItems"
.path=${mdiRefresh}
.label=${this.hass.localize("ui.common.refresh")}
@click=${this._handleRefresh}
></ha-icon-button>
<div class="content">
<div class="calendar-list">
<div class="calendar-list-header">
${this.hass.localize("ui.components.calendar.my_calendars")}
</div>
${this._calendars.map(
(selCal) => html`
<div>
<mwc-formfield .label=${selCal.name}>
<mwc-checkbox
style=${styleMap({
"--mdc-theme-secondary": selCal.backgroundColor!,
})}
.value=${selCal.entity_id}
.checked=${!this._deSelectedCalendars.includes(
selCal.entity_id
)}
@change=${this._handleToggle}
></mwc-checkbox>
</mwc-formfield>
</div>
`
)}
</div>
${showPane
? html`<mwc-list slot="pane" multi}>${calendarItems}</mwc-list>`
: nothing}
<ha-full-calendar
.events=${this._events}
.calendars=${this._calendars}
@ -105,8 +167,7 @@ class PanelCalendar extends LitElement {
.error=${this._error}
@view-changed=${this._handleViewChanged}
></ha-full-calendar>
</div>
</ha-top-app-bar-fixed>
</ha-two-pane-top-app-bar-fixed>
`;
}
@ -117,46 +178,45 @@ class PanelCalendar extends LitElement {
}
private async _fetchEvents(
start: Date,
end: Date,
start: Date | undefined,
end: Date | undefined,
calendars: Calendar[]
): Promise<{ events: CalendarEvent[]; errors: string[] }> {
if (!calendars.length) {
if (!calendars.length || !start || !end) {
return { events: [], errors: [] };
}
return fetchCalendarEvents(this.hass, start, end, calendars);
}
private async _handleToggle(ev): Promise<void> {
const results = this._calendars.map(async (cal) => {
if (ev.target.value !== cal.entity_id) {
return cal;
private async _requestSelected(ev: CustomEvent<RequestSelectedDetail>) {
ev.stopPropagation();
const entityId = (ev.target as HaListItem).value;
if (ev.detail.selected) {
this._deSelectedCalendars = this._deSelectedCalendars.filter(
(cal) => cal !== entityId
);
if (ev.detail.source === "interaction") {
// prevent adding the same calendar twice, an interaction event will be followed by a property event
return;
}
const checked = ev.target.checked;
if (checked) {
const result = await this._fetchEvents(this._start!, this._end!, [cal]);
const calendar = this._calendars.find(
(cal) => cal.entity_id === entityId
);
if (!calendar) {
return;
}
const result = await this._fetchEvents(this._start, this._end, [
calendar,
]);
this._events = [...this._events, ...result.events];
this._handleErrors(result.errors);
this._deSelectedCalendars = this._deSelectedCalendars.filter(
(deCal) => deCal !== cal.entity_id
);
} else {
this._deSelectedCalendars = [...this._deSelectedCalendars, entityId];
this._events = this._events.filter(
(event) => event.calendar !== cal.entity_id
(event) => event.calendar !== entityId
);
this._deSelectedCalendars = [
...this._deSelectedCalendars,
cal.entity_id,
];
}
return cal;
});
this._calendars = await Promise.all(results);
}
private async _handleViewChanged(
@ -175,8 +235,8 @@ class PanelCalendar extends LitElement {
private async _handleRefresh(): Promise<void> {
const result = await this._fetchEvents(
this._start!,
this._end!,
this._start,
this._end,
this._selectedCalendars
);
this._events = result.events;
@ -204,56 +264,42 @@ class PanelCalendar extends LitElement {
return [
haStyle,
css`
.content {
padding: 16px;
display: flex;
box-sizing: border-box;
:host {
display: block;
}
:host(:not([narrow])) .content {
height: calc(100vh - var(--header-height));
}
.calendar-list {
padding-right: 16px;
padding-inline-end: 16px;
padding-inline-start: initial;
min-width: 170px;
flex: 0 0 15%;
overflow-x: hidden;
overflow-y: auto;
--mdc-theme-text-primary-on-background: var(--primary-text-color);
direction: var(--direction);
}
.calendar-list > div {
white-space: nowrap;
text-overflow: ellipsis;
overflow: hidden;
}
.calendar-list-header {
font-size: 16px;
padding: 16px 16px 8px 8px;
}
ha-full-calendar {
flex-grow: 1;
height: calc(100vh - var(--header-height));
--calendar-header-padding: 12px;
--calendar-border-radius: 0;
--calendar-border-width: 1px 0;
}
:host([narrow]) ha-full-calendar {
height: calc(100vh - 72px);
ha-button-menu ha-button {
--mdc-theme-primary: currentColor;
--mdc-typography-button-text-transform: none;
--mdc-typography-button-font-size: var(
--mdc-typography-headline6-font-size,
1.25rem
);
--mdc-typography-button-font-weight: var(
--mdc-typography-headline6-font-weight,
500
);
--mdc-typography-button-letter-spacing: var(
--mdc-typography-headline6-letter-spacing,
0.0125em
);
--mdc-typography-button-line-height: var(
--mdc-typography-headline6-line-height,
2rem
);
--button-height: 40px;
}
:host([narrow]) .content {
flex-direction: column-reverse;
padding: 8px 0 0 0;
:host([mobile]) .lists {
--mdc-menu-min-width: 100vw;
}
:host([narrow]) .calendar-list {
margin-bottom: 24px;
width: 100%;
padding-right: 0;
:host([mobile]) ha-button-menu {
--mdc-shape-medium: 0 0 var(--mdc-shape-medium)
var(--mdc-shape-medium);
}
`,
];

View File

@ -2106,7 +2106,16 @@ __metadata:
languageName: node
linkType: hard
"@lit-labs/ssr-dom-shim@npm:^1.0.0, @lit-labs/ssr-dom-shim@npm:^1.1.0":
"@lit-labs/observers@npm:2.0.1":
version: 2.0.1
resolution: "@lit-labs/observers@npm:2.0.1"
dependencies:
"@lit/reactive-element": ^2.0.0
checksum: 6c4518ee37678d86b263799590edd5c202a9f6800b52b52d2a0fc47e572d029c587fa9722108e449ab6bc7b1b1aee9e780b3e42ca2e91d63a43fc96541a80f98
languageName: node
linkType: hard
"@lit-labs/ssr-dom-shim@npm:^1.0.0, @lit-labs/ssr-dom-shim@npm:^1.1.0, @lit-labs/ssr-dom-shim@npm:^1.1.2-pre.0":
version: 1.1.2
resolution: "@lit-labs/ssr-dom-shim@npm:1.1.2"
checksum: 73fd787893851d4ec4aaa5c775405ed2aae4ca0891b2dd3c973b32c2f4bf70ada5481dd0224e52b786d037aa8a00052186ad1623c44551affd66f6409cca8da6
@ -2132,6 +2141,15 @@ __metadata:
languageName: node
linkType: hard
"@lit/reactive-element@npm:^2.0.0":
version: 2.0.0
resolution: "@lit/reactive-element@npm:2.0.0"
dependencies:
"@lit-labs/ssr-dom-shim": ^1.1.2-pre.0
checksum: afa12f1cf72e8735cb7eaa51d428610785ee796882ca52108310e75ac54bbf5690da718c8bf85d042060f98c139ff0d5efd54f677a9d3fc4d794ad2e0f7a12c5
languageName: node
linkType: hard
"@lokalise/node-api@npm:12.0.0":
version: 12.0.0
resolution: "@lokalise/node-api@npm:12.0.0"
@ -2497,7 +2515,7 @@ __metadata:
languageName: node
linkType: hard
"@material/mwc-base@npm:^0.27.0":
"@material/mwc-base@npm:0.27.0, @material/mwc-base@npm:^0.27.0":
version: 0.27.0
resolution: "@material/mwc-base@npm:0.27.0"
dependencies:
@ -9636,11 +9654,13 @@ __metadata:
"@lezer/highlight": 1.1.6
"@lit-labs/context": 0.4.1
"@lit-labs/motion": 1.0.4
"@lit-labs/observers": 2.0.1
"@lit-labs/virtualizer": 2.0.7
"@lokalise/node-api": 12.0.0
"@lrnwebcomponents/simple-tooltip": 7.0.18
"@material/chips": =14.0.0-canary.53b3cad2f.0
"@material/data-table": =14.0.0-canary.53b3cad2f.0
"@material/mwc-base": 0.27.0
"@material/mwc-button": 0.27.0
"@material/mwc-checkbox": 0.27.0
"@material/mwc-circular-progress": 0.27.0