diff --git a/studio-bvh/src/joint.rs b/studio-bvh/src/joint.rs index 4632498..a1b0cf4 100644 --- a/studio-bvh/src/joint.rs +++ b/studio-bvh/src/joint.rs @@ -494,9 +494,7 @@ impl<'a> Iterator for Joints<'a> { }) } JointIterType::Sequence(ref mut joints) => { - let Some(next) = joints.pop_front() else { - return None; - }; + let next = joints.pop_front()?; if next >= self.joints.len() { return None; diff --git a/studio-frontend/src/app/api/MakeMultipartRequest.ts b/studio-frontend/src/app/api/MakeMultipartRequest.ts new file mode 100644 index 0000000..337a662 --- /dev/null +++ b/studio-frontend/src/app/api/MakeMultipartRequest.ts @@ -0,0 +1,23 @@ +// import GetApiHost from "./GetApiHost"; + +// const { formatUrl, host = "" } = GetApiHost(); + +const MakeMultipartRequest = (endpoint = "", body: any) => { + const formData = new FormData(); + + Object.keys(body).forEach((key) => formData.append(key, body[key])); + + // formData.append('source', "video"); + + return fetch("https://funnel.tailce84f.ts.net/preview/", { + method: 'POST', + credentials: 'include', + headers: { + 'Accept': 'application/json', + }, + body: formData, + }) + .then(res => res.blob()); +} + +export default MakeMultipartRequest; \ No newline at end of file diff --git a/studio-frontend/src/app/api/MakeRequest.ts b/studio-frontend/src/app/api/MakeRequest.ts index b951133..142d9fc 100644 --- a/studio-frontend/src/app/api/MakeRequest.ts +++ b/studio-frontend/src/app/api/MakeRequest.ts @@ -8,8 +8,8 @@ interface RouteSetup { method: string; multipart?: boolean; routingFunction: UrlRoutingFunction; -} - +} + const METHOD_OMITS_BODY: { [key: string]: boolean } = { DELETE: false, GET: true, @@ -19,25 +19,50 @@ const METHOD_OMITS_BODY: { [key: string]: boolean } = { PUT: false, }; -const MakeRequest = (routeSetup: RouteSetup) : (urlRouteArgs: UrlRouteArgs, request: Request, queries?: UrlParams) => Promise => { - return async function(urlRouteArgs: UrlRouteArgs, request: Request, queries?: any) : Promise { - const newQueries = queries ? Object.keys(queries).map((key, i) => { - return `${ i ? "&" : "" }${ key }=${ Array.isArray(queries[key]) ? queries[key].join(`&${ key }=`) : queries[key] }` - }).join("") : null; +export default < + UrlRouteArgs, + Request, + Response, + UrlParams extends Record, +>(routeSetup: RouteSetup) => { + return async function ( + urlRouteArgs: UrlRouteArgs, + request: Request, + queries?: UrlParams, + overrideHost?: string + ): Promise { + const newQueries = queries + ? Object + .keys(queries) + .map((key, i) => + `${ i ? "&" : "" }${ key }=${ + Array.isArray(queries[key]) + ? (queries[key] as unknown[]).join(`&${ key }=`) + : queries[key] + }` + ) + .join("") + : null; - const endpoint = `${ routeSetup.routingFunction(urlRouteArgs) }${ newQueries ? "?" + newQueries : "" }`; + const endpoint = `${ + routeSetup.routingFunction(urlRouteArgs) + }${ + newQueries ? "?" + newQueries : "" + }`; const method = routeSetup.method; const methodOmitsBody = METHOD_OMITS_BODY[method] || false; console.log("🛼",request) - return fetch(`https://api.fakeyou.com${ endpoint }`, { - method, - headers: { - "Accept": "application/json", - ...methodOmitsBody? {} : { "Content-Type": "application/json" } + return fetch( + `${ overrideHost ? overrideHost : "https://api.fakeyou.com" }${ endpoint }`, + { + method, + headers: { + "Accept": "application/json", + ...methodOmitsBody? {} : { "Content-Type": "application/json" } }, - credentials: 'include', + credentials: "include", ...methodOmitsBody ? {} : { body: JSON.stringify(request) }, }) .then(res => res.json()) @@ -52,5 +77,3 @@ const MakeRequest = (routeSetup: Rou .catch(e => ({ success : false })); } }; - -export default MakeRequest; \ No newline at end of file diff --git a/studio-frontend/src/app/api/VideoStylePreview.ts b/studio-frontend/src/app/api/VideoStylePreview.ts new file mode 100644 index 0000000..2b2482f --- /dev/null +++ b/studio-frontend/src/app/api/VideoStylePreview.ts @@ -0,0 +1,10 @@ +import MakeMultipartRequest from "./MakeMultipartRequest"; + +export interface VideoStylePreviewRequest { + video: File, + request: string +} + +export const VideoStylePreview = (request: VideoStylePreviewRequest) => { + return MakeMultipartRequest("",request); +} diff --git a/studio-frontend/src/app/app.element.ts b/studio-frontend/src/app/app.element.ts index 26f7bfa..11ed178 100644 --- a/studio-frontend/src/app/app.element.ts +++ b/studio-frontend/src/app/app.element.ts @@ -37,6 +37,7 @@ import "@storyteller/studio-web/studio"; import "@storyteller/studio-web/transform-toolbar"; import "./anim-editor.element"; import "./help.element"; +import "./temp-video-preview.element"; import "./app.element.scss"; import { mock_save_load } from "./util"; @@ -60,6 +61,7 @@ export class AppElement extends LitElement { private api = new StorytellerApi(); #addToSceneButtonRef = createRef(); + #videoStylePreviewBtnRef = createRef(); #mediaLibraryRef = createRef(); #inspectorRef = createRef(); #hierarchyRef = createRef(); @@ -161,7 +163,7 @@ export class AppElement extends LitElement { - + ${this.#params.mode === StudioMode.Editor ? html` + + @@ -234,7 +242,7 @@ export class AppElement extends LitElement { ${this.renderSidebar()} @@ -252,7 +260,7 @@ export class AppElement extends LitElement {
- + ` : nothing} - + + + + + ${this.showHelp ? html`
` : nothing} - + ${this.loading ? html` diff --git a/studio-frontend/src/app/bubble-select.element.scss b/studio-frontend/src/app/bubble-select.element.scss index 621a66c..31e7f57 100644 --- a/studio-frontend/src/app/bubble-select.element.scss +++ b/studio-frontend/src/app/bubble-select.element.scss @@ -1,36 +1,57 @@ -.studio-bubble-select { +@use "sass:math"; + +:host { // background: red; display: flex; - list-style: none; - margin-block-start: 0px; - margin-block-end: 0px; - padding-inline-start: 0px; + gap: 0.6rem; + box-sizing: border-box; +} - li { - flex: 0 0 auto; - border: 2px solid darken(purple, 3%); - padding: 8px 16px; - border-radius: 0.5rem; - background-color: darken(purple, 3%); - cursor: pointer; - outline: none; - transition: all 0.2s ease; - font-size: 15px; - font-weight: 500; - color: rgba(#fff, 0.75); - white-space: nowrap; +*, *::before, *::after { + box-sizing: border-box; +} - &.selected-bubble { - color: #fff; - border-color: red; - background-color: lighten(purple, 6%); - - &.fill { - background-color: red; - } - } +.bubble { + position: relative; + flex: 0 0 auto; + display: flex; + align-items: flex-end; + width: 80px; + height: 80px; + padding: 0.5rem; + border-radius: 0.5rem; + background: { + size: cover; + position: center center; } + outline: #FFF2 solid 2px; + color: rgba(#fff, 0.75); + transition: { + property: background-color, outline-color; + duration: 200ms; + timing-function: ease; + } + white-space: nowrap; + cursor: pointer; -} \ No newline at end of file + &.selected { + outline-color: var(--color-accent); + } + + .gradient { + position: absolute; + inset: 0; + background: linear-gradient(to top, #000B, #0000); + border-radius: 0.5rem; + } + + .label { + position: relative; + font: { + size: math.div(12, 16) * 1rem; + weight: 500; + } + } +} diff --git a/studio-frontend/src/app/bubble-select.element.ts b/studio-frontend/src/app/bubble-select.element.ts index 2daf081..b08ca16 100644 --- a/studio-frontend/src/app/bubble-select.element.ts +++ b/studio-frontend/src/app/bubble-select.element.ts @@ -1,28 +1,54 @@ -import { LitElement, html, nothing, unsafeCSS } from "lit"; -import { customElement, property, state } from "lit/decorators.js"; +import { FormFieldBaseElement } from "@storyteller/studio-ui/forms/form-field-base"; +import { PropertyValues, html, unsafeCSS } from "lit"; +import { customElement, property } from "lit/decorators.js"; +import { classMap } from "lit/directives/class-map.js"; +import { styleMap } from "lit/directives/style-map.js"; + +import { type StyleOption, type VstStyle } from "./inspector-tabs/style-tab"; import styles from "./bubble-select.element.scss?inline"; -// const abc = "word"; - @customElement("sts-bubble-select") -export class BubbleSelect extends LitElement { +export class BubbleSelect extends FormFieldBaseElement { static override styles = unsafeCSS(styles); - @property() name = ""; - @property() onChange = (e: any) => {}; - @property() options = []; - @property() value = ""; + + @property({ reflect: true }) + override role = "listbox" + + @property({ attribute: false }) + options: readonly StyleOption[] = []; + + @property() value?: VstStyle; + + protected override update(changes: PropertyValues): void { + if (changes.has("value")) + this.internals.setFormValue(this.value ?? ""); + + super.update(changes); + } protected override render = () => html` -
    - ${ this.options.map(({ label, value: itemValue },i) => - html`
  • { - this.onChange({ target: { name: this.name, value: itemValue } }); - } } - >${ label }
  • ` - )} -
+ ${ this.options.map(({ label, value, imageUrl }) => html` +
{ + // this.onChange({ target: { name: this.name, value: itemValue } }); + this.dispatchEvent(new CustomEvent("value-change", { + detail: value, + })) + } } + > +
+ + ${ label } + +
` + )} ` -} \ No newline at end of file +} diff --git a/studio-frontend/src/app/inspector-tabs/style-tab.ts b/studio-frontend/src/app/inspector-tabs/style-tab.ts index 922e781..1311167 100644 --- a/studio-frontend/src/app/inspector-tabs/style-tab.ts +++ b/studio-frontend/src/app/inspector-tabs/style-tab.ts @@ -1,41 +1,56 @@ -import { LitElement, html, nothing, unsafeCSS } from "lit"; +import { bind, on } from "@storyteller/framework"; +import { LitElement, TemplateResult, html, unsafeCSS } from "lit"; import { customElement, property, state } from "lit/decorators.js"; -import styles from "./style-tab.scss?inline"; -import { EnqueueEngineCompositing } from "../api/EnqueueEngineCompositing"; import { v4 as uuid } from "uuid"; -import { drag, on, provide } from "@storyteller/framework"; + +import { EnqueueEngineCompositing } from "../api/EnqueueEngineCompositing"; + +import styles from "./style-tab.scss?inline"; + +export type VstStyle + = "anime_2d_flat" + | "cartoon_3d" + | "ink_bw_style" + | "paper_origami" + ; + +export interface StyleOption { + label: string; + imageUrl: string; + value: VstStyle; +} + +export const STYLE_OPTIONS: readonly StyleOption[] = [ + { + label: "2D Anime", + imageUrl: "https://fakeyou.com/images/landing/onboarding/styles/style-2d-anime.webp", + value: "anime_2d_flat", + }, + { + label: "3D Cartoon", + imageUrl: "https://fakeyou.com/images/landing/onboarding/styles/style-3d-cartoon.webp", + value: "cartoon_3d", + }, + { + label: "Ink B&W", + imageUrl: "https://fakeyou.com/images/landing/onboarding/styles/style-ink-bw.webp", + value: "ink_bw_style", + }, + { + label: "Origami", + imageUrl: "https://fakeyou.com/images/landing/onboarding/styles/style-origami.webp", + value: "paper_origami", + }, +]; @customElement("sts-style-tab") export class StyleTab extends LitElement { static override styles = unsafeCSS(styles); - @state() vstStyle = "anime_2d_flat"; + @state() vstStyle: VstStyle = "anime_2d_flat"; @state() posPrompt = ""; @state() negPrompt = ""; @property() sceneToken = ""; - styleOptions = [ - { - label: "2D Anime", - imageUrl: "/images/landing/onboarding/styles/style-2d-anime.webp", - value: "anime_2d_flat", - }, - { - label: "3D Cartoon", - imageUrl: "/images/landing/onboarding/styles/style-3d-cartoon.webp", - value: "cartoon_3d", - }, - { - label: "Ink B&W", - imageUrl: "/images/landing/onboarding/styles/style-ink-bw.webp", - value: "ink_bw_style", - }, - { - label: "Origami", - imageUrl: "/images/landing/onboarding/styles/style-origami.webp", - value: "paper_origami", - }, - ]; - inputChange = ({ target }: { target: { name: string, value: any } }) => { // console.log("💁🏼‍♀️", this[target.name]); // this[target.name] = target.value; // fix later @@ -69,32 +84,36 @@ export class StyleTab extends LitElement { console.log("🏎️", this.sceneToken); } - protected override render = () => html`
- ${ this.vstStyle } - - { this.vstStyle = target.value; } } - .options=${this.styleOptions} - .value=${this.vstStyle} - > - - - - - - Generate your movie - -
`; -} \ No newline at end of file + protected override render = (): TemplateResult => html` +
+ ${ this.vstStyle } + + + + + + + + + + + Generate your movie + +
+ `; +} diff --git a/studio-frontend/src/app/inspector.element.scss b/studio-frontend/src/app/inspector.element.scss deleted file mode 100644 index 5917751..0000000 --- a/studio-frontend/src/app/inspector.element.scss +++ /dev/null @@ -1,6 +0,0 @@ -.studio-inspector { - color: #fff; - background: blue; - position: absolute; - top: 50%; -} \ No newline at end of file diff --git a/studio-frontend/src/app/temp-video-preview.element.scss b/studio-frontend/src/app/temp-video-preview.element.scss new file mode 100644 index 0000000..a432086 --- /dev/null +++ b/studio-frontend/src/app/temp-video-preview.element.scss @@ -0,0 +1,109 @@ +@use "sass:math"; + +.video-style-preview { + width: calc(100vw - 3rem); + height: calc(100vh - 3rem); + + &::part(content) { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + gap: 2rem; + } + + &__side-by-side { + display: flex; + width: 100%; + } + + &__panel { + flex: 0 0 50%; + display: flex; + flex-direction: column; + align-items: center; + gap: 1rem; + } + + &__heading { + margin: 0; + font: { + family: "Fira Sans"; + size: 1rem; + weight: 500; + } + } + + &__image { + position: relative; + display: flex; + align-items: center; + justify-content: center; + width: 100%; + height: 0; + padding-top: calc((536 / 960) * 100%); + background: { + size: cover; + position: center center; + } + + * { + position: absolute; + top: 50%; + left: 50%; + transform: translateX(-50%) translateY(-50%); + } + } + + &__prompts { + display: flex; + gap: 1rem; + } + + &__form-field { + display: flex; + flex-direction: column; + gap: 0.75rem; + + &--base-style { + align-items: center; + } + } + + &__label { + font: { + family: "Fira Sans"; + size: math.div(15, 16) * 1rem; + weight: 500; + } + } + + &__input { + box-sizing: border-box; + display: flex; + align-items: center; + width: 100%; + padding: 0.5rem 0.75rem; + border-radius: 0.5rem; + gap: 0.75rem; + + background: #0004; + border: 2px solid #FFF2; + color: #FFFC; + transition: { + property: background-color, border-color, color; + duration: 33.333ms; + timing-function: linear; + } + + font: { + family: "Fira Sans", sans-serif; + size: math.div(15, 16) * 1rem; + } + + &:focus { + outline: none; + border-color: var(--color-accent); + } + } +} diff --git a/studio-frontend/src/app/temp-video-preview.element.ts b/studio-frontend/src/app/temp-video-preview.element.ts new file mode 100644 index 0000000..72e8cf0 --- /dev/null +++ b/studio-frontend/src/app/temp-video-preview.element.ts @@ -0,0 +1,269 @@ +import * as studio from "@storyteller/studio"; +import { LitElement, type PropertyValues, html, nothing, unsafeCSS, TemplateResult } from "lit"; +import { type Ref, createRef, ref } from "lit/directives/ref.js"; +import { customElement, property, state } from "lit/decorators.js"; +import { ifDefined } from "lit/directives/if-defined.js"; +import { styleMap } from "lit/directives/style-map.js"; +import { type ScreenshotReadyEvent } from "@storyteller/studio"; +import { DialogElement } from "@storyteller/studio-ui/dialog"; +import { bind, on } from "@storyteller/framework"; + +import { VideoStylePreview } from "./api/VideoStylePreview"; + +import "./bubble-select.element"; + +import styles from "./temp-video-preview.element.scss?inline"; +import { STYLE_OPTIONS, type VstStyle } from "./inspector-tabs/style-tab"; + +enum InspectorMode { + Properties, + Style +} + +@customElement("sts-temp-video-preview") +export class TempVideoPreview extends LitElement { + @property({ attribute: false }) + trigger?: Ref; + + @state() sourceBlob?: Blob; + @state() resultBlob?: Blob; + + @state() sourceImageUrl?: string; + @state() resultImageUrl?: string; + + @state() selectedStyle: VstStyle = "anime_2d_flat"; + @state() prompt = ""; + @state() negPrompt = ""; + + #temporaryVideoPreviewRef = createRef(); + + protected override willUpdate(changes: PropertyValues): void { + if (changes.has("trigger")) { + const prevTrigger = changes.get("trigger"); + prevTrigger?.value?.removeEventListener("click", this.onDialogOpen); + + this.trigger?.value?.addEventListener("click", this.onDialogOpen); + } + + super.willUpdate(changes) + } + + override disconnectedCallback(): void { + this.trigger?.value?.removeEventListener("click", this.onDialogOpen); + + if (this.sourceImageUrl) { + URL.revokeObjectURL(this.sourceImageUrl); + this.sourceImageUrl = undefined; + } + + if (this.resultImageUrl) { + URL.revokeObjectURL(this.resultImageUrl); + this.resultImageUrl = undefined; + } + + super.disconnectedCallback(); + } + + requestScreen = () => { + studio.requestScreenshot(); + console.log("🟢"); + } + + onDialogOpen = () => { + studio.requestScreenshot(); + } + + @on("window:screenshot-ready") + async onScreenshotReady (event: ScreenshotReadyEvent) { + console.log("🟣",event.detail.data); + const file = new File( + [event.detail.data], + "studio-scene-screen.png", + { type: "image/png" }, + ); + this.sourceBlob = file; + + const prevSourceUrl = this.sourceImageUrl; + this.sourceImageUrl = URL.createObjectURL(this.sourceBlob); + + if (prevSourceUrl) + URL.revokeObjectURL(prevSourceUrl); + + this.resultImageUrl = undefined; + + try { + this.resultBlob = await VideoStylePreview({ + video: file, + request: + JSON.stringify({ + "style": this.selectedStyle, + "positive_prompt": this.prompt, + "negative_prompt": this.negPrompt, + }) + }); + console.log("🟡", this.resultBlob); + } + catch (err) { + // TODO: Show error message + console.error(err); + return; + } + + const prevResultUrl = this.resultImageUrl; + this.resultImageUrl = URL.createObjectURL(this.resultBlob); + + if (prevResultUrl) + URL.revokeObjectURL(prevResultUrl); + } + + stopTextInputBubbling(event: KeyboardEvent): void { + // We want modifier keys like "Alt" to always be received by the Bevy + // canvas because they affect the camera controller behavior, but we do + // NOT want Bevy to receive typing inputs like W, A, S, and D while the + // user is trying to type into the Prompt field. + if (/^(Alt|Control)$/.test(event.key)) + return; + + event.stopPropagation(); + } + + protected override render = (): TemplateResult => html` + { + this.#temporaryVideoPreviewRef.value?.close(); + }} + @keydown=${this.stopTextInputBubbling} + @keyup=${this.stopTextInputBubbling} + > + + Video Style Preview + + + + +
+
+

+ Raw Preview +

+
+ ${!this.sourceImageUrl ? html` +

+ Loading... +

+ ` : nothing} +
+
+
+

+ Styled Preview +

+
+ ${!this.resultImageUrl ? html` +

+ Loading... +

+ ` : nothing} +
+
+
+ +
+ + +
+ +
+
+ + +
+
+ + +
+
+ + + + Update + + + + `] as const} + >
+ ` +} diff --git a/studio-ui/src/lib/button.element.scss b/studio-ui/src/lib/button.element.scss index 42c2d83..8ec7210 100644 --- a/studio-ui/src/lib/button.element.scss +++ b/studio-ui/src/lib/button.element.scss @@ -16,7 +16,7 @@ cursor: pointer; user-select: none; - background: #0006; + background: var(--background-color, #0006); color: #FFFC; backdrop-filter: blur(16px); transition: { diff --git a/studio-ui/src/lib/button.element.ts b/studio-ui/src/lib/button.element.ts index 2a532e0..06d25bc 100644 --- a/studio-ui/src/lib/button.element.ts +++ b/studio-ui/src/lib/button.element.ts @@ -16,16 +16,27 @@ export class ButtonElement extends LitElement { @property() icon?: string; @property({ type: Boolean }) disabled = false; + @property({ type: Boolean }) + primary?: boolean; + override tabIndex = 0; - protected override willUpdate(changes: PropertyValues): void { + protected override update(changes: PropertyValues): void { if (changes.has("disabled")) { this.ariaDisabled = this.disabled ? "true" : null; this.tabIndex = this.disabled ? -1 : 0; this.classList.toggle("disabled", this.disabled); } - super.willUpdate(changes); + if (changes.has("primary")) { + if (this.primary) { + this.style.setProperty("--background-color", "var(--color-accent)"); + } else { + this.style.removeProperty("--background-color"); + } + } + + super.update(changes); } @on("keydown") diff --git a/studio-ui/src/lib/dialog/dialog.element.ts b/studio-ui/src/lib/dialog/dialog.element.ts index 9800fcc..c5c1e72 100644 --- a/studio-ui/src/lib/dialog/dialog.element.ts +++ b/studio-ui/src/lib/dialog/dialog.element.ts @@ -392,6 +392,11 @@ export class DialogElement extends LitElement { trigger.addEventListener("click", this.onTriggerClick); } + if (changes.has("template")) { + if (this.#overlayHandle && this.template) + this._overlay.reRender(this.#overlayHandle, this.template); + } + super.updated(changes); } diff --git a/studio-ui/src/lib/forms/index.ts b/studio-ui/src/lib/forms/index.ts index ae82dc4..9f1ad51 100644 --- a/studio-ui/src/lib/forms/index.ts +++ b/studio-ui/src/lib/forms/index.ts @@ -1,2 +1,3 @@ +export * from "./form-field-base.element"; export * from "./text-field.element"; export * from "./scalar-field.element"; diff --git a/studio-ui/src/lib/icon.element.ts b/studio-ui/src/lib/icon.element.ts index a5610e9..952733d 100644 --- a/studio-ui/src/lib/icon.element.ts +++ b/studio-ui/src/lib/icon.element.ts @@ -45,6 +45,7 @@ import farMagnifyingGlass from "@fortawesome/fontawesome-pro/svgs/regular/magnif import fasHouse from "@fortawesome/fontawesome-pro/svgs/solid/house.svg?raw"; import fasRecord from "@fortawesome/fontawesome-pro/svgs/solid/circle.svg?raw"; import fasUpload from "@fortawesome/fontawesome-pro/svgs/solid/upload.svg?raw"; +import fasFilms from "@fortawesome/fontawesome-pro/svgs/solid/films.svg?raw"; import customCylinder from "../assets/icons/cylinder.svg?raw"; import customSphere from "../assets/icons/sphere.svg?raw"; @@ -91,6 +92,7 @@ function renderSvg(iconId: string) { "figure": () => fasPersonRunning, "file-import": () => farFileImport, "film": () => farFilm, + "films": () => fasFilms, "floppy-disk": () => fasFloppyDisk, "fwd-fast": () => fasFwdFast, "fwd-step": () => fasFwdStep, diff --git a/studio-ui/src/lib/menu.element.ts b/studio-ui/src/lib/menu.element.ts index 6c51893..f1d9a99 100644 --- a/studio-ui/src/lib/menu.element.ts +++ b/studio-ui/src/lib/menu.element.ts @@ -1,5 +1,5 @@ import { inject, on } from "@storyteller/framework"; -import { LitElement, PropertyValues, css, html, unsafeCSS } from "lit"; +import { LitElement, PropertyValues, TemplateResult, 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"; @@ -82,6 +82,8 @@ export class MenuElement extends LitElement { #panelRef = createRef(); #handle?: WeakRef; + #template?: TemplateResult; + show(): void { const trigger = this.trigger.value; if (!trigger) return; @@ -90,8 +92,7 @@ export class MenuElement extends LitElement { trigger.ariaExpanded = "true"; const rect = trigger.getBoundingClientRect(); - - this.#handle = this._overlay.attach(this, html` + this.#template = html` 's slot this.#slotRef.value?.assignedNodes() } - `); + `; + + this.#handle = this._overlay.attach(this, this.#template); } hide(): void { @@ -134,8 +137,9 @@ export class MenuElement extends LitElement { protected override update(changes: PropertyValues): void { super.update(changes); - if (this.#handle) - this._overlay.reRender(this.#handle); + + if (this.#handle && this.#template) + this._overlay.reRender(this.#handle, [this, this.#template]); } protected override updated(changes: PropertyValues): void { diff --git a/studio-ui/src/lib/overlay-provider.element.ts b/studio-ui/src/lib/overlay-provider.element.ts index 9a4a66e..40b9605 100644 --- a/studio-ui/src/lib/overlay-provider.element.ts +++ b/studio-ui/src/lib/overlay-provider.element.ts @@ -83,7 +83,7 @@ export interface OverlayProvider { detach(handle: WeakRef): void; /** Re-render a currently-attached overlay. */ - reRender(handle: WeakRef): void; + reRender(handle: WeakRef, template: [LitElement, TemplateResult]): void; } export interface AttachOptions { @@ -226,19 +226,14 @@ export class OverlayProviderElement // TODO: I'm not actually sure whether this serves a necessary function. // Needs further testing. - reRender(handle: WeakRef): void { + reRender(handle: WeakRef, template: [LitElement, TemplateResult]): void { const index = handle.deref()?.id; if (index == null || index === -1) return; - const container = this.#containerRefs[index]?.value; - if (!container) return; + this.#templates[index] = template; - const tuple = this.#templates[index]; - if (!tuple) return; - - const [host, template] = tuple; - render(template, container, { host }); + this.#updateLightDom(); } #updateLightDom(): void { diff --git a/tsconfig.base.json b/tsconfig.base.json index 7b67ed2..df667bb 100644 --- a/tsconfig.base.json +++ b/tsconfig.base.json @@ -26,6 +26,7 @@ "@storyteller/studio-ui/progress": ["studio-ui/src/lib/progress.element.ts"], "@storyteller/studio-ui/toolbar": ["studio-ui/src/lib/toolbar.element.ts"], "@storyteller/studio-ui/tree": ["studio-ui/src/lib/tree.element.ts"], + "@storyteller/studio-ui/forms/form-field-base": ["studio-ui/src/lib/forms/form-field-base.element.ts"], "@storyteller/studio-ui/forms/text-field": ["studio-ui/src/lib/forms/text-field.element.ts"], "@storyteller/studio-ui/forms/scalar-field": ["studio-ui/src/lib/forms/scalar-field.element.ts"], "@storyteller/studio-web": ["studio-web/src/index.ts"],