diff --git a/CHANGELOG.md b/CHANGELOG.md index c83c2158..0aca797a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,10 +5,20 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). -## [Unreleased] +## [1.2.0] - 2026-03-15 ### Added +#### Mobile Responsive & PWA + +- **Mobile-first responsive design**: Full mobile adaptation for the beach scene, overlay panels, and all feature pages +- **Responsive sprite positioning**: `SpriteData` interface extended with `mobile` and `landscape` override fields; `spriteStyle()` and `labelStyle()` functions select the correct tier (landscape > mobile > desktop) +- **Landscape orientation support**: Separate sprite positioning config for landscape mode via `useIsMobile` composable returning `isMobile`, `isSmall`, and `isLandscape` reactive refs +- **Portrait scroll range**: Increased horizontal parallax scroll distance on portrait mobile (`maxOffset` multiplier 1.5x vs 1.2x desktop) +- **PWA service worker**: Service worker registration for offline caching, gated behind `import.meta.env.PROD` to prevent stale-cache issues during development +- **Touch gesture support**: `touch-action: pan-x` on `.scene` element enables horizontal swipe navigation on mobile +- **Dynamic viewport units**: `dvh`/`vh` fallback pattern across all mobile and landscape media queries for correct viewport height on mobile browsers with dynamic toolbars + #### Navigation & Scene Interaction - **Collapsible NavBar**: Glassmorphism floating navigation menu in the top-right corner; collapsed by default, expands on click with frosted glass styling and monochrome outline icons @@ -26,8 +36,18 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Fixed +- **dvh/vh fallback order**: Corrected dynamic viewport height fallback across `mobile.css`, `OverlayPanel.vue`, and `RoleSelectPanel.vue` — `vh` first (fallback), `dvh` second (override) +- **touch-action scope**: Moved `touch-action: pan-x` from `body` to `.scene` only, so overlay panel content can scroll vertically +- **Event listener leaks**: Extracted anonymous `resize` and `orientationchange` handlers to named functions with proper cleanup in `onUnmounted` (`useParallax.ts`) +- **useIsMobile layout flash**: Compute initial `isMobile`/`isSmall`/`isLandscape` values synchronously in `setup()` to prevent false → true flicker on first render +- **useIsMobile MediaQueryList reuse**: `update()` reads `.matches` from stored MQL objects instead of recreating them on each call - **Go static analysis**: Replaced `fmt.Errorf` with `errors.New` for constant error strings across `photo.go`, `task.go`, and `whisper.go` (12 occurrences, SA1006) +### Security + +- **Dockerfile hardening**: Replaced `COPY . .` with explicit file/directory copies in both `frontend/Dockerfile` and `backend/Dockerfile` to prevent leaking secrets, `.env` files, or unintended files into container images +- **Go version alignment**: Updated `backend/Dockerfile` from `golang:1.23-alpine` to `golang:1.25-alpine` to match `go.mod` requirement + ### Performance - **Gesture control optimization**: Reduced GPU contention on Mac by switching to MediaPipe lite model, lowering camera resolution to 320×240, throttling inference to ~10fps with manual rAF loop, and adding GSAP camera follow throttle with `overwrite: true` diff --git a/README.md b/README.md index a3273abe..ebe4360d 100644 --- a/README.md +++ b/README.md @@ -25,6 +25,7 @@ AI-powered wellness platform combining emotional companionship, community suppor | **Photo Gallery** | Photo wall with AI-generated images, lifecycle management, and drag/zoom UI | | **Whisper** | Audio-to-text conversation using speech recognition | | **Tasks** | Goal-setting and task-tracking system with partner support | +| **Mobile & PWA** | Responsive mobile layout with portrait/landscape sprite configs, touch gestures, dynamic viewport units, and offline-capable service worker | | **Admin Panel** | Embedded single-page admin at `/admin` — dashboard, user CRUD, config management | ## Quick Start @@ -55,7 +56,7 @@ MomShell/ ├── frontend/ # Vue 3 (Vite + TypeScript + Pinia) │ └── src/ │ ├── components/ # Overlay panels + beach scene + React 3D shell -│ ├── composables/# Animation, parallax, waves, music +│ ├── composables/# Animation, parallax, waves, music, mobile detection │ ├── lib/api/ # API client modules (chat, community, echo, photo, etc.) │ ├── stores/ # Pinia stores (auth, UI) │ ├── types/ # TypeScript type definitions diff --git a/backend/Dockerfile b/backend/Dockerfile index 4f2adb12..760a9281 100644 --- a/backend/Dockerfile +++ b/backend/Dockerfile @@ -1,8 +1,10 @@ -FROM golang:1.23-alpine AS builder +FROM golang:1.25-alpine AS builder WORKDIR /app COPY go.mod go.sum ./ RUN go mod download -COPY . . +COPY cmd/ cmd/ +COPY internal/ internal/ +COPY pkg/ pkg/ RUN CGO_ENABLED=0 go build -o /server cmd/server/main.go FROM alpine:3.20 diff --git a/frontend/Dockerfile b/frontend/Dockerfile index f7312109..4a77882f 100644 --- a/frontend/Dockerfile +++ b/frontend/Dockerfile @@ -2,7 +2,9 @@ FROM node:24-alpine AS builder WORKDIR /app COPY package.json package-lock.json ./ RUN npm ci --ignore-scripts -COPY . . +COPY index.html tsconfig.json tsconfig.node.json vite.config.ts env.d.ts eslint.config.js ./ +COPY src/ src/ +COPY public/ public/ ARG VITE_API_BASE_URL= ENV VITE_API_BASE_URL=$VITE_API_BASE_URL RUN npm run build diff --git a/frontend/src/composables/useIsMobile.ts b/frontend/src/composables/useIsMobile.ts index 61dfedfa..df14a089 100644 --- a/frontend/src/composables/useIsMobile.ts +++ b/frontend/src/composables/useIsMobile.ts @@ -1,28 +1,36 @@ import { ref, onMounted, onUnmounted } from "vue"; +const MQ_MOBILE = "(max-width: 768px)"; +const MQ_SMALL = "(max-width: 480px)"; +const MQ_LANDSCAPE = "(max-height: 500px) and (orientation: landscape)"; + export function useIsMobile() { - const isMobile = ref(false); - const isSmall = ref(false); - const isLandscape = ref(false); + const isMobile = ref( + typeof window !== "undefined" && window.matchMedia(MQ_MOBILE).matches, + ); + const isSmall = ref( + typeof window !== "undefined" && window.matchMedia(MQ_SMALL).matches, + ); + const isLandscape = ref( + typeof window !== "undefined" && window.matchMedia(MQ_LANDSCAPE).matches, + ); function update() { - isMobile.value = window.matchMedia("(max-width: 768px)").matches; - isSmall.value = window.matchMedia("(max-width: 480px)").matches; - isLandscape.value = window.matchMedia( - "(max-height: 500px) and (orientation: landscape)", - ).matches; + if (mql768 && mql480 && mqlLandscape) { + isMobile.value = mql768.matches; + isSmall.value = mql480.matches; + isLandscape.value = mqlLandscape.matches; + } } - let mql768: MediaQueryList; - let mql480: MediaQueryList; - let mqlLandscape: MediaQueryList; + let mql768: MediaQueryList | undefined; + let mql480: MediaQueryList | undefined; + let mqlLandscape: MediaQueryList | undefined; onMounted(() => { - mql768 = window.matchMedia("(max-width: 768px)"); - mql480 = window.matchMedia("(max-width: 480px)"); - mqlLandscape = window.matchMedia( - "(max-height: 500px) and (orientation: landscape)", - ); + mql768 = window.matchMedia(MQ_MOBILE); + mql480 = window.matchMedia(MQ_SMALL); + mqlLandscape = window.matchMedia(MQ_LANDSCAPE); update(); mql768.addEventListener("change", update); mql480.addEventListener("change", update);