Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
4 changes: 2 additions & 2 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -154,7 +154,7 @@ export const [getLinkContext, setLinkContext] = createContext<LinkContext>();
Wrap in `$derived` for reactivity.

```svelte
<script>
<script lang="ts">
import { getNote } from "$lib/remote/notes.remote";
let { noteId } = $props();
let noteQuery = $derived(getNote(noteId));
Expand Down Expand Up @@ -193,7 +193,7 @@ In SSR, module state is shared across requests.
Use `$derived` overrides or `$state.eager`.

```svelte
<script>
<script lang="ts">
let { post, like } = $props();
let likes = $derived(post.likes);
async function onclick() {
Expand Down
2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,9 @@
"fast-diff": "^1.3.0",
"katex": "^0.16.27",
"loro-crdt": "^1.10.3",
"runed": "^0.37.0",
"svelte": "^5.45.8",
"temporal-polyfill": "^0.3.0",
"tiptap-markdown": "^0.9.0",
"typescript-svelte-plugin": "^0.3.50"
},
Expand Down
45 changes: 45 additions & 0 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

12 changes: 12 additions & 0 deletions src/app.d.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
// See https://svelte.dev/docs/kit/types#app.d.ts
// for information about these interfaces

import type { ResolvedPathname } from "$app/types";
import type { User } from "$lib/schema.ts";
import type { Session } from "$lib/server/auth.ts";
import type { HTMLAnchorAttributes } from "svelte/elements";

declare global {
namespace App {
Expand All @@ -17,4 +19,14 @@ declare global {
// interface PageState {}
// interface Platform {}
}

namespace svelteHTML {
interface IntrinsicElements {
a: Omit<HTMLAnchorAttributes, "href"> & {
// The (string & {}) trick prevents 'string' from collapsing the union,
// preserving Intellisense for your Pathnames.
href?: ResolvedPathname | (string & {}) | null;
};
}
}
}
2 changes: 1 addition & 1 deletion src/hooks.server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ const handleAuth: Handle = async ({ event, resolve }) => {
auth.deleteSessionTokenCookie(event.cookies);

// Redirect to login if session invalid
if (event.url.pathname === "/") {
if (!event.route.id?.startsWith("/(auth)")) {
return new Response("Redirect", {
status: 303,
headers: { Location: "/login" },
Expand Down
134 changes: 86 additions & 48 deletions src/lib/components/Sidebar.svelte
Original file line number Diff line number Diff line change
@@ -1,33 +1,36 @@
<script lang="ts">
import { onMount } from "svelte";
import { dropTargetForElements } from "@atlaskit/pragmatic-drag-and-drop/element/adapter";
import TreeItem from "./TreeItem.svelte";
import { SvelteSet } from "svelte/reactivity";
import {
FolderPlus,
FilePlus,
File,
Plus,
Trash2,
Pencil,
} from "@lucide/svelte";
import type { User } from "$lib/schema.ts";
import ProfilePicture from "./ProfilePicture.svelte";
import { goto } from "$app/navigation";
import { resolve } from "$app/paths";
import { page } from "$app/state";
import { encryptKeyForUser, generateNoteKey } from "$lib/crypto.ts";
import { logout } from "$lib/remote/accounts.remote.ts";
import { unawaited } from "$lib/unawaited.ts";
import {
createNote,
deleteNote,
updateNote,
reorderNotes,
getNotes,
reorderNotes,
updateNote,
} from "$lib/remote/notes.remote.ts";
import type { User } from "$lib/schema.ts";
import { unawaited } from "$lib/unawaited.ts";
import { buildNotesTree } from "$lib/utils/tree.ts";
import { generateNoteKey, encryptKeyForUser } from "$lib/crypto";
import { goto } from "$app/navigation";
import { resolve } from "$app/paths";
import type { NoteOrFolder } from "$lib/schema.ts";
import { page } from "$app/state";
import { dropTargetForElements } from "@atlaskit/pragmatic-drag-and-drop/element/adapter";
import {
File,
FilePlus,
FolderPlus,
LogOut,
PanelLeftClose,
Pencil,
Plus,
Trash2,
} from "@lucide/svelte";
import { onMount } from "svelte";
import { SvelteSet } from "svelte/reactivity";
import ProfilePicture from "./ProfilePicture.svelte";
import { LoroDoc } from "loro-crdt";
import { getEncryptedSnapshot } from "$lib/loro.ts";
import TreeItem from "./TreeItem.svelte";

interface ContextState {
x: number;
Expand All @@ -38,21 +41,23 @@

interface Props {
user: User | undefined;
notesList: NoteOrFolder[];
isCollapsed: boolean;
toggleSidebar: () => void;
}

let { user, notesList }: Props = $props();
let { user, isCollapsed, toggleSidebar }: Props = $props();

let expandedFolders = new SvelteSet<string>();
let renamingId = $state<string | null>(null);
let renameTitle = $state("");
let contextMenu = $state<ContextState>();
let renameModal: HTMLDialogElement;

let notesTree = $derived(buildNotesTree(notesList));

let rootContainer: HTMLElement;
let isRootDropTarget = $state(false);

let notesListQuery = $derived(getNotes());

// Set up root drop target
onMount(() => {
const cleanup = dropTargetForElements({
Expand Down Expand Up @@ -176,13 +181,17 @@
const noteKey = await generateNoteKey();

// Encrypt note key with user's public key
const encryptedKey = await encryptKeyForUser(noteKey, publicKey);
const [encryptedKey, encryptedSnapshot] = await Promise.all([
encryptKeyForUser(noteKey, publicKey),
getEncryptedSnapshot(new LoroDoc(), noteKey),
]);

const newNote = await createNote({
title,
encryptedKey,
parentId,
isFolder,
encryptedSnapshot,
}).updates(
// TODO: add optimistic update.
getNotes(),
Expand All @@ -200,31 +209,60 @@
renameModal.close();
}
});

let notesList = $derived(await notesListQuery);
let notesTree = $derived(buildNotesTree(notesList));
</script>

<svelte:window onclick={onWindowClick} />

<div
class="sidebar flex h-full w-64 flex-col border-r border-base-content/10 [view-transition-name:sidebar]"
class={[
"sidebar flex h-full w-64 flex-col border-base-content/10 transition-all duration-300 [view-transition-name:sidebar]",
isCollapsed ? "-ml-64" : "ml-0 border-r",
]}
>
<!-- User Header -->
<div
class="flex items-center justify-between border-b border-base-content/10 p-4"
>
<div class="flex items-center gap-2">
<ProfilePicture name={user?.username ?? "A"} />
<span class="max-w-28 truncate text-sm font-medium"
>{user?.username ?? "Anonymous"}</span
<div class="dropdown dropdown-bottom">
<div
tabindex="0"
role="button"
class="btn h-auto min-h-0 gap-3 rounded-lg px-3 py-2 normal-case btn-ghost hover:bg-base-200"
>
</div>
<form {...logout}>
<button
type="submit"
class="text-xs text-base-content/40 transition-colors hover:text-base-content/60"
<ProfilePicture name={user?.username[0] ?? "A"} />
<span class="max-w-30 truncate text-sm font-semibold">
{user?.username ?? "Anonymous"}
</span>
</div>
<!-- svelte-ignore a11y_no_noninteractive_tabindex -->
<ul
tabindex="0"
class="dropdown-content menu z-1 w-52 rounded-box bg-base-100 p-2 shadow"
>
Log out
</button>
</form>
<li>
<form {...logout}>
<button
type="submit"
class="flex w-full items-center gap-2 text-xs text-base-content/40 transition-colors hover:text-base-content/60"
>
<LogOut size={16} />
Log out
</button>
</form>
</li>
</ul>
</div>

<button
onclick={toggleSidebar}
class="btn btn-square btn-ghost btn-sm"
title="Collapse sidebar (Ctrl+B)"
>
<PanelLeftClose size={20} />
</button>
</div>

<!-- Actions -->
Expand Down Expand Up @@ -258,6 +296,7 @@
"flex-1 space-y-1 overflow-y-auto px-2 py-2 transition-all",
isRootDropTarget && "bg-indigo-50 ring-2 ring-primary ring-inset",
]}
role="tree"
>
{#each notesTree as item, idx (item.id)}
<TreeItem
Expand Down Expand Up @@ -295,19 +334,18 @@
{#if contextMenu.isFolder}
<button
class="flex w-full cursor-pointer items-center gap-2 px-4 py-2 text-left text-sm text-base-content hover:bg-primary hover:text-primary-content"
onclick={() => {
onclick={async () => {
if (user === undefined) {
throw new Error("Cannot create note whilst logged out.");
}

unawaited(
handleCreateNote(
"An Untitled Note",
clickedId,
false,
user.publicKey,
),
await handleCreateNote(
"An Untitled Note",
clickedId,
false,
user.publicKey,
);

closeContextMenu();
}}><Plus /> New Note Inside</button
>
Expand Down
10 changes: 10 additions & 0 deletions src/lib/components/sidebar-context.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import { createContext } from "svelte";

interface SidebarContext {
get isCollapsed(): boolean;

toggle: () => boolean;
}

export const [getSidebarContext, setSidebarContext] =
createContext<SidebarContext>();
Loading