@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.
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 +36 -36
  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,284 @@
1
+ ---
2
+ component: ScPagination
3
+ package: "@streamoid/ui"
4
+ category: overlays
5
+ status: stable
6
+ renders: div > div > (SiconRight, input[type=number], SiconRight, span)
7
+ tags: [pagination, pager, page, paging, offset, next, prev, table, list]
8
+ related: [ScTableHeader, ScTableList, ScCatalogixStoreTableList, ScCounter, ScOnlyField]
9
+ do_not_confuse_with: [ScCounter, ScTabs, ScOnlyField]
10
+ used_by: [catalogix]
11
+ required_props: [itemsCount, itemsPerPage, page, setPage, setOffsetValue, onPageChangeAction]
12
+ ---
13
+
14
+ # ScPagination
15
+
16
+ **The offset-based page stepper for tables.** A right-aligned row of
17
+ `‹ [ 3 ] › of 17`: a numeric input clamped to `[1, maxPage]` between two chevrons,
18
+ plus the derived total. It is fully controlled and it reports an **offset**, not a
19
+ page — a direct port of Catalogix's local `Pagination`, so it wraps as a
20
+ pass-through.
21
+
22
+ ## TL;DR for agents
23
+
24
+ - **Reach for it when:** you have a server-paged table or list and you already hold
25
+ `page` + `offset` state and a refetch function.
26
+ - **Don't reach for it when:** you want a numeric stepper in a form
27
+ (→ `ScCounter`), page-size selection (not provided), or numbered page links
28
+ (`1 2 3 …` — not this component).
29
+ - **Five things that will bite you:**
30
+ 1. **All six functional props are required.** No defaults, nothing optional
31
+ except `className`.
32
+ 2. It reports an **offset** via `setOffsetValue((page - 1) * perPage)` *and*
33
+ separately calls `onPageChangeAction()`. `setPage` alone never refetches.
34
+ 3. `maxPage` is **derived** from `itemsCount / itemsPerPage` — there is no
35
+ `totalPages` prop.
36
+ 4. Typing commits on **blur**. There is no `keydown` handler, so **Enter does
37
+ nothing**.
38
+ 5. Clearing the input and blurring commits a **negative offset**
39
+ (`(Number("") - 1) * perPage`). Guard your fetch.
40
+
41
+ ---
42
+
43
+ ## 1. How to use it
44
+
45
+ ### Import
46
+
47
+ ```tsx
48
+ import { ScPagination } from "@streamoid/ui";
49
+ import "@streamoid/ui/dist/index.css"; // once, at your app root
50
+ ```
51
+
52
+ ### Minimal usage
53
+
54
+ ```tsx
55
+ const [page, setPage] = useState<number | string>(1);
56
+ const [offset, setOffset] = useState(0);
57
+
58
+ <ScPagination
59
+ itemsCount={total}
60
+ itemsPerPage={PER_PAGE}
61
+ page={page}
62
+ setPage={setPage}
63
+ setOffsetValue={setOffset}
64
+ onPageChangeAction={() => refetch()}
65
+ />
66
+ ```
67
+
68
+ ### Props
69
+
70
+ | Prop | Type | Required | Notes |
71
+ |---|---|---|---|
72
+ | `itemsCount` | `number` | **yes** | Total rows on the server. `maxPage` is computed from this — `itemsCount % perPage ? floor(itemsCount / perPage) + 1 : itemsCount / perPage`. |
73
+ | `itemsPerPage` | `number \| string` | **yes** | Parsed with `parseInt(String(v), 10) \|\| 1`, so `0`, `""` or `"all"` silently becomes **1**. |
74
+ | `page` | `number \| string` | **yes** | The current page, 1-based. It is a `string` while the user is typing (see Gotcha 5). Renders straight into the input's `value`. |
75
+ | `setPage` | `(page: number \| string) => void` | **yes** | Receives the **raw string** from typing (clamped to `1` / `maxPage` at the bounds) and a **number** from the arrows. |
76
+ | `setOffsetValue` | `(offset: number) => void` | **yes** | The commit channel: `(page - 1) * perPage`. This is what your query should read. |
77
+ | `onPageChangeAction` | `() => void` | **yes** | Called immediately after `setOffsetValue` on every commit. Your refetch. |
78
+ | `className` | `string` | no | Appended after `.scPagination` (only when truthy). |
79
+
80
+ No `...props` spread — anything else is a type error.
81
+
82
+ ### When each callback fires
83
+
84
+ | Interaction | `setPage` | `setOffsetValue` | `onPageChangeAction` |
85
+ |---|---|---|---|
86
+ | Typing a digit | ✓ (raw string, clamped at the bounds) | — | — |
87
+ | Blurring the input | — | ✓ `(Number(value) - 1) * perPage` | ✓ |
88
+ | Pressing Enter | — | — | — (no keydown handler) |
89
+ | `‹` at page > 1 | ✓ `p - 1` | ✓ `(p - 2) * perPage` | ✓ |
90
+ | `›` at page < maxPage | ✓ `p + 1` | ✓ `p * perPage` | ✓ |
91
+ | `‹` at page 1 / `›` at maxPage | — | — | — (silent no-op) |
92
+
93
+ ### What it actually renders
94
+
95
+ ```html
96
+ <div class="scPagination {className}"> <!-- width:100%; display:flex; justify-content:flex-end -->
97
+ <div class="section"> <!-- overflow:auto; 16px/1.375 --alias-text-and-icons-primary -->
98
+ <SiconRight size=24 class="leftArrow" /> <!-- the SAME icon, CSS-rotated 180deg -->
99
+ <span class="page"><input type="number" pattern="\d*" /></span>
100
+ <!-- 60px wide, centered, radius 16px,
101
+ --alias-surface-canvasbase, spinners removed -->
102
+ <SiconRight size=24 />
103
+ <span class="pageCount">of {maxPage}</span>
104
+ </div>
105
+ </div>
106
+ ```
107
+
108
+ ### Recipes
109
+
110
+ ```tsx
111
+ // Offset-driven query — the intended wiring
112
+ const [page, setPage] = useState<number | string>(1);
113
+ const [offset, setOffset] = useState(0);
114
+ const { data, refetch } = useProducts({ limit: PER_PAGE, offset });
115
+
116
+ <ScPagination
117
+ itemsCount={data?.total ?? 0}
118
+ itemsPerPage={PER_PAGE}
119
+ page={page}
120
+ setPage={setPage}
121
+ setOffsetValue={setOffset}
122
+ onPageChangeAction={refetch}
123
+ />
124
+
125
+ // Guard the negative offset an emptied input produces
126
+ setOffsetValue={(o) => setOffset(Math.max(0, o))}
127
+
128
+ // If your query keys off `offset`, you may not need onPageChangeAction to do work
129
+ onPageChangeAction={() => {}} // still required — pass a no-op
130
+
131
+ // Reset to page 1 whenever filters change (the component won't do it for you)
132
+ useEffect(() => { setPage(1); setOffset(0); }, [filters]);
133
+ ```
134
+
135
+ ---
136
+
137
+ ## 2. Where to use it
138
+
139
+ - **Under a paged table** — after `ScTableHeader` + `ScTableList` rows, or
140
+ `ScCatalogixStoreHeader` + `ScCatalogixStoreTableList`.
141
+ - **Under a paged card/product grid.**
142
+ - **Catalogix** wraps it as `app/components/Pagination`, which adds the outer
143
+ `margin: 24px 24px 18px 0` and passes every prop straight through. Inside
144
+ Catalogix, use the wrapper. Catalogix is the only host consumer.
145
+
146
+ The root is `width: 100%; justify-content: flex-end`, so it right-aligns itself
147
+ inside whatever block you put it in — do not wrap it in your own flex row expecting
148
+ to control alignment.
149
+
150
+ ---
151
+
152
+ ## 3. When to use it
153
+
154
+ ### Use it when
155
+
156
+ - Paging happens **on the server** and your query takes a `limit` + `offset`.
157
+ - The user needs to **jump** to a page number, not just step one at a time.
158
+ - The list is long enough that page count matters, and you already have the total.
159
+
160
+ ### Don't use it — reach for this instead
161
+
162
+ | Situation | Use instead |
163
+ |---|---|
164
+ | A bounded numeric input in a form (quantity, batch size) | `ScCounter` |
165
+ | A bare number input with no stepper chrome | `ScOnlyField` |
166
+ | Switching between views/sections | `ScTabs` / `ScTabSwitcher` / `ScSelectionPillGroup` |
167
+ | Infinite scroll / "Load more" | no DS component — build it app-local |
168
+ | Numbered page links (`1 2 3 … 17`) | not this component; it's a single input + arrows |
169
+ | Choosing the page size ("20 / 50 / 100 per row") | `ScSelect` beside it; `itemsPerPage` here is read-only input |
170
+ | Client-side paging of an in-memory array | works, but you must derive your slice from the offset yourself |
171
+
172
+ ### Don't confuse with
173
+
174
+ | You may actually want | Not this |
175
+ |---|---|
176
+ | `ScCounter` — the −/value/+ stepper with clamp bounds, for forms | `ScPagination` reports an *offset* and calls a refetch |
177
+ | `ScOnlyField` — a plain input | This input is pre-wired with clamping and blur-commit |
178
+ | Catalogix's `app/components/Pagination` — the wrapper that adds the page margin | `ScPagination` is the bare DS part |
179
+
180
+ ---
181
+
182
+ ## 4. Why to use it
183
+
184
+ - **The clamp and offset arithmetic are in one place.** Typing `999` snaps to
185
+ `maxPage`, typing `0` snaps to `1`, and the offset is always
186
+ `(page - 1) * perPage`. Every app that recomputed that inline got the off-by-one
187
+ wrong at least once.
188
+ - **`maxPage` handles the partial last page.** `itemsCount % perPage ? floor(…) + 1
189
+ : itemsCount / perPage` — 401 items at 100/page is 5 pages, not 4.
190
+ - **The number input is already de-chromed and tokenised.** Native spinners removed
191
+ (`-webkit-appearance: none`, `-moz-appearance: textfield`), focus outline removed,
192
+ 60px wide, centred, on `--alias-surface-canvasbase` with primary text — so it
193
+ reads as a pill, not a form field, in both themes.
194
+ - **Prop-compatible with the Catalogix original,** which is why the migration was a
195
+ pass-through wrapper rather than a rewrite of every table.
196
+
197
+ ---
198
+
199
+ ## Gotchas
200
+
201
+ **1. Six required props.** There are no defaults. Omitting any is a type error, and
202
+ `setPage` without `setOffsetValue` + `onPageChangeAction` gives you a pager that
203
+ changes the number and never fetches anything.
204
+
205
+ **2. It reports an offset, not a page.** Your data layer must take `offset`. If it
206
+ takes `page`, convert: `page = offset / perPage + 1`.
207
+
208
+ **3. Enter does nothing.** Commit is `onBlur` only — there is no `onKeyDown`. Users
209
+ who type a number and hit Enter see nothing happen until they click away. Consider
210
+ wrapping the input's container with your own Enter handling if that matters, or tell
211
+ users to use the arrows.
212
+
213
+ **4. Emptying the input commits a negative offset.** `handlePageValue` passes `""`
214
+ straight through to `setPage`, then `commit` computes
215
+ `(Number("") - 1) * perPage === -perPage`. Clamp in your setter:
216
+ `setOffsetValue={(o) => setOffset(Math.max(0, o))}`.
217
+
218
+ **5. `page` is `number | string` for a reason.** Typing yields the raw string
219
+ (`"12"`), the arrows yield a number. Never do arithmetic on it without `Number()`,
220
+ and type your state as `number | string`.
221
+
222
+ ```tsx
223
+ // WRONG — string concatenation on some renders
224
+ const nextOffset = (page - 1) * PER_PAGE;
225
+
226
+ // RIGHT
227
+ const nextOffset = (Number(page) - 1) * PER_PAGE;
228
+ ```
229
+
230
+ **6. `itemsPerPage` falls back to 1, silently.** `parseInt(String(v), 10) || 1`, so
231
+ `0`, `undefined`-coerced values or `"all"` make `maxPage === itemsCount`. Never pass
232
+ a sentinel value here.
233
+
234
+ **7. `itemsCount = 0` gives `maxPage = 0`.** The input clamps typed values to `0`
235
+ and the label reads "of 0". Hide the pager when there are no results.
236
+
237
+ **8. The arrows have no disabled state.** At page 1 and at `maxPage`, `step()` is a
238
+ no-op but the chevrons keep their `cursor: pointer` and full-contrast colour. Users
239
+ get no signal that they are at an end. There is no prop to change this.
240
+
241
+ **9. Both arrows are the same icon.** `SiconRight` twice; `.leftArrow` applies
242
+ `transform: rotate(180deg)`. If you override the icon colour or add your own
243
+ transform via `className`, you will flip the wrong one.
244
+
245
+ **10. Almost no a11y.** The chevrons are bare SVGs with `onClick` — no `<button>`,
246
+ no `role`, no `tabIndex`, no `aria-label`. Keyboard users can only reach the number
247
+ input. The `of {maxPage}` text has no `aria-live`, so a page change is not announced.
248
+
249
+ **11. Hardcoded English `of {maxPage}`.** No label prop. Do not use in a localised
250
+ surface without changing the DS.
251
+
252
+ **12. It always spans the full width and right-aligns.** `width: 100%` +
253
+ `justify-content: flex-end` on the root, plus `overflow: auto` on the inner section.
254
+ Positioning is the parent's job via margin (which is exactly what the Catalogix
255
+ wrapper adds).
256
+
257
+ **13. It does not reset on filter change.** Change a filter and you keep page 7 of a
258
+ now-2-page result set. Reset `page` and `offset` yourself.
259
+
260
+ ---
261
+
262
+ ## In the wild
263
+
264
+ ```jsx
265
+ // catalogix/dashboard app/components/Pagination/index.jsx:12
266
+ export default function Pagination(props) {
267
+ return (
268
+ <div style={{ margin: "24px 24px 18px 0" }}>
269
+ <ScPagination {...props} />
270
+ </div>
271
+ );
272
+ }
273
+ // props = { itemsCount, itemsPerPage, page, setPage, setOffsetValue, onPageChangeAction }
274
+ ```
275
+
276
+ ---
277
+
278
+ ## Related
279
+
280
+ - `ScTableHeader` / `ScTableList` — the table this sits under.
281
+ - `ScCatalogixStoreHeader` / `ScCatalogixStoreTableList` — the Catalogix stores table.
282
+ - `ScCounter` — the form-field stepper, for when you wanted a quantity not a page.
283
+ - `ScOnlyField` — a bare input, if you only need the number box.
284
+ - `ScSelect` — pair it beside the pager for a page-size picker (not built in).
@@ -0,0 +1,287 @@
1
+ ---
2
+ component: ScPairtext
3
+ package: "@streamoid/ui"
4
+ category: forms
5
+ status: stable
6
+ renders: div
7
+ tags: [icon-text, label-row, checkbox-row, radio-row, option-row, meta-row, atom]
8
+ related: [ScCheckField, ScCheckbox, ScRadio, ScBadges, ScCatalogixInvite, ScWorkspaceCard]
9
+ do_not_confuse_with: [ScCheckField, ScCheckbox, ScRadio, ScSelection, ScSelectionPill, ScMenuOptions]
10
+ ---
11
+
12
+ # ScPairtext
13
+
14
+ **One row: a leading (or trailing) icon / checkbox / radio, paired with a single
15
+ line of text.** It is the atom the DS uses for option rows and icon-and-label meta
16
+ rows — `ScCheckField`, `ScCatalogixInvite`'s store list, `ScWorkspaceCard`,
17
+ `ScPlanDetailsCard`, `ScCreditsUsageCard` and `ScBillingHistoryTableList` are all
18
+ built from it.
19
+
20
+ Despite the name it is **not** a key→value pair: there is exactly one text string.
21
+
22
+ ## TL;DR for agents
23
+
24
+ - **Reach for it when:** you are composing a DS component and need the standard
25
+ "control + label" or "icon + label" row, and you want it to line up with the
26
+ rest of the library.
27
+ - **Don't reach for it when:** you are building a form in a host app — use
28
+ `ScCheckField` (label + checkbox list) or `ScCheckbox`/`ScRadio` directly, both
29
+ of which have real props and a11y.
30
+ - **Five things that will bite you:**
31
+ 1. `content` defaults to the literal string `"Pairtext"`. Forget it and that
32
+ word ships (it does today, in `ScCheckField`'s and `ScCatalogixInvite`'s
33
+ placeholder rows).
34
+ 2. `icon` is only rendered when `type="icon"`. With `radio`/`checkbox` it is
35
+ silently ignored.
36
+ 3. The root **always** has `cursor: pointer` and **always** calls
37
+ `onChange(!checked)` on click — even for a read-only `type="icon"` display row.
38
+ 4. Extra props are **dropped**. No `style`, no `id`, no `data-*`, no `aria-*`,
39
+ no `onClick`, no `onMouseEnter` — `className` is the entire escape hatch.
40
+ 5. No a11y. It is a `div` with an `onClick`; there is no `role="checkbox"`,
41
+ no `aria-checked`, no `tabIndex`, no keyboard.
42
+
43
+ ---
44
+
45
+ ## 1. How to use it
46
+
47
+ ### Import
48
+
49
+ ```tsx
50
+ import { ScPairtext } from "@streamoid/ui";
51
+ import "@streamoid/ui/dist/index.css"; // once, at your app root
52
+ ```
53
+
54
+ ### Minimal usage
55
+
56
+ ```tsx
57
+ // icon + label (display)
58
+ <ScPairtext icon={<SiconTeam />} content="12 members" />
59
+
60
+ // checkbox + label (interactive)
61
+ <ScPairtext type="checkbox" content="Can use credits" checked={canUse} onChange={setCanUse} />
62
+ ```
63
+
64
+ ### Props
65
+
66
+ `ScPairtext` does **not** extend `HTMLAttributes` and does **not** spread extra
67
+ props onto the root.
68
+
69
+ | Prop | Type | Default | Notes |
70
+ |---|---|---|---|
71
+ | `content` | `string` | `"Pairtext"` | ⚠️ Real default. The single text line. `flex: 1`, so it fills the row. Wraps — it does not truncate. |
72
+ | `icon` | `JSX.Element` | `<SiconHome />` | ⚠️ Real default. **Only rendered when `type="icon"`.** Not size-normalised or recoloured. |
73
+ | `iconPosition` | `"left"` \| `"right"` | `"left"` | Which side the icon/control sits on. `"left"` → control then text; `"right"` → text then control. |
74
+ | `type` | `"icon"` \| `"radio"` \| `"checkbox"` | `"icon"` | What the leading/trailing element is. `radio` renders `ScRadio`, `checkbox` renders `ScCheckbox`. |
75
+ | `checked` | `boolean` | – | Drives the control's `state` (`checked ? "selected" : "default"`) and is forwarded to `ScCheckbox`. |
76
+ | `onChange` | `(checked: boolean) => void` | – | Called with `!checked` on **any** click on the row. |
77
+ | `className` | `string` | – | Appended to the root class list. The only styling hook. |
78
+
79
+ #### What renders for each `type`
80
+
81
+ | `type` | Leading/trailing element | `icon` used? | `checked` used? |
82
+ |---|---|---|---|
83
+ | `"icon"` | your `icon` (or the default home icon) | yes | only to compute a state nothing renders |
84
+ | `"radio"` | `ScRadio state={checked ? "selected" : "default"}` | **no** | yes (visual only) |
85
+ | `"checkbox"` | `ScCheckbox state={…} checked={checked}` | **no** | yes |
86
+
87
+ ### Recipes
88
+
89
+ ```tsx
90
+ // Meta row inside a card: icon + value (the ScWorkspaceCard idiom)
91
+ <ScPairtext icon={<SiconCrown />} content={ownerEmail} />
92
+
93
+ // Checkbox option row with the box on the RIGHT (the store-access idiom)
94
+ <ScPairtext
95
+ type="checkbox"
96
+ iconPosition="right"
97
+ content={store.name}
98
+ checked={selectedStores.includes(store.id)}
99
+ onChange={() => toggleStore(store.id)}
100
+ />
101
+
102
+ // A radio group — YOU own exclusivity; ScPairtext has no name/group concept
103
+ {plans.map((p) => (
104
+ <ScPairtext
105
+ key={p.id}
106
+ type="radio"
107
+ content={p.label}
108
+ checked={p.id === selectedPlan}
109
+ onChange={() => setSelectedPlan(p.id)} // never rely on the boolean argument
110
+ />
111
+ ))}
112
+
113
+ // In a host app, prefer the field wrapper over hand-stacking rows
114
+ <ScCheckField
115
+ label="Permission"
116
+ checkboxes={[
117
+ { label: "Can Use Credits", checked: canUseCredits, onChange: setCanUseCredits },
118
+ { label: "Can Invite User", checked: canInviteUser, onChange: setCanInviteUser },
119
+ ]}
120
+ />
121
+ ```
122
+
123
+ ---
124
+
125
+ ## 2. Where to use it
126
+
127
+ Inside other components. Current internal render sites in `@streamoid/ui`:
128
+
129
+ | Parent | How it is used |
130
+ |---|---|
131
+ | `ScCheckField` | one `type="checkbox"` row per entry in `checkboxes` |
132
+ | `ScCatalogixInvite` | the store-access list (`type="checkbox"`, `iconPosition="right"`) |
133
+ | `ScWorkspaceCard` | two `type="icon"` meta rows (member count, owner email) |
134
+ | `StreamoidWorkspaceSwitcher` (`SC-WorkspaceSwitcher`) | a workspace row |
135
+ | `ScPlanDetailsCard` | the plan header row (billing icon + plan label) |
136
+ | `ScCreditsUsageCard` | the credits header row |
137
+ | `ScBillingHistoryTableList` | the "download invoice" cell (download icon + label) |
138
+
139
+ In a host app, reach for it only when you are building a **new composite** that
140
+ must line up with those. For plain forms, `ScCheckField` already wraps it.
141
+
142
+ ---
143
+
144
+ ## 3. When to use it
145
+
146
+ ### Use it when
147
+
148
+ - You are authoring a DS component and need the canonical icon/control + label
149
+ row with the library's 8 px gap and 14 px primary text.
150
+ - You need a **checkbox or radio on the right** of its label —
151
+ `iconPosition="right"` is the cheapest way to get that alignment (the text is
152
+ `flex: 1`, so the control is pushed to the far edge).
153
+
154
+ ### Don't use it — reach for this instead
155
+
156
+ | Situation | Use instead |
157
+ |---|---|
158
+ | A labelled group of checkboxes in a form | `ScCheckField` |
159
+ | One checkbox, with `disabled` / `partial` / a custom size | `ScCheckbox` (it has `disabled`, `size`, `state="partial"`, `style`, `onClick`) |
160
+ | A radio in a real group | `ScRadio`, with exclusivity managed by you |
161
+ | A read-only status chip | `ScBadges` |
162
+ | A row in a dropdown/popup menu | `ScMenuOptions` |
163
+ | A segmented filter row | `ScSelectionPill` / `ScSelectionPillGroup` |
164
+ | An option row in the agent chat transcript | `ScSelection` / `ScSelectionList` (agent runtime) |
165
+ | A true key→value row (label left, value right) | not this — build it, or use `ScTableList` for tabular data |
166
+
167
+ ### Don't confuse with
168
+
169
+ | You may actually want | Not this |
170
+ |---|---|
171
+ | `ScCheckField` — label + a list of checkbox rows, with real props | `ScPairtext` is one row and has no label above it |
172
+ | `ScCheckbox` / `ScRadio` — the controls themselves, with `disabled`, `size`, `style` | `ScPairtext` wraps them and hides most of their API |
173
+ | A key→value display (older docs call this "paired text") | there is only **one** text prop, `content` |
174
+ | `ScSelection` — in-chat radio row with title *and* description (agent runtime) | `ScPairtext` is a dashboard/composite atom |
175
+ | `ScMenuOptions` — icon + label row built for menus | different padding, hover and click contract |
176
+
177
+ ---
178
+
179
+ ## 4. Why to use it
180
+
181
+ - **It is what the rest of the library uses**, so a new composite built from it
182
+ inherits exactly the same 8 px gap, 14 px `text-and-icons-primary` label and
183
+ vertical centring as the workspace, plan and billing cards.
184
+ - **`flex: 1` on the text** gives you right-aligned controls for free without a
185
+ `justify-content: space-between` wrapper, and `min-width: 0` means long values
186
+ don't blow the row's width out.
187
+ - **The control's visual state is derived** from `checked`, so a row can't show a
188
+ ticked box with `checked={false}`.
189
+ - **`flex-shrink: 0` on the nested control** is already set, which is the exact
190
+ rule that goes missing when these rows are hand-rolled inside a constrained
191
+ flex column.
192
+
193
+ ---
194
+
195
+ ## Gotchas
196
+
197
+ **1. `content` defaults to `"Pairtext"`.** This is live in the product today:
198
+ `ScCheckField` with no `checkboxes` and `ScCatalogixInvite` with no `stores` both
199
+ render placeholder rows reading "Pairtext".
200
+
201
+ ```tsx
202
+ // WRONG — renders the word "Pairtext"
203
+ <ScPairtext type="checkbox" checked={v} onChange={setV} />
204
+
205
+ // RIGHT
206
+ <ScPairtext type="checkbox" content="Can invite users" checked={v} onChange={setV} />
207
+ ```
208
+
209
+ **2. `icon` is ignored unless `type="icon"`.** Passing both an `icon` and
210
+ `type="checkbox"` renders only the checkbox — a mistake that exists in the DS's
211
+ own placeholder branches.
212
+
213
+ **3. Everything is clickable, always.** The root carries inline
214
+ `cursor: pointer` and an `onClick` that fires `onChange?.(!checked)` regardless of
215
+ `type`. A `type="icon"` display row therefore *looks* interactive. There is no
216
+ `disabled` and no way to turn the handler off — omitting `onChange` leaves the
217
+ pointer cursor.
218
+
219
+ **4. `onChange` receives `!checked`, which lies when `checked` is undefined.**
220
+ `!undefined === true`, so an uncontrolled row reports `true` on every click.
221
+
222
+ ```tsx
223
+ // WRONG — always logs true, even on the "second" click
224
+ <ScPairtext type="checkbox" content="Beta" onChange={(v) => save(v)} />
225
+
226
+ // RIGHT — control it, or ignore the argument
227
+ <ScPairtext type="checkbox" content="Beta" checked={beta} onChange={setBeta} />
228
+ ```
229
+
230
+ **5. Extra props are silently dropped.** The component destructures `...props` and
231
+ never spreads it, and the interface doesn't extend `HTMLAttributes`. So there is
232
+ no `style`, `id`, `data-testid`, `title`, `aria-label`, `onClick`, `onMouseEnter`
233
+ or `key`-adjacent escape hatch — only `className`. If you need any of those, wrap
234
+ it in your own element or use `ScCheckbox`/`ScRadio` directly (both accept
235
+ `style` and spread props).
236
+
237
+ **6. No accessibility.** No `role`, no `aria-checked`, no `tabIndex`, no
238
+ Enter/Space. For a genuinely accessible option list, wrap `ScCheckbox` yourself
239
+ with a real `<label>`.
240
+
241
+ **7. `radio` has no group semantics.** `ScRadio` receives only a visual `state`;
242
+ there is no `name`, so nothing stops every row in your list from looking selected.
243
+ You own exclusivity.
244
+
245
+ **8. Long text wraps, it doesn't truncate.** `.content` sets `flex: 1;
246
+ min-width: 0` but no `overflow`/`text-overflow`/`white-space`. A long store name
247
+ will grow the row's height. Truncate upstream or add a `className` rule.
248
+
249
+ **9. Your icon is not resized or recoloured.** Unlike `ScButton`, there is no
250
+ `cloneElement`; `Sicon*` defaults to `size={24}`, which is often too big for a
251
+ dense row. Pass `size` and `color` explicitly.
252
+
253
+ **10. The row gap uses a non-standard token,** `var(--gp8, 0.5rem)` rather than
254
+ `--spacing-md`. A known outlier; don't copy it into new components.
255
+
256
+ ---
257
+
258
+ ## In the wild
259
+
260
+ _No host render site found — used by the agent runtime / composed internally._
261
+
262
+ Composed internally is the accurate story here: it is rendered by seven other
263
+ `@streamoid/ui` components (see §2) and never called directly from CXO,
264
+ Catalogix, Photogenix or Artifax. Where a host *needs* this shape, the supported
265
+ entry point is `ScCheckField`:
266
+
267
+ ```tsx
268
+ // cxo-dashboard src/app/components/mobile-teams-invite-popup.tsx:150
269
+ <ScCheckField
270
+ label="Permission"
271
+ checkboxes={[
272
+ { label: "Can Use Credits", checked: canUseCredits, onChange: setCanUseCredits },
273
+ { label: "Can Invite User", checked: canInviteUser, onChange: setCanInviteUser },
274
+ ]}
275
+ className="w-full shrink-0"
276
+ />
277
+ ```
278
+
279
+ ---
280
+
281
+ ## Related
282
+
283
+ - `ScCheckField` — the host-facing wrapper: label + a list of `ScPairtext` checkbox rows.
284
+ - `ScCheckbox` / `ScRadio` — the controls, with the full API (`disabled`, `size`, `style`).
285
+ - `ScCatalogixInvite` / `ScWorkspaceCard` / `ScPlanDetailsCard` — composites that render it.
286
+ - `ScBadges` — for non-interactive status text instead of a fake-clickable row.
287
+ - `ScMenuOptions` — the icon + label row for menus.