diff --git a/src/admin/components/PlanGrid.tsx b/src/admin/components/PlanGrid.tsx index c7ba23a..fe55e4a 100644 --- a/src/admin/components/PlanGrid.tsx +++ b/src/admin/components/PlanGrid.tsx @@ -1,5 +1,7 @@ import { useMemo } from "react"; import type { CSSProperties } from "react"; +import { styled } from "@mui/material/styles"; +import Box from "@mui/material/Box"; import { GridData, ResolvedCell, @@ -10,6 +12,356 @@ import { import type { Project } from "../lib/queries/projects"; import PlanRangeChips from "./PlanRangeChips"; import { LoadingState } from "../ui"; +import { fonts } from "../theme"; + +/** + * The planner surface, fully MUI/theme-driven (replaces the bespoke plan.css + * grid styling). All colors come from the theme palette via `theme.vars`, so it + * adapts to light/dark automatically; category colors come from the DB and are + * injected per cell as the `--cat-color` CSS variable. Class names from the JSX + * below are styled here as scoped nested selectors. + */ +const PlanGridRoot = styled(Box)(({ theme }) => ({ + position: "relative", + overflow: "auto", + WebkitOverflowScrolling: "touch", + overscrollBehavior: "contain", + background: theme.vars!.palette.background.default, + border: `1px solid ${theme.vars!.palette.divider}`, + borderRadius: 14, + boxShadow: theme.shadows[2], + maxHeight: "calc(100dvh - 240px)", + isolation: "isolate", + + "& .plan-grid": { + position: "relative", + zIndex: 1, + borderCollapse: "separate", + borderSpacing: 0, + width: "100%", + fontSize: "12.5px", + color: theme.vars!.palette.text.primary, + fontFamily: fonts.body, + }, + "& col.plan-grid-date-col": { width: 88 }, + "& tbody tr td + td": { + borderLeft: `1px solid ${theme.vars!.palette.divider}`, + }, + + // Sticky header + "& thead th": { + position: "sticky", + top: 0, + zIndex: 3, + padding: "14px 12px 12px", + background: theme.vars!.palette.background.paper, + color: theme.vars!.palette.text.primary, + borderBottom: `1px solid ${theme.vars!.palette.divider}`, + textAlign: "left", + fontWeight: 600, + whiteSpace: "nowrap", + }, + "& thead th.plan-grid-date-col": { + left: 0, + zIndex: 4, + minWidth: 88, + padding: "12px 10px 10px 14px", + fontSize: "10.5px", + fontWeight: 700, + letterSpacing: "0.18em", + textTransform: "uppercase", + color: theme.vars!.palette.text.secondary, + }, + "& .plan-person-head": { + display: "inline-flex", + alignItems: "center", + gap: 9, + minWidth: 0, + }, + "& .plan-person-dot": { + flexShrink: 0, + width: 9, + height: 9, + borderRadius: "50%", + background: theme.vars!.palette.action.disabled, + }, + "& .plan-person-name": { + display: "inline-flex", + flexDirection: "column", + minWidth: 0, + lineHeight: 1.1, + }, + "& .plan-person-name strong": { + fontWeight: 600, + fontSize: 13, + color: theme.vars!.palette.text.primary, + whiteSpace: "nowrap", + overflow: "hidden", + textOverflow: "ellipsis", + maxWidth: 180, + }, + "& .plan-person-name small": { + fontSize: 10, + fontWeight: 500, + color: theme.vars!.palette.text.disabled, + letterSpacing: "0.04em", + textTransform: "uppercase", + fontFamily: fonts.mono, + marginTop: 1, + }, + + // Sticky date column + "& tbody td.plan-grid-date-col": { + position: "sticky", + left: 0, + zIndex: 1, + background: theme.vars!.palette.background.paper, + fontWeight: 500, + color: theme.vars!.palette.text.secondary, + whiteSpace: "nowrap", + minWidth: 88, + padding: "9px 10px 9px 14px", + borderRight: `1px solid ${theme.vars!.palette.divider}`, + borderBottom: `1px solid ${theme.vars!.palette.divider}`, + }, + "& .plan-date-stamp": { + display: "flex", + flexDirection: "column", + alignItems: "flex-start", + lineHeight: 1.1, + gap: 1, + }, + "& .plan-date-daynum": { + fontFamily: fonts.mono, + fontSize: 17, + fontWeight: 600, + color: theme.vars!.palette.text.primary, + fontVariantNumeric: "tabular-nums", + lineHeight: 1.05, + }, + "& .plan-date-dow": { + fontSize: 10, + fontWeight: 600, + letterSpacing: "0.14em", + textTransform: "uppercase", + color: theme.vars!.palette.text.disabled, + lineHeight: 1.3, + }, + + // Today + "& tbody tr.is-today td.plan-grid-date-col": { + background: theme.vars!.palette.background.paper, + "&::before": { + content: '""', + position: "absolute", + left: 0, + top: 6, + bottom: 6, + width: 3, + borderRadius: "0 2px 2px 0", + background: theme.vars!.palette.primary.main, + boxShadow: `0 0 14px rgba(${theme.vars!.palette.primary.mainChannel} / 0.3)`, + }, + "& .plan-date-daynum": { color: theme.vars!.palette.primary.main }, + }, + "& tbody tr.is-today td:not(.plan-grid-date-col)": { + background: `rgba(${theme.vars!.palette.primary.mainChannel} / 0.06)`, + }, + + // Body cells + "& tbody td": { + padding: 0, + borderBottom: `1px solid ${theme.vars!.palette.divider}`, + verticalAlign: "stretch", + minWidth: 168, + background: theme.vars!.palette.background.paper, + }, + + // Weekend + "& tr.plan-grid-weekend td": { + background: `rgba(${theme.vars!.palette.text.primaryChannel} / 0.03)`, + }, + "& tr.plan-grid-weekend td.plan-grid-date-col": { + background: theme.vars!.palette.background.paper, + color: theme.vars!.palette.text.disabled, + "& .plan-date-dow": { color: theme.vars!.palette.warning.main }, + "& .plan-date-daynum": { color: theme.vars!.palette.text.secondary }, + }, + + // The cell button + marker tape + "& .plan-cell": { + position: "relative", + display: "block", + width: "100%", + height: "100%", + minHeight: 60, + textAlign: "left", + background: "transparent", + border: 0, + borderRadius: 0, + padding: "9px 11px 10px 16px", + cursor: "pointer", + font: "inherit", + color: "inherit", + transition: "background 150ms ease, transform 150ms ease", + "&::before": { + content: '""', + position: "absolute", + left: 4, + top: 8, + bottom: 8, + width: 3, + borderRadius: 2, + background: `var(--cat-color, ${theme.vars!.palette.divider})`, + transition: + "background 150ms ease, box-shadow 150ms ease, transform 150ms ease", + }, + "&:hover": { background: theme.vars!.palette.action.hover }, + "&:hover::before": { + transform: "scaleX(1.4)", + boxShadow: "0 0 12px currentColor", + }, + "&:active": { transform: "translateY(0.5px)" }, + "&:focus-visible": { + outline: "none", + boxShadow: `inset 0 0 0 2px ${theme.vars!.palette.primary.main}, inset 0 0 0 4px ${theme.vars!.palette.background.paper}`, + }, + }, + "& .plan-cell--readonly": { + cursor: "default", + "&:hover": { background: "transparent" }, + "&:hover::before": { transform: "none", boxShadow: "none" }, + }, + // A read-only cell that HAS data is still clickable (view mode). + "& .plan-cell--readonly:not(.plan-cell--empty):not(.plan-cell--past)": { + cursor: "pointer", + "&:hover": { background: theme.vars!.palette.action.hover }, + "&:hover::before": { + transform: "scaleX(1.4)", + boxShadow: "0 0 12px currentColor", + }, + }, + "& .plan-cell--past": { + cursor: "default", + opacity: 0.55, + "&:hover": { background: "transparent" }, + "&:hover::before": { transform: "none", boxShadow: "none" }, + }, + // Empty cells: dashed tape + a "+" hover hint. + "& .plan-cell--empty::before": { + background: "transparent", + border: `1px dashed ${theme.vars!.palette.divider}`, + width: 2, + left: 5, + }, + "& .plan-cell--empty:hover::before": { + background: theme.vars!.palette.primary.main, + borderColor: theme.vars!.palette.primary.main, + transform: "scaleX(1.6)", + }, + "& .plan-cell--empty::after": { + content: '"+"', + position: "absolute", + inset: 0, + display: "grid", + placeItems: "center", + color: theme.vars!.palette.text.disabled, + opacity: 0, + fontSize: 18, + fontWeight: 300, + pointerEvents: "none", + transition: "opacity 150ms ease", + }, + "& .plan-cell--empty:hover::after": { + opacity: 0.7, + color: theme.vars!.palette.primary.main, + }, + "& .plan-cell--empty.plan-cell--readonly::after": { content: "none" }, + + // One-shot mutation pulse + "@keyframes plan-cell-pulse": { + "0%": { + boxShadow: `inset 0 0 0 2px ${theme.vars!.palette.primary.main}, 0 0 0 0 rgba(${theme.vars!.palette.primary.mainChannel} / 0.4)`, + }, + "60%": { + boxShadow: `inset 0 0 0 2px ${theme.vars!.palette.primary.main}, 0 0 0 6px transparent`, + }, + "100%": { boxShadow: "inset 0 0 0 2px transparent, 0 0 0 0 transparent" }, + }, + "& .plan-cell--pulse": { animation: "plan-cell-pulse 600ms ease-out" }, + + // Chips inside a cell + "& .plan-chip-block": { + display: "flex", + flexDirection: "column", + gap: 4, + textAlign: "left", + width: "100%", + minWidth: 0, + }, + "& .plan-chip-line": { + display: "flex", + alignItems: "center", + gap: 7, + flexWrap: "wrap", + minWidth: 0, + }, + "& .plan-chip": { + display: "inline-flex", + alignItems: "center", + gap: 5, + borderRadius: 4, + padding: "2px 7px", + fontSize: "10.5px", + fontWeight: 600, + letterSpacing: "0.04em", + textTransform: "uppercase", + color: `var(--cat-color, ${theme.vars!.palette.text.primary})`, + background: `color-mix(in srgb, var(--cat-color, transparent) 12%, ${theme.vars!.palette.background.paper})`, + border: `1px solid color-mix(in srgb, var(--cat-color, ${theme.vars!.palette.divider}) 35%, transparent)`, + whiteSpace: "nowrap", + }, + "& .plan-chip-project": { + fontFamily: fonts.mono, + fontSize: "12.5px", + fontWeight: 600, + color: theme.vars!.palette.text.primary, + overflow: "hidden", + textOverflow: "ellipsis", + whiteSpace: "nowrap", + maxWidth: "100%", + }, + "& .plan-cell-note": { + display: "-webkit-box", + WebkitLineClamp: 2, + WebkitBoxOrient: "vertical", + overflow: "hidden", + marginTop: 1, + fontSize: "11.5px", + color: theme.vars!.palette.text.secondary, + lineHeight: 1.35, + fontStyle: "italic", + }, + + "@media (max-width: 480px)": { + "& col.plan-grid-date-col": { width: 72 }, + "& tbody td": { minWidth: 132 }, + "& .plan-cell": { minHeight: 56 }, + }, + // Touch: show a faint persistent "+" on empty editable cells. + "@media (hover: none)": { + "& .plan-cell--empty:not(.plan-cell--readonly)::after": { + opacity: 0.26, + color: theme.vars!.palette.text.disabled, + }, + }, + "@media (prefers-reduced-motion: reduce)": { + "& .plan-cell, & .plan-cell::before, & .plan-cell::after": { + transition: "none", + }, + "& .plan-cell--pulse": { animation: "none" }, + }, +})); interface Props { data: GridData | undefined; @@ -120,7 +472,7 @@ export default function PlanGrid({ : undefined; return ( -
+ {/* The colgroup is what actually controls column width in a table — `min-width` on `
`/`` is just a floor that @@ -251,6 +603,6 @@ export default function PlanGrid({ })}
-
+ ); } diff --git a/src/admin/plan.css b/src/admin/plan.css index 9feba9e..34a26d4 100644 --- a/src/admin/plan.css +++ b/src/admin/plan.css @@ -1,1109 +1,17 @@ /* ============================================================================ - Plán prací (work schedule) — editorial / atelier aesthetic - A dispatcher's desk: paper, ink, color-coded marker tape. - Designed to scale to 20+ people columns without losing legibility. - ============================================================================ */ + Plán prací — leftover bespoke styles. -/* ---- Theme tokens for the plan surface ----------------------------------- */ + The planner GRID is now fully MUI/theme-driven (see PlanGrid's styled() + wrapper), and the toolbar / range label / banner / day-choice modal were + migrated to MUI components. Only the category-manager modal rows remain + here; they keep a single paper-rule token for their swatch/divider borders. + ============================================================================ */ :root { - /* Marker tape — these are the *plan*-specific category hues. - Tuned for the same Czech workflow categories in the chip classes. */ - --plan-tape-work: #2563eb; /* deep blue */ - --plan-tape-preparation: #0d9488; /* teal */ - --plan-tape-travel: #ca8a04; /* mustard */ - --plan-tape-leave: #16a34a; /* green */ - --plan-tape-sick: #dc2626; /* red */ - --plan-tape-training: #7c3aed; /* violet */ - --plan-tape-other: #6b7280; /* graphite */ - - /* Paper / ink tones (overridden in [data-theme] blocks). */ - --plan-paper: #f7f4ee; --plan-paper-rule: rgba(20, 20, 24, 0.08); - --plan-paper-soft: #fbf9f4; - --plan-ink: #1a1a1a; - --plan-ink-muted: #6b6b6b; - --plan-ink-faint: #9a958c; - --plan-today-bar: #c73030; - --plan-today-fill: rgba(199, 48, 48, 0.06); - --plan-today-glow: rgba(199, 48, 48, 0.18); - --plan-weekend-fill: rgba(20, 20, 24, 0.025); - --plan-weekend-stripe: rgba(20, 20, 24, 0.04); - --plan-column-rule: rgba(20, 20, 24, 0.06); - --plan-row-rule: rgba(20, 20, 24, 0.07); - --plan-shadow: - 0 1px 0 rgba(20, 20, 24, 0.04), 0 12px 32px -16px rgba(20, 20, 24, 0.12); - --plan-cell-bg: #ffffff; - --plan-cell-bg-hover: #fffdf8; - --plan-cell-shadow: 0 1px 0 rgba(20, 20, 24, 0.03); - --plan-header-bg: #f1ede5; - --plan-header-ink: #1a1a1a; - --plan-person-dot: #d1cdbf; - - /* Type scale (works with the app's existing --font-body / --font-mono). */ - --plan-font-display: var(--font-mono); - --plan-font-body: var(--font-body); } - [data-theme="dark"] { - --plan-paper: #0d0d0e; - --plan-paper-soft: #131316; --plan-paper-rule: rgba(255, 255, 255, 0.06); - --plan-ink: #f4f1ea; - --plan-ink-muted: #9b9b9b; - --plan-ink-faint: #5d5d5d; - --plan-today-bar: #ff5a5f; - --plan-today-fill: rgba(255, 90, 95, 0.07); - --plan-today-glow: rgba(255, 90, 95, 0.25); - --plan-weekend-fill: rgba(255, 255, 255, 0.015); - --plan-weekend-stripe: rgba(255, 255, 255, 0.025); - --plan-column-rule: rgba(255, 255, 255, 0.05); - --plan-row-rule: rgba(255, 255, 255, 0.05); - --plan-shadow: - 0 1px 0 rgba(0, 0, 0, 0.4), 0 18px 40px -20px rgba(0, 0, 0, 0.6); - --plan-cell-bg: #16161a; - --plan-cell-bg-hover: #1c1c22; - --plan-cell-shadow: 0 1px 0 rgba(0, 0, 0, 0.5); - --plan-header-bg: #131316; - --plan-header-ink: #f4f1ea; - --plan-person-dot: #2c2c30; -} - -/* ============================================================================ - Page surface — give the planner room to breathe - ============================================================================ */ - -.plan-work-page { - position: relative; -} - -/* Soft paper-grain behind the grid. Doesn't tile from an image — just two - layered radial gradients in the theme paper tone. Adds warmth in light - mode, depth in dark. */ -.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; -} - -.plan-grid-wrap::before { - content: ""; - position: absolute; - inset: 0; - pointer-events: none; - background: - radial-gradient( - 1100px 320px at 0% 0%, - color-mix(in srgb, var(--plan-tape-work) 5%, transparent), - transparent 60% - ), - radial-gradient( - 900px 280px at 100% 100%, - color-mix(in srgb, var(--plan-tape-training) 5%, transparent), - transparent 60% - ); - z-index: 0; -} - -.plan-grid-wrap::after { - /* Very subtle horizontal "rule" lines to evoke ruled paper — drawn via - repeating gradient so the table doesn't have to ship extra DOM. */ - content: ""; - position: absolute; - inset: 0; - pointer-events: none; - background-image: repeating-linear-gradient( - to bottom, - transparent 0, - transparent 55px, - var(--plan-paper-rule) 55px, - var(--plan-paper-rule) 56px - ); - opacity: 0.35; - mix-blend-mode: multiply; - z-index: 0; -} - -[data-theme="dark"] .plan-grid-wrap::after { - mix-blend-mode: screen; - opacity: 0.18; -} - -/* Placeholder while fetching — matches wrap height so the toolbar doesn't jump. */ -.plan-grid-placeholder { - min-height: 320px; - border: 1px dashed var(--plan-paper-rule); - border-radius: 14px; - background: var(--plan-paper); - opacity: 0.6; -} - -/* ============================================================================ - Grid table - ============================================================================ */ - -.plan-grid { - position: relative; - z-index: 1; - border-collapse: separate; - border-spacing: 0; - width: 100%; - font-size: 12.5px; - color: var(--plan-ink); - font-family: var(--plan-font-body); -} - -/* The element in is what actually pins column width - under `table-layout: auto`. The first in PlanGrid.tsx carries - `plan-grid-date-col` — sizing it to 88px here makes the date column - that width, full stop (the `min-width` on the th/td is just a floor - and the auto-layout algorithm happily stretches it otherwise). */ -.plan-grid col.plan-grid-date-col { - width: 88px; -} - -/* ---- Column rules (vertical) — drawn via background on the *first* cell - of each row so we don't ship extra DOM. */ -.plan-grid tbody tr td + td { - border-left: 1px solid var(--plan-column-rule); -} - -/* ---- Sticky header ------------------------------------------------------ */ - -.plan-grid thead th { - position: sticky; - top: 0; - z-index: 3; - padding: 14px 12px 12px; - background: var(--plan-header-bg); - color: var(--plan-header-ink); - border-bottom: 1px solid var(--plan-paper-rule); - text-align: left; - font-weight: 600; - white-space: nowrap; - font-family: var(--plan-font-body); - letter-spacing: -0.005em; - /* A double-rule under the header — typographer's "French rule". */ - box-shadow: - inset 0 -1px 0 var(--plan-paper-rule), - inset 0 -3px 0 color-mix(in srgb, var(--plan-paper-rule) 60%, transparent); -} - -/* The date column header reads like a section label, not a column name. */ -.plan-grid thead th.plan-grid-date-col { - left: 0; - z-index: 4; - min-width: 88px; - padding: 12px 10px 10px 14px; - font-size: 10.5px; - font-weight: 700; - letter-spacing: 0.18em; - text-transform: uppercase; - color: var(--plan-ink-muted); - font-family: var(--plan-font-body); -} - -/* Person column header — avatar dot + name (two-line) */ -.plan-grid thead th .plan-person-head { - display: inline-flex; - align-items: center; - gap: 9px; - min-width: 0; -} - -.plan-grid thead th .plan-person-dot { - flex-shrink: 0; - width: 9px; - height: 9px; - border-radius: 50%; - background: var(--plan-person-dot); - box-shadow: inset 0 0 0 2px var(--plan-header-bg); -} - -.plan-grid thead th .plan-person-name { - display: inline-flex; - flex-direction: column; - min-width: 0; - line-height: 1.1; -} - -.plan-grid thead th .plan-person-name strong { - font-weight: 600; - font-size: 13px; - color: var(--plan-header-ink); - letter-spacing: -0.005em; - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; - max-width: 180px; -} - -.plan-grid thead th .plan-person-name small { - font-size: 10px; - font-weight: 500; - color: var(--plan-ink-faint); - letter-spacing: 0.04em; - text-transform: uppercase; - font-family: var(--plan-font-display); - margin-top: 1px; -} - -/* ---- Date column cells (sticky left) ----------------------------------- */ - -.plan-grid tbody td.plan-grid-date-col { - position: sticky; - left: 0; - z-index: 1; - background: var(--plan-paper-soft); - font-weight: 500; - color: var(--plan-ink-muted); - white-space: nowrap; - /* Override the inherited `tbody td { min-width: 168px }` — the date - column is just a "stamp" with the day number and weekday name. The - month is already shown in the page range label at the top, so the - cell no longer carries a "· 06" suffix. 88px fits "Pondělí" with - comfortable padding on either side. */ - min-width: 88px; - padding: 9px 10px 9px 14px; - border-right: 1px solid var(--plan-paper-rule); - border-bottom: 1px solid var(--plan-row-rule); - font-family: var(--plan-font-body); - /* The date column is the "tab" — let it sit on its own little slab. */ - box-shadow: inset -1px 0 0 var(--plan-paper-rule); -} - -.plan-grid tbody td.plan-grid-date-col .plan-date-stamp { - display: flex; - flex-direction: column; - align-items: flex-start; - line-height: 1.1; - gap: 1px; -} - -.plan-grid tbody td.plan-grid-date-col .plan-date-daynum { - font-family: var(--plan-font-display); - font-size: 17px; - font-weight: 600; - color: var(--plan-ink); - letter-spacing: -0.01em; - font-variant-numeric: tabular-nums; - line-height: 1.05; -} - -.plan-grid tbody td.plan-grid-date-col .plan-date-dow { - font-size: 10px; - font-weight: 600; - letter-spacing: 0.14em; - text-transform: uppercase; - color: var(--plan-ink-faint); - /* Slightly looser leading so Czech diacritics (ě, í, ý, á) on full - weekday names have headroom and don't visually clip. */ - line-height: 1.3; -} - -/* Mark "today" with a heavy red rule on the left of the date cell. The - cell is sticky, so the bar stays put while the rest of the row scrolls. */ -.plan-grid tbody tr.is-today td.plan-grid-date-col { - /* Sticky column must stay opaque or horizontally-scrolled cells show - through it. Layer the (translucent) today tint over the opaque paper - base instead of replacing it. */ - background: - linear-gradient(var(--plan-today-fill), var(--plan-today-fill)), - var(--plan-paper-soft); - position: sticky; -} - -.plan-grid tbody tr.is-today td.plan-grid-date-col::before { - content: ""; - position: absolute; - left: 0; - top: 6px; - bottom: 6px; - width: 3px; - border-radius: 0 2px 2px 0; - background: var(--plan-today-bar); - box-shadow: 0 0 14px var(--plan-today-glow); -} - -.plan-grid tbody tr.is-today td.plan-grid-date-col .plan-date-daynum { - color: var(--plan-today-bar); -} - -/* The rest of the cells in today's row get a faint warm wash so the row - reads as "the focal row" without being loud. */ -.plan-grid tbody tr.is-today td:not(.plan-grid-date-col) { - background: var(--plan-today-fill); -} - -/* ---- Body cells --------------------------------------------------------- */ - -.plan-grid tbody td { - padding: 0; - border-bottom: 1px solid var(--plan-row-rule); - vertical-align: stretch; - min-width: 168px; - background: var(--plan-cell-bg); -} - -/* ---- Weekend treatment -------------------------------------------------- - The rule was a hard tinted background; now we use a soft paper-stripe - (the ::after paper grain is on the wrap, so the row needs its own - gentle wash that doesn't fight the grain). */ -.plan-grid tr.plan-grid-weekend td { - background: var(--plan-weekend-fill); -} - -.plan-grid tr.plan-grid-weekend td.plan-grid-date-col { - /* Opaque base under the translucent weekend tint so the sticky column - doesn't reveal scrolled cells behind it. */ - background: - linear-gradient(var(--plan-weekend-fill), var(--plan-weekend-fill)), - var(--plan-paper-soft); - color: var(--plan-ink-faint); -} - -.plan-grid tr.plan-grid-weekend td.plan-grid-date-col .plan-date-dow { - color: var(--plan-tape-travel); -} - -.plan-grid tr.plan-grid-weekend td.plan-grid-date-col .plan-date-daynum { - color: var(--plan-ink-muted); -} - -/* Weekend empty cells: the row already reads as weekend (date column - tints the DOW amber, the row gets the soft weekend fill). We - deliberately leave the `::after` slot free here so the base - `.plan-cell--empty::after` "+" hover affordance keeps working — - a higher-specificity `::after` here would lock the content to "—" - and break the hover hint. */ - -/* ============================================================================ - The cell itself — the marker chip - ============================================================================ */ - -.plan-cell { - position: relative; - display: block; - width: 100%; - height: 100%; - min-height: 60px; - text-align: left; - background: transparent; - border: 0; - border-radius: 0; - padding: 9px 11px 10px 16px; /* extra left padding for the tape */ - cursor: pointer; - font: inherit; - color: inherit; - transition: - background var(--motion-fast) ease, - transform var(--motion-fast) ease; -} - -/* The colored "marker tape" on the left edge of every filled cell. - Drawn with a thin gradient on the cell's left padding box. The actual - color comes from a category-specific class added on the chip — but to - keep the tape present even when the cell is empty (so empty cells - don't feel "broken"), we draw a faint rule for the empty state. */ -.plan-cell::before { - content: ""; - position: absolute; - left: 4px; - top: 8px; - bottom: 8px; - width: 3px; - border-radius: 2px; - background: var(--cat-color, var(--plan-paper-rule)); - transition: - background var(--motion-fast) ease, - box-shadow var(--motion-fast) ease, - transform var(--motion-fast) ease; -} - -.plan-cell:hover { - background: var(--plan-cell-bg-hover); -} - -.plan-cell:hover::before { - transform: scaleX(1.4); - box-shadow: 0 0 12px currentColor; -} - -.plan-cell:active { - transform: translateY(0.5px); -} - -.plan-cell:focus-visible { - outline: none; - box-shadow: - inset 0 0 0 2px var(--accent-color), - inset 0 0 0 4px var(--plan-paper); -} - -.plan-cell--readonly { - cursor: default; -} -.plan-cell--readonly:hover { - background: transparent; -} -.plan-cell--readonly:hover::before { - transform: none; - box-shadow: none; -} - -/* A read-only cell that HAS a record is still clickable — it opens the - read-only detail view (see PlanWork.onCellClick: a cell with data routes - to kind: "view" when !canEdit). So keep the hover affordance and pointer - cursor for those, mirroring the editable cell's hover. Empty read-only - cells and past cells stay inert (nothing to view / cannot act). The - :not() chain out-specifies the readonly-kills-hover rules above. */ -.plan-cell--readonly:not(.plan-cell--empty):not(.plan-cell--past) { - cursor: pointer; -} -.plan-cell--readonly:not(.plan-cell--empty):not(.plan-cell--past):hover { - background: var(--plan-cell-bg-hover); -} -.plan-cell--readonly:not(.plan-cell--empty):not( - .plan-cell--past - ):hover::before { - transform: scaleX(1.4); - box-shadow: 0 0 12px currentColor; -} - -/* Past-day cells: even if the user has edit permission, the server - rejects create/edit on past dates (see assertNotPastDate in - plan.service.ts) so we lock the cell to view-only in the UI. Visually - dim them and drop the "click to add" affordance so the user doesn't - expect to be able to act on them. */ -.plan-cell--past { - cursor: default; - opacity: 0.55; -} -.plan-cell--past:hover { - background: transparent; -} -.plan-cell--past:hover::before { - transform: none; - box-shadow: none; - background: var(--plan-paper-rule); - border-color: var(--plan-paper-rule); -} -.plan-cell--past:hover::after { - /* Suppress the "+" hover hint on past empty cells — clicking does - nothing useful there (no record to view, no record to create). */ - opacity: 0 !important; - color: var(--plan-ink-faint) !important; -} - -/* "Empty" cells get a subtle add-icon affordance on hover, so the user - knows clicking creates something. We do this without changing the DOM. */ -.plan-cell--empty::before { - background: transparent; - border: 1px dashed var(--plan-paper-rule); - width: 2px; - left: 5px; -} - -.plan-cell--empty:hover { - background: var(--plan-cell-bg-hover); -} -.plan-cell--empty:hover::before { - background: var(--accent-color); - border-color: var(--accent-color); - transform: scaleX(1.6); - box-shadow: 0 0 10px var(--accent-glow, rgba(214, 48, 49, 0.4)); -} - -.plan-cell--empty::after { - /* Tiny "+" hint that fades in on hover. */ - content: "+"; - position: absolute; - inset: 0; - display: grid; - place-items: center; - color: var(--plan-ink-faint); - opacity: 0; - font-size: 18px; - font-weight: 300; - pointer-events: none; - transition: opacity var(--motion-fast) ease; -} -.plan-cell--empty:hover::after { - opacity: 0.7; - color: var(--accent-color); -} - -/* View-only (and otherwise locked) empty cells must NOT advertise an "add" - action: no "+" glyph and no hover highlight. The click is already a no-op - for these — PlanWork's onCellClick only opens the create modal when - canEdit. Two-class selectors out-specify the single-class hover rules - above. */ -.plan-cell--empty.plan-cell--readonly::after { - content: none; -} -.plan-cell--empty.plan-cell--readonly:hover { - background: transparent; -} -.plan-cell--empty.plan-cell--readonly:hover::after { - opacity: 0; -} - -/* ============================================================================ - Per-cell pulse — fired by mutations (create/edit/delete). - - Replaces the old "full-grid re-animates after a mutation" behavior. The - mutation's onSuccess in usePlanWork patches the affected cell into the - React Query cache, PlanWork bumps `lastMutated`, and PlanGrid applies - `plan-cell--pulse` to that single cell. The keyframe runs once for - 600ms, then PlanWork clears `lastMutated` (which removes the class). - - We use an outline + box-shadow on the button and a glow on the - marker-tape `::before` — no layout shift, no remount, no global - animation. The marker-tape glow uses `currentColor` so it picks up - the category color via the existing :has() rules. - ============================================================================ */ - -@keyframes plan-cell-pulse { - 0% { - box-shadow: - inset 0 0 0 2px var(--accent-color), - inset 0 0 0 4px var(--plan-paper), - 0 0 0 0 var(--accent-glow, rgba(214, 48, 49, 0.4)); - } - 60% { - box-shadow: - inset 0 0 0 2px var(--accent-color), - inset 0 0 0 4px var(--plan-paper), - 0 0 0 6px transparent; - } - 100% { - box-shadow: - inset 0 0 0 2px transparent, - inset 0 0 0 4px transparent, - 0 0 0 0 transparent; - } -} - -@keyframes plan-cell-tape-pulse { - 0% { - box-shadow: - 0 0 0 0 currentColor, - 0 0 12px currentColor; - } - 100% { - box-shadow: - 0 0 0 4px transparent, - 0 0 0 transparent; - } -} - -.plan-cell--pulse { - animation: plan-cell-pulse 600ms ease-out; -} - -.plan-cell--pulse::before { - /* The tape is a single ::before pseudo-element. We give it a one-shot - glow that matches the category color via `currentColor` (set by the - cell's text color). The animation runs alongside the existing - :hover transform on the tape; both apply to the same pseudo-element - and stack. */ - animation: plan-cell-tape-pulse 600ms ease-out; -} - -/* ============================================================================ - Chips — the actual data inside a cell - ============================================================================ */ - -.plan-chip-block { - display: flex; - flex-direction: column; - gap: 4px; - text-align: left; - width: 100%; - min-width: 0; -} - -.plan-chip-line { - display: flex; - align-items: center; - gap: 7px; - flex-wrap: wrap; - min-width: 0; -} - -.plan-chip { - display: inline-flex; - align-items: center; - gap: 5px; - border-radius: 4px; - padding: 2px 7px; - font-size: 10.5px; - font-weight: 600; - letter-spacing: 0.04em; - text-transform: uppercase; - font-family: var(--plan-font-body); - /* Category chip is small, monoline. The heavy color lives on the tape - on the cell edge — the chip is the *text* version of the same color. */ - color: var(--cat-color, var(--plan-ink)); - background: color-mix( - in srgb, - var(--cat-color, var(--plan-paper-soft)) 10%, - var(--plan-paper) - ); - border: 1px solid - color-mix( - in srgb, - var(--cat-color, var(--plan-paper-rule)) 35%, - transparent - ); - white-space: nowrap; -} - -[data-theme="dark"] .plan-chip { - background: color-mix( - in srgb, - var(--cat-color, var(--plan-cell-bg)) 16%, - var(--plan-paper) - ); -} - -.plan-chip-project { - font-family: var(--plan-font-display); - font-size: 12.5px; - font-weight: 600; - color: var(--plan-ink); - overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; - max-width: 100%; - letter-spacing: -0.005em; -} - -.plan-cell-note { - display: block; - margin-top: 1px; - font-size: 11.5px; - color: var(--plan-ink-muted); - line-height: 1.35; - font-style: italic; - font-family: var(--plan-font-body); - /* Show first two lines max, with a soft fade for the third */ - display: -webkit-box; - -webkit-line-clamp: 2; - -webkit-box-orient: vertical; - overflow: hidden; - position: relative; -} - -.plan-cell-note::before { - content: "“"; - margin-right: 1px; - color: var(--plan-ink-faint); - font-style: normal; -} -.plan-cell-note::after { - content: "”"; - margin-left: 1px; - color: var(--plan-ink-faint); - font-style: normal; -} - -/* ============================================================================ - Toolbar - ============================================================================ */ - -.plan-toolbar { - display: flex; - gap: 12px; - align-items: center; - margin-top: 1.25rem; - margin-bottom: 1.25rem; - 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; - gap: 2px; - padding: 3px; - background: var(--bg-tertiary); - border: 1px solid var(--border-color); - border-radius: 999px; - box-shadow: inset 0 1px 0 var(--border-color); -} - -.plan-view-toggle .admin-btn { - border-radius: 999px; - padding: 5px 14px; - font-size: 12px; - letter-spacing: 0.01em; -} - -.plan-range-label-slot { - display: inline-flex; - align-items: baseline; - gap: 6px; - /* Wide enough for "Týden 23 (1.6. – 7.6. 2026)" — the longest - range label — so the slot doesn't collapse during AnimatePresence - exit/enter transitions. Without this, the toolbar jumps when the - label width changes between week and month views. */ - min-width: 230px; - color: var(--plan-ink); - font-weight: 500; - font-variant-numeric: tabular-nums; - overflow: hidden; - font-family: var(--font-body); - position: relative; -} - -.plan-range-label { - white-space: nowrap; - font-size: 14px; - font-weight: 600; - letter-spacing: -0.005em; - color: var(--plan-ink); -} - -/* ============================================================================ - Banner (read-only notice) - ============================================================================ */ - -.plan-banner { - padding: 12px 16px; - margin-bottom: 16px; - background: var(--info-soft); - border: 1px solid color-mix(in srgb, var(--info) 25%, transparent); - border-left: 3px solid var(--info); - border-radius: 10px; - color: var(--text-primary); - font-size: 13.5px; - display: flex; - align-items: center; - gap: 10px; - line-height: 1.45; -} - -.plan-banner-icon { - display: inline-grid; - place-items: center; - width: 22px; - height: 22px; - border-radius: 50%; - background: color-mix(in srgb, var(--info) 18%, transparent); - color: var(--info); - font-weight: 700; - font-style: italic; - font-family: var(--font-display, var(--font-mono)); - font-size: 12px; - flex-shrink: 0; -} - -.plan-banner code { - background: transparent; - padding: 0; -} - -/* ============================================================================ - Day-in-range choice panel (modal) - ============================================================================ */ - -.plan-modal-actions { - display: flex; - gap: 8px; - justify-content: flex-end; - margin-top: 12px; - margin-bottom: 0; -} - -.plan-modal-intro { - margin: 0 0 14px; - font-size: 14px; - line-height: 1.55; - color: var(--text-secondary); -} -.plan-modal-intro strong { - color: var(--text-primary); - font-weight: 600; - font-variant-numeric: tabular-nums; -} - -.plan-choices { - list-style: none; - margin: 0; - padding: 0; - display: flex; - flex-direction: column; - gap: 10px; -} - -.plan-choice { - display: flex; - align-items: center; - gap: 14px; - padding: 14px 16px; - background: var(--bg-secondary); - border: 1px solid var(--border-color); - border-radius: 10px; - transition: - border-color var(--motion-fast) ease, - background var(--motion-fast) ease, - transform var(--motion-fast) ease; -} - -.plan-choice:hover { - border-color: var(--border-color-hover); - background: var(--bg-tertiary); - transform: translateY(-1px); -} - -.plan-choice-primary { - border-left: 3px solid var(--accent-color); -} - -.plan-choice-body { - flex: 1; - min-width: 0; -} - -.plan-choice-title { - font-size: 14px; - font-weight: 600; - color: var(--text-primary); - margin-bottom: 2px; -} - -.plan-choice-desc { - font-size: 12.5px; - line-height: 1.45; - color: var(--text-secondary); -} - -.plan-choice .admin-btn { - flex-shrink: 0; -} - -.plan-modal-actions--solo { - justify-content: center; - margin-top: 14px; - padding-top: 12px; - border-top: 1px solid var(--border-color); -} -.plan-modal-actions--solo .admin-btn-ghost { - width: 100%; -} - -@media (max-width: 480px) { - .plan-choice { - flex-direction: column; - align-items: stretch; - gap: 10px; - } - .plan-choice .admin-btn { - width: 100%; - } -} - -/* ============================================================================ - Scale handling — when there are many person columns - ============================================================================ */ - -@media (min-width: 1280px) { - .plan-grid thead th .plan-person-name strong { - max-width: 140px; - } -} - -/* On very wide grids (20+ people) — tighten the cell min-width so the - viewport doesn't need aggressive horizontal scrolling. */ -@media (min-width: 1600px) { - .plan-grid tbody td { - min-width: 142px; - } -} - -@media (min-width: 2000px) { - .plan-grid tbody td { - min-width: 120px; - } - .plan-grid { - font-size: 12px; - } -} - -/* On narrow viewports, the grid's main use is the "I'm working today" view. - Drop the desktop date-column "tab" treatment to a compact inline one. */ -@media (max-width: 720px) { - .plan-grid tbody td.plan-grid-date-col { - /* Matches desktop (no `· 06` suffix to fit), just tighter padding - for cramped screens. */ - min-width: 80px; - padding: 8px 8px; - } - .plan-grid tbody td.plan-grid-date-col .plan-date-daynum { - font-size: 15px; - } - .plan-grid tbody td.plan-grid-date-col .plan-date-dow { - font-size: 9px; - letter-spacing: 0.1em; - } - .plan-grid tbody td { - min-width: 140px; - } -} - -/* ---------------------------------------------------------------------------- - 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) { - .plan-cell, - .plan-cell::before, - .plan-cell::after, - .plan-cell-note { - transition: none !important; - } - .plan-cell:hover::before { - transform: none; - } - .plan-grid-wrap::after { - display: none; - } - /* Kill the mutation pulse animation. */ - .plan-cell--pulse, - .plan-cell--pulse::before { - animation: none !important; - } } /* Category manager modal rows */