@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,62 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
/**
|
|
3
|
+
* StorageTags — what kind of storage a row is, in the same words everywhere.
|
|
4
|
+
*
|
|
5
|
+
* ⚠⚠ ONE vocabulary (release-candidate sweep, 2026-09-21, QA #34): the admin
|
|
6
|
+
* panel had TWO lists of the same storages, and they described them in two
|
|
7
|
+
* dialects — Admin → Storages marked a read-only storage "RO" next to a raw
|
|
8
|
+
* driver id ("local"), Connections → Storages marked it "SALT OKUNUR" next to
|
|
9
|
+
* "LOCAL" (the same id, upper-cased by CSS), and the explorer's side bar says
|
|
10
|
+
* "Salt okunur". This component is the one place those words are chosen: the
|
|
11
|
+
* driver's NAME (the same `storages.driver.*` names the storage form offers),
|
|
12
|
+
* then "Read-only" and "Disabled" when they apply. Both lists draw it.
|
|
13
|
+
*
|
|
14
|
+
* A driver this build has no name for (an external plugin's) is shown as its
|
|
15
|
+
* id rather than hidden.
|
|
16
|
+
*
|
|
17
|
+
* ⚠ It is also THE read-only mark of the explorer: the side panel's storage
|
|
18
|
+
* row and the Home card draw it with no `driver` (only the fact that matters
|
|
19
|
+
* there). Wave 2 had grown two looks for one fact — the explorer's tinted pill
|
|
20
|
+
* ("Salt okunur" on the panel and on Home) and this bordered one in the admin
|
|
21
|
+
* lists — so the explorer now draws this component too.
|
|
22
|
+
*/
|
|
23
|
+
import { computed } from 'vue';
|
|
24
|
+
import type { LocaleCode } from '../types/ExplorerConfig';
|
|
25
|
+
import { useLocale } from '../composables/useLocale';
|
|
26
|
+
|
|
27
|
+
const props = withDefaults(
|
|
28
|
+
defineProps<{
|
|
29
|
+
/** The driver id; absent draws no driver tag (the explorer's read-only mark). */
|
|
30
|
+
driver?: string;
|
|
31
|
+
readOnly?: boolean;
|
|
32
|
+
/** `false` draws "Disabled"; `undefined` / `true` draws nothing. */
|
|
33
|
+
enabled?: boolean;
|
|
34
|
+
locale?: LocaleCode | string;
|
|
35
|
+
}>(),
|
|
36
|
+
// ⚠ An explicit `undefined` default. Vue casts an ABSENT boolean prop to
|
|
37
|
+
// `false`, so without it every caller that does not pass `enabled` (the
|
|
38
|
+
// explorer's marks, Admin → Replica) drew "Disabled" beside "Read-only".
|
|
39
|
+
{ enabled: undefined },
|
|
40
|
+
);
|
|
41
|
+
|
|
42
|
+
const { t } = useLocale(() => props.locale ?? 'en');
|
|
43
|
+
|
|
44
|
+
const driverName = computed(() => {
|
|
45
|
+
if (!props.driver) return '';
|
|
46
|
+
const key = `storages.driver.${props.driver}`;
|
|
47
|
+
const out = t(key);
|
|
48
|
+
return out === key ? props.driver : out;
|
|
49
|
+
});
|
|
50
|
+
</script>
|
|
51
|
+
|
|
52
|
+
<template>
|
|
53
|
+
<span class="fe-stags" data-testid="storage-tags">
|
|
54
|
+
<span v-if="driver" class="fe-stag" :title="driver" data-testid="storage-tag-driver">{{ driverName }}</span>
|
|
55
|
+
<span v-if="readOnly" class="fe-stag fe-stag--warn" data-testid="storage-tag-readonly">{{
|
|
56
|
+
t('sidenav.storage.readOnly')
|
|
57
|
+
}}</span>
|
|
58
|
+
<span v-if="enabled === false" class="fe-stag" data-testid="storage-tag-disabled">{{
|
|
59
|
+
t('conn.list.disabled')
|
|
60
|
+
}}</span>
|
|
61
|
+
</span>
|
|
62
|
+
</template>
|
|
@@ -162,7 +162,7 @@ function onAux(id: string, ev: MouseEvent) {
|
|
|
162
162
|
@dragend="onDragEnd"
|
|
163
163
|
>
|
|
164
164
|
<span v-if="tab.split" class="fe-tabs__splitdot" aria-hidden="true"></span>
|
|
165
|
-
<span class="fe-tabs__label">{{ tab.label }}</span>
|
|
165
|
+
<span class="fe-tabs__label"><bdi>{{ tab.label }}</bdi></span>
|
|
166
166
|
<!-- ⚠ Not drawn when this is the only tab. Owner's decision,
|
|
167
167
|
2026-09-13, verbatim: *"tek kalan tab'de x gözükmemeli, onu da
|
|
168
168
|
kaldırırsın."* It is not a style choice — `useTabs.closeTab()`
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
/**
|
|
3
|
+
* TagKindIcon — the one glyph that says which KIND a tag is.
|
|
4
|
+
*
|
|
5
|
+
* ⚠⚠ Why this exists (v0.43.0): tags used to be one kind, silently shared with
|
|
6
|
+
* every account on the server while the code called them per-user; a tester's
|
|
7
|
+
* "müşteri teklifi" showed up in other people's panels and they could remove
|
|
8
|
+
* it, and nothing on screen had ever said a tag was shared. With two kinds the
|
|
9
|
+
* screen has to say it EVERYWHERE a tag appears — the picker, the chips on a
|
|
10
|
+
* file, the navigation panel, the tag view's heading, the search filter — and
|
|
11
|
+
* one component is the only way those five cannot draw it five ways.
|
|
12
|
+
*
|
|
13
|
+
* personal — one figure: yours, like a star.
|
|
14
|
+
* team — two figures: everyone in your team who can see the file.
|
|
15
|
+
*
|
|
16
|
+
* Decorative (`aria-hidden`): every caller also puts the kind into words
|
|
17
|
+
* (`tags.kind.*` in the title / accessible name), because an icon alone is
|
|
18
|
+
* not an explanation.
|
|
19
|
+
*/
|
|
20
|
+
import type { TagKind } from '../lib/tags';
|
|
21
|
+
|
|
22
|
+
defineProps<{
|
|
23
|
+
kind: TagKind;
|
|
24
|
+
/** Edge length in px. */
|
|
25
|
+
size?: number;
|
|
26
|
+
}>();
|
|
27
|
+
</script>
|
|
28
|
+
|
|
29
|
+
<template>
|
|
30
|
+
<svg
|
|
31
|
+
class="fe-tagkind"
|
|
32
|
+
:class="`fe-tagkind--${kind}`"
|
|
33
|
+
:width="size ?? 12"
|
|
34
|
+
:height="size ?? 12"
|
|
35
|
+
viewBox="0 0 24 24"
|
|
36
|
+
fill="none"
|
|
37
|
+
stroke="currentColor"
|
|
38
|
+
stroke-width="2"
|
|
39
|
+
stroke-linecap="round"
|
|
40
|
+
stroke-linejoin="round"
|
|
41
|
+
aria-hidden="true"
|
|
42
|
+
focusable="false"
|
|
43
|
+
:data-tag-kind="kind"
|
|
44
|
+
>
|
|
45
|
+
<template v-if="kind === 'team'">
|
|
46
|
+
<circle cx="9" cy="8" r="3.2" />
|
|
47
|
+
<path d="M3 20c0-3.3 2.7-6 6-6s6 2.7 6 6" />
|
|
48
|
+
<circle cx="17" cy="9" r="2.6" />
|
|
49
|
+
<path d="M16.5 14.2c2.6.3 4.5 2.6 4.5 5.3" />
|
|
50
|
+
</template>
|
|
51
|
+
<template v-else>
|
|
52
|
+
<circle cx="12" cy="8" r="3.6" />
|
|
53
|
+
<path d="M5 20c0-3.9 3.1-7 7-7s7 3.1 7 7" />
|
|
54
|
+
</template>
|
|
55
|
+
</svg>
|
|
56
|
+
</template>
|
|
@@ -1,13 +1,36 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
|
+
import { requestFailure } from '../lib/errorWords';
|
|
2
3
|
/**
|
|
3
4
|
* TagPicker — small chip list with add/remove for a node.
|
|
4
5
|
*
|
|
5
6
|
* Reads `GET /api/files/manager/tags?node_id=…` on mount, writes the
|
|
6
|
-
* full new
|
|
7
|
+
* full new set via `POST /api/files/manager/tags`.
|
|
7
8
|
*
|
|
8
9
|
* Tag colors are deterministic: hash(name) → palette index. Consumers
|
|
9
10
|
* can override with the `palette` prop.
|
|
10
11
|
*
|
|
12
|
+
* === etiket:k2 — TWO KINDS, AND THE CHIP SAYS WHICH (v0.43.0) ===========
|
|
13
|
+
* ⚠⚠ Tester, 2026-09-22: a non-admin's tag "müşteri teklifi" appeared in
|
|
14
|
+
* another user's and the admin's panel and on the file, and the other user
|
|
15
|
+
* could remove it. Tags were one shared label per file while the code called
|
|
16
|
+
* them per-user, and this component never said a word about who could see
|
|
17
|
+
* one. The owner's decision: personal and team tags.
|
|
18
|
+
*
|
|
19
|
+
* - Every chip carries its kind — `TagKindIcon` plus the words in its title
|
|
20
|
+
* and accessible name — so nobody has to guess who sees it.
|
|
21
|
+
* - Adding asks WHO SEES IT, with the answer written under each choice.
|
|
22
|
+
* Personal is the default: a label nobody chose to share is not shared.
|
|
23
|
+
* - "Team" is offered only where saving it would succeed: the server says
|
|
24
|
+
* `can_edit_team` (edit permission on the file). For a viewer it stays on
|
|
25
|
+
* screen, disabled, with the reason — a choice that vanishes cannot tell
|
|
26
|
+
* the person it exists. A team chip's × is likewise absent for them.
|
|
27
|
+
* - The name keeps its capitals ("Müşteri Teklifi"); the server decides
|
|
28
|
+
* sameness (lib/tags `tagKey` mirrors it only to avoid a pointless POST).
|
|
29
|
+
* - An OLDER server (no `items` in its answer) has one shared kind: the
|
|
30
|
+
* chips read as team and the kind choice is not offered, because the old
|
|
31
|
+
* server would ignore it and the person would believe a label was private
|
|
32
|
+
* when it is not.
|
|
33
|
+
*
|
|
11
34
|
* === etiket:t1 — THE CHIP IS A DOOR ==================================
|
|
12
35
|
* ⚠⚠ Owner, 2026-09-13: "taglediğim dosya klasör tag'ine gitmiyor." The tag
|
|
13
36
|
* VIEW existed and the panel listed every tag, but from a file there was no way
|
|
@@ -28,6 +51,9 @@
|
|
|
28
51
|
import { ref, watch, onMounted, computed } from 'vue';
|
|
29
52
|
import { useLocale } from '../composables/useLocale';
|
|
30
53
|
import type { LocaleCode } from '../types/ExplorerConfig';
|
|
54
|
+
import { announceTagsChanged, tagItemsOf, tagKey, type TagItem, type TagKind } from '../lib/tags';
|
|
55
|
+
import TagKindIcon from './TagKindIcon.vue';
|
|
56
|
+
import ChoiceButtons, { type ChoiceOption } from './ChoiceButtons.vue';
|
|
31
57
|
|
|
32
58
|
const props = defineProps<{
|
|
33
59
|
nodeId: number;
|
|
@@ -52,24 +78,50 @@ const props = defineProps<{
|
|
|
52
78
|
}>();
|
|
53
79
|
|
|
54
80
|
const emit = defineEmits<{
|
|
55
|
-
(
|
|
81
|
+
/** The names (the pre-v0.43 payload, kept for embedders), then the items. */
|
|
82
|
+
(e: 'change', tags: string[], items: TagItem[]): void;
|
|
56
83
|
(e: 'error', message: string): void;
|
|
57
|
-
/** etiket:t1 — "show me everything tagged this". The host owns navigation.
|
|
58
|
-
(
|
|
84
|
+
/** etiket:t1 — "show me everything tagged this". The host owns navigation.
|
|
85
|
+
* `kind` (v0.43) says WHICH of two same-named tags was clicked. */
|
|
86
|
+
(e: 'open', tag: string, kind: TagKind): void;
|
|
59
87
|
}>();
|
|
60
88
|
|
|
61
89
|
const { t } = useLocale(() => props.locale ?? 'en');
|
|
62
90
|
|
|
63
|
-
/** "Open tag: invoices" — the chip's title and its accessible
|
|
64
|
-
* so the two cannot say different things.
|
|
65
|
-
|
|
66
|
-
|
|
91
|
+
/** "Open tag: invoices · Personal tag …" — the chip's title and its accessible
|
|
92
|
+
* name, one value so the two cannot say different things. The kind sentence
|
|
93
|
+
* rides along, so a screen reader hears WHO sees the tag, not only its name. */
|
|
94
|
+
function openTitle(item: TagItem): string {
|
|
95
|
+
return `${t('tags.open', { tag: item.name })} · ${t(`tags.chip.${item.kind}`, { tag: item.name })}`;
|
|
67
96
|
}
|
|
68
97
|
|
|
69
|
-
const
|
|
98
|
+
const items = ref<TagItem[]>([]);
|
|
99
|
+
/** Whether TEAM tags may be added or removed here (server: `can_edit_team`). */
|
|
100
|
+
const canEditTeam = ref(true);
|
|
101
|
+
/** The server predates kinds: one shared kind, no choice to offer. */
|
|
102
|
+
const legacy = ref(false);
|
|
70
103
|
const loading = ref(false);
|
|
71
104
|
const adding = ref(false);
|
|
72
105
|
const newTag = ref('');
|
|
106
|
+
const newKind = ref<TagKind>('personal');
|
|
107
|
+
const addForm = ref<HTMLFormElement | null>(null);
|
|
108
|
+
|
|
109
|
+
/** Personal chips first, then team — the grouping the navigation panel uses,
|
|
110
|
+
* so the same file reads the same in every place. */
|
|
111
|
+
const shown = computed(() => [
|
|
112
|
+
...items.value.filter((i) => i.kind === 'personal'),
|
|
113
|
+
...items.value.filter((i) => i.kind === 'team'),
|
|
114
|
+
]);
|
|
115
|
+
|
|
116
|
+
const kindOptions = computed<ChoiceOption[]>(() => [
|
|
117
|
+
{ value: 'personal', label: t('tags.kind.personal'), help: t('tags.kind.personal_help') },
|
|
118
|
+
{
|
|
119
|
+
value: 'team',
|
|
120
|
+
label: t('tags.kind.team'),
|
|
121
|
+
help: canEditTeam.value ? t('tags.kind.team_help') : t('tags.kind.team_locked'),
|
|
122
|
+
disabled: !canEditTeam.value,
|
|
123
|
+
},
|
|
124
|
+
]);
|
|
73
125
|
|
|
74
126
|
const palette = computed(() => props.palette ?? [
|
|
75
127
|
'#ef4444', '#f59e0b', '#10b981', '#3b82f6',
|
|
@@ -83,10 +135,22 @@ function colorFor(tag: string): string {
|
|
|
83
135
|
return arr[Math.abs(h) % arr.length];
|
|
84
136
|
}
|
|
85
137
|
|
|
138
|
+
/** A team chip a viewer cannot take off draws no ×. */
|
|
139
|
+
function canRemove(item: TagItem): boolean {
|
|
140
|
+
return item.kind === 'personal' || canEditTeam.value;
|
|
141
|
+
}
|
|
142
|
+
|
|
86
143
|
async function buildHeaders(extra: Record<string, string> = {}): Promise<Record<string, string>> {
|
|
87
144
|
return { ...(await (props.authHeaders ?? (() => ({})))()), ...extra };
|
|
88
145
|
}
|
|
89
146
|
|
|
147
|
+
function absorb(body: unknown) {
|
|
148
|
+
const b = (body ?? {}) as { items?: unknown; can_edit_team?: unknown };
|
|
149
|
+
legacy.value = !Array.isArray(b.items);
|
|
150
|
+
items.value = tagItemsOf(body);
|
|
151
|
+
if (typeof b.can_edit_team === 'boolean') canEditTeam.value = b.can_edit_team;
|
|
152
|
+
}
|
|
153
|
+
|
|
90
154
|
async function load() {
|
|
91
155
|
loading.value = true;
|
|
92
156
|
try {
|
|
@@ -95,10 +159,7 @@ async function load() {
|
|
|
95
159
|
headers: await buildHeaders(),
|
|
96
160
|
credentials: props.authCredentials ?? 'same-origin',
|
|
97
161
|
});
|
|
98
|
-
if (res.ok)
|
|
99
|
-
const body = await res.json();
|
|
100
|
-
tags.value = Array.isArray(body.tags) ? body.tags : [];
|
|
101
|
-
}
|
|
162
|
+
if (res.ok) absorb(await res.json());
|
|
102
163
|
} catch (err) {
|
|
103
164
|
emit('error', err instanceof Error ? err.message : String(err));
|
|
104
165
|
} finally {
|
|
@@ -106,39 +167,82 @@ async function load() {
|
|
|
106
167
|
}
|
|
107
168
|
}
|
|
108
169
|
|
|
109
|
-
async function persist(next:
|
|
110
|
-
const previous = [...
|
|
111
|
-
|
|
170
|
+
async function persist(next: TagItem[]) {
|
|
171
|
+
const previous = [...items.value];
|
|
172
|
+
items.value = next;
|
|
112
173
|
try {
|
|
113
174
|
const base = props.apiBase ?? '';
|
|
175
|
+
// ⚠ An older server gets the old shape: it would ignore `items` and read
|
|
176
|
+
// the missing `tags` as "clear every tag".
|
|
177
|
+
const body = legacy.value
|
|
178
|
+
? { node_id: props.nodeId, tags: next.map((i) => i.name) }
|
|
179
|
+
: { node_id: props.nodeId, items: next };
|
|
114
180
|
const res = await fetch(`${base}/api/files/manager/tags`, {
|
|
115
181
|
method: 'POST',
|
|
116
182
|
headers: await buildHeaders({ 'Content-Type': 'application/json' }),
|
|
117
183
|
credentials: props.authCredentials ?? 'same-origin',
|
|
118
|
-
body: JSON.stringify(
|
|
184
|
+
body: JSON.stringify(body),
|
|
119
185
|
});
|
|
120
|
-
if (!res.ok) throw
|
|
121
|
-
|
|
186
|
+
if (!res.ok) throw requestFailure(res.status, await res.text().catch(() => ''), props.locale);
|
|
187
|
+
// The server's answer is the truth: it keeps the FIRST spelling of a name
|
|
188
|
+
// ("müşteri teklifi" typed on a second file lands as "Müşteri Teklifi").
|
|
189
|
+
const answer: unknown = await res.json().catch(() => null);
|
|
190
|
+
if (!legacy.value && answer && Array.isArray((answer as { items?: unknown }).items)) {
|
|
191
|
+
items.value = tagItemsOf(answer);
|
|
192
|
+
}
|
|
193
|
+
// ⚠ Announced, not only emitted: if the dialog was closed while the
|
|
194
|
+
// request was in flight this component is gone and Vue drops its emit —
|
|
195
|
+
// the panel would keep the list from before the save (lib/tags).
|
|
196
|
+
announceTagsChanged();
|
|
197
|
+
emit('change', items.value.map((i) => i.name), items.value);
|
|
122
198
|
} catch (err) {
|
|
123
|
-
|
|
199
|
+
items.value = previous;
|
|
124
200
|
emit('error', err instanceof Error ? err.message : String(err));
|
|
125
201
|
}
|
|
126
202
|
}
|
|
127
203
|
|
|
204
|
+
function closeAdd() {
|
|
205
|
+
newTag.value = '';
|
|
206
|
+
newKind.value = 'personal';
|
|
207
|
+
adding.value = false;
|
|
208
|
+
}
|
|
209
|
+
|
|
128
210
|
function add() {
|
|
129
211
|
const v = newTag.value.trim();
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
212
|
+
const kind: TagKind = legacy.value ? 'team' : newKind.value;
|
|
213
|
+
if (!v || items.value.some((i) => i.kind === kind && tagKey(i.name) === tagKey(v))) {
|
|
214
|
+
closeAdd();
|
|
133
215
|
return;
|
|
134
216
|
}
|
|
135
|
-
persist([...
|
|
136
|
-
|
|
137
|
-
|
|
217
|
+
void persist([...items.value, { name: v, kind }]);
|
|
218
|
+
closeAdd();
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
/**
|
|
222
|
+
* What leaving the name field does.
|
|
223
|
+
*
|
|
224
|
+
* ⚠⚠ Before kinds, leaving the field SAVED what was typed. With a kind choice
|
|
225
|
+
* beside the field that is a trap: the pointer's way to "Team" is a blur of
|
|
226
|
+
* the field, so the tag would be saved as PERSONAL on the way to the button —
|
|
227
|
+
* and Safari does not focus a clicked button at all, so there is no
|
|
228
|
+
* `relatedTarget` to recognise the move by. So with kinds, only Enter or the
|
|
229
|
+
* Add button saves; the choice and the button swallow `mousedown` (below) so
|
|
230
|
+
* a pointer never takes focus off the field; and leaving an EMPTY field closes
|
|
231
|
+
* the form, which is the tidy-up the old blur gave. An older server has no
|
|
232
|
+
* choice to lose, so it keeps the old save-on-leave.
|
|
233
|
+
*/
|
|
234
|
+
function onFieldBlur(e: FocusEvent) {
|
|
235
|
+
if (legacy.value) {
|
|
236
|
+
add();
|
|
237
|
+
return;
|
|
238
|
+
}
|
|
239
|
+
const to = e.relatedTarget as Node | null;
|
|
240
|
+
if (to && addForm.value?.contains(to)) return;
|
|
241
|
+
if (!newTag.value.trim()) closeAdd();
|
|
138
242
|
}
|
|
139
243
|
|
|
140
|
-
function remove(
|
|
141
|
-
persist(
|
|
244
|
+
function remove(item: TagItem) {
|
|
245
|
+
void persist(items.value.filter((i) => i !== item));
|
|
142
246
|
}
|
|
143
247
|
|
|
144
248
|
onMounted(load);
|
|
@@ -148,31 +252,53 @@ watch(() => props.nodeId, load);
|
|
|
148
252
|
<template>
|
|
149
253
|
<div class="filex-tag-picker" :class="{ 'is-loading': loading }">
|
|
150
254
|
<span
|
|
151
|
-
v-for="
|
|
152
|
-
:key="
|
|
255
|
+
v-for="item in shown"
|
|
256
|
+
:key="`${item.kind}:${item.name}`"
|
|
153
257
|
class="filex-tag"
|
|
154
|
-
:
|
|
258
|
+
:class="`filex-tag--${item.kind}`"
|
|
259
|
+
:data-tag-kind="item.kind"
|
|
260
|
+
:style="{ '--filex-tag-color': colorFor(item.name) }"
|
|
155
261
|
>
|
|
262
|
+
<TagKindIcon :kind="item.kind" />
|
|
156
263
|
<button
|
|
157
264
|
class="filex-tag-open"
|
|
158
265
|
type="button"
|
|
159
|
-
:title="openTitle(
|
|
160
|
-
:aria-label="openTitle(
|
|
161
|
-
:data-testid="`tag-open-${
|
|
162
|
-
@click="emit('open',
|
|
163
|
-
>{{
|
|
164
|
-
<button
|
|
266
|
+
:title="openTitle(item)"
|
|
267
|
+
:aria-label="openTitle(item)"
|
|
268
|
+
:data-testid="`tag-open-${item.name}`"
|
|
269
|
+
@click="emit('open', item.name, item.kind)"
|
|
270
|
+
>{{ item.name }}</button>
|
|
271
|
+
<button
|
|
272
|
+
v-if="canRemove(item)"
|
|
273
|
+
class="filex-tag-x"
|
|
274
|
+
type="button"
|
|
275
|
+
:aria-label="`${t('tags.remove')}: ${item.name}`"
|
|
276
|
+
@click="remove(item)"
|
|
277
|
+
>×</button>
|
|
165
278
|
</span>
|
|
166
279
|
|
|
167
|
-
<form v-if="adding" class="filex-tag-add" @submit.prevent="add">
|
|
280
|
+
<form v-if="adding" ref="addForm" class="filex-tag-add" @submit.prevent="add">
|
|
168
281
|
<input
|
|
169
282
|
v-model="newTag"
|
|
170
283
|
autofocus
|
|
284
|
+
maxlength="64"
|
|
171
285
|
:placeholder="t('tags.name')"
|
|
172
286
|
:aria-label="t('tags.name')"
|
|
173
|
-
@blur="
|
|
174
|
-
@keydown.escape="
|
|
287
|
+
@blur="onFieldBlur"
|
|
288
|
+
@keydown.escape="closeAdd"
|
|
175
289
|
/>
|
|
290
|
+
<!-- `mousedown.prevent`: a pointer on the choice or the button must not
|
|
291
|
+
take focus off the field (see onFieldBlur). -->
|
|
292
|
+
<div v-if="!legacy" class="filex-tag-kindwrap" @mousedown.prevent>
|
|
293
|
+
<ChoiceButtons
|
|
294
|
+
:model-value="newKind"
|
|
295
|
+
:options="kindOptions"
|
|
296
|
+
:aria-label="t('tags.kind.label')"
|
|
297
|
+
testid-prefix="tag-kind"
|
|
298
|
+
@update:model-value="(v: string | string[]) => (newKind = v === 'team' ? 'team' : 'personal')"
|
|
299
|
+
/>
|
|
300
|
+
</div>
|
|
301
|
+
<button class="filex-tag-add-submit" type="submit" @mousedown.prevent>{{ t('tags.add_submit') }}</button>
|
|
176
302
|
</form>
|
|
177
303
|
<button v-else class="filex-tag-add-btn" type="button" @click="adding = true">
|
|
178
304
|
{{ t('tags.add') }}
|
|
@@ -247,6 +373,12 @@ watch(() => props.nodeId, load);
|
|
|
247
373
|
font-size: 12px;
|
|
248
374
|
width: 110px;
|
|
249
375
|
}
|
|
376
|
+
.filex-tag-add > input {
|
|
377
|
+
width: auto;
|
|
378
|
+
min-width: 0;
|
|
379
|
+
grid-column: 1;
|
|
380
|
+
grid-row: 1;
|
|
381
|
+
}
|
|
250
382
|
.filex-tag-add-btn {
|
|
251
383
|
border: 1px dashed var(--filex-border, #d1d5db);
|
|
252
384
|
background: transparent;
|
|
@@ -265,4 +397,58 @@ watch(() => props.nodeId, load);
|
|
|
265
397
|
border-color: var(--filex-accent, var(--fe-primary, #2f6ceb));
|
|
266
398
|
color: var(--filex-accent, var(--fe-primary, #2f6ceb));
|
|
267
399
|
}
|
|
400
|
+
/* etiket:k2 — the kind rides inside the chip, before the name, in the chip's
|
|
401
|
+
own colour: the icon is the at-a-glance half, the title the spelled-out one.
|
|
402
|
+
A personal chip is OUTLINED and a team chip FILLED, so the two read apart
|
|
403
|
+
even for someone who cannot tell two 12px glyphs apart. */
|
|
404
|
+
.filex-tag .fe-tagkind {
|
|
405
|
+
flex: none;
|
|
406
|
+
opacity: 0.9;
|
|
407
|
+
}
|
|
408
|
+
.filex-tag--personal {
|
|
409
|
+
background: transparent;
|
|
410
|
+
/* The page's own text colour, not the tag colour: an amber outline is
|
|
411
|
+
legible, amber letters on white are not. */
|
|
412
|
+
color: var(--fe-text, inherit);
|
|
413
|
+
box-shadow: inset 0 0 0 1.5px var(--filex-tag-color);
|
|
414
|
+
}
|
|
415
|
+
.filex-tag--personal .fe-tagkind {
|
|
416
|
+
color: var(--filex-tag-color);
|
|
417
|
+
opacity: 1;
|
|
418
|
+
}
|
|
419
|
+
.filex-tag--personal .filex-tag-x {
|
|
420
|
+
color: inherit;
|
|
421
|
+
opacity: 0.7;
|
|
422
|
+
}
|
|
423
|
+
/* The add form: the name and Add on one line, the kind choice under them at
|
|
424
|
+
full width with its two answers side by side, so "who sees this" reads as
|
|
425
|
+
one question with two answers rather than two stray boxes. */
|
|
426
|
+
.filex-tag-add {
|
|
427
|
+
display: grid;
|
|
428
|
+
grid-template-columns: minmax(0, 1fr) auto;
|
|
429
|
+
align-items: start;
|
|
430
|
+
gap: 6px;
|
|
431
|
+
flex-basis: 100%;
|
|
432
|
+
}
|
|
433
|
+
.filex-tag-add .filex-tag-add-submit {
|
|
434
|
+
grid-column: 2;
|
|
435
|
+
grid-row: 1;
|
|
436
|
+
}
|
|
437
|
+
.filex-tag-kindwrap {
|
|
438
|
+
grid-column: 1 / -1;
|
|
439
|
+
grid-row: 2;
|
|
440
|
+
}
|
|
441
|
+
.filex-tag-kindwrap .fe-choice {
|
|
442
|
+
display: grid;
|
|
443
|
+
grid-template-columns: repeat(2, minmax(0, 1fr));
|
|
444
|
+
}
|
|
445
|
+
.filex-tag-add-submit {
|
|
446
|
+
border: 1px solid var(--filex-border, #d1d5db);
|
|
447
|
+
background: transparent;
|
|
448
|
+
border-radius: 12px;
|
|
449
|
+
padding: 2px 12px;
|
|
450
|
+
font-size: 12px;
|
|
451
|
+
color: inherit;
|
|
452
|
+
cursor: pointer;
|
|
453
|
+
}
|
|
268
454
|
</style>
|
|
@@ -25,7 +25,7 @@
|
|
|
25
25
|
*/
|
|
26
26
|
import type { LocaleCode } from '../types/ExplorerConfig';
|
|
27
27
|
import { useLocale } from '../composables/useLocale';
|
|
28
|
-
import {
|
|
28
|
+
import { allThemes, themeName, type ThemeDef } from '../lib/themes';
|
|
29
29
|
|
|
30
30
|
const props = defineProps<{
|
|
31
31
|
locale: LocaleCode;
|
|
@@ -52,7 +52,7 @@ function cardVars(th: ThemeDef): Record<string, string> {
|
|
|
52
52
|
<p v-if="hint !== false" class="fe-themes__hint">{{ t('theme.hint') }}</p>
|
|
53
53
|
<div class="fe-themes" role="listbox" :aria-label="t('theme.title')">
|
|
54
54
|
<button
|
|
55
|
-
v-for="th in
|
|
55
|
+
v-for="th in allThemes()"
|
|
56
56
|
:key="th.id"
|
|
57
57
|
type="button"
|
|
58
58
|
class="fe-themecard"
|
|
@@ -83,7 +83,7 @@ function cardVars(th: ThemeDef): Record<string, string> {
|
|
|
83
83
|
</span>
|
|
84
84
|
</span>
|
|
85
85
|
<span class="fe-themecard__name">
|
|
86
|
-
{{
|
|
86
|
+
{{ themeName(th, t) }}
|
|
87
87
|
<span v-if="th.id === current" class="fe-themecard__check" :title="t('theme.selected')">✓</span>
|
|
88
88
|
</span>
|
|
89
89
|
</button>
|
|
@@ -121,7 +121,7 @@ function cardVars(th: ThemeDef): Record<string, string> {
|
|
|
121
121
|
background: var(--fe-bg);
|
|
122
122
|
cursor: pointer;
|
|
123
123
|
overflow: hidden;
|
|
124
|
-
text-align:
|
|
124
|
+
text-align: start;
|
|
125
125
|
transition: transform 0.12s ease, box-shadow 0.12s ease;
|
|
126
126
|
font: inherit;
|
|
127
127
|
}
|
|
@@ -141,14 +141,33 @@ const ALIASES: Record<string, string> = {
|
|
|
141
141
|
'Asia/Kolkata': 'india calcutta ist',
|
|
142
142
|
};
|
|
143
143
|
|
|
144
|
-
/**
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
144
|
+
/**
|
|
145
|
+
* A zone's offset from UTC in minutes, or null when the engine will not say.
|
|
146
|
+
*
|
|
147
|
+
* ⚠⚠ Read in ONE fixed shape — `en-US` — and parsed as a NUMBER. The offset
|
|
148
|
+
* the row DISPLAYS is in the interface language, and that is not "GMT…"
|
|
149
|
+
* everywhere: Arabic prints "غرينتش+3", other languages print their own word
|
|
150
|
+
* or their own digits. The search aliases used to be parsed out of that
|
|
151
|
+
* localised text with a `GMT` pattern, so under Arabic not one zone could be
|
|
152
|
+
* found by typing "+3" or "UTC+3" (translator finding, v0.43.0).
|
|
153
|
+
*/
|
|
154
|
+
function offsetMinutes(zone: string): number | null {
|
|
155
|
+
const m = /^GMT(?:([+-])(\d{1,2})(?::(\d{2}))?)?$/.exec(namePart(zone, 'en-US', 'shortOffset').trim());
|
|
156
|
+
if (!m) return null;
|
|
157
|
+
if (!m[1]) return 0;
|
|
158
|
+
const minutes = Number(m[2]) * 60 + Number(m[3] ?? 0);
|
|
159
|
+
return m[1] === '-' ? -minutes : minutes;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/** Every way a person might type an offset — "+3", "+03", "+03:00", "gmt+3", "utc+0300". */
|
|
163
|
+
function offsetAliases(minutes: number | null): string {
|
|
164
|
+
if (minutes === null) return '';
|
|
165
|
+
if (minutes === 0) return 'gmt utc';
|
|
166
|
+
const sign = minutes < 0 ? '-' : '+';
|
|
167
|
+
const abs = Math.abs(minutes);
|
|
168
|
+
const h = String(Math.floor(abs / 60));
|
|
150
169
|
const hh = h.padStart(2, '0');
|
|
151
|
-
const mm =
|
|
170
|
+
const mm = String(abs % 60).padStart(2, '0');
|
|
152
171
|
const forms = [`${sign}${h}`, `${sign}${hh}`, `${sign}${hh}:${mm}`, `${sign}${hh}${mm}`];
|
|
153
172
|
return [...forms.map((f) => `gmt${f}`), ...forms.map((f) => `utc${f}`), ...forms].join(' ');
|
|
154
173
|
}
|
|
@@ -226,7 +245,7 @@ function buildIndex(): TzRow[] {
|
|
|
226
245
|
const genericLocal = namePart(zone, loc, 'longGeneric');
|
|
227
246
|
const genericEn = loc === 'en-US' ? '' : namePart(zone, 'en-US', 'longGeneric');
|
|
228
247
|
const haystack = fold(
|
|
229
|
-
[zone, city, region, genericLocal, genericEn, ALIASES[zone] ?? '', offsetAliases(
|
|
248
|
+
[zone, city, region, genericLocal, genericEn, ALIASES[zone] ?? '', offset, offsetAliases(offsetMinutes(zone))]
|
|
230
249
|
.filter(Boolean)
|
|
231
250
|
.join(' '),
|
|
232
251
|
);
|
|
@@ -605,8 +624,8 @@ function onKeydown(ev: KeyboardEvent): void {
|
|
|
605
624
|
.fe-tzpick__list {
|
|
606
625
|
position: absolute;
|
|
607
626
|
top: calc(100% + 4px);
|
|
608
|
-
|
|
609
|
-
|
|
627
|
+
inset-inline-start: 0;
|
|
628
|
+
inset-inline-end: 0;
|
|
610
629
|
z-index: 5;
|
|
611
630
|
box-sizing: border-box;
|
|
612
631
|
margin: 0;
|