From a77df3c17482fce94a19554283bdba6f03135f67 Mon Sep 17 00:00:00 2001 From: David Hill Date: Sun, 21 Dec 2025 16:45:49 +0000 Subject: wip: new release modal - highlight key updates or new features - needs some transition love - all copy including text and video placeholder --- .../app/src/components/dialog-release-notes.tsx | 185 +++++++++++++++++++++ .../app/src/components/release-notes-handler.tsx | 31 ++++ 2 files changed, 216 insertions(+) create mode 100644 packages/app/src/components/dialog-release-notes.tsx create mode 100644 packages/app/src/components/release-notes-handler.tsx (limited to 'packages/app/src/components') diff --git a/packages/app/src/components/dialog-release-notes.tsx b/packages/app/src/components/dialog-release-notes.tsx new file mode 100644 index 000000000..c5a1e15c1 --- /dev/null +++ b/packages/app/src/components/dialog-release-notes.tsx @@ -0,0 +1,185 @@ +import { createSignal, createEffect, onMount, onCleanup } from "solid-js" +import { Dialog } from "@opencode-ai/ui/dialog" +import { Button } from "@opencode-ai/ui/button" +import { useDialog } from "@opencode-ai/ui/context/dialog" +import { markReleaseNotesSeen } from "@/lib/release-notes" + +export interface ReleaseFeature { + title: string + description: string + tag?: string + media?: { + type: "image" | "video" + src: string + alt?: string + } +} + +export interface ReleaseNote { + version: string + features: ReleaseFeature[] +} + +// Current release notes - update this with each release +export const CURRENT_RELEASE: ReleaseNote = { + version: "1.0.0", + features: [ + { + title: "Cleaner tab experience", + description: "Chat is now fixed to the side of your tabs, and review is now available as a dedicated tab. ", + tag: "New", + media: { + type: "video", + src: "/release/release-example.mp4", + alt: "Cleaner tab experience", + }, + }, + { + title: "Share with control", + description: "Keep your sessions private by default, or publish them to the web with a shareable URL.", + tag: "New", + media: { + type: "image", + src: "/release/release-share.png", + alt: "Share with control", + }, + }, + { + title: "Improved attachment management", + description: "Upload and manage attachments more easily, to help build and maintain context.", + tag: "New", + media: { + type: "video", + src: "/release/release-example.mp4", + alt: "Improved attachment management", + }, + }, + ], +} + +export function DialogReleaseNotes(props: { release?: ReleaseNote }) { + const dialog = useDialog() + const release = props.release ?? CURRENT_RELEASE + const [index, setIndex] = createSignal(0) + + const feature = () => release.features[index()] + const total = release.features.length + const isFirst = () => index() === 0 + const isLast = () => index() === total - 1 + + function handleNext() { + if (!isLast()) setIndex(index() + 1) + } + + function handleBack() { + if (!isFirst()) setIndex(index() - 1) + } + + function handleClose() { + markReleaseNotesSeen() + dialog.close() + } + + let focusTrap: HTMLDivElement | undefined + + function handleKeyDown(e: KeyboardEvent) { + if (e.key === "ArrowLeft" && !isFirst()) { + e.preventDefault() + setIndex(index() - 1) + } + if (e.key === "ArrowRight" && !isLast()) { + e.preventDefault() + setIndex(index() + 1) + } + } + + onMount(() => { + focusTrap?.focus() + document.addEventListener("keydown", handleKeyDown) + onCleanup(() => document.removeEventListener("keydown", handleKeyDown)) + }) + + // Refocus the trap when index changes to ensure escape always works + createEffect(() => { + index() // track index + focusTrap?.focus() + }) + + return ( + + {/* Hidden element to capture initial focus and handle escape */} +
+ {/* Left side - Text content */} +
+ {/* Top section - feature content (fixed position from top) */} +
+
+

{feature().title}

+ {feature().tag && ( + + {feature().tag} + + )} +
+

{feature().description}

+
+ + {/* Spacer to push buttons to bottom */} +
+ + {/* Bottom section - buttons and indicators (fixed position) */} +
+
+ {isLast() ? ( + + ) : ( + + )} +
+ + {total > 1 && ( +
+ {release.features.map((_, i) => ( + + ))} +
+ )} +
+
+ + {/* Right side - Media content (edge to edge) */} + {feature().media && ( +
+ {feature().media!.type === "image" ? ( + {feature().media!.alt + ) : ( +
+ )} +
+ ) +} diff --git a/packages/app/src/components/release-notes-handler.tsx b/packages/app/src/components/release-notes-handler.tsx new file mode 100644 index 000000000..45237b577 --- /dev/null +++ b/packages/app/src/components/release-notes-handler.tsx @@ -0,0 +1,31 @@ +import { onMount } from "solid-js" +import { useDialog } from "@opencode-ai/ui/context/dialog" +import { DialogReleaseNotes } from "./dialog-release-notes" +import { shouldShowReleaseNotes, markReleaseNotesSeen } from "@/lib/release-notes" + +/** + * Component that handles showing release notes modal on app startup. + * Shows the modal if: + * - DEV_ALWAYS_SHOW_RELEASE_NOTES is true in lib/release-notes.ts + * - OR the user hasn't seen the current version's release notes yet + * + * To disable the dev mode behavior, set DEV_ALWAYS_SHOW_RELEASE_NOTES to false + * in packages/app/src/lib/release-notes.ts + */ +export function ReleaseNotesHandler() { + const dialog = useDialog() + + onMount(() => { + // Small delay to ensure app is fully loaded before showing modal + setTimeout(() => { + if (shouldShowReleaseNotes()) { + dialog.show( + () => , + () => markReleaseNotesSeen(), + ) + } + }, 500) + }) + + return null +} -- cgit v1.2.3