@brftech/filex-core 0.4.1 → 0.5.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/dist/{ArchiveViewer-BOXJ2ub7.js → ArchiveViewer-DKW-HyXT.js} +2 -2
- package/dist/{ArchiveViewer-BOXJ2ub7.js.map → ArchiveViewer-DKW-HyXT.js.map} +1 -1
- package/dist/{CsvViewer-CDs9EpJx.js → CsvViewer-D2HmP2yr.js} +2 -2
- package/dist/{CsvViewer-CDs9EpJx.js.map → CsvViewer-D2HmP2yr.js.map} +1 -1
- package/dist/{DrawioViewer-Q__JcUmq.js → DrawioViewer-DPNvOFCs.js} +2 -2
- package/dist/{DrawioViewer-Q__JcUmq.js.map → DrawioViewer-DPNvOFCs.js.map} +1 -1
- package/dist/{EpubViewer-BlsOD2xl.js → EpubViewer-ByqjNuNL.js} +2 -2
- package/dist/{EpubViewer-BlsOD2xl.js.map → EpubViewer-ByqjNuNL.js.map} +1 -1
- package/dist/{IpynbViewer-DKSFiZmv.js → IpynbViewer-2iONhbuJ.js} +2 -2
- package/dist/{IpynbViewer-DKSFiZmv.js.map → IpynbViewer-2iONhbuJ.js.map} +1 -1
- package/dist/{MermaidViewer-D2Gnp4yX.js → MermaidViewer-CmiU7hIT.js} +2 -2
- package/dist/{MermaidViewer-D2Gnp4yX.js.map → MermaidViewer-CmiU7hIT.js.map} +1 -1
- package/dist/{PsdViewer-BD-rIrF1.js → PsdViewer-Inhl2yN9.js} +2 -2
- package/dist/{PsdViewer-BD-rIrF1.js.map → PsdViewer-Inhl2yN9.js.map} +1 -1
- package/dist/{TiffViewer-8qitRwcZ.js → TiffViewer-C2AFFEze.js} +2 -2
- package/dist/{TiffViewer-8qitRwcZ.js.map → TiffViewer-C2AFFEze.js.map} +1 -1
- package/dist/{Viewer3D-WLmIjgmZ.js → Viewer3D-YdW14q_n.js} +2 -2
- package/dist/{Viewer3D-WLmIjgmZ.js.map → Viewer3D-YdW14q_n.js.map} +1 -1
- package/dist/filex-core.js +47 -25
- package/dist/filex-core.umd.cjs +40 -40
- package/dist/filex-core.umd.cjs.map +1 -1
- package/dist/index-eARevpz0.js +9952 -0
- package/dist/index-eARevpz0.js.map +1 -0
- package/dist/index.d.ts +363 -3
- package/dist/style.css +1 -1
- package/package.json +1 -1
- package/src/FileExplorer.vue +269 -1
- package/src/components/CommandPalette.vue +12 -0
- package/src/components/ContextMenu.vue +114 -8
- package/src/components/GridView.vue +56 -1
- package/src/components/ListView.vue +39 -6
- package/src/components/OnboardingTour.vue +332 -0
- package/src/components/OperationsCenter.vue +284 -0
- package/src/components/PendingOpsTray.vue +50 -66
- package/src/components/QuickLook.vue +142 -0
- package/src/components/ShortcutSettings.vue +270 -0
- package/src/components/ShortcutsHelp.vue +30 -9
- package/src/components/ThemeGallery.vue +253 -0
- package/src/components/Toolbar.vue +52 -6
- package/src/components/UploadProgress.vue +49 -57
- package/src/composables/useKeyboardShortcuts.ts +321 -137
- package/src/composables/useOperations.ts +291 -0
- package/src/index.ts +45 -0
- package/src/lib/dragGhost.ts +41 -0
- package/src/lib/themes.ts +505 -0
- package/src/locales/en.ts +92 -0
- package/src/locales/tr.ts +92 -0
- package/src/modals/Modal.vue +41 -5
- package/src/styles/base.css +689 -1
- package/dist/index-f-_N5BV1.js +0 -7972
- package/dist/index-f-_N5BV1.js.map +0 -1
|
@@ -0,0 +1,270 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
/**
|
|
3
|
+
* ShortcutSettings — wiring:c2 keyboard-shortcut customization modal.
|
|
4
|
+
*
|
|
5
|
+
* Category-grouped rows from the shortcut registry; each row shows the
|
|
6
|
+
* action label + current combo badge(s) + "Change" (press-to-capture:
|
|
7
|
+
* the next key combination is recorded, Esc cancels) + a per-row
|
|
8
|
+
* "reset to default" and a header "reset all".
|
|
9
|
+
*
|
|
10
|
+
* Conflicts: assigning a combo that another action already uses turns
|
|
11
|
+
* the row into a red warning with two choices — unbind the old action
|
|
12
|
+
* and take the combo, or cancel. Combos reserved by fixed shortcuts
|
|
13
|
+
* (Backspace, Esc) can never be taken.
|
|
14
|
+
*
|
|
15
|
+
* While the modal is open a window-level CAPTURE listener swallows
|
|
16
|
+
* keydowns (except Esc/Tab) so experimenting with keys can't trigger
|
|
17
|
+
* explorer actions behind the modal; native button activation (Enter/
|
|
18
|
+
* Space on a focused button) is a default action and still works.
|
|
19
|
+
*/
|
|
20
|
+
import { computed, onBeforeUnmount, ref, watch } from 'vue';
|
|
21
|
+
import type { LocaleCode } from '../types/ExplorerConfig';
|
|
22
|
+
import { useLocale } from '../composables/useLocale';
|
|
23
|
+
import {
|
|
24
|
+
comboFromEvent,
|
|
25
|
+
effectiveCombo,
|
|
26
|
+
findShortcutConflict,
|
|
27
|
+
resetAllShortcuts,
|
|
28
|
+
resetShortcut,
|
|
29
|
+
setShortcutOverride,
|
|
30
|
+
useShortcutList,
|
|
31
|
+
SHORTCUT_ACTIONS,
|
|
32
|
+
type ShortcutView,
|
|
33
|
+
} from '../composables/useKeyboardShortcuts';
|
|
34
|
+
import Modal from '../modals/Modal.vue';
|
|
35
|
+
|
|
36
|
+
const props = defineProps<{
|
|
37
|
+
open: boolean;
|
|
38
|
+
locale: LocaleCode;
|
|
39
|
+
theme?: 'light' | 'dark' | 'auto';
|
|
40
|
+
}>();
|
|
41
|
+
|
|
42
|
+
const emit = defineEmits<{
|
|
43
|
+
(e: 'close'): void;
|
|
44
|
+
}>();
|
|
45
|
+
|
|
46
|
+
const { t } = useLocale(() => props.locale);
|
|
47
|
+
|
|
48
|
+
const list = useShortcutList();
|
|
49
|
+
|
|
50
|
+
const groups = computed(() => {
|
|
51
|
+
const order: string[] = [];
|
|
52
|
+
const map = new Map<string, ShortcutView[]>();
|
|
53
|
+
for (const s of list.value) {
|
|
54
|
+
if (!map.has(s.groupKey)) {
|
|
55
|
+
map.set(s.groupKey, []);
|
|
56
|
+
order.push(s.groupKey);
|
|
57
|
+
}
|
|
58
|
+
map.get(s.groupKey)!.push(s);
|
|
59
|
+
}
|
|
60
|
+
return order.map((key) => ({ key, items: map.get(key)! }));
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
const anyOverridden = computed(() => list.value.some((s) => s.overridden));
|
|
64
|
+
|
|
65
|
+
// --- capture / conflict state ---
|
|
66
|
+
|
|
67
|
+
const capturingId = ref<string | null>(null);
|
|
68
|
+
const conflict = ref<{ id: string; combo: string; conflictId: string; fixed: boolean } | null>(null);
|
|
69
|
+
|
|
70
|
+
function labelOf(id: string): string {
|
|
71
|
+
const def = SHORTCUT_ACTIONS.find((a) => a.id === id);
|
|
72
|
+
return def ? t(def.labelKey) : id;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function beginCapture(id: string) {
|
|
76
|
+
conflict.value = null;
|
|
77
|
+
capturingId.value = id;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function cancelCapture() {
|
|
81
|
+
capturingId.value = null;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function attemptAssign(id: string, combo: string) {
|
|
85
|
+
capturingId.value = null;
|
|
86
|
+
if (combo === effectiveCombo(id)) return; // unchanged
|
|
87
|
+
const clash = findShortcutConflict(combo, id);
|
|
88
|
+
if (clash) {
|
|
89
|
+
conflict.value = { id, combo, conflictId: clash.id, fixed: clash.fixed };
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
setShortcutOverride(id, combo);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function confirmConflictTake() {
|
|
96
|
+
const c = conflict.value;
|
|
97
|
+
if (!c || c.fixed) return;
|
|
98
|
+
setShortcutOverride(c.conflictId, ''); // unbind the old owner
|
|
99
|
+
setShortcutOverride(c.id, c.combo);
|
|
100
|
+
conflict.value = null;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function dismissConflict() {
|
|
104
|
+
conflict.value = null;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function onReset(id: string) {
|
|
108
|
+
conflict.value = null;
|
|
109
|
+
resetShortcut(id);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function onResetAll() {
|
|
113
|
+
conflict.value = null;
|
|
114
|
+
capturingId.value = null;
|
|
115
|
+
resetAllShortcuts();
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// --- window capture listener (only while open) ---
|
|
119
|
+
|
|
120
|
+
function onWindowKeydown(e: KeyboardEvent) {
|
|
121
|
+
if (capturingId.value) {
|
|
122
|
+
e.preventDefault();
|
|
123
|
+
e.stopPropagation();
|
|
124
|
+
if (e.key === 'Escape') {
|
|
125
|
+
cancelCapture();
|
|
126
|
+
return;
|
|
127
|
+
}
|
|
128
|
+
const combo = comboFromEvent(e);
|
|
129
|
+
if (!combo) return; // bare modifier — keep listening
|
|
130
|
+
attemptAssign(capturingId.value, combo);
|
|
131
|
+
return;
|
|
132
|
+
}
|
|
133
|
+
// Modal open but idle: let Esc (close) and Tab (focus nav) through,
|
|
134
|
+
// swallow everything else so explorer shortcuts can't fire behind us.
|
|
135
|
+
if (e.key === 'Escape' || e.key === 'Tab') return;
|
|
136
|
+
e.stopPropagation();
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
watch(
|
|
140
|
+
() => props.open,
|
|
141
|
+
(open) => {
|
|
142
|
+
if (open) {
|
|
143
|
+
window.addEventListener('keydown', onWindowKeydown, true);
|
|
144
|
+
} else {
|
|
145
|
+
window.removeEventListener('keydown', onWindowKeydown, true);
|
|
146
|
+
capturingId.value = null;
|
|
147
|
+
conflict.value = null;
|
|
148
|
+
}
|
|
149
|
+
},
|
|
150
|
+
);
|
|
151
|
+
|
|
152
|
+
onBeforeUnmount(() => window.removeEventListener('keydown', onWindowKeydown, true));
|
|
153
|
+
|
|
154
|
+
function onClose() {
|
|
155
|
+
if (capturingId.value) {
|
|
156
|
+
cancelCapture();
|
|
157
|
+
return;
|
|
158
|
+
}
|
|
159
|
+
emit('close');
|
|
160
|
+
}
|
|
161
|
+
</script>
|
|
162
|
+
|
|
163
|
+
<template>
|
|
164
|
+
<Modal :open="open" :title="t('shortcuts.settings.title')" size="lg" :theme="theme" @close="onClose">
|
|
165
|
+
<div class="fe-shortset">
|
|
166
|
+
<div class="fe-shortset__topbar">
|
|
167
|
+
<p class="fe-shortset__hint">{{ t('shortcuts.settings.hint') }}</p>
|
|
168
|
+
<button
|
|
169
|
+
type="button"
|
|
170
|
+
class="fe-btn fe-shortset__reset-all"
|
|
171
|
+
:disabled="!anyOverridden"
|
|
172
|
+
@click="onResetAll"
|
|
173
|
+
>
|
|
174
|
+
↺ {{ t('shortcuts.settings.reset_all') }}
|
|
175
|
+
</button>
|
|
176
|
+
</div>
|
|
177
|
+
|
|
178
|
+
<section v-for="g in groups" :key="g.key" class="fe-shortset__group">
|
|
179
|
+
<h3 class="fe-shortcuts__heading">{{ t(g.key) }}</h3>
|
|
180
|
+
<div
|
|
181
|
+
v-for="s in g.items"
|
|
182
|
+
:key="s.id"
|
|
183
|
+
class="fe-shortset__item"
|
|
184
|
+
:class="{
|
|
185
|
+
'is-capturing': capturingId === s.id,
|
|
186
|
+
'is-conflict': conflict?.id === s.id,
|
|
187
|
+
}"
|
|
188
|
+
>
|
|
189
|
+
<div class="fe-shortset__row">
|
|
190
|
+
<span class="fe-shortset__label">
|
|
191
|
+
{{ t(s.labelKey) }}
|
|
192
|
+
<span v-if="s.overridden" class="fe-shortcuts__custom" :title="t('shortcuts.settings.customized')">●</span>
|
|
193
|
+
</span>
|
|
194
|
+
<span class="fe-shortset__keys">
|
|
195
|
+
<span v-if="capturingId === s.id" class="fe-shortset__capture" role="status">
|
|
196
|
+
{{ t('shortcuts.settings.capture') }}
|
|
197
|
+
</span>
|
|
198
|
+
<template v-else>
|
|
199
|
+
<span v-if="s.keys.length === 0" class="fe-shortcuts__unbound">
|
|
200
|
+
{{ t('shortcuts.settings.unbound') }}
|
|
201
|
+
</span>
|
|
202
|
+
<template v-for="(combo, i) in s.keys" v-else :key="combo">
|
|
203
|
+
<kbd class="fe-kbd">{{ combo }}</kbd>
|
|
204
|
+
<span v-if="i < s.keys.length - 1" class="fe-shortcuts__or" aria-hidden="true">/</span>
|
|
205
|
+
</template>
|
|
206
|
+
</template>
|
|
207
|
+
</span>
|
|
208
|
+
<span class="fe-shortset__actions">
|
|
209
|
+
<template v-if="s.customizable">
|
|
210
|
+
<button
|
|
211
|
+
v-if="capturingId !== s.id"
|
|
212
|
+
type="button"
|
|
213
|
+
class="fe-btn fe-shortset__btn"
|
|
214
|
+
@click="beginCapture(s.id)"
|
|
215
|
+
>
|
|
216
|
+
{{ t('shortcuts.settings.change') }}
|
|
217
|
+
</button>
|
|
218
|
+
<button
|
|
219
|
+
v-else
|
|
220
|
+
type="button"
|
|
221
|
+
class="fe-btn fe-shortset__btn"
|
|
222
|
+
@click="cancelCapture"
|
|
223
|
+
>
|
|
224
|
+
{{ t('shortcuts.settings.conflict_cancel') }}
|
|
225
|
+
</button>
|
|
226
|
+
<button
|
|
227
|
+
v-if="s.overridden"
|
|
228
|
+
type="button"
|
|
229
|
+
class="fe-btn fe-btn--icon-only fe-shortset__btn"
|
|
230
|
+
:title="t('shortcuts.settings.reset')"
|
|
231
|
+
:aria-label="t('shortcuts.settings.reset')"
|
|
232
|
+
@click="onReset(s.id)"
|
|
233
|
+
>
|
|
234
|
+
↺
|
|
235
|
+
</button>
|
|
236
|
+
</template>
|
|
237
|
+
<span v-else class="fe-shortset__fixed">{{ t('shortcuts.settings.fixed') }}</span>
|
|
238
|
+
</span>
|
|
239
|
+
</div>
|
|
240
|
+
<div v-if="conflict && conflict.id === s.id" class="fe-shortset__conflict" role="alert">
|
|
241
|
+
<span class="fe-shortset__conflict-msg">
|
|
242
|
+
<kbd class="fe-kbd">{{ conflict.combo }}</kbd>
|
|
243
|
+
{{
|
|
244
|
+
conflict.fixed
|
|
245
|
+
? t('shortcuts.settings.conflict_fixed', { name: labelOf(conflict.conflictId) })
|
|
246
|
+
: t('shortcuts.settings.conflict', { name: labelOf(conflict.conflictId) })
|
|
247
|
+
}}
|
|
248
|
+
</span>
|
|
249
|
+
<span class="fe-shortset__conflict-actions">
|
|
250
|
+
<button
|
|
251
|
+
v-if="!conflict.fixed"
|
|
252
|
+
type="button"
|
|
253
|
+
class="fe-btn fe-btn--danger fe-shortset__btn"
|
|
254
|
+
@click="confirmConflictTake"
|
|
255
|
+
>
|
|
256
|
+
{{ t('shortcuts.settings.conflict_take') }}
|
|
257
|
+
</button>
|
|
258
|
+
<button type="button" class="fe-btn fe-shortset__btn" @click="dismissConflict">
|
|
259
|
+
{{ t('shortcuts.settings.conflict_cancel') }}
|
|
260
|
+
</button>
|
|
261
|
+
</span>
|
|
262
|
+
</div>
|
|
263
|
+
</div>
|
|
264
|
+
</section>
|
|
265
|
+
</div>
|
|
266
|
+
<template #actions>
|
|
267
|
+
<button type="button" class="fe-btn" @click="onClose">{{ t('viewer.close') }}</button>
|
|
268
|
+
</template>
|
|
269
|
+
</Modal>
|
|
270
|
+
</template>
|
|
@@ -2,14 +2,16 @@
|
|
|
2
2
|
/**
|
|
3
3
|
* ShortcutsHelp — "?" cheat-sheet modal.
|
|
4
4
|
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
5
|
+
* wiring:c2 — renders the live shortcut registry (useShortcutList) so
|
|
6
|
+
* user remaps show up here too, grouped by section. Unbound actions are
|
|
7
|
+
* listed with an "unassigned" chip. The footer carries a "Customize"
|
|
8
|
+
* button that asks the host to open ShortcutSettings. Modal.vue
|
|
9
|
+
* supplies Esc handling, backdrop, focus and the close button.
|
|
8
10
|
*/
|
|
9
11
|
import { computed } from 'vue';
|
|
10
12
|
import type { LocaleCode } from '../types/ExplorerConfig';
|
|
11
13
|
import { useLocale } from '../composables/useLocale';
|
|
12
|
-
import {
|
|
14
|
+
import { useShortcutList, type ShortcutView } from '../composables/useKeyboardShortcuts';
|
|
13
15
|
import Modal from '../modals/Modal.vue';
|
|
14
16
|
|
|
15
17
|
const props = defineProps<{
|
|
@@ -19,14 +21,17 @@ const props = defineProps<{
|
|
|
19
21
|
|
|
20
22
|
const emit = defineEmits<{
|
|
21
23
|
(e: 'close'): void;
|
|
24
|
+
(e: 'customize'): void;
|
|
22
25
|
}>();
|
|
23
26
|
|
|
24
27
|
const { t } = useLocale(() => props.locale);
|
|
25
28
|
|
|
29
|
+
const list = useShortcutList();
|
|
30
|
+
|
|
26
31
|
const groups = computed(() => {
|
|
27
32
|
const order: string[] = [];
|
|
28
|
-
const map = new Map<string,
|
|
29
|
-
for (const s of
|
|
33
|
+
const map = new Map<string, ShortcutView[]>();
|
|
34
|
+
for (const s of list.value) {
|
|
30
35
|
if (!map.has(s.groupKey)) {
|
|
31
36
|
map.set(s.groupKey, []);
|
|
32
37
|
order.push(s.groupKey);
|
|
@@ -43,17 +48,33 @@ const groups = computed(() => {
|
|
|
43
48
|
<section v-for="g in groups" :key="g.key" class="fe-shortcuts__group">
|
|
44
49
|
<h3 class="fe-shortcuts__heading">{{ t(g.key) }}</h3>
|
|
45
50
|
<div class="fe-shortcuts__table">
|
|
46
|
-
<div v-for="s in g.items" :key="s.
|
|
51
|
+
<div v-for="s in g.items" :key="s.id" class="fe-shortcuts__row">
|
|
47
52
|
<span class="fe-shortcuts__keys">
|
|
48
|
-
<template v-
|
|
53
|
+
<template v-if="s.keys.length === 0">
|
|
54
|
+
<span class="fe-shortcuts__unbound">{{ t('shortcuts.settings.unbound') }}</span>
|
|
55
|
+
</template>
|
|
56
|
+
<template v-for="(combo, i) in s.keys" v-else :key="combo">
|
|
49
57
|
<kbd class="fe-kbd">{{ combo }}</kbd>
|
|
50
58
|
<span v-if="i < s.keys.length - 1" class="fe-shortcuts__or" aria-hidden="true">/</span>
|
|
51
59
|
</template>
|
|
52
60
|
</span>
|
|
53
|
-
<span class="fe-shortcuts__desc">
|
|
61
|
+
<span class="fe-shortcuts__desc">
|
|
62
|
+
{{ t(s.labelKey) }}
|
|
63
|
+
<span
|
|
64
|
+
v-if="s.overridden"
|
|
65
|
+
class="fe-shortcuts__custom"
|
|
66
|
+
:title="t('shortcuts.settings.customized')"
|
|
67
|
+
>●</span>
|
|
68
|
+
</span>
|
|
54
69
|
</div>
|
|
55
70
|
</div>
|
|
56
71
|
</section>
|
|
57
72
|
</div>
|
|
73
|
+
<template #actions>
|
|
74
|
+
<button type="button" class="fe-btn fe-btn--primary" @click="emit('customize')">
|
|
75
|
+
⌨ {{ t('shortcuts.customize') }}
|
|
76
|
+
</button>
|
|
77
|
+
<button type="button" class="fe-btn" @click="emit('close')">{{ t('viewer.close') }}</button>
|
|
78
|
+
</template>
|
|
58
79
|
</Modal>
|
|
59
80
|
</template>
|
|
@@ -0,0 +1,253 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
/**
|
|
3
|
+
* ThemeGallery — wiring:c1 theme picker modal.
|
|
4
|
+
*
|
|
5
|
+
* A grid of theme cards, each rendering a mini live preview (surface +
|
|
6
|
+
* toolbar chips + sample rows) painted with that theme's tokens for the
|
|
7
|
+
* CURRENTLY resolved light/dark mode — so the card shows exactly what
|
|
8
|
+
* you'd get. Click applies instantly (live preview behind the modal) and
|
|
9
|
+
* persists; the selected card carries a ✓. "Varsayılana dön" resets.
|
|
10
|
+
*
|
|
11
|
+
* Presentational only — selection state + persistence live in
|
|
12
|
+
* lib/themes.ts (shared across instances); the parent forwards `select`.
|
|
13
|
+
*/
|
|
14
|
+
import type { LocaleCode, ThemeMode } from '../types/ExplorerConfig';
|
|
15
|
+
import { useLocale } from '../composables/useLocale';
|
|
16
|
+
import { THEMES, DEFAULT_THEME_ID, type ThemeDef } from '../lib/themes';
|
|
17
|
+
import Modal from '../modals/Modal.vue';
|
|
18
|
+
|
|
19
|
+
const props = defineProps<{
|
|
20
|
+
open: boolean;
|
|
21
|
+
locale: LocaleCode;
|
|
22
|
+
/** Host mode (forwarded to Modal so the backdrop themes correctly). */
|
|
23
|
+
theme?: ThemeMode;
|
|
24
|
+
/** Resolved mode — decides which variant the preview cards paint. */
|
|
25
|
+
dark: boolean;
|
|
26
|
+
/** Currently selected theme id. */
|
|
27
|
+
current: string;
|
|
28
|
+
}>();
|
|
29
|
+
|
|
30
|
+
const emit = defineEmits<{
|
|
31
|
+
(e: 'close'): void;
|
|
32
|
+
(e: 'select', id: string): void;
|
|
33
|
+
}>();
|
|
34
|
+
|
|
35
|
+
const { t } = useLocale(() => props.locale);
|
|
36
|
+
|
|
37
|
+
/** Inline variables for one card — a self-contained mini `--fe-*` scope,
|
|
38
|
+
* so the preview markup can use the exact same tokens the real UI uses. */
|
|
39
|
+
function cardVars(th: ThemeDef): Record<string, string> {
|
|
40
|
+
return props.dark ? th.dark : th.light;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const DEFAULT_ID = DEFAULT_THEME_ID;
|
|
44
|
+
</script>
|
|
45
|
+
|
|
46
|
+
<template>
|
|
47
|
+
<Modal :open="open" :title="t('theme.title')" size="lg" :theme="theme" @close="emit('close')">
|
|
48
|
+
<p class="fe-themes__hint">{{ t('theme.hint') }}</p>
|
|
49
|
+
<div class="fe-themes" role="listbox" :aria-label="t('theme.title')">
|
|
50
|
+
<button
|
|
51
|
+
v-for="th in THEMES"
|
|
52
|
+
:key="th.id"
|
|
53
|
+
type="button"
|
|
54
|
+
class="fe-themecard"
|
|
55
|
+
:class="{ 'is-selected': th.id === current }"
|
|
56
|
+
role="option"
|
|
57
|
+
:aria-selected="th.id === current"
|
|
58
|
+
:style="cardVars(th)"
|
|
59
|
+
@click="emit('select', th.id)"
|
|
60
|
+
>
|
|
61
|
+
<span class="fe-themecard__preview" aria-hidden="true">
|
|
62
|
+
<span class="fe-themecard__topbar">
|
|
63
|
+
<span class="fe-themecard__chip fe-themecard__chip--primary"></span>
|
|
64
|
+
<span class="fe-themecard__chip"></span>
|
|
65
|
+
<span class="fe-themecard__chip fe-themecard__chip--ghost"></span>
|
|
66
|
+
</span>
|
|
67
|
+
<span class="fe-themecard__row">
|
|
68
|
+
<span class="fe-themecard__dot"></span>
|
|
69
|
+
<span class="fe-themecard__line" style="width: 62%"></span>
|
|
70
|
+
</span>
|
|
71
|
+
<span class="fe-themecard__row fe-themecard__row--selected">
|
|
72
|
+
<span class="fe-themecard__dot fe-themecard__dot--primary"></span>
|
|
73
|
+
<span class="fe-themecard__line" style="width: 44%"></span>
|
|
74
|
+
</span>
|
|
75
|
+
<span class="fe-themecard__row">
|
|
76
|
+
<span class="fe-themecard__dot fe-themecard__dot--muted"></span>
|
|
77
|
+
<span class="fe-themecard__line fe-themecard__line--muted" style="width: 74%"></span>
|
|
78
|
+
</span>
|
|
79
|
+
</span>
|
|
80
|
+
<span class="fe-themecard__name">
|
|
81
|
+
{{ t(th.nameKey) }}
|
|
82
|
+
<span v-if="th.id === current" class="fe-themecard__check" :title="t('theme.selected')">✓</span>
|
|
83
|
+
</span>
|
|
84
|
+
</button>
|
|
85
|
+
</div>
|
|
86
|
+
<template #actions>
|
|
87
|
+
<button
|
|
88
|
+
type="button"
|
|
89
|
+
class="fe-btn"
|
|
90
|
+
:disabled="current === DEFAULT_ID"
|
|
91
|
+
@click="emit('select', DEFAULT_ID)"
|
|
92
|
+
>
|
|
93
|
+
{{ t('theme.reset') }}
|
|
94
|
+
</button>
|
|
95
|
+
<button type="button" class="fe-btn fe-btn--primary" @click="emit('close')">
|
|
96
|
+
{{ t('theme.close') }}
|
|
97
|
+
</button>
|
|
98
|
+
</template>
|
|
99
|
+
</Modal>
|
|
100
|
+
</template>
|
|
101
|
+
|
|
102
|
+
<!-- Deliberately NOT scoped: the webcomponent injects the CORE package's
|
|
103
|
+
built style.css, whose data-v scope ids don't match the ids the
|
|
104
|
+
webcomponent's own compile assigns (verified: fe-themecard got
|
|
105
|
+
data-v-99a95ed7 in core vs data-v-5d4cc1fd in the wc bundle) — scoped
|
|
106
|
+
rules would silently not apply in embeds. Class names are fe-*
|
|
107
|
+
namespaced, so global is collision-safe (same approach as base.css). -->
|
|
108
|
+
<style>
|
|
109
|
+
.fe-themes__hint {
|
|
110
|
+
margin: 0 0 12px;
|
|
111
|
+
font-size: 12.5px;
|
|
112
|
+
color: var(--fe-text-muted);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
.fe-themes {
|
|
116
|
+
display: grid;
|
|
117
|
+
grid-template-columns: repeat(auto-fill, minmax(150px, 1fr));
|
|
118
|
+
gap: 12px;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/* Each card is its own mini token scope (inline --fe-* vars from the theme
|
|
122
|
+
definition), so every color below refers to the CARD's theme — not the
|
|
123
|
+
active page theme. */
|
|
124
|
+
.fe-themecard {
|
|
125
|
+
display: flex;
|
|
126
|
+
flex-direction: column;
|
|
127
|
+
padding: 0;
|
|
128
|
+
border: 2px solid var(--fe-border-strong);
|
|
129
|
+
border-radius: var(--fe-radius, 8px);
|
|
130
|
+
background: var(--fe-bg);
|
|
131
|
+
cursor: pointer;
|
|
132
|
+
overflow: hidden;
|
|
133
|
+
text-align: left;
|
|
134
|
+
transition: transform 0.12s ease, box-shadow 0.12s ease;
|
|
135
|
+
font: inherit;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
.fe-themecard:hover {
|
|
139
|
+
transform: translateY(-2px);
|
|
140
|
+
box-shadow: var(--fe-shadow-sm, 0 2px 6px rgba(15, 23, 42, 0.08));
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
.fe-themecard:focus-visible {
|
|
144
|
+
outline: 2px solid var(--fe-primary);
|
|
145
|
+
outline-offset: 2px;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
.fe-themecard.is-selected {
|
|
149
|
+
border-color: var(--fe-primary);
|
|
150
|
+
box-shadow: 0 0 0 2px var(--fe-primary);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
.fe-themecard__preview {
|
|
154
|
+
display: flex;
|
|
155
|
+
flex-direction: column;
|
|
156
|
+
gap: 6px;
|
|
157
|
+
padding: 10px;
|
|
158
|
+
background: var(--fe-bg);
|
|
159
|
+
border-bottom: 1px solid var(--fe-border);
|
|
160
|
+
min-height: 84px;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
.fe-themecard__topbar {
|
|
164
|
+
display: flex;
|
|
165
|
+
gap: 4px;
|
|
166
|
+
margin-bottom: 2px;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
.fe-themecard__chip {
|
|
170
|
+
height: 8px;
|
|
171
|
+
width: 22px;
|
|
172
|
+
border-radius: 4px;
|
|
173
|
+
background: var(--fe-bg-hover);
|
|
174
|
+
border: 1px solid var(--fe-border);
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
.fe-themecard__chip--primary {
|
|
178
|
+
background: var(--fe-primary);
|
|
179
|
+
border-color: var(--fe-primary);
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
.fe-themecard__chip--ghost {
|
|
183
|
+
background: var(--fe-bg-elev);
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
.fe-themecard__row {
|
|
187
|
+
display: flex;
|
|
188
|
+
align-items: center;
|
|
189
|
+
gap: 6px;
|
|
190
|
+
padding: 3px 4px;
|
|
191
|
+
border-radius: 4px;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
.fe-themecard__row--selected {
|
|
195
|
+
background: var(--fe-bg-selected);
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
.fe-themecard__dot {
|
|
199
|
+
width: 8px;
|
|
200
|
+
height: 8px;
|
|
201
|
+
border-radius: 50%;
|
|
202
|
+
background: var(--fe-text);
|
|
203
|
+
opacity: 0.75;
|
|
204
|
+
flex: none;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
.fe-themecard__dot--primary {
|
|
208
|
+
background: var(--fe-primary);
|
|
209
|
+
opacity: 1;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
.fe-themecard__dot--muted {
|
|
213
|
+
background: var(--fe-text-muted);
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
.fe-themecard__line {
|
|
217
|
+
height: 6px;
|
|
218
|
+
border-radius: 3px;
|
|
219
|
+
background: var(--fe-text);
|
|
220
|
+
opacity: 0.55;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
.fe-themecard__line--muted {
|
|
224
|
+
background: var(--fe-text-muted);
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
.fe-themecard__name {
|
|
228
|
+
display: flex;
|
|
229
|
+
align-items: center;
|
|
230
|
+
justify-content: space-between;
|
|
231
|
+
gap: 8px;
|
|
232
|
+
padding: 8px 10px;
|
|
233
|
+
font-size: 12.5px;
|
|
234
|
+
font-weight: 600;
|
|
235
|
+
color: var(--fe-text);
|
|
236
|
+
background: var(--fe-bg-elev);
|
|
237
|
+
font-family: var(--fe-font, inherit);
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
.fe-themecard__check {
|
|
241
|
+
flex: none;
|
|
242
|
+
width: 18px;
|
|
243
|
+
height: 18px;
|
|
244
|
+
display: inline-flex;
|
|
245
|
+
align-items: center;
|
|
246
|
+
justify-content: center;
|
|
247
|
+
border-radius: 50%;
|
|
248
|
+
background: var(--fe-primary);
|
|
249
|
+
color: var(--fe-text-on-primary);
|
|
250
|
+
font-size: 11px;
|
|
251
|
+
line-height: 1;
|
|
252
|
+
}
|
|
253
|
+
</style>
|