@protobase/ui 0.19.15 → 0.20.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/app/app.js +2 -2
- package/dist/app/charts/chart-groups.js +52 -0
- package/dist/app/charts/chart-view.js +75 -0
- package/dist/app/charts/group-bars.js +49 -0
- package/dist/app/charts/group-donut.js +84 -0
- package/dist/app/charts/period-chart.js +174 -0
- package/dist/app/displays/board-column.js +114 -0
- package/dist/app/displays/board-columns.js +45 -0
- package/dist/app/displays/board-display.js +96 -0
- package/dist/app/displays/board-dnd.js +32 -0
- package/dist/app/displays/list-displays.js +13 -1
- package/dist/app/displays/record-card.js +16 -5
- package/dist/app/displays/use-board-column.js +23 -0
- package/dist/app/displays/use-board-move.js +60 -0
- package/dist/app/displays/virtual-cards.js +1 -0
- package/dist/app/embedded-list/embedded-list.js +9 -0
- package/dist/app/export/export-options.js +2 -2
- package/dist/app/gallery/use-gallery-photos.js +2 -2
- package/dist/app/import/lookups.js +2 -2
- package/dist/app/list-edit/cell-field-editor.js +61 -0
- package/dist/app/list-edit/use-list-cell-editor.js +30 -0
- package/dist/app/list-edit/use-list-edits.js +75 -0
- package/dist/app/list-group-chart.js +37 -0
- package/dist/app/list-page.js +38 -13
- package/dist/app/map/map-area.js +6 -6
- package/dist/app/pages/action-href.js +4 -2
- package/dist/app/pages/blocks/chart.js +26 -0
- package/dist/app/pages/render-node.js +2 -0
- package/dist/app/pages/use-run-action.js +4 -3
- package/dist/app/record-page.js +30 -16
- package/dist/app/record-preview.js +25 -0
- package/dist/app/record-row/record-row-body.js +2 -2
- package/dist/app/record-row/record-row.js +1 -0
- package/dist/app/related-dots/card-dots-lines.js +16 -0
- package/dist/app/related-dots/card-dots.js +34 -0
- package/dist/app/related-dots/dot-card.js +3 -3
- package/dist/app/related-dots/related-dots-columns.js +3 -1
- package/dist/app/related-dots/related-dots.js +22 -9
- package/dist/app/related-section.js +4 -4
- package/dist/app/relation-options.js +44 -0
- package/dist/app/relation-picker.js +114 -35
- package/dist/app/saved/saved-records.js +2 -2
- package/dist/app/search/search-model.js +2 -2
- package/dist/app/searches/saved-search.js +3 -3
- package/dist/app/searches/search-diff.js +8 -8
- package/dist/app/use-nav-recent.js +1 -2
- package/dist/data/api-provider.d.ts +4 -1
- package/dist/data/api-provider.js +12 -5
- package/dist/data/focus-refresh.js +44 -0
- package/dist/data/query-keys.js +18 -0
- package/dist/data/use-aggregates.js +14 -1
- package/dist/data/use-record.js +17 -2
- package/dist/data-grid/cell-moves.js +26 -0
- package/dist/data-grid/data-grid.js +32 -7
- package/dist/data-grid/grid-header.js +1 -1
- package/dist/data-grid/grid-row.js +17 -13
- package/dist/data-grid/page-cache.js +3 -0
- package/dist/data-grid/use-cell-editing.js +113 -0
- package/dist/data-grid/use-paged-rows.js +25 -4
- package/dist/index.js +2 -2
- package/dist/live/field-values.js +3 -1
- package/dist/live/related-column-rows.js +3 -3
- package/dist/live/relation-labels.js +2 -2
- package/dist/live/use-live-rows.js +17 -4
- package/dist/live/use-reference-search.js +2 -2
- package/dist/package.js +1 -1
- package/dist/shell/assets/app-B5h_q_JL.js +69 -0
- package/dist/shell/assets/{index-CzV9U_NJ.js → index-B0veahji.js} +1 -1
- package/dist/shell/assets/index-BZmb28OT.css +2 -0
- package/dist/shell/assets/{maplibre-G35v6TYU.js → maplibre-CbjT6RKG.js} +1 -1
- package/dist/shell/assets/{markdown-content-DGFnggSX.js → markdown-content-BjRGtSOA.js} +1 -1
- package/dist/shell/assets/{record-map-ZAC_U52Y.js → record-map-rmkTnAs8.js} +1 -1
- package/dist/shell/assets/{records-map-DLXOQVpM.js → records-map-D1vLGY06.js} +1 -1
- package/dist/shell/assets/{shared-protobase-ui-xByMDSPg.js → shared-protobase-ui-PnkJqBqB.js} +1 -1
- package/dist/shell/index.html +4 -4
- package/dist/shell/protobase.shell.json +1 -1
- package/dist/tokens.css +17 -0
- package/package.json +4 -4
- package/dist/shell/assets/app-WromtADN.js +0 -69
- package/dist/shell/assets/index-DEEIUcYb.css +0 -2
package/dist/app/list-page.js
CHANGED
|
@@ -9,7 +9,7 @@ import { useProjectUi } from "./pages/project-ui.js";
|
|
|
9
9
|
import { Notice, useAdminMeta, usePermissions } from "./meta-gate.js";
|
|
10
10
|
import { Sheet } from "../primitives/sheet.js";
|
|
11
11
|
import { ErrorBanner } from "./error-banner.js";
|
|
12
|
-
import { loadedFields, shownDisplay } from "./displays/list-displays.js";
|
|
12
|
+
import { displayRow, loadedFields, shownDisplay } from "./displays/list-displays.js";
|
|
13
13
|
import { parseSort, sortToOrderBy } from "./list-order.js";
|
|
14
14
|
import { listedResource } from "./current-address.js";
|
|
15
15
|
import { AppLink, useAppAddress } from "./use-app-address.js";
|
|
@@ -26,7 +26,11 @@ import { FilterPanel } from "../filter-panel/filter-panel.js";
|
|
|
26
26
|
import { stateToText, textToState } from "../live/filter-string.js";
|
|
27
27
|
import { widgetFields } from "../live/view-fields.js";
|
|
28
28
|
import { facetNames, filterChips } from "./searches/saved-search.js";
|
|
29
|
+
import { useSeedRecord } from "./record-preview.js";
|
|
29
30
|
import { SignInPrompt } from "../auth/sign-in-prompt.js";
|
|
31
|
+
import { relatedColumnLoads } from "./related-dots/dot-card.js";
|
|
32
|
+
import { useDisplayRows } from "./displays/use-display-rows.js";
|
|
33
|
+
import { BoardDisplay } from "./displays/board-display.js";
|
|
30
34
|
import { CustomDisplay } from "./displays/custom-display.js";
|
|
31
35
|
import { MapDisplay } from "./displays/map-display.js";
|
|
32
36
|
import { DisplaySwitch, SortMenu } from "./displays/display-controls.js";
|
|
@@ -39,22 +43,22 @@ import { ImportButton } from "./import/import-button.js";
|
|
|
39
43
|
import { exportOffered, exportsSelection } from "./export/export-options.js";
|
|
40
44
|
import { useExportFlow } from "./export/use-export-flow.js";
|
|
41
45
|
import { ListCards } from "./displays/list-cards.js";
|
|
42
|
-
import { useDisplayRows } from "./displays/use-display-rows.js";
|
|
43
46
|
import { useListScroll } from "./list-scroll.js";
|
|
44
47
|
import { ListChart } from "./list-chart.js";
|
|
48
|
+
import { ListGroupChart } from "./list-group-chart.js";
|
|
45
49
|
import { ListSearch } from "./list-search.js";
|
|
46
|
-
import { relatedColumnLoads } from "./related-dots/dot-card.js";
|
|
47
50
|
import { withRelatedDotsColumns } from "./related-dots/related-dots-columns.js";
|
|
48
51
|
import { SavedList } from "./saved/saved-list.js";
|
|
49
52
|
import { SaveSearchButton } from "./searches/save-search-button.js";
|
|
50
53
|
import { SavedSearches } from "./searches/saved-searches.js";
|
|
54
|
+
import { useListCellEditor } from "./list-edit/use-list-cell-editor.js";
|
|
51
55
|
import { useMemo, useState } from "react";
|
|
52
56
|
import { Download, Plus, SlidersHorizontal } from "lucide-react";
|
|
53
57
|
import { Fragment as Fragment$1, jsx, jsxs } from "react/jsx-runtime";
|
|
54
58
|
import { createWhere } from "@protobase/schema";
|
|
55
|
-
import { tableDisplay } from "@protobase/schema/internal";
|
|
59
|
+
import { isGroupChart, tableDisplay } from "@protobase/schema/internal";
|
|
56
60
|
//#region src/app/list-page.tsx
|
|
57
|
-
const where = createWhere();
|
|
61
|
+
const where$1 = createWhere();
|
|
58
62
|
const searchText = (extras) => extras.find((clause) => clause.kind === "search");
|
|
59
63
|
/**
|
|
60
64
|
* A resource's list: its rows, with `listOf` the records its relation points at (`SavedList`), or with `searchesOf` the
|
|
@@ -169,6 +173,7 @@ const LoadedList = ({ model, address, scope, heading }) => {
|
|
|
169
173
|
});
|
|
170
174
|
const singular = view?.names?.singular ?? humanize(resource);
|
|
171
175
|
const permissions = usePermissions(resource);
|
|
176
|
+
const cellEditing = useListCellEditor(model, view, permissions);
|
|
172
177
|
const { state: auth } = useAuth();
|
|
173
178
|
const restore = useRestore(model);
|
|
174
179
|
const remove = useDeleteFlow({
|
|
@@ -185,7 +190,7 @@ const LoadedList = ({ model, address, scope, heading }) => {
|
|
|
185
190
|
const search = searchText(extras);
|
|
186
191
|
const commitSearch = (text) => {
|
|
187
192
|
const others = extras.filter((clause) => clause.kind !== "search");
|
|
188
|
-
setFilter(state, text ? [...others, where.search(text)] : others);
|
|
193
|
+
setFilter(state, text ? [...others, where$1.search(text)] : others);
|
|
189
194
|
};
|
|
190
195
|
const plural = view?.names?.plural ?? humanize(resource);
|
|
191
196
|
const sortMenu = display && /* @__PURE__ */ jsx(SortMenu, {
|
|
@@ -309,17 +314,23 @@ const LoadedList = ({ model, address, scope, heading }) => {
|
|
|
309
314
|
onChange: (name) => set({ display: name }),
|
|
310
315
|
menu
|
|
311
316
|
});
|
|
312
|
-
const
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
+
const seedRecord = useSeedRecord(model);
|
|
318
|
+
const openRecord = (row) => {
|
|
319
|
+
seedRecord(displayRow(model, row).record);
|
|
320
|
+
go({
|
|
321
|
+
kind: "record",
|
|
322
|
+
resource,
|
|
323
|
+
key: recordKey(model, row)
|
|
324
|
+
});
|
|
325
|
+
};
|
|
317
326
|
const table = rows.source ? /* @__PURE__ */ jsx(DataGrid, {
|
|
318
327
|
columns,
|
|
319
328
|
source: rows.source,
|
|
320
329
|
sort: parseSort(orderBy),
|
|
321
330
|
onSortChange: (sort) => set({ orderBy: sortToOrderBy(sort) }),
|
|
322
331
|
onOpenRow: openRecord,
|
|
332
|
+
cellEditor: cellEditing.editor,
|
|
333
|
+
rowPatches: cellEditing.patches,
|
|
323
334
|
initialScrollIndex: scroll.initialIndex,
|
|
324
335
|
onScrollIndex: scroll.remember,
|
|
325
336
|
rowActions: [{
|
|
@@ -368,6 +379,14 @@ const LoadedList = ({ model, address, scope, heading }) => {
|
|
|
368
379
|
onMove: (map) => set({ map }),
|
|
369
380
|
flush: true
|
|
370
381
|
});
|
|
382
|
+
if (display.kind === "board") return /* @__PURE__ */ jsx(BoardDisplay, {
|
|
383
|
+
model,
|
|
384
|
+
view,
|
|
385
|
+
display,
|
|
386
|
+
fields: loaded,
|
|
387
|
+
filter: loadFilter,
|
|
388
|
+
orderBy
|
|
389
|
+
});
|
|
371
390
|
return /* @__PURE__ */ jsx(CustomDisplay, {
|
|
372
391
|
display,
|
|
373
392
|
list: displayRows
|
|
@@ -404,13 +423,19 @@ const LoadedList = ({ model, address, scope, heading }) => {
|
|
|
404
423
|
title: "Could not load rows",
|
|
405
424
|
error: rows.error
|
|
406
425
|
}),
|
|
407
|
-
view?.chart && /* @__PURE__ */ jsx(
|
|
426
|
+
view?.chart && (isGroupChart(view.chart) ? /* @__PURE__ */ jsx(ListGroupChart, {
|
|
427
|
+
model,
|
|
428
|
+
view,
|
|
429
|
+
chart: view.chart,
|
|
430
|
+
plural,
|
|
431
|
+
filter: loadFilter
|
|
432
|
+
}) : /* @__PURE__ */ jsx(ListChart, {
|
|
408
433
|
resource,
|
|
409
434
|
chart: view.chart,
|
|
410
435
|
title: `${plural} created`,
|
|
411
436
|
noun: plural.toLowerCase(),
|
|
412
437
|
filter: loadFilter
|
|
413
|
-
}),
|
|
438
|
+
})),
|
|
414
439
|
desktop ? desktopToolbar : phoneFilters,
|
|
415
440
|
desktop && layout === "panel" ? /* @__PURE__ */ jsxs("div", {
|
|
416
441
|
className: "flex min-h-0 flex-1 gap-4",
|
package/dist/app/map/map-area.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { createWhere, printFilter } from "@protobase/schema";
|
|
2
2
|
//#region src/app/map/map-area.ts
|
|
3
|
-
const where = createWhere();
|
|
3
|
+
const where$1 = createWhere();
|
|
4
4
|
/** The whole world from as far out as a map goes: what a map asks for before it knows where its records are. */
|
|
5
5
|
const worldArea = {
|
|
6
6
|
bounds: {
|
|
@@ -20,11 +20,11 @@ const wrapped = (longitude) => ((longitude + 180) % 360 + 360) % 360 - 180;
|
|
|
20
20
|
* A view across the antimeridian takes the longitudes on both sides of it, and one around the whole world takes them all.
|
|
21
21
|
*/
|
|
22
22
|
const boundsFilter = (fields, bounds) => {
|
|
23
|
-
const latitude = [where.gte(fields.latitude, down(Math.max(bounds.south, -90))), where.lte(fields.latitude, up(Math.min(bounds.north, 90)))];
|
|
24
|
-
if (bounds.east - bounds.west >= 360) return where.and(...latitude);
|
|
25
|
-
const west = where.gte(fields.longitude, down(wrapped(bounds.west)));
|
|
26
|
-
const east = where.lte(fields.longitude, up(wrapped(bounds.east)));
|
|
27
|
-
return wrapped(bounds.west) <= wrapped(bounds.east) ? where.and(...latitude, west, east) : where.and(...latitude, where.or(west, east));
|
|
23
|
+
const latitude = [where$1.gte(fields.latitude, down(Math.max(bounds.south, -90))), where$1.lte(fields.latitude, up(Math.min(bounds.north, 90)))];
|
|
24
|
+
if (bounds.east - bounds.west >= 360) return where$1.and(...latitude);
|
|
25
|
+
const west = where$1.gte(fields.longitude, down(wrapped(bounds.west)));
|
|
26
|
+
const east = where$1.lte(fields.longitude, up(wrapped(bounds.east)));
|
|
27
|
+
return wrapped(bounds.west) <= wrapped(bounds.east) ? where$1.and(...latitude, west, east) : where$1.and(...latitude, where$1.or(west, east));
|
|
28
28
|
};
|
|
29
29
|
/** The list's filter, its search included, narrowed to the records inside the map's bounds. */
|
|
30
30
|
const areaFilter = (listFilter, fields, bounds) => {
|
|
@@ -25,12 +25,14 @@ const openLink = (target) => {
|
|
|
25
25
|
else window.location.assign(target);
|
|
26
26
|
};
|
|
27
27
|
const hostOf = (target) => URL.canParse(target) ? new URL(target).hostname.replace(/^www\./, "") : "";
|
|
28
|
+
const holdsAll = (record, values) => Object.entries(values).every(([name, value]) => Object.is(record[name], value));
|
|
28
29
|
/**
|
|
29
|
-
* `action` as it shows for `record`:
|
|
30
|
-
* becomes the host its target opens, without `www.`.
|
|
30
|
+
* `action` as it shows for `record`: an update that would change nothing there, and a link whose `href` has no target
|
|
31
|
+
* there, are left out, and `{host}` in a link's label becomes the host its target opens, without `www.`.
|
|
31
32
|
*/
|
|
32
33
|
const actionFor = (action, record) => {
|
|
33
34
|
const run = action.run;
|
|
35
|
+
if (run?.kind === "update") return record && holdsAll(record, run.values) ? void 0 : action;
|
|
34
36
|
if (run?.kind !== "link" || !("href" in run)) return action;
|
|
35
37
|
const target = linkTarget(run.href, record);
|
|
36
38
|
if (target === void 0) return void 0;
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { useAdminMeta } from "../../meta-gate.js";
|
|
2
|
+
import { propsOf } from "./block-props.js";
|
|
3
|
+
import { CardFrame } from "./card-frame.js";
|
|
4
|
+
import { ChartView } from "../../charts/chart-view.js";
|
|
5
|
+
import { jsx } from "react/jsx-runtime";
|
|
6
|
+
//#region src/app/pages/blocks/chart.tsx
|
|
7
|
+
/** Counts, sums or averages of the records that match, per value or date bucket of a field, in a card. */
|
|
8
|
+
const ChartBlock = ({ node, slots }) => {
|
|
9
|
+
const { resource, title, description, actions: _actions, span: _span, ...chart } = propsOf(node);
|
|
10
|
+
const { resources, views } = useAdminMeta();
|
|
11
|
+
const model = resources[resource];
|
|
12
|
+
if (!model) return null;
|
|
13
|
+
return /* @__PURE__ */ jsx(CardFrame, {
|
|
14
|
+
title,
|
|
15
|
+
description,
|
|
16
|
+
actions: slots.actions,
|
|
17
|
+
className: "h-full",
|
|
18
|
+
children: /* @__PURE__ */ jsx(ChartView, {
|
|
19
|
+
model,
|
|
20
|
+
view: views[resource],
|
|
21
|
+
...chart
|
|
22
|
+
})
|
|
23
|
+
});
|
|
24
|
+
};
|
|
25
|
+
//#endregion
|
|
26
|
+
export { ChartBlock };
|
|
@@ -4,6 +4,7 @@ import { useProjectUi } from "./project-ui.js";
|
|
|
4
4
|
import { ActionBlock } from "./blocks/action.js";
|
|
5
5
|
import { CardBlock } from "./blocks/card.js";
|
|
6
6
|
import { CardRowBlock } from "./blocks/card-row.js";
|
|
7
|
+
import { ChartBlock } from "./blocks/chart.js";
|
|
7
8
|
import { FieldBlock } from "./blocks/field.js";
|
|
8
9
|
import { GridBlock } from "./blocks/grid.js";
|
|
9
10
|
import { LinkBlock } from "./blocks/link.js";
|
|
@@ -30,6 +31,7 @@ const blocks = {
|
|
|
30
31
|
Table: TableBlock,
|
|
31
32
|
CardRow: CardRowBlock,
|
|
32
33
|
RecordRow: RecordRowBlock,
|
|
34
|
+
Chart: ChartBlock,
|
|
33
35
|
Progress: ProgressBlock,
|
|
34
36
|
ModalForm: ModalFormBlock,
|
|
35
37
|
Action: ActionBlock,
|
|
@@ -12,9 +12,10 @@ import { fillAddress } from "@protobase/schema/internal";
|
|
|
12
12
|
const message = (error) => error instanceof Error ? error.message : "Something went wrong";
|
|
13
13
|
/**
|
|
14
14
|
* The named actions of `resource`'s view, ready to run on `scope` (the record they act on): built-in behaviour first
|
|
15
|
-
* (`update`, `delete`, `link`), else the handler the project registered under the action's name.
|
|
16
|
-
* record holds is left out while it holds none. `blocked` says
|
|
17
|
-
* that succeeded. `submit` runs one with the answers to its
|
|
15
|
+
* (`update`, `delete`, `link`), else the handler the project registered under the action's name. An update whose values
|
|
16
|
+
* the record holds already is left out, and so is a link to a URL the record holds while it holds none. `blocked` says
|
|
17
|
+
* why one cannot run; `afterRun` hears about every action that succeeded. `submit` runs one with the answers to its
|
|
18
|
+
* inputs and throws its failure, for the dialog to show.
|
|
18
19
|
*/
|
|
19
20
|
const useActionRunner = (resource, scope, afterRun) => {
|
|
20
21
|
const { views } = useAdminMeta();
|
package/dist/app/record-page.js
CHANGED
|
@@ -1,11 +1,13 @@
|
|
|
1
1
|
import { cn } from "../lib/cn.js";
|
|
2
|
+
import { Skeleton } from "../primitives/skeleton.js";
|
|
2
3
|
import { inApp } from "../app-shell/breadcrumb.js";
|
|
3
4
|
import { useAuth } from "../auth/auth-provider.js";
|
|
4
5
|
import { Button, buttonClass } from "../primitives/button.js";
|
|
6
|
+
import { usePauseFocusRefresh } from "../data/focus-refresh.js";
|
|
5
7
|
import { keys } from "../data/query-keys.js";
|
|
6
8
|
import { useClient } from "../data/api-provider.js";
|
|
7
9
|
import { useNavigationGuard, useRouter } from "./router.js";
|
|
8
|
-
import {
|
|
10
|
+
import { useOpenedRecord } from "../data/use-record.js";
|
|
9
11
|
import { recordTitle } from "../live/model-helpers.js";
|
|
10
12
|
import { humanize } from "../live/naming.js";
|
|
11
13
|
import { useProjectUi } from "./pages/project-ui.js";
|
|
@@ -16,6 +18,7 @@ import { MetadataCard, RelatedRecords, SidebarCard } from "../record-view/record
|
|
|
16
18
|
import { RecordHeader } from "../record-view/record-header.js";
|
|
17
19
|
import { SpecTable } from "../record-view/spec-table.js";
|
|
18
20
|
import { KeyFacts } from "../record-view/key-facts.js";
|
|
21
|
+
import { isRequiredOnEdit } from "../live/field-values.js";
|
|
19
22
|
import { fieldHelp, helpShown, recordShown, whenShown } from "./record-mode/record-mode.js";
|
|
20
23
|
import { ErrorBanner } from "./error-banner.js";
|
|
21
24
|
import { displayValue } from "../live/display-value.js";
|
|
@@ -70,7 +73,7 @@ const layoutFor = (model, view, read) => {
|
|
|
70
73
|
const RecordPage = ({ resource, recordKey }) => {
|
|
71
74
|
const { resources } = useAdminMeta();
|
|
72
75
|
const model = resources[resource];
|
|
73
|
-
const record =
|
|
76
|
+
const record = useOpenedRecord(resource, recordKey);
|
|
74
77
|
if (!model) return /* @__PURE__ */ jsxs(Notice, {
|
|
75
78
|
title: "Not found",
|
|
76
79
|
children: [
|
|
@@ -96,10 +99,11 @@ const RecordPage = ({ resource, recordKey }) => {
|
|
|
96
99
|
return /* @__PURE__ */ jsx(LoadedRecord, {
|
|
97
100
|
model,
|
|
98
101
|
recordKey,
|
|
99
|
-
stored: record.data
|
|
100
|
-
|
|
102
|
+
stored: record.data,
|
|
103
|
+
preview: record.isPlaceholderData
|
|
104
|
+
}, record.isPlaceholderData ? "preview" : "loaded");
|
|
101
105
|
};
|
|
102
|
-
const LoadedRecord = ({ model, recordKey, stored }) => {
|
|
106
|
+
const LoadedRecord = ({ model, recordKey, stored, preview }) => {
|
|
103
107
|
const meta = useAdminMeta();
|
|
104
108
|
const { basePath } = useRouter();
|
|
105
109
|
const { address, go, open } = useAppAddress();
|
|
@@ -115,7 +119,7 @@ const LoadedRecord = ({ model, recordKey, stored }) => {
|
|
|
115
119
|
recordKey,
|
|
116
120
|
mode: view?.saveFeedback ?? "toast",
|
|
117
121
|
subject: title,
|
|
118
|
-
draftKey: auth.kind === "signed-in" ? draftStorageKey(auth.user.id, model.name, recordKey) : void 0,
|
|
122
|
+
draftKey: !preview && auth.kind === "signed-in" ? draftStorageKey(auth.user.id, model.name, recordKey) : void 0,
|
|
119
123
|
lines: linesDraft
|
|
120
124
|
});
|
|
121
125
|
const queryClient = useQueryClient();
|
|
@@ -125,6 +129,7 @@ const LoadedRecord = ({ model, recordKey, stored }) => {
|
|
|
125
129
|
const canUpdate = can(permissions, "update") && stored.permissions?.update !== false;
|
|
126
130
|
const { shown, offersEdit, editing } = recordShown(view?.recordMode, canUpdate, address?.kind === "record" && address.edit === true);
|
|
127
131
|
useNavigationGuard(editor.dirty && shown === "form");
|
|
132
|
+
usePauseFocusRefresh(editor.dirty);
|
|
128
133
|
const readAddress = {
|
|
129
134
|
kind: "record",
|
|
130
135
|
resource: model.name,
|
|
@@ -156,6 +161,7 @@ const LoadedRecord = ({ model, recordKey, stored }) => {
|
|
|
156
161
|
const updatedAt = auditField(model, "updated");
|
|
157
162
|
const hint = (name) => view?.fields[name];
|
|
158
163
|
const visible = (name) => fieldAccess(stored.permissions, name) !== "hidden" && whenShown(hint(name)?.when, editing);
|
|
164
|
+
const known = (name) => !preview || Object.hasOwn(stored.record, name);
|
|
159
165
|
const fieldView = (name) => {
|
|
160
166
|
const field = model.fields[name];
|
|
161
167
|
const access = fieldAccess(stored.permissions, name);
|
|
@@ -163,6 +169,7 @@ const LoadedRecord = ({ model, recordKey, stored }) => {
|
|
|
163
169
|
const conflict = editor.conflicts[name];
|
|
164
170
|
return /* @__PURE__ */ jsx(Field, {
|
|
165
171
|
label: hint(name)?.label ?? humanize(name),
|
|
172
|
+
required: shown === "form" && access !== "read" && isRequiredOnEdit(field),
|
|
166
173
|
help: fieldHelp(hint(name), view, editing),
|
|
167
174
|
error: editor.errors[name],
|
|
168
175
|
wide: Boolean(hint(name)?.multiline || hint(name)?.markdown),
|
|
@@ -177,7 +184,7 @@ const LoadedRecord = ({ model, recordKey, stored }) => {
|
|
|
177
184
|
onKeepMine: () => editor.keepMine(name),
|
|
178
185
|
onUseTheirs: () => editor.useTheirs(name)
|
|
179
186
|
},
|
|
180
|
-
children: /* @__PURE__ */ jsx(FieldEditor, {
|
|
187
|
+
children: known(name) ? /* @__PURE__ */ jsx(FieldEditor, {
|
|
181
188
|
field,
|
|
182
189
|
hints: hint(name),
|
|
183
190
|
value: editor.value(name),
|
|
@@ -189,12 +196,12 @@ const LoadedRecord = ({ model, recordKey, stored }) => {
|
|
|
189
196
|
reveal: field.sensitive ? () => client.reveal(model.name, recordKey, name) : void 0,
|
|
190
197
|
resource: model.name,
|
|
191
198
|
onChange: (next) => editor.change(name, next)
|
|
192
|
-
})
|
|
199
|
+
}) : /* @__PURE__ */ jsx(Skeleton, { className: "h-9" })
|
|
193
200
|
}, field.sensitive ? `${name}:${stored.etag}` : name);
|
|
194
201
|
};
|
|
195
202
|
const readRow = (name) => {
|
|
196
203
|
const field = model.fields[name];
|
|
197
|
-
const value = field.sensitive ? /* @__PURE__ */ jsx(FieldEditor, {
|
|
204
|
+
const value = !known(name) ? /* @__PURE__ */ jsx(Skeleton, { className: "h-4 w-32" }) : field.sensitive ? /* @__PURE__ */ jsx(FieldEditor, {
|
|
198
205
|
field,
|
|
199
206
|
hints: hint(name),
|
|
200
207
|
value: "",
|
|
@@ -222,6 +229,10 @@ const LoadedRecord = ({ model, recordKey, stored }) => {
|
|
|
222
229
|
rows: sidebarFields.flatMap((name) => {
|
|
223
230
|
const field = model.fields[name];
|
|
224
231
|
if (!field || !visible(name)) return [];
|
|
232
|
+
if (!known(name)) return [{
|
|
233
|
+
label: hint(name)?.label ?? humanize(name),
|
|
234
|
+
value: /* @__PURE__ */ jsx(Skeleton, { className: "h-4 w-24" })
|
|
235
|
+
}];
|
|
225
236
|
const text = displayValue(field, stored.record[name], hint(name));
|
|
226
237
|
const value = stored.record[name];
|
|
227
238
|
const markdown = isMarkdown(field, hint(name)) && typeof value === "string" && value !== "";
|
|
@@ -284,6 +295,7 @@ const LoadedRecord = ({ model, recordKey, stored }) => {
|
|
|
284
295
|
] });
|
|
285
296
|
return /* @__PURE__ */ jsxs("div", {
|
|
286
297
|
className: "flex flex-1 flex-col",
|
|
298
|
+
inert: preview,
|
|
287
299
|
children: [
|
|
288
300
|
remove.dialogs,
|
|
289
301
|
/* @__PURE__ */ jsx("div", {
|
|
@@ -355,6 +367,7 @@ const LoadedRecord = ({ model, recordKey, stored }) => {
|
|
|
355
367
|
recordKey,
|
|
356
368
|
owner: model.name
|
|
357
369
|
}, `related:${item.title}`)];
|
|
370
|
+
if (preview && (item.kind === "custom" || item.kind === "map" || item.kind === "gallery")) return [];
|
|
358
371
|
if (item.kind === "custom") return item.sidebar ? [] : [/* @__PURE__ */ jsx(CustomSection, {
|
|
359
372
|
item,
|
|
360
373
|
scope
|
|
@@ -385,7 +398,7 @@ const LoadedRecord = ({ model, recordKey, stored }) => {
|
|
|
385
398
|
children: fields.map(fieldView)
|
|
386
399
|
})
|
|
387
400
|
}, item.title)];
|
|
388
|
-
}), linesModel && /* @__PURE__ */ jsx(RecordLines, {
|
|
401
|
+
}), linesModel && !preview && /* @__PURE__ */ jsx(RecordLines, {
|
|
389
402
|
lines: linesModel,
|
|
390
403
|
draft: linesDraft,
|
|
391
404
|
vatRate: Number(stored.record.vatRate ?? 21) / 100,
|
|
@@ -399,7 +412,7 @@ const LoadedRecord = ({ model, recordKey, stored }) => {
|
|
|
399
412
|
className: "hidden flex-col items-center gap-2 md:flex",
|
|
400
413
|
children: formControls
|
|
401
414
|
}),
|
|
402
|
-
/* @__PURE__ */ jsx(RecordActions, {
|
|
415
|
+
!preview && /* @__PURE__ */ jsx(RecordActions, {
|
|
403
416
|
model,
|
|
404
417
|
stored,
|
|
405
418
|
recordKey,
|
|
@@ -408,11 +421,11 @@ const LoadedRecord = ({ model, recordKey, stored }) => {
|
|
|
408
421
|
}),
|
|
409
422
|
firstSidebar === -1 && summary,
|
|
410
423
|
layout.map((item, index) => {
|
|
411
|
-
if (item.kind === "custom" && item.sidebar) return /* @__PURE__ */ jsx(CustomSection, {
|
|
424
|
+
if (item.kind === "custom" && item.sidebar && !preview) return /* @__PURE__ */ jsx(CustomSection, {
|
|
412
425
|
item,
|
|
413
426
|
scope
|
|
414
427
|
}, `custom:${index}`);
|
|
415
|
-
if (item.kind === "map" && item.sidebar) return /* @__PURE__ */ jsx(MapSection, {
|
|
428
|
+
if (item.kind === "map" && item.sidebar && !preview) return /* @__PURE__ */ jsx(MapSection, {
|
|
416
429
|
item,
|
|
417
430
|
record: stored.record
|
|
418
431
|
}, `map:${index}`);
|
|
@@ -422,10 +435,11 @@ const LoadedRecord = ({ model, recordKey, stored }) => {
|
|
|
422
435
|
title: "Details",
|
|
423
436
|
rows: [auditField(model, "created"), updatedAt].flatMap((name) => {
|
|
424
437
|
const field = name ? model.fields[name] : void 0;
|
|
425
|
-
|
|
438
|
+
if (!name || !field || !visible(name)) return [];
|
|
439
|
+
return [{
|
|
426
440
|
label: hint(name)?.label ?? humanize(name),
|
|
427
|
-
value: displayValue(field, stored.record[name], hint(name))
|
|
428
|
-
}]
|
|
441
|
+
value: known(name) ? displayValue(field, stored.record[name], hint(name)) : /* @__PURE__ */ jsx(Skeleton, { className: "h-4 w-24" })
|
|
442
|
+
}];
|
|
429
443
|
})
|
|
430
444
|
}),
|
|
431
445
|
related.records.length > 0 && /* @__PURE__ */ jsx(SidebarCard, {
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { keys } from "../data/query-keys.js";
|
|
2
|
+
import { recordKey } from "../live/model-helpers.js";
|
|
3
|
+
import { useCallback } from "react";
|
|
4
|
+
import { useQueryClient } from "@tanstack/react-query";
|
|
5
|
+
import { encodeKey } from "@protobase/schema/internal";
|
|
6
|
+
//#region src/app/record-preview.ts
|
|
7
|
+
/**
|
|
8
|
+
* A record as far as a list row knows it: the fields the row loaded, its ETag and permissions as the list gives them.
|
|
9
|
+
* `row` holds relations as their keys, as `displayRow` splits a loaded row.
|
|
10
|
+
*/
|
|
11
|
+
const recordPreview = (model, row) => ({
|
|
12
|
+
record: Object.fromEntries(Object.entries(row).filter(([name]) => Object.hasOwn(model.fields, name))),
|
|
13
|
+
etag: typeof row.etag === "string" ? row.etag : "",
|
|
14
|
+
...typeof row.permissions === "object" && row.permissions !== null && { permissions: row.permissions }
|
|
15
|
+
});
|
|
16
|
+
/**
|
|
17
|
+
* Keeps what a list row knows of its record for the record's page, which shows it until the record has loaded
|
|
18
|
+
* (`useRecord`), so a record opened from a list shows its fields at once.
|
|
19
|
+
*/
|
|
20
|
+
const useSeedRecord = (model) => {
|
|
21
|
+
const queryClient = useQueryClient();
|
|
22
|
+
return useCallback((row) => queryClient.setQueryData(keys.recordPreview(model.name, encodeKey(recordKey(model, row))), recordPreview(model, row)), [queryClient, model]);
|
|
23
|
+
};
|
|
24
|
+
//#endregion
|
|
25
|
+
export { recordPreview, useSeedRecord };
|
|
@@ -16,12 +16,12 @@ import { ArrowRight } from "lucide-react";
|
|
|
16
16
|
import { Fragment as Fragment$1, jsx, jsxs } from "react/jsx-runtime";
|
|
17
17
|
import { createWhere, parseFilter, printFilter } from "@protobase/schema";
|
|
18
18
|
//#region src/app/record-row/record-row-body.tsx
|
|
19
|
-
const where = createWhere();
|
|
19
|
+
const where$1 = createWhere();
|
|
20
20
|
const scoped = (filter, scope) => {
|
|
21
21
|
if (!scope) return filter;
|
|
22
22
|
const parsed = parseFilter(filter);
|
|
23
23
|
if (!parsed.ok) throw new Error(`The filter cannot be read: ${parsed.errors.map((error) => error.message).join("; ")}`);
|
|
24
|
-
return printFilter(parsed.ast ? where.and(parsed.ast, scope) : scope);
|
|
24
|
+
return printFilter(parsed.ast ? where$1.and(parsed.ast, scope) : scope);
|
|
25
25
|
};
|
|
26
26
|
/** The display a row draws its cards with: the one named, else the view's first cards display, else a card of the title. */
|
|
27
27
|
const rowDisplay = (view, name) => {
|
|
@@ -26,6 +26,7 @@ const RecordRow = ({ model, view, display, source, label, isNew }) => {
|
|
|
26
26
|
total,
|
|
27
27
|
rowAt: source.rowAt,
|
|
28
28
|
loadRows: source.loadRows,
|
|
29
|
+
revision: source.revision,
|
|
29
30
|
latencyMs: 0
|
|
30
31
|
}, scroll.start, Math.max(scroll.start, scroll.end - 1));
|
|
31
32
|
const indexes = Array.from({ length: scroll.end - scroll.start }, (_, offset) => scroll.start + offset);
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
//#region src/app/related-dots/card-dots-lines.ts
|
|
2
|
+
/**
|
|
3
|
+
* The lines of dots a card of `display` shows for a row, in the order it names their columns: those of the list's
|
|
4
|
+
* related columns that were loaded with the row and hold a record. A row loaded without them, as a map's or a saved
|
|
5
|
+
* search's, shows none.
|
|
6
|
+
*/
|
|
7
|
+
const cardDotsLines = (view, display, row) => (display.dots ?? []).flatMap((name) => {
|
|
8
|
+
const column = view?.list?.relatedColumns?.find((candidate) => candidate.name === name);
|
|
9
|
+
const records = row.record[name];
|
|
10
|
+
return column && Array.isArray(records) && records.length > 0 ? [{
|
|
11
|
+
column,
|
|
12
|
+
records
|
|
13
|
+
}] : [];
|
|
14
|
+
});
|
|
15
|
+
//#endregion
|
|
16
|
+
export { cardDotsLines };
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { useAdminMeta } from "../meta-gate.js";
|
|
2
|
+
import { DotsRoom, RelatedDots } from "./related-dots.js";
|
|
3
|
+
import { jsx } from "react/jsx-runtime";
|
|
4
|
+
//#region src/app/related-dots/card-dots.tsx
|
|
5
|
+
const lines = "flex flex-col gap-1.5";
|
|
6
|
+
/** The room a card's lines of dots take at its bottom, inside its link, which the dots are drawn over. */
|
|
7
|
+
const CardDotsRoom = ({ dots }) => /* @__PURE__ */ jsx("span", {
|
|
8
|
+
className: lines,
|
|
9
|
+
children: dots.map(({ column, records }) => /* @__PURE__ */ jsx(DotsRoom, { count: records.length }, column.name))
|
|
10
|
+
});
|
|
11
|
+
/**
|
|
12
|
+
* A card's lines of dots, over the room its link holds for them at its bottom, inside its border and padding: beside
|
|
13
|
+
* the link in the document, so a dot is a link of its own and the pointer between the dots reaches the card's.
|
|
14
|
+
* The card's list item must be `relative`.
|
|
15
|
+
*/
|
|
16
|
+
const CardDots = ({ dots, Card }) => {
|
|
17
|
+
const { resources, views } = useAdminMeta();
|
|
18
|
+
return /* @__PURE__ */ jsx("div", {
|
|
19
|
+
className: `pointer-events-none absolute inset-x-[calc(0.75rem+1px)] bottom-[calc(0.75rem+1px)] ${lines}`,
|
|
20
|
+
children: dots.map(({ column, records }) => {
|
|
21
|
+
const target = resources[column.resource];
|
|
22
|
+
return target && /* @__PURE__ */ jsx(RelatedDots, {
|
|
23
|
+
column,
|
|
24
|
+
records,
|
|
25
|
+
target,
|
|
26
|
+
view: views[target.name],
|
|
27
|
+
Card,
|
|
28
|
+
wrap: true
|
|
29
|
+
}, column.name);
|
|
30
|
+
})
|
|
31
|
+
});
|
|
32
|
+
};
|
|
33
|
+
//#endregion
|
|
34
|
+
export { CardDots, CardDotsRoom };
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { displaysOf, shownFields } from "../displays/list-displays.js";
|
|
2
2
|
//#region src/app/related-dots/dot-card.ts
|
|
3
3
|
/**
|
|
4
|
-
* The card a dot shows of its record: the first cards display of the record's view, without the photos
|
|
5
|
-
* button a small card has no room for, else the record's title with its status as the badge.
|
|
4
|
+
* The card a dot shows of its record: the first cards display of the record's view, without the photos, favorite
|
|
5
|
+
* button and dots a small card has no room for, else the record's title with its status as the badge.
|
|
6
6
|
*/
|
|
7
7
|
const dotCard = (view, status) => {
|
|
8
8
|
const cards = displaysOf(view).find((display) => display.kind === "cards");
|
|
@@ -11,7 +11,7 @@ const dotCard = (view, status) => {
|
|
|
11
11
|
name: "dot",
|
|
12
12
|
badge: status
|
|
13
13
|
};
|
|
14
|
-
const { photos: _, favorite: __, ...card } = cards;
|
|
14
|
+
const { photos: _, favorite: __, dots: ___, ...card } = cards;
|
|
15
15
|
return card;
|
|
16
16
|
};
|
|
17
17
|
/** What the list loads for the related columns of its view among `columns`: each with the fields its dots' cards show. */
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { humanize } from "../../live/naming.js";
|
|
2
2
|
import { headerWidth, listColumnNames } from "../../live/columns-from-model.js";
|
|
3
3
|
import { RelatedDots } from "./related-dots.js";
|
|
4
|
+
import { RecordCard } from "../displays/record-card.js";
|
|
4
5
|
import { jsx } from "react/jsx-runtime";
|
|
5
6
|
//#region src/app/related-dots/related-dots-columns.tsx
|
|
6
7
|
/**
|
|
@@ -29,7 +30,8 @@ const withRelatedDotsColumns = (columns, model, view, resources, views) => {
|
|
|
29
30
|
column,
|
|
30
31
|
records: row[column.name] ?? [],
|
|
31
32
|
target,
|
|
32
|
-
view: views[target.name]
|
|
33
|
+
view: views[target.name],
|
|
34
|
+
Card: RecordCard
|
|
33
35
|
})
|
|
34
36
|
}];
|
|
35
37
|
});
|
|
@@ -7,21 +7,24 @@ import { displayRow } from "../displays/list-displays.js";
|
|
|
7
7
|
import { AppLink } from "../use-app-address.js";
|
|
8
8
|
import { toneFor } from "../../live/columns-from-model.js";
|
|
9
9
|
import { valueColor } from "../../live/value-color.js";
|
|
10
|
-
import { RecordCard } from "../displays/record-card.js";
|
|
11
|
-
import { dotCard } from "./dot-card.js";
|
|
12
10
|
import { placeCard } from "./card-place.js";
|
|
11
|
+
import { dotCard } from "./dot-card.js";
|
|
13
12
|
import { dotsSummary } from "./dots-summary.js";
|
|
14
13
|
import { useEffect, useLayoutEffect, useMemo, useRef, useState } from "react";
|
|
15
14
|
import { jsx, jsxs } from "react/jsx-runtime";
|
|
16
15
|
import { createPortal } from "react-dom";
|
|
17
16
|
//#region src/app/related-dots/related-dots.tsx
|
|
17
|
+
/** A dot's box and its mark, which a card's room for its dots (`DotsRoom`) takes the size of. */
|
|
18
|
+
const dotBox = "flex shrink-0 items-center justify-center rounded-full p-0.5 pointer-coarse:p-1.5";
|
|
19
|
+
const dotMark = "size-2.5 rounded-full";
|
|
20
|
+
const dotsLine = "flex min-w-0 items-center gap-0.5";
|
|
18
21
|
const hideMs = 150;
|
|
19
22
|
const cardWidth = 288;
|
|
20
23
|
/**
|
|
21
24
|
* The card of the dot that is hovered, focused or tapped, portalled to the body: every grid row is its own stacking
|
|
22
25
|
* context and the grid body clips, so a card inside the row would sit under the rows below it.
|
|
23
26
|
*/
|
|
24
|
-
const DotCardPopover = ({ shown, row, target, view, column, onEnter, onLeave }) => {
|
|
27
|
+
const DotCardPopover = ({ shown, row, target, view, column, Card, onEnter, onLeave }) => {
|
|
25
28
|
const card = useRef(null);
|
|
26
29
|
const [place, setPlace] = useState();
|
|
27
30
|
useLayoutEffect(() => {
|
|
@@ -48,7 +51,7 @@ const DotCardPopover = ({ shown, row, target, view, column, onEnter, onLeave })
|
|
|
48
51
|
}
|
|
49
52
|
},
|
|
50
53
|
className: "fixed z-50 rounded-lg shadow-pop",
|
|
51
|
-
children: /* @__PURE__ */ jsx(
|
|
54
|
+
children: /* @__PURE__ */ jsx(Card, {
|
|
52
55
|
model: target,
|
|
53
56
|
view,
|
|
54
57
|
display: dotCard(view, column.status),
|
|
@@ -61,7 +64,7 @@ const DotCardPopover = ({ shown, row, target, view, column, onEnter, onLeave })
|
|
|
61
64
|
* or focusing a dot shows the record's card; on a touch screen the first tap shows it, and the card opens the record.
|
|
62
65
|
* The dots say how many records have each status to a screen reader.
|
|
63
66
|
*/
|
|
64
|
-
const RelatedDots = ({ column, records, target, view }) => {
|
|
67
|
+
const RelatedDots = ({ column, records, target, view, wrap = false, Card }) => {
|
|
65
68
|
const rows = useMemo(() => records.map((record) => displayRow(target, record)), [records, target]);
|
|
66
69
|
const [shown, setShown] = useState();
|
|
67
70
|
const hideTimer = useRef(void 0);
|
|
@@ -123,7 +126,7 @@ const RelatedDots = ({ column, records, target, view }) => {
|
|
|
123
126
|
const shownRow = shown && rows.find((row) => row.key === shown.key);
|
|
124
127
|
return /* @__PURE__ */ jsxs("div", {
|
|
125
128
|
ref: box,
|
|
126
|
-
className: "
|
|
129
|
+
className: cn(dotsLine, wrap && "pointer-events-none flex-wrap"),
|
|
127
130
|
onClick: (event) => event.stopPropagation(),
|
|
128
131
|
onPointerDown: (event) => pointer.current = event.pointerType,
|
|
129
132
|
children: [
|
|
@@ -146,11 +149,11 @@ const RelatedDots = ({ column, records, target, view }) => {
|
|
|
146
149
|
onFocus: (event) => event.currentTarget.matches(":focus-visible") && show(row.key, event.currentTarget),
|
|
147
150
|
onBlur: blur,
|
|
148
151
|
onClick: tap(row.key),
|
|
149
|
-
className: "
|
|
152
|
+
className: cn(dotBox, "pointer-events-auto focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary/40"),
|
|
150
153
|
children: /* @__PURE__ */ jsx("span", {
|
|
151
154
|
"aria-hidden": true,
|
|
152
155
|
style: color === void 0 ? void 0 : { backgroundColor: color },
|
|
153
|
-
className: cn(
|
|
156
|
+
className: cn(dotMark, color === void 0 && toneDots[toneFor(String(value))], shown?.key === row.key && "ring-2 ring-foreground/30")
|
|
154
157
|
})
|
|
155
158
|
}, row.key);
|
|
156
159
|
}),
|
|
@@ -160,11 +163,21 @@ const RelatedDots = ({ column, records, target, view }) => {
|
|
|
160
163
|
target,
|
|
161
164
|
view,
|
|
162
165
|
column,
|
|
166
|
+
Card,
|
|
163
167
|
onEnter: cancelHide,
|
|
164
168
|
onLeave: hideSoon
|
|
165
169
|
})
|
|
166
170
|
]
|
|
167
171
|
});
|
|
168
172
|
};
|
|
173
|
+
/** Room for `count` dots that wrap as `RelatedDots` with `wrap` does, for a card to hold the dots drawn over it. */
|
|
174
|
+
const DotsRoom = ({ count }) => /* @__PURE__ */ jsx("span", {
|
|
175
|
+
"aria-hidden": true,
|
|
176
|
+
className: cn(dotsLine, "invisible flex-wrap"),
|
|
177
|
+
children: Array.from({ length: count }, (_, index) => /* @__PURE__ */ jsx("span", {
|
|
178
|
+
className: dotBox,
|
|
179
|
+
children: /* @__PURE__ */ jsx("span", { className: dotMark })
|
|
180
|
+
}, index))
|
|
181
|
+
});
|
|
169
182
|
//#endregion
|
|
170
|
-
export { RelatedDots };
|
|
183
|
+
export { DotsRoom, RelatedDots };
|