diff --git a/README.md b/README.md
index 2fd1f03..a1ecc26 100644
--- a/README.md
+++ b/README.md
@@ -1,37 +1,32 @@
-# RoamJS Extension Base
+
+
+
-Stock base for [RoamJS](https://roamjs.com) Roam Research extensions. **Fork this repo** to start a new extension.
+# Sticky Notes
-## What's included
+**Quick, ephemeral notes that float on top of your Roam Research graph.** Jot something down, drag it wherever you want, and when you're done—remove it. No clutter, no permanent page unless you want one.
-- **roamjs-components** — shared utilities, DOM helpers, queries, writes, and UI components
-- **Samepage build** — `samepage build` produces the Roam Depot–ready bundle
-- **Settings panel** — example `extensionAPI.settings.panel.create` with an Enable switch
-- **TypeScript** — tsconfig extending `@samepage/scripts`
-- **CI** — GitHub Actions to build on push/PR (uses RoamJS secrets for publish)
+[](https://deepwiki.com/RoamJS/sticky-notes)
-## After forking
+
-1. **Rename the repo** and update `package.json`:
- - `name`: your extension slug (e.g. `my-extension`)
- - `description`: one line describing the extension
+## What are Sticky Notes?
-2. **Implement in `src/index.ts`**:
- - Keep or replace the settings panel
- - Add your logic using `roamjs-components` (e.g. `createHTMLObserver`, `createBlock`, `renderToast`)
- - Return `{ unload }` to clean up on unload
+Sticky Notes are lightweight notes that sit in a layer above your Roam graph. They're meant for **quick, temporary capture**: ideas you're playing with, reminders for the current session, or scratch space while you work. When you delete a sticky note, it's gone—the block is removed from your graph. No archive, no cleanup later.
-3. **Optional**: Add React components under `src/components/` (see [autocomplete](https://github.com/RoamJS/autocomplete), [giphy](https://github.com/RoamJS/giphy) for examples).
+## Features
-4. **Secrets (for publish)** — in the forked repo, configure:
- - `ROAMJS_RELEASE_TOKEN`, `AWS_ACCESS_KEY_ID`, `AWS_SECRET_ACCESS_KEY`
- - `AWS_REGION`, `ROAMJS_PROXY` (vars)
+- **Full Roam inside every note** — Each sticky is a real Roam block. Use **tags** `#like-this`, **images**, **embeds**, **links**, and everything else you normally do in Roam.
+- **Drag anywhere** — Grab the note by the header bar and drag it anywhere on your screen. Position and size are remembered for your session.
+- **Resize** — Drag the corner or edge of a note to make it bigger or smaller.
+- **Minimize** — Collapse a note to just its title bar when you want it out of the way but still visible.
+- **Ephemeral by design** — Delete a note with the ✕ button and the block is removed from Roam. Perfect for throwaway thoughts and temporary scratch space.
-## Scripts
+## How to use
-- `npm start` — samepage dev (local development)
-- `npm run build:roam` — build for Roam (dry run; CI runs `npx samepage build`)
+1. **Create a sticky note** — Open the command palette (`Ctrl/Cmd + Shift + P`), run **"Sticky Notes: Create Sticky Note"**, and a new note appears.
+2. **Move it** — Click and drag the colored header bar to place the note wherever you like.
+3. **Edit** — Type in the note as you would in any Roam block. Use `#tags`, `/commands`, images, and links.
+4. **Remove it** — Click the **✕** on the note when you're done. The note and its content are deleted from your graph.
-## License
-
-MIT
+Sticky notes are stored under a single Roam page (`roam/js/sticky-note`) so they stay in your graph while they exist, but the extension is built so you can treat them as disposable: create, use, delete.
diff --git a/package.json b/package.json
index b2c6884..26ee491 100644
--- a/package.json
+++ b/package.json
@@ -1,7 +1,7 @@
{
- "name": "extension-base",
+ "name": "sticky-notes",
"version": "1.0.0",
- "description": "Stock RoamJS Roam Research Extension base — fork this repo to start new extensions.",
+ "description": "Sticky Notes Extension for Roam Research",
"main": "index.js",
"scripts": {
"postinstall": "patch-package",
@@ -10,7 +10,13 @@
"build:roam": "samepage build --dry",
"test": "samepage test"
},
- "keywords": ["roam", "roam-research", "roamjs"],
+ "keywords": [
+ "roam",
+ "roam-research",
+ "roamjs",
+ "sticky-notes",
+ "extension"
+ ],
"license": "MIT",
"dependencies": {
"roamjs-components": "^0.86.4"
diff --git a/src/index.ts b/src/index.ts
index 6ffa421..3afb7e5 100644
--- a/src/index.ts
+++ b/src/index.ts
@@ -1,6 +1,551 @@
+import addStyle from "roamjs-components/dom/addStyle";
import runExtension from "roamjs-components/util/runExtension";
+import { createBlock, createPage } from "roamjs-components/writes";
+
+type StickyNoteLayout = {
+ x: number;
+ y: number;
+ width: number;
+ height: number;
+ minimized: boolean;
+ rotation: number;
+};
+
+type StickyNoteLayouts = Record;
+
+type StickyNoteMeta = {
+ titleUid: string;
+ titleText: string;
+};
+
+type ReactLike = {
+ createElement: (component: unknown, props: Record) => unknown;
+};
+
+type ReactDomLike = {
+ render: (element: unknown, container: Element) => void;
+ unmountComponentAtNode: (container: Element) => boolean;
+};
+
+type RoamReactApi = {
+ Block: (props: {
+ uid: string;
+ open?: boolean;
+ zoomPath?: boolean;
+ zoomStartAfterUid?: string;
+ }) => unknown;
+};
+
+const PAGE_TITLE = "roam/js/sticky-note";
+const STORAGE_KEY = "roam-sticky-note-layouts";
+const COMMAND_LABEL = "Sticky Notes: Create Sticky Note";
+const NOTE_CLASS = "roamjs-sticky-note";
+const NOTE_MINIMIZED_CLASS = "roamjs-sticky-note--minimized";
+const NOTE_DRAGGING_CLASS = "roamjs-sticky-note--dragging";
+
+const logRoamMutationError = ({
+ operation,
+ uid,
+ error,
+}: {
+ operation: "updateBlock" | "deleteBlock";
+ uid: string;
+ error: unknown;
+}): void => {
+ console.error(`[sticky-note] Failed to ${operation} for block ${uid}`, error);
+};
+
+const randomRotation = (): number =>
+ Math.round((Math.random() * 3 - 1.5) * 10) / 10;
+
+const normalizeLayout = (layout: StickyNoteLayout): StickyNoteLayout => ({
+ ...layout,
+ rotation: Number.isFinite(layout.rotation) ? layout.rotation : randomRotation(),
+});
+
+const getLayouts = (): StickyNoteLayouts => {
+ const raw = window.localStorage.getItem(STORAGE_KEY);
+ if (!raw) {
+ return {};
+ }
+ try {
+ const parsed = JSON.parse(raw) as StickyNoteLayouts;
+ return Object.fromEntries(
+ Object.entries(parsed).map(([uid, layout]) => [uid, normalizeLayout(layout)])
+ );
+ } catch {
+ return {};
+ }
+};
+
+const setLayouts = (layouts: StickyNoteLayouts): void => {
+ window.localStorage.setItem(STORAGE_KEY, JSON.stringify(layouts));
+};
+
+const getPageUid = (): string | null => {
+ const result = window.roamAlphaAPI.q(
+ `[:find ?uid :where [?p :node/title "${PAGE_TITLE}"] [?p :block/uid ?uid]]`
+ ) as [string][];
+ return result.length ? result[0][0] : null;
+};
+
+const ensurePageUid = async (): Promise => {
+ const existing = getPageUid();
+ if (existing) {
+ return existing;
+ }
+ return createPage({ title: PAGE_TITLE });
+};
+
+const fetchStickyNoteUids = (): string[] => {
+ const result = window.roamAlphaAPI.q(
+ `[:find ?uid ?order
+ :where
+ [?p :node/title "${PAGE_TITLE}"]
+ [?p :block/children ?c]
+ [?c :block/uid ?uid]
+ [?c :block/order ?order]]`
+ ) as [string, number][];
+ return result
+ .sort((a, b) => a[1] - b[1])
+ .map((entry) => entry[0]);
+};
+
+const fetchBlockText = (uid: string): string => {
+ const result = window.roamAlphaAPI.q(
+ `[:find ?text
+ :in $ ?uid
+ :where
+ [?b :block/uid ?uid]
+ [(get-else $ ?b :block/string "") ?text]]`,
+ uid
+ ) as [string][];
+ return result.length ? result[0][0] : "";
+};
+
+const ensureStickyNoteMeta = async (noteUid: string): Promise => {
+ const noteText = fetchBlockText(noteUid).trim();
+
+ if (noteText) {
+ return {
+ titleUid: noteUid,
+ titleText: noteText,
+ };
+ }
+
+ try {
+ await window.roamAlphaAPI.updateBlock({
+ block: { uid: noteUid, string: "Sticky Note" },
+ });
+ } catch (error) {
+ logRoamMutationError({ operation: "updateBlock", uid: noteUid, error });
+ }
+ return {
+ titleUid: noteUid,
+ titleText: "Sticky Note",
+ };
+};
+
+const mountRoamBlock = ({
+ uid,
+ el,
+ open,
+}: {
+ uid: string;
+ el: HTMLElement;
+ open?: boolean;
+}): (() => void) => {
+ const globalWindow = window as unknown as {
+ React?: ReactLike;
+ ReactDOM?: ReactDomLike;
+ roamAlphaAPI?: { ui?: { react?: RoamReactApi } };
+ };
+ const React = globalWindow.React;
+ const ReactDOM = globalWindow.ReactDOM;
+ const Block = globalWindow.roamAlphaAPI?.ui?.react?.Block;
+ if (!React || !ReactDOM || !Block) {
+ return () => undefined;
+ }
+
+ ReactDOM.render(React.createElement(Block, { uid, open }), el);
+ return () => {
+ ReactDOM.unmountComponentAtNode(el);
+ };
+};
+
+const defaultLayout = (
+ index: number,
+ viewportWidth: number,
+ viewportHeight: number
+): StickyNoteLayout => {
+ const width = 240;
+ const height = 220;
+ const offset = 30 * index;
+ const x = Math.min(100 + offset, Math.max(20, viewportWidth - width - 20));
+ const y = Math.min(120 + offset, Math.max(20, viewportHeight - height - 20));
+ return {
+ x,
+ y,
+ width,
+ height,
+ minimized: false,
+ rotation: randomRotation(),
+ };
+};
+
+const applyLayout = (
+ note: HTMLElement,
+ layout: StickyNoteLayout
+): void => {
+ note.style.left = `${layout.x}px`;
+ note.style.top = `${layout.y}px`;
+ note.style.width = `${layout.width}px`;
+ note.style.height = `${layout.height}px`;
+ note.style.transform = `rotate(${layout.rotation}deg)`;
+ note.classList.toggle(NOTE_MINIMIZED_CLASS, layout.minimized);
+};
+
+const mutateLayout = (
+ layouts: StickyNoteLayouts,
+ uid: string,
+ next: Partial
+): void => {
+ const current = layouts[uid];
+ layouts[uid] = {
+ ...(current || defaultLayout(0, window.innerWidth, window.innerHeight)),
+ ...next,
+ };
+};
+
+const getStickyRenderedIdFromUid = ({
+ uid,
+ root = document,
+}: {
+ uid: string;
+ root?: ParentNode;
+}): string | null => {
+ const el = root.querySelector(
+ `.roamjs-sticky-note__embedded-root [id^="block-input-"][id$="-${uid}"]`
+ ) as HTMLElement | null;
+ return el?.id || null;
+};
+
+const getStickyWindowIdFromUid = ({
+ uid,
+ root = document,
+}: {
+ uid: string;
+ root?: ParentNode;
+}): string | null => {
+ const id = getStickyRenderedIdFromUid({ uid, root });
+ if (!id) {
+ return null;
+ }
+ const match = id.match(/^block-input-(.+)-([A-Za-z0-9_-]{9})$/);
+ return match ? match[1] : null;
+};
+
+const focusStickyRenderedUid = ({
+ uid,
+ root = document,
+}: {
+ uid: string;
+ root?: ParentNode;
+}): boolean => {
+ const windowId = getStickyWindowIdFromUid({ uid, root });
+ if (!windowId) {
+ return false;
+ }
+ window.roamAlphaAPI.ui.setBlockFocusAndSelection({
+ location: {
+ "block-uid": uid,
+ "window-id": windowId,
+ },
+ });
+ return true;
+};
+
+const focusStickyRenderedUidWithRetries = ({
+ uid,
+ root,
+}: {
+ uid: string;
+ root: ParentNode;
+}): void => {
+ const timerIds: number[] = [];
+ let focused = false;
+ [60, 140, 280, 520, 900].forEach((delay) => {
+ const timerId = window.setTimeout(() => {
+ if (focused) {
+ return;
+ }
+ focused = focusStickyRenderedUid({ uid, root });
+ if (focused) {
+ timerIds.forEach((id) => {
+ if (id !== timerId) {
+ window.clearTimeout(id);
+ }
+ });
+ }
+ }, delay);
+ timerIds.push(timerId);
+ });
+};
+
+const createStickyNoteElement = ({
+ uid,
+ layout,
+ layouts,
+ meta,
+ resizeObservers,
+ blockUnmounts,
+}: {
+ uid: string;
+ layout: StickyNoteLayout;
+ layouts: StickyNoteLayouts;
+ meta: StickyNoteMeta;
+ resizeObservers: Set;
+ blockUnmounts: Set<() => void>;
+}): HTMLDivElement => {
+ const note = document.createElement("div");
+ note.className = NOTE_CLASS;
+ note.dataset.uid = uid;
+ applyLayout(note, layout);
+
+ const header = document.createElement("div");
+ header.className = "roamjs-sticky-note__header";
+
+ const title = document.createElement("input");
+ title.className = "roamjs-sticky-note__title";
+ title.type = "text";
+ title.value = meta.titleText || "Sticky Note";
+ title.setAttribute("aria-label", "Sticky note title");
+ const measureTitleWidth = (value: string): number => {
+ const text = value.trim() || "Sticky Note";
+ const canvas = document.createElement("canvas");
+ const ctx = canvas.getContext("2d");
+ if (!ctx) {
+ return Math.max(36, text.length * 8);
+ }
+ const computed = window.getComputedStyle(title);
+ ctx.font = `${computed.fontWeight} ${computed.fontSize} ${computed.fontFamily}`;
+ return Math.max(36, Math.ceil(ctx.measureText(text).width) + 10);
+ };
+ const syncTitleWidth = (): void => {
+ title.style.width = `${measureTitleWidth(title.value)}px`;
+ };
+ syncTitleWidth();
+
+ const actions = document.createElement("div");
+ actions.className = "roamjs-sticky-note__actions";
+
+ const minimizeButton = document.createElement("button");
+ minimizeButton.type = "button";
+ minimizeButton.className = "bp3-button bp3-minimal roamjs-sticky-note__button";
+ minimizeButton.setAttribute(
+ "aria-label",
+ layout.minimized ? "Expand sticky note" : "Minimize sticky note"
+ );
+ minimizeButton.textContent = layout.minimized ? "▢" : "–";
+
+ const deleteButton = document.createElement("button");
+ deleteButton.type = "button";
+ deleteButton.className = "bp3-button bp3-minimal roamjs-sticky-note__button";
+ deleteButton.setAttribute("aria-label", "Delete sticky note");
+ deleteButton.textContent = "✕";
+
+ actions.append(minimizeButton, deleteButton);
+ header.append(title, actions);
+
+ const content = document.createElement("div");
+ content.className = "roamjs-sticky-note__content";
+
+ note.append(header, content);
+ syncTitleWidth();
+ window.requestAnimationFrame(syncTitleWidth);
+ window.setTimeout(syncTitleWidth, 80);
+
+ const blockContainer = document.createElement("div");
+ blockContainer.className = "roamjs-sticky-note__embedded-root";
+ content.append(blockContainer);
+ const unmountBlock = mountRoamBlock({ uid, el: blockContainer, open: true });
+ const hideEmbeddedRootTitle = (): void => {
+ const rootMain = blockContainer.querySelector(
+ ".rm-level-0 > .rm-block-main, .rm-block-main"
+ ) as HTMLElement | null;
+ if (rootMain) {
+ rootMain.style.display = "none";
+ }
+ };
+ hideEmbeddedRootTitle();
+ const embedObserver = new MutationObserver(() => hideEmbeddedRootTitle());
+ embedObserver.observe(blockContainer, { childList: true, subtree: true });
+ const cleanupEmbeddedBlock = (): void => {
+ embedObserver.disconnect();
+ unmountBlock();
+ };
+ blockUnmounts.add(cleanupEmbeddedBlock);
+
+ const commitTitle = (): void => {
+ const nextTitle = title.value.trim() || "Sticky Note";
+ if (title.value !== nextTitle) {
+ title.value = nextTitle;
+ }
+ syncTitleWidth();
+ void window.roamAlphaAPI
+ .updateBlock({
+ block: { uid: meta.titleUid, string: nextTitle },
+ })
+ .catch((error) => {
+ logRoamMutationError({
+ operation: "updateBlock",
+ uid: meta.titleUid,
+ error,
+ });
+ });
+ };
+
+ title.addEventListener("input", syncTitleWidth);
+ title.addEventListener("blur", commitTitle);
+ title.addEventListener("keydown", (event) => {
+ if (event.key === "Enter") {
+ event.preventDefault();
+ title.blur();
+ }
+ });
+
+ let dragOffsetX = 0;
+ let dragOffsetY = 0;
+ let isDragging = false;
+ let previousBodyUserSelect = "";
+ let resizePersistTimeout: number | null = null;
+
+ const scheduleLayoutPersistence = (): void => {
+ if (resizePersistTimeout) {
+ window.clearTimeout(resizePersistTimeout);
+ }
+ resizePersistTimeout = window.setTimeout(() => {
+ resizePersistTimeout = null;
+ setLayouts(layouts);
+ }, 250);
+ };
+
+ const onPointerMove = (event: PointerEvent): void => {
+ if (!isDragging) {
+ return;
+ }
+ const x = event.clientX - dragOffsetX;
+ const y = event.clientY - dragOffsetY;
+ note.style.left = `${x}px`;
+ note.style.top = `${y}px`;
+ mutateLayout(layouts, uid, { x, y });
+ };
+
+ const onPointerUp = (): void => {
+ if (!isDragging) {
+ return;
+ }
+ isDragging = false;
+ note.classList.remove(NOTE_DRAGGING_CLASS);
+ document.body.style.userSelect = previousBodyUserSelect;
+ document.removeEventListener("pointermove", onPointerMove);
+ document.removeEventListener("pointerup", onPointerUp);
+ setLayouts(layouts);
+ };
+
+ const onPointerDown = (event: PointerEvent): void => {
+ const target = event.target as HTMLElement;
+ if (target.closest("button, input, textarea, [contenteditable='true']")) {
+ return;
+ }
+ isDragging = true;
+ dragOffsetX = event.clientX - note.offsetLeft;
+ dragOffsetY = event.clientY - note.offsetTop;
+ note.classList.add(NOTE_DRAGGING_CLASS);
+ previousBodyUserSelect = document.body.style.userSelect;
+ document.body.style.userSelect = "none";
+ document.addEventListener("pointermove", onPointerMove);
+ document.addEventListener("pointerup", onPointerUp);
+ };
+
+ header.addEventListener("pointerdown", onPointerDown);
+
+ minimizeButton.addEventListener("click", () => {
+ const nextMinimized = !note.classList.contains(NOTE_MINIMIZED_CLASS);
+ note.classList.toggle(NOTE_MINIMIZED_CLASS, nextMinimized);
+ minimizeButton.textContent = nextMinimized ? "▢" : "–";
+ minimizeButton.setAttribute(
+ "aria-label",
+ nextMinimized ? "Expand sticky note" : "Minimize sticky note"
+ );
+ mutateLayout(layouts, uid, { minimized: nextMinimized });
+ setLayouts(layouts);
+ window.requestAnimationFrame(() => {
+ const activeElement = document.activeElement as HTMLElement | null;
+ activeElement?.blur();
+ });
+ });
+
+ const resizeObserver = new ResizeObserver((entries) => {
+ entries.forEach((entry) => {
+ if (!(entry.target instanceof HTMLElement)) {
+ return;
+ }
+ if (entry.target.classList.contains(NOTE_MINIMIZED_CLASS)) {
+ return;
+ }
+ mutateLayout(layouts, uid, {
+ width: Math.round(entry.target.offsetWidth),
+ height: Math.round(entry.target.offsetHeight),
+ });
+ scheduleLayoutPersistence();
+ });
+ });
+ resizeObservers.add(resizeObserver);
+ resizeObserver.observe(note);
+
+ deleteButton.addEventListener("click", async () => {
+ deleteButton.disabled = true;
+ try {
+ await window.roamAlphaAPI.deleteBlock({ block: { uid } });
+ } catch (error) {
+ deleteButton.disabled = false;
+ logRoamMutationError({ operation: "deleteBlock", uid, error });
+ return;
+ }
+
+ if (resizePersistTimeout) {
+ window.clearTimeout(resizePersistTimeout);
+ resizePersistTimeout = null;
+ }
+ resizeObserver.disconnect();
+ resizeObservers.delete(resizeObserver);
+ cleanupEmbeddedBlock();
+ blockUnmounts.delete(cleanupEmbeddedBlock);
+ note.remove();
+ delete layouts[uid];
+ setLayouts(layouts);
+ });
+
+ return note;
+};
export default runExtension(async ({ extensionAPI }) => {
+ const stickyNoteDebug = window as unknown as {
+ roamjsStickyNoteDebug?: {
+ getStickyRenderedIdFromUid: typeof getStickyRenderedIdFromUid;
+ getStickyWindowIdFromUid: typeof getStickyWindowIdFromUid;
+ focusStickyRenderedUid: typeof focusStickyRenderedUid;
+ focusStickyRenderedUidWithRetries: typeof focusStickyRenderedUidWithRetries;
+ };
+ };
+ stickyNoteDebug.roamjsStickyNoteDebug = {
+ getStickyRenderedIdFromUid,
+ getStickyWindowIdFromUid,
+ focusStickyRenderedUid,
+ focusStickyRenderedUidWithRetries,
+ };
+
extensionAPI.settings.panel.create({
tabTitle: "Extension",
settings: [
@@ -16,12 +561,256 @@ export default runExtension(async ({ extensionAPI }) => {
const enabled = extensionAPI.settings.get("enabled") as boolean | undefined;
if (enabled === false) return;
- // Add your extension logic here.
- // Use roamjs-components: dom/*, queries/*, writes/*, util/*, components/*
+ const style = addStyle(
+ `
+ .${NOTE_CLASS} {
+ position: absolute;
+ background: #f8e88b;
+ border: 1px solid #e5d671;
+ border-radius: 10px;
+ box-shadow: 0 6px 18px rgba(0, 0, 0, 0.18);
+ display: flex;
+ flex-direction: column;
+ resize: both;
+ overflow: hidden;
+ min-width: 180px;
+ min-height: 160px;
+ pointer-events: auto;
+ z-index: 1;
+ transition: box-shadow 120ms ease;
+ transform-origin: center center;
+ }
+
+ .${NOTE_DRAGGING_CLASS} {
+ box-shadow: 0 14px 28px rgba(0, 0, 0, 0.32);
+ }
+
+ .${NOTE_CLASS}::after {
+ content: "";
+ position: absolute;
+ inset: 0;
+ border-radius: 10px;
+ pointer-events: none;
+ box-shadow: inset 0 0 0 1px rgba(255, 255, 255, 0.35);
+ }
+
+ .${NOTE_CLASS} .roamjs-sticky-note__header {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ padding: 8px 10px;
+ cursor: grab;
+ user-select: none;
+ font-weight: 600;
+ color: #5a4b1d;
+ background: rgba(255, 255, 255, 0.4);
+ }
+
+ .${NOTE_CLASS} .roamjs-sticky-note__header:active {
+ cursor: grabbing;
+ }
+
+ .${NOTE_CLASS} .roamjs-sticky-note__title {
+ font-size: 13px;
+ font-weight: 600;
+ color: #5a4b1d;
+ background: transparent;
+ border: none;
+ outline: none;
+ flex: 0 0 auto;
+ min-width: 0;
+ max-width: calc(100% - 60px);
+ margin-right: 8px;
+ padding: 0 1px;
+ box-sizing: border-box;
+ }
+
+ .${NOTE_CLASS} .roamjs-sticky-note__title:focus {
+ background: rgba(255, 255, 255, 0.55);
+ border-radius: 4px;
+ }
+
+ .${NOTE_CLASS} .roamjs-sticky-note__actions {
+ display: flex;
+ gap: 4px;
+ }
+
+ .${NOTE_CLASS} .roamjs-sticky-note__button {
+ min-width: 24px;
+ height: 24px;
+ padding: 0;
+ color: #5a4b1d;
+ }
+
+ .${NOTE_CLASS} .roamjs-sticky-note__button:focus,
+ .${NOTE_CLASS} .roamjs-sticky-note__button:focus-visible,
+ .${NOTE_CLASS} .roamjs-sticky-note__button.bp3-active,
+ .${NOTE_CLASS} .roamjs-sticky-note__button.bp3-active:focus {
+ outline: none !important;
+ box-shadow: none !important;
+ }
+
+ .${NOTE_CLASS} .roamjs-sticky-note__content {
+ padding: 6px 10px 12px;
+ flex: 1;
+ overflow: auto;
+ }
+
+ .${NOTE_CLASS} .roamjs-sticky-note__embedded-root {
+ width: 100%;
+ min-width: 0;
+ }
+
+ .${NOTE_CLASS} .roamjs-sticky-note__embedded-root > .roam-block-container {
+ width: 100%;
+ min-width: 0;
+ }
+
+ .${NOTE_CLASS} .roamjs-sticky-note__embedded-root > .roam-block-container > .rm-block-main {
+ display: none;
+ }
+
+ .${NOTE_CLASS} .roamjs-sticky-note__embedded-root .roam-block-container > .rm-block-children.rm-level-1 {
+ margin-left: 0 !important;
+ }
+
+ .${NOTE_CLASS} .roamjs-sticky-note__embedded-root .roam-block-container,
+ .${NOTE_CLASS} .roamjs-sticky-note__embedded-root .rm-level-0,
+ .${NOTE_CLASS} .roamjs-sticky-note__embedded-root .rm-block-main,
+ .${NOTE_CLASS} .roamjs-sticky-note__embedded-root .roam-block {
+ width: 100%;
+ min-width: 0;
+ max-width: 100%;
+ }
+
+ .${NOTE_CLASS} .roamjs-sticky-note__embedded-root .rm-block-separator {
+ display: none;
+ }
+
+ .${NOTE_CLASS} .roamjs-sticky-note__embedded-root .rm-multibar {
+ display: none;
+ }
+
+ .${NOTE_MINIMIZED_CLASS} {
+ height: auto !important;
+ min-height: 0 !important;
+ resize: none;
+ }
+
+ .${NOTE_MINIMIZED_CLASS} .roamjs-sticky-note__content {
+ display: none;
+ }
+ `,
+ "roamjs-sticky-note-style"
+ );
+
+ const container = document.createElement("div");
+ container.id = "roamjs-sticky-note-container";
+ container.style.position = "fixed";
+ container.style.left = "0";
+ container.style.top = "0";
+ container.style.width = "100%";
+ container.style.height = "100%";
+ container.style.pointerEvents = "none";
+ container.style.zIndex = "19";
+ document.body.append(container);
+
+ const layouts = getLayouts();
+ const resizeObservers = new Set();
+ const blockUnmounts = new Set<() => void>();
+ await ensurePageUid();
+ const existingUids = fetchStickyNoteUids();
+
+ for (const [index, uid] of existingUids.entries()) {
+ const meta = await ensureStickyNoteMeta(uid);
+ const layout =
+ layouts[uid] ||
+ defaultLayout(index, window.innerWidth, window.innerHeight);
+ layouts[uid] = layout;
+ const note = createStickyNoteElement({
+ uid,
+ layout,
+ layouts,
+ meta,
+ resizeObservers,
+ blockUnmounts,
+ });
+ container.append(note);
+ }
+ for (const key of Object.keys(layouts)) {
+ if (!existingUids.includes(key)) {
+ delete layouts[key];
+ }
+ }
+ setLayouts(layouts);
+
+ const createStickyNote = async (): Promise => {
+ const pageUid = await ensurePageUid();
+ let uid: string | null = null;
+ let firstContentUid: string | null = null;
+ try {
+ uid = await createBlock({
+ parentUid: pageUid,
+ order: "last",
+ node: { text: "Sticky Note" },
+ });
+ firstContentUid = await createBlock({
+ parentUid: uid,
+ order: 0,
+ node: { text: "" },
+ });
+
+ const layout = defaultLayout(
+ Object.keys(layouts).length,
+ window.innerWidth,
+ window.innerHeight
+ );
+ layouts[uid] = layout;
+ setLayouts(layouts);
+ const note = createStickyNoteElement({
+ uid,
+ layout,
+ layouts,
+ meta: { titleUid: uid, titleText: "Sticky Note" },
+ resizeObservers,
+ blockUnmounts,
+ });
+ container.append(note);
+ focusStickyRenderedUidWithRetries({ uid: firstContentUid, root: note });
+ } catch (error) {
+ if (uid) {
+ try {
+ await window.roamAlphaAPI.deleteBlock({ block: { uid } });
+ } catch (rollbackError) {
+ logRoamMutationError({
+ operation: "deleteBlock",
+ uid,
+ error: rollbackError,
+ });
+ }
+ delete layouts[uid];
+ setLayouts(layouts);
+ }
+ console.error("[sticky-note] Failed to create sticky note", error);
+ throw error;
+ }
+ };
+
+ await extensionAPI.ui.commandPalette.addCommand({
+ label: COMMAND_LABEL,
+ callback: createStickyNote,
+ });
return {
+ elements: [style],
+ commands: [COMMAND_LABEL],
unload: () => {
- // Clean up observers, listeners, command palette, etc.
+ delete stickyNoteDebug.roamjsStickyNoteDebug;
+ resizeObservers.forEach((observer) => observer.disconnect());
+ resizeObservers.clear();
+ blockUnmounts.forEach((unmount) => unmount());
+ blockUnmounts.clear();
+ container.remove();
},
};
});