UI infrastructure and simple "Help" dialog (#3)

Sets up some WIP infrastructure for building out the UI, and adds a simple popup
to show the navigation controls for the app.
This commit is contained in:
Danny McGee
2023-10-09 22:19:58 -04:00
committed by GitHub
parent 57d64f08f0
commit c84fd31387
40 changed files with 1608 additions and 590 deletions
-5
View File
@@ -1,5 +0,0 @@
{
"presets": [
"@nx/js/babel"
]
}
+1 -1
View File
@@ -3,6 +3,6 @@
"parser": {
"syntax": "typescript"
},
"target": "es2016"
"target": "ES2020"
}
}
+3
View File
@@ -8,6 +8,9 @@
<meta name="viewport" content="width=device-width, initial-scale=1" />
<link rel="icon" type="image/x-icon" href="/favicon.ico" />
<link rel="stylesheet" href="/src/styles.scss" />
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Fira+Sans:wght@400;500;700&display=swap" rel="stylesheet">
</head>
<body>
<app-root></app-root>
+17
View File
@@ -0,0 +1,17 @@
:host {
position: fixed;
inset: 0;
pointer-events: none;
}
.btn-help {
position: absolute;
top: 16px;
right: 16px;
}
.help {
position: absolute;
top: 60px;
right: 16px;
}
+44 -15
View File
@@ -1,13 +1,40 @@
import { on } from "@storyteller/framework";
import * as studio from "@storyteller/studio";
import { ViewportSize } from "@storyteller/studio";
import { throttle } from "lodash";
import { LitElement, html, unsafeCSS } from "lit";
import { customElement, state } from "lit/decorators.js";
import "./app.element.scss";
import "./button.element";
import "./help.element";
import "./icons/fa-circle-info.element";
export class AppElement extends HTMLElement {
static observedAttributes = [];
import styles from "./app.element.scss?inline";
connectedCallback(): void {
@customElement("app-root")
export class AppElement extends LitElement {
static override styles = unsafeCSS(styles);
@state() showHelp = false;
#studioCanvas?: HTMLCanvasElement | null;
override connectedCallback(): void {
this.#studioCanvas = this.#initStudio();
super.connectedCallback();
}
@on("window:resize")
onWindowResize(): void {
const width = window.innerWidth;
const height = window.innerHeight;
studio.resize(new ViewportSize(width, height));
}
toggleHelp(): void {
this.showHelp = !this.showHelp;
}
#initStudio(): HTMLCanvasElement | null {
const width = window.innerWidth;
const height = window.innerHeight;
try {
@@ -17,17 +44,19 @@ export class AppElement extends HTMLElement {
throw err;
}
window.addEventListener("resize", this.onResize);
return document.querySelector<HTMLCanvasElement>(`canvas[alt="Storyteller Studio"]`);
}
disconnectedCallback(): void {
window.removeEventListener("resize", this.onResize);
}
protected override render = () => html`
<sts-button class="btn-help"
icon="circle-info"
@click=${this.toggleHelp}
>
Help
</sts-button>
onResize = throttle(() => {
const width = window.innerWidth;
const height = window.innerHeight;
studio.resize(new ViewportSize(width, height));
}, 16.666);
${this.showHelp ? html`
<sts-help class="help"></sts-help>
` : null}
`;
}
customElements.define("app-root", AppElement);
@@ -0,0 +1,31 @@
:host {
display: inline-flex;
align-items: center;
gap: 0.5rem;
padding: 10px 20px;
border-radius: 0.5rem;
font: {
family: "Fira Sans", sans-serif;
size: 15px;
weight: 500;
}
line-height: 1;
pointer-events: auto;
cursor: pointer;
user-select: none;
background: #0006;
color: #FFF;
backdrop-filter: blur(16px);
transition: background 0.1s linear;
}
:host(:hover) {
background: #0008;
}
.icon {
fill: currentColor;
}
+30
View File
@@ -0,0 +1,30 @@
import { match } from "@storyteller/utility";
import { on } from "@storyteller/framework";
import { LitElement, html, unsafeCSS } from "lit";
import { customElement, property } from "lit/decorators.js";
import styles from "./button.element.scss?inline";
@customElement("sts-button")
export class ButtonElement extends LitElement {
static override styles = unsafeCSS(styles);
@property() icon?: string;
override role = "button";
override tabIndex = 0;
@on("keydown")
onKeyDown({ key }: KeyboardEvent) {
if (/^( |Enter)$/.test(key))
this.click();
}
protected override render = () => html`
${match (this.icon, {
"circle-info": () => html`<fa-circle-info class="icon"></fa-circle-info>`,
_: () => null,
})}
<slot></slot>
`
}
+32
View File
@@ -0,0 +1,32 @@
:host {
padding: 20px 30px;
border-radius: 0.5rem;
font: {
family: "Fira Sans", sans-serif;
size: 15px;
weight: 400;
}
background: #0006;
backdrop-filter: blur(16px);
pointer-events: auto;
}
th {
text-align: start;
color: #FFF;
font-weight: 700;
line-height: 2;
}
td {
color: #FFFA;
vertical-align: top;
line-height: 1.4;
&:has(+ td) {
padding-right: 30px;
}
}
+49
View File
@@ -0,0 +1,49 @@
import { LitElement, html, unsafeCSS } from "lit";
import { customElement } from "lit/decorators.js";
import styles from "./help.element.scss?inline";
@customElement("sts-help")
export class HelpElement extends LitElement {
static override styles = unsafeCSS(styles);
protected override render = () => html`
<table>
<thead>
<tr>
<th>Action</th>
<th>Controls</th>
<th>Alternate</th>
</tr>
</thead>
<tbody>
<tr>
<td>Pan view</td>
<td colspan="2">MMB</td>
</tr>
<tr>
<td>Orbit around point</td>
<td colspan="2">Alt + LMB</td>
</tr>
<tr>
<td>Zoom</td>
<td>Alt + RMB</td>
<td>Mouse Wheel</td>
</tr>
<tr>
<td>Free fly</td>
<td colspan="2">W, A, S, D</td>
</tr>
<tr>
<td style="padding-left: 20px">Up / Down</td>
<td colspan="2">E, Q</td>
</tr>
<tr>
<td>Free look</td>
<td>RMB</td>
<td>Shift</td>
</tr>
</tbody>
</table>
`;
}
@@ -0,0 +1,20 @@
import { LitElement, html } from "lit";
import { customElement } from "lit/decorators.js";
@customElement("fa-circle-info")
export class CircleInfoIcon extends LitElement {
override render = () => html`
<svg xmlns="http://www.w3.org/2000/svg"
height="1em"
viewBox="0 0 512 512"
>
<!--!
Font Awesome Free 6.4.2 by @fontawesome - https://fontawesome.com
License - https://fontawesome.com/license (Commercial License)
Copyright 2023 Fonticons, Inc.
-->
<path d="M256 512A256 256 0 1 0 256 0a256 256 0 1 0 0 512zM216 336h24V272H216c-13.3 0-24-10.7-24-24s10.7-24 24-24h48c13.3 0 24 10.7 24 24v88h8c13.3 0 24 10.7 24 24s-10.7 24-24 24H216c-13.3 0-24-10.7-24-24s10.7-24 24-24zm40-208a32 32 0 1 1 0 64 32 32 0 1 1 0-64z"
/>
</svg>
`;
}
+8 -2
View File
@@ -4,6 +4,12 @@
"outDir": "../dist/out-tsc",
"types": ["node"]
},
"exclude": ["src/**/*.spec.ts", "src/**/*.test.ts"],
"include": ["src/**/*.ts"]
"exclude": [
"src/**/*.spec.ts",
"src/**/*.test.ts"
],
"include": [
"src/**/*.ts",
"typings/*.d.ts"
]
}
+16 -9
View File
@@ -2,20 +2,27 @@
"extends": "../tsconfig.base.json",
"files": [],
"compilerOptions": {
"target": "ESNext",
"useDefineForClassFields": true,
"target": "ES2020",
"experimentalDecorators": true,
"useDefineForClassFields": false,
"module": "ESNext",
"lib": ["ESNext", "DOM"],
"moduleResolution": "Node",
"strict": true,
"lib": ["ES2020", "DOM", "DOM.Iterable"],
"skipLibCheck": true,
"moduleResolution": "Bundler",
"allowImportingTsExtensions": true,
"resolveJsonModule": true,
"isolatedModules": true,
"esModuleInterop": true,
"noEmit": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"esModuleInterop": true,
"strict": true,
"noUnusedLocals": false,
"noUnusedParameters": false,
"noImplicitAny": true,
"noImplicitOverride": true,
"noImplicitReturns": true,
"skipLibCheck": true,
"types": ["vite/client"]
},
"include": ["src"],
+4
View File
@@ -0,0 +1,4 @@
declare module "*.scss?inline" {
const styles: import("lit").CSSResult;
export default styles;
}