@brftech/filex-core 0.42.2 → 0.43.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +14 -2
- package/dist/ArchiveViewer-BqU761Wk.js +149 -0
- package/dist/ArchiveViewer-BqU761Wk.js.map +1 -0
- package/dist/CsvViewer-ByHVK5ga.js +172 -0
- package/dist/CsvViewer-ByHVK5ga.js.map +1 -0
- package/dist/{DrawioViewer-CC6rJXeP.js → DrawioViewer-Bgrj8-zG.js} +45 -44
- package/dist/DrawioViewer-Bgrj8-zG.js.map +1 -0
- package/dist/{EpubViewer-CAoTSXTO.js → EpubViewer-DJwUKlHW.js} +50 -50
- package/dist/EpubViewer-DJwUKlHW.js.map +1 -0
- package/dist/IpynbViewer-Bz0eJ4NS.js +184 -0
- package/dist/IpynbViewer-Bz0eJ4NS.js.map +1 -0
- package/dist/MermaidViewer-CEDTD_pp.js +136 -0
- package/dist/MermaidViewer-CEDTD_pp.js.map +1 -0
- package/dist/PsdViewer-CZcxUpZO.js +121 -0
- package/dist/PsdViewer-CZcxUpZO.js.map +1 -0
- package/dist/TiffViewer-BwI62CbQ.js +141 -0
- package/dist/TiffViewer-BwI62CbQ.js.map +1 -0
- package/dist/{Viewer3D-CdZeK4j4.js → Viewer3D-DoeZcuMm.js} +16 -16
- package/dist/Viewer3D-DoeZcuMm.js.map +1 -0
- package/dist/filex-core.js +475 -219
- package/dist/filex-core.umd.cjs +165 -108
- package/dist/filex-core.umd.cjs.map +1 -1
- package/dist/fonts/caveat-latin-ext.woff2 +0 -0
- package/dist/fonts/caveat-latin.woff2 +0 -0
- package/dist/fonts/dancing-script-latin-ext.woff2 +0 -0
- package/dist/fonts/dancing-script-latin.woff2 +0 -0
- package/dist/fonts/homemade-apple-latin.woff2 +0 -0
- package/dist/fonts/source-serif-4-latin-ext.woff2 +0 -0
- package/dist/fonts/source-serif-4-latin.woff2 +0 -0
- package/dist/index-DuWnGuv5.js +35901 -0
- package/dist/index-DuWnGuv5.js.map +1 -0
- package/dist/index.d.ts +5227 -553
- package/dist/pdf-CaPYsOUo.js +21302 -0
- package/dist/pdf-CaPYsOUo.js.map +1 -0
- package/dist/style.css +1 -1
- package/package.json +1 -1
- package/src/FileExplorer.vue +1055 -253
- package/src/assets/fonts/LICENSE-Apache-2.0.txt +202 -0
- package/src/assets/fonts/LICENSE-OFL-1.1.txt +93 -0
- package/src/assets/fonts/README.md +58 -0
- package/src/assets/fonts/caveat-latin-ext.woff2 +0 -0
- package/src/assets/fonts/caveat-latin.woff2 +0 -0
- package/src/assets/fonts/dancing-script-latin-ext.woff2 +0 -0
- package/src/assets/fonts/dancing-script-latin.woff2 +0 -0
- package/src/assets/fonts/homemade-apple-latin.woff2 +0 -0
- package/src/assets/fonts/source-serif-4-latin-ext.woff2 +0 -0
- package/src/assets/fonts/source-serif-4-latin.woff2 +0 -0
- package/src/components/AdvancedSearch.vue +49 -0
- package/src/components/Breadcrumb.vue +5 -5
- package/src/components/ChoiceButtons.vue +152 -0
- package/src/components/CommandPalette.vue +5 -5
- package/src/components/ConnectionGuideView.vue +1 -1
- package/src/components/ConnectionNotice.vue +59 -0
- package/src/components/ConnectionsPanel.vue +30 -568
- package/src/components/ContextMenu.vue +31 -5
- package/src/components/DataTable.vue +1331 -0
- package/src/components/FilePane.vue +3 -0
- package/src/components/FilterBar.vue +12 -5
- package/src/components/GalleryView.vue +50 -2
- package/src/components/GridView.vue +51 -3
- package/src/components/HomeView.vue +18 -6
- package/src/components/InspectorPanel.vue +134 -17
- package/src/components/ListView.vue +366 -900
- package/src/components/LogoMark.vue +57 -0
- package/src/components/NFSExportsPanel.vue +76 -28
- package/src/components/OnboardingTour.vue +19 -7
- package/src/components/OperationsCenter.vue +77 -1
- package/src/components/PendingOpsTray.vue +59 -5
- package/src/components/PresenceBar.vue +7 -7
- package/src/components/ProductVersion.vue +31 -0
- package/src/components/QuickLook.vue +10 -14
- package/src/components/RecentlyOpened.vue +31 -15
- package/src/components/RecoveryKeyModal.vue +1 -1
- package/src/components/RowActions.vue +134 -0
- package/src/components/S3KeysPanel.vue +98 -62
- package/src/components/SSHKeysPanel.vue +79 -30
- package/src/components/SideNav.vue +260 -96
- package/src/components/StarButton.vue +1 -0
- package/src/components/StorageFields.vue +74 -6
- package/src/components/StorageTags.vue +62 -0
- package/src/components/TabBar.vue +1 -1
- package/src/components/TagKindIcon.vue +56 -0
- package/src/components/TagPicker.vue +226 -40
- package/src/components/ThemePalette.vue +4 -4
- package/src/components/TimeZonePicker.vue +29 -10
- package/src/components/TokensPanel.vue +82 -31
- package/src/components/Toolbar.vue +28 -6
- package/src/components/plugin/PluginConfirmModal.vue +54 -0
- package/src/components/plugin/PluginInspectorSection.vue +149 -0
- package/src/components/plugin/PluginPageView.vue +339 -0
- package/src/components/plugin/PluginViewModal.vue +159 -0
- package/src/components/plugin/SurfaceConversation.vue +76 -0
- package/src/components/plugin/SurfaceFooterButtons.vue +73 -0
- package/src/components/plugin/SurfaceRenderer.vue +299 -0
- package/src/components/plugin/SurfaceSections.vue +91 -0
- package/src/components/plugin/nodes/PdfFieldEditor.vue +509 -0
- package/src/components/plugin/nodes/SurfaceFileChooser.vue +94 -0
- package/src/components/plugin/nodes/SurfaceForm.vue +82 -0
- package/src/components/plugin/nodes/SurfaceList.vue +127 -0
- package/src/components/plugin/nodes/SurfacePdfFields.vue +1285 -0
- package/src/components/plugin/nodes/SurfacePeoplePicker.vue +213 -0
- package/src/components/plugin/nodes/SurfacePinInput.vue +57 -0
- package/src/components/plugin/nodes/SurfacePreview.vue +119 -0
- package/src/components/plugin/nodes/SurfaceProgress.vue +48 -0
- package/src/components/plugin/nodes/SurfaceSignaturePad.vue +472 -0
- package/src/components/plugin/nodes/SurfaceSteps.vue +53 -0
- package/src/components/public/PublicLanguagePicker.vue +58 -0
- package/src/components/public/PublicLinkPage.vue +286 -0
- package/src/components/public/PublicLinkPreview.vue +85 -0
- package/src/components/public/PublicPinGate.vue +108 -0
- package/src/components/public/PublicRequestBody.vue +204 -0
- package/src/components/public/PublicShareBody.vue +188 -0
- package/src/components/public/PublicShell.vue +332 -0
- package/src/composables/useConnections.ts +8 -2
- package/src/composables/useFileApi.ts +183 -44
- package/src/composables/useLocale.ts +277 -41
- package/src/composables/useNFSExports.ts +8 -10
- package/src/composables/useOperations.ts +44 -3
- package/src/composables/usePendingOps.ts +59 -8
- package/src/composables/usePluginActions.ts +121 -0
- package/src/composables/usePluginSurface.ts +223 -0
- package/src/composables/usePublicBranding.ts +172 -0
- package/src/composables/usePublicLink.ts +573 -0
- package/src/composables/usePublicPage.ts +61 -0
- package/src/composables/useS3Keys.ts +17 -12
- package/src/composables/useSSHKeys.ts +8 -10
- package/src/composables/useTokens.ts +8 -12
- package/src/composables/useUploadChunked.ts +18 -8
- package/src/composables/useViewerFetch.ts +6 -3
- package/src/index.ts +404 -8
- package/src/lib/actionIcons.ts +46 -1
- package/src/lib/advSearch.ts +14 -7
- package/src/lib/appLock.ts +130 -0
- package/src/lib/connection.ts +110 -0
- package/src/lib/destinationTree.ts +19 -3
- package/src/lib/direction.ts +353 -0
- package/src/lib/dragGhost.ts +6 -0
- package/src/lib/dropLimits.ts +69 -0
- package/src/lib/errorWords.ts +394 -0
- package/src/lib/externalReach.ts +5 -1
- package/src/lib/fileFilters.ts +7 -2
- package/src/lib/fileIcons.ts +4 -0
- package/src/lib/internalPaths.ts +77 -0
- package/src/lib/listInput.ts +64 -0
- package/src/lib/listing.ts +58 -23
- package/src/lib/modalStack.ts +26 -0
- package/src/lib/nodeRow.ts +18 -3
- package/src/lib/pdfFieldRules.ts +126 -0
- package/src/lib/pdfFields.ts +589 -0
- package/src/lib/pdfFieldsGeom.ts +203 -0
- package/src/lib/pdfjsLoader.ts +51 -0
- package/src/lib/personName.ts +47 -0
- package/src/lib/pluginApplies.ts +123 -0
- package/src/lib/pluginLabel.ts +72 -0
- package/src/lib/pluginMenu.ts +253 -0
- package/src/lib/pluginPage.ts +58 -0
- package/src/lib/plural.ts +181 -0
- package/src/lib/popupLayer.ts +103 -0
- package/src/lib/prefs.ts +563 -0
- package/src/lib/productVersion.ts +35 -0
- package/src/lib/publicLayout.ts +39 -0
- package/src/lib/serviceGate.ts +104 -0
- package/src/lib/shareTtl.ts +25 -11
- package/src/lib/signFonts.ts +85 -0
- package/src/lib/signaturePad.ts +111 -0
- package/src/lib/sortOrder.ts +100 -38
- package/src/lib/star.ts +7 -1
- package/src/lib/surfaceCell.ts +41 -0
- package/src/lib/surfaceConditions.ts +156 -0
- package/src/lib/surfaceOpen.ts +106 -0
- package/src/lib/surfaceSteps.ts +68 -0
- package/src/lib/surfaceValues.ts +191 -0
- package/src/lib/symlink.ts +150 -0
- package/src/lib/tableColumns.ts +493 -0
- package/src/lib/tableEnv.ts +34 -0
- package/src/lib/tablePrefs.ts +84 -0
- package/src/lib/tags.ts +121 -12
- package/src/lib/themes.ts +286 -18
- package/src/lib/tour.ts +152 -0
- package/src/lib/trashEmpty.ts +111 -0
- package/src/lib/trashTimeLeft.ts +23 -0
- package/src/lib/uiLocales.ts +460 -0
- package/src/lib/viewPrefs.ts +696 -660
- package/src/lib/viewPrefsHttp.ts +78 -0
- package/src/locales/en.ts +515 -117
- package/src/locales/resolve.ts +17 -8
- package/src/locales/tr.ts +536 -152
- package/src/modals/ConvertModal.vue +62 -11
- package/src/modals/DestinationPickerModal.vue +59 -16
- package/src/modals/Modal.vue +113 -23
- package/src/modals/NewDocumentModal.vue +16 -6
- package/src/modals/NewFolderModal.vue +8 -0
- package/src/modals/PermissionsModal.vue +272 -163
- package/src/modals/PreviewModal.vue +119 -20
- package/src/modals/RenameModal.vue +37 -1
- package/src/styles/base.css +2890 -260
- package/src/styles/sign-fonts.css +118 -0
- package/src/styles/variables.css +60 -0
- package/src/types/Connections.ts +17 -0
- package/src/types/ExplorerConfig.ts +117 -1
- package/src/types/FileNode.ts +50 -0
- package/src/types/Plugins.ts +494 -0
- package/src/types/Public.ts +212 -0
- package/src/viewers/ArchiveViewer.vue +93 -48
- package/src/viewers/CsvViewer.vue +99 -53
- package/src/viewers/DrawioViewer.vue +35 -9
- package/src/viewers/EpubViewer.vue +7 -8
- package/src/viewers/IpynbViewer.vue +3 -1
- package/src/viewers/MermaidViewer.vue +4 -5
- package/src/viewers/PdfViewer.vue +8 -17
- package/src/viewers/PsdViewer.vue +2 -4
- package/src/viewers/TiffViewer.vue +4 -5
- package/src/viewers/Viewer3D.vue +1 -1
- package/LICENSE +0 -21
- package/dist/ArchiveViewer-BUQWLhwX.js +0 -124
- package/dist/ArchiveViewer-BUQWLhwX.js.map +0 -1
- package/dist/CsvViewer-Bg2_aqzc.js +0 -141
- package/dist/CsvViewer-Bg2_aqzc.js.map +0 -1
- package/dist/DrawioViewer-CC6rJXeP.js.map +0 -1
- package/dist/EpubViewer-CAoTSXTO.js.map +0 -1
- package/dist/IpynbViewer-D0GwGpnE.js +0 -184
- package/dist/IpynbViewer-D0GwGpnE.js.map +0 -1
- package/dist/MermaidViewer-DGwAvRZg.js +0 -137
- package/dist/MermaidViewer-DGwAvRZg.js.map +0 -1
- package/dist/PsdViewer-DhGweA0m.js +0 -122
- package/dist/PsdViewer-DhGweA0m.js.map +0 -1
- package/dist/TiffViewer-D0AurvAh.js +0 -142
- package/dist/TiffViewer-D0AurvAh.js.map +0 -1
- package/dist/Viewer3D-CdZeK4j4.js.map +0 -1
- package/dist/index-BArI-B1M.js +0 -26911
- package/dist/index-BArI-B1M.js.map +0 -1
- package/dist/useViewerFetch-czqbd2Lj.js +0 -25
- package/dist/useViewerFetch-czqbd2Lj.js.map +0 -1
- package/src/composables/useScrolledX.ts +0 -21
|
@@ -0,0 +1,472 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
/**
|
|
3
|
+
* SurfaceSignaturePad — a plugin surface's `signature-pad` node: a
|
|
4
|
+
* signature as a transparent PNG into `data.values[id]` (`{png_b64, mode}`).
|
|
5
|
+
*
|
|
6
|
+
* Three ways in, the node picks which: draw (pointer strokes on a canvas,
|
|
7
|
+
* DPR-aware so a phone's signature is not a blur), type (a name set in one of
|
|
8
|
+
* the five shipped faces, rendered to the same canvas size), upload (a
|
|
9
|
+
* PNG/JPEG of at most 200 KB, fitted into 600×200). Every way ends in the
|
|
10
|
+
* same value; Clear ends in `null`. The ink is black whatever the theme: the
|
|
11
|
+
* picture is bound for a document, not for this screen.
|
|
12
|
+
*
|
|
13
|
+
* ⚠⚠ The typed face is CHOSEN and the choice travels (`value.font`). It
|
|
14
|
+
* used to be one CSS stack of fonts the machine might happen to have, so the
|
|
15
|
+
* same name signed on a Mac, on Windows and on a phone came out in three
|
|
16
|
+
* different hands — and on a machine with none of them, in the browser's
|
|
17
|
+
* generic cursive. The five are shipped with the package (`lib/signFonts`,
|
|
18
|
+
* `styles/sign-fonts.css`) and the same five are offered on a PDF text field,
|
|
19
|
+
* so a document signed here and filled there matches.
|
|
20
|
+
*
|
|
21
|
+
* The rules (modes, sizes, verdicts) are in `lib/signaturePad`; this file
|
|
22
|
+
* is the canvas work.
|
|
23
|
+
*/
|
|
24
|
+
import { computed, nextTick, onBeforeUnmount, onMounted, ref, watch } from 'vue';
|
|
25
|
+
import type { LocaleCode } from '../../../types/ExplorerConfig';
|
|
26
|
+
import { useLocale } from '../../../composables/useLocale';
|
|
27
|
+
import { SIGN_FONTS, isSignFontKey, signFont, type SignFontKey } from '../../../lib/signFonts';
|
|
28
|
+
import {
|
|
29
|
+
SIGNATURE_FONT_DEFAULT,
|
|
30
|
+
SIGNATURE_INK,
|
|
31
|
+
SIGNATURE_MAX_HEIGHT,
|
|
32
|
+
SIGNATURE_MAX_WIDTH,
|
|
33
|
+
fitFontSize,
|
|
34
|
+
fitWithin,
|
|
35
|
+
isSignatureValue,
|
|
36
|
+
padSize,
|
|
37
|
+
signatureFontOf,
|
|
38
|
+
signatureModes,
|
|
39
|
+
stripDataUrl,
|
|
40
|
+
uploadVerdict,
|
|
41
|
+
type SignatureMode,
|
|
42
|
+
type SignatureValue,
|
|
43
|
+
} from '../../../lib/signaturePad';
|
|
44
|
+
|
|
45
|
+
const props = defineProps<{
|
|
46
|
+
id: string;
|
|
47
|
+
modes?: unknown;
|
|
48
|
+
width?: unknown;
|
|
49
|
+
height?: unknown;
|
|
50
|
+
modelValue: SignatureValue | null | undefined;
|
|
51
|
+
locale: LocaleCode;
|
|
52
|
+
disabled?: boolean;
|
|
53
|
+
invalid?: boolean;
|
|
54
|
+
/** The face a typed signature starts in (a `lib/signFonts` key). */
|
|
55
|
+
font?: unknown;
|
|
56
|
+
/**
|
|
57
|
+
* The faces a typed signature may use; one face means no picker at all.
|
|
58
|
+
* A box whose face the REQUESTER chose passes just that one, so what the
|
|
59
|
+
* signer sees is what the stamper prints.
|
|
60
|
+
*/
|
|
61
|
+
fonts?: unknown;
|
|
62
|
+
/**
|
|
63
|
+
* The box's name, drawn as a field label — with the same `*` a required
|
|
64
|
+
* form field carries (v3). ⚠ The owner, 2026-09-21: "imza zorunluydu ama
|
|
65
|
+
* imzanın zorunlu olduğunu `*` ile belirtmiyoruz". The signature was the
|
|
66
|
+
* one required box on the screen that did not say so, because it was a
|
|
67
|
+
* plain text line above the pad rather than a label.
|
|
68
|
+
*/
|
|
69
|
+
label?: string;
|
|
70
|
+
required?: boolean;
|
|
71
|
+
}>();
|
|
72
|
+
|
|
73
|
+
const emit = defineEmits<{
|
|
74
|
+
(e: 'update:modelValue', v: SignatureValue | null): void;
|
|
75
|
+
}>();
|
|
76
|
+
|
|
77
|
+
const { t } = useLocale(() => props.locale);
|
|
78
|
+
|
|
79
|
+
const modes = computed(() => signatureModes(props.modes));
|
|
80
|
+
const size = computed(() => padSize(props.width, props.height));
|
|
81
|
+
const mode = ref<SignatureMode>(isSignatureValue(props.modelValue) && modes.value.includes(props.modelValue.mode) ? props.modelValue.mode : modes.value[0]);
|
|
82
|
+
|
|
83
|
+
/** The faces this pad offers — all five unless the node narrowed them. */
|
|
84
|
+
const offeredFonts = computed(() => {
|
|
85
|
+
const want = Array.isArray(props.fonts) ? (props.fonts as unknown[]).filter(isSignFontKey) : [];
|
|
86
|
+
return want.length ? SIGN_FONTS.filter((f) => want.includes(f.key)) : SIGN_FONTS;
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
function startFont(): SignFontKey {
|
|
90
|
+
const offered = offeredFonts.value.map((f) => f.key);
|
|
91
|
+
const fromValue = isSignatureValue(props.modelValue) ? signatureFontOf(props.modelValue) : undefined;
|
|
92
|
+
if (fromValue && offered.includes(fromValue)) return fromValue;
|
|
93
|
+
if (isSignFontKey(props.font) && offered.includes(props.font)) return props.font;
|
|
94
|
+
return offered.includes(SIGNATURE_FONT_DEFAULT) ? SIGNATURE_FONT_DEFAULT : offered[0];
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
const canvas = ref<HTMLCanvasElement | null>(null);
|
|
98
|
+
const typed = ref('');
|
|
99
|
+
/** The face the typed signature is set in — seeded from an arriving value. */
|
|
100
|
+
const font = ref<SignFontKey>(startFont());
|
|
101
|
+
const uploadError = ref<'' | 'too_big' | 'bad_type' | 'unreadable'>('');
|
|
102
|
+
/** What the pad currently shows for `type` / `upload` (a data URL). */
|
|
103
|
+
const preview = ref('');
|
|
104
|
+
|
|
105
|
+
const hasValue = computed(() => isSignatureValue(props.modelValue));
|
|
106
|
+
/** The picture the value holds, for the non-draw modes and for a value that arrived with the surface. */
|
|
107
|
+
const valueUrl = computed(() => (isSignatureValue(props.modelValue) ? `data:image/png;base64,${props.modelValue.png_b64}` : ''));
|
|
108
|
+
|
|
109
|
+
function ctxOf(c: HTMLCanvasElement | null): CanvasRenderingContext2D | null {
|
|
110
|
+
if (!c) return null;
|
|
111
|
+
try {
|
|
112
|
+
return c.getContext('2d');
|
|
113
|
+
} catch {
|
|
114
|
+
return null;
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function dpr(): number {
|
|
119
|
+
return typeof window !== 'undefined' && window.devicePixelRatio > 0 ? Math.min(3, window.devicePixelRatio) : 1;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/** The drawing canvas sized for the pad (CSS px × DPR), cleared, ink ready. */
|
|
123
|
+
function prepareCanvas(): void {
|
|
124
|
+
const c = canvas.value;
|
|
125
|
+
if (!c) return;
|
|
126
|
+
const k = dpr();
|
|
127
|
+
c.width = Math.round(size.value.width * k);
|
|
128
|
+
c.height = Math.round(size.value.height * k);
|
|
129
|
+
const ctx = ctxOf(c);
|
|
130
|
+
if (!ctx) return;
|
|
131
|
+
ctx.setTransform(1, 0, 0, 1, 0, 0);
|
|
132
|
+
ctx.clearRect(0, 0, c.width, c.height);
|
|
133
|
+
ctx.scale(k, k);
|
|
134
|
+
ctx.lineWidth = 2.2;
|
|
135
|
+
ctx.lineCap = 'round';
|
|
136
|
+
ctx.lineJoin = 'round';
|
|
137
|
+
ctx.strokeStyle = SIGNATURE_INK;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/* ── draw ─────────────────────────────────────────────────────────────── */
|
|
141
|
+
|
|
142
|
+
let drawing = false;
|
|
143
|
+
let last: { x: number; y: number } | null = null;
|
|
144
|
+
|
|
145
|
+
function pointOf(ev: PointerEvent): { x: number; y: number } {
|
|
146
|
+
const c = canvas.value;
|
|
147
|
+
const r = c?.getBoundingClientRect();
|
|
148
|
+
if (!c || !r || !r.width) return { x: ev.clientX, y: ev.clientY };
|
|
149
|
+
// The canvas may be drawn narrower than its logical size on a phone.
|
|
150
|
+
const kx = size.value.width / r.width;
|
|
151
|
+
const ky = size.value.height / r.height;
|
|
152
|
+
return { x: (ev.clientX - r.left) * kx, y: (ev.clientY - r.top) * ky };
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
function onDown(ev: PointerEvent): void {
|
|
156
|
+
if (props.disabled) return;
|
|
157
|
+
const ctx = ctxOf(canvas.value);
|
|
158
|
+
if (!ctx) return;
|
|
159
|
+
drawing = true;
|
|
160
|
+
last = pointOf(ev);
|
|
161
|
+
try {
|
|
162
|
+
(ev.target as Element).setPointerCapture?.(ev.pointerId);
|
|
163
|
+
} catch {
|
|
164
|
+
/* not every runtime has pointer capture */
|
|
165
|
+
}
|
|
166
|
+
// A dot: a tap must leave a mark too.
|
|
167
|
+
ctx.beginPath();
|
|
168
|
+
ctx.moveTo(last.x, last.y);
|
|
169
|
+
ctx.lineTo(last.x + 0.1, last.y + 0.1);
|
|
170
|
+
ctx.stroke();
|
|
171
|
+
ev.preventDefault?.();
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
function onMove(ev: PointerEvent): void {
|
|
175
|
+
if (!drawing || !last) return;
|
|
176
|
+
const ctx = ctxOf(canvas.value);
|
|
177
|
+
if (!ctx) return;
|
|
178
|
+
const p = pointOf(ev);
|
|
179
|
+
ctx.beginPath();
|
|
180
|
+
ctx.moveTo(last.x, last.y);
|
|
181
|
+
ctx.lineTo(p.x, p.y);
|
|
182
|
+
ctx.stroke();
|
|
183
|
+
last = p;
|
|
184
|
+
ev.preventDefault?.();
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
function onUp(): void {
|
|
188
|
+
if (!drawing) return;
|
|
189
|
+
drawing = false;
|
|
190
|
+
last = null;
|
|
191
|
+
exportCanvas(canvas.value, 'draw');
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
function exportCanvas(c: HTMLCanvasElement | null, m: SignatureMode): void {
|
|
195
|
+
if (!c) return;
|
|
196
|
+
let url = '';
|
|
197
|
+
try {
|
|
198
|
+
url = c.toDataURL('image/png');
|
|
199
|
+
} catch {
|
|
200
|
+
url = '';
|
|
201
|
+
}
|
|
202
|
+
const b64 = stripDataUrl(url);
|
|
203
|
+
if (!b64) return;
|
|
204
|
+
if (m !== 'draw') preview.value = url;
|
|
205
|
+
// ⚠ The face rides ONLY with a typed signature: on a drawn or uploaded
|
|
206
|
+
// one it would be a fact about nothing, and a plugin reading `font` would
|
|
207
|
+
// re-render somebody's handwriting as text.
|
|
208
|
+
emit('update:modelValue', m === 'type' ? { png_b64: b64, mode: m, font: font.value } : { png_b64: b64, mode: m });
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
/* ── type ─────────────────────────────────────────────────────────────── */
|
|
212
|
+
|
|
213
|
+
/**
|
|
214
|
+
* Make sure the chosen face is actually LOADED before the canvas uses it.
|
|
215
|
+
*
|
|
216
|
+
* ⚠⚠ A canvas does not wait. `ctx.font = "48px Caveat"` on a face the
|
|
217
|
+
* browser has not fetched yet silently falls back to the generic, the PNG is
|
|
218
|
+
* exported from that, and the signature is wrong in a way nothing on screen
|
|
219
|
+
* shows — the preview repaints correctly a moment later, when the font
|
|
220
|
+
* arrives, while the exported bytes keep the fallback. `document.fonts.load`
|
|
221
|
+
* is the only honest gate; a browser without the Font Loading API just
|
|
222
|
+
* renders as it always did.
|
|
223
|
+
*/
|
|
224
|
+
async function ensureFont(px: number, stack: string): Promise<void> {
|
|
225
|
+
const fonts = (document as Document & { fonts?: FontFaceSet }).fonts;
|
|
226
|
+
if (!fonts || typeof fonts.load !== 'function') return;
|
|
227
|
+
try {
|
|
228
|
+
await fonts.load(`${px}px ${stack}`, 'Aa');
|
|
229
|
+
} catch {
|
|
230
|
+
/* an unknown descriptor is not worth failing a signature over */
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
async function renderTyped(): Promise<void> {
|
|
235
|
+
const text = typed.value.trim();
|
|
236
|
+
if (!text) {
|
|
237
|
+
preview.value = '';
|
|
238
|
+
emit('update:modelValue', null);
|
|
239
|
+
return;
|
|
240
|
+
}
|
|
241
|
+
const stack = signFont(font.value).stack;
|
|
242
|
+
await ensureFont(Math.round(size.value.height * 0.6), stack);
|
|
243
|
+
// The person may have cleared the field while the face was loading.
|
|
244
|
+
if (typed.value.trim() !== text) return;
|
|
245
|
+
const c = document.createElement('canvas');
|
|
246
|
+
const k = 2;
|
|
247
|
+
const { width, height } = size.value;
|
|
248
|
+
c.width = width * k;
|
|
249
|
+
c.height = height * k;
|
|
250
|
+
const ctx = ctxOf(c);
|
|
251
|
+
if (!ctx) return;
|
|
252
|
+
ctx.scale(k, k);
|
|
253
|
+
ctx.fillStyle = SIGNATURE_INK;
|
|
254
|
+
ctx.textBaseline = 'middle';
|
|
255
|
+
const px = fitFontSize(
|
|
256
|
+
(n) => {
|
|
257
|
+
ctx.font = `${n}px ${stack}`;
|
|
258
|
+
return ctx.measureText(text).width;
|
|
259
|
+
},
|
|
260
|
+
width - 24,
|
|
261
|
+
height,
|
|
262
|
+
);
|
|
263
|
+
ctx.font = `${px}px ${stack}`;
|
|
264
|
+
ctx.fillText(text, 12, height / 2);
|
|
265
|
+
exportCanvas(c, 'type');
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
/** Another face for the same name: re-render, so the value matches the pick. */
|
|
269
|
+
function pickFont(key: SignFontKey): void {
|
|
270
|
+
if (props.disabled || key === font.value) return;
|
|
271
|
+
font.value = key;
|
|
272
|
+
if (typed.value.trim()) void renderTyped();
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
/* ── upload ───────────────────────────────────────────────────────────── */
|
|
276
|
+
|
|
277
|
+
const fileInput = ref<HTMLInputElement | null>(null);
|
|
278
|
+
|
|
279
|
+
function onFile(ev: Event): void {
|
|
280
|
+
const input = ev.target as HTMLInputElement;
|
|
281
|
+
const file = input.files?.[0];
|
|
282
|
+
input.value = '';
|
|
283
|
+
if (!file) return;
|
|
284
|
+
const verdict = uploadVerdict(file);
|
|
285
|
+
if (verdict !== 'ok') {
|
|
286
|
+
uploadError.value = verdict;
|
|
287
|
+
return;
|
|
288
|
+
}
|
|
289
|
+
uploadError.value = '';
|
|
290
|
+
const url = URL.createObjectURL(file);
|
|
291
|
+
const img = new Image();
|
|
292
|
+
img.onload = () => {
|
|
293
|
+
URL.revokeObjectURL(url);
|
|
294
|
+
const fit = fitWithin(img.naturalWidth, img.naturalHeight, SIGNATURE_MAX_WIDTH, SIGNATURE_MAX_HEIGHT);
|
|
295
|
+
const c = document.createElement('canvas');
|
|
296
|
+
c.width = fit.width;
|
|
297
|
+
c.height = fit.height;
|
|
298
|
+
const ctx = ctxOf(c);
|
|
299
|
+
if (!ctx) return;
|
|
300
|
+
ctx.drawImage(img, 0, 0, fit.width, fit.height);
|
|
301
|
+
exportCanvas(c, 'upload');
|
|
302
|
+
};
|
|
303
|
+
img.onerror = () => {
|
|
304
|
+
URL.revokeObjectURL(url);
|
|
305
|
+
uploadError.value = 'unreadable';
|
|
306
|
+
};
|
|
307
|
+
img.src = url;
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
/* ── shared ───────────────────────────────────────────────────────────── */
|
|
311
|
+
|
|
312
|
+
function clear(): void {
|
|
313
|
+
typed.value = '';
|
|
314
|
+
preview.value = '';
|
|
315
|
+
uploadError.value = '';
|
|
316
|
+
prepareCanvas();
|
|
317
|
+
emit('update:modelValue', null);
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
function pick(m: SignatureMode): void {
|
|
321
|
+
if (m === mode.value) return;
|
|
322
|
+
mode.value = m;
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
watch(mode, async (m) => {
|
|
326
|
+
if (m === 'draw') {
|
|
327
|
+
await nextTick();
|
|
328
|
+
prepareCanvas();
|
|
329
|
+
}
|
|
330
|
+
});
|
|
331
|
+
|
|
332
|
+
watch(size, () => {
|
|
333
|
+
if (mode.value === 'draw') prepareCanvas();
|
|
334
|
+
});
|
|
335
|
+
|
|
336
|
+
onMounted(() => {
|
|
337
|
+
if (mode.value === 'draw') prepareCanvas();
|
|
338
|
+
});
|
|
339
|
+
|
|
340
|
+
onBeforeUnmount(() => {
|
|
341
|
+
drawing = false;
|
|
342
|
+
});
|
|
343
|
+
|
|
344
|
+
const uploadErrorText = computed(() => {
|
|
345
|
+
switch (uploadError.value) {
|
|
346
|
+
case 'too_big':
|
|
347
|
+
return t('plugin.sig.upload_too_big', { kb: 200 });
|
|
348
|
+
case 'bad_type':
|
|
349
|
+
return t('plugin.sig.upload_bad_type');
|
|
350
|
+
case 'unreadable':
|
|
351
|
+
return t('plugin.sig.upload_unreadable');
|
|
352
|
+
default:
|
|
353
|
+
return '';
|
|
354
|
+
}
|
|
355
|
+
});
|
|
356
|
+
</script>
|
|
357
|
+
|
|
358
|
+
<template>
|
|
359
|
+
<div
|
|
360
|
+
class="fe-ssig"
|
|
361
|
+
data-testid="surface-signature"
|
|
362
|
+
:class="{ 'is-invalid': invalid, 'is-disabled': disabled }"
|
|
363
|
+
:data-mode="mode"
|
|
364
|
+
:role="label ? 'group' : undefined"
|
|
365
|
+
:aria-labelledby="label ? `fe-ssig-${id}-lbl` : undefined"
|
|
366
|
+
>
|
|
367
|
+
<!-- The same label a form field wears, star included. -->
|
|
368
|
+
<span v-if="label" :id="`fe-ssig-${id}-lbl`" class="fe-cfield__label" data-testid="surface-signature-label">
|
|
369
|
+
{{ label }}<span v-if="required" class="fe-cfield__req" aria-hidden="true">*</span>
|
|
370
|
+
</span>
|
|
371
|
+
<div v-if="modes.length > 1" class="fe-ssig__modes" role="tablist">
|
|
372
|
+
<button
|
|
373
|
+
v-for="m in modes"
|
|
374
|
+
:key="m"
|
|
375
|
+
type="button"
|
|
376
|
+
class="fe-btn fe-btn--sm"
|
|
377
|
+
:class="{ 'fe-btn--primary': m === mode }"
|
|
378
|
+
role="tab"
|
|
379
|
+
:aria-selected="m === mode ? 'true' : 'false'"
|
|
380
|
+
:disabled="disabled"
|
|
381
|
+
:data-testid="`surface-signature-mode-${m}`"
|
|
382
|
+
@click="pick(m)"
|
|
383
|
+
>
|
|
384
|
+
{{ t(`plugin.sig.mode_${m}`) }}
|
|
385
|
+
</button>
|
|
386
|
+
</div>
|
|
387
|
+
|
|
388
|
+
<div v-if="mode === 'draw'" class="fe-ssig__padwrap" :style="{ maxWidth: `${size.width}px` }">
|
|
389
|
+
<canvas
|
|
390
|
+
ref="canvas"
|
|
391
|
+
class="fe-ssig__canvas"
|
|
392
|
+
:style="{ aspectRatio: `${size.width} / ${size.height}` }"
|
|
393
|
+
:aria-label="t('plugin.sig.draw_hint')"
|
|
394
|
+
data-testid="surface-signature-canvas"
|
|
395
|
+
@pointerdown="onDown"
|
|
396
|
+
@pointermove="onMove"
|
|
397
|
+
@pointerup="onUp"
|
|
398
|
+
@pointercancel="onUp"
|
|
399
|
+
@pointerleave="onUp"
|
|
400
|
+
></canvas>
|
|
401
|
+
<p v-if="!hasValue" class="fe-ssig__hint">{{ t('plugin.sig.draw_hint') }}</p>
|
|
402
|
+
</div>
|
|
403
|
+
|
|
404
|
+
<div v-else-if="mode === 'type'" class="fe-ssig__typewrap">
|
|
405
|
+
<input
|
|
406
|
+
:id="`fe-ssig-${id}`"
|
|
407
|
+
class="fe-cfield__input fe-ssig__typed"
|
|
408
|
+
type="text"
|
|
409
|
+
:placeholder="t('plugin.sig.type_placeholder')"
|
|
410
|
+
:disabled="disabled"
|
|
411
|
+
:value="typed"
|
|
412
|
+
autocomplete="name"
|
|
413
|
+
data-testid="surface-signature-typed"
|
|
414
|
+
@input="typed = ($event.target as HTMLInputElement).value; void renderTyped()"
|
|
415
|
+
/>
|
|
416
|
+
<!-- The five faces, each button set in the face it offers: a name of a
|
|
417
|
+
font tells nobody what their signature will look like. -->
|
|
418
|
+
<div
|
|
419
|
+
v-if="offeredFonts.length > 1"
|
|
420
|
+
class="fe-signfonts"
|
|
421
|
+
role="group"
|
|
422
|
+
:aria-label="t('plugin.sign.font_label')"
|
|
423
|
+
data-testid="surface-signature-fonts"
|
|
424
|
+
>
|
|
425
|
+
<button
|
|
426
|
+
v-for="f in offeredFonts"
|
|
427
|
+
:key="f.key"
|
|
428
|
+
type="button"
|
|
429
|
+
class="fe-signfonts__btn"
|
|
430
|
+
:class="[f.className, { 'is-on': f.key === font }]"
|
|
431
|
+
:disabled="disabled"
|
|
432
|
+
:title="f.name"
|
|
433
|
+
:aria-pressed="f.key === font ? 'true' : 'false'"
|
|
434
|
+
:data-testid="`surface-signature-font-${f.key}`"
|
|
435
|
+
@click="pickFont(f.key)"
|
|
436
|
+
>
|
|
437
|
+
{{ typed.trim() || f.name }}
|
|
438
|
+
</button>
|
|
439
|
+
</div>
|
|
440
|
+
<div class="fe-ssig__preview" :style="{ maxWidth: `${size.width}px`, aspectRatio: `${size.width} / ${size.height}` }">
|
|
441
|
+
<img v-if="preview || valueUrl" class="fe-ssig__img" :src="preview || valueUrl" alt="" />
|
|
442
|
+
</div>
|
|
443
|
+
</div>
|
|
444
|
+
|
|
445
|
+
<div v-else class="fe-ssig__uploadwrap">
|
|
446
|
+
<label class="fe-btn fe-btn--sm fe-ssig__pick">
|
|
447
|
+
<input
|
|
448
|
+
ref="fileInput"
|
|
449
|
+
type="file"
|
|
450
|
+
accept="image/png,image/jpeg"
|
|
451
|
+
class="fe-ssig__file"
|
|
452
|
+
:disabled="disabled"
|
|
453
|
+
data-testid="surface-signature-file"
|
|
454
|
+
@change="onFile"
|
|
455
|
+
/>
|
|
456
|
+
{{ t('plugin.sig.upload_pick') }}
|
|
457
|
+
</label>
|
|
458
|
+
<p class="fe-ssig__hint">{{ t('plugin.sig.upload_hint', { kb: 200 }) }}</p>
|
|
459
|
+
<p v-if="uploadErrorText" class="fe-surface__error" role="alert">{{ uploadErrorText }}</p>
|
|
460
|
+
<div v-if="preview || valueUrl" class="fe-ssig__preview" :style="{ maxWidth: `${size.width}px` }">
|
|
461
|
+
<img class="fe-ssig__img" :src="preview || valueUrl" alt="" />
|
|
462
|
+
</div>
|
|
463
|
+
</div>
|
|
464
|
+
|
|
465
|
+
<div class="fe-ssig__foot">
|
|
466
|
+
<button type="button" class="fe-btn fe-btn--sm" :disabled="disabled || !hasValue" data-testid="surface-signature-clear" @click="clear">
|
|
467
|
+
{{ t('plugin.sig.clear') }}
|
|
468
|
+
</button>
|
|
469
|
+
<span v-if="hasValue" class="fe-ssig__signed">{{ t('plugin.sig.signed') }}</span>
|
|
470
|
+
</div>
|
|
471
|
+
</div>
|
|
472
|
+
</template>
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
/**
|
|
3
|
+
* SurfaceSteps — a plugin surface's `steps` node: where the person is in a
|
|
4
|
+
* wizard. Purely a picture; the plugin moves the active step by answering
|
|
5
|
+
* with a new surface.
|
|
6
|
+
*/
|
|
7
|
+
import type { LocaleCode } from '../../../types/ExplorerConfig';
|
|
8
|
+
import type { StepsNodeProps } from '../../../types/Plugins';
|
|
9
|
+
import { useLocale } from '../../../composables/useLocale';
|
|
10
|
+
import { labelOf } from '../../../lib/pluginLabel';
|
|
11
|
+
|
|
12
|
+
const props = defineProps<{
|
|
13
|
+
items: StepsNodeProps['items'];
|
|
14
|
+
locale: LocaleCode;
|
|
15
|
+
}>();
|
|
16
|
+
|
|
17
|
+
const { t } = useLocale(() => props.locale);
|
|
18
|
+
|
|
19
|
+
function stateOf(s: unknown): 'done' | 'active' | 'todo' {
|
|
20
|
+
return s === 'done' || s === 'active' ? s : 'todo';
|
|
21
|
+
}
|
|
22
|
+
</script>
|
|
23
|
+
|
|
24
|
+
<template>
|
|
25
|
+
<ol class="fe-steps" :aria-label="t('plugin.steps.label')" data-testid="surface-steps">
|
|
26
|
+
<li
|
|
27
|
+
v-for="(it, i) in items ?? []"
|
|
28
|
+
:key="it.id || i"
|
|
29
|
+
class="fe-steps__item"
|
|
30
|
+
:class="`is-${stateOf(it.state)}`"
|
|
31
|
+
:aria-current="stateOf(it.state) === 'active' ? 'step' : undefined"
|
|
32
|
+
:data-step="it.id"
|
|
33
|
+
>
|
|
34
|
+
<span class="fe-steps__marker" aria-hidden="true">
|
|
35
|
+
<svg
|
|
36
|
+
v-if="stateOf(it.state) === 'done'"
|
|
37
|
+
class="fe-ficon"
|
|
38
|
+
viewBox="0 0 24 24"
|
|
39
|
+
fill="none"
|
|
40
|
+
stroke="currentColor"
|
|
41
|
+
stroke-width="2.2"
|
|
42
|
+
stroke-linecap="round"
|
|
43
|
+
stroke-linejoin="round"
|
|
44
|
+
focusable="false"
|
|
45
|
+
>
|
|
46
|
+
<path d="M4.8 12.6l4.7 4.7L19.2 6.9" />
|
|
47
|
+
</svg>
|
|
48
|
+
<template v-else>{{ i + 1 }}</template>
|
|
49
|
+
</span>
|
|
50
|
+
<span class="fe-steps__label">{{ labelOf(it.label, locale) }}</span>
|
|
51
|
+
</li>
|
|
52
|
+
</ol>
|
|
53
|
+
</template>
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
/**
|
|
3
|
+
* PublicLanguagePicker — the language a stranger reads the page in.
|
|
4
|
+
*
|
|
5
|
+
* ⚠ A share link is opened by somebody who has no account here, so there is
|
|
6
|
+
* no saved preference to read and the server's guess (`Accept-Language`) is
|
|
7
|
+
* all there was. This is how they correct it — and the correction is kept on
|
|
8
|
+
* the DEVICE only, because there is nobody to keep it for.
|
|
9
|
+
*
|
|
10
|
+
* ⚠⚠ The list is NOT hardcoded (v3 §5). It is whatever `lib/uiLocales` says
|
|
11
|
+
* the instance offers, which includes a language an app plugin shipped —
|
|
12
|
+
* marked with the app it came from, so nobody wonders why "العربية" appeared
|
|
13
|
+
* on a filex install. Buttons, not a dropdown, for the same reason the rest
|
|
14
|
+
* of this round is: with two or three languages the answer should be
|
|
15
|
+
* readable without opening anything.
|
|
16
|
+
*/
|
|
17
|
+
import { computed } from 'vue';
|
|
18
|
+
import { availableLocales, localesVersion } from '../../lib/uiLocales';
|
|
19
|
+
|
|
20
|
+
const props = defineProps<{
|
|
21
|
+
modelValue: string;
|
|
22
|
+
/** Shown beside a language an app added ("from <app>"). */
|
|
23
|
+
pluginNote?: (plugin: string) => string;
|
|
24
|
+
}>();
|
|
25
|
+
|
|
26
|
+
const emit = defineEmits<{
|
|
27
|
+
(e: 'update:modelValue', v: string): void;
|
|
28
|
+
}>();
|
|
29
|
+
|
|
30
|
+
const options = computed(() => {
|
|
31
|
+
void localesVersion.value;
|
|
32
|
+
return availableLocales();
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
/** One language is not a choice — the picker draws nothing rather than a stub. */
|
|
36
|
+
const show = computed(() => options.value.length > 1);
|
|
37
|
+
</script>
|
|
38
|
+
|
|
39
|
+
<template>
|
|
40
|
+
<div v-if="show" class="fe-plang" role="group" data-testid="public-language">
|
|
41
|
+
<button
|
|
42
|
+
v-for="o in options"
|
|
43
|
+
:key="o.code"
|
|
44
|
+
type="button"
|
|
45
|
+
class="fe-plang__btn"
|
|
46
|
+
:class="{ 'is-on': o.code === props.modelValue }"
|
|
47
|
+
:lang="o.code"
|
|
48
|
+
:aria-pressed="o.code === props.modelValue ? 'true' : 'false'"
|
|
49
|
+
:title="o.source === 'plugin' && o.plugin && pluginNote ? pluginNote(o.plugin) : undefined"
|
|
50
|
+
:data-testid="`public-language-${o.code}`"
|
|
51
|
+
@click="emit('update:modelValue', o.code)"
|
|
52
|
+
>
|
|
53
|
+
{{ o.label }}
|
|
54
|
+
<small v-if="o.source === 'plugin' && o.plugin" class="fe-plang__src">{{ o.plugin }}</small>
|
|
55
|
+
</button>
|
|
56
|
+
</div>
|
|
57
|
+
</template>
|
|
58
|
+
|