diff --git a/lib/folk-drawfast.ts b/lib/folk-drawfast.ts index a4d61034..3ae63954 100644 --- a/lib/folk-drawfast.ts +++ b/lib/folk-drawfast.ts @@ -584,6 +584,8 @@ export class FolkDrawfast extends FolkShape { #brushSize = 4; #tool = "pen"; // pen | eraser #isDrawing = false; + #activePointerId: number | null = null; + #penIsActive = false; #isGenerating = false; #autoGenerate = false; #autoDebounceTimer: ReturnType | null = null; @@ -739,15 +741,27 @@ export class FolkDrawfast extends FolkShape { }); providerSelect.addEventListener("pointerdown", (e) => e.stopPropagation()); - // Drawing events + // Drawing events — pen takes priority over touch (palm rejection). + // If a pen pointer is active, touch pointers are ignored until the pen lifts. this.#canvas.addEventListener("pointerdown", (e) => { + // Palm rejection: when pen is down, ignore touch pointers entirely + if (this.#penIsActive && e.pointerType === "touch") { + e.preventDefault(); + return; + } + // Only one drawing pointer at a time (pinch gestures should bubble) + if (this.#isDrawing) return; + e.stopPropagation(); e.preventDefault(); this.#isDrawing = true; + this.#activePointerId = e.pointerId; + if (e.pointerType === "pen") this.#penIsActive = true; this.#canvas!.setPointerCapture(e.pointerId); const pos = this.#getCanvasPos(e); + const pressure = e.pointerType === "pen" ? (e.pressure || 0.5) : 0.5; this.#currentStroke = { - points: [{ ...pos, pressure: e.pressure || 0.5 }], + points: [{ ...pos, pressure }], color: this.#tool === "eraser" ? "#ffffff" : this.#color, size: this.#tool === "eraser" ? this.#brushSize * 3 : this.#brushSize, tool: this.#tool, @@ -756,15 +770,21 @@ export class FolkDrawfast extends FolkShape { this.#canvas.addEventListener("pointermove", (e) => { if (!this.#isDrawing || !this.#currentStroke) return; + if (e.pointerId !== this.#activePointerId) return; e.stopPropagation(); const pos = this.#getCanvasPos(e); - this.#currentStroke.points.push({ ...pos, pressure: e.pressure || 0.5 }); + // Pen: honor real pressure. Mouse/touch: use constant 0.5. + const pressure = e.pointerType === "pen" ? (e.pressure || 0.5) : 0.5; + this.#currentStroke.points.push({ ...pos, pressure }); this.#drawStroke(this.#currentStroke); }); const endDraw = (e: PointerEvent) => { if (!this.#isDrawing) return; + if (e.pointerId !== this.#activePointerId) return; this.#isDrawing = false; + this.#activePointerId = null; + if (e.pointerType === "pen") this.#penIsActive = false; if (this.#currentStroke && this.#currentStroke.points.length > 0) { // Try gesture recognition before adding stroke let gestureResult: RecognizeResult | null = null; @@ -799,6 +819,7 @@ export class FolkDrawfast extends FolkShape { this.#canvas.addEventListener("pointerup", endDraw); this.#canvas.addEventListener("pointerleave", endDraw); + this.#canvas.addEventListener("pointercancel", endDraw); // Generate button this.#generateBtn.addEventListener("click", (e) => { diff --git a/lib/folk-makereal.ts b/lib/folk-makereal.ts index dea086c8..c22f84d5 100644 --- a/lib/folk-makereal.ts +++ b/lib/folk-makereal.ts @@ -344,6 +344,8 @@ export class FolkMakereal extends FolkShape { #brushSize = 4; #tool = "pen"; #isDrawing = false; + #activePointerId: number | null = null; + #penIsActive = false; #isGenerating = false; #framework = "html"; #lastHtml: string | null = null; @@ -464,15 +466,23 @@ export class FolkMakereal extends FolkShape { }); frameworkSelect.addEventListener("pointerdown", (e) => e.stopPropagation()); - // Drawing events + // Drawing events — pen takes priority over touch (palm rejection). this.#canvas.addEventListener("pointerdown", (e) => { + if (this.#penIsActive && e.pointerType === "touch") { + e.preventDefault(); + return; + } + if (this.#isDrawing) return; e.stopPropagation(); e.preventDefault(); this.#isDrawing = true; + this.#activePointerId = e.pointerId; + if (e.pointerType === "pen") this.#penIsActive = true; this.#canvas!.setPointerCapture(e.pointerId); const pos = this.#getCanvasPos(e); + const pressure = e.pointerType === "pen" ? (e.pressure || 0.5) : 0.5; this.#currentStroke = { - points: [{ ...pos, pressure: e.pressure || 0.5 }], + points: [{ ...pos, pressure }], color: this.#tool === "eraser" ? "#ffffff" : this.#color, size: this.#tool === "eraser" ? this.#brushSize * 3 : this.#brushSize, tool: this.#tool, @@ -481,15 +491,20 @@ export class FolkMakereal extends FolkShape { this.#canvas.addEventListener("pointermove", (e) => { if (!this.#isDrawing || !this.#currentStroke) return; + if (e.pointerId !== this.#activePointerId) return; e.stopPropagation(); const pos = this.#getCanvasPos(e); - this.#currentStroke.points.push({ ...pos, pressure: e.pressure || 0.5 }); + const pressure = e.pointerType === "pen" ? (e.pressure || 0.5) : 0.5; + this.#currentStroke.points.push({ ...pos, pressure }); this.#drawStroke(this.#currentStroke); }); - const endDraw = () => { + const endDraw = (e: PointerEvent) => { if (!this.#isDrawing) return; + if (e.pointerId !== this.#activePointerId) return; this.#isDrawing = false; + this.#activePointerId = null; + if (e.pointerType === "pen") this.#penIsActive = false; if (this.#currentStroke && this.#currentStroke.points.length > 0) { this.#strokes.push(this.#currentStroke); } @@ -498,6 +513,7 @@ export class FolkMakereal extends FolkShape { this.#canvas.addEventListener("pointerup", endDraw); this.#canvas.addEventListener("pointerleave", endDraw); + this.#canvas.addEventListener("pointercancel", endDraw); // Generate button this.#generateBtn.addEventListener("click", (e) => { diff --git a/modules/rmaps/components/folk-map-viewer.ts b/modules/rmaps/components/folk-map-viewer.ts index a051be5b..fb3b539a 100644 --- a/modules/rmaps/components/folk-map-viewer.ts +++ b/modules/rmaps/components/folk-map-viewer.ts @@ -676,10 +676,28 @@ class FolkMapViewer extends HTMLElement { const sheetHandle = this.shadow.getElementById("sheet-handle"); if (sheet && sheetHandle) { sheetHandle.addEventListener("click", () => sheet.classList.toggle("expanded")); + // Pointer events: unified mouse/touch/pen, so pen users get drag-to-expand too let startY = 0; + let startX = 0; + let activePointer: number | null = null; let sheetWasExpanded = false; - sheetHandle.addEventListener("touchstart", (e: Event) => { const te = e as TouchEvent; startY = te.touches[0].clientY; sheetWasExpanded = sheet.classList.contains("expanded"); }, { passive: true }); - sheetHandle.addEventListener("touchend", (e: Event) => { const te = e as TouchEvent; const dy = te.changedTouches[0].clientY - startY; if (sheetWasExpanded && dy > 40) sheet.classList.remove("expanded"); else if (!sheetWasExpanded && dy < -40) sheet.classList.add("expanded"); }, { passive: true }); + sheetHandle.addEventListener("pointerdown", (e: PointerEvent) => { + activePointer = e.pointerId; + startY = e.clientY; + startX = e.clientX; + sheetWasExpanded = sheet.classList.contains("expanded"); + sheetHandle.setPointerCapture?.(e.pointerId); + }); + sheetHandle.addEventListener("pointerup", (e: PointerEvent) => { + if (activePointer !== e.pointerId) return; + activePointer = null; + const dy = e.clientY - startY; + const dx = Math.abs(e.clientX - startX); + if (dx > 50) return; // Horizontal swipe — ignore + if (sheetWasExpanded && dy > 40) sheet.classList.remove("expanded"); + else if (!sheetWasExpanded && dy < -40) sheet.classList.add("expanded"); + }); + sheetHandle.addEventListener("pointercancel", () => { activePointer = null; }); } // Restricted controls — show toast diff --git a/modules/rmaps/mod.ts b/modules/rmaps/mod.ts index 5afbe1d1..10db6fd9 100644 --- a/modules/rmaps/mod.ts +++ b/modules/rmaps/mod.ts @@ -425,7 +425,7 @@ routes.get("/", (c) => { body: ``, scripts: ` - `, + `, styles: ``, })); }); @@ -444,7 +444,7 @@ routes.get("/:room", (c) => { body: ``, scripts: ` - `, + `, })); }); diff --git a/modules/rnotes/components/folk-notes-app.ts b/modules/rnotes/components/folk-notes-app.ts index 6c65e0f5..9ed45936 100644 --- a/modules/rnotes/components/folk-notes-app.ts +++ b/modules/rnotes/components/folk-notes-app.ts @@ -182,6 +182,7 @@ export class FolkNotesApp extends HTMLElement { private _loading = false; private _uploadOpen = false; private _uploadStatus = ''; + private _mobileView: 'vaults' | 'files' | 'preview' = 'vaults'; private _shadow: ShadowRoot; constructor() { @@ -233,6 +234,7 @@ export class FolkNotesApp extends HTMLElement { this._selectedNotePath = null; this._noteContent = ''; this._notes = []; + this._mobileView = 'files'; this._render(); try { const base = this._getApiBase(); @@ -249,6 +251,7 @@ export class FolkNotesApp extends HTMLElement { if (!this._selectedVaultId) return; this._selectedNotePath = path; this._noteContent = ''; + this._mobileView = 'preview'; this._render(); try { const base = this._getApiBase(); @@ -312,6 +315,7 @@ export class FolkNotesApp extends HTMLElement { /* Layout */ .layout { display: flex; flex: 1; overflow: hidden; } +.mobile-back { display: none; background: none; border: none; color: #14b8a6; cursor: pointer; font-size: 14px; padding: 4px 8px; margin-right: 6px; } /* Left sidebar */ .sidebar { width: 250px; flex-shrink: 0; background: #111; border-right: 1px solid #222; display: flex; flex-direction: column; overflow: hidden; } @@ -394,6 +398,34 @@ export class FolkNotesApp extends HTMLElement { /* Loading spinner */ .spinner { display: inline-block; width: 16px; height: 16px; border: 2px solid #333; border-top-color: #14b8a6; border-radius: 50%; animation: spin 0.7s linear infinite; } @keyframes spin { to { transform: rotate(360deg); } } + +/* ── Mobile: drill-down layout (vaults → files → preview) ── */ +@media (max-width: 767px) { + :host { font-size: 15px; } + .toolbar { padding: 10px 12px; gap: 8px; } + .toolbar-title { font-size: 13px; } + .btn { padding: 8px 12px; font-size: 14px; min-height: 40px; } + .search-input { font-size: 16px; min-height: 40px; } + .layout { position: relative; } + .sidebar, .file-tree, .preview { + position: absolute; inset: 0; width: 100%; height: 100%; + border-right: none; + } + .layout[data-view="vaults"] .sidebar { z-index: 3; } + .layout[data-view="files"] .file-tree { z-index: 3; } + .layout[data-view="preview"] .preview { z-index: 3; } + .layout[data-view="vaults"] .file-tree, + .layout[data-view="vaults"] .preview, + .layout[data-view="files"] .sidebar, + .layout[data-view="files"] .preview, + .layout[data-view="preview"] .sidebar, + .layout[data-view="preview"] .file-tree { display: none; } + .mobile-back { display: inline-flex; align-items: center; min-height: 40px; min-width: 40px; } + .vault-item, .note-item, .folder-header { padding: 12px 14px; min-height: 44px; } + .preview-body { padding: 18px 16px; } + .dialog { padding: 20px; width: 100%; max-width: 92vw; border-radius: 12px; } + .field input, .field select { font-size: 16px; padding: 10px; min-height: 40px; } +} `; } @@ -404,7 +436,7 @@ export class FolkNotesApp extends HTMLElement { ${ICON_SEARCH} ${escHtml(this._space || 'rNotes')} -
+
${this._renderSidebar()} ${this._renderFileTree()} ${this._renderPreview(vault)} @@ -487,6 +519,7 @@ ${this._uploadOpen ? this._renderUploadDialog() : ''} return `
@@ -513,6 +546,7 @@ ${this._uploadOpen ? this._renderUploadDialog() : ''} return `
+
${escHtml(this._selectedNotePath)} ${modTime ? `· ${modTime}` : ''}
${tags ? `
${tags}
` : ''}
@@ -610,6 +644,14 @@ ${this._uploadOpen ? this._renderUploadDialog() : ''} this._render(); }); + // Mobile drill-down back buttons + $('mb-back-vaults')?.addEventListener('click', () => { + this._mobileView = 'vaults'; this._render(); + }); + $('mb-back-files')?.addEventListener('click', () => { + this._mobileView = 'files'; this._render(); + }); + // Wikilinks in preview this._shadow.querySelectorAll('a.wikilink').forEach(a => { a.addEventListener('click', (e) => { diff --git a/modules/rnotes/mod.ts b/modules/rnotes/mod.ts index 35e34b37..44ff26c7 100644 --- a/modules/rnotes/mod.ts +++ b/modules/rnotes/mod.ts @@ -160,7 +160,7 @@ routes.get("/", (c) => { modules: getModuleInfoList(), theme: "dark", body: ``, - scripts: ``, + scripts: ``, }), ); }); diff --git a/modules/rphotos/components/folk-photo-gallery.ts b/modules/rphotos/components/folk-photo-gallery.ts index 9ebc6d15..5f181a6c 100644 --- a/modules/rphotos/components/folk-photo-gallery.ts +++ b/modules/rphotos/components/folk-photo-gallery.ts @@ -318,6 +318,23 @@ class FolkPhotoGallery extends HTMLElement { this.render(); } + private currentAssetList(): Asset[] { + return this.selectedAlbum ? this.albumAssets : this.assets; + } + + private navigateLightbox(dir: 1 | -1) { + if (!this.lightboxAsset) return; + const list = this.currentAssetList(); + const idx = list.findIndex((a) => a.id === this.lightboxAsset!.id); + if (idx < 0) return; + const next = list[idx + dir]; + if (next) { + this.lightboxAsset = next; + this._history.push("lightbox", { assetId: next.id }); + this.render(); + } + } + private goBack() { const prev = this._history.back(); if (!prev) return; @@ -527,6 +544,36 @@ class FolkPhotoGallery extends HTMLElement { max-height: 80vh; object-fit: contain; border-radius: 8px; + touch-action: none; + user-select: none; + -webkit-user-select: none; + -webkit-user-drag: none; + transform-origin: center center; + transition: transform 0.2s ease; + will-change: transform; + } + .lightbox img.dragging { transition: none; } + .lightbox-nav { + position: absolute; + top: 50%; + transform: translateY(-50%); + background: rgba(255,255,255,0.1); + border: none; + color: #fff; + font-size: 28px; + width: 48px; + height: 48px; + border-radius: 50%; + cursor: pointer; + display: flex; + align-items: center; + justify-content: center; + } + .lightbox-nav:hover { background: rgba(255,255,255,0.2); } + .lightbox-nav.prev { left: 16px; } + .lightbox-nav.next { right: 16px; } + @media (max-width: 640px) { + .lightbox-nav { display: none; } /* mobile uses swipe */ } .lightbox-close { position: absolute; @@ -780,6 +827,11 @@ class FolkPhotoGallery extends HTMLElement { const demoMeta = this.isDemo() ? this.getDemoAssetMeta(asset.id) : null; const displayName = asset.originalFileName.replace(/\.[^.]+$/, "").replace(/-/g, " "); + const list = this.currentAssetList(); + const idx = list.findIndex((a) => a.id === asset.id); + const hasPrev = idx > 0; + const hasNext = idx >= 0 && idx < list.length - 1; + return ` ` : ''} + ${hasPrev ? `` : ''} + ${hasNext ? `` : ''} ${demoMeta ? `
${this.esc(displayName)}
` - : `${this.esc(asset.originalFileName)}`} + : `${this.esc(asset.originalFileName)}`}