@rebasepro/app 0.13.1-canary.gf57a27e → 0.14.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.
@@ -0,0 +1,86 @@
1
+ /**
2
+ * The browser half of the OAuth authorization-code flow.
3
+ *
4
+ * The server only ever sees a `code` and a `redirectUri` taken from the
5
+ * request body, so `state`, PKCE and the binding between "this browser started
6
+ * a login" and "this code came back" are, by construction, the client's job.
7
+ * In the admin login they had been nobody's job: the authorize URL carried no
8
+ * `state`, and the return leg accepted any `?code=` as long as a localStorage
9
+ * marker naming the *provider* was set — a marker written on button click and
10
+ * removed only on a callback that carried a code, so abandoning the flow left
11
+ * it set indefinitely.
12
+ *
13
+ * Everything here is provider-agnostic on purpose. A button that wants a new
14
+ * provider calls {@link startOAuthRedirect} and gets state, expiry and PKCE
15
+ * without deciding anything, which is what stops the next provider shipping
16
+ * without them.
17
+ */
18
+ export interface OAuthRedirectRequest {
19
+ /** Provider id, as mounted at `POST /auth/<id>`. */
20
+ provider: string;
21
+ /** The provider's authorization endpoint. */
22
+ authorizeUrl: string;
23
+ clientId: string;
24
+ scope: string;
25
+ /** Where the provider sends the browser back. Echoed to the token exchange. */
26
+ redirectUri: string;
27
+ /** Set when the provider implements PKCE. */
28
+ pkce?: boolean;
29
+ /** Extra authorize-endpoint parameters (`response_type`, and so on). */
30
+ params?: Record<string, string>;
31
+ }
32
+ /** What the browser remembers between leaving for the provider and coming back. */
33
+ export interface PendingOAuthRedirect {
34
+ provider: string;
35
+ state: string;
36
+ codeVerifier?: string;
37
+ redirectUri: string;
38
+ startedAt: number;
39
+ }
40
+ export type OAuthCallbackResult =
41
+ /** No OAuth callback in this URL. */
42
+ {
43
+ status: "none";
44
+ }
45
+ /** The provider reported a failure, or the user declined. */
46
+ | {
47
+ status: "error";
48
+ error: string;
49
+ }
50
+ /**
51
+ * A code arrived that this browser cannot account for: no pending
52
+ * authorization, an expired one, or a `state` that does not match. This is
53
+ * what a login-CSRF / code-injection attempt looks like.
54
+ */
55
+ | {
56
+ status: "mismatch";
57
+ } | {
58
+ status: "ok";
59
+ provider: string;
60
+ code: string;
61
+ codeVerifier?: string;
62
+ redirectUri: string;
63
+ };
64
+ type Storage = Pick<globalThis.Storage, "getItem" | "setItem" | "removeItem">;
65
+ export declare function readPendingOAuthRedirect(storage?: Storage | null): PendingOAuthRedirect | null;
66
+ export declare function clearPendingOAuthRedirect(storage?: Storage | null): void;
67
+ /**
68
+ * Build the authorize URL and the pending record that will validate its
69
+ * callback. Split out from {@link startOAuthRedirect} so it can be tested
70
+ * without navigating.
71
+ */
72
+ export declare function buildOAuthAuthorization(request: OAuthRedirectRequest, now?: number): Promise<{
73
+ url: string;
74
+ pending: PendingOAuthRedirect;
75
+ }>;
76
+ /** Remember the authorization and send the browser to the provider. */
77
+ export declare function startOAuthRedirect(request: OAuthRedirectRequest, storage?: Storage | null): Promise<void>;
78
+ /**
79
+ * Interpret a return from the provider.
80
+ *
81
+ * Clears the pending record on **every** path that saw a callback — including
82
+ * `?error=` and a state mismatch — so a declined or abandoned consent screen
83
+ * cannot leave the browser primed to accept somebody else's code later.
84
+ */
85
+ export declare function consumeOAuthCallback(search: string, storage?: Storage | null, now?: number): OAuthCallbackResult;
86
+ export {};
@@ -36,6 +36,17 @@ export interface UseRelationSelectorProps<M extends Record<string, any> = any> {
36
36
  * Property name to use as the secondary display field
37
37
  */
38
38
  descriptionProperty?: keyof M;
39
+ /**
40
+ * Whether the list should be fetched at all. Defaults to `true`.
41
+ *
42
+ * A picker that is mounted is not a picker that is open, and the two used
43
+ * to be the same thing here: the fetch ran on mount, so a collection table
44
+ * with a relation column paid for one query — or one realtime subscription
45
+ * — per rendered row before anyone clicked a cell. Pass `false` until the
46
+ * list is actually needed and nothing is requested; flipping it to `true`
47
+ * fetches once, and it never goes back.
48
+ */
49
+ enabled?: boolean;
39
50
  }
40
51
  export interface RelationSelectorController {
41
52
  items: RelationItem[];
@@ -49,4 +60,4 @@ export interface RelationSelectorController {
49
60
  /**
50
61
  * Hook to manage relation selection with data fetching from Rebase data source
51
62
  */
52
- export declare function useRelationSelector<M extends Record<string, any> = any>({ path, collection, fixedFilter, pageSize, getLabelFromEntity, getDescriptionFromEntity, descriptionProperty }: UseRelationSelectorProps<M>): RelationSelectorController;
63
+ export declare function useRelationSelector<M extends Record<string, any> = any>({ path, collection, fixedFilter, pageSize, getLabelFromEntity, getDescriptionFromEntity, descriptionProperty, enabled }: UseRelationSelectorProps<M>): RelationSelectorController;
package/dist/index.es.js CHANGED
@@ -1,5 +1,5 @@
1
1
  import React, { createContext, lazy, useCallback, useContext, useEffect, useId, useLayoutEffect, useMemo, useRef, useState } from "react";
2
- import { Alert, AlertCircleIcon, AlertTriangleIcon, AppWindow, ArrowDownIcon, ArrowLeftIcon, ArrowRightLeftIcon, ArrowUpDownIcon, ArrowUpIcon, Avatar, BooleanSwitch, Button, CalendarIcon, Card, CenteredView, CheckCircle2Icon, CheckIcon, Checkbox, ChevronDownIcon, ChevronLeftIcon, ChevronRightIcon, ChevronsLeftIcon, ChevronsRightIcon, Chip, CircleDotIcon, CircleUserIcon, CircularProgress, ColumnsIcon, Container, Dialog, DialogActions, DialogContent, DialogTitle, DollarSignIcon, ErrorBoundary, FileIcon, FileTextIcon, FilterChip, FilterIcon, FolderIcon, FolderKanbanIcon, IconButton, KanbanIcon, KanbanView, LanguagesIcon, LayoutGridIcon, ListIcon, ListPlusIcon, ListTodoIcon, LoadingButton, LogOutIcon, MailIcon, Menu, MenuItem, MessageCircleIcon, MoonIcon, MultiSelect, MultiSelectItem, PanelLeftIcon, Paper, PenLineIcon, PencilIcon, PhoneIcon, PinIcon, PlusIcon, Popover, RefreshCwIcon, SearchBar, Select, SelectItem, Separator, SettingsIcon, Skeleton, StickyNoteIcon, SunIcon, SunMoonIcon, Tab, Table, TableBody, TableCell, TableHeader, TableRow, Tabs, TagIcon, TextField, ToggleButtonGroup, Tooltip, Trash2Icon, TrendingUpIcon, TypeIcon, Typography, UserIcon, UserPlus, UsersIcon, VideoIcon, Wand2Icon, WrenchIcon, XIcon, cls, colorClassesMapping, coolIconKeys, defaultBorderMixin, getColorSchemeForKey, getColorSchemeForSeed, iconKeys, iconSize, lucideIcons } from "@rebasepro/ui";
2
+ import { Alert, AlertCircleIcon, AlertTriangleIcon, AppWindow, ArrowDownIcon, ArrowLeftIcon, ArrowRightLeftIcon, ArrowUpDownIcon, ArrowUpIcon, Avatar, BooleanSwitch, Button, CalendarIcon, Card, CenteredView, CheckCircle2Icon, CheckIcon, Checkbox, ChevronDownIcon, ChevronLeftIcon, ChevronRightIcon, ChevronsLeftIcon, ChevronsRightIcon, Chip, CircleDotIcon, CircleUserIcon, CircularProgress, ColumnsIcon, Container, Dialog, DialogActions, DialogContent, DialogTitle, DollarSignIcon, ErrorBoundary, FileIcon, FileTextIcon, FilterChip, FilterIcon, FolderIcon, FolderKanbanIcon, IconButton, KanbanIcon, KanbanView, LanguagesIcon, LayoutGridIcon, ListIcon, ListPlusIcon, ListTodoIcon, LoadingButton, LogOutIcon, LucideIconByName, MailIcon, Menu, MenuItem, MessageCircleIcon, MoonIcon, MultiSelect, MultiSelectItem, PanelLeftIcon, Paper, PenLineIcon, PencilIcon, PhoneIcon, PinIcon, PlusIcon, Popover, RefreshCwIcon, SearchBar, Select, SelectItem, Separator, SettingsIcon, Skeleton, StickyNoteIcon, SunIcon, SunMoonIcon, Tab, Table, TableBody, TableCell, TableHeader, TableRow, Tabs, TagIcon, TextField, ToggleButtonGroup, Tooltip, Trash2Icon, TrendingUpIcon, TypeIcon, Typography, UserIcon, UserPlus, UsersIcon, VideoIcon, Wand2Icon, WrenchIcon, XIcon, cls, colorClassesMapping, coolIconKeys, defaultBorderMixin, getColorSchemeForKey, getColorSchemeForSeed, iconKeys, iconSize } from "@rebasepro/ui";
3
3
  import { ALL_WHERE_FILTER_OPS, DEFAULT_DATA_SOURCE_KEY, DEFAULT_FILTERABLE_RELATION_KINDS, DEFAULT_STORAGE_SOURCE_KEY, EntityReference, EntityRelation, GeoPoint, RebaseApiError, Vector, getDataSourceCapabilities, isLazyComponentRef, isRelationalCollectionConfig } from "@rebasepro/types";
4
4
  import { UNRENDERED_SLOTS, resolveAdminCollection } from "@rebasepro/admin-types";
5
5
  import { Fragment, jsx, jsxs } from "react/jsx-runtime";
@@ -1007,7 +1007,7 @@ var DEFAULT_PAGE_SIZE$1 = 10;
1007
1007
  /**
1008
1008
  * Hook to manage relation selection with data fetching from Rebase data source
1009
1009
  */
1010
- function useRelationSelector({ path, collection, fixedFilter, pageSize = DEFAULT_PAGE_SIZE$1, getLabelFromEntity, getDescriptionFromEntity, descriptionProperty }) {
1010
+ function useRelationSelector({ path, collection, fixedFilter, pageSize = DEFAULT_PAGE_SIZE$1, getLabelFromEntity, getDescriptionFromEntity, descriptionProperty, enabled = true }) {
1011
1011
  const dataClient = useData();
1012
1012
  const [items, setItems] = useState([]);
1013
1013
  const [isLoading, setIsLoading] = useState(false);
@@ -1018,6 +1018,7 @@ function useRelationSelector({ path, collection, fixedFilter, pageSize = DEFAULT
1018
1018
  const [limit, setLimit] = useState(pageSize);
1019
1019
  const unsubscribeRef = useRef(null);
1020
1020
  const searchTimeoutRef = useRef(null);
1021
+ const hasLoadedRef = useRef(false);
1021
1022
  const setLoading = useCallback((loading) => {
1022
1023
  isLoadingRef.current = loading;
1023
1024
  setIsLoading(loading);
@@ -1066,12 +1067,14 @@ function useRelationSelector({ path, collection, fixedFilter, pageSize = DEFAULT
1066
1067
  const whereParams = fixedFilter && Object.keys(fixedFilter).length > 0 ? fixedFilter : void 0;
1067
1068
  const onEntitiesUpdate = (res) => {
1068
1069
  const newItems = res.data.map((e) => entityToRelationItem(e));
1070
+ hasLoadedRef.current = true;
1069
1071
  setItems(newItems);
1070
1072
  setHasMore(res.meta.hasMore);
1071
1073
  setLoading(false);
1072
1074
  };
1073
1075
  const onErrorUpdate = (fetchError) => {
1074
1076
  console.error("useRelationSelector: Error fetching data:", fetchError);
1077
+ hasLoadedRef.current = true;
1075
1078
  setError(fetchError);
1076
1079
  setLoading(false);
1077
1080
  };
@@ -1141,11 +1144,16 @@ function useRelationSelector({ path, collection, fixedFilter, pageSize = DEFAULT
1141
1144
  setLoading
1142
1145
  ]);
1143
1146
  useEffect(() => {
1147
+ if (!enabled) return;
1144
1148
  fetchData();
1145
1149
  return () => {
1146
1150
  cleanupSubscription();
1147
1151
  };
1148
- }, [fetchData]);
1152
+ }, [
1153
+ fetchData,
1154
+ enabled,
1155
+ cleanupSubscription
1156
+ ]);
1149
1157
  useEffect(() => {
1150
1158
  return () => {
1151
1159
  if (searchTimeoutRef.current) clearTimeout(searchTimeoutRef.current);
@@ -1153,7 +1161,7 @@ function useRelationSelector({ path, collection, fixedFilter, pageSize = DEFAULT
1153
1161
  }, []);
1154
1162
  return useMemo(() => ({
1155
1163
  items,
1156
- isLoading,
1164
+ isLoading: isLoading || enabled && !hasLoadedRef.current,
1157
1165
  error,
1158
1166
  search,
1159
1167
  loadMore,
@@ -1166,7 +1174,8 @@ function useRelationSelector({ path, collection, fixedFilter, pageSize = DEFAULT
1166
1174
  search,
1167
1175
  loadMore,
1168
1176
  hasMore,
1169
- entityToRelationItem
1177
+ entityToRelationItem,
1178
+ enabled
1170
1179
  ]);
1171
1180
  }
1172
1181
  //#endregion
@@ -9358,6 +9367,133 @@ function UserDisplay({ user }) {
9358
9367
  });
9359
9368
  }
9360
9369
  //#endregion
9370
+ //#region src/components/LoginView/oauth-redirect-flow.ts
9371
+ /**
9372
+ * The browser half of the OAuth authorization-code flow.
9373
+ *
9374
+ * The server only ever sees a `code` and a `redirectUri` taken from the
9375
+ * request body, so `state`, PKCE and the binding between "this browser started
9376
+ * a login" and "this code came back" are, by construction, the client's job.
9377
+ * In the admin login they had been nobody's job: the authorize URL carried no
9378
+ * `state`, and the return leg accepted any `?code=` as long as a localStorage
9379
+ * marker naming the *provider* was set — a marker written on button click and
9380
+ * removed only on a callback that carried a code, so abandoning the flow left
9381
+ * it set indefinitely.
9382
+ *
9383
+ * Everything here is provider-agnostic on purpose. A button that wants a new
9384
+ * provider calls {@link startOAuthRedirect} and gets state, expiry and PKCE
9385
+ * without deciding anything, which is what stops the next provider shipping
9386
+ * without them.
9387
+ */
9388
+ /** sessionStorage, not localStorage: a login attempt belongs to one tab. */
9389
+ var STORAGE_KEY = "rebase_oauth_redirect";
9390
+ /** How long a started authorization stays valid. */
9391
+ var PENDING_TTL_MS = 600 * 1e3;
9392
+ function defaultStorage() {
9393
+ try {
9394
+ return window.sessionStorage;
9395
+ } catch {
9396
+ return null;
9397
+ }
9398
+ }
9399
+ function base64Url(bytes) {
9400
+ let binary = "";
9401
+ for (const byte of bytes) binary += String.fromCharCode(byte);
9402
+ return btoa(binary).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
9403
+ }
9404
+ function randomToken(byteLength = 32) {
9405
+ const bytes = new Uint8Array(byteLength);
9406
+ crypto.getRandomValues(bytes);
9407
+ return base64Url(bytes);
9408
+ }
9409
+ async function pkceChallenge(verifier) {
9410
+ const digest = await crypto.subtle.digest("SHA-256", new TextEncoder().encode(verifier));
9411
+ return base64Url(new Uint8Array(digest));
9412
+ }
9413
+ function readPendingOAuthRedirect(storage = defaultStorage()) {
9414
+ if (!storage) return null;
9415
+ const raw = storage.getItem(STORAGE_KEY);
9416
+ if (!raw) return null;
9417
+ try {
9418
+ const parsed = JSON.parse(raw);
9419
+ if (!parsed?.provider || !parsed?.state) return null;
9420
+ return parsed;
9421
+ } catch {
9422
+ return null;
9423
+ }
9424
+ }
9425
+ function clearPendingOAuthRedirect(storage = defaultStorage()) {
9426
+ storage?.removeItem(STORAGE_KEY);
9427
+ }
9428
+ /**
9429
+ * Build the authorize URL and the pending record that will validate its
9430
+ * callback. Split out from {@link startOAuthRedirect} so it can be tested
9431
+ * without navigating.
9432
+ */
9433
+ async function buildOAuthAuthorization(request, now = Date.now()) {
9434
+ const state = randomToken();
9435
+ const url = new URL(request.authorizeUrl);
9436
+ url.searchParams.set("response_type", "code");
9437
+ url.searchParams.set("client_id", request.clientId);
9438
+ url.searchParams.set("redirect_uri", request.redirectUri);
9439
+ url.searchParams.set("scope", request.scope);
9440
+ url.searchParams.set("state", state);
9441
+ for (const [key, value] of Object.entries(request.params ?? {})) url.searchParams.set(key, value);
9442
+ let codeVerifier;
9443
+ if (request.pkce) {
9444
+ codeVerifier = randomToken(32);
9445
+ url.searchParams.set("code_challenge", await pkceChallenge(codeVerifier));
9446
+ url.searchParams.set("code_challenge_method", "S256");
9447
+ }
9448
+ return {
9449
+ url: url.toString(),
9450
+ pending: {
9451
+ provider: request.provider,
9452
+ state,
9453
+ codeVerifier,
9454
+ redirectUri: request.redirectUri,
9455
+ startedAt: now
9456
+ }
9457
+ };
9458
+ }
9459
+ /** Remember the authorization and send the browser to the provider. */
9460
+ async function startOAuthRedirect(request, storage = defaultStorage()) {
9461
+ const { url, pending } = await buildOAuthAuthorization(request);
9462
+ storage?.setItem(STORAGE_KEY, JSON.stringify(pending));
9463
+ window.location.href = url;
9464
+ }
9465
+ /**
9466
+ * Interpret a return from the provider.
9467
+ *
9468
+ * Clears the pending record on **every** path that saw a callback — including
9469
+ * `?error=` and a state mismatch — so a declined or abandoned consent screen
9470
+ * cannot leave the browser primed to accept somebody else's code later.
9471
+ */
9472
+ function consumeOAuthCallback(search, storage = defaultStorage(), now = Date.now()) {
9473
+ const params = new URLSearchParams(search);
9474
+ const code = params.get("code");
9475
+ const state = params.get("state");
9476
+ const error = params.get("error");
9477
+ if (!code && !error && !state) return { status: "none" };
9478
+ const pending = readPendingOAuthRedirect(storage);
9479
+ clearPendingOAuthRedirect(storage);
9480
+ if (error) return {
9481
+ status: "error",
9482
+ error
9483
+ };
9484
+ if (!code) return { status: "mismatch" };
9485
+ if (!pending) return { status: "mismatch" };
9486
+ if (now - pending.startedAt > PENDING_TTL_MS) return { status: "mismatch" };
9487
+ if (!state || state !== pending.state) return { status: "mismatch" };
9488
+ return {
9489
+ status: "ok",
9490
+ provider: pending.provider,
9491
+ code,
9492
+ codeVerifier: pending.codeVerifier,
9493
+ redirectUri: pending.redirectUri
9494
+ };
9495
+ }
9496
+ //#endregion
9361
9497
  //#region src/components/LoginView/LoginView.tsx
9362
9498
  /**
9363
9499
  * The shared field background mixin (`dark:bg-black/30`) is invisible on the
@@ -9427,19 +9563,25 @@ function LoginView({ logo, authController, noUserComponent, disableSignupScreen
9427
9563
  return () => clearTimeout(timer);
9428
9564
  }, []);
9429
9565
  useEffect(() => {
9430
- const code = new URLSearchParams(window.location.search).get("code");
9431
- const provider = localStorage.getItem("rebase_oauth_provider");
9432
- if (code && provider) {
9433
- localStorage.removeItem("rebase_oauth_provider");
9434
- const cleanUrl = window.location.origin + window.location.pathname;
9435
- window.history.replaceState({}, document.title, cleanUrl);
9436
- if (authController.oauthLogin) authController.oauthLogin(provider, {
9437
- code,
9438
- redirectUri: cleanUrl
9439
- }).catch((err) => {
9440
- console.error(`${provider} login failed:`, err);
9441
- });
9566
+ const result = consumeOAuthCallback(window.location.search);
9567
+ if (result.status === "none") return;
9568
+ const cleanUrl = window.location.origin + window.location.pathname;
9569
+ window.history.replaceState({}, document.title, cleanUrl);
9570
+ if (result.status === "error") {
9571
+ console.error(`OAuth sign-in failed: ${result.error}`);
9572
+ return;
9573
+ }
9574
+ if (result.status === "mismatch") {
9575
+ console.error("Ignoring an OAuth code that does not match a sign-in this browser started.");
9576
+ return;
9442
9577
  }
9578
+ if (authController.oauthLogin) authController.oauthLogin(result.provider, {
9579
+ code: result.code,
9580
+ redirectUri: result.redirectUri,
9581
+ ...result.codeVerifier ? { codeVerifier: result.codeVerifier } : {}
9582
+ }).catch((err) => {
9583
+ console.error(`${result.provider} login failed:`, err);
9584
+ });
9443
9585
  }, [authController]);
9444
9586
  let logoComponent;
9445
9587
  if (logo) logoComponent = /* @__PURE__ */ jsx("img", {
@@ -9710,10 +9852,13 @@ var GitHubIcon = () => /* @__PURE__ */ jsx("svg", {
9710
9852
  });
9711
9853
  function GitHubLoginButton({ disabled, githubClientId }) {
9712
9854
  const handleClick = () => {
9713
- localStorage.setItem("rebase_oauth_provider", "github");
9714
- const redirectUri = encodeURIComponent(window.location.origin + window.location.pathname);
9715
- const scope = "read:user,user:email";
9716
- window.location.href = `https://github.com/login/oauth/authorize?client_id=${githubClientId}&redirect_uri=${redirectUri}&scope=${scope}`;
9855
+ startOAuthRedirect({
9856
+ provider: "github",
9857
+ authorizeUrl: "https://github.com/login/oauth/authorize",
9858
+ clientId: githubClientId,
9859
+ redirectUri: window.location.origin + window.location.pathname,
9860
+ scope: "read:user,user:email"
9861
+ });
9717
9862
  };
9718
9863
  return /* @__PURE__ */ jsx(LoginButton, {
9719
9864
  disabled,
@@ -9731,10 +9876,13 @@ var LinkedInIcon = () => /* @__PURE__ */ jsx("svg", {
9731
9876
  });
9732
9877
  function LinkedInLoginButton({ disabled, linkedinClientId }) {
9733
9878
  const handleClick = () => {
9734
- localStorage.setItem("rebase_oauth_provider", "linkedin");
9735
- const redirectUri = encodeURIComponent(window.location.origin + window.location.pathname);
9736
- const scope = "openid profile email";
9737
- window.location.href = `https://www.linkedin.com/oauth/v2/authorization?response_type=code&client_id=${linkedinClientId}&redirect_uri=${redirectUri}&scope=${scope}`;
9879
+ startOAuthRedirect({
9880
+ provider: "linkedin",
9881
+ authorizeUrl: "https://www.linkedin.com/oauth/v2/authorization",
9882
+ clientId: linkedinClientId,
9883
+ redirectUri: window.location.origin + window.location.pathname,
9884
+ scope: "openid profile email"
9885
+ });
9738
9886
  };
9739
9887
  return /* @__PURE__ */ jsx(LoginButton, {
9740
9888
  disabled,
@@ -9746,6 +9894,9 @@ function LinkedInLoginButton({ disabled, linkedinClientId }) {
9746
9894
  function LoginForm({ onClose, onForgotPassword, authController, registrationMode, noUserComponent, disableSignupScreen, bootstrapMode = false, switchToRegister, switchToLogin, defaultEmail, defaultPassword, onNewsletterOptIn, newsletterOptIn = false, setNewsletterOptIn }) {
9747
9895
  const passwordRef = useRef(null);
9748
9896
  const { t } = useTranslation();
9897
+ const emailId = useId();
9898
+ const passwordId = useId();
9899
+ const displayNameId = useId();
9749
9900
  const [email, setEmail] = useState(defaultEmail);
9750
9901
  const [password, setPassword] = useState(defaultPassword);
9751
9902
  const [displayName, setDisplayName] = useState();
@@ -9814,11 +9965,14 @@ function LoginForm({ onClose, onForgotPassword, authController, registrationMode
9814
9965
  className: "w-full mb-3",
9815
9966
  children: [/* @__PURE__ */ jsx(Typography, {
9816
9967
  variant: "label",
9968
+ component: "label",
9817
9969
  color: "secondary",
9818
9970
  className: "mb-1",
9971
+ htmlFor: displayNameId,
9819
9972
  children: "Display Name"
9820
9973
  }), /* @__PURE__ */ jsx(TextField, {
9821
9974
  placeholder: "Jane Doe (optional)",
9975
+ id: displayNameId,
9822
9976
  className: loginFieldClasses,
9823
9977
  value: displayName ?? "",
9824
9978
  disabled: authController.initialLoading,
@@ -9831,11 +9985,14 @@ function LoginForm({ onClose, onForgotPassword, authController, registrationMode
9831
9985
  className: "w-full mb-3",
9832
9986
  children: [/* @__PURE__ */ jsx(Typography, {
9833
9987
  variant: "label",
9988
+ component: "label",
9834
9989
  color: "secondary",
9835
9990
  className: "mb-1",
9991
+ htmlFor: emailId,
9836
9992
  children: "Email"
9837
9993
  }), /* @__PURE__ */ jsx(TextField, {
9838
9994
  placeholder: "you@example.com",
9995
+ id: emailId,
9839
9996
  className: loginFieldClasses,
9840
9997
  autoFocus: true,
9841
9998
  value: email ?? "",
@@ -9849,11 +10006,14 @@ function LoginForm({ onClose, onForgotPassword, authController, registrationMode
9849
10006
  className: "w-full mb-1",
9850
10007
  children: [/* @__PURE__ */ jsx(Typography, {
9851
10008
  variant: "label",
10009
+ component: "label",
9852
10010
  color: "secondary",
9853
10011
  className: "mb-1",
10012
+ htmlFor: passwordId,
9854
10013
  children: "Password"
9855
10014
  }), /* @__PURE__ */ jsx(TextField, {
9856
10015
  placeholder: "••••••••",
10016
+ id: passwordId,
9857
10017
  className: loginFieldClasses,
9858
10018
  value: password ?? "",
9859
10019
  disabled: authController.initialLoading,
@@ -9942,6 +10102,7 @@ function ForgotPasswordForm({ onClose, authController }) {
9942
10102
  const [email, setEmail] = useState("");
9943
10103
  const [submitted, setSubmitted] = useState(false);
9944
10104
  const [error, setError] = useState(null);
10105
+ const emailId = useId();
9945
10106
  useEffect(() => {
9946
10107
  if (!document) return;
9947
10108
  const escFunction = (event) => {
@@ -10042,11 +10203,14 @@ function ForgotPasswordForm({ onClose, authController }) {
10042
10203
  className: "w-full mb-3",
10043
10204
  children: [/* @__PURE__ */ jsx(Typography, {
10044
10205
  variant: "label",
10206
+ component: "label",
10045
10207
  color: "secondary",
10046
10208
  className: "mb-1",
10209
+ htmlFor: emailId,
10047
10210
  children: "Email"
10048
10211
  }), /* @__PURE__ */ jsx(TextField, {
10049
10212
  placeholder: "you@example.com",
10213
+ id: emailId,
10050
10214
  className: loginFieldClasses,
10051
10215
  autoFocus: true,
10052
10216
  value: email,
@@ -10470,6 +10634,7 @@ var en = {
10470
10634
  flatten_arrays: "Flatten arrays",
10471
10635
  download: "Download",
10472
10636
  large_number_of_documents: "This collection has a large number of documents ({{count}}).",
10637
+ too_many_documents_to_export: "This collection has more documents ({{count}}) than the browser can export in a single file (limit {{limit}}). Filter it down before exporting.",
10473
10638
  include_undefined_values: "Include undefined values",
10474
10639
  submit: "Submit",
10475
10640
  no_filterable_properties: "No filterable properties available",
@@ -11368,6 +11533,7 @@ var es = {
11368
11533
  flatten_arrays: "Aplanar arrays",
11369
11534
  download: "Descargar",
11370
11535
  large_number_of_documents: "Esta colección posee un gran número de documentos ({{count}}).",
11536
+ too_many_documents_to_export: "Esta colección tiene más documentos ({{count}}) de los que el navegador puede exportar en un solo archivo (límite {{limit}}). Fíltrala antes de exportar.",
11371
11537
  include_undefined_values: "Incluir valores omitidos (undefined)",
11372
11538
  submit: "Enviar",
11373
11539
  no_filterable_properties: "No hay propiedades filtrables disponibles",
@@ -12225,6 +12391,7 @@ var de = {
12225
12391
  flatten_arrays: "Arrays glätten",
12226
12392
  download: "Herunterladen",
12227
12393
  large_number_of_documents: "Diese Sammlung hat eine große Anzahl von Dokumenten ({{count}}).",
12394
+ too_many_documents_to_export: "Diese Sammlung enthält mehr Dokumente ({{count}}), als der Browser in einer einzigen Datei exportieren kann (Grenze {{limit}}). Filtern Sie sie vor dem Export.",
12228
12395
  include_undefined_values: "Undefinierte Werte einschließen",
12229
12396
  submit: "Einreichen",
12230
12397
  no_filterable_properties: "Keine filterbaren Eigenschaften verfügbar",
@@ -13072,6 +13239,7 @@ var fr = {
13072
13239
  flatten_arrays: "Aplatir les tableaux",
13073
13240
  download: "Télécharger",
13074
13241
  large_number_of_documents: "Cette collection contient un grand nombre de documents ({{count}}).",
13242
+ too_many_documents_to_export: "Cette collection contient plus de documents ({{count}}) que le navigateur ne peut en exporter dans un seul fichier (limite {{limit}}). Filtrez-la avant d'exporter.",
13075
13243
  include_undefined_values: "Inclure les valeurs non définies",
13076
13244
  submit: "Soumettre",
13077
13245
  no_filterable_properties: "Aucune propriété filtrable disponible",
@@ -13919,6 +14087,7 @@ var it = {
13919
14087
  flatten_arrays: "Appiattisci gli array",
13920
14088
  download: "Scarica",
13921
14089
  large_number_of_documents: "Questa collezione contiene un numero elevato di documenti ({{count}})",
14090
+ too_many_documents_to_export: "Questa collezione contiene più documenti ({{count}}) di quanti il browser possa esportarne in un solo file (limite {{limit}}). Filtrala prima di esportare.",
13922
14091
  include_undefined_values: "Includi valori non definiti",
13923
14092
  submit: "Invia",
13924
14093
  no_filterable_properties: "Nessuna proprietà filtrabile disponibile",
@@ -14766,6 +14935,7 @@ var hi = {
14766
14935
  flatten_arrays: "ऐरे को फ़्लैट करें",
14767
14936
  download: "डाउनलोड",
14768
14937
  large_number_of_documents: "इस संग्रह में बड़ी संख्या में दस्तावेज़ हैं ({{count}})।",
14938
+ too_many_documents_to_export: "इस संग्रह में इतने दस्तावेज़ ({{count}}) हैं कि ब्राउज़र उन्हें एक ही फ़ाइल में निर्यात नहीं कर सकता (सीमा {{limit}})। निर्यात से पहले इसे फ़िल्टर करें।",
14769
14939
  include_undefined_values: "अपरिभाषित (undefined) मान शामिल करें",
14770
14940
  submit: "सबमिट करें",
14771
14941
  no_filterable_properties: "कोई फ़िल्टर करने योग्य गुण उपलब्ध नहीं हैं",
@@ -15618,6 +15788,7 @@ var pt = {
15618
15788
  flatten_arrays: "Achatar listas",
15619
15789
  download: "Descarregar",
15620
15790
  large_number_of_documents: "Esta coleção tem um grande número de documentos ({{count}}).",
15791
+ too_many_documents_to_export: "Esta coleção tem mais documentos ({{count}}) do que o navegador consegue exportar num único ficheiro (limite {{limit}}). Filtre-a antes de exportar.",
15621
15792
  include_undefined_values: "Incluir valores indefinidos",
15622
15793
  submit: "Submeter",
15623
15794
  no_filterable_properties: "Não há propriedades filtráveis disponíveis",
@@ -16548,6 +16719,7 @@ function useRebaseAuthController(props = {}) {
16548
16719
  const syncState = async (event, session) => {
16549
16720
  await updateState(session);
16550
16721
  if (event === "SIGNED_OUT") {
16722
+ clearFetchCache();
16551
16723
  if (isMountedRef.current) setLoginSkipped(false);
16552
16724
  onSignOutRef.current?.();
16553
16725
  }
@@ -16876,23 +17048,13 @@ var iconKeysMap = iconKeys.reduce((acc, key) => {
16876
17048
  acc[key] = key;
16877
17049
  return acc;
16878
17050
  }, {});
16879
- function toPascalCase(str) {
16880
- return str.split(/[-_]/).map((word) => word.charAt(0).toUpperCase() + word.slice(1)).join("");
16881
- }
16882
- /**
16883
- * Resolve a Lucide icon component by string key.
16884
- * Tries direct match, PascalCase conversion, and falls back to CircleAlert.
16885
- */
16886
- function resolveIcon(iconKey) {
16887
- const iconsMap = lucideIcons;
16888
- let icon = iconsMap[iconKey];
16889
- if (!icon) icon = iconsMap[toPascalCase(iconKey)];
16890
- if (!icon) icon = iconsMap.CircleAlert;
16891
- return icon ?? null;
16892
- }
16893
17051
  /**
16894
17052
  * Render an icon element from a string key or existing React element.
16895
- * This resolves the icon from the lucide-react icons map directly.
17053
+ *
17054
+ * Whether a key names an icon is decided here, against `iconKeys` — an array
17055
+ * of strings. The component behind the key is fetched by `LucideIconByName` on
17056
+ * first use: this used to index lucide's full `icons` map, which is the whole
17057
+ * library and cannot be tree-shaken.
16896
17058
  */
16897
17059
  function getIcon(iconKey, className, color, size) {
16898
17060
  if (React.isValidElement(iconKey)) return iconKey;
@@ -16902,9 +17064,8 @@ function getIcon(iconKey, className, color, size) {
16902
17064
  const slugifiedKey = slugify(iconKey).replace(/-/g, "_");
16903
17065
  const mappedKey = iconKeysMap[iconKey] || iconKeysMap[lowerKey] || iconKeysMap[slugifiedKey];
16904
17066
  if (!mappedKey) return;
16905
- const LucideIcon = resolveIcon(mappedKey);
16906
- if (!LucideIcon) return void 0;
16907
- return /* @__PURE__ */ jsx(LucideIcon, {
17067
+ return /* @__PURE__ */ jsx(LucideIconByName, {
17068
+ name: mappedKey,
16908
17069
  size: typeof size === "number" ? size : iconSize[size ?? "medium"],
16909
17070
  className: cls(color ? colorClassesMapping[color] : "", "select-none shrink-0", className)
16910
17071
  });
@@ -16924,10 +17085,10 @@ var IconForView = React.memo(function IconForView({ collectionOrView, className,
16924
17085
  }
16925
17086
  const iconsCount = coolIconKeys.length;
16926
17087
  if (!key) key = coolIconKeys[hashString(collectionOrView.slug) % iconsCount];
16927
- const LucideIcon = resolveIcon(key);
16928
- if (!LucideIcon) return /* @__PURE__ */ jsx(Fragment, {});
16929
- return /* @__PURE__ */ jsx(LucideIcon, {
16930
- size: typeof size === "number" ? size : iconSize[size],
17088
+ const sizeInPx = typeof size === "number" ? size : iconSize[size];
17089
+ return /* @__PURE__ */ jsx(LucideIconByName, {
17090
+ name: key,
17091
+ size: sizeInPx,
16931
17092
  className: cls(color ? colorClassesMapping[color] : "", "select-none shrink-0", className)
16932
17093
  });
16933
17094
  }, (prevProps, nextProps) => {