@activecollab/components 2.0.408 → 2.0.409

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.
Files changed (83) hide show
  1. package/dist/cjs/components/Icons/collection/Project.js +98 -0
  2. package/dist/cjs/components/Icons/collection/Project.js.map +1 -0
  3. package/dist/cjs/components/Icons/collection/Task.js +61 -0
  4. package/dist/cjs/components/Icons/collection/Task.js.map +1 -0
  5. package/dist/cjs/components/Icons/collection/index.js +14 -0
  6. package/dist/cjs/components/Icons/collection/index.js.map +1 -1
  7. package/dist/cjs/presentation/stackedCard/content/NoteCard.js +7 -3
  8. package/dist/cjs/presentation/stackedCard/content/NoteCard.js.map +1 -1
  9. package/dist/cjs/presentation/stackedCard/content/ProjectCard.js +136 -0
  10. package/dist/cjs/presentation/stackedCard/content/ProjectCard.js.map +1 -0
  11. package/dist/cjs/presentation/stackedCard/content/TaskCard.js +183 -56
  12. package/dist/cjs/presentation/stackedCard/content/TaskCard.js.map +1 -1
  13. package/dist/cjs/presentation/stackedCard/content/index.js +11 -0
  14. package/dist/cjs/presentation/stackedCard/content/index.js.map +1 -1
  15. package/dist/cjs/presentation/stackedCard/content/shared.js +66 -19
  16. package/dist/cjs/presentation/stackedCard/content/shared.js.map +1 -1
  17. package/dist/cjs/presentation/whiteboard/bottomDock.js +95 -32
  18. package/dist/cjs/presentation/whiteboard/bottomDock.js.map +1 -1
  19. package/dist/cjs/presentation/whiteboard/canvasElements.js +196 -8
  20. package/dist/cjs/presentation/whiteboard/canvasElements.js.map +1 -1
  21. package/dist/cjs/presentation/whiteboard/inspector.js +149 -47
  22. package/dist/cjs/presentation/whiteboard/inspector.js.map +1 -1
  23. package/dist/cjs/presentation/whiteboard/outline.js +91 -18
  24. package/dist/cjs/presentation/whiteboard/outline.js.map +1 -1
  25. package/dist/cjs/presentation/whiteboard/projectElements.js +883 -0
  26. package/dist/cjs/presentation/whiteboard/projectElements.js.map +1 -0
  27. package/dist/esm/components/Icons/collection/Project.d.ts +36 -0
  28. package/dist/esm/components/Icons/collection/Project.d.ts.map +1 -0
  29. package/dist/esm/components/Icons/collection/Project.js +91 -0
  30. package/dist/esm/components/Icons/collection/Project.js.map +1 -0
  31. package/dist/esm/components/Icons/collection/Task.d.ts +32 -0
  32. package/dist/esm/components/Icons/collection/Task.d.ts.map +1 -0
  33. package/dist/esm/components/Icons/collection/Task.js +54 -0
  34. package/dist/esm/components/Icons/collection/Task.js.map +1 -0
  35. package/dist/esm/components/Icons/collection/index.d.ts +2 -0
  36. package/dist/esm/components/Icons/collection/index.d.ts.map +1 -1
  37. package/dist/esm/components/Icons/collection/index.js +2 -0
  38. package/dist/esm/components/Icons/collection/index.js.map +1 -1
  39. package/dist/esm/presentation/stackedCard/content/NoteCard.d.ts +8 -1
  40. package/dist/esm/presentation/stackedCard/content/NoteCard.d.ts.map +1 -1
  41. package/dist/esm/presentation/stackedCard/content/NoteCard.js +8 -4
  42. package/dist/esm/presentation/stackedCard/content/NoteCard.js.map +1 -1
  43. package/dist/esm/presentation/stackedCard/content/ProjectCard.d.ts +64 -0
  44. package/dist/esm/presentation/stackedCard/content/ProjectCard.d.ts.map +1 -0
  45. package/dist/esm/presentation/stackedCard/content/ProjectCard.js +128 -0
  46. package/dist/esm/presentation/stackedCard/content/ProjectCard.js.map +1 -0
  47. package/dist/esm/presentation/stackedCard/content/TaskCard.d.ts +64 -2
  48. package/dist/esm/presentation/stackedCard/content/TaskCard.d.ts.map +1 -1
  49. package/dist/esm/presentation/stackedCard/content/TaskCard.js +181 -58
  50. package/dist/esm/presentation/stackedCard/content/TaskCard.js.map +1 -1
  51. package/dist/esm/presentation/stackedCard/content/index.d.ts +1 -0
  52. package/dist/esm/presentation/stackedCard/content/index.d.ts.map +1 -1
  53. package/dist/esm/presentation/stackedCard/content/index.js +1 -0
  54. package/dist/esm/presentation/stackedCard/content/index.js.map +1 -1
  55. package/dist/esm/presentation/stackedCard/content/shared.d.ts +30 -0
  56. package/dist/esm/presentation/stackedCard/content/shared.d.ts.map +1 -1
  57. package/dist/esm/presentation/stackedCard/content/shared.js +66 -19
  58. package/dist/esm/presentation/stackedCard/content/shared.js.map +1 -1
  59. package/dist/esm/presentation/whiteboard/bottomDock.d.ts +30 -7
  60. package/dist/esm/presentation/whiteboard/bottomDock.d.ts.map +1 -1
  61. package/dist/esm/presentation/whiteboard/bottomDock.js +79 -32
  62. package/dist/esm/presentation/whiteboard/bottomDock.js.map +1 -1
  63. package/dist/esm/presentation/whiteboard/canvasElements.d.ts +4 -1
  64. package/dist/esm/presentation/whiteboard/canvasElements.d.ts.map +1 -1
  65. package/dist/esm/presentation/whiteboard/canvasElements.js +158 -7
  66. package/dist/esm/presentation/whiteboard/canvasElements.js.map +1 -1
  67. package/dist/esm/presentation/whiteboard/inspector.d.ts +35 -1
  68. package/dist/esm/presentation/whiteboard/inspector.d.ts.map +1 -1
  69. package/dist/esm/presentation/whiteboard/inspector.js +170 -68
  70. package/dist/esm/presentation/whiteboard/inspector.js.map +1 -1
  71. package/dist/esm/presentation/whiteboard/outline.d.ts +23 -3
  72. package/dist/esm/presentation/whiteboard/outline.d.ts.map +1 -1
  73. package/dist/esm/presentation/whiteboard/outline.js +89 -18
  74. package/dist/esm/presentation/whiteboard/outline.js.map +1 -1
  75. package/dist/esm/presentation/whiteboard/projectElements.d.ts +267 -0
  76. package/dist/esm/presentation/whiteboard/projectElements.d.ts.map +1 -0
  77. package/dist/esm/presentation/whiteboard/projectElements.js +824 -0
  78. package/dist/esm/presentation/whiteboard/projectElements.js.map +1 -0
  79. package/dist/index.js +629 -490
  80. package/dist/index.js.map +1 -1
  81. package/dist/index.min.js +1 -1
  82. package/dist/index.min.js.map +1 -1
  83. package/package.json +1 -1
@@ -0,0 +1,824 @@
1
+ import _extends from "@babel/runtime/helpers/esm/extends";
2
+ import React, { useMemo, useRef, useState } from "react";
3
+ import styled from "styled-components";
4
+ import { SELECTION_BLUE, StyledCanvasElement } from "./elements";
5
+ import { IconButton } from "../../components/IconButton";
6
+ import { ArrowLeftIcon, SortGeneralIcon } from "../../components/Icons";
7
+ import { Input } from "../../components/Input";
8
+ import { SkeletonLoader } from "../../components/Loaders";
9
+ import { Modal } from "../../components/Modal";
10
+ import { Paper } from "../../components/Paper";
11
+ import { RadioButton } from "../../components/RadioButton";
12
+ import { StackedCard } from "../../components/StackedCard";
13
+ import { Tooltip } from "../../components/Tooltip";
14
+ import { Body2, Caption1 } from "../../components/Typography";
15
+ import { AVATAR_COLORS } from "../shared";
16
+ import { INTERNAL_CLIENT_LABEL as CONTENT_INTERNAL_CLIENT_LABEL, ProjectCard, TaskCard } from "../stackedCard/content";
17
+
18
+ /* ------------------------------------------------------------------ */
19
+ /* Project elements — placeholder → picker → resolved card */
20
+ /* ------------------------------------------------------------------ */
21
+
22
+ /**
23
+ * The board's third "draw a box, then say what goes in it" element, after the
24
+ * link and the file card. The gesture ladder is deliberately the same one those
25
+ * two already established, because it is the only one the dock teaches:
26
+ *
27
+ * armed the Project Elements dock button opens a submenu (Project /
28
+ * Task); picking one arms the tool and puts the canvas in
29
+ * crosshair mode.
30
+ * drawn mouse down → drag → up leaves a dashed PLACEHOLDER at the drawn
31
+ * box (clamped to a card-sized minimum), exactly where the real
32
+ * card will sit.
33
+ * picking the release opens the picker — a centred popover replicating the
34
+ * app's `MenuSelector/ProjectList` (and, for tasks, the two-step
35
+ * `ProjectTaskMenuSelector`: projects, then that project's tasks).
36
+ * resolving a short skeleton flash in the placeholder's own box, standing in
37
+ * for "the board is fetching this record's details".
38
+ * resolved the placeholder becomes the real card, anchored at the
39
+ * placeholder's top-left and grown to the card's natural size.
40
+ *
41
+ * Escape or a click on the overlay cancels: the picker closes and the
42
+ * placeholder is thrown away, so a cancelled gesture leaves no debris.
43
+ */
44
+
45
+ /* A drawn box smaller than this reads as "I just want a card here", so the
46
+ placeholder takes the minimum rather than the (unusable) drawn size. */
47
+ export const PLACEHOLDER_MIN_WIDTH = 260;
48
+ export const PLACEHOLDER_MIN_HEIGHT = 120;
49
+
50
+ /* The natural width each resolved card grows to. Heights follow the content,
51
+ like every other card on this board. */
52
+ export const PROJECT_CARD_WIDTH = 300;
53
+ export const TASK_CARD_WIDTH = 260;
54
+
55
+ /* How long the "details are being resolved from the backend" flash lasts. */
56
+ export const RESOLVE_MS = 480;
57
+ /** True for the two board tools this module owns. */
58
+ export const isProjectTool = tool => tool === "project" || tool === "task";
59
+
60
+ /* ------------------------------------------------------------------ */
61
+ /* Mock data */
62
+ /* ------------------------------------------------------------------ */
63
+
64
+ /**
65
+ * The people and labels the board carries are exactly what the shared content
66
+ * cards read, so the picker's types ARE the cards' types — one shape, so a card
67
+ * cannot be fed something it does not understand.
68
+ */
69
+
70
+ /**
71
+ * A project label. The app renders it as a FILLED pill whose background is the
72
+ * label's own colour and whose text is a darker shade of it
73
+ * (`ProjectLabel.jsx`: `EntityLabel` with `backgroundColor: label.color` and
74
+ * `color: label.darker_text_color`), which is the design system's `Chip` — not
75
+ * the colour-text `Tag` the column cards use. Both colours are data. The shape
76
+ * is the shared `ProjectCard`'s own.
77
+ */
78
+
79
+ /**
80
+ * What an internal project's client line says. The word belongs to the card (it
81
+ * is the card that has to decide what to print for a project with no client), so
82
+ * the board re-exports the card's own constant rather than keeping a second copy
83
+ * that could drift.
84
+ */
85
+ export const INTERNAL_CLIENT_LABEL = CONTENT_INTERNAL_CLIENT_LABEL;
86
+
87
+ /* One rule for both the card's client line and the picker's group header, so a
88
+ project can never be filed under one name and labelled with another. */
89
+ export const projectClientLabel = project => {
90
+ var _project$client;
91
+ return (_project$client = project.client) != null ? _project$client : INTERNAL_CLIENT_LABEL;
92
+ };
93
+ /* Avatar colours are DATA, not theme — they stand in for real user avatars. */
94
+ export const PICKER_PEOPLE = {
95
+ jasmina: {
96
+ name: "Jasmina Sekulić",
97
+ initials: "JS",
98
+ color: AVATAR_COLORS.purple
99
+ },
100
+ marko: {
101
+ name: "Marko Antić",
102
+ initials: "MA",
103
+ color: AVATAR_COLORS.teal
104
+ },
105
+ dina: {
106
+ name: "Dina Kovač",
107
+ initials: "DK",
108
+ color: AVATAR_COLORS.orange
109
+ },
110
+ elena: {
111
+ name: "Elena Petrović",
112
+ initials: "EP",
113
+ color: AVATAR_COLORS.yellow
114
+ },
115
+ tomas: {
116
+ name: "Tomas Slavik",
117
+ initials: "TS",
118
+ color: AVATAR_COLORS.blue
119
+ }
120
+ };
121
+
122
+ /* Label colours are data too: they are the label's own colour in the app. */
123
+ export const PICKER_LABELS = {
124
+ assigned: {
125
+ text: "ASSIGNED",
126
+ color: "#E08B8B"
127
+ },
128
+ blocked: {
129
+ text: "BLOCKED",
130
+ color: "#D9534F"
131
+ },
132
+ inProgress: {
133
+ text: "IN PROGRESS",
134
+ color: "#9BA0A8"
135
+ },
136
+ review: {
137
+ text: "REVIEW",
138
+ color: "#D4A017"
139
+ }
140
+ };
141
+
142
+ /* Project labels carry a background AND a darker text colour, the pair the app
143
+ stores as `color` / `darker_text_color`. Uppercase because that is how the
144
+ app saves a new project label (ProjectLabel.jsx upper-cases the name). */
145
+ export const PICKER_PROJECT_LABELS = {
146
+ client: {
147
+ text: "CLIENT WORK",
148
+ color: "#FBBB75",
149
+ textColor: "#6B4416"
150
+ },
151
+ internal: {
152
+ text: "INTERNAL",
153
+ color: "#B7D8B0",
154
+ textColor: "#2F5B28"
155
+ },
156
+ urgent: {
157
+ text: "URGENT",
158
+ color: "#F3A7A7",
159
+ textColor: "#7A2222"
160
+ },
161
+ research: {
162
+ text: "RESEARCH",
163
+ color: "#B8CDEB",
164
+ textColor: "#274974"
165
+ }
166
+ };
167
+
168
+ /* A compact constructor so every mocked task stays one readable row. */
169
+ const mockTask = function (number, name, taskList, extra) {
170
+ if (extra === void 0) {
171
+ extra = {};
172
+ }
173
+ return _extends({
174
+ id: "task-" + number,
175
+ number,
176
+ name,
177
+ taskList,
178
+ assignees: [PICKER_PEOPLE.elena],
179
+ date: "16. Jul"
180
+ }, extra);
181
+ };
182
+ export const PICKER_PROJECTS = [{
183
+ id: "p-ama",
184
+ name: "AMA Webinar",
185
+ client: "ActiveCollab Inc.",
186
+ description: "Quarterly live session with the community. Collect questions up front, keep the run of show to forty minutes, and publish the recap the same week.",
187
+ lead: PICKER_PEOPLE.jasmina,
188
+ label: PICKER_PROJECT_LABELS.client,
189
+ tasks: [mockTask(184, "Draft the run of show", "Preparation", {
190
+ subtasks: 3
191
+ }), mockTask(185, "Collect questions from the community", "Preparation", {
192
+ label: PICKER_LABELS.assigned,
193
+ comments: 4
194
+ }), mockTask(186, "Record the intro segment", "Production", {
195
+ assignees: [PICKER_PEOPLE.marko]
196
+ }), mockTask(187, "Publish the recap post", "Follow-up", {
197
+ label: PICKER_LABELS.review,
198
+ date: "24. Jul"
199
+ })]
200
+ }, {
201
+ id: "p-care",
202
+ /* Internal project WITH a description and a leader, but no label. */
203
+ name: "Customer Care Team",
204
+ client: null,
205
+ description: "Where the support team plans its week. Macros, escalation paths and the weekly churn review all live here.",
206
+ lead: PICKER_PEOPLE.dina,
207
+ tasks: [mockTask(201, "Rewrite the macro replies", "Inbox", {
208
+ label: PICKER_LABELS.inProgress,
209
+ comments: 2
210
+ }), mockTask(202, "Weekly churn review", "Reporting", {
211
+ subtasks: 5
212
+ }), mockTask(203, "Escalation path for billing", "Inbox", {
213
+ assignees: [PICKER_PEOPLE.tomas]
214
+ }), mockTask(204, "Onboarding call script", "Reporting", {
215
+ date: "2. Aug"
216
+ })]
217
+ }, {
218
+ id: "p-backlog",
219
+ /* No description and NO leader — the meta row is the label alone, pushed
220
+ to the right of an otherwise empty row. */
221
+ name: "Design Backlog",
222
+ client: null,
223
+ label: PICKER_PROJECT_LABELS.internal,
224
+ tasks: [mockTask(311, "Whiteboard element inspector", "Ideas", {
225
+ label: PICKER_LABELS.assigned,
226
+ subtasks: 2,
227
+ comments: 3
228
+ }), mockTask(312, "Empty states for the board list", "Ideas"), mockTask(313, "Dark theme audit of the dock", "Ready", {
229
+ label: PICKER_LABELS.blocked,
230
+ assignees: [PICKER_PEOPLE.jasmina, PICKER_PEOPLE.marko]
231
+ }), mockTask(314, "Icon set for board elements", "Ready", {
232
+ date: "9. Aug"
233
+ })]
234
+ }, {
235
+ id: "p-design-team",
236
+ /* Leader only: no description, no label. */
237
+ name: "Design Team",
238
+ client: null,
239
+ lead: PICKER_PEOPLE.elena,
240
+ tasks: [mockTask(402, "Design system spring cleanup", "This Sprint", {
241
+ subtasks: 8
242
+ }), mockTask(403, "Retro board template", "This Sprint", {
243
+ label: PICKER_LABELS.inProgress,
244
+ assignees: [PICKER_PEOPLE.dina]
245
+ }), mockTask(404, "Handoff checklist", "Next Sprint", {
246
+ comments: 1
247
+ }), mockTask(405, "Component audit: cards", "Next Sprint", {
248
+ date: "30. Jul"
249
+ })]
250
+ }, {
251
+ id: "p-foo",
252
+ /* The bare minimum a project card can be: name and client line only. No
253
+ description, no leader, no label — so no meta row at all. */
254
+ name: "Foo Kanban",
255
+ client: null,
256
+ tasks: [mockTask(77, "Set up the columns", "Task List", {
257
+ subtasks: 1
258
+ }), mockTask(78, "Import the old cards", "Task List", {
259
+ label: PICKER_LABELS.blocked
260
+ }), mockTask(79, "Invite the team", "Task List 2", {
261
+ assignees: [PICKER_PEOPLE.marko]
262
+ }), mockTask(80, "Archive stale swimlanes", "Task List 2", {
263
+ date: "5. Aug"
264
+ })]
265
+ }, {
266
+ id: "p-marketing",
267
+ name: "Marketing Site",
268
+ client: "ActiveCollab Inc.",
269
+ description: "Pricing, case studies and the homepage. Copy passes go through content, performance budgets through engineering.",
270
+ lead: PICKER_PEOPLE.marko,
271
+ label: PICKER_PROJECT_LABELS.urgent,
272
+ tasks: [mockTask(510, "Pricing page copy pass", "Content", {
273
+ label: PICKER_LABELS.review,
274
+ comments: 6
275
+ }), mockTask(511, "Case study: Bright Lane", "Content", {
276
+ subtasks: 4
277
+ }), mockTask(512, "Lighthouse budget for the homepage", "Engineering", {
278
+ assignees: [PICKER_PEOPLE.tomas]
279
+ }), mockTask(513, "Cookie banner rework", "Engineering", {
280
+ date: "11. Aug"
281
+ })]
282
+ }, {
283
+ id: "p-research",
284
+ name: "Product Research",
285
+ client: null,
286
+ description: "Interviews, synthesis and concept tests for the board work. Findings are shared with design at the end of each round.",
287
+ lead: PICKER_PEOPLE.jasmina,
288
+ label: PICKER_PROJECT_LABELS.research,
289
+ tasks: [mockTask(620, "Interview script for board users", "Discovery", {
290
+ subtasks: 3
291
+ }), mockTask(621, "Synthesis of the June calls", "Discovery", {
292
+ label: PICKER_LABELS.inProgress,
293
+ comments: 2
294
+ }), mockTask(622, "Concept test: project cards", "Validation", {
295
+ assignees: [PICKER_PEOPLE.elena, PICKER_PEOPLE.dina]
296
+ }), mockTask(623, "Share findings with design", "Validation", {
297
+ date: "19. Aug"
298
+ })]
299
+ }, {
300
+ id: "p-brand",
301
+ name: "Brand Refresh",
302
+ client: "Bright Lane Studio",
303
+ description: "Identity work: logo lockups, a typography scale and the photography direction, collected into a brand book.",
304
+ lead: PICKER_PEOPLE.dina,
305
+ label: PICKER_PROJECT_LABELS.client,
306
+ tasks: [mockTask(701, "Logo lockup variations", "Identity", {
307
+ subtasks: 6
308
+ }), mockTask(702, "Typography scale", "Identity", {
309
+ label: PICKER_LABELS.assigned
310
+ }), mockTask(703, "Photography direction", "Guidelines", {
311
+ comments: 3
312
+ }), mockTask(704, "Brand book layout", "Guidelines", {
313
+ date: "28. Jul"
314
+ })]
315
+ }, {
316
+ id: "p-portal",
317
+ /* Leader AND label, but no description. */
318
+ name: "Client Portal",
319
+ client: "Bright Lane Studio",
320
+ lead: PICKER_PEOPLE.tomas,
321
+ label: PICKER_PROJECT_LABELS.urgent,
322
+ tasks: [mockTask(810, "Invoice list pagination", "Backend", {
323
+ label: PICKER_LABELS.blocked,
324
+ subtasks: 2
325
+ }), mockTask(811, "Single sign-on for clients", "Backend", {
326
+ comments: 5
327
+ }), mockTask(812, "Dashboard tiles", "Frontend", {
328
+ assignees: [PICKER_PEOPLE.jasmina]
329
+ }), mockTask(813, "Mobile navigation", "Frontend", {
330
+ date: "3. Sep"
331
+ })]
332
+ }];
333
+
334
+ /* The picker groups projects by client and tasks by task list, exactly the way
335
+ `ProjectList` / `TaskList` group them in the app. Order is the data's own
336
+ order — the first project of a client decides where its group sits. Internal
337
+ projects group under the same word their card shows, so the header and the
338
+ card cannot say different things about the same project. */
339
+ export const groupProjects = projects => projects.reduce((groups, project) => {
340
+ const client = projectClientLabel(project);
341
+ const group = groups.find(entry => entry.client === client);
342
+ if (group) {
343
+ group.projects.push(project);
344
+ return groups;
345
+ }
346
+ return [...groups, {
347
+ client,
348
+ projects: [project]
349
+ }];
350
+ }, []);
351
+ export const groupTasks = tasks => tasks.reduce((groups, task) => {
352
+ const group = groups.find(entry => entry.taskList === task.taskList);
353
+ if (group) {
354
+ group.tasks.push(task);
355
+ return groups;
356
+ }
357
+ return [...groups, {
358
+ taskList: task.taskList,
359
+ tasks: [task]
360
+ }];
361
+ }, []);
362
+ const matchesQuery = (value, query) => value.toLowerCase().includes(query.trim().toLowerCase());
363
+
364
+ /** `#123: Name` — `utils/taskDisplayName`, the app's own task display name. */
365
+ export const taskDisplayName = task => "#" + task.number + ": " + task.name;
366
+
367
+ /* ------------------------------------------------------------------ */
368
+ /* Limited access */
369
+ /* ------------------------------------------------------------------ */
370
+
371
+ /**
372
+ * A board is shared with people who do not all see the same projects. When a
373
+ * card points at a task or project the current viewer cannot open, the board
374
+ * cannot just drop it: the element is part of everyone else's layout, and a
375
+ * hole would be a worse lie than a blank. So the card stays and says exactly
376
+ * one thing — something is here, and it is not yours to see.
377
+ *
378
+ * This follows the app's existing limited-access language (Workload):
379
+ * - the wording is "Limited Access", the phrase `EntityPlaceholder`,
380
+ * `WorkloadCalendarEvent`, Expenses and Timelist all already use;
381
+ * - nothing else is rendered. Workload's own limited cell
382
+ * (`.workload_null_project_cell`) shows an aggregate and no name, and the
383
+ * shape it is fed is literally `{ hoursOccupied, leftOffset }` — a quantity
384
+ * and a position, never an identity;
385
+ * - the surface is muted and hatched, the treatment the app reserves for
386
+ * "this area is not live" (weekend / day-off cells, `--muted-surface`);
387
+ * - no affordance suggests opening: Workload points the link at "#", disables
388
+ * the name tooltip so the title cannot leak, and marks blocked interactions
389
+ * `cursor: not-allowed`.
390
+ */
391
+ export const LIMITED_ACCESS_LABEL = "Limited Access";
392
+
393
+ /* The one sentence a limited card is allowed to say, in the app's own voice.
394
+ It states the viewer's situation as a fact and stops — Workload's secondary
395
+ line is "with your limited access" (WorkloadProjectContainer.js), and its
396
+ status sentences read "Work scheduled for a day off." and "task doesn't have
397
+ an estimate": second person, sentence case, contraction, no apology, and
398
+ never a word about why the thing is on the screen. */
399
+ export const LIMITED_ACCESS_MESSAGE = "You don't have access to this item.";
400
+
401
+ /* ------------------------------------------------------------------ */
402
+ /* The element on the board */
403
+ /* ------------------------------------------------------------------ */
404
+
405
+ /** A project / task element on the board. Position lives in `positions`. */
406
+
407
+ /** The natural width a resolved card of this kind grows to. */
408
+ export const projectCardWidth = kind => kind === "project" ? PROJECT_CARD_WIDTH : TASK_CARD_WIDTH;
409
+
410
+ /**
411
+ * What the card is OF — the task's or the project's own name.
412
+ *
413
+ * These cards are a view of a record that lives in the project, so the board
414
+ * does not get to name them: the title is the entity's, and it is read-only
415
+ * wherever it appears. A placeholder has no entity yet, so it says what it is
416
+ * still waiting for.
417
+ */
418
+ export const projectElementTitle = element => {
419
+ if (element.limited) {
420
+ return LIMITED_ACCESS_LABEL;
421
+ }
422
+ if (element.kind === "project") {
423
+ var _element$project$name, _element$project;
424
+ return (_element$project$name = (_element$project = element.project) == null ? void 0 : _element$project.name) != null ? _element$project$name : "Project";
425
+ }
426
+ return element.task ? taskDisplayName(element.task) : "Task";
427
+ };
428
+
429
+ /* ------------------------------------------------------------------ */
430
+ /* Seeded board state */
431
+ /* ------------------------------------------------------------------ */
432
+
433
+ /**
434
+ * Two cards the board starts with, both limited.
435
+ *
436
+ * They are seeded rather than reachable through the picker on purpose: limited
437
+ * access is a property of the VIEWER, and you cannot pick a card you are not
438
+ * allowed to see. Someone else placed these; this session is simply the one
439
+ * that cannot open them. Seeding is also the whole demonstration — no "view as"
440
+ * switch is needed to show what the state looks like.
441
+ *
442
+ * Both are `resolved` with `project` and `task` left null: the record was never
443
+ * withheld pending a fetch, it is withheld full stop.
444
+ */
445
+
446
+ const SEEDED = [{
447
+ element: {
448
+ id: "limited-task",
449
+ kind: "task",
450
+ width: TASK_CARD_WIDTH,
451
+ height: PLACEHOLDER_MIN_HEIGHT,
452
+ status: "resolved",
453
+ limited: true,
454
+ project: null,
455
+ task: null
456
+ },
457
+ x: 120,
458
+ y: 200
459
+ }, {
460
+ element: {
461
+ id: "limited-project",
462
+ kind: "project",
463
+ width: PROJECT_CARD_WIDTH,
464
+ height: PLACEHOLDER_MIN_HEIGHT,
465
+ status: "resolved",
466
+ limited: true,
467
+ project: null,
468
+ task: null
469
+ },
470
+ x: 120,
471
+ y: 380
472
+ }];
473
+
474
+ /* Derived from one list, the way INITIAL_POSITIONS is derived from
475
+ CANVAS_ELEMENTS — a seed's position is written once, not twice. */
476
+ export const SEEDED_PROJECT_ELEMENTS = SEEDED.map(seed => seed.element);
477
+ export const SEEDED_PROJECT_ELEMENT_POSITIONS = SEEDED.reduce((acc, seed) => {
478
+ acc[seed.element.id] = {
479
+ x: seed.x,
480
+ y: seed.y
481
+ };
482
+ return acc;
483
+ }, {});
484
+
485
+ /* The board wrapper. It is the canvas element box, only rounded — the shared
486
+ box is square because shapes are, and a square selection ring around a
487
+ rounded card reads as a second, wrong border. The transition is what makes
488
+ the placeholder GROW into its card instead of being replaced by one. */
489
+ export const StyledProjectElementBox = styled(StyledCanvasElement).withConfig({
490
+ displayName: "projectElements__StyledProjectElementBox",
491
+ componentId: "sc-1cayxi5-0"
492
+ })(["border-radius:8px;transition:width 160ms ease;&.limited{cursor:not-allowed;}"]);
493
+
494
+ /* The rubber band and the armed tool's hover preview — the same blue dashed
495
+ box the link tool draws, so the gesture reads as one board convention. */
496
+ export const StyledProjectDraft = styled.div.withConfig({
497
+ displayName: "projectElements__StyledProjectDraft",
498
+ componentId: "sc-1cayxi5-1"
499
+ })(["position:absolute;box-sizing:border-box;border:1.5px dashed ", ";border-radius:8px;background-color:rgba(59,130,246,0.08);pointer-events:none;"], SELECTION_BLUE);
500
+
501
+ /* The placeholder that waits for the picker's answer. Card-like on purpose:
502
+ rounded, dashed, tinted — a card-shaped hole, not a selection marquee. */
503
+ export const StyledPlaceholder = styled.div.withConfig({
504
+ displayName: "projectElements__StyledPlaceholder",
505
+ componentId: "sc-1cayxi5-2"
506
+ })(["box-sizing:border-box;width:100%;height:100%;display:flex;align-items:center;justify-content:center;padding:12px;border:1.5px dashed var(--border-primary);border-radius:8px;background-color:var(--color-theme-200);user-select:none;"]);
507
+
508
+ /* The resolving flash: the placeholder's box, with the card's rows sketched in
509
+ so the swap to the real card does not read as a different element. */
510
+ export const StyledResolving = styled(StyledPlaceholder).withConfig({
511
+ displayName: "projectElements__StyledResolving",
512
+ componentId: "sc-1cayxi5-3"
513
+ })(["flex-direction:column;align-items:stretch;justify-content:center;gap:10px;.c-loader{height:12px;}"]);
514
+
515
+ /**
516
+ * The limited-access face — the card-shaped translation of Workload's black
517
+ * `.workload_null_project_cell`.
518
+ *
519
+ * The app hardcodes that cell `#000000` on `#ffffff` text, which works there
520
+ * because it always sits on light paper. A canvas is not always light: on the
521
+ * dark board a black slab would sink into the background instead of reading as
522
+ * something withheld. So the same idea is expressed with the design system's
523
+ * own "not live" tokens — `--muted-surface` / `--muted-surface-text` are the
524
+ * pair the app itself substitutes for muted surfaces in dark themes, and they
525
+ * are a legible, deliberately flat combination in every theme.
526
+ *
527
+ * The -45° hatch is copied stroke for stroke from the app's unavailable
528
+ * surfaces (weekend and day-off cells): `transparent 5px, <line> 5px, <line>
529
+ * 5.7px`, with `--stripe-pattern` as the line — the token workload.less uses
530
+ * for exactly this in dark themes.
531
+ */
532
+ export const StyledLimitedCard = styled.div.withConfig({
533
+ displayName: "projectElements__StyledLimitedCard",
534
+ componentId: "sc-1cayxi5-4"
535
+ })(["box-sizing:border-box;width:100%;height:100%;min-height:96px;display:flex;align-items:center;justify-content:center;padding:12px;border:1px solid var(--border-primary);border-radius:8px;background-color:var(--muted-surface);background-image:repeating-linear-gradient( -45deg,transparent,transparent 5px,var(--stripe-pattern) 5px,var(--stripe-pattern) 5.7px );color:var(--muted-surface-text);cursor:not-allowed;user-select:none;.limited-label{color:var(--muted-surface-text);}"]);
536
+
537
+ /**
538
+ * Both resolved faces are the design system's `StackedCard` — the same shell the
539
+ * board's link and file cards already stand on — filled with the shared typed
540
+ * content cards from `../stackedCard/content`. The board owns only the width and
541
+ * the selection state; everything inside the card is the card's business.
542
+ *
543
+ * Identical to `StyledLinkCard`: the shell's reference 260px width gives way to
544
+ * the box the board drew, and the height stays auto.
545
+ */
546
+ const StyledEntityCard = styled(StackedCard).withConfig({
547
+ displayName: "projectElements__StyledEntityCard",
548
+ componentId: "sc-1cayxi5-5"
549
+ })(["&&{width:100%;}user-select:none;"]);
550
+ /**
551
+ * The task face — the shared `TaskCard`, with the project line switched on.
552
+ *
553
+ * The board writes no anatomy of its own: the title clamp, the info line (labels
554
+ * as colour text, the counter signifiers and their 2px dot separators) and the
555
+ * date / assignees row all live in the content card, where the column-view
556
+ * baseline they preserve is documented. The one thing the board asks for is the
557
+ * project line above the title: on a canvas a task card has lost the column that
558
+ * used to say which project it came from.
559
+ *
560
+ * The title is the app's own display name (`#123: Name`, `taskDisplayName`), so
561
+ * the card's separate task-number prefix stays off.
562
+ */
563
+ export const TaskElementCard = _ref => {
564
+ var _task$subtasks, _task$comments, _task$date;
565
+ let project = _ref.project,
566
+ task = _ref.task,
567
+ selected = _ref.selected;
568
+ return /*#__PURE__*/React.createElement(StyledEntityCard, {
569
+ selected: selected
570
+ }, /*#__PURE__*/React.createElement(TaskCard, {
571
+ title: taskDisplayName(task),
572
+ showProjectName: true,
573
+ projectName: project.name,
574
+ labels: task.label ? [task.label] : [],
575
+ subtaskCount: (_task$subtasks = task.subtasks) != null ? _task$subtasks : 0,
576
+ commentCount: (_task$comments = task.comments) != null ? _task$comments : 0,
577
+ dateLabel: (_task$date = task.date) != null ? _task$date : null,
578
+ assigned: task.assignees.length > 0,
579
+ assignees: task.assignees
580
+ }));
581
+ };
582
+
583
+ /**
584
+ * The project face — the shared `ProjectCard`.
585
+ *
586
+ * The whiteboard's face spec and the content type's are the SAME spec, so it is
587
+ * implemented once, in the card: name and client, an optional description
588
+ * excerpt, and one row carrying the leader on the left and the label on the
589
+ * right. No progress indicator, and no row at all when the project has neither a
590
+ * leader nor a label.
591
+ */
592
+ export const ProjectElementCard = _ref2 => {
593
+ let project = _ref2.project,
594
+ selected = _ref2.selected;
595
+ return /*#__PURE__*/React.createElement(StyledEntityCard, {
596
+ selected: selected
597
+ }, /*#__PURE__*/React.createElement(ProjectCard, {
598
+ name: project.name,
599
+ client: project.client,
600
+ description: project.description,
601
+ leader: project.lead,
602
+ label: project.label
603
+ }));
604
+ };
605
+
606
+ /**
607
+ * The limited-access card. Deliberately the shortest component in this file:
608
+ * one phrase, no icon, no entity data of any kind.
609
+ *
610
+ * No icon because the app uses none — Expenses and Timelist both render the
611
+ * bare `Caption1 weight="bold"` "Limited Access", and an icon here would be a
612
+ * second signal competing with the hatch. Even the KIND (task vs project) is
613
+ * withheld: the viewer is told that a card exists, not what sort of thing it
614
+ * points at.
615
+ *
616
+ * The tooltip is allowed to exist because it carries the viewer's situation,
617
+ * not the entity's identity — the opposite of the tooltip Workload disables on
618
+ * a limited event, which would have leaked the name.
619
+ */
620
+ export const LimitedElementCard = () => /*#__PURE__*/React.createElement(Tooltip, {
621
+ title: LIMITED_ACCESS_MESSAGE
622
+ }, /*#__PURE__*/React.createElement(StyledLimitedCard, {
623
+ "data-testid": "wb-limited-card"
624
+ }, /*#__PURE__*/React.createElement(Caption1, {
625
+ weight: "bold",
626
+ className: "limited-label"
627
+ }, LIMITED_ACCESS_LABEL)));
628
+
629
+ /** One board element, in whichever of its three states it currently is. */
630
+ export const ProjectElementNode = _ref3 => {
631
+ let element = _ref3.element,
632
+ selected = _ref3.selected;
633
+ /* Checked before anything else: a limited element must never be able to fall
634
+ through to a branch that renders entity data. */
635
+ if (element.limited) {
636
+ return /*#__PURE__*/React.createElement(LimitedElementCard, null);
637
+ }
638
+ if (element.status === "placeholder") {
639
+ return /*#__PURE__*/React.createElement(StyledPlaceholder, null, /*#__PURE__*/React.createElement(Caption1, {
640
+ color: "tertiary"
641
+ }, element.kind === "project" ? "Pick a project…" : "Pick a task…"));
642
+ }
643
+ if (element.status === "resolving") {
644
+ return /*#__PURE__*/React.createElement(StyledResolving, null, /*#__PURE__*/React.createElement(SkeletonLoader, {
645
+ style: {
646
+ width: "80%"
647
+ }
648
+ }), /*#__PURE__*/React.createElement(SkeletonLoader, {
649
+ style: {
650
+ width: "55%"
651
+ }
652
+ }), /*#__PURE__*/React.createElement(SkeletonLoader, {
653
+ style: {
654
+ width: "40%"
655
+ }
656
+ }));
657
+ }
658
+ if (element.kind === "project" && element.project) {
659
+ return /*#__PURE__*/React.createElement(ProjectElementCard, {
660
+ project: element.project,
661
+ selected: selected
662
+ });
663
+ }
664
+ if (element.project && element.task) {
665
+ return /*#__PURE__*/React.createElement(TaskElementCard, {
666
+ project: element.project,
667
+ task: element.task,
668
+ selected: selected
669
+ });
670
+ }
671
+ return /*#__PURE__*/React.createElement(StyledPlaceholder, null);
672
+ };
673
+
674
+ /* ------------------------------------------------------------------ */
675
+ /* The picker */
676
+ /* ------------------------------------------------------------------ */
677
+
678
+ /* The popover, centred over the board's own overlay. It is deliberately NOT a
679
+ DS `Dialog`: the app's picker has no title and no action buttons — you search,
680
+ you pick, it closes. `Modal` is used only for what a dialog and a popover do
681
+ share: the portal, the overlay, the focus lock and the Esc handler. */
682
+ const StyledPickerPanel = styled(Paper).withConfig({
683
+ displayName: "projectElements__StyledPickerPanel",
684
+ componentId: "sc-1cayxi5-6"
685
+ })(["position:absolute;top:50%;left:50%;transform:translate(-50%,-50%);display:flex;flex-direction:column;width:470px;max-width:calc(100vw - 32px);max-height:70vh;overflow:hidden;&&{border-radius:12px;border:1px solid var(--border-primary);box-shadow:0 18px 48px rgba(0,0,0,0.24);}"]);
686
+
687
+ /* Search + adornments, mirroring `Select`'s own form row: the back arrow is a
688
+ start adornment (TaskList), the group-by sort a end adornment (ProjectList). */
689
+ const StyledPickerHeader = styled.div.withConfig({
690
+ displayName: "projectElements__StyledPickerHeader",
691
+ componentId: "sc-1cayxi5-7"
692
+ })(["display:flex;align-items:center;gap:8px;flex-shrink:0;padding:12px 12px 8px;.picker-search{width:100%;border-radius:8px;}.picker-search:focus-within{border-color:var(--color-primary);outline:1px solid var(--color-primary);}"]);
693
+ const StyledPickerList = styled.div.withConfig({
694
+ displayName: "projectElements__StyledPickerList",
695
+ componentId: "sc-1cayxi5-8"
696
+ })(["flex:1 1 auto;min-height:0;overflow-y:auto;padding:4px 0 12px;.picker-group-header{box-sizing:border-box;height:36px;display:flex;align-items:center;padding:0 20px;margin-top:4px;}.picker-empty{padding:24px 20px;text-align:center;}"]);
697
+
698
+ /* A generously spaced row: the name on the left, an empty radio on the right —
699
+ the shape `Select` gives a single-choice option, at picker scale. */
700
+ const StyledPickerRow = styled.div.withConfig({
701
+ displayName: "projectElements__StyledPickerRow",
702
+ componentId: "sc-1cayxi5-9"
703
+ })(["box-sizing:border-box;height:56px;display:flex;align-items:center;justify-content:space-between;gap:12px;padding:0 20px;cursor:pointer;outline:none;&:hover,&:focus-visible{background-color:var(--color-theme-200);}.row-name{min-width:0;overflow:hidden;white-space:nowrap;text-overflow:ellipsis;}.c-radio-btn{flex-shrink:0;}"]);
704
+ const PickerRow = _ref4 => {
705
+ let id = _ref4.id,
706
+ name = _ref4.name,
707
+ onSelect = _ref4.onSelect;
708
+ return /*#__PURE__*/React.createElement(StyledPickerRow, {
709
+ role: "button",
710
+ tabIndex: 0,
711
+ onClick: onSelect,
712
+ onKeyDown: event => {
713
+ if (event.key === "Enter" || event.key === " ") {
714
+ event.preventDefault();
715
+ onSelect();
716
+ }
717
+ }
718
+ }, /*#__PURE__*/React.createElement("span", {
719
+ className: "row-name"
720
+ }, /*#__PURE__*/React.createElement(Body2, null, name)), /*#__PURE__*/React.createElement(RadioButton, {
721
+ id: "wb-picker-" + id,
722
+ tabIndex: -1
723
+ }));
724
+ };
725
+ /**
726
+ * One picker, two flows — the same split the app makes between
727
+ * `MenuSelector/ProjectList` (one step) and `ProjectTaskMenuSelector` (project,
728
+ * then task). The task flow deliberately drops the app's "Directly on project"
729
+ * default option: a task card has to be a task.
730
+ */
731
+ export const ProjectElementPicker = _ref5 => {
732
+ let kind = _ref5.kind,
733
+ onSelectProject = _ref5.onSelectProject,
734
+ onSelectTask = _ref5.onSelectTask,
735
+ onCancel = _ref5.onCancel;
736
+ const _useState = useState(null),
737
+ project = _useState[0],
738
+ setProject = _useState[1];
739
+ const _useState2 = useState(""),
740
+ query = _useState2[0],
741
+ setQuery = _useState2[1];
742
+ /* The picker opens on a mouseUP — the end of the draw gesture — so the click
743
+ that closes that gesture arrives when the overlay is already on screen. A
744
+ dismissal therefore has to be a WHOLE click on the overlay: press, then
745
+ release. Without this the placeholder could be cancelled by the very
746
+ gesture that created it. */
747
+ const pressedInsideRef = useRef(false);
748
+ const onTaskStep = kind === "task" && project !== null;
749
+ const projectGroups = useMemo(() => groupProjects(PICKER_PROJECTS.filter(entry => matchesQuery(entry.name, query))), [query]);
750
+ const taskGroups = useMemo(() => project ? groupTasks(project.tasks.filter(task => matchesQuery(taskDisplayName(task), query))) : [], [project, query]);
751
+ const handleProject = picked => {
752
+ if (kind === "project") {
753
+ onSelectProject(picked);
754
+ return;
755
+ }
756
+ setProject(picked);
757
+ setQuery("");
758
+ };
759
+ const handleBack = () => {
760
+ setProject(null);
761
+ setQuery("");
762
+ };
763
+ const isEmpty = onTaskStep ? taskGroups.length === 0 : projectGroups.length === 0;
764
+ return /*#__PURE__*/React.createElement(Modal, {
765
+ open: true,
766
+ onClose: onCancel,
767
+ disableBackgroundClick: true,
768
+ onMouseDown: () => {
769
+ pressedInsideRef.current = true;
770
+ },
771
+ onClickOutside: () => {
772
+ if (pressedInsideRef.current) {
773
+ onCancel();
774
+ }
775
+ }
776
+ }, /*#__PURE__*/React.createElement(StyledPickerPanel, {
777
+ type: "paper-2"
778
+ }, /*#__PURE__*/React.createElement(StyledPickerHeader, null, onTaskStep ? /*#__PURE__*/React.createElement(Tooltip, {
779
+ title: "Back to projects"
780
+ }, /*#__PURE__*/React.createElement(IconButton, {
781
+ type: "button",
782
+ variant: "text gray",
783
+ onClick: handleBack,
784
+ "aria-label": "Back to projects"
785
+ }, /*#__PURE__*/React.createElement(ArrowLeftIcon, null))) : null, /*#__PURE__*/React.createElement(Input, {
786
+ className: "picker-search",
787
+ autoFocus: true,
788
+ value: query,
789
+ placeholder: onTaskStep ? "Search a task" : "Search a project",
790
+ onChange: event => setQuery(event.target.value)
791
+ }), onTaskStep ? null : /*#__PURE__*/React.createElement(Tooltip, {
792
+ title: "Group by"
793
+ }, /*#__PURE__*/React.createElement(IconButton, {
794
+ type: "button",
795
+ variant: "text gray"
796
+ }, /*#__PURE__*/React.createElement(SortGeneralIcon, null)))), /*#__PURE__*/React.createElement(StyledPickerList, null, isEmpty ? /*#__PURE__*/React.createElement("div", {
797
+ className: "picker-empty"
798
+ }, /*#__PURE__*/React.createElement(Body2, {
799
+ color: "tertiary"
800
+ }, onTaskStep ? "No Result" : "No Projects")) : null, onTaskStep && project ? taskGroups.map(group => /*#__PURE__*/React.createElement("div", {
801
+ key: group.taskList
802
+ }, /*#__PURE__*/React.createElement("div", {
803
+ className: "picker-group-header"
804
+ }, /*#__PURE__*/React.createElement(Body2, {
805
+ weight: "bold"
806
+ }, group.taskList)), group.tasks.map(task => /*#__PURE__*/React.createElement(PickerRow, {
807
+ key: task.id,
808
+ id: task.id,
809
+ name: taskDisplayName(task),
810
+ onSelect: () => onSelectTask(project, task)
811
+ })))) : null, onTaskStep ? null : projectGroups.map(group => /*#__PURE__*/React.createElement("div", {
812
+ key: group.client
813
+ }, /*#__PURE__*/React.createElement("div", {
814
+ className: "picker-group-header"
815
+ }, /*#__PURE__*/React.createElement(Body2, {
816
+ weight: "bold"
817
+ }, group.client)), group.projects.map(entry => /*#__PURE__*/React.createElement(PickerRow, {
818
+ key: entry.id,
819
+ id: entry.id,
820
+ name: entry.name,
821
+ onSelect: () => handleProject(entry)
822
+ })))))));
823
+ };
824
+ //# sourceMappingURL=projectElements.js.map