@subvalue/cli 0.1.4 → 0.1.5

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/PRE_RELEASE.md CHANGED
@@ -20,7 +20,7 @@ The first command checks detection without reading session contents. The second
20
20
  ## Check the app
21
21
 
22
22
  1. Confirm the detected providers. A detected provider may have no usage in the selected period.
23
- 2. Enter your billing mode and subscription amount when needed. They are declarations, never inferred from credentials.
23
+ 2. Choose your subscription plan or **No subscription** for each provider. A custom amount is also available. These are declarations, never inferred from credentials.
24
24
  3. Check Overview, 7D/30D/Month, Receipt and Settings. Pricing coverage describes imported usage, not every request ever made.
25
25
  4. Export PNG. Check readable amounts and the absence of project names, paths, prompts and session IDs.
26
26
  5. Use **Refresh usage** at the top of Overview after new activity. It updates local metadata without restarting the app. Period controls only filter cached data.
package/README.md CHANGED
@@ -42,7 +42,7 @@ npm run demo
42
42
  - **Receipt:** local canvas preview and PNG export of the exact same pixels. No projects, paths, requests or sessions in exports.
43
43
  - **Settings:** Sources, Billing, USD, Privacy / data. Inactive providers stay here, with optional manual inclusion.
44
44
 
45
- First run shows detected history, then asks how each provider was used before rendering results. Subscription users can select a plan or enter a custom USD amount. API, mixed usage and an explicit “Set up later” choice never imply a subscription.
45
+ First run shows detected history, then offers subscription plans directly for each provider with imported usage. Choose a plan, a custom USD amount, or **No subscription** independently for Codex and Claude Code. No subscription requires no amount and never implies API billing. **Set up later** leaves billing unknown.
46
46
 
47
47
  Month opens a floating calendar-month picker (including historical months). Previous/next arrows move by one calendar month, seven or thirty days for rolling presets, or the selected custom duration. Comparisons use the full declared monthly price for each complete calendar month selected. The current month is marked In progress. 7D, 30D and partial custom ranges show API-equivalent usage without subscription comparisons. Custom ranges covering whole calendar months compare the corresponding number of monthly charges. Amounts and API prices are USD; no invented exchange rates. Billing declarations do not overwrite historical records or prove their original billing mode. Settings offers bundled, verified ChatGPT/Claude plan presets and a custom amount. Annual presets retain the exact annual price divided by 12, with per-seat plans labeled.
48
48
 
@@ -68,7 +68,7 @@ Codex excludes inherited ordinal prefixes, uses cumulative deltas, suppresses re
68
68
 
69
69
  The versioned catalog supports mappings with confidence and primary-source provenance, half-open effective intervals, cache reads/writes, Claude cache durations, and long-context bands. Rates and release evidence were checked against [OpenAI pricing](https://developers.openai.com/api/docs/pricing) and [Claude pricing](https://platform.claude.com/docs/en/about-claude/pricing) on **2026-10-05**. Historical versions include the July 30 Luna/Terra reductions and August 21 Sol promotion. Exact public GPT-5.6/6 and Claude identifiers carry mapping provenance; long-context rates use the verified 272k request threshold. New-model launch days use their documented initial API-equivalent rate. Calendar-dated changes between rates remain unknown because their exact UTC switch time is not documented. Sol pricing after the confirmed promotion remains unavailable. See [the pricing evidence ledger](dist/PRICING.md). Internal/unknown model identifiers are never mapped by resemblance.
70
70
 
71
- Overview and receipt display the sum of priceable events; the compact priced percentage identifies partial pricing, and unpriced model identities remain visible in Details. No guessed price or zero is assigned to unknown events. Both views share a display comparison of the known priced amount with the declared full-month subscription: Observed Value / You Saved is their difference, and Value Multiple is their ratio. The normalized summary still marks the full-usage cost and comparison unknown when pricing is incomplete. Partial coverage remains visible; these observed values do not infer a certain outcome or an exact break-even date. API and mixed billing have no subscription comparison. Totals from several subscriptions cannot include API/mixed providers in a subscription comparison.
71
+ Overview and receipt display the sum of priceable events; the compact priced percentage identifies partial pricing, and unpriced model identities remain visible in Details. No guessed price or zero is assigned to unknown events. Both views share a display comparison of the known priced amount with the declared full-month subscription: Observed Value / You Saved is their difference, and Value Multiple is their ratio. The normalized summary still marks the full-usage cost and comparison unknown when pricing is incomplete. Partial coverage remains visible; these observed values do not infer a certain outcome or an exact break-even date. No subscription, unknown billing, and legacy API/mixed settings have no subscription comparison. Combined subscription comparisons require a declared subscription for every active provider.
72
72
 
73
73
  ## Architecture
74
74
 
@@ -25,7 +25,7 @@ const mime = {
25
25
  '.woff2': 'font/woff2',
26
26
  '.txt': 'text/plain; charset=utf-8',
27
27
  };
28
- const billingModes = ['UNKNOWN', 'SUBSCRIPTION', 'API', 'MIXED'] ;
28
+ const billingModes = ['UNKNOWN', 'SUBSCRIPTION', 'NO_SUBSCRIPTION', 'API', 'MIXED'] ;
29
29
  const isBillingMode = (value ) =>
30
30
  billingModes.some((mode) => mode === value);
31
31
 
@@ -205,19 +205,21 @@ export function summarize(
205
205
  ? subscriptionForRange(billing.monthly, range)
206
206
  : null;
207
207
  let comparison = neutral(
208
- billing.mode === 'API'
209
- ? 'API usage'
210
- : billing.mode === 'MIXED'
211
- ? 'Mixed billing'
212
- : billing.mode === 'UNKNOWN'
213
- ? 'Billing not set'
214
- : fullCalendarMonths(range) === null
215
- ? 'Choose a full month'
216
- : subscription === null
217
- ? 'Subscription price not set'
218
- : !completePricing
219
- ? 'Pricing unavailable'
220
- : 'Partial history',
208
+ billing.mode === 'NO_SUBSCRIPTION'
209
+ ? 'No subscription'
210
+ : billing.mode === 'API'
211
+ ? 'API usage'
212
+ : billing.mode === 'MIXED'
213
+ ? 'Mixed billing'
214
+ : billing.mode === 'UNKNOWN'
215
+ ? 'Billing not set'
216
+ : fullCalendarMonths(range) === null
217
+ ? 'Choose a full month'
218
+ : subscription === null
219
+ ? 'Subscription price not set'
220
+ : !completePricing
221
+ ? 'Pricing unavailable'
222
+ : 'Partial history',
221
223
  subscription,
222
224
  );
223
225
  if (completePricing && subscription !== null) {
@@ -311,7 +313,7 @@ export function summarize(
311
313
  const apiEquivalent = count > 0 && pricedRecords === count ? knownSubtotal : null;
312
314
  const subs = active.filter((p) => p.mode === 'SUBSCRIPTION');
313
315
  let comparison = neutral(
314
- active.some((p) => p.mode === 'API' || p.mode === 'MIXED' || p.mode === 'UNKNOWN')
316
+ active.some((p) => p.mode !== 'SUBSCRIPTION')
315
317
  ? 'Subscription comparison unavailable'
316
318
  : 'Partial history',
317
319
  );
@@ -1,6 +1,7 @@
1
1
 
2
2
 
3
-
3
+ // API and MIXED remain readable for existing local settings.
4
+
4
5
 
5
6
 
6
7
 
@@ -216,7 +216,12 @@ export function drawReceipt(
216
216
  canvas ,
217
217
  model ,
218
218
  theme = 'dark',
219
+ presentation = 'standard',
219
220
  ) {
221
+ // A public example needs larger print when viewed inside the dashboard preview.
222
+ // The paper, layout and export renderer remain shared with the local application.
223
+ const printSize = (size ) =>
224
+ presentation === 'preview' ? Math.max(13, size * 1.12) : size;
220
225
  const light = theme === 'light';
221
226
  const ink = (color ) =>
222
227
  light
@@ -288,6 +293,7 @@ export function drawReceipt(
288
293
  bold = false,
289
294
  maxWidth = width - 48,
290
295
  ) => {
296
+ size = printSize(size);
291
297
  c.font = `${bold ? '600 ' : ''}${size}px ${mono}`;
292
298
  while (c.measureText(value).width > maxWidth && size > 8) {
293
299
  size -= 0.5;
@@ -335,7 +341,7 @@ export function drawReceipt(
335
341
  ? '#e1e2e4'
336
342
  : '#bdc0c6';
337
343
  const valueSize = multiple ? 25 : row.accent ? 22 : row.strong ? 18 : 16;
338
- c.font = `${valueSize}px ${mono}`;
344
+ c.font = `${printSize(valueSize)}px ${mono}`;
339
345
  const valueWidth = c.measureText(row.right).width;
340
346
  text(
341
347
  label,
@@ -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,29 @@ 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
+ @media (max-width: 760px) {
312
+ .overview-title {
313
+ flex-direction: column;
314
+ align-items: start;
315
+ gap: 5px;
316
+ }
317
+ .selected-period {
318
+ font-size: 15px;
319
+ }
320
+ }
298
321
  .eyebrow {
299
322
  font: 9px var(--mono);
300
323
  letter-spacing: 1.7px;
@@ -2223,7 +2246,7 @@ dialog::backdrop {
2223
2246
  }
2224
2247
  }
2225
2248
  .billing-row {
2226
- grid-template-columns: 100px 145px minmax(0, 1fr);
2249
+ grid-template-columns: 100px minmax(0, 1fr);
2227
2250
  gap: 16px;
2228
2251
  align-items: start;
2229
2252
  }
@@ -2242,7 +2265,7 @@ dialog::backdrop {
2242
2265
  .amount-field input[readonly] {
2243
2266
  color: var(--muted);
2244
2267
  }
2245
- .subscription-fields.hidden {
2268
+ .billing-amount.hidden {
2246
2269
  display: none;
2247
2270
  }
2248
2271
  .compact-details a {
@@ -60,8 +60,13 @@ const example = {
60
60
  demo: true,
61
61
  };
62
62
  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>
63
+ <a class="brand" href="#"><span class="brand-mark">${productMark}</span>${productNameMarkup}</a>
64
+ <nav class="landing-links" aria-label="Site navigation">
65
+ <a class="install-link" href="#release">Install <span>↓</span></a>
66
+ <a class="github-link" href="${product.repository}" target="_blank" rel="noreferrer"
67
+ >GitHub <span>↗</span></a
68
+ >
69
+ </nav>
65
70
  </header>
66
71
  <main>
67
72
  <section class="hero">
@@ -81,7 +86,9 @@ document.querySelector('#landing') .innerHTML = /* HTML */ `<header class="landi
81
86
  </button>
82
87
  </div>
83
88
  <div class="trust-row">
84
- <span>✓ Local</span><span>✓ Read-only</span><span>✓ No telemetry</span>
89
+ <span>✓ Local</span><span>✓ Read-only</span>
90
+ <a href="${product.repository}" target="_blank" rel="noreferrer">✓ Open source</a>
91
+ <span>✓ No telemetry</span>
85
92
  </div>
86
93
  <span class="release-note">Node.js 24.13+ · Windows · macOS · Linux</span>
87
94
  </section>
@@ -182,6 +189,8 @@ await receiptFonts();
182
189
  drawReceipt(
183
190
  document.querySelector ('#example-receipt') ,
184
191
  receiptModel(example, new Date(2026, 10, 1, 9, 30)),
192
+ 'dark',
193
+ 'preview',
185
194
  );
186
195
  document.querySelectorAll ('.copy-command').forEach((b) =>
187
196
  b.addEventListener('click', async () => {
@@ -572,7 +572,10 @@ function overview() {
572
572
  const months = fullCalendarMonths(summary.range),
573
573
  showSubs = months !== null && active.length && active.every((p) => p.mode === 'SUBSCRIPTION');
574
574
  return /* HTML */ `<div class="page-heading">
575
- <div><h1>Overview</h1></div>
575
+ <div class="overview-title">
576
+ <h1>Overview</h1>
577
+ <span class="selected-period" aria-live="polite">${esc(periodLabel())}</span>
578
+ </div>
576
579
  ${refreshButton()}
577
580
  </div>
578
581
  ${coverage()}
@@ -614,38 +617,40 @@ function receiptPage() {
614
617
  }
615
618
  function billingFields(p , firstRun = false) {
616
619
  const b = settings.billing[p],
617
- plan = subscriptionPlan(p, b.planId);
620
+ plan = subscriptionPlan(p, b.planId),
621
+ subscribed = b.mode === 'SUBSCRIPTION',
622
+ selection = subscribed
623
+ ? (plan?.id ?? 'custom')
624
+ : b.mode === 'NO_SUBSCRIPTION' || b.mode === 'API'
625
+ ? 'none'
626
+ : '';
618
627
  return /* HTML */ `<div class="billing-row">
619
- <label class="provider-setting-name" for="mode-${p}">${name(p)}</label
620
- ><select
621
- id="mode-${p}"
622
- name="mode-${p}"
623
- data-billing-mode="${p}"
624
- ${firstRun ? 'required data-first-run="true"' : ''}
625
- >
626
- <option value="${firstRun ? '' : 'UNKNOWN'}" ${b.mode === 'UNKNOWN' ? 'selected' : ''}>
627
- ${firstRun ? 'Choose usage' : 'Not set'}
628
- </option>
629
- <option value="SUBSCRIPTION" ${b.mode === 'SUBSCRIPTION' ? 'selected' : ''}>
630
- Subscription
631
- </option>
632
- <option value="API" ${b.mode === 'API' ? 'selected' : ''}>API</option>
633
- <option value="MIXED" ${b.mode === 'MIXED' ? 'selected' : ''}>Mixed / not sure</option>
634
- </select>
635
- <div class="subscription-fields ${b.mode === 'SUBSCRIPTION' ? '' : 'hidden'}" id="amount-${p}">
628
+ <label class="provider-setting-name" for="plan-${p}">${name(p)}</label>
629
+ <div class="subscription-fields">
636
630
  <label class="sr-only" for="plan-${p}">${name(p)} subscription plan</label
637
- ><select id="plan-${p}" name="plan-${p}" data-subscription-plan="${p}">
638
- <option value="custom" ${!plan ? 'selected' : ''}>Custom amount</option>
631
+ ><select
632
+ id="plan-${p}"
633
+ name="plan-${p}"
634
+ data-subscription-plan="${p}"
635
+ ${firstRun ? 'required data-first-run="true"' : ''}
636
+ >
637
+ <option value="" ${selection === '' ? 'selected' : ''}>
638
+ ${firstRun ? 'Choose subscription' : 'Not set'}
639
+ </option>
640
+ <option value="none" ${selection === 'none' ? 'selected' : ''}>No subscription</option>
639
641
  ${subscriptionPlans
640
642
  .filter((plan) => plan.provider === p)
641
643
  .map(
642
644
  (option) =>
643
- /* HTML */ `<option value="${option.id}" ${plan?.id === option.id ? 'selected' : ''}>
645
+ /* HTML */ `<option value="${option.id}" ${selection === option.id ? 'selected' : ''}>
644
646
  ${esc(subscriptionPlanLabel(option))}
645
647
  </option>`,
646
648
  )
647
- .join('')}</select
648
- ><label class="amount-field"
649
+ .join('')}
650
+ <option value="custom" ${selection === 'custom' ? 'selected' : ''}>
651
+ Custom amount
652
+ </option></select
653
+ ><label class="amount-field billing-amount ${subscribed ? '' : 'hidden'}" id="amount-${p}"
649
654
  ><span class="sr-only">${name(p)} monthly subscription in USD</span><span>$</span
650
655
  ><input
651
656
  name="monthly-${p}"
@@ -655,7 +660,8 @@ function billingFields(p , firstRun = false) {
655
660
  step="0.01"
656
661
  value="${plan ? plan.monthly.toFixed(2) : (b.monthly ?? '')}"
657
662
  ${plan ? 'readonly' : ''}
658
- ${firstRun && b.mode === 'SUBSCRIPTION' ? 'required' : ''}
663
+ ${!subscribed ? 'disabled' : ''}
664
+ ${firstRun && subscribed ? 'required' : ''}
659
665
  placeholder="Amount"
660
666
  /><span>/ month</span></label
661
667
  >
@@ -754,8 +760,8 @@ function settingsPage() {
754
760
  </p>
755
761
  <p>
756
762
  These are your declarations, not inferred billing history. Amounts are in USD.
757
- Comparisons use the full monthly price for full calendar months only. API and mixed
758
- usage have no subscription comparison.
763
+ Comparisons use the full monthly price for full calendar months only. No subscription
764
+ means no subscription comparison.
759
765
  </p>
760
766
  </details>
761
767
  </section>
@@ -792,7 +798,7 @@ function onboarding() {
792
798
  <span class="brand-mark">${productMark}</span
793
799
  ><span class="eyebrow">${esc(product.name.toUpperCase())} / FIRST RUN</span>
794
800
  <h2 id="onboarding-title">
795
- ${onboardingStage === 'found' ? (found.length ? 'Local usage found' : 'No local usage found') : 'How did you use it?'}
801
+ ${onboardingStage === 'found' ? (found.length ? 'Local usage found' : 'No local usage found') : 'Your subscriptions'}
796
802
  </h2>
797
803
  ${
798
804
  onboardingStage === 'found'
@@ -804,7 +810,7 @@ function onboarding() {
804
810
  ${billable
805
811
  .map(
806
812
  (p) =>
807
- /* HTML */ `<h3>How did you use ${name(p.provider)}?</h3>
813
+ /* HTML */ `<h3>${name(p.provider)}</h3>
808
814
  ${billingFields(p.provider, true)}`,
809
815
  )
810
816
  .join('')}<button class="button" type="submit">
@@ -842,12 +848,14 @@ function render() {
842
848
  function readBilling(form ) {
843
849
  const data = new FormData(form);
844
850
  for (const p of ['codex', 'claude'] ) {
845
- const mode = data.get(`mode-${p}`);
846
- if (mode) {
851
+ const selection = data.get(`plan-${p}`);
852
+ if (selection !== null) {
853
+ const mode =
854
+ selection === 'none' ? 'NO_SUBSCRIPTION' : selection === '' ? 'UNKNOWN' : 'SUBSCRIPTION';
847
855
  const amount = data.get(`monthly-${p}`),
848
- plan = subscriptionPlan(p, String(data.get(`plan-${p}`)));
856
+ plan = subscriptionPlan(p, String(selection));
849
857
  settings.billing[p] = {
850
- mode: mode ,
858
+ mode,
851
859
  monthly:
852
860
  mode === 'SUBSCRIPTION'
853
861
  ? (plan?.monthly ?? (amount !== '' && amount !== null ? Number(amount) : null))
@@ -1011,23 +1019,15 @@ function bind() {
1011
1019
  const data = new FormData(e.currentTarget );
1012
1020
  void choosePeriod('month', from, to, String(data.get('month')));
1013
1021
  });
1014
- document.querySelectorAll ('[data-billing-mode]').forEach((select) =>
1015
- select.addEventListener('change', () => {
1016
- document
1017
- .querySelector(`#amount-${select.dataset.billingMode}`)
1018
- ?.classList.toggle('hidden', select.value !== 'SUBSCRIPTION');
1019
- const amount = document.querySelector (
1020
- `[name="monthly-${select.dataset.billingMode}"]`,
1021
- );
1022
- if (amount)
1023
- amount.required = select.dataset.firstRun === 'true' && select.value === 'SUBSCRIPTION';
1024
- }),
1025
- );
1026
1022
  document.querySelectorAll ('[data-subscription-plan]').forEach((select) =>
1027
1023
  select.addEventListener('change', () => {
1028
1024
  const p = select.dataset.subscriptionPlan ,
1029
1025
  plan = subscriptionPlan(p, select.value),
1030
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
1031
  input.readOnly = !!plan;
1032
1032
  if (plan) input.value = plan.monthly.toFixed(2);
1033
1033
  }),
@@ -295,6 +295,29 @@ 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
+ @media (max-width: 760px) {
312
+ .overview-title {
313
+ flex-direction: column;
314
+ align-items: start;
315
+ gap: 5px;
316
+ }
317
+ .selected-period {
318
+ font-size: 15px;
319
+ }
320
+ }
298
321
  .eyebrow {
299
322
  font: 9px var(--mono);
300
323
  letter-spacing: 1.7px;
@@ -2223,7 +2246,7 @@ dialog::backdrop {
2223
2246
  }
2224
2247
  }
2225
2248
  .billing-row {
2226
- grid-template-columns: 100px 145px minmax(0, 1fr);
2249
+ grid-template-columns: 100px minmax(0, 1fr);
2227
2250
  gap: 16px;
2228
2251
  align-items: start;
2229
2252
  }
@@ -2242,7 +2265,7 @@ dialog::backdrop {
2242
2265
  .amount-field input[readonly] {
2243
2266
  color: var(--muted);
2244
2267
  }
2245
- .subscription-fields.hidden {
2268
+ .billing-amount.hidden {
2246
2269
  display: none;
2247
2270
  }
2248
2271
  .compact-details a {
@@ -2314,13 +2337,27 @@ dialog {
2314
2337
  justify-content: space-between;
2315
2338
  align-items: center;
2316
2339
  }
2317
- .github-link {
2318
- font-size: 12px;
2340
+ .landing-links {
2341
+ display: flex;
2342
+ align-items: center;
2343
+ gap: 22px;
2344
+ }
2345
+ .github-link,
2346
+ .install-link {
2347
+ font-size: 13px;
2319
2348
  color: var(--muted);
2320
2349
  display: flex;
2350
+ align-items: center;
2351
+ min-height: 44px;
2321
2352
  gap: 8px;
2322
2353
  }
2323
- .github-link span {
2354
+ .github-link span,
2355
+ .install-link span {
2356
+ color: var(--green);
2357
+ }
2358
+ .github-link:hover,
2359
+ .install-link:hover,
2360
+ .trust-row a:hover {
2324
2361
  color: var(--green);
2325
2362
  }
2326
2363
  .hero {
@@ -2393,13 +2430,16 @@ dialog {
2393
2430
  }
2394
2431
  .trust-row {
2395
2432
  display: flex;
2433
+ flex-wrap: wrap;
2396
2434
  justify-content: center;
2397
2435
  gap: 25px;
2436
+ row-gap: 12px;
2398
2437
  margin-top: 22px;
2399
2438
  color: #a3b0a6;
2400
2439
  font-size: 10px;
2401
2440
  }
2402
- .trust-row span {
2441
+ .trust-row span,
2442
+ .trust-row a {
2403
2443
  white-space: nowrap;
2404
2444
  }
2405
2445
  .release-note {
@@ -2415,7 +2455,7 @@ dialog {
2415
2455
  .preview-label {
2416
2456
  display: flex;
2417
2457
  justify-content: space-between;
2418
- font: 8px var(--mono);
2458
+ font: 11px var(--mono);
2419
2459
  letter-spacing: 1.2px;
2420
2460
  color: var(--muted);
2421
2461
  margin-bottom: 12px;
@@ -2433,7 +2473,7 @@ dialog {
2433
2473
  gap: 29px;
2434
2474
  padding: 20px 24px;
2435
2475
  border-bottom: 1px solid var(--border);
2436
- font-size: 10px;
2476
+ font-size: 12px;
2437
2477
  color: var(--muted);
2438
2478
  }
2439
2479
  .preview-topbar .brand {
@@ -2458,7 +2498,7 @@ dialog {
2458
2498
  }
2459
2499
  .preview-date {
2460
2500
  margin-left: auto;
2461
- font: 8px var(--mono);
2501
+ font: 11px var(--mono);
2462
2502
  }
2463
2503
  .preview-body {
2464
2504
  display: grid;
@@ -2490,7 +2530,7 @@ dialog {
2490
2530
  padding-top: 15px;
2491
2531
  }
2492
2532
  .preview-metrics span {
2493
- font-size: 8px;
2533
+ font-size: 12px;
2494
2534
  color: var(--muted);
2495
2535
  }
2496
2536
  .preview-metrics strong {
@@ -2525,7 +2565,7 @@ dialog {
2525
2565
  font-weight: 500;
2526
2566
  }
2527
2567
  .preview-provider > div span {
2528
- font-size: 9px;
2568
+ font-size: 12px;
2529
2569
  color: var(--muted);
2530
2570
  }
2531
2571
  .preview-provider b {
@@ -2535,7 +2575,7 @@ dialog {
2535
2575
  letter-spacing: -0.5px;
2536
2576
  }
2537
2577
  .preview-active {
2538
- font: 8px var(--mono);
2578
+ font: 11px var(--mono);
2539
2579
  background: #153020;
2540
2580
  border: 1px solid #284f35;
2541
2581
  color: var(--green);
@@ -2552,16 +2592,16 @@ dialog {
2552
2592
  display: flex;
2553
2593
  justify-content: space-between;
2554
2594
  gap: 12px;
2555
- font-size: 9px;
2595
+ font-size: 12px;
2556
2596
  margin-bottom: 19px;
2557
2597
  }
2558
2598
  .preview-chart strong {
2559
- font-size: 11px;
2599
+ font-size: 13px;
2560
2600
  font-weight: 450;
2561
2601
  }
2562
2602
  .preview-chart > div:first-child span {
2563
2603
  color: var(--green);
2564
- font-size: 8px;
2604
+ font-size: 11px;
2565
2605
  }
2566
2606
  .preview-bars {
2567
2607
  height: 143px;
@@ -2587,7 +2627,7 @@ dialog {
2587
2627
  .preview-chart-axis {
2588
2628
  display: flex;
2589
2629
  justify-content: space-between;
2590
- font: 8px var(--mono);
2630
+ font: 11px var(--mono);
2591
2631
  color: var(--muted);
2592
2632
  margin-top: 12px;
2593
2633
  }
@@ -2604,6 +2644,9 @@ dialog {
2604
2644
  display: flex;
2605
2645
  flex-direction: column;
2606
2646
  justify-content: start;
2647
+ min-width: 0;
2648
+ width: 100%;
2649
+ max-width: 368px;
2607
2650
  gap: 13px;
2608
2651
  }
2609
2652
  .preview-receipt canvas {
@@ -2721,7 +2764,7 @@ dialog {
2721
2764
  padding: 14px 10px;
2722
2765
  }
2723
2766
  .preview-metrics span {
2724
- font-size: 7px;
2767
+ font-size: 12px;
2725
2768
  }
2726
2769
  }
2727
2770
  @media (max-width: 760px) {
@@ -2767,13 +2810,13 @@ dialog {
2767
2810
  margin-top: 13px;
2768
2811
  }
2769
2812
  .preview-label {
2770
- font-size: 6px;
2813
+ font-size: 11px;
2771
2814
  letter-spacing: 0.8px;
2772
2815
  }
2773
2816
  .preview-topbar {
2774
2817
  padding: 18px;
2775
2818
  gap: 19px;
2776
- font-size: 8px;
2819
+ font-size: 12px;
2777
2820
  }
2778
2821
  .preview-topbar .brand {
2779
2822
  font-size: 14px;
@@ -2783,7 +2826,7 @@ dialog {
2783
2826
  display: none;
2784
2827
  }
2785
2828
  .preview-date {
2786
- font-size: 8px;
2829
+ font-size: 11px;
2787
2830
  }
2788
2831
  .preview-body {
2789
2832
  grid-template-columns: 1fr;
@@ -2802,7 +2845,7 @@ dialog {
2802
2845
  font-size: 25px;
2803
2846
  }
2804
2847
  .preview-metrics span {
2805
- font-size: 9px;
2848
+ font-size: 12px;
2806
2849
  }
2807
2850
  .preview-provider {
2808
2851
  padding: 17px;
@@ -2897,7 +2940,7 @@ dialog {
2897
2940
  color: #a8b7af;
2898
2941
  }
2899
2942
  .product-preview {
2900
- max-width: 1180px;
2943
+ max-width: 1240px;
2901
2944
  }
2902
2945
  .preview-window {
2903
2946
  background: #0b0d10;
@@ -2933,7 +2976,7 @@ dialog {
2933
2976
  letter-spacing: -0.9px;
2934
2977
  }
2935
2978
  .preview-metrics span {
2936
- font-size: 9px;
2979
+ font-size: 12px;
2937
2980
  color: #b7c0c7;
2938
2981
  }
2939
2982
  .preview-provider,
@@ -3039,13 +3082,13 @@ dialog {
3039
3082
  padding-top: 16px;
3040
3083
  }
3041
3084
  .preview-metrics span {
3042
- font-size: 10px;
3085
+ font-size: 12px;
3043
3086
  }
3044
3087
  .preview-metrics strong {
3045
3088
  font-size: 27px;
3046
3089
  }
3047
3090
  .preview-receipt {
3048
- max-width: 335px;
3091
+ max-width: 368px;
3049
3092
  }
3050
3093
  .preview-bars {
3051
3094
  height: 138px;
@@ -3067,7 +3110,7 @@ dialog {
3067
3110
  padding-bottom: 36px;
3068
3111
  }
3069
3112
  .preview-label {
3070
- font-size: 7px;
3113
+ font-size: 11px;
3071
3114
  }
3072
3115
  .copy-command {
3073
3116
  min-height: 40px;
@@ -3092,7 +3135,7 @@ dialog {
3092
3135
  object-fit: contain;
3093
3136
  }
3094
3137
  .preview-body {
3095
- grid-template-columns: minmax(0, 1fr) 285px;
3138
+ grid-template-columns: minmax(0, 1fr) 368px;
3096
3139
  }
3097
3140
  .preview-receipt canvas {
3098
3141
  filter: drop-shadow(0 2px 2px #0007) drop-shadow(0 10px 14px #0006);
@@ -3107,9 +3150,17 @@ dialog {
3107
3150
  .preview-bars i {
3108
3151
  background: linear-gradient(to bottom, #37ec85, #278451);
3109
3152
  }
3153
+ @media (max-width: 1100px) {
3154
+ .preview-metrics {
3155
+ grid-template-columns: repeat(2, minmax(0, 1fr));
3156
+ }
3157
+ }
3110
3158
  @media (max-width: 1000px) {
3111
3159
  .preview-body {
3112
- grid-template-columns: minmax(0, 1fr) 245px;
3160
+ grid-template-columns: 1fr;
3161
+ }
3162
+ .preview-receipt {
3163
+ margin-inline: auto;
3113
3164
  }
3114
3165
  }
3115
3166
  @media (max-width: 760px) {
@@ -3117,7 +3168,7 @@ dialog {
3117
3168
  grid-template-columns: 1fr;
3118
3169
  }
3119
3170
  .preview-receipt {
3120
- max-width: 335px;
3171
+ max-width: 368px;
3121
3172
  }
3122
3173
  .preview-topbar .brand-mark {
3123
3174
  width: 20px;
@@ -216,7 +216,12 @@ export function drawReceipt(
216
216
  canvas ,
217
217
  model ,
218
218
  theme = 'dark',
219
+ presentation = 'standard',
219
220
  ) {
221
+ // A public example needs larger print when viewed inside the dashboard preview.
222
+ // The paper, layout and export renderer remain shared with the local application.
223
+ const printSize = (size ) =>
224
+ presentation === 'preview' ? Math.max(13, size * 1.12) : size;
220
225
  const light = theme === 'light';
221
226
  const ink = (color ) =>
222
227
  light
@@ -288,6 +293,7 @@ export function drawReceipt(
288
293
  bold = false,
289
294
  maxWidth = width - 48,
290
295
  ) => {
296
+ size = printSize(size);
291
297
  c.font = `${bold ? '600 ' : ''}${size}px ${mono}`;
292
298
  while (c.measureText(value).width > maxWidth && size > 8) {
293
299
  size -= 0.5;
@@ -335,7 +341,7 @@ export function drawReceipt(
335
341
  ? '#e1e2e4'
336
342
  : '#bdc0c6';
337
343
  const valueSize = multiple ? 25 : row.accent ? 22 : row.strong ? 18 : 16;
338
- c.font = `${valueSize}px ${mono}`;
344
+ c.font = `${printSize(valueSize)}px ${mono}`;
339
345
  const valueWidth = c.measureText(row.right).width;
340
346
  text(
341
347
  label,
@@ -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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@subvalue/cli",
3
- "version": "0.1.4",
3
+ "version": "0.1.5",
4
4
  "description": "Local, read-only Codex and Claude Code API-equivalent usage dashboard",
5
5
  "type": "module",
6
6
  "license": "MIT",