@nice2dev/ui-printing 1.2.1 → 1.3.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/dist/index.cjs +2 -2
- package/dist/index.mjs +1589 -1504
- package/package.json +3 -3
- package/src/print-button/NicePrintButton.tsx +2 -2
- package/src/print-preview/NicePrintPreview.tsx +73 -18
- package/src/template-browser/NiceTemplateBrowser.tsx +4 -2
- package/src/template-editor/NiceTemplateEditor.tsx +36 -16
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nice2dev/ui-printing",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.3.0",
|
|
4
4
|
"description": "React components for print management: template editor, template browser, print preview, print queue and print button",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"printing",
|
|
@@ -47,10 +47,10 @@
|
|
|
47
47
|
"peerDependencies": {
|
|
48
48
|
"react": ">=17.0.0",
|
|
49
49
|
"react-dom": ">=17.0.0",
|
|
50
|
-
"@nice2dev/ui": "^1.
|
|
50
|
+
"@nice2dev/ui": "^1.3.0"
|
|
51
51
|
},
|
|
52
52
|
"devDependencies": {
|
|
53
|
-
"@nice2dev/ui": "^1.
|
|
53
|
+
"@nice2dev/ui": "^1.3.0",
|
|
54
54
|
"@types/react": "^19.2.18",
|
|
55
55
|
"@types/react-dom": "^19.2.7",
|
|
56
56
|
"@vitejs/plugin-react": "^4.7.0",
|
|
@@ -139,7 +139,7 @@ export const NicePrintButton: React.FC<NicePrintButtonProps> = ({
|
|
|
139
139
|
>
|
|
140
140
|
{p.name}{' '}
|
|
141
141
|
{p.status === 'offline' || (!p.isOnline && p.status == null)
|
|
142
|
-
? '(Offline)'
|
|
142
|
+
? t('printing.offline', '(Offline)')
|
|
143
143
|
: p.status === 'busy'
|
|
144
144
|
? `(${t('printing.printerBusyLabel', 'Busy')})`
|
|
145
145
|
: ''}
|
|
@@ -161,7 +161,7 @@ export const NicePrintButton: React.FC<NicePrintButtonProps> = ({
|
|
|
161
161
|
: s === 'busy'
|
|
162
162
|
? t('printing.printerBusyLabel', 'Busy')
|
|
163
163
|
: s === 'offline'
|
|
164
|
-
? 'Offline'
|
|
164
|
+
? t('printing.offline2', 'Offline')
|
|
165
165
|
: t('printing.printerError', 'Error')}
|
|
166
166
|
</span>
|
|
167
167
|
</>
|
|
@@ -1,5 +1,11 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
1
|
+
import {
|
|
2
|
+
NiceIcon,
|
|
3
|
+
useInputHints,
|
|
4
|
+
useIntent,
|
|
5
|
+
useNiceTranslation,
|
|
6
|
+
useOverlayFocus,
|
|
7
|
+
} from '@nice2dev/ui';
|
|
8
|
+
import React, { useState, useMemo, useEffect, useRef } from 'react';
|
|
3
9
|
import { createPortal } from 'react-dom';
|
|
4
10
|
|
|
5
11
|
import type {
|
|
@@ -259,18 +265,10 @@ export const NicePrintPreview: React.FC<NicePrintPreviewProps> = ({
|
|
|
259
265
|
if (!asPopup || !isOpen) {
|
|
260
266
|
return;
|
|
261
267
|
}
|
|
262
|
-
const handler = (e: KeyboardEvent) => {
|
|
263
|
-
if (e.key === 'Escape') {
|
|
264
|
-
handleClose();
|
|
265
|
-
}
|
|
266
|
-
};
|
|
267
|
-
document.addEventListener('keydown', handler);
|
|
268
268
|
document.body.style.overflow = 'hidden';
|
|
269
269
|
return () => {
|
|
270
|
-
document.removeEventListener('keydown', handler);
|
|
271
270
|
document.body.style.overflow = '';
|
|
272
271
|
};
|
|
273
|
-
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
274
272
|
}, [asPopup, isOpen]);
|
|
275
273
|
|
|
276
274
|
const dataList = useMemo(() => {
|
|
@@ -285,12 +283,53 @@ export const NicePrintPreview: React.FC<NicePrintPreviewProps> = ({
|
|
|
285
283
|
|
|
286
284
|
const currentData = dataList[entityIdx] ?? {};
|
|
287
285
|
|
|
286
|
+
// The popup holds focus; Escape / B close it — only the top overlay — and focus goes back to
|
|
287
|
+
// the opener (1.3.0, a document listener before). A pad: LB / RB between the records, LT / RT
|
|
288
|
+
// zoom.
|
|
289
|
+
const dialogRef = useRef<HTMLDivElement>(null);
|
|
290
|
+
const rootRef = useRef<HTMLDivElement>(null);
|
|
291
|
+
useOverlayFocus(dialogRef, { open: asPopup && isOpen, onClose: handleClose });
|
|
292
|
+
useIntent(
|
|
293
|
+
['sectionPrev', 'sectionNext', 'pagePrev', 'pageNext'],
|
|
294
|
+
(d) => {
|
|
295
|
+
if (d.intent === 'sectionPrev' || d.intent === 'sectionNext') {
|
|
296
|
+
const by = d.intent === 'sectionNext' ? 1 : -1;
|
|
297
|
+
setEntityIdx((i) => Math.min(Math.max(i + by, 0), dataList.length - 1));
|
|
298
|
+
} else if (d.source === 'keyboard') {
|
|
299
|
+
return false;
|
|
300
|
+
} else {
|
|
301
|
+
const by = d.intent === 'pageNext' ? 0.1 : -0.1;
|
|
302
|
+
setZoom((z) => Math.min(2.5, Math.max(0.3, z + by)));
|
|
303
|
+
}
|
|
304
|
+
},
|
|
305
|
+
{ ref: rootRef, enabled: !asPopup || isOpen },
|
|
306
|
+
);
|
|
307
|
+
useInputHints(
|
|
308
|
+
rootRef,
|
|
309
|
+
!asPopup || isOpen
|
|
310
|
+
? [
|
|
311
|
+
...(dataList.length > 1
|
|
312
|
+
? [
|
|
313
|
+
{
|
|
314
|
+
intent: 'sectionPrev' as const,
|
|
315
|
+
label: t('printPreview.previousPage', 'Previous page'),
|
|
316
|
+
},
|
|
317
|
+
{ intent: 'sectionNext' as const, label: t('printPreview.nextPage', 'Next page') },
|
|
318
|
+
]
|
|
319
|
+
: []),
|
|
320
|
+
{ intent: 'pageNext', label: t('printPreview.zoomIn', 'Zoom in') },
|
|
321
|
+
{ intent: 'pagePrev', label: t('printPreview.zoomOut', 'Zoom out') },
|
|
322
|
+
]
|
|
323
|
+
: [],
|
|
324
|
+
);
|
|
325
|
+
|
|
288
326
|
if (asPopup && !isOpen) {
|
|
289
327
|
return null;
|
|
290
328
|
}
|
|
291
329
|
|
|
292
330
|
const body = (
|
|
293
331
|
<div
|
|
332
|
+
ref={rootRef}
|
|
294
333
|
className={`${styles.root} ${theme === 'dark' ? styles.dark : ''} ${className ?? ''}`}
|
|
295
334
|
style={style}
|
|
296
335
|
>
|
|
@@ -303,8 +342,9 @@ export const NicePrintPreview: React.FC<NicePrintPreviewProps> = ({
|
|
|
303
342
|
className={styles.navBtn}
|
|
304
343
|
disabled={entityIdx === 0}
|
|
305
344
|
onClick={() => setEntityIdx((i) => i - 1)}
|
|
345
|
+
aria-label={t('printPreview.previousPage', 'Previous page')}
|
|
306
346
|
>
|
|
307
|
-
|
|
347
|
+
<NiceIcon name="chevron-left" size={14} />
|
|
308
348
|
</button>
|
|
309
349
|
<span className={styles.navLabel}>
|
|
310
350
|
{entityIdx + 1} / {dataList.length}
|
|
@@ -313,8 +353,9 @@ export const NicePrintPreview: React.FC<NicePrintPreviewProps> = ({
|
|
|
313
353
|
className={styles.navBtn}
|
|
314
354
|
disabled={entityIdx >= dataList.length - 1}
|
|
315
355
|
onClick={() => setEntityIdx((i) => i + 1)}
|
|
356
|
+
aria-label={t('printPreview.nextPage', 'Next page')}
|
|
316
357
|
>
|
|
317
|
-
|
|
358
|
+
<NiceIcon name="chevron-right" size={14} />
|
|
318
359
|
</button>
|
|
319
360
|
</>
|
|
320
361
|
)}
|
|
@@ -324,19 +365,28 @@ export const NicePrintPreview: React.FC<NicePrintPreviewProps> = ({
|
|
|
324
365
|
</div>
|
|
325
366
|
|
|
326
367
|
<div className={styles.zoomBar}>
|
|
327
|
-
<button
|
|
328
|
-
|
|
368
|
+
<button
|
|
369
|
+
className={styles.zoomBtn}
|
|
370
|
+
onClick={() => setZoom((z) => Math.max(0.3, z - 0.1))}
|
|
371
|
+
aria-label={t('printPreview.zoomOut', 'Zoom out')}
|
|
372
|
+
>
|
|
373
|
+
<NiceIcon name="minus" size={14} />
|
|
329
374
|
</button>
|
|
330
375
|
<span className={styles.zoomLabel}>{Math.round(zoom * 100)}%</span>
|
|
331
|
-
<button
|
|
332
|
-
|
|
376
|
+
<button
|
|
377
|
+
className={styles.zoomBtn}
|
|
378
|
+
onClick={() => setZoom((z) => Math.min(2.5, z + 0.1))}
|
|
379
|
+
aria-label={t('printPreview.zoomIn', 'Zoom in')}
|
|
380
|
+
>
|
|
381
|
+
<NiceIcon name="plus" size={14} />
|
|
333
382
|
</button>
|
|
334
383
|
<button
|
|
335
384
|
className={styles.zoomBtn}
|
|
336
385
|
onClick={() => setZoom(0.8)}
|
|
337
386
|
title={t('printing.fit', 'Fit')}
|
|
387
|
+
aria-label={t('printing.fit', 'Fit')}
|
|
338
388
|
>
|
|
339
|
-
|
|
389
|
+
<NiceIcon name="expand" size={14} />
|
|
340
390
|
</button>
|
|
341
391
|
</div>
|
|
342
392
|
|
|
@@ -370,7 +420,10 @@ export const NicePrintPreview: React.FC<NicePrintPreviewProps> = ({
|
|
|
370
420
|
<div className={styles.footerBar}>
|
|
371
421
|
<span>{template.name}</span>
|
|
372
422
|
<span>
|
|
373
|
-
{template.paperSize} ·
|
|
423
|
+
{template.paperSize} ·{' '}
|
|
424
|
+
{template.orientation === 'portrait'
|
|
425
|
+
? t('printing.portrait', 'Portrait')
|
|
426
|
+
: t('printing.landscape', 'Landscape')}
|
|
374
427
|
</span>
|
|
375
428
|
{template.sections.reduce((acc, s) => acc + s.fields.length, 0)}{' '}
|
|
376
429
|
{t('printing.fields', 'fields')}
|
|
@@ -402,8 +455,10 @@ export const NicePrintPreview: React.FC<NicePrintPreviewProps> = ({
|
|
|
402
455
|
role="presentation"
|
|
403
456
|
>
|
|
404
457
|
<div
|
|
458
|
+
ref={dialogRef}
|
|
405
459
|
role="dialog"
|
|
406
460
|
aria-modal="true"
|
|
461
|
+
aria-label={template.name}
|
|
407
462
|
style={{
|
|
408
463
|
background: 'var(--nice-bg-elevated, var(--nice-bg-primary, #ffffff))',
|
|
409
464
|
borderRadius: 12,
|
|
@@ -1,5 +1,4 @@
|
|
|
1
|
-
|
|
2
|
-
import { NiceIcon, useNiceTranslation } from '@nice2dev/ui';
|
|
1
|
+
import { clickOnActivationKey, NiceIcon, useNiceTranslation } from '@nice2dev/ui';
|
|
3
2
|
import React, { useState } from 'react';
|
|
4
3
|
|
|
5
4
|
import type {
|
|
@@ -144,6 +143,9 @@ export const NiceTemplateBrowser: React.FC<NiceTemplateBrowserProps> = ({
|
|
|
144
143
|
<div className={styles.grid}>
|
|
145
144
|
{filtered.map((tpl) => (
|
|
146
145
|
<div
|
|
146
|
+
role={onSelect ? 'button' : undefined}
|
|
147
|
+
tabIndex={onSelect ? 0 : undefined}
|
|
148
|
+
onKeyDown={onSelect ? clickOnActivationKey : undefined}
|
|
147
149
|
key={tpl.id}
|
|
148
150
|
className={`${styles.card} ${selectedId === tpl.id ? styles.cardSelected : ''}`}
|
|
149
151
|
onClick={() => onSelect?.(tpl)}
|
|
@@ -1,5 +1,4 @@
|
|
|
1
|
-
|
|
2
|
-
import { NiceAppShell, NiceIcon, useNiceTranslation } from '@nice2dev/ui';
|
|
1
|
+
import { clickOnActivationKey, NiceAppShell, NiceIcon, useNiceTranslation } from '@nice2dev/ui';
|
|
3
2
|
import type { NiceTranslateFn } from '@nice2dev/ui';
|
|
4
3
|
import React, {
|
|
5
4
|
useState,
|
|
@@ -286,6 +285,7 @@ const FieldElement: React.FC<FieldElementProps> = ({
|
|
|
286
285
|
onResizeStart,
|
|
287
286
|
readOnly,
|
|
288
287
|
}) => {
|
|
288
|
+
const { t } = useNiceTranslation();
|
|
289
289
|
const pos = field.position;
|
|
290
290
|
const st = field.style;
|
|
291
291
|
const mm = (v: number) => v * zoom * MM_TO_PX;
|
|
@@ -330,7 +330,12 @@ const FieldElement: React.FC<FieldElementProps> = ({
|
|
|
330
330
|
};
|
|
331
331
|
|
|
332
332
|
return (
|
|
333
|
-
<div
|
|
333
|
+
<div
|
|
334
|
+
role="presentation"
|
|
335
|
+
style={baseStyle}
|
|
336
|
+
onClick={onClick}
|
|
337
|
+
onMouseDown={readOnly ? undefined : onDragStart}
|
|
338
|
+
>
|
|
334
339
|
{isLine ? (
|
|
335
340
|
<div
|
|
336
341
|
style={{
|
|
@@ -345,7 +350,7 @@ const FieldElement: React.FC<FieldElementProps> = ({
|
|
|
345
350
|
) : isBarcode ? (
|
|
346
351
|
<div className={styles.fieldBarcodePlaceholder}>
|
|
347
352
|
<NiceIcon name={field.type === 'qr' ? 'qr-code' : 'barcode'} size={14} />{' '}
|
|
348
|
-
{field.type === 'qr' ? 'QR' : 'Barcode'}
|
|
353
|
+
{field.type === 'qr' ? 'QR' : t('templateEditor.barcode', 'Barcode')}
|
|
349
354
|
</div>
|
|
350
355
|
) : field.type === 'signature' ? (
|
|
351
356
|
<div className={styles.fieldSignaturePlaceholder}>
|
|
@@ -899,6 +904,9 @@ const LeftPanel: React.FC<LeftPanelProps> = ({
|
|
|
899
904
|
<div className={styles.fieldList}>
|
|
900
905
|
{(filteredFields ?? []).map((f) => (
|
|
901
906
|
<div
|
|
907
|
+
role="button"
|
|
908
|
+
tabIndex={0}
|
|
909
|
+
onKeyDown={clickOnActivationKey}
|
|
902
910
|
key={f.path}
|
|
903
911
|
className={styles.fieldItem}
|
|
904
912
|
draggable
|
|
@@ -953,6 +961,9 @@ const LeftPanel: React.FC<LeftPanelProps> = ({
|
|
|
953
961
|
.reverse()
|
|
954
962
|
.map((f) => (
|
|
955
963
|
<div
|
|
964
|
+
role="button"
|
|
965
|
+
tabIndex={0}
|
|
966
|
+
onKeyDown={clickOnActivationKey}
|
|
956
967
|
key={f.id}
|
|
957
968
|
className={`${styles.layerItem} ${selectedIds.has(f.id) ? styles.layerItemSelected : ''}`}
|
|
958
969
|
onClick={() => onSelectField(f.id)}
|
|
@@ -1121,7 +1132,7 @@ const LeftPanel: React.FC<LeftPanelProps> = ({
|
|
|
1121
1132
|
{
|
|
1122
1133
|
id: uid(),
|
|
1123
1134
|
type: 'text',
|
|
1124
|
-
text: 'COPY',
|
|
1135
|
+
text: t('templateEditor.copy', 'COPY'),
|
|
1125
1136
|
opacity: 0.12,
|
|
1126
1137
|
angle: -45,
|
|
1127
1138
|
fontSize: 60,
|
|
@@ -1298,7 +1309,7 @@ const LeftPanel: React.FC<LeftPanelProps> = ({
|
|
|
1298
1309
|
onUpdateTpl({ margins: { ...tpl.margins, [side]: +e.target.value } })
|
|
1299
1310
|
}
|
|
1300
1311
|
/>
|
|
1301
|
-
<span className={styles.settingsUnit}>mm</span>
|
|
1312
|
+
<span className={styles.settingsUnit}>{t('templateEditor.mm', 'mm')}</span>
|
|
1302
1313
|
</div>
|
|
1303
1314
|
))}
|
|
1304
1315
|
</div>
|
|
@@ -1409,8 +1420,8 @@ const PropertiesPanel: React.FC<PropertiesPanelProps> = ({
|
|
|
1409
1420
|
{[
|
|
1410
1421
|
{ key: 'x' as const, label: 'X', unit: 'mm' },
|
|
1411
1422
|
{ key: 'y' as const, label: 'Y', unit: 'mm' },
|
|
1412
|
-
{ key: 'width' as const, label: '
|
|
1413
|
-
{ key: 'height' as const, label: '
|
|
1423
|
+
{ key: 'width' as const, label: t('templateEditor.widthShort', 'W'), unit: 'mm' },
|
|
1424
|
+
{ key: 'height' as const, label: t('templateEditor.heightShort', 'H'), unit: 'mm' },
|
|
1414
1425
|
].map(({ key, label, unit }) => (
|
|
1415
1426
|
<div key={key} className={styles.posCell}>
|
|
1416
1427
|
<label className={styles.posLabel}>{label}</label>
|
|
@@ -1693,7 +1704,7 @@ const PropertiesPanel: React.FC<PropertiesPanelProps> = ({
|
|
|
1693
1704
|
value={field.style.padding ?? 0}
|
|
1694
1705
|
onChange={(e) => s({ padding: +e.target.value })}
|
|
1695
1706
|
/>
|
|
1696
|
-
<span className={styles.styleUnit}>mm</span>
|
|
1707
|
+
<span className={styles.styleUnit}>{t('templateEditor.mm', 'mm')}</span>
|
|
1697
1708
|
</div>
|
|
1698
1709
|
<div className={styles.styleRow}>
|
|
1699
1710
|
<label className={styles.styleLabel}>
|
|
@@ -1838,9 +1849,9 @@ const PropertiesPanel: React.FC<PropertiesPanelProps> = ({
|
|
|
1838
1849
|
>
|
|
1839
1850
|
<option value="eq">= {tr('printing.editor.opEqual', 'equal')}</option>
|
|
1840
1851
|
<option value="neq">≠ {tr('printing.editor.opNotEqual', 'not equal')}</option>
|
|
1841
|
-
<option value="gt"
|
|
1842
|
-
<option value="lt"
|
|
1843
|
-
<option value="contains">
|
|
1852
|
+
<option value="gt">{t('templateEditor.greaterThan', '> greater than')}</option>
|
|
1853
|
+
<option value="lt">{t('templateEditor.lessThan', '< less than')}</option>
|
|
1854
|
+
<option value="contains">{t('templateEditor.contains', 'contains')}</option>
|
|
1844
1855
|
</select>
|
|
1845
1856
|
</div>
|
|
1846
1857
|
<div className={styles.styleRow}>
|
|
@@ -1906,6 +1917,7 @@ export const NiceTemplateEditor: React.FC<NiceTemplateEditorProps> = ({
|
|
|
1906
1917
|
style,
|
|
1907
1918
|
theme = 'light',
|
|
1908
1919
|
}) => {
|
|
1920
|
+
const { t } = useNiceTranslation();
|
|
1909
1921
|
const [{ past, present: tpl, future }, dispatch] = useReducer(historyReducer, {
|
|
1910
1922
|
past: [],
|
|
1911
1923
|
present: initialTemplate ?? defaultTemplate(),
|
|
@@ -1984,7 +1996,7 @@ export const NiceTemplateEditor: React.FC<NiceTemplateEditorProps> = ({
|
|
|
1984
1996
|
{
|
|
1985
1997
|
id: uid(),
|
|
1986
1998
|
type: 'text',
|
|
1987
|
-
text: 'COPY',
|
|
1999
|
+
text: t('templateEditor.copy', 'COPY'),
|
|
1988
2000
|
opacity: 0.12,
|
|
1989
2001
|
angle: -45,
|
|
1990
2002
|
fontSize: 60,
|
|
@@ -2027,7 +2039,7 @@ export const NiceTemplateEditor: React.FC<NiceTemplateEditorProps> = ({
|
|
|
2027
2039
|
onChange?.({ ...tpl, ...updated });
|
|
2028
2040
|
setSelectedIds(new Set([field.id]));
|
|
2029
2041
|
},
|
|
2030
|
-
[tpl, activeSection, onChange],
|
|
2042
|
+
[tpl, activeSection, onChange, t, tr, update],
|
|
2031
2043
|
);
|
|
2032
2044
|
|
|
2033
2045
|
// -- Drop from left panel --
|
|
@@ -2457,7 +2469,11 @@ export const NiceTemplateEditor: React.FC<NiceTemplateEditorProps> = ({
|
|
|
2457
2469
|
}
|
|
2458
2470
|
>
|
|
2459
2471
|
{/* Canvas area */}
|
|
2460
|
-
<div
|
|
2472
|
+
<div
|
|
2473
|
+
role="presentation"
|
|
2474
|
+
className={styles.canvasArea}
|
|
2475
|
+
onClick={() => setSelectedIds(new Set())}
|
|
2476
|
+
>
|
|
2461
2477
|
{/* Test data bar */}
|
|
2462
2478
|
{Array.isArray(sampleData) && sampleData.length > 0 && (
|
|
2463
2479
|
<div className={styles.testDataBar}>
|
|
@@ -2488,7 +2504,11 @@ export const NiceTemplateEditor: React.FC<NiceTemplateEditorProps> = ({
|
|
|
2488
2504
|
)}
|
|
2489
2505
|
|
|
2490
2506
|
{/* Scrollable paper area */}
|
|
2491
|
-
<div
|
|
2507
|
+
<div
|
|
2508
|
+
role="presentation"
|
|
2509
|
+
className={styles.paperScroll}
|
|
2510
|
+
onClick={(e) => e.stopPropagation()}
|
|
2511
|
+
>
|
|
2492
2512
|
<div className={styles.paperShadow} style={{ width: paperWPx, minHeight: paperHPx }}>
|
|
2493
2513
|
{/* Margin guides */}
|
|
2494
2514
|
<div
|