@anchrd/intel-ui 0.47.0 → 0.49.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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@anchrd/intel-ui",
3
- "version": "0.47.0",
3
+ "version": "0.49.0",
4
4
  "type": "module",
5
5
  "license": "UNLICENSED",
6
6
  "repository": {
@@ -33,7 +33,7 @@
33
33
  "typecheck": "tsc --noEmit"
34
34
  },
35
35
  "dependencies": {
36
- "@anchrd/intel-contract": "^0.26.0",
36
+ "@anchrd/intel-contract": "^0.27.0",
37
37
  "@blocknote/core": "^0.52.1",
38
38
  "@blocknote/react": "^0.52.1",
39
39
  "@blocknote/shadcn": "^0.52.1",
@@ -26,7 +26,11 @@ interface ArchivedEntry {
26
26
  // Both sides of the tree can be purged (#457) — each through its own door, because node and flow
27
27
  // stay separate everywhere else (ADR-0004).
28
28
  purge(): Promise<{ purged: true; title: string }>;
29
- previewPurge?(): Promise<{ inboundLinks: number; totalItems: number }>;
29
+ previewPurge?(): Promise<{
30
+ inboundLinks: number;
31
+ totalItems: number;
32
+ items: Array<{ id: string; title: string }>;
33
+ }>;
30
34
  }
31
35
 
32
36
  /**
@@ -341,6 +345,23 @@ export function Archive() {
341
345
  {confirming.previewPurge && preview.data ? (
342
346
  <div className="mt-2 space-y-2 text-sm text-muted-foreground">
343
347
  <p>{i18n.t("archive.purge.items", { count: preview.data.totalItems })}</p>
348
+ {/* ⚠️ The names, not only the number (D71, #733): what is confirmed here cannot be
349
+ undone, and "82 items" is not something a person can decide about. The list the
350
+ API sends is capped, so whatever it does not carry is counted out loud. */}
351
+ {preview.data.items.length > 0 ? (
352
+ <ul className="max-h-40 list-disc overflow-y-auto pl-5">
353
+ {preview.data.items.map((entry) => (
354
+ <li key={entry.id}>{entry.title}</li>
355
+ ))}
356
+ </ul>
357
+ ) : null}
358
+ {preview.data.totalItems > preview.data.items.length ? (
359
+ <p>
360
+ {i18n.t("archive.purge.items.more", {
361
+ count: preview.data.totalItems - preview.data.items.length,
362
+ })}
363
+ </p>
364
+ ) : null}
344
365
  <p>
345
366
  {preview.data.inboundLinks === 0
346
367
  ? i18n.t("archive.purge.links.none")
@@ -2,7 +2,6 @@ import type { BoardTask } from "@anchrd/intel-contract/board";
2
2
  import { Lock, Plus, Square, SquareCheck } from "lucide-react";
3
3
  import { useState } from "react";
4
4
  import { AssigneeChip } from "@/board/board-assignee/board-assignee.tsx";
5
- import { AssigneePicker } from "@/board/board-assignee/board-assignee-picker.tsx";
6
5
  import { BoardChip } from "@/board/board-chip/board-chip.tsx";
7
6
  import type { BoardHandle } from "@/board/board-data/board-data.types.ts";
8
7
  import { DatesChip } from "@/board/board-dates/board-dates.tsx";
@@ -122,17 +121,21 @@ export function BoardTaskDocument({
122
121
  />
123
122
  ))}
124
123
 
125
- <Adder task={task} board={board} write={write} />
124
+ {/* ⚠️ **The last VALUE, and the plus comes after it** (#750, Jack's decision 2026-08-22). It
125
+ is the only round element in the row; standing between the chips it breaks the line. But
126
+ the plus is not a value, it is the control that adds one, and a control belongs behind
127
+ everything it can add to.
126
128
 
127
- {/* ⚠️ **Last, always, and after the plus** (#724). It is the only round element in the row;
128
- standing between the chips it breaks the line. And it is drawn even when nobody has the
129
- card: an empty circle is the one control on a row that says the card COULD be given to
130
- somebody, which is precisely what an unowned card needs. */}
129
+ ⚠️ Drawn even when nobody has the card: an empty circle is the one control on the row
130
+ that says the card COULD be given to somebody, which is precisely what an unowned card
131
+ needs (#724). */}
131
132
  <AssigneeChip
132
133
  id={task.assigneeId}
133
134
  boardId={board.boardId}
134
135
  onPick={(assigneeId) => write({ assigneeId })}
135
136
  />
137
+
138
+ <Adder task={task} board={board} write={write} />
136
139
  </div>
137
140
 
138
141
  {/* ⚠️ ONE list, not three blocks: the icon carries the kind. And it exists only when there is
@@ -178,10 +181,11 @@ export function BoardTaskDocument({
178
181
  * is no list to choose from anywhere in the data provider. An entry that opens nothing is worse than
179
182
  * a missing one, because it looks like the feature is there (`#700`).
180
183
  */
181
- // ⚠️ `assignee` is first, and that is Jacks Vorgabe 2026-08-21 read back into the menu: on a card
182
- // the assignee chip stands LAST so the round element does not break the row, while in the menu the
183
- // most-used entry stands first. The two orders answer different questions and are not a mismatch.
184
- const addable = ["assignee", "label", "start", "due"] as const;
184
+ // ⚠️ **No `assignee` here** (#750). It stood first once, back when the circle was drawn only for
185
+ // somebody and an unowned card had nothing to press. Since #724 the circle is always there and opens
186
+ // the same picker, so an entry here would be a second way to the same thing, two lines apart — and
187
+ // the one somebody finds first would depend on where they happened to look.
188
+ const addable = ["label", "start", "due"] as const;
185
189
  const linkable = ["subtask", "blocker"] as const;
186
190
  type Addable = (typeof addable)[number] | (typeof linkable)[number];
187
191
 
@@ -271,27 +275,7 @@ function Adder({
271
275
  </DropdownMenuContent>
272
276
  </DropdownMenu>
273
277
 
274
- {kind !== "assignee" ? null : (
275
- <AssigneePicker
276
- boardId={board.boardId}
277
- current={task.assigneeId}
278
- onPick={(assigneeId) => write({ assigneeId })}
279
- open
280
- onOpenChange={(next) => !next && close()}
281
- >
282
- {/* ⚠️ The anchor. Opened from the plus menu there is nothing on screen to hang the menu
283
- on, so the picker brings its own pill — the same shape the blocker branch below uses,
284
- and for the same reason. */}
285
- <button
286
- type="button"
287
- className="rounded-full border px-2.5 py-0.5 text-xs outline-none focus-visible:ring-2 focus-visible:ring-ring"
288
- >
289
- {i18n.t("board.add.assignee")}
290
- </button>
291
- </AssigneePicker>
292
- )}
293
-
294
- {kind === null || kind === "blocker" || kind === "assignee" ? null : (
278
+ {kind === null || kind === "blocker" ? null : (
295
279
  <input
296
280
  // biome-ignore lint/a11y/noAutofocus: it opens on a deliberate click, never on load
297
281
  autoFocus
package/src/feed/feed.tsx CHANGED
@@ -18,6 +18,21 @@ import { initials, useSessionUser } from "@/user-name/user-name.ts";
18
18
 
19
19
  const PageSize = 30;
20
20
 
21
+ // How many steps of the trail a card shows.
22
+ //
23
+ // ⚠️ The LAST ones, not the first, and that is the whole decision (#758). A path answers "where does
24
+ // this sit", and the answer is at its end: `… / system / Kundenwissen` says where `Kunde A` lives,
25
+ // `Agenten / Anton Anchrd / sys…` says where the tree begins, which every card shares. Cutting from
26
+ // the front is what `truncate` does on its own, so this has to be decided here rather than left to
27
+ // CSS.
28
+ const TrailSteps = 2;
29
+
30
+ /** The tail of a trail, with a leading ellipsis where something was cut. */
31
+ export function shortTrail(titles: readonly string[]): string {
32
+ if (titles.length <= TrailSteps) return titles.join(" / ");
33
+ return `… / ${titles.slice(-TrailSteps).join(" / ")}`;
34
+ }
35
+
21
36
  // Who to show. Only the first two reach the server; `people` is applied here because whether an
22
37
  // actor is a machine is Gate's answer and arrives with the names, not with the events.
23
38
  type Audience = "all" | "mine" | "people";
@@ -280,9 +295,17 @@ function FeedCard({
280
295
  <span className="text-sm leading-snug">
281
296
  {i18n.t(`feed.action.${event.action}`, { who, title: event.nodeTitle })}
282
297
  </span>
283
- <span className="flex items-center justify-between gap-3">
284
- <span className="truncate text-xs text-muted-foreground">
285
- {event.path.map((step) => step.title).join(" / ")}
298
+ {/* ⚠️ `min-w-0` is not decoration. A grid item defaults to `min-width: auto`, so without it
299
+ this row grows to whatever the path needs and pushes the button PAST the card's edge —
300
+ which is what a four-step path did on a phone. `truncate` below cannot save it either: it
301
+ shortens against the width it is given, and its parent was already too wide. */}
302
+ <span className="flex min-w-0 items-center justify-between gap-3">
303
+ <span
304
+ className="truncate text-xs text-muted-foreground"
305
+ // The whole path stays reachable where there is a pointer to rest.
306
+ title={event.path.map((step) => step.title).join(" / ")}
307
+ >
308
+ {shortTrail(event.path.map((step) => step.title))}
286
309
  </span>
287
310
  {/* ⚠️ A button, not a label. The whole point of the mark is getting there, and a span
288
311
  carries no focus — so the keyboard requirement could not be met by a surface that had
package/src/i18n/de.json CHANGED
@@ -11,6 +11,7 @@
11
11
  "archive.purge.confirm": "Endgültig löschen",
12
12
  "archive.purge.grants": "Die Freigaben werden ebenfalls gelöscht.",
13
13
  "archive.purge.items": "Insgesamt endgültig gelöscht: {count}.",
14
+ "archive.purge.items.more": "Hier nicht aufgeführt: {count}.",
14
15
  "archive.purge.links.many": "{count} andere Dokumente verweisen darauf; diese Verweise werden brechen.",
15
16
  "archive.purge.links.none": "Keine anderen Dokumente verweisen darauf.",
16
17
  "archive.purge.links.one": "Ein anderes Dokument verweist darauf; dieser Verweis wird brechen.",
@@ -76,7 +77,6 @@
76
77
  "auth.signOut": "Abmelden",
77
78
  "auth.signOutFailed": "Das Abmelden ist fehlgeschlagen. Prüfe deine Verbindung und versuche es erneut.",
78
79
  "board.add": "Etwas hinzufügen",
79
- "board.add.assignee": "Zuständigkeit",
80
80
  "board.add.due": "Fällig",
81
81
  "board.add.label": "Label",
82
82
  "board.add.start": "Start",
package/src/i18n/en.json CHANGED
@@ -11,6 +11,7 @@
11
11
  "archive.purge.confirm": "Delete for good",
12
12
  "archive.purge.grants": "Its shares will also be deleted.",
13
13
  "archive.purge.items": "Deleted for good in total: {count}.",
14
+ "archive.purge.items.more": "Not listed here: {count}.",
14
15
  "archive.purge.links.many": "{count} other documents link to it and those links will break.",
15
16
  "archive.purge.links.none": "No other documents link to it.",
16
17
  "archive.purge.links.one": "One other document links to it and that link will break.",
@@ -76,7 +77,6 @@
76
77
  "auth.signOut": "Sign out",
77
78
  "auth.signOutFailed": "Signing out failed. Check your connection and try again.",
78
79
  "board.add": "Add something",
79
- "board.add.assignee": "Assignee",
80
80
  "board.add.due": "Due",
81
81
  "board.add.label": "Label",
82
82
  "board.add.start": "Start",
package/src/i18n/es.json CHANGED
@@ -11,6 +11,7 @@
11
11
  "archive.purge.confirm": "Eliminar definitivamente",
12
12
  "archive.purge.grants": "Sus permisos compartidos también se eliminarán.",
13
13
  "archive.purge.items": "Eliminado definitivamente en total: {count}.",
14
+ "archive.purge.items.more": "No se enumeran aquí: {count}.",
14
15
  "archive.purge.links.many": "Otros {count} documentos contienen enlaces a este elemento y dejarán de funcionar.",
15
16
  "archive.purge.links.none": "Ningún otro documento contiene un enlace a este elemento.",
16
17
  "archive.purge.links.one": "Otro documento contiene un enlace a este elemento y ese enlace dejará de funcionar.",
@@ -76,7 +77,6 @@
76
77
  "auth.signOut": "Cerrar sesión",
77
78
  "auth.signOutFailed": "El cierre de sesión ha fallado. Comprueba tu conexión e inténtalo de nuevo.",
78
79
  "board.add": "Añadir algo",
79
- "board.add.assignee": "Responsable",
80
80
  "board.add.due": "Vencimiento",
81
81
  "board.add.label": "Etiqueta",
82
82
  "board.add.start": "Inicio",