@anchrd/intel-ui 0.18.0 → 0.20.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 (78) hide show
  1. package/package.json +2 -9
  2. package/src/app/app-tree/app-tree.tsx +68 -147
  3. package/src/app/header-search/header-search.tsx +2 -1
  4. package/src/app/reindex-dialog/reindex-dialog.tsx +79 -0
  5. package/src/app/settings-dialog/settings-dialog.tsx +10 -32
  6. package/src/app/user-footer/user-footer.tsx +19 -0
  7. package/src/archive/archive.tsx +2 -1
  8. package/src/data/intel-data-provider/intel-data-provider.ts +42 -191
  9. package/src/data/intel-data-provider/intel-data-provider.types.ts +36 -120
  10. package/src/document-link/document-link.tsx +1 -1
  11. package/src/entry-picker/entry-picker.tsx +7 -7
  12. package/src/flow-runs/flow-runs.tsx +1 -1
  13. package/src/flows/flows.tsx +3 -2
  14. package/src/flows/node-icon/node-icon.ts +1 -1
  15. package/src/flows/node-palette/node-palette.tsx +1 -1
  16. package/src/flows/node-palette/node-palette.types.ts +1 -1
  17. package/src/graph-pane/graph-pane.tsx +1 -1
  18. package/src/i18n/de.json +14 -217
  19. package/src/i18n/en.json +14 -217
  20. package/src/i18n/es.json +14 -217
  21. package/src/import-bundle/import-bundle.tsx +152 -0
  22. package/src/kind-icon.ts +1 -15
  23. package/src/main.tsx +7 -10
  24. package/src/node-editor/node-editor.tsx +1 -1
  25. package/src/node-graph/graph-notice.tsx +1 -1
  26. package/src/node-graph/node-graph.ts +1 -1
  27. package/src/node-graph/node-graph.tsx +1 -1
  28. package/src/node-table/node-table.tsx +3 -3
  29. package/src/nodes/nodes.tsx +1 -35
  30. package/src/resource-menu/resource-menu.tsx +45 -21
  31. package/src/section-hint/section-hint.tsx +2 -2
  32. package/src/title-row/title-row.tsx +5 -22
  33. package/src/tools/tools.tsx +1 -3
  34. package/vite.config.ts +0 -4
  35. package/src/agent/agent-avatar/agent-avatar.tsx +0 -34
  36. package/src/agent/agent-calendar/agent-calendar.tsx +0 -160
  37. package/src/agent/agent-chat/agent-chat.tsx +0 -118
  38. package/src/agent/agent-costs/agent-costs.ts +0 -95
  39. package/src/agent/agent-cron/agent-cron.ts +0 -68
  40. package/src/agent/agent-definition/agent-definition.ts +0 -96
  41. package/src/agent/agent-delegation-notice/agent-delegation-notice.ts +0 -48
  42. package/src/agent/agent-entry-title/agent-entry-title.ts +0 -72
  43. package/src/agent/agent-log/agent-log.tsx +0 -308
  44. package/src/agent/agent-models/agent-models.ts +0 -166
  45. package/src/agent/agent-models/agent-models.types.ts +0 -24
  46. package/src/agent/agent-profile/agent-profile.tsx +0 -1006
  47. package/src/agent/agent-state/agent-state.ts +0 -85
  48. package/src/agent/agent-status-dot/agent-status-dot.ts +0 -73
  49. package/src/agent/agent.tsx +0 -507
  50. package/src/board/board-calendar/board-calendar.tsx +0 -89
  51. package/src/board/board-card/board-card.tsx +0 -106
  52. package/src/board/board-data/board-data.ts +0 -241
  53. package/src/board/board-data/board-data.types.ts +0 -63
  54. package/src/board/board-detail/board-detail.tsx +0 -629
  55. package/src/board/board-gantt/board-gantt.ts +0 -545
  56. package/src/board/board-gantt/board-gantt.tsx +0 -286
  57. package/src/board/board-graph/board-graph.ts +0 -174
  58. package/src/board/board-graph/board-graph.tsx +0 -168
  59. package/src/board/board-items/board-items.ts +0 -183
  60. package/src/board/board-kanban/board-kanban.ts +0 -137
  61. package/src/board/board-kanban/board-kanban.tsx +0 -257
  62. package/src/board/board-status/board-status.ts +0 -59
  63. package/src/board/board-statuses/board-statuses.ts +0 -63
  64. package/src/board/board-statuses/board-statuses.tsx +0 -228
  65. package/src/board/board-table/board-table.ts +0 -33
  66. package/src/board/board-table/board-table.tsx +0 -413
  67. package/src/board/board-views/board-views.tsx +0 -68
  68. package/src/board/board-views/board-views.types.ts +0 -29
  69. package/src/board/board.tsx +0 -251
  70. package/src/components/ui/item-calendar.tsx +0 -181
  71. package/src/components/ui/item-gantt.tsx +0 -463
  72. package/src/components/ui/kanban.tsx +0 -282
  73. package/src/data/agent-runtime/agent-runtime.ts +0 -131
  74. package/src/hooks/use-capabilities.ts +0 -22
  75. package/src/hooks/use-model-catalog.ts +0 -26
  76. package/src/timezone/timezone-combobox/timezone-combobox.tsx +0 -149
  77. package/src/timezone/timezone-context.tsx +0 -65
  78. package/src/timezone/timezone.ts +0 -174
@@ -1,413 +0,0 @@
1
- import {
2
- type ColumnFiltersState,
3
- type ColumnVisibilityState,
4
- columnFilteringFeature,
5
- columnGroupingFeature,
6
- columnVisibilityFeature,
7
- createColumnHelper,
8
- createExpandedRowModel,
9
- createFilteredRowModel,
10
- createGroupedRowModel,
11
- createSortedRowModel,
12
- type ExpandedState,
13
- filterFn_includesString,
14
- type GroupingState,
15
- rowExpandingFeature,
16
- rowSortingFeature,
17
- type SortingState,
18
- sortFn_alphanumeric,
19
- sortFn_basic,
20
- tableFeatures,
21
- useTable,
22
- } from "@tanstack/react-table";
23
- import { ChevronDown, ChevronRight, Columns3, Search } from "lucide-react";
24
- import { useMemo, useState } from "react";
25
- import { BoardAssigneeLabel } from "@/board/board-card/board-card.tsx";
26
- import { shownTasks } from "@/board/board-data/board-data.ts";
27
- import { labelOf, toneOf } from "@/board/board-status/board-status.ts";
28
- import type { BoardViewProps } from "@/board/board-views/board-views.types.ts";
29
- import {
30
- DropdownMenu,
31
- DropdownMenuCheckboxItem,
32
- DropdownMenuContent,
33
- DropdownMenuTrigger,
34
- } from "@/components/ui/dropdown-menu";
35
- import { useI18n } from "@/i18n/i18n-context.tsx";
36
- import { type BoardRow, boardRows } from "./board-table.ts";
37
-
38
- // ⚠️ Registered one by one rather than through `stockFeatures`. Every feature the board's table does
39
- // not use — pagination, selection, pinning, resizing, faceting — would otherwise be in the bundle
40
- // of a screen that never calls it, and the list below is also the honest answer to "what can this
41
- // table do".
42
- const features = tableFeatures({
43
- columnFilteringFeature,
44
- columnGroupingFeature,
45
- columnVisibilityFeature,
46
- rowExpandingFeature,
47
- rowSortingFeature,
48
- // ⚠️ A row-model slot has to stand AFTER the feature it needs. Filtering before grouping and
49
- // expanding last is the order the rows actually pass through.
50
- filteredRowModel: createFilteredRowModel(),
51
- sortedRowModel: createSortedRowModel(),
52
- groupedRowModel: createGroupedRowModel(),
53
- expandedRowModel: createExpandedRowModel(),
54
- filterFns: { includesString: filterFn_includesString },
55
- sortFns: { alphanumeric: sortFn_alphanumeric, basic: sortFn_basic },
56
- });
57
-
58
- const helper = createColumnHelper<typeof features, BoardRow>();
59
-
60
- // The one place a grouping is offered. Not free-form: a board has fixed fields (#285) and there is
61
- // no UI to add one, so grouping by anything else would be grouping by something nobody can create.
62
- const groupBy = ["none", "status", "assignee"] as const;
63
- type GroupBy = (typeof groupBy)[number];
64
-
65
- /**
66
- * The board as a table (anchrd/intel#286).
67
- *
68
- * ⚠️ The hierarchy is `getSubRows` and nothing else. Epic, task and subtask are a DEPTH in this
69
- * schema and not three types (#285), so the table has one row shape and indents it by
70
- * `row.depth` — there is no second column saying which of the three a row is, because the schema
71
- * has no such fact to show.
72
- *
73
- * ⚠️ The columns are fixed and there is no UI to add one. A board is not a table node (`kind:
74
- * "table"`), and a custom-field editor here would be a second, weaker version of the thing Intel
75
- * already has for exactly that.
76
- */
77
- export function BoardTable({ board, select, showArchived }: BoardViewProps) {
78
- const i18n = useI18n();
79
- const [sorting, setSorting] = useState<SortingState>([]);
80
- const [columnFilters, setColumnFilters] = useState<ColumnFiltersState>([]);
81
- const [columnVisibility, setColumnVisibility] = useState<ColumnVisibilityState>({});
82
- const [expanded, setExpanded] = useState<ExpandedState>(true);
83
- const [grouping, setGrouping] = useState<GroupingState>([]);
84
-
85
- const data = useMemo(
86
- () => boardRows(shownTasks(board.tasks, showArchived)),
87
- [board.tasks, showArchived],
88
- );
89
-
90
- const columns = useMemo(
91
- () =>
92
- helper.columns([
93
- helper.accessor("title", {
94
- id: "title",
95
- header: i18n.t("common.title"),
96
- filterFn: "includesString",
97
- sortFn: "alphanumeric",
98
- // Grouping by the title would make one group per row, which is the table again with an
99
- // extra click in front of it.
100
- enableGrouping: false,
101
- }),
102
- helper.accessor((task) => labelOf(task.status, board.statuses), {
103
- id: "status",
104
- header: i18n.t("board.column.status"),
105
- sortFn: "alphanumeric",
106
- }),
107
- helper.accessor(
108
- // ⚠️ Grouped by a STABLE key, never by the rendered name. Most person ids cannot be
109
- // resolved to a name in this browser (`useUserName`), so grouping by what is drawn would
110
- // put every unnamed person into one group called "assigned".
111
- (task) =>
112
- task.assignee === null
113
- ? ""
114
- : task.assignee.type === "agent"
115
- ? `agent:${task.assignee.nodeId}`
116
- : `user:${task.assignee.id}`,
117
- {
118
- id: "assignee",
119
- header: i18n.t("board.column.assignee"),
120
- sortFn: "alphanumeric",
121
- cell: ({ row }) =>
122
- row.original.assignee === null ? (
123
- <span className="text-muted-foreground">{i18n.t("board.unassigned")}</span>
124
- ) : (
125
- <BoardAssigneeLabel assignee={row.original.assignee} />
126
- ),
127
- },
128
- ),
129
- /**
130
- * ⚠️ `undefined` for a task with no due date, and never `null`.
131
- *
132
- * `sortUndefined` only fires on `undefined` (`createSortedRowModel`), so with `null` the
133
- * dateless rows fall through to the sort function — where `null < "2026-08-19"` and
134
- * `null > "2026-08-19"` are both false. That makes the comparator report "equal" for pairs
135
- * that are not, which is not a comparator at all: the sort then produces an order that
136
- * depends on where the dateless rows happened to sit. Observed as a "sorted" column reading
137
- * 8/6, 8/19, —, 8/14.
138
- *
139
- * An ISO date sorts correctly as a string, so `basic` is the whole of the comparison.
140
- */
141
- helper.accessor((task) => task.dueDate ?? undefined, {
142
- id: "dueDate",
143
- header: i18n.t("board.column.dueDate"),
144
- sortFn: "basic",
145
- sortUndefined: "last",
146
- enableGrouping: false,
147
- cell: ({ row }) => {
148
- const value = row.original.dueDate;
149
- return value === null ? (
150
- <span className="text-muted-foreground">—</span>
151
- ) : (
152
- <time dateTime={value} className="tabular-nums">
153
- {new Date(`${value}T00:00:00`).toLocaleDateString(i18n.locale)}
154
- </time>
155
- );
156
- },
157
- }),
158
- helper.accessor((task) => task.labels.join(", "), {
159
- id: "labels",
160
- header: i18n.t("board.column.labels"),
161
- filterFn: "includesString",
162
- enableGrouping: false,
163
- enableSorting: false,
164
- }),
165
- ]),
166
- [i18n, board.statuses],
167
- );
168
-
169
- const table = useTable({
170
- features,
171
- columns,
172
- data,
173
- // The hierarchy the DoD asks for, in one option. Everything else about depth — the indent, the
174
- // chevron, which rows exist at all — follows from it.
175
- getSubRows: (row: BoardRow) => row.subRows,
176
- getRowId: (row: BoardRow) => row.id,
177
- state: { sorting, columnFilters, columnVisibility, expanded, grouping },
178
- onSortingChange: setSorting,
179
- onColumnFiltersChange: setColumnFilters,
180
- onColumnVisibilityChange: setColumnVisibility,
181
- onExpandedChange: setExpanded,
182
- onGroupingChange: setGrouping,
183
- /**
184
- * ⚠️ Off, and it has to be off.
185
- *
186
- * TanStack resets the expanded state whenever the row structure changes, and on a board the row
187
- * structure changes constantly: every card edit, every drag and every archived-switch flip
188
- * hands `data` a new array. With the reset on, opening an epic and then renaming anything on the
189
- * board closes it again — and turning grouping on collapsed every group the moment it appeared,
190
- * which is how this was found (the groups in the first screenshot were shut).
191
- */
192
- autoResetExpanded: false,
193
- });
194
-
195
- const currentGrouping: GroupBy = (grouping[0] as GroupBy) ?? "none";
196
- const titleFilter = String(table.getColumn("title")?.getFilterValue() ?? "");
197
-
198
- return (
199
- <div className="flex min-h-0 flex-1 flex-col gap-3 p-6">
200
- <div className="flex flex-wrap items-center gap-2">
201
- <label className="relative">
202
- <span className="sr-only">{i18n.t("board.filterTitle")}</span>
203
- <Search
204
- aria-hidden="true"
205
- className="pointer-events-none absolute top-1/2 left-2.5 size-4 -translate-y-1/2 text-muted-foreground"
206
- />
207
- <input
208
- type="search"
209
- value={titleFilter}
210
- placeholder={i18n.t("board.filterTitle")}
211
- onChange={(event) =>
212
- table.getColumn("title")?.setFilterValue(event.currentTarget.value || undefined)
213
- }
214
- className="h-8 w-56 rounded-md border bg-background pr-3 pl-8 text-sm outline-none focus-visible:ring-2 focus-visible:ring-ring"
215
- />
216
- </label>
217
- <label className="flex items-center gap-2 text-sm">
218
- <span className="text-muted-foreground">{i18n.t("board.groupBy")}</span>
219
- <select
220
- value={currentGrouping}
221
- onChange={(event) => {
222
- const next = event.currentTarget.value as GroupBy;
223
- setGrouping(next === "none" ? [] : [next]);
224
- // A group that opens closed is a screen with nothing on it. Regrouping therefore
225
- // expands again, which is also the state the table starts in.
226
- setExpanded(true);
227
- }}
228
- className="h-8 rounded-md border bg-background px-2 text-sm outline-none focus-visible:ring-2 focus-visible:ring-ring"
229
- >
230
- {groupBy.map((option) => (
231
- <option key={option} value={option}>
232
- {i18n.t(`board.groupBy.${option}`)}
233
- </option>
234
- ))}
235
- </select>
236
- </label>
237
- <DropdownMenu>
238
- <DropdownMenuTrigger className="ml-auto inline-flex h-8 items-center gap-2 rounded-md border px-3 text-sm outline-none hover:bg-muted focus-visible:ring-2 focus-visible:ring-ring">
239
- <Columns3 aria-hidden="true" className="size-4" />
240
- {i18n.t("board.columns")}
241
- </DropdownMenuTrigger>
242
- <DropdownMenuContent align="end">
243
- {table.getAllLeafColumns().map((column) => (
244
- <DropdownMenuCheckboxItem
245
- key={column.id}
246
- checked={column.getIsVisible()}
247
- onCheckedChange={(value) => column.toggleVisibility(Boolean(value))}
248
- // The last visible column cannot be hidden: an empty table is a screen that has
249
- // lost its data as far as anybody looking at it can tell.
250
- disabled={column.getIsVisible() && table.getVisibleLeafColumns().length === 1}
251
- >
252
- {i18n.t(`board.column.${column.id === "title" ? "title" : column.id}`)}
253
- </DropdownMenuCheckboxItem>
254
- ))}
255
- </DropdownMenuContent>
256
- </DropdownMenu>
257
- </div>
258
- <div className="min-h-0 flex-1 overflow-auto rounded-lg border">
259
- <table className="w-full border-collapse text-sm">
260
- <thead className="sticky top-0 z-10 bg-card">
261
- {table.getHeaderGroups().map((group) => (
262
- <tr key={group.id}>
263
- {group.headers.map((header) => (
264
- <th
265
- key={header.id}
266
- scope="col"
267
- aria-sort={
268
- header.column.getIsSorted() === "asc"
269
- ? "ascending"
270
- : header.column.getIsSorted() === "desc"
271
- ? "descending"
272
- : "none"
273
- }
274
- className="border-b px-3 py-2 text-left font-medium whitespace-nowrap"
275
- >
276
- {header.isPlaceholder ? null : header.column.getCanSort() ? (
277
- <button
278
- type="button"
279
- onClick={header.column.getToggleSortingHandler()}
280
- className="inline-flex items-center gap-1 rounded outline-none hover:underline focus-visible:ring-2 focus-visible:ring-ring"
281
- >
282
- <table.FlexRender header={header} />
283
- <span aria-hidden="true" className="text-muted-foreground">
284
- {header.column.getIsSorted() === "asc"
285
- ? "↑"
286
- : header.column.getIsSorted() === "desc"
287
- ? "↓"
288
- : ""}
289
- </span>
290
- </button>
291
- ) : (
292
- <table.FlexRender header={header} />
293
- )}
294
- </th>
295
- ))}
296
- </tr>
297
- ))}
298
- </thead>
299
- <tbody>
300
- {table.getRowModel().rows.map((row) => (
301
- <tr key={row.id} className="border-b last:border-b-0 hover:bg-muted/60">
302
- {row.getVisibleCells().map((cell) => (
303
- <td key={cell.id} className="px-3 py-2 align-top">
304
- {/* ⚠️ A group row carries the grouped value and NOTHING else. Left to render
305
- itself, every other cell of a group row falls back to the first task in the
306
- group — so the heading "Backlog 3" came with one arbitrary member's title
307
- and its date beside it, which reads as a row of its own and is a lie about
308
- the two below it. */}
309
- {row.getIsGrouped() && !cell.getIsGrouped() ? null : cell.getIsGrouped() ? (
310
- <button
311
- type="button"
312
- onClick={row.getToggleExpandedHandler()}
313
- aria-expanded={row.getIsExpanded()}
314
- className="inline-flex items-center gap-2 rounded font-medium outline-none focus-visible:ring-2 focus-visible:ring-ring"
315
- >
316
- {row.getIsExpanded() ? (
317
- <ChevronDown aria-hidden="true" className="size-4" />
318
- ) : (
319
- <ChevronRight aria-hidden="true" className="size-4" />
320
- )}
321
- <GroupLabel row={row} columnId={cell.column.id} value={cell.getValue()} />
322
- <span className="text-xs text-muted-foreground tabular-nums">
323
- {row.subRows.length}
324
- </span>
325
- </button>
326
- ) : cell.getIsPlaceholder() ? null : cell.column.id === "title" ? (
327
- <div
328
- className="flex min-w-0 items-start gap-1.5"
329
- // ⚠️ The indent is a style and not a nested table. A row nested in markup
330
- // could not be sorted or grouped with its siblings, and a screen reader
331
- // would read four tables where there is one.
332
- style={{ paddingInlineStart: `${row.depth * 1.25}rem` }}
333
- >
334
- {row.getCanExpand() ? (
335
- <button
336
- type="button"
337
- onClick={row.getToggleExpandedHandler()}
338
- aria-expanded={row.getIsExpanded()}
339
- aria-label={i18n.t("board.toggleSubtasks", {
340
- task: row.original.title,
341
- })}
342
- className="mt-0.5 rounded outline-none focus-visible:ring-2 focus-visible:ring-ring"
343
- >
344
- {row.getIsExpanded() ? (
345
- <ChevronDown aria-hidden="true" className="size-4" />
346
- ) : (
347
- <ChevronRight aria-hidden="true" className="size-4" />
348
- )}
349
- </button>
350
- ) : (
351
- <span aria-hidden="true" className="size-4 shrink-0" />
352
- )}
353
- <button
354
- type="button"
355
- onClick={() => select(row.original.id)}
356
- className="min-w-0 rounded text-left outline-none hover:underline focus-visible:ring-2 focus-visible:ring-ring"
357
- >
358
- {row.original.title}
359
- </button>
360
- {board.blockedBy(row.original).length > 0 ? (
361
- <span className="shrink-0 rounded-md border border-destructive px-1 text-xs text-destructive">
362
- {i18n.t("board.blocked")}
363
- </span>
364
- ) : null}
365
- </div>
366
- ) : cell.column.id === "status" ? (
367
- <span className="inline-flex items-center gap-1.5 whitespace-nowrap">
368
- <span
369
- aria-hidden="true"
370
- className={`size-2 rounded-full ${toneOf(row.original.status, board.statuses).dot}`}
371
- />
372
- <table.FlexRender cell={cell} />
373
- </span>
374
- ) : (
375
- <table.FlexRender cell={cell} />
376
- )}
377
- </td>
378
- ))}
379
- </tr>
380
- ))}
381
- </tbody>
382
- </table>
383
- {table.getRowModel().rows.length === 0 ? (
384
- <p className="p-6 text-sm text-muted-foreground">{i18n.t("board.noTasks")}</p>
385
- ) : null}
386
- </div>
387
- </div>
388
- );
389
- }
390
-
391
- /**
392
- * The value a group was formed on, said in words.
393
- *
394
- * ⚠️ The assignee column groups on a STABLE key (`user:…`, `agent:…`) and that key is not a name.
395
- * The heading is therefore read off the first row of the group rather than off the key — that is
396
- * the only place an agent's title can be resolved, and the only place a person who cannot be named
397
- * is named honestly rather than by their id.
398
- */
399
- function GroupLabel({
400
- row,
401
- columnId,
402
- value,
403
- }: {
404
- row: { subRows: { original: BoardRow }[] };
405
- columnId: string;
406
- value: unknown;
407
- }) {
408
- const i18n = useI18n();
409
- if (columnId !== "assignee") return <span>{String(value ?? "")}</span>;
410
- const assignee = row.subRows[0]?.original.assignee ?? null;
411
- if (assignee === null) return <span>{i18n.t("board.unassigned")}</span>;
412
- return <BoardAssigneeLabel assignee={assignee} />;
413
- }
@@ -1,68 +0,0 @@
1
- import { CalendarDays, ChartGantt, KanbanSquare, Share2, Table2 } from "lucide-react";
2
- import { lazy, Suspense } from "react";
3
- import { BoardCalendar } from "@/board/board-calendar/board-calendar.tsx";
4
- import { BoardGantt } from "@/board/board-gantt/board-gantt.tsx";
5
- import { BoardKanban } from "@/board/board-kanban/board-kanban.tsx";
6
- import { BoardTable } from "@/board/board-table/board-table.tsx";
7
- import type { BoardView, BoardViewProps } from "./board-views.types.ts";
8
-
9
- // Sigma and graphology are the heaviest thing on this screen and only one view needs them, the
10
- // same arrangement `graph-pane` already has for the relation graph.
11
- const BoardGraph = lazy(async () => ({
12
- default: (await import("@/board/board-graph/board-graph.tsx")).BoardGraph,
13
- }));
14
-
15
- /**
16
- * Which views a board has (anchrd/intel#286).
17
- *
18
- * ⚠️ A LIST, and the switch above it renders whatever is in the list — there is deliberately no
19
- * `switch (view)` with one case per view anywhere. Gantt (anchrd/intel#293) proved it: it is one
20
- * more entry here and nothing else, reading the same `BoardHandle` and the same dated items
21
- * (`board-items.ts`), so the work it cost was a renderer. Hard-wired cases would have made it a
22
- * change to the screen, the switch, the URL parser and the tests instead.
23
- *
24
- * ⚠️ The order is the order the switch draws. Kanban first because it is what a board is opened
25
- * for; the graph last because it answers a question one has after looking, not before.
26
- */
27
- export const boardViews: readonly BoardView[] = [
28
- {
29
- id: "kanban",
30
- labelKey: "board.view.kanban",
31
- icon: KanbanSquare,
32
- render: (props: BoardViewProps) => <BoardKanban {...props} />,
33
- },
34
- {
35
- id: "table",
36
- labelKey: "board.view.table",
37
- icon: Table2,
38
- render: (props: BoardViewProps) => <BoardTable {...props} />,
39
- },
40
- {
41
- id: "calendar",
42
- labelKey: "board.view.calendar",
43
- icon: CalendarDays,
44
- render: (props: BoardViewProps) => <BoardCalendar {...props} />,
45
- },
46
- {
47
- id: "gantt",
48
- labelKey: "board.view.gantt",
49
- icon: ChartGantt,
50
- render: (props: BoardViewProps) => <BoardGantt {...props} />,
51
- },
52
- {
53
- id: "graph",
54
- labelKey: "board.view.graph",
55
- icon: Share2,
56
- render: (props: BoardViewProps) => (
57
- <Suspense fallback={null}>
58
- <BoardGraph {...props} />
59
- </Suspense>
60
- ),
61
- },
62
- ];
63
-
64
- export function boardViewById(id: string | null): BoardView {
65
- // The first entry is the fallback rather than a named one: a view removed from the list must not
66
- // leave a screen pointing at nothing, and an unknown id in a bookmarked URL is the same case.
67
- return boardViews.find((view) => view.id === id) ?? (boardViews[0] as BoardView);
68
- }
@@ -1,29 +0,0 @@
1
- import type { LucideIcon } from "lucide-react";
2
- import type { ReactNode } from "react";
3
- import type { BoardHandle } from "@/board/board-data/board-data.types.ts";
4
-
5
- /**
6
- * Everything a view of a board gets, and the whole of it.
7
- *
8
- * ⚠️ It carries no loader and no writer of its own — `board` is the one data layer, and a view that
9
- * reached past it would be a second cache entry for the same board.
10
- */
11
- export interface BoardViewProps {
12
- board: BoardHandle;
13
- // How a view asks for the detail panel. `null` closes it. There is deliberately no "which task is
14
- // open" alongside it: no view highlights the selection today, and a prop threaded through four
15
- // renderers that none of them reads is a prop that will be wrong before it is used.
16
- select(taskId: string | null): void;
17
- // Whether the shelf is on screen. One switch above every view rather than one per view: it is a
18
- // question about the board, not about how the board is drawn.
19
- showArchived: boolean;
20
- }
21
-
22
- export interface BoardView {
23
- // What stands in the URL and in the switch. A view keeps its id forever; a bookmarked board must
24
- // not open on a different view because a label was rewritten.
25
- id: string;
26
- labelKey: string;
27
- icon: LucideIcon;
28
- render(props: BoardViewProps): ReactNode;
29
- }