@anchrd/intel-ui 0.16.0 → 0.17.0

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 (35) hide show
  1. package/package.json +10 -2
  2. package/src/agent/agent-profile/agent-profile.tsx +2 -0
  3. package/src/board/board-calendar/board-calendar.tsx +89 -0
  4. package/src/board/board-card/board-card.tsx +106 -0
  5. package/src/board/board-data/board-data.ts +241 -0
  6. package/src/board/board-data/board-data.types.ts +63 -0
  7. package/src/board/board-detail/board-detail.tsx +629 -0
  8. package/src/board/board-gantt/board-gantt.ts +545 -0
  9. package/src/board/board-gantt/board-gantt.tsx +286 -0
  10. package/src/board/board-graph/board-graph.ts +174 -0
  11. package/src/board/board-graph/board-graph.tsx +168 -0
  12. package/src/board/board-items/board-items.ts +183 -0
  13. package/src/board/board-kanban/board-kanban.ts +97 -0
  14. package/src/board/board-kanban/board-kanban.tsx +211 -0
  15. package/src/board/board-status/board-status.ts +59 -0
  16. package/src/board/board-statuses/board-statuses.ts +63 -0
  17. package/src/board/board-statuses/board-statuses.tsx +228 -0
  18. package/src/board/board-table/board-table.ts +33 -0
  19. package/src/board/board-table/board-table.tsx +413 -0
  20. package/src/board/board-views/board-views.tsx +68 -0
  21. package/src/board/board-views/board-views.types.ts +29 -0
  22. package/src/board/board.tsx +251 -0
  23. package/src/components/ui/dropdown-menu.tsx +25 -0
  24. package/src/components/ui/item-calendar.tsx +181 -0
  25. package/src/components/ui/item-gantt.tsx +463 -0
  26. package/src/components/ui/kanban.tsx +245 -0
  27. package/src/components/ui/switch.tsx +25 -0
  28. package/src/data/intel-data-provider/intel-data-provider.ts +52 -0
  29. package/src/data/intel-data-provider/intel-data-provider.types.ts +31 -0
  30. package/src/i18n/de.json +88 -1
  31. package/src/i18n/en.json +88 -1
  32. package/src/i18n/es.json +88 -1
  33. package/src/kind-icon.ts +12 -1
  34. package/src/nodes/nodes.tsx +16 -0
  35. package/src/styles.css +33 -0
@@ -0,0 +1,245 @@
1
+ import {
2
+ type Announcements,
3
+ closestCorners,
4
+ DndContext,
5
+ DragOverlay,
6
+ type DragStartEvent,
7
+ KeyboardSensor,
8
+ PointerSensor,
9
+ useDroppable,
10
+ useSensor,
11
+ useSensors,
12
+ } from "@dnd-kit/core";
13
+ import { SortableContext, useSortable, verticalListSortingStrategy } from "@dnd-kit/sortable";
14
+ import { CSS } from "@dnd-kit/utilities";
15
+ import { GripVertical } from "lucide-react";
16
+ import { type ReactNode, useState } from "react";
17
+ import { createPortal } from "react-dom";
18
+ import { cn } from "@/lib/utils";
19
+
20
+ /**
21
+ * A kanban board on dnd-kit, from the Kibo UI registry (MIT) and changed where it had to be.
22
+ *
23
+ * Registry code is our code, and four things about the original do not survive contact with a board
24
+ * whose truth is on a server:
25
+ *
26
+ * ⚠️ **It reported a drop as a new ARRAY, and it reported one twice.** The original called
27
+ * `onDataChange` in `onDragOver` for a cross-column move and again in `onDragEnd`, and both times
28
+ * with a reordered copy of the whole list. Against a server that is two writes for one drag — and
29
+ * the second one describes a state the first already changed. This version says nothing while the
30
+ * card is in the air and emits ONE drop at the end, naming what was dropped and what it was dropped
31
+ * on. What that means is the caller's to decide (`board-kanban.ts`), which is also where the single
32
+ * `board_task_move` is sent from.
33
+ *
34
+ * ⚠️ **It mutated its own props** — `newData[activeIndex].column = overColumn` writes into the
35
+ * object the caller passed in. With TanStack Query holding that object, a drag edited the cache in
36
+ * place and no re-render followed.
37
+ *
38
+ * ⚠️ **`tunnel-rat` is gone.** It existed to render the dragged card into the overlay from inside
39
+ * the card component; `DragOverlay` already does that, and the caller draws the overlay itself.
40
+ * One dependency fewer for something React has a portal for.
41
+ *
42
+ * ⚠️ **The screen reader announcements are handed in.** The originals are English string literals
43
+ * in the component; Intel speaks three languages and its text comes from `useI18n()`.
44
+ *
45
+ * ⚠️ **The card is not itself a `role="button"`.** The original spread `useSortable().attributes`
46
+ * onto the card, which contributes `role="button"` and `tabIndex={0}` — and a card that also has to
47
+ * be OPENED then holds a real button inside that one. Two tab stops computing the same accessible
48
+ * name from the same contents, and an Enter that means "start dragging" on the outside and "open
49
+ * this" on the inside. Split instead: the card keeps `listeners`, so a pointer can still drag it
50
+ * from anywhere, and `attributes` moves to a handle that is the one tab stop for dragging and says
51
+ * so.
52
+ */
53
+
54
+ export interface KanbanColumn {
55
+ id: string;
56
+ name: string;
57
+ }
58
+
59
+ export interface KanbanDrop {
60
+ activeId: string;
61
+ // The column or the card it was let go over. `null` means it was let go over nothing — a drag
62
+ // that ended outside the board, which is a cancel and not a move.
63
+ overId: string | null;
64
+ }
65
+
66
+ export function KanbanBoard({
67
+ id,
68
+ droppable = true,
69
+ children,
70
+ className,
71
+ }: {
72
+ id: string;
73
+ // A column that exists to be READ but must not be written into. It still draws and still holds
74
+ // cards; it simply never becomes a drop target, so nothing can be dropped where the answer would
75
+ // be a refusal (`board-kanban.tsx`).
76
+ droppable?: boolean;
77
+ children: ReactNode;
78
+ className?: string;
79
+ }) {
80
+ const { isOver, setNodeRef } = useDroppable({ id, disabled: !droppable });
81
+ return (
82
+ <div
83
+ ref={setNodeRef}
84
+ data-slot="kanban-column"
85
+ data-droppable={droppable}
86
+ className={cn(
87
+ "flex size-full min-h-40 flex-col divide-y overflow-hidden rounded-lg border bg-muted/40 text-xs ring-2 transition-colors",
88
+ isOver ? "ring-ring" : "ring-transparent",
89
+ className,
90
+ )}
91
+ >
92
+ {children}
93
+ </div>
94
+ );
95
+ }
96
+
97
+ export function KanbanHeader({ children }: { children: ReactNode }) {
98
+ return <div className="flex items-center gap-2 p-2 text-sm font-semibold">{children}</div>;
99
+ }
100
+
101
+ export function KanbanCards({ id, children }: { id: string; children: ReactNode }) {
102
+ return (
103
+ <div
104
+ // The column scrolls, not the page: four lanes of different lengths must not make the board
105
+ // as tall as its longest one.
106
+ className="flex min-h-0 flex-1 flex-col gap-2 overflow-y-auto p-2"
107
+ data-slot="kanban-cards"
108
+ data-column={id}
109
+ >
110
+ {children}
111
+ </div>
112
+ );
113
+ }
114
+
115
+ export function KanbanCard({
116
+ id,
117
+ dragLabel,
118
+ children,
119
+ className,
120
+ }: {
121
+ id: string;
122
+ // What the drag handle is called. Handed in because only the caller knows what the card is of.
123
+ dragLabel: string;
124
+ children: ReactNode;
125
+ className?: string;
126
+ }) {
127
+ const {
128
+ attributes,
129
+ listeners,
130
+ setNodeRef,
131
+ setActivatorNodeRef,
132
+ transition,
133
+ transform,
134
+ isDragging,
135
+ } = useSortable({ id });
136
+ return (
137
+ <div
138
+ ref={setNodeRef}
139
+ style={{ transition, transform: CSS.Transform.toString(transform) }}
140
+ // ⚠️ `listeners` here, `attributes` NOT. The pointer sensor has to see a press anywhere on the
141
+ // card — a kanban whose cards can only be dragged by a grip is a kanban nobody drags — but
142
+ // `attributes` is what carries `role="button"` and `tabIndex`, and those on the card would
143
+ // wrap the control that OPENS it in a second button. Pointer drag from the whole card,
144
+ // keyboard drag from the handle, one tab stop each.
145
+ //
146
+ // ⚠️ The split only holds because the handle registers itself as the ACTIVATOR below. Without
147
+ // that, `listeners` on this element make every Enter and Space inside the card a drag: the
148
+ // key handler bubbles up from whatever is focused, `KeyboardSensor` accepts it and calls
149
+ // `preventDefault()`, and the button that opens the task never sees its own activation.
150
+ {...listeners}
151
+ className={cn(
152
+ "flex cursor-grab items-start gap-1 rounded-lg border bg-card p-3 text-left shadow-sm",
153
+ // Left where it was, faded, rather than removed: a lane that reflows while a card is over
154
+ // it moves the drop target out from under the pointer.
155
+ isDragging && "opacity-40",
156
+ className,
157
+ )}
158
+ >
159
+ {/* The keyboard half of the drag: `attributes` puts the card into the tab order exactly once,
160
+ here, where it has a name of its own that says what it does.
161
+
162
+ ⚠️ `setActivatorNodeRef` is what makes that true rather than merely intended. dnd-kit's
163
+ `KeyboardSensor` opens with `if (activator && event.target !== activator) return false` —
164
+ a guard that only exists once something has claimed to BE the activator. Unclaimed, the
165
+ keyboard sensor accepts a key press from anywhere under the listeners, so Enter on the
166
+ card's own "open" button started a drag and swallowed the click. The pointer drag is
167
+ unaffected: `PointerSensor` never consults the activator node. */}
168
+ <button
169
+ type="button"
170
+ ref={setActivatorNodeRef}
171
+ aria-label={dragLabel}
172
+ {...listeners}
173
+ {...attributes}
174
+ className="mt-0.5 shrink-0 cursor-grab rounded text-muted-foreground outline-none focus-visible:ring-2 focus-visible:ring-ring"
175
+ >
176
+ <GripVertical aria-hidden="true" className="size-4" />
177
+ </button>
178
+ <div className="min-w-0 flex-1">{children}</div>
179
+ </div>
180
+ );
181
+ }
182
+
183
+ export function KanbanProvider({
184
+ columns,
185
+ itemsByColumn,
186
+ children,
187
+ announcements,
188
+ overlay,
189
+ onDrop,
190
+ className,
191
+ }: {
192
+ columns: KanbanColumn[];
193
+ // Which cards stand in which column, in the order they are drawn. The provider needs it for the
194
+ // sortable contexts and asks for it rather than deriving it, so the caller stays the only place
195
+ // that knows how a board is ordered.
196
+ itemsByColumn: Map<string, string[]>;
197
+ children: (column: KanbanColumn) => ReactNode;
198
+ announcements: Announcements;
199
+ overlay(activeId: string): ReactNode;
200
+ onDrop(drop: KanbanDrop): void;
201
+ className?: string;
202
+ }) {
203
+ const [activeId, setActiveId] = useState<string | null>(null);
204
+ const sensors = useSensors(
205
+ // ⚠️ A distance before a drag starts, and it is not decoration: without it every click on a
206
+ // card is a drag of zero pixels, and the card can then no longer be opened by clicking it.
207
+ useSensor(PointerSensor, { activationConstraint: { distance: 4 } }),
208
+ useSensor(KeyboardSensor),
209
+ );
210
+
211
+ return (
212
+ <DndContext
213
+ accessibility={{ announcements }}
214
+ // Corners rather than centres: a tall card over a short one wins on centre distance even
215
+ // when the pointer is nowhere near it.
216
+ collisionDetection={closestCorners}
217
+ sensors={sensors}
218
+ onDragStart={(event: DragStartEvent) => setActiveId(String(event.active.id))}
219
+ onDragCancel={() => setActiveId(null)}
220
+ onDragEnd={(event) => {
221
+ setActiveId(null);
222
+ const over = event.over === null ? null : String(event.over.id);
223
+ onDrop({ activeId: String(event.active.id), overId: over });
224
+ }}
225
+ >
226
+ <div className={cn("grid size-full auto-cols-fr grid-flow-col gap-3", className)}>
227
+ {columns.map((column) => (
228
+ <SortableContext
229
+ key={column.id}
230
+ items={itemsByColumn.get(column.id) ?? []}
231
+ strategy={verticalListSortingStrategy}
232
+ >
233
+ {children(column)}
234
+ </SortableContext>
235
+ ))}
236
+ </div>
237
+ {typeof document === "undefined"
238
+ ? null
239
+ : createPortal(
240
+ <DragOverlay>{activeId === null ? null : overlay(activeId)}</DragOverlay>,
241
+ document.body,
242
+ )}
243
+ </DndContext>
244
+ );
245
+ }
@@ -0,0 +1,25 @@
1
+ import { Switch as SwitchPrimitive } from "radix-ui";
2
+ import type * as React from "react";
3
+ import { cn } from "@/lib/utils";
4
+
5
+ function Switch({ className, ...props }: React.ComponentProps<typeof SwitchPrimitive.Root>) {
6
+ return (
7
+ <SwitchPrimitive.Root
8
+ data-slot="switch"
9
+ className={cn(
10
+ "peer inline-flex h-5 w-9 shrink-0 items-center rounded-full border border-transparent shadow-xs transition-all outline-none focus-visible:ring-2 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:bg-primary data-[state=unchecked]:bg-input",
11
+ className,
12
+ )}
13
+ {...props}
14
+ >
15
+ <SwitchPrimitive.Thumb
16
+ data-slot="switch-thumb"
17
+ className={cn(
18
+ "pointer-events-none block size-4 rounded-full bg-background ring-0 transition-transform data-[state=checked]:translate-x-[calc(100%-2px)] data-[state=unchecked]:translate-x-0",
19
+ )}
20
+ />
21
+ </SwitchPrimitive.Root>
22
+ );
23
+ }
24
+
25
+ export { Switch };
@@ -1,17 +1,23 @@
1
1
  import {
2
+ AddBoardTaskInput,
2
3
  AgentCosts,
3
4
  AgentKeyRotated,
4
5
  AppendTableRowsInput,
5
6
  AppendTableRowsResult,
6
7
  ArchiveFlowInput,
7
8
  ArchiveNodeInput,
9
+ BoardTaskResult,
8
10
  BundleImportResult,
9
11
  CompleteFlowRunStepInput,
12
+ ConfigureBoardInput,
13
+ ConfigureBoardResult,
10
14
  CreateAgentInput,
11
15
  CreatedAgent,
12
16
  CreateFlowInput,
13
17
  CreateNodeInput,
14
18
  DefineTableInput,
19
+ DeleteBoardTaskInput,
20
+ DeleteBoardTaskResult,
15
21
  Flow,
16
22
  FlowDocument,
17
23
  FlowList,
@@ -26,8 +32,10 @@ import {
26
32
  ListFlowsInput,
27
33
  ListNodesInput,
28
34
  ModelCatalog,
35
+ MoveBoardTaskInput,
29
36
  Node,
30
37
  NodeAgent,
38
+ NodeBoard,
31
39
  NodeDocument,
32
40
  NodeGraph,
33
41
  NodeLinkList,
@@ -56,6 +64,7 @@ import {
56
64
  StartFlowRunInput,
57
65
  ToolCatalog,
58
66
  ToolServerCatalog,
67
+ UpdateBoardTaskInput,
59
68
  UpdateFlowInput,
60
69
  UpdateNodeInput,
61
70
  } from "@anchrd/intel-contract";
@@ -327,6 +336,49 @@ export function createIntelDataProvider(
327
336
  { method: "POST", body: JSON.stringify(parsed) },
328
337
  );
329
338
  },
339
+ async getBoard(nodeId) {
340
+ return await request(`/nodes/${encodeURIComponent(nodeId)}/board`, NodeBoard);
341
+ },
342
+ async configureBoard(input) {
343
+ const parsed = ConfigureBoardInput.parse(input);
344
+ return await request(
345
+ `/nodes/${encodeURIComponent(parsed.nodeId)}/board/configure`,
346
+ ConfigureBoardResult,
347
+ { method: "POST", body: JSON.stringify(parsed) },
348
+ );
349
+ },
350
+ async addBoardTask(input) {
351
+ const parsed = AddBoardTaskInput.parse(input);
352
+ return await request(
353
+ `/nodes/${encodeURIComponent(parsed.nodeId)}/board/tasks`,
354
+ BoardTaskResult,
355
+ { method: "POST", body: JSON.stringify(parsed) },
356
+ );
357
+ },
358
+ async updateBoardTask(input) {
359
+ const parsed = UpdateBoardTaskInput.parse(input);
360
+ return await request(
361
+ `/nodes/${encodeURIComponent(parsed.nodeId)}/board/tasks/update`,
362
+ BoardTaskResult,
363
+ { method: "POST", body: JSON.stringify(parsed) },
364
+ );
365
+ },
366
+ async moveBoardTask(input) {
367
+ const parsed = MoveBoardTaskInput.parse(input);
368
+ return await request(
369
+ `/nodes/${encodeURIComponent(parsed.nodeId)}/board/tasks/move`,
370
+ BoardTaskResult,
371
+ { method: "POST", body: JSON.stringify(parsed) },
372
+ );
373
+ },
374
+ async deleteBoardTask(input) {
375
+ const parsed = DeleteBoardTaskInput.parse(input);
376
+ return await request(
377
+ `/nodes/${encodeURIComponent(parsed.nodeId)}/board/tasks/delete`,
378
+ DeleteBoardTaskResult,
379
+ { method: "POST", body: JSON.stringify(parsed) },
380
+ );
381
+ },
330
382
  async listNodeVersions(nodeId) {
331
383
  return await request(`/nodes/${encodeURIComponent(nodeId)}/versions`, NodeVersionList);
332
384
  },
@@ -1,4 +1,5 @@
1
1
  import type {
2
+ AddBoardTaskInput,
2
3
  AgentCosts,
3
4
  AgentKeyRotated,
4
5
  AgentScheduleTarget,
@@ -6,13 +7,18 @@ import type {
6
7
  AppendTableRowsResult,
7
8
  ArchiveFlowInput,
8
9
  ArchiveNodeInput,
10
+ BoardTaskResult,
9
11
  BundleImportResult,
10
12
  CompleteFlowRunStepInput,
13
+ ConfigureBoardInput,
14
+ ConfigureBoardResult,
11
15
  CreateAgentInput,
12
16
  CreatedAgent,
13
17
  CreateFlowInput,
14
18
  CreateNodeInput,
15
19
  DefineTableInput,
20
+ DeleteBoardTaskInput,
21
+ DeleteBoardTaskResult,
16
22
  Flow,
17
23
  FlowDocument,
18
24
  FlowList,
@@ -27,8 +33,10 @@ import type {
27
33
  ListFlowsInput,
28
34
  ListNodesInput,
29
35
  ModelCatalog,
36
+ MoveBoardTaskInput,
30
37
  Node,
31
38
  NodeAgent,
39
+ NodeBoard,
32
40
  NodeDocument,
33
41
  NodeGraph,
34
42
  NodeKind,
@@ -57,6 +65,7 @@ import type {
57
65
  StartFlowRunInput,
58
66
  ToolCatalog,
59
67
  ToolServerCatalog,
68
+ UpdateBoardTaskInput,
60
69
  UpdateFlowInput,
61
70
  UpdateNodeInput,
62
71
  } from "@anchrd/intel-contract";
@@ -127,6 +136,28 @@ export interface IntelDataProvider {
127
136
  getNodeTable(nodeId: string): Promise<NodeTable>;
128
137
  defineTable(input: DefineTableInput): Promise<NodeTable>;
129
138
  appendTableRows(input: AppendTableRowsInput): Promise<AppendTableRowsResult>;
139
+
140
+ // A board's whole document — the status list and every task — as one read (#285). There is no
141
+ // per-task GET and there is not meant to be: the file IS the board, and every view of it
142
+ // (anchrd/intel#286) draws the same list.
143
+ getBoard(nodeId: string): Promise<NodeBoard>;
144
+ // The status list, written whole and in the order it should be drawn. Adding, renaming and
145
+ // reordering are all this one call.
146
+ configureBoard(input: ConfigureBoardInput): Promise<ConfigureBoardResult>;
147
+ // ⚠️ The five task operations answer with the ONE task that was written, never the whole board.
148
+ // A board holds up to five thousand tasks, and a one-card edit that answered with all of them
149
+ // would make every write pay for a full read — so the cache is patched from the answer here
150
+ // (`board-data.ts`) rather than refetched.
151
+ addBoardTask(input: AddBoardTaskInput): Promise<BoardTaskResult>;
152
+ updateBoardTask(input: UpdateBoardTaskInput): Promise<BoardTaskResult>;
153
+ // Where a task sits: its column, its parent, its place among its neighbours. ⚠️ A move names
154
+ // its NEIGHBOURS and never an index — the server mints the order key, so one drag writes one
155
+ // task and renumbers nothing.
156
+ moveBoardTask(input: MoveBoardTaskInput): Promise<BoardTaskResult>;
157
+ // ⚠️ Deleting cascades to every descendant and `deleted` counts them, so a surface can say what
158
+ // it is about to do before it does it rather than afterwards.
159
+ deleteBoardTask(input: DeleteBoardTaskInput): Promise<DeleteBoardTaskResult>;
160
+
130
161
  // Walks the browser through the portal's OAuth flow without asking anybody anything: Gate is the
131
162
  // identity provider Cloudflare Access consumes, so a signed-in person is already known there
132
163
  // (#60). It navigates away — the caller renders no button for it and gets no answer back.
package/src/i18n/de.json CHANGED
@@ -50,6 +50,7 @@
50
50
  "tree.kind.attachment": "Datei",
51
51
  "tree.kind.table": "Tabelle",
52
52
  "tree.kind.agent": "Agent",
53
+ "tree.kind.board": "Board",
53
54
  "tree.kind.flow": "Flow",
54
55
  "tree.move.action": "Verschieben nach …",
55
56
  "tree.move.title": "{title} verschieben",
@@ -108,6 +109,7 @@
108
109
  "node.kind.attachment": "Upload",
109
110
  "node.kind.table": "Tabelle",
110
111
  "node.kind.agent": "Agent",
112
+ "node.kind.board": "Board",
111
113
  "node.kind.flow": "Flow",
112
114
  "node.share": "Freigeben",
113
115
  "node.shareAction": "Zugriff geben",
@@ -422,5 +424,90 @@
422
424
  "common.loading": "Wird geladen …",
423
425
  "common.retry": "Erneut versuchen",
424
426
  "common.close": "Schließen",
425
- "common.unavailable": "Intel ist derzeit nicht verfügbar."
427
+ "common.unavailable": "Intel ist derzeit nicht verfügbar.",
428
+ "board.summary": "{tasks} Aufgaben in {statuses} Status",
429
+ "board.views": "Ansichten dieses Boards",
430
+ "board.view.kanban": "Board",
431
+ "board.view.table": "Tabelle",
432
+ "board.view.calendar": "Kalender",
433
+ "board.view.graph": "Graph",
434
+ "board.view.gantt": "Gantt",
435
+ "board.statuses": "Status",
436
+ "board.statusLabel": "Statusname",
437
+ "board.statusUp": "Status nach oben",
438
+ "board.statusDown": "Status nach unten",
439
+ "board.statusRemove": "Status {label} entfernen",
440
+ "board.addStatus": "Neuer Status",
441
+ "board.shelfFixed": "Immer der letzte",
442
+ "board.showArchived": "Archivierte zeigen",
443
+ "board.addTask": "Neue Aufgabe",
444
+ "board.empty": "Auf diesem Board steht noch nichts. Lege oben die erste Aufgabe an.",
445
+ "board.noTasks": "Hier passt nichts.",
446
+ "board.laneEmpty": "Leer",
447
+ "board.archivedEmpty": "Nichts archiviert.",
448
+ "board.blocked": "Blockiert",
449
+ "board.open": "Offen",
450
+ "board.subtaskCount": "{done}/{total} Teilaufgaben",
451
+ "board.unassigned": "Nicht zugewiesen",
452
+ "board.assignee.me": "Ich",
453
+ "board.assignee.unnamed": "Zugewiesen",
454
+ "board.unknownTask": "Aufgabe für dich nicht verfügbar",
455
+ "board.column.title": "Titel",
456
+ "board.column.status": "Status",
457
+ "board.column.assignee": "Zuständig",
458
+ "board.column.startDate": "Startdatum",
459
+ "board.column.dueDate": "Fällig am",
460
+ "board.column.labels": "Labels",
461
+ "board.columns": "Spalten",
462
+ "board.filterTitle": "Nach Titel filtern",
463
+ "board.groupBy": "Gruppieren nach",
464
+ "board.groupBy.none": "Nichts",
465
+ "board.groupBy.status": "Status",
466
+ "board.groupBy.assignee": "Zuständig",
467
+ "board.toggleSubtasks": "Teilaufgaben von {task} ein- oder ausblenden",
468
+ "board.calendar.previous": "Vorheriger Monat",
469
+ "board.calendar.next": "Nächster Monat",
470
+ "board.calendar.today": "Heute",
471
+ "board.calendar.more": "+{count} weitere",
472
+ "board.calendar.undated": "{count} ohne Fälligkeitsdatum — nicht im Raster, aber auf dem Board und in der Tabelle.",
473
+ "board.gantt.scale": "Maßstab",
474
+ "board.gantt.scale.days": "Tage",
475
+ "board.gantt.scale.weeks": "Wochen",
476
+ "board.gantt.scale.months": "Monate",
477
+ "board.gantt.today": "Heute",
478
+ "board.gantt.move": "{task} verschieben",
479
+ "board.gantt.resizeStart": "Beginn von {task} ändern",
480
+ "board.gantt.resizeEnd": "Fälligkeit von {task} ändern",
481
+ "board.gantt.spanLabel": "{start} bis {end}",
482
+ "board.gantt.waitsFor": "wartet auf {tasks}",
483
+ "board.gantt.hiddenLinks": "{count} Abhängigkeiten sind nicht gezeichnet: die erwartete Aufgabe hat hier keinen Balken.",
484
+ "board.gantt.undated": "Ohne Datum",
485
+ "board.gantt.undatedCount": "{count} ohne Datum — auf dem Board, aber auf keiner Zeitachse.",
486
+ "board.gantt.nothingDated": "Nichts auf diesem Board hat ein Datum. Gib einer Aufgabe ein Fälligkeitsdatum, dann erscheint sie hier.",
487
+ "board.graph.subtree": "Teilbaum",
488
+ "board.graph.wholeBoard": "Ganzes Board",
489
+ "board.graph.parentEdge": "enthält",
490
+ "board.graph.dependsEdge": "wartet auf",
491
+ "board.detail": "Aufgabe",
492
+ "board.description": "Beschreibung",
493
+ "board.subtasks": "Teilaufgaben",
494
+ "board.addSubtask": "Neue Teilaufgabe",
495
+ "board.dependencies": "Wartet auf",
496
+ "board.addDependency": "Abhängigkeit hinzufügen",
497
+ "board.removeDependency": "Diese Abhängigkeit entfernen",
498
+ "board.references": "Referenzen",
499
+ "board.addReference": "Referenz hinzufügen",
500
+ "board.removeReference": "Diese Referenz entfernen",
501
+ "board.addLabel": "Neues Label",
502
+ "board.removeLabel": "Label {label} entfernen",
503
+ "board.deleteTask": "Aufgabe löschen",
504
+ "board.deleteConfirm": "„{task}“ zu löschen entfernt die Aufgabe und alles darunter: insgesamt {count}. Abhängigkeiten darauf werden entfernt.",
505
+ "board.deleted": "Gelöscht: {count}.",
506
+ "board.archivedHint": "Diese Aufgabe liegt im Archiv. Ein Wechsel in einen anderen Status holt sie zurück.",
507
+ "board.dnd.start": "{task} aufgenommen",
508
+ "board.dnd.over": "{task} über {lane}",
509
+ "board.dnd.end": "{task} in {lane} abgelegt",
510
+ "board.dnd.cancel": "Ablegen von {task} abgebrochen",
511
+ "board.dragCard": "{task} ziehen",
512
+ "board.statusTerminal": "Gilt als erledigt"
426
513
  }
package/src/i18n/en.json CHANGED
@@ -50,6 +50,7 @@
50
50
  "tree.kind.attachment": "File",
51
51
  "tree.kind.table": "Table",
52
52
  "tree.kind.agent": "Agent",
53
+ "tree.kind.board": "Board",
53
54
  "tree.kind.flow": "Flow",
54
55
  "tree.move.action": "Move to…",
55
56
  "tree.move.title": "Move {title}",
@@ -108,6 +109,7 @@
108
109
  "node.kind.attachment": "Upload",
109
110
  "node.kind.table": "Table",
110
111
  "node.kind.agent": "Agent",
112
+ "node.kind.board": "Board",
111
113
  "node.kind.flow": "Flow",
112
114
  "node.share": "Share",
113
115
  "node.shareAction": "Grant access",
@@ -422,5 +424,90 @@
422
424
  "common.loading": "Loading…",
423
425
  "common.retry": "Try again",
424
426
  "common.close": "Close",
425
- "common.unavailable": "Intel is currently unavailable."
427
+ "common.unavailable": "Intel is currently unavailable.",
428
+ "board.summary": "{tasks} tasks in {statuses} statuses",
429
+ "board.views": "Views of this board",
430
+ "board.view.kanban": "Board",
431
+ "board.view.table": "Table",
432
+ "board.view.calendar": "Calendar",
433
+ "board.view.graph": "Graph",
434
+ "board.view.gantt": "Gantt",
435
+ "board.statuses": "Statuses",
436
+ "board.statusLabel": "Status name",
437
+ "board.statusUp": "Move status up",
438
+ "board.statusDown": "Move status down",
439
+ "board.statusRemove": "Remove the status {label}",
440
+ "board.addStatus": "New status",
441
+ "board.shelfFixed": "Always the last one",
442
+ "board.showArchived": "Show archived",
443
+ "board.addTask": "New task",
444
+ "board.empty": "Nothing on this board yet. Add the first task above.",
445
+ "board.noTasks": "Nothing here that matches.",
446
+ "board.laneEmpty": "Empty",
447
+ "board.archivedEmpty": "Nothing archived.",
448
+ "board.blocked": "Blocked",
449
+ "board.open": "Open",
450
+ "board.subtaskCount": "{done}/{total} subtasks",
451
+ "board.unassigned": "Unassigned",
452
+ "board.assignee.me": "Me",
453
+ "board.assignee.unnamed": "Assigned",
454
+ "board.unknownTask": "Task not available to you",
455
+ "board.column.title": "Title",
456
+ "board.column.status": "Status",
457
+ "board.column.assignee": "Assignee",
458
+ "board.column.startDate": "Start date",
459
+ "board.column.dueDate": "Due date",
460
+ "board.column.labels": "Labels",
461
+ "board.columns": "Columns",
462
+ "board.filterTitle": "Filter by title",
463
+ "board.groupBy": "Group by",
464
+ "board.groupBy.none": "Nothing",
465
+ "board.groupBy.status": "Status",
466
+ "board.groupBy.assignee": "Assignee",
467
+ "board.toggleSubtasks": "Show or hide the subtasks of {task}",
468
+ "board.calendar.previous": "Previous month",
469
+ "board.calendar.next": "Next month",
470
+ "board.calendar.today": "Today",
471
+ "board.calendar.more": "+{count} more",
472
+ "board.calendar.undated": "{count} without a due date — not in this grid, but on the board and in the table.",
473
+ "board.gantt.scale": "Scale",
474
+ "board.gantt.scale.days": "Days",
475
+ "board.gantt.scale.weeks": "Weeks",
476
+ "board.gantt.scale.months": "Months",
477
+ "board.gantt.today": "Today",
478
+ "board.gantt.move": "Move {task}",
479
+ "board.gantt.resizeStart": "Change when {task} starts",
480
+ "board.gantt.resizeEnd": "Change when {task} is due",
481
+ "board.gantt.spanLabel": "{start} to {end}",
482
+ "board.gantt.waitsFor": "waits for {tasks}",
483
+ "board.gantt.hiddenLinks": "{count} dependencies are not drawn: the task waited for has no bar here.",
484
+ "board.gantt.undated": "Without dates",
485
+ "board.gantt.undatedCount": "{count} without dates — on the board, but on no timeline.",
486
+ "board.gantt.nothingDated": "Nothing on this board has a date yet. Give a task a due date and it appears here.",
487
+ "board.graph.subtree": "Subtree",
488
+ "board.graph.wholeBoard": "Whole board",
489
+ "board.graph.parentEdge": "contains",
490
+ "board.graph.dependsEdge": "waits for",
491
+ "board.detail": "Task",
492
+ "board.description": "Description",
493
+ "board.subtasks": "Subtasks",
494
+ "board.addSubtask": "New subtask",
495
+ "board.dependencies": "Waits for",
496
+ "board.addDependency": "Add a dependency",
497
+ "board.removeDependency": "Remove this dependency",
498
+ "board.references": "References",
499
+ "board.addReference": "Add a reference",
500
+ "board.removeReference": "Remove this reference",
501
+ "board.addLabel": "New label",
502
+ "board.removeLabel": "Remove the label {label}",
503
+ "board.deleteTask": "Delete task",
504
+ "board.deleteConfirm": "Deleting “{task}” removes it and everything below it: {count} in total. Any dependency pointing into that is cleared.",
505
+ "board.deleted": "Deleted: {count}.",
506
+ "board.archivedHint": "This task is on the archive shelf. Move it to another status to bring it back.",
507
+ "board.dnd.start": "Picked up {task}",
508
+ "board.dnd.over": "{task} is over {lane}",
509
+ "board.dnd.end": "Dropped {task} into {lane}",
510
+ "board.dnd.cancel": "Dropping {task} was cancelled",
511
+ "board.dragCard": "Drag {task}",
512
+ "board.statusTerminal": "Means done"
426
513
  }