@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.
Files changed (75) hide show
  1. package/dist/app/current-address.js +19 -1
  2. package/dist/app/displays/card-carousel.js +47 -39
  3. package/dist/app/displays/card-image.js +25 -0
  4. package/dist/app/displays/card-photos.js +11 -15
  5. package/dist/app/displays/cards-display.js +2 -1
  6. package/dist/app/displays/list-cards.js +2 -2
  7. package/dist/app/displays/list-displays.d.ts +2 -0
  8. package/dist/app/displays/list-displays.js +9 -3
  9. package/dist/app/displays/map-display.js +5 -4
  10. package/dist/app/displays/record-card.js +28 -34
  11. package/dist/app/displays/use-map-records.js +8 -6
  12. package/dist/app/displays/virtual-cards.js +43 -65
  13. package/dist/app/embedded-list/embedded-columns.js +1 -1
  14. package/dist/app/embedded-list/use-embedded-rows.js +11 -6
  15. package/dist/app/list-page.js +53 -13
  16. package/dist/app/menu-icons.js +3 -1
  17. package/dist/app/named-icon.js +5 -3
  18. package/dist/app/nav-from-meta.js +21 -1
  19. package/dist/app/pages/blocks/card-row.js +1 -1
  20. package/dist/app/pages/blocks/modal-form.js +1 -1
  21. package/dist/app/pages/blocks/record-card.js +1 -1
  22. package/dist/app/pages/blocks/show.js +1 -1
  23. package/dist/app/pages/blocks/stat.js +1 -1
  24. package/dist/app/pages/use-run-action.js +1 -1
  25. package/dist/app/saved/favorite-button.js +49 -0
  26. package/dist/app/saved/saved-list.js +43 -0
  27. package/dist/app/saved/saved-records.js +38 -0
  28. package/dist/app/saved/use-favorite.js +62 -0
  29. package/dist/app/saved/use-saved-records.js +36 -0
  30. package/dist/app/use-app-address.js +5 -2
  31. package/dist/app-shell/app-shell.js +54 -38
  32. package/dist/app-shell/bottom-bar.d.ts +1 -0
  33. package/dist/app-shell/bottom-bar.js +3 -2
  34. package/dist/app-shell/index.d.ts +1 -0
  35. package/dist/app-shell/page-scroll.d.ts +1 -0
  36. package/dist/app-shell/page-scroll.js +56 -0
  37. package/dist/app-shell/sidebar.js +2 -2
  38. package/dist/app-shell/top-bar.js +3 -2
  39. package/dist/auth/sign-in-prompt.d.ts +22 -0
  40. package/dist/auth/sign-in-prompt.js +84 -0
  41. package/dist/data/api-provider.d.ts +2 -0
  42. package/dist/data/meta-data.d.ts +3 -1
  43. package/dist/data/query-keys.js +0 -5
  44. package/dist/data/use-meta.js +1 -0
  45. package/dist/data-grid/data-grid.js +92 -85
  46. package/dist/data-grid/geometry.js +26 -6
  47. package/dist/data-grid/grid-footer.js +5 -12
  48. package/dist/data-grid/scroll-parent.js +35 -0
  49. package/dist/data-grid/use-virtual-rows.js +123 -18
  50. package/dist/index.d.ts +3 -1
  51. package/dist/index.js +3 -1
  52. package/dist/live/grid-loader.js +8 -3
  53. package/dist/live/reference-options.js +2 -2
  54. package/dist/live/relation-labels.js +16 -18
  55. package/dist/live/row-photos.js +18 -0
  56. package/dist/live/use-filter-config.js +7 -2
  57. package/dist/package.js +1 -1
  58. package/dist/primitives/button.js +7 -2
  59. package/dist/primitives/dialog.js +1 -1
  60. package/dist/primitives/responsive-dialog.d.ts +17 -0
  61. package/dist/primitives/responsive-dialog.js +44 -0
  62. package/dist/shell/assets/app-B2GAOqtJ.js +66 -0
  63. package/dist/shell/assets/index-BXYHpFoe.css +2 -0
  64. package/dist/shell/assets/{index-CwUFCDf9.js → index-DTbA97zz.js} +1 -1
  65. package/dist/shell/assets/{maplibre-gl-QGc1hPKt.js → maplibre-gl-CSTJJJQT.js} +1 -1
  66. package/dist/shell/assets/{record-map-D5vqYcYw.js → record-map-CeTF-ArN.js} +2 -2
  67. package/dist/shell/assets/{records-map-DqqaK1rm.js → records-map-Cl0Ofb1s.js} +1 -1
  68. package/dist/shell/assets/shared-protobase-ui-4J-P80A-.js +1 -0
  69. package/dist/shell/index.html +4 -4
  70. package/dist/shell/protobase.shell.json +1 -1
  71. package/package.json +4 -4
  72. package/dist/data-grid/use-grid-scroll.js +0 -87
  73. package/dist/shell/assets/app-DtPlcCA3.js +0 -66
  74. package/dist/shell/assets/index-CYWfU_c1.css +0 -2
  75. 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 sidebar = (mode) => /* @__PURE__ */ jsx(Sidebar, {
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__ */ jsxs("div", {
34
- className: cn("flex h-full min-h-0 w-full overflow-hidden bg-background", className),
35
- children: [
36
- desktop ? sidebar(sidebarMode) : /* @__PURE__ */ jsx(Drawer, {
37
- id: drawerId,
38
- open: drawerOpen,
39
- onClose: () => setDrawerOpen(false),
40
- children: sidebar("text-large")
41
- }),
42
- /* @__PURE__ */ jsxs("div", {
43
- className: "flex min-w-0 flex-1 flex-col",
44
- children: [
45
- /* @__PURE__ */ jsx(TopBar, {
46
- breadcrumb,
47
- onNavigate,
48
- actions,
49
- onMenu: desktop || tabs ? void 0 : () => setDrawerOpen(true),
50
- menuOpen: drawerOpen,
51
- menuControls: drawerId,
52
- search
53
- }),
54
- /* @__PURE__ */ jsx("main", {
55
- className: "flex min-h-0 flex-1 flex-col",
56
- children
57
- }),
58
- tabs && /* @__PURE__ */ jsx(BottomBar, {
59
- tabs,
60
- onNavigate,
61
- onMore: () => setDrawerOpen(true),
62
- moreOpen: drawerOpen,
63
- moreControls: drawerId
64
- })
65
- ]
66
- }),
67
- rightPanel
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
@@ -1 +1,2 @@
1
+ import { Ref } from "react";
1
2
  import "lucide-react";
@@ -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", {
@@ -6,6 +6,7 @@ import "./global-search.js";
6
6
  import "./breadcrumb.js";
7
7
  import "./app-shell.js";
8
8
  import "./page-header.js";
9
+ import "./page-scroll.js";
9
10
  import "./sidebar.js";
10
11
  import "./status-dot.js";
11
12
  export type { SidebarMode };
@@ -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
- className: "relative flex h-12 shrink-0 items-center gap-1 border-b bg-background px-2 md:gap-4 md:px-5",
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;
@@ -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. */
@@ -101,11 +101,6 @@ const keys = {
101
101
  resource,
102
102
  key,
103
103
  path
104
- ],
105
- labelMap: (resource, ids) => [
106
- "label-map",
107
- resource,
108
- ids
109
104
  ]
110
105
  };
111
106
  //#endregion
@@ -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 { ref, onScroll, viewport, top, geometry, rows: virtualRows, first: firstIndex, last: lastIndex } = useVirtualRows({
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: initialScrollIndex,
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 bodyHeight = Math.max(0, viewport - headerHeight);
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 = horizontal.ref.current?.querySelector("[role=row]");
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 visibleRows = Math.max(1, Math.floor(bodyHeight / rowHeight));
122
- const first = Math.min(total, Math.floor(top / rowHeight) + 1);
123
- const last = Math.min(total, first + visibleRows - 1);
124
- return /* @__PURE__ */ jsxs("div", {
125
- className: cn("flex min-h-0 flex-col overflow-hidden rounded-lg border bg-background", className),
126
- style: fit ? { height: headerHeight + total * rowHeight + 2 } : void 0,
127
- children: [/* @__PURE__ */ jsx("div", {
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
- onScroll,
130
- className: "relative min-h-0 flex-1 overflow-y-auto overflow-x-hidden",
131
- role: "grid",
132
- "aria-rowcount": total,
133
- children: /* @__PURE__ */ jsx("div", {
134
- style: { height: geometry.contentHeight || void 0 },
135
- children: /* @__PURE__ */ jsx("div", {
136
- className: "sticky top-0",
137
- style: { height: viewport },
138
- children: viewport > 0 && /* @__PURE__ */ jsxs(Fragment$1, { children: [/* @__PURE__ */ jsx("div", {
139
- ref: horizontal.attach,
140
- onScroll: horizontal.onScroll,
141
- className: "h-full overflow-x-auto overflow-y-hidden overscroll-x-contain [-webkit-overflow-scrolling:touch]",
142
- children: /* @__PURE__ */ jsxs("div", {
143
- className: "flex h-full flex-col",
144
- style: { minWidth: gridMinWidth(columns, gutter, trailing, minScale) },
145
- children: [/* @__PURE__ */ jsx(GridHeader, {
146
- cells: headerCells,
147
- specs: columns,
148
- gutter,
149
- trailing,
150
- minScale,
151
- pins,
152
- lastPinned,
153
- scrolled: horizontal.scrolled,
154
- onTogglePin: togglePin,
155
- selectable,
156
- selectedCount,
157
- onClearSelection: () => setRowSelection({})
158
- }), /* @__PURE__ */ jsx("div", {
159
- className: "relative min-h-0 flex-1 overflow-clip",
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);
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
- first,
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),