@streamoid/ui 0.6.16 → 0.6.18

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (131) hide show
  1. package/README.md +35 -18
  2. package/dist/docs/AGENTS.md +321 -0
  3. package/dist/docs/CreditWarningBanner.md +305 -0
  4. package/dist/docs/InvoiceHistoryMobile.md +222 -0
  5. package/dist/docs/ScAccess.md +259 -0
  6. package/dist/docs/ScAppCard.md +244 -0
  7. package/dist/docs/ScAppCardForCopilot.md +230 -0
  8. package/dist/docs/ScAppCardV3.md +273 -0
  9. package/dist/docs/ScAppField.md +308 -0
  10. package/dist/docs/ScAppListingCard.md +271 -0
  11. package/dist/docs/ScAppSwitchPanel.md +286 -0
  12. package/dist/docs/ScAppcardLogos.md +226 -0
  13. package/dist/docs/ScArtifaxInvite.md +262 -0
  14. package/dist/docs/ScArtifaxSidebar.md +330 -0
  15. package/dist/docs/ScAskAgentButton.md +307 -0
  16. package/dist/docs/ScBadges.md +261 -0
  17. package/dist/docs/ScBeacon.md +244 -0
  18. package/dist/docs/ScBillingHistoryHeader.md +210 -0
  19. package/dist/docs/ScBillingHistoryTableList.md +243 -0
  20. package/dist/docs/ScBillingLogsTableHeader.md +212 -0
  21. package/dist/docs/ScBillingLogsTableList.md +251 -0
  22. package/dist/docs/ScBriefCard.md +255 -0
  23. package/dist/docs/ScButton.md +251 -0
  24. package/dist/docs/ScCalendar.md +268 -0
  25. package/dist/docs/ScCalendarDateComps.md +264 -0
  26. package/dist/docs/ScCatalogixInvite.md +345 -0
  27. package/dist/docs/ScCatalogixSidebar.md +337 -0
  28. package/dist/docs/ScCatalogixStoreHeader.md +246 -0
  29. package/dist/docs/ScCatalogixStoreTableList.md +316 -0
  30. package/dist/docs/ScCheckField.md +233 -0
  31. package/dist/docs/ScCheckbox.md +272 -0
  32. package/dist/docs/ScCounter.md +235 -0
  33. package/dist/docs/ScCreditsUsageCard.md +247 -0
  34. package/dist/docs/ScCreditsUsageCardMobile.md +224 -0
  35. package/dist/docs/ScDefaultCard.md +269 -0
  36. package/dist/docs/ScDp.md +245 -0
  37. package/dist/docs/ScDrawer.md +318 -0
  38. package/dist/docs/ScFieldButton.md +255 -0
  39. package/dist/docs/ScFileField.md +268 -0
  40. package/dist/docs/ScGoogleSignIn.md +250 -0
  41. package/dist/docs/ScGuide.md +278 -0
  42. package/dist/docs/ScHDivider.md +213 -0
  43. package/dist/docs/ScHeader.md +222 -0
  44. package/dist/docs/ScImageField.md +253 -0
  45. package/dist/docs/ScInChatList.md +277 -0
  46. package/dist/docs/ScInChatMessage.md +205 -0
  47. package/dist/docs/ScInfoPopup.md +248 -0
  48. package/dist/docs/ScIntialProfileCover.md +233 -0
  49. package/dist/docs/ScInvoiceHistoryMobile.md +187 -0
  50. package/dist/docs/ScLogoUnit.md +232 -0
  51. package/dist/docs/ScMappingCard.md +241 -0
  52. package/dist/docs/ScMediaApproval.md +301 -0
  53. package/dist/docs/ScMediaSelect.md +310 -0
  54. package/dist/docs/ScMenuOptions.md +308 -0
  55. package/dist/docs/ScMobileBottomAction.md +252 -0
  56. package/dist/docs/ScMobileTopNav.md +279 -0
  57. package/dist/docs/ScModal.md +291 -0
  58. package/dist/docs/ScOnlyField.md +302 -0
  59. package/dist/docs/ScOnlyIcon.md +213 -0
  60. package/dist/docs/ScPagination.md +284 -0
  61. package/dist/docs/ScPairtext.md +287 -0
  62. package/dist/docs/ScPendingAction.md +238 -0
  63. package/dist/docs/ScPhtogenixInvite.md +275 -0
  64. package/dist/docs/ScPlanCard.md +302 -0
  65. package/dist/docs/ScPlanComparison.md +264 -0
  66. package/dist/docs/ScPlanDetailsCard.md +246 -0
  67. package/dist/docs/ScPlanDetailsCardMobile.md +240 -0
  68. package/dist/docs/ScPopUpMenu.md +224 -0
  69. package/dist/docs/ScProfile.md +234 -0
  70. package/dist/docs/ScProfileImageUpdate.md +261 -0
  71. package/dist/docs/ScProfileOptions.md +245 -0
  72. package/dist/docs/ScProfilePopup.md +396 -0
  73. package/dist/docs/ScProfileSettingsComp.md +250 -0
  74. package/dist/docs/ScProfileV2Mobile.md +216 -0
  75. package/dist/docs/ScProgressBar.md +267 -0
  76. package/dist/docs/ScQuickPrompt.md +277 -0
  77. package/dist/docs/ScRadio.md +228 -0
  78. package/dist/docs/ScReferralCardMobile.md +226 -0
  79. package/dist/docs/ScReferralTableHeader.md +260 -0
  80. package/dist/docs/ScReferralTableList.md +293 -0
  81. package/dist/docs/ScRole.md +226 -0
  82. package/dist/docs/ScRoleMobile.md +199 -0
  83. package/dist/docs/ScSelect.md +270 -0
  84. package/dist/docs/ScSelection.md +256 -0
  85. package/dist/docs/ScSelectionList.md +272 -0
  86. package/dist/docs/ScSelectionPill.md +240 -0
  87. package/dist/docs/ScSelectionPillGroup.md +302 -0
  88. package/dist/docs/ScSettingsNav.md +212 -0
  89. package/dist/docs/ScSettingsTabComp.md +260 -0
  90. package/dist/docs/ScSideBarLogoUnit.md +340 -0
  91. package/dist/docs/ScSidebar.md +243 -0
  92. package/dist/docs/ScSidebarIcons.md +232 -0
  93. package/dist/docs/ScSidebarMenu.md +283 -0
  94. package/dist/docs/ScSidebarProfile.md +231 -0
  95. package/dist/docs/ScSidebarSwitchMenu.md +258 -0
  96. package/dist/docs/ScSlider.md +194 -0
  97. package/dist/docs/ScStoreCard.md +252 -0
  98. package/dist/docs/ScStrLogo.md +253 -0
  99. package/dist/docs/ScStreamoidWordmark.md +302 -0
  100. package/dist/docs/ScSubAgent.md +226 -0
  101. package/dist/docs/ScTabComp.md +308 -0
  102. package/dist/docs/ScTabField.md +258 -0
  103. package/dist/docs/ScTabSwitcher.md +307 -0
  104. package/dist/docs/ScTableHeader.md +261 -0
  105. package/dist/docs/ScTableList.md +301 -0
  106. package/dist/docs/ScTableListMobile.md +282 -0
  107. package/dist/docs/ScTabs.md +268 -0
  108. package/dist/docs/ScTaxonomyPill.md +263 -0
  109. package/dist/docs/ScTextArea.md +259 -0
  110. package/dist/docs/ScTextField.md +324 -0
  111. package/dist/docs/ScThinkingStepIcon.md +249 -0
  112. package/dist/docs/ScTodoList.md +288 -0
  113. package/dist/docs/ScToggleSwitch.md +229 -0
  114. package/dist/docs/ScUsageHistoryMobile.md +194 -0
  115. package/dist/docs/ScVDivider.md +215 -0
  116. package/dist/docs/ScValueMappingL1.md +256 -0
  117. package/dist/docs/ScVersion.md +251 -0
  118. package/dist/docs/ScWorkspace.md +233 -0
  119. package/dist/docs/ScWorkspaceCard.md +234 -0
  120. package/dist/docs/ScWorkspaceSettingsMobile.md +265 -0
  121. package/dist/docs/ScWorkspaceSwitchCard.md +314 -0
  122. package/dist/docs/ScWorkspaceSwitchMobile.md +241 -0
  123. package/dist/docs/ScWorkspaceSwitchMobileV2.md +278 -0
  124. package/dist/docs/StreamoidSidebar.md +403 -0
  125. package/dist/docs/StreamoidWorkspaceSwitcher.md +307 -0
  126. package/dist/docs/UsageHistoryMobile.md +235 -0
  127. package/dist/docs/components.json +4849 -0
  128. package/dist/index.css +43 -37
  129. package/dist/index.d.mts +10 -0
  130. package/dist/index.d.ts +10 -0
  131. package/package.json +3 -2
@@ -0,0 +1,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.