codex-work-receipt 0.7.4 → 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 +14 -0
- 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/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 +343 -220
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,
|
|
@@ -13,7 +14,14 @@ import {
|
|
|
13
14
|
import { getHtmlStarPrompt } from "../core/open-source.mjs";
|
|
14
15
|
|
|
15
16
|
const require = createRequire(import.meta.url);
|
|
16
|
-
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";
|
|
17
25
|
|
|
18
26
|
function inlineScript(value) {
|
|
19
27
|
return String(value).replaceAll("</script", "<\\/script");
|
|
@@ -54,17 +62,11 @@ function formatDateKey(value, locale) {
|
|
|
54
62
|
return locale === "en" ? `${match[2]}/${match[3]}/${match[1]}` : `${match[1]}/${match[2]}/${match[3]}`;
|
|
55
63
|
}
|
|
56
64
|
|
|
57
|
-
function
|
|
58
|
-
return Object.entries(values).reduce(
|
|
59
|
-
(result, [key, value]) => result.replaceAll(`{${key}}`, String(value)),
|
|
60
|
-
String(template),
|
|
61
|
-
);
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
export function renderHtml({ record, dataQrDataUrl = null, dataQrDataUrls = null, miniProgramCodeDataUrl = null }) {
|
|
65
|
+
export function renderHtml({ record, dataQrDataUrl = null, miniProgramCodeDataUrl = null, transferFile = null }) {
|
|
65
66
|
const locale = record.locale || DEFAULT_LOCALE;
|
|
66
67
|
const copy = getReceiptCopy(locale);
|
|
67
68
|
const githubStarPrompt = getHtmlStarPrompt(locale);
|
|
69
|
+
const changelogUrl = `${githubStarPrompt.url}/blob/main/CHANGELOG.md`;
|
|
68
70
|
const startAt = new Date(record.period.start_at);
|
|
69
71
|
const endAt = new Date(record.period.end_at);
|
|
70
72
|
const timezone = record.period.timezone;
|
|
@@ -92,13 +94,25 @@ export function renderHtml({ record, dataQrDataUrl = null, dataQrDataUrls = null
|
|
|
92
94
|
minimumFractionDigits: 1,
|
|
93
95
|
maximumFractionDigits: 1,
|
|
94
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)}`}`;
|
|
95
98
|
const exportConfig = JSON.stringify({
|
|
96
99
|
idleLabel: copy.exportImage,
|
|
97
100
|
busyLabel: copy.exportingImage,
|
|
98
101
|
successLabel: copy.exportSuccess,
|
|
99
102
|
errorLabel: copy.exportError,
|
|
100
103
|
miniProgramLabel: copy.exportMiniProgramLabel,
|
|
101
|
-
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,
|
|
102
116
|
}).replaceAll("<", "\\u003c");
|
|
103
117
|
const rows = [
|
|
104
118
|
receiptRow(copy.rows.scope, scopeLabel),
|
|
@@ -116,61 +130,45 @@ export function renderHtml({ record, dataQrDataUrl = null, dataQrDataUrls = null
|
|
|
116
130
|
const miniProgramVisual = miniProgramCodeDataUrl
|
|
117
131
|
? `<img src="${miniProgramCodeDataUrl}" alt="${escapeHtml(copy.miniProgramAlt)}">`
|
|
118
132
|
: `<div class="mini-placeholder" role="img" aria-label="${escapeHtml(copy.placeholderAria)}"><span>${escapeHtml(copy.placeholderLabel)}</span><strong>${escapeHtml(copy.placeholderValue)}</strong></div>`;
|
|
119
|
-
const
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
: [];
|
|
124
|
-
const dataQrItems = qrUrls.map((url, index) => `
|
|
125
|
-
<div class="qr-item" data-data-qr-index="${index}">
|
|
126
|
-
<div class="qr-frame"><img src="${url}" alt="${escapeHtml(copy.dataQrAlt)} ${index + 1}/${qrUrls.length}"></div>
|
|
127
|
-
<strong>${escapeHtml(copy.importData)}${qrUrls.length > 1 ? ` ${index + 1}/${qrUrls.length}` : ""}</strong>
|
|
128
|
-
<span>${escapeHtml(copy.importDataHint)}</span>
|
|
129
|
-
</div>`).join("");
|
|
130
|
-
const isMultipart = qrUrls.length > 1;
|
|
131
|
-
const multipartSetupSeconds = 10;
|
|
132
|
-
const multipartConfig = JSON.stringify({
|
|
133
|
-
enabled: isMultipart,
|
|
134
|
-
setupSeconds: multipartSetupSeconds,
|
|
135
|
-
frameMs: 4000,
|
|
136
|
-
blankMs: 240,
|
|
137
|
-
setupHint: copy.multipartOpenHint,
|
|
138
|
-
partLabel: copy.multipartPartLabel,
|
|
139
|
-
}).replaceAll("<", "\\u003c");
|
|
140
|
-
const transferVisual = isMultipart
|
|
133
|
+
const fileImportSteps = copy.fileImportSteps
|
|
134
|
+
.map((step) => `<li>${escapeHtml(step)}</li>`)
|
|
135
|
+
.join("");
|
|
136
|
+
const dataQrPanel = dataQrDataUrl
|
|
141
137
|
? `
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
<
|
|
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>
|
|
145
157
|
<div class="qr-frame qr-frame--large">${miniProgramVisual}</div>
|
|
146
|
-
<strong>${escapeHtml(copy.
|
|
147
|
-
<span
|
|
158
|
+
<strong data-export-mini-label>${escapeHtml(copy.openMiniProgram)}</strong>
|
|
159
|
+
<span>${escapeHtml(copy.openMiniProgramHint)}</span>
|
|
148
160
|
</div>
|
|
161
|
+
${dataQrPanel}
|
|
149
162
|
</div>
|
|
150
|
-
<div class="
|
|
151
|
-
<
|
|
152
|
-
<
|
|
153
|
-
<
|
|
154
|
-
<span class="
|
|
155
|
-
<
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
<div class="qr-grid qr-grid--export-only" hidden>
|
|
159
|
-
<div class="qr-item">
|
|
160
|
-
<div class="qr-frame">${miniProgramVisual}</div>
|
|
161
|
-
<strong data-export-mini-label>${escapeHtml(copy.openMiniProgram)}</strong>
|
|
162
|
-
<span>${escapeHtml(copy.openMiniProgramHint)}</span>
|
|
163
|
-
</div>
|
|
164
|
-
${dataQrItems}
|
|
165
|
-
</div>`
|
|
166
|
-
: `
|
|
167
|
-
<div class="qr-grid qr-grid--single">
|
|
168
|
-
<div class="qr-item">
|
|
169
|
-
<div class="qr-frame">${miniProgramVisual}</div>
|
|
170
|
-
<strong data-export-mini-label>${escapeHtml(copy.openMiniProgram)}</strong>
|
|
171
|
-
<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}
|
|
172
171
|
</div>
|
|
173
|
-
${dataQrItems}
|
|
174
172
|
</div>`;
|
|
175
173
|
|
|
176
174
|
return `<!doctype html>
|
|
@@ -228,82 +226,150 @@ export function renderHtml({ record, dataQrDataUrl = null, dataQrDataUrls = null
|
|
|
228
226
|
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
|
|
229
227
|
transition: background .2s ease, color .2s ease;
|
|
230
228
|
}
|
|
231
|
-
.
|
|
232
|
-
|
|
229
|
+
.layout {
|
|
230
|
+
display: grid;
|
|
231
|
+
grid-template-columns: minmax(0, 540px) 180px;
|
|
232
|
+
gap: 240px;
|
|
233
|
+
max-width: 1020px;
|
|
233
234
|
margin: 0 auto;
|
|
234
235
|
padding: 30px 18px 54px;
|
|
236
|
+
align-items: start;
|
|
235
237
|
}
|
|
236
|
-
.
|
|
237
|
-
display: grid;
|
|
238
|
+
.page {
|
|
238
239
|
width: 100%;
|
|
239
|
-
|
|
240
|
+
padding: 0;
|
|
241
|
+
}
|
|
242
|
+
.sidebar {
|
|
243
|
+
position: sticky;
|
|
244
|
+
top: 30px;
|
|
245
|
+
display: flex;
|
|
246
|
+
flex-direction: column;
|
|
240
247
|
gap: 12px;
|
|
241
|
-
margin-bottom: 22px;
|
|
242
248
|
}
|
|
243
|
-
.
|
|
244
|
-
|
|
245
|
-
|
|
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;
|
|
246
258
|
align-items: center;
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
background: color-mix(in srgb, var(--paper) 78%, transparent);
|
|
252
|
-
color: var(--ink);
|
|
259
|
+
gap: 5px;
|
|
260
|
+
margin: 0 0 6px;
|
|
261
|
+
color: #e0e0e0;
|
|
262
|
+
font-family: system-ui, -apple-system, BlinkMacSystemFont, "PingFang SC", sans-serif;
|
|
253
263
|
font-size: 11px;
|
|
254
264
|
font-weight: 700;
|
|
255
|
-
|
|
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;
|
|
256
283
|
text-decoration: none;
|
|
257
|
-
transition: background .15s ease, color .15s ease, transform .15s ease;
|
|
258
284
|
}
|
|
259
|
-
.
|
|
260
|
-
.
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
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;
|
|
264
321
|
}
|
|
265
|
-
.github-star-link:active { transform: translateY(0); }
|
|
266
322
|
.theme-switcher {
|
|
267
323
|
display: flex;
|
|
268
|
-
flex-wrap:
|
|
269
|
-
justify-content: center;
|
|
270
|
-
gap: 8px;
|
|
324
|
+
flex-wrap: nowrap;
|
|
271
325
|
}
|
|
272
326
|
.theme-button {
|
|
273
327
|
appearance: none;
|
|
274
328
|
border: 1px solid color-mix(in srgb, var(--ink) 28%, transparent);
|
|
275
|
-
border-radius:
|
|
329
|
+
border-radius: 0;
|
|
276
330
|
background: color-mix(in srgb, var(--paper) 74%, transparent);
|
|
277
331
|
color: var(--ink);
|
|
278
332
|
cursor: pointer;
|
|
279
333
|
font: inherit;
|
|
280
334
|
font-size: 12px;
|
|
281
|
-
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;
|
|
282
345
|
}
|
|
283
346
|
.theme-button[aria-pressed="true"] {
|
|
347
|
+
position: relative;
|
|
348
|
+
z-index: 1;
|
|
349
|
+
border-color: var(--ink);
|
|
284
350
|
background: var(--ink);
|
|
285
351
|
color: var(--paper);
|
|
286
352
|
}
|
|
287
353
|
.export-actions {
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
354
|
+
position: relative;
|
|
355
|
+
display: flex;
|
|
356
|
+
align-items: center;
|
|
357
|
+
gap: 8px;
|
|
291
358
|
}
|
|
292
359
|
.export-button {
|
|
293
360
|
appearance: none;
|
|
294
|
-
|
|
295
|
-
border:
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
color: var(--paper);
|
|
361
|
+
border: 1px solid #333;
|
|
362
|
+
border-radius: 6px;
|
|
363
|
+
background: #222;
|
|
364
|
+
color: #fff;
|
|
299
365
|
cursor: pointer;
|
|
300
366
|
font: inherit;
|
|
301
|
-
font-size:
|
|
302
|
-
font-weight:
|
|
303
|
-
padding:
|
|
367
|
+
font-size: 12px;
|
|
368
|
+
font-weight: 600;
|
|
369
|
+
padding: 7px 14px;
|
|
304
370
|
transition: opacity .15s ease, transform .15s ease;
|
|
305
371
|
}
|
|
306
|
-
.export-button:hover { transform: translateY(-1px); }
|
|
372
|
+
.export-button:hover { opacity: .85; transform: translateY(-1px); }
|
|
307
373
|
.export-button:disabled { cursor: wait; opacity: .62; transform: none; }
|
|
308
374
|
.export-status {
|
|
309
375
|
min-height: 16px;
|
|
@@ -449,13 +515,16 @@ export function renderHtml({ record, dataQrDataUrl = null, dataQrDataUrls = null
|
|
|
449
515
|
.transfer-heading { text-align: center; }
|
|
450
516
|
.transfer-heading h2 { margin: 0; font-size: 18px; letter-spacing: .08em; }
|
|
451
517
|
.transfer-heading p { margin: 7px 0 0; color: var(--muted); font-size: 11px; }
|
|
452
|
-
.
|
|
518
|
+
.transfer-layout {
|
|
453
519
|
display: grid;
|
|
454
|
-
grid-template-columns: 1fr
|
|
455
|
-
|
|
520
|
+
grid-template-columns: minmax(0, 1fr) minmax(0, 1.35fr);
|
|
521
|
+
align-items: start;
|
|
522
|
+
gap: 22px;
|
|
456
523
|
margin-top: 20px;
|
|
457
524
|
}
|
|
525
|
+
.qr-switcher { min-width: 0; }
|
|
458
526
|
.qr-item { text-align: center; }
|
|
527
|
+
.qr-panel[hidden] { display: none; }
|
|
459
528
|
.qr-frame {
|
|
460
529
|
display: grid;
|
|
461
530
|
place-items: center;
|
|
@@ -469,29 +538,67 @@ export function renderHtml({ record, dataQrDataUrl = null, dataQrDataUrls = null
|
|
|
469
538
|
.qr-frame img { display: block; width: 100%; height: 100%; object-fit: contain; }
|
|
470
539
|
.qr-item strong { display: block; font-size: 12px; }
|
|
471
540
|
.qr-item span { display: block; margin-top: 4px; color: var(--muted); font-size: 10px; line-height: 1.45; }
|
|
472
|
-
.qr-
|
|
473
|
-
.
|
|
474
|
-
.multipart-panel {
|
|
475
|
-
display: grid;
|
|
476
|
-
place-items: center;
|
|
477
|
-
min-height: 310px;
|
|
541
|
+
.qr-frame--large { width: min(100%, 210px); }
|
|
542
|
+
.file-import-card {
|
|
478
543
|
padding: 18px;
|
|
479
544
|
border: 1px solid var(--line);
|
|
480
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);
|
|
481
594
|
text-align: center;
|
|
482
595
|
}
|
|
483
|
-
.
|
|
484
|
-
.
|
|
485
|
-
.
|
|
486
|
-
.
|
|
487
|
-
.
|
|
488
|
-
|
|
489
|
-
.multipart-stage__title { margin-top: 4px; font-size: 14px; }
|
|
490
|
-
.multipart-stage__part { margin-top: 8px; font-size: 12px; font-weight: 800; letter-spacing: .06em; }
|
|
491
|
-
.multipart-stage__hint { max-width: 360px; margin-top: 6px; color: var(--muted); font-size: 10px; line-height: 1.5; }
|
|
492
|
-
#multipart-active-qr[aria-busy="true"] { visibility: hidden; }
|
|
493
|
-
.multipart-secondary {
|
|
494
|
-
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;
|
|
495
602
|
padding: 7px 12px;
|
|
496
603
|
border: 1px solid var(--line);
|
|
497
604
|
border-radius: 999px;
|
|
@@ -501,7 +608,7 @@ export function renderHtml({ record, dataQrDataUrl = null, dataQrDataUrls = null
|
|
|
501
608
|
font-size: 10px;
|
|
502
609
|
cursor: pointer;
|
|
503
610
|
}
|
|
504
|
-
.
|
|
611
|
+
.secondary-button:hover { background: color-mix(in srgb, var(--ink) 7%, transparent); }
|
|
505
612
|
.mini-placeholder {
|
|
506
613
|
display: grid;
|
|
507
614
|
place-content: center;
|
|
@@ -539,20 +646,43 @@ export function renderHtml({ record, dataQrDataUrl = null, dataQrDataUrls = null
|
|
|
539
646
|
line-height: 1.6;
|
|
540
647
|
text-align: center;
|
|
541
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
|
+
}
|
|
542
667
|
@media (max-width: 420px) {
|
|
543
|
-
.
|
|
544
|
-
.
|
|
668
|
+
.layout { padding-inline: 10px; }
|
|
669
|
+
.toolbar {
|
|
670
|
+
flex-direction: column;
|
|
671
|
+
gap: 10px;
|
|
672
|
+
}
|
|
545
673
|
.receipt { padding-inline: 20px; }
|
|
546
674
|
.meta { grid-template-columns: 1fr; }
|
|
547
675
|
.meta div:nth-child(even) { text-align: left; }
|
|
548
|
-
.
|
|
676
|
+
.transfer-layout { grid-template-columns: 1fr; }
|
|
677
|
+
.sidebar { flex-direction: column; }
|
|
678
|
+
.sidebar-card { flex-basis: auto; }
|
|
549
679
|
}
|
|
550
680
|
</style>
|
|
551
681
|
</head>
|
|
552
682
|
<body>
|
|
683
|
+
<div class="layout">
|
|
553
684
|
<main class="page">
|
|
554
685
|
<div class="toolbar">
|
|
555
|
-
<a class="github-star-link" href="${escapeHtml(githubStarPrompt.url)}" target="_blank" rel="noopener noreferrer">${escapeHtml(githubStarPrompt.label)}</a>
|
|
556
686
|
<nav class="theme-switcher" aria-label="${escapeHtml(copy.themeAria)}">
|
|
557
687
|
<button class="theme-button" type="button" data-theme-value="classic">${escapeHtml(copy.themes.classic)}</button>
|
|
558
688
|
<button class="theme-button" type="button" data-theme-value="diner">${escapeHtml(copy.themes.diner)}</button>
|
|
@@ -615,10 +745,35 @@ export function renderHtml({ record, dataQrDataUrl = null, dataQrDataUrls = null
|
|
|
615
745
|
|
|
616
746
|
<p class="privacy">${escapeHtml(copy.privacy)}</p>
|
|
617
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>
|
|
618
773
|
<script>${inlineScript(DOM_TO_IMAGE_SOURCE)}</script>
|
|
619
774
|
<script>
|
|
620
775
|
const exportConfig = ${exportConfig};
|
|
621
|
-
const
|
|
776
|
+
const transferConfig = ${transferConfig};
|
|
622
777
|
const themes = new Set(["classic", "diner", "payroll"]);
|
|
623
778
|
const buttons = [...document.querySelectorAll("[data-theme-value]")];
|
|
624
779
|
function applyTheme(theme) {
|
|
@@ -632,86 +787,44 @@ export function renderHtml({ record, dataQrDataUrl = null, dataQrDataUrls = null
|
|
|
632
787
|
try { savedTheme = localStorage.getItem("codex-work-receipt-theme"); } catch {}
|
|
633
788
|
applyTheme(savedTheme || document.documentElement.dataset.theme);
|
|
634
789
|
|
|
635
|
-
const
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
rotationTimer = null;
|
|
657
|
-
switchTimer = null;
|
|
658
|
-
}
|
|
659
|
-
|
|
660
|
-
function setupHintText(seconds) {
|
|
661
|
-
return String(multipartConfig.setupHint).replaceAll("{seconds}", String(seconds));
|
|
662
|
-
}
|
|
663
|
-
|
|
664
|
-
function partLabelText(index) {
|
|
665
|
-
return String(multipartConfig.partLabel)
|
|
666
|
-
.replaceAll("{current}", String(index + 1))
|
|
667
|
-
.replaceAll("{total}", String(urls.length));
|
|
668
|
-
}
|
|
669
|
-
|
|
670
|
-
function showPart(index, immediate = false) {
|
|
671
|
-
if (!urls.length) return;
|
|
672
|
-
activeIndex = (index + urls.length) % urls.length;
|
|
673
|
-
const applyPart = () => {
|
|
674
|
-
activeQr.src = urls[activeIndex];
|
|
675
|
-
activeQr.alt = partLabelText(activeIndex);
|
|
676
|
-
activeQr.setAttribute("aria-busy", "false");
|
|
677
|
-
partLabel.textContent = partLabelText(activeIndex);
|
|
678
|
-
switchTimer = null;
|
|
679
|
-
};
|
|
680
|
-
if (immediate) {
|
|
681
|
-
applyPart();
|
|
682
|
-
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";
|
|
683
811
|
}
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
}
|
|
687
|
-
|
|
688
|
-
function startMultipartTransfer() {
|
|
689
|
-
stopMultipartTimers();
|
|
690
|
-
setup.hidden = true;
|
|
691
|
-
stage.hidden = false;
|
|
692
|
-
showPart(0, true);
|
|
693
|
-
rotationTimer = setInterval(() => showPart(activeIndex + 1), multipartConfig.frameMs);
|
|
694
|
-
}
|
|
695
|
-
|
|
696
|
-
function showMultipartSetup() {
|
|
697
|
-
stopMultipartTimers();
|
|
698
|
-
stage.hidden = true;
|
|
699
|
-
setup.hidden = false;
|
|
700
|
-
let remaining = multipartConfig.setupSeconds;
|
|
701
|
-
setupHint.textContent = setupHintText(remaining);
|
|
702
|
-
setupTimer = setInterval(() => {
|
|
703
|
-
remaining -= 1;
|
|
704
|
-
if (remaining <= 0) {
|
|
705
|
-
startMultipartTransfer();
|
|
706
|
-
return;
|
|
707
|
-
}
|
|
708
|
-
setupHint.textContent = setupHintText(remaining);
|
|
709
|
-
}, 1000);
|
|
710
|
-
}
|
|
812
|
+
});
|
|
813
|
+
}
|
|
711
814
|
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
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
|
+
});
|
|
715
828
|
}
|
|
716
829
|
|
|
717
830
|
const exportButton = document.getElementById("save-receipt-image");
|
|
@@ -734,33 +847,42 @@ export function renderHtml({ record, dataQrDataUrl = null, dataQrDataUrls = null
|
|
|
734
847
|
}
|
|
735
848
|
|
|
736
849
|
function sanitizeExportNode(node) {
|
|
737
|
-
const
|
|
738
|
-
if (
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
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";
|
|
744
860
|
}
|
|
745
861
|
|
|
746
|
-
node.querySelectorAll("[data-data-qr-index]").forEach((item) => item.remove());
|
|
747
|
-
node.querySelectorAll(".qr-grid").forEach((grid) => {
|
|
748
|
-
grid.style.gridTemplateColumns = "minmax(0, 1fr)";
|
|
749
|
-
grid.style.justifyItems = "center";
|
|
750
|
-
});
|
|
751
|
-
|
|
752
862
|
const miniProgramLabel = node.querySelector("[data-export-mini-label]");
|
|
753
863
|
if (miniProgramLabel) miniProgramLabel.textContent = exportConfig.miniProgramLabel;
|
|
754
864
|
const transferDescription = node.querySelector(".transfer-heading p");
|
|
755
865
|
if (transferDescription) transferDescription.remove();
|
|
756
|
-
|
|
757
|
-
if (transferNote) transferNote.remove();
|
|
866
|
+
node.querySelectorAll(".transfer-note").forEach((note) => note.remove());
|
|
758
867
|
|
|
759
868
|
node.querySelectorAll(".paper").forEach((paper) => {
|
|
760
869
|
paper.style.boxShadow = "none";
|
|
761
870
|
});
|
|
762
871
|
}
|
|
763
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
|
+
|
|
764
886
|
function createExportNode(source, paperColor) {
|
|
765
887
|
const clone = source.cloneNode(true);
|
|
766
888
|
sanitizeExportNode(clone);
|
|
@@ -813,6 +935,7 @@ export function renderHtml({ record, dataQrDataUrl = null, dataQrDataUrls = null
|
|
|
813
935
|
style: { background: paperColor },
|
|
814
936
|
onclone(clone) {
|
|
815
937
|
sanitizeExportNode(clone);
|
|
938
|
+
normalizeExportTextLayout(clone);
|
|
816
939
|
clone.style.position = "static";
|
|
817
940
|
clone.style.left = "auto";
|
|
818
941
|
clone.style.top = "auto";
|