@protobase/ui 0.40.7 → 0.40.9

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 (34) hide show
  1. package/dist/app/app.d.ts +4 -1
  2. package/dist/app/app.js +9 -3
  3. package/dist/app/main.js +3 -1
  4. package/dist/app/native-auth-fetch.d.ts +8 -0
  5. package/dist/app/native-auth-fetch.js +53 -0
  6. package/dist/app/new-version/messages.js +6 -2
  7. package/dist/app/new-version/new-version-banner.js +37 -0
  8. package/dist/app/pages/blocks/record-cards.js +14 -4
  9. package/dist/app/related-cards/related-cards.js +18 -13
  10. package/dist/app-shell/app-shell.js +6 -3
  11. package/dist/app-shell/sidebar-notice.js +12 -0
  12. package/dist/app-shell/sidebar.d.ts +1 -1
  13. package/dist/app-shell/sidebar.js +2 -1
  14. package/dist/auth/auth-provider.js +16 -5
  15. package/dist/package.js +1 -1
  16. package/dist/primitives/snap-row-width.js +13 -0
  17. package/dist/primitives/snap-row.js +21 -0
  18. package/dist/shell/assets/app-DA4sAj85.js +9 -0
  19. package/dist/shell/assets/index-DORKU14t.js +2 -0
  20. package/dist/shell/assets/{index-BX850JXd.css → index-DT92y8N1.css} +1 -1
  21. package/dist/shell/assets/{records-map-DD0ZBUZe.js → records-map-BWPZWDHO.js} +2 -2
  22. package/dist/shell/assets/{shared-protobase-ui-pUw1X1dc.js → shared-protobase-ui-KwzlPgvP.js} +2 -2
  23. package/dist/shell/index.html +4 -4
  24. package/dist/shell/protobase.shell.json +1 -1
  25. package/dist/shell/protobase.sw.js +2 -2
  26. package/dist/shell-source-maps/assets/app-DA4sAj85.js.map +1 -0
  27. package/dist/shell-source-maps/assets/index-DORKU14t.js.map +1 -0
  28. package/dist/shell-source-maps/assets/{records-map-DD0ZBUZe.js.map → records-map-BWPZWDHO.js.map} +1 -1
  29. package/dist/shell-source-maps/assets/{shared-protobase-ui-pUw1X1dc.js.map → shared-protobase-ui-KwzlPgvP.js.map} +1 -1
  30. package/package.json +4 -4
  31. package/dist/shell/assets/app-BpvlppLd.js +0 -9
  32. package/dist/shell/assets/index-B37wY6Go.js +0 -2
  33. package/dist/shell-source-maps/assets/app-BpvlppLd.js.map +0 -1
  34. package/dist/shell-source-maps/assets/index-B37wY6Go.js.map +0 -1
package/dist/app/app.d.ts CHANGED
@@ -2,6 +2,7 @@ import { SidebarMode } from "../app-shell/sidebar-mode.js";
2
2
  import "../app-shell/index.js";
3
3
  import { ProjectUi } from "./pages/project-ui.js";
4
4
  import { PluginLoader } from "../plugins/load-plugin.js";
5
+ import { NativeBridge } from "./native-auth-fetch.js";
5
6
  import { Locale } from "@protobase/schema/internal";
6
7
  import { AssistantClient, AuthSession, Client, RuntimeClient } from "@protobase/client";
7
8
  //#region src/app/app.d.ts
@@ -30,11 +31,13 @@ export type AppProps = {
30
31
  pluginLoader?: PluginLoader;
31
32
  /** The language the screens are in when the app's `/meta` sets none, such as the browser's (`browserLocale()`); English by default. */
32
33
  locale?: Locale;
34
+ /** The native bridge of the iOS or Android app (`nativeBridgeOf`): sign-in goes through native HTTP. */
35
+ nativeBridge?: NativeBridge;
33
36
  };
34
37
  /**
35
38
  * The admin: a public form at `/-/forms/<name>` for anyone, the sign-in route first, `/-/sign-in`, or browsing as a
36
39
  * visitor when the project's anonymous role reads, then a sidebar from `/meta`, `/:page` composed pages, `/:resource`
37
40
  * lists and `/:resource/:key` records. Fills its parent, in the theme this browser chose, light, dark or the system's.
38
41
  */
39
- export declare const App: ({ auth, workspace, basePath, initialUrl, pluginLoader, locale, ...rest }: AppProps) => import("react").JSX.Element;
42
+ export declare const App: ({ auth, workspace, basePath, initialUrl, pluginLoader, locale, nativeBridge, ...rest }: AppProps) => import("react").JSX.Element;
40
43
  //#endregion
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,
@@ -323,11 +328,12 @@ const PublicFormGate = ({ baseUrl, client, ui, children }) => {
323
328
  * visitor when the project's anonymous role reads, then a sidebar from `/meta`, `/:page` composed pages, `/:resource`
324
329
  * lists and `/:resource/:key` records. Fills its parent, in the theme this browser chose, light, dark or the system's.
325
330
  */
326
- const App = ({ auth, workspace, basePath, initialUrl, pluginLoader, locale, ...rest }) => {
331
+ const App = ({ auth, workspace, basePath, initialUrl, pluginLoader, locale, nativeBridge, ...rest }) => {
327
332
  setFallbackLocale(locale ?? "en");
328
333
  return /* @__PURE__ */ jsx(ThemeProvider, { children: /* @__PURE__ */ jsx(AuthProvider, {
329
334
  session: auth,
330
335
  ...rest.baseUrl && { origin: new URL(rest.baseUrl, window.location.href).origin },
336
+ ...nativeBridge && { nativeBridge },
331
337
  children: /* @__PURE__ */ jsx(Router, {
332
338
  basePath,
333
339
  initialUrl,
package/dist/app/main.js CHANGED
@@ -1,4 +1,5 @@
1
1
  import { browserLocale } from "../locale/active-locale.js";
2
+ import { nativeBridgeOf } from "./native-auth-fetch.js";
2
3
  import { uiFromModule } from "./pages/ui-from-module.js";
3
4
  import { App } from "./app.js";
4
5
  import { pageApiOrigin } from "./page-api-origin.js";
@@ -13,6 +14,7 @@ if (!root) throw new Error("index.html needs an element with id \"root\"");
13
14
  createRoot(root).render(/* @__PURE__ */ jsx(StrictMode, { children: /* @__PURE__ */ jsx(App, {
14
15
  baseUrl: `${pageApiOrigin() ?? ""}/api/v1`,
15
16
  ui: uiFromModule(projectUi),
16
- locale: browserLocale()
17
+ locale: browserLocale(),
18
+ nativeBridge: nativeBridgeOf(window)
17
19
  }) }));
18
20
  //#endregion
@@ -0,0 +1,8 @@
1
+ //#region src/app/native-auth-fetch.d.ts
2
+ /** What the iOS and Android app's native bridge, `window.Capacitor`, offers the page; the shell never imports Capacitor. */
3
+ export type NativeBridge = {
4
+ isNativePlatform(): boolean;
5
+ getPlatform(): string;
6
+ nativePromise(plugin: string, method: string, options: object): Promise<unknown>;
7
+ };
8
+ //#endregion
@@ -0,0 +1,53 @@
1
+ import { authBasePath, capacitorOrigin } from "@protobase/schema/internal";
2
+ //#region src/app/native-auth-fetch.ts
3
+ /** The native bridge of the page, when it runs in the iOS or Android app; `undefined` in a browser. */
4
+ const nativeBridgeOf = (win) => win.Capacitor?.isNativePlatform() ? win.Capacitor : void 0;
5
+ const nullBodyStatuses = [
6
+ 101,
7
+ 204,
8
+ 205,
9
+ 304
10
+ ];
11
+ const encodedSeparator = /%2f|%5c/i;
12
+ const isPlatform = (platform) => Object.hasOwn(capacitorOrigin, platform);
13
+ const allowedUrl = (url, apiOrigin) => url.origin === apiOrigin && !url.username && !url.password && url.pathname.startsWith(`${authBasePath}/`) && !encodedSeparator.test(url.pathname);
14
+ const bodyOf = ({ status, headers, data }) => {
15
+ if (nullBodyStatuses.includes(status)) return null;
16
+ if (Object.entries(headers).some(([name, value]) => name.toLowerCase() === "content-type" && value.startsWith("application/json"))) return JSON.stringify(data);
17
+ return typeof data === "string" ? data : JSON.stringify(data);
18
+ };
19
+ /**
20
+ * A `fetch` for the app's sign-in, at `<apiOrigin>/api/auth/...` only, through Capacitor's native HTTP: the session
21
+ * cookie lives in the device's cookie jar, which native HTTP sends and keeps, and the request names the Capacitor
22
+ * origin of the platform, which the server trusts for sign-in with a mobile section. The page cannot set `Cookie`, so
23
+ * the cookie comes from the native jar alone. Any other address is refused before the bridge is called.
24
+ */
25
+ const nativeAuthFetch = (bridge, apiOrigin) => {
26
+ const platform = bridge.getPlatform();
27
+ if (!isPlatform(platform)) throw new Error(`Signing in through native HTTP is for iOS and Android, not ${platform}`);
28
+ const origin = capacitorOrigin[platform];
29
+ return async (input, init) => {
30
+ const request = new Request(input, init);
31
+ const url = new URL(request.url);
32
+ if (!allowedUrl(url, apiOrigin)) throw new Error(`Native sign-in requests go to ${apiOrigin}${authBasePath}/ only, not ${request.url}`);
33
+ const withBody = request.method !== "GET" && request.method !== "HEAD";
34
+ const answer = await bridge.nativePromise("CapacitorHttp", "request", {
35
+ url: url.href,
36
+ method: request.method,
37
+ headers: {
38
+ ...Object.fromEntries(request.headers),
39
+ origin
40
+ },
41
+ ...withBody && { data: await request.text() },
42
+ responseType: "text",
43
+ disableRedirects: true
44
+ });
45
+ const headers = Object.entries(answer.headers).filter(([name]) => name.toLowerCase() !== "set-cookie");
46
+ return new Response(bodyOf(answer), {
47
+ status: answer.status,
48
+ headers
49
+ });
50
+ };
51
+ };
52
+ //#endregion
53
+ export { nativeAuthFetch, nativeBridgeOf };
@@ -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, {
@@ -1,3 +1,4 @@
1
+ import { nativeAuthFetch } from "../app/native-auth-fetch.js";
1
2
  import { providerName, withoutSignInLink } from "./provider-link.js";
2
3
  import { text } from "./messages.js";
3
4
  import { createContext, useCallback, useContext, useEffect, useMemo, useRef, useState } from "react";
@@ -12,9 +13,10 @@ const useAuth = () => {
12
13
  };
13
14
  /**
14
15
  * Finds out whether the server has a login and who is signed in, and keeps that state for the app. Without a `session`,
15
- * it makes one on `origin`, the page's own by default.
16
+ * it makes one on `origin`, the page's own by default; in the iOS or Android app, with its `nativeBridge`, the session
17
+ * signs in through native HTTP, and "Forgot password?" is not offered, as a reset link cannot come back to the app.
16
18
  */
17
- const AuthProvider = ({ session: given, origin, children }) => {
19
+ const AuthProvider = ({ session: given, origin, nativeBridge, children }) => {
18
20
  const [state, setState] = useState({ kind: "loading" });
19
21
  const [signInMethods, setSignInMethods] = useState(["password"]);
20
22
  const [passwordReset, setPasswordReset] = useState(false);
@@ -35,8 +37,13 @@ const AuthProvider = ({ session: given, origin, children }) => {
35
37
  signedOut.current = markSignedOut;
36
38
  const session = useMemo(() => given ?? createAuthSession({
37
39
  ...origin && { origin },
40
+ ...nativeBridge && { fetch: nativeAuthFetch(nativeBridge, origin ?? window.location.origin) },
38
41
  onSignedOut: () => signedOut.current()
39
- }), [given, origin]);
42
+ }), [
43
+ given,
44
+ origin,
45
+ nativeBridge
46
+ ]);
40
47
  const enter = useCallback(async (user) => {
41
48
  const [{ missing }, staff] = await Promise.all([session.account.signInMethods(), session.staff.current()]);
42
49
  setState(missing.length > 0 ? {
@@ -57,7 +64,7 @@ const AuthProvider = ({ session: given, origin, children }) => {
57
64
  setState({ kind: "loading" });
58
65
  session.status().then(async (status) => {
59
66
  setSignInMethods(status.signInMethods);
60
- setPasswordReset(status.passwordReset);
67
+ setPasswordReset(status.passwordReset && !nativeBridge);
61
68
  setMcp(Boolean(status.mcp));
62
69
  setCanSignUp(Boolean(status.signUp));
63
70
  setPasskeyDomains(status.passkeyDomains ?? []);
@@ -76,7 +83,11 @@ const AuthProvider = ({ session: given, origin, children }) => {
76
83
  kind: "error",
77
84
  message: error instanceof Error ? error.message : text("noAnswer")
78
85
  }));
79
- }, [session, enter]);
86
+ }, [
87
+ session,
88
+ enter,
89
+ nativeBridge
90
+ ]);
80
91
  useEffect(recheck, [recheck]);
81
92
  const api = useMemo(() => ({
82
93
  state,
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.9";
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 };