@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,249 @@
1
+ ---
2
+ component: ScThinkingStepIcon
3
+ package: "@streamoid/ui"
4
+ category: chat-agent
5
+ status: stable
6
+ renders: span
7
+ tags: [chat, agent, spinner, status, step, thinking, reasoning, tick, error, glyph, inchat]
8
+ related: [ScInChatList, ScSubAgent, ScProgressBar, ScBeacon, ScTodoList]
9
+ do_not_confuse_with: [ScBeacon, ScProgressBar, ScBadges, ScInChatList]
10
+ ---
11
+
12
+ # ScThinkingStepIcon
13
+
14
+ **The 12px status glyph for one agent reasoning step.** Four mutually exclusive
15
+ inline-SVG paths in one `<span>`: a hollow ring (not started), a spinning 8-spoke
16
+ asterisk (working), a circled tick in success green (completed), a circled cross in
17
+ error red (error). Stroke colour is the only thing `state` changes.
18
+
19
+ ## TL;DR for agents
20
+
21
+ - **Reach for it when:** you are laying out your own step/progress row and need the
22
+ product's pending → working → done → failed glyph.
23
+ - **Don't reach for it when:** you want the whole step row (→ `ScInChatList`, which
24
+ renders this internally), a determinate progress bar (→ `ScProgressBar`), a live
25
+ "something changed here" dot (→ `ScBeacon`), or a status label
26
+ (→ `ScBadges`).
27
+ - **Three things that will bite you:**
28
+ 1. `state="default"` draws the ring in `--alias-border-divider` — **deliberately
29
+ almost invisible**. It reads as "not started", not as "loading".
30
+ 2. The SVG is `aria-hidden`. The glyph conveys nothing to a screen reader unless
31
+ you label the wrapper yourself.
32
+ 3. `strokeWidth` is fixed at `1.25` **in viewBox units** (viewBox `13.25`), so the
33
+ rendered stroke is `1.25 × size / 13.25` px — ~1.1px at the 12px default, ~3px at
34
+ `size={32}`. You cannot keep a hairline stroke while scaling the glyph up.
35
+
36
+ ---
37
+
38
+ ## 1. How to use it
39
+
40
+ ### Import
41
+
42
+ ```tsx
43
+ import { ScThinkingStepIcon } from "@streamoid/ui";
44
+ import "@streamoid/ui/dist/index.css"; // once, at your app root
45
+ ```
46
+
47
+ ### Minimal usage
48
+
49
+ ```tsx
50
+ <ScThinkingStepIcon state="working" />
51
+ ```
52
+
53
+ ### Props
54
+
55
+ | Prop | Type | Default | Notes |
56
+ |---|---|---|---|
57
+ | `state` | `"default"` \| `"working"` \| `"completed"` \| `"error"` | `"default"` | Selects both the path and the stroke token. The `IcListIconState` type exported from `ScInChatList` is the same union. |
58
+ | `size` | `number` | `12` | Pixels. Applied to the wrapper `<span>` (`width`/`height`) **and** the `<svg>` width/height. |
59
+ | `className` | `string` | – | Inserted between the base class and the state class. |
60
+ | `style` | `React.CSSProperties` | – | Merged **after** `width`/`height`, so `style={{ width: 20 }}` overrides `size` for the span (but not for the svg). |
61
+ | `...props` | `Omit<HTMLAttributes<HTMLSpanElement>, "children">` | – | `role`, `aria-label`, `title`, `data-*`, `onClick` spread onto the span. `children` is deliberately excluded. |
62
+
63
+ ### What renders in each state
64
+
65
+ | `state` | Glyph | Stroke token | Animation |
66
+ |---|---|---|---|
67
+ | `default` | hollow circle, r=6 | `--alias-border-divider` (`#242424`) | — |
68
+ | `working` | 8-spoke asterisk | `--alias-text-and-icons-primary` | `rotate` 360° / 1s linear infinite |
69
+ | `completed` | near-full circle + tick | `--alias-text-and-icons-success` (`#00b96b`) | — |
70
+ | `error` | circle + ✕ | `--alias-text-and-icons-error` (`#d11a1a`) | — |
71
+
72
+ `.stroke { fill: none }` on every path, and `.glyph { overflow: visible }` so the
73
+ 1.25 stroke is never clipped at the viewBox edge.
74
+
75
+ ### Recipes
76
+
77
+ ```tsx
78
+ // Map your own status enum onto the glyph
79
+ const glyphState = (s: StepStatus) =>
80
+ s === "running" ? "working" : s === "done" ? "completed" : s === "failed" ? "error" : "default";
81
+
82
+ <ScThinkingStepIcon state={glyphState(step.status)} />
83
+
84
+ // A bespoke step row (what ScInChatList does internally, in a 20×26 gutter)
85
+ <div style={{ display: "flex", alignItems: "flex-start", gap: 8 }}>
86
+ <span style={{ display: "flex", alignItems: "center", width: 20, height: 26, flexShrink: 0 }}>
87
+ <ScThinkingStepIcon state="working" />
88
+ </span>
89
+ <p>Resolving the taxonomy…</p>
90
+ </div>
91
+
92
+ // Announced to assistive tech (the svg is aria-hidden, so label the span)
93
+ <ScThinkingStepIcon state="working" role="img" aria-label="In progress" />
94
+
95
+ // Larger, for a standalone empty/loading state
96
+ <ScThinkingStepIcon state="working" size={24} />
97
+ ```
98
+
99
+ ---
100
+
101
+ ## 2. Where to use it
102
+
103
+ - **Inside `ScInChatList`**, as its `leadingIndicator` — this is where 100% of current
104
+ usage comes from. `ScInChatList` renders `<ScThinkingStepIcon state={iconState} />`
105
+ at the default 12px inside a fixed 20×26 gutter, which is what keeps a stack of step
106
+ titles optically aligned as the glyph swaps.
107
+ - **Your own step/progress rows** in the chat surface, when `ScInChatList`'s layout
108
+ (single-line truncating title + tool pill) is not what you want.
109
+ - **Anywhere a four-state indeterminate status glyph** fits: an import queue, a deploy
110
+ log, a pre-flight checklist.
111
+
112
+ If you are also going to want a title, a description and a tool pill, stop and use
113
+ `ScInChatList` instead of assembling them.
114
+
115
+ ---
116
+
117
+ ## 3. When to use it
118
+
119
+ ### Use it when
120
+
121
+ - The status is **indeterminate** (no percentage) and has exactly these four outcomes.
122
+ - The glyph sits next to text that already says what the step is — the icon is
123
+ reinforcement, not the message.
124
+
125
+ ### Don't use it — reach for this instead
126
+
127
+ | Situation | Use instead |
128
+ |---|---|
129
+ | A whole agent step row (glyph + title + description + tool pill) | `ScInChatList` |
130
+ | A "Using <sub-agent>…" chip | `ScSubAgent` |
131
+ | Determinate progress (a percentage, credits used, upload %) | `ScProgressBar` |
132
+ | A small attention dot / "new here" marker in a dashboard | `ScBeacon` |
133
+ | A textual status chip ("Active", "Failed") | `ScBadges` |
134
+ | A checkbox the user toggles | `ScTodoList` (its own 18px circle) or `ScCheckbox` |
135
+ | A button-level loading state | `ScButton`'s `loading` prop |
136
+
137
+ ### Don't confuse with
138
+
139
+ | You may actually want | Not this |
140
+ |---|---|
141
+ | `ScBeacon` — a tonal attention dot with its own tone prop | This is a four-state step glyph with fixed semantics |
142
+ | `ScProgressBar` — determinate, has a value | This is indeterminate; `working` spins forever |
143
+ | `ScInChatList` — the row that already contains this glyph | Rendering both nests two glyphs |
144
+ | `ScTodoList`'s circle — an 18px **interactive** checkbox | This glyph is decorative and inert |
145
+
146
+ ---
147
+
148
+ ## 4. Why to use it
149
+
150
+ - **Four semantic tokens, one component.** `border-divider` / `text-primary` /
151
+ `text-and-icons-success` / `text-and-icons-error` mean the glyph matches every other
152
+ status signal in the product and inverts correctly in light mode — the exact thing a
153
+ hand-rolled `#00b96b` tick fails at.
154
+ - **`prefers-reduced-motion` is already honoured.** The spin animation is disabled
155
+ under the media query, so you don't ship a vestibular-trigger spinner.
156
+ - **`transform-box: fill-box` + `transform-origin: center`** on the spinner: the
157
+ asterisk rotates about its own centre rather than the SVG origin, which is the bug
158
+ every hand-rolled SVG spinner has.
159
+ - **Optically consistent stroke.** One `1.25` stroke across all four glyphs in a shared
160
+ `13.25` viewBox means the ring, spinner, tick and cross carry identical visual
161
+ weight — swapping states doesn't make the row "jump".
162
+ - **It is the same glyph `ScInChatList` uses**, so a bespoke row you build will match
163
+ the transcript rows next to it exactly.
164
+
165
+ ---
166
+
167
+ ## Gotchas
168
+
169
+ **1. `default` is nearly invisible, on purpose.** `--alias-border-divider` is the
170
+ faintest token in the palette. If you were reaching for "loading", you want
171
+ `"working"`.
172
+
173
+ ```tsx
174
+ // WRONG — a step that is running looks like a step that hasn't started
175
+ <ScThinkingStepIcon />
176
+
177
+ // RIGHT
178
+ <ScThinkingStepIcon state="working" />
179
+ ```
180
+
181
+ **2. It announces nothing.** The `<svg>` is `aria-hidden="true"` and the span has no
182
+ role. Screen-reader users get the glyph's meaning only from adjacent text — or from a
183
+ label you add.
184
+
185
+ ```tsx
186
+ // RIGHT — when the glyph is the only signal
187
+ <ScThinkingStepIcon state="error" role="img" aria-label="Step failed" />
188
+ ```
189
+
190
+ **3. The stroke scales with `size`, and you cannot control it.** `strokeWidth="1.25"`
191
+ is in viewBox units, so the rendered stroke is `1.25 × size / 13.25` px: ~1.1px at 12,
192
+ ~2.3px at 24, ~3px at 32. There is no `strokeWidth` prop, so a large glyph gets a
193
+ proportionally heavy stroke next to UI drawn at a fixed 1.5px.
194
+
195
+ **4. `style` can desynchronise the span from the svg.** `style` is merged after
196
+ `width`/`height`, so `style={{ width: 24, height: 24 }}` resizes the box but leaves the
197
+ SVG at `size`. Use `size` to resize; use `style` only for margins/positioning.
198
+
199
+ ```tsx
200
+ // WRONG — 24px box containing a 12px glyph
201
+ <ScThinkingStepIcon state="working" style={{ width: 24, height: 24 }} />
202
+
203
+ // RIGHT
204
+ <ScThinkingStepIcon state="working" size={24} />
205
+ ```
206
+
207
+ **5. `children` is excluded from the props type.** You cannot nest anything inside it —
208
+ by design, it is a leaf.
209
+
210
+ **6. The colour is not overridable.** There is no `color` prop and the stroke is set by
211
+ a state class, so `style={{ color: … }}` does nothing (the paths use `stroke`, not
212
+ `currentColor`). To recolour, you must beat `.state-*` in specificity — which means you
213
+ probably want a different component.
214
+
215
+ **7. `completed` is a circle with a deliberate gap.** The tick path exits the ring at the top
216
+ right, so at very small sizes (`size < 10`) the open arc reads as a broken circle.
217
+ Don't go below the 12px default.
218
+
219
+ **8. Two spaces creep into the class list** when `className` is omitted
220
+ (`base + " " + "" + " " + state`). Harmless, but exact `className` assertions in
221
+ snapshot tests will see it.
222
+
223
+ ---
224
+
225
+ ## In the wild
226
+
227
+ _No host render site found — used by the agent runtime / composed internally._
228
+
229
+ It is composed **inside `ScInChatList`** rather than called directly:
230
+
231
+ ```tsx
232
+ // @streamoid/ui packages/ui/src/SC-InChatList/ScInChatList.tsx:49
233
+ <ScThinkingStepIcon state={iconState} />
234
+ ```
235
+
236
+ That row component is itself rendered by the chat runtime
237
+ (`stream-agent frontend/src/components/chat/AgentSteps.tsx:448`). If you need the
238
+ glyph directly, the natural home is a bespoke step/progress row in the same chat
239
+ surface.
240
+
241
+ ---
242
+
243
+ ## Related
244
+
245
+ - `ScInChatList` — the step row that already renders this glyph; prefer it.
246
+ - `ScSubAgent` — the delegation pill in the same steps stack.
247
+ - `ScProgressBar` — determinate progress with a value.
248
+ - `ScBeacon` — the dashboard-side attention dot.
249
+ - `ScTodoList` — its own 18px interactive check circle, not this glyph.
@@ -0,0 +1,288 @@
1
+ ---
2
+ component: ScTodoList
3
+ package: "@streamoid/ui"
4
+ category: chat-agent
5
+ status: stable
6
+ renders: div
7
+ tags: [chat, agent, todo, checklist, tasks, plan, dynamicform, add, edit, delete]
8
+ related: [ScCheckField, ScCheckbox, ScThinkingStepIcon, ScInChatList, ScMediaSelect]
9
+ do_not_confuse_with: [ScCheckField, ScCheckbox, ScInChatList, ScTableList, ScMenuOptions]
10
+ used_by: [agent]
11
+ ---
12
+
13
+ # ScTodoList
14
+
15
+ **The agent's task checklist, editable by the user.** A "`n` of `m` completed" header,
16
+ one row per item (18px circle + text + hover-revealed edit/delete), and an always-on
17
+ "add an item" input with a `+` button. Items are yours; the edit buffer is the
18
+ component's.
19
+
20
+ ## TL;DR for agents
21
+
22
+ - **Reach for it when:** the agent produced a plan or task list and the user must be
23
+ able to tick, rename, remove and add items — the `todo_list` field of the agent's
24
+ in-chat DynamicForm.
25
+ - **Don't reach for it when:** the list is read-only agent progress
26
+ (→ `ScInChatList`, one row per step), it's a form's multi-select
27
+ (→ `ScCheckField` / `ScCheckbox`), or it's tabular data (→ `ScTableList`).
28
+ - **Four things that will bite you:**
29
+ 1. The **add row always renders** — even with zero items, even with no `onAdd`
30
+ handler. There is no way to make the list read-only.
31
+ 2. The per-row **edit/delete buttons are `opacity: 0; pointer-events: none` until
32
+ `:hover`/`:focus-within`** — unreachable on touch devices.
33
+ 3. `onAdd` receives the **untrimmed** string, even though the empty check is done on
34
+ the trimmed one.
35
+ 4. `items` is fully controlled but the **edit text is internal state** — the
36
+ component won't re-render an in-progress edit if you change `items` underneath it.
37
+
38
+ ---
39
+
40
+ ## 1. How to use it
41
+
42
+ ### Import
43
+
44
+ ```tsx
45
+ import { ScTodoList } from "@streamoid/ui";
46
+ import type { IScTodoItem } from "@streamoid/ui";
47
+ import "@streamoid/ui/dist/index.css"; // once, at your app root
48
+ ```
49
+
50
+ ### Minimal usage
51
+
52
+ ```tsx
53
+ <ScTodoList
54
+ items={todos} // IScTodoItem[]
55
+ onToggle={(id) => toggle(id)}
56
+ onAdd={(content) => add(content.trim())}
57
+ />
58
+ ```
59
+
60
+ ### Props
61
+
62
+ | Prop | Type | Default | Notes |
63
+ |---|---|---|---|
64
+ | `items` | `IScTodoItem[]` | `[]` | Controlled. Rendered in array order — there is no sorting and no drag-reorder. |
65
+ | `onToggle` | `(id: string) => void` | – | Fires on the circle button. You flip `status` yourself. |
66
+ | `onEdit` | `(id: string, content: string) => void` | – | Fires on Enter **or blur** of the inline input. Receives the raw buffer, **not trimmed**. |
67
+ | `onDelete` | `(id: string) => void` | – | Fires on the trash button. No confirmation. |
68
+ | `onAdd` | `(content: string) => void` | – | Fires on Enter in the add input or on the `+` button. Receives the **untrimmed** value. |
69
+ | `addPlaceholder` | `string` | `"Add an item"` | The only overridable string in the component. |
70
+ | `className` | `string` | – | Appended after the internal class. |
71
+
72
+ `IScTodoItem`
73
+
74
+ | Field | Type | Notes |
75
+ |---|---|---|
76
+ | `id` | `string` | React key **and** the identity passed to every callback. Must be unique and stable. |
77
+ | `content` | `string` | Row text. Wraps (`word-break: break-word`). |
78
+ | `status` | `"pending"` \| `"completed"` | `"completed"` ⇒ filled circle + tick, muted text, `line-through`. Any other value renders as pending. |
79
+
80
+ ⚠️ `IScTodoListProps` does **not** extend `HTMLAttributes` — no `style`, `id`,
81
+ `data-*`, `aria-*` or `onClick` on the root. Wrap it if you need those.
82
+
83
+ ### What renders when
84
+
85
+ | Region | Condition |
86
+ |---|---|
87
+ | `"n of m completed"` header | `items.length > 0` only |
88
+ | Row edit/delete buttons | only while the row is `:hover` or `:focus-within`, and only when that row is **not** being edited |
89
+ | Inline edit input | while that row's id is the internal `editingId` |
90
+ | Add row (input + `+`) | **always** |
91
+ | `+` button enabled | only when the trimmed add-input value is non-empty |
92
+
93
+ ### Recipes
94
+
95
+ ```tsx
96
+ // The canonical DynamicForm wiring — the form owns the array
97
+ <ScTodoList
98
+ items={todoItems.map((i) => ({
99
+ id: i.id,
100
+ content: i.content,
101
+ status: i.status === "completed" ? "completed" : "pending",
102
+ }))}
103
+ onToggle={(id) => handleTodoToggle(fieldId, id)}
104
+ onEdit={(id, content) => handleTodoEdit(fieldId, id, content)}
105
+ onDelete={(id) => handleTodoDelete(fieldId, id)}
106
+ onAdd={(content) => handleTodoAdd(fieldId, content)}
107
+ addPlaceholder="Ask for follow-up changes"
108
+ />
109
+
110
+ // Reducer-style state, and trimming at the boundary (the component doesn't)
111
+ const [items, setItems] = useState<IScTodoItem[]>([]);
112
+ <ScTodoList
113
+ items={items}
114
+ onToggle={(id) =>
115
+ setItems((xs) => xs.map((x) => x.id === id
116
+ ? { ...x, status: x.status === "completed" ? "pending" : "completed" }
117
+ : x))}
118
+ onEdit={(id, content) => {
119
+ const next = content.trim();
120
+ if (!next) return; // reject empty renames yourself
121
+ setItems((xs) => xs.map((x) => (x.id === id ? { ...x, content: next } : x)));
122
+ }}
123
+ onDelete={(id) => setItems((xs) => xs.filter((x) => x.id !== id))}
124
+ onAdd={(content) => setItems((xs) => [...xs, { id: crypto.randomUUID(), content: content.trim(), status: "pending" }])}
125
+ />
126
+
127
+ // Read-only-ish: you still get the add row, so hide it in CSS.
128
+ // The add row is the LAST CHILD OF THE COMPONENT'S OWN ROOT, so you need two levels:
129
+ // .agentPlan > div > div:last-child { display: none }
130
+ <div className="agentPlan"><ScTodoList items={plan} /></div>
131
+ ```
132
+
133
+ ---
134
+
135
+ ## 2. Where to use it
136
+
137
+ - **The agent's in-chat DynamicForm**, `todo_list` field type — the only real consumer.
138
+ Live at `stream-agent packages/chat-components/src/DynamicForm.tsx`, where the agent
139
+ proposes a plan and the user edits it before submitting.
140
+ - **A plan-review step in the chat transcript**, under the reasoning steps
141
+ (`ScInChatList`) that produced it.
142
+ - Any surface where the **user negotiates a short list with the agent**. It is not a
143
+ general-purpose dashboard task list: there is no persistence, no assignee, no due
144
+ date, no reordering, no grouping.
145
+
146
+ ---
147
+
148
+ ## 3. When to use it
149
+
150
+ ### Use it when
151
+
152
+ - The list is **short** (a handful of items), flat, and fully in view.
153
+ - All four mutations — toggle, rename, delete, add — are things the user should be
154
+ able to do inline.
155
+ - The completion count is useful feedback on its own.
156
+
157
+ ### Don't use it — reach for this instead
158
+
159
+ | Situation | Use instead |
160
+ |---|---|
161
+ | Read-only agent progress, one row per step, with per-row status glyphs | `ScInChatList` |
162
+ | A labelled group of checkboxes in a form (fixed options) | `ScCheckField` |
163
+ | A single checkbox | `ScCheckbox` |
164
+ | Approving/rejecting one suggested line | `ScSelectionList` |
165
+ | Picking media thumbnails | `ScMediaSelect` |
166
+ | Tabular rows with columns and actions | `ScTableList` |
167
+ | A dropdown of actions | `ScMenuOptions` inside `ScPopUpMenu` |
168
+ | Anything needing reorder, nesting, assignees or due dates | build it — this component has none of that |
169
+
170
+ ### Don't confuse with
171
+
172
+ | You may actually want | Not this |
173
+ |---|---|
174
+ | `ScCheckField` — a **form field**: label + fixed checkbox options, no add/delete | This is a mutable list with its own add row |
175
+ | `ScInChatList` — read-only step rows with spinner/tick/cross | This has interactive circles and no "working" state |
176
+ | `ScThinkingStepIcon` — the agent's 12px status glyph | The row circle here is a bespoke 18px **button**, not that glyph |
177
+ | A `<ul>`/`<li>` list | It renders nested `div`s — no list semantics for screen readers |
178
+
179
+ ---
180
+
181
+ ## 4. Why to use it
182
+
183
+ - **Four mutations, one component, no reflow.** The edit/delete buttons keep their
184
+ layout space at `opacity: 0` and only fade in on hover, so rows never jump width when
185
+ the pointer enters — the classic hand-rolled bug.
186
+ - **The edit affordance is already correct**: `autoFocus`, Enter to commit, Escape to
187
+ cancel, blur to commit. Getting blur-vs-Escape ordering right by hand is fiddly (here
188
+ `cancelEdit` unmounts the input before blur can fire, so Escape genuinely cancels).
189
+ - **Completion is communicated twice** — muted colour *and* strikethrough — so it
190
+ survives greyscale and colour-blindness.
191
+ - **The check circle is drawn with tokens** (`--alias-fill-base-base` fill,
192
+ `--alias-text-and-icons-inverse` tick), so the "filled" circle inverts properly in
193
+ light mode instead of becoming a white dot on white.
194
+ - **The add input matches the DS field skin** (`fill-neutral-neutralplus` → `neutral`
195
+ on hover/focus, `border-subtle` → `border-default`), so the list sits correctly inside
196
+ a form built from `ScTextField`/`ScTextArea`.
197
+
198
+ ---
199
+
200
+ ## Gotchas
201
+
202
+ **1. You cannot hide the add row.** It renders unconditionally, with no `onAdd` needed.
203
+ An agent-owned, read-only plan will still show an editable input and a `+`.
204
+
205
+ ```tsx
206
+ // WRONG — assumes omitting onAdd removes the affordance
207
+ <ScTodoList items={plan} /> // still renders the input + `+` button
208
+
209
+ // RIGHT — wrap and suppress it, or don't use this component for read-only lists
210
+ <div className="readOnlyPlan"><ScTodoList items={plan} /></div>
211
+ // .readOnlyPlan > div > div:last-child { display: none }
212
+ // ^ the wrapper ^ the component root ^ .addRow
213
+ // (`.readOnlyPlan > div:last-child` would match the component's own root and hide
214
+ // the whole list — the add row is one level deeper.)
215
+ ```
216
+
217
+ **2. Row actions are hover-only.** `.actions { opacity: 0; pointer-events: none }`
218
+ until `.row:hover` / `.row:focus-within`. On a touch device there is no hover, so
219
+ **edit and delete are unreachable**. Do not use this component on a mobile surface
220
+ without overriding that rule.
221
+
222
+ **3. `onAdd` and `onEdit` hand you untrimmed strings.** `commitAdd` guards on
223
+ `newValue.trim().length === 0` but then calls `onAdd?.(newValue)` — the raw value.
224
+ Trim at your boundary or you will store `" fix colours "`.
225
+
226
+ **4. Blur commits an edit.** Clicking anywhere else while editing calls
227
+ `onEdit(id, buffer)` — including with an **empty** buffer, which will blank the item if
228
+ you don't guard. Escape is the only way to cancel.
229
+
230
+ **5. The edit buffer is internal.** `editingId`/`editValue` are `useState` inside the
231
+ component. If a websocket updates `items` mid-edit, the input keeps the stale buffer and
232
+ the next blur overwrites the fresh content. Pause external updates while `onEdit` is
233
+ pending, or key the whole component on a revision id.
234
+
235
+ **6. `id` must be stable.** It's the React key and the callback identity. Index-based
236
+ ids will misroute toggles the moment an item is deleted.
237
+
238
+ **7. No confirmation on delete.** The trash button fires `onDelete` immediately. Add
239
+ your own undo if the items matter.
240
+
241
+ **8. The header only appears when there is at least one item.** An empty list shows
242
+ just the add row — no "0 of 0 completed". Render your own empty state above it if you
243
+ want copy there.
244
+
245
+ **9. Hardcoded English.** `"{n} of {m} completed"`, and the aria-labels
246
+ `"Mark as completed"` / `"Mark as pending"` / `"Edit item"` / `"Delete item"` /
247
+ `"Add item"`. Only `addPlaceholder` is overridable. Not usable in a localised surface
248
+ without changing the DS.
249
+
250
+ **10. No root-level props.** `IScTodoListProps` doesn't extend `HTMLAttributes`, so
251
+ `style`, `id`, `data-testid` and `aria-*` are type errors. Wrap the component.
252
+
253
+ **11. Not a list element.** Rows are `div`s, not `<li>`, and there is no `role="list"`.
254
+ Screen readers get a run of buttons and spans with no count or position.
255
+
256
+ ---
257
+
258
+ ## In the wild
259
+
260
+ Rendered by the **agent runtime**, not by any of the four dashboards — the live call
261
+ site is the chat DynamicForm's `todo_list` field in the `stream-agent` repo
262
+ (`@streamoid/chat-components`):
263
+
264
+ ```tsx
265
+ // stream-agent packages/chat-components/src/DynamicForm.tsx:951
266
+ <ScTodoList
267
+ items={todoItems.map((i) => ({
268
+ id: i.id,
269
+ content: i.content,
270
+ status: i.status === "completed" ? "completed" : "pending",
271
+ }))}
272
+ onToggle={(id) => handleTodoToggle(field.id, id)}
273
+ onEdit={(id, content) => handleTodoEdit(field.id, id, content)}
274
+ onDelete={(id) => handleTodoDelete(field.id, id)}
275
+ onAdd={(content) => handleTodoAdd(field.id, content)}
276
+ addPlaceholder="Ask for follow-up changes"
277
+ />
278
+ ```
279
+
280
+ ---
281
+
282
+ ## Related
283
+
284
+ - `ScInChatList` — read-only agent step rows; use it for progress, not tasks.
285
+ - `ScCheckField` / `ScCheckbox` — the form-side checkbox vocabulary.
286
+ - `ScSelectionList` — accept/decline one suggested line instead of editing a list.
287
+ - `ScMediaSelect` — the same "pick from what the agent produced" idea, for media.
288
+ - `ScThinkingStepIcon` — the agent's status glyph, not the row circle used here.