UNPKG

@activecollab/components

Version:

ActiveCollab Components

340 lines (325 loc) 15.4 kB
import _extends from "@babel/runtime/helpers/esm/extends"; import React from "react"; import styled, { css } from "styled-components"; import { CARD_FONT, CardProjectLine, EntityDots, StyledClamp, StyledTitleBlock, coverManagement, playControl, Placeholder } from "./shared"; import { ChecklistSmallIcon, DependencySmallIcon, EyeOffSmallIcon, MessageSmallIcon, PersonPlusIcon } from "../../../components/Icons"; import { SkeletonLoader } from "../../../components/Loaders"; import { Signifier } from "../../../components/Signifier"; import { STACKED_CARD_COVER_RATIO as COVER_RATIO, StackedCardCover as Cover, StackedCardRow as StackRow } from "../../../components/StackedCard"; import { Tag } from "../../../components/Tag"; import { Caption1 } from "../../../components/Typography"; import { Typography } from "../../../components/Typography/Typography"; import { RoundAvatar, AVATAR_COLORS } from "../../shared"; /** * TaskCard — the baseline-preservation card. * * This is the replacement for today's column card, so it must reproduce every * behavior that card has (note 52104 §B2) — none of them are optional, each is * a silent-regression risk. Every one is a prop here so the story can toggle it: * * - task-number prefix in the title (config-gated, mirrors `show_task_id`) * - overdue date colouring (red ONLY when overdue AND not completed) * - dependency counter (behind a feature-flag prop) * - hidden-from-clients signifier (shown only when it DIFFERS from the * project default) * - inline assignee affordance (avatar, or an add-assignee placeholder) * - completed (teal tint, no "…" menu, no overdue colour) * - assignees appear ONCE (in the info line when the card has no * date range, in the date row when it * has one — never in both) * * The pending/optimistic and priority states are SHELL states (dimmed + * pointer-events off; red left bar), so the card is wrapped in a `<StackedCard * pending>` / `<StackedCard priorityColor>` by the host — they are not the * card's own concern and live on the shell, not here. * * Everything is measured against the app: `ColumnCardBody.jsx`, * `ColumnCardBodyInfo.jsx`, `ColumnCardDate.jsx` and * `view.project_tasks_column_mode.less`. */ /** * Eight labels, so the default card demonstrates the collapsed form. Colours are * the labels' own, which is why they are literals. */ const DEMO_LABELS = [{ text: "DESIGN", color: "#378ADD" }, { text: "BLOCKED", color: "#D85A30" }, { text: "READY", color: "#639922" }, { text: "REVIEW", color: "#EF9F27" }, { text: "RESEARCH", color: "#7F77DD" }, { text: "COPY", color: "#8C5A2B" }, { text: "QA", color: "#2F5DA8" }, { text: "RELEASE", color: "#8A3E6B" }]; /** * How many labels read as text before the line has to collapse them, and how * many dots stand in for the rest. Two colour-text `Tag`s fit a 260px card; past * that the app's own answer (and the design system's mapping) is coloured dots * plus an overflow count. */ const LABELS_AS_TEXT_MAX = 2; const LABEL_DOTS_MAX = 5; /* The completed tint fills the card body; the shell keeps the priority bar and selection ring on top of it. */ const StyledBody = styled.div.withConfig({ displayName: "TaskCard__StyledBody", componentId: "sc-5iuhg8-0" })(["display:flex;flex-direction:column;", ""], _ref => { let $completed = _ref.$completed; return $completed && css(["background:var(--sc-completed-tint,rgba(72,201,176,0.16));"]); }); /** * The info line — labels, then signifiers, then (only when the card has no date * range) the assignees. * * The separator is the app's, rule for rule: a 2px round dot in * `--color-theme-600` with `margin: 0 6px`, drawn by `::before` on every item * EXCEPT the first (`view.project_tasks_column_mode.less`: * `.c-signifier:not(:first-child):before`, `.label_single_container:not(:first-child) * .label_item:before`, `.dot:before`). Because the rule keys off position rather * than off "am I a signifier", a line that starts with a signifier — a task with * counters but no labels — cannot open with a dangling dot. * * `font-size: 11px` is the `.labels` block's own; the vertical rhythm the app * gets from `margin-top: 10px` comes from the `meta` row's role padding here. */ const StyledInfoLine = styled.div.withConfig({ displayName: "TaskCard__StyledInfoLine", componentId: "sc-5iuhg8-1" })(["display:flex;flex-wrap:wrap;align-items:center;width:100%;font-size:11px;> *{display:flex;align-items:center;min-height:18px;}> *:not(:first-child):not(.sc-assignees)::before{content:\"\";flex-shrink:0;width:2px;height:2px;margin:0 6px;border-radius:100%;background-color:var(--color-theme-600);}.sc-label{max-width:74px;}.sc-label .c-tag > div{overflow:hidden;white-space:nowrap;text-overflow:ellipsis;max-width:60px;}.sc-dots{gap:4px;}.sc-assignees{flex:1 1 auto;justify-content:flex-end;}"]); const StyledDot = styled.span.withConfig({ displayName: "TaskCard__StyledDot", componentId: "sc-5iuhg8-2" })(["width:11px;height:11px;border-radius:50%;background:", ";"], _ref2 => { let $color = _ref2.$color; return $color; }); /** * The date row's text. * * A raw `<span>` in this Storybook inherits the browser's serif default at the * ancestor's size, which is why the date used to read as huge serif type. The * app sets the same four values in CSS on a plain `div` (`.due_date`: 13px, * bold, 24px tall, `--color-theme-900`, `--red-alert` when `.overdue`), so the * fix is to state them here — plus the font stack the missing reset does not * supply. */ const StyledDate = styled.span.withConfig({ displayName: "TaskCard__StyledDate", componentId: "sc-5iuhg8-3" })(["height:24px;font-family:", ";font-size:13px;font-weight:bold;line-height:24px;letter-spacing:0;overflow:hidden;white-space:nowrap;text-overflow:ellipsis;color:", ";"], CARD_FONT, _ref3 => { let $overdue = _ref3.$overdue; return $overdue ? "var(--red-alert)" : "var(--color-theme-900)"; }); /* Avatars are limited to 3 and overlap, as MultiAssigneesPicker does. */ const StyledAssignees = styled.span.withConfig({ displayName: "TaskCard__StyledAssignees", componentId: "sc-5iuhg8-4" })(["display:flex;align-items:center;justify-content:flex-end;"]); /* Inline assignee affordance placeholder — a dashed add-assignee target that stands in until the real picker is wired at integration. */ const StyledAddAssignee = styled.button.withConfig({ displayName: "TaskCard__StyledAddAssignee", componentId: "sc-5iuhg8-5" })(["display:inline-flex;align-items:center;justify-content:center;width:24px;height:24px;padding:0;border:1.5px dashed var(--color-theme-400);border-radius:50%;background:transparent;color:var(--color-theme-600);cursor:pointer;svg{width:14px;height:14px;display:block;}&:hover{border-color:var(--sc-selection-ring);color:var(--color-theme-800);}"]); /** The app's assignee limit (MultiAssigneesPicker `limit={3}`). */ const ASSIGNEE_LIMIT = 3; export const TaskCard = _ref4 => { let _ref4$title = _ref4.title, title = _ref4$title === void 0 ? "Implement the design from the reference on this page" : _ref4$title, _ref4$cover = _ref4.cover, cover = _ref4$cover === void 0 ? false : _ref4$cover, _ref4$playable = _ref4.playable, playable = _ref4$playable === void 0 ? false : _ref4$playable, _ref4$coverControls = _ref4.coverControls, coverControls = _ref4$coverControls === void 0 ? false : _ref4$coverControls, _ref4$showTaskId = _ref4.showTaskId, showTaskId = _ref4$showTaskId === void 0 ? false : _ref4$showTaskId, _ref4$taskId = _ref4.taskId, taskId = _ref4$taskId === void 0 ? 4521 : _ref4$taskId, _ref4$showProjectName = _ref4.showProjectName, showProjectName = _ref4$showProjectName === void 0 ? false : _ref4$showProjectName, _ref4$projectName = _ref4.projectName, projectName = _ref4$projectName === void 0 ? "Marketing Site" : _ref4$projectName, _ref4$labels = _ref4.labels, labels = _ref4$labels === void 0 ? DEMO_LABELS : _ref4$labels, _ref4$subtaskCount = _ref4.subtaskCount, subtaskCount = _ref4$subtaskCount === void 0 ? 2 : _ref4$subtaskCount, _ref4$commentCount = _ref4.commentCount, commentCount = _ref4$commentCount === void 0 ? 7 : _ref4$commentCount, _ref4$dateLabel = _ref4.dateLabel, dateLabel = _ref4$dateLabel === void 0 ? "May 8 – Sep 10" : _ref4$dateLabel, _ref4$overdue = _ref4.overdue, overdue = _ref4$overdue === void 0 ? false : _ref4$overdue, _ref4$showDependencie = _ref4.showDependencies, showDependencies = _ref4$showDependencie === void 0 ? false : _ref4$showDependencie, _ref4$dependencyCount = _ref4.dependencyCount, dependencyCount = _ref4$dependencyCount === void 0 ? 2 : _ref4$dependencyCount, _ref4$hiddenFromClien = _ref4.hiddenFromClients, hiddenFromClients = _ref4$hiddenFromClien === void 0 ? false : _ref4$hiddenFromClien, _ref4$projectDefaultH = _ref4.projectDefaultHidden, projectDefaultHidden = _ref4$projectDefaultH === void 0 ? false : _ref4$projectDefaultH, _ref4$assigned = _ref4.assigned, assigned = _ref4$assigned === void 0 ? true : _ref4$assigned, assignees = _ref4.assignees, _ref4$completed = _ref4.completed, completed = _ref4$completed === void 0 ? false : _ref4$completed; // Signifier appears only when the task's visibility differs from the default. const showHiddenSignifier = hiddenFromClients !== projectDefaultHidden; // Overdue colour is suppressed on completed tasks. const dateIsOverdue = overdue && !completed; const hasDate = dateLabel != null && dateLabel !== ""; const collapseLabels = labels.length > LABELS_AS_TEXT_MAX; /* The assignee slot: the real avatars when the host has them, the demo avatar when it only says "assigned", the add affordance when it says nothing is. */ const assigneeSlot = assigned ? /*#__PURE__*/React.createElement(StyledAssignees, { className: "sc-assignees", "data-fixed": true }, assignees ? assignees.slice(0, ASSIGNEE_LIMIT).map(person => /*#__PURE__*/React.createElement(RoundAvatar, { key: person.name, $bg: person.color, $size: 24, $overlap: true }, person.initials)) : /*#__PURE__*/React.createElement(RoundAvatar, { $bg: AVATAR_COLORS.teal, $size: 24, $bordered: false }, "MS")) : /*#__PURE__*/React.createElement(StyledAssignees, { className: "sc-assignees", "data-fixed": true }, /*#__PURE__*/React.createElement(StyledAddAssignee, { type: "button", "aria-label": "Assign task" }, /*#__PURE__*/React.createElement(PersonPlusIcon, null))); /* Signifiers in the app's fixed order: subtasks → comments → dependencies → hidden-from-clients. Each is one child of the info line, so the line's own separator rule spaces them. */ const signifiers = []; if (subtaskCount > 0) { signifiers.push(/*#__PURE__*/React.createElement(Signifier, { key: "subtasks", type: ChecklistSmallIcon, value: subtaskCount, tooltipText: "Subtasks" })); } if (commentCount > 0) { signifiers.push(/*#__PURE__*/React.createElement(Signifier, { key: "comments", type: MessageSmallIcon, value: commentCount, tooltipText: "Comments" })); } if (showDependencies && dependencyCount > 0) { signifiers.push(/*#__PURE__*/React.createElement(Signifier, { key: "dependencies", type: DependencySmallIcon, value: dependencyCount, tooltipText: "Dependencies" })); } if (showHiddenSignifier) { signifiers.push(/*#__PURE__*/React.createElement(Signifier, { key: "hidden", type: EyeOffSmallIcon, tooltipText: "Hidden from clients" })); } /* The info line exists when it has something to say — labels, signifiers, or the assignees it has to carry because there is no date row. */ const showInfoLine = labels.length > 0 || signifiers.length > 0 || !hasDate; return /*#__PURE__*/React.createElement(StyledBody, { $completed: completed }, cover ? /*#__PURE__*/React.createElement(Cover, { aspectRatio: COVER_RATIO, centerSlot: playable ? playControl : undefined, topRightSlot: coverControls ? coverManagement() : undefined }, /*#__PURE__*/React.createElement("div", { style: { width: "100%", height: "100%", background: "var(--color-theme-300)" } })) : null, /*#__PURE__*/React.createElement(StackRow, { role: "title" }, /*#__PURE__*/React.createElement(StyledTitleBlock, null, showProjectName ? /*#__PURE__*/React.createElement(CardProjectLine, null, projectName) : null, /*#__PURE__*/React.createElement(StyledClamp, null, showTaskId ? /*#__PURE__*/React.createElement(Typography, { as: "span", variant: "Caption 1", color: "tertiary" }, "#" + taskId + " ") : null, title)), completed ? null : /*#__PURE__*/React.createElement(EntityDots, { "aria-label": "Task options" })), showInfoLine ? /*#__PURE__*/React.createElement(StackRow, { role: "meta" }, /*#__PURE__*/React.createElement(StyledInfoLine, null, collapseLabels ? /*#__PURE__*/React.createElement("span", { className: "sc-dots" }, labels.slice(0, LABEL_DOTS_MAX).map(label => /*#__PURE__*/React.createElement(StyledDot, { key: label.text, $color: label.color })), labels.length > LABEL_DOTS_MAX ? /*#__PURE__*/React.createElement(Caption1, { color: "tertiary" }, "+" + (labels.length - LABEL_DOTS_MAX)) : null) : labels.map(label => /*#__PURE__*/React.createElement("span", { className: "sc-label", key: label.text }, /*#__PURE__*/React.createElement(Tag, { name: label.text, color: label.color, showDot: false }))), signifiers, hasDate ? null : assigneeSlot)) : null, hasDate ? /*#__PURE__*/React.createElement(StackRow, { role: "footer" }, /*#__PURE__*/React.createElement(StyledDate, { $overdue: dateIsOverdue }, dateLabel), assigneeSlot) : null); }; /* ---- placeholder + skeleton ---- */ export const TaskPlaceholder = props => /*#__PURE__*/React.createElement(Placeholder, _extends({ icon: /*#__PURE__*/React.createElement(ChecklistSmallIcon, null), label: "Name this task" }, props)); /* Skeleton mirrors the task anatomy row for row: title, meta, footer. */ export const TaskSkeleton = () => /*#__PURE__*/React.createElement(React.Fragment, null, /*#__PURE__*/React.createElement(StackRow, { role: "title" }, /*#__PURE__*/React.createElement(SkeletonLoader, { style: { height: 14, width: "90%" } })), /*#__PURE__*/React.createElement(StackRow, { role: "meta" }, /*#__PURE__*/React.createElement(SkeletonLoader, { style: { height: 10, width: "55%" } })), /*#__PURE__*/React.createElement(StackRow, { role: "footer" }, /*#__PURE__*/React.createElement(SkeletonLoader, { style: { height: 12, width: 64 }, "data-fixed": true }), /*#__PURE__*/React.createElement("span", null), /*#__PURE__*/React.createElement(SkeletonLoader, { "data-fixed": true, style: { width: 24, height: 24, borderRadius: "50%" } }))); //# sourceMappingURL=TaskCard.js.map