kviewer 0.2.0 → 0.2.2

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 (114) hide show
  1. package/README.md +47 -0
  2. package/dist/module.d.mts +8 -1
  3. package/dist/module.json +1 -1
  4. package/dist/module.mjs +25 -3
  5. package/dist/runtime/annotation/engine/canvas-resolution.d.ts +7 -0
  6. package/dist/runtime/annotation/engine/canvas-resolution.js +21 -0
  7. package/dist/runtime/annotation/engine/editor/editor.d.ts +5 -0
  8. package/dist/runtime/annotation/engine/editor/editor.js +14 -0
  9. package/dist/runtime/annotation/engine/editor/selector.d.ts +13 -1
  10. package/dist/runtime/annotation/engine/editor/selector.js +60 -8
  11. package/dist/runtime/annotation/engine/painter.d.ts +31 -0
  12. package/dist/runtime/annotation/engine/painter.js +88 -2
  13. package/dist/runtime/annotation/engine/store.js +3 -0
  14. package/dist/runtime/annotation/engine/tools/arrow.js +0 -1
  15. package/dist/runtime/annotation/engine/tools/circle.js +0 -2
  16. package/dist/runtime/annotation/engine/tools/free-highlight.js +0 -2
  17. package/dist/runtime/annotation/engine/tools/freehand.js +0 -2
  18. package/dist/runtime/annotation/engine/tools/rectangle.js +0 -2
  19. package/dist/runtime/annotation/engine/types.d.ts +5 -0
  20. package/dist/runtime/annotation/font-style.d.ts +15 -0
  21. package/dist/runtime/annotation/font-style.js +19 -0
  22. package/dist/runtime/annotation/parsers/extractCheckboxStyles.d.ts +8 -0
  23. package/dist/runtime/annotation/parsers/extractCheckboxStyles.js +24 -1
  24. package/dist/runtime/annotation/pdf-export/annotation-flags.d.ts +8 -0
  25. package/dist/runtime/annotation/pdf-export/annotation-flags.js +5 -0
  26. package/dist/runtime/annotation/pdf-export/export-form-fields.js +3 -1
  27. package/dist/runtime/annotation/pdf-export/export.js +13 -1
  28. package/dist/runtime/annotation/pdf-export/parse_circle.js +3 -0
  29. package/dist/runtime/annotation/pdf-export/parse_freetext.js +31 -19
  30. package/dist/runtime/annotation/pdf-export/parse_highlight.js +3 -0
  31. package/dist/runtime/annotation/pdf-export/parse_ink.js +5 -1
  32. package/dist/runtime/annotation/pdf-export/parse_line.js +5 -1
  33. package/dist/runtime/annotation/pdf-export/parse_polyline.js +5 -1
  34. package/dist/runtime/annotation/pdf-export/parse_square.js +3 -0
  35. package/dist/runtime/annotation/pdf-export/parse_stamp.js +4 -2
  36. package/dist/runtime/annotation/pdf-export/parse_strikeout.js +3 -0
  37. package/dist/runtime/annotation/pdf-export/parse_text.js +3 -0
  38. package/dist/runtime/annotation/pdf-export/parse_underline.js +3 -0
  39. package/dist/runtime/annotation/pdf-export/unicode-font.d.ts +5 -0
  40. package/dist/runtime/annotation/pdf-export/unicode-font.js +31 -0
  41. package/dist/runtime/annotation/pdf-export/winansi.d.ts +17 -0
  42. package/dist/runtime/annotation/pdf-export/winansi.js +77 -0
  43. package/dist/runtime/annotation/pdf-import/decode_circle.js +0 -1
  44. package/dist/runtime/annotation/pdf-import/decode_ink.js +0 -1
  45. package/dist/runtime/annotation/pdf-import/decode_line.js +0 -1
  46. package/dist/runtime/annotation/pdf-import/decode_square.js +0 -1
  47. package/dist/runtime/annotation/pdf-import/extract_stamp_appearance.d.ts +12 -0
  48. package/dist/runtime/annotation/pdf-import/extract_stamp_appearance.js +17 -4
  49. package/dist/runtime/annotation/pdf-import/types.d.ts +2 -0
  50. package/dist/runtime/assets/fonts/LICENSE-LiberationSans +102 -0
  51. package/dist/runtime/assets/fonts/LiberationSans-Regular.ttf +0 -0
  52. package/dist/runtime/assets/kviewer.css +1 -1
  53. package/dist/runtime/components/AnnotationToolbar.vue +6 -4
  54. package/dist/runtime/components/PdfPage.d.vue.ts +16 -0
  55. package/dist/runtime/components/PdfPage.vue +170 -9
  56. package/dist/runtime/components/PdfPage.vue.d.ts +16 -0
  57. package/dist/runtime/components/PerfHud.d.vue.ts +3 -0
  58. package/dist/runtime/components/PerfHud.vue +97 -0
  59. package/dist/runtime/components/PerfHud.vue.d.ts +3 -0
  60. package/dist/runtime/components/ToolButton.vue +4 -1
  61. package/dist/runtime/components/Viewer.d.vue.ts +37 -2
  62. package/dist/runtime/components/Viewer.vue +208 -13
  63. package/dist/runtime/components/Viewer.vue.d.ts +37 -2
  64. package/dist/runtime/components/ViewerBar.d.vue.ts +21 -1
  65. package/dist/runtime/components/ViewerBar.vue +183 -101
  66. package/dist/runtime/components/ViewerBar.vue.d.ts +21 -1
  67. package/dist/runtime/components/ViewerTabs.d.vue.ts +10 -2
  68. package/dist/runtime/components/ViewerTabs.vue +13 -2
  69. package/dist/runtime/components/ViewerTabs.vue.d.ts +10 -2
  70. package/dist/runtime/components/form-fields/FormDropdown.vue +50 -19
  71. package/dist/runtime/components/form-fields/FormSignatureField.vue +4 -2
  72. package/dist/runtime/components/form-fields/FormTextField.vue +7 -18
  73. package/dist/runtime/components/modals/FreeTextModal.vue +6 -4
  74. package/dist/runtime/components/modals/SignatureDrawModal.vue +6 -4
  75. package/dist/runtime/components/panels/PlacedFieldSidebar.vue +67 -50
  76. package/dist/runtime/components/panels/SignaturePicker.vue +6 -4
  77. package/dist/runtime/components/panels/StampPicker.vue +3 -1
  78. package/dist/runtime/components/tools/AnnotationToolButton.d.vue.ts +10 -0
  79. package/dist/runtime/components/tools/AnnotationToolButton.vue +33 -0
  80. package/dist/runtime/components/tools/AnnotationToolButton.vue.d.ts +10 -0
  81. package/dist/runtime/components/tools/MarqueeTool.vue +3 -1
  82. package/dist/runtime/components/tools/PageSettings.vue +13 -11
  83. package/dist/runtime/components/tools/SearchTool.vue +12 -10
  84. package/dist/runtime/components/tools/ToolProperties.vue +7 -5
  85. package/dist/runtime/components/tools/ToolbarMenu.d.vue.ts +9 -0
  86. package/dist/runtime/components/tools/ToolbarMenu.vue +79 -0
  87. package/dist/runtime/components/tools/ToolbarMenu.vue.d.ts +9 -0
  88. package/dist/runtime/components/tools/ZoomControls.vue +17 -9
  89. package/dist/runtime/composables/canvas-limits.d.ts +22 -0
  90. package/dist/runtime/composables/canvas-limits.js +10 -0
  91. package/dist/runtime/composables/useFormFields.d.ts +6 -0
  92. package/dist/runtime/composables/useFormFields.js +28 -0
  93. package/dist/runtime/composables/useInertiaPanzoom.d.ts +10 -0
  94. package/dist/runtime/composables/useInertiaPanzoom.js +57 -10
  95. package/dist/runtime/composables/useKviewerI18n.d.ts +18 -0
  96. package/dist/runtime/composables/useKviewerI18n.js +29 -0
  97. package/dist/runtime/composables/usePageVirtualization.d.ts +11 -0
  98. package/dist/runtime/composables/usePageVirtualization.js +40 -0
  99. package/dist/runtime/composables/useScriptingManager.d.ts +8 -4
  100. package/dist/runtime/composables/useScriptingManager.js +2 -0
  101. package/dist/runtime/composables/useViewerState.d.ts +4 -0
  102. package/dist/runtime/composables/useViewerState.js +8 -0
  103. package/dist/runtime/embed/bridge-client.d.ts +9 -1
  104. package/dist/runtime/embed/bridge-client.js +13 -0
  105. package/dist/runtime/embed/bridge-host.d.ts +3 -0
  106. package/dist/runtime/embed/bridge-host.js +30 -8
  107. package/dist/runtime/embed/protocol.d.ts +21 -0
  108. package/dist/runtime/i18n/messages.d.ts +252 -0
  109. package/dist/runtime/i18n/messages.js +249 -0
  110. package/dist/runtime/public-types.d.ts +2 -0
  111. package/dist/runtime/toolbar-tools.d.ts +54 -0
  112. package/dist/runtime/toolbar-tools.js +79 -0
  113. package/dist/types.d.mts +1 -1
  114. package/package.json +14 -31
@@ -5,7 +5,7 @@
5
5
  :variant="search.isOpen.value ? 'soft' : 'ghost'"
6
6
  color="neutral"
7
7
  size="xs"
8
- title="Search"
8
+ :title="t('search')"
9
9
  data-testid="search-toggle"
10
10
  @click="toggleOpen"
11
11
  />
@@ -17,7 +17,7 @@
17
17
  <UInput
18
18
  ref="queryInputRef"
19
19
  :model-value="search.query.value"
20
- placeholder="Find in document..."
20
+ :placeholder="t('searchPlaceholder')"
21
21
  variant="outline"
22
22
  color="neutral"
23
23
  size="xs"
@@ -37,7 +37,7 @@
37
37
  color="neutral"
38
38
  size="xs"
39
39
  :disabled="!hasMatches"
40
- title="Previous match"
40
+ :title="t('previousMatch')"
41
41
  @click="search.findPrevious"
42
42
  />
43
43
  <UButton
@@ -46,7 +46,7 @@
46
46
  color="neutral"
47
47
  size="xs"
48
48
  :disabled="!hasMatches"
49
- title="Next match"
49
+ :title="t('nextMatch')"
50
50
  @click="search.findNext"
51
51
  />
52
52
 
@@ -54,7 +54,7 @@
54
54
  :variant="search.matchCase.value ? 'soft' : 'ghost'"
55
55
  color="neutral"
56
56
  size="xs"
57
- title="Match Case"
57
+ :title="t('matchCase')"
58
58
  @click="search.toggleMatchCase"
59
59
  >
60
60
  Aa
@@ -63,7 +63,7 @@
63
63
  :variant="search.wholeWord.value ? 'soft' : 'ghost'"
64
64
  color="neutral"
65
65
  size="xs"
66
- title="Whole Word"
66
+ :title="t('wholeWord')"
67
67
  @click="search.toggleWholeWord"
68
68
  >
69
69
  W
@@ -72,7 +72,7 @@
72
72
  :variant="search.highlightAll.value ? 'soft' : 'ghost'"
73
73
  color="neutral"
74
74
  size="xs"
75
- title="Highlight All"
75
+ :title="t('highlightAll')"
76
76
  @click="search.toggleHighlightAll"
77
77
  >
78
78
  All
@@ -83,7 +83,7 @@
83
83
  variant="ghost"
84
84
  color="neutral"
85
85
  size="xs"
86
- title="Close search"
86
+ :title="t('closeSearch')"
87
87
  @click="search.close"
88
88
  />
89
89
  </div>
@@ -93,12 +93,14 @@
93
93
  <script setup>
94
94
  import { computed, nextTick, onMounted, ref, watch } from "vue";
95
95
  import { useViewerSearch } from "../../composables/useViewerSearch";
96
+ import { useKviewerI18n } from "../../composables/useKviewerI18n";
96
97
  const search = useViewerSearch();
98
+ const { t } = useKviewerI18n();
97
99
  const queryInputRef = ref(null);
98
100
  const hasMatches = computed(() => search.totalMatches.value > 0);
99
101
  const statusLabel = computed(() => {
100
- if (!search.query.value) return "Type to search";
101
- if (search.notFound.value) return "No results";
102
+ if (!search.query.value) return t("typeToSearch");
103
+ if (search.notFound.value) return t("noResults");
102
104
  return `${search.currentMatch.value}/${search.totalMatches.value}`;
103
105
  });
104
106
  function resolveInputElement() {
@@ -40,7 +40,7 @@
40
40
  <UPopover v-model:open="colorPopoverOpen">
41
41
  <button
42
42
  class="flex items-center justify-center w-7 h-7 rounded-md hover:bg-default cursor-pointer"
43
- title="Color"
43
+ :title="t('color')"
44
44
  >
45
45
  <span
46
46
  class="w-4 h-4 rounded-full border border-white/30"
@@ -67,7 +67,7 @@
67
67
  <UPopover v-if="activeType === AnnotationType.FREEHAND">
68
68
  <button
69
69
  class="flex items-center justify-center h-7 px-1.5 gap-0.5 rounded-md hover:bg-default cursor-pointer"
70
- title="Stroke width"
70
+ :title="t('strokeWidth')"
71
71
  >
72
72
  <UIcon name="i-lucide-pencil" class="text-sm text-muted" />
73
73
  <span class="text-[10px] text-muted tabular-nums">{{ freehandStrokeWidth }}</span>
@@ -77,7 +77,7 @@
77
77
  <button
78
78
  class="flex items-center justify-center w-8 h-8 rounded-md hover:bg-default cursor-pointer disabled:opacity-40 disabled:cursor-not-allowed"
79
79
  :disabled="freehandStrokeWidth <= 1"
80
- title="Decrease stroke width"
80
+ :title="t('decreaseStrokeWidth')"
81
81
  @click="decrementStrokeWidth"
82
82
  >
83
83
  <UIcon name="i-lucide-minus" class="text-sm" />
@@ -88,7 +88,7 @@
88
88
  <button
89
89
  class="flex items-center justify-center w-8 h-8 rounded-md hover:bg-default cursor-pointer disabled:opacity-40 disabled:cursor-not-allowed"
90
90
  :disabled="freehandStrokeWidth >= 24"
91
- title="Increase stroke width"
91
+ :title="t('increaseStrokeWidth')"
92
92
  @click="incrementStrokeWidth"
93
93
  >
94
94
  <UIcon name="i-lucide-plus" class="text-sm" />
@@ -102,7 +102,7 @@
102
102
  <template v-else>
103
103
  <div class="flex items-center gap-1.5 text-xs text-muted">
104
104
  <UIcon name="i-lucide-sliders-horizontal" class="text-sm" />
105
- <span>No properties</span>
105
+ <span>{{ t("noProperties") }}</span>
106
106
  </div>
107
107
  </template>
108
108
  </div>
@@ -113,8 +113,10 @@ import { computed, ref } from "vue";
113
113
  import { defaultOptions, annotationDefinitions } from "../../annotation/engine/config";
114
114
  import { AnnotationType } from "../../annotation/engine/types";
115
115
  import { useViewerState } from "../../composables/useViewerState";
116
+ import { useKviewerI18n } from "../../composables/useKviewerI18n";
116
117
  const emit = defineEmits(["styleUpdated"]);
117
118
  const state = useViewerState();
119
+ const { t } = useKviewerI18n();
118
120
  const colors = defaultOptions.colors;
119
121
  const styleVersion = ref(0);
120
122
  const colorPopoverOpen = ref(false);
@@ -0,0 +1,9 @@
1
+ import type { ViewerMenuItem } from '../../menu-items.js';
2
+ type __VLS_Props = {
3
+ /** Extra items appended to the burger menu after the built-ins.
4
+ * See {@link ViewerMenuItem}. Pure forwarding from `<KViewer>`. */
5
+ menuItems?: ViewerMenuItem[];
6
+ };
7
+ declare const __VLS_export: import("vue").DefineComponent<__VLS_Props, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {}, string, import("vue").PublicProps, Readonly<__VLS_Props> & Readonly<{}>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
8
+ declare const _default: typeof __VLS_export;
9
+ export default _default;
@@ -0,0 +1,79 @@
1
+ <template>
2
+ <UPopover>
3
+ <UButton icon="i-lucide-menu" variant="ghost" color="neutral" size="xs" data-testid="viewer-menu" />
4
+ <template #content>
5
+ <div class="p-1 flex flex-col gap-0.5">
6
+ <UButton
7
+ icon="i-lucide-download"
8
+ :label="t('download')"
9
+ variant="ghost"
10
+ color="neutral"
11
+ size="xs"
12
+ class="w-full justify-start"
13
+ data-testid="viewer-download"
14
+ @click="onDownload"
15
+ />
16
+ <UButton
17
+ :icon="state.shapeDetection.value ? 'i-lucide-square-check-big' : 'i-lucide-square-dashed'"
18
+ :label="state.shapeDetection.value ? t('disableFormFieldDetection') : t('detectFormFields')"
19
+ :variant="state.shapeDetection.value ? 'soft' : 'ghost'"
20
+ :color="state.shapeDetection.value ? 'primary' : 'neutral'"
21
+ size="xs"
22
+ class="w-full justify-start"
23
+ data-testid="viewer-toggle-shape-detection"
24
+ @click="state.shapeDetection.value = !state.shapeDetection.value"
25
+ />
26
+ <!-- Host-supplied menu items appended after the built-ins.
27
+ A small separator visually marks the boundary; we only
28
+ render it when there is at least one custom item. -->
29
+ <template v-if="props.menuItems && props.menuItems.length > 0">
30
+ <div class="my-1 border-t border-default" />
31
+ <template v-for="item in props.menuItems" :key="item.key">
32
+ <div v-if="item.type === 'separator'" class="my-1 border-t border-default" />
33
+ <UButton
34
+ v-else-if="item.type === 'button'"
35
+ :icon="item.icon"
36
+ :label="item.label"
37
+ :disabled="item.disabled"
38
+ variant="ghost"
39
+ color="neutral"
40
+ size="xs"
41
+ class="w-full justify-start"
42
+ :data-testid="`viewer-menu-${item.key}`"
43
+ @click="item.onSelect()"
44
+ />
45
+ <UButton
46
+ v-else-if="item.type === 'checkbox'"
47
+ :icon="item.icon ?? (item.checked ? 'i-lucide-check-square' : 'i-lucide-square')"
48
+ :label="item.label"
49
+ :variant="item.checked ? 'soft' : 'ghost'"
50
+ :color="item.checked ? 'primary' : 'neutral'"
51
+ :disabled="item.disabled"
52
+ size="xs"
53
+ class="w-full justify-start"
54
+ :data-testid="`viewer-menu-${item.key}`"
55
+ @click="item.onUpdate(!item.checked)"
56
+ />
57
+ </template>
58
+ </template>
59
+ </div>
60
+ </template>
61
+ </UPopover>
62
+ </template>
63
+
64
+ <script setup>
65
+ import { useViewerState } from "../../composables/useViewerState";
66
+ import { useKviewerI18n } from "../../composables/useKviewerI18n";
67
+ const props = defineProps({
68
+ menuItems: { type: Array, required: false }
69
+ });
70
+ const state = useViewerState();
71
+ const { t } = useKviewerI18n();
72
+ async function onDownload() {
73
+ try {
74
+ await state.downloadPdf();
75
+ } catch (error) {
76
+ console.error("Failed to export PDF from menu action", error);
77
+ }
78
+ }
79
+ </script>
@@ -0,0 +1,9 @@
1
+ import type { ViewerMenuItem } from '../../menu-items.js';
2
+ type __VLS_Props = {
3
+ /** Extra items appended to the burger menu after the built-ins.
4
+ * See {@link ViewerMenuItem}. Pure forwarding from `<KViewer>`. */
5
+ menuItems?: ViewerMenuItem[];
6
+ };
7
+ declare const __VLS_export: import("vue").DefineComponent<__VLS_Props, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {}, string, import("vue").PublicProps, Readonly<__VLS_Props> & Readonly<{}>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
8
+ declare const _default: typeof __VLS_export;
9
+ export default _default;
@@ -32,19 +32,27 @@
32
32
  <script setup>
33
33
  import { computed } from "vue";
34
34
  import { useViewerState } from "../../composables/useViewerState";
35
+ import { useKviewerI18n } from "../../composables/useKviewerI18n";
35
36
  const state = useViewerState();
36
- const fitModeItems = [
37
- { label: "Fit Width", value: "fit-width" },
38
- { label: "Fit Page", value: "fit-page" }
39
- ];
40
- const scaleItems = computed(
41
- () => state.scaleOptions.map((s) => ({
37
+ const { t } = useKviewerI18n();
38
+ const fitModeItems = computed(() => [
39
+ { label: t("fitWidth"), value: "fit-width" },
40
+ { label: t("fitPage"), value: "fit-page" }
41
+ ]);
42
+ const scaleItems = computed(() => {
43
+ const options = [...state.scaleOptions];
44
+ const current = state.scale.value;
45
+ if (state.fitMode.value === "custom" && !options.includes(current)) {
46
+ options.push(current);
47
+ options.sort((a, b) => a - b);
48
+ }
49
+ return options.map((s) => ({
42
50
  label: `${Math.round(s * 100)}%`,
43
51
  value: `scale:${s}`
44
- }))
45
- );
52
+ }));
53
+ });
46
54
  const zoomItems = computed(() => [
47
- ...fitModeItems,
55
+ ...fitModeItems.value,
48
56
  { label: "---", value: "_separator", disabled: true },
49
57
  ...scaleItems.value
50
58
  ]);
@@ -0,0 +1,22 @@
1
+ /**
2
+ * Canvas backing-store limits.
3
+ *
4
+ * iOS Safari rejects canvases above ~16.7M pixels — the allocation fails
5
+ * SILENTLY and the page renders blank. Large PDF pages on iPad (DPR 2,
6
+ * supersampling) cross that limit at moderate zoom. Other engines allow
7
+ * far larger canvases; 64M keeps memory bounded without visibly capping
8
+ * desktop zoom. pdfjs's own viewer applies the same strategy via its
9
+ * `maxCanvasPixels` option (default 2^24 ≈ 16.7M).
10
+ */
11
+ export declare const IOS_MAX_CANVAS_PIXELS = 16000000;
12
+ export declare const DEFAULT_MAX_CANVAS_PIXELS = 64000000;
13
+ /**
14
+ * Clamp a desired render scale so the resulting backing store stays under
15
+ * `maxPixels`. The canvas's CSS size is set independently, so clamping
16
+ * only reduces sharpness at extreme zoom instead of blanking the page.
17
+ *
18
+ * @param desiredScale page-to-canvas scale (zoom × quality × pixel ratio)
19
+ * @param baseWidth page width at scale 1
20
+ * @param baseHeight page height at scale 1
21
+ */
22
+ export declare function clampRenderScale(desiredScale: number, baseWidth: number, baseHeight: number, maxPixels: number): number;
@@ -0,0 +1,10 @@
1
+ export const IOS_MAX_CANVAS_PIXELS = 16e6;
2
+ export const DEFAULT_MAX_CANVAS_PIXELS = 64e6;
3
+ export function clampRenderScale(desiredScale, baseWidth, baseHeight, maxPixels) {
4
+ const basePixels = baseWidth * baseHeight;
5
+ if (basePixels <= 0) return desiredScale;
6
+ if (basePixels * desiredScale * desiredScale <= maxPixels) {
7
+ return desiredScale;
8
+ }
9
+ return Math.sqrt(maxPixels / basePixels);
10
+ }
@@ -62,6 +62,12 @@ export interface FormFieldsState {
62
62
  * PDF) to existing parsed button defs and store the map for any defs
63
63
  * parsed afterwards. Keyed by button fieldName. */
64
64
  applyButtonCaptions: (captions: Map<string, string>) => void;
65
+ /** Mark parsed signature defs whose source-PDF field is already
66
+ * digitally signed (`/V` holds a `/Sig` dict — extracted via the
67
+ * pdf-lib side-channel; pdf.js nulls a signature widget's fieldValue
68
+ * so the signed state is invisible in its annotation objects). Keyed
69
+ * by fully qualified field name. */
70
+ applySignedSignatures: (names: Set<string>) => void;
65
71
  /** Whether this field is locked by another (signed) signature field's
66
72
  * lock rule. Reactive to both definitions and values, so a UI computed
67
73
  * that calls this re-runs when any signature is signed or its lock
@@ -37,6 +37,7 @@ export function provideFormFields() {
37
37
  }
38
38
  let checkboxStyleMap = null;
39
39
  let buttonCaptionMap = null;
40
+ let signedSignatureNames = null;
40
41
  function applyCheckboxStyles(styles) {
41
42
  checkboxStyleMap = styles;
42
43
  let changed = false;
@@ -77,6 +78,24 @@ export function provideFormFields() {
77
78
  }
78
79
  if (changed) triggerRef(fieldDefinitions);
79
80
  }
81
+ function applySignedSignatures(names) {
82
+ signedSignatureNames = names;
83
+ let changed = false;
84
+ for (const [page, defs] of fieldDefinitions.value.entries()) {
85
+ let pageChanged = false;
86
+ const next = defs.map((d) => {
87
+ if (d.fieldType !== "signature" || d.origin !== "parsed") return d;
88
+ if (d.signedInSource || !names.has(d.fieldName)) return d;
89
+ pageChanged = true;
90
+ return { ...d, signedInSource: true };
91
+ });
92
+ if (pageChanged) {
93
+ fieldDefinitions.value.set(page, next);
94
+ changed = true;
95
+ }
96
+ }
97
+ if (changed) triggerRef(fieldDefinitions);
98
+ }
80
99
  function parsePageFields(pageNumber, rawAnnotations) {
81
100
  if (fieldDefinitions.value.has(pageNumber)) return;
82
101
  const defs = parseFormFields(rawAnnotations, pageNumber);
@@ -99,6 +118,13 @@ export function provideFormFields() {
99
118
  if (caption) def.buttonLabel = caption;
100
119
  }
101
120
  }
121
+ if (signedSignatureNames) {
122
+ for (const def of defs) {
123
+ if (def.fieldType === "signature" && signedSignatureNames.has(def.fieldName)) {
124
+ def.signedInSource = true;
125
+ }
126
+ }
127
+ }
102
128
  fieldDefinitions.value.set(pageNumber, defs);
103
129
  triggerRef(fieldDefinitions);
104
130
  for (const def of defs) {
@@ -464,6 +490,7 @@ export function provideFormFields() {
464
490
  selectedPlacedFieldId.value = null;
465
491
  checkboxStyleMap = null;
466
492
  buttonCaptionMap = null;
493
+ signedSignatureNames = null;
467
494
  valueWriteSink = null;
468
495
  triggerRef(fieldDefinitions);
469
496
  triggerRef(fieldValues);
@@ -489,6 +516,7 @@ export function provideFormFields() {
489
516
  selectPlacedField,
490
517
  applyCheckboxStyles,
491
518
  applyButtonCaptions,
519
+ applySignedSignatures,
492
520
  isFieldLocked,
493
521
  resetValues,
494
522
  reset,
@@ -36,5 +36,15 @@ export interface InertiaPanzoomHandle {
36
36
  setGesturesEnabled: (enabled: boolean) => void;
37
37
  /** Subscribe to transform updates. Returns an unsubscribe function. */
38
38
  onChange: (cb: (t: Transform) => void) => () => void;
39
+ /** Subscribe to the end of a zoom gesture (pinch lifted / ctrl+wheel idle).
40
+ * Fires with the current visual scale so the host can commit it into the
41
+ * PDF render scale. Returns an unsubscribe function. */
42
+ onZoomEnd: (cb: (scale: number) => void) => () => void;
43
+ /** Replace min/max visual scale at runtime (e.g. to cap the *effective*
44
+ * zoom when the committed render scale changes). */
45
+ setScaleLimits: (min: number, max: number) => void;
46
+ /** Set the transform directly (translate clamped to bounds). In-flight
47
+ * gestures are rebased so they continue from the new transform. */
48
+ setTransform: (t: Partial<Transform>) => void;
39
49
  }
40
50
  export declare function useInertiaPanzoom(options?: InertiaPanzoomOptions): InertiaPanzoomHandle;
@@ -6,11 +6,13 @@ const BOUNCE_VELOCITY_DECAY = 0.04;
6
6
  const MIN_VELOCITY = 0.01;
7
7
  export function useInertiaPanzoom(options = {}) {
8
8
  const {
9
- minScale = 0.5,
10
- maxScale = 4,
11
9
  axisLockThreshold = 10,
12
10
  axisLockRatio = 2
13
11
  } = options;
12
+ let minScale = options.minScale ?? 0.5;
13
+ let maxScale = options.maxScale ?? 4;
14
+ const subscribers = /* @__PURE__ */ new Set();
15
+ const zoomEndSubscribers = /* @__PURE__ */ new Set();
14
16
  let controller = null;
15
17
  function attach(target) {
16
18
  detach();
@@ -30,7 +32,11 @@ export function useInertiaPanzoom(options = {}) {
30
32
  let vy = 0;
31
33
  let momentumRaf = 0;
32
34
  let lastStepTime = 0;
33
- const subscribers = /* @__PURE__ */ new Set();
35
+ let pinchZoomed = false;
36
+ let wheelZoomEndTimer = 0;
37
+ function fireZoomEnd() {
38
+ zoomEndSubscribers.forEach((cb) => cb(scale));
39
+ }
34
40
  target.style.transformOrigin = "0 0";
35
41
  target.style.touchAction = "none";
36
42
  parent.style.touchAction = "none";
@@ -320,6 +326,7 @@ export function useInertiaPanzoom(options = {}) {
320
326
  const focalY = (pinchStart.midY - rect.top - pinchStart.ty) / pinchStart.s;
321
327
  tx = midX - rect.left - newScale * focalX;
322
328
  ty = midY - rect.top - newScale * focalY;
329
+ if (newScale !== scale) pinchZoomed = true;
323
330
  scale = newScale;
324
331
  apply();
325
332
  } else if (pointers.size === 1 && panStart) {
@@ -352,7 +359,7 @@ export function useInertiaPanzoom(options = {}) {
352
359
  }
353
360
  }
354
361
  function onUp(e) {
355
- pointers.delete(e.pointerId);
362
+ if (!pointers.delete(e.pointerId)) return;
356
363
  if (pointers.size < 2) pinchStart = null;
357
364
  if (pointers.size === 0) {
358
365
  const hadPan = panStart !== null;
@@ -365,6 +372,10 @@ export function useInertiaPanzoom(options = {}) {
365
372
  }
366
373
  if (hadPan && lockedAxis === "y") vx = 0;
367
374
  startMomentum();
375
+ if (pinchZoomed) {
376
+ pinchZoomed = false;
377
+ fireZoomEnd();
378
+ }
368
379
  }
369
380
  }
370
381
  function onWheelZoom(e) {
@@ -382,6 +393,33 @@ export function useInertiaPanzoom(options = {}) {
382
393
  ty = my - newScale * focalY;
383
394
  scale = newScale;
384
395
  apply();
396
+ clearTimeout(wheelZoomEndTimer);
397
+ wheelZoomEndTimer = window.setTimeout(fireZoomEnd, 200);
398
+ }
399
+ function setTransform(t) {
400
+ stopMomentum();
401
+ if (t.scale !== void 0) scale = Math.max(minScale, Math.min(maxScale, t.scale));
402
+ if (t.tx !== void 0) tx = t.tx;
403
+ if (t.ty !== void 0) ty = t.ty;
404
+ const b = getBounds();
405
+ tx = Math.max(b.minX, Math.min(b.maxX, tx));
406
+ ty = Math.max(b.minY, Math.min(b.maxY, ty));
407
+ if (pinchStart && pointers.size >= 2) {
408
+ const pts = [...pointers.values()];
409
+ pinchStart = {
410
+ dist: Math.hypot(pts[0].x - pts[1].x, pts[0].y - pts[1].y),
411
+ midX: (pts[0].x + pts[1].x) / 2,
412
+ midY: (pts[0].y + pts[1].y) / 2,
413
+ s: scale,
414
+ tx,
415
+ ty
416
+ };
417
+ }
418
+ if (panStart && pointers.size === 1) {
419
+ const p = [...pointers.values()][0];
420
+ panStart = { x: p.x, y: p.y, tx, ty, axisLock: panStart.axisLock };
421
+ }
422
+ apply();
385
423
  }
386
424
  function scrollBy(dx, dy) {
387
425
  stopMomentum();
@@ -401,6 +439,7 @@ export function useInertiaPanzoom(options = {}) {
401
439
  cleanup: () => {
402
440
  stopMomentum();
403
441
  clearTimeout(hideTimer);
442
+ clearTimeout(wheelZoomEndTimer);
404
443
  parent.removeEventListener("pointerdown", onDown);
405
444
  parent.removeEventListener("pointermove", onMove);
406
445
  parent.removeEventListener("pointerup", onUp);
@@ -449,10 +488,7 @@ export function useInertiaPanzoom(options = {}) {
449
488
  setWillChangeActive(false);
450
489
  }
451
490
  },
452
- onChange: (cb) => {
453
- subscribers.add(cb);
454
- return () => subscribers.delete(cb);
455
- }
491
+ setTransform
456
492
  };
457
493
  }
458
494
  function detach() {
@@ -470,7 +506,18 @@ export function useInertiaPanzoom(options = {}) {
470
506
  scrollToElement: (elem, opts) => controller?.scrollToElement(elem, opts),
471
507
  scrollBy: (dx, dy) => controller?.scrollBy(dx, dy),
472
508
  setGesturesEnabled: (enabled) => controller?.setGesturesEnabled(enabled),
473
- onChange: (cb) => controller?.onChange(cb) ?? (() => {
474
- })
509
+ setTransform: (t) => controller?.setTransform(t),
510
+ setScaleLimits: (min, max) => {
511
+ minScale = min;
512
+ maxScale = max;
513
+ },
514
+ onChange: (cb) => {
515
+ subscribers.add(cb);
516
+ return () => subscribers.delete(cb);
517
+ },
518
+ onZoomEnd: (cb) => {
519
+ zoomEndSubscribers.add(cb);
520
+ return () => zoomEndSubscribers.delete(cb);
521
+ }
475
522
  };
476
523
  }
@@ -0,0 +1,18 @@
1
+ import { type ComputedRef, type InjectionKey } from 'vue';
2
+ import { type KviewerLocale, type MessageKey } from '../i18n/messages.js';
3
+ export type KviewerMessageOverrides = Partial<Record<MessageKey, string>>;
4
+ export declare const kviewerLocaleKey: InjectionKey<ComputedRef<KviewerLocale>>;
5
+ export declare const kviewerMessagesKey: InjectionKey<ComputedRef<KviewerMessageOverrides>>;
6
+ /** Called once by the Viewer root to make locale + overrides available. */
7
+ export declare function provideKviewerI18n(locale: ComputedRef<KviewerLocale>, overrides: ComputedRef<KviewerMessageOverrides>): void;
8
+ /**
9
+ * Translate built-in viewer strings.
10
+ *
11
+ * Resolution order: host override (`messages` prop) → active locale →
12
+ * English. `{param}` placeholders are substituted from `params`.
13
+ */
14
+ export declare function useKviewerI18n(): {
15
+ t: (key: MessageKey, params?: Record<string, string | number>) => string;
16
+ toolLabel: (name: string) => string;
17
+ locale: ComputedRef<"en" | "de">;
18
+ };
@@ -0,0 +1,29 @@
1
+ import { computed, inject, provide } from "vue";
2
+ import { messages } from "../i18n/messages.js";
3
+ export const kviewerLocaleKey = Symbol("kviewer-locale");
4
+ export const kviewerMessagesKey = Symbol("kviewer-messages");
5
+ export function provideKviewerI18n(locale, overrides) {
6
+ provide(kviewerLocaleKey, locale);
7
+ provide(kviewerMessagesKey, overrides);
8
+ }
9
+ export function useKviewerI18n() {
10
+ const locale = inject(kviewerLocaleKey, computed(() => "en"));
11
+ const overrides = inject(
12
+ kviewerMessagesKey,
13
+ computed(() => ({}))
14
+ );
15
+ function t(key, params) {
16
+ let message = overrides.value[key] ?? messages[locale.value]?.[key] ?? messages.en[key] ?? key;
17
+ if (params) {
18
+ for (const [name, value] of Object.entries(params)) {
19
+ message = message.replace(`{${name}}`, String(value));
20
+ }
21
+ }
22
+ return message;
23
+ }
24
+ function toolLabel(name) {
25
+ const key = `tool${name.charAt(0).toUpperCase()}${name.slice(1)}`;
26
+ return key in messages.en ? t(key) : name;
27
+ }
28
+ return { t, toolLabel, locale };
29
+ }
@@ -21,6 +21,17 @@ export interface PageVirtualization {
21
21
  pageMetas: ShallowRef<PageMeta[]>;
22
22
  currentPage: Ref<number>;
23
23
  renderedPages: ComputedRef<Set<number>>;
24
+ /** Pages that have entered the viewport at least once since the document
25
+ * loaded (or since the last `resetViewedPages()`). Marked from scroll/pan
26
+ * geometry — not render state — so a fast fling that skips rendering the
27
+ * middle pages still counts them as viewed. */
28
+ viewedPages: ShallowRef<Set<number>>;
29
+ /** True once every page has been viewed. */
30
+ allPagesRead: ComputedRef<boolean>;
31
+ /** Mark a single page as viewed (used by single-page navigation). */
32
+ markPageViewed: (pageNumber: number) => void;
33
+ /** Clear the viewed-pages set, e.g. to start a fresh signing session. */
34
+ resetViewedPages: () => void;
24
35
  isPageRendered: (pageNumber: number) => boolean;
25
36
  observePage: (pageNumber: number, element: HTMLElement) => void;
26
37
  unobservePage: (pageNumber: number) => void;