@soba-so/react 0.0.27 → 0.0.29

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/dist/styles.css CHANGED
@@ -1533,6 +1533,7 @@ a.sc-btn {
1533
1533
 
1534
1534
  .sc-mp-label {
1535
1535
  min-width: 0;
1536
+ max-width: 24ch;
1536
1537
  overflow: hidden;
1537
1538
  text-overflow: ellipsis;
1538
1539
  white-space: nowrap;
@@ -1544,6 +1545,274 @@ a.sc-btn {
1544
1545
  color: var(--sc-faint, #a1a1aa);
1545
1546
  }
1546
1547
 
1548
+ .sc-mm {
1549
+ position: fixed;
1550
+ inset: auto;
1551
+ z-index: 2147483647;
1552
+ display: flex;
1553
+ flex-direction: column;
1554
+ min-width: calc(240px * var(--sc-scale, 1));
1555
+ max-width: min(calc(340px * var(--sc-scale, 1)), calc(100vw - 16px));
1556
+ width: max-content;
1557
+ margin: 0;
1558
+ padding: 0;
1559
+ overflow: hidden;
1560
+ border: 1px solid var(--sc-line, #e9e9eb);
1561
+ border-radius: calc(12px * var(--sc-scale, 1));
1562
+ background: var(--sc-tray-card, #ffffff);
1563
+ box-shadow:
1564
+ 0 12px 32px rgba(0, 0, 0, 0.12),
1565
+ 0 2px 6px rgba(0, 0, 0, 0.06);
1566
+ color: var(--sc-text, #1a1a1c);
1567
+ font-size: var(--sc-fs-small, 13px);
1568
+ line-height: 1.3;
1569
+ outline: none;
1570
+ }
1571
+
1572
+ .sc-mm-search {
1573
+ display: flex;
1574
+ flex: none;
1575
+ align-items: center;
1576
+ gap: 8px;
1577
+ padding: 0 12px;
1578
+ border-bottom: 1px solid var(--sc-line, #e9e9eb);
1579
+ color: var(--sc-faint, #a1a1aa);
1580
+ }
1581
+
1582
+ .sc-mm-find {
1583
+ flex: none;
1584
+ width: 14px;
1585
+ height: 14px;
1586
+ }
1587
+
1588
+ .sc-mm-search input {
1589
+ flex: 1;
1590
+ min-width: 0;
1591
+ height: calc(38px * var(--sc-scale, 1));
1592
+ padding: 0;
1593
+ border: 0;
1594
+ background: transparent;
1595
+ font: inherit;
1596
+
1597
+ font-size: max(16px, var(--sc-fs-small, 13px));
1598
+ color: var(--sc-text, #1a1a1c);
1599
+ outline: none;
1600
+ }
1601
+ @media (pointer: fine) {
1602
+ .sc-mm-search input {
1603
+ font-size: inherit;
1604
+ }
1605
+ }
1606
+ .sc-mm-search input::placeholder {
1607
+ color: var(--sc-faint, #a1a1aa);
1608
+ }
1609
+
1610
+ .sc-mm-list {
1611
+ flex: 1;
1612
+ min-height: 0;
1613
+ padding: 4px;
1614
+ overflow-y: auto;
1615
+ overscroll-behavior: contain;
1616
+ scroll-padding-block: 4px;
1617
+ }
1618
+
1619
+ .sc-menu,
1620
+ .sc-mm-list,
1621
+ .sc-mm-flyout {
1622
+ scrollbar-width: none;
1623
+ }
1624
+ .sc-menu::-webkit-scrollbar,
1625
+ .sc-mm-list::-webkit-scrollbar,
1626
+ .sc-mm-flyout::-webkit-scrollbar {
1627
+ display: none;
1628
+ }
1629
+
1630
+ .sc-mm-group + .sc-mm-group {
1631
+ margin-top: 4px;
1632
+ padding-top: 4px;
1633
+ border-top: 1px solid var(--sc-line, #e9e9eb);
1634
+ }
1635
+
1636
+ .sc-mm-head {
1637
+ display: flex;
1638
+ align-items: center;
1639
+ gap: 6px;
1640
+ padding: 8px 8px 4px;
1641
+ font-size: var(--sc-fs-label);
1642
+ font-weight: 500;
1643
+ color: var(--sc-muted, #6b6b73);
1644
+ }
1645
+
1646
+ .sc-mm-logo {
1647
+ flex: none;
1648
+ width: 14px;
1649
+ height: 14px;
1650
+ border-radius: 3px;
1651
+ }
1652
+
1653
+ .sc-mm-emoji {
1654
+ display: inline-grid;
1655
+ place-items: center;
1656
+ font-size: 12px;
1657
+ line-height: 1;
1658
+ }
1659
+
1660
+ .sc-mm-item {
1661
+ display: flex;
1662
+ align-items: center;
1663
+ gap: 8px;
1664
+ min-height: calc(32px * var(--sc-scale, 1));
1665
+ padding: 0 8px;
1666
+ border-radius: calc(7px * var(--sc-scale, 1));
1667
+ cursor: pointer;
1668
+ user-select: none;
1669
+ }
1670
+
1671
+ .sc-mm-item[data-active] {
1672
+ background: var(--sc-tray-sunk, #f4f4f5);
1673
+ }
1674
+
1675
+ .sc-mm-inline {
1676
+ position: static;
1677
+ z-index: auto;
1678
+ max-height: 380px;
1679
+ }
1680
+
1681
+ .sc-mm-item[data-locked] {
1682
+ cursor: default;
1683
+ }
1684
+
1685
+ .sc-mm-item[data-locked] .sc-mm-label {
1686
+ color: var(--sc-muted, #6b6b73);
1687
+ }
1688
+
1689
+ .sc-mm-label {
1690
+ flex: none;
1691
+ max-width: 100%;
1692
+ overflow: hidden;
1693
+ text-overflow: ellipsis;
1694
+ white-space: nowrap;
1695
+ }
1696
+
1697
+ .sc-mm-item[aria-selected="true"] .sc-mm-label {
1698
+ font-weight: 500;
1699
+ }
1700
+
1701
+ .sc-mm-note {
1702
+ flex: 1;
1703
+ min-width: 0;
1704
+ overflow: hidden;
1705
+ text-overflow: ellipsis;
1706
+ white-space: nowrap;
1707
+ color: var(--sc-muted, #6b6b73);
1708
+ }
1709
+
1710
+ .sc-mm-item .sc-menu-check {
1711
+ margin-left: auto;
1712
+ width: 14px;
1713
+ height: 14px;
1714
+ }
1715
+
1716
+ .sc-mm-empty {
1717
+ padding: 16px 8px;
1718
+ text-align: center;
1719
+ color: var(--sc-muted, #6b6b73);
1720
+ }
1721
+
1722
+ .sc-mm-tile {
1723
+ flex: none;
1724
+ width: 16px;
1725
+ height: 16px;
1726
+ border-radius: 4px;
1727
+ object-fit: cover;
1728
+ }
1729
+ .sc-mm-tile-box {
1730
+ display: inline-grid;
1731
+ place-items: center;
1732
+ background: color-mix(in srgb, currentColor 6%, transparent);
1733
+ box-shadow: inset 0 0 0 1px color-mix(in srgb, currentColor 10%, transparent);
1734
+ color: var(--sc-text, #1a1a1c);
1735
+ }
1736
+ .sc-mm-tile-glyph {
1737
+ width: 11px;
1738
+ height: 11px;
1739
+ }
1740
+ .sc-mm-tile-dim {
1741
+ opacity: 0.5;
1742
+ filter: grayscale(1);
1743
+ }
1744
+ .sc-mm-tile.sc-mm-emoji {
1745
+ font-size: 11px;
1746
+ }
1747
+
1748
+ .sc-mm-sub .sc-chev {
1749
+ flex: none;
1750
+ width: 14px;
1751
+ height: 14px;
1752
+ margin-left: auto;
1753
+ color: var(--sc-muted, #6b6b73);
1754
+ }
1755
+ .sc-mm-sub .sc-mm-label {
1756
+ flex: 1;
1757
+ min-width: 0;
1758
+ }
1759
+ .sc-mm-sub[data-locked] .sc-mm-tile {
1760
+ opacity: 0.5;
1761
+ filter: grayscale(1);
1762
+ }
1763
+
1764
+ .sc-mm-pill {
1765
+ flex: none;
1766
+ margin-left: auto;
1767
+ padding: 1px 6px;
1768
+ border-radius: 999px;
1769
+ box-shadow: inset 0 0 0 1px var(--sc-line, #e9e9eb);
1770
+ font-size: 10px;
1771
+ font-weight: 500;
1772
+ letter-spacing: 0.02em;
1773
+ color: var(--sc-muted, #6b6b73);
1774
+ }
1775
+ .sc-mm-sub .sc-mm-pill {
1776
+ margin-left: 0;
1777
+ }
1778
+
1779
+ .sc-mm-dot {
1780
+ flex: none;
1781
+ width: 6px;
1782
+ height: 6px;
1783
+ border-radius: 999px;
1784
+ background: color-mix(in srgb, var(--sc-text, #1a1a1c) 60%, transparent);
1785
+ }
1786
+
1787
+ .sc-mm-rule {
1788
+ height: 1px;
1789
+ margin: 4px -4px;
1790
+ background: var(--sc-line, #e9e9eb);
1791
+ }
1792
+ .sc-mm-label-row {
1793
+ padding: 6px 8px 4px;
1794
+ font-size: var(--sc-fs-label, 12px);
1795
+ color: var(--sc-muted, #6b6b73);
1796
+ }
1797
+
1798
+ .sc-mm-flyout {
1799
+ position: fixed;
1800
+ z-index: 1;
1801
+ padding: 4px;
1802
+ overflow-y: auto;
1803
+ overscroll-behavior: contain;
1804
+ border: 1px solid var(--sc-line, #e9e9eb);
1805
+ border-radius: calc(12px * var(--sc-scale, 1));
1806
+ background: var(--sc-tray-card, #ffffff);
1807
+ box-shadow:
1808
+ 0 12px 32px rgba(0, 0, 0, 0.12),
1809
+ 0 2px 6px rgba(0, 0, 0, 0.06);
1810
+ }
1811
+ .sc-mm-inline {
1812
+ position: relative;
1813
+ overflow: visible;
1814
+ }
1815
+
1547
1816
  .sc-atile .sc-runtime-logo {
1548
1817
  width: calc(25px * var(--sc-scale, 1));
1549
1818
  height: calc(25px * var(--sc-scale, 1));
@@ -1936,6 +2205,19 @@ a.sc-btn {
1936
2205
  color: var(--sc-muted, #6b6b73);
1937
2206
  }
1938
2207
 
2208
+ .sc-plan-feature::before {
2209
+ content: "✓ ";
2210
+ color: var(--sc-text, currentColor);
2211
+ }
2212
+
2213
+ .sc-plan-feature-absent {
2214
+ text-decoration: line-through;
2215
+ opacity: 0.6;
2216
+ }
2217
+ .sc-plan-feature-absent::before {
2218
+ content: "✕ ";
2219
+ }
2220
+
1939
2221
  .sc-plan-badge {
1940
2222
  font-size: var(--sc-fs-label, 11px);
1941
2223
  padding: 2px 6px;
@@ -4183,3 +4465,20 @@ li.sc-plan-row.sc-plan-current {
4183
4465
  transform: none;
4184
4466
  }
4185
4467
  }
4468
+
4469
+ .sc-redeem {
4470
+ display: flex;
4471
+ gap: 8px;
4472
+ align-items: stretch;
4473
+ }
4474
+ .sc-redeem .sc-input {
4475
+ flex: 1;
4476
+ min-width: 0;
4477
+ }
4478
+ .sc-redeem .sc-btn {
4479
+ flex: none;
4480
+ }
4481
+ .sc-redeem + .sc-error,
4482
+ .sc-redeem + .sc-note {
4483
+ margin-top: 8px;
4484
+ }
package/dist/types.d.ts CHANGED
@@ -127,9 +127,10 @@ export type Launch =
127
127
  * Derived from what they have connected rather than configured anywhere: see
128
128
  * `useModel()`, and `models.ts` in the panel for the three facts it reconciles.
129
129
  */
130
- export type ModelOptionKind = "auto" | "size" | "named";
130
+ export type ModelOptionKind = "auto" | "size" | "named" | "provider";
131
131
  export interface ModelOption {
132
- /** `auto`, `small`, `large`, or `<runtime>/<model>`. */
132
+ /** `auto`, `small`, `large`, `<runtime>/<model>`, or `@<provider>/<model>`
133
+ * for one of the app's own providers. */
133
134
  id: string;
134
135
  kind: ModelOptionKind;
135
136
  /** `Automatic`, `Fast`, `Opus`, `llama3.2:3b`. */
@@ -153,6 +154,87 @@ export interface ModelOption {
153
154
  costClass?: string;
154
155
  /** The machine advertising it, when exactly one of theirs does. */
155
156
  machine?: string;
157
+ /** The app's provider serving it, on a provider's model only. */
158
+ providerId?: string;
159
+ /** That provider's name, as the app's owner gave it. */
160
+ providerName?: string;
161
+ /** Whose API it is — `openai`, `groq` — for `<ProviderMark />`. */
162
+ providerMark?: string;
163
+ /** Listed and not pickable: the plan does not reach this provider, or its
164
+ * allowance is used. Draw it as an upgrade, never select it. */
165
+ locked?: "plan" | "allowance";
166
+ /** The app's own entry this was made from — a name set on Providers (its
167
+ * alias) or a `ModelChoice.id` in code: the app's words on a real model. */
168
+ choice?: string;
169
+ /** A name's run may land on any of these classes. */
170
+ costClasses?: string[];
171
+ /** A name's own logo, set on the dashboard: an https address or one emoji.
172
+ * Drawn in place of the provider's mark. */
173
+ icon?: string;
174
+ }
175
+ /** How a picker lays out the app's providers. See `UseModelOptions.layout`. */
176
+ export type ModelLayout = "providers" | "models" | "choices" | "automatic";
177
+ /** One entry the app writes itself, naming a model it exposes. */
178
+ export interface ModelChoice {
179
+ /** Stable; the pick is stored as `choice:<id>`. */
180
+ id: string;
181
+ /** "Fast", "Smart". */
182
+ label: string;
183
+ /** The quiet clause beside it. */
184
+ note?: string;
185
+ /** The model it runs, as the provider names it: `gpt-5-mini`. */
186
+ model: string;
187
+ /** Which provider, when two expose it: `openai`, its dashboard name, or its
188
+ * id. Any provider when left out. */
189
+ provider?: string;
190
+ /** Or one of the person's own agents, `claude-code`, for one of its models. */
191
+ runtime?: string;
192
+ }
193
+ /** One of the app's own providers, as `/v1/model-options` lists it. */
194
+ export interface ModelProviderWire {
195
+ id: string;
196
+ provider: string | null;
197
+ name: string;
198
+ cost_class: "open-weights" | "frontier";
199
+ locked: "plan" | "allowance" | null;
200
+ models: {
201
+ model: string;
202
+ default?: true;
203
+ }[];
204
+ }
205
+ /** One of the app's own names for a model, set on Providers. */
206
+ export interface ModelNameWire {
207
+ alias: string;
208
+ label: string;
209
+ note: string | null;
210
+ /** Its logo, set on the dashboard: an https address or one emoji. */
211
+ icon?: string;
212
+ cost_classes: string[];
213
+ provider: string | null;
214
+ locked: "plan" | "allowance" | null;
215
+ default?: true;
216
+ }
217
+ export interface ModelOptionsWire {
218
+ route: {
219
+ allow: string[];
220
+ prefer: string[];
221
+ };
222
+ providers: ModelProviderWire[];
223
+ names: ModelNameWire[];
224
+ layout: "providers" | "models" | "choices" | "automatic";
225
+ /** Whether Fast and Best are offered, as designed on Connect. Absent from a
226
+ * gateway older than the setting, which offered them. */
227
+ sizes?: boolean;
228
+ /** Which of Automatic, Fast and Best each layout offers, for an app whose
229
+ * code picks another than the one designed. Absent from an older gateway:
230
+ * `sizes` stands in for both sizes, and Automatic is always there. */
231
+ rowsByLayout?: Partial<Record<"providers" | "models" | "choices" | "automatic", {
232
+ auto: boolean;
233
+ fast: boolean;
234
+ best: boolean;
235
+ own?: boolean;
236
+ search?: boolean;
237
+ }>>;
156
238
  }
157
239
  export interface ModelGroup {
158
240
  /** Null for the first group — Automatic and the sizes head nothing. */
@@ -209,7 +291,15 @@ export interface Plan {
209
291
  price: number;
210
292
  interval: string;
211
293
  unit: string;
294
+ /** Runs a month on AI the app pays for. Null is unlimited. */
212
295
  included_units: number | null;
296
+ /** Runs a month on the user's own AI, counted apart from `included_units`.
297
+ * Null is unlimited. Optional so an older server that omits it parses. */
298
+ own_included_units?: number | null;
299
+ /** What `own_included_units` counts: `unit` unless the plan splits —
300
+ * "200 runs on your own AI, 2,000 credits on ours". Optional for older
301
+ * servers, which mean `unit`. */
302
+ own_unit?: string;
213
303
  /** What happens past the allowance: `stop` or `meter`. The difference
214
304
  * between a wall and a bill, and not guessable from the price. */
215
305
  overage: string;
@@ -232,10 +322,21 @@ export interface Plan {
232
322
  ask_to_connect?: boolean;
233
323
  /** Whether it can be taken without a payment rail. */
234
324
  selectable: boolean;
325
+ /** What the app calls a run, plural: "tasks". Optional for older servers;
326
+ * absent reads "runs". */
327
+ task_noun?: string;
328
+ /** About how many runs a credit plan's allowance buys at this app's typical
329
+ * run cost: "About 265 tasks a month". Null when it cannot be said. */
330
+ estimated_tasks?: number | null;
331
+ /** A plan sold by conversation: "Talk to us" links here. */
332
+ contact_url?: string | null;
333
+ /** "Everything in" this plan id; its card lists that, then `features_own`. */
334
+ features_from?: string | null;
335
+ features_own?: PlanFeatureWire[] | null;
235
336
  /**
236
337
  * WHAT CONNECTING THEIR OWN COMPUTE EARNS THEM.
237
338
  *
238
- * `none` | `unlimited` | `included` | `credit`. The one dimension no other
339
+ * `none` | `included` | `credit`. The one dimension no other
239
340
  * pricing component has a column for, and the reason a free plan on this
240
341
  * product can be worth choosing.
241
342
  */
@@ -244,16 +345,51 @@ export interface Plan {
244
345
  connected_included_units: number | null;
245
346
  /** Dollars back per interval on a `credit` reward. Zero otherwise. */
246
347
  connected_credit: number;
348
+ /** What the vendor's app enforces, as the card lists it (INT-91). Optional
349
+ * so an older server that omits it parses. */
350
+ features?: PlanFeatureWire[];
247
351
  }
248
352
  export interface CurrentPlan {
249
353
  plan_id: string;
354
+ /** Spent against `included_units`. */
250
355
  units_used: number;
356
+ /** Spent against `own_included_units`. Optional for older servers. */
357
+ own_units_used?: number;
358
+ /**
359
+ * WHAT SOBA ENFORCES: both allowances, what is left, when they reset. The
360
+ * gateway stops a run here; your app never has to. Optional for older
361
+ * servers, null when the plan is no longer in the catalogue.
362
+ */
363
+ limits?: {
364
+ unit: string;
365
+ included_units: number | null;
366
+ units_used: number;
367
+ remaining: number | null;
368
+ own_included_units: number | null;
369
+ /** What the own_* figures count. Optional for older servers: `unit`. */
370
+ own_unit?: string;
371
+ own_units_used: number;
372
+ own_remaining: number | null;
373
+ resets_at: string | null;
374
+ } | null;
375
+ /**
376
+ * WHAT YOUR APP ENFORCES (INT-91): feature id to value, as this person
377
+ * bought it. Soba stops nothing on these; read them and decide.
378
+ */
379
+ features?: Record<string, boolean | number | string | null>;
380
+ /** How many of each priced feature are in use, as your app last reported
381
+ * (`{ seats: 12 }`). Separate from `features`, where `seats` is the cap. */
382
+ quantities?: Record<string, number>;
251
383
  period_end: string | null;
252
384
  trial_ends_at: string | null;
385
+ /** A free plan past its time limit: runs are refused, and every feature
386
+ * reads as off, so stop what the plan carried too. Absent on older servers. */
387
+ ended?: boolean;
253
388
  }
254
389
  export interface ConnectApi {
255
390
  context(): Promise<ConnectContext>;
256
391
  machines(): Promise<Machine[]>;
392
+ modelOptions(): Promise<ModelOptionsWire | null>;
257
393
  plans(): Promise<{
258
394
  plans: Plan[];
259
395
  current: CurrentPlan | null;
@@ -347,3 +483,47 @@ export interface Message {
347
483
  role: "user" | "assistant";
348
484
  content: string;
349
485
  }
486
+ /** A plan feature: what the vendor's app enforces, never Soba (INT-91). */
487
+ export interface PlanFeatureWire {
488
+ id: string;
489
+ name: string;
490
+ type: "boolean" | "number" | "text";
491
+ /** `true`/`false`, a whole number, or null for unlimited. */
492
+ value: boolean | number | string | null;
493
+ unit?: string;
494
+ /** "month": the count starts over each billing period. Absent: never. */
495
+ resets?: "month";
496
+ /** Seats: a price per unit, in micros, past `included` units. */
497
+ price?: {
498
+ amountMicros: number;
499
+ included: number;
500
+ };
501
+ }
502
+ /**
503
+ * WHY A FEATURE IS NOT AVAILABLE, in the words an app branches on — the same
504
+ * four `soba.users.check` answers with. Declared here, not imported from
505
+ * connect-ui, because connect-ui is private and bundled: a published type
506
+ * that pointed at it would not resolve in an app.
507
+ */
508
+ export type FeatureDenial = "no_plan" | "plan_ended" | "not_included" | "limit_reached";
509
+ /** What the browser's check says about one feature. */
510
+ export interface FeatureCheck {
511
+ feature: string;
512
+ allowed: boolean;
513
+ /** Null when allowed. */
514
+ reason: FeatureDenial | null;
515
+ /** The plan's number for it: a cap, or null for unlimited. Null for a switch. */
516
+ limit: number | null;
517
+ /** How many are in use now. Zero for a switch. */
518
+ used: number;
519
+ /** What is left under the cap, never below zero. Null when unlimited or a switch. */
520
+ remaining: number | null;
521
+ unlimited: boolean;
522
+ }
523
+ /** One line of a plan's features on its card, and whether the plan has it —
524
+ * a "no" is drawn crossed out. Declared here for the reason `FeatureCheck`
525
+ * is: connect-ui is bundled, and a published type cannot name it. */
526
+ export interface FeatureItem {
527
+ text: string;
528
+ included: boolean;
529
+ }
@@ -14,9 +14,9 @@ import type { CurrentPlan, Plan } from "./types.js";
14
14
  * - `connected_reward: "included"` RAISES the ceiling to
15
15
  * `connected_included_units` while a machine is attached, awake or not,
16
16
  * because a larger allowance is a promise about the month.
17
- * - `connected_reward: "unlimited"` raises nothing. It takes the runs their own
18
- * AI SERVES out of the count, so the ceiling stays the plan's and the number
19
- * simply stops rising for those runs.
17
+ * - Runs their own AI SERVES never count toward `included_units`: they have
18
+ * their own allowance, `own_included_units` (INT-20). So the number here
19
+ * only rises for runs the app pays for, whatever the reward.
20
20
  * - `credit` touches neither: it settles at renewal.
21
21
  *
22
22
  * Those last three mirror `allowanceFor` in `@soba-platform/core`, which is the
@@ -94,5 +94,11 @@ export interface CurrentPlanView {
94
94
  /** One sentence, already chosen from the state above. The thing a billing
95
95
  * page has to get right and the reason this hook exists. */
96
96
  summary: string;
97
+ /**
98
+ * WHAT YOUR APP ENFORCES (INT-91): feature id to value, as this person
99
+ * bought it — `features.projects`, `features.export`. Soba stops nothing on
100
+ * these; everything above is what it does stop. Empty with no plan.
101
+ */
102
+ features: Record<string, boolean | number | string | null>;
97
103
  }
98
104
  export declare function useCurrentPlan(): CurrentPlanView;
@@ -0,0 +1,27 @@
1
+ import type { FeatureCheck } from "./types.js";
2
+ /**
3
+ * MAY THIS PERSON DO THIS — for drawing the button, not for trusting it.
4
+ *
5
+ * The same rule the server's `soba.users.check` runs, applied in the browser
6
+ * to the plan the provider already holds: the features they bought, how many
7
+ * are in use, and whether a free plan's time is up. So a "New workspace"
8
+ * button can be drawn disabled, with the reason, before anyone presses it.
9
+ *
10
+ * It decides what to SHOW. What actually happens is still the server's call:
11
+ * create the workspace behind `soba.users.check(user, "workspaces", { use: 1 })`
12
+ * and the count and the limit are tested together. After a server-side
13
+ * `check` with `use` or a `track`, call `refresh()` for the new count.
14
+ */
15
+ export interface FeatureView extends FeatureCheck {
16
+ /** The feature's name on the plan ("Workspaces"), or its id. */
17
+ name: string;
18
+ /** What it counts ("workspaces"), for "3 of 3 workspaces". */
19
+ unit: string | null;
20
+ loading: boolean;
21
+ error: Error | null;
22
+ /** Reload the plan and the counts. */
23
+ refresh(): Promise<void>;
24
+ }
25
+ export declare function useFeature(feature: string, options?: {
26
+ use?: number;
27
+ }): FeatureView;