Skip to content

Commit 76fcfe9

Browse files
feat/changelog-display-navigation (#2814)
* feat(changelog): enhance version changelog display * feat(devtool): add changelog navigation button * fix(ui): optimize link container width class * feat(desktop): update DevtoolCard with optional maxHeight prop * docs(changelog): update multiple nightly release changelog entries * feat(changelog): improve rendering and scrolling experience * refactor(changelog): improve markdown parsing and changelog rendering * fix(ui): adjust calendar icon size in session metadata * add error handling Co-authored-by: graphite-app[bot] <96075541+graphite-app[bot]@users.noreply.github.com> --------- Co-authored-by: graphite-app[bot] <96075541+graphite-app[bot]@users.noreply.github.com>
1 parent 3fbb967 commit 76fcfe9

37 files changed

+237
-136
lines changed
Lines changed: 198 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,73 @@
11
import { openUrl } from "@tauri-apps/plugin-opener";
2+
import { ExternalLinkIcon, SparklesIcon } from "lucide-react";
23
import {
3-
ExternalLinkIcon,
4-
GitCompareArrowsIcon,
5-
SparklesIcon,
6-
} from "lucide-react";
4+
type RefObject,
5+
useCallback,
6+
useEffect,
7+
useRef,
8+
useState,
9+
} from "react";
10+
import { useResizeObserver } from "usehooks-ts";
11+
12+
import NoteEditor from "@hypr/tiptap/editor";
13+
import { md2json } from "@hypr/tiptap/shared";
14+
import {
15+
Breadcrumb,
16+
BreadcrumbItem,
17+
BreadcrumbList,
18+
BreadcrumbPage,
19+
BreadcrumbSeparator,
20+
} from "@hypr/ui/components/ui/breadcrumb";
21+
import { Button } from "@hypr/ui/components/ui/button";
22+
import { cn } from "@hypr/utils";
723

824
import { type Tab } from "../../../../store/zustand/tabs";
925
import { StandardTabWrapper } from "../index";
1026
import { type TabItem, TabItemBase } from "../shared";
1127

28+
export const changelogFiles = import.meta.glob(
29+
"../../../../../../web/content/changelog/*.mdx",
30+
{ query: "?raw", import: "default" },
31+
);
32+
33+
export function getLatestVersion(): string | null {
34+
const versions = Object.keys(changelogFiles)
35+
.map((k) => {
36+
const match = k.match(/\/([^/]+)\.mdx$/);
37+
return match ? match[1] : null;
38+
})
39+
.filter((v): v is string => v !== null)
40+
.filter((v) => !v.includes("nightly"))
41+
.sort((a, b) => b.localeCompare(a, undefined, { numeric: true }));
42+
43+
return versions[0] || null;
44+
}
45+
46+
function stripFrontmatter(content: string): string {
47+
return content.trim();
48+
}
49+
50+
function stripImageLine(content: string): string {
51+
return content.replace(/^!\[.*?\]\(.*?\)\s*\n*/m, "");
52+
}
53+
54+
function addEmptyParagraphsBeforeHeaders(
55+
json: ReturnType<typeof md2json>,
56+
): ReturnType<typeof md2json> {
57+
if (!json.content) return json;
58+
59+
const newContent: typeof json.content = [];
60+
for (let i = 0; i < json.content.length; i++) {
61+
const node = json.content[i];
62+
if (node.type === "heading" && i > 0) {
63+
newContent.push({ type: "paragraph" });
64+
}
65+
newContent.push(node);
66+
}
67+
68+
return { ...json, content: newContent };
69+
}
70+
1271
export const TabItemChangelog: TabItem<Extract<Tab, { type: "changelog" }>> = ({
1372
tab,
1473
tabIndex,
@@ -39,43 +98,149 @@ export function TabContentChangelog({
3998
}: {
4099
tab: Extract<Tab, { type: "changelog" }>;
41100
}) {
42-
const { previous, current } = tab.state;
101+
const { current } = tab.state;
102+
103+
const { content, loading } = useChangelogContent(current);
104+
const { scrollRef, atStart, atEnd } = useScrollFade<HTMLDivElement>();
43105

44106
return (
45107
<StandardTabWrapper>
46-
<div className="flex flex-1 flex-col items-center justify-center gap-6">
47-
<div className="text-center">
48-
<h1 className="text-2xl font-semibold text-neutral-900">
49-
Updated to v{current}
108+
<div className="flex flex-col h-full">
109+
<div className="pl-2 pr-1 shrink-0">
110+
<ChangelogHeader version={current} />
111+
</div>
112+
113+
<div className="mt-2 px-3 shrink-0">
114+
<h1 className="text-xl font-semibold text-neutral-900">
115+
What's new in {current}?
50116
</h1>
51-
{previous && (
52-
<p className="mt-1 text-sm text-neutral-500">from v{previous}</p>
53-
)}
54117
</div>
55118

56-
<div className="flex flex-col gap-2">
57-
<button
58-
onClick={() => openUrl(`https://hyprnote.com/changelog/${current}`)}
59-
className="flex items-center gap-3 px-4 py-2.5 text-sm text-neutral-600 hover:text-neutral-900 hover:bg-neutral-100 rounded-lg transition-colors"
60-
>
61-
<ExternalLinkIcon className="w-4 h-4" />
62-
View Changelog
63-
</button>
64-
{previous && (
65-
<button
66-
onClick={() =>
67-
openUrl(
68-
`https://github.com/fastrepl/hyprnote/compare/desktop_${previous}...desktop_v${current}`,
69-
)
70-
}
71-
className="flex items-center gap-3 px-4 py-2.5 text-sm text-neutral-600 hover:text-neutral-900 hover:bg-neutral-100 rounded-lg transition-colors"
72-
>
73-
<GitCompareArrowsIcon className="w-4 h-4" />
74-
View GitHub Diff
75-
</button>
76-
)}
119+
<div className="mt-4 flex-1 min-h-0 relative overflow-hidden">
120+
{!atStart && <ScrollFadeOverlay position="top" />}
121+
{!atEnd && <ScrollFadeOverlay position="bottom" />}
122+
<div ref={scrollRef} className="h-full overflow-y-auto px-3">
123+
{loading ? (
124+
<p className="text-neutral-500">Loading...</p>
125+
) : content ? (
126+
<NoteEditor initialContent={content} editable={false} />
127+
) : (
128+
<p className="text-neutral-500">
129+
No changelog available for this version.
130+
</p>
131+
)}
132+
</div>
77133
</div>
78134
</div>
79135
</StandardTabWrapper>
80136
);
81137
}
138+
139+
function ChangelogHeader({ version }: { version: string }) {
140+
return (
141+
<div className="w-full pt-1">
142+
<div className="flex items-center gap-2">
143+
<div className="min-w-0 flex-1">
144+
<Breadcrumb className="ml-1.5 min-w-0">
145+
<BreadcrumbList className="text-neutral-700 text-xs flex-nowrap overflow-hidden gap-0.5">
146+
<BreadcrumbItem className="shrink-0">
147+
<span className="text-neutral-500">Changelog</span>
148+
</BreadcrumbItem>
149+
<BreadcrumbSeparator className="shrink-0" />
150+
<BreadcrumbItem className="overflow-hidden">
151+
<BreadcrumbPage className="truncate">{version}</BreadcrumbPage>
152+
</BreadcrumbItem>
153+
</BreadcrumbList>
154+
</Breadcrumb>
155+
</div>
156+
157+
<div className="flex items-center shrink-0">
158+
<Button
159+
size="sm"
160+
variant="ghost"
161+
className="gap-1.5"
162+
onClick={() => openUrl("https://hyprnote.com/changelog")}
163+
>
164+
<ExternalLinkIcon size={14} className="-mt-0.5" />
165+
<span>See all</span>
166+
</Button>
167+
</div>
168+
</div>
169+
</div>
170+
);
171+
}
172+
173+
function useChangelogContent(version: string) {
174+
const [content, setContent] = useState<ReturnType<typeof md2json> | null>(
175+
null,
176+
);
177+
const [loading, setLoading] = useState(true);
178+
179+
useEffect(() => {
180+
const key = Object.keys(changelogFiles).find((k) =>
181+
k.endsWith(`/${version}.mdx`),
182+
);
183+
184+
if (!key) {
185+
setLoading(false);
186+
return;
187+
}
188+
189+
changelogFiles[key]()
190+
.then((raw) => {
191+
const markdown = stripImageLine(stripFrontmatter(raw as string));
192+
const json = md2json(markdown);
193+
setContent(addEmptyParagraphsBeforeHeaders(json));
194+
setLoading(false);
195+
})
196+
.catch(() => {
197+
setContent(null);
198+
setLoading(false);
199+
});
200+
}, [version]);
201+
202+
return { content, loading };
203+
}
204+
205+
function useScrollFade<T extends HTMLElement>() {
206+
const scrollRef = useRef<T>(null);
207+
const [state, setState] = useState({ atStart: true, atEnd: true });
208+
209+
const update = useCallback(() => {
210+
const el = scrollRef.current;
211+
if (!el) return;
212+
213+
const { scrollTop, scrollHeight, clientHeight } = el;
214+
setState({
215+
atStart: scrollTop <= 1,
216+
atEnd: scrollTop + clientHeight >= scrollHeight - 1,
217+
});
218+
}, []);
219+
220+
useResizeObserver({ ref: scrollRef as RefObject<T>, onResize: update });
221+
222+
useEffect(() => {
223+
const el = scrollRef.current;
224+
if (!el) return;
225+
226+
update();
227+
el.addEventListener("scroll", update);
228+
return () => el.removeEventListener("scroll", update);
229+
}, [update]);
230+
231+
return { scrollRef, ...state };
232+
}
233+
234+
function ScrollFadeOverlay({ position }: { position: "top" | "bottom" }) {
235+
return (
236+
<div
237+
className={cn([
238+
"absolute left-0 w-full h-8 z-20 pointer-events-none",
239+
position === "top" &&
240+
"top-0 bg-gradient-to-b from-white to-transparent",
241+
position === "bottom" &&
242+
"bottom-0 bg-gradient-to-t from-white to-transparent",
243+
])}
244+
/>
245+
);
246+
}

apps/desktop/src/components/main/body/sessions/outer-header/metadata/index.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,7 @@ const TriggerInner = forwardRef<
5151
size="sm"
5252
className={cn([open && "bg-neutral-100"])}
5353
>
54-
<CalendarIcon size={16} className="-mt-0.5" />
54+
<CalendarIcon size={14} className="-mt-0.5" />
5555
{formatRelativeOrAbsolute(createdAt ? new Date(createdAt) : new Date())}
5656
</Button>
5757
);

apps/desktop/src/components/main/sidebar/devtool.tsx

Lines changed: 37 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,10 @@ import {
99
type Store as MainStore,
1010
STORE_ID as STORE_ID_PERSISTED,
1111
} from "../../../store/tinybase/store/main";
12+
import { useTabs } from "../../../store/zustand/tabs";
1213
import { type SeedDefinition, seeds } from "../../devtool/seed/index";
1314
import { useTrialExpiredModal } from "../../devtool/trial-expired-modal";
15+
import { getLatestVersion } from "../body/changelog";
1416

1517
declare global {
1618
interface Window {
@@ -87,24 +89,32 @@ export function DevtoolView() {
8789
function DevtoolCard({
8890
title,
8991
children,
92+
maxHeight,
9093
}: {
9194
title: string;
9295
children: React.ReactNode;
96+
maxHeight?: string;
9397
}) {
9498
return (
9599
<div
96100
className={cn([
97101
"rounded-lg border border-neutral-200 bg-white",
98102
"shadow-sm",
99103
"overflow-hidden",
104+
"shrink-0",
100105
])}
101106
>
102107
<div className="px-2 py-1.5 border-b border-neutral-100 bg-neutral-50">
103108
<h2 className="text-xs font-semibold text-neutral-600 uppercase tracking-wide">
104109
{title}
105110
</h2>
106111
</div>
107-
<div className="p-2">{children}</div>
112+
<div
113+
className="p-2 overflow-y-auto"
114+
style={maxHeight ? { maxHeight } : undefined}
115+
>
116+
{children}
117+
</div>
108118
</div>
109119
);
110120
}
@@ -189,7 +199,7 @@ function CalendarMockCard() {
189199

190200
function SeedCard({ onSeed }: { onSeed: (seed: SeedDefinition) => void }) {
191201
return (
192-
<DevtoolCard title="Seeds">
202+
<DevtoolCard title="Seeds" maxHeight="200px">
193203
<div className="flex flex-col gap-1.5">
194204
{seeds.map((seed) => (
195205
<button
@@ -213,6 +223,8 @@ function SeedCard({ onSeed }: { onSeed: (seed: SeedDefinition) => void }) {
213223
}
214224

215225
function NavigationCard() {
226+
const openNew = useTabs((s) => s.openNew);
227+
216228
const handleShowMain = useCallback(() => {
217229
void windowsCommands.windowShow({ type: "main" });
218230
}, []);
@@ -230,6 +242,16 @@ function NavigationCard() {
230242
void windowsCommands.windowShow({ type: "control" });
231243
}, []);
232244

245+
const handleShowChangelog = useCallback(() => {
246+
const latestVersion = getLatestVersion();
247+
if (latestVersion) {
248+
openNew({
249+
type: "changelog",
250+
state: { current: latestVersion, previous: null },
251+
});
252+
}
253+
}, [openNew]);
254+
233255
return (
234256
<DevtoolCard title="Navigation">
235257
<div className="flex flex-col gap-1.5">
@@ -272,6 +294,19 @@ function NavigationCard() {
272294
>
273295
Control
274296
</button>
297+
<button
298+
type="button"
299+
onClick={handleShowChangelog}
300+
className={cn([
301+
"w-full px-2.5 py-1.5 rounded-md",
302+
"text-xs font-medium text-left",
303+
"border border-neutral-200 text-neutral-700",
304+
"cursor-pointer transition-colors",
305+
"hover:bg-neutral-50 hover:border-neutral-300",
306+
])}
307+
>
308+
Changelog
309+
</button>
275310
</div>
276311
</DevtoolCard>
277312
);
Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1 @@
1-
---
2-
---
3-
41
- Initial release.

apps/web/content/changelog/1.0.0-nightly.10.mdx

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,3 @@
1-
---
2-
---
3-
41
- Add Intel macOS (x86_64) support.
52
- Add Deno-powered extensions runtime with iframe-based isolation and TinyBase sync.
63
- Add calendar extension. (not included yet)

apps/web/content/changelog/1.0.0-nightly.11.mdx

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,3 @@
1-
---
2-
---
3-
41
- Add .deb build for Linux x86_64.
52
- Add network availability monitoring plugin.
63
- Add NetworkContext for network status in desktop app.

apps/web/content/changelog/1.0.0-nightly.12.mdx

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,3 @@
1-
---
2-
---
3-
41
- Add Gladia STT provider with realtime and batch transcription support.
52
- Add OpenAI STT provider with realtime and batch transcription support.
63
- Add prompt, shortcut, template, and extensions tabs in settings.

apps/web/content/changelog/1.0.0-nightly.13.mdx

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,3 @@
1-
---
2-
---
3-
41
- Unify VAD logic with StreamingVad for better audio processing.
52
- Fix batch audio processing bugs in listener2 plugin.
63
- Update onboarding flow with new model selection.

0 commit comments

Comments
 (0)