@anchrd/intel-ui 0.37.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/package.json +2 -2
- package/src/app/app-tree/app-tree.tsx +15 -6
- package/src/board/board-data/board-data.ts +169 -0
- package/src/board/board-data/board-data.types.ts +29 -0
- package/src/board/board-kanban/board-kanban.ts +108 -0
- package/src/board/board-kanban/board-kanban.tsx +295 -0
- package/src/board/board-panel/board-panel.tsx +74 -0
- package/src/board/board-table/board-table.ts +135 -0
- package/src/board/board-table/board-table.tsx +199 -0
- package/src/data/intel-data-provider/intel-data-provider.ts +50 -0
- package/src/data/intel-data-provider/intel-data-provider.types.ts +22 -0
- package/src/document-link/document-link.tsx +0 -18
- package/src/editor-schema/editor-schema.ts +21 -0
- package/src/frontmatter/frontmatter-markdown.ts +63 -0
- package/src/frontmatter/frontmatter.tsx +184 -0
- package/src/i18n/de.json +389 -362
- package/src/i18n/en.json +389 -362
- package/src/i18n/es.json +389 -362
- package/src/kind-icon.ts +17 -1
- package/src/node-editor/node-editor.tsx +37 -29
- package/src/nodes/nodes.tsx +3 -0
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
import { createReactBlockSpec } from "@blocknote/react";
|
|
2
|
+
import { ChevronRight } from "lucide-react";
|
|
3
|
+
import { createContext, Fragment, useContext, useState } from "react";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* The head of a knowledge node, as one block (#657).
|
|
7
|
+
*
|
|
8
|
+
* ⚠️ The type lives here rather than in the contract, unlike `documentLink`. The server reads
|
|
9
|
+
* document links out of the stored blocks and therefore has to know that word; nothing on the
|
|
10
|
+
* server side ever looks at this one. What the server reads of a head is the `markdown` field
|
|
11
|
+
* beside the blocks — which is exactly why the export half of this module matters more than the
|
|
12
|
+
* block does.
|
|
13
|
+
*/
|
|
14
|
+
export const FrontmatterBlockType = "frontmatter";
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* The head, exactly as it stood between the two fences.
|
|
18
|
+
*
|
|
19
|
+
* ⚠️ Stored raw, never as parsed pairs, and this is the whole reason the roundtrip cannot lose
|
|
20
|
+
* anything. A head that is not valid YAML — a typo, a half-written list, a value somebody is in the
|
|
21
|
+
* middle of writing — travels through unchanged and comes back out as it went in. Parsing happens
|
|
22
|
+
* for the DISPLAY only, in `frontmatterEntries`, and a line that will not split is shown as it
|
|
23
|
+
* stands rather than dropped. The alternative, parsing on the way in and re-emitting on the way
|
|
24
|
+
* out, turns every gap in the parser into silent data loss at the next save.
|
|
25
|
+
*/
|
|
26
|
+
const FRONTMATTER_PROPS = { raw: { default: "" } } as const;
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* A head at the very start of the document, and only there.
|
|
30
|
+
*
|
|
31
|
+
* ⚠️ No `m` flag, and the `^` is doing real work — though not on the documents one first reaches
|
|
32
|
+
* for. `exec` without `g` finds a head at position 0 with or without the anchor; what the anchor
|
|
33
|
+
* decides is a document that has NO head and two rules further down. Unanchored, the first rule and
|
|
34
|
+
* the text after it are swallowed into an invented head. That is the case its test uses.
|
|
35
|
+
*
|
|
36
|
+
* ⚠️ `(?![ \t]*\r?\n)` — a head begins on the line after the fence, never with a blank one. This
|
|
37
|
+
* is what keeps `---\n\nErster Teil\n\n---` from reading as a head: nothing is lost from
|
|
38
|
+
* `markdown` when it does, but the rule, the text and the second rule all vanish behind a folded
|
|
39
|
+
* "Metadata" strip, and the reader has no way to know where their paragraph went.
|
|
40
|
+
*
|
|
41
|
+
* `[\s\S]+?` rather than `*?` says the same thing the falsy check in `splitFrontmatter` already
|
|
42
|
+
* says — an empty capture is no head — and is kept for saying it here rather than two lines later.
|
|
43
|
+
* It is not what makes `---\n---` two thematic breaks; that check is.
|
|
44
|
+
*
|
|
45
|
+
* The blank lines after the closing fence belong to the fence, not to the body. Leaving them on
|
|
46
|
+
* would make `body` start with an empty line that the head had put there — and the join in
|
|
47
|
+
* `blocksToMarkdown` writes exactly one back, so taking them here is what keeps a document from
|
|
48
|
+
* growing a blank line per save.
|
|
49
|
+
*/
|
|
50
|
+
const LEADING_FRONTMATTER =
|
|
51
|
+
/^---\r?\n(?![ \t]*\r?\n)([\s\S]+?)\r?\n---[ \t]*(?:\r?\n|$)(?:[ \t]*\r?\n)*/;
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* A document's markdown, cut into its head and the rest.
|
|
55
|
+
*
|
|
56
|
+
* `raw` is `null` when there is no head, and then `body` is the whole input — the caller has one
|
|
57
|
+
* branch, not two shapes to tell apart.
|
|
58
|
+
*/
|
|
59
|
+
export function splitFrontmatter(markdown: string): { raw: string | null; body: string } {
|
|
60
|
+
const match = LEADING_FRONTMATTER.exec(markdown);
|
|
61
|
+
if (!match?.[1]) return { raw: null, body: markdown };
|
|
62
|
+
return { raw: match[1], body: markdown.slice(match[0].length) };
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* The head split into what a reader sees, for display only.
|
|
67
|
+
*
|
|
68
|
+
* ⚠️ It is never the source of the stored value — `raw` is. A line without a colon is not a broken
|
|
69
|
+
* pair but a normal part of YAML (a list item under the key above it, a folded value's second
|
|
70
|
+
* line), and it is shown whole rather than split at nothing.
|
|
71
|
+
*/
|
|
72
|
+
export function frontmatterEntries(raw: string): { key: string; value: string }[] {
|
|
73
|
+
return raw
|
|
74
|
+
.split(/\r?\n/)
|
|
75
|
+
.filter((line) => line.trim().length > 0)
|
|
76
|
+
.map((line) => {
|
|
77
|
+
const colon = line.indexOf(":");
|
|
78
|
+
const continues = /^[\s-]/.test(line);
|
|
79
|
+
if (colon < 0 || continues) return { key: "", value: line.trim() };
|
|
80
|
+
return { key: line.slice(0, colon).trim(), value: line.slice(colon + 1).trim() };
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* The two words the block says. They travel in a context for the same reason the document link's
|
|
86
|
+
* one word does: a block is rendered by the editor, not by a view that could reach the catalog.
|
|
87
|
+
*/
|
|
88
|
+
export interface FrontmatterLabels {
|
|
89
|
+
summary: string;
|
|
90
|
+
expand: string;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
const FrontmatterContext = createContext<FrontmatterLabels>({
|
|
94
|
+
summary: "Metadata",
|
|
95
|
+
expand: "Show metadata",
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
export const FrontmatterProvider = FrontmatterContext.Provider;
|
|
99
|
+
|
|
100
|
+
export function FrontmatterHead({ raw }: { raw: string }) {
|
|
101
|
+
const labels = useContext(FrontmatterContext);
|
|
102
|
+
// ⚠️ Local state, deliberately NOT a block prop. The fold is how the head is being looked at, not
|
|
103
|
+
// what it says — and a prop would put it into the stored document, which means opening the head
|
|
104
|
+
// to read it would mark the document unsaved and offer to write a "change" nobody made.
|
|
105
|
+
const [open, setOpen] = useState(false);
|
|
106
|
+
const entries = frontmatterEntries(raw);
|
|
107
|
+
|
|
108
|
+
return (
|
|
109
|
+
<div
|
|
110
|
+
// The editor owns the text around this; the head itself is not typed into.
|
|
111
|
+
contentEditable={false}
|
|
112
|
+
data-frontmatter={open ? "open" : "closed"}
|
|
113
|
+
className="my-2 w-full rounded-md border border-border bg-muted/40"
|
|
114
|
+
>
|
|
115
|
+
<button
|
|
116
|
+
type="button"
|
|
117
|
+
aria-expanded={open}
|
|
118
|
+
aria-label={labels.expand}
|
|
119
|
+
onClick={() => setOpen(!open)}
|
|
120
|
+
className="flex w-full items-center gap-2 rounded-md px-3 py-1.5 text-left text-sm text-muted-foreground outline-none hover:text-foreground focus-visible:ring-2 focus-visible:ring-ring"
|
|
121
|
+
>
|
|
122
|
+
<ChevronRight
|
|
123
|
+
aria-hidden="true"
|
|
124
|
+
className={
|
|
125
|
+
open ? "size-3.5 rotate-90 transition-transform" : "size-3.5 transition-transform"
|
|
126
|
+
}
|
|
127
|
+
/>
|
|
128
|
+
{/* ⚠️ Closed, it says THAT there is a head, never what is in it. A preview of `title` or
|
|
129
|
+
`status` here would be the H2 from #657 again, one size smaller. */}
|
|
130
|
+
{labels.summary}
|
|
131
|
+
</button>
|
|
132
|
+
{open ? (
|
|
133
|
+
<dl className="grid grid-cols-[max-content_1fr] gap-x-4 gap-y-1 px-3 pb-3 font-mono text-xs">
|
|
134
|
+
{entries.map((entry, index) =>
|
|
135
|
+
entry.key ? (
|
|
136
|
+
// Nothing in a head is unique — two `- Q-04` lines under `quellen` are the ordinary
|
|
137
|
+
// case — so the position is the only honest key. It is also a safe one here: the list
|
|
138
|
+
// is derived from an immutable `raw` and is never sorted, inserted into or filtered.
|
|
139
|
+
// biome-ignore lint/suspicious/noArrayIndexKey: a head's lines have no other key
|
|
140
|
+
<Fragment key={index}>
|
|
141
|
+
<dt className="text-muted-foreground">{entry.key}</dt>
|
|
142
|
+
<dd className="break-words text-foreground">{entry.value}</dd>
|
|
143
|
+
</Fragment>
|
|
144
|
+
) : (
|
|
145
|
+
// A line that is not a pair — a list item, a folded value's second line — spans both
|
|
146
|
+
// columns rather than being squeezed into the value one, and is indented so it still
|
|
147
|
+
// reads as belonging to the key above it.
|
|
148
|
+
// biome-ignore lint/suspicious/noArrayIndexKey: a head's lines have no other key
|
|
149
|
+
<dd key={index} className="col-span-2 break-words pl-4 text-foreground">
|
|
150
|
+
{entry.value}
|
|
151
|
+
</dd>
|
|
152
|
+
),
|
|
153
|
+
)}
|
|
154
|
+
</dl>
|
|
155
|
+
) : null}
|
|
156
|
+
</div>
|
|
157
|
+
);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
export const frontmatterSpec = createReactBlockSpec(
|
|
161
|
+
{
|
|
162
|
+
type: FrontmatterBlockType,
|
|
163
|
+
propSchema: FRONTMATTER_PROPS,
|
|
164
|
+
// The head is not editable text. It is written by whoever writes the document's markdown, and a
|
|
165
|
+
// caret inside it would let a reader break `type:` without it looking like anything happened.
|
|
166
|
+
content: "none",
|
|
167
|
+
},
|
|
168
|
+
{
|
|
169
|
+
render: (props) => <FrontmatterHead raw={String(props.block.props.raw)} />,
|
|
170
|
+
// ⚠️ The net under `blocksToMarkdown`, for every path that does not go through it. Without a
|
|
171
|
+
// serialiser BlockNote falls back to the RENDERED DOM, so a head would leave the editor as the
|
|
172
|
+
// word "Metadata" — its own label, in place of the metadata. A fenced block is not a head, but
|
|
173
|
+
// it is visible and it is lossless, which is what a fallback owes.
|
|
174
|
+
//
|
|
175
|
+
// ⚠️ `<pre><code>`, not `<pre>` alone. Measured: a bare `<pre>` survives the HTML export and is
|
|
176
|
+
// then dropped by the HTML-to-markdown step, which returns `"\n"` — a net that looks like one
|
|
177
|
+
// in the HTML and catches nothing where it matters.
|
|
178
|
+
toExternalHTML: (props) => (
|
|
179
|
+
<pre>
|
|
180
|
+
<code>{String(props.block.props.raw)}</code>
|
|
181
|
+
</pre>
|
|
182
|
+
),
|
|
183
|
+
},
|
|
184
|
+
);
|