diff --git a/emain/emain-builder.ts b/emain/emain-builder.ts index 33ca244681..8b223c0f9c 100644 --- a/emain/emain-builder.ts +++ b/emain/emain-builder.ts @@ -4,7 +4,7 @@ import { ClientService } from "@/app/store/services"; import { RpcApi } from "@/app/store/wshclientapi"; import { randomUUID } from "crypto"; -import { BrowserWindow } from "electron"; +import { BrowserWindow, webContents } from "electron"; import { globalEvents } from "emain/emain-events"; import path from "path"; import { getElectronAppBasePath, isDevVite, unamePlatform } from "./emain-platform"; @@ -87,6 +87,20 @@ export async function createBuilderWindow(appId: string): Promise { + const wc = typedBuilderWindow.webContents; + if (wc.isDevToolsOpened()) { + wc.closeDevTools(); + } + for (const guest of webContents.getAllWebContents()) { + if (guest.getType() === "webview" && guest.hostWebContents?.id === wc.id) { + if (guest.isDevToolsOpened()) { + guest.closeDevTools(); + } + } + } + }); + typedBuilderWindow.on("focus", () => { focusedBuilderWindow = typedBuilderWindow; console.log("builder window focused", builderId); diff --git a/emain/emain-ipc.ts b/emain/emain-ipc.ts index 38067b7790..5e5f15b302 100644 --- a/emain/emain-ipc.ts +++ b/emain/emain-ipc.ts @@ -490,6 +490,17 @@ export function initIpcHandlers() { console.error("Error deleting builder rtinfo:", e); } } + const wc = bw.webContents; + if (wc.isDevToolsOpened()) { + wc.closeDevTools(); + } + for (const guest of electron.webContents.getAllWebContents()) { + if (guest.getType() === "webview" && guest.hostWebContents?.id === wc.id) { + if (guest.isDevToolsOpened()) { + guest.closeDevTools(); + } + } + } bw.destroy(); }); diff --git a/emain/emain-window.ts b/emain/emain-window.ts index 98276bbdd2..e3bfa87751 100644 --- a/emain/emain-window.ts +++ b/emain/emain-window.ts @@ -5,7 +5,7 @@ import { ClientService, ObjectService, WindowService, WorkspaceService } from "@ import { waveEventSubscribeSingle } from "@/app/store/wps"; import { RpcApi } from "@/app/store/wshclientapi"; import { fireAndForget } from "@/util/util"; -import { BaseWindow, BaseWindowConstructorOptions, dialog, globalShortcut, ipcMain, screen } from "electron"; +import { BaseWindow, BaseWindowConstructorOptions, dialog, globalShortcut, ipcMain, screen, webContents } from "electron"; import { globalEvents } from "emain/emain-events"; import path from "path"; import { debounce } from "throttle-debounce"; @@ -299,6 +299,7 @@ export class WaveBrowserWindow extends BaseWindow { if (this.isDestroyed()) { return; } + this.closeAllDevTools(); console.log("win 'close' handler fired", this.waveWindowId); if (getGlobalIsQuitting() || updater?.status == "installing" || getGlobalIsRelaunching()) { return; @@ -358,6 +359,24 @@ export class WaveBrowserWindow extends BaseWindow { setTimeout(() => globalEvents.emit("windows-updated"), 50); } + private closeAllDevTools() { + for (const tabView of this.allLoadedTabViews.values()) { + if (tabView.webContents?.isDevToolsOpened()) { + tabView.webContents.closeDevTools(); + } + } + const tabViewIds = new Set( + [...this.allLoadedTabViews.values()].map((tv) => tv.webContents?.id).filter((id) => id != null) + ); + for (const wc of webContents.getAllWebContents()) { + if (wc.getType() === "webview" && tabViewIds.has(wc.hostWebContents?.id)) { + if (wc.isDevToolsOpened()) { + wc.closeDevTools(); + } + } + } + } + private removeAllChildViews() { for (const tabView of this.allLoadedTabViews.values()) { if (!this.isDestroyed()) { diff --git a/frontend/app/aipanel/waveai-model.tsx b/frontend/app/aipanel/waveai-model.tsx index 9af1d88508..194005adc6 100644 --- a/frontend/app/aipanel/waveai-model.tsx +++ b/frontend/app/aipanel/waveai-model.tsx @@ -41,6 +41,27 @@ export interface DroppedFile { previewUrl?: string; } +const BuilderAIModeConfigs: Record = { + "waveaibuilder@default": { + "display:name": "Builder Default", + "display:order": -2, + "display:icon": "sparkles", + "display:description": "Good mix of speed and accuracy\n(gpt-5.4 with minimal thinking)", + "ai:provider": "wave", + "ai:switchcompat": ["wavecloud"], + "waveai:premium": true, + }, + "waveaibuilder@deep": { + "display:name": "Builder Deep", + "display:order": -1, + "display:icon": "lightbulb", + "display:description": "Slower but most capable\n(gpt-5.4 with full reasoning)", + "ai:provider": "wave", + "ai:switchcompat": ["wavecloud"], + "waveai:premium": true, + }, +}; + export class WaveAIModel { private static instance: WaveAIModel | null = null; inputRef: React.RefObject | null = null; @@ -80,7 +101,11 @@ export class WaveAIModel { this.orefContext = orefContext; this.inBuilder = inBuilder; this.chatId = jotai.atom(null) as jotai.PrimitiveAtom; - this.aiModeConfigs = atoms.waveaiModeConfigAtom; + if (inBuilder) { + this.aiModeConfigs = jotai.atom(BuilderAIModeConfigs) as jotai.Atom>; + } else { + this.aiModeConfigs = atoms.waveaiModeConfigAtom; + } this.hasPremiumAtom = jotai.atom((get) => { const rateLimitInfo = get(atoms.waveAIRateLimitInfoAtom); @@ -118,7 +143,7 @@ export class WaveAIModel { this.defaultModeAtom = jotai.atom((get) => { const telemetryEnabled = get(getSettingsKeyAtom("telemetry:enabled")) ?? false; if (this.inBuilder) { - return telemetryEnabled ? "waveai@balanced" : "invalid"; + return telemetryEnabled ? "waveaibuilder@default" : "invalid"; } const aiModeConfigs = get(this.aiModeConfigs); if (!telemetryEnabled) { diff --git a/frontend/app/view/webview/webview.tsx b/frontend/app/view/webview/webview.tsx index 551f23bbb7..f6d98b8f22 100644 --- a/frontend/app/view/webview/webview.tsx +++ b/frontend/app/view/webview/webview.tsx @@ -19,9 +19,9 @@ import { openLink } from "@/store/global"; import { adaptFromReactOrNativeKeyEvent, checkKeyPressed } from "@/util/keyutil"; import { fireAndForget, useAtomValueSafe } from "@/util/util"; import clsx from "clsx"; -import { WebviewTag } from "electron"; +import type { WebviewTag } from "electron"; import { Atom, PrimitiveAtom, atom, useAtomValue, useSetAtom } from "jotai"; -import { Fragment, createRef, memo, useCallback, useEffect, useRef, useState } from "react"; +import { Fragment, createRef, memo, useCallback, useEffect, useLayoutEffect, useRef, useState } from "react"; import "./webview.scss"; import type { WebViewEnv } from "./webviewenv"; @@ -951,6 +951,15 @@ const WebView = memo(({ model, onFailLoad, blockRef, initialSrc }: WebViewProps) }, 100); } + useLayoutEffect(() => { + return () => { + const webview = model.webviewRef.current; + if (webview?.isDevToolsOpened()) { + webview.closeDevTools(); + } + }; + }, []); + useEffect(() => { return () => { globalStore.set(model.domReady, false); diff --git a/frontend/builder/builder-apppanel.tsx b/frontend/builder/builder-apppanel.tsx index 780f6efa99..3c7e08f0f8 100644 --- a/frontend/builder/builder-apppanel.tsx +++ b/frontend/builder/builder-apppanel.tsx @@ -257,6 +257,10 @@ const BuilderAppPanel = memo(() => { model.switchBuilderApp(); }, [model]); + const handleOpenDevToolsClick = useCallback(() => { + model.openPreviewDevTools(); + }, [model]); + const handleKebabClick = useCallback( (e: React.MouseEvent) => { const menu: ContextMenuItem[] = [ @@ -267,6 +271,13 @@ const BuilderAppPanel = memo(() => { { type: "separator", }, + { + label: "Open DevTools", + click: handleOpenDevToolsClick, + }, + { + type: "separator", + }, { label: "Switch App", click: handleSwitchAppClick, @@ -274,7 +285,7 @@ const BuilderAppPanel = memo(() => { ]; ContextMenuModel.getInstance().showContextMenu(menu, e); }, - [handleSwitchAppClick, handlePublishClick] + [handleSwitchAppClick, handlePublishClick, handleOpenDevToolsClick] ); return ( diff --git a/frontend/builder/store/builder-apppanel-model.ts b/frontend/builder/store/builder-apppanel-model.ts index 4decca651a..3065687cde 100644 --- a/frontend/builder/store/builder-apppanel-model.ts +++ b/frontend/builder/store/builder-apppanel-model.ts @@ -7,6 +7,7 @@ import { RpcApi } from "@/app/store/wshclientapi"; import { TabRpcClient } from "@/app/store/wshrpcutil"; import { atoms, getApi, WOS } from "@/store/global"; import { base64ToString, stringToBase64 } from "@/util/util"; +import type { WebviewTag } from "electron"; import { atom, type Atom, type PrimitiveAtom } from "jotai"; import type * as MonacoTypes from "monaco-editor"; import { debounce } from "throttle-debounce"; @@ -35,6 +36,7 @@ export class BuilderAppPanelModel { saveNeededAtom!: Atom; focusElemRef: { current: HTMLInputElement | null } = { current: null }; monacoEditorRef: { current: MonacoTypes.editor.IStandaloneCodeEditor | null } = { current: null }; + webviewRef: { current: WebviewTag | null } = { current: null }; statusUnsubFn: (() => void) | null = null; appGoUpdateUnsubFn: (() => void) | null = null; debouncedRestart: (() => void) & { cancel: () => void }; @@ -314,6 +316,15 @@ export class BuilderAppPanelModel { this.monacoEditorRef.current = ref; } + openPreviewDevTools() { + if (!this.webviewRef.current) return; + if (this.webviewRef.current.isDevToolsOpened()) { + this.webviewRef.current.closeDevTools(); + } else { + this.webviewRef.current.openDevTools(); + } + } + dispose() { if (this.statusUnsubFn) { this.statusUnsubFn(); diff --git a/frontend/builder/tabs/builder-previewtab.tsx b/frontend/builder/tabs/builder-previewtab.tsx index 2258e31441..2976080680 100644 --- a/frontend/builder/tabs/builder-previewtab.tsx +++ b/frontend/builder/tabs/builder-previewtab.tsx @@ -70,8 +70,8 @@ const ErrorStateView = memo(({ errorMsg }: { errorMsg: string }) => {

Secrets Required

- This app requires secrets that must be configured. Please use the Secrets tab to set and bind - the required secrets for your app to run. + This app requires secrets that must be configured. Please use the Secrets tab to set and + bind the required secrets for your app to run.

{displayMsg}
@@ -178,47 +178,48 @@ const BuilderPreviewTab = memo(() => { const originalContent = useAtomValue(model.originalContentAtom); const builderStatus = useAtomValue(model.builderStatusAtom); const builderId = useAtomValue(atoms.builderId); - const fileExists = originalContent.length > 0; - - if (isLoading) { - return null; - } - - if (builderStatus?.status === "error") { - return ; - } - - if (!fileExists) { - return ; - } + const [lastKnownUrl, setLastKnownUrl] = useState(null); const status = builderStatus?.status || "init"; + const isWebViewActive = status === "running" && builderStatus?.port && builderStatus.port !== 0; - if (status === "init") { - return null; - } - - if (status === "building") { - return ; - } - - if (status === "stopped") { - return model.startBuilder()} />; + if (isWebViewActive) { + const previewUrl = `http://localhost:${builderStatus.port}/?clientid=wave:${builderId}`; + if (previewUrl !== lastKnownUrl) { + setLastKnownUrl(previewUrl); + } } - const shouldShowWebView = status === "running" && builderStatus?.port && builderStatus.port !== 0; - - if (shouldShowWebView) { - const previewUrl = `http://localhost:${builderStatus.port}/?clientid=wave:${builderId}`; - return ( -
- -
- ); + let overlay = null; + if (!isLoading && !isWebViewActive) { + if (builderStatus?.status === "error") { + overlay = ; + } else if (!fileExists || status === "init") { + overlay = ; + } else if (status === "building") { + overlay = ; + } else if (status === "stopped") { + overlay = model.startBuilder()} />; + } } - return null; + return ( +
+ {lastKnownUrl && ( + + )} + {overlay &&
{overlay}
} +
+ ); }); BuilderPreviewTab.displayName = "BuilderPreviewTab"; diff --git a/pkg/aiusechat/uctypes/uctypes.go b/pkg/aiusechat/uctypes/uctypes.go index d2b25bbc1b..a222eb5c9c 100644 --- a/pkg/aiusechat/uctypes/uctypes.go +++ b/pkg/aiusechat/uctypes/uctypes.go @@ -163,9 +163,11 @@ const ( ) const ( - AIModeQuick = "waveai@quick" - AIModeBalanced = "waveai@balanced" - AIModeDeep = "waveai@deep" + AIModeQuick = "waveai@quick" + AIModeBalanced = "waveai@balanced" + AIModeDeep = "waveai@deep" + AIModeBuilderDefault = "waveaibuilder@default" + AIModeBuilderDeep = "waveaibuilder@deep" ) const ( diff --git a/pkg/aiusechat/usechat-mode.go b/pkg/aiusechat/usechat-mode.go index 1b1875202e..94fe20ef9d 100644 --- a/pkg/aiusechat/usechat-mode.go +++ b/pkg/aiusechat/usechat-mode.go @@ -247,7 +247,44 @@ func isValidAzureResourceName(name string) bool { return AzureResourceNameRegex.MatchString(name) } +var builderModeConfigs = map[string]wconfig.AIModeConfigType{ + uctypes.AIModeBuilderDefault: { + DisplayName: "Builder Default", + DisplayOrder: -2, + DisplayIcon: "sparkles", + DisplayDescription: "Good mix of speed and accuracy\n(gpt-5.4 with minimal thinking)", + Provider: uctypes.AIProvider_Wave, + APIType: uctypes.APIType_OpenAIResponses, + Model: "gpt-5.4", + ThinkingLevel: uctypes.ThinkingLevelLow, + Verbosity: uctypes.VerbosityLevelLow, + Capabilities: []string{uctypes.AICapabilityTools, uctypes.AICapabilityImages, uctypes.AICapabilityPdfs}, + WaveAIPremium: true, + SwitchCompat: []string{"wavecloud"}, + }, + uctypes.AIModeBuilderDeep: { + DisplayName: "Builder Deep", + DisplayOrder: -1, + DisplayIcon: "lightbulb", + DisplayDescription: "Slower but most capable\n(gpt-5.4 with full reasoning)", + Provider: uctypes.AIProvider_Wave, + APIType: uctypes.APIType_OpenAIResponses, + Model: "gpt-5.4", + ThinkingLevel: uctypes.ThinkingLevelMedium, + Verbosity: uctypes.VerbosityLevelLow, + Capabilities: []string{uctypes.AICapabilityTools, uctypes.AICapabilityImages, uctypes.AICapabilityPdfs}, + WaveAIPremium: true, + SwitchCompat: []string{"wavecloud"}, + }, +} + func getAIModeConfig(aiMode string) (*wconfig.AIModeConfigType, error) { + if config, ok := builderModeConfigs[aiMode]; ok { + resolved := config + applyProviderDefaults(&resolved) + return &resolved, nil + } + fullConfig := wconfig.GetWatcher().GetFullConfig() config, ok := fullConfig.WaveAIModes[aiMode] if !ok { @@ -271,13 +308,13 @@ func handleConfigUpdate(fullConfig wconfig.FullConfigType) { func ComputeResolvedAIModeConfigs(fullConfig wconfig.FullConfigType) map[string]wconfig.AIModeConfigType { resolvedConfigs := make(map[string]wconfig.AIModeConfigType) - + for modeName, modeConfig := range fullConfig.WaveAIModes { resolved := modeConfig applyProviderDefaults(&resolved) resolvedConfigs[modeName] = resolved } - + return resolvedConfigs } @@ -285,7 +322,7 @@ func broadcastAIModeConfigs(configs map[string]wconfig.AIModeConfigType) { update := wconfig.AIModeConfigUpdate{ Configs: configs, } - + wps.Broker.Publish(wps.WaveEvent{ Event: wps.Event_AIModeConfig, Data: update, diff --git a/pkg/aiusechat/usechat.go b/pkg/aiusechat/usechat.go index a55a10060a..d9de760afd 100644 --- a/pkg/aiusechat/usechat.go +++ b/pkg/aiusechat/usechat.go @@ -670,8 +670,8 @@ func WaveAIPostMessageHandler(w http.ResponseWriter, r *http.Request) { } // Get WaveAI settings - premium := shouldUsePremium() builderMode := req.BuilderId != "" + premium := shouldUsePremium() || builderMode if req.AIMode == "" { http.Error(w, "aimode is required in request body", http.StatusBadRequest) return diff --git a/tsconfig.json b/tsconfig.json index 8fd50d2f96..d12f31cd6c 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -13,7 +13,6 @@ "resolveJsonModule": true, "isolatedModules": true, "experimentalDecorators": true, - "downlevelIteration": true, "baseUrl": "./", "paths": { "@/app/*": ["frontend/app/*"], diff --git a/tsunami/frontend/src/app.tsx b/tsunami/frontend/src/app.tsx index 38c94e0a51..9b12c38b52 100644 --- a/tsunami/frontend/src/app.tsx +++ b/tsunami/frontend/src/app.tsx @@ -9,7 +9,7 @@ const globalModel = new TsunamiModel(); function App() { return ( -
+
); diff --git a/tsunami/frontend/src/tailwind.css b/tsunami/frontend/src/tailwind.css index 945398cd53..c6ae61ecb2 100644 --- a/tsunami/frontend/src/tailwind.css +++ b/tsunami/frontend/src/tailwind.css @@ -62,7 +62,10 @@ } /* Disable overscroll behavior */ -html, body { +html, body, #root { + height: 100%; + color-scheme: dark; + background: var(--color-background); overscroll-behavior: none; overscroll-behavior-x: none; overscroll-behavior-y: none; diff --git a/tsunami/frontend/src/types/custom.d.ts b/tsunami/frontend/src/types/custom.d.ts index b7c843aeb2..92264260e8 100644 --- a/tsunami/frontend/src/types/custom.d.ts +++ b/tsunami/frontend/src/types/custom.d.ts @@ -12,4 +12,5 @@ type KeyPressDecl = { }; key: string; keyType: string; + nomatch?: boolean; }; diff --git a/tsunami/frontend/src/types/vdom.d.ts b/tsunami/frontend/src/types/vdom.d.ts index 2ca0f73867..80b9215452 100644 --- a/tsunami/frontend/src/types/vdom.d.ts +++ b/tsunami/frontend/src/types/vdom.d.ts @@ -82,8 +82,10 @@ type VDomFunc = { type: "func"; stoppropagation?: boolean; preventdefault?: boolean; + preventbackend?: boolean; globalevent?: string; keys?: string[]; + jscode?: string; }; // vdom.VDomMessage diff --git a/tsunami/frontend/src/util/keyutil.ts b/tsunami/frontend/src/util/keyutil.ts index 625cc1fc7c..68eb0b6823 100644 --- a/tsunami/frontend/src/util/keyutil.ts +++ b/tsunami/frontend/src/util/keyutil.ts @@ -72,7 +72,9 @@ function parseKey(key: string): { key: string; type: string } { function parseKeyDescription(keyDescription: string): KeyPressDecl { let rtn = { key: "", mods: {} } as KeyPressDecl; let keys = keyDescription.replace(/[()]/g, "").split(":"); - for (let key of keys) { + for (let i = 0; i < keys.length; i++) { + let key = keys[i]; + let isLastToken = i === keys.length - 1; if (key == "Cmd") { if (PLATFORM == PlatformMacOS) { rtn.mods.Meta = true; @@ -106,6 +108,10 @@ function parseKeyDescription(keyDescription: string): KeyPressDecl { } rtn.mods.Meta = true; } else { + if (!isLastToken) { + rtn.nomatch = true; + return rtn; + } let { key: parsedKey, type: keyType } = parseKey(key); rtn.key = parsedKey; rtn.keyType = keyType; @@ -194,6 +200,9 @@ function isInputEvent(event: VDomKeyboardEvent): boolean { function checkKeyPressed(event: VDomKeyboardEvent, keyDescription: string): boolean { let keyPress = parseKeyDescription(keyDescription); + if (keyPress.nomatch) { + return false; + } if (notMod(keyPress.mods.Option, event.option)) { return false; } @@ -236,6 +245,9 @@ function checkKeyPressed(event: VDomKeyboardEvent, keyDescription: string): bool } function adaptFromReactOrNativeKeyEvent(event: React.KeyboardEvent | KeyboardEvent): VDomKeyboardEvent { + if (event == null || typeof event.key !== "string") { + return { type: "unknown" } as VDomKeyboardEvent; + } let rtn: VDomKeyboardEvent = {} as VDomKeyboardEvent; rtn.control = event.ctrlKey; rtn.shift = event.shiftKey; diff --git a/tsunami/frontend/src/vdom.tsx b/tsunami/frontend/src/vdom.tsx index a51e119193..b2753e1f7c 100644 --- a/tsunami/frontend/src/vdom.tsx +++ b/tsunami/frontend/src/vdom.tsx @@ -170,15 +170,21 @@ const SvgUrlIdAttributes = { "text-decoration": true, }; -function convertVDomFunc(model: TsunamiModel, fnDecl: VDomFunc, compId: string, propName: string): (e: any) => void { - return (e: any) => { +function convertVDomFunc( + model: TsunamiModel, + fnDecl: VDomFunc, + compId: string, + propName: string +): (...args: any[]) => any { + return (...args: any[]) => { + const e = args[0]; if ((propName == "onKeyDown" || propName == "onKeyDownCapture") && fnDecl["keys"]) { dlog("key event", fnDecl, e); let waveEvent = adaptFromReactOrNativeKeyEvent(e); for (let keyDesc of fnDecl["keys"] || []) { if (checkKeyPressed(waveEvent, keyDesc)) { - e.preventDefault(); - e.stopPropagation(); + e?.preventDefault?.(); + e?.stopPropagation?.(); model.callVDomFunc(fnDecl, e, compId, propName); return; } @@ -186,12 +192,24 @@ function convertVDomFunc(model: TsunamiModel, fnDecl: VDomFunc, compId: string, return; } if (fnDecl.preventdefault) { - e.preventDefault(); + e?.preventDefault?.(); } if (fnDecl.stoppropagation) { - e.stopPropagation(); + e?.stopPropagation?.(); } - model.callVDomFunc(fnDecl, e, compId, propName); + let retVal: any; + if (fnDecl.jscode) { + try { + const fn = eval(fnDecl.jscode); + if (typeof fn === "function") retVal = fn(...args); + } catch (err) { + console.error("vdom jscode error:", err); + } + } + if (!fnDecl.preventbackend) { + model.callVDomFunc(fnDecl, e, compId, propName); + } + return retVal; }; } @@ -254,7 +272,7 @@ function convertChildren(elem: VDomElem, model: TsunamiModel): React.ReactNode[] if (elem.children == null || elem.children.length == 0) { return null; } - let childrenComps: React.ReactNode[] = []; + const childrenComps: React.ReactNode[] = []; for (let child of elem.children) { if (child == null) { continue; diff --git a/tsunami/vdom/vdom.go b/tsunami/vdom/vdom.go index 513325dc0d..bd7099a200 100644 --- a/tsunami/vdom/vdom.go +++ b/tsunami/vdom/vdom.go @@ -99,6 +99,20 @@ func H(tag string, props map[string]any, children ...any) *VDomElem { return rtn } +// JSFunc creates a VDomFunc that executes client-side JS only, with no backend call. +// jsCode must be a JS function expression whose signature matches the natural arguments of the event handler +// (e.g. (e) => { ... } for DOM events, or whatever args the underlying handler receives). +func JSFunc(jsCode string) *VDomFunc { + return &VDomFunc{Type: ObjectType_Func, JsCode: jsCode, PreventBackend: true} +} + +// CombinedFunc creates a VDomFunc that executes client-side JS first, then fires to the backend. +// jsCode must be a JS function expression whose signature matches the natural arguments of the event handler +// (e.g. (e) => { ... } for DOM events, or whatever args the underlying handler receives). +func CombinedFunc(jsCode string, fn any) *VDomFunc { + return &VDomFunc{Type: ObjectType_Func, JsCode: jsCode, Fn: fn} +} + // If returns the provided part if the condition is true, otherwise returns nil. // This is useful for conditional rendering in VDOM children lists, props, and style attributes. func If(cond bool, part any) any { diff --git a/tsunami/vdom/vdom_types.go b/tsunami/vdom/vdom_types.go index 58725e4010..f3fcf558fc 100644 --- a/tsunami/vdom/vdom_types.go +++ b/tsunami/vdom/vdom_types.go @@ -32,8 +32,10 @@ type VDomFunc struct { Type string `json:"type" tstype:"\"func\""` StopPropagation bool `json:"stoppropagation,omitempty"` // set to call e.stopPropagation() on the client side PreventDefault bool `json:"preventdefault,omitempty"` // set to call e.preventDefault() on the client side + PreventBackend bool `json:"preventbackend,omitempty"` // set to skip firing the event to the backend GlobalEvent string `json:"globalevent,omitempty"` Keys []string `json:"keys,omitempty"` // special for keyDown events a list of keys to "capture" + JsCode string `json:"jscode,omitempty"` // client-side JS function expression: (e, elem) => { ... } } // used in props