@dbcdk/react-components 0.0.182 → 0.0.183
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/dist/client.cjs +21 -0
- package/dist/client.d.ts +3 -0
- package/dist/client.js +3 -0
- package/dist/components/clear-button/ClearButton.cjs +7 -2
- package/dist/components/clear-button/ClearButton.d.ts +8 -1
- package/dist/components/clear-button/ClearButton.js +7 -2
- package/dist/components/clear-button/ClearButton.module.css +12 -0
- package/dist/components/forms/select/Select.cjs +2 -1
- package/dist/components/forms/select/Select.js +2 -1
- package/dist/components/json-viewer/JsonViewer.cjs +3 -2
- package/dist/components/json-viewer/JsonViewer.d.ts +1 -1
- package/dist/components/json-viewer/JsonViewer.js +3 -2
- package/dist/components/json-viewer/JsonViewer.module.css +7 -0
- package/dist/components/json-viewer/types.d.ts +6 -0
- package/dist/components/line-viewer/LineViewer.cjs +174 -0
- package/dist/components/line-viewer/LineViewer.d.ts +20 -0
- package/dist/components/line-viewer/LineViewer.js +168 -0
- package/dist/components/line-viewer/LineViewer.module.css +112 -0
- package/dist/components/line-viewer/tokenizeLineFormat.cjs +52 -0
- package/dist/components/line-viewer/tokenizeLineFormat.d.ts +14 -0
- package/dist/components/line-viewer/tokenizeLineFormat.js +50 -0
- package/dist/components/tree-view/TreeNodeRow.cjs +99 -0
- package/dist/components/tree-view/TreeNodeRow.d.ts +18 -0
- package/dist/components/tree-view/TreeNodeRow.js +93 -0
- package/dist/components/tree-view/TreeView.cjs +93 -0
- package/dist/components/tree-view/TreeView.d.ts +20 -0
- package/dist/components/tree-view/TreeView.js +79 -0
- package/dist/components/tree-view/TreeView.module.css +141 -0
- package/dist/components/tree-view/types.cjs +2 -0
- package/dist/components/tree-view/types.d.ts +44 -0
- package/dist/components/tree-view/types.js +1 -0
- package/dist/components/tree-view/useTreeExpansion.cjs +47 -0
- package/dist/components/tree-view/useTreeExpansion.d.ts +17 -0
- package/dist/components/tree-view/useTreeExpansion.js +45 -0
- package/dist/components/tree-view/utils.cjs +32 -0
- package/dist/components/tree-view/utils.d.ts +16 -0
- package/dist/components/tree-view/utils.js +29 -0
- package/dist/components/xml-viewer/XmlViewer.cjs +175 -0
- package/dist/components/xml-viewer/XmlViewer.d.ts +20 -0
- package/dist/components/xml-viewer/XmlViewer.js +169 -0
- package/dist/components/xml-viewer/XmlViewer.module.css +109 -0
- package/dist/components/xml-viewer/tokenizeXml.cjs +50 -0
- package/dist/components/xml-viewer/tokenizeXml.d.ts +12 -0
- package/dist/components/xml-viewer/tokenizeXml.js +48 -0
- package/dist/index.cjs +7 -0
- package/dist/index.css +3 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/utils/date/formatDate.cjs +29 -17
- package/dist/utils/date/formatDate.d.ts +7 -0
- package/dist/utils/date/formatDate.js +29 -17
- package/package.json +1 -1
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
.viewer {
|
|
2
|
+
--line-pane-bg: var(--color-bg-inverse);
|
|
3
|
+
--line-pane-border: color-mix(in oklab, var(--color-fg-inverse) 12%, transparent);
|
|
4
|
+
--line-pane-text: var(--color-fg-inverse);
|
|
5
|
+
|
|
6
|
+
--line-tag: color-mix(in oklab, var(--color-fg-inverse) 66%, var(--color-syntax-accent) 34%);
|
|
7
|
+
--line-indicator: color-mix(in oklab, var(--color-fg-inverse) 46%, transparent);
|
|
8
|
+
--line-subfield-code: color-mix(in oklab, var(--color-fg-inverse) 30%, var(--color-syntax-number) 70%);
|
|
9
|
+
--line-value: color-mix(in oklab, var(--color-fg-inverse) 20%, var(--color-syntax-string) 80%);
|
|
10
|
+
--line-terminator: color-mix(in oklab, var(--color-fg-inverse) 30%, var(--color-syntax-boolean) 70%);
|
|
11
|
+
--line-punctuation: color-mix(in oklab, var(--color-fg-inverse) 60%, transparent);
|
|
12
|
+
--line-continuation: color-mix(in oklab, var(--color-fg-inverse) 46%, transparent);
|
|
13
|
+
|
|
14
|
+
position: relative;
|
|
15
|
+
background: var(--line-pane-bg);
|
|
16
|
+
border: var(--border-width-thin) solid var(--line-pane-border);
|
|
17
|
+
border-radius: var(--border-radius-default);
|
|
18
|
+
box-shadow: var(--shadow-md);
|
|
19
|
+
font-family: var(--font-family-mono);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
.viewer[data-appearance='light'] {
|
|
23
|
+
--line-pane-bg: var(--color-bg-surface);
|
|
24
|
+
--line-pane-border: var(--color-border-subtle);
|
|
25
|
+
--line-pane-text: var(--color-fg-default);
|
|
26
|
+
|
|
27
|
+
--line-tag: var(--color-link);
|
|
28
|
+
--line-indicator: var(--color-fg-subtle);
|
|
29
|
+
--line-subfield-code: var(--color-status-warning-fg);
|
|
30
|
+
--line-value: var(--color-status-success-fg);
|
|
31
|
+
--line-terminator: var(--color-accent-fg);
|
|
32
|
+
--line-punctuation: var(--color-fg-subtle);
|
|
33
|
+
--line-continuation: var(--color-fg-muted);
|
|
34
|
+
|
|
35
|
+
box-shadow: var(--shadow-xs);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
.viewer.borderless {
|
|
39
|
+
border: none;
|
|
40
|
+
border-radius: 0;
|
|
41
|
+
box-shadow: none;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
.toolbar {
|
|
45
|
+
position: absolute;
|
|
46
|
+
top: var(--spacing-xs);
|
|
47
|
+
right: var(--spacing-xs);
|
|
48
|
+
z-index: 1;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
.codePane {
|
|
52
|
+
margin: 0;
|
|
53
|
+
padding: var(--spacing-sm);
|
|
54
|
+
overflow: auto;
|
|
55
|
+
color: var(--line-pane-text);
|
|
56
|
+
font-family: inherit;
|
|
57
|
+
line-height: 1.35;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
.wrap {
|
|
61
|
+
white-space: pre-wrap;
|
|
62
|
+
overflow-wrap: break-word;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
.noWrap {
|
|
66
|
+
white-space: pre;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
.xs,
|
|
70
|
+
.sm {
|
|
71
|
+
font-size: var(--font-size-xs);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
.md {
|
|
75
|
+
font-size: var(--font-size-sm);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
.lg {
|
|
79
|
+
font-size: var(--font-size-base);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
.tag {
|
|
83
|
+
color: var(--line-tag);
|
|
84
|
+
font-weight: var(--font-weight-medium);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
.indicator {
|
|
88
|
+
color: var(--line-indicator);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
.subfieldMarker {
|
|
92
|
+
color: var(--line-punctuation);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
.subfieldCode {
|
|
96
|
+
color: var(--line-subfield-code);
|
|
97
|
+
font-weight: var(--font-weight-medium);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
.value {
|
|
101
|
+
color: var(--line-value);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
.terminator {
|
|
105
|
+
color: var(--line-terminator);
|
|
106
|
+
font-weight: var(--font-weight-medium);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
.text {
|
|
110
|
+
color: var(--line-continuation);
|
|
111
|
+
font-style: italic;
|
|
112
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const FIELD_LINE_PATTERN = /^(\S+)(\s+)(\S+)(\s+)(\*[\s\S]*)$/;
|
|
4
|
+
const SUBFIELD_PATTERN = /\*(\S)([^*]*)/g;
|
|
5
|
+
function tokenizeFieldLine(line) {
|
|
6
|
+
const match = FIELD_LINE_PATTERN.exec(line);
|
|
7
|
+
if (!match) return [{ type: "text", value: line }];
|
|
8
|
+
const [, tag, gap1, indicator, gap2, rest] = match;
|
|
9
|
+
const tokens = [
|
|
10
|
+
{ type: "tag", value: tag },
|
|
11
|
+
{ type: "whitespace", value: gap1 },
|
|
12
|
+
{ type: "indicator", value: indicator },
|
|
13
|
+
{ type: "whitespace", value: gap2 }
|
|
14
|
+
];
|
|
15
|
+
SUBFIELD_PATTERN.lastIndex = 0;
|
|
16
|
+
let match2;
|
|
17
|
+
let lastIndex = 0;
|
|
18
|
+
while (match2 = SUBFIELD_PATTERN.exec(rest)) {
|
|
19
|
+
tokens.push({ type: "subfieldMarker", value: "*" });
|
|
20
|
+
tokens.push({ type: "subfieldCode", value: match2[1] });
|
|
21
|
+
tokens.push({ type: "value", value: match2[2] });
|
|
22
|
+
lastIndex = SUBFIELD_PATTERN.lastIndex;
|
|
23
|
+
}
|
|
24
|
+
if (lastIndex < rest.length) {
|
|
25
|
+
tokens.push({ type: "text", value: rest.slice(lastIndex) });
|
|
26
|
+
}
|
|
27
|
+
return tokens;
|
|
28
|
+
}
|
|
29
|
+
function tokenizeLineFormat(source) {
|
|
30
|
+
const tokens = [];
|
|
31
|
+
const chunks = source.split(/(\n)/);
|
|
32
|
+
for (const chunk of chunks) {
|
|
33
|
+
if (chunk === "") continue;
|
|
34
|
+
if (chunk === "\n") {
|
|
35
|
+
tokens.push({ type: "whitespace", value: "\n" });
|
|
36
|
+
continue;
|
|
37
|
+
}
|
|
38
|
+
if (chunk.trim() === "$") {
|
|
39
|
+
const dollarIndex = chunk.indexOf("$");
|
|
40
|
+
const leading = chunk.slice(0, dollarIndex);
|
|
41
|
+
const trailing = chunk.slice(dollarIndex + 1);
|
|
42
|
+
if (leading) tokens.push({ type: "text", value: leading });
|
|
43
|
+
tokens.push({ type: "terminator", value: "$" });
|
|
44
|
+
if (trailing) tokens.push({ type: "text", value: trailing });
|
|
45
|
+
continue;
|
|
46
|
+
}
|
|
47
|
+
tokens.push(...tokenizeFieldLine(chunk));
|
|
48
|
+
}
|
|
49
|
+
return tokens;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
exports.tokenizeLineFormat = tokenizeLineFormat;
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
export type LineTokenType = 'tag' | 'indicator' | 'subfieldMarker' | 'subfieldCode' | 'value' | 'terminator' | 'text' | 'whitespace';
|
|
2
|
+
export interface LineToken {
|
|
3
|
+
type: LineTokenType;
|
|
4
|
+
value: string;
|
|
5
|
+
}
|
|
6
|
+
/**
|
|
7
|
+
* Tokenizes a MARC-style "line format" record (field tag, indicators,
|
|
8
|
+
* `*code value` subfields, terminated by a lone `$`) for syntax
|
|
9
|
+
* highlighting without imposing a strict grammar. Preserves the source
|
|
10
|
+
* exactly - concatenating every token's `value` reproduces the input - so a
|
|
11
|
+
* wrapped continuation line or malformed field still displays as plain
|
|
12
|
+
* text instead of breaking the tokenizer.
|
|
13
|
+
*/
|
|
14
|
+
export declare function tokenizeLineFormat(source: string): LineToken[];
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
const FIELD_LINE_PATTERN = /^(\S+)(\s+)(\S+)(\s+)(\*[\s\S]*)$/;
|
|
2
|
+
const SUBFIELD_PATTERN = /\*(\S)([^*]*)/g;
|
|
3
|
+
function tokenizeFieldLine(line) {
|
|
4
|
+
const match = FIELD_LINE_PATTERN.exec(line);
|
|
5
|
+
if (!match) return [{ type: "text", value: line }];
|
|
6
|
+
const [, tag, gap1, indicator, gap2, rest] = match;
|
|
7
|
+
const tokens = [
|
|
8
|
+
{ type: "tag", value: tag },
|
|
9
|
+
{ type: "whitespace", value: gap1 },
|
|
10
|
+
{ type: "indicator", value: indicator },
|
|
11
|
+
{ type: "whitespace", value: gap2 }
|
|
12
|
+
];
|
|
13
|
+
SUBFIELD_PATTERN.lastIndex = 0;
|
|
14
|
+
let match2;
|
|
15
|
+
let lastIndex = 0;
|
|
16
|
+
while (match2 = SUBFIELD_PATTERN.exec(rest)) {
|
|
17
|
+
tokens.push({ type: "subfieldMarker", value: "*" });
|
|
18
|
+
tokens.push({ type: "subfieldCode", value: match2[1] });
|
|
19
|
+
tokens.push({ type: "value", value: match2[2] });
|
|
20
|
+
lastIndex = SUBFIELD_PATTERN.lastIndex;
|
|
21
|
+
}
|
|
22
|
+
if (lastIndex < rest.length) {
|
|
23
|
+
tokens.push({ type: "text", value: rest.slice(lastIndex) });
|
|
24
|
+
}
|
|
25
|
+
return tokens;
|
|
26
|
+
}
|
|
27
|
+
function tokenizeLineFormat(source) {
|
|
28
|
+
const tokens = [];
|
|
29
|
+
const chunks = source.split(/(\n)/);
|
|
30
|
+
for (const chunk of chunks) {
|
|
31
|
+
if (chunk === "") continue;
|
|
32
|
+
if (chunk === "\n") {
|
|
33
|
+
tokens.push({ type: "whitespace", value: "\n" });
|
|
34
|
+
continue;
|
|
35
|
+
}
|
|
36
|
+
if (chunk.trim() === "$") {
|
|
37
|
+
const dollarIndex = chunk.indexOf("$");
|
|
38
|
+
const leading = chunk.slice(0, dollarIndex);
|
|
39
|
+
const trailing = chunk.slice(dollarIndex + 1);
|
|
40
|
+
if (leading) tokens.push({ type: "text", value: leading });
|
|
41
|
+
tokens.push({ type: "terminator", value: "$" });
|
|
42
|
+
if (trailing) tokens.push({ type: "text", value: trailing });
|
|
43
|
+
continue;
|
|
44
|
+
}
|
|
45
|
+
tokens.push(...tokenizeFieldLine(chunk));
|
|
46
|
+
}
|
|
47
|
+
return tokens;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export { tokenizeLineFormat };
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
'use strict';
|
|
3
|
+
|
|
4
|
+
var jsxRuntime = require('react/jsx-runtime');
|
|
5
|
+
var lucideReact = require('lucide-react');
|
|
6
|
+
var react = require('react');
|
|
7
|
+
var styles = require('./TreeView.module.css');
|
|
8
|
+
|
|
9
|
+
function _interopDefault (e) { return e && e.__esModule ? e : { default: e }; }
|
|
10
|
+
|
|
11
|
+
var styles__default = /*#__PURE__*/_interopDefault(styles);
|
|
12
|
+
|
|
13
|
+
function TreeNodeRow({
|
|
14
|
+
node,
|
|
15
|
+
depth,
|
|
16
|
+
isExpanded,
|
|
17
|
+
toggle,
|
|
18
|
+
resolvedChildren,
|
|
19
|
+
loadingIds,
|
|
20
|
+
errorIds,
|
|
21
|
+
requestLoad,
|
|
22
|
+
loadChildren,
|
|
23
|
+
selectedId,
|
|
24
|
+
onSelectNode,
|
|
25
|
+
renderLabel
|
|
26
|
+
}) {
|
|
27
|
+
var _a;
|
|
28
|
+
const children = (_a = node.children) != null ? _a : resolvedChildren[node.id];
|
|
29
|
+
const isResolved = children !== void 0;
|
|
30
|
+
const expandable = isResolved ? children.length > 0 : Boolean(loadChildren) && node.hasChildren !== false;
|
|
31
|
+
const expanded = isExpanded(node.id);
|
|
32
|
+
const loading = loadingIds.has(node.id);
|
|
33
|
+
const error = errorIds[node.id];
|
|
34
|
+
const selected = selectedId !== void 0 && selectedId === node.id;
|
|
35
|
+
react.useEffect(() => {
|
|
36
|
+
if (expanded && children === void 0 && !loading && !error && loadChildren) {
|
|
37
|
+
requestLoad(node);
|
|
38
|
+
}
|
|
39
|
+
}, [expanded, children, loading, error]);
|
|
40
|
+
return /* @__PURE__ */ jsxRuntime.jsxs(
|
|
41
|
+
"div",
|
|
42
|
+
{
|
|
43
|
+
role: "treeitem",
|
|
44
|
+
"aria-expanded": expandable ? expanded : void 0,
|
|
45
|
+
"aria-selected": selected,
|
|
46
|
+
style: { ["--tree-depth"]: depth },
|
|
47
|
+
children: [
|
|
48
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: styles__default.default.row, "data-selected": selected || void 0, children: [
|
|
49
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
50
|
+
"button",
|
|
51
|
+
{
|
|
52
|
+
type: "button",
|
|
53
|
+
className: styles__default.default.toggle,
|
|
54
|
+
onClick: () => expandable && toggle(node.id),
|
|
55
|
+
disabled: !expandable,
|
|
56
|
+
"aria-hidden": !expandable,
|
|
57
|
+
tabIndex: expandable ? 0 : -1,
|
|
58
|
+
children: loading ? /* @__PURE__ */ jsxRuntime.jsx("span", { className: `spin ${styles__default.default.spinner}`, children: /* @__PURE__ */ jsxRuntime.jsx(lucideReact.LoaderCircle, { "aria-hidden": "true" }) }) : expandable ? (
|
|
59
|
+
// A resolved node's chevron means "this has children"; an
|
|
60
|
+
// unresolved lazy node's dimmer chevron means "might have
|
|
61
|
+
// children, click to check" - we genuinely don't know yet,
|
|
62
|
+
// and shouldn't visually assert otherwise.
|
|
63
|
+
/* @__PURE__ */ jsxRuntime.jsx("span", { className: isResolved ? void 0 : styles__default.default.toggleUnresolved, children: expanded ? /* @__PURE__ */ jsxRuntime.jsx(lucideReact.ChevronDown, { "aria-hidden": "true" }) : /* @__PURE__ */ jsxRuntime.jsx(lucideReact.ChevronRight, { "aria-hidden": "true" }) })
|
|
64
|
+
) : /* @__PURE__ */ jsxRuntime.jsx("span", { className: styles__default.default.leafSpacer, "aria-hidden": "true" })
|
|
65
|
+
}
|
|
66
|
+
),
|
|
67
|
+
/* @__PURE__ */ jsxRuntime.jsxs("button", { type: "button", className: styles__default.default.labelButton, onClick: () => onSelectNode == null ? void 0 : onSelectNode(node), children: [
|
|
68
|
+
node.icon ? /* @__PURE__ */ jsxRuntime.jsx("span", { className: styles__default.default.icon, "aria-hidden": "true", children: node.icon }) : null,
|
|
69
|
+
/* @__PURE__ */ jsxRuntime.jsx("span", { className: styles__default.default.label, children: renderLabel ? renderLabel(node) : node.label })
|
|
70
|
+
] })
|
|
71
|
+
] }),
|
|
72
|
+
error ? /* @__PURE__ */ jsxRuntime.jsxs("div", { className: styles__default.default.errorRow, children: [
|
|
73
|
+
/* @__PURE__ */ jsxRuntime.jsx("span", { children: error }),
|
|
74
|
+
/* @__PURE__ */ jsxRuntime.jsx("button", { type: "button", className: styles__default.default.retryButton, onClick: () => requestLoad(node), children: "Pr\xF8v igen" })
|
|
75
|
+
] }) : null,
|
|
76
|
+
expanded && children && children.length > 0 ? /* @__PURE__ */ jsxRuntime.jsx("div", { role: "group", className: styles__default.default.children, children: children.map((child) => /* @__PURE__ */ jsxRuntime.jsx(
|
|
77
|
+
TreeNodeRow,
|
|
78
|
+
{
|
|
79
|
+
node: child,
|
|
80
|
+
depth: depth + 1,
|
|
81
|
+
isExpanded,
|
|
82
|
+
toggle,
|
|
83
|
+
resolvedChildren,
|
|
84
|
+
loadingIds,
|
|
85
|
+
errorIds,
|
|
86
|
+
requestLoad,
|
|
87
|
+
loadChildren,
|
|
88
|
+
selectedId,
|
|
89
|
+
onSelectNode,
|
|
90
|
+
renderLabel
|
|
91
|
+
},
|
|
92
|
+
child.id
|
|
93
|
+
)) }) : null
|
|
94
|
+
]
|
|
95
|
+
}
|
|
96
|
+
);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
exports.TreeNodeRow = TreeNodeRow;
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { JSX, ReactNode } from 'react';
|
|
2
|
+
import type { LoadTreeChildren, TreeNode } from './types';
|
|
3
|
+
type TreeNodeRowProps<T> = {
|
|
4
|
+
node: TreeNode<T>;
|
|
5
|
+
depth: number;
|
|
6
|
+
isExpanded: (id: string) => boolean;
|
|
7
|
+
toggle: (id: string) => void;
|
|
8
|
+
resolvedChildren: Record<string, TreeNode<T>[]>;
|
|
9
|
+
loadingIds: Set<string>;
|
|
10
|
+
errorIds: Record<string, ReactNode>;
|
|
11
|
+
requestLoad: (node: TreeNode<T>) => void;
|
|
12
|
+
loadChildren?: LoadTreeChildren<T>;
|
|
13
|
+
selectedId?: string;
|
|
14
|
+
onSelectNode?: (node: TreeNode<T>) => void;
|
|
15
|
+
renderLabel?: (node: TreeNode<T>) => ReactNode;
|
|
16
|
+
};
|
|
17
|
+
export declare function TreeNodeRow<T>({ node, depth, isExpanded, toggle, resolvedChildren, loadingIds, errorIds, requestLoad, loadChildren, selectedId, onSelectNode, renderLabel, }: TreeNodeRowProps<T>): JSX.Element;
|
|
18
|
+
export {};
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import { jsxs, jsx } from 'react/jsx-runtime';
|
|
3
|
+
import { LoaderCircle, ChevronDown, ChevronRight } from 'lucide-react';
|
|
4
|
+
import { useEffect } from 'react';
|
|
5
|
+
import styles from './TreeView.module.css';
|
|
6
|
+
|
|
7
|
+
function TreeNodeRow({
|
|
8
|
+
node,
|
|
9
|
+
depth,
|
|
10
|
+
isExpanded,
|
|
11
|
+
toggle,
|
|
12
|
+
resolvedChildren,
|
|
13
|
+
loadingIds,
|
|
14
|
+
errorIds,
|
|
15
|
+
requestLoad,
|
|
16
|
+
loadChildren,
|
|
17
|
+
selectedId,
|
|
18
|
+
onSelectNode,
|
|
19
|
+
renderLabel
|
|
20
|
+
}) {
|
|
21
|
+
var _a;
|
|
22
|
+
const children = (_a = node.children) != null ? _a : resolvedChildren[node.id];
|
|
23
|
+
const isResolved = children !== void 0;
|
|
24
|
+
const expandable = isResolved ? children.length > 0 : Boolean(loadChildren) && node.hasChildren !== false;
|
|
25
|
+
const expanded = isExpanded(node.id);
|
|
26
|
+
const loading = loadingIds.has(node.id);
|
|
27
|
+
const error = errorIds[node.id];
|
|
28
|
+
const selected = selectedId !== void 0 && selectedId === node.id;
|
|
29
|
+
useEffect(() => {
|
|
30
|
+
if (expanded && children === void 0 && !loading && !error && loadChildren) {
|
|
31
|
+
requestLoad(node);
|
|
32
|
+
}
|
|
33
|
+
}, [expanded, children, loading, error]);
|
|
34
|
+
return /* @__PURE__ */ jsxs(
|
|
35
|
+
"div",
|
|
36
|
+
{
|
|
37
|
+
role: "treeitem",
|
|
38
|
+
"aria-expanded": expandable ? expanded : void 0,
|
|
39
|
+
"aria-selected": selected,
|
|
40
|
+
style: { ["--tree-depth"]: depth },
|
|
41
|
+
children: [
|
|
42
|
+
/* @__PURE__ */ jsxs("div", { className: styles.row, "data-selected": selected || void 0, children: [
|
|
43
|
+
/* @__PURE__ */ jsx(
|
|
44
|
+
"button",
|
|
45
|
+
{
|
|
46
|
+
type: "button",
|
|
47
|
+
className: styles.toggle,
|
|
48
|
+
onClick: () => expandable && toggle(node.id),
|
|
49
|
+
disabled: !expandable,
|
|
50
|
+
"aria-hidden": !expandable,
|
|
51
|
+
tabIndex: expandable ? 0 : -1,
|
|
52
|
+
children: loading ? /* @__PURE__ */ jsx("span", { className: `spin ${styles.spinner}`, children: /* @__PURE__ */ jsx(LoaderCircle, { "aria-hidden": "true" }) }) : expandable ? (
|
|
53
|
+
// A resolved node's chevron means "this has children"; an
|
|
54
|
+
// unresolved lazy node's dimmer chevron means "might have
|
|
55
|
+
// children, click to check" - we genuinely don't know yet,
|
|
56
|
+
// and shouldn't visually assert otherwise.
|
|
57
|
+
/* @__PURE__ */ jsx("span", { className: isResolved ? void 0 : styles.toggleUnresolved, children: expanded ? /* @__PURE__ */ jsx(ChevronDown, { "aria-hidden": "true" }) : /* @__PURE__ */ jsx(ChevronRight, { "aria-hidden": "true" }) })
|
|
58
|
+
) : /* @__PURE__ */ jsx("span", { className: styles.leafSpacer, "aria-hidden": "true" })
|
|
59
|
+
}
|
|
60
|
+
),
|
|
61
|
+
/* @__PURE__ */ jsxs("button", { type: "button", className: styles.labelButton, onClick: () => onSelectNode == null ? void 0 : onSelectNode(node), children: [
|
|
62
|
+
node.icon ? /* @__PURE__ */ jsx("span", { className: styles.icon, "aria-hidden": "true", children: node.icon }) : null,
|
|
63
|
+
/* @__PURE__ */ jsx("span", { className: styles.label, children: renderLabel ? renderLabel(node) : node.label })
|
|
64
|
+
] })
|
|
65
|
+
] }),
|
|
66
|
+
error ? /* @__PURE__ */ jsxs("div", { className: styles.errorRow, children: [
|
|
67
|
+
/* @__PURE__ */ jsx("span", { children: error }),
|
|
68
|
+
/* @__PURE__ */ jsx("button", { type: "button", className: styles.retryButton, onClick: () => requestLoad(node), children: "Pr\xF8v igen" })
|
|
69
|
+
] }) : null,
|
|
70
|
+
expanded && children && children.length > 0 ? /* @__PURE__ */ jsx("div", { role: "group", className: styles.children, children: children.map((child) => /* @__PURE__ */ jsx(
|
|
71
|
+
TreeNodeRow,
|
|
72
|
+
{
|
|
73
|
+
node: child,
|
|
74
|
+
depth: depth + 1,
|
|
75
|
+
isExpanded,
|
|
76
|
+
toggle,
|
|
77
|
+
resolvedChildren,
|
|
78
|
+
loadingIds,
|
|
79
|
+
errorIds,
|
|
80
|
+
requestLoad,
|
|
81
|
+
loadChildren,
|
|
82
|
+
selectedId,
|
|
83
|
+
onSelectNode,
|
|
84
|
+
renderLabel
|
|
85
|
+
},
|
|
86
|
+
child.id
|
|
87
|
+
)) }) : null
|
|
88
|
+
]
|
|
89
|
+
}
|
|
90
|
+
);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
export { TreeNodeRow };
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
'use strict';
|
|
3
|
+
|
|
4
|
+
var jsxRuntime = require('react/jsx-runtime');
|
|
5
|
+
var react = require('react');
|
|
6
|
+
var TreeNodeRow = require('./TreeNodeRow');
|
|
7
|
+
var styles = require('./TreeView.module.css');
|
|
8
|
+
var useTreeExpansion = require('./useTreeExpansion');
|
|
9
|
+
var utils = require('./utils');
|
|
10
|
+
|
|
11
|
+
function _interopDefault (e) { return e && e.__esModule ? e : { default: e }; }
|
|
12
|
+
|
|
13
|
+
var styles__default = /*#__PURE__*/_interopDefault(styles);
|
|
14
|
+
|
|
15
|
+
function TreeView({
|
|
16
|
+
data,
|
|
17
|
+
loadChildren,
|
|
18
|
+
defaultExpandedIds,
|
|
19
|
+
expandedIds,
|
|
20
|
+
onExpandedIdsChange,
|
|
21
|
+
selectedId,
|
|
22
|
+
onSelectNode,
|
|
23
|
+
renderLabel,
|
|
24
|
+
emptyMessage = "Intet at vise.",
|
|
25
|
+
loadErrorMessage = () => "Der skete en fejl under indl\xE6sning.",
|
|
26
|
+
className,
|
|
27
|
+
...rest
|
|
28
|
+
}) {
|
|
29
|
+
const { isExpanded, toggle, expand } = useTreeExpansion.useTreeExpansion(
|
|
30
|
+
defaultExpandedIds,
|
|
31
|
+
expandedIds,
|
|
32
|
+
onExpandedIdsChange
|
|
33
|
+
);
|
|
34
|
+
const [resolvedChildren, setResolvedChildren] = react.useState({});
|
|
35
|
+
const [loadingIds, setLoadingIds] = react.useState(/* @__PURE__ */ new Set());
|
|
36
|
+
const [errorIds, setErrorIds] = react.useState({});
|
|
37
|
+
const requestLoad = react.useCallback(
|
|
38
|
+
(node) => {
|
|
39
|
+
if (!loadChildren || loadingIds.has(node.id)) return;
|
|
40
|
+
setLoadingIds((prev) => new Set(prev).add(node.id));
|
|
41
|
+
setErrorIds((prev) => {
|
|
42
|
+
if (!(node.id in prev)) return prev;
|
|
43
|
+
const next = { ...prev };
|
|
44
|
+
delete next[node.id];
|
|
45
|
+
return next;
|
|
46
|
+
});
|
|
47
|
+
loadChildren(node).then((children) => {
|
|
48
|
+
setResolvedChildren((prev) => ({ ...prev, [node.id]: children }));
|
|
49
|
+
expand(node.id);
|
|
50
|
+
}).catch((err) => {
|
|
51
|
+
setErrorIds((prev) => ({ ...prev, [node.id]: loadErrorMessage(err, node) }));
|
|
52
|
+
}).finally(() => {
|
|
53
|
+
setLoadingIds((prev) => {
|
|
54
|
+
const next = new Set(prev);
|
|
55
|
+
next.delete(node.id);
|
|
56
|
+
return next;
|
|
57
|
+
});
|
|
58
|
+
});
|
|
59
|
+
},
|
|
60
|
+
[loadChildren, loadingIds, expand, loadErrorMessage]
|
|
61
|
+
);
|
|
62
|
+
if (data.length === 0) {
|
|
63
|
+
return /* @__PURE__ */ jsxRuntime.jsx("div", { className: [styles__default.default.emptyState, className].filter(Boolean).join(" "), ...rest, children: emptyMessage });
|
|
64
|
+
}
|
|
65
|
+
return /* @__PURE__ */ jsxRuntime.jsx("div", { role: "tree", className: [styles__default.default.tree, className].filter(Boolean).join(" "), ...rest, children: data.map((node) => /* @__PURE__ */ jsxRuntime.jsx(
|
|
66
|
+
TreeNodeRow.TreeNodeRow,
|
|
67
|
+
{
|
|
68
|
+
node,
|
|
69
|
+
depth: 0,
|
|
70
|
+
isExpanded,
|
|
71
|
+
toggle,
|
|
72
|
+
resolvedChildren,
|
|
73
|
+
loadingIds,
|
|
74
|
+
errorIds,
|
|
75
|
+
requestLoad,
|
|
76
|
+
loadChildren,
|
|
77
|
+
selectedId,
|
|
78
|
+
onSelectNode,
|
|
79
|
+
renderLabel
|
|
80
|
+
},
|
|
81
|
+
node.id
|
|
82
|
+
)) });
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
Object.defineProperty(exports, "collectExpandableIds", {
|
|
86
|
+
enumerable: true,
|
|
87
|
+
get: function () { return utils.collectExpandableIds; }
|
|
88
|
+
});
|
|
89
|
+
Object.defineProperty(exports, "findAncestorIds", {
|
|
90
|
+
enumerable: true,
|
|
91
|
+
get: function () { return utils.findAncestorIds; }
|
|
92
|
+
});
|
|
93
|
+
exports.TreeView = TreeView;
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import type { JSX } from 'react';
|
|
2
|
+
import type { TreeViewProps } from './types';
|
|
3
|
+
export type { LoadTreeChildren, TreeNode, TreeViewProps } from './types';
|
|
4
|
+
export { collectExpandableIds, findAncestorIds } from './utils';
|
|
5
|
+
/**
|
|
6
|
+
* A single component for both shapes of tree data:
|
|
7
|
+
* - Eager: every node's full subtree is already in `data` via `children`.
|
|
8
|
+
* No `loadChildren` needed - toggling a node only ever flips its
|
|
9
|
+
* expanded/collapsed state.
|
|
10
|
+
* - Lazy: `data` only has to contain what you already know; pass
|
|
11
|
+
* `loadChildren` and a node whose `children` is undefined gets fetched
|
|
12
|
+
* the first time it's expanded (see TreeNode's docs on `hasChildren` for
|
|
13
|
+
* how a not-yet-fetched node still shows a toggle).
|
|
14
|
+
*
|
|
15
|
+
* Expand/collapse state follows the same model as the Sidebar: a single
|
|
16
|
+
* `Set<string>` of expanded ids is the source of truth (see
|
|
17
|
+
* useTreeExpansion), and it can be left uncontrolled or owned by the
|
|
18
|
+
* caller via `expandedIds`/`onExpandedIdsChange`.
|
|
19
|
+
*/
|
|
20
|
+
export declare function TreeView<T = unknown>({ data, loadChildren, defaultExpandedIds, expandedIds, onExpandedIdsChange, selectedId, onSelectNode, renderLabel, emptyMessage, loadErrorMessage, className, ...rest }: TreeViewProps<T>): JSX.Element;
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import { jsx } from 'react/jsx-runtime';
|
|
3
|
+
import { useState, useCallback } from 'react';
|
|
4
|
+
import { TreeNodeRow } from './TreeNodeRow';
|
|
5
|
+
import styles from './TreeView.module.css';
|
|
6
|
+
import { useTreeExpansion } from './useTreeExpansion';
|
|
7
|
+
export { collectExpandableIds, findAncestorIds } from './utils';
|
|
8
|
+
|
|
9
|
+
function TreeView({
|
|
10
|
+
data,
|
|
11
|
+
loadChildren,
|
|
12
|
+
defaultExpandedIds,
|
|
13
|
+
expandedIds,
|
|
14
|
+
onExpandedIdsChange,
|
|
15
|
+
selectedId,
|
|
16
|
+
onSelectNode,
|
|
17
|
+
renderLabel,
|
|
18
|
+
emptyMessage = "Intet at vise.",
|
|
19
|
+
loadErrorMessage = () => "Der skete en fejl under indl\xE6sning.",
|
|
20
|
+
className,
|
|
21
|
+
...rest
|
|
22
|
+
}) {
|
|
23
|
+
const { isExpanded, toggle, expand } = useTreeExpansion(
|
|
24
|
+
defaultExpandedIds,
|
|
25
|
+
expandedIds,
|
|
26
|
+
onExpandedIdsChange
|
|
27
|
+
);
|
|
28
|
+
const [resolvedChildren, setResolvedChildren] = useState({});
|
|
29
|
+
const [loadingIds, setLoadingIds] = useState(/* @__PURE__ */ new Set());
|
|
30
|
+
const [errorIds, setErrorIds] = useState({});
|
|
31
|
+
const requestLoad = useCallback(
|
|
32
|
+
(node) => {
|
|
33
|
+
if (!loadChildren || loadingIds.has(node.id)) return;
|
|
34
|
+
setLoadingIds((prev) => new Set(prev).add(node.id));
|
|
35
|
+
setErrorIds((prev) => {
|
|
36
|
+
if (!(node.id in prev)) return prev;
|
|
37
|
+
const next = { ...prev };
|
|
38
|
+
delete next[node.id];
|
|
39
|
+
return next;
|
|
40
|
+
});
|
|
41
|
+
loadChildren(node).then((children) => {
|
|
42
|
+
setResolvedChildren((prev) => ({ ...prev, [node.id]: children }));
|
|
43
|
+
expand(node.id);
|
|
44
|
+
}).catch((err) => {
|
|
45
|
+
setErrorIds((prev) => ({ ...prev, [node.id]: loadErrorMessage(err, node) }));
|
|
46
|
+
}).finally(() => {
|
|
47
|
+
setLoadingIds((prev) => {
|
|
48
|
+
const next = new Set(prev);
|
|
49
|
+
next.delete(node.id);
|
|
50
|
+
return next;
|
|
51
|
+
});
|
|
52
|
+
});
|
|
53
|
+
},
|
|
54
|
+
[loadChildren, loadingIds, expand, loadErrorMessage]
|
|
55
|
+
);
|
|
56
|
+
if (data.length === 0) {
|
|
57
|
+
return /* @__PURE__ */ jsx("div", { className: [styles.emptyState, className].filter(Boolean).join(" "), ...rest, children: emptyMessage });
|
|
58
|
+
}
|
|
59
|
+
return /* @__PURE__ */ jsx("div", { role: "tree", className: [styles.tree, className].filter(Boolean).join(" "), ...rest, children: data.map((node) => /* @__PURE__ */ jsx(
|
|
60
|
+
TreeNodeRow,
|
|
61
|
+
{
|
|
62
|
+
node,
|
|
63
|
+
depth: 0,
|
|
64
|
+
isExpanded,
|
|
65
|
+
toggle,
|
|
66
|
+
resolvedChildren,
|
|
67
|
+
loadingIds,
|
|
68
|
+
errorIds,
|
|
69
|
+
requestLoad,
|
|
70
|
+
loadChildren,
|
|
71
|
+
selectedId,
|
|
72
|
+
onSelectNode,
|
|
73
|
+
renderLabel
|
|
74
|
+
},
|
|
75
|
+
node.id
|
|
76
|
+
)) });
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export { TreeView };
|