torch-glare 2.4.4 → 2.5.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 (147) hide show
  1. package/apps/lib/components/DataViews/{badgeAdapter.ts → badge.ts} +2 -2
  2. package/apps/lib/components/DataViews/cell.tsx +324 -0
  3. package/apps/lib/components/DataViews/context.ts +144 -0
  4. package/apps/lib/components/DataViews/data-views.tsx +383 -0
  5. package/apps/lib/components/DataViews/filters/children.tsx +98 -0
  6. package/apps/lib/components/DataViews/filters/custom.tsx +34 -0
  7. package/apps/lib/components/DataViews/filters/filters.tsx +163 -0
  8. package/apps/lib/components/DataViews/filters/index.ts +4 -0
  9. package/apps/lib/components/DataViews/filters/labelled.tsx +20 -0
  10. package/apps/lib/components/DataViews/filters/presets.tsx +65 -0
  11. package/apps/lib/components/DataViews/filters/summary.tsx +65 -0
  12. package/apps/lib/components/DataViews/filters/sync.tsx +35 -0
  13. package/apps/lib/components/DataViews/filters/values.ts +173 -0
  14. package/apps/lib/components/DataViews/header.tsx +217 -0
  15. package/apps/lib/components/DataViews/hooks/index.ts +5 -0
  16. package/apps/lib/components/DataViews/hooks/useActiveRow.ts +22 -0
  17. package/apps/lib/components/DataViews/hooks/useControllable.ts +52 -0
  18. package/apps/lib/components/DataViews/index.ts +74 -26
  19. package/apps/lib/components/DataViews/panel/columns.tsx +153 -0
  20. package/apps/lib/components/DataViews/panel/controls.tsx +106 -0
  21. package/apps/lib/components/DataViews/panel/index.ts +3 -0
  22. package/apps/lib/components/DataViews/panel/panel.tsx +164 -0
  23. package/apps/lib/components/DataViews/panel/saved-views.tsx +67 -0
  24. package/apps/lib/components/DataViews/panel/section.tsx +79 -0
  25. package/apps/lib/components/DataViews/panel/sort.tsx +42 -0
  26. package/apps/lib/components/DataViews/panel/tab.tsx +31 -0
  27. package/apps/lib/components/DataViews/slots.ts +63 -0
  28. package/apps/lib/components/DataViews/states.tsx +38 -0
  29. package/apps/lib/components/DataViews/types.ts +485 -178
  30. package/apps/lib/components/DataViews/views/board-view.tsx +379 -0
  31. package/apps/lib/components/DataViews/views/card-rows.tsx +36 -0
  32. package/apps/lib/components/DataViews/views/inbox-view.tsx +257 -0
  33. package/apps/lib/components/DataViews/views/pane-views.tsx +192 -0
  34. package/apps/lib/components/DataViews/views/table-view.tsx +426 -0
  35. package/apps/lib/components/DataViews/views/tree-view.tsx +365 -0
  36. package/apps/lib/components/FormBuilder/context.ts +20 -6
  37. package/apps/lib/components/FormBuilder/field-kind.ts +28 -0
  38. package/apps/lib/components/FormBuilder/fields/DateField.tsx +3 -3
  39. package/apps/lib/components/FormBuilder/fields/FieldShell.tsx +7 -6
  40. package/apps/lib/components/FormBuilder/fields/PhoneField.tsx +30 -4
  41. package/apps/lib/components/FormBuilder/fields/SelectField.tsx +7 -7
  42. package/apps/lib/components/FormBuilder/fields/TableField.tsx +80 -52
  43. package/apps/lib/components/FormBuilder/fields/TextField.tsx +9 -9
  44. package/apps/lib/components/FormBuilder/form-builder.tsx +66 -6
  45. package/apps/lib/components/FormBuilder/index.ts +3 -1
  46. package/apps/lib/components/FormBuilder/types.ts +40 -0
  47. package/apps/lib/components/Input.tsx +3 -0
  48. package/apps/lib/components/SearchableTable.tsx +5 -4
  49. package/apps/lib/components/SectionBlock.tsx +58 -11
  50. package/apps/lib/components/Select.tsx +3 -1
  51. package/apps/lib/components/TabSwitch.tsx +16 -4
  52. package/apps/lib/components/Table.tsx +265 -67
  53. package/apps/lib/components/TreeFolder/TreeFolder.tsx +6 -3
  54. package/apps/lib/components/TreeFolder/TreeFolderRow.tsx +16 -14
  55. package/apps/lib/components/TreeFolder/index.ts +1 -1
  56. package/apps/lib/components/TreeFolder/useTreeFolderDnD.ts +70 -207
  57. package/apps/lib/hooks/useDragDrop.tsx +365 -0
  58. package/apps/lib/hooks/useInfiniteScroll.ts +108 -0
  59. package/apps/lib/registry.json +159 -4
  60. package/apps/lib/tsconfig.tsbuildinfo +1 -1
  61. package/apps/lib/utils/dataViews/path.ts +67 -0
  62. package/apps/lib/utils/dataViews/query.ts +73 -0
  63. package/apps/lib/utils/dataViews/types.ts +187 -0
  64. package/docs/components/breadcrumb.md +1 -1
  65. package/docs/components/button-group.md +1 -1
  66. package/docs/components/button.md +1 -1
  67. package/docs/components/card.md +1 -1
  68. package/docs/components/checkbox.md +1 -1
  69. package/docs/components/data-views/backend-response.md +324 -0
  70. package/docs/components/data-views/examples/a11y-rtl.md +250 -0
  71. package/docs/components/data-views/examples/api-orders-route.md +130 -0
  72. package/docs/components/data-views/examples/fields.md +362 -0
  73. package/docs/components/data-views/examples/filters.md +308 -0
  74. package/docs/components/data-views/examples/inbox-routing.md +218 -0
  75. package/docs/components/data-views/examples/index.md +29 -0
  76. package/docs/components/data-views/examples/overview.md +244 -0
  77. package/docs/components/data-views/examples/panel.md +212 -0
  78. package/docs/components/data-views/examples/scale.md +231 -0
  79. package/docs/components/data-views/examples/server-side.md +210 -0
  80. package/docs/components/data-views/examples/state.md +250 -0
  81. package/docs/components/data-views/examples/tree-custom.md +388 -0
  82. package/docs/components/data-views/examples/view-registry.md +313 -0
  83. package/docs/components/data-views/examples/views.md +534 -0
  84. package/docs/components/data-views/guide.md +405 -0
  85. package/docs/components/data-views/index.md +1504 -0
  86. package/docs/components/data-views/migration.md +79 -0
  87. package/docs/components/date-picker.md +0 -1
  88. package/docs/components/form-builder.md +19 -8
  89. package/docs/components/form-renderer.md +2 -1
  90. package/docs/components/form.md +1 -1
  91. package/docs/components/input-field.md +1 -1
  92. package/docs/components/input-otp.md +1 -1
  93. package/docs/components/input.md +1 -1
  94. package/docs/components/labeled-check-box.md +1 -1
  95. package/docs/components/labeled-radio.md +1 -1
  96. package/docs/components/radio-card.md +1 -1
  97. package/docs/components/radio.md +1 -1
  98. package/docs/components/search-field.md +1 -1
  99. package/docs/components/section-block.md +79 -3
  100. package/docs/components/select.md +1 -1
  101. package/docs/components/simple-select.md +1 -1
  102. package/docs/components/switch.md +1 -1
  103. package/docs/components/tab-switch.md +1 -1
  104. package/docs/components/table.md +45 -8
  105. package/docs/components/text-editor.md +1 -1
  106. package/docs/components/textarea.md +1 -1
  107. package/docs/components/toggle-button.md +1 -1
  108. package/docs/components/toggle.md +1 -1
  109. package/docs/components/tree-folder.md +110 -0
  110. package/docs/how-to/forms-with-form-builder.md +6 -4
  111. package/docs/reference/components.md +16 -6
  112. package/docs/tutorials/component-composition.md +11 -13
  113. package/package.json +3 -2
  114. package/apps/lib/components/DataViews/DataViewRadio.tsx +0 -49
  115. package/apps/lib/components/DataViews/DataViewsConfigPanel.tsx +0 -393
  116. package/apps/lib/components/DataViews/DataViewsHeader.tsx +0 -207
  117. package/apps/lib/components/DataViews/DataViewsLayout.tsx +0 -332
  118. package/apps/lib/components/DataViews/FilterPanel.tsx +0 -493
  119. package/apps/lib/components/DataViews/HeaderSearch.tsx +0 -93
  120. package/apps/lib/components/DataViews/InboxView.tsx +0 -463
  121. package/apps/lib/components/DataViews/InboxViewCard.tsx +0 -127
  122. package/apps/lib/components/DataViews/KanbanView.tsx +0 -336
  123. package/apps/lib/components/DataViews/PanelControls.tsx +0 -39
  124. package/apps/lib/components/DataViews/SettingsPanel.tsx +0 -279
  125. package/apps/lib/components/DataViews/TableView.tsx +0 -212
  126. package/apps/lib/components/DataViews/TreeView.tsx +0 -364
  127. package/apps/lib/components/DataViews/fieldRenderers.tsx +0 -299
  128. package/apps/lib/components/DataViews/filters/DatePickerRangeFilter.tsx +0 -87
  129. package/apps/lib/components/DataViews/filters/DateRangePopover.tsx +0 -120
  130. package/apps/lib/components/DataViews/filters/PresetChips.tsx +0 -45
  131. package/apps/lib/components/DataViews/filters/RangeSliderWithInputs.tsx +0 -165
  132. package/apps/lib/components/DataViews/tree/TreeDrawer.tsx +0 -50
  133. package/apps/lib/components/DataViews/tree/TreeSidebar.tsx +0 -74
  134. package/apps/lib/hooks/useDataViewsState.ts +0 -175
  135. package/apps/lib/utils/dataViews/columnUtils.ts +0 -132
  136. package/apps/lib/utils/dataViews/fieldUtils.ts +0 -197
  137. package/apps/lib/utils/dataViews/nestedDataUtils.tsx +0 -371
  138. package/apps/lib/utils/dataViews/pathUtils.ts +0 -139
  139. package/apps/lib/utils/dataViews/rangeUtils.ts +0 -234
  140. package/apps/lib/utils/dataViews/treeUtils.ts +0 -396
  141. package/docs/components/data-views-config-panel.md +0 -208
  142. package/docs/components/data-views-layout.md +0 -291
  143. package/docs/components/inbox-view.md +0 -170
  144. package/docs/components/kanban-view.md +0 -135
  145. package/docs/components/table-view.md +0 -141
  146. package/docs/components/tree-view.md +0 -147
  147. package/docs/how-to/data-views-from-backend-response.md +0 -194
@@ -0,0 +1,313 @@
1
+ ---
2
+ title: DataViews example — View registry
3
+ description: A view of your own via `markView`, beside the built-in four.
4
+ group: examples
5
+ component: DataViews
6
+ keywords: [data-views, example, examples, view, registry]
7
+ ---
8
+
9
+ # DataViews example — View registry
10
+
11
+ A view of your own via `markView`, beside the built-in four.
12
+
13
+ Complete and runnable — this is the page itself, not an excerpt. In the monorepo it lives at `apps/app/data-views/view-registry/page.tsx`.
14
+
15
+ See the [component reference](../index.md) for what each prop does, or the [guide](../guide.md) for the same ground as scenarios.
16
+
17
+ ```tsx
18
+ "use client";
19
+
20
+ import { useMemo, useState } from "react";
21
+ import { useInfiniteQuery } from "@tanstack/react-query";
22
+ import { Filter, Settings } from "lucide-react";
23
+ import { Button } from "@/components/Button";
24
+ import {
25
+ Cell,
26
+ DataViews,
27
+ emptyQuery,
28
+ markView,
29
+ queryToParams,
30
+ SkeletonBar,
31
+ skeletonKeys,
32
+ useDataViewsData,
33
+ type SavedView,
34
+ } from "@/components/DataViews";
35
+ import { FormBuilder } from "@/components/FormBuilder";
36
+ import { cn } from "@/utils/cn";
37
+ import type {
38
+ ViewBaseProps,
39
+ } from "@/components/DataViews";
40
+ import type {
41
+ DataViewsQuery,
42
+ FieldConfig,
43
+ Row,
44
+ RowGroup,
45
+ TreeNode,
46
+ } from "@/utils/dataViews/types";
47
+
48
+ /**
49
+ * A view the library does not ship — a gallery of cards — registered with `markView`.
50
+ *
51
+ * It is here to prove two things a custom view needs and could otherwise only be told about:
52
+ * `useDataViewsData()` hands it the same `rows`/`visibleFields` the built-ins get, **and the same
53
+ * `loading` flag**, so it can paint a skeleton in its own shape using the exported pieces rather
54
+ * than inventing a second loading look.
55
+ */
56
+ const GalleryView = markView(
57
+ function GalleryView({ className }: ViewBaseProps) {
58
+ const { rows, visibleFields, getRowId, loading } = useDataViewsData();
59
+ const [title, ...rest] = visibleFields;
60
+
61
+ return (
62
+ <div
63
+ className={cn(
64
+ "bg-background-presentation-body-primary grid h-full content-start gap-3 overflow-y-auto p-3",
65
+ "grid-cols-[repeat(auto-fill,minmax(220px,1fr))]",
66
+ className,
67
+ )}
68
+ >
69
+ {loading
70
+ ? skeletonKeys(8).map((i) => (
71
+ <div
72
+ key={`skeleton-${i}`}
73
+ className="border-border-presentation-global-primary bg-background-presentation-form-base flex flex-col gap-2 rounded-[12px] border p-3"
74
+ >
75
+ <SkeletonBar className="h-[18px] w-[70%]" />
76
+ <SkeletonBar className="w-[45%]" />
77
+ </div>
78
+ ))
79
+ : rows.map((row, index) => (
80
+ <div
81
+ key={getRowId(row, index)}
82
+ className="border-border-presentation-global-primary bg-background-presentation-form-base flex flex-col gap-2 rounded-[12px] border p-3"
83
+ >
84
+ <span className="typography-body-large-semibold text-content-presentation-global-primary">
85
+ {title && <Cell field={title} row={row} />}
86
+ </span>
87
+ {rest.slice(0, 2).map((field, i) => (
88
+ <span key={`${field.path}-${i}`} className="typography-body-small-regular">
89
+ <Cell field={field} row={row} />
90
+ </span>
91
+ ))}
92
+ </div>
93
+ ))}
94
+ </div>
95
+ );
96
+ },
97
+ { defaultId: "gallery", defaultLabel: "Gallery" },
98
+ );
99
+
100
+ // ─── Data ─────────────────────────────────────────────────────────────────────
101
+
102
+ interface Order extends Row {
103
+ id: number;
104
+ customer: { name: string };
105
+ status: "Pending" | "Shipped" | "Delivered";
106
+ priority: "High" | "Medium" | "Low";
107
+ total: number;
108
+ }
109
+
110
+ const FIELDS: FieldConfig[] = [
111
+ { path: "id", label: "Order #", type: "number" },
112
+ { path: "customer.name", label: "Customer", type: "text" },
113
+ { path: "brand.name", label: "Brand", type: "text" },
114
+ { path: "status", label: "Status", type: "enum-badge", variants: { Pending: "yellow", Shipped: "blue", Delivered: "green" } },
115
+ { path: "priority", label: "Priority", type: "enum-badge", variants: { High: "redOrange", Medium: "purple", Low: "gray" } },
116
+ { path: "total", label: "Total", type: "currency", currency: "USD" },
117
+ ];
118
+
119
+ /** Dynamic sets — in a real app these come from the endpoint that also does the filtering. */
120
+ const CUSTOMER_OPTIONS = [
121
+ "Acme Inc.", "Globex Corp.", "Initech", "Umbrella",
122
+ "Hooli", "Stark Industries", "Wayne Enterprises", "Cyberdyne",
123
+ ].map((v) => ({ label: v, value: v }));
124
+
125
+ const BRAND_OPTIONS = ["Bosch", "Makita", "DeWalt", "Hilti"].map((v) => ({ label: v, value: v }));
126
+
127
+ const PRIORITY_OPTIONS = ["High", "Medium", "Low"].map((v) => ({ label: v, value: v }));
128
+
129
+ const STATUS_OPTIONS = [
130
+ { label: "Pending", value: "Pending" },
131
+ { label: "Shipped", value: "Shipped" },
132
+ { label: "Delivered", value: "Delivered" },
133
+ ];
134
+
135
+ /**
136
+ * The request this page makes. The querying itself happens in `app/api/orders/route.ts` —
137
+ * nothing on this page filters, sorts or pages anything, which is the split DataViews is built
138
+ * around.
139
+ */
140
+ async function fetchOrders(q: DataViewsQuery): Promise<{ rows: Order[]; total: number }> {
141
+ const params = queryToParams(q);
142
+ const res = await fetch(`/api/orders?${params}`);
143
+ if (!res.ok) throw new Error(`${res.status} ${res.statusText}`);
144
+ return res.json();
145
+ }
146
+
147
+ const byStatus = (rows: readonly Order[]): RowGroup[] =>
148
+ (["Pending", "Shipped", "Delivered"] as const).map((status) => ({
149
+ id: status,
150
+ label: status,
151
+ color: ({ Pending: "gray", Shipped: "blue", Delivered: "green" } as const)[status],
152
+ rows: rows.filter((row) => row.status === status),
153
+ }));
154
+
155
+ const byPriority = (rows: readonly Order[]): RowGroup[] =>
156
+ (["High", "Medium", "Low"] as const).map((priority) => ({
157
+ id: priority,
158
+ label: priority,
159
+ color: ({ High: "red", Medium: "purple", Low: "gray" } as const)[priority],
160
+ rows: rows.filter((row) => row.priority === priority),
161
+ }));
162
+
163
+ const nodesFromRows = (rows: readonly Order[]): TreeNode[] =>
164
+ rows.map((row) => ({ id: String(row.id), row, depth: 0, children: [] }));
165
+
166
+ // ─── Page ─────────────────────────────────────────────────────────────────────
167
+
168
+ /**
169
+ * A view exists because you rendered it — the switcher is built from the views in the tree, so
170
+ * there is no visibility map to keep in step and no way for the two to disagree.
171
+ *
172
+ * Each view defaults its own id and label, which is why the two boards below **must** carry
173
+ * explicit `id`s: without them both would answer to `"board"` and the second would be
174
+ * unreachable. Revoking the one you are looking at converges on the first registered view and
175
+ * reports it through `onViewChange`, in an effect rather than during render.
176
+ */
177
+ export default function ViewRegistryExample() {
178
+ const [query, setQuery] = useState(emptyQuery());
179
+ const [saved, setSaved] = useState<SavedView[]>([]);
180
+
181
+
182
+ const [onlyOne, setOnlyOne] = useState(false);
183
+ const [swapIcon, setSwapIcon] = useState(false);
184
+ const [canSeeBoard, setCanSeeBoard] = useState(true);
185
+
186
+ const { data, isPending, fetchNextPage, isFetchingNextPage } = useInfiniteQuery({
187
+ // The key *is* the query: touch any part of it and TanStack refetches, and a response that
188
+ // has been superseded is discarded rather than landing on top of a newer one.
189
+ queryKey: ["registry-orders", { ...query, page: undefined }],
190
+ queryFn: ({ pageParam }) => fetchOrders({ ...query, page: pageParam }),
191
+ initialPageParam: 1,
192
+ // Undefined means "no more" — which is what the component's `hasMore` resolves to.
193
+ getNextPageParam: (last, pages) => {
194
+ const loaded = pages.reduce((n, page) => n + page.rows.length, 0);
195
+ return loaded < last.total ? pages.length + 1 : undefined;
196
+ },
197
+ });
198
+
199
+ const rows = useMemo(() => data?.pages.flatMap((page) => page.rows) ?? [], [data]);
200
+ const total = data?.pages[0]?.total ?? 0;
201
+
202
+ return (
203
+ <div className="flex h-full min-h-0 flex-col p-4">
204
+ <DataViews
205
+ rows={rows}
206
+ fields={FIELDS}
207
+ total={total}
208
+ loading={isPending}
209
+ onLoadMore={fetchNextPage}
210
+ loadingMore={isFetchingNextPage}
211
+ onQueryChange={setQuery}
212
+ className="h-full"
213
+ >
214
+ <DataViews.Header title="Orders">
215
+ <DataViews.ViewSwitch />
216
+ <DataViews.Search />
217
+ <DataViews.Actions>
218
+ <Button variant="BluColStyle" size="M" onClick={() => setOnlyOne((v) => !v)}>
219
+ {onlyOne ? "All views" : "Only the table"}
220
+ </Button>
221
+ <Button variant="BluColStyle" size="M" onClick={() => setSwapIcon((v) => !v)}>
222
+ Swap icons
223
+ </Button>
224
+ <Button variant="BluColStyle"
225
+ size="M"
226
+ onClick={() => setCanSeeBoard((v) => !v)}
227
+ >
228
+ {canSeeBoard ? "Revoke the board" : "Grant the board"}
229
+ </Button>
230
+ </DataViews.Actions>
231
+ <DataViews.PanelToggle />
232
+ </DataViews.Header>
233
+
234
+ <DataViews.Table
235
+ icon={<i className={swapIcon ? "ri-list-check" : "ri-table-line"} />}
236
+ />
237
+
238
+ {/* `{flag && <View/>}` is how a view disappears behind a permission check:
239
+ `Children.toArray` drops the `false`, and the view unregisters. */}
240
+ {!onlyOne && canSeeBoard && (
241
+ <DataViews.Board
242
+ id="board-status"
243
+ label="By status"
244
+ icon={<i className={swapIcon ? "ri-flag-line" : "ri-kanban-view"} />}
245
+ groups={byStatus(rows)}
246
+ titlePath="customer.name"
247
+ />
248
+ )}
249
+ {!onlyOne && (
250
+ <DataViews.Board
251
+ id="board-priority"
252
+ label="By priority"
253
+ icon={<i className={swapIcon ? "ri-fire-line" : "ri-alarm-warning-line"} />}
254
+ groups={byPriority(rows)}
255
+ titlePath="customer.name"
256
+ />
257
+ )}
258
+ {!onlyOne && (
259
+ <DataViews.Tree nodes={nodesFromRows(rows)} labelPath="customer.name">
260
+ <DataViews.Tree.Table />
261
+ <DataViews.Tree.Cards />
262
+ </DataViews.Tree>
263
+ )}
264
+ {/* Not one of the four — a view of this page's own, registered by `markView` and reading
265
+ the same context, loading state included. */}
266
+ {!onlyOne && <GalleryView icon={<i className="ri-layout-grid-line" />} />}
267
+
268
+ <DataViews.Panel>
269
+ <DataViews.Panel.Tab value="config" label="Config." icon={<Settings />}>
270
+ {/* Saving is the app's job — a view outlives the component. Restoring is not:
271
+ hand the snapshot back and selecting it puts everything back internally. */}
272
+ <DataViews.Panel.SavedViews
273
+ views={saved}
274
+ onSave={(snapshot) =>
275
+ setSaved((prev) => [
276
+ ...prev,
277
+ { id: `view-${prev.length + 1}`, label: `View ${prev.length + 1}`, snapshot },
278
+ ])
279
+ }
280
+ />
281
+ <DataViews.Panel.Columns />
282
+ <DataViews.Panel.Sort />
283
+ </DataViews.Panel.Tab>
284
+
285
+ <DataViews.Panel.Tab value="filters" label="Filters" icon={<Filter />}>
286
+ {/* The controls are FormBuilder fields — the same Select and Slider any form in
287
+ this library uses. The <FormBuilder> itself lives inside Filters; you write only
288
+ its fields, and Filters reads each one's name, label and bounds to learn what
289
+ it is. */}
290
+ <DataViews.Filters
291
+ title={null}
292
+ className="border-b-0 p-0"
293
+ >
294
+ {/* One control per section type. Which one a field gets is decided by the data:
295
+ can the option set grow, and can the user pick more than one. */}
296
+ <FormBuilder.CheckboxGroup name="status" label="Status" options={STATUS_OPTIONS} />
297
+ <FormBuilder.RadioList name="priority" label="Priority" options={PRIORITY_OPTIONS} />
298
+ <FormBuilder.SearchableSelect
299
+ name="customer.name"
300
+ label="Customer"
301
+ options={CUSTOMER_OPTIONS}
302
+ />
303
+ <FormBuilder.MultiSelect name="brand.name" label="Brand" options={BRAND_OPTIONS} />
304
+ <FormBuilder.Slider name="total" label="Total" range min={0} max={15000} step={100} />
305
+ <FormBuilder.DateRange name="createdAt" label="Created" />
306
+ </DataViews.Filters>
307
+ </DataViews.Panel.Tab>
308
+ </DataViews.Panel>
309
+ </DataViews>
310
+ </div>
311
+ );
312
+ }
313
+ ```