@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.
- package/README.md +35 -18
- package/dist/docs/AGENTS.md +325 -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 +210 -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/ScWorkspaceAccountMenu.md +115 -0
- package/dist/docs/ScWorkspaceCard.md +234 -0
- package/dist/docs/ScWorkspaceSettingsMobile.md +265 -0
- package/dist/docs/ScWorkspaceSwitchCard.md +312 -0
- package/dist/docs/ScWorkspaceSwitchMobile.md +241 -0
- package/dist/docs/ScWorkspaceSwitchMobileV2.md +278 -0
- package/dist/docs/StreamoidSidebar.md +413 -0
- package/dist/docs/StreamoidWorkspaceSwitcher.md +307 -0
- package/dist/docs/UsageHistoryMobile.md +235 -0
- package/dist/docs/components.json +4931 -0
- package/dist/index.css +361 -36
- package/dist/index.d.mts +213 -88
- package/dist/index.d.ts +213 -88
- package/dist/index.js +2486 -1629
- package/dist/index.mjs +2487 -1620
- package/package.json +5 -3
|
@@ -0,0 +1,279 @@
|
|
|
1
|
+
---
|
|
2
|
+
component: ScMobileTopNav
|
|
3
|
+
package: "@streamoid/ui"
|
|
4
|
+
category: mobile
|
|
5
|
+
status: stable
|
|
6
|
+
renders: div
|
|
7
|
+
tags: [mobile, nav, topbar, header, appbar, menu, search, chat, heading]
|
|
8
|
+
related: [ScMobileBottomAction, ScHeader, ScTableHeader, ScSideBarLogoUnit, ScOnlyField]
|
|
9
|
+
do_not_confuse_with: [ScHeader, ScTableHeader, ScSideBarLogoUnit, ScMobileBottomAction, ScTabSwitcher]
|
|
10
|
+
---
|
|
11
|
+
|
|
12
|
+
# ScMobileTopNav
|
|
13
|
+
|
|
14
|
+
**The mobile app bar, in three layouts.** A flex row over
|
|
15
|
+
`--alias-surface-canvas`, 56px tall because its 56×56 icon slots make it so (the root
|
|
16
|
+
itself sets no `height`), with three `type`s: `home` (menu icon only, no border), `chat`
|
|
17
|
+
(menu · chat title · more, bordered), and `inApp` (menu · centred heading · optional
|
|
18
|
+
search icon, bordered) — where `inApp` can flip into a full-width search field.
|
|
19
|
+
|
|
20
|
+
## TL;DR for agents
|
|
21
|
+
|
|
22
|
+
- **Reach for it when:** a mobile screen needs the standard top bar — hamburger on the
|
|
23
|
+
left, a title, and an optional search or overflow affordance on the right.
|
|
24
|
+
- **Don't reach for it when:** you need a desktop page header (→ `ScHeader`), a table's
|
|
25
|
+
column header (→ `ScTableHeader`), a sidebar logo row (→ `ScSideBarLogoUnit`), or the
|
|
26
|
+
sticky footer action bar (→ `ScMobileBottomAction`).
|
|
27
|
+
- **Five things that will bite you:**
|
|
28
|
+
1. **No icons are supplied.** `menuIcon`, `searchIconElement`, `closeIcon` and
|
|
29
|
+
`moreIcon` all fall back to an **invisible** 24×24 placeholder div. Render it bare
|
|
30
|
+
and you get an empty bar.
|
|
31
|
+
2. **`search={true}` with `searchIcon={false}` renders nothing at all.** Both flags
|
|
32
|
+
must be true for the search layout, and the non-search layout is gated on
|
|
33
|
+
`!search` — so that combination falls through every branch. See Gotcha 2.
|
|
34
|
+
3. **The icon hit areas are `div`s with `onClick`** — no `role`, no `tabIndex`, no
|
|
35
|
+
`aria-label`. Keyboard users cannot reach them.
|
|
36
|
+
4. **It is not sticky or fixed.** You position it.
|
|
37
|
+
5. `heading` defaults to `"Heading"` and `chatTitle` to
|
|
38
|
+
`"Chat title comes here..."`.
|
|
39
|
+
|
|
40
|
+
---
|
|
41
|
+
|
|
42
|
+
## 1. How to use it
|
|
43
|
+
|
|
44
|
+
### Import
|
|
45
|
+
|
|
46
|
+
```tsx
|
|
47
|
+
import { ScMobileTopNav } from "@streamoid/ui";
|
|
48
|
+
import "@streamoid/ui/dist/index.css"; // once, at your app root
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
### Minimal usage
|
|
52
|
+
|
|
53
|
+
```tsx
|
|
54
|
+
import { SiconMenu } from "@streamoid/icons";
|
|
55
|
+
|
|
56
|
+
<ScMobileTopNav type="home" menuIcon={<SiconMenu size={24} />} onMenuClick={openDrawer} />
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
### Props
|
|
60
|
+
|
|
61
|
+
| Prop | Type | Default | Notes |
|
|
62
|
+
|---|---|---|---|
|
|
63
|
+
| `type` | `"home"` \| `"chat"` \| `"inApp"` | `"home"` | The layout. `chat` and `inApp` add a bottom border **and** a 16px gap; `home` has neither. |
|
|
64
|
+
| `searchIcon` | `boolean` | `false` | `inApp` only. `false` → the trailing slot still renders but is `opacity: 0; pointer-events: none` (keeps the heading centred). |
|
|
65
|
+
| `search` | `boolean` | `false` | `inApp` only, **and only with `searchIcon`** — flips the bar into search-field mode. See Gotcha 2. |
|
|
66
|
+
| `heading` | `string` | `"Heading"` | ⚠️ `inApp` only. Centred, 16px secondary. **Does not truncate** — see Gotcha 7. |
|
|
67
|
+
| `chatTitle` | `string` | `"Chat title comes here..."` | ⚠️ `chat` only. Left-aligned, truncates with ellipsis. |
|
|
68
|
+
| `searchPlaceholder` | `string` | `"Search..."` | ⚠️ Hardcoded English default. |
|
|
69
|
+
| `searchValue` | `string` | – | Passed straight to `<input value>`. Omit → uncontrolled; pass it → you **must** wire `onSearchChange` or the field freezes. |
|
|
70
|
+
| `menuIcon` | `JSX.Element` | – | ⚠️ No default; falls back to an invisible 24×24 div. `home`, `chat`, `inApp` (non-search). |
|
|
71
|
+
| `searchIconElement` | `JSX.Element` | – | ⚠️ No default. The magnifier — in the trailing slot for `inApp`, and **leading the field** in search mode. |
|
|
72
|
+
| `closeIcon` | `JSX.Element` | – | ⚠️ No default. Search mode only, trailing. |
|
|
73
|
+
| `moreIcon` | `JSX.Element` | – | ⚠️ No default. `chat` only, trailing. |
|
|
74
|
+
| `onMenuClick` | `(e: React.MouseEvent) => void` | – | On the leading 56×56 slot. |
|
|
75
|
+
| `onSearchClick` | `(e: React.MouseEvent) => void` | – | On the trailing magnifier (`inApp`) **and** on the leading magnifier inside search mode. |
|
|
76
|
+
| `onCloseClick` | `(e: React.MouseEvent) => void` | – | Search mode's trailing slot. |
|
|
77
|
+
| `onMoreClick` | `(e: React.MouseEvent) => void` | – | `chat` mode's trailing slot. |
|
|
78
|
+
| `onSearchChange` | `(value: string) => void` | – | Receives `e.target.value` — the string, not the event. |
|
|
79
|
+
| `className` | `string` | – | Appended after internal classes. Unguarded concat — see Gotcha 8. |
|
|
80
|
+
| `...props` | `React.HTMLAttributes<HTMLDivElement>` | – | Spread onto the root `div`. |
|
|
81
|
+
|
|
82
|
+
### What renders in each state
|
|
83
|
+
|
|
84
|
+
| `type` | `searchIcon` | `search` | Layout | Border |
|
|
85
|
+
|---|---|---|---|---|
|
|
86
|
+
| `home` | — | — | `menuIcon` only, left-aligned | no |
|
|
87
|
+
| `chat` | — | — | `menuIcon` · `chatTitle` (grows, truncates) · `moreIcon` | yes |
|
|
88
|
+
| `inApp` | `false` | `false` | `menuIcon` · `heading` (centred) · **invisible** 56px spacer | yes |
|
|
89
|
+
| `inApp` | `true` | `false` | `menuIcon` · `heading` (centred) · `searchIconElement` | yes |
|
|
90
|
+
| `inApp` | `true` | `true` | `searchIconElement` + text input (grows) · `closeIcon` — **no menu, no heading** | yes |
|
|
91
|
+
| `inApp` | `false` | `true` | ⚠️ **nothing renders** — an empty 0-height bar. See Gotcha 2. |
|
|
92
|
+
|
|
93
|
+
### Recipes
|
|
94
|
+
|
|
95
|
+
```tsx
|
|
96
|
+
import { SiconMenu, SiconSearch, SiconClose, SiconMore } from "@streamoid/icons";
|
|
97
|
+
|
|
98
|
+
// A searchable in-app screen — the full state machine
|
|
99
|
+
const [searching, setSearching] = useState(false);
|
|
100
|
+
const [q, setQ] = useState("");
|
|
101
|
+
|
|
102
|
+
<ScMobileTopNav
|
|
103
|
+
type="inApp"
|
|
104
|
+
heading="Team"
|
|
105
|
+
searchIcon // must stay true in BOTH states
|
|
106
|
+
search={searching}
|
|
107
|
+
menuIcon={<SiconMenu size={24} />}
|
|
108
|
+
searchIconElement={<SiconSearch size={24} />}
|
|
109
|
+
closeIcon={<SiconClose size={24} />}
|
|
110
|
+
searchValue={q}
|
|
111
|
+
searchPlaceholder="Search members…"
|
|
112
|
+
onMenuClick={openDrawer}
|
|
113
|
+
onSearchClick={() => setSearching(true)}
|
|
114
|
+
onCloseClick={() => { setSearching(false); setQ(""); }}
|
|
115
|
+
onSearchChange={setQ}
|
|
116
|
+
/>
|
|
117
|
+
|
|
118
|
+
// A chat screen
|
|
119
|
+
<ScMobileTopNav
|
|
120
|
+
type="chat"
|
|
121
|
+
chatTitle={thread.title}
|
|
122
|
+
menuIcon={<SiconMenu size={24} />}
|
|
123
|
+
moreIcon={<SiconMore size={24} />}
|
|
124
|
+
onMenuClick={openDrawer}
|
|
125
|
+
onMoreClick={openThreadMenu}
|
|
126
|
+
/>
|
|
127
|
+
|
|
128
|
+
// Make it stick to the top of a scrolling screen — the component does not
|
|
129
|
+
<div style={{ position: "sticky", top: 0, zIndex: 10 }}>
|
|
130
|
+
<ScMobileTopNav type="home" menuIcon={<SiconMenu size={24} />} onMenuClick={openDrawer} />
|
|
131
|
+
</div>
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
---
|
|
135
|
+
|
|
136
|
+
## 2. Where to use it
|
|
137
|
+
|
|
138
|
+
The first child of every mobile screen — above the scrolling body and above
|
|
139
|
+
`ScMobileBottomAction`. The intended mobile shell is:
|
|
140
|
+
|
|
141
|
+
```
|
|
142
|
+
ScMobileTopNav ← 56px, shrink-0
|
|
143
|
+
<scrolling body> ← flex-1, overflow-y auto
|
|
144
|
+
ScMobileBottomAction ← shrink-0, sticky footer
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
It composes nothing from the DS — icons are slots, and the search field is a bare
|
|
148
|
+
`<input>`, not `ScOnlyField`.
|
|
149
|
+
|
|
150
|
+
---
|
|
151
|
+
|
|
152
|
+
## 3. When to use it
|
|
153
|
+
|
|
154
|
+
### Use it when
|
|
155
|
+
|
|
156
|
+
- The screen is a mobile route with a drawer/hamburger and a title.
|
|
157
|
+
- You want the 56px hit areas, the border-on-scroll-surfaces rule and the
|
|
158
|
+
centred-heading spacer logic without rebuilding them.
|
|
159
|
+
- The screen has an in-place search that replaces the bar (the `inApp` + `search` flow).
|
|
160
|
+
|
|
161
|
+
### Don't use it — reach for this instead
|
|
162
|
+
|
|
163
|
+
| Situation | Use instead |
|
|
164
|
+
|---|---|
|
|
165
|
+
| Desktop page/section header with a title | `ScHeader` (`text`, `state: up/down/none`) |
|
|
166
|
+
| A table's column header row | `ScTableHeader` / `ScBillingHistoryHeader` / `ScReferralTableHeader` |
|
|
167
|
+
| The sidebar's logo + app-switcher row | `ScSideBarLogoUnit` |
|
|
168
|
+
| Mobile sticky footer with 1–2 buttons | `ScMobileBottomAction` |
|
|
169
|
+
| Switching between views inside a screen | `ScTabSwitcher` + `ScTabComp`, below the nav |
|
|
170
|
+
| A styled search input anywhere else | `ScOnlyField` / `ScTextField` |
|
|
171
|
+
| The desktop app shell | `StreamoidSidebar` |
|
|
172
|
+
|
|
173
|
+
### Don't confuse with
|
|
174
|
+
|
|
175
|
+
| You may actually want | Not this |
|
|
176
|
+
|---|---|
|
|
177
|
+
| `ScHeader` — desktop title bar with a sort/collapse `state` | `ScMobileTopNav` is the mobile app bar with icon slots |
|
|
178
|
+
| `ScTableHeader` — a table's column labels | Not navigation |
|
|
179
|
+
| `ScSideBarLogoUnit` — logo + chevron, `expanded`/`collapsed` | Sidebar chrome, not a top bar |
|
|
180
|
+
| `ScMobileBottomAction` — the *bottom* bar, buttons not icons | Same family, opposite end of the screen |
|
|
181
|
+
|
|
182
|
+
---
|
|
183
|
+
|
|
184
|
+
## 4. Why to use it
|
|
185
|
+
|
|
186
|
+
- **The centred-heading trick is already right.** In `inApp` with `searchIcon={false}`
|
|
187
|
+
the trailing 56px slot still renders, hidden — that invisible spacer is what keeps
|
|
188
|
+
`heading` optically centred. Hand-rolled bars centre the title with the icon on one
|
|
189
|
+
side and it drifts.
|
|
190
|
+
- **56×56 hit areas** meet the touch-target minimum on every slot, consistently.
|
|
191
|
+
- **The border rule is encoded**: `home` (which sits on a hero/canvas) gets no divider;
|
|
192
|
+
`chat` and `inApp` (which sit above content) get a 0.5px `--alias-border-subtle` line
|
|
193
|
+
and a 16px gap.
|
|
194
|
+
- **Search is a layout swap, not an overlay**, so you don't manage a second component's
|
|
195
|
+
z-index and mount lifecycle — one `search` boolean.
|
|
196
|
+
- **Icon slots keep app iconography out of the DS**, so CXO, Catalogix and Photogenix can
|
|
197
|
+
each pass their own `Sicon*` set.
|
|
198
|
+
|
|
199
|
+
---
|
|
200
|
+
|
|
201
|
+
## Gotchas
|
|
202
|
+
|
|
203
|
+
**1. No icons are provided.** Every icon prop falls back to
|
|
204
|
+
`<div className={styles.iconPlaceholder} />` — a transparent 24×24 box. So the bar
|
|
205
|
+
renders, occupies 56px, responds to clicks, and shows **nothing**.
|
|
206
|
+
|
|
207
|
+
```tsx
|
|
208
|
+
// WRONG — an empty bar with an invisible tap target
|
|
209
|
+
<ScMobileTopNav type="home" onMenuClick={openDrawer} />
|
|
210
|
+
|
|
211
|
+
// RIGHT
|
|
212
|
+
<ScMobileTopNav type="home" menuIcon={<SiconMenu size={24} />} onMenuClick={openDrawer} />
|
|
213
|
+
```
|
|
214
|
+
|
|
215
|
+
**2. `search` without `searchIcon` renders an empty bar.** The two branches are
|
|
216
|
+
`type === "inApp" && searchIcon && search` and `type === "inApp" && !search`. With
|
|
217
|
+
`searchIcon={false}, search={true}` neither matches.
|
|
218
|
+
|
|
219
|
+
```tsx
|
|
220
|
+
// WRONG — renders an empty div; nothing at all inside the nav
|
|
221
|
+
<ScMobileTopNav type="inApp" search heading="Team" />
|
|
222
|
+
|
|
223
|
+
// RIGHT — keep searchIcon true in both states and toggle only `search`
|
|
224
|
+
<ScMobileTopNav type="inApp" searchIcon search={searching} heading="Team" />
|
|
225
|
+
```
|
|
226
|
+
|
|
227
|
+
**3. The icon slots are not accessible.** They are `div`s with `onClick` and
|
|
228
|
+
`cursor: pointer` — no `role="button"`, no `tabIndex`, no `aria-label`. Screen-reader and
|
|
229
|
+
keyboard users get nothing. There is no prop to fix this from outside; if the surface must
|
|
230
|
+
be accessible, pass an interactive element *as* the icon (e.g. a `<button aria-label="Menu">`
|
|
231
|
+
wrapping your `Sicon`), or fix the DS.
|
|
232
|
+
|
|
233
|
+
**4. The search `<input>` has no label.** Only `placeholder`. Add
|
|
234
|
+
`aria-label` support in the DS, or accept the gap.
|
|
235
|
+
|
|
236
|
+
**5. `searchValue` makes the input controlled.** Pass it without `onSearchChange` and the
|
|
237
|
+
field is frozen. `onSearchChange` receives the **string**, not the event.
|
|
238
|
+
|
|
239
|
+
**6. No submit.** There is no `<form>` and no Enter handling — wire debounced filtering
|
|
240
|
+
off `onSearchChange`, or add a `onKeyDown` via… you can't; `...props` goes to the root, not
|
|
241
|
+
the input. Filter as-you-type.
|
|
242
|
+
|
|
243
|
+
**7. `heading` does not truncate.** `.heading` has `flex: 1 0 0; min-width: 0` and
|
|
244
|
+
`text-align: center` but **no** `overflow`/`ellipsis` (unlike `.chatTitle`, which has
|
|
245
|
+
all three). A long heading wraps to two lines and breaks the 56px height.
|
|
246
|
+
|
|
247
|
+
**8. `className` is concatenated unguarded.** Omit it and the root carries a literal
|
|
248
|
+
`undefined` class.
|
|
249
|
+
|
|
250
|
+
**9. Not sticky, not fixed.** The root is a static `width: 100%` flex row over
|
|
251
|
+
`--alias-surface-canvas`. Wrap it yourself for sticky behaviour, and remember it has no
|
|
252
|
+
`z-index` of its own.
|
|
253
|
+
|
|
254
|
+
**10. In search mode the menu icon disappears.** There is no hamburger and no heading —
|
|
255
|
+
only the magnifier, the field, and close. If your drawer must stay reachable while
|
|
256
|
+
searching, this layout is wrong for you.
|
|
257
|
+
|
|
258
|
+
---
|
|
259
|
+
|
|
260
|
+
## In the wild
|
|
261
|
+
|
|
262
|
+
_No host render site found — used by the agent runtime / composed internally._
|
|
263
|
+
|
|
264
|
+
To be precise: it is exported from `@streamoid/ui` but no host app renders it, and it is
|
|
265
|
+
not agent-runtime — it is currently unused. CXO's mobile screens hand-roll exactly this
|
|
266
|
+
layout instead: `cxo-dashboard/src/app/components/mobile-teams-content.tsx` builds the
|
|
267
|
+
same bar from `SiconMenu` / `SiconSearch` / `SiconClose` in 56×56 `div`s (see the icon
|
|
268
|
+
uses at lines 174, 201, 211 and 240). Those screens are where this component belongs.
|
|
269
|
+
|
|
270
|
+
---
|
|
271
|
+
|
|
272
|
+
## Related
|
|
273
|
+
|
|
274
|
+
- `ScMobileBottomAction` — the other half of the mobile shell (sticky footer buttons).
|
|
275
|
+
- `ScHeader` — the desktop page-header equivalent.
|
|
276
|
+
- `ScSideBarLogoUnit` / `StreamoidSidebar` — the desktop shell's chrome.
|
|
277
|
+
- `ScTabSwitcher` / `ScTabComp` — what usually sits directly below this bar.
|
|
278
|
+
- `@streamoid/icons` — `SiconMenu`, `SiconSearch`, `SiconClose`, `SiconMore`; see
|
|
279
|
+
`packages/icons/ICONS.md`.
|
|
@@ -0,0 +1,291 @@
|
|
|
1
|
+
---
|
|
2
|
+
component: ScModal
|
|
3
|
+
package: "@streamoid/ui"
|
|
4
|
+
category: overlays
|
|
5
|
+
status: stable
|
|
6
|
+
renders: div (portalled to document.body)
|
|
7
|
+
tags: [modal, dialog, overlay, scrim, backdrop, portal, centered, blocking, escape]
|
|
8
|
+
related: [ScDrawer, ScInfoPopup, ScProfilePopup, ScGuide, ScButton]
|
|
9
|
+
do_not_confuse_with: [ScDrawer, ScInfoPopup, ScProfilePopup]
|
|
10
|
+
used_by: [catalogix]
|
|
11
|
+
---
|
|
12
|
+
|
|
13
|
+
# ScModal
|
|
14
|
+
|
|
15
|
+
**The centered blocking-dialog shell.** A fixed full-screen scrim portalled to
|
|
16
|
+
`<body>` with one padded, rounded content box centered inside it. It supplies the
|
|
17
|
+
scrim, the portal, backdrop-click close and Escape close — and *nothing else*: no
|
|
18
|
+
header, no footer, no close button, no width. Your dialog is `children`.
|
|
19
|
+
|
|
20
|
+
## TL;DR for agents
|
|
21
|
+
|
|
22
|
+
- **Reach for it when:** you need a centered dialog that blocks the page — confirm,
|
|
23
|
+
create/edit form, image preview, invite flow.
|
|
24
|
+
- **Don't reach for it when:** the panel should slide in from the screen edge
|
|
25
|
+
(→ `ScDrawer`), it's a small contextual "?" bubble (→ `ScInfoPopup`), or it's the
|
|
26
|
+
profile/account flyout (→ `ScProfilePopup`, which is a positioned panel with no
|
|
27
|
+
overlay of its own).
|
|
28
|
+
- **Four things that will bite you:**
|
|
29
|
+
1. `open` defaults to **`true`**. Mounting it shows it.
|
|
30
|
+
2. `className` / `style` land on the **backdrop**. The panel is
|
|
31
|
+
`contentClassName` / `contentStyle`. This is the reverse of most modal APIs.
|
|
32
|
+
3. There is **no `role="dialog"`, no `aria-modal`, no focus trap and no
|
|
33
|
+
body-scroll lock**, and no prop to add them — you must render your own
|
|
34
|
+
`role="dialog"` wrapper as the child.
|
|
35
|
+
4. The content box has **no width and no max-height**. Tall content runs off the
|
|
36
|
+
viewport with nothing to scroll. Set both in `contentStyle`.
|
|
37
|
+
|
|
38
|
+
---
|
|
39
|
+
|
|
40
|
+
## 1. How to use it
|
|
41
|
+
|
|
42
|
+
### Import
|
|
43
|
+
|
|
44
|
+
```tsx
|
|
45
|
+
import { ScModal } from "@streamoid/ui";
|
|
46
|
+
import "@streamoid/ui/dist/index.css"; // once, at your app root
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
### Minimal usage
|
|
50
|
+
|
|
51
|
+
The idiomatic host pattern is conditional mounting — don't bother with `open`:
|
|
52
|
+
|
|
53
|
+
```tsx
|
|
54
|
+
{isOpen && (
|
|
55
|
+
<ScModal onClose={() => setIsOpen(false)}>
|
|
56
|
+
<h2>Delete store?</h2>
|
|
57
|
+
<ScButton text="Delete" variant="error" size="md" onClick={confirmDelete} />
|
|
58
|
+
</ScModal>
|
|
59
|
+
)}
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
### Props
|
|
63
|
+
|
|
64
|
+
| Prop | Type | Default | Notes |
|
|
65
|
+
|---|---|---|---|
|
|
66
|
+
| `children` | `ReactNode` | – | Your whole dialog. The component adds no chrome. |
|
|
67
|
+
| `open` | `boolean` | `true` | ⚠️ Defaults to open. `false` returns `null` (unmounts, no CSS transition). |
|
|
68
|
+
| `onClose` | `() => void` | – | Called by backdrop mousedown and by Escape. Optional — omit it and the modal cannot be dismissed. |
|
|
69
|
+
| `closeOnBackdrop` | `boolean` | `true` | Backdrop **mousedown** (not click) fires `onClose`, and only when the event target *is* the backdrop. |
|
|
70
|
+
| `closeOnEsc` | `boolean` | `true` | Document-level `keydown` listener, active only while `open`. |
|
|
71
|
+
| `className` | `string` | – | **On the backdrop**, appended after `styles.backdrop`. |
|
|
72
|
+
| `style` | `CSSProperties` | – | **On the backdrop.** Use it to change the scrim or the z-index. |
|
|
73
|
+
| `contentClassName` | `string` | – | On the centered panel. |
|
|
74
|
+
| `contentStyle` | `CSSProperties` | – | On the centered panel. **This is where width / max-height / padding go.** |
|
|
75
|
+
|
|
76
|
+
There is no `...props` spread — anything not in this table is a type error and has
|
|
77
|
+
no runtime effect.
|
|
78
|
+
|
|
79
|
+
### What it actually renders
|
|
80
|
+
|
|
81
|
+
```html
|
|
82
|
+
<!-- portalled into document.body -->
|
|
83
|
+
<div class="backdrop {className}" style={style}> <!-- position:fixed, inset 0, z-index:999,
|
|
84
|
+
padding-left: var(--leftMenuWidth, 0),
|
|
85
|
+
background: rgba(16,16,16,0.7) -->
|
|
86
|
+
<div class="content {contentClassName}" style={contentStyle}> <!-- padding:24px; radius:16px;
|
|
87
|
+
background: --alias-surface-base -->
|
|
88
|
+
{children}
|
|
89
|
+
</div>
|
|
90
|
+
</div>
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
### Recipes
|
|
94
|
+
|
|
95
|
+
```tsx
|
|
96
|
+
// Sized, scrollable dialog with real dialog semantics
|
|
97
|
+
{isOpen && (
|
|
98
|
+
<ScModal
|
|
99
|
+
onClose={close}
|
|
100
|
+
contentStyle={{ width: 560, maxWidth: "calc(100vw - 48px)", maxHeight: "80vh", overflow: "auto" }}
|
|
101
|
+
>
|
|
102
|
+
<div role="dialog" aria-modal="true" aria-labelledby="invite-title">
|
|
103
|
+
<h2 id="invite-title">Invite a teammate</h2>
|
|
104
|
+
{/* … */}
|
|
105
|
+
</div>
|
|
106
|
+
</ScModal>
|
|
107
|
+
)}
|
|
108
|
+
|
|
109
|
+
// A form dialog that must NOT discard typed input on Escape (the Catalogix idiom)
|
|
110
|
+
<ScModal onClose={close} closeOnEsc={false}>
|
|
111
|
+
<QuickInviteForm />
|
|
112
|
+
</ScModal>
|
|
113
|
+
|
|
114
|
+
// Edge-to-edge content (image preview, embedded canvas) — kill the 24px padding
|
|
115
|
+
<ScModal onClose={close} contentStyle={{ padding: 0, borderRadius: 12, overflow: "hidden" }}>
|
|
116
|
+
<img src={url} style={{ display: "block", maxWidth: "90vw", maxHeight: "90vh" }} />
|
|
117
|
+
</ScModal>
|
|
118
|
+
|
|
119
|
+
// Lock body scroll yourself — the modal does not
|
|
120
|
+
useEffect(() => {
|
|
121
|
+
if (!isOpen) return;
|
|
122
|
+
const prev = document.body.style.overflow;
|
|
123
|
+
document.body.style.overflow = "hidden";
|
|
124
|
+
return () => { document.body.style.overflow = prev; };
|
|
125
|
+
}, [isOpen]);
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
---
|
|
129
|
+
|
|
130
|
+
## 2. Where to use it
|
|
131
|
+
|
|
132
|
+
- **Confirm / destructive-action dialogs** — the "are you sure?" pair with
|
|
133
|
+
`ScButton variant="error"`.
|
|
134
|
+
- **Create / edit forms** that are short enough not to want a drawer.
|
|
135
|
+
- **Media preview** overlays (pass `contentStyle={{ padding: 0 }}`).
|
|
136
|
+
- **Invite and quick-action flows** — Catalogix's `QuickInviteModal` goes through
|
|
137
|
+
its local `CenterModal` wrapper, which is a pass-through to this component.
|
|
138
|
+
|
|
139
|
+
Catalogix is the only host consumer today, via
|
|
140
|
+
`app/components/CenterModal` (its historical `ConterModal` spelling still works —
|
|
141
|
+
the default export is bound by position). CXO and Photogenix use app-local modal
|
|
142
|
+
shells; if you touch either, this is what they should converge on.
|
|
143
|
+
|
|
144
|
+
---
|
|
145
|
+
|
|
146
|
+
## 3. When to use it
|
|
147
|
+
|
|
148
|
+
### Use it when
|
|
149
|
+
|
|
150
|
+
- The task is **modal**: the user must finish or cancel before continuing.
|
|
151
|
+
- You want the scrim colour, the portal escape-hatch and the dismissal wiring to
|
|
152
|
+
match every other dialog in the product.
|
|
153
|
+
- Your dialog needs to **escape a transformed / `overflow: hidden` ancestor** — the
|
|
154
|
+
portal to `<body>` is the main reason to use this over a locally-positioned div.
|
|
155
|
+
|
|
156
|
+
### Don't use it — reach for this instead
|
|
157
|
+
|
|
158
|
+
| Situation | Use instead |
|
|
159
|
+
|---|---|
|
|
160
|
+
| Panel slides in from the right/left edge, full height | `ScDrawer` |
|
|
161
|
+
| Small contextual help bubble anchored to an "?" icon | `ScInfoPopup` |
|
|
162
|
+
| Profile / account flyout anchored to the sidebar footer | `ScProfilePopup` (panel body only — you position it) |
|
|
163
|
+
| Cross-product app switcher overlay | `ScAppSwitchPanel` via `StreamoidSidebar`'s `switchPanel` / `switchPanelOpen` |
|
|
164
|
+
| Kebab / context menu of actions | `ScMenuOptions` rows inside your own positioned container |
|
|
165
|
+
| Onboarding step callout with Skip / Next | `ScGuide` |
|
|
166
|
+
| Non-blocking toast / inline banner | `CreditWarningBanner` (exported without the `Sc` prefix), or your app's toast |
|
|
167
|
+
|
|
168
|
+
### Don't confuse with
|
|
169
|
+
|
|
170
|
+
| You may actually want | Not this |
|
|
171
|
+
|---|---|
|
|
172
|
+
| `ScDrawer` — same portal + Escape contract, but edge-anchored, 512px wide, and **no backdrop by default** | `ScModal` always has a scrim |
|
|
173
|
+
| `ScProfilePopup` — a *panel body*, renders no overlay and owns no open state | `ScModal` is the overlay |
|
|
174
|
+
| Catalogix's local `Modal` (`app/components/Modal`) — a much larger legacy shell with pagers/tabs/blur | `CenterModal` is the thin `ScModal` wrapper |
|
|
175
|
+
|
|
176
|
+
---
|
|
177
|
+
|
|
178
|
+
## 4. Why to use it
|
|
179
|
+
|
|
180
|
+
- **Portalling is already correct.** `createPortal(…, document.body)` means the
|
|
181
|
+
dialog is genuinely viewport-fixed even when rendered from inside a transformed,
|
|
182
|
+
`overflow: hidden` or `z-index`-trapped subtree. This is the single most common
|
|
183
|
+
hand-rolled-modal bug.
|
|
184
|
+
- **The scrim is the *fixed* scrim.** It is a deliberate neutral
|
|
185
|
+
`rgba(16, 16, 16, 0.7)` — the legacy navy `rgba(11, 8, 30, 0.5)` cast a blue tint
|
|
186
|
+
over everything behind the dialog. Rolling your own reintroduces that.
|
|
187
|
+
- **Dismissal is wired once.** Escape is a document listener that is added and
|
|
188
|
+
removed with `open`, and backdrop close checks `e.target === e.currentTarget` so
|
|
189
|
+
a mousedown inside your content never closes the dialog.
|
|
190
|
+
- **SSR-safe.** Returns `null` when `typeof document === "undefined"` instead of
|
|
191
|
+
throwing in `createPortal`.
|
|
192
|
+
- **One place to change.** Scrim opacity, panel radius and the left-nav inset are
|
|
193
|
+
one edit for every dialog in the app.
|
|
194
|
+
|
|
195
|
+
---
|
|
196
|
+
|
|
197
|
+
## Gotchas
|
|
198
|
+
|
|
199
|
+
**1. `open` defaults to `true`.** A mounted `ScModal` with no props is a visible
|
|
200
|
+
modal. Either mount it conditionally or always pass `open`.
|
|
201
|
+
|
|
202
|
+
```tsx
|
|
203
|
+
// WRONG — permanently open
|
|
204
|
+
<ScModal onClose={close}>…</ScModal> // rendered unconditionally
|
|
205
|
+
|
|
206
|
+
// RIGHT — either of these
|
|
207
|
+
{isOpen && <ScModal onClose={close}>…</ScModal>}
|
|
208
|
+
<ScModal open={isOpen} onClose={close}>…</ScModal>
|
|
209
|
+
```
|
|
210
|
+
|
|
211
|
+
**2. `className` / `style` are the *backdrop*, not the panel.** Sizing the dialog
|
|
212
|
+
with `style` silently resizes the scrim instead.
|
|
213
|
+
|
|
214
|
+
```tsx
|
|
215
|
+
// WRONG — sets width on the full-screen scrim; the panel stays content-sized
|
|
216
|
+
<ScModal style={{ width: 560 }}>…</ScModal>
|
|
217
|
+
|
|
218
|
+
// RIGHT
|
|
219
|
+
<ScModal contentStyle={{ width: 560 }}>…</ScModal>
|
|
220
|
+
```
|
|
221
|
+
|
|
222
|
+
**3. No dialog a11y at all.** No `role="dialog"`, no `aria-modal`, no focus trap,
|
|
223
|
+
no focus restore, no `aria-labelledby`, no body-scroll lock. And because there is
|
|
224
|
+
no props spread, you cannot pass aria onto the panel — wrap your children:
|
|
225
|
+
|
|
226
|
+
```tsx
|
|
227
|
+
<ScModal onClose={close}>
|
|
228
|
+
<div role="dialog" aria-modal="true" aria-labelledby="t">
|
|
229
|
+
<h2 id="t">Title</h2>
|
|
230
|
+
</div>
|
|
231
|
+
</ScModal>
|
|
232
|
+
```
|
|
233
|
+
|
|
234
|
+
**4. Tall content overflows the viewport with no scrollbar.** The panel has
|
|
235
|
+
`padding: 24px` and a radius and *no* size constraints. Always set
|
|
236
|
+
`maxHeight` + `overflow: auto` in `contentStyle` for anything list-shaped.
|
|
237
|
+
|
|
238
|
+
**5. Backdrop close is on `mousedown`, not `click`.** Two consequences: a
|
|
239
|
+
text-selection drag that ends on the backdrop still closes; and the modal
|
|
240
|
+
disappears before any underlying `click` handler would fire. Set
|
|
241
|
+
`closeOnBackdrop={false}` for dialogs with drag interactions inside.
|
|
242
|
+
|
|
243
|
+
**6. `padding-left: var(--leftMenuWidth, 0)` is a Catalogix contract.** The scrim
|
|
244
|
+
insets itself by that variable so the dialog centers in the content area rather
|
|
245
|
+
than the whole window. Catalogix defines it; **CXO, Photogenix and Artifax do
|
|
246
|
+
not**, so it falls back to `0` and the scrim covers the sidebar too. Define
|
|
247
|
+
`--leftMenuWidth` on your app root if you want the inset.
|
|
248
|
+
|
|
249
|
+
**7. The scrim is intentionally not theme-flipped.** It's a hardcoded dark
|
|
250
|
+
`rgba(16, 16, 16, 0.7)`, because a scrim is dark in both light and dark mode.
|
|
251
|
+
Don't "fix" it with a light-mode override.
|
|
252
|
+
|
|
253
|
+
**8. `z-index: 999` is hardcoded** — there is no `zIndex` prop (unlike `ScDrawer`).
|
|
254
|
+
To stack something above a modal, either give that thing a higher z-index or
|
|
255
|
+
raise this one via `style={{ zIndex: … }}`. A drawer over a modal is exactly what
|
|
256
|
+
`ScDrawer`'s `zIndex` prop exists for.
|
|
257
|
+
|
|
258
|
+
**9. `onClose` is optional.** Omit it and neither Escape nor the backdrop does
|
|
259
|
+
anything — you have built an undismissable modal with no close button.
|
|
260
|
+
|
|
261
|
+
**10. No enter/exit animation.** `open={false}` unmounts immediately. If you need
|
|
262
|
+
a fade, animate inside `children`.
|
|
263
|
+
|
|
264
|
+
---
|
|
265
|
+
|
|
266
|
+
## In the wild
|
|
267
|
+
|
|
268
|
+
```jsx
|
|
269
|
+
// catalogix/dashboard app/components/CenterModal/index.jsx:12
|
|
270
|
+
<ScModal
|
|
271
|
+
onClose={props.onClose}
|
|
272
|
+
className={props.className}
|
|
273
|
+
style={props.style}
|
|
274
|
+
contentStyle={props.contentStyle}
|
|
275
|
+
// Legacy ConterModal closed only on outside/backdrop click, never on Escape.
|
|
276
|
+
// Opt out to preserve parity (QuickInviteModal must not discard a typed email).
|
|
277
|
+
closeOnEsc={false}
|
|
278
|
+
>
|
|
279
|
+
{props.children}
|
|
280
|
+
</ScModal>
|
|
281
|
+
```
|
|
282
|
+
|
|
283
|
+
---
|
|
284
|
+
|
|
285
|
+
## Related
|
|
286
|
+
|
|
287
|
+
- `ScDrawer` — the edge-anchored sibling; same portal/Escape contract, opt-in backdrop, `zIndex` prop.
|
|
288
|
+
- `ScInfoPopup` — the tiny non-blocking contextual popover.
|
|
289
|
+
- `ScProfilePopup` — panel body for the profile flyout; you own the overlay and placement.
|
|
290
|
+
- `ScGuide` — onboarding callout card (Skip / step / Next), not a dialog shell.
|
|
291
|
+
- `ScButton` — the confirm/cancel pair inside the footer you build.
|