feat(plan): migrate the planner grid to MUI (theme-driven, off plan.css)
PlanGrid's bespoke plan.css surface is replaced by a styled(Box) wrapper that scopes all the grid + chip rules and pulls every color from the theme palette via theme.vars (auto light/dark — no more [data-theme] CSS-var blocks). Sticky header + date column, marker-tape cells, category color via the per-cell --cat-color var, weekend/today treatment, hover affordances, the '+' empty hint, and the one-shot mutation pulse are all preserved; the paper-grain/ruled-paper texture was dropped for a clean MUI surface. plan.css trimmed from ~1150 lines to just the category-manager modal rows (the grid is MUI now; the toolbar/banner/day-choice modal had already moved to MUI components). Verified on /plan-work in both light and dark. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -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 (
|
||||
<div className="plan-grid-wrap" data-pulse={pulseAttr}>
|
||||
<PlanGridRoot data-pulse={pulseAttr}>
|
||||
<table className="plan-grid">
|
||||
{/* The colgroup is what actually controls column width in a
|
||||
table — `min-width` on `<th>`/`<td>` is just a floor that
|
||||
@@ -251,6 +603,6 @@ export default function PlanGrid({
|
||||
})}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</PlanGridRoot>
|
||||
);
|
||||
}
|
||||
|
||||
1104
src/admin/plan.css
1104
src/admin/plan.css
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user