@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,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).