Merge branch 'dev'

This commit is contained in:
Jeff Emmett 2026-03-25 16:59:19 -07:00
commit 96ae343748
8 changed files with 141 additions and 20 deletions

View File

@ -235,6 +235,13 @@ export class FolkShape extends FolkElement {
#lastTouchPos: Point | null = null;
#isTouchDragging = false;
#dragOffset: Point | null = null;
#touchStartPos: Point | null = null;
#longPressTimer: ReturnType<typeof setTimeout> | null = null;
#extraLongPressTimer: ReturnType<typeof setTimeout> | null = null;
#touchDidLongPress = false;
static LONG_PRESS_MS = 500;
static EXTRA_LONG_PRESS_MS = 1000;
static TOUCH_MOVE_THRESHOLD = 8; // px before considered a drag
get x() {
return this.#rect.x;
@ -485,6 +492,11 @@ export class FolkShape extends FolkElement {
};
}
#clearLongPressTimers() {
if (this.#longPressTimer) { clearTimeout(this.#longPressTimer); this.#longPressTimer = null; }
if (this.#extraLongPressTimer) { clearTimeout(this.#extraLongPressTimer); this.#extraLongPressTimer = null; }
}
handleEvent(event: PointerEvent | KeyboardEvent | TouchEvent) {
// In feed mode, suppress all drag/resize interactions
if (this.closest('#canvas.feed-mode')) return;
@ -506,11 +518,11 @@ export class FolkShape extends FolkElement {
// Two-finger gesture → cancel shape drag so canvas pan takes over
if (event.touches.length >= 2) {
if (this.#isTouchDragging) {
this.#lastTouchPos = null;
this.#isTouchDragging = false;
this.#internals.states.delete("move");
}
this.#clearLongPressTimers();
this.#lastTouchPos = null;
this.#isTouchDragging = false;
this.#touchDidLongPress = false;
this.#internals.states.delete("move");
return;
}
@ -526,16 +538,59 @@ export class FolkShape extends FolkElement {
return;
}
this.#touchStartPos = { x: touch.clientX, y: touch.clientY };
this.#lastTouchPos = { x: touch.clientX, y: touch.clientY };
this.#isTouchDragging = true;
this.#internals.states.add("move");
this.#isTouchDragging = false;
this.#touchDidLongPress = false;
// Long press → select shape
this.#longPressTimer = setTimeout(() => {
this.#longPressTimer = null;
this.#touchDidLongPress = true;
// Stop any drag — switch to selected state
this.#isTouchDragging = false;
this.#internals.states.delete("move");
navigator?.vibrate?.(30);
this.dispatchEvent(new CustomEvent("touch-select", {
bubbles: true, composed: true,
detail: { shapeId: this.id },
}));
}, FolkShape.LONG_PRESS_MS);
// Extra long press → context menu
this.#extraLongPressTimer = setTimeout(() => {
this.#extraLongPressTimer = null;
navigator?.vibrate?.(50);
const cmEvent = new MouseEvent("contextmenu", {
bubbles: true, cancelable: true,
clientX: touch.clientX, clientY: touch.clientY,
});
this.dispatchEvent(cmEvent);
}, FolkShape.EXTRA_LONG_PRESS_MS);
this.focus();
return;
}
if (event.type === "touchmove" && this.#isTouchDragging && event.touches.length === 1) {
if (event.type === "touchmove" && event.touches.length === 1) {
const touch = event.touches[0];
if (this.#lastTouchPos) {
// Check if finger moved past threshold to start drag
if (!this.#isTouchDragging && this.#touchStartPos) {
const dx = touch.clientX - this.#touchStartPos.x;
const dy = touch.clientY - this.#touchStartPos.y;
if (Math.abs(dx) > FolkShape.TOUCH_MOVE_THRESHOLD ||
Math.abs(dy) > FolkShape.TOUCH_MOVE_THRESHOLD) {
// Movement → cancel long press timers, begin drag
this.#clearLongPressTimers();
if (!this.#touchDidLongPress) {
this.#isTouchDragging = true;
this.#internals.states.add("move");
}
}
}
if (this.#isTouchDragging && this.#lastTouchPos) {
const zoom = (window.visualViewport?.scale ?? 1) * this.#getParentScale();
const moveDelta = {
x: (touch.clientX - this.#lastTouchPos.x) / zoom,
@ -548,13 +603,18 @@ export class FolkShape extends FolkElement {
this.#rect.y += moveDelta.y;
this.requestUpdate();
this.#dispatchTransformEvent();
} else {
this.#lastTouchPos = { x: touch.clientX, y: touch.clientY };
}
return;
}
if (event.type === "touchend") {
this.#clearLongPressTimers();
this.#lastTouchPos = null;
this.#touchStartPos = null;
this.#isTouchDragging = false;
this.#touchDidLongPress = false;
this.#internals.states.delete("move");
return;
}

View File

@ -465,6 +465,7 @@ class FolkFileBrowser extends HTMLElement {
.upload-row { flex-direction: column; align-items: stretch; }
.file-card { padding: 10px; }
.card-form-row { flex-direction: column; }
input[type="text"], select, textarea { font-size: 16px; }
}
</style>

View File

@ -253,6 +253,14 @@ class NotesCommentPanel extends HTMLElement {
.new-comment-actions { display: flex; justify-content: flex-end; gap: 6px; margin-top: 6px; }
.first-message-text { word-break: break-word; overflow-wrap: anywhere; }
.message-text { word-break: break-word; overflow-wrap: anywhere; }
@media (max-width: 480px) {
.panel { max-height: none; height: 100%; }
.thread-action { padding: 8px 10px; font-size: 12px; }
.reply-btn, .reply-cancel-btn { padding: 8px 16px; }
.reply-input { padding: 8px 10px; font-size: 14px; }
.emoji-pick { padding: 6px 8px; font-size: 18px; }
.new-comment-input { min-height: 44px; max-height: 100px; font-size: 14px; }
}
</style>
<div class="panel" id="comment-panel">
<div class="panel-title" data-action="toggle-collapse">

View File

@ -2130,8 +2130,10 @@ Gear: EUR 400 (10%)</code></pre><p><em>Maya is tracking expenses in rF
popover.className = 'url-popover';
const hostRect = (this.shadow.host as HTMLElement).getBoundingClientRect();
popover.style.left = `${anchorRect.left - hostRect.left}px`;
popover.style.top = `${anchorRect.bottom - hostRect.top + 4}px`;
if (window.innerWidth > 640) {
popover.style.left = `${anchorRect.left - hostRect.left}px`;
popover.style.top = `${anchorRect.bottom - hostRect.top + 4}px`;
}
const input = document.createElement('input');
input.type = 'url';
@ -3118,6 +3120,10 @@ Gear: EUR 400 (10%)</code></pre><p><em>Maya is tracking expenses in rF
}
.sbt-notebook-header:hover .sbt-nb-add { opacity: 1; }
.sbt-nb-add:hover { color: var(--rs-primary); background: var(--rs-bg-surface-raised); }
@media (pointer: coarse) {
.sbt-nb-add { opacity: 0.6; }
.sbt-nb-add:active { opacity: 1; color: var(--rs-primary); }
}
.sbt-notes { padding-left: 20px; }
.sbt-note {
display: flex; align-items: center; gap: 8px;
@ -3201,11 +3207,16 @@ Gear: EUR 400 (10%)</code></pre><p><em>Maya is tracking expenses in rF
@media (max-width: 480px) {
.editor-with-comments { flex-direction: column; }
.comment-sidebar.has-comments {
width: 100%;
border-left: none;
border-top: 1px solid var(--rs-border, #e5e7eb);
max-height: 250px;
overflow-y: auto;
width: 100%; border-left: none;
border-top: 2px solid var(--rs-border, #e5e7eb);
max-height: 250px; max-height: 40dvh;
min-height: 120px; overflow-y: auto;
border-radius: 12px 12px 0 0; padding-top: 4px;
}
.comment-sidebar.has-comments::before {
content: ''; display: block; width: 32px; height: 4px;
background: var(--rs-border-strong, #d1d5db); border-radius: 2px;
margin: 0 auto 4px;
}
}
@ -3521,13 +3532,17 @@ Gear: EUR 400 (10%)</code></pre><p><em>Maya is tracking expenses in rF
.mobile-sidebar-toggle { display: flex !important; }
.editor-wrapper .editable-title { padding: 12px 14px 0; }
.tiptap-container .tiptap { padding: 14px 16px; }
.sidebar-footer-btn { min-height: 36px; padding: 7px 12px; }
}
@media (max-width: 480px) {
.rapp-nav__btn { padding: 5px 10px; font-size: 12px; }
.editable-title { font-size: 18px; }
.tiptap-container .tiptap { font-size: 14px; padding: 12px 14px; min-height: 200px; }
.editor-toolbar { padding: 3px 4px; gap: 1px; }
.toolbar-btn { width: 26px; height: 24px; }
.editor-toolbar { padding: 3px 4px; gap: 1px; overflow-x: auto; -webkit-overflow-scrolling: touch; flex-wrap: nowrap; }
.toolbar-btn { width: 36px; height: 36px; }
.toolbar-sep { display: none; }
.code-textarea { min-height: 200px; }
.image-preview { max-height: 240px; }
.note-actions-bar { flex-wrap: wrap; gap: 6px; }
.note-action-btn { padding: 5px 10px; font-size: 11px; }
}
@ -3570,6 +3585,13 @@ Gear: EUR 400 (10%)</code></pre><p><em>Maya is tracking expenses in rF
border: 1px solid var(--rs-border);
}
.url-popover__btn--cancel:hover { color: var(--rs-text-primary); border-color: var(--rs-border-strong); }
@media (max-width: 640px) {
.url-popover {
position: fixed; left: 8px !important; right: 8px !important;
top: auto !important; bottom: max(env(safe-area-inset-bottom), 8px);
min-width: 0; width: auto; border-radius: 12px 12px 0 0;
}
}
/* ── Slash Menu ── */
.slash-menu {
@ -3604,6 +3626,12 @@ Gear: EUR 400 (10%)</code></pre><p><em>Maya is tracking expenses in rF
font-size: 10px; color: var(--rs-text-muted); padding: 1px 6px;
background: var(--rs-bg-surface-raised); border-radius: 3px; margin-left: auto;
}
@media (max-width: 480px) {
.slash-menu { min-width: 200px; max-height: 260px; }
.slash-menu-item { padding: 10px 12px; }
.slash-menu-desc { display: none; }
.slash-menu-hint { display: none; }
}
/* ── Code highlighting (lowlight) ── */
.tiptap-container .tiptap .hljs-keyword { color: #c792ea; }

View File

@ -191,7 +191,11 @@ export function createSlashCommandPlugin(editor: Editor, shadowRoot: ShadowRoot)
const shadowHost = shadowRoot.host as HTMLElement;
const hostRect = shadowHost.getBoundingClientRect();
menuEl.style.left = `${coords.left - hostRect.left}px`;
let left = coords.left - hostRect.left;
const menuWidth = 240;
const maxLeft = window.innerWidth - menuWidth - 8 - hostRect.left;
left = Math.max(4, Math.min(left, maxLeft));
menuEl.style.left = `${left}px`;
menuEl.style.top = `${coords.bottom - hostRect.top + 4}px`;
}

View File

@ -656,6 +656,8 @@ class FolkTasksBoard extends HTMLElement {
.card { padding: 10px; }
.card-title { font-size: 13px; }
.card-meta { font-size: 10px; }
.create-form input, .create-form select, .create-form textarea { font-size: 16px; }
.create-form-actions button { min-height: 36px; padding: 6px 14px; }
}
</style>

View File

@ -3866,9 +3866,10 @@ Use real coordinates, YYYY-MM-DD dates, ISO currency codes. Ask clarifying quest
});
// Track selection for MI bridge — supports Shift/Ctrl+click multi-select
// On touch devices, first touch moves; selection comes from long-press (touch-select event)
shape.addEventListener("pointerdown", (e) => {
if (e.pointerType === "touch") return; // handled by touch-select
if (e.shiftKey || e.metaKey || e.ctrlKey) {
// Additive toggle
if (selectedShapeIds.has(shape.id)) {
selectedShapeIds.delete(shape.id);
} else {
@ -3882,6 +3883,16 @@ Use real coordinates, YYYY-MM-DD dates, ISO currency codes. Ask clarifying quest
updateSelectionVisuals();
});
// Mobile long-press → select shape
shape.addEventListener("touch-select", (e) => {
if (!selectedShapeIds.has(shape.id)) {
selectedShapeIds.clear();
selectedShapeIds.add(shape.id);
}
selectedShapeId = shape.id;
updateSelectionVisuals();
});
// Close button — forget (fade) instead of remove
shape.addEventListener("close", () => {
const did = getLocalDID();

View File

@ -443,6 +443,7 @@ body.rstack-sidebar-open #toolbar {
body.rstack-sidebar-open rstack-user-dashboard { left: 0; }
body.rstack-sidebar-open .rspace-iframe-wrap { left: 0; }
body.rstack-sidebar-open #toolbar { left: 12px; }
.rapp-nav__btn, .rapp-nav__back { min-height: 36px; }
}
@media (max-width: 480px) {
@ -482,3 +483,9 @@ body.rstack-sidebar-open #toolbar {
border-radius: 50%;
animation: rspace-spin 0.8s linear infinite;
}
/* ── Touch-device utilities ── */
@media (pointer: coarse) {
.hover-reveal { opacity: 0.5 !important; }
.hover-reveal:active { opacity: 1 !important; }
}