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
@@ -1,117 +1,128 @@
1
1
  <template>
2
2
  <div class="flex flex-col shrink-0 border-b border-default">
3
- <!-- Top navigation bar -->
4
- <div class="flex items-center h-[40px] px-3 gap-2 bg-elevated/50">
5
- <UPopover>
6
- <UButton icon="i-lucide-menu" variant="ghost" color="neutral" size="xs" data-testid="viewer-menu" />
7
- <template #content>
8
- <div class="p-1 flex flex-col gap-0.5">
9
- <UButton
10
- icon="i-lucide-download"
11
- label="Download"
12
- variant="ghost"
13
- color="neutral"
14
- size="xs"
15
- class="w-full justify-start"
16
- data-testid="viewer-download"
17
- @click="onDownload"
18
- />
19
- <UButton
20
- :icon="state.shapeDetection.value ? 'i-lucide-square-check-big' : 'i-lucide-square-dashed'"
21
- :label="state.shapeDetection.value ? 'Disable form field detection' : 'Detect form fields'"
22
- :variant="state.shapeDetection.value ? 'soft' : 'ghost'"
23
- :color="state.shapeDetection.value ? 'primary' : 'neutral'"
24
- size="xs"
25
- class="w-full justify-start"
26
- data-testid="viewer-toggle-shape-detection"
27
- @click="state.shapeDetection.value = !state.shapeDetection.value"
28
- />
29
- <!-- Host-supplied menu items appended after the built-ins.
30
- A small separator visually marks the boundary; we only
31
- render it when there is at least one custom item. -->
32
- <template v-if="props.menuItems && props.menuItems.length > 0">
33
- <div class="my-1 border-t border-default" />
34
- <template v-for="item in props.menuItems" :key="item.key">
35
- <div v-if="item.type === 'separator'" class="my-1 border-t border-default" />
36
- <UButton
37
- v-else-if="item.type === 'button'"
38
- :icon="item.icon"
39
- :label="item.label"
40
- :disabled="item.disabled"
41
- variant="ghost"
42
- color="neutral"
43
- size="xs"
44
- class="w-full justify-start"
45
- :data-testid="`viewer-menu-${item.key}`"
46
- @click="item.onSelect()"
47
- />
48
- <UButton
49
- v-else-if="item.type === 'checkbox'"
50
- :icon="item.icon ?? (item.checked ? 'i-lucide-check-square' : 'i-lucide-square')"
51
- :label="item.label"
52
- :variant="item.checked ? 'soft' : 'ghost'"
53
- :color="item.checked ? 'primary' : 'neutral'"
54
- :disabled="item.disabled"
55
- size="xs"
56
- class="w-full justify-start"
57
- :data-testid="`viewer-menu-${item.key}`"
58
- @click="item.onUpdate(!item.checked)"
59
- />
60
- </template>
61
- </template>
62
- </div>
63
- </template>
64
- </UPopover>
65
- <PageSettings />
3
+ <!-- Default layout: omitting `tools` renders today's toolbar verbatim. -->
4
+ <template v-if="props.tools === void 0">
5
+ <!-- Top navigation bar -->
6
+ <div class="flex items-center h-[40px] px-3 gap-2 bg-elevated/50">
7
+ <ToolbarMenu :menu-items="props.menuItems" />
8
+ <PageSettings />
66
9
 
67
- <USeparator orientation="vertical" class="h-5" color="neutral" />
10
+ <USeparator orientation="vertical" class="h-5" :ui="{ border: 'border-accented' }" />
68
11
 
69
- <ZoomControls />
12
+ <ZoomControls />
70
13
 
71
- <template v-if="!state.readonly.value">
72
- <USeparator orientation="vertical" class="h-5" color="neutral" />
14
+ <template v-if="!state.readonly.value">
15
+ <USeparator orientation="vertical" class="h-5" :ui="{ border: 'border-accented' }" />
73
16
 
74
- <HandTool />
75
- <MarqueeTool />
76
-
77
- <UButton
78
- icon="i-lucide-form-input"
79
- :variant="state.formEditMode.value ? 'soft' : 'ghost'"
80
- :color="state.formEditMode.value ? 'primary' : 'neutral'"
81
- size="xs"
82
- :title="state.formEditMode.value ? 'Exit form-edit mode' : 'Enter form-edit mode'"
83
- data-testid="form-edit-toggle"
84
- @click="state.setFormEditMode(!state.formEditMode.value)"
85
- />
86
- </template>
17
+ <HandTool />
18
+ <MarqueeTool />
19
+ </template>
87
20
 
88
- <div class="ml-auto flex items-center gap-2">
89
- <PageInfo />
90
- <SearchTool />
21
+ <div class="ml-auto flex items-center gap-2">
22
+ <PageInfo />
23
+ <SearchTool />
24
+ </div>
91
25
  </div>
92
- </div>
93
26
 
94
- <!-- Tool bar -->
95
- <div v-if="!state.readonly.value" class="grid grid-cols-3 items-center h-[44px] px-3 gap-2 bg-elevated">
96
- <div class="justify-self-start">
97
- <ToolProperties @style-updated="bumpStyleVersion" />
27
+ <!-- Tool bar -->
28
+ <div v-if="!state.readonly.value" class="grid grid-cols-3 items-center h-[44px] px-3 gap-2 bg-elevated">
29
+ <div class="justify-self-start">
30
+ <ToolProperties @style-updated="bumpStyleVersion" />
31
+ </div>
32
+
33
+ <div class="justify-self-center">
34
+ <FormFieldTools v-if="state.formEditMode.value" />
35
+ <DrawingTools v-else :style-version="styleVersion" />
36
+ </div>
37
+
38
+ <div class="justify-self-end flex items-center gap-1">
39
+ <ActionTools />
40
+ </div>
98
41
  </div>
42
+ </template>
99
43
 
100
- <div class="justify-self-center">
101
- <FormFieldTools v-if="state.formEditMode.value" />
102
- <DrawingTools v-else :style-version="styleVersion" />
44
+ <!-- Config-driven layout: `tools` chooses which buttons render, in what
45
+ order, with custom buttons interleaved via #tool-<name> slots. -->
46
+ <template v-else>
47
+ <!-- Top navigation bar -->
48
+ <div class="flex items-center h-[40px] px-3 gap-2 bg-elevated/50">
49
+ <template v-for="(entry, i) in topEntries" :key="entryKey(entry, i)">
50
+ <USeparator v-if="entry === 'separator'" orientation="vertical" class="h-5" :ui="{ border: 'border-accented' }" />
51
+ <div v-else-if="entry === 'spacer'" class="ml-auto" />
52
+ <ToolbarMenu v-else-if="entry === 'menu'" :menu-items="props.menuItems" />
53
+ <PageSettings v-else-if="entry === 'pageSettings'" />
54
+ <ZoomControls v-else-if="entry === 'zoom'" />
55
+ <HandTool v-else-if="entry === 'hand' && isEnabled('hand')" />
56
+ <MarqueeTool v-else-if="entry === 'marquee' && isEnabled('marquee')" />
57
+ <PageInfo v-else-if="entry === 'pageInfo'" />
58
+ <SearchTool v-else-if="entry === 'search'" />
59
+ <slot v-else-if="isSlot(entry)" :name="slotName(entry)" :state="state" />
60
+ </template>
103
61
  </div>
104
62
 
105
- <div class="justify-self-end flex items-center gap-1">
106
- <ActionTools />
63
+ <!-- Tool bar hidden entirely in readonly, like the default layout.
64
+ Columns are split on `spacer`; three columns lay out as a grid so
65
+ the middle (tools) is centered between properties and actions. -->
66
+ <div
67
+ v-if="!state.readonly.value && bottomEntries.length > 0"
68
+ :class="bottomRowClass"
69
+ >
70
+ <div
71
+ v-for="(segment, si) in bottomSegments"
72
+ :key="`segment-${si}`"
73
+ class="flex items-center gap-1"
74
+ :class="segmentClass(bottomSegments.length, si)"
75
+ >
76
+ <template v-for="(entry, i) in segment" :key="entryKey(entry, i)">
77
+ <USeparator v-if="entry === 'separator'" orientation="vertical" class="h-5" :ui="{ border: 'border-accented' }" />
78
+ <ToolProperties v-else-if="entry === 'properties'" @style-updated="bumpStyleVersion" />
79
+ <StampPicker v-else-if="entry === 'stamp' && modeOk('stamp')" />
80
+ <SignaturePicker v-else-if="entry === 'signature' && modeOk('signature') && state.signatureHandlers" />
81
+ <UButton
82
+ v-else-if="entry === 'undo'"
83
+ icon="i-lucide-undo-2"
84
+ variant="ghost"
85
+ color="neutral"
86
+ size="sm"
87
+ data-testid="action-undo"
88
+ :disabled="!state.history.canUndo.value"
89
+ @click="state.history.undo"
90
+ />
91
+ <UButton
92
+ v-else-if="entry === 'redo'"
93
+ icon="i-lucide-redo-2"
94
+ variant="ghost"
95
+ color="neutral"
96
+ size="sm"
97
+ data-testid="action-redo"
98
+ :disabled="!state.history.canRedo.value"
99
+ @click="state.history.redo"
100
+ />
101
+ <AnnotationToolButton
102
+ v-else-if="annotationButtonType(entry) !== void 0 && modeOk(entry)"
103
+ :type="annotationButtonType(entry)"
104
+ :data-testid="entry === 'eraser' ? 'action-eraser' : `tool-${entry}`"
105
+ />
106
+ <slot v-else-if="isSlot(entry)" :name="slotName(entry)" :state="state" />
107
+ </template>
108
+ </div>
107
109
  </div>
108
- </div>
110
+ </template>
109
111
  </div>
110
112
  </template>
111
113
 
112
114
  <script setup>
113
- import { ref } from "vue";
115
+ import { computed, provide, ref } from "vue";
114
116
  import { useViewerState } from "../composables/useViewerState";
117
+ import {
118
+ ANNOTATION_TOOL_TYPES,
119
+ BOTTOM_TOOL_IDS,
120
+ DRAW_TOOLS,
121
+ FORM_TOOLS,
122
+ READONLY_HIDDEN_TOP,
123
+ STYLE_VERSION_KEY,
124
+ TOP_TOOL_IDS
125
+ } from "../toolbar-tools";
115
126
  import PageSettings from "./tools/PageSettings.vue";
116
127
  import ZoomControls from "./tools/ZoomControls.vue";
117
128
  import HandTool from "./tools/HandTool.vue";
@@ -122,19 +133,90 @@ import ActionTools from "./tools/ActionTools.vue";
122
133
  import PageInfo from "./tools/PageInfo.vue";
123
134
  import SearchTool from "./tools/SearchTool.vue";
124
135
  import ToolProperties from "./tools/ToolProperties.vue";
136
+ import ToolbarMenu from "./tools/ToolbarMenu.vue";
137
+ import AnnotationToolButton from "./tools/AnnotationToolButton.vue";
138
+ import StampPicker from "./panels/StampPicker.vue";
139
+ import SignaturePicker from "./panels/SignaturePicker.vue";
125
140
  const props = defineProps({
126
- menuItems: { type: Array, required: false }
141
+ menuItems: { type: Array, required: false },
142
+ tools: { type: Array, required: false }
127
143
  });
128
144
  const state = useViewerState();
129
145
  const styleVersion = ref(0);
146
+ provide(STYLE_VERSION_KEY, styleVersion);
130
147
  function bumpStyleVersion() {
131
148
  styleVersion.value += 1;
132
149
  }
133
- async function onDownload() {
134
- try {
135
- await state.downloadPdf();
136
- } catch (error) {
137
- console.error("Failed to export PDF from menu action", error);
150
+ function isSlot(entry) {
151
+ return typeof entry === "object" && entry !== null && entry.type === "slot";
152
+ }
153
+ function slotName(entry) {
154
+ if (!isSlot(entry)) return "";
155
+ return `tool-${entry.name ?? entry.key}`;
156
+ }
157
+ function entryKey(entry, index) {
158
+ return `${isSlot(entry) ? `slot-${entry.key}` : entry}-${index}`;
159
+ }
160
+ function homeRow(entry) {
161
+ if (typeof entry !== "string") return null;
162
+ if (TOP_TOOL_IDS.has(entry)) return "top";
163
+ if (BOTTOM_TOOL_IDS.has(entry)) return "bottom";
164
+ return null;
165
+ }
166
+ const rows = computed(() => {
167
+ const top = [];
168
+ const bottom = [];
169
+ let current = "top";
170
+ for (const entry of props.tools ?? []) {
171
+ const home = homeRow(entry);
172
+ let row;
173
+ if (home) {
174
+ row = home;
175
+ current = home;
176
+ } else if (isSlot(entry) && entry.row) {
177
+ row = entry.row;
178
+ current = entry.row;
179
+ } else {
180
+ row = current;
181
+ }
182
+ (row === "top" ? top : bottom).push(entry);
183
+ }
184
+ return { top, bottom };
185
+ });
186
+ const topEntries = computed(() => rows.value.top);
187
+ const bottomEntries = computed(() => rows.value.bottom);
188
+ const bottomSegments = computed(() => {
189
+ const segments = [[]];
190
+ for (const entry of bottomEntries.value) {
191
+ if (entry === "spacer") segments.push([]);
192
+ else segments[segments.length - 1].push(entry);
193
+ }
194
+ return segments;
195
+ });
196
+ const bottomRowClass = computed(
197
+ () => bottomSegments.value.length === 3 ? "grid grid-cols-3 items-center h-[44px] px-3 gap-2 bg-elevated" : "flex items-center h-[44px] px-3 gap-2 bg-elevated"
198
+ );
199
+ function segmentClass(count, index) {
200
+ if (count === 3) {
201
+ return ["justify-self-start", "justify-self-center", "justify-self-end"][index] ?? "";
202
+ }
203
+ if (count === 2 && index === 1) return "ml-auto";
204
+ return "";
205
+ }
206
+ function isEnabled(id) {
207
+ if (state.readonly.value && READONLY_HIDDEN_TOP.has(id)) return false;
208
+ return true;
209
+ }
210
+ function modeOk(id) {
211
+ if (FORM_TOOLS.has(id)) return state.formEditMode.value;
212
+ if (DRAW_TOOLS.has(id)) return !state.formEditMode.value;
213
+ return true;
214
+ }
215
+ function annotationButtonType(entry) {
216
+ if (entry === "eraser") return "eraser";
217
+ if (typeof entry === "string" && entry in ANNOTATION_TOOL_TYPES) {
218
+ return ANNOTATION_TOOL_TYPES[entry];
138
219
  }
220
+ return void 0;
139
221
  }
140
222
  </script>
@@ -1,9 +1,29 @@
1
1
  import type { ViewerMenuItem } from '../menu-items.js';
2
+ import { type ViewerToolEntry } from '../toolbar-tools.js';
2
3
  type __VLS_Props = {
3
4
  /** Extra items appended to the burger menu after the built-ins.
4
5
  * See {@link ViewerMenuItem}. Pure forwarding from `<KViewer>`. */
5
6
  menuItems?: ViewerMenuItem[];
7
+ /** Configures which toolbar tools render and in what order. When
8
+ * omitted, the default toolbar is rendered. See {@link ViewerToolEntry}. */
9
+ tools?: ViewerToolEntry[];
6
10
  };
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>;
11
+ declare var __VLS_109: string, __VLS_110: {
12
+ state: import("../composables/useViewerState.js").ViewerState;
13
+ }, __VLS_154: string, __VLS_155: {
14
+ state: import("../composables/useViewerState.js").ViewerState;
15
+ };
16
+ type __VLS_Slots = {} & {
17
+ [K in NonNullable<typeof __VLS_109>]?: (props: typeof __VLS_110) => any;
18
+ } & {
19
+ [K in NonNullable<typeof __VLS_154>]?: (props: typeof __VLS_155) => any;
20
+ };
21
+ declare const __VLS_base: 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>;
22
+ declare const __VLS_export: __VLS_WithSlots<typeof __VLS_base, __VLS_Slots>;
8
23
  declare const _default: typeof __VLS_export;
9
24
  export default _default;
25
+ type __VLS_WithSlots<T, S> = T & {
26
+ new (): {
27
+ $slots: S;
28
+ };
29
+ };
@@ -2,6 +2,7 @@ import { type ComponentPublicInstance } from 'vue';
2
2
  import type { ViewMode } from '../composables/useViewerState.js';
3
3
  import type { StampDefinition, SignatureHandlers } from '../annotation/engine/types.js';
4
4
  import type { ViewerMenuItem } from '../menu-items.js';
5
+ import type { ViewerToolEntry } from '../toolbar-tools.js';
5
6
  export interface ViewerTabItem {
6
7
  /** Unique identifier for the tab. */
7
8
  id: string;
@@ -62,6 +63,8 @@ type __VLS_Props = {
62
63
  scripting?: boolean;
63
64
  /** Forwarded to every Viewer instance — see KViewer's `menuItems`. */
64
65
  menuItems?: ViewerMenuItem[];
66
+ /** Forwarded to every Viewer instance — see KViewer's `tools`. */
67
+ tools?: ViewerToolEntry[];
65
68
  };
66
69
  declare function addTab(item: Omit<ViewerTabItem, 'id'> & {
67
70
  id?: string;
@@ -269,8 +272,12 @@ declare var __VLS_7: {}, __VLS_21: {}, __VLS_32: {
269
272
  shapeDetection?: boolean | undefined;
270
273
  scripting?: boolean | undefined;
271
274
  };
272
- }, __VLS_37: {};
275
+ }, __VLS_39: string, __VLS_40: {
276
+ state: import("../composables/useViewerState.js").ViewerState;
277
+ }, __VLS_42: {};
273
278
  type __VLS_Slots = {} & {
279
+ [K in NonNullable<typeof __VLS_39>]?: (props: typeof __VLS_40) => any;
280
+ } & {
274
281
  'tabs-leading'?: (props: typeof __VLS_7) => any;
275
282
  } & {
276
283
  'tabs-trailing'?: (props: typeof __VLS_21) => any;
@@ -279,7 +286,7 @@ type __VLS_Slots = {} & {
279
286
  } & {
280
287
  footer?: (props: typeof __VLS_35) => any;
281
288
  } & {
282
- empty?: (props: typeof __VLS_37) => any;
289
+ empty?: (props: typeof __VLS_42) => any;
283
290
  };
284
291
  declare const __VLS_base: import("vue").DefineComponent<__VLS_Props, {
285
292
  addTab: typeof addTab;
@@ -402,6 +409,7 @@ declare const __VLS_base: import("vue").DefineComponent<__VLS_Props, {
402
409
  userName: string;
403
410
  zoom: number;
404
411
  menuItems: ViewerMenuItem[];
412
+ tools: ViewerToolEntry[];
405
413
  scripting: boolean;
406
414
  stamps: StampDefinition[];
407
415
  signatureHandlers: SignatureHandlers;
@@ -61,6 +61,7 @@
61
61
  :shape-detection="activeTab.shapeDetection ?? shapeDetection"
62
62
  :scripting="activeTab.scripting ?? scripting"
63
63
  :menu-items="menuItems"
64
+ :tools="tools"
64
65
  :role-colors="roleColors"
65
66
  :active-role-id="activeRoleId"
66
67
  @update:active-role-id="(v) => emit('update:activeRoleId', v)"
@@ -71,6 +72,11 @@
71
72
  <template v-if="$slots.footer" #footer>
72
73
  <slot name="footer" :tab="activeTab" />
73
74
  </template>
75
+ <!-- Forward custom #tool-* slots so tabs consumers can inject
76
+ custom toolbar buttons; tabs-*/header/footer/empty untouched. -->
77
+ <template v-for="name in toolSlotNames" :key="name" #[name]="scope">
78
+ <slot :name="name" v-bind="scope" />
79
+ </template>
74
80
  </Viewer>
75
81
  </div>
76
82
 
@@ -83,7 +89,7 @@
83
89
  </template>
84
90
 
85
91
  <script setup>
86
- import { ref, computed, watch, nextTick } from "vue";
92
+ import { ref, computed, watch, nextTick, useSlots } from "vue";
87
93
  import Viewer from "./Viewer.vue";
88
94
  const props = defineProps({
89
95
  items: { type: Array, required: true },
@@ -100,8 +106,13 @@ const props = defineProps({
100
106
  roleColors: { type: Object, required: false, default: void 0 },
101
107
  activeRoleId: { type: [String, null], required: false, default: void 0 },
102
108
  scripting: { type: Boolean, required: false, default: false },
103
- menuItems: { type: Array, required: false, default: void 0 }
109
+ menuItems: { type: Array, required: false, default: void 0 },
110
+ tools: { type: Array, required: false, default: void 0 }
104
111
  });
112
+ const slots = useSlots();
113
+ const toolSlotNames = computed(
114
+ () => Object.keys(slots).filter((name) => name.startsWith("tool-"))
115
+ );
105
116
  const emit = defineEmits(["update:activeTab", "tab-added", "tab-close", "tab-removed", "update:activeRoleId"]);
106
117
  let idCounter = 0;
107
118
  function generateId() {
@@ -2,6 +2,7 @@ import { type ComponentPublicInstance } from 'vue';
2
2
  import type { ViewMode } from '../composables/useViewerState.js';
3
3
  import type { StampDefinition, SignatureHandlers } from '../annotation/engine/types.js';
4
4
  import type { ViewerMenuItem } from '../menu-items.js';
5
+ import type { ViewerToolEntry } from '../toolbar-tools.js';
5
6
  export interface ViewerTabItem {
6
7
  /** Unique identifier for the tab. */
7
8
  id: string;
@@ -62,6 +63,8 @@ type __VLS_Props = {
62
63
  scripting?: boolean;
63
64
  /** Forwarded to every Viewer instance — see KViewer's `menuItems`. */
64
65
  menuItems?: ViewerMenuItem[];
66
+ /** Forwarded to every Viewer instance — see KViewer's `tools`. */
67
+ tools?: ViewerToolEntry[];
65
68
  };
66
69
  declare function addTab(item: Omit<ViewerTabItem, 'id'> & {
67
70
  id?: string;
@@ -269,8 +272,12 @@ declare var __VLS_7: {}, __VLS_21: {}, __VLS_32: {
269
272
  shapeDetection?: boolean | undefined;
270
273
  scripting?: boolean | undefined;
271
274
  };
272
- }, __VLS_37: {};
275
+ }, __VLS_39: string, __VLS_40: {
276
+ state: import("../composables/useViewerState.js").ViewerState;
277
+ }, __VLS_42: {};
273
278
  type __VLS_Slots = {} & {
279
+ [K in NonNullable<typeof __VLS_39>]?: (props: typeof __VLS_40) => any;
280
+ } & {
274
281
  'tabs-leading'?: (props: typeof __VLS_7) => any;
275
282
  } & {
276
283
  'tabs-trailing'?: (props: typeof __VLS_21) => any;
@@ -279,7 +286,7 @@ type __VLS_Slots = {} & {
279
286
  } & {
280
287
  footer?: (props: typeof __VLS_35) => any;
281
288
  } & {
282
- empty?: (props: typeof __VLS_37) => any;
289
+ empty?: (props: typeof __VLS_42) => any;
283
290
  };
284
291
  declare const __VLS_base: import("vue").DefineComponent<__VLS_Props, {
285
292
  addTab: typeof addTab;
@@ -402,6 +409,7 @@ declare const __VLS_base: import("vue").DefineComponent<__VLS_Props, {
402
409
  userName: string;
403
410
  zoom: number;
404
411
  menuItems: ViewerMenuItem[];
412
+ tools: ViewerToolEntry[];
405
413
  scripting: boolean;
406
414
  stamps: StampDefinition[];
407
415
  signatureHandlers: SignatureHandlers;
@@ -42,31 +42,33 @@
42
42
  </option>
43
43
  </select>
44
44
 
45
- <!-- Standard dropdown combo box -->
46
- <select
45
+ <!-- Standard dropdown combo box. Rendered with Nuxt UI's USelect so the
46
+ option list is portalled to <body> and shows at a readable size,
47
+ independent of the page-zoom transform — a native <select> popup is
48
+ drawn by the OS and can't be styled (issue #71). The closed trigger
49
+ keeps the field's look and height-fitted font via the
50
+ .kviewer-form-uselect class + inline fieldStyle. -->
51
+ <USelect
47
52
  v-else
48
- :value="currentValue"
53
+ :items="selectItems"
54
+ :model-value="currentValue"
49
55
  :disabled="isReadOnly"
50
56
  :required="props.field.required"
57
+ placeholder="—"
58
+ variant="none"
59
+ color="neutral"
60
+ size="md"
61
+ trailing-icon="i-lucide-chevron-down"
51
62
  :style="fieldStyle"
52
- class="kviewer-form-input kviewer-form-select"
53
- @change="onChange"
54
- >
55
- <option v-if="!props.field.required" value="">
56
-
57
- </option>
58
- <option
59
- v-for="opt in props.field.options ?? []"
60
- :key="opt.exportValue"
61
- :value="opt.exportValue"
62
- >
63
- {{ opt.displayValue }}
64
- </option>
65
- </select>
63
+ :ui="selectUi"
64
+ class="kviewer-form-uselect"
65
+ @update:model-value="onSelectChange"
66
+ />
66
67
  </template>
67
68
 
68
69
  <script setup>
69
70
  import { computed } from "vue";
71
+ import { computeFieldFontSize, decodeFontName } from "../../annotation/font-style";
70
72
  import { useFormFields } from "../../composables/useFormFields";
71
73
  import { useViewerState } from "../../composables/useViewerState";
72
74
  const props = defineProps({
@@ -78,10 +80,25 @@ const isReadOnly = computed(
78
80
  () => state.readonly.value || props.field.readOnly || formFields.isFieldLocked(props.field.id)
79
81
  );
80
82
  const datalistId = computed(() => `kviewer-datalist-${props.field.id}`);
83
+ const isStandardCombo = computed(() => props.field.combo && !props.field.editable);
81
84
  const listBoxSize = computed(() => {
82
85
  const optCount = props.field.options?.length ?? 0;
83
86
  return Math.max(2, optCount);
84
87
  });
88
+ const CLEAR_VALUE = "__kviewer_clear__";
89
+ const selectItems = computed(() => {
90
+ const opts = (props.field.options ?? []).map((o) => ({
91
+ label: o.displayValue,
92
+ value: o.exportValue
93
+ }));
94
+ if (!props.field.required) opts.unshift({ label: "\u2014", value: CLEAR_VALUE });
95
+ return opts;
96
+ });
97
+ const selectUi = {
98
+ trailing: "pe-0.5",
99
+ trailingIcon: "size-2",
100
+ content: "min-w-(--reka-select-trigger-width)"
101
+ };
85
102
  const currentValue = computed(() => {
86
103
  const fv = formFields.getFieldValue(props.field.id);
87
104
  if (!fv) return "";
@@ -90,9 +107,20 @@ const currentValue = computed(() => {
90
107
  });
91
108
  const fieldStyle = computed(() => {
92
109
  const style = {};
93
- if (props.field.fontSize && props.field.fontSize > 0) {
94
- style.fontSize = `${props.field.fontSize}px`;
110
+ const rect = props.field.rect;
111
+ const rectHeight = rect[3] - rect[1];
112
+ if (isStandardCombo.value) {
113
+ const declared = props.field.fontSize && props.field.fontSize > 0 ? props.field.fontSize : Infinity;
114
+ const fill = Math.max(6, Math.min(rectHeight - 3, 14));
115
+ style.fontSize = `${Math.round(10 * Math.min(declared, fill)) / 10}px`;
116
+ style.lineHeight = "1";
117
+ } else {
118
+ style.fontSize = `${computeFieldFontSize(props.field.fontSize, rectHeight)}px`;
95
119
  }
120
+ const fontStyle = decodeFontName(props.field.fontName);
121
+ style.fontFamily = fontStyle.fontFamily;
122
+ if (fontStyle.fontWeight !== "normal") style.fontWeight = fontStyle.fontWeight;
123
+ if (fontStyle.fontStyle !== "normal") style.fontStyle = fontStyle.fontStyle;
96
124
  if (props.field.color && props.field.color.length >= 3) {
97
125
  style.color = `rgb(${props.field.color[0]}, ${props.field.color[1]}, ${props.field.color[2]})`;
98
126
  }
@@ -114,4 +142,7 @@ function onInputChange(event) {
114
142
  const target = event.target;
115
143
  formFields.setFieldValue(props.field.id, target.value);
116
144
  }
145
+ function onSelectChange(value) {
146
+ formFields.setFieldValue(props.field.id, value === CLEAR_VALUE ? "" : value);
147
+ }
117
148
  </script>
@@ -8,10 +8,10 @@
8
8
  v-if="signatureUrl"
9
9
  :src="signatureUrl"
10
10
  class="kviewer-form-signature__preview"
11
- alt="Signature"
11
+ :alt="t('signature')"
12
12
  >
13
13
  <span v-else class="kviewer-form-signature__placeholder">
14
- {{ props.field.promptText || "Click to sign" }}
14
+ {{ props.field.promptText || t("clickToSign") }}
15
15
  </span>
16
16
  </div>
17
17
 
@@ -28,12 +28,14 @@
28
28
  import { ref, computed } from "vue";
29
29
  import { useFormFields } from "../../composables/useFormFields";
30
30
  import { useViewerState } from "../../composables/useViewerState";
31
+ import { useKviewerI18n } from "../../composables/useKviewerI18n";
31
32
  import SignatureDrawModal from "../modals/SignatureDrawModal.vue";
32
33
  const props = defineProps({
33
34
  field: { type: Object, required: true }
34
35
  });
35
36
  const formFields = useFormFields();
36
37
  const state = useViewerState();
38
+ const { t } = useKviewerI18n();
37
39
  const drawModalOpen = ref(false);
38
40
  const signatureUrl = computed(() => {
39
41
  const fv = formFields.getFieldValue(props.field.id);
@@ -25,7 +25,7 @@
25
25
 
26
26
  <script setup>
27
27
  import { computed } from "vue";
28
- import { decodeFontName } from "../../annotation/font-style";
28
+ import { computeFieldFontSize, decodeFontName } from "../../annotation/font-style";
29
29
  import { useFormFields } from "../../composables/useFormFields";
30
30
  import { useViewerState } from "../../composables/useViewerState";
31
31
  const props = defineProps({
@@ -40,26 +40,15 @@ const currentValue = computed(() => {
40
40
  const fv = formFields.getFieldValue(props.field.id);
41
41
  return typeof fv?.value === "string" ? fv.value : "";
42
42
  });
43
- const FONT_BORDER_SIZE = 2;
44
- const LINE_HEIGHT_FACTOR = 1.35;
45
- const DEFAULT_FIELD_FONT_SIZE = 9;
46
43
  const fieldStyle = computed(() => {
47
44
  const style = {};
48
- const declared = props.field.fontSize && props.field.fontSize > 0 ? props.field.fontSize : DEFAULT_FIELD_FONT_SIZE;
49
45
  const rect = props.field.rect;
50
- const rectHeight = Math.abs(rect[3] - rect[1] - FONT_BORDER_SIZE);
51
- let computed2;
52
- if (props.field.multiLine) {
53
- const numberOfLines = Math.max(
54
- 1,
55
- Math.round(rectHeight / (LINE_HEIGHT_FACTOR * declared))
56
- );
57
- const lineHeight = rectHeight / numberOfLines;
58
- computed2 = Math.min(declared, lineHeight / LINE_HEIGHT_FACTOR);
59
- } else {
60
- computed2 = Math.min(declared, rectHeight / LINE_HEIGHT_FACTOR);
61
- }
62
- style.fontSize = `${Math.round(10 * computed2) / 10}px`;
46
+ const computed2 = computeFieldFontSize(
47
+ props.field.fontSize,
48
+ rect[3] - rect[1],
49
+ { multiLine: props.field.multiLine }
50
+ );
51
+ style.fontSize = `${computed2}px`;
63
52
  const fontStyle = decodeFontName(props.field.fontName);
64
53
  style.fontFamily = fontStyle.fontFamily;
65
54
  if (fontStyle.fontWeight !== "normal") style.fontWeight = fontStyle.fontWeight;