@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.
- package/README.md +35 -18
- package/dist/docs/AGENTS.md +321 -0
- package/dist/docs/CreditWarningBanner.md +305 -0
- package/dist/docs/InvoiceHistoryMobile.md +222 -0
- package/dist/docs/ScAccess.md +259 -0
- package/dist/docs/ScAppCard.md +244 -0
- package/dist/docs/ScAppCardForCopilot.md +230 -0
- package/dist/docs/ScAppCardV3.md +273 -0
- package/dist/docs/ScAppField.md +308 -0
- package/dist/docs/ScAppListingCard.md +271 -0
- package/dist/docs/ScAppSwitchPanel.md +286 -0
- package/dist/docs/ScAppcardLogos.md +226 -0
- package/dist/docs/ScArtifaxInvite.md +262 -0
- package/dist/docs/ScArtifaxSidebar.md +330 -0
- package/dist/docs/ScAskAgentButton.md +307 -0
- package/dist/docs/ScBadges.md +261 -0
- package/dist/docs/ScBeacon.md +244 -0
- package/dist/docs/ScBillingHistoryHeader.md +210 -0
- package/dist/docs/ScBillingHistoryTableList.md +243 -0
- package/dist/docs/ScBillingLogsTableHeader.md +212 -0
- package/dist/docs/ScBillingLogsTableList.md +251 -0
- package/dist/docs/ScBriefCard.md +255 -0
- package/dist/docs/ScButton.md +251 -0
- package/dist/docs/ScCalendar.md +268 -0
- package/dist/docs/ScCalendarDateComps.md +264 -0
- package/dist/docs/ScCatalogixInvite.md +345 -0
- package/dist/docs/ScCatalogixSidebar.md +337 -0
- package/dist/docs/ScCatalogixStoreHeader.md +246 -0
- package/dist/docs/ScCatalogixStoreTableList.md +316 -0
- package/dist/docs/ScCheckField.md +233 -0
- package/dist/docs/ScCheckbox.md +272 -0
- package/dist/docs/ScCounter.md +235 -0
- package/dist/docs/ScCreditsUsageCard.md +247 -0
- package/dist/docs/ScCreditsUsageCardMobile.md +224 -0
- package/dist/docs/ScDefaultCard.md +269 -0
- package/dist/docs/ScDp.md +245 -0
- package/dist/docs/ScDrawer.md +318 -0
- package/dist/docs/ScFieldButton.md +255 -0
- package/dist/docs/ScFileField.md +268 -0
- package/dist/docs/ScGoogleSignIn.md +250 -0
- package/dist/docs/ScGuide.md +278 -0
- package/dist/docs/ScHDivider.md +213 -0
- package/dist/docs/ScHeader.md +222 -0
- package/dist/docs/ScImageField.md +253 -0
- package/dist/docs/ScInChatList.md +277 -0
- package/dist/docs/ScInChatMessage.md +205 -0
- package/dist/docs/ScInfoPopup.md +248 -0
- package/dist/docs/ScIntialProfileCover.md +233 -0
- package/dist/docs/ScInvoiceHistoryMobile.md +187 -0
- package/dist/docs/ScLogoUnit.md +232 -0
- package/dist/docs/ScMappingCard.md +241 -0
- package/dist/docs/ScMediaApproval.md +301 -0
- package/dist/docs/ScMediaSelect.md +310 -0
- package/dist/docs/ScMenuOptions.md +308 -0
- package/dist/docs/ScMobileBottomAction.md +252 -0
- package/dist/docs/ScMobileTopNav.md +279 -0
- package/dist/docs/ScModal.md +291 -0
- package/dist/docs/ScOnlyField.md +302 -0
- package/dist/docs/ScOnlyIcon.md +213 -0
- package/dist/docs/ScPagination.md +284 -0
- package/dist/docs/ScPairtext.md +287 -0
- package/dist/docs/ScPendingAction.md +238 -0
- package/dist/docs/ScPhtogenixInvite.md +275 -0
- package/dist/docs/ScPlanCard.md +302 -0
- package/dist/docs/ScPlanComparison.md +264 -0
- package/dist/docs/ScPlanDetailsCard.md +246 -0
- package/dist/docs/ScPlanDetailsCardMobile.md +240 -0
- package/dist/docs/ScPopUpMenu.md +224 -0
- package/dist/docs/ScProfile.md +234 -0
- package/dist/docs/ScProfileImageUpdate.md +261 -0
- package/dist/docs/ScProfileOptions.md +245 -0
- package/dist/docs/ScProfilePopup.md +396 -0
- package/dist/docs/ScProfileSettingsComp.md +250 -0
- package/dist/docs/ScProfileV2Mobile.md +216 -0
- package/dist/docs/ScProgressBar.md +267 -0
- package/dist/docs/ScQuickPrompt.md +277 -0
- package/dist/docs/ScRadio.md +228 -0
- package/dist/docs/ScReferralCardMobile.md +226 -0
- package/dist/docs/ScReferralTableHeader.md +260 -0
- package/dist/docs/ScReferralTableList.md +293 -0
- package/dist/docs/ScRole.md +226 -0
- package/dist/docs/ScRoleMobile.md +199 -0
- package/dist/docs/ScSelect.md +270 -0
- package/dist/docs/ScSelection.md +256 -0
- package/dist/docs/ScSelectionList.md +272 -0
- package/dist/docs/ScSelectionPill.md +240 -0
- package/dist/docs/ScSelectionPillGroup.md +302 -0
- package/dist/docs/ScSettingsNav.md +212 -0
- package/dist/docs/ScSettingsTabComp.md +260 -0
- package/dist/docs/ScSideBarLogoUnit.md +340 -0
- package/dist/docs/ScSidebar.md +243 -0
- package/dist/docs/ScSidebarIcons.md +232 -0
- package/dist/docs/ScSidebarMenu.md +283 -0
- package/dist/docs/ScSidebarProfile.md +231 -0
- package/dist/docs/ScSidebarSwitchMenu.md +258 -0
- package/dist/docs/ScSlider.md +194 -0
- package/dist/docs/ScStoreCard.md +252 -0
- package/dist/docs/ScStrLogo.md +253 -0
- package/dist/docs/ScStreamoidWordmark.md +302 -0
- package/dist/docs/ScSubAgent.md +226 -0
- package/dist/docs/ScTabComp.md +308 -0
- package/dist/docs/ScTabField.md +258 -0
- package/dist/docs/ScTabSwitcher.md +307 -0
- package/dist/docs/ScTableHeader.md +261 -0
- package/dist/docs/ScTableList.md +301 -0
- package/dist/docs/ScTableListMobile.md +282 -0
- package/dist/docs/ScTabs.md +268 -0
- package/dist/docs/ScTaxonomyPill.md +263 -0
- package/dist/docs/ScTextArea.md +259 -0
- package/dist/docs/ScTextField.md +324 -0
- package/dist/docs/ScThinkingStepIcon.md +249 -0
- package/dist/docs/ScTodoList.md +288 -0
- package/dist/docs/ScToggleSwitch.md +229 -0
- package/dist/docs/ScUsageHistoryMobile.md +194 -0
- package/dist/docs/ScVDivider.md +215 -0
- package/dist/docs/ScValueMappingL1.md +256 -0
- package/dist/docs/ScVersion.md +251 -0
- package/dist/docs/ScWorkspace.md +233 -0
- package/dist/docs/ScWorkspaceCard.md +234 -0
- package/dist/docs/ScWorkspaceSettingsMobile.md +265 -0
- package/dist/docs/ScWorkspaceSwitchCard.md +314 -0
- package/dist/docs/ScWorkspaceSwitchMobile.md +241 -0
- package/dist/docs/ScWorkspaceSwitchMobileV2.md +278 -0
- package/dist/docs/StreamoidSidebar.md +403 -0
- package/dist/docs/StreamoidWorkspaceSwitcher.md +307 -0
- package/dist/docs/UsageHistoryMobile.md +235 -0
- package/dist/docs/components.json +4849 -0
- package/dist/index.css +43 -37
- package/dist/index.d.mts +10 -0
- package/dist/index.d.ts +10 -0
- package/package.json +3 -2
|
@@ -0,0 +1,307 @@
|
|
|
1
|
+
---
|
|
2
|
+
component: ScTabSwitcher
|
|
3
|
+
package: "@streamoid/ui"
|
|
4
|
+
category: layout
|
|
5
|
+
status: stable
|
|
6
|
+
renders: div
|
|
7
|
+
tags: [segmented, tab-switcher, toggle, view-switch, 2-tab, 3-tab, slots, container]
|
|
8
|
+
related: [ScTabComp, ScTabField, ScTabs, ScSettingsTabComp, ScSelectionPillGroup]
|
|
9
|
+
do_not_confuse_with: [ScTabs, ScTabComp, ScTabField, ScSettingsTabComp, ScSelectionPillGroup]
|
|
10
|
+
used_by: [cxo, photogenix, catalogix, artifax]
|
|
11
|
+
---
|
|
12
|
+
|
|
13
|
+
# ScTabSwitcher
|
|
14
|
+
|
|
15
|
+
**The bordered shell around a 2–5 way segmented control.** It draws the rounded
|
|
16
|
+
`surface-base` track with a hairline border and 4px padding, then renders whatever you
|
|
17
|
+
put in its `component` … `component5` slots. It holds **no state**: each `ScTabComp`
|
|
18
|
+
child owns its own `active` and `onClick`.
|
|
19
|
+
|
|
20
|
+
## TL;DR for agents
|
|
21
|
+
|
|
22
|
+
- **Reach for it when:** you have a compact 2–5 way view toggle and want the standard
|
|
23
|
+
track: theme switcher, Active/Pending, grid/list, Fast/Pro.
|
|
24
|
+
- **Don't reach for it when:** the tab list is dynamic or longer than five
|
|
25
|
+
(→ `ScTabs`), it's a settings underline strip (→ `ScSettingsTabComp`), or it's a
|
|
26
|
+
labelled form field (→ `ScTabField`, which wraps this for you).
|
|
27
|
+
- **Four things that will bite you:**
|
|
28
|
+
1. **Empty slots are not empty.** A missing `component` renders a *default*
|
|
29
|
+
`ScTabComp` — a home icon, and the first one is `active="true"`.
|
|
30
|
+
2. `tabCount` is a **string** (`"2"`…`"5"`) and it **gates** the slots:
|
|
31
|
+
`component3` is dropped unless `tabCount` is `"3"`+.
|
|
32
|
+
3. Children you supply need **`style={{ flex: 1 }}`** or the tabs come out unequal.
|
|
33
|
+
4. `scTabCompicon` / `scTabCompactive` / `scTabComptype` are accepted by the type
|
|
34
|
+
and **completely ignored** — Figma leftovers.
|
|
35
|
+
|
|
36
|
+
---
|
|
37
|
+
|
|
38
|
+
## 1. How to use it
|
|
39
|
+
|
|
40
|
+
### Import
|
|
41
|
+
|
|
42
|
+
```tsx
|
|
43
|
+
import { ScTabSwitcher, ScTabComp } from "@streamoid/ui";
|
|
44
|
+
import "@streamoid/ui/dist/index.css"; // once, at your app root
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
### Minimal usage
|
|
48
|
+
|
|
49
|
+
```tsx
|
|
50
|
+
<ScTabSwitcher
|
|
51
|
+
tabCount="2"
|
|
52
|
+
component={
|
|
53
|
+
<ScTabComp text="Active" type="text-only"
|
|
54
|
+
active={tab === "active" ? "true" : "false"}
|
|
55
|
+
onClick={() => setTab("active")} style={{ flex: 1 }} />
|
|
56
|
+
}
|
|
57
|
+
component2={
|
|
58
|
+
<ScTabComp text="Pending" type="text-only"
|
|
59
|
+
active={tab === "pending" ? "true" : "false"}
|
|
60
|
+
onClick={() => setTab("pending")} style={{ flex: 1 }} />
|
|
61
|
+
}
|
|
62
|
+
/>
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
### Props
|
|
66
|
+
|
|
67
|
+
| Prop | Type | Default | Notes |
|
|
68
|
+
|---|---|---|---|
|
|
69
|
+
| `tabCount` | `"2"` \| `"3"` \| `"4"` \| `"5"` | `"2"` | ⚠️ A **string**. Controls how many slots render — slots above it are dropped. Applies **no styling** (see Gotcha 5). |
|
|
70
|
+
| `component` | `JSX.Element` | ⚠️ a default `ScTabComp` with `active="true"` and `SiconHome` | Slot 1. Always rendered. |
|
|
71
|
+
| `component2` | `JSX.Element` | ⚠️ a default `ScTabComp` with `SiconHome` | Slot 2. Always rendered. |
|
|
72
|
+
| `component3` | `JSX.Element` | ⚠️ a default `ScTabComp` with `SiconHome` | Slot 3. Rendered only when `tabCount` is `"3"`/`"4"`/`"5"`. |
|
|
73
|
+
| `component4` | `JSX.Element` | ⚠️ a default `ScTabComp` with `SiconHome` | Slot 4. Rendered only when `tabCount` is `"4"`/`"5"`. |
|
|
74
|
+
| `component5` | `JSX.Element` | ⚠️ a default `ScTabComp` with `SiconHome` | Slot 5. Rendered only when `tabCount` is `"5"`. |
|
|
75
|
+
| `scTabCompicon` | `JSX.Element` | – | **Ignored.** Destructured to `_scTabCompicon` and dropped. |
|
|
76
|
+
| `scTabCompactive` | `string` | – | **Ignored.** |
|
|
77
|
+
| `scTabComptype` | `string` | – | **Ignored.** |
|
|
78
|
+
| `className` | `string` | – | Appended after the internal classes. Hosts use it for `w-full`. |
|
|
79
|
+
| `...props` | `HTMLAttributes<HTMLDivElement>` | – | Spread onto the track div: `style`, `role="tablist"`, `aria-label`, `data-*`. |
|
|
80
|
+
|
|
81
|
+
### What renders for each `tabCount`
|
|
82
|
+
|
|
83
|
+
| `tabCount` | Slots rendered | Slots silently dropped |
|
|
84
|
+
|---|---|---|
|
|
85
|
+
| `"2"` (default) | `component`, `component2` | 3, 4, 5 |
|
|
86
|
+
| `"3"` | + `component3` | 4, 5 |
|
|
87
|
+
| `"4"` | + `component4` | 5 |
|
|
88
|
+
| `"5"` | all five | — |
|
|
89
|
+
|
|
90
|
+
### Recipes
|
|
91
|
+
|
|
92
|
+
```tsx
|
|
93
|
+
// Three icon tabs, full width (Photogenix / Artifax theme switcher)
|
|
94
|
+
<ScTabSwitcher
|
|
95
|
+
tabCount="3"
|
|
96
|
+
className="w-full"
|
|
97
|
+
role="tablist"
|
|
98
|
+
aria-label="Theme"
|
|
99
|
+
component={<ScTabComp type="icon-only" style={{ flex: 1 }}
|
|
100
|
+
active={theme === "system" ? "true" : "false"} onClick={() => setTheme("system")}
|
|
101
|
+
icon={<SiconSystem className="w-5 h-5"
|
|
102
|
+
color={theme === "system" ? "var(--alias-text---icons-inverse)" : "var(--alias-text---icons-muted)"} />} />}
|
|
103
|
+
component2={<ScTabComp type="icon-only" style={{ flex: 1 }}
|
|
104
|
+
active={theme === "light" ? "true" : "false"} onClick={() => setTheme("light")}
|
|
105
|
+
icon={<SiconLight className="w-5 h-5"
|
|
106
|
+
color={theme === "light" ? "var(--alias-text---icons-inverse)" : "var(--alias-text---icons-muted)"} />} />}
|
|
107
|
+
component3={<ScTabComp type="icon-only" style={{ flex: 1 }}
|
|
108
|
+
active={theme === "dark" ? "true" : "false"} onClick={() => setTheme("dark")}
|
|
109
|
+
icon={<SiconDark className="w-5 h-5"
|
|
110
|
+
color={theme === "dark" ? "var(--alias-text---icons-inverse)" : "var(--alias-text---icons-muted)"} />} />}
|
|
111
|
+
/>
|
|
112
|
+
|
|
113
|
+
// Shrink-wrapped in a header row (it stretches by default — wrap it)
|
|
114
|
+
<div className="inline-flex">
|
|
115
|
+
<ScTabSwitcher tabCount="2" component={…} component2={…} />
|
|
116
|
+
</div>
|
|
117
|
+
|
|
118
|
+
// Driving both slots from one array (do NOT exceed tabCount)
|
|
119
|
+
<ScTabSwitcher
|
|
120
|
+
tabCount="2"
|
|
121
|
+
component={renderTab(views[0])}
|
|
122
|
+
component2={renderTab(views[1])}
|
|
123
|
+
/>
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
---
|
|
127
|
+
|
|
128
|
+
## 2. Where to use it
|
|
129
|
+
|
|
130
|
+
- **Sidebar footers** — the light/dark/system theme switcher in Photogenix's
|
|
131
|
+
`Sidebar`, Artifax's `DashboardSidebar`, and the DS's own `ScProfilePopup` /
|
|
132
|
+
`ScProfileOptions`.
|
|
133
|
+
- **Table/list header rows** — CXO teams & organization (Active/Pending), Catalogix
|
|
134
|
+
PLP views.
|
|
135
|
+
- **Inspector panels** — Photogenix retouch/reference views, Catalogix's text
|
|
136
|
+
attribute editor.
|
|
137
|
+
- **Inside `ScTabField`** — the DS field wrapper composes one for you.
|
|
138
|
+
|
|
139
|
+
All four host apps use it; it's part of the universal core.
|
|
140
|
+
|
|
141
|
+
---
|
|
142
|
+
|
|
143
|
+
## 3. When to use it
|
|
144
|
+
|
|
145
|
+
### Use it when
|
|
146
|
+
|
|
147
|
+
- The control has **2–5 fixed** segments known at author time.
|
|
148
|
+
- You want the standard bordered track (radius `xl`, 0.5px `border-divider`,
|
|
149
|
+
`surface-base` fill) instead of hand-rolling it.
|
|
150
|
+
|
|
151
|
+
### Don't use it — reach for this instead
|
|
152
|
+
|
|
153
|
+
| Situation | Use instead |
|
|
154
|
+
|---|---|
|
|
155
|
+
| 6+ segments, or segments from data | `ScTabs` |
|
|
156
|
+
| A single tab cell on its own | `ScTabComp` standalone (no track) |
|
|
157
|
+
| A labelled segmented control inside a form | `ScTabField` — it wraps this + two `ScTabComp`s |
|
|
158
|
+
| Settings / mobile-settings underline strip | `ScSettingsTabComp` |
|
|
159
|
+
| Accessible segmented control (real buttons + `aria-pressed`) | `ScSelectionPillGroup` |
|
|
160
|
+
| Pills whose selected state is a dark fill (Catalogix curation look) | `ScSelectionPillGroup` |
|
|
161
|
+
| Two plain actions side by side | two `ScButton`s |
|
|
162
|
+
|
|
163
|
+
### Don't confuse with
|
|
164
|
+
|
|
165
|
+
| You may actually want | Not this |
|
|
166
|
+
|---|---|
|
|
167
|
+
| `ScTabs` — data-driven, unbounded, borderless pill row | `ScTabSwitcher` is a fixed 2–5 slot bordered track |
|
|
168
|
+
| `ScTabField` — already contains a switcher + a field label | Don't nest a switcher inside your own label markup |
|
|
169
|
+
| `ScSelectionPillGroup` — owns the active value and gives you `onChange` | `ScTabSwitcher` owns nothing; each child tracks itself |
|
|
170
|
+
| `ScSettingsTabComp` — underline tabs, no track | different visual family entirely |
|
|
171
|
+
|
|
172
|
+
### The tab family at a glance
|
|
173
|
+
|
|
174
|
+
| Component | Shape | Selection API | Root | Spreads DOM props? |
|
|
175
|
+
|---|---|---|---|---|
|
|
176
|
+
| `ScTabSwitcher` | 2–5 slot bordered track | none — children own it | `div` | ✅ |
|
|
177
|
+
| `ScTabComp` | one tab cell | `active="true" \| "false"` (strings) | `div` | ✅ |
|
|
178
|
+
| `ScTabs` | N pills, data-driven | `activeTab` + `onClickTab(value)` | `div` | ❌ |
|
|
179
|
+
| `ScTabField` | field label + 2-tab switcher | `activeTab` + `onTabChange` | `div` | ❌ |
|
|
180
|
+
| `ScSettingsTabComp` | underline tab | `active` (boolean) | `div` | ❌ |
|
|
181
|
+
| `ScSelectionPillGroup` | segmented pills | `value` + `onChange` | `div` of `button`s | ✅ |
|
|
182
|
+
|
|
183
|
+
---
|
|
184
|
+
|
|
185
|
+
## 4. Why to use it
|
|
186
|
+
|
|
187
|
+
- **The track is the part everyone gets wrong.** Radius `--radius-xl`, 0.5px
|
|
188
|
+
`--alias-border-divider`, `--alias-surface-base` fill, `--spacing-xs` padding,
|
|
189
|
+
zero gap — five values that must match the theme switcher in four apps.
|
|
190
|
+
- **Slots, not data.** Because each segment is a full element, one segment can carry a
|
|
191
|
+
tooltip wrapper, a different icon set, or a disabled state without the container
|
|
192
|
+
needing to know — which is why Photogenix can wrap each tab in its own `Tooltip`.
|
|
193
|
+
- **Stateless by design.** Selection stays in your store; there is no internal state to
|
|
194
|
+
desync from a URL param or a persisted theme.
|
|
195
|
+
- **`align-self: stretch` + `flex-shrink: 0`** means it fills a sidebar column without
|
|
196
|
+
width plumbing and won't collapse in a constrained flex parent.
|
|
197
|
+
|
|
198
|
+
---
|
|
199
|
+
|
|
200
|
+
## Gotchas
|
|
201
|
+
|
|
202
|
+
**1. Missing slots render placeholder tabs.** `component`…`component5` each fall back
|
|
203
|
+
to a default `ScTabComp` with `SiconHome`; slot 1's fallback is also `active="true"`.
|
|
204
|
+
|
|
205
|
+
```tsx
|
|
206
|
+
// WRONG — renders your tab plus a second, home-icon "Tab"
|
|
207
|
+
<ScTabSwitcher tabCount="2" component={<ScTabComp text="Active" style={{ flex: 1 }} />} />
|
|
208
|
+
|
|
209
|
+
// RIGHT — fill every slot up to tabCount
|
|
210
|
+
<ScTabSwitcher
|
|
211
|
+
tabCount="2"
|
|
212
|
+
component={<ScTabComp text="Active" type="text-only" style={{ flex: 1 }} />}
|
|
213
|
+
component2={<ScTabComp text="Pending" type="text-only" style={{ flex: 1 }} />}
|
|
214
|
+
/>
|
|
215
|
+
```
|
|
216
|
+
|
|
217
|
+
**2. `tabCount` gates the slots — and it's a string.**
|
|
218
|
+
|
|
219
|
+
```tsx
|
|
220
|
+
// WRONG — component3 never renders (tabCount defaults to "2")
|
|
221
|
+
<ScTabSwitcher component={a} component2={b} component3={c} />
|
|
222
|
+
|
|
223
|
+
// ALSO WRONG — numeric literal is a type error
|
|
224
|
+
<ScTabSwitcher tabCount={3} … />
|
|
225
|
+
|
|
226
|
+
// RIGHT
|
|
227
|
+
<ScTabSwitcher tabCount="3" component={a} component2={b} component3={c} />
|
|
228
|
+
```
|
|
229
|
+
|
|
230
|
+
**3. Custom children need `style={{ flex: 1 }}`.** The `flex: 1; align-self: unset`
|
|
231
|
+
override is applied through an internal class that only the *default* children get.
|
|
232
|
+
Without it your tabs size to their content and the track looks lopsided. Every host
|
|
233
|
+
call site passes it.
|
|
234
|
+
|
|
235
|
+
**4. Three props are dead.** `scTabCompicon`, `scTabCompactive`, `scTabComptype` exist
|
|
236
|
+
only so old Figma-generated call sites still typecheck. They are destructured into
|
|
237
|
+
`_`-prefixed locals and never used. Do not try to configure children through them.
|
|
238
|
+
|
|
239
|
+
**5. `tabCount` applies no CSS.** The component computes
|
|
240
|
+
`styles["tab-count-" + tabCount]`, but the stylesheet defines no `.tab-count-*` rules —
|
|
241
|
+
so the class resolves to `undefined` and lands in the class string as the literal text
|
|
242
|
+
`"undefined"`. `tabCount` is purely a render-count switch.
|
|
243
|
+
|
|
244
|
+
**6. It stretches.** `align-self: stretch` on the root means a flex-column parent makes
|
|
245
|
+
it full width. Wrap it in an `inline-flex` container when it should shrink-wrap
|
|
246
|
+
(Photogenix's `QualityToggle` does exactly that).
|
|
247
|
+
|
|
248
|
+
**7. No `role`, no keyboard, no roving tabindex.** The track is a plain div and the
|
|
249
|
+
children are divs. Add `role="tablist"` here and `role="tab"`/`aria-selected` on the
|
|
250
|
+
children if the control needs to be accessible — props are spread on both, so it
|
|
251
|
+
compiles.
|
|
252
|
+
|
|
253
|
+
**8. `className` is concatenated unguarded**; omitting it puts `"undefined"` in the
|
|
254
|
+
class list.
|
|
255
|
+
|
|
256
|
+
**9. `ScTabField` only wires slots 1 and 2.** If you pass it three tabs it sets
|
|
257
|
+
`tabCount="3"` but leaves `component3` unset — so the third tab renders as the home-icon
|
|
258
|
+
placeholder. Use `ScTabSwitcher` directly for 3+.
|
|
259
|
+
|
|
260
|
+
---
|
|
261
|
+
|
|
262
|
+
## In the wild
|
|
263
|
+
|
|
264
|
+
```tsx
|
|
265
|
+
// cxo-dashboard src/app/components/teams-content.tsx:1171
|
|
266
|
+
<ScTabSwitcher
|
|
267
|
+
tabCount="2"
|
|
268
|
+
component={
|
|
269
|
+
<ScTabComp
|
|
270
|
+
text="Active"
|
|
271
|
+
type="text-only"
|
|
272
|
+
active={activeTab === "active" ? "true" : "false"}
|
|
273
|
+
onClick={() => setActiveTab("active")}
|
|
274
|
+
style={{ flex: 1 }}
|
|
275
|
+
/>
|
|
276
|
+
}
|
|
277
|
+
component2={
|
|
278
|
+
<ScTabComp
|
|
279
|
+
text="Pending"
|
|
280
|
+
type="text-only"
|
|
281
|
+
active={activeTab === "pending" ? "true" : "false"}
|
|
282
|
+
onClick={() => setActiveTab("pending")}
|
|
283
|
+
style={{ flex: 1 }}
|
|
284
|
+
/>
|
|
285
|
+
}
|
|
286
|
+
/>
|
|
287
|
+
|
|
288
|
+
// photogenix_v2 dashboard/client/src/components/layout/Sidebar.tsx:997
|
|
289
|
+
<ScTabSwitcher tabCount="3" className="w-full"
|
|
290
|
+
component={<ScTabComp icon={<SiconSystem className="w-5 h-5" … />} active={…} type="icon-only" onClick={() => setTheme("system")} style={{ flex: 1 }} />}
|
|
291
|
+
component2={…}
|
|
292
|
+
component3={…}
|
|
293
|
+
/>
|
|
294
|
+
```
|
|
295
|
+
|
|
296
|
+
---
|
|
297
|
+
|
|
298
|
+
## Related
|
|
299
|
+
|
|
300
|
+
- `ScTabComp` — the tab cell that goes in the slots; read its README for the
|
|
301
|
+
`active="true"` string trap.
|
|
302
|
+
- `ScTabField` — labelled form field that composes this with two `ScTabComp`s.
|
|
303
|
+
- `ScTabs` — data-driven N-tab pill bar for 6+ or dynamic tabs.
|
|
304
|
+
- `ScSettingsTabComp` — underline tab strip for settings screens.
|
|
305
|
+
- `ScSelectionPillGroup` — accessible segmented control that owns its value.
|
|
306
|
+
- `ScProfilePopup` / `ScProfileOptions` — DS components that already contain a
|
|
307
|
+
theme switcher built from this.
|
|
@@ -0,0 +1,261 @@
|
|
|
1
|
+
---
|
|
2
|
+
component: ScTableHeader
|
|
3
|
+
package: "@streamoid/ui"
|
|
4
|
+
category: tables
|
|
5
|
+
status: stable
|
|
6
|
+
renders: div
|
|
7
|
+
tags: [table, header, column-headers, members, team, users, invites, pending, settings]
|
|
8
|
+
related: [ScTableList, ScHeader, ScTableListMobile, ScCatalogixStoreHeader, ScBillingHistoryHeader, ScBillingLogsTableHeader, ScReferralTableHeader]
|
|
9
|
+
do_not_confuse_with: [ScHeader, ScReferralTableHeader, ScCatalogixStoreHeader, ScBillingHistoryHeader, ScBillingLogsTableHeader, ScMobileTopNav, ScTableList]
|
|
10
|
+
used_by: [cxo]
|
|
11
|
+
---
|
|
12
|
+
|
|
13
|
+
# ScTableHeader
|
|
14
|
+
|
|
15
|
+
**The column-header strip of the Teams / members table.** A shaded flex row of five
|
|
16
|
+
uppercase muted labels — USER, ROLE, ACCESS, INVITED BY, then either SIGNED ON or
|
|
17
|
+
INVITE ACTION depending on `type` — rendered as five internal `ScHeader` cells on the
|
|
18
|
+
exact widths the member-row markup uses. It is the *ruler*; the rows are the data.
|
|
19
|
+
|
|
20
|
+
## TL;DR for agents
|
|
21
|
+
|
|
22
|
+
- **Reach for it when:** you are building the Teams / members / pending-invites table
|
|
23
|
+
and need the header strip above the rows.
|
|
24
|
+
- **Don't reach for it when:** you need a *different* table's header
|
|
25
|
+
(→ `ScCatalogixStoreHeader`, `ScBillingHistoryHeader`, `ScBillingLogsTableHeader`,
|
|
26
|
+
`ScReferralTableHeader`), a single sortable column label (→ `ScHeader`), or the
|
|
27
|
+
mobile members list, which has no header at all (→ `ScTableListMobile`).
|
|
28
|
+
- **Four things that will bite you:**
|
|
29
|
+
1. **Every column label is hardcoded English.** There is no label prop. `type` is
|
|
30
|
+
the *only* prop besides `className`.
|
|
31
|
+
2. **`...props` is destructured but never spread.** `style`, `onClick`, `data-*`,
|
|
32
|
+
`aria-*` are silently dropped (and are type errors — the interface does not
|
|
33
|
+
extend `HTMLAttributes`). Wrap it in your own div instead.
|
|
34
|
+
3. The USER column shows a **descending chevron that is pure decoration** —
|
|
35
|
+
nothing is sortable, nothing is clickable.
|
|
36
|
+
4. It is **not a `<table>`**. No `role="table"`, no `columnheader`, no `aria-sort`.
|
|
37
|
+
|
|
38
|
+
---
|
|
39
|
+
|
|
40
|
+
## 1. How to use it
|
|
41
|
+
|
|
42
|
+
### Import
|
|
43
|
+
|
|
44
|
+
```tsx
|
|
45
|
+
import { ScTableHeader } from "@streamoid/ui";
|
|
46
|
+
import "@streamoid/ui/dist/index.css"; // once, at your app root
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
### Minimal usage
|
|
50
|
+
|
|
51
|
+
```tsx
|
|
52
|
+
<ScTableHeader />
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
### Props
|
|
56
|
+
|
|
57
|
+
| Prop | Type | Default | Notes |
|
|
58
|
+
|---|---|---|---|
|
|
59
|
+
| `type` | `"default"` \| `"pending"` | `"default"` | Picks the fifth column: `default` → `SIGNED ON` (7.5rem), `pending` → `INVITE ACTION` (10rem). Nothing else changes. |
|
|
60
|
+
| `className` | `string` | – | Appended after the internal class. The only escape hatch — there is no `style` prop. |
|
|
61
|
+
|
|
62
|
+
That is the entire API. ⚠️ `...props` exists in the signature but is **never applied
|
|
63
|
+
to the DOM**, and `IScTableHeaderProps` does not extend `React.HTMLAttributes`, so
|
|
64
|
+
anything else you pass is both a compile error and a no-op.
|
|
65
|
+
|
|
66
|
+
### What renders in each `type`
|
|
67
|
+
|
|
68
|
+
| Column | `type="default"` | `type="pending"` | Width |
|
|
69
|
+
|---|---|---|---|
|
|
70
|
+
| USER | ✅ (with a down chevron) | ✅ (with a down chevron) | `flex: 1` |
|
|
71
|
+
| ROLE | ✅ | ✅ | `5rem` |
|
|
72
|
+
| ACCESS | ✅ | ✅ | `11.25rem` |
|
|
73
|
+
| INVITED BY | ✅ | ✅ | `7.5rem` |
|
|
74
|
+
| SIGNED ON | ✅ | — | `7.5rem` |
|
|
75
|
+
| INVITE ACTION | — | ✅ | `10rem` |
|
|
76
|
+
|
|
77
|
+
### The column contract (shared with the row markup)
|
|
78
|
+
|
|
79
|
+
`ScTableList` — and every host's hand-rolled equivalent — matches these exactly:
|
|
80
|
+
|
|
81
|
+
| Column | Header width | Row element | Row width |
|
|
82
|
+
|---|---|---|---|
|
|
83
|
+
| USER | `flex: 1` | `ScProfile` | `flex: 1` |
|
|
84
|
+
| ROLE | `5rem` | `ScRole` | `5rem` (fixed inside `ScRole`) |
|
|
85
|
+
| ACCESS | `11.25rem` | the access-icon group wrapper | `11.25rem` |
|
|
86
|
+
| INVITED BY | `7.5rem` | a truncating text cell | `7.5rem` |
|
|
87
|
+
| SIGNED ON | `7.5rem` | a truncating text cell | `7.5rem` |
|
|
88
|
+
| INVITE ACTION | `10rem` | `ScPendingAction` | `10rem` (fixed inside `ScPendingAction`) |
|
|
89
|
+
|
|
90
|
+
Both sides use `gap: 1.5rem` and `padding-inline: 1rem`. That, plus the widths above,
|
|
91
|
+
is what makes header and rows line up. Change neither via `className` unless you
|
|
92
|
+
change both.
|
|
93
|
+
|
|
94
|
+
### Recipes
|
|
95
|
+
|
|
96
|
+
```tsx
|
|
97
|
+
// Tabbed Teams table — the tab decides the fifth column, header and rows together
|
|
98
|
+
<ScTableHeader type={activeTab === "active" ? "default" : "pending"} />
|
|
99
|
+
{members.map((m) => (
|
|
100
|
+
<ScTableList
|
|
101
|
+
key={m.id}
|
|
102
|
+
variant={activeTab} // must track the header's `type`
|
|
103
|
+
userEmail={m.userEmail}
|
|
104
|
+
invitedBy={m.invitedBy}
|
|
105
|
+
signedOn={m.signedOn}
|
|
106
|
+
/>
|
|
107
|
+
))}
|
|
108
|
+
// ...or your own row on the widths below — which is what both hosts actually do,
|
|
109
|
+
// because ScTableList hardcodes name/role/access. See SC-tableList/README.md.
|
|
110
|
+
|
|
111
|
+
// Make it sticky. You cannot pass `style`, so use className (or wrap it).
|
|
112
|
+
<ScTableHeader className={styles.stickyHead} />
|
|
113
|
+
/* .stickyHead { position: sticky; top: 0; z-index: 1; background: var(--alias-surface-base); } */
|
|
114
|
+
|
|
115
|
+
// The wrapper form — what @streamoid/settings actually does, because `style` is dropped
|
|
116
|
+
// (CXO's own teams-content.tsx renders it unwrapped, so its header scrolls away)
|
|
117
|
+
<div style={{ position: "sticky", top: 0, zIndex: 1, backgroundColor: "var(--alias-surface-base)" }}>
|
|
118
|
+
<ScTableHeader type="pending" />
|
|
119
|
+
</div>
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
---
|
|
123
|
+
|
|
124
|
+
## 2. Where to use it
|
|
125
|
+
|
|
126
|
+
- **Settings → Teams**, desktop only, on both tabs: Active members (`type="default"`)
|
|
127
|
+
and Pending invitations (`type="pending"`). This is the reason it exists.
|
|
128
|
+
- Rendered directly by CXO's `teams-content.tsx`, and by the shared
|
|
129
|
+
`@streamoid/settings` Teams screen that ships into CXO's desktop settings.
|
|
130
|
+
- Nothing else should use it: the column set *is* the workspace-member schema, baked
|
|
131
|
+
into the JSX with no label props.
|
|
132
|
+
|
|
133
|
+
It composes five `ScHeader` cells and a background. Nothing else.
|
|
134
|
+
|
|
135
|
+
---
|
|
136
|
+
|
|
137
|
+
## 3. When to use it
|
|
138
|
+
|
|
139
|
+
### Use it when
|
|
140
|
+
|
|
141
|
+
- You are rendering **workspace members or pending invites** in a desktop table and
|
|
142
|
+
want the header pinned to the same widths the DS row uses.
|
|
143
|
+
- You want the "shaded header strip over transparent rows" treatment shared by every
|
|
144
|
+
table in the products.
|
|
145
|
+
|
|
146
|
+
### Don't use it — reach for this instead
|
|
147
|
+
|
|
148
|
+
| Situation | Use instead |
|
|
149
|
+
|---|---|
|
|
150
|
+
| Catalogix stores list header | `ScCatalogixStoreHeader` (and it takes label props, unlike this) |
|
|
151
|
+
| Billing history table header (Invoice / Amount / Date / Action) | `ScBillingHistoryHeader` |
|
|
152
|
+
| Credit & usage logs header (App / Activity / Used by / Date / Credits used / Balance) | `ScBillingLogsTableHeader` |
|
|
153
|
+
| Referral history header (User / Date / Status / Reward) | `ScReferralTableHeader` |
|
|
154
|
+
| One sortable column label with an asc/desc chevron | `ScHeader` — this strip is five of them, pre-labelled |
|
|
155
|
+
| A genuinely generic N-column table header | Compose `ScHeader` cells yourself; the labels and widths here are hard-wired |
|
|
156
|
+
| The mobile members list | `ScTableListMobile` — the mobile pattern is stacked cards with **no header row** |
|
|
157
|
+
| A page title bar with actions | Neither this nor `ScHeader`; use your page layout + `ScButton` |
|
|
158
|
+
|
|
159
|
+
### Don't confuse with
|
|
160
|
+
|
|
161
|
+
| You may actually want | Not this |
|
|
162
|
+
|---|---|
|
|
163
|
+
| `ScHeader` — a **single** column-label cell, `state="up" \| "down" \| "none"`, sort chevron | `ScTableHeader` is the whole strip and hardcodes the five labels |
|
|
164
|
+
| `ScReferralTableHeader` — same shape, **referral** columns, no `type` prop at all | This one is the members table |
|
|
165
|
+
| `ScCatalogixStoreHeader` — same shape but every label is a **prop**, plus `showAssets`/`showCreatedBy` gating | This one has no label props |
|
|
166
|
+
| `ScTableList` — the *row* sibling, not the header | Different component; see its README before using it |
|
|
167
|
+
| `ScMobileTopNav` — the mobile app bar | Unrelated; "header" here means column headers |
|
|
168
|
+
|
|
169
|
+
---
|
|
170
|
+
|
|
171
|
+
## 4. Why to use it
|
|
172
|
+
|
|
173
|
+
- **It is the alignment contract.** The widths (`flex:1`, `5`, `11.25`, `7.5`, then
|
|
174
|
+
`7.5` or `10` rem), the `1.5rem` gap and the `1rem` inline padding exist in this
|
|
175
|
+
file and in the row file. Hand-rolling the header is how member tables shear apart
|
|
176
|
+
when someone adds a column.
|
|
177
|
+
- **The `type` switch is the pending/active difference in one prop** — you don't
|
|
178
|
+
duplicate a five-column strip per tab.
|
|
179
|
+
- **Two-tone that survives light mode.** The strip paints
|
|
180
|
+
`--alias-fill-neutral-neutral` while rows stay transparent, so it still reads as a
|
|
181
|
+
header when the theme flips, instead of relying on a grey that collapses to white.
|
|
182
|
+
- **Label typography is already the DS's** `font-size-xs` + `--alias-text-and-icons-muted`
|
|
183
|
+
+ uppercase, matching every other table header in the products.
|
|
184
|
+
|
|
185
|
+
---
|
|
186
|
+
|
|
187
|
+
## Gotchas
|
|
188
|
+
|
|
189
|
+
**1. There are no label props.** "User", "Role", "Access", "Invited by", "Signed On"
|
|
190
|
+
and "Invite action" are literal strings inside the component. Do not use it on a
|
|
191
|
+
localised surface without changing the DS first.
|
|
192
|
+
|
|
193
|
+
**2. `...props` is destructured and thrown away.** The root div receives only
|
|
194
|
+
`className`. `style`, `onClick`, `id`, `data-*` and `aria-*` never reach the DOM.
|
|
195
|
+
|
|
196
|
+
```tsx
|
|
197
|
+
// WRONG — type error, and even with a cast the style is dropped
|
|
198
|
+
<ScTableHeader type="pending" style={{ position: "sticky", top: 0 }} />
|
|
199
|
+
|
|
200
|
+
// RIGHT — className, or your own wrapper
|
|
201
|
+
<div style={{ position: "sticky", top: 0, zIndex: 1 }}>
|
|
202
|
+
<ScTableHeader type="pending" />
|
|
203
|
+
</div>
|
|
204
|
+
```
|
|
205
|
+
|
|
206
|
+
**3. The USER chevron is decoration.** The first cell is `<ScHeader state="down">`, so
|
|
207
|
+
it renders a double-down arrow that *looks* like "sorted descending". No `onClick` is
|
|
208
|
+
passed to any cell and there is no sort prop anywhere. Either wire sorting outside the
|
|
209
|
+
component or expect users to click a dead chevron.
|
|
210
|
+
|
|
211
|
+
**4. It is not a table.** No `role="table"`/`row`/`columnheader`, no `<th>`, no
|
|
212
|
+
`scope`, no `aria-sort`. Row cells are not associated with these labels for assistive
|
|
213
|
+
tech. If accessibility matters on the screen, add roles on your own wrapper and rows.
|
|
214
|
+
|
|
215
|
+
**5. Not sticky, and it doesn't own the scroll.** Long member lists scroll the header
|
|
216
|
+
away. Add `position: sticky; top: 0` **plus** an opaque `background` and a `z-index`
|
|
217
|
+
(the rows are `position: relative`, so without one they paint over it).
|
|
218
|
+
|
|
219
|
+
**6. `className` lands as the literal string `"undefined"` when omitted.** The class
|
|
220
|
+
list is built by string concatenation (`scTableHeader undefined type-default`).
|
|
221
|
+
Harmless in the browser, but exact-string class assertions in tests will fail.
|
|
222
|
+
|
|
223
|
+
**7. `type` must be exactly `"default"` or `"pending"`.** The variant class is
|
|
224
|
+
`styles["type-" + type]`; an unknown value yields the class `undefined` and the base
|
|
225
|
+
widths, so you get the four common columns and no fifth one, silently.
|
|
226
|
+
|
|
227
|
+
**8. `flex-shrink: 0` + `align-self: stretch`.** It expects to be a direct child of a
|
|
228
|
+
`flex-direction: column` container that is at least as wide as
|
|
229
|
+
`flex:1 + 5 + 11.25 + 7.5 + 7.5rem + gaps`. Below that the flexible USER column
|
|
230
|
+
collapses first; the fixed columns never shrink and will overflow the container
|
|
231
|
+
horizontally.
|
|
232
|
+
|
|
233
|
+
---
|
|
234
|
+
|
|
235
|
+
## In the wild
|
|
236
|
+
|
|
237
|
+
```tsx
|
|
238
|
+
// cxo-dashboard src/app/components/teams-content.tsx:1225
|
|
239
|
+
<ScTableHeader
|
|
240
|
+
type={activeTab === "active" ? "default" : "pending"}
|
|
241
|
+
/>
|
|
242
|
+
```
|
|
243
|
+
|
|
244
|
+
The shared settings package does the same at
|
|
245
|
+
`npm-components packages/settings/src/teams-content.tsx:1149`, there wrapped in a
|
|
246
|
+
`position: sticky` div (Gotcha 5 / Gotcha 2). In both places the rows underneath are a
|
|
247
|
+
local `TeamRow`, **not** `ScTableList` — see `SC-tableList/README.md` for why.
|
|
248
|
+
|
|
249
|
+
---
|
|
250
|
+
|
|
251
|
+
## Related
|
|
252
|
+
|
|
253
|
+
- `ScTableList` — the DS row sibling for this header (read its README first; most of
|
|
254
|
+
its content is hardcoded and hosts hand-roll the row instead).
|
|
255
|
+
- `ScHeader` — the single column-label cell this strip is built from; use it to
|
|
256
|
+
compose a header for a table the DS doesn't cover.
|
|
257
|
+
- `ScRole`, `ScAccess`, `ScProfile`, `ScPendingAction` — the per-cell pieces the row
|
|
258
|
+
side of this contract is made of.
|
|
259
|
+
- `ScTableListMobile` — the mobile members list; stacked cards, no header strip.
|
|
260
|
+
- `ScReferralTableHeader` / `ScBillingHistoryHeader` / `ScBillingLogsTableHeader` /
|
|
261
|
+
`ScCatalogixStoreHeader` — the other domain-specific header/row pairs, same pattern.
|