Skip to content

Commit 60780bb

Browse files
authored
Merge pull request #637 from dripnex/develop
chore(release): promote 0.23.0
2 parents 1e251f6 + 87db74c commit 60780bb

105 files changed

Lines changed: 7791 additions & 197 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

apps/desktop/e2e/authgate.spec.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ test.describe('AuthGate is the first window', () => {
1313
await expect(page.getByRole('button', { name: 'Email me a link' })).toBeVisible();
1414
await expect(page.locator('canvas')).toHaveCount(1);
1515

16-
await expect(page.getByRole('button', { name: 'Create Your First Note' })).toHaveCount(0);
16+
await expect(page.getByRole('button', { name: 'Create a note' })).toHaveCount(0);
1717
await expect(page.getByRole('button', { name: /continue locally/i })).toHaveCount(0);
1818

1919
const [settings] = await Promise.all([

apps/desktop/e2e/fixtures.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -84,7 +84,7 @@ export async function launchApp({
8484
* New notes start as `# Untitled\\n\\n` (see useNoteActions).
8585
*/
8686
export async function openFirstNote(window: Page): Promise<Locator> {
87-
const create = window.getByRole('button', { name: 'Create Your First Note' });
87+
const create = window.getByRole('button', { name: 'Create a note' });
8888
await create.waitFor({ state: 'visible', timeout: 15_000 });
8989
await create.click();
9090
const content = window.locator('.cm-content');

apps/desktop/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,7 @@
5151
"cross-fetch": "^4.1.0",
5252
"diff": "^9.0.0",
5353
"electron-updater": "^6.8.9",
54+
"gsap": "^3.15.0",
5455
"isomorphic-git": "^1.38.4",
5556
"katex": "^0.18.4",
5657
"lucide": "^1.17.0",

apps/desktop/src/renderer/App.tsx

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { useState, useEffect, useCallback, useMemo, useRef } from 'react';
1+
import { useState, useEffect, useLayoutEffect, useCallback, useMemo, useRef } from 'react';
22
import { useCssVariables, usePluginStyles, useThemeOverrides } from '@dripnex/plugin-api';
33
import { scanMarkdown } from '@dripnex/markdown';
44
import type { NoteSnapshot } from '../preload/index';
@@ -61,10 +61,25 @@ import { useMcpLocalPath } from './hooks/useMcpLocalPath';
6161
import type { PaletteMode } from './utils/paletteQuery';
6262
import { useEditorBufferStore, selectContentForNote } from './stores/editorBufferStore';
6363
import { useHeadingJumpStore } from './stores/headingJumpStore';
64+
import { initGsapRuntime, playMotion, setPerformanceLow } from './motion/gsapRuntime';
65+
import { shouldPlaySidebarIn } from './motion/sidebarIn';
66+
import { usePerformanceStore } from './stores/performanceStore';
6467

6568
function NotesApp() {
6669
usePerformanceMode();
6770
useOfficialThemes();
71+
72+
useEffect(() => {
73+
const stop = initGsapRuntime();
74+
setPerformanceLow(usePerformanceStore.getState().mode === 'low');
75+
const unsub = usePerformanceStore.subscribe(state => {
76+
setPerformanceLow(state.mode === 'low');
77+
});
78+
return () => {
79+
stop();
80+
unsub();
81+
};
82+
}, []);
6883
useThemeOverrides();
6984
useAppearanceSettings();
7085
useCssVariables();
@@ -131,6 +146,15 @@ function SignedInApp({
131146

132147
const hideSidebar = sidebarCollapsed || distractionFree;
133148
const hideNoteList = distractionFree;
149+
const sidebarRef = useRef<HTMLElement>(null);
150+
const sidebarWasHiddenRef = useRef(hideSidebar);
151+
152+
useLayoutEffect(() => {
153+
if (shouldPlaySidebarIn(sidebarWasHiddenRef.current, hideSidebar)) {
154+
playMotion('sidebar-in', sidebarRef.current);
155+
}
156+
sidebarWasHiddenRef.current = hideSidebar;
157+
}, [hideSidebar]);
134158

135159
useEffect(() => {
136160
const setVisibility = window.dripnex.windows.setButtonVisibility;
@@ -434,6 +458,7 @@ function SignedInApp({
434458
<UpdateBanner />
435459
<div className="app__layout">
436460
<aside
461+
ref={sidebarRef}
437462
className="app__sidebar"
438463
data-collapsed={hideSidebar ? 'true' : 'false'}
439464
style={{ width: sidebarWidth }}

apps/desktop/src/renderer/components/CommandPalette.module.css

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,6 @@
4444
border-radius: 12px;
4545
box-shadow: var(--shadow-xl);
4646
overflow: hidden;
47-
animation: command-palette-slide-in 150ms ease-out;
4847
}
4948

5049
:global(:root[data-color-scheme='light']) .command-palette {

apps/desktop/src/renderer/components/CommandPalette.tsx

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { useState, useEffect, useRef, useMemo, useCallback } from 'react';
1+
import { useState, useEffect, useLayoutEffect, useRef, useMemo, useCallback } from 'react';
22
import { createPortal } from 'react-dom';
33
import {
44
Bold,
@@ -55,6 +55,7 @@ import {
5555
parsePaletteQuery,
5656
type PaletteMode,
5757
} from '../utils/paletteQuery';
58+
import { playMotion } from '../motion/gsapRuntime';
5859
import { cssm } from '../lib/cssm';
5960
import styles from './CommandPalette.module.css';
6061

@@ -142,6 +143,7 @@ export function CommandPalette({
142143
const [selectedIndex, setSelectedIndex] = useState(0);
143144
const inputRef = useRef<HTMLInputElement>(null);
144145
const listRef = useRef<HTMLDivElement>(null);
146+
const paletteRef = useRef<HTMLDivElement>(null);
145147
const previousFocusRef = useRef<HTMLElement | null>(null);
146148

147149
const parsed = useMemo(() => parsePaletteQuery(query, mode), [query, mode]);
@@ -225,6 +227,11 @@ export function CommandPalette({
225227
}
226228
}, [isOpen, mode]);
227229

230+
useLayoutEffect(() => {
231+
if (!isOpen) return;
232+
playMotion('palette-in', paletteRef.current);
233+
}, [isOpen]);
234+
228235
useEffect(() => {
229236
setSelectedIndex(0);
230237
}, [query]);
@@ -371,6 +378,7 @@ export function CommandPalette({
371378
aria-modal="true"
372379
>
373380
<div
381+
ref={paletteRef}
374382
className={sc('command-palette')}
375383
onClick={e => e.stopPropagation()}
376384
style={{

apps/desktop/src/renderer/components/GraphView.module.css

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -174,6 +174,39 @@
174174
color: var(--danger);
175175
}
176176

177+
.filterEmpty {
178+
position: absolute;
179+
inset: 0;
180+
z-index: 1;
181+
display: flex;
182+
flex: 1;
183+
flex-direction: column;
184+
align-items: center;
185+
justify-content: center;
186+
gap: var(--space-2);
187+
padding: var(--space-8) var(--space-6);
188+
pointer-events: none;
189+
text-align: center;
190+
}
191+
192+
.filterEmptyIcon {
193+
color: var(--text-faint);
194+
}
195+
196+
.filterEmptyTitle {
197+
margin: 0;
198+
font-size: var(--text-lg);
199+
font-weight: 500;
200+
letter-spacing: var(--tracking-tight);
201+
color: var(--text-primary);
202+
}
203+
204+
.filterEmptyHint {
205+
margin: 0;
206+
font-size: var(--text-sm);
207+
color: var(--text-muted);
208+
}
209+
177210
/* ── Inspector ──────────────────────────────────────────────────────────── */
178211

179212
.inspector {

apps/desktop/src/renderer/components/GraphView.tsx

Lines changed: 23 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
import { useCallback, useEffect, useMemo, useRef, useState, type ReactNode } from 'react';
77
import { useQuery, useQueryClient } from '@tanstack/react-query';
88
import ForceGraph2D from 'react-force-graph-2d';
9-
import { X } from 'lucide';
9+
import { Search, X } from 'lucide';
1010
import { Icon } from '../ui/icons/Icon';
1111
import { useGraphData } from '../hooks/useLinks';
1212
import { noteKeys } from '../hooks/useNotes';
@@ -21,6 +21,13 @@ import {
2121
type NoteKind,
2222
} from '../lib/knowledge';
2323
import { GraphInspector, type GraphActivityEvent, type GraphRelation } from './GraphInspector';
24+
import {
25+
GRAPH_EMPTY_HINT,
26+
GRAPH_EMPTY_TITLE,
27+
GRAPH_ERROR_HINT,
28+
GRAPH_ERROR_TITLE,
29+
graphFilterEmpty,
30+
} from './graphCopy';
2431
import styles from './GraphView.module.css';
2532

2633
interface GraphViewProps {
@@ -449,21 +456,23 @@ export function GraphView({ selectedNoteId, onOpenNote, onAskNote, onClose }: Gr
449456
if (error) {
450457
return stage(
451458
<div className={`${styles.center} ${styles.centerError}`}>
452-
Failed to load graph
453-
<p className={styles.centerHint}>Wikilinks will appear here once notes are indexed.</p>
459+
{GRAPH_ERROR_TITLE}
460+
<p className={styles.centerHint}>{GRAPH_ERROR_HINT}</p>
454461
</div>
455462
);
456463
}
457464

458465
if (graphData.nodes.length === 0) {
459466
return stage(
460467
<div className={styles.center}>
461-
No notes to map
462-
<p className={styles.centerHint}>Create notes and connect them with [[wikilinks]].</p>
468+
{GRAPH_EMPTY_TITLE}
469+
<p className={styles.centerHint}>{GRAPH_EMPTY_HINT}</p>
463470
</div>
464471
);
465472
}
466473

474+
const filterEmpty = graphFilterEmpty(query, matchIds ? matchIds.size : null);
475+
467476
return (
468477
<div className={styles.graph}>
469478
<div className={styles.stage}>
@@ -501,6 +510,15 @@ export function GraphView({ selectedNoteId, onOpenNote, onAskNote, onClose }: Gr
501510
</div>
502511

503512
<div ref={setCanvasHost} className={styles.canvasHost}>
513+
{filterEmpty ? (
514+
<div className={styles.filterEmpty} role="status" aria-live="polite">
515+
<span className={styles.filterEmptyIcon} aria-hidden="true">
516+
<Icon icon={Search} size={28} />
517+
</span>
518+
<p className={styles.filterEmptyTitle}>{filterEmpty.title}</p>
519+
<p className={styles.filterEmptyHint}>{filterEmpty.hint}</p>
520+
</div>
521+
) : null}
504522
{canvasSize.width > 0 && canvasSize.height > 0 ? (
505523
<ForceGraph2D
506524
ref={graphRef}

apps/desktop/src/renderer/components/NoteList.module.css

Lines changed: 2 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -198,21 +198,6 @@
198198
padding: 0;
199199
}
200200

201-
/* ============================================================================
202-
Note List Item Enter Animation
203-
============================================================================ */
204-
205-
@keyframes fade-slide-in {
206-
from {
207-
opacity: 0;
208-
transform: translateY(-8px);
209-
}
210-
to {
211-
opacity: 1;
212-
transform: translateY(0);
213-
}
214-
}
215-
216201
/* ============================================================================
217202
Note List Item
218203
============================================================================ */
@@ -223,14 +208,6 @@
223208
cursor: pointer;
224209
position: relative;
225210
transition: background var(--transition-fast);
226-
animation: fade-slide-in 150ms ease both;
227-
animation-delay: calc(var(--item-index, 0) * 20ms);
228-
}
229-
230-
@media (prefers-reduced-motion: reduce) {
231-
.note-list-item {
232-
animation: none;
233-
}
234211
}
235212

236213
.note-list-item:hover {
@@ -414,7 +391,8 @@
414391
============================================================================ */
415392

416393
@keyframes skeleton-pulse {
417-
0%, 100% {
394+
0%,
395+
100% {
418396
opacity: 0.4;
419397
}
420398
50% {

apps/desktop/src/renderer/components/NoteList.tsx

Lines changed: 33 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { useState, useCallback, useEffect, useRef } from 'react';
1+
import { useState, useCallback, useEffect, useLayoutEffect, useRef } from 'react';
22
import {
33
Archive,
44
Search,
@@ -29,6 +29,8 @@ import { dispatchCommand } from '../hooks/useCommandRegistry';
2929
import { IconButton } from '../ui/primitives';
3030
import { noteListNavDirection } from '../utils/noteListKeys';
3131
import { modAccel } from '../utils/modAccel';
32+
import { playListEnter, playMotion } from '../motion/gsapRuntime';
33+
import { elementsForNoteIds, planListEnter } from '../motion/listEnter';
3234
import type { QuickFilterType } from './sidebar';
3335
import { NoteListContextMenu } from './NoteListContextMenu';
3436
import { NotebookPicker } from './NotebookPicker';
@@ -178,12 +180,41 @@ export function NoteList({
178180
const { data: notebooks = [] } = useNotebookList();
179181
const { data: notebook } = useNotebook(selectedNotebookId);
180182
const listItemsRef = useRef<HTMLUListElement | null>(null);
183+
const seenNoteIdsRef = useRef(new Set<string>());
184+
const prevSelectedIdRef = useRef<string | null>(null);
181185

182186
useEffect(() => {
183187
const selected = listItemsRef.current?.querySelector('[aria-selected="true"]');
184188
selected?.scrollIntoView({ block: 'nearest' });
185189
}, [selectedId]);
186190

191+
const noteIdsKey = notes.map(n => n.id).join('\0');
192+
193+
useLayoutEffect(() => {
194+
const noteIds = noteIdsKey ? noteIdsKey.split('\0') : [];
195+
if (!listItemsRef.current) {
196+
if (noteIds.length === 0) seenNoteIdsRef.current = new Set();
197+
return;
198+
}
199+
200+
const plan = planListEnter({
201+
noteIds,
202+
seenIds: seenNoteIdsRef.current,
203+
});
204+
seenNoteIdsRef.current = new Set(noteIds);
205+
206+
if (plan.mode !== 'none') {
207+
playListEnter(elementsForNoteIds(plan.ids));
208+
prevSelectedIdRef.current = selectedId;
209+
return;
210+
}
211+
212+
if (selectedId && selectedId !== prevSelectedIdRef.current) {
213+
playMotion('list-select', document.getElementById(`note-${selectedId}`));
214+
}
215+
prevSelectedIdRef.current = selectedId;
216+
}, [noteIdsKey, selectedId, isLoading]);
217+
187218
useEffect(() => {
188219
const onKeyDown = (event: KeyboardEvent) => {
189220
const direction = noteListNavDirection(event);
@@ -349,11 +380,10 @@ export function NoteList({
349380
aria-label="Notes"
350381
aria-activedescendant={selectedId ? `note-${selectedId}` : undefined}
351382
>
352-
{notes.map((note, index) => (
383+
{notes.map(note => (
353384
<NoteListItem
354385
key={note.id}
355386
note={note}
356-
index={index}
357387
isSelected={note.id === selectedId}
358388
onSelect={onSelect}
359389
onTagClick={onTagClick}
@@ -412,7 +442,6 @@ export function NoteList({
412442

413443
interface NoteListItemProps {
414444
note: NoteWithExcerpt;
415-
index: number;
416445
isSelected: boolean;
417446
onSelect: (id: string) => void;
418447
onTagClick: (tag: string) => void;
@@ -421,7 +450,6 @@ interface NoteListItemProps {
421450

422451
function NoteListItem({
423452
note,
424-
index,
425453
isSelected,
426454
onSelect,
427455
onTagClick,
@@ -453,7 +481,6 @@ function NoteListItem({
453481
role="option"
454482
aria-selected={isSelected}
455483
className={sc('note-list-item', isSelected && 'selected')}
456-
style={{ '--item-index': Math.min(index, 10) } as React.CSSProperties}
457484
onClick={() => onSelect(note.id)}
458485
onContextMenu={e => onContextMenu(e, note)}
459486
onKeyDown={e => {

0 commit comments

Comments
 (0)