Files
trip-plan/public/css/checklist.css
T
grabowski e342cd9a91 Add trip checklists with rule-based packing advice
Each trip gets a checklist whose items group under free-text categories
(Documents, Clothing, Toiletries, Health, Electronics, Extras first, then
any custom ones alphabetically). Items are either shared — every member
sees and can tick them, and checked_by records who — or personal to one
member, which nobody else can see or touch. Items carry an optional
quantity, drag-reorder within their category, and "Uncheck all" resets the
list for the trip home.

The "Suggestions" modal is deterministic, offline advice derived from the
trip itself (src/server/util/packing.js) — no LLM and no external calls, so
it stays unit-testable and works on a self-hosted box. Nights scale
clothing quantities, flights add liquids/power-bank/check-in, rentals add
licence + IDP, ferries add motion-sickness tablets, tropical stops add sun
cream and repellent, and the destination country picks the plug type from a
bundled ~50-country table. Every suggestion carries a short reason, and
already-added ones are keyed by suggestion_key so they can't be duplicated.

Two rules deliberately differ from the naive reading, both regression-tested:
a latitude floor stops a December trip to Bangkok being tagged cold as well
as tropical, and only a flight segment's arrival airport counts, since the
first segment's departure airport is home rather than a destination.

checklist_items is a new table, so the existing CREATE TABLE IF NOT EXISTS
path creates it on upgrade; no MIGRATIONS entry is needed and existing data
is untouched.

docs/API.md documents the full contract. 113/113 tests pass.
2026-08-03 18:18:25 +07:00

67 lines
4.7 KiB
CSS

/* Checklist (packing list) card + suggestions modal. Split out of styles.css
— which was already at its 500-line cap — following the same per-feature
stylesheet convention as flipclock.css. Reuses the shared palette
(var(--brand) etc.) and existing vocabulary (.card, .btn, .icon-btn,
.field, .form-actions, .slideover-head, .overlay, .entry-drag-handle,
.empty-state) rather than re-declaring it. */
.checklist-section { display: flex; flex-direction: column; }
.checklist-actions { display: flex; gap: 0.4rem; flex-shrink: 0; }
.checklist-progress { margin-bottom: 1rem; }
.checklist-progress-label { font-size: 0.85rem; font-weight: 600; color: var(--text-muted); margin-bottom: 0.35rem; }
.checklist-progress-bar { height: 0.5rem; background: var(--surface-2); border: 1px solid var(--border); border-radius: 999px; overflow: hidden; }
.checklist-progress-fill { height: 100%; background: linear-gradient(90deg, var(--brand), var(--success)); border-radius: 999px; transition: width 0.2s ease; }
.checklist-category { margin-top: 1.1rem; }
.checklist-category:first-of-type { margin-top: 0; }
.checklist-category-head { display: flex; align-items: baseline; gap: 0.45rem; margin-bottom: 0.5rem; }
.checklist-category-count { font-size: 0.74rem; font-weight: 600; color: var(--text-faint); }
.checklist-list { display: flex; flex-direction: column; gap: 0.4rem; }
.checklist-row { display: flex; align-items: center; gap: 0.5rem; padding: 0.4rem 0.55rem; border: 1px solid var(--border); border-radius: var(--radius-sm); background: var(--surface-2); }
.checklist-row.checked .checklist-row-text { text-decoration: line-through; color: var(--text-faint); }
.checklist-row.dragging { opacity: 0.4; }
.checklist-row.drop-before { box-shadow: inset 0 3px 0 var(--brand); }
.checklist-row.drop-after { box-shadow: inset 0 -3px 0 var(--brand); }
.checklist-check { width: 1.05rem; height: 1.05rem; flex-shrink: 0; accent-color: var(--brand); cursor: pointer; }
.checklist-row-text { flex: 1; min-width: 0; font-size: 0.9rem; overflow-wrap: break-word; }
.checklist-qty { flex-shrink: 0; font-size: 0.72rem; font-weight: 700; color: var(--text-muted); background: var(--surface); border: 1px solid var(--border); padding: 0.05rem 0.4rem; border-radius: 999px; }
.checklist-lock { flex-shrink: 0; font-size: 0.85rem; }
.checklist-add { display: flex; flex-wrap: wrap; gap: 0.6rem; align-items: flex-end; margin-top: 1.1rem; padding-top: 1rem; border-top: 1px solid var(--border); }
.checklist-add .field { flex: 1 1 130px; }
.checklist-add .form-error { flex-basis: 100%; margin: 0; }
.checklist-add-category { min-width: 100px; }
.checklist-add-qty { min-width: 70px; }
.checklist-personal-toggle { display: flex; align-items: center; gap: 0.3rem; font-size: 0.85rem; color: var(--text-muted); cursor: pointer; white-space: nowrap; padding-bottom: 0.55rem; }
/* ---------- Suggestions modal ---------- */
.overlay-center { justify-content: center; align-items: center; }
/* Only the suggestion list scrolls — the heading and the scope/Add footer stay
put. With ~28 suggestions the content runs to roughly three screens, and a
footer inside the scroll area would mean scrolling past every suggestion to
reach the "Add selected" button. */
.modal { width: min(560px, 92vw); max-height: 85vh; overflow: hidden; background: var(--surface); border-radius: var(--radius); box-shadow: var(--shadow-lg); padding: 1.3rem; display: flex; flex-direction: column; gap: 1rem; animation: fade 0.15s ease; }
.modal > .slideover-head,
.modal > .suggest-scope,
.modal > .form-error,
.modal > .form-actions { flex-shrink: 0; }
.suggest-list { display: flex; flex-direction: column; gap: 1.1rem; flex: 1 1 auto; min-height: 0; overflow-y: auto; }
.suggest-category h3 { margin-bottom: 0.5rem; }
.suggest-rows { display: flex; flex-direction: column; gap: 0.4rem; }
.suggest-row { display: flex; align-items: flex-start; gap: 0.55rem; padding: 0.5rem 0.6rem; border: 1px solid var(--border); border-radius: var(--radius-sm); background: var(--surface-2); cursor: pointer; }
.suggest-row.added { opacity: 0.55; cursor: default; }
.suggest-check { width: 1.05rem; height: 1.05rem; margin-top: 0.15rem; flex-shrink: 0; accent-color: var(--brand); }
.suggest-body { flex: 1; min-width: 0; }
.suggest-text { font-size: 0.9rem; font-weight: 600; }
.suggest-qty { margin-left: 0.35rem; font-size: 0.8rem; font-weight: 700; color: var(--text-muted); }
.suggest-reason { font-size: 0.78rem; margin-top: 0.1rem; }
.suggest-scope { display: flex; gap: 1.2rem; align-items: center; font-size: 0.85rem; padding-top: 0.7rem; border-top: 1px solid var(--border); }
.suggest-scope label { display: flex; align-items: center; gap: 0.3rem; cursor: pointer; }
@media (max-width: 480px) {
.checklist-add .field { flex-basis: 100%; }
}