@streamoid/ui 0.6.17 → 0.6.19

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (134) hide show
  1. package/README.md +35 -18
  2. package/dist/docs/AGENTS.md +325 -0
  3. package/dist/docs/CreditWarningBanner.md +305 -0
  4. package/dist/docs/InvoiceHistoryMobile.md +222 -0
  5. package/dist/docs/ScAccess.md +259 -0
  6. package/dist/docs/ScAppCard.md +244 -0
  7. package/dist/docs/ScAppCardForCopilot.md +230 -0
  8. package/dist/docs/ScAppCardV3.md +273 -0
  9. package/dist/docs/ScAppField.md +308 -0
  10. package/dist/docs/ScAppListingCard.md +271 -0
  11. package/dist/docs/ScAppSwitchPanel.md +286 -0
  12. package/dist/docs/ScAppcardLogos.md +226 -0
  13. package/dist/docs/ScArtifaxInvite.md +262 -0
  14. package/dist/docs/ScArtifaxSidebar.md +330 -0
  15. package/dist/docs/ScAskAgentButton.md +307 -0
  16. package/dist/docs/ScBadges.md +261 -0
  17. package/dist/docs/ScBeacon.md +244 -0
  18. package/dist/docs/ScBillingHistoryHeader.md +210 -0
  19. package/dist/docs/ScBillingHistoryTableList.md +243 -0
  20. package/dist/docs/ScBillingLogsTableHeader.md +212 -0
  21. package/dist/docs/ScBillingLogsTableList.md +251 -0
  22. package/dist/docs/ScBriefCard.md +255 -0
  23. package/dist/docs/ScButton.md +251 -0
  24. package/dist/docs/ScCalendar.md +268 -0
  25. package/dist/docs/ScCalendarDateComps.md +264 -0
  26. package/dist/docs/ScCatalogixInvite.md +345 -0
  27. package/dist/docs/ScCatalogixSidebar.md +337 -0
  28. package/dist/docs/ScCatalogixStoreHeader.md +246 -0
  29. package/dist/docs/ScCatalogixStoreTableList.md +316 -0
  30. package/dist/docs/ScCheckField.md +233 -0
  31. package/dist/docs/ScCheckbox.md +272 -0
  32. package/dist/docs/ScCounter.md +235 -0
  33. package/dist/docs/ScCreditsUsageCard.md +247 -0
  34. package/dist/docs/ScCreditsUsageCardMobile.md +224 -0
  35. package/dist/docs/ScDefaultCard.md +269 -0
  36. package/dist/docs/ScDp.md +245 -0
  37. package/dist/docs/ScDrawer.md +318 -0
  38. package/dist/docs/ScFieldButton.md +255 -0
  39. package/dist/docs/ScFileField.md +268 -0
  40. package/dist/docs/ScGoogleSignIn.md +250 -0
  41. package/dist/docs/ScGuide.md +278 -0
  42. package/dist/docs/ScHDivider.md +210 -0
  43. package/dist/docs/ScHeader.md +222 -0
  44. package/dist/docs/ScImageField.md +253 -0
  45. package/dist/docs/ScInChatList.md +277 -0
  46. package/dist/docs/ScInChatMessage.md +205 -0
  47. package/dist/docs/ScInfoPopup.md +248 -0
  48. package/dist/docs/ScIntialProfileCover.md +233 -0
  49. package/dist/docs/ScInvoiceHistoryMobile.md +187 -0
  50. package/dist/docs/ScLogoUnit.md +232 -0
  51. package/dist/docs/ScMappingCard.md +241 -0
  52. package/dist/docs/ScMediaApproval.md +301 -0
  53. package/dist/docs/ScMediaSelect.md +310 -0
  54. package/dist/docs/ScMenuOptions.md +308 -0
  55. package/dist/docs/ScMobileBottomAction.md +252 -0
  56. package/dist/docs/ScMobileTopNav.md +279 -0
  57. package/dist/docs/ScModal.md +291 -0
  58. package/dist/docs/ScOnlyField.md +302 -0
  59. package/dist/docs/ScOnlyIcon.md +213 -0
  60. package/dist/docs/ScPagination.md +284 -0
  61. package/dist/docs/ScPairtext.md +287 -0
  62. package/dist/docs/ScPendingAction.md +238 -0
  63. package/dist/docs/ScPhtogenixInvite.md +275 -0
  64. package/dist/docs/ScPlanCard.md +302 -0
  65. package/dist/docs/ScPlanComparison.md +264 -0
  66. package/dist/docs/ScPlanDetailsCard.md +246 -0
  67. package/dist/docs/ScPlanDetailsCardMobile.md +240 -0
  68. package/dist/docs/ScPopUpMenu.md +224 -0
  69. package/dist/docs/ScProfile.md +234 -0
  70. package/dist/docs/ScProfileImageUpdate.md +261 -0
  71. package/dist/docs/ScProfileOptions.md +245 -0
  72. package/dist/docs/ScProfilePopup.md +396 -0
  73. package/dist/docs/ScProfileSettingsComp.md +250 -0
  74. package/dist/docs/ScProfileV2Mobile.md +216 -0
  75. package/dist/docs/ScProgressBar.md +267 -0
  76. package/dist/docs/ScQuickPrompt.md +277 -0
  77. package/dist/docs/ScRadio.md +228 -0
  78. package/dist/docs/ScReferralCardMobile.md +226 -0
  79. package/dist/docs/ScReferralTableHeader.md +260 -0
  80. package/dist/docs/ScReferralTableList.md +293 -0
  81. package/dist/docs/ScRole.md +226 -0
  82. package/dist/docs/ScRoleMobile.md +199 -0
  83. package/dist/docs/ScSelect.md +270 -0
  84. package/dist/docs/ScSelection.md +256 -0
  85. package/dist/docs/ScSelectionList.md +272 -0
  86. package/dist/docs/ScSelectionPill.md +240 -0
  87. package/dist/docs/ScSelectionPillGroup.md +302 -0
  88. package/dist/docs/ScSettingsNav.md +212 -0
  89. package/dist/docs/ScSettingsTabComp.md +260 -0
  90. package/dist/docs/ScSideBarLogoUnit.md +340 -0
  91. package/dist/docs/ScSidebar.md +243 -0
  92. package/dist/docs/ScSidebarIcons.md +232 -0
  93. package/dist/docs/ScSidebarMenu.md +283 -0
  94. package/dist/docs/ScSidebarProfile.md +231 -0
  95. package/dist/docs/ScSidebarSwitchMenu.md +258 -0
  96. package/dist/docs/ScSlider.md +194 -0
  97. package/dist/docs/ScStoreCard.md +252 -0
  98. package/dist/docs/ScStrLogo.md +253 -0
  99. package/dist/docs/ScStreamoidWordmark.md +302 -0
  100. package/dist/docs/ScSubAgent.md +226 -0
  101. package/dist/docs/ScTabComp.md +308 -0
  102. package/dist/docs/ScTabField.md +258 -0
  103. package/dist/docs/ScTabSwitcher.md +307 -0
  104. package/dist/docs/ScTableHeader.md +261 -0
  105. package/dist/docs/ScTableList.md +301 -0
  106. package/dist/docs/ScTableListMobile.md +282 -0
  107. package/dist/docs/ScTabs.md +268 -0
  108. package/dist/docs/ScTaxonomyPill.md +263 -0
  109. package/dist/docs/ScTextArea.md +259 -0
  110. package/dist/docs/ScTextField.md +324 -0
  111. package/dist/docs/ScThinkingStepIcon.md +249 -0
  112. package/dist/docs/ScTodoList.md +288 -0
  113. package/dist/docs/ScToggleSwitch.md +229 -0
  114. package/dist/docs/ScUsageHistoryMobile.md +194 -0
  115. package/dist/docs/ScVDivider.md +215 -0
  116. package/dist/docs/ScValueMappingL1.md +256 -0
  117. package/dist/docs/ScVersion.md +251 -0
  118. package/dist/docs/ScWorkspace.md +233 -0
  119. package/dist/docs/ScWorkspaceAccountMenu.md +115 -0
  120. package/dist/docs/ScWorkspaceCard.md +234 -0
  121. package/dist/docs/ScWorkspaceSettingsMobile.md +265 -0
  122. package/dist/docs/ScWorkspaceSwitchCard.md +312 -0
  123. package/dist/docs/ScWorkspaceSwitchMobile.md +241 -0
  124. package/dist/docs/ScWorkspaceSwitchMobileV2.md +278 -0
  125. package/dist/docs/StreamoidSidebar.md +413 -0
  126. package/dist/docs/StreamoidWorkspaceSwitcher.md +307 -0
  127. package/dist/docs/UsageHistoryMobile.md +235 -0
  128. package/dist/docs/components.json +4931 -0
  129. package/dist/index.css +361 -36
  130. package/dist/index.d.mts +213 -88
  131. package/dist/index.d.ts +213 -88
  132. package/dist/index.js +2486 -1629
  133. package/dist/index.mjs +2487 -1620
  134. package/package.json +5 -3
@@ -0,0 +1,413 @@
1
+ ---
2
+ component: StreamoidSidebar
3
+ package: "@streamoid/ui"
4
+ category: sidebar
5
+ status: stable
6
+ renders: div
7
+ tags: [sidebar, nav, shell, rail, collapse, expand, app-shell, switcher, primary-navigation]
8
+ related: [ScSideBarLogoUnit, ScAppSwitchPanel, ScSidebarMenu, ScAskAgentButton, CreditWarningBanner, ScArtifaxSidebar]
9
+ do_not_confuse_with: [ScSidebar, ScArtifaxSidebar, ScCatalogixSidebar, ScSidebarMenu, ScLogoUnit]
10
+ used_by: [cxo, photogenix, catalogix]
11
+ required_props: [expanded, onToggle, config, iconMap]
12
+ also_exports: [ScSidebarResizeHandle, advanceSidebarResizeGesture, SIDEBAR_RESIZE_THRESHOLD_PX]
13
+ ---
14
+
15
+ # StreamoidSidebar
16
+
17
+ **The current shared sidebar shell for every desktop Streamoid app.** It renders the
18
+ rounded 256px rail (or the 56px collapsed rail), a logo slot, the nav, an optional
19
+ gradient assistant CTA, a pre-footer slot, the profile row and the collapse toggle —
20
+ plus the app-switch panel as an *overlay* so opening it never pushes the nav down.
21
+
22
+ Note the name: it is **not** `Sc`-prefixed. `ScSidebar` is a different, legacy component.
23
+
24
+ ## TL;DR for agents
25
+
26
+ - **Reach for it when:** you are building (or fixing) a desktop app's primary
27
+ left navigation and you want the same chrome CXO, Photogenix and Catalogix use.
28
+ - **Don't reach for it when:** you are in Artifax (→ `ScArtifaxSidebar`, which owns
29
+ collapsible sections), you want one nav row (→ `ScSidebarMenu`), or you want the
30
+ product-switch list itself (→ `ScAppSwitchPanel`).
31
+ - **Five things that will bite you:**
32
+ 1. `versionText` defaults to **`"v1.0.0"`** — a fake version string. Every host
33
+ passes `versionText=""`.
34
+ 2. `bodyContent` is **ignored unless `showBody` is `true`**, and when it renders
35
+ it **replaces** `config.sections` entirely.
36
+ 3. `toggleInProfile` puts the collapse toggle inside the profile row — so with
37
+ `toggleInProfile` and **no `profile`, there is no toggle at all**.
38
+ 4. `config` and `iconMap` are **required** even when you drive the whole nav
39
+ through `bodyContent`. Pass `{ sections: [] }` and `{}`.
40
+ 5. Layout comes from **Tailwind utility class names** (`flex`, `shrink-0`,
41
+ `truncate`, `min-w-0`, `overflow-y-auto`). `dist/index.css` does **not** ship
42
+ them — your app must have Tailwind (all three hosts do).
43
+
44
+ ---
45
+
46
+ ## 1. How to use it
47
+
48
+ ### Import
49
+
50
+ ```tsx
51
+ import {
52
+ StreamoidSidebar,
53
+ type SidebarConfig,
54
+ type SidebarIconMap,
55
+ } from "@streamoid/ui";
56
+ import "@streamoid/ui/dist/index.css"; // once, at your app root
57
+ ```
58
+
59
+ ### Minimal usage
60
+
61
+ ```tsx
62
+ const config: SidebarConfig = {
63
+ sections: [{ id: "main", label: "Manage", items: [{ id: "stores", label: "Stores", iconKey: "bag" }] }],
64
+ };
65
+ const iconMap: SidebarIconMap = { bag: <SiconBag /> };
66
+
67
+ <StreamoidSidebar
68
+ expanded={expanded}
69
+ onToggle={() => setExpanded((v) => !v)}
70
+ config={config}
71
+ iconMap={iconMap}
72
+ activeItemId="stores"
73
+ onItemSelect={(item) => navigate(`/${item.id}`)}
74
+ versionText="" // else you ship "v1.0.0"
75
+ />
76
+ ```
77
+
78
+ ### Props
79
+
80
+ | Prop | Type | Default | Notes |
81
+ |---|---|---|---|
82
+ | `expanded` | `boolean` | — | **Required.** You own the state. `true` → 256px rounded card; `false` → 56px rail. Two completely different render branches. |
83
+ | `onToggle` | `() => void` | — | **Required.** Fired by the collapse toggle (and the mobile close icon). |
84
+ | `resizable` | `boolean` | `false` | Shows the shared 12px right-edge resize handle on desktop. The rail still has only expanded and collapsed widths. |
85
+ | `onExpandedChange` | `(expanded: boolean) => void` | – | Controlled resize callback. One drag may call it more than once as the pointer reverses; omit it to fall back to `onToggle`. |
86
+ | `config` | `SidebarConfig` | — | **Required.** `{ topItem?, sections, bottomItems? }`. Pass `{ sections: [] }` if you drive nav via `bodyContent`. |
87
+ | `iconMap` | `SidebarIconMap` | — | **Required.** `Record<iconKey, ReactNode \| ({item, active, expanded}) => ReactNode>`. A missing key renders **no icon**, silently. |
88
+ | `activeItemId` | `string` | – | Matching item gets `state="active"`. |
89
+ | `highlightedItemIds` | `Set<string>` | – | Items get `state="default-highlight"`. ⚠️ Only honoured for `topItems` in the **expanded** rail — see Gotcha 8. |
90
+ | `onItemSelect` | `(item, sectionId?) => void` | – | `sectionId` is `"top"`, `"bottom"`, or the section's `id`. |
91
+ | `moreIcon` | `ReactNode` | – | Trailing "…" affordance on a row. ⚠️ Expanded rail passes it to **top items only**. |
92
+ | `onMoreClick` | `(item, sectionId?) => void` | – | Same restriction as `moreIcon`. |
93
+ | `topItems` | `SidebarMenuItemConfig[]` | – | Stacked items above the first divider. **Supersedes `config.topItem`.** |
94
+ | `topItemStates` | `Record<string, "default" \| "active" \| "hover" \| "default-highlight" \| "default-active">` | – | Per-id override; wins over `activeItemId` / `highlightedItemIds`. Top items only. |
95
+ | `expandedLogo` | `ReactNode` | – | Slot. Normally a `ScSideBarLogoUnit state="expanded"`. |
96
+ | `collapsedLogo` | `ReactNode` | – | Slot. Normally a `ScSideBarLogoUnit state="collapsed"`. |
97
+ | `profile` | `SidebarProfileConfig` | – | `{ name, subtitle?, avatar?, onClick? }`. Footer row (expanded) / avatar only (collapsed). |
98
+ | `versionText` | `string` | `"v1.0.0"` | ⚠️ **Real default.** Pass `""` to hide (all hosts do). Empty string still renders the footer wrapper + toggle. |
99
+ | `toggleIcon` | `ReactNode \| ((expanded: boolean) => ReactNode)` | – | No default glyph — omit it and the toggle is an **invisible click target**. |
100
+ | `toggleInProfile` | `boolean` | `false` | Footer = profile (flex-1) + a separate toggle button beside it, no version line. Expanded only. Requires `profile`. |
101
+ | `hideFooter` | `boolean` | `false` | Hides the version/toggle footer. ⚠️ Asymmetric with `isMobile` — see Gotcha 6. |
102
+ | `assistantCta` | `AssistantCta \| null` | – | Renders `ScAskAgentSlot` (the gradient "Ask CXO" pill / collapsed square) above `preFooterContent`. |
103
+ | `preFooterContent` | `ReactNode` | – | Slot between the nav and the bottom items — where `CreditWarningBanner` goes. |
104
+ | `bodyContent` | `ReactNode` | – | Slot replacing the whole sections area. **Only rendered when `showBody`.** |
105
+ | `showBody` | `boolean` | `false` | ⚠️ Gate for `bodyContent`. `false` → your body is dropped and `config.sections` renders instead. |
106
+ | `switchPanel` | `ReactNode` | – | The app-switch list (normally `ScAppSwitchPanel`). Mounted whenever supplied **and `!isMobile`**, so open/close can animate. |
107
+ | `switchPanelOpen` | `boolean` | `false` | Drives the open styling, the backdrop and `pointer-events`. |
108
+ | `onSwitchPanelClose` | `() => void` | – | Fired by the dim backdrop (expanded) / the fixed click-catcher (collapsed). |
109
+ | `isMobile` | `boolean` | `false` | Moves the toggle into the logo row, forces `hideFooter` in the expanded branch, and **disables `switchPanel` entirely**. |
110
+ | `className` | `string` | – | Appended on the **outer** wrapper, not the card. |
111
+ | `style` | `CSSProperties` | – | Merged **after** the wrapper's padding in the expanded branch, so `style={{ padding: 0 }}` wins. |
112
+
113
+ ### Types
114
+
115
+ ```ts
116
+ interface SidebarMenuItemConfig { id: string; label?: string; iconKey: string }
117
+ interface SidebarSectionConfig { id: string; label?: string; items: SidebarMenuItemConfig[] }
118
+ interface SidebarConfig { topItem?: SidebarMenuItemConfig; sections: SidebarSectionConfig[]; bottomItems?: SidebarMenuItemConfig[] }
119
+ interface SidebarProfileConfig { name: string; subtitle?: string; avatar?: ReactNode; onClick?: () => void }
120
+ type SidebarIconRenderer = (ctx: { item: SidebarMenuItemConfig; active: boolean; expanded: boolean }) => ReactNode;
121
+ type SidebarIconMap = Record<string, ReactNode | SidebarIconRenderer>;
122
+ ```
123
+
124
+ ### What renders in each rail
125
+
126
+ | Region | `expanded` | `expanded` + `isMobile` | collapsed |
127
+ |---|---|---|---|
128
+ | Outer | 16px padding wrapper → 256px card, 16px radius, 1px subtle border overlay | same | no padding, 56px-ish rail, right border only |
129
+ | Logo row | `expandedLogo` (+ toggle only when `isMobile`) | + close/toggle icon on the right | `collapsedLogo` |
130
+ | Top items | `topItems ?? config.topItem`, wrapped in two dividers | same | same, tighter gaps |
131
+ | Body | `showBody ? bodyContent : config.sections` (labels shown) | same | `showBody ? bodyContent : config.sections` (**labels dropped**) |
132
+ | Assistant CTA | `ScAskAgentSlot` pill | pill | `ScAskAgentSlot collapsed` square |
133
+ | Pre-footer | `preFooterContent` | same | same |
134
+ | Bottom items | `config.bottomItems` | same | same |
135
+ | Footer | profile + (toggle inline, or version + toggle) | footer hidden (`isMobile`) | avatar, then toggle + `versionText` (56px wide) |
136
+ | Switch panel | animated drop-down inside a bordered card + 90% backdrop dim | **not rendered** | 240px flyout at `left: calc(100% + 8px)`, fixed transparent click-catcher |
137
+
138
+ ### Recipes
139
+
140
+ ```tsx
141
+ // The idiomatic host wiring (what CXO, Photogenix and Catalogix all do):
142
+ // nav rendered through bodyContent, empty config, no version, toggle in profile.
143
+ <StreamoidSidebar
144
+ expanded={!collapsed}
145
+ onToggle={() => setCollapsed((v) => !v)}
146
+ config={{ sections: [] }}
147
+ iconMap={{}}
148
+ showBody
149
+ bodyContent={<MyNav collapsed={collapsed} />}
150
+ expandedLogo={<ScSideBarLogoUnit wordmark={<MyWordmark />} onClick={toggleAppList} />}
151
+ collapsedLogo={<ScSideBarLogoUnit state="collapsed" wordmark={<MyMark />} onClick={toggleAppList} hover={appListOpen ? true : undefined} />}
152
+ switchPanel={apps.length ? <ScAppSwitchPanel apps={apps} /> : undefined}
153
+ switchPanelOpen={appListOpen}
154
+ onSwitchPanelClose={() => setAppListOpen(false)}
155
+ profile={{ name, subtitle: workspace, avatar: <ScDp type="initial" initial={initial} size={32} />, onClick: openProfile }}
156
+ versionText=""
157
+ toggleInProfile
158
+ toggleIcon={(exp) => <SiconCollapse size={20} style={exp ? undefined : { transform: "rotate(180deg)" }} />}
159
+ />
160
+
161
+ // Config-driven nav (the JSON path — see sidebar-config.example.json)
162
+ <StreamoidSidebar
163
+ expanded={expanded}
164
+ onToggle={toggle}
165
+ config={sidebarConfigJson as SidebarConfig}
166
+ iconMap={{ plus: <SiconPlus />, dot: ({ active }) => <Dot on={active} /> }}
167
+ activeItemId={activeId}
168
+ onItemSelect={(item, sectionId) => route(sectionId, item.id)}
169
+ versionText=""
170
+ />
171
+
172
+ // Gradient assistant CTA + low-credits banner in the pre-footer
173
+ <StreamoidSidebar
174
+ /* … */
175
+ assistantCta={{ label: "Ask CXO", icon: <SiconBolt size={20} color="currentColor" />, active: askOpen, onClick: toggleAsk }}
176
+ preFooterContent={
177
+ lowCredits ? (
178
+ <CreditWarningBanner
179
+ availableCredits={credits}
180
+ remainingPct={Math.round(pct)}
181
+ level="danger"
182
+ expanded={expanded}
183
+ onBuyCredits={goBilling}
184
+ onCollapsedClick={onToggle}
185
+ />
186
+ ) : undefined
187
+ }
188
+ />
189
+ ```
190
+
191
+ ---
192
+
193
+ ## 2. Where to use it
194
+
195
+ - **The primary left navigation of every desktop app.** CXO
196
+ (`src/app/components/app-sidebar.tsx`), Photogenix
197
+ (`dashboard/client/src/components/layout/Sidebar.tsx`) and Catalogix
198
+ (`app/containers/LeftMenu/index.jsx`) each render exactly one, at the root of
199
+ their app shell.
200
+ - It composes, or expects you to compose into its slots:
201
+ `ScSideBarLogoUnit` (logo slots), `ScAppSwitchPanel` (`switchPanel`),
202
+ `ScSidebarMenu` (rendered internally from `config`, or by you inside
203
+ `bodyContent`), `ScAskAgentSlot` (from `assistantCta`),
204
+ `CreditWarningBanner` (`preFooterContent`), `ScDp` (`profile.avatar`).
205
+ - `ScCatalogixSidebar` is a thin wrapper around it. Artifax deliberately does
206
+ **not** use it (`ScArtifaxSidebar` instead).
207
+
208
+ ---
209
+
210
+ ## 3. When to use it
211
+
212
+ ### Use it when
213
+
214
+ - You need the shared app-shell rail: 256/56px, rounded card, dividers, profile
215
+ footer, collapse toggle, animated app-switch overlay.
216
+ - You want the app switcher to behave identically everywhere — as an overlay,
217
+ never pushing the nav down.
218
+ - Your nav is either a plain data structure (`config`) *or* app-specific enough
219
+ that you'd rather render it yourself (`bodyContent`) while keeping the chrome.
220
+
221
+ ### Don't use it — reach for this instead
222
+
223
+ | Situation | Use instead |
224
+ |---|---|
225
+ | Artifax's sidebar (collapsible labelled sections, no `switchPanel`) | `ScArtifaxSidebar` |
226
+ | Catalogix with default Stores/AI-Training/Insights nav | `ScCatalogixSidebar` (a wrapper — but Catalogix now composes `StreamoidSidebar` directly) |
227
+ | One nav row | `ScSidebarMenu` |
228
+ | One product-switch row (icon + name + description) | `ScSidebarSwitchMenu` |
229
+ | The product-switch list itself | `ScAppSwitchPanel` |
230
+ | The logo + switch chevron row | `ScSideBarLogoUnit` |
231
+ | Mobile top bar / bottom action bar | `ScMobileTopNav` / `ScMobileBottomAction` |
232
+ | A settings-page left nav | `ScSettingsNav` / `ScSettingsTabComp` |
233
+ | The workspace switcher | `ScWorkspaceSwitchCard` / `StreamoidWorkspaceSwitcher` |
234
+
235
+ ### Don't confuse with
236
+
237
+ | You may actually want | Not this |
238
+ |---|---|
239
+ | `ScSidebar` — a legacy, pre-`StreamoidSidebar` shell with only `state` + `onToggle` | `StreamoidSidebar` is the current one |
240
+ | `ScSidebarMenu` — one row | This is the whole rail |
241
+ | `ScLogoUnit` — legacy logo part | Use `ScSideBarLogoUnit` |
242
+ | `ScSidebarProfile` / `ScSidebarSwitchMenu` — à-la-carte legacy parts | This renders the profile row itself |
243
+ | `ScArtifaxSidebar` — sibling shell, **not** a wrapper of this one | Different props (`sections`, `openSections`), no `switchPanel` |
244
+ | `ScCatalogixSidebar` — **is** a wrapper of this one | Hides most props behind Catalogix defaults |
245
+
246
+ ---
247
+
248
+ ## 4. Why to use it
249
+
250
+ - **The two rails are already correct.** Expanded and collapsed are separate
251
+ render branches with different padding, gaps, divider rules and footer layout.
252
+ Reimplementing that from a design spec is a day of pixel-chasing.
253
+ - **The switch-panel overlay is the hard part, and it's done.** Expanded, it
254
+ animates via `grid-template-rows: 0fr → 1fr` inside an absolutely-positioned
255
+ card so it floats instead of pushing the nav down; the rest of the rail dims to
256
+ 90%. Collapsed, it becomes a 240px flyout with a fixed transparent click-catcher.
257
+ Both stay mounted so *closing* animates too.
258
+ - **Slot design, not a fork.** Every app has a different nav; `bodyContent`,
259
+ `preFooterContent`, `switchPanel` and the logo slots let you keep the chrome and
260
+ own the content, which is why all three apps converged on one component.
261
+ - **Token-only surfaces.** `--alias-surface-base`, `--alias-border-subtle`,
262
+ `--alias-fill-neutral-neutral` for hover — it flips to light mode with no
263
+ conditionals in your code.
264
+ - **Layout-stable toggling.** The switch card uses `inset box-shadow` rather than
265
+ `border` so opening it can't shift a single pixel of the card below.
266
+
267
+ ---
268
+
269
+ ## Gotchas
270
+
271
+ **1. `versionText` defaults to `"v1.0.0"`.** A hardcoded fake version in your
272
+ production nav. Every host passes `""`.
273
+
274
+ ```tsx
275
+ // WRONG — ships "v1.0.0"
276
+ <StreamoidSidebar expanded onToggle={t} config={c} iconMap={m} />
277
+
278
+ // RIGHT
279
+ <StreamoidSidebar expanded onToggle={t} config={c} iconMap={m} versionText="" />
280
+ ```
281
+
282
+ **2. `bodyContent` needs `showBody`, and then it replaces `config.sections`.**
283
+
284
+ ```tsx
285
+ // WRONG — body silently dropped, empty sections render instead
286
+ <StreamoidSidebar config={{ sections: [] }} bodyContent={<MyNav />} … />
287
+
288
+ // RIGHT
289
+ <StreamoidSidebar config={{ sections: [] }} showBody bodyContent={<MyNav />} … />
290
+ ```
291
+
292
+ **3. `toggleInProfile` without `profile` removes the toggle entirely.** The
293
+ expanded footer is either *(profile + inline toggle)* or *(version + toggle)*;
294
+ `toggleInProfile` selects the first branch, and that branch only renders inside
295
+ `profile ? … : null`. No profile → no collapse affordance at all.
296
+
297
+ **4. There is no default `toggleIcon`.** `resolveToggleIcon` returns `null` when
298
+ `toggleIcon` is omitted, so the toggle becomes an invisible (but clickable) box.
299
+ Always pass a glyph, and rotate it yourself for the collapsed state.
300
+
301
+ **5. The logo → nav divider is only drawn on the `topItems` path.** With no
302
+ `topItems`/`config.topItem` and `showBody`, nothing separates the logo from your
303
+ body — which is why all three hosts emit their own `<ScHDivider />` as the first
304
+ child of `bodyContent`.
305
+
306
+ **6. `hideFooter` and `isMobile` are asymmetric.** Expanded uses
307
+ `hideFooter || isMobile`; the **collapsed** branch checks `hideFooter` only. A
308
+ collapsed mobile rail still renders the version/toggle footer.
309
+
310
+ **7. `switchPanel` is dead when `isMobile`.** `hasSwitchCard = !!(switchPanel && !isMobile)`
311
+ — so on mobile the logo row's own toggle is your only affordance, and
312
+ `onSwitchPanelClose` never fires.
313
+
314
+ **8. `moreIcon` / `onMoreClick` / `highlightedItemIds` are dropped for section and
315
+ bottom items in the *expanded* rail.** They are only forwarded to `topItems` there,
316
+ while the collapsed rail forwards them everywhere. A "…" menu that appears when
317
+ collapsed and vanishes when expanded is this, not your code.
318
+
319
+ ```tsx
320
+ // Expanded sections get no more-menu. Render your rows yourself if you need one:
321
+ <StreamoidSidebar showBody bodyContent={
322
+ items.map((i) => <ScSidebarMenu key={i.id} text={i.label} moreIcon={<SiconMore />} onMoreClick={…} />)
323
+ } … />
324
+ ```
325
+
326
+ **9. Section labels only exist in the expanded rail.** `SectionHeader` isn't
327
+ rendered in the collapsed branch — don't put meaning-bearing text there only.
328
+
329
+ **10. The profile row and the collapse toggle are `div`s with `onClick`.** No
330
+ `<button>`, no `tabIndex`, no keyboard access, and hover is wired through
331
+ `onMouseEnter`/`onMouseLeave` writing inline styles. The root is a plain `div` with
332
+ no `<nav>`/`aria-label`; wrap it yourself:
333
+
334
+ ```tsx
335
+ <nav aria-label="Main navigation"><StreamoidSidebar … /></nav>
336
+ ```
337
+
338
+ **11. Widths are hardcoded.** Expanded card `width: 256`, inside a
339
+ `var(--spacing-3xl)` padded wrapper (≈288px footprint); the collapsed footer block
340
+ is `width: 56`. Not responsive, not overridable via props — hosts measure these
341
+ constants themselves.
342
+
343
+ The optional resize edge is a two-state gesture, not a free-width splitter.
344
+ After either state change it re-anchors at the crossing point, so the user can
345
+ collapse and expand again in the same uninterrupted drag. Hosts should pass a
346
+ controlled setter: `onExpandedChange={setExpanded}`. In the collapsed state,
347
+ the 12px hit target straddles the rail boundary and its blue line renders just
348
+ outside the rail; expanded positioning remains on the wrapper's outside edge.
349
+
350
+ **12. Tailwind utilities are a hard requirement.** The component's structure is
351
+ `className="flex flex-col flex-1 min-h-0 overflow-y-auto shrink-0 truncate …"` with
352
+ inline styles only for colours/spacing. `dist/index.css` ships the
353
+ `.text-text-*` typography classes but **none** of those utilities, and no
354
+ `.sidebar-scroll` rule either (that class is a hook for the host to style the
355
+ scrollbar). Without Tailwind the rail collapses and labels stop truncating.
356
+
357
+ **13. The collapsed flyout is `position: absolute` inside the rail.** Any host
358
+ ancestor with `overflow: hidden` will clip it. Hosts that can't guarantee that
359
+ (Artifax) portal their own flyout to `document.body` instead.
360
+
361
+ **14. `iconMap` misses fail silently.** A wrong `iconKey` yields `null` — in the
362
+ collapsed rail that is a blank, still-clickable row.
363
+
364
+ ---
365
+
366
+ ## In the wild
367
+
368
+ ```tsx
369
+ // cxo-dashboard src/app/components/app-sidebar.tsx:965
370
+ <StreamoidSidebar
371
+ expanded={sidebarExpanded}
372
+ onToggle={onToggle}
373
+ isMobile={isMobile}
374
+ showBody={showCopilotPanel}
375
+ config={isMobile ? MOBILE_SIDEBAR_CONFIG : BASE_SIDEBAR_CONFIG}
376
+ iconMap={iconMap}
377
+ onItemSelect={onItemSelect}
378
+ expandedLogo={<ExpandedLogo onClick={toggleAppList} />}
379
+ collapsedLogo={
380
+ <div style={{ paddingBottom: "var(--spacing-md)" }}>
381
+ <CollapsedLogo onClick={toggleAppList} open={appListOpen} />
382
+ </div>
383
+ }
384
+ bodyContent={sidebarContent}
385
+ switchPanel={switchPanelContent}
386
+ switchPanelOpen={appListOpen}
387
+ onSwitchPanelClose={() => setAppListOpen(false)}
388
+ preFooterContent={creditWarning ? <CreditWarningBanner … /> : undefined}
389
+ profile={{ name: displayName, subtitle: displayWorkspace, avatar: <ProfileAvatar … />, onClick: … }}
390
+ versionText=""
391
+ toggleInProfile
392
+ toggleIcon={(exp) => <SiconCollapse className="size-5" style={exp ? undefined : { transform: "rotate(180deg)" }} />}
393
+ />
394
+ ```
395
+
396
+ Also: `catalogix/dashboard app/containers/LeftMenu/index.jsx:991` (adds
397
+ `assistantCta` + `activeItemId`), and
398
+ `photogenix_v2 dashboard/client/src/components/layout/Sidebar.tsx:854`
399
+ (`preFooterContent` carries an upgrade CTA + low-credits node).
400
+
401
+ ---
402
+
403
+ ## Related
404
+
405
+ - `ScSideBarLogoUnit` — what goes in `expandedLogo` / `collapsedLogo`.
406
+ - `ScAppSwitchPanel` — what goes in `switchPanel`.
407
+ - `ScSidebarMenu` — the row this renders from `config`; render it yourself inside `bodyContent`.
408
+ - `ScAskAgentButton` / `ScAskAgentSlot` — what `assistantCta` renders.
409
+ - `CreditWarningBanner` — the usual `preFooterContent`.
410
+ - `ScArtifaxSidebar` — the sibling shell used by Artifax.
411
+ - `ScCatalogixSidebar` — a Catalogix-flavoured wrapper of this component.
412
+ - `ScSidebar` / `ScLogoUnit` / `ScSidebarProfile` / `ScSidebarSwitchMenu` — legacy parts this replaced.
413
+ - `sidebar-config.example.json` (this folder) — a complete `SidebarConfig` to copy.