@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,392 @@
1
+ // ─── Main component ───────────────────────────────────────────────────────────
2
+
3
+ // ── Toolbar ───────────────────────────────────────────────────────────────────
4
+ interface ToolbarProps {
5
+ template: PrintTemplate;
6
+ onPaperChange: (size: PaperSize, orientation: PaperOrientation) => void;
7
+ onSave?: () => void;
8
+ onInsert: (type: string) => void;
9
+ readOnly: boolean;
10
+ zoom: number;
11
+ onZoomChange: (z: number) => void;
12
+ }
13
+ const Toolbar: React.FC<ToolbarProps> = ({ template, onPaperChange, onSave, onInsert, readOnly, zoom, onZoomChange }) => (
14
+ <div className={styles.toolbar}>
15
+ <div className={styles.toolbarGroup}>
16
+ <select className={styles.toolbarSelect} value={template.paperSize}
17
+ onChange={e => onPaperChange(e.target.value as PaperSize, template.orientation)}>
18
+ {(['A4', 'A5', 'A3', 'Letter', 'Legal', 'custom'] as PaperSize[]).map(s => (
19
+ <option key={s} value={s}>{s}</option>
20
+ ))}
21
+ </select>
22
+ <button className={`${styles.toolbarBtn} ${template.orientation === 'portrait' ? styles.toolbarBtnActive : ''}`}
23
+ onClick={() => onPaperChange(template.paperSize, 'portrait')} title="Pion">⬜</button>
24
+ <button className={`${styles.toolbarBtn} ${template.orientation === 'landscape' ? styles.toolbarBtnActive : ''}`}
25
+ onClick={() => onPaperChange(template.paperSize, 'landscape')} title="Poziom">▭</button>
26
+ </div>
27
+
28
+ {!readOnly && (
29
+ <div className={styles.toolbarGroup}>
30
+ <span className={styles.toolbarLabel}>Wstaw:</span>
31
+ <button className={styles.toolbarBtn} onClick={() => onInsert('text')} title="Pole danych">📝 Pole</button>
32
+ <button className={styles.toolbarBtn} onClick={() => onInsert('static_text')} title="Tekst stały">T Tekst</button>
33
+ <button className={styles.toolbarBtn} onClick={() => onInsert('image')} title="Obraz">🖼 Obraz</button>
34
+ <button className={styles.toolbarBtn} onClick={() => onInsert('barcode')} title="Kod kreskowy">▮▌▮ Kod</button>
35
+ <button className={styles.toolbarBtn} onClick={() => onInsert('qr')} title="QR">⬛ QR</button>
36
+ <button className={styles.toolbarBtn} onClick={() => onInsert('watermark')} title="Znak wodny">💧 Znak</button>
37
+ <button className={styles.toolbarBtn} onClick={() => onInsert('signature')} title="Podpis">✍ Podpis</button>
38
+ <button className={styles.toolbarBtn} onClick={() => onInsert('line')} title="Linia">─ Linia</button>
39
+ </div>
40
+ )}
41
+
42
+ <div className={styles.toolbarGroup}>
43
+ <button className={styles.toolbarBtn} onClick={() => onZoomChange(Math.max(0.3, zoom - 0.1))}>−</button>
44
+ <span className={styles.zoomLabel}>{Math.round(zoom * 100)}%</span>
45
+ <button className={styles.toolbarBtn} onClick={() => onZoomChange(Math.min(3, zoom + 0.1))}>+</button>
46
+ </div>
47
+
48
+ {!readOnly && onSave && (
49
+ <button className={styles.saveBtn} onClick={onSave}>💾 Zapisz</button>
50
+ )}
51
+ </div>
52
+ );
53
+
54
+ // ── Field palette ─────────────────────────────────────────────────────────────
55
+ interface FieldPaletteProps {
56
+ schema?: { fields: DataSourceField[] };
57
+ onInsertField: (field: DataSourceField) => void;
58
+ }
59
+ const FieldPalette: React.FC<FieldPaletteProps> = ({ schema, onInsertField }) => (
60
+ <div className={styles.fieldPalette}>
61
+ <div className={styles.paletteTitle}>Pola danych</div>
62
+ {schema ? (
63
+ <div className={styles.paletteList}>
64
+ {schema.fields.map(f => (
65
+ <div key={f.path} className={styles.paletteField}
66
+ draggable
67
+ onDragStart={e => e.dataTransfer.setData('fieldPath', f.path)}
68
+ onClick={() => onInsertField(f)}>
69
+ <span className={styles.paletteFieldIcon}>{f.type === 'image' ? '🖼' : f.type === 'date' ? '📅' : '📝'}</span>
70
+ <div className={styles.paletteFieldInfo}>
71
+ <span className={styles.paletteFieldLabel}>{f.label}</span>
72
+ <span className={styles.paletteFieldPath}>{f.path}</span>
73
+ </div>
74
+ </div>
75
+ ))}
76
+ </div>
77
+ ) : (
78
+ <div className={styles.paletteEmpty}>Brak schematu danych. Przypisz encję do szablonu.</div>
79
+ )}
80
+ </div>
81
+ );
82
+
83
+ // ── Properties panel ──────────────────────────────────────────────────────────
84
+ interface PropertiesPanelProps {
85
+ field: PrintDataField | null;
86
+ onChange: (patch: Partial<PrintDataField>) => void;
87
+ }
88
+ const PropertiesPanel: React.FC<PropertiesPanelProps> = ({ field, onChange }) => {
89
+ if (!field) return <div className={styles.propsPanel}><div className={styles.propsEmpty}>Zaznacz element</div></div>;
90
+ return (
91
+ <div className={styles.propsPanel}>
92
+ <div className={styles.propsTitle}>Właściwości</div>
93
+
94
+ <div className={styles.propsSection}>
95
+ <div className={styles.propsSectionTitle}>Pozycja</div>
96
+ {(['x', 'y', 'width', 'height'] as const).map(k => (
97
+ <div key={k} className={styles.propsRow}>
98
+ <span className={styles.propsLabel}>{k}</span>
99
+ <input type="number" step={1} className={styles.propsInput}
100
+ value={field.position[k]}
101
+ onChange={e => onChange({ position: { ...field.position, [k]: +e.target.value } })} />
102
+ <span className={styles.propsUnit}>mm</span>
103
+ </div>
104
+ ))}
105
+ <div className={styles.propsRow}>
106
+ <span className={styles.propsLabel}>Obrót</span>
107
+ <input type="number" step={5} className={styles.propsInput}
108
+ value={field.style.rotation ?? 0}
109
+ onChange={e => onChange({ style: { ...field.style, rotation: +e.target.value } })} />
110
+ <span className={styles.propsUnit}>°</span>
111
+ </div>
112
+ </div>
113
+
114
+ <div className={styles.propsSection}>
115
+ <div className={styles.propsSectionTitle}>Czcionka</div>
116
+ <div className={styles.propsRow}>
117
+ <span className={styles.propsLabel}>Rozmiar</span>
118
+ <input type="number" step={1} min={4} max={96} className={styles.propsInput}
119
+ value={field.style.fontSize ?? 12}
120
+ onChange={e => onChange({ style: { ...field.style, fontSize: +e.target.value } })} />
121
+ <span className={styles.propsUnit}>pt</span>
122
+ </div>
123
+ <div className={styles.propsRow}>
124
+ <span className={styles.propsLabel}>Grubość</span>
125
+ <select className={styles.propsSelect} value={field.style.fontWeight ?? 'normal'}
126
+ onChange={e => onChange({ style: { ...field.style, fontWeight: e.target.value as 'normal' | 'bold' } })}>
127
+ <option value="normal">Normalna</option>
128
+ <option value="bold">Pogrubiona</option>
129
+ </select>
130
+ </div>
131
+ <div className={styles.propsRow}>
132
+ <span className={styles.propsLabel}>Kolor</span>
133
+ <input type="color" className={styles.propsColor}
134
+ value={field.style.color ?? '#000000'}
135
+ onChange={e => onChange({ style: { ...field.style, color: e.target.value } })} />
136
+ </div>
137
+ <div className={styles.propsRow}>
138
+ <span className={styles.propsLabel}>Wyrówn.</span>
139
+ <select className={styles.propsSelect} value={field.style.textAlign ?? 'left'}
140
+ onChange={e => onChange({ style: { ...field.style, textAlign: e.target.value as any } })}>
141
+ <option value="left">Lewo</option>
142
+ <option value="center">Centrum</option>
143
+ <option value="right">Prawo</option>
144
+ </select>
145
+ </div>
146
+ </div>
147
+
148
+ {field.dataPath && (
149
+ <div className={styles.propsSection}>
150
+ <div className={styles.propsSectionTitle}>Dane</div>
151
+ <div className={styles.propsRow}>
152
+ <span className={styles.propsLabel}>Ścieżka</span>
153
+ <input className={styles.propsInputFull}
154
+ value={field.dataPath}
155
+ onChange={e => onChange({ dataPath: e.target.value })} />
156
+ </div>
157
+ <div className={styles.propsRow}>
158
+ <span className={styles.propsLabel}>Format</span>
159
+ <input className={styles.propsInputFull}
160
+ value={field.format ?? ''}
161
+ placeholder="DD.MM.YYYY / #,##0.00"
162
+ onChange={e => onChange({ format: e.target.value })} />
163
+ </div>
164
+ <div className={styles.propsRow}>
165
+ <span className={styles.propsLabel}>Domyślny</span>
166
+ <input className={styles.propsInputFull}
167
+ value={field.fallback ?? ''}
168
+ onChange={e => onChange({ fallback: e.target.value })} />
169
+ </div>
170
+ </div>
171
+ )}
172
+ </div>
173
+ );
174
+ };
175
+
176
+ // ── Template section canvas ───────────────────────────────────────────────────
177
+ interface SectionCanvasProps {
178
+ section: PrintSection;
179
+ zoom: number;
180
+ selectedFieldId: string | null;
181
+ onSelectField: (id: string | null) => void;
182
+ readOnly: boolean;
183
+ }
184
+ const SectionCanvas: React.FC<SectionCanvasProps> = ({ section, zoom, selectedFieldId, onSelectField, readOnly }) => (
185
+ <div className={`${styles.sectionCanvas} ${styles[`section_${section.type}`]}`}>
186
+ <div className={styles.sectionLabel}>{section.type}</div>
187
+ {section.fields.map(field => (
188
+ <div
189
+ key={field.id}
190
+ className={`${styles.fieldEl} ${selectedFieldId === field.id ? styles.fieldElSelected : ''}`}
191
+ style={{
192
+ left: field.position.x * zoom * 3.78,
193
+ top: field.position.y * zoom * 3.78,
194
+ width: field.position.width * zoom * 3.78,
195
+ height: field.position.height * zoom * 3.78,
196
+ fontSize: (field.style.fontSize ?? 12) * zoom,
197
+ fontWeight: field.style.fontWeight ?? 'normal',
198
+ fontStyle: field.style.fontStyle ?? 'normal',
199
+ color: field.style.color ?? '#000',
200
+ textAlign: field.style.textAlign ?? 'left',
201
+ backgroundColor: field.style.backgroundColor,
202
+ transform: field.style.rotation ? `rotate(${field.style.rotation}deg)` : undefined,
203
+ zIndex: field.position.zIndex ?? 1,
204
+ } as React.CSSProperties}
205
+ onClick={e => { e.stopPropagation(); if (!readOnly) onSelectField(field.id); }}
206
+ >
207
+ {field.type === 'static_text' ? field.fallback ?? '' : `{${field.dataPath}}`}
208
+ </div>
209
+ ))}
210
+ </div>
211
+ );
212
+
213
+ // ── Main component ────────────────────────────────────────────────────────────
214
+ const defaultMargins = { top: 20, right: 15, bottom: 20, left: 15 };
215
+ const defaultTemplate = (): PrintTemplate => ({
216
+ id: `tpl-${Date.now()}`, name: 'Bez nazwy', paperSize: 'A4',
217
+ orientation: 'portrait', margins: defaultMargins,
218
+ sections: [
219
+ { id: 'header', type: 'header', heightMm: 30, fields: [] },
220
+ { id: 'body', type: 'body', fields: [] },
221
+ { id: 'footer', type: 'footer', heightMm: 20, fields: [] },
222
+ ],
223
+ });
224
+
225
+ export const NiceTemplateEditor: React.FC<NiceTemplateEditorProps> = ({
226
+ template: initialTemplate,
227
+ dataSchema,
228
+ sampleData = [],
229
+ onChange,
230
+ onSave,
231
+ readOnly = false,
232
+ className,
233
+ style,
234
+ theme = 'light',
235
+ }) => {
236
+ const [tpl, setTpl] = useState<PrintTemplate>(initialTemplate ?? defaultTemplate());
237
+ const [selectedFieldId, setSelectedFieldId] = useState<string | null>(null);
238
+ const [activeSection, setActiveSection] = useState<SectionType>('body');
239
+ const [zoom, setZoom] = useState(1);
240
+ const [testDataIdx, setTestDataIdx] = useState(0);
241
+
242
+ const update = useCallback((patch: Partial<PrintTemplate>) => {
243
+ const next = { ...tpl, ...patch };
244
+ setTpl(next);
245
+ onChange?.(next);
246
+ }, [tpl, onChange]);
247
+
248
+ const getDim = (): [number, number] => {
249
+ const [w, h] = PAPER_DIM[tpl.paperSize] ?? PAPER_DIM.A4;
250
+ return tpl.orientation === 'landscape' ? [h, w] : [w, h];
251
+ };
252
+ const [paperW, paperH] = getDim();
253
+
254
+ const insertField = useCallback((schemaField: DataSourceField) => {
255
+ const section = tpl.sections.find(s => s.type === activeSection);
256
+ if (!section) return;
257
+ const newField: PrintDataField = {
258
+ id: `field-${Date.now()}`,
259
+ name: schemaField.label,
260
+ label: schemaField.label,
261
+ type: schemaField.type === 'image' ? 'image' : 'text',
262
+ dataPath: schemaField.path,
263
+ format: schemaField.type === 'date' ? 'DD.MM.YYYY' : undefined,
264
+ position: { x: 10, y: 10, width: 60, height: 8 },
265
+ style: { fontSize: 10, color: '#000' },
266
+ };
267
+ update({
268
+ sections: tpl.sections.map(s =>
269
+ s.id === section.id ? { ...s, fields: [...s.fields, newField] } : s
270
+ ),
271
+ });
272
+ setSelectedFieldId(newField.id);
273
+ }, [tpl, activeSection, update]);
274
+
275
+ const insertStaticField = useCallback((type: string) => {
276
+ const section = tpl.sections.find(s => s.type === activeSection);
277
+ if (!section) return;
278
+ const newField: PrintDataField = {
279
+ id: `field-${Date.now()}`,
280
+ name: type,
281
+ label: type,
282
+ type: type as any,
283
+ dataPath: '',
284
+ fallback: type === 'static_text' ? 'Tekst statyczny' : undefined,
285
+ position: { x: 10, y: 10, width: 40, height: 8 },
286
+ style: { fontSize: 10, color: '#000' },
287
+ };
288
+ update({
289
+ sections: tpl.sections.map(s =>
290
+ s.id === section.id ? { ...s, fields: [...s.fields, newField] } : s
291
+ ),
292
+ });
293
+ setSelectedFieldId(newField.id);
294
+ }, [tpl, activeSection, update]);
295
+
296
+ const selectedField = tpl.sections.flatMap(s => s.fields).find(f => f.id === selectedFieldId) ?? null;
297
+
298
+ const updateField = useCallback((patch: Partial<PrintDataField>) => {
299
+ update({
300
+ sections: tpl.sections.map(s => ({
301
+ ...s,
302
+ fields: s.fields.map(f => f.id === selectedFieldId ? { ...f, ...patch } : f),
303
+ })),
304
+ });
305
+ }, [tpl, selectedFieldId, update]);
306
+
307
+ return (
308
+ <div className={`${styles.root} ${theme === 'dark' ? styles.dark : ''} ${className ?? ''}`} style={style}>
309
+ <Toolbar
310
+ template={tpl}
311
+ onPaperChange={(size, orientation) => update({ paperSize: size, orientation })}
312
+ onSave={onSave ? () => onSave(tpl) : undefined}
313
+ onInsert={type => type === 'watermark'
314
+ ? update({ watermarks: [...(tpl.watermarks ?? []), { id: `wm-${Date.now()}`, type: 'text', text: 'KOPIA', opacity: 0.15, angle: -45 }] })
315
+ : type === 'signature'
316
+ ? update({ signatures: [...(tpl.signatures ?? []), { id: `sig-${Date.now()}`, label: 'Podpis', position: { x: 10, y: 10, width: 60, height: 20 } }] })
317
+ : insertStaticField(type)
318
+ }
319
+ readOnly={readOnly}
320
+ zoom={zoom}
321
+ onZoomChange={setZoom}
322
+ />
323
+
324
+ <div className={styles.body}>
325
+ {/* Field palette */}
326
+ {!readOnly && (
327
+ <FieldPalette schema={dataSchema} onInsertField={insertField} />
328
+ )}
329
+
330
+ {/* Paper canvas wrapper */}
331
+ <div className={styles.canvasArea} onClick={() => setSelectedFieldId(null)}>
332
+ {/* Test data selector */}
333
+ {sampleData.length > 0 && (
334
+ <div className={styles.testDataBar}>
335
+ <span className={styles.testDataLabel}>Podgląd</span>
336
+ <select className={styles.testDataSelect} value={testDataIdx}
337
+ onChange={e => setTestDataIdx(+e.target.value)}>
338
+ {sampleData.map((d, i) => (
339
+ <option key={i} value={i}>{d.label ?? d.entityName}</option>
340
+ ))}
341
+ </select>
342
+ </div>
343
+ )}
344
+
345
+ {/* Section tab selector */}
346
+ <div className={styles.sectionTabs}>
347
+ {tpl.sections.map(s => (
348
+ <button key={s.id}
349
+ className={`${styles.sectionTab} ${activeSection === s.type ? styles.sectionTabActive : ''}`}
350
+ onClick={() => setActiveSection(s.type)}>
351
+ {s.type}
352
+ </button>
353
+ ))}
354
+ </div>
355
+
356
+ {/* Paper */}
357
+ <div
358
+ className={styles.paper}
359
+ style={{
360
+ width: paperW * zoom * 3.78,
361
+ minHeight: paperH * zoom * 3.78,
362
+ padding: `${tpl.margins.top * zoom * 3.78}px ${tpl.margins.right * zoom * 3.78}px ${tpl.margins.bottom * zoom * 3.78}px ${tpl.margins.left * zoom * 3.78}px`,
363
+ } as React.CSSProperties}
364
+ >
365
+ {tpl.sections.map(section => (
366
+ <SectionCanvas
367
+ key={section.id}
368
+ section={section}
369
+ zoom={zoom}
370
+ selectedFieldId={selectedFieldId}
371
+ onSelectField={setSelectedFieldId}
372
+ readOnly={readOnly}
373
+ />
374
+ ))}
375
+
376
+ {/* Watermarks */}
377
+ {tpl.watermarks?.map(wm => (
378
+ <div key={wm.id} className={styles.watermark}
379
+ style={{ opacity: wm.opacity, transform: `rotate(${wm.angle}deg)` } as React.CSSProperties}>
380
+ {wm.type === 'text' ? <span style={{ fontSize: (wm.fontSize ?? 48) * zoom } as React.CSSProperties}>{wm.text}</span>
381
+ : <img src={wm.imageUrl} alt="watermark" className={styles.watermarkImg} />}
382
+ </div>
383
+ ))}
384
+ </div>
385
+ </div>
386
+
387
+ {/* Properties panel */}
388
+ <PropertiesPanel field={selectedField} onChange={updateField} />
389
+ </div>
390
+ </div>
391
+ );
392
+ };