@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.
- package/dist/app/app.d.ts +4 -1
- package/dist/app/app.js +9 -3
- package/dist/app/main.js +3 -1
- package/dist/app/native-auth-fetch.d.ts +8 -0
- package/dist/app/native-auth-fetch.js +53 -0
- package/dist/app/new-version/messages.js +6 -2
- package/dist/app/new-version/new-version-banner.js +37 -0
- package/dist/app/pages/blocks/record-cards.js +14 -4
- package/dist/app/related-cards/related-cards.js +18 -13
- package/dist/app-shell/app-shell.js +6 -3
- package/dist/app-shell/sidebar-notice.js +12 -0
- package/dist/app-shell/sidebar.d.ts +1 -1
- package/dist/app-shell/sidebar.js +2 -1
- package/dist/auth/auth-provider.js +16 -5
- package/dist/package.js +1 -1
- package/dist/primitives/snap-row-width.js +13 -0
- package/dist/primitives/snap-row.js +21 -0
- package/dist/shell/assets/app-DA4sAj85.js +9 -0
- package/dist/shell/assets/index-DORKU14t.js +2 -0
- package/dist/shell/assets/{index-BX850JXd.css → index-DT92y8N1.css} +1 -1
- package/dist/shell/assets/{records-map-DD0ZBUZe.js → records-map-BWPZWDHO.js} +2 -2
- package/dist/shell/assets/{shared-protobase-ui-pUw1X1dc.js → shared-protobase-ui-KwzlPgvP.js} +2 -2
- package/dist/shell/index.html +4 -4
- package/dist/shell/protobase.shell.json +1 -1
- package/dist/shell/protobase.sw.js +2 -2
- package/dist/shell-source-maps/assets/app-DA4sAj85.js.map +1 -0
- package/dist/shell-source-maps/assets/index-DORKU14t.js.map +1 -0
- package/dist/shell-source-maps/assets/{records-map-DD0ZBUZe.js.map → records-map-BWPZWDHO.js.map} +1 -1
- package/dist/shell-source-maps/assets/{shared-protobase-ui-pUw1X1dc.js.map → shared-protobase-ui-KwzlPgvP.js.map} +1 -1
- package/package.json +4 -4
- package/dist/shell/assets/app-BpvlppLd.js +0 -9
- package/dist/shell/assets/index-B37wY6Go.js +0 -2
- package/dist/shell-source-maps/assets/app-BpvlppLd.js.map +0 -1
- 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
|
|
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
|
|
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
|
|
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
|
|
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:
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
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 };
|
|
@@ -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
|
-
}), [
|
|
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
|
-
}, [
|
|
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
|
@@ -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 };
|