@protobase/ui 0.26.9 → 0.28.0

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/app.d.ts CHANGED
@@ -12,7 +12,7 @@ export type AppProps = {
12
12
  client?: Client;
13
13
  /** Ready-made clients of the sources `/meta` names, by their URL, for stories and tests. */
14
14
  sources?: Record<string, Client>;
15
- /** Sign-in state for the app; made from the page's origin when omitted. */
15
+ /** Sign-in state for the app; made from `baseUrl`'s origin when omitted. */
16
16
  auth?: AuthSession;
17
17
  /** Path prefix the app is mounted under, for example `/admin`. */
18
18
  basePath?: string;
package/dist/app/app.js CHANGED
@@ -201,6 +201,7 @@ const Routes = ({ workspace, sidebarMode, assistant, runtime }) => {
201
201
  signIn: user ? void 0 : signIn.link(),
202
202
  workspace: current?.organization?.name ?? workspace,
203
203
  appName: meta.appName,
204
+ appIcon: meta.appIcon,
204
205
  logo: ProjectLogo,
205
206
  navGroups: groups,
206
207
  bottomBar: bottomBarFromMeta(meta, basePath, place),
@@ -309,6 +310,7 @@ const App = ({ auth, workspace, basePath, initialUrl, pluginLoader, locale, ...r
309
310
  setFallbackLocale(locale ?? "en");
310
311
  return /* @__PURE__ */ jsx(ThemeProvider, { children: /* @__PURE__ */ jsx(AuthProvider, {
311
312
  session: auth,
313
+ ...rest.baseUrl && { origin: new URL(rest.baseUrl, window.location.href).origin },
312
314
  children: /* @__PURE__ */ jsx(Router, {
313
315
  basePath,
314
316
  initialUrl,
package/dist/app/main.js CHANGED
@@ -1,6 +1,7 @@
1
1
  import { browserLocale } from "../locale/active-locale.js";
2
2
  import { uiFromModule } from "./pages/ui-from-module.js";
3
3
  import { App } from "./app.js";
4
+ import { pageApiOrigin } from "./page-api-origin.js";
4
5
  import { StrictMode } from "react";
5
6
  import { jsx } from "react/jsx-runtime";
6
7
  import { createRoot } from "react-dom/client";
@@ -10,7 +11,7 @@ import "../styles.css";
10
11
  const root = document.getElementById("root");
11
12
  if (!root) throw new Error("index.html needs an element with id \"root\"");
12
13
  createRoot(root).render(/* @__PURE__ */ jsx(StrictMode, { children: /* @__PURE__ */ jsx(App, {
13
- baseUrl: "/api/v1",
14
+ baseUrl: `${pageApiOrigin() ?? ""}/api/v1`,
14
15
  ui: uiFromModule(projectUi),
15
16
  locale: browserLocale()
16
17
  }) }));
@@ -0,0 +1,18 @@
1
+ import { apiOriginMeta } from "@protobase/schema/internal";
2
+ //#region src/app/page-api-origin.ts
3
+ /**
4
+ * The API origin the page's `<meta name="protobase-api-origin">` tags name: `undefined` without one. Anything but one
5
+ * tag holding exactly an http or https origin, such as `https://shop.example.com`, is an error.
6
+ */
7
+ const apiOriginOf = (contents) => {
8
+ if (contents.length === 0) return void 0;
9
+ if (contents.length > 1) throw new Error(`The page has ${contents.length} ${apiOriginMeta} meta tags; it may have one`);
10
+ const value = contents[0];
11
+ const url = URL.canParse(value) ? new URL(value) : void 0;
12
+ if (!url || url.protocol !== "http:" && url.protocol !== "https:" || url.origin !== value) throw new Error(`The page's ${apiOriginMeta} meta tag holds "${value}", which is not an http or https origin such as https://shop.example.com`);
13
+ return value;
14
+ };
15
+ /** The API origin this page names in its `<meta name="protobase-api-origin">` (see `apiOriginOf`). */
16
+ const pageApiOrigin = () => apiOriginOf([...document.querySelectorAll(`meta[name="${apiOriginMeta}"]`)].map((meta) => meta.content));
17
+ //#endregion
18
+ export { apiOriginOf, pageApiOrigin };
@@ -10,7 +10,7 @@ import { useEffect, useRef, useState } from "react";
10
10
  import { jsx, jsxs } from "react/jsx-runtime";
11
11
  //#region src/app-shell/app-shell.tsx
12
12
  const drawerId = "app-drawer";
13
- const AppShell = ({ sidebarMode, activeItem, breadcrumb, user, workspace, appName, logo, actions, rightPanel, forceExpanded, profileMenuOpen, navGroups, userMenu, bottomBar, onNavigate, onSignOut, signIn, organizations, roles, theme, search, pageScroll: scrollsByDefault = false, className, children }) => {
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
14
  const desktop = useIsDesktop();
15
15
  const [drawerOpen, setDrawerOpen] = useState(false);
16
16
  useEffect(() => {
@@ -31,6 +31,7 @@ const AppShell = ({ sidebarMode, activeItem, breadcrumb, user, workspace, appNam
31
31
  user,
32
32
  workspace,
33
33
  appName,
34
+ appIcon,
34
35
  forceExpanded,
35
36
  profileMenuOpen,
36
37
  groups: navGroups,
@@ -4,12 +4,14 @@ export declare const LogoMark: ({ className }: {
4
4
  className?: string;
5
5
  }) => import("react").JSX.Element;
6
6
  /**
7
- * What the logo at the top of the sidebar gets: `compact` when the sidebar shows only icons, the workspace's name, and
8
- * the app's name, such as `Property Finder` (`PROTOBASE_APP_NAME` or `createAuth`'s `appName`).
7
+ * What the logo at the top of the sidebar gets: `compact` when the sidebar shows only icons, the workspace's name, the
8
+ * app's name, such as `Property Finder` (`PROTOBASE_APP_NAME` or `createAuth`'s `appName`), and the path of its own
9
+ * icon, its `protobase.favicon.svg`, when the project has one.
9
10
  */
10
11
  export type ShellLogoProps = {
11
12
  compact: boolean;
12
13
  workspace?: string;
13
14
  appName?: string;
15
+ appIcon?: string;
14
16
  };
15
17
  //#endregion
@@ -31,16 +31,21 @@ const Wordmark = ({ className }) => /* @__PURE__ */ jsx("svg", {
31
31
  });
32
32
  const brandName = "Protobase";
33
33
  /**
34
- * The logo at the top of the sidebar: Protobase's mark, then the app's name, as the brand's wordmark when the app goes by
35
- * Protobase's name, with the workspace's name under it; the mark alone when compact.
34
+ * The logo at the top of the sidebar: the app's own icon, else Protobase's mark, then the app's name, as the brand's
35
+ * wordmark when the app goes by Protobase's name and has no icon of its own, with the workspace's name under it; the
36
+ * icon alone when compact.
36
37
  */
37
- const Logo = ({ compact, workspace, appName }) => /* @__PURE__ */ jsxs("div", {
38
+ const Logo = ({ compact, workspace, appName, appIcon }) => /* @__PURE__ */ jsxs("div", {
38
39
  className: "flex items-center gap-2.5 overflow-hidden",
39
- children: [/* @__PURE__ */ jsx(LogoMark, {}), !compact && /* @__PURE__ */ jsxs("div", {
40
+ children: [appIcon ? /* @__PURE__ */ jsx("img", {
41
+ src: appIcon,
42
+ alt: "",
43
+ className: "size-7 shrink-0 object-contain"
44
+ }) : /* @__PURE__ */ jsx(LogoMark, {}), !compact && /* @__PURE__ */ jsxs("div", {
40
45
  className: "flex min-w-0 flex-col gap-0.5 leading-tight",
41
- children: [appName && appName !== brandName ? /* @__PURE__ */ jsx("div", {
46
+ children: [appIcon || appName && appName !== brandName ? /* @__PURE__ */ jsx("div", {
42
47
  className: "truncate text-sm font-semibold text-foreground",
43
- children: appName
48
+ children: appName ?? brandName
44
49
  }) : /* @__PURE__ */ jsx(Wordmark, { className: "self-start" }), workspace && /* @__PURE__ */ jsx("div", {
45
50
  className: "truncate text-[11px] text-muted-foreground",
46
51
  children: workspace
@@ -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, 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 }) => {
15
15
  const expanding = mode === "icon-expand";
16
16
  const expanded = expanding && forceExpanded;
17
17
  const inline = isTextMode(mode) || expanding;
@@ -26,7 +26,8 @@ const Sidebar = ({ mode, activeItem, user, workspace, appName, forceExpanded, pr
26
26
  children: /* @__PURE__ */ jsx(SidebarLogo, {
27
27
  compact: !inline,
28
28
  workspace,
29
- appName
29
+ appName,
30
+ appIcon
30
31
  })
31
32
  }),
32
33
  /* @__PURE__ */ jsx("div", {
@@ -10,8 +10,11 @@ const useAuth = () => {
10
10
  if (!api) throw new Error("useAuth must be used inside AuthProvider");
11
11
  return api;
12
12
  };
13
- /** Finds out whether the server has a login and who is signed in, and keeps that state for the app. */
14
- const AuthProvider = ({ session: given, children }) => {
13
+ /**
14
+ * 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
+ */
17
+ const AuthProvider = ({ session: given, origin, children }) => {
15
18
  const [state, setState] = useState({ kind: "loading" });
16
19
  const [signInMethods, setSignInMethods] = useState(["password"]);
17
20
  const [passwordReset, setPasswordReset] = useState(false);
@@ -29,7 +32,10 @@ const AuthProvider = ({ session: given, children }) => {
29
32
  const markSignedOut = useCallback(() => setState((current) => current.kind === "signed-in" ? { kind: "signed-out" } : current), []);
30
33
  const signedOut = useRef(markSignedOut);
31
34
  signedOut.current = markSignedOut;
32
- const session = useMemo(() => given ?? createAuthSession({ onSignedOut: () => signedOut.current() }), [given]);
35
+ const session = useMemo(() => given ?? createAuthSession({
36
+ ...origin && { origin },
37
+ onSignedOut: () => signedOut.current()
38
+ }), [given, origin]);
33
39
  const enter = useCallback(async (user) => {
34
40
  const [{ missing }, staff] = await Promise.all([session.account.signInMethods(), session.staff.current()]);
35
41
  setState(missing.length > 0 ? {
@@ -7,6 +7,8 @@ export type MetaData = {
7
7
  etag: string;
8
8
  /** The app's name, which ends the tab titles. */
9
9
  appName: string;
10
+ /** The path of the app's own icon, when the project has one, which the sidebar's logo draws. */
11
+ appIcon?: string;
10
12
  resources: Record<string, ResourceModel>;
11
13
  views: Record<string, ViewModel>;
12
14
  /** Composed pages by name. */
@@ -27,6 +27,7 @@ const useMeta = () => {
27
27
  return {
28
28
  etag: result.etag,
29
29
  appName: result.meta.appName,
30
+ ...result.meta.appIcon && { appIcon: result.meta.appIcon },
30
31
  resources: Object.fromEntries(result.meta.resources.map((model) => [model.name, model])),
31
32
  views: Object.fromEntries(result.meta.views.map((model) => [model.resource, model])),
32
33
  pages: Object.fromEntries((result.meta.pages ?? []).map((model) => [model.name, model])),
package/dist/package.js CHANGED
@@ -1,5 +1,5 @@
1
1
  //#region package.json
2
- var version = "0.26.9";
2
+ var version = "0.28.0";
3
3
  var protobase = { "pluginApi": 1 };
4
4
  //#endregion
5
5
  export { protobase, version };