@protobase/ui 0.14.0 → 0.14.2
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 +19 -1
- package/dist/app/displays/card-carousel.js +47 -39
- package/dist/app/displays/card-image.js +25 -0
- package/dist/app/displays/card-photos.js +11 -15
- package/dist/app/displays/cards-display.js +2 -1
- package/dist/app/displays/list-cards.js +2 -2
- package/dist/app/displays/list-displays.d.ts +2 -0
- package/dist/app/displays/list-displays.js +9 -3
- package/dist/app/displays/map-display.js +5 -4
- package/dist/app/displays/record-card.js +28 -34
- package/dist/app/displays/use-map-records.js +8 -6
- package/dist/app/displays/virtual-cards.js +43 -65
- package/dist/app/embedded-list/embedded-columns.js +1 -1
- package/dist/app/embedded-list/use-embedded-rows.js +11 -6
- package/dist/app/list-page.js +53 -13
- package/dist/app/menu-icons.js +3 -1
- package/dist/app/named-icon.js +5 -3
- package/dist/app/nav-from-meta.js +21 -1
- package/dist/app/pages/blocks/card-row.js +1 -1
- package/dist/app/pages/blocks/modal-form.js +1 -1
- package/dist/app/pages/blocks/record-card.js +1 -1
- package/dist/app/pages/blocks/show.js +1 -1
- package/dist/app/pages/blocks/stat.js +1 -1
- package/dist/app/pages/use-run-action.js +1 -1
- package/dist/app/saved/favorite-button.js +49 -0
- package/dist/app/saved/saved-list.js +43 -0
- package/dist/app/saved/saved-records.js +38 -0
- package/dist/app/saved/use-favorite.js +62 -0
- package/dist/app/saved/use-saved-records.js +36 -0
- package/dist/app/use-app-address.js +5 -2
- 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/auth/sign-in-prompt.d.ts +22 -0
- package/dist/auth/sign-in-prompt.js +84 -0
- package/dist/data/api-provider.d.ts +2 -0
- package/dist/data/meta-data.d.ts +3 -1
- package/dist/data/query-keys.js +0 -5
- package/dist/data/use-meta.js +1 -0
- 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/index.d.ts +3 -1
- package/dist/index.js +3 -1
- package/dist/live/grid-loader.js +8 -3
- package/dist/live/reference-options.js +2 -2
- package/dist/live/relation-labels.js +16 -18
- package/dist/live/row-photos.js +18 -0
- package/dist/live/use-filter-config.js +7 -2
- package/dist/package.js +1 -1
- package/dist/primitives/button.js +7 -2
- package/dist/primitives/dialog.js +1 -1
- package/dist/primitives/responsive-dialog.d.ts +17 -0
- package/dist/primitives/responsive-dialog.js +44 -0
- package/dist/shell/assets/app-B2GAOqtJ.js +66 -0
- package/dist/shell/assets/index-BXYHpFoe.css +2 -0
- package/dist/shell/assets/{index-CwUFCDf9.js → index-DTbA97zz.js} +1 -1
- package/dist/shell/assets/{maplibre-gl-QGc1hPKt.js → maplibre-gl-CSTJJJQT.js} +1 -1
- package/dist/shell/assets/{record-map-D5vqYcYw.js → record-map-CeTF-ArN.js} +2 -2
- package/dist/shell/assets/{records-map-DqqaK1rm.js → records-map-Cl0Ofb1s.js} +1 -1
- package/dist/shell/assets/shared-protobase-ui-4J-P80A-.js +1 -0
- 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-DtPlcCA3.js +0 -66
- package/dist/shell/assets/index-CYWfU_c1.css +0 -2
- package/dist/shell/assets/shared-protobase-ui-0ozDlcDr.js +0 -1
|
@@ -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",
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { SignInPromptModel } from "@protobase/schema";
|
|
2
|
+
//#region src/auth/sign-in-prompt.d.ts
|
|
3
|
+
export type SignInPromptProps = {
|
|
4
|
+
prompt: SignInPromptModel;
|
|
5
|
+
} & (
|
|
6
|
+
/** A page section, in place of what the visitor cannot use yet. */
|
|
7
|
+
{
|
|
8
|
+
as: 'page';
|
|
9
|
+
} |
|
|
10
|
+
/** A modal on a desktop and a sheet on a phone (`ResponsiveDialog`), after the visitor tried what needs an account. */
|
|
11
|
+
{
|
|
12
|
+
as: 'dialog';
|
|
13
|
+
open: boolean;
|
|
14
|
+
onClose: () => void;
|
|
15
|
+
});
|
|
16
|
+
/**
|
|
17
|
+
* What a visitor who is not signed in is told about something they could use once signed in: the prompt's title, what
|
|
18
|
+
* signing in gives and its benefits, with "Sign in" and, when the prompt names one, a page to make an account. As a page
|
|
19
|
+
* it stands in for a list the visitor cannot see; as a dialog it answers a press on a button that needs an account.
|
|
20
|
+
*/
|
|
21
|
+
export declare const SignInPrompt: (props: SignInPromptProps) => import("react").JSX.Element;
|
|
22
|
+
//#endregion
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import { useAuth } from "./auth-provider.js";
|
|
2
|
+
import { Button, buttonClass } from "../primitives/button.js";
|
|
3
|
+
import { namedIcon } from "../app/named-icon.js";
|
|
4
|
+
import { ResponsiveDialog } from "../primitives/responsive-dialog.js";
|
|
5
|
+
import { useId } from "react";
|
|
6
|
+
import { Check, LogIn } from "lucide-react";
|
|
7
|
+
import { Fragment as Fragment$1, jsx, jsxs } from "react/jsx-runtime";
|
|
8
|
+
//#region src/auth/sign-in-prompt.tsx
|
|
9
|
+
const Benefits = ({ benefits }) => benefits && benefits.length > 0 ? /* @__PURE__ */ jsx("ul", {
|
|
10
|
+
className: "flex flex-col gap-2 text-left text-[13px]",
|
|
11
|
+
children: benefits.map((benefit) => /* @__PURE__ */ jsxs("li", {
|
|
12
|
+
className: "flex items-start gap-2",
|
|
13
|
+
children: [/* @__PURE__ */ jsx(Check, {
|
|
14
|
+
className: "mt-0.5 size-4 shrink-0 text-primary",
|
|
15
|
+
"aria-hidden": true
|
|
16
|
+
}), benefit]
|
|
17
|
+
}, benefit))
|
|
18
|
+
}) : null;
|
|
19
|
+
const Actions = ({ prompt }) => {
|
|
20
|
+
const { showSignIn } = useAuth();
|
|
21
|
+
return /* @__PURE__ */ jsxs(Fragment$1, { children: [prompt.signUp && /* @__PURE__ */ jsx("a", {
|
|
22
|
+
href: prompt.signUp.href,
|
|
23
|
+
target: "_blank",
|
|
24
|
+
rel: "noreferrer",
|
|
25
|
+
className: buttonClass(),
|
|
26
|
+
children: prompt.signUp.label
|
|
27
|
+
}), /* @__PURE__ */ jsx(Button, {
|
|
28
|
+
variant: "primary",
|
|
29
|
+
onClick: showSignIn,
|
|
30
|
+
children: "Sign in"
|
|
31
|
+
})] });
|
|
32
|
+
};
|
|
33
|
+
/**
|
|
34
|
+
* What a visitor who is not signed in is told about something they could use once signed in: the prompt's title, what
|
|
35
|
+
* signing in gives and its benefits, with "Sign in" and, when the prompt names one, a page to make an account. As a page
|
|
36
|
+
* it stands in for a list the visitor cannot see; as a dialog it answers a press on a button that needs an account.
|
|
37
|
+
*/
|
|
38
|
+
const SignInPrompt = (props) => {
|
|
39
|
+
const { prompt } = props;
|
|
40
|
+
const heading = useId();
|
|
41
|
+
if (props.as === "dialog") return /* @__PURE__ */ jsx(ResponsiveDialog, {
|
|
42
|
+
title: prompt.title,
|
|
43
|
+
description: prompt.description,
|
|
44
|
+
open: props.open,
|
|
45
|
+
onClose: props.onClose,
|
|
46
|
+
actions: /* @__PURE__ */ jsxs(Fragment$1, { children: [/* @__PURE__ */ jsx(Button, {
|
|
47
|
+
onClick: props.onClose,
|
|
48
|
+
children: "Not now"
|
|
49
|
+
}), /* @__PURE__ */ jsx(Actions, { prompt })] }),
|
|
50
|
+
children: /* @__PURE__ */ jsx(Benefits, { benefits: prompt.benefits })
|
|
51
|
+
});
|
|
52
|
+
const Icon = namedIcon(prompt.icon) ?? LogIn;
|
|
53
|
+
return /* @__PURE__ */ jsxs("section", {
|
|
54
|
+
"aria-labelledby": heading,
|
|
55
|
+
className: "mx-auto flex w-full max-w-md flex-col items-center gap-4 px-5 py-10 text-center sm:py-16",
|
|
56
|
+
children: [
|
|
57
|
+
/* @__PURE__ */ jsx("span", {
|
|
58
|
+
className: "flex size-12 items-center justify-center rounded-full bg-primary/10 text-primary",
|
|
59
|
+
children: /* @__PURE__ */ jsx(Icon, {
|
|
60
|
+
className: "size-6",
|
|
61
|
+
"aria-hidden": true
|
|
62
|
+
})
|
|
63
|
+
}),
|
|
64
|
+
/* @__PURE__ */ jsxs("div", {
|
|
65
|
+
className: "flex flex-col gap-1.5",
|
|
66
|
+
children: [/* @__PURE__ */ jsx("h1", {
|
|
67
|
+
id: heading,
|
|
68
|
+
className: "text-lg font-semibold",
|
|
69
|
+
children: prompt.title
|
|
70
|
+
}), /* @__PURE__ */ jsx("p", {
|
|
71
|
+
className: "text-[13px] text-muted-foreground",
|
|
72
|
+
children: prompt.description
|
|
73
|
+
})]
|
|
74
|
+
}),
|
|
75
|
+
/* @__PURE__ */ jsx(Benefits, { benefits: prompt.benefits }),
|
|
76
|
+
/* @__PURE__ */ jsx("div", {
|
|
77
|
+
className: "mt-2 flex w-full flex-col-reverse gap-2 sm:w-auto sm:flex-row sm:justify-center [&>*]:min-h-11 sm:[&>*]:min-h-0",
|
|
78
|
+
children: /* @__PURE__ */ jsx(Actions, { prompt })
|
|
79
|
+
})
|
|
80
|
+
]
|
|
81
|
+
});
|
|
82
|
+
};
|
|
83
|
+
//#endregion
|
|
84
|
+
export { SignInPrompt };
|
|
@@ -20,6 +20,7 @@ export declare const useClient: () => {
|
|
|
20
20
|
facets: (resource: import("@protobase/client").ResourceRef, field: string, o?: {
|
|
21
21
|
filter?: import("@protobase/client").FilterInput;
|
|
22
22
|
limit?: number;
|
|
23
|
+
label?: string;
|
|
23
24
|
}) => Promise<import("@protobase/client").Facet[]>;
|
|
24
25
|
series: (resource: import("@protobase/client").ResourceRef, params: import("@protobase/client").SeriesParams & {
|
|
25
26
|
filter?: import("@protobase/client").FilterInput;
|
|
@@ -59,6 +60,7 @@ export declare const useResourceClient: (resource: string) => {
|
|
|
59
60
|
facets: (resource: import("@protobase/client").ResourceRef, field: string, o?: {
|
|
60
61
|
filter?: import("@protobase/client").FilterInput;
|
|
61
62
|
limit?: number;
|
|
63
|
+
label?: string;
|
|
62
64
|
}) => Promise<import("@protobase/client").Facet[]>;
|
|
63
65
|
series: (resource: import("@protobase/client").ResourceRef, params: import("@protobase/client").SeriesParams & {
|
|
64
66
|
filter?: import("@protobase/client").FilterInput;
|
package/dist/data/meta-data.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { PluginRef, ResourcePermissions, SourceRef } from "@protobase/client";
|
|
2
|
-
import { BottomBarModel, ResourceModel, UserMenuModel, ViewModel } from "@protobase/schema";
|
|
2
|
+
import { BottomBarModel, ResourceModel, SignInViewModel, UserMenuModel, ViewModel } from "@protobase/schema";
|
|
3
3
|
import { PageModel } from "@protobase/layout";
|
|
4
4
|
//#region src/data/meta-data.d.ts
|
|
5
5
|
export type MetaData = {
|
|
@@ -10,6 +10,8 @@ export type MetaData = {
|
|
|
10
10
|
pages: Record<string, PageModel>;
|
|
11
11
|
permissions: Record<string, ResourcePermissions>;
|
|
12
12
|
userMenu?: UserMenuModel;
|
|
13
|
+
/** For a visitor, the resources they could use once signed in, by name, with what to tell them (see `SignInPrompt`). */
|
|
14
|
+
signIn?: Record<string, SignInViewModel>;
|
|
13
15
|
/** The tabs of the user's bottom bar on phones, when one names their active role. */
|
|
14
16
|
bottomBar?: Omit<BottomBarModel, 'roles'>;
|
|
15
17
|
/** The assistant backend, when the app has one and the user has the admin or ai role. */
|
package/dist/data/query-keys.js
CHANGED
package/dist/data/use-meta.js
CHANGED
|
@@ -31,6 +31,7 @@ const useMeta = () => {
|
|
|
31
31
|
pages: Object.fromEntries((result.meta.pages ?? []).map((model) => [model.name, model])),
|
|
32
32
|
permissions: result.meta.permissions ?? {},
|
|
33
33
|
...result.meta.userMenu && { userMenu: result.meta.userMenu },
|
|
34
|
+
...result.meta.signIn && { signIn: Object.fromEntries(result.meta.signIn.map((entry) => [entry.resource, entry])) },
|
|
34
35
|
...result.meta.bottomBar && { bottomBar: result.meta.bottomBar },
|
|
35
36
|
...result.meta.assistant && { assistant: result.meta.assistant },
|
|
36
37
|
...result.meta.runtime && { runtime: result.meta.runtime },
|
|
@@ -15,7 +15,7 @@ import { Fragment as Fragment$1, jsx, jsxs } from "react/jsx-runtime";
|
|
|
15
15
|
import { useTable } from "@tanstack/react-table";
|
|
16
16
|
//#region src/data-grid/data-grid.tsx
|
|
17
17
|
const headerHeight = 32;
|
|
18
|
-
const DataGrid = ({ columns: given, source, naturalSort, initialScrollIndex = 0, onScrollIndex, latencyMs = 150, sort: controlledSort, onSortChange, onOpenRow, rowActions, bulkActions, pinKey, defaultPinned, rowNumbers = true, compact = false, selectable = true, fit = false, className }) => {
|
|
18
|
+
const DataGrid = ({ columns: given, source, naturalSort, initialScrollIndex = 0, onScrollIndex, latencyMs = 150, sort: controlledSort, onSortChange, onOpenRow, rowActions, bulkActions, pinKey, defaultPinned, rowNumbers = true, compact = false, selectable = true, fit = false, scroll = "own", className }) => {
|
|
19
19
|
const rowHeight = compact ? 28 : 36;
|
|
20
20
|
const [pinned, setPinned] = usePinnedColumns(pinKey, defaultPinned);
|
|
21
21
|
const columns = useMemo(() => {
|
|
@@ -50,16 +50,16 @@ const DataGrid = ({ columns: given, source, naturalSort, initialScrollIndex = 0,
|
|
|
50
50
|
reverse,
|
|
51
51
|
total
|
|
52
52
|
]);
|
|
53
|
-
const
|
|
53
|
+
const [startIndex] = useState(initialScrollIndex);
|
|
54
|
+
const { ref, top, geometry, rows: virtualRows, first: firstIndex, last: lastIndex } = useVirtualRows({
|
|
54
55
|
total,
|
|
55
56
|
rowHeight,
|
|
56
|
-
initialIndex:
|
|
57
|
+
initialIndex: startIndex,
|
|
57
58
|
inset: headerHeight
|
|
58
59
|
});
|
|
59
60
|
const narrow = !useIsDesktop();
|
|
60
61
|
const gutter = narrow || !rowNumbers ? 40 : gutterWidthFor(total);
|
|
61
|
-
const
|
|
62
|
-
const topIndex = Math.floor(top / rowHeight);
|
|
62
|
+
const topIndex = Math.floor(Math.max(0, top) / rowHeight);
|
|
63
63
|
useEffect(() => onScrollIndex?.(topIndex), [topIndex, onScrollIndex]);
|
|
64
64
|
const getRow = usePagedRows({
|
|
65
65
|
total,
|
|
@@ -99,9 +99,14 @@ const DataGrid = ({ columns: given, source, naturalSort, initialScrollIndex = 0,
|
|
|
99
99
|
const trailing = rowActions && rowActions.length > 0 ? 40 : 0;
|
|
100
100
|
const minScale = narrow ? 1 : desktopMinScale;
|
|
101
101
|
const horizontal = useHorizontalScroll();
|
|
102
|
+
const headerRef = useRef(null);
|
|
103
|
+
const onBodyScroll = () => {
|
|
104
|
+
horizontal.onScroll();
|
|
105
|
+
if (headerRef.current && horizontal.ref.current) headerRef.current.scrollLeft = horizontal.ref.current.scrollLeft;
|
|
106
|
+
};
|
|
102
107
|
const [pins, setPins] = useState({});
|
|
103
108
|
useLayoutEffect(() => {
|
|
104
|
-
const header =
|
|
109
|
+
const header = headerRef.current?.querySelector("[role=row]");
|
|
105
110
|
if (!header) return;
|
|
106
111
|
let left = gutter;
|
|
107
112
|
const next = {};
|
|
@@ -118,89 +123,91 @@ const DataGrid = ({ columns: given, source, naturalSort, initialScrollIndex = 0,
|
|
|
118
123
|
sorted: header.column.getIsSorted(),
|
|
119
124
|
onSort: (event) => header.column.getToggleSortingHandler()?.(event)
|
|
120
125
|
})));
|
|
121
|
-
const
|
|
122
|
-
const
|
|
123
|
-
const
|
|
124
|
-
|
|
125
|
-
className: cn("
|
|
126
|
-
|
|
127
|
-
|
|
126
|
+
const minWidth = gridMinWidth(columns, gutter, trailing, minScale);
|
|
127
|
+
const own = scroll === "own";
|
|
128
|
+
const header = /* @__PURE__ */ jsx("div", {
|
|
129
|
+
ref: headerRef,
|
|
130
|
+
className: cn("sticky z-20 shrink-0 overflow-hidden", own ? "top-0" : "top-[var(--pb-sticky-top,0px)]"),
|
|
131
|
+
children: /* @__PURE__ */ jsx("div", {
|
|
132
|
+
style: { minWidth },
|
|
133
|
+
children: /* @__PURE__ */ jsx(GridHeader, {
|
|
134
|
+
cells: headerCells,
|
|
135
|
+
specs: columns,
|
|
136
|
+
gutter,
|
|
137
|
+
trailing,
|
|
138
|
+
minScale,
|
|
139
|
+
pins,
|
|
140
|
+
lastPinned,
|
|
141
|
+
scrolled: horizontal.scrolled,
|
|
142
|
+
onTogglePin: togglePin,
|
|
143
|
+
selectable,
|
|
144
|
+
selectedCount,
|
|
145
|
+
onClearSelection: () => setRowSelection({})
|
|
146
|
+
})
|
|
147
|
+
})
|
|
148
|
+
});
|
|
149
|
+
const body = /* @__PURE__ */ jsx("div", {
|
|
150
|
+
ref: horizontal.attach,
|
|
151
|
+
onScroll: onBodyScroll,
|
|
152
|
+
className: "overflow-x-auto overflow-y-hidden overscroll-x-contain [-webkit-overflow-scrolling:touch]",
|
|
153
|
+
children: /* @__PURE__ */ jsx("div", {
|
|
128
154
|
ref,
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
offset,
|
|
166
|
-
height: rowHeight,
|
|
167
|
-
selectable,
|
|
168
|
-
trailing,
|
|
169
|
-
minScale,
|
|
170
|
-
scrolled: horizontal.scrolled,
|
|
171
|
-
pins,
|
|
172
|
-
lastPinned
|
|
173
|
-
};
|
|
174
|
-
const tableRow = row ? tableRows.get(row.id) : void 0;
|
|
175
|
-
if (!tableRow) return /* @__PURE__ */ jsx(SkeletonRow, { ...frame }, `skeleton-${index}`);
|
|
176
|
-
const actions = trailing && row ? availableActions(rowActions, row) : [];
|
|
177
|
-
return /* @__PURE__ */ jsx(DataRow, {
|
|
178
|
-
...frame,
|
|
179
|
-
selected: tableRow.getIsSelected(),
|
|
180
|
-
onToggle: () => tableRow.toggleSelected(),
|
|
181
|
-
onOpen: onOpenRow && row ? () => onOpenRow(row) : void 0,
|
|
182
|
-
menu: row && actions.length > 0 ? /* @__PURE__ */ jsx(RowMenu, {
|
|
183
|
-
row,
|
|
184
|
-
actions
|
|
185
|
-
}) : void 0,
|
|
186
|
-
cells: tableRow.getAllCells().map((cell) => ({
|
|
187
|
-
spec: columns.find((spec) => spec.id === cell.column.id),
|
|
188
|
-
content: /* @__PURE__ */ jsx(table.FlexRender, { cell })
|
|
189
|
-
}))
|
|
190
|
-
}, row?.id);
|
|
191
|
-
})
|
|
192
|
-
})]
|
|
193
|
-
})
|
|
194
|
-
}), horizontal.more && /* @__PURE__ */ jsx("div", {
|
|
195
|
-
"aria-hidden": true,
|
|
196
|
-
className: "pointer-events-none absolute inset-y-0 right-0 w-8 bg-gradient-to-l from-foreground/15 to-transparent"
|
|
197
|
-
})] })
|
|
198
|
-
})
|
|
155
|
+
className: "relative",
|
|
156
|
+
style: {
|
|
157
|
+
minWidth,
|
|
158
|
+
height: geometry.contentHeight
|
|
159
|
+
},
|
|
160
|
+
children: loaded.map(({ index, offset, row }) => {
|
|
161
|
+
const frame = {
|
|
162
|
+
specs: columns,
|
|
163
|
+
gutter,
|
|
164
|
+
index,
|
|
165
|
+
offset,
|
|
166
|
+
height: rowHeight,
|
|
167
|
+
selectable,
|
|
168
|
+
trailing,
|
|
169
|
+
minScale,
|
|
170
|
+
scrolled: horizontal.scrolled,
|
|
171
|
+
pins,
|
|
172
|
+
lastPinned
|
|
173
|
+
};
|
|
174
|
+
const tableRow = row ? tableRows.get(row.id) : void 0;
|
|
175
|
+
if (!tableRow) return /* @__PURE__ */ jsx(SkeletonRow, { ...frame }, `skeleton-${index}`);
|
|
176
|
+
const actions = trailing && row ? availableActions(rowActions, row) : [];
|
|
177
|
+
return /* @__PURE__ */ jsx(DataRow, {
|
|
178
|
+
...frame,
|
|
179
|
+
selected: tableRow.getIsSelected(),
|
|
180
|
+
onToggle: () => tableRow.toggleSelected(),
|
|
181
|
+
onOpen: onOpenRow && row ? () => onOpenRow(row) : void 0,
|
|
182
|
+
menu: row && actions.length > 0 ? /* @__PURE__ */ jsx(RowMenu, {
|
|
183
|
+
row,
|
|
184
|
+
actions
|
|
185
|
+
}) : void 0,
|
|
186
|
+
cells: tableRow.getAllCells().map((cell) => ({
|
|
187
|
+
spec: columns.find((spec) => spec.id === cell.column.id),
|
|
188
|
+
content: /* @__PURE__ */ jsx(table.FlexRender, { cell })
|
|
189
|
+
}))
|
|
190
|
+
}, row?.id);
|
|
199
191
|
})
|
|
192
|
+
})
|
|
193
|
+
});
|
|
194
|
+
return /* @__PURE__ */ jsxs("div", {
|
|
195
|
+
role: "grid",
|
|
196
|
+
"aria-rowcount": total,
|
|
197
|
+
className: cn("flex flex-col rounded-lg border bg-background", own ? "min-h-0 overflow-hidden" : "overflow-clip", className),
|
|
198
|
+
style: fit && own ? { height: headerHeight + total * rowHeight + 2 } : void 0,
|
|
199
|
+
children: [/* @__PURE__ */ jsxs("div", {
|
|
200
|
+
className: cn("relative", own && "flex min-h-0 flex-1 flex-col"),
|
|
201
|
+
children: [own ? /* @__PURE__ */ jsxs("div", {
|
|
202
|
+
className: "min-h-0 flex-1 overflow-y-auto overflow-x-hidden",
|
|
203
|
+
children: [header, body]
|
|
204
|
+
}) : /* @__PURE__ */ jsxs(Fragment$1, { children: [header, body] }), horizontal.more && /* @__PURE__ */ jsx("div", {
|
|
205
|
+
"aria-hidden": true,
|
|
206
|
+
className: "pointer-events-none absolute inset-y-0 right-0 z-20 w-8 bg-gradient-to-l from-foreground/15 to-transparent"
|
|
207
|
+
})]
|
|
200
208
|
}), !fit && /* @__PURE__ */ jsx(GridFooter, {
|
|
201
209
|
total,
|
|
202
|
-
|
|
203
|
-
last,
|
|
210
|
+
className: own ? void 0 : "sticky bottom-[var(--pb-sticky-bottom,0px)] z-20",
|
|
204
211
|
selectedCount,
|
|
205
212
|
bulkActions: bulkActions?.map((action) => ({
|
|
206
213
|
label: action.label(selectedCount),
|