Merge pull request #506 from AtalayaLabs/claude/frontend-performance-analysis-iwtyx1
This commit is contained in:
@@ -6,7 +6,7 @@
|
||||
import { searchFiles } from '$lib/api/endpoints/search';
|
||||
import { fileInlineUrl } from '$lib/api/endpoints/files';
|
||||
import type { FileItem, FolderItem } from '$lib/api/types';
|
||||
import CommandPalette from '$lib/components/CommandPalette.svelte';
|
||||
import { lazyComponent } from '$lib/composables/lazyComponent.svelte';
|
||||
import Icon from '$lib/icons/Icon.svelte';
|
||||
import { iconNameFromClass } from '$lib/utils/display';
|
||||
import { userInitials, avatarColorIndex } from '$lib/utils/avatar';
|
||||
@@ -19,6 +19,10 @@
|
||||
|
||||
let { children }: { children: Snippet } = $props();
|
||||
|
||||
// The command palette is loaded on its first Cmd/Ctrl+K and mounted open.
|
||||
// Until then its ~400-line module stays out of the initial bundle.
|
||||
const palette = lazyComponent(() => import('$lib/components/CommandPalette.svelte'));
|
||||
|
||||
interface NavLink {
|
||||
href: string;
|
||||
label: string;
|
||||
@@ -232,6 +236,13 @@
|
||||
<svelte:window
|
||||
onclick={closeMenus}
|
||||
onkeydown={(e) => {
|
||||
// First Cmd/Ctrl+K loads the palette and mounts it open; once mounted,
|
||||
// the palette's own handler takes over toggling/closing.
|
||||
if ((e.metaKey || e.ctrlKey) && e.key.toLowerCase() === 'k' && !palette.component) {
|
||||
e.preventDefault();
|
||||
void palette.load();
|
||||
return;
|
||||
}
|
||||
if (e.key !== 'Escape') return;
|
||||
if (aboutOpen) aboutOpen = false;
|
||||
else if (searchActive) closeMobileSearch();
|
||||
@@ -692,7 +703,10 @@
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<CommandPalette />
|
||||
{#if palette.component}
|
||||
{@const CommandPalette = palette.component}
|
||||
<CommandPalette autoOpen />
|
||||
{/if}
|
||||
|
||||
<style>
|
||||
/* Body becomes the sidebar+main flex row only while the shell is mounted. */
|
||||
|
||||
@@ -17,6 +17,10 @@
|
||||
run: () => void;
|
||||
}
|
||||
|
||||
// `autoOpen` lets a lazy host (AppShell) mount us already-open on the first
|
||||
// Cmd/Ctrl+K, since our own key listener only exists once we're mounted.
|
||||
let { autoOpen = false }: { autoOpen?: boolean } = $props();
|
||||
|
||||
let open = $state(false);
|
||||
// Drives the enter animation: flipped on after mount so the overlay/panel
|
||||
// transition from their initial (faded/offset) state.
|
||||
@@ -30,6 +34,18 @@
|
||||
// Element focused before the palette opened, restored on close.
|
||||
let prevFocus: HTMLElement | null = null;
|
||||
|
||||
// When mounted already-open (autoOpen), run the same enter sequence the
|
||||
// keyboard path uses. Guarded so it fires once, not on every reopen.
|
||||
let didAutoOpen = false;
|
||||
$effect(() => {
|
||||
if (didAutoOpen || !autoOpen) return;
|
||||
didAutoOpen = true;
|
||||
open = true;
|
||||
prevFocus = document.activeElement as HTMLElement | null;
|
||||
requestAnimationFrame(() => (entered = true));
|
||||
queueMicrotask(() => input?.focus());
|
||||
});
|
||||
|
||||
function close() {
|
||||
open = false;
|
||||
entered = false;
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
/**
|
||||
* Defer loading a heavy Svelte component until it is first needed.
|
||||
*
|
||||
* The dynamic `import()` puts the component in its own chunk, keeping it out of
|
||||
* the initial bundle. The component type is inferred from the module's default
|
||||
* export, so binding and prop typing at the call site stay fully checked. Call
|
||||
* `load()` right before the component is shown, then render it once `component`
|
||||
* is non-null:
|
||||
*
|
||||
* ```svelte
|
||||
* const viewer = lazyComponent(() => import('$lib/components/FileViewer.svelte'));
|
||||
* $effect(() => { if (open) void viewer.load(); });
|
||||
* …
|
||||
* {#if viewer.component}
|
||||
* {@const Viewer = viewer.component}
|
||||
* <Viewer bind:open {file} />
|
||||
* {/if}
|
||||
* ```
|
||||
*/
|
||||
export function lazyComponent<C>(loader: () => Promise<{ default: C }>) {
|
||||
let component = $state<C | null>(null);
|
||||
let pending: Promise<void> | null = null;
|
||||
|
||||
return {
|
||||
get component() {
|
||||
return component;
|
||||
},
|
||||
/** Idempotent: kicks off the import once, resolves when the chunk is ready. */
|
||||
load(): Promise<void> {
|
||||
if (component) return Promise.resolve();
|
||||
if (!pending) {
|
||||
pending = loader().then((mod) => {
|
||||
component = mod.default;
|
||||
});
|
||||
}
|
||||
return pending;
|
||||
}
|
||||
};
|
||||
}
|
||||
@@ -31,14 +31,16 @@ export class OwnerCache {
|
||||
|
||||
/** Resolve every not-yet-cached id in parallel; nullish ids are skipped. */
|
||||
async resolve(ids: Iterable<string | null | undefined>): Promise<void> {
|
||||
const unique = [...new Set([...ids].filter((id): id is string => !!id))];
|
||||
await Promise.all(
|
||||
unique.map(async (id) => {
|
||||
if (this.#names[id]) return;
|
||||
const name = await this.#resolver(id);
|
||||
this.#names = { ...this.#names, [id]: name };
|
||||
})
|
||||
const pending = [...new Set([...ids].filter((id): id is string => !!id))].filter(
|
||||
(id) => !this.#names[id]
|
||||
);
|
||||
if (pending.length === 0) return;
|
||||
const resolved = await Promise.all(
|
||||
pending.map(async (id) => [id, await this.#resolver(id)] as const)
|
||||
);
|
||||
// One reactive assignment for the whole batch instead of one per id, so a
|
||||
// large resolve doesn't spread-copy the record N times (and re-run derives N times).
|
||||
this.#names = { ...this.#names, ...Object.fromEntries(resolved) };
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -96,10 +96,15 @@ class FilesStore {
|
||||
this.selection = new Set();
|
||||
}
|
||||
|
||||
// Soft ceiling so the per-item toggle can't grow the set without bound.
|
||||
// (Bulk "select all" lives in the views and intentionally isn't capped —
|
||||
// silently dropping ids there would break batch delete/move.)
|
||||
static readonly MAX_SELECTION = 10_000;
|
||||
|
||||
toggleSelected(id: string): void {
|
||||
const next = new Set(this.selection);
|
||||
if (next.has(id)) next.delete(id);
|
||||
else next.add(id);
|
||||
else if (next.size < FilesStore.MAX_SELECTION) next.add(id);
|
||||
this.selection = next;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user