summaryrefslogtreecommitdiffhomepage
path: root/packages/app/src/components
diff options
context:
space:
mode:
authorDavid Hill <[email protected]>2025-12-21 16:45:49 +0000
committeradamelmore <[email protected]>2026-01-26 15:35:09 -0600
commita77df3c17482fce94a19554283bdba6f03135f67 (patch)
tree23993b7dce6df37eb92349b9e86040a0b60e3a8b /packages/app/src/components
parent9d1cf98192fdcfb9a76044c0cb9818565198a769 (diff)
downloadopencode-a77df3c17482fce94a19554283bdba6f03135f67.tar.gz
opencode-a77df3c17482fce94a19554283bdba6f03135f67.zip
wip: new release modal
- highlight key updates or new features - needs some transition love - all copy including text and video placeholder
Diffstat (limited to 'packages/app/src/components')
-rw-r--r--packages/app/src/components/dialog-release-notes.tsx185
-rw-r--r--packages/app/src/components/release-notes-handler.tsx31
2 files changed, 216 insertions, 0 deletions
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 (
+ <Dialog class="dialog-release-notes">
+ {/* Hidden element to capture initial focus and handle escape */}
+ <div ref={focusTrap} tabindex="0" class="absolute opacity-0 pointer-events-none" />
+ {/* Left side - Text content */}
+ <div class="flex flex-col flex-1 min-w-0 p-8">
+ {/* Top section - feature content (fixed position from top) */}
+ <div class="flex flex-col gap-2 pt-22">
+ <div class="flex items-center gap-2">
+ <h1 class="text-16-medium text-text-strong">{feature().title}</h1>
+ {feature().tag && (
+ <span
+ class="text-12-medium text-text-weak px-1.5 py-0.5 bg-surface-base rounded-sm border border-border-weak-base"
+ style={{ "border-width": "0.5px" }}
+ >
+ {feature().tag}
+ </span>
+ )}
+ </div>
+ <p class="text-14-regular text-text-base">{feature().description}</p>
+ </div>
+
+ {/* Spacer to push buttons to bottom */}
+ <div class="flex-1" />
+
+ {/* Bottom section - buttons and indicators (fixed position) */}
+ <div class="flex flex-col gap-12">
+ <div class="flex items-center gap-3">
+ {isLast() ? (
+ <Button variant="primary" size="large" onClick={handleClose}>
+ Get started
+ </Button>
+ ) : (
+ <Button variant="secondary" size="large" onClick={handleNext}>
+ Next
+ </Button>
+ )}
+ </div>
+
+ {total > 1 && (
+ <div class="flex items-center gap-1.5 -my-2.5">
+ {release.features.map((_, i) => (
+ <button
+ type="button"
+ class="w-8 h-6 flex items-center cursor-pointer bg-transparent border-none p-0"
+ onClick={() => setIndex(i)}
+ >
+ <div
+ class="w-full h-0.5 rounded-[1px] transition-colors"
+ classList={{
+ "bg-icon-strong-base": i === index(),
+ "bg-icon-weak-base": i !== index(),
+ }}
+ />
+ </button>
+ ))}
+ </div>
+ )}
+ </div>
+ </div>
+
+ {/* Right side - Media content (edge to edge) */}
+ {feature().media && (
+ <div class="flex-1 min-w-0 bg-surface-base overflow-hidden rounded-r-xl">
+ {feature().media!.type === "image" ? (
+ <img
+ src={feature().media!.src}
+ alt={feature().media!.alt ?? "Release preview"}
+ class="w-full h-full object-cover"
+ />
+ ) : (
+ <video src={feature().media!.src} autoplay loop muted playsinline class="w-full h-full object-cover" />
+ )}
+ </div>
+ )}
+ </Dialog>
+ )
+}
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(
+ () => <DialogReleaseNotes />,
+ () => markReleaseNotesSeen(),
+ )
+ }
+ }, 500)
+ })
+
+ return null
+}