@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,256 @@
1
+ ---
2
+ component: ScSelection
3
+ package: "@streamoid/ui"
4
+ category: chat-agent
5
+ status: stable
6
+ renders: div
7
+ tags: [chat, agent, radio, option, choice, single-select, inchat, dynamicform]
8
+ related: [ScRadio, ScSelectionList, ScHDivider, ScCheckbox, ScSelectionPill]
9
+ do_not_confuse_with: [ScSelectionPill, ScSelectionPillGroup, ScSelectionList, ScRadio, ScSelect]
10
+ used_by: [agent]
11
+ ---
12
+
13
+ # ScSelection
14
+
15
+ **One radio option row in an in-chat form.** A 20px `ScRadio` glyph on the left, a
16
+ title and an optional description on the right, and a rounded fill when active. You
17
+ stack them yourself — there is no group component.
18
+
19
+ ## TL;DR for agents
20
+
21
+ - **Reach for it when:** the agent asks the user to pick **one** of several labelled
22
+ options inside the chat transcript (the `radio` field type of the agent's
23
+ DynamicForm).
24
+ - **Don't reach for it when:** you want the dashboard's segmented tab pills
25
+ (→ `ScSelectionPill` / `ScSelectionPillGroup`, a **completely different family**),
26
+ a bare radio glyph (→ `ScRadio`), a dropdown (→ `ScSelect`), multi-select
27
+ (→ `ScCheckbox` / `ScCheckField`), or the approve/reject suggestion row
28
+ (→ `ScSelectionList`).
29
+ - **Four things that will bite you:**
30
+ 1. `ScSelection` is **not** the singular of `ScSelectionPill`, and
31
+ `ScSelectionList` is **not** a list of `ScSelection`. Three unrelated things.
32
+ 2. `title` defaults to `"Title"` and `description` to `"Description"`.
33
+ 3. It is a plain `div` — no `role="radio"`, no `tabIndex`, no keyboard, and the
34
+ inner `ScRadio` is an SVG, not an `<input>`. Screen readers get nothing.
35
+ 4. `onClick` **and** `onSelect` both fire, in that order.
36
+
37
+ ---
38
+
39
+ ## 1. How to use it
40
+
41
+ ### Import
42
+
43
+ ```tsx
44
+ import { ScSelection } from "@streamoid/ui";
45
+ import "@streamoid/ui/dist/index.css"; // once, at your app root
46
+ ```
47
+
48
+ ### Minimal usage
49
+
50
+ ```tsx
51
+ <ScSelection
52
+ title="Replace the background"
53
+ description=""
54
+ value="replace_bg"
55
+ active={choice === "replace_bg"}
56
+ onSelect={setChoice}
57
+ />
58
+ ```
59
+
60
+ ### Props
61
+
62
+ | Prop | Type | Default | Notes |
63
+ |---|---|---|---|
64
+ | `title` | `string` | `"Title"` | ⚠️ Real default. Primary label, `text-primary`, 14/20, wraps (`word-break: break-word`). |
65
+ | `description` | `string` | `"Description"` | ⚠️ Real default. Second line, `text-muted`. **Falsy ⇒ not rendered**, so pass `""` to suppress it. |
66
+ | `active` | `boolean` | `false` | Fills the row with `--alias-fill-neutral-neutral` and switches `ScRadio` to `state="selected"`. |
67
+ | `value` | `string` | – | Echoed back to `onSelect`. Omit and you get `onSelect(undefined)`. |
68
+ | `onSelect` | `(value?: string) => void` | – | Fires **after** `onClick`, with `value`. The handler you normally want. |
69
+ | `onClick` | `MouseEventHandler<HTMLDivElement>` | – | Native click on the row, fires **first**. Both run. |
70
+ | `className` | `string` | – | Appended after the internal classes. |
71
+ | `...props` | `Omit<HTMLAttributes<HTMLDivElement>, "onSelect">` | – | `style`, `id`, `data-*`, `aria-*`, `role`, `tabIndex` spread onto the root. |
72
+
73
+ ### Recipes
74
+
75
+ ```tsx
76
+ // The canonical in-chat radio group: you own the state and the dividers
77
+ <div style={{ display: "flex", flexDirection: "column", gap: 4 }}>
78
+ {options.map((option, idx) => (
79
+ <React.Fragment key={option.id}>
80
+ {idx > 0 && <ScHDivider />}
81
+ <ScSelection
82
+ value={option.id}
83
+ active={selected === option.id}
84
+ onSelect={(v) => setSelected(v as string)}
85
+ title={option.label}
86
+ description={option.description ?? ""} // "" hides the second line
87
+ />
88
+ </React.Fragment>
89
+ ))}
90
+ </div>
91
+
92
+ // Title-only rows
93
+ <ScSelection title="Square (1:1)" description="" value="1:1" active={ratio === "1:1"} onSelect={setRatio} />
94
+
95
+ // Patching in the a11y the component doesn't provide
96
+ <div role="radiogroup" aria-label="Aspect ratio">
97
+ {ratios.map((r) => (
98
+ <ScSelection
99
+ key={r}
100
+ role="radio"
101
+ tabIndex={0}
102
+ aria-checked={ratio === r}
103
+ onKeyDown={(e) => { if (e.key === " " || e.key === "Enter") { e.preventDefault(); setRatio(r); } }}
104
+ title={r}
105
+ description=""
106
+ value={r}
107
+ active={ratio === r}
108
+ onSelect={setRatio}
109
+ />
110
+ ))}
111
+ </div>
112
+ ```
113
+
114
+ ---
115
+
116
+ ## 2. Where to use it
117
+
118
+ - **The agent's in-chat DynamicForm**, `radio` field type — the only real consumer.
119
+ Live at `stream-agent packages/chat-components/src/DynamicForm.tsx`, which stacks
120
+ them with a `ScHDivider` between each row.
121
+ - Any **chat-surface** "pick one" prompt where each option needs a sentence of
122
+ explanation and the row should be tappable end-to-end.
123
+
124
+ It composes `ScRadio` internally (scaled from its native 24px to 20px by a class
125
+ override). Do not nest your own radio inside it.
126
+
127
+ ---
128
+
129
+ ## 3. When to use it
130
+
131
+ ### Use it when
132
+
133
+ - The choice happens **inside the chat transcript**, not in a dashboard form.
134
+ - Each option benefits from a **description line** under its label.
135
+ - The whole row should be the hit target, with a filled active state.
136
+
137
+ ### Don't use it — reach for this instead
138
+
139
+ | Situation | Use instead |
140
+ |---|---|
141
+ | Segmented tab/filter pills in a dashboard | `ScSelectionPill` + `ScSelectionPillGroup` |
142
+ | Just the radio glyph, in your own row layout | `ScRadio` |
143
+ | A dropdown of many options | `ScSelect` |
144
+ | Multi-select (more than one active) | `ScCheckbox` / `ScCheckField` |
145
+ | A segmented single-choice **field** in a dashboard form | `ScTabField` |
146
+ | Approving/rejecting an agent's *suggested values* | `ScSelectionList` |
147
+ | Picking media thumbnails | `ScMediaSelect` |
148
+ | A generic titled option card outside chat | `ScDefaultCard` |
149
+
150
+ ### Don't confuse with
151
+
152
+ | You may actually want | Not this |
153
+ |---|---|
154
+ | `ScSelectionPill` / `ScSelectionPillGroup` — dashboard segmented pills, real `<button aria-pressed>` | `ScSelection` is an in-chat row with a radio glyph |
155
+ | `ScSelectionList` — a single approve/reject row for a comma-joined suggestion string | Not a list of `ScSelection`; the names lie |
156
+ | `ScRadio` — the 24px glyph alone, `default`/`selected` | `ScSelection` is the glyph plus the labelled row |
157
+ | `ScSelect` — a real dropdown | Unrelated despite the prefix |
158
+
159
+ The `ScSelection*` (no "Pill") names belong to the **chat/agent runtime**;
160
+ `ScSelectionPill*` belongs to the **dashboards**. This is the single biggest source
161
+ of wrong picks in the library.
162
+
163
+ ---
164
+
165
+ ## 4. Why to use it
166
+
167
+ - **The active fill is a token, not a grey.** `--alias-fill-neutral-neutral` keeps the
168
+ selected row visible in light mode, where a hand-rolled `#1a1a1a` inverts to
169
+ invisible or to a black slab.
170
+ - **The radio glyph is the product's, not the browser's.** A native
171
+ `<input type="radio">` cannot be styled to match, and the DS glyph already flips
172
+ its fill between `text-primary` and `text-muted` per theme.
173
+ - **Optical sizing is handled.** `ScRadio` ships at 24px; this row overrides it to
174
+ 20px and forces the SVG to fill the box, so the glyph lines up with a 14/20 title
175
+ without magic numbers at your call site.
176
+ - **One hit target.** Padding, radius and the 16px gap between glyph and text match
177
+ the rest of the in-chat family, so a form built from these components measures
178
+ correctly against `ScInChatMessage` and `ScSelectionList`.
179
+
180
+ ---
181
+
182
+ ## Gotchas
183
+
184
+ **1. `title` and `description` have real content defaults.**
185
+
186
+ ```tsx
187
+ // WRONG — renders the literal words "Title" and "Description"
188
+ <ScSelection value="a" active />
189
+
190
+ // RIGHT
191
+ <ScSelection value="a" active title="Use my brand palette" description="" />
192
+ ```
193
+
194
+ **2. `description=""` is how you hide the second line.** `description` is rendered
195
+ `{description && …}` — falsy is dropped, `undefined` falls back to the default
196
+ `"Description"`. So *omitting* it shows text; passing `""` removes it.
197
+
198
+ **3. `onClick` and `onSelect` both fire, in that order.** Wiring the same setter to
199
+ both runs it twice.
200
+
201
+ ```tsx
202
+ // WRONG — setChoice called twice per click
203
+ <ScSelection value="a" onClick={() => setChoice("a")} onSelect={setChoice} />
204
+
205
+ // RIGHT
206
+ <ScSelection value="a" onSelect={setChoice} />
207
+ ```
208
+
209
+ **4. `onSelect` yields `undefined` when `value` is unset.** Always pass `value`.
210
+
211
+ **5. It is fully controlled.** No internal state — omit `active` and no row ever looks
212
+ selected, and nothing enforces that only one row is active. Exclusivity is your job.
213
+
214
+ **6. No a11y at all.** The root is a `div` with an `onClick`: no `role`, no
215
+ `tabIndex`, no `aria-checked`, and `ScRadio` renders an SVG rather than an input. It
216
+ is keyboard-inoperable out of the box. Add `role="radio"` / `tabIndex` /
217
+ `aria-checked` / `onKeyDown` yourself (see the third recipe), and wrap the stack in
218
+ `role="radiogroup"`.
219
+
220
+ **7. There is no `disabled`.** `cursor: pointer` is unconditional. To disable a row
221
+ you must dim and block it yourself via `className` (`opacity` + `pointer-events: none`).
222
+
223
+ **8. There is no group component.** Unlike `ScSelectionPillGroup`, nothing wires
224
+ exclusivity, dividers or keyboard arrow navigation. Map and manage state yourself.
225
+
226
+ **9. `width: 100%`** — the row always fills its parent. Constrain the parent, not
227
+ the row.
228
+
229
+ ---
230
+
231
+ ## In the wild
232
+
233
+ Rendered by the **agent runtime**, not by any of the four dashboards — the live call
234
+ site is the chat DynamicForm's `radio` field in the `stream-agent` repo
235
+ (`@streamoid/chat-components`):
236
+
237
+ ```tsx
238
+ // stream-agent packages/chat-components/src/DynamicForm.tsx:842
239
+ <ScSelection
240
+ value={option.id}
241
+ active={((formValues[field.id] as string) || "") === option.id}
242
+ onSelect={(v) => handleFieldChange(field.id, v as string)}
243
+ title={option.label}
244
+ description={option.description ?? ""}
245
+ />
246
+ ```
247
+
248
+ ---
249
+
250
+ ## Related
251
+
252
+ - `ScRadio` — the glyph this row composes; use it alone for a custom row.
253
+ - `ScHDivider` — what the runtime puts between stacked rows.
254
+ - `ScSelectionList` — the other in-chat "selection" component: approve/reject, not pick-one.
255
+ - `ScSelectionPill` / `ScSelectionPillGroup` — the unrelated dashboard segmented family.
256
+ - `ScSelect` / `ScCheckbox` / `ScCheckField` — dropdown and multi-select alternatives.
@@ -0,0 +1,272 @@
1
+ ---
2
+ component: ScSelectionList
3
+ package: "@streamoid/ui"
4
+ category: chat-agent
5
+ status: stable
6
+ renders: div
7
+ tags: [chat, agent, approve, reject, suggestion, toggle, inchat, review, accept]
8
+ related: [ScSelection, ScInChatList, ScMediaApproval, ScTodoList]
9
+ do_not_confuse_with: [ScSelection, ScSelectionPill, ScSelectionPillGroup, ScTodoList, ScMediaApproval]
10
+ ---
11
+
12
+ # ScSelectionList
13
+
14
+ **One approve-or-reject row for a suggestion the agent made.** Two 20px circular
15
+ toggles (✓ and ✕) on the left, then a label and the suggested values in a rounded
16
+ pill: `"Suggested Colours: "` + `"Pink, Green, Red, Blue"`. Approving fills the tick;
17
+ rejecting fills the cross red and strikes the text through.
18
+
19
+ ## TL;DR for agents
20
+
21
+ - **Reach for it when:** the agent proposes a value (or a comma-joined set of values)
22
+ and the user must accept or decline **that one line**.
23
+ - **Don't reach for it when:** the user picks one of several options
24
+ (→ `ScSelection`), the suggestion is media (→ `ScMediaApproval` / `ScMediaSelect`),
25
+ the items are an editable checklist (→ `ScTodoList`), or you want dashboard
26
+ segmented pills (→ `ScSelectionPill`, unrelated family).
27
+ - **Four things that will bite you:**
28
+ 1. It is **not a list**. One row, one label, one values string. `optionValues` is a
29
+ `string`, not `string[]` — you join it yourself.
30
+ 2. Both text spans are `white-space: nowrap` with `flex-shrink: 0`, so a long
31
+ values string **overflows** rather than wrapping.
32
+ 3. Every default is Catalogix demo copy: `label = "Suggested Colours: "`,
33
+ `optionValues = "Pink, Green, Red, Blue"`.
34
+ 4. Fully controlled and **toggle-aware**: clicking the already-active toggle emits
35
+ `onChange("default")` — a revoke, not a no-op.
36
+
37
+ ---
38
+
39
+ ## 1. How to use it
40
+
41
+ ### Import
42
+
43
+ ```tsx
44
+ import { ScSelectionList } from "@streamoid/ui";
45
+ import type { SelectionListState } from "@streamoid/ui";
46
+ import "@streamoid/ui/dist/index.css"; // once, at your app root
47
+ ```
48
+
49
+ ### Minimal usage
50
+
51
+ ```tsx
52
+ const [state, setState] = useState<SelectionListState>("default");
53
+
54
+ <ScSelectionList
55
+ label="Suggested colours: "
56
+ optionValues={colours.join(", ")}
57
+ state={state}
58
+ onChange={setState}
59
+ />
60
+ ```
61
+
62
+ ### Props
63
+
64
+ | Prop | Type | Default | Notes |
65
+ |---|---|---|---|
66
+ | `state` | `SelectionListState` = `"default"` \| `"approved"` \| `"rejected"` | `"default"` | The whole visual state machine. Controlled — the component keeps no state. |
67
+ | `label` | `string` | `"Suggested Colours: "` | ⚠️ Real default, note the **trailing space** (there is no separator styling — the space is the separator). `nowrap`. |
68
+ | `optionValues` | `string` | `"Pink, Green, Red, Blue"` | ⚠️ Real default. A **single string**, rendered in one pill. Join arrays yourself. `nowrap`. |
69
+ | `onChange` | `(next: SelectionListState) => void` | – | Fires on **every** toggle click with the next state, including the revoke back to `"default"`. This is the handler to drive state with. |
70
+ | `onApprove` | `() => void` | – | Fires **only** on the positive transition into `"approved"` — not on revoke. |
71
+ | `onReject` | `() => void` | – | Fires **only** on the positive transition into `"rejected"` — not on revoke. |
72
+ | `disabled` | `boolean` | `false` | `opacity: 0.5` + `pointer-events: none` on the root, plus `disabled` on both buttons. |
73
+ | `className` | `string` | – | Appended to the root's class list (before the `state-*` class in the attribute — see Gotcha 10 for why that does *not* help you override). |
74
+ | `...props` | `Omit<HTMLAttributes<HTMLDivElement>, "onChange">` | – | `style`, `id`, `data-*`, `aria-*` spread onto the root. |
75
+
76
+ ### What renders in each state
77
+
78
+ | Region | `default` | `approved` | `rejected` |
79
+ |---|---|---|---|
80
+ | ✓ toggle | outlined, disabled-grey glyph | filled `text-primary`, inverse glyph | outlined, `opacity: 0.3` |
81
+ | ✕ toggle | outlined, disabled-grey glyph | outlined, `opacity: 0.3` | filled `fill-error-solidhover`, inverse glyph |
82
+ | Label + pill | normal | normal | `opacity: 0.5` + `line-through` |
83
+ | ✓ `aria-label` | `"Approve"` | `"Revoke approval"` | `"Approve"` |
84
+ | ✕ `aria-label` | `"Reject"` | `"Reject"` | `"Revoke rejection"` |
85
+
86
+ ### Recipes
87
+
88
+ ```tsx
89
+ // Drive state from onChange, and fire side effects from onApprove/onReject
90
+ <ScSelectionList
91
+ label="Suggested colours: "
92
+ optionValues={suggestion.values.join(", ")}
93
+ state={decisions[suggestion.id] ?? "default"}
94
+ onChange={(next) => setDecisions((d) => ({ ...d, [suggestion.id]: next }))}
95
+ onApprove={() => track("suggestion_approved", suggestion.id)}
96
+ onReject={() => track("suggestion_rejected", suggestion.id)}
97
+ />
98
+
99
+ // A stack of suggestions — you own the stack, the component owns one row
100
+ <div style={{ display: "flex", flexDirection: "column", gap: 4 }}>
101
+ {suggestions.map((s) => (
102
+ <ScSelectionList
103
+ key={s.id}
104
+ label={`${s.attribute}: `}
105
+ optionValues={s.values.join(", ")}
106
+ state={decisions[s.id] ?? "default"}
107
+ onChange={(next) => decide(s.id, next)}
108
+ />
109
+ ))}
110
+ </div>
111
+
112
+ // Locked while the decision is in flight
113
+ <ScSelectionList
114
+ label="Suggested colours: "
115
+ optionValues={values.join(", ")}
116
+ state={state}
117
+ onChange={setState}
118
+ disabled={isSubmitting}
119
+ />
120
+
121
+ // Long value sets: cap the string yourself — the pill will not wrap
122
+ <ScSelectionList
123
+ label="Suggested colours: "
124
+ optionValues={values.slice(0, 4).join(", ") + (values.length > 4 ? ` +${values.length - 4}` : "")}
125
+ state={state}
126
+ onChange={setState}
127
+ />
128
+ ```
129
+
130
+ ---
131
+
132
+ ## 2. Where to use it
133
+
134
+ - **The agent chat transcript**, under an assistant turn that proposes attribute
135
+ values, tags, colours or any other short enumerable the user should sign off on —
136
+ one row per proposal.
137
+ - Alongside `ScInChatList` (the reasoning steps that produced the suggestion) and
138
+ `ScInChatMessage` (the surrounding prose). It shares their `max-width: 600px`
139
+ measure, so a mixed stack aligns.
140
+
141
+ It composes nothing — the ✓ and ✕ are inline SVG paths, not `@streamoid/icons`.
142
+
143
+ > **Status note:** this component has **no render site anywhere yet** — not in the
144
+ > four dashboards and not in the current agent runtime. It is a built-but-unwired
145
+ > Figma part from the "InChat Default comps" set. Treat the API as unproven.
146
+
147
+ ---
148
+
149
+ ## 3. When to use it
150
+
151
+ ### Use it when
152
+
153
+ - The unit of decision is **one line of proposed values**, and the two outcomes are
154
+ accept / decline.
155
+ - The user should be able to **change their mind** — re-clicking the active toggle
156
+ clears the decision.
157
+ - The rejected state should stay visible (struck through) rather than disappearing.
158
+
159
+ ### Don't use it — reach for this instead
160
+
161
+ | Situation | Use instead |
162
+ |---|---|
163
+ | The user picks **one** of several labelled options | `ScSelection` |
164
+ | The user picks **several** items from a set | `ScCheckbox` / `ScCheckField`, or `ScMediaSelect` for thumbnails |
165
+ | The suggestion is an image or video needing feedback text | `ScMediaApproval` |
166
+ | An editable checklist of tasks | `ScTodoList` |
167
+ | Segmented tabs/filters in a dashboard | `ScSelectionPill` + `ScSelectionPillGroup` |
168
+ | A confirm/cancel pair of real buttons | two `ScButton`s (`variant="mono"` / `variant="error"`) |
169
+ | A whole mapping row with confirm/ignore and a footer | `ScMappingCard` |
170
+
171
+ ### Don't confuse with
172
+
173
+ | You may actually want | Not this |
174
+ |---|---|
175
+ | `ScSelection` — an in-chat **radio option** row (pick one of N) | This is an accept/decline row for **one** suggestion |
176
+ | `ScSelectionPill` / `ScSelectionPillGroup` — dashboard segmented pills | Unrelated family, unrelated surface |
177
+ | A component that takes an array of options | `optionValues` is one `string` in one pill |
178
+ | `ScTodoList` — per-item checkboxes the user toggles | This has no per-value toggles; the whole line is one decision |
179
+
180
+ ---
181
+
182
+ ## 4. Why to use it
183
+
184
+ - **Real button semantics for the toggles.** Both are `<button type="button">` with
185
+ `aria-pressed` tracking `state` and an `aria-label` that flips to
186
+ `"Revoke approval"` / `"Revoke rejection"` — so the revoke affordance is announced,
187
+ which a pair of clickable divs would never do.
188
+ - **The approved fill is deliberately `text-primary`, not white.** Paired with an
189
+ `text-and-icons-inverse` glyph, the tick stays legible in **both** themes; a fixed
190
+ `#fff` circle would vanish on a light background.
191
+ - **Rejected is communicated three ways** — red fill, 50% content opacity, and
192
+ strikethrough — so the decision survives colour-blindness and greyscale printing.
193
+ - **The revoke transition is already modelled.** `onChange` gets the next state,
194
+ `onApprove`/`onReject` only fire on the positive edge, so you can wire analytics
195
+ and mutations without writing the "was it already approved?" branch yourself.
196
+
197
+ ---
198
+
199
+ ## Gotchas
200
+
201
+ **1. `optionValues` is a `string`, not an array.**
202
+
203
+ ```tsx
204
+ // WRONG — type error, and would render "PinkGreenRed" if it slipped through as any
205
+ <ScSelectionList optionValues={["Pink", "Green", "Red"]} />
206
+
207
+ // RIGHT
208
+ <ScSelectionList optionValues={["Pink", "Green", "Red"].join(", ")} />
209
+ ```
210
+
211
+ **2. Nothing wraps.** `.label` and `.valuesText` are both `white-space: nowrap`, and
212
+ `.valuesPill` is `flex-shrink: 0`. A long values string overflows the row's 600px
213
+ measure and can push a chat transcript into horizontal scroll. Truncate or summarise
214
+ before passing it (see the fourth recipe).
215
+
216
+ **3. Every default is demo copy.** Omit `label`/`optionValues` and you ship
217
+ `"Suggested Colours: Pink, Green, Red, Blue"`.
218
+
219
+ **4. `label` needs its own trailing separator.** There is no gap styling between the
220
+ label text and the pill beyond an 8px flex gap; the default value ends in a space and
221
+ a colon is part of the string. Format the whole label yourself.
222
+
223
+ **5. Clicking the active toggle revokes.** This is by design but surprising.
224
+
225
+ ```tsx
226
+ // WRONG — assumes onChange only ever moves forward
227
+ onChange={(next) => submitDecision(next)} // fires submitDecision("default") on revoke
228
+
229
+ // RIGHT — treat "default" as "cleared"
230
+ onChange={(next) => (next === "default" ? clearDecision() : submitDecision(next))}
231
+ ```
232
+
233
+ **6. `onApprove` / `onReject` never fire on revoke.** If you only wire those two, a
234
+ revoke is silent — you will lose the state change. Always wire `onChange`.
235
+
236
+ **7. It keeps no state.** Omit `state` and both toggles stay outlined forever no
237
+ matter how many times the user clicks.
238
+
239
+ **8. `disabled` sets `pointer-events: none` on the root.** Tooltips, `title`
240
+ attributes and hover affordances anywhere inside the row stop working while disabled —
241
+ put any "why is this disabled?" hint on a wrapping element.
242
+
243
+ **9. There is no per-value interaction.** The values pill is inert text; the user
244
+ cannot drop one colour from the set. If they need to, this is the wrong component
245
+ (→ `ScCheckField` or `ScMediaSelect`).
246
+
247
+ **10. The state skins are two-class selectors, so a plain `className` loses.** Every
248
+ state rule is written `.scSelectionList.state-rejected …` (specificity 0-2-0); your
249
+ single custom class is 0-1-0 and will not win no matter where it sits in the class
250
+ attribute. Raise your own specificity (`.mine.mine`) or use `style` if you must
251
+ override the state skin.
252
+
253
+ ---
254
+
255
+ ## In the wild
256
+
257
+ _No host render site found — and none in the agent runtime either._
258
+
259
+ It belongs in the chat
260
+ transcript beside `ScInChatList` / `ScInChatMessage` in the `stream-agent` repo
261
+ (`@streamoid/chat-components`), most naturally as a new DynamicForm field type for
262
+ "agent proposes values, user signs off" — the sibling of the existing
263
+ `media_approval` field that renders `ScMediaApproval`.
264
+
265
+ ---
266
+
267
+ ## Related
268
+
269
+ - `ScSelection` — the other in-chat "selection" component: pick one of N options.
270
+ - `ScMediaApproval` — the same accept-with-feedback idea for generated images/video.
271
+ - `ScTodoList` — when the user must edit the items, not just accept them.
272
+ - `ScInChatList` / `ScInChatMessage` — the transcript rows this sits between.