@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.
- package/README.md +6 -2
- package/dist/packages/cli/src/server.js +74 -27
- package/dist/packages/core/src/billing.js +112 -0
- package/dist/packages/core/src/summary.js +69 -70
- package/dist/packages/core/src/types.js +10 -2
- package/dist/packages/receipt/src/index.js +158 -16
- package/dist/public/app.css +185 -0
- package/dist/public/apps/web/src/landing.js +8 -2
- package/dist/public/apps/web/src/main.js +243 -44
- package/dist/public/landing.css +185 -0
- package/dist/public/packages/core/src/billing.js +112 -0
- package/dist/public/packages/receipt/src/index.js +158 -16
- package/package.json +1 -1
|
@@ -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 &&
|
|
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('
|
|
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
|
-
${
|
|
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(
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
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
|
-
|
|
628
|
-
|
|
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' : ''}>
|
|
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
|
|
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(
|
|
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
|
|
764
|
-
|
|
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
|
|
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
|
|
852
|
-
if (
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
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
|
-
|
|
1023
|
-
select.
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
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) => {
|
package/dist/public/landing.css
CHANGED
|
@@ -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;
|