@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,417 @@
1
+ import { useNiceTranslation } from '@nice2dev/ui';
2
+ import React, { useState, useMemo, useEffect } from 'react';
3
+ import { createPortal } from 'react-dom';
4
+
5
+ import type {
6
+ PrintTemplate,
7
+ PrintDataField,
8
+ TemplateSampleData as _TemplateSampleData,
9
+ PrintJobOptions as _PrintJobOptions,
10
+ NicePrintPreviewProps,
11
+ } from '../types/printingTypes';
12
+
13
+ import styles from './PrintPreview.module.css';
14
+
15
+ // ── Paper dimensions (mm → px at 96dpi, 1mm ≈ 3.78px) ───────────────────────
16
+ const MM_TO_PX = 3.78;
17
+
18
+ const PAPER_DIM: Record<string, [number, number]> = {
19
+ A3: [297, 420],
20
+ A4: [210, 297],
21
+ A5: [148, 210],
22
+ A6: [105, 148],
23
+ Letter: [216, 279],
24
+ Legal: [216, 356],
25
+ Tabloid: [279, 432],
26
+ };
27
+
28
+ function resolveValue(path: string, data: Record<string, unknown>): unknown {
29
+ try {
30
+ return path.split('.').reduce<unknown>((obj, key) => {
31
+ if (obj && typeof obj === 'object') {
32
+ return (obj as Record<string, unknown>)[key];
33
+ }
34
+ return null;
35
+ }, data);
36
+ } catch {
37
+ return null;
38
+ }
39
+ }
40
+
41
+ function formatValue(val: unknown, type: string, format?: string): string {
42
+ if (val == null) {
43
+ return '';
44
+ }
45
+ if (type === 'date' && format) {
46
+ try {
47
+ const d = new Date(val as string);
48
+ // Simple manual format for DD.MM.YYYY
49
+ if (format === 'DD.MM.YYYY') {
50
+ const dd = String(d.getDate()).padStart(2, '0');
51
+ const mm = String(d.getMonth() + 1).padStart(2, '0');
52
+ return `${dd}.${mm}.${d.getFullYear()}`;
53
+ }
54
+ return d.toLocaleDateString();
55
+ } catch {
56
+ return String(val);
57
+ }
58
+ }
59
+ if (type === 'currency' && format) {
60
+ const num = Number(val);
61
+ if (!isNaN(num)) {
62
+ return `${num.toFixed(2)} ${format}`;
63
+ }
64
+ }
65
+ if (type === 'number' && typeof val === 'number') {
66
+ return val.toLocaleString('pl-PL');
67
+ }
68
+ return String(val);
69
+ }
70
+
71
+ // ── Rendered field ────────────────────────────────────────────────────────────
72
+ interface RenderedFieldProps {
73
+ field: PrintDataField;
74
+ data: Record<string, unknown>;
75
+ zoom: number;
76
+ }
77
+ const RenderedField: React.FC<RenderedFieldProps> = ({ field, data, zoom }) => {
78
+ const raw = field.dataPath ? resolveValue(field.dataPath, data) : null;
79
+ const value = raw != null ? formatValue(raw, field.type, field.format) : (field.fallback ?? '');
80
+
81
+ const px = (mm: number) => mm * zoom * MM_TO_PX;
82
+
83
+ const baseStyle: React.CSSProperties = {
84
+ position: 'absolute',
85
+ left: px(field.position.x),
86
+ top: px(field.position.y),
87
+ width: px(field.position.width),
88
+ height: px(field.position.height),
89
+ fontSize: (field.style.fontSize ?? 10) * zoom,
90
+ fontWeight: field.style.fontWeight ?? 'normal',
91
+ fontStyle: field.style.fontStyle ?? 'normal',
92
+ color: field.style.color ?? 'var(--text-primary, #0f172a)',
93
+ textAlign: field.style.textAlign ?? 'left',
94
+ backgroundColor: field.style.backgroundColor,
95
+ transform: field.style.rotation ? `rotate(${field.style.rotation}deg)` : undefined,
96
+ overflow: 'hidden',
97
+ display: 'flex',
98
+ alignItems:
99
+ field.style.verticalAlign === 'middle'
100
+ ? 'center'
101
+ : field.style.verticalAlign === 'bottom'
102
+ ? 'flex-end'
103
+ : 'flex-start',
104
+ lineHeight: field.style.lineHeight ?? 1.4,
105
+ padding: field.style.padding ? px(field.style.padding) : 0,
106
+ boxSizing: 'border-box',
107
+ borderWidth: field.style.borderWidth,
108
+ borderColor: field.style.borderColor,
109
+ borderStyle: field.style.borderStyle,
110
+ borderRadius: field.style.borderRadius,
111
+ zIndex: field.position.zIndex ?? 1,
112
+ };
113
+
114
+ if (field.type === 'image' && raw) {
115
+ return (
116
+ <div style={baseStyle}>
117
+ <img
118
+ src={String(raw)}
119
+ alt=""
120
+ style={{ width: '100%', height: '100%', objectFit: 'contain' }}
121
+ />
122
+ </div>
123
+ );
124
+ }
125
+ if (field.type === 'barcode' || field.type === 'qr') {
126
+ return (
127
+ <div
128
+ style={{
129
+ ...baseStyle,
130
+ background: 'var(--bg-secondary, #f8fafc)',
131
+ border: '1px dashed var(--border-color, #e2e8f0)',
132
+ alignItems: 'center',
133
+ justifyContent: 'center',
134
+ fontSize: 10 * zoom,
135
+ }}
136
+ >
137
+ {field.type === 'qr' ? '⬛' : '▮▌▮'} {value}
138
+ </div>
139
+ );
140
+ }
141
+ if (field.type === 'signature') {
142
+ return (
143
+ <div
144
+ style={{
145
+ ...baseStyle,
146
+ borderBottom: '1px solid var(--text-primary, #0f172a)',
147
+ alignItems: 'flex-end',
148
+ }}
149
+ >
150
+ <span style={{ fontSize: 8 * zoom, color: 'var(--text-muted, #94a3b8)' }}>
151
+ {field.label}
152
+ </span>
153
+ </div>
154
+ );
155
+ }
156
+ if (field.type === 'line') {
157
+ return (
158
+ <div
159
+ style={{
160
+ ...baseStyle,
161
+ borderBottom: `${field.style.borderWidth ?? 1}px ${field.style.borderStyle ?? 'solid'} ${field.style.borderColor ?? 'var(--text-primary, #0f172a)'}`,
162
+ }}
163
+ />
164
+ );
165
+ }
166
+ return <div style={baseStyle}>{value}</div>;
167
+ };
168
+
169
+ // ── Page renderer ─────────────────────────────────────────────────────────────
170
+ interface PageProps {
171
+ template: PrintTemplate;
172
+ data: Record<string, unknown>;
173
+ zoom: number;
174
+ }
175
+ const RenderedPage: React.FC<PageProps> = ({ template, data, zoom }) => {
176
+ const [pw, ph] = (() => {
177
+ const [w, h] = PAPER_DIM[template.paperSize] ?? PAPER_DIM.A4;
178
+ return template.orientation === 'landscape'
179
+ ? [h * MM_TO_PX * zoom, w * MM_TO_PX * zoom]
180
+ : [w * MM_TO_PX * zoom, h * MM_TO_PX * zoom];
181
+ })();
182
+ const m = template.margins;
183
+ const allFields = template.sections.flatMap((s) => s.fields);
184
+
185
+ return (
186
+ <div className={styles.page} style={{ width: pw, minHeight: ph } as React.CSSProperties}>
187
+ <div
188
+ style={
189
+ {
190
+ position: 'absolute',
191
+ top: m.top * zoom * MM_TO_PX,
192
+ left: m.left * zoom * MM_TO_PX,
193
+ right: m.right * zoom * MM_TO_PX,
194
+ bottom: m.bottom * zoom * MM_TO_PX,
195
+ } as React.CSSProperties
196
+ }
197
+ >
198
+ {allFields.map((field) => (
199
+ <RenderedField key={field.id} field={field} data={data} zoom={zoom} />
200
+ ))}
201
+ </div>
202
+
203
+ {/* Watermarks */}
204
+ {template.watermarks?.map((wm) => (
205
+ <div
206
+ key={wm.id}
207
+ className={styles.watermark}
208
+ style={
209
+ {
210
+ opacity: wm.opacity,
211
+ transform: `rotate(${wm.angle}deg)`,
212
+ fontSize: (wm.fontSize ?? 48) * zoom,
213
+ } as React.CSSProperties
214
+ }
215
+ >
216
+ {wm.type === 'text' ? (
217
+ wm.text
218
+ ) : (
219
+ <img
220
+ src={wm.imageUrl}
221
+ alt="wm"
222
+ style={{ maxWidth: 200 * zoom } as React.CSSProperties}
223
+ />
224
+ )}
225
+ </div>
226
+ ))}
227
+ </div>
228
+ );
229
+ };
230
+
231
+ // ── Main component ────────────────────────────────────────────────────────────
232
+ export const NicePrintPreview: React.FC<NicePrintPreviewProps> = ({
233
+ template,
234
+ entities = [],
235
+ sampleData,
236
+ onPrint,
237
+ className,
238
+ style,
239
+ theme = 'light',
240
+ asPopup = true,
241
+ open: openProp,
242
+ onClose,
243
+ }) => {
244
+ const { t } = useNiceTranslation();
245
+ const [entityIdx, setEntityIdx] = useState(0);
246
+ const [zoom, setZoom] = useState(0.8);
247
+ const [internalOpen, setInternalOpen] = useState(true);
248
+
249
+ const isControlled = openProp !== undefined;
250
+ const isOpen = isControlled ? openProp : internalOpen;
251
+ const handleClose = () => {
252
+ if (!isControlled) {
253
+ setInternalOpen(false);
254
+ }
255
+ onClose?.();
256
+ };
257
+
258
+ useEffect(() => {
259
+ if (!asPopup || !isOpen) {
260
+ return;
261
+ }
262
+ const handler = (e: KeyboardEvent) => {
263
+ if (e.key === 'Escape') {
264
+ handleClose();
265
+ }
266
+ };
267
+ document.addEventListener('keydown', handler);
268
+ document.body.style.overflow = 'hidden';
269
+ return () => {
270
+ document.removeEventListener('keydown', handler);
271
+ document.body.style.overflow = '';
272
+ };
273
+ // eslint-disable-next-line react-hooks/exhaustive-deps
274
+ }, [asPopup, isOpen]);
275
+
276
+ const dataList = useMemo(() => {
277
+ if (entities.length > 0) {
278
+ return entities;
279
+ }
280
+ if (sampleData) {
281
+ return [sampleData.data];
282
+ }
283
+ return [{}];
284
+ }, [entities, sampleData]);
285
+
286
+ const currentData = dataList[entityIdx] ?? {};
287
+
288
+ if (asPopup && !isOpen) {
289
+ return null;
290
+ }
291
+
292
+ const body = (
293
+ <div
294
+ className={`${styles.root} ${theme === 'dark' ? styles.dark : ''} ${className ?? ''}`}
295
+ style={style}
296
+ >
297
+ {/* Header bar */}
298
+ <div className={styles.headerBar}>
299
+ <div className={styles.entityNav}>
300
+ {dataList.length > 1 && (
301
+ <>
302
+ <button
303
+ className={styles.navBtn}
304
+ disabled={entityIdx === 0}
305
+ onClick={() => setEntityIdx((i) => i - 1)}
306
+ >
307
+
308
+ </button>
309
+ <span className={styles.navLabel}>
310
+ {entityIdx + 1} / {dataList.length}
311
+ </span>
312
+ <button
313
+ className={styles.navBtn}
314
+ disabled={entityIdx >= dataList.length - 1}
315
+ onClick={() => setEntityIdx((i) => i + 1)}
316
+ >
317
+
318
+ </button>
319
+ </>
320
+ )}
321
+ {entities.length === 0 && sampleData && (
322
+ <span className={styles.sampleBadge}>Przykładowe dane</span>
323
+ )}
324
+ </div>
325
+
326
+ <div className={styles.zoomBar}>
327
+ <button className={styles.zoomBtn} onClick={() => setZoom((z) => Math.max(0.3, z - 0.1))}>
328
+
329
+ </button>
330
+ <span className={styles.zoomLabel}>{Math.round(zoom * 100)}%</span>
331
+ <button className={styles.zoomBtn} onClick={() => setZoom((z) => Math.min(2.5, z + 0.1))}>
332
+ +
333
+ </button>
334
+ <button className={styles.zoomBtn} onClick={() => setZoom(0.8)} title="Dopasuj">
335
+
336
+ </button>
337
+ </div>
338
+
339
+ <div style={{ display: 'flex', gap: 8 }}>
340
+ <button
341
+ className={styles.printBtn}
342
+ onClick={() => onPrint?.({ copies: 1, printerId: '' })}
343
+ >
344
+ {t('printPreview.print', 'Drukuj')}
345
+ </button>
346
+ {asPopup && (
347
+ <button
348
+ type="button"
349
+ className={styles.zoomBtn}
350
+ onClick={handleClose}
351
+ aria-label={t('common.close', 'Zamknij')}
352
+ title={t('common.close', 'Zamknij')}
353
+ >
354
+
355
+ </button>
356
+ )}
357
+ </div>
358
+ </div>
359
+
360
+ {/* Preview canvas */}
361
+ <div className={styles.canvas}>
362
+ <RenderedPage template={template} data={currentData} zoom={zoom} />
363
+ </div>
364
+
365
+ {/* Footer metadata */}
366
+ <div className={styles.footerBar}>
367
+ <span>{template.name}</span>
368
+ <span>
369
+ {template.paperSize} · {template.orientation === 'portrait' ? 'Pion' : 'Poziom'}
370
+ </span>
371
+ {template.sections.reduce((acc, s) => acc + s.fields.length, 0)} pól
372
+ </div>
373
+ </div>
374
+ );
375
+
376
+ if (!asPopup) {
377
+ return body;
378
+ }
379
+
380
+ const overlay = (
381
+ <div
382
+ style={{
383
+ position: 'fixed',
384
+ inset: 0,
385
+ background: 'color-mix(in srgb, var(--text-primary, #000) 50%, transparent)',
386
+ display: 'flex',
387
+ alignItems: 'center',
388
+ justifyContent: 'center',
389
+ zIndex: 1000,
390
+ padding: 24,
391
+ }}
392
+ onMouseDown={(e) => {
393
+ if (e.target === e.currentTarget) {
394
+ handleClose();
395
+ }
396
+ }}
397
+ role="presentation"
398
+ >
399
+ <div
400
+ role="dialog"
401
+ aria-modal="true"
402
+ style={{
403
+ background: 'var(--bg-elevated, var(--bg-primary, #ffffff))',
404
+ borderRadius: 12,
405
+ boxShadow: '0 20px 60px color-mix(in srgb, var(--text-primary, #000) 30%, transparent)',
406
+ maxWidth: '90vw',
407
+ maxHeight: '90vh',
408
+ overflow: 'auto',
409
+ }}
410
+ >
411
+ {body}
412
+ </div>
413
+ </div>
414
+ );
415
+
416
+ return createPortal(overlay, document.body);
417
+ };
@@ -0,0 +1,122 @@
1
+ .root {
2
+ display: flex;
3
+ flex-direction: column;
4
+ height: 100%;
5
+ background: var(--nice-bg-secondary, #f1f5f9);
6
+ font-family: system-ui, sans-serif;
7
+ font-size: 14px;
8
+ color: var(--nice-text, #1e293b);
9
+ }
10
+ .dark { background: var(--nice-text, #1e293b); color: var(--nice-bg-secondary, #f1f5f9); }
11
+
12
+ /* Header bar */
13
+ .headerBar {
14
+ display: flex;
15
+ align-items: center;
16
+ gap: var(--nice-space-3, 12px);
17
+ padding: var(--nice-space-2, 8px) var(--nice-space-4, 16px);
18
+ background: var(--nice-bg, #FFF);
19
+ border-bottom: 1px solid var(--nice-border, #e2e8f0);
20
+ flex-shrink: 0;
21
+ }
22
+ .dark .headerBar { background: var(--nice-text, #0f172a); border-color: var(--nice-text, #334155); }
23
+
24
+ .entityNav { display: flex; align-items: center; gap: var(--nice-space-1-5, 6px); }
25
+ .navBtn {
26
+ padding: var(--nice-space-1, 4px) var(--nice-space-2, 8px);
27
+ background: var(--nice-bg-secondary, #f8fafc);
28
+ border: 1px solid var(--nice-border, #e2e8f0);
29
+ border-radius: var(--nice-radius-sm, 4px);
30
+ cursor: pointer;
31
+ font-size: 16px;
32
+ line-height: 1;
33
+ color: var(--nice-text-secondary, #475569);
34
+ }
35
+ .navBtn:disabled { opacity: 0.4; cursor: default; }
36
+ .navBtn:not(:disabled):hover { background: var(--nice-border, #e2e8f0); }
37
+ .navLabel { color: var(--nice-text-secondary, #64748b); font-size: 13px; min-width: 48px; text-align: center; }
38
+
39
+ .sampleBadge {
40
+ padding: var(--nice-space-0-75, 3px) var(--nice-space-2-5, 10px);
41
+ background: var(--nice-warning-bg, #fef9c3);
42
+ border: 1px solid #fde047;
43
+ border-radius: var(--nice-radius-xl, 12px);
44
+ font-size: 12px;
45
+ color: var(--nice-warning-dark, #854d0e);
46
+ }
47
+
48
+ .zoomBar { display: flex; align-items: center; gap: var(--nice-space-1-5, 6px); margin-left: auto; }
49
+ .zoomBtn {
50
+ width: 28px; height: 28px;
51
+ display: flex; align-items: center; justify-content: center;
52
+ background: var(--nice-bg-secondary, #f8fafc);
53
+ border: 1px solid var(--nice-border, #e2e8f0);
54
+ border-radius: var(--nice-radius-sm, 4px);
55
+ cursor: pointer;
56
+ font-size: 15px;
57
+ color: var(--nice-text-secondary, #475569);
58
+ }
59
+ .zoomBtn:hover { background: var(--nice-border, #e2e8f0); }
60
+ .zoomLabel { font-size: 13px; min-width: 42px; text-align: center; color: var(--nice-text-secondary, #64748b); }
61
+
62
+ .printBtn {
63
+ padding: var(--nice-space-1-5, 6px) var(--nice-space-4, 16px);
64
+ background: var(--nice-primary-hover, #2563eb);
65
+ color: var(--nice-bg, #FFF);
66
+ border: none;
67
+ border-radius: var(--nice-radius-md, 6px);
68
+ cursor: pointer;
69
+ font-size: 13px;
70
+ font-weight: 500;
71
+ }
72
+ .printBtn:hover { background: var(--nice-primary-dark, #1d4ed8); }
73
+
74
+ /* Canvas area */
75
+ .canvas {
76
+ flex: 1;
77
+ overflow: auto;
78
+ display: flex;
79
+ align-items: flex-start;
80
+ justify-content: center;
81
+ padding: var(--nice-space-8, 32px);
82
+ }
83
+
84
+ /* Page paper */
85
+ .page {
86
+ position: relative;
87
+ background: var(--nice-bg, #FFF);
88
+ box-shadow: 0 4px 32px var(--nice-overlay-20, rgba(0, 0, 0, 0.18));
89
+ border-radius: var(--nice-radius-sm, 2px);
90
+ overflow: hidden;
91
+ flex-shrink: 0;
92
+ }
93
+ .dark .page { box-shadow: 0 4px 32px var(--nice-overlay-60, rgba(0, 0, 0, 0.6)); }
94
+
95
+ /* Watermark */
96
+ .watermark {
97
+ position: absolute;
98
+ inset: 0;
99
+ display: flex;
100
+ align-items: center;
101
+ justify-content: center;
102
+ pointer-events: none;
103
+ color: var(--nice-text-muted, #94a3b8);
104
+ font-weight: bold;
105
+ letter-spacing: 0.05em;
106
+ z-index: 100;
107
+ white-space: nowrap;
108
+ }
109
+
110
+ /* Footer bar */
111
+ .footerBar {
112
+ display: flex;
113
+ align-items: center;
114
+ gap: var(--nice-space-4, 16px);
115
+ padding: var(--nice-space-1-5, 6px) var(--nice-space-4, 16px);
116
+ background: var(--nice-bg, #FFF);
117
+ border-top: 1px solid var(--nice-border, #e2e8f0);
118
+ font-size: 12px;
119
+ color: var(--nice-text-muted, #94a3b8);
120
+ flex-shrink: 0;
121
+ }
122
+ .dark .footerBar { background: var(--nice-text, #0f172a); border-color: var(--nice-text, #334155); }
@@ -0,0 +1 @@
1
+ export { NicePrintPreview } from './NicePrintPreview';