@nice2dev/ui-printing 1.0.28

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.
Files changed (49) hide show
  1. package/README.md +56 -0
  2. package/dist/index.cjs +2 -0
  3. package/dist/index.d.ts +15 -0
  4. package/dist/index.d.ts.map +1 -0
  5. package/dist/index.mjs +3085 -0
  6. package/dist/print-button/NicePrintButton.d.ts +5 -0
  7. package/dist/print-button/NicePrintButton.d.ts.map +1 -0
  8. package/dist/print-button/index.d.ts +2 -0
  9. package/dist/print-button/index.d.ts.map +1 -0
  10. package/dist/print-preview/NicePrintPreview.d.ts +5 -0
  11. package/dist/print-preview/NicePrintPreview.d.ts.map +1 -0
  12. package/dist/print-preview/index.d.ts +2 -0
  13. package/dist/print-preview/index.d.ts.map +1 -0
  14. package/dist/print-queue/NicePrintQueue.d.ts +5 -0
  15. package/dist/print-queue/NicePrintQueue.d.ts.map +1 -0
  16. package/dist/print-queue/index.d.ts +2 -0
  17. package/dist/print-queue/index.d.ts.map +1 -0
  18. package/dist/style.css +1 -0
  19. package/dist/template-browser/NiceTemplateBrowser.d.ts +5 -0
  20. package/dist/template-browser/NiceTemplateBrowser.d.ts.map +1 -0
  21. package/dist/template-browser/index.d.ts +2 -0
  22. package/dist/template-browser/index.d.ts.map +1 -0
  23. package/dist/template-editor/NiceTemplateEditor.d.ts +6 -0
  24. package/dist/template-editor/NiceTemplateEditor.d.ts.map +1 -0
  25. package/dist/template-editor/index.d.ts +2 -0
  26. package/dist/template-editor/index.d.ts.map +1 -0
  27. package/dist/types/printingTypes.d.ts +249 -0
  28. package/dist/types/printingTypes.d.ts.map +1 -0
  29. package/package.json +66 -0
  30. package/src/globals.d.ts +9 -0
  31. package/src/index.ts +69 -0
  32. package/src/print-button/NicePrintButton.tsx +182 -0
  33. package/src/print-button/PrintButton.module.css +167 -0
  34. package/src/print-button/index.ts +1 -0
  35. package/src/print-preview/NicePrintPreview.tsx +417 -0
  36. package/src/print-preview/PrintPreview.module.css +122 -0
  37. package/src/print-preview/index.ts +1 -0
  38. package/src/print-queue/NicePrintQueue.tsx +350 -0
  39. package/src/print-queue/PrintQueue.module.css +203 -0
  40. package/src/print-queue/index.ts +1 -0
  41. package/src/template-browser/NiceTemplateBrowser.tsx +221 -0
  42. package/src/template-browser/TemplateBrowser.module.css +277 -0
  43. package/src/template-browser/index.ts +1 -0
  44. package/src/template-editor/NiceTemplateEditor.tsx +2386 -0
  45. package/src/template-editor/NiceTemplateEditor.tsx.first +1011 -0
  46. package/src/template-editor/NiceTemplateEditor.tsx.tmp +392 -0
  47. package/src/template-editor/TemplateEditor.module.css +1462 -0
  48. package/src/template-editor/index.ts +1 -0
  49. package/src/types/printingTypes.ts +301 -0
@@ -0,0 +1,350 @@
1
+ import React, { useState, useCallback } from 'react';
2
+
3
+ import type { PrintJob, PrinterInfo, NicePrintQueueProps } from '../types/printingTypes';
4
+
5
+ import styles from './PrintQueue.module.css';
6
+
7
+ // ── Helpers ───────────────────────────────────────────────────────────────────
8
+ const STATUS_LABELS: Record<string, string> = {
9
+ queued: 'Kolejka',
10
+ printing: 'Drukowanie',
11
+ done: 'Wydrukowano',
12
+ error: 'Błąd',
13
+ paused: 'Wstrzymano',
14
+ cancelled: 'Anulowano',
15
+ };
16
+
17
+ function formatTime(iso: string): string {
18
+ try {
19
+ const d = new Date(iso);
20
+ return d.toLocaleTimeString('pl-PL', { hour: '2-digit', minute: '2-digit' });
21
+ } catch {
22
+ return iso;
23
+ }
24
+ }
25
+
26
+ function formatDuration(startIso: string): string {
27
+ const diff = Date.now() - new Date(startIso).getTime();
28
+ const s = Math.floor(diff / 1000);
29
+ if (s < 60) {
30
+ return `${s}s`;
31
+ }
32
+ const m = Math.floor(s / 60);
33
+ return `${m}m ${s % 60}s`;
34
+ }
35
+
36
+ // ── Stats bar ─────────────────────────────────────────────────────────────────
37
+ interface StatsBarProps {
38
+ jobs: PrintJob[];
39
+ }
40
+ const StatsBar: React.FC<StatsBarProps> = ({ jobs }) => {
41
+ const printing = jobs.filter((j) => j.status === 'printing').length;
42
+ const queued = jobs.filter((j) => j.status === 'queued').length;
43
+ const done = jobs.filter((j) => j.status === 'done').length;
44
+ const errored = jobs.filter((j) => j.status === 'error').length;
45
+ return (
46
+ <div className={styles.statsBar}>
47
+ <span className={`${styles.stat} ${styles.statPrinting}`}>
48
+ <span className={styles.dot} />
49
+ Drukuje: <strong>{printing}</strong>
50
+ </span>
51
+ <span className={`${styles.stat} ${styles.statQueued}`}>
52
+ <span className={styles.dot} />W kolejce: <strong>{queued}</strong>
53
+ </span>
54
+ <span className={`${styles.stat} ${styles.statDone}`}>
55
+ <span className={styles.dot} />
56
+ Gotowe: <strong>{done}</strong>
57
+ </span>
58
+ {errored > 0 && (
59
+ <span className={`${styles.stat} ${styles.statError}`}>
60
+ <span className={styles.dot} />
61
+ Błędy: <strong>{errored}</strong>
62
+ </span>
63
+ )}
64
+ </div>
65
+ );
66
+ };
67
+
68
+ // ── Job row ───────────────────────────────────────────────────────────────────
69
+ interface JobRowProps {
70
+ job: PrintJob;
71
+ index: number;
72
+ total: number;
73
+ printers: PrinterInfo[];
74
+ selected: boolean;
75
+ onSelect: (id: string) => void;
76
+ onUpdate: (id: string, patch: Partial<PrintJob>) => void;
77
+ onRemove: (id: string) => void;
78
+ onPrint: (id: string) => void;
79
+ onReorder: (fromIdx: number, toIdx: number) => void;
80
+ }
81
+ const JobRow: React.FC<JobRowProps> = ({
82
+ job,
83
+ index,
84
+ total,
85
+ printers,
86
+ selected,
87
+ onSelect,
88
+ onUpdate,
89
+ onRemove,
90
+ onPrint,
91
+ onReorder,
92
+ }) => {
93
+ const printer = printers.find((p) => p.id === job.options.printerId);
94
+ const canMoveUp = index > 0 && job.status === 'queued';
95
+ const canMoveDown = index < total - 1 && job.status === 'queued';
96
+
97
+ return (
98
+ <div
99
+ className={`${styles.jobRow} ${selected ? styles.jobRowSelected : ''} ${styles[`status-${job.status}`] ?? ''}`}
100
+ >
101
+ {/* Checkbox select */}
102
+ <input
103
+ type="checkbox"
104
+ className={styles.jobCheckbox}
105
+ checked={selected}
106
+ onChange={() => onSelect(job.id)}
107
+ />
108
+
109
+ {/* Status badge */}
110
+ <span className={`${styles.statusBadge} ${styles[`badge-${job.status}`]}`}>
111
+ {STATUS_LABELS[job.status] ?? job.status}
112
+ </span>
113
+
114
+ {/* Job info */}
115
+ <div className={styles.jobInfo}>
116
+ <div className={styles.jobName}>{job.templateName ?? job.templateId}</div>
117
+ <div className={styles.jobMeta}>
118
+ {job.entityLabel && <span>{job.entityLabel}</span>}
119
+ {printer && <span>🖨 {printer.name}</span>}
120
+ <span>×{job.options.copies}</span>
121
+ {job.createdAt && <span className={styles.jobTime}>{formatTime(job.createdAt)}</span>}
122
+ {job.startedAt && job.status !== 'done' && (
123
+ <span className={styles.jobDuration}>{formatDuration(job.startedAt)}</span>
124
+ )}
125
+ </div>
126
+ {/* Progress bar */}
127
+ {job.status === 'printing' && job.progress != null && (
128
+ <div className={styles.progressBar}>
129
+ <div className={styles.progressFill} style={{ width: `${job.progress}%` }} />
130
+ </div>
131
+ )}
132
+ {/* Error message */}
133
+ {job.status === 'error' && job.errorMessage && (
134
+ <div className={styles.jobError}>{job.errorMessage}</div>
135
+ )}
136
+ </div>
137
+
138
+ {/* Copies editor */}
139
+ <div className={styles.copiesEditor}>
140
+ <button
141
+ className={styles.copiesBtn}
142
+ disabled={job.options.copies <= 1}
143
+ onClick={() =>
144
+ onUpdate(job.id, { options: { ...job.options, copies: job.options.copies - 1 } })
145
+ }
146
+ >
147
+
148
+ </button>
149
+ <span className={styles.copiesValue}>{job.options.copies}</span>
150
+ <button
151
+ className={styles.copiesBtn}
152
+ onClick={() =>
153
+ onUpdate(job.id, { options: { ...job.options, copies: job.options.copies + 1 } })
154
+ }
155
+ >
156
+ +
157
+ </button>
158
+ </div>
159
+
160
+ {/* Actions */}
161
+ <div className={styles.jobActions}>
162
+ {canMoveUp && (
163
+ <button
164
+ className={styles.actionBtn}
165
+ title="Przesuń wyżej"
166
+ onClick={() => onReorder(index, index - 1)}
167
+ >
168
+
169
+ </button>
170
+ )}
171
+ {canMoveDown && (
172
+ <button
173
+ className={styles.actionBtn}
174
+ title="Przesuń niżej"
175
+ onClick={() => onReorder(index, index + 1)}
176
+ >
177
+
178
+ </button>
179
+ )}
180
+ {job.status === 'queued' && (
181
+ <button className={styles.actionBtn} title="Drukuj teraz" onClick={() => onPrint(job.id)}>
182
+
183
+ </button>
184
+ )}
185
+ {job.status === 'printing' && (
186
+ <button
187
+ className={styles.actionBtn}
188
+ title="Wstrzymaj"
189
+ onClick={() => onUpdate(job.id, { status: 'paused' })}
190
+ >
191
+
192
+ </button>
193
+ )}
194
+ {job.status === 'paused' && (
195
+ <button
196
+ className={styles.actionBtn}
197
+ title="Wznów"
198
+ onClick={() => onUpdate(job.id, { status: 'queued' })}
199
+ >
200
+
201
+ </button>
202
+ )}
203
+ <button
204
+ className={`${styles.actionBtn} ${styles.removeBtn}`}
205
+ title="Usuń"
206
+ onClick={() => onRemove(job.id)}
207
+ >
208
+
209
+ </button>
210
+ </div>
211
+ </div>
212
+ );
213
+ };
214
+
215
+ // ── Main component ────────────────────────────────────────────────────────────
216
+ export const NicePrintQueue: React.FC<NicePrintQueueProps> = ({
217
+ jobs,
218
+ printers = [],
219
+ onJobUpdate,
220
+ onJobRemove,
221
+ onJobReorder,
222
+ onJobPrint,
223
+ onBulkRemove,
224
+ onBulkPrint,
225
+ maxHeight = 500,
226
+ className,
227
+ style,
228
+ theme = 'light',
229
+ }) => {
230
+ const [selectedIds, setSelectedIds] = useState<Set<string>>(new Set());
231
+ const [filter, setFilter] = useState<'all' | 'queued' | 'printing' | 'done' | 'error'>('all');
232
+
233
+ const filteredJobs = filter === 'all' ? jobs : jobs.filter((j) => j.status === filter);
234
+
235
+ const toggleSelect = useCallback((id: string) => {
236
+ setSelectedIds((prev) => {
237
+ const next = new Set(prev);
238
+ if (next.has(id)) {
239
+ next.delete(id);
240
+ } else {
241
+ next.add(id);
242
+ }
243
+ return next;
244
+ });
245
+ }, []);
246
+
247
+ const selectAll = useCallback(() => {
248
+ if (selectedIds.size === filteredJobs.length) {
249
+ setSelectedIds(new Set());
250
+ } else {
251
+ setSelectedIds(new Set(filteredJobs.map((j) => j.id)));
252
+ }
253
+ }, [filteredJobs, selectedIds.size]);
254
+
255
+ const bulkRemove = useCallback(() => {
256
+ onBulkRemove?.(Array.from(selectedIds));
257
+ setSelectedIds(new Set());
258
+ }, [selectedIds, onBulkRemove]);
259
+
260
+ const bulkPrint = useCallback(() => {
261
+ onBulkPrint?.(Array.from(selectedIds));
262
+ setSelectedIds(new Set());
263
+ }, [selectedIds, onBulkPrint]);
264
+
265
+ return (
266
+ <div
267
+ className={`${styles.root} ${theme === 'dark' ? styles.dark : ''} ${className ?? ''}`}
268
+ style={style}
269
+ >
270
+ {/* Stats */}
271
+ <StatsBar jobs={jobs} />
272
+
273
+ {/* Toolbar */}
274
+ <div className={styles.toolbar}>
275
+ {/* Filter tabs */}
276
+ <div className={styles.filterTabs}>
277
+ {(['all', 'queued', 'printing', 'done', 'error'] as const).map((f) => (
278
+ <button
279
+ key={f}
280
+ className={`${styles.filterTab} ${filter === f ? styles.filterTabActive : ''}`}
281
+ onClick={() => setFilter(f)}
282
+ >
283
+ {f === 'all' ? 'Wszystkie' : STATUS_LABELS[f]}
284
+ <span className={styles.filterCount}>
285
+ {f === 'all' ? jobs.length : jobs.filter((j) => j.status === f).length}
286
+ </span>
287
+ </button>
288
+ ))}
289
+ </div>
290
+
291
+ {/* Bulk actions */}
292
+ {selectedIds.size > 0 && (
293
+ <div className={styles.bulkActions}>
294
+ <span className={styles.bulkCount}>{selectedIds.size} zaznaczonych</span>
295
+ {onBulkPrint && (
296
+ <button className={styles.bulkBtn} onClick={bulkPrint}>
297
+ ▶ Drukuj wybrane
298
+ </button>
299
+ )}
300
+ {onBulkRemove && (
301
+ <button className={`${styles.bulkBtn} ${styles.bulkBtnRemove}`} onClick={bulkRemove}>
302
+ ✕ Usuń wybrane
303
+ </button>
304
+ )}
305
+ </div>
306
+ )}
307
+ </div>
308
+
309
+ {/* Job list */}
310
+ <div className={styles.jobList} style={{ maxHeight }}>
311
+ {filteredJobs.length === 0 ? (
312
+ <div className={styles.emptyState}>
313
+ {filter === 'all'
314
+ ? 'Brak zadań w kolejce'
315
+ : `Brak zadań ze statusem "${STATUS_LABELS[filter]}"`}
316
+ </div>
317
+ ) : (
318
+ <>
319
+ {/* Select-all header */}
320
+ <div className={styles.listHeader}>
321
+ <input
322
+ type="checkbox"
323
+ onChange={selectAll}
324
+ checked={selectedIds.size === filteredJobs.length && filteredJobs.length > 0}
325
+ />
326
+ <span className={styles.headerLabel}>Zadanie</span>
327
+ <span className={styles.headerLabel}>Kopie</span>
328
+ <span className={styles.headerLabel}>Akcje</span>
329
+ </div>
330
+ {filteredJobs.map((job, _index) => (
331
+ <JobRow
332
+ key={job.id}
333
+ job={job}
334
+ index={jobs.indexOf(job)}
335
+ total={jobs.length}
336
+ printers={printers}
337
+ selected={selectedIds.has(job.id)}
338
+ onSelect={toggleSelect}
339
+ onUpdate={onJobUpdate}
340
+ onRemove={onJobRemove}
341
+ onPrint={onJobPrint}
342
+ onReorder={onJobReorder}
343
+ />
344
+ ))}
345
+ </>
346
+ )}
347
+ </div>
348
+ </div>
349
+ );
350
+ };
@@ -0,0 +1,203 @@
1
+ .root {
2
+ display: flex;
3
+ flex-direction: column;
4
+ background: var(--nice-bg, #FFF);
5
+ border: 1px solid var(--nice-border, #e2e8f0);
6
+ border-radius: var(--nice-radius-xl, 12px);
7
+ overflow: hidden;
8
+ font-family: system-ui, sans-serif;
9
+ font-size: 14px;
10
+ color: var(--nice-text, #1e293b);
11
+ }
12
+ .dark { background: var(--nice-text, #1e293b); border-color: var(--nice-text, #334155); color: var(--nice-bg-secondary, #f1f5f9); }
13
+
14
+ /* Stats bar */
15
+ .statsBar {
16
+ display: flex;
17
+ align-items: center;
18
+ gap: var(--nice-space-5, 20px);
19
+ padding: var(--nice-space-2-5, 10px) var(--nice-space-4, 16px);
20
+ background: var(--nice-bg-secondary, #f8fafc);
21
+ border-bottom: 1px solid var(--nice-border, #e2e8f0);
22
+ }
23
+ .dark .statsBar { background: var(--nice-text, #0f172a); border-color: var(--nice-text, #334155); }
24
+
25
+ .stat { display: flex; align-items: center; gap: var(--nice-space-1-5, 6px); font-size: 13px; }
26
+ .dot { width: 8px; height: 8px; border-radius: var(--nice-radius-full, 50%); display: inline-block; }
27
+ .statPrinting .dot { background: var(--nice-primary-hover, #2563eb); }
28
+ .statQueued .dot { background: var(--nice-text-secondary, #64748b); }
29
+ .statDone .dot { background: var(--nice-success-dark, #16a34a); }
30
+ .statError .dot { background: var(--nice-danger-dark, #dc2626); }
31
+
32
+ /* Toolbar */
33
+ .toolbar {
34
+ display: flex;
35
+ align-items: center;
36
+ gap: var(--nice-space-3, 12px);
37
+ padding: var(--nice-space-2, 8px) var(--nice-space-4, 16px);
38
+ border-bottom: 1px solid var(--nice-border, #e2e8f0);
39
+ flex-wrap: wrap;
40
+ }
41
+ .dark .toolbar { border-color: var(--nice-text, #334155); }
42
+
43
+ .filterTabs { display: flex; gap: var(--nice-space-1, 4px); }
44
+ .filterTab {
45
+ display: flex; align-items: center; gap: var(--nice-space-1-25, 5px);
46
+ padding: var(--nice-space-1, 4px) var(--nice-space-2-5, 10px);
47
+ background: none;
48
+ border: 1px solid var(--nice-border, #e2e8f0);
49
+ border-radius: var(--nice-radius-md, 6px);
50
+ cursor: pointer;
51
+ font-size: 12px;
52
+ color: var(--nice-text-secondary, #475569);
53
+ }
54
+ .filterTab:hover { background: var(--nice-bg-secondary, #f1f5f9); }
55
+ .filterTabActive { background: var(--nice-primary-bg, #eff6ff); border-color: var(--nice-primary-hover, #2563eb); color: var(--nice-primary-hover, #2563eb); }
56
+ .filterCount {
57
+ background: var(--nice-bg-secondary, #f1f5f9); padding: var(--nice-space-px, 1px) var(--nice-space-1-25, 5px); border-radius: var(--nice-radius-lg, 8px);
58
+ font-size: 11px; color: var(--nice-text-secondary, #64748b);
59
+ }
60
+ .filterTabActive .filterCount { background: var(--nice-primary-bg, #dbeafe); color: var(--nice-primary-hover, #2563eb); }
61
+
62
+ .bulkActions { display: flex; align-items: center; gap: var(--nice-space-2, 8px); margin-left: auto; }
63
+ .bulkCount { font-size: 13px; color: var(--nice-text-secondary, #64748b); }
64
+ .bulkBtn {
65
+ padding: var(--nice-space-1, 4px) var(--nice-space-2-5, 10px);
66
+ border-radius: var(--nice-radius-md, 6px);
67
+ border: 1px solid var(--nice-border, #e2e8f0);
68
+ cursor: pointer;
69
+ font-size: 12px;
70
+ background: var(--nice-bg, #FFF);
71
+ color: var(--nice-text, #1e293b);
72
+ }
73
+ .bulkBtn:hover { background: var(--nice-bg-secondary, #f1f5f9); }
74
+ .bulkBtnRemove { color: var(--nice-danger-dark, #dc2626); border-color: var(--nice-danger-bg, #fecaca); }
75
+ .bulkBtnRemove:hover { background: var(--nice-danger-bg, #fef2f2); }
76
+
77
+ /* Job list */
78
+ .jobList { overflow-y: auto; }
79
+
80
+ .listHeader {
81
+ display: grid;
82
+ grid-template-columns: 40px 1fr min-content min-content;
83
+ align-items: center;
84
+ gap: var(--nice-space-3, 12px);
85
+ padding: var(--nice-space-2, 8px) var(--nice-space-4, 16px);
86
+ font-size: 12px;
87
+ font-weight: 600;
88
+ color: var(--nice-text-muted, #94a3b8);
89
+ text-transform: uppercase;
90
+ letter-spacing: 0.05em;
91
+ border-bottom: 1px solid var(--nice-bg-secondary, #f1f5f9);
92
+ }
93
+ .dark .listHeader { border-color: var(--nice-text, #1e293b); }
94
+ .headerLabel { white-space: nowrap; }
95
+
96
+ /* Job row */
97
+ .jobRow {
98
+ display: grid;
99
+ grid-template-columns: 40px auto 1fr min-content min-content;
100
+ align-items: center;
101
+ gap: var(--nice-space-3, 12px);
102
+ padding: var(--nice-space-2-5, 10px) var(--nice-space-4, 16px);
103
+ border-bottom: 1px solid var(--nice-bg-secondary, #f1f5f9);
104
+ transition: background 0.1s;
105
+ }
106
+ .dark .jobRow { border-color: var(--nice-text, #334155); }
107
+ .jobRow:hover { background: var(--nice-bg-secondary, #f8fafc); }
108
+ .dark .jobRow:hover { background: var(--nice-text, #0f172a); }
109
+ .jobRowSelected { background: var(--nice-primary-bg, #eff6ff) !important; }
110
+ .dark .jobRowSelected { background: #172554 !important; }
111
+
112
+ .jobCheckbox { cursor: pointer; width: 16px; height: 16px; }
113
+
114
+ /* Status badges */
115
+ .statusBadge {
116
+ display: inline-flex;
117
+ align-items: center;
118
+ padding: var(--nice-space-0-75, 3px) var(--nice-space-2, 8px);
119
+ border-radius: var(--nice-radius-xl, 12px);
120
+ font-size: 11px;
121
+ font-weight: 500;
122
+ white-space: nowrap;
123
+ }
124
+ .badge-queued { background: var(--nice-bg-secondary, #f1f5f9); color: var(--nice-text-secondary, #64748b); }
125
+ .badge-printing { background: var(--nice-primary-bg, #dbeafe); color: var(--nice-primary-dark, #1d4ed8); animation: pulse 1.5s ease-in-out infinite; }
126
+ .badge-done { background: var(--nice-success-bg, #dcfce7); color: var(--nice-success-dark, #16a34a); }
127
+ .badge-error { background: var(--nice-danger-bg, #fee2e2); color: var(--nice-danger-dark, #dc2626); }
128
+ .badge-paused { background: var(--nice-warning-bg, #fef9c3); color: var(--nice-warning-dark, #854d0e); }
129
+ .badge-cancelled { background: var(--nice-bg-secondary, #f1f5f9); color: var(--nice-text-muted, #94a3b8); }
130
+
131
+ @keyframes pulse { 0%, 100% { opacity: 1; } 50% { opacity: 0.7; } }
132
+
133
+ /* Job info */
134
+ .jobInfo { min-width: 0; }
135
+ .jobName { font-weight: 500; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
136
+ .jobMeta {
137
+ display: flex; align-items: center; gap: var(--nice-space-2, 8px);
138
+ font-size: 12px; color: var(--nice-text-muted, #94a3b8);
139
+ margin-top: var(--nice-space-0-5, 2px);
140
+ }
141
+ .jobTime { color: var(--nice-text-secondary, #64748b); }
142
+ .jobDuration { color: var(--nice-warning, #f59e0b); }
143
+ .jobError { font-size: 12px; color: var(--nice-danger-dark, #dc2626); margin-top: var(--nice-space-1, 4px); }
144
+
145
+ /* Progress bar */
146
+ .progressBar {
147
+ height: 3px;
148
+ background: var(--nice-border, #e2e8f0);
149
+ border-radius: var(--nice-radius-sm, 2px);
150
+ margin-top: var(--nice-space-1-5, 6px);
151
+ overflow: hidden;
152
+ }
153
+ .progressFill {
154
+ height: 100%;
155
+ background: var(--nice-primary-hover, #2563eb);
156
+ border-radius: var(--nice-radius-sm, 2px);
157
+ transition: width 0.3s;
158
+ }
159
+
160
+ /* Copies editor */
161
+ .copiesEditor {
162
+ display: flex;
163
+ align-items: center;
164
+ gap: var(--nice-space-1-5, 6px);
165
+ white-space: nowrap;
166
+ }
167
+ .copiesBtn {
168
+ width: 22px; height: 22px;
169
+ display: flex; align-items: center; justify-content: center;
170
+ background: var(--nice-bg-secondary, #f8fafc);
171
+ border: 1px solid var(--nice-border, #e2e8f0);
172
+ border-radius: var(--nice-radius-sm, 4px);
173
+ cursor: pointer;
174
+ font-size: 14px;
175
+ color: var(--nice-text-secondary, #475569);
176
+ }
177
+ .copiesBtn:disabled { opacity: 0.4; cursor: default; }
178
+ .copiesBtn:not(:disabled):hover { background: var(--nice-border, #e2e8f0); }
179
+ .copiesValue { min-width: 20px; text-align: center; font-size: 13px; }
180
+
181
+ /* Actions */
182
+ .jobActions { display: flex; align-items: center; gap: var(--nice-space-1, 4px); }
183
+ .actionBtn {
184
+ width: 28px; height: 28px;
185
+ display: flex; align-items: center; justify-content: center;
186
+ background: none;
187
+ border: 1px solid var(--nice-border, #e2e8f0);
188
+ border-radius: var(--nice-radius-md, 6px);
189
+ cursor: pointer;
190
+ font-size: 13px;
191
+ color: var(--nice-text-secondary, #475569);
192
+ }
193
+ .actionBtn:hover { background: var(--nice-bg-secondary, #f1f5f9); }
194
+ .removeBtn { color: var(--nice-danger, #ef4444); border-color: var(--nice-danger-bg, #fecaca); }
195
+ .removeBtn:hover { background: var(--nice-danger-bg, #fef2f2); }
196
+
197
+ /* Empty state */
198
+ .emptyState {
199
+ text-align: center;
200
+ padding: var(--nice-space-10, 40px) var(--nice-space-6, 24px);
201
+ color: var(--nice-text-muted, #94a3b8);
202
+ font-size: 14px;
203
+ }
@@ -0,0 +1 @@
1
+ export { NicePrintQueue } from './NicePrintQueue';