Andrey Epifancev 10285371b2 feat(web): карточка задачи окном поверх списка и доски (#37)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-15 13:09:20 +04:00

59 lines
3.7 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import { memo, type PointerEvent } from "react";
import { Link } from "react-router";
import { BOARD_CARD_STATE } from "../../issues/modal.ts";
import { CW, JIRA_URL, KIND, PRIO } from "../constants.ts";
import { lineClamp, type Rect, type RelBox } from "../layout/layout.ts";
import { fmtDate, shortName, unassigned } from "../model/util.ts";
import type { VisNode } from "../types.ts";
export interface CardProps {
node: VisNode;
box?: Rect; // нет — карточка слоя измерения (ширина CW, высота по содержимому)
rel?: RelBox;
multiSprint: boolean;
sprintSel: Set<string>;
onPointerDown?: (ev: PointerEvent<HTMLDivElement>, key: string) => void;
onAutoSize?: (key: string) => void;
}
// T11.1: при перетаскивании одной карточки place() создаёт новые box у всех — сравниваем box по значению
export function cardPropsEqual(a: CardProps, b: CardProps): boolean {
const sameBox = a.box === b.box || (!!a.box && !!b.box && a.box.x === b.box.x && a.box.y === b.box.y && a.box.w === b.box.w && a.box.h === b.box.h);
return sameBox && a.node === b.node && a.rel === b.rel && a.multiSprint === b.multiSprint && a.sprintSel === b.sprintSel &&
a.onPointerDown === b.onPointerDown && a.onAutoSize === b.onAutoSize;
}
export const Card = memo(function Card({ node: n, box, rel, multiSprint, sprintSel, onPointerDown, onAutoSize }: CardProps) {
const cls = "card" + (n.dim || n.stub ? " out" : "") + (n.crit ? " crit" : "") + (n.focus ? " focus" : "");
const style = box ? { left: box.x, top: box.y, width: box.w, height: box.h } : { width: CW };
const showTag = !n.stub && (multiSprint || n.dim); // метка спринта — если спринтов на доске больше одного или это сосед
return (
<div className={cls} title={n.title} style={style} data-key={n.key}
onPointerDown={onPointerDown ? (e) => onPointerDown(e, n.key) : undefined}>
<div className="head">
<a className="key" href={`${JIRA_URL}/browse/${n.key}`} target="_blank" rel="noopener">{n.key}</a>
{!n.stub && <Link className="open-card" to={`/issues/${n.key}`} state={BOARD_CARD_STATE} title="Открыть карточку задачи"
aria-label={`Открыть карточку ${n.key}`}>ⓘ</Link>}
{showTag && (n.sprint
? <span className={"sprint-tag" + (sprintSel.has(n.sprint) ? " sel" : "")} title={n.sprint}>{n.sprint.replace(/^AWPCA_/, "")}</span>
: <span className="sprint-tag none">без спринта</span>)}
</div>
<div className="title" style={{ WebkitLineClamp: box && rel ? lineClamp(rel) : "unset" }}>{n.title}</div>
<div className="meta">
<span className="kind"><i style={{ background: `var(--k-${KIND[n.kind] || "org"})` }} />{n.kind}</span>
<span className="prio" style={{ color: `var(--p-${PRIO[n.prio] || "mid"})` }}>{n.prio}</span>
</div>
{(n.status || n.due) && (
<div className="state">
{n.status && <span className="status" title={n.status}>{n.status}</span>}
{n.due && <span className="due">до {fmtDate(n.due)}</span>}
</div>
)}
{!n.stub && <div className={"who" + (unassigned(n.who) ? " none" : "")} title={n.who}>{shortName(n.who)}</div>}
<div className="rsz" title="Потянуть — изменить размер; двойной щелчок — размер по содержимому"
onDoubleClick={onAutoSize ? () => onAutoSize(n.key) : undefined} />
<div className="grip t" title="Тянуть за рамку" /><div className="grip b" /><div className="grip l" /><div className="grip r" />
</div>
);
}, cardPropsEqual);