@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,10 +1,11 @@
1
1
 
2
+
2
3
  import { fullCalendarMonths } from '../../core/src/calendar.js';
3
4
  import { formatPricingCoverage } from '../../core/src/coverage.js';
4
- import { product } from '../../ui/src/brand.js';
5
+ import { product, wordmarkSvg } from '../../ui/src/brand.js';
5
6
  /** Only public, aggregated fields needed by the receipt and its example. */
6
7
 
7
-
8
+
8
9
 
9
10
 
10
11
 
@@ -14,7 +15,8 @@ import { product } from '../../ui/src/brand.js';
14
15
 
15
16
 
16
17
 
17
-
18
+
19
+
18
20
 
19
21
 
20
22
 
@@ -37,23 +39,67 @@ export const money = (n , signed = false) =>
37
39
  ? 'Unavailable'
38
40
  : `${signed && n >= 0 ? '+' : ''}${new Intl.NumberFormat('en-US', { style: 'currency', currency: 'USD' }).format(n)}`;
39
41
  /** Compare only the displayed priced amount; preserve uncertainty and billing eligibility. */
40
- export function comparisonForDisplay(s ) {
42
+
43
+
44
+
45
+
46
+
47
+ export function comparisonForDisplay(s ) {
41
48
  const comparison = s.total.comparison,
42
49
  amount = s.total.apiEquivalent ?? s.total.knownSubtotal;
43
50
  if (fullCalendarMonths(s.range) === null)
44
51
  return {
45
52
  ...comparison,
46
53
  subscription: null,
54
+ apiSpend: null,
55
+ paid: null,
47
56
  value: null,
48
57
  roi: null,
49
58
  breakEven: null,
50
59
  outcome: 'neutral',
60
+ scope: [],
61
+ apiEquivalent: null,
51
62
  };
52
- if (amount === null || comparison.subscription === null) return comparison;
63
+ const paidFor = (p ) =>
64
+ p.comparison?.paid !== undefined
65
+ ? p.comparison.paid
66
+ : p.mode === 'SUBSCRIPTION'
67
+ ? (p.comparison?.subscription ?? null)
68
+ : null;
69
+ const active = s.providers.filter((p) => p.records > 0),
70
+ included = active.filter((p) => paidFor(p) !== null);
71
+ if (included.length > 0 && included.length < active.length) {
72
+ const subscription = included.reduce((n, p) => n + (p.comparison?.subscription ?? 0), 0),
73
+ apiSpend = included.reduce((n, p) => n + (p.comparison?.apiSpend ?? 0), 0),
74
+ paid = included.reduce((n, p) => n + paidFor(p) , 0),
75
+ priced = included.map((p) => p.apiEquivalent ?? p.knownSubtotal),
76
+ scopedAmount = priced.every((cost) => cost !== null)
77
+ ? priced.reduce ((n, cost) => n + cost , 0)
78
+ : null,
79
+ certain = !s.historyPartial && included.every((p) => p.apiEquivalent !== null);
80
+ return {
81
+ subscription,
82
+ apiSpend,
83
+ paid,
84
+ value: scopedAmount === null ? null : scopedAmount - paid,
85
+ roi: scopedAmount === null || paid <= 0 ? null : scopedAmount / paid,
86
+ outcome: certain && included.length === 1 ? included[0].comparison .outcome : 'neutral',
87
+ breakEven: certain && included.length === 1 ? included[0].comparison .breakEven : null,
88
+ reason: 'Providers with declared expenses only',
89
+ scope: included.map((p) => p.provider),
90
+ apiEquivalent: scopedAmount,
91
+ };
92
+ }
93
+ const paid = comparison.paid === undefined ? comparison.subscription : comparison.paid;
94
+ if (amount === null || paid === null)
95
+ return { ...comparison, paid, scope: [], apiEquivalent: amount };
53
96
  return {
54
97
  ...comparison,
55
- value: comparison.value ?? amount - comparison.subscription,
56
- roi: comparison.roi ?? (comparison.subscription > 0 ? amount / comparison.subscription : null),
98
+ paid,
99
+ value: comparison.value ?? amount - paid,
100
+ roi: comparison.roi ?? (paid > 0 ? amount / paid : null),
101
+ scope: [],
102
+ apiEquivalent: amount,
57
103
  };
58
104
  }
59
105
  export function receiptModel(s , generatedAt = new Date()) {
@@ -86,12 +132,33 @@ export function receiptModel(s , generatedAt = new Date())
86
132
  strong: true,
87
133
  });
88
134
  const comp = comparisonForDisplay(s);
89
- if (comp.subscription !== null)
135
+ if (comp.scope.length)
136
+ rows.push(
137
+ { left: '', right: '' },
138
+ {
139
+ left:
140
+ comp.scope
141
+ .map((provider) => (provider === 'codex' ? 'CODEX' : 'CLAUDE CODE'))
142
+ .join(' + ') + ' COMPARISON',
143
+ right: '',
144
+ strong: true,
145
+ },
146
+ {
147
+ left: 'API Equivalent',
148
+ right: comp.apiEquivalent === null ? '—' : money(comp.apiEquivalent),
149
+ },
150
+ );
151
+ if (comp.subscription != null && comp.subscription > 0)
90
152
  rows.push({ left: 'Subscription Cost', right: money(comp.subscription) });
153
+ if (comp.apiSpend != null && (comp.apiSpend > 0 || comp.subscription === 0)) {
154
+ rows.push({ left: 'API Paid', right: money(comp.apiSpend) });
155
+ if (comp.subscription != null && comp.subscription > 0)
156
+ rows.push({ left: 'Total Paid', right: money(comp.paid ?? null), strong: true });
157
+ }
91
158
  const value = comp.value;
92
159
  if (value !== null)
93
160
  rows.push({
94
- left: 'You Saved',
161
+ left: 'Total Saved',
95
162
  right: money(value, true),
96
163
  accent: value >= 0 ? 'positive' : 'negative',
97
164
  strong: true,
@@ -179,8 +246,53 @@ export async function receiptFonts() {
179
246
  await Promise.all([
180
247
  document.fonts.load('14px "Plex Mono"'),
181
248
  document.fonts.load('600 14px "Plex Mono"'),
249
+ receiptBrandAssets(document),
182
250
  ]);
183
251
  }
252
+ // Decode the same bundled product marks used by the dashboard before drawing.
253
+ // Canvas previews and exported PNGs therefore include identical local artwork.
254
+ const brandAssets = new WeakMap ();
255
+ const decodedBrands = new WeakMap ();
256
+ const decodedWordmarks = new WeakMap ();
257
+ function receiptBrandAssets(document ) {
258
+ let assets = brandAssets.get(document);
259
+ if (!assets) {
260
+ assets = Promise.all(
261
+ ['codex-outline.svg', 'claude-code-clawd.svg'].map(async (file) => {
262
+ const image = document.createElement('img');
263
+ image.src = `/brands/${file}`;
264
+ await image.decode();
265
+ return image;
266
+ }),
267
+ ).then(async ([codex, claude]) => {
268
+ const svg = wordmarkSvg();
269
+ if (svg) {
270
+ const dimensions = /viewBox="0 0 (\d+) (\d+)"/.exec(svg) ;
271
+ const images = await Promise.all(
272
+ (['dark', 'light'] ).map(async (theme) => {
273
+ const image = document.createElement('img');
274
+ const artwork = svg
275
+ .replace('<svg ', `<svg width="${dimensions[1]}" height="${dimensions[2]}" `)
276
+ .replace('fill="currentColor"', `fill="${theme === 'dark' ? '#eef2f4' : '#22272d'}"`)
277
+ .replaceAll(
278
+ 'class="brand-name-accent"',
279
+ `fill="${theme === 'dark' ? '#37ec85' : '#137744'}"`,
280
+ );
281
+ image.src = 'data:image/svg+xml,' + encodeURIComponent(artwork);
282
+ await image.decode();
283
+ return image;
284
+ }),
285
+ );
286
+ decodedWordmarks.set(document, { dark: images[0], light: images[1] });
287
+ }
288
+ const marks = { codex, claude };
289
+ decodedBrands.set(document, marks);
290
+ return marks;
291
+ });
292
+ brandAssets.set(document, assets);
293
+ }
294
+ return assets;
295
+ }
184
296
  const paperTextures = new WeakMap ();
185
297
  function paperTexture(canvas ) {
186
298
  const document = canvas.ownerDocument;
@@ -202,6 +314,8 @@ function paperTexture(canvas ) {
202
314
  paperTextures.set(document, texture);
203
315
  return texture;
204
316
  }
317
+ const isTokenRow = (row ) =>
318
+ ['Input Tokens', 'Output Tokens', 'Cache Reads', 'Tokens', 'Known Tokens'].includes(row.left);
205
319
  const rowHeight = (row ) =>
206
320
  !row.left && !row.right
207
321
  ? 34
@@ -211,7 +325,9 @@ const rowHeight = (row ) =>
211
325
  ? 46
212
326
  : row.accent
213
327
  ? 32
214
- : 28;
328
+ : isTokenRow(row)
329
+ ? 24
330
+ : 28;
215
331
  export function drawReceipt(
216
332
  canvas ,
217
333
  model ,
@@ -313,7 +429,13 @@ export function drawReceipt(
313
429
  c.stroke();
314
430
  c.setLineDash([]);
315
431
  };
316
- text(product.name, width / 2, 64, 29, '#f0f0ef', 'center');
432
+ const wordmark = decodedWordmarks.get(canvas.ownerDocument)?.[theme];
433
+ if (wordmark) {
434
+ // Compact, centered lettering only; no symbol or terminal cursor on the paper.
435
+ const logoWidth = 156,
436
+ logoHeight = logoWidth * (wordmark.naturalHeight / wordmark.naturalWidth);
437
+ c.drawImage(wordmark, (width - logoWidth) / 2, 64 - logoHeight, logoWidth, logoHeight);
438
+ } else text(product.name, width / 2, 64, 29, '#f0f0ef', 'center');
317
439
  text('USAGE RECEIPT', width / 2, 92, 13, '#b6b8bd', 'center');
318
440
  if (model.demo) text('EXAMPLE DATA', width / 2, 112, 10, '#a3a5a9', 'center');
319
441
  text('Period', left, 137, 12.5, '#b4b5b9');
@@ -331,7 +453,8 @@ export function drawReceipt(
331
453
  }
332
454
  const provider = !row.right,
333
455
  multiple = row.left === 'VALUE MULTIPLE';
334
- const label = provider ? (row.left === 'CODEX' ? 'Codex' : 'Claude Code') : row.left;
456
+ const label =
457
+ row.left === 'CODEX' ? 'Codex' : row.left === 'CLAUDE CODE' ? 'Claude Code' : row.left;
335
458
  const color =
336
459
  row.accent === 'positive'
337
460
  ? '#37ec85'
@@ -340,18 +463,37 @@ export function drawReceipt(
340
463
  : row.strong
341
464
  ? '#e1e2e4'
342
465
  : '#bdc0c6';
343
- const valueSize = multiple ? 25 : row.accent ? 22 : row.strong ? 18 : 16;
466
+ const tokenRow = isTokenRow(row);
467
+ const valueSize = multiple ? 25 : row.accent ? 22 : row.strong ? 18 : tokenRow ? 13 : 16;
344
468
  c.font = `${printSize(valueSize)}px ${mono}`;
345
469
  const valueWidth = c.measureText(row.right).width;
470
+ const brand = row.left === 'CODEX' ? 'codex' : row.left === 'CLAUDE CODE' ? 'claude' : null;
471
+ const mark = brand && decodedBrands.get(canvas.ownerDocument)?.[brand];
472
+ let labelLeft = left;
473
+ if (mark) {
474
+ const size = 22;
475
+ const markHeight = size * (mark.naturalHeight / mark.naturalWidth);
476
+ if (brand === 'codex') {
477
+ const tinted = canvas.ownerDocument.createElement('canvas');
478
+ tinted.width = tinted.height = size * scale;
479
+ const context = tinted.getContext('2d') ;
480
+ context.drawImage(mark, 0, 0, tinted.width, tinted.height);
481
+ context.globalCompositeOperation = 'source-in';
482
+ context.fillStyle = ink('#e1e2e4');
483
+ context.fillRect(0, 0, tinted.width, tinted.height);
484
+ c.drawImage(tinted, left, y - 17, size, size);
485
+ } else c.drawImage(mark, left, y - 14, size, markHeight);
486
+ labelLeft += size + 8;
487
+ }
346
488
  text(
347
489
  label,
348
- left,
490
+ labelLeft,
349
491
  y,
350
- multiple ? 17 : provider ? 17 : row.strong ? 14 : 14,
492
+ multiple ? 17 : provider ? 17 : tokenRow ? 12 : 14,
351
493
  multiple ? '#e1e2e4' : color,
352
494
  'left',
353
495
  provider,
354
- provider ? width - 48 : width - 62 - valueWidth,
496
+ provider ? right - labelLeft : width - 62 - valueWidth,
355
497
  );
356
498
  text(row.right, right, y, valueSize, row.accent ? color : '#e3e4e7', 'right', row.strong, 160);
357
499
  y += step;
@@ -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,3 +2319,182 @@ 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
+ }
@@ -29,6 +29,7 @@ const example = {
29
29
  records: 1,
30
30
  tokens: 20530000,
31
31
  tokensPartial: false,
32
+ tokenBreakdown: { input: 19230000, output: 1300000, cacheRead: 15000000 },
32
33
  apiEquivalent: 201.36,
33
34
  knownSubtotal: 201.36,
34
35
  },
@@ -38,6 +39,7 @@ const example = {
38
39
  records: 1,
39
40
  tokens: 7840000,
40
41
  tokensPartial: false,
42
+ tokenBreakdown: { input: 440000, output: 400000, cacheRead: 7000000 },
41
43
  apiEquivalent: 86.06,
42
44
  knownSubtotal: 86.06,
43
45
  },
@@ -59,6 +61,7 @@ const example = {
59
61
  confidence: 'HIGH',
60
62
  demo: true,
61
63
  };
64
+ const exampleReceipt = receiptModel(example, new Date(2026, 10, 1, 9, 30));
62
65
  document.querySelector('#landing') .innerHTML = /* HTML */ `<header class="landing-nav">
63
66
  <a class="brand" href="#"><span class="brand-mark">${productMark}</span>${productNameMarkup}</a>
64
67
  <nav class="landing-links" aria-label="Site navigation">
@@ -150,7 +153,10 @@ document.querySelector('#landing') .innerHTML = /* HTML */ `<header class="landi
150
153
  <canvas
151
154
  id="example-receipt"
152
155
  role="img"
153
- aria-label="Illustrative ${product.name} receipt: API equivalent 287 dollars 42 cents, subscription 220 dollars, value 67 dollars 42 cents. Codex and Claude Code example data."
156
+ aria-label="Illustrative ${product.name} receipt. Codex and Claude Code example data. ${exampleReceipt.rows
157
+ .filter((row) => row.left)
158
+ .map((row) => row.left + ' ' + row.right)
159
+ .join('. ')}"
154
160
  ></canvas
155
161
  ><span>Export PNG ↓</span>
156
162
  </div>
@@ -188,7 +194,7 @@ document.querySelector('#landing') .innerHTML = /* HTML */ `<header class="landi
188
194
  await receiptFonts();
189
195
  drawReceipt(
190
196
  document.querySelector ('#example-receipt') ,
191
- receiptModel(example, new Date(2026, 10, 1, 9, 30)),
197
+ exampleReceipt,
192
198
  'dark',
193
199
  'preview',
194
200
  );