feat(sdk,client): add menu to help navigation between apps
All checks were successful
arcad/edge/pipeline/head This commit looks good
6
pkg/sdk/client/src/components/icons/cloud.svg
Normal file
@ -0,0 +1,6 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5"
|
||||
stroke="currentColor" class="w-6 h-6">
|
||||
<path stroke-linecap="round" stroke-linejoin="round"
|
||||
d="M2.25 15a4.5 4.5 0 004.5 4.5H18a3.75 3.75 0 001.332-7.257 3 3 0 00-3.758-3.848 5.25 5.25 0 00-10.233 2.33A4.502 4.502 0 002.25 15z" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 346 B |
6
pkg/sdk/client/src/components/icons/cog.svg
Normal file
@ -0,0 +1,6 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5"
|
||||
stroke="currentColor" class="w-6 h-6">
|
||||
<path stroke-linecap="round" stroke-linejoin="round"
|
||||
d="M9.594 3.94c.09-.542.56-.94 1.11-.94h2.593c.55 0 1.02.398 1.11.94l.213 1.281c.063.374.313.686.645.87.074.04.147.083.22.127.324.196.72.257 1.075.124l1.217-.456a1.125 1.125 0 011.37.49l1.296 2.247a1.125 1.125 0 01-.26 1.431l-1.003.827c-.293.24-.438.613-.431.992a6.759 6.759 0 010 .255c-.007.378.138.75.43.99l1.005.828c.424.35.534.954.26 1.43l-1.298 2.247a1.125 1.125 0 01-1.369.491l-1.217-.456c-.355-.133-.75-.072-1.076.124a6.57 6.57 0 01-.22.128c-.331.183-.581.495-.644.869l-.213 1.28c-.09.543-.56.941-1.11.941h-2.594c-.55 0-1.02-.398-1.11-.94l-.213-1.281c-.062-.374-.312-.686-.644-.87a6.52 6.52 0 01-.22-.127c-.325-.196-.72-.257-1.076-.124l-1.217.456a1.125 1.125 0 01-1.369-.49l-1.297-2.247a1.125 1.125 0 01.26-1.431l1.004-.827c.292-.24.437-.613.43-.992a6.932 6.932 0 010-.255c.007-.378-.138-.75-.43-.99l-1.004-.828a1.125 1.125 0 01-.26-1.43l1.297-2.247a1.125 1.125 0 011.37-.491l1.216.456c.356.133.751.072 1.076-.124.072-.044.146-.087.22-.128.332-.183.582-.495.644-.869l.214-1.281z" />
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z" />
|
||||
</svg>
|
After Width: | Height: | Size: 1.3 KiB |
6
pkg/sdk/client/src/components/icons/home.svg
Normal file
@ -0,0 +1,6 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5"
|
||||
stroke="currentColor" class="w-6 h-6">
|
||||
<path stroke-linecap="round" stroke-linejoin="round"
|
||||
d="M2.25 12l8.954-8.955c.44-.439 1.152-.439 1.591 0L21.75 12M4.5 9.75v10.125c0 .621.504 1.125 1.125 1.125H9.75v-4.875c0-.621.504-1.125 1.125-1.125h2.25c.621 0 1.125.504 1.125 1.125V21h4.125c.621 0 1.125-.504 1.125-1.125V9.75M8.25 21h8.25" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 450 B |
9
pkg/sdk/client/src/components/icons/index.ts
Normal file
@ -0,0 +1,9 @@
|
||||
import UserCircleIcon from './user-circle.svg';
|
||||
import MenuIcon from './menu.svg';
|
||||
import CloudIcon from './cloud.svg';
|
||||
import LoginIcon from './login.svg';
|
||||
import HomeIcon from './home.svg';
|
||||
import LinkIcon from './link.svg';
|
||||
import LogoutIcon from './logout.svg';
|
||||
|
||||
export { UserCircleIcon, MenuIcon, CloudIcon, LoginIcon, HomeIcon, LinkIcon, LogoutIcon }
|
5
pkg/sdk/client/src/components/icons/link.svg
Normal file
@ -0,0 +1,5 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5"
|
||||
stroke="currentColor" class="w-6 h-6">
|
||||
<path stroke-linecap="round" stroke-linejoin="round"
|
||||
d="M13.19 8.688a4.5 4.5 0 011.242 7.244l-4.5 4.5a4.5 4.5 0 01-6.364-6.364l1.757-1.757m13.35-.622l1.757-1.757a4.5 4.5 0 00-6.364-6.364l-4.5 4.5a4.5 4.5 0 001.242 7.244" />
|
||||
</svg>
|
After Width: | Height: | Size: 376 B |
6
pkg/sdk/client/src/components/icons/login.svg
Normal file
@ -0,0 +1,6 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5"
|
||||
stroke="currentColor" class="w-6 h-6">
|
||||
<path stroke-linecap="round" stroke-linejoin="round"
|
||||
d="M15.75 9V5.25A2.25 2.25 0 0013.5 3h-6a2.25 2.25 0 00-2.25 2.25v13.5A2.25 2.25 0 007.5 21h6a2.25 2.25 0 002.25-2.25V15m3 0l3-3m0 0l-3-3m3 3H9" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 356 B |
6
pkg/sdk/client/src/components/icons/logout.svg
Normal file
@ -0,0 +1,6 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5"
|
||||
stroke="currentColor" class="w-6 h-6">
|
||||
<path stroke-linecap="round" stroke-linejoin="round"
|
||||
d="M15.75 9V5.25A2.25 2.25 0 0013.5 3h-6a2.25 2.25 0 00-2.25 2.25v13.5A2.25 2.25 0 007.5 21h6a2.25 2.25 0 002.25-2.25V15M12 9l-3 3m0 0l3 3m-3-3h12.75" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 362 B |
6
pkg/sdk/client/src/components/icons/menu.svg
Normal file
@ -0,0 +1,6 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5"
|
||||
stroke="currentColor" class="w-6 h-6">
|
||||
<path stroke-linecap="round" stroke-linejoin="round"
|
||||
d="M3.75 6.75h16.5M3.75 12h16.5m-16.5 5.25h16.5" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 260 B |
5
pkg/sdk/client/src/components/icons/square.svg
Normal file
@ -0,0 +1,5 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5"
|
||||
stroke="currentColor" class="w-6 h-6">
|
||||
<path stroke-linecap="round" stroke-linejoin="round"
|
||||
d="M3.75 6A2.25 2.25 0 016 3.75h2.25A2.25 2.25 0 0110.5 6v2.25a2.25 2.25 0 01-2.25 2.25H6a2.25 2.25 0 01-2.25-2.25V6zM3.75 15.75A2.25 2.25 0 016 13.5h2.25a2.25 2.25 0 012.25 2.25V18a2.25 2.25 0 01-2.25 2.25H6A2.25 2.25 0 013.75 18v-2.25zM13.5 6a2.25 2.25 0 012.25-2.25H18A2.25 2.25 0 0120.25 6v2.25A2.25 2.25 0 0118 10.5h-2.25a2.25 2.25 0 01-2.25-2.25V6zM13.5 15.75a2.25 2.25 0 012.25-2.25H18a2.25 2.25 0 012.25 2.25V18A2.25 2.25 0 0118 20.25h-2.25A2.25 2.25 0 0113.5 18v-2.25z" />
|
||||
</svg>
|
After Width: | Height: | Size: 687 B |
6
pkg/sdk/client/src/components/icons/user-circle.svg
Normal file
@ -0,0 +1,6 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5"
|
||||
stroke="currentColor" class="w-6 h-6">
|
||||
<path stroke-linecap="round" stroke-linejoin="round"
|
||||
d="M17.982 18.725A7.488 7.488 0 0012 15.75a7.488 7.488 0 00-5.982 2.975m11.963 0a9 9 0 10-11.963 0m11.963 0A8.966 8.966 0 0112 21a8.966 8.966 0 01-5.982-2.275M15 9.75a3 3 0 11-6 0 3 3 0 016 0z" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 405 B |
130
pkg/sdk/client/src/components/menu-item.ts
Normal file
@ -0,0 +1,130 @@
|
||||
import { LitElement, html, css } from 'lit';
|
||||
import { property, state } from 'lit/decorators.js';
|
||||
|
||||
export const EVENT_MENU_ITEM_SELECTED = 'menu-item-selected';
|
||||
export const EVENT_MENU_ITEM_UNSELECTED = 'menu-item-unselected';
|
||||
|
||||
export class MenuItem extends LitElement {
|
||||
@property({ attribute: 'icon-url', type: String })
|
||||
iconUrl: string;
|
||||
|
||||
@property({ attribute: 'label', type: String })
|
||||
label: string;
|
||||
|
||||
static styles = css`
|
||||
:host {
|
||||
display: inline-block;
|
||||
height: 100%;
|
||||
flex: 1;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-bottom: 1px solid rgb(229,231,235);
|
||||
border-top: 10px solid transparent;
|
||||
transition: all 100ms ease-out;
|
||||
background-color: #fff;
|
||||
}
|
||||
:host(:hover) {
|
||||
background-color: rgb(249,250,251);
|
||||
}
|
||||
:host(.selected) {
|
||||
border-top: 10px solid #03A9F4;
|
||||
border-bottom: 1px solid transparent;
|
||||
background-color: #fff;
|
||||
}
|
||||
:host(.unselected) {
|
||||
background-color: hsl(210 20% 95% / 1);
|
||||
}
|
||||
.menu-item-icon {
|
||||
height: 30px;
|
||||
width: 30px;
|
||||
overflow: hidden;
|
||||
}
|
||||
.menu-item-icon > img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
.menu-item-panel {
|
||||
display: none;
|
||||
position: fixed;
|
||||
top: 65px;
|
||||
left: 0;
|
||||
right: 0;
|
||||
z-index: 9999;
|
||||
background-color: #fff;
|
||||
box-shadow: 0px 4px 5px 0px hsl(0deg 0% 0% / 10%);
|
||||
max-height: 75%;
|
||||
overflow-y: auto;
|
||||
}
|
||||
:host(.selected) .menu-item-panel {
|
||||
display: block;
|
||||
}
|
||||
.menu-item-label {
|
||||
font-family: Arial, Helvetica Neue, Helvetica, sans-serif;
|
||||
color: black;
|
||||
font-size: 14px;
|
||||
margin: 3px 0;
|
||||
}
|
||||
`;
|
||||
|
||||
@state()
|
||||
selected: boolean
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
this.addEventListener('click', this._handleClick.bind(this));
|
||||
}
|
||||
|
||||
|
||||
render() {
|
||||
return html`
|
||||
<div class="menu-item-icon">
|
||||
${
|
||||
this.iconUrl ?
|
||||
html`<img src="${this.iconUrl}" />` :
|
||||
''
|
||||
}
|
||||
</div>
|
||||
<div class="menu-item-label">
|
||||
${this.label}
|
||||
</div>
|
||||
<div class="menu-item-panel">
|
||||
<slot></slot>
|
||||
</div>
|
||||
`
|
||||
}
|
||||
|
||||
_handleClick() {
|
||||
if (this.selected) {
|
||||
this.unselect();
|
||||
} else {
|
||||
this.select();
|
||||
}
|
||||
}
|
||||
|
||||
select() {
|
||||
this.selected = true;
|
||||
const event = new CustomEvent(EVENT_MENU_ITEM_SELECTED, {
|
||||
bubbles: true,
|
||||
composed: true,
|
||||
detail: {
|
||||
element: this,
|
||||
}
|
||||
});
|
||||
this.dispatchEvent(event);
|
||||
}
|
||||
|
||||
unselect() {
|
||||
this.selected = false;
|
||||
const event = new CustomEvent(EVENT_MENU_ITEM_UNSELECTED, {
|
||||
bubbles: true,
|
||||
composed: true,
|
||||
detail: {
|
||||
element: this,
|
||||
}
|
||||
});
|
||||
this.dispatchEvent(event);
|
||||
}
|
||||
}
|
63
pkg/sdk/client/src/components/menu-sub-item.ts
Normal file
@ -0,0 +1,63 @@
|
||||
import { LitElement, html, css } from 'lit';
|
||||
import { property, state } from 'lit/decorators.js';
|
||||
import { LinkIcon } from './icons';
|
||||
|
||||
export class MenuSubItem extends LitElement {
|
||||
@property({ attribute: 'label' })
|
||||
label: string;
|
||||
|
||||
@property({ attribute: 'icon-url' })
|
||||
iconUrl: string;
|
||||
|
||||
@property({ attribute: 'link-url' })
|
||||
linkUrl: string;
|
||||
|
||||
@property({ attribute: 'inactive', type: Boolean })
|
||||
inactive: boolean;
|
||||
|
||||
static styles = css`
|
||||
:host {
|
||||
display: block;
|
||||
flex: 1;
|
||||
cursor: pointer;
|
||||
transition: all 100ms ease-out;
|
||||
border-bottom: 1px solid rgb(229,231,235);
|
||||
padding: 5px 0 5px 7px;
|
||||
border-left: 5px solid transparent;
|
||||
}
|
||||
:host([inactive]) {
|
||||
cursor: initial;
|
||||
}
|
||||
:host(:hover) {
|
||||
border-left: 5px solid #03A9F4;
|
||||
background-color: rgb(28 169 247 / 10%);
|
||||
}
|
||||
a {
|
||||
font-size: 20px;
|
||||
font-family: Arial, Helvetica Neue, Helvetica, sans-serif;
|
||||
text-decoration: none;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
justify-content: flex-start;
|
||||
height: 40px;
|
||||
color: black;
|
||||
}
|
||||
.edge-menu-sub-item-icon {
|
||||
height: 25px;
|
||||
width: 25px;
|
||||
}
|
||||
.edge-menu-sub-item-label {
|
||||
margin-left: 5px;
|
||||
}
|
||||
`;
|
||||
|
||||
render() {
|
||||
return html`
|
||||
<a href="${this.linkUrl ? this.linkUrl : '#'}">
|
||||
<img class="edge-menu-sub-item-icon" src="${this.iconUrl ? this.iconUrl : LinkIcon}" />
|
||||
<span class="edge-menu-sub-item-label">${this.label}</span>
|
||||
</a>
|
||||
`
|
||||
}
|
||||
}
|
239
pkg/sdk/client/src/components/menu.ts
Normal file
@ -0,0 +1,239 @@
|
||||
import { LitElement, html, css } from 'lit';
|
||||
import { property, queryAll } from 'lit/decorators.js';
|
||||
import { CloudIcon, HomeIcon, LoginIcon, LinkIcon, MenuIcon, UserCircleIcon, LogoutIcon } from './icons'
|
||||
import { EVENT_MENU_ITEM_SELECTED, EVENT_MENU_ITEM_UNSELECTED, MenuItem } from './menu-item';
|
||||
import { MenuSubItem } from './menu-sub-item';
|
||||
|
||||
interface Manifest {
|
||||
id: string
|
||||
description: string
|
||||
metadata: { [key: string]: any }
|
||||
tags: string[]
|
||||
title: string
|
||||
version: string
|
||||
url?: string
|
||||
}
|
||||
|
||||
interface Profile {
|
||||
sub?: string
|
||||
preferred_username?: string
|
||||
iss?: string
|
||||
edge_role?: string
|
||||
edge_tenant?: string
|
||||
edge_entrypoint?: string
|
||||
}
|
||||
|
||||
const BASE_API_URL = '/edge/api/v1';
|
||||
|
||||
enum Roles {
|
||||
visitor = 0,
|
||||
user = 1,
|
||||
superuser = 2,
|
||||
admin = 3,
|
||||
superadmin = 4
|
||||
}
|
||||
|
||||
export class Menu extends LitElement {
|
||||
@property({ attribute: 'app-icon-url', type: String })
|
||||
appIconUrl: string;
|
||||
|
||||
@property({ attribute: 'app-title', type: String })
|
||||
appTitle: string;
|
||||
|
||||
@property({ attribute: 'hidden', type: Boolean })
|
||||
hidden: boolean;
|
||||
|
||||
@property()
|
||||
_apps: Manifest[] = []
|
||||
|
||||
@property()
|
||||
_profile: Profile
|
||||
|
||||
static styles = css`
|
||||
:host {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
height: 60px;
|
||||
background-color: #fff;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
z-index: 9999;
|
||||
}
|
||||
|
||||
:host([hidden]) {
|
||||
display: none;
|
||||
}
|
||||
`;
|
||||
|
||||
@queryAll('edge-menu-item')
|
||||
_menuItems: NodeListOf<MenuItem>
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
this.addEventListener(EVENT_MENU_ITEM_SELECTED, this._handleMenuItemSelected.bind(this));
|
||||
this.addEventListener(EVENT_MENU_ITEM_UNSELECTED, this._handleMenuItemUnselected.bind(this));
|
||||
|
||||
this._fetchApps();
|
||||
this._fetchProfile();
|
||||
}
|
||||
|
||||
render() {
|
||||
const apps = this._renderApps()
|
||||
|
||||
return html`
|
||||
<edge-menu-item name='menu' label="${ this.appTitle || "App" }" icon-url='${ this.appIconUrl || MenuIcon }'>
|
||||
<edge-menu-sub-item name='home' label='Home' icon-url='${HomeIcon}' link-url='/'></edge-menu-sub-item>
|
||||
<slot></slot>
|
||||
</edge-menu-item>
|
||||
${ this._renderApps() }
|
||||
${ this._renderProfile() }
|
||||
`;
|
||||
}
|
||||
|
||||
_fetchApps() {
|
||||
return fetch(`${BASE_API_URL}/apps`)
|
||||
.then(res => res.json())
|
||||
.then(result => {
|
||||
if (result.error) {
|
||||
throw new Error(`Unexpected server error: ${result.error.code}`);
|
||||
}
|
||||
|
||||
return result.data?.manifests || [];
|
||||
})
|
||||
.then((manifests: Manifest[]) => {
|
||||
const promises = manifests.map((m: Manifest) => {
|
||||
const fetchOptions: RequestInit = {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({}),
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
}
|
||||
};
|
||||
return fetch(`${BASE_API_URL}/apps/${m.id}/url`, fetchOptions)
|
||||
.then(res => res.json())
|
||||
.then(result => {
|
||||
if (result.error) {
|
||||
throw new Error(`Unexpected server error: ${result.error.code}`);
|
||||
}
|
||||
|
||||
m.url = result.data?.url;
|
||||
|
||||
return m;
|
||||
})
|
||||
;
|
||||
});
|
||||
|
||||
return Promise.all(promises);
|
||||
})
|
||||
.then((manifests: Manifest[]) => {
|
||||
this._apps = manifests;
|
||||
})
|
||||
.catch(err => console.error(err))
|
||||
}
|
||||
|
||||
_fetchProfile() {
|
||||
return fetch(`${BASE_API_URL}/profile`)
|
||||
.then(res => res.json())
|
||||
.then(result => {
|
||||
if (result.error) {
|
||||
switch (result.error.code) {
|
||||
case "unauthorized":
|
||||
return null;
|
||||
default:
|
||||
throw new Error(`Unexpected server error: ${result.error.code}`);
|
||||
}
|
||||
}
|
||||
|
||||
return result.data?.profile;
|
||||
})
|
||||
.then(profile => {
|
||||
this._profile = profile;
|
||||
})
|
||||
.catch(err => console.error(err))
|
||||
;
|
||||
}
|
||||
|
||||
_renderApps() {
|
||||
const apps = this._apps
|
||||
.filter(manifest => this._canAccess(manifest))
|
||||
.map(manifest => {
|
||||
const iconUrl = ( ( manifest.url || '') + ( manifest.metadata?.paths?.icon || '' ) ) || LinkIcon;
|
||||
return html`
|
||||
<edge-menu-sub-item
|
||||
name='${ manifest.id }'
|
||||
label='${ manifest.title }'
|
||||
icon-url='${ iconUrl }'
|
||||
link-url='${ manifest.url || '#' }'>
|
||||
</edge-menu-sub-item>
|
||||
`
|
||||
});
|
||||
|
||||
return html`
|
||||
<edge-menu-item name='apps' label='Apps' icon-url='${CloudIcon}'>
|
||||
${ apps }
|
||||
</edge-menu-item>
|
||||
`;
|
||||
}
|
||||
|
||||
_canAccess(manifest: Manifest): boolean {
|
||||
const currentRole = this._profile?.edge_role || 'visitor';
|
||||
const minimumRole = manifest.metadata?.minimumRole || 'visitor';
|
||||
|
||||
return Roles[currentRole] >= Roles[minimumRole];
|
||||
}
|
||||
|
||||
_renderProfile() {
|
||||
const profile = this._profile;
|
||||
return html`
|
||||
<edge-menu-item name='profile' label="${profile?.preferred_username || 'Profile'}" icon-url='${UserCircleIcon}'>
|
||||
${
|
||||
profile ?
|
||||
html`<edge-menu-sub-item name='login' label='Logout' icon-url='${LogoutIcon}' link-url='/edge/auth/logout'></edge-menu-sub-item>` :
|
||||
html`<edge-menu-sub-item name='login' label='Login' icon-url='${LoginIcon}' link-url='/edge/auth/login'></edge-menu-sub-item>`
|
||||
}
|
||||
</edge-menu-item>
|
||||
`;
|
||||
}
|
||||
|
||||
_handleMenuItemSelected(evt: CustomEvent) {
|
||||
const selectedMenuItem: HTMLElement = evt.detail.element;
|
||||
|
||||
selectedMenuItem.classList.add('selected');
|
||||
selectedMenuItem.classList.remove('unselected');
|
||||
|
||||
for (let item, i = 0; (item = this._menuItems[i]); i++) {
|
||||
if (item === selectedMenuItem) continue;
|
||||
|
||||
item.unselect();
|
||||
item.classList.add('unselected');
|
||||
}
|
||||
}
|
||||
|
||||
_handleMenuItemUnselected(evt: CustomEvent) {
|
||||
const unselectedMenuItem: HTMLElement = evt.detail.element;
|
||||
|
||||
unselectedMenuItem.classList.remove('selected');
|
||||
|
||||
const hasSelectedItem = this.renderRoot.querySelectorAll('edge-menu-item.selected').length !== 0
|
||||
|
||||
if (hasSelectedItem) {
|
||||
return
|
||||
}
|
||||
|
||||
for (let item, i = 0; (item = this._menuItems[i]); i++) {
|
||||
item.classList.remove('unselected');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"edge-menu": Menu;
|
||||
"edge-menu-item": MenuItem;
|
||||
"edge-menu-sub-item": MenuSubItem;
|
||||
}
|
||||
}
|