@streamoid/settings
v0.6.2
Published
Shared Settings module (Account, Workspace, Organization, Billing, API Keys, Referral). The host application provides auth, workspace, API, navigation, toast, and image-upload implementations through a single **adapter** object — the package contains no h
Readme
@streamoid/settings
Shared Settings module (Account, Workspace, Organization, Billing, API Keys, Referral). The host application provides auth, workspace, API, navigation, toast, and image-upload implementations through a single adapter object — the package contains no host-specific code.
Install
npm install @streamoid/settings @streamoid/ui @streamoid/iconsreact, react-dom, @streamoid/ui, and @streamoid/icons are peer deps.
Usage
import { SettingsContent, type SettingsAdapters } from "@streamoid/settings";
function MySettingsPage() {
const adapters: SettingsAdapters = {
auth: {
user: currentUser,
refreshProfile: async () => { /* ... */ },
updateProfile: async (patch) => { /* PATCH /profile */ },
clearAuth: () => { /* clear local auth state */ },
logout: async () => { /* POST /logout */ },
},
workspace: {
current: { id: currentWorkspaceId, name, role },
list: async () => fetchWorkspaceList(),
create: async ({ name, imageUrl }) => { /* POST /workspace */ },
update: async (id, patch) => { /* PATCH /workspace/:id */ },
switch: async (id) => { /* set active workspace */ },
leave: async (id) => { /* DELETE /workspace/:id/leave */ },
delete: async (id) => { /* DELETE /workspace/:id */ },
},
api: {
fetchPlans: () => fetch("/billing/plans").then(r => r.json()),
fetchCurrentPlan: () => fetch("/billing/subscription").then(r => r.json()),
fetchCreditsUsage: async () => ({ used: 120, total: 500 }),
fetchInvoices: () => fetch("/billing/invoices").then(r => r.json()),
fetchUsageLogs: () => fetch("/billing/usage").then(r => r.json()),
downloadInvoice: (id) => fetch(`/billing/invoices/${id}`).then(r => r.json()),
exportAllInvoices: () => fetch(`/billing/invoices/export`).then(r => r.json()),
buyCredits: ({ quantity }) => fetch("/billing/credits", { method: "POST", body: JSON.stringify({ quantity }) }),
upgradePlan: (planId) => fetch(`/billing/upgrade/${planId}`, { method: "POST" }),
fetchMembers: () => fetch("/team/members").then(r => r.json()),
fetchPendingInvites: () => fetch("/team/invites").then(r => r.json()),
inviteMember: ({ email, role }) => fetch("/team/invites", { method: "POST", body: JSON.stringify({ email, role }) }),
updateMemberRole: (id, role) => fetch(`/team/members/${id}`, { method: "PATCH", body: JSON.stringify({ role }) }),
removeMember: (id) => fetch(`/team/members/${id}`, { method: "DELETE" }).then(() => undefined),
cancelInvite: (id) => fetch(`/team/invites/${id}`, { method: "DELETE" }).then(() => undefined),
resendInvite: (id) => fetch(`/team/invites/${id}/resend`, { method: "POST" }).then(() => undefined),
fetchTokens: () => fetch("/tokens").then(r => r.json()),
createToken: ({ name, scopes, expiresAt }) => fetch("/tokens", { method: "POST", body: JSON.stringify({ name, scopes, expiresAt }) }).then(r => r.json()),
revokeToken: (id) => fetch(`/tokens/${id}`, { method: "DELETE" }).then(() => undefined),
},
navigation: {
// `replace` matters once you wire up `onClose` below — otherwise a
// history-based close walks back through every tab the user visited.
navigateToTab: (tab) => router.replace(`/settings/${TAB_TO_URL[tab]}`),
openExternal: (url) => window.open(url, "_blank", "noopener"),
},
toast: {
success: (msg) => toast.success(msg),
error: (msg) => toast.error(msg),
info: (msg) => toast(msg),
},
imageUpload: {
upload: (file) => uploadToR2(file),
},
};
return (
<SettingsContent
activeTab={resolveSettingsTabFromPath(location.pathname)}
adapters={adapters}
onClose={() => router.back()}
/>
);
}Closing settings
Settings is a full page in every host, so the package cannot decide what
"closed" means — pass onClose (or adapters.navigation.close) and the shell
renders a × button in the header and dismisses on Escape. Pass neither and no
close affordance appears.
Escape defers to anything stacked on top of the panel. overlay-registry.ts
keeps an ordered stack of open modals — only the frontmost one acts on a press,
and the shell acts only once the stack is empty. The billing Plans sub-view
steps back to billing before the panel itself closes.
Modals added to this package should call useOverlayOpen(open, onClose) at the
top of the component. That is what registers them in the stack; skip it and
Escape will close the whole panel out from under the modal.
Navigation is scope-first (0.4.0)
Destinations are grouped by whose settings they are. workspace/* tabs act
on the workspace you are currently in; organization/* tabs act on the org
above it:
| Group | Tabs | URL |
|---|---|---|
| — | profile | /account |
| Workspace (named) | workspace, workspaceMembers, workspaceList | /workspace/general, /workspace/members, /workspace/all |
| Organization (named) | organization, organizationWorkspaces, organizationDomains | /organization/people, /organization/workspaces, /organization/domains |
| — | billing, apikey | /billing, /api-keys |
The group headings carry the entity's own name, so no leaf label repeats the word "Workspace" or "Organization" — and "Team", which named a workspace's member list without saying which workspace, is gone.
Host routing. Scoped slugs have two segments, so a host matching
/settings/:tab needs a second route for /settings/:tab/:sub.
resolveSettingsTabFromPath prefers the two-segment match and still resolves
the pre-0.4 slugs team, workspace and organization, so old links keep
working.
Organizations are optional. When organization.current is null the whole
group collapses to one Organization row leading to the create/join screen,
rather than a heading with no name and three empty pages under it.
Breaking: the "team" member of SettingsTab is now "workspaceMembers",
and WorkspaceTab is renamed WorkspaceListTab (the old name stays as a
deprecated alias).
Explaining the tabs
Every sub-nav destination carries a hover/focus tooltip sourced from the
exported TAB_DESCRIPTIONS map. Override the copy in your host by rendering
your own nav, or edit the map here so all hosts stay consistent — the latter is
usually what you want.
Adapter surface
| Adapter | Purpose |
|---|---|
| auth | Current user + profile mutations + logout |
| workspace | Current workspace + workspace CRUD + switch |
| api | Billing, team management, and API token endpoints |
| navigation | Imperative navigation between tabs and to external URLs |
| toast | User-facing success / error / info notifications |
| imageUpload | Single upload(file) for avatar / workspace image uploads |
See src/adapters.ts for full type definitions.
Status
- 0.1.0 — Adapter surface and package scaffold. Tab content lands in
subsequent releases as files migrate from
cxo-dashboardinto the package.
