@anchrd/intel-ui 0.36.0 → 0.38.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/src/kind-icon.ts CHANGED
@@ -1,4 +1,13 @@
1
- import { FileText, Folder, type LucideIcon, Paperclip, Table, Workflow } from "lucide-react";
1
+ import {
2
+ CircleCheck,
3
+ FileText,
4
+ Folder,
5
+ KanbanSquare,
6
+ type LucideIcon,
7
+ Paperclip,
8
+ Table,
9
+ Workflow,
10
+ } from "lucide-react";
2
11
  import type { TreeEntry } from "@/data/intel-data-provider/intel-data-provider.types.ts";
3
12
 
4
13
  /**
@@ -16,4 +25,11 @@ export const kindIcons: Record<TreeEntry["kind"], LucideIcon> = {
16
25
  attachment: Paperclip,
17
26
  table: Table,
18
27
  flow: Workflow,
28
+ // ⚠️ A board draws as a board, never as a folder, although it behaves like one in the tree. The
29
+ // icon is the ONLY thing that tells a reader the row will not open (D66, #376) — the chevron is
30
+ // absent, and an absent chevron looks exactly like an empty folder.
31
+ board: KanbanSquare,
32
+ // A task has an icon although the tree never draws one: the folder table, search results and the
33
+ // relation graph all reach a task by its own address, and each of them asks this map.
34
+ task: CircleCheck,
19
35
  };
@@ -14,13 +14,14 @@ import { ActionSlot } from "@/app/action-slot/action-slot.tsx";
14
14
  import { BlockNoteView, defaultSlashMenuItems } from "@/blocknote-view/blocknote-view.tsx";
15
15
  import { IntelRequestError } from "@/data/intel-data-provider/intel-data-provider.ts";
16
16
  import type { IntelDataProvider } from "@/data/intel-data-provider/intel-data-provider.types.ts";
17
+ import { DocumentLinkProvider, documentLinkIds } from "@/document-link/document-link.tsx";
17
18
  import {
18
- DocumentLinkProvider,
19
- documentLinkIds,
20
19
  type IntelEditor,
21
20
  type IntelEditorPartialBlock,
22
21
  intelEditorSchema,
23
- } from "@/document-link/document-link.tsx";
22
+ } from "@/editor-schema/editor-schema.ts";
23
+ import { FrontmatterProvider } from "@/frontmatter/frontmatter.tsx";
24
+ import { blocksToMarkdown, markdownToBlocks } from "@/frontmatter/frontmatter-markdown.ts";
24
25
  import type { I18n } from "@/i18n/i18n.types.ts";
25
26
  import { Modal } from "@/modal/modal.tsx";
26
27
  import { SaveButton, UnsavedChangesGuard } from "@/save-button/save-button.tsx";
@@ -135,7 +136,7 @@ export function NodeEditor({
135
136
  format: "blocknote",
136
137
  schemaVersion: 1,
137
138
  blocks,
138
- markdown: await editor.blocksToMarkdownLossy(blocks),
139
+ markdown: await blocksToMarkdown(editor, blocks),
139
140
  });
140
141
  const node = await data.saveNode({
141
142
  nodeId: document.node.id,
@@ -163,7 +164,7 @@ export function NodeEditor({
163
164
 
164
165
  useEffect(() => {
165
166
  if (!document.content || initialBlocks) return;
166
- const blocks = editor.tryParseMarkdownToBlocks(document.content);
167
+ const blocks = markdownToBlocks(editor, document.content);
167
168
  editor.replaceBlocks(editor.document, blocks);
168
169
  // ⚠️ Loading the content IS a change to the editor, and `onChange` reports it as one. This is
169
170
  // the line that keeps a document nobody has touched from asking to be saved: what was just
@@ -207,33 +208,40 @@ export function NodeEditor({
207
208
  unresolvedLabel: i18n.t("node.link.unresolved"),
208
209
  }}
209
210
  >
210
- <BlockNoteView
211
- editor={editor}
212
- onChange={() => {
213
- settle();
214
- setLinkedIds(documentLinkIds(editor.document));
211
+ <FrontmatterProvider
212
+ value={{
213
+ summary: i18n.t("node.frontmatter.summary"),
214
+ expand: i18n.t("node.frontmatter.toggle"),
215
215
  }}
216
216
  >
217
- {/* The `/` line BlockNote already has, with one item added. A second way to reach the
217
+ <BlockNoteView
218
+ editor={editor}
219
+ onChange={() => {
220
+ settle();
221
+ setLinkedIds(documentLinkIds(editor.document));
222
+ }}
223
+ >
224
+ {/* The `/` line BlockNote already has, with one item added. A second way to reach the
218
225
  same thing would be the second way #41 exists to remove. */}
219
- <SuggestionMenuController
220
- triggerCharacter="/"
221
- getItems={async (query) =>
222
- filterSuggestionItems(
223
- [
224
- ...defaultSlashMenuItems(editor),
225
- {
226
- title: i18n.t("node.link.insert"),
227
- group: i18n.t("node.link.group"),
228
- icon: <Link2 aria-hidden="true" className="size-4" />,
229
- onItemClick: () => setPicking(true),
230
- },
231
- ],
232
- query,
233
- )
234
- }
235
- />
236
- </BlockNoteView>
226
+ <SuggestionMenuController
227
+ triggerCharacter="/"
228
+ getItems={async (query) =>
229
+ filterSuggestionItems(
230
+ [
231
+ ...defaultSlashMenuItems(editor),
232
+ {
233
+ title: i18n.t("node.link.insert"),
234
+ group: i18n.t("node.link.group"),
235
+ icon: <Link2 aria-hidden="true" className="size-4" />,
236
+ onItemClick: () => setPicking(true),
237
+ },
238
+ ],
239
+ query,
240
+ )
241
+ }
242
+ />
243
+ </BlockNoteView>
244
+ </FrontmatterProvider>
237
245
  </DocumentLinkProvider>
238
246
  </TitleRowScrollArea>
239
247
  {picking ? (
@@ -5,6 +5,7 @@ import { Download, Paperclip } from "lucide-react";
5
5
  import { lazy, Suspense } from "react";
6
6
  import { treeLevelKey } from "@/app/tree-move/tree-move.tsx";
7
7
  import { ViewToggle } from "@/app/view-toggle/view-toggle.tsx";
8
+ import { BoardPanel } from "@/board/board-panel/board-panel.tsx";
8
9
  import { RefusalNotice } from "@/data/request-refusal/refusal-notice.tsx";
9
10
  import { refusalOf } from "@/data/request-refusal/request-refusal.ts";
10
11
  import { FolderContents } from "@/folder-contents/folder-contents.tsx";
@@ -160,6 +161,8 @@ export function Nodes() {
160
161
  <AttachmentPanel node={selected} />
161
162
  ) : selected.kind === "table" ? (
162
163
  <NodeTablePanel node={selected} />
164
+ ) : selected.kind === "board" ? (
165
+ <BoardPanel node={selected} />
163
166
  ) : document.data ? (
164
167
  <Suspense
165
168
  fallback={
@@ -18,7 +18,7 @@ import {
18
18
  ShieldCheck,
19
19
  } from "lucide-react";
20
20
  import type * as React from "react";
21
- import { useId, useState } from "react";
21
+ import { useState } from "react";
22
22
  import { AccessSummary } from "@/access-summary/access-summary.tsx";
23
23
  import { allTreeLevelsKey, moveErrorKey, useTreeMove } from "@/app/tree-move/tree-move.tsx";
24
24
  import {
@@ -154,11 +154,6 @@ export function ResourceMenu({
154
154
  // nowhere else: a person had strictly less reach on their own table than a model did.
155
155
  const isTable = node !== null && node.kind === "table";
156
156
  const bundleImport = useBundleImport({ where: title });
157
- // ⚠️ Per instance, not a constant: the folder table renders one menu PER ROW, so a fixed id would
158
- // put the same `id` in the document twenty times and every entry's name would resolve to the
159
- // first one's.
160
- const exportLabelId = useId();
161
- const exportHintId = useId();
162
157
 
163
158
  // Everything a change here can make stale. The row sits in one level of the tree, the open screen
164
159
  // reads the record, and the flat collections behind the search, the link picker and the relation
@@ -323,38 +318,17 @@ export function ResourceMenu({
323
318
  (#534). That is the export's gap on every kind it can hit, not something this entry
324
319
  was covering — which is why it is fixed there and not by keeping a second entry.
325
320
 
326
- ⚠️ The second line is not decoration and it belongs HERE rather than after the click
327
- (#433, ADR-0006). A bundle carries one version per node; whoever moves an installation
328
- this way loses every earlier one, and until now the only place that said so was
329
- `excluded` in a `manifest.json` inside the zipread, if ever, long after the source
330
- installation is gone. A loss that cannot be undone has to be readable BEFORE the
331
- gesture, and a menu item is where this gesture is chosen.
332
-
333
- ⚠️ Its third clause is the flow half (#585), and it is a DIFFERENT loss from the first
334
- two rather than an example of them: a published flow travels as its published graph,
335
- so edits drafted since do not travel at all. Those edits are not an earlier version —
336
- they are the newest state there is, and somebody reading only "earlier versions stay
337
- behind" would expect the opposite of what happens. `Excluded` in `bundle.ts` names it
338
- with a word of its own for the same reason.
339
-
340
- ⚠️ It is the entry's DESCRIPTION, not part of its name: `aria-labelledby` keeps the
341
- name at the one word every other entry here uses, and `aria-describedby` is what a
342
- screen reader reads after it. Folding a whole sentence into the name would make this
343
- the only entry in the menu somebody has to listen through to know what it does — and
344
- the sentence is a condition of the action, which is what a description is for. */}
345
- <DropdownMenuItem
346
- className="items-start"
347
- aria-labelledby={exportLabelId}
348
- aria-describedby={exportHintId}
349
- onSelect={() => exportBundle.mutate()}
350
- >
351
- <FolderDown aria-hidden="true" className="mt-0.5" />
352
- <span className="flex flex-col gap-0.5">
353
- <span id={exportLabelId}>{i18n.t("resource.export")}</span>
354
- <span id={exportHintId} className="text-xs text-muted-foreground">
355
- {i18n.t("resource.exportExcludes")}
356
- </span>
357
- </span>
321
+ ⚠️ The entry carried a three-clause description under its name until 2026-08-19, put
322
+ there by #433/ADR-0006 so that an irreversible loss was readable BEFORE the gesture: a
323
+ bundle takes one version per node, shares and runs stay behind, and a published flow
324
+ travels as its published graph (#585). Jack removed itit was the only entry running
325
+ over two lines and it pushed the menu apart. What the reader now gets instead is
326
+ `excluded` in the zip's `manifest.json` and `BundleImportResult.excluded` after an
327
+ import, both of which are read only once the zip exists. That is a deliberate trade,
328
+ recorded in ADR-0006 and in #636, and NOT a leftover to be restored. */}
329
+ <DropdownMenuItem onSelect={() => exportBundle.mutate()}>
330
+ <FolderDown aria-hidden="true" />
331
+ {i18n.t("resource.export")}
358
332
  </DropdownMenuItem>
359
333
  {/* The other half of the same round trip, next to it rather than in the tree's plus
360
334
  (#346): one word in the menu, both sources under it. */}
@@ -69,11 +69,22 @@ export function TitleRow({
69
69
  title,
70
70
  description,
71
71
  target,
72
+ separated,
72
73
  children,
73
74
  }: {
74
75
  title: string;
75
76
  description?: string | null;
76
77
  target: ResourceTarget;
78
+ /**
79
+ * A separator that does NOT wait for a scroll.
80
+ *
81
+ * ⚠️ For a surface where scrolling cannot happen, and that is the only case it is for (#634). The
82
+ * edge below this row is normally `scrolled`, set by `TitleRowScrollArea` — which is right where
83
+ * content moves under the row. The flow canvas does not scroll, it pans: nothing ever sets
84
+ * `scrolled`, so the row sat on the dark canvas with no edge at all. Do not pass this where the
85
+ * content does scroll; a permanent edge there would replace a signal with decoration.
86
+ */
87
+ separated?: boolean;
77
88
  children?: ReactNode;
78
89
  }) {
79
90
  const scroll = useContext(TitleRowScrollContext);
@@ -88,7 +99,7 @@ export function TitleRow({
88
99
  data-scrolled={scroll?.scrolled || undefined}
89
100
  className={cn(
90
101
  "relative z-10 flex shrink-0 items-start justify-between gap-5 px-6 py-4 transition-shadow",
91
- scroll?.scrolled && "shadow-[inset_0_-1px_0_var(--border)]",
102
+ (separated || scroll?.scrolled) && "shadow-[inset_0_-1px_0_var(--border)]",
92
103
  )}
93
104
  >
94
105
  <div className="flex min-w-0 items-center gap-3">