fix(plan): make Plán prací usable on mobile + dev port → 3050
Mobile work-plan (Plán prací) responsive fixes — desktop unchanged: - Toolbar stacks into clean full-width rows on ≤640px (nav group, centered range label, full-width segmented view toggle, full-width category button); drops the fixed 230px label min-width and the margin-left:auto that made the toolbar ragged on phones. - Grid scroll container: 100dvh-based max-height, momentum scroll, and overscroll containment so a fling does not bounce the whole page. - ≤480px tightens columns (date col 72px via the colgroup <col>, person cols 132px) so ~2 people are comfortably visible at 360–390px. - Touch (@media hover:none): faint persistent + on empty editable cells, since the hover-reveal add hint never fires on a phone. Dev: `npm run dev` (backend) now listens on 3050 instead of 3000 to avoid colliding with other local apps; CLAUDE.md dev-port note updated. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -104,7 +104,7 @@ TOTP_ENCRYPTION_KEY=<64-char hex string>
|
|||||||
Optional (with defaults):
|
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
|
HOST=127.0.0.1
|
||||||
APP_ENV=local|production # Default: local. Controls CSP, CORS, HSTS
|
APP_ENV=local|production # Default: local. Controls CSP, CORS, HSTS
|
||||||
ACCESS_TOKEN_EXPIRY=900 # 15 minutes
|
ACCESS_TOKEN_EXPIRY=900 # 15 minutes
|
||||||
|
|||||||
4
package-lock.json
generated
4
package-lock.json
generated
@@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "app-ts",
|
"name": "app-ts",
|
||||||
"version": "1.9.1",
|
"version": "1.9.2",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "app-ts",
|
"name": "app-ts",
|
||||||
"version": "1.9.1",
|
"version": "1.9.2",
|
||||||
"license": "ISC",
|
"license": "ISC",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@dnd-kit/core": "^6.3.1",
|
"@dnd-kit/core": "^6.3.1",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "app-ts",
|
"name": "app-ts",
|
||||||
"version": "1.9.1",
|
"version": "1.9.2",
|
||||||
"description": "",
|
"description": "",
|
||||||
"main": "dist/server.js",
|
"main": "dist/server.js",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
|||||||
@@ -576,29 +576,35 @@ export default function PlanWork() {
|
|||||||
animate={{ opacity: 1, y: 0 }}
|
animate={{ opacity: 1, y: 0 }}
|
||||||
transition={{ duration: 0.25, delay: 0.12 }}
|
transition={{ duration: 0.25, delay: 0.12 }}
|
||||||
>
|
>
|
||||||
<button
|
{/* Nav buttons grouped so they stay on one row when the toolbar
|
||||||
type="button"
|
stacks on mobile (see .plan-toolbar-nav in plan.css). On desktop
|
||||||
className="admin-btn admin-btn-secondary"
|
the wrapper is an inline-flex with the same 12px gap, so the
|
||||||
onClick={goToToday}
|
layout is visually identical. */}
|
||||||
>
|
<div className="plan-toolbar-nav">
|
||||||
Dnes
|
<button
|
||||||
</button>
|
type="button"
|
||||||
<button
|
className="admin-btn admin-btn-secondary"
|
||||||
type="button"
|
onClick={goToToday}
|
||||||
className="admin-btn admin-btn-secondary"
|
>
|
||||||
onClick={goPrev}
|
Dnes
|
||||||
aria-label="Předchozí"
|
</button>
|
||||||
>
|
<button
|
||||||
←
|
type="button"
|
||||||
</button>
|
className="admin-btn admin-btn-secondary"
|
||||||
<button
|
onClick={goPrev}
|
||||||
type="button"
|
aria-label="Předchozí"
|
||||||
className="admin-btn admin-btn-secondary"
|
>
|
||||||
onClick={goNext}
|
←
|
||||||
aria-label="Další"
|
</button>
|
||||||
>
|
<button
|
||||||
→
|
type="button"
|
||||||
</button>
|
className="admin-btn admin-btn-secondary"
|
||||||
|
onClick={goNext}
|
||||||
|
aria-label="Další"
|
||||||
|
>
|
||||||
|
→
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
<span className="plan-range-label-slot">
|
<span className="plan-range-label-slot">
|
||||||
<AnimatePresence mode="popLayout" initial={false}>
|
<AnimatePresence mode="popLayout" initial={false}>
|
||||||
<motion.span
|
<motion.span
|
||||||
@@ -644,7 +650,7 @@ export default function PlanWork() {
|
|||||||
{canEdit && (
|
{canEdit && (
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className="admin-btn admin-btn-secondary"
|
className="admin-btn admin-btn-secondary plan-toolbar-cat"
|
||||||
onClick={() => setCatModalOpen(true)}
|
onClick={() => setCatModalOpen(true)}
|
||||||
>
|
>
|
||||||
Správa kategorií
|
Správa kategorií
|
||||||
|
|||||||
@@ -83,11 +83,17 @@
|
|||||||
.plan-grid-wrap {
|
.plan-grid-wrap {
|
||||||
position: relative;
|
position: relative;
|
||||||
overflow: auto;
|
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);
|
background: var(--plan-paper);
|
||||||
border: 1px solid var(--plan-paper-rule);
|
border: 1px solid var(--plan-paper-rule);
|
||||||
border-radius: 14px;
|
border-radius: 14px;
|
||||||
box-shadow: var(--plan-shadow);
|
box-shadow: var(--plan-shadow);
|
||||||
max-height: calc(100vh - 240px);
|
max-height: calc(100vh - 240px);
|
||||||
|
max-height: calc(100dvh - 240px);
|
||||||
isolation: isolate;
|
isolation: isolate;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -726,6 +732,16 @@
|
|||||||
flex-wrap: wrap;
|
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 {
|
.plan-view-toggle {
|
||||||
margin-left: auto;
|
margin-left: auto;
|
||||||
display: inline-flex;
|
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 <col> 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
|
/* Respect users who don't want a moving target — kill the hover transforms
|
||||||
and the rule-line animation. */
|
and the rule-line animation. */
|
||||||
@media (prefers-reduced-motion: reduce) {
|
@media (prefers-reduced-motion: reduce) {
|
||||||
|
|||||||
@@ -220,7 +220,9 @@ async function start() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// --- 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 {
|
try {
|
||||||
await app.listen({ port, host: config.host });
|
await app.listen({ port, host: config.host });
|
||||||
app.log.info(`Server running on http://${config.host}:${port}`);
|
app.log.info(`Server running on http://${config.host}:${port}`);
|
||||||
|
|||||||
Reference in New Issue
Block a user