codex-work-receipt 0.7.3 → 0.8.0
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/CHANGELOG.md +18 -2
- package/README.en.md +27 -7
- package/README.md +27 -7
- package/docs/cli.en.md +55 -5
- package/docs/cli.md +55 -5
- package/docs/codex-pet.en.md +1 -1
- package/docs/codex-pet.md +1 -1
- package/docs/codex-skill.en.md +2 -0
- package/docs/codex-skill.md +2 -0
- package/docs/data-schema.en.md +29 -4
- package/docs/data-schema.md +29 -4
- package/docs/fixtures/cwr-file-v1.json +33 -0
- package/docs/miniprogram-file-import.en.md +67 -0
- package/docs/miniprogram-file-import.md +67 -0
- package/docs/mobile-import.en.md +19 -11
- package/docs/mobile-import.md +18 -10
- package/docs/privacy.en.md +20 -5
- package/docs/privacy.md +20 -5
- package/package.json +2 -2
- package/skills/ai-work-receipt/SKILL.md +2 -2
- package/src/auto-hook.mjs +46 -0
- package/src/auto-runner.mjs +172 -0
- package/src/cli.mjs +158 -60
- package/src/core/args.mjs +34 -0
- package/src/core/auto-mode.mjs +364 -0
- package/src/core/file-payload.mjs +74 -0
- package/src/core/generator.mjs +98 -0
- package/src/core/mode-selector.mjs +31 -0
- package/src/core/open-source.mjs +13 -0
- package/src/core/presentation.mjs +64 -28
- package/src/core/qr-payload.mjs +7 -178
- package/src/core/receipt-record.mjs +18 -10
- package/src/core/transfer-record.mjs +137 -0
- package/src/lib/files.mjs +52 -0
- package/src/parsers/codex.mjs +6 -6
- package/src/renderers/html.mjs +352 -201
package/src/renderers/html.mjs
CHANGED
|
@@ -2,6 +2,7 @@ import fs from "node:fs";
|
|
|
2
2
|
import { createRequire } from "node:module";
|
|
3
3
|
|
|
4
4
|
import { buildCode128B } from "../core/barcode.mjs";
|
|
5
|
+
import { createReceiptFile } from "../core/file-payload.mjs";
|
|
5
6
|
import { formatDate, formatDuration, formatNumber, formatTime } from "../lib/time.mjs";
|
|
6
7
|
import {
|
|
7
8
|
buildCompensation,
|
|
@@ -10,9 +11,17 @@ import {
|
|
|
10
11
|
getRollingSummaryNotice,
|
|
11
12
|
getScopeLabel,
|
|
12
13
|
} from "../core/presentation.mjs";
|
|
14
|
+
import { getHtmlStarPrompt } from "../core/open-source.mjs";
|
|
13
15
|
|
|
14
16
|
const require = createRequire(import.meta.url);
|
|
15
|
-
const DOM_TO_IMAGE_SOURCE = fs.readFileSync(
|
|
17
|
+
const DOM_TO_IMAGE_SOURCE = fs.readFileSync(
|
|
18
|
+
process.env.CODEX_WORK_RECEIPT_DOM_TO_IMAGE || require.resolve("dom-to-image-more"),
|
|
19
|
+
"utf8",
|
|
20
|
+
);
|
|
21
|
+
const MODELFLARE_LOGO_DATA_URL = `data:image/png;base64,${fs.readFileSync(
|
|
22
|
+
new URL("../../docs/images/sponsors/modelflare-logo.png", import.meta.url),
|
|
23
|
+
).toString("base64")}`;
|
|
24
|
+
const MODELFLARE_URL = "https://modelflare.dev/sign-up?partner=OB9YXNSEEGOL";
|
|
16
25
|
|
|
17
26
|
function inlineScript(value) {
|
|
18
27
|
return String(value).replaceAll("</script", "<\\/script");
|
|
@@ -53,16 +62,11 @@ function formatDateKey(value, locale) {
|
|
|
53
62
|
return locale === "en" ? `${match[2]}/${match[3]}/${match[1]}` : `${match[1]}/${match[2]}/${match[3]}`;
|
|
54
63
|
}
|
|
55
64
|
|
|
56
|
-
function
|
|
57
|
-
return Object.entries(values).reduce(
|
|
58
|
-
(result, [key, value]) => result.replaceAll(`{${key}}`, String(value)),
|
|
59
|
-
String(template),
|
|
60
|
-
);
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
export function renderHtml({ record, dataQrDataUrl = null, dataQrDataUrls = null, miniProgramCodeDataUrl = null }) {
|
|
65
|
+
export function renderHtml({ record, dataQrDataUrl = null, miniProgramCodeDataUrl = null, transferFile = null }) {
|
|
64
66
|
const locale = record.locale || DEFAULT_LOCALE;
|
|
65
67
|
const copy = getReceiptCopy(locale);
|
|
68
|
+
const githubStarPrompt = getHtmlStarPrompt(locale);
|
|
69
|
+
const changelogUrl = `${githubStarPrompt.url}/blob/main/CHANGELOG.md`;
|
|
66
70
|
const startAt = new Date(record.period.start_at);
|
|
67
71
|
const endAt = new Date(record.period.end_at);
|
|
68
72
|
const timezone = record.period.timezone;
|
|
@@ -90,13 +94,25 @@ export function renderHtml({ record, dataQrDataUrl = null, dataQrDataUrls = null
|
|
|
90
94
|
minimumFractionDigits: 1,
|
|
91
95
|
maximumFractionDigits: 1,
|
|
92
96
|
}).format(record.stats.average_first_token_ms / 1000);
|
|
97
|
+
const fileBase = `codex-work-receipt-${record.source.scope}-${spansMultipleDates ? `${rangeStartDate}-to-${rangeEndDate}` : `${rangeEndDate}-${record.id.slice(4, 12)}`}`;
|
|
93
98
|
const exportConfig = JSON.stringify({
|
|
94
99
|
idleLabel: copy.exportImage,
|
|
95
100
|
busyLabel: copy.exportingImage,
|
|
96
101
|
successLabel: copy.exportSuccess,
|
|
97
102
|
errorLabel: copy.exportError,
|
|
98
103
|
miniProgramLabel: copy.exportMiniProgramLabel,
|
|
99
|
-
fileBase
|
|
104
|
+
fileBase,
|
|
105
|
+
}).replaceAll("<", "\\u003c");
|
|
106
|
+
const resolvedTransferFile = transferFile || {
|
|
107
|
+
...createReceiptFile(record),
|
|
108
|
+
filename: `${fileBase}.cwr.json`,
|
|
109
|
+
};
|
|
110
|
+
const transferConfig = JSON.stringify({
|
|
111
|
+
filename: resolvedTransferFile.filename || `${fileBase}.cwr.json`,
|
|
112
|
+
content: resolvedTransferFile.content,
|
|
113
|
+
mimeType: resolvedTransferFile.mimeType,
|
|
114
|
+
successLabel: copy.downloadSuccess,
|
|
115
|
+
errorLabel: copy.downloadError,
|
|
100
116
|
}).replaceAll("<", "\\u003c");
|
|
101
117
|
const rows = [
|
|
102
118
|
receiptRow(copy.rows.scope, scopeLabel),
|
|
@@ -114,61 +130,45 @@ export function renderHtml({ record, dataQrDataUrl = null, dataQrDataUrls = null
|
|
|
114
130
|
const miniProgramVisual = miniProgramCodeDataUrl
|
|
115
131
|
? `<img src="${miniProgramCodeDataUrl}" alt="${escapeHtml(copy.miniProgramAlt)}">`
|
|
116
132
|
: `<div class="mini-placeholder" role="img" aria-label="${escapeHtml(copy.placeholderAria)}"><span>${escapeHtml(copy.placeholderLabel)}</span><strong>${escapeHtml(copy.placeholderValue)}</strong></div>`;
|
|
117
|
-
const
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
: [];
|
|
122
|
-
const dataQrItems = qrUrls.map((url, index) => `
|
|
123
|
-
<div class="qr-item" data-data-qr-index="${index}">
|
|
124
|
-
<div class="qr-frame"><img src="${url}" alt="${escapeHtml(copy.dataQrAlt)} ${index + 1}/${qrUrls.length}"></div>
|
|
125
|
-
<strong>${escapeHtml(copy.importData)}${qrUrls.length > 1 ? ` ${index + 1}/${qrUrls.length}` : ""}</strong>
|
|
126
|
-
<span>${escapeHtml(copy.importDataHint)}</span>
|
|
127
|
-
</div>`).join("");
|
|
128
|
-
const isMultipart = qrUrls.length > 1;
|
|
129
|
-
const multipartSetupSeconds = 10;
|
|
130
|
-
const multipartConfig = JSON.stringify({
|
|
131
|
-
enabled: isMultipart,
|
|
132
|
-
setupSeconds: multipartSetupSeconds,
|
|
133
|
-
frameMs: 4000,
|
|
134
|
-
blankMs: 240,
|
|
135
|
-
setupHint: copy.multipartOpenHint,
|
|
136
|
-
partLabel: copy.multipartPartLabel,
|
|
137
|
-
}).replaceAll("<", "\\u003c");
|
|
138
|
-
const transferVisual = isMultipart
|
|
133
|
+
const fileImportSteps = copy.fileImportSteps
|
|
134
|
+
.map((step) => `<li>${escapeHtml(step)}</li>`)
|
|
135
|
+
.join("");
|
|
136
|
+
const dataQrPanel = dataQrDataUrl
|
|
139
137
|
? `
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
<
|
|
138
|
+
<div class="qr-item qr-panel" data-data-qr-panel hidden>
|
|
139
|
+
<div class="qr-frame qr-frame--large"><img src="${dataQrDataUrl}" alt="${escapeHtml(copy.dataQrAlt)}"></div>
|
|
140
|
+
<strong>${escapeHtml(copy.dataQrTitle)}</strong>
|
|
141
|
+
<span>${escapeHtml(copy.dataQrHint)}</span>
|
|
142
|
+
<button class="secondary-button" data-show-mini-program type="button">${escapeHtml(copy.showMiniProgramCode)}</button>
|
|
143
|
+
</div>`
|
|
144
|
+
: "";
|
|
145
|
+
const scanAlternative = dataQrDataUrl
|
|
146
|
+
? `
|
|
147
|
+
<div class="scan-alternative">
|
|
148
|
+
<strong>${escapeHtml(copy.scanAlternativeTitle)}</strong>
|
|
149
|
+
<span>${escapeHtml(copy.scanAlternativeHint)}</span>
|
|
150
|
+
<button class="secondary-button" data-show-data-qr type="button">${escapeHtml(copy.showDataQr)}</button>
|
|
151
|
+
</div>`
|
|
152
|
+
: "";
|
|
153
|
+
const transferVisual = `
|
|
154
|
+
<div class="transfer-layout">
|
|
155
|
+
<div class="qr-switcher">
|
|
156
|
+
<div class="qr-item qr-panel" data-mini-program-panel>
|
|
143
157
|
<div class="qr-frame qr-frame--large">${miniProgramVisual}</div>
|
|
144
|
-
<strong>${escapeHtml(copy.
|
|
145
|
-
<span
|
|
158
|
+
<strong data-export-mini-label>${escapeHtml(copy.openMiniProgram)}</strong>
|
|
159
|
+
<span>${escapeHtml(copy.openMiniProgramHint)}</span>
|
|
146
160
|
</div>
|
|
161
|
+
${dataQrPanel}
|
|
147
162
|
</div>
|
|
148
|
-
<div class="
|
|
149
|
-
<
|
|
150
|
-
<
|
|
151
|
-
<
|
|
152
|
-
<span class="
|
|
153
|
-
<
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
<div class="qr-grid qr-grid--export-only" hidden>
|
|
157
|
-
<div class="qr-item">
|
|
158
|
-
<div class="qr-frame">${miniProgramVisual}</div>
|
|
159
|
-
<strong data-export-mini-label>${escapeHtml(copy.openMiniProgram)}</strong>
|
|
160
|
-
<span>${escapeHtml(copy.openMiniProgramHint)}</span>
|
|
161
|
-
</div>
|
|
162
|
-
${dataQrItems}
|
|
163
|
-
</div>`
|
|
164
|
-
: `
|
|
165
|
-
<div class="qr-grid qr-grid--single">
|
|
166
|
-
<div class="qr-item">
|
|
167
|
-
<div class="qr-frame">${miniProgramVisual}</div>
|
|
168
|
-
<strong data-export-mini-label>${escapeHtml(copy.openMiniProgram)}</strong>
|
|
169
|
-
<span>${escapeHtml(copy.openMiniProgramHint)}</span>
|
|
163
|
+
<div class="file-import-card" data-file-import-controls>
|
|
164
|
+
<strong>${escapeHtml(copy.fileImportTitle)}</strong>
|
|
165
|
+
<ol>${fileImportSteps}</ol>
|
|
166
|
+
<button class="download-file-button" id="download-import-file" type="button">${escapeHtml(copy.downloadFile)}</button>
|
|
167
|
+
<span class="download-file-hint">${escapeHtml(copy.downloadFileHint)}</span>
|
|
168
|
+
<span class="download-status" id="download-status" role="status" aria-live="polite"></span>
|
|
169
|
+
<p>${escapeHtml(copy.fileImportPrivacy)}</p>
|
|
170
|
+
${scanAlternative}
|
|
170
171
|
</div>
|
|
171
|
-
${dataQrItems}
|
|
172
172
|
</div>`;
|
|
173
173
|
|
|
174
174
|
return `<!doctype html>
|
|
@@ -226,58 +226,150 @@ export function renderHtml({ record, dataQrDataUrl = null, dataQrDataUrls = null
|
|
|
226
226
|
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
|
|
227
227
|
transition: background .2s ease, color .2s ease;
|
|
228
228
|
}
|
|
229
|
-
.
|
|
230
|
-
|
|
229
|
+
.layout {
|
|
230
|
+
display: grid;
|
|
231
|
+
grid-template-columns: minmax(0, 540px) 180px;
|
|
232
|
+
gap: 240px;
|
|
233
|
+
max-width: 1020px;
|
|
231
234
|
margin: 0 auto;
|
|
232
235
|
padding: 30px 18px 54px;
|
|
236
|
+
align-items: start;
|
|
233
237
|
}
|
|
234
|
-
.
|
|
235
|
-
|
|
236
|
-
|
|
238
|
+
.page {
|
|
239
|
+
width: 100%;
|
|
240
|
+
padding: 0;
|
|
241
|
+
}
|
|
242
|
+
.sidebar {
|
|
243
|
+
position: sticky;
|
|
244
|
+
top: 30px;
|
|
245
|
+
display: flex;
|
|
246
|
+
flex-direction: column;
|
|
237
247
|
gap: 12px;
|
|
238
|
-
|
|
248
|
+
}
|
|
249
|
+
.sidebar-card {
|
|
250
|
+
padding: 12px;
|
|
251
|
+
border: 1px solid #333;
|
|
252
|
+
border-radius: 8px;
|
|
253
|
+
background: #1a1a1a;
|
|
254
|
+
color: #ccc;
|
|
255
|
+
}
|
|
256
|
+
.sidebar-card__title {
|
|
257
|
+
display: flex;
|
|
258
|
+
align-items: center;
|
|
259
|
+
gap: 5px;
|
|
260
|
+
margin: 0 0 6px;
|
|
261
|
+
color: #e0e0e0;
|
|
262
|
+
font-family: system-ui, -apple-system, BlinkMacSystemFont, "PingFang SC", sans-serif;
|
|
263
|
+
font-size: 11px;
|
|
264
|
+
font-weight: 700;
|
|
265
|
+
letter-spacing: .03em;
|
|
266
|
+
}
|
|
267
|
+
.sidebar-card__title svg {
|
|
268
|
+
width: 12px;
|
|
269
|
+
height: 12px;
|
|
270
|
+
flex-shrink: 0;
|
|
271
|
+
color: #e0e0e0;
|
|
272
|
+
}
|
|
273
|
+
.sidebar-card p {
|
|
274
|
+
margin: 0 0 8px;
|
|
275
|
+
color: #999;
|
|
276
|
+
font-family: system-ui, -apple-system, BlinkMacSystemFont, "PingFang SC", sans-serif;
|
|
277
|
+
font-size: 11px;
|
|
278
|
+
line-height: 1.5;
|
|
279
|
+
}
|
|
280
|
+
.sidebar-card a {
|
|
281
|
+
color: #fff;
|
|
282
|
+
font-size: 11px;
|
|
283
|
+
text-decoration: none;
|
|
284
|
+
}
|
|
285
|
+
.sidebar-card a:hover,
|
|
286
|
+
.sidebar-card a:focus-visible {
|
|
287
|
+
color: #fff;
|
|
288
|
+
text-decoration: underline;
|
|
289
|
+
}
|
|
290
|
+
.sidebar-sponsor { text-align: center; }
|
|
291
|
+
.sidebar-sponsor__label {
|
|
292
|
+
display: block;
|
|
293
|
+
margin-bottom: 8px;
|
|
294
|
+
color: #777;
|
|
295
|
+
font-family: system-ui, -apple-system, BlinkMacSystemFont, "PingFang SC", sans-serif;
|
|
296
|
+
font-size: 9px;
|
|
297
|
+
letter-spacing: .08em;
|
|
298
|
+
text-transform: uppercase;
|
|
299
|
+
}
|
|
300
|
+
.sidebar-sponsor__link { display: block; }
|
|
301
|
+
.sidebar-sponsor img {
|
|
302
|
+
display: block;
|
|
303
|
+
width: 40px;
|
|
304
|
+
height: 40px;
|
|
305
|
+
margin: 0 auto 6px;
|
|
306
|
+
border-radius: 4px;
|
|
307
|
+
}
|
|
308
|
+
.sidebar-sponsor__name {
|
|
309
|
+
color: #ccc;
|
|
310
|
+
font-family: system-ui, -apple-system, BlinkMacSystemFont, "PingFang SC", sans-serif;
|
|
311
|
+
font-size: 11px;
|
|
312
|
+
font-weight: 600;
|
|
313
|
+
line-height: 1.4;
|
|
314
|
+
}
|
|
315
|
+
.toolbar {
|
|
316
|
+
display: flex;
|
|
317
|
+
width: 100%;
|
|
318
|
+
justify-content: space-between;
|
|
319
|
+
align-items: center;
|
|
320
|
+
margin-bottom: 20px;
|
|
239
321
|
}
|
|
240
322
|
.theme-switcher {
|
|
241
323
|
display: flex;
|
|
242
|
-
flex-wrap:
|
|
243
|
-
justify-content: center;
|
|
244
|
-
gap: 8px;
|
|
324
|
+
flex-wrap: nowrap;
|
|
245
325
|
}
|
|
246
326
|
.theme-button {
|
|
247
327
|
appearance: none;
|
|
248
328
|
border: 1px solid color-mix(in srgb, var(--ink) 28%, transparent);
|
|
249
|
-
border-radius:
|
|
329
|
+
border-radius: 0;
|
|
250
330
|
background: color-mix(in srgb, var(--paper) 74%, transparent);
|
|
251
331
|
color: var(--ink);
|
|
252
332
|
cursor: pointer;
|
|
253
333
|
font: inherit;
|
|
254
334
|
font-size: 12px;
|
|
255
|
-
padding:
|
|
335
|
+
padding: 7px 12px;
|
|
336
|
+
margin-left: -1px;
|
|
337
|
+
transition: background .15s ease, color .15s ease;
|
|
338
|
+
}
|
|
339
|
+
.theme-button:first-child {
|
|
340
|
+
margin-left: 0;
|
|
341
|
+
border-radius: 6px 0 0 6px;
|
|
342
|
+
}
|
|
343
|
+
.theme-button:last-child {
|
|
344
|
+
border-radius: 0 6px 6px 0;
|
|
256
345
|
}
|
|
257
346
|
.theme-button[aria-pressed="true"] {
|
|
347
|
+
position: relative;
|
|
348
|
+
z-index: 1;
|
|
349
|
+
border-color: var(--ink);
|
|
258
350
|
background: var(--ink);
|
|
259
351
|
color: var(--paper);
|
|
260
352
|
}
|
|
261
353
|
.export-actions {
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
354
|
+
position: relative;
|
|
355
|
+
display: flex;
|
|
356
|
+
align-items: center;
|
|
357
|
+
gap: 8px;
|
|
265
358
|
}
|
|
266
359
|
.export-button {
|
|
267
360
|
appearance: none;
|
|
268
|
-
|
|
269
|
-
border:
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
color: var(--paper);
|
|
361
|
+
border: 1px solid #333;
|
|
362
|
+
border-radius: 6px;
|
|
363
|
+
background: #222;
|
|
364
|
+
color: #fff;
|
|
273
365
|
cursor: pointer;
|
|
274
366
|
font: inherit;
|
|
275
|
-
font-size:
|
|
276
|
-
font-weight:
|
|
277
|
-
padding:
|
|
367
|
+
font-size: 12px;
|
|
368
|
+
font-weight: 600;
|
|
369
|
+
padding: 7px 14px;
|
|
278
370
|
transition: opacity .15s ease, transform .15s ease;
|
|
279
371
|
}
|
|
280
|
-
.export-button:hover { transform: translateY(-1px); }
|
|
372
|
+
.export-button:hover { opacity: .85; transform: translateY(-1px); }
|
|
281
373
|
.export-button:disabled { cursor: wait; opacity: .62; transform: none; }
|
|
282
374
|
.export-status {
|
|
283
375
|
min-height: 16px;
|
|
@@ -423,13 +515,16 @@ export function renderHtml({ record, dataQrDataUrl = null, dataQrDataUrls = null
|
|
|
423
515
|
.transfer-heading { text-align: center; }
|
|
424
516
|
.transfer-heading h2 { margin: 0; font-size: 18px; letter-spacing: .08em; }
|
|
425
517
|
.transfer-heading p { margin: 7px 0 0; color: var(--muted); font-size: 11px; }
|
|
426
|
-
.
|
|
518
|
+
.transfer-layout {
|
|
427
519
|
display: grid;
|
|
428
|
-
grid-template-columns: 1fr
|
|
429
|
-
|
|
520
|
+
grid-template-columns: minmax(0, 1fr) minmax(0, 1.35fr);
|
|
521
|
+
align-items: start;
|
|
522
|
+
gap: 22px;
|
|
430
523
|
margin-top: 20px;
|
|
431
524
|
}
|
|
525
|
+
.qr-switcher { min-width: 0; }
|
|
432
526
|
.qr-item { text-align: center; }
|
|
527
|
+
.qr-panel[hidden] { display: none; }
|
|
433
528
|
.qr-frame {
|
|
434
529
|
display: grid;
|
|
435
530
|
place-items: center;
|
|
@@ -443,29 +538,67 @@ export function renderHtml({ record, dataQrDataUrl = null, dataQrDataUrls = null
|
|
|
443
538
|
.qr-frame img { display: block; width: 100%; height: 100%; object-fit: contain; }
|
|
444
539
|
.qr-item strong { display: block; font-size: 12px; }
|
|
445
540
|
.qr-item span { display: block; margin-top: 4px; color: var(--muted); font-size: 10px; line-height: 1.45; }
|
|
446
|
-
.qr-
|
|
447
|
-
.
|
|
448
|
-
.multipart-panel {
|
|
449
|
-
display: grid;
|
|
450
|
-
place-items: center;
|
|
451
|
-
min-height: 310px;
|
|
541
|
+
.qr-frame--large { width: min(100%, 210px); }
|
|
542
|
+
.file-import-card {
|
|
452
543
|
padding: 18px;
|
|
453
544
|
border: 1px solid var(--line);
|
|
454
545
|
background: color-mix(in srgb, var(--paper) 94%, var(--ink));
|
|
546
|
+
}
|
|
547
|
+
.file-import-card > strong { display: block; font-size: 14px; }
|
|
548
|
+
.file-import-card ol {
|
|
549
|
+
margin: 12px 0 16px;
|
|
550
|
+
padding-left: 20px;
|
|
551
|
+
color: var(--muted);
|
|
552
|
+
font-family: system-ui, -apple-system, BlinkMacSystemFont, "PingFang SC", sans-serif;
|
|
553
|
+
font-size: 11px;
|
|
554
|
+
line-height: 1.65;
|
|
555
|
+
}
|
|
556
|
+
.download-file-button {
|
|
557
|
+
width: 100%;
|
|
558
|
+
padding: 10px 14px;
|
|
559
|
+
border: 1px solid var(--ink);
|
|
560
|
+
border-radius: 999px;
|
|
561
|
+
background: var(--ink);
|
|
562
|
+
color: var(--paper);
|
|
563
|
+
cursor: pointer;
|
|
564
|
+
font: inherit;
|
|
565
|
+
font-size: 12px;
|
|
566
|
+
font-weight: 800;
|
|
567
|
+
}
|
|
568
|
+
.download-file-button:hover { transform: translateY(-1px); }
|
|
569
|
+
.download-file-hint,
|
|
570
|
+
.download-status {
|
|
571
|
+
display: block;
|
|
572
|
+
margin-top: 6px;
|
|
573
|
+
color: var(--muted);
|
|
574
|
+
font-family: system-ui, -apple-system, BlinkMacSystemFont, "PingFang SC", sans-serif;
|
|
575
|
+
font-size: 10px;
|
|
576
|
+
line-height: 1.45;
|
|
577
|
+
text-align: center;
|
|
578
|
+
}
|
|
579
|
+
.download-status { min-height: 14px; }
|
|
580
|
+
.download-status[data-state="success"] { color: #287a36; }
|
|
581
|
+
.download-status[data-state="error"] { color: #b33a2e; }
|
|
582
|
+
.file-import-card > p {
|
|
583
|
+
margin: 14px 0 0;
|
|
584
|
+
color: var(--muted);
|
|
585
|
+
font-family: system-ui, -apple-system, BlinkMacSystemFont, "PingFang SC", sans-serif;
|
|
586
|
+
font-size: 10px;
|
|
587
|
+
line-height: 1.55;
|
|
588
|
+
text-align: center;
|
|
589
|
+
}
|
|
590
|
+
.scan-alternative {
|
|
591
|
+
margin-top: 16px;
|
|
592
|
+
padding-top: 14px;
|
|
593
|
+
border-top: 1px dashed var(--line);
|
|
455
594
|
text-align: center;
|
|
456
595
|
}
|
|
457
|
-
.
|
|
458
|
-
.
|
|
459
|
-
.
|
|
460
|
-
.
|
|
461
|
-
.
|
|
462
|
-
|
|
463
|
-
.multipart-stage__title { margin-top: 4px; font-size: 14px; }
|
|
464
|
-
.multipart-stage__part { margin-top: 8px; font-size: 12px; font-weight: 800; letter-spacing: .06em; }
|
|
465
|
-
.multipart-stage__hint { max-width: 360px; margin-top: 6px; color: var(--muted); font-size: 10px; line-height: 1.5; }
|
|
466
|
-
#multipart-active-qr[aria-busy="true"] { visibility: hidden; }
|
|
467
|
-
.multipart-secondary {
|
|
468
|
-
margin-top: 14px;
|
|
596
|
+
.scan-alternative strong,
|
|
597
|
+
.scan-alternative span { display: block; }
|
|
598
|
+
.scan-alternative strong { font-size: 12px; }
|
|
599
|
+
.scan-alternative span { margin-top: 4px; color: var(--muted); font-size: 10px; line-height: 1.45; }
|
|
600
|
+
.secondary-button {
|
|
601
|
+
margin-top: 10px;
|
|
469
602
|
padding: 7px 12px;
|
|
470
603
|
border: 1px solid var(--line);
|
|
471
604
|
border-radius: 999px;
|
|
@@ -475,7 +608,7 @@ export function renderHtml({ record, dataQrDataUrl = null, dataQrDataUrls = null
|
|
|
475
608
|
font-size: 10px;
|
|
476
609
|
cursor: pointer;
|
|
477
610
|
}
|
|
478
|
-
.
|
|
611
|
+
.secondary-button:hover { background: color-mix(in srgb, var(--ink) 7%, transparent); }
|
|
479
612
|
.mini-placeholder {
|
|
480
613
|
display: grid;
|
|
481
614
|
place-content: center;
|
|
@@ -513,16 +646,41 @@ export function renderHtml({ record, dataQrDataUrl = null, dataQrDataUrls = null
|
|
|
513
646
|
line-height: 1.6;
|
|
514
647
|
text-align: center;
|
|
515
648
|
}
|
|
649
|
+
@media (max-width: 1020px) {
|
|
650
|
+
.layout {
|
|
651
|
+
grid-template-columns: 1fr;
|
|
652
|
+
max-width: 540px;
|
|
653
|
+
gap: 0;
|
|
654
|
+
}
|
|
655
|
+
.sidebar {
|
|
656
|
+
position: static;
|
|
657
|
+
flex-direction: row;
|
|
658
|
+
flex-wrap: wrap;
|
|
659
|
+
gap: 10px;
|
|
660
|
+
margin-top: 28px;
|
|
661
|
+
}
|
|
662
|
+
.sidebar-card {
|
|
663
|
+
flex: 1 1 160px;
|
|
664
|
+
min-width: 0;
|
|
665
|
+
}
|
|
666
|
+
}
|
|
516
667
|
@media (max-width: 420px) {
|
|
517
|
-
.
|
|
668
|
+
.layout { padding-inline: 10px; }
|
|
669
|
+
.toolbar {
|
|
670
|
+
flex-direction: column;
|
|
671
|
+
gap: 10px;
|
|
672
|
+
}
|
|
518
673
|
.receipt { padding-inline: 20px; }
|
|
519
674
|
.meta { grid-template-columns: 1fr; }
|
|
520
675
|
.meta div:nth-child(even) { text-align: left; }
|
|
521
|
-
.
|
|
676
|
+
.transfer-layout { grid-template-columns: 1fr; }
|
|
677
|
+
.sidebar { flex-direction: column; }
|
|
678
|
+
.sidebar-card { flex-basis: auto; }
|
|
522
679
|
}
|
|
523
680
|
</style>
|
|
524
681
|
</head>
|
|
525
682
|
<body>
|
|
683
|
+
<div class="layout">
|
|
526
684
|
<main class="page">
|
|
527
685
|
<div class="toolbar">
|
|
528
686
|
<nav class="theme-switcher" aria-label="${escapeHtml(copy.themeAria)}">
|
|
@@ -587,10 +745,35 @@ export function renderHtml({ record, dataQrDataUrl = null, dataQrDataUrls = null
|
|
|
587
745
|
|
|
588
746
|
<p class="privacy">${escapeHtml(copy.privacy)}</p>
|
|
589
747
|
</main>
|
|
748
|
+
<aside class="sidebar" aria-label="${escapeHtml(copy.sidebar.aria)}">
|
|
749
|
+
<div class="sidebar-card">
|
|
750
|
+
<h3 class="sidebar-card__title">
|
|
751
|
+
<svg viewBox="0 0 16 16" fill="currentColor" aria-hidden="true"><path d="M8 .25a.75.75 0 0 1 .673.418l1.882 3.815 4.21.612a.75.75 0 0 1 .416 1.279l-3.046 2.97.719 4.192a.75.75 0 0 1-1.088.791L8 12.347l-3.766 1.98a.75.75 0 0 1-1.088-.79l.72-4.194L.818 6.374a.75.75 0 0 1 .416-1.28l4.21-.611L7.327.668A.75.75 0 0 1 8 .25Z"/></svg>
|
|
752
|
+
${escapeHtml(copy.sidebar.supportTitle)}
|
|
753
|
+
</h3>
|
|
754
|
+
<p>${escapeHtml(copy.sidebar.supportDescription)}</p>
|
|
755
|
+
<a class="github-star-link" href="${escapeHtml(githubStarPrompt.url)}" target="_blank" rel="noopener noreferrer">${escapeHtml(githubStarPrompt.label)}</a>
|
|
756
|
+
</div>
|
|
757
|
+
<div class="sidebar-card">
|
|
758
|
+
<h3 class="sidebar-card__title">
|
|
759
|
+
<svg viewBox="0 0 16 16" fill="currentColor" aria-hidden="true"><path d="M11.93 8.5a4.002 4.002 0 0 1-7.86 0H.75a.75.75 0 0 1 0-1.5h3.32a4.002 4.002 0 0 1 7.86 0h3.32a.75.75 0 0 1 0 1.5Zm-1.43-.75a2.5 2.5 0 1 0-5 0 2.5 2.5 0 0 0 5 0Z"/></svg>
|
|
760
|
+
${escapeHtml(copy.sidebar.changelogTitle)}
|
|
761
|
+
</h3>
|
|
762
|
+
<a href="${escapeHtml(changelogUrl)}" target="_blank" rel="noopener noreferrer">${escapeHtml(copy.sidebar.changelogLink)}</a>
|
|
763
|
+
</div>
|
|
764
|
+
<div class="sidebar-card sidebar-sponsor">
|
|
765
|
+
<span class="sidebar-sponsor__label">${escapeHtml(copy.sidebar.sponsorLabel)}</span>
|
|
766
|
+
<a class="sidebar-sponsor__link" href="${MODELFLARE_URL}" target="_blank" rel="noopener noreferrer">
|
|
767
|
+
<img src="${MODELFLARE_LOGO_DATA_URL}" alt="${escapeHtml(copy.sidebar.sponsorAlt)}" width="40" height="40">
|
|
768
|
+
<span class="sidebar-sponsor__name">ModelFlare<br>modelflare.dev</span>
|
|
769
|
+
</a>
|
|
770
|
+
</div>
|
|
771
|
+
</aside>
|
|
772
|
+
</div>
|
|
590
773
|
<script>${inlineScript(DOM_TO_IMAGE_SOURCE)}</script>
|
|
591
774
|
<script>
|
|
592
775
|
const exportConfig = ${exportConfig};
|
|
593
|
-
const
|
|
776
|
+
const transferConfig = ${transferConfig};
|
|
594
777
|
const themes = new Set(["classic", "diner", "payroll"]);
|
|
595
778
|
const buttons = [...document.querySelectorAll("[data-theme-value]")];
|
|
596
779
|
function applyTheme(theme) {
|
|
@@ -604,86 +787,44 @@ export function renderHtml({ record, dataQrDataUrl = null, dataQrDataUrls = null
|
|
|
604
787
|
try { savedTheme = localStorage.getItem("codex-work-receipt-theme"); } catch {}
|
|
605
788
|
applyTheme(savedTheme || document.documentElement.dataset.theme);
|
|
606
789
|
|
|
607
|
-
const
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
rotationTimer = null;
|
|
629
|
-
switchTimer = null;
|
|
630
|
-
}
|
|
631
|
-
|
|
632
|
-
function setupHintText(seconds) {
|
|
633
|
-
return String(multipartConfig.setupHint).replaceAll("{seconds}", String(seconds));
|
|
634
|
-
}
|
|
635
|
-
|
|
636
|
-
function partLabelText(index) {
|
|
637
|
-
return String(multipartConfig.partLabel)
|
|
638
|
-
.replaceAll("{current}", String(index + 1))
|
|
639
|
-
.replaceAll("{total}", String(urls.length));
|
|
640
|
-
}
|
|
641
|
-
|
|
642
|
-
function showPart(index, immediate = false) {
|
|
643
|
-
if (!urls.length) return;
|
|
644
|
-
activeIndex = (index + urls.length) % urls.length;
|
|
645
|
-
const applyPart = () => {
|
|
646
|
-
activeQr.src = urls[activeIndex];
|
|
647
|
-
activeQr.alt = partLabelText(activeIndex);
|
|
648
|
-
activeQr.setAttribute("aria-busy", "false");
|
|
649
|
-
partLabel.textContent = partLabelText(activeIndex);
|
|
650
|
-
switchTimer = null;
|
|
651
|
-
};
|
|
652
|
-
if (immediate) {
|
|
653
|
-
applyPart();
|
|
654
|
-
return;
|
|
790
|
+
const downloadFileButton = document.getElementById("download-import-file");
|
|
791
|
+
const downloadStatus = document.getElementById("download-status");
|
|
792
|
+
if (downloadFileButton && downloadStatus) {
|
|
793
|
+
downloadFileButton.addEventListener("click", () => {
|
|
794
|
+
try {
|
|
795
|
+
const blob = new Blob([transferConfig.content], { type: transferConfig.mimeType });
|
|
796
|
+
const objectUrl = URL.createObjectURL(blob);
|
|
797
|
+
const link = document.createElement("a");
|
|
798
|
+
link.href = objectUrl;
|
|
799
|
+
link.download = transferConfig.filename;
|
|
800
|
+
link.rel = "noopener";
|
|
801
|
+
document.body.appendChild(link);
|
|
802
|
+
link.click();
|
|
803
|
+
link.remove();
|
|
804
|
+
setTimeout(() => URL.revokeObjectURL(objectUrl), 0);
|
|
805
|
+
downloadStatus.textContent = transferConfig.successLabel;
|
|
806
|
+
downloadStatus.dataset.state = "success";
|
|
807
|
+
} catch (error) {
|
|
808
|
+
console.error(error);
|
|
809
|
+
downloadStatus.textContent = transferConfig.errorLabel;
|
|
810
|
+
downloadStatus.dataset.state = "error";
|
|
655
811
|
}
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
}
|
|
659
|
-
|
|
660
|
-
function startMultipartTransfer() {
|
|
661
|
-
stopMultipartTimers();
|
|
662
|
-
setup.hidden = true;
|
|
663
|
-
stage.hidden = false;
|
|
664
|
-
showPart(0, true);
|
|
665
|
-
rotationTimer = setInterval(() => showPart(activeIndex + 1), multipartConfig.frameMs);
|
|
666
|
-
}
|
|
667
|
-
|
|
668
|
-
function showMultipartSetup() {
|
|
669
|
-
stopMultipartTimers();
|
|
670
|
-
stage.hidden = true;
|
|
671
|
-
setup.hidden = false;
|
|
672
|
-
let remaining = multipartConfig.setupSeconds;
|
|
673
|
-
setupHint.textContent = setupHintText(remaining);
|
|
674
|
-
setupTimer = setInterval(() => {
|
|
675
|
-
remaining -= 1;
|
|
676
|
-
if (remaining <= 0) {
|
|
677
|
-
startMultipartTransfer();
|
|
678
|
-
return;
|
|
679
|
-
}
|
|
680
|
-
setupHint.textContent = setupHintText(remaining);
|
|
681
|
-
}, 1000);
|
|
682
|
-
}
|
|
812
|
+
});
|
|
813
|
+
}
|
|
683
814
|
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
815
|
+
const miniProgramPanel = document.querySelector("[data-mini-program-panel]");
|
|
816
|
+
const dataQrPanel = document.querySelector("[data-data-qr-panel]");
|
|
817
|
+
const showDataQrButton = document.querySelector("[data-show-data-qr]");
|
|
818
|
+
const showMiniProgramButton = document.querySelector("[data-show-mini-program]");
|
|
819
|
+
if (miniProgramPanel && dataQrPanel && showDataQrButton && showMiniProgramButton) {
|
|
820
|
+
showDataQrButton.addEventListener("click", () => {
|
|
821
|
+
miniProgramPanel.hidden = true;
|
|
822
|
+
dataQrPanel.hidden = false;
|
|
823
|
+
});
|
|
824
|
+
showMiniProgramButton.addEventListener("click", () => {
|
|
825
|
+
dataQrPanel.hidden = true;
|
|
826
|
+
miniProgramPanel.hidden = false;
|
|
827
|
+
});
|
|
687
828
|
}
|
|
688
829
|
|
|
689
830
|
const exportButton = document.getElementById("save-receipt-image");
|
|
@@ -706,33 +847,42 @@ export function renderHtml({ record, dataQrDataUrl = null, dataQrDataUrls = null
|
|
|
706
847
|
}
|
|
707
848
|
|
|
708
849
|
function sanitizeExportNode(node) {
|
|
709
|
-
const
|
|
710
|
-
if (
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
850
|
+
const fileImportControls = node.querySelector("[data-file-import-controls]");
|
|
851
|
+
if (fileImportControls) fileImportControls.remove();
|
|
852
|
+
const dataQr = node.querySelector("[data-data-qr-panel]");
|
|
853
|
+
if (dataQr) dataQr.remove();
|
|
854
|
+
const miniProgram = node.querySelector("[data-mini-program-panel]");
|
|
855
|
+
if (miniProgram) miniProgram.hidden = false;
|
|
856
|
+
const transferLayout = node.querySelector(".transfer-layout");
|
|
857
|
+
if (transferLayout) {
|
|
858
|
+
transferLayout.style.gridTemplateColumns = "minmax(0, 1fr)";
|
|
859
|
+
transferLayout.style.justifyItems = "center";
|
|
716
860
|
}
|
|
717
861
|
|
|
718
|
-
node.querySelectorAll("[data-data-qr-index]").forEach((item) => item.remove());
|
|
719
|
-
node.querySelectorAll(".qr-grid").forEach((grid) => {
|
|
720
|
-
grid.style.gridTemplateColumns = "minmax(0, 1fr)";
|
|
721
|
-
grid.style.justifyItems = "center";
|
|
722
|
-
});
|
|
723
|
-
|
|
724
862
|
const miniProgramLabel = node.querySelector("[data-export-mini-label]");
|
|
725
863
|
if (miniProgramLabel) miniProgramLabel.textContent = exportConfig.miniProgramLabel;
|
|
726
864
|
const transferDescription = node.querySelector(".transfer-heading p");
|
|
727
865
|
if (transferDescription) transferDescription.remove();
|
|
728
|
-
|
|
729
|
-
if (transferNote) transferNote.remove();
|
|
866
|
+
node.querySelectorAll(".transfer-note").forEach((note) => note.remove());
|
|
730
867
|
|
|
731
868
|
node.querySelectorAll(".paper").forEach((paper) => {
|
|
732
869
|
paper.style.boxShadow = "none";
|
|
733
870
|
});
|
|
734
871
|
}
|
|
735
872
|
|
|
873
|
+
function normalizeExportTextLayout(node) {
|
|
874
|
+
const selectors = ".meta > div, .receipt-row > span, .receipt-row > strong, .salary-line > span, .salary-line > strong";
|
|
875
|
+
node.querySelectorAll(selectors).forEach((item) => {
|
|
876
|
+
["width", "height", "inline-size", "block-size"].forEach((property) => {
|
|
877
|
+
item.style.removeProperty(property);
|
|
878
|
+
});
|
|
879
|
+
});
|
|
880
|
+
node.querySelectorAll(".receipt-row > strong, .salary-line > strong").forEach((value) => {
|
|
881
|
+
value.style.setProperty("white-space", "nowrap");
|
|
882
|
+
value.style.setProperty("flex-shrink", "0");
|
|
883
|
+
});
|
|
884
|
+
}
|
|
885
|
+
|
|
736
886
|
function createExportNode(source, paperColor) {
|
|
737
887
|
const clone = source.cloneNode(true);
|
|
738
888
|
sanitizeExportNode(clone);
|
|
@@ -785,6 +935,7 @@ export function renderHtml({ record, dataQrDataUrl = null, dataQrDataUrls = null
|
|
|
785
935
|
style: { background: paperColor },
|
|
786
936
|
onclone(clone) {
|
|
787
937
|
sanitizeExportNode(clone);
|
|
938
|
+
normalizeExportTextLayout(clone);
|
|
788
939
|
clone.style.position = "static";
|
|
789
940
|
clone.style.left = "auto";
|
|
790
941
|
clone.style.top = "auto";
|