@reekon-tools/boldr-utils 1.6.27 → 1.6.29

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.
@@ -1,2 +1,5 @@
1
1
  import type { Tool } from '../Tool.js';
2
- export declare const createSelectTool: () => Tool;
2
+ export interface SelectToolOptions {
3
+ panOnEmptyDrag?: boolean;
4
+ }
5
+ export declare const createSelectTool: (options?: SelectToolOptions) => Tool;
@@ -472,7 +472,7 @@ const dragPatch = (s, doc, delta, zoom) => {
472
472
  const op = translatePatch(s.elementKind, s.id, doc, delta);
473
473
  return op ? { ops: [op] } : null;
474
474
  };
475
- export const createSelectTool = () => ({
475
+ export const createSelectTool = (options = {}) => ({
476
476
  id: 'select',
477
477
  label: 'Select',
478
478
  cursor: 'default',
@@ -582,6 +582,11 @@ export const createSelectTool = () => ({
582
582
  const hit = findHit(ctx.document, world, zoom, ctx.tileViewportScale);
583
583
  if (!hit) {
584
584
  ctx.setSelection(null);
585
+ // Nothing under the pointer: with panOnEmptyDrag the gesture pans the
586
+ // viewport instead of dead-ending, so bare drags navigate the canvas.
587
+ if (options.panOnEmptyDrag) {
588
+ return { kind: 'panning', lastScreen: event.screen };
589
+ }
585
590
  return { kind: 'idle' };
586
591
  }
587
592
  // Re-tapping an already-selected text shape (without dragging) opens its
@@ -610,6 +615,13 @@ export const createSelectTool = () => ({
610
615
  },
611
616
  onPointerMove(event, ctx, state) {
612
617
  const s = state;
618
+ if (s?.kind === 'panning') {
619
+ ctx.applyPan({
620
+ x: event.screen.x - s.lastScreen.x,
621
+ y: event.screen.y - s.lastScreen.y,
622
+ });
623
+ return { ...s, lastScreen: event.screen };
624
+ }
613
625
  if (s?.kind !== 'dragging')
614
626
  return s;
615
627
  const delta = {
@@ -6,6 +6,7 @@ export interface UseAnnotationCanvasDocOptions {
6
6
  fileId: string | null;
7
7
  fallbackViewport?: Partial<AnnotationViewport>;
8
8
  debounceMs?: number;
9
+ autoSave?: boolean;
9
10
  createSeed?: {
10
11
  name?: string;
11
12
  fileType?: 'sketch' | 'document';
@@ -32,7 +32,7 @@ const makeClientId = () => `${Date.now().toString(36)}-${Math.floor(Math.random(
32
32
  // no `fileId` was supplied. Composes the existing low-level hooks so it stays
33
33
  // decoupled from any specific Firebase SDK.
34
34
  export const useAnnotationCanvasDoc = (options) => {
35
- const { scope, fileId, fallbackViewport, debounceMs = 800, createSeed, onFileCreated, onSaveError, captureThumbnail, debugLogging = false, } = options;
35
+ const { scope, fileId, fallbackViewport, debounceMs = 800, autoSave = true, createSeed, onFileCreated, onSaveError, captureThumbnail, debugLogging = false, } = options;
36
36
  const { data, loading, error } = useAnnotationDoc(scope, fileId);
37
37
  const { create, update, uploadImage, deleteImage } = useAnnotationMutations(scope ?? EMPTY_SCOPE);
38
38
  const [working, setWorking] = useState(null);
@@ -142,8 +142,12 @@ export const useAnnotationCanvasDoc = (options) => {
142
142
  return;
143
143
  }
144
144
  // A write of ours is queued or in flight — ignore the snapshot; it is
145
- // either the echo of our write or about to be superseded by it.
146
- if (statusRef.current === 'saving' || timerRef.current !== null)
145
+ // either the echo of our write or about to be superseded by it. 'dirty'
146
+ // matters for the explicit-save mode (autoSave false), where local edits
147
+ // sit unsaved indefinitely and must not be clobbered by a remote write.
148
+ if (statusRef.current === 'saving' ||
149
+ statusRef.current === 'dirty' ||
150
+ timerRef.current !== null)
147
151
  return;
148
152
  const incoming = data?.fileData.canvas;
149
153
  if (!incoming)
@@ -292,13 +296,17 @@ export const useAnnotationCanvasDoc = (options) => {
292
296
  return next;
293
297
  });
294
298
  setStatus('dirty');
299
+ // Explicit-save mode: the edit stays local (status 'dirty') until the
300
+ // caller invokes save() — no debounce, no write.
301
+ if (!autoSave)
302
+ return;
295
303
  if (timerRef.current)
296
304
  clearTimeout(timerRef.current);
297
305
  timerRef.current = setTimeout(() => {
298
306
  timerRef.current = null;
299
307
  void flush();
300
308
  }, debounceMs);
301
- }, [debounceMs, flush, setStatus]);
309
+ }, [autoSave, debounceMs, flush, setStatus]);
302
310
  // Explicit save (Save button, "done" actions): also refreshes the file's
303
311
  // thumbnail, which autosaves deliberately skip. Resolves only after the
304
312
  // capture+upload too, so a caller may unmount the view right after.
package/dist/exports.d.ts CHANGED
@@ -8,6 +8,7 @@ export * from './types/layout.js';
8
8
  export * from './types/annotation.js';
9
9
  export { getToleranceColor, calculateDeviationPercentage, isWithinTolerance, generateToleranceGradient, createDefaultToleranceThresholds, DEFAULT_TOLERANCE_COLORS, type ToleranceThreshold, type ToleranceConfig, } from './utils/tolerance.js';
10
10
  export { DEFAULT_GROUP_INDEX, isDefaultGroup, findDefaultGroup, } from './utils/groups.js';
11
+ export { numberToLetterIndex, formGroupInputLabel, listGroupMeasurementLabel, } from './utils/indexLabels.js';
11
12
  export { isFieldOp, type AnnotationDataProvider, type AnnotationFile, type AnnotationFileSummary, type FieldOp, type ImageBlob, type JobGroupScope, type JobScope, type Patch, type Unsubscribe, type UploadedImageRef, } from './annotation/data/AnnotationDataProvider.js';
12
13
  export { AnnotationDataProviderContext, useAnnotationData, type AnnotationDataProviderProps, } from './annotation/data/AnnotationDataContext.js';
13
14
  export { useAnnotationDoc, type UseAnnotationDocResult, } from './annotation/data/hooks/useAnnotationDoc.js';
@@ -24,7 +25,7 @@ export type { MeasurementStampRenderArgs, RenderMeasurementStamp, } from './anno
24
25
  export { STAMP_TILE_SIZE, DEFAULT_TILE_SCALE, TILE_SCALE_MIN, TILE_SCALE_MAX, clampTileScale, stampTileSize, isUnassociatedStamp, } from './annotation/canvas/stampLayout.js';
25
26
  export { createViewportApi, fitToScreen, fitRectToScreen, imageDocRect, panBy, zoomAt, DEFAULT_VIEWPORT, type ViewportApi, type ViewportState, } from './annotation/canvas/viewport.js';
26
27
  export { createPenTool, type PenToolOptions, } from './annotation/canvas/tools/penTool.js';
27
- export { createSelectTool } from './annotation/canvas/tools/selectTool.js';
28
+ export { createSelectTool, type SelectToolOptions, } from './annotation/canvas/tools/selectTool.js';
28
29
  export { createMeasurementStampTool, type MeasurementStampToolOptions, } from './annotation/canvas/tools/measurementStampTool.js';
29
30
  export { createPanTool, type PanToolOptions, } from './annotation/canvas/tools/panTool.js';
30
31
  export { createTextTool, type TextToolOptions, } from './annotation/canvas/tools/textTool.js';
package/dist/exports.js CHANGED
@@ -12,6 +12,7 @@ export * from './types/layout.js';
12
12
  export * from './types/annotation.js';
13
13
  export { getToleranceColor, calculateDeviationPercentage, isWithinTolerance, generateToleranceGradient, createDefaultToleranceThresholds, DEFAULT_TOLERANCE_COLORS, } from './utils/tolerance.js';
14
14
  export { DEFAULT_GROUP_INDEX, isDefaultGroup, findDefaultGroup, } from './utils/groups.js';
15
+ export { numberToLetterIndex, formGroupInputLabel, listGroupMeasurementLabel, } from './utils/indexLabels.js';
15
16
  // Annotation data layer (SDK-neutral; apps provide their own provider).
16
17
  export { isFieldOp, } from './annotation/data/AnnotationDataProvider.js';
17
18
  export { AnnotationDataProviderContext, useAnnotationData, } from './annotation/data/AnnotationDataContext.js';
@@ -24,7 +25,7 @@ export { InMemoryAnnotationProvider } from './annotation/data/InMemoryAnnotation
24
25
  export { STAMP_TILE_SIZE, DEFAULT_TILE_SCALE, TILE_SCALE_MIN, TILE_SCALE_MAX, clampTileScale, stampTileSize, isUnassociatedStamp, } from './annotation/canvas/stampLayout.js';
25
26
  export { createViewportApi, fitToScreen, fitRectToScreen, imageDocRect, panBy, zoomAt, DEFAULT_VIEWPORT, } from './annotation/canvas/viewport.js';
26
27
  export { createPenTool, } from './annotation/canvas/tools/penTool.js';
27
- export { createSelectTool } from './annotation/canvas/tools/selectTool.js';
28
+ export { createSelectTool, } from './annotation/canvas/tools/selectTool.js';
28
29
  export { createMeasurementStampTool, } from './annotation/canvas/tools/measurementStampTool.js';
29
30
  export { createPanTool, } from './annotation/canvas/tools/panTool.js';
30
31
  export { createTextTool, } from './annotation/canvas/tools/textTool.js';
@@ -0,0 +1,3 @@
1
+ export declare const numberToLetterIndex: (index: number) => string;
2
+ export declare const formGroupInputLabel: (columnIndex: number, groupIndex: number) => string;
3
+ export declare const listGroupMeasurementLabel: (deviceMeasurementNumber: number | null | undefined, groupIndex: number) => string;
@@ -0,0 +1,40 @@
1
+ // Canonical measurement / input index labels, shared by rock-desktop and
2
+ // rock-pro-app so every surface (tables, forms, PDF tile overlays, mobile
3
+ // lists) renders the same identifier for the same datum.
4
+ //
5
+ // Two schemes exist:
6
+ //
7
+ // - FORM groups: inputs are identified positionally — the column's index in
8
+ // the section's tableConfig as a letter, plus the group's 1-based number.
9
+ // First column in the second group → "A2"; fifth column in the third
10
+ // group → "E3".
11
+ //
12
+ // - LIST groups: entries are always measurements, identified by their stable
13
+ // deviceMeasurementNumber (capture sequence) as the letter, plus the same
14
+ // group number. deviceMeasurementNumber 4 in the second group → "E2".
15
+ // Legacy measurements without a deviceMeasurementNumber fall back to the
16
+ // bare group number ("2") — positional letters would shift as the list
17
+ // re-sorts, so no letter is shown.
18
+ // 0-based index → spreadsheet-style letters: 0 → "A", 25 → "Z", 26 → "AA".
19
+ // Negative / non-finite input yields ''.
20
+ export const numberToLetterIndex = (index) => {
21
+ if (!Number.isFinite(index) || index < 0)
22
+ return '';
23
+ let n = Math.floor(index) + 1;
24
+ let result = '';
25
+ while (n > 0) {
26
+ n--;
27
+ result = String.fromCharCode((n % 26) + 65) + result;
28
+ n = Math.floor(n / 26);
29
+ }
30
+ return result;
31
+ };
32
+ // Label for a FORM-group input: column position (0-based) + group number.
33
+ // formGroupInputLabel(0, 1) → "A2"
34
+ export const formGroupInputLabel = (columnIndex, groupIndex) => `${numberToLetterIndex(columnIndex)}${groupIndex + 1}`;
35
+ // Label for a LIST-group measurement: deviceMeasurementNumber (0-based) +
36
+ // group number, or the bare group number when the measurement has no device
37
+ // number. listGroupMeasurementLabel(4, 1) → "E2"; (null, 1) → "2".
38
+ export const listGroupMeasurementLabel = (deviceMeasurementNumber, groupIndex) => deviceMeasurementNumber != null && deviceMeasurementNumber >= 0
39
+ ? `${numberToLetterIndex(deviceMeasurementNumber)}${groupIndex + 1}`
40
+ : `${groupIndex + 1}`;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@reekon-tools/boldr-utils",
3
- "version": "1.6.27",
3
+ "version": "1.6.29",
4
4
  "description": "Shared utilities for formulas and measurement conversion used in Reekon apps",
5
5
  "author": "REEKON Tools",
6
6
  "license": "MIT",