@protobase/ui 0.13.4 → 0.14.1
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/current-address.js +1 -1
- package/dist/app/custom-section.js +1 -1
- package/dist/app/displays/custom-display.js +1 -1
- package/dist/app/displays/list-cards.js +2 -2
- package/dist/app/displays/list-displays.js +1 -1
- package/dist/app/displays/virtual-cards.js +43 -65
- package/dist/app/error-reporting.js +1 -1
- package/dist/app/list-page.js +11 -3
- package/dist/app/nav-recent.js +2 -1
- package/dist/app/pages/blocks/link.js +2 -1
- package/dist/app/pages/render-node.js +1 -1
- package/dist/app/pages/use-layout-data.js +1 -1
- package/dist/app/pages/use-run-action.js +1 -1
- package/dist/app/search/search-model.js +2 -1
- package/dist/app/use-lines-draft.js +1 -1
- package/dist/app-shell/app-shell.js +54 -38
- package/dist/app-shell/bottom-bar.d.ts +1 -0
- package/dist/app-shell/bottom-bar.js +3 -2
- package/dist/app-shell/index.d.ts +1 -0
- package/dist/app-shell/page-scroll.d.ts +1 -0
- package/dist/app-shell/page-scroll.js +56 -0
- package/dist/app-shell/sidebar.js +2 -2
- package/dist/app-shell/top-bar.js +3 -2
- package/dist/assistant-dock/use-assistant.js +1 -1
- package/dist/data/use-record.js +1 -1
- package/dist/data/use-update-record.js +1 -1
- package/dist/data-grid/data-grid.js +92 -85
- package/dist/data-grid/geometry.js +26 -6
- package/dist/data-grid/grid-footer.js +5 -12
- package/dist/data-grid/scroll-parent.js +35 -0
- package/dist/data-grid/use-virtual-rows.js +123 -18
- package/dist/package.js +1 -1
- package/dist/plugins/load-plugin.d.ts +1 -1
- package/dist/plugins/load-plugin.js +1 -1
- package/dist/shell/assets/app-Dkz6z_t_.js +66 -0
- package/dist/shell/assets/{index-rR2iasG9.js → index-BCLK6g9a.js} +1 -1
- package/dist/shell/assets/index-BQ4FIU4i.css +2 -0
- package/dist/shell/assets/{maplibre-gl-estTAzEH.js → maplibre-gl-DWx1yzYO.js} +1 -1
- package/dist/shell/assets/{record-map-B57A7PQ0.js → record-map-C2M8dEjy.js} +2 -2
- package/dist/shell/assets/{records-map-BwsWXD8A.js → records-map-huS0J9xV.js} +1 -1
- package/dist/shell/assets/{shared-protobase-ui-CE7mIalS.js → shared-protobase-ui-CsJxPCx6.js} +1 -1
- package/dist/shell/index.html +4 -4
- package/dist/shell/protobase.shell.json +1 -1
- package/package.json +4 -4
- package/dist/data-grid/use-grid-scroll.js +0 -87
- package/dist/shell/assets/app-BBslM0jG.js +0 -66
- package/dist/shell/assets/index-CYWfU_c1.css +0 -2
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { defaultDisplay } from "./displays/list-displays.js";
|
|
2
2
|
import { defaultOrderBy } from "./list-order.js";
|
|
3
|
-
import { encodeKey } from "@protobase/schema";
|
|
3
|
+
import { encodeKey } from "@protobase/schema/internal";
|
|
4
4
|
//#region src/app/current-address.ts
|
|
5
5
|
/** What a list shows when its address names nothing: its default display for the screen, the list's sort, the filters in a bar. */
|
|
6
6
|
const listDefaults = (model, view, xs = false) => ({
|
|
@@ -5,7 +5,7 @@ import { Section } from "../record-view/section.js";
|
|
|
5
5
|
import { SidebarCard } from "../record-view/record-sidebar.js";
|
|
6
6
|
import { RecordScope } from "./pages/record-scope.js";
|
|
7
7
|
import { jsx, jsxs } from "react/jsx-runtime";
|
|
8
|
-
import { unmarkFieldRefs } from "@protobase/schema";
|
|
8
|
+
import { unmarkFieldRefs } from "@protobase/schema/internal";
|
|
9
9
|
//#region src/app/custom-section.tsx
|
|
10
10
|
/**
|
|
11
11
|
* A component of the record layout (`l.custom`), drawn with the record around it, so `useLayoutRecord()` gives it, in a
|
|
@@ -3,7 +3,7 @@ import { Problem } from "../pages/problem.js";
|
|
|
3
3
|
import { CustomBoundary } from "../pages/custom-boundary.js";
|
|
4
4
|
import { ListDisplayProvider } from "./display-context.js";
|
|
5
5
|
import { jsx, jsxs } from "react/jsx-runtime";
|
|
6
|
-
import { unmarkFieldRefs } from "@protobase/schema";
|
|
6
|
+
import { unmarkFieldRefs } from "@protobase/schema/internal";
|
|
7
7
|
//#region src/app/displays/custom-display.tsx
|
|
8
8
|
/**
|
|
9
9
|
* A display the project draws: the component registered under its name gets the display's props, field references
|
|
@@ -10,8 +10,8 @@ const grid = "grid grid-cols-[repeat(auto-fill,minmax(15rem,1fr))] gap-3";
|
|
|
10
10
|
const pictureHeight = (width) => width * .75;
|
|
11
11
|
const plainHeight = 44;
|
|
12
12
|
/**
|
|
13
|
-
* The list page's cards display: a card per record (see `RecordCard`), scrolled like the table
|
|
14
|
-
*
|
|
13
|
+
* The list page's cards display: a card per record (see `RecordCard`), scrolled like the table. An embedded list,
|
|
14
|
+
* which shows a few cards whole, draws them with `CardsDisplay`.
|
|
15
15
|
*/
|
|
16
16
|
const ListCards = ({ model, view, display, list }) => {
|
|
17
17
|
const plural = view?.names?.plural ?? humanize(model.name);
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { recordKey, recordTitleField } from "../../live/model-helpers.js";
|
|
2
2
|
import { humanize } from "../../live/naming.js";
|
|
3
3
|
import { displayValue } from "../../live/display-value.js";
|
|
4
|
-
import { encodeKey, tableDisplay } from "@protobase/schema";
|
|
4
|
+
import { encodeKey, tableDisplay } from "@protobase/schema/internal";
|
|
5
5
|
//#region src/app/displays/list-displays.ts
|
|
6
6
|
const displaysOf = (view) => view?.list?.displays ?? [];
|
|
7
7
|
/**
|
|
@@ -3,15 +3,17 @@ import { usePagedRows } from "../../data-grid/use-paged-rows.js";
|
|
|
3
3
|
import { useVirtualRows } from "../../data-grid/use-virtual-rows.js";
|
|
4
4
|
import { anchoredTop, cardColumns, cardWidth, recordRange } from "./card-layout.js";
|
|
5
5
|
import { useCallback, useLayoutEffect, useRef, useState } from "react";
|
|
6
|
-
import { jsx
|
|
6
|
+
import { jsx } from "react/jsx-runtime";
|
|
7
7
|
//#region src/app/displays/virtual-cards.tsx
|
|
8
8
|
/**
|
|
9
|
-
* Cards in rows as wide as the list, scrolled like the table
|
|
10
|
-
*
|
|
11
|
-
* pages and the first record in view. Every card in a width is as tall as the tallest
|
|
9
|
+
* Cards in rows as wide as the list, scrolled like the table by the page or the nearest scrolling ancestor: the list is
|
|
10
|
+
* as tall as all its rows, only the rows in view are drawn, and the records are loaded in the table's pages, so a
|
|
11
|
+
* change of width keeps the loaded pages and the first record in view. Every card in a width is as tall as the tallest
|
|
12
|
+
* drawn so far.
|
|
12
13
|
*/
|
|
13
14
|
const VirtualCards = ({ source, label, initialIndex, onScrollIndex, minHeight, children }) => {
|
|
14
15
|
const { total } = source;
|
|
16
|
+
const [startIndex] = useState(initialIndex);
|
|
15
17
|
const [width, setWidth] = useState(0);
|
|
16
18
|
const columns = cardColumns(width);
|
|
17
19
|
const itemWidth = cardWidth(width, columns);
|
|
@@ -29,13 +31,15 @@ const VirtualCards = ({ source, label, initialIndex, onScrollIndex, minHeight, c
|
|
|
29
31
|
layout,
|
|
30
32
|
top: 0
|
|
31
33
|
});
|
|
32
|
-
const changed = anchor.current.layout.columns !== columns || anchor.current.layout.rowHeight !== rowHeight;
|
|
34
|
+
const changed = anchor.current.top > 0 && (anchor.current.layout.columns !== columns || anchor.current.layout.rowHeight !== rowHeight);
|
|
33
35
|
const settle = (top) => changed ? anchoredTop(anchor.current.top, anchor.current.layout, layout) : top;
|
|
36
|
+
const ready = itemWidth > 0 && measured.width === itemWidth;
|
|
34
37
|
const scroll = useVirtualRows({
|
|
35
38
|
total: Math.ceil(total / columns),
|
|
36
39
|
rowHeight,
|
|
37
|
-
initialIndex: Math.floor(
|
|
38
|
-
settle
|
|
40
|
+
initialIndex: Math.floor(startIndex / columns),
|
|
41
|
+
settle,
|
|
42
|
+
ready
|
|
39
43
|
});
|
|
40
44
|
useLayoutEffect(() => {
|
|
41
45
|
if (changed) scroll.scrollTo(scroll.top);
|
|
@@ -43,7 +47,7 @@ const VirtualCards = ({ source, label, initialIndex, onScrollIndex, minHeight, c
|
|
|
43
47
|
layout,
|
|
44
48
|
top: scroll.top
|
|
45
49
|
};
|
|
46
|
-
if (!changed) onScrollIndex(Math.floor(scroll.top / rowHeight) * columns);
|
|
50
|
+
if (!changed) onScrollIndex(Math.floor(Math.max(0, scroll.top) / rowHeight) * columns);
|
|
47
51
|
});
|
|
48
52
|
const range = recordRange(scroll.first, scroll.last, columns, total);
|
|
49
53
|
const getRow = usePagedRows({
|
|
@@ -52,9 +56,8 @@ const VirtualCards = ({ source, label, initialIndex, onScrollIndex, minHeight, c
|
|
|
52
56
|
loadRows: source.loadRows,
|
|
53
57
|
latencyMs: 0
|
|
54
58
|
}, range.start, range.end);
|
|
55
|
-
const bodyRef = useRef(null);
|
|
56
59
|
useLayoutEffect(() => {
|
|
57
|
-
const body =
|
|
60
|
+
const body = scroll.ref.current;
|
|
58
61
|
if (!body) return;
|
|
59
62
|
const observer = new ResizeObserver(() => setWidth(body.clientWidth));
|
|
60
63
|
observer.observe(body);
|
|
@@ -77,61 +80,36 @@ const VirtualCards = ({ source, label, initialIndex, onScrollIndex, minHeight, c
|
|
|
77
80
|
observer.observe(item);
|
|
78
81
|
return () => observer.unobserve(item);
|
|
79
82
|
}, [observer]);
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
style: place,
|
|
111
|
-
children: /* @__PURE__ */ jsx(Skeleton, { className: "h-full rounded-lg" })
|
|
112
|
-
}, `skeleton-${index}`);
|
|
113
|
-
return /* @__PURE__ */ jsx("li", {
|
|
114
|
-
ref: observe,
|
|
115
|
-
"aria-posinset": index + 1,
|
|
116
|
-
"aria-setsize": total,
|
|
117
|
-
className: "absolute grid grid-cols-[minmax(0,1fr)]",
|
|
118
|
-
style: place,
|
|
119
|
-
children: children(row)
|
|
120
|
-
}, row.id);
|
|
121
|
-
}))
|
|
122
|
-
})
|
|
123
|
-
})
|
|
124
|
-
})
|
|
125
|
-
}), /* @__PURE__ */ jsxs("div", {
|
|
126
|
-
className: "py-3 text-xs tabular-nums text-muted-foreground",
|
|
127
|
-
children: [
|
|
128
|
-
firstShown.toLocaleString("en-IE"),
|
|
129
|
-
"–",
|
|
130
|
-
lastShown.toLocaleString("en-IE"),
|
|
131
|
-
" of ",
|
|
132
|
-
total.toLocaleString("en-IE")
|
|
133
|
-
]
|
|
134
|
-
})]
|
|
83
|
+
return /* @__PURE__ */ jsx("ul", {
|
|
84
|
+
ref: scroll.ref,
|
|
85
|
+
className: "relative min-w-0 flex-auto",
|
|
86
|
+
"aria-label": label,
|
|
87
|
+
style: { height: scroll.geometry.contentHeight },
|
|
88
|
+
children: scroll.rows.flatMap(({ index: rowIndex, offset }) => Array.from({ length: columns }, (_, column) => {
|
|
89
|
+
const index = rowIndex * columns + column;
|
|
90
|
+
if (index >= total) return [];
|
|
91
|
+
const row = getRow(index);
|
|
92
|
+
const place = {
|
|
93
|
+
top: offset,
|
|
94
|
+
left: column * (itemWidth + 12),
|
|
95
|
+
width: itemWidth,
|
|
96
|
+
minHeight: itemHeight
|
|
97
|
+
};
|
|
98
|
+
if (!row) return /* @__PURE__ */ jsx("li", {
|
|
99
|
+
"aria-hidden": true,
|
|
100
|
+
className: "absolute grid grid-cols-[minmax(0,1fr)]",
|
|
101
|
+
style: place,
|
|
102
|
+
children: /* @__PURE__ */ jsx(Skeleton, { className: "h-full rounded-lg" })
|
|
103
|
+
}, `skeleton-${index}`);
|
|
104
|
+
return /* @__PURE__ */ jsx("li", {
|
|
105
|
+
ref: observe,
|
|
106
|
+
"aria-posinset": index + 1,
|
|
107
|
+
"aria-setsize": total,
|
|
108
|
+
className: "absolute grid grid-cols-[minmax(0,1fr)]",
|
|
109
|
+
style: place,
|
|
110
|
+
children: children(row)
|
|
111
|
+
}, row.id);
|
|
112
|
+
}))
|
|
135
113
|
});
|
|
136
114
|
};
|
|
137
115
|
//#endregion
|
package/dist/app/list-page.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { useIsDesktop } from "../lib/use-media-query.js";
|
|
2
|
+
import { usePageScroll } from "../app-shell/page-scroll.js";
|
|
2
3
|
import { PageHeader } from "../app-shell/page-header.js";
|
|
3
4
|
import { useAuth } from "../auth/auth-provider.js";
|
|
4
5
|
import { Button } from "../primitives/button.js";
|
|
@@ -38,7 +39,8 @@ import { withFieldTypeCells } from "./field-types/field-type-cells.js";
|
|
|
38
39
|
import { useMemo, useState } from "react";
|
|
39
40
|
import { Plus, SlidersHorizontal } from "lucide-react";
|
|
40
41
|
import { Fragment as Fragment$1, jsx, jsxs } from "react/jsx-runtime";
|
|
41
|
-
import {
|
|
42
|
+
import { tableDisplay } from "@protobase/schema/internal";
|
|
43
|
+
import { createWhere } from "@protobase/schema";
|
|
42
44
|
//#region src/app/list-page.tsx
|
|
43
45
|
const where = createWhere();
|
|
44
46
|
const searchText = (extras) => extras.find((clause) => clause.kind === "search");
|
|
@@ -99,6 +101,8 @@ const LoadedList = ({ model, address }) => {
|
|
|
99
101
|
orderBy
|
|
100
102
|
});
|
|
101
103
|
const display = shownDisplay(view, address.display);
|
|
104
|
+
const pageScroll = !display || display.kind === "cards";
|
|
105
|
+
usePageScroll(pageScroll);
|
|
102
106
|
const displayRows = useDisplayRows(model, rows.page, rows.key, display?.kind === "custom");
|
|
103
107
|
const scroll = useListScroll();
|
|
104
108
|
const titleField = recordTitleField(model, view);
|
|
@@ -254,7 +258,8 @@ const LoadedList = ({ model, address }) => {
|
|
|
254
258
|
}] : [],
|
|
255
259
|
rowNumbers: false,
|
|
256
260
|
pinKey: auth.kind === "signed-in" ? pinnedStorageKey(auth.user.id, resource) : void 0,
|
|
257
|
-
|
|
261
|
+
scroll: "parent",
|
|
262
|
+
className: "flex-1"
|
|
258
263
|
}) : /* @__PURE__ */ jsx("div", { className: "flex-1 rounded-lg border bg-surface" });
|
|
259
264
|
const cardsList = {
|
|
260
265
|
...rows,
|
|
@@ -313,7 +318,10 @@ const LoadedList = ({ model, address }) => {
|
|
|
313
318
|
desktop ? desktopToolbar : phoneFilters,
|
|
314
319
|
desktop && layout === "panel" ? /* @__PURE__ */ jsxs("div", {
|
|
315
320
|
className: "flex min-h-0 flex-1 gap-4",
|
|
316
|
-
children: [
|
|
321
|
+
children: [/* @__PURE__ */ jsx("div", {
|
|
322
|
+
className: pageScroll ? "sticky top-[calc(var(--pb-sticky-top,0px)+1rem)] flex max-h-[calc(100dvh-var(--pb-sticky-top,0px)-var(--pb-sticky-bottom,0px)-2rem)] shrink-0 self-start" : "flex shrink-0",
|
|
323
|
+
children: panel
|
|
324
|
+
}), shown]
|
|
317
325
|
}) : shown
|
|
318
326
|
]
|
|
319
327
|
});
|
package/dist/app/nav-recent.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { recordKey, recordTitle, recordTitleField } from "../live/model-helpers.js";
|
|
2
2
|
import { defaultOrderBy } from "./list-order.js";
|
|
3
|
-
import {
|
|
3
|
+
import { defaultRecentLimit, encodeKey } from "@protobase/schema/internal";
|
|
4
|
+
import { appPath, enumLabel } from "@protobase/schema";
|
|
4
5
|
//#region src/app/nav-recent.ts
|
|
5
6
|
/** The list request behind a sidebar group: its filter, order (the list's sort when unset), size, and only the fields it shows. */
|
|
6
7
|
const recentRequest = (model, view, recent) => ({
|
|
@@ -2,7 +2,8 @@ import { AppLink } from "../../use-app-address.js";
|
|
|
2
2
|
import { useLayoutRecord } from "../record-scope.js";
|
|
3
3
|
import { propsOf } from "./block-props.js";
|
|
4
4
|
import { jsx } from "react/jsx-runtime";
|
|
5
|
-
import {
|
|
5
|
+
import { fillAddress } from "@protobase/schema/internal";
|
|
6
|
+
import { appPath } from "@protobase/schema";
|
|
6
7
|
//#region src/app/pages/blocks/link.tsx
|
|
7
8
|
const style = "font-medium text-primary-text hover:underline";
|
|
8
9
|
/**
|
|
@@ -16,7 +16,7 @@ import { StatBlock } from "./blocks/stat.js";
|
|
|
16
16
|
import { TableBlock } from "./blocks/table.js";
|
|
17
17
|
import { Fragment } from "react";
|
|
18
18
|
import { Fragment as Fragment$1, jsx, jsxs } from "react/jsx-runtime";
|
|
19
|
-
import { unmarkFieldRefs } from "@protobase/schema";
|
|
19
|
+
import { unmarkFieldRefs } from "@protobase/schema/internal";
|
|
20
20
|
import { isLayoutNode } from "@protobase/layout";
|
|
21
21
|
//#region src/app/pages/render-node.tsx
|
|
22
22
|
const blocks = {
|
|
@@ -3,7 +3,7 @@ import { useClient } from "../../data/api-provider.js";
|
|
|
3
3
|
import { recordKey } from "../../live/model-helpers.js";
|
|
4
4
|
import { useCallback } from "react";
|
|
5
5
|
import { keepPreviousData, useQuery, useQueryClient } from "@tanstack/react-query";
|
|
6
|
-
import { encodeKey } from "@protobase/schema";
|
|
6
|
+
import { encodeKey } from "@protobase/schema/internal";
|
|
7
7
|
//#region src/app/pages/use-layout-data.ts
|
|
8
8
|
/** A list item as a record scope: the key as the URL carries it, and the etag and permissions the item carries. */
|
|
9
9
|
const scopeOf = (model, row) => ({
|
|
@@ -7,7 +7,7 @@ import { can } from "../permissions.js";
|
|
|
7
7
|
import { fillHref } from "./action-href.js";
|
|
8
8
|
import { useRefreshResource } from "./use-layout-data.js";
|
|
9
9
|
import { useState } from "react";
|
|
10
|
-
import { fillAddress } from "@protobase/schema";
|
|
10
|
+
import { fillAddress } from "@protobase/schema/internal";
|
|
11
11
|
//#region src/app/pages/use-run-action.ts
|
|
12
12
|
const message = (error) => error instanceof Error ? error.message : "Something went wrong";
|
|
13
13
|
/**
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { recordKey, recordTitleField } from "../../live/model-helpers.js";
|
|
2
2
|
import { humanize } from "../../live/naming.js";
|
|
3
|
-
import {
|
|
3
|
+
import { encodeKey, matchedSearchField } from "@protobase/schema/internal";
|
|
4
|
+
import { appPath, createWhere, enumLabel, printFilter } from "@protobase/schema";
|
|
4
5
|
//#region src/app/search/search-model.ts
|
|
5
6
|
const where = createWhere();
|
|
6
7
|
const searchTargets = (meta) => Object.values(meta.resources).flatMap((model) => {
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { recordKey } from "../live/model-helpers.js";
|
|
2
2
|
import { useCallback, useMemo, useState } from "react";
|
|
3
|
-
import { encodeKey } from "@protobase/schema";
|
|
3
|
+
import { encodeKey } from "@protobase/schema/internal";
|
|
4
4
|
//#region src/app/use-lines-draft.ts
|
|
5
5
|
/**
|
|
6
6
|
* The order the person has given the lines of a record, kept in the form until Save writes it, together with the
|
|
@@ -2,9 +2,10 @@ import { cn } from "../lib/cn.js";
|
|
|
2
2
|
import { useIsDesktop } from "../lib/use-media-query.js";
|
|
3
3
|
import { BottomBar } from "./bottom-bar.js";
|
|
4
4
|
import { Drawer } from "./drawer.js";
|
|
5
|
+
import { PageScrollContext, usePageInsets } from "./page-scroll.js";
|
|
5
6
|
import { Sidebar } from "./sidebar.js";
|
|
6
7
|
import { TopBar } from "./top-bar.js";
|
|
7
|
-
import { useEffect, useState } from "react";
|
|
8
|
+
import { useEffect, useRef, useState } from "react";
|
|
8
9
|
import { jsx, jsxs } from "react/jsx-runtime";
|
|
9
10
|
//#region src/app-shell/app-shell.tsx
|
|
10
11
|
const drawerId = "app-drawer";
|
|
@@ -15,8 +16,13 @@ const AppShell = ({ sidebarMode, activeItem, breadcrumb, user, workspace, action
|
|
|
15
16
|
if (desktop) setDrawerOpen(false);
|
|
16
17
|
}, [desktop]);
|
|
17
18
|
const tabs = !desktop && bottomBar && bottomBar.length > 0 ? bottomBar : void 0;
|
|
18
|
-
const
|
|
19
|
+
const [pageScroll, setPageScroll] = useState(false);
|
|
20
|
+
const topBarRef = useRef(null);
|
|
21
|
+
const bottomBarRef = useRef(null);
|
|
22
|
+
usePageInsets(pageScroll, topBarRef, bottomBarRef, Boolean(tabs));
|
|
23
|
+
const sidebar = (mode, sticky = false) => /* @__PURE__ */ jsx(Sidebar, {
|
|
19
24
|
mode,
|
|
25
|
+
sticky,
|
|
20
26
|
activeItem,
|
|
21
27
|
user,
|
|
22
28
|
workspace,
|
|
@@ -30,42 +36,52 @@ const AppShell = ({ sidebarMode, activeItem, breadcrumb, user, workspace, action
|
|
|
30
36
|
organizations,
|
|
31
37
|
roles
|
|
32
38
|
});
|
|
33
|
-
return /* @__PURE__ */
|
|
34
|
-
|
|
35
|
-
children:
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
39
|
+
return /* @__PURE__ */ jsx(PageScrollContext.Provider, {
|
|
40
|
+
value: setPageScroll,
|
|
41
|
+
children: /* @__PURE__ */ jsxs("div", {
|
|
42
|
+
className: cn(pageScroll ? "flex min-h-full w-full overflow-x-clip bg-background" : "flex h-full min-h-0 w-full overflow-hidden bg-background", className),
|
|
43
|
+
children: [
|
|
44
|
+
desktop ? sidebar(sidebarMode, pageScroll) : /* @__PURE__ */ jsx(Drawer, {
|
|
45
|
+
id: drawerId,
|
|
46
|
+
open: drawerOpen,
|
|
47
|
+
onClose: () => setDrawerOpen(false),
|
|
48
|
+
children: sidebar("text-large")
|
|
49
|
+
}),
|
|
50
|
+
/* @__PURE__ */ jsxs("div", {
|
|
51
|
+
className: "flex min-w-0 flex-1 flex-col",
|
|
52
|
+
children: [
|
|
53
|
+
/* @__PURE__ */ jsx(TopBar, {
|
|
54
|
+
ref: topBarRef,
|
|
55
|
+
sticky: pageScroll,
|
|
56
|
+
breadcrumb,
|
|
57
|
+
onNavigate,
|
|
58
|
+
actions,
|
|
59
|
+
onMenu: desktop || tabs ? void 0 : () => setDrawerOpen(true),
|
|
60
|
+
menuOpen: drawerOpen,
|
|
61
|
+
menuControls: drawerId,
|
|
62
|
+
search
|
|
63
|
+
}),
|
|
64
|
+
/* @__PURE__ */ jsx("main", {
|
|
65
|
+
className: pageScroll ? "flex flex-1 flex-col" : "flex min-h-0 flex-1 flex-col",
|
|
66
|
+
children
|
|
67
|
+
}),
|
|
68
|
+
tabs && /* @__PURE__ */ jsx(BottomBar, {
|
|
69
|
+
ref: bottomBarRef,
|
|
70
|
+
sticky: pageScroll,
|
|
71
|
+
tabs,
|
|
72
|
+
onNavigate,
|
|
73
|
+
onMore: () => setDrawerOpen(true),
|
|
74
|
+
moreOpen: drawerOpen,
|
|
75
|
+
moreControls: drawerId
|
|
76
|
+
})
|
|
77
|
+
]
|
|
78
|
+
}),
|
|
79
|
+
rightPanel && /* @__PURE__ */ jsx("div", {
|
|
80
|
+
className: pageScroll ? "sticky top-0 flex h-dvh shrink-0 self-start" : "flex shrink-0",
|
|
81
|
+
children: rightPanel
|
|
82
|
+
})
|
|
83
|
+
]
|
|
84
|
+
})
|
|
69
85
|
});
|
|
70
86
|
};
|
|
71
87
|
//#endregion
|
|
@@ -9,11 +9,12 @@ const tab = "flex min-h-[49px] min-w-0 flex-1 flex-col items-center justify-cent
|
|
|
9
9
|
* last, which opens the navigation drawer and is the current tab on screens the others do not hold. The padding below
|
|
10
10
|
* keeps the tabs clear of the iPhone home indicator.
|
|
11
11
|
*/
|
|
12
|
-
const BottomBar = ({ tabs, onNavigate, onMore, moreOpen, moreControls }) => {
|
|
12
|
+
const BottomBar = ({ tabs, onNavigate, onMore, moreOpen, moreControls, sticky, ref }) => {
|
|
13
13
|
const moreCurrent = !tabs.some((item) => item.active);
|
|
14
14
|
return /* @__PURE__ */ jsxs("nav", {
|
|
15
|
+
ref,
|
|
15
16
|
"aria-label": "Tabs",
|
|
16
|
-
className: "flex shrink-0 border-t bg-background pb-[env(safe-area-inset-bottom)] md:hidden",
|
|
17
|
+
className: cn("flex shrink-0 border-t bg-background pb-[env(safe-area-inset-bottom)] md:hidden", sticky && "sticky bottom-0 z-30"),
|
|
17
18
|
children: [tabs.map((item) => {
|
|
18
19
|
const Icon = item.icon;
|
|
19
20
|
return /* @__PURE__ */ jsxs("a", {
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import "react";
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { createContext, useContext, useLayoutEffect } from "react";
|
|
2
|
+
//#region src/app-shell/page-scroll.ts
|
|
3
|
+
/** Turns the shell's page scrolling on or off; pages use `usePageScroll`. */
|
|
4
|
+
const PageScrollContext = createContext(() => {});
|
|
5
|
+
/**
|
|
6
|
+
* While the calling page is shown and `on`, the document scrolls instead of a box of the screen's height: the top and
|
|
7
|
+
* bottom bars and the sidebar stick, and the page grows with its content, so the browser's own scrolling applies
|
|
8
|
+
* throughout, scrolling to the top included, as tapping the status bar on an iPhone does. Sticky content in the page
|
|
9
|
+
* stays clear of the bars with `--pb-sticky-top` and `--pb-sticky-bottom`.
|
|
10
|
+
*/
|
|
11
|
+
const usePageScroll = (on) => {
|
|
12
|
+
const set = useContext(PageScrollContext);
|
|
13
|
+
useLayoutEffect(() => {
|
|
14
|
+
set(on);
|
|
15
|
+
return () => set(false);
|
|
16
|
+
}, [set, on]);
|
|
17
|
+
};
|
|
18
|
+
const properties = [
|
|
19
|
+
"--pb-sticky-top",
|
|
20
|
+
"--pb-sticky-bottom",
|
|
21
|
+
"scroll-padding-top",
|
|
22
|
+
"scroll-padding-bottom"
|
|
23
|
+
];
|
|
24
|
+
/**
|
|
25
|
+
* While the page scrolls, says how tall its sticky bars are, on the root element: as `--pb-sticky-top` and
|
|
26
|
+
* `--pb-sticky-bottom` for content that sticks inside them, and as the page's scroll padding, which the browser keeps
|
|
27
|
+
* focused and scrolled-to content clear of, and which a virtual list reads as the part of the page it shows in.
|
|
28
|
+
*/
|
|
29
|
+
const usePageInsets = (on, top, bottom, bottomShown) => {
|
|
30
|
+
useLayoutEffect(() => {
|
|
31
|
+
if (!on) return;
|
|
32
|
+
const root = document.documentElement;
|
|
33
|
+
const update = () => {
|
|
34
|
+
const above = `${top.current?.offsetHeight ?? 0}px`;
|
|
35
|
+
const below = `${bottom.current?.offsetHeight ?? 0}px`;
|
|
36
|
+
root.style.setProperty("--pb-sticky-top", above);
|
|
37
|
+
root.style.setProperty("--pb-sticky-bottom", below);
|
|
38
|
+
root.style.setProperty("scroll-padding-top", above);
|
|
39
|
+
root.style.setProperty("scroll-padding-bottom", below);
|
|
40
|
+
};
|
|
41
|
+
const observer = new ResizeObserver(update);
|
|
42
|
+
for (const bar of [top.current, bottom.current]) if (bar) observer.observe(bar);
|
|
43
|
+
update();
|
|
44
|
+
return () => {
|
|
45
|
+
observer.disconnect();
|
|
46
|
+
for (const property of properties) root.style.removeProperty(property);
|
|
47
|
+
};
|
|
48
|
+
}, [
|
|
49
|
+
on,
|
|
50
|
+
top,
|
|
51
|
+
bottom,
|
|
52
|
+
bottomShown
|
|
53
|
+
]);
|
|
54
|
+
};
|
|
55
|
+
//#endregion
|
|
56
|
+
export { PageScrollContext, usePageInsets, usePageScroll };
|
|
@@ -8,13 +8,13 @@ import { SidebarGroup } from "./sidebar-group.js";
|
|
|
8
8
|
import { LogIn } from "lucide-react";
|
|
9
9
|
import { jsx, jsxs } from "react/jsx-runtime";
|
|
10
10
|
//#region src/app-shell/sidebar.tsx
|
|
11
|
-
const Sidebar = ({ mode, activeItem, user, workspace, forceExpanded, profileMenuOpen, groups = navGroups, userMenu, onNavigate, onSignOut, onSignIn, organizations, roles }) => {
|
|
11
|
+
const Sidebar = ({ mode, activeItem, user, workspace, forceExpanded, profileMenuOpen, groups = navGroups, userMenu, onNavigate, onSignOut, onSignIn, organizations, roles, sticky }) => {
|
|
12
12
|
const expanding = mode === "icon-expand";
|
|
13
13
|
const expanded = expanding && forceExpanded;
|
|
14
14
|
const inline = isTextMode(mode) || expanding;
|
|
15
15
|
return /* @__PURE__ */ jsx("nav", {
|
|
16
16
|
"aria-label": "Resources",
|
|
17
|
-
className: cn("relative z-30 shrink-0", sidebarWidth[mode]),
|
|
17
|
+
className: cn(sticky ? "sticky top-0 h-dvh self-start" : "relative", "z-30 shrink-0", sidebarWidth[mode]),
|
|
18
18
|
children: /* @__PURE__ */ jsxs("div", {
|
|
19
19
|
className: cn("group/side absolute inset-y-0 left-0 flex flex-col overflow-hidden border-r bg-sidebar", expanding ? "w-14 transition-[width,box-shadow] duration-150 hover:w-60 hover:shadow-pop focus-within:w-60 focus-within:shadow-pop" : "w-full", expanded && "w-60 shadow-pop"),
|
|
20
20
|
children: [
|
|
@@ -7,7 +7,7 @@ import { ChevronLeft, Menu, Search, X } from "lucide-react";
|
|
|
7
7
|
import { Fragment as Fragment$1, jsx, jsxs } from "react/jsx-runtime";
|
|
8
8
|
//#region src/app-shell/top-bar.tsx
|
|
9
9
|
const iconButton = "inline-flex size-11 shrink-0 items-center justify-center rounded-md text-muted-foreground hover:bg-muted md:hidden";
|
|
10
|
-
const TopBar = ({ breadcrumb, onNavigate, actions, onMenu, menuOpen, menuControls, search }) => {
|
|
10
|
+
const TopBar = ({ breadcrumb, onNavigate, actions, onMenu, menuOpen, menuControls, search, sticky, ref }) => {
|
|
11
11
|
const [searchOpen, setSearchOpen] = useState(false);
|
|
12
12
|
const back = backTarget(breadcrumb);
|
|
13
13
|
const desktopSearch = useRef(null);
|
|
@@ -24,7 +24,8 @@ const TopBar = ({ breadcrumb, onNavigate, actions, onMenu, menuOpen, menuControl
|
|
|
24
24
|
return () => window.removeEventListener("keydown", onKey);
|
|
25
25
|
}, [search]);
|
|
26
26
|
return /* @__PURE__ */ jsxs("header", {
|
|
27
|
-
|
|
27
|
+
ref,
|
|
28
|
+
className: cn(sticky ? "sticky top-0 z-30" : "relative", "flex h-12 shrink-0 items-center gap-1 border-b bg-background px-2 md:gap-4 md:px-5"),
|
|
28
29
|
children: [
|
|
29
30
|
onMenu && /* @__PURE__ */ jsx("button", {
|
|
30
31
|
type: "button",
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { useEffect, useState } from "react";
|
|
2
|
-
import { applyAssistantEvent, emptyAssistantState } from "@protobase/schema";
|
|
2
|
+
import { applyAssistantEvent, emptyAssistantState } from "@protobase/schema/internal";
|
|
3
3
|
//#region src/assistant-dock/use-assistant.ts
|
|
4
4
|
const messageOf = (error) => error instanceof Error ? error.message : "The assistant did not answer";
|
|
5
5
|
/** Follows an assistant backend's event stream into state, with its two requests; `undefined` client means not yet. */
|
package/dist/data/use-record.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { keys } from "./query-keys.js";
|
|
2
2
|
import { useClient } from "./api-provider.js";
|
|
3
3
|
import { useQuery } from "@tanstack/react-query";
|
|
4
|
-
import { encodeKey } from "@protobase/schema";
|
|
4
|
+
import { encodeKey } from "@protobase/schema/internal";
|
|
5
5
|
//#region src/data/use-record.ts
|
|
6
6
|
const useRecord = (resource, key, enabled = true) => {
|
|
7
7
|
const client = useClient();
|
|
@@ -2,7 +2,7 @@ import { keys } from "./query-keys.js";
|
|
|
2
2
|
import { useClient } from "./api-provider.js";
|
|
3
3
|
import { PreconditionFailedError } from "@protobase/client";
|
|
4
4
|
import { useMutation, useQueryClient } from "@tanstack/react-query";
|
|
5
|
-
import { encodeKey } from "@protobase/schema";
|
|
5
|
+
import { encodeKey } from "@protobase/schema/internal";
|
|
6
6
|
//#region src/data/use-update-record.ts
|
|
7
7
|
/**
|
|
8
8
|
* PATCH with `If-Match`. On success the cached record gets the new ETag and lists refetch;
|