diff options
| author | David Hill <[email protected]> | 2025-12-21 16:45:49 +0000 |
|---|---|---|
| committer | adamelmore <[email protected]> | 2026-01-26 15:35:09 -0600 |
| commit | a77df3c17482fce94a19554283bdba6f03135f67 (patch) | |
| tree | 23993b7dce6df37eb92349b9e86040a0b60e3a8b /packages | |
| parent | 9d1cf98192fdcfb9a76044c0cb9818565198a769 (diff) | |
| download | opencode-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')
| -rwxr-xr-x | packages/app/public/release/release-example.mp4 | bin | 0 -> 2308074 bytes | |||
| -rw-r--r-- | packages/app/public/release/release-share.png | bin | 0 -> 44076 bytes | |||
| -rw-r--r-- | packages/app/src/components/dialog-release-notes.tsx | 185 | ||||
| -rw-r--r-- | packages/app/src/components/release-notes-handler.tsx | 31 | ||||
| -rw-r--r-- | packages/app/src/index.css | 27 | ||||
| -rw-r--r-- | packages/app/src/lib/release-notes.ts | 53 | ||||
| -rw-r--r-- | packages/app/src/pages/layout.tsx | 7 |
7 files changed, 303 insertions, 0 deletions
diff --git a/packages/app/public/release/release-example.mp4 b/packages/app/public/release/release-example.mp4 Binary files differnew file mode 100755 index 000000000..6cb4dd585 --- /dev/null +++ b/packages/app/public/release/release-example.mp4 diff --git a/packages/app/public/release/release-share.png b/packages/app/public/release/release-share.png Binary files differnew file mode 100644 index 000000000..e4b99d2db --- /dev/null +++ b/packages/app/public/release/release-share.png 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 +} diff --git a/packages/app/src/index.css b/packages/app/src/index.css index 3d7b9db7a..c0c7da858 100644 --- a/packages/app/src/index.css +++ b/packages/app/src/index.css @@ -55,3 +55,30 @@ scrollbar-width: thin !important; scrollbar-color: var(--border-weak-base) transparent !important; } + +/* Wider dialog variant for release notes modal */ +[data-component="dialog"]:has(.dialog-release-notes) { + padding: 20px; + box-sizing: border-box; + + [data-slot="dialog-container"] { + width: min(100%, 720px); + height: min(100%, 400px); + margin-top: -80px; + + [data-slot="dialog-content"] { + min-height: auto; + overflow: hidden; + height: 100%; + border: none; + box-shadow: var(--shadow-lg-border-base); + } + + [data-slot="dialog-body"] { + overflow: hidden; + height: 100%; + display: flex; + flex-direction: row; + } + } +} diff --git a/packages/app/src/lib/release-notes.ts b/packages/app/src/lib/release-notes.ts new file mode 100644 index 000000000..a28843acd --- /dev/null +++ b/packages/app/src/lib/release-notes.ts @@ -0,0 +1,53 @@ +import { CURRENT_RELEASE } from "@/components/dialog-release-notes" + +const STORAGE_KEY = "opencode:last-seen-version" + +// ============================================================================ +// DEV MODE: Set this to true to always show the release notes modal on startup +// Set to false for production behavior (only shows after updates) +// ============================================================================ +const DEV_ALWAYS_SHOW_RELEASE_NOTES = true + +/** + * Check if release notes should be shown + * Returns true if: + * - DEV_ALWAYS_SHOW_RELEASE_NOTES is true (for development) + * - OR the current version is newer than the last seen version + */ +export function shouldShowReleaseNotes(): boolean { + if (DEV_ALWAYS_SHOW_RELEASE_NOTES) { + console.log("[ReleaseNotes] DEV mode: always showing release notes") + return true + } + + const lastSeen = localStorage.getItem(STORAGE_KEY) + if (!lastSeen) { + // First time user - show release notes + return true + } + + // Compare versions - show if current is newer + return CURRENT_RELEASE.version !== lastSeen +} + +/** + * Mark the current release notes as seen + * Call this when the user closes the release notes modal + */ +export function markReleaseNotesSeen(): void { + localStorage.setItem(STORAGE_KEY, CURRENT_RELEASE.version) +} + +/** + * Get the current version + */ +export function getCurrentVersion(): string { + return CURRENT_RELEASE.version +} + +/** + * Reset the seen status (useful for testing) + */ +export function resetReleaseNotesSeen(): void { + localStorage.removeItem(STORAGE_KEY) +} diff --git a/packages/app/src/pages/layout.tsx b/packages/app/src/pages/layout.tsx index b13cb1ac3..601a24067 100644 --- a/packages/app/src/pages/layout.tsx +++ b/packages/app/src/pages/layout.tsx @@ -68,6 +68,7 @@ import { ConstrainDragXAxis } from "@/utils/solid-dnd" import { navStart } from "@/utils/perf" import { DialogSelectDirectory } from "@/components/dialog-select-directory" import { DialogEditProject } from "@/components/dialog-edit-project" +import { ReleaseNotesHandler } from "@/components/release-notes-handler" import { Titlebar } from "@/components/titlebar" import { useServer } from "@/context/server" import { useLanguage, type Locale } from "@/context/language" @@ -1444,6 +1445,11 @@ export default function Layout(props: ParentProps) { ) createEffect(() => { + const sidebarWidth = layout.sidebar.opened() ? layout.sidebar.width() : 48 + document.documentElement.style.setProperty("--dialog-left-margin", `${sidebarWidth}px`) + }) + + createEffect(() => { const project = currentProject() if (!project) return @@ -2791,6 +2797,7 @@ export default function Layout(props: ParentProps) { </main> </div> <Toast.Region /> + <ReleaseNotesHandler /> </div> ) } |
