@terpjs/react-core 0.1.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.
Files changed (154) hide show
  1. package/README.md +190 -0
  2. package/package.json +44 -0
  3. package/src/AppShell.test.tsx +152 -0
  4. package/src/AppShell.tsx +554 -0
  5. package/src/Authorized.test.tsx +60 -0
  6. package/src/Authorized.tsx +21 -0
  7. package/src/Breadcrumbs.test.tsx +45 -0
  8. package/src/Breadcrumbs.tsx +110 -0
  9. package/src/ConfirmDialog.tsx +170 -0
  10. package/src/DetailPage.tsx +28 -0
  11. package/src/EmptyState.tsx +74 -0
  12. package/src/ErrorState.tsx +108 -0
  13. package/src/Field.test.tsx +53 -0
  14. package/src/Field.tsx +51 -0
  15. package/src/HubPage.test.tsx +108 -0
  16. package/src/HubPage.tsx +204 -0
  17. package/src/LoadingState.test.tsx +40 -0
  18. package/src/LoadingState.tsx +96 -0
  19. package/src/LoginView.test.tsx +57 -0
  20. package/src/LoginView.tsx +203 -0
  21. package/src/ModuleNav.test.tsx +96 -0
  22. package/src/ModuleNav.tsx +88 -0
  23. package/src/OverviewPage.tsx +26 -0
  24. package/src/Page.test.tsx +147 -0
  25. package/src/Page.tsx +158 -0
  26. package/src/PageActions.test.tsx +104 -0
  27. package/src/PageActions.tsx +72 -0
  28. package/src/ProfileView.test.tsx +112 -0
  29. package/src/ProfileView.tsx +84 -0
  30. package/src/RequireAuth.test.tsx +89 -0
  31. package/src/RequireAuth.tsx +22 -0
  32. package/src/ResourceList.test.tsx +176 -0
  33. package/src/ResourceList.tsx +123 -0
  34. package/src/TerpProvider.tsx +320 -0
  35. package/src/UserMenu.test.tsx +166 -0
  36. package/src/UserMenu.tsx +125 -0
  37. package/src/admin/AdminHub.tsx +108 -0
  38. package/src/admin/AuditLogAdmin.tsx +116 -0
  39. package/src/admin/GroupCreate.tsx +90 -0
  40. package/src/admin/GroupDetail.tsx +446 -0
  41. package/src/admin/GroupsAdmin.tsx +109 -0
  42. package/src/admin/UserCreate.tsx +115 -0
  43. package/src/admin/UserDetail.tsx +228 -0
  44. package/src/admin/UsersAdmin.tsx +111 -0
  45. package/src/admin/admin.test.tsx +537 -0
  46. package/src/admin/crumbs.tsx +14 -0
  47. package/src/admin/module.tsx +51 -0
  48. package/src/admin/roles.ts +19 -0
  49. package/src/bootstrap.test.tsx +67 -0
  50. package/src/bootstrap.tsx +270 -0
  51. package/src/capabilities.test.ts +24 -0
  52. package/src/capabilities.ts +26 -0
  53. package/src/createAuthClient.test.ts +176 -0
  54. package/src/createAuthClient.ts +105 -0
  55. package/src/dataview/DataView.test.tsx +392 -0
  56. package/src/dataview/DataView.tsx +467 -0
  57. package/src/dataview/DataViewCardList.tsx +189 -0
  58. package/src/dataview/DataViewColumnSettings.tsx +118 -0
  59. package/src/dataview/DataViewExpandableRow.tsx +67 -0
  60. package/src/dataview/DataViewPagination.tsx +113 -0
  61. package/src/dataview/DataViewRowActions.tsx +131 -0
  62. package/src/dataview/DataViewTable.tsx +359 -0
  63. package/src/dataview/DataViewToolbar.tsx +260 -0
  64. package/src/dataview/README.md +138 -0
  65. package/src/dataview/glyphs.tsx +175 -0
  66. package/src/dataview/hooks/hooks.test.tsx +240 -0
  67. package/src/dataview/hooks/useDataViewQuery.ts +72 -0
  68. package/src/dataview/hooks/useDataViewState.ts +310 -0
  69. package/src/dataview/hooks/useServerDataView.ts +154 -0
  70. package/src/dataview/hooks/useViewSearch.ts +68 -0
  71. package/src/dataview/index.ts +62 -0
  72. package/src/dataview/internal.tsx +96 -0
  73. package/src/dataview/repositories/HttpDataViewRepository.ts +110 -0
  74. package/src/dataview/repositories/InMemoryDataViewRepository.ts +145 -0
  75. package/src/dataview/repositories/repositories.test.ts +158 -0
  76. package/src/dataview/repositories/viewState.test.ts +90 -0
  77. package/src/dataview/repositories/viewState.ts +128 -0
  78. package/src/dataview/types.ts +249 -0
  79. package/src/errorMessages.test.tsx +83 -0
  80. package/src/errorMessages.tsx +79 -0
  81. package/src/feedback.test.tsx +167 -0
  82. package/src/files.test.tsx +142 -0
  83. package/src/files.tsx +174 -0
  84. package/src/icons.test.tsx +46 -0
  85. package/src/icons.tsx +533 -0
  86. package/src/index.ts +155 -0
  87. package/src/layout.test.tsx +72 -0
  88. package/src/layout.tsx +90 -0
  89. package/src/layoutContract.test.tsx +179 -0
  90. package/src/layoutContract.ts +137 -0
  91. package/src/locale.test.tsx +97 -0
  92. package/src/locale.tsx +246 -0
  93. package/src/nav.test.ts +21 -0
  94. package/src/nav.ts +13 -0
  95. package/src/pageMarker.ts +15 -0
  96. package/src/raw.d.ts +7 -0
  97. package/src/realtime-hook.test.tsx +226 -0
  98. package/src/realtime.test.ts +44 -0
  99. package/src/realtime.ts +307 -0
  100. package/src/refresh-session.test.tsx +114 -0
  101. package/src/revocation.test.tsx +81 -0
  102. package/src/router.test.tsx +307 -0
  103. package/src/router.tsx +222 -0
  104. package/src/sso.test.tsx +128 -0
  105. package/src/sso.ts +142 -0
  106. package/src/ssr.test.tsx +45 -0
  107. package/src/styles.test.ts +21 -0
  108. package/src/styles.ts +302 -0
  109. package/src/theme.test.tsx +74 -0
  110. package/src/theme.tsx +143 -0
  111. package/src/toast.test.tsx +94 -0
  112. package/src/toast.tsx +214 -0
  113. package/src/tokens.guard.test.ts +51 -0
  114. package/src/ui/Alert.test.tsx +19 -0
  115. package/src/ui/Alert.tsx +115 -0
  116. package/src/ui/Badge.test.tsx +14 -0
  117. package/src/ui/Badge.tsx +48 -0
  118. package/src/ui/Button.test.tsx +36 -0
  119. package/src/ui/Button.tsx +95 -0
  120. package/src/ui/Card.test.tsx +40 -0
  121. package/src/ui/Card.tsx +92 -0
  122. package/src/ui/Checkbox.test.tsx +17 -0
  123. package/src/ui/Checkbox.tsx +51 -0
  124. package/src/ui/Combobox.test.tsx +58 -0
  125. package/src/ui/Combobox.tsx +313 -0
  126. package/src/ui/DatePicker.test.tsx +60 -0
  127. package/src/ui/DatePicker.tsx +421 -0
  128. package/src/ui/Input.tsx +30 -0
  129. package/src/ui/Markdown.test.tsx +32 -0
  130. package/src/ui/Markdown.tsx +213 -0
  131. package/src/ui/Menu.test.tsx +85 -0
  132. package/src/ui/Menu.tsx +216 -0
  133. package/src/ui/Popover.tsx +218 -0
  134. package/src/ui/Radio.test.tsx +29 -0
  135. package/src/ui/Radio.tsx +127 -0
  136. package/src/ui/Select.tsx +40 -0
  137. package/src/ui/Switch.test.tsx +17 -0
  138. package/src/ui/Switch.tsx +53 -0
  139. package/src/ui/Tabs.test.tsx +29 -0
  140. package/src/ui/Tabs.tsx +128 -0
  141. package/src/ui/Textarea.tsx +27 -0
  142. package/src/ui/Tooltip.test.tsx +28 -0
  143. package/src/ui/Tooltip.tsx +67 -0
  144. package/src/ui/controlStyles.ts +9 -0
  145. package/src/uiText.test.tsx +93 -0
  146. package/src/uiText.tsx +342 -0
  147. package/src/unwrap.test.ts +67 -0
  148. package/src/unwrap.ts +101 -0
  149. package/src/useResource.test.tsx +118 -0
  150. package/src/useResource.ts +110 -0
  151. package/src/useTerpClient.test.ts +35 -0
  152. package/tsconfig.json +17 -0
  153. package/vite.config.ts +14 -0
  154. package/vitest.setup.ts +58 -0
@@ -0,0 +1,118 @@
1
+ import { ArrowDownGlyph, ArrowUpGlyph, ColumnsGlyph } from "./glyphs";
2
+ import { DataViewMenu, useDataViewText } from "./internal";
3
+ import type { DataViewColumn } from "./types";
4
+
5
+ export interface DataViewColumnSettingsProps<T> {
6
+ /** The user columns in effective order (system columns are never listed here). */
7
+ columns: DataViewColumn<T>[];
8
+ columnVisibility: Record<string, boolean>;
9
+ onColumnVisibleChange: (columnId: string, visible: boolean) => void;
10
+ onMoveColumn: (columnId: string, direction: -1 | 1) => void;
11
+ }
12
+
13
+ /**
14
+ * The "view options" menu: per-column show/hide checkboxes and up/down reordering.
15
+ * Only user columns appear — the pinned system columns (select/expand/actions) are
16
+ * never hideable or reorderable, and are skipped when computing reorder targets
17
+ * because this list simply does not contain them.
18
+ */
19
+ export function DataViewColumnSettings<T>({
20
+ columns,
21
+ columnVisibility,
22
+ onColumnVisibleChange,
23
+ onMoveColumn,
24
+ }: DataViewColumnSettingsProps<T>) {
25
+ const { strings, resolve } = useDataViewText();
26
+
27
+ return (
28
+ <DataViewMenu
29
+ trigger={
30
+ <>
31
+ <ColumnsGlyph />
32
+ <span style={{ fontSize: "var(--font-size-sm)" }}>{resolve(strings.viewOptions)}</span>
33
+ </>
34
+ }
35
+ triggerLabel={resolve(strings.viewOptions)}
36
+ >
37
+ {() => (
38
+ <div style={{ display: "grid", gap: "var(--space-1)" }}>
39
+ <div
40
+ style={{
41
+ padding: "var(--space-1) var(--space-2)",
42
+ fontSize: "var(--font-size-sm)",
43
+ fontWeight: "var(--font-weight-medium)" as never,
44
+ color: "var(--color-neutral-500)",
45
+ }}
46
+ >
47
+ {resolve(strings.columns)}
48
+ </div>
49
+ {columns.map((column, index) => {
50
+ const label = resolve(column.meta?.label ?? column.header);
51
+ const visible = columnVisibility[column.id] !== false;
52
+ return (
53
+ <div
54
+ key={column.id}
55
+ style={{
56
+ display: "flex",
57
+ alignItems: "center",
58
+ gap: "var(--space-2)",
59
+ padding: "var(--space-1) var(--space-2)",
60
+ borderRadius: "var(--radius-sm)",
61
+ }}
62
+ >
63
+ <label
64
+ style={{
65
+ display: "flex",
66
+ alignItems: "center",
67
+ gap: "var(--space-2)",
68
+ flex: 1,
69
+ cursor: "pointer",
70
+ fontSize: "var(--font-size-sm)",
71
+ }}
72
+ >
73
+ <input
74
+ type="checkbox"
75
+ checked={visible}
76
+ onChange={(event) => onColumnVisibleChange(column.id, event.target.checked)}
77
+ aria-label={label}
78
+ />
79
+ {label}
80
+ </label>
81
+ <button
82
+ type="button"
83
+ aria-label={`${resolve(strings.moveUp)}: ${label}`}
84
+ disabled={index === 0}
85
+ onClick={() => onMoveColumn(column.id, -1)}
86
+ style={reorderButtonStyle(index === 0)}
87
+ >
88
+ <ArrowUpGlyph size={14} />
89
+ </button>
90
+ <button
91
+ type="button"
92
+ aria-label={`${resolve(strings.moveDown)}: ${label}`}
93
+ disabled={index === columns.length - 1}
94
+ onClick={() => onMoveColumn(column.id, 1)}
95
+ style={reorderButtonStyle(index === columns.length - 1)}
96
+ >
97
+ <ArrowDownGlyph size={14} />
98
+ </button>
99
+ </div>
100
+ );
101
+ })}
102
+ </div>
103
+ )}
104
+ </DataViewMenu>
105
+ );
106
+ }
107
+
108
+ function reorderButtonStyle(disabled: boolean) {
109
+ return {
110
+ display: "inline-flex",
111
+ padding: "var(--space-1)",
112
+ background: "transparent",
113
+ border: "none",
114
+ borderRadius: "var(--radius-sm)",
115
+ cursor: disabled ? "not-allowed" : "pointer",
116
+ color: disabled ? "var(--color-neutral-300)" : "var(--color-neutral-700)",
117
+ } as const;
118
+ }
@@ -0,0 +1,67 @@
1
+ import type { ReactNode } from "react";
2
+
3
+ import { ChevronDownGlyph, ChevronRightGlyph } from "./glyphs";
4
+ import { useDataViewText } from "./internal";
5
+
6
+ /**
7
+ * The expand-toggle chevron rendered in the expand system column (and on cards).
8
+ * Stops click propagation so toggling never triggers row navigation.
9
+ */
10
+ export function DataViewExpandToggle({
11
+ expanded,
12
+ onToggle,
13
+ }: {
14
+ expanded: boolean;
15
+ onToggle: () => void;
16
+ }) {
17
+ const { strings, resolve } = useDataViewText();
18
+ return (
19
+ <button
20
+ type="button"
21
+ aria-label={resolve(expanded ? strings.collapseRow : strings.expandRow)}
22
+ aria-expanded={expanded}
23
+ onClick={(event) => {
24
+ event.stopPropagation();
25
+ onToggle();
26
+ }}
27
+ style={{
28
+ display: "inline-flex",
29
+ padding: "var(--space-1)",
30
+ background: "transparent",
31
+ border: "none",
32
+ borderRadius: "var(--radius-sm)",
33
+ cursor: "pointer",
34
+ color: "var(--color-neutral-500)",
35
+ }}
36
+ >
37
+ {expanded ? <ChevronDownGlyph /> : <ChevronRightGlyph />}
38
+ </button>
39
+ );
40
+ }
41
+
42
+ /**
43
+ * The full-width detail panel row rendered directly under an expanded row: one cell
44
+ * spanning every column, visually attached to its parent row.
45
+ */
46
+ export function DataViewExpandableRow({
47
+ colSpan,
48
+ children,
49
+ }: {
50
+ colSpan: number;
51
+ children: ReactNode;
52
+ }) {
53
+ return (
54
+ <tr>
55
+ <td
56
+ colSpan={colSpan}
57
+ style={{
58
+ padding: "var(--space-3) var(--space-4)",
59
+ background: "var(--color-neutral-50)",
60
+ borderBottom: "1px solid var(--color-neutral-200)",
61
+ }}
62
+ >
63
+ {children}
64
+ </td>
65
+ </tr>
66
+ );
67
+ }
@@ -0,0 +1,113 @@
1
+ import type { CSSProperties } from "react";
2
+
3
+ import { useDataViewText } from "./internal";
4
+ import {
5
+ PageFirstGlyph,
6
+ PageLastGlyph,
7
+ PageNextGlyph,
8
+ PagePrevGlyph,
9
+ } from "./glyphs";
10
+ import type { DataViewPaginationState } from "./hooks/useDataViewState";
11
+
12
+ export interface DataViewPaginationProps {
13
+ pagination: DataViewPaginationState;
14
+ totalCount: number;
15
+ onPaginationChange: (pagination: DataViewPaginationState) => void;
16
+ }
17
+
18
+ const pagerButtonStyle = (disabled: boolean): CSSProperties => ({
19
+ display: "inline-flex",
20
+ alignItems: "center",
21
+ minHeight: "2rem",
22
+ padding: "var(--space-1) var(--space-2)",
23
+ background: "var(--color-neutral-0)",
24
+ border: "1px solid var(--color-neutral-300)",
25
+ borderRadius: "var(--radius-md)",
26
+ cursor: disabled ? "not-allowed" : "pointer",
27
+ color: disabled ? "var(--color-neutral-300)" : "var(--color-neutral-700)",
28
+ });
29
+
30
+ /**
31
+ * The footer pagination bar: "X–Y of Z results", the current page / page count, and
32
+ * first / prev / next / last controls (disabled at bounds; page controls hidden when
33
+ * there is only one page).
34
+ */
35
+ export function DataViewPagination({
36
+ pagination,
37
+ totalCount,
38
+ onPaginationChange,
39
+ }: DataViewPaginationProps) {
40
+ const { strings, format } = useDataViewText();
41
+
42
+ const pageCount = Math.max(1, Math.ceil(totalCount / pagination.pageSize));
43
+ const pageIndex = Math.min(pagination.pageIndex, pageCount - 1);
44
+ const from = totalCount === 0 ? 0 : pageIndex * pagination.pageSize + 1;
45
+ const to = Math.min(totalCount, (pageIndex + 1) * pagination.pageSize);
46
+
47
+ const goTo = (index: number) => onPaginationChange({ ...pagination, pageIndex: index });
48
+ const atFirst = pageIndex === 0;
49
+ const atLast = pageIndex >= pageCount - 1;
50
+
51
+ return (
52
+ <div
53
+ style={{
54
+ display: "flex",
55
+ alignItems: "center",
56
+ justifyContent: "space-between",
57
+ gap: "var(--space-3)",
58
+ flexWrap: "wrap",
59
+ padding: "var(--space-2) var(--space-3)",
60
+ borderTop: "1px solid var(--color-neutral-200)",
61
+ fontSize: "var(--font-size-sm)",
62
+ color: "var(--color-neutral-500)",
63
+ }}
64
+ >
65
+ <span>{format(strings.resultsRange, { from, to, total: totalCount })}</span>
66
+ {pageCount > 1 && (
67
+ <span style={{ display: "inline-flex", alignItems: "center", gap: "var(--space-2)" }}>
68
+ <span>{format(strings.pageOf, { page: pageIndex + 1, pages: pageCount })}</span>
69
+ <button
70
+ type="button"
71
+ aria-label={format(strings.firstPage, {})}
72
+ disabled={atFirst}
73
+ onClick={() => goTo(0)}
74
+ data-terp="iconbutton"
75
+ style={pagerButtonStyle(atFirst)}
76
+ >
77
+ <PageFirstGlyph />
78
+ </button>
79
+ <button
80
+ type="button"
81
+ aria-label={format(strings.previousPage, {})}
82
+ disabled={atFirst}
83
+ onClick={() => goTo(pageIndex - 1)}
84
+ data-terp="iconbutton"
85
+ style={pagerButtonStyle(atFirst)}
86
+ >
87
+ <PagePrevGlyph />
88
+ </button>
89
+ <button
90
+ type="button"
91
+ aria-label={format(strings.nextPage, {})}
92
+ disabled={atLast}
93
+ onClick={() => goTo(pageIndex + 1)}
94
+ data-terp="iconbutton"
95
+ style={pagerButtonStyle(atLast)}
96
+ >
97
+ <PageNextGlyph />
98
+ </button>
99
+ <button
100
+ type="button"
101
+ aria-label={format(strings.lastPage, {})}
102
+ disabled={atLast}
103
+ onClick={() => goTo(pageCount - 1)}
104
+ data-terp="iconbutton"
105
+ style={pagerButtonStyle(atLast)}
106
+ >
107
+ <PageLastGlyph />
108
+ </button>
109
+ </span>
110
+ )}
111
+ </div>
112
+ );
113
+ }
@@ -0,0 +1,131 @@
1
+ import type { MouseEvent, ReactNode } from "react";
2
+
3
+ import { EllipsisGlyph } from "./glyphs";
4
+ import { DataViewMenu, DataViewMenuItem, useDataViewText } from "./internal";
5
+ import type { DataViewRowAction } from "./types";
6
+ import { resolveRowFlag } from "./types";
7
+
8
+ export type DataViewRowActionsLayout = "menu" | "inline";
9
+
10
+ export interface DataViewRowActionsProps<T> {
11
+ row: T;
12
+ actions: DataViewRowAction<T>[];
13
+ /** "menu" (default): ellipsis dropdown, `inline`-flagged actions beside it. */
14
+ layout: DataViewRowActionsLayout;
15
+ /** On mobile all standard actions collapse into the menu regardless of flags. */
16
+ isMobile: boolean;
17
+ }
18
+
19
+ function InlineActionButton<T>({ action, row }: { action: DataViewRowAction<T>; row: T }) {
20
+ const { resolve } = useDataViewText();
21
+ const disabled = resolveRowFlag(action.disabled, row);
22
+ const destructive = action.variant === "destructive";
23
+ return (
24
+ <button
25
+ type="button"
26
+ disabled={disabled}
27
+ onClick={() => action.onClick?.(row)}
28
+ style={{
29
+ font: "inherit",
30
+ fontSize: "var(--font-size-sm)",
31
+ display: "inline-flex",
32
+ alignItems: "center",
33
+ minHeight: "2rem",
34
+ gap: "var(--space-1)",
35
+ padding: "var(--space-1) var(--space-2)",
36
+ background: "transparent",
37
+ border: "1px solid var(--color-neutral-300)",
38
+ borderRadius: "var(--radius-md)",
39
+ cursor: disabled ? "not-allowed" : "pointer",
40
+ color: disabled
41
+ ? "var(--color-neutral-300)"
42
+ : destructive
43
+ ? "var(--color-status-danger)"
44
+ : "var(--color-neutral-700)",
45
+ }}
46
+ >
47
+ {action.icon !== undefined && (
48
+ <span aria-hidden style={{ display: "inline-flex" }}>{action.icon}</span>
49
+ )}
50
+ {resolve(action.label)}
51
+ </button>
52
+ );
53
+ }
54
+
55
+ /**
56
+ * The per-row actions control: renders custom controls inline always, standard actions
57
+ * either as buttons ("inline" layout / `inline` flag) or in an ellipsis menu. The whole
58
+ * cluster stops click propagation so actions never trigger row navigation.
59
+ */
60
+ export function DataViewRowActions<T>({
61
+ row,
62
+ actions,
63
+ layout,
64
+ isMobile,
65
+ }: DataViewRowActionsProps<T>) {
66
+ const { strings, resolve } = useDataViewText();
67
+
68
+ const visible = actions.filter((action) => !resolveRowFlag(action.hidden, row));
69
+ if (visible.length === 0) {
70
+ return null;
71
+ }
72
+
73
+ // Custom controls always render inline and own their interaction surface.
74
+ const custom = visible.filter((action) => action.render !== undefined);
75
+ const standard = visible.filter((action) => action.render === undefined);
76
+ const inline = isMobile
77
+ ? []
78
+ : layout === "inline"
79
+ ? standard
80
+ : standard.filter((action) => action.inline === true);
81
+ const menu = standard.filter((action) => !inline.includes(action));
82
+
83
+ const stop = (event: MouseEvent) => event.stopPropagation();
84
+
85
+ const renderCustom = (action: DataViewRowAction<T>, index: number): ReactNode => (
86
+ <span key={`custom-${index}`} style={{ display: "inline-flex" }}>
87
+ {action.render?.(row)}
88
+ </span>
89
+ );
90
+
91
+ return (
92
+ <span
93
+ onClick={stop}
94
+ style={{
95
+ display: "inline-flex",
96
+ alignItems: "center",
97
+ justifyContent: "flex-end",
98
+ gap: "var(--space-1)",
99
+ }}
100
+ >
101
+ {custom.map(renderCustom)}
102
+ {inline.map((action, index) => (
103
+ <InlineActionButton key={`inline-${index}`} action={action} row={row} />
104
+ ))}
105
+ {menu.length > 0 && (
106
+ <DataViewMenu
107
+ trigger={<EllipsisGlyph />}
108
+ triggerLabel={resolve(strings.moreActions)}
109
+ >
110
+ {(close) => (
111
+ <>
112
+ {menu.map((action, index) => (
113
+ <DataViewMenuItem
114
+ key={index}
115
+ label={resolve(action.label)}
116
+ icon={action.icon}
117
+ destructive={action.variant === "destructive"}
118
+ disabled={resolveRowFlag(action.disabled, row)}
119
+ onSelect={() => {
120
+ close();
121
+ action.onClick?.(row);
122
+ }}
123
+ />
124
+ ))}
125
+ </>
126
+ )}
127
+ </DataViewMenu>
128
+ )}
129
+ </span>
130
+ );
131
+ }