feat(*): one task editor, vim navigation and undo
This commit is contained in:
1 parent
0873cbb366
commit
83ca32d98e
37 files changed
+4043
-505
No files matched your search
@@ -0,0 +1,108 @@
|
||||
<script lang="ts">
|
||||
import { Portal } from "bits-ui";
|
||||
import type { Snippet } from "svelte";
|
||||
import { onScreen, place } from "../lib/place";
|
||||
import { cn } from "../lib/utils";
|
||||
import { useView } from "./context";
|
||||
|
||||
interface Props {
|
||||
/** CSS selector for whatever this should sit under. */
|
||||
anchor: string;
|
||||
/** Off when the panel's contents put the caret somewhere themselves. */
|
||||
autofocus?: boolean;
|
||||
children: Snippet;
|
||||
class?: string;
|
||||
onclose: () => void;
|
||||
onkeydown?: (event: KeyboardEvent) => void;
|
||||
}
|
||||
|
||||
let {
|
||||
anchor,
|
||||
autofocus = true,
|
||||
children,
|
||||
class: className,
|
||||
onclose,
|
||||
onkeydown,
|
||||
}: Props = $props();
|
||||
|
||||
const view = useView();
|
||||
|
||||
let panel = $state<HTMLElement | null>(null);
|
||||
let at = $state({ top: 0, left: 0 });
|
||||
let placed = $state(false);
|
||||
|
||||
/**
|
||||
* Placed by hand rather than by a floating layer: these panels are summoned
|
||||
* by a key press, and a floating layer wants a trigger element to hang off.
|
||||
*/
|
||||
function measure(): void {
|
||||
const node = panel;
|
||||
if (!node) return;
|
||||
|
||||
const boxes = [...document.querySelectorAll(anchor)].map((element) =>
|
||||
element.getBoundingClientRect(),
|
||||
);
|
||||
at = place(onScreen(boxes), node.getBoundingClientRect(), {
|
||||
width: window.innerWidth,
|
||||
height: window.innerHeight,
|
||||
});
|
||||
placed = true;
|
||||
}
|
||||
|
||||
function mount(node: HTMLElement) {
|
||||
panel = node;
|
||||
measure();
|
||||
if (autofocus) {
|
||||
(node.querySelector<HTMLElement>("input") ?? node).focus({
|
||||
preventScroll: true,
|
||||
});
|
||||
}
|
||||
|
||||
const outside = (event: PointerEvent): void => {
|
||||
if (!node.contains(event.target as Node)) onclose();
|
||||
};
|
||||
const reflow = (): void => measure();
|
||||
document.addEventListener("pointerdown", outside, true);
|
||||
window.addEventListener("resize", reflow);
|
||||
|
||||
return {
|
||||
destroy() {
|
||||
document.removeEventListener("pointerdown", outside, true);
|
||||
window.removeEventListener("resize", reflow);
|
||||
panel = null;
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function keys(event: KeyboardEvent): void {
|
||||
event.stopPropagation();
|
||||
if (event.key === "Escape" && !event.defaultPrevented) {
|
||||
event.preventDefault();
|
||||
onclose();
|
||||
return;
|
||||
}
|
||||
onkeydown?.(event);
|
||||
}
|
||||
|
||||
/** Re-measures once the body has grown or shrunk under the cursor. */
|
||||
export function reposition(): void {
|
||||
measure();
|
||||
}
|
||||
</script>
|
||||
|
||||
<Portal to={view.portal()}>
|
||||
<!-- svelte-ignore a11y_no_noninteractive_element_interactions -->
|
||||
<div
|
||||
class={cn(
|
||||
"fixed z-50 overflow-hidden rounded-xl border border-border bg-popover text-popover-foreground shadow-[0_16px_40px_-12px_rgb(0_0_0/45%)] outline-none",
|
||||
className,
|
||||
)}
|
||||
onkeydown={keys}
|
||||
role="dialog"
|
||||
style="top: {at.top}px; left: {at.left}px; opacity: {placed ? 1 : 0}"
|
||||
tabindex="-1"
|
||||
use:mount
|
||||
>
|
||||
{@render children()}
|
||||
</div>
|
||||
</Portal>
|
||||
Reference in new issue
Block a user