Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
64 commits
Select commit Hold shift + click to select a range
4a8dc5b
feat(fuji): add pinned and soft-delete fields to entries schema
braden-w Apr 8, 2026
496e13e
refactor(fuji): extract state into dedicated modules
braden-w Apr 8, 2026
4d1bcdf
feat(fuji): add command palette and enhanced search
braden-w Apr 8, 2026
e0e8191
feat(fuji): show search results in sidebar when query is active
braden-w Apr 8, 2026
c571ab0
feat(fuji): add status bar with word count and richer editor schema
braden-w Apr 8, 2026
7f62a86
feat(fuji): add persisted sort preference via workspace KV
braden-w Apr 8, 2026
4521085
chore(fuji): remove dead code and fix stale JSDoc
braden-w Apr 8, 2026
ad9a4c6
feat(fuji): export createFujiWorkspace factory for external consumption
braden-w Apr 8, 2026
0e9abf2
refactor(fuji): move entry creation into workspace .withActions
braden-w Apr 8, 2026
1ae6cf5
refactor(fuji): add entries.create action and use workspace factory
braden-w Apr 8, 2026
253d37e
feat(fuji): wire sortBy KV to table and timeline sorting
braden-w Apr 8, 2026
469fdaa
refactor(fuji): wire clearFilters to sidebar All Entries button
braden-w Apr 8, 2026
fa7a02b
fix(fuji): add typebox dependency for workspace actions
braden-w Apr 8, 2026
a2ac3ce
chore(fuji): remove duplicate import, tighten StatusBar prop, fix JSDoc
braden-w Apr 8, 2026
3b4135e
feat(fuji): add app header, global status bar, resizable sidebar, and…
braden-w Apr 8, 2026
d0a67d2
feat(ui): add natural language date input component
braden-w Apr 8, 2026
0fc84b6
fix(fuji): drop SidebarProvider, use collapsible=none for Resizable c…
braden-w Apr 8, 2026
2fbc779
fix(fuji): correct _v type (number not string) and move GithubIcon to…
braden-w Apr 9, 2026
cb25229
refactor(fuji): remove dead code from audit
braden-w Apr 9, 2026
2a784bc
refactor(ui): replace custom GithubIcon with shadcn-svelte-extras Git…
braden-w Apr 9, 2026
433150b
feat(workspace): add password-based key derivation helpers for vault …
braden-w Apr 9, 2026
25a2d4b
feat(fuji): wire NLP date editor into entry status bar
braden-w Apr 9, 2026
26bc300
refactor(ui): remove dead NaturalLanguageDateInput and parsing pipeline
braden-w Apr 9, 2026
673f431
refactor(fuji): extract shared parseDateTime and matchesEntrySearch h…
braden-w Apr 9, 2026
9545b3a
refactor(fuji): remove dead entriesState.get() and updateEntry() wrap…
braden-w Apr 9, 2026
486d533
refactor(fuji): delete barrel files, inline GlobalStatusBar
braden-w Apr 9, 2026
177d01b
refactor(fuji): move createEntry selection side-effect to +page.svelte
braden-w Apr 9, 2026
b3474e9
refactor(fuji): sidebar imports viewState directly, rename to Entries…
braden-w Apr 9, 2026
062c9f7
refactor(fuji): rename filterByType/filterByTag to setTypeFilter/setT…
braden-w Apr 9, 2026
86426e8
refactor(ui): fix stale import, rename parse-date, extract timezone-c…
braden-w Apr 9, 2026
96d467d
refactor(ui): rename NLPDateInput to NaturalLanguageDateInput
braden-w Apr 9, 2026
8cb926f
fix(fuji): preserve tag/type casing in TagInput
braden-w Apr 9, 2026
a207c9c
refactor(fuji): flatten entries-state to module-level exports
braden-w Apr 9, 2026
23318da
refactor(fuji): flatten single-file folders into $lib/ root
braden-w Apr 9, 2026
9055b9c
fix(workspace): preserve sync/async distinction in action return types
braden-w Apr 10, 2026
9aebe2b
fix: replace unicode escape sequences with actual characters in Svelt…
braden-w Apr 10, 2026
be0208b
fix(fuji): remove duplicate cmd+k handler that double-toggled command…
braden-w Apr 10, 2026
528d75e
refactor(workspace): consolidate DateTimeString into companion object
braden-w Apr 10, 2026
90bedc8
refactor(fuji): delete dates.ts, use DateTimeString.toDate instead
braden-w Apr 10, 2026
2450011
refactor: migrate dateTimeStringNow() to DateTimeString.now()
braden-w Apr 10, 2026
ce0a978
docs(svelte-skill): consolidate references into single source of truth
braden-w Apr 10, 2026
269c1d9
chore(fuji): remove broken exports and unused chrono-node dep
braden-w Apr 10, 2026
6c392a5
refactor(fuji): wrap entries state in factory, revert activeEntries t…
braden-w Apr 10, 2026
c4244f1
fix(whispering): restore indentation in MigrationDialog Field.Descrip…
braden-w Apr 10, 2026
8d70da4
feat(fuji): add user-definable date field, move system timestamps to …
braden-w Apr 10, 2026
161a51a
chore: fix lingering imports
braden-w Apr 10, 2026
256d7a5
refactor: remove dead methods and fix formatting
braden-w Apr 10, 2026
7c1ea4b
fix: export materializer
braden-w Apr 10, 2026
c05797f
refactor(fuji): inline StatusBar, fix metadata layout, add date to ma…
braden-w Apr 10, 2026
7dac9b9
fix(cli): match non-empty array type for applyEncryptionKeys parameter
braden-w Apr 10, 2026
7fd49e0
refactor(fuji): align sortBy KV values to column names, add date to l…
braden-w Apr 10, 2026
5c6cf98
refactor(fuji): rename sortBy values to match column names, add date …
braden-w Apr 10, 2026
3c97cdd
refactor(fuji): flatten workspace/ into single workspace.ts
braden-w Apr 10, 2026
d8ee951
refactor(fuji): inline auth.ts into client.ts
braden-w Apr 10, 2026
2761944
refactor(fuji): inline search predicate, rename entries to entriesState
braden-w Apr 10, 2026
1b96994
refactor(fuji): remove prop drilling from EntriesTable and EntryTimeline
braden-w Apr 10, 2026
c1826c2
feat(fuji): add sign-in button with sync status popover
braden-w Apr 10, 2026
141b1fe
feat(fuji): add entries.update action that auto-bumps updatedAt
braden-w Apr 10, 2026
249a477
fix(fuji): add matchesEntrySearch and fix entriesState export name
braden-w Apr 10, 2026
0096636
fix(fuji): use Type.Unsafe<DateTimeString> for branded date input
braden-w Apr 10, 2026
1455512
refactor(fuji): deduplicate createEntry, tighten entriesState API
braden-w Apr 10, 2026
809fed3
refactor(fuji): drop onCreateEntry prop from AppHeader
braden-w Apr 10, 2026
b412daa
refactor(fuji): drop onUpdate/onBack props from EntryEditor
braden-w Apr 10, 2026
7a51818
Merge remote-tracking branch 'origin/main' into opencode/misty-rocket
braden-w Apr 11, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 10 additions & 6 deletions apps/fuji/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,8 @@
"version": "0.0.1",
"type": "module",
"exports": {
"./workspace": "./src/lib/workspace/index.ts"
"./workspace": "./src/lib/workspace/workspace.ts",
"./materializer": "./src/lib/materializer.ts"
},
"scripts": {
"dev": "bun run dev:local",
Expand Down Expand Up @@ -36,20 +37,23 @@
"@epicenter/constants": "workspace:*",
"@epicenter/svelte": "workspace:*",
"@epicenter/workspace": "workspace:*",
"@sindresorhus/slugify": "catalog:",
"@tanstack/svelte-table": "catalog:",
"@tanstack/table-core": "9.0.0-alpha.10",
"prosemirror-commands": "^1.6.0",
"prosemirror-inputrules": "^1.4.0",
"prosemirror-keymap": "^1.2.0",
"prosemirror-schema-basic": "^1.2.0",
"prosemirror-schema-list": "^1.4.0",
"arktype": "catalog:",
"bits-ui": "catalog:",
"date-fns": "catalog:",
"filenamify": "^7.0.1",
"nanoid": "catalog:",
"prosemirror-commands": "^1.6.0",
"prosemirror-inputrules": "^1.4.0",
"prosemirror-keymap": "^1.2.0",
"prosemirror-model": "^1.25.0",
"prosemirror-schema-basic": "^1.2.0",
"prosemirror-schema-list": "^1.4.0",
"prosemirror-state": "^1.4.0",
"prosemirror-view": "^1.41.0",
"typebox": "catalog:",
"wellcrafted": "catalog:",
"y-prosemirror": "^1.3.7",
"yjs": "catalog:"
Expand Down
8 changes: 0 additions & 8 deletions apps/fuji/src/lib/auth.ts

This file was deleted.

15 changes: 10 additions & 5 deletions apps/fuji/src/lib/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,17 +8,22 @@
*/

import { APP_URLS } from '@epicenter/constants/vite';
import { createAuth } from '@epicenter/svelte/auth';
import { createWorkspace } from '@epicenter/workspace';
import { createPersistedState } from '@epicenter/svelte';
import { AuthSession, createAuth } from '@epicenter/svelte/auth';
import { indexeddbPersistence } from '@epicenter/workspace/extensions/persistence/indexeddb';
import {
createSyncExtension,
toWsUrl,
} from '@epicenter/workspace/extensions/sync/websocket';
import { session } from '$lib/auth';
import { fujiWorkspace } from './workspace/definition';
import { createFujiWorkspace } from '$lib/workspace';

export const workspace = createWorkspace(fujiWorkspace)
const session = createPersistedState({
key: 'fuji:authSession',
schema: AuthSession.or('null'),
defaultValue: null,
});

export const workspace = createFujiWorkspace()
.withExtension('persistence', indexeddbPersistence)
.withExtension(
'sync',
Expand Down
56 changes: 56 additions & 0 deletions apps/fuji/src/lib/components/AppHeader.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
<script lang="ts">
import { Button } from '@epicenter/ui/button';
import { getStars, GitHubButton } from '@epicenter/ui/github-button';
import { Kbd } from '@epicenter/ui/kbd';
import { LightSwitch } from '@epicenter/ui/light-switch';
import * as Tooltip from '@epicenter/ui/tooltip';
import PlusIcon from '@lucide/svelte/icons/plus';
import SearchIcon from '@lucide/svelte/icons/search';
import SyncStatusIndicator from './SyncStatusIndicator.svelte';
import { entriesState } from '$lib/entries.svelte';

let { onOpenSearch }: { onOpenSearch: () => void } = $props();
</script>

<div class="flex h-8 shrink-0 items-center justify-between border-b px-2">
<!-- Left: branding + actions -->
<div class="flex items-center gap-1.5">
<span class="text-xs font-semibold tracking-tight">Fuji</span>
<Tooltip.Root>
<Tooltip.Trigger>
{#snippet child({ props })}
<Button {...props} variant="ghost" size="icon-xs" onclick={onOpenSearch}>
<SearchIcon class="size-3.5" />
</Button>
{/snippet}
</Tooltip.Trigger>
<Tooltip.Content>
Search entries <Kbd>⌘K</Kbd>
</Tooltip.Content>
</Tooltip.Root>
<Tooltip.Root>
<Tooltip.Trigger>
{#snippet child({ props })}
<Button {...props} variant="ghost" size="icon-xs" onclick={entriesState.createEntry}>
<PlusIcon class="size-3.5" />
</Button>
{/snippet}
</Tooltip.Trigger>
<Tooltip.Content>
New entry <Kbd>⌘N</Kbd>
</Tooltip.Content>
</Tooltip.Root>
</div>
<!-- Right: external links + theme -->
<div class="flex items-center gap-0.5">
<SyncStatusIndicator />
<GitHubButton
repo={{ owner: 'EpicenterHQ', repo: 'epicenter' }}
path="/tree/main/apps/fuji"
stars={getStars({ owner: 'EpicenterHQ', repo: 'epicenter', fallback: 500 })}
variant="ghost"
size="sm"
/>
<LightSwitch variant="ghost" />
</div>
</div>
214 changes: 214 additions & 0 deletions apps/fuji/src/lib/components/EntriesSidebar.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,214 @@
<script lang="ts">
import * as Sidebar from '@epicenter/ui/sidebar';
import FileTextIcon from '@lucide/svelte/icons/file-text';
import HashIcon from '@lucide/svelte/icons/hash';
import TagIcon from '@lucide/svelte/icons/tag';
import { format, isToday, isYesterday } from 'date-fns';
import type { Entry } from '$lib/workspace';
import { viewState } from '$lib/view.svelte';
import { DateTimeString } from '@epicenter/workspace';
import { matchesEntrySearch } from '$lib/entries.svelte';

let { entries }: { entries: Entry[] } = $props();

const isSearching = $derived(viewState.searchQuery.trim().length > 0);

/** Entries matching the search query across title, subtitle, tags, and type. */
const searchResults = $derived.by(() => {
if (!isSearching) return [];
return entries.filter((entry) => matchesEntrySearch(entry, viewState.searchQuery));
});

/** Unique types with entry counts, sorted by count descending. */
const typeGroups = $derived.by(() => {
const counts = new Map<string, number>();
for (const entry of entries) {
for (const t of entry.type) {
counts.set(t, (counts.get(t) ?? 0) + 1);
}
}
return [...counts.entries()]
.sort((a, b) => b[1] - a[1])
.map(([name, count]) => ({ name, count }));
});

/** Unique tags with entry counts, sorted by count descending. */
const tagGroups = $derived.by(() => {
const counts = new Map<string, number>();
for (const entry of entries) {
for (const tag of entry.tags) {
counts.set(tag, (counts.get(tag) ?? 0) + 1);
}
}
return [...counts.entries()]
.sort((a, b) => b[1] - a[1])
.map(([name, count]) => ({ name, count }));
});

/** Recent entries sorted by updatedAt, limited to 10. */
const recentEntries = $derived(
[...entries]
.sort((a, b) => b.updatedAt.localeCompare(a.updatedAt))
.slice(0, 10),
);

function getDateLabel(dts: string): string {
const date = DateTimeString.toDate(dts);
if (isToday(date)) return 'Today';
if (isYesterday(date)) return 'Yesterday';
return format(date, 'MMM d');
}
</script>

<Sidebar.Root collapsible="none" class="h-full w-full">
<Sidebar.Header>
<div class="px-2 pb-1">
<Sidebar.Input
placeholder="Search entries…"
value={viewState.searchQuery}
oninput={(e) => viewState.setSearchQuery(e.currentTarget.value)}
/>
</div>
</Sidebar.Header>

<Sidebar.Content>
<!-- All Entries -->
<Sidebar.Group>
<Sidebar.GroupContent>
<Sidebar.Menu>
<Sidebar.MenuItem>
<Sidebar.MenuButton
isActive={viewState.activeTypeFilter === null && viewState.activeTagFilter === null && !isSearching}
onclick={() => viewState.clearFilters()}
>
<FileTextIcon class="size-4" />
<span>All Entries</span>
<span class="ml-auto text-xs text-muted-foreground">
{entries.length}
</span>
</Sidebar.MenuButton>
</Sidebar.MenuItem>
</Sidebar.Menu>
</Sidebar.GroupContent>
</Sidebar.Group>

{#if isSearching}
<!-- Search Results -->
<Sidebar.Group>
<Sidebar.GroupLabel>
Search Results ({searchResults.length})
</Sidebar.GroupLabel>
<Sidebar.GroupContent>
<Sidebar.Menu>
{#if searchResults.length > 0}
{#each searchResults as entry (entry.id)}
<Sidebar.MenuItem>
<Sidebar.MenuButton onclick={() => viewState.selectEntry(entry.id)}>
<div class="flex w-full flex-col gap-0.5 overflow-hidden">
<span class="truncate text-sm font-medium">
{entry.title || 'Untitled'}
</span>
{#if entry.subtitle}
<span class="truncate text-xs text-muted-foreground">
{entry.subtitle}
</span>
{/if}
</div>
</Sidebar.MenuButton>
</Sidebar.MenuItem>
{/each}
{:else}
<Sidebar.MenuItem>
<span class="px-2 py-1 text-xs text-muted-foreground">
No entries match "{viewState.searchQuery}"
</span>
</Sidebar.MenuItem>
{/if}
</Sidebar.Menu>
</Sidebar.GroupContent>
</Sidebar.Group>
{:else}
<!-- Type Groups -->
{#if typeGroups.length > 0}
<Sidebar.Group>
<Sidebar.GroupLabel>Type</Sidebar.GroupLabel>
<Sidebar.GroupContent>
<Sidebar.Menu>
{#each typeGroups as group (group.name)}
<Sidebar.MenuItem>
<Sidebar.MenuButton
isActive={viewState.activeTypeFilter === group.name}
onclick={() =>
viewState.setTypeFilter(
viewState.activeTypeFilter === group.name ? null : group.name,
)}
>
<HashIcon class="size-4" />
<span>{group.name}</span>
<span class="ml-auto text-xs text-muted-foreground">
{group.count}
</span>
</Sidebar.MenuButton>
</Sidebar.MenuItem>
{/each}
</Sidebar.Menu>
</Sidebar.GroupContent>
</Sidebar.Group>
{/if}

<!-- Tag Groups -->
{#if tagGroups.length > 0}
<Sidebar.Group>
<Sidebar.GroupLabel>Tags</Sidebar.GroupLabel>
<Sidebar.GroupContent>
<Sidebar.Menu>
{#each tagGroups as group (group.name)}
<Sidebar.MenuItem>
<Sidebar.MenuButton
isActive={viewState.activeTagFilter === group.name}
onclick={() =>
viewState.setTagFilter(
viewState.activeTagFilter === group.name ? null : group.name,
)}
>
<TagIcon class="size-4" />
<span>{group.name}</span>
<span class="ml-auto text-xs text-muted-foreground">
{group.count}
</span>
</Sidebar.MenuButton>
</Sidebar.MenuItem>
{/each}
</Sidebar.Menu>
</Sidebar.GroupContent>
</Sidebar.Group>
{/if}

<!-- Recent Entries -->
{#if recentEntries.length > 0}
<Sidebar.Group>
<Sidebar.GroupLabel>Recent</Sidebar.GroupLabel>
<Sidebar.GroupContent>
<Sidebar.Menu>
{#each recentEntries as entry (entry.id)}
<Sidebar.MenuItem>
<Sidebar.MenuButton onclick={() => viewState.selectEntry(entry.id)}>
<div class="flex w-full flex-col gap-0.5 overflow-hidden">
<span class="truncate text-sm font-medium">
{entry.title || 'Untitled'}
</span>
<span class="truncate text-xs text-muted-foreground">
{getDateLabel(entry.updatedAt)}
</span>
</div>
</Sidebar.MenuButton>
</Sidebar.MenuItem>
{/each}
</Sidebar.Menu>
</Sidebar.GroupContent>
</Sidebar.Group>
{/if}
{/if}
</Sidebar.Content>

</Sidebar.Root>
Loading
Loading