@protobase/ui 0.51.3 → 0.52.1

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 (30) hide show
  1. package/dist/account/blocks/connected-apps-block.js +15 -3
  2. package/dist/account/connector-dialog.js +14 -0
  3. package/dist/account/mcp-address-section.js +45 -0
  4. package/dist/account/messages.js +10 -2
  5. package/dist/app/app.js +6 -16
  6. package/dist/auth/auth-layout.d.ts +4 -2
  7. package/dist/auth/auth-layout.js +7 -3
  8. package/dist/auth/auth-provider.js +2 -2
  9. package/dist/auth/sign-in-prompt.js +4 -4
  10. package/dist/connect/connect-page.js +38 -35
  11. package/dist/connect/connect-route.js +37 -0
  12. package/dist/notifications/bell/messages.js +0 -2
  13. package/dist/notifications/bell/notification-item.js +2 -10
  14. package/dist/notifications/bell/notification-target.js +1 -1
  15. package/dist/package.js +1 -1
  16. package/dist/shell/assets/{app-BiL2dM7x.js → app-DT9luXUb.js} +8 -8
  17. package/dist/shell/assets/index-CymocR26.css +2 -0
  18. package/dist/shell/assets/{index-DnaLvHu6.js → index-DnjfXnJH.js} +2 -2
  19. package/dist/shell/assets/{records-map-CvVnB3Ev.js → records-map-2ZUhf18Z.js} +2 -2
  20. package/dist/shell/assets/{shared-protobase-ui-CTj1OZ6i.js → shared-protobase-ui-4S2lZi18.js} +2 -2
  21. package/dist/shell/index.html +4 -4
  22. package/dist/shell/protobase.shell.json +1 -1
  23. package/dist/shell/protobase.sw.js +2 -2
  24. package/dist/shell-source-maps/assets/app-DT9luXUb.js.map +1 -0
  25. package/dist/shell-source-maps/assets/{index-DnaLvHu6.js.map → index-DnjfXnJH.js.map} +1 -1
  26. package/dist/shell-source-maps/assets/{records-map-CvVnB3Ev.js.map → records-map-2ZUhf18Z.js.map} +1 -1
  27. package/dist/shell-source-maps/assets/{shared-protobase-ui-CTj1OZ6i.js.map → shared-protobase-ui-4S2lZi18.js.map} +1 -1
  28. package/package.json +4 -4
  29. package/dist/shell/assets/index-DETGvbIP.css +0 -2
  30. package/dist/shell-source-maps/assets/app-BiL2dM7x.js.map +0 -1
@@ -1,10 +1,12 @@
1
1
  import { text } from "../messages.js";
2
2
  import { useAuth } from "../../auth/auth-provider.js";
3
+ import { useAdminMeta } from "../../app/meta-gate.js";
3
4
  import { AccountLoading } from "./account-loading.js";
4
5
  import { useAccountData } from "./use-account-data.js";
5
6
  import { ConnectedAppsSection } from "../connected-apps-section.js";
7
+ import { McpAddressSection } from "../mcp-address-section.js";
6
8
  import { useCallback } from "react";
7
- import { jsx } from "react/jsx-runtime";
9
+ import { Fragment as Fragment$1, jsx, jsxs } from "react/jsx-runtime";
8
10
  //#region src/account/blocks/connected-apps-block.tsx
9
11
  const Apps = () => {
10
12
  const { account } = useAuth().session;
@@ -15,7 +17,17 @@ const Apps = () => {
15
17
  onDisconnect: (clientId) => account.disconnectApp(clientId).then(reload)
16
18
  });
17
19
  };
18
- /** The `ConnectedApps` block: the apps the signed-in person allowed to act as them, hidden when the app has no MCP. */
19
- const ConnectedAppsBlock = () => useAuth().mcp ? /* @__PURE__ */ jsx(Apps, {}) : null;
20
+ const Mcp = ({ url }) => /* @__PURE__ */ jsxs(Fragment$1, { children: [/* @__PURE__ */ jsx(McpAddressSection, {
21
+ url,
22
+ appName: useAdminMeta().appName
23
+ }), /* @__PURE__ */ jsx(Apps, {})] });
24
+ /**
25
+ * The `ConnectedApps` block: the address of the app's MCP endpoint and the apps the signed-in person allowed to act as
26
+ * them, hidden when the app has no MCP.
27
+ */
28
+ const ConnectedAppsBlock = () => {
29
+ const { mcp } = useAuth();
30
+ return mcp ? /* @__PURE__ */ jsx(Mcp, { url: mcp.url }) : null;
31
+ };
20
32
  //#endregion
21
33
  export { ConnectedAppsBlock };
@@ -0,0 +1,14 @@
1
+ //#region src/account/connector-dialog.ts
2
+ /**
3
+ * The address of Claude's dialog that adds a custom connector, filled in with the MCP endpoint's `url` and, when the
4
+ * app has one, its name; the person sees both there and confirms.
5
+ */
6
+ const connectorDialogUrl = (url, appName) => {
7
+ const dialog = new URL("https://claude.ai/customize/connectors");
8
+ dialog.searchParams.set("modal", "add-custom-connector");
9
+ if (appName) dialog.searchParams.set("connectorName", appName);
10
+ dialog.searchParams.set("connectorUrl", url);
11
+ return dialog.href;
12
+ };
13
+ //#endregion
14
+ export { connectorDialogUrl };
@@ -0,0 +1,45 @@
1
+ import { text } from "./messages.js";
2
+ import { buttonClass } from "../primitives/button.js";
3
+ import { AccountSection } from "./account-section.js";
4
+ import { connectorDialogUrl } from "./connector-dialog.js";
5
+ import { useState } from "react";
6
+ import { Check, Copy, ExternalLink } from "lucide-react";
7
+ import { jsx, jsxs } from "react/jsx-runtime";
8
+ //#region src/account/mcp-address-section.tsx
9
+ /** The address of the app's MCP endpoint, to copy into an AI client or add to Claude, with how connecting goes. */
10
+ const McpAddressSection = ({ url, appName }) => {
11
+ const [copied, setCopied] = useState(false);
12
+ return /* @__PURE__ */ jsx(AccountSection, {
13
+ title: text("mcpAddress"),
14
+ description: text("mcpAddressDescription"),
15
+ children: /* @__PURE__ */ jsxs("div", {
16
+ className: "flex flex-wrap items-center gap-2",
17
+ children: [/* @__PURE__ */ jsxs("div", {
18
+ className: "flex min-w-0 flex-1 items-center gap-2 rounded-md border bg-surface py-1.5 pl-3 pr-1.5",
19
+ children: [/* @__PURE__ */ jsx("code", {
20
+ className: "min-w-0 flex-1 break-all font-mono text-xs",
21
+ children: url
22
+ }), /* @__PURE__ */ jsx("button", {
23
+ type: "button",
24
+ "aria-label": copied ? text("copied") : text("copyMcpAddress"),
25
+ onClick: () => {
26
+ navigator.clipboard.writeText(url).then(() => {
27
+ setCopied(true);
28
+ setTimeout(() => setCopied(false), 2e3);
29
+ });
30
+ },
31
+ className: "inline-flex size-8 shrink-0 items-center justify-center rounded text-muted-foreground hover:bg-muted pointer-coarse:size-11",
32
+ children: copied ? /* @__PURE__ */ jsx(Check, { className: "size-4 text-success" }) : /* @__PURE__ */ jsx(Copy, { className: "size-4" })
33
+ })]
34
+ }), /* @__PURE__ */ jsxs("a", {
35
+ href: connectorDialogUrl(url, appName),
36
+ target: "_blank",
37
+ rel: "noreferrer",
38
+ className: buttonClass(),
39
+ children: [/* @__PURE__ */ jsx(ExternalLink, { className: "size-3.5" }), text("addToClaude")]
40
+ })]
41
+ })
42
+ });
43
+ };
44
+ //#endregion
45
+ export { McpAddressSection };
@@ -149,7 +149,11 @@ const text = defineMessages({
149
149
  couldNotReadStaffSignIns: "Could not read the staff sign-ins",
150
150
  couldNotReadPolicy: "Could not read the sign-in policy",
151
151
  policySaved: "Saved. It applies to every sign-in from now on.",
152
- policySubtitle: "How people sign in to this app. Signed-in people keep their session, and are asked for what is newly required within 15 minutes."
152
+ policySubtitle: "How people sign in to this app. Signed-in people keep their session, and are asked for what is newly required within 15 minutes.",
153
+ mcpAddress: "MCP address",
154
+ mcpAddressDescription: "Add this address to an AI assistant or code editor that speaks MCP, as a custom connector or remote MCP server. It opens this app in your browser, where you sign in and allow it; it then works with your access.",
155
+ copyMcpAddress: "Copy the MCP address",
156
+ addToClaude: "Add to Claude"
153
157
  },
154
158
  nl: {
155
159
  account: "Account",
@@ -299,7 +303,11 @@ const text = defineMessages({
299
303
  couldNotReadStaffSignIns: "Kon de inlogs van medewerkers niet lezen",
300
304
  couldNotReadPolicy: "Kon het inlogbeleid niet lezen",
301
305
  policySaved: "Bewaard. Het geldt vanaf nu voor elke inlog.",
302
- policySubtitle: "Hoe mensen inloggen bij deze app. Wie is ingelogd, houdt de sessie en krijgt binnen 15 minuten de vraag naar wat nieuw verplicht is."
306
+ policySubtitle: "Hoe mensen inloggen bij deze app. Wie is ingelogd, houdt de sessie en krijgt binnen 15 minuten de vraag naar wat nieuw verplicht is.",
307
+ mcpAddress: "MCP-adres",
308
+ mcpAddressDescription: "Voeg dit adres toe aan een AI-assistent of code-editor die MCP spreekt, als aangepaste connector of externe MCP-server. Die opent deze app in je browser, waar je inlogt en hem toestaat; daarna werkt hij met jouw toegang.",
309
+ copyMcpAddress: "Het MCP-adres kopiëren",
310
+ addToClaude: "Toevoegen aan Claude"
303
311
  }
304
312
  });
305
313
  //#endregion
package/dist/app/app.js CHANGED
@@ -18,10 +18,10 @@ import { CustomBoundary } from "./pages/custom-boundary.js";
18
18
  import { useSidebarNoticeShown } from "../app-shell/sidebar-notice.js";
19
19
  import { AppShell } from "../app-shell/app-shell.js";
20
20
  import { useSignInAddress } from "../auth/use-sign-in-address.js";
21
- import { SignInPrompt } from "../auth/sign-in-prompt.js";
22
21
  import { managesOrganization, organizationMenuItems, organizationPageTitle } from "../organizations/organization-paths.js";
23
22
  import { AccountPage, useAccountPage } from "../account/account-page.js";
24
- import { ConnectPage } from "../connect/connect-page.js";
23
+ import { documentTitle, titleParts, useDocumentTitle } from "./document-title.js";
24
+ import { ConnectRoute } from "../connect/connect-route.js";
25
25
  import { SignInPolicyPage } from "../account/sign-in-policy-page.js";
26
26
  import { PluginSourceProvider } from "../plugins/plugin-source.js";
27
27
  import { AuthGate } from "../auth/auth-gate.js";
@@ -29,7 +29,6 @@ import { isAdmin, userToShell } from "../auth/user-to-shell.js";
29
29
  import { AuthLocale } from "../auth/auth-locale.js";
30
30
  import { useLiveUpdates } from "../data/use-live-updates.js";
31
31
  import { useOpenedRecord } from "../data/use-record.js";
32
- import { documentTitle, titleParts, useDocumentTitle } from "./document-title.js";
33
32
  import { PublicFormPage } from "../forms/public-form-page.js";
34
33
  import { useFormAddress } from "../forms/use-form-address.js";
35
34
  import { ThemeProvider, useTheme } from "../theme/theme-provider.js";
@@ -73,10 +72,6 @@ const placeOf = (address) => {
73
72
  return address?.resource;
74
73
  };
75
74
  const recordOf = (address) => address?.kind === "record" ? address : void 0;
76
- const connectPrompt = {
77
- title: "Sign in to connect an app",
78
- description: "An app asks to act as you here. Sign in, then choose whether to allow it."
79
- };
80
75
  const Routes = ({ workspace, sidebarMode, assistant, runtime }) => {
81
76
  const meta = useAdminMeta();
82
77
  const { state, session, signOut, stopStaffSession, organizations: organizationSettings } = useAuth();
@@ -151,7 +146,7 @@ const Routes = ({ workspace, sidebarMode, assistant, runtime }) => {
151
146
  const account = useAccountPage();
152
147
  const accountAddress = user ? readAccountPath(path) : void 0;
153
148
  const accountTab = accountAddress && account ? shownPageTab(pageTabs(account.tree), accountAddress.tab) : void 0;
154
- const accountPage = path === connectRoute ? "Connect an app" : !user ? void 0 : accountAddress ? account?.title ?? text("account") : path === "/-/sign-in-policy" ? text("signInPolicy") : organizationPage;
149
+ const accountPage = !user ? void 0 : accountAddress ? account?.title ?? text("account") : path === "/-/sign-in-policy" ? text("signInPolicy") : organizationPage;
155
150
  const resourceLabel = view?.names?.plural ?? (resource && humanize(resource));
156
151
  const breadcrumb = accountPage ? [accountPage] : breadcrumbFor({
157
152
  basePath,
@@ -180,13 +175,7 @@ const Routes = ({ workspace, sidebarMode, assistant, runtime }) => {
180
175
  recordTitle: title,
181
176
  recordLoading: titleLoading
182
177
  }), meta.appName));
183
- const content = path === connectRoute ? user ? /* @__PURE__ */ jsx(ConnectPage, {
184
- email: user.email,
185
- appName: meta.appName ?? "this app"
186
- }) : /* @__PURE__ */ jsx(SignInPrompt, {
187
- as: "page",
188
- prompt: connectPrompt
189
- }) : accountAddress ? /* @__PURE__ */ jsx(AccountPage, { ...accountAddress }) : user && path === "/-/sign-in-policy" ? admin ? /* @__PURE__ */ jsx(SignInPolicyPage, {}) : /* @__PURE__ */ jsx(Notice, {
178
+ const content = accountAddress ? /* @__PURE__ */ jsx(AccountPage, { ...accountAddress }) : user && path === "/-/sign-in-policy" ? admin ? /* @__PURE__ */ jsx(SignInPolicyPage, {}) : /* @__PURE__ */ jsx(Notice, {
190
179
  title: text("adminsOnly"),
191
180
  children: text("onlyAdminPolicy")
192
181
  }) : organizationPage && path === "/-/organization" ? /* @__PURE__ */ jsx(OrganizationPage, {
@@ -294,13 +283,14 @@ const Routes = ({ workspace, sidebarMode, assistant, runtime }) => {
294
283
  const Authenticated = ({ baseUrl, client, sources, workspace, sidebarMode, ui, assistant, runtime }) => {
295
284
  const { state, session, markSignedOut } = useAuth();
296
285
  const sidebarBanner = useSidebarNoticeShown(sidebarMode ?? "text-small");
286
+ const { path } = useRouter();
297
287
  return /* @__PURE__ */ jsx(ApiProvider, {
298
288
  baseUrl,
299
289
  client,
300
290
  sources,
301
291
  token: state.kind === "visitor" ? void 0 : session.token,
302
292
  onUnauthenticated: markSignedOut,
303
- children: /* @__PURE__ */ jsx(MetaGate, { children: /* @__PURE__ */ jsxs(ToastProvider, { children: [!sidebarBanner && /* @__PURE__ */ jsx(NewVersionToast, {}), /* @__PURE__ */ jsx(PluginGate, {
293
+ children: /* @__PURE__ */ jsx(MetaGate, { children: /* @__PURE__ */ jsxs(ToastProvider, { children: [!sidebarBanner && /* @__PURE__ */ jsx(NewVersionToast, {}), path === connectRoute ? /* @__PURE__ */ jsx(ConnectRoute, {}) : /* @__PURE__ */ jsx(PluginGate, {
304
294
  ui,
305
295
  children: /* @__PURE__ */ jsx(Routes, {
306
296
  workspace,
@@ -5,11 +5,13 @@ export type AuthLayoutProps = {
5
5
  description?: string;
6
6
  /** How wide the card is: `sm`, the default, for a few short inputs; `md` for a form with longer ones. */
7
7
  size?: 'sm' | 'md';
8
+ /** The path of the app's own icon, shown in place of Protobase's mark. */
9
+ icon?: string;
8
10
  children: ReactNode;
9
11
  };
10
12
  /**
11
- * The frame of the sign-in and first-run pages and of public forms: logo, title and a card. On a page as short as a
13
+ * The frame of the sign-in and first-run pages and of public forms: logo, or the app's own icon, title and a card. On a page as short as a
12
14
  * phone's above its keyboard the logo and most of the margins go, so the card still shows whole above the keyboard.
13
15
  */
14
- export declare const AuthLayout: ({ title, description, size, children }: AuthLayoutProps) => import("react").JSX.Element;
16
+ export declare const AuthLayout: ({ title, description, size, icon, children }: AuthLayoutProps) => import("react").JSX.Element;
15
17
  //#endregion
@@ -3,17 +3,21 @@ import { LogoMark } from "../app-shell/logo.js";
3
3
  import { jsx, jsxs } from "react/jsx-runtime";
4
4
  //#region src/auth/auth-layout.tsx
5
5
  /**
6
- * The frame of the sign-in and first-run pages and of public forms: logo, title and a card. On a page as short as a
6
+ * The frame of the sign-in and first-run pages and of public forms: logo, or the app's own icon, title and a card. On a page as short as a
7
7
  * phone's above its keyboard the logo and most of the margins go, so the card still shows whole above the keyboard.
8
8
  */
9
- const AuthLayout = ({ title, description, size = "sm", children }) => /* @__PURE__ */ jsx("div", {
9
+ const AuthLayout = ({ title, description, size = "sm", icon, children }) => /* @__PURE__ */ jsx("div", {
10
10
  className: "flex min-h-full w-full items-center justify-center bg-surface px-4 pb-[calc(2.5rem+var(--pb-safe-bottom))] pt-[calc(2.5rem+var(--pb-safe-top))] short:pb-[calc(1rem+var(--pb-safe-bottom))] short:pt-[calc(1rem+var(--pb-safe-top))]",
11
11
  children: /* @__PURE__ */ jsxs("div", {
12
12
  className: cn("w-full", size === "sm" ? "max-w-sm" : "max-w-lg"),
13
13
  children: [/* @__PURE__ */ jsxs("div", {
14
14
  className: "mb-6 flex flex-col items-center text-center short:mb-4",
15
15
  children: [
16
- /* @__PURE__ */ jsx(LogoMark, { className: "size-10 short:hidden" }),
16
+ icon ? /* @__PURE__ */ jsx("img", {
17
+ src: icon,
18
+ alt: "",
19
+ className: "size-10 object-contain short:hidden"
20
+ }) : /* @__PURE__ */ jsx(LogoMark, { className: "size-10 short:hidden" }),
17
21
  /* @__PURE__ */ jsx("h1", {
18
22
  className: "mt-4 text-xl font-semibold tracking-tight short:mt-0",
19
23
  children: title
@@ -25,7 +25,7 @@ const AuthProvider = ({ session: given, origin, nativeBridge, children }) => {
25
25
  const [signInFirst, setSignInFirst] = useState("password");
26
26
  const [passwordReset, setPasswordReset] = useState(false);
27
27
  const [changeEmail, setChangeEmail] = useState(false);
28
- const [mcp, setMcp] = useState(false);
28
+ const [mcp, setMcp] = useState();
29
29
  const [canSignUp, setCanSignUp] = useState(false);
30
30
  const [passkeyDomains, setPasskeyDomains] = useState([]);
31
31
  const [socialProviders, setSocialProviders] = useState([]);
@@ -110,7 +110,7 @@ const AuthProvider = ({ session: given, origin, nativeBridge, children }) => {
110
110
  setSignInFirst(status.signInFirst ?? "password");
111
111
  setPasswordReset(status.passwordReset && !nativeBridge);
112
112
  setChangeEmail(Boolean(status.changeEmail));
113
- setMcp(Boolean(status.mcp));
113
+ setMcp(status.mcp);
114
114
  setCanSignUp(Boolean(status.signUp));
115
115
  setPasskeyDomains(status.passkeyDomains ?? []);
116
116
  setSocialProviders(status.socialProviders);
@@ -19,7 +19,7 @@ const Benefits = ({ benefits }) => benefits && benefits.length > 0 ? /* @__PURE_
19
19
  }), benefit]
20
20
  }, benefit))
21
21
  }) : null;
22
- const Actions = ({ prompt }) => {
22
+ const SignInPromptActions = ({ prompt }) => {
23
23
  const { canSignUp } = useAuth();
24
24
  const { link } = useSignInAddress();
25
25
  const primary = buttonClass({ variant: "primary" });
@@ -57,7 +57,7 @@ const SignInPrompt = (props) => {
57
57
  actions: /* @__PURE__ */ jsxs(Fragment$1, { children: [/* @__PURE__ */ jsx(Button, {
58
58
  onClick: props.onClose,
59
59
  children: text("notNow")
60
- }), /* @__PURE__ */ jsx(Actions, { prompt })] }),
60
+ }), /* @__PURE__ */ jsx(SignInPromptActions, { prompt })] }),
61
61
  children: /* @__PURE__ */ jsx(Benefits, { benefits: prompt.benefits })
62
62
  });
63
63
  const Icon = namedIcon(prompt.icon) ?? LogIn;
@@ -86,10 +86,10 @@ const SignInPrompt = (props) => {
86
86
  /* @__PURE__ */ jsx(Benefits, { benefits: prompt.benefits }),
87
87
  /* @__PURE__ */ jsx("div", {
88
88
  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",
89
- children: /* @__PURE__ */ jsx(Actions, { prompt })
89
+ children: /* @__PURE__ */ jsx(SignInPromptActions, { prompt })
90
90
  })
91
91
  ]
92
92
  });
93
93
  };
94
94
  //#endregion
95
- export { SignInPrompt };
95
+ export { SignInPrompt, SignInPromptActions };
@@ -1,9 +1,9 @@
1
1
  import { useAuth } from "../auth/auth-provider.js";
2
2
  import { Spinner } from "../primitives/spinner.js";
3
- import { Notice } from "../app/meta-gate.js";
4
3
  import { Button } from "../primitives/button.js";
5
4
  import { useRouter } from "../app/router.js";
6
5
  import { authMessage } from "../auth/auth-messages.js";
6
+ import { AuthLayout } from "../auth/auth-layout.js";
7
7
  import { clientName } from "./client-name.js";
8
8
  import { clientPublisher } from "./client-publisher.js";
9
9
  import { isFramed, useArmedAfterFocus, useGuardedPress } from "./interaction-guard.js";
@@ -39,7 +39,15 @@ const TargetText = ({ target }) => target.kind === "site" ? /* @__PURE__ */ jsxs
39
39
  className: "break-all text-foreground",
40
40
  children: target.uri
41
41
  })] });
42
- const Request = ({ asking, appName, email, onAnswer, onSignOut }) => {
42
+ const Notice = ({ title, icon, children }) => /* @__PURE__ */ jsx(AuthLayout, {
43
+ title,
44
+ icon,
45
+ children: /* @__PURE__ */ jsx("p", {
46
+ className: "text-muted-foreground",
47
+ children
48
+ })
49
+ });
50
+ const Request = ({ asking, appName, appIcon, email, onAnswer, onSignOut }) => {
43
51
  const [busy, setBusy] = useState(false);
44
52
  const armed = useArmedAfterFocus(500);
45
53
  const answer = (accept) => {
@@ -50,38 +58,28 @@ const Request = ({ asking, appName, email, onAnswer, onSignOut }) => {
50
58
  const name = clientName(asking.client.client_name);
51
59
  const publisher = clientPublisher(asking.client.client_id);
52
60
  const { target } = asking;
53
- return /* @__PURE__ */ jsx("div", {
54
- className: "mx-auto flex w-full max-w-lg flex-col gap-4 px-4 py-10",
61
+ return /* @__PURE__ */ jsx(AuthLayout, {
62
+ title: `Allow an app to act as you in ${appName}?`,
63
+ icon: appIcon,
64
+ size: "md",
55
65
  children: /* @__PURE__ */ jsxs("section", {
56
66
  "aria-label": "Allow an app",
57
- className: "rounded-lg border bg-background p-5",
58
67
  children: [
59
- /* @__PURE__ */ jsxs("h1", {
60
- className: "text-[16px] font-semibold",
61
- children: [
62
- "Allow an app to act as you in ",
63
- appName,
64
- "?"
65
- ]
66
- }),
67
- /* @__PURE__ */ jsxs("p", {
68
- className: "mt-3",
69
- children: [
70
- "An app that calls itself",
71
- " ",
72
- /* @__PURE__ */ jsxs("bdi", {
73
- className: "inline-block max-w-full truncate align-bottom font-medium",
74
- title: name,
75
- children: [
76
- "“",
77
- name ?? "no name",
78
- "”"
79
- ]
80
- }),
81
- " ",
82
- "asks to act as you here."
83
- ]
84
- }),
68
+ /* @__PURE__ */ jsxs("p", { children: [
69
+ "An app that calls itself",
70
+ " ",
71
+ /* @__PURE__ */ jsxs("bdi", {
72
+ className: "inline-block max-w-full truncate align-bottom font-medium",
73
+ title: name,
74
+ children: [
75
+ "“",
76
+ name ?? "no name",
77
+ "”"
78
+ ]
79
+ }),
80
+ " ",
81
+ "asks to act as you here."
82
+ ] }),
85
83
  publisher && /* @__PURE__ */ jsxs("p", {
86
84
  className: "mt-1 text-muted-foreground",
87
85
  children: [
@@ -139,7 +137,7 @@ const Request = ({ asking, appName, email, onAnswer, onSignOut }) => {
139
137
  })
140
138
  });
141
139
  };
142
- const Connecting = ({ oauthQuery, appName, email, assign }) => {
140
+ const Connecting = ({ oauthQuery, appName, appIcon, email, assign }) => {
143
141
  const { session, signOut } = useAuth();
144
142
  const { path, basePath } = useRouter();
145
143
  const go = goWith(assign);
@@ -185,15 +183,17 @@ const Connecting = ({ oauthQuery, appName, email, assign }) => {
185
183
  };
186
184
  if (state.kind === "failed") return /* @__PURE__ */ jsx(Notice, {
187
185
  title: "This app cannot be connected",
186
+ icon: appIcon,
188
187
  children: state.message
189
188
  });
190
189
  if (state.kind === "loading") return /* @__PURE__ */ jsxs("div", {
191
- className: "flex min-h-48 items-center justify-center gap-2 text-muted-foreground",
190
+ className: "flex min-h-full w-full items-center justify-center gap-2 bg-surface text-muted-foreground",
192
191
  children: [/* @__PURE__ */ jsx(Spinner, {}), " Loading"]
193
192
  });
194
193
  return /* @__PURE__ */ jsx(Request, {
195
194
  asking: state.asking,
196
195
  appName,
196
+ appIcon,
197
197
  email,
198
198
  onAnswer: (accept) => answer(accept, state.asking),
199
199
  onSignOut: () => void signOut()
@@ -201,14 +201,15 @@ const Connecting = ({ oauthQuery, appName, email, assign }) => {
201
201
  };
202
202
  /**
203
203
  * The connect page: someone signed in allows an outside app, such as an MCP client, to act as them, from the request
204
- * the server signed and sent here. Inside another page's frame it shows only that it cannot be used there and calls
204
+ * the server signed and sent here. It stands alone on the page, as the sign-in pages do, without the app's shell. Inside another page's frame it shows only that it cannot be used there and calls
205
205
  * nothing, whatever headers the host sent; Allow takes a click only once the page has been in front for a moment.
206
206
  */
207
- const ConnectPage = ({ email, appName, isFramed: isFramed$1 = isFramed, assign = (url) => window.location.assign(url) }) => {
207
+ const ConnectPage = ({ email, appName, appIcon, isFramed: isFramed$1 = isFramed, assign = (url) => window.location.assign(url) }) => {
208
208
  const [framed] = useState(isFramed$1);
209
209
  const { oauthQuery } = useConnectAddress();
210
210
  if (framed) return /* @__PURE__ */ jsxs(Notice, {
211
211
  title: "This page cannot be used inside another site",
212
+ icon: appIcon,
212
213
  children: [
213
214
  /* @__PURE__ */ jsx("a", {
214
215
  href: window.location.href,
@@ -223,11 +224,13 @@ const ConnectPage = ({ email, appName, isFramed: isFramed$1 = isFramed, assign =
223
224
  });
224
225
  if (!oauthQuery) return /* @__PURE__ */ jsx(Notice, {
225
226
  title: "Nothing to connect",
227
+ icon: appIcon,
226
228
  children: "Connect again from your app."
227
229
  });
228
230
  return /* @__PURE__ */ jsx(Connecting, {
229
231
  oauthQuery,
230
232
  appName,
233
+ appIcon,
231
234
  email,
232
235
  assign
233
236
  }, oauthQuery);
@@ -0,0 +1,37 @@
1
+ import { useAuth } from "../auth/auth-provider.js";
2
+ import { useAdminMeta } from "../app/meta-gate.js";
3
+ import { SignInPromptActions } from "../auth/sign-in-prompt.js";
4
+ import { AuthLayout } from "../auth/auth-layout.js";
5
+ import { documentTitle, useDocumentTitle } from "../app/document-title.js";
6
+ import { ConnectPage } from "./connect-page.js";
7
+ import { jsx } from "react/jsx-runtime";
8
+ //#region src/connect/connect-route.tsx
9
+ const connectPrompt = {
10
+ title: "Sign in to connect an app",
11
+ description: "An app asks to act as you here. Sign in, then choose whether to allow it."
12
+ };
13
+ /**
14
+ * `/-/connect`, alone on the page without the app's shell, as the sign-in pages are: the connect page for someone
15
+ * signed in, else the way to sign in first. Both carry the app's name and icon.
16
+ */
17
+ const ConnectRoute = () => {
18
+ const meta = useAdminMeta();
19
+ const { state } = useAuth();
20
+ useDocumentTitle(documentTitle(["Connect an app"], meta.appName));
21
+ if (state.kind === "signed-in") return /* @__PURE__ */ jsx(ConnectPage, {
22
+ email: state.user.email,
23
+ appName: meta.appName ?? "this app",
24
+ appIcon: meta.appIcon
25
+ });
26
+ return /* @__PURE__ */ jsx(AuthLayout, {
27
+ title: connectPrompt.title,
28
+ description: connectPrompt.description,
29
+ icon: meta.appIcon,
30
+ children: /* @__PURE__ */ jsx("div", {
31
+ className: "flex flex-col gap-2 [&>*]:min-h-10 [&>*]:w-full",
32
+ children: /* @__PURE__ */ jsx(SignInPromptActions, { prompt: connectPrompt })
33
+ })
34
+ });
35
+ };
36
+ //#endregion
37
+ export { ConnectRoute };
@@ -11,7 +11,6 @@ const text = defineMessages({
11
11
  loadFailed: "Your notifications could not be loaded.",
12
12
  tryAgain: "Try again",
13
13
  unread: "Unread",
14
- opensNewTab: "(opens in a new tab)",
15
14
  pushOff: "Get notifications on this device, even when the app is closed.",
16
15
  turnOn: "Turn on",
17
16
  pushOn: "Notifications are on for this device.",
@@ -28,7 +27,6 @@ const text = defineMessages({
28
27
  loadFailed: "Je meldingen konden niet worden geladen.",
29
28
  tryAgain: "Opnieuw proberen",
30
29
  unread: "Ongelezen",
31
- opensNewTab: "(opent in een nieuw tabblad)",
32
30
  pushOff: "Ontvang meldingen op dit apparaat, ook als de app gesloten is.",
33
31
  turnOn: "Inschakelen",
34
32
  pushOn: "Meldingen staan op dit apparaat aan.",
@@ -8,8 +8,8 @@ import { Fragment, jsx, jsxs } from "react/jsx-runtime";
8
8
  const row = "flex w-full gap-2.5 px-3 py-2.5 text-left";
9
9
  /**
10
10
  * One notification of the panel: unread ones in bold beside a dot, its text, and how long ago it came. A notification
11
- * with a url is a link to it, followed in the app for the app's own pages and opened in a new tab for another site;
12
- * one without is not a link.
11
+ * with a url is a link to it, followed in the app for the app's own pages and loaded in this tab for any other; one
12
+ * without is not a link. The history never answers a url on another site.
13
13
  */
14
14
  const NotificationItem = ({ item, target, onNavigate, onOpen, now }) => {
15
15
  const unread = item.readAt === null;
@@ -34,10 +34,6 @@ const NotificationItem = ({ item, target, onNavigate, onOpen, now }) => {
34
34
  dateTime: item.createdAt,
35
35
  className: "mt-0.5 block text-xs text-faint-foreground",
36
36
  children: formatRelativeTime((now - Date.parse(item.createdAt)) / 1e3)
37
- }),
38
- target?.kind === "external" && /* @__PURE__ */ jsx("span", {
39
- className: "sr-only",
40
- children: text("opensNewTab")
41
37
  })
42
38
  ]
43
39
  })] });
@@ -64,10 +60,6 @@ const NotificationItem = ({ item, target, onNavigate, onOpen, now }) => {
64
60
  onClick: opened,
65
61
  onAuxClick: middle,
66
62
  className: cn(row, "hover:bg-muted"),
67
- ...target.kind === "external" && {
68
- target: "_blank",
69
- rel: "noopener"
70
- },
71
63
  children: content
72
64
  });
73
65
  };
@@ -5,7 +5,7 @@ const notificationTarget = (url, { origin, basePath }) => {
5
5
  const target = new URL(url, origin);
6
6
  if (target.protocol !== "http:" && target.protocol !== "https:") return void 0;
7
7
  if (target.origin !== origin) return {
8
- kind: "external",
8
+ kind: "page",
9
9
  href: target.href
10
10
  };
11
11
  const local = `${target.pathname}${target.search}${target.hash}`;
package/dist/package.js CHANGED
@@ -1,5 +1,5 @@
1
1
  //#region package.json
2
- var version = "0.51.3";
2
+ var version = "0.52.1";
3
3
  var protobase = { "pluginApi": 1 };
4
4
  //#endregion
5
5
  export { protobase, version };