@subvalue/cli 0.1.5 → 0.1.6

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.
@@ -1,7 +1,17 @@
1
1
 
2
2
  import { product, productMark, productNameMarkup } from '../../../packages/ui/src/brand.js';
3
3
  document.title = product.name + ' — Local usage';
4
-
4
+
5
+
6
+
7
+
8
+
9
+
10
+ import {
11
+ billingForSelection,
12
+ withPeriodBilling,
13
+ showExpenseAction,
14
+ } from '../../../packages/core/src/billing.js';
5
15
  import {
6
16
  subscriptionPlans,
7
17
  subscriptionPlan,
@@ -60,6 +70,8 @@ const icon = (key ) => {
60
70
  <path d="${paths[key] ?? paths.token}" />
61
71
  </svg>`;
62
72
  };
73
+ let expenseProvider = null;
74
+ let expensesSaving = false;
63
75
  let settings ,
64
76
  sources = [],
65
77
  summary ,
@@ -521,6 +533,17 @@ function providerCards() {
521
533
  >
522
534
  </div>
523
535
  </div>
536
+ ${
537
+ showExpenseAction(p, summary.providers)
538
+ ? `<button type="button" class="provider-expense-toggle" data-expense-toggle="${p.provider}" aria-controls="card-expenses-${p.provider}" aria-expanded="false"><span>Categorize expenses for this period</span> ${icon('next')}</button>
539
+ <div id="card-expenses-${p.provider}" class="provider-expenses-popover" popover aria-label="${name(p.provider)} expenses">
540
+ <div class="expenses-heading"><h2>${name(p.provider)} · Expenses</h2><button type="button" class="text-button" popovertarget="card-expenses-${p.provider}" popovertargetaction="hide" aria-label="Close expenses">×</button></div>
541
+ <form class="provider-expenses-form" data-period-billing="${p.provider}">
542
+ ${billingFields(p.provider, true, billingForSelection(settings, p.provider, summary.range), true, 'card-')}
543
+ <div class="expense-actions"><small>${esc(periodLabel())}</small><button class="button small" type="submit" ${expensesSaving ? 'disabled' : ''}>Save</button></div>
544
+ <p class="billing-error" role="alert" hidden></p></form></div>`
545
+ : ''
546
+ }
524
547
  </article>`,
525
548
  )
526
549
  .join('')}
@@ -570,7 +593,24 @@ function overview() {
570
593
  c = comparisonForDisplay(summary);
571
594
  const active = summary.providers.filter((p) => p.records);
572
595
  const months = fullCalendarMonths(summary.range),
573
- showSubs = months !== null && active.length && active.every((p) => p.mode === 'SUBSCRIPTION');
596
+ showSubs = months !== null && active.length && (c.paid != null || c.scope.length > 0),
597
+ scopeLabel = c.scope.map(name).join(' + '),
598
+ first = summary.providers
599
+ .map((p) => p.first)
600
+ .filter((value) => !!value)
601
+ .sort()[0],
602
+ last = summary.providers
603
+ .map((p) => p.last)
604
+ .filter((value) => !!value)
605
+ .sort()
606
+ .at(-1),
607
+ historyDate = (value ) =>
608
+ new Date(value).toLocaleDateString('en-US', {
609
+ month: 'short',
610
+ day: 'numeric',
611
+ year: 'numeric',
612
+ timeZone: 'UTC',
613
+ });
574
614
  return /* HTML */ `<div class="page-heading">
575
615
  <div class="overview-title">
576
616
  <h1>Overview</h1>
@@ -582,14 +622,26 @@ function overview() {
582
622
  <div class="overview-layout">
583
623
  <div class="overview-main">
584
624
  <div class="metrics ${showSubs ? '' : 'single'}">
585
- ${metric('API equivalent', t.apiEquivalent === null ? (t.knownSubtotal !== null ? money(t.knownSubtotal) : t.records ? '—' : 'No data') : money(t.apiEquivalent), periodLabel(), 'api-metric')}${showSubs ? metric('Subscription', c.subscription === null ? 'Not set' : money(c.subscription), months === 1 ? 'Full month' : `${months} full months`) : ''}${showSubs ? metric(summary.historyPartial ? 'Observed value' : 'Value', c.value === null ? '—' : money(c.value, true), c.value === null ? 'Comparison unavailable' : summary.historyPartial ? 'Partial history' : '', c.value !== null ? (c.value >= 0 ? 'positive' : 'negative') : '') : ''}${showSubs ? metric('Value multiple', c.roi === null ? '—' : `${c.roi.toFixed(2)}<span class="multiply">×</span>`, c.breakEven ? `Break-even ${new Date(c.breakEven + 'T12:00:00').toLocaleDateString('en-US', { month: 'short', day: 'numeric' })}` : c.roi === null ? 'Unavailable' : t.apiEquivalent === null ? 'Priced portion' : '') : ''}
625
+ ${metric('API equivalent', t.apiEquivalent === null ? (t.knownSubtotal !== null ? money(t.knownSubtotal) : t.records ? '—' : 'No data') : money(t.apiEquivalent), periodLabel(), 'api-metric')}${showSubs ? metric(c.apiSpend ? 'You paid' : c.subscription === 0 ? 'API paid' : 'Subscription', money(c.paid ?? c.subscription), [scopeLabel, months === 1 ? 'Full month' : `${months} full months`].filter(Boolean).join(' · ')) : ''}${showSubs ? metric(summary.historyPartial ? 'Observed value' : 'Value', c.value === null ? '—' : money(c.value, true), [scopeLabel, c.value === null ? 'Comparison unavailable' : summary.historyPartial ? 'Partial history' : ''].filter(Boolean).join(' · '), c.value !== null ? (c.value >= 0 ? 'positive' : 'negative') : '') : ''}${showSubs ? metric('Value multiple', c.roi === null ? '—' : `${c.roi.toFixed(2)}<span class="multiply">×</span>`, [scopeLabel, c.breakEven ? `Break-even ${new Date(c.breakEven + 'T12:00:00').toLocaleDateString('en-US', { month: 'short', day: 'numeric' })}` : c.roi === null ? 'Unavailable' : t.apiEquivalent === null ? 'Priced portion' : ''].filter(Boolean).join(' · ')) : ''}
586
626
  </div>
587
627
  ${providerCards()}${chart()}
588
628
  <div class="overview-bottom">
589
629
  <span>${icon('shield')}Local analysis. Read-only sources.</span
590
630
  ><button class="text-button" data-page="settings">Manage sources ${icon('arrow')}</button>
591
631
  </div>
592
- ${!t.records ? '<section class="panel empty-panel"><h2>No data for this period</h2><button class="text-button" data-page="settings">View sources →</button></section>' : ''}
632
+ ${
633
+ !t.records
634
+ ? /* HTML */ `<section class="panel empty-panel">
635
+ <h2>No data for this period</h2>
636
+ ${first && last ? `<p class="empty-history">Available history · ${esc(historyDate(first))} – ${esc(historyDate(last))}</p>` : ''}<button
637
+ class="text-button"
638
+ data-page="settings"
639
+ >
640
+ View sources →
641
+ </button>
642
+ </section>`
643
+ : ''
644
+ }
593
645
  </div>
594
646
  ${receiptPanel(true)}
595
647
  </div>`;
@@ -615,21 +667,27 @@ function receiptPage() {
615
667
  </aside>
616
668
  </div>`;
617
669
  }
618
- function billingFields(p , firstRun = false) {
619
- const b = settings.billing[p],
620
- plan = subscriptionPlan(p, b.planId),
621
- subscribed = b.mode === 'SUBSCRIPTION',
670
+ function billingFields(
671
+ p ,
672
+ firstRun = false,
673
+ b = settings.billing[p],
674
+ expenses = false,
675
+ prefix = '',
676
+ ) {
677
+ const plan = subscriptionPlan(p, b.planId),
678
+ subscribed = b.mode === 'SUBSCRIPTION' || b.mode === 'MIXED',
622
679
  selection = subscribed
623
680
  ? (plan?.id ?? 'custom')
624
681
  : b.mode === 'NO_SUBSCRIPTION' || b.mode === 'API'
625
682
  ? 'none'
626
683
  : '';
627
- return /* HTML */ `<div class="billing-row">
628
- <label class="provider-setting-name" for="plan-${p}">${name(p)}</label>
684
+ const api = !subscribed || b.mode === 'MIXED';
685
+ return /* HTML */ `<div class="billing-row" data-billing-provider="${p}">
686
+ <label class="provider-setting-name" for="${prefix}plan-${p}">${name(p)}</label>
629
687
  <div class="subscription-fields">
630
- <label class="sr-only" for="plan-${p}">${name(p)} subscription plan</label
688
+ <label class="sr-only" for="${prefix}plan-${p}">${name(p)} subscription plan</label
631
689
  ><select
632
- id="plan-${p}"
690
+ id="${prefix}plan-${p}"
633
691
  name="plan-${p}"
634
692
  data-subscription-plan="${p}"
635
693
  ${firstRun ? 'required data-first-run="true"' : ''}
@@ -637,7 +695,9 @@ function billingFields(p , firstRun = false) {
637
695
  <option value="" ${selection === '' ? 'selected' : ''}>
638
696
  ${firstRun ? 'Choose subscription' : 'Not set'}
639
697
  </option>
640
- <option value="none" ${selection === 'none' ? 'selected' : ''}>No subscription</option>
698
+ <option value="none" ${selection === 'none' ? 'selected' : ''}>
699
+ No subscription · API
700
+ </option>
641
701
  ${subscriptionPlans
642
702
  .filter((plan) => plan.provider === p)
643
703
  .map(
@@ -650,7 +710,9 @@ function billingFields(p , firstRun = false) {
650
710
  <option value="custom" ${selection === 'custom' ? 'selected' : ''}>
651
711
  Custom amount
652
712
  </option></select
653
- ><label class="amount-field billing-amount ${subscribed ? '' : 'hidden'}" id="amount-${p}"
713
+ ><label
714
+ class="amount-field billing-amount ${subscribed ? '' : 'hidden'}"
715
+ id="${prefix}amount-${p}"
654
716
  ><span class="sr-only">${name(p)} monthly subscription in USD</span><span>$</span
655
717
  ><input
656
718
  name="monthly-${p}"
@@ -665,9 +727,31 @@ function billingFields(p , firstRun = false) {
665
727
  placeholder="Amount"
666
728
  /><span>/ month</span></label
667
729
  >
730
+ <label class="mixed-field ${subscribed ? '' : 'hidden'}"
731
+ ><input
732
+ type="checkbox"
733
+ name="mixed-${p}"
734
+ data-mixed-billing="${p}"
735
+ ${b.mode === 'MIXED' ? 'checked' : ''}
736
+ ${subscribed ? '' : 'disabled'}
737
+ />Subscription + API</label
738
+ >
739
+ ${expenses ? `<label class="api-spend-field ${api ? '' : 'hidden'}"><span>API paid</span><div class="amount-field"><span>$</span><input name="api-spend-${p}" type="number" min="0" max="1000000" step="0.01" value="${b.apiSpend ?? (b.mode === 'MIXED' ? 0 : '')}" ${api ? '' : 'disabled'} placeholder="Not set" aria-label="${name(p)} API paid in USD"/></div></label>` : ''}
668
740
  </div>
669
741
  </div>`;
670
742
  }
743
+ function expenseDialog() {
744
+ if (!expenseProvider) return '';
745
+ const billing = billingForSelection(settings, expenseProvider, summary.range);
746
+ return `<dialog id="expenses-dialog" aria-labelledby="expenses-title">
747
+ <div class="expenses-heading"><h2 id="expenses-title">${name(expenseProvider)} · Expenses</h2><button type="button" class="text-button" id="close-expenses" aria-label="Close expenses">×</button></div>
748
+ <form id="expenses-form">
749
+ <p class="expense-period">${esc(periodLabel())}</p>
750
+ ${billingFields(expenseProvider, true, billing, true, 'period-')}
751
+ <p class="expense-note">This period only · USD</p><p id="expense-error" role="alert" hidden></p>
752
+ <div class="expense-actions"><button type="button" class="text-button" id="reset-expenses">Use default</button><button class="button" type="submit">Save</button></div>
753
+ </form></dialog>`;
754
+ }
671
755
  function settingsPage() {
672
756
  return /* HTML */ `<div class="page-heading">
673
757
  <div><h1>Settings</h1></div>
@@ -740,7 +824,11 @@ function settingsPage() {
740
824
  <h2>Billing</h2>
741
825
  ${(['codex', 'claude'] )
742
826
  .filter((p) => sources.find((s) => s.provider === p)?.detected)
743
- .map((p) => billingFields(p))
827
+ .map(
828
+ (p) =>
829
+ billingFields(p) +
830
+ `<button type="button" class="text-button settings-expenses" data-expenses="${p}">${name(p)} · Monthly expenses</button>`,
831
+ )
744
832
  .join('')}
745
833
  <details class="compact-details">
746
834
  <summary>Details</summary>
@@ -760,8 +848,8 @@ function settingsPage() {
760
848
  </p>
761
849
  <p>
762
850
  These are your declarations, not inferred billing history. Amounts are in USD.
763
- Comparisons use the full monthly price for full calendar months only. No subscription
764
- means no subscription comparison.
851
+ Comparisons use full calendar months. API spend belongs to the selected month; missing
852
+ amounts stay unknown. Value multiple is API equivalent divided by total paid.
765
853
  </p>
766
854
  </details>
767
855
  </section>
@@ -836,6 +924,14 @@ function render() {
836
924
  </footer>
837
925
  </div>`
838
926
  }`;
927
+ app.insertAdjacentHTML('beforeend', expenseDialog());
928
+ const expense = document.querySelector ('#expenses-dialog');
929
+ if (expense) {
930
+ expense.addEventListener('cancel', () => {
931
+ expenseProvider = null;
932
+ });
933
+ expense.showModal();
934
+ }
839
935
  const canvas = document.querySelector ('#receipt-canvas');
840
936
  if (canvas) drawReceipt(canvas, displayedReceipt, settings.receiptTheme ?? 'dark');
841
937
  const dialog = document.querySelector ('#onboarding');
@@ -845,24 +941,36 @@ function render() {
845
941
  }
846
942
  bind();
847
943
  }
848
- function readBilling(form ) {
944
+ function billingFromForm(form , p ) {
849
945
  const data = new FormData(form);
946
+ const selection = data.get(`plan-${p}`);
947
+ if (selection === null) return null;
948
+ const subscribed = selection !== 'none' && selection !== '',
949
+ mode = subscribed ? (data.has(`mixed-${p}`) ? 'MIXED' : 'SUBSCRIPTION') : 'API',
950
+ plan = subscriptionPlan(p, String(selection)),
951
+ amount = data.get(`monthly-${p}`),
952
+ api = data.get(`api-spend-${p}`);
953
+ return {
954
+ mode,
955
+ monthly: subscribed
956
+ ? (plan?.monthly ?? (amount !== '' && amount !== null ? Number(amount) : null))
957
+ : null,
958
+ ...(subscribed ? { planId: plan?.id ?? null } : {}),
959
+ ...(mode !== 'SUBSCRIPTION'
960
+ ? { apiSpend: api !== '' && api !== null ? Number(api) : null }
961
+ : {}),
962
+ };
963
+ }
964
+ function readBilling(form ) {
850
965
  for (const p of ['codex', 'claude'] ) {
851
- const selection = data.get(`plan-${p}`);
852
- if (selection !== null) {
853
- const mode =
854
- selection === 'none' ? 'NO_SUBSCRIPTION' : selection === '' ? 'UNKNOWN' : 'SUBSCRIPTION';
855
- const amount = data.get(`monthly-${p}`),
856
- plan = subscriptionPlan(p, String(selection));
857
- settings.billing[p] = {
858
- mode,
859
- monthly:
860
- mode === 'SUBSCRIPTION'
861
- ? (plan?.monthly ?? (amount !== '' && amount !== null ? Number(amount) : null))
862
- : null,
863
- ...(mode === 'SUBSCRIPTION' ? { planId: plan?.id ?? null } : {}),
864
- };
865
- }
966
+ const billing = billingFromForm(form, p);
967
+ if (!billing) continue;
968
+ // API payments are entered in the provider card for a specific period.
969
+ settings.billing[p] = {
970
+ mode: billing.mode,
971
+ monthly: billing.monthly,
972
+ ...(billing.planId !== undefined ? { planId: billing.planId } : {}),
973
+ };
866
974
  }
867
975
  }
868
976
  async function save() {
@@ -1019,19 +1127,110 @@ function bind() {
1019
1127
  const data = new FormData(e.currentTarget );
1020
1128
  void choosePeriod('month', from, to, String(data.get('month')));
1021
1129
  });
1022
- document.querySelectorAll ('[data-subscription-plan]').forEach((select) =>
1023
- select.addEventListener('change', () => {
1024
- const p = select.dataset.subscriptionPlan ,
1025
- plan = subscriptionPlan(p, select.value),
1026
- input = document.querySelector (`[name="monthly-${p}"]`) ;
1027
- const subscribed = select.value !== '' && select.value !== 'none';
1028
- document.querySelector(`#amount-${p}`)?.classList.toggle('hidden', !subscribed);
1029
- input.disabled = !subscribed;
1030
- input.required = select.dataset.firstRun === 'true' && subscribed;
1031
- input.readOnly = !!plan;
1032
- if (plan) input.value = plan.monthly.toFixed(2);
1130
+ const updateBillingFields = (row ) => {
1131
+ const select = row.querySelector ('[data-subscription-plan]') ;
1132
+ const p = select.dataset.subscriptionPlan ,
1133
+ plan = subscriptionPlan(p, select.value),
1134
+ input = row.querySelector (`[name="monthly-${p}"]`) ,
1135
+ mixed = row.querySelector ('[data-mixed-billing]') ;
1136
+ const subscribed = select.value !== '' && select.value !== 'none';
1137
+ row.querySelector('.billing-amount')?.classList.toggle('hidden', !subscribed);
1138
+ row.querySelector('.mixed-field')?.classList.toggle('hidden', !subscribed);
1139
+ input.disabled = mixed.disabled = !subscribed;
1140
+ input.required = select.dataset.firstRun === 'true' && subscribed;
1141
+ input.readOnly = !!plan;
1142
+ if (plan) input.value = plan.monthly.toFixed(2);
1143
+ const api = !subscribed || mixed.checked;
1144
+ row.querySelector('.api-spend-field')?.classList.toggle('hidden', !api);
1145
+ const apiInput = row.querySelector (`[name="api-spend-${p}"]`);
1146
+ if (apiInput) {
1147
+ apiInput.disabled = !api;
1148
+ if (mixed.checked && subscribed && apiInput.value === '') apiInput.value = '0';
1149
+ }
1150
+ };
1151
+ document.querySelectorAll ('[data-billing-provider]').forEach((row) => {
1152
+ row
1153
+ .querySelectorAll('select, [data-mixed-billing]')
1154
+ .forEach((field) => field.addEventListener('change', () => updateBillingFields(row)));
1155
+ });
1156
+ document.querySelectorAll ('[data-expenses]').forEach((button) =>
1157
+ button.addEventListener('click', () => {
1158
+ expenseProvider = button.dataset.expenses ;
1159
+ render();
1160
+ }),
1161
+ );
1162
+ document.querySelectorAll ('[data-expense-toggle]').forEach((button) => {
1163
+ const panel = document.getElementById(button.getAttribute('aria-controls') ) ;
1164
+ panel.addEventListener('toggle', () =>
1165
+ button.setAttribute('aria-expanded', String(panel.matches(':popover-open'))),
1166
+ );
1167
+ button.addEventListener('click', () => {
1168
+ if (panel.matches(':popover-open')) {
1169
+ panel.hidePopover();
1170
+ return;
1171
+ }
1172
+ panel.showPopover();
1173
+ const bounds = button.getBoundingClientRect();
1174
+ panel.style.left =
1175
+ Math.max(
1176
+ 16,
1177
+ Math.min(innerWidth - panel.offsetWidth - 16, bounds.right - panel.offsetWidth),
1178
+ ) + 'px';
1179
+ panel.style.top =
1180
+ Math.max(16, Math.min(innerHeight - panel.offsetHeight - 16, bounds.bottom + 8)) + 'px';
1181
+ panel.querySelector ('select')?.focus();
1182
+ });
1183
+ });
1184
+ document.querySelector('#close-expenses')?.addEventListener('click', () => {
1185
+ expenseProvider = null;
1186
+ render();
1187
+ });
1188
+ const persistExpenses = async (provider , form , reset = false) => {
1189
+ if (expensesSaving) return;
1190
+ expensesSaving = true;
1191
+ const billing = billingFromForm(form, provider),
1192
+ previous = settings,
1193
+ next = withPeriodBilling(settings, provider, summary.range, reset ? null : billing);
1194
+ document
1195
+ .querySelectorAll ('[data-period-billing] button, #expenses-form button')
1196
+ .forEach((button) => (button.disabled = true));
1197
+ try {
1198
+ await api('/api/settings', 'POST', next);
1199
+ settings = next;
1200
+ await load();
1201
+ expenseProvider = null;
1202
+ expensesSaving = false;
1203
+ render();
1204
+ toast('Saved locally');
1205
+ } catch {
1206
+ settings = previous;
1207
+ expensesSaving = false;
1208
+ const error = form.querySelector ('.billing-error, #expense-error') ;
1209
+ error.hidden = false;
1210
+ error.textContent = 'Unable to save expenses';
1211
+ document
1212
+ .querySelectorAll ('[data-period-billing] button, #expenses-form button')
1213
+ .forEach((button) => (button.disabled = false));
1214
+ }
1215
+ };
1216
+ document.querySelectorAll ('[data-period-billing]').forEach((form) =>
1217
+ form.addEventListener('submit', (e) => {
1218
+ e.preventDefault();
1219
+ void persistExpenses(form.dataset.periodBilling , form);
1033
1220
  }),
1034
1221
  );
1222
+ document.querySelector ('#expenses-form')?.addEventListener('submit', (e) => {
1223
+ e.preventDefault();
1224
+ if (expenseProvider) void persistExpenses(expenseProvider, e.currentTarget );
1225
+ });
1226
+ document.querySelector('#reset-expenses')?.addEventListener('click', () => {
1227
+ if (expenseProvider)
1228
+ void persistExpenses(
1229
+ expenseProvider,
1230
+ document.querySelector ('#expenses-form') ,
1231
+ true,
1232
+ );
1233
+ });
1035
1234
  document
1036
1235
  .querySelector ('#settings-form')
1037
1236
  ?.addEventListener('submit', async (e) => {
@@ -308,6 +308,12 @@ p {
308
308
  font-weight: 550;
309
309
  letter-spacing: -0.3px;
310
310
  }
311
+ .empty-history {
312
+ color: var(--muted);
313
+ font-size: 12px;
314
+ line-height: 1.6;
315
+ margin: 10px 0 16px;
316
+ }
311
317
  @media (max-width: 760px) {
312
318
  .overview-title {
313
319
  flex-direction: column;
@@ -2313,6 +2319,185 @@ dialog {
2313
2319
  max-height: calc(100dvh - 32px);
2314
2320
  overflow-y: auto;
2315
2321
  }
2322
+ #expenses-dialog {
2323
+ width: min(440px, calc(100vw - 32px));
2324
+ max-height: calc(100dvh - 32px);
2325
+ overflow: auto;
2326
+ }
2327
+ .expenses-heading {
2328
+ display: flex;
2329
+ align-items: center;
2330
+ justify-content: space-between;
2331
+ gap: 12px;
2332
+ }
2333
+ .expenses-heading h2 {
2334
+ margin: 0;
2335
+ font-size: 18px;
2336
+ }
2337
+ .expenses-heading button {
2338
+ font-size: 24px;
2339
+ }
2340
+ .expense-month {
2341
+ display: flex;
2342
+ align-items: center;
2343
+ justify-content: space-between;
2344
+ gap: 16px;
2345
+ margin: 24px 0;
2346
+ color: var(--muted);
2347
+ font-size: 13px;
2348
+ }
2349
+ #expenses-dialog .billing-row {
2350
+ display: block;
2351
+ }
2352
+ #expenses-dialog .provider-setting-name {
2353
+ display: none;
2354
+ }
2355
+ #expenses-dialog .subscription-fields {
2356
+ display: flex;
2357
+ flex-direction: column;
2358
+ align-items: stretch;
2359
+ gap: 14px;
2360
+ }
2361
+ #expenses-dialog select {
2362
+ width: 100%;
2363
+ }
2364
+ .mixed-field {
2365
+ display: flex;
2366
+ align-items: center;
2367
+ gap: 8px;
2368
+ color: var(--muted);
2369
+ font-size: 12px;
2370
+ }
2371
+ .api-spend-field {
2372
+ display: flex;
2373
+ align-items: center;
2374
+ justify-content: space-between;
2375
+ gap: 16px;
2376
+ font-size: 13px;
2377
+ }
2378
+ .expense-note {
2379
+ color: var(--muted);
2380
+ font-size: 12px;
2381
+ margin: 20px 0;
2382
+ }
2383
+ .expense-actions {
2384
+ display: flex;
2385
+ align-items: center;
2386
+ justify-content: space-between;
2387
+ gap: 12px;
2388
+ }
2389
+ .expense-actions .button {
2390
+ width: auto;
2391
+ }
2392
+ #expense-error {
2393
+ color: var(--red);
2394
+ font-size: 12px;
2395
+ }
2396
+ .subscription-fields {
2397
+ flex-wrap: wrap;
2398
+ }
2399
+ .subscription-fields .mixed-field {
2400
+ flex-basis: 100%;
2401
+ }
2402
+ .hidden {
2403
+ display: none !important;
2404
+ }
2405
+ .provider-expenses-form {
2406
+ border-top: 1px solid var(--border);
2407
+ margin-top: 18px;
2408
+ padding-top: 16px;
2409
+ }
2410
+ .provider-expense-toggle {
2411
+ display: flex;
2412
+ align-items: center;
2413
+ justify-content: space-between;
2414
+ gap: 12px;
2415
+ width: 100%;
2416
+ min-height: 40px;
2417
+ margin: 14px 0 0;
2418
+ padding: 10px 12px;
2419
+ color: var(--green);
2420
+ background: #37ec850c;
2421
+ border: 1px solid #37ec8540;
2422
+ border-radius: 4px;
2423
+ font-size: 12px;
2424
+ text-align: left;
2425
+ line-height: 1.4;
2426
+ }
2427
+ .provider-expense-toggle:hover {
2428
+ background: #37ec8518;
2429
+ border-color: #37ec8580;
2430
+ }
2431
+ .provider-expense-toggle svg {
2432
+ width: 13px;
2433
+ height: 13px;
2434
+ flex-shrink: 0;
2435
+ }
2436
+ .provider-expense-toggle[aria-expanded='true'] {
2437
+ border-color: var(--green);
2438
+ }
2439
+ .provider-expenses-popover {
2440
+ position: fixed;
2441
+ inset: auto;
2442
+ margin: 0;
2443
+ width: min(350px, calc(100vw - 32px));
2444
+ max-height: calc(100dvh - 32px);
2445
+ overflow-y: auto;
2446
+ padding: 20px;
2447
+ color: var(--text);
2448
+ background: #14191e;
2449
+ border: 1px solid #3a4249;
2450
+ border-radius: 6px;
2451
+ box-shadow: 0 18px 48px #0009;
2452
+ }
2453
+ .provider-expenses-popover .expenses-heading h2 {
2454
+ font-size: 14px;
2455
+ }
2456
+ .provider-expenses-form .billing-row {
2457
+ display: block;
2458
+ }
2459
+ .provider-expenses-form .provider-setting-name {
2460
+ display: none;
2461
+ }
2462
+ .provider-expenses-form .subscription-fields {
2463
+ display: flex;
2464
+ flex-direction: column;
2465
+ align-items: stretch;
2466
+ gap: 12px;
2467
+ }
2468
+ .provider-expenses-form select {
2469
+ width: 100%;
2470
+ min-width: 0;
2471
+ }
2472
+ .provider-expenses-form .mixed-field {
2473
+ flex-basis: auto;
2474
+ }
2475
+ .provider-expenses-form .amount-field {
2476
+ width: fit-content;
2477
+ max-width: 100%;
2478
+ }
2479
+ .provider-expenses-form .api-spend-field .amount-field {
2480
+ width: 130px;
2481
+ }
2482
+ .provider-expenses-form .expense-actions {
2483
+ margin-top: 14px;
2484
+ }
2485
+ .provider-expenses-form small,
2486
+ .expense-period {
2487
+ color: var(--muted);
2488
+ font-size: 12px;
2489
+ }
2490
+ .billing-error {
2491
+ color: var(--red);
2492
+ font-size: 12px;
2493
+ }
2494
+ .settings-expenses {
2495
+ margin: 12px 0 22px;
2496
+ font-size: 12px;
2497
+ }
2498
+ .provider-grid {
2499
+ align-items: stretch;
2500
+ }
2316
2501
 
2317
2502
  .hero h1 .brand-name {
2318
2503
  font-weight: 850;