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}`);