@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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nice2dev/ui-printing",
3
- "version": "1.2.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.2.0"
50
+ "@nice2dev/ui": "^1.3.0"
51
51
  },
52
52
  "devDependencies": {
53
- "@nice2dev/ui": "^1.2.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 { NiceIcon, useNiceTranslation } from '@nice2dev/ui';
2
- import React, { useState, useMemo, useEffect } from 'react';
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 className={styles.zoomBtn} onClick={() => setZoom((z) => Math.max(0.3, z - 0.1))}>
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 className={styles.zoomBtn} onClick={() => setZoom((z) => Math.min(2.5, z + 0.1))}>
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} · {template.orientation === 'portrait' ? 'Pion' : 'Poziom'}
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
- /* 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';
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
- /* 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 { 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 style={baseStyle} onClick={onClick} onMouseDown={readOnly ? undefined : onDragStart}>
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: 'Szer.', unit: 'mm' },
1413
- { key: 'height' as const, label: 'Wys.', unit: 'mm' },
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">&gt; wieksze</option>
1842
- <option value="lt">&lt; mniejsze</option>
1843
- <option value="contains">zawiera</option>
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 className={styles.canvasArea} onClick={() => setSelectedIds(new Set())}>
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 className={styles.paperScroll} onClick={(e) => e.stopPropagation()}>
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