From a40585e3d8e8a1f80af5768b5f2fe1ba75eb04c9 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 12 Nov 2025 07:30:01 +0000 Subject: [PATCH 1/5] Initial plan From 7d84e75a4b47f7c760e00e16763b2b0c72bcc00c Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 12 Nov 2025 07:36:03 +0000 Subject: [PATCH 2/5] Add EditableDiv component with wave:editablediv tag support Co-authored-by: sawka <2722291+sawka@users.noreply.github.com> --- tsunami/frontend/src/element/editablediv.tsx | 63 ++++++++++++++++++++ tsunami/frontend/src/vdom.tsx | 18 ++++++ 2 files changed, 81 insertions(+) create mode 100644 tsunami/frontend/src/element/editablediv.tsx diff --git a/tsunami/frontend/src/element/editablediv.tsx b/tsunami/frontend/src/element/editablediv.tsx new file mode 100644 index 0000000000..ba41aa91b1 --- /dev/null +++ b/tsunami/frontend/src/element/editablediv.tsx @@ -0,0 +1,63 @@ +// Copyright 2025, Command Line Inc. +// SPDX-License-Identifier: Apache-2.0 + +import React, { useEffect, useRef } from "react"; +import { twMerge } from "tailwind-merge"; + +interface EditableDivProps extends Omit, 'onChange'> { + className?: string; + text: string; + onChange: (newText: string) => void; + placeholder?: string; +} + +export function EditableDiv({ className, text, onChange, placeholder, ...otherProps }: EditableDivProps) { + const divRef = useRef(null); + const textRef = useRef(text); + + // Update DOM when text prop changes + useEffect(() => { + if (divRef.current && divRef.current.textContent !== text) { + divRef.current.textContent = text; + textRef.current = text; + } + }, [text]); + + const handleBlur = () => { + const newText = divRef.current?.textContent || ""; + if (newText !== textRef.current) { + textRef.current = newText; + onChange(newText); + } + }; + + const handleKeyDown = (e: React.KeyboardEvent) => { + // Prevent default Enter key behavior (single-line editing) + if (e.key === "Enter") { + e.preventDefault(); + } + + // Call original onKeyDown if provided + if (otherProps.onKeyDown) { + otherProps.onKeyDown(e); + } + }; + + const isEmpty = !text || text.trim() === ""; + const placeholderClass = isEmpty && placeholder ? "empty" : ""; + + return ( +
+ {text} +
+ ); +} diff --git a/tsunami/frontend/src/vdom.tsx b/tsunami/frontend/src/vdom.tsx index 37de4c0f1c..571f986d1d 100644 --- a/tsunami/frontend/src/vdom.tsx +++ b/tsunami/frontend/src/vdom.tsx @@ -9,6 +9,7 @@ import { twMerge } from "tailwind-merge"; import { AlertModal, ConfirmModal } from "@/element/modals"; import { Markdown } from "@/element/markdown"; +import { EditableDiv } from "@/element/editablediv"; import { getTextChildren } from "@/model/model-utils"; import type { TsunamiModel } from "@/model/tsunami-model"; import { RechartsTag } from "@/recharts/recharts"; @@ -30,6 +31,7 @@ type VDomReactTagType = (props: { elem: VDomElem; model: TsunamiModel }) => Reac const WaveTagMap: Record = { "wave:markdown": WaveMarkdown, + "wave:editablediv": WaveEditableDiv, }; const AllowedSimpleTags: { [tagName: string]: boolean } = { @@ -278,6 +280,22 @@ function WaveMarkdown({ elem, model }: { elem: VDomElem; model: TsunamiModel }) ); } +function WaveEditableDiv({ elem, model }: { elem: VDomElem; model: TsunamiModel }) { + const props = useVDom(model, elem); + // Extract EditableDiv specific props + const { text, onChange, placeholder, className, style, ...otherProps } = props; + return ( + + ); +} + function StyleTag({ elem, model }: { elem: VDomElem; model: TsunamiModel }) { const styleText = getTextChildren(elem); if (styleText == null) { From 358172651f2bc850e7b96ad9666ba15217f64a43 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 12 Nov 2025 07:36:56 +0000 Subject: [PATCH 3/5] Add CSS placeholder styling for EditableDiv Co-authored-by: sawka <2722291+sawka@users.noreply.github.com> --- tsunami/frontend/src/tailwind.css | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/tsunami/frontend/src/tailwind.css b/tsunami/frontend/src/tailwind.css index 945398cd53..f93cbf14c7 100644 --- a/tsunami/frontend/src/tailwind.css +++ b/tsunami/frontend/src/tailwind.css @@ -108,3 +108,12 @@ html, body { scrollbar-width: thin; scrollbar-color: rgba(255, 255, 255, 0.15) rgba(0, 0, 0, 0.1); } + +/* EditableDiv placeholder styling */ +[contenteditable][data-placeholder]:empty:before { + content: attr(data-placeholder); + color: var(--color-muted); + pointer-events: none; + position: absolute; +} + From 64d1a9e1c7f2437cdf78bf9a9ce5d254fd07a27f Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 12 Nov 2025 07:38:01 +0000 Subject: [PATCH 4/5] Clean up unnecessary placeholder class logic in EditableDiv Co-authored-by: sawka <2722291+sawka@users.noreply.github.com> --- tsunami/frontend/src/element/editablediv.tsx | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/tsunami/frontend/src/element/editablediv.tsx b/tsunami/frontend/src/element/editablediv.tsx index ba41aa91b1..b1c7ab288b 100644 --- a/tsunami/frontend/src/element/editablediv.tsx +++ b/tsunami/frontend/src/element/editablediv.tsx @@ -43,15 +43,12 @@ export function EditableDiv({ className, text, onChange, placeholder, ...otherPr } }; - const isEmpty = !text || text.trim() === ""; - const placeholderClass = isEmpty && placeholder ? "empty" : ""; - return (
Date: Wed, 12 Nov 2025 17:29:11 +0000 Subject: [PATCH 5/5] Add Enter to submit and Escape to revert in EditableDiv keydown handler Co-authored-by: sawka <2722291+sawka@users.noreply.github.com> --- tsunami/frontend/src/element/editablediv.tsx | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/tsunami/frontend/src/element/editablediv.tsx b/tsunami/frontend/src/element/editablediv.tsx index b1c7ab288b..6a2228d0be 100644 --- a/tsunami/frontend/src/element/editablediv.tsx +++ b/tsunami/frontend/src/element/editablediv.tsx @@ -32,9 +32,17 @@ export function EditableDiv({ className, text, onChange, placeholder, ...otherPr }; const handleKeyDown = (e: React.KeyboardEvent) => { - // Prevent default Enter key behavior (single-line editing) if (e.key === "Enter") { e.preventDefault(); + // Submit the edit - stop editing and fire onChange + divRef.current?.blur(); + } else if (e.key === "Escape") { + e.preventDefault(); + // Revert to original contents and stop editing + if (divRef.current) { + divRef.current.textContent = textRef.current; + divRef.current.blur(); + } } // Call original onKeyDown if provided