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