mirror of
https://github.com/storytold/storyteller-bevy.git
synced 2026-10-09 00:09:55 +00:00
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:
Vendored
+3
@@ -1,3 +1,6 @@
|
||||
{
|
||||
"rust-analyzer.showUnlinkedFileNotification": false,
|
||||
"explorer.fileNesting.patterns": {
|
||||
"*.element.ts": "${capture}.element.scss, ${capture}.element.spec.ts",
|
||||
},
|
||||
}
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
# framework
|
||||
|
||||
This library was generated with [Nx](https://nx.dev).
|
||||
|
||||
## Building
|
||||
|
||||
Run `nx build framework` to build the library.
|
||||
|
||||
## Running unit tests
|
||||
|
||||
Run `nx test framework` to execute the unit tests via [Jest](https://jestjs.io).
|
||||
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"name": "@storyteller/framework",
|
||||
"version": "0.0.1",
|
||||
"dependencies": {},
|
||||
"main": "./index.js",
|
||||
"module": "./index.mjs",
|
||||
"typings": "./index.d.ts"
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
{
|
||||
"name": "framework",
|
||||
"$schema": "../node_modules/nx/schemas/project-schema.json",
|
||||
"sourceRoot": "framework/src",
|
||||
"projectType": "library",
|
||||
"targets": {
|
||||
"build": {
|
||||
"executor": "@nx/vite:build",
|
||||
"outputs": ["{options.outputPath}"],
|
||||
"options": {
|
||||
"outputPath": "dist/framework"
|
||||
}
|
||||
},
|
||||
"test": {
|
||||
"executor": "@nx/vite:test",
|
||||
"outputs": ["{options.reportsDirectory}"],
|
||||
"options": {
|
||||
"passWithNoTests": true,
|
||||
"reportsDirectory": "../coverage/framework"
|
||||
}
|
||||
}
|
||||
},
|
||||
"tags": []
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export * from "./lib/decorators/on";
|
||||
@@ -0,0 +1,33 @@
|
||||
import { LitElement } from "lit";
|
||||
import { customElement, state } from "lit/decorators.js";
|
||||
|
||||
import { on } from "./on";
|
||||
|
||||
@customElement("test-counter")
|
||||
class TestCounter extends LitElement {
|
||||
@state() count = 0;
|
||||
|
||||
@on("click")
|
||||
incrementCount(): void {
|
||||
this.count += 1;
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"test-counter": TestCounter;
|
||||
}
|
||||
}
|
||||
|
||||
describe("@on(eventName)", () => {
|
||||
it("binds a Custom Element method as an event handler", () => {
|
||||
const counter = document.createElement("test-counter");
|
||||
document.body.appendChild(counter);
|
||||
|
||||
counter.click();
|
||||
counter.click();
|
||||
counter.click();
|
||||
|
||||
expect(counter.count).toBe(3);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,51 @@
|
||||
import { match } from "@storyteller/utility";
|
||||
import type { LitElement } from "lit";
|
||||
|
||||
/**
|
||||
* Adds the decorated method as an event listener for the given event name, and
|
||||
* automatically cleans up the listener when the element is removed from the
|
||||
* DOM.
|
||||
*
|
||||
* The event name can be prefixed with `window:` or `document:` to add the
|
||||
* listener to one of those targets instead of the custom element itself.
|
||||
*/
|
||||
export function on(eventSelector: string) {
|
||||
const [targetName, eventName] = eventSelector.includes(":")
|
||||
? eventSelector.split(":")
|
||||
: [null, eventSelector];
|
||||
|
||||
let target = match (targetName, {
|
||||
"window": () => window as EventTarget,
|
||||
"document": () => document as EventTarget,
|
||||
_: () => null,
|
||||
});
|
||||
|
||||
return <T extends LitElement>(proto: T, propName: keyof T, desc: PropertyDescriptor) => {
|
||||
const handler = desc?.value ?? proto[propName] ?? (() => {});
|
||||
|
||||
const connectedCb = proto.connectedCallback;
|
||||
const disconnectedCb = proto.disconnectedCallback;
|
||||
|
||||
Object.defineProperty(proto, "connectedCallback", {
|
||||
value: function (this: HTMLElement) {
|
||||
target ??= this;
|
||||
target.addEventListener(eventName, handler);
|
||||
connectedCb.call(this);
|
||||
},
|
||||
configurable: true,
|
||||
writable: true,
|
||||
});
|
||||
|
||||
Object.defineProperty(proto, "disconnectedCallback", {
|
||||
value: function (this: HTMLElement) {
|
||||
target ??= this;
|
||||
target.removeEventListener(eventName, handler);
|
||||
disconnectedCb.call(this);
|
||||
},
|
||||
configurable: true,
|
||||
writable: true,
|
||||
});
|
||||
|
||||
return desc;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
{
|
||||
"extends": "../tsconfig.base.json",
|
||||
"compilerOptions": {
|
||||
"module": "commonjs",
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"strict": true,
|
||||
"noImplicitOverride": true,
|
||||
"noPropertyAccessFromIndexSignature": true,
|
||||
"noImplicitReturns": true,
|
||||
"noFallthroughCasesInSwitch": true
|
||||
},
|
||||
"files": [],
|
||||
"include": [],
|
||||
"references": [
|
||||
{
|
||||
"path": "./tsconfig.lib.json"
|
||||
},
|
||||
{
|
||||
"path": "./tsconfig.spec.json"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"extends": "./tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"outDir": "../dist/out-tsc",
|
||||
"declaration": true,
|
||||
"types": ["node", "vite/client"]
|
||||
},
|
||||
"include": ["src/**/*.ts"],
|
||||
"exclude": ["jest.config.ts", "src/**/*.spec.ts", "src/**/*.test.ts"]
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
{
|
||||
"extends": "./tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"outDir": "../../dist/out-tsc",
|
||||
"types": [
|
||||
"vitest/globals",
|
||||
"vitest/importMeta",
|
||||
"vite/client",
|
||||
"node",
|
||||
"vitest"
|
||||
]
|
||||
},
|
||||
"include": [
|
||||
"vite.config.ts",
|
||||
"src/**/*.test.ts",
|
||||
"src/**/*.spec.ts",
|
||||
"src/**/*.test.tsx",
|
||||
"src/**/*.spec.tsx",
|
||||
"src/**/*.test.js",
|
||||
"src/**/*.spec.js",
|
||||
"src/**/*.test.jsx",
|
||||
"src/**/*.spec.jsx",
|
||||
"src/**/*.d.ts"
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
/// <reference types='vitest' />
|
||||
import { defineConfig } from "vite";
|
||||
import dts from "vite-plugin-dts";
|
||||
import * as path from "path";
|
||||
import { nxViteTsPaths } from "@nx/vite/plugins/nx-tsconfig-paths.plugin";
|
||||
|
||||
export default defineConfig({
|
||||
cacheDir: "../node_modules/.vite/framework",
|
||||
|
||||
plugins: [
|
||||
nxViteTsPaths(),
|
||||
dts({
|
||||
entryRoot: "src",
|
||||
tsConfigFilePath: path.join(__dirname, "tsconfig.lib.json"),
|
||||
skipDiagnostics: true,
|
||||
}),
|
||||
],
|
||||
|
||||
// Uncomment this if you are using workers.
|
||||
// worker: {
|
||||
// plugins: [ nxViteTsPaths() ],
|
||||
// },
|
||||
|
||||
// Configuration for building your library.
|
||||
// See: https://vitejs.dev/guide/build.html#library-mode
|
||||
build: {
|
||||
lib: {
|
||||
// Could also be a dictionary or array of multiple entry points.
|
||||
entry: "src/index.ts",
|
||||
name: "framework",
|
||||
fileName: "index",
|
||||
// Change this to the formats you want to support.
|
||||
// Don't forget to update your package.json as well.
|
||||
formats: ["es", "cjs"],
|
||||
},
|
||||
rollupOptions: {
|
||||
// External packages that should not be bundled into your library.
|
||||
external: [],
|
||||
},
|
||||
},
|
||||
|
||||
test: {
|
||||
globals: true,
|
||||
cache: {
|
||||
dir: "../node_modules/.vitest",
|
||||
},
|
||||
environment: "jsdom",
|
||||
include: ["src/**/*.{test,spec}.{js,mjs,cjs,ts,mts,cts,jsx,tsx}"],
|
||||
},
|
||||
});
|
||||
@@ -35,6 +35,7 @@
|
||||
"default": {
|
||||
"runner": "nx/tasks-runners/default",
|
||||
"options": {
|
||||
"useDaemonProcess": false,
|
||||
"cacheableOperations": [
|
||||
"build",
|
||||
"lint",
|
||||
|
||||
Generated
+858
-556
File diff suppressed because it is too large
Load Diff
@@ -13,6 +13,7 @@
|
||||
"@swc/core": "~1.3.85",
|
||||
"@types/lodash": "^4.14.199",
|
||||
"@types/node": "18.7.1",
|
||||
"@vitest/coverage-c8": "~0.32.0",
|
||||
"@vitest/ui": "~0.32.0",
|
||||
"chalk": "^5.3.0",
|
||||
"glob": "^10.3.10",
|
||||
@@ -23,12 +24,14 @@
|
||||
"swc-loader": "0.1.15",
|
||||
"typescript": "~5.1.3",
|
||||
"vite": "~4.3.9",
|
||||
"vite-plugin-dts": "~2.3.0",
|
||||
"vite-plugin-top-level-await": "^1.3.1",
|
||||
"vite-plugin-wasm": "^3.2.2",
|
||||
"vitest": "~0.32.0",
|
||||
"wasm-pack": "^0.12.1"
|
||||
},
|
||||
"dependencies": {
|
||||
"lit": "^2.8.0",
|
||||
"lodash": "^4.17.21",
|
||||
"tslib": "^2.3.0"
|
||||
}
|
||||
|
||||
@@ -1,5 +0,0 @@
|
||||
{
|
||||
"presets": [
|
||||
"@nx/js/babel"
|
||||
]
|
||||
}
|
||||
@@ -3,6 +3,6 @@
|
||||
"parser": {
|
||||
"syntax": "typescript"
|
||||
},
|
||||
"target": "es2016"
|
||||
"target": "ES2020"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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>
|
||||
`
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
`;
|
||||
}
|
||||
@@ -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"
|
||||
]
|
||||
}
|
||||
|
||||
@@ -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"],
|
||||
|
||||
Vendored
+4
@@ -0,0 +1,4 @@
|
||||
declare module "*.scss?inline" {
|
||||
const styles: import("lit").CSSResult;
|
||||
export default styles;
|
||||
}
|
||||
@@ -30,6 +30,7 @@ pub fn init(viewport_size: ViewportSize) {
|
||||
DefaultPlugins.set(WindowPlugin {
|
||||
primary_window: Some(Window {
|
||||
resolution: viewport_size.into(),
|
||||
title: "Storyteller Studio".into(),
|
||||
..default()
|
||||
}),
|
||||
..default()
|
||||
|
||||
@@ -30,8 +30,8 @@ fn layout(mut cmd: Commands) {
|
||||
.with_text_alignment(TextAlignment::Right)
|
||||
.with_style(Style {
|
||||
position_type: PositionType::Absolute,
|
||||
top: Val::Px(16.),
|
||||
right: Val::Px(16.),
|
||||
bottom: Val::Px(16.),
|
||||
..default()
|
||||
}),
|
||||
));
|
||||
|
||||
+3
-1
@@ -15,7 +15,9 @@
|
||||
"skipDefaultLibCheck": true,
|
||||
"baseUrl": ".",
|
||||
"paths": {
|
||||
"@storyteller/studio": ["dist/studio"]
|
||||
"@storyteller/framework": ["framework/src/index.ts"],
|
||||
"@storyteller/studio": ["dist/studio"],
|
||||
"@storyteller/utility": ["utility/src/index.ts"]
|
||||
}
|
||||
},
|
||||
"exclude": ["node_modules", "tmp"]
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
# utility
|
||||
|
||||
This library was generated with [Nx](https://nx.dev).
|
||||
|
||||
## Building
|
||||
|
||||
Run `nx build utility` to build the library.
|
||||
|
||||
## Running unit tests
|
||||
|
||||
Run `nx test utility` to execute the unit tests via [Jest](https://jestjs.io).
|
||||
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"name": "@storyteller/utility",
|
||||
"version": "0.0.1",
|
||||
"dependencies": {},
|
||||
"main": "./index.js",
|
||||
"module": "./index.mjs",
|
||||
"typings": "./index.d.ts"
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
{
|
||||
"name": "utility",
|
||||
"$schema": "../node_modules/nx/schemas/project-schema.json",
|
||||
"sourceRoot": "utility/src",
|
||||
"projectType": "library",
|
||||
"targets": {
|
||||
"build": {
|
||||
"executor": "@nx/vite:build",
|
||||
"outputs": ["{options.outputPath}"],
|
||||
"options": {
|
||||
"outputPath": "dist/utility"
|
||||
}
|
||||
},
|
||||
"test": {
|
||||
"executor": "@nx/vite:test",
|
||||
"outputs": ["{options.reportsDirectory}"],
|
||||
"options": {
|
||||
"passWithNoTests": true,
|
||||
"reportsDirectory": "../coverage/utility"
|
||||
}
|
||||
}
|
||||
},
|
||||
"tags": []
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
export * from "./lib/match";
|
||||
export * from "./lib/types";
|
||||
@@ -0,0 +1,93 @@
|
||||
import { Fn } from "./types";
|
||||
|
||||
type Key = string | number;
|
||||
|
||||
type ExhaustiveMatcher<Union extends Key>
|
||||
= Record<Union, () => any>
|
||||
;
|
||||
type MatcherWithFallback<Union extends Key>
|
||||
= Partial<Record<Union, () => any>>
|
||||
& { _: () => any }
|
||||
;
|
||||
type Matcher<Union extends Key>
|
||||
= ExhaustiveMatcher<Union>
|
||||
| MatcherWithFallback<Union>
|
||||
;
|
||||
type MatchReturn<K extends Key, T extends Matcher<K>>
|
||||
// if K is a known, specific key of T, infer the matching function's return type
|
||||
= T[K] extends Fn<[], infer R> ? R
|
||||
// if K is unknown at comptime, infer the union of all matcher return types
|
||||
: T[keyof T] extends Fn<[], infer R> ? R
|
||||
// Unreachable
|
||||
: never
|
||||
;
|
||||
|
||||
/**
|
||||
* Behaves like a simple [switch expression](https://learn.microsoft.com/en-us/dotnet/csharp/language-reference/operators/switch-expression)
|
||||
* for JavaScript.
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* enum Direction {
|
||||
* Forward,
|
||||
* Left,
|
||||
* Back,
|
||||
* Right,
|
||||
* }
|
||||
*
|
||||
* function getDirection(event: KeyboardEvent): Direction {
|
||||
* return match (event.key, {
|
||||
* "W": () => Direction.Forward,
|
||||
* "A": () => Direction.Left,
|
||||
* "S": () => Direction.Back,
|
||||
* "D": () => Direction.Right,
|
||||
* _: () => {
|
||||
* throw new Error(`${event.key} is not bound to a direction!`);
|
||||
* },
|
||||
* });
|
||||
* }
|
||||
* ```
|
||||
* An enum can also be used as the subject, as long as its enumerators are
|
||||
* assigned to values that can be used as JavaScript object keys (i.e. `string`,
|
||||
* `number`, or `symbol`):
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* enum Direction {
|
||||
* Up = "up",
|
||||
* Down = "down",
|
||||
* Right = "right",
|
||||
* Left = "left",
|
||||
* }
|
||||
*
|
||||
* enum Orientation {
|
||||
* North,
|
||||
* South,
|
||||
* East,
|
||||
* West,
|
||||
* }
|
||||
*
|
||||
* function toOrientation(direction: Direction): Orientation {
|
||||
* // Note that if the `matcher` argument is exhaustive,
|
||||
* // you don't need to provide a `_` fallback.
|
||||
* return match (direction, {
|
||||
* [Direction.Up]: () => Orientation.North,
|
||||
* [Direction.Right]: () => Orientation.East,
|
||||
* [Direction.Down]: () => Orientation.South,
|
||||
* [Direction.Left]: () => Orientation.West,
|
||||
* });
|
||||
* }
|
||||
* ```
|
||||
*/
|
||||
export function match<K extends Key, T extends Matcher<K>>(
|
||||
subject: K | null | undefined,
|
||||
matcher: T,
|
||||
): MatchReturn<K, T> {
|
||||
if (subject && subject in matcher)
|
||||
return matcher[subject]!();
|
||||
|
||||
if ("_" in matcher)
|
||||
return matcher._();
|
||||
|
||||
throw new Error(`No match found for subject \`${subject}\` in matcher`);
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
export interface Fn<Args extends any[] = [], R = void> {
|
||||
(...args: Args): R;
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
{
|
||||
"extends": "../tsconfig.base.json",
|
||||
"compilerOptions": {
|
||||
"module": "commonjs",
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"strict": true,
|
||||
"noImplicitOverride": true,
|
||||
"noPropertyAccessFromIndexSignature": true,
|
||||
"noImplicitReturns": true,
|
||||
"noFallthroughCasesInSwitch": true
|
||||
},
|
||||
"files": [],
|
||||
"include": [],
|
||||
"references": [
|
||||
{
|
||||
"path": "./tsconfig.lib.json"
|
||||
},
|
||||
{
|
||||
"path": "./tsconfig.spec.json"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"extends": "./tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"outDir": "../dist/out-tsc",
|
||||
"declaration": true,
|
||||
"types": ["node", "vite/client"]
|
||||
},
|
||||
"include": ["src/**/*.ts"],
|
||||
"exclude": ["jest.config.ts", "src/**/*.spec.ts", "src/**/*.test.ts"]
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
{
|
||||
"extends": "./tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"outDir": "../../dist/out-tsc",
|
||||
"types": [
|
||||
"vitest/globals",
|
||||
"vitest/importMeta",
|
||||
"vite/client",
|
||||
"node",
|
||||
"vitest"
|
||||
]
|
||||
},
|
||||
"include": [
|
||||
"vite.config.ts",
|
||||
"src/**/*.test.ts",
|
||||
"src/**/*.spec.ts",
|
||||
"src/**/*.test.tsx",
|
||||
"src/**/*.spec.tsx",
|
||||
"src/**/*.test.js",
|
||||
"src/**/*.spec.js",
|
||||
"src/**/*.test.jsx",
|
||||
"src/**/*.spec.jsx",
|
||||
"src/**/*.d.ts"
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
/// <reference types='vitest' />
|
||||
import { defineConfig } from "vite";
|
||||
import dts from "vite-plugin-dts";
|
||||
import * as path from "node:path";
|
||||
import { nxViteTsPaths } from "@nx/vite/plugins/nx-tsconfig-paths.plugin";
|
||||
|
||||
export default defineConfig({
|
||||
cacheDir: "../node_modules/.vite/utility",
|
||||
|
||||
plugins: [
|
||||
nxViteTsPaths(),
|
||||
dts({
|
||||
entryRoot: "src",
|
||||
tsConfigFilePath: path.join(__dirname, "tsconfig.lib.json"),
|
||||
skipDiagnostics: true,
|
||||
}),
|
||||
],
|
||||
|
||||
// Uncomment this if you are using workers.
|
||||
// worker: {
|
||||
// plugins: [ nxViteTsPaths() ],
|
||||
// },
|
||||
|
||||
// Configuration for building your library.
|
||||
// See: https://vitejs.dev/guide/build.html#library-mode
|
||||
build: {
|
||||
lib: {
|
||||
// Could also be a dictionary or array of multiple entry points.
|
||||
entry: "src/index.ts",
|
||||
name: "utility",
|
||||
fileName: "index",
|
||||
// Change this to the formats you want to support.
|
||||
// Don't forget to update your package.json as well.
|
||||
formats: ["es", "cjs"],
|
||||
},
|
||||
rollupOptions: {
|
||||
// External packages that should not be bundled into your library.
|
||||
external: [],
|
||||
},
|
||||
},
|
||||
|
||||
test: {
|
||||
globals: true,
|
||||
cache: {
|
||||
dir: "../node_modules/.vitest",
|
||||
},
|
||||
environment: "node",
|
||||
include: ["src/**/*.{test,spec}.{js,mjs,cjs,ts,mts,cts,jsx,tsx}"],
|
||||
},
|
||||
});
|
||||
Reference in New Issue
Block a user