@streamoid/ui 0.6.17 → 0.6.19

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 (134) hide show
  1. package/README.md +35 -18
  2. package/dist/docs/AGENTS.md +325 -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 +210 -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/ScWorkspaceAccountMenu.md +115 -0
  120. package/dist/docs/ScWorkspaceCard.md +234 -0
  121. package/dist/docs/ScWorkspaceSettingsMobile.md +265 -0
  122. package/dist/docs/ScWorkspaceSwitchCard.md +312 -0
  123. package/dist/docs/ScWorkspaceSwitchMobile.md +241 -0
  124. package/dist/docs/ScWorkspaceSwitchMobileV2.md +278 -0
  125. package/dist/docs/StreamoidSidebar.md +413 -0
  126. package/dist/docs/StreamoidWorkspaceSwitcher.md +307 -0
  127. package/dist/docs/UsageHistoryMobile.md +235 -0
  128. package/dist/docs/components.json +4931 -0
  129. package/dist/index.css +361 -36
  130. package/dist/index.d.mts +213 -88
  131. package/dist/index.d.ts +213 -88
  132. package/dist/index.js +2486 -1629
  133. package/dist/index.mjs +2487 -1620
  134. package/package.json +5 -3
@@ -0,0 +1,308 @@
1
+ ---
2
+ component: ScMenuOptions
3
+ package: "@streamoid/ui"
4
+ category: overlays
5
+ status: stable
6
+ renders: div
7
+ tags: [menu, menu-item, menu-row, option, kebab, context-menu, sidebar-row, dropdown-item, whats-new]
8
+ related: [ScProfilePopup, ScProfileOptions, ScSidebarMenu, ScButton, ScHDivider]
9
+ do_not_confuse_with: [ScPopUpMenu, ScSidebarMenu, ScProfileOptions, ScTabComp]
10
+ used_by: [cxo, photogenix, artifax]
11
+ ---
12
+
13
+ # ScMenuOptions
14
+
15
+ **One row of a menu.** Icon + label (+ an optional right-aligned version string),
16
+ with hover fills and a red `error` variant. It renders no overlay, no list and no
17
+ positioning — you stack these rows inside `ScProfilePopup`, a sidebar block, or your
18
+ own anchored container.
19
+
20
+ ## TL;DR for agents
21
+
22
+ - **Reach for it when:** you need a row in a profile flyout, an account/settings
23
+ menu, or a kebab/overflow menu.
24
+ - **Don't reach for it when:** it's a primary-navigation row in the sidebar rail
25
+ (→ `ScSidebarMenu`), it's a button (→ `ScButton`), or you wanted the *container*
26
+ (→ `ScProfilePopup` / `ScModal` / `ScDrawer`).
27
+ - **Five things that will bite you:**
28
+ 1. **Three loud defaults**: `icon` is `SiconTeam`, `text` is `"Menu Option"`,
29
+ and `version` is the stale literal **`"v2.0.21"`** ⚠️.
30
+ 2. `hover` is the **string** `"true" | "false"`, not a boolean.
31
+ 3. Fixed `width: 12.5rem` (200px) and **no `cursor`** — every real call site
32
+ passes `style={{ width: "100%", cursor: "pointer" }}`.
33
+ 4. It's a plain `<div>`: no role, no tabIndex, no keyboard activation. All three
34
+ hosts add those at the call site.
35
+ 5. `version` only renders under `variant="with-v"`, and `variant="error"` only
36
+ colours the **label**, not your icon.
37
+
38
+ ---
39
+
40
+ ## 1. How to use it
41
+
42
+ ### Import
43
+
44
+ ```tsx
45
+ import { ScMenuOptions } from "@streamoid/ui";
46
+ import { SiconSettings } from "@streamoid/icons";
47
+ import "@streamoid/ui/dist/index.css"; // once, at your app root
48
+ ```
49
+
50
+ ### Minimal usage — the shape all three hosts ship
51
+
52
+ ```tsx
53
+ <ScMenuOptions
54
+ icon={<SiconSettings size={24} />}
55
+ text="Settings"
56
+ variant="default"
57
+ style={{ width: "100%", cursor: "pointer" }}
58
+ role="button"
59
+ tabIndex={0}
60
+ onClick={openSettings}
61
+ onKeyDown={(e) => {
62
+ if (e.key === "Enter" || e.key === " ") { e.preventDefault(); openSettings(); }
63
+ }}
64
+ />
65
+ ```
66
+
67
+ ### Props
68
+
69
+ | Prop | Type | Default | Notes |
70
+ |---|---|---|---|
71
+ | `text` | `string` | `"Menu Option"` | ⚠️ Real default. The label. `flex: 1`, `nowrap`, `text-overflow: ellipsis` — and **no `title`**, so long labels truncate with no tooltip. |
72
+ | `icon` | `JSX.Element` | `<SiconTeam />` | ⚠️ Real default — forget it and you ship a "team" glyph. Rendered raw (no clone, no size/colour normalisation). |
73
+ | `variant` | `"default"` \| `"with-v"` \| `"error"` | `"default"` | `with-v` adds the right-aligned `version`; `error` turns the **label** `--alias-text-and-icons-error` and the hover fill `--alias-fill-error-secondary`. |
74
+ | `version` | `string` | `"v2.0.21"` | ⚠️ A stale hardcoded version string. **Only rendered when `variant="with-v"`.** |
75
+ | `hover` | `"true"` \| `"false"` | `"false"` | ⚠️ A **string** union, not a boolean. `"true"` force-renders the hover fill (Figma parity / screenshots). Real `:hover` already works. |
76
+ | `className` | `string` | – | Concatenated unconditionally — see Gotcha 6. |
77
+ | `...props` | `HTMLAttributes<HTMLDivElement>` | – | Spread onto the root div: `onClick`, `style`, `role`, `tabIndex`, `onKeyDown`, `aria-*`, `data-*`. |
78
+
79
+ ### What renders per variant
80
+
81
+ | Region | `default` | `with-v` | `error` |
82
+ |---|---|---|---|
83
+ | Icon | `icon` (raw, unstyled) | same | same — **not** recoloured |
84
+ | Label | `--alias-text-and-icons-primary` | same | `--alias-text-and-icons-error` |
85
+ | Right slot | — | `version` in `--alias-text-and-icons-muted`, 12px, right-aligned | — |
86
+ | Hover / `hover="true"` fill | `--alias-fill-neutral-neutralhover` | same | `--alias-fill-error-secondary` |
87
+
88
+ There is no combination that gives you both a version string and error colouring.
89
+
90
+ ### Recipes
91
+
92
+ ```tsx
93
+ // A menu block: rows + divider + a destructive row
94
+ <div style={{ display: "flex", flexDirection: "column", gap: "var(--spacing-md)" }}>
95
+ <ScMenuOptions icon={<SiconTeam size={24} />} text="Teams" style={ROW} onClick={goTeams} />
96
+ <ScMenuOptions icon={<SiconBilling size={24} />} text="Billing" style={ROW} onClick={goBilling} />
97
+ <ScMenuOptions icon={<SiconSettings size={24} />} text="All settings" style={ROW} onClick={goSettings} />
98
+ <ScHDivider />
99
+ <ScMenuOptions icon={<SiconLogout size={24} />} text="Log out" variant="error" style={ROW} onClick={logout} />
100
+ </div>
101
+ // const ROW = { width: "100%", cursor: "pointer" };
102
+
103
+ // The "What's new" row — ALWAYS pass version, or you ship "v2.0.21"
104
+ <ScMenuOptions
105
+ icon={<SiconBolt size={24} />}
106
+ text="What's new"
107
+ variant="with-v"
108
+ version={`v${APP_VERSION}`}
109
+ style={ROW}
110
+ onClick={openChangelog}
111
+ />
112
+
113
+ // Inside ScProfilePopup's extra-rows slot
114
+ <ScProfilePopup
115
+ menuContent={<ScMenuOptions icon={<SiconCatalog size={24} />} text="Catalogs" style={ROW} onClick={goCatalogs} />}
116
+ />
117
+
118
+ // Recolour the icon to match variant="error" yourself — the component won't
119
+ <ScMenuOptions
120
+ variant="error"
121
+ icon={<SiconLogout size={24} color="var(--alias-text-and-icons-error)" />}
122
+ text="Log out"
123
+ />
124
+ ```
125
+
126
+ ---
127
+
128
+ ## 2. Where to use it
129
+
130
+ - **Profile / account flyouts** — the Teams / Billing / Settings / What's new stack.
131
+ CXO (`profile-popup.tsx`), Artifax (`DashboardSidebar.tsx`) and Photogenix
132
+ (`layout/Sidebar.tsx`) all render exactly that quartet.
133
+ - **Kebab / row-overflow menus** — Photogenix's `studio/BatchList.tsx`.
134
+ - **Inside DS composites** — `ScProfilePopup` builds every one of its menu rows from
135
+ this component (adding `role="button"`, `tabIndex={0}` and an Enter/Space
136
+ `onKeyDown`), and `ScProfileOptions` does the same.
137
+ - **Sidebar secondary blocks** — the non-navigation rows below the divider.
138
+
139
+ One of the DS's most widely reached-for parts (the catalog's "universal core"). It
140
+ is *not* used in Catalogix, which has app-local menus.
141
+
142
+ ---
143
+
144
+ ## 3. When to use it
145
+
146
+ ### Use it when
147
+
148
+ - The row is **one action in a list of actions**, presented in a menu or flyout.
149
+ - You want the hover fill, radius, gap, label truncation and semantic error
150
+ colouring to match every other menu in the product.
151
+ - You are filling `ScProfilePopup`'s `menuContent` / `preFooterContent` slots.
152
+
153
+ ### Don't use it — reach for this instead
154
+
155
+ | Situation | Use instead |
156
+ |---|---|
157
+ | A primary-navigation row in the sidebar rail (active state, collapsed mode) | `ScSidebarMenu` (or `StreamoidSidebar`'s `config`) |
158
+ | The profile flyout **panel** itself | `ScProfilePopup` (panel body; you position + dismiss) |
159
+ | A whole ready-made profile-options block | `ScProfileOptions` |
160
+ | The overlay/container for your rows | `ScModal` (centered) / `ScDrawer` (edge) |
161
+ | A call-to-action | `ScButton` |
162
+ | A tab / view switch | `ScTabComp` / `ScTabSwitcher` |
163
+ | A key:value display row | `ScPairtext` |
164
+ | A checkbox or radio option row | `ScCheckField` / `ScRadio` |
165
+ | A row in a data table | `ScTableList` / `ScTableHeader` |
166
+ | A contextual help bubble | `ScInfoPopup` |
167
+
168
+ ### Don't confuse with
169
+
170
+ | You may actually want | Not this |
171
+ |---|---|
172
+ | `ScPopUpMenu` — legacy 242px twin whose label prop is `menu` and which has **no hover styling**; zero call sites | `ScMenuOptions` is the current row |
173
+ | `ScSidebarMenu` — nav row with active/collapsed states, built for the rail | `ScMenuOptions` has no active state and no collapsed mode |
174
+ | `ScProfileOptions` — a composed *block* of these rows | `ScMenuOptions` is one row |
175
+ | `ScProfilePopup` — the panel body | `ScMenuOptions` never positions itself |
176
+ | `ScTabComp` — visually similar icon+label div row, but a *tab* with an `active` prop | Different vocabulary; don't build tabs from menu rows |
177
+
178
+ ---
179
+
180
+ ## 4. Why to use it
181
+
182
+ - **The hover fills are re-specced tokens, not guesses.** The default hover moved
183
+ from `neutralToHover` to `--alias-fill-neutral-neutralhover` (Figma node
184
+ 2004:3893) and the error hover is `--alias-fill-error-secondary`. Every app gets
185
+ that in one place; hand-rolled rows are where light-mode hover contrast breaks.
186
+ - **Error semantics come from tokens.** `variant="error"` reads
187
+ `--alias-text-and-icons-error`, so "Log out" / "Delete" look identical in CXO,
188
+ Photogenix and Artifax.
189
+ - **Truncation is already handled.** `flex: 1` + `overflow: hidden` +
190
+ `text-overflow: ellipsis` + `white-space: nowrap` means a long workspace or
191
+ catalog name shortens instead of stretching the menu.
192
+ - **The version row is a first-class variant.** "What's new · v1.2.3" is a real
193
+ product pattern; `with-v` gives you the muted 12px right-aligned slot rather than
194
+ a hand-aligned span.
195
+ - **It composes into `ScProfilePopup` for free** — same radius, gap and padding, so
196
+ rows you add via the slots are indistinguishable from the built-in ones.
197
+
198
+ ---
199
+
200
+ ## Gotchas
201
+
202
+ **1. Three defaults that ship real content.** `text="Menu Option"`,
203
+ `icon=<SiconTeam />`, `version="v2.0.21"`. The last one is the nastiest: a stale
204
+ version number that looks plausible.
205
+
206
+ ```tsx
207
+ // WRONG — renders "What's new v2.0.21" forever
208
+ <ScMenuOptions text="What's new" variant="with-v" icon={<SiconBolt size={24} />} />
209
+
210
+ // RIGHT
211
+ <ScMenuOptions text="What's new" variant="with-v" version={`v${APP_VERSION}`} icon={<SiconBolt size={24} />} />
212
+ ```
213
+
214
+ **2. `hover` is a string, not a boolean.**
215
+
216
+ ```tsx
217
+ // WRONG — type error
218
+ <ScMenuOptions text="Teams" hover={true} />
219
+
220
+ // RIGHT (and only for design parity / screenshots — real :hover already works)
221
+ <ScMenuOptions text="Teams" hover="true" />
222
+ ```
223
+
224
+ **3. Fixed `width: 12.5rem` (200px) and no `cursor`.** Dropped into a flexible
225
+ container it stays 200px, and the pointer never becomes a hand. The universal fix,
226
+ present at every host call site:
227
+
228
+ ```tsx
229
+ <ScMenuOptions … style={{ width: "100%", cursor: "pointer" }} />
230
+ ```
231
+
232
+ **4. It's a `<div>` — no keyboard, no role.** Add them via the spread. Artifax
233
+ factors this into a `buildMenuItemA11yProps(...)` helper; `ScProfilePopup` uses an
234
+ `activateOnKey(handler)` helper. Do the same rather than shipping mouse-only menus.
235
+
236
+ ```tsx
237
+ <ScMenuOptions
238
+ text="Billing" style={ROW}
239
+ role="button" tabIndex={0}
240
+ onClick={goBilling}
241
+ onKeyDown={(e) => { if (e.key === "Enter" || e.key === " ") { e.preventDefault(); goBilling(); } }}
242
+ />
243
+ ```
244
+
245
+ **5. `variant="error"` does not recolour your icon.** The icon is rendered raw —
246
+ no `cloneElement`, no `currentColor` (unlike `ScButton`). A red "Log out" label
247
+ next to a white logout glyph is the default outcome; pass
248
+ `color="var(--alias-text-and-icons-error)"` on the icon yourself.
249
+
250
+ **6. `className` is concatenated unconditionally.** Omit it and the class attribute
251
+ contains the literal string `undefined`. Cosmetic, but it breaks exact-match
252
+ snapshot assertions.
253
+
254
+ **7. `version` is silently dropped outside `with-v`,** and `with-v` + `error` is not
255
+ a thing — the variants are mutually exclusive.
256
+
257
+ **8. Long labels truncate with no tooltip.** There is no `title` attribute. If the
258
+ label can be user data (a workspace or catalog name), wrap it or add `title` via the
259
+ spread — the spread lands on the root, so `title` works there.
260
+
261
+ **9. It is a row, not a menu.** No overlay, no `open`, no outside-click, no
262
+ positioning, no `role="menu"` grouping. Wrap the stack yourself (`ScProfilePopup`,
263
+ or your own absolutely-positioned container with `role="menu"`).
264
+
265
+ **10. `icon` size is yours to manage.** The component does not size it. Hosts pass
266
+ either `size={24}` or Tailwind `w-6 h-6` / `h-5 w-5`; mixing sizes in one menu makes
267
+ the labels visibly misalign.
268
+
269
+ ---
270
+
271
+ ## In the wild
272
+
273
+ ```tsx
274
+ // cxo-dashboard src/app/components/profile-popup.tsx:298
275
+ <ScMenuOptions
276
+ icon={<SiconTeam className="w-6 h-6" color={PRIMARY_ICON} />}
277
+ text="Teams"
278
+ variant="default"
279
+ style={{ width: "100%", cursor: "pointer" }}
280
+ onClick={() => {
281
+ onClose();
282
+ navigate(to("/settings/team"));
283
+ }}
284
+ />
285
+ ```
286
+
287
+ ```tsx
288
+ // artifax packages/shared/src/components/DashboardSidebar.tsx:952
289
+ <ScMenuOptions
290
+ icon={<SiconBolt className="h-5 w-5" />}
291
+ text="What's new"
292
+ variant="with-v"
293
+ version={`v${__APP_VERSION__}`}
294
+ style={{ width: '100%', cursor: 'pointer' }}
295
+ {...buildMenuItemA11yProps("What's new", onClose)}
296
+ />
297
+ ```
298
+
299
+ ---
300
+
301
+ ## Related
302
+
303
+ - `ScProfilePopup` — the profile flyout panel; builds its rows from this component and exposes `menuContent` for more.
304
+ - `ScProfileOptions` — an older composed profile-options block, also made of these rows.
305
+ - `ScSidebarMenu` — the primary-nav sibling, with active + collapsed states.
306
+ - `ScPopUpMenu` — **legacy**, no hover styling, no call sites; don't pick it by mistake.
307
+ - `ScHDivider` — the 0.5px rule you put between menu groups.
308
+ - `@streamoid/icons` — check `packages/icons/ICONS.md` before drawing a new glyph.
@@ -0,0 +1,252 @@
1
+ ---
2
+ component: ScMobileBottomAction
3
+ package: "@streamoid/ui"
4
+ category: mobile
5
+ status: stable
6
+ renders: div
7
+ tags: [mobile, bottom, footer, action-bar, cta, save, cancel, sticky, buttons]
8
+ related: [ScMobileTopNav, ScButton, ScModal, ScDrawer]
9
+ do_not_confuse_with: [ScButton, ScMobileTopNav, ScFieldButton, ScOnlyIcon]
10
+ ---
11
+
12
+ # ScMobileBottomAction
13
+
14
+ **The mobile screen's bottom action bar.** A bordered strip over
15
+ `--alias-surface-canvas` holding one full-width primary button, or two 50/50 buttons
16
+ (outline + primary) when `secondaryText` is set. The buttons are **real `<button>`
17
+ elements** written from scratch — this component does **not** use `ScButton`.
18
+
19
+ ## TL;DR for agents
20
+
21
+ - **Reach for it when:** a mobile screen or sheet ends in a committed action —
22
+ Save / Cancel, Continue, Delete.
23
+ - **Don't reach for it when:** you need one button anywhere else (→ `ScButton`), an
24
+ icon+label CTA (→ `ScButton` with `styleVariant`), or the top bar
25
+ (→ `ScMobileTopNav`).
26
+ - **Five things that will bite you:**
27
+ 1. **These are native `<button>`s with no `type` attribute**, so inside a `<form>`
28
+ they default to `type="submit"` and will submit it.
29
+ 2. **It is not `ScButton`.** No `variant`, no `loading`, no `icon`, no `size` — and
30
+ the primary's fill/text tokens differ from `ScButton`'s.
31
+ 3. **The two labels have different font sizes** — primary 14px, secondary 16px.
32
+ That is what the CSS says; it is not a typo you can fix from outside.
33
+ 4. **`text` defaults to `"Save Changes"`.**
34
+ 5. **It is not sticky.** You position it.
35
+
36
+ ---
37
+
38
+ ## 1. How to use it
39
+
40
+ ### Import
41
+
42
+ ```tsx
43
+ import { ScMobileBottomAction } from "@streamoid/ui";
44
+ import "@streamoid/ui/dist/index.css"; // once, at your app root
45
+ ```
46
+
47
+ ### Minimal usage
48
+
49
+ ```tsx
50
+ <ScMobileBottomAction text="Save changes" onClick={save} disabled={!isDirty} />
51
+ ```
52
+
53
+ ### Props
54
+
55
+ | Prop | Type | Default | Notes |
56
+ |---|---|---|---|
57
+ | `text` | `string` | `"Save Changes"` | ⚠️ Primary button label. Hardcoded English default. |
58
+ | `disabled` | `boolean` | `false` | Sets the native `disabled` attribute **and** `opacity: 0.5` + `cursor: not-allowed`. Genuinely blocks clicks (unlike `ScButton`'s `loading`). |
59
+ | `onClick` | `(e: React.MouseEvent) => void` | – | On the primary button. Does **not** land on the root. |
60
+ | `secondaryText` | `string` | – | **Presence of this prop switches to the 2-button layout.** Renders the outline button, to the **left** of the primary. |
61
+ | `secondaryDisabled` | `boolean` | `false` | Same treatment as `disabled`, for the outline button. |
62
+ | `onSecondaryClick` | `(e: React.MouseEvent) => void` | – | On the outline button. |
63
+ | `className` | `string` | – | Appended after internal classes. Unguarded concat — see Gotcha 6. |
64
+ | `...props` | `React.HTMLAttributes<HTMLDivElement>` | – | Spread onto the root strip, **not** the buttons. |
65
+
66
+ ### What renders
67
+
68
+ | `secondaryText` | Layout | Root gap |
69
+ |---|---|---|
70
+ | unset | one primary button, `flex: 1 0 0` (full width) | `0` |
71
+ | set | outline button (left) + primary button (right), both `flex: 1 0 0` → 50/50 | `16px` |
72
+
73
+ Both buttons are 40px tall, `--medium-5` (12px) radius, `8px 16px` padding. Primary =
74
+ `--alias-fill-base-base` fill with `--alias-text-and-icons-inverse` text. Outline =
75
+ transparent with a 1.25px `--alias-border-default` border and
76
+ `--alias-text-and-icons-primary` text.
77
+
78
+ ### Recipes
79
+
80
+ ```tsx
81
+ // One action
82
+ <ScMobileBottomAction text="Continue" onClick={next} disabled={!canProceed} />
83
+
84
+ // Two actions — secondary is the outline one, and sits on the LEFT
85
+ <ScMobileBottomAction
86
+ text="Save changes"
87
+ onClick={save}
88
+ disabled={!isDirty || saving}
89
+ secondaryText="Cancel"
90
+ onSecondaryClick={close}
91
+ secondaryDisabled={saving}
92
+ />
93
+
94
+ // Make it a sticky footer of a full-height mobile screen — the component does not do this
95
+ <div className="flex flex-col h-full">
96
+ <ScMobileTopNav type="inApp" heading="Settings" menuIcon={<SiconMenu size={24} />} />
97
+ <div className="flex-1 overflow-y-auto">{body}</div>
98
+ <div className="shrink-0">
99
+ <ScMobileBottomAction text="Save changes" onClick={save} />
100
+ </div>
101
+ </div>
102
+
103
+ // Inside a <form>: give the buttons no chance to submit — put the bar outside the form,
104
+ // or don't use a form at all (CXO wires plain onClick handlers).
105
+ ```
106
+
107
+ ---
108
+
109
+ ## 2. Where to use it
110
+
111
+ The last child of a mobile screen or bottom sheet, below the scrolling body. The intended
112
+ mobile shell is:
113
+
114
+ ```
115
+ ScMobileTopNav ← 56px, shrink-0
116
+ <scrolling body> ← flex-1, overflow-y auto
117
+ ScMobileBottomAction ← shrink-0
118
+ ```
119
+
120
+ It also fits the footer of a mobile-width `ScDrawer` / `ScModal`, where the desktop would
121
+ use a pair of `ScButton`s.
122
+
123
+ It composes nothing from the DS.
124
+
125
+ ---
126
+
127
+ ## 3. When to use it
128
+
129
+ ### Use it when
130
+
131
+ - The action is the **commitment point** of a whole mobile screen, not an inline control.
132
+ - There are exactly one or two actions, and they should each take half the width.
133
+ - You want the footer's border, canvas fill and 16/16/20px padding to match the rest of
134
+ the mobile surface.
135
+
136
+ ### Don't use it — reach for this instead
137
+
138
+ | Situation | Use instead |
139
+ |---|---|
140
+ | Any single button, anywhere in a page body | `ScButton` |
141
+ | A CTA that needs an icon, a spinner, or a colour role (`error`, `outline`, `mono`) | `ScButton` — this bar has none of those |
142
+ | Three or more actions | `ScButton`s in your own row, or a `ScPopUpMenu` |
143
+ | A button that sits inline with a form field | `ScFieldButton` |
144
+ | A bare icon affordance | `ScOnlyIcon` |
145
+ | The mobile top bar | `ScMobileTopNav` |
146
+ | A destructive confirm | `ScModal` + `ScButton variant="error"` — this bar cannot render red |
147
+
148
+ ### Don't confuse with
149
+
150
+ | You may actually want | Not this |
151
+ |---|---|
152
+ | `ScButton` — the real button primitive: `variant`, `type`, `size`, `loading`, `icon`, `styleVariant`, `state` | `ScMobileBottomAction` re-implements two buttons in CSS and exposes only text + disabled |
153
+ | `ScButton state="disabled"` — a `div` with `pointer-events: none` | This bar's `disabled` is the **native** attribute on a real `<button>`, so it is properly announced and unclickable |
154
+ | `ScMobileTopNav` — the top bar, icon slots | Same family, opposite end |
155
+
156
+ A note on consistency: because this bar does **not** compose `ScButton`, its primary
157
+ button's fill (`--alias-fill-base-base`) and radius (`--medium-5`) are its own. If you put
158
+ an `ScButton` next to it, expect a small mismatch — CXO's mobile billing screen sidesteps
159
+ this by hand-rolling the footer strip and putting a real `ScButton` inside it.
160
+
161
+ ---
162
+
163
+ ## 4. Why to use it
164
+
165
+ - **Real `<button>` semantics.** Unlike `ScButton` (which is a `div role="button"`), both
166
+ buttons here are native, so `disabled` is genuinely announced and genuinely
167
+ unclickable, and Enter/Space work without extra wiring. That is the one place this
168
+ component is *better* than the primitive.
169
+ - **The 1-vs-2 layout switch is one prop.** Adding `secondaryText` flips the root to
170
+ `gap: 16px` and inserts the outline button in the correct (left) position — no
171
+ conditional JSX at the call site.
172
+ - **Correct footer chrome**: `--alias-surface-canvas` fill, 0.5px
173
+ `--alias-border-divider` top rule, and asymmetric `16px 16px 20px` padding so the bar
174
+ clears a phone's home indicator.
175
+ - **Both buttons are `flex: 1 0 0; min-width: 0`,** so they always split the bar evenly.
176
+ (Note `flex-shrink: 0` plus `white-space: nowrap` labels: a very long label grows the
177
+ button past its share rather than ellipsising, so keep labels short.)
178
+
179
+ ---
180
+
181
+ ## Gotchas
182
+
183
+ **1. The buttons have no `type` attribute, so inside a form they submit it.** A
184
+ `<button>` without `type` defaults to `type="submit"`.
185
+
186
+ ```tsx
187
+ // WRONG — tapping "Cancel" submits the form
188
+ <form onSubmit={save}>
189
+ {fields}
190
+ <ScMobileBottomAction text="Save" onClick={save} secondaryText="Cancel" onSecondaryClick={close} />
191
+ </form>
192
+
193
+ // RIGHT — keep the bar outside the form, and drive it with onClick
194
+ <form onSubmit={(e) => e.preventDefault()}>{fields}</form>
195
+ <ScMobileBottomAction text="Save" onClick={save} secondaryText="Cancel" onSecondaryClick={close} />
196
+ ```
197
+
198
+ **2. It is not `ScButton`.** There is no `variant`, `type`, `size`, `loading`,
199
+ `styleVariant`, `icon` or `state`. You cannot make either button red, and you cannot show
200
+ a spinner — track your own saving state and use `disabled`.
201
+
202
+ **3. The two labels are different sizes.** `.buttonText` is
203
+ `--font-size-font-size-sm` (14px) and `.outlineButtonText` is
204
+ `--font-size-font-size-md` (16px). In the 2-button layout the *secondary* label is the
205
+ bigger one. Nothing at the call site can change that.
206
+
207
+ **4. `secondaryText` is the layout switch, not a style flag.** An empty string is falsy,
208
+ so `secondaryText=""` silently gives you the 1-button layout.
209
+
210
+ **5. `text` defaults to `"Save Changes"`.** Forget the prop and you ship that label on a
211
+ "Delete account" screen.
212
+
213
+ **6. `className` is concatenated unguarded.** Omit it and the root carries a literal
214
+ `undefined` class. Also note `className` lands on the **root strip**, so you cannot
215
+ restyle the buttons through it without descendant selectors.
216
+
217
+ **7. Not sticky, not fixed, no `z-index`.** It is a static `width: 100%` strip. Wrap it
218
+ yourself (`position: sticky; bottom: 0`, or a `shrink-0` child of a
219
+ `flex-col h-full` screen).
220
+
221
+ **8. `onClick` goes to the primary button, not the root.** Unlike most components in this
222
+ family, `...props` on the root and `onClick` are separate — so `onClick` can never make
223
+ the whole bar clickable.
224
+
225
+ **9. `--medium-5` is not a real token.** The radius resolves via its `12px` fallback
226
+ everywhere; `--radius-xl` is the token the rest of the library uses for the same value.
227
+
228
+ ---
229
+
230
+ ## In the wild
231
+
232
+ _No host render site found — used by the agent runtime / composed internally._
233
+
234
+ To be precise: it is exported from `@streamoid/ui` but no host app renders it, and it is
235
+ not agent-runtime — it is currently unused. CXO's mobile screens hand-roll the same strip
236
+ and put a real `ScButton` inside it — see
237
+ `cxo-dashboard/src/app/components/mobile-billing-content.tsx:275`, which builds a
238
+ `shrink-0` div with `backgroundColor: T.canvas`, a `0.5px` top border and
239
+ `16px 16px 20px` padding (exactly this component's CSS) around an
240
+ `ScButton variant="tertiary" styleVariant="icon-left"`. That call site is both where this
241
+ component belongs and the reason it was skipped: the screen needed an icon in the button,
242
+ which this bar cannot render.
243
+
244
+ ---
245
+
246
+ ## Related
247
+
248
+ - `ScMobileTopNav` — the other half of the mobile shell.
249
+ - `ScButton` — the real button primitive; reach for it whenever you need an icon, a
250
+ spinner, or a colour role.
251
+ - `ScModal` / `ScDrawer` — the overlays whose mobile footers this bar suits.
252
+ - `ScFieldButton` / `ScOnlyIcon` — the other action shapes in the library.