@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,213 @@
1
+ ---
2
+ component: ScHDivider
3
+ package: "@streamoid/ui"
4
+ category: layout
5
+ status: stable
6
+ renders: div
7
+ tags: [divider, separator, hairline, rule, hr, horizontal, line, section-break]
8
+ related: [ScVDivider]
9
+ do_not_confuse_with: [ScVDivider]
10
+ used_by: [cxo, photogenix, catalogix, artifax]
11
+ ---
12
+
13
+ # ScHDivider
14
+
15
+ **The horizontal hairline.** A single `<div>` that is 0.5px tall, stretched across
16
+ its parent, painted with `--alias-border-divider`. No margin, no text, no
17
+ semantics — it exists so nobody hand-rolls a `border-top` with a hex colour again.
18
+
19
+ ## TL;DR for agents
20
+
21
+ - **Reach for it when:** you need a horizontal rule between stacked sections, rows,
22
+ or card regions.
23
+ - **Don't reach for it when:** the rule runs vertically between two columns
24
+ (→ `ScVDivider`), or you need a semantic `<hr>` in prose.
25
+ - **Three things that will bite you:**
26
+ 1. `IScHDividerProps` only declares `className`. `style`, `onClick`, `data-*`
27
+ are **TypeScript errors** even though they are spread onto the div at runtime.
28
+ 2. It has **no margin**. Two sections separated by it will look cramped unless the
29
+ parent supplies the gap.
30
+ 3. In a **flex-row** parent it vanishes (0.5px tall, no width). It needs a block
31
+ or flex-column parent.
32
+
33
+ ---
34
+
35
+ ## 1. How to use it
36
+
37
+ ### Import
38
+
39
+ ```tsx
40
+ import { ScHDivider } from "@streamoid/ui";
41
+ import "@streamoid/ui/dist/index.css"; // once, at your app root
42
+ ```
43
+
44
+ ### Minimal usage
45
+
46
+ ```tsx
47
+ <ScHDivider />
48
+ ```
49
+
50
+ ### Props
51
+
52
+ | Prop | Type | Default | Notes |
53
+ |---|---|---|---|
54
+ | `className` | `string` | – | Appended after the internal class. **The only typed prop** — it is your hook for margin, colour and width overrides. |
55
+ | `...props` | *(untyped)* | – | ⚠️ Spread onto the root div at runtime, but `IScHDividerProps` does **not** extend `HTMLAttributes`, so passing `style`/`onClick`/`role`/`data-*` is a compile error. See Gotcha 1. |
56
+
57
+ ### What the stylesheet does
58
+
59
+ | Declaration | Consequence |
60
+ |---|---|
61
+ | `height: 0.5px` | A hairline, deliberately lighter than a 1px border. |
62
+ | `background-color: var(--alias-border-divider)` | Theme-correct in dark **and** light (both map to `--primitives-neutral-800`). |
63
+ | `align-self: stretch` | Full width inside a flex-column parent, without you setting `width`. |
64
+ | `flex-shrink: 0` | Survives a constrained flex column — it will not be squeezed to nothing. |
65
+
66
+ ### Recipes
67
+
68
+ ```tsx
69
+ // Between sidebar sections — the host supplies the vertical rhythm
70
+ <div className="flex flex-col gap-2">
71
+ <NavSection />
72
+ <ScHDivider />
73
+ <NavSection />
74
+ </div>
75
+
76
+ // With its own spacing, via className (Tailwind hosts do exactly this)
77
+ <ScHDivider className="my-2" />
78
+
79
+ // Inside a card, edge-to-edge under the header
80
+ <div className={styles.card}>
81
+ <CardHeader />
82
+ <ScHDivider className={styles.cardRule} />
83
+ <CardBody />
84
+ </div>
85
+ ```
86
+
87
+ ---
88
+
89
+ ## 2. Where to use it
90
+
91
+ - **Sidebars** — between the logo unit, the top items, the nav sections and the
92
+ footer. CXO's `app-sidebar`, Artifax's `DashboardSidebar` and Catalogix's
93
+ `LeftMenu` all do this.
94
+ - **Cards** — as the internal rule. Several DS cards already render one for you:
95
+ `ScCreditsUsageCard`, `ScPlanDetailsCard`, `ScStoreCard`, `ScProfileOptions`,
96
+ `ScCalendar`, `ScGuide`, `ScArtifaxSidebar`, `ScCatalogixSidebar`, `ScSidebar`.
97
+ Don't add a second one on top of theirs.
98
+ - **Menus and popovers** — between option groups (`ScProfilePopup` uses it twice).
99
+ - **Notice / locked-state panels** — between body copy and the action row.
100
+
101
+ Used by all four host apps; it's part of the universal core.
102
+
103
+ ---
104
+
105
+ ## 3. When to use it
106
+
107
+ ### Use it when
108
+
109
+ - Two stacked regions need visual separation and a full card/border would be too much.
110
+ - You want the divider colour to track the theme without writing a
111
+ `prefers-color-scheme` branch.
112
+
113
+ ### Don't use it — reach for this instead
114
+
115
+ | Situation | Use instead |
116
+ |---|---|
117
+ | The rule runs vertically between two columns | `ScVDivider` |
118
+ | You need a recolourable rule via a prop | `ScVDivider` (it has a `color` prop; this one does not) |
119
+ | Semantic content separation in prose / a11y-meaningful break | a real `<hr>`, or a `role="separator"` element |
120
+ | A section heading with a rule under it | `ScHeader` (table column label) or your own heading + this divider |
121
+ | Spacing only, with no visible line | a `gap` / margin — don't ship an invisible divider |
122
+
123
+ ### Don't confuse with
124
+
125
+ | You may actually want | Not this |
126
+ |---|---|
127
+ | `ScVDivider` — vertical, implemented as a `border-left` on a 0-width div, accepts `color` | `ScHDivider` is a 0.5px-tall background fill and takes no colour prop |
128
+ | `ScHeader` — the uppercase table **column header** label | Despite the name, `ScHeader` is not a header *bar* and draws no rule |
129
+
130
+ ---
131
+
132
+ ## 4. Why to use it
133
+
134
+ - **One token, one place.** Every rule in the product resolves
135
+ `--alias-border-divider`, so the day design changes divider contrast (it already
136
+ did once, `#ededed` → `#d2d2d2` for light-mode AA), all four apps change together.
137
+ - **It survives constrained flex columns.** `flex-shrink: 0` is the reason DS
138
+ elements don't collapse to zero inside a host's `min-h-0` scroll column — a bug
139
+ class that has bitten this codebase before.
140
+ - **`align-self: stretch` means no width plumbing.** Hand-rolled rules typically get
141
+ `width: 100%` which then fights the parent's padding.
142
+
143
+ ---
144
+
145
+ ## Gotchas
146
+
147
+ **1. The props type is not `HTMLAttributes`.** The component spreads `...props`, but
148
+ the interface declares `className` only — so the compiler rejects everything else.
149
+
150
+ ```tsx
151
+ // WRONG — TS error (Property 'style' does not exist on type 'IScHDividerProps')
152
+ <ScHDivider style={{ margin: "8px 0" }} />
153
+
154
+ // RIGHT — spacing via a class
155
+ <ScHDivider className="my-2" />
156
+ ```
157
+
158
+ **2. `className` is concatenated unconditionally.** Omitting it produces
159
+ `class="scHDivider undefined"`. Harmless in a browser, but exact-`className`
160
+ assertions in tests will fail.
161
+
162
+ **3. It disappears in a flex-row parent.** `height: 0.5px` beats `align-self:
163
+ stretch`, and there is no `width`, so a row-direction parent gives it zero area.
164
+
165
+ ```tsx
166
+ // WRONG — nothing renders between the two columns
167
+ <div style={{ display: "flex", flexDirection: "row" }}>
168
+ <Left /><ScHDivider /><Right />
169
+ </div>
170
+
171
+ // RIGHT
172
+ <div style={{ display: "flex", flexDirection: "row" }}>
173
+ <Left /><ScVDivider /><Right />
174
+ </div>
175
+ ```
176
+
177
+ **4. No margin, ever.** The parent owns the rhythm. Use the parent's `gap`, or pass
178
+ a spacing class.
179
+
180
+ **5. No `color` prop.** Its sibling `ScVDivider` has one; this doesn't. To recolour,
181
+ override `background-color` from your own class.
182
+
183
+ **6. It carries no semantics.** It is a decorative `<div>` — no `role="separator"`,
184
+ no `<hr>`. Screen readers skip it. That is usually what you want; if it isn't, use
185
+ a real `<hr>`.
186
+
187
+ **7. Don't double up on DS cards.** `ScPlanDetailsCard`, `ScCreditsUsageCard`,
188
+ `ScStoreCard`, `ScProfileOptions`, `ScGuide` and the sidebar wrappers already render
189
+ their own — adding another gives you two rules 0.5px apart.
190
+
191
+ ---
192
+
193
+ ## In the wild
194
+
195
+ ```tsx
196
+ // artifax packages/shared/src/components/DashboardSidebar.tsx:875
197
+ <ScHDivider className="my-2" />
198
+
199
+ // catalogix/dashboard app/components/LockedScreen/index.jsx:53
200
+ <ScHDivider className={styles["notice-divider"]} />
201
+
202
+ // cxo-dashboard src/app/components/app-sidebar.tsx:939
203
+ <ScHDivider />
204
+ ```
205
+
206
+ ---
207
+
208
+ ## Related
209
+
210
+ - `ScVDivider` — the vertical sibling; has a `color` prop and is a `border-left`.
211
+ - `ScHeader` — uppercase table **column** label (not a header bar, despite the name).
212
+ - `ScGuide`, `ScStoreCard`, `ScProfileOptions`, `ScCreditsUsageCard`,
213
+ `ScPlanDetailsCard` — DS components that already compose one internally.
@@ -0,0 +1,222 @@
1
+ ---
2
+ component: ScHeader
3
+ package: "@streamoid/ui"
4
+ category: layout
5
+ status: stable
6
+ renders: div
7
+ tags: [table, column-header, th, sort, sort-arrow, uppercase, label, caption]
8
+ related: [ScTableHeader, ScBillingHistoryHeader, ScBillingLogsTableHeader, ScReferralTableHeader]
9
+ do_not_confuse_with: [ScTableHeader, ScMobileTopNav, ScSideBarLogoUnit, ScHDivider]
10
+ ---
11
+
12
+ # ScHeader
13
+
14
+ **One column-header label for a table — not a page header.** It renders an uppercase
15
+ 12px muted string plus an optional decorative double-chevron. The name is misleading:
16
+ this is a `<th>`-shaped *cell label*, and it is only ever composed inside the DS's
17
+ table-header rows.
18
+
19
+ ## TL;DR for agents
20
+
21
+ - **Reach for it when:** you are building a bespoke table header row and want the
22
+ column captions to match every other table in the product.
23
+ - **Don't reach for it when:** you want the whole header row for a known table
24
+ (→ `ScTableHeader`, `ScBillingHistoryHeader`, `ScBillingLogsTableHeader`,
25
+ `ScReferralTableHeader`), a page title bar (→ your own heading), or a mobile app
26
+ bar (→ `ScMobileTopNav`).
27
+ - **Three things that will bite you:**
28
+ 1. `state` defaults to **`"up"`**, so a bare `<ScHeader text="App" />` ships a
29
+ sort chevron you didn't ask for. Every real call site passes `state="none"`.
30
+ 2. The chevrons are **decoration only** — the component does not sort and wires no
31
+ handler. `state` is *your* sort indicator to drive.
32
+ 3. `text` defaults to `"Header"`, and CSS uppercases it. Don't `.toUpperCase()` in JS.
33
+
34
+ ---
35
+
36
+ ## 1. How to use it
37
+
38
+ ### Import
39
+
40
+ ```tsx
41
+ import { ScHeader } from "@streamoid/ui";
42
+ import "@streamoid/ui/dist/index.css"; // once, at your app root
43
+ ```
44
+
45
+ ### Minimal usage
46
+
47
+ ```tsx
48
+ <ScHeader text="App" state="none" />
49
+ ```
50
+
51
+ ### Props
52
+
53
+ | Prop | Type | Default | Notes |
54
+ |---|---|---|---|
55
+ | `text` | `string` | `"Header"` | ⚠️ Has a real default. Rendered with a **trailing space** in the DOM, and uppercased by CSS. |
56
+ | `state` | `"up"` \| `"down"` \| `"none"` | `"up"` | ⚠️ Default draws `SiconDoubleArrowUp`. `"down"` draws `SiconDoubleArrowDown`. `"none"` draws no icon. Purely visual. |
57
+ | `className` | `string` | – | Appended after the internal class. |
58
+ | `...props` | `HTMLAttributes<HTMLDivElement>` | – | Spread onto the root div — this is how you supply `style={{ flex: 1 }}` / fixed widths and `onClick` for sorting. |
59
+
60
+ ### What renders in each state
61
+
62
+ | `state` | Icon | CSS variant class present? |
63
+ |---|---|---|
64
+ | `"up"` | `SiconDoubleArrowUp` (1rem) | ❌ no `.state-up` rule exists — the class resolves to `undefined` |
65
+ | `"down"` | `SiconDoubleArrowDown` (1rem) | ✅ `.scHeader.state-down .siconDoubleArrowDownInstance` |
66
+ | `"none"` | none | ❌ no `.state-none` rule exists |
67
+
68
+ ### Recipes
69
+
70
+ ```tsx
71
+ // A custom header row — column widths come from YOUR style props
72
+ <div style={{ display: "flex", gap: "var(--spacing-6xl)", alignItems: "center" }}>
73
+ <ScHeader text="App" state="none" style={{ flex: 1, alignItems: "flex-start", gap: "0.25rem" }} />
74
+ <ScHeader text="Activity" state="none" style={{ flex: 1, alignItems: "flex-start", gap: "0.25rem" }} />
75
+ <ScHeader text="Used by" state="none" style={{ flexShrink: 0, width: "7.5rem" }} />
76
+ <ScHeader text="Date" state="none" style={{ flexShrink: 0, width: "7.5rem" }} />
77
+ </div>
78
+
79
+ // Driving it as a real sort control (you own the sorting)
80
+ <ScHeader
81
+ text="Signed on"
82
+ state={sortKey === "signedOn" ? (sortDir === "asc" ? "up" : "down") : "none"}
83
+ onClick={() => toggleSort("signedOn")}
84
+ role="columnheader"
85
+ aria-sort={sortKey === "signedOn" ? (sortDir === "asc" ? "ascending" : "descending") : "none"}
86
+ style={{ cursor: "pointer", flex: 1 }}
87
+ />
88
+ ```
89
+
90
+ ---
91
+
92
+ ## 2. Where to use it
93
+
94
+ - **Inside DS table-header rows.** It is the atom of `ScTableHeader` (User / Role /
95
+ Access / Invited by / Signed On | Invite action), `ScBillingHistoryHeader`,
96
+ `ScBillingLogsTableHeader` and `ScReferralTableHeader`. If your table is one of
97
+ those, render the parent, not this.
98
+ - **In `@streamoid/settings`** — the billing "credits used" table builds its header
99
+ row from five `ScHeader`s (App / Activity / Used by / Date / Credits) with inline
100
+ flex/width styling, because the column set is screen-specific.
101
+ - **Any bespoke table** whose columns don't match an existing DS header component.
102
+
103
+ ---
104
+
105
+ ## 3. When to use it
106
+
107
+ ### Use it when
108
+
109
+ - You need a **single column caption** with the product's uppercase/muted treatment.
110
+ - You are assembling a header row whose columns are app-specific.
111
+
112
+ ### Don't use it — reach for this instead
113
+
114
+ | Situation | Use instead |
115
+ |---|---|
116
+ | The teams/users table header | `ScTableHeader` (`type="default" \| "pending"`) |
117
+ | Billing history table header | `ScBillingHistoryHeader` |
118
+ | Credit/usage logs table header | `ScBillingLogsTableHeader` |
119
+ | Referral table header | `ScReferralTableHeader` |
120
+ | Catalogix stores table header | `ScCatalogixStoreHeader` |
121
+ | A page/screen title | your own heading element — this is 12px uppercase muted, not a title |
122
+ | A mobile screen's top app bar | `ScMobileTopNav` |
123
+ | A sidebar's logo/app-switcher row | `ScSideBarLogoUnit` |
124
+ | A horizontal rule under a heading | `ScHDivider` |
125
+ | A tab strip | `ScTabs` / `ScTabSwitcher` + `ScTabComp` |
126
+
127
+ ### Don't confuse with
128
+
129
+ | You may actually want | Not this |
130
+ |---|---|
131
+ | `ScTableHeader` — the whole header **row**, composed of `ScHeader` cells | `ScHeader` is one cell |
132
+ | A page header bar (title + actions) | `ScHeader` renders no bar, no background, no actions |
133
+ | A sortable column that actually sorts | `state` is a static indicator; sorting is entirely yours |
134
+
135
+ The catalog line "Page/section header bar" is wrong — read this file, not that row.
136
+
137
+ ---
138
+
139
+ ## 4. Why to use it
140
+
141
+ - **Column captions stay identical across every table.** 12px / `line-height-xs` /
142
+ `--alias-text-and-icons-muted` / uppercase, in one place. Tables built by hand
143
+ drift within a release.
144
+ - **The sort chevrons are the DS's icons at the DS's size** (1rem, forced with
145
+ `!important`), so a sorted column looks the same in billing, teams and referrals.
146
+ - **Typed props spread**, so column sizing (`flex: 1` vs a fixed `7.5rem`) stays a
147
+ call-site decision instead of a fork of the component.
148
+
149
+ ---
150
+
151
+ ## Gotchas
152
+
153
+ **1. `state="up"` is the default.** A "plain" header is `state="none"`.
154
+
155
+ ```tsx
156
+ // WRONG — renders an up-chevron beside every caption
157
+ <ScHeader text="Credits" />
158
+
159
+ // RIGHT
160
+ <ScHeader text="Credits" state="none" />
161
+ ```
162
+
163
+ **2. It doesn't sort.** No `onClick`, no `aria-sort`, no cursor change. Add all three
164
+ yourself (props are spread, so they compile).
165
+
166
+ **3. `text` defaults to `"Header"`.** Forget the prop and you ship the placeholder.
167
+
168
+ **4. CSS uppercases the label.** `text-transform: uppercase` means `"Used by"`,
169
+ `"used by"` and `"USED BY"` all render identically. Uppercasing in JS only makes the
170
+ source harder to read (and breaks your own `getByText`).
171
+
172
+ **5. There is a trailing space in the DOM.** The JSX is `{text} `, so
173
+ `el.textContent === "App "`. Whitespace-normalising queries (testing-library's
174
+ `getByText`) are fine; strict equality assertions are not.
175
+
176
+ **6. `.state-up` and `.state-none` don't exist in the stylesheet.** Only
177
+ `.state-down` has a rule, so `className` ends up containing the literal string
178
+ `"undefined"` for the other two states. Don't use the variant class as a styling or
179
+ test hook.
180
+
181
+ **7. Omitting `className` also injects `"undefined"`** — the class string is built
182
+ with unguarded concatenation.
183
+
184
+ **8. No width, no truncation, no `title`.** It's a flex row that sizes to content.
185
+ Long captions push the row wide; column widths are the caller's job.
186
+
187
+ **9. Not a `<th>`.** It's a div with no table semantics. Inside a real `<table>`,
188
+ render it *inside* a `<th>`; otherwise add `role="columnheader"`.
189
+
190
+ ---
191
+
192
+ ## In the wild
193
+
194
+ _No host render site found — used by the agent runtime / composed internally._
195
+
196
+ It belongs inside table-header rows, and that is where it lives today — in the DS's
197
+ own header components and in the shared settings package:
198
+
199
+ ```tsx
200
+ // npm-components packages/ui/src/SC-tableHeader/ScTableHeader.tsx:22
201
+ <ScHeader text="User" state="down" className={styles.scHeaderInstance} />
202
+
203
+ // npm-components packages/settings/src/billing-content.tsx:1221
204
+ <ScHeader
205
+ text="App"
206
+ state="none"
207
+ style={{ flex: 1, alignItems: "flex-start", gap: "0.25rem" }}
208
+ />
209
+ ```
210
+
211
+ (CXO renders that settings screen, so `ScHeader` reaches production through
212
+ `@streamoid/settings` rather than through a `cxo-dashboard` call site.)
213
+
214
+ ---
215
+
216
+ ## Related
217
+
218
+ - `ScTableHeader` — the generic users/teams header row built from these.
219
+ - `ScBillingHistoryHeader` / `ScBillingLogsTableHeader` / `ScReferralTableHeader` —
220
+ billing-family header rows, also built from these.
221
+ - `ScTableList` / `ScTableListMobile` — the row components that sit under a header.
222
+ - `ScHDivider` — the rule you usually want beneath a header row.
@@ -0,0 +1,253 @@
1
+ ---
2
+ component: ScImageField
3
+ package: "@streamoid/ui"
4
+ category: forms
5
+ status: stable
6
+ renders: div
7
+ tags: [image, upload, logo, avatar, workspace-image, preview, thumbnail, form-field]
8
+ related: [ScFileField, ScProfileImageUpdate, ScDp, ScTextField]
9
+ do_not_confuse_with: [ScFileField, ScProfileImageUpdate, ScDp, ScIntialProfileCover]
10
+ used_by: [cxo]
11
+ ---
12
+
13
+ # ScImageField
14
+
15
+ **The image-upload form field.** A labelled 120px-tall panel that reads
16
+ "⬆ Click to upload" until you hand it a preview URL, then swaps to a square
17
+ thumbnail + filename + a red delete icon. It is the workspace-logo picker in CXO's
18
+ mobile settings.
19
+
20
+ ## TL;DR for agents
21
+
22
+ - **Reach for it when:** a form takes **one image** and the user should see it —
23
+ workspace logo, brand mark, cover image.
24
+ - **Don't reach for it when:** the file isn't an image or you need progress/error
25
+ (→ `ScFileField`), or it's a round profile avatar with separate upload/delete
26
+ buttons (→ `ScProfileImageUpdate`).
27
+ - **Four things that will bite you:**
28
+ 1. `label` defaults to the literal string **`"Label"`**. ⚠️ Always pass one.
29
+ 2. `accept="image/*"` is **hardcoded**. No prop. You cannot restrict to PNG.
30
+ 3. It is **controlled by `imagePreview`** — you create the blob URL (and revoke it).
31
+ 4. Both affordances are **`div`/`svg` with `onClick`**: no button, no `tabIndex`,
32
+ no `aria-label`. Mouse-only, invisible to a screen reader.
33
+
34
+ ---
35
+
36
+ ## 1. How to use it
37
+
38
+ ### Import
39
+
40
+ ```tsx
41
+ import { ScImageField } from "@streamoid/ui";
42
+ import "@streamoid/ui/dist/index.css"; // once, at your app root
43
+ ```
44
+
45
+ ### Minimal usage
46
+
47
+ ```tsx
48
+ const [file, setFile] = useState<File | null>(null);
49
+ const [preview, setPreview] = useState<string | null>(null);
50
+
51
+ <ScImageField
52
+ label="Workspace profile image"
53
+ imagePreview={preview}
54
+ imageName={file?.name}
55
+ onFileSelect={(f) => { setFile(f); setPreview(URL.createObjectURL(f)); }}
56
+ onRemove={() => { if (preview) URL.revokeObjectURL(preview); setFile(null); setPreview(null); }}
57
+ />
58
+ ```
59
+
60
+ ### Props
61
+
62
+ | Prop | Type | Default | Notes |
63
+ |---|---|---|---|
64
+ | `label` | `string` | `"Label"` | ⚠️ Has a real default — forget it and you ship the word "Label". Always rendered (unlike `ScFileField`, there is no "no label" path). |
65
+ | `imagePreview` | `string \| null` | – | **The state switch.** Truthy → preview row; falsy → upload panel. A blob URL or a remote URL. |
66
+ | `imageName` | `string \| null` | – | Filename beside the thumbnail. ⚠️ Falls back to the literal `"image"` when empty. |
67
+ | `onFileSelect` | `(file: File) => void` | – | A **single `File`**, not a `FileList`. Only fires when a file was actually chosen. The input's value is reset afterwards, so re-picking the same file fires again. |
68
+ | `onRemove` | `() => void` | – | The red trash icon in the preview row. `stopPropagation` is already called for you. |
69
+ | `className` | `string` | – | Appended after the root class. |
70
+
71
+ Extra DOM props are **not** accepted (the interface does not extend
72
+ `HTMLAttributes`) — no `style`, no `data-*`, no `id`, no `onClick`.
73
+
74
+ ### What renders in each state
75
+
76
+ | `imagePreview` | You get |
77
+ |---|---|
78
+ | falsy | 7.5rem panel, centred `SiconUpload` + "Click to upload", whole panel clickable |
79
+ | truthy | 7.5rem panel: square `object-fit: cover` thumbnail, `imageName` (ellipsised), red `SiconDelete`. **The panel itself is no longer clickable.** |
80
+
81
+ ### Recipes
82
+
83
+ ```tsx
84
+ // Editing an existing workspace: seed the preview from the server URL
85
+ <ScImageField
86
+ label="Workspace profile image"
87
+ imagePreview={newPreview ?? workspace.logoUrl ?? null}
88
+ imageName={newFile?.name ?? workspace.logoName}
89
+ onFileSelect={handleFileSelect}
90
+ onRemove={handleRemoveImage}
91
+ className="w-full shrink-0"
92
+ />
93
+
94
+ // Enforcing a stricter type / size than the hardcoded image/*
95
+ <ScImageField
96
+ label="Brand logo (PNG, under 2 MB)"
97
+ imagePreview={preview}
98
+ onFileSelect={(f) => {
99
+ if (f.type !== "image/png") return setError("PNG only");
100
+ if (f.size > 2_000_000) return setError("Under 2 MB please");
101
+ setError(null); setPreview(URL.createObjectURL(f));
102
+ }}
103
+ onRemove={clear}
104
+ />
105
+ {error && <span style={{ color: "var(--alias-text-and-icons-error)" }}>{error}</span>}
106
+ ```
107
+
108
+ ---
109
+
110
+ ## 2. Where to use it
111
+
112
+ - **CXO mobile settings** — the create-workspace and manage-workspace bottom sheets,
113
+ stacked directly under a `ScTextField` for the workspace name.
114
+ - **Any `ScModal` / `ScDrawer` form** that takes a logo or cover: it is
115
+ `width: 100%`, `flex-direction: column`, and matches `ScTextField`'s label
116
+ typography and 0.25rem label gap, so a field stack lines up with no extra work.
117
+ - Anywhere a **workspace / brand logo** is picked. That is the shape CXO uses it in,
118
+ and the only shape it is designed for — one image, one preview.
119
+
120
+ ---
121
+
122
+ ## 3. When to use it
123
+
124
+ ### Use it when
125
+
126
+ - Exactly **one image**, and seeing it matters (a logo the user must confirm).
127
+ - The surrounding form already uses `ScTextField` — this is the matching image row.
128
+ - You are happy owning the blob URL lifecycle and any validation.
129
+
130
+ ### Don't use it — reach for this instead
131
+
132
+ | Situation | Use instead |
133
+ |---|---|
134
+ | A non-image file, or you need upload progress / an error line | `ScFileField` |
135
+ | A round **profile avatar** with initials fallback and separate upload + delete icon buttons | `ScProfileImageUpdate` |
136
+ | Just *displaying* an existing image/initials (no upload) | `ScDp` (`variant="workspace" \| "profile"`) or `ScIntialProfileCover` |
137
+ | Multiple images / a gallery | Nothing in this family — `ScMediaSelect` / `ScMediaApproval` exist but are **chat-runtime** components, not form fields |
138
+ | Restricting to a specific image type at the OS picker level | Nothing — `accept` is hardcoded; validate in `onFileSelect` |
139
+
140
+ ### Don't confuse with
141
+
142
+ | You may actually want | Not this |
143
+ |---|---|
144
+ | `ScFileField` — same "Click to upload" copy, dashed border, has `accept`, `multiple`, `uploading`, `progress`, `error`, and gives you a `FileList` | `ScImageField` is image-only, has none of those, and gives you a single `File` |
145
+ | `ScProfileImageUpdate` — 100px circle, `initials` fallback, two `ScButton`s | `ScImageField` is a rectangular labelled form row |
146
+ | `ScDp` / `ScIntialProfileCover` — read-only avatar renderers | Neither uploads anything |
147
+
148
+ The three uploaders (`ScFileField`, `ScImageField`, `ScProfileImageUpdate`) share
149
+ **no** props beyond `className`, and their select callbacks differ:
150
+ `ScFileField → FileList | null`, `ScImageField → File`, `ScProfileImageUpdate → File`.
151
+
152
+ ---
153
+
154
+ ## 4. Why to use it
155
+
156
+ - **Label + control + preview in one prop set.** The 0.25rem label gap, tertiary
157
+ label colour, 1rem radius panel and 1:1 `object-fit: cover` thumbnail all match
158
+ `ScTextField` and `ScDp` without you measuring anything.
159
+ - **The input is already reset after each pick** (`e.target.value = ""`), so the
160
+ classic "user re-selects the same file and nothing fires" bug is handled.
161
+ - **Theme-correct surfaces.** Panel fill, label and text are `--alias-*` tokens; the
162
+ delete icon uses `--alias-text-and-icons-error`, so it is the same red as every
163
+ other destructive affordance.
164
+ - **One place to fix.** The a11y gaps below are DS bugs — fixing them here fixes
165
+ every consumer, which is not true of a hand-rolled `<input type="file">`.
166
+
167
+ ---
168
+
169
+ ## Gotchas
170
+
171
+ **1. `label` defaults to `"Label"`.**
172
+
173
+ ```tsx
174
+ // WRONG — renders the word "Label" above the panel
175
+ <ScImageField imagePreview={preview} onFileSelect={pick} />
176
+
177
+ // RIGHT
178
+ <ScImageField label="Workspace profile image" imagePreview={preview} onFileSelect={pick} />
179
+ ```
180
+
181
+ **2. `accept="image/*"` is hardcoded and there is no prop for it.** PNG-only or
182
+ size limits must be enforced in your `onFileSelect` *after* the user has picked.
183
+
184
+ **3. It's controlled — nothing appears until `imagePreview` is truthy.** The
185
+ component never derives a preview from the `File` you were handed.
186
+
187
+ ```tsx
188
+ // WRONG — user picks an image, the panel still says "Click to upload"
189
+ <ScImageField label="Logo" onFileSelect={(f) => setFile(f)} />
190
+
191
+ // RIGHT
192
+ <ScImageField label="Logo" imagePreview={preview} onFileSelect={(f) => { setFile(f); setPreview(URL.createObjectURL(f)); }} />
193
+ ```
194
+
195
+ **4. You must revoke the blob URL.** The component creates nothing and cleans up
196
+ nothing. Call `URL.revokeObjectURL(preview)` in `onRemove` and on unmount, or you
197
+ leak.
198
+
199
+ **5. You cannot replace the image by clicking the preview.** `onClick` sits only on
200
+ the upload panel; in the preview state the panel is inert. The user must delete
201
+ first. Add your own "Replace" control if that flow matters.
202
+
203
+ **6. Mouse-only, and silent to assistive tech.** The upload panel is a
204
+ `div onClick` with no `role`, no `tabIndex`, no `aria-label`; the delete affordance is
205
+ the raw `SiconDelete` **svg** with an `onClick` — also unfocusable. The `<img>` has
206
+ `alt="preview"` hardcoded, and the `<input>` is `display: none` (so, unlike
207
+ `ScFileField`, it can't be reached by keyboard either). Tab order skips the whole
208
+ component.
209
+
210
+ **7. `imageName` falls back to `"image"`.** An uploaded file with no name prop shows
211
+ the literal word "image" — pass `file.name`.
212
+
213
+ **8. Fixed 7.5rem (120px) height, both states.** Not a prop. A tall portrait image is
214
+ cropped to a square via `object-fit: cover` and `aspect-ratio: 1`; there is no
215
+ "fit" mode.
216
+
217
+ **9. `className` is concatenated with a space even when undefined,** so the root
218
+ class list can contain a stray empty segment. Harmless, but don't assert on exact
219
+ `className` strings in tests.
220
+
221
+ **10. Hardcoded English:** `"Click to upload"`, the `"image"` fallback, `alt="preview"`.
222
+ No label props for any of them.
223
+
224
+ ---
225
+
226
+ ## In the wild
227
+
228
+ ```tsx
229
+ // cxo-dashboard src/app/components/mobile-settings-create-workspace-popup.tsx:116
230
+ <ScImageField
231
+ label="Workspace profile image"
232
+ imagePreview={imagePreview}
233
+ imageName={imageFile?.name}
234
+ onFileSelect={handleFileSelect}
235
+ onRemove={handleRemoveImage}
236
+ className="w-full shrink-0"
237
+ />
238
+ ```
239
+
240
+ The same call shape appears in
241
+ `cxo-dashboard src/app/components/mobile-settings-manage-workspace-popup.tsx:140`.
242
+ CXO's `handleFileSelect` is the canonical pattern:
243
+ `setImageFile(file); setImagePreview(URL.createObjectURL(file));` with
244
+ `URL.revokeObjectURL` in `handleRemoveImage`.
245
+
246
+ ---
247
+
248
+ ## Related
249
+
250
+ - `ScFileField` — non-image sibling; adds `accept`, `multiple`, `uploading`, `progress`, `error`.
251
+ - `ScProfileImageUpdate` — round avatar editor with upload + delete icon buttons.
252
+ - `ScDp` / `ScIntialProfileCover` — read-only avatar/initials renderers for the value once saved.
253
+ - `ScTextField` — the field this one is almost always stacked with.