@protobase/ui 0.40.7 → 0.40.8

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 (27) hide show
  1. package/dist/app/app.js +7 -2
  2. package/dist/app/new-version/messages.js +6 -2
  3. package/dist/app/new-version/new-version-banner.js +37 -0
  4. package/dist/app/pages/blocks/record-cards.js +14 -4
  5. package/dist/app/related-cards/related-cards.js +18 -13
  6. package/dist/app-shell/app-shell.js +6 -3
  7. package/dist/app-shell/sidebar-notice.js +12 -0
  8. package/dist/app-shell/sidebar.d.ts +1 -1
  9. package/dist/app-shell/sidebar.js +2 -1
  10. package/dist/package.js +1 -1
  11. package/dist/primitives/snap-row-width.js +13 -0
  12. package/dist/primitives/snap-row.js +21 -0
  13. package/dist/shell/assets/app-DXbtlR1O.js +9 -0
  14. package/dist/shell/assets/{index-BX850JXd.css → index-DT92y8N1.css} +1 -1
  15. package/dist/shell/assets/{index-B37wY6Go.js → index-hbOjGIrj.js} +2 -2
  16. package/dist/shell/assets/{records-map-DD0ZBUZe.js → records-map-DW4-2p9A.js} +2 -2
  17. package/dist/shell/assets/{shared-protobase-ui-pUw1X1dc.js → shared-protobase-ui-DOLAGMZi.js} +2 -2
  18. package/dist/shell/index.html +4 -4
  19. package/dist/shell/protobase.shell.json +1 -1
  20. package/dist/shell/protobase.sw.js +2 -2
  21. package/dist/shell-source-maps/assets/app-DXbtlR1O.js.map +1 -0
  22. package/dist/shell-source-maps/assets/{index-B37wY6Go.js.map → index-hbOjGIrj.js.map} +1 -1
  23. package/dist/shell-source-maps/assets/{records-map-DD0ZBUZe.js.map → records-map-DW4-2p9A.js.map} +1 -1
  24. package/dist/shell-source-maps/assets/{shared-protobase-ui-pUw1X1dc.js.map → shared-protobase-ui-DOLAGMZi.js.map} +1 -1
  25. package/package.json +4 -4
  26. package/dist/shell/assets/app-BpvlppLd.js +0 -9
  27. package/dist/shell-source-maps/assets/app-BpvlppLd.js.map +0 -1
package/dist/app/app.js CHANGED
@@ -1,5 +1,6 @@
1
1
  import { setFallbackLocale } from "../locale/active-locale.js";
2
2
  import { accountMenuItems } from "../account/account-pages.js";
3
+ import { useSidebarNoticeShown } from "../app-shell/sidebar-notice.js";
3
4
  import { AppShell } from "../app-shell/app-shell.js";
4
5
  import { AuthProvider, useAuth } from "../auth/auth-provider.js";
5
6
  import { AccountSecurityPage } from "../account/account-security-page.js";
@@ -52,6 +53,7 @@ import { PluginNotice } from "../plugins/plugin-notice.js";
52
53
  import { RecordPage } from "./record-page.js";
53
54
  import { SourceNotice } from "./source-notice.js";
54
55
  import { useGlobalSearch } from "./search/use-global-search.js";
56
+ import { NewVersionBanner } from "./new-version/new-version-banner.js";
55
57
  import { NewVersionButton } from "./new-version/new-version-button.js";
56
58
  import { NewVersionToast } from "./new-version/new-version-toast.js";
57
59
  import { RuntimeUpdateButton } from "./runtime/runtime-update.js";
@@ -142,7 +144,8 @@ const Routes = ({ workspace, sidebarMode, assistant, runtime }) => {
142
144
  url: runtimeUrl,
143
145
  client: runtime
144
146
  });
145
- const reloadButton = meta.versionChange && /* @__PURE__ */ jsx(NewVersionButton, {});
147
+ const sidebarBanner = useSidebarNoticeShown(sidebarMode ?? "text-small");
148
+ const reloadButton = meta.versionChange && !sidebarBanner && /* @__PURE__ */ jsx(NewVersionButton, {});
146
149
  const role = useActiveRole(useCallback(() => reopenIn(basePath), [basePath]), Boolean(user));
147
150
  const acting = user && actingUser(user, role.current);
148
151
  const admin = acting ? isAdmin(acting) : false;
@@ -244,6 +247,7 @@ const Routes = ({ workspace, sidebarMode, assistant, runtime }) => {
244
247
  onSwitch: role.switchTo
245
248
  } : void 0,
246
249
  onNavigate: open,
250
+ sidebarNotice: /* @__PURE__ */ jsx(NewVersionBanner, {}),
247
251
  search: search.available ? {
248
252
  placeholder: search.placeholder,
249
253
  text: search.text,
@@ -289,13 +293,14 @@ const Routes = ({ workspace, sidebarMode, assistant, runtime }) => {
289
293
  };
290
294
  const Authenticated = ({ baseUrl, client, sources, workspace, sidebarMode, ui, assistant, runtime }) => {
291
295
  const { state, session, markSignedOut } = useAuth();
296
+ const sidebarBanner = useSidebarNoticeShown(sidebarMode ?? "text-small");
292
297
  return /* @__PURE__ */ jsx(ApiProvider, {
293
298
  baseUrl,
294
299
  client,
295
300
  sources,
296
301
  token: state.kind === "visitor" ? void 0 : session.token,
297
302
  onUnauthenticated: markSignedOut,
298
- children: /* @__PURE__ */ jsx(MetaGate, { children: /* @__PURE__ */ jsxs(ToastProvider, { children: [/* @__PURE__ */ jsx(NewVersionToast, {}), /* @__PURE__ */ jsx(PluginGate, {
303
+ children: /* @__PURE__ */ jsx(MetaGate, { children: /* @__PURE__ */ jsxs(ToastProvider, { children: [!sidebarBanner && /* @__PURE__ */ jsx(NewVersionToast, {}), /* @__PURE__ */ jsx(PluginGate, {
299
304
  ui,
300
305
  children: /* @__PURE__ */ jsx(Routes, {
301
306
  workspace,
@@ -6,14 +6,18 @@ const text = defineMessages({
6
6
  description: "Reload the page to use the new version, when it suits you.",
7
7
  reload: "Reload",
8
8
  reloadPage: "Reload page",
9
- reloadPageHint: "Reload page to use the new version"
9
+ reloadPageHint: "Reload page to use the new version",
10
+ bannerTitle: "{} updated",
11
+ bannerLine: "Save your work, then click here to reload. You may see errors until you do."
10
12
  },
11
13
  nl: {
12
14
  title: "Deze app is bijgewerkt",
13
15
  description: "Laad de pagina opnieuw voor de nieuwe versie, wanneer het uitkomt.",
14
16
  reload: "Opnieuw laden",
15
17
  reloadPage: "Opnieuw laden",
16
- reloadPageHint: "Opnieuw laden om de nieuwe versie te gebruiken"
18
+ reloadPageHint: "Opnieuw laden om de nieuwe versie te gebruiken",
19
+ bannerTitle: "{} bijgewerkt",
20
+ bannerLine: "Sla je werk op en klik hier om te herladen. Tot dan kunnen er fouten optreden."
17
21
  }
18
22
  });
19
23
  //#endregion
@@ -0,0 +1,37 @@
1
+ import { useRouter } from "../router.js";
2
+ import { useAdminMeta } from "../meta-gate.js";
3
+ import { slotted } from "../../locale/slotted.js";
4
+ import { text } from "./messages.js";
5
+ import { RotateCw } from "lucide-react";
6
+ import { jsx, jsxs } from "react/jsx-runtime";
7
+ //#region src/app/new-version/new-version-banner.tsx
8
+ /**
9
+ * Across the bottom of a full-width sidebar, from the moment the page keeps its `/meta` for a new version until it
10
+ * reloads: the app's name, that it was updated, and a click that reloads, asking first while a guard holds unsaved
11
+ * changes. It stands in for `NewVersionToast` and `NewVersionButton`, which float over the page.
12
+ */
13
+ const NewVersionBanner = () => {
14
+ const { versionChange, appName } = useAdminMeta();
15
+ const { reload } = useRouter();
16
+ if (!versionChange) return null;
17
+ return /* @__PURE__ */ jsxs("button", {
18
+ type: "button",
19
+ onClick: reload,
20
+ className: "flex w-full shrink-0 items-start gap-2 border-t border-warning/40 bg-warning-soft px-3.5 py-2 text-left text-warning-text transition-colors hover:bg-warning/15 focus-visible:outline-2 focus-visible:-outline-offset-2 focus-visible:outline-primary",
21
+ children: [/* @__PURE__ */ jsx(RotateCw, {
22
+ "aria-hidden": true,
23
+ className: "mt-0.5 size-3.5 shrink-0"
24
+ }), /* @__PURE__ */ jsxs("span", {
25
+ className: "flex min-w-0 flex-col gap-0.5",
26
+ children: [/* @__PURE__ */ jsx("span", {
27
+ className: "break-words font-semibold",
28
+ children: appName ? slotted(text("bannerTitle"), appName) : text("title")
29
+ }), /* @__PURE__ */ jsx("span", {
30
+ className: "text-xs",
31
+ children: text("bannerLine")
32
+ })]
33
+ })]
34
+ });
35
+ };
36
+ //#endregion
37
+ export { NewVersionBanner };
@@ -4,13 +4,22 @@ import { listColumnNames } from "../../../live/columns-from-model.js";
4
4
  import { propsOf } from "./block-props.js";
5
5
  import { RelatedCards } from "../../related-cards/related-cards.js";
6
6
  import { jsx } from "react/jsx-runtime";
7
+ import { cardsLayoutOf } from "@protobase/layout";
7
8
  //#region src/app/pages/blocks/record-cards.tsx
9
+ const shares = {
10
+ "1/4": 1 / 4,
11
+ "1/3": 1 / 3,
12
+ "1/2": 1 / 2,
13
+ "2/3": 2 / 3
14
+ };
8
15
  /**
9
- * What a filter finds as the record page's related cards, stacked full width under a heading like a record page
10
- * section's, with no card frame around them, so no card sits in another.
16
+ * What a filter finds as the record page's related cards, stacked full width or in a row that scrolls sideways, under
17
+ * a heading like a record page section's, with no card frame around them, so no card sits in another.
11
18
  */
12
19
  const RecordCardsBlock = ({ node, slots }) => {
13
- const { resource, filter = "", sort = "", fields, cardActions = [], limit = 10, display, title, description, empty } = propsOf(node);
20
+ const props = propsOf(node);
21
+ const { resource, filter = "", sort = "", fields, cardActions = [], limit = 10, display, title, description, empty } = props;
22
+ const { layout, cardWidth } = cardsLayoutOf(props);
14
23
  const { resources, views } = useAdminMeta();
15
24
  const model = resources[resource];
16
25
  if (!model) return null;
@@ -31,7 +40,8 @@ const RecordCardsBlock = ({ node, slots }) => {
31
40
  actions: cardActions,
32
41
  limit,
33
42
  display,
34
- empty
43
+ empty,
44
+ carousel: layout === "carousel" ? shares[cardWidth] : void 0
35
45
  })
36
46
  })
37
47
  });
@@ -5,6 +5,7 @@ import { defaultOrderBy } from "../list-order.js";
5
5
  import { AppLink } from "../use-app-address.js";
6
6
  import { ErrorBanner } from "../error-banner.js";
7
7
  import { scopeOf, useCount, useListPage } from "../pages/use-layout-data.js";
8
+ import { SnapRow } from "../../primitives/snap-row.js";
8
9
  import { text } from "../embedded-list/messages.js";
9
10
  import { cutOffNote } from "../embedded-list/cut-off-note.js";
10
11
  import { embeddedDisplay, viewAllAddress } from "../embedded-list/embedded-display.js";
@@ -14,10 +15,10 @@ import { ArrowRight } from "lucide-react";
14
15
  import { jsx, jsxs } from "react/jsx-runtime";
15
16
  //#region src/app/related-cards/related-cards.tsx
16
17
  /**
17
- * The first `limit` records of a list as cards stacked full width, each with its fields and its actions' buttons, and a
18
- * link to the whole list beside a note such as "The first 10 of 37." when there are more.
18
+ * The first `limit` records of a list as cards stacked full width or in a `carousel`, each with its fields and its
19
+ * actions' buttons, and a link to the whole list beside a note such as "The first 10 of 37." when there are more.
19
20
  */
20
- const RelatedCards = ({ model, view, filter, sort, fields, actions, limit, display, empty }) => {
21
+ const RelatedCards = ({ model, view, filter, sort, fields, actions, limit, display, empty, carousel }) => {
21
22
  const xs = useIsXs();
22
23
  const orderBy = sort || defaultOrderBy(model, view);
23
24
  const page = useListPage(model, {
@@ -39,20 +40,24 @@ const RelatedCards = ({ model, view, filter, sort, fields, actions, limit, displ
39
40
  className: "text-muted-foreground",
40
41
  children: empty ?? text$1("nothingYet")
41
42
  });
43
+ const cards = rows.map((row) => {
44
+ const scope = scopeOf(model, row);
45
+ return /* @__PURE__ */ jsx(RelatedCard, {
46
+ scope,
47
+ view,
48
+ fields,
49
+ actions
50
+ }, scope.key);
51
+ });
42
52
  const note = count.data === void 0 ? void 0 : cutOffNote(rows.length, count.data, false);
43
53
  return /* @__PURE__ */ jsxs("div", {
44
54
  className: "flex flex-col gap-3",
45
- children: [/* @__PURE__ */ jsx("ul", {
55
+ children: [carousel === void 0 ? /* @__PURE__ */ jsx("ul", {
46
56
  className: "flex flex-col gap-3",
47
- children: rows.map((row) => {
48
- const scope = scopeOf(model, row);
49
- return /* @__PURE__ */ jsx(RelatedCard, {
50
- scope,
51
- view,
52
- fields,
53
- actions
54
- }, scope.key);
55
- })
57
+ children: cards
58
+ }) : /* @__PURE__ */ jsx(SnapRow, {
59
+ share: carousel,
60
+ children: cards
56
61
  }), /* @__PURE__ */ jsxs("div", {
57
62
  className: "flex flex-wrap items-center justify-between gap-2",
58
63
  children: [/* @__PURE__ */ jsx("p", {
@@ -4,14 +4,16 @@ import { BottomBar } from "./bottom-bar.js";
4
4
  import { Drawer } from "./drawer.js";
5
5
  import { PageScrollContext, usePageGutter, usePageInsets } from "./page-scroll.js";
6
6
  import { Sidebar } from "./sidebar.js";
7
+ import { useSidebarNoticeShown } from "./sidebar-notice.js";
7
8
  import { TopBarSlotContext } from "./top-bar-slot.js";
8
9
  import { TopBar } from "./top-bar.js";
9
10
  import { useEffect, useRef, useState } from "react";
10
11
  import { jsx, jsxs } from "react/jsx-runtime";
11
12
  //#region src/app-shell/app-shell.tsx
12
13
  const drawerId = "app-drawer";
13
- const AppShell = ({ sidebarMode, activeItem, breadcrumb, user, workspace, appName, appIcon, logo, actions, rightPanel, forceExpanded, profileMenuOpen, navGroups, userMenu, bottomBar, onNavigate, onSignOut, signIn, organizations, roles, theme, search, pageScroll: scrollsByDefault = false, className, children }) => {
14
+ const AppShell = ({ sidebarMode, activeItem, breadcrumb, user, workspace, appName, appIcon, logo, actions, rightPanel, sidebarNotice, forceExpanded, profileMenuOpen, navGroups, userMenu, bottomBar, onNavigate, onSignOut, signIn, organizations, roles, theme, search, pageScroll: scrollsByDefault = false, className, children }) => {
14
15
  const desktop = useIsDesktop();
16
+ const noticeShown = useSidebarNoticeShown(sidebarMode);
15
17
  const [drawerOpen, setDrawerOpen] = useState(false);
16
18
  useEffect(() => {
17
19
  if (desktop) setDrawerOpen(false);
@@ -24,9 +26,10 @@ const AppShell = ({ sidebarMode, activeItem, breadcrumb, user, workspace, appNam
24
26
  const bottomBarRef = useRef(null);
25
27
  usePageInsets(pageScroll, topBarRef, bottomBarRef, Boolean(tabs));
26
28
  usePageGutter(pageScroll);
27
- const sidebar = (mode, sticky = false) => /* @__PURE__ */ jsx(Sidebar, {
29
+ const sidebar = (mode, sticky = false, notice) => /* @__PURE__ */ jsx(Sidebar, {
28
30
  mode,
29
31
  sticky,
32
+ notice,
30
33
  activeItem,
31
34
  user,
32
35
  workspace,
@@ -49,7 +52,7 @@ const AppShell = ({ sidebarMode, activeItem, breadcrumb, user, workspace, appNam
49
52
  children: /* @__PURE__ */ jsxs("div", {
50
53
  className: cn(pageScroll ? "flex min-h-full w-full overflow-x-clip bg-background" : "relative flex h-full min-h-0 w-full overflow-hidden bg-background", className),
51
54
  children: [
52
- desktop ? sidebar(sidebarMode, pageScroll) : /* @__PURE__ */ jsx(Drawer, {
55
+ desktop ? sidebar(sidebarMode, pageScroll, noticeShown && sidebarNotice) : /* @__PURE__ */ jsx(Drawer, {
53
56
  id: drawerId,
54
57
  open: drawerOpen,
55
58
  onClose: () => setDrawerOpen(false),
@@ -0,0 +1,12 @@
1
+ import { useIsDesktop } from "../lib/use-media-query.js";
2
+ import { isTextMode } from "./sidebar-mode.js";
3
+ //#region src/app-shell/sidebar-notice.ts
4
+ /**
5
+ * Whether the shell draws its `sidebarNotice`: where the sidebar shows at full width, from the `md` breakpoint up in a
6
+ * text mode. Collapsed to icons, or a drawer on phones, it has no room, so the app says the same another way.
7
+ */
8
+ const useSidebarNoticeShown = (mode) => {
9
+ return useIsDesktop() && isTextMode(mode);
10
+ };
11
+ //#endregion
12
+ export { useSidebarNoticeShown };
@@ -1,4 +1,4 @@
1
1
  import "../primitives/route-link.js";
2
2
  import "./nav.js";
3
3
  import "./profile-menu.js";
4
- import { ComponentType } from "react";
4
+ import { ComponentType, ReactNode } from "react";
@@ -11,7 +11,7 @@ import { SidebarGroup } from "./sidebar-group.js";
11
11
  import { LogIn } from "lucide-react";
12
12
  import { Fragment, jsx, jsxs } from "react/jsx-runtime";
13
13
  //#region src/app-shell/sidebar.tsx
14
- const Sidebar = ({ mode, activeItem, user, workspace, appName, appIcon, forceExpanded, profileMenuOpen, groups = navGroups, userMenu, onNavigate, onSignOut, signIn, organizations, roles, theme, sticky, logo: SidebarLogo = Logo }) => {
14
+ const Sidebar = ({ mode, activeItem, user, workspace, appName, appIcon, forceExpanded, profileMenuOpen, groups = navGroups, userMenu, onNavigate, onSignOut, signIn, organizations, roles, theme, sticky, logo: SidebarLogo = Logo, notice }) => {
15
15
  const expanding = mode === "icon-expand";
16
16
  const expanded = expanding && forceExpanded;
17
17
  const inline = isTextMode(mode) || expanding;
@@ -51,6 +51,7 @@ const Sidebar = ({ mode, activeItem, user, workspace, appName, appIcon, forceExp
51
51
  }, item.id))]
52
52
  }, group.label))
53
53
  }),
54
+ notice,
54
55
  /* @__PURE__ */ jsx("div", {
55
56
  className: cn("shrink-0 border-t p-2", mode !== "text-large" && mode !== "text-small" && !expanding && "flex justify-center"),
56
57
  children: user ? /* @__PURE__ */ jsx(ProfileMenu, {
package/dist/package.js CHANGED
@@ -1,5 +1,5 @@
1
1
  //#region package.json
2
- var version = "0.40.7";
2
+ var version = "0.40.8";
3
3
  var protobase = { "pluginApi": 1 };
4
4
  //#endregion
5
5
  export { protobase, version };
@@ -0,0 +1,13 @@
1
+ //#region src/primitives/snap-row-width.ts
2
+ /** The space between the items of a `SnapRow`, Tailwind's `gap-3`. */
3
+ const snapRowGap = "0.75rem";
4
+ /**
5
+ * How wide an item of a row is at `share` of the row's width, the gaps taken off so that `1 / share` items fill it
6
+ * exactly: a third leaves room for three items and the two gaps between them.
7
+ */
8
+ const snapItemWidth = (share) => {
9
+ if (!(share > 0 && share <= 1)) throw new Error(`An item's share of a row is above 0 and at most 1, not ${share}`);
10
+ return `calc((100% + ${snapRowGap}) * ${share} - ${snapRowGap})`;
11
+ };
12
+ //#endregion
13
+ export { snapItemWidth, snapRowGap };
@@ -0,0 +1,21 @@
1
+ import { snapItemWidth, snapRowGap } from "./snap-row-width.js";
2
+ import { jsx } from "react/jsx-runtime";
3
+ //#region src/primitives/snap-row.tsx
4
+ /**
5
+ * A list laid out as one row that scrolls sideways, by touch, trackpad, wheel or its scrollbar, and snaps each item to
6
+ * its left edge. Each item is `share` of the row wide, but never narrower than 12rem or the row, so narrow shares stay
7
+ * readable on a phone; the items are top aligned. The row scrolls on its own because the page cannot scroll sideways,
8
+ * and only sideways: it is as tall as its tallest item. A swipe past its last item stops there.
9
+ */
10
+ const SnapRow = ({ share, label, children }) => /* @__PURE__ */ jsx("ul", {
11
+ "aria-label": label,
12
+ className: "flex snap-x snap-mandatory items-start overflow-x-auto overflow-y-hidden overscroll-x-contain pb-1 *:w-(--snap-item-width) *:min-w-[min(100%,12rem)] *:shrink-0 *:snap-start",
13
+ style: {
14
+ gap: snapRowGap,
15
+ scrollbarWidth: "thin",
16
+ "--snap-item-width": snapItemWidth(share)
17
+ },
18
+ children
19
+ });
20
+ //#endregion
21
+ export { SnapRow };