kviewer 0.0.11 → 0.1.1

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 (45) hide show
  1. package/dist/module.d.mts +9 -1
  2. package/dist/module.json +1 -1
  3. package/dist/module.mjs +22 -2
  4. package/dist/runtime/annotation/engine/painter.d.ts +8 -1
  5. package/dist/runtime/annotation/engine/painter.js +11 -6
  6. package/dist/runtime/annotation/engine/tools/form-field.d.ts +6 -0
  7. package/dist/runtime/annotation/engine/tools/form-field.js +3 -1
  8. package/dist/runtime/annotation/engine/types.d.ts +64 -0
  9. package/dist/runtime/annotation/pdf-export/export-form-fields.js +102 -52
  10. package/dist/runtime/assets/kviewer.css +1 -1
  11. package/dist/runtime/components/PdfPage.vue +6 -1
  12. package/dist/runtime/components/Viewer.d.vue.ts +41 -1
  13. package/dist/runtime/components/Viewer.vue +84 -3
  14. package/dist/runtime/components/Viewer.vue.d.ts +41 -1
  15. package/dist/runtime/components/ViewerBar.d.vue.ts +7 -1
  16. package/dist/runtime/components/ViewerBar.vue +36 -0
  17. package/dist/runtime/components/ViewerBar.vue.d.ts +7 -1
  18. package/dist/runtime/components/ViewerTabs.d.vue.ts +29 -6
  19. package/dist/runtime/components/ViewerTabs.vue +11 -2
  20. package/dist/runtime/components/ViewerTabs.vue.d.ts +29 -6
  21. package/dist/runtime/components/form-fields/FormFieldWrapper.vue +10 -2
  22. package/dist/runtime/components/form-fields/PlacedFieldChrome.vue +1 -1
  23. package/dist/runtime/composables/useAnnotationEngine.d.ts +1 -0
  24. package/dist/runtime/composables/useAnnotationEngine.js +2 -1
  25. package/dist/runtime/composables/useFormFields.d.ts +40 -3
  26. package/dist/runtime/composables/useFormFields.js +101 -1
  27. package/dist/runtime/composables/useInertiaPanzoom.js +1 -0
  28. package/dist/runtime/composables/usePageProxyCache.d.ts +4 -0
  29. package/dist/runtime/composables/usePageProxyCache.js +4 -1
  30. package/dist/runtime/composables/useScriptingBridge.d.ts +29 -0
  31. package/dist/runtime/composables/useScriptingBridge.js +74 -0
  32. package/dist/runtime/composables/useScriptingManager.d.ts +65 -0
  33. package/dist/runtime/composables/useScriptingManager.js +123 -0
  34. package/dist/runtime/embed/bridge-client.d.ts +58 -0
  35. package/dist/runtime/embed/bridge-client.js +143 -0
  36. package/dist/runtime/embed/bridge-host.d.ts +59 -0
  37. package/dist/runtime/embed/bridge-host.js +136 -0
  38. package/dist/runtime/embed/protocol.d.ts +105 -0
  39. package/dist/runtime/embed/protocol.js +13 -0
  40. package/dist/runtime/menu-items.d.ts +34 -0
  41. package/dist/runtime/menu-items.js +0 -0
  42. package/dist/runtime/public-types.d.ts +8 -1
  43. package/dist/runtime/public-types.js +3 -0
  44. package/dist/types.d.mts +1 -1
  45. package/package.json +1 -1
@@ -2,7 +2,7 @@
2
2
  <div ref="viewerRoot" class="flex flex-col h-full">
3
3
  <!-- Header slot: defaults to ViewerBar -->
4
4
  <slot name="header">
5
- <ViewerBar />
5
+ <ViewerBar :menu-items="props.menuItems" />
6
6
  </slot>
7
7
 
8
8
  <div class="flex flex-1 min-h-0">
@@ -209,6 +209,8 @@ import { provideFormFields } from "../composables/useFormFields";
209
209
  import { extractCheckboxStyles, extractButtonCaptions } from "../annotation/parsers/extractCheckboxStyles";
210
210
  import { isIPad } from "../annotation/engine/input-device";
211
211
  import { useInertiaPanzoom } from "../composables/useInertiaPanzoom";
212
+ import { createScriptingManager } from "../composables/useScriptingManager";
213
+ import { createScriptingBridge } from "../composables/useScriptingBridge";
212
214
  const props = defineProps({
213
215
  source: { type: null, required: true },
214
216
  textLayer: { type: Boolean, required: false },
@@ -221,9 +223,13 @@ const props = defineProps({
221
223
  shapeDetection: { type: Boolean, required: false, default: false },
222
224
  active: { type: Boolean, required: false, default: true },
223
225
  freehandGroupingDelay: { type: Number, required: false, default: 1e3 },
224
- formEditMode: { type: Boolean, required: false, default: void 0 }
226
+ formEditMode: { type: Boolean, required: false, default: void 0 },
227
+ roleColors: { type: Object, required: false, default: void 0 },
228
+ activeRoleId: { type: [String, null], required: false, default: void 0 },
229
+ scripting: { type: Boolean, required: false, default: false },
230
+ menuItems: { type: Array, required: false, default: void 0 }
225
231
  });
226
- const emit = defineEmits(["update:formEditMode"]);
232
+ const emit = defineEmits(["update:formEditMode", "update:activeRoleId"]);
227
233
  const viewerRoot = ref(null);
228
234
  const scrollContainer = ref(null);
229
235
  const { state: viewerState, setScrollToPageFn, setDownloadPdfFn } = provideViewerState();
@@ -249,6 +255,29 @@ watch(
249
255
  );
250
256
  const viewerSearch = provideViewerSearch();
251
257
  const formFieldsState = provideFormFields();
258
+ watch(
259
+ () => props.roleColors,
260
+ (v) => {
261
+ formFieldsState.setRoleColors(v ?? {});
262
+ },
263
+ { immediate: true }
264
+ );
265
+ watch(
266
+ () => props.activeRoleId,
267
+ (v) => {
268
+ if (v === void 0) return;
269
+ if (formFieldsState.activeRoleId.value !== v) {
270
+ formFieldsState.setActiveRole(v);
271
+ }
272
+ },
273
+ { immediate: true }
274
+ );
275
+ watch(
276
+ () => formFieldsState.activeRoleId.value,
277
+ (v) => {
278
+ if (props.activeRoleId !== v) emit("update:activeRoleId", v);
279
+ }
280
+ );
252
281
  const pageSettings = providePageSettings(viewerRoot);
253
282
  const virtualization = createPageVirtualization();
254
283
  setScrollToPageFn((pageNumber) => {
@@ -267,6 +296,8 @@ const searchIndex = createSearchIndex();
267
296
  const shapeDetectionCache = createShapeDetection();
268
297
  const doc = shallowRef(null);
269
298
  let painter = null;
299
+ let scriptingManager = null;
300
+ let scriptingBridge = null;
270
301
  const pageMetas = virtualization.pageMetas;
271
302
  const { isPageRendered } = virtualization;
272
303
  watch(
@@ -474,6 +505,33 @@ async function loadDocument() {
474
505
  totalPages: pdfDoc.numPages
475
506
  });
476
507
  });
508
+ if (props.scripting) {
509
+ try {
510
+ if (!scriptingManager) {
511
+ scriptingManager = await createScriptingManager({
512
+ viewerState: {
513
+ currentPage: viewerState.currentPage,
514
+ totalPages: viewerState.totalPages,
515
+ scale: viewerState.scale,
516
+ setScale: viewerState.setScale,
517
+ scrollToPage: viewerState.scrollToPage
518
+ },
519
+ virtualization: { isPageRendered: virtualization.isPageRendered },
520
+ proxyCache: { getPageSync: proxyCache.getPageSync }
521
+ });
522
+ }
523
+ await scriptingManager.setDocument(pdfDoc);
524
+ scriptingBridge = createScriptingBridge({
525
+ formFields: formFieldsState,
526
+ scripting: scriptingManager,
527
+ pdfDoc
528
+ });
529
+ } catch (err) {
530
+ console.warn("[KViewer] Failed to initialize PDF scripting sandbox:", err);
531
+ scriptingBridge?.destroy();
532
+ scriptingBridge = null;
533
+ }
534
+ }
477
535
  } catch (err) {
478
536
  viewerState.error.value = "Failed to load document";
479
537
  console.error("[KViewer] Document load error:", err);
@@ -686,6 +744,11 @@ onMounted(() => {
686
744
  formFieldsState.addPlacedField(payload);
687
745
  viewerState.selectTool("hand");
688
746
  },
747
+ getFormFieldPreviewColor: () => {
748
+ const id = formFieldsState.activeRoleId.value;
749
+ if (!id) return void 0;
750
+ return formFieldsState.roleColors.value[id];
751
+ },
689
752
  freehandGroupingDelay: props.freehandGroupingDelay,
690
753
  // Suspend panzoom gestures while an annotation is being dragged or
691
754
  // transformed. Otherwise single-finger pan runs in parallel with the
@@ -746,6 +809,9 @@ onMounted(() => {
746
809
  watch(
747
810
  () => props.source,
748
811
  () => {
812
+ scriptingBridge?.destroy();
813
+ scriptingBridge = null;
814
+ scriptingManager?.setDocument(null);
749
815
  viewerSearch.reset();
750
816
  formFieldsState.reset();
751
817
  shapeDetectionCache.clearAllShapeCache();
@@ -787,6 +853,11 @@ onBeforeUnmount(() => {
787
853
  scrollContainer.value?.removeEventListener("wheel", onWheel);
788
854
  inertiaPanzoom.detach();
789
855
  resizeObserver?.disconnect();
856
+ scriptingBridge?.destroy();
857
+ scriptingBridge = null;
858
+ scriptingManager?.destroy().catch(() => {
859
+ });
860
+ scriptingManager = null;
790
861
  viewerSearch.reset();
791
862
  shapeDetectionCache.clearAllShapeCache();
792
863
  searchIndex.destroy();
@@ -802,6 +873,16 @@ defineExpose({
802
873
  getKonvaCanvasState,
803
874
  getFormFieldValues: () => formFieldsState.getAllFieldValues(),
804
875
  setFormFieldValue: (fieldName, value) => formFieldsState.setFieldValueByName(fieldName, value),
876
+ /** Add a form field programmatically. Returns the created definition.
877
+ * The new field is exported into the PDF on `exportPdf()` like any
878
+ * field placed via the placement tool. */
879
+ addFormField: (payload) => formFieldsState.addFormField(payload),
880
+ /** Patch a form field by id (rect, name, flags, type-specific props). */
881
+ updateFormField: (id, patch) => formFieldsState.updatePlacedField(id, patch),
882
+ /** Remove a form field by id (drops its value too). */
883
+ removeFormField: (id) => formFieldsState.removePlacedField(id),
884
+ /** All form-field definitions (parsed, detected, and placed), flat. */
885
+ getFormFields: () => formFieldsState.getAllFields(),
805
886
  /** Reactive form-edit-mode flag — read with `.value`. */
806
887
  formEditMode: viewerState.formEditMode,
807
888
  /** Programmatically enter or leave form-edit mode. */
@@ -1,6 +1,7 @@
1
1
  import { type ViewMode } from '../composables/useViewerState.js';
2
- import type { IAnnotationStore, StampDefinition, SignatureHandlers } from '../annotation/engine/types.js';
2
+ import type { AddFormFieldPayload, FormFieldDefinition, IAnnotationStore, StampDefinition, SignatureHandlers } from '../annotation/engine/types.js';
3
3
  import { type ExportPdfOptions } from '../annotation/pdf-export/export.js';
4
+ import type { ViewerMenuItem } from '../menu-items.js';
4
5
  type __VLS_Props = {
5
6
  source: string | Uint8Array | object;
6
7
  textLayer?: boolean;
@@ -21,6 +22,29 @@ type __VLS_Props = {
21
22
  * with selection chrome, can be moved/resized, and the property panel
22
23
  * is shown. Supports v-model via `v-model:form-edit-mode`. */
23
24
  formEditMode?: boolean;
25
+ /** Map of roleId → CSS color used to color-code form fields in
26
+ * form-edit mode. The editor stays unopinionated about role
27
+ * definitions: the host owns the role list, names, and color picker,
28
+ * and just hands a flat lookup table to the viewer. Unknown or
29
+ * missing ids fall back to the default blue. */
30
+ roleColors?: Record<string, string>;
31
+ /** Active role id new field placements are auto-tagged with. The host
32
+ * drives this — typically by binding it to a role picker in its own
33
+ * UI. Supports v-model via `v-model:active-role-id`. */
34
+ activeRoleId?: string | null;
35
+ /** Execute embedded JavaScript inside the PDF — field-level AA scripts
36
+ * (Mouse Up, Calculate, Format, Validate, Keystroke), document-level
37
+ * open actions, and page-level scripts. Powered by pdf.js's QuickJS
38
+ * WASM sandbox (no DOM/network/host-JS escape), but arbitrary JS from
39
+ * untrusted PDFs is still real attack surface — default is `false`.
40
+ * Treated as read-once per document; toggling at runtime requires
41
+ * changing the `source` prop or remounting the component. */
42
+ scripting?: boolean;
43
+ /** Extra items appended to the default burger menu (after Download
44
+ * and the form-field-detection toggle). Use this to expose app-level
45
+ * toggles or actions without replacing the entire header. Ignored
46
+ * when the host supplies a custom `#header` slot. */
47
+ menuItems?: ViewerMenuItem[];
24
48
  };
25
49
  declare function exportPdf(options?: ExportPdfOptions): Promise<Uint8Array>;
26
50
  type ImportMode = 'replace' | 'merge';
@@ -44,6 +68,16 @@ declare const __VLS_base: import("vue").DefineComponent<__VLS_Props, {
44
68
  getKonvaCanvasState: typeof getKonvaCanvasState;
45
69
  getFormFieldValues: () => import("../annotation/engine/types.js").FormFieldValue[];
46
70
  setFormFieldValue: (fieldName: string, value: string | boolean | string[]) => void;
71
+ /** Add a form field programmatically. Returns the created definition.
72
+ * The new field is exported into the PDF on `exportPdf()` like any
73
+ * field placed via the placement tool. */
74
+ addFormField: (payload: AddFormFieldPayload) => FormFieldDefinition;
75
+ /** Patch a form field by id (rect, name, flags, type-specific props). */
76
+ updateFormField: (id: string, patch: Partial<FormFieldDefinition>) => void;
77
+ /** Remove a form field by id (drops its value too). */
78
+ removeFormField: (id: string) => void;
79
+ /** All form-field definitions (parsed, detected, and placed), flat. */
80
+ getFormFields: () => FormFieldDefinition[];
47
81
  /** Reactive form-edit-mode flag — read with `.value`. */
48
82
  formEditMode: import("vue").Ref<boolean, boolean>;
49
83
  /** Programmatically enter or leave form-edit mode. */
@@ -52,19 +86,25 @@ declare const __VLS_base: import("vue").DefineComponent<__VLS_Props, {
52
86
  toggleFormEditMode: () => boolean;
53
87
  }, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {
54
88
  "update:formEditMode": (value: boolean) => any;
89
+ "update:activeRoleId": (value: string | null) => any;
55
90
  }, string, import("vue").PublicProps, Readonly<__VLS_Props> & Readonly<{
56
91
  "onUpdate:formEditMode"?: ((value: boolean) => any) | undefined;
92
+ "onUpdate:activeRoleId"?: ((value: string | null) => any) | undefined;
57
93
  }>, {
58
94
  userName: string;
59
95
  freehandGroupingDelay: number;
60
96
  zoom: number;
61
97
  readonly: boolean;
62
98
  active: boolean;
99
+ menuItems: ViewerMenuItem[];
100
+ scripting: boolean;
63
101
  stamps: StampDefinition[];
64
102
  signatureHandlers: SignatureHandlers;
65
103
  viewMode: ViewMode;
66
104
  shapeDetection: boolean;
67
105
  formEditMode: boolean;
106
+ roleColors: Record<string, string>;
107
+ activeRoleId: string | null;
68
108
  }, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
69
109
  declare const __VLS_export: __VLS_WithSlots<typeof __VLS_base, __VLS_Slots>;
70
110
  declare const _default: typeof __VLS_export;
@@ -1,3 +1,9 @@
1
- declare const __VLS_export: import("vue").DefineComponent<{}, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {}, string, import("vue").PublicProps, Readonly<{}> & Readonly<{}>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, true, {}, any>;
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>;
2
8
  declare const _default: typeof __VLS_export;
3
9
  export default _default;
@@ -26,6 +26,39 @@
26
26
  data-testid="viewer-toggle-shape-detection"
27
27
  @click="state.shapeDetection.value = !state.shapeDetection.value"
28
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>
29
62
  </div>
30
63
  </template>
31
64
  </UPopover>
@@ -89,6 +122,9 @@ import ActionTools from "./tools/ActionTools.vue";
89
122
  import PageInfo from "./tools/PageInfo.vue";
90
123
  import SearchTool from "./tools/SearchTool.vue";
91
124
  import ToolProperties from "./tools/ToolProperties.vue";
125
+ const props = defineProps({
126
+ menuItems: { type: Array, required: false }
127
+ });
92
128
  const state = useViewerState();
93
129
  const styleVersion = ref(0);
94
130
  function bumpStyleVersion() {
@@ -1,3 +1,9 @@
1
- declare const __VLS_export: import("vue").DefineComponent<{}, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {}, string, import("vue").PublicProps, Readonly<{}> & Readonly<{}>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, true, {}, any>;
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>;
2
8
  declare const _default: typeof __VLS_export;
3
9
  export default _default;
@@ -1,6 +1,7 @@
1
1
  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
+ import type { ViewerMenuItem } from '../menu-items.js';
4
5
  export interface ViewerTabItem {
5
6
  /** Unique identifier for the tab. */
6
7
  id: string;
@@ -18,6 +19,9 @@ export interface ViewerTabItem {
18
19
  zoom?: number;
19
20
  /** Enable shape detection for this document. */
20
21
  shapeDetection?: boolean;
22
+ /** Execute embedded PDF JavaScript for this document. See KViewer's
23
+ * `scripting` prop — default false, opt-in per tab. */
24
+ scripting?: boolean;
21
25
  }
22
26
  export interface AddTabOptions {
23
27
  /** Insert at specific index. Defaults to end. */
@@ -48,6 +52,16 @@ type __VLS_Props = {
48
52
  zoom?: number;
49
53
  /** Minimum number of tabs -- prevents closing below this count. */
50
54
  minTabs?: number;
55
+ /** Forwarded to every Viewer instance — see KViewer's `roleColors`. */
56
+ roleColors?: Record<string, string>;
57
+ /** Forwarded to every Viewer instance — see KViewer's `activeRoleId`.
58
+ * Supports v-model via `v-model:active-role-id`. */
59
+ activeRoleId?: string | null;
60
+ /** Forwarded to every Viewer instance — see KViewer's `scripting`.
61
+ * Per-tab override via `ViewerTabItem.scripting` wins when set. */
62
+ scripting?: boolean;
63
+ /** Forwarded to every Viewer instance — see KViewer's `menuItems`. */
64
+ menuItems?: ViewerMenuItem[];
51
65
  };
52
66
  declare function addTab(item: Omit<ViewerTabItem, 'id'> & {
53
67
  id?: string;
@@ -55,7 +69,7 @@ declare function addTab(item: Omit<ViewerTabItem, 'id'> & {
55
69
  declare function removeTab(id: string): boolean;
56
70
  declare function activateTab(id: string): void;
57
71
  declare function getViewer(id: string): ComponentPublicInstance | null;
58
- declare var __VLS_7: {}, __VLS_21: {}, __VLS_30: {
72
+ declare var __VLS_7: {}, __VLS_21: {}, __VLS_32: {
59
73
  tab: {
60
74
  id: string;
61
75
  label: string;
@@ -153,8 +167,9 @@ declare var __VLS_7: {}, __VLS_21: {}, __VLS_30: {
153
167
  viewMode?: ViewMode | undefined;
154
168
  zoom?: number | undefined;
155
169
  shapeDetection?: boolean | undefined;
170
+ scripting?: boolean | undefined;
156
171
  };
157
- }, __VLS_33: {
172
+ }, __VLS_35: {
158
173
  tab: {
159
174
  id: string;
160
175
  label: string;
@@ -252,18 +267,19 @@ declare var __VLS_7: {}, __VLS_21: {}, __VLS_30: {
252
267
  viewMode?: ViewMode | undefined;
253
268
  zoom?: number | undefined;
254
269
  shapeDetection?: boolean | undefined;
270
+ scripting?: boolean | undefined;
255
271
  };
256
- }, __VLS_35: {};
272
+ }, __VLS_37: {};
257
273
  type __VLS_Slots = {} & {
258
274
  'tabs-leading'?: (props: typeof __VLS_7) => any;
259
275
  } & {
260
276
  'tabs-trailing'?: (props: typeof __VLS_21) => any;
261
277
  } & {
262
- header?: (props: typeof __VLS_30) => any;
278
+ header?: (props: typeof __VLS_32) => any;
263
279
  } & {
264
- footer?: (props: typeof __VLS_33) => any;
280
+ footer?: (props: typeof __VLS_35) => any;
265
281
  } & {
266
- empty?: (props: typeof __VLS_35) => any;
282
+ empty?: (props: typeof __VLS_37) => any;
267
283
  };
268
284
  declare const __VLS_base: import("vue").DefineComponent<__VLS_Props, {
269
285
  addTab: typeof addTab;
@@ -368,13 +384,16 @@ declare const __VLS_base: import("vue").DefineComponent<__VLS_Props, {
368
384
  viewMode?: ViewMode | undefined;
369
385
  zoom?: number | undefined;
370
386
  shapeDetection?: boolean | undefined;
387
+ scripting?: boolean | undefined;
371
388
  }[];
372
389
  }, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {
390
+ "update:activeRoleId": (value: string | null) => any;
373
391
  "update:activeTab": (id: string) => any;
374
392
  "tab-added": (tab: ViewerTabItem) => any;
375
393
  "tab-close": (id: string) => any;
376
394
  "tab-removed": (id: string) => any;
377
395
  }, string, import("vue").PublicProps, Readonly<__VLS_Props> & Readonly<{
396
+ "onUpdate:activeRoleId"?: ((value: string | null) => any) | undefined;
378
397
  "onUpdate:activeTab"?: ((id: string) => any) | undefined;
379
398
  "onTab-added"?: ((tab: ViewerTabItem) => any) | undefined;
380
399
  "onTab-close"?: ((id: string) => any) | undefined;
@@ -382,9 +401,13 @@ declare const __VLS_base: import("vue").DefineComponent<__VLS_Props, {
382
401
  }>, {
383
402
  userName: string;
384
403
  zoom: number;
404
+ menuItems: ViewerMenuItem[];
405
+ scripting: boolean;
385
406
  stamps: StampDefinition[];
386
407
  signatureHandlers: SignatureHandlers;
387
408
  viewMode: ViewMode;
409
+ roleColors: Record<string, string>;
410
+ activeRoleId: string | null;
388
411
  defaultActiveTab: string;
389
412
  minTabs: number;
390
413
  }, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
@@ -59,6 +59,11 @@
59
59
  :zoom="activeTab.zoom ?? zoom"
60
60
  :readonly="readonly"
61
61
  :shape-detection="activeTab.shapeDetection ?? shapeDetection"
62
+ :scripting="activeTab.scripting ?? scripting"
63
+ :menu-items="menuItems"
64
+ :role-colors="roleColors"
65
+ :active-role-id="activeRoleId"
66
+ @update:active-role-id="(v) => emit('update:activeRoleId', v)"
62
67
  >
63
68
  <template v-if="$slots.header" #header>
64
69
  <slot name="header" :tab="activeTab" />
@@ -91,9 +96,13 @@ const props = defineProps({
91
96
  shapeDetection: { type: Boolean, required: false },
92
97
  viewMode: { type: String, required: false, default: "fit-width" },
93
98
  zoom: { type: Number, required: false, default: 1 },
94
- minTabs: { type: Number, required: false, default: 0 }
99
+ minTabs: { type: Number, required: false, default: 0 },
100
+ roleColors: { type: Object, required: false, default: void 0 },
101
+ activeRoleId: { type: [String, null], required: false, default: void 0 },
102
+ scripting: { type: Boolean, required: false, default: false },
103
+ menuItems: { type: Array, required: false, default: void 0 }
95
104
  });
96
- const emit = defineEmits(["update:activeTab", "tab-added", "tab-close", "tab-removed"]);
105
+ const emit = defineEmits(["update:activeTab", "tab-added", "tab-close", "tab-removed", "update:activeRoleId"]);
97
106
  let idCounter = 0;
98
107
  function generateId() {
99
108
  return `tab-${Date.now()}-${++idCounter}`;
@@ -1,6 +1,7 @@
1
1
  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
+ import type { ViewerMenuItem } from '../menu-items.js';
4
5
  export interface ViewerTabItem {
5
6
  /** Unique identifier for the tab. */
6
7
  id: string;
@@ -18,6 +19,9 @@ export interface ViewerTabItem {
18
19
  zoom?: number;
19
20
  /** Enable shape detection for this document. */
20
21
  shapeDetection?: boolean;
22
+ /** Execute embedded PDF JavaScript for this document. See KViewer's
23
+ * `scripting` prop — default false, opt-in per tab. */
24
+ scripting?: boolean;
21
25
  }
22
26
  export interface AddTabOptions {
23
27
  /** Insert at specific index. Defaults to end. */
@@ -48,6 +52,16 @@ type __VLS_Props = {
48
52
  zoom?: number;
49
53
  /** Minimum number of tabs -- prevents closing below this count. */
50
54
  minTabs?: number;
55
+ /** Forwarded to every Viewer instance — see KViewer's `roleColors`. */
56
+ roleColors?: Record<string, string>;
57
+ /** Forwarded to every Viewer instance — see KViewer's `activeRoleId`.
58
+ * Supports v-model via `v-model:active-role-id`. */
59
+ activeRoleId?: string | null;
60
+ /** Forwarded to every Viewer instance — see KViewer's `scripting`.
61
+ * Per-tab override via `ViewerTabItem.scripting` wins when set. */
62
+ scripting?: boolean;
63
+ /** Forwarded to every Viewer instance — see KViewer's `menuItems`. */
64
+ menuItems?: ViewerMenuItem[];
51
65
  };
52
66
  declare function addTab(item: Omit<ViewerTabItem, 'id'> & {
53
67
  id?: string;
@@ -55,7 +69,7 @@ declare function addTab(item: Omit<ViewerTabItem, 'id'> & {
55
69
  declare function removeTab(id: string): boolean;
56
70
  declare function activateTab(id: string): void;
57
71
  declare function getViewer(id: string): ComponentPublicInstance | null;
58
- declare var __VLS_7: {}, __VLS_21: {}, __VLS_30: {
72
+ declare var __VLS_7: {}, __VLS_21: {}, __VLS_32: {
59
73
  tab: {
60
74
  id: string;
61
75
  label: string;
@@ -153,8 +167,9 @@ declare var __VLS_7: {}, __VLS_21: {}, __VLS_30: {
153
167
  viewMode?: ViewMode | undefined;
154
168
  zoom?: number | undefined;
155
169
  shapeDetection?: boolean | undefined;
170
+ scripting?: boolean | undefined;
156
171
  };
157
- }, __VLS_33: {
172
+ }, __VLS_35: {
158
173
  tab: {
159
174
  id: string;
160
175
  label: string;
@@ -252,18 +267,19 @@ declare var __VLS_7: {}, __VLS_21: {}, __VLS_30: {
252
267
  viewMode?: ViewMode | undefined;
253
268
  zoom?: number | undefined;
254
269
  shapeDetection?: boolean | undefined;
270
+ scripting?: boolean | undefined;
255
271
  };
256
- }, __VLS_35: {};
272
+ }, __VLS_37: {};
257
273
  type __VLS_Slots = {} & {
258
274
  'tabs-leading'?: (props: typeof __VLS_7) => any;
259
275
  } & {
260
276
  'tabs-trailing'?: (props: typeof __VLS_21) => any;
261
277
  } & {
262
- header?: (props: typeof __VLS_30) => any;
278
+ header?: (props: typeof __VLS_32) => any;
263
279
  } & {
264
- footer?: (props: typeof __VLS_33) => any;
280
+ footer?: (props: typeof __VLS_35) => any;
265
281
  } & {
266
- empty?: (props: typeof __VLS_35) => any;
282
+ empty?: (props: typeof __VLS_37) => any;
267
283
  };
268
284
  declare const __VLS_base: import("vue").DefineComponent<__VLS_Props, {
269
285
  addTab: typeof addTab;
@@ -368,13 +384,16 @@ declare const __VLS_base: import("vue").DefineComponent<__VLS_Props, {
368
384
  viewMode?: ViewMode | undefined;
369
385
  zoom?: number | undefined;
370
386
  shapeDetection?: boolean | undefined;
387
+ scripting?: boolean | undefined;
371
388
  }[];
372
389
  }, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {
390
+ "update:activeRoleId": (value: string | null) => any;
373
391
  "update:activeTab": (id: string) => any;
374
392
  "tab-added": (tab: ViewerTabItem) => any;
375
393
  "tab-close": (id: string) => any;
376
394
  "tab-removed": (id: string) => any;
377
395
  }, string, import("vue").PublicProps, Readonly<__VLS_Props> & Readonly<{
396
+ "onUpdate:activeRoleId"?: ((value: string | null) => any) | undefined;
378
397
  "onUpdate:activeTab"?: ((id: string) => any) | undefined;
379
398
  "onTab-added"?: ((tab: ViewerTabItem) => any) | undefined;
380
399
  "onTab-close"?: ((id: string) => any) | undefined;
@@ -382,9 +401,13 @@ declare const __VLS_base: import("vue").DefineComponent<__VLS_Props, {
382
401
  }>, {
383
402
  userName: string;
384
403
  zoom: number;
404
+ menuItems: ViewerMenuItem[];
405
+ scripting: boolean;
385
406
  stamps: StampDefinition[];
386
407
  signatureHandlers: SignatureHandlers;
387
408
  viewMode: ViewMode;
409
+ roleColors: Record<string, string>;
410
+ activeRoleId: string | null;
388
411
  defaultActiveTab: string;
389
412
  minTabs: number;
390
413
  }, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
@@ -6,7 +6,8 @@
6
6
  'kviewer-form-field--placed': isPlaced,
7
7
  'kviewer-form-field--selected': props.selected
8
8
  }"
9
- :style="positionStyle"
9
+ :style="[positionStyle, { '--kvw-field-color': chromeColor }]"
10
+ :data-element-id="props.field.id"
10
11
  >
11
12
  <div
12
13
  class="kviewer-form-field__body"
@@ -34,6 +35,7 @@
34
35
  <script setup>
35
36
  import { computed } from "vue";
36
37
  import { useViewerState } from "../../composables/useViewerState";
38
+ import { useFormFields } from "../../composables/useFormFields";
37
39
  import FormTextField from "./FormTextField.vue";
38
40
  import FormCheckbox from "./FormCheckbox.vue";
39
41
  import FormRadioButton from "./FormRadioButton.vue";
@@ -51,9 +53,15 @@ const props = defineProps({
51
53
  });
52
54
  const emit = defineEmits(["select"]);
53
55
  const state = useViewerState();
56
+ const formFields = useFormFields();
54
57
  const isPlaced = computed(() => props.field.origin === "placed");
55
58
  const isFormEditMode = computed(() => state.formEditMode.value);
56
59
  const showChrome = computed(() => isFormEditMode.value);
60
+ const chromeColor = computed(() => {
61
+ const id = props.field.roleId;
62
+ if (!id) return "#1677ff";
63
+ return formFields.roleColors.value[id] ?? "#1677ff";
64
+ });
57
65
  const positionStyle = computed(() => {
58
66
  const rect = props.field.rect;
59
67
  const vx = props.viewOffsetX ?? 0;
@@ -73,5 +81,5 @@ const positionStyle = computed(() => {
73
81
  </script>
74
82
 
75
83
  <style>
76
- .kviewer-form-field__body{height:100%;width:100%}.kviewer-form-field__body--locked{pointer-events:none}.kviewer-form-field--editable{outline:1px dashed rgba(22,119,255,.45);outline-offset:0}.kviewer-form-field--editable.kviewer-form-field--placed{outline-color:rgba(22,119,255,.75)}.kviewer-form-field--editable.kviewer-form-field--selected{outline:none}
84
+ .kviewer-form-field__body{height:100%;width:100%}.kviewer-form-field__body--locked{pointer-events:none}.kviewer-form-field--editable{outline:1px dashed color-mix(in srgb,var(--kvw-field-color,#1677ff) 45%,transparent);outline-offset:0}.kviewer-form-field--editable.kviewer-form-field--placed{outline-color:color-mix(in srgb,var(--kvw-field-color,#1677ff) 75%,transparent)}.kviewer-form-field--editable.kviewer-form-field--selected{outline:none}
77
85
  </style>
@@ -127,5 +127,5 @@ function onHandlePointerDown(e, name) {
127
127
  </script>
128
128
 
129
129
  <style>
130
- .kviewer-placed-chrome{cursor:move;inset:0;pointer-events:auto;position:absolute;touch-action:none}.kviewer-placed-chrome--selected{outline:1.5px dashed #1677ff;outline-offset:0}.kviewer-placed-chrome__grip{align-items:center;background:rgba(22,119,255,.15);border:1px solid rgba(22,119,255,.4);border-radius:calc(3px*var(--kvw-handle-zoom, 1));bottom:calc(100% + 3px*var(--kvw-handle-zoom, 1));color:#1677ff;cursor:move;display:flex;font-size:calc(9px*var(--kvw-handle-zoom, 1));gap:calc(3px*var(--kvw-handle-zoom, 1));height:calc(16px*var(--kvw-handle-zoom, 1));left:0;line-height:1;overflow:hidden;padding:0 calc(6px*var(--kvw-handle-zoom, 1)) 0 calc(2px*var(--kvw-handle-zoom, 1));pointer-events:auto;position:absolute;right:0;touch-action:none;-webkit-user-select:none;-moz-user-select:none;user-select:none}.kviewer-placed-chrome__grip-dots{color:#1677ff;flex:0 0 auto;height:calc(10px*var(--kvw-handle-zoom, 1));width:calc(10px*var(--kvw-handle-zoom, 1))}.kviewer-placed-chrome__grip-label{flex:1 1 auto;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.kviewer-placed-chrome__handle{background:#fff;border:1px solid #1677ff;border-radius:2px;height:calc(8px*var(--kvw-handle-zoom, 1));pointer-events:auto;position:absolute;touch-action:none;width:calc(8px*var(--kvw-handle-zoom, 1))}.kviewer-placed-chrome__handle:before{content:"";inset:calc(-8px*var(--kvw-handle-zoom, 1));position:absolute}.kviewer-placed-chrome__handle--n{left:50%;top:calc(-4px*var(--kvw-handle-zoom, 1));transform:translateX(-50%)}.kviewer-placed-chrome__handle--s{bottom:calc(-4px*var(--kvw-handle-zoom, 1));left:50%;transform:translateX(-50%)}.kviewer-placed-chrome__handle--e{right:calc(-4px*var(--kvw-handle-zoom, 1));top:50%;transform:translateY(-50%)}.kviewer-placed-chrome__handle--w{left:calc(-4px*var(--kvw-handle-zoom, 1));top:50%;transform:translateY(-50%)}.kviewer-placed-chrome__handle--ne{right:calc(-4px*var(--kvw-handle-zoom, 1));top:calc(-4px*var(--kvw-handle-zoom, 1))}.kviewer-placed-chrome__handle--nw{left:calc(-4px*var(--kvw-handle-zoom, 1));top:calc(-4px*var(--kvw-handle-zoom, 1))}.kviewer-placed-chrome__handle--se{bottom:calc(-4px*var(--kvw-handle-zoom, 1));right:calc(-4px*var(--kvw-handle-zoom, 1))}.kviewer-placed-chrome__handle--sw{bottom:calc(-4px*var(--kvw-handle-zoom, 1));left:calc(-4px*var(--kvw-handle-zoom, 1))}
130
+ .kviewer-placed-chrome{cursor:move;inset:0;pointer-events:auto;position:absolute;touch-action:none}.kviewer-placed-chrome--selected{outline:1.5px dashed var(--kvw-field-color,#1677ff);outline-offset:0}.kviewer-placed-chrome__grip{align-items:center;background:color-mix(in srgb,var(--kvw-field-color,#1677ff) 15%,transparent);border:1px solid color-mix(in srgb,var(--kvw-field-color,#1677ff) 40%,transparent);border-radius:calc(3px*var(--kvw-handle-zoom, 1));bottom:calc(100% + 3px*var(--kvw-handle-zoom, 1));color:var(--kvw-field-color,#1677ff);cursor:move;display:flex;font-size:calc(9px*var(--kvw-handle-zoom, 1));gap:calc(3px*var(--kvw-handle-zoom, 1));height:calc(16px*var(--kvw-handle-zoom, 1));left:0;line-height:1;overflow:hidden;padding:0 calc(6px*var(--kvw-handle-zoom, 1)) 0 calc(2px*var(--kvw-handle-zoom, 1));pointer-events:auto;position:absolute;right:0;touch-action:none;-webkit-user-select:none;-moz-user-select:none;user-select:none}.kviewer-placed-chrome__grip-dots{color:var(--kvw-field-color,#1677ff);flex:0 0 auto;height:calc(10px*var(--kvw-handle-zoom, 1));width:calc(10px*var(--kvw-handle-zoom, 1))}.kviewer-placed-chrome__grip-label{flex:1 1 auto;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.kviewer-placed-chrome__handle{background:#fff;border:1px solid var(--kvw-field-color,#1677ff);border-radius:2px;height:calc(8px*var(--kvw-handle-zoom, 1));pointer-events:auto;position:absolute;touch-action:none;width:calc(8px*var(--kvw-handle-zoom, 1))}.kviewer-placed-chrome__handle:before{content:"";inset:calc(-8px*var(--kvw-handle-zoom, 1));position:absolute}.kviewer-placed-chrome__handle--n{left:50%;top:calc(-4px*var(--kvw-handle-zoom, 1));transform:translateX(-50%)}.kviewer-placed-chrome__handle--s{bottom:calc(-4px*var(--kvw-handle-zoom, 1));left:50%;transform:translateX(-50%)}.kviewer-placed-chrome__handle--e{right:calc(-4px*var(--kvw-handle-zoom, 1));top:50%;transform:translateY(-50%)}.kviewer-placed-chrome__handle--w{left:calc(-4px*var(--kvw-handle-zoom, 1));top:50%;transform:translateY(-50%)}.kviewer-placed-chrome__handle--ne{right:calc(-4px*var(--kvw-handle-zoom, 1));top:calc(-4px*var(--kvw-handle-zoom, 1))}.kviewer-placed-chrome__handle--nw{left:calc(-4px*var(--kvw-handle-zoom, 1));top:calc(-4px*var(--kvw-handle-zoom, 1))}.kviewer-placed-chrome__handle--se{bottom:calc(-4px*var(--kvw-handle-zoom, 1));right:calc(-4px*var(--kvw-handle-zoom, 1))}.kviewer-placed-chrome__handle--sw{bottom:calc(-4px*var(--kvw-handle-zoom, 1));left:calc(-4px*var(--kvw-handle-zoom, 1))}
131
131
  </style>
@@ -5,6 +5,7 @@ export declare function createAnnotationEngine(viewerState: ViewerState, options
5
5
  onRequestTextInput: PainterCallbacks['onRequestTextInput'];
6
6
  onRequestDeleteConfirm?: PainterCallbacks['onRequestDeleteConfirm'];
7
7
  onPlaceField?: PainterCallbacks['onPlaceField'];
8
+ getFormFieldPreviewColor?: PainterCallbacks['getFormFieldPreviewColor'];
8
9
  freehandGroupingDelay?: number;
9
10
  setExternalGesturesEnabled?: (enabled: boolean) => void;
10
11
  }): Painter;
@@ -61,7 +61,8 @@ export function createAnnotationEngine(viewerState, options) {
61
61
  },
62
62
  onRequestTextInput: options.onRequestTextInput,
63
63
  onRequestDeleteConfirm: options.onRequestDeleteConfirm,
64
- onPlaceField: options.onPlaceField
64
+ onPlaceField: options.onPlaceField,
65
+ getFormFieldPreviewColor: options.getFormFieldPreviewColor
65
66
  };
66
67
  const painter = new Painter({
67
68
  userName: options.userName ?? "User",