@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.
Files changed (125) hide show
  1. package/README.md +32 -25
  2. package/dist/ArchiveViewer-BNqukFg8.js +124 -0
  3. package/dist/ArchiveViewer-BNqukFg8.js.map +1 -0
  4. package/dist/CsvViewer-ii_-MgmG.js +141 -0
  5. package/dist/CsvViewer-ii_-MgmG.js.map +1 -0
  6. package/dist/{DrawioViewer-BNALOB04.js → DrawioViewer-B2tuu4rX.js} +48 -42
  7. package/dist/DrawioViewer-B2tuu4rX.js.map +1 -0
  8. package/dist/EpubViewer-p4B8iiWb.js +163 -0
  9. package/dist/EpubViewer-p4B8iiWb.js.map +1 -0
  10. package/dist/IpynbViewer-D_qYRJhu.js +184 -0
  11. package/dist/IpynbViewer-D_qYRJhu.js.map +1 -0
  12. package/dist/MermaidViewer-3ZkfNM8v.js +137 -0
  13. package/dist/MermaidViewer-3ZkfNM8v.js.map +1 -0
  14. package/dist/PsdViewer-CuV-OPSd.js +122 -0
  15. package/dist/PsdViewer-CuV-OPSd.js.map +1 -0
  16. package/dist/TiffViewer-BLeoF4Be.js +142 -0
  17. package/dist/TiffViewer-BLeoF4Be.js.map +1 -0
  18. package/dist/Viewer3D-DHhhU76E.js +75 -0
  19. package/dist/Viewer3D-DHhhU76E.js.map +1 -0
  20. package/dist/filex-core.js +217 -20077
  21. package/dist/filex-core.js.map +1 -1
  22. package/dist/filex-core.umd.cjs +61 -62
  23. package/dist/filex-core.umd.cjs.map +1 -1
  24. package/dist/index-BraG7Cz4.js +26664 -0
  25. package/dist/index-BraG7Cz4.js.map +1 -0
  26. package/dist/index.d.ts +4276 -851
  27. package/dist/style.css +1 -1
  28. package/package.json +1 -1
  29. package/src/FileExplorer.vue +2170 -492
  30. package/src/components/AdvancedSearch.vue +870 -0
  31. package/src/components/Breadcrumb.vue +191 -19
  32. package/src/components/CommandPalette.vue +66 -22
  33. package/src/components/ConnectionsPanel.vue +6 -2
  34. package/src/components/ContextMenu.vue +57 -2
  35. package/src/components/EncryptedFolderModal.vue +31 -25
  36. package/src/components/FilePane.vue +939 -0
  37. package/src/components/FilterBar.vue +568 -72
  38. package/src/components/GalleryView.vue +162 -30
  39. package/src/components/GridView.vue +322 -58
  40. package/src/components/HomeView.vue +331 -0
  41. package/src/components/InspectorPanel.vue +296 -53
  42. package/src/components/ListView.vue +1158 -167
  43. package/src/components/NFSExportsPanel.vue +7 -4
  44. package/src/components/OnboardingTour.vue +156 -33
  45. package/src/components/QuickLook.vue +34 -5
  46. package/src/components/RecentlyOpened.vue +17 -2
  47. package/src/components/S3KeysPanel.vue +7 -4
  48. package/src/components/SSHKeysPanel.vue +7 -4
  49. package/src/components/ShortcutSettings.vue +43 -2
  50. package/src/components/ShortcutsHelp.vue +4 -1
  51. package/src/components/SideNav.vue +206 -140
  52. package/src/components/StorageFields.vue +19 -1
  53. package/src/components/TabBar.vue +87 -1
  54. package/src/components/TagPicker.vue +77 -6
  55. package/src/components/ThemeGallery.vue +13 -198
  56. package/src/components/ThemePalette.vue +244 -0
  57. package/src/components/TimeZoneDialog.vue +127 -0
  58. package/src/components/TimeZonePicker.vue +696 -0
  59. package/src/components/TokensPanel.vue +16 -4
  60. package/src/components/Toolbar.vue +965 -469
  61. package/src/components/ViewSwitcher.vue +9 -4
  62. package/src/composables/useConnections.ts +35 -4
  63. package/src/composables/useExplorerTimeZone.ts +85 -0
  64. package/src/composables/useFileApi.ts +53 -4
  65. package/src/composables/useKeyboardShortcuts.ts +122 -0
  66. package/src/composables/useLocale.ts +365 -25
  67. package/src/index.ts +182 -3
  68. package/src/lib/actionIcons.ts +330 -0
  69. package/src/lib/advSearch.ts +162 -0
  70. package/src/lib/dateGroups.ts +197 -0
  71. package/src/lib/destinationTree.ts +235 -0
  72. package/src/lib/downloadSelection.ts +154 -0
  73. package/src/lib/fileFilters.ts +319 -12
  74. package/src/lib/fileIcons.ts +299 -5
  75. package/src/lib/filePreview.ts +605 -0
  76. package/src/lib/listing.ts +72 -0
  77. package/src/lib/shareTtl.ts +11 -1
  78. package/src/lib/sortOrder.ts +459 -0
  79. package/src/lib/themes.ts +152 -23
  80. package/src/lib/timezone.ts +456 -0
  81. package/src/lib/uiProfile.ts +82 -0
  82. package/src/lib/viewPrefs.ts +1036 -0
  83. package/src/locales/en.ts +432 -25
  84. package/src/locales/tr.ts +425 -25
  85. package/src/modals/ConvertModal.vue +23 -3
  86. package/src/modals/DestinationPickerModal.vue +341 -0
  87. package/src/modals/Modal.vue +18 -4
  88. package/src/modals/NewDocumentModal.vue +525 -0
  89. package/src/modals/NewFolderModal.vue +15 -9
  90. package/src/modals/PermissionsModal.vue +543 -473
  91. package/src/modals/PreviewModal.vue +516 -156
  92. package/src/styles/base.css +5435 -8
  93. package/src/styles/variables.css +140 -60
  94. package/src/types/Connections.ts +9 -0
  95. package/src/types/ExplorerConfig.ts +144 -39
  96. package/src/types/FileNode.ts +28 -0
  97. package/src/viewers/ArchiveViewer.vue +46 -8
  98. package/src/viewers/CsvViewer.vue +19 -2
  99. package/src/viewers/DrawioViewer.vue +12 -1
  100. package/src/viewers/EpubViewer.vue +50 -6
  101. package/src/viewers/IpynbViewer.vue +19 -2
  102. package/src/viewers/MermaidViewer.vue +20 -3
  103. package/src/viewers/PdfViewer.vue +24 -4
  104. package/src/viewers/PsdViewer.vue +28 -4
  105. package/src/viewers/TiffViewer.vue +19 -2
  106. package/src/viewers/Viewer3D.vue +36 -2
  107. package/dist/ArchiveViewer-Cdfv9LFC.js +0 -99
  108. package/dist/ArchiveViewer-Cdfv9LFC.js.map +0 -1
  109. package/dist/CsvViewer-CqWeV8VO.js +0 -131
  110. package/dist/CsvViewer-CqWeV8VO.js.map +0 -1
  111. package/dist/DrawioViewer-BNALOB04.js.map +0 -1
  112. package/dist/EpubViewer-BPmlImG7.js +0 -145
  113. package/dist/EpubViewer-BPmlImG7.js.map +0 -1
  114. package/dist/IpynbViewer-CDlQmuCV.js +0 -175
  115. package/dist/IpynbViewer-CDlQmuCV.js.map +0 -1
  116. package/dist/MermaidViewer-sfZWdR-d.js +0 -127
  117. package/dist/MermaidViewer-sfZWdR-d.js.map +0 -1
  118. package/dist/PsdViewer-DwuySZsb.js +0 -112
  119. package/dist/PsdViewer-DwuySZsb.js.map +0 -1
  120. package/dist/TiffViewer-BDQ3d3CQ.js +0 -132
  121. package/dist/TiffViewer-BDQ3d3CQ.js.map +0 -1
  122. package/dist/Viewer3D-CJJJgkuv.js +0 -60
  123. package/dist/Viewer3D-CJJJgkuv.js.map +0 -1
  124. package/src/components/SecondaryPane.vue +0 -447
  125. 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
- {{ tag }}
119
- <button class="filex-tag-x" type="button" @click="remove(tag)" aria-label="Remove tag">×</button>
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="tag name"
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">+ Add tag</button>
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, #6366f1);
195
- color: var(--filex-accent, #6366f1);
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 { THEMES, DEFAULT_THEME_ID, type ThemeDef, type ThemeModePref } from '../lib/themes';
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
- <p class="fe-themes__hint">{{ t('theme.hint') }}</p>
112
- <div class="fe-themes" role="listbox" :aria-label="t('theme.title')">
113
- <button
114
- v-for="th in THEMES"
115
- :key="th.id"
116
- type="button"
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>