diff --git a/source/ui/Logo.ts b/source/ui/Logo.ts deleted file mode 100644 index af35805f..00000000 --- a/source/ui/Logo.ts +++ /dev/null @@ -1,61 +0,0 @@ -/** - * 3D Foundation Project - * Copyright 2019 Smithsonian Institution - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import CustomElement, { customElement, html } from "@ff/ui/CustomElement"; - -//////////////////////////////////////////////////////////////////////////////// - -@customElement("sv-logo") -export default class Logo extends CustomElement -{ - protected static readonly h = html`
- - -
`; - protected static readonly text = html`
- - - - - - - - - - -
`; - - protected firstConnected() - { - super.firstConnected(); - this.classList.add("sv-logo"); - } - - protected render() - { - return html`
${Logo.h}
${Logo.h}${Logo.text}
`; - } -} \ No newline at end of file diff --git a/source/ui/MainView.ts b/source/ui/MainView.ts index dfabc4f0..b6030832 100644 --- a/source/ui/MainView.ts +++ b/source/ui/MainView.ts @@ -1,10 +1,10 @@ import { LitElement, html, customElement } from 'lit-element'; -import Notification from "@ff/ui/Notification"; -import styles from '!lit-css-loader?{"specifier":"lit-element"}!sass-loader!./styles.scss'; +import styles from '!lit-css-loader?{"specifier":"lit-element"}!sass-loader!./styles/main.scss'; + +import "./styles/globals.scss"; -import "./globals.scss"; import "./composants/UploadButton"; import "./composants/navbar/NavLink"; @@ -19,28 +19,13 @@ import "./screens/UserSettings"; import "./screens/Home" import "./composants/Modal"; +import Notification from "./composants/Notification"; + import { updateLogin, withUser } from './state/auth'; import Modal from './composants/Modal'; import i18n from './state/translate'; import { route, router } from './state/router'; -/** - * Simplified from path-to-regex for our simple use-case - * @see https://github.com/pillarjs/path-to-regexp - */ -function toRegex(path:string|RegExp){ - if(path instanceof RegExp) return path; - const matcher = `[^\/#\?]+` - let parts = path.split("/") - .filter(p=>p) - .map( p =>{ - let param = /:(\w+)/.exec(p); - if(!param) return p; - return `(?<${param[1]}>${matcher})`; - }) - return new RegExp(`^/${parts.join("/")}\/?$`,"i") -} - @customElement("ecorpus-main") export default class MainView extends router(i18n(withUser(LitElement))){ @@ -57,7 +42,7 @@ export default class MainView extends router(i18n(withUser(LitElement))){ connectedCallback(): void { super.connectedCallback(); - Notification.shadowRootNode = this.shadowRoot; + // FIXME : configure notifications updateLogin().catch(e => { Modal.show({header: "Error", body: e.message}); }); diff --git a/source/ui/assets/favicon.png b/source/ui/assets/favicon.png new file mode 100644 index 00000000..6e781651 Binary files /dev/null and b/source/ui/assets/favicon.png differ diff --git a/source/ui/assets/images/defaultSprite.svg b/source/ui/assets/images/defaultSprite.svg new file mode 100644 index 00000000..933235e5 --- /dev/null +++ b/source/ui/assets/images/defaultSprite.svg @@ -0,0 +1,5 @@ + + Cube + + + \ No newline at end of file diff --git a/source/ui/assets/images/logo-full.svg b/source/ui/assets/images/logo-full.svg new file mode 100644 index 00000000..eea97313 --- /dev/null +++ b/source/ui/assets/images/logo-full.svg @@ -0,0 +1,30 @@ + + + + + + + + + + + + + + diff --git a/source/ui/assets/images/logo-sm.svg b/source/ui/assets/images/logo-sm.svg new file mode 100644 index 00000000..b7b94ba4 --- /dev/null +++ b/source/ui/assets/images/logo-sm.svg @@ -0,0 +1,10 @@ + + + diff --git a/source/ui/assets/images/sketch_ethesaurus.png b/source/ui/assets/images/sketch_ethesaurus.png new file mode 100644 index 00000000..fcfcf854 Binary files /dev/null and b/source/ui/assets/images/sketch_ethesaurus.png differ diff --git a/source/ui/assets/images/spinner.svg b/source/ui/assets/images/spinner.svg new file mode 100644 index 00000000..ccf98dc0 --- /dev/null +++ b/source/ui/assets/images/spinner.svg @@ -0,0 +1,27 @@ + + + + + + + + \ No newline at end of file diff --git a/source/ui/composants/Button.ts b/source/ui/composants/Button.ts new file mode 100644 index 00000000..97eea62f --- /dev/null +++ b/source/ui/composants/Button.ts @@ -0,0 +1,168 @@ +/** + * FF Typescript Foundation Library + * Copyright 2019 Ralph Wiedemeier, Frame Factory GmbH + * + * License: MIT + */ + +import "./Icon"; + +import { customElement, property, html, PropertyValues, LitElement } from "lit-element"; + +//////////////////////////////////////////////////////////////////////////////// + +/** + * Emitted by [[Button]] if clicked. + * @event + */ +export interface IButtonClickEvent extends MouseEvent +{ + type: "click"; + target: Button; +} + +export interface IButtonKeyboardEvent extends KeyboardEvent +{ + type: "click"; + target: Button; +} + +/** + * Custom element displaying a button with a text and/or an icon. + * The button emits a [[IButtonClickEvent]] if clicked. + * Classes assigned: "ff-button", "ff-control". + */ +@customElement("ui-button") +export default class Button extends LitElement +{ + /** Optional name to identify the button. */ + @property({ type: String }) + name = ""; + + /** Optional index to identify the button. */ + @property({ type: Number }) + index = 0; + + @property({ type: Number }) + selectedIndex = -1; + + @property({ type: Number }) + tabbingIndex = 0; + + /** If true, adds "ui-selected" class to element. */ + @property({ type: Boolean, reflect: true }) + selected = false; + + /** If true, toggles selected state every time the button is clicked. */ + @property({ type: Boolean }) + selectable = false; + + @property({ type: Boolean }) + disabled = false; + + /** Optional text to be displayed on the button. */ + @property() + text: string; + + /** Optional name of the icon to be displayed on the button. */ + @property() + icon = ""; + + /** Optional role - defaults to 'button'. */ + @property() + role = "button"; + + /** If true, displays a downward facing triangle at the right side. */ + @property({ type: Boolean }) + caret = false; + + @property({ type: Boolean }) + inline = false; + + @property({ type: Boolean }) + transparent = false; + + constructor() + { + super(); + + this.addEventListener("click", (e) => this.onClick(e)); + this.addEventListener("keydown", (e) => this.onKeyDown(e)); + } + + protected firstConnected() + { + this.tabIndex = this.tabbingIndex; + this.setAttribute("role", this.role); + this.classList.add("ff-button"); + } + + protected shouldUpdate(changedProperties: PropertyValues) + { + if (changedProperties.has("selectedIndex") || changedProperties.has("index")) { + if (this.selectedIndex >= 0) { + this.selected = this.index === this.selectedIndex; + } + } + + if (changedProperties.has("disabled")) { + this.classList.toggle("ff-disabled"); + } + + return true; + } + + protected update(changedProperties: PropertyValues) + { + this.classList.remove("ff-inline", "ff-transparent", "ff-control"); + + if (this.inline) { + this.classList.add("ff-inline"); + } + else if (this.transparent) { + this.classList.add("ff-transparent"); + } + else { + this.classList.add("ff-control"); + } + + super.update(changedProperties); + } + + protected render() + { + return html`${this.renderIcon()}${this.renderText()}${this.renderCaret()}`; + } + + protected renderIcon() + { + return this.icon ? html`` : null; + } + + protected renderText() + { + return this.text ? html`
${this.text}
` : null; + } + + protected renderCaret() + { + return this.caret ? html`
` : null; + } + + protected onClick(event: MouseEvent) + { + if (this.selectable) { + this.selected = !this.selected; + } + } + + protected onKeyDown(event: KeyboardEvent) + { + const activeElement = document.activeElement.shadowRoot ? document.activeElement.shadowRoot.activeElement : document.activeElement; + + if (activeElement === this && (event.code === "Space" || event.code === "Enter")) { + event.preventDefault(); + this.dispatchEvent(new MouseEvent("click", { bubbles: true })); + } + } +} diff --git a/source/ui/composants/DocView.ts b/source/ui/composants/DocView.ts index 945779b8..9486137a 100644 --- a/source/ui/composants/DocView.ts +++ b/source/ui/composants/DocView.ts @@ -2,13 +2,12 @@ import { customElement, property, html, TemplateResult, LitElement, css } from "lit-element"; import {unsafeHTML} from 'lit-html/directives/unsafe-html.js'; -import "@ff/ui/Button"; -import "client/ui/Spinner"; +import "./Spinner"; import i18n from "../state/translate"; import { Language } from "../state/strings"; -import styles from '!lit-css-loader?{"specifier":"lit-element"}!sass-loader!../styles.scss'; +import styles from '!lit-css-loader?{"specifier":"lit-element"}!sass-loader!../styles/common.scss'; /** * Main UI view for the Voyager Explorer application. @@ -91,7 +90,7 @@ import styles from '!lit-css-loader?{"specifier":"lit-element"}!sass-loader!../s if(this.error){ return html`

Error

${this.error.message}
`; }else if(!this.content || this.content =="loading..."){ - return html`
Loading...
` + return html`
Loading...
` } return html`${unsafeHTML(this.content)}`; } diff --git a/source/ui/composants/Icon.ts b/source/ui/composants/Icon.ts index 18418c78..d288efcf 100644 --- a/source/ui/composants/Icon.ts +++ b/source/ui/composants/Icon.ts @@ -1,7 +1,88 @@ -import {html} from "lit-element"; -import Icon from "@ff/ui/Icon" +import {LitElement, customElement, property, html, TemplateResult} from "lit-element"; +/** + * Imported from + * FF Typescript Foundation Library + * Copyright 2019 Ralph Wiedemeier, Frame Factory GmbH + * + * License: MIT + */ +@customElement("ui-icon") +export default class Icon extends LitElement +{ + protected static templates = {}; + createRenderRoot() { + return this; + } + + static add(name: string, template: TemplateResult) + { + if (Icon.templates[name]) { + throw new Error(`icon already registered: '${name}'`); + } + Icon.templates[name] = template; + } + + static getTemplateNames() + { + return Object.keys(Icon.templates); + } + + @property({ attribute: false }) + template: TemplateResult = null; + + @property({ type: String }) + name: string; + + constructor(name?: string) + { + super(); + this.name = name || ""; + } + + protected firstConnected() + { + this.classList.add("ui-icon"); + } + + protected render() + { + if (this.name) { + const template = (this.constructor as typeof Icon).templates[this.name]; + if (!template) { + console.warn(`icon not found: '${this.name}'`); + } + return template; + } + if (this.template) { + return this.template; + } + + return html`[icon undefined]`; + } +} + + + +Icon.add("empty", html``); + +Icon.add("check", html``); +Icon.add("close", html``); +Icon.add("grip", html``); +Icon.add("up", html``); +Icon.add("down", html``); + +Icon.add("caret-up", html``); +Icon.add("caret-down", html``); + +Icon.add("folder", html``); +Icon.add("file", html``); + +Icon.add("info", html``); +Icon.add("warning", html``); +Icon.add("error", html``); +Icon.add("prompt", html``); Icon.add("trash", html``); Icon.add("plus", html``) @@ -29,5 +110,3 @@ Icon.add("comment", html``); Icon.add("audio", html``); Icon.add("eye", html``); - -export default Icon; diff --git a/source/ui/composants/Modal.ts b/source/ui/composants/Modal.ts index 486bf4f6..c8926a60 100644 --- a/source/ui/composants/Modal.ts +++ b/source/ui/composants/Modal.ts @@ -3,7 +3,7 @@ import { css, LitElement,customElement, property, html, TemplateResult, query } import "./Icon" -import styles from '!lit-css-loader?{"specifier":"lit-element"}!sass-loader!../styles.scss'; +import styles from '!lit-css-loader?{"specifier":"lit-element"}!sass-loader!../styles/common.scss'; interface ModalOptions{ header :TemplateResult|string; @@ -82,7 +82,7 @@ interface ModalOptions{
${this.body}
${this.buttons}
`; } diff --git a/source/ui/composants/Notification.ts b/source/ui/composants/Notification.ts new file mode 100644 index 00000000..3fa65e20 --- /dev/null +++ b/source/ui/composants/Notification.ts @@ -0,0 +1,65 @@ +import { LitElement, customElement, property } from "lit-element"; + + +type NotificationLevel = "info" | "success" | "warning" | "error"; + +const _levelClasses = { + "info": "notification-info", + "success": "notification-success", + "warning": "notification-warning", + "error": "notification-error" +} as const; + +const _levelIcons = { + "info": "info", + "success": "check", + "warning": "warning", + "error": "error" +}; + +const _levelTimeouts = { + "info": 2000, + "success": 2000, + "warning": 5000, + "error": 0 +} as const; + + +@customElement("notification-line") +class Notification extends LitElement{ + @property({ type: String }) + message: string; + + @property({ type: String }) + level: NotificationLevel; + + @property({ type: Number }) + timeout: number; + + + createRenderRoot() { + return this; + } + + constructor(message?: string, level?: NotificationLevel, timeout?: number) + { + super(); + this.message = message || ""; + this.level = level || "info"; + this.timeout = timeout !== undefined ? timeout : _levelTimeouts[this.level]; + } + +} + + +@customElement("notification-stack") +export default class Notifications extends LitElement{ + static container: HTMLElement = null; + static show(message: string, level?: NotificationLevel, timeout?: number){ + let line = new Notification(message, level, timeout); + if(!Notifications.container){ + return console.error("Notification stack not configured. Please mount in your DOM before calling Notification.show"); + } + } + +} \ No newline at end of file diff --git a/source/ui/composants/SceneCard.ts b/source/ui/composants/SceneCard.ts index d637ba62..c39e5a39 100644 --- a/source/ui/composants/SceneCard.ts +++ b/source/ui/composants/SceneCard.ts @@ -1,5 +1,7 @@ import { LitElement, customElement, property, html, TemplateResult, css } from "lit-element"; -import WebDAVProvider from "@ff/scene/assets/WebDAVProvider"; + +import defaultSprite from "../assets/images/defaultSprite.svg"; + import i18n from "../state/translate"; import { AccessType, AccessTypes, Scene } from "../state/withScenes"; @@ -13,7 +15,6 @@ import { AccessType, AccessTypes, Scene } from "../state/withScenes"; @customElement("scene-card") export default class SceneCard extends i18n(LitElement) { - static _assets = new WebDAVProvider(); @property() thumb :string; @@ -52,13 +53,7 @@ import { AccessType, AccessTypes, Scene } from "../state/withScenes"; if(this.cardStyle == "grid") this.classList.add("card-grid"); if(!this.thumb ){ - SceneCard._assets.get(this.path, false).then(p=>{ - let thumbProps = p.find(f=> f.name.endsWith(`-image-thumb.jpg`)); - if(!thumbProps) return console.log("No thumbnail for", this.name); - this.thumb = thumbProps.url; - }, (e)=>{ - console.warn("Failed to PROPFIND %s :", this.path, e); - }); + console.warn("Failed to PROPFIND %s :", this.path); } } @@ -72,7 +67,7 @@ import { AccessType, AccessTypes, Scene } from "../state/withScenes";
- ${this.thumb? html``: html``} + ${this.thumb? html``: html``}

${this.name}

@@ -80,9 +75,9 @@ import { AccessType, AccessTypes, Scene } from "../state/withScenes";
- ${this.t("ui.view")} - ${this.can("write")? html`${this.t("ui.edit")}`:null} - ${this.can("admin")? html`${this.t("ui.admin")}`:null} + ${this.t("ui.view")} + ${this.can("write")? html`${this.t("ui.edit")}`:null} + ${this.can("admin")? html`${this.t("ui.admin")}`:null}
${(this.onChange? html` diff --git a/source/ui/composants/Spinner.ts b/source/ui/composants/Spinner.ts new file mode 100644 index 00000000..86514923 --- /dev/null +++ b/source/ui/composants/Spinner.ts @@ -0,0 +1,89 @@ +import { LitElement, property, customElement, html, css } from "lit-element"; + + +@customElement("spin-loader") +export default class Spinner extends LitElement{ + + @property({type: Boolean}) + overlay :boolean = false; + @property({type: Boolean}) + visible :boolean = false; + + + + render(){ + return html`
+ + +
`; + } + static readonly styles = [css` + .spin-loader{ + position:relative; + } + + .loading-overlay{ + position: absolute; + inset: 0; + background: rgba(0, 0, 0, 0.6); + transition: opacity 0.5s ease-out; + pointer-events: auto; + z-index: 10; + } + + :host:not([visible]) .loading-overlay{ + pointer-events: none; + opacity: 0; + } + + + .loader { + top: calc(50% - 48px); + left: calc(50% - 48px); + width: 96px; + height: 96px; + border: 6px solid #FFF; + border-radius: 50%; + display: inline-block; + position: relative; + box-sizing: border-box; + animation: rotation 1s linear infinite; + transition: transform 0.5s ease-out; + } + + .loader::after { + content: ''; + box-sizing: border-box; + position: absolute; + left: 50%; + top: 50%; + transform: translate(-50%, -50%); + width: 80px; + height: 80px; + border-radius: 50%; + border: 6px solid; + border-color: var(--color-primary) transparent; + } + + .load-text{ + position: absolute; + bottom:10px; + left:0; + right:0; + text-align: center; + font-size: 2rem; + } + .load-text:empty{ + display: none; + } + + @keyframes rotation { + 0% { + transform: rotate(0deg); + } + 100% { + transform: rotate(360deg); + } + } + `]; +} \ No newline at end of file diff --git a/source/ui/composants/UserLogin.ts b/source/ui/composants/UserLogin.ts index 97b9cf0f..6f0b3c93 100644 --- a/source/ui/composants/UserLogin.ts +++ b/source/ui/composants/UserLogin.ts @@ -5,8 +5,8 @@ import { css, LitElement,customElement, property, html, TemplateResult } from "l import { doLogin } from "../state/auth"; import i18n from "../state/translate"; -import styles from '!lit-css-loader?{"specifier":"lit-element"}!sass-loader!../styles.scss'; -import Notification from "@ff/ui/Notification"; +import styles from '!lit-css-loader?{"specifier":"lit-element"}!sass-loader!../styles/common.scss'; +import Notification from "./Notification"; /** * Main UI view for the Voyager Explorer application. diff --git a/source/ui/composants/navbar/ChangeLocale.ts b/source/ui/composants/navbar/ChangeLocale.ts index 94904502..85b7ebdf 100644 --- a/source/ui/composants/navbar/ChangeLocale.ts +++ b/source/ui/composants/navbar/ChangeLocale.ts @@ -1,11 +1,12 @@ import { LitElement, html, customElement, property, css, TemplateResult } from 'lit-element'; -import Button from "@ff/ui/Button"; + import i18n, {Localization} from '../../state/translate'; @customElement("change-locale") -export default class ChangeLocale extends i18n(Button){ +export default class ChangeLocale extends i18n(LitElement){ constructor(){ super(); + this.addEventListener("click", (e) => this.onClick()); } onclick = (ev :MouseEvent)=>{ ev.preventDefault(); @@ -15,12 +16,12 @@ export default class ChangeLocale extends i18n(Button){ protected createRenderRoot(): Element | ShadowRoot { return this; } + onClick = ()=>{ Localization.Instance.setLanguage(this.language == "fr"? "en": "fr"); } + protected render(): TemplateResult { - this.text = this.language; - console.log("lang render : ", Localization.Instance); - return super.render(); + return html`
${this.language}
`; } } \ No newline at end of file diff --git a/source/ui/composants/navbar/Navbar.ts b/source/ui/composants/navbar/Navbar.ts index 68e1c4bd..77fb9c32 100644 --- a/source/ui/composants/navbar/Navbar.ts +++ b/source/ui/composants/navbar/Navbar.ts @@ -3,6 +3,7 @@ import { css, customElement, html, LitElement, TemplateResult } from "lit-elemen import styles from '!lit-css-loader?{"specifier":"lit-element"}!sass-loader!./styles.scss'; +import favicon from "../../assets/favicon.png"; /** * Main UI view for the Voyager Explorer application. @@ -20,7 +21,7 @@ import styles from '!lit-css-loader?{"specifier":"lit-element"}!sass-loader!./st return html`