@brftech/filex-core 0.2.0 → 0.3.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 (34) hide show
  1. package/dist/{ArchiveViewer-DCZ1FZYV.js → ArchiveViewer-CYrKPEbE.js} +2 -2
  2. package/dist/{ArchiveViewer-DCZ1FZYV.js.map → ArchiveViewer-CYrKPEbE.js.map} +1 -1
  3. package/dist/{CsvViewer-DMvnp82x.js → CsvViewer-C3ZU4Uua.js} +2 -2
  4. package/dist/{CsvViewer-DMvnp82x.js.map → CsvViewer-C3ZU4Uua.js.map} +1 -1
  5. package/dist/{DrawioViewer-B5Io0yTO.js → DrawioViewer-15ZL4ken.js} +2 -2
  6. package/dist/{DrawioViewer-B5Io0yTO.js.map → DrawioViewer-15ZL4ken.js.map} +1 -1
  7. package/dist/{EpubViewer-z6RZd3K8.js → EpubViewer-CrXcDgmo.js} +2 -2
  8. package/dist/{EpubViewer-z6RZd3K8.js.map → EpubViewer-CrXcDgmo.js.map} +1 -1
  9. package/dist/{IpynbViewer-DfL4EauH.js → IpynbViewer-BAb9rV2Q.js} +2 -2
  10. package/dist/{IpynbViewer-DfL4EauH.js.map → IpynbViewer-BAb9rV2Q.js.map} +1 -1
  11. package/dist/{MermaidViewer-QNl_o7V-.js → MermaidViewer-BN8GN4FY.js} +2 -2
  12. package/dist/{MermaidViewer-QNl_o7V-.js.map → MermaidViewer-BN8GN4FY.js.map} +1 -1
  13. package/dist/{PsdViewer-Btmpr0Fz.js → PsdViewer-o-kWQExw.js} +2 -2
  14. package/dist/{PsdViewer-Btmpr0Fz.js.map → PsdViewer-o-kWQExw.js.map} +1 -1
  15. package/dist/{TiffViewer-CMLpwds5.js → TiffViewer-AjMVBzxp.js} +2 -2
  16. package/dist/{TiffViewer-CMLpwds5.js.map → TiffViewer-AjMVBzxp.js.map} +1 -1
  17. package/dist/{Viewer3D-gf3cb2gv.js → Viewer3D-BR3WDbxQ.js} +2 -2
  18. package/dist/{Viewer3D-gf3cb2gv.js.map → Viewer3D-BR3WDbxQ.js.map} +1 -1
  19. package/dist/filex-core.js +1 -1
  20. package/dist/filex-core.umd.cjs +38 -38
  21. package/dist/filex-core.umd.cjs.map +1 -1
  22. package/dist/index-BG5upYlH.js +7392 -0
  23. package/dist/index-BG5upYlH.js.map +1 -0
  24. package/dist/index.d.ts +49 -2
  25. package/dist/style.css +1 -1
  26. package/package.json +1 -1
  27. package/src/FileExplorer.vue +62 -0
  28. package/src/components/ContextMenu.vue +101 -3
  29. package/src/components/Toolbar.vue +216 -5
  30. package/src/locales/en.ts +6 -0
  31. package/src/locales/tr.ts +6 -0
  32. package/src/styles/base.css +137 -0
  33. package/dist/index-D49iw00E.js +0 -7119
  34. package/dist/index-D49iw00E.js.map +0 -1
@@ -31,13 +31,22 @@ const props = defineProps<{
31
31
  * explorer tree.
32
32
  */
33
33
  theme?: ThemeMode;
34
+ /**
35
+ * bag:b4 — bottom-sheet presentation for coarse-pointer (touch)
36
+ * contexts. When true the menu renders as a full-width sheet sliding
37
+ * up from the bottom edge (grab handle, ≥44px touch targets; closed
38
+ * by overlay tap, dragging the handle down, or Esc). The position
39
+ * passed to show() is ignored. When absent/false the classic anchored
40
+ * menu renders — desktop right-click behavior is untouched.
41
+ */
42
+ sheet?: boolean;
34
43
  }>();
35
44
 
36
45
  const emit = defineEmits<{
37
46
  (e: 'select', action: ContextAction, target: FileNode[]): void;
38
47
  }>();
39
48
 
40
- void useLocale(() => props.locale); // eager-instantiate t/lookup so future templates can reuse
49
+ const { t } = useLocale(() => props.locale); // bag:b4 sheet aria labels need t()
41
50
 
42
51
  const open = ref(false);
43
52
  const x = ref(0);
@@ -54,6 +63,12 @@ async function show(ev: { clientX: number; clientY: number }, nodes: FileNode[])
54
63
  // bottom/right edge doesn't push the menu off-screen (it opens down-right
55
64
  // from the cursor by default).
56
65
  await nextTick();
66
+ if (props.sheet) {
67
+ // bag:b4 — sheet mode: no anchoring; focus the panel so Esc closes it
68
+ // without requiring a prior click inside.
69
+ sheetEl.value?.focus();
70
+ return;
71
+ }
57
72
  clampToViewport();
58
73
  }
59
74
 
@@ -74,7 +89,48 @@ function clampToViewport() {
74
89
 
75
90
  function hide() {
76
91
  open.value = false;
92
+ /* bag:b4 — reset any in-flight sheet drag so reopening starts clean. */
93
+ sheetDragging.value = false;
94
+ sheetDragY.value = 0;
95
+ }
96
+
97
+ /* bag:b4 — sheet drag-to-dismiss. Bound to the grab handle only, so the
98
+ * scrollable item list keeps its native touch scrolling. Dragging past the
99
+ * threshold closes; anything less snaps back. */
100
+ const sheetEl = ref<HTMLElement | null>(null);
101
+ const sheetDragging = ref(false);
102
+ const sheetDragY = ref(0);
103
+ let sheetDragStart = 0;
104
+
105
+ function onSheetDragStart(e: TouchEvent) {
106
+ const t0 = e.touches[0];
107
+ if (!t0) return;
108
+ sheetDragging.value = true;
109
+ sheetDragStart = t0.clientY;
110
+ sheetDragY.value = 0;
111
+ }
112
+ function onSheetDragMove(e: TouchEvent) {
113
+ if (!sheetDragging.value) return;
114
+ const t0 = e.touches[0];
115
+ if (!t0) return;
116
+ sheetDragY.value = Math.max(0, t0.clientY - sheetDragStart);
77
117
  }
118
+ function onSheetDragEnd() {
119
+ if (!sheetDragging.value) return;
120
+ const shouldClose = sheetDragY.value > 72;
121
+ sheetDragging.value = false;
122
+ if (shouldClose) {
123
+ hide();
124
+ } else {
125
+ sheetDragY.value = 0;
126
+ }
127
+ }
128
+ const sheetStyle = computed(() =>
129
+ sheetDragY.value > 0
130
+ ? { transform: `translateY(${sheetDragY.value}px)`, transition: 'none' }
131
+ : undefined,
132
+ );
133
+ /* /bag:b4 */
78
134
 
79
135
  function pick(a: ContextAction) {
80
136
  if (a.disabled) return;
@@ -113,17 +169,59 @@ defineExpose({ show, hide });
113
169
 
114
170
  <template>
115
171
  <Teleport to="body">
116
- <transition name="fe-ctx">
172
+ <transition :name="sheet ? 'fe-sheet' : 'fe-ctx'">
117
173
  <div
118
174
  v-if="open"
119
175
  class="fe-ctx-backdrop"
120
- :class="themeClass"
176
+ :class="[themeClass, { 'fe-ctx-backdrop--sheet': sheet }]"
121
177
  :data-prefers-dark="prefersDark ? '1' : '0'"
122
178
  @click="hide"
123
179
  @contextmenu.prevent="hide"
124
180
  @keydown="onKey"
125
181
  >
182
+ <!-- bag:b4 — bottom-sheet variant (coarse pointer / touch) -->
183
+ <div
184
+ v-if="sheet"
185
+ ref="sheetEl"
186
+ class="fe-sheet"
187
+ role="menu"
188
+ :aria-label="t('sheet.menu')"
189
+ tabindex="-1"
190
+ :style="sheetStyle"
191
+ @click.stop
192
+ >
193
+ <div
194
+ class="fe-sheet__handle"
195
+ role="button"
196
+ tabindex="0"
197
+ :aria-label="t('sheet.close')"
198
+ @click="hide"
199
+ @keydown.enter.prevent="hide"
200
+ @touchstart.passive="onSheetDragStart"
201
+ @touchmove.passive="onSheetDragMove"
202
+ @touchend="onSheetDragEnd"
203
+ @touchcancel="onSheetDragEnd"
204
+ />
205
+ <div class="fe-sheet__items">
206
+ <template v-for="(a, i) in visibleActions" :key="a.key || i">
207
+ <div v-if="a.divider" class="fe-ctx__sep" />
208
+ <button
209
+ v-else
210
+ type="button"
211
+ class="fe-ctx__item fe-sheet__item"
212
+ :class="{ 'is-danger': a.danger, 'is-disabled': a.disabled }"
213
+ :disabled="a.disabled"
214
+ role="menuitem"
215
+ @click="pick(a)"
216
+ >
217
+ <span v-if="a.icon" class="fe-ctx__icon" aria-hidden="true">{{ a.icon }}</span>
218
+ <span class="fe-ctx__label">{{ a.label }}</span>
219
+ </button>
220
+ </template>
221
+ </div>
222
+ </div>
126
223
  <div
224
+ v-else
127
225
  ref="menuEl"
128
226
  class="fe-ctx"
129
227
  role="menu"
@@ -11,10 +11,10 @@
11
11
  * Presentational only — all logic (rename, share, …) lives in
12
12
  * FileExplorer.vue, which listens for `action` emits.
13
13
  */
14
- import { computed, onMounted, ref, watch } from 'vue';
14
+ import { computed, nextTick, onBeforeUnmount, onMounted, ref, watch } from 'vue';
15
15
  import type { ViewMode } from '../types/FileNode';
16
- import type { LocaleCode } from '../types/ExplorerConfig';
17
- import type { ContextAction } from './ContextMenu.vue';
16
+ import type { LocaleCode, ThemeMode } from '../types/ExplorerConfig';
17
+ import ContextMenu, { type ContextAction } from './ContextMenu.vue';
18
18
  import { useLocale } from '../composables/useLocale';
19
19
 
20
20
  export type SelectionMode = 'none' | 'single-file' | 'single-dir' | 'multi';
@@ -58,6 +58,19 @@ const props = defineProps<{
58
58
  */
59
59
  canWrite?: boolean;
60
60
  locale: LocaleCode;
61
+ /**
62
+ * bag:b4 — narrow/embed mini mode. When true the toolbar collapses to
63
+ * [↑?] [🔍] [⬆] [⋯]: secondary actions (New Folder, Refresh, density,
64
+ * view switcher, selection actions, Paste) move into the "⋯" overflow
65
+ * menu and search expands from an icon to a full-width input. When
66
+ * absent/false the classic wide layout renders unchanged.
67
+ */
68
+ narrow?: boolean;
69
+ /**
70
+ * bag:b4 — resolved theme, forwarded to the teleported overflow menu
71
+ * (which loses the `.fe` variable scope under <body>).
72
+ */
73
+ theme?: ThemeMode;
61
74
  }>();
62
75
 
63
76
  const emit = defineEmits<{
@@ -114,7 +127,12 @@ function onSearchInput(ev: Event) {
114
127
  debounce = setTimeout(() => emit('update:searchQuery', v), 200);
115
128
  }
116
129
 
117
- function focusSearch() {
130
+ async function focusSearch() {
131
+ /* bag:b4 — in narrow mode the input is collapsed behind the icon. */
132
+ if (props.narrow && !searchOpen.value) {
133
+ searchOpen.value = true;
134
+ await nextTick();
135
+ }
118
136
  searchEl.value?.focus();
119
137
  searchEl.value?.select();
120
138
  }
@@ -132,10 +150,120 @@ const toolbarItems = computed(() => props.actions.filter((a) => !a.divider && !a
132
150
  function fire(key: string) {
133
151
  emit('action', key);
134
152
  }
153
+
154
+ /* === bag:b4 — narrow-mode state: expandable search + "⋯" overflow menu === */
155
+
156
+ const searchOpen = ref(false);
157
+ // Leaving narrow mode discards the transient expanded-search state so the
158
+ // wide layout always comes back exactly as it was.
159
+ watch(
160
+ () => props.narrow,
161
+ (n) => {
162
+ if (!n) searchOpen.value = false;
163
+ },
164
+ );
165
+
166
+ async function openSearch() {
167
+ searchOpen.value = true;
168
+ await nextTick();
169
+ searchEl.value?.focus();
170
+ }
171
+ function closeSearch() {
172
+ // Keep the query (it still filters the listing); the icon shows an
173
+ // active state while a query is set.
174
+ searchOpen.value = false;
175
+ }
176
+
177
+ // Coarse-pointer detection — the overflow menu renders as a bottom sheet on
178
+ // touch devices, matching the file context menu.
179
+ const coarse = ref(false);
180
+ let coarseMq: MediaQueryList | undefined;
181
+ function syncCoarse(e?: MediaQueryListEvent | MediaQueryList) {
182
+ coarse.value = !!(e && 'matches' in e && e.matches);
183
+ }
184
+ onMounted(() => {
185
+ if (typeof window === 'undefined' || !window.matchMedia) return;
186
+ coarseMq = window.matchMedia('(pointer: coarse)');
187
+ syncCoarse(coarseMq);
188
+ coarseMq.addEventListener?.('change', syncCoarse);
189
+ });
190
+ onBeforeUnmount(() => {
191
+ coarseMq?.removeEventListener?.('change', syncCoarse);
192
+ });
193
+
194
+ const moreBtnEl = ref<HTMLElement | null>(null);
195
+ const moreRef = ref<InstanceType<typeof ContextMenu> | null>(null);
196
+
197
+ // Everything the wide toolbar renders as standalone buttons, folded into one
198
+ // action list: folder-level writes (New Folder / Paste), the shared
199
+ // selection actions, then the view utilities (Refresh / density / view mode).
200
+ const moreActions = computed<ContextAction[]>(() => {
201
+ const list: ContextAction[] = [];
202
+ const writable =
203
+ !props.trashActive && !props.atVirtualRoot && props.canWrite !== false;
204
+ if (mode.value === 'none' && writable) {
205
+ list.push({ key: 'new-folder', label: t('toolbar.new_folder'), icon: '📁' });
206
+ if (props.pasteEnabled) list.push({ key: 'paste', label: t('ctx.paste'), icon: '📋' });
207
+ }
208
+ list.push(...toolbarItems.value);
209
+ if (list.length) list.push({ divider: true, key: 'bag-sep', label: '' });
210
+ list.push({ key: 'refresh', label: t('toolbar.refresh'), icon: '↻' });
211
+ list.push({
212
+ key: 'density',
213
+ label:
214
+ density.value === 'compact'
215
+ ? t('toolbar.density.comfortable')
216
+ : t('toolbar.density.compact'),
217
+ icon: '⇅',
218
+ });
219
+ list.push(
220
+ props.viewMode === 'list'
221
+ ? { key: 'view-grid', label: t('toolbar.view.grid'), icon: '▦' }
222
+ : { key: 'view-list', label: t('toolbar.view.list'), icon: '☰' },
223
+ );
224
+ return list;
225
+ });
226
+
227
+ function openMore() {
228
+ const r = moreBtnEl.value?.getBoundingClientRect();
229
+ moreRef.value?.show({ clientX: r ? r.right : 0, clientY: r ? r.bottom + 4 : 0 }, []);
230
+ }
231
+
232
+ function onMoreSelect(a: ContextAction) {
233
+ switch (a.key) {
234
+ case 'new-folder':
235
+ emit('new-folder');
236
+ break;
237
+ case 'refresh':
238
+ emit('refresh');
239
+ break;
240
+ case 'density':
241
+ toggleDensity();
242
+ break;
243
+ case 'view-list':
244
+ emit('update:viewMode', 'list');
245
+ break;
246
+ case 'view-grid':
247
+ emit('update:viewMode', 'grid');
248
+ break;
249
+ default:
250
+ fire(a.key);
251
+ }
252
+ }
253
+
254
+ /* === /bag:b4 === */
135
255
  </script>
136
256
 
137
257
  <template>
138
- <div class="fe-toolbar">
258
+ <div
259
+ class="fe-toolbar"
260
+ :class="{
261
+ 'fe-toolbar--narrow': narrow /* bag:b4 */,
262
+ 'fe-toolbar--searching': narrow && searchOpen /* bag:b4 */,
263
+ }"
264
+ >
265
+ <!-- bag:b4 — wide layout, untouched; renders exactly as before when not narrow -->
266
+ <template v-if="!narrow">
139
267
  <div class="fe-toolbar__primary">
140
268
  <button
141
269
  v-if="canGoUp"
@@ -280,5 +408,88 @@ function fire(key: string) {
280
408
  <span class="fe-icon">▦</span>
281
409
  </button>
282
410
  </div>
411
+ </template>
412
+
413
+ <!-- bag:b4 — narrow layout: [↑?] ... [🔍] [⬆] [⋯], search expands full-width -->
414
+ <template v-else>
415
+ <template v-if="searchOpen">
416
+ <div class="fe-search fe-search--full">
417
+ <input
418
+ ref="searchEl"
419
+ type="search"
420
+ class="fe-search__input"
421
+ :placeholder="t('toolbar.search.placeholder')"
422
+ :value="localSearch"
423
+ :aria-label="t('toolbar.search')"
424
+ @input="onSearchInput"
425
+ @keydown.esc.stop.prevent="closeSearch"
426
+ />
427
+ </div>
428
+ <button
429
+ type="button"
430
+ class="fe-btn fe-btn--icon-only"
431
+ :title="t('toolbar.search.close')"
432
+ :aria-label="t('toolbar.search.close')"
433
+ @click="closeSearch"
434
+ >
435
+ <span class="fe-icon">✕</span>
436
+ </button>
437
+ </template>
438
+ <template v-else>
439
+ <button
440
+ v-if="canGoUp"
441
+ type="button"
442
+ class="fe-btn fe-btn--icon-only"
443
+ :title="t('toolbar.go_up')"
444
+ :aria-label="t('toolbar.go_up')"
445
+ @click="emit('go-up')"
446
+ >
447
+ <span class="fe-icon">↑</span>
448
+ </button>
449
+
450
+ <div class="fe-toolbar__spacer" />
451
+
452
+ <button
453
+ type="button"
454
+ class="fe-btn fe-btn--icon-only fe-toolbar__search-toggle"
455
+ :class="{ 'is-active': !!localSearch }"
456
+ :title="t('toolbar.search')"
457
+ :aria-label="t('toolbar.search')"
458
+ @click="openSearch"
459
+ >
460
+ <span class="fe-icon">🔍</span>
461
+ </button>
462
+ <button
463
+ v-if="!atVirtualRoot && canWrite !== false"
464
+ type="button"
465
+ class="fe-btn fe-btn--icon-only"
466
+ :title="t('toolbar.upload')"
467
+ :aria-label="t('toolbar.upload')"
468
+ @click="emit('upload')"
469
+ >
470
+ <span class="fe-icon">⬆</span>
471
+ </button>
472
+ <button
473
+ ref="moreBtnEl"
474
+ type="button"
475
+ class="fe-btn fe-btn--icon-only fe-toolbar__more"
476
+ :title="t('toolbar.more')"
477
+ :aria-label="t('toolbar.more')"
478
+ aria-haspopup="menu"
479
+ @click="openMore"
480
+ >
481
+ <span class="fe-icon">⋯</span>
482
+ </button>
483
+ </template>
484
+
485
+ <ContextMenu
486
+ ref="moreRef"
487
+ :locale="locale"
488
+ :theme="theme || 'auto'"
489
+ :sheet="coarse"
490
+ :actions="moreActions"
491
+ @select="onMoreSelect"
492
+ />
493
+ </template>
283
494
  </div>
284
495
  </template>
package/src/locales/en.ts CHANGED
@@ -195,4 +195,10 @@ export const en: Record<string, string> = {
195
195
  'palette.save': 'Save search',
196
196
  'palette.saved.delete': 'Delete saved search',
197
197
  'search.in_content': 'In content',
198
+
199
+ /* === bag:b4 === */
200
+ 'toolbar.more': 'More actions',
201
+ 'toolbar.search.close': 'Close search',
202
+ 'sheet.menu': 'Actions',
203
+ 'sheet.close': 'Close',
198
204
  };
package/src/locales/tr.ts CHANGED
@@ -195,4 +195,10 @@ export const tr: Record<string, string> = {
195
195
  'palette.save': 'Aramayı kaydet',
196
196
  'palette.saved.delete': 'Kayıtlı aramayı sil',
197
197
  'search.in_content': 'İçerikte',
198
+
199
+ /* === bag:b4 === */
200
+ 'toolbar.more': 'Diğer işlemler',
201
+ 'toolbar.search.close': 'Aramayı kapat',
202
+ 'sheet.menu': 'İşlemler',
203
+ 'sheet.close': 'Kapat',
198
204
  };
@@ -1931,3 +1931,140 @@ filex-explorer {
1931
1931
  .fe-list__snippet {
1932
1932
  max-width: 100%;
1933
1933
  }
1934
+
1935
+ /* === bag:b4 — narrow/embed mini mode === */
1936
+ /* Everything below is scoped to `.fe--narrow`, `.fe-toolbar--narrow`,
1937
+ * `.fe-fab` or `.fe-sheet` — none of which exist in the wide desktop
1938
+ * layout, so the classic look is untouched when the mode is off. */
1939
+
1940
+ /* Narrow toolbar: a single non-wrapping row of icon buttons. */
1941
+ .fe-toolbar--narrow {
1942
+ flex-wrap: nowrap;
1943
+ gap: 6px;
1944
+ }
1945
+ .fe-toolbar--narrow .fe-btn.is-active {
1946
+ background: var(--fe-bg-selected);
1947
+ }
1948
+ /* Expanded search: the input takes the whole row. */
1949
+ .fe-toolbar--narrow .fe-search--full {
1950
+ flex: 1 1 auto;
1951
+ min-width: 0;
1952
+ }
1953
+ .fe-toolbar--narrow .fe-search--full .fe-search__input {
1954
+ width: 100%;
1955
+ min-width: 0;
1956
+ }
1957
+
1958
+ /* Upload FAB — bottom-right corner of the explorer (narrow mode only). */
1959
+ .fe-fab {
1960
+ position: absolute;
1961
+ right: 16px;
1962
+ bottom: 16px;
1963
+ width: 56px;
1964
+ height: 56px;
1965
+ border: 0;
1966
+ border-radius: 50%;
1967
+ background: var(--fe-primary);
1968
+ color: var(--fe-text-on-primary);
1969
+ font-size: 24px;
1970
+ display: flex;
1971
+ align-items: center;
1972
+ justify-content: center;
1973
+ box-shadow: var(--fe-shadow);
1974
+ cursor: pointer;
1975
+ z-index: 45;
1976
+ transition: background 0.15s, transform 0.15s;
1977
+ }
1978
+ .fe-fab:hover {
1979
+ background: var(--fe-primary-hover);
1980
+ }
1981
+ .fe-fab:active {
1982
+ transform: scale(0.94);
1983
+ }
1984
+ .fe-fab:focus-visible {
1985
+ outline: 2px solid var(--fe-primary);
1986
+ outline-offset: 3px;
1987
+ }
1988
+ /* Keep the corner trays clear of the FAB while narrow. */
1989
+ .fe--narrow .fe-upload,
1990
+ .fe--narrow .fe-ops {
1991
+ bottom: 84px;
1992
+ }
1993
+
1994
+ /* Bottom sheet (ContextMenu sheet variant, coarse-pointer devices). */
1995
+ .fe-ctx-backdrop--sheet {
1996
+ background: rgba(13, 19, 27, 0.42);
1997
+ }
1998
+ .fe-sheet {
1999
+ position: fixed;
2000
+ left: 0;
2001
+ right: 0;
2002
+ bottom: 0;
2003
+ max-height: 70vh;
2004
+ display: flex;
2005
+ flex-direction: column;
2006
+ background: var(--fe-bg);
2007
+ color: var(--fe-text);
2008
+ border: 1px solid var(--fe-border);
2009
+ border-bottom: 0;
2010
+ border-top-left-radius: var(--fe-radius-lg);
2011
+ border-top-right-radius: var(--fe-radius-lg);
2012
+ box-shadow: 0 -10px 32px rgba(15, 23, 42, 0.28);
2013
+ padding: 0 8px calc(8px + env(safe-area-inset-bottom, 0px));
2014
+ outline: none;
2015
+ z-index: 90;
2016
+ }
2017
+ .fe-sheet__handle {
2018
+ flex: 0 0 auto;
2019
+ display: flex;
2020
+ align-items: center;
2021
+ justify-content: center;
2022
+ padding: 12px 0 8px;
2023
+ cursor: grab;
2024
+ touch-action: none;
2025
+ }
2026
+ .fe-sheet__handle::before {
2027
+ content: '';
2028
+ width: 40px;
2029
+ height: 4px;
2030
+ border-radius: 99px;
2031
+ background: var(--fe-border-strong);
2032
+ }
2033
+ .fe-sheet__items {
2034
+ overflow-y: auto;
2035
+ overscroll-behavior: contain;
2036
+ -webkit-overflow-scrolling: touch;
2037
+ }
2038
+ /* Big touch targets (≥44px) on sheet entries. */
2039
+ .fe-sheet__item {
2040
+ min-height: 44px;
2041
+ padding: 10px 14px;
2042
+ font-size: 15px;
2043
+ gap: 12px;
2044
+ }
2045
+ /* Sheet transitions: backdrop fades, panel slides up from the bottom. */
2046
+ .fe-sheet-enter-active,
2047
+ .fe-sheet-leave-active {
2048
+ transition: opacity 0.18s ease;
2049
+ }
2050
+ .fe-sheet-enter-active .fe-sheet,
2051
+ .fe-sheet-leave-active .fe-sheet {
2052
+ transition: transform 0.22s ease;
2053
+ }
2054
+ .fe-sheet-enter-from,
2055
+ .fe-sheet-leave-to {
2056
+ opacity: 0;
2057
+ }
2058
+ .fe-sheet-enter-from .fe-sheet,
2059
+ .fe-sheet-leave-to .fe-sheet {
2060
+ transform: translateY(100%);
2061
+ }
2062
+ @media (prefers-reduced-motion: reduce) {
2063
+ .fe-sheet-enter-active,
2064
+ .fe-sheet-leave-active,
2065
+ .fe-sheet-enter-active .fe-sheet,
2066
+ .fe-sheet-leave-active .fe-sheet,
2067
+ .fe-fab {
2068
+ transition: none;
2069
+ }
2070
+ }