From c05d1c3f1baca1332c7abe3ca4963aacccb85125 Mon Sep 17 00:00:00 2001 From: arthurcai Date: Sat, 14 Mar 2026 20:48:30 +0800 Subject: [PATCH 01/10] fix: add .dockerignore to prevent sensitive files in Docker context Add backend/ and frontend/ .dockerignore files to exclude .env, secrets, IDE configs, build artifacts, and other non-source files from the Docker build context. Resolves SonarCloud security hotspot about recursive COPY potentially including sensitive data. --- backend/.dockerignore | 26 ++++++++++++++++++++++++++ frontend/.dockerignore | 27 +++++++++++++++++++++++++++ 2 files changed, 53 insertions(+) create mode 100644 backend/.dockerignore create mode 100644 frontend/.dockerignore diff --git a/backend/.dockerignore b/backend/.dockerignore new file mode 100644 index 00000000..ee6556b9 --- /dev/null +++ b/backend/.dockerignore @@ -0,0 +1,26 @@ +# Secrets & environment +.env +.env.* +!.env.example + +# Build artifacts +bin +.cache +uploads + +# IDE & editor +.idea +.vscode +*.swp +*.swo +*~ + +# Docker config +Dockerfile +.dockerignore + +# Docs & misc +*.md +*.log +.DS_Store +Makefile diff --git a/frontend/.dockerignore b/frontend/.dockerignore new file mode 100644 index 00000000..898014ec --- /dev/null +++ b/frontend/.dockerignore @@ -0,0 +1,27 @@ +# Secrets & environment +.env +.env.* +!.env.example + +# Dependencies & build output +node_modules +dist +coverage +*.tsbuildinfo +.eslintcache + +# IDE & editor +.idea +.vscode +*.swp +*.swo +*~ + +# Docker config +Dockerfile +.dockerignore + +# Docs & misc +*.md +*.log +.DS_Store From db3ae627237c5c65b0d03ccbfc291a18eb68054c Mon Sep 17 00:00:00 2001 From: arthurcai Date: Sat, 14 Mar 2026 22:41:33 +0800 Subject: [PATCH 02/10] =?UTF-8?q?feat:=20add=20mobile=20infrastructure=20?= =?UTF-8?q?=E2=80=94=20viewport=20fix,=20PWA=20support,=20useIsMobile?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Fix #app not filling viewport with width/height/overflow in reset.css - Add mobile.css with orientation-specific styles for portrait/landscape - Add useIsMobile composable for reactive breakpoint detection (768/480px) - Import mobile.css in main.ts - Fix deprecated apple-mobile-web-app-capable meta tag - Add SVG favicon, PWA manifest.json, service worker, and icon assets - Add public/ to ESLint ignores (static assets) --- frontend/eslint.config.js | 60 ++++++++++----------- frontend/index.html | 11 +++- frontend/public/favicon.svg | 5 ++ frontend/public/icons/icon-192.png | 3 ++ frontend/public/icons/icon-512.png | 3 ++ frontend/public/icons/icon-maskable-512.png | 3 ++ frontend/public/manifest.json | 15 ++++++ frontend/public/sw.js | 31 +++++++++++ frontend/src/composables/useIsMobile.ts | 29 ++++++++++ frontend/src/main.ts | 5 ++ frontend/src/styles/mobile.css | 42 +++++++++++++++ frontend/src/styles/reset.css | 7 +++ 12 files changed, 182 insertions(+), 32 deletions(-) create mode 100644 frontend/public/favicon.svg create mode 100644 frontend/public/icons/icon-192.png create mode 100644 frontend/public/icons/icon-512.png create mode 100644 frontend/public/icons/icon-maskable-512.png create mode 100644 frontend/public/manifest.json create mode 100644 frontend/public/sw.js create mode 100644 frontend/src/composables/useIsMobile.ts create mode 100644 frontend/src/styles/mobile.css diff --git a/frontend/eslint.config.js b/frontend/eslint.config.js index 848fa7f8..d124fbb3 100644 --- a/frontend/eslint.config.js +++ b/frontend/eslint.config.js @@ -1,57 +1,57 @@ -import eslint from '@eslint/js' -import tseslint from 'typescript-eslint' -import pluginVue from 'eslint-plugin-vue' +import eslint from "@eslint/js"; +import tseslint from "typescript-eslint"; +import pluginVue from "eslint-plugin-vue"; export default tseslint.config( eslint.configs.recommended, ...tseslint.configs.recommended, - ...pluginVue.configs['flat/recommended'], + ...pluginVue.configs["flat/recommended"], { - files: ['**/*.vue'], + files: ["**/*.vue"], languageOptions: { parserOptions: { parser: tseslint.parser, }, globals: { - HTMLElement: 'readonly', - HTMLStyleElement: 'readonly', - HTMLCanvasElement: 'readonly', - HTMLImageElement: 'readonly', - document: 'readonly', - window: 'readonly', - requestAnimationFrame: 'readonly', - cancelAnimationFrame: 'readonly', - setTimeout: 'readonly', - clearTimeout: 'readonly', - setInterval: 'readonly', - clearInterval: 'readonly', - console: 'readonly', - Image: 'readonly', + HTMLElement: "readonly", + HTMLStyleElement: "readonly", + HTMLCanvasElement: "readonly", + HTMLImageElement: "readonly", + document: "readonly", + window: "readonly", + requestAnimationFrame: "readonly", + cancelAnimationFrame: "readonly", + setTimeout: "readonly", + clearTimeout: "readonly", + setInterval: "readonly", + clearInterval: "readonly", + console: "readonly", + Image: "readonly", }, }, }, { - files: ['**/*.vue'], + files: ["**/*.vue"], rules: { - 'no-undef': 'off', + "no-undef": "off", }, }, { - files: ['env.d.ts'], + files: ["env.d.ts"], rules: { - '@typescript-eslint/no-empty-object-type': 'off', - '@typescript-eslint/no-explicit-any': 'off', + "@typescript-eslint/no-empty-object-type": "off", + "@typescript-eslint/no-explicit-any": "off", }, }, { rules: { - 'vue/max-attributes-per-line': 'off', - 'vue/singleline-html-element-content-newline': 'off', - 'vue/html-self-closing': 'off', - 'vue/attributes-order': 'off', + "vue/max-attributes-per-line": "off", + "vue/singleline-html-element-content-newline": "off", + "vue/html-self-closing": "off", + "vue/attributes-order": "off", }, }, { - ignores: ['dist/', 'node_modules/'], + ignores: ["dist/", "node_modules/", "public/"], }, -) +); diff --git a/frontend/index.html b/frontend/index.html index c9d0907f..90a40b82 100755 --- a/frontend/index.html +++ b/frontend/index.html @@ -2,8 +2,15 @@ - - Sunset Beach + + + + + + + + + MomShell
diff --git a/frontend/public/favicon.svg b/frontend/public/favicon.svg new file mode 100644 index 00000000..2e49bee5 --- /dev/null +++ b/frontend/public/favicon.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/frontend/public/icons/icon-192.png b/frontend/public/icons/icon-192.png new file mode 100644 index 00000000..3e24fc01 --- /dev/null +++ b/frontend/public/icons/icon-192.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:eea6cc229d1ba3abe94d5cf9c463625a624a21dbaa604c159944c231031ebd6d +size 594 diff --git a/frontend/public/icons/icon-512.png b/frontend/public/icons/icon-512.png new file mode 100644 index 00000000..7ee17b73 --- /dev/null +++ b/frontend/public/icons/icon-512.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:700d233f5cfac21820e5e46df1dd856f4082ec609cf2b8b86a51e37560e2a935 +size 2201 diff --git a/frontend/public/icons/icon-maskable-512.png b/frontend/public/icons/icon-maskable-512.png new file mode 100644 index 00000000..7ee17b73 --- /dev/null +++ b/frontend/public/icons/icon-maskable-512.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:700d233f5cfac21820e5e46df1dd856f4082ec609cf2b8b86a51e37560e2a935 +size 2201 diff --git a/frontend/public/manifest.json b/frontend/public/manifest.json new file mode 100644 index 00000000..bc75f10d --- /dev/null +++ b/frontend/public/manifest.json @@ -0,0 +1,15 @@ +{ + "name": "MomShell - 贝壳回响", + "short_name": "MomShell", + "description": "亲密关系成长平台", + "start_url": "/", + "display": "standalone", + "orientation": "any", + "background_color": "#f5e0c8", + "theme_color": "#f2b8c8", + "icons": [ + { "src": "/icons/icon-192.png", "sizes": "192x192", "type": "image/png" }, + { "src": "/icons/icon-512.png", "sizes": "512x512", "type": "image/png" }, + { "src": "/icons/icon-maskable-512.png", "sizes": "512x512", "type": "image/png", "purpose": "maskable" } + ] +} diff --git a/frontend/public/sw.js b/frontend/public/sw.js new file mode 100644 index 00000000..00c26b57 --- /dev/null +++ b/frontend/public/sw.js @@ -0,0 +1,31 @@ +/* eslint-disable no-undef */ +const CACHE_NAME = "momshell-v1"; +const PRECACHE_URLS = ["/", "/index.html"]; + +self.addEventListener("install", (event) => { + event.waitUntil( + caches.open(CACHE_NAME).then((cache) => cache.addAll(PRECACHE_URLS)), + ); + self.skipWaiting(); +}); + +self.addEventListener("activate", (event) => { + event.waitUntil( + caches + .keys() + .then((keys) => + Promise.all( + keys.filter((k) => k !== CACHE_NAME).map((k) => caches.delete(k)), + ), + ), + ); + self.clients.claim(); +}); + +self.addEventListener("fetch", (event) => { + if (event.request.mode === "navigate") { + event.respondWith( + fetch(event.request).catch(() => caches.match("/index.html")), + ); + } +}); diff --git a/frontend/src/composables/useIsMobile.ts b/frontend/src/composables/useIsMobile.ts new file mode 100644 index 00000000..0332ca7c --- /dev/null +++ b/frontend/src/composables/useIsMobile.ts @@ -0,0 +1,29 @@ +import { ref, onMounted, onUnmounted } from 'vue' + +export function useIsMobile() { + const isMobile = ref(false) + const isSmall = ref(false) + + function update() { + isMobile.value = window.matchMedia('(max-width: 768px)').matches + isSmall.value = window.matchMedia('(max-width: 480px)').matches + } + + let mql768: MediaQueryList + let mql480: MediaQueryList + + onMounted(() => { + mql768 = window.matchMedia('(max-width: 768px)') + mql480 = window.matchMedia('(max-width: 480px)') + update() + mql768.addEventListener('change', update) + mql480.addEventListener('change', update) + }) + + onUnmounted(() => { + mql768?.removeEventListener('change', update) + mql480?.removeEventListener('change', update) + }) + + return { isMobile, isSmall } +} diff --git a/frontend/src/main.ts b/frontend/src/main.ts index 725c0635..554acd61 100644 --- a/frontend/src/main.ts +++ b/frontend/src/main.ts @@ -5,7 +5,12 @@ import App from "./App.vue"; import "./styles/reset.css"; import "./styles/variables.css"; import "./styles/animations.css"; +import "./styles/mobile.css"; const app = createApp(App); app.use(createPinia()); app.mount("#app"); + +if ("serviceWorker" in navigator) { + navigator.serviceWorker.register("/sw.js").catch(() => {}); +} diff --git a/frontend/src/styles/mobile.css b/frontend/src/styles/mobile.css new file mode 100644 index 00000000..22b9b6b3 --- /dev/null +++ b/frontend/src/styles/mobile.css @@ -0,0 +1,42 @@ +/* ── Global mobile overrides ── */ + +@media (max-width: 768px) { + body { + overscroll-behavior: none; + -webkit-overflow-scrolling: touch; + touch-action: pan-x; + } + + /* Prevent any accidental overflow causing gray margins */ + html, body, #app { + width: 100%; + height: 100%; + overflow: hidden; + } +} + +/* ── Portrait orientation on mobile ── */ +@media (max-width: 768px) and (orientation: portrait) { + /* In portrait mode, the scene is tall and narrow. + The parallax still works horizontally. */ + .scene { + height: 100dvh; + height: 100vh; + } +} + +/* ── Landscape orientation on mobile ── */ +@media (max-height: 500px) and (orientation: landscape) { + /* In landscape, viewport is short but wide. + Ensure the scene fills the height. */ + .scene { + height: 100dvh; + height: 100vh; + } + + /* NavBar needs to be more compact in landscape */ + .nav-wrapper { + top: 8px; + right: 8px; + } +} diff --git a/frontend/src/styles/reset.css b/frontend/src/styles/reset.css index 06ccb246..e811f745 100644 --- a/frontend/src/styles/reset.css +++ b/frontend/src/styles/reset.css @@ -14,6 +14,13 @@ html, body { background: #f5e0c8; } +#app { + width: 100%; + height: 100%; + overflow: hidden; + position: relative; +} + body.dragging { cursor: grabbing; } From 1f77a4ccf5c81611fef6393ed525dc851966b77e Mon Sep 17 00:00:00 2001 From: arthurcai Date: Sat, 14 Mar 2026 22:42:11 +0800 Subject: [PATCH 03/10] feat: add orientation change support for parallax scene - Listen for orientationchange event with 150ms delay for recalculation - Add background-color fallback to BeachScene --- frontend/src/components/scene/BeachScene.vue | 2 ++ frontend/src/composables/useParallax.ts | 8 ++++++++ 2 files changed, 10 insertions(+) diff --git a/frontend/src/components/scene/BeachScene.vue b/frontend/src/components/scene/BeachScene.vue index 3a1f7ce6..e94aa0dc 100644 --- a/frontend/src/components/scene/BeachScene.vue +++ b/frontend/src/components/scene/BeachScene.vue @@ -63,6 +63,8 @@ uiStore.setParallaxScrollTo(scrollTo) position: relative; width: 100%; height: 100vh; + height: 100dvh; overflow: hidden; + background: #f5e0c8; } diff --git a/frontend/src/composables/useParallax.ts b/frontend/src/composables/useParallax.ts index 0f1814ba..ddfcf5c1 100644 --- a/frontend/src/composables/useParallax.ts +++ b/frontend/src/composables/useParallax.ts @@ -209,6 +209,14 @@ export function useParallax() { cancelAnimationFrame(resizeRaf); resizeRaf = requestAnimationFrame(recalcParallax); }); + window.addEventListener("orientationchange", () => { + // Delay recalc to let the browser settle after orientation change + setTimeout(() => { + recalcParallax(); + applyParallax(); + startLoop(); + }, 150); + }); setTimeout(() => { hintHidden.value = true; From 3b7be6bf4e034c891aa035c79f3b033d551734a3 Mon Sep 17 00:00:00 2001 From: arthurcai Date: Sat, 14 Mar 2026 22:42:54 +0800 Subject: [PATCH 04/10] feat: add responsive sprite positioning and mobile scene UI - Add mobile override field to SpriteData for responsive positioning - Use useIsMobile in SpritesLayer to apply mobile-specific top/left/width - Add mobile max-width and text wrapping to speech bubbles - Add mobile touch target sizes and spacing to NavBar and HintOverlay --- frontend/src/components/scene/HintOverlay.vue | 15 +- frontend/src/components/scene/NavBar.vue | 22 +++ .../src/components/scene/SpritesLayer.vue | 44 ++++-- frontend/src/constants/sprites.ts | 132 ++++++++++++++---- 4 files changed, 175 insertions(+), 38 deletions(-) diff --git a/frontend/src/components/scene/HintOverlay.vue b/frontend/src/components/scene/HintOverlay.vue index bca96f08..4e7bf663 100644 --- a/frontend/src/components/scene/HintOverlay.vue +++ b/frontend/src/components/scene/HintOverlay.vue @@ -1,13 +1,17 @@ diff --git a/frontend/src/components/scene/NavBar.vue b/frontend/src/components/scene/NavBar.vue index 9be6effd..b73abf3e 100644 --- a/frontend/src/components/scene/NavBar.vue +++ b/frontend/src/components/scene/NavBar.vue @@ -254,6 +254,28 @@ const showNav = computed(() => { opacity: 0; transform: translateY(-8px) scale(0.95); } + +/* ── Mobile ── */ +@media (max-width: 768px) { + .nav-toggle { + width: 44px; + height: 44px; + } + + .nav-item { + padding: 12px 16px; + min-height: 44px; + } + + .nav-label { + font-size: 14px; + } + + .nav-icon { + width: 22px; + height: 22px; + } +} diff --git a/frontend/src/components/scene/SpritesLayer.vue b/frontend/src/components/scene/SpritesLayer.vue index 8c27b0cb..57d3c870 100644 --- a/frontend/src/components/scene/SpritesLayer.vue +++ b/frontend/src/components/scene/SpritesLayer.vue @@ -5,12 +5,7 @@ :key="s.id" :id="`sprite-${s.id}`" :class="['sprite-wrapper', { clickable: isSpriteClickable(s.id) }]" - :style="{ - left: s.left, - top: s.top, - width: s.width, - zIndex: s.zIndex ?? undefined, - }" + :style="spriteStyle(s)" > (null) const bubbleLayerEl = ref(null) -const crabSpriteEl = ref(null) +const crabSpriteEl = ref(null) const ctx = inject(PARALLAX_KEY)! const uiStore = useUiStore() const authStore = useAuthStore() @@ -106,7 +113,7 @@ function setSpriteEl(id: string, element: Element | ComponentPublicInstance | nu return } - crabSpriteEl.value = element instanceof HTMLImageElement ? element : null + crabSpriteEl.value = element instanceof HTMLElement ? element : null } function updateCrabBubblePosition() { @@ -247,6 +254,17 @@ onUnmounted(() => { user-select: none; } +@media (max-width: 768px) { + .sprite-wrapper.clickable { + min-width: 44px; + min-height: 44px; + } + + .sprite-label { + font-size: 0.7rem; + } +} + .speech-bubble { position: absolute; background: rgba(255, 252, 246, 0.96); @@ -257,12 +275,22 @@ onUnmounted(() => { border-radius: 1.1em; white-space: nowrap; width: max-content; + max-width: 80vw; pointer-events: none; box-shadow: 0 10px 24px rgba(0, 0, 0, 0.16); border: 1px solid rgba(255, 214, 170, 0.7); z-index: 1; } +@media (max-width: 768px) { + .speech-bubble { + white-space: normal; + width: auto; + max-width: min(280px, 70vw); + font-size: 0.85rem; + } +} + .crab-bubble { --bubble-shift-x: -10%; --bubble-shift-y: calc(-100% - 0.5rem); diff --git a/frontend/src/constants/sprites.ts b/frontend/src/constants/sprites.ts index ad1ac9f2..b79bb9dc 100644 --- a/frontend/src/constants/sprites.ts +++ b/frontend/src/constants/sprites.ts @@ -1,40 +1,114 @@ export interface SpriteData { - id: string - src: string - left: string - top: string - width: string - rotate?: number - scaleX?: number - scaleY?: number - zIndex?: number - label?: string - labelSize?: string // e.g. '1rem', '0.9rem' - labelOffsetY?: string // e.g. '-20%', '4px' + id: string; + src: string; + left: string; + top: string; + width: string; + rotate?: number; + scaleX?: number; + scaleY?: number; + zIndex?: number; + label?: string; + labelSize?: string; // e.g. '1rem', '0.9rem' + labelOffsetY?: string; // e.g. '-20%', '4px' + /** Override position/size on mobile (max-width: 768px) */ + mobile?: { + left?: string; + top?: string; + width?: string; + }; } -import carImg from '@/assets/images/car.png' -import barImg from '@/assets/images/bar.png' -import stoneImg from '@/assets/images/stone.png' -import crabImg from '@/assets/images/crab.png' -import shellImg from '@/assets/images/shell.png' -import chairImg from '@/assets/images/chairs.png' -import mailboxImg from '@/assets/images/mailbox.png' +import carImg from "@/assets/images/car.png"; +import barImg from "@/assets/images/bar.png"; +import stoneImg from "@/assets/images/stone.png"; +import crabImg from "@/assets/images/crab.png"; +import shellImg from "@/assets/images/shell.png"; +import chairImg from "@/assets/images/chairs.png"; +import mailboxImg from "@/assets/images/mailbox.png"; export const SPRITES: SpriteData[] = [ + { + id: "car", + src: carImg, + left: "40%", + top: "-2.5%", + width: "50vw", + label: "个人中心", + labelSize: "1.2rem", + labelOffsetY: "-7%", + mobile: { width: "70vw", top: "18%" }, + }, - { id: 'car', src: carImg, left: '40%', top: '-2.5%', width: '50vw', label: '个人中心', labelSize: '1.2rem', labelOffsetY: '-7%' }, + { + id: "bar", + src: barImg, + left: "24%", + top: "-15%", + width: "60vw", + label: "智育社区", + labelSize: "1.2rem", + labelOffsetY: "-9%", + mobile: { width: "85vw", top: "12%" }, + }, - { id: 'bar', src: barImg, left: '24%', top: '-15%', width: '60vw', label: '智育社区', labelSize: '1.2rem', labelOffsetY: '-9%' }, + { + id: "stone", + src: stoneImg, + left: "68%", + top: "20%", + width: "30vw", + label: "智聊助手", + labelSize: "1.2rem", + labelOffsetY: "-18%", + mobile: { width: "45vw", top: "16%" }, + }, - { id: 'stone', src: stoneImg, left: '68%', top: '20%', width: '30vw', label: '智聊助手', labelSize: '1.2rem', labelOffsetY: '-18%' }, + { + id: "crab", + src: crabImg, + left: "55%", + top: "12%", + width: "6vw", + zIndex: 10, + mobile: { width: "10vw", top: "30%" }, + }, - { id: 'crab', src: crabImg, left: '55%', top: '12%', width: '6vw', zIndex: 10 }, + { + id: "shell", + src: shellImg, + left: "50.75%", + top: "60%", + width: "5vw", + rotate: 0, + zIndex: 10, + label: "生成相片", + labelSize: "1.2rem", + mobile: { width: "10vw", top: "60%" }, + }, - { id: 'shell', src: shellImg, left: '50.75%', top: '60%', width: '5vw', rotate: 0, zIndex: 10, label: '生成相片', labelSize: '1.2rem' }, + { + id: "chair", + src: chairImg, + left: "56%", + top: "2.5%", + width: "40vw", + rotate: 0, + label: "同频任务", + labelSize: "1.2rem", + labelOffsetY: "-6%", + mobile: { width: "55vw", top: "10%" }, + }, - { id: 'chair', src: chairImg, left: '56%', top: '2.5%', width: '40vw', rotate: 0, label: '同频任务', labelSize: '1.2rem', labelOffsetY: '-6%' }, - - { id: 'mailbox', src: mailboxImg, left: '51.5%', top: '22%', width: '12vw', rotate: 0, label: '心愿签', labelSize: '1.2rem' }, - -] + { + id: "mailbox", + src: mailboxImg, + left: "51.5%", + top: "22%", + width: "12vw", + rotate: 0, + label: "心愿签", + labelSize: "1.2rem", + mobile: { width: "18vw", top: "35%" }, + }, +]; From 1f0b2da26ff21a2149d29f94708826fd4d29fde5 Mon Sep 17 00:00:00 2001 From: arthurcai Date: Sat, 14 Mar 2026 22:43:35 +0800 Subject: [PATCH 05/10] feat: add mobile and landscape styles to overlay panel framework - Make overlay panels fill viewport on mobile (100vw x 100dvh) - Add landscape media query for 80vw width with rounded corners - Add landscape layout for RoleSelectPanel with side-by-side roles --- .../src/components/overlay/OverlayPanel.vue | 52 +++++++++++++++++++ .../components/overlay/RoleSelectPanel.vue | 28 +++++++++- 2 files changed, 79 insertions(+), 1 deletion(-) diff --git a/frontend/src/components/overlay/OverlayPanel.vue b/frontend/src/components/overlay/OverlayPanel.vue index 050381fd..8719ed78 100644 --- a/frontend/src/components/overlay/OverlayPanel.vue +++ b/frontend/src/components/overlay/OverlayPanel.vue @@ -165,4 +165,56 @@ function onBackdropClick() { transform: scale(0.97); opacity: 0; } + +/* ── Mobile ── */ +@media (max-width: 768px) { + .overlay-center { + width: 100vw; + height: 100dvh; + height: 100vh; + max-height: 100dvh; + max-height: 100vh; + border-radius: 0; + } + + .overlay-right { + width: 100vw; + height: 100dvh; + height: 100vh; + max-height: 100dvh; + max-height: 100vh; + border-radius: 0; + } + + .overlay-fullscreen { + height: 100dvh; + height: 100vh; + } + + .overlay-panel { + border-radius: 0; + } + + .overlay-close { + width: 44px; + height: 44px; + top: 12px; + right: 12px; + } +} + +/* ── Mobile landscape ── */ +@media (max-height: 500px) and (orientation: landscape) { + .overlay-center, + .overlay-right { + width: 80vw; + max-height: 100dvh; + max-height: 100vh; + border-radius: 12px; + } + + .overlay-panel { + border-radius: 12px; + } +} diff --git a/frontend/src/components/overlay/RoleSelectPanel.vue b/frontend/src/components/overlay/RoleSelectPanel.vue index 152a67c7..5320a441 100644 --- a/frontend/src/components/overlay/RoleSelectPanel.vue +++ b/frontend/src/components/overlay/RoleSelectPanel.vue @@ -154,12 +154,38 @@ async function onConfirm() { color: rgba(255, 255, 255, 0.5); } -@media (max-width: 640px) { +@media (max-width: 768px) { .role-select { flex-direction: column; + height: 100dvh; + height: 100vh; } .role-half { + height: 50dvh; height: 50vh; + padding: 24px 16px; + } + .role-title { + font-size: 24px; + } + .role-subtitle { + font-size: 13px; + } +} + +/* Mobile landscape: side-by-side layout */ +@media (max-height: 500px) and (orientation: landscape) { + .role-select { + flex-direction: row; + height: 100dvh; + height: 100vh; + } + .role-half { + height: 100%; + padding: 16px; + } + .role-title { + font-size: 22px; } } From 054d5aa295395a37eccff301caaaed502cb8cc9a Mon Sep 17 00:00:00 2001 From: arthurcai Date: Sat, 14 Mar 2026 22:49:20 +0800 Subject: [PATCH 06/10] feat: add mobile styles to individual overlay panels Add responsive mobile and landscape styles to AuthPanel, ChatPanel, WhisperPanel, MemoryPanel, TaskPanel, AiMemoryPanel, CommunityPanel, and LandingOverlay for proper display on small screens. --- .../src/components/overlay/AiMemoryPanel.vue | 29 ++++++++++++++ frontend/src/components/overlay/AuthPanel.vue | 22 +++++++++++ frontend/src/components/overlay/ChatPanel.vue | 33 ++++++++++++++++ .../src/components/overlay/CommunityPanel.vue | 32 ++++++++++++++++ .../src/components/overlay/LandingOverlay.vue | 32 ++++++++++++++++ .../src/components/overlay/MemoryPanel.vue | 34 +++++++++++++++++ frontend/src/components/overlay/TaskPanel.vue | 38 +++++++++++++++++++ .../src/components/overlay/WhisperPanel.vue | 20 ++++++++++ 8 files changed, 240 insertions(+) diff --git a/frontend/src/components/overlay/AiMemoryPanel.vue b/frontend/src/components/overlay/AiMemoryPanel.vue index 03465040..802edc3a 100644 --- a/frontend/src/components/overlay/AiMemoryPanel.vue +++ b/frontend/src/components/overlay/AiMemoryPanel.vue @@ -578,4 +578,33 @@ async function onClearHistory() { background: rgba(255, 255, 255, 0.14); color: var(--text-primary); } + +/* ── Mobile ── */ +@media (max-width: 768px) { + .ai-memory-panel { + padding: 24px 16px 20px; + } + + .panel-title { + font-size: 20px; + } + + .facts-list { + max-height: 55dvh; + max-height: 55vh; + } + + .fact-card { + padding: 12px; + } + + .fact-delete { + min-width: 44px; + min-height: 44px; + } + + .fact-text { + font-size: 13px; + } +} diff --git a/frontend/src/components/overlay/AuthPanel.vue b/frontend/src/components/overlay/AuthPanel.vue index 9e8eecc7..36766847 100644 --- a/frontend/src/components/overlay/AuthPanel.vue +++ b/frontend/src/components/overlay/AuthPanel.vue @@ -271,4 +271,26 @@ async function onRegister() { font-size: 13px; line-height: 1.6; } + +/* ── Mobile ── */ +@media (max-width: 768px) { + .auth-panel { + padding: 24px 16px 20px; + } + + .auth-input { + font-size: 16px; + padding: 14px 16px; + } + + .auth-tab { + font-size: 13px; + min-height: 44px; + } + + .auth-submit { + min-height: 48px; + font-size: 16px; + } +} diff --git a/frontend/src/components/overlay/ChatPanel.vue b/frontend/src/components/overlay/ChatPanel.vue index 2951d277..7f862667 100644 --- a/frontend/src/components/overlay/ChatPanel.vue +++ b/frontend/src/components/overlay/ChatPanel.vue @@ -671,4 +671,37 @@ async function onSend() { 0%, 100% { opacity: 1; } 50% { opacity: 0; } } + +/* ── Mobile ── */ +@media (max-width: 768px) { + .chat-header { + padding: 16px 48px 12px 16px; + } + + .messages { + padding: 0 16px 12px; + } + + .msg-center { + max-width: 100%; + } + + .assistant-text { + max-width: 100%; + } + + .chat-input-area { + padding: 0 16px 16px; + } + + .chat-input { + padding: 12px 16px; + font-size: 16px; + } + + .memory-btn { + width: 44px; + height: 44px; + } +} diff --git a/frontend/src/components/overlay/CommunityPanel.vue b/frontend/src/components/overlay/CommunityPanel.vue index a01a4e5e..bcd7291a 100644 --- a/frontend/src/components/overlay/CommunityPanel.vue +++ b/frontend/src/components/overlay/CommunityPanel.vue @@ -1470,4 +1470,36 @@ function clearCommentTarget() { font-weight: 600; cursor: pointer; } + +/* ── Mobile ── */ +@media (max-width: 768px) { + .community-panel { + padding: 24px 16px; + } + + .panel-title { + font-size: 20px; + } + + .channel-tab { + font-size: 13px; + padding: 8px 0; + } + + .question-card { + padding: 14px; + } + + .q-meta { + flex-wrap: wrap; + gap: 6px; + } + + .compose-input, + .compose-textarea, + .answer-input, + .reply-input { + font-size: 16px; + } +} diff --git a/frontend/src/components/overlay/LandingOverlay.vue b/frontend/src/components/overlay/LandingOverlay.vue index d2be4681..b4fd8927 100644 --- a/frontend/src/components/overlay/LandingOverlay.vue +++ b/frontend/src/components/overlay/LandingOverlay.vue @@ -104,4 +104,36 @@ function onOpen() { .landing-enter-active { transition: opacity 0.5s ease; } .landing-leave-active { transition: opacity 0.6s ease; } .landing-enter-from, .landing-leave-to { opacity: 0; } + +/* ── Mobile ── */ +@media (max-width: 768px) { + .landing-glow { + width: 200px; + height: 200px; + } + + .landing-btn { + padding: 16px 40px; + } + + .landing-btn-text { + font-size: 18px; + letter-spacing: 3px; + } + + .landing-subtitle { + font-size: 12px; + } +} + +@media (max-width: 480px) { + .landing-glow { + width: 160px; + height: 160px; + } + + .landing-btn-text { + font-size: 16px; + } +} diff --git a/frontend/src/components/overlay/MemoryPanel.vue b/frontend/src/components/overlay/MemoryPanel.vue index 4f78f31a..3be3dcdf 100644 --- a/frontend/src/components/overlay/MemoryPanel.vue +++ b/frontend/src/components/overlay/MemoryPanel.vue @@ -434,4 +434,38 @@ function onRegenerateImage() { color: var(--text-secondary); font-size: 14px; } + +/* ── Mobile ── */ +@media (max-width: 768px) { + .memory-panel { + padding: 24px 16px 20px; + } + + .memory-title { + font-size: 20px; + } + + .memory-input-area { + flex-direction: column; + } + + .memory-input { + font-size: 16px; + } + + .generate-btn { + width: 100%; + min-height: 44px; + } + + .result-actions { + flex-wrap: wrap; + } + + .result-action-btn { + min-height: 44px; + flex: 1; + min-width: 120px; + } +} diff --git a/frontend/src/components/overlay/TaskPanel.vue b/frontend/src/components/overlay/TaskPanel.vue index c3d94503..8c7e82df 100644 --- a/frontend/src/components/overlay/TaskPanel.vue +++ b/frontend/src/components/overlay/TaskPanel.vue @@ -732,4 +732,42 @@ function difficultyStars(d: number) { .age-fade-leave-to { opacity: 0; } + +/* ── Mobile ── */ +@media (max-width: 768px) { + .task-panel { + padding: 24px 16px 20px; + } + + .panel-title { + font-size: 20px; + } + + .task-card { + padding: 14px; + } + + .task-title { + font-size: 15px; + } + + .task-desc { + font-size: 12px; + } + + .star-btn { + width: 44px; + height: 44px; + font-size: 24px; + } + + .score-input { + font-size: 16px; + } + + .age-menu-btn { + min-width: 44px; + min-height: 44px; + } +} diff --git a/frontend/src/components/overlay/WhisperPanel.vue b/frontend/src/components/overlay/WhisperPanel.vue index 345284e8..f46fe7d9 100644 --- a/frontend/src/components/overlay/WhisperPanel.vue +++ b/frontend/src/components/overlay/WhisperPanel.vue @@ -329,4 +329,24 @@ function formatTime(iso: string) { color: var(--text-secondary); font-size: 14px; } + +/* ── Mobile ── */ +@media (max-width: 768px) { + .whisper-panel { + padding: 24px 16px 20px; + } + + .panel-title { + font-size: 20px; + } + + .whisper-textarea { + font-size: 16px; + } + + .submit-btn, + .tips-btn { + min-height: 44px; + } +} From e06056d9553fc74f6692d26f99d4c5ae2f4cd582 Mon Sep 17 00:00:00 2001 From: arthurcai Date: Sat, 14 Mar 2026 22:49:50 +0800 Subject: [PATCH 07/10] feat: add mobile layout for CarPage and BarPage CarPage: Replace car interior background with centered wooden board on mobile, show 3-column photo grid, add floating profile and suitcase buttons for mobile access. BarPage: Make column count responsive (4 desktop, 2 mobile) using useIsMobile composable with computed layout constants. --- frontend/src/components/overlay/BarPage.vue | 107 ++++++++- frontend/src/components/overlay/CarPage.vue | 229 ++++++++++++++++++++ 2 files changed, 327 insertions(+), 9 deletions(-) diff --git a/frontend/src/components/overlay/BarPage.vue b/frontend/src/components/overlay/BarPage.vue index 1f3ecf1f..2449c7df 100644 --- a/frontend/src/components/overlay/BarPage.vue +++ b/frontend/src/components/overlay/BarPage.vue @@ -290,6 +290,7 @@