@nice2dev/ui-printing 1.1.7 → 1.1.10
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/dist/index.cjs +2 -2
- package/dist/index.mjs +1900 -1734
- package/dist/ui-printing.css +1 -1
- package/package.json +3 -7
- package/src/print-button/NicePrintButton.tsx +39 -17
- package/src/print-button/PrintButton.module.css +9 -9
- package/src/print-preview/NicePrintPreview.tsx +10 -9
- package/src/print-preview/PrintPreview.module.css +7 -7
- package/src/print-queue/NicePrintQueue.tsx +76 -42
- package/src/print-queue/PrintQueue.module.css +9 -9
- package/src/template-browser/NiceTemplateBrowser.tsx +39 -18
- package/src/template-browser/TemplateBrowser.module.css +17 -17
- package/src/template-editor/NiceTemplateEditor.tsx +306 -179
- package/src/template-editor/TemplateEditor.module.css +90 -83
- package/src/types/printingTypes.ts +1 -1
- package/dist/index.d.ts.map +0 -1
- package/dist/print-button/NicePrintButton.d.ts.map +0 -1
- package/dist/print-button/index.d.ts.map +0 -1
- package/dist/print-preview/NicePrintPreview.d.ts.map +0 -1
- package/dist/print-preview/index.d.ts.map +0 -1
- package/dist/print-queue/NicePrintQueue.d.ts.map +0 -1
- package/dist/print-queue/index.d.ts.map +0 -1
- package/dist/style.css +0 -1
- package/dist/template-browser/NiceTemplateBrowser.d.ts.map +0 -1
- package/dist/template-browser/index.d.ts.map +0 -1
- package/dist/template-editor/NiceTemplateEditor.d.ts.map +0 -1
- package/dist/template-editor/index.d.ts.map +0 -1
- package/dist/types/printingTypes.d.ts.map +0 -1
- package/src/template-editor/NiceTemplateEditor.tsx.first +0 -1011
- package/src/template-editor/NiceTemplateEditor.tsx.tmp +0 -392
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { NiceIcon, useNiceTranslation } from '@nice2dev/ui';
|
|
2
|
+
import type { NiceTranslateFn } from '@nice2dev/ui';
|
|
1
3
|
import React, { useState, useCallback } from 'react';
|
|
2
4
|
|
|
3
5
|
import type { PrintJob, PrinterInfo, NicePrintQueueProps } from '../types/printingTypes';
|
|
@@ -5,32 +7,46 @@ import type { PrintJob, PrinterInfo, NicePrintQueueProps } from '../types/printi
|
|
|
5
7
|
import styles from './PrintQueue.module.css';
|
|
6
8
|
|
|
7
9
|
// ── Helpers ───────────────────────────────────────────────────────────────────
|
|
8
|
-
const
|
|
9
|
-
queued: '
|
|
10
|
-
printing: '
|
|
11
|
-
done: '
|
|
12
|
-
error: '
|
|
13
|
-
paused: '
|
|
14
|
-
cancelled: '
|
|
10
|
+
const STATUS_DEFAULTS: Record<string, string> = {
|
|
11
|
+
queued: 'Queued',
|
|
12
|
+
printing: 'Printing',
|
|
13
|
+
done: 'Printed',
|
|
14
|
+
error: 'Error',
|
|
15
|
+
paused: 'Paused',
|
|
16
|
+
cancelled: 'Cancelled',
|
|
15
17
|
};
|
|
16
18
|
|
|
17
|
-
|
|
19
|
+
const statusLabel = (t: NiceTranslateFn, status: string): string =>
|
|
20
|
+
STATUS_DEFAULTS[status] ? t(`printing.status.${status}`, STATUS_DEFAULTS[status]) : status;
|
|
21
|
+
|
|
22
|
+
function formatTime(lang: string, iso: string): string {
|
|
18
23
|
try {
|
|
19
24
|
const d = new Date(iso);
|
|
20
|
-
return d.toLocaleTimeString(
|
|
25
|
+
return d.toLocaleTimeString(lang, { hour: '2-digit', minute: '2-digit' });
|
|
21
26
|
} catch {
|
|
22
27
|
return iso;
|
|
23
28
|
}
|
|
24
29
|
}
|
|
25
30
|
|
|
31
|
+
/**
|
|
32
|
+
* How long a job has been running. Rolls over into hours and days — without
|
|
33
|
+
* that, a job stuck since yesterday rendered as `1680m 12s`, which reads as a
|
|
34
|
+
* bug rather than as a stuck job.
|
|
35
|
+
*/
|
|
26
36
|
function formatDuration(startIso: string): string {
|
|
27
|
-
const
|
|
28
|
-
const s = Math.floor(diff / 1000);
|
|
37
|
+
const s = Math.floor((Date.now() - new Date(startIso).getTime()) / 1000);
|
|
29
38
|
if (s < 60) {
|
|
30
39
|
return `${s}s`;
|
|
31
40
|
}
|
|
32
41
|
const m = Math.floor(s / 60);
|
|
33
|
-
|
|
42
|
+
if (m < 60) {
|
|
43
|
+
return `${m}m ${s % 60}s`;
|
|
44
|
+
}
|
|
45
|
+
const h = Math.floor(m / 60);
|
|
46
|
+
if (h < 24) {
|
|
47
|
+
return `${h}h ${m % 60}m`;
|
|
48
|
+
}
|
|
49
|
+
return `${Math.floor(h / 24)}d ${h % 24}h`;
|
|
34
50
|
}
|
|
35
51
|
|
|
36
52
|
// ── Stats bar ─────────────────────────────────────────────────────────────────
|
|
@@ -38,6 +54,7 @@ interface StatsBarProps {
|
|
|
38
54
|
jobs: PrintJob[];
|
|
39
55
|
}
|
|
40
56
|
const StatsBar: React.FC<StatsBarProps> = ({ jobs }) => {
|
|
57
|
+
const { t } = useNiceTranslation();
|
|
41
58
|
const printing = jobs.filter((j) => j.status === 'printing').length;
|
|
42
59
|
const queued = jobs.filter((j) => j.status === 'queued').length;
|
|
43
60
|
const done = jobs.filter((j) => j.status === 'done').length;
|
|
@@ -46,19 +63,20 @@ const StatsBar: React.FC<StatsBarProps> = ({ jobs }) => {
|
|
|
46
63
|
<div className={styles.statsBar}>
|
|
47
64
|
<span className={`${styles.stat} ${styles.statPrinting}`}>
|
|
48
65
|
<span className={styles.dot} />
|
|
49
|
-
|
|
66
|
+
{t('printing.stats.active', 'Printing')}: <strong>{printing}</strong>
|
|
50
67
|
</span>
|
|
51
68
|
<span className={`${styles.stat} ${styles.statQueued}`}>
|
|
52
|
-
<span className={styles.dot} />
|
|
69
|
+
<span className={styles.dot} />
|
|
70
|
+
{t('printing.stats.queued', 'In queue')}: <strong>{queued}</strong>
|
|
53
71
|
</span>
|
|
54
72
|
<span className={`${styles.stat} ${styles.statDone}`}>
|
|
55
73
|
<span className={styles.dot} />
|
|
56
|
-
|
|
74
|
+
{t('printing.stats.done', 'Done')}: <strong>{done}</strong>
|
|
57
75
|
</span>
|
|
58
76
|
{errored > 0 && (
|
|
59
77
|
<span className={`${styles.stat} ${styles.statError}`}>
|
|
60
78
|
<span className={styles.dot} />
|
|
61
|
-
|
|
79
|
+
{t('printing.stats.errors', 'Errors')}: <strong>{errored}</strong>
|
|
62
80
|
</span>
|
|
63
81
|
)}
|
|
64
82
|
</div>
|
|
@@ -90,6 +108,7 @@ const JobRow: React.FC<JobRowProps> = ({
|
|
|
90
108
|
onPrint,
|
|
91
109
|
onReorder,
|
|
92
110
|
}) => {
|
|
111
|
+
const { t, lang } = useNiceTranslation();
|
|
93
112
|
const printer = printers.find((p) => p.id === job.options.printerId);
|
|
94
113
|
const canMoveUp = index > 0 && job.status === 'queued';
|
|
95
114
|
const canMoveDown = index < total - 1 && job.status === 'queued';
|
|
@@ -108,7 +127,7 @@ const JobRow: React.FC<JobRowProps> = ({
|
|
|
108
127
|
|
|
109
128
|
{/* Status badge */}
|
|
110
129
|
<span className={`${styles.statusBadge} ${styles[`badge-${job.status}`]}`}>
|
|
111
|
-
{
|
|
130
|
+
{statusLabel(t, job.status)}
|
|
112
131
|
</span>
|
|
113
132
|
|
|
114
133
|
{/* Job info */}
|
|
@@ -116,9 +135,15 @@ const JobRow: React.FC<JobRowProps> = ({
|
|
|
116
135
|
<div className={styles.jobName}>{job.templateName ?? job.templateId}</div>
|
|
117
136
|
<div className={styles.jobMeta}>
|
|
118
137
|
{job.entityLabel && <span>{job.entityLabel}</span>}
|
|
119
|
-
{printer &&
|
|
138
|
+
{printer && (
|
|
139
|
+
<span>
|
|
140
|
+
<NiceIcon name="printer" size={12} /> {printer.name}
|
|
141
|
+
</span>
|
|
142
|
+
)}
|
|
120
143
|
<span>×{job.options.copies}</span>
|
|
121
|
-
{job.createdAt &&
|
|
144
|
+
{job.createdAt && (
|
|
145
|
+
<span className={styles.jobTime}>{formatTime(lang, job.createdAt)}</span>
|
|
146
|
+
)}
|
|
122
147
|
{job.startedAt && job.status !== 'done' && (
|
|
123
148
|
<span className={styles.jobDuration}>{formatDuration(job.startedAt)}</span>
|
|
124
149
|
)}
|
|
@@ -140,20 +165,22 @@ const JobRow: React.FC<JobRowProps> = ({
|
|
|
140
165
|
<button
|
|
141
166
|
className={styles.copiesBtn}
|
|
142
167
|
disabled={job.options.copies <= 1}
|
|
168
|
+
aria-label={t('printing.copies.decrease', 'One copy fewer')}
|
|
143
169
|
onClick={() =>
|
|
144
170
|
onUpdate(job.id, { options: { ...job.options, copies: job.options.copies - 1 } })
|
|
145
171
|
}
|
|
146
172
|
>
|
|
147
|
-
|
|
173
|
+
<NiceIcon name="minus" size={12} />
|
|
148
174
|
</button>
|
|
149
175
|
<span className={styles.copiesValue}>{job.options.copies}</span>
|
|
150
176
|
<button
|
|
151
177
|
className={styles.copiesBtn}
|
|
178
|
+
aria-label={t('printing.copies.increase', 'One copy more')}
|
|
152
179
|
onClick={() =>
|
|
153
180
|
onUpdate(job.id, { options: { ...job.options, copies: job.options.copies + 1 } })
|
|
154
181
|
}
|
|
155
182
|
>
|
|
156
|
-
|
|
183
|
+
<NiceIcon name="plus" size={12} />
|
|
157
184
|
</button>
|
|
158
185
|
</div>
|
|
159
186
|
|
|
@@ -162,50 +189,54 @@ const JobRow: React.FC<JobRowProps> = ({
|
|
|
162
189
|
{canMoveUp && (
|
|
163
190
|
<button
|
|
164
191
|
className={styles.actionBtn}
|
|
165
|
-
title=
|
|
192
|
+
title={t('printing.action.moveUp', 'Move up')}
|
|
166
193
|
onClick={() => onReorder(index, index - 1)}
|
|
167
194
|
>
|
|
168
|
-
|
|
195
|
+
<NiceIcon name="arrow-up" size={14} />
|
|
169
196
|
</button>
|
|
170
197
|
)}
|
|
171
198
|
{canMoveDown && (
|
|
172
199
|
<button
|
|
173
200
|
className={styles.actionBtn}
|
|
174
|
-
title=
|
|
201
|
+
title={t('printing.action.moveDown', 'Move down')}
|
|
175
202
|
onClick={() => onReorder(index, index + 1)}
|
|
176
203
|
>
|
|
177
|
-
|
|
204
|
+
<NiceIcon name="arrow-down" size={14} />
|
|
178
205
|
</button>
|
|
179
206
|
)}
|
|
180
207
|
{job.status === 'queued' && (
|
|
181
|
-
<button
|
|
182
|
-
|
|
208
|
+
<button
|
|
209
|
+
className={styles.actionBtn}
|
|
210
|
+
title={t('printing.action.printNow', 'Print now')}
|
|
211
|
+
onClick={() => onPrint(job.id)}
|
|
212
|
+
>
|
|
213
|
+
<NiceIcon name="play" size={14} />
|
|
183
214
|
</button>
|
|
184
215
|
)}
|
|
185
216
|
{job.status === 'printing' && (
|
|
186
217
|
<button
|
|
187
218
|
className={styles.actionBtn}
|
|
188
|
-
title=
|
|
219
|
+
title={t('printing.action.pause', 'Pause')}
|
|
189
220
|
onClick={() => onUpdate(job.id, { status: 'paused' })}
|
|
190
221
|
>
|
|
191
|
-
|
|
222
|
+
<NiceIcon name="pause" size={14} />
|
|
192
223
|
</button>
|
|
193
224
|
)}
|
|
194
225
|
{job.status === 'paused' && (
|
|
195
226
|
<button
|
|
196
227
|
className={styles.actionBtn}
|
|
197
|
-
title=
|
|
228
|
+
title={t('printing.action.resume', 'Resume')}
|
|
198
229
|
onClick={() => onUpdate(job.id, { status: 'queued' })}
|
|
199
230
|
>
|
|
200
|
-
|
|
231
|
+
<NiceIcon name="play" size={14} />
|
|
201
232
|
</button>
|
|
202
233
|
)}
|
|
203
234
|
<button
|
|
204
235
|
className={`${styles.actionBtn} ${styles.removeBtn}`}
|
|
205
|
-
title=
|
|
236
|
+
title={t('printing.action.remove', 'Remove')}
|
|
206
237
|
onClick={() => onRemove(job.id)}
|
|
207
238
|
>
|
|
208
|
-
|
|
239
|
+
<NiceIcon name="close" size={14} />
|
|
209
240
|
</button>
|
|
210
241
|
</div>
|
|
211
242
|
</div>
|
|
@@ -227,6 +258,7 @@ export const NicePrintQueue: React.FC<NicePrintQueueProps> = ({
|
|
|
227
258
|
style,
|
|
228
259
|
theme = 'light',
|
|
229
260
|
}) => {
|
|
261
|
+
const { t } = useNiceTranslation();
|
|
230
262
|
const [selectedIds, setSelectedIds] = useState<Set<string>>(new Set());
|
|
231
263
|
const [filter, setFilter] = useState<'all' | 'queued' | 'printing' | 'done' | 'error'>('all');
|
|
232
264
|
|
|
@@ -280,7 +312,7 @@ export const NicePrintQueue: React.FC<NicePrintQueueProps> = ({
|
|
|
280
312
|
className={`${styles.filterTab} ${filter === f ? styles.filterTabActive : ''}`}
|
|
281
313
|
onClick={() => setFilter(f)}
|
|
282
314
|
>
|
|
283
|
-
{f === 'all' ? '
|
|
315
|
+
{f === 'all' ? t('printing.filter.all', 'All') : statusLabel(t, f)}
|
|
284
316
|
<span className={styles.filterCount}>
|
|
285
317
|
{f === 'all' ? jobs.length : jobs.filter((j) => j.status === f).length}
|
|
286
318
|
</span>
|
|
@@ -291,15 +323,17 @@ export const NicePrintQueue: React.FC<NicePrintQueueProps> = ({
|
|
|
291
323
|
{/* Bulk actions */}
|
|
292
324
|
{selectedIds.size > 0 && (
|
|
293
325
|
<div className={styles.bulkActions}>
|
|
294
|
-
<span className={styles.bulkCount}>
|
|
326
|
+
<span className={styles.bulkCount}>
|
|
327
|
+
{selectedIds.size} {t('printing.bulk.selected', 'selected')}
|
|
328
|
+
</span>
|
|
295
329
|
{onBulkPrint && (
|
|
296
330
|
<button className={styles.bulkBtn} onClick={bulkPrint}>
|
|
297
|
-
|
|
331
|
+
<NiceIcon name="printer" size={14} /> {t('printing.bulk.print', 'Print selected')}
|
|
298
332
|
</button>
|
|
299
333
|
)}
|
|
300
334
|
{onBulkRemove && (
|
|
301
335
|
<button className={`${styles.bulkBtn} ${styles.bulkBtnRemove}`} onClick={bulkRemove}>
|
|
302
|
-
|
|
336
|
+
<NiceIcon name="trash" size={14} /> {t('printing.bulk.remove', 'Remove selected')}
|
|
303
337
|
</button>
|
|
304
338
|
)}
|
|
305
339
|
</div>
|
|
@@ -311,8 +345,8 @@ export const NicePrintQueue: React.FC<NicePrintQueueProps> = ({
|
|
|
311
345
|
{filteredJobs.length === 0 ? (
|
|
312
346
|
<div className={styles.emptyState}>
|
|
313
347
|
{filter === 'all'
|
|
314
|
-
? '
|
|
315
|
-
:
|
|
348
|
+
? t('printing.empty.queue', 'No jobs in the queue')
|
|
349
|
+
: `${t('printing.empty.filtered', 'No jobs with status')} "${statusLabel(t, filter)}"`}
|
|
316
350
|
</div>
|
|
317
351
|
) : (
|
|
318
352
|
<>
|
|
@@ -323,9 +357,9 @@ export const NicePrintQueue: React.FC<NicePrintQueueProps> = ({
|
|
|
323
357
|
onChange={selectAll}
|
|
324
358
|
checked={selectedIds.size === filteredJobs.length && filteredJobs.length > 0}
|
|
325
359
|
/>
|
|
326
|
-
<span className={styles.headerLabel}>
|
|
327
|
-
<span className={styles.headerLabel}>
|
|
328
|
-
<span className={styles.headerLabel}>
|
|
360
|
+
<span className={styles.headerLabel}>{t('printing.col.job', 'Job')}</span>
|
|
361
|
+
<span className={styles.headerLabel}>{t('printing.col.copies', 'Copies')}</span>
|
|
362
|
+
<span className={styles.headerLabel}>{t('printing.col.actions', 'Actions')}</span>
|
|
329
363
|
</div>
|
|
330
364
|
{filteredJobs.map((job, _index) => (
|
|
331
365
|
<JobRow
|
|
@@ -10,9 +10,9 @@
|
|
|
10
10
|
color: var(--nice-text, #1e293b);
|
|
11
11
|
}
|
|
12
12
|
.dark {
|
|
13
|
-
background: var(--nice-
|
|
14
|
-
border-color: var(--nice-
|
|
15
|
-
color: var(--nice-
|
|
13
|
+
background: var(--nice-bg-secondary, #1e293b);
|
|
14
|
+
border-color: var(--nice-border-color, #334155);
|
|
15
|
+
color: var(--nice-text-primary, #f1f5f9);
|
|
16
16
|
}
|
|
17
17
|
|
|
18
18
|
/* Stats bar */
|
|
@@ -25,8 +25,8 @@
|
|
|
25
25
|
border-bottom: 1px solid var(--nice-border, #e2e8f0);
|
|
26
26
|
}
|
|
27
27
|
.dark .statsBar {
|
|
28
|
-
background: var(--nice-
|
|
29
|
-
border-color: var(--nice-
|
|
28
|
+
background: var(--nice-bg-primary, #0f172a);
|
|
29
|
+
border-color: var(--nice-border-color, #334155);
|
|
30
30
|
}
|
|
31
31
|
|
|
32
32
|
.stat {
|
|
@@ -64,7 +64,7 @@
|
|
|
64
64
|
flex-wrap: wrap;
|
|
65
65
|
}
|
|
66
66
|
.dark .toolbar {
|
|
67
|
-
border-color: var(--nice-
|
|
67
|
+
border-color: var(--nice-border-color, #334155);
|
|
68
68
|
}
|
|
69
69
|
|
|
70
70
|
.filterTabs {
|
|
@@ -152,7 +152,7 @@
|
|
|
152
152
|
border-bottom: 1px solid var(--nice-bg-secondary, #f1f5f9);
|
|
153
153
|
}
|
|
154
154
|
.dark .listHeader {
|
|
155
|
-
border-color: var(--nice-
|
|
155
|
+
border-color: var(--nice-border-color, #1e293b);
|
|
156
156
|
}
|
|
157
157
|
.headerLabel {
|
|
158
158
|
white-space: nowrap;
|
|
@@ -169,13 +169,13 @@
|
|
|
169
169
|
transition: background 0.1s;
|
|
170
170
|
}
|
|
171
171
|
.dark .jobRow {
|
|
172
|
-
border-color: var(--nice-
|
|
172
|
+
border-color: var(--nice-border-color, #334155);
|
|
173
173
|
}
|
|
174
174
|
.jobRow:hover {
|
|
175
175
|
background: var(--nice-bg-secondary, #f8fafc);
|
|
176
176
|
}
|
|
177
177
|
.dark .jobRow:hover {
|
|
178
|
-
background: var(--nice-
|
|
178
|
+
background: var(--nice-bg-primary, #0f172a);
|
|
179
179
|
}
|
|
180
180
|
.jobRowSelected {
|
|
181
181
|
background: var(--nice-primary-bg, #eff6ff) !important;
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
/* eslint-disable jsx-a11y/click-events-have-key-events -- modal overlay backdrops (Esc-on-parent provides keyboard close), selection rows in .map() accompanied by visible cursor:pointer styling and sibling buttons; full keyboard migration via useAccessibleClick + extracted subcomponents tracked in TODO_1.0.10 A2.3 */
|
|
2
|
+
import { NiceIcon, useNiceTranslation } from '@nice2dev/ui';
|
|
2
3
|
import React, { useState } from 'react';
|
|
3
4
|
|
|
4
5
|
import type {
|
|
@@ -9,7 +10,15 @@ import type {
|
|
|
9
10
|
|
|
10
11
|
import styles from './TemplateBrowser.module.css';
|
|
11
12
|
|
|
12
|
-
|
|
13
|
+
/** Built-in category chips; `key` is what a template's `category` field holds. */
|
|
14
|
+
const CATEGORIES: { key: string; fallback: string }[] = [
|
|
15
|
+
{ key: 'printing.category.invoices', fallback: 'Invoices' },
|
|
16
|
+
{ key: 'printing.category.reports', fallback: 'Reports' },
|
|
17
|
+
{ key: 'printing.category.labels', fallback: 'Labels' },
|
|
18
|
+
{ key: 'printing.category.letters', fallback: 'Letters' },
|
|
19
|
+
{ key: 'printing.category.contracts', fallback: 'Contracts' },
|
|
20
|
+
{ key: 'printing.category.other', fallback: 'Other' },
|
|
21
|
+
];
|
|
13
22
|
|
|
14
23
|
function importTemplateFromJson(file: File): Promise<PrintTemplate> {
|
|
15
24
|
return new Promise((resolve, reject) => {
|
|
@@ -47,6 +56,7 @@ export const NiceTemplateBrowser: React.FC<NiceTemplateBrowserProps> = ({
|
|
|
47
56
|
style,
|
|
48
57
|
theme = 'light',
|
|
49
58
|
}) => {
|
|
59
|
+
const { t } = useNiceTranslation();
|
|
50
60
|
const [search, setSearch] = useState('');
|
|
51
61
|
const [categoryFilter, setCategoryFilter] = useState<string | null>(null);
|
|
52
62
|
const [hoveredId, setHoveredId] = useState<string | null>(null);
|
|
@@ -86,19 +96,23 @@ export const NiceTemplateBrowser: React.FC<NiceTemplateBrowserProps> = ({
|
|
|
86
96
|
<input
|
|
87
97
|
type="search"
|
|
88
98
|
className={styles.searchInput}
|
|
89
|
-
placeholder=
|
|
99
|
+
placeholder={t('printing.searchTemplates', 'Search templates…')}
|
|
90
100
|
value={search}
|
|
91
101
|
onChange={(e) => setSearch(e.target.value)}
|
|
92
102
|
/>
|
|
93
|
-
<button
|
|
94
|
-
|
|
103
|
+
<button
|
|
104
|
+
className={styles.actionBtn}
|
|
105
|
+
onClick={onNew}
|
|
106
|
+
title={t('printing.newTemplate', 'New template')}
|
|
107
|
+
>
|
|
108
|
+
<NiceIcon name="plus" size={14} /> {t('printing.new', 'New')}
|
|
95
109
|
</button>
|
|
96
110
|
<button
|
|
97
111
|
className={styles.actionBtn}
|
|
98
112
|
onClick={() => fileInputRef.current?.click()}
|
|
99
|
-
title=
|
|
113
|
+
title={t('printing.importJson', 'Import JSON')}
|
|
100
114
|
>
|
|
101
|
-
|
|
115
|
+
<NiceIcon name="upload" size={14} /> {t('printing.import', 'Import')}
|
|
102
116
|
</button>
|
|
103
117
|
<input ref={fileInputRef} type="file" accept=".json" hidden onChange={handleFileImport} />
|
|
104
118
|
</div>
|
|
@@ -109,15 +123,15 @@ export const NiceTemplateBrowser: React.FC<NiceTemplateBrowserProps> = ({
|
|
|
109
123
|
className={`${styles.catChip} ${!categoryFilter ? styles.catChipActive : ''}`}
|
|
110
124
|
onClick={() => setCategoryFilter(null)}
|
|
111
125
|
>
|
|
112
|
-
|
|
126
|
+
{t('printing.filter.all', 'All')}
|
|
113
127
|
</button>
|
|
114
128
|
{CATEGORIES.map((cat) => (
|
|
115
129
|
<button
|
|
116
|
-
key={cat}
|
|
117
|
-
className={`${styles.catChip} ${categoryFilter === cat ? styles.catChipActive : ''}`}
|
|
118
|
-
onClick={() => setCategoryFilter((c) => (c === cat ? null : cat))}
|
|
130
|
+
key={cat.key}
|
|
131
|
+
className={`${styles.catChip} ${categoryFilter === cat.fallback ? styles.catChipActive : ''}`}
|
|
132
|
+
onClick={() => setCategoryFilter((c) => (c === cat.fallback ? null : cat.fallback))}
|
|
119
133
|
>
|
|
120
|
-
{cat}
|
|
134
|
+
{t(cat.key, cat.fallback)}
|
|
121
135
|
</button>
|
|
122
136
|
))}
|
|
123
137
|
</div>
|
|
@@ -168,9 +182,9 @@ export const NiceTemplateBrowser: React.FC<NiceTemplateBrowserProps> = ({
|
|
|
168
182
|
onExport?.(tpl);
|
|
169
183
|
exportTemplate(tpl);
|
|
170
184
|
}}
|
|
171
|
-
title=
|
|
185
|
+
title={t('printing.export', 'Export')}
|
|
172
186
|
>
|
|
173
|
-
|
|
187
|
+
<NiceIcon name="download" size={14} />
|
|
174
188
|
</button>
|
|
175
189
|
{onDelete && (
|
|
176
190
|
<button
|
|
@@ -179,9 +193,9 @@ export const NiceTemplateBrowser: React.FC<NiceTemplateBrowserProps> = ({
|
|
|
179
193
|
e.stopPropagation();
|
|
180
194
|
onDelete(tpl.id);
|
|
181
195
|
}}
|
|
182
|
-
title=
|
|
196
|
+
title={t('printing.action.remove', 'Remove')}
|
|
183
197
|
>
|
|
184
|
-
|
|
198
|
+
<NiceIcon name="trash" size={14} />
|
|
185
199
|
</button>
|
|
186
200
|
)}
|
|
187
201
|
</div>
|
|
@@ -194,7 +208,10 @@ export const NiceTemplateBrowser: React.FC<NiceTemplateBrowserProps> = ({
|
|
|
194
208
|
{tpl.description && <div className={styles.cardDesc}>{tpl.description}</div>}
|
|
195
209
|
<div className={styles.cardMeta}>
|
|
196
210
|
<span className={styles.cardSize}>
|
|
197
|
-
{tpl.paperSize}
|
|
211
|
+
{tpl.paperSize}{' '}
|
|
212
|
+
{tpl.orientation === 'landscape'
|
|
213
|
+
? t('printing.orientation.landscape', 'landscape')
|
|
214
|
+
: t('printing.orientation.portrait', 'portrait')}
|
|
198
215
|
</span>
|
|
199
216
|
{tpl.category && <span className={styles.cardCat}>{tpl.category}</span>}
|
|
200
217
|
</div>
|
|
@@ -213,9 +230,13 @@ export const NiceTemplateBrowser: React.FC<NiceTemplateBrowserProps> = ({
|
|
|
213
230
|
|
|
214
231
|
{filtered.length === 0 && (
|
|
215
232
|
<div className={styles.emptyState}>
|
|
216
|
-
<span className={styles.emptyIcon}
|
|
233
|
+
<span className={styles.emptyIcon}>
|
|
234
|
+
<NiceIcon name="file" size={32} />
|
|
235
|
+
</span>
|
|
217
236
|
<span>
|
|
218
|
-
{search
|
|
237
|
+
{search
|
|
238
|
+
? `${t('printing.noMatches', 'No matches for')} "${search}"`
|
|
239
|
+
: t('printing.noTemplates', 'No templates yet.')}
|
|
219
240
|
</span>
|
|
220
241
|
</div>
|
|
221
242
|
)}
|
|
@@ -11,9 +11,9 @@
|
|
|
11
11
|
}
|
|
12
12
|
|
|
13
13
|
.dark {
|
|
14
|
-
background: var(--nice-
|
|
15
|
-
color: var(--nice-
|
|
16
|
-
border-color: var(--nice-
|
|
14
|
+
background: var(--nice-bg-primary, #0f172a);
|
|
15
|
+
color: var(--nice-text-secondary, #e2e8f0);
|
|
16
|
+
border-color: var(--nice-border-color, #1e293b);
|
|
17
17
|
}
|
|
18
18
|
|
|
19
19
|
/* ── Top bar ── */
|
|
@@ -28,8 +28,8 @@
|
|
|
28
28
|
}
|
|
29
29
|
|
|
30
30
|
.dark .topBar {
|
|
31
|
-
background: var(--nice-
|
|
32
|
-
border-bottom-color: var(--nice-
|
|
31
|
+
background: var(--nice-bg-secondary, #1e293b);
|
|
32
|
+
border-bottom-color: var(--nice-border-color, #334155);
|
|
33
33
|
}
|
|
34
34
|
|
|
35
35
|
.searchInput {
|
|
@@ -48,9 +48,9 @@
|
|
|
48
48
|
background: var(--nice-bg, #fff);
|
|
49
49
|
}
|
|
50
50
|
.dark .searchInput {
|
|
51
|
-
background: var(--nice-
|
|
52
|
-
border-color: var(--nice-
|
|
53
|
-
color: var(--nice-
|
|
51
|
+
background: var(--nice-bg-tertiary, #334155);
|
|
52
|
+
border-color: var(--nice-border-color, #475569);
|
|
53
|
+
color: var(--nice-text-secondary, #e2e8f0);
|
|
54
54
|
}
|
|
55
55
|
|
|
56
56
|
.actionBtn {
|
|
@@ -70,9 +70,9 @@
|
|
|
70
70
|
background: var(--nice-bg-secondary, #f1f5f9);
|
|
71
71
|
}
|
|
72
72
|
.dark .actionBtn {
|
|
73
|
-
background: var(--nice-
|
|
74
|
-
border-color: var(--nice-
|
|
75
|
-
color: var(--nice-
|
|
73
|
+
background: var(--nice-bg-tertiary, #334155);
|
|
74
|
+
border-color: var(--nice-border-color, #475569);
|
|
75
|
+
color: var(--nice-text-secondary, #e2e8f0);
|
|
76
76
|
}
|
|
77
77
|
|
|
78
78
|
/* ── Categories ── */
|
|
@@ -87,8 +87,8 @@
|
|
|
87
87
|
}
|
|
88
88
|
|
|
89
89
|
.dark .categoryRow {
|
|
90
|
-
background: var(--nice-
|
|
91
|
-
border-bottom-color: var(--nice-
|
|
90
|
+
background: var(--nice-bg-secondary, #1e293b);
|
|
91
|
+
border-bottom-color: var(--nice-border-color, #334155);
|
|
92
92
|
}
|
|
93
93
|
|
|
94
94
|
.catChip {
|
|
@@ -109,7 +109,7 @@
|
|
|
109
109
|
.catChipActive {
|
|
110
110
|
background: var(--nice-primary-hover, #2563eb);
|
|
111
111
|
border-color: var(--nice-primary-hover, #2563eb);
|
|
112
|
-
color: var(--nice-
|
|
112
|
+
color: var(--nice-text-on-primary, #fff);
|
|
113
113
|
}
|
|
114
114
|
|
|
115
115
|
/* ── Template grid ── */
|
|
@@ -146,8 +146,8 @@
|
|
|
146
146
|
}
|
|
147
147
|
|
|
148
148
|
.dark .card {
|
|
149
|
-
background: var(--nice-
|
|
150
|
-
border-color: var(--nice-
|
|
149
|
+
background: var(--nice-bg-secondary, #1e293b);
|
|
150
|
+
border-color: var(--nice-border-color, #334155);
|
|
151
151
|
}
|
|
152
152
|
|
|
153
153
|
/* Thumbnail */
|
|
@@ -253,7 +253,7 @@
|
|
|
253
253
|
}
|
|
254
254
|
|
|
255
255
|
.dark .cardName {
|
|
256
|
-
color: var(--nice-
|
|
256
|
+
color: var(--nice-text-primary, #f1f5f9);
|
|
257
257
|
}
|
|
258
258
|
|
|
259
259
|
.cardDesc {
|