Add media management dialog (#11787)

Co-authored-by: Bram Kragten <mail@bramkragten.nl>
This commit is contained in:
Paulus Schoutsen 2022-02-23 03:43:49 -08:00 committed by GitHub
parent aa988c758d
commit df35496c6e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
13 changed files with 638 additions and 114 deletions

View File

@ -46,6 +46,7 @@
"@fullcalendar/daygrid": "5.9.0",
"@fullcalendar/interaction": "5.9.0",
"@fullcalendar/list": "5.9.0",
"@lit-labs/motion": "^1.0.2",
"@lit-labs/virtualizer": "patch:@lit-labs/virtualizer@0.7.0-pre.2#./.yarn/patches/@lit-labs/virtualizer/event-target-shim.patch",
"@material/chips": "14.0.0-canary.261f2db59.0",
"@material/data-table": "14.0.0-canary.261f2db59.0",

View File

@ -0,0 +1,337 @@
import { animate } from "@lit-labs/motion";
import "@material/mwc-list/mwc-check-list-item";
import "@material/mwc-list/mwc-list-item";
import "@material/mwc-list/mwc-list";
import { repeat } from "lit/directives/repeat";
import { mdiClose, mdiDelete } from "@mdi/js";
import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
import { customElement, property, state } from "lit/decorators";
import { fireEvent } from "../../common/dom/fire_event";
import { computeRTLDirection } from "../../common/util/compute_rtl";
import { haStyleDialog } from "../../resources/styles";
import type { HomeAssistant } from "../../types";
import "../ha-header-bar";
import "../ha-dialog";
import "../ha-svg-icon";
import "../ha-circular-progress";
import "./ha-media-player-browse";
import "./ha-media-upload-button";
import type { MediaManageDialogParams } from "./show-media-manage-dialog";
import {
MediaClassBrowserSettings,
MediaPlayerItem,
} from "../../data/media-player";
import {
browseLocalMediaPlayer,
removeLocalMedia,
} from "../../data/media_source";
import { showConfirmationDialog } from "../../dialogs/generic/show-dialog-box";
@customElement("dialog-media-manage")
class DialogMediaManage extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@state() private _currentItem?: MediaPlayerItem;
@state() private _params?: MediaManageDialogParams;
@state() private _uploading = false;
@state() private _deleting = false;
@state() private _selected = new Set<number>();
private _filesChanged = false;
public showDialog(params: MediaManageDialogParams): void {
this._params = params;
this._refreshMedia();
}
public closeDialog() {
if (this._filesChanged && this._params!.onClose) {
this._params!.onClose();
}
this._params = undefined;
this._currentItem = undefined;
this._uploading = false;
this._deleting = false;
this._filesChanged = false;
fireEvent(this, "dialog-closed", { dialog: this.localName });
}
protected render(): TemplateResult {
if (!this._params) {
return html``;
}
const children =
this._currentItem?.children?.filter((child) => !child.can_expand) || [];
let fileIndex = 0;
return html`
<ha-dialog
open
scrimClickAction
escapeKeyAction
hideActions
flexContent
.heading=${this._params.currentItem.title}
@closed=${this.closeDialog}
>
<ha-header-bar slot="heading">
${this._selected.size === 0
? html`
<span slot="title">
${this.hass.localize(
"ui.components.media-browser.file_management.title"
)}
</span>
<ha-media-upload-button
.disabled=${this._deleting}
.hass=${this.hass}
.currentItem=${this._params.currentItem}
@uploading=${this._startUploading}
@media-refresh=${this._doneUploading}
slot="actionItems"
></ha-media-upload-button>
${this._uploading
? ""
: html`
<ha-icon-button
.label=${this.hass.localize("ui.dialogs.generic.close")}
.path=${mdiClose}
dialogAction="close"
slot="actionItems"
class="header_button"
dir=${computeRTLDirection(this.hass)}
></ha-icon-button>
`}
`
: html`
<mwc-button
class="danger"
slot="title"
.disabled=${this._deleting}
.label=${this.hass.localize(
`ui.components.media-browser.file_management.${
this._deleting ? "deleting" : "delete"
}`,
{ count: this._selected.size }
)}
@click=${this._handleDelete}
>
<ha-svg-icon .path=${mdiDelete} slot="icon"></ha-svg-icon>
</mwc-button>
${this._deleting
? ""
: html`
<mwc-button
slot="actionItems"
.label=${`Deselect all`}
@click=${this._handleDeselectAll}
>
<ha-svg-icon
.path=${mdiClose}
slot="icon"
></ha-svg-icon>
</mwc-button>
`}
`}
</ha-header-bar>
${!this._currentItem
? html`
<div class="refresh">
<ha-circular-progress active></ha-circular-progress>
</div>
`
: !children.length
? html`<div class="no-items">
<p>
${this.hass.localize(
"ui.components.media-browser.file_management.no_items"
)}
</p>
${this._currentItem?.children?.length
? html`<span class="folders"
>${this.hass.localize(
"ui.components.media-browser.file_management.folders_not_supported"
)}</span
>`
: ""}
</div>`
: html`
<mwc-list multi @selected=${this._handleSelected}>
${repeat(
children,
(item) => item.media_content_id,
(item) => {
const icon = html`
<ha-svg-icon
slot="graphic"
.path=${MediaClassBrowserSettings[
item.media_class === "directory"
? item.children_media_class || item.media_class
: item.media_class
].icon}
></ha-svg-icon>
`;
return html`
<mwc-check-list-item
${animate({
id: item.media_content_id,
skipInitial: true,
})}
graphic="icon"
.disabled=${this._uploading || this._deleting}
.selected=${this._selected.has(fileIndex++)}
.item=${item}
>
${icon} ${item.title}
</mwc-check-list-item>
`;
}
)}
</mwc-list>
`}
</ha-dialog>
`;
}
private _handleSelected(ev) {
this._selected = ev.detail.index;
}
private _startUploading() {
this._uploading = true;
this._filesChanged = true;
}
private _doneUploading() {
this._uploading = false;
this._refreshMedia();
}
private _handleDeselectAll() {
if (this._selected.size) {
this._selected = new Set();
}
}
private async _handleDelete() {
if (
!(await showConfirmationDialog(this, {
text: this.hass.localize(
"ui.components.media-browser.file_management.confirm_delete",
{ count: this._selected.size }
),
warning: true,
}))
) {
return;
}
this._filesChanged = true;
this._deleting = true;
const toDelete: MediaPlayerItem[] = [];
let fileIndex = 0;
this._currentItem!.children!.forEach((item) => {
if (item.can_expand) {
return;
}
if (this._selected.has(fileIndex++)) {
toDelete.push(item);
}
});
try {
await Promise.all(
toDelete.map(async (item) => {
await removeLocalMedia(this.hass, item.media_content_id);
this._currentItem = {
...this._currentItem!,
children: this._currentItem!.children!.filter((i) => i !== item),
};
})
);
} finally {
this._deleting = false;
this._selected = new Set();
}
}
private async _refreshMedia() {
this._selected = new Set();
this._currentItem = undefined;
this._currentItem = await browseLocalMediaPlayer(
this.hass,
this._params!.currentItem.media_content_id
);
}
static get styles(): CSSResultGroup {
return [
haStyleDialog,
css`
ha-dialog {
--dialog-z-index: 8;
--dialog-content-padding: 0;
}
@media (min-width: 800px) {
ha-dialog {
--mdc-dialog-max-width: 800px;
--dialog-surface-position: fixed;
--dialog-surface-top: 40px;
--mdc-dialog-max-height: calc(100vh - 72px);
}
}
ha-header-bar {
--mdc-theme-on-primary: var(--primary-text-color);
--mdc-theme-primary: var(--mdc-theme-surface);
flex-shrink: 0;
border-bottom: 1px solid var(--divider-color, rgba(0, 0, 0, 0.12));
}
ha-media-upload-button,
mwc-button {
--mdc-theme-primary: var(--mdc-theme-on-primary);
}
.danger {
--mdc-theme-primary: var(--error-color);
}
ha-svg-icon[slot="icon"] {
vertical-align: middle;
}
.refresh {
display: flex;
height: 200px;
justify-content: center;
align-items: center;
}
.no-items {
text-align: center;
padding: 16px;
}
.folders {
color: var(--secondary-text-color);
font-style: italic;
}
`,
];
}
}
declare global {
interface HTMLElementTagNameMap {
"dialog-media-manage": DialogMediaManage;
}
}

View File

@ -1,7 +1,7 @@
import "../ha-header-bar";
import { mdiArrowLeft, mdiClose } from "@mdi/js";
import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
import { customElement, property, state } from "lit/decorators";
import { customElement, property, query, state } from "lit/decorators";
import { fireEvent, HASSDomEvent } from "../../common/dom/fire_event";
import { computeRTLDirection } from "../../common/util/compute_rtl";
import type {
@ -13,7 +13,11 @@ import { haStyleDialog } from "../../resources/styles";
import type { HomeAssistant } from "../../types";
import "../ha-dialog";
import "./ha-media-player-browse";
import type { MediaPlayerItemId } from "./ha-media-player-browse";
import "./ha-media-manage-button";
import type {
HaMediaPlayerBrowse,
MediaPlayerItemId,
} from "./ha-media-player-browse";
import { MediaPlayerBrowseDialogParams } from "./show-media-browser-dialog";
@customElement("dialog-media-player-browse")
@ -26,6 +30,8 @@ class DialogMediaPlayerBrowse extends LitElement {
@state() private _params?: MediaPlayerBrowseDialogParams;
@query("ha-media-player-browse") private _browser!: HaMediaPlayerBrowse;
public showDialog(params: MediaPlayerBrowseDialogParams): void {
this._params = params;
this._navigateIds = params.navigateIds || [
@ -80,6 +86,12 @@ class DialogMediaPlayerBrowse extends LitElement {
: this._currentItem.title}
</span>
<ha-media-manage-button
slot="actionItems"
.hass=${this.hass}
.currentItem=${this._currentItem}
@media-refresh=${this._refreshMedia}
></ha-media-manage-button>
<ha-icon-button
.label=${this.hass.localize("ui.dialogs.generic.close")}
.path=${mdiClose}
@ -124,6 +136,10 @@ class DialogMediaPlayerBrowse extends LitElement {
return this._params!.action || "play";
}
private _refreshMedia() {
this._browser.refresh();
}
static get styles(): CSSResultGroup {
return [
haStyleDialog,
@ -157,6 +173,10 @@ class DialogMediaPlayerBrowse extends LitElement {
flex-shrink: 0;
border-bottom: 1px solid var(--divider-color, rgba(0, 0, 0, 0.12));
}
ha-media-manage-button {
--mdc-theme-primary: var(--mdc-theme-on-primary);
}
`,
];
}

View File

@ -0,0 +1,69 @@
import { mdiFolderEdit } from "@mdi/js";
import "@material/mwc-button";
import { css, html, LitElement, TemplateResult } from "lit";
import { customElement, property, state } from "lit/decorators";
import { MediaPlayerItem } from "../../data/media-player";
import "../ha-svg-icon";
import { isLocalMediaSourceContentId } from "../../data/media_source";
import type { HomeAssistant } from "../../types";
import { showMediaManageDialog } from "./show-media-manage-dialog";
import { fireEvent } from "../../common/dom/fire_event";
declare global {
interface HASSDomEvents {
"media-refresh": unknown;
}
}
@customElement("ha-media-manage-button")
class MediaManageButton extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@property() currentItem?: MediaPlayerItem;
@state() _uploading = 0;
protected render(): TemplateResult {
if (
!this.currentItem ||
!isLocalMediaSourceContentId(this.currentItem.media_content_id || "")
) {
return html``;
}
return html`
<mwc-button
.label=${this.hass.localize(
"ui.components.media-browser.file_management.manage"
)}
@click=${this._manage}
>
<ha-svg-icon .path=${mdiFolderEdit} slot="icon"></ha-svg-icon>
</mwc-button>
`;
}
private _manage() {
showMediaManageDialog(this, {
currentItem: this.currentItem!,
onClose: () => fireEvent(this, "media-refresh"),
});
}
static styles = css`
mwc-button {
/* We use icon + text to show disabled state */
--mdc-button-disabled-ink-color: --mdc-theme-primary;
}
ha-svg-icon[slot="icon"],
ha-circular-progress[slot="icon"] {
vertical-align: middle;
}
`;
}
declare global {
interface HTMLElementTagNameMap {
"ha-media-manage-button": MediaManageButton;
}
}

View File

@ -131,6 +131,11 @@ export class HaMediaPlayerBrowse extends LitElement {
currentId.media_content_id,
currentId.media_content_type
);
// Update the parent with latest item.
fireEvent(this, "media-browsed", {
ids: this.navigateIds,
current: this._currentItem,
});
} catch (err) {
this._setError(err);
}

View File

@ -0,0 +1,129 @@
import { mdiUpload } from "@mdi/js";
import "@material/mwc-button";
import { css, html, LitElement, TemplateResult } from "lit";
import { customElement, property, state } from "lit/decorators";
import { fireEvent } from "../../common/dom/fire_event";
import { MediaPlayerItem } from "../../data/media-player";
import "../ha-circular-progress";
import "../ha-svg-icon";
import {
isLocalMediaSourceContentId,
uploadLocalMedia,
} from "../../data/media_source";
import type { HomeAssistant } from "../../types";
import { showAlertDialog } from "../../dialogs/generic/show-dialog-box";
declare global {
interface HASSDomEvents {
uploading: unknown;
"media-refresh": unknown;
}
}
@customElement("ha-media-upload-button")
class MediaUploadButton extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@property() currentItem?: MediaPlayerItem;
@state() _uploading = 0;
protected render(): TemplateResult {
if (
!this.currentItem ||
!isLocalMediaSourceContentId(this.currentItem.media_content_id || "")
) {
return html``;
}
return html`
<mwc-button
.label=${this._uploading > 0
? this.hass.localize(
"ui.components.media-browser.file_management.uploading",
{
count: this._uploading,
}
)
: this.hass.localize(
"ui.components.media-browser.file_management.add_media"
)}
.disabled=${this._uploading > 0}
@click=${this._startUpload}
>
${this._uploading > 0
? html`
<ha-circular-progress
size="tiny"
active
alt=""
slot="icon"
></ha-circular-progress>
`
: html` <ha-svg-icon .path=${mdiUpload} slot="icon"></ha-svg-icon> `}
</mwc-button>
`;
}
private async _startUpload() {
if (this._uploading > 0) {
return;
}
const input = document.createElement("input");
input.type = "file";
input.accept = "audio/*,video/*,image/*";
input.multiple = true;
input.addEventListener(
"change",
async () => {
fireEvent(this, "uploading");
const files = input.files!;
document.body.removeChild(input);
const target = this.currentItem!.media_content_id!;
for (let i = 0; i < files.length; i++) {
this._uploading = files.length - i;
try {
// eslint-disable-next-line no-await-in-loop
await uploadLocalMedia(this.hass, target, files[i]);
} catch (err: any) {
showAlertDialog(this, {
text: this.hass.localize(
"ui.components.media-browser.file_management.upload_failed",
{
reason: err.message || err,
}
),
});
break;
}
}
this._uploading = 0;
fireEvent(this, "media-refresh");
},
{ once: true }
);
// https://stackoverflow.com/questions/47664777/javascript-file-input-onchange-not-working-ios-safari-only
input.style.display = "none";
document.body.append(input);
input.click();
}
static styles = css`
mwc-button {
/* We use icon + text to show disabled state */
--mdc-button-disabled-ink-color: --mdc-theme-primary;
}
ha-svg-icon[slot="icon"],
ha-circular-progress[slot="icon"] {
vertical-align: middle;
}
`;
}
declare global {
interface HTMLElementTagNameMap {
"ha-media-upload-button": MediaUploadButton;
}
}

View File

@ -0,0 +1,18 @@
import { fireEvent } from "../../common/dom/fire_event";
import { MediaPlayerItem } from "../../data/media-player";
export interface MediaManageDialogParams {
currentItem: MediaPlayerItem;
onClose?: () => void;
}
export const showMediaManageDialog = (
element: HTMLElement,
dialogParams: MediaManageDialogParams
): void => {
fireEvent(element, "show-dialog", {
dialogTag: "dialog-media-manage",
dialogImport: () => import("./dialog-media-manage"),
dialogParams,
});
};

View File

@ -49,3 +49,12 @@ export const uploadLocalMedia = async (
}
return resp.json();
};
export const removeLocalMedia = async (
hass: HomeAssistant,
media_content_id: string
) =>
hass.callWS({
type: "media_source/local_source/remove",
media_content_id,
});

View File

@ -1,9 +1,10 @@
import "@material/mwc-button/mwc-button";
import { mdiAlertOutline } from "@mdi/js";
import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
import { customElement, property, state } from "lit/decorators";
import { classMap } from "lit/directives/class-map";
import { fireEvent } from "../../common/dom/fire_event";
import "../../components/ha-dialog";
import "../../components/ha-svg-icon";
import "../../components/ha-switch";
import "../../components/ha-textfield";
import { haStyleDialog } from "../../resources/styles";
@ -50,20 +51,22 @@ class DialogBox extends LitElement {
?escapeKeyAction=${confirmPrompt}
@closed=${this._dialogClosed}
defaultAction="ignore"
.heading=${this._params.title
.heading=${html`${this._params.warning
? html`<ha-svg-icon
.path=${mdiAlertOutline}
style="color: var(--warning-color)"
></ha-svg-icon> `
: ""}${this._params.title
? this._params.title
: this._params.confirmation &&
this.hass.localize("ui.dialogs.generic.default_confirmation_title")}
this.hass.localize(
"ui.dialogs.generic.default_confirmation_title"
)}`}
>
<div>
${this._params.text
? html`
<p
class=${classMap({
"no-bottom-padding": Boolean(this._params.prompt),
warning: Boolean(this._params.warning),
})}
>
<p class=${this._params.prompt ? "no-bottom-padding" : ""}>
${this._params.text}
</p>
`
@ -172,9 +175,6 @@ class DialogBox extends LitElement {
/* Place above other dialogs */
--dialog-z-index: 104;
}
.warning {
color: var(--warning-color);
}
`,
];
}

View File

@ -1,4 +1,4 @@
import { mdiArrowLeft, mdiUpload } from "@mdi/js";
import { mdiArrowLeft } from "@mdi/js";
import "@polymer/app-layout/app-header/app-header";
import "@polymer/app-layout/app-toolbar/app-toolbar";
import "@material/mwc-button";
@ -15,10 +15,9 @@ import { LocalStorage } from "../../common/decorators/local-storage";
import { fireEvent, HASSDomEvent } from "../../common/dom/fire_event";
import { navigate } from "../../common/navigate";
import "../../components/ha-menu-button";
import "../../components/ha-circular-progress";
import "../../components/ha-icon-button";
import "../../components/ha-svg-icon";
import "../../components/media-player/ha-media-player-browse";
import "../../components/media-player/ha-media-manage-button";
import type {
HaMediaPlayerBrowse,
MediaPlayerItemId,
@ -28,11 +27,7 @@ import {
MediaPickedEvent,
MediaPlayerItem,
} from "../../data/media-player";
import {
isLocalMediaSourceContentId,
resolveMediaSource,
uploadLocalMedia,
} from "../../data/media_source";
import { resolveMediaSource } from "../../data/media_source";
import "../../layouts/ha-app-layout";
import { haStyle } from "../../resources/styles";
import type { HomeAssistant, Route } from "../../types";
@ -66,8 +61,6 @@ class PanelMediaBrowser extends LitElement {
@state() _currentItem?: MediaPlayerItem;
@state() _uploading = 0;
private _navigateIds: MediaPlayerItemId[] = [
{
media_content_id: undefined,
@ -107,43 +100,11 @@ class PanelMediaBrowser extends LitElement {
)
: this._currentItem.title}
</div>
${this._currentItem &&
isLocalMediaSourceContentId(
this._currentItem.media_content_id || ""
)
? html`
<mwc-button
.label=${this._uploading > 0
? this.hass.localize(
"ui.components.media-browser.file_management.uploading",
{
count: this._uploading,
}
)
: this.hass.localize(
"ui.components.media-browser.file_management.add_media"
)}
.disabled=${this._uploading > 0}
@click=${this._startUpload}
>
${this._uploading > 0
? html`
<ha-circular-progress
size="tiny"
active
alt=""
slot="icon"
></ha-circular-progress>
`
: html`
<ha-svg-icon
.path=${mdiUpload}
slot="icon"
></ha-svg-icon>
`}
</mwc-button>
`
: ""}
<ha-media-manage-button
.hass=${this.hass}
.currentItem=${this._currentItem}
@media-refresh=${this._refreshMedia}
></ha-media-manage-button>
</app-toolbar>
</app-header>
<ha-media-player-browse
@ -290,57 +251,16 @@ class PanelMediaBrowser extends LitElement {
navigate(createMediaPanelUrl(entityId, this._navigateIds));
}
private async _startUpload() {
if (this._uploading > 0) {
return;
}
const input = document.createElement("input");
input.type = "file";
input.accept = "audio/*,video/*,image/*";
input.multiple = true;
input.addEventListener(
"change",
async () => {
const files = input.files!;
document.body.removeChild(input);
const target = this._currentItem!.media_content_id!;
for (let i = 0; i < files.length; i++) {
this._uploading = files.length - i;
try {
// eslint-disable-next-line no-await-in-loop
await uploadLocalMedia(this.hass, target, files[i]);
} catch (err: any) {
showAlertDialog(this, {
text: this.hass.localize(
"ui.components.media-browser.file_management.upload_failed",
{
reason: err.message || err,
}
),
});
break;
}
}
this._uploading = 0;
await this._browser.refresh();
},
{ once: true }
);
// https://stackoverflow.com/questions/47664777/javascript-file-input-onchange-not-working-ios-safari-only
input.style.display = "none";
document.body.append(input);
input.click();
private _refreshMedia() {
this._browser.refresh();
}
static get styles(): CSSResultGroup {
return [
haStyle,
css`
app-toolbar mwc-button {
app-toolbar {
--mdc-theme-primary: var(--app-header-text-color);
/* We use icon + text to show disabled state */
--mdc-button-disabled-ink-color: var(--app-header-text-color);
}
ha-media-player-browse {
@ -357,11 +277,6 @@ class PanelMediaBrowser extends LitElement {
left: 0;
right: 0;
}
ha-svg-icon[slot="icon"],
ha-circular-progress[slot="icon"] {
vertical-align: middle;
}
`,
];
}

View File

@ -3,7 +3,7 @@ import "@polymer/app-layout/app-header/app-header";
import "@polymer/app-layout/app-toolbar/app-toolbar";
import { UnsubscribeFunc } from "home-assistant-js-websocket";
import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
import { property, state } from "lit/decorators";
import { customElement, property, state } from "lit/decorators";
import { fireEvent } from "../../common/dom/fire_event";
import "../../components/ha-card";
import "../../components/ha-menu-button";
@ -33,6 +33,7 @@ import "./ha-refresh-tokens-card";
import "./ha-set-suspend-row";
import "./ha-set-vibrate-row";
@customElement("ha-panel-profile")
class HaPanelProfile extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@ -252,5 +253,8 @@ class HaPanelProfile extends LitElement {
];
}
}
customElements.define("ha-panel-profile", HaPanelProfile);
declare global {
interface HTMLElementTagNameMap {
"ha-panel-profile": HaPanelProfile;
}
}

View File

@ -549,10 +549,17 @@
"no_media_folder": "It looks like you have not yet created a media directory.",
"setup_local_help": "Check the {documentation} on how to setup local media.",
"file_management": {
"title": "Media Management",
"manage": "Manage",
"no_items": "No media items found",
"folders_not_supported": "Folders can not be managed via the UI.",
"highlight_button": "Click here to upload your first media",
"upload_failed": "Upload failed: {reason}",
"add_media": "Add Media",
"uploading": "Uploading {count} {count, plural,\n one {file}\n other {files}\n}"
"uploading": "Uploading {count} {count, plural,\n one {file}\n other {files}\n}",
"confirm_delete": "Do you want to delete {count} {count, plural,\n one {file}\n other {files}\n}?",
"delete": "Delete {count}",
"deleting": "Deleting {count}"
},
"class": {
"album": "Album",

View File

@ -1960,6 +1960,15 @@ __metadata:
languageName: node
linkType: hard
"@lit-labs/motion@npm:^1.0.2":
version: 1.0.2
resolution: "@lit-labs/motion@npm:1.0.2"
dependencies:
lit: ^2.0.0
checksum: 598e0be22a3f931ec971fa001e863c5a4dd82f572d8d0214211bde1d6403b00e3c8fafa92f30b0c02b7272bc12510ec40060bf2c8ab18151bdb264cf32f0ef71
languageName: node
linkType: hard
"@lit-labs/virtualizer@0.7.0-pre.2":
version: 0.7.0-pre.2
resolution: "@lit-labs/virtualizer@npm:0.7.0-pre.2"
@ -9131,6 +9140,7 @@ fsevents@^1.2.7:
"@fullcalendar/interaction": 5.9.0
"@fullcalendar/list": 5.9.0
"@koa/cors": ^3.1.0
"@lit-labs/motion": ^1.0.2
"@lit-labs/virtualizer": "patch:@lit-labs/virtualizer@0.7.0-pre.2#./.yarn/patches/@lit-labs/virtualizer/event-target-shim.patch"
"@material/chips": 14.0.0-canary.261f2db59.0
"@material/data-table": 14.0.0-canary.261f2db59.0