@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/README.md CHANGED
@@ -18,22 +18,24 @@ its package metadata.
18
18
 
19
19
  ## Install
20
20
 
21
- Install the React SDK and declare the schema package you import directly:
21
+ This README describes the `0.12.0-beta.1` release candidate. After publication, install
22
+ matching versions of the React SDK and the schema package you import directly:
22
23
 
23
24
  ```bash
24
- npm install @basictech/react @basictech/schema
25
+ npm install @basictech/react@0.12.0-beta.1 @basictech/schema@0.12.0-beta.1
25
26
  ```
26
27
 
27
28
  React is a peer dependency and must already be installed by your application.
28
29
 
29
30
  ## Auth update guide
30
31
 
31
- Upgrade the packages you use together from npm's default `latest` channel:
32
+ Upgrade the packages you use together. npm's `latest` channel remains on `0.11.0` and
33
+ does not contain all of the API changes described here:
32
34
 
33
35
  ```bash
34
- npm install @basictech/react@latest @basictech/schema@latest
36
+ npm install @basictech/react@0.12.0-beta.1 @basictech/schema@0.12.0-beta.1
35
37
  # If you import core directly, update that dependency too:
36
- npm install @basictech/core@latest
38
+ npm install @basictech/core@0.12.0-beta.1
37
39
  ```
38
40
 
39
41
  Keep the public packages on matching versions. No browser storage reset or persisted-profile
@@ -42,9 +44,8 @@ that an older installed beta contains the update.
42
44
 
43
45
  ### Update auth checks and editing controls
44
46
 
45
- - Replace `useAuth().status === 'reauth_required'` with `status === 'expired'`.
46
- Core client snapshots likewise use `authStatus: 'expired'`; only low-level `AuthSession`
47
- retains the legacy compatibility state.
47
+ - Replace `useAuth().status === 'reauth_required'` with `status === 'expired'`. The
48
+ `reauth_required` state no longer exists anywhere, including low-level `AuthSession`.
48
49
  - Use `auth.canWrite` to disable mutation controls and `auth.readOnlyReason` for explanation.
49
50
  It is account eligibility, not a guarantee of connectivity or permission to a particular source.
50
51
  Catch imperative mutation errors too: auth can expire after the UI renders. Newly attempted
@@ -72,8 +73,10 @@ function AuthNotice() {
72
73
 
73
74
  ### Render per-account status
74
75
 
75
- `useAccounts().accounts` now returns `BasicAccount[]`, including anonymous and expired accounts.
76
- Each account adds `auth: { status, reason, checkedAt }` to the existing profile fields:
76
+ `useAccounts().accounts` returns `BasicAccount[]`, including anonymous and expired accounts, and
77
+ `useAccounts().activeAccount` is this tab's selection. The former `profiles`/`users`/`activeUser`
78
+ snapshot aliases and the `active` hook field are gone. Each account adds
79
+ `auth: { status, reason, checkedAt }` to the existing profile fields:
77
80
 
78
81
  | Account status | Meaning |
79
82
  | --- | --- |
@@ -90,6 +93,11 @@ is expired; otherwise an unverified remembered account is checking. The active a
90
93
  has `auth.status: 'anon'`, while top-level `useAuth()` reports `status: 'signed_out'` and
91
94
  `isAnonymous: true`. REST exposes one remembered account, not a multi-account registry.
92
95
 
96
+ `account.auth.reason` is exactly `'AUTH_EXPIRED' | null`. Raw active-account diagnostics such as
97
+ `invalid_grant` remain available as `useAuth().error`. Sign-in prerequisite errors returned by
98
+ server-backed hooks are stable within each hook but intentionally distinct objects from that raw
99
+ diagnostic; an expired hook can report `AUTH_EXPIRED` while the auth diagnostic is `invalid_grant`.
100
+
93
101
  Ordinary network loss is not expiry, and anonymous editing still works. `goOnline()` cannot
94
102
  bypass expired read-only enforcement. Reauthorization resumes retained queued work; already
95
103
  accepted server requests cannot be undone by client cancellation. REST has no offline cache.
@@ -278,7 +286,7 @@ The package also exports the adapters for explicit composition: `BrowserKeyValue
278
286
  `browserStorage`, `BrowserTokenStore`, `PersistenceStore`, `browserUploadTransport`,
279
287
  `createBrowserMessageChannel`, `createBrowserAuthChannelFactory`, `browserNavigate`,
280
288
  `browserCurrentUrl`, and `browserReplaceUrl`. Most applications only need `createBasic`, which
281
- installs them.
289
+ installs them. `supportsAccessTokenAdoption` is internal and is not a public React export.
282
290
 
283
291
  ### Token storage
284
292
 
@@ -320,6 +328,93 @@ With the default `renderWhileLoading={false}`, hooks under the Provider first re
320
328
  client is ready. Setting it to `true` is useful for an application shell that reads `isReady` and
321
329
  owns its loading UI.
322
330
 
331
+ ## Prebuilt components
332
+
333
+ Drop-in account and sharing UI uses Base UI's accessible menu, avatar, and dialog primitives.
334
+ Components use the nearest `BasicProvider` (including `basic.Provider`); they do not create a
335
+ second auth client. Import the optional stylesheet once. Omit it to supply your own styles.
336
+
337
+ ```tsx
338
+ import {
339
+ BasicUIProvider, SignInButton, UserButton, AuthStatus, SyncStatus, SharesModal,
340
+ } from '@basictech/react'
341
+ import '@basictech/react/styles.css'
342
+ import { basic } from './basic'
343
+
344
+ export function AccountTools() {
345
+ return (
346
+ <basic.Provider>
347
+ <BasicUIProvider appearance={{ theme: 'light', accent: '#354f45', radius: '8px' }}>
348
+ <SignInButton />
349
+ <UserButton allowAddAccount />
350
+ <AuthStatus />
351
+ <SyncStatus />
352
+ <SharesModal scope={[{ table: 'todos', recordIds: ['todo-1'] }]} />
353
+ </BasicUIProvider>
354
+ </basic.Provider>
355
+ )
356
+ }
357
+ ```
358
+
359
+ | Component | Options and behavior |
360
+ | --- | --- |
361
+ | `BasicButton` | Native button props/ref; `variant="solid\|outline\|ghost"`. Defaults to `type="button"`. |
362
+ | `SignInButton`, `SignOutButton` | Native button props and variant, pending/error feedback. Sign-in accepts `input` (handle/DID); `onClick.preventDefault()` cancels the action. Expired sessions can reauthorize or sign out. |
363
+ | `UserAvatar` | Borderless avatar trigger for the shared user menu. Current account or explicit `profile` (`null` means guest); `size`, `className`, `style`; image failure falls back to initials. Sync badge defaults on in sync mode; `showSyncBadge={false}` hides it. REST mode never shows a sync badge. |
364
+ | `UserButton` | Avatar, name, handle, and dropdown icon with a bordered hoverable trigger. `shape="rounded\|square"` defaults to rounded. Opens the shared user menu. Local accounts never show a handle. |
365
+ | `UserMenu` | Shared menu used by both triggers; `trigger="avatar\|button"` defaults to avatar. Current account at top, Manage account, other saved accounts, and auth actions. The switch section appears only when other accounts exist. Only expired auth gets an `Expired` warning; no signed-in badge. `showSyncStatus={false}` hides the header's sync label; REST mode always hides sync badges. `allowAddAccount` defaults to true, starting sign-in in a new profile in REST or sync mode. Local accounts offer Clear account with a destructive-action confirmation. Optional `accountSettingsUrl`. |
366
+ | `AuthStatus` | Loading, signed-in, guest, signed-out, recovering, and expired session labels. UI visibility is not authorization. |
367
+ | `SyncStatus` | Connectivity, pending counts, and conflict/rejection count; optional `source` for a repo or mount. An online queue is not labelled synced. |
368
+ | `AccountSettingsModal` | Profile tab edits the project display name with Cancel/Save changes; drafts survive tab switches. Advanced shows read-only DID, PDS URL, local account/storage identifiers, auth/write state, and sync counts. No tokens or credentials are rendered. Local accounts show explicit DID/PDS fallbacks. `projectProfile` supplies a complete preview hook result. Optional `accountSettingsUrl` links to universal identity settings; `children` adds host-owned Profile content. |
369
+ | `SharesModal` | Same identity-header layout with Outgoing/Incoming tabs. Required explicit `scope`, optional `repo` (default: `'default'`). Viewer/editor invitations by handle/DID, outgoing list, refresh, confirmed cancellation/revocation. Incoming acceptance and mount management remain in Basic ID. |
370
+
371
+ Both modals accept `open`/`onOpenChange`, an optional button element or label as `trigger`, and
372
+ `finalFocus` for externally controlled use. `trigger={null}` omits the trigger. Custom trigger
373
+ components must forward their ref and DOM props. Dialogs trap focus and close with Escape.
374
+
375
+ `BasicUIProvider` accepts `appearance: { theme, accent, radius, density }`; density is
376
+ `'comfortable'` or `'compact'`. Nested providers inherit appearance; portaled menus/dialogs retain
377
+ it. Choose an accent with sufficient contrast against white for solid buttons. Fonts inherit from
378
+ the host. Override the prefixed `basic-*` classes and `--basic-*` CSS tokens for finer styling.
379
+
380
+ For controlled previews, components accept complete hook results via `auth`, `accounts`, `sync`,
381
+ or `shares` as appropriate. These replace rendered state **and action callbacks**; a BasicProvider
382
+ is still required and hooks still subscribe/read. Do not use these props as an authorization
383
+ boundary. Real mutations remain subject to SDK and server permissions.
384
+
385
+ Share creation requires an explicit nonempty table/record scope and defaults to viewer access.
386
+ The list is labelled **all permissions**, not filtered to the new-invitation scope. Revocation cannot
387
+ retract downloaded data. Signing out removes the account's local cache and pending edits.
388
+ The incoming-invitations section links to Basic ID: inbox/accept/decline APIs require owner
389
+ credentials, not app tokens. Server project-profile GET/PATCH/DELETE APIs exist at
390
+ `/account/:project_id/profile` with `app:profile:read/update/delete` grants respectively.
391
+ Configure those requested OAuth scopes and reauthorize existing sessions when needed.
392
+ The shared menu shows sync status and any expiry warning with the current account in its header.
393
+ Inactive accounts show saved expiry state, not the active account's sync status.
394
+ Both `UserButton` and `UserAvatar` accept `showSyncBadge` (default follows SDK sync mode); circles use
395
+ green for synced, blue for connecting/pending, orange for errors, gray for offline/local/idle.
396
+ The demo's `?page=components` gallery offers live behavior, isolated simulated actions, design
397
+ controls, and a copyable usage recipe. Live OAuth/shares require a configured signed-in account.
398
+
399
+ ### Project profile API
400
+
401
+ `basic.useProjectProfile()` (also exported as `useProjectProfile`) returns `data`, `isLoading`,
402
+ `error`, `refresh()`, `update(patch)`, and `reset()`. Pass `false` to disable automatic reads.
403
+ `data` is `{ profile: { name?, avatar_url?, status? }, meta? }` or null before loading/sign-in.
404
+ The core client exposes the same operations directly:
405
+
406
+ ```ts
407
+ await basic.client.getProjectProfile()
408
+ await basic.client.updateProjectProfile({ name: 'Name in this app', status: null })
409
+ await basic.client.resetProjectProfile()
410
+ ```
411
+
412
+ Requests always target the configured client ID using the active app token. Omitted fields stay
413
+ unchanged; null removes an override. Metadata objects merge on the server; `meta: null` clears
414
+ metadata. Reset removes all project overrides and metadata. Universal profile data is untouched.
415
+ Permission failures propagate as SDK errors. Profile responses are not written into the local
416
+ account registry; consumers needing the updated project identity should use this hook's data.
417
+
323
418
  ## Hook reference
324
419
 
325
420
  The schema-bound factory returns every hook below. Bound hooks are preferred because they reject
@@ -352,8 +447,8 @@ unknown table names and infer fields in reads, writes, and queries.
352
447
  | `isAnonymous` | `boolean` | The active sync profile is anonymous and not signed in |
353
448
  | `status` | `'bootstrapping' \| 'signed_out' \| 'authenticated' \| 'recovering' \| 'expired'` | Current public auth lifecycle |
354
449
  | `canWrite` | `boolean` | Active-account write eligibility; source permissions still apply |
355
- | `readOnlyReason` | `string \| null` | For example `AUTH_EXPIRED` when reauthorization is needed |
356
- | `error` | `BasicError \| null` | Stable auth bootstrap/refresh error code when available |
450
+ | `readOnlyReason` | `ReadOnlyReason \| null` | `'AUTH_EXPIRED' \| 'AUTH_CHECKING' \| 'AUTHORIZATION_REQUIRED' \| 'CLIENT_NOT_STARTED'`; switch exhaustively for banners |
451
+ | `error` | `BasicError \| null` | Raw active auth diagnostic; same object while its code and expired state are unchanged |
357
452
  | `user` | `AuthUser \| null` | OIDC user info (`sub`, `pds_url`, and optional `email`, `name`, `picture`, `handle`) |
358
453
  | `did` | `string \| null` | Signed-in account DID |
359
454
  | `handle` | `string \| null` | Resolved account handle |
@@ -399,31 +494,33 @@ replicas and returns to another local profile or a fresh anonymous one when enab
399
494
 
400
495
  ### `basic.useAccounts()`
401
496
 
402
- `useAccounts()` returns `{ accounts, active, switchAccount, addAccount, removeAccount }`:
497
+ `useAccounts()` returns `{ accounts, activeAccount, switchAccount, addAccount, removeAccount }`:
403
498
 
404
499
  - `accounts: BasicAccount[]` lists local profiles with per-account auth summaries.
405
- - `active: BasicAccount | null` is this tab's selected profile and auth summary.
500
+ - `activeAccount: BasicAccount | null` is this tab's selected profile and auth summary.
406
501
  - `switchAccount(id): Promise<void>` changes the active profile in this tab.
407
- - `addAccount(): Promise<BasicProfile>` creates and activates a new anonymous profile from which to
408
- start another sign-in.
502
+ - `addAccount({ signIn?: boolean }): Promise<BasicProfile>` creates and activates a new profile.
503
+ Omit options to stay local; pass `{ signIn: true }` to validate redirect configuration first and
504
+ start sign-in in the new profile. The prebuilt menu uses this sign-in option.
409
505
  - `removeAccount(id): Promise<void>` removes that profile and its local credentials/replicas. If it
410
506
  is active, this follows the sign-out path. It does not delete the server account.
411
507
 
412
508
  `BasicProfile` contains `id`, `kind: 'anon' | 'account'`, `storagePrefix`, `createdAt`,
413
- `lastActiveAt`, and optional/null `did`, `handle`, `email`, `name`, and `picture`. Multi-account and
414
- `addAccount()` require sync mode with anonymous profiles enabled.
509
+ `lastActiveAt`, and optional/null `did`, `handle`, `email`, `name`, and `picture`. Saved accounts and
510
+ switching work in REST and sync modes. `addAccount({ signIn: true })` works in either mode;
511
+ `addAccount()` without sign-in still requires sync mode with anonymous profiles enabled.
415
512
 
416
513
  ```tsx
417
514
  // file: AccountSwitcher.tsx
418
515
  import { basic } from './basic'
419
516
 
420
517
  export function AccountSwitcher() {
421
- const { accounts, active, switchAccount, addAccount, removeAccount } = basic.useAccounts()
518
+ const { accounts, activeAccount, switchAccount, addAccount, removeAccount } = basic.useAccounts()
422
519
 
423
520
  return (
424
521
  <section>
425
522
  <select
426
- value={active?.id ?? ''}
523
+ value={activeAccount?.id ?? ''}
427
524
  onChange={(event) => void switchAccount(event.currentTarget.value)}
428
525
  >
429
526
  {accounts.map((account) => (
@@ -433,7 +530,7 @@ export function AccountSwitcher() {
433
530
  ))}
434
531
  </select>
435
532
  <button onClick={() => void addAccount()}>Add account</button>
436
- {active && <button onClick={() => void removeAccount(active.id)}>Remove local profile</button>}
533
+ {activeAccount && <button onClick={() => void removeAccount(activeAccount.id)}>Remove local profile</button>}
437
534
  </section>
438
535
  )
439
536
  }
@@ -821,9 +918,8 @@ Shares v2 is capability-gated and requires sign-in. `useOutgoingShares()` return
821
918
  ```text
822
919
  {
823
920
  data: OutgoingShareInfo[]
824
- outgoingShares: OutgoingShareInfo[] // alias of data
825
921
  isLoading: boolean
826
- error: BasicError | null
922
+ error: BasicError | null // load failure, or AUTHORIZATION_REQUIRED / AUTH_EXPIRED while not signed in
827
923
  refresh(): void
828
924
  create(input: CreateOutgoingShareInput): Promise<OutgoingShareInfo>
829
925
  get(id: string): Promise<OutgoingShareInfo>
@@ -869,7 +965,7 @@ export function OutgoingShares() {
869
965
  <button onClick={() => location.assign(shares.manageUrl())}>Manage in Basic ID</button>
870
966
  {shares.error && <p>{shares.error.code}</p>}
871
967
  <ul>
872
- {shares.outgoingShares.map((share) => (
968
+ {shares.data.map((share) => (
873
969
  <li key={share.id}>
874
970
  {share.recipientDid}: {share.effectiveState}
875
971
  {share.state === 'pending' && (
@@ -907,9 +1003,8 @@ management belong there. `client.shares.leave()` is intentionally owner-only and
907
1003
  ```text
908
1004
  {
909
1005
  data: MountInfo[]
910
- mounts: MountInfo[] // alias of data
911
1006
  isLoading: boolean
912
- error: BasicError | null
1007
+ error: BasicError | null // load failure, or AUTHORIZATION_REQUIRED / AUTH_EXPIRED while not signed in
913
1008
  refresh(): void
914
1009
  open(mountId: string): Promise<MountHandle<S>>
915
1010
  manageUrl(): string
@@ -955,7 +1050,7 @@ export function IncomingMounts() {
955
1050
  <section>
956
1051
  <button onClick={() => location.assign(mounts.manageUrl())}>Manage mounts</button>
957
1052
  <ul>
958
- {mounts.mounts.map((mount) => (
1053
+ {mounts.data.map((mount) => (
959
1054
  <li key={mount.id}>
960
1055
  {mount.role} from {mount.originOwnerDid}
961
1056
  <button onClick={() => void open(mount.id)}>Open</button>
@@ -1090,7 +1185,8 @@ The unbound exports mirror the bound hooks:
1090
1185
  | `useMounts()`, `useOutgoingShares()` | Same share results |
1091
1186
 
1092
1187
  Every unbound hook must run below `BasicProvider`; otherwise it throws `Basic hooks must be used
1093
- within a <BasicProvider>`.
1188
+ within a <BasicProvider>`. Bound hooks likewise throw outside their own `basic.Provider`, because
1189
+ the Provider is what starts and stops the client.
1094
1190
 
1095
1191
  ## Error handling
1096
1192
 
package/dist/index.d.mts CHANGED
@@ -1,6 +1,6 @@
1
1
  import * as react from 'react';
2
- import { ReactNode, ReactElement } from 'react';
3
- import { BasicSchema, BasicConfig, BasicClient, BasicAccount, BasicProfile, AuthStatus, BasicError, AuthUser, BasicSyncStatus, BasicRejection, BasicConflict, SourceRef, SchemaStatus, BasicDb, Repo, FileRecord, MountFileInfo, StorageInfo, FileListQuery, MountInfo, MountHandle, OutgoingShareInfo, CreateOutgoingShareInput, MountsQuery, JsonObject, BasicRecord, Query, MountViewerFiles, OwnerFiles, TableNames, Collection, InferValue, KeyValueStorage, TokenStore, AuthMessageChannel, ReplicaStoreFactory, StoragePartition, ReplicaStore, UploadTransportAdapter } from '@basictech/core';
2
+ import { ReactNode, ReactElement, RefObject, ButtonHTMLAttributes, HTMLAttributes } from 'react';
3
+ import { BasicSchema, BasicConfig, BasicClient, BasicAccount, BasicProfile, ReadOnlyReason, AuthStatus as AuthStatus$1, BasicError, AuthUser, BasicSyncStatus, BasicRejection, BasicConflict, SourceRef, SchemaStatus, BasicDb, Repo, FileRecord, MountFileInfo, StorageInfo, FileListQuery, MountInfo, MountHandle, OutgoingShareInfo, CreateOutgoingShareInput, MountsQuery, JsonObject, BasicRecord, Query, ProjectProfile, ProjectProfilePatch, MountViewerFiles, OwnerFiles, TableNames, Collection, InferValue, KeyValueStorage, TokenStore, AuthChannelContext, AuthMessageChannel, ReplicaStoreFactory, StoragePartition, ReplicaStore, UploadTransportAdapter } from '@basictech/core';
4
4
 
5
5
  type BrowserBasicConfig<S extends BasicSchema = BasicSchema> = BasicConfig<S>;
6
6
  /** Create a core client with the complete browser adapter set installed. */
@@ -8,9 +8,11 @@ declare function createBasicClient<S extends BasicSchema>(config: BrowserBasicCo
8
8
 
9
9
  interface UseAccountsResult {
10
10
  accounts: BasicAccount[];
11
- active: BasicAccount | null;
11
+ activeAccount: BasicAccount | null;
12
12
  switchAccount(id: string): Promise<void>;
13
- addAccount(): Promise<BasicProfile>;
13
+ addAccount(options?: {
14
+ signIn?: boolean;
15
+ }): Promise<BasicProfile>;
14
16
  removeAccount(id: string): Promise<void>;
15
17
  }
16
18
  declare function useAccounts(): UseAccountsResult;
@@ -20,8 +22,9 @@ interface UseAuthResult {
20
22
  isSignedIn: boolean;
21
23
  isAnonymous: boolean;
22
24
  canWrite: boolean;
23
- readOnlyReason: string | null;
24
- status: AuthStatus;
25
+ readOnlyReason: ReadOnlyReason | null;
26
+ status: AuthStatus$1;
27
+ /** Raw auth diagnostic; stable identity while its code and expired state are unchanged. */
25
28
  error: BasicError | null;
26
29
  user: AuthUser | null;
27
30
  did: string | null;
@@ -64,6 +67,7 @@ declare function useFiles(query?: FileListQuery, options?: {
64
67
  interface UseStorageInfoResult {
65
68
  data: StorageInfo | null;
66
69
  isLoading: boolean;
70
+ /** Load failure, or the sign-in requirement while no account is signed in. */
67
71
  error: BasicError | null;
68
72
  refresh(): void;
69
73
  }
@@ -71,8 +75,8 @@ declare function useStorageInfo(): UseStorageInfoResult;
71
75
 
72
76
  interface UseMountsResult<S extends BasicSchema = BasicSchema> {
73
77
  data: MountInfo[];
74
- mounts: MountInfo[];
75
78
  isLoading: boolean;
79
+ /** Load failure, or the sign-in requirement while no account is signed in. */
76
80
  error: BasicError | null;
77
81
  refresh(): void;
78
82
  open(mountId: string): Promise<MountHandle<S>>;
@@ -81,8 +85,8 @@ interface UseMountsResult<S extends BasicSchema = BasicSchema> {
81
85
  declare function useMounts(query?: MountsQuery): UseMountsResult;
82
86
  interface UseOutgoingSharesResult {
83
87
  data: OutgoingShareInfo[];
84
- outgoingShares: OutgoingShareInfo[];
85
88
  isLoading: boolean;
89
+ /** Load failure, or the sign-in requirement while no account is signed in. */
86
90
  error: BasicError | null;
87
91
  refresh(): void;
88
92
  create(input: CreateOutgoingShareInput): Promise<OutgoingShareInfo>;
@@ -116,6 +120,19 @@ interface UseReposResult {
116
120
  }
117
121
  declare function useRepos(): UseReposResult;
118
122
 
123
+ interface AsyncResult<T> {
124
+ data: T;
125
+ isLoading: boolean;
126
+ error: BasicError | null;
127
+ refresh(): void;
128
+ }
129
+
130
+ interface UseProjectProfileResult extends AsyncResult<ProjectProfile | null> {
131
+ update(patch: ProjectProfilePatch): Promise<ProjectProfile>;
132
+ reset(): Promise<ProjectProfile>;
133
+ }
134
+ declare function useProjectProfile(enabled?: boolean): UseProjectProfileResult;
135
+
119
136
  type CreateBasicConfig<S extends BasicSchema> = BrowserBasicConfig<S> & {
120
137
  schema: S;
121
138
  };
@@ -145,6 +162,7 @@ interface CreatedBasic<S extends BasicSchema> {
145
162
  useSyncStatus(source?: SourceRef): UseSyncStatusResult;
146
163
  useSchemaStatus(source?: SourceRef): SchemaStatus;
147
164
  useRepos(): UseReposResult;
165
+ useProjectProfile(enabled?: boolean): UseProjectProfileResult;
148
166
  useFiles(query?: FileListQuery, options?: {
149
167
  source?: SourceRef;
150
168
  }): UseFilesResult;
@@ -178,6 +196,93 @@ declare function useCollection<V extends JsonObject = JsonObject>(name: string,
178
196
  source?: SourceRef;
179
197
  }): Collection<V>;
180
198
 
199
+ interface BasicAppearance {
200
+ theme?: 'light' | 'dark';
201
+ accent?: string;
202
+ radius?: string;
203
+ density?: 'compact' | 'comfortable';
204
+ }
205
+ /** Optional styling provider. CSS is opt-in via @basictech/react/styles.css. */
206
+ declare function BasicUIProvider({ appearance, children, }: {
207
+ appearance?: BasicAppearance;
208
+ children: ReactNode;
209
+ }): react.JSX.Element;
210
+ interface BasicButtonProps extends ButtonHTMLAttributes<HTMLButtonElement> {
211
+ variant?: 'solid' | 'outline' | 'ghost';
212
+ }
213
+ declare const BasicButton: react.ForwardRefExoticComponent<BasicButtonProps & react.RefAttributes<HTMLButtonElement>>;
214
+ interface AuthButtonProps extends BasicButtonProps {
215
+ auth?: UseAuthResult;
216
+ input?: string;
217
+ }
218
+ declare function SignInButton(props: AuthButtonProps): react.JSX.Element;
219
+ declare function SignOutButton(props: AuthButtonProps): react.JSX.Element;
220
+ interface UserAvatarProps extends HTMLAttributes<HTMLSpanElement> {
221
+ profile?: (Pick<BasicProfile, 'name' | 'handle' | 'email' | 'picture'> & Partial<Pick<BasicProfile, 'kind'>>) | null;
222
+ size?: number;
223
+ showSyncBadge?: boolean;
224
+ sync?: UseSyncStatusResult;
225
+ accounts?: UseAccountsResult;
226
+ auth?: UseAuthResult;
227
+ projectProfile?: UseProjectProfileResult;
228
+ allowAddAccount?: boolean;
229
+ }
230
+ declare function UserAvatar({ accounts, auth, projectProfile, allowAddAccount, ...avatarProps }: UserAvatarProps): react.JSX.Element;
231
+ declare function AuthStatus({ auth: supplied, className, ...props }: HTMLAttributes<HTMLSpanElement> & {
232
+ auth?: UseAuthResult;
233
+ }): react.JSX.Element;
234
+ declare function SyncStatus({ sync: supplied, source, className, ...props }: HTMLAttributes<HTMLSpanElement> & {
235
+ sync?: UseSyncStatusResult;
236
+ source?: SourceRef;
237
+ }): react.JSX.Element | null;
238
+ interface UserButtonProps {
239
+ accounts?: UseAccountsResult;
240
+ auth?: UseAuthResult;
241
+ sync?: UseSyncStatusResult;
242
+ /** Show synchronization status in the account menu. Defaults to true. */
243
+ showSyncStatus?: boolean;
244
+ /** Defaults to enabled in sync mode. */
245
+ showSyncBadge?: boolean;
246
+ shape?: 'rounded' | 'square';
247
+ projectProfile?: UseProjectProfileResult;
248
+ /** Show the action to sign in to another account. Defaults to true. */
249
+ allowAddAccount?: boolean;
250
+ className?: string;
251
+ accountSettingsUrl?: string;
252
+ }
253
+ declare function UserButton(props: UserButtonProps): react.JSX.Element;
254
+ interface UserMenuProps extends UserButtonProps {
255
+ trigger?: 'avatar' | 'button';
256
+ avatarProps?: UserAvatarProps;
257
+ }
258
+ declare function UserMenu({ accounts: supplied, auth: suppliedAuth, sync, showSyncStatus, showSyncBadge, shape, trigger, avatarProps, projectProfile, allowAddAccount, className, accountSettingsUrl, }: UserMenuProps): react.JSX.Element;
259
+ interface BasicModalProps {
260
+ open?: boolean;
261
+ onOpenChange?: (open: boolean) => void;
262
+ finalFocus?: RefObject<HTMLElement>;
263
+ /** Supply a button element; null omits the trigger for controlled use. */
264
+ trigger?: ReactNode;
265
+ }
266
+ interface AccountSettingsModalProps extends BasicModalProps {
267
+ auth?: UseAuthResult;
268
+ accounts?: UseAccountsResult;
269
+ projectProfile?: UseProjectProfileResult;
270
+ sync?: UseSyncStatusResult;
271
+ accountSettingsUrl?: string;
272
+ children?: ReactNode;
273
+ }
274
+ declare function AccountSettingsModal({ auth: suppliedAuth, accounts: suppliedAccounts, accountSettingsUrl, projectProfile, sync, children, ...props }: AccountSettingsModalProps): react.JSX.Element;
275
+ interface SharesModalProps extends BasicModalProps {
276
+ auth?: UseAuthResult;
277
+ accounts?: UseAccountsResult;
278
+ sync?: UseSyncStatusResult;
279
+ shares?: UseOutgoingSharesResult;
280
+ /** Explicit least-privilege scope; the component never infers all tables. */
281
+ scope: CreateOutgoingShareInput['scope'];
282
+ repo?: CreateOutgoingShareInput['repo'];
283
+ }
284
+ declare function SharesModal({ auth: supplied, accounts: suppliedAccounts, sync, shares, scope, repo, ...props }: SharesModalProps): react.JSX.Element;
285
+
181
286
  /** Synchronous browser Storage adapted to core's key-value contract. */
182
287
  declare class BrowserKeyValueStorage implements KeyValueStorage {
183
288
  private readonly storage;
@@ -211,6 +316,8 @@ declare class BrowserTokenStore implements TokenStore {
211
316
  private waitForAccessToken;
212
317
  }
213
318
 
319
+ /** The subset of BrowserTokenStore the auth channel needs; any token store providing it works. */
320
+ type AccessTokenAdopter = Pick<BrowserTokenStore, 'accessToken' | 'adoptAccessToken'>;
214
321
  interface BrowserMessageChannel {
215
322
  postMessage(message: unknown): void;
216
323
  close(): void;
@@ -223,9 +330,11 @@ declare function createBrowserMessageChannel(name: string): BrowserMessageChanne
223
330
  /**
224
331
  * Access tokens may cross tabs transiently but are never written to browser
225
332
  * storage. The receiving adapter adopts the token before core handles the
226
- * ordinary token-rotation notification.
333
+ * ordinary token-rotation notification. Core supplies the token key for the
334
+ * profile's auth channel; channels without a context (the accounts channel)
335
+ * are plain BroadcastChannels.
227
336
  */
228
- declare function createBrowserAuthChannelFactory(clientId: string, tokenStore: BrowserTokenStore): (name: string) => AuthMessageChannel;
337
+ declare function createBrowserAuthChannelFactory(tokenStore: AccessTokenAdopter): (name: string, context?: AuthChannelContext) => AuthMessageChannel;
229
338
 
230
339
  declare function browserNavigate(url: string): void;
231
340
  declare function browserCurrentUrl(): string;
@@ -269,4 +378,4 @@ declare class PersistenceStore implements ReplicaStoreFactory {
269
378
  /** Browser multipart transport with upload progress, adapted from the Drive UI. */
270
379
  declare const browserUploadTransport: UploadTransportAdapter;
271
380
 
272
- export { BasicProvider, type BasicProviderProps, type BoundBasicProviderProps, type BrowserBasicConfig, BrowserKeyValueStorage, type BrowserLockManager, type BrowserMessageChannel, BrowserTokenStore, type CreateBasicConfig, type CreatedBasic, PersistenceStore, type PersistenceStoreOptions, type UseAccountsResult, type UseAuthResult, type UseBasicResult, type UseFilesResult, type UseMountsResult, type UseOutgoingSharesResult, type UseQueryResult, type UseReposResult, type UseStorageInfoResult, type UseSyncStatusResult, browserCurrentUrl, browserNavigate, browserReplaceUrl, browserStorage, browserUploadTransport, createBasic, createBasicClient, createBrowserAuthChannelFactory, createBrowserMessageChannel, useAccounts, useAuth, useBasic, useCollection, useDb, useFiles, useMounts, useOutgoingShares, useQuery, useRepos, useSchemaStatus, useStorageInfo, useSyncStatus };
381
+ export { type AccessTokenAdopter, AccountSettingsModal, type AccountSettingsModalProps, type AuthButtonProps, AuthStatus, type BasicAppearance, BasicButton, type BasicButtonProps, type BasicModalProps, BasicProvider, type BasicProviderProps, BasicUIProvider, type BoundBasicProviderProps, type BrowserBasicConfig, BrowserKeyValueStorage, type BrowserLockManager, type BrowserMessageChannel, BrowserTokenStore, type CreateBasicConfig, type CreatedBasic, PersistenceStore, type PersistenceStoreOptions, SharesModal, type SharesModalProps, SignInButton, SignOutButton, SyncStatus, type UseAccountsResult, type UseAuthResult, type UseBasicResult, type UseFilesResult, type UseMountsResult, type UseOutgoingSharesResult, type UseProjectProfileResult, type UseQueryResult, type UseReposResult, type UseStorageInfoResult, type UseSyncStatusResult, UserAvatar, type UserAvatarProps, UserButton, type UserButtonProps, UserMenu, type UserMenuProps, browserCurrentUrl, browserNavigate, browserReplaceUrl, browserStorage, browserUploadTransport, createBasic, createBasicClient, createBrowserAuthChannelFactory, createBrowserMessageChannel, useAccounts, useAuth, useBasic, useCollection, useDb, useFiles, useMounts, useOutgoingShares, useProjectProfile, useQuery, useRepos, useSchemaStatus, useStorageInfo, useSyncStatus };