@streamoid/ui 0.6.16 → 0.6.18

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