From 77bc9e13206ac2848c326b149a78ac83478bff1d Mon Sep 17 00:00:00 2001 From: Adib Hanna Date: Mon, 5 Oct 2026 14:31:53 -0500 Subject: [PATCH 1/2] fix(editor): scrolling a note in Edit mode no longer snaps back to the caret With the keyboard up and the caret in a note, every swipe was pulled back to the caret mid-fling, so a long note could not be scrolled past it (ZenNotes/zennotes#891, reported on Android; this shell has the same file). The keyboard-scroll helper watched the whole page with a MutationObserver and, on every DOM change, re-observed the toolbars and revealed the caret. CodeMirror draws lines as a note scrolls, so each swipe asked for a reveal. The caret is now revealed only when something can have covered it: the keyboard landing, the formatting toolbar or the selection bubble appearing or growing, or the viewport getting smaller. Touch-driven scrolling of the note, and the fling that follows it, belongs to the reader: no reveal runs during it, and it cancels the keyboard's pending settle steps. Both files are byte-identical to the Android shell's (ZenNotes/zennotesandroid#96). Verified on an iPhone 17 simulator (iOS 27.0) with real XCUITest swipes, caret on line 3 of a 250-line note, keyboard up: before, five swipes ended on line 19 with two to five snaps back per swipe; after, the same swipes reached line 232 with none. A tap on a line the keyboard will cover still lifts the caret above the toolbar, and typing near the bottom keeps it there. --- src/ui-mobile/editor-keyboard-scroll.test.ts | 222 +++++++++++++++++++ src/ui-mobile/editor-keyboard-scroll.ts | 101 +++++++-- 2 files changed, 310 insertions(+), 13 deletions(-) create mode 100644 src/ui-mobile/editor-keyboard-scroll.test.ts diff --git a/src/ui-mobile/editor-keyboard-scroll.test.ts b/src/ui-mobile/editor-keyboard-scroll.test.ts new file mode 100644 index 0000000..ab98aa1 --- /dev/null +++ b/src/ui-mobile/editor-keyboard-scroll.test.ts @@ -0,0 +1,222 @@ +import assert from 'node:assert/strict' +import test from 'node:test' +import { loadMobileModule } from '../../tooling/load-mobile-module.ts' + +type Listener = (...args: any[]) => void + +function define(name: string, value: unknown): void { + Object.defineProperty(globalThis, name, { configurable: true, writable: true, value }) +} + +/** The real module over a scripted DOM: frames, timers, observers and the clock move only when a test says so. */ +async function setup() { + const counts = { reveals: 0, refreshes: 0, observed: 0, released: 0 } + const frames = new Map() + const timers = new Map() + const windowListeners = new Map() + const documentListeners = new Map() + const keyboard = new Map() + const viewport = { width: 412, height: 839 } + const clock = { now: 1000 } + let ids = 0 + let overlays: object[] = [] + let onMutations: Listener = () => {} + let onSizes: Listener = () => {} + define('performance', { now: () => clock.now }) + define('requestAnimationFrame', (run: Listener) => { frames.set(++ids, run); return ids }) + define('cancelAnimationFrame', (id: number) => { frames.delete(id) }) + define('window', { + get innerWidth() { return viewport.width }, + get innerHeight() { return viewport.height }, + setTimeout: (run: Listener) => { timers.set(++ids, run); return ids }, + clearTimeout: (id: number) => { timers.delete(id) }, + addEventListener: (type: string, run: Listener) => { windowListeners.set(type, run) }, + removeEventListener: (type: string, run: Listener) => { if (windowListeners.get(type) === run) windowListeners.delete(type) } + }) + define('document', { + body: {}, + documentElement: {}, + querySelectorAll: () => overlays, + addEventListener: (type: string, run: Listener) => { documentListeners.set(type, run) }, + removeEventListener: (type: string, run: Listener) => { if (documentListeners.get(type) === run) documentListeners.delete(type) } + }) + define('MutationObserver', class { + constructor(run: Listener) { onMutations = run } + observe() {} + disconnect() { onMutations = () => {} } + }) + define('ResizeObserver', class { + constructor(run: Listener) { onSizes = run } + observe() { counts.observed++ } + unobserve() {} + disconnect() { onSizes = () => {} } + }) + const scroll = await loadMobileModule('./src/ui-mobile/editor-keyboard-scroll.ts', { + '@capacitor/keyboard': { + Keyboard: { + addListener: (event: string, run: Listener) => { + keyboard.set(event, run) + return Promise.resolve({ remove: () => { keyboard.delete(event) } }) + } + } + }, + '@zennotes/app-core/editor': { revealEditorCaret: () => { counts.reveals++; return true } }, + './editor-host': { + installMobileEditorHost: () => () => { counts.released++ }, + refreshMobileEditorHost: () => { counts.refreshes++ } + } + }) + const uninstall: () => void = scroll.installEditorKeyboardScroll() + const target = (inNote: boolean) => ({ closest: (selector: string) => (inNote && selector === '.cm-editor' ? {} : null) }) + return { + counts, + uninstall, + windowListeners, + documentListeners, + keyboardListeners: keyboard, + /** One rendering frame: observer reports first, as in the browser, then animation frames. */ + frame(sized: object[] = []) { + if (sized.length) onSizes(sized.map((node) => ({ target: node }))) + const due = [...frames.values()] + frames.clear() + for (const run of due) run(clock.now) + }, + timers() { + const due = [...timers.values()] + timers.clear() + for (const run of due) run() + }, + advance(ms: number) { clock.now += ms }, + mutate(type: 'childList' | 'attributes' = 'childList') { onMutations([{ type }]) }, + setOverlays(next: object[]) { overlays = next }, + resize(width: number, height: number) { + viewport.width = width + viewport.height = height + windowListeners.get('resize')?.() + }, + keyboard: (event: string) => keyboard.get(event)?.(), + touchStart: (inNote = true) => documentListeners.get('touchstart')?.({ target: target(inNote), touches: [{}] }), + touchEnd: () => documentListeners.get('touchend')?.({ target: target(true), touches: [] }), + scrollNote: () => documentListeners.get('scroll')?.({ target: target(true) }) + } +} + +test('DOM changes while a note scrolls never pull it back to the caret (ZenNotes/zennotes#891)', async () => { + const s = await setup() + // CodeMirror redraws lines as the note scrolls: every swipe is a run of childList batches. + for (let i = 0; i < 20; i++) { s.mutate(); s.frame() } + assert.equal(s.counts.reveals, 0) + + const toolbar = {} + s.setOverlays([toolbar]) + s.mutate() + s.frame([toolbar]) + s.frame() + assert.equal(s.counts.reveals, 1, 'the keyboard toolbar arriving keeps the caret above it') + for (let i = 0; i < 20; i++) { s.mutate(); s.frame() } + assert.equal(s.counts.reveals, 1, 'scrolling with the keyboard up stays where the reader put it') + assert.equal(s.counts.observed, 1, 'an overlay is observed once, not re-reported on every mutation') + s.uninstall() +}) + +test('an overlay growing reveals the caret; one leaving or a class change only re-measures', async () => { + const s = await setup() + const toolbar = {} + const bubble = {} + s.setOverlays([toolbar]) + s.mutate() + s.frame([toolbar]) + s.frame() + s.setOverlays([toolbar, bubble]) + s.mutate() + s.frame([bubble]) + s.frame() + s.frame([bubble]) + s.frame() + assert.equal(s.counts.reveals, 3) + + const refreshes = s.counts.refreshes + s.setOverlays([toolbar]) + s.mutate() + s.frame() + s.mutate('attributes') + s.frame() + assert.equal(s.counts.reveals, 3) + assert.equal(s.counts.refreshes, refreshes + 2) + s.uninstall() +}) + +test('only a smaller viewport reveals the caret, so closing the keyboard keeps the scroll', async () => { + const s = await setup() + s.resize(412, 527) + s.frame() + assert.equal(s.counts.reveals, 1, 'the keyboard opening shrinks the viewport') + s.resize(412, 839) + s.frame() + assert.equal(s.counts.reveals, 1, 'the keyboard closing grows it') + s.resize(915, 412) + s.frame() + assert.equal(s.counts.reveals, 2, 'rotating to landscape shortens it') + s.resize(412, 839) + s.frame() + assert.equal(s.counts.reveals, 3, 'rotating back narrows it, which rewraps lines') + s.uninstall() +}) + +test('the keyboard settles the caret into view, but never while the reader scrolls the note', async () => { + const s = await setup() + s.keyboard('keyboardDidShow') + s.frame() + s.timers() + assert.equal(s.counts.reveals, 4, 'one reveal per settle step while the keyboard lands') + + // A swipe begun right after the tap that raised the keyboard, which lands mid-swipe. + s.touchStart() + s.scrollNote() + s.keyboard('keyboardDidShow') + s.frame() + s.touchEnd() + for (let i = 0; i < 4; i++) { s.advance(100); s.scrollNote() } + s.advance(500) + s.timers() + assert.equal(s.counts.reveals, 4, 'not under the finger, through the fling, or once the fling stops') + + s.keyboard('keyboardDidShow') + s.frame() + assert.equal(s.counts.reveals, 5, 'the keyboard landing again after the fling reveals again') + s.advance(1000) + s.scrollNote() + s.keyboard('keyboardDidShow') + s.frame() + assert.equal(s.counts.reveals, 6, 'a scroll no finger started, such as a reveal, does not hold reveals back') + s.uninstall() +}) + +test('a long press that selects without scrolling still reveals what it selected', async () => { + const s = await setup() + const bubble = {} + s.touchStart() + s.setOverlays([bubble]) + s.mutate() + s.frame([bubble]) + s.frame() + assert.equal(s.counts.reveals, 1) + s.touchEnd() + + s.touchStart(false) + s.scrollNote() + s.resize(412, 527) + s.frame() + assert.equal(s.counts.reveals, 2, 'a finger outside the note is not the reader scrolling it') + s.uninstall() +}) + +test('uninstalling removes every listener and releases the editor host', async () => { + const s = await setup() + s.uninstall() + await Promise.resolve() + assert.equal(s.windowListeners.size, 0) + assert.equal(s.documentListeners.size, 0) + assert.equal(s.keyboardListeners.size, 0) + assert.equal(s.counts.released, 1) +}) diff --git a/src/ui-mobile/editor-keyboard-scroll.ts b/src/ui-mobile/editor-keyboard-scroll.ts index 41e1aff..5652d69 100644 --- a/src/ui-mobile/editor-keyboard-scroll.ts +++ b/src/ui-mobile/editor-keyboard-scroll.ts @@ -2,34 +2,109 @@ import { Keyboard } from '@capacitor/keyboard' import { revealEditorCaret } from '@zennotes/app-core/editor' import { installMobileEditorHost, refreshMobileEditorHost } from './editor-host' +const OVERLAYS = '.zn-editor-toolbar, [data-selection-toolbar]' +/** Longer than any pause between the scroll events of one drag and its fling. */ +const SCROLL_PAUSE_MS = 150 +let touching = false +let scrolledAt = Number.NEGATIVE_INFINITY +let settleFrame = 0 +let settleTimers: number[] = [] + export function revealCaretAboveKeyboard(): void { refreshMobileEditorHost() - revealEditorCaret() + // The note is never moved while the reader is moving it. + if (performance.now() - scrolledAt >= SCROLL_PAUSE_MS) revealEditorCaret() +} +function stopSettling(): void { + cancelAnimationFrame(settleFrame) + for (const timer of settleTimers) window.clearTimeout(timer) + settleFrame = 0 + settleTimers = [] } export function revealCaretAboveKeyboardSoon(): void { - requestAnimationFrame(revealCaretAboveKeyboard) - for (const ms of [150, 400, 800]) window.setTimeout(revealCaretAboveKeyboard, ms) + stopSettling() + settleFrame = requestAnimationFrame(revealCaretAboveKeyboard) + settleTimers = [150, 400, 800].map(ms => window.setTimeout(revealCaretAboveKeyboard, ms)) } +/** + * The caret is revealed only when something can have covered it: the keyboard + * landing, an overlay appearing or growing, the viewport getting smaller. Any + * other DOM change includes CodeMirror drawing lines as the note scrolls, and + * revealing on those pulled the reader back to the caret on every swipe, with + * the keyboard up or merely dismissed (ZenNotes/zennotes#891). + */ export function installEditorKeyboardScroll(): () => void { const release = installMobileEditorHost() + let overlays: Element[] = [] let frame = 0 - const refresh = (): void => { if (!frame) frame = requestAnimationFrame(() => { frame = 0; revealCaretAboveKeyboard() }) } - const sizes = new ResizeObserver(refresh) - const observe = (): void => { - sizes.disconnect() - for (const node of document.querySelectorAll('.zn-editor-toolbar, [data-selection-toolbar]')) sizes.observe(node) - refresh() + let reveal = false + const schedule = (withReveal: boolean): void => { + reveal ||= withReveal + if (!frame) frame = requestAnimationFrame(() => { + frame = 0 + if (reveal) revealCaretAboveKeyboard() + else refreshMobileEditorHost() + reveal = false + }) + } + // Observing an element reports its size once, so only overlays new to the + // set are observed; re-observing all of them reported each as resized. + const sizes = new ResizeObserver(() => schedule(true)) + const track = (): void => { + const next = [...document.querySelectorAll(OVERLAYS)] + const gone = overlays.filter(node => !next.includes(node)) + for (const node of gone) sizes.unobserve(node) + for (const node of next) if (!overlays.includes(node)) sizes.observe(node) + overlays = next + // Space an overlay leaves behind cannot hide the caret, only its inset needs dropping. + if (gone.length) schedule(false) } - const mutations = new MutationObserver(observe) + const mutations = new MutationObserver(records => { + track() + if (records.some(record => record.type === 'attributes')) schedule(false) + }) mutations.observe(document.body, { childList: true, subtree: true }) mutations.observe(document.documentElement, { attributes: true, attributeFilter: ['class'] }) - observe() + track() + // A taller viewport cannot hide a caret that was in view, and the keyboard + // closing must not undo a scroll made while it was open. + let { innerWidth: width, innerHeight: height } = window + const onResize = (): void => { + const shrank = window.innerWidth < width || window.innerHeight < height + width = window.innerWidth + height = window.innerHeight + schedule(shrank) + } + const inNote = (event: Event): boolean => !!(event.target as Element | null)?.closest?.('.cm-editor') + const onTouchStart = (event: TouchEvent): void => { touching ||= inNote(event) } + const onTouchEnd = (event: TouchEvent): void => { if (!event.touches.length) touching = false } + // The reader scrolls the note with a finger on it, and goes on scrolling for + // as long as its fling runs without a pause; no touch event arrives during a + // fling, so the scroll events themselves mark it. The keyboard often lands + // mid-swipe, and the settle steps it leaves would pull the note back to the + // caret the moment the fling stops. + const onScroll = (event: Event): void => { + if (!inNote(event)) return + const now = performance.now() + if (!touching && now - scrolledAt >= SCROLL_PAUSE_MS) return + scrolledAt = now + stopSettling() + } + const passive = { capture: true, passive: true } + document.addEventListener('touchstart', onTouchStart, passive) + document.addEventListener('touchend', onTouchEnd, passive) + document.addEventListener('touchcancel', onTouchEnd, passive) + document.addEventListener('scroll', onScroll, passive) const didShow = Keyboard.addListener('keyboardDidShow', revealCaretAboveKeyboardSoon) - window.addEventListener('resize', refresh) + window.addEventListener('resize', onResize) return () => { cancelAnimationFrame(frame) mutations.disconnect(); sizes.disconnect(); release() void didShow.then(handle => handle.remove()).catch(() => {}) - window.removeEventListener('resize', refresh) + window.removeEventListener('resize', onResize) + document.removeEventListener('touchstart', onTouchStart, true) + document.removeEventListener('touchend', onTouchEnd, true) + document.removeEventListener('touchcancel', onTouchEnd, true) + document.removeEventListener('scroll', onScroll, true) } } From 5734605b0d8f2c756e0f0d2f2eafdf3a19e5162e Mon Sep 17 00:00:00 2001 From: Adib Hanna Date: Mon, 5 Oct 2026 14:08:41 -0500 Subject: [PATCH 2/2] chore(deps): keep braces out of the production tree (GHSA-vfj7-8cjw-p6xm) A high-severity advisory for braces (stack exhaustion on deeply nested patterns, no fixed release) failed the production npm audit gate on every pull request. It reached production through @excalidraw/excalidraw, which lists sass 1.51.0 as a dependency; that sass pulls in chokidar 3, which pulls in braces. Excalidraw never loads sass at runtime: nothing in its build output imports it. Excalidraw's sass is overridden to 1.103.1, the version the desktop repo already carries. It watches files with a chokidar that has no braces, so braces stays only under build tools. No existing package changes version. --- package-lock.json | 407 ++++++++++++++++++++++++++++++++++++++++++---- package.json | 3 +- 2 files changed, 376 insertions(+), 34 deletions(-) diff --git a/package-lock.json b/package-lock.json index b45de6c..4c93a29 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1180,12 +1180,6 @@ "integrity": "sha512-Tbsj02wXCbqGmzdnXNk0SOF19ChhRU70BsroIi4Pm6Ehp56in6vch94mfbdQ17DozxkL3BAVjbZ4Qc1a0HFRAg==", "license": "MIT" }, - "node_modules/@excalidraw/excalidraw/node_modules/immutable": { - "version": "4.3.9", - "resolved": "https://registry.npmjs.org/immutable/-/immutable-4.3.9.tgz", - "integrity": "sha512-ObHy4YN7ycwZOUCLI1/6svfyAFu7vL8RhAvVu/bh/RZW9EPlOyDaQ9jDQWCtdqzaXUjgXZCW1migtHE7YI7UGQ==", - "license": "MIT" - }, "node_modules/@excalidraw/excalidraw/node_modules/points-on-curve": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/points-on-curve/-/points-on-curve-1.0.1.tgz", @@ -1210,23 +1204,6 @@ "integrity": "sha512-0mYKnYYe9ZcqMCWhUjItv/oHjvgEsfKvnUTg8sAtnHr3GVy7rGkXCb6d5cSyqrWqL4k81b9CPg3urd+T7aop3A==", "license": "MIT" }, - "node_modules/@excalidraw/excalidraw/node_modules/sass": { - "version": "1.51.0", - "resolved": "https://registry.npmjs.org/sass/-/sass-1.51.0.tgz", - "integrity": "sha512-haGdpTgywJTvHC2b91GSq+clTKGbtkkZmVAb82jZQN/wTy6qs8DdFm2lhEQbEwrY0QDRgSQ3xDurqM977C3noA==", - "license": "MIT", - "dependencies": { - "chokidar": ">=3.0.0 <4.0.0", - "immutable": "^4.0.0", - "source-map-js": ">=0.6.2 <2.0.0" - }, - "bin": { - "sass": "sass.js" - }, - "engines": { - "node": ">=12.0.0" - } - }, "node_modules/@excalidraw/laser-pointer": { "version": "1.3.1", "resolved": "https://registry.npmjs.org/@excalidraw/laser-pointer/-/laser-pointer-1.3.1.tgz", @@ -1806,6 +1783,294 @@ "url": "https://github.com/sponsors/oxc-project" } }, + "node_modules/@parcel/watcher": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/@parcel/watcher/-/watcher-2.6.0.tgz", + "integrity": "sha512-7FNeNl8NCE7aINx7WXiKQrPYZWC/hvrTsmk6zmxbI7LTXE7hVek/n8AfVgpe2y82zl3w0HvCHN0bVKMBoJcC0w==", + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "dependencies": { + "detect-libc": "^2.0.3", + "is-glob": "^4.0.3", + "node-addon-api": "^7.0.0", + "picomatch": "^4.0.4" + }, + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + }, + "optionalDependencies": { + "@parcel/watcher-android-arm64": "2.6.0", + "@parcel/watcher-darwin-arm64": "2.6.0", + "@parcel/watcher-darwin-x64": "2.6.0", + "@parcel/watcher-freebsd-x64": "2.6.0", + "@parcel/watcher-linux-arm-glibc": "2.6.0", + "@parcel/watcher-linux-arm-musl": "2.6.0", + "@parcel/watcher-linux-arm64-glibc": "2.6.0", + "@parcel/watcher-linux-arm64-musl": "2.6.0", + "@parcel/watcher-linux-x64-glibc": "2.6.0", + "@parcel/watcher-linux-x64-musl": "2.6.0", + "@parcel/watcher-win32-arm64": "2.6.0", + "@parcel/watcher-win32-x64": "2.6.0" + } + }, + "node_modules/@parcel/watcher-android-arm64": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/@parcel/watcher-android-arm64/-/watcher-android-arm64-2.6.0.tgz", + "integrity": "sha512-trgpLSCKRC/huFjXX/Smh+0sWe4+YtKfktIToiMl59ghz7z+qkH6kMvNnUbLyRs9N11t8l4svSCs1+5B3rOAhA==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-darwin-arm64": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/@parcel/watcher-darwin-arm64/-/watcher-darwin-arm64-2.6.0.tgz", + "integrity": "sha512-Y3QV0gl7Q1zbfueunkWIERICbEojQFCgpyG7YqOGNFLsckXyI1xu9mAIUpKY9QBYzBtSkN8dBPwd3yiAO9ovMw==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-darwin-x64": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/@parcel/watcher-darwin-x64/-/watcher-darwin-x64-2.6.0.tgz", + "integrity": "sha512-Ohv6OpzhUfKYD7Beb8kDvG0jbIxORCYY1JRdZnaBtnjjkJxgD7ZVL0nw2sCYd0yTMKTvz3nnTnOF3cDifK+kvw==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-freebsd-x64": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/@parcel/watcher-freebsd-x64/-/watcher-freebsd-x64-2.6.0.tgz", + "integrity": "sha512-5HmXvDgs8VK+74jF9y9/2FE3/OnlcKmc56tjmSrEuZjpSZOGL+fvAu+HKJBdPs9uwoP2hE6TlSUpXZ/C5jUFmQ==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-linux-arm-glibc": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm-glibc/-/watcher-linux-arm-glibc-2.6.0.tgz", + "integrity": "sha512-Ps/hui3A+vMbjdqlqAowK2ZL8+BO8dBjxeWXj6npTBs3jx4wWmbPpaLuqwrQrSqIVMCnpWo238bJ1U37GhQOYg==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-linux-arm-musl": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm-musl/-/watcher-linux-arm-musl-2.6.0.tgz", + "integrity": "sha512-9c6AUHgHoG+IY88MRIHupztQiQnrbqHYQjkM2btA+Bf/wQnQMuiD0Wfk1EVv3TlNT3x41uU71rn6E4xh/+zvkw==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-linux-arm64-glibc": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm64-glibc/-/watcher-linux-arm64-glibc-2.6.0.tgz", + "integrity": "sha512-yHRqS2owEXe6Hic9z6Mh1ECsCd+ODVOGvZDyciqRd21+v+o+DnXMOrw50DSpIG2sb8GPEaPPmfeCAWKPJdq46g==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-linux-arm64-musl": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm64-musl/-/watcher-linux-arm64-musl-2.6.0.tgz", + "integrity": "sha512-WhB2e/V7rqdHHWZusBSPuy5Ei8S6lSz6FE5TKKQz5h3a0O+C+mhY7vxU9b/stqvMb8beLnPY82ZrFTLKs+SrKA==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-linux-x64-glibc": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-x64-glibc/-/watcher-linux-x64-glibc-2.6.0.tgz", + "integrity": "sha512-ulGE6x6Oz6iAwg75T8YQSoguBWasniIbX+QWpaYPcCnDOpdWX3k+4xbEYPZVLxOuoJI+svJJPD3sEj8G7lrQ3A==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-linux-x64-musl": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-x64-musl/-/watcher-linux-x64-musl-2.6.0.tgz", + "integrity": "sha512-tkBYKt7YQrjIJWYDnto2YgO8MRkjlMTSNoRHzsXinBqbLdeOM3L32wPZJvIZxqaLMfSlS/4sUjH/6STVP/XDLw==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-win32-arm64": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/@parcel/watcher-win32-arm64/-/watcher-win32-arm64-2.6.0.tgz", + "integrity": "sha512-gIZAP23jaHjGWasY/TY6yL7NHFClf0Ga7FN+iINvk+KN94rhm94lYZhFsbYFNcA04/onvGD9kKmiJLJB2HbNwQ==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-win32-x64": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/@parcel/watcher-win32-x64/-/watcher-win32-x64-2.6.0.tgz", + "integrity": "sha512-cA+/pXV2YkfxlIcXOQ5fSWqAzzPyD78/x5qbK/I0vUkrlYHA8TIz+MXjAbGouguKVSI4bOmkTSJ1/poVSsgt+A==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher/node_modules/picomatch": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.7.tgz", + "integrity": "sha512-qcJu88Q2IWqJsDD529JKMdwGm/dvInW4HvQnRwiH9JtihJvzGOscDtHE3x1pBKeUOTysQ8kVmLnJ2kJu7yhcGA==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, "node_modules/@radix-ui/primitive": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/@radix-ui/primitive/-/primitive-1.1.1.tgz", @@ -3478,6 +3743,7 @@ "version": "3.1.3", "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", + "dev": true, "license": "ISC", "dependencies": { "normalize-path": "^3.0.0", @@ -3640,6 +3906,7 @@ "version": "2.3.0", "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==", + "dev": true, "license": "MIT", "engines": { "node": ">=8" @@ -3678,6 +3945,7 @@ "version": "3.0.3", "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "dev": true, "license": "MIT", "dependencies": { "fill-range": "^7.1.1" @@ -3826,6 +4094,7 @@ "version": "3.6.0", "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", + "dev": true, "license": "MIT", "dependencies": { "anymatch": "~3.1.2", @@ -3850,6 +4119,7 @@ "version": "5.1.2", "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, "license": "ISC", "dependencies": { "is-glob": "^4.0.1" @@ -4908,6 +5178,7 @@ "version": "7.1.1", "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "dev": true, "license": "MIT", "dependencies": { "to-regex-range": "^5.0.1" @@ -4966,6 +5237,7 @@ "version": "2.3.3", "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, "hasInstallScript": true, "license": "MIT", "optional": true, @@ -5370,6 +5642,12 @@ "pica": "^7.1.0" } }, + "node_modules/immutable": { + "version": "5.1.9", + "resolved": "https://registry.npmjs.org/immutable/-/immutable-5.1.9.tgz", + "integrity": "sha512-m8nVez3rwrgmWxtLMt1ZYXB2Lv7OKYn/disyxAlSDYAlKSlFoPPfIAmAM/M5xqL4m4C/wAPw7S2/CNaUii1Hxg==", + "license": "MIT" + }, "node_modules/import-meta-resolve": { "version": "4.2.0", "resolved": "https://registry.npmjs.org/import-meta-resolve/-/import-meta-resolve-4.2.0.tgz", @@ -5428,6 +5706,7 @@ "version": "2.1.0", "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", + "dev": true, "license": "MIT", "dependencies": { "binary-extensions": "^2.0.0" @@ -5481,6 +5760,7 @@ "version": "2.1.1", "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "devOptional": true, "license": "MIT", "engines": { "node": ">=0.10.0" @@ -5500,6 +5780,7 @@ "version": "4.0.3", "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "devOptional": true, "license": "MIT", "dependencies": { "is-extglob": "^2.1.1" @@ -5512,6 +5793,7 @@ "version": "7.0.0", "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true, "license": "MIT", "engines": { "node": ">=0.12.0" @@ -7120,6 +7402,13 @@ "double-bits": "^1.1.0" } }, + "node_modules/node-addon-api": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-7.1.1.tgz", + "integrity": "sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ==", + "license": "MIT", + "optional": true + }, "node_modules/node-releases": { "version": "2.0.55", "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.55.tgz", @@ -7134,6 +7423,7 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "dev": true, "license": "MIT", "engines": { "node": ">=0.10.0" @@ -7299,6 +7589,7 @@ "version": "2.3.2", "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz", "integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==", + "dev": true, "license": "MIT", "engines": { "node": ">=8.6" @@ -7745,6 +8036,7 @@ "version": "3.6.0", "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", + "dev": true, "license": "MIT", "dependencies": { "picomatch": "^2.2.1" @@ -8094,6 +8386,54 @@ "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", "license": "MIT" }, + "node_modules/sass": { + "version": "1.103.1", + "resolved": "https://registry.npmjs.org/sass/-/sass-1.103.1.tgz", + "integrity": "sha512-9icZURbP51S6S0QGoyaeqk9uB06GNWxsFYWfH5RgpFgqK5FA8tJcM3AdVxrZEVJ7dz+L87nG95gBKf4VuaMHGw==", + "license": "MIT", + "dependencies": { + "chokidar": "^5.0.0", + "immutable": "^5.1.5", + "source-map-js": ">=0.6.2 <2.0.0" + }, + "bin": { + "sass": "sass.js" + }, + "engines": { + "node": ">=20.19.0" + }, + "optionalDependencies": { + "@parcel/watcher": "^2.4.1" + } + }, + "node_modules/sass/node_modules/chokidar": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-5.0.0.tgz", + "integrity": "sha512-TQMmc3w+5AxjpL8iIiwebF73dRDF4fBIieAqGn9RGCWaEVwQ6Fb2cGe31Yns0RRIzii5goJ1Y7xbMwo1TxMplw==", + "license": "MIT", + "dependencies": { + "readdirp": "^5.0.0" + }, + "engines": { + "node": ">= 20.19.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/sass/node_modules/readdirp": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-5.1.1.tgz", + "integrity": "sha512-Kko+Y5XQ6fM+Ce3dq3m9YGxnacYZYl9cA1wZjaF3Vbry2L3i1qVg8+CAgNPsXRArPMUMCaOR7oa9Nqntc43JKA==", + "license": "MIT", + "engines": { + "node": ">= 20.19.0" + }, + "funding": { + "type": "individual", + "url": "https://paulmillr.com/funding/" + } + }, "node_modules/sax": { "version": "1.1.4", "resolved": "https://registry.npmjs.org/sax/-/sax-1.1.4.tgz", @@ -8237,6 +8577,16 @@ "integrity": "sha512-cEmp9QeXXRmjj/rVp9oyiqcvyocWab/HaoN4+bwFeZ7QzykJD6L3yD4v12K1x0tHpqRqVpJevN3gW7kyM39Bqg==", "license": "MIT" }, + "node_modules/string_decoder": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "dev": true, + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.2.0" + } + }, "node_modules/string-width": { "version": "4.2.3", "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", @@ -8252,16 +8602,6 @@ "node": ">=8" } }, - "node_modules/string_decoder": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", - "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", - "dev": true, - "license": "MIT", - "dependencies": { - "safe-buffer": "~5.2.0" - } - }, "node_modules/stringify-entities": { "version": "4.0.4", "resolved": "https://registry.npmjs.org/stringify-entities/-/stringify-entities-4.0.4.tgz", @@ -8515,6 +8855,7 @@ "version": "5.0.1", "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, "license": "MIT", "dependencies": { "is-number": "^7.0.0" diff --git a/package.json b/package.json index bbb300d..6823b35 100644 --- a/package.json +++ b/package.json @@ -88,7 +88,8 @@ }, "overrides": { "@excalidraw/excalidraw": { - "nanoid": "3.3.18" + "nanoid": "3.3.18", + "sass": "1.103.1" }, "@excalidraw/mermaid-to-excalidraw": { "@mermaid-js/parser": "1.2.1",