@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.
- package/PRE_RELEASE.md +1 -1
- package/README.md +6 -2
- package/dist/packages/cli/src/server.js +75 -28
- package/dist/packages/core/src/billing.js +112 -0
- package/dist/packages/core/src/summary.js +69 -68
- package/dist/packages/core/src/types.js +11 -2
- package/dist/packages/receipt/src/index.js +165 -17
- package/dist/packages/ui/src/brand.js +1 -0
- package/dist/public/app.css +210 -2
- package/dist/public/apps/web/src/landing.js +20 -5
- package/dist/public/apps/web/src/main.js +268 -69
- package/dist/public/landing.css +265 -29
- package/dist/public/packages/core/src/billing.js +112 -0
- package/dist/public/packages/receipt/src/index.js +165 -17
- package/dist/public/packages/ui/src/brand.js +1 -0
- package/package.json +1 -1
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
56
|
-
|
|
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.
|
|
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: '
|
|
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
|
-
:
|
|
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
|
-
|
|
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 =
|
|
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
|
|
338
|
-
|
|
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
|
-
|
|
490
|
+
labelLeft,
|
|
343
491
|
y,
|
|
344
|
-
multiple ? 17 : provider ? 17 :
|
|
492
|
+
multiple ? 17 : provider ? 17 : tokenRow ? 12 : 14,
|
|
345
493
|
multiple ? '#e1e2e4' : color,
|
|
346
494
|
'left',
|
|
347
495
|
provider,
|
|
348
|
-
provider ?
|
|
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;
|
package/dist/public/app.css
CHANGED
|
@@ -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
|
|
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
|
-
.
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
197
|
+
exampleReceipt,
|
|
198
|
+
'dark',
|
|
199
|
+
'preview',
|
|
185
200
|
);
|
|
186
201
|
document.querySelectorAll ('.copy-command').forEach((b) =>
|
|
187
202
|
b.addEventListener('click', async () => {
|