diff --git a/CLAUDE.md b/CLAUDE.md index d362374..64f1a67 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -104,7 +104,7 @@ TOTP_ENCRYPTION_KEY=<64-char hex string> Optional (with defaults): ``` -PORT=3001 # Production port (dev default: 3000) +PORT=3001 # Production port (dev default: 3050, hardcoded in server.ts) HOST=127.0.0.1 APP_ENV=local|production # Default: local. Controls CSP, CORS, HSTS ACCESS_TOKEN_EXPIRY=900 # 15 minutes diff --git a/package-lock.json b/package-lock.json index c82b2d6..ceb0a15 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "app-ts", - "version": "1.9.1", + "version": "1.9.2", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "app-ts", - "version": "1.9.1", + "version": "1.9.2", "license": "ISC", "dependencies": { "@dnd-kit/core": "^6.3.1", diff --git a/package.json b/package.json index 41a189a..926fc4b 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "app-ts", - "version": "1.9.1", + "version": "1.9.2", "description": "", "main": "dist/server.js", "scripts": { diff --git a/src/admin/pages/PlanWork.tsx b/src/admin/pages/PlanWork.tsx index c76b4ed..5881f54 100644 --- a/src/admin/pages/PlanWork.tsx +++ b/src/admin/pages/PlanWork.tsx @@ -576,29 +576,35 @@ export default function PlanWork() { animate={{ opacity: 1, y: 0 }} transition={{ duration: 0.25, delay: 0.12 }} > - - - + {/* Nav buttons grouped so they stay on one row when the toolbar + stacks on mobile (see .plan-toolbar-nav in plan.css). On desktop + the wrapper is an inline-flex with the same 12px gap, so the + layout is visually identical. */} +
+ + + +
setCatModalOpen(true)} > Správa kategorií diff --git a/src/admin/plan.css b/src/admin/plan.css index 65ed333..9feba9e 100644 --- a/src/admin/plan.css +++ b/src/admin/plan.css @@ -83,11 +83,17 @@ .plan-grid-wrap { position: relative; overflow: auto; + /* Smooth momentum scroll on touch, and keep scroll chaining contained so a + fling inside the grid doesn't bounce the whole page (or trigger + pull-to-refresh) on mobile. */ + -webkit-overflow-scrolling: touch; + overscroll-behavior: contain; background: var(--plan-paper); border: 1px solid var(--plan-paper-rule); border-radius: 14px; box-shadow: var(--plan-shadow); max-height: calc(100vh - 240px); + max-height: calc(100dvh - 240px); isolation: isolate; } @@ -726,6 +732,16 @@ flex-wrap: wrap; } +/* Nav button group (Dnes / ← / →). On desktop it's an inline-flex with the + same 12px gap the toolbar used between the three loose buttons before, so + the layout is unchanged. On mobile it becomes a full-width row (see the + ≤640px block below). */ +.plan-toolbar-nav { + display: inline-flex; + align-items: center; + gap: 12px; +} + .plan-view-toggle { margin-left: auto; display: inline-flex; @@ -956,6 +972,118 @@ } } +/* ---------------------------------------------------------------------------- + Mobile toolbar — stack into clean full-width rows. + + The desktop toolbar is one wrap-row with a fixed-width range label and a + `margin-left:auto` view toggle; on a phone that collapses into a ragged, + gappy mess. Here we lay it out as tidy stacked rows: nav buttons on row 1, + the range label centered on its own row, a full-width segmented view toggle, + then the category-manager button full-width. Source order already produces + this stack once each block is forced to 100% width — no `order` juggling. + ---------------------------------------------------------------------------- */ +@media (max-width: 640px) { + .plan-toolbar { + gap: 8px; + margin-top: 1rem; + margin-bottom: 1rem; + } + + /* Row 1: Dnes (grows) + the two arrow buttons (compact). */ + .plan-toolbar-nav { + display: flex; + flex: 1 1 100%; + gap: 8px; + } + .plan-toolbar-nav .admin-btn { + flex: 1 1 auto; + } + /* The ← / → buttons carry an aria-label; "Dnes" does not — keep the arrows + compact and let "Dnes" take the remaining width. */ + .plan-toolbar-nav .admin-btn[aria-label] { + flex: 0 0 auto; + min-width: 56px; + } + + /* Row 2: range label, full width, centered — drop the desktop min-width. */ + .plan-range-label-slot { + flex: 1 1 100%; + min-width: 0; + justify-content: center; + } + .plan-range-label { + font-size: 15px; + } + + /* Row 3: view toggle as a full-width segmented control, two equal halves. */ + .plan-view-toggle { + flex: 1 1 100%; + margin-left: 0; + border-radius: 12px; + } + .plan-view-toggle .admin-btn { + flex: 1 1 0; + } + + /* Row 4: category-manager button, full width. */ + .plan-toolbar-cat { + flex: 1 1 100%; + } + + /* Keep the whole grid inside the viewport so the toolbar stays put and the + sticky header / date column do the orienting (rather than the page + scrolling the header out of view). */ + .plan-grid-wrap { + max-height: calc(100dvh - 300px); + min-height: 280px; + border-radius: 12px; + } +} + +/* ---------------------------------------------------------------------------- + Small phones — tighten columns so ~2 people are comfortably visible at + 360–390px, and shrink the date "stamp" a touch. The colgroup width is + what actually pins the date column (min-width on the cells is only a floor), + so it must be set here too, not just on the cells. + ---------------------------------------------------------------------------- */ +@media (max-width: 480px) { + .plan-grid col.plan-grid-date-col { + width: 72px; + } + .plan-grid thead th.plan-grid-date-col { + min-width: 72px; + padding-left: 10px; + padding-right: 8px; + } + .plan-grid tbody td.plan-grid-date-col { + min-width: 72px; + padding: 8px 8px 8px 10px; + } + .plan-grid tbody td.plan-grid-date-col .plan-date-daynum { + font-size: 14px; + } + .plan-grid tbody td { + min-width: 132px; + } + .plan-cell { + min-height: 56px; + padding: 8px 9px 9px 14px; + } +} + +/* ---------------------------------------------------------------------------- + Touch devices — the "+" add-hint and the tape-grow are hover-only, so they + never surface on a phone. Show a faint, persistent "+" on empty *editable* + cells so tapping-to-add is discoverable. Read-only and past cells already + suppress the glyph (content:none / the readonly chain), so they stay inert. + ---------------------------------------------------------------------------- */ +@media (hover: none) { + .plan-cell--empty:not(.plan-cell--readonly)::after { + opacity: 0.26; + color: var(--plan-ink-faint); + } +} + /* Respect users who don't want a moving target — kill the hover transforms and the rule-line animation. */ @media (prefers-reduced-motion: reduce) { diff --git a/src/server.ts b/src/server.ts index f2fc278..22fa0fa 100644 --- a/src/server.ts +++ b/src/server.ts @@ -220,7 +220,9 @@ async function start() { } // --- Start --- - const port = config.isProduction ? config.port : 3000; + // Dev listens on a fixed local port (prod uses config.port / the PORT env). + // 3050 keeps it clear of other local apps that commonly grab 3000. + const port = config.isProduction ? config.port : 3050; try { await app.listen({ port, host: config.host }); app.log.info(`Server running on http://${config.host}:${port}`);