@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,233 @@
1
+ ---
2
+ component: ScCheckField
3
+ package: "@streamoid/ui"
4
+ category: forms
5
+ status: stable
6
+ renders: div
7
+ tags: [checkbox, field, form, multi-select, permissions, label, checkbox-group, pairtext]
8
+ related: [ScCheckbox, ScPairtext, ScTabField, ScTextField, ScAppField]
9
+ do_not_confuse_with: [ScCheckbox, ScPairtext, ScTabField, ScAppField]
10
+ used_by: [cxo]
11
+ ---
12
+
13
+ # ScCheckField
14
+
15
+ **A labelled multi-select form field: caption on top, bordered box underneath with
16
+ your checkbox options in a single horizontal row.** Each option is a `ScPairtext`
17
+ (`type="checkbox"`), so the whole label+box pair is one click target. Designed to
18
+ stack with `ScTextField` / `ScTabField` inside a form.
19
+
20
+ ## TL;DR for agents
21
+
22
+ - **Reach for it when:** a form needs 2 (maybe 3) boolean options under one caption
23
+ — "Permission: Can Use Credits / Can Invite User".
24
+ - **Don't reach for it when:** you need a bare glyph (→ `ScCheckbox`), one labelled
25
+ row on its own (→ `ScPairtext`), single-choice (→ `ScTabField`), or app-permission
26
+ toggles (→ `ScAppField`).
27
+ - **Three things that will bite you:**
28
+ 1. Omit `checkboxes` and you ship **two placeholder rows labelled "Pairtext"** —
29
+ the fallback is a Figma stub, not an empty state.
30
+ 2. The container is `flex-direction: row` with **no wrapping**. It is a
31
+ side-by-side pair, not a vertical checklist. Four options squash to unreadable.
32
+ 3. `label` defaults to `"Label"` ⚠️ — forget it and that ships.
33
+
34
+ ---
35
+
36
+ ## 1. How to use it
37
+
38
+ ### Import
39
+
40
+ ```tsx
41
+ import { ScCheckField } from "@streamoid/ui";
42
+ import "@streamoid/ui/dist/index.css"; // once, at your app root
43
+ ```
44
+
45
+ ### Minimal usage
46
+
47
+ ```tsx
48
+ <ScCheckField
49
+ label="Permission"
50
+ checkboxes={[
51
+ { label: "Can Use Credits", checked: canUseCredits, onChange: setCanUseCredits },
52
+ { label: "Can Invite User", checked: canInviteUser, onChange: setCanInviteUser },
53
+ ]}
54
+ />
55
+ ```
56
+
57
+ ### Props
58
+
59
+ | Prop | Type | Default | Notes |
60
+ |---|---|---|---|
61
+ | `label` | `string` | `"Label"` | ⚠️ Real default. The muted caption above the box (`--alias-text-and-icons-tertiary`, text-sm). Fixed 1.25rem height — it does not wrap. |
62
+ | `checkboxes` | `{ label: string; checked: boolean; onChange: (checked: boolean) => void }[]` | – | One `ScPairtext` per item, in order, each `flex: 1`. Omit it and you get the placeholder — see Gotcha 1. |
63
+ | `className` | `string` | – | Concatenated onto the outer column wrapper (not the bordered box). |
64
+ | `...props` | `HTMLAttributes<HTMLDivElement>` | – | Spread onto the root: `style`, `onClick`, `id`, `data-*`, `aria-*`. |
65
+
66
+ There is **no** `disabled`, no `required`, no error/helper text, no `partial` state,
67
+ and no `name` — each item is a plain controlled boolean.
68
+
69
+ ### Recipes
70
+
71
+ ```tsx
72
+ // The CXO idiom: conditional permission field inside a mobile invite form
73
+ {role === "member" && (
74
+ <ScCheckField
75
+ label="Permission"
76
+ checkboxes={[
77
+ { label: "Can Use Credits", checked: canUseCredits, onChange: setCanUseCredits },
78
+ { label: "Can Invite User", checked: canInviteUser, onChange: setCanInviteUser },
79
+ ]}
80
+ className="w-full shrink-0"
81
+ />
82
+ )}
83
+
84
+ // Driving it from a permissions object
85
+ <ScCheckField
86
+ label="Permission"
87
+ checkboxes={PERMISSIONS.map((p) => ({
88
+ label: p.label,
89
+ checked: !!perms[p.key],
90
+ onChange: (next) => setPerms({ ...perms, [p.key]: next }),
91
+ }))}
92
+ />
93
+
94
+ // More than ~3 options: don't. Stack ScPairtext rows in your own column instead.
95
+ <div style={{ display: "flex", flexDirection: "column", gap: 8 }}>
96
+ {options.map((o) => (
97
+ <ScPairtext
98
+ key={o.key}
99
+ type="checkbox"
100
+ content={o.label}
101
+ checked={!!perms[o.key]}
102
+ onChange={(next) => setPerms({ ...perms, [o.key]: next })}
103
+ />
104
+ ))}
105
+ </div>
106
+ ```
107
+
108
+ ---
109
+
110
+ ## 2. Where to use it
111
+
112
+ - **CXO's mobile teams popups** — the "Permission" field in
113
+ `mobile-teams-invite-popup` and `mobile-teams-manage-popup`, sitting in a vertical
114
+ stack between `ScTabField` ("Role") and `ScAppField` ("App permission").
115
+ - Any **short boolean pair inside a form column**, where the caption + bordered box
116
+ must match the other DS fields' rhythm (`ScTextField`, `ScTabField`, `ScOnlyField`
117
+ all use the same caption style and `--radius-xl` container).
118
+
119
+ It composes `ScPairtext` → `ScCheckbox` internally. Nothing else in the DS renders it.
120
+
121
+ ---
122
+
123
+ ## 3. When to use it
124
+
125
+ ### Use it when
126
+
127
+ - The options are **boolean, few, and belong to one caption**.
128
+ - The value is **staged** — the user confirms with a Save/Invite button afterwards.
129
+ - You want the field to look like every other field in the same form stack.
130
+
131
+ ### Don't use it — reach for this instead
132
+
133
+ | Situation | Use instead |
134
+ |---|---|
135
+ | Just the tickable box, no caption or container | `ScCheckbox` |
136
+ | One labelled option row, laid out by you | `ScPairtext` (`type="checkbox"`) |
137
+ | More than ~3 options, or a vertical checklist | your own column of `ScPairtext` — this field does not wrap |
138
+ | Exactly one of two/three choices, same field look | `ScTabField` |
139
+ | Per-app enable toggles + store pickers (invite flows) | `ScAppField` — it owns that whole block |
140
+ | A setting that applies the moment it's flipped | `ScToggleSwitch` |
141
+ | Text input with a caption | `ScTextField` / `ScOnlyField` |
142
+
143
+ ### Don't confuse with
144
+
145
+ | You may actually want | Not this |
146
+ |---|---|
147
+ | `ScCheckbox` — the 24px glyph only | `ScCheckField` is caption + container + N rows |
148
+ | `ScPairtext` — a single label+control row (checkbox / radio / icon) | This field *renders* those rows for you |
149
+ | `ScTabField` — same caption + container chrome but **single**-choice, rendered as a segmented switcher | Both are "…Field"; only this one is multi-select |
150
+ | `ScAppField` — the CXO app-permission block (per-app toggles, store lists) | Different domain, not a generic checkbox field |
151
+
152
+ ---
153
+
154
+ ## 4. Why to use it
155
+
156
+ - **Field rhythm for free.** The caption colour/size, the `--spacing-xs` gap, the
157
+ `--radius-xl` container and the hairline `--alias-border-divider` border are the
158
+ same tokens `ScTextField` and `ScTabField` use, so a stack of mixed fields lines
159
+ up without per-call-site CSS.
160
+ - **The rows are one click target.** `ScPairtext` puts the click on the whole row and
161
+ sets `cursor: pointer`, so the label text toggles the box — something you don't get
162
+ by hand, because `ScCheckbox` has no inner `<input>` for a `<label>` to activate.
163
+ - **Even columns.** Each row is `flex: 1 !important`, so two options are always the
164
+ same width regardless of label length instead of hugging their text.
165
+ - **Theme-correct in one place.** The container background is
166
+ `--alias-surface-base`, so it stays distinct from the page surface in both themes.
167
+
168
+ ---
169
+
170
+ ## Gotchas
171
+
172
+ **1. No `checkboxes` → two rows labelled "Pairtext".** The fallback branch renders
173
+ two `ScPairtext`s with only an `icon` prop set — and `type="checkbox"` means the icon
174
+ is never drawn, so you see the default `content` string instead.
175
+
176
+ ```tsx
177
+ // WRONG — renders two unchecked rows reading "Pairtext"
178
+ <ScCheckField label="Permission" />
179
+
180
+ // RIGHT — always pass checkboxes
181
+ <ScCheckField label="Permission" checkboxes={perms} />
182
+ ```
183
+
184
+ **2. It is a horizontal row, and it does not wrap.** The container is
185
+ `display: flex; flex-direction: row; gap: 1rem` with no `flex-wrap`, and every item
186
+ is `flex: 1`. Two options is the design; three is tight; four is unusable. Long
187
+ labels do not truncate with an ellipsis — they wrap inside their own cell and grow
188
+ the box.
189
+
190
+ **3. `label` defaults to `"Label"`.** ⚠️ Always set it.
191
+
192
+ **4. Everything is controlled and per-item.** Each entry supplies its own `checked`
193
+ and `onChange`; the field keeps no state and there is no group-level `onChange`.
194
+ Forget to update your state and the box never moves.
195
+
196
+ **5. No indeterminate, no disabled.** The item shape is `checked: boolean` only, so
197
+ `ScCheckbox`'s `partial` state is unreachable here, and there is no way to grey out
198
+ a single option. Gate the whole field by conditionally rendering it (that's what CXO
199
+ does with `role === "member"`).
200
+
201
+ **6. `className` lands on the outer column, not the bordered box.** To restyle the
202
+ box itself you need a descendant selector from your own class; there is no
203
+ `boxClassName`.
204
+
205
+ **7. Row order is array order, and `key` is the index.** Reordering `checkboxes`
206
+ between renders re-labels existing rows rather than moving them. Keep the array
207
+ stable.
208
+
209
+ ---
210
+
211
+ ## In the wild
212
+
213
+ ```tsx
214
+ // cxo-dashboard src/app/components/mobile-teams-invite-popup.tsx:150
215
+ <ScCheckField
216
+ label="Permission"
217
+ checkboxes={[
218
+ { label: "Can Use Credits", checked: canUseCredits, onChange: setCanUseCredits },
219
+ { label: "Can Invite User", checked: canInviteUser, onChange: setCanInviteUser },
220
+ ]}
221
+ className="w-full shrink-0"
222
+ />
223
+ ```
224
+
225
+ ---
226
+
227
+ ## Related
228
+
229
+ - `ScPairtext` — the row this field is made of; use it directly for vertical lists.
230
+ - `ScCheckbox` — the glyph underneath, incl. the `partial` state this field can't reach.
231
+ - `ScTabField` — the single-choice sibling with identical caption/container chrome.
232
+ - `ScTextField` / `ScOnlyField` — the text fields it stacks with.
233
+ - `ScAppField` — the app-permission block that sits below it in CXO's invite forms.
@@ -0,0 +1,272 @@
1
+ ---
2
+ component: ScCheckbox
3
+ package: "@streamoid/ui"
4
+ category: forms
5
+ status: stable
6
+ renders: div
7
+ tags: [checkbox, check, tick, tri-state, indeterminate, partial, select-all, multi-select, boolean, row-selection]
8
+ related: [ScCheckField, ScPairtext, ScRadio, ScToggleSwitch]
9
+ do_not_confuse_with: [ScCheckField, ScPairtext, ScToggleSwitch, ScRadio]
10
+ used_by: [catalogix, photogenix]
11
+ ---
12
+
13
+ # ScCheckbox
14
+
15
+ **The bare checkbox glyph — a 24×24 box, nothing else.** Renders one of three
16
+ inline SVGs (empty ring / dash / tick) inside a `<div>` that toggles on click. It
17
+ carries **no label**, no native `<input>`, and no internal state.
18
+
19
+ ## TL;DR for agents
20
+
21
+ - **Reach for it when:** you need a tickable box next to content you are laying out
22
+ yourself — table-row selection, grid-cell selection, a "select all" header.
23
+ - **Don't reach for it when:** you want a labelled row (→ `ScPairtext`), a whole
24
+ labelled multi-select *field* with a bordered container (→ `ScCheckField`), an
25
+ instant-apply on/off switch (→ `ScToggleSwitch`), or single-choice (→ `ScRadio`).
26
+ - **Four things that will bite you:**
27
+ 1. `checked` **overrides `state`** — pass `checked` and `state="partial"` becomes
28
+ unreachable. Indeterminate requires `state="partial"` with `checked` omitted.
29
+ 2. It is **not an `<input>`**. No form value, no keyboard, no `role="checkbox"`,
30
+ no `aria-checked`. Wrapping it in a `<label>` does nothing.
31
+ 3. It holds **zero internal state**. If you don't move `checked`/`state`, the box
32
+ never visually changes, even though `onChange` fired.
33
+ 4. `onChange` **and** `onClick` both fire on every click (`onChange` first).
34
+
35
+ ---
36
+
37
+ ## 1. How to use it
38
+
39
+ ### Import
40
+
41
+ ```tsx
42
+ import { ScCheckbox } from "@streamoid/ui";
43
+ import "@streamoid/ui/dist/index.css"; // once, at your app root
44
+ ```
45
+
46
+ ### Minimal usage
47
+
48
+ ```tsx
49
+ <ScCheckbox checked={isSelected} onChange={setIsSelected} />
50
+ ```
51
+
52
+ ### Props
53
+
54
+ | Prop | Type | Default | Notes |
55
+ |---|---|---|---|
56
+ | `state` | `"default"` \| `"partial"` \| `"selected"` | `"default"` | The tri-state artwork. **Ignored whenever `checked` is not `undefined`.** `"partial"` = the indeterminate dash. |
57
+ | `checked` | `boolean` | – | Controlled boolean. When supplied it wins over `state` and maps to `selected`/`default` only. |
58
+ | `disabled` | `boolean` | `false` | `cursor: not-allowed` + `opacity: 0.5`, and the click handler early-returns. **Does not stop propagation** — see Gotcha 6. |
59
+ | `onChange` | `(checked: boolean) => void` | – | Fires with the *next* value. The handler you normally want. |
60
+ | `onClick` | `MouseEventHandler<HTMLDivElement>` | – | Native click, fires **after** `onChange`. Both run. |
61
+ | `size` | `number \| string` | – | Box size; a number becomes `px`. The SVG fills the box, so the artwork scales proportionally. Default box is `1.5rem` (24px) from CSS. |
62
+ | `style` | `React.CSSProperties` | – | Spread **last**, so it beats `size`, the cursor and the opacity. |
63
+ | `className` | `string` | – | Concatenated between the base and state classes. |
64
+ | `...props` | `Omit<HTMLAttributes<HTMLDivElement>, "onChange">` | – | `id`, `data-*`, `aria-*`, `title` land on the root div. |
65
+
66
+ ### What renders in each state
67
+
68
+ | Effective state | Artwork | Tokens |
69
+ |---|---|---|
70
+ | `default` | 22.5px rounded square outline, 1.5 stroke | `--alias-border-default` |
71
+ | `partial` | filled square + horizontal dash | fill `--alias-fill-base-base`, dash `--alias-text---icons-inverse` |
72
+ | `selected` | filled square + tick | fill `--alias-fill-base-base`, tick `--alias-text---icons-inverse` |
73
+
74
+ Effective state is `checked !== undefined ? (checked ? "selected" : "default") : state`.
75
+
76
+ ### Recipes
77
+
78
+ ```tsx
79
+ // Row selection in a table (the Photogenix idiom) — dense 16px box
80
+ <ScCheckbox
81
+ checked={selectedIds.has(row.id)}
82
+ onChange={() => toggle(row.id)}
83
+ style={{ width: 16, height: 16 }}
84
+ />
85
+
86
+ // Select-all header with an indeterminate middle state.
87
+ // NOTE: `checked` must be omitted, or "partial" is dropped.
88
+ <ScCheckbox
89
+ state={allSelected ? "selected" : someSelected ? "partial" : "default"}
90
+ onChange={() => (allSelected ? clearAll() : selectAll())}
91
+ />
92
+
93
+ // Your own labelled row — the label text is NOT clickable for free
94
+ <div
95
+ style={{ display: "flex", gap: 8, alignItems: "center", cursor: "pointer" }}
96
+ onClick={() => setAddToAll(!addToAll)}
97
+ >
98
+ <ScCheckbox checked={addToAll} />
99
+ <span>Add to all style codes ({styleCodes.length})</span>
100
+ </div>
101
+
102
+ // Inside a clickable row — stop the cell swallowing/duplicating the click
103
+ <td onClick={(e) => e.stopPropagation()}>
104
+ <ScCheckbox checked={sel} onChange={onToggle} />
105
+ </td>
106
+ ```
107
+
108
+ ---
109
+
110
+ ## 2. Where to use it
111
+
112
+ - **Table and grid row selection** — Photogenix's batch-detail product rows and
113
+ admin history view; Catalogix's PLP grid (16px via the local `Checkbox` wrapper).
114
+ - **"Apply to all" options in modals** — Catalogix
115
+ `app/containers/Assets/AddToProducts` ("Add to all style codes").
116
+ - **Inside the DS itself** — `ScPairtext` with `type="checkbox"` renders one, and
117
+ `ScCheckField` renders a row of `ScPairtext`s. If you need label + box, go up a
118
+ level rather than composing this by hand.
119
+
120
+ Catalogix and Photogenix both wrap it in a local shim (`app/components/Checkbox`,
121
+ Photogenix's test stub) to preserve legacy `partiallyChecked` / `onClick(next)` APIs.
122
+
123
+ ---
124
+
125
+ ## 3. When to use it
126
+
127
+ ### Use it when
128
+
129
+ - The choice is **boolean and staged** (the user confirms later with a button), or
130
+ it selects rows for a bulk action.
131
+ - You are laying out the label/row yourself and only want the glyph to be
132
+ token-correct and theme-correct.
133
+ - You need the **indeterminate** dash — this is the only DS control that has one.
134
+
135
+ ### Don't use it — reach for this instead
136
+
137
+ | Situation | Use instead |
138
+ |---|---|
139
+ | Box **plus** a text label as one clickable row | `ScPairtext` (`type="checkbox"`) |
140
+ | A labelled field: caption + bordered box + several options | `ScCheckField` |
141
+ | On/off that applies **immediately** (a setting) | `ScToggleSwitch` |
142
+ | Exactly one of N | `ScRadio`, or `ScTabField` / `ScSelectionPillGroup` for a segmented look |
143
+ | App-permission rows in an invite flow | `ScAppField` (already composes the toggles) |
144
+ | A real form control that submits with a `<form>` | a native `<input type="checkbox">` — this div never participates in form data |
145
+
146
+ ### Don't confuse with
147
+
148
+ | You may actually want | Not this |
149
+ |---|---|
150
+ | `ScCheckField` — the *field*: label + bordered container + N labelled checkboxes | `ScCheckbox` is only the 24px glyph |
151
+ | `ScPairtext` — one label+control row; `type` picks checkbox / radio / icon | `ScCheckbox` has no text at all |
152
+ | `ScToggleSwitch` — 40×24 switch, different semantics (instant apply) | Not a styling variant of this |
153
+ | `ScSelectionList` — in-chat approve/reject row (**agent runtime**) | Unrelated to forms |
154
+
155
+ ---
156
+
157
+ ## 4. Why to use it
158
+
159
+ - **It themes.** Both host apps replaced baked-dark PNG/SVG `<img>` ticks with this
160
+ because those did not invert in light mode. The three states are drawn from
161
+ `--alias-*` tokens, so they resolve in both themes with no conditionals.
162
+ - **`size` actually scales the artwork.** The inner SVG has literal `width="24"`
163
+ attributes; the CSS forces `width/height: 100%` on it so a shrunk box scales the
164
+ tick instead of letting a 24px glyph spill out of a 16px frame. Hand-rolling that
165
+ is exactly the bug the CSS comment documents.
166
+ - **A real indeterminate state**, drawn to the design, without an
167
+ `input.indeterminate` imperative ref dance.
168
+ - **One glyph everywhere.** Row selection in Photogenix, the Catalogix PLP grid and
169
+ every `ScPairtext` share one shape, radius and stroke weight.
170
+
171
+ ---
172
+
173
+ ## Gotchas
174
+
175
+ **1. `checked` beats `state`, and kills `partial`.** The moment `checked` is not
176
+ `undefined` the component ignores `state` entirely.
177
+
178
+ ```tsx
179
+ // WRONG — renders the empty box; "partial" is discarded because `checked` is defined
180
+ <ScCheckbox checked={false} state="partial" />
181
+
182
+ // RIGHT — omit `checked` when you need the dash
183
+ <ScCheckbox state="partial" onChange={selectAll} />
184
+ ```
185
+
186
+ **2. It's not an `<input>` and has no a11y wiring.** No `role`, no `aria-checked`,
187
+ no `tabIndex`, no keyboard activation, no `name`/`value`, no form participation.
188
+ Add semantics yourself if the surface needs them:
189
+
190
+ ```tsx
191
+ <ScCheckbox
192
+ checked={sel}
193
+ onChange={onToggle}
194
+ role="checkbox"
195
+ aria-checked={sel}
196
+ aria-label="Select product"
197
+ tabIndex={0}
198
+ />
199
+ ```
200
+
201
+ **3. There is no label, and `<label>` won't help.** Because there is no inner
202
+ `<input>`, wrapping the glyph and some text in a `<label>` gives you *no* click
203
+ target on the text. Put the `onClick` on the wrapper row (see Recipes) or use
204
+ `ScPairtext`.
205
+
206
+ **4. It is fully controlled with no internal state.** `onChange` firing does not
207
+ change the picture. If your handler doesn't move `checked`/`state`, nothing happens.
208
+
209
+ **5. `onChange` and `onClick` both fire, `onChange` first.** Don't wire the same
210
+ mutation to both.
211
+
212
+ ```tsx
213
+ // WRONG — toggles twice per click
214
+ <ScCheckbox checked={c} onChange={() => setC(!c)} onClick={() => setC(!c)} />
215
+
216
+ // RIGHT
217
+ <ScCheckbox checked={c} onChange={setC} />
218
+ ```
219
+
220
+ **6. `disabled` does not stop propagation.** It early-returns before *both*
221
+ `onChange` and your `onClick`, but the DOM click still bubbles — a parent row's
222
+ `onClick` (row navigation, expand) still fires. Stop it yourself if that matters.
223
+
224
+ **7. `style` wins over `size`, and over `disabled` styling.** `style` is spread
225
+ last, so `style={{ opacity: 1 }}` visually un-dims a disabled box (it stays
226
+ non-interactive). Photogenix sizes via `style={{ width: 16, height: 16 }}`; both
227
+ routes work, don't pass both.
228
+
229
+ **8. `className` is string-concatenated, not joined.** With `className` unset the
230
+ DOM class list literally contains `undefined`. Harmless in the browser, but it
231
+ shows up in snapshot tests — don't assert on the exact class string.
232
+
233
+ **9. jsdom tests need a stub.** Importing from the `@streamoid/ui` barrel pulls
234
+ `ScButton`, which touches `CSS.registerProperty` at module load. Both host repos
235
+ `vi.mock("@streamoid/ui")` with a div that surfaces `state` and forwards `onClick`
236
+ (see `catalogix/dashboard app/tests/formControls.test.jsx`).
237
+
238
+ ---
239
+
240
+ ## In the wild
241
+
242
+ ```tsx
243
+ // photogenix_v2 dashboard/client/src/components/studio/batchDetail/BatchDetailProductRow.tsx:225
244
+ <ScCheckbox
245
+ checked={state.selectedProducts.has(product.id)}
246
+ onChange={() => state.toggleSelect(product.id)}
247
+ style={{ width: 16, height: 16 }}
248
+ />
249
+ ```
250
+
251
+ ```jsx
252
+ // catalogix/dashboard app/components/Checkbox/index.jsx:31 (legacy-API shim)
253
+ <ScCheckbox
254
+ size={props.size}
255
+ state={props.partiallyChecked ? "partial" : checked ? "selected" : "default"}
256
+ style={{
257
+ ...(props.style || {}),
258
+ ...(props.disabled ? { opacity: 0.5, cursor: "not-allowed" } : {}),
259
+ }}
260
+ onClick={onClickFunc}
261
+ />
262
+ ```
263
+
264
+ ---
265
+
266
+ ## Related
267
+
268
+ - `ScPairtext` — label + checkbox/radio/icon as one clickable row; the level above this.
269
+ - `ScCheckField` — full labelled multi-select field; composes `ScPairtext`.
270
+ - `ScRadio` — the single-choice sibling glyph (presentational only, no `checked`).
271
+ - `ScToggleSwitch` — instant-apply on/off switch.
272
+ - `ScAppField` — app-permission block; already wires its own controls.