@brftech/filex-core 0.39.1 → 0.41.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 +32 -25
- package/dist/ArchiveViewer-BNqukFg8.js +124 -0
- package/dist/ArchiveViewer-BNqukFg8.js.map +1 -0
- package/dist/CsvViewer-ii_-MgmG.js +141 -0
- package/dist/CsvViewer-ii_-MgmG.js.map +1 -0
- package/dist/{DrawioViewer-BNALOB04.js → DrawioViewer-B2tuu4rX.js} +48 -42
- package/dist/DrawioViewer-B2tuu4rX.js.map +1 -0
- package/dist/EpubViewer-p4B8iiWb.js +163 -0
- package/dist/EpubViewer-p4B8iiWb.js.map +1 -0
- package/dist/IpynbViewer-D_qYRJhu.js +184 -0
- package/dist/IpynbViewer-D_qYRJhu.js.map +1 -0
- package/dist/MermaidViewer-3ZkfNM8v.js +137 -0
- package/dist/MermaidViewer-3ZkfNM8v.js.map +1 -0
- package/dist/PsdViewer-CuV-OPSd.js +122 -0
- package/dist/PsdViewer-CuV-OPSd.js.map +1 -0
- package/dist/TiffViewer-BLeoF4Be.js +142 -0
- package/dist/TiffViewer-BLeoF4Be.js.map +1 -0
- package/dist/Viewer3D-DHhhU76E.js +75 -0
- package/dist/Viewer3D-DHhhU76E.js.map +1 -0
- package/dist/filex-core.js +217 -20077
- package/dist/filex-core.js.map +1 -1
- package/dist/filex-core.umd.cjs +61 -62
- package/dist/filex-core.umd.cjs.map +1 -1
- package/dist/index-BraG7Cz4.js +26664 -0
- package/dist/index-BraG7Cz4.js.map +1 -0
- package/dist/index.d.ts +4276 -851
- package/dist/style.css +1 -1
- package/package.json +1 -1
- package/src/FileExplorer.vue +2170 -492
- package/src/components/AdvancedSearch.vue +870 -0
- package/src/components/Breadcrumb.vue +191 -19
- package/src/components/CommandPalette.vue +66 -22
- package/src/components/ConnectionsPanel.vue +6 -2
- package/src/components/ContextMenu.vue +57 -2
- package/src/components/EncryptedFolderModal.vue +31 -25
- package/src/components/FilePane.vue +939 -0
- package/src/components/FilterBar.vue +568 -72
- package/src/components/GalleryView.vue +162 -30
- package/src/components/GridView.vue +322 -58
- package/src/components/HomeView.vue +331 -0
- package/src/components/InspectorPanel.vue +296 -53
- package/src/components/ListView.vue +1158 -167
- package/src/components/NFSExportsPanel.vue +7 -4
- package/src/components/OnboardingTour.vue +156 -33
- package/src/components/QuickLook.vue +34 -5
- package/src/components/RecentlyOpened.vue +17 -2
- package/src/components/S3KeysPanel.vue +7 -4
- package/src/components/SSHKeysPanel.vue +7 -4
- package/src/components/ShortcutSettings.vue +43 -2
- package/src/components/ShortcutsHelp.vue +4 -1
- package/src/components/SideNav.vue +206 -140
- package/src/components/StorageFields.vue +19 -1
- package/src/components/TabBar.vue +87 -1
- package/src/components/TagPicker.vue +77 -6
- package/src/components/ThemeGallery.vue +13 -198
- package/src/components/ThemePalette.vue +244 -0
- package/src/components/TimeZoneDialog.vue +127 -0
- package/src/components/TimeZonePicker.vue +696 -0
- package/src/components/TokensPanel.vue +16 -4
- package/src/components/Toolbar.vue +965 -469
- package/src/components/ViewSwitcher.vue +9 -4
- package/src/composables/useConnections.ts +35 -4
- package/src/composables/useExplorerTimeZone.ts +85 -0
- package/src/composables/useFileApi.ts +53 -4
- package/src/composables/useKeyboardShortcuts.ts +122 -0
- package/src/composables/useLocale.ts +365 -25
- package/src/index.ts +182 -3
- package/src/lib/actionIcons.ts +330 -0
- package/src/lib/advSearch.ts +162 -0
- package/src/lib/dateGroups.ts +197 -0
- package/src/lib/destinationTree.ts +235 -0
- package/src/lib/downloadSelection.ts +154 -0
- package/src/lib/fileFilters.ts +319 -12
- package/src/lib/fileIcons.ts +299 -5
- package/src/lib/filePreview.ts +605 -0
- package/src/lib/listing.ts +72 -0
- package/src/lib/shareTtl.ts +11 -1
- package/src/lib/sortOrder.ts +459 -0
- package/src/lib/themes.ts +152 -23
- package/src/lib/timezone.ts +456 -0
- package/src/lib/uiProfile.ts +82 -0
- package/src/lib/viewPrefs.ts +1036 -0
- package/src/locales/en.ts +432 -25
- package/src/locales/tr.ts +425 -25
- package/src/modals/ConvertModal.vue +23 -3
- package/src/modals/DestinationPickerModal.vue +341 -0
- package/src/modals/Modal.vue +18 -4
- package/src/modals/NewDocumentModal.vue +525 -0
- package/src/modals/NewFolderModal.vue +15 -9
- package/src/modals/PermissionsModal.vue +543 -473
- package/src/modals/PreviewModal.vue +516 -156
- package/src/styles/base.css +5435 -8
- package/src/styles/variables.css +140 -60
- package/src/types/Connections.ts +9 -0
- package/src/types/ExplorerConfig.ts +144 -39
- package/src/types/FileNode.ts +28 -0
- package/src/viewers/ArchiveViewer.vue +46 -8
- package/src/viewers/CsvViewer.vue +19 -2
- package/src/viewers/DrawioViewer.vue +12 -1
- package/src/viewers/EpubViewer.vue +50 -6
- package/src/viewers/IpynbViewer.vue +19 -2
- package/src/viewers/MermaidViewer.vue +20 -3
- package/src/viewers/PdfViewer.vue +24 -4
- package/src/viewers/PsdViewer.vue +28 -4
- package/src/viewers/TiffViewer.vue +19 -2
- package/src/viewers/Viewer3D.vue +36 -2
- package/dist/ArchiveViewer-Cdfv9LFC.js +0 -99
- package/dist/ArchiveViewer-Cdfv9LFC.js.map +0 -1
- package/dist/CsvViewer-CqWeV8VO.js +0 -131
- package/dist/CsvViewer-CqWeV8VO.js.map +0 -1
- package/dist/DrawioViewer-BNALOB04.js.map +0 -1
- package/dist/EpubViewer-BPmlImG7.js +0 -145
- package/dist/EpubViewer-BPmlImG7.js.map +0 -1
- package/dist/IpynbViewer-CDlQmuCV.js +0 -175
- package/dist/IpynbViewer-CDlQmuCV.js.map +0 -1
- package/dist/MermaidViewer-sfZWdR-d.js +0 -127
- package/dist/MermaidViewer-sfZWdR-d.js.map +0 -1
- package/dist/PsdViewer-DwuySZsb.js +0 -112
- package/dist/PsdViewer-DwuySZsb.js.map +0 -1
- package/dist/TiffViewer-BDQ3d3CQ.js +0 -132
- package/dist/TiffViewer-BDQ3d3CQ.js.map +0 -1
- package/dist/Viewer3D-CJJJgkuv.js +0 -60
- package/dist/Viewer3D-CJJJgkuv.js.map +0 -1
- package/src/components/SecondaryPane.vue +0 -447
- package/src/modals/ShareModal.vue +0 -139
|
@@ -35,6 +35,35 @@ const props = defineProps<{
|
|
|
35
35
|
splitEnabled?: boolean;
|
|
36
36
|
/** true → the split toggle renders pressed (active tab has a pane). */
|
|
37
37
|
splitActive?: boolean;
|
|
38
|
+
/**
|
|
39
|
+
* gorunum:v5-panerow — the details (inspector) toggle lives in this row too.
|
|
40
|
+
*
|
|
41
|
+
* ⚠ Opt-in, and absent means NOT DRAWN rather than "closed": until the host
|
|
42
|
+
* moves its own copy out of the breadcrumb row there would otherwise be two
|
|
43
|
+
* of the same button on screen, which is the duplicate this wave removes.
|
|
44
|
+
*/
|
|
45
|
+
inspectorEnabled?: boolean;
|
|
46
|
+
/** true → the details toggle renders pressed (the panel is open). */
|
|
47
|
+
inspectorOpen?: boolean;
|
|
48
|
+
/**
|
|
49
|
+
* gorunum:v5-panerow — hide the tabs themselves (and "+"), keeping the row.
|
|
50
|
+
*
|
|
51
|
+
* ⚠ True is not "no tabs exist", it is "this deployment does not offer
|
|
52
|
+
* them": the `simple` profile drops tabs deliberately (#14 named them as
|
|
53
|
+
* power-user chrome) but still needs this row, because the details toggle
|
|
54
|
+
* lives in it. Without the flag the row could only be all-or-nothing, and
|
|
55
|
+
* the ⓘ would have to keep a second home in the breadcrumb row for that one
|
|
56
|
+
* profile — a control with two homes is how the two drift.
|
|
57
|
+
*
|
|
58
|
+
* ⚠⚠ Phrased as HIDE, not SHOW, and that is not taste. A prop declared
|
|
59
|
+
* `showTabs?: boolean` compiles to a runtime Boolean prop, and Vue casts an
|
|
60
|
+
* ABSENT Boolean prop to `false` — not to `undefined`. So `showTabs !== false`
|
|
61
|
+
* read false for every host that passed nothing, and the strip rendered with
|
|
62
|
+
* no tabs and no "+" at all. Measured in a real browser: `.fe-tabs__tab`
|
|
63
|
+
* count went from 1 to 0 the moment the prop was added, in both themes at
|
|
64
|
+
* both widths. Negative default = safe default.
|
|
65
|
+
*/
|
|
66
|
+
hideTabs?: boolean;
|
|
38
67
|
}>();
|
|
39
68
|
|
|
40
69
|
const emit = defineEmits<{
|
|
@@ -43,6 +72,7 @@ const emit = defineEmits<{
|
|
|
43
72
|
(e: 'new'): void;
|
|
44
73
|
(e: 'reorder', from: number, to: number): void;
|
|
45
74
|
(e: 'toggle-split'): void;
|
|
75
|
+
(e: 'toggle-inspector'): void /* gorunum:v5-panerow */;
|
|
46
76
|
}>();
|
|
47
77
|
|
|
48
78
|
const { t } = useLocale(() => props.locale);
|
|
@@ -97,13 +127,17 @@ function onAux(id: string, ev: MouseEvent) {
|
|
|
97
127
|
if (ev.button !== 1) return;
|
|
98
128
|
ev.preventDefault();
|
|
99
129
|
ev.stopPropagation();
|
|
130
|
+
// ⚠ The same guard the × has. `useTabs.closeTab()` refuses the last tab, so
|
|
131
|
+
// middle-clicking it was already a no-op; leaving it live while the × is
|
|
132
|
+
// hidden would be two doors to one verb disagreeing about whether it exists.
|
|
133
|
+
if (props.tabs.length <= 1) return;
|
|
100
134
|
emit('close', id);
|
|
101
135
|
}
|
|
102
136
|
</script>
|
|
103
137
|
|
|
104
138
|
<template>
|
|
105
139
|
<div class="fe-tabs" role="tablist" :aria-label="t('tabs.strip')">
|
|
106
|
-
<div class="fe-tabs__scroll">
|
|
140
|
+
<div v-if="!hideTabs" class="fe-tabs__scroll">
|
|
107
141
|
<div
|
|
108
142
|
v-for="(tab, i) in tabs"
|
|
109
143
|
:key="tab.id"
|
|
@@ -129,7 +163,14 @@ function onAux(id: string, ev: MouseEvent) {
|
|
|
129
163
|
>
|
|
130
164
|
<span v-if="tab.split" class="fe-tabs__splitdot" aria-hidden="true"></span>
|
|
131
165
|
<span class="fe-tabs__label">{{ tab.label }}</span>
|
|
166
|
+
<!-- ⚠ Not drawn when this is the only tab. Owner's decision,
|
|
167
|
+
2026-09-13, verbatim: *"tek kalan tab'de x gözükmemeli, onu da
|
|
168
|
+
kaldırırsın."* It is not a style choice — `useTabs.closeTab()`
|
|
169
|
+
opens with `if (tabs.length <= 1) return null`, so at one tab the
|
|
170
|
+
× has always been a control that cannot do anything. Drawing it
|
|
171
|
+
was inviting a dead click. -->
|
|
132
172
|
<button
|
|
173
|
+
v-if="tabs.length > 1"
|
|
133
174
|
type="button"
|
|
134
175
|
class="fe-tabs__close"
|
|
135
176
|
:aria-label="t('tabs.close')"
|
|
@@ -143,12 +184,23 @@ function onAux(id: string, ev: MouseEvent) {
|
|
|
143
184
|
the only way back to it was scrolling a strip most people do not know
|
|
144
185
|
scrolls. Pinned here it is always where it was. -->
|
|
145
186
|
<button
|
|
187
|
+
v-if="!hideTabs"
|
|
146
188
|
type="button"
|
|
147
189
|
class="fe-tabs__new"
|
|
148
190
|
:aria-label="t('tabs.new')"
|
|
149
191
|
:title="t('tabs.new')"
|
|
150
192
|
@click="emit('new')"
|
|
151
193
|
>+</button>
|
|
194
|
+
<!-- gorunum:v5-panerow — back in THIS row, where it was.
|
|
195
|
+
⚠ It spent one round teleported up into the breadcrumb row beside the
|
|
196
|
+
details toggle. The reasoning was sound (that row is always drawn,
|
|
197
|
+
this strip is `v-if`'d) and it was approved, but the owner looked at
|
|
198
|
+
the result and rejected it: *"kanka split pane ve info butonunu alt
|
|
199
|
+
kısımda bırak"*. He decides. The reachability problem the teleport was
|
|
200
|
+
solving is real and is answered properly in the handover diff instead:
|
|
201
|
+
this row becomes the WINDOW's row — tabs, +, split and the details
|
|
202
|
+
toggle — and is drawn whenever any of the four has something to do,
|
|
203
|
+
rather than only when a second tab exists. -->
|
|
152
204
|
<button
|
|
153
205
|
v-if="splitEnabled !== false"
|
|
154
206
|
type="button"
|
|
@@ -157,6 +209,7 @@ function onAux(id: string, ev: MouseEvent) {
|
|
|
157
209
|
:aria-label="splitActive ? t('tabs.split_off') : t('tabs.split')"
|
|
158
210
|
:title="splitActive ? t('tabs.split_off') : t('tabs.split')"
|
|
159
211
|
:aria-pressed="splitActive ? 'true' : 'false'"
|
|
212
|
+
data-testid="tabs-split"
|
|
160
213
|
@click="emit('toggle-split')"
|
|
161
214
|
>
|
|
162
215
|
<svg
|
|
@@ -174,5 +227,38 @@ function onAux(id: string, ev: MouseEvent) {
|
|
|
174
227
|
<path d="M12 4v16" />
|
|
175
228
|
</svg>
|
|
176
229
|
</button>
|
|
230
|
+
|
|
231
|
+
<!-- gorunum:v5-panerow — the details toggle's home in this row.
|
|
232
|
+
⚠ Rendered ONLY when the host passes `inspectorEnabled`, which nothing
|
|
233
|
+
does yet: until the FileExplorer diff in the handover is applied, the
|
|
234
|
+
ⓘ is still drawn in the breadcrumb row and this must stay silent, or
|
|
235
|
+
there would be two of it. When the diff lands, `infoPanelToggle` is
|
|
236
|
+
passed here and the subhead's copy is deleted in the same change. -->
|
|
237
|
+
<button
|
|
238
|
+
v-if="inspectorEnabled"
|
|
239
|
+
type="button"
|
|
240
|
+
class="fe-tabs__inspector"
|
|
241
|
+
:class="{ 'is-active': inspectorOpen }"
|
|
242
|
+
:aria-pressed="inspectorOpen ? 'true' : 'false'"
|
|
243
|
+
:title="t('toolbar.inspector')"
|
|
244
|
+
:aria-label="t('toolbar.inspector')"
|
|
245
|
+
data-testid="tabs-inspector"
|
|
246
|
+
@click="emit('toggle-inspector')"
|
|
247
|
+
>
|
|
248
|
+
<svg
|
|
249
|
+
class="fe-ficon"
|
|
250
|
+
viewBox="0 0 24 24"
|
|
251
|
+
fill="none"
|
|
252
|
+
stroke="currentColor"
|
|
253
|
+
stroke-width="1.8"
|
|
254
|
+
stroke-linecap="round"
|
|
255
|
+
aria-hidden="true"
|
|
256
|
+
focusable="false"
|
|
257
|
+
>
|
|
258
|
+
<circle cx="12" cy="12" r="9" />
|
|
259
|
+
<path d="M12 11v5" />
|
|
260
|
+
<circle cx="12" cy="7.6" r="1" fill="currentColor" stroke="none" />
|
|
261
|
+
</svg>
|
|
262
|
+
</button>
|
|
177
263
|
</div>
|
|
178
264
|
</template>
|
|
@@ -7,11 +7,39 @@
|
|
|
7
7
|
*
|
|
8
8
|
* Tag colors are deterministic: hash(name) → palette index. Consumers
|
|
9
9
|
* can override with the `palette` prop.
|
|
10
|
+
*
|
|
11
|
+
* === etiket:t1 — THE CHIP IS A DOOR ==================================
|
|
12
|
+
* ⚠⚠ Owner, 2026-09-13: "taglediğim dosya klasör tag'ine gitmiyor." The tag
|
|
13
|
+
* VIEW existed and the panel listed every tag, but from a file there was no way
|
|
14
|
+
* into the one it carries: the chip was a `<span>`, so the only route from
|
|
15
|
+
* "this file is tagged invoices" to "what else is tagged invoices" was to read
|
|
16
|
+
* the word, find it again in the panel and click it there.
|
|
17
|
+
*
|
|
18
|
+
* ⚠ The name is the button and the × stays its own. Two targets in one chip,
|
|
19
|
+
* neither of which may fire the other's action — a mis-hit here either loses a
|
|
20
|
+
* tag or navigates away from the file you were reading about.
|
|
21
|
+
*
|
|
22
|
+
* ⚠ It navigates from BOTH mounts, the details panel's and the modal the
|
|
23
|
+
* right-click menu opens. One component, one behaviour: a chip that led
|
|
24
|
+
* somewhere in one place and was inert three inches away in another would be
|
|
25
|
+
* the surface-specific split this package exists to avoid. The host closes the
|
|
26
|
+
* modal on the way (it opened it).
|
|
10
27
|
*/
|
|
11
28
|
import { ref, watch, onMounted, computed } from 'vue';
|
|
29
|
+
import { useLocale } from '../composables/useLocale';
|
|
30
|
+
import type { LocaleCode } from '../types/ExplorerConfig';
|
|
12
31
|
|
|
13
32
|
const props = defineProps<{
|
|
14
33
|
nodeId: number;
|
|
34
|
+
/**
|
|
35
|
+
* ⚠ Added 2026-09-13 with the chip's new "open this tag" affordance, and it
|
|
36
|
+
* pulls three OLDER strings out of English with it: this component printed
|
|
37
|
+
* "+ Add tag", "tag name" and "Remove tag" literally, so a Turkish user
|
|
38
|
+
* reading "Etiketler" as the section heading got three English controls under
|
|
39
|
+
* it. Optional, defaulting to the catalogue's own default, so an embedder
|
|
40
|
+
* that never passed one is unaffected.
|
|
41
|
+
*/
|
|
42
|
+
locale?: LocaleCode;
|
|
15
43
|
apiBase?: string;
|
|
16
44
|
authHeaders?: () => Record<string, string> | Promise<Record<string, string>>;
|
|
17
45
|
/** Credentials mode, from the explorer's auth kind. ⚠ Defaults to
|
|
@@ -26,8 +54,18 @@ const props = defineProps<{
|
|
|
26
54
|
const emit = defineEmits<{
|
|
27
55
|
(e: 'change', tags: string[]): void;
|
|
28
56
|
(e: 'error', message: string): void;
|
|
57
|
+
/** etiket:t1 — "show me everything tagged this". The host owns navigation. */
|
|
58
|
+
(e: 'open', tag: string): void;
|
|
29
59
|
}>();
|
|
30
60
|
|
|
61
|
+
const { t } = useLocale(() => props.locale ?? 'en');
|
|
62
|
+
|
|
63
|
+
/** "Open tag: invoices" — the chip's title and its accessible name, one value
|
|
64
|
+
* so the two cannot say different things. */
|
|
65
|
+
function openTitle(tag: string): string {
|
|
66
|
+
return t('tags.open', { tag });
|
|
67
|
+
}
|
|
68
|
+
|
|
31
69
|
const tags = ref<string[]>([]);
|
|
32
70
|
const loading = ref(false);
|
|
33
71
|
const adding = ref(false);
|
|
@@ -115,20 +153,30 @@ watch(() => props.nodeId, load);
|
|
|
115
153
|
class="filex-tag"
|
|
116
154
|
:style="{ '--filex-tag-color': colorFor(tag) }"
|
|
117
155
|
>
|
|
118
|
-
|
|
119
|
-
|
|
156
|
+
<button
|
|
157
|
+
class="filex-tag-open"
|
|
158
|
+
type="button"
|
|
159
|
+
:title="openTitle(tag)"
|
|
160
|
+
:aria-label="openTitle(tag)"
|
|
161
|
+
:data-testid="`tag-open-${tag}`"
|
|
162
|
+
@click="emit('open', tag)"
|
|
163
|
+
>{{ tag }}</button>
|
|
164
|
+
<button class="filex-tag-x" type="button" @click="remove(tag)" :aria-label="t('tags.remove')">×</button>
|
|
120
165
|
</span>
|
|
121
166
|
|
|
122
167
|
<form v-if="adding" class="filex-tag-add" @submit.prevent="add">
|
|
123
168
|
<input
|
|
124
169
|
v-model="newTag"
|
|
125
170
|
autofocus
|
|
126
|
-
placeholder="
|
|
171
|
+
:placeholder="t('tags.name')"
|
|
172
|
+
:aria-label="t('tags.name')"
|
|
127
173
|
@blur="add"
|
|
128
174
|
@keydown.escape="adding = false; newTag = ''"
|
|
129
175
|
/>
|
|
130
176
|
</form>
|
|
131
|
-
<button v-else class="filex-tag-add-btn" type="button" @click="adding = true"
|
|
177
|
+
<button v-else class="filex-tag-add-btn" type="button" @click="adding = true">
|
|
178
|
+
{{ t('tags.add') }}
|
|
179
|
+
</button>
|
|
132
180
|
</div>
|
|
133
181
|
</template>
|
|
134
182
|
|
|
@@ -162,6 +210,24 @@ watch(() => props.nodeId, load);
|
|
|
162
210
|
color: #fff;
|
|
163
211
|
background: var(--filex-tag-color);
|
|
164
212
|
}
|
|
213
|
+
/* etiket:t1 — the tag's NAME is the door to its view. A bare button: it
|
|
214
|
+
inherits the chip's own colour and type so the chip still reads as one
|
|
215
|
+
object, and the underline appears only under the pointer, where the chip has
|
|
216
|
+
already said it is interactive by changing the cursor. */
|
|
217
|
+
.filex-tag-open {
|
|
218
|
+
background: transparent;
|
|
219
|
+
border: 0;
|
|
220
|
+
padding: 0;
|
|
221
|
+
margin: 0;
|
|
222
|
+
color: inherit;
|
|
223
|
+
font: inherit;
|
|
224
|
+
line-height: inherit;
|
|
225
|
+
cursor: pointer;
|
|
226
|
+
}
|
|
227
|
+
.filex-tag-open:hover,
|
|
228
|
+
.filex-tag-open:focus-visible {
|
|
229
|
+
text-decoration: underline;
|
|
230
|
+
}
|
|
165
231
|
.filex-tag-x {
|
|
166
232
|
background: transparent;
|
|
167
233
|
border: none;
|
|
@@ -190,8 +256,13 @@ watch(() => props.nodeId, load);
|
|
|
190
256
|
color: var(--filex-text-muted, #6b7280);
|
|
191
257
|
cursor: pointer;
|
|
192
258
|
}
|
|
259
|
+
/* gorunum:v1 — a host override still wins, but the fallback chain now ends at
|
|
260
|
+
the product blue instead of the old indigo, and it asks the palette first:
|
|
261
|
+
in a themed context this hover follows --fe-primary (and so goes light blue
|
|
262
|
+
in dark mode), and only a context with no stylesheet at all — the very case
|
|
263
|
+
where a fallback is the only thing painting — lands on the literal. */
|
|
193
264
|
.filex-tag-add-btn:hover {
|
|
194
|
-
border-color: var(--filex-accent, #
|
|
195
|
-
color: var(--filex-accent, #
|
|
265
|
+
border-color: var(--filex-accent, var(--fe-primary, #2f6ceb));
|
|
266
|
+
color: var(--filex-accent, var(--fe-primary, #2f6ceb));
|
|
196
267
|
}
|
|
197
268
|
</style>
|
|
@@ -10,12 +10,18 @@
|
|
|
10
10
|
*
|
|
11
11
|
* Presentational only — selection state + persistence live in
|
|
12
12
|
* lib/themes.ts (shared across instances); the parent forwards `select`.
|
|
13
|
+
*
|
|
14
|
+
* ⚠ The cards themselves live in `ThemePalette.vue` and are mounted, not
|
|
15
|
+
* copied: the web app's user-settings modal shows the same grid inside a pane
|
|
16
|
+
* and cannot mount this component, because this one IS a modal. See that
|
|
17
|
+
* file's header.
|
|
13
18
|
*/
|
|
14
19
|
import { computed } from 'vue';
|
|
15
20
|
import type { LocaleCode, ThemeMode } from '../types/ExplorerConfig';
|
|
16
21
|
import { useLocale } from '../composables/useLocale';
|
|
17
|
-
import {
|
|
22
|
+
import { DEFAULT_THEME_ID, type ThemeModePref } from '../lib/themes';
|
|
18
23
|
import Modal from '../modals/Modal.vue';
|
|
24
|
+
import ThemePalette from './ThemePalette.vue';
|
|
19
25
|
|
|
20
26
|
const props = defineProps<{
|
|
21
27
|
open: boolean;
|
|
@@ -57,12 +63,6 @@ const MODES: { id: Exclude<ThemeModePref, 'host'>; labelKey: string }[] = [
|
|
|
57
63
|
|
|
58
64
|
const shownMode = computed(() => (props.mode === 'host' ? props.hostMode : props.mode));
|
|
59
65
|
|
|
60
|
-
/** Inline variables for one card — a self-contained mini `--fe-*` scope,
|
|
61
|
-
* so the preview markup can use the exact same tokens the real UI uses. */
|
|
62
|
-
function cardVars(th: ThemeDef): Record<string, string> {
|
|
63
|
-
return props.dark ? th.dark : th.light;
|
|
64
|
-
}
|
|
65
|
-
|
|
66
66
|
const DEFAULT_ID = DEFAULT_THEME_ID;
|
|
67
67
|
</script>
|
|
68
68
|
|
|
@@ -108,44 +108,12 @@ const DEFAULT_ID = DEFAULT_THEME_ID;
|
|
|
108
108
|
</button>
|
|
109
109
|
</div>
|
|
110
110
|
</div>
|
|
111
|
-
<
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
class="fe-themecard"
|
|
118
|
-
:class="{ 'is-selected': th.id === current }"
|
|
119
|
-
role="option"
|
|
120
|
-
:aria-selected="th.id === current"
|
|
121
|
-
:style="cardVars(th)"
|
|
122
|
-
@click="emit('select', th.id)"
|
|
123
|
-
>
|
|
124
|
-
<span class="fe-themecard__preview" aria-hidden="true">
|
|
125
|
-
<span class="fe-themecard__topbar">
|
|
126
|
-
<span class="fe-themecard__chip fe-themecard__chip--primary"></span>
|
|
127
|
-
<span class="fe-themecard__chip"></span>
|
|
128
|
-
<span class="fe-themecard__chip fe-themecard__chip--ghost"></span>
|
|
129
|
-
</span>
|
|
130
|
-
<span class="fe-themecard__row">
|
|
131
|
-
<span class="fe-themecard__dot"></span>
|
|
132
|
-
<span class="fe-themecard__line" style="width: 62%"></span>
|
|
133
|
-
</span>
|
|
134
|
-
<span class="fe-themecard__row fe-themecard__row--selected">
|
|
135
|
-
<span class="fe-themecard__dot fe-themecard__dot--primary"></span>
|
|
136
|
-
<span class="fe-themecard__line" style="width: 44%"></span>
|
|
137
|
-
</span>
|
|
138
|
-
<span class="fe-themecard__row">
|
|
139
|
-
<span class="fe-themecard__dot fe-themecard__dot--muted"></span>
|
|
140
|
-
<span class="fe-themecard__line fe-themecard__line--muted" style="width: 74%"></span>
|
|
141
|
-
</span>
|
|
142
|
-
</span>
|
|
143
|
-
<span class="fe-themecard__name">
|
|
144
|
-
{{ t(th.nameKey) }}
|
|
145
|
-
<span v-if="th.id === current" class="fe-themecard__check" :title="t('theme.selected')">✓</span>
|
|
146
|
-
</span>
|
|
147
|
-
</button>
|
|
148
|
-
</div>
|
|
111
|
+
<ThemePalette
|
|
112
|
+
:locale="locale"
|
|
113
|
+
:dark="dark"
|
|
114
|
+
:current="current"
|
|
115
|
+
@select="(id: string) => emit('select', id)"
|
|
116
|
+
/>
|
|
149
117
|
<template #actions>
|
|
150
118
|
<button
|
|
151
119
|
type="button"
|
|
@@ -161,156 +129,3 @@ const DEFAULT_ID = DEFAULT_THEME_ID;
|
|
|
161
129
|
</template>
|
|
162
130
|
</Modal>
|
|
163
131
|
</template>
|
|
164
|
-
|
|
165
|
-
<!-- Deliberately NOT scoped: the webcomponent injects the CORE package's
|
|
166
|
-
built style.css, whose data-v scope ids don't match the ids the
|
|
167
|
-
webcomponent's own compile assigns (verified: fe-themecard got
|
|
168
|
-
data-v-99a95ed7 in core vs data-v-5d4cc1fd in the wc bundle) — scoped
|
|
169
|
-
rules would silently not apply in embeds. Class names are fe-*
|
|
170
|
-
namespaced, so global is collision-safe (same approach as base.css). -->
|
|
171
|
-
<style>
|
|
172
|
-
.fe-themes__hint {
|
|
173
|
-
margin: 0 0 12px;
|
|
174
|
-
font-size: 12.5px;
|
|
175
|
-
color: var(--fe-text-muted);
|
|
176
|
-
}
|
|
177
|
-
|
|
178
|
-
.fe-themes {
|
|
179
|
-
display: grid;
|
|
180
|
-
grid-template-columns: repeat(auto-fill, minmax(150px, 1fr));
|
|
181
|
-
gap: 12px;
|
|
182
|
-
}
|
|
183
|
-
|
|
184
|
-
/* Each card is its own mini token scope (inline --fe-* vars from the theme
|
|
185
|
-
definition), so every color below refers to the CARD's theme — not the
|
|
186
|
-
active page theme. */
|
|
187
|
-
.fe-themecard {
|
|
188
|
-
display: flex;
|
|
189
|
-
flex-direction: column;
|
|
190
|
-
padding: 0;
|
|
191
|
-
border: 2px solid var(--fe-border-strong);
|
|
192
|
-
border-radius: var(--fe-radius, 8px);
|
|
193
|
-
background: var(--fe-bg);
|
|
194
|
-
cursor: pointer;
|
|
195
|
-
overflow: hidden;
|
|
196
|
-
text-align: left;
|
|
197
|
-
transition: transform 0.12s ease, box-shadow 0.12s ease;
|
|
198
|
-
font: inherit;
|
|
199
|
-
}
|
|
200
|
-
|
|
201
|
-
.fe-themecard:hover {
|
|
202
|
-
transform: translateY(-2px);
|
|
203
|
-
box-shadow: var(--fe-shadow-sm, 0 2px 6px rgba(15, 23, 42, 0.08));
|
|
204
|
-
}
|
|
205
|
-
|
|
206
|
-
.fe-themecard:focus-visible {
|
|
207
|
-
outline: 2px solid var(--fe-primary);
|
|
208
|
-
outline-offset: 2px;
|
|
209
|
-
}
|
|
210
|
-
|
|
211
|
-
.fe-themecard.is-selected {
|
|
212
|
-
border-color: var(--fe-primary);
|
|
213
|
-
box-shadow: 0 0 0 2px var(--fe-primary);
|
|
214
|
-
}
|
|
215
|
-
|
|
216
|
-
.fe-themecard__preview {
|
|
217
|
-
display: flex;
|
|
218
|
-
flex-direction: column;
|
|
219
|
-
gap: 6px;
|
|
220
|
-
padding: 10px;
|
|
221
|
-
background: var(--fe-bg);
|
|
222
|
-
border-bottom: 1px solid var(--fe-border);
|
|
223
|
-
min-height: 84px;
|
|
224
|
-
}
|
|
225
|
-
|
|
226
|
-
.fe-themecard__topbar {
|
|
227
|
-
display: flex;
|
|
228
|
-
gap: 4px;
|
|
229
|
-
margin-bottom: 2px;
|
|
230
|
-
}
|
|
231
|
-
|
|
232
|
-
.fe-themecard__chip {
|
|
233
|
-
height: 8px;
|
|
234
|
-
width: 22px;
|
|
235
|
-
border-radius: 4px;
|
|
236
|
-
background: var(--fe-bg-hover);
|
|
237
|
-
border: 1px solid var(--fe-border);
|
|
238
|
-
}
|
|
239
|
-
|
|
240
|
-
.fe-themecard__chip--primary {
|
|
241
|
-
background: var(--fe-primary);
|
|
242
|
-
border-color: var(--fe-primary);
|
|
243
|
-
}
|
|
244
|
-
|
|
245
|
-
.fe-themecard__chip--ghost {
|
|
246
|
-
background: var(--fe-bg-elev);
|
|
247
|
-
}
|
|
248
|
-
|
|
249
|
-
.fe-themecard__row {
|
|
250
|
-
display: flex;
|
|
251
|
-
align-items: center;
|
|
252
|
-
gap: 6px;
|
|
253
|
-
padding: 3px 4px;
|
|
254
|
-
border-radius: 4px;
|
|
255
|
-
}
|
|
256
|
-
|
|
257
|
-
.fe-themecard__row--selected {
|
|
258
|
-
background: var(--fe-bg-selected);
|
|
259
|
-
}
|
|
260
|
-
|
|
261
|
-
.fe-themecard__dot {
|
|
262
|
-
width: 8px;
|
|
263
|
-
height: 8px;
|
|
264
|
-
border-radius: 50%;
|
|
265
|
-
background: var(--fe-text);
|
|
266
|
-
opacity: 0.75;
|
|
267
|
-
flex: none;
|
|
268
|
-
}
|
|
269
|
-
|
|
270
|
-
.fe-themecard__dot--primary {
|
|
271
|
-
background: var(--fe-primary);
|
|
272
|
-
opacity: 1;
|
|
273
|
-
}
|
|
274
|
-
|
|
275
|
-
.fe-themecard__dot--muted {
|
|
276
|
-
background: var(--fe-text-muted);
|
|
277
|
-
}
|
|
278
|
-
|
|
279
|
-
.fe-themecard__line {
|
|
280
|
-
height: 6px;
|
|
281
|
-
border-radius: 3px;
|
|
282
|
-
background: var(--fe-text);
|
|
283
|
-
opacity: 0.55;
|
|
284
|
-
}
|
|
285
|
-
|
|
286
|
-
.fe-themecard__line--muted {
|
|
287
|
-
background: var(--fe-text-muted);
|
|
288
|
-
}
|
|
289
|
-
|
|
290
|
-
.fe-themecard__name {
|
|
291
|
-
display: flex;
|
|
292
|
-
align-items: center;
|
|
293
|
-
justify-content: space-between;
|
|
294
|
-
gap: 8px;
|
|
295
|
-
padding: 8px 10px;
|
|
296
|
-
font-size: 12.5px;
|
|
297
|
-
font-weight: 600;
|
|
298
|
-
color: var(--fe-text);
|
|
299
|
-
background: var(--fe-bg-elev);
|
|
300
|
-
font-family: var(--fe-font, inherit);
|
|
301
|
-
}
|
|
302
|
-
|
|
303
|
-
.fe-themecard__check {
|
|
304
|
-
flex: none;
|
|
305
|
-
width: 18px;
|
|
306
|
-
height: 18px;
|
|
307
|
-
display: inline-flex;
|
|
308
|
-
align-items: center;
|
|
309
|
-
justify-content: center;
|
|
310
|
-
border-radius: 50%;
|
|
311
|
-
background: var(--fe-primary);
|
|
312
|
-
color: var(--fe-text-on-primary);
|
|
313
|
-
font-size: 11px;
|
|
314
|
-
line-height: 1;
|
|
315
|
-
}
|
|
316
|
-
</style>
|