@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.
@@ -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,12 +325,19 @@ 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 ,
218
334
  theme = 'dark',
335
+ presentation = 'standard',
219
336
  ) {
337
+ // A public example needs larger print when viewed inside the dashboard preview.
338
+ // The paper, layout and export renderer remain shared with the local application.
339
+ const printSize = (size ) =>
340
+ presentation === 'preview' ? Math.max(13, size * 1.12) : size;
220
341
  const light = theme === 'light';
221
342
  const ink = (color ) =>
222
343
  light
@@ -288,6 +409,7 @@ export function drawReceipt(
288
409
  bold = false,
289
410
  maxWidth = width - 48,
290
411
  ) => {
412
+ size = printSize(size);
291
413
  c.font = `${bold ? '600 ' : ''}${size}px ${mono}`;
292
414
  while (c.measureText(value).width > maxWidth && size > 8) {
293
415
  size -= 0.5;
@@ -307,7 +429,13 @@ export function drawReceipt(
307
429
  c.stroke();
308
430
  c.setLineDash([]);
309
431
  };
310
- 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');
311
439
  text('USAGE RECEIPT', width / 2, 92, 13, '#b6b8bd', 'center');
312
440
  if (model.demo) text('EXAMPLE DATA', width / 2, 112, 10, '#a3a5a9', 'center');
313
441
  text('Period', left, 137, 12.5, '#b4b5b9');
@@ -325,7 +453,8 @@ export function drawReceipt(
325
453
  }
326
454
  const provider = !row.right,
327
455
  multiple = row.left === 'VALUE MULTIPLE';
328
- 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;
329
458
  const color =
330
459
  row.accent === 'positive'
331
460
  ? '#37ec85'
@@ -334,18 +463,37 @@ export function drawReceipt(
334
463
  : row.strong
335
464
  ? '#e1e2e4'
336
465
  : '#bdc0c6';
337
- const valueSize = multiple ? 25 : row.accent ? 22 : row.strong ? 18 : 16;
338
- c.font = `${valueSize}px ${mono}`;
466
+ const tokenRow = isTokenRow(row);
467
+ const valueSize = multiple ? 25 : row.accent ? 22 : row.strong ? 18 : tokenRow ? 13 : 16;
468
+ c.font = `${printSize(valueSize)}px ${mono}`;
339
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
+ }
340
488
  text(
341
489
  label,
342
- left,
490
+ labelLeft,
343
491
  y,
344
- multiple ? 17 : provider ? 17 : row.strong ? 14 : 14,
492
+ multiple ? 17 : provider ? 17 : tokenRow ? 12 : 14,
345
493
  multiple ? '#e1e2e4' : color,
346
494
  'left',
347
495
  provider,
348
- provider ? width - 48 : width - 62 - valueWidth,
496
+ provider ? right - labelLeft : width - 62 - valueWidth,
349
497
  );
350
498
  text(row.right, right, y, valueSize, row.accent ? color : '#e3e4e7', 'right', row.strong, 160);
351
499
  y += step;
@@ -2,6 +2,7 @@
2
2
  export const product = {
3
3
  name: 'SubValue',
4
4
  site: 'subvalue.dev',
5
+ repository: 'https://github.com/axelmtn/subvalue',
5
6
  exportBasename: 'subvalue-receipt',
6
7
  };
7
8
  // Typography remains separate from the neutral symbol and follows the display name.
@@ -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,3 +2319,182 @@ 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
+ }
@@ -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,9 +61,15 @@ 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
- <a class="brand" href="#"><span class="brand-mark">${productMark}</span>${productNameMarkup}</a
64
- ><a class="github-link" href="#release">Install <span>↓</span></a>
66
+ <a class="brand" href="#"><span class="brand-mark">${productMark}</span>${productNameMarkup}</a>
67
+ <nav class="landing-links" aria-label="Site navigation">
68
+ <a class="install-link" href="#release">Install <span>↓</span></a>
69
+ <a class="github-link" href="${product.repository}" target="_blank" rel="noreferrer"
70
+ >GitHub <span>↗</span></a
71
+ >
72
+ </nav>
65
73
  </header>
66
74
  <main>
67
75
  <section class="hero">
@@ -81,7 +89,9 @@ document.querySelector('#landing') .innerHTML = /* HTML */ `<header class="landi
81
89
  </button>
82
90
  </div>
83
91
  <div class="trust-row">
84
- <span>✓ Local</span><span>✓ Read-only</span><span>✓ No telemetry</span>
92
+ <span>✓ Local</span><span>✓ Read-only</span>
93
+ <a href="${product.repository}" target="_blank" rel="noreferrer">✓ Open source</a>
94
+ <span>✓ No telemetry</span>
85
95
  </div>
86
96
  <span class="release-note">Node.js 24.13+ · Windows · macOS · Linux</span>
87
97
  </section>
@@ -143,7 +153,10 @@ document.querySelector('#landing') .innerHTML = /* HTML */ `<header class="landi
143
153
  <canvas
144
154
  id="example-receipt"
145
155
  role="img"
146
- 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('. ')}"
147
160
  ></canvas
148
161
  ><span>Export PNG ↓</span>
149
162
  </div>
@@ -181,7 +194,9 @@ document.querySelector('#landing') .innerHTML = /* HTML */ `<header class="landi
181
194
  await receiptFonts();
182
195
  drawReceipt(
183
196
  document.querySelector ('#example-receipt') ,
184
- receiptModel(example, new Date(2026, 10, 1, 9, 30)),
197
+ exampleReceipt,
198
+ 'dark',
199
+ 'preview',
185
200
  );
186
201
  document.querySelectorAll ('.copy-command').forEach((b) =>
187
202
  b.addEventListener('click', async () => {