@dbcdk/react-components 0.0.208 → 0.0.210

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 (54) hide show
  1. package/dist/client.cjs +10 -0
  2. package/dist/client.d.ts +4 -0
  3. package/dist/client.js +2 -0
  4. package/dist/components/forms/markdown-editor/MarkdownEditor.cjs +475 -0
  5. package/dist/components/forms/markdown-editor/MarkdownEditor.d.ts +68 -0
  6. package/dist/components/forms/markdown-editor/MarkdownEditor.js +468 -0
  7. package/dist/components/forms/markdown-editor/MarkdownEditor.module.css +202 -0
  8. package/dist/components/forms/markdown-editor/MarkdownViewer.cjs +29 -0
  9. package/dist/components/forms/markdown-editor/MarkdownViewer.d.ts +27 -0
  10. package/dist/components/forms/markdown-editor/MarkdownViewer.js +23 -0
  11. package/dist/components/forms/markdown-editor/MarkdownViewer.module.css +97 -0
  12. package/dist/components/forms/markdown-editor/markdown.utils.cjs +325 -0
  13. package/dist/components/forms/markdown-editor/markdown.utils.d.ts +66 -0
  14. package/dist/components/forms/markdown-editor/markdown.utils.js +310 -0
  15. package/dist/components/listbox/Listbox.cjs +2 -2
  16. package/dist/components/listbox/Listbox.d.ts +2 -2
  17. package/dist/components/listbox/Listbox.js +2 -2
  18. package/dist/components/meta-bar/MetaBar.cjs +2 -2
  19. package/dist/components/meta-bar/MetaBar.d.ts +2 -1
  20. package/dist/components/meta-bar/MetaBar.js +2 -2
  21. package/dist/components/meta-bar/MetaBar.module.css +7 -0
  22. package/dist/components/overlay/side-panel/SidePanel.cjs +80 -2
  23. package/dist/components/overlay/side-panel/SidePanel.js +80 -2
  24. package/dist/components/overlay/tooltip/TooltipProvider.cjs +10 -1
  25. package/dist/components/overlay/tooltip/TooltipProvider.js +11 -2
  26. package/dist/components/overlay/tooltip/useTooltipTrigger.cjs +9 -2
  27. package/dist/components/overlay/tooltip/useTooltipTrigger.js +9 -2
  28. package/dist/components/sidebar/components/sidebar-container/SidebarContainer.cjs +1 -1
  29. package/dist/components/sidebar/components/sidebar-container/SidebarContainer.js +1 -1
  30. package/dist/components/split-pane/SplitPane.cjs +22 -7
  31. package/dist/components/split-pane/SplitPane.d.ts +5 -2
  32. package/dist/components/split-pane/SplitPane.js +22 -7
  33. package/dist/components/split-pane/SplitPane.module.css +9 -0
  34. package/dist/components/split-pane/provider/SplitPaneContext.cjs +5 -2
  35. package/dist/components/split-pane/provider/SplitPaneContext.d.ts +3 -1
  36. package/dist/components/split-pane/provider/SplitPaneContext.js +5 -2
  37. package/dist/components/tabs/Tabs.cjs +83 -69
  38. package/dist/components/tabs/Tabs.d.ts +5 -1
  39. package/dist/components/tabs/Tabs.js +83 -69
  40. package/dist/components/tabs/Tabs.module.css +39 -0
  41. package/dist/components/tree-view/TreeNodeRow.cjs +70 -30
  42. package/dist/components/tree-view/TreeNodeRow.d.ts +6 -1
  43. package/dist/components/tree-view/TreeNodeRow.js +70 -30
  44. package/dist/components/tree-view/TreeView.cjs +107 -19
  45. package/dist/components/tree-view/TreeView.d.ts +1 -1
  46. package/dist/components/tree-view/TreeView.js +107 -19
  47. package/dist/components/tree-view/TreeView.module.css +47 -3
  48. package/dist/components/tree-view/types.d.ts +6 -0
  49. package/dist/index.cjs +213 -208
  50. package/dist/index.css +2 -0
  51. package/dist/index.d.ts +2 -0
  52. package/dist/index.js +1 -0
  53. package/dist/styles/css-helper-classes/typography.css +4 -0
  54. package/package.json +4 -1
@@ -0,0 +1,310 @@
1
+ import { jsx, jsxs } from 'react/jsx-runtime';
2
+ import MarkdownIt from 'markdown-it';
3
+ import React from 'react';
4
+
5
+ const ESCAPE_PATTERN = /\\[!-/:-@[-`{-~]/;
6
+ const LINK_PATTERN = /\[((?:\\[^\n]|[^\]\\\n])+)\]\(((?:[^()\s]|\([^()\s]*\))+)\)/;
7
+ const escapeRegExp = (text) => text.replace(/[.*+?^$()|[\]\\{}]/g, "\\$&");
8
+ const linkPattern = (urlPlaceholder) => new RegExp(LINK_PATTERN.source.replace(/\)\\\)$/, `|${escapeRegExp(urlPlaceholder)})\\)`));
9
+ const LINK_PLACEHOLDER_TEXT = "linktekst";
10
+ const LINK_PLACEHOLDER_URL = "inds\xE6t url";
11
+ const parser = new MarkdownIt();
12
+ parser.validateLink = (url) => getSafeHref(url) !== void 0;
13
+ function getSafeHref(url) {
14
+ if ([...url].some((character) => character < " " || character === "\x7F")) return void 0;
15
+ const hasScheme = /^[a-z][a-z0-9+.-]*:/i.test(url);
16
+ return !hasScheme || /^(https?|mailto|tel):/i.test(url) ? url : void 0;
17
+ }
18
+ function findLinkAt(value, selectionStart, selectionEnd, urlPlaceholder = LINK_PLACEHOLDER_URL) {
19
+ const pattern = new RegExp(`${ESCAPE_PATTERN.source}|${linkPattern(urlPlaceholder).source}`, "g");
20
+ let match;
21
+ while ((match = pattern.exec(value)) !== null) {
22
+ if (match.index > selectionStart) return void 0;
23
+ const end = match.index + match[0].length;
24
+ if (match[1] !== void 0 && selectionEnd <= end) {
25
+ return { start: match.index, end, text: match[1] };
26
+ }
27
+ }
28
+ return void 0;
29
+ }
30
+ function escapeMarkdown(value) {
31
+ return value.replace(/[!-/:-@[-`{-~]/g, "\\$&");
32
+ }
33
+ function unescapeMarkdown(value) {
34
+ return value.replace(/\\([!-/:-@[-`{-~])/g, "$1");
35
+ }
36
+ const INLINE_SYNTAX = /\\(?=[!-/:-@[-`{-~]|$)|[`*_[<~|]|&(?=#?[a-z0-9]+;)/gi;
37
+ const BLOCK_SYNTAX = /^(\s*)(#{1,6}(?=\s|$)|>|[-+=](?=\s|$)|[-=]+\s*$)|^(\s*\d+)([.)])(?=\s|$)/;
38
+ const INDENT_ENTITIES = { " ": "&nbsp;", " ": "&#9;" };
39
+ const ENCODED_INDENT = /^(?:&nbsp;|&#9;)+/gm;
40
+ function escapePlainLine(line) {
41
+ if (!line.trim()) return line;
42
+ const indent = /^[ \t]*/.exec(line)[0];
43
+ const escaped = line.slice(indent.length).replace(INLINE_SYNTAX, "\\$&");
44
+ if (indent) return `${indent.replace(/[ \t]/g, (space) => INDENT_ENTITIES[space])}${escaped}`;
45
+ return escaped.replace(
46
+ BLOCK_SYNTAX,
47
+ (_, indent2, marker, number, delimiter) => marker === void 0 ? `${number}\\${delimiter}` : `${indent2}\\${marker}`
48
+ );
49
+ }
50
+ function plainTextToMarkdown(text) {
51
+ const lines = text.split("\n");
52
+ return lines.map((line, index) => {
53
+ const escaped = escapePlainLine(line);
54
+ return index < lines.length - 1 && line.trim() && lines[index + 1].trim() ? `${escaped}\\` : escaped;
55
+ }).join("\n");
56
+ }
57
+ function limitPlainText(previous, next, maxLength) {
58
+ if (maxLength === void 0 || next.length < previous.length || plainTextToMarkdown(next).length <= maxLength) {
59
+ return { text: next, caret: -1 };
60
+ }
61
+ let start = 0;
62
+ while (start < previous.length && start < next.length && previous[start] === next[start]) start++;
63
+ let end = 0;
64
+ while (end < previous.length - start && end < next.length - start && previous[previous.length - 1 - end] === next[next.length - 1 - end]) {
65
+ end++;
66
+ }
67
+ const inserted = next.slice(start, next.length - end);
68
+ const withInserted = (length) => `${next.slice(0, start)}${inserted.slice(0, length)}${next.slice(next.length - end)}`;
69
+ let low = 0;
70
+ let high = inserted.length;
71
+ while (low < high) {
72
+ const middle = Math.ceil((low + high) / 2);
73
+ if (plainTextToMarkdown(withInserted(middle)).length <= maxLength) low = middle;
74
+ else high = middle - 1;
75
+ }
76
+ const text = withInserted(low);
77
+ return text === previous ? void 0 : { text, caret: start + low };
78
+ }
79
+ function markdownToPlainText(markdown) {
80
+ return markdown.replace(ENCODED_INDENT, (indent) => indent.replace(/&nbsp;/g, " ").replace(/&#9;/g, " ")).replace(/\\([!-/:-@[-`{-~\n])/g, "$1");
81
+ }
82
+ function isPlainMarkdown(markdown) {
83
+ const normalize = (html) => html.replace(/<br>\n/g, "\n").replace(/(^|>|\n)[\u00a0\t]+/g, "$1");
84
+ return normalize(parser.render(markdown)) === normalize(parser.render(plainTextToMarkdown(markdownToPlainText(markdown))));
85
+ }
86
+ const SELECTION_SYNTAX = /\\[!-/:-@[-`{-~]|[`*_[<~|]|&(?=#?[a-z0-9]+;)/gi;
87
+ function escapeSyntax(text, startsLine) {
88
+ return text.split("\n").map((line, index) => {
89
+ const escaped = line.replace(
90
+ SELECTION_SYNTAX,
91
+ (part) => part.length === 2 ? part : `\\${part}`
92
+ );
93
+ if (index === 0 && !startsLine) return escaped;
94
+ return escaped.replace(
95
+ BLOCK_SYNTAX,
96
+ (_, indent, marker, number, delimiter) => marker === void 0 ? `${number}\\${delimiter}` : `${indent}\\${marker}`
97
+ );
98
+ }).join("\n");
99
+ }
100
+ function isSelectionEscaped(value, selectionStart, selectionEnd) {
101
+ const selectedText = value.slice(selectionStart, selectionEnd);
102
+ const startsLine = selectionStart === 0 || value[selectionStart - 1] === "\n";
103
+ return ESCAPE_PATTERN.test(selectedText) && escapeSyntax(selectedText, startsLine) === selectedText;
104
+ }
105
+ function isEscaped(value, index) {
106
+ let backslashes = 0;
107
+ while (index > 0 && value[--index] === "\\") backslashes++;
108
+ return backslashes % 2 === 1;
109
+ }
110
+ function formatMarkdownSelection(value, selectionStart, selectionEnd, format) {
111
+ var _a, _b;
112
+ if (format.type === "link") {
113
+ const textPlaceholder = (_a = format.textPlaceholder) != null ? _a : LINK_PLACEHOLDER_TEXT;
114
+ const urlPlaceholder = (_b = format.urlPlaceholder) != null ? _b : LINK_PLACEHOLDER_URL;
115
+ const link = findLinkAt(value, selectionStart, selectionEnd, urlPlaceholder);
116
+ if (link) {
117
+ return {
118
+ value: `${value.slice(0, link.start)}${link.text}${value.slice(link.end)}`,
119
+ selectionStart: link.start,
120
+ selectionEnd: link.start + link.text.length
121
+ };
122
+ }
123
+ const selectedText = value.slice(selectionStart, selectionEnd);
124
+ const textStart = selectionStart + 1;
125
+ if (/^(https?:\/\/|mailto:)\S+$/i.test(selectedText)) {
126
+ return {
127
+ value: `${value.slice(0, selectionStart)}[${textPlaceholder}](${selectedText})${value.slice(selectionEnd)}`,
128
+ selectionStart: textStart,
129
+ selectionEnd: textStart + textPlaceholder.length
130
+ };
131
+ }
132
+ const text = selectedText || textPlaceholder;
133
+ const urlStart = textStart + text.length + 2;
134
+ return {
135
+ value: `${value.slice(0, selectionStart)}[${text}](${urlPlaceholder})${value.slice(selectionEnd)}`,
136
+ selectionStart: selectedText ? urlStart : textStart,
137
+ selectionEnd: selectedText ? urlStart + urlPlaceholder.length : textStart + text.length
138
+ };
139
+ }
140
+ if (format.type === "escape") {
141
+ const selectedText = value.slice(selectionStart, selectionEnd);
142
+ const startsLine = selectionStart === 0 || value[selectionStart - 1] === "\n";
143
+ const replacement2 = isSelectionEscaped(value, selectionStart, selectionEnd) ? unescapeMarkdown(selectedText) : escapeSyntax(selectedText, startsLine);
144
+ return {
145
+ value: `${value.slice(0, selectionStart)}${replacement2}${value.slice(selectionEnd)}`,
146
+ selectionStart,
147
+ selectionEnd: selectionStart + replacement2.length
148
+ };
149
+ }
150
+ if (format.type === "wrap") {
151
+ const selectedText = value.slice(selectionStart, selectionEnd);
152
+ const markerStart = selectionStart - format.marker.length;
153
+ const isAlreadyWrapped = isMarkdownFormatActive(value, selectionStart, selectionEnd, format);
154
+ if (isAlreadyWrapped) {
155
+ return {
156
+ value: `${value.slice(0, markerStart)}${selectedText}${value.slice(selectionEnd + format.marker.length)}`,
157
+ selectionStart: markerStart,
158
+ selectionEnd: markerStart + selectedText.length
159
+ };
160
+ }
161
+ const replacement2 = `${format.marker}${selectedText}${format.marker}`;
162
+ const innerStart = selectionStart + format.marker.length;
163
+ return {
164
+ value: `${value.slice(0, selectionStart)}${replacement2}${value.slice(selectionEnd)}`,
165
+ selectionStart: innerStart,
166
+ selectionEnd: innerStart + selectedText.length
167
+ };
168
+ }
169
+ const previousNewline = selectionStart > 0 ? value.lastIndexOf("\n", selectionStart - 1) : -1;
170
+ const lineStart = previousNewline + 1;
171
+ const selectedEnd = selectionEnd > selectionStart && value[selectionEnd - 1] === "\n" ? selectionEnd - 1 : selectionEnd;
172
+ const lineEndIndex = value.indexOf("\n", selectedEnd);
173
+ const lineEnd = lineEndIndex === -1 ? value.length : lineEndIndex;
174
+ const selectedLines = value.slice(lineStart, lineEnd).split("\n");
175
+ const removePrefix = format.toggle && isMarkdownFormatActive(value, selectionStart, selectionEnd, format);
176
+ const formattedLines = selectedLines.map((line) => {
177
+ if (removePrefix) return line.slice(format.prefix.length);
178
+ if (format.prefix.startsWith("#")) return `${format.prefix}${line.replace(/^#{1,6}\s+/, "")}`;
179
+ return `${format.prefix}${line}`;
180
+ });
181
+ const replacement = formattedLines.join("\n");
182
+ return {
183
+ value: `${value.slice(0, lineStart)}${replacement}${value.slice(lineEnd)}`,
184
+ selectionStart: lineStart,
185
+ selectionEnd: lineStart + replacement.length
186
+ };
187
+ }
188
+ function isMarkdownFormatActive(value, selectionStart, selectionEnd, format) {
189
+ if (format.type === "link") {
190
+ return findLinkAt(value, selectionStart, selectionEnd, format.urlPlaceholder) !== void 0;
191
+ }
192
+ if (format.type === "escape") return isSelectionEscaped(value, selectionStart, selectionEnd);
193
+ if (format.type === "wrap") {
194
+ const character = format.marker[0];
195
+ let start = selectionStart;
196
+ let end = selectionEnd;
197
+ while (start > 0 && value[start - 1] === character && !isEscaped(value, start - 1)) start--;
198
+ while (end < value.length && value[end] === character && !isEscaped(value, end)) end++;
199
+ const markerCount = Math.min(selectionStart - start, end - selectionEnd);
200
+ return format.marker.length === 1 ? markerCount % 2 === 1 : markerCount >= 2;
201
+ }
202
+ const previousNewline = selectionStart > 0 ? value.lastIndexOf("\n", selectionStart - 1) : -1;
203
+ const lineStart = previousNewline + 1;
204
+ const selectedEnd = selectionEnd > selectionStart && value[selectionEnd - 1] === "\n" ? selectionEnd - 1 : selectionEnd;
205
+ const lineEndIndex = value.indexOf("\n", selectedEnd);
206
+ const lineEnd = lineEndIndex === -1 ? value.length : lineEndIndex;
207
+ const lines = value.slice(lineStart, lineEnd).split("\n");
208
+ if (format.prefix.startsWith("#")) {
209
+ return lines.every((line) => {
210
+ var _a;
211
+ return ((_a = /^(#{1,6}\s+)/.exec(line)) == null ? void 0 : _a[0]) === format.prefix;
212
+ });
213
+ }
214
+ return lines.every((line) => line.startsWith(format.prefix));
215
+ }
216
+ function getAttr(token, name) {
217
+ const value = token.attrGet(name);
218
+ return value === null ? void 0 : String(value);
219
+ }
220
+ function toStyle(style) {
221
+ var _a;
222
+ const align = style && ((_a = /^text-align:\s*(left|right|center)$/.exec(style)) == null ? void 0 : _a[1]);
223
+ return align ? { textAlign: align } : void 0;
224
+ }
225
+ function renderLeaf(token, key, options) {
226
+ var _a, _b;
227
+ switch (token.type) {
228
+ case "text":
229
+ return token.content;
230
+ case "softbreak":
231
+ return "\n";
232
+ case "hardbreak":
233
+ return /* @__PURE__ */ jsx("br", {}, key);
234
+ case "code_inline":
235
+ return /* @__PURE__ */ jsx("code", { children: token.content }, key);
236
+ case "code_block":
237
+ return /* @__PURE__ */ jsx("pre", { children: /* @__PURE__ */ jsx("code", { children: token.content }) }, key);
238
+ case "fence": {
239
+ const language = token.info.trim().split(/\s+/)[0];
240
+ return /* @__PURE__ */ jsx("pre", { children: /* @__PURE__ */ jsx("code", { className: language ? `language-${language}` : void 0, children: token.content }) }, key);
241
+ }
242
+ case "hr":
243
+ return /* @__PURE__ */ jsx("hr", {}, key);
244
+ case "image":
245
+ return parser.renderer.renderInlineAsText((_a = token.children) != null ? _a : [], parser.options, {});
246
+ case "inline":
247
+ return renderTokens((_b = token.children) != null ? _b : [], options);
248
+ default:
249
+ return token.content || null;
250
+ }
251
+ }
252
+ function renderElement(open, children, key, options) {
253
+ var _a;
254
+ if (open.hidden) return /* @__PURE__ */ jsx(React.Fragment, { children }, key);
255
+ if (/^h[1-6]$/.test(open.tag)) {
256
+ const level = Math.min(6, Math.max(1, Number(open.tag[1]) + ((_a = options.headingOffset) != null ? _a : 0)));
257
+ return React.createElement(`h${level}`, { key }, children);
258
+ }
259
+ if (open.tag === "a") {
260
+ const href = getAttr(open, "href");
261
+ const newTab = options.openLinksInNewTab && !/^(mailto|tel):/i.test(href != null ? href : "");
262
+ return /* @__PURE__ */ jsxs(
263
+ "a",
264
+ {
265
+ href,
266
+ title: getAttr(open, "title"),
267
+ target: newTab ? "_blank" : void 0,
268
+ rel: newTab ? "noopener noreferrer" : void 0,
269
+ children: [
270
+ children,
271
+ newTab && options.newTabHint
272
+ ]
273
+ },
274
+ key
275
+ );
276
+ }
277
+ if (open.tag === "ol") {
278
+ const start = getAttr(open, "start");
279
+ return /* @__PURE__ */ jsx("ol", { start: start ? Number(start) : void 0, children }, key);
280
+ }
281
+ if (open.tag === "th" || open.tag === "td") {
282
+ return React.createElement(open.tag, { key, style: toStyle(getAttr(open, "style")) }, children);
283
+ }
284
+ return React.createElement(open.tag, { key }, children);
285
+ }
286
+ function renderTokens(tokens, options) {
287
+ const root = [];
288
+ const stack = [];
289
+ tokens.forEach((token, index) => {
290
+ const siblings = stack.length ? stack[stack.length - 1].children : root;
291
+ if (token.nesting === 1) {
292
+ stack.push({ open: token, children: [] });
293
+ } else if (token.nesting === -1) {
294
+ const element = stack.pop();
295
+ if (!element) return;
296
+ const parent = stack.length ? stack[stack.length - 1].children : root;
297
+ parent.push(renderElement(element.open, element.children, index, options));
298
+ } else {
299
+ const leaf = renderLeaf(token, index, options);
300
+ if (Array.isArray(leaf)) siblings.push(/* @__PURE__ */ jsx(React.Fragment, { children: leaf }, index));
301
+ else if (leaf !== null) siblings.push(leaf);
302
+ }
303
+ });
304
+ return root;
305
+ }
306
+ function renderMarkdown(value, options = {}) {
307
+ return renderTokens(parser.parse(value, {}), options);
308
+ }
309
+
310
+ export { escapeMarkdown, formatMarkdownSelection, getSafeHref, isMarkdownFormatActive, isPlainMarkdown, limitPlainText, markdownToPlainText, plainTextToMarkdown, renderMarkdown };
@@ -111,9 +111,9 @@ function Listbox({
111
111
  ].filter(Boolean).join(" "),
112
112
  disabled: option.disabled,
113
113
  onFocus: () => setActiveValue(option.value),
114
- onClick: () => {
114
+ onClick: (event) => {
115
115
  setActiveValue(option.value);
116
- onOptionSelect(option.value);
116
+ onOptionSelect(option.value, event);
117
117
  },
118
118
  children: /* @__PURE__ */ jsxRuntime.jsxs("span", { className: styles__default.default.optionContent, children: [
119
119
  option.icon ? /* @__PURE__ */ jsxRuntime.jsx("span", { className: styles__default.default.icon, "aria-hidden": "true", children: option.icon }) : null,
@@ -1,4 +1,4 @@
1
- import { type HTMLAttributes, type ReactNode } from 'react';
1
+ import { type HTMLAttributes, type MouseEvent, type ReactNode } from 'react';
2
2
  export type ListboxOption<T extends string | number = string> = {
3
3
  value: T;
4
4
  label: ReactNode;
@@ -15,7 +15,7 @@ export type ListboxProps<T extends string | number = string> = Omit<HTMLAttribut
15
15
  selectedValues: readonly T[];
16
16
  selectionMode?: 'single' | 'multiple';
17
17
  autoFocusValue?: T;
18
- onOptionSelect: (value: T) => void;
18
+ onOptionSelect: (value: T, event: MouseEvent<HTMLButtonElement>) => void;
19
19
  onActiveChange?: (value: T) => void;
20
20
  renderAction?: (option: ListboxOption<T>, selected: boolean) => ReactNode;
21
21
  };
@@ -104,9 +104,9 @@ function Listbox({
104
104
  ].filter(Boolean).join(" "),
105
105
  disabled: option.disabled,
106
106
  onFocus: () => setActiveValue(option.value),
107
- onClick: () => {
107
+ onClick: (event) => {
108
108
  setActiveValue(option.value);
109
- onOptionSelect(option.value);
109
+ onOptionSelect(option.value, event);
110
110
  },
111
111
  children: /* @__PURE__ */ jsxs("span", { className: styles.optionContent, children: [
112
112
  option.icon ? /* @__PURE__ */ jsx("span", { className: styles.icon, "aria-hidden": "true", children: option.icon }) : null,
@@ -8,12 +8,12 @@ function _interopDefault (e) { return e && e.__esModule ? e : { default: e }; }
8
8
 
9
9
  var styles__default = /*#__PURE__*/_interopDefault(styles);
10
10
 
11
- function MetaBar({ items, separator = "\u2022" }) {
11
+ function MetaBar({ items, separator = "\u2022", size = "sm" }) {
12
12
  const visibleItems = items.filter(
13
13
  (item) => item.value !== null && item.value !== void 0 && item.value !== ""
14
14
  );
15
15
  if (visibleItems.length === 0) return null;
16
- return /* @__PURE__ */ jsxRuntime.jsx("div", { className: styles__default.default.meta, children: visibleItems.map((item, index) => /* @__PURE__ */ jsxRuntime.jsxs(react.Fragment, { children: [
16
+ return /* @__PURE__ */ jsxRuntime.jsx("div", { className: `${styles__default.default.meta} ${styles__default.default[size]}`, children: visibleItems.map((item, index) => /* @__PURE__ */ jsxRuntime.jsxs(react.Fragment, { children: [
17
17
  /* @__PURE__ */ jsxRuntime.jsxs("span", { className: item.muted === false ? styles__default.default.itemDefault : styles__default.default.itemMuted, children: [
18
18
  item.label && /* @__PURE__ */ jsxRuntime.jsxs("span", { children: [
19
19
  item.label,
@@ -6,7 +6,8 @@ export interface MetaItem {
6
6
  }
7
7
  interface MetaBarProps {
8
8
  items: MetaItem[];
9
+ size?: 'sm' | 'md';
9
10
  separator?: ReactNode;
10
11
  }
11
- export declare function MetaBar({ items, separator }: MetaBarProps): JSX.Element | null;
12
+ export declare function MetaBar({ items, separator, size }: MetaBarProps): JSX.Element | null;
12
13
  export {};
@@ -2,12 +2,12 @@ import { jsx, jsxs } from 'react/jsx-runtime';
2
2
  import { Fragment } from 'react';
3
3
  import styles from './MetaBar.module.css';
4
4
 
5
- function MetaBar({ items, separator = "\u2022" }) {
5
+ function MetaBar({ items, separator = "\u2022", size = "sm" }) {
6
6
  const visibleItems = items.filter(
7
7
  (item) => item.value !== null && item.value !== void 0 && item.value !== ""
8
8
  );
9
9
  if (visibleItems.length === 0) return null;
10
- return /* @__PURE__ */ jsx("div", { className: styles.meta, children: visibleItems.map((item, index) => /* @__PURE__ */ jsxs(Fragment, { children: [
10
+ return /* @__PURE__ */ jsx("div", { className: `${styles.meta} ${styles[size]}`, children: visibleItems.map((item, index) => /* @__PURE__ */ jsxs(Fragment, { children: [
11
11
  /* @__PURE__ */ jsxs("span", { className: item.muted === false ? styles.itemDefault : styles.itemMuted, children: [
12
12
  item.label && /* @__PURE__ */ jsxs("span", { children: [
13
13
  item.label,
@@ -3,9 +3,16 @@
3
3
  flex-wrap: wrap;
4
4
  align-items: center;
5
5
  gap: var(--spacing-xs);
6
+ }
7
+
8
+ .sm {
6
9
  font-size: var(--font-size-xs);
7
10
  }
8
11
 
12
+ .md {
13
+ font-size: var(--font-size-sm);
14
+ }
15
+
9
16
  .itemMuted,
10
17
  .itemDefault {
11
18
  display: inline-flex;
@@ -13,6 +13,36 @@ function _interopDefault (e) { return e && e.__esModule ? e : { default: e }; }
13
13
 
14
14
  var styles__default = /*#__PURE__*/_interopDefault(styles);
15
15
 
16
+ function getFocusable(container) {
17
+ var _a, _b;
18
+ const selector = [
19
+ 'a[href]:not([aria-disabled="true"])',
20
+ "button:not([disabled])",
21
+ "input:not([disabled])",
22
+ "select:not([disabled])",
23
+ "textarea:not([disabled])",
24
+ '[tabindex]:not([tabindex="-1"])'
25
+ ].join(",");
26
+ const focusables = [];
27
+ const addFocusableContent = (root) => {
28
+ const candidates = root.querySelectorAll(selector);
29
+ for (const element of candidates) {
30
+ if (element.hasAttribute("hidden") || element.getAttribute("aria-hidden") === "true" || element.getAttribute("aria-disabled") === "true") {
31
+ continue;
32
+ }
33
+ if (!focusables.includes(element)) focusables.push(element);
34
+ }
35
+ };
36
+ addFocusableContent(container);
37
+ for (const trigger of container.querySelectorAll("[aria-controls]")) {
38
+ const controlledIds = (_b = (_a = trigger.getAttribute("aria-controls")) == null ? void 0 : _a.split(/\s+/)) != null ? _b : [];
39
+ for (const controlledId of controlledIds) {
40
+ const controlledContent = document.getElementById(controlledId);
41
+ if (controlledContent) addFocusableContent(controlledContent);
42
+ }
43
+ }
44
+ return focusables;
45
+ }
16
46
  function SidePanel({
17
47
  isOpen,
18
48
  onClose,
@@ -36,23 +66,70 @@ function SidePanel({
36
66
  const [shouldRender, setShouldRender] = react.useState(isOpen);
37
67
  const [isActive, setIsActive] = react.useState(false);
38
68
  const panelRef = react.useRef(null);
69
+ const returnFocusRef = react.useRef(null);
70
+ const wasOpenRef = react.useRef(false);
39
71
  react.useEffect(() => setMounted(true), []);
40
72
  react.useEffect(() => {
41
73
  if (isOpen) setShouldRender(true);
42
74
  }, [isOpen]);
43
75
  react.useEffect(() => {
44
- if (!isOpen) return;
76
+ if (!isOpen || !shouldRender || !mounted) return;
77
+ if (!wasOpenRef.current) {
78
+ const activeElement = document.activeElement;
79
+ returnFocusRef.current = activeElement instanceof HTMLElement ? activeElement : null;
80
+ wasOpenRef.current = true;
81
+ }
82
+ const focusInitial = () => {
83
+ var _a;
84
+ const panel = panelRef.current;
85
+ if (!panel) return;
86
+ const closeButton = panel.querySelector(
87
+ '[data-cy="side-panel-close-button"]'
88
+ );
89
+ const focusables = getFocusable(panel);
90
+ ((_a = closeButton != null ? closeButton : focusables[0]) != null ? _a : panel).focus();
91
+ };
92
+ focusInitial();
93
+ const frame = requestAnimationFrame(focusInitial);
45
94
  const handleKeyDown = (e) => {
46
95
  if (e.key === "Escape") {
47
96
  e.stopPropagation();
48
97
  onClose();
98
+ return;
99
+ }
100
+ if (e.key !== "Tab") return;
101
+ const panel = panelRef.current;
102
+ if (!panel) return;
103
+ const focusables = getFocusable(panel);
104
+ if (focusables.length === 0) {
105
+ e.preventDefault();
106
+ panel.focus();
107
+ return;
108
+ }
109
+ const first = focusables[0];
110
+ const last = focusables[focusables.length - 1];
111
+ const active = document.activeElement;
112
+ if (e.shiftKey && (active === first || !panel.contains(active))) {
113
+ e.preventDefault();
114
+ last.focus();
115
+ } else if (!e.shiftKey && (active === last || !panel.contains(active))) {
116
+ e.preventDefault();
117
+ first.focus();
49
118
  }
50
119
  };
51
120
  document.addEventListener("keydown", handleKeyDown);
52
121
  return () => {
122
+ cancelAnimationFrame(frame);
53
123
  document.removeEventListener("keydown", handleKeyDown);
54
124
  };
55
- }, [isOpen, onClose]);
125
+ }, [isOpen, mounted, onClose, shouldRender]);
126
+ react.useEffect(() => {
127
+ var _a;
128
+ if (isOpen || !wasOpenRef.current) return;
129
+ wasOpenRef.current = false;
130
+ (_a = returnFocusRef.current) == null ? void 0 : _a.focus();
131
+ returnFocusRef.current = null;
132
+ }, [isOpen]);
56
133
  react.useEffect(() => {
57
134
  if (!shouldRender) return;
58
135
  if (!isOpen) {
@@ -103,6 +180,7 @@ function SidePanel({
103
180
  "data-cy": "details-panel",
104
181
  role: "dialog",
105
182
  "aria-modal": "true",
183
+ tabIndex: -1,
106
184
  children: [
107
185
  hasDetails ? /* @__PURE__ */ jsxRuntime.jsxs("aside", { className: styles__default.default.detailsCol, "data-cy": "details-panel-details", children: [
108
186
  /* @__PURE__ */ jsxRuntime.jsxs("div", { className: styles__default.default.detailsHeader, children: [
@@ -7,6 +7,36 @@ import { Headline } from '../../../components/headline/Headline';
7
7
  import { MOTION_MS } from '../../../styles/animation';
8
8
  import styles from './SidePanel.module.css';
9
9
 
10
+ function getFocusable(container) {
11
+ var _a, _b;
12
+ const selector = [
13
+ 'a[href]:not([aria-disabled="true"])',
14
+ "button:not([disabled])",
15
+ "input:not([disabled])",
16
+ "select:not([disabled])",
17
+ "textarea:not([disabled])",
18
+ '[tabindex]:not([tabindex="-1"])'
19
+ ].join(",");
20
+ const focusables = [];
21
+ const addFocusableContent = (root) => {
22
+ const candidates = root.querySelectorAll(selector);
23
+ for (const element of candidates) {
24
+ if (element.hasAttribute("hidden") || element.getAttribute("aria-hidden") === "true" || element.getAttribute("aria-disabled") === "true") {
25
+ continue;
26
+ }
27
+ if (!focusables.includes(element)) focusables.push(element);
28
+ }
29
+ };
30
+ addFocusableContent(container);
31
+ for (const trigger of container.querySelectorAll("[aria-controls]")) {
32
+ const controlledIds = (_b = (_a = trigger.getAttribute("aria-controls")) == null ? void 0 : _a.split(/\s+/)) != null ? _b : [];
33
+ for (const controlledId of controlledIds) {
34
+ const controlledContent = document.getElementById(controlledId);
35
+ if (controlledContent) addFocusableContent(controlledContent);
36
+ }
37
+ }
38
+ return focusables;
39
+ }
10
40
  function SidePanel({
11
41
  isOpen,
12
42
  onClose,
@@ -30,23 +60,70 @@ function SidePanel({
30
60
  const [shouldRender, setShouldRender] = useState(isOpen);
31
61
  const [isActive, setIsActive] = useState(false);
32
62
  const panelRef = useRef(null);
63
+ const returnFocusRef = useRef(null);
64
+ const wasOpenRef = useRef(false);
33
65
  useEffect(() => setMounted(true), []);
34
66
  useEffect(() => {
35
67
  if (isOpen) setShouldRender(true);
36
68
  }, [isOpen]);
37
69
  useEffect(() => {
38
- if (!isOpen) return;
70
+ if (!isOpen || !shouldRender || !mounted) return;
71
+ if (!wasOpenRef.current) {
72
+ const activeElement = document.activeElement;
73
+ returnFocusRef.current = activeElement instanceof HTMLElement ? activeElement : null;
74
+ wasOpenRef.current = true;
75
+ }
76
+ const focusInitial = () => {
77
+ var _a;
78
+ const panel = panelRef.current;
79
+ if (!panel) return;
80
+ const closeButton = panel.querySelector(
81
+ '[data-cy="side-panel-close-button"]'
82
+ );
83
+ const focusables = getFocusable(panel);
84
+ ((_a = closeButton != null ? closeButton : focusables[0]) != null ? _a : panel).focus();
85
+ };
86
+ focusInitial();
87
+ const frame = requestAnimationFrame(focusInitial);
39
88
  const handleKeyDown = (e) => {
40
89
  if (e.key === "Escape") {
41
90
  e.stopPropagation();
42
91
  onClose();
92
+ return;
93
+ }
94
+ if (e.key !== "Tab") return;
95
+ const panel = panelRef.current;
96
+ if (!panel) return;
97
+ const focusables = getFocusable(panel);
98
+ if (focusables.length === 0) {
99
+ e.preventDefault();
100
+ panel.focus();
101
+ return;
102
+ }
103
+ const first = focusables[0];
104
+ const last = focusables[focusables.length - 1];
105
+ const active = document.activeElement;
106
+ if (e.shiftKey && (active === first || !panel.contains(active))) {
107
+ e.preventDefault();
108
+ last.focus();
109
+ } else if (!e.shiftKey && (active === last || !panel.contains(active))) {
110
+ e.preventDefault();
111
+ first.focus();
43
112
  }
44
113
  };
45
114
  document.addEventListener("keydown", handleKeyDown);
46
115
  return () => {
116
+ cancelAnimationFrame(frame);
47
117
  document.removeEventListener("keydown", handleKeyDown);
48
118
  };
49
- }, [isOpen, onClose]);
119
+ }, [isOpen, mounted, onClose, shouldRender]);
120
+ useEffect(() => {
121
+ var _a;
122
+ if (isOpen || !wasOpenRef.current) return;
123
+ wasOpenRef.current = false;
124
+ (_a = returnFocusRef.current) == null ? void 0 : _a.focus();
125
+ returnFocusRef.current = null;
126
+ }, [isOpen]);
50
127
  useEffect(() => {
51
128
  if (!shouldRender) return;
52
129
  if (!isOpen) {
@@ -97,6 +174,7 @@ function SidePanel({
97
174
  "data-cy": "details-panel",
98
175
  role: "dialog",
99
176
  "aria-modal": "true",
177
+ tabIndex: -1,
100
178
  children: [
101
179
  hasDetails ? /* @__PURE__ */ jsxs("aside", { className: styles.detailsCol, "data-cy": "details-panel-details", children: [
102
180
  /* @__PURE__ */ jsxs("div", { className: styles.detailsHeader, children: [