From e22218a01f418fffc89d0c16d88ada84a764b448 Mon Sep 17 00:00:00 2001 From: Yasutake Yohei <61961825+yasutakeyohei@users.noreply.github.com> Date: Mon, 5 Oct 2026 13:13:01 +0900 Subject: bluebey-studio: スマホのパネルをハンドルのドラッグで上下に動かせるように MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- public/bluebey-studio/src/main.js | 50 +++++++++++++++++++++++++++++++++++++ public/bluebey-studio/src/style.css | 33 +++++++++++++++++------- 2 files changed, 74 insertions(+), 9 deletions(-) diff --git a/public/bluebey-studio/src/main.js b/public/bluebey-studio/src/main.js index e282e9d..49494a4 100644 --- a/public/bluebey-studio/src/main.js +++ b/public/bluebey-studio/src/main.js @@ -1356,6 +1356,56 @@ async function init() { const panel = buildPanel(app, document.getElementById('panel')); app.panel = panel; + // On a phone the panel is a bottom sheet. Its grab handle drags the sheet's top + // edge up and down, so the menu can be pulled out of the way of the character. + const panelEl = document.getElementById('panel'); + const panelGrip = document.createElement('div'); + panelGrip.className = 'panel-grip'; + panelGrip.setAttribute('aria-hidden', 'true'); + panelEl.prepend(panelGrip); + installSheetDrag(panelEl, panelGrip); + + /** Drag on the phone sheet's handle; a no-op anywhere else. */ + function installSheetDrag(sheet, grip) { + const phone = window.matchMedia('(max-width: 768px), (max-height: 500px) and (pointer: coarse)'); + let drag = null; + const minHeight = () => Math.round(window.innerHeight * 0.16); + const maxHeight = () => Math.round(window.innerHeight * 0.94); + + grip.addEventListener('pointerdown', (event) => { + if (!phone.matches) return; + const rect = sheet.getBoundingClientRect(); + // Start from the height the sheet has now, so the first move does not jump. + drag = { id: event.pointerId, fromY: event.clientY, startH: rect.height }; + sheet.style.maxHeight = 'none'; + sheet.style.height = `${rect.height}px`; + try { grip.setPointerCapture(event.pointerId); } catch { /* capture is optional */ } + event.preventDefault(); + }); + grip.addEventListener('pointermove', (event) => { + if (!drag || event.pointerId !== drag.id) return; + // Dragging up grows the sheet (its top edge climbs); dragging down shrinks it. + const next = drag.startH + (drag.fromY - event.clientY); + sheet.style.height = `${Math.min(maxHeight(), Math.max(minHeight(), next))}px`; + event.preventDefault(); + }); + const end = (event) => { + if (!drag || (event && event.pointerId !== drag.id)) return; + drag = null; + }; + grip.addEventListener('pointerup', end); + grip.addEventListener('pointercancel', end); + + // A window grown to the desktop layout keeps the side column: drop the height + // the drag had fixed, so the panel goes back to filling the screen. + const clearOnDesktop = () => { + if (phone.matches) return; + sheet.style.height = ''; + sheet.style.maxHeight = ''; + }; + window.addEventListener('resize', clearOnDesktop); + } + Object.assign(app.actions, { capturePose, applyState, diff --git a/public/bluebey-studio/src/style.css b/public/bluebey-studio/src/style.css index efd0a18..f4c9270 100644 --- a/public/bluebey-studio/src/style.css +++ b/public/bluebey-studio/src/style.css @@ -132,6 +132,10 @@ body:not(.panel-hidden) #stage { #panel::-webkit-scrollbar { width: 9px; } #panel::-webkit-scrollbar-thumb { background: #d8d1e8; border-radius: 9px; } +/* The phone sheet's grab handle is a real element (built in the app) so it can + carry a drag; it is hidden everywhere else. */ +.panel-grip { display: none; } + body.panel-hidden #panel { display: none; } body.panel-hidden #topbar { max-width: calc(100vw - 48px); } @@ -203,33 +207,44 @@ body.panel-hidden #topbar { max-width: calc(100vw - 48px); } bottom: 0; width: auto; max-height: 65dvh; - padding: 20px 14px calc(20px + env(safe-area-inset-bottom)); + padding: 0 14px calc(20px + env(safe-area-inset-bottom)); border-left: 0; border-top: 1px solid var(--border); border-radius: 18px 18px 0 0; box-shadow: 0 -6px 24px rgba(40, 20, 80, 0.18); } - /* A grab handle so the sheet reads as an edge that opens and closes; it - sticks to the top of the scroll area as the sheet is scrolled. */ - #panel::before { - content: ""; + /* The grab handle: sticky at the top of the scroll area, so it stays grabbable + however far the menu is scrolled, and `touch-action: none` so a drag on it + resizes the sheet instead of scrolling it. */ + .panel-grip { + display: flex; + align-items: center; + justify-content: center; position: sticky; top: 0; - display: block; + z-index: 6; + height: 24px; + margin: 0 -14px; + background: var(--card); + touch-action: none; + cursor: grab; + } + .panel-grip::after { + content: ""; width: 44px; height: 5px; - margin: 0 auto 12px; border-radius: 999px; background: #cfc6e4; } + .panel-grip:active { cursor: grabbing; } body.panel-hidden #topbar { max-width: none; } /* The grab handle is sticky at the top of the sheet, so the action bar and the tabs start below it. */ - .panel-topbar { top: 17px; } - .tabs { top: 51px; } + .panel-topbar { top: 24px; } + .tabs { top: 58px; } } /* ----------------------------------------------------------------- sections */ -- cgit v1.3.1