@activecollab/components 2.0.365 → 2.0.366

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,855 @@
1
+ import _extends from "@babel/runtime/helpers/esm/extends";
2
+ import _objectWithoutPropertiesLoose from "@babel/runtime/helpers/esm/objectWithoutPropertiesLoose";
3
+ const _excluded = ["open", "className", "children"],
4
+ _excluded2 = ["labels"],
5
+ _excluded3 = ["value", "tooltip", "overdue"];
6
+ import React, { createContext, forwardRef, useCallback, useContext, useMemo, useState } from "react";
7
+ import moment from "moment-timezone";
8
+ import styled, { css } from "styled-components";
9
+ import { Button } from "../../components/Button";
10
+ import { Checkbox } from "../../components/Checkbox";
11
+ import { Chip } from "../../components/Chip";
12
+ import { DatePicker } from "../../components/DatePicker";
13
+ import { Dot } from "../../components/Dot";
14
+ import { CalendarIcon, ClockAddIcon, CollapseExpandSingleIcon } from "../../components/Icons";
15
+ import { InputHours } from "../../components/Input";
16
+ import { Label } from "../../components/Label";
17
+ import { Modal } from "../../components/Modal";
18
+ import { ProgressPie } from "../../components/ProgressPie";
19
+ import { Select } from "../../components/Select";
20
+ import { SelectTrigger } from "../../components/SelectTrigger";
21
+ import { Textarea } from "../../components/Textarea";
22
+ import { SlideFromTop } from "../../components/Transitions";
23
+ import { Body2, Caption1, Caption2, Header3 } from "../../components/Typography";
24
+ import { formatHours } from "../../utils";
25
+
26
+ /**
27
+ * Interactive list-view column cells for the `Presentation/List view` story —
28
+ * the Label, Date and Time cells with their editors, mirroring
29
+ * `new-activecollab-front/src/components/ListView/columns/*`:
30
+ *
31
+ * - **Label** (`columns/Label.js` + `LabelTrigger`/`LabelItems`): chips for 1–2
32
+ * labels, dots from 3 up, `+N` past 8, and a DS `Select` picker (search,
33
+ * colored dot + label name in the label's own colour, checkbox, "+ Add new").
34
+ * - **Date** (`columns/Date.js`): a range trigger that formats like the app
35
+ * (`formatDateRange`), turns red when overdue, and opens the DS `DatePicker`
36
+ * in atomic mode so it gets the Save / ✕ / Clear footer.
37
+ * - **Time** (`columns/Time.js`): `ProgressPie` + `tracked/estimated`, coloured
38
+ * by how much of the estimate is used, opening the "Add Time Record" dialog.
39
+ *
40
+ * Everything is local state — there is no store, so edits live as long as the
41
+ * story is mounted.
42
+ */
43
+
44
+ const SANS = '-apple-system, BlinkMacSystemFont, "Inter", "Roboto", "Helvetica Neue", Arial, sans-serif';
45
+
46
+ /**
47
+ * CSS-only tooltip, mirroring the app's `.c-cooltip.c-cooltip--top` (see
48
+ * `styles/c_cooltip.less`). The list renders hundreds of these, and the DS
49
+ * `Tooltip` mounts a Portal + Popper per instance — far too heavy per cell — so
50
+ * the cells use `data-tooltip` exactly like the real list view does.
51
+ */
52
+ const CooltipStyle = css(["&[data-tooltip],[data-tooltip]{position:relative;}&[data-tooltip]::after,[data-tooltip]::after{pointer-events:none;content:attr(data-tooltip);position:absolute;bottom:calc(100% + 0.5rem);left:50%;transform:scale(0.85) translateX(-50%);transform-origin:0;opacity:0;max-width:12.5rem;padding:0.25rem 0.625rem;border-radius:0.5rem;background-color:rgba(0,0,0,0.9);color:#fff;font-family:", ";font-size:0.75rem;font-weight:500;line-height:1.3;text-align:center;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;}@media (hover:hover){&[data-tooltip]:not(:focus):hover::after,[data-tooltip]:not(:focus):hover::after{transition:opacity 0.15s ease,transform 0.15s ease;transition-delay:0.6s;transform:scale(1) translateX(-50%);opacity:1;}}"], SANS);
53
+
54
+ /* ================================================================== */
55
+ /* Shared trigger button (mirrors ListView/ListButton.js) */
56
+ /* ================================================================== */
57
+
58
+ /**
59
+ * `Menu` clones its `target` and injects `open`, so — like the app's
60
+ * `ListButton` — the trigger has to swallow that prop and turn it into the DS
61
+ * `active` state plus an `.open` class the row CSS uses to keep the trigger
62
+ * visible while its menu is open.
63
+ */
64
+ const ListButtonBase = /*#__PURE__*/forwardRef((_ref, ref) => {
65
+ let open = _ref.open,
66
+ className = _ref.className,
67
+ children = _ref.children,
68
+ rest = _objectWithoutPropertiesLoose(_ref, _excluded);
69
+ return /*#__PURE__*/React.createElement(Button, _extends({}, rest, {
70
+ ref: ref,
71
+ type: "button",
72
+ variant: "text gray",
73
+ active: open,
74
+ className: "list-button" + (className ? " " + className : "") + (open ? " open" : "")
75
+ }), children);
76
+ });
77
+ ListButtonBase.displayName = "ListButtonBase";
78
+
79
+ /*
80
+ * The DS Button treats a single element child as "icon only" — inline-flex,
81
+ * centered, a fixed 32px wide and `overflow: hidden`, which squashes a date or
82
+ * a chip. `&&` doubles the specificity so these rules win over that block; the
83
+ * button then hugs its content and the column's `justify-content: flex-end`
84
+ * parks it against the right edge.
85
+ */
86
+ const StyledListButton = styled(ListButtonBase).withConfig({
87
+ displayName: "columns__StyledListButton",
88
+ componentId: "sc-9f4385-0"
89
+ })(["&&{display:inline-flex;align-items:center;justify-content:flex-end;width:auto;min-width:0;max-width:100%;padding:4px 8px;overflow:visible;text-align:right;}", ""], CooltipStyle);
90
+
91
+ /* ================================================================== */
92
+ /* Labels */
93
+ /* ================================================================== */
94
+
95
+ /* The project's label catalogue. Names are uppercase, like in the app. */
96
+ const INITIAL_LABELS = [{
97
+ id: "in-progress",
98
+ name: "IN PROGRESS",
99
+ color: "#CDE8F5",
100
+ textColor: "#3D7F9E"
101
+ }, {
102
+ id: "needs-review",
103
+ name: "NEEDS REVIEW",
104
+ color: "#F9E79F",
105
+ textColor: "#A98A17"
106
+ }, {
107
+ id: "approved",
108
+ name: "APPROVED",
109
+ color: "#A9DFBF",
110
+ textColor: "#3D9A6A"
111
+ }, {
112
+ id: "blocked",
113
+ name: "BLOCKED",
114
+ color: "#F1948A",
115
+ textColor: "#B5544C"
116
+ }, {
117
+ id: "on-hold",
118
+ name: "ON HOLD",
119
+ color: "#F5CBA7",
120
+ textColor: "#B4713A"
121
+ }, {
122
+ id: "idea",
123
+ name: "IDEA",
124
+ color: "#FAD7A0",
125
+ textColor: "#B98A2B"
126
+ }, {
127
+ id: "todo",
128
+ name: "TODO",
129
+ color: "#D6EAF8",
130
+ textColor: "#4A87B5"
131
+ }, {
132
+ id: "research",
133
+ name: "RESEARCH",
134
+ color: "#D7BDE2",
135
+ textColor: "#8155A0"
136
+ }, {
137
+ id: "accessibility",
138
+ name: "ACCESSIBILITY",
139
+ color: "#AED6F1",
140
+ textColor: "#3E7CA6"
141
+ }, {
142
+ id: "copy",
143
+ name: "COPY",
144
+ color: "#F5B7B1",
145
+ textColor: "#B5544C"
146
+ }, {
147
+ id: "design-debt",
148
+ name: "DESIGN DEBT",
149
+ color: "#E8DAEF",
150
+ textColor: "#7D5A99"
151
+ }, {
152
+ id: "qa",
153
+ name: "QA",
154
+ color: "#A3E4D7",
155
+ textColor: "#34907C"
156
+ }, {
157
+ id: "client-input",
158
+ name: "CLIENT INPUT",
159
+ color: "#FDEBD0",
160
+ textColor: "#B08640"
161
+ }, {
162
+ id: "mobile",
163
+ name: "MOBILE",
164
+ color: "#D1F2EB",
165
+ textColor: "#2E8B7A"
166
+ }, {
167
+ id: "performance",
168
+ name: "PERFORMANCE",
169
+ color: "#FCF3CF",
170
+ textColor: "#A38B1E"
171
+ }, {
172
+ id: "asset",
173
+ name: "ASSET",
174
+ color: "#D5D8DC",
175
+ textColor: "#6B7378"
176
+ }];
177
+
178
+ /* Colours handed out to labels created from the picker's "+ Add new" row. */
179
+ const NEW_LABEL_COLORS = [{
180
+ color: "#F9E0F2",
181
+ textColor: "#A4569B"
182
+ }, {
183
+ color: "#DDEBFB",
184
+ textColor: "#4E7BA8"
185
+ }, {
186
+ color: "#E4F6D8",
187
+ textColor: "#5F9440"
188
+ }, {
189
+ color: "#FDE2D5",
190
+ textColor: "#B36A45"
191
+ }];
192
+ const LabelsContext = /*#__PURE__*/createContext({
193
+ labels: INITIAL_LABELS,
194
+ labelsById: {},
195
+ addLabel: () => ""
196
+ });
197
+
198
+ /**
199
+ * Holds the label catalogue for the whole list, so a label created in one
200
+ * task's picker shows up in every other picker too.
201
+ */
202
+ export const LabelsProvider = _ref2 => {
203
+ let children = _ref2.children;
204
+ const _useState = useState(INITIAL_LABELS),
205
+ labels = _useState[0],
206
+ setLabels = _useState[1];
207
+ const addLabel = useCallback(name => {
208
+ const trimmed = name.trim().toUpperCase();
209
+ const id = "label-" + trimmed.toLowerCase().replace(/[^a-z0-9]+/g, "-");
210
+ setLabels(prev => {
211
+ if (prev.some(label => label.id === id)) {
212
+ return prev;
213
+ }
214
+ const palette = NEW_LABEL_COLORS[prev.length % NEW_LABEL_COLORS.length];
215
+ return [...prev, _extends({
216
+ id,
217
+ name: trimmed
218
+ }, palette)];
219
+ });
220
+ return id;
221
+ }, []);
222
+ const value = useMemo(() => ({
223
+ labels,
224
+ labelsById: labels.reduce((acc, label) => _extends({}, acc, {
225
+ [label.id]: label
226
+ }), {}),
227
+ addLabel
228
+ }), [addLabel, labels]);
229
+ return /*#__PURE__*/React.createElement(LabelsContext.Provider, {
230
+ value: value
231
+ }, children);
232
+ };
233
+ const useTaskLabels = () => useContext(LabelsContext);
234
+
235
+ /* Empty state pill (mirrors EntityLabel + .entity-label in list-view.less) */
236
+ const StyledLabelPlaceholder = styled.span.withConfig({
237
+ displayName: "columns__StyledLabelPlaceholder",
238
+ componentId: "sc-9f4385-1"
239
+ })(["display:inline-flex;align-items:center;padding:1px 6px;border:1px solid var(--color-theme-700);border-radius:4px;.list-button.open &,.list-button:hover &{border-color:var(--color-primary-700,var(--color-primary));}"]);
240
+ const StyledLabelItems = styled.span.withConfig({
241
+ displayName: "columns__StyledLabelItems",
242
+ componentId: "sc-9f4385-2"
243
+ })(["display:inline-flex;align-items:center;min-width:0;> *:not(:last-child){margin-right:2px;}.rest-count{margin-left:2px;}", ""], CooltipStyle);
244
+ const MAX_SHOWN_LABELS = 8;
245
+ const LabelItems = _ref3 => {
246
+ let labels = _ref3.labels;
247
+ const showAsDot = labels.length > 2;
248
+ let shown = labels;
249
+ let restCount = 0;
250
+ if (labels.length > MAX_SHOWN_LABELS) {
251
+ restCount = Math.min(labels.length - MAX_SHOWN_LABELS, 99);
252
+ shown = labels.slice(0, MAX_SHOWN_LABELS - 1);
253
+ }
254
+ return /*#__PURE__*/React.createElement(StyledLabelItems, null, shown.map(label => showAsDot ? /*#__PURE__*/React.createElement(Dot, {
255
+ key: label.id,
256
+ color: label.color,
257
+ size: 20,
258
+ "data-tooltip": label.name
259
+ }) : /*#__PURE__*/React.createElement(Chip, {
260
+ key: label.id,
261
+ label: label.name,
262
+ backgroundColor: label.color,
263
+ color: "#303037",
264
+ style: {
265
+ maxWidth: "90px"
266
+ },
267
+ typographyProps: {
268
+ variant: "Caption 2",
269
+ weight: "bold"
270
+ },
271
+ "data-tooltip": label.name
272
+ })), restCount > 0 ? /*#__PURE__*/React.createElement(Body2, {
273
+ weight: "bold",
274
+ className: "rest-count"
275
+ }, "+", restCount) : null);
276
+ };
277
+ const LabelTrigger = /*#__PURE__*/forwardRef((_ref4, ref) => {
278
+ let labels = _ref4.labels,
279
+ rest = _objectWithoutPropertiesLoose(_ref4, _excluded2);
280
+ if (labels.length === 0) {
281
+ return /*#__PURE__*/React.createElement(StyledListButton, _extends({}, rest, {
282
+ ref: ref,
283
+ className: "show-on-hover",
284
+ "aria-label": "Add labels",
285
+ "data-tooltip": "Add Labels"
286
+ }), /*#__PURE__*/React.createElement(StyledLabelPlaceholder, null, /*#__PURE__*/React.createElement(Caption2, {
287
+ weight: "bold",
288
+ color: "tertiary"
289
+ }, "LABEL")));
290
+ }
291
+ return /*#__PURE__*/React.createElement(StyledListButton, _extends({}, rest, {
292
+ ref: ref,
293
+ "aria-label": "Change labels"
294
+ }), /*#__PURE__*/React.createElement(LabelItems, {
295
+ labels: labels
296
+ }));
297
+ });
298
+ LabelTrigger.displayName = "LabelTrigger";
299
+ export const LabelCell = _ref5 => {
300
+ let labelIds = _ref5.labelIds,
301
+ _ref5$completed = _ref5.completed,
302
+ completed = _ref5$completed === void 0 ? false : _ref5$completed;
303
+ const _useTaskLabels = useTaskLabels(),
304
+ catalog = _useTaskLabels.labels,
305
+ labelsById = _useTaskLabels.labelsById,
306
+ addLabel = _useTaskLabels.addLabel;
307
+ const _useState2 = useState(labelIds != null ? labelIds : []),
308
+ selected = _useState2[0],
309
+ setSelected = _useState2[1];
310
+
311
+ /* alphabetical, like LabelTrigger sorts before rendering */
312
+ const selectedLabels = useMemo(() => selected.map(id => labelsById[id]).filter(label => Boolean(label)).sort((a, b) => a.name.localeCompare(b.name)), [labelsById, selected]);
313
+ const options = useMemo(() => catalog.map(label => ({
314
+ id: label.id,
315
+ name: label.name,
316
+ color: label.color,
317
+ textColor: label.textColor
318
+ })), [catalog]);
319
+ const handleChange = useCallback(value => {
320
+ setSelected(Array.isArray(value) ? value.map(String) : [String(value)].filter(Boolean));
321
+ }, []);
322
+ const handleAddNew = useCallback(name => {
323
+ if (!name || !name.trim()) {
324
+ return;
325
+ }
326
+ const id = addLabel(name);
327
+ setSelected(prev => prev.includes(id) ? prev : [...prev, id]);
328
+ }, [addLabel]);
329
+
330
+ /* Completed tasks show their labels read-only (mirrors Label.js) */
331
+ if (completed) {
332
+ return /*#__PURE__*/React.createElement(StyledReadOnlyCell, {
333
+ className: "col col-label"
334
+ }, selectedLabels.length > 0 ? /*#__PURE__*/React.createElement(LabelItems, {
335
+ labels: selectedLabels
336
+ }) : null);
337
+ }
338
+ return /*#__PURE__*/React.createElement("div", {
339
+ className: "col col-label"
340
+ }, /*#__PURE__*/React.createElement(Select, {
341
+ type: "multiple",
342
+ options: options,
343
+ selected: selected,
344
+ onChange: handleChange,
345
+ emptyValue: "Add new",
346
+ emptyAction: handleAddNew,
347
+ autoHeightMax: 278,
348
+ position: "bottom-end",
349
+ target: /*#__PURE__*/React.createElement(LabelTrigger, {
350
+ labels: selectedLabels
351
+ })
352
+ }));
353
+ };
354
+
355
+ /* ================================================================== */
356
+ /* Date */
357
+ /* ================================================================== */
358
+
359
+ /** The personalized date format the app defaults to (`MMM D. YYYY`). */
360
+ const DATE_FORMAT = "MMM D. YYYY";
361
+
362
+ /** Midnight UTC today — the reference point for the mock data and overdue. */
363
+ const todayUnix = () => moment.utc().startOf("day").unix();
364
+
365
+ /** Build a mock timestamp N days from today (negative = in the past). */
366
+ export const dayOffset = offset => moment.utc().startOf("day").add(offset, "days").unix();
367
+
368
+ /** mirrors utils/date.js → formatDateRange (month-first format) */
369
+ export const formatDateRange = (from, to) => {
370
+ if (from === undefined || from === null) {
371
+ return undefined;
372
+ }
373
+ const start = moment.unix(from).utc();
374
+ const due = moment.unix(to != null ? to : from).utc();
375
+ const withoutYear = DATE_FORMAT.replace(/YYYY|,$/gi, "").trim();
376
+ const withoutYearAndMonth = withoutYear.replace(/MMM|,$/gi, "").trim();
377
+ if (from === to || to === undefined) {
378
+ return start.format(withoutYear);
379
+ }
380
+ if (start.year() === due.year() && start.month() === due.month()) {
381
+ return start.format(withoutYear) + " - " + due.format(withoutYearAndMonth);
382
+ }
383
+ return start.format(withoutYear) + " - " + due.format(withoutYear);
384
+ };
385
+
386
+ /** mirrors utils/date.js → formatDateSmart(…, long) */
387
+ const formatDateLong = value => value ? moment.unix(value).utc().format(DATE_FORMAT) : "";
388
+ export const isOverdue = (dueOn, completed) => Boolean(dueOn && !completed && dueOn < todayUnix());
389
+ const StyledDateValue = styled.span.withConfig({
390
+ displayName: "columns__StyledDateValue",
391
+ componentId: "sc-9f4385-3"
392
+ })(["font-family:", ";white-space:nowrap;"], SANS);
393
+
394
+ /* completed rows render values without a trigger, so the cell itself hosts the
395
+ CSS tooltip */
396
+ const StyledReadOnlyCell = styled.div.withConfig({
397
+ displayName: "columns__StyledReadOnlyCell",
398
+ componentId: "sc-9f4385-4"
399
+ })(["padding-right:8px;", ""], CooltipStyle);
400
+
401
+ /**
402
+ * `DatePicker` wraps itself in a `width: 100%` inline-block, which would stretch
403
+ * across the column and left-align the trigger the moment the picker mounts —
404
+ * the value would visibly jump and the popper would anchor to the wrong spot.
405
+ * Keeping that wrapper right-aligned makes the open state geometrically
406
+ * identical to the closed one.
407
+ */
408
+ const StyledPickerCell = styled.div.withConfig({
409
+ displayName: "columns__StyledPickerCell",
410
+ componentId: "sc-9f4385-5"
411
+ })(["> div{text-align:right;}"]);
412
+ const DateTrigger = /*#__PURE__*/forwardRef((_ref6, ref) => {
413
+ let value = _ref6.value,
414
+ tooltip = _ref6.tooltip,
415
+ overdue = _ref6.overdue,
416
+ rest = _objectWithoutPropertiesLoose(_ref6, _excluded3);
417
+ return /*#__PURE__*/React.createElement(StyledListButton, _extends({}, rest, {
418
+ ref: ref,
419
+ className: value ? "show-on-hover has-value" : "show-on-hover",
420
+ "aria-label": value ? "Change dates" : "Add date",
421
+ "data-tooltip": tooltip
422
+ }), value ? /*#__PURE__*/React.createElement(StyledDateValue, null, /*#__PURE__*/React.createElement(Caption1, {
423
+ weight: "bold",
424
+ color: overdue ? "alert" : "secondary"
425
+ }, value)) : /*#__PURE__*/React.createElement(CalendarIcon, null));
426
+ });
427
+ DateTrigger.displayName = "DateTrigger";
428
+ export const DateCell = _ref7 => {
429
+ var _dates$from, _dates$to2;
430
+ let startOn = _ref7.startOn,
431
+ dueOn = _ref7.dueOn,
432
+ _ref7$completed = _ref7.completed,
433
+ completed = _ref7$completed === void 0 ? false : _ref7$completed;
434
+ const _useState3 = useState({
435
+ from: startOn,
436
+ to: dueOn
437
+ }),
438
+ dates = _useState3[0],
439
+ setDates = _useState3[1];
440
+ /**
441
+ * The DS `DatePicker` does real work on mount (timezone guess, month grids,
442
+ * key handlers) — with one per row that is the single most expensive thing on
443
+ * this page. So the closed state is just the trigger; the picker is mounted
444
+ * on first click, already open, and unmounted again when it closes.
445
+ */
446
+ const _useState4 = useState(false),
447
+ pickerMounted = _useState4[0],
448
+ setPickerMounted = _useState4[1];
449
+
450
+ /* moment isn't cheap either; only reformat when the dates actually change */
451
+ const _useMemo = useMemo(() => {
452
+ var _dates$to;
453
+ const formatted = formatDateRange(dates.from, dates.to);
454
+ let hint = "Add Date";
455
+ if (dates.from) {
456
+ hint = !dates.to || dates.to === dates.from ? formatDateLong(dates.from) : formatDateLong(dates.from) + " - " + formatDateLong(dates.to);
457
+ }
458
+ return {
459
+ value: formatted,
460
+ overdue: isOverdue((_dates$to = dates.to) != null ? _dates$to : dates.from, completed),
461
+ tooltip: hint
462
+ };
463
+ }, [completed, dates.from, dates.to]),
464
+ value = _useMemo.value,
465
+ overdue = _useMemo.overdue,
466
+ tooltip = _useMemo.tooltip;
467
+ const handleSave = useCallback(range => {
468
+ if (!range || range.from === null || range.from === undefined) {
469
+ setDates({});
470
+ return;
471
+ }
472
+ setDates({
473
+ from: Number(range.from),
474
+ to: range.to ? Number(range.to) : Number(range.from)
475
+ });
476
+ }, []);
477
+ const handleClear = useCallback(() => setDates({}), []);
478
+ const handleCalendarToggle = useCallback(next => {
479
+ if (!next) {
480
+ setPickerMounted(false);
481
+ }
482
+ }, []);
483
+
484
+ /* Completed tasks show the date read-only (mirrors Date.js) */
485
+ if (completed) {
486
+ return /*#__PURE__*/React.createElement(StyledReadOnlyCell, {
487
+ className: "col col-date"
488
+ }, value ? /*#__PURE__*/React.createElement(StyledDateValue, {
489
+ "data-tooltip": tooltip
490
+ }, /*#__PURE__*/React.createElement(Caption1, {
491
+ weight: "bold",
492
+ color: overdue ? "alert" : "secondary"
493
+ }, value)) : null);
494
+ }
495
+ if (!pickerMounted) {
496
+ return /*#__PURE__*/React.createElement("div", {
497
+ className: "col col-date"
498
+ }, /*#__PURE__*/React.createElement(DateTrigger, {
499
+ value: value,
500
+ tooltip: tooltip,
501
+ overdue: overdue,
502
+ onClick: () => setPickerMounted(true)
503
+ }));
504
+ }
505
+ return /*#__PURE__*/React.createElement(StyledPickerCell, {
506
+ className: "col col-date"
507
+ }, /*#__PURE__*/React.createElement(DatePicker, {
508
+ open: true,
509
+ mode: "custom",
510
+ instant: false,
511
+ maxRange: 366,
512
+ firstDayOfWeek: 1,
513
+ position: "bottom-end",
514
+ selected: {
515
+ from: (_dates$from = dates.from) != null ? _dates$from : null,
516
+ to: (_dates$to2 = dates.to) != null ? _dates$to2 : null
517
+ },
518
+ onSave: handleSave,
519
+ onClear: handleClear,
520
+ onCalendarToggle: handleCalendarToggle,
521
+ target: /*#__PURE__*/React.createElement(DateTrigger, {
522
+ value: value,
523
+ tooltip: tooltip,
524
+ overdue: overdue
525
+ })
526
+ }));
527
+ };
528
+
529
+ /* ================================================================== */
530
+ /* Time */
531
+ /* ================================================================== */
532
+
533
+ /** mirrors utils/utils.jsx → calculateTimeToEstimatePercentage */
534
+ const timeToEstimatePercentage = (tracked, estimated) => {
535
+ if (estimated > 0) {
536
+ return tracked > 0 ? Math.round(tracked / estimated * 100) : 0;
537
+ }
538
+ return null;
539
+ };
540
+
541
+ /** mirrors utils/utils.jsx → formatTrackedAndEstimatedTime */
542
+ const formatTrackedAndEstimated = (tracked, estimated) => estimated > 0 ? formatHours(tracked, false, false, "short") + "/" + formatHours(estimated, false, false, "short") : formatHours(tracked, false, false, "short");
543
+
544
+ /* mirrors Time.js → progressColor() */
545
+ const progressColor = percent => {
546
+ if (percent >= 100) {
547
+ return "var(--red-alert)";
548
+ }
549
+ if (percent >= 75) {
550
+ return "var(--warning)";
551
+ }
552
+ return "var(--color-sucess-green)";
553
+ };
554
+ const StyledTimeValue = styled.span.withConfig({
555
+ displayName: "columns__StyledTimeValue",
556
+ componentId: "sc-9f4385-6"
557
+ })(["display:inline-flex;align-items:center;font-family:", ";white-space:nowrap;.c-progress-pie,svg{flex-shrink:0;}.time-pie{margin-right:8px;}"], SANS);
558
+ const TimeValue = _ref8 => {
559
+ let tracked = _ref8.tracked,
560
+ estimated = _ref8.estimated;
561
+ const percent = timeToEstimatePercentage(tracked, estimated);
562
+ return /*#__PURE__*/React.createElement(StyledTimeValue, null, estimated > 0 ? /*#__PURE__*/React.createElement(ProgressPie, {
563
+ className: "time-pie",
564
+ radius: 10,
565
+ progress: percent != null ? percent : 0,
566
+ progressColor: progressColor(percent != null ? percent : 0),
567
+ backgroundColor: "var(--color-theme-300)"
568
+ }) : null, /*#__PURE__*/React.createElement(Caption1, {
569
+ weight: "bold"
570
+ }, formatTrackedAndEstimated(tracked, estimated)));
571
+ };
572
+ const TimeTrigger = _ref9 => {
573
+ let tracked = _ref9.tracked,
574
+ estimated = _ref9.estimated,
575
+ onClick = _ref9.onClick;
576
+ const hasTime = tracked > 0 || estimated > 0;
577
+ return /*#__PURE__*/React.createElement(StyledListButton, {
578
+ onClick: onClick,
579
+ className: hasTime ? "show-on-hover open" : "show-on-hover",
580
+ "aria-label": hasTime ? "Edit time" : "Add time",
581
+ "data-tooltip": hasTime ? "Edit Time" : "Add Time"
582
+ }, hasTime ? /*#__PURE__*/React.createElement(TimeValue, {
583
+ tracked: tracked,
584
+ estimated: estimated
585
+ }) : /*#__PURE__*/React.createElement(ClockAddIcon, null));
586
+ };
587
+ export const TimeCell = _ref0 => {
588
+ let taskName = _ref0.taskName,
589
+ _ref0$tracked = _ref0.tracked,
590
+ tracked = _ref0$tracked === void 0 ? 0 : _ref0$tracked,
591
+ _ref0$estimated = _ref0.estimated,
592
+ estimated = _ref0$estimated === void 0 ? 0 : _ref0$estimated,
593
+ users = _ref0.users,
594
+ currentUserId = _ref0.currentUserId,
595
+ _ref0$completed = _ref0.completed,
596
+ completed = _ref0$completed === void 0 ? false : _ref0$completed;
597
+ const _useState5 = useState(false),
598
+ open = _useState5[0],
599
+ setOpen = _useState5[1];
600
+ const _useState6 = useState(() => {
601
+ var _users$find$name, _users$find;
602
+ return tracked > 0 ? [{
603
+ id: 1,
604
+ date: dayOffset(-3),
605
+ userId: currentUserId,
606
+ userName: (_users$find$name = (_users$find = users.find(user => user.id === currentUserId)) == null ? void 0 : _users$find.name) != null ? _users$find$name : "You",
607
+ jobType: "Design",
608
+ hours: tracked,
609
+ billable: true
610
+ }] : [];
611
+ }),
612
+ records = _useState6[0],
613
+ setRecords = _useState6[1];
614
+ const total = useMemo(() => records.reduce((sum, record) => sum + record.hours, 0), [records]);
615
+ const handleAdd = useCallback(record => {
616
+ setRecords(prev => [_extends({}, record, {
617
+ id: prev.length + 1
618
+ }), ...prev]);
619
+ setOpen(false);
620
+ }, []);
621
+
622
+ /* Completed tasks show the value without the "add" affordance (mirrors
623
+ Time.js passing hideIcon for completed tasks) */
624
+ if (completed) {
625
+ return /*#__PURE__*/React.createElement(StyledReadOnlyCell, {
626
+ className: "col col-time"
627
+ }, total > 0 || estimated > 0 ? /*#__PURE__*/React.createElement(TimeValue, {
628
+ tracked: total,
629
+ estimated: estimated
630
+ }) : null);
631
+ }
632
+ return /*#__PURE__*/React.createElement("div", {
633
+ className: "col col-time"
634
+ }, /*#__PURE__*/React.createElement(TimeTrigger, {
635
+ tracked: total,
636
+ estimated: estimated,
637
+ onClick: () => setOpen(true)
638
+ }), open ? /*#__PURE__*/React.createElement(TimeRecordDialog, {
639
+ open: open,
640
+ taskName: taskName,
641
+ users: users,
642
+ currentUserId: currentUserId,
643
+ records: records,
644
+ total: total,
645
+ onSubmit: handleAdd,
646
+ onClose: () => setOpen(false)
647
+ }) : null);
648
+ };
649
+
650
+ /* ------------------------------------------------------------------ */
651
+ /* Add Time Record dialog (mirrors TimeRecordForm/*) */
652
+ /* ------------------------------------------------------------------ */
653
+
654
+ const JOB_TYPES = [{
655
+ id: "design",
656
+ name: "Design"
657
+ }, {
658
+ id: "development",
659
+ name: "Development"
660
+ }, {
661
+ id: "devops",
662
+ name: "Devops"
663
+ }, {
664
+ id: "consulting",
665
+ name: "Consulting"
666
+ }, {
667
+ id: "qa",
668
+ name: "QA"
669
+ }];
670
+
671
+ /* The dialog paper — the app builds this one by hand rather than with DS
672
+ `Dialog` because it is 770px wide (`.time_record_form_modal_wrapper`). */
673
+ const StyledTimeRecordPaper = styled.div.withConfig({
674
+ displayName: "columns__StyledTimeRecordPaper",
675
+ componentId: "sc-9f4385-7"
676
+ })(["position:relative;width:770px;max-width:95vw;max-height:calc(100vh - 156px);margin:126px auto 30px auto;padding:20px 30px;overflow:auto;border-radius:8px;background-color:var(--page-paper-main);color:var(--color-theme-900);box-shadow:var(--shadow-primary);box-sizing:border-box;font-family:", ";*{box-sizing:border-box;}@media (max-width:830px){width:calc(100vw - 60px);}.trd-header{display:flex;align-items:center;justify-content:space-between;}.trd-title{overflow:hidden;white-space:nowrap;text-overflow:ellipsis;margin-right:16px;font-size:1rem;font-weight:700;}.trd-date button{width:auto;}.trd-date > div{text-align:right;}.trd-divider{margin:20px 0 32px 0;border-bottom:1px solid var(--border-primary);height:1px;}.trd-body{display:flex;justify-content:space-between;position:relative;}.trd-form{width:100%;}.trd-fields{display:flex;align-items:flex-end;}.trd-hours{width:144px;margin-right:16px;flex-shrink:0;}.trd-hours .c-input-wrapper,.trd-hours input{width:100%;}.trd-field{margin-right:16px;}.trd-field:last-of-type{margin-right:20px;}.trd-field .c-label{display:block;margin-bottom:2px;}.trd-field button{width:144px;}.trd-billable{display:flex;align-items:center;margin-top:24px;}.trd-billable .c-label{margin-left:8px;cursor:pointer;}.trd-description-trigger{display:inline-flex;align-items:center;margin-top:16px;color:var(--color-secondary);font-size:14px;font-weight:500;background:none;border:none;padding:0;cursor:pointer;font-family:", ";}.trd-description-trigger svg{margin-right:4px;fill:var(--color-primary);transition:transform 200ms ease;}.trd-description-trigger.collapsed svg{transform:rotate(180deg);}.trd-textarea{width:360px;max-width:100%;margin-top:8px;font-size:12px;resize:none;}.trd-actions{display:flex;margin-top:24px;}.trd-actions .c-button:first-child{margin-right:12px;}.trd-total{position:absolute;right:0;bottom:0;display:inline-flex;align-items:center;align-self:flex-end;background:none;border:none;padding:0;cursor:pointer;}.trd-total svg{margin-right:4px;fill:var(--color-primary);transition:transform 200ms ease;}.trd-total.collapsed svg{transform:rotate(180deg);}.trd-records{margin-top:32px;border-top:1px solid var(--border-primary);}.trd-record{display:flex;align-items:center;justify-content:space-between;height:36px;border-bottom:1px solid var(--color-theme-300);}.trd-record-left{display:flex;align-items:center;gap:12px;min-width:0;}"], SANS, SANS);
677
+ const TimeRecordDialog = _ref1 => {
678
+ let open = _ref1.open,
679
+ taskName = _ref1.taskName,
680
+ users = _ref1.users,
681
+ currentUserId = _ref1.currentUserId,
682
+ records = _ref1.records,
683
+ total = _ref1.total,
684
+ onSubmit = _ref1.onSubmit,
685
+ onClose = _ref1.onClose;
686
+ const _useState7 = useState(""),
687
+ value = _useState7[0],
688
+ setValue = _useState7[1];
689
+ const _useState8 = useState(todayUnix()),
690
+ date = _useState8[0],
691
+ setDate = _useState8[1];
692
+ const _useState9 = useState(currentUserId),
693
+ userId = _useState9[0],
694
+ setUserId = _useState9[1];
695
+ const _useState0 = useState("devops"),
696
+ jobTypeId = _useState0[0],
697
+ setJobTypeId = _useState0[1];
698
+ const _useState1 = useState(false),
699
+ billable = _useState1[0],
700
+ setBillable = _useState1[1];
701
+ const _useState10 = useState(true),
702
+ showDescription = _useState10[0],
703
+ setShowDescription = _useState10[1];
704
+ const _useState11 = useState(""),
705
+ description = _useState11[0],
706
+ setDescription = _useState11[1];
707
+ const _useState12 = useState(false),
708
+ showRecords = _useState12[0],
709
+ setShowRecords = _useState12[1];
710
+ const user = users.find(item => item.id === userId);
711
+ const jobType = JOB_TYPES.find(item => item.id === jobTypeId);
712
+
713
+ /** "1:30" / "1.5" → decimal hours */
714
+ const hours = useMemo(() => {
715
+ const trimmed = value.trim();
716
+ if (!trimmed) {
717
+ return 0;
718
+ }
719
+ if (trimmed.includes(":")) {
720
+ const _trimmed$split = trimmed.split(":"),
721
+ h = _trimmed$split[0],
722
+ m = _trimmed$split[1];
723
+ return Number(h || 0) + Number(m || 0) / 60;
724
+ }
725
+ return Number(trimmed) || 0;
726
+ }, [value]);
727
+ const handleSubmit = useCallback(() => {
728
+ var _user$name, _jobType$name;
729
+ onSubmit({
730
+ date,
731
+ userId,
732
+ userName: (_user$name = user == null ? void 0 : user.name) != null ? _user$name : "",
733
+ jobType: (_jobType$name = jobType == null ? void 0 : jobType.name) != null ? _jobType$name : "",
734
+ hours,
735
+ billable,
736
+ description
737
+ });
738
+ }, [billable, date, description, hours, jobType, onSubmit, user, userId]);
739
+ return /*#__PURE__*/React.createElement(Modal, {
740
+ open: open,
741
+ onClose: onClose
742
+ }, /*#__PURE__*/React.createElement(SlideFromTop, {
743
+ in: open
744
+ }, /*#__PURE__*/React.createElement(StyledTimeRecordPaper, {
745
+ role: "dialog",
746
+ "aria-label": "Add time record"
747
+ }, /*#__PURE__*/React.createElement("div", {
748
+ className: "trd-header"
749
+ }, /*#__PURE__*/React.createElement("span", {
750
+ className: "trd-title"
751
+ }, taskName), /*#__PURE__*/React.createElement("span", {
752
+ className: "trd-date"
753
+ }, /*#__PURE__*/React.createElement(DatePicker, {
754
+ mode: "daily",
755
+ required: true,
756
+ firstDayOfWeek: 1,
757
+ position: "bottom-end",
758
+ selected: {
759
+ from: date,
760
+ to: date
761
+ },
762
+ onSave: range => range != null && range.from ? setDate(Number(range.from)) : undefined,
763
+ target: /*#__PURE__*/React.createElement(SelectTrigger, null, formatDateRange(date, date))
764
+ }))), /*#__PURE__*/React.createElement("div", {
765
+ className: "trd-divider"
766
+ }), /*#__PURE__*/React.createElement("div", {
767
+ className: "trd-body"
768
+ }, /*#__PURE__*/React.createElement("div", {
769
+ className: "trd-form"
770
+ }, /*#__PURE__*/React.createElement("div", {
771
+ className: "trd-fields"
772
+ }, /*#__PURE__*/React.createElement("span", {
773
+ className: "trd-hours"
774
+ }, /*#__PURE__*/React.createElement(InputHours, {
775
+ autoFocus: true,
776
+ allowEmptyValue: true,
777
+ size: "biggest",
778
+ placeholder: "00:00",
779
+ value: value,
780
+ onChange: setValue
781
+ })), /*#__PURE__*/React.createElement("span", {
782
+ className: "trd-field"
783
+ }, /*#__PURE__*/React.createElement(Label, {
784
+ size: "small"
785
+ }, "User"), /*#__PURE__*/React.createElement(Select, {
786
+ options: users,
787
+ selected: userId,
788
+ forceCloseMenu: true,
789
+ onChange: next => setUserId(String(next)),
790
+ target: /*#__PURE__*/React.createElement(SelectTrigger, {
791
+ id: "trd-user"
792
+ }, user == null ? void 0 : user.name)
793
+ })), /*#__PURE__*/React.createElement("span", {
794
+ className: "trd-field"
795
+ }, /*#__PURE__*/React.createElement(Label, {
796
+ size: "small"
797
+ }, "Job Type"), /*#__PURE__*/React.createElement(Select, {
798
+ options: JOB_TYPES,
799
+ selected: jobTypeId,
800
+ forceCloseMenu: true,
801
+ onChange: next => setJobTypeId(String(next)),
802
+ target: /*#__PURE__*/React.createElement(SelectTrigger, {
803
+ id: "trd-job-type"
804
+ }, jobType == null ? void 0 : jobType.name)
805
+ })), /*#__PURE__*/React.createElement("span", {
806
+ className: "trd-billable"
807
+ }, /*#__PURE__*/React.createElement(Checkbox, {
808
+ id: "trd-billable",
809
+ checked: billable,
810
+ onChange: () => setBillable(prev => !prev)
811
+ }), /*#__PURE__*/React.createElement(Label, {
812
+ htmlFor: "trd-billable",
813
+ weight: "regular"
814
+ }, "Billable"))), /*#__PURE__*/React.createElement("button", {
815
+ type: "button",
816
+ className: showDescription ? "trd-description-trigger" : "trd-description-trigger collapsed",
817
+ onClick: () => setShowDescription(prev => !prev)
818
+ }, /*#__PURE__*/React.createElement(CollapseExpandSingleIcon, null), /*#__PURE__*/React.createElement("span", null, "Description")), showDescription ? /*#__PURE__*/React.createElement("div", null, /*#__PURE__*/React.createElement(Textarea, {
819
+ className: "trd-textarea",
820
+ value: description,
821
+ onChange: event => setDescription(event.target.value)
822
+ })) : null, /*#__PURE__*/React.createElement("div", {
823
+ className: "trd-actions"
824
+ }, /*#__PURE__*/React.createElement(Button, {
825
+ variant: "primary",
826
+ disabled: hours <= 0,
827
+ onClick: handleSubmit
828
+ }, "Add Time Record"), /*#__PURE__*/React.createElement(Button, {
829
+ variant: "secondary",
830
+ onClick: onClose
831
+ }, "Cancel"))), total > 0 ? /*#__PURE__*/React.createElement("button", {
832
+ type: "button",
833
+ className: showRecords ? "trd-total" : "trd-total collapsed",
834
+ onClick: () => setShowRecords(prev => !prev)
835
+ }, /*#__PURE__*/React.createElement(CollapseExpandSingleIcon, null), /*#__PURE__*/React.createElement(Header3, {
836
+ color: "theme-secondary",
837
+ tabularNums: true
838
+ }, "Total: ", formatHours(total, false, true), "h")) : null), showRecords && records.length > 0 ? /*#__PURE__*/React.createElement("div", {
839
+ className: "trd-records"
840
+ }, records.map(record => /*#__PURE__*/React.createElement("div", {
841
+ key: record.id,
842
+ className: "trd-record"
843
+ }, /*#__PURE__*/React.createElement("span", {
844
+ className: "trd-record-left"
845
+ }, /*#__PURE__*/React.createElement(Caption1, {
846
+ weight: "bold",
847
+ color: "secondary"
848
+ }, formatDateRange(record.date, record.date)), /*#__PURE__*/React.createElement(Body2, null, record.userName), /*#__PURE__*/React.createElement(Caption1, {
849
+ color: "secondary"
850
+ }, record.jobType)), /*#__PURE__*/React.createElement(Body2, {
851
+ weight: "bold",
852
+ tabularNums: true
853
+ }, formatHours(record.hours), "h")))) : null)));
854
+ };
855
+ //# sourceMappingURL=columns.js.map