@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.
- package/README.md +35 -18
- package/dist/docs/AGENTS.md +321 -0
- package/dist/docs/CreditWarningBanner.md +305 -0
- package/dist/docs/InvoiceHistoryMobile.md +222 -0
- package/dist/docs/ScAccess.md +259 -0
- package/dist/docs/ScAppCard.md +244 -0
- package/dist/docs/ScAppCardForCopilot.md +230 -0
- package/dist/docs/ScAppCardV3.md +273 -0
- package/dist/docs/ScAppField.md +308 -0
- package/dist/docs/ScAppListingCard.md +271 -0
- package/dist/docs/ScAppSwitchPanel.md +286 -0
- package/dist/docs/ScAppcardLogos.md +226 -0
- package/dist/docs/ScArtifaxInvite.md +262 -0
- package/dist/docs/ScArtifaxSidebar.md +330 -0
- package/dist/docs/ScAskAgentButton.md +307 -0
- package/dist/docs/ScBadges.md +261 -0
- package/dist/docs/ScBeacon.md +244 -0
- package/dist/docs/ScBillingHistoryHeader.md +210 -0
- package/dist/docs/ScBillingHistoryTableList.md +243 -0
- package/dist/docs/ScBillingLogsTableHeader.md +212 -0
- package/dist/docs/ScBillingLogsTableList.md +251 -0
- package/dist/docs/ScBriefCard.md +255 -0
- package/dist/docs/ScButton.md +251 -0
- package/dist/docs/ScCalendar.md +268 -0
- package/dist/docs/ScCalendarDateComps.md +264 -0
- package/dist/docs/ScCatalogixInvite.md +345 -0
- package/dist/docs/ScCatalogixSidebar.md +337 -0
- package/dist/docs/ScCatalogixStoreHeader.md +246 -0
- package/dist/docs/ScCatalogixStoreTableList.md +316 -0
- package/dist/docs/ScCheckField.md +233 -0
- package/dist/docs/ScCheckbox.md +272 -0
- package/dist/docs/ScCounter.md +235 -0
- package/dist/docs/ScCreditsUsageCard.md +247 -0
- package/dist/docs/ScCreditsUsageCardMobile.md +224 -0
- package/dist/docs/ScDefaultCard.md +269 -0
- package/dist/docs/ScDp.md +245 -0
- package/dist/docs/ScDrawer.md +318 -0
- package/dist/docs/ScFieldButton.md +255 -0
- package/dist/docs/ScFileField.md +268 -0
- package/dist/docs/ScGoogleSignIn.md +250 -0
- package/dist/docs/ScGuide.md +278 -0
- package/dist/docs/ScHDivider.md +213 -0
- package/dist/docs/ScHeader.md +222 -0
- package/dist/docs/ScImageField.md +253 -0
- package/dist/docs/ScInChatList.md +277 -0
- package/dist/docs/ScInChatMessage.md +205 -0
- package/dist/docs/ScInfoPopup.md +248 -0
- package/dist/docs/ScIntialProfileCover.md +233 -0
- package/dist/docs/ScInvoiceHistoryMobile.md +187 -0
- package/dist/docs/ScLogoUnit.md +232 -0
- package/dist/docs/ScMappingCard.md +241 -0
- package/dist/docs/ScMediaApproval.md +301 -0
- package/dist/docs/ScMediaSelect.md +310 -0
- package/dist/docs/ScMenuOptions.md +308 -0
- package/dist/docs/ScMobileBottomAction.md +252 -0
- package/dist/docs/ScMobileTopNav.md +279 -0
- package/dist/docs/ScModal.md +291 -0
- package/dist/docs/ScOnlyField.md +302 -0
- package/dist/docs/ScOnlyIcon.md +213 -0
- package/dist/docs/ScPagination.md +284 -0
- package/dist/docs/ScPairtext.md +287 -0
- package/dist/docs/ScPendingAction.md +238 -0
- package/dist/docs/ScPhtogenixInvite.md +275 -0
- package/dist/docs/ScPlanCard.md +302 -0
- package/dist/docs/ScPlanComparison.md +264 -0
- package/dist/docs/ScPlanDetailsCard.md +246 -0
- package/dist/docs/ScPlanDetailsCardMobile.md +240 -0
- package/dist/docs/ScPopUpMenu.md +224 -0
- package/dist/docs/ScProfile.md +234 -0
- package/dist/docs/ScProfileImageUpdate.md +261 -0
- package/dist/docs/ScProfileOptions.md +245 -0
- package/dist/docs/ScProfilePopup.md +396 -0
- package/dist/docs/ScProfileSettingsComp.md +250 -0
- package/dist/docs/ScProfileV2Mobile.md +216 -0
- package/dist/docs/ScProgressBar.md +267 -0
- package/dist/docs/ScQuickPrompt.md +277 -0
- package/dist/docs/ScRadio.md +228 -0
- package/dist/docs/ScReferralCardMobile.md +226 -0
- package/dist/docs/ScReferralTableHeader.md +260 -0
- package/dist/docs/ScReferralTableList.md +293 -0
- package/dist/docs/ScRole.md +226 -0
- package/dist/docs/ScRoleMobile.md +199 -0
- package/dist/docs/ScSelect.md +270 -0
- package/dist/docs/ScSelection.md +256 -0
- package/dist/docs/ScSelectionList.md +272 -0
- package/dist/docs/ScSelectionPill.md +240 -0
- package/dist/docs/ScSelectionPillGroup.md +302 -0
- package/dist/docs/ScSettingsNav.md +212 -0
- package/dist/docs/ScSettingsTabComp.md +260 -0
- package/dist/docs/ScSideBarLogoUnit.md +340 -0
- package/dist/docs/ScSidebar.md +243 -0
- package/dist/docs/ScSidebarIcons.md +232 -0
- package/dist/docs/ScSidebarMenu.md +283 -0
- package/dist/docs/ScSidebarProfile.md +231 -0
- package/dist/docs/ScSidebarSwitchMenu.md +258 -0
- package/dist/docs/ScSlider.md +194 -0
- package/dist/docs/ScStoreCard.md +252 -0
- package/dist/docs/ScStrLogo.md +253 -0
- package/dist/docs/ScStreamoidWordmark.md +302 -0
- package/dist/docs/ScSubAgent.md +226 -0
- package/dist/docs/ScTabComp.md +308 -0
- package/dist/docs/ScTabField.md +258 -0
- package/dist/docs/ScTabSwitcher.md +307 -0
- package/dist/docs/ScTableHeader.md +261 -0
- package/dist/docs/ScTableList.md +301 -0
- package/dist/docs/ScTableListMobile.md +282 -0
- package/dist/docs/ScTabs.md +268 -0
- package/dist/docs/ScTaxonomyPill.md +263 -0
- package/dist/docs/ScTextArea.md +259 -0
- package/dist/docs/ScTextField.md +324 -0
- package/dist/docs/ScThinkingStepIcon.md +249 -0
- package/dist/docs/ScTodoList.md +288 -0
- package/dist/docs/ScToggleSwitch.md +229 -0
- package/dist/docs/ScUsageHistoryMobile.md +194 -0
- package/dist/docs/ScVDivider.md +215 -0
- package/dist/docs/ScValueMappingL1.md +256 -0
- package/dist/docs/ScVersion.md +251 -0
- package/dist/docs/ScWorkspace.md +233 -0
- package/dist/docs/ScWorkspaceCard.md +234 -0
- package/dist/docs/ScWorkspaceSettingsMobile.md +265 -0
- package/dist/docs/ScWorkspaceSwitchCard.md +314 -0
- package/dist/docs/ScWorkspaceSwitchMobile.md +241 -0
- package/dist/docs/ScWorkspaceSwitchMobileV2.md +278 -0
- package/dist/docs/StreamoidSidebar.md +403 -0
- package/dist/docs/StreamoidWorkspaceSwitcher.md +307 -0
- package/dist/docs/UsageHistoryMobile.md +235 -0
- package/dist/docs/components.json +4849 -0
- package/dist/index.css +36 -36
- package/dist/index.d.mts +10 -0
- package/dist/index.d.ts +10 -0
- package/package.json +3 -2
|
@@ -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.
|