@streamoid/ui 0.6.17 → 0.6.18

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.
Files changed (131) hide show
  1. package/README.md +35 -18
  2. package/dist/docs/AGENTS.md +321 -0
  3. package/dist/docs/CreditWarningBanner.md +305 -0
  4. package/dist/docs/InvoiceHistoryMobile.md +222 -0
  5. package/dist/docs/ScAccess.md +259 -0
  6. package/dist/docs/ScAppCard.md +244 -0
  7. package/dist/docs/ScAppCardForCopilot.md +230 -0
  8. package/dist/docs/ScAppCardV3.md +273 -0
  9. package/dist/docs/ScAppField.md +308 -0
  10. package/dist/docs/ScAppListingCard.md +271 -0
  11. package/dist/docs/ScAppSwitchPanel.md +286 -0
  12. package/dist/docs/ScAppcardLogos.md +226 -0
  13. package/dist/docs/ScArtifaxInvite.md +262 -0
  14. package/dist/docs/ScArtifaxSidebar.md +330 -0
  15. package/dist/docs/ScAskAgentButton.md +307 -0
  16. package/dist/docs/ScBadges.md +261 -0
  17. package/dist/docs/ScBeacon.md +244 -0
  18. package/dist/docs/ScBillingHistoryHeader.md +210 -0
  19. package/dist/docs/ScBillingHistoryTableList.md +243 -0
  20. package/dist/docs/ScBillingLogsTableHeader.md +212 -0
  21. package/dist/docs/ScBillingLogsTableList.md +251 -0
  22. package/dist/docs/ScBriefCard.md +255 -0
  23. package/dist/docs/ScButton.md +251 -0
  24. package/dist/docs/ScCalendar.md +268 -0
  25. package/dist/docs/ScCalendarDateComps.md +264 -0
  26. package/dist/docs/ScCatalogixInvite.md +345 -0
  27. package/dist/docs/ScCatalogixSidebar.md +337 -0
  28. package/dist/docs/ScCatalogixStoreHeader.md +246 -0
  29. package/dist/docs/ScCatalogixStoreTableList.md +316 -0
  30. package/dist/docs/ScCheckField.md +233 -0
  31. package/dist/docs/ScCheckbox.md +272 -0
  32. package/dist/docs/ScCounter.md +235 -0
  33. package/dist/docs/ScCreditsUsageCard.md +247 -0
  34. package/dist/docs/ScCreditsUsageCardMobile.md +224 -0
  35. package/dist/docs/ScDefaultCard.md +269 -0
  36. package/dist/docs/ScDp.md +245 -0
  37. package/dist/docs/ScDrawer.md +318 -0
  38. package/dist/docs/ScFieldButton.md +255 -0
  39. package/dist/docs/ScFileField.md +268 -0
  40. package/dist/docs/ScGoogleSignIn.md +250 -0
  41. package/dist/docs/ScGuide.md +278 -0
  42. package/dist/docs/ScHDivider.md +213 -0
  43. package/dist/docs/ScHeader.md +222 -0
  44. package/dist/docs/ScImageField.md +253 -0
  45. package/dist/docs/ScInChatList.md +277 -0
  46. package/dist/docs/ScInChatMessage.md +205 -0
  47. package/dist/docs/ScInfoPopup.md +248 -0
  48. package/dist/docs/ScIntialProfileCover.md +233 -0
  49. package/dist/docs/ScInvoiceHistoryMobile.md +187 -0
  50. package/dist/docs/ScLogoUnit.md +232 -0
  51. package/dist/docs/ScMappingCard.md +241 -0
  52. package/dist/docs/ScMediaApproval.md +301 -0
  53. package/dist/docs/ScMediaSelect.md +310 -0
  54. package/dist/docs/ScMenuOptions.md +308 -0
  55. package/dist/docs/ScMobileBottomAction.md +252 -0
  56. package/dist/docs/ScMobileTopNav.md +279 -0
  57. package/dist/docs/ScModal.md +291 -0
  58. package/dist/docs/ScOnlyField.md +302 -0
  59. package/dist/docs/ScOnlyIcon.md +213 -0
  60. package/dist/docs/ScPagination.md +284 -0
  61. package/dist/docs/ScPairtext.md +287 -0
  62. package/dist/docs/ScPendingAction.md +238 -0
  63. package/dist/docs/ScPhtogenixInvite.md +275 -0
  64. package/dist/docs/ScPlanCard.md +302 -0
  65. package/dist/docs/ScPlanComparison.md +264 -0
  66. package/dist/docs/ScPlanDetailsCard.md +246 -0
  67. package/dist/docs/ScPlanDetailsCardMobile.md +240 -0
  68. package/dist/docs/ScPopUpMenu.md +224 -0
  69. package/dist/docs/ScProfile.md +234 -0
  70. package/dist/docs/ScProfileImageUpdate.md +261 -0
  71. package/dist/docs/ScProfileOptions.md +245 -0
  72. package/dist/docs/ScProfilePopup.md +396 -0
  73. package/dist/docs/ScProfileSettingsComp.md +250 -0
  74. package/dist/docs/ScProfileV2Mobile.md +216 -0
  75. package/dist/docs/ScProgressBar.md +267 -0
  76. package/dist/docs/ScQuickPrompt.md +277 -0
  77. package/dist/docs/ScRadio.md +228 -0
  78. package/dist/docs/ScReferralCardMobile.md +226 -0
  79. package/dist/docs/ScReferralTableHeader.md +260 -0
  80. package/dist/docs/ScReferralTableList.md +293 -0
  81. package/dist/docs/ScRole.md +226 -0
  82. package/dist/docs/ScRoleMobile.md +199 -0
  83. package/dist/docs/ScSelect.md +270 -0
  84. package/dist/docs/ScSelection.md +256 -0
  85. package/dist/docs/ScSelectionList.md +272 -0
  86. package/dist/docs/ScSelectionPill.md +240 -0
  87. package/dist/docs/ScSelectionPillGroup.md +302 -0
  88. package/dist/docs/ScSettingsNav.md +212 -0
  89. package/dist/docs/ScSettingsTabComp.md +260 -0
  90. package/dist/docs/ScSideBarLogoUnit.md +340 -0
  91. package/dist/docs/ScSidebar.md +243 -0
  92. package/dist/docs/ScSidebarIcons.md +232 -0
  93. package/dist/docs/ScSidebarMenu.md +283 -0
  94. package/dist/docs/ScSidebarProfile.md +231 -0
  95. package/dist/docs/ScSidebarSwitchMenu.md +258 -0
  96. package/dist/docs/ScSlider.md +194 -0
  97. package/dist/docs/ScStoreCard.md +252 -0
  98. package/dist/docs/ScStrLogo.md +253 -0
  99. package/dist/docs/ScStreamoidWordmark.md +302 -0
  100. package/dist/docs/ScSubAgent.md +226 -0
  101. package/dist/docs/ScTabComp.md +308 -0
  102. package/dist/docs/ScTabField.md +258 -0
  103. package/dist/docs/ScTabSwitcher.md +307 -0
  104. package/dist/docs/ScTableHeader.md +261 -0
  105. package/dist/docs/ScTableList.md +301 -0
  106. package/dist/docs/ScTableListMobile.md +282 -0
  107. package/dist/docs/ScTabs.md +268 -0
  108. package/dist/docs/ScTaxonomyPill.md +263 -0
  109. package/dist/docs/ScTextArea.md +259 -0
  110. package/dist/docs/ScTextField.md +324 -0
  111. package/dist/docs/ScThinkingStepIcon.md +249 -0
  112. package/dist/docs/ScTodoList.md +288 -0
  113. package/dist/docs/ScToggleSwitch.md +229 -0
  114. package/dist/docs/ScUsageHistoryMobile.md +194 -0
  115. package/dist/docs/ScVDivider.md +215 -0
  116. package/dist/docs/ScValueMappingL1.md +256 -0
  117. package/dist/docs/ScVersion.md +251 -0
  118. package/dist/docs/ScWorkspace.md +233 -0
  119. package/dist/docs/ScWorkspaceCard.md +234 -0
  120. package/dist/docs/ScWorkspaceSettingsMobile.md +265 -0
  121. package/dist/docs/ScWorkspaceSwitchCard.md +314 -0
  122. package/dist/docs/ScWorkspaceSwitchMobile.md +241 -0
  123. package/dist/docs/ScWorkspaceSwitchMobileV2.md +278 -0
  124. package/dist/docs/StreamoidSidebar.md +403 -0
  125. package/dist/docs/StreamoidWorkspaceSwitcher.md +307 -0
  126. package/dist/docs/UsageHistoryMobile.md +235 -0
  127. package/dist/docs/components.json +4849 -0
  128. package/dist/index.css +36 -36
  129. package/dist/index.d.mts +10 -0
  130. package/dist/index.d.ts +10 -0
  131. package/package.json +3 -2
@@ -0,0 +1,307 @@
1
+ ---
2
+ component: StreamoidWorkspaceSwitcher
3
+ package: "@streamoid/ui"
4
+ category: overlays
5
+ status: legacy
6
+ renders: div
7
+ tags: [workspace-switcher, switch-workspace, modal, overlay, tenant, workspace-list, tailwind]
8
+ related: [ScWorkspaceSwitchCard, ScIntialProfileCover, ScRole, ScPairtext, ScProfilePopup]
9
+ do_not_confuse_with: [ScWorkspaceSwitchCard, ScWorkspaceCard, ScWorkspace, ScWorkspaceSwitchMobileV2, ScModal]
10
+ required_props: [open, onClose, workspaces, onSwitchWorkspace]
11
+ ---
12
+
13
+ # StreamoidWorkspaceSwitcher
14
+
15
+ **The DS's prebuilt "switch workspace" modal — and the one component in this family
16
+ that needs Tailwind in the host.** A 1000×60vh centred dialog with a title bar, a
17
+ close icon, a scrolling list of workspace rows (initials tile, name, role pill, user
18
+ count, owner, and a Switch / Current button), Escape-to-close and backdrop-click
19
+ dismissal. Note the export is **`StreamoidWorkspaceSwitcher`** — no `Sc` prefix.
20
+
21
+ ## TL;DR for agents
22
+
23
+ - **Reach for it when:** your host already runs Tailwind and you want the whole
24
+ switch-workspace modal without building the shell.
25
+ - **Don't reach for it when:** you want the row only (→ `ScWorkspaceSwitchCard`, what
26
+ all four apps actually use), your host has no Tailwind, or you need the current/other
27
+ grouping and a search box (this component has neither).
28
+ - **Four things that will bite you:**
29
+ 1. Its layout is written in **Tailwind utility classes** (`fixed inset-0 z-50`,
30
+ `flex`, `truncate`, `size-full`, `min-w-0`, `overflow-y-auto`) that
31
+ `@streamoid/ui/dist/index.css` **does not ship**. Without Tailwind in the host
32
+ the modal does not overlay, does not scroll and does not centre.
33
+ 2. **No host app uses it.** CXO, Catalogix, Photogenix and Artifax each hand-roll
34
+ the modal around `ScWorkspaceSwitchCard` instead.
35
+ 3. It returns `null` when `open` is false — it does **not** render a hidden panel,
36
+ and it does not portal, so it inherits your stacking context.
37
+ 4. It has no `className` and no style escape hatch. Every dimension (1000px cap,
38
+ 60vh height, the 80/120/200px column widths) is hardcoded inline.
39
+
40
+ ---
41
+
42
+ ## 1. How to use it
43
+
44
+ ### Import
45
+
46
+ ```tsx
47
+ import {
48
+ StreamoidWorkspaceSwitcher,
49
+ type WorkspaceSwitcherItem,
50
+ type WorkspaceSwitcherConfig,
51
+ } from "@streamoid/ui";
52
+ import "@streamoid/ui/dist/index.css"; // once, at your app root — plus Tailwind, see Gotcha 1
53
+ ```
54
+
55
+ > The previous version of this file said `@streamoid/workspace`. **That package does
56
+ > not exist** — this component ships in `@streamoid/ui`.
57
+
58
+ ### Minimal usage
59
+
60
+ ```tsx
61
+ <StreamoidWorkspaceSwitcher
62
+ open={open}
63
+ onClose={() => setOpen(false)}
64
+ workspaces={workspaces}
65
+ onSwitchWorkspace={(ws) => void switchTo(ws.id)}
66
+ />
67
+ ```
68
+
69
+ ### Props
70
+
71
+ `StreamoidWorkspaceSwitcherProps`
72
+
73
+ | Prop | Type | Default | Notes |
74
+ |---|---|---|---|
75
+ | `open` | `boolean` | — | **Required.** `false` → returns `null` (nothing mounted). |
76
+ | `onClose` | `() => void` | — | **Required.** Called by the close icon, a backdrop click (only when the click target *is* the backdrop), and Escape. |
77
+ | `workspaces` | `WorkspaceSwitcherItem[]` | — | **Required.** Rendered in array order — no grouping, no sorting, no search. |
78
+ | `onSwitchWorkspace` | `(ws: WorkspaceSwitcherItem) => void \| Promise<void>` | — | **Required.** Fires only for rows where `isCurrent` is falsy; a current row closes the modal instead. |
79
+ | `loading` | `boolean` | `false` | Shows `config.loadingText` **only while `workspaces` is also empty**. No spinner, no skeleton. |
80
+ | `config` | `WorkspaceSwitcherConfig` | – | The four label strings. |
81
+
82
+ `WorkspaceSwitcherItem`
83
+
84
+ | Field | Type | Notes |
85
+ |---|---|---|
86
+ | `id` | `string` | Required; used as the React `key`. |
87
+ | `name` | `string` | Required. Clamped to **2 lines** (`-webkit-line-clamp: 2`). |
88
+ | `initials` | `string?` | Falls back to first letters of the first two words, else `"WS"`. |
89
+ | `role` | `"admin" \| "member" \| string` | Normalised: **only the exact lowercase `"admin"` is admin**; everything else is member. |
90
+ | `userCount` | `string?` | Free text — you format it (`"12 Users"`). Renders `"–"` when absent. |
91
+ | `ownerEmail` | `string?` | Renders `"–"` when absent. |
92
+ | `isCurrent` | `boolean?` | Swaps the trailing button to a disabled "Current workspace" at `opacity: 0.5`. |
93
+
94
+ `WorkspaceSwitcherConfig`
95
+
96
+ | Field | Type | Default | Notes |
97
+ |---|---|---|---|
98
+ | `title` | `string?` | `"Switch workspace"` | Dialog title. |
99
+ | `loadingText` | `string?` | `"Loading workspaces…"` | Shown only when `loading && workspaces.length === 0`. |
100
+ | `currentWorkspaceText` | `string?` | `"Current workspace"` | Disabled button label on the current row. |
101
+ | `switchWorkspaceText` | `string?` | `"Switch workspace"` | Button label on every other row. |
102
+
103
+ ### What each row renders
104
+
105
+ | Slot | Content | Width |
106
+ |---|---|---|
107
+ | Tile | `ScIntialProfileCover` — **initials only, no image support** | 60px (its default) |
108
+ | Name | 2-line clamp | flex |
109
+ | Role | `ScRole type={admin\|member}` | forced `80px` inline |
110
+ | Users | `ScPairtext` with `SiconTeam` | `120px` inline |
111
+ | Owner | `SiconCrown` + truncated email | `200px` inline |
112
+ | Action | `ScButton` — `secondary` + `icon-left` switch, or `outline` + `state="disabled"` | `200px` inline |
113
+
114
+ ### Recipes
115
+
116
+ ```tsx
117
+ // Driving it from your workspace store
118
+ <StreamoidWorkspaceSwitcher
119
+ open={switcherOpen}
120
+ onClose={() => setSwitcherOpen(false)}
121
+ loading={wsLoading}
122
+ workspaces={workspaces.map((w) => ({
123
+ id: w.id,
124
+ name: w.name,
125
+ initials: w.initials,
126
+ role: w.role === "admin" ? "admin" : "member", // exact lowercase, see Gotcha 5
127
+ userCount: `${w.memberCount} Users`, // you format the string
128
+ ownerEmail: w.ownerEmail,
129
+ isCurrent: w.id === currentWorkspaceId,
130
+ }))}
131
+ onSwitchWorkspace={async (ws) => { await switchTo(ws.id); setSwitcherOpen(false); }}
132
+ config={{ title: "Switch workspace", switchWorkspaceText: "Switch" }}
133
+ />
134
+
135
+ // Opened from the profile menu
136
+ <ScProfilePopup /* … */ onSwitchWorkspace={() => setSwitcherOpen(true)} />
137
+
138
+ // What the hosts do instead: own the shell, use the row
139
+ <ScModal open={open} onClose={close}>
140
+ {others.map((ws) => (
141
+ <ScWorkspaceSwitchCard key={ws.id} wsName={ws.name} role={ws.role} plan={ws.plan}
142
+ ownerId={ws.ownerEmail} type="otherWS" onSwitch={() => void switchTo(ws)} />
143
+ ))}
144
+ </ScModal>
145
+ ```
146
+
147
+ A JSON shape for the labels + rows is checked in beside the source at
148
+ `src/SC-WorkspaceSwitcher/workspace-switcher.example.json`.
149
+
150
+ ---
151
+
152
+ ## 2. Where to use it
153
+
154
+ Intended for any desktop shell's "Switch workspace" flow, opened from
155
+ `ScProfilePopup`'s `onSwitchWorkspace` or a sidebar workspace row.
156
+
157
+ In practice **all four hosts built their own modal** and reused only the row:
158
+
159
+ | App | Their shell |
160
+ |---|---|
161
+ | CXO | `src/app/components/app-workspace-switcher.tsx` (+ search + current/other sections) |
162
+ | Catalogix | `app/containers/LeftMenu/WorkspaceListing/index.jsx` |
163
+ | Photogenix | `dashboard/client/src/components/layout/Sidebar.tsx` |
164
+ | Artifax | `packages/shared/src/components/DashboardSidebar.tsx` (also fetches plans per row) |
165
+
166
+ They did that because they needed search, current/other grouping, a plan column and
167
+ per-row settings/leave actions — none of which this component offers.
168
+
169
+ ---
170
+
171
+ ## 3. When to use it
172
+
173
+ ### Use it when
174
+
175
+ - The host already ships Tailwind **and** a flat, unsearchable list of workspaces is
176
+ genuinely enough.
177
+ - You want Escape + backdrop dismissal handed to you.
178
+
179
+ ### Don't use it — reach for this instead
180
+
181
+ | Situation | Use instead |
182
+ |---|---|
183
+ | Anything in a non-Tailwind host | `ScWorkspaceSwitchCard` inside `ScModal` / `ScDrawer` |
184
+ | You need search, grouping, or a plan column | `ScWorkspaceSwitchCard` in your own shell (what all four apps do) |
185
+ | You need per-row settings / leave actions | `ScWorkspaceSwitchCard`'s `onSettings` / `onLeave` |
186
+ | Workspace rows with avatars (not just initials) | `ScWorkspaceSwitchCard` (`imageUrl` → `ScDp`) |
187
+ | Mobile | `ScWorkspaceSwitchMobileV2` |
188
+ | A generic modal shell you fill yourself | `ScModal` |
189
+ | A workspace card with a user count and a CTA | `ScWorkspaceCard` |
190
+
191
+ ### Don't confuse with
192
+
193
+ | You may actually want | Not this |
194
+ |---|---|
195
+ | `ScWorkspaceSwitchCard` — **one row**, `wsName`/`role`/`plan`/`ownerId`, hover actions, avatar support | `StreamoidWorkspaceSwitcher` is the whole modal, initials only, no plan |
196
+ | `ScWorkspace` — the bare identity line (not exported) | this is a dialog |
197
+ | `ScWorkspaceCard` — card + persistent CTA | this lists many |
198
+ | `ScModal` — the DS modal primitive | this is a purpose-built dialog with no children slot |
199
+ | `ScProfilePopup` — the profile menu that *opens* this | different panel |
200
+
201
+ Naming: this is the only workspace component **without** the `Sc` prefix — the sibling
202
+ of `StreamoidSidebar` in that respect. `ScWorkspaceSwitcher` does not exist.
203
+
204
+ ---
205
+
206
+ ## 4. Why to use it
207
+
208
+ What it does give you, if the Tailwind precondition holds:
209
+
210
+ - **Dismissal wired three ways** — the close icon, Escape (a `keydown` listener added
211
+ only while `open`, and cleaned up), and a backdrop click that correctly checks
212
+ `e.target === e.currentTarget` so clicks inside the dialog don't close it.
213
+ - **`isCurrent` handled** — the current row's button becomes a disabled
214
+ "Current workspace", and clicking its switch path closes the modal instead of firing
215
+ `onSwitchWorkspace`.
216
+ - **Async-aware** — `onSwitchWorkspace` may return a promise; the component `void`s it
217
+ rather than assuming sync.
218
+ - **Composed from real DS parts** — `ScIntialProfileCover`, `ScRole`, `ScPairtext`,
219
+ `ScVDivider`, `ScButton` — so the row internals stay on-token.
220
+ - **Initials derivation and role normalisation** are built in.
221
+
222
+ What it costs: a Tailwind dependency, no style hooks, no search/grouping, and a
223
+ divergence from the row every host already renders. That is why it is marked
224
+ **legacy** here — not because it is broken, but because the ecosystem chose
225
+ `ScWorkspaceSwitchCard` + a host shell.
226
+
227
+ ---
228
+
229
+ ## Gotchas
230
+
231
+ **1. It needs Tailwind, and the DS does not ship it.** The overlay, centring,
232
+ scrolling and truncation are Tailwind utilities (`fixed inset-0 z-50 flex items-center
233
+ justify-center`, `flex-1 min-h-0 overflow-y-auto`, `truncate`, `size-full`, `w-6 h-6`,
234
+ `shrink-0`). None of those class names exist in `@streamoid/ui/dist/index.css`. In a
235
+ plain-CSS host the "modal" renders as an inline block in the document flow.
236
+
237
+ ```tsx
238
+ // WRONG in a non-Tailwind host — renders unstyled, in flow, with no overlay
239
+ <StreamoidWorkspaceSwitcher open workspaces={list} onClose={close} onSwitchWorkspace={go} />
240
+
241
+ // RIGHT — build the shell with tokens/ScModal and reuse the row
242
+ <ScModal open={open} onClose={close}>
243
+ {list.map((ws) => <ScWorkspaceSwitchCard key={ws.id} wsName={ws.name} type="otherWS" onSwitch={() => go(ws)} /* … */ />)}
244
+ </ScModal>
245
+ ```
246
+
247
+ **2. No `className`, no `style`, no spread.** `StreamoidWorkspaceSwitcherProps` has
248
+ exactly six keys. Width (`min(1000px, 90vw)`), height (`60vh`) and the 80/120/200px
249
+ column widths are inline literals you cannot reach.
250
+
251
+ **3. It does not portal.** The `fixed` root is rendered where you mount it, so it
252
+ inherits ancestor `transform` / `filter` / `contain` — any of which turn `fixed` into
253
+ "relative to that ancestor" and break the overlay. Mount it near the app root.
254
+
255
+ **4. `loading` only shows while the list is empty.** `loading && workspaces.length === 0`
256
+ is the condition, and the fallback is a single line of text — no spinner, and no
257
+ indication during a refresh of an already-populated list.
258
+
259
+ **5. Role matching is exact-lowercase.** `normalizeRole` is
260
+ `role === "admin" ? "admin" : "member"` — `"Admin"`, `"ADMIN"` and `"owner"` all render
261
+ the grey **Member** pill. Lowercase before you pass it. (This differs from
262
+ `ScWorkspaceSwitchCard`, which lowercases for you.)
263
+
264
+ **6. `userCount` is a formatted string, not a number.** Pass `"12 Users"`; a bare
265
+ number renders as bare digits with no unit.
266
+
267
+ **7. Initials only — no avatars.** The tile is `ScIntialProfileCover`, which has no
268
+ image support at all. `ScWorkspaceSwitchCard` uses `ScDp` and accepts `imageUrl`.
269
+
270
+ **8. `name` is clamped to two lines, not ellipsised to one.** Row height varies with
271
+ name length.
272
+
273
+ **9. The dialog has no ARIA.** No `role="dialog"`, no `aria-modal`, no `aria-labelledby`,
274
+ no focus trap and no focus restore on close. Escape works; nothing else about it is
275
+ accessible. Wrap it or use `ScModal` if that matters.
276
+
277
+ **10. Nothing in the rows is keyboard-reachable except the buttons.** The close "icon"
278
+ is a `<div onClick>` with `cursor: pointer` — not a button, not focusable, no
279
+ `aria-label`.
280
+
281
+ **11. `config` is all-or-nothing per field, and there is no `emptyText`.** An empty
282
+ `workspaces` array with `loading={false}` renders a blank scroll area.
283
+
284
+ ---
285
+
286
+ ## In the wild
287
+
288
+ _No host render site found — used by the agent runtime / composed internally._
289
+
290
+ No app imports it (the only hits outside the package are Vite's prebundled dep cache,
291
+ `artifax/frontend-react/.vite/deps/@streamoid_ui.js`, which is build output, not a
292
+ render site). The surface it belongs on is the workspace-switch modal each host builds
293
+ by hand — e.g. `cxo-dashboard src/app/components/app-workspace-switcher.tsx:408` or
294
+ `catalogix/dashboard app/containers/LeftMenu/WorkspaceListing/index.jsx:371`, both of
295
+ which render `ScWorkspaceSwitchCard` rows inside their own shell. Adopting this
296
+ component would mean adding search + current/other grouping + a plan column to it
297
+ first.
298
+
299
+ ---
300
+
301
+ ## Related
302
+
303
+ - `ScWorkspaceSwitchCard` — **the row all four hosts actually use**; richer than the row here.
304
+ - `ScModal` / `ScDrawer` — the shells to build your own switcher in.
305
+ - `ScIntialProfileCover` / `ScRole` / `ScPairtext` / `ScButton` — the parts each row composes.
306
+ - `ScProfilePopup` — `onSwitchWorkspace` is the trigger for this flow.
307
+ - `ScWorkspaceSwitchMobileV2` — the mobile surface.
@@ -0,0 +1,235 @@
1
+ ---
2
+ component: UsageHistoryMobile
3
+ package: "@streamoid/ui"
4
+ category: mobile
5
+ status: stable
6
+ renders: div
7
+ tags: [mobile, billing, usage, credits, log, history, row, list-item, cxo]
8
+ related: [InvoiceHistoryMobile, ScBillingLogsTableList, ScBillingLogsTableHeader, ScUsageHistoryMobile]
9
+ do_not_confuse_with: [ScUsageHistoryMobile, InvoiceHistoryMobile, ScBillingLogsTableList, ScCreditsUsageCardMobile]
10
+ used_by: [cxo]
11
+ ---
12
+
13
+ # UsageHistoryMobile
14
+
15
+ **One credit-usage log row in CXO's mobile billing list.** Two rows inside one row:
16
+ service name + a **red** credit figure on top, then `user · date` + a running balance
17
+ underneath, with a 0.5px bottom border. Note the export name: **no `Sc` prefix**.
18
+
19
+ ## TL;DR for agents
20
+
21
+ - **Reach for it when:** you are rendering the "Usage history" tab of a mobile billing
22
+ screen, one component per credit-debit log entry.
23
+ - **Don't reach for it when:** you're on desktop (→ `ScBillingLogsTableHeader` +
24
+ `ScBillingLogsTableList`), the row is an invoice (→ `InvoiceHistoryMobile`), or you
25
+ want the aggregate credits meter (→ `ScCreditsUsageCardMobile`).
26
+ - **Four things that will bite you:**
27
+ 1. **The export is `UsageHistoryMobile`, not `ScUsageHistoryMobile`.** A
28
+ `ScUsageHistoryMobile` exists in the source tree
29
+ (`src/SC-UsageHistory-Mobile/`) but is **not exported**.
30
+ 2. **No `onClick`, and no props spread.** `className` is the only extra prop.
31
+ 3. **`credits` is always red** and **`balance` always tertiary grey** — a credit
32
+ *refund* still renders as a debit.
33
+ 4. All five text props have demo defaults (`"Artifax Generation"`, `"50"`,
34
+ `"Rohan"`, `"Nov 9th, 2025"`, `"84,902"`).
35
+
36
+ ---
37
+
38
+ ## 1. How to use it
39
+
40
+ ### Import
41
+
42
+ ```tsx
43
+ import { UsageHistoryMobile } from "@streamoid/ui";
44
+ import "@streamoid/ui/dist/index.css"; // once, at your app root
45
+ ```
46
+
47
+ ### Minimal usage
48
+
49
+ ```tsx
50
+ {logs.map((log) => (
51
+ <UsageHistoryMobile
52
+ key={log.id}
53
+ serviceName={log.serviceName}
54
+ credits={log.amount.toLocaleString("en-US")}
55
+ userName={log.userName}
56
+ date={formatDate(log.createdAt)}
57
+ balance={log.balanceAfter.toLocaleString("en-US")}
58
+ />
59
+ ))}
60
+ ```
61
+
62
+ ### Props
63
+
64
+ | Prop | Type | Default | Notes |
65
+ |---|---|---|---|
66
+ | `serviceName` | `string` | `"Artifax Generation"` | ⚠️ Demo default. 14px primary, top-left, `flex: 1` + ellipsis. |
67
+ | `credits` | `string` | `"50"` | ⚠️ Demo default. 16px/600 top-right, hardcoded `--alias-text-and-icons-error` **red**. You supply any sign/prefix. |
68
+ | `userName` | `string` | `"Rohan"` | ⚠️ Demo default. Bottom-left, before a 12px vertical divider. |
69
+ | `date` | `string` | `"Nov 9th, 2025"` | ⚠️ Demo default. Bottom-left, after the divider. Pre-formatted string. |
70
+ | `balance` | `string` | `"84,902"` | ⚠️ Demo default. Bottom-right, tertiary grey — the balance **after** this entry. |
71
+ | `className` | `string` | – | Appended after internal classes (safely — uses `className ?? ""`). **The only extra prop it accepts.** |
72
+
73
+ That is the whole API. No `onClick`, no `...props`, no icon, no status.
74
+
75
+ ### Layout
76
+
77
+ | | left | right |
78
+ |---|---|---|
79
+ | **row 1** | `serviceName` (14px primary, truncates) | `credits` (16px/600 **red**) |
80
+ | **row 2** | `userName` │ `date` (14px tertiary, 9px gap, 0.5px vertical rule) | `balance` (14px tertiary) |
81
+
82
+ Row 2's left group is `flex: 1; min-width: 0`, but `userName` and `date` are each
83
+ `flex-shrink: 0`, so a very long user name pushes the date out rather than truncating
84
+ itself.
85
+
86
+ ### Recipes
87
+
88
+ ```tsx
89
+ // Make the row tappable — the component has no onClick
90
+ <div role="button" tabIndex={0} onClick={() => openLog(log)} className="cursor-pointer">
91
+ <UsageHistoryMobile
92
+ serviceName={log.service?.serviceGroupName ?? "—"}
93
+ credits={log.amount.toLocaleString("en-US")}
94
+ userName={userMap[log.triggeredByUserId] ?? "User"}
95
+ date={formatDate(log.createdAt)}
96
+ balance={log.balanceAfter.toLocaleString("en-US")}
97
+ />
98
+ </div>
99
+
100
+ // System-triggered entries have no user — show an em dash, not "undefined"
101
+ <UsageHistoryMobile
102
+ serviceName={log.service?.serviceGroupName ?? log.meta?.info ?? "—"}
103
+ credits={log.amount.toLocaleString("en-US")}
104
+ userName={log.triggeredByUserId ? (userMap[log.triggeredByUserId] ?? "User") : "—"}
105
+ date={formatDate(log.createdAt)}
106
+ balance={formatCredits(log.balanceAfter)}
107
+ />
108
+ ```
109
+
110
+ ---
111
+
112
+ ## 2. Where to use it
113
+
114
+ - **CXO mobile billing screen**, inside the "Usage history" tab of the
115
+ `ScTabSwitcher`, below `ScPlanDetailsCardMobile` and `ScCreditsUsageCardMobile`.
116
+ - Any narrow scrolling ledger of "what was spent, by whom, when, balance after".
117
+
118
+ The row draws its own bottom border, so stack them with no gap and no dividers of your
119
+ own. It composes nothing from the DS (the vertical rule is a 0.5px `border-left` div,
120
+ not `ScVDivider`).
121
+
122
+ ---
123
+
124
+ ## 3. When to use it
125
+
126
+ ### Use it when
127
+
128
+ - The viewport is mobile and each log entry needs four facts plus a service name.
129
+ - You already have every value as a display string.
130
+ - The list is read-only, or you own the tap target yourself.
131
+
132
+ ### Don't use it — reach for this instead
133
+
134
+ | Situation | Use instead |
135
+ |---|---|
136
+ | Desktop credit/usage log table | `ScBillingLogsTableHeader` + `ScBillingLogsTableList` |
137
+ | Mobile invoice rows (money in, green) | `InvoiceHistoryMobile` |
138
+ | Desktop invoice table | `ScBillingHistoryHeader` + `ScBillingHistoryTableList` |
139
+ | The credits meter / "buy credits" summary | `ScCreditsUsageCardMobile` |
140
+ | A generic member row on mobile | `ScTableListMobile` |
141
+ | A row that must be clickable with DS semantics | `ScTableListMobile` (`onRowClick`) |
142
+
143
+ ### Don't confuse with
144
+
145
+ | You may actually want | Not this |
146
+ |---|---|
147
+ | `ScUsageHistoryMobile` in `src/SC-UsageHistory-Mobile/` — a near-identical copy with `title`/`usageCount`/`totalCount` instead of `serviceName`/`credits`/`balance`, plus `onClick` | It is **not exported** from `index.ts`, its CSS is **not** in `dist/index.css`. Importing it will fail. |
148
+ | `InvoiceHistoryMobile` — same chrome, one text column, **green** amount | Usage is red credits out; invoices are green money in |
149
+ | `ScCreditsUsageCardMobile` — the aggregate card at the top of the screen | This is a single ledger line |
150
+ | `ScBillingLogsTableList` — the desktop row | Different metrics; needs a header component |
151
+
152
+ ---
153
+
154
+ ## 4. Why to use it
155
+
156
+ - **Four values in a 2×2 grid without a table.** The nested flex rows with
157
+ `min-width: 0` and per-cell `flex-shrink` are exactly what hand-rolled mobile
158
+ ledgers get wrong — the money never wraps and never gets pushed off-screen.
159
+ - **Colour semantics match the invoice row.** Red `--alias-text-and-icons-error` for
160
+ credits out, green for money in, tertiary grey for context — one consistent reading
161
+ across both tabs.
162
+ - **Correct tokens.** It uses the real `--alias-text-and-icons-*` names (unlike much of
163
+ the mobile family), so it genuinely flips light/dark.
164
+ - **Self-dividing**, so no trailing rule at the end of the list.
165
+
166
+ ---
167
+
168
+ ## Gotchas
169
+
170
+ **1. The name has no `Sc` prefix.** Autocomplete for `Sc…` will not find it.
171
+
172
+ ```tsx
173
+ // WRONG — not exported
174
+ import { ScUsageHistoryMobile } from "@streamoid/ui";
175
+
176
+ // RIGHT
177
+ import { UsageHistoryMobile } from "@streamoid/ui";
178
+ ```
179
+
180
+ **2. No props spread at all.** The signature is
181
+ `({ serviceName, credits, userName, date, balance, className })`.
182
+
183
+ ```tsx
184
+ // SILENTLY IGNORED — none of these reach the DOM
185
+ <UsageHistoryMobile serviceName="X" onClick={open} style={{ opacity: 0.5 }} data-id="1" />
186
+
187
+ // RIGHT — wrap it
188
+ <div onClick={open} data-id="1"><UsageHistoryMobile serviceName="X" /></div>
189
+ ```
190
+
191
+ **3. `credits` is unconditionally red.** There is no sign handling and no variant, so a
192
+ credit grant or refund still reads as a debit. If you need both directions, prefix the
193
+ string (`"+ 500"`) and accept the colour, or use a different row.
194
+
195
+ **4. `balance` is not computed.** The component does no arithmetic — it is whatever
196
+ string you pass. Passing the same value for `credits` and `balance` renders a
197
+ plausible-looking but wrong ledger, so derive the running balance from your data.
198
+
199
+ **5. Demo defaults on all five props.** Bare, it renders
200
+ `Artifax Generation / 50 / Rohan · Nov 9th, 2025 / 84,902` — indistinguishable from
201
+ real data in a screenshot.
202
+
203
+ **6. `userName` and `date` don't truncate, they shove.** Both are `flex-shrink: 0`.
204
+ A long user name pushes the date out of the visible row instead of ellipsising. Cap
205
+ long names yourself.
206
+
207
+ **7. The vertical rule between user and date is unconditional.** With
208
+ `userName=""` you get a leading rule. Pass `"—"` rather than an empty string.
209
+
210
+ ---
211
+
212
+ ## In the wild
213
+
214
+ ```tsx
215
+ // cxo-dashboard src/app/components/mobile-billing-content.tsx:254
216
+ <UsageHistoryMobile
217
+ key={log._id}
218
+ serviceName={log.service?.serviceGroupName ?? log.meta?.info ?? "—"}
219
+ credits={log.amount.toLocaleString("en-US")}
220
+ userName={log.triggeredByUserId ? (userMap[log.triggeredByUserId] ?? "User") : "—"}
221
+ date={formatDate(log.createdAt)}
222
+ balance={formatCredits(log.amount)}
223
+ />
224
+ ```
225
+
226
+ ---
227
+
228
+ ## Related
229
+
230
+ - `InvoiceHistoryMobile` — the sibling row for the "Invoice history" tab.
231
+ - `ScUsageHistoryMobile` (`src/SC-UsageHistory-Mobile/`) — the unexported earlier copy;
232
+ do not import it.
233
+ - `ScBillingLogsTableHeader` / `ScBillingLogsTableList` — the desktop table.
234
+ - `ScCreditsUsageCardMobile` — the aggregate meter above the tabs.
235
+ - `ScTabSwitcher` / `ScTabComp` — what toggles invoice vs usage.