feat(fuji): UI polish, workspace actions, state architecture, and date fields#1647
Merged
feat(fuji): UI polish, workspace actions, state architecture, and date fields#1647
Conversation
Entries now support pinning (sort to top of lists) and soft deletion via deletedAt timestamp. Soft-deleted entries are filtered from the active entries list and sidebar. This is critical for CRDT conflict safety when two devices diverge—edits on a deleted entry resolve cleanly instead of causing data loss.
Move entry CRUD, view mode, filters, and selection state out of +page.svelte into entries-state.svelte.ts and view-state.svelte.ts. Follows the factory function singleton pattern used by Opensidian and Honeycrisp. The page component is now purely rendering logic.
Add Cmd+K command palette using @epicenter/ui/command-palette. Entries are searchable by title, subtitle, tags, and type via the palette's built-in fuzzy filtering. Also enhance the table's global filter to search across all metadata fields instead of just title.
When the user types in the sidebar search input, type/tag/recent groups are replaced with a flat list of matching entries. Matches against title, subtitle, tags, and type (case-insensitive). Clearing the search restores the normal browsing view.
Replace the timestamps footer with a StatusBar component showing word count, tags, and creation/update dates. Add strikethrough (Cmd+Shift+S) and underline (Cmd+U) marks to the ProseMirror schema. Word count updates in real-time via a ProseMirror plugin.
Add sortBy KV key to the workspace definition with options for dateEdited, dateCreated, and title. Expose via viewState with getter and setter. Persisted across reloads and synced across devices.
Remove unused parseDateTime/format import from EntryEditor (orphaned after StatusBar extraction), unused SearchIcon import from FujiSidebar, and fix JSDoc that said Tiptap when the code uses ProseMirror directly.
The vault project needs to import the fuji workspace definition to wire up filesystem persistence and markdown materialization. Adds the factory function and re-exports the definition from the workspace barrel.
Add a create action to the workspace factory so entry creation is available to CLI, AI tools, and any future consumer—not just the Svelte UI. client.ts now uses createFujiWorkspace() instead of calling createWorkspace() directly. entries-state delegates to the workspace action and only adds the view-state selection side-effect.
Add a create action to createFujiWorkspace().withActions() so entry creation is available to CLI, AI tools, and any future consumer. client.ts now uses the factory instead of calling createWorkspace() directly, removing the unused createWorkspace import.
The persisted sortBy preference now drives the initial sort column in EntriesTable and the date grouping field in EntryTimeline. Clicking a column header in the table propagates the change back to the KV store so it survives reloads and syncs across devices.
Replace three inline filter-clearing calls with a single onClearFilters callback that delegates to viewState.clearFilters(). Removes the last dead method in the view state module.
Vite's strict module resolution requires typebox to be listed explicitly. Other apps (Opensidian, tab-manager) already list it via catalog.
Remove duplicate `import type * as Y from 'yjs'` in EntryEditor. Change StatusBar entry prop from `Entry | null` to `Entry` since it's only rendered inside a selectedEntry guard. Remove stale 'workspace actions' framing from entries-state JSDoc.
… rich empty states Replaces SidebarProvider with Resizable.PaneGroup for drag-resizable sidebar. Adds persistent AppHeader with branding, GitHub link, theme toggle, and keyboard shortcut hints. Adds GlobalStatusBar with entry count. Upgrades empty states in table/timeline to use Empty.* compound component from packages/ui. Spec: specs/20260408T160000-fuji-ui-polish.md
chrono-node/en parses NL text ("next tuesday 3pm", "tomorrow at noon")
into date components. Intl.DateTimeFormat resolves those components in
the user-selected IANA timezone to a UTC instant, avoiding DST edge
cases. The result serializes as DateTimeString for two-way Svelte 5
binding via $bindable().
Components: NaturalLanguageDateInput (main), TimezoneCombobox
(searchable IANA selector), parse-date.ts (parsing + bridging utility).
6 bun:test cases passing.
…ompat Sidebar.Root with default collapsible uses position:fixed which fights Resizable panels. Setting collapsible=none renders a plain div that flows correctly inside Resizable.Pane. Also swaps Lucide GitHub icon for Simple Icons SVG (matching Opensidian).
… packages/ui Schema defines _v as Arktype '1' which parses as number literal 1. Action was writing string '1', causing validation failure and entries never appearing in the reactive SvelteMap. Also moves GithubIcon SVG from per-app local copies to @epicenter/ui/github-icon for shared use.
- Remove sidebarCollapsed KV key + getter/setter (sidebar is resizable now, not collapsible) - Remove allEntries, deletedEntries getters (no consumers) - Remove softDeleteEntry, restoreEntry, permanentlyDeleteEntry, togglePin methods (no callers, no UI for these features yet) - Remove dead dateTimeStringNow import - Remove redundant Tooltip.Provider from AppHeader (layout already provides one)
…HubButton Installs github-button from shadcn-svelte-extras into packages/ui. Replaces the hand-rolled GithubIcon SVG + tooltip-wrapped Button in Fuji and the buttonVariants anchor in Opensidian with the standardized GitHubButton component. Removes the now-unused github-icon package.
…E2E encryption Add deriveKeyFromPassword, generateSalt, and buildEncryptionKeys to the crypto module. These bridge password input to the existing encryption flow so vault workspaces can derive keys client-side via PBKDF2. Includes 13 tests covering determinism, round-trips, and end-to-end integration through encrypt/decrypt.
StatusBar "Created" date is now clickable—opens a Popover with a
chrono-node suggestions input and IANA timezone combobox. Selecting a
suggestion converts the Date + timezone to DateTimeString and writes
it back via onUpdate({ createdAt }).
Removed yeezy-dates in favor of direct chrono-node. Merged the
duplicate nlp-date-input/ folder into natural-language-date-input/
so everything lives under one package export path.
Also fixed chrono-node/en → chrono-node import (Vite resolves from
the consuming app's context, not the declaring package's node_modules).
The spec's Input+Preview+Confirm component was superseded by the suggestions-based NLPDateInput. Nobody imported the original component, and its 200-line DST-safe chrono→Intl bridging function was only called by that dead component. Deleted: natural-language-date-input.svelte, parseNaturalLanguageDate(), extractDateComponents(), getOffsetMillisecondsForTimezone(), isValidDate(), DateComponents type, ParseNaturalLanguageDateResult type, and their tests. Kept: toDateTimeString() and localTimezone() (both alive in StatusBar), nlp-date-input.svelte, timezone-combobox.svelte.
…elpers Unify the pipe-delimited DateTimeString parser (duplicated in 4 files) and entry search predicate (duplicated in sidebar + table) into $lib/utils/ so search semantics stay consistent across views.
…pers Both were trivial passthroughs to workspace table methods with no side effects. +page.svelte now uses entriesMap.get() for reactive lookup and workspace.tables.entries.update() directly. Skipped: pinned field removal (requires CRDT schema migration), BadgeList inlining (TanStack Table renderComponent forces component refs).
Remove workspace/index.ts and state/index.ts barrel re-exports—all consumers now import directly from definition.ts, entries-state.svelte.ts, and view-state.svelte.ts. Inline GlobalStatusBar (16 lines) into +page.svelte since it had no reuse and was app-specific status markup.
entries-state no longer imports viewState. The create-then-select pairing now lives in the page orchestrator where the coupling is explicit and visible.
…Sidebar Drop 8 callback/state props that were just viewState pass-throughs. The sidebar now imports viewState directly and only accepts entries as a prop. Renamed FujiSidebar → EntriesSidebar since the Fuji prefix was redundant inside apps/fuji.
…agFilter The old names implied filtering logic; they just set state variables. New names match the setSearchQuery convention already in viewState.
…ombobox - Fix StatusBar importing NLPDateInput from deleted @epicenter/ui/nlp-date-input path; consolidated to @epicenter/ui/natural-language-date-input - Rename parse-date.ts → datetime-string.ts (file no longer parses dates, it serializes DateTimeString and gets local timezone) - Move timezone-combobox.svelte to its own packages/ui/src/timezone-combobox/ folder—it's a standalone reusable component, not NLP-specific
… palette CommandPalette from @epicenter/ui already registers its own cmd+k keydown handler. Fuji's additional handler on svelte:window toggled the same bound state twice per keypress, causing the dialog to flash open then immediately close. Matches opensidian's working pattern of letting CommandPalette own the shortcut.
Replaces the standalone dateTimeStringNow() function with methods on
the DateTimeString validator itself: .now(), .parse(), .stringify(),
.toDate(), and .is(). Follows the JSON.parse/JSON.stringify pattern
for familiarity. Also tightens validation from indexOf('|') to a
strict regex, and brands DateIsoString/TimezoneId.
parseDateTime was a 3-line wrapper doing new Date(dts.split('|')[0]).
DateTimeString.toDate now provides the same operation as a first-class
workspace utility. Four callers updated, file removed.
Consumer-side migration for the DateTimeString companion object. Updates fuji, honeycrisp, and CLI test imports.
Delete 5 reference files (4 were 100% duplicates of SKILL.md, 1 had 2 unique sections). Merge view-mode branching and data-driven markup from component-patterns.md. Add template gotchas section documenting the unicode escape pitfall in HTML context. Rename "Mutation Pattern Preference" to "Mutation Patterns".
Both ./workspace and ./materializer exports pointed to non-existent index.ts files with zero consumers across the monorepo. chrono-node was listed but never imported—the natural language date input lives in @epicenter/ui. Also sorts deps alphabetically.
…o $derived activeEntries was recently changed from a $derived value to a plain function, causing it to re-filter on every call (4× per render cycle). Wrap in createEntriesState() factory matching the view.svelte.ts pattern, keep entriesMap inside the closure, and expose entries.active as a getter over a single $derived computation.
…status bar Entries now have a required `date` field (DateTimeString) representing the user's intended date for the content—publish date, event date, reference date. Defaults to creation time, editable via NaturalLanguageDateInput popover in the metadata section. createdAt and updatedAt are no longer user-editable. Both display as read-only timestamps in the status bar alongside word count. Removed duplicate tag display from status bar.
Remove migrationDialog.openDialog() (zero callers) and terminalState.printWelcome() (zero callers — ensureWelcome is called internally by toggle/show/init). Fix stray leading tab on terminal-state import.
…terializer StatusBar was 21 lines with no state—inlined into EntryEditor. Date picker now shares a single flex row with Type and Tags instead of sitting orphaned on its own line. Removed unnecessary DateTimeStringType alias. Fixed materializer frontmatter missing the new date field.
EncryptionKeys is defined as [T, ...T[]] via arktype, but createCliUnlock declared it as T[]. Function parameter contravariance made the context's signature incompatible with the inline type.
…ist views sortBy values were 'dateEdited'/'dateCreated' which required mapping tables to translate to the actual column IDs 'updatedAt'/'createdAt'. Replaced with direct column names: 'date' | 'updatedAt' | 'createdAt' | 'title'. Added date column to EntriesTable. Updated EntryTimeline to support grouping by date. Default sort is now 'date'—the most natural ordering for a CMS.
…sort Replaced dateEdited/dateCreated with updatedAt/createdAt in the sortBy KV union—eliminates the mapping tables in EntriesTable that existed solely to translate between two naming conventions. Added date column to the table and date as the default sort (most natural for a CMS). Timeline now supports grouping by the user-defined date field.
Merge definition.ts and workspace.ts into lib/workspace.ts. The two-file directory added indirection for one table and one action. Update all imports from $lib/workspace/definition and ./workspace/workspace.
Eight lines, one consumer. The session persisted state is only used by createAuth in client.ts—no reason for a separate file.
Move matchesEntrySearch into entries.svelte.ts—it's entry-specific logic with two consumers, not a search module. Rename the singleton export to entriesState to match the monorepo convention (viewState, skillsState, notesState).
Both components now import viewState and workspace directly instead of receiving searchQuery, sortBy, selectedEntryId, onSelectEntry, onAddEntry, and onSortChange as props. Matches EntriesSidebar which already used direct imports. The only remaining prop is entries (the filtered list).
Replicates the opensidian SyncStatusIndicator pattern—cloud icon in the top-right header that opens a popover with AuthForm (signed out) or user info, sync status, and sign-out (signed in).
Previously, editing entry metadata (title, subtitle, tags, type, date) bypassed updatedAt because only the withDocument onUpdate hook touched it. Route all field edits through a workspace action so the timestamp stays consistent across UI, CLI, and AI callers.
The Wave 4 write didn't persist—entries.svelte.ts was still exporting `entries` instead of `entriesState`, and matchesEntrySearch was missing after search.ts was deleted.
Type.String() infers as plain string, causing a type mismatch when the handler passes the value to tables.entries.update which expects DateTimeString. Type.Unsafe preserves the branded type at the TypeScript level while emitting identical JSON Schema.
Move createEntry into entriesState so the three identical copies in +page.svelte, EntriesTable, and EntryTimeline collapse into one method. Replace the exposed SvelteMap with a get(id) method—the only operation any consumer actually uses.
AppHeader now imports entriesState directly and calls createEntry itself, matching the pattern used by all other components. The only remaining callback prop is onOpenSearch, which controls local page state.
EntryEditor now imports viewState and workspace directly. Back navigation calls viewState.selectEntry(null), field updates go through the workspace entries.update action. The only remaining props are entry and yxmlfragment—both computed values from the page's document handle effect.
# Conflicts: # apps/fuji/src/lib/components/EntriesTable.svelte # apps/fuji/src/lib/components/EntryEditor.svelte # apps/fuji/src/routes/+page.svelte # apps/honeycrisp/src/lib/state/notes.svelte.ts # apps/opensidian/src/lib/components/editor/StatusBar.svelte # apps/opensidian/src/lib/components/editor/TabBar.svelte # apps/opensidian/src/lib/state/terminal-state.svelte.ts # bun.lock # packages/cli/test/e2e-honeycrisp.test.ts # packages/ui/package.json # packages/ui/src/natural-language-date-input/index.ts # packages/ui/src/natural-language-date-input/natural-language-date-input.svelte # packages/workspace/src/shared/crypto/index.ts
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fuji had the bones of a journal app but no personality. The sidebar was static, search did nothing visible, entries had no timestamps the user could control, and the whole state layer was a single 300-line component. This reworks the app from data layer through UI.
The workspace definition now owns entry lifecycle through typed actions. Creating and updating entries goes through
withActions()instead of ad-hoctable.set()calls scattered across components:State went from one monolith to three focused modules.
The old
+page.sveltemixed view preferences, entry CRUD, search filtering, and sort logic in one file. Now each concern has its own module that imports the workspace directly:Components import state directly instead of receiving it through props.
EntriesTable,EntryTimeline, andEntriesSidebarall read from the state modules, which eliminated ~15 prop-drilling chains across the page.The UI got a proper shell. An
AppHeaderwith a command palette (Cmd+K), a resizable sidebar that shows search results inline when a query is active, aSyncStatusIndicatorpopover for sign-in and connection status, and rich empty states for each view mode.Entries gained a user-controllable date field backed by the NLP date input from
@epicenter/ui. System timestamps (createdAt, updatedAt) moved to a metadata bar inside the editor. Sort preferences persist via workspace KV.The UI package picks up a few pieces too. The NLP date input got renamed from
NLPDateInputtoNaturalLanguageDateInput, its parsing pipeline was trimmed to just the datetime-string conversion it actually needs, and the timezone combobox was extracted to its ownpackages/ui/src/timezone-combobox/module. AGitHubButtoncomponent replaces the hand-rolled SVG icon that was duplicated across apps.Also flattened the fuji file structure:
workspace/definition.ts+workspace/workspace.tscollapsed into a singleworkspace.ts,state/entries-state.svelte.tsbecameentries.svelte.ts, barrel files deleted,auth.tsinlined intoclient.ts.30 files changed, +2154/-619. Stacks on #1644, #1645, #1646 (all merged).