@streamoid/settings 0.3.8 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,6 +1,7 @@
1
1
  # @streamoid/settings
2
2
 
3
- Shared Settings module (Account, Workspace, Team, Billing, API Keys, Referral).
3
+ Shared Settings module (Account, Workspace, Organization, Billing, API Keys,
4
+ Referral).
4
5
  The host application provides auth, workspace, API, navigation, toast, and
5
6
  image-upload implementations through a single **adapter** object — the package
6
7
  contains no host-specific code.
@@ -99,6 +100,37 @@ Modals added to this package should call `useOverlayOpen(open, onClose)` at the
99
100
  top of the component. That is what registers them in the stack; skip it and
100
101
  Escape will close the whole panel out from under the modal.
101
102
 
103
+ ## Navigation is scope-first (0.4.0)
104
+
105
+ Destinations are grouped by *whose* settings they are. `workspace/*` tabs act
106
+ on the workspace you are currently in; `organization/*` tabs act on the org
107
+ above it:
108
+
109
+ | Group | Tabs | URL |
110
+ |---|---|---|
111
+ | — | `profile` | `/account` |
112
+ | Workspace *(named)* | `workspace`, `workspaceMembers`, `workspaceList` | `/workspace/general`, `/workspace/members`, `/workspace/all` |
113
+ | Organization *(named)* | `organization`, `organizationWorkspaces`, `organizationDomains` | `/organization/people`, `/organization/workspaces`, `/organization/domains` |
114
+ | — | `billing`, `apikey` | `/billing`, `/api-keys` |
115
+
116
+ The group headings carry the entity's own name, so no leaf label repeats the
117
+ word "Workspace" or "Organization" — and "Team", which named a workspace's
118
+ member list without saying which workspace, is gone.
119
+
120
+ **Host routing.** Scoped slugs have two segments, so a host matching
121
+ `/settings/:tab` needs a second route for `/settings/:tab/:sub`.
122
+ `resolveSettingsTabFromPath` prefers the two-segment match and still resolves
123
+ the pre-0.4 slugs `team`, `workspace` and `organization`, so old links keep
124
+ working.
125
+
126
+ **Organizations are optional.** When `organization.current` is `null` the whole
127
+ group collapses to one `Organization` row leading to the create/join screen,
128
+ rather than a heading with no name and three empty pages under it.
129
+
130
+ **Breaking:** the `"team"` member of `SettingsTab` is now `"workspaceMembers"`,
131
+ and `WorkspaceTab` is renamed `WorkspaceListTab` (the old name stays as a
132
+ deprecated alias).
133
+
102
134
  ## Explaining the tabs
103
135
 
104
136
  Every sub-nav destination carries a hover/focus tooltip sourced from the
package/dist/index.d.mts CHANGED
@@ -8,13 +8,49 @@ import react__default, { SVGProps, ReactNode } from 'react';
8
8
  * resolved tab into <SettingsContent activeTab={...} />. These helpers exist
9
9
  * so consumers don't have to re-implement tab/URL conversion if they don't
10
10
  * want to.
11
+ *
12
+ * ## Scope-first navigation
13
+ *
14
+ * Destinations are grouped by *whose* settings they are, not by feature. The
15
+ * data model is Organization → Workspaces → Members, and the nav now says so:
16
+ * `workspace/*` tabs act on one workspace, `organization/*` tabs act on the
17
+ * org above it. The old flat trio — Workspace / Team / Organization — read as
18
+ * three peers and left "Team" scoped to nothing the user could see.
19
+ *
20
+ * Pre-0.4 URLs (`/settings/team`, `/settings/workspace`, `/settings/organization`)
21
+ * still resolve, so existing links and bookmarks keep working.
11
22
  */
12
- type SettingsTab = "profile" | "workspace" | "referral" | "team" | "organization" | "billing" | "apikey";
13
- /** Internal tab id → URL slug. */
23
+ type SettingsTab = "profile"
24
+ /** Workspace › General — name, image, org affiliation, leave/delete. */
25
+ | "workspace"
26
+ /** Workspace › Members — was the top-level "Team" tab. */
27
+ | "workspaceMembers"
28
+ /** Workspace › All workspaces — the switch/create list. */
29
+ | "workspaceList"
30
+ /** Organization › People. */
31
+ | "organization"
32
+ /** Organization › Workspaces. */
33
+ | "organizationWorkspaces"
34
+ /** Organization › Domains & join requests. */
35
+ | "organizationDomains" | "referral" | "billing" | "apikey";
36
+ /** Which entity a tab acts on. Drives the nav grouping and the breadcrumb. */
37
+ type SettingsScope = "account" | "workspace" | "organization" | "global";
38
+ declare const TAB_SCOPE: Record<SettingsTab, SettingsScope>;
39
+ /** Fallback scope names, used before the entity's real name has loaded. */
40
+ declare const SCOPE_LABELS: Record<SettingsScope, string>;
41
+ /** Internal tab id → URL slug. Scoped tabs get a two-segment slug. */
14
42
  declare const TAB_TO_URL: Record<SettingsTab, string>;
15
- /** URL slug → internal tab id. */
43
+ /**
44
+ * URL slug → internal tab id. Two-segment slugs are matched first by
45
+ * `resolveSettingsTabFromPath`, so the bare `workspace` / `organization`
46
+ * entries below only catch pre-0.4 links.
47
+ */
16
48
  declare const URL_TO_TAB: Record<string, SettingsTab>;
17
- /** Human-readable label for each tab (used for breadcrumbs etc.). */
49
+ /**
50
+ * Human-readable label for each tab. These are *leaf* labels — the scope they
51
+ * belong to supplies the other half ("Acme Retail › Members"), so no label
52
+ * needs to repeat the word "Workspace" or "Organization".
53
+ */
18
54
  declare const TAB_LABELS: Record<SettingsTab, string>;
19
55
  /**
20
56
  * One-line explanation of what each destination is for, shown on hover over the
@@ -24,10 +60,15 @@ declare const TAB_LABELS: Record<SettingsTab, string>;
24
60
  */
25
61
  declare const TAB_DESCRIPTIONS: Record<SettingsTab, string>;
26
62
  /**
27
- * Resolve a `SettingsTab` from a URL pathname. Looks for the last segment that
28
- * matches a known slug; falls back to `"profile"` (the default Account tab).
63
+ * Resolve a `SettingsTab` from a URL pathname. Two-segment slugs
64
+ * (`workspace/members`) win over single ones, so `/settings/workspace/members`
65
+ * resolves to the member list rather than to the workspace's General tab.
66
+ * Falls back to `"profile"` (the default Account tab).
29
67
  *
30
- * Example: `/settings/billing` → `"billing"`.
68
+ * Examples:
69
+ * `/settings/billing` → `"billing"`
70
+ * `/settings/workspace/members`→ `"workspaceMembers"`
71
+ * `/settings/team` → `"workspaceMembers"` (legacy)
31
72
  */
32
73
  declare function resolveSettingsTabFromPath(path: string): SettingsTab;
33
74
 
@@ -744,7 +785,17 @@ type WorkspaceInfo = SettingsWorkspaceListItem & {
744
785
  ownerEmail?: string;
745
786
  };
746
787
  declare function ProfileTab(): react.JSX.Element;
747
- declare function WorkspaceTab({ workspaces, loading, onCreateClick: _onCreateClick, onEditClick, onSwitchClick, onLeaveClick, }: {
788
+ /**
789
+ * Workspace › All workspaces — every workspace you belong to, for switching
790
+ * between them and starting new ones.
791
+ *
792
+ * This was the whole of the old "Workspace" tab, which is why that tab never
793
+ * looked like settings: it is a picker. It keeps the per-row Settings action
794
+ * (opening `WorkspaceModal`) because from here you may well be editing a
795
+ * workspace you are not currently inside — the current one has its own
796
+ * General page.
797
+ */
798
+ declare function WorkspaceListTab({ workspaces, loading, onCreateClick: _onCreateClick, onEditClick, onSwitchClick, onLeaveClick, }: {
748
799
  workspaces: WorkspaceInfo[];
749
800
  loading: boolean;
750
801
  onCreateClick: () => void;
@@ -752,6 +803,12 @@ declare function WorkspaceTab({ workspaces, loading, onCreateClick: _onCreateCli
752
803
  onSwitchClick: (ws: WorkspaceInfo) => void;
753
804
  onLeaveClick: (ws: WorkspaceInfo) => void;
754
805
  }): react.JSX.Element;
806
+ /**
807
+ * @deprecated Renamed to `WorkspaceListTab` in 0.4.0 — the tab is a workspace
808
+ * picker, not the settings of a workspace. Kept as an alias so consumers on
809
+ * the old name keep compiling.
810
+ */
811
+ declare const WorkspaceTab: typeof WorkspaceListTab;
755
812
  declare function ReferralTab(): react.JSX.Element;
756
813
  interface SettingsContentProps {
757
814
  adapters: SettingsAdapters;
@@ -770,6 +827,44 @@ interface SettingsContentProps {
770
827
  }
771
828
  declare function SettingsContent({ adapters, activeTab, onClose, }: SettingsContentProps): react.JSX.Element;
772
829
 
830
+ interface WorkspaceData {
831
+ id: string;
832
+ name: string;
833
+ initials: string;
834
+ role: string;
835
+ plan: string;
836
+ ownerEmail: string;
837
+ isCurrent: boolean;
838
+ imageUrl?: string;
839
+ imageName?: string;
840
+ }
841
+ interface WorkspaceModalProps {
842
+ open: boolean;
843
+ mode: "create" | "update";
844
+ workspace?: WorkspaceData | null;
845
+ onClose: () => void;
846
+ onCreate?: (name: string, imageFile: File | null) => void | boolean | Promise<void | boolean>;
847
+ onUpdate?: (name: string, imageFile: File | null, imageRemoved: boolean) => void | boolean | Promise<void | boolean>;
848
+ onLeave?: () => void | boolean | Promise<void | boolean>;
849
+ onDelete?: () => void | boolean | Promise<void | boolean>;
850
+ }
851
+ declare function WorkspaceModal({ open, mode, workspace, onClose, onCreate, onUpdate, onLeave, onDelete, }: WorkspaceModalProps): react.JSX.Element | null;
852
+
853
+ interface WorkspaceGeneralTabProps {
854
+ /** The workspace being edited — always the currently active one. */
855
+ workspace: WorkspaceData | null;
856
+ loading: boolean;
857
+ /** Members in this workspace, when the host has counted them. */
858
+ memberCount?: number | null;
859
+ /** Name of the parent organization, when the workspace has joined one. */
860
+ organizationName?: string | null;
861
+ saving: boolean;
862
+ onSave: (name: string, imageFile: File | null, imageRemoved: boolean) => Promise<boolean>;
863
+ onLeave: () => void;
864
+ onDelete: () => void;
865
+ }
866
+ declare function WorkspaceGeneralTab({ workspace, loading, memberCount, organizationName, saving, onSave, onLeave, onDelete, }: WorkspaceGeneralTabProps): react.JSX.Element;
867
+
773
868
  type IconProps = Omit<SVGProps<SVGSVGElement>, 'color' | 'strokeWidth' | 'width' | 'height'> & {
774
869
  color?: string;
775
870
  strokeWidth?: number | string;
@@ -785,14 +880,36 @@ declare function BillingContent({ embedded, showPlans: showPlansProp, onShowPlan
785
880
  onShowPlansChange?: (open: boolean) => void;
786
881
  }): react__default.JSX.Element;
787
882
 
788
- declare function TeamsContent({ embedded, onInviteOpen, onInviteOpenChange, }?: {
883
+ declare function TeamsContent({ embedded, onInviteOpen, onInviteOpenChange, workspaceName, workspaceImageUrl, organizationName, }?: {
789
884
  embedded?: boolean;
790
885
  onInviteOpen?: boolean;
791
886
  onInviteOpenChange?: (open: boolean) => void;
887
+ /**
888
+ * The workspace these members belong to. Supplied by the settings shell so
889
+ * the page can name its own subject — without it this screen was a list of
890
+ * people with no indication of which workspace they were in, and switching
891
+ * workspaces silently changed the contents.
892
+ */
893
+ workspaceName?: string | null;
894
+ workspaceImageUrl?: string;
895
+ /** Parent organization, when the workspace has joined one. */
896
+ organizationName?: string | null;
792
897
  }): react.JSX.Element;
793
898
 
794
- declare function OrganizationContent({ embedded, onInviteOpen, onInviteOpenChange, }?: {
899
+ /**
900
+ * Which slice of the organization to show. The tab used to stack all three on
901
+ * one scrolling page, so the members table, the workspace list and the join
902
+ * requests competed for the same screen and the sections below the table were
903
+ * easy to miss entirely.
904
+ *
905
+ * `people` and `domains` split invites by kind on purpose: an email invite is
906
+ * a person you are expecting, a domain invite is a standing rule. They were
907
+ * interleaved in one "Pending" list, where a row could be either.
908
+ */
909
+ type OrganizationView = "people" | "workspaces" | "domains";
910
+ declare function OrganizationContent({ embedded, view, onInviteOpen, onInviteOpenChange, }?: {
795
911
  embedded?: boolean;
912
+ view?: OrganizationView;
796
913
  onInviteOpen?: boolean;
797
914
  onInviteOpenChange?: (open: boolean) => void;
798
915
  }): react.JSX.Element;
@@ -818,6 +935,84 @@ interface ProfileAvatarProps {
818
935
  */
819
936
  declare function ProfileAvatar({ name, profileImage, size, initialsClassName, borderRadius, }: ProfileAvatarProps): react.JSX.Element;
820
937
 
938
+ interface ScopeHeaderProps {
939
+ /** Which level of the hierarchy this page acts on. */
940
+ kind: "workspace" | "organization";
941
+ /** The entity's own name. Falls back to the kind when still loading. */
942
+ name?: string | null;
943
+ imageUrl?: string;
944
+ /** The viewer's role in this entity. */
945
+ role?: "admin" | "member" | string | null;
946
+ /**
947
+ * Short factual phrases about the entity — member counts, parent org,
948
+ * workspace counts. Rendered as a single "·"-separated line; empty and
949
+ * nullish entries are dropped so callers can pass conditionals inline.
950
+ */
951
+ facts?: Array<string | null | undefined | false>;
952
+ /** Optional trailing control (e.g. a "Manage" button). */
953
+ action?: ReactNode;
954
+ }
955
+ declare function ScopeHeader({ kind, name, imageUrl, role, facts, action, }: ScopeHeaderProps): react.JSX.Element;
956
+ /** `"1 member"` / `"12 members"` — keeps the plural out of every call site. */
957
+ declare function countLabel(n: number, singular: string, plural?: string): string;
958
+
959
+ type MemberListTab = "active" | "pending";
960
+ interface MemberListToolbarProps {
961
+ activeTab: MemberListTab;
962
+ onTabChange: (tab: MemberListTab) => void;
963
+ /** Label for the left tab — "Active" for a workspace, "Members" for an org. */
964
+ activeLabel: string;
965
+ /** Row counts, appended to each tab label. Pass `null` while still loading. */
966
+ activeCount: number | null;
967
+ pendingCount: number | null;
968
+ searchQuery: string;
969
+ onSearchChange: (value: string) => void;
970
+ /** Optional trailing control, e.g. an Invite button on the page itself. */
971
+ action?: ReactNode;
972
+ }
973
+ declare function MemberListToolbar({ activeTab, onTabChange, activeLabel, activeCount, pendingCount, searchQuery, onSearchChange, action, }: MemberListToolbarProps): react.JSX.Element;
974
+ interface MemberListFrameProps {
975
+ /** Column labels, pinned while the rows scroll under them. */
976
+ header?: ReactNode;
977
+ /** Shown in place of the rows when `isEmpty`. */
978
+ emptyText: string;
979
+ isEmpty: boolean;
980
+ /**
981
+ * `"fill"` claims the remaining height and scrolls internally — right for a
982
+ * page whose only content is the list. `"cap"` stops at `maxHeight` so
983
+ * sections below it stay reachable.
984
+ */
985
+ grow?: "fill" | "cap";
986
+ maxHeight?: number;
987
+ /**
988
+ * Minimum width of the rows. Member rows are a flexible identity column plus
989
+ * several fixed ones, so on a narrow screen the identity column is what
990
+ * collapses — leaving an avatar and a role chip with no name or email beside
991
+ * them. Holding the content to a minimum makes the frame scroll sideways
992
+ * instead, which keeps every column readable.
993
+ */
994
+ contentMinWidth?: number;
995
+ children?: ReactNode;
996
+ }
997
+ declare function MemberListFrame({ header, emptyText, isEmpty, grow, maxHeight, contentMinWidth, children, }: MemberListFrameProps): react.JSX.Element;
998
+ /**
999
+ * A small pill marking a workspace member who is *also* a member of the
1000
+ * organization above this workspace.
1001
+ *
1002
+ * It answers a question neither list used to acknowledge: removing someone
1003
+ * here may not remove all their access. Per the org contract, admins of the
1004
+ * parent organization keep implicit access to its workspaces, and a workspace
1005
+ * set to organization-wide visibility is readable by every org member.
1006
+ *
1007
+ * Derived by intersecting this workspace's members with the organization's, so
1008
+ * it states a fact both APIs already report — it is not a guess at an
1009
+ * "inherited permission" flag, which no endpoint exposes.
1010
+ */
1011
+ declare function OrgMembershipTag({ organizationName, isOrgAdmin, }: {
1012
+ organizationName?: string | null;
1013
+ isOrgAdmin?: boolean;
1014
+ }): react.JSX.Element;
1015
+
821
1016
  type SettingsTooltipPlacement = "right" | "top" | "bottom";
822
1017
  interface SettingsTooltipProps {
823
1018
  /** Explanation shown on hover / keyboard focus. Falsy renders the child bare. */
@@ -836,29 +1031,6 @@ interface SettingsTooltipProps {
836
1031
  }
837
1032
  declare function SettingsTooltip({ content, placement, delay, children, className, inline, }: SettingsTooltipProps): react.JSX.Element;
838
1033
 
839
- interface WorkspaceData {
840
- id: string;
841
- name: string;
842
- initials: string;
843
- role: string;
844
- plan: string;
845
- ownerEmail: string;
846
- isCurrent: boolean;
847
- imageUrl?: string;
848
- imageName?: string;
849
- }
850
- interface WorkspaceModalProps {
851
- open: boolean;
852
- mode: "create" | "update";
853
- workspace?: WorkspaceData | null;
854
- onClose: () => void;
855
- onCreate?: (name: string, imageFile: File | null) => void | boolean | Promise<void | boolean>;
856
- onUpdate?: (name: string, imageFile: File | null, imageRemoved: boolean) => void | boolean | Promise<void | boolean>;
857
- onLeave?: () => void | boolean | Promise<void | boolean>;
858
- onDelete?: () => void | boolean | Promise<void | boolean>;
859
- }
860
- declare function WorkspaceModal({ open, mode, workspace, onClose, onCreate, onUpdate, onLeave, onDelete, }: WorkspaceModalProps): react.JSX.Element | null;
861
-
862
1034
  interface OrganizationData {
863
1035
  id: string;
864
1036
  name: string;
@@ -1002,4 +1174,4 @@ declare function SettingsAdaptersProvider({ adapters, children, }: {
1002
1174
  */
1003
1175
  declare function useSettingsAdapters(): SettingsAdapters;
1004
1176
 
1005
- export { type ApiToken, type AvailableCreditsResponse, BillingContent, type BillingEventPayload, type BillingProduct, type BillingSubscription, BuyCreditsModal, type BuyCreditsPayload, type BuyCreditsResponse, ConfirmDeleteModal, ConfirmLeaveModal, type CreatedToken, type CustomCreditInfo, type CustomCreditResponse, DEFAULT_PERMISSIONS, ExportDateModal, InviteUpdateModal, type InvoicePdfResponse, type MemberPermissionsPayload, OrganizationContent, type OrganizationData, type OrganizationInvite, OrganizationInviteModal, type OrganizationInviteModalProps, type OrganizationJoinRequest, type OrganizationMember, OrganizationModal, type OrganizationWorkspaceItem, type Price, type PriceMetadata, type ProductsResponse, ProfileAvatar, ProfileTab, ReferralTab, type ServiceItem, type SettingsAdapters, SettingsAdaptersProvider, type SettingsApiAdapter, type SettingsAuthAdapter, type SettingsAuthUser, SettingsContent, type SettingsContentProps, type SettingsImageUploadAdapter, type SettingsNavigationAdapter, type SettingsOrganizationAdapter, type SettingsOrganizationCurrent, type SettingsOrganizationListItem, type SettingsTab, type SettingsToastAdapter, SettingsTooltip, type SettingsTooltipPlacement, type SettingsTooltipProps, type SettingsWorkspaceAdapter, type SettingsWorkspaceCurrent, type SettingsWorkspaceListItem, SiconKey, type StoreItem, type SubscriptionPlan, type SubscriptionResponse, TAB_DESCRIPTIONS, TAB_LABELS, TAB_TO_URL, type TeamMember, TeamsContent, type TokenExpiry, TokensContent, URL_TO_TAB, type UpdateSubscriptionPayload, type UpdateSubscriptionResponse, type UsageHistoryItem, type UsageHistoryResponse, type UserPermissions, type WorkspaceData, type WorkspaceInvite, type WorkspaceMember, WorkspaceModal, WorkspaceTab, resolveSettingsTabFromPath, useSettingsAdapters };
1177
+ export { type ApiToken, type AvailableCreditsResponse, BillingContent, type BillingEventPayload, type BillingProduct, type BillingSubscription, BuyCreditsModal, type BuyCreditsPayload, type BuyCreditsResponse, ConfirmDeleteModal, ConfirmLeaveModal, type CreatedToken, type CustomCreditInfo, type CustomCreditResponse, DEFAULT_PERMISSIONS, ExportDateModal, InviteUpdateModal, type InvoicePdfResponse, MemberListFrame, type MemberListFrameProps, type MemberListTab, MemberListToolbar, type MemberListToolbarProps, type MemberPermissionsPayload, OrgMembershipTag, OrganizationContent, type OrganizationData, type OrganizationInvite, OrganizationInviteModal, type OrganizationInviteModalProps, type OrganizationJoinRequest, type OrganizationMember, OrganizationModal, type OrganizationView, type OrganizationWorkspaceItem, type Price, type PriceMetadata, type ProductsResponse, ProfileAvatar, ProfileTab, ReferralTab, SCOPE_LABELS, ScopeHeader, type ScopeHeaderProps, type ServiceItem, type SettingsAdapters, SettingsAdaptersProvider, type SettingsApiAdapter, type SettingsAuthAdapter, type SettingsAuthUser, SettingsContent, type SettingsContentProps, type SettingsImageUploadAdapter, type SettingsNavigationAdapter, type SettingsOrganizationAdapter, type SettingsOrganizationCurrent, type SettingsOrganizationListItem, type SettingsScope, type SettingsTab, type SettingsToastAdapter, SettingsTooltip, type SettingsTooltipPlacement, type SettingsTooltipProps, type SettingsWorkspaceAdapter, type SettingsWorkspaceCurrent, type SettingsWorkspaceListItem, SiconKey, type StoreItem, type SubscriptionPlan, type SubscriptionResponse, TAB_DESCRIPTIONS, TAB_LABELS, TAB_SCOPE, TAB_TO_URL, type TeamMember, TeamsContent, type TokenExpiry, TokensContent, URL_TO_TAB, type UpdateSubscriptionPayload, type UpdateSubscriptionResponse, type UsageHistoryItem, type UsageHistoryResponse, type UserPermissions, type WorkspaceData, WorkspaceGeneralTab, type WorkspaceGeneralTabProps, type WorkspaceInvite, WorkspaceListTab, type WorkspaceMember, WorkspaceModal, WorkspaceTab, countLabel, resolveSettingsTabFromPath, useSettingsAdapters };