@subvalue/cli 0.1.4 → 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.
@@ -295,6 +295,35 @@ p {
295
295
  gap: 20px;
296
296
  margin-bottom: 22px;
297
297
  }
298
+ .overview-title {
299
+ display: flex;
300
+ flex-wrap: wrap;
301
+ align-items: baseline;
302
+ gap: 6px 14px;
303
+ min-width: 0;
304
+ }
305
+ .selected-period {
306
+ color: var(--green);
307
+ font-size: 17px;
308
+ font-weight: 550;
309
+ letter-spacing: -0.3px;
310
+ }
311
+ .empty-history {
312
+ color: var(--muted);
313
+ font-size: 12px;
314
+ line-height: 1.6;
315
+ margin: 10px 0 16px;
316
+ }
317
+ @media (max-width: 760px) {
318
+ .overview-title {
319
+ flex-direction: column;
320
+ align-items: start;
321
+ gap: 5px;
322
+ }
323
+ .selected-period {
324
+ font-size: 15px;
325
+ }
326
+ }
298
327
  .eyebrow {
299
328
  font: 9px var(--mono);
300
329
  letter-spacing: 1.7px;
@@ -2223,7 +2252,7 @@ dialog::backdrop {
2223
2252
  }
2224
2253
  }
2225
2254
  .billing-row {
2226
- grid-template-columns: 100px 145px minmax(0, 1fr);
2255
+ grid-template-columns: 100px minmax(0, 1fr);
2227
2256
  gap: 16px;
2228
2257
  align-items: start;
2229
2258
  }
@@ -2242,7 +2271,7 @@ dialog::backdrop {
2242
2271
  .amount-field input[readonly] {
2243
2272
  color: var(--muted);
2244
2273
  }
2245
- .subscription-fields.hidden {
2274
+ .billing-amount.hidden {
2246
2275
  display: none;
2247
2276
  }
2248
2277
  .compact-details a {
@@ -2290,6 +2319,185 @@ dialog {
2290
2319
  max-height: calc(100dvh - 32px);
2291
2320
  overflow-y: auto;
2292
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
+ }
2293
2501
 
2294
2502
  .hero h1 .brand-name {
2295
2503
  font-weight: 850;
@@ -2314,13 +2522,27 @@ dialog {
2314
2522
  justify-content: space-between;
2315
2523
  align-items: center;
2316
2524
  }
2317
- .github-link {
2318
- font-size: 12px;
2525
+ .landing-links {
2526
+ display: flex;
2527
+ align-items: center;
2528
+ gap: 22px;
2529
+ }
2530
+ .github-link,
2531
+ .install-link {
2532
+ font-size: 13px;
2319
2533
  color: var(--muted);
2320
2534
  display: flex;
2535
+ align-items: center;
2536
+ min-height: 44px;
2321
2537
  gap: 8px;
2322
2538
  }
2323
- .github-link span {
2539
+ .github-link span,
2540
+ .install-link span {
2541
+ color: var(--green);
2542
+ }
2543
+ .github-link:hover,
2544
+ .install-link:hover,
2545
+ .trust-row a:hover {
2324
2546
  color: var(--green);
2325
2547
  }
2326
2548
  .hero {
@@ -2393,13 +2615,16 @@ dialog {
2393
2615
  }
2394
2616
  .trust-row {
2395
2617
  display: flex;
2618
+ flex-wrap: wrap;
2396
2619
  justify-content: center;
2397
2620
  gap: 25px;
2621
+ row-gap: 12px;
2398
2622
  margin-top: 22px;
2399
2623
  color: #a3b0a6;
2400
2624
  font-size: 10px;
2401
2625
  }
2402
- .trust-row span {
2626
+ .trust-row span,
2627
+ .trust-row a {
2403
2628
  white-space: nowrap;
2404
2629
  }
2405
2630
  .release-note {
@@ -2415,7 +2640,7 @@ dialog {
2415
2640
  .preview-label {
2416
2641
  display: flex;
2417
2642
  justify-content: space-between;
2418
- font: 8px var(--mono);
2643
+ font: 11px var(--mono);
2419
2644
  letter-spacing: 1.2px;
2420
2645
  color: var(--muted);
2421
2646
  margin-bottom: 12px;
@@ -2433,7 +2658,7 @@ dialog {
2433
2658
  gap: 29px;
2434
2659
  padding: 20px 24px;
2435
2660
  border-bottom: 1px solid var(--border);
2436
- font-size: 10px;
2661
+ font-size: 12px;
2437
2662
  color: var(--muted);
2438
2663
  }
2439
2664
  .preview-topbar .brand {
@@ -2458,7 +2683,7 @@ dialog {
2458
2683
  }
2459
2684
  .preview-date {
2460
2685
  margin-left: auto;
2461
- font: 8px var(--mono);
2686
+ font: 11px var(--mono);
2462
2687
  }
2463
2688
  .preview-body {
2464
2689
  display: grid;
@@ -2490,7 +2715,7 @@ dialog {
2490
2715
  padding-top: 15px;
2491
2716
  }
2492
2717
  .preview-metrics span {
2493
- font-size: 8px;
2718
+ font-size: 12px;
2494
2719
  color: var(--muted);
2495
2720
  }
2496
2721
  .preview-metrics strong {
@@ -2525,7 +2750,7 @@ dialog {
2525
2750
  font-weight: 500;
2526
2751
  }
2527
2752
  .preview-provider > div span {
2528
- font-size: 9px;
2753
+ font-size: 12px;
2529
2754
  color: var(--muted);
2530
2755
  }
2531
2756
  .preview-provider b {
@@ -2535,7 +2760,7 @@ dialog {
2535
2760
  letter-spacing: -0.5px;
2536
2761
  }
2537
2762
  .preview-active {
2538
- font: 8px var(--mono);
2763
+ font: 11px var(--mono);
2539
2764
  background: #153020;
2540
2765
  border: 1px solid #284f35;
2541
2766
  color: var(--green);
@@ -2552,16 +2777,16 @@ dialog {
2552
2777
  display: flex;
2553
2778
  justify-content: space-between;
2554
2779
  gap: 12px;
2555
- font-size: 9px;
2780
+ font-size: 12px;
2556
2781
  margin-bottom: 19px;
2557
2782
  }
2558
2783
  .preview-chart strong {
2559
- font-size: 11px;
2784
+ font-size: 13px;
2560
2785
  font-weight: 450;
2561
2786
  }
2562
2787
  .preview-chart > div:first-child span {
2563
2788
  color: var(--green);
2564
- font-size: 8px;
2789
+ font-size: 11px;
2565
2790
  }
2566
2791
  .preview-bars {
2567
2792
  height: 143px;
@@ -2587,7 +2812,7 @@ dialog {
2587
2812
  .preview-chart-axis {
2588
2813
  display: flex;
2589
2814
  justify-content: space-between;
2590
- font: 8px var(--mono);
2815
+ font: 11px var(--mono);
2591
2816
  color: var(--muted);
2592
2817
  margin-top: 12px;
2593
2818
  }
@@ -2604,6 +2829,9 @@ dialog {
2604
2829
  display: flex;
2605
2830
  flex-direction: column;
2606
2831
  justify-content: start;
2832
+ min-width: 0;
2833
+ width: 100%;
2834
+ max-width: 368px;
2607
2835
  gap: 13px;
2608
2836
  }
2609
2837
  .preview-receipt canvas {
@@ -2721,7 +2949,7 @@ dialog {
2721
2949
  padding: 14px 10px;
2722
2950
  }
2723
2951
  .preview-metrics span {
2724
- font-size: 7px;
2952
+ font-size: 12px;
2725
2953
  }
2726
2954
  }
2727
2955
  @media (max-width: 760px) {
@@ -2767,13 +2995,13 @@ dialog {
2767
2995
  margin-top: 13px;
2768
2996
  }
2769
2997
  .preview-label {
2770
- font-size: 6px;
2998
+ font-size: 11px;
2771
2999
  letter-spacing: 0.8px;
2772
3000
  }
2773
3001
  .preview-topbar {
2774
3002
  padding: 18px;
2775
3003
  gap: 19px;
2776
- font-size: 8px;
3004
+ font-size: 12px;
2777
3005
  }
2778
3006
  .preview-topbar .brand {
2779
3007
  font-size: 14px;
@@ -2783,7 +3011,7 @@ dialog {
2783
3011
  display: none;
2784
3012
  }
2785
3013
  .preview-date {
2786
- font-size: 8px;
3014
+ font-size: 11px;
2787
3015
  }
2788
3016
  .preview-body {
2789
3017
  grid-template-columns: 1fr;
@@ -2802,7 +3030,7 @@ dialog {
2802
3030
  font-size: 25px;
2803
3031
  }
2804
3032
  .preview-metrics span {
2805
- font-size: 9px;
3033
+ font-size: 12px;
2806
3034
  }
2807
3035
  .preview-provider {
2808
3036
  padding: 17px;
@@ -2897,7 +3125,7 @@ dialog {
2897
3125
  color: #a8b7af;
2898
3126
  }
2899
3127
  .product-preview {
2900
- max-width: 1180px;
3128
+ max-width: 1240px;
2901
3129
  }
2902
3130
  .preview-window {
2903
3131
  background: #0b0d10;
@@ -2933,7 +3161,7 @@ dialog {
2933
3161
  letter-spacing: -0.9px;
2934
3162
  }
2935
3163
  .preview-metrics span {
2936
- font-size: 9px;
3164
+ font-size: 12px;
2937
3165
  color: #b7c0c7;
2938
3166
  }
2939
3167
  .preview-provider,
@@ -3039,13 +3267,13 @@ dialog {
3039
3267
  padding-top: 16px;
3040
3268
  }
3041
3269
  .preview-metrics span {
3042
- font-size: 10px;
3270
+ font-size: 12px;
3043
3271
  }
3044
3272
  .preview-metrics strong {
3045
3273
  font-size: 27px;
3046
3274
  }
3047
3275
  .preview-receipt {
3048
- max-width: 335px;
3276
+ max-width: 368px;
3049
3277
  }
3050
3278
  .preview-bars {
3051
3279
  height: 138px;
@@ -3067,7 +3295,7 @@ dialog {
3067
3295
  padding-bottom: 36px;
3068
3296
  }
3069
3297
  .preview-label {
3070
- font-size: 7px;
3298
+ font-size: 11px;
3071
3299
  }
3072
3300
  .copy-command {
3073
3301
  min-height: 40px;
@@ -3092,7 +3320,7 @@ dialog {
3092
3320
  object-fit: contain;
3093
3321
  }
3094
3322
  .preview-body {
3095
- grid-template-columns: minmax(0, 1fr) 285px;
3323
+ grid-template-columns: minmax(0, 1fr) 368px;
3096
3324
  }
3097
3325
  .preview-receipt canvas {
3098
3326
  filter: drop-shadow(0 2px 2px #0007) drop-shadow(0 10px 14px #0006);
@@ -3107,9 +3335,17 @@ dialog {
3107
3335
  .preview-bars i {
3108
3336
  background: linear-gradient(to bottom, #37ec85, #278451);
3109
3337
  }
3338
+ @media (max-width: 1100px) {
3339
+ .preview-metrics {
3340
+ grid-template-columns: repeat(2, minmax(0, 1fr));
3341
+ }
3342
+ }
3110
3343
  @media (max-width: 1000px) {
3111
3344
  .preview-body {
3112
- grid-template-columns: minmax(0, 1fr) 245px;
3345
+ grid-template-columns: 1fr;
3346
+ }
3347
+ .preview-receipt {
3348
+ margin-inline: auto;
3113
3349
  }
3114
3350
  }
3115
3351
  @media (max-width: 760px) {
@@ -3117,7 +3353,7 @@ dialog {
3117
3353
  grid-template-columns: 1fr;
3118
3354
  }
3119
3355
  .preview-receipt {
3120
- max-width: 335px;
3356
+ max-width: 368px;
3121
3357
  }
3122
3358
  .preview-topbar .brand-mark {
3123
3359
  width: 20px;
@@ -0,0 +1,112 @@
1
+
2
+
3
+ import { fullCalendarMonths, shiftCalendarMonth } from './calendar.js';
4
+
5
+ function civilDate(date ) {
6
+ return `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, '0')}-${String(date.getDate()).padStart(2, '0')}`;
7
+ }
8
+ export function billingPeriodKey(range ) {
9
+ return (
10
+ (range.calendarFrom ?? civilDate(new Date(range.from))) +
11
+ ':' +
12
+ (range.calendarTo ?? civilDate(new Date(Date.parse(range.until) - 1)))
13
+ );
14
+ }
15
+ export function withPeriodBilling(
16
+ settings ,
17
+ provider ,
18
+ range ,
19
+ billing ,
20
+ ) {
21
+ const next = structuredClone(settings),
22
+ exactKey = billingPeriodKey(range),
23
+ monthly = fullCalendarMonths(range) === 1,
24
+ key = monthly ? exactKey.slice(0, 7) : exactKey,
25
+ declarations = monthly ? (next.monthlyBilling ??= {}) : (next.periodBilling ??= {});
26
+ // A rolling range can share dates with a calendar month. Editing that month
27
+ // supersedes an earlier exact-date declaration instead of leaving a stale override.
28
+ if (monthly && next.periodBilling?.[exactKey]?.[provider]) {
29
+ delete next.periodBilling[exactKey][provider];
30
+ if (!Object.keys(next.periodBilling[exactKey]).length) delete next.periodBilling[exactKey];
31
+ }
32
+ declarations[key] ??= {};
33
+ if (billing) declarations[key][provider] = billing;
34
+ else delete declarations[key][provider];
35
+ if (!Object.keys(declarations[key]).length) delete declarations[key];
36
+ return next;
37
+ }
38
+ export function billingForSelection(settings , provider , range ) {
39
+ const exact = settings.periodBilling?.[billingPeriodKey(range)]?.[provider];
40
+ if (exact) return exact;
41
+ if (fullCalendarMonths(range) === 1)
42
+ return billingForMonth(settings, provider, billingPeriodKey(range).slice(0, 7));
43
+ return billingForMonth({ ...settings, monthlyBilling: undefined }, provider, '');
44
+ }
45
+
46
+ /** Keep the dashboard action limited to mixed billing or a second provider without a plan. */
47
+ export function showExpenseAction(
48
+ provider ,
49
+ providers ,
50
+ ) {
51
+ if (!provider.records) return false;
52
+ if (provider.mode === 'MIXED') return true;
53
+ return (
54
+ (provider.mode === 'API' ||
55
+ provider.mode === 'NO_SUBSCRIPTION' ||
56
+ provider.mode === 'UNKNOWN') &&
57
+ providers.some(
58
+ (other) =>
59
+ other.provider !== provider.provider &&
60
+ other.records > 0 &&
61
+ (other.mode === 'SUBSCRIPTION' || other.mode === 'MIXED') &&
62
+ (other.monthly != null || (other.comparison.subscription ?? 0) > 0),
63
+ )
64
+ );
65
+ }
66
+
67
+ /** A default is a user preference, not evidence of how a historical request was billed. */
68
+ export function billingForMonth(settings , provider , month ) {
69
+ const saved = settings.monthlyBilling?.[month]?.[provider] ?? settings.billing[provider];
70
+ return {
71
+ ...saved,
72
+ mode: saved.mode === 'UNKNOWN' || saved.mode === 'NO_SUBSCRIPTION' ? 'API' : saved.mode,
73
+ };
74
+ }
75
+
76
+ export function billingForRange(settings , provider , range ) {
77
+ const count = fullCalendarMonths(range);
78
+ const exact = settings.periodBilling?.[billingPeriodKey(range)]?.[provider];
79
+ const start = new Date(range.from);
80
+ const first =
81
+ range.calendarFrom?.slice(0, 7) ??
82
+ `${start.getFullYear()}-${String(start.getMonth() + 1).padStart(2, '0')}`;
83
+ const months = Array.from(
84
+ { length: count ?? 1 },
85
+ (_, index) => exact ?? billingForMonth(settings, provider, shiftCalendarMonth(first, index) ),
86
+ );
87
+ const mode = months.every((b) => b.mode === months[0].mode)
88
+ ? months[0].mode
89
+ : 'MIXED';
90
+ const subscriptionKnown = months.every((b) => b.mode === 'API' || b.monthly !== null);
91
+ const apiKnown = months.every((b) => b.mode === 'SUBSCRIPTION' || b.apiSpend != null);
92
+ const subscription =
93
+ count !== null && subscriptionKnown
94
+ ? months.reduce((total, b) => total + (b.mode === 'API' ? 0 : b.monthly ), 0)
95
+ : null;
96
+ const apiSpend =
97
+ count !== null && apiKnown
98
+ ? exact
99
+ ? exact.mode === 'SUBSCRIPTION'
100
+ ? 0
101
+ : exact.apiSpend
102
+ : months.reduce((total, b) => total + (b.mode === 'SUBSCRIPTION' ? 0 : b.apiSpend ), 0)
103
+ : null;
104
+ return {
105
+ mode,
106
+ monthly: months.length === 1 ? months[0].monthly : null,
107
+ subscription,
108
+ apiSpend,
109
+ paid: subscription !== null && apiSpend !== null ? subscription + apiSpend : null,
110
+ needsBilling: !subscriptionKnown || !apiKnown,
111
+ };
112
+ }