Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions check-baseline.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"comment": "27-I: the svelte-check floor, read ONLY by scripts/check-ratchet.cjs. It used to be hardcoded in release.yml's shell block, where it went stale (362 while the tree measured 359). Ratchet it DOWN whenever a change legitimately removes errors - that is the project convention, and --update does it in one command.",
"errors": 300,
"warnings": 41,
"measured": "2026-10-07"
"warnings": 40,
"measured": "2026-10-08"
}
18 changes: 18 additions & 0 deletions src/components/ContextMenu.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,9 @@
import { minimalScroll } from '$lib/ui/minimalScroll.js';
import { autofocusOk, typeToFocus } from '$lib/inputDevice';
import { safeStorage } from '$lib/safeStorage';
import SheetGrip from './ui/SheetGrip.svelte';
import { readSheetH, saveSheetH } from '$lib/ui/sheetDrag.js';
import { phoneShellActive, phoneSheetMaxH } from '$lib/ui/phoneShell.js';

// Generic context menu. items: [{ label, action?, disabled?, tooltip?, danger?,
// icon?, hint?, checked?, keepOpen?, rowActions?, children?: items[] } |
Expand Down Expand Up @@ -59,6 +62,18 @@
* Absent = byte-identical to before. */
export let startSearch: boolean = false;

// 40 F1: on the phone every menu is a bottom sheet (phone.css), and the Add list is the
// one opened from the bar — so it wears the shared grab bar: drag to resize (remembered,
// `menuSheetH`), swipe down to the end to close. The height is ONE root CSS var, so a
// submenu laid over its parent (drill in place) takes the same height.
let menuSheetH = readSheetH('menuSheetH', 0);
$: menuShownH = Math.min(menuSheetH || Math.round((typeof window === 'undefined' ? 800 : window.innerHeight) * 0.6), $phoneSheetMaxH || 9999);
$: if ($phoneShellActive && typeof document !== 'undefined') document.documentElement.style.setProperty('--ctx-sheet-h', menuShownH + 'px');
function menuSheetResize(h: number, done: boolean) {
menuSheetH = Math.round(h);
if (done) saveSheetH('menuSheetH', menuSheetH);
}

const rawDispatch = createEventDispatcher();
const dispatch = (name: string) => {
if (name === 'close') onclose?.();
Expand Down Expand Up @@ -425,6 +440,9 @@
on:click|capture={guardLift}
on:contextmenu|preventDefault
>
{#if $phoneShellActive}
<SheetGrip class="ctx-grip-bar" label="menu" height={menuShownH} onresize={menuSheetResize} onclose={() => dispatch('close')} />
{/if}
{#if headerItem}
<ContextMenuItems items={[headerItem]} onrun={run} />
{/if}
Expand Down
73 changes: 65 additions & 8 deletions src/components/editors/Explorer.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -345,6 +345,9 @@
// 24-C1: the in-app clipboard for files and folders
import { explorerClipboard, setClipboard, clearClipboard, isCutPending, clipboardLabel } from '$lib/explorerClipboard';
import { focusStack } from '$lib/windowFocus';
import ScrollStrip from '../ui/ScrollStrip.svelte';
import { phoneShellActive } from '$lib/ui/phoneShell.js';
import { touchHold, holdEvent, HOLD_MS } from '$lib/ui/touchHold.js';
import { tabbable, resizeGroup, tabGroups } from '$lib/windowTabs';
import { dockable } from '$lib/docking';
import ContextMenu from '../ContextMenu.svelte';
Expand Down Expand Up @@ -5904,6 +5907,9 @@
let tSuppressClick = false;
/** 39: this touch drag belongs to placeDrag (it reads the pointer from the window) */
let tPlace = false;
/** 40 F3: a HOLD opened the card's menu (the phone action sheet); moving on from there
* closes it and picks the card up instead (`pickUp`), lifting keeps the menu */
let tHeld: { pickUp: () => void } | null = null;

function onCardPointerDown(e: PointerEvent, item: any) {
if (e.pointerType === 'mouse') return; // desktop uses native HTML5 drag
Expand All @@ -5919,7 +5925,8 @@
const carried = payload.items?.length ?? 1;
const label = carried > 1 ? `${item.name} + ${carried - 1} more` : item.name;
clearTimeout(tPressTimer);
tPressTimer = window.setTimeout(() => {
tHeld = null;
const pickUp = () => {
// 39 P1: an object card picked up by touch is a PLACE drag — the ghost on the viewport,
// the sheet collapsed out of the way; the controller owns the gesture from here
if (isPlaceable(item)) {
Expand All @@ -5944,15 +5951,32 @@
try {
navigator.vibrate?.(15);
} catch {}
}, 300);
};
// 40 F3: a still HOLD opens the card's menu (long-press-explorer-file); the drag is the
// hold that then MOVES (onCardPointerMove) — hold-and-lift keeps the menu
tPressTimer = window.setTimeout(() => {
tHeld = { pickUp };
try {
navigator.vibrate?.(12);
} catch {}
itemMenu(holdEvent(tStartX, tStartY), item);
}, HOLD_MS);
}
function onCardPointerMove(e: PointerEvent) {
if (e.pointerType === 'mouse') return;
if (tPlace) return void e.preventDefault();
if (!tDragging) {
const travelled = Math.abs(e.clientX - tStartX) > 10 || Math.abs(e.clientY - tStartY) > 10;
// 40 F3: held (the menu is up) and now moving: the menu gives way to the pick-up
if (tHeld && travelled) {
const { pickUp } = tHeld;
tHeld = null;
menu = null;
pickUp();
return;
}
// moved before the long-press fired -> it's a scroll, not a pick-up
if (Math.abs(e.clientX - tStartX) > 10 || Math.abs(e.clientY - tStartY) > 10)
clearTimeout(tPressTimer);
if (travelled) clearTimeout(tPressTimer);
return;
}
e.preventDefault();
Expand All @@ -5961,6 +5985,12 @@
}
function onCardPointerUp(e: PointerEvent) {
clearTimeout(tPressTimer);
if (tHeld) {
// 40 F3: hold-and-lift — the menu stays; the lift is not a tap-select
tHeld = null;
tSuppressClick = true;
return;
}
if (tPlace) {
// placeDrag already took the release (its window listener runs first)
tPlace = false;
Expand Down Expand Up @@ -6507,7 +6537,7 @@
{#snippet editRow(depth: number)}
<div class="flex flex-col gap-0.5" style="padding-left: {8 + depth * 14}px">
<input
class="ui-input w-40 py-0.5 {isValidName(editing.value) ? '' : 'border-ink-bad'}"
class="ex-edit ui-input w-40 py-0.5 {isValidName(editing.value) ? '' : 'border-ink-bad'}"
value={editing.value}
use:focusSelect
oninput={(e) => (editing = { ...editing, value: e.currentTarget.value })}
Expand All @@ -6525,7 +6555,7 @@
<!-- `select-text`: the grid is `select-none` (the marquee's text-drag cure), which
would otherwise reach into the one input that lives inside it -->
<input
class="ui-input w-full select-text py-0 text-center text-[10px] {isValidName(editing.value) ? '' : 'border-ink-bad'}"
class="ex-edit ui-input w-full select-text py-0 text-center text-[10px] {isValidName(editing.value) ? '' : 'border-ink-bad'}"
value={editing.value}
use:focusSelect
oninput={(e) => (editing = { ...editing, value: e.currentTarget.value })}
Expand Down Expand Up @@ -6679,6 +6709,8 @@
onpointerdown={(e) => !isFolder && onCardPointerDown(e, item)}
onpointermove={(e) => !isFolder && onCardPointerMove(e)}
onpointerup={(e) => !isFolder && onCardPointerUp(e)}
onpointercancel={() => clearTimeout(tPressTimer)}
use:touchHold={{ onhold: isFolder ? (x, y) => folderMenu(holdEvent(x, y), folder, false) : undefined }}
oncontextmenu={(e) => (isFolder ? folderMenu(e, folder, false) : itemMenu(e, item))}
onclick={(e) => (isFolder ? onFolderCardClick(e, folder) : onCardClick(e, item))}
ondblclick={() => (isFolder ? openFolder(folder.id) : openItem(item))}
Expand Down Expand Up @@ -7709,6 +7741,7 @@
ondragover={(e) => dragOverInto(e, folder.id)}
ondragleave={() => (dropFolder = null)}
ondrop={(e) => dropInto(e, folder.id)}
use:touchHold={{ onhold: (x, y) => folderMenu(holdEvent(x, y), folder, false) }}
oncontextmenu={(e) => folderMenu(e, folder, false)}
onclick={(e) => onFolderCardClick(e, folder)}
ondblclick={() => openFolder(folder.id)}
Expand Down Expand Up @@ -7763,6 +7796,8 @@
onpointerdown={(e) => onCardPointerDown(e, item)}
onpointermove={onCardPointerMove}
onpointerup={onCardPointerUp}
onpointercancel={() => clearTimeout(tPressTimer)}
use:touchHold
data-cut={!!$explorerClipboard && isCutPending(item.id) ? '1' : undefined}
class:opacity-50={!!$explorerClipboard && isCutPending(item.id)}
oncontextmenu={(e) => itemMenu(e, item)}
Expand Down Expand Up @@ -8310,7 +8345,7 @@
onpointerup={endResize}
></div>
<DockTabs />
<div class="mb-1 flex items-center gap-2" use:headerWidth>
{#snippet dockRow()}
<span class="tp-dock-title"><Icon name="folder-tree" size={16} />Explorer</span>
<!-- `shrink-0`: the identity chip beside it is the flex item that gives way.
W6 deliberately left this row's LAYOUT alone — its narrow-width behaviour
Expand All @@ -8335,7 +8370,18 @@
aria-label="Undock into a floating window"
onclick={() => setDocked(false)}><Icon name="app-window" size={16} /></button
>
</div>
{/snippet}
{#if $phoneShellActive}
<!-- 40 F3: on the phone the toolbar SCROLLS sideways (ui/ScrollStrip) instead of
squeezing the project name to "Unt…" — every chip stays reachable -->
<div class="mb-1" use:headerWidth>
<ScrollStrip id="explorer-dock-strip" gap="var(--space-2)" label="Explorer tools">{@render dockRow()}</ScrollStrip>
</div>
{:else}
<div class="mb-1 flex items-center gap-2" use:headerWidth>
{@render dockRow()}
</div>
{/if}
<div style="height: {$dockHeight - 44}px">
{@render content()}
</div>
Expand Down Expand Up @@ -8436,6 +8482,7 @@
<!-- mobile touch-drag ghost that follows the finger onto the viewport -->
{#if tDragging && tDrag}
<div
id="explorer-touch-ghost"
class="pointer-events-none fixed z-1400 max-w-[160px] -translate-x-1/2 -translate-y-1/2 truncate rounded-sm border border-accent bg-surface-1 px-2 py-1 text-center text-xs font-semibold text-text shadow-lg"
style="left: {tGhostX}px; top: {tGhostY}px;"
>
Expand Down Expand Up @@ -8901,4 +8948,14 @@
flex: 1;
}
}
/* 40 F3: the rename field's SELECTED text is readable in every theme — the browser default
selection (and the phone's) fought the field's own colours; the kit's accent pair is the
one guaranteed to contrast. The field inside a card keeps the platform text callout. */
.ex-edit::selection {
background: var(--accent-fill);
color: var(--on-accent);
}
.ex-edit {
-webkit-touch-callout: default;
}
</style>
28 changes: 25 additions & 3 deletions src/components/menu/CharacterPanel.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@
import { pingColor, pingSound, previewPing } from '$lib/ping';
import { PING_SOUNDS, playPing } from '$lib/pingAudio';
import { safeStorage } from '$lib/safeStorage';
import { settleSheet } from '$lib/ui/sheetSnap.js';
import { phoneSheetMaxH } from '$lib/ui/phoneShell.js';
import { flyTo } from '$lib/objectActions';
import Icon from '../ui/Icon.svelte';
// 38 R7 + NOTES-38 #2: a resizable DRAWER on the redesign kit — WindowChrome size="modal" header,
Expand Down Expand Up @@ -113,7 +115,9 @@
const MIN_W = 300;
const MIN_H = 220;
const maxW = () => Math.max(MIN_W, Math.round(window.innerWidth * 0.6));
const maxH = () => Math.max(MIN_H, Math.round(window.innerHeight * 0.8));
// 40 F1: on the phone the sheet's ceiling is the shell's sheet room (clear of the top bar and the
// selection strip), like every other phone sheet
const maxH = () => Math.max(MIN_H, Math.round(Math.min(window.innerHeight * 0.8, get(phoneSheetMaxH) || Infinity)));
const sheetH = () => (drawerH > 0 ? Math.min(Math.max(drawerH, MIN_H), maxH()) : Math.min(window.innerHeight * 0.52, 460));
const panelW = () => Math.min(Math.max(drawerW, MIN_W), maxW());

Expand Down Expand Up @@ -162,16 +166,34 @@
const y0 = e.clientY;
const w0 = panelW();
const h0 = sheetH();
// 40 F1: the phone sheet closes like every other one — swiped down to the end, or flicked
// down from its min (the shared rule, sheetSnap.settleSheet)
let rawH = h0;
/** @type {[number, number][]} */
let samples = [[e.timeStamp, e.clientY]];
/** @param {PointerEvent} ev */
const move = (ev) => {
if (narrow) drawerH = Math.min(Math.max(h0 - (ev.clientY - y0), MIN_H), maxH());
rawH = h0 - (ev.clientY - y0);
samples.push([ev.timeStamp, ev.clientY]);
if (samples.length > 6) samples.shift();
if (narrow) drawerH = Math.min(Math.max(rawH, MIN_H), maxH());
else drawerW = Math.min(Math.max(w0 - (ev.clientX - x0), MIN_W), maxW());
};
const up = () => {
/** @param {PointerEvent} ev */
const up = (ev) => {
window.removeEventListener('pointermove', move);
window.removeEventListener('pointerup', up);
window.removeEventListener('pointercancel', up);
resizing = false;
if (narrow && ev.type === 'pointerup') {
const first = samples[0];
const velocity = (ev.clientY - first[1]) / Math.max(1, ev.timeStamp - first[0]);
if (settleSheet({ height: rawH, velocity, min: MIN_H, max: maxH() }) === 'closed') {
drawerH = h0; // the height it had: a swipe-away is not a resize
end(false);
return;
}
}
safeStorage.setItem(SIZE_KEY, JSON.stringify({ w: drawerW, h: drawerH }));
aim(300);
};
Expand Down
52 changes: 19 additions & 33 deletions src/components/menu/Inspector.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,9 @@
import FlowPathSection from '../sim/FlowPathSection.svelte'; // 36-fb F24
import FluidTankSection from '../sim/FluidTankSection.svelte'; // 36-sim U2b
import DragRow from '../ui/DragRow.svelte';
import SheetGrip from '../ui/SheetGrip.svelte';
import { saveSheetH } from '$lib/ui/sheetDrag.js';
import { phoneSheetMaxH } from '$lib/ui/phoneShell.js';
import Segmented from '../ui/Segmented.svelte';
import ParticleMotionRows from '../fx/ParticleMotionRows.svelte';
import ColorPicker, { ChromeVariant } from 'svelte-awesome-color-picker';
Expand Down Expand Up @@ -339,30 +342,12 @@
const saved = parseInt(safeStorage.getItem('inspectorSheetH') || '');
inspectorH = !saved || Number.isNaN(saved) ? Math.round(window.innerHeight * 0.45) : saved;
});
let insResizing = $state(false);
/** @param {PointerEvent} e */
function insStartResize(e) {
insResizing = true;
/** @type {HTMLElement} */ (e.currentTarget).setPointerCapture?.(e.pointerId);
e.preventDefault();
}
/** @param {PointerEvent} e */
function insDoResize(e) {
if (!insResizing) return;
// sheet is bottom:0, so height = viewport height - finger y; cap the top below
// the Connect bar + top-right chrome (same limit as the Flow/Explorer dock)
const cb = parseFloat(getComputedStyle(document.documentElement).getPropertyValue('--connect-bottom')) || 54;
const maxH = Math.max(200, window.innerHeight - cb - 56);
inspectorH = Math.min(Math.max(160, window.innerHeight - e.clientY), maxH);
}
/** @param {PointerEvent} e */
function insEndResize(e) {
if (!insResizing) return;
insResizing = false;
/** @type {HTMLElement} */ (e.currentTarget).releasePointerCapture?.(e.pointerId);
try {
safeStorage.setItem('inspectorSheetH', String(inspectorH));
} catch {}
// 40 F1: the grab bar is ui/SheetGrip (sticky, swipe down to the end closes). The height
// stays this drawer's own, under the same key.
/** @param {number} h @param {boolean} done */
function insResize(h, done) {
inspectorH = Math.round(h);
if (done) saveSheetH('inspectorSheetH', inspectorH);
}

// C1 (roadmap #13): scene-mode physics-objects list. Recomputes on scene/graph
Expand Down Expand Up @@ -1638,16 +1623,17 @@
role="region"
aria-label="Properties"
>
<!-- bottom-sheet drag handle (shown only in the narrow bottom-sheet layout) -->
<div
<!-- bottom-sheet grab bar (shown only in the narrow bottom-sheet layout): STICKY, so
scrolling the properties never takes it away (40 F1) -->
<SheetGrip
class="ins-resize"
title="Drag to resize"
onpointerdown={insStartResize}
onpointermove={insDoResize}
onpointerup={insEndResize}
>
<span class="ins-grabber"></span>
</div>
label={$inspectorKind === 'scene' ? 'Configure Scene' : 'Properties'}
height={Math.min(inspectorH, $phoneSheetMaxH || inspectorH)}
min={160}
inset={16}
onresize={insResize}
onclose={() => inspectorClose.set(true)}
/>
{#if $inspectorKind === 'file'}
<!-- Explorer file properties (107) -->
<InspectorHead title={inspectedItem?.name ?? 'File'} icon="file-text" badge="File" onclose={() => inspectorClose.set(true)} />
Expand Down
Loading
Loading