@streamoid/ui 0.6.16 → 0.6.18
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +35 -18
- package/dist/docs/AGENTS.md +321 -0
- package/dist/docs/CreditWarningBanner.md +305 -0
- package/dist/docs/InvoiceHistoryMobile.md +222 -0
- package/dist/docs/ScAccess.md +259 -0
- package/dist/docs/ScAppCard.md +244 -0
- package/dist/docs/ScAppCardForCopilot.md +230 -0
- package/dist/docs/ScAppCardV3.md +273 -0
- package/dist/docs/ScAppField.md +308 -0
- package/dist/docs/ScAppListingCard.md +271 -0
- package/dist/docs/ScAppSwitchPanel.md +286 -0
- package/dist/docs/ScAppcardLogos.md +226 -0
- package/dist/docs/ScArtifaxInvite.md +262 -0
- package/dist/docs/ScArtifaxSidebar.md +330 -0
- package/dist/docs/ScAskAgentButton.md +307 -0
- package/dist/docs/ScBadges.md +261 -0
- package/dist/docs/ScBeacon.md +244 -0
- package/dist/docs/ScBillingHistoryHeader.md +210 -0
- package/dist/docs/ScBillingHistoryTableList.md +243 -0
- package/dist/docs/ScBillingLogsTableHeader.md +212 -0
- package/dist/docs/ScBillingLogsTableList.md +251 -0
- package/dist/docs/ScBriefCard.md +255 -0
- package/dist/docs/ScButton.md +251 -0
- package/dist/docs/ScCalendar.md +268 -0
- package/dist/docs/ScCalendarDateComps.md +264 -0
- package/dist/docs/ScCatalogixInvite.md +345 -0
- package/dist/docs/ScCatalogixSidebar.md +337 -0
- package/dist/docs/ScCatalogixStoreHeader.md +246 -0
- package/dist/docs/ScCatalogixStoreTableList.md +316 -0
- package/dist/docs/ScCheckField.md +233 -0
- package/dist/docs/ScCheckbox.md +272 -0
- package/dist/docs/ScCounter.md +235 -0
- package/dist/docs/ScCreditsUsageCard.md +247 -0
- package/dist/docs/ScCreditsUsageCardMobile.md +224 -0
- package/dist/docs/ScDefaultCard.md +269 -0
- package/dist/docs/ScDp.md +245 -0
- package/dist/docs/ScDrawer.md +318 -0
- package/dist/docs/ScFieldButton.md +255 -0
- package/dist/docs/ScFileField.md +268 -0
- package/dist/docs/ScGoogleSignIn.md +250 -0
- package/dist/docs/ScGuide.md +278 -0
- package/dist/docs/ScHDivider.md +213 -0
- package/dist/docs/ScHeader.md +222 -0
- package/dist/docs/ScImageField.md +253 -0
- package/dist/docs/ScInChatList.md +277 -0
- package/dist/docs/ScInChatMessage.md +205 -0
- package/dist/docs/ScInfoPopup.md +248 -0
- package/dist/docs/ScIntialProfileCover.md +233 -0
- package/dist/docs/ScInvoiceHistoryMobile.md +187 -0
- package/dist/docs/ScLogoUnit.md +232 -0
- package/dist/docs/ScMappingCard.md +241 -0
- package/dist/docs/ScMediaApproval.md +301 -0
- package/dist/docs/ScMediaSelect.md +310 -0
- package/dist/docs/ScMenuOptions.md +308 -0
- package/dist/docs/ScMobileBottomAction.md +252 -0
- package/dist/docs/ScMobileTopNav.md +279 -0
- package/dist/docs/ScModal.md +291 -0
- package/dist/docs/ScOnlyField.md +302 -0
- package/dist/docs/ScOnlyIcon.md +213 -0
- package/dist/docs/ScPagination.md +284 -0
- package/dist/docs/ScPairtext.md +287 -0
- package/dist/docs/ScPendingAction.md +238 -0
- package/dist/docs/ScPhtogenixInvite.md +275 -0
- package/dist/docs/ScPlanCard.md +302 -0
- package/dist/docs/ScPlanComparison.md +264 -0
- package/dist/docs/ScPlanDetailsCard.md +246 -0
- package/dist/docs/ScPlanDetailsCardMobile.md +240 -0
- package/dist/docs/ScPopUpMenu.md +224 -0
- package/dist/docs/ScProfile.md +234 -0
- package/dist/docs/ScProfileImageUpdate.md +261 -0
- package/dist/docs/ScProfileOptions.md +245 -0
- package/dist/docs/ScProfilePopup.md +396 -0
- package/dist/docs/ScProfileSettingsComp.md +250 -0
- package/dist/docs/ScProfileV2Mobile.md +216 -0
- package/dist/docs/ScProgressBar.md +267 -0
- package/dist/docs/ScQuickPrompt.md +277 -0
- package/dist/docs/ScRadio.md +228 -0
- package/dist/docs/ScReferralCardMobile.md +226 -0
- package/dist/docs/ScReferralTableHeader.md +260 -0
- package/dist/docs/ScReferralTableList.md +293 -0
- package/dist/docs/ScRole.md +226 -0
- package/dist/docs/ScRoleMobile.md +199 -0
- package/dist/docs/ScSelect.md +270 -0
- package/dist/docs/ScSelection.md +256 -0
- package/dist/docs/ScSelectionList.md +272 -0
- package/dist/docs/ScSelectionPill.md +240 -0
- package/dist/docs/ScSelectionPillGroup.md +302 -0
- package/dist/docs/ScSettingsNav.md +212 -0
- package/dist/docs/ScSettingsTabComp.md +260 -0
- package/dist/docs/ScSideBarLogoUnit.md +340 -0
- package/dist/docs/ScSidebar.md +243 -0
- package/dist/docs/ScSidebarIcons.md +232 -0
- package/dist/docs/ScSidebarMenu.md +283 -0
- package/dist/docs/ScSidebarProfile.md +231 -0
- package/dist/docs/ScSidebarSwitchMenu.md +258 -0
- package/dist/docs/ScSlider.md +194 -0
- package/dist/docs/ScStoreCard.md +252 -0
- package/dist/docs/ScStrLogo.md +253 -0
- package/dist/docs/ScStreamoidWordmark.md +302 -0
- package/dist/docs/ScSubAgent.md +226 -0
- package/dist/docs/ScTabComp.md +308 -0
- package/dist/docs/ScTabField.md +258 -0
- package/dist/docs/ScTabSwitcher.md +307 -0
- package/dist/docs/ScTableHeader.md +261 -0
- package/dist/docs/ScTableList.md +301 -0
- package/dist/docs/ScTableListMobile.md +282 -0
- package/dist/docs/ScTabs.md +268 -0
- package/dist/docs/ScTaxonomyPill.md +263 -0
- package/dist/docs/ScTextArea.md +259 -0
- package/dist/docs/ScTextField.md +324 -0
- package/dist/docs/ScThinkingStepIcon.md +249 -0
- package/dist/docs/ScTodoList.md +288 -0
- package/dist/docs/ScToggleSwitch.md +229 -0
- package/dist/docs/ScUsageHistoryMobile.md +194 -0
- package/dist/docs/ScVDivider.md +215 -0
- package/dist/docs/ScValueMappingL1.md +256 -0
- package/dist/docs/ScVersion.md +251 -0
- package/dist/docs/ScWorkspace.md +233 -0
- package/dist/docs/ScWorkspaceCard.md +234 -0
- package/dist/docs/ScWorkspaceSettingsMobile.md +265 -0
- package/dist/docs/ScWorkspaceSwitchCard.md +314 -0
- package/dist/docs/ScWorkspaceSwitchMobile.md +241 -0
- package/dist/docs/ScWorkspaceSwitchMobileV2.md +278 -0
- package/dist/docs/StreamoidSidebar.md +403 -0
- package/dist/docs/StreamoidWorkspaceSwitcher.md +307 -0
- package/dist/docs/UsageHistoryMobile.md +235 -0
- package/dist/docs/components.json +4849 -0
- package/dist/index.css +43 -37
- package/dist/index.d.mts +10 -0
- package/dist/index.d.ts +10 -0
- package/package.json +3 -2
|
@@ -0,0 +1,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.
|