diff --git a/package-lock.json b/package-lock.json index 9d13de8..024ae5a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,12 +10,12 @@ "license": "MIT", "dependencies": { "@fortawesome/fontawesome-pro": "^6.4.2", + "axios": "^1.6.7", "lit": "^2.8.0", "lodash": "^4.17.21", "react": "18.2.0", "react-dom": "18.2.0", - "tslib": "^2.3.0", - "uuid": "^9.0.1" + "tslib": "^2.3.0" }, "devDependencies": { "@nx/js": "16.9.1", @@ -4976,8 +4976,7 @@ "node_modules/asynckit": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", - "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", - "dev": true + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==" }, "node_modules/available-typed-arrays": { "version": "1.0.5", @@ -4992,12 +4991,11 @@ } }, "node_modules/axios": { - "version": "1.5.1", - "resolved": "https://registry.npmjs.org/axios/-/axios-1.5.1.tgz", - "integrity": "sha512-Q28iYCWzNHjAm+yEAot5QaAMxhMghWLFVf7rRdwhUI+c2jix2DUXjAHXVi+s1ibs3mjPO/cCgbA++3BjD0vP/A==", - "dev": true, + "version": "1.6.7", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.6.7.tgz", + "integrity": "sha512-/hDJGff6/c7u0hDkvkGxR/oy6CbCs8ziCsC7SqmhjfozqiJGc8Z11wrv9z9lYfY4K8l+H9TpjcMDX0xOZmx+RA==", "dependencies": { - "follow-redirects": "^1.15.0", + "follow-redirects": "^1.15.4", "form-data": "^4.0.0", "proxy-from-env": "^1.1.0" } @@ -5606,7 +5604,6 @@ "version": "1.0.8", "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", - "dev": true, "dependencies": { "delayed-stream": "~1.0.0" }, @@ -5950,7 +5947,6 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", - "dev": true, "engines": { "node": ">=0.4.0" } @@ -6531,10 +6527,9 @@ "dev": true }, "node_modules/follow-redirects": { - "version": "1.15.3", - "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.3.tgz", - "integrity": "sha512-1VzOtuEM8pC9SFU1E+8KfTjZyMztRsgEfwQl44z8A25uy13jSzTj6dyK2Df52iV0vgHCfBwLhDWevLn95w5v6Q==", - "dev": true, + "version": "1.15.5", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.5.tgz", + "integrity": "sha512-vSFWUON1B+yAw1VN4xMfxgn5fTUiaOzAJCKBwIIgT/+7CuGy9+r+5gITvP62j3RmaD5Ph65UaERdOSRGUzZtgw==", "funding": [ { "type": "individual", @@ -6591,7 +6586,6 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz", "integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==", - "dev": true, "dependencies": { "asynckit": "^0.4.0", "combined-stream": "^1.0.8", @@ -8041,7 +8035,6 @@ "version": "1.52.0", "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", - "dev": true, "engines": { "node": ">= 0.6" } @@ -8050,7 +8043,6 @@ "version": "2.1.35", "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", - "dev": true, "dependencies": { "mime-db": "1.52.0" }, @@ -8834,8 +8826,7 @@ "node_modules/proxy-from-env": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", - "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==", - "dev": true + "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==" }, "node_modules/psl": { "version": "1.9.0", @@ -10202,6 +10193,7 @@ "version": "9.0.1", "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.1.tgz", "integrity": "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==", + "dev": true, "funding": [ "https://github.com/sponsors/broofa", "https://github.com/sponsors/ctavan" diff --git a/package.json b/package.json index 65ddb8a..e2e5c35 100644 --- a/package.json +++ b/package.json @@ -42,11 +42,11 @@ }, "dependencies": { "@fortawesome/fontawesome-pro": "^6.4.2", + "axios": "^1.6.7", "lit": "^2.8.0", "lodash": "^4.17.21", "react": "18.2.0", "react-dom": "18.2.0", - "tslib": "^2.3.0", - "uuid": "^9.0.1" + "tslib": "^2.3.0" } } diff --git a/studio-frontend/src/app/app.element.scss b/studio-frontend/src/app/app.element.scss index 5d05551..5ad99a2 100644 --- a/studio-frontend/src/app/app.element.scss +++ b/studio-frontend/src/app/app.element.scss @@ -165,3 +165,9 @@ sts-button.active { } } } + +// TODO: Move this +.menu-button-icon { + font-size: 12px; + opacity: 0.75; +} diff --git a/studio-frontend/src/app/app.element.ts b/studio-frontend/src/app/app.element.ts index 9212f25..1c574be 100644 --- a/studio-frontend/src/app/app.element.ts +++ b/studio-frontend/src/app/app.element.ts @@ -1,4 +1,4 @@ -import { drag, on } from "@storyteller/framework"; +import { drag, on, provide } from "@storyteller/framework"; import * as studio from "@storyteller/studio"; import { type SceneSavedEvent, @@ -6,8 +6,11 @@ import { SceneState, StudioMode, } from "@storyteller/studio"; +import { ButtonElement } from "@storyteller/studio-ui/button"; +import { DialogElement } from "@storyteller/studio-ui/dialog"; import { SceneHierarchyElement, + StorytellerApi, StudioURLParams, } from "@storyteller/studio-web"; import { match } from "@storyteller/utility"; @@ -16,27 +19,32 @@ import { customElement, state } from "lit/decorators.js"; import { classMap } from "lit/directives/class-map.js"; import { createRef, ref } from "lit/directives/ref.js"; import { styleMap } from "lit/directives/style-map.js"; -import { v4 as uuid } from "uuid"; import "@storyteller/studio-ui/button"; +import "@storyteller/studio-ui/dialog"; import "@storyteller/studio-ui/icon"; +import "@storyteller/studio-ui/overlay-provider"; import "@storyteller/studio-ui/toolbar"; import "@storyteller/studio-ui/tree"; +import "@storyteller/studio-web/add-to-scene-menu"; import "@storyteller/studio-web/canvas-provider"; import "@storyteller/studio-web/entity-inspector"; import "@storyteller/studio-web/iframe-messaging-bridge"; import "@storyteller/studio-web/loading"; +import "@storyteller/studio-web/media-library-dialog"; import "@storyteller/studio-web/scene-hierarchy"; import "@storyteller/studio-web/studio"; import "@storyteller/studio-web/transform-toolbar"; import "./anim-editor.element"; import "./help.element"; -import "./blocking.element"; - import "./app.element.scss"; @customElement("app-root") +@provide({ + token: StorytellerApi, + provide() { return this.api; } +}) export class AppElement extends LitElement { @state() showHelp = false; @state() showInspector = false; @@ -46,8 +54,11 @@ export class AppElement extends LitElement { @state() inspectorWidth = 320; @state() hierarchyHeight?: number; + private api = new StorytellerApi(); #enableAnimTimeline = false; // TODO + #addToSceneButtonRef = createRef(); + #mediaLibraryRef = createRef(); #inspectorRef = createRef(); #hierarchyRef = createRef(); @@ -59,34 +70,15 @@ export class AppElement extends LitElement { @on("scene-saved") async onSceneSaved({ detail: scene }: SceneSavedEvent): Promise { - const file = new File([scene], "scene.scn.ron", { type: "text/ron" }); - const formData = new FormData(); - - formData.append("uuid_idempotency_token", uuid()); - formData.append("file", file); - try { - const response = await fetch("https://api.fakeyou.com/v1/engine/create_scene", { - method: "POST", - credentials: "include", - headers: { "Accept": "text/plain" }, - body: formData, - }); + const file = new File([scene], "scene.scn.ron", { type: "text/ron" }); + const token = await this.api.createScene(file); - if (response.ok) { - const resultJson: { - success: boolean; - media_file_token: string; - } = await response.json(); - - if (resultJson.success) { - this.dispatchEvent(new CustomEvent("scene-uploaded", { - detail: resultJson.media_file_token, - bubbles: true, - composed: true, - })); - } - } + this.dispatchEvent(new CustomEvent("scene-uploaded", { + detail: token, + bubbles: true, + composed: true, + })); } catch (err) { console.error(err); @@ -145,6 +137,7 @@ export class AppElement extends LitElement { } protected override render = () => html` + @@ -174,6 +167,16 @@ export class AppElement extends LitElement { > Save + + Add to Scene + + ` : nothing} ${this.#params.mode === StudioMode.Editor ? html` - + { + this.#mediaLibraryRef.value?.open(); + }} + > + @@ -276,5 +288,6 @@ export class AppElement extends LitElement { ` : nothing} + `; } diff --git a/studio-frontend/src/app/blocking.element.scss b/studio-frontend/src/app/blocking.element.scss deleted file mode 100644 index 19a9a3d..0000000 --- a/studio-frontend/src/app/blocking.element.scss +++ /dev/null @@ -1,8 +0,0 @@ -:host { - display: grid; - grid-template-columns: 1fr 1fr; - gap: 1rem; - position: absolute; - top: 60px; - left: 16px; -} \ No newline at end of file diff --git a/studio-frontend/src/app/blocking.element.ts b/studio-frontend/src/app/blocking.element.ts deleted file mode 100644 index 9b88573..0000000 --- a/studio-frontend/src/app/blocking.element.ts +++ /dev/null @@ -1,41 +0,0 @@ - -import { LitElement, html, nothing, unsafeCSS } from "lit"; - -import styles from "./blocking.element.scss?inline"; -import { customElement } from "lit/decorators.js"; -import * as studio from "@storyteller/studio"; - -@customElement("sts-blocking") -export class BlockingElement extends LitElement { - static override styles = unsafeCSS(styles); - - override render = () => html` - { - studio.dispatchBlockingCommand(studio.BlockingCommand.Cube); - }}> - Cube - - - { - studio.dispatchBlockingCommand(studio.BlockingCommand.Sphere); - }}> - Sphere - - - { - studio.dispatchBlockingCommand(studio.BlockingCommand.Cylinder); - }}> - Cylinder - - - { - studio.dispatchBlockingCommand(studio.BlockingCommand.Torus); - }}> - Torus - - `; -} \ No newline at end of file diff --git a/studio-frontend/tsconfig.json b/studio-frontend/tsconfig.json index 724ccfc..a8f3853 100644 --- a/studio-frontend/tsconfig.json +++ b/studio-frontend/tsconfig.json @@ -6,7 +6,7 @@ "experimentalDecorators": true, "useDefineForClassFields": false, "module": "ESNext", - "lib": ["ES2020", "DOM", "DOM.Iterable"], + "lib": ["ES2021", "DOM", "DOM.Iterable"], "skipLibCheck": true, "moduleResolution": "Bundler", diff --git a/studio-ui/src/assets/icons/cylinder.svg b/studio-ui/src/assets/icons/cylinder.svg new file mode 100644 index 0000000..c411bd6 --- /dev/null +++ b/studio-ui/src/assets/icons/cylinder.svg @@ -0,0 +1,3 @@ + + + diff --git a/studio-ui/src/assets/icons/sphere.svg b/studio-ui/src/assets/icons/sphere.svg new file mode 100644 index 0000000..7c948a1 --- /dev/null +++ b/studio-ui/src/assets/icons/sphere.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/studio-ui/src/assets/icons/torus.svg b/studio-ui/src/assets/icons/torus.svg new file mode 100644 index 0000000..7cd4b8b --- /dev/null +++ b/studio-ui/src/assets/icons/torus.svg @@ -0,0 +1,4 @@ + + + + diff --git a/studio-ui/src/index.ts b/studio-ui/src/index.ts index fe3b7a5..b306d48 100644 --- a/studio-ui/src/index.ts +++ b/studio-ui/src/index.ts @@ -1,5 +1,8 @@ export * from "./lib/button.element"; +export * from "./lib/dialog.element"; export * from "./lib/icon.element"; +export * from "./lib/menu.element"; +export * from "./lib/overlay-provider.element"; export * from "./lib/progress.element"; export * from "./lib/toolbar.element"; export * from "./lib/tree.element"; diff --git a/studio-ui/src/lib/button.element.scss b/studio-ui/src/lib/button.element.scss index a171b45..42c2d83 100644 --- a/studio-ui/src/lib/button.element.scss +++ b/studio-ui/src/lib/button.element.scss @@ -35,6 +35,7 @@ :host(.disabled:hover) { background: #0003; color: #FFF6; + cursor: default; } .icon { diff --git a/studio-ui/src/lib/button.element.ts b/studio-ui/src/lib/button.element.ts index 73d6f5b..2a532e0 100644 --- a/studio-ui/src/lib/button.element.ts +++ b/studio-ui/src/lib/button.element.ts @@ -10,10 +10,12 @@ import "./icon.element"; export class ButtonElement extends LitElement { static override styles = unsafeCSS(styles); + @property({ reflect: true }) + override role = "button"; + @property() icon?: string; @property({ type: Boolean }) disabled = false; - override role = "button"; override tabIndex = 0; protected override willUpdate(changes: PropertyValues): void { @@ -35,9 +37,13 @@ export class ButtonElement extends LitElement { } protected override render = () => html` - ${this.icon - ? html`` - : nothing} + ${this.icon ? html` + + ` : nothing} ` } diff --git a/studio-ui/src/lib/dialog.element.ts b/studio-ui/src/lib/dialog.element.ts new file mode 100644 index 0000000..43326c8 --- /dev/null +++ b/studio-ui/src/lib/dialog.element.ts @@ -0,0 +1,191 @@ +import { inject, on } from "@storyteller/framework"; +import { LitElement, type PropertyValues } from "lit"; +import { html, unsafeStatic } from "lit/static-html.js"; +import { customElement, property } from "lit/decorators.js"; +import { ifDefined } from "lit/directives/if-defined.js"; +import { createRef, ref } from "lit/directives/ref.js"; +import { + OVERLAY_PROVIDER, + type OverlayHandle, + type OverlayProvider, +} from "./overlay-provider.element"; + +/** + * A simple, WIP [dialog widget](https://www.w3.org/WAI/ARIA/apg/patterns/dialog-modal/) + * implementation. + * + * @warning + * This component should be considered experimental! Its API is still a work-in- + * progress, potential use cases have not been thoroughly explored, and it may + * not behave as expected in all circumstances. + * + * @example + * ``` + * \@customElement("my-dialog") + * class MyDialogElement extends DialogBaseElement { + * render = () => html` + *
+ *

My Dialog

+ * this.dispatchEvent(new CustomEvent("close"))} + * > + *
+ *
+ *

Hello, world!

+ *
+ * ` + * } + * ``` + * ### Trigger-based implementation + * ``` + * \@customElement("my-app") + * class MyApp extends LitElement { + * dialogTriggerRef = createRef(); + * + * render = () => html` + * + * + * Open Dialog + * + * + * + * + * ` + * } + * ``` + * ### Manual-open implementation + * ``` + * \@customElement("my-app") + * class MyApp extends LitElement { + * dialogRef = createRef(); + * + * render = () => html` + * + * { + * this.dialogRef.value?.open(); + * }} + * > + * Open Dialog + * + * + * + * + * ` + * } + * ``` + */ +@customElement("sts-dialog") +export class DialogElement extends LitElement { + /** + * Tag-name of a custom element inheriting from {@linkcode DialogBaseElement}` + */ + @property() tag!: string; + + /** ARIA role to pass to the custom element defined by `tag`. */ + @property() dialogRole?: "dialog" | "alert"; + + /** + * @optional + * Ref of the HTML element which should open the dialog when clicked. + */ + @property({ attribute: false }) + trigger = createRef(); + + @inject(OVERLAY_PROVIDER) + _overlay!: OverlayProvider; + + #overlayHandle?: WeakRef; + #dialogRef = createRef(); + + open(): void { + this.#overlayHandle = this._overlay.attach(this, html` + <${unsafeStatic(this.tag)} + ${ref(this.#dialogRef)} + role=${ifDefined(this.dialogRole)} + @close=${() => this.close()} + > + `, { + containerStyles: { + display: "flex", + alignItems: "center", + justifyContent: "center", + }, + }); + } + + close(): void { + if (this.#overlayHandle) + this._overlay.detach(this.#overlayHandle); + } + + override connectedCallback(): void { + if (!this.tag) + throw new Error("DialogElement requires a static `tag` attribute!"); + + super.connectedCallback(); + } + + protected override updated(changes: PropertyValues): void { + if (changes.has("tag") && changes.get("tag") != null) { + throw new Error(`DialogElement's "tag" attribute should not be changed after rendering!`); + } + + if (changes.has("trigger")) { + const prevTrigger = changes.get("trigger"); + if (prevTrigger?.value) { + prevTrigger.value.removeEventListener("click", this.onTriggerClick); + } + + const trigger = this.trigger.value; + if (!trigger) return; + + trigger.addEventListener("click", this.onTriggerClick); + } + + super.updated(changes); + } + + override disconnectedCallback(): void { + this.trigger.value?.removeEventListener("click", this.onTriggerClick); + super.disconnectedCallback(); + } + + onTriggerClick = (): void => { + this.open(); + } +} + +export class DialogBaseElement extends LitElement { + @property({ reflect: true }) + override role = "dialog"; + + override tabIndex = -1; + + override connectedCallback(): void { + this.focus(); + super.connectedCallback(); + } + + @on("window:click") + onWindowClick(event: PointerEvent): void { + if (!event.composedPath().includes(this)) + this.dispatchEvent(new CustomEvent("close")); + } + + @on("keydown") + onKeydown(event: KeyboardEvent): void { + if (/^(Escape|Shift)$/.test(event.key)) + event.stopPropagation(); + + if (event.key === "Escape") + this.dispatchEvent(new CustomEvent("close")); + } +} diff --git a/studio-ui/src/lib/forms/text-field.element.scss b/studio-ui/src/lib/forms/text-field.element.scss index 27f954b..0236a5a 100644 --- a/studio-ui/src/lib/forms/text-field.element.scss +++ b/studio-ui/src/lib/forms/text-field.element.scss @@ -2,10 +2,12 @@ :host { box-sizing: border-box; - display: block; + display: flex; + align-items: center; width: 100%; padding: 0.5rem 0.75rem; border-radius: 0.25rem; + gap: 0.75rem; background: #0004; border: 1px solid #FFF2; @@ -22,6 +24,10 @@ } } +[part^="icon-"] { + color: #FFF6; +} + :host(:focus) { outline: none; border-color: #FFF4; @@ -30,11 +36,16 @@ } .input { - width: 100%; + flex: 1 0 0; appearance: none; background: transparent; border: none; font: inherit; color: inherit; outline: none !important; + + &::placeholder { + font: inherit; + color: #FFF6; + } } diff --git a/studio-ui/src/lib/forms/text-field.element.ts b/studio-ui/src/lib/forms/text-field.element.ts index 95b4fe9..5df4053 100644 --- a/studio-ui/src/lib/forms/text-field.element.ts +++ b/studio-ui/src/lib/forms/text-field.element.ts @@ -1,6 +1,7 @@ import { on } from "@storyteller/framework"; -import { html, unsafeCSS } from "lit"; +import { html, nothing, unsafeCSS } from "lit"; import { customElement, property } from "lit/decorators.js"; +import { ifDefined } from "lit/directives/if-defined.js"; import { FormFieldBaseElement } from "./form-field-base.element"; @@ -14,8 +15,18 @@ export class TextFieldElement extends FormFieldBaseElement { delegatesFocus: true, } + @property({ + attribute: "type", + reflect: true, + }) + fieldType = "text"; + + @property() placeholder?: string; @property() value = ""; + @property({ attribute: "icon-pre" }) iconPre?: string; + @property({ attribute: "icon-post" }) iconPost?: string; + @on("keydown") @on("keyup") blockKeypressBubbling(event: KeyboardEvent): void { @@ -24,18 +35,24 @@ export class TextFieldElement extends FormFieldBaseElement { } #onInput(event: InputEvent): void { - const updated = (event.target as HTMLInputElement).value; - console.log("updated:", updated); - - this.dispatchEvent(new CustomEvent("value-change", { detail: updated })); + this.dispatchEvent(new CustomEvent("value-change", { + detail: (event.target as HTMLInputElement).value, + })); } protected override render = () => html` + ${this.iconPre ? html` + + ` : nothing} + ${this.iconPost ? html` + + ` : nothing} `; } diff --git a/studio-ui/src/lib/icon.element.ts b/studio-ui/src/lib/icon.element.ts index 7e6b556..4a503b6 100644 --- a/studio-ui/src/lib/icon.element.ts +++ b/studio-ui/src/lib/icon.element.ts @@ -31,6 +31,21 @@ import fasCircleXmark from "@fortawesome/fontawesome-pro/svgs/solid/circle-xmark import fasMinus from "@fortawesome/fontawesome-pro/svgs/solid/minus.svg?raw"; import fasListTree from "@fortawesome/fontawesome-pro/svgs/solid/list-tree.svg?raw"; import fasBars from "@fortawesome/fontawesome-pro/svgs/solid/bars.svg?raw"; +import fasPlusLarge from "@fortawesome/fontawesome-pro/svgs/solid/plus-large.svg?raw"; +import farFileImport from "@fortawesome/fontawesome-pro/svgs/regular/file-import.svg?raw"; +import farPhotoFilmMusic from "@fortawesome/fontawesome-pro/svgs/regular/photo-film-music.svg?raw"; +import fasChevronDown from "@fortawesome/fontawesome-pro/svgs/solid/chevron-down.svg?raw"; +import farVolume from "@fortawesome/fontawesome-pro/svgs/regular/volume.svg?raw"; +import farFilm from "@fortawesome/fontawesome-pro/svgs/regular/film.svg?raw"; +import fasPersonRunning from "@fortawesome/fontawesome-pro/svgs/solid/person-running.svg?raw"; +import farMountainSun from "@fortawesome/fontawesome-pro/svgs/regular/mountain-sun.svg?raw"; +import farArrowRightToBracket from "@fortawesome/fontawesome-pro/svgs/regular/arrow-right-to-bracket.svg?raw"; +import farMagnifyingGlass from "@fortawesome/fontawesome-pro/svgs/regular/magnifying-glass.svg?raw"; +import fasHouse from "@fortawesome/fontawesome-pro/svgs/solid/house.svg?raw"; + +import customCylinder from "../assets/icons/cylinder.svg?raw"; +import customSphere from "../assets/icons/sphere.svg?raw"; +import customTorus from "../assets/icons/torus.svg?raw"; import styles from "./icon.element.scss?inline"; @@ -61,35 +76,49 @@ function renderSvg(iconId: string) { "bone": () => farBone, "bwd-fast": () => fasBwdFast, "bwd-step": () => fasBwdStep, + "caret-right": () => fasCaretRight, "check": () => fasCheck, + "chevron-down": () => fasChevronDown, "circle-check": () => fasCircleCheck, "circle-question": () => fasCircleQuestion, "circle-xmark": () => fasCircleXmark, - "caret-right": () => fasCaretRight, "crosshairs": () => farCrosshairs, "cube": () => farCube, + "cylinder": () => customCylinder, + "figure": () => fasPersonRunning, + "file-import": () => farFileImport, + "film": () => farFilm, "floppy-disk": () => fasFloppyDisk, "fwd-fast": () => fasFwdFast, "fwd-step": () => fasFwdStep, + "home": () => fasHouse, + "import": () => farArrowRightToBracket, "key": () => fasKey, "light": () => fasBrightness, "list-tree": () => fasListTree, "minus": () => fasMinus, "globe": () => farGlobe, + "photo-film-music": () => farPhotoFilmMusic, "play": () => fasPlay, + "plus-large": () => fasPlusLarge, "question": () => fasQuestion, "rotate": () => farRotate, "scale": () => farScale, + "scene": () => farMountainSun, + "search": () => farMagnifyingGlass, "skeleton": () => farSkeleton, + "sphere": () => customSphere, + "torus": () => customTorus, "up-down-left-right": () => farUpDownLeftRight, "video": () => farVideo, + "volume": () => farVolume, "xmark": () => fasXmark, _: () => null, }); if (!svgSrc) return nothing; - const [, attrsString, body] = svgSrc.match(/(.+?)<\/svg>/)!; + const [, attrsString, body] = svgSrc.match(/(.+?)<\/svg>/s)!; const attrs = Object.fromEntries( Array.from(attrsString.matchAll(/([-:_a-zA-Z0-9]+)(?:=['"]?([^'"]*)['"]?)/g)) .map(([, attr, value]) => [attr, value]) diff --git a/studio-ui/src/lib/menu.element.scss b/studio-ui/src/lib/menu.element.scss new file mode 100644 index 0000000..f1cb459 --- /dev/null +++ b/studio-ui/src/lib/menu.element.scss @@ -0,0 +1,35 @@ +:host { + display: flex; + flex-direction: column; + border-radius: 0.5rem; + padding: 0.5rem 0; + min-width: 220px; + + background: #0006; + backdrop-filter: blur(16px); + overflow: hidden; +} + +::slotted(sts-button) { + background: unset; + backdrop-filter: unset; + border-radius: unset; +} + +::slotted(sts-button:hover) { + background: #0004; + + // FIXME: https://github.com/w3c/csswg-drafts/issues/3896 + &::part(icon) { + opacity: 0.75; + } +} + +::slotted(hr) { + display: block; + width: 100%; + border: none; + height: 1px; + margin: 0.5rem 0; + background: #FFF1; +} diff --git a/studio-ui/src/lib/menu.element.ts b/studio-ui/src/lib/menu.element.ts new file mode 100644 index 0000000..6c51893 --- /dev/null +++ b/studio-ui/src/lib/menu.element.ts @@ -0,0 +1,213 @@ +import { inject, on } from "@storyteller/framework"; +import { LitElement, PropertyValues, css, html, unsafeCSS } from "lit"; +import { customElement, property } from "lit/decorators.js"; +import { ifDefined } from "lit/directives/if-defined.js"; +import { createRef, ref, type Ref } from "lit/directives/ref.js"; +import { styleMap } from "lit/directives/style-map.js"; +import { v4 as uuid } from "uuid"; + +import { + OVERLAY_PROVIDER, + type OverlayHandle, + type OverlayProvider, +} from "./overlay-provider.element"; + +import panelStyles from "./menu.element.scss?inline"; + +/** + * A simple, WIP [menu widget](https://www.w3.org/WAI/ARIA/apg/patterns/menubar/) + * implementation. + * + * @note + * This should _not_ be used for a `