@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,302 @@
|
|
|
1
|
+
---
|
|
2
|
+
component: ScPlanCard
|
|
3
|
+
package: "@streamoid/ui"
|
|
4
|
+
category: billing
|
|
5
|
+
status: stable
|
|
6
|
+
renders: div
|
|
7
|
+
tags: [plan, pricing, tile, upgrade, subscription, stripe, credits, billing, free, pro, enterprise]
|
|
8
|
+
related: [ScPlanComparison, ScPlanDetailsCard, ScCreditsUsageCard, ScButton]
|
|
9
|
+
do_not_confuse_with: [ScPlanDetailsCard, ScPlanDetailsCardMobile, ScPlanComparison, ScAppCard]
|
|
10
|
+
used_by: [cxo]
|
|
11
|
+
---
|
|
12
|
+
|
|
13
|
+
# ScPlanCard
|
|
14
|
+
|
|
15
|
+
**One purchasable plan tile in the Plans grid.** Plan name, a big formatted price
|
|
16
|
+
with a small currency symbol, a tagline, a credits chip, and a full-width CTA at
|
|
17
|
+
the bottom — in three flavours (`free`, `popular`, `custom`) that differ in what
|
|
18
|
+
they render, not just how they look.
|
|
19
|
+
|
|
20
|
+
## TL;DR for agents
|
|
21
|
+
|
|
22
|
+
- **Reach for it when:** you are building the 3-up "choose a plan" grid
|
|
23
|
+
(Free / Pro / Enterprise) where each tile ends in an Upgrade/Downgrade/Contact CTA.
|
|
24
|
+
- **Don't reach for it when:** you want the *current* plan summary strip on the
|
|
25
|
+
billing overview (→ `ScPlanDetailsCard`), the feature-by-feature grid under the
|
|
26
|
+
tiles (→ `ScPlanComparison`), or a product/app tile (→ `ScAppCard`).
|
|
27
|
+
- **Five things that will bite you:**
|
|
28
|
+
1. `price` must **not** contain `"/ month"` — the card appends `pricePeriod`
|
|
29
|
+
itself, so you get the suffix twice. (`ScPlanDetailsCard` is the opposite:
|
|
30
|
+
there you pass the whole string *including* the period.)
|
|
31
|
+
2. `type="custom"` **drops `price`, `credits`, `creditsDropdown` and the
|
|
32
|
+
Increase button entirely.** Only name + tagline + button render.
|
|
33
|
+
3. `buttonVariant` has five values but only **two** looks: `"primary"` =
|
|
34
|
+
red→orange gradient, everything else = white fill.
|
|
35
|
+
4. **No prop spread.** `className` and `style` are the only escape hatches —
|
|
36
|
+
no `onClick`, no `data-testid`, no `aria-*` on the root.
|
|
37
|
+
5. `features` is `@deprecated` and **renders nothing**. Feature lists moved to
|
|
38
|
+
`ScPlanComparison`.
|
|
39
|
+
|
|
40
|
+
---
|
|
41
|
+
|
|
42
|
+
## 1. How to use it
|
|
43
|
+
|
|
44
|
+
### Import
|
|
45
|
+
|
|
46
|
+
```tsx
|
|
47
|
+
import { ScPlanCard } from "@streamoid/ui";
|
|
48
|
+
import "@streamoid/ui/dist/index.css"; // once, at your app root
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
### Minimal usage
|
|
52
|
+
|
|
53
|
+
```tsx
|
|
54
|
+
<ScPlanCard
|
|
55
|
+
planName="Free"
|
|
56
|
+
price="$0"
|
|
57
|
+
credits="3,000 credits"
|
|
58
|
+
type="free"
|
|
59
|
+
tagline="For trying things out"
|
|
60
|
+
buttonText="Current plan"
|
|
61
|
+
buttonState="disabled"
|
|
62
|
+
buttonVariant="secondary"
|
|
63
|
+
/>
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
### Props
|
|
67
|
+
|
|
68
|
+
| Prop | Type | Default | Notes |
|
|
69
|
+
|---|---|---|---|
|
|
70
|
+
| `planName` | `string` | `"Pro"` | ⚠️ Real default. Heading, secondary text colour. |
|
|
71
|
+
| `price` | `string` | `"$0"` | ⚠️ Real default. Formatted, **with** currency symbol, **without** the period. Leading non-digits render small, the rest large. |
|
|
72
|
+
| `pricePeriod` | `string` | `"/ month"` | ⚠️ Real default, appended muted after the amount. Only rendered when `price` contains a digit. |
|
|
73
|
+
| `tagline` | `ReactNode` | – | One-line blurb under the price. |
|
|
74
|
+
| `description` | `ReactNode` | – | Back-compat alias. `tagline ?? description` — `tagline` wins. |
|
|
75
|
+
| `credits` | `string` | `"300 Credits"` | ⚠️ Real default. Text of the credits chip. **Not rendered for `type="custom"`.** |
|
|
76
|
+
| `type` | `"free"` \| `"popular"` \| `"custom"` | `"free"` | ⚠️ Defaults to `free`, not `popular`. Gates whole regions — see the table below. |
|
|
77
|
+
| `increaseLabel` | `string` | `"Increase"` | Label on the credit-tier pill. **`popular` only.** |
|
|
78
|
+
| `onIncreaseCredits` | `() => void` | – | Click of that pill. **`popular` only.** |
|
|
79
|
+
| `creditsDropdown` | `ReactNode` | – | **SLOT** rendered inside the (relatively positioned) credits wrapper. Despite the JSDoc saying "popular only", it renders for **`free` and `popular`** — just not `custom`. |
|
|
80
|
+
| `buttonText` | `string` | `"Upgrade"` | ⚠️ Real default. The CTA label. |
|
|
81
|
+
| `buttonState` | `"default"` \| `"disabled"` | `"default"` | `disabled` → native `disabled`, `opacity: .5`, `cursor: default`, `onUpgrade` not wired. |
|
|
82
|
+
| `buttonVariant` | `"primary"` \| `"secondary"` \| `"tertiary"` \| `"outline"` \| `"error"` | `"primary"` | ⚠️ Only `"primary"` is distinct (gradient). The other four all render the same white fill. |
|
|
83
|
+
| `onUpgrade` | `() => void` | – | CTA click. Ignored while `buttonState="disabled"`. |
|
|
84
|
+
| `features` | `PlanFeatureSection[]` | – | ⚠️ `@deprecated`, **never rendered**. Kept only so old imports still typecheck. |
|
|
85
|
+
| `className` | `string` | – | On the root div. |
|
|
86
|
+
| `style` | `CSSProperties` | – | Merged **last** into the card style, so it overrides `background`/`border`. |
|
|
87
|
+
|
|
88
|
+
There is **no** `...props` rest spread and the interface does **not** extend
|
|
89
|
+
`HTMLAttributes`.
|
|
90
|
+
|
|
91
|
+
### What renders in each `type`
|
|
92
|
+
|
|
93
|
+
| Region | `free` | `popular` | `custom` |
|
|
94
|
+
|---|---|---|---|
|
|
95
|
+
| Name + price + `pricePeriod` | ✅ | ✅ | name only, no price |
|
|
96
|
+
| `tagline` / `description` | ✅ | ✅ | ✅ |
|
|
97
|
+
| Credits chip (`credits`) | ✅ | ✅ | — |
|
|
98
|
+
| `increaseLabel` pill + `onIncreaseCredits` | — | ✅ | — |
|
|
99
|
+
| `creditsDropdown` slot | ✅ | ✅ | — |
|
|
100
|
+
| Card edge | 0.5px subtle grey border | red→orange gradient border | 0.5px subtle grey border |
|
|
101
|
+
| CTA button | ✅ | ✅ | ✅ |
|
|
102
|
+
|
|
103
|
+
### Recipes
|
|
104
|
+
|
|
105
|
+
```tsx
|
|
106
|
+
// The paid tile with a credit-tier picker in the slot.
|
|
107
|
+
// The card is `overflow: visible` on purpose so this panel can overlay below it.
|
|
108
|
+
<ScPlanCard
|
|
109
|
+
planName="Pro"
|
|
110
|
+
type="popular"
|
|
111
|
+
price={stripMonthly(selected.price)} // "$49.99", NOT "$49.99 / month"
|
|
112
|
+
credits={selected.credits}
|
|
113
|
+
tagline="For teams shipping every week"
|
|
114
|
+
increaseLabel="Increase"
|
|
115
|
+
onIncreaseCredits={() => setTierOpen((o) => !o)}
|
|
116
|
+
creditsDropdown={
|
|
117
|
+
tierOpen ? (
|
|
118
|
+
<div style={{ position: "absolute", left: 0, right: 0, top: "calc(100% + 4px)", zIndex: 20 }}>
|
|
119
|
+
{/* your tier options */}
|
|
120
|
+
</div>
|
|
121
|
+
) : undefined
|
|
122
|
+
}
|
|
123
|
+
buttonText="Upgrade"
|
|
124
|
+
buttonVariant="primary"
|
|
125
|
+
onUpgrade={() => setConfirmOpen(true)}
|
|
126
|
+
/>
|
|
127
|
+
|
|
128
|
+
// Enterprise / "talk to us" tile — no price, no credits
|
|
129
|
+
<ScPlanCard
|
|
130
|
+
planName="Enterprise"
|
|
131
|
+
type="custom"
|
|
132
|
+
tagline="Custom volume, custom terms"
|
|
133
|
+
buttonText="Contact Sales"
|
|
134
|
+
buttonVariant="secondary"
|
|
135
|
+
onUpgrade={() => window.open("https://cal.com/streamoid/30min", "_blank")}
|
|
136
|
+
/>
|
|
137
|
+
|
|
138
|
+
// Equal-height tiles: the card has no intrinsic height — stretch it from outside
|
|
139
|
+
<div className="grid grid-cols-1 md:grid-cols-2 xl:grid-cols-3 items-stretch" style={{ gap: 32 }}>
|
|
140
|
+
{tiles.map((t) => (
|
|
141
|
+
<div key={t.id} className="h-full">
|
|
142
|
+
<ScPlanCard {...t} className="h-full" />
|
|
143
|
+
</div>
|
|
144
|
+
))}
|
|
145
|
+
</div>
|
|
146
|
+
|
|
147
|
+
// A price the API couldn't resolve: no digits ⇒ rendered whole, and the
|
|
148
|
+
// "/ month" suffix is suppressed automatically.
|
|
149
|
+
<ScPlanCard planName="Pro" type="popular" price="—" credits="—" />
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
---
|
|
153
|
+
|
|
154
|
+
## 2. Where to use it
|
|
155
|
+
|
|
156
|
+
- **The Billing → Plans sub-view**, three tiles side by side. That is the only
|
|
157
|
+
surface it exists for.
|
|
158
|
+
- Rendered from the shared **`@streamoid/settings`** billing page
|
|
159
|
+
(`packages/settings/src/billing-content.tsx`, `PlansView`), which **CXO** mounts
|
|
160
|
+
through `src/app/components/settings-content.tsx`. CXO has a local
|
|
161
|
+
`billing-content.tsx` that also calls it, but nothing imports that file — it is
|
|
162
|
+
dead code. Edit the settings package, not CXO's copy.
|
|
163
|
+
- Pair it with `ScPlanComparison` directly underneath — the tiles carry
|
|
164
|
+
price/credits/CTA, the comparison grid carries the feature matrix.
|
|
165
|
+
|
|
166
|
+
---
|
|
167
|
+
|
|
168
|
+
## 3. When to use it
|
|
169
|
+
|
|
170
|
+
### Use it when
|
|
171
|
+
|
|
172
|
+
- The tile is **purchasable**: it ends in an action that changes the subscription.
|
|
173
|
+
- You have a **formatted** price string from the billing API (the card does no
|
|
174
|
+
currency maths, no locale formatting, no minor-unit division).
|
|
175
|
+
- You need the Pro tile's gradient edge and credit-tier affordance without
|
|
176
|
+
rebuilding the padding-box/border-box gradient trick.
|
|
177
|
+
|
|
178
|
+
### Don't use it — reach for this instead
|
|
179
|
+
|
|
180
|
+
| Situation | Use instead |
|
|
181
|
+
|---|---|
|
|
182
|
+
| "Current plan · 30,000 credits · ₹0.00 / month" summary strip with an Active badge | `ScPlanDetailsCard` (desktop) / `ScPlanDetailsCardMobile` |
|
|
183
|
+
| Feature-by-feature table under the tiles | `ScPlanComparison` |
|
|
184
|
+
| Credits remaining / usage meter | `ScCreditsUsageCard` |
|
|
185
|
+
| A tile for a *product* (Artifax, Photogenix…) rather than a plan | `ScAppCard` / `ScAppCardV3` / `ScAppListingCard` |
|
|
186
|
+
| A standalone CTA anywhere else | `ScButton` |
|
|
187
|
+
| Inline feature bullet lists inside the tile | Not supported — `features` is dead. Move them to `ScPlanComparison`. |
|
|
188
|
+
|
|
189
|
+
### Don't confuse with
|
|
190
|
+
|
|
191
|
+
| You may actually want | Not this |
|
|
192
|
+
|---|---|
|
|
193
|
+
| `ScPlanDetailsCard` — the **one** card describing the plan you already have | `ScPlanCard` is one of **many** tiles describing plans you could buy |
|
|
194
|
+
| `ScPlanComparison` — collapsible feature grid, no price, no CTA | This tile has price + CTA and no feature rows |
|
|
195
|
+
| `ScPlanCard`'s `price` (period **excluded**) | `ScPlanDetailsCard`'s `planPrice` (period **included**) — the two sibling cards take opposite conventions |
|
|
196
|
+
|
|
197
|
+
---
|
|
198
|
+
|
|
199
|
+
## 4. Why to use it
|
|
200
|
+
|
|
201
|
+
- **The price typography is done for you.** Splitting the leading currency symbol
|
|
202
|
+
to 14px while the amount stays 20px, and suppressing `/ month` when the price
|
|
203
|
+
is a placeholder, is fiddly regex work you'd otherwise repeat per tile.
|
|
204
|
+
- **The Pro gradient border is not a border colour.** It is a
|
|
205
|
+
`padding-box`/`border-box` double-background with a 0.5px transparent border.
|
|
206
|
+
Hand-rolling it usually ends in a 1px gradient *outline* that doesn't follow the
|
|
207
|
+
24px radius.
|
|
208
|
+
- **`overflow: visible` is deliberate,** so the credit-tier dropdown can hang
|
|
209
|
+
below the card. A generic card would clip it.
|
|
210
|
+
- **Bottom-pinned CTA.** The content region is `flex: 1 1 auto; min-height: 0`, so
|
|
211
|
+
every tile's button lines up across a grid row regardless of tagline length.
|
|
212
|
+
|
|
213
|
+
---
|
|
214
|
+
|
|
215
|
+
## Gotchas
|
|
216
|
+
|
|
217
|
+
**1. Strip the period from `price`.** The card renders `pricePeriod` itself.
|
|
218
|
+
|
|
219
|
+
```tsx
|
|
220
|
+
// WRONG — renders "$49.99 / month / month"
|
|
221
|
+
<ScPlanCard price="$49.99 / month" />
|
|
222
|
+
|
|
223
|
+
// RIGHT
|
|
224
|
+
<ScPlanCard price={price.replace(/\s*\/\s*month\s*$/i, "")} />
|
|
225
|
+
```
|
|
226
|
+
|
|
227
|
+
**2. `type="custom"` silently drops props.** `price`, `credits`,
|
|
228
|
+
`creditsDropdown`, `increaseLabel` and `onIncreaseCredits` are not rendered at
|
|
229
|
+
all — a common "why is my Enterprise price missing" bug.
|
|
230
|
+
|
|
231
|
+
**3. `buttonVariant` collapses to two skins.** `"secondary"`, `"tertiary"`,
|
|
232
|
+
`"outline"` and `"error"` are all the same white fill. If you need a genuinely
|
|
233
|
+
outlined or destructive CTA, this card can't express it.
|
|
234
|
+
|
|
235
|
+
**4. Nothing is spread onto the root.** No `data-testid`, no `onClick`, no
|
|
236
|
+
`aria-*`, no `id`. Wrap the card in a div if you need any of those.
|
|
237
|
+
|
|
238
|
+
```tsx
|
|
239
|
+
// WRONG — TS error, and nothing would land on the DOM anyway
|
|
240
|
+
<ScPlanCard data-testid="pro-tile" />
|
|
241
|
+
|
|
242
|
+
// RIGHT
|
|
243
|
+
<div data-testid="pro-tile"><ScPlanCard /></div>
|
|
244
|
+
```
|
|
245
|
+
|
|
246
|
+
**5. Every text default is demo copy.** Omit `planName`, `price`,
|
|
247
|
+
`credits` or `buttonText` and you ship `"Pro"`, `"$0"`, `"300 Credits"`,
|
|
248
|
+
`"Upgrade"`. There is no empty state.
|
|
249
|
+
|
|
250
|
+
**6. The gradient is a raw hex pair, not a token.**
|
|
251
|
+
`linear-gradient(135deg, #d91536, #ee5e3a)` for the CTA and the Pro border tint
|
|
252
|
+
are hardcoded, so they look identical in light and dark mode by design. Don't
|
|
253
|
+
expect them to respond to a theme change.
|
|
254
|
+
|
|
255
|
+
**7. `style` overrides the card skin.** It is merged after the computed styles, so
|
|
256
|
+
passing `style={{ background: "..." }}` on a `popular` card destroys the gradient
|
|
257
|
+
border (the trick needs both background layers).
|
|
258
|
+
|
|
259
|
+
**8. `ScPlanCard.module.css` is dead.** The component is 100% inline styles and
|
|
260
|
+
never imports it. Host CSS targeting `.ScPlanCard_scPlanCard` (or any class in
|
|
261
|
+
that file) will not match anything — style via `className` + `style` instead.
|
|
262
|
+
|
|
263
|
+
**9. `creditsDropdown` is a slot with no state.** No `aria-expanded` is wired on
|
|
264
|
+
the Increase pill, and the card never closes your panel — own the open state and
|
|
265
|
+
the outside-click handler yourself.
|
|
266
|
+
|
|
267
|
+
**10. No intrinsic height.** In a grid, tiles will be different heights unless you
|
|
268
|
+
pass `className="h-full"` (or equivalent) and stretch the grid items.
|
|
269
|
+
|
|
270
|
+
---
|
|
271
|
+
|
|
272
|
+
## In the wild
|
|
273
|
+
|
|
274
|
+
```tsx
|
|
275
|
+
// npm-components packages/settings/src/billing-content.tsx:791
|
|
276
|
+
<ScPlanCard
|
|
277
|
+
planName="Pro"
|
|
278
|
+
price={stripMonthly(selectedMiddlePlan?.price || "—")}
|
|
279
|
+
credits={selectedMiddlePlan?.credits || "—"}
|
|
280
|
+
type="popular"
|
|
281
|
+
tagline={PLAN_TAGLINES.pro}
|
|
282
|
+
buttonText={isMiddleActionLoading ? "Processing..." : /* … */ "Upgrade"}
|
|
283
|
+
buttonState={selectedMiddlePlan?.isCurrent ? "disabled" : "default"}
|
|
284
|
+
buttonVariant={selectedMiddlePlan?.isCurrent ? "outline" : "primary"}
|
|
285
|
+
onUpgrade={() => setShowUpgradeConfirmModal(true)}
|
|
286
|
+
onIncreaseCredits={() => setIsPlanSelectorOpen((prev) => !prev)}
|
|
287
|
+
creditsDropdown={isPlanSelectorOpen ? (/* absolutely-positioned tier list */) : undefined}
|
|
288
|
+
className="h-full"
|
|
289
|
+
/>
|
|
290
|
+
```
|
|
291
|
+
|
|
292
|
+
(The `free` tile is at `:750`, the `custom`/Enterprise tile at `:909`.)
|
|
293
|
+
|
|
294
|
+
---
|
|
295
|
+
|
|
296
|
+
## Related
|
|
297
|
+
|
|
298
|
+
- `ScPlanComparison` — the feature matrix that replaced this card's `features` prop.
|
|
299
|
+
- `ScPlanDetailsCard` / `ScPlanDetailsCardMobile` — the current-plan summary.
|
|
300
|
+
- `ScCreditsUsageCard` — the credits meter beside it on the billing overview.
|
|
301
|
+
- `ScButton` — for any CTA outside a plan tile; note this card renders its own
|
|
302
|
+
plain `<button>`, not `ScButton`.
|
|
@@ -0,0 +1,264 @@
|
|
|
1
|
+
---
|
|
2
|
+
component: ScPlanComparison
|
|
3
|
+
package: "@streamoid/ui"
|
|
4
|
+
category: billing
|
|
5
|
+
status: stable
|
|
6
|
+
renders: div
|
|
7
|
+
tags: [compare-plans, pricing-table, feature-matrix, collapsible, accordion, billing, tabbed, mobile]
|
|
8
|
+
related: [ScPlanCard, ScPlanDetailsCard, ScTabComp, ScTabs]
|
|
9
|
+
do_not_confuse_with: [ScPlanCard, ScTableList, ScTabSwitcher, ScBillingLogsTableHeader]
|
|
10
|
+
used_by: [cxo]
|
|
11
|
+
required_props: [columns, sections]
|
|
12
|
+
---
|
|
13
|
+
|
|
14
|
+
# ScPlanComparison
|
|
15
|
+
|
|
16
|
+
**The collapsible "Compare Plans" feature matrix under the plan tiles.** A title
|
|
17
|
+
row with a chevron toggle, then a rounded grid: one label column plus one column
|
|
18
|
+
per plan, grouped into optional full-width section headers, with the Pro column
|
|
19
|
+
tinted and edged in red. `layout="tabbed"` collapses the same data to one plan at
|
|
20
|
+
a time for narrow screens.
|
|
21
|
+
|
|
22
|
+
## TL;DR for agents
|
|
23
|
+
|
|
24
|
+
- **Reach for it when:** you have static marketing copy comparing N plans across
|
|
25
|
+
many features and want it collapsed under the pricing tiles.
|
|
26
|
+
- **Don't reach for it when:** the data is per-row API data with actions
|
|
27
|
+
(→ `ScTableList`), or you want the price/CTA tile itself (→ `ScPlanCard`).
|
|
28
|
+
- **Four things that will bite you:**
|
|
29
|
+
1. `columns` and every `row.values` array must be the **same length and order**.
|
|
30
|
+
A short `values` array silently renders fewer cells and the grid goes crooked.
|
|
31
|
+
2. `defaultOpen` is **uncontrolled** — the open state lives in `useState`, and
|
|
32
|
+
there is no `onToggle`. Changing the prop later does nothing.
|
|
33
|
+
3. `highlightColumnIndex` defaults to **`1`** — it assumes a 3-column
|
|
34
|
+
Free/Pro/Enterprise layout with Pro in the middle.
|
|
35
|
+
4. It is **divs, not a `<table>`**. No `<th>`, no row/column association for
|
|
36
|
+
screen readers, and `layout="tabbed"` has `role="tab"` with no `tabpanel`.
|
|
37
|
+
|
|
38
|
+
---
|
|
39
|
+
|
|
40
|
+
## 1. How to use it
|
|
41
|
+
|
|
42
|
+
### Import
|
|
43
|
+
|
|
44
|
+
```tsx
|
|
45
|
+
import { ScPlanComparison } from "@streamoid/ui";
|
|
46
|
+
import type { PlanComparisonSection } from "@streamoid/ui";
|
|
47
|
+
import "@streamoid/ui/dist/index.css"; // once, at your app root
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
### Minimal usage
|
|
51
|
+
|
|
52
|
+
```tsx
|
|
53
|
+
<ScPlanComparison
|
|
54
|
+
columns={["Free", "Pro", "Enterprise"]}
|
|
55
|
+
sections={[
|
|
56
|
+
{ rows: [{ label: "Usage volume", values: ["Limited", "Higher limits", "Unlimited"] }] },
|
|
57
|
+
]}
|
|
58
|
+
/>
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
### Props
|
|
62
|
+
|
|
63
|
+
| Prop | Type | Default | Notes |
|
|
64
|
+
|---|---|---|---|
|
|
65
|
+
| `columns` | `string[]` | — | **Required.** Plan column headers, in order. Drives the header row (table) or the tab strip (tabbed). |
|
|
66
|
+
| `sections` | `PlanComparisonSection[]` | — | **Required.** Ordered groups of rows. |
|
|
67
|
+
| `title` | `string` | `"Compare Plans"` | ⚠️ Real default. The collapsible heading. |
|
|
68
|
+
| `firstColumnLabel` | `string` | `"Compare plans"` | ⚠️ Real default. Top-left header cell. **Not rendered in `tabbed` layout.** |
|
|
69
|
+
| `note` | `ReactNode` | – | Centred caption row directly under the header row (or under the tabs). |
|
|
70
|
+
| `highlightColumnIndex` | `number` | `1` | ⚠️ Which value column gets the red left edge + warm tint. Pass an out-of-range index (e.g. `-1`) to highlight nothing. |
|
|
71
|
+
| `defaultOpen` | `boolean` | `true` | Initial open state only — see Gotcha 2. |
|
|
72
|
+
| `layout` | `"table"` \| `"tabbed"` | `"table"` | `table` = full grid; `tabbed` = tab strip + a single value column. You switch it; there is no internal breakpoint. |
|
|
73
|
+
| `className` | `string` | – | On the outer column. |
|
|
74
|
+
| `style` | `CSSProperties` | – | Merged last onto the outer column. |
|
|
75
|
+
|
|
76
|
+
There is **no** `...props` rest spread — no `id`, `data-*` or `aria-*` on the root.
|
|
77
|
+
|
|
78
|
+
### Supporting types
|
|
79
|
+
|
|
80
|
+
```ts
|
|
81
|
+
interface PlanComparisonRow {
|
|
82
|
+
label?: ReactNode; // may be omitted/"" for a design sub-row
|
|
83
|
+
values: ReactNode[]; // one entry per column, in column order
|
|
84
|
+
}
|
|
85
|
+
interface PlanComparisonSection {
|
|
86
|
+
header?: string; // omit for an ungrouped leading row
|
|
87
|
+
rows: PlanComparisonRow[];
|
|
88
|
+
}
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
### What renders in each layout
|
|
92
|
+
|
|
93
|
+
| Region | `layout="table"` | `layout="tabbed"` |
|
|
94
|
+
|---|---|---|
|
|
95
|
+
| Title + chevron toggle | ✅ | ✅ |
|
|
96
|
+
| `firstColumnLabel` + `columns` header row | ✅ | — (replaced by the tab strip) |
|
|
97
|
+
| Tab strip over `columns` | — | ✅ (internal `activeCol`, starts at 0) |
|
|
98
|
+
| `note` | full-width centred row inside the grid | centred paragraph above the grid |
|
|
99
|
+
| Section headers | full-width row, canvas background | same |
|
|
100
|
+
| Feature rows | label + **every** value | label + **only** `row.values[activeCol]` |
|
|
101
|
+
|
|
102
|
+
### Recipes
|
|
103
|
+
|
|
104
|
+
```tsx
|
|
105
|
+
// Static content module (the real pattern — there is no API for this copy)
|
|
106
|
+
const SECTIONS: PlanComparisonSection[] = [
|
|
107
|
+
{ rows: [{ label: "Usage volume", values: ["Limited", "Higher limits", "Unlimited"] }] },
|
|
108
|
+
{
|
|
109
|
+
header: "Photogenix (Imagery & Video)",
|
|
110
|
+
rows: [
|
|
111
|
+
{ label: "Image generation", values: ["Up to 60 images", "Up to 200 images", "Unlimited"] },
|
|
112
|
+
{ label: "", values: ["Up to 100 styles", "Up to 333 styles", "Unlimited"] }, // sub-row
|
|
113
|
+
],
|
|
114
|
+
},
|
|
115
|
+
];
|
|
116
|
+
|
|
117
|
+
<ScPlanComparison
|
|
118
|
+
layout="table"
|
|
119
|
+
columns={["Free", "Pro", "Enterprise"]}
|
|
120
|
+
note="Credits roll over monthly and only expire if your plan is no longer active"
|
|
121
|
+
sections={SECTIONS}
|
|
122
|
+
highlightColumnIndex={1}
|
|
123
|
+
/>
|
|
124
|
+
|
|
125
|
+
// Responsive: you own the switch
|
|
126
|
+
<ScPlanComparison
|
|
127
|
+
layout={isMobile ? "tabbed" : "table"}
|
|
128
|
+
columns={["Free", "Pro", "Enterprise"]}
|
|
129
|
+
sections={SECTIONS}
|
|
130
|
+
/>
|
|
131
|
+
|
|
132
|
+
// Start collapsed (initial value only)
|
|
133
|
+
<ScPlanComparison defaultOpen={false} columns={cols} sections={SECTIONS} />
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
---
|
|
137
|
+
|
|
138
|
+
## 2. Where to use it
|
|
139
|
+
|
|
140
|
+
- **Directly under the `ScPlanCard` grid** on the Billing → Plans sub-view. That
|
|
141
|
+
is its only surface.
|
|
142
|
+
- Rendered from the shared **`@streamoid/settings`** billing page
|
|
143
|
+
(`packages/settings/src/billing-content.tsx`, `PlansView`), which **CXO** mounts
|
|
144
|
+
through `settings-content.tsx`. The copy lives in
|
|
145
|
+
`packages/settings/src/plan-comparison-data.ts` (`PLAN_COMPARISON_COLUMNS`,
|
|
146
|
+
`PLAN_COMPARISON_NOTE`, `PLAN_COMPARISON_SECTIONS`) — extend that file rather
|
|
147
|
+
than inlining literals at the call site.
|
|
148
|
+
- Only `layout="table"` is used in production today; `"tabbed"` ships but has no
|
|
149
|
+
live call site.
|
|
150
|
+
|
|
151
|
+
---
|
|
152
|
+
|
|
153
|
+
## 3. When to use it
|
|
154
|
+
|
|
155
|
+
### Use it when
|
|
156
|
+
|
|
157
|
+
- The matrix is **static content**: marketing copy, one string per (feature, plan)
|
|
158
|
+
cell, no interactivity inside cells.
|
|
159
|
+
- You want it **collapsed by default under the tiles** rather than as its own page.
|
|
160
|
+
- One column should be visually promoted (Pro) without you rebuilding the tint +
|
|
161
|
+
red left-edge treatment.
|
|
162
|
+
|
|
163
|
+
### Don't use it — reach for this instead
|
|
164
|
+
|
|
165
|
+
| Situation | Use instead |
|
|
166
|
+
|---|---|
|
|
167
|
+
| Rows come from an API and have per-row actions | `ScTableList` / `ScBillingHistoryTableList` + `ScHeader` |
|
|
168
|
+
| You need price + CTA per plan | `ScPlanCard` |
|
|
169
|
+
| The current plan's summary | `ScPlanDetailsCard` |
|
|
170
|
+
| A generic in-page tab bar | `ScTabs` / `ScTabComp` / `ScTabSwitcher` — the tab strip here is internal and not reusable |
|
|
171
|
+
| A sortable data grid | `ScHeader` (`state="up"/"down"`) + your own rows; this component has no sorting |
|
|
172
|
+
| Cells that contain checkmarks/icons | Supported — `values` are `ReactNode`, so pass an `Sicon*`; just keep the row heights consistent |
|
|
173
|
+
|
|
174
|
+
### Don't confuse with
|
|
175
|
+
|
|
176
|
+
| You may actually want | Not this |
|
|
177
|
+
|---|---|
|
|
178
|
+
| `ScPlanCard` — one tile per plan, price + CTA | This is the shared feature matrix below the tiles |
|
|
179
|
+
| `ScBillingLogsTableHeader` / `ScBillingHistoryHeader` — real table header strips with fixed column widths | This component owns its own header row internally |
|
|
180
|
+
| `ScTabSwitcher` / `ScTabComp` — the reusable tab vocabulary | The `tabbed` layout's buttons are private, un-styleable, and report nothing back |
|
|
181
|
+
|
|
182
|
+
---
|
|
183
|
+
|
|
184
|
+
## 4. Why to use it
|
|
185
|
+
|
|
186
|
+
- **One data shape, two layouts.** The same `sections` array drives the desktop
|
|
187
|
+
grid and the mobile tabbed view, so the copy never forks per breakpoint.
|
|
188
|
+
- **Column emphasis is computed, not hand-painted.** `valueColBg()` gives column 0
|
|
189
|
+
a raised neutral, the highlighted column a warm tint plus a red left border, and
|
|
190
|
+
the rest the canvas colour — consistent down every row without per-cell classes.
|
|
191
|
+
- **The rounded clip is right.** A 32px radius with `overflow: hidden` on the grid
|
|
192
|
+
wrapper means the first and last rows are trimmed to the corners; doing this by
|
|
193
|
+
hand usually leaves square row corners poking out.
|
|
194
|
+
- **Row text is tokenised** (`--alias-text---icons-secondary`), so the matrix stays
|
|
195
|
+
readable in light mode, unlike a hardcoded grey.
|
|
196
|
+
|
|
197
|
+
---
|
|
198
|
+
|
|
199
|
+
## Gotchas
|
|
200
|
+
|
|
201
|
+
**1. Ragged `values` arrays break alignment silently.** Every row must have exactly
|
|
202
|
+
`columns.length` entries.
|
|
203
|
+
|
|
204
|
+
```tsx
|
|
205
|
+
// WRONG — 3 columns, 2 values: the Enterprise cell is missing and the row shifts
|
|
206
|
+
{ label: "Tech packs", values: ["Manual fill", "Up to 5"] }
|
|
207
|
+
|
|
208
|
+
// RIGHT
|
|
209
|
+
{ label: "Tech packs", values: ["Manual fill", "Up to 5", "Unlimited"] }
|
|
210
|
+
```
|
|
211
|
+
|
|
212
|
+
**2. Open/closed state is internal.** `defaultOpen` seeds `useState` once. You
|
|
213
|
+
cannot force it open later, and you get no callback when the user toggles it.
|
|
214
|
+
|
|
215
|
+
**3. `highlightColumnIndex` is not "the current plan".** It is a fixed design
|
|
216
|
+
emphasis (Pro). Don't wire it to the user's active plan unless you actually want
|
|
217
|
+
the tint to move.
|
|
218
|
+
|
|
219
|
+
**4. In `tabbed` layout, `firstColumnLabel` and the column header row vanish,**
|
|
220
|
+
and the active column is internal state starting at `0` — you cannot preselect
|
|
221
|
+
"Pro" or read the user's choice.
|
|
222
|
+
|
|
223
|
+
**5. The Pro tint is deliberately theme-agnostic.** `rgba(255,123,71,0.05)` is a
|
|
224
|
+
raw value, not a token, so on a white light-mode surface it reads as a faint peach
|
|
225
|
+
wash rather than flipping.
|
|
226
|
+
|
|
227
|
+
**6. No table semantics.** Rows are `div`s and cells are `<p>`s. Screen readers get
|
|
228
|
+
a flat run of text with no header association. If you need an accessible pricing
|
|
229
|
+
table for compliance, this component is not it yet.
|
|
230
|
+
|
|
231
|
+
**7. `overflow: hidden` on the grid.** Nothing inside a cell can escape — no
|
|
232
|
+
tooltips, popovers or dropdowns unless you portal them to `document.body`.
|
|
233
|
+
|
|
234
|
+
**8. Cell padding is a hardcoded 20px** and rows have no minimum height, so a cell
|
|
235
|
+
with a long string grows only that row. Keep values short or accept ragged row
|
|
236
|
+
heights.
|
|
237
|
+
|
|
238
|
+
**9. The toggle's `aria-label` is hardcoded English** (`"Collapse"` / `"Expand"`),
|
|
239
|
+
as is the `title` default `"Compare Plans"`. Not localisable without a DS change.
|
|
240
|
+
|
|
241
|
+
---
|
|
242
|
+
|
|
243
|
+
## In the wild
|
|
244
|
+
|
|
245
|
+
```tsx
|
|
246
|
+
// npm-components packages/settings/src/billing-content.tsx:924
|
|
247
|
+
<ScPlanComparison
|
|
248
|
+
layout="table"
|
|
249
|
+
columns={[...PLAN_COMPARISON_COLUMNS]}
|
|
250
|
+
note={PLAN_COMPARISON_NOTE}
|
|
251
|
+
sections={PLAN_COMPARISON_SECTIONS}
|
|
252
|
+
highlightColumnIndex={1}
|
|
253
|
+
/>
|
|
254
|
+
```
|
|
255
|
+
|
|
256
|
+
---
|
|
257
|
+
|
|
258
|
+
## Related
|
|
259
|
+
|
|
260
|
+
- `ScPlanCard` — the tiles above it; its `features` prop is deprecated *because*
|
|
261
|
+
this component exists.
|
|
262
|
+
- `ScPlanDetailsCard` / `ScCreditsUsageCard` — the billing overview cards.
|
|
263
|
+
- `ScTabComp` — the real tab component used elsewhere on the billing page (the
|
|
264
|
+
Invoice history / Usage logs switch).
|