@streamoid/ui 0.6.17 → 0.6.18
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +35 -18
- package/dist/docs/AGENTS.md +321 -0
- package/dist/docs/CreditWarningBanner.md +305 -0
- package/dist/docs/InvoiceHistoryMobile.md +222 -0
- package/dist/docs/ScAccess.md +259 -0
- package/dist/docs/ScAppCard.md +244 -0
- package/dist/docs/ScAppCardForCopilot.md +230 -0
- package/dist/docs/ScAppCardV3.md +273 -0
- package/dist/docs/ScAppField.md +308 -0
- package/dist/docs/ScAppListingCard.md +271 -0
- package/dist/docs/ScAppSwitchPanel.md +286 -0
- package/dist/docs/ScAppcardLogos.md +226 -0
- package/dist/docs/ScArtifaxInvite.md +262 -0
- package/dist/docs/ScArtifaxSidebar.md +330 -0
- package/dist/docs/ScAskAgentButton.md +307 -0
- package/dist/docs/ScBadges.md +261 -0
- package/dist/docs/ScBeacon.md +244 -0
- package/dist/docs/ScBillingHistoryHeader.md +210 -0
- package/dist/docs/ScBillingHistoryTableList.md +243 -0
- package/dist/docs/ScBillingLogsTableHeader.md +212 -0
- package/dist/docs/ScBillingLogsTableList.md +251 -0
- package/dist/docs/ScBriefCard.md +255 -0
- package/dist/docs/ScButton.md +251 -0
- package/dist/docs/ScCalendar.md +268 -0
- package/dist/docs/ScCalendarDateComps.md +264 -0
- package/dist/docs/ScCatalogixInvite.md +345 -0
- package/dist/docs/ScCatalogixSidebar.md +337 -0
- package/dist/docs/ScCatalogixStoreHeader.md +246 -0
- package/dist/docs/ScCatalogixStoreTableList.md +316 -0
- package/dist/docs/ScCheckField.md +233 -0
- package/dist/docs/ScCheckbox.md +272 -0
- package/dist/docs/ScCounter.md +235 -0
- package/dist/docs/ScCreditsUsageCard.md +247 -0
- package/dist/docs/ScCreditsUsageCardMobile.md +224 -0
- package/dist/docs/ScDefaultCard.md +269 -0
- package/dist/docs/ScDp.md +245 -0
- package/dist/docs/ScDrawer.md +318 -0
- package/dist/docs/ScFieldButton.md +255 -0
- package/dist/docs/ScFileField.md +268 -0
- package/dist/docs/ScGoogleSignIn.md +250 -0
- package/dist/docs/ScGuide.md +278 -0
- package/dist/docs/ScHDivider.md +213 -0
- package/dist/docs/ScHeader.md +222 -0
- package/dist/docs/ScImageField.md +253 -0
- package/dist/docs/ScInChatList.md +277 -0
- package/dist/docs/ScInChatMessage.md +205 -0
- package/dist/docs/ScInfoPopup.md +248 -0
- package/dist/docs/ScIntialProfileCover.md +233 -0
- package/dist/docs/ScInvoiceHistoryMobile.md +187 -0
- package/dist/docs/ScLogoUnit.md +232 -0
- package/dist/docs/ScMappingCard.md +241 -0
- package/dist/docs/ScMediaApproval.md +301 -0
- package/dist/docs/ScMediaSelect.md +310 -0
- package/dist/docs/ScMenuOptions.md +308 -0
- package/dist/docs/ScMobileBottomAction.md +252 -0
- package/dist/docs/ScMobileTopNav.md +279 -0
- package/dist/docs/ScModal.md +291 -0
- package/dist/docs/ScOnlyField.md +302 -0
- package/dist/docs/ScOnlyIcon.md +213 -0
- package/dist/docs/ScPagination.md +284 -0
- package/dist/docs/ScPairtext.md +287 -0
- package/dist/docs/ScPendingAction.md +238 -0
- package/dist/docs/ScPhtogenixInvite.md +275 -0
- package/dist/docs/ScPlanCard.md +302 -0
- package/dist/docs/ScPlanComparison.md +264 -0
- package/dist/docs/ScPlanDetailsCard.md +246 -0
- package/dist/docs/ScPlanDetailsCardMobile.md +240 -0
- package/dist/docs/ScPopUpMenu.md +224 -0
- package/dist/docs/ScProfile.md +234 -0
- package/dist/docs/ScProfileImageUpdate.md +261 -0
- package/dist/docs/ScProfileOptions.md +245 -0
- package/dist/docs/ScProfilePopup.md +396 -0
- package/dist/docs/ScProfileSettingsComp.md +250 -0
- package/dist/docs/ScProfileV2Mobile.md +216 -0
- package/dist/docs/ScProgressBar.md +267 -0
- package/dist/docs/ScQuickPrompt.md +277 -0
- package/dist/docs/ScRadio.md +228 -0
- package/dist/docs/ScReferralCardMobile.md +226 -0
- package/dist/docs/ScReferralTableHeader.md +260 -0
- package/dist/docs/ScReferralTableList.md +293 -0
- package/dist/docs/ScRole.md +226 -0
- package/dist/docs/ScRoleMobile.md +199 -0
- package/dist/docs/ScSelect.md +270 -0
- package/dist/docs/ScSelection.md +256 -0
- package/dist/docs/ScSelectionList.md +272 -0
- package/dist/docs/ScSelectionPill.md +240 -0
- package/dist/docs/ScSelectionPillGroup.md +302 -0
- package/dist/docs/ScSettingsNav.md +212 -0
- package/dist/docs/ScSettingsTabComp.md +260 -0
- package/dist/docs/ScSideBarLogoUnit.md +340 -0
- package/dist/docs/ScSidebar.md +243 -0
- package/dist/docs/ScSidebarIcons.md +232 -0
- package/dist/docs/ScSidebarMenu.md +283 -0
- package/dist/docs/ScSidebarProfile.md +231 -0
- package/dist/docs/ScSidebarSwitchMenu.md +258 -0
- package/dist/docs/ScSlider.md +194 -0
- package/dist/docs/ScStoreCard.md +252 -0
- package/dist/docs/ScStrLogo.md +253 -0
- package/dist/docs/ScStreamoidWordmark.md +302 -0
- package/dist/docs/ScSubAgent.md +226 -0
- package/dist/docs/ScTabComp.md +308 -0
- package/dist/docs/ScTabField.md +258 -0
- package/dist/docs/ScTabSwitcher.md +307 -0
- package/dist/docs/ScTableHeader.md +261 -0
- package/dist/docs/ScTableList.md +301 -0
- package/dist/docs/ScTableListMobile.md +282 -0
- package/dist/docs/ScTabs.md +268 -0
- package/dist/docs/ScTaxonomyPill.md +263 -0
- package/dist/docs/ScTextArea.md +259 -0
- package/dist/docs/ScTextField.md +324 -0
- package/dist/docs/ScThinkingStepIcon.md +249 -0
- package/dist/docs/ScTodoList.md +288 -0
- package/dist/docs/ScToggleSwitch.md +229 -0
- package/dist/docs/ScUsageHistoryMobile.md +194 -0
- package/dist/docs/ScVDivider.md +215 -0
- package/dist/docs/ScValueMappingL1.md +256 -0
- package/dist/docs/ScVersion.md +251 -0
- package/dist/docs/ScWorkspace.md +233 -0
- package/dist/docs/ScWorkspaceCard.md +234 -0
- package/dist/docs/ScWorkspaceSettingsMobile.md +265 -0
- package/dist/docs/ScWorkspaceSwitchCard.md +314 -0
- package/dist/docs/ScWorkspaceSwitchMobile.md +241 -0
- package/dist/docs/ScWorkspaceSwitchMobileV2.md +278 -0
- package/dist/docs/StreamoidSidebar.md +403 -0
- package/dist/docs/StreamoidWorkspaceSwitcher.md +307 -0
- package/dist/docs/UsageHistoryMobile.md +235 -0
- package/dist/docs/components.json +4849 -0
- package/dist/index.css +36 -36
- package/dist/index.d.mts +10 -0
- package/dist/index.d.ts +10 -0
- package/package.json +3 -2
|
@@ -0,0 +1,308 @@
|
|
|
1
|
+
---
|
|
2
|
+
component: ScTabComp
|
|
3
|
+
package: "@streamoid/ui"
|
|
4
|
+
category: layout
|
|
5
|
+
status: stable
|
|
6
|
+
renders: div
|
|
7
|
+
tags: [tab, tab-cell, segmented, icon-tab, toggle, view-switch, theme-switcher]
|
|
8
|
+
related: [ScTabSwitcher, ScTabField, ScTabs, ScSettingsTabComp, ScSelectionPill]
|
|
9
|
+
do_not_confuse_with: [ScTabs, ScTabSwitcher, ScTabField, ScSettingsTabComp, ScSelectionPill]
|
|
10
|
+
used_by: [cxo, photogenix, catalogix, artifax]
|
|
11
|
+
---
|
|
12
|
+
|
|
13
|
+
# ScTabComp
|
|
14
|
+
|
|
15
|
+
**One tab cell — icon *or* text, never both.** The atom of the 2–5 way segmented
|
|
16
|
+
control: a rounded, padded div that turns light-filled with inverse text when
|
|
17
|
+
`active="true"`. Almost always rendered into a `ScTabSwitcher` slot, but usable
|
|
18
|
+
standalone (Catalogix's stores grid/list toggle does exactly that).
|
|
19
|
+
|
|
20
|
+
## TL;DR for agents
|
|
21
|
+
|
|
22
|
+
- **Reach for it when:** you need one segment of a compact 2–5 way view toggle —
|
|
23
|
+
theme switcher, grid/list, Active/Pending, Fast/Pro.
|
|
24
|
+
- **Don't reach for it when:** the tab list is data-driven or longer than five
|
|
25
|
+
(→ `ScTabs`), it's a settings underline strip (→ `ScSettingsTabComp`), or it's a
|
|
26
|
+
form value (→ `ScTabField`).
|
|
27
|
+
- **Four things that will bite you:**
|
|
28
|
+
1. `active` is the **string** `"true"` / `"false"`, not a boolean.
|
|
29
|
+
2. `icon` defaults to `SiconHome` and `text` defaults to `"Tab"` — forget both and
|
|
30
|
+
you ship a house labelled "Tab".
|
|
31
|
+
3. `type` is exclusive: `"text-only"` renders no icon, `"icon-only"` renders no
|
|
32
|
+
text, and **any other value renders an empty tab**.
|
|
33
|
+
4. Going active recolours only the **text**. Your icon's colour is your job.
|
|
34
|
+
|
|
35
|
+
---
|
|
36
|
+
|
|
37
|
+
## 1. How to use it
|
|
38
|
+
|
|
39
|
+
### Import
|
|
40
|
+
|
|
41
|
+
```tsx
|
|
42
|
+
import { ScTabComp } from "@streamoid/ui";
|
|
43
|
+
import "@streamoid/ui/dist/index.css"; // once, at your app root
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
### Minimal usage
|
|
47
|
+
|
|
48
|
+
```tsx
|
|
49
|
+
<ScTabComp text="Active" type="text-only" active="true" onClick={() => setTab("active")} />
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
### Props
|
|
53
|
+
|
|
54
|
+
| Prop | Type | Default | Notes |
|
|
55
|
+
|---|---|---|---|
|
|
56
|
+
| `text` | `string` | `"Tab"` | ⚠️ Real default. Only rendered when `type="text-only"`. Emitted with a trailing space. |
|
|
57
|
+
| `icon` | `JSX.Element` | `<SiconHome />` (1.25rem) | ⚠️ Real default. Only rendered when `type="icon-only"`. **Not** cloned/recoloured — you control its `color`. |
|
|
58
|
+
| `type` | `"text-only"` \| `"icon-only"` | `"text-only"` | Exclusive. No combined icon+text variant exists. |
|
|
59
|
+
| `active` | `"true"` \| `"false"` | `"false"` | ⚠️ **A string, not a boolean.** Drives the `.active-true` fill + inverse label. |
|
|
60
|
+
| `className` | `string` | – | Appended after the internal classes. |
|
|
61
|
+
| `...props` | `HTMLAttributes<HTMLDivElement>` | – | Spread onto the root div: `onClick`, `style`, `role`, `aria-*`, `data-*`. This is where selection handling lives. |
|
|
62
|
+
|
|
63
|
+
### What renders in each combination
|
|
64
|
+
|
|
65
|
+
| `type` | `active` | Renders |
|
|
66
|
+
|---|---|---|
|
|
67
|
+
| `"text-only"` | `"false"` | muted 14px/400 label, transparent background |
|
|
68
|
+
| `"text-only"` | `"true"` | inverse 14px/**500** label on `--alias-fill-base-basehover` |
|
|
69
|
+
| `"icon-only"` | `"false"` | your `icon` at 1.25rem, transparent background |
|
|
70
|
+
| `"icon-only"` | `"true"` | your `icon` at 1.25rem on `--alias-fill-base-basehover` — **icon colour unchanged** |
|
|
71
|
+
| anything else | any | **an empty div** (see Gotcha 3) |
|
|
72
|
+
|
|
73
|
+
### Recipes
|
|
74
|
+
|
|
75
|
+
```tsx
|
|
76
|
+
// Standalone icon toggle with real tab semantics (Catalogix stores view switch)
|
|
77
|
+
<ScTabComp
|
|
78
|
+
type="icon-only"
|
|
79
|
+
active={view === "grid" ? "true" : "false"}
|
|
80
|
+
role="tab"
|
|
81
|
+
aria-label="Grid view"
|
|
82
|
+
aria-selected={view === "grid"}
|
|
83
|
+
icon={
|
|
84
|
+
<SiconGrid
|
|
85
|
+
size={20}
|
|
86
|
+
color={view === "grid"
|
|
87
|
+
? "var(--alias-text-and-icons-inverse)"
|
|
88
|
+
: "var(--alias-text-and-icons-tertiary)"}
|
|
89
|
+
/>
|
|
90
|
+
}
|
|
91
|
+
onClick={() => changeView("grid")}
|
|
92
|
+
/>
|
|
93
|
+
|
|
94
|
+
// Inside ScTabSwitcher — note style={{ flex: 1 }}, it is NOT optional
|
|
95
|
+
<ScTabSwitcher
|
|
96
|
+
tabCount="2"
|
|
97
|
+
component={
|
|
98
|
+
<ScTabComp text="Active" type="text-only"
|
|
99
|
+
active={tab === "active" ? "true" : "false"}
|
|
100
|
+
onClick={() => setTab("active")} style={{ flex: 1 }} />
|
|
101
|
+
}
|
|
102
|
+
component2={
|
|
103
|
+
<ScTabComp text="Pending" type="text-only"
|
|
104
|
+
active={tab === "pending" ? "true" : "false"}
|
|
105
|
+
onClick={() => setTab("pending")} style={{ flex: 1 }} />
|
|
106
|
+
}
|
|
107
|
+
/>
|
|
108
|
+
|
|
109
|
+
// Three-way icon switcher (Photogenix theme picker)
|
|
110
|
+
<ScTabComp
|
|
111
|
+
type="icon-only"
|
|
112
|
+
active={theme === "dark" ? "true" : "false"}
|
|
113
|
+
icon={<SiconDark className="w-5 h-5"
|
|
114
|
+
color={theme === "dark" ? "var(--alias-text---icons-inverse)" : "var(--alias-text---icons-muted)"} />}
|
|
115
|
+
onClick={() => setTheme("dark")}
|
|
116
|
+
style={{ flex: 1 }}
|
|
117
|
+
/>
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
---
|
|
121
|
+
|
|
122
|
+
## 2. Where to use it
|
|
123
|
+
|
|
124
|
+
- **Inside `ScTabSwitcher`** — the dominant pattern. All four apps do this:
|
|
125
|
+
CXO teams Active/Pending, Photogenix theme + Fast/Pro + retouch modes, Catalogix
|
|
126
|
+
PLP views + attribute editor, Artifax sidebar theme switcher.
|
|
127
|
+
- **Inside `ScTabField`** — the DS field wrapper builds a 2-tab switcher out of these
|
|
128
|
+
for you; pass `tabs` to the field instead of composing.
|
|
129
|
+
- **Inside `ScProfilePopup` / `ScProfileOptions`** — the DS theme switcher rows.
|
|
130
|
+
- **Standalone**, as a bare icon toggle in a page header (Catalogix stores listing).
|
|
131
|
+
|
|
132
|
+
### Where it does *not* belong
|
|
133
|
+
|
|
134
|
+
Not in `@streamoid/settings`-owned settings screens with underline tabs — those use
|
|
135
|
+
`ScSettingsTabComp`.
|
|
136
|
+
|
|
137
|
+
---
|
|
138
|
+
|
|
139
|
+
## 3. When to use it
|
|
140
|
+
|
|
141
|
+
### Use it when
|
|
142
|
+
|
|
143
|
+
- The control has **2–5 mutually exclusive** segments, all visible at once.
|
|
144
|
+
- Each segment is a single icon **or** a single short word.
|
|
145
|
+
- You want the "selected = light fill + inverse text" treatment that matches the rest
|
|
146
|
+
of the product's toggles.
|
|
147
|
+
|
|
148
|
+
### Don't use it — reach for this instead
|
|
149
|
+
|
|
150
|
+
| Situation | Use instead |
|
|
151
|
+
|---|---|
|
|
152
|
+
| The bordered container around 2–5 of these | `ScTabSwitcher` (don't hand-roll the border/padding) |
|
|
153
|
+
| Icon **and** label in the same tab | nothing in the DS does this — use `ScSelectionPill`, or two elements inside `children`-less layout of your own |
|
|
154
|
+
| 6+ tabs, or tabs from data | `ScTabs` |
|
|
155
|
+
| Settings/mobile-settings underline strip | `ScSettingsTabComp` |
|
|
156
|
+
| A segmented control that is a saved form value | `ScTabField` |
|
|
157
|
+
| Accessible segmented control (real buttons, `aria-pressed`) | `ScSelectionPill` / `ScSelectionPillGroup` |
|
|
158
|
+
| A plain action | `ScButton`; a bare icon action → `ScOnlyIcon` |
|
|
159
|
+
|
|
160
|
+
### Don't confuse with
|
|
161
|
+
|
|
162
|
+
| You may actually want | Not this |
|
|
163
|
+
|---|---|
|
|
164
|
+
| `ScSettingsTabComp` — underline tab whose `active` is a **boolean** | `ScTabComp`'s `active` is the string `"true"`/`"false"` |
|
|
165
|
+
| `ScTabs` — the whole pill row, driven by `tabs` + `activeTab` | `ScTabComp` is one cell and knows nothing about its siblings |
|
|
166
|
+
| `ScTabSwitcher` — the container | `ScTabComp` is the content |
|
|
167
|
+
| `ScTabField` — label + 2-tab switcher, already composed | Don't rebuild it out of `ScTabComp`s |
|
|
168
|
+
|
|
169
|
+
### The tab family at a glance
|
|
170
|
+
|
|
171
|
+
| Component | Shape | Selection API | Root | Spreads DOM props? |
|
|
172
|
+
|---|---|---|---|---|
|
|
173
|
+
| `ScTabComp` | one tab cell | `active="true" \| "false"` (strings) | `div` | ✅ |
|
|
174
|
+
| `ScTabSwitcher` | 2–5 slot container | none — children own it | `div` | ✅ |
|
|
175
|
+
| `ScTabs` | N pills, data-driven | `activeTab` + `onClickTab(value)` | `div` | ❌ |
|
|
176
|
+
| `ScTabField` | field label + 2-tab switcher | `activeTab` + `onTabChange` | `div` | ❌ |
|
|
177
|
+
| `ScSettingsTabComp` | underline tab | `active` (boolean) | `div` | ❌ |
|
|
178
|
+
| `ScSelectionPill` | one segmented pill | `selected` (boolean) + `onSelect(value)` | `button[type="button"][aria-pressed]` | ✅ |
|
|
179
|
+
| `ScSelectionPillGroup` | segmented pill row | `value` + `onChange(value)` | `div[role="tablist"]` of `button`s | ✅ |
|
|
180
|
+
|
|
181
|
+
---
|
|
182
|
+
|
|
183
|
+
## 4. Why to use it
|
|
184
|
+
|
|
185
|
+
- **The active skin is a token pair.** `--alias-fill-base-basehover` fill +
|
|
186
|
+
`--alias-text-and-icons-inverse` label resolves correctly in both themes — the
|
|
187
|
+
single most common hand-rolled-toggle bug is a selected state that turns
|
|
188
|
+
white-on-white in light mode.
|
|
189
|
+
- **The type ramp changes with state**, not just the colour: idle is `text-sm-regular`
|
|
190
|
+
(400), active is `text-sm-medium` (500). Hand-rolled toggles forget the weight
|
|
191
|
+
change and the control reads flat.
|
|
192
|
+
- **Props are spread**, so `role="tab"` / `aria-selected` / `data-testid` are
|
|
193
|
+
available — this component can be made accessible, unlike `ScTabs`.
|
|
194
|
+
- **It composes into `ScTabSwitcher`, `ScTabField`, `ScProfilePopup`** — one visual
|
|
195
|
+
change to the tab cell updates the theme switcher, the teams filter and the PLP
|
|
196
|
+
toggle at once.
|
|
197
|
+
|
|
198
|
+
---
|
|
199
|
+
|
|
200
|
+
## Gotchas
|
|
201
|
+
|
|
202
|
+
**1. `active` is a string.** `active={true}` is a type error; `active={String(x)}`
|
|
203
|
+
compiles but isn't narrowed. Use a ternary.
|
|
204
|
+
|
|
205
|
+
```tsx
|
|
206
|
+
// WRONG
|
|
207
|
+
<ScTabComp text="Active" active={tab === "active"} />
|
|
208
|
+
|
|
209
|
+
// RIGHT
|
|
210
|
+
<ScTabComp text="Active" active={tab === "active" ? "true" : "false"} />
|
|
211
|
+
```
|
|
212
|
+
|
|
213
|
+
**2. Placeholder defaults.** `icon` is `SiconHome`, `text` is `"Tab"`. `type="icon-only"`
|
|
214
|
+
without an `icon` ships a home glyph; `type="text-only"` without `text` ships "Tab".
|
|
215
|
+
|
|
216
|
+
**3. An unrecognised `type` renders an empty tab.** The body is two exclusive
|
|
217
|
+
`type === …` checks, so anything outside the union produces a clickable blank.
|
|
218
|
+
|
|
219
|
+
```tsx
|
|
220
|
+
// WRONG — casts past the type and renders NOTHING inside the tab
|
|
221
|
+
<ScTabComp type={"default" as "icon-only" | "text-only"} icon={<Zap />} text="Fast" />
|
|
222
|
+
|
|
223
|
+
// RIGHT — pick a side
|
|
224
|
+
<ScTabComp type={showLabels ? "text-only" : "icon-only"} icon={<Zap />} text="Fast" />
|
|
225
|
+
```
|
|
226
|
+
|
|
227
|
+
(There is a live instance of the wrong form in Photogenix's `QualityToggle.tsx` —
|
|
228
|
+
when labels are enabled, the tab body is empty.)
|
|
229
|
+
|
|
230
|
+
**4. There is no icon+text variant.** `type` is exclusive; passing both props only
|
|
231
|
+
means one of them is ignored.
|
|
232
|
+
|
|
233
|
+
**5. Active does not recolour your icon.** The stylesheet only retargets `.tabText`.
|
|
234
|
+
Every host call site switches the icon colour manually — do the same, using
|
|
235
|
+
`--alias-text-and-icons-inverse` when active.
|
|
236
|
+
|
|
237
|
+
```tsx
|
|
238
|
+
// WRONG — icon stays muted on the light active fill (poor contrast)
|
|
239
|
+
<ScTabComp type="icon-only" active="true" icon={<SiconGrid size={20} />} />
|
|
240
|
+
|
|
241
|
+
// RIGHT
|
|
242
|
+
<ScTabComp type="icon-only" active="true"
|
|
243
|
+
icon={<SiconGrid size={20} color="var(--alias-text-and-icons-inverse)" />} />
|
|
244
|
+
```
|
|
245
|
+
|
|
246
|
+
**6. Inside `ScTabSwitcher` you must pass `style={{ flex: 1 }}`.** The switcher's
|
|
247
|
+
`flex: 1` override lands on the *default* children it creates itself, via a class you
|
|
248
|
+
don't get. A custom child keeps `align-self: stretch; flex-shrink: 0` and sizes to its
|
|
249
|
+
content, so tabs come out unequal widths.
|
|
250
|
+
|
|
251
|
+
**7. It's a `div` with `cursor: pointer`.** No `role`, no `tabIndex`, no
|
|
252
|
+
`aria-selected`, no Enter/Space handling. Add them yourself (props are spread) if the
|
|
253
|
+
control matters for keyboard users.
|
|
254
|
+
|
|
255
|
+
**8. `className` is concatenated unguarded** — omit it and the class string contains
|
|
256
|
+
the literal `"undefined"`.
|
|
257
|
+
|
|
258
|
+
**9. `align-self: stretch` on the root** means it fills its parent's cross axis. In a
|
|
259
|
+
flex-column parent it goes full width; that's usually not what a tab wants.
|
|
260
|
+
|
|
261
|
+
**10. The label has a trailing space** (`{text} ` in JSX), so
|
|
262
|
+
`textContent === "Active "`.
|
|
263
|
+
|
|
264
|
+
---
|
|
265
|
+
|
|
266
|
+
## In the wild
|
|
267
|
+
|
|
268
|
+
```jsx
|
|
269
|
+
// catalogix/dashboard app/containers/StoresListing/index.jsx:306
|
|
270
|
+
<ScTabComp
|
|
271
|
+
type="icon-only"
|
|
272
|
+
active={view === "grid" ? "true" : "false"}
|
|
273
|
+
role="tab"
|
|
274
|
+
aria-label="Grid view"
|
|
275
|
+
aria-selected={view === "grid"}
|
|
276
|
+
icon={
|
|
277
|
+
<SiconGrid
|
|
278
|
+
size={20}
|
|
279
|
+
color={view === "grid"
|
|
280
|
+
? "var(--alias-text-and-icons-inverse)"
|
|
281
|
+
: "var(--alias-text-and-icons-tertiary)"}
|
|
282
|
+
/>
|
|
283
|
+
}
|
|
284
|
+
onClick={() => changeView("grid")}
|
|
285
|
+
/>
|
|
286
|
+
```
|
|
287
|
+
|
|
288
|
+
```tsx
|
|
289
|
+
// cxo-dashboard src/app/components/teams-content.tsx:1174
|
|
290
|
+
<ScTabComp
|
|
291
|
+
text="Active"
|
|
292
|
+
type="text-only"
|
|
293
|
+
active={activeTab === "active" ? "true" : "false"}
|
|
294
|
+
onClick={() => setActiveTab("active")}
|
|
295
|
+
style={{ flex: 1 }}
|
|
296
|
+
/>
|
|
297
|
+
```
|
|
298
|
+
|
|
299
|
+
---
|
|
300
|
+
|
|
301
|
+
## Related
|
|
302
|
+
|
|
303
|
+
- `ScTabSwitcher` — the bordered container; pass these in via `component`…`component5`.
|
|
304
|
+
- `ScTabField` — a labelled form field that builds a 2-tab switcher out of these.
|
|
305
|
+
- `ScTabs` — data-driven N-tab pill bar when 5 isn't enough.
|
|
306
|
+
- `ScSettingsTabComp` — underline tab for settings strips (boolean `active`).
|
|
307
|
+
- `ScSelectionPill` / `ScSelectionPillGroup` — accessible segmented alternative.
|
|
308
|
+
- `@streamoid/icons` — the `Sicon*` set; check `packages/icons/ICONS.md` first.
|
|
@@ -0,0 +1,258 @@
|
|
|
1
|
+
---
|
|
2
|
+
component: ScTabField
|
|
3
|
+
package: "@streamoid/ui"
|
|
4
|
+
category: forms
|
|
5
|
+
status: stable
|
|
6
|
+
renders: div
|
|
7
|
+
tags: [tab, field, form, segmented, single-select, role-picker, two-choice, switcher]
|
|
8
|
+
related: [ScTabSwitcher, ScTabComp, ScCheckField, ScTextField, ScSelectionPillGroup, ScTabs]
|
|
9
|
+
do_not_confuse_with: [ScTabSwitcher, ScTabComp, ScTabs, ScSettingsTabComp, ScSelectionPillGroup, ScCheckField]
|
|
10
|
+
used_by: [cxo]
|
|
11
|
+
---
|
|
12
|
+
|
|
13
|
+
# ScTabField
|
|
14
|
+
|
|
15
|
+
**A labelled single-choice form field rendered as a segmented switcher.** Muted
|
|
16
|
+
caption on top, a bordered `ScTabSwitcher` underneath holding two `ScTabComp` tabs.
|
|
17
|
+
Built for form stacks — "Role: Admin | Member" — not for page navigation.
|
|
18
|
+
|
|
19
|
+
## TL;DR for agents
|
|
20
|
+
|
|
21
|
+
- **Reach for it when:** a form needs **exactly two** mutually exclusive values under
|
|
22
|
+
one caption, and you want it to match the `ScTextField` / `ScCheckField` rhythm.
|
|
23
|
+
- **Don't reach for it when:** you have 3+ options (only two are wired — see
|
|
24
|
+
Gotcha 2), you're building a page/panel tab bar (→ `ScTabs` / `ScTabSwitcher`), or
|
|
25
|
+
you need multi-select (→ `ScCheckField`).
|
|
26
|
+
- **Four things that will bite you:**
|
|
27
|
+
1. **Only `tabs[0]` and `tabs[1]` get wired.** A third entry renders a dead
|
|
28
|
+
placeholder tab reading **"Tab"**.
|
|
29
|
+
2. Omit `tabs` entirely and you ship **two placeholder tabs both labelled "Tab"**.
|
|
30
|
+
3. `label` defaults to `"Label"` ⚠️.
|
|
31
|
+
4. It does **not** extend `HTMLAttributes` — `style`, `id`, `data-*`, `onClick`
|
|
32
|
+
are silently unavailable (the rest prop is collected and thrown away).
|
|
33
|
+
|
|
34
|
+
---
|
|
35
|
+
|
|
36
|
+
## 1. How to use it
|
|
37
|
+
|
|
38
|
+
### Import
|
|
39
|
+
|
|
40
|
+
```tsx
|
|
41
|
+
import { ScTabField } from "@streamoid/ui";
|
|
42
|
+
import "@streamoid/ui/dist/index.css"; // once, at your app root
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
### Minimal usage
|
|
46
|
+
|
|
47
|
+
```tsx
|
|
48
|
+
<ScTabField
|
|
49
|
+
label="Role"
|
|
50
|
+
tabs={[
|
|
51
|
+
{ label: "Admin", value: "admin" },
|
|
52
|
+
{ label: "Member", value: "member" },
|
|
53
|
+
]}
|
|
54
|
+
activeTab={role}
|
|
55
|
+
onTabChange={setRole}
|
|
56
|
+
/>
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
### Props
|
|
60
|
+
|
|
61
|
+
| Prop | Type | Default | Notes |
|
|
62
|
+
|---|---|---|---|
|
|
63
|
+
| `label` | `string` | `"Label"` | ⚠️ Real default. Muted caption (`--alias-text-and-icons-tertiary`, text-sm), fixed 1.25rem height — it does not wrap. |
|
|
64
|
+
| `tabs` | `{ label: string; value: string }[]` | – | **Only the first two entries are rendered as live tabs.** `tabs.length` is passed straight through as `ScTabSwitcher`'s `tabCount`. |
|
|
65
|
+
| `activeTab` | `string` | – | Compared against each tab's `value` to set `active`. Controlled — no internal state. |
|
|
66
|
+
| `onTabChange` | `(value: string) => void` | – | Fires with the clicked tab's `value`. |
|
|
67
|
+
| `className` | `string` | – | Concatenated onto the outer column (not the switcher). |
|
|
68
|
+
|
|
69
|
+
⚠️ `IScTabFieldProps` does **not** extend `React.HTMLAttributes`. The component
|
|
70
|
+
destructures `...props` and never spreads it, so even if you cast around the types
|
|
71
|
+
nothing reaches the DOM. `className` is the only passthrough.
|
|
72
|
+
|
|
73
|
+
### What renders for each `tabs` length
|
|
74
|
+
|
|
75
|
+
| `tabs.length` | Result |
|
|
76
|
+
|---|---|
|
|
77
|
+
| `undefined` (prop omitted) | Default `ScTabSwitcher`: **two placeholder `ScTabComp`s labelled "Tab"**, the first `active="true"`, neither clickable |
|
|
78
|
+
| 1 | One live tab + **one placeholder "Tab"** (slot 2 falls back) |
|
|
79
|
+
| 2 | ✅ The intended case: two live tabs |
|
|
80
|
+
| 3–5 | Two live tabs + `(length − 2)` **dead placeholder "Tab"** tabs |
|
|
81
|
+
| >5 | Two live tabs only; `tabCount` is out of range so slots 3–5 don't render |
|
|
82
|
+
|
|
83
|
+
### Recipes
|
|
84
|
+
|
|
85
|
+
```tsx
|
|
86
|
+
// The CXO idiom — role picker in a mobile invite form, typed value
|
|
87
|
+
<ScTabField
|
|
88
|
+
label="Role"
|
|
89
|
+
tabs={[
|
|
90
|
+
{ label: "Admin", value: "admin" },
|
|
91
|
+
{ label: "Member", value: "member" },
|
|
92
|
+
]}
|
|
93
|
+
activeTab={role}
|
|
94
|
+
onTabChange={(v) => setRole(v as "admin" | "member")}
|
|
95
|
+
className="w-full shrink-0"
|
|
96
|
+
/>
|
|
97
|
+
|
|
98
|
+
// THREE options? Don't use ScTabField. Drive ScTabSwitcher directly:
|
|
99
|
+
<ScTabSwitcher
|
|
100
|
+
tabCount="3"
|
|
101
|
+
component={<ScTabComp text="Day" type="text-only" active={p === "day" ? "true" : "false"} onClick={() => setP("day")} style={{ flex: 1 }} />}
|
|
102
|
+
component2={<ScTabComp text="Week" type="text-only" active={p === "week" ? "true" : "false"} onClick={() => setP("week")} style={{ flex: 1 }} />}
|
|
103
|
+
component3={<ScTabComp text="Month" type="text-only" active={p === "month" ? "true" : "false"} onClick={() => setP("month")} style={{ flex: 1 }} />}
|
|
104
|
+
/>
|
|
105
|
+
|
|
106
|
+
// …and add the caption yourself if it must look like a field
|
|
107
|
+
<div style={{ display: "flex", flexDirection: "column", gap: 4, width: "100%" }}>
|
|
108
|
+
<div style={{ color: "var(--alias-text-and-icons-tertiary)", fontSize: "0.875rem" }}>Period</div>
|
|
109
|
+
<ScTabSwitcher tabCount="3" /* …components… */ />
|
|
110
|
+
</div>
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
---
|
|
114
|
+
|
|
115
|
+
## 2. Where to use it
|
|
116
|
+
|
|
117
|
+
- **CXO's mobile teams popups** — the "Role" field in `mobile-teams-invite-popup`
|
|
118
|
+
and `mobile-teams-manage-popup`, directly above `ScCheckField` ("Permission") and
|
|
119
|
+
`ScAppField` ("App permission").
|
|
120
|
+
- Any **two-way choice inside a vertical form stack** where the caption and the
|
|
121
|
+
bordered container must line up with `ScTextField` / `ScCheckField` /
|
|
122
|
+
`ScOnlyField`. Those share the caption style, the `--spacing-xs` gap and the
|
|
123
|
+
`--radius-xl` container.
|
|
124
|
+
|
|
125
|
+
It composes `ScTabSwitcher` → `ScTabComp`. Nothing else in the DS renders it.
|
|
126
|
+
|
|
127
|
+
---
|
|
128
|
+
|
|
129
|
+
## 3. When to use it
|
|
130
|
+
|
|
131
|
+
### Use it when
|
|
132
|
+
|
|
133
|
+
- There are **exactly two** values, both worth showing at once.
|
|
134
|
+
- The value is a **form value** (staged, saved on submit), not a view switch.
|
|
135
|
+
- The control must read as a field: caption above, bordered container below.
|
|
136
|
+
|
|
137
|
+
### Don't use it — reach for this instead
|
|
138
|
+
|
|
139
|
+
| Situation | Use instead |
|
|
140
|
+
|---|---|
|
|
141
|
+
| 3–5 options | `ScTabSwitcher` + `ScTabComp` (supply `component3`…`component5`) or `ScSelectionPillGroup` |
|
|
142
|
+
| A full-width in-page tab bar | `ScTabs` |
|
|
143
|
+
| A compact view toggle in a page header (not a form) | `ScTabSwitcher` + `ScTabComp` |
|
|
144
|
+
| The settings-page tab strip | `ScSettingsTabComp` / `ScSettingsNav` |
|
|
145
|
+
| Filter pills over a list | `ScSelectionPillGroup` |
|
|
146
|
+
| Multi-select booleans under one caption | `ScCheckField` |
|
|
147
|
+
| A long list of single choices | `ScRadio` rows, or `ScSelect` |
|
|
148
|
+
| An on/off setting | `ScToggleSwitch` |
|
|
149
|
+
|
|
150
|
+
### Don't confuse with
|
|
151
|
+
|
|
152
|
+
| You may actually want | Not this |
|
|
153
|
+
|---|---|
|
|
154
|
+
| `ScTabSwitcher` — the bordered segmented container; takes `component`…`component5` and supports 2–5 tabs | `ScTabField` is that container **plus a caption**, capped at 2 live tabs |
|
|
155
|
+
| `ScTabComp` — one tab inside the switcher (`text-only` / `icon-only`, `active` as a **string**) | `ScTabField` builds these for you |
|
|
156
|
+
| `ScTabs` — the in-page tab bar for navigating panel content | Not a form field |
|
|
157
|
+
| `ScSettingsTabComp` — the settings-page tab strip | Different surface |
|
|
158
|
+
| `ScSelectionPillGroup` — free-standing pills, no caption, no container | Different look, same job for filters |
|
|
159
|
+
| `ScCheckField` — same caption + container chrome, but **multi**-select | Both end in "…Field"; only this one is single-choice |
|
|
160
|
+
|
|
161
|
+
---
|
|
162
|
+
|
|
163
|
+
## 4. Why to use it
|
|
164
|
+
|
|
165
|
+
- **Field rhythm for free.** Caption colour/size, `--spacing-xs` gap, `--radius-xl`
|
|
166
|
+
container and the `0.03125rem --alias-border-divider` hairline are the same tokens
|
|
167
|
+
the other DS fields use, so a mixed stack aligns without call-site CSS.
|
|
168
|
+
- **Equal-width tabs.** The field passes `style={{ flex: 1 }}` into each `ScTabComp`
|
|
169
|
+
it builds, so "Admin" and "Member" are the same width instead of hugging their text.
|
|
170
|
+
(If you drive `ScTabSwitcher` yourself you must add that `flex: 1` — the switcher's
|
|
171
|
+
own `flex: 1 !important` class is applied only to its *fallback* tabs, not to
|
|
172
|
+
components you pass in.)
|
|
173
|
+
- **The `active` plumbing is done.** `ScTabComp`'s `active` is a `"true"`/`"false"`
|
|
174
|
+
**string** (a Figma-ism that is easy to get wrong by hand); this field derives it
|
|
175
|
+
from `activeTab === tab.value` for you.
|
|
176
|
+
- **Theme-correct container** — `--alias-surface-base` keeps the switcher distinct
|
|
177
|
+
from the page in both themes.
|
|
178
|
+
|
|
179
|
+
---
|
|
180
|
+
|
|
181
|
+
## Gotchas
|
|
182
|
+
|
|
183
|
+
**1. No `tabs` → two dead tabs labelled "Tab".** The fallback renders a bare
|
|
184
|
+
`ScTabSwitcher`, whose own fallback is two default `ScTabComp`s (`text = "Tab"`), the
|
|
185
|
+
first hardcoded `active="true"`, with no click handlers.
|
|
186
|
+
|
|
187
|
+
```tsx
|
|
188
|
+
// WRONG — ships "Label" over two inert tabs reading "Tab"
|
|
189
|
+
<ScTabField />
|
|
190
|
+
|
|
191
|
+
// RIGHT
|
|
192
|
+
<ScTabField label="Role" tabs={ROLES} activeTab={role} onTabChange={setRole} />
|
|
193
|
+
```
|
|
194
|
+
|
|
195
|
+
**2. Only the first two tabs are wired.** `tabCount` becomes `String(tabs.length)`,
|
|
196
|
+
so slots 3–5 *render* — but `component3`…`component5` are never supplied, so each
|
|
197
|
+
falls back to a placeholder `ScTabComp` reading **"Tab"** with no `onClick`.
|
|
198
|
+
|
|
199
|
+
```tsx
|
|
200
|
+
// WRONG — renders: Admin | Member | Tab (the third is dead)
|
|
201
|
+
<ScTabField label="Role" tabs={[
|
|
202
|
+
{ label: "Admin", value: "admin" },
|
|
203
|
+
{ label: "Member", value: "member" },
|
|
204
|
+
{ label: "Viewer", value: "viewer" },
|
|
205
|
+
]} activeTab={role} onTabChange={setRole} />
|
|
206
|
+
|
|
207
|
+
// RIGHT — 3+ options: drive ScTabSwitcher directly (see Recipes)
|
|
208
|
+
```
|
|
209
|
+
|
|
210
|
+
**3. `label` defaults to `"Label"`.** ⚠️
|
|
211
|
+
|
|
212
|
+
**4. No DOM passthrough at all.** The interface doesn't extend `HTMLAttributes`, and
|
|
213
|
+
the collected `...props` is never spread. No `style`, `id`, `data-testid`, `onClick`,
|
|
214
|
+
`aria-*`. Wrap it in your own div when you need any of those. (`className` does work
|
|
215
|
+
— CXO passes Tailwind classes through it.)
|
|
216
|
+
|
|
217
|
+
**5. Fully controlled.** No internal state: if `onTabChange` doesn't move
|
|
218
|
+
`activeTab`, the highlight never moves.
|
|
219
|
+
|
|
220
|
+
**6. `activeTab` with no match = nothing active.** Unlike the no-`tabs` fallback
|
|
221
|
+
(which force-activates the first tab), a mismatched `activeTab` leaves *both* tabs
|
|
222
|
+
inactive. Initialise your state to a real `value`.
|
|
223
|
+
|
|
224
|
+
**7. `className` lands on the outer column, not the switcher.** There is no
|
|
225
|
+
`switcherClassName`; restyle via a descendant selector from your own class.
|
|
226
|
+
|
|
227
|
+
**8. `tabs` is keyed positionally.** `tabs[0]`/`tabs[1]` are read by index, so
|
|
228
|
+
reordering the array reorders the meaning of the tabs — fine, but don't rely on
|
|
229
|
+
`value` alone determining position.
|
|
230
|
+
|
|
231
|
+
---
|
|
232
|
+
|
|
233
|
+
## In the wild
|
|
234
|
+
|
|
235
|
+
```tsx
|
|
236
|
+
// cxo-dashboard src/app/components/mobile-teams-invite-popup.tsx:137
|
|
237
|
+
<ScTabField
|
|
238
|
+
label="Role"
|
|
239
|
+
tabs={[
|
|
240
|
+
{ label: "Admin", value: "admin" },
|
|
241
|
+
{ label: "Member", value: "member" },
|
|
242
|
+
]}
|
|
243
|
+
activeTab={role}
|
|
244
|
+
onTabChange={(v) => setRole(v as "admin" | "member")}
|
|
245
|
+
className="w-full shrink-0"
|
|
246
|
+
/>
|
|
247
|
+
```
|
|
248
|
+
|
|
249
|
+
---
|
|
250
|
+
|
|
251
|
+
## Related
|
|
252
|
+
|
|
253
|
+
- `ScTabSwitcher` — the container this wraps; use it directly for 3–5 tabs.
|
|
254
|
+
- `ScTabComp` — one tab; note `active` is the string `"true"`/`"false"`.
|
|
255
|
+
- `ScCheckField` — the multi-select sibling with identical field chrome.
|
|
256
|
+
- `ScTextField` / `ScOnlyField` — the text fields it stacks with.
|
|
257
|
+
- `ScTabs` / `ScSettingsTabComp` — navigation tab bars, not form fields.
|
|
258
|
+
- `ScSelectionPillGroup` — pill-shaped single-choice for filters and views.
|