@brftech/filex-core 0.4.2 → 0.6.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.
Files changed (58) hide show
  1. package/dist/{ArchiveViewer-B_0IQExT.js → ArchiveViewer-EGgNqLPE.js} +2 -2
  2. package/dist/{ArchiveViewer-B_0IQExT.js.map → ArchiveViewer-EGgNqLPE.js.map} +1 -1
  3. package/dist/{CsvViewer-maMfUy5i.js → CsvViewer-BvriJ42M.js} +2 -2
  4. package/dist/{CsvViewer-maMfUy5i.js.map → CsvViewer-BvriJ42M.js.map} +1 -1
  5. package/dist/{DrawioViewer-DP_pXI00.js → DrawioViewer-B_bSPsWX.js} +2 -2
  6. package/dist/{DrawioViewer-DP_pXI00.js.map → DrawioViewer-B_bSPsWX.js.map} +1 -1
  7. package/dist/{EpubViewer-DSaqGnVx.js → EpubViewer-hl68h4gG.js} +2 -2
  8. package/dist/{EpubViewer-DSaqGnVx.js.map → EpubViewer-hl68h4gG.js.map} +1 -1
  9. package/dist/{IpynbViewer-BmdBanF1.js → IpynbViewer-lhwOjXK7.js} +2 -2
  10. package/dist/{IpynbViewer-BmdBanF1.js.map → IpynbViewer-lhwOjXK7.js.map} +1 -1
  11. package/dist/{MermaidViewer-Drlkl0Z5.js → MermaidViewer-H2Z6AYXg.js} +2 -2
  12. package/dist/{MermaidViewer-Drlkl0Z5.js.map → MermaidViewer-H2Z6AYXg.js.map} +1 -1
  13. package/dist/{PsdViewer-gftUJrsm.js → PsdViewer-Bsk-11yH.js} +2 -2
  14. package/dist/{PsdViewer-gftUJrsm.js.map → PsdViewer-Bsk-11yH.js.map} +1 -1
  15. package/dist/{TiffViewer-0Uv0-Lxh.js → TiffViewer-D-j9vycG.js} +2 -2
  16. package/dist/{TiffViewer-0Uv0-Lxh.js.map → TiffViewer-D-j9vycG.js.map} +1 -1
  17. package/dist/{Viewer3D-Gu3kUpxV.js → Viewer3D-Bcr70rTe.js} +2 -2
  18. package/dist/{Viewer3D-Gu3kUpxV.js.map → Viewer3D-Bcr70rTe.js.map} +1 -1
  19. package/dist/filex-core.js +50 -25
  20. package/dist/filex-core.umd.cjs +59 -58
  21. package/dist/filex-core.umd.cjs.map +1 -1
  22. package/dist/index-C_ZSnk_I.js +11109 -0
  23. package/dist/index-C_ZSnk_I.js.map +1 -0
  24. package/dist/index.d.ts +597 -4
  25. package/dist/style.css +1 -1
  26. package/package.json +1 -1
  27. package/src/FileExplorer.vue +636 -15
  28. package/src/components/CommandPalette.vue +24 -1
  29. package/src/components/ContextMenu.vue +114 -8
  30. package/src/components/GalleryView.vue +214 -0
  31. package/src/components/GridView.vue +34 -1
  32. package/src/components/InspectorPanel.vue +170 -0
  33. package/src/components/ListView.vue +40 -6
  34. package/src/components/OnboardingTour.vue +332 -0
  35. package/src/components/OperationsCenter.vue +284 -0
  36. package/src/components/PendingOpsTray.vue +50 -66
  37. package/src/components/QuickLook.vue +142 -0
  38. package/src/components/SecondaryPane.vue +411 -0
  39. package/src/components/ShortcutSettings.vue +270 -0
  40. package/src/components/ShortcutsHelp.vue +30 -9
  41. package/src/components/TabBar.vue +174 -0
  42. package/src/components/ThemeGallery.vue +253 -0
  43. package/src/components/Toolbar.vue +75 -11
  44. package/src/components/UploadProgress.vue +49 -57
  45. package/src/composables/useFileApi.ts +53 -0
  46. package/src/composables/useKeyboardShortcuts.ts +340 -137
  47. package/src/composables/useOperations.ts +291 -0
  48. package/src/composables/useTabs.ts +222 -0
  49. package/src/index.ts +51 -0
  50. package/src/lib/dragGhost.ts +41 -0
  51. package/src/lib/themes.ts +505 -0
  52. package/src/locales/en.ts +125 -0
  53. package/src/locales/tr.ts +125 -0
  54. package/src/modals/Modal.vue +41 -5
  55. package/src/styles/base.css +1210 -0
  56. package/src/types/FileNode.ts +1 -1
  57. package/dist/index-BLocdbmc.js +0 -7993
  58. package/dist/index-BLocdbmc.js.map +0 -1
@@ -2,14 +2,16 @@
2
2
  /**
3
3
  * ShortcutsHelp — "?" cheat-sheet modal.
4
4
  *
5
- * Renders the SHORTCUTS table exported by useKeyboardShortcuts (single
6
- * source of truth) grouped by section. Modal.vue supplies Esc handling,
7
- * backdrop, focus and the close button.
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 { SHORTCUTS, type ShortcutDef } from '../composables/useKeyboardShortcuts';
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, ShortcutDef[]>();
29
- for (const s of SHORTCUTS) {
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.labelKey" class="fe-shortcuts__row">
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-for="(combo, i) in s.keys" :key="combo">
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">{{ t(s.labelKey) }}</span>
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,174 @@
1
+ <script setup lang="ts">
2
+ /**
3
+ * TabBar — wiring:d1 sekme şeridi.
4
+ *
5
+ * Thin strip that sits ABOVE the toolbar. The host renders it only when
6
+ * 2+ tabs exist (embed pixel-parity: a single tab shows nothing at all).
7
+ * Purely presentational: every mutation is an emit; useTabs owns state.
8
+ *
9
+ * Interactions:
10
+ * click tab → select
11
+ * × / middle-tık → close
12
+ * + → new tab (clone of the current location)
13
+ * drag → reorder (HTML5 DnD; live-swap while dragging)
14
+ * ⫿ (split) → toggle the active tab's secondary pane
15
+ *
16
+ * UNSCOPED styles (`fe-tabs*` in base.css) — the webcomponent build
17
+ * cannot carry scoped data-v hashes (v0.5.0 discovery).
18
+ */
19
+ import { ref } from 'vue';
20
+ import type { LocaleCode } from '../types/ExplorerConfig';
21
+ import { useLocale } from '../composables/useLocale';
22
+
23
+ export interface TabItem {
24
+ id: string;
25
+ label: string;
26
+ /** true → the tab carries a split pane (small accent dot). */
27
+ split: boolean;
28
+ }
29
+
30
+ const props = defineProps<{
31
+ tabs: TabItem[];
32
+ activeId: string;
33
+ locale: LocaleCode;
34
+ /** false → the split toggle is hidden (narrow embeds). */
35
+ splitEnabled?: boolean;
36
+ /** true → the split toggle renders pressed (active tab has a pane). */
37
+ splitActive?: boolean;
38
+ }>();
39
+
40
+ const emit = defineEmits<{
41
+ (e: 'select', id: string): void;
42
+ (e: 'close', id: string): void;
43
+ (e: 'new'): void;
44
+ (e: 'reorder', from: number, to: number): void;
45
+ (e: 'toggle-split'): void;
46
+ }>();
47
+
48
+ const { t } = useLocale(() => props.locale);
49
+
50
+ // ---- drag-sort (HTML5 DnD, live swap) ------------------------------
51
+ const dragIndex = ref<number | null>(null);
52
+ const overIndex = ref<number | null>(null);
53
+
54
+ function onDragStart(i: number, ev: DragEvent) {
55
+ dragIndex.value = i;
56
+ if (ev.dataTransfer) {
57
+ ev.dataTransfer.effectAllowed = 'move';
58
+ // Firefox requires SOME payload for a drag to start. Deliberately not
59
+ // the internal file-DnD MIME, so the explorer's drop surfaces ignore it.
60
+ ev.dataTransfer.setData('text/plain', props.tabs[i]?.id ?? '');
61
+ }
62
+ }
63
+
64
+ function onDragOver(i: number, ev: DragEvent) {
65
+ if (dragIndex.value === null) return;
66
+ ev.preventDefault();
67
+ ev.stopPropagation();
68
+ if (ev.dataTransfer) ev.dataTransfer.dropEffect = 'move';
69
+ overIndex.value = i;
70
+ }
71
+
72
+ function onDragLeave(i: number) {
73
+ if (overIndex.value === i) overIndex.value = null;
74
+ }
75
+
76
+ function onDrop(i: number, ev: DragEvent) {
77
+ if (dragIndex.value === null) return;
78
+ ev.preventDefault();
79
+ ev.stopPropagation();
80
+ if (dragIndex.value !== i) emit('reorder', dragIndex.value, i);
81
+ dragIndex.value = null;
82
+ overIndex.value = null;
83
+ }
84
+
85
+ function onDragEnd() {
86
+ dragIndex.value = null;
87
+ overIndex.value = null;
88
+ }
89
+
90
+ // Middle-click closes a tab (browser convention). The mousedown guard
91
+ // cancels Chromium's autoscroll on the scrollable strip — without it the
92
+ // auxclick event is never generated.
93
+ function onTabMiddleDown(ev: MouseEvent) {
94
+ if (ev.button === 1) ev.preventDefault();
95
+ }
96
+ function onAux(id: string, ev: MouseEvent) {
97
+ if (ev.button !== 1) return;
98
+ ev.preventDefault();
99
+ ev.stopPropagation();
100
+ emit('close', id);
101
+ }
102
+ </script>
103
+
104
+ <template>
105
+ <div class="fe-tabs" role="tablist" :aria-label="t('tabs.strip')">
106
+ <div class="fe-tabs__scroll">
107
+ <div
108
+ v-for="(tab, i) in tabs"
109
+ :key="tab.id"
110
+ class="fe-tabs__tab"
111
+ :class="{
112
+ 'is-active': tab.id === activeId,
113
+ 'is-dragover': overIndex === i && dragIndex !== null && dragIndex !== i,
114
+ }"
115
+ role="tab"
116
+ :aria-selected="tab.id === activeId ? 'true' : 'false'"
117
+ tabindex="0"
118
+ draggable="true"
119
+ :title="tab.label"
120
+ @click="emit('select', tab.id)"
121
+ @keydown.enter.prevent="emit('select', tab.id)"
122
+ @mousedown="onTabMiddleDown($event)"
123
+ @auxclick="onAux(tab.id, $event)"
124
+ @dragstart="onDragStart(i, $event)"
125
+ @dragover="onDragOver(i, $event)"
126
+ @dragleave="onDragLeave(i)"
127
+ @drop="onDrop(i, $event)"
128
+ @dragend="onDragEnd"
129
+ >
130
+ <span v-if="tab.split" class="fe-tabs__splitdot" aria-hidden="true"></span>
131
+ <span class="fe-tabs__label">{{ tab.label }}</span>
132
+ <button
133
+ type="button"
134
+ class="fe-tabs__close"
135
+ :aria-label="t('tabs.close')"
136
+ :title="t('tabs.close')"
137
+ @click.stop="emit('close', tab.id)"
138
+ >×</button>
139
+ </div>
140
+ <button
141
+ type="button"
142
+ class="fe-tabs__new"
143
+ :aria-label="t('tabs.new')"
144
+ :title="t('tabs.new')"
145
+ @click="emit('new')"
146
+ >+</button>
147
+ </div>
148
+ <button
149
+ v-if="splitEnabled !== false"
150
+ type="button"
151
+ class="fe-tabs__split"
152
+ :class="{ 'is-active': splitActive }"
153
+ :aria-label="splitActive ? t('tabs.split_off') : t('tabs.split')"
154
+ :title="splitActive ? t('tabs.split_off') : t('tabs.split')"
155
+ :aria-pressed="splitActive ? 'true' : 'false'"
156
+ @click="emit('toggle-split')"
157
+ >
158
+ <svg
159
+ class="fe-ficon"
160
+ viewBox="0 0 24 24"
161
+ fill="none"
162
+ stroke="currentColor"
163
+ stroke-width="1.8"
164
+ stroke-linecap="round"
165
+ stroke-linejoin="round"
166
+ aria-hidden="true"
167
+ focusable="false"
168
+ >
169
+ <rect x="3" y="4" width="18" height="16" rx="2" />
170
+ <path d="M12 4v16" />
171
+ </svg>
172
+ </button>
173
+ </div>
174
+ </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>
@@ -86,6 +86,8 @@ const emit = defineEmits<{
86
86
  (e: 'open-recents'): void;
87
87
  (e: 'update:density', v: Density): void;
88
88
  (e: 'toggle-inspector'): void /* koru:k1 */;
89
+ (e: 'open-theme'): void /* wiring:c1 — tema galerisi */;
90
+ (e: 'open-shortcut-settings'): void /* wiring:c2 */;
89
91
  }>();
90
92
 
91
93
  // Density toggle — the toolbar owns the persisted preference; the parent
@@ -219,13 +221,18 @@ const moreActions = computed<ContextAction[]>(() => {
219
221
  : t('toolbar.density.compact'),
220
222
  icon: '⇅',
221
223
  });
222
- list.push(
223
- props.viewMode === 'list'
224
- ? { key: 'view-grid', label: t('toolbar.view.grid'), icon: '' }
225
- : { key: 'view-list', label: t('toolbar.view.list'), icon: '' },
226
- );
224
+ /* wiring:d2 — dar mod ⋯ menüsü: aktif olmayan İKİ görünüm de listelenir
225
+ (list/grid/gallery); eski tekli toggle üç modda eksik kalıyordu. */
226
+ if (props.viewMode !== 'list') list.push({ key: 'view-list', label: t('toolbar.view.list'), icon: '' });
227
+ if (props.viewMode !== 'grid') list.push({ key: 'view-grid', label: t('toolbar.view.grid'), icon: '' });
228
+ if (props.viewMode !== 'gallery') list.push({ key: 'view-gallery', label: t('toolbar.view.gallery'), icon: '▣' });
229
+ /* /wiring:d2 */
227
230
  /* koru:k1 — inspector toggle also reachable from the narrow overflow menu */
228
231
  list.push({ key: 'inspector', label: t('toolbar.inspector'), icon: 'ℹ' });
232
+ /* wiring:c1 — tema galerisi de dar modda ⋯ menüsünden açılır */
233
+ list.push({ key: 'theme', label: t('theme.menu'), icon: '🎨' });
234
+ list.push({ key: 'shortcut-settings', label: t('shortcuts.settings.menu'), icon: '⌨' }) /* wiring:c2 */;
235
+ list.push({ key: 'tour', label: t('tour.restart'), icon: '🎓' }); /* wiring:c4 */
229
236
  return list;
230
237
  });
231
238
 
@@ -251,9 +258,21 @@ function onMoreSelect(a: ContextAction) {
251
258
  case 'view-grid':
252
259
  emit('update:viewMode', 'grid');
253
260
  break;
261
+ case 'view-gallery' /* wiring:d2 */:
262
+ emit('update:viewMode', 'gallery');
263
+ break;
254
264
  case 'inspector' /* koru:k1 */:
255
265
  emit('toggle-inspector');
256
266
  break;
267
+ case 'theme' /* wiring:c1 */:
268
+ emit('open-theme');
269
+ break;
270
+ case 'shortcut-settings' /* wiring:c2 */:
271
+ emit('open-shortcut-settings');
272
+ break;
273
+ case 'tour' /* wiring:c4 — bubbles to the FileExplorer root listener */:
274
+ moreBtnEl.value?.dispatchEvent(new CustomEvent('fe:tour-restart', { bubbles: true }));
275
+ break;
257
276
  default:
258
277
  fire(a.key);
259
278
  }
@@ -331,7 +350,7 @@ function onMoreSelect(a: ContextAction) {
331
350
  class="fe-search__input"
332
351
  :placeholder="t('toolbar.search.placeholder')"
333
352
  :value="localSearch"
334
- aria-label="Search"
353
+ :aria-label="t('toolbar.search') /* wiring:c4 — was hardcoded EN */"
335
354
  @input="onSearchInput"
336
355
  />
337
356
  </div>
@@ -340,17 +359,19 @@ function onMoreSelect(a: ContextAction) {
340
359
  type="button"
341
360
  class="fe-btn fe-btn--icon-only"
342
361
  :title="t('toolbar.upload')"
362
+ :aria-label="t('toolbar.upload') /* wiring:c4 */"
343
363
  @click="emit('upload')"
344
364
  >
345
- <span class="fe-icon">⬆</span>
365
+ <span class="fe-icon" aria-hidden="true">⬆</span>
346
366
  </button>
347
367
  <button
348
368
  type="button"
349
369
  class="fe-btn fe-btn--icon-only"
350
370
  :title="t('toolbar.refresh')"
371
+ :aria-label="t('toolbar.refresh') /* wiring:c4 */"
351
372
  @click="emit('refresh')"
352
373
  >
353
- <span class="fe-icon">↻</span>
374
+ <span class="fe-icon" aria-hidden="true">↻</span>
354
375
  </button>
355
376
  </div>
356
377
 
@@ -418,7 +439,34 @@ function onMoreSelect(a: ContextAction) {
418
439
  </svg>
419
440
  </button>
420
441
 
421
- <div class="fe-toolbar__view" role="tablist" aria-label="View">
442
+ <!-- wiring:c1 tema galerisi (palet ikonu) -->
443
+ <button
444
+ type="button"
445
+ class="fe-btn fe-btn--icon-only fe-toolbar__theme"
446
+ :title="t('theme.menu')"
447
+ :aria-label="t('theme.menu')"
448
+ @click="emit('open-theme')"
449
+ >
450
+ <svg
451
+ class="fe-ficon"
452
+ viewBox="0 0 24 24"
453
+ fill="none"
454
+ stroke="currentColor"
455
+ stroke-width="1.8"
456
+ stroke-linecap="round"
457
+ stroke-linejoin="round"
458
+ aria-hidden="true"
459
+ focusable="false"
460
+ >
461
+ <path d="M12 3a9 9 0 1 0 0 18h1.4a2.1 2.1 0 0 0 1.5-3.6 2.1 2.1 0 0 1 1.5-3.6H19a2 2 0 0 0 2-2A9 9 0 0 0 12 3z" />
462
+ <circle cx="7.8" cy="10.2" r="1.15" fill="currentColor" stroke="none" />
463
+ <circle cx="11.6" cy="7.2" r="1.15" fill="currentColor" stroke="none" />
464
+ <circle cx="16" cy="8.6" r="1.15" fill="currentColor" stroke="none" />
465
+ </svg>
466
+ </button>
467
+ <!-- /wiring:c1 -->
468
+
469
+ <div class="fe-toolbar__view" role="tablist" :aria-label="t('toolbar.view_label') /* wiring:c4 — was hardcoded EN */">
422
470
  <button
423
471
  type="button"
424
472
  class="fe-btn fe-btn--icon-only"
@@ -426,9 +474,10 @@ function onMoreSelect(a: ContextAction) {
426
474
  role="tab"
427
475
  :aria-selected="viewMode === 'list'"
428
476
  :title="t('toolbar.view.list')"
477
+ :aria-label="t('toolbar.view.list') /* wiring:c4 */"
429
478
  @click="emit('update:viewMode', 'list')"
430
479
  >
431
- <span class="fe-icon">☰</span>
480
+ <span class="fe-icon" aria-hidden="true">☰</span>
432
481
  </button>
433
482
  <button
434
483
  type="button"
@@ -437,10 +486,25 @@ function onMoreSelect(a: ContextAction) {
437
486
  role="tab"
438
487
  :aria-selected="viewMode === 'grid'"
439
488
  :title="t('toolbar.view.grid')"
489
+ :aria-label="t('toolbar.view.grid') /* wiring:c4 */"
440
490
  @click="emit('update:viewMode', 'grid')"
441
491
  >
442
- <span class="fe-icon">▦</span>
492
+ <span class="fe-icon" aria-hidden="true">▦</span>
493
+ </button>
494
+ <!-- wiring:d2 — üçüncü görünüm: galeri -->
495
+ <button
496
+ type="button"
497
+ class="fe-btn fe-btn--icon-only"
498
+ :class="{ 'is-active': viewMode === 'gallery' }"
499
+ role="tab"
500
+ :aria-selected="viewMode === 'gallery'"
501
+ :title="t('toolbar.view.gallery')"
502
+ :aria-label="t('toolbar.view.gallery')"
503
+ @click="emit('update:viewMode', 'gallery')"
504
+ >
505
+ <span class="fe-icon" aria-hidden="true">▣</span>
443
506
  </button>
507
+ <!-- /wiring:d2 -->
444
508
  </div>
445
509
  </template>
446
510