@basictech/react 0.12.0-beta.0 → 0.12.0-beta.2

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/index.mjs CHANGED
@@ -1,3 +1,12 @@
1
+ 'use client';
2
+
3
+ // src/create-basic.tsx
4
+ import { useContext as useContext2 } from "react";
5
+
6
+ // src/context.ts
7
+ import { createContext } from "react";
8
+ var BasicClientContext = createContext(null);
9
+
1
10
  // src/create-client.ts
2
11
  import {
3
12
  MemoryKeyValueStorage,
@@ -6,6 +15,10 @@ import {
6
15
  } from "@basictech/core";
7
16
 
8
17
  // src/adapters/broadcast-channel.ts
18
+ function supportsAccessTokenAdoption(store) {
19
+ const candidate = store;
20
+ return typeof candidate?.accessToken === "function" && typeof candidate?.adoptAccessToken === "function";
21
+ }
9
22
  function createBrowserMessageChannel(name) {
10
23
  const channel = new BroadcastChannel(name);
11
24
  const adapter = {
@@ -16,12 +29,11 @@ function createBrowserMessageChannel(name) {
16
29
  channel.onmessage = (event) => adapter.onmessage?.({ data: event.data });
17
30
  return adapter;
18
31
  }
19
- function createBrowserAuthChannelFactory(clientId, tokenStore) {
20
- const prefix = `basic_auth:${clientId}:`;
21
- return (name) => {
32
+ function createBrowserAuthChannelFactory(tokenStore) {
33
+ return (name, context) => {
22
34
  const channel = createBrowserMessageChannel(name);
23
- const profileId = name.startsWith(prefix) ? name.slice(prefix.length) : "";
24
- const tokenKey = `u:${profileId}:basic_tokens`;
35
+ if (!context) return channel;
36
+ const { tokenKey } = context;
25
37
  const adapter = {
26
38
  postMessage: (message) => {
27
39
  const outgoing = message;
@@ -568,32 +580,27 @@ function browserReconcileTrigger(listener) {
568
580
  function createBasicClient(config) {
569
581
  const local = browserStorage("localStorage") ?? new MemoryKeyValueStorage();
570
582
  const session = browserStorage("sessionStorage") ?? new MemoryKeyValueStorage();
571
- const defaultTokenStore = config.tokenStore ? null : new BrowserTokenStore(local);
583
+ const tokenStore = config.tokenStore ?? new BrowserTokenStore(local);
572
584
  const canBroadcast = typeof BroadcastChannel !== "undefined";
573
585
  const canPersistReplica = typeof indexedDB !== "undefined";
586
+ const messageChannel = !canBroadcast ? void 0 : supportsAccessTokenAdoption(tokenStore) ? createBrowserAuthChannelFactory(tokenStore) : createBrowserMessageChannel;
574
587
  return createCoreClient({
575
588
  ...config,
576
589
  kv: config.kv ?? local,
577
- tokenStore: config.tokenStore ?? defaultTokenStore,
590
+ tokenStore,
578
591
  sessionStorage: config.sessionStorage ?? session,
579
592
  replicaStore: config.replicaStore ?? (canPersistReplica ? new PersistenceStore(config.clientId) : new MemoryReplicaStoreFactory()),
580
593
  navigate: config.navigate ?? browserNavigate,
581
594
  currentUrl: config.currentUrl ?? browserCurrentUrl,
582
595
  replaceUrl: config.replaceUrl ?? browserReplaceUrl,
583
596
  reconcileTrigger: config.reconcileTrigger ?? browserReconcileTrigger,
584
- createMessageChannel: config.createMessageChannel ?? (canBroadcast && defaultTokenStore ? createBrowserAuthChannelFactory(config.clientId, defaultTokenStore) : void 0),
597
+ createMessageChannel: config.createMessageChannel ?? messageChannel,
585
598
  uploadTransport: config.uploadTransport ?? (typeof XMLHttpRequest === "undefined" ? void 0 : browserUploadTransport)
586
599
  });
587
600
  }
588
601
 
589
602
  // src/provider.tsx
590
603
  import { useEffect, useRef, useState, useSyncExternalStore } from "react";
591
-
592
- // src/context.ts
593
- import { createContext } from "react";
594
- var BasicClientContext = createContext(null);
595
-
596
- // src/provider.tsx
597
604
  import { jsx } from "react/jsx-runtime";
598
605
  var clientLifecycles = /* @__PURE__ */ new WeakMap();
599
606
  function retainClient(client) {
@@ -662,10 +669,10 @@ function BasicProvider(props) {
662
669
  }
663
670
 
664
671
  // src/hooks/accounts.ts
665
- import { useCallback, useMemo } from "react";
672
+ import { useCallback, useMemo as useMemo2 } from "react";
666
673
 
667
674
  // src/hooks/client.ts
668
- import { useContext, useEffect as useEffect2, useRef as useRef2, useState as useState2, useSyncExternalStore as useSyncExternalStore2 } from "react";
675
+ import { useContext, useEffect as useEffect2, useMemo, useRef as useRef2, useState as useState2, useSyncExternalStore as useSyncExternalStore2 } from "react";
669
676
  import {
670
677
  BasicError as BasicError4
671
678
  } from "@basictech/core";
@@ -677,6 +684,10 @@ function useRequiredClient() {
677
684
  function useClientSnapshot(client) {
678
685
  return useSyncExternalStore2(client.subscribe, client.getSnapshot, client.getSnapshot);
679
686
  }
687
+ function useSignInGate(snapshot) {
688
+ const code = snapshot.isReady && !snapshot.isSignedIn ? snapshot.readOnlyReason ?? "AUTHORIZATION_REQUIRED" : null;
689
+ return useMemo(() => code === null ? null : new BasicError4(code, code === "AUTH_EXPIRED" ? "Session expired. Sign in again to load this list." : "Sign in to load this list."), [code]);
690
+ }
680
691
  function toBasicError(error, fallback = "UNKNOWN_ERROR") {
681
692
  if (error instanceof BasicError4) return error;
682
693
  return new BasicError4(fallback, error instanceof Error ? error.message : String(error));
@@ -725,38 +736,35 @@ function useAsyncResult(load, initialData, enabled, settled, dependencies) {
725
736
  function useAccountsFor(client) {
726
737
  const snapshot = useClientSnapshot(client);
727
738
  const switchAccount = useCallback((id) => client.switchAccount(id), [client]);
728
- const addAccount = useCallback(() => client.addAccount(), [client]);
739
+ const addAccount = useCallback((options) => client.addAccount(options), [client]);
729
740
  const removeAccount = useCallback((id) => client.removeAccount(id), [client]);
730
- return useMemo(() => ({
741
+ return useMemo2(() => ({
731
742
  accounts: snapshot.accounts,
732
- active: snapshot.activeProfile,
743
+ activeAccount: snapshot.activeAccount,
733
744
  switchAccount,
734
745
  addAccount,
735
746
  removeAccount
736
- }), [snapshot.accounts, snapshot.activeProfile, switchAccount, addAccount, removeAccount]);
747
+ }), [snapshot.accounts, snapshot.activeAccount, switchAccount, addAccount, removeAccount]);
737
748
  }
738
749
  function useAccounts() {
739
750
  return useAccountsFor(useRequiredClient());
740
751
  }
741
752
 
742
753
  // src/hooks/auth.ts
743
- import { useCallback as useCallback2, useMemo as useMemo2 } from "react";
744
- import {
745
- BasicError as BasicError5
746
- } from "@basictech/core";
754
+ import { useCallback as useCallback2, useMemo as useMemo3 } from "react";
747
755
  function useAuthFor(client) {
748
756
  const snapshot = useClientSnapshot(client);
749
757
  const signIn = useCallback2((input) => client.signIn(input), [client]);
750
758
  const signOut = useCallback2(() => client.signOut(), [client]);
751
759
  const getToken = useCallback2(() => client.getToken(), [client]);
752
- return useMemo2(() => ({
760
+ return useMemo3(() => ({
753
761
  isReady: snapshot.isReady,
754
762
  isSignedIn: snapshot.isSignedIn,
755
763
  isAnonymous: snapshot.isAnonymous,
756
764
  canWrite: snapshot.canWrite,
757
765
  readOnlyReason: snapshot.readOnlyReason,
758
766
  status: snapshot.authStatus,
759
- error: snapshot.authErrorCode ? new BasicError5(snapshot.authErrorCode) : null,
767
+ error: snapshot.authError,
760
768
  user: snapshot.user,
761
769
  did: snapshot.did,
762
770
  handle: snapshot.handle,
@@ -769,6 +777,9 @@ function useAuth() {
769
777
  return useAuthFor(useRequiredClient());
770
778
  }
771
779
 
780
+ // src/hooks/basic.ts
781
+ import { useMemo as useMemo6 } from "react";
782
+
772
783
  // src/hooks/db.ts
773
784
  function useDbFor(client, source = "default") {
774
785
  return client.db(source);
@@ -784,13 +795,13 @@ function useCollection(name, options = {}) {
784
795
  }
785
796
 
786
797
  // src/hooks/repos.ts
787
- import { useCallback as useCallback3, useMemo as useMemo3 } from "react";
798
+ import { useCallback as useCallback3, useMemo as useMemo4 } from "react";
788
799
  function useReposFor(client) {
789
800
  const snapshot = useClientSnapshot(client);
790
801
  const refresh = useCallback3(() => client.refreshRepos(), [client]);
791
802
  const create = useCallback3((input) => client.createRepo(input), [client]);
792
803
  const archive = useCallback3((repoId) => client.archiveRepo(repoId), [client]);
793
- return useMemo3(() => ({
804
+ return useMemo4(() => ({
794
805
  repos: snapshot.repos,
795
806
  defaultRepoId: snapshot.defaultRepoId,
796
807
  refresh,
@@ -803,7 +814,7 @@ function useRepos() {
803
814
  }
804
815
 
805
816
  // src/hooks/status.ts
806
- import { useCallback as useCallback4, useMemo as useMemo4 } from "react";
817
+ import { useCallback as useCallback4, useMemo as useMemo5 } from "react";
807
818
  function useSyncStatusFor(client, source) {
808
819
  const snapshot = useClientSnapshot(client);
809
820
  const sourceKey = JSON.stringify(source);
@@ -818,7 +829,7 @@ function useSyncStatusFor(client, source) {
818
829
  // eslint-disable-next-line react-hooks/exhaustive-deps
819
830
  [client, sourceKey]
820
831
  );
821
- return useMemo4(() => ({
832
+ return useMemo5(() => ({
822
833
  status: source === void 0 ? snapshot.syncStatus : client.status(source),
823
834
  pendingCount: client.pending(source).length,
824
835
  rejected: client.rejected(source),
@@ -845,51 +856,41 @@ function useBasicFor(client) {
845
856
  const auth = useAuthFor(client);
846
857
  const accounts = useAccountsFor(client);
847
858
  const sync = useSyncStatusFor(client);
848
- const repos = useReposFor(client);
849
- return {
859
+ const repos = useReposFor(client).repos;
860
+ const db = useDbFor(client);
861
+ return useMemo6(() => ({
850
862
  ...auth,
851
863
  client,
852
- db: useDbFor(client),
864
+ db,
853
865
  accounts,
854
866
  sync,
855
- repos: repos.repos
856
- };
867
+ repos
868
+ }), [auth, client, db, accounts, sync, repos]);
857
869
  }
858
870
  function useBasic() {
859
871
  return useBasicFor(useRequiredClient());
860
872
  }
861
873
 
862
874
  // src/hooks/files.ts
863
- import { useMemo as useMemo5, useSyncExternalStore as useSyncExternalStore3 } from "react";
875
+ import { useMemo as useMemo7, useSyncExternalStore as useSyncExternalStore3 } from "react";
864
876
  var zero = () => 0;
865
877
  function useFilesFor(client, query = {}, options = {}) {
866
878
  const source = options.source ?? "default";
867
879
  const sourceKey = JSON.stringify(source);
868
880
  const queryKey = JSON.stringify(query);
869
881
  const snapshot = useClientSnapshot(client);
870
- const subscription = useMemo5(
882
+ const subscription = useMemo7(
871
883
  () => client.queryStore.collection(source, "_files"),
872
884
  // eslint-disable-next-line react-hooks/exhaustive-deps
873
885
  [client, sourceKey]
874
886
  );
875
887
  const version = useSyncExternalStore3(subscription.subscribe, subscription.getSnapshot, zero);
876
888
  return useAsyncResult(
877
- async () => {
878
- const files = client.db(source).files;
879
- if (source !== "default" && "mountId" in source) {
880
- const page = await files.list({
881
- ...query.prefix !== void 0 ? { prefix: query.prefix } : {},
882
- ...query.limit !== void 0 ? { limit: query.limit } : {},
883
- ...query.cursor !== void 0 ? { cursor: query.cursor } : {}
884
- });
885
- return page.data;
886
- }
887
- return (await files.list(query)).data;
888
- },
889
+ async () => (await client.db(source).files.list(query)).data,
889
890
  [],
890
891
  snapshot.isReady,
891
892
  snapshot.isReady,
892
- [sourceKey, queryKey, version, snapshot.activeProfile?.id]
893
+ [sourceKey, queryKey, version, snapshot.activeAccount?.id]
893
894
  );
894
895
  }
895
896
  function useFiles(query = {}, options = {}) {
@@ -897,13 +898,15 @@ function useFiles(query = {}, options = {}) {
897
898
  }
898
899
  function useStorageInfoFor(client) {
899
900
  const snapshot = useClientSnapshot(client);
900
- return useAsyncResult(
901
+ const gate = useSignInGate(snapshot);
902
+ const result = useAsyncResult(
901
903
  () => client.storageInfo(client.resolveRepoId()),
902
904
  null,
903
905
  snapshot.isReady && snapshot.isSignedIn,
904
906
  snapshot.isReady,
905
- [snapshot.activeProfile?.id]
907
+ [snapshot.activeAccount?.id]
906
908
  );
909
+ return { ...result, error: result.error ?? gate };
907
910
  }
908
911
  function useStorageInfo() {
909
912
  return useStorageInfoFor(useRequiredClient());
@@ -913,33 +916,34 @@ function useStorageInfo() {
913
916
  import { useCallback as useCallback5 } from "react";
914
917
  function useMountsFor(client, query = {}) {
915
918
  const snapshot = useClientSnapshot(client);
919
+ const gate = useSignInGate(snapshot);
916
920
  const queryKey = JSON.stringify(query);
917
921
  const result = useAsyncResult(
918
922
  () => client.shares.mounts(query),
919
923
  [],
920
924
  snapshot.isReady && snapshot.isSignedIn,
921
925
  snapshot.isReady,
922
- [queryKey, snapshot.activeProfile?.id]
926
+ [queryKey, snapshot.activeAccount?.id]
923
927
  );
924
928
  const open = useCallback5(
925
929
  (mountId) => client.shares.mount(mountId),
926
930
  [client]
927
931
  );
928
932
  const manageUrl = useCallback5(() => client.shares.manageUrl(), [client]);
929
- const { data: mounts, ...state } = result;
930
- return { data: mounts, mounts, ...state, open, manageUrl };
933
+ return { ...result, error: result.error ?? gate, open, manageUrl };
931
934
  }
932
935
  function useMounts(query = {}) {
933
936
  return useMountsFor(useRequiredClient(), query);
934
937
  }
935
938
  function useOutgoingSharesFor(client) {
936
939
  const snapshot = useClientSnapshot(client);
940
+ const gate = useSignInGate(snapshot);
937
941
  const result = useAsyncResult(
938
942
  () => client.shares.listOutgoing(),
939
943
  [],
940
944
  snapshot.isReady && snapshot.isSignedIn,
941
945
  snapshot.isReady,
942
- [snapshot.activeProfile?.id]
946
+ [snapshot.activeAccount?.id]
943
947
  );
944
948
  const create = useCallback5((input) => client.shares.createOutgoing(input), [client]);
945
949
  const get = useCallback5((id) => client.shares.getOutgoing(id), [client]);
@@ -947,11 +951,9 @@ function useOutgoingSharesFor(client) {
947
951
  const revoke = useCallback5((id) => client.shares.revoke(id), [client]);
948
952
  const getContactHandle = useCallback5((did) => client.shares.getContactHandle(did), [client]);
949
953
  const manageUrl = useCallback5(() => client.shares.manageUrl(), [client]);
950
- const { data: outgoingShares, ...state } = result;
951
954
  return {
952
- data: outgoingShares,
953
- outgoingShares,
954
- ...state,
955
+ ...result,
956
+ error: result.error ?? gate,
955
957
  create,
956
958
  get,
957
959
  cancel,
@@ -965,58 +967,35 @@ function useOutgoingShares() {
965
967
  }
966
968
 
967
969
  // src/hooks/query.ts
968
- import { useEffect as useEffect3, useMemo as useMemo6, useState as useState3, useSyncExternalStore as useSyncExternalStore4 } from "react";
969
- var subscribeNever = () => () => {
970
- };
971
- var zero2 = () => 0;
970
+ import { useEffect as useEffect3, useState as useState3 } from "react";
972
971
  function useQueryValueFor(client, collection, query = {}, options = {}) {
973
972
  const source = options.source ?? "default";
974
973
  const sourceKey = JSON.stringify(source);
975
974
  const queryKey = JSON.stringify(query);
976
975
  const snapshot = useClientSnapshot(client);
977
- const subscription = useMemo6(
978
- () => client.queryStore.collection(source, collection),
979
- // sourceKey captures semantic source identity rather than object identity.
980
- // eslint-disable-next-line react-hooks/exhaustive-deps
981
- [client, sourceKey, collection]
982
- );
983
- const version = useSyncExternalStore4(
984
- subscription?.subscribe ?? subscribeNever,
985
- subscription?.getSnapshot ?? zero2,
986
- zero2
987
- );
988
976
  const [result, setResult] = useState3({
989
977
  data: [],
990
978
  isLoading: true,
991
979
  error: null
992
980
  });
993
981
  useEffect3(() => {
994
- let active = true;
995
- if (!snapshot.isReady) {
996
- setResult((current) => ({ ...current, isLoading: true, error: null }));
997
- return () => {
998
- active = false;
999
- };
1000
- }
1001
982
  setResult((current) => ({ ...current, isLoading: true, error: null }));
1002
- const collectionApi = client.db(source).collection(collection);
1003
- const list = collectionApi.list;
1004
- void list.call(collectionApi, query).then(
1005
- (page) => {
1006
- if (active) setResult({ data: page.data, isLoading: false, error: null });
1007
- },
1008
- (error) => {
1009
- if (active) setResult((current) => ({
983
+ if (!snapshot.isReady) return;
984
+ try {
985
+ return client.db(source).collection(collection).watch(
986
+ query,
987
+ (page) => setResult({ data: page.data, isLoading: false, error: null }),
988
+ (error) => setResult((current) => ({
1010
989
  ...current,
1011
990
  isLoading: false,
1012
991
  error: toBasicError(error, "QUERY_FAILED")
1013
- }));
1014
- }
1015
- );
1016
- return () => {
1017
- active = false;
1018
- };
1019
- }, [client, collection, sourceKey, queryKey, version, snapshot.isReady]);
992
+ }))
993
+ );
994
+ } catch (error) {
995
+ setResult((current) => ({ ...current, isLoading: false, error: toBasicError(error, "QUERY_FAILED") }));
996
+ return;
997
+ }
998
+ }, [client, collection, sourceKey, queryKey, snapshot.isReady]);
1020
999
  return result;
1021
1000
  }
1022
1001
  function useQueryFor(client, collection, query = {}, options = {}) {
@@ -1026,6 +1005,47 @@ function useQuery(collection, query = {}, options = {}) {
1026
1005
  return useQueryValueFor(useRequiredClient(), collection, query, options);
1027
1006
  }
1028
1007
 
1008
+ // src/hooks/profile.ts
1009
+ import {
1010
+ BasicError as BasicError5
1011
+ } from "@basictech/core";
1012
+ function useProjectProfileFor(client, enabled = true) {
1013
+ const snapshot = useClientSnapshot(client);
1014
+ const accountId = snapshot.activeAccount?.id;
1015
+ const result = useAsyncResult(
1016
+ async () => ({ accountId, profile: await client.getProjectProfile() }),
1017
+ null,
1018
+ enabled && snapshot.isReady && snapshot.isSignedIn,
1019
+ snapshot.isReady,
1020
+ [snapshot.activeAccount?.id]
1021
+ );
1022
+ const checkAccount = () => {
1023
+ if (client.getSnapshot().activeAccount?.id !== accountId)
1024
+ throw new BasicError5("ACCOUNT_CHANGED");
1025
+ };
1026
+ return {
1027
+ ...result,
1028
+ data: enabled && snapshot.isSignedIn && result.data && result.data.accountId === accountId ? result.data.profile : null,
1029
+ async update(patch) {
1030
+ checkAccount();
1031
+ const profile = await client.updateProjectProfile(patch);
1032
+ checkAccount();
1033
+ result.refresh();
1034
+ return profile;
1035
+ },
1036
+ async reset() {
1037
+ checkAccount();
1038
+ const profile = await client.resetProjectProfile();
1039
+ checkAccount();
1040
+ result.refresh();
1041
+ return profile;
1042
+ }
1043
+ };
1044
+ }
1045
+ function useProjectProfile(enabled = true) {
1046
+ return useProjectProfileFor(useRequiredClient(), enabled);
1047
+ }
1048
+
1029
1049
  // src/create-basic.tsx
1030
1050
  import { jsx as jsx2 } from "react/jsx-runtime";
1031
1051
  function createBasic(config) {
@@ -1033,32 +1053,1066 @@ function createBasic(config) {
1033
1053
  function Provider({ children, renderWhileLoading }) {
1034
1054
  return /* @__PURE__ */ jsx2(BasicProvider, { client, renderWhileLoading, children });
1035
1055
  }
1056
+ function useBound() {
1057
+ if (useContext2(BasicClientContext) !== client) {
1058
+ throw new Error("Bound Basic hooks must be used within their own <basic.Provider>");
1059
+ }
1060
+ return client;
1061
+ }
1036
1062
  function useBoundDb(source = "default") {
1037
- return client.db(source);
1063
+ return useBound().db(source);
1038
1064
  }
1039
1065
  return {
1040
1066
  client,
1041
1067
  Provider,
1042
- useBasic: () => useBasicFor(client),
1043
- useAuth: () => useAuthFor(client),
1044
- useAccounts: () => useAccountsFor(client),
1068
+ useBasic: () => useBasicFor(useBound()),
1069
+ useAuth: () => useAuthFor(useBound()),
1070
+ useAccounts: () => useAccountsFor(useBound()),
1045
1071
  useDb: useBoundDb,
1046
- useCollection: (name, options = {}) => useCollectionFor(client, name, options),
1047
- useQuery: (collection, query = {}, options = {}) => useQueryFor(client, collection, query, options),
1048
- useSyncStatus: (source) => useSyncStatusFor(client, source),
1049
- useSchemaStatus: (source = "default") => useSchemaStatusFor(client, source),
1050
- useRepos: () => useReposFor(client),
1051
- useFiles: (query = {}, options = {}) => useFilesFor(client, query, options),
1052
- useStorageInfo: () => useStorageInfoFor(client),
1053
- useMounts: (query = {}) => useMountsFor(client, query),
1054
- useOutgoingShares: () => useOutgoingSharesFor(client)
1072
+ useCollection: (name, options = {}) => useCollectionFor(useBound(), name, options),
1073
+ useQuery: (collection, query = {}, options = {}) => useQueryFor(useBound(), collection, query, options),
1074
+ useSyncStatus: (source) => useSyncStatusFor(useBound(), source),
1075
+ useSchemaStatus: (source = "default") => useSchemaStatusFor(useBound(), source),
1076
+ useRepos: () => useReposFor(useBound()),
1077
+ useProjectProfile: (enabled) => useProjectProfileFor(useBound(), enabled),
1078
+ useFiles: (query = {}, options = {}) => useFilesFor(useBound(), query, options),
1079
+ useStorageInfo: () => useStorageInfoFor(useBound()),
1080
+ useMounts: (query = {}) => useMountsFor(useBound(), query),
1081
+ useOutgoingShares: () => useOutgoingSharesFor(useBound())
1082
+ };
1083
+ }
1084
+
1085
+ // src/components.tsx
1086
+ import {
1087
+ createContext as createContext2,
1088
+ Fragment,
1089
+ forwardRef,
1090
+ isValidElement,
1091
+ useContext as useContext3,
1092
+ useEffect as useEffect4,
1093
+ useRef as useRef3,
1094
+ useState as useState4
1095
+ } from "react";
1096
+ import { Avatar } from "@base-ui/react/avatar";
1097
+ import { Dialog } from "@base-ui/react/dialog";
1098
+ import { Menu } from "@base-ui/react/menu";
1099
+ import { Tabs } from "@base-ui/react/tabs";
1100
+ import { BasicError as BasicError6 } from "@basictech/core";
1101
+ import { Fragment as Fragment2, jsx as jsx3, jsxs } from "react/jsx-runtime";
1102
+ var AppearanceContext = createContext2({});
1103
+ function appearanceStyle(appearance) {
1104
+ return {
1105
+ ...appearance.accent ? { "--basic-accent": appearance.accent } : {},
1106
+ ...appearance.radius ? { "--basic-radius": appearance.radius } : {}
1107
+ };
1108
+ }
1109
+ function BasicUIProvider({
1110
+ appearance = {},
1111
+ children
1112
+ }) {
1113
+ const parent = useContext3(AppearanceContext);
1114
+ const value = { ...parent, ...appearance };
1115
+ return /* @__PURE__ */ jsx3(AppearanceContext.Provider, { value, children: /* @__PURE__ */ jsx3(
1116
+ "div",
1117
+ {
1118
+ className: "basic-ui",
1119
+ "data-theme": value.theme ?? "light",
1120
+ "data-density": value.density ?? "comfortable",
1121
+ style: appearanceStyle(value),
1122
+ children
1123
+ }
1124
+ ) });
1125
+ }
1126
+ function useAppearanceProps() {
1127
+ const appearance = useContext3(AppearanceContext);
1128
+ return {
1129
+ "data-theme": appearance.theme ?? "light",
1130
+ "data-density": appearance.density ?? "comfortable",
1131
+ style: appearanceStyle(appearance)
1132
+ };
1133
+ }
1134
+ var BasicButton = forwardRef(
1135
+ function BasicButton2({ variant = "outline", className = "", type = "button", ...props }, ref) {
1136
+ return /* @__PURE__ */ jsx3(
1137
+ "button",
1138
+ {
1139
+ ...props,
1140
+ ref,
1141
+ type,
1142
+ className: `basic-button ${className}`,
1143
+ "data-variant": variant
1144
+ }
1145
+ );
1146
+ }
1147
+ );
1148
+ function useAction() {
1149
+ const [pending, setPending] = useState4(false);
1150
+ const [error, setError] = useState4(null);
1151
+ const busy = useRef3(false);
1152
+ const mounted = useRef3(true);
1153
+ useEffect4(() => {
1154
+ mounted.current = true;
1155
+ return () => {
1156
+ mounted.current = false;
1157
+ };
1158
+ }, []);
1159
+ async function run(action) {
1160
+ if (busy.current) return;
1161
+ busy.current = true;
1162
+ setPending(true);
1163
+ setError(null);
1164
+ try {
1165
+ await action();
1166
+ } catch (cause) {
1167
+ if (mounted.current)
1168
+ setError(
1169
+ cause instanceof BasicError6 && cause.code === "REDIRECT_URI_NOT_REGISTERED" ? "Sign-in is unavailable on this URL. Ask the app owner to register its callback URL in project settings." : cause instanceof Error ? cause.message : "The action failed. Please try again."
1170
+ );
1171
+ } finally {
1172
+ busy.current = false;
1173
+ if (mounted.current) setPending(false);
1174
+ }
1175
+ }
1176
+ return { pending, error, run, clearError: () => setError(null) };
1177
+ }
1178
+ function AuthButton({
1179
+ auth: supplied,
1180
+ input,
1181
+ signOut = false,
1182
+ children,
1183
+ disabled,
1184
+ onClick,
1185
+ ...props
1186
+ }) {
1187
+ const live = useAuth();
1188
+ const auth = supplied ?? live;
1189
+ const action = useAction();
1190
+ const expired = auth.status === "expired";
1191
+ return /* @__PURE__ */ jsxs("span", { className: "basic-action", children: [
1192
+ /* @__PURE__ */ jsx3(
1193
+ BasicButton,
1194
+ {
1195
+ ...props,
1196
+ disabled: disabled || !auth.isReady || action.pending || (signOut ? !auth.isSignedIn && !expired : auth.isSignedIn && !expired),
1197
+ "aria-busy": action.pending,
1198
+ onClick: (event) => {
1199
+ onClick?.(event);
1200
+ if (!event.defaultPrevented)
1201
+ void action.run(
1202
+ () => signOut ? auth.signOut() : auth.signIn(input)
1203
+ );
1204
+ },
1205
+ children: action.pending ? "Please wait\u2026" : children ?? (signOut ? "Sign out" : expired ? "Sign in again" : "Sign in")
1206
+ }
1207
+ ),
1208
+ /* @__PURE__ */ jsx3(ActionError, { error: action.error, onClose: action.clearError })
1209
+ ] });
1210
+ }
1211
+ function SignInButton(props) {
1212
+ return /* @__PURE__ */ jsx3(AuthButton, { ...props });
1213
+ }
1214
+ function SignOutButton(props) {
1215
+ return /* @__PURE__ */ jsx3(AuthButton, { ...props, signOut: true });
1216
+ }
1217
+ function UserAvatar({
1218
+ accounts,
1219
+ auth,
1220
+ projectProfile,
1221
+ allowAddAccount,
1222
+ ...avatarProps
1223
+ }) {
1224
+ return /* @__PURE__ */ jsx3(
1225
+ UserMenu,
1226
+ {
1227
+ accounts,
1228
+ auth,
1229
+ projectProfile,
1230
+ allowAddAccount,
1231
+ sync: avatarProps.sync,
1232
+ showSyncBadge: avatarProps.showSyncBadge,
1233
+ avatarProps
1234
+ }
1235
+ );
1236
+ }
1237
+ function AccountAvatar({
1238
+ profile: supplied,
1239
+ size = 32,
1240
+ showSyncBadge,
1241
+ sync,
1242
+ className = "",
1243
+ style,
1244
+ ...props
1245
+ }) {
1246
+ const accounts = useAccounts();
1247
+ const client = useRequiredClient();
1248
+ const profile = supplied === void 0 ? accounts.activeAccount : supplied;
1249
+ const name = profile?.name || (profile?.kind === "anon" ? "Local account" : profile?.handle || profile?.email || "Guest");
1250
+ const initials = name.trim().split(/\s+/).slice(0, 2).map((part) => Array.from(part)[0]).join("").toUpperCase();
1251
+ const avatar = /* @__PURE__ */ jsxs(
1252
+ Avatar.Root,
1253
+ {
1254
+ ...props,
1255
+ className: `basic-avatar ${className}`,
1256
+ style: { width: size, height: size, ...style },
1257
+ role: "img",
1258
+ "aria-label": props["aria-label"] ?? name,
1259
+ children: [
1260
+ profile?.picture && /* @__PURE__ */ jsx3(
1261
+ Avatar.Image,
1262
+ {
1263
+ src: profile.picture,
1264
+ alt: "",
1265
+ className: "basic-avatar-image",
1266
+ referrerPolicy: "no-referrer"
1267
+ }
1268
+ ),
1269
+ /* @__PURE__ */ jsx3(Avatar.Fallback, { className: "basic-avatar-fallback", children: initials || "?" })
1270
+ ]
1271
+ }
1272
+ );
1273
+ return showSyncBadge ?? client.mode === "sync" ? /* @__PURE__ */ jsxs("span", { className: "basic-avatar-container", children: [
1274
+ avatar,
1275
+ /* @__PURE__ */ jsx3(SyncStatus, { sync, className: "basic-avatar-sync" })
1276
+ ] }) : avatar;
1277
+ }
1278
+ function AuthStatus({
1279
+ auth: supplied,
1280
+ className = "",
1281
+ ...props
1282
+ }) {
1283
+ const live = useAuth();
1284
+ const auth = supplied ?? live;
1285
+ const expired = auth.status === "expired";
1286
+ const label = !auth.isReady || auth.status === "bootstrapping" ? "Checking session" : expired ? "Session expired" : auth.status === "recovering" ? "Reconnecting session" : auth.isSignedIn ? "Signed in" : auth.isAnonymous ? "Guest \xB7 local only" : "Signed out";
1287
+ return /* @__PURE__ */ jsxs(
1288
+ "span",
1289
+ {
1290
+ ...props,
1291
+ role: "status",
1292
+ className: `basic-status ${className}`,
1293
+ "data-tone": expired || auth.error ? "warning" : auth.isSignedIn ? "success" : "neutral",
1294
+ children: [
1295
+ /* @__PURE__ */ jsx3("span", { className: "basic-dot" }),
1296
+ label
1297
+ ]
1298
+ }
1299
+ );
1300
+ }
1301
+ function SyncStatus({
1302
+ sync: supplied,
1303
+ source,
1304
+ className = "",
1305
+ ...props
1306
+ }) {
1307
+ const live = useSyncStatus(source);
1308
+ const client = useRequiredClient();
1309
+ const sync = supplied ?? live;
1310
+ if (client.mode === "rest") return null;
1311
+ const issues = sync.rejected.length + sync.conflicts.length;
1312
+ const labels = {
1313
+ idle: "Idle",
1314
+ bootstrapping: "Starting sync",
1315
+ connecting: "Connecting",
1316
+ online: "Synced",
1317
+ offline: "Offline",
1318
+ stopped: "Sync stopped",
1319
+ error: "Sync error",
1320
+ local: "Local only",
1321
+ stale: "Sync stale",
1322
+ ended: "Access ended"
1055
1323
  };
1324
+ const label = issues ? `${issues} sync ${issues === 1 ? "issue" : "issues"}` : sync.status === "online" && sync.pendingCount ? "Syncing" : labels[sync.status];
1325
+ return /* @__PURE__ */ jsxs(
1326
+ "span",
1327
+ {
1328
+ ...props,
1329
+ role: "status",
1330
+ title: props.title ?? `${label}${sync.pendingCount > 0 ? ` \xB7 ${sync.pendingCount} pending` : ""}`,
1331
+ className: `basic-status ${className}`,
1332
+ "data-tone": issues || ["error", "stale", "ended"].includes(sync.status) ? "warning" : sync.status === "online" && !sync.pendingCount ? "success" : ["bootstrapping", "connecting"].includes(sync.status) || sync.status === "online" && sync.pendingCount > 0 ? "progress" : "neutral",
1333
+ children: [
1334
+ /* @__PURE__ */ jsx3("span", { className: "basic-dot" }),
1335
+ label,
1336
+ sync.pendingCount > 0 && ` \xB7 ${sync.pendingCount} pending`
1337
+ ]
1338
+ }
1339
+ );
1340
+ }
1341
+ function displayHandle(handle) {
1342
+ return handle ? `@${handle.replace(/^@+/, "")}` : "";
1343
+ }
1344
+ function UserButton(props) {
1345
+ return /* @__PURE__ */ jsx3(UserMenu, { ...props, trigger: "button" });
1346
+ }
1347
+ function UserMenu({
1348
+ accounts: supplied,
1349
+ auth: suppliedAuth,
1350
+ sync,
1351
+ showSyncStatus = true,
1352
+ showSyncBadge,
1353
+ shape = "rounded",
1354
+ trigger = "avatar",
1355
+ avatarProps,
1356
+ projectProfile,
1357
+ allowAddAccount = true,
1358
+ className = "",
1359
+ accountSettingsUrl
1360
+ }) {
1361
+ const liveAccounts = useAccounts();
1362
+ const liveAuth = useAuth();
1363
+ const accounts = supplied ?? liveAccounts;
1364
+ const auth = suppliedAuth ?? liveAuth;
1365
+ const action = useAction();
1366
+ const [settingsOpen, setSettingsOpen] = useState4(false);
1367
+ const [clearAccountId, setClearAccountId] = useState4(null);
1368
+ const triggerRef = useRef3(null);
1369
+ const appearance = useAppearanceProps();
1370
+ const expired = auth.status === "expired";
1371
+ const active = accounts.activeAccount;
1372
+ const otherAccounts = accounts.accounts.filter(
1373
+ (account) => account.id !== active?.id
1374
+ );
1375
+ const name = active?.name || active?.kind !== "anon" && displayHandle(active?.handle) || "Local account";
1376
+ return /* @__PURE__ */ jsxs("span", { className: `basic-action ${className}`, children: [
1377
+ /* @__PURE__ */ jsxs(Menu.Root, { children: [
1378
+ /* @__PURE__ */ jsxs(
1379
+ Menu.Trigger,
1380
+ {
1381
+ ref: triggerRef,
1382
+ className: `basic-button basic-account-trigger basic-trigger-${trigger}`,
1383
+ "data-shape": shape,
1384
+ disabled: !auth.isReady || action.pending,
1385
+ "aria-label": "Open user menu",
1386
+ children: [
1387
+ /* @__PURE__ */ jsx3(
1388
+ AccountAvatar,
1389
+ {
1390
+ profile: accounts.activeAccount,
1391
+ sync,
1392
+ showSyncBadge,
1393
+ ...avatarProps
1394
+ }
1395
+ ),
1396
+ trigger === "button" && /* @__PURE__ */ jsxs(Fragment2, { children: [
1397
+ /* @__PURE__ */ jsxs("span", { className: "basic-identity", children: [
1398
+ /* @__PURE__ */ jsx3("strong", { children: name }),
1399
+ active?.kind !== "anon" && active?.handle && /* @__PURE__ */ jsx3("small", { children: displayHandle(active.handle) })
1400
+ ] }),
1401
+ /* @__PURE__ */ jsx3(
1402
+ "svg",
1403
+ {
1404
+ className: "basic-chevron",
1405
+ width: "16",
1406
+ height: "16",
1407
+ viewBox: "0 0 24 24",
1408
+ fill: "none",
1409
+ stroke: "currentColor",
1410
+ strokeWidth: "2",
1411
+ strokeLinecap: "round",
1412
+ strokeLinejoin: "round",
1413
+ "aria-hidden": "true",
1414
+ children: /* @__PURE__ */ jsx3("path", { d: "m6 9 6 6 6-6" })
1415
+ }
1416
+ )
1417
+ ] })
1418
+ ]
1419
+ }
1420
+ ),
1421
+ /* @__PURE__ */ jsx3(Menu.Portal, { children: /* @__PURE__ */ jsx3(Menu.Positioner, { sideOffset: 8, className: "basic-positioner", children: /* @__PURE__ */ jsxs(Menu.Popup, { ...appearance, className: "basic-ui basic-menu", children: [
1422
+ /* @__PURE__ */ jsxs("div", { className: "basic-menu-profile", children: [
1423
+ /* @__PURE__ */ jsx3(
1424
+ AccountAvatar,
1425
+ {
1426
+ profile: active,
1427
+ size: 48,
1428
+ sync,
1429
+ showSyncBadge: false
1430
+ }
1431
+ ),
1432
+ /* @__PURE__ */ jsxs("span", { className: "basic-identity", children: [
1433
+ /* @__PURE__ */ jsx3("strong", { children: active ? name : "No active account" }),
1434
+ active?.kind !== "anon" && active?.handle && /* @__PURE__ */ jsx3("small", { children: displayHandle(active.handle) }),
1435
+ /* @__PURE__ */ jsxs("span", { className: "basic-account-statuses", children: [
1436
+ expired && /* @__PURE__ */ jsx3("span", { className: "basic-status", "data-tone": "warning", children: "Expired" }),
1437
+ showSyncStatus && /* @__PURE__ */ jsx3(SyncStatus, { sync })
1438
+ ] })
1439
+ ] })
1440
+ ] }),
1441
+ expired && /* @__PURE__ */ jsx3(
1442
+ Menu.Item,
1443
+ {
1444
+ className: "basic-menu-item basic-menu-reauth",
1445
+ disabled: action.pending,
1446
+ onClick: () => void action.run(() => auth.signIn()),
1447
+ children: "Sign in again"
1448
+ }
1449
+ ),
1450
+ /* @__PURE__ */ jsxs(
1451
+ Menu.Item,
1452
+ {
1453
+ className: "basic-menu-item basic-manage-account",
1454
+ onClick: () => setSettingsOpen(true),
1455
+ children: [
1456
+ /* @__PURE__ */ jsxs(
1457
+ "svg",
1458
+ {
1459
+ className: "basic-menu-icon",
1460
+ width: "16",
1461
+ height: "16",
1462
+ viewBox: "0 0 24 24",
1463
+ fill: "none",
1464
+ stroke: "currentColor",
1465
+ strokeWidth: "1.75",
1466
+ strokeLinecap: "round",
1467
+ strokeLinejoin: "round",
1468
+ "aria-hidden": "true",
1469
+ children: [
1470
+ /* @__PURE__ */ jsx3("circle", { cx: "12", cy: "7", r: "4" }),
1471
+ /* @__PURE__ */ jsx3("path", { d: "M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2" })
1472
+ ]
1473
+ }
1474
+ ),
1475
+ "Manage account"
1476
+ ]
1477
+ }
1478
+ ),
1479
+ (auth.isSignedIn || expired || active?.kind === "anon") && /* @__PURE__ */ jsxs(
1480
+ Menu.Item,
1481
+ {
1482
+ className: "basic-menu-item basic-menu-signout",
1483
+ disabled: action.pending,
1484
+ onClick: () => active?.kind === "anon" ? setClearAccountId(active.id) : void action.run(() => auth.signOut()),
1485
+ children: [
1486
+ /* @__PURE__ */ jsx3(
1487
+ "svg",
1488
+ {
1489
+ className: "basic-menu-icon",
1490
+ width: "16",
1491
+ height: "16",
1492
+ viewBox: "0 0 24 24",
1493
+ fill: "none",
1494
+ stroke: "currentColor",
1495
+ strokeWidth: "1.75",
1496
+ strokeLinecap: "round",
1497
+ strokeLinejoin: "round",
1498
+ "aria-hidden": "true",
1499
+ children: /* @__PURE__ */ jsx3("path", { d: "M9 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h4M16 17l5-5-5-5M21 12H9" })
1500
+ }
1501
+ ),
1502
+ active?.kind === "anon" ? "Clear account" : "Sign out"
1503
+ ]
1504
+ }
1505
+ ),
1506
+ otherAccounts.length > 0 && /* @__PURE__ */ jsxs(Fragment2, { children: [
1507
+ /* @__PURE__ */ jsx3(Menu.Separator, { className: "basic-separator" }),
1508
+ /* @__PURE__ */ jsx3("div", { className: "basic-menu-heading", children: "Switch accounts" })
1509
+ ] }),
1510
+ otherAccounts.map((account) => /* @__PURE__ */ jsxs(
1511
+ Menu.Item,
1512
+ {
1513
+ className: "basic-menu-item",
1514
+ disabled: action.pending,
1515
+ onClick: () => void action.run(() => accounts.switchAccount(account.id)),
1516
+ children: [
1517
+ /* @__PURE__ */ jsx3(
1518
+ AccountAvatar,
1519
+ {
1520
+ profile: account,
1521
+ size: 32,
1522
+ sync,
1523
+ showSyncBadge: false
1524
+ }
1525
+ ),
1526
+ /* @__PURE__ */ jsxs("span", { className: "basic-identity", children: [
1527
+ /* @__PURE__ */ jsx3("strong", { children: account.name || account.kind !== "anon" && displayHandle(account.handle) || (account.kind === "anon" ? "Local account" : "Account") }),
1528
+ account.kind !== "anon" && account.handle && /* @__PURE__ */ jsx3("small", { children: displayHandle(account.handle) })
1529
+ ] }),
1530
+ /* @__PURE__ */ jsx3("span", { className: "basic-account-statuses", children: account.auth.status === "expired" ? /* @__PURE__ */ jsx3("span", { className: "basic-status", "data-tone": "warning", children: "Expired" }) : null })
1531
+ ]
1532
+ },
1533
+ account.id
1534
+ )),
1535
+ (allowAddAccount || !auth.isSignedIn && !expired) && /* @__PURE__ */ jsx3(
1536
+ Menu.Item,
1537
+ {
1538
+ className: "basic-menu-item",
1539
+ disabled: action.pending,
1540
+ onClick: () => void action.run(
1541
+ () => allowAddAccount ? accounts.addAccount({ signIn: true }) : auth.signIn()
1542
+ ),
1543
+ children: "\uFF0B Add account"
1544
+ }
1545
+ )
1546
+ ] }) }) })
1547
+ ] }),
1548
+ /* @__PURE__ */ jsx3(
1549
+ ActionError,
1550
+ {
1551
+ error: action.error,
1552
+ onClose: action.clearError,
1553
+ finalFocus: triggerRef
1554
+ }
1555
+ ),
1556
+ /* @__PURE__ */ jsx3(
1557
+ Modal,
1558
+ {
1559
+ open: clearAccountId !== null,
1560
+ onOpenChange: (open) => {
1561
+ if (!open) setClearAccountId(null);
1562
+ },
1563
+ trigger: null,
1564
+ finalFocus: triggerRef,
1565
+ title: "Clear local account?",
1566
+ description: "This deletes this account\u2019s local data, including unsynced changes. This cannot be undone.",
1567
+ children: /* @__PURE__ */ jsxs("div", { className: "basic-actions", children: [
1568
+ /* @__PURE__ */ jsx3(
1569
+ BasicButton,
1570
+ {
1571
+ disabled: action.pending,
1572
+ onClick: () => setClearAccountId(null),
1573
+ children: "Cancel"
1574
+ }
1575
+ ),
1576
+ /* @__PURE__ */ jsx3(
1577
+ BasicButton,
1578
+ {
1579
+ disabled: action.pending,
1580
+ onClick: () => {
1581
+ const id = clearAccountId;
1582
+ if (!id) return;
1583
+ setClearAccountId(null);
1584
+ void action.run(() => accounts.removeAccount(id));
1585
+ },
1586
+ children: "Clear account"
1587
+ }
1588
+ )
1589
+ ] })
1590
+ }
1591
+ ),
1592
+ /* @__PURE__ */ jsx3(
1593
+ AccountSettingsModal,
1594
+ {
1595
+ auth,
1596
+ accounts,
1597
+ open: settingsOpen,
1598
+ onOpenChange: setSettingsOpen,
1599
+ trigger: null,
1600
+ finalFocus: triggerRef,
1601
+ accountSettingsUrl,
1602
+ projectProfile,
1603
+ sync
1604
+ }
1605
+ )
1606
+ ] });
1607
+ }
1608
+ function ActionError({
1609
+ error,
1610
+ onClose,
1611
+ finalFocus
1612
+ }) {
1613
+ return /* @__PURE__ */ jsxs(
1614
+ Modal,
1615
+ {
1616
+ open: !!error,
1617
+ onOpenChange: (open) => {
1618
+ if (!open) onClose();
1619
+ },
1620
+ trigger: null,
1621
+ finalFocus,
1622
+ title: "Unable to complete action",
1623
+ description: "Your request could not be completed.",
1624
+ children: [
1625
+ /* @__PURE__ */ jsx3("p", { role: "alert", className: "basic-error", children: error }),
1626
+ /* @__PURE__ */ jsx3(BasicButton, { onClick: onClose, children: "Dismiss" })
1627
+ ]
1628
+ }
1629
+ );
1630
+ }
1631
+ function Modal({
1632
+ open,
1633
+ onOpenChange,
1634
+ trigger,
1635
+ finalFocus,
1636
+ title,
1637
+ description,
1638
+ header,
1639
+ sectionLabel,
1640
+ children
1641
+ }) {
1642
+ const appearance = useAppearanceProps();
1643
+ return /* @__PURE__ */ jsxs(Dialog.Root, { open, onOpenChange, children: [
1644
+ trigger !== null && (isValidElement(trigger) ? /* @__PURE__ */ jsx3(Dialog.Trigger, { render: trigger }) : /* @__PURE__ */ jsx3(Dialog.Trigger, { render: /* @__PURE__ */ jsx3(BasicButton, {}), children: trigger ?? title })),
1645
+ /* @__PURE__ */ jsxs(Dialog.Portal, { children: [
1646
+ /* @__PURE__ */ jsx3(Dialog.Backdrop, { className: "basic-backdrop" }),
1647
+ /* @__PURE__ */ jsxs(
1648
+ Dialog.Popup,
1649
+ {
1650
+ ...appearance,
1651
+ finalFocus,
1652
+ className: "basic-ui basic-dialog",
1653
+ children: [
1654
+ /* @__PURE__ */ jsxs(
1655
+ "div",
1656
+ {
1657
+ className: `basic-dialog-heading${header ? " basic-dialog-heading-overlay" : ""}`,
1658
+ children: [
1659
+ /* @__PURE__ */ jsx3(Dialog.Title, { className: header ? "basic-sr-only" : "basic-title", children: title }),
1660
+ /* @__PURE__ */ jsx3(
1661
+ Dialog.Close,
1662
+ {
1663
+ className: "basic-button",
1664
+ "aria-label": `Close ${title.toLowerCase()}`,
1665
+ children: "\u2715"
1666
+ }
1667
+ )
1668
+ ]
1669
+ }
1670
+ ),
1671
+ header,
1672
+ /* @__PURE__ */ jsx3(
1673
+ Dialog.Description,
1674
+ {
1675
+ className: header ? "basic-sr-only" : "basic-muted",
1676
+ children: description
1677
+ }
1678
+ ),
1679
+ sectionLabel && /* @__PURE__ */ jsx3("div", { className: "basic-modal-section", children: sectionLabel }),
1680
+ children
1681
+ ]
1682
+ }
1683
+ )
1684
+ ] })
1685
+ ] });
1686
+ }
1687
+ function AccountSettingsModal({
1688
+ auth: suppliedAuth,
1689
+ accounts: suppliedAccounts,
1690
+ accountSettingsUrl,
1691
+ projectProfile,
1692
+ sync,
1693
+ children,
1694
+ ...props
1695
+ }) {
1696
+ const liveAuth = useAuth();
1697
+ const liveAccounts = useAccounts();
1698
+ const auth = suppliedAuth ?? liveAuth;
1699
+ const accounts = suppliedAccounts ?? liveAccounts;
1700
+ const liveSync = useSyncStatus();
1701
+ const syncState = sync ?? liveSync;
1702
+ const client = useRequiredClient();
1703
+ const account = accounts.activeAccount;
1704
+ const copyAction = useAction();
1705
+ const [copied, setCopied] = useState4(null);
1706
+ const details = [
1707
+ [
1708
+ "Account DID",
1709
+ account?.kind === "anon" ? "Not assigned (local account)" : account?.did ?? auth.did ?? "Unavailable"
1710
+ ],
1711
+ [
1712
+ "PDS URL",
1713
+ account?.kind === "anon" ? "Not connected (local account)" : auth.user?.pds_url ?? "Unavailable"
1714
+ ],
1715
+ ["Local account ID", account?.id ?? "None"],
1716
+ ["Account kind", account?.kind ?? "None"],
1717
+ ["Storage prefix", account?.storagePrefix ?? "None"],
1718
+ ["Client mode", client.mode],
1719
+ ["Auth state", auth.status],
1720
+ ["Ready", auth.isReady ? "Yes" : "No"],
1721
+ ["Can write", auth.canWrite ? "Yes" : "No"],
1722
+ ["Read-only reason", auth.readOnlyReason ?? "None"],
1723
+ ["Auth error code", auth.error?.code ?? "None"],
1724
+ ["Sync state", syncState.status],
1725
+ ["Pending writes", String(syncState.pendingCount)],
1726
+ ["Rejected writes", String(syncState.rejected.length)],
1727
+ ["Conflicts", String(syncState.conflicts.length)]
1728
+ ];
1729
+ const diagnostics = JSON.stringify(Object.fromEntries(details), null, 2);
1730
+ return /* @__PURE__ */ jsx3(
1731
+ Modal,
1732
+ {
1733
+ ...props,
1734
+ title: "Account settings",
1735
+ description: "Your identity and session on this device.",
1736
+ header: /* @__PURE__ */ jsx3(AccountHeader, { account: accounts.activeAccount, sync }),
1737
+ children: /* @__PURE__ */ jsxs(Tabs.Root, { defaultValue: "profile", children: [
1738
+ /* @__PURE__ */ jsxs(
1739
+ Tabs.List,
1740
+ {
1741
+ className: "basic-tabs",
1742
+ "aria-label": "Account settings sections",
1743
+ children: [
1744
+ /* @__PURE__ */ jsx3(Tabs.Tab, { value: "profile", children: "Profile" }),
1745
+ /* @__PURE__ */ jsx3(Tabs.Tab, { value: "advanced", children: "Advanced" })
1746
+ ]
1747
+ }
1748
+ ),
1749
+ /* @__PURE__ */ jsxs(Tabs.Panel, { value: "profile", keepMounted: true, children: [
1750
+ auth.isSignedIn && /* @__PURE__ */ jsx3(
1751
+ ProjectProfileEditor,
1752
+ {
1753
+ profile: projectProfile,
1754
+ canWrite: auth.canWrite
1755
+ },
1756
+ accounts.activeAccount?.id ?? auth.did
1757
+ ),
1758
+ accountSettingsUrl && /* @__PURE__ */ jsx3("a", { className: "basic-link", href: accountSettingsUrl, children: "Manage profile and security \u2197" }),
1759
+ children,
1760
+ !auth.isSignedIn && /* @__PURE__ */ jsx3("div", { className: "basic-actions", children: /* @__PURE__ */ jsx3(SignInButton, { auth }) })
1761
+ ] }),
1762
+ /* @__PURE__ */ jsxs(Tabs.Panel, { value: "advanced", children: [
1763
+ /* @__PURE__ */ jsx3("p", { className: "basic-muted", children: "Read-only diagnostics for this account." }),
1764
+ /* @__PURE__ */ jsx3("dl", { className: "basic-details", children: details.map(([label, value]) => /* @__PURE__ */ jsxs(Fragment, { children: [
1765
+ /* @__PURE__ */ jsx3("dt", { children: label }),
1766
+ /* @__PURE__ */ jsx3("dd", { children: value })
1767
+ ] }, label)) }),
1768
+ /* @__PURE__ */ jsxs("div", { className: "basic-actions", children: [
1769
+ /* @__PURE__ */ jsx3(
1770
+ BasicButton,
1771
+ {
1772
+ disabled: copyAction.pending,
1773
+ onClick: () => {
1774
+ setCopied(null);
1775
+ void copyAction.run(async () => {
1776
+ if (!navigator.clipboard?.writeText)
1777
+ throw new Error("Clipboard is unavailable in this browser.");
1778
+ await navigator.clipboard.writeText(diagnostics);
1779
+ setCopied(diagnostics);
1780
+ });
1781
+ },
1782
+ children: "Copy to clipboard"
1783
+ }
1784
+ ),
1785
+ copied === diagnostics && /* @__PURE__ */ jsx3("span", { role: "status", className: "basic-muted", children: "Copied" })
1786
+ ] }),
1787
+ copyAction.error && /* @__PURE__ */ jsx3("p", { role: "alert", className: "basic-error", children: copyAction.error })
1788
+ ] })
1789
+ ] })
1790
+ }
1791
+ );
1792
+ }
1793
+ function AccountHeader({
1794
+ account,
1795
+ sync
1796
+ }) {
1797
+ return /* @__PURE__ */ jsxs("div", { className: "basic-profile", children: [
1798
+ /* @__PURE__ */ jsx3(
1799
+ AccountAvatar,
1800
+ {
1801
+ profile: account,
1802
+ size: 72,
1803
+ sync,
1804
+ showSyncBadge: false
1805
+ }
1806
+ ),
1807
+ /* @__PURE__ */ jsxs("span", { className: "basic-identity", children: [
1808
+ /* @__PURE__ */ jsx3("strong", { children: account?.name || account?.kind !== "anon" && displayHandle(account?.handle) || "Local account" }),
1809
+ account?.kind !== "anon" && account?.handle && /* @__PURE__ */ jsx3("small", { children: displayHandle(account.handle) }),
1810
+ /* @__PURE__ */ jsx3("span", { className: "basic-account-statuses", children: /* @__PURE__ */ jsx3(SyncStatus, { sync }) })
1811
+ ] })
1812
+ ] });
1813
+ }
1814
+ function ProjectProfileEditor({
1815
+ profile: supplied,
1816
+ canWrite
1817
+ }) {
1818
+ const live = useProjectProfile(supplied === void 0);
1819
+ const profile = supplied ?? live;
1820
+ const action = useAction();
1821
+ const [draft, setDraft] = useState4({});
1822
+ const [saved, setSaved] = useState4(false);
1823
+ return /* @__PURE__ */ jsxs(
1824
+ "form",
1825
+ {
1826
+ className: "basic-profile-editor",
1827
+ onSubmit: (event) => {
1828
+ event.preventDefault();
1829
+ if (!canWrite || !Object.keys(draft).length) return;
1830
+ void action.run(async () => {
1831
+ await profile.update(draft);
1832
+ setDraft({});
1833
+ setSaved(true);
1834
+ });
1835
+ },
1836
+ children: [
1837
+ /* @__PURE__ */ jsx3("h3", { className: "basic-section-label", children: "Personal information" }),
1838
+ /* @__PURE__ */ jsx3("p", { className: "basic-muted", children: "Only this app sees these overrides. Clear a field to use your universal profile value." }),
1839
+ profile.isLoading ? /* @__PURE__ */ jsx3("p", { role: "status", children: "Loading project profile\u2026" }) : profile.error ? /* @__PURE__ */ jsxs(Fragment2, { children: [
1840
+ /* @__PURE__ */ jsx3("p", { role: "alert", className: "basic-error", children: profile.error.message }),
1841
+ /* @__PURE__ */ jsx3(BasicButton, { onClick: profile.refresh, children: "Retry profile" })
1842
+ ] }) : profile.data && /* @__PURE__ */ jsxs("fieldset", { disabled: !canWrite || action.pending, children: [
1843
+ /* @__PURE__ */ jsxs("label", { className: "basic-field", children: [
1844
+ "Display name",
1845
+ /* @__PURE__ */ jsx3(
1846
+ "input",
1847
+ {
1848
+ className: "basic-input",
1849
+ type: "text",
1850
+ maxLength: 64,
1851
+ value: ("name" in draft ? draft.name : profile.data?.profile.name) ?? "",
1852
+ onChange: (event) => {
1853
+ setSaved(false);
1854
+ setDraft((current) => ({
1855
+ ...current,
1856
+ name: event.target.value || null
1857
+ }));
1858
+ }
1859
+ }
1860
+ )
1861
+ ] }),
1862
+ /* @__PURE__ */ jsxs("div", { className: "basic-actions basic-modal-footer", children: [
1863
+ /* @__PURE__ */ jsx3(
1864
+ BasicButton,
1865
+ {
1866
+ variant: "ghost",
1867
+ onClick: () => {
1868
+ setDraft({});
1869
+ setSaved(false);
1870
+ },
1871
+ children: "Cancel"
1872
+ }
1873
+ ),
1874
+ /* @__PURE__ */ jsx3(
1875
+ BasicButton,
1876
+ {
1877
+ type: "submit",
1878
+ variant: "solid",
1879
+ disabled: !Object.keys(draft).length,
1880
+ children: "Save changes"
1881
+ }
1882
+ )
1883
+ ] })
1884
+ ] }),
1885
+ action.error && /* @__PURE__ */ jsx3("p", { role: "alert", className: "basic-error", children: action.error }),
1886
+ saved && /* @__PURE__ */ jsx3("p", { role: "status", children: "Project profile saved." })
1887
+ ]
1888
+ }
1889
+ );
1890
+ }
1891
+ function SharesModal({
1892
+ auth: supplied,
1893
+ accounts: suppliedAccounts,
1894
+ sync,
1895
+ shares,
1896
+ scope,
1897
+ repo = "default",
1898
+ ...props
1899
+ }) {
1900
+ const live = useAuth();
1901
+ const liveAccounts = useAccounts();
1902
+ const auth = supplied ?? live;
1903
+ return /* @__PURE__ */ jsx3(
1904
+ Modal,
1905
+ {
1906
+ ...props,
1907
+ title: "Shares",
1908
+ description: "Manage incoming invitations and share access to selected data.",
1909
+ header: /* @__PURE__ */ jsx3(
1910
+ AccountHeader,
1911
+ {
1912
+ account: (suppliedAccounts ?? liveAccounts).activeAccount,
1913
+ sync
1914
+ }
1915
+ ),
1916
+ children: !auth.isReady ? /* @__PURE__ */ jsx3("p", { role: "status", children: "Checking session\u2026" }) : !auth.isSignedIn ? /* @__PURE__ */ jsxs(Fragment2, { children: [
1917
+ /* @__PURE__ */ jsx3("p", { className: "basic-muted", children: "Sign in to share data." }),
1918
+ /* @__PURE__ */ jsx3(SignInButton, { auth })
1919
+ ] }) : /* @__PURE__ */ jsx3(
1920
+ SharesContent,
1921
+ {
1922
+ shares,
1923
+ scope,
1924
+ repo,
1925
+ canWrite: auth.canWrite
1926
+ },
1927
+ auth.did ?? "account"
1928
+ )
1929
+ }
1930
+ );
1931
+ }
1932
+ function SharesContent({
1933
+ shares: supplied,
1934
+ scope,
1935
+ repo,
1936
+ canWrite
1937
+ }) {
1938
+ const live = useOutgoingShares();
1939
+ const shares = supplied ?? live;
1940
+ const action = useAction();
1941
+ const [recipient, setRecipient] = useState4("");
1942
+ const [role, setRole] = useState4("viewer");
1943
+ const [confirmation, setConfirmation] = useState4(null);
1944
+ const [message, setMessage] = useState4("");
1945
+ const scopeValid = scope.length > 0 && scope.every(
1946
+ (item) => item.table.trim() && (item.recordIds === void 0 || item.recordIds.length > 0)
1947
+ );
1948
+ return /* @__PURE__ */ jsxs(Tabs.Root, { defaultValue: "outgoing", className: "basic-shares", children: [
1949
+ /* @__PURE__ */ jsxs(Tabs.List, { className: "basic-tabs", "aria-label": "Share direction", children: [
1950
+ /* @__PURE__ */ jsx3(Tabs.Tab, { value: "outgoing", children: "Outgoing" }),
1951
+ /* @__PURE__ */ jsx3(Tabs.Tab, { value: "incoming", children: "Incoming invitations" })
1952
+ ] }),
1953
+ /* @__PURE__ */ jsx3(Tabs.Panel, { value: "incoming", children: /* @__PURE__ */ jsxs("section", { "aria-label": "Incoming invitations", children: [
1954
+ /* @__PURE__ */ jsx3("h3", { className: "basic-title", children: "Incoming invitations" }),
1955
+ /* @__PURE__ */ jsx3("p", { className: "basic-muted", children: "View, accept, or decline incoming invitations in Basic ID. Your app session cannot access the account-wide inbox." }),
1956
+ /* @__PURE__ */ jsx3("a", { className: "basic-link", href: shares.manageUrl(), children: "Open incoming invitations in Basic ID \u2197" })
1957
+ ] }) }),
1958
+ /* @__PURE__ */ jsxs(Tabs.Panel, { value: "outgoing", children: [
1959
+ /* @__PURE__ */ jsx3("p", { className: "basic-muted", children: "Revoking access stops future requests. It cannot retract data the recipient already downloaded." }),
1960
+ /* @__PURE__ */ jsxs("div", { className: "basic-scope", children: [
1961
+ /* @__PURE__ */ jsx3("strong", { children: "New invitation permissions" }),
1962
+ scope.map((item, index) => /* @__PURE__ */ jsxs("span", { children: [
1963
+ item.table,
1964
+ " \xB7",
1965
+ " ",
1966
+ item.recordIds ? `${item.recordIds.length} selected records` : "all records"
1967
+ ] }, index))
1968
+ ] }),
1969
+ !canWrite && /* @__PURE__ */ jsx3("p", { className: "basic-error", children: "This session is read only. Sharing changes are disabled." }),
1970
+ /* @__PURE__ */ jsxs(
1971
+ "form",
1972
+ {
1973
+ className: "basic-share-form",
1974
+ onSubmit: (event) => {
1975
+ event.preventDefault();
1976
+ if (!recipient.trim() || !scopeValid || !canWrite) return;
1977
+ void action.run(async () => {
1978
+ await shares.create({
1979
+ repo,
1980
+ scope,
1981
+ role,
1982
+ ...recipient.trim().startsWith("did:") ? { recipientDid: recipient.trim() } : { recipientHandle: recipient.trim().replace(/^@+/, "") }
1983
+ });
1984
+ setRecipient("");
1985
+ setMessage("Invitation sent.");
1986
+ shares.refresh();
1987
+ });
1988
+ },
1989
+ children: [
1990
+ /* @__PURE__ */ jsxs("label", { className: "basic-field", children: [
1991
+ "Recipient handle or DID",
1992
+ /* @__PURE__ */ jsx3(
1993
+ "input",
1994
+ {
1995
+ className: "basic-input",
1996
+ value: recipient,
1997
+ onChange: (event) => setRecipient(event.target.value),
1998
+ placeholder: "@alex.basic.id",
1999
+ required: true,
2000
+ disabled: action.pending || !canWrite
2001
+ }
2002
+ )
2003
+ ] }),
2004
+ /* @__PURE__ */ jsxs("label", { className: "basic-field", children: [
2005
+ "Permission",
2006
+ /* @__PURE__ */ jsxs(
2007
+ "select",
2008
+ {
2009
+ className: "basic-input",
2010
+ value: role,
2011
+ onChange: (event) => setRole(event.target.value),
2012
+ disabled: action.pending || !canWrite,
2013
+ children: [
2014
+ /* @__PURE__ */ jsx3("option", { value: "viewer", children: "Can view" }),
2015
+ /* @__PURE__ */ jsx3("option", { value: "editor", children: "Can edit" })
2016
+ ]
2017
+ }
2018
+ )
2019
+ ] }),
2020
+ /* @__PURE__ */ jsx3(
2021
+ BasicButton,
2022
+ {
2023
+ type: "submit",
2024
+ variant: "solid",
2025
+ disabled: action.pending || !canWrite || !scopeValid || !recipient.trim(),
2026
+ children: action.pending ? "Please wait\u2026" : "Send invitation"
2027
+ }
2028
+ )
2029
+ ]
2030
+ }
2031
+ ),
2032
+ !scopeValid && /* @__PURE__ */ jsx3("p", { role: "alert", className: "basic-error", children: "Select at least one table or record to share." }),
2033
+ action.error && /* @__PURE__ */ jsx3("p", { role: "alert", className: "basic-error", children: action.error }),
2034
+ message && /* @__PURE__ */ jsx3("p", { role: "status", className: "basic-muted", children: message }),
2035
+ /* @__PURE__ */ jsxs("div", { className: "basic-section-heading", children: [
2036
+ /* @__PURE__ */ jsx3("h3", { className: "basic-title", children: "Outgoing shares \xB7 all permissions" }),
2037
+ /* @__PURE__ */ jsx3(
2038
+ BasicButton,
2039
+ {
2040
+ disabled: action.pending || shares.isLoading,
2041
+ onClick: shares.refresh,
2042
+ children: "Refresh"
2043
+ }
2044
+ )
2045
+ ] }),
2046
+ shares.isLoading ? /* @__PURE__ */ jsx3("p", { role: "status", children: "Loading shares\u2026" }) : shares.error ? /* @__PURE__ */ jsx3("p", { role: "alert", className: "basic-error", children: shares.error.message }) : !shares.data.length ? /* @__PURE__ */ jsx3("p", { className: "basic-empty", children: "No outgoing shares yet." }) : /* @__PURE__ */ jsx3("ul", { className: "basic-share-list", children: shares.data.map((share) => /* @__PURE__ */ jsxs("li", { children: [
2047
+ /* @__PURE__ */ jsxs("div", { className: "basic-identity", children: [
2048
+ /* @__PURE__ */ jsx3("strong", { children: share.display.shareName || share.recipientDid }),
2049
+ /* @__PURE__ */ jsxs("small", { children: [
2050
+ share.role === "viewer" ? "Can view" : "Can edit",
2051
+ " \xB7",
2052
+ " ",
2053
+ share.effectiveState,
2054
+ " \xB7",
2055
+ " ",
2056
+ share.scope.map((item) => item.table).join(", ")
2057
+ ] })
2058
+ ] }),
2059
+ share.state !== "ended" && (confirmation === share.id ? /* @__PURE__ */ jsxs("div", { className: "basic-actions", children: [
2060
+ /* @__PURE__ */ jsx3("span", { children: "Remove access?" }),
2061
+ /* @__PURE__ */ jsxs(
2062
+ BasicButton,
2063
+ {
2064
+ disabled: action.pending || !canWrite,
2065
+ onClick: () => void action.run(async () => {
2066
+ await (share.state === "pending" ? shares.cancel(share.id) : shares.revoke(share.id));
2067
+ setConfirmation(null);
2068
+ setMessage(
2069
+ share.state === "pending" ? "Invitation canceled." : "Access revoked."
2070
+ );
2071
+ shares.refresh();
2072
+ }),
2073
+ children: [
2074
+ "Confirm",
2075
+ " ",
2076
+ share.state === "pending" ? "cancel" : "revoke"
2077
+ ]
2078
+ }
2079
+ ),
2080
+ /* @__PURE__ */ jsx3(
2081
+ BasicButton,
2082
+ {
2083
+ disabled: action.pending,
2084
+ onClick: () => setConfirmation(null),
2085
+ children: "Keep access"
2086
+ }
2087
+ )
2088
+ ] }) : /* @__PURE__ */ jsx3(
2089
+ BasicButton,
2090
+ {
2091
+ disabled: action.pending || !canWrite,
2092
+ onClick: () => setConfirmation(share.id),
2093
+ children: share.state === "pending" ? "Cancel invitation" : "Revoke access"
2094
+ }
2095
+ ))
2096
+ ] }, share.id)) })
2097
+ ] })
2098
+ ] });
1056
2099
  }
1057
2100
  export {
2101
+ AccountSettingsModal,
2102
+ AuthStatus,
2103
+ BasicButton,
1058
2104
  BasicProvider,
2105
+ BasicUIProvider,
1059
2106
  BrowserKeyValueStorage,
1060
2107
  BrowserTokenStore,
1061
2108
  PersistenceStore,
2109
+ SharesModal,
2110
+ SignInButton,
2111
+ SignOutButton,
2112
+ SyncStatus,
2113
+ UserAvatar,
2114
+ UserButton,
2115
+ UserMenu,
1062
2116
  browserCurrentUrl,
1063
2117
  browserNavigate,
1064
2118
  browserReplaceUrl,
@@ -1076,6 +2130,7 @@ export {
1076
2130
  useFiles,
1077
2131
  useMounts,
1078
2132
  useOutgoingShares,
2133
+ useProjectProfile,
1079
2134
  useQuery,
1080
2135
  useRepos,
1081
2136
  useSchemaStatus,