diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 33d953a33..0bf3540db 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -53,6 +53,11 @@ jobs: # the subscriber for the session. - name: no bare localStorage run: node scripts/check-storage.cjs + # 38 R2/R11: colours come from the redesign tokens — a HARD FAIL since R11 (every + # component was migrated). A colour that is data, not chrome, carries + # `tokens-ok: ` on its line; scripts/check-tokens.cjs says how. + - name: redesign tokens + run: node scripts/check-tokens.cjs # 34 B3: scene-lint over every scene core ships (the seed) and every scene of the scenes ref # the perf budget is about (perf/budgets.json), with the packs checkout for the kit rules. @@ -134,3 +139,55 @@ jobs: - name: dev server log on failure if: failure() run: tail -40 /tmp/dev.log + + # 38 (NOTES-38 #18): THE TOKEN SCREENSHOT DIFF. /kit renders every redesign primitive in every + # state; this job shoots it in the dark, light and custom themes on the PR's BASE ref and on its + # HEAD, on this same runner (so fonts and anti-aliasing cannot drift between machines), and diffs + # them (tests/e2e/tools/kit-diff.cjs). It FAILS when a theme moved more than 0.01% of the kit's + # pixels — a signal to look at the diff images in the artifact, not a required check: a PR that + # changes a token on purpose turns it red and says so. A base without /kit is skipped. + kit-diff: + if: github.event_name == 'pull_request' + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + - uses: actions/setup-node@v4 + with: + node-version: 24 + cache: npm + - run: npm ci + - run: npx playwright install --with-deps chromium + - run: npm run certs + - name: base checkout + id: base + run: | + git worktree add ../kit-base "origin/${{ github.base_ref }}" + if [ -f ../kit-base/src/routes/kit/+page.svelte ]; then + echo "has_kit=1" >> "$GITHUB_OUTPUT" + cp -r certs ../kit-base/ && (cd ../kit-base && npm ci) + else + echo "has_kit=0" >> "$GITHUB_OUTPUT" + echo "The base branch has no /kit yet — nothing to diff against." >> "$GITHUB_STEP_SUMMARY" + fi + - name: shoot base and head + if: steps.base.outputs.has_kit == '1' + run: | + serve() { (cd "$1" && npm run dev -- --port "$2" --strictPort --host localhost > "/tmp/dev-$2.log" 2>&1 &) + for i in $(seq 1 90); do curl -sk -o /dev/null "https://localhost:$2/kit" && break; sleep 1; done; } + serve ../kit-base 5174 + serve . 5173 + node tests/e2e/tools/kit-diff.cjs shoot https://localhost:5174/ /tmp/kit/base + node tests/e2e/tools/kit-diff.cjs shoot https://localhost:5173/ /tmp/kit/head + - name: diff + if: steps.base.outputs.has_kit == '1' + run: | + node tests/e2e/tools/kit-diff.cjs diff /tmp/kit/base /tmp/kit/head /tmp/kit/diff; rc=$? + cat /tmp/kit/diff/report.md >> "$GITHUB_STEP_SUMMARY" + exit $rc + - uses: actions/upload-artifact@v4 + if: always() && steps.base.outputs.has_kit == '1' + with: + name: kit-diff + path: /tmp/kit diff --git a/CLAUDE.md b/CLAUDE.md index d8954dbfa..9d8a587cb 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -2632,6 +2632,21 @@ loadable play content. Everything a user does must be visible to connected peers pushes/pulls a one-grip world, as Edit does an object); `addSetting({onLevels: true})` draws a choice as tabs on the VR Levels page, which now lists every level. +## UI design system (redesign epic) + +- All UI follows the redesign spec kept in the `cloud` repo: `docs/design/redesign/SPEC.md` + (mockups in `docs/design/redesign/mockups/`). Locally that is + `../cloud/docs/design/redesign/` from this checkout (`theprototype-app/cloud`). +- Build UI only from `src/components/ui` primitives (WindowChrome, Tabs, Segmented, Chips, + Toggle, Button, Badge, SettingRow, PropRow, NavRow, EmptyState, Sheet). Add a primitive + there before inventing a one-off. +- Colours, radii, spacing and type come from the semantic tokens in `app.css` only — no raw + hex, no ad-hoc Tailwind palette colours in components (`npm run check:tokens`). +- Every icon goes through `ui/Icon.svelte` (sizes 16 or 20 only; custom glyphs as `tool:`; enforced by `tests/unit/iconLint.test.js`). +- A redesign change must not alter behaviour: DragRow scrubbing, modifiers, click-to-type, + shortcuts, undo grouping, peer sync, storage keys and defaults. The behaviour-lock e2e + suites must stay green. + ## Replication golden rules 1. Every mutation = apply locally + `$peers.send({type, ...})`; receivers apply WITHOUT @@ -4943,11 +4958,19 @@ loadable play content. Everything a user does must be visible to connected peers `error during build: undefined` (svelte-check never runs; vite dev 500s too). Same trap in any non-TS component: JSDoc for types, never TS syntax (#13-B3). - **Icons = `@lucide/svelte` SVG components** (Font Awesome fully REMOVED post-1.0.1 - — never add `fa-` classes). Static markup imports named components - (`import { Play } from '@lucide/svelte'`; sizes 16 inline/menu, 18-20 toolbar/ - header, 24 the play FAB; `aria-hidden="true"` when decorative); DATA-DRIVEN icon - names (Explorer KIND_ICONS, menu-item defs) render via `components/ui/Icon.svelte` - (kebab lucide names). Icons inherit `currentColor` — never hardcode grays; + — never add `fa-` classes). **Redesign (SPEC §7): EVERY icon goes through + `components/ui/Icon.svelte`** — data-driven names (Explorer KIND_ICONS, menu-item defs) + AND static markup (``, kebab lucide names; add the name to its MAP, + an unmapped name renders a box and warns in dev). Sizes 16 (inline/menu/rows) or 20 + (toolbar/header); 24/32/48 only for placeholder ART (an empty Explorer tile, the Play + face); anything else SNAPS (`$lib/ui/icons.js` `iconSize`) and one stroke (`ICON_STROKE` + 1.75) — never size an icon with a `h-*`/`w-*` class. The custom glyph set is + `` (ToolIcon.svelte's duotone PATHS, lucide's grid) — the + slot the commissioned domain set fills (cloud `docs/design/redesign/icons-brief.md`). + `snap={false}` is for authored game-HUD content and the checkbox tick only. + `tests/unit/iconLint.test.js` (R10) FAILS on a component importing `@lucide/svelte` or + ToolIcon, an unknown literal name, an off-scale size, a class-sized icon, or an + `icon: '…'` data name missing from the map. Icons inherit `currentColor` — never hardcode grays; semantic colors come from the `--icon-*` theme tokens. TWO TRAPS: a `class` passed to a lucide component lands on the CHILD-scope ``, so scoped CSS targeting it needs `:global(...)` (bit cx-chevron/tp-toast-icon/role-caret — silent style loss, diff --git a/check-baseline.json b/check-baseline.json index 7574f4241..d0fb7f8bb 100644 --- a/check-baseline.json +++ b/check-baseline.json @@ -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": 318, - "warnings": 47, - "measured": "2026-10-05" + "errors": 312, + "warnings": 41, + "measured": "2026-10-07" } diff --git a/package.json b/package.json index ba50defff..e85e5189a 100644 --- a/package.json +++ b/package.json @@ -19,10 +19,12 @@ "format": "prettier --write .", "lint": "prettier --check .", "e2e": "node tests/e2e/run.cjs", + "e2e:lock": "node tests/e2e/run.cjs @lock", "deps:check": "node scripts/deps-check.cjs", "sync-llms": "node scripts/sync-llms.cjs", "test:unit": "vitest run", - "check:storage": "node scripts/check-storage.cjs" + "check:storage": "node scripts/check-storage.cjs", + "check:tokens": "node scripts/check-tokens.cjs" }, "devDependencies": { "@sveltejs/adapter-static": "^3.0.10", diff --git a/scripts/check-tokens.cjs b/scripts/check-tokens.cjs new file mode 100644 index 000000000..d1c0b09a3 --- /dev/null +++ b/scripts/check-tokens.cjs @@ -0,0 +1,154 @@ +#!/usr/bin/env node +// 38 R2/R11 — `npm run check:tokens`: colours come from the redesign tokens only +// (cloud docs/design/redesign/SPEC.md §1; the tokens live in src/styles/tokens.css and app.css). +// +// What counts as a raw colour (src/components/**, src/routes/**, src/styles/** minus the files +// that DEFINE the tokens, and the core modules' .svelte components): +// - a hex literal #fff #1b212d #1b212dcc (also inside a var() fallback) +// - an rgb()/rgba()/hsl()/hsla() literal +// - a Tailwind PALETTE utility bg-gray-800 text-red-400 hover:border-blue-500/50 +// bg-white text-black … (token utilities — bg-surface-2, text-text-muted — are fine) +// Comments are stripped first, so a note that NAMES a colour is not a violation. +// +// R11: a HARD FAIL (CI runs it as is). The redesign migrated every component; a colour that is +// DATA, not chrome — a default the user edits in a colour picker, a three.js material, pixels a +// canvas draws for the 3D view or a headset — says so where it stands: +// const DEFAULT_TINT = '#ffffff'; // tokens-ok: the picker's starting value (user data) +// /* tokens-ok-begin: node category hues (graph data, same in every theme) */ … /* tokens-ok-end */ +// The reason after `tokens-ok:` is required (a bare `tokens-ok` is itself a violation), so +// every exemption reads as a decision in review. +// +// node scripts/check-tokens.cjs report per file, exit 1 on any raw colour +// node scripts/check-tokens.cjs --warn report only, exit 0 +// node scripts/check-tokens.cjs --json machine-readable { total, files: {rel: n} } +// node scripts/check-tokens.cjs only these files (a lane checking its own diff) + +const fs = require('fs'); +const path = require('path'); + +const ROOT = path.resolve(__dirname, '..'); +const SCOPE = ['src/components', 'src/routes', 'src/styles', 'src/modules'].map((d) => path.join(ROOT, d)); +/** the files that DEFINE the tokens (and the legacy palette they derive from) */ +const DEFINITIONS = new Set(['src/styles/tokens.css', 'src/styles/theme.css']); +/** files another branch is redesigning (scripts/redesign-pending.json says which and why) */ +const PENDING = new Set(require('./redesign-pending.json').files); + +const PALETTE = + 'slate|gray|zinc|neutral|stone|red|orange|amber|yellow|lime|green|emerald|teal|cyan|sky|blue|indigo|violet|purple|fuchsia|pink|rose|primary'; +const UTIL = + 'bg|text|border|border-[trblxy]|ring|ring-offset|outline|divide|from|via|to|fill|stroke|placeholder|caret|accent|decoration|shadow'; +const RULES = [ + { kind: 'hex', re: /(?, CSS/JS block + * comments, and // line comments (only after start-of-line or whitespace, so `https://` + * and a `//` inside a string literal of a URL survive). + * @param {string} text @returns {string} + */ +function stripComments(text) { + const blank = (/** @type {string} */ m) => m.replace(/[^\n]/g, ' '); + return text + .replace(//g, blank) + .replace(/\/\*[\s\S]*?\*\//g, blank) + .replace(/(^|[\s;{}])\/\/[^\n]*/g, (m, lead) => lead + blank(m.slice(lead.length))); +} + +/** `tokens-ok: ` on a line, or a `tokens-ok-begin: ` … `tokens-ok-end` block */ +const PRAGMA = /tokens-ok(?:-begin)?\s*:\s*\S.{2,}/; +const BARE = /tokens-ok(?!-end)(?!-begin\s*:\s*\S)(?!\s*:\s*\S)/; + +/** + * Every raw colour in one file's text, minus the lines an exemption pragma covers. A pragma + * with no reason is reported as kind `pragma`. + * @param {string} text + * @returns {{line: number, kind: string, match: string}[]} + */ +function scanText(text) { + /** @type {{line: number, kind: string, match: string}[]} */ + const out = []; + const raw = text.split('\n'); + const lines = stripComments(text).split('\n'); + let block = false; + lines.forEach((line, i) => { + const src = raw[i] ?? ''; + if (BARE.test(src)) out.push({ line: i + 1, kind: 'pragma', match: 'tokens-ok without a reason' }); + const opens = /tokens-ok-begin\s*:/.test(src); + const closes = /tokens-ok-end/.test(src); + if (opens) block = true; + const exempt = block || PRAGMA.test(src); + if (closes) block = false; + if (exempt) return; + for (const { kind, re } of RULES) { + re.lastIndex = 0; + let m; + while ((m = re.exec(line))) out.push({ line: i + 1, kind, match: m[0] }); + } + }); + return out; +} + +/** @param {string} dir @param {string[]} out @returns {string[]} */ +function walk(dir, out = []) { + for (const entry of fs.readdirSync(dir, { withFileTypes: true })) { + const full = path.join(dir, entry.name); + if (entry.isDirectory()) walk(full, out); + // src/modules: only the components (module.js files build three.js content, whose colours are data) + else if (/\.(svelte|css|js|ts)$/.test(entry.name) && (entry.name.endsWith('.svelte') || !relOf(full).startsWith('src/modules/'))) out.push(full); + } + return out; +} + +/** @param {string} file */ +const relOf = (file) => path.relative(ROOT, path.resolve(file)).split(path.sep).join('/'); + +function main() { + const args = process.argv.slice(2); + const warn = args.includes('--warn'); + const json = args.includes('--json'); + const only = args.filter((a) => !a.startsWith('--')); + const files = only.length + ? only.map((f) => path.resolve(ROOT, f)) + : SCOPE.flatMap((d) => walk(d)).filter((f) => !DEFINITIONS.has(relOf(f)) && !PENDING.has(relOf(f))); + + /** @type {Record} */ + const perFile = {}; + /** @type {string[]} */ + const lines = []; + let total = 0; + for (const file of files) { + if (!fs.existsSync(file)) continue; + const rel = relOf(file); + const hits = scanText(fs.readFileSync(file, 'utf8')); + if (!hits.length) continue; + perFile[rel] = hits.length; + total += hits.length; + for (const h of hits) lines.push(` ${rel}:${h.line} ${h.kind} ${h.match}`); + } + + if (json) { + console.log(JSON.stringify({ total, files: perFile }, null, 2)); + } else if (!total) { + console.log(`check:tokens — clean (${files.length} files${only.length ? '' : `; ${PENDING.size} pending another branch`})`); + } else { + const ranked = Object.entries(perFile).sort((a, b) => b[1] - a[1]); + console.error(`check:tokens — ${total} raw colour(s) in ${ranked.length} file(s)` + (warn ? ' (--warn: not failing)' : '')); + for (const l of lines.slice(0, 200)) console.error(l); + if (lines.length > 200) console.error(` … and ${lines.length - 200} more (--json for the per-file counts)`); + console.error( + '\nUse the tokens (src/styles/tokens.css; Tailwind: bg-surface-2, text-text-muted, text-ink-bad …). A colour that is' + + '\nDATA (a picker default, a three.js / canvas colour) takes `// tokens-ok: ` on its line.' + ); + } + process.exit(total && !warn ? 1 : 0); +} + +if (require.main === module) main(); + +module.exports = { scanText, stripComments, DEFINITIONS, PENDING }; diff --git a/scripts/redesign-pending.json b/scripts/redesign-pending.json new file mode 100644 index 000000000..851248f64 --- /dev/null +++ b/scripts/redesign-pending.json @@ -0,0 +1,19 @@ +{ + "about": "38 R11: files whose redesign lands from ANOTHER branch, so the R11 gates (check:tokens, tests/unit/noNativeScrollbars.test.js, tests/unit/noFlowbite.test.js) skip them here instead of this branch rewriting them a second time. Each entry names the branch and why. EMPTY THIS LIST when that branch merges: the gates then cover the files, and its versions are already (nearly) clean — whatever they still flag is the merge's to fix.", + "owner": "37-settings (core PR #335 -> feat/1.26): the Settings window redesign rewrote these; its versions use the kit, minimalScroll and the tokens (5 raw colours left across AiSettings/SceneSettings/AiSttSettings/TouchControlsSettings/VRSettingsSection, and Settings.svelte keeps flowbite's Modal)", + "files": [ + "src/components/menu/Settings.svelte", + "src/components/menu/SettingRow.svelte", + "src/components/menu/NodeTypesSection.svelte", + "src/components/menu/AiSttSettings.svelte", + "src/components/menu/TouchControlsSettings.svelte", + "src/components/menu/VRSettingsSection.svelte", + "src/components/menu/settings/AvatarSettings.svelte", + "src/components/menu/settings/CheckpointSettings.svelte", + "src/components/menu/settings/LoadingSettings.svelte", + "src/components/menu/settings/SettingsNav.svelte", + "src/components/menu/settings/SettingsSection.svelte", + "src/components/menu/settings/TextSelectionSettings.svelte", + "src/components/menu/settings/ToursSettings.svelte" + ] +} diff --git a/src/App.svelte b/src/App.svelte index 625aa53cb..6fc738e10 100644 --- a/src/App.svelte +++ b/src/App.svelte @@ -53,6 +53,8 @@ import { startLodGroups } from './lib/lodGroup' import { startKitInstancing } from './lib/kitInstancing' import PlayReticle from './components/play/PlayReticle.svelte' + import PlayBanner from './components/play/PlayBanner.svelte' + import CommandPalette from './components/menu/CommandPalette.svelte' import TouchPlayControls from './components/play/TouchPlayControls.svelte' import DrawToolbar from './components/menu/DrawToolbar.svelte' import SculptToolbar from './components/menu/SculptToolbar.svelte' @@ -426,6 +428,8 @@ import { startMusicToolbox } from './lib/musicToolbox' + + @@ -443,6 +447,8 @@ import { startMusicToolbox } from './lib/musicToolbox' + + diff --git a/src/app.css b/src/app.css index 52768c78b..c6165bbd0 100644 --- a/src/app.css +++ b/src/app.css @@ -2,6 +2,15 @@ @import './styles/ui.css' layer(utilities); @import './styles/ui.utilities.css'; @import './styles/theme.css' layer(utilities); +/* 38 R2: the redesign tokens (SPEC §1) — after theme.css, whose legacy blocks they derive from */ +@import './styles/tokens.css' layer(utilities); +/* 38 R6: the window surface, unlayered so it outranks the ui-panel utility */ +@import './styles/windows.css'; +/* 38 R8: the main HUD's shared look (Controls, Connect, top-right, corner buttons, toasts) */ +@import './styles/hud.css' layer(utilities); +/* 38 R9: the phone shell's placement rules — UNLAYERED on purpose: they re-seat reused + chrome over inline styles and must beat every utility (all scoped to :root.phone-shell) */ +@import './styles/phone.css'; @plugin 'flowbite/plugin'; @@ -22,6 +31,84 @@ --color-primary-900: #a5371b; } +/* 38 R2: the redesign tokens as Tailwind utilities. `inline` so a utility emits + var(--surface-2) itself (resolved per ELEMENT): the colliding names only carry their + SPEC values inside `.tp-ui`, and a theme switch must repaint without a rebuild. + Names: bg-app, bg-surface-1/2/inset, border-border/-strong/-input, text-text/-2/-muted/ + -faint, bg-accent, bg-accent-soft, text-accent-text, bg-live, text-warn-text, bg-danger, + bg-badge text-badge-text; sizes text-badge … text-page-title; rounded-input … rounded-pill; + font-ui / font-ui-mono. (Never --font-mono / --radius-lg here: those would restyle the + existing UI's font-mono / rounded-lg.) */ +@theme inline { + --color-app: var(--bg-app); + --color-surface-1: var(--surface-1); + --color-surface-2: var(--surface-2); + --color-surface-inset: var(--surface-inset); + --color-surface-hover: var(--surface-hover); + --color-border: var(--border); + --color-border-strong: var(--border-strong); + --color-border-input: var(--border-input); + --color-text: var(--text); + --color-text-2: var(--text-2); + --color-text-muted: var(--text-muted); + --color-text-faint: var(--text-faint); + --color-accent: var(--accent); + --color-accent-soft: var(--accent-soft); + --color-accent-muted: var(--accent-muted); + --color-accent-soft-text: var(--accent-soft-text); + --color-accent-text: var(--accent-text); + --color-live: var(--live); + --color-warn-text: var(--warn-text); + --color-danger: var(--danger); + --color-badge: var(--badge-bg); + --color-badge-text: var(--badge-text); + /* 38 R11: the rest of the semantic set, so no component needs a palette colour */ + --color-surface-active: var(--surface-active); + --color-accent-fill: var(--accent-fill); + --color-on-accent: var(--on-accent); + --color-on-live: var(--on-live); + --color-on-danger: var(--on-danger); + --color-ink-bad: var(--ink-bad); + --color-ink-warn: var(--ink-warn); + --color-ink-good: var(--ink-good); + --color-control-off: var(--control-off); + --color-scrim: var(--scrim); + --color-knob: var(--knob); + --color-speaking: var(--speaking); + --color-axis-x: var(--axis-x); + --color-axis-y: var(--axis-y); + --color-axis-z: var(--axis-z); + --color-icon-folder: var(--icon-folder); + --color-icon-image: var(--icon-image); + --color-icon-audio: var(--icon-audio); + --color-icon-doc: var(--icon-doc); + --color-icon-object: var(--icon-object); + --color-icon-prefab: var(--icon-prefab); + --color-icon-danger: var(--icon-danger); + --color-icon-warning: var(--icon-warning); + --color-icon-accent: var(--icon-accent); + --text-badge: var(--fs-badge); + --text-section: var(--fs-section); + --text-desc: var(--fs-desc); + --text-body: var(--fs-body); + --text-panel-title: var(--fs-panel-title); + --text-modal-title: var(--fs-modal-title); + --text-page-title: var(--fs-page-title); +} +/* radii + the two UI fonts are plain values, so they live HERE (one name for the CSS var and + the utility; `static` emits them on :root even before a utility uses them). The green and + 8-bit themes override --font-ui/--font-ui-mono in tokens.css. */ +@theme static { + --radius-input: 6px; + --radius-button: 8px; + --radius-card: 10px; + --radius-window: 12px; + --radius-modal: 14px; + --radius-pill: 999px; + --font-ui: 'IBM Plex Sans', ui-sans-serif, system-ui, sans-serif; + --font-ui-mono: 'IBM Plex Mono', ui-monospace, 'Cascadia Mono', Consolas, monospace; +} + /* The default border color has changed to `currentcolor` in Tailwind CSS v4, so we've added these compatibility styles to make sure everything still @@ -40,26 +127,22 @@ } } -/* width */ -::-webkit-scrollbar { - width: 10px; -} - -/* Track */ -::-webkit-scrollbar-track { - background: #888; - border-radius: 5px; +/* 38 R11 (NOTES-38 #1): NO native scrollbar anywhere in the app. Every element that scrolls + draws the app's minimal overlay thumb (`use:minimalScroll`, $lib/ui/minimalScroll.js) or + scrolls with no bar by design (`tp-noscrollbar`: chip/tab strips, the burger menu); + tests/unit/noNativeScrollbars.test.js fails on a scroller that says neither, and on any + stylesheet that styles a native bar back in. This rule is the safety net under that lint + (a third-party or dynamic scroller still shows no platform bar). */ +* { + scrollbar-width: none; } - -/* Handle */ -::-webkit-scrollbar-thumb { - background: #000; - border-radius: 5px; +*::-webkit-scrollbar { + display: none; + width: 0; + height: 0; } - -/* Handle on hover */ -::-webkit-scrollbar-thumb:hover { - background: #555; +.tp-noscrollbar { + scrollbar-width: none; } /* Removes Flowbite button borders */ diff --git a/src/components/AnnotationPins.svelte b/src/components/AnnotationPins.svelte index c03a71a5e..118cd679e 100644 --- a/src/components/AnnotationPins.svelte +++ b/src/components/AnnotationPins.svelte @@ -295,6 +295,7 @@ side={THREE.DoubleSide} /> + + {/if} diff --git a/src/components/ContextLostOverlay.svelte b/src/components/ContextLostOverlay.svelte index 294cb01c7..6109e9ea7 100644 --- a/src/components/ContextLostOverlay.svelte +++ b/src/components/ContextLostOverlay.svelte @@ -45,7 +45,7 @@ display: flex; align-items: center; justify-content: center; - background: rgba(0, 0, 0, 0.72); + background: var(--scrim); backdrop-filter: blur(2px); } .gl-lost-card { @@ -53,9 +53,10 @@ margin: 16px; padding: 20px 22px; border-radius: 10px; - background: var(--surface, #1f2937); - color: var(--text, #fff); - box-shadow: 0 18px 48px rgba(0, 0, 0, 0.5); + background: var(--surface-1); + color: var(--text); + border: 1px solid var(--border); + box-shadow: var(--shadow-window); } .gl-lost-card h2 { margin: 0 0 8px; @@ -66,7 +67,7 @@ margin: 0 0 16px; font-size: 13px; line-height: 1.5; - color: var(--text-2, #d1d5db); + color: var(--text-2); } .gl-lost-actions { display: flex; @@ -78,17 +79,19 @@ border: 0; border-radius: 7px; font-size: 13px; - color: #fff; - background: #374151; + color: var(--text); + background: var(--surface-2); cursor: pointer; } .gl-lost-btn:hover { - background: #4b5563; + background: var(--surface-active); } .gl-lost-primary { - background: #2563eb; + color: var(--on-accent); + background: var(--accent-fill); } .gl-lost-primary:hover { - background: #1d4ed8; + background: var(--accent-fill); + filter: brightness(0.92); } diff --git a/src/components/ContextMenu.svelte b/src/components/ContextMenu.svelte index 2f20f383f..4fed80151 100644 --- a/src/components/ContextMenu.svelte +++ b/src/components/ContextMenu.svelte @@ -3,6 +3,7 @@ import ContextMenuItems from './ContextMenuItems.svelte'; import Icon from './ui/Icon.svelte'; import { collectLeaves, rankMatches } from '$lib/menuFilter'; + import { minimalScroll } from '$lib/ui/minimalScroll.js'; import { autofocusOk, typeToFocus } from '$lib/inputDevice'; import { safeStorage } from '$lib/safeStorage'; @@ -107,6 +108,16 @@ * opened. Requiring the pointerdown too makes the backdrop dismiss only real * outside taps. */ let backdropPressed = false; + /** 38 R9: the same rule for the MENU itself. On a phone the menu is a bottom sheet, + * so a long press low on the screen opens it UNDER the finger and the lift used to + * tap whatever row landed there. A touch click counts only when its press also + * landed in the menu (keyboard and synthetic clicks carry no pointerType). */ + let menuPressed = false; + function guardLift(e: MouseEvent) { + if ((e as PointerEvent).pointerType !== 'touch' || menuPressed) return; + e.stopPropagation(); + e.preventDefault(); + } /** how many rows the empty-query browse list shows (it scrolls) */ const BROWSE_CAP = 200; /** 16-Q5: default height of the SEARCH list. A menu that unfolds down the whole @@ -401,14 +412,17 @@ exists, this menu is already mounted under the cursor and IS its target. No blocker on the surface that was right-clicked can help: the event never reaches it, in either phase. Submenus are DOM children here, so they bubble to this. --> - +