@brftech/filex-core 0.42.1 → 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-BuhZOAx6.js → DrawioViewer-Bgrj8-zG.js} +45 -44
- package/dist/DrawioViewer-Bgrj8-zG.js.map +1 -0
- package/dist/{EpubViewer-DS3mb3k8.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-CIrOn63I.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 +5241 -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 +1207 -343
- 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 +47 -17
- package/src/components/RecoveryKeyModal.vue +1 -1
- package/src/components/RowActions.vue +134 -0
- package/src/components/S3KeysPanel.vue +98 -56
- package/src/components/SSHKeysPanel.vue +79 -26
- 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 -27
- 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 +121 -0
- 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 +522 -119
- package/src/locales/resolve.ts +17 -8
- package/src/locales/tr.ts +539 -155
- 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 +2895 -241
- 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 +58 -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-CLNevfPE.js +0 -124
- package/dist/ArchiveViewer-CLNevfPE.js.map +0 -1
- package/dist/CsvViewer-K8XA3z7N.js +0 -141
- package/dist/CsvViewer-K8XA3z7N.js.map +0 -1
- package/dist/DrawioViewer-BuhZOAx6.js.map +0 -1
- package/dist/EpubViewer-DS3mb3k8.js.map +0 -1
- package/dist/IpynbViewer-BTk7p4Dn.js +0 -184
- package/dist/IpynbViewer-BTk7p4Dn.js.map +0 -1
- package/dist/MermaidViewer-DqucH_io.js +0 -137
- package/dist/MermaidViewer-DqucH_io.js.map +0 -1
- package/dist/PsdViewer-CwiVENmq.js +0 -122
- package/dist/PsdViewer-CwiVENmq.js.map +0 -1
- package/dist/TiffViewer-CpewSVmQ.js +0 -142
- package/dist/TiffViewer-CpewSVmQ.js.map +0 -1
- package/dist/Viewer3D-CIrOn63I.js.map +0 -1
- package/dist/index-Dx0IbBcI.js +0 -26831
- package/dist/index-Dx0IbBcI.js.map +0 -1
- package/dist/useViewerFetch-czqbd2Lj.js +0 -25
- package/dist/useViewerFetch-czqbd2Lj.js.map +0 -1
|
@@ -30,7 +30,12 @@
|
|
|
30
30
|
*/
|
|
31
31
|
import { computed, ref } from 'vue';
|
|
32
32
|
import { useLocale } from '../composables/useLocale';
|
|
33
|
+
import { inlineStartX } from '../lib/direction';
|
|
33
34
|
import type { LocaleCode, ThemeMode } from '../types/ExplorerConfig';
|
|
35
|
+
import { actionIconSvg } from '../lib/actionIcons';
|
|
36
|
+
import type { TagItem, TagKind } from '../lib/tags';
|
|
37
|
+
import TagKindIcon from './TagKindIcon.vue';
|
|
38
|
+
import StorageTags from './StorageTags.vue';
|
|
34
39
|
import ContextMenu, { type ContextAction } from './ContextMenu.vue';
|
|
35
40
|
|
|
36
41
|
/** The virtual listings the panel can open. '' = an ordinary folder.
|
|
@@ -61,6 +66,18 @@ export interface NavStorage {
|
|
|
61
66
|
readOnly?: boolean;
|
|
62
67
|
}
|
|
63
68
|
|
|
69
|
+
/**
|
|
70
|
+
* A row of the "Apps" section — an app plugin's `home` view, already read in
|
|
71
|
+
* the viewer's language by the host (docs/APP-PLUGINS-API.md → Placements).
|
|
72
|
+
*/
|
|
73
|
+
export interface NavApp {
|
|
74
|
+
/** `<plugin>/<view>` — what `open-app` carries back. */
|
|
75
|
+
key: string;
|
|
76
|
+
label: string;
|
|
77
|
+
/** A manifest icon name; drawn when `lib/actionIcons` has it, else the plugin glyph. */
|
|
78
|
+
icon?: string;
|
|
79
|
+
}
|
|
80
|
+
|
|
64
81
|
const props = defineProps<{
|
|
65
82
|
/** Expanded (labels visible) vs collapsed to the icon rail. */
|
|
66
83
|
expanded: boolean;
|
|
@@ -86,14 +103,17 @@ const props = defineProps<{
|
|
|
86
103
|
* dynamic in name. Still presentational here — the host fetches
|
|
87
104
|
* `tags/all` (once, cached) and hands the list over, exactly as it does
|
|
88
105
|
* for storages. */
|
|
89
|
-
/** Every tag
|
|
90
|
-
* "no tags yet" line
|
|
91
|
-
|
|
106
|
+
/** Every tag the person can see, alphabetical, WITH its kind (v0.43:
|
|
107
|
+
* personal or team). Empty → the section shows its own "no tags yet" line
|
|
108
|
+
* rather than disappearing. */
|
|
109
|
+
tags?: TagItem[];
|
|
92
110
|
/** False until the first answer arrives, so "no tags yet" is never shown
|
|
93
111
|
* to somebody who is simply still waiting. */
|
|
94
112
|
tagsLoaded?: boolean;
|
|
95
113
|
/** The tag currently on screen (activeView === 'tag'). */
|
|
96
114
|
activeTag?: string;
|
|
115
|
+
/** …and its kind ('' = a view of both kinds, e.g. an old `#.tag~x` link). */
|
|
116
|
+
activeTagKind?: TagKind | '';
|
|
97
117
|
/**
|
|
98
118
|
* Show the Connections entries — "How to connect" and "API keys".
|
|
99
119
|
* ⚠ Never derived from a role here or anywhere: the backend decides what a
|
|
@@ -101,6 +121,18 @@ const props = defineProps<{
|
|
|
101
121
|
* accounts that need it (see ExplorerConfig.connections).
|
|
102
122
|
*/
|
|
103
123
|
showConnections?: boolean;
|
|
124
|
+
/**
|
|
125
|
+
* paylas:m1 — draw the "My shares" row (mirrors ExplorerConfig.mySharesVisible).
|
|
126
|
+
*
|
|
127
|
+
* ⚠ Absent means NO, and that is the one place this prop differs from
|
|
128
|
+
* `trashVisible` beside it: the row's destination is a page of the HOST, so
|
|
129
|
+
* a panel that draws it without being asked draws a button that does
|
|
130
|
+
* nothing. Vue casts an absent Boolean prop to `false`, which is exactly the
|
|
131
|
+
* answer wanted here — a host that says nothing has not promised a screen.
|
|
132
|
+
* (The inverse default is the trap lib-wide; see ExplorerConfig.trashVisible,
|
|
133
|
+
* read as `!== false`, and `withDefaults` everywhere a default is ON.)
|
|
134
|
+
*/
|
|
135
|
+
showMyShares?: boolean;
|
|
104
136
|
/**
|
|
105
137
|
* Draw the surfaces that only mean something for ONE person: API keys,
|
|
106
138
|
* Recent, Starred, Shared with me. False when the caller is an app token —
|
|
@@ -152,12 +184,18 @@ const props = defineProps<{
|
|
|
152
184
|
quota?: { used: number; total: number; unlimited: boolean } | null;
|
|
153
185
|
/** Resolved theme — the teleported New menu leaves the `.fe` variable scope. */
|
|
154
186
|
theme?: ThemeMode;
|
|
187
|
+
/**
|
|
188
|
+
* App plugins — the `home` views, one row each under "Apps". Empty or
|
|
189
|
+
* absent hides the section entirely: a heading over nothing is a promise
|
|
190
|
+
* the deployment cannot keep.
|
|
191
|
+
*/
|
|
192
|
+
apps?: NavApp[];
|
|
155
193
|
}>();
|
|
156
194
|
|
|
157
195
|
const emit = defineEmits<{
|
|
158
196
|
(e: 'toggle'): void;
|
|
159
197
|
(e: 'open-view', view: NavDest): void;
|
|
160
|
-
(e: 'open-tag', tag: string): void;
|
|
198
|
+
(e: 'open-tag', tag: string, kind: TagKind): void;
|
|
161
199
|
(e: 'open-storage', name: string): void;
|
|
162
200
|
(e: 'open-root'): void;
|
|
163
201
|
(e: 'upload'): void;
|
|
@@ -165,13 +203,25 @@ const emit = defineEmits<{
|
|
|
165
203
|
(e: 'new-document'): void;
|
|
166
204
|
(e: 'open-connections'): void;
|
|
167
205
|
(e: 'open-tokens'): void;
|
|
206
|
+
/**
|
|
207
|
+
* "My shares" — the public links THIS person handed out.
|
|
208
|
+
*
|
|
209
|
+
* ⚠ Unlike every other row in the views group, it does not change the
|
|
210
|
+
* listing: the screen lives in the HOST (the SPA's `my-shares` route), so
|
|
211
|
+
* the panel can only announce the intent and the host decides where it
|
|
212
|
+
* lands. A host with no such screen does not listen, and nothing here
|
|
213
|
+
* pretends otherwise.
|
|
214
|
+
*/
|
|
215
|
+
(e: 'open-my-shares'): void;
|
|
216
|
+
/** App plugins — an "Apps" row: open that `home` view. */
|
|
217
|
+
(e: 'open-app', key: string): void;
|
|
168
218
|
/* surucu:d1 — "Request files": the access modal on THIS folder, drop tab. */
|
|
169
219
|
(e: 'request-files'): void;
|
|
170
220
|
/** Drawer scrim / Esc — narrow mode only. */
|
|
171
221
|
(e: 'close'): void;
|
|
172
222
|
}>();
|
|
173
223
|
|
|
174
|
-
const { t, formatSize } = useLocale(() => props.locale);
|
|
224
|
+
const { t, formatSize, dir } = useLocale(() => props.locale);
|
|
175
225
|
|
|
176
226
|
// In drawer mode "expanded" is the only meaningful state: a rail inside an
|
|
177
227
|
// overlay would be an overlay that shows nothing but icons while covering the
|
|
@@ -242,6 +292,14 @@ const views = computed(() => {
|
|
|
242
292
|
|
|
243
293
|
const writable = computed(() => props.canWrite !== false);
|
|
244
294
|
|
|
295
|
+
/** App plugins — the "Apps" rows, with a glyph the icon library knows. */
|
|
296
|
+
const appRows = computed(() =>
|
|
297
|
+
(props.apps ?? []).map((a) => ({
|
|
298
|
+
...a,
|
|
299
|
+
svg: actionIconSvg(a.icon && actionIconSvg(a.icon) !== '' ? a.icon : 'plugin'),
|
|
300
|
+
})),
|
|
301
|
+
);
|
|
302
|
+
|
|
245
303
|
/**
|
|
246
304
|
* The storage the listing is in is mounted read-only. Its "+ New" menu is not
|
|
247
305
|
* disabled but GONE: on a read-only mount there is no folder where any of
|
|
@@ -252,6 +310,14 @@ const activeReadOnly = computed(
|
|
|
252
310
|
() => !!props.activeStorage && props.storages.some((s) => s.name === props.activeStorage && s.readOnly === true),
|
|
253
311
|
);
|
|
254
312
|
|
|
313
|
+
/**
|
|
314
|
+
* The Trash is read-only in the same sense, and gets the same answer: no
|
|
315
|
+
* "+ New". It used to stand there with every row greyed (QA, 2026-09-21) —
|
|
316
|
+
* nothing is ever made inside the Trash, so a menu of things you cannot make
|
|
317
|
+
* there only asks a question it cannot answer.
|
|
318
|
+
*/
|
|
319
|
+
const newShown = computed(() => !activeReadOnly.value && props.activeView !== 'trash');
|
|
320
|
+
|
|
255
321
|
/**
|
|
256
322
|
* Which row reads as the one you are standing on.
|
|
257
323
|
*
|
|
@@ -283,10 +349,28 @@ function isActiveDest(key: NavDest): boolean {
|
|
|
283
349
|
const TAG_PEEK = 8;
|
|
284
350
|
const tagsExpanded = ref(false);
|
|
285
351
|
const allTags = computed(() => props.tags ?? []);
|
|
286
|
-
|
|
287
|
-
|
|
352
|
+
/* etiket:k2 (v0.43.0) — the list is two GROUPS, Personal then Team, each under
|
|
353
|
+
* its own caption with the kind's glyph, and each row repeats the glyph. Until
|
|
354
|
+
* this release there was one kind, shared with every account on the server,
|
|
355
|
+
* and the panel never said so (tester, 2026-09-22). The peek applies per
|
|
356
|
+
* group: sixty personal tags must not push the team's off the panel. */
|
|
357
|
+
const tagGroups = computed(() =>
|
|
358
|
+
(['personal', 'team'] as TagKind[])
|
|
359
|
+
.map((kind) => {
|
|
360
|
+
const list = allTags.value.filter((tag) => tag.kind === kind);
|
|
361
|
+
return { kind, list, visible: tagsExpanded.value ? list : list.slice(0, TAG_PEEK) };
|
|
362
|
+
})
|
|
363
|
+
.filter((g) => g.list.length > 0),
|
|
288
364
|
);
|
|
289
|
-
const hiddenTagCount = computed(() =>
|
|
365
|
+
const hiddenTagCount = computed(() =>
|
|
366
|
+
tagGroups.value.reduce((n, g) => n + g.list.length - g.visible.length, 0),
|
|
367
|
+
);
|
|
368
|
+
const anyGroupLong = computed(() => tagGroups.value.some((g) => g.list.length > TAG_PEEK));
|
|
369
|
+
|
|
370
|
+
function tagIsActive(tag: TagItem): boolean {
|
|
371
|
+
if (props.activeView !== 'tag' || props.activeTag !== tag.name) return false;
|
|
372
|
+
return !props.activeTagKind || props.activeTagKind === tag.kind;
|
|
373
|
+
}
|
|
290
374
|
/* The section is rendered as soon as the panel knows there ARE tags, and also
|
|
291
375
|
* once the answer came back empty — an empty section that says why is how a
|
|
292
376
|
* user learns the feature exists at all. It stays hidden only while the first
|
|
@@ -341,7 +425,8 @@ const newActions = computed<ContextAction[]>(() => {
|
|
|
341
425
|
function openNewMenu() {
|
|
342
426
|
const r = newBtnEl.value?.getBoundingClientRect();
|
|
343
427
|
newMenuRef.value?.show(
|
|
344
|
-
|
|
428
|
+
// ⚠ RTL: hangs from the pill's START edge (its right side in RTL).
|
|
429
|
+
{ clientX: r ? inlineStartX(r, dir.value) : 0, clientY: r ? r.bottom + 6 : 0 } as MouseEvent,
|
|
345
430
|
[],
|
|
346
431
|
);
|
|
347
432
|
}
|
|
@@ -436,7 +521,7 @@ const toggleLabel = computed(() => t('sidenav.close'));
|
|
|
436
521
|
stand here behind `v-if="!newMenu"` (Upload + New folder) is gone:
|
|
437
522
|
both verbs are the first two rows of this menu. -->
|
|
438
523
|
<button
|
|
439
|
-
v-if="
|
|
524
|
+
v-if="newShown"
|
|
440
525
|
ref="newBtnEl"
|
|
441
526
|
type="button"
|
|
442
527
|
class="fe-sidenav__new"
|
|
@@ -494,58 +579,113 @@ const toggleLabel = computed(() => t('sidenav.close'));
|
|
|
494
579
|
|
|
495
580
|
<div class="fe-sidenav__scroll">
|
|
496
581
|
<ul class="fe-sidenav__group" :aria-label="t('sidenav.views')">
|
|
497
|
-
<
|
|
498
|
-
<
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
<svg
|
|
509
|
-
class="fe-ficon"
|
|
510
|
-
viewBox="0 0 24 24"
|
|
511
|
-
fill="none"
|
|
512
|
-
stroke="currentColor"
|
|
513
|
-
stroke-width="1.8"
|
|
514
|
-
stroke-linecap="round"
|
|
515
|
-
stroke-linejoin="round"
|
|
516
|
-
aria-hidden="true"
|
|
517
|
-
focusable="false"
|
|
582
|
+
<template v-for="v in views" :key="v.key">
|
|
583
|
+
<li>
|
|
584
|
+
<button
|
|
585
|
+
type="button"
|
|
586
|
+
class="fe-sidenav__item"
|
|
587
|
+
:class="{ 'is-active': isActiveDest(v.key) }"
|
|
588
|
+
:aria-current="isActiveDest(v.key) ? 'page' : undefined"
|
|
589
|
+
:title="v.label"
|
|
590
|
+
:aria-label="v.label"
|
|
591
|
+
:data-testid="`sidenav-view-${v.key}`"
|
|
592
|
+
@click="emit('open-view', v.key)"
|
|
518
593
|
>
|
|
519
|
-
<
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
<
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
<
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
<
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
594
|
+
<svg
|
|
595
|
+
class="fe-ficon"
|
|
596
|
+
viewBox="0 0 24 24"
|
|
597
|
+
fill="none"
|
|
598
|
+
stroke="currentColor"
|
|
599
|
+
stroke-width="1.8"
|
|
600
|
+
stroke-linecap="round"
|
|
601
|
+
stroke-linejoin="round"
|
|
602
|
+
aria-hidden="true"
|
|
603
|
+
focusable="false"
|
|
604
|
+
>
|
|
605
|
+
<template v-if="v.key === 'home'">
|
|
606
|
+
<path d="M4 10.5L12 4l8 6.5" />
|
|
607
|
+
<path d="M6 9.8V19a1 1 0 0 0 1 1h10a1 1 0 0 0 1-1V9.8" />
|
|
608
|
+
<path d="M10 20v-5.5h4V20" />
|
|
609
|
+
</template>
|
|
610
|
+
<template v-else-if="v.key === 'myfiles'">
|
|
611
|
+
<path d="M3.5 7.5A1.5 1.5 0 0 1 5 6h4l2 2.5h8A1.5 1.5 0 0 1 20.5 10v7.5A1.5 1.5 0 0 1 19 19H5a1.5 1.5 0 0 1-1.5-1.5z" />
|
|
612
|
+
</template>
|
|
613
|
+
<template v-else-if="v.key === 'recent'">
|
|
614
|
+
<circle cx="12" cy="12" r="8.5" />
|
|
615
|
+
<path d="M12 7.5V12l3 2" />
|
|
616
|
+
</template>
|
|
617
|
+
<template v-else-if="v.key === 'starred'">
|
|
618
|
+
<path d="M12 4l2.4 4.9 5.4.8-3.9 3.8.9 5.4-4.8-2.5-4.8 2.5.9-5.4-3.9-3.8 5.4-.8z" />
|
|
619
|
+
</template>
|
|
620
|
+
<template v-else-if="v.key === 'shared'">
|
|
621
|
+
<circle cx="17.5" cy="6.5" r="2.5" />
|
|
622
|
+
<circle cx="6.5" cy="12" r="2.5" />
|
|
623
|
+
<circle cx="17.5" cy="17.5" r="2.5" />
|
|
624
|
+
<path d="M8.8 10.8l6.4-3.2M8.8 13.2l6.4 3.2" />
|
|
625
|
+
</template>
|
|
626
|
+
<template v-else>
|
|
627
|
+
<path d="M4.5 7h15" />
|
|
628
|
+
<path d="M9.5 7V5.2A1.2 1.2 0 0 1 10.7 4h2.6a1.2 1.2 0 0 1 1.2 1.2V7" />
|
|
629
|
+
<path d="M6.5 7l.9 11.1A1.4 1.4 0 0 0 8.8 19.4h6.4a1.4 1.4 0 0 0 1.4-1.3L17.5 7" />
|
|
630
|
+
</template>
|
|
631
|
+
</svg>
|
|
632
|
+
<span v-if="showLabels" class="fe-sidenav__text">{{ v.label }}</span>
|
|
633
|
+
</button>
|
|
634
|
+
</li>
|
|
635
|
+
|
|
636
|
+
<!-- paylas:m1 — "My shares", directly under "Shared with me".
|
|
637
|
+
The pair is one question asked in both directions: what other
|
|
638
|
+
people handed to me, and what I handed out. A person hunting for
|
|
639
|
+
a link they minted looks where the sharing rows are, not under
|
|
640
|
+
Connections — that group is the one-time setup corner (how to
|
|
641
|
+
mount this, which API keys exist), and burying a daily lookup in
|
|
642
|
+
it is how a screen stays undiscovered.
|
|
643
|
+
|
|
644
|
+
⚠ Anchored to the 'shared' row rather than appended to the group:
|
|
645
|
+
it has to sit BESIDE its mirror, and `views` may or may not carry
|
|
646
|
+
"My files" above it. That anchoring also carries the app-token
|
|
647
|
+
rule for free — `views` drops the identity rows for an app token,
|
|
648
|
+
so "Shared with me" goes and this row goes with it, which is
|
|
649
|
+
right: an app token is not a person and has shared nothing.
|
|
650
|
+
|
|
651
|
+
⚠ It emits rather than navigates. The destination is a host
|
|
652
|
+
route, not a listing this panel can open — which is exactly why
|
|
653
|
+
`showMyShares` gates it and why that gate is OFF unless a host
|
|
654
|
+
asks. In an embed (`<filex-explorer>` on work.example.com, in the
|
|
655
|
+
fishapp, on fm.example.com) nothing listens for `open-my-shares`, so
|
|
656
|
+
an ungated row would be the one entry in this panel that draws,
|
|
657
|
+
takes a click and leads nowhere. Every other row is either
|
|
658
|
+
handled inside FileExplorer or gated by a flag of its own
|
|
659
|
+
(`trashVisible`, `showConnections`); this one now is too. -->
|
|
660
|
+
<li v-if="v.key === 'shared' && showMyShares">
|
|
661
|
+
<button
|
|
662
|
+
type="button"
|
|
663
|
+
class="fe-sidenav__item"
|
|
664
|
+
:title="t('sidenav.myshares')"
|
|
665
|
+
:aria-label="t('sidenav.myshares')"
|
|
666
|
+
data-testid="sidenav-my-shares"
|
|
667
|
+
@click="emit('open-my-shares')"
|
|
668
|
+
>
|
|
669
|
+
<!-- A chain link, against the three-node graph the row above
|
|
670
|
+
draws: what leaves here is a URL. -->
|
|
671
|
+
<svg
|
|
672
|
+
class="fe-ficon"
|
|
673
|
+
viewBox="0 0 24 24"
|
|
674
|
+
fill="none"
|
|
675
|
+
stroke="currentColor"
|
|
676
|
+
stroke-width="1.8"
|
|
677
|
+
stroke-linecap="round"
|
|
678
|
+
stroke-linejoin="round"
|
|
679
|
+
aria-hidden="true"
|
|
680
|
+
focusable="false"
|
|
681
|
+
>
|
|
682
|
+
<path d="M10 13.5a4.5 4.5 0 0 0 6.8.5l2.7-2.7a4.5 4.5 0 0 0-6.36-6.36L11.6 6.5" />
|
|
683
|
+
<path d="M14 10.5a4.5 4.5 0 0 0-6.8-.5l-2.7 2.7a4.5 4.5 0 0 0 6.36 6.36l1.53-1.53" />
|
|
684
|
+
</svg>
|
|
685
|
+
<span v-if="showLabels" class="fe-sidenav__text">{{ t('sidenav.myshares') }}</span>
|
|
686
|
+
</button>
|
|
687
|
+
</li>
|
|
688
|
+
</template>
|
|
549
689
|
</ul>
|
|
550
690
|
|
|
551
691
|
<!-- etiket:t1 — Tags. Between the views and the storages because a tag
|
|
@@ -557,35 +697,36 @@ const toggleLabel = computed(() => t('sidenav.close'));
|
|
|
557
697
|
<div v-if="showTags" class="fe-sidenav__section">
|
|
558
698
|
<template v-if="showLabels">
|
|
559
699
|
<p class="fe-sidenav__heading">{{ t('sidenav.tags') }}</p>
|
|
560
|
-
<
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
>
|
|
572
|
-
<
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
700
|
+
<template v-for="g in tagGroups" :key="g.kind">
|
|
701
|
+
<!-- etiket:k2 — whose tags these are, in words and in the glyph
|
|
702
|
+
every tag chip carries. -->
|
|
703
|
+
<p class="fe-sidenav__subheading" :data-testid="`sidenav-tags-${g.kind}`">
|
|
704
|
+
<TagKindIcon :kind="g.kind" :size="12" />
|
|
705
|
+
<span>{{ t(`sidenav.tags.${g.kind}`) }}</span>
|
|
706
|
+
</p>
|
|
707
|
+
<ul
|
|
708
|
+
class="fe-sidenav__group"
|
|
709
|
+
:aria-label="`${t('sidenav.tags')} — ${t(`sidenav.tags.${g.kind}`)}`"
|
|
710
|
+
>
|
|
711
|
+
<li v-for="tag in g.visible" :key="`${tag.kind}:${tag.name}`">
|
|
712
|
+
<button
|
|
713
|
+
type="button"
|
|
714
|
+
class="fe-sidenav__item fe-sidenav__item--tag"
|
|
715
|
+
:class="{ 'is-active': tagIsActive(tag) }"
|
|
716
|
+
:aria-current="tagIsActive(tag) ? 'page' : undefined"
|
|
717
|
+
:title="t(`tags.chip.${tag.kind}`, { tag: tag.name })"
|
|
718
|
+
:aria-label="t(`tags.chip.${tag.kind}`, { tag: tag.name })"
|
|
719
|
+
:data-testid="`sidenav-tag-${tag.name}`"
|
|
720
|
+
:data-tag-kind="tag.kind"
|
|
721
|
+
@click="emit('open-tag', tag.name, tag.kind)"
|
|
582
722
|
>
|
|
583
|
-
<
|
|
584
|
-
<
|
|
585
|
-
</
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
723
|
+
<TagKindIcon class="fe-ficon" :kind="tag.kind" :size="16" />
|
|
724
|
+
<span class="fe-sidenav__text">{{ tag.name }}</span>
|
|
725
|
+
</button>
|
|
726
|
+
</li>
|
|
727
|
+
</ul>
|
|
728
|
+
</template>
|
|
729
|
+
<ul class="fe-sidenav__group" :aria-label="t('sidenav.tags')">
|
|
589
730
|
<!-- Nothing tagged yet: the section stays, and says how tags get
|
|
590
731
|
made. A section that only exists once you already know the
|
|
591
732
|
feature teaches nobody. -->
|
|
@@ -602,7 +743,7 @@ const toggleLabel = computed(() => t('sidenav.close'));
|
|
|
602
743
|
{{ t('sidenav.tags.more', { count: hiddenTagCount }) }}
|
|
603
744
|
</button>
|
|
604
745
|
</li>
|
|
605
|
-
<li v-else-if="tagsExpanded &&
|
|
746
|
+
<li v-else-if="tagsExpanded && anyGroupLong">
|
|
606
747
|
<button
|
|
607
748
|
type="button"
|
|
608
749
|
class="fe-sidenav__more"
|
|
@@ -707,14 +848,37 @@ const toggleLabel = computed(() => t('sidenav.close'));
|
|
|
707
848
|
<!-- A read-only mount says so on the row, where the user looks
|
|
708
849
|
for it. The admin list had an "RO" badge; the people it
|
|
709
850
|
applied to never saw one (issue #30). -->
|
|
710
|
-
<
|
|
851
|
+
<StorageTags
|
|
711
852
|
v-if="showLabels && s.readOnly"
|
|
712
|
-
|
|
713
|
-
|
|
853
|
+
read-only
|
|
854
|
+
:locale="locale"
|
|
855
|
+
class="fe-sidenav__stags"
|
|
714
856
|
data-testid="sidenav-storage-readonly"
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
857
|
+
/>
|
|
858
|
+
</button>
|
|
859
|
+
</li>
|
|
860
|
+
</ul>
|
|
861
|
+
</div>
|
|
862
|
+
|
|
863
|
+
<!-- App plugins — "Apps": one row per `home` view. Between the storages
|
|
864
|
+
and Connections: an app is a place you go, like a drive, not a
|
|
865
|
+
setting. Hidden entirely when there are none. -->
|
|
866
|
+
<div v-if="appRows.length" class="fe-sidenav__section" data-testid="sidenav-apps">
|
|
867
|
+
<p v-if="showLabels" class="fe-sidenav__heading">{{ t('sidenav.apps') }}</p>
|
|
868
|
+
<hr v-else class="fe-sidenav__rule" aria-hidden="true" />
|
|
869
|
+
<ul class="fe-sidenav__group" :aria-label="t('sidenav.apps')">
|
|
870
|
+
<li v-for="a in appRows" :key="a.key">
|
|
871
|
+
<button
|
|
872
|
+
type="button"
|
|
873
|
+
class="fe-sidenav__item"
|
|
874
|
+
:title="a.label"
|
|
875
|
+
:aria-label="a.label"
|
|
876
|
+
:data-testid="`sidenav-app-${a.key}`"
|
|
877
|
+
@click="emit('open-app', a.key)"
|
|
878
|
+
>
|
|
879
|
+
<!-- eslint-disable-next-line vue/no-v-html -- static markup from lib/actionIcons -->
|
|
880
|
+
<span class="fe-sidenav__appicon" aria-hidden="true" v-html="a.svg"></span>
|
|
881
|
+
<span v-if="showLabels" class="fe-sidenav__text">{{ a.label }}</span>
|
|
718
882
|
</button>
|
|
719
883
|
</li>
|
|
720
884
|
</ul>
|
|
@@ -20,6 +20,7 @@ import type { LocaleCode } from '../types/ExplorerConfig';
|
|
|
20
20
|
import type { StorageField, StorageFieldOption } from '../types/Connections';
|
|
21
21
|
import { useLocale } from '../composables/useLocale';
|
|
22
22
|
import { actionIconSvg } from '../lib/actionIcons'; /* ikon:emoji */
|
|
23
|
+
import ChoiceButtons, { type ChoiceOption } from './ChoiceButtons.vue';
|
|
23
24
|
|
|
24
25
|
const props = defineProps<{
|
|
25
26
|
fields: StorageField[];
|
|
@@ -27,6 +28,12 @@ const props = defineProps<{
|
|
|
27
28
|
locale: LocaleCode;
|
|
28
29
|
/** Keys to mark as failing validation (missing required). */
|
|
29
30
|
invalid?: string[];
|
|
31
|
+
/**
|
|
32
|
+
* Field-level messages, keyed by field key — what a server (an app
|
|
33
|
+
* plugin's `surface.errors`, a driver's 400) said about the value. A key
|
|
34
|
+
* here is invalid too, and its own words replace the generic "required".
|
|
35
|
+
*/
|
|
36
|
+
errors?: Record<string, string>;
|
|
30
37
|
disabled?: boolean;
|
|
31
38
|
}>();
|
|
32
39
|
|
|
@@ -98,7 +105,53 @@ function bool(f: StorageField): boolean {
|
|
|
98
105
|
}
|
|
99
106
|
|
|
100
107
|
function isInvalid(f: StorageField): boolean {
|
|
101
|
-
return (props.invalid ?? []).includes(f.key);
|
|
108
|
+
return (props.invalid ?? []).includes(f.key) || !!props.errors?.[f.key];
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function errorText(f: StorageField): string {
|
|
112
|
+
return props.errors?.[f.key] || (isInvalid(f) ? t('conn.form.required') : '');
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* The options a `choice` field draws as buttons.
|
|
117
|
+
*
|
|
118
|
+
* A `bool` has two by definition and the catalogue names them, unless the
|
|
119
|
+
* declaration named them itself — "Keep the original / Replace it" reads
|
|
120
|
+
* better than "Yes / No" and is the plugin author's call, not ours.
|
|
121
|
+
*/
|
|
122
|
+
function choiceOptions(f: StorageField): ChoiceOption[] {
|
|
123
|
+
if (f.type === 'bool') {
|
|
124
|
+
const given = (f.options ?? []).filter((o) => o.value === 'true' || o.value === 'false');
|
|
125
|
+
const yes = given.find((o) => o.value === 'true');
|
|
126
|
+
const no = given.find((o) => o.value === 'false');
|
|
127
|
+
return [
|
|
128
|
+
{ value: 'true', label: yes ? optionLabel(yes) : t('conn.form.yes') },
|
|
129
|
+
{ value: 'false', label: no ? optionLabel(no) : t('conn.form.no') },
|
|
130
|
+
];
|
|
131
|
+
}
|
|
132
|
+
return (f.options ?? []).map((o) => ({ value: o.value, label: optionLabel(o) }));
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/** What a `choice` field holds right now, in the shape ChoiceButtons reads. */
|
|
136
|
+
function choiceValue(f: StorageField): string | string[] {
|
|
137
|
+
const v = raw(f);
|
|
138
|
+
if (f.type === 'bool') return v === true || v === 'true' ? 'true' : v === false || v === 'false' ? 'false' : '';
|
|
139
|
+
if (f.multi) return Array.isArray(v) ? v.map((x) => String(x)) : v === undefined || v === null || v === '' ? [] : [String(v)];
|
|
140
|
+
return v === undefined || v === null ? '' : String(v);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/** …and back: a bool becomes a boolean again, everything else stays as declared. */
|
|
144
|
+
function setChoice(f: StorageField, v: string | string[]): void {
|
|
145
|
+
if (f.type === 'bool') {
|
|
146
|
+
set(f.key, v === 'true');
|
|
147
|
+
return;
|
|
148
|
+
}
|
|
149
|
+
set(f.key, v);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/** A field drawn as buttons — never as a dropdown, never as a lone checkbox. */
|
|
153
|
+
function isChoice(f: StorageField): boolean {
|
|
154
|
+
return f.choice === true && (f.type === 'select' || f.type === 'bool');
|
|
102
155
|
}
|
|
103
156
|
|
|
104
157
|
function inputType(f: StorageField): string {
|
|
@@ -118,8 +171,10 @@ function toggleReveal(key: string) {
|
|
|
118
171
|
|
|
119
172
|
<template v-for="f in basic" :key="f.key">
|
|
120
173
|
<div class="fe-cfield__row" :class="{ 'is-invalid': isInvalid(f) }">
|
|
121
|
-
<!-- bool: the label belongs next to the box, not above it
|
|
122
|
-
|
|
174
|
+
<!-- bool: the label belongs next to the box, not above it — unless
|
|
175
|
+
the field is a DECISION, which is drawn as two buttons under a
|
|
176
|
+
label of its own (v3 §2.2). -->
|
|
177
|
+
<label v-if="f.type === 'bool' && !isChoice(f)" class="fe-cfield__check">
|
|
123
178
|
<input
|
|
124
179
|
type="checkbox"
|
|
125
180
|
:checked="bool(f)"
|
|
@@ -132,8 +187,20 @@ function toggleReveal(key: string) {
|
|
|
132
187
|
{{ label(f) }}<span v-if="f.required" class="fe-cfield__req" aria-hidden="true">*</span>
|
|
133
188
|
</label>
|
|
134
189
|
|
|
190
|
+
<ChoiceButtons
|
|
191
|
+
v-if="isChoice(f)"
|
|
192
|
+
:options="choiceOptions(f)"
|
|
193
|
+
:model-value="choiceValue(f)"
|
|
194
|
+
:multi="f.multi === true"
|
|
195
|
+
:disabled="disabled"
|
|
196
|
+
:invalid="isInvalid(f)"
|
|
197
|
+
:aria-label="label(f)"
|
|
198
|
+
:testid-prefix="`fe-choice-${f.key}`"
|
|
199
|
+
@update:model-value="(v: string | string[]) => setChoice(f, v)"
|
|
200
|
+
/>
|
|
201
|
+
|
|
135
202
|
<select
|
|
136
|
-
v-if="f.type === 'select'"
|
|
203
|
+
v-else-if="f.type === 'select'"
|
|
137
204
|
:id="`fe-cf-${f.key}`"
|
|
138
205
|
class="fe-cfield__input"
|
|
139
206
|
:value="str(f)"
|
|
@@ -213,7 +280,7 @@ function toggleReveal(key: string) {
|
|
|
213
280
|
/>
|
|
214
281
|
|
|
215
282
|
<p v-if="help(f)" class="fe-cfield__help">{{ help(f) }}</p>
|
|
216
|
-
<p v-if="
|
|
283
|
+
<p v-if="errorText(f)" class="fe-cfield__error">{{ errorText(f) }}</p>
|
|
217
284
|
</div>
|
|
218
285
|
</template>
|
|
219
286
|
|
|
@@ -255,6 +322,7 @@ function toggleReveal(key: string) {
|
|
|
255
322
|
/>
|
|
256
323
|
</template>
|
|
257
324
|
<p v-if="help(f)" class="fe-cfield__help">{{ help(f) }}</p>
|
|
325
|
+
<p v-if="errorText(f)" class="fe-cfield__error">{{ errorText(f) }}</p>
|
|
258
326
|
</div>
|
|
259
327
|
</div>
|
|
260
328
|
</div>
|
|
@@ -284,7 +352,7 @@ function toggleReveal(key: string) {
|
|
|
284
352
|
}
|
|
285
353
|
.fe-cfield__req {
|
|
286
354
|
color: var(--fe-danger);
|
|
287
|
-
margin-
|
|
355
|
+
margin-inline-start: 3px;
|
|
288
356
|
}
|
|
289
357
|
.fe-cfield__input {
|
|
290
358
|
font: inherit;
|