feat(plan): render category colors from DB via --cat-color
Replace 14 hardcoded per-category CSS rules with a single --cat-color custom property set inline on each filled cell button, driven by the DB categories list threaded from PlanWork → PlanGrid → PlanRangeChips. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1,5 +1,12 @@
|
|||||||
import { useMemo } from "react";
|
import { useMemo } from "react";
|
||||||
import { GridData, ResolvedCell, planCategoryLabel } from "../lib/queries/plan";
|
import type { CSSProperties } from "react";
|
||||||
|
import {
|
||||||
|
GridData,
|
||||||
|
ResolvedCell,
|
||||||
|
planCategoryLabel,
|
||||||
|
categoryMap,
|
||||||
|
PlanCategory,
|
||||||
|
} from "../lib/queries/plan";
|
||||||
import type { Project } from "../lib/queries/projects";
|
import type { Project } from "../lib/queries/projects";
|
||||||
import PlanRangeChips from "./PlanRangeChips";
|
import PlanRangeChips from "./PlanRangeChips";
|
||||||
|
|
||||||
@@ -7,6 +14,7 @@ interface Props {
|
|||||||
data: GridData | undefined;
|
data: GridData | undefined;
|
||||||
canEdit: boolean;
|
canEdit: boolean;
|
||||||
projects: Project[];
|
projects: Project[];
|
||||||
|
categories: PlanCategory[];
|
||||||
// The (userId, date) of the most recent successful mutation. The
|
// The (userId, date) of the most recent successful mutation. The
|
||||||
// matching cell gets a one-shot highlight pulse. `nonce` is part of
|
// matching cell gets a one-shot highlight pulse. `nonce` is part of
|
||||||
// the value so back-to-back mutations on the same cell re-trigger
|
// the value so back-to-back mutations on the same cell re-trigger
|
||||||
@@ -92,10 +100,12 @@ export default function PlanGrid({
|
|||||||
data,
|
data,
|
||||||
canEdit,
|
canEdit,
|
||||||
projects,
|
projects,
|
||||||
|
categories,
|
||||||
pulseKey,
|
pulseKey,
|
||||||
onCellClick,
|
onCellClick,
|
||||||
}: Props) {
|
}: Props) {
|
||||||
const today = useMemo(() => todayIso(), []);
|
const today = useMemo(() => todayIso(), []);
|
||||||
|
const catMap = useMemo(() => categoryMap(categories), [categories]);
|
||||||
|
|
||||||
if (!data)
|
if (!data)
|
||||||
return (
|
return (
|
||||||
@@ -206,10 +216,17 @@ export default function PlanGrid({
|
|||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className={cls}
|
className={cls}
|
||||||
|
style={
|
||||||
|
cell
|
||||||
|
? ({
|
||||||
|
"--cat-color": catMap[cell.category]?.color,
|
||||||
|
} as CSSProperties)
|
||||||
|
: undefined
|
||||||
|
}
|
||||||
onClick={() => onCellClick(u.id, date, cell)}
|
onClick={() => onCellClick(u.id, date, cell)}
|
||||||
aria-label={
|
aria-label={
|
||||||
cell
|
cell
|
||||||
? `${u.full_name}, ${date}, ${planCategoryLabel(cell.category)}`
|
? `${u.full_name}, ${date}, ${planCategoryLabel(cell.category, catMap)}`
|
||||||
: isLocked
|
: isLocked
|
||||||
? `${u.full_name}, ${date}, ${past ? "uplynulý den" : "prázdné"} — bez záznamu`
|
? `${u.full_name}, ${date}, ${past ? "uplynulý den" : "prázdné"} — bez záznamu`
|
||||||
: `${u.full_name}, ${date}, prázdné — přidat záznam`
|
: `${u.full_name}, ${date}, prázdné — přidat záznam`
|
||||||
@@ -225,6 +242,9 @@ export default function PlanGrid({
|
|||||||
: null
|
: null
|
||||||
}
|
}
|
||||||
readonly={!canEdit}
|
readonly={!canEdit}
|
||||||
|
categoryLabel={
|
||||||
|
cell ? planCategoryLabel(cell.category, catMap) : ""
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
</button>
|
</button>
|
||||||
</td>
|
</td>
|
||||||
|
|||||||
@@ -1,17 +1,19 @@
|
|||||||
import {
|
import { ResolvedCell, cellProjectLabel } from "../lib/queries/plan";
|
||||||
ResolvedCell,
|
|
||||||
planCategoryLabel,
|
|
||||||
cellProjectLabel,
|
|
||||||
} from "../lib/queries/plan";
|
|
||||||
import type { Project } from "../lib/queries/projects";
|
import type { Project } from "../lib/queries/projects";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
cell: ResolvedCell | null;
|
cell: ResolvedCell | null;
|
||||||
project: Project | null;
|
project: Project | null;
|
||||||
readonly?: boolean;
|
readonly?: boolean;
|
||||||
|
categoryLabel: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function PlanRangeChips({ cell, project, readonly }: Props) {
|
export default function PlanRangeChips({
|
||||||
|
cell,
|
||||||
|
project,
|
||||||
|
readonly,
|
||||||
|
categoryLabel,
|
||||||
|
}: Props) {
|
||||||
void readonly;
|
void readonly;
|
||||||
if (!cell) return null;
|
if (!cell) return null;
|
||||||
// Prefer the server-embedded project label (always present). Fall back to
|
// Prefer the server-embedded project label (always present). Fall back to
|
||||||
@@ -27,9 +29,7 @@ export default function PlanRangeChips({ cell, project, readonly }: Props) {
|
|||||||
return (
|
return (
|
||||||
<div className="plan-chip-block">
|
<div className="plan-chip-block">
|
||||||
<div className="plan-chip-line">
|
<div className="plan-chip-line">
|
||||||
<span className={`plan-chip plan-chip--${cell.category}`}>
|
<span className="plan-chip">{categoryLabel}</span>
|
||||||
{planCategoryLabel(cell.category)}
|
|
||||||
</span>
|
|
||||||
{projectLabel && (
|
{projectLabel && (
|
||||||
<span className="plan-chip-project" title={projectTitle}>
|
<span className="plan-chip-project" title={projectTitle}>
|
||||||
{projectLabel}
|
{projectLabel}
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ import PlanGrid from "../components/PlanGrid";
|
|||||||
import PlanCellModal from "../components/PlanCellModal";
|
import PlanCellModal from "../components/PlanCellModal";
|
||||||
import Forbidden from "../components/Forbidden";
|
import Forbidden from "../components/Forbidden";
|
||||||
import { projectListOptions, type Project } from "../lib/queries/projects";
|
import { projectListOptions, type Project } from "../lib/queries/projects";
|
||||||
|
import { planCategoriesOptions, type PlanCategory } from "../lib/queries/plan";
|
||||||
import "../plan.css";
|
import "../plan.css";
|
||||||
|
|
||||||
const MONTH_NAMES = [
|
const MONTH_NAMES = [
|
||||||
@@ -125,6 +126,9 @@ export default function PlanWork() {
|
|||||||
const { data: projectsData } = useQuery(projectListOptions({ perPage: 200 }));
|
const { data: projectsData } = useQuery(projectListOptions({ perPage: 200 }));
|
||||||
const projects: Project[] = (projectsData?.data as Project[]) ?? [];
|
const projects: Project[] = (projectsData?.data as Project[]) ?? [];
|
||||||
|
|
||||||
|
const { data: categoriesData } = useQuery(planCategoriesOptions());
|
||||||
|
const categories: PlanCategory[] = categoriesData ?? [];
|
||||||
|
|
||||||
// `lastMutated` is the (userId, date) of the most recent successful
|
// `lastMutated` is the (userId, date) of the most recent successful
|
||||||
// mutation. We pass it to PlanGrid as `pulseKey`; the matching cell
|
// mutation. We pass it to PlanGrid as `pulseKey`; the matching cell
|
||||||
// gets a one-shot CSS pulse animation. We clear it after 700ms (the
|
// gets a one-shot CSS pulse animation. We clear it after 700ms (the
|
||||||
@@ -667,6 +671,7 @@ export default function PlanWork() {
|
|||||||
<PlanGrid
|
<PlanGrid
|
||||||
data={grid}
|
data={grid}
|
||||||
projects={projects}
|
projects={projects}
|
||||||
|
categories={categories}
|
||||||
canEdit={canEdit}
|
canEdit={canEdit}
|
||||||
pulseKey={lastMutated}
|
pulseKey={lastMutated}
|
||||||
onCellClick={openCell}
|
onCellClick={openCell}
|
||||||
|
|||||||
@@ -418,41 +418,13 @@
|
|||||||
bottom: 8px;
|
bottom: 8px;
|
||||||
width: 3px;
|
width: 3px;
|
||||||
border-radius: 2px;
|
border-radius: 2px;
|
||||||
background: var(--plan-paper-rule);
|
background: var(--cat-color, var(--plan-paper-rule));
|
||||||
transition:
|
transition:
|
||||||
background var(--motion-fast) ease,
|
background var(--motion-fast) ease,
|
||||||
box-shadow var(--motion-fast) ease,
|
box-shadow var(--motion-fast) ease,
|
||||||
transform var(--motion-fast) ease;
|
transform var(--motion-fast) ease;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* When the cell has a chip, paint the tape to match the category. We use
|
|
||||||
the chip's class — but since the tape lives on the *button* and the
|
|
||||||
category class lives on the inner chip span, we paint it from a custom
|
|
||||||
property set on the button via JS-free attribute mapping. As a simpler
|
|
||||||
approach, color the tape by reading the .plan-chip--* class of the
|
|
||||||
*first descendant* with a CSS selector — modern browsers support :has(). */
|
|
||||||
.plan-cell:has(.plan-chip--work)::before {
|
|
||||||
background: var(--plan-tape-work);
|
|
||||||
}
|
|
||||||
.plan-cell:has(.plan-chip--preparation)::before {
|
|
||||||
background: var(--plan-tape-preparation);
|
|
||||||
}
|
|
||||||
.plan-cell:has(.plan-chip--travel)::before {
|
|
||||||
background: var(--plan-tape-travel);
|
|
||||||
}
|
|
||||||
.plan-cell:has(.plan-chip--leave)::before {
|
|
||||||
background: var(--plan-tape-leave);
|
|
||||||
}
|
|
||||||
.plan-cell:has(.plan-chip--sick)::before {
|
|
||||||
background: var(--plan-tape-sick);
|
|
||||||
}
|
|
||||||
.plan-cell:has(.plan-chip--training)::before {
|
|
||||||
background: var(--plan-tape-training);
|
|
||||||
}
|
|
||||||
.plan-cell:has(.plan-chip--other)::before {
|
|
||||||
background: var(--plan-tape-other);
|
|
||||||
}
|
|
||||||
|
|
||||||
.plan-cell:hover {
|
.plan-cell:hover {
|
||||||
background: var(--plan-cell-bg-hover);
|
background: var(--plan-cell-bg-hover);
|
||||||
}
|
}
|
||||||
@@ -677,67 +649,27 @@
|
|||||||
font-family: var(--plan-font-body);
|
font-family: var(--plan-font-body);
|
||||||
/* Category chip is small, monoline. The heavy color lives on the tape
|
/* 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. */
|
on the cell edge — the chip is the *text* version of the same color. */
|
||||||
color: var(--plan-ink);
|
color: var(--cat-color, var(--plan-ink));
|
||||||
background: color-mix(in srgb, var(--plan-paper) 60%, var(--plan-paper-soft));
|
background: color-mix(
|
||||||
border: 1px solid var(--plan-paper-rule);
|
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;
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Category chips inherit the cell's tape color via :has() */
|
|
||||||
.plan-cell:has(.plan-chip--work) .plan-chip--work {
|
|
||||||
color: var(--plan-tape-work);
|
|
||||||
border-color: color-mix(in srgb, var(--plan-tape-work) 35%, transparent);
|
|
||||||
background: color-mix(in srgb, var(--plan-tape-work) 10%, var(--plan-paper));
|
|
||||||
}
|
|
||||||
.plan-cell:has(.plan-chip--preparation) .plan-chip--preparation {
|
|
||||||
color: var(--plan-tape-preparation);
|
|
||||||
border-color: color-mix(
|
|
||||||
in srgb,
|
|
||||||
var(--plan-tape-preparation) 35%,
|
|
||||||
transparent
|
|
||||||
);
|
|
||||||
background: color-mix(
|
|
||||||
in srgb,
|
|
||||||
var(--plan-tape-preparation) 10%,
|
|
||||||
var(--plan-paper)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
.plan-cell:has(.plan-chip--travel) .plan-chip--travel {
|
|
||||||
color: var(--plan-tape-travel);
|
|
||||||
border-color: color-mix(in srgb, var(--plan-tape-travel) 35%, transparent);
|
|
||||||
background: color-mix(
|
|
||||||
in srgb,
|
|
||||||
var(--plan-tape-travel) 10%,
|
|
||||||
var(--plan-paper)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
.plan-cell:has(.plan-chip--leave) .plan-chip--leave {
|
|
||||||
color: var(--plan-tape-leave);
|
|
||||||
border-color: color-mix(in srgb, var(--plan-tape-leave) 35%, transparent);
|
|
||||||
background: color-mix(in srgb, var(--plan-tape-leave) 10%, var(--plan-paper));
|
|
||||||
}
|
|
||||||
.plan-cell:has(.plan-chip--sick) .plan-chip--sick {
|
|
||||||
color: var(--plan-tape-sick);
|
|
||||||
border-color: color-mix(in srgb, var(--plan-tape-sick) 35%, transparent);
|
|
||||||
background: color-mix(in srgb, var(--plan-tape-sick) 10%, var(--plan-paper));
|
|
||||||
}
|
|
||||||
.plan-cell:has(.plan-chip--training) .plan-chip--training {
|
|
||||||
color: var(--plan-tape-training);
|
|
||||||
border-color: color-mix(in srgb, var(--plan-tape-training) 35%, transparent);
|
|
||||||
background: color-mix(
|
|
||||||
in srgb,
|
|
||||||
var(--plan-tape-training) 10%,
|
|
||||||
var(--plan-paper)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
.plan-cell:has(.plan-chip--other) .plan-chip--other {
|
|
||||||
color: var(--plan-tape-other);
|
|
||||||
border-color: color-mix(in srgb, var(--plan-tape-other) 35%, transparent);
|
|
||||||
background: color-mix(in srgb, var(--plan-tape-other) 10%, var(--plan-paper));
|
|
||||||
}
|
|
||||||
|
|
||||||
[data-theme="dark"] .plan-chip {
|
[data-theme="dark"] .plan-chip {
|
||||||
background: color-mix(in srgb, var(--plan-cell-bg) 90%, var(--plan-paper));
|
background: color-mix(
|
||||||
|
in srgb,
|
||||||
|
var(--cat-color, var(--plan-cell-bg)) 16%,
|
||||||
|
var(--plan-paper)
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
.plan-chip-project {
|
.plan-chip-project {
|
||||||
|
|||||||
Reference in New Issue
Block a user